detached 3e6f9e9

fix(wayland): implement viewport source crop and synchronized subsurfaces (#13)

* fix(wayland): apply viewport-only size changes on commit

* fix(viewport): apply source crops to content and sampling

* fix(wayland): queue synchronized subsurface updates with parent commits

* test(wayland): cover cached viewport and desync update drain

* test(wayland): cover queued child teardown and unpublished buffer retirement

* test(wayland): remove unused compositor binding

* test(wayland): verify desync preserves captured parent dependencies

* test(wayland): cover parent destruction with captured child update

* test(wayland): cover dropped synchronized explicit-sync child

* test(wayland): verify cached child buffer lifetime on role destruction

* fix(vulkan): preserve source crops across SDR and HDR composition

* fix(wayland): retain queued buffers and discard superseded feedback

* fix(viewport): skip out_of_buffer checks for null-attach commits

* fix(wayland): keep the retained crop when a queued buffer is destroyed

* fix(render): cache tiled surface content by stable identity

* test(wayland): guard queued viewport and buffer scale commits

* test(wayland): cover queued buffer and viewport replacement

* test(render): verify tiled playback copies only changing surface

* test(wayland): guard changed tiled snapshot allocations

* test(vulkan): give cached SHM fixtures distinct content versions

* fix(viewport): tolerate 1/256 rounding at the source edge and log every rejection

* fix(wayland): ignore position after subsurface parent destruction

* fix(wayland): publish only drawn commit graphs

* fix(vulkan): clamp YUV sampling to source crop

* test(wayland): describe callback and copy budget guard

* test(wayland): assert rounded viewport crop stays in buffer

* test(vulkan): exercise stable root identity in tiled scene budget

* test(wayland): keep callback copy guard in perf checks
commit
3e6f9e9abfd2dcda37b004e58eb35850c4493e84
author
bnema · · 2026-09-27 14:15 UTC
committer
GitHub · · 2026-09-27 14:15 UTC
parent
0843224
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
31 files · +2127 -212
M Makefile +5 -2
@@ -1,4 +1,4 @@
1-.PHONY: build test vet race mocks mocks-check spv-check fakes-check arch check bin tty logs pkg
1+.PHONY: build test vet race mocks mocks-check spv-check fakes-check arch check perf-check bin tty logs pkg
22
33 # 0 runs until quit; set e.g. TTY_TIMEOUT=60s for a safety net.
44 TTY_TIMEOUT ?= 0
@@ -48,7 +48,10 @@ fakes-check:
4848 [ $$rc -eq 1 ] || { [ $$rc -eq 0 ] && echo 'handwritten test double: add an interface and a Mockery entry (see AGENTS.md)' >&2; exit 1; }
4949 arch:
5050 $(HOME)/go/bin/hexcheck -hexcheck.config .hexcheck.yaml -hexcheck.root . ./...
51-check: vet test arch fakes-check
51+# Fast, mandatory guards for tiled content publications and SHM copies.
52+perf-check:
53+ CGO_ENABLED=0 go test ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget)$$' -count=1
54+check: vet test arch fakes-check perf-check
5255
5356 # Arch package of the committed HEAD (packaging/arch/PKGBUILD). Go modules
5457 # come from the module proxy in prepare(); the build itself runs offline.
@@ -1,4 +1,4 @@
1-.PHONY: build test vet race mocks mocks-check spv-check fakes-check arch check bin tty logs pkg1+.PHONY: build test vet race mocks mocks-check spv-check fakes-check arch check perf-check bin tty logs pkg
2 2
3 # 0 runs until quit; set e.g. TTY_TIMEOUT=60s for a safety net.3 # 0 runs until quit; set e.g. TTY_TIMEOUT=60s for a safety net.
4 TTY_TIMEOUT ?= 04 TTY_TIMEOUT ?= 0
@@ -48,7 +48,10 @@ fakes-check:
48 [ $$rc -eq 1 ] || { [ $$rc -eq 0 ] && echo 'handwritten test double: add an interface and a Mockery entry (see AGENTS.md)' >&2; exit 1; }48 [ $$rc -eq 1 ] || { [ $$rc -eq 0 ] && echo 'handwritten test double: add an interface and a Mockery entry (see AGENTS.md)' >&2; exit 1; }
49 arch:49 arch:
50 $(HOME)/go/bin/hexcheck -hexcheck.config .hexcheck.yaml -hexcheck.root . ./...50 $(HOME)/go/bin/hexcheck -hexcheck.config .hexcheck.yaml -hexcheck.root . ./...
51-check: vet test arch fakes-check51+# Fast, mandatory guards for tiled content publications and SHM copies.
52+perf-check:
53+ CGO_ENABLED=0 go test ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget)$$' -count=1
54+check: vet test arch fakes-check perf-check
52 55
53 # Arch package of the committed HEAD (packaging/arch/PKGBUILD). Go modules56 # Arch package of the committed HEAD (packaging/arch/PKGBUILD). Go modules
54 # come from the module proxy in prepare(); the build itself runs offline.57 # come from the module proxy in prepare(); the build itself runs offline.
M internal/adapters/vulkan/compose.go +9 -4
@@ -52,6 +52,7 @@ type pushConstants struct {
5252 rect [4]int32
5353 mapv [4]float32
5454 color [4]float32
55+ crop [4]float32
5556 buf [4]uint32
5657 misc [4]uint32
5758 }
@@ -298,16 +299,20 @@ func (r *Renderer) fillDraw(rect image.Rectangle, c [3]uint8) draw {
298299
299300 // contentDraw draws the visible rect of a w×h buffer mapped onto full
300301 // (target pixels). exact: the buffer is drawn at its size.
301-func (r *Renderer) contentDraw(rect, full image.Rectangle, w, h int, mode uint32, opaque bool) draw {
302+func (r *Renderer) contentDraw(rect, full image.Rectangle, w, h int, source [4]float32, mode uint32, opaque bool) draw {
302303 var dr draw
303- sx, sy := float32(w)/float32(full.Dx()), float32(h)/float32(full.Dy())
304+ if source[2] <= 0 {
305+ source = [4]float32{0, 0, float32(w), float32(h)}
306+ }
307+ sx, sy := source[2]/float32(full.Dx()), source[3]/float32(full.Dy())
304308 dr.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)}
305- dr.pc.mapv = [4]float32{-float32(full.Min.X) * sx, -float32(full.Min.Y) * sy, sx, sy}
309+ dr.pc.mapv = [4]float32{source[0] - float32(full.Min.X)*sx, source[1] - float32(full.Min.Y)*sy, sx, sy}
310+ dr.pc.crop = [4]float32{source[0], source[1], source[0] + source[2], source[1] + source[3]}
306311 flags := uint32(0)
307312 if opaque {
308313 flags |= flagOpaque
309314 }
310- if full.Dx() == w && full.Dy() == h {
315+ if full.Dx() == int(source[2]) && full.Dy() == int(source[3]) && source[2] == float32(int(source[2])) && source[3] == float32(int(source[3])) {
311316 flags |= flagExact
312317 }
313318 dr.pc.misc = [4]uint32{mode, flags, uint32(r.width), uint32(r.height)}
@@ -52,6 +52,7 @@ type pushConstants struct {
52 rect [4]int3252 rect [4]int32
53 mapv [4]float3253 mapv [4]float32
54 color [4]float3254 color [4]float32
55+ crop [4]float32
55 buf [4]uint3256 buf [4]uint32
56 misc [4]uint3257 misc [4]uint32
57 }58 }
@@ -298,16 +299,20 @@ func (r *Renderer) fillDraw(rect image.Rectangle, c [3]uint8) draw {
298 299
299 // contentDraw draws the visible rect of a w×h buffer mapped onto full300 // contentDraw draws the visible rect of a w×h buffer mapped onto full
300 // (target pixels). exact: the buffer is drawn at its size.301 // (target pixels). exact: the buffer is drawn at its size.
301-func (r *Renderer) contentDraw(rect, full image.Rectangle, w, h int, mode uint32, opaque bool) draw {302+func (r *Renderer) contentDraw(rect, full image.Rectangle, w, h int, source [4]float32, mode uint32, opaque bool) draw {
302 var dr draw303 var dr draw
303- sx, sy := float32(w)/float32(full.Dx()), float32(h)/float32(full.Dy())304+ if source[2] <= 0 {
305+ source = [4]float32{0, 0, float32(w), float32(h)}
306+ }
307+ sx, sy := source[2]/float32(full.Dx()), source[3]/float32(full.Dy())
304 dr.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)}308 dr.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)}
305- dr.pc.mapv = [4]float32{-float32(full.Min.X) * sx, -float32(full.Min.Y) * sy, sx, sy}309+ dr.pc.mapv = [4]float32{source[0] - float32(full.Min.X)*sx, source[1] - float32(full.Min.Y)*sy, sx, sy}
310+ dr.pc.crop = [4]float32{source[0], source[1], source[0] + source[2], source[1] + source[3]}
306 flags := uint32(0)311 flags := uint32(0)
307 if opaque {312 if opaque {
308 flags |= flagOpaque313 flags |= flagOpaque
309 }314 }
310- if full.Dx() == w && full.Dy() == h {315+ if full.Dx() == int(source[2]) && full.Dy() == int(source[3]) && source[2] == float32(int(source[2])) && source[3] == float32(int(source[3])) {
311 flags |= flagExact316 flags |= flagExact
312 }317 }
313 dr.pc.misc = [4]uint32{mode, flags, uint32(r.width), uint32(r.height)}318 dr.pc.misc = [4]uint32{mode, flags, uint32(r.width), uint32(r.height)}
M internal/adapters/vulkan/compose_test.go +30 -4
@@ -43,6 +43,30 @@ func TestRendererScaledSHMFiltersLinearly(t *testing.T) {
4343 }
4444 }
4545
46+func TestRendererSourceCrop(t *testing.T) {
47+ r, err := New(16, 8)
48+ if err != nil {
49+ t.Skipf("Vulkan unavailable: %v", err)
50+ }
51+ defer r.Close()
52+ px := []byte{0, 0, 255, 255, 0, 255, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255}
53+ c := shmContent(t, 4, 1, 16, px)
54+ c.LogicalW, c.LogicalH = 16, 8
55+ c.Source = [4]float32{1, 0, 2, 1}
56+ scene := ports.Scene{Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 8}}}}
57+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {
58+ t.Fatal(err)
59+ }
60+ for _, tc := range []struct {
61+ x int
62+ want color.RGBA
63+ }{{0, color.RGBA{0, 255, 0, 255}}, {15, color.RGBA{0, 0, 255, 255}}} {
64+ if got := r.Pixels().RGBAAt(tc.x, 4); !near(got, tc.want, 5) {
65+ t.Errorf("crop x=%d: %v want %v", tc.x, got, tc.want)
66+ }
67+ }
68+}
69+
4670 // Scaled dmabufs are sampled with the linear sampler.
4771 func TestRendererScaledDMABufFiltersLinearly(t *testing.T) {
4872 r, err := New(128, 4)
@@ -107,7 +131,7 @@ func TestRendererSHMCopiedOncePerContent(t *testing.T) {
107131 }
108132 defer r.Close()
109133 c := solidContent(t, 16, 16, color.RGBA{1, 2, 3, 255})
110- c.ID, c.Seq = 1, 1
134+ c.ID, c.Seq, c.Version = 1, 1, 1
111135 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
112136 contents := map[ports.WindowID]ports.SurfaceContent{1: c}
113137 render := func() {
@@ -130,7 +154,7 @@ func TestRendererSHMCopiedOncePerContent(t *testing.T) {
130154 if r.copied != 16*16*4 {
131155 t.Fatalf("unchanged content copied again: %d", r.copied)
132156 }
133- c.Seq = 2
157+ c.Seq, c.Version = 2, 2
134158 contents[1] = c
135159 render()
136160 if r.copied != 2*16*16*4 {
@@ -148,8 +172,9 @@ func TestRendererSkipsOccludedSurface(t *testing.T) {
148172 defer r.Close()
149173 green := color.RGBA{0, 255, 0, 255}
150174 root := solidContent(t, 16, 16, color.RGBA{255, 0, 0, 255})
151- root.ID, root.Seq = 1, 1
175+ root.ID, root.Seq, root.Surface, root.Version = 1, 1, 1, 1
152176 game := solidContent(t, 16, 16, green)
177+ game.Surface, game.Version = 2, 1
153178 game.Opaque = true
154179 root.Children = []ports.Subsurface{{SurfaceContent: game}}
155180 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
@@ -164,6 +189,7 @@ func TestRendererSkipsOccludedSurface(t *testing.T) {
164189 }
165190 // A smaller opaque child leaves part of the root visible: it is copied.
166191 small := solidContent(t, 8, 8, green)
192+ small.Surface, small.Version = 3, 1
167193 small.Opaque = true
168194 root.Seq = 2
169195 root.Children = []ports.Subsurface{{SurfaceContent: small}}
@@ -227,7 +253,7 @@ func TestRendererSHMDoubleBufferWaitsForReaders(t *testing.T) {
227253 var readers []uint64
228254 for seq := uint64(1); seq <= 3; seq++ {
229255 c := solidContent(t, 16, 16, color.RGBA{uint8(seq * 60), 0, 0, 255})
230- c.ID, c.Seq = 1, seq
256+ c.ID, c.Seq, c.Version = 1, seq, seq
231257 if seq == 3 {
232258 // The buffer about to be reused was last read by frame 1.
233259 if r.completed >= readers[0] {
@@ -43,6 +43,30 @@ func TestRendererScaledSHMFiltersLinearly(t *testing.T) {
43 }43 }
44 }44 }
45 45
46+func TestRendererSourceCrop(t *testing.T) {
47+ r, err := New(16, 8)
48+ if err != nil {
49+ t.Skipf("Vulkan unavailable: %v", err)
50+ }
51+ defer r.Close()
52+ px := []byte{0, 0, 255, 255, 0, 255, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255}
53+ c := shmContent(t, 4, 1, 16, px)
54+ c.LogicalW, c.LogicalH = 16, 8
55+ c.Source = [4]float32{1, 0, 2, 1}
56+ scene := ports.Scene{Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 8}}}}
57+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {
58+ t.Fatal(err)
59+ }
60+ for _, tc := range []struct {
61+ x int
62+ want color.RGBA
63+ }{{0, color.RGBA{0, 255, 0, 255}}, {15, color.RGBA{0, 0, 255, 255}}} {
64+ if got := r.Pixels().RGBAAt(tc.x, 4); !near(got, tc.want, 5) {
65+ t.Errorf("crop x=%d: %v want %v", tc.x, got, tc.want)
66+ }
67+ }
68+}
69+
46 // Scaled dmabufs are sampled with the linear sampler.70 // Scaled dmabufs are sampled with the linear sampler.
47 func TestRendererScaledDMABufFiltersLinearly(t *testing.T) {71 func TestRendererScaledDMABufFiltersLinearly(t *testing.T) {
48 r, err := New(128, 4)72 r, err := New(128, 4)
@@ -107,7 +131,7 @@ func TestRendererSHMCopiedOncePerContent(t *testing.T) {
107 }131 }
108 defer r.Close()132 defer r.Close()
109 c := solidContent(t, 16, 16, color.RGBA{1, 2, 3, 255})133 c := solidContent(t, 16, 16, color.RGBA{1, 2, 3, 255})
110- c.ID, c.Seq = 1, 1134+ c.ID, c.Seq, c.Version = 1, 1, 1
111 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}135 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
112 contents := map[ports.WindowID]ports.SurfaceContent{1: c}136 contents := map[ports.WindowID]ports.SurfaceContent{1: c}
113 render := func() {137 render := func() {
@@ -130,7 +154,7 @@ func TestRendererSHMCopiedOncePerContent(t *testing.T) {
130 if r.copied != 16*16*4 {154 if r.copied != 16*16*4 {
131 t.Fatalf("unchanged content copied again: %d", r.copied)155 t.Fatalf("unchanged content copied again: %d", r.copied)
132 }156 }
133- c.Seq = 2157+ c.Seq, c.Version = 2, 2
134 contents[1] = c158 contents[1] = c
135 render()159 render()
136 if r.copied != 2*16*16*4 {160 if r.copied != 2*16*16*4 {
@@ -148,8 +172,9 @@ func TestRendererSkipsOccludedSurface(t *testing.T) {
148 defer r.Close()172 defer r.Close()
149 green := color.RGBA{0, 255, 0, 255}173 green := color.RGBA{0, 255, 0, 255}
150 root := solidContent(t, 16, 16, color.RGBA{255, 0, 0, 255})174 root := solidContent(t, 16, 16, color.RGBA{255, 0, 0, 255})
151- root.ID, root.Seq = 1, 1175+ root.ID, root.Seq, root.Surface, root.Version = 1, 1, 1, 1
152 game := solidContent(t, 16, 16, green)176 game := solidContent(t, 16, 16, green)
177+ game.Surface, game.Version = 2, 1
153 game.Opaque = true178 game.Opaque = true
154 root.Children = []ports.Subsurface{{SurfaceContent: game}}179 root.Children = []ports.Subsurface{{SurfaceContent: game}}
155 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}180 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
@@ -164,6 +189,7 @@ func TestRendererSkipsOccludedSurface(t *testing.T) {
164 }189 }
165 // A smaller opaque child leaves part of the root visible: it is copied.190 // A smaller opaque child leaves part of the root visible: it is copied.
166 small := solidContent(t, 8, 8, green)191 small := solidContent(t, 8, 8, green)
192+ small.Surface, small.Version = 3, 1
167 small.Opaque = true193 small.Opaque = true
168 root.Seq = 2194 root.Seq = 2
169 root.Children = []ports.Subsurface{{SurfaceContent: small}}195 root.Children = []ports.Subsurface{{SurfaceContent: small}}
@@ -227,7 +253,7 @@ func TestRendererSHMDoubleBufferWaitsForReaders(t *testing.T) {
227 var readers []uint64253 var readers []uint64
228 for seq := uint64(1); seq <= 3; seq++ {254 for seq := uint64(1); seq <= 3; seq++ {
229 c := solidContent(t, 16, 16, color.RGBA{uint8(seq * 60), 0, 0, 255})255 c := solidContent(t, 16, 16, color.RGBA{uint8(seq * 60), 0, 0, 255})
230- c.ID, c.Seq = 1, seq256+ c.ID, c.Seq, c.Version = 1, seq, seq
231 if seq == 3 {257 if seq == 3 {
232 // The buffer about to be reused was last read by frame 1.258 // The buffer about to be reused was last read by frame 1.
233 if r.completed >= readers[0] {259 if r.completed >= readers[0] {
M internal/adapters/vulkan/renderer.go +4 -0
@@ -310,6 +310,10 @@ func (r *Renderer) findMemoryType(bits uint32, props vk.MemoryPropertyFlags) (ui
310310 return 0, fmt.Errorf("no Vulkan memory type for bits %#x and properties %#x", bits, props)
311311 }
312312
313+// CopiedBytes reports cumulative wl_shm bytes copied into GPU storage.
314+// It is diagnostic only and must be read by the renderer's owner goroutine.
315+func (r *Renderer) CopiedBytes() int { return r.copied }
316+
313317 func (r *Renderer) Pixels() *image.RGBA {
314318 out := image.NewRGBA(image.Rect(0, 0, r.width, r.height))
315319 if r.mapped == nil || r.readback() != nil {
@@ -310,6 +310,10 @@ func (r *Renderer) findMemoryType(bits uint32, props vk.MemoryPropertyFlags) (ui
310 return 0, fmt.Errorf("no Vulkan memory type for bits %#x and properties %#x", bits, props)310 return 0, fmt.Errorf("no Vulkan memory type for bits %#x and properties %#x", bits, props)
311 }311 }
312 312
313+// CopiedBytes reports cumulative wl_shm bytes copied into GPU storage.
314+// It is diagnostic only and must be read by the renderer's owner goroutine.
315+func (r *Renderer) CopiedBytes() int { return r.copied }
316+
313 func (r *Renderer) Pixels() *image.RGBA {317 func (r *Renderer) Pixels() *image.RGBA {
314 out := image.NewRGBA(image.Rect(0, 0, r.width, r.height))318 out := image.NewRGBA(image.Rect(0, 0, r.width, r.height))
315 if r.mapped == nil || r.readback() != nil {319 if r.mapped == nil || r.readback() != nil {
M internal/adapters/vulkan/renderer_test.go +7 -2
@@ -321,12 +321,17 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) {
321321 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 10, Y: 10, W: 20, H: 10}}}}
322322 // A 30×20 surface whose window is the 20×10 at (5, 5): a 5px shadow.
323323 root := solidContent(t, 30, 20, red)
324+ root.Surface, root.Version = 1, 1
324325 root.Geometry = ports.Rect{X: 5, Y: 5, W: 20, H: 10}
325326 // Green covers the window's top-left 4×4 corner; blue sits below the
326327 // root, hidden by it.
328+ below := solidContent(t, 30, 20, blue)
329+ below.Surface, below.Version = 2, 1
330+ above := solidContent(t, 4, 4, green)
331+ above.Surface, above.Version = 3, 1
327332 root.Children = []ports.Subsurface{
328- {X: 3, Y: 3, Below: true, SurfaceContent: solidContent(t, 30, 20, blue)},
329- {X: 5, Y: 5, SurfaceContent: solidContent(t, 4, 4, green)},
333+ {X: 3, Y: 3, Below: true, SurfaceContent: below},
334+ {X: 5, Y: 5, SurfaceContent: above},
330335 }
331336 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
332337 t.Fatal(err)
@@ -321,12 +321,17 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) {
321 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 10, Y: 10, W: 20, H: 10}}}}321 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 10, Y: 10, W: 20, H: 10}}}}
322 // A 30×20 surface whose window is the 20×10 at (5, 5): a 5px shadow.322 // A 30×20 surface whose window is the 20×10 at (5, 5): a 5px shadow.
323 root := solidContent(t, 30, 20, red)323 root := solidContent(t, 30, 20, red)
324+ root.Surface, root.Version = 1, 1
324 root.Geometry = ports.Rect{X: 5, Y: 5, W: 20, H: 10}325 root.Geometry = ports.Rect{X: 5, Y: 5, W: 20, H: 10}
325 // Green covers the window's top-left 4×4 corner; blue sits below the326 // Green covers the window's top-left 4×4 corner; blue sits below the
326 // root, hidden by it.327 // root, hidden by it.
328+ below := solidContent(t, 30, 20, blue)
329+ below.Surface, below.Version = 2, 1
330+ above := solidContent(t, 4, 4, green)
331+ above.Surface, above.Version = 3, 1
327 root.Children = []ports.Subsurface{332 root.Children = []ports.Subsurface{
328- {X: 3, Y: 3, Below: true, SurfaceContent: solidContent(t, 30, 20, blue)},333+ {X: 3, Y: 3, Below: true, SurfaceContent: below},
329- {X: 5, Y: 5, SurfaceContent: solidContent(t, 4, 4, green)},334+ {X: 5, Y: 5, SurfaceContent: above},
330 }335 }
331 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {336 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
332 t.Fatal(err)337 t.Fatal(err)
M internal/adapters/vulkan/scene.go +18 -15
@@ -136,21 +136,20 @@ func (w *sceneWalk) popups(overLayers bool) {
136136
137137 // place draws a surface tree with its window geometry at (x, y) logical,
138138 // clipped to width×height logical: client shadows fall outside. Every
139-// surface of the tree is keyed by its window and place in it; the tree's
140-// Seq changes with any of them.
139+// surface of the tree is keyed by its stable surface identity.
141140 func (w *sceneWalk) place(id ports.WindowID, content *ports.SurfaceContent, x, y, width, height int) {
142141 clip := image.Rect(x, y, x+width, y+height)
143142 ox, oy := x-content.Geometry.X, y-content.Geometry.Y
144143 covers := w.opaqueChildren(content, ox, oy, clip)
145144 for i := range content.Children {
146145 if ch := &content.Children[i]; ch.Below {
147- w.surface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq, covers)
146+ w.surface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, ch.Surface}, ch.Version, covers, false)
148147 }
149148 }
150- w.surface(content, ox, oy, clip, shmKey{id, 0}, content.Seq, covers)
149+ w.surface(content, ox, oy, clip, shmKey{id, content.Surface}, content.Version, covers, true)
151150 for i := range content.Children {
152151 if ch := &content.Children[i]; !ch.Below {
153- w.surface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq, nil)
152+ w.surface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, ch.Surface}, ch.Version, nil, false)
154153 }
155154 }
156155 }
@@ -209,8 +208,12 @@ func (w *sceneWalk) surfaceRects(content *ports.SurfaceContent, x, y int, clip i
209208 // A buffer drawn at the physical size (fractional-scale clients round
210209 // w*scale, we round per edge) is copied 1:1, never resampled for 1px.
211210 near := func(a, b int) bool { return a-b <= 1 && b-a <= 1 }
212- if near(full.Dx(), content.Width) && near(full.Dy(), content.Height) {
213- full.Max = full.Min.Add(image.Pt(content.Width, content.Height))
211+ sourceW, sourceH := content.Width, content.Height
212+ if content.Source[2] > 0 {
213+ sourceW, sourceH = int(content.Source[2]), int(content.Source[3])
214+ }
215+ if near(full.Dx(), sourceW) && near(full.Dy(), sourceH) {
216+ full.Max = full.Min.Add(image.Pt(sourceW, sourceH))
214217 }
215218 dst = full.Intersect(w.physRect(clip.Min.X, clip.Min.Y, clip.Dx(), clip.Dy()))
216219 return full, dst, !dst.Empty()
@@ -219,7 +222,7 @@ func (w *sceneWalk) surfaceRects(content *ports.SurfaceContent, x, y int, clip i
219222 // surface draws one surface buffer with its origin at (x, y) logical,
220223 // clipped to clip (logical), unless an opaque surface above it (covers,
221224 // physical) hides all of it: its buffer is then neither copied nor drawn.
222-func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y int, clip image.Rectangle, key shmKey, seq uint64, covers []image.Rectangle) {
225+func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y int, clip image.Rectangle, key shmKey, seq uint64, covers []image.Rectangle, root bool) {
223226 full, dst, ok := w.surfaceRects(content, x, y, clip)
224227 if !ok {
225228 return
@@ -229,15 +232,15 @@ func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y int, clip image.
229232 return
230233 }
231234 }
232- if key.index == 0 {
235+ if root {
233236 w.dmg.content(key.win, content, full, dst)
234237 }
235- w.content(dst, full, content, key, seq)
238+ w.content(dst, full, content, key, seq, root)
236239 }
237240
238241 // content draws the part of a buffer mapped onto full (physical pixels)
239242 // that dst shows. key names the surface, seq its content.
240-func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceContent, key shmKey, seq uint64) {
243+func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceContent, key shmKey, seq uint64, root bool) {
241244 r := w.r
242245 rect := dst.Intersect(w.bounds)
243246 if rect.Empty() {
@@ -249,7 +252,7 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon
249252 // The window shows its background until a buffer imports.
250253 return
251254 }
252- dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque)
255+ dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, modeImage, content.Opaque)
253256 r.setContentColor(&dr, content.Color)
254257 if im.yuv {
255258 dr.pc.misc[1] |= flagYUV
@@ -275,17 +278,17 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon
275278 if err != nil {
276279 return
277280 }
278- st := shmState{w: content.Width, h: content.Height, seq: seq}
281+ st := shmState{w: content.Width, h: content.Height, seq: seq, windowSeq: content.Seq}
279282 // Damage history is the root surface's: children copy in full.
280283 var damage func(uint64) ([]ports.Rect, bool)
281- if key.index == 0 {
284+ if root {
282285 damage = content.DamageSince
283286 }
284287 c, err := r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage)
285288 if err != nil {
286289 return
287290 }
288- dr := r.contentDraw(rect, full, content.Width, content.Height, modeBuffer, content.Opaque)
291+ dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, modeBuffer, content.Opaque)
289292 r.setContentColor(&dr, content.Color)
290293 dr.set = c.set
291294 dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0}
@@ -136,21 +136,20 @@ func (w *sceneWalk) popups(overLayers bool) {
136 136
137 // place draws a surface tree with its window geometry at (x, y) logical,137 // place draws a surface tree with its window geometry at (x, y) logical,
138 // clipped to width×height logical: client shadows fall outside. Every138 // clipped to width×height logical: client shadows fall outside. Every
139-// surface of the tree is keyed by its window and place in it; the tree's139+// surface of the tree is keyed by its stable surface identity.
140-// Seq changes with any of them.
141 func (w *sceneWalk) place(id ports.WindowID, content *ports.SurfaceContent, x, y, width, height int) {140 func (w *sceneWalk) place(id ports.WindowID, content *ports.SurfaceContent, x, y, width, height int) {
142 clip := image.Rect(x, y, x+width, y+height)141 clip := image.Rect(x, y, x+width, y+height)
143 ox, oy := x-content.Geometry.X, y-content.Geometry.Y142 ox, oy := x-content.Geometry.X, y-content.Geometry.Y
144 covers := w.opaqueChildren(content, ox, oy, clip)143 covers := w.opaqueChildren(content, ox, oy, clip)
145 for i := range content.Children {144 for i := range content.Children {
146 if ch := &content.Children[i]; ch.Below {145 if ch := &content.Children[i]; ch.Below {
147- w.surface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq, covers)146+ w.surface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, ch.Surface}, ch.Version, covers, false)
148 }147 }
149 }148 }
150- w.surface(content, ox, oy, clip, shmKey{id, 0}, content.Seq, covers)149+ w.surface(content, ox, oy, clip, shmKey{id, content.Surface}, content.Version, covers, true)
151 for i := range content.Children {150 for i := range content.Children {
152 if ch := &content.Children[i]; !ch.Below {151 if ch := &content.Children[i]; !ch.Below {
153- w.surface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq, nil)152+ w.surface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, ch.Surface}, ch.Version, nil, false)
154 }153 }
155 }154 }
156 }155 }
@@ -209,8 +208,12 @@ func (w *sceneWalk) surfaceRects(content *ports.SurfaceContent, x, y int, clip i
209 // A buffer drawn at the physical size (fractional-scale clients round208 // A buffer drawn at the physical size (fractional-scale clients round
210 // w*scale, we round per edge) is copied 1:1, never resampled for 1px.209 // w*scale, we round per edge) is copied 1:1, never resampled for 1px.
211 near := func(a, b int) bool { return a-b <= 1 && b-a <= 1 }210 near := func(a, b int) bool { return a-b <= 1 && b-a <= 1 }
212- if near(full.Dx(), content.Width) && near(full.Dy(), content.Height) {211+ sourceW, sourceH := content.Width, content.Height
213- full.Max = full.Min.Add(image.Pt(content.Width, content.Height))212+ if content.Source[2] > 0 {
213+ sourceW, sourceH = int(content.Source[2]), int(content.Source[3])
214+ }
215+ if near(full.Dx(), sourceW) && near(full.Dy(), sourceH) {
216+ full.Max = full.Min.Add(image.Pt(sourceW, sourceH))
214 }217 }
215 dst = full.Intersect(w.physRect(clip.Min.X, clip.Min.Y, clip.Dx(), clip.Dy()))218 dst = full.Intersect(w.physRect(clip.Min.X, clip.Min.Y, clip.Dx(), clip.Dy()))
216 return full, dst, !dst.Empty()219 return full, dst, !dst.Empty()
@@ -219,7 +222,7 @@ func (w *sceneWalk) surfaceRects(content *ports.SurfaceContent, x, y int, clip i
219 // surface draws one surface buffer with its origin at (x, y) logical,222 // surface draws one surface buffer with its origin at (x, y) logical,
220 // clipped to clip (logical), unless an opaque surface above it (covers,223 // clipped to clip (logical), unless an opaque surface above it (covers,
221 // physical) hides all of it: its buffer is then neither copied nor drawn.224 // physical) hides all of it: its buffer is then neither copied nor drawn.
222-func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y int, clip image.Rectangle, key shmKey, seq uint64, covers []image.Rectangle) {225+func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y int, clip image.Rectangle, key shmKey, seq uint64, covers []image.Rectangle, root bool) {
223 full, dst, ok := w.surfaceRects(content, x, y, clip)226 full, dst, ok := w.surfaceRects(content, x, y, clip)
224 if !ok {227 if !ok {
225 return228 return
@@ -229,15 +232,15 @@ func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y int, clip image.
229 return232 return
230 }233 }
231 }234 }
232- if key.index == 0 {235+ if root {
233 w.dmg.content(key.win, content, full, dst)236 w.dmg.content(key.win, content, full, dst)
234 }237 }
235- w.content(dst, full, content, key, seq)238+ w.content(dst, full, content, key, seq, root)
236 }239 }
237 240
238 // content draws the part of a buffer mapped onto full (physical pixels)241 // content draws the part of a buffer mapped onto full (physical pixels)
239 // that dst shows. key names the surface, seq its content.242 // that dst shows. key names the surface, seq its content.
240-func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceContent, key shmKey, seq uint64) {243+func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceContent, key shmKey, seq uint64, root bool) {
241 r := w.r244 r := w.r
242 rect := dst.Intersect(w.bounds)245 rect := dst.Intersect(w.bounds)
243 if rect.Empty() {246 if rect.Empty() {
@@ -249,7 +252,7 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon
249 // The window shows its background until a buffer imports.252 // The window shows its background until a buffer imports.
250 return253 return
251 }254 }
252- dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque)255+ dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, modeImage, content.Opaque)
253 r.setContentColor(&dr, content.Color)256 r.setContentColor(&dr, content.Color)
254 if im.yuv {257 if im.yuv {
255 dr.pc.misc[1] |= flagYUV258 dr.pc.misc[1] |= flagYUV
@@ -275,17 +278,17 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon
275 if err != nil {278 if err != nil {
276 return279 return
277 }280 }
278- st := shmState{w: content.Width, h: content.Height, seq: seq}281+ st := shmState{w: content.Width, h: content.Height, seq: seq, windowSeq: content.Seq}
279 // Damage history is the root surface's: children copy in full.282 // Damage history is the root surface's: children copy in full.
280 var damage func(uint64) ([]ports.Rect, bool)283 var damage func(uint64) ([]ports.Rect, bool)
281- if key.index == 0 {284+ if root {
282 damage = content.DamageSince285 damage = content.DamageSince
283 }286 }
284 c, err := r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage)287 c, err := r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage)
285 if err != nil {288 if err != nil {
286 return289 return
287 }290 }
288- dr := r.contentDraw(rect, full, content.Width, content.Height, modeBuffer, content.Opaque)291+ dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, modeBuffer, content.Opaque)
289 r.setContentColor(&dr, content.Color)292 r.setContentColor(&dr, content.Color)
290 dr.set = c.set293 dr.set = c.set
291 dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0}294 dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0}
A internal/adapters/vulkan/scene_perf_test.go +71 -0
@@ -0,0 +1,71 @@
1+package vulkan
2+
3+import (
4+ "image/color"
5+ "testing"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+// TestSceneWalkUnchangedTiledSHM guards the GPU copy budget of an unchanged
11+// subsurface tree. Each child has its own real wl_shm pool.
12+func TestSceneWalkUnchangedTiledSHM(t *testing.T) {
13+ const tiles = 64
14+ r, err := New(128, 128)
15+ if err != nil {
16+ t.Skipf("Vulkan unavailable: %v", err)
17+ }
18+ defer r.Close()
19+ root := ports.SurfaceContent{ID: 1, Seq: 1, Surface: 1, Version: 1}
20+ for i := range tiles {
21+ child := solidContent(t, 8, 8, color.RGBA{R: uint8(i + 1), A: 255})
22+ child.ID, child.Seq, child.Surface, child.Version = 1, 1, uint64(i+2), 1
23+ root.Children = append(root.Children, ports.Subsurface{X: i % 8 * 8, Y: i / 8 * 8, SurfaceContent: child})
24+ }
25+ video := solidContent(t, 8, 8, color.RGBA{G: 255, A: 255})
26+ video.ID, video.Seq, video.Surface, video.Version = 1, 1, 66, 1
27+ root.Children = append(root.Children, ports.Subsurface{X: 80, Y: 0, SurfaceContent: video})
28+ scene := ports.Scene{Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 128, H: 128}}}}
29+ contents := map[ports.WindowID]ports.SurfaceContent{1: root}
30+ if err := render(r, scene, contents); err != nil {
31+ t.Fatal(err)
32+ }
33+ for _, count := range []int{16, 64} {
34+ contents[1] = ports.SurfaceContent{ID: 1, Seq: root.Seq, Surface: root.Surface, Version: root.Version, Children: root.Children[:count]}
35+ walk := func() {
36+ dmg := newDamage(&target{}, scene, r.Pixels().Bounds())
37+ _ = r.draws(scene, contents, dmg)
38+ }
39+ walk()
40+ if allocs := testing.AllocsPerRun(20, walk); allocs > 16 {
41+ t.Errorf("%d tiles: %.1f allocations/walk, want <=16", count, allocs)
42+ } else {
43+ t.Logf("%d tiles: %.1f allocations/walk", count, allocs)
44+ }
45+ }
46+ contents[1] = root
47+ before := r.copied
48+ for i := range 100 {
49+ root.Seq++
50+ root.Children[tiles].Version++
51+ contents[1] = root
52+ if err := render(r, scene, contents); err != nil {
53+ t.Fatal(err)
54+ }
55+ if delta := r.copied - before; delta != 8*8*4 {
56+ t.Fatalf("publication %d copied %d bytes, want video only (%d)", i, delta, 8*8*4)
57+ }
58+ before = r.copied
59+ }
60+ // Reordering equal-sized children must not reuse another child's pixels.
61+ root.Children[0], root.Children[1] = root.Children[1], root.Children[0]
62+ root.Children[0].X, root.Children[1].X = 0, 8
63+ root.Seq++
64+ contents[1] = root
65+ if err := render(r, scene, contents); err != nil {
66+ t.Fatal(err)
67+ }
68+ if got := r.Pixels().RGBAAt(4, 4); got != (color.RGBA{R: 2, A: 255}) {
69+ t.Fatalf("restacked tile has pixels %v, want second child's colour", got)
70+ }
71+}
@@ -0,0 +1,71 @@
1+package vulkan
2+
3+import (
4+ "image/color"
5+ "testing"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+// TestSceneWalkUnchangedTiledSHM guards the GPU copy budget of an unchanged
11+// subsurface tree. Each child has its own real wl_shm pool.
12+func TestSceneWalkUnchangedTiledSHM(t *testing.T) {
13+ const tiles = 64
14+ r, err := New(128, 128)
15+ if err != nil {
16+ t.Skipf("Vulkan unavailable: %v", err)
17+ }
18+ defer r.Close()
19+ root := ports.SurfaceContent{ID: 1, Seq: 1, Surface: 1, Version: 1}
20+ for i := range tiles {
21+ child := solidContent(t, 8, 8, color.RGBA{R: uint8(i + 1), A: 255})
22+ child.ID, child.Seq, child.Surface, child.Version = 1, 1, uint64(i+2), 1
23+ root.Children = append(root.Children, ports.Subsurface{X: i % 8 * 8, Y: i / 8 * 8, SurfaceContent: child})
24+ }
25+ video := solidContent(t, 8, 8, color.RGBA{G: 255, A: 255})
26+ video.ID, video.Seq, video.Surface, video.Version = 1, 1, 66, 1
27+ root.Children = append(root.Children, ports.Subsurface{X: 80, Y: 0, SurfaceContent: video})
28+ scene := ports.Scene{Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 128, H: 128}}}}
29+ contents := map[ports.WindowID]ports.SurfaceContent{1: root}
30+ if err := render(r, scene, contents); err != nil {
31+ t.Fatal(err)
32+ }
33+ for _, count := range []int{16, 64} {
34+ contents[1] = ports.SurfaceContent{ID: 1, Seq: root.Seq, Surface: root.Surface, Version: root.Version, Children: root.Children[:count]}
35+ walk := func() {
36+ dmg := newDamage(&target{}, scene, r.Pixels().Bounds())
37+ _ = r.draws(scene, contents, dmg)
38+ }
39+ walk()
40+ if allocs := testing.AllocsPerRun(20, walk); allocs > 16 {
41+ t.Errorf("%d tiles: %.1f allocations/walk, want <=16", count, allocs)
42+ } else {
43+ t.Logf("%d tiles: %.1f allocations/walk", count, allocs)
44+ }
45+ }
46+ contents[1] = root
47+ before := r.copied
48+ for i := range 100 {
49+ root.Seq++
50+ root.Children[tiles].Version++
51+ contents[1] = root
52+ if err := render(r, scene, contents); err != nil {
53+ t.Fatal(err)
54+ }
55+ if delta := r.copied - before; delta != 8*8*4 {
56+ t.Fatalf("publication %d copied %d bytes, want video only (%d)", i, delta, 8*8*4)
57+ }
58+ before = r.copied
59+ }
60+ // Reordering equal-sized children must not reuse another child's pixels.
61+ root.Children[0], root.Children[1] = root.Children[1], root.Children[0]
62+ root.Children[0].X, root.Children[1].X = 0, 8
63+ root.Seq++
64+ contents[1] = root
65+ if err := render(r, scene, contents); err != nil {
66+ t.Fatal(err)
67+ }
68+ if got := r.Pixels().RGBAAt(4, 4); got != (color.RGBA{R: 2, A: 255}) {
69+ t.Fatalf("restacked tile has pixels %v, want second child's colour", got)
70+ }
71+}
M internal/adapters/vulkan/shaders/compose.frag +11 -5
@@ -9,6 +9,7 @@ layout(push_constant) uniform Draw {
99 ivec4 rect;
1010 vec4 map;
1111 vec4 color;
12+ vec4 crop;
1213 uvec4 buf;
1314 uvec4 misc;
1415 } d;
@@ -49,9 +50,13 @@ vec3 pqToSRGB(vec3 v, float whiteNits) {
4950 // vec2 coordinates are normalized to the chroma image for linear filtering.
5051 vec4 sampleYUV(vec2 src) {
5152 ivec2 size = textureSize(tex, 0);
52- vec2 lumaUV = src / vec2(size);
53- float y = texture(tex, lumaUV).r;
54- vec2 chromaUV = vec2((src.x + 0.5) / float(size.x), src.y / float(size.y));
53+ vec2 luma = clamp(src, d.crop.xy + 0.5, d.crop.zw - 0.5);
54+ float y = texture(tex, luma / vec2(size)).r;
55+ // Type 0 chroma is shifted half a luma pixel in x. Clamp in
56+ // half-resolution texel space before filtering the chroma plane.
57+ vec2 chroma = clamp((src + vec2(0.5, 0.0)) * 0.5,
58+ d.crop.xy * 0.5 + 0.5, d.crop.zw * 0.5 - 0.5);
59+ vec2 chromaUV = chroma * 2.0 / vec2(size);
5560 vec2 cbcr = texture(chromaTex, chromaUV).rg;
5661 bool tenBit = (d.misc.y & flagP010) != 0u;
5762 bool limited = ((d.buf.w >> 8) & 255u) == 2u;
@@ -73,7 +78,7 @@ vec4 sampleYUV(vec2 src) {
7378
7479 // texel reads buffer pixel p, clamped to the buffer (clamp to edge).
7580 vec4 texel(ivec2 p) {
76- p = clamp(p, ivec2(0), ivec2(d.buf.yz) - 1);
81+ p = clamp(p, ivec2(floor(d.crop.xy)), min(ivec2(d.buf.yz) - 1, ivec2(ceil(d.crop.zw)) - 1));
7782 // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A).
7883 return unpackUnorm4x8(pixels[d.buf.x + uint(p.y) * d.buf.y + uint(p.x)]).zyxw;
7984 }
@@ -88,7 +93,8 @@ void main() {
8893 c = sampleYUV(src);
8994 } else if (d.misc.x == modeImage) {
9095 ivec2 size = textureSize(tex, 0);
91- c = exact ? texelFetch(tex, clamp(ivec2(floor(src)), ivec2(0), size - 1), 0) : texture(tex, src / vec2(size));
96+ vec2 p = clamp(src, d.crop.xy + 0.5, d.crop.zw - 0.5);
97+ c = exact ? texelFetch(tex, clamp(ivec2(floor(p)), ivec2(0), size - 1), 0) : texture(tex, p / vec2(size));
9298 } else if (exact) {
9399 c = texel(ivec2(floor(src)));
94100 } else {
@@ -9,6 +9,7 @@ layout(push_constant) uniform Draw {
9 ivec4 rect;9 ivec4 rect;
10 vec4 map;10 vec4 map;
11 vec4 color;11 vec4 color;
12+ vec4 crop;
12 uvec4 buf;13 uvec4 buf;
13 uvec4 misc;14 uvec4 misc;
14 } d;15 } d;
@@ -49,9 +50,13 @@ vec3 pqToSRGB(vec3 v, float whiteNits) {
49 // vec2 coordinates are normalized to the chroma image for linear filtering.50 // vec2 coordinates are normalized to the chroma image for linear filtering.
50 vec4 sampleYUV(vec2 src) {51 vec4 sampleYUV(vec2 src) {
51 ivec2 size = textureSize(tex, 0);52 ivec2 size = textureSize(tex, 0);
52- vec2 lumaUV = src / vec2(size);53+ vec2 luma = clamp(src, d.crop.xy + 0.5, d.crop.zw - 0.5);
53- float y = texture(tex, lumaUV).r;54+ float y = texture(tex, luma / vec2(size)).r;
54- vec2 chromaUV = vec2((src.x + 0.5) / float(size.x), src.y / float(size.y));55+ // Type 0 chroma is shifted half a luma pixel in x. Clamp in
56+ // half-resolution texel space before filtering the chroma plane.
57+ vec2 chroma = clamp((src + vec2(0.5, 0.0)) * 0.5,
58+ d.crop.xy * 0.5 + 0.5, d.crop.zw * 0.5 - 0.5);
59+ vec2 chromaUV = chroma * 2.0 / vec2(size);
55 vec2 cbcr = texture(chromaTex, chromaUV).rg;60 vec2 cbcr = texture(chromaTex, chromaUV).rg;
56 bool tenBit = (d.misc.y & flagP010) != 0u;61 bool tenBit = (d.misc.y & flagP010) != 0u;
57 bool limited = ((d.buf.w >> 8) & 255u) == 2u;62 bool limited = ((d.buf.w >> 8) & 255u) == 2u;
@@ -73,7 +78,7 @@ vec4 sampleYUV(vec2 src) {
73 78
74 // texel reads buffer pixel p, clamped to the buffer (clamp to edge).79 // texel reads buffer pixel p, clamped to the buffer (clamp to edge).
75 vec4 texel(ivec2 p) {80 vec4 texel(ivec2 p) {
76- p = clamp(p, ivec2(0), ivec2(d.buf.yz) - 1);81+ p = clamp(p, ivec2(floor(d.crop.xy)), min(ivec2(d.buf.yz) - 1, ivec2(ceil(d.crop.zw)) - 1));
77 // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A).82 // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A).
78 return unpackUnorm4x8(pixels[d.buf.x + uint(p.y) * d.buf.y + uint(p.x)]).zyxw;83 return unpackUnorm4x8(pixels[d.buf.x + uint(p.y) * d.buf.y + uint(p.x)]).zyxw;
79 }84 }
@@ -88,7 +93,8 @@ void main() {
88 c = sampleYUV(src);93 c = sampleYUV(src);
89 } else if (d.misc.x == modeImage) {94 } else if (d.misc.x == modeImage) {
90 ivec2 size = textureSize(tex, 0);95 ivec2 size = textureSize(tex, 0);
91- c = exact ? texelFetch(tex, clamp(ivec2(floor(src)), ivec2(0), size - 1), 0) : texture(tex, src / vec2(size));96+ vec2 p = clamp(src, d.crop.xy + 0.5, d.crop.zw - 0.5);
97+ c = exact ? texelFetch(tex, clamp(ivec2(floor(p)), ivec2(0), size - 1), 0) : texture(tex, p / vec2(size));
92 } else if (exact) {98 } else if (exact) {
93 c = texel(ivec2(floor(src)));99 c = texel(ivec2(floor(src)));
94 } else {100 } else {
M internal/adapters/vulkan/shaders/compose.frag.spv +0 -0 binary file: no text diff

binary file: no text diff

M internal/adapters/vulkan/shaders/compose.vert +1 -0
@@ -5,6 +5,7 @@ layout(push_constant) uniform Draw {
55 ivec4 rect; // visible x0, y0, x1, y1 in target pixels
66 vec4 map; // source texel = map.xy + fragment centre * map.zw
77 vec4 color; // solid fills: premultiplied RGBA
8+ vec4 crop; // source crop bounds in buffer pixels
89 uvec4 buf; // buffer draws: first word, row width, height, unused
910 uvec4 misc; // mode, flags, target width, target height
1011 } d;
@@ -5,6 +5,7 @@ layout(push_constant) uniform Draw {
5 ivec4 rect; // visible x0, y0, x1, y1 in target pixels5 ivec4 rect; // visible x0, y0, x1, y1 in target pixels
6 vec4 map; // source texel = map.xy + fragment centre * map.zw6 vec4 map; // source texel = map.xy + fragment centre * map.zw
7 vec4 color; // solid fills: premultiplied RGBA7 vec4 color; // solid fills: premultiplied RGBA
8+ vec4 crop; // source crop bounds in buffer pixels
8 uvec4 buf; // buffer draws: first word, row width, height, unused9 uvec4 buf; // buffer draws: first word, row width, height, unused
9 uvec4 misc; // mode, flags, target width, target height10 uvec4 misc; // mode, flags, target width, target height
10 } d;11 } d;
M internal/adapters/vulkan/shaders/compose.vert.spv +0 -0 binary file: no text diff

binary file: no text diff

M internal/adapters/vulkan/shaders/compose_hdr.frag +11 -5
@@ -9,6 +9,7 @@ layout(push_constant) uniform Draw {
99 ivec4 rect;
1010 vec4 map;
1111 vec4 color;
12+ vec4 crop;
1213 uvec4 buf;
1314 uvec4 misc;
1415 } d;
@@ -48,9 +49,13 @@ vec3 pqToLinear709(vec3 v, float whiteNits) {
4849 // vec2 coordinates are normalized to the chroma image for linear filtering.
4950 vec4 sampleYUV(vec2 src) {
5051 ivec2 size = textureSize(tex, 0);
51- vec2 lumaUV = src / vec2(size);
52- float y = texture(tex, lumaUV).r;
53- vec2 chromaUV = vec2((src.x + 0.5) / float(size.x), src.y / float(size.y));
52+ vec2 luma = clamp(src, d.crop.xy + 0.5, d.crop.zw - 0.5);
53+ float y = texture(tex, luma / vec2(size)).r;
54+ // Type 0 chroma is shifted half a luma pixel in x. Clamp in
55+ // half-resolution texel space before filtering the chroma plane.
56+ vec2 chroma = clamp((src + vec2(0.5, 0.0)) * 0.5,
57+ d.crop.xy * 0.5 + 0.5, d.crop.zw * 0.5 - 0.5);
58+ vec2 chromaUV = chroma * 2.0 / vec2(size);
5459 vec2 cbcr = texture(chromaTex, chromaUV).rg;
5560 bool tenBit = (d.misc.y & flagP010) != 0u;
5661 bool limited = ((d.buf.w >> 8) & 255u) == 2u;
@@ -72,7 +77,7 @@ vec4 sampleYUV(vec2 src) {
7277
7378 // texel reads buffer pixel p, clamped to the buffer (clamp to edge).
7479 vec4 texel(ivec2 p) {
75- p = clamp(p, ivec2(0), ivec2(d.buf.yz) - 1);
80+ p = clamp(p, ivec2(floor(d.crop.xy)), min(ivec2(d.buf.yz) - 1, ivec2(ceil(d.crop.zw)) - 1));
7681 // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A).
7782 return unpackUnorm4x8(pixels[d.buf.x + uint(p.y) * d.buf.y + uint(p.x)]).zyxw;
7883 }
@@ -87,7 +92,8 @@ void main() {
8792 c = sampleYUV(src);
8893 } else if (d.misc.x == modeImage) {
8994 ivec2 size = textureSize(tex, 0);
90- c = exact ? texelFetch(tex, clamp(ivec2(floor(src)), ivec2(0), size - 1), 0) : texture(tex, src / vec2(size));
95+ vec2 p = clamp(src, d.crop.xy + 0.5, d.crop.zw - 0.5);
96+ c = exact ? texelFetch(tex, clamp(ivec2(floor(p)), ivec2(0), size - 1), 0) : texture(tex, p / vec2(size));
9197 } else if (exact) {
9298 c = texel(ivec2(floor(src)));
9399 } else {
@@ -9,6 +9,7 @@ layout(push_constant) uniform Draw {
9 ivec4 rect;9 ivec4 rect;
10 vec4 map;10 vec4 map;
11 vec4 color;11 vec4 color;
12+ vec4 crop;
12 uvec4 buf;13 uvec4 buf;
13 uvec4 misc;14 uvec4 misc;
14 } d;15 } d;
@@ -48,9 +49,13 @@ vec3 pqToLinear709(vec3 v, float whiteNits) {
48 // vec2 coordinates are normalized to the chroma image for linear filtering.49 // vec2 coordinates are normalized to the chroma image for linear filtering.
49 vec4 sampleYUV(vec2 src) {50 vec4 sampleYUV(vec2 src) {
50 ivec2 size = textureSize(tex, 0);51 ivec2 size = textureSize(tex, 0);
51- vec2 lumaUV = src / vec2(size);52+ vec2 luma = clamp(src, d.crop.xy + 0.5, d.crop.zw - 0.5);
52- float y = texture(tex, lumaUV).r;53+ float y = texture(tex, luma / vec2(size)).r;
53- vec2 chromaUV = vec2((src.x + 0.5) / float(size.x), src.y / float(size.y));54+ // Type 0 chroma is shifted half a luma pixel in x. Clamp in
55+ // half-resolution texel space before filtering the chroma plane.
56+ vec2 chroma = clamp((src + vec2(0.5, 0.0)) * 0.5,
57+ d.crop.xy * 0.5 + 0.5, d.crop.zw * 0.5 - 0.5);
58+ vec2 chromaUV = chroma * 2.0 / vec2(size);
54 vec2 cbcr = texture(chromaTex, chromaUV).rg;59 vec2 cbcr = texture(chromaTex, chromaUV).rg;
55 bool tenBit = (d.misc.y & flagP010) != 0u;60 bool tenBit = (d.misc.y & flagP010) != 0u;
56 bool limited = ((d.buf.w >> 8) & 255u) == 2u;61 bool limited = ((d.buf.w >> 8) & 255u) == 2u;
@@ -72,7 +77,7 @@ vec4 sampleYUV(vec2 src) {
72 77
73 // texel reads buffer pixel p, clamped to the buffer (clamp to edge).78 // texel reads buffer pixel p, clamped to the buffer (clamp to edge).
74 vec4 texel(ivec2 p) {79 vec4 texel(ivec2 p) {
75- p = clamp(p, ivec2(0), ivec2(d.buf.yz) - 1);80+ p = clamp(p, ivec2(floor(d.crop.xy)), min(ivec2(d.buf.yz) - 1, ivec2(ceil(d.crop.zw)) - 1));
76 // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A).81 // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A).
77 return unpackUnorm4x8(pixels[d.buf.x + uint(p.y) * d.buf.y + uint(p.x)]).zyxw;82 return unpackUnorm4x8(pixels[d.buf.x + uint(p.y) * d.buf.y + uint(p.x)]).zyxw;
78 }83 }
@@ -87,7 +92,8 @@ void main() {
87 c = sampleYUV(src);92 c = sampleYUV(src);
88 } else if (d.misc.x == modeImage) {93 } else if (d.misc.x == modeImage) {
89 ivec2 size = textureSize(tex, 0);94 ivec2 size = textureSize(tex, 0);
90- c = exact ? texelFetch(tex, clamp(ivec2(floor(src)), ivec2(0), size - 1), 0) : texture(tex, src / vec2(size));95+ vec2 p = clamp(src, d.crop.xy + 0.5, d.crop.zw - 0.5);
96+ c = exact ? texelFetch(tex, clamp(ivec2(floor(p)), ivec2(0), size - 1), 0) : texture(tex, p / vec2(size));
91 } else if (exact) {97 } else if (exact) {
92 c = texel(ivec2(floor(src)));98 c = texel(ivec2(floor(src)));
93 } else {99 } else {
M internal/adapters/vulkan/shaders/compose_hdr.frag.spv +0 -0 binary file: no text diff

binary file: no text diff

M internal/adapters/vulkan/shm.go +8 -7
@@ -148,17 +148,18 @@ func (r *Renderer) freeGPUBuffer(b *gpuBuffer) {
148148 *b = gpuBuffer{}
149149 }
150150
151-// shmKey is a surface of a window: index 0 is the root, i+1 its child i.
151+// shmKey identifies a surface independently of its position in a tree.
152152 type shmKey struct {
153- win ports.WindowID
154- index int
153+ win ports.WindowID
154+ surface uint64
155155 }
156156
157157 // shmState is what a GPU buffer holds: a w×h client buffer at a content
158158 // Seq.
159159 type shmState struct {
160- w, h int
161- seq uint64
160+ w, h int
161+ seq uint64
162+ windowSeq uint64 // root damage history uses window publications
162163 }
163164
164165 // shmCopy is one of a surface's two GPU buffers and the set binding it.
@@ -189,7 +190,7 @@ func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, st
189190 }
190191 s.last = r.frame
191192 for _, c := range s.bufs {
192- if c != nil && c.valid && c.holds == st {
193+ if c != nil && c.valid && c.holds.w == st.w && c.holds.h == st.h && c.holds.seq == st.seq {
193194 c.gpu.last = r.frame
194195 return c, nil
195196 }
@@ -227,7 +228,7 @@ func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, st
227228 row := st.w * 4
228229 rects := []ports.Rect{{W: st.w, H: st.h}}
229230 if c.valid && c.holds.w == st.w && c.holds.h == st.h && damage != nil {
230- if d, ok := damage(c.holds.seq); ok {
231+ if d, ok := damage(c.holds.windowSeq); ok {
231232 rects = d
232233 }
233234 }
@@ -148,17 +148,18 @@ func (r *Renderer) freeGPUBuffer(b *gpuBuffer) {
148 *b = gpuBuffer{}148 *b = gpuBuffer{}
149 }149 }
150 150
151-// shmKey is a surface of a window: index 0 is the root, i+1 its child i.151+// shmKey identifies a surface independently of its position in a tree.
152 type shmKey struct {152 type shmKey struct {
153- win ports.WindowID153+ win ports.WindowID
154- index int154+ surface uint64
155 }155 }
156 156
157 // shmState is what a GPU buffer holds: a w×h client buffer at a content157 // shmState is what a GPU buffer holds: a w×h client buffer at a content
158 // Seq.158 // Seq.
159 type shmState struct {159 type shmState struct {
160- w, h int160+ w, h int
161- seq uint64161+ seq uint64
162+ windowSeq uint64 // root damage history uses window publications
162 }163 }
163 164
164 // shmCopy is one of a surface's two GPU buffers and the set binding it.165 // shmCopy is one of a surface's two GPU buffers and the set binding it.
@@ -189,7 +190,7 @@ func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, st
189 }190 }
190 s.last = r.frame191 s.last = r.frame
191 for _, c := range s.bufs {192 for _, c := range s.bufs {
192- if c != nil && c.valid && c.holds == st {193+ if c != nil && c.valid && c.holds.w == st.w && c.holds.h == st.h && c.holds.seq == st.seq {
193 c.gpu.last = r.frame194 c.gpu.last = r.frame
194 return c, nil195 return c, nil
195 }196 }
@@ -227,7 +228,7 @@ func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, st
227 row := st.w * 4228 row := st.w * 4
228 rects := []ports.Rect{{W: st.w, H: st.h}}229 rects := []ports.Rect{{W: st.w, H: st.h}}
229 if c.valid && c.holds.w == st.w && c.holds.h == st.h && damage != nil {230 if c.valid && c.holds.w == st.w && c.holds.h == st.h && damage != nil {
230- if d, ok := damage(c.holds.seq); ok {231+ if d, ok := damage(c.holds.windowSeq); ok {
231 rects = d232 rects = d
232 }233 }
233 }234 }
M internal/adapters/vulkan/yuv_test.go +31 -1
@@ -13,7 +13,7 @@ import (
1313 )
1414
1515 // Each plane can be backed by a separate DMA-BUF, as with VAAPI exports.
16-func yuvTestBuffer(t *testing.T, p010 bool, y, u, v int) *ports.DMABuf {
16+func yuvTestBuffer(t *testing.T, p010 bool, y, u, v int, rows ...[3]int) *ports.DMABuf {
1717 t.Helper()
1818 format := fourcc('N', 'V', '1', '2')
1919 if p010 {
@@ -49,6 +49,13 @@ func yuvTestBuffer(t *testing.T, p010 bool, y, u, v int) *ports.DMABuf {
4949 }
5050 for n, ch := start, 0; n < end; ch++ {
5151 val := values[ch%len(values)]
52+ if len(rows) > 0 {
53+ if i == 0 && (n-start)/int(stride) < 4 {
54+ val = rows[0][0]
55+ } else if i == 1 && (n-start)/int(stride) < 2 {
56+ val = rows[0][ch%len(values)+1]
57+ }
58+ }
5259 if p010 {
5360 code := uint16(val << 6)
5461 data[n], data[n+1] = byte(code), byte(code>>8)
@@ -159,3 +166,26 @@ func pqDecode(code float64) float64 {
159166 p := math.Pow(code, 32.0/2523)
160167 return math.Pow(math.Max(p-3424.0/4096, 0)/(2413.0/128-p*2392.0/128), 16384.0/2610) * 10000
161168 }
169+
170+// Luma and chroma rows outside the crop must not bleed into its edge.
171+func TestYUVSourceCropEdges(t *testing.T) {
172+ r, err := New(64, 16)
173+ if err != nil {
174+ t.Skipf("Vulkan unavailable: %v", err)
175+ }
176+ defer r.Close()
177+ b := yuvTestBuffer(t, false, 235, 128, 128, [3]int{16, 16, 240})
178+ if !slices.Contains(r.DMABuf().Formats, ports.DMABufFormat{Format: b.Format}) {
179+ t.Skipf("linear NV12 not importable")
180+ }
181+ c := ports.SurfaceContent{ID: 1, Width: 64, Height: 16, LogicalW: 64, LogicalH: 16, Source: [4]float32{0, 4, 64, 8}, Opaque: true, DMABuf: b, Color: ports.SurfaceColor{Coefficients: 2, Range: 2, Chroma: 1}}
182+ scene := ports.Scene{Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}}}}
183+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
184+ t.Fatal(err)
185+ }
186+ for _, y := range []int{0, 1, 8, 15} {
187+ if got := r.Pixels().RGBAAt(8, y); !near(got, color.RGBA{255, 255, 255, 255}, 8) {
188+ t.Errorf("cropped edge y=%d: %v, want white", y, got)
189+ }
190+ }
191+}
@@ -13,7 +13,7 @@ import (
13 )13 )
14 14
15 // Each plane can be backed by a separate DMA-BUF, as with VAAPI exports.15 // Each plane can be backed by a separate DMA-BUF, as with VAAPI exports.
16-func yuvTestBuffer(t *testing.T, p010 bool, y, u, v int) *ports.DMABuf {16+func yuvTestBuffer(t *testing.T, p010 bool, y, u, v int, rows ...[3]int) *ports.DMABuf {
17 t.Helper()17 t.Helper()
18 format := fourcc('N', 'V', '1', '2')18 format := fourcc('N', 'V', '1', '2')
19 if p010 {19 if p010 {
@@ -49,6 +49,13 @@ func yuvTestBuffer(t *testing.T, p010 bool, y, u, v int) *ports.DMABuf {
49 }49 }
50 for n, ch := start, 0; n < end; ch++ {50 for n, ch := start, 0; n < end; ch++ {
51 val := values[ch%len(values)]51 val := values[ch%len(values)]
52+ if len(rows) > 0 {
53+ if i == 0 && (n-start)/int(stride) < 4 {
54+ val = rows[0][0]
55+ } else if i == 1 && (n-start)/int(stride) < 2 {
56+ val = rows[0][ch%len(values)+1]
57+ }
58+ }
52 if p010 {59 if p010 {
53 code := uint16(val << 6)60 code := uint16(val << 6)
54 data[n], data[n+1] = byte(code), byte(code>>8)61 data[n], data[n+1] = byte(code), byte(code>>8)
@@ -159,3 +166,26 @@ func pqDecode(code float64) float64 {
159 p := math.Pow(code, 32.0/2523)166 p := math.Pow(code, 32.0/2523)
160 return math.Pow(math.Max(p-3424.0/4096, 0)/(2413.0/128-p*2392.0/128), 16384.0/2610) * 10000167 return math.Pow(math.Max(p-3424.0/4096, 0)/(2413.0/128-p*2392.0/128), 16384.0/2610) * 10000
161 }168 }
169+
170+// Luma and chroma rows outside the crop must not bleed into its edge.
171+func TestYUVSourceCropEdges(t *testing.T) {
172+ r, err := New(64, 16)
173+ if err != nil {
174+ t.Skipf("Vulkan unavailable: %v", err)
175+ }
176+ defer r.Close()
177+ b := yuvTestBuffer(t, false, 235, 128, 128, [3]int{16, 16, 240})
178+ if !slices.Contains(r.DMABuf().Formats, ports.DMABufFormat{Format: b.Format}) {
179+ t.Skipf("linear NV12 not importable")
180+ }
181+ c := ports.SurfaceContent{ID: 1, Width: 64, Height: 16, LogicalW: 64, LogicalH: 16, Source: [4]float32{0, 4, 64, 8}, Opaque: true, DMABuf: b, Color: ports.SurfaceColor{Coefficients: 2, Range: 2, Chroma: 1}}
182+ scene := ports.Scene{Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}}}}
183+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
184+ t.Fatal(err)
185+ }
186+ for _, y := range []int{0, 1, 8, 15} {
187+ if got := r.Pixels().RGBAAt(8, y); !near(got, color.RGBA{255, 255, 255, 255}, 8) {
188+ t.Errorf("cropped edge y=%d: %v, want white", y, got)
189+ }
190+ }
191+}
M internal/adapters/wayland/fifo.go +227 -58
@@ -198,36 +198,68 @@ func (s *Server) surfaceOf(w *wayland.Surface) *surface {
198198
199199 // update is a surface's pending state taken out by a commit that waits.
200200 type update struct {
201- attached bool
202- buffer *wayland.Buffer
203- scale int
204- async bool
205- kind uint32
206- callbacks []*wayland.Callback
207- vp *viewport
208- vpW, vpH int32
209- vpSet bool
210- moves []childMove
211- xdg *xdgSurface
212- geometry ports.Rect
213- cons *constraint
214- region *ports.Rect
215- layer *layerSurface
216- layerNext layerState
217- barrier bool
218- wait bool
219- at time.Time
220- feedback []*presentationtime.WpPresentationFeedback
221- sync *commitSync
222- color SurfaceColor
223- representation surfaceRepresentation
224- damage []ports.Rect
225- bufDamage []ports.Rect
226-}
227-
228-type childMove struct {
201+ attached bool
202+ buffer *wayland.Buffer
203+ scale int
204+ async bool
205+ kind uint32
206+ callbacks []*wayland.Callback
207+ vp *viewport
208+ vpW, vpH int32
209+ vpSet bool
210+ vpSrc [4]server.Fixed
211+ vpCrop bool
212+ layout []childLayout
213+ deps []*update
214+ owner *surface
215+ prev *update
216+ synced, bound bool
217+ xdg *xdgSurface
218+ geometry ports.Rect
219+ cons *constraint
220+ region *ports.Rect
221+ layer *layerSurface
222+ layerNext layerState
223+ barrier bool
224+ wait bool
225+ at time.Time
226+ feedback []*presentationtime.WpPresentationFeedback
227+ sync *commitSync
228+ color SurfaceColor
229+ representation surfaceRepresentation
230+ damage []ports.Rect
231+ bufDamage []ports.Rect
232+ readyGeneration uint64
233+ readyResult bool
234+}
235+
236+type childLayout struct {
229237 child *surface
230238 x, y int
239+ below bool
240+}
241+
242+func removeLayout(layout []childLayout, child *surface) []childLayout {
243+ layout = append([]childLayout(nil), layout...)
244+ for i := 0; i < len(layout); i++ {
245+ if layout[i].child == child {
246+ layout = append(layout[:i:i], layout[i+1:]...)
247+ i--
248+ }
249+ }
250+ return layout
251+}
252+
253+func sameLayout(a, b []childLayout) bool {
254+ if len(a) != len(b) {
255+ return false
256+ }
257+ for i := range a {
258+ if a[i] != b[i] {
259+ return false
260+ }
261+ }
262+ return true
231263 }
232264
233265 // takePending moves the pending state into an update: one-shot state is
@@ -239,14 +271,9 @@ func (s *surface) takePending() update {
239271 s.pendingDamage, s.pendingBufDamage = nil, nil
240272 s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{}
241273 if v := s.viewport; v != nil {
242- u.vp, u.vpW, u.vpH, u.vpSet = v, v.pendingW, v.pendingH, v.pendingSet
243- }
244- for _, ch := range s.sub.children {
245- if ch.sub.moved {
246- u.moves = append(u.moves, childMove{ch, ch.sub.pendX, ch.sub.pendY})
247- ch.sub.moved = false
248- }
274+ u.vp, u.vpW, u.vpH, u.vpSet, u.vpSrc, u.vpCrop = v, v.pendingW, v.pendingH, v.pendingSet, v.pendingSrc, v.pendingCrop
249275 }
276+ u.layout = s.sub.pendingLayout
250277 if s.xdg != nil {
251278 u.xdg, u.geometry = s.xdg, s.xdg.pendingGeometry
252279 }
@@ -269,14 +296,14 @@ func (s *surface) putPending(u update) {
269296 s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at
270297 s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage
271298 s.pendingFeedback, s.pendingSync = u.feedback, u.sync
272- if u.vp != nil && s.viewport == u.vp {
299+ if u.vp != nil && u.vp.resource != nil && u.vp.resource.Resource.Alive() {
300+ s.viewport = u.vp
273301 u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet
302+ u.vp.pendingSrc, u.vp.pendingCrop = u.vpSrc, u.vpCrop
303+ } else {
304+ s.viewport = nil
274305 }
275- for _, m := range u.moves {
276- if m.child.sub.parent == s {
277- m.child.sub.pendX, m.child.sub.pendY, m.child.sub.moved = m.x, m.y, true
278- }
279- }
306+ s.sub.pendingLayout = u.layout
280307 if u.xdg != nil && s.xdg == u.xdg {
281308 u.xdg.pendingGeometry = u.geometry
282309 }
@@ -288,11 +315,6 @@ func (s *surface) putPending(u update) {
288315 }
289316 }
290317
291-// mustWait reports whether the pending commit cannot apply now.
292-func (s *surface) mustWait(now time.Time) bool {
293- return len(s.queue) > 0 || (s.pendingWait && s.barrier) || s.tooEarly(s.pendingTime, now) || !s.syncReady(s.pendingSync)
294-}
295-
296318 // tooEarly reports whether content applied now would show before at:
297319 // it reaches the screen one refresh later.
298320 func (s *surface) tooEarly(at, now time.Time) bool {
@@ -319,22 +341,64 @@ func (s *Server) nextRefresh(name string, now time.Time) time.Time {
319341
320342 // queueUpdate queues the pending commit until it can apply.
321343 func (s *surface) queueUpdate() {
322- s.queue = append(s.queue, s.takePending())
344+ u := s.takePending()
345+ u.owner, u.synced = s, s.effectivelySynced()
346+ for _, ch := range s.sub.children {
347+ for i := len(ch.queue) - 1; i >= 0; i-- {
348+ candidate := ch.queue[i]
349+ if candidate.synced && !candidate.bound {
350+ candidate.bound = true
351+ u.deps = append(u.deps, candidate)
352+ break
353+ }
354+ }
355+ }
356+ if len(s.queue) > 0 {
357+ u.prev = s.queue[len(s.queue)-1]
358+ }
359+ s.queue = append(s.queue, &u)
323360 s.server.fifoSurfaces[s] = struct{}{}
324361 s.server.wakePacer()
325362 }
326363
327364 // applyUpdate applies a queued update, keeping the requests made since.
328-func (s *surface) applyUpdate(u update) {
365+func (s *surface) applyUpdate(u *update) {
329366 // A buffer destroyed while queued (a swapchain resize) is skipped: the
330367 // surface keeps its content, and is not unmapped behind the client.
368+ s.commitSkipped = false
369+ s.queuedScale = u.scale
370+ s.queuedBuffer = 0
371+ if u.buffer != nil {
372+ s.queuedBuffer = u.buffer.ID()
373+ }
331374 if u.buffer != nil && !u.buffer.Resource.Alive() {
375+ s.commitSkipped = true
332376 u.buffer, u.attached = nil, false
377+ // Scale and crop describe the same retained buffer.
378+ u.scale = s.bufferScale
379+ // The viewport change was made for that buffer: keep the crop that
380+ // matches the retained content instead of validating a mismatch.
381+ if u.vp != nil && u.vp.resource != nil && u.vp.resource.Resource.Alive() {
382+ if v := s.committedViewport; v != nil {
383+ u.vpW, u.vpH, u.vpSet, u.vpSrc, u.vpCrop = v.destW, v.destH, v.dest, v.src, v.crop
384+ } else {
385+ u.vpSet, u.vpCrop = false, false
386+ }
387+ }
333388 }
334389 later := s.takePending()
335- s.putPending(u)
390+ originalViewport := s.viewport
391+ originalLayout := s.sub.pendingLayout
392+ s.putPending(*u)
336393 s.applyCommit()
337394 s.putPending(later)
395+ if originalViewport != nil {
396+ originalViewport.pendingW, originalViewport.pendingH, originalViewport.pendingSet = later.vpW, later.vpH, later.vpSet
397+ originalViewport.pendingSrc, originalViewport.pendingCrop = later.vpSrc, later.vpCrop
398+ }
399+ s.viewport = originalViewport
400+ s.sub.pendingLayout = originalLayout
401+ s.commitSkipped = false
338402 }
339403
340404 // dropQueue discards the queued updates of a destroyed surface or role.
@@ -356,7 +420,7 @@ func (s *surface) dropQueue() {
356420 if u.sync != nil {
357421 continue // explicit sync: the release point replaced release
358422 }
359- if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() {
423+ if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() && !s.bufferQueuedElsewhere(u.buffer) {
360424 released[u.buffer.Resource] = true
361425 u.buffer.SendRelease()
362426 }
@@ -365,6 +429,23 @@ func (s *surface) dropQueue() {
365429 delete(s.server.fifoSurfaces, s)
366430 }
367431
432+func (s *surface) bufferQueuedElsewhere(b *wayland.Buffer) bool {
433+ for _, other := range s.server.surfaces {
434+ if other == s {
435+ continue
436+ }
437+ if sameBuffer(other.current, b) {
438+ return true
439+ }
440+ for _, u := range other.queue {
441+ if sameBuffer(u.buffer, b) {
442+ return true
443+ }
444+ }
445+ }
446+ return false
447+}
448+
368449 // setBarrier applies a committed set_barrier.
369450 func (s *surface) setBarrier(now time.Time) {
370451 s.barrier, s.barrierAt = true, now
@@ -406,6 +487,7 @@ func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration
406487 }
407488 }
408489 wait := defaultFramePeriod
490+ s.readinessGeneration++
409491 for surf := range s.fifoSurfaces {
410492 name := s.fifoOutput(surf)
411493 p := s.outputPeriod(name)
@@ -419,18 +501,38 @@ func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration
419501 }
420502 for len(surf.queue) > 0 && !surf.destroyed {
421503 u := surf.queue[0]
422- if u.wait && surf.barrier {
504+ if u.synced || !u.graphReady(now) {
423505 break
424506 }
425- if !surf.syncReady(u.sync) {
426- break
507+ s.applyingGraph = true
508+ s.graphDrawn = false
509+ s.graphFeedback = s.graphFeedback[:0]
510+ u.applyGraph()
511+ s.applyingGraph = false
512+ // Applying a graph changes queues and readiness within this tick.
513+ s.readinessGeneration++
514+ if s.graphDrawn {
515+ surf.redraw()
427516 }
428- if surf.tooEarly(u.at, now) {
429- wait = min(wait, u.at.Sub(s.nextRefresh(name, now))+time.Millisecond)
430- break
517+ // Only the last commit per surface can be sampled from this
518+ // publication. Earlier callbacks were already queued on apply.
519+ for i, fb := range s.graphFeedback {
520+ superseded := false
521+ for j := i + 1; j < len(s.graphFeedback); j++ {
522+ if s.graphFeedback[j].surf == fb.surf && s.graphFeedback[j].fresh {
523+ superseded = true
524+ break
525+ }
526+ }
527+ if superseded {
528+ for _, pending := range fb.pending {
529+ discard(pending)
530+ }
531+ continue
532+ }
533+ fb.surf.commitFeedback(fb.pending, fb.fresh)
431534 }
432- surf.queue = surf.queue[1:]
433- surf.applyUpdate(u)
535+ s.graphFeedback = s.graphFeedback[:0]
434536 }
435537 if surf.barrier {
436538 wait = min(wait, deadline.Sub(now))
@@ -442,6 +544,73 @@ func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration
442544 return max(wait, time.Millisecond), len(s.fifoSurfaces) > 0
443545 }
444546
547+func (s *surface) effectivelySynced() bool {
548+ for p := s; p != nil; p = p.sub.parent {
549+ if p.sub.parent != nil && p.sub.synced {
550+ return true
551+ }
552+ }
553+ return false
554+}
555+
556+func (s *surface) flushDesync() {
557+ if !s.effectivelySynced() {
558+ for _, u := range s.queue {
559+ if u.synced && !u.bound {
560+ u.synced = false
561+ }
562+ }
563+ }
564+ for _, ch := range s.sub.children {
565+ ch.flushDesync()
566+ }
567+ s.server.wakePacer()
568+}
569+
570+func (u *update) graphReady(now time.Time) bool {
571+ s := u.owner
572+ gen := s.server.readinessGeneration
573+ if gen != 0 && u.readyGeneration == gen {
574+ return u.readyResult
575+ }
576+ if gen != 0 {
577+ u.readyGeneration, u.readyResult = gen, false
578+ }
579+ if u.prev != nil && !u.prev.graphReady(now) {
580+ return false
581+ }
582+ if u.wait && s.barrier || !s.syncReady(u.sync) || s.tooEarly(u.at, now) {
583+ return false
584+ }
585+ for _, dep := range u.deps {
586+ if !dep.owner.destroyed && !dep.graphReady(now) {
587+ return false
588+ }
589+ }
590+ u.readyResult = true
591+ return true
592+}
593+
594+func (u *update) applyGraph() {
595+ s := u.owner
596+ for len(s.queue) > 0 && s.queue[0] != u {
597+ s.queue[0].applyGraph()
598+ }
599+ for _, dep := range u.deps {
600+ if !dep.owner.destroyed {
601+ dep.applyGraph()
602+ }
603+ }
604+ if len(s.queue) == 0 || s.queue[0] != u {
605+ return
606+ }
607+ s.queue = s.queue[1:]
608+ if len(s.queue) > 0 {
609+ s.queue[0].prev = nil
610+ }
611+ s.applyUpdate(u)
612+}
613+
445614 // sameBuffer reports whether two wrappers are the same wl_buffer.
446615 func sameBuffer(a, b *wayland.Buffer) bool {
447616 return a != nil && b != nil && a.Resource == b.Resource
@@ -198,36 +198,68 @@ func (s *Server) surfaceOf(w *wayland.Surface) *surface {
198 198
199 // update is a surface's pending state taken out by a commit that waits.199 // update is a surface's pending state taken out by a commit that waits.
200 type update struct {200 type update struct {
201- attached bool201+ attached bool
202- buffer *wayland.Buffer202+ buffer *wayland.Buffer
203- scale int203+ scale int
204- async bool204+ async bool
205- kind uint32205+ kind uint32
206- callbacks []*wayland.Callback206+ callbacks []*wayland.Callback
207- vp *viewport207+ vp *viewport
208- vpW, vpH int32208+ vpW, vpH int32
209- vpSet bool209+ vpSet bool
210- moves []childMove210+ vpSrc [4]server.Fixed
211- xdg *xdgSurface211+ vpCrop bool
212- geometry ports.Rect212+ layout []childLayout
213- cons *constraint213+ deps []*update
214- region *ports.Rect214+ owner *surface
215- layer *layerSurface215+ prev *update
216- layerNext layerState216+ synced, bound bool
217- barrier bool217+ xdg *xdgSurface
218- wait bool218+ geometry ports.Rect
219- at time.Time219+ cons *constraint
220- feedback []*presentationtime.WpPresentationFeedback220+ region *ports.Rect
221- sync *commitSync221+ layer *layerSurface
222- color SurfaceColor222+ layerNext layerState
223- representation surfaceRepresentation223+ barrier bool
224- damage []ports.Rect224+ wait bool
225- bufDamage []ports.Rect225+ at time.Time
226-}226+ feedback []*presentationtime.WpPresentationFeedback
227-227+ sync *commitSync
228-type childMove struct {228+ color SurfaceColor
229+ representation surfaceRepresentation
230+ damage []ports.Rect
231+ bufDamage []ports.Rect
232+ readyGeneration uint64
233+ readyResult bool
234+}
235+
236+type childLayout struct {
229 child *surface237 child *surface
230 x, y int238 x, y int
239+ below bool
240+}
241+
242+func removeLayout(layout []childLayout, child *surface) []childLayout {
243+ layout = append([]childLayout(nil), layout...)
244+ for i := 0; i < len(layout); i++ {
245+ if layout[i].child == child {
246+ layout = append(layout[:i:i], layout[i+1:]...)
247+ i--
248+ }
249+ }
250+ return layout
251+}
252+
253+func sameLayout(a, b []childLayout) bool {
254+ if len(a) != len(b) {
255+ return false
256+ }
257+ for i := range a {
258+ if a[i] != b[i] {
259+ return false
260+ }
261+ }
262+ return true
231 }263 }
232 264
233 // takePending moves the pending state into an update: one-shot state is265 // takePending moves the pending state into an update: one-shot state is
@@ -239,14 +271,9 @@ func (s *surface) takePending() update {
239 s.pendingDamage, s.pendingBufDamage = nil, nil271 s.pendingDamage, s.pendingBufDamage = nil, nil
240 s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{}272 s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{}
241 if v := s.viewport; v != nil {273 if v := s.viewport; v != nil {
242- u.vp, u.vpW, u.vpH, u.vpSet = v, v.pendingW, v.pendingH, v.pendingSet274+ u.vp, u.vpW, u.vpH, u.vpSet, u.vpSrc, u.vpCrop = v, v.pendingW, v.pendingH, v.pendingSet, v.pendingSrc, v.pendingCrop
243- }
244- for _, ch := range s.sub.children {
245- if ch.sub.moved {
246- u.moves = append(u.moves, childMove{ch, ch.sub.pendX, ch.sub.pendY})
247- ch.sub.moved = false
248- }
249 }275 }
276+ u.layout = s.sub.pendingLayout
250 if s.xdg != nil {277 if s.xdg != nil {
251 u.xdg, u.geometry = s.xdg, s.xdg.pendingGeometry278 u.xdg, u.geometry = s.xdg, s.xdg.pendingGeometry
252 }279 }
@@ -269,14 +296,14 @@ func (s *surface) putPending(u update) {
269 s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at296 s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at
270 s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage297 s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage
271 s.pendingFeedback, s.pendingSync = u.feedback, u.sync298 s.pendingFeedback, s.pendingSync = u.feedback, u.sync
272- if u.vp != nil && s.viewport == u.vp {299+ if u.vp != nil && u.vp.resource != nil && u.vp.resource.Resource.Alive() {
300+ s.viewport = u.vp
273 u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet301 u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet
302+ u.vp.pendingSrc, u.vp.pendingCrop = u.vpSrc, u.vpCrop
303+ } else {
304+ s.viewport = nil
274 }305 }
275- for _, m := range u.moves {306+ s.sub.pendingLayout = u.layout
276- if m.child.sub.parent == s {
277- m.child.sub.pendX, m.child.sub.pendY, m.child.sub.moved = m.x, m.y, true
278- }
279- }
280 if u.xdg != nil && s.xdg == u.xdg {307 if u.xdg != nil && s.xdg == u.xdg {
281 u.xdg.pendingGeometry = u.geometry308 u.xdg.pendingGeometry = u.geometry
282 }309 }
@@ -288,11 +315,6 @@ func (s *surface) putPending(u update) {
288 }315 }
289 }316 }
290 317
291-// mustWait reports whether the pending commit cannot apply now.
292-func (s *surface) mustWait(now time.Time) bool {
293- return len(s.queue) > 0 || (s.pendingWait && s.barrier) || s.tooEarly(s.pendingTime, now) || !s.syncReady(s.pendingSync)
294-}
295-
296 // tooEarly reports whether content applied now would show before at:318 // tooEarly reports whether content applied now would show before at:
297 // it reaches the screen one refresh later.319 // it reaches the screen one refresh later.
298 func (s *surface) tooEarly(at, now time.Time) bool {320 func (s *surface) tooEarly(at, now time.Time) bool {
@@ -319,22 +341,64 @@ func (s *Server) nextRefresh(name string, now time.Time) time.Time {
319 341
320 // queueUpdate queues the pending commit until it can apply.342 // queueUpdate queues the pending commit until it can apply.
321 func (s *surface) queueUpdate() {343 func (s *surface) queueUpdate() {
322- s.queue = append(s.queue, s.takePending())344+ u := s.takePending()
345+ u.owner, u.synced = s, s.effectivelySynced()
346+ for _, ch := range s.sub.children {
347+ for i := len(ch.queue) - 1; i >= 0; i-- {
348+ candidate := ch.queue[i]
349+ if candidate.synced && !candidate.bound {
350+ candidate.bound = true
351+ u.deps = append(u.deps, candidate)
352+ break
353+ }
354+ }
355+ }
356+ if len(s.queue) > 0 {
357+ u.prev = s.queue[len(s.queue)-1]
358+ }
359+ s.queue = append(s.queue, &u)
323 s.server.fifoSurfaces[s] = struct{}{}360 s.server.fifoSurfaces[s] = struct{}{}
324 s.server.wakePacer()361 s.server.wakePacer()
325 }362 }
326 363
327 // applyUpdate applies a queued update, keeping the requests made since.364 // applyUpdate applies a queued update, keeping the requests made since.
328-func (s *surface) applyUpdate(u update) {365+func (s *surface) applyUpdate(u *update) {
329 // A buffer destroyed while queued (a swapchain resize) is skipped: the366 // A buffer destroyed while queued (a swapchain resize) is skipped: the
330 // surface keeps its content, and is not unmapped behind the client.367 // surface keeps its content, and is not unmapped behind the client.
368+ s.commitSkipped = false
369+ s.queuedScale = u.scale
370+ s.queuedBuffer = 0
371+ if u.buffer != nil {
372+ s.queuedBuffer = u.buffer.ID()
373+ }
331 if u.buffer != nil && !u.buffer.Resource.Alive() {374 if u.buffer != nil && !u.buffer.Resource.Alive() {
375+ s.commitSkipped = true
332 u.buffer, u.attached = nil, false376 u.buffer, u.attached = nil, false
377+ // Scale and crop describe the same retained buffer.
378+ u.scale = s.bufferScale
379+ // The viewport change was made for that buffer: keep the crop that
380+ // matches the retained content instead of validating a mismatch.
381+ if u.vp != nil && u.vp.resource != nil && u.vp.resource.Resource.Alive() {
382+ if v := s.committedViewport; v != nil {
383+ u.vpW, u.vpH, u.vpSet, u.vpSrc, u.vpCrop = v.destW, v.destH, v.dest, v.src, v.crop
384+ } else {
385+ u.vpSet, u.vpCrop = false, false
386+ }
387+ }
333 }388 }
334 later := s.takePending()389 later := s.takePending()
335- s.putPending(u)390+ originalViewport := s.viewport
391+ originalLayout := s.sub.pendingLayout
392+ s.putPending(*u)
336 s.applyCommit()393 s.applyCommit()
337 s.putPending(later)394 s.putPending(later)
395+ if originalViewport != nil {
396+ originalViewport.pendingW, originalViewport.pendingH, originalViewport.pendingSet = later.vpW, later.vpH, later.vpSet
397+ originalViewport.pendingSrc, originalViewport.pendingCrop = later.vpSrc, later.vpCrop
398+ }
399+ s.viewport = originalViewport
400+ s.sub.pendingLayout = originalLayout
401+ s.commitSkipped = false
338 }402 }
339 403
340 // dropQueue discards the queued updates of a destroyed surface or role.404 // dropQueue discards the queued updates of a destroyed surface or role.
@@ -356,7 +420,7 @@ func (s *surface) dropQueue() {
356 if u.sync != nil {420 if u.sync != nil {
357 continue // explicit sync: the release point replaced release421 continue // explicit sync: the release point replaced release
358 }422 }
359- if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() {423+ if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() && !s.bufferQueuedElsewhere(u.buffer) {
360 released[u.buffer.Resource] = true424 released[u.buffer.Resource] = true
361 u.buffer.SendRelease()425 u.buffer.SendRelease()
362 }426 }
@@ -365,6 +429,23 @@ func (s *surface) dropQueue() {
365 delete(s.server.fifoSurfaces, s)429 delete(s.server.fifoSurfaces, s)
366 }430 }
367 431
432+func (s *surface) bufferQueuedElsewhere(b *wayland.Buffer) bool {
433+ for _, other := range s.server.surfaces {
434+ if other == s {
435+ continue
436+ }
437+ if sameBuffer(other.current, b) {
438+ return true
439+ }
440+ for _, u := range other.queue {
441+ if sameBuffer(u.buffer, b) {
442+ return true
443+ }
444+ }
445+ }
446+ return false
447+}
448+
368 // setBarrier applies a committed set_barrier.449 // setBarrier applies a committed set_barrier.
369 func (s *surface) setBarrier(now time.Time) {450 func (s *surface) setBarrier(now time.Time) {
370 s.barrier, s.barrierAt = true, now451 s.barrier, s.barrierAt = true, now
@@ -406,6 +487,7 @@ func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration
406 }487 }
407 }488 }
408 wait := defaultFramePeriod489 wait := defaultFramePeriod
490+ s.readinessGeneration++
409 for surf := range s.fifoSurfaces {491 for surf := range s.fifoSurfaces {
410 name := s.fifoOutput(surf)492 name := s.fifoOutput(surf)
411 p := s.outputPeriod(name)493 p := s.outputPeriod(name)
@@ -419,18 +501,38 @@ func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration
419 }501 }
420 for len(surf.queue) > 0 && !surf.destroyed {502 for len(surf.queue) > 0 && !surf.destroyed {
421 u := surf.queue[0]503 u := surf.queue[0]
422- if u.wait && surf.barrier {504+ if u.synced || !u.graphReady(now) {
423 break505 break
424 }506 }
425- if !surf.syncReady(u.sync) {507+ s.applyingGraph = true
426- break508+ s.graphDrawn = false
509+ s.graphFeedback = s.graphFeedback[:0]
510+ u.applyGraph()
511+ s.applyingGraph = false
512+ // Applying a graph changes queues and readiness within this tick.
513+ s.readinessGeneration++
514+ if s.graphDrawn {
515+ surf.redraw()
427 }516 }
428- if surf.tooEarly(u.at, now) {517+ // Only the last commit per surface can be sampled from this
429- wait = min(wait, u.at.Sub(s.nextRefresh(name, now))+time.Millisecond)518+ // publication. Earlier callbacks were already queued on apply.
430- break519+ for i, fb := range s.graphFeedback {
520+ superseded := false
521+ for j := i + 1; j < len(s.graphFeedback); j++ {
522+ if s.graphFeedback[j].surf == fb.surf && s.graphFeedback[j].fresh {
523+ superseded = true
524+ break
525+ }
526+ }
527+ if superseded {
528+ for _, pending := range fb.pending {
529+ discard(pending)
530+ }
531+ continue
532+ }
533+ fb.surf.commitFeedback(fb.pending, fb.fresh)
431 }534 }
432- surf.queue = surf.queue[1:]535+ s.graphFeedback = s.graphFeedback[:0]
433- surf.applyUpdate(u)
434 }536 }
435 if surf.barrier {537 if surf.barrier {
436 wait = min(wait, deadline.Sub(now))538 wait = min(wait, deadline.Sub(now))
@@ -442,6 +544,73 @@ func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration
442 return max(wait, time.Millisecond), len(s.fifoSurfaces) > 0544 return max(wait, time.Millisecond), len(s.fifoSurfaces) > 0
443 }545 }
444 546
547+func (s *surface) effectivelySynced() bool {
548+ for p := s; p != nil; p = p.sub.parent {
549+ if p.sub.parent != nil && p.sub.synced {
550+ return true
551+ }
552+ }
553+ return false
554+}
555+
556+func (s *surface) flushDesync() {
557+ if !s.effectivelySynced() {
558+ for _, u := range s.queue {
559+ if u.synced && !u.bound {
560+ u.synced = false
561+ }
562+ }
563+ }
564+ for _, ch := range s.sub.children {
565+ ch.flushDesync()
566+ }
567+ s.server.wakePacer()
568+}
569+
570+func (u *update) graphReady(now time.Time) bool {
571+ s := u.owner
572+ gen := s.server.readinessGeneration
573+ if gen != 0 && u.readyGeneration == gen {
574+ return u.readyResult
575+ }
576+ if gen != 0 {
577+ u.readyGeneration, u.readyResult = gen, false
578+ }
579+ if u.prev != nil && !u.prev.graphReady(now) {
580+ return false
581+ }
582+ if u.wait && s.barrier || !s.syncReady(u.sync) || s.tooEarly(u.at, now) {
583+ return false
584+ }
585+ for _, dep := range u.deps {
586+ if !dep.owner.destroyed && !dep.graphReady(now) {
587+ return false
588+ }
589+ }
590+ u.readyResult = true
591+ return true
592+}
593+
594+func (u *update) applyGraph() {
595+ s := u.owner
596+ for len(s.queue) > 0 && s.queue[0] != u {
597+ s.queue[0].applyGraph()
598+ }
599+ for _, dep := range u.deps {
600+ if !dep.owner.destroyed {
601+ dep.applyGraph()
602+ }
603+ }
604+ if len(s.queue) == 0 || s.queue[0] != u {
605+ return
606+ }
607+ s.queue = s.queue[1:]
608+ if len(s.queue) > 0 {
609+ s.queue[0].prev = nil
610+ }
611+ s.applyUpdate(u)
612+}
613+
445 // sameBuffer reports whether two wrappers are the same wl_buffer.614 // sameBuffer reports whether two wrappers are the same wl_buffer.
446 func sameBuffer(a, b *wayland.Buffer) bool {615 func sameBuffer(a, b *wayland.Buffer) bool {
447 return a != nil && b != nil && a.Resource == b.Resource616 return a != nil && b != nil && a.Resource == b.Resource
M internal/adapters/wayland/fifo_test.go +19 -0
@@ -206,3 +206,22 @@ func TestFifoQueuedBufferDestroyed(t *testing.T) {
206206 }
207207 }
208208 }
209+
210+// Rechecking a shared predecessor in a traversal must reuse its readiness.
211+func TestGraphReadyMemoizesPredecessors(t *testing.T) {
212+ s := &Server{readinessGeneration: 1}
213+ surf := &surface{server: s}
214+ first := &update{owner: surf}
215+ second := &update{owner: surf}
216+ third := &update{owner: surf}
217+ surf.queue = []*update{first, second, third}
218+ second.prev = first
219+ third.prev = second
220+ now := time.Now()
221+ if !third.graphReady(now) {
222+ t.Fatal("ready chain rejected")
223+ }
224+ if first.readyGeneration != s.readinessGeneration || second.readyGeneration != s.readinessGeneration {
225+ t.Fatal("predecessors not memoized")
226+ }
227+}
@@ -206,3 +206,22 @@ func TestFifoQueuedBufferDestroyed(t *testing.T) {
206 }206 }
207 }207 }
208 }208 }
209+
210+// Rechecking a shared predecessor in a traversal must reuse its readiness.
211+func TestGraphReadyMemoizesPredecessors(t *testing.T) {
212+ s := &Server{readinessGeneration: 1}
213+ surf := &surface{server: s}
214+ first := &update{owner: surf}
215+ second := &update{owner: surf}
216+ third := &update{owner: surf}
217+ surf.queue = []*update{first, second, third}
218+ second.prev = first
219+ third.prev = second
220+ now := time.Now()
221+ if !third.graphReady(now) {
222+ t.Fatal("ready chain rejected")
223+ }
224+ if first.readyGeneration != s.readinessGeneration || second.readyGeneration != s.readinessGeneration {
225+ t.Fatal("predecessors not memoized")
226+ }
227+}
M internal/adapters/wayland/globals.go +46 -40
@@ -75,6 +75,8 @@ type compositor struct{ server *Server }
7575 func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) {
7676 state := &surface{server: c.server, bufferScale: 1}
7777 if w, err := wayland.NewSurface(r.Client(), r.Version(), id, state); err == nil {
78+ c.server.nextSurface++
79+ state.identity = c.server.nextSurface
7880 state.wl = w
7981 c.server.surfaces[w.Resource] = state
8082 state.sendScale()
@@ -118,8 +120,9 @@ func (c subcompositor) GetSubsurface(r *wayland.Subcompositor, id uint32, w, par
118120 }
119121 state.kind = roleSubsurface
120122 state.role = func(bool) {}
121- state.sub = subState{role: sub, parent: up, children: state.sub.children}
123+ state.sub = subState{role: sub, parent: up, children: state.sub.children, layout: state.sub.layout, pendingLayout: state.sub.pendingLayout, synced: true}
122124 up.sub.children = append(up.sub.children, state)
125+ up.sub.pendingLayout = append(append([]childLayout(nil), up.sub.pendingLayout...), childLayout{child: state})
123126 // The child follows its root window's output and scale.
124127 state.sendScale()
125128 sub.OnDestroy = func() {
@@ -138,7 +141,18 @@ type subsurface struct{ surface *surface }
138141
139142 func (subsurface) Destroy(*wayland.Subsurface) {}
140143 func (s subsurface) SetPosition(_ *wayland.Subsurface, x, y int32) {
141- s.surface.sub.pendX, s.surface.sub.pendY, s.surface.sub.moved = int(x), int(y), true
144+ p := s.surface.sub.parent
145+ if p == nil {
146+ return
147+ }
148+ p.sub.pendingLayout = append([]childLayout(nil), p.sub.pendingLayout...)
149+ for i := range p.sub.pendingLayout {
150+ item := &p.sub.pendingLayout[i]
151+ if item.child == s.surface {
152+ item.x, item.y = int(x), int(y)
153+ break
154+ }
155+ }
142156 }
143157 func (s subsurface) PlaceAbove(r *wayland.Subsurface, sibling *wayland.Surface) {
144158 s.restack(r, sibling, true)
@@ -146,11 +160,16 @@ func (s subsurface) PlaceAbove(r *wayland.Subsurface, sibling *wayland.Surface)
146160 func (s subsurface) PlaceBelow(r *wayland.Subsurface, sibling *wayland.Surface) {
147161 s.restack(r, sibling, false)
148162 }
149-func (subsurface) SetSync(*wayland.Subsurface) {}
150-func (subsurface) SetDesync(*wayland.Subsurface) {}
163+func (s subsurface) SetSync(*wayland.Subsurface) { s.surface.sub.synced = true }
164+func (s subsurface) SetDesync(*wayland.Subsurface) {
165+ was := s.surface.effectivelySynced()
166+ s.surface.sub.synced = false
167+ if was && !s.surface.effectivelySynced() {
168+ s.surface.flushDesync()
169+ }
170+}
151171
152-// restack moves the surface next to a sibling or its parent. The order
153-// applies at once rather than on the parent commit.
172+// restack changes only the pending layout of the parent.
154173 func (s subsurface) restack(r *wayland.Subsurface, sibling *wayland.Surface, above bool) {
155174 me := s.surface
156175 p := me.sub.parent
@@ -162,54 +181,41 @@ func (s subsurface) restack(r *wayland.Subsurface, sibling *wayland.Surface, abo
162181 r.PostError(uint32(wayland.SubsurfaceErrorBadSurface), "not a sibling or the parent")
163182 return
164183 }
165- list := p.sub.children
166- for i, ch := range list {
167- if ch == me {
168- list = append(list[:i:i], list[i+1:]...)
184+ list := append([]childLayout(nil), p.sub.pendingLayout...)
185+ var item childLayout
186+ for _, entry := range list {
187+ if entry.child == me {
188+ item = entry
169189 break
170190 }
171191 }
192+ list = removeLayout(list, me)
193+ at := len(list)
172194 if other == p {
173- // Relative to the parent: just above it or just below it.
174- me.sub.below = !above
195+ item.below = !above
175196 if above {
176- list = append([]*surface{me}, list...)
177- } else {
178- list = append(list, me)
197+ for i, entry := range list {
198+ if !entry.below {
199+ at = i
200+ break
201+ }
202+ }
179203 }
180- // Keep below children first so the flattening stays ordered.
181- sortBelowFirst(list)
182204 } else {
183- me.sub.below = other.sub.below
184- at := len(list)
185- for i, ch := range list {
186- if ch == other {
187- at = i
205+ for i, entry := range list {
206+ if entry.child == other {
207+ at, item.below = i, entry.below
188208 if above {
189209 at++
190210 }
191211 break
192212 }
193213 }
194- list = append(list[:at:at], append([]*surface{me}, list[at:]...)...)
195- }
196- p.sub.children = list
197- me.redraw()
198-}
199-
200-// sortBelowFirst moves children below their parent ahead of those above,
201-// keeping each group's order.
202-func sortBelowFirst(list []*surface) {
203- below := make([]*surface, 0, len(list))
204- above := make([]*surface, 0, len(list))
205- for _, ch := range list {
206- if ch.sub.below {
207- below = append(below, ch)
208- } else {
209- above = append(above, ch)
210- }
211214 }
212- copy(list, append(below, above...))
215+ list = append(list, childLayout{})
216+ copy(list[at+1:], list[at:])
217+ list[at] = item
218+ p.sub.pendingLayout = list
213219 }
214220
215221 // region keeps the bounding box of the rectangles added: pointer
@@ -75,6 +75,8 @@ type compositor struct{ server *Server }
75 func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) {75 func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) {
76 state := &surface{server: c.server, bufferScale: 1}76 state := &surface{server: c.server, bufferScale: 1}
77 if w, err := wayland.NewSurface(r.Client(), r.Version(), id, state); err == nil {77 if w, err := wayland.NewSurface(r.Client(), r.Version(), id, state); err == nil {
78+ c.server.nextSurface++
79+ state.identity = c.server.nextSurface
78 state.wl = w80 state.wl = w
79 c.server.surfaces[w.Resource] = state81 c.server.surfaces[w.Resource] = state
80 state.sendScale()82 state.sendScale()
@@ -118,8 +120,9 @@ func (c subcompositor) GetSubsurface(r *wayland.Subcompositor, id uint32, w, par
118 }120 }
119 state.kind = roleSubsurface121 state.kind = roleSubsurface
120 state.role = func(bool) {}122 state.role = func(bool) {}
121- state.sub = subState{role: sub, parent: up, children: state.sub.children}123+ state.sub = subState{role: sub, parent: up, children: state.sub.children, layout: state.sub.layout, pendingLayout: state.sub.pendingLayout, synced: true}
122 up.sub.children = append(up.sub.children, state)124 up.sub.children = append(up.sub.children, state)
125+ up.sub.pendingLayout = append(append([]childLayout(nil), up.sub.pendingLayout...), childLayout{child: state})
123 // The child follows its root window's output and scale.126 // The child follows its root window's output and scale.
124 state.sendScale()127 state.sendScale()
125 sub.OnDestroy = func() {128 sub.OnDestroy = func() {
@@ -138,7 +141,18 @@ type subsurface struct{ surface *surface }
138 141
139 func (subsurface) Destroy(*wayland.Subsurface) {}142 func (subsurface) Destroy(*wayland.Subsurface) {}
140 func (s subsurface) SetPosition(_ *wayland.Subsurface, x, y int32) {143 func (s subsurface) SetPosition(_ *wayland.Subsurface, x, y int32) {
141- s.surface.sub.pendX, s.surface.sub.pendY, s.surface.sub.moved = int(x), int(y), true144+ p := s.surface.sub.parent
145+ if p == nil {
146+ return
147+ }
148+ p.sub.pendingLayout = append([]childLayout(nil), p.sub.pendingLayout...)
149+ for i := range p.sub.pendingLayout {
150+ item := &p.sub.pendingLayout[i]
151+ if item.child == s.surface {
152+ item.x, item.y = int(x), int(y)
153+ break
154+ }
155+ }
142 }156 }
143 func (s subsurface) PlaceAbove(r *wayland.Subsurface, sibling *wayland.Surface) {157 func (s subsurface) PlaceAbove(r *wayland.Subsurface, sibling *wayland.Surface) {
144 s.restack(r, sibling, true)158 s.restack(r, sibling, true)
@@ -146,11 +160,16 @@ func (s subsurface) PlaceAbove(r *wayland.Subsurface, sibling *wayland.Surface)
146 func (s subsurface) PlaceBelow(r *wayland.Subsurface, sibling *wayland.Surface) {160 func (s subsurface) PlaceBelow(r *wayland.Subsurface, sibling *wayland.Surface) {
147 s.restack(r, sibling, false)161 s.restack(r, sibling, false)
148 }162 }
149-func (subsurface) SetSync(*wayland.Subsurface) {}163+func (s subsurface) SetSync(*wayland.Subsurface) { s.surface.sub.synced = true }
150-func (subsurface) SetDesync(*wayland.Subsurface) {}164+func (s subsurface) SetDesync(*wayland.Subsurface) {
165+ was := s.surface.effectivelySynced()
166+ s.surface.sub.synced = false
167+ if was && !s.surface.effectivelySynced() {
168+ s.surface.flushDesync()
169+ }
170+}
151 171
152-// restack moves the surface next to a sibling or its parent. The order172+// restack changes only the pending layout of the parent.
153-// applies at once rather than on the parent commit.
154 func (s subsurface) restack(r *wayland.Subsurface, sibling *wayland.Surface, above bool) {173 func (s subsurface) restack(r *wayland.Subsurface, sibling *wayland.Surface, above bool) {
155 me := s.surface174 me := s.surface
156 p := me.sub.parent175 p := me.sub.parent
@@ -162,54 +181,41 @@ func (s subsurface) restack(r *wayland.Subsurface, sibling *wayland.Surface, abo
162 r.PostError(uint32(wayland.SubsurfaceErrorBadSurface), "not a sibling or the parent")181 r.PostError(uint32(wayland.SubsurfaceErrorBadSurface), "not a sibling or the parent")
163 return182 return
164 }183 }
165- list := p.sub.children184+ list := append([]childLayout(nil), p.sub.pendingLayout...)
166- for i, ch := range list {185+ var item childLayout
167- if ch == me {186+ for _, entry := range list {
168- list = append(list[:i:i], list[i+1:]...)187+ if entry.child == me {
188+ item = entry
169 break189 break
170 }190 }
171 }191 }
192+ list = removeLayout(list, me)
193+ at := len(list)
172 if other == p {194 if other == p {
173- // Relative to the parent: just above it or just below it.195+ item.below = !above
174- me.sub.below = !above
175 if above {196 if above {
176- list = append([]*surface{me}, list...)197+ for i, entry := range list {
177- } else {198+ if !entry.below {
178- list = append(list, me)199+ at = i
200+ break
201+ }
202+ }
179 }203 }
180- // Keep below children first so the flattening stays ordered.
181- sortBelowFirst(list)
182 } else {204 } else {
183- me.sub.below = other.sub.below205+ for i, entry := range list {
184- at := len(list)206+ if entry.child == other {
185- for i, ch := range list {207+ at, item.below = i, entry.below
186- if ch == other {
187- at = i
188 if above {208 if above {
189 at++209 at++
190 }210 }
191 break211 break
192 }212 }
193 }213 }
194- list = append(list[:at:at], append([]*surface{me}, list[at:]...)...)
195- }
196- p.sub.children = list
197- me.redraw()
198-}
199-
200-// sortBelowFirst moves children below their parent ahead of those above,
201-// keeping each group's order.
202-func sortBelowFirst(list []*surface) {
203- below := make([]*surface, 0, len(list))
204- above := make([]*surface, 0, len(list))
205- for _, ch := range list {
206- if ch.sub.below {
207- below = append(below, ch)
208- } else {
209- above = append(above, ch)
210- }
211 }214 }
212- copy(list, append(below, above...))215+ list = append(list, childLayout{})
216+ copy(list[at+1:], list[at:])
217+ list[at] = item
218+ p.sub.pendingLayout = list
213 }219 }
214 220
215 // region keeps the bounding box of the rectangles added: pointer221 // region keeps the bounding box of the rectangles added: pointer
M internal/adapters/wayland/presentation.go +6 -0
@@ -21,6 +21,12 @@ const presentationVersion = 2
2121
2222 // feedbackWait is a committed feedback: presented once a flip on the
2323 // surface's output shows content seq of window win.
24+type graphFeedback struct {
25+ surf *surface
26+ pending []*presentationtime.WpPresentationFeedback
27+ fresh bool
28+}
29+
2430 type feedbackWait struct {
2531 fb *presentationtime.WpPresentationFeedback
2632 surf *surface
@@ -21,6 +21,12 @@ const presentationVersion = 2
21 21
22 // feedbackWait is a committed feedback: presented once a flip on the22 // feedbackWait is a committed feedback: presented once a flip on the
23 // surface's output shows content seq of window win.23 // surface's output shows content seq of window win.
24+type graphFeedback struct {
25+ surf *surface
26+ pending []*presentationtime.WpPresentationFeedback
27+ fresh bool
28+}
29+
24 type feedbackWait struct {30 type feedbackWait struct {
25 fb *presentationtime.WpPresentationFeedback31 fb *presentationtime.WpPresentationFeedback
26 surf *surface32 surf *surface
M internal/adapters/wayland/release.go +20 -1
@@ -105,7 +105,7 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo
105105 }
106106 kept := s.held[:0]
107107 for _, h := range s.held {
108- if keepHeld(h, live, now) {
108+ if s.bufferReferenced(h.res) || keepHeld(h, live, now) {
109109 kept = append(kept, h)
110110 continue
111111 }
@@ -116,6 +116,25 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo
116116 return len(s.held) > 0
117117 }
118118
119+// bufferReferenced checks queued, cached and pending commits on the owner
120+// goroutine. Never return a buffer still promised to a later commit.
121+func (s *Server) bufferReferenced(b *wayland.Buffer) bool {
122+ if b == nil {
123+ return false
124+ }
125+ for _, surf := range s.surfaces {
126+ if surf.attached && sameBuffer(surf.pending, b) {
127+ return true
128+ }
129+ for _, u := range surf.queue {
130+ if u.attached && sameBuffer(u.buffer, b) {
131+ return true
132+ }
133+ }
134+ }
135+ return false
136+}
137+
119138 // keepHeld reports whether an output may still read a held buffer: one
120139 // scans it out or queued it, or one has not got a later content of its
121140 // window yet and heldTimeout has not passed.
@@ -105,7 +105,7 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo
105 }105 }
106 kept := s.held[:0]106 kept := s.held[:0]
107 for _, h := range s.held {107 for _, h := range s.held {
108- if keepHeld(h, live, now) {108+ if s.bufferReferenced(h.res) || keepHeld(h, live, now) {
109 kept = append(kept, h)109 kept = append(kept, h)
110 continue110 continue
111 }111 }
@@ -116,6 +116,25 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo
116 return len(s.held) > 0116 return len(s.held) > 0
117 }117 }
118 118
119+// bufferReferenced checks queued, cached and pending commits on the owner
120+// goroutine. Never return a buffer still promised to a later commit.
121+func (s *Server) bufferReferenced(b *wayland.Buffer) bool {
122+ if b == nil {
123+ return false
124+ }
125+ for _, surf := range s.surfaces {
126+ if surf.attached && sameBuffer(surf.pending, b) {
127+ return true
128+ }
129+ for _, u := range surf.queue {
130+ if u.attached && sameBuffer(u.buffer, b) {
131+ return true
132+ }
133+ }
134+ }
135+ return false
136+}
137+
119 // keepHeld reports whether an output may still read a held buffer: one138 // keepHeld reports whether an output may still read a held buffer: one
120 // scans it out or queued it, or one has not got a later content of its139 // scans it out or queued it, or one has not got a later content of its
121 // window yet and heldTimeout has not passed.140 // window yet and heldTimeout has not passed.
M internal/adapters/wayland/release_test.go +100 -0
@@ -5,8 +5,50 @@ import (
55 "time"
66
77 "github.com/bnema/neferwl/internal/ports"
8+ "github.com/bnema/purego-libwayland/protocol/fifo"
9+ "github.com/bnema/purego-libwayland/protocol/wayland"
810 )
911
12+// A cached child buffer is not released until its role is destroyed;
13+// nothing in the output could have sampled an unpublished commit.
14+func TestCachedSubsurfaceBufferReleaseOnRoleDestroy(t *testing.T) {
15+ s, events, _, contents, dir := contentServer(t)
16+ c := protocolClient(t, s, dir)
17+ _, root, _ := surfaceMapper(t, c, events)()
18+ drainContents(contents)
19+ comp := bindProtocol(t, c, "wl_compositor")
20+ subc := bindProtocol(t, c, "wl_subcompositor")
21+ child := c.AllocateID()
22+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
23+ registerProtocol(t, c, child)
24+ sub := c.AllocateID()
25+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
26+ registerProtocol(t, c, sub)
27+ b := shmBuffer(t, c)
28+ p := &syncReleaseProxy{released: make(chan struct{}, 4)}
29+ p.SetID(b)
30+ c.Context().Register(p)
31+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
32+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
33+ if err := c.Roundtrip(); err != nil {
34+ t.Fatal(err)
35+ }
36+ select {
37+ case <-p.released:
38+ t.Fatal("cached child buffer released before parent commit")
39+ case <-time.After(40 * time.Millisecond):
40+ }
41+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestDestroy)
42+ if err := c.Roundtrip(); err != nil {
43+ t.Fatal(err)
44+ }
45+ select {
46+ case <-p.released:
47+ case <-time.After(2 * time.Second):
48+ t.Fatal("dropped child buffer never released")
49+ }
50+}
51+
1052 func TestKeepHeld(t *testing.T) {
1153 now := time.Unix(100, 0)
1254 dma := heldBuffer{window: 1, id: 7, after: 5, at: now}
@@ -41,3 +83,61 @@ func TestKeepHeld(t *testing.T) {
4183 }
4284 }
4385 }
86+
87+// A later queued use of the same wl_buffer still owns it after the output
88+// acknowledges the intervening commit.
89+func TestHeldBufferRequeuedBeforeOutputAck(t *testing.T) {
90+ s, events, commands, contents, dir := contentServer(t)
91+ c := protocolClient(t, s, dir)
92+ w, root, _ := surfaceMapper(t, c, events)()
93+ commands <- ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: "HEADLESS-1"}
94+ if err := c.Roundtrip(); err != nil {
95+ t.Fatal(err)
96+ }
97+ manager := bindProtocol(t, c, "wp_fifo_manager_v1")
98+ f := c.AllocateID()
99+ registerProtocol(t, c, f)
100+ requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, root)
101+ b, other := shmBuffer(t, c), shmBuffer(t, c)
102+ p := &syncReleaseProxy{released: make(chan struct{}, 4)}
103+ p.SetID(b)
104+ c.Context().Register(p)
105+ attach := func(buf uint32) {
106+ requestProtocol(t, c, root, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
107+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
108+ }
109+ attach(b)
110+ if err := c.Roundtrip(); err != nil {
111+ t.Fatal(err)
112+ }
113+ drainContents(contents)
114+ requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier)
115+ attach(other)
116+ requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier)
117+ attach(b)
118+ if err := c.Roundtrip(); err != nil {
119+ t.Fatal(err)
120+ }
121+ s.display.Do(func() {
122+ surf := s.windows[w.ID].xdg.surface
123+ if len(surf.queue) != 1 || surf.queue[0].buffer == nil || sameBuffer(surf.queue[0].buffer, surf.current) || len(s.held) == 0 {
124+ t.Errorf("expected replaced buffer and queued reuse: queue=%d held=%d", len(surf.queue), len(s.held))
125+ }
126+ s.reports["HEADLESS-1"] = ports.OutputPresented{Output: "HEADLESS-1", Seen: map[ports.WindowID]uint64{w.ID: ^uint64(0)}}
127+ s.releaseHeld(time.Now(), nil)
128+ if len(s.held) == 0 {
129+ t.Error("released held buffer despite queued reuse")
130+ }
131+ surf.barrier = false
132+ s.tickFifo(time.Now(), nil)
133+ s.releaseHeld(time.Now().Add(heldTimeout), nil)
134+ if len(s.held) != 0 {
135+ t.Error("held buffer not released after queued reuse applied")
136+ }
137+ })
138+ select {
139+ case <-p.released:
140+ t.Fatal("queued buffer released after output ack")
141+ case <-time.After(40 * time.Millisecond):
142+ }
143+}
@@ -5,8 +5,50 @@ import (
5 "time"5 "time"
6 6
7 "github.com/bnema/neferwl/internal/ports"7 "github.com/bnema/neferwl/internal/ports"
8+ "github.com/bnema/purego-libwayland/protocol/fifo"
9+ "github.com/bnema/purego-libwayland/protocol/wayland"
8 )10 )
9 11
12+// A cached child buffer is not released until its role is destroyed;
13+// nothing in the output could have sampled an unpublished commit.
14+func TestCachedSubsurfaceBufferReleaseOnRoleDestroy(t *testing.T) {
15+ s, events, _, contents, dir := contentServer(t)
16+ c := protocolClient(t, s, dir)
17+ _, root, _ := surfaceMapper(t, c, events)()
18+ drainContents(contents)
19+ comp := bindProtocol(t, c, "wl_compositor")
20+ subc := bindProtocol(t, c, "wl_subcompositor")
21+ child := c.AllocateID()
22+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
23+ registerProtocol(t, c, child)
24+ sub := c.AllocateID()
25+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
26+ registerProtocol(t, c, sub)
27+ b := shmBuffer(t, c)
28+ p := &syncReleaseProxy{released: make(chan struct{}, 4)}
29+ p.SetID(b)
30+ c.Context().Register(p)
31+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
32+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
33+ if err := c.Roundtrip(); err != nil {
34+ t.Fatal(err)
35+ }
36+ select {
37+ case <-p.released:
38+ t.Fatal("cached child buffer released before parent commit")
39+ case <-time.After(40 * time.Millisecond):
40+ }
41+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestDestroy)
42+ if err := c.Roundtrip(); err != nil {
43+ t.Fatal(err)
44+ }
45+ select {
46+ case <-p.released:
47+ case <-time.After(2 * time.Second):
48+ t.Fatal("dropped child buffer never released")
49+ }
50+}
51+
10 func TestKeepHeld(t *testing.T) {52 func TestKeepHeld(t *testing.T) {
11 now := time.Unix(100, 0)53 now := time.Unix(100, 0)
12 dma := heldBuffer{window: 1, id: 7, after: 5, at: now}54 dma := heldBuffer{window: 1, id: 7, after: 5, at: now}
@@ -41,3 +83,61 @@ func TestKeepHeld(t *testing.T) {
41 }83 }
42 }84 }
43 }85 }
86+
87+// A later queued use of the same wl_buffer still owns it after the output
88+// acknowledges the intervening commit.
89+func TestHeldBufferRequeuedBeforeOutputAck(t *testing.T) {
90+ s, events, commands, contents, dir := contentServer(t)
91+ c := protocolClient(t, s, dir)
92+ w, root, _ := surfaceMapper(t, c, events)()
93+ commands <- ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: "HEADLESS-1"}
94+ if err := c.Roundtrip(); err != nil {
95+ t.Fatal(err)
96+ }
97+ manager := bindProtocol(t, c, "wp_fifo_manager_v1")
98+ f := c.AllocateID()
99+ registerProtocol(t, c, f)
100+ requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, root)
101+ b, other := shmBuffer(t, c), shmBuffer(t, c)
102+ p := &syncReleaseProxy{released: make(chan struct{}, 4)}
103+ p.SetID(b)
104+ c.Context().Register(p)
105+ attach := func(buf uint32) {
106+ requestProtocol(t, c, root, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
107+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
108+ }
109+ attach(b)
110+ if err := c.Roundtrip(); err != nil {
111+ t.Fatal(err)
112+ }
113+ drainContents(contents)
114+ requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier)
115+ attach(other)
116+ requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier)
117+ attach(b)
118+ if err := c.Roundtrip(); err != nil {
119+ t.Fatal(err)
120+ }
121+ s.display.Do(func() {
122+ surf := s.windows[w.ID].xdg.surface
123+ if len(surf.queue) != 1 || surf.queue[0].buffer == nil || sameBuffer(surf.queue[0].buffer, surf.current) || len(s.held) == 0 {
124+ t.Errorf("expected replaced buffer and queued reuse: queue=%d held=%d", len(surf.queue), len(s.held))
125+ }
126+ s.reports["HEADLESS-1"] = ports.OutputPresented{Output: "HEADLESS-1", Seen: map[ports.WindowID]uint64{w.ID: ^uint64(0)}}
127+ s.releaseHeld(time.Now(), nil)
128+ if len(s.held) == 0 {
129+ t.Error("released held buffer despite queued reuse")
130+ }
131+ surf.barrier = false
132+ s.tickFifo(time.Now(), nil)
133+ s.releaseHeld(time.Now().Add(heldTimeout), nil)
134+ if len(s.held) != 0 {
135+ t.Error("held buffer not released after queued reuse applied")
136+ }
137+ })
138+ select {
139+ case <-p.released:
140+ t.Fatal("queued buffer released after output ack")
141+ case <-time.After(40 * time.Millisecond):
142+ }
143+}
M internal/adapters/wayland/scale.go +96 -10
@@ -158,6 +158,7 @@ func (h viewporterHandler) GetViewport(r *viewporter.WpViewporter, id uint32, w
158158 return
159159 }
160160 surf.viewport = vp
161+ vp.resource = res
161162 res.OnDestroy = func() {
162163 // Destroying the viewport resets the size on the next commit.
163164 if surf.viewport == vp {
@@ -166,17 +167,29 @@ func (h viewporterHandler) GetViewport(r *viewporter.WpViewporter, id uint32, w
166167 }
167168 }
168169
169-// viewport holds the pending and committed wp_viewport destination; the
170-// source crop is accepted and ignored (clients use it for video crops).
170+// viewport holds double-buffered crop and destination state.
171171 type viewport struct {
172- surface *surface
173- pendingW, destW int32
174- pendingH, destH int32
175- pendingSet, dest bool
172+ surface *surface
173+ resource *viewporter.WpViewport
174+ pendingW, destW int32
175+ pendingH, destH int32
176+ pendingSet, dest bool
177+ pendingSrc, src [4]server.Fixed
178+ pendingCrop, crop bool
176179 }
177180
178181 func (*viewport) Destroy(*viewporter.WpViewport) {}
179-func (*viewport) SetSource(*viewporter.WpViewport, server.Fixed, server.Fixed, server.Fixed, server.Fixed) {
182+func (v *viewport) SetSource(r *viewporter.WpViewport, x, y, w, h server.Fixed) {
183+ if v.surface.destroyed {
184+ r.PostError(uint32(viewporter.WpViewportErrorNoSurface), "surface destroyed")
185+ return
186+ }
187+ unset := x == -256 && y == -256 && w == -256 && h == -256
188+ if !unset && (x < 0 || y < 0 || w <= 0 || h <= 0) {
189+ r.PostError(uint32(viewporter.WpViewportErrorBadValue), "invalid source rectangle")
190+ return
191+ }
192+ v.pendingSrc, v.pendingCrop = [4]server.Fixed{x, y, w, h}, !unset
180193 }
181194 func (v *viewport) SetDestination(r *viewporter.WpViewport, w, h int32) {
182195 if v.surface.destroyed {
@@ -189,13 +202,86 @@ func (v *viewport) SetDestination(r *viewporter.WpViewport, w, h int32) {
189202 }
190203 v.pendingW, v.pendingH, v.pendingSet = w, h, w != -1
191204 }
192-func (v *viewport) commit() { v.destW, v.destH, v.dest = v.pendingW, v.pendingH, v.pendingSet }
205+func (v *viewport) commit() {
206+ v.destW, v.destH, v.dest = v.pendingW, v.pendingH, v.pendingSet
207+ v.src, v.crop = v.pendingSrc, v.pendingCrop
208+}
209+
210+// source returns buffer-pixel crop coordinates, precomputed at commit.
211+func (s *surface) source(bw, bh int) ([4]float32, bool) {
212+ if s.committedViewport == nil || !s.committedViewport.crop {
213+ return [4]float32{}, false
214+ }
215+ v := s.committedViewport.src
216+ scale := float32(max(s.bufferScale, 1)) / 256
217+ src := [4]float32{float32(v[0]) * scale, float32(v[1]) * scale, float32(v[2]) * scale, float32(v[3]) * scale}
218+ // validateViewport tolerates a rounding overshoot: sample inside the buffer.
219+ src[2] = min(src[2], float32(bw)-src[0])
220+ src[3] = min(src[3], float32(bh)-src[1])
221+ return src, true
222+}
223+
224+func (s *surface) validateViewport(bw, bh int) bool {
225+ v := s.committedViewport
226+ if v == nil || !v.crop {
227+ return true
228+ }
229+ if !v.dest && (v.src[2]%256 != 0 || v.src[3]%256 != 0) {
230+ s.viewportError(viewporter.WpViewportErrorBadSize, "non-integer source size without destination")
231+ return false
232+ }
233+ // int64 prevents overflow when adding signed fixed coordinates.
234+ // Position and size are each rounded to 1/256 by the client, so an edge
235+ // computed in floating point can land up to two units past the buffer:
236+ // that rounding is tolerated, anything larger is a real overflow.
237+ const rounding = 2
238+ if int64(v.src[0])+int64(v.src[2]) > int64(bw)*256/int64(max(s.bufferScale, 1))+rounding ||
239+ int64(v.src[1])+int64(v.src[3]) > int64(bh)*256/int64(max(s.bufferScale, 1))+rounding {
240+ // The error disconnects the client: log every occurrence, with the
241+ // source edges and limits in pixels to show which bound failed.
242+ {
243+ limitW, limitH := float64(bw)/float64(max(s.bufferScale, 1)), float64(bh)/float64(max(s.bufferScale, 1))
244+ surfaceID, viewportID, bufferID := uint32(0), uint32(0), uint32(0)
245+ if s.wl != nil {
246+ surfaceID = s.wl.ID()
247+ }
248+ if s.viewport != nil && s.viewport.resource != nil {
249+ viewportID = s.viewport.resource.ID()
250+ }
251+ if s.current != nil {
252+ bufferID = s.current.ID()
253+ }
254+ s.server.log.Warn().Str("component", "wayland").Uint32("surface", surfaceID).Uint32("viewport", viewportID).
255+ Bool("fresh", s.commitFresh).Bool("retained", !s.commitFresh).Bool("skipped_destroyed", s.commitSkipped).
256+ Uint32("buffer", bufferID).Uint32("queued_buffer", s.queuedBuffer).Int("buffer_width", bw).Int("buffer_height", bh).
257+ Int("committed_scale", s.bufferScale).Int("queued_scale", s.queuedScale).
258+ Ints32("raw_source", []int32{int32(v.src[0]), int32(v.src[1]), int32(v.src[2]), int32(v.src[3])}).
259+ Float64("source_right", float64(int64(v.src[0])+int64(v.src[2]))/256).Float64("limit_w", limitW).
260+ Float64("source_bottom", float64(int64(v.src[1])+int64(v.src[3]))/256).Float64("limit_h", limitH).
261+ Bool("destination_set", v.dest).Msg("viewport source exceeds buffer")
262+ }
263+ s.viewportError(viewporter.WpViewportErrorOutOfBuffer, "source exceeds buffer")
264+ return false
265+ }
266+ return true
267+}
268+
269+func (s *surface) viewportError(code viewporter.WpViewportError, message string) {
270+ if s.viewport != nil && s.viewport.resource != nil {
271+ s.viewport.resource.PostError(uint32(code), message)
272+ }
273+}
193274
194275 // logicalSize is the surface size in logical pixels: the viewport
195276 // destination, else the buffer divided by its integer buffer scale.
196277 func (s *surface) logicalSize(bw, bh int) (int, int) {
197- if s.viewport != nil && s.viewport.dest {
198- return int(s.viewport.destW), int(s.viewport.destH)
278+ if s.committedViewport != nil {
279+ if s.committedViewport.dest {
280+ return int(s.committedViewport.destW), int(s.committedViewport.destH)
281+ }
282+ if s.committedViewport.crop {
283+ return int(s.committedViewport.src[2]) / 256, int(s.committedViewport.src[3]) / 256
284+ }
199285 }
200286 scale := max(s.bufferScale, 1)
201287 return bw / scale, bh / scale
@@ -158,6 +158,7 @@ func (h viewporterHandler) GetViewport(r *viewporter.WpViewporter, id uint32, w
158 return158 return
159 }159 }
160 surf.viewport = vp160 surf.viewport = vp
161+ vp.resource = res
161 res.OnDestroy = func() {162 res.OnDestroy = func() {
162 // Destroying the viewport resets the size on the next commit.163 // Destroying the viewport resets the size on the next commit.
163 if surf.viewport == vp {164 if surf.viewport == vp {
@@ -166,17 +167,29 @@ func (h viewporterHandler) GetViewport(r *viewporter.WpViewporter, id uint32, w
166 }167 }
167 }168 }
168 169
169-// viewport holds the pending and committed wp_viewport destination; the170+// viewport holds double-buffered crop and destination state.
170-// source crop is accepted and ignored (clients use it for video crops).
171 type viewport struct {171 type viewport struct {
172- surface *surface172+ surface *surface
173- pendingW, destW int32173+ resource *viewporter.WpViewport
174- pendingH, destH int32174+ pendingW, destW int32
175- pendingSet, dest bool175+ pendingH, destH int32
176+ pendingSet, dest bool
177+ pendingSrc, src [4]server.Fixed
178+ pendingCrop, crop bool
176 }179 }
177 180
178 func (*viewport) Destroy(*viewporter.WpViewport) {}181 func (*viewport) Destroy(*viewporter.WpViewport) {}
179-func (*viewport) SetSource(*viewporter.WpViewport, server.Fixed, server.Fixed, server.Fixed, server.Fixed) {182+func (v *viewport) SetSource(r *viewporter.WpViewport, x, y, w, h server.Fixed) {
183+ if v.surface.destroyed {
184+ r.PostError(uint32(viewporter.WpViewportErrorNoSurface), "surface destroyed")
185+ return
186+ }
187+ unset := x == -256 && y == -256 && w == -256 && h == -256
188+ if !unset && (x < 0 || y < 0 || w <= 0 || h <= 0) {
189+ r.PostError(uint32(viewporter.WpViewportErrorBadValue), "invalid source rectangle")
190+ return
191+ }
192+ v.pendingSrc, v.pendingCrop = [4]server.Fixed{x, y, w, h}, !unset
180 }193 }
181 func (v *viewport) SetDestination(r *viewporter.WpViewport, w, h int32) {194 func (v *viewport) SetDestination(r *viewporter.WpViewport, w, h int32) {
182 if v.surface.destroyed {195 if v.surface.destroyed {
@@ -189,13 +202,86 @@ func (v *viewport) SetDestination(r *viewporter.WpViewport, w, h int32) {
189 }202 }
190 v.pendingW, v.pendingH, v.pendingSet = w, h, w != -1203 v.pendingW, v.pendingH, v.pendingSet = w, h, w != -1
191 }204 }
192-func (v *viewport) commit() { v.destW, v.destH, v.dest = v.pendingW, v.pendingH, v.pendingSet }205+func (v *viewport) commit() {
206+ v.destW, v.destH, v.dest = v.pendingW, v.pendingH, v.pendingSet
207+ v.src, v.crop = v.pendingSrc, v.pendingCrop
208+}
209+
210+// source returns buffer-pixel crop coordinates, precomputed at commit.
211+func (s *surface) source(bw, bh int) ([4]float32, bool) {
212+ if s.committedViewport == nil || !s.committedViewport.crop {
213+ return [4]float32{}, false
214+ }
215+ v := s.committedViewport.src
216+ scale := float32(max(s.bufferScale, 1)) / 256
217+ src := [4]float32{float32(v[0]) * scale, float32(v[1]) * scale, float32(v[2]) * scale, float32(v[3]) * scale}
218+ // validateViewport tolerates a rounding overshoot: sample inside the buffer.
219+ src[2] = min(src[2], float32(bw)-src[0])
220+ src[3] = min(src[3], float32(bh)-src[1])
221+ return src, true
222+}
223+
224+func (s *surface) validateViewport(bw, bh int) bool {
225+ v := s.committedViewport
226+ if v == nil || !v.crop {
227+ return true
228+ }
229+ if !v.dest && (v.src[2]%256 != 0 || v.src[3]%256 != 0) {
230+ s.viewportError(viewporter.WpViewportErrorBadSize, "non-integer source size without destination")
231+ return false
232+ }
233+ // int64 prevents overflow when adding signed fixed coordinates.
234+ // Position and size are each rounded to 1/256 by the client, so an edge
235+ // computed in floating point can land up to two units past the buffer:
236+ // that rounding is tolerated, anything larger is a real overflow.
237+ const rounding = 2
238+ if int64(v.src[0])+int64(v.src[2]) > int64(bw)*256/int64(max(s.bufferScale, 1))+rounding ||
239+ int64(v.src[1])+int64(v.src[3]) > int64(bh)*256/int64(max(s.bufferScale, 1))+rounding {
240+ // The error disconnects the client: log every occurrence, with the
241+ // source edges and limits in pixels to show which bound failed.
242+ {
243+ limitW, limitH := float64(bw)/float64(max(s.bufferScale, 1)), float64(bh)/float64(max(s.bufferScale, 1))
244+ surfaceID, viewportID, bufferID := uint32(0), uint32(0), uint32(0)
245+ if s.wl != nil {
246+ surfaceID = s.wl.ID()
247+ }
248+ if s.viewport != nil && s.viewport.resource != nil {
249+ viewportID = s.viewport.resource.ID()
250+ }
251+ if s.current != nil {
252+ bufferID = s.current.ID()
253+ }
254+ s.server.log.Warn().Str("component", "wayland").Uint32("surface", surfaceID).Uint32("viewport", viewportID).
255+ Bool("fresh", s.commitFresh).Bool("retained", !s.commitFresh).Bool("skipped_destroyed", s.commitSkipped).
256+ Uint32("buffer", bufferID).Uint32("queued_buffer", s.queuedBuffer).Int("buffer_width", bw).Int("buffer_height", bh).
257+ Int("committed_scale", s.bufferScale).Int("queued_scale", s.queuedScale).
258+ Ints32("raw_source", []int32{int32(v.src[0]), int32(v.src[1]), int32(v.src[2]), int32(v.src[3])}).
259+ Float64("source_right", float64(int64(v.src[0])+int64(v.src[2]))/256).Float64("limit_w", limitW).
260+ Float64("source_bottom", float64(int64(v.src[1])+int64(v.src[3]))/256).Float64("limit_h", limitH).
261+ Bool("destination_set", v.dest).Msg("viewport source exceeds buffer")
262+ }
263+ s.viewportError(viewporter.WpViewportErrorOutOfBuffer, "source exceeds buffer")
264+ return false
265+ }
266+ return true
267+}
268+
269+func (s *surface) viewportError(code viewporter.WpViewportError, message string) {
270+ if s.viewport != nil && s.viewport.resource != nil {
271+ s.viewport.resource.PostError(uint32(code), message)
272+ }
273+}
193 274
194 // logicalSize is the surface size in logical pixels: the viewport275 // logicalSize is the surface size in logical pixels: the viewport
195 // destination, else the buffer divided by its integer buffer scale.276 // destination, else the buffer divided by its integer buffer scale.
196 func (s *surface) logicalSize(bw, bh int) (int, int) {277 func (s *surface) logicalSize(bw, bh int) (int, int) {
197- if s.viewport != nil && s.viewport.dest {278+ if s.committedViewport != nil {
198- return int(s.viewport.destW), int(s.viewport.destH)279+ if s.committedViewport.dest {
280+ return int(s.committedViewport.destW), int(s.committedViewport.destH)
281+ }
282+ if s.committedViewport.crop {
283+ return int(s.committedViewport.src[2]) / 256, int(s.committedViewport.src[3]) / 256
284+ }
199 }285 }
200 scale := max(s.bufferScale, 1)286 scale := max(s.bufferScale, 1)
201 return bw / scale, bh / scale287 return bw / scale, bh / scale
M internal/adapters/wayland/scale_test.go +197 -2
@@ -163,11 +163,55 @@ func TestFractionalScaleAndViewport(t *testing.T) {
163163 if got.Width != 30 || got.Height != 15 || got.LogicalW != 20 || got.LogicalH != 10 {
164164 t.Fatalf("%+v", got)
165165 }
166- return
166+ goto viewportOnly
167167 case <-deadline:
168168 t.Fatal("no content")
169169 }
170170 }
171+viewportOnly:
172+ // Changing only the destination must update content with the current buffer.
173+ for _, size := range [][2]int32{{1, 1}, {7, 3}} {
174+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, size[0], size[1])
175+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
176+ if err := c.Roundtrip(); err != nil {
177+ t.Fatal(err)
178+ }
179+ select {
180+ case got := <-contents:
181+ if got.LogicalW != int(size[0]) || got.LogicalH != int(size[1]) || got.Width != 30 || got.Height != 15 {
182+ t.Fatalf("viewport-only content: %+v", got)
183+ }
184+ case <-time.After(2 * time.Second):
185+ t.Fatal("no viewport-only content")
186+ }
187+ }
188+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(256), int32(256), int32(5*256), int32(4*256))
189+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(-1), int32(-1))
190+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
191+ if err := c.Roundtrip(); err != nil {
192+ t.Fatal(err)
193+ }
194+ select {
195+ case got := <-contents:
196+ if got.LogicalW != 5 || got.LogicalH != 4 || got.Source != [4]float32{1, 1, 5, 4} {
197+ t.Fatalf("source-only crop: %+v", got)
198+ }
199+ case <-time.After(2 * time.Second):
200+ t.Fatal("no crop content")
201+ }
202+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(2), int32(3))
203+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
204+ if err := c.Roundtrip(); err != nil {
205+ t.Fatal(err)
206+ }
207+ select {
208+ case got := <-contents:
209+ if got.LogicalW != 2 || got.LogicalH != 3 || got.Source != [4]float32{1, 1, 5, 4} {
210+ t.Fatalf("source and destination: %+v", got)
211+ }
212+ case <-time.After(2 * time.Second):
213+ t.Fatal("no source and destination content")
214+ }
171215 }
172216
173217 func TestBufferScaleLogicalSize(t *testing.T) {
@@ -175,12 +219,163 @@ func TestBufferScaleLogicalSize(t *testing.T) {
175219 if w, h := st.logicalSize(200, 100); w != 100 || h != 50 {
176220 t.Fatal(w, h)
177221 }
178- st.viewport = &viewport{surface: st, destW: 7, destH: 3, dest: true}
222+ st.committedViewport = &viewport{surface: st, destW: 7, destH: 3, dest: true}
179223 if w, h := st.logicalSize(200, 100); w != 7 || h != 3 {
180224 t.Fatal(w, h)
181225 }
182226 }
183227
228+func TestViewportBadSource(t *testing.T) {
229+ s, _, _, _, dir := contentServer(t)
230+ c := protocolClient(t, s, dir)
231+ comp := bindProtocol(t, c, "wl_compositor")
232+ vpm := bindProtocol(t, c, "wp_viewporter")
233+ surf, vp := c.AllocateID(), c.AllocateID()
234+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
235+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
236+ registerProtocol(t, c, vp)
237+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(-256), int32(0), int32(256), int32(256))
238+ expectProtocolError(t, c, vp, uint32(viewporter.WpViewportErrorBadValue))
239+}
240+
241+func TestViewportApplyErrors(t *testing.T) {
242+ for _, tc := range []struct {
243+ name string
244+ width int32
245+ destination bool
246+ code viewporter.WpViewportError
247+ }{
248+ {"bad_size", 128, false, viewporter.WpViewportErrorBadSize},
249+ {"out_of_buffer", 512, true, viewporter.WpViewportErrorOutOfBuffer},
250+ } {
251+ t.Run(tc.name, func(t *testing.T) {
252+ s, _, _, _, dir := contentServer(t)
253+ c := protocolClient(t, s, dir)
254+ comp := bindProtocol(t, c, "wl_compositor")
255+ vpm := bindProtocol(t, c, "wp_viewporter")
256+ shm := bindProtocol(t, c, "wl_shm")
257+ registerProtocol(t, c, shm)
258+ surf, vp := c.AllocateID(), c.AllocateID()
259+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
260+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
261+ registerProtocol(t, c, vp)
262+ fd, err := unix.MemfdCreate("viewport", 0)
263+ if err != nil {
264+ t.Fatal(err)
265+ }
266+ defer unix.Close(fd)
267+ if err := unix.Ftruncate(fd, 4); err != nil {
268+ t.Fatal(err)
269+ }
270+ pool, buf := c.AllocateID(), c.AllocateID()
271+ registerProtocol(t, c, pool)
272+ registerProtocol(t, c, buf)
273+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
274+ t.Fatal(err)
275+ }
276+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1), int32(1), int32(4), uint32(0))
277+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), tc.width, int32(256))
278+ if tc.destination {
279+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(1), int32(1))
280+ }
281+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
282+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
283+ expectProtocolError(t, c, vp, uint32(tc.code))
284+ })
285+ }
286+}
287+
288+// A client rounds the source position and size to 1/256 independently, so
289+// their sum can pass the buffer edge by a unit: that is accepted. The
290+// values are a real rejected source (y 213.86, height 1946.14 on 2160 rows).
291+// One pixel past the edge is still out_of_buffer.
292+func TestViewportSourceEdgeRounding(t *testing.T) {
293+ for _, tc := range []struct {
294+ name string
295+ y, h int32
296+ error bool
297+ }{
298+ {"rounded edge", 54749, 498212, false},
299+ {"one pixel past", 54749, 498212 + 256, true},
300+ } {
301+ t.Run(tc.name, func(t *testing.T) {
302+ s, events, _, contents, dir := contentServer(t)
303+ c := protocolClient(t, s, dir)
304+ win, surf, _ := surfaceMapper(t, c, events)()
305+ drainContents(contents)
306+ vpm := bindProtocol(t, c, "wp_viewporter")
307+ shm := bindProtocol(t, c, "wl_shm")
308+ registerProtocol(t, c, shm)
309+ vp := c.AllocateID()
310+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
311+ registerProtocol(t, c, vp)
312+ const w, h = 4, 2160
313+ fd, err := unix.MemfdCreate("viewport-edge", 0)
314+ if err != nil {
315+ t.Fatal(err)
316+ }
317+ defer unix.Close(fd)
318+ if err := unix.Ftruncate(fd, w*h*4); err != nil {
319+ t.Fatal(err)
320+ }
321+ pool, buf := c.AllocateID(), c.AllocateID()
322+ registerProtocol(t, c, pool)
323+ registerProtocol(t, c, buf)
324+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(w*h*4)); err != nil {
325+ t.Fatal(err)
326+ }
327+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(w), int32(h), int32(w*4), uint32(0))
328+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(w), int32(100))
329+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), tc.y, int32(w*256), tc.h)
330+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
331+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
332+ if tc.error {
333+ expectProtocolError(t, c, vp, uint32(viewporter.WpViewportErrorOutOfBuffer))
334+ return
335+ }
336+ roundtrip(t, c)
337+ select {
338+ case got := <-contents:
339+ if got.ID != win.ID || got.Source[1]+got.Source[3] > h || got.Source[1] == 0 {
340+ t.Fatalf("published source outside buffer: %+v", got)
341+ }
342+ case <-time.After(2 * time.Second):
343+ t.Fatal("missing viewport publication")
344+ }
345+ })
346+ }
347+}
348+
349+// A NULL attach unmaps the surface: viewporter.xml exempts it from
350+// out_of_buffer, so a source larger than the previous buffer is not an error.
351+// A later commit that keeps the retained buffer is still validated.
352+func TestViewportNullAttachSkipsBufferBounds(t *testing.T) {
353+ s, _, _, _, dir := contentServer(t)
354+ c := protocolClient(t, s, dir)
355+ comp := bindProtocol(t, c, "wl_compositor")
356+ vpm := bindProtocol(t, c, "wp_viewporter")
357+ surf, vp := c.AllocateID(), c.AllocateID()
358+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
359+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
360+ registerProtocol(t, c, vp)
361+ buf := shmBuffer(t, c)
362+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(256), int32(256))
363+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
364+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
365+ roundtrip(t, c)
366+
367+ // Oversized source with a NULL attach: no error.
368+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(512), int32(256))
369+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
370+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
371+ roundtrip(t, c)
372+
373+ // Reattach the 1x1 buffer: the oversized source now applies to it.
374+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
375+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
376+ expectProtocolError(t, c, vp, uint32(viewporter.WpViewportErrorOutOfBuffer))
377+}
378+
184379 func TestViewportBadDestination(t *testing.T) {
185380 s, _, _, _, dir := contentServer(t)
186381 c := protocolClient(t, s, dir)
@@ -163,11 +163,55 @@ func TestFractionalScaleAndViewport(t *testing.T) {
163 if got.Width != 30 || got.Height != 15 || got.LogicalW != 20 || got.LogicalH != 10 {163 if got.Width != 30 || got.Height != 15 || got.LogicalW != 20 || got.LogicalH != 10 {
164 t.Fatalf("%+v", got)164 t.Fatalf("%+v", got)
165 }165 }
166- return166+ goto viewportOnly
167 case <-deadline:167 case <-deadline:
168 t.Fatal("no content")168 t.Fatal("no content")
169 }169 }
170 }170 }
171+viewportOnly:
172+ // Changing only the destination must update content with the current buffer.
173+ for _, size := range [][2]int32{{1, 1}, {7, 3}} {
174+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, size[0], size[1])
175+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
176+ if err := c.Roundtrip(); err != nil {
177+ t.Fatal(err)
178+ }
179+ select {
180+ case got := <-contents:
181+ if got.LogicalW != int(size[0]) || got.LogicalH != int(size[1]) || got.Width != 30 || got.Height != 15 {
182+ t.Fatalf("viewport-only content: %+v", got)
183+ }
184+ case <-time.After(2 * time.Second):
185+ t.Fatal("no viewport-only content")
186+ }
187+ }
188+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(256), int32(256), int32(5*256), int32(4*256))
189+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(-1), int32(-1))
190+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
191+ if err := c.Roundtrip(); err != nil {
192+ t.Fatal(err)
193+ }
194+ select {
195+ case got := <-contents:
196+ if got.LogicalW != 5 || got.LogicalH != 4 || got.Source != [4]float32{1, 1, 5, 4} {
197+ t.Fatalf("source-only crop: %+v", got)
198+ }
199+ case <-time.After(2 * time.Second):
200+ t.Fatal("no crop content")
201+ }
202+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(2), int32(3))
203+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
204+ if err := c.Roundtrip(); err != nil {
205+ t.Fatal(err)
206+ }
207+ select {
208+ case got := <-contents:
209+ if got.LogicalW != 2 || got.LogicalH != 3 || got.Source != [4]float32{1, 1, 5, 4} {
210+ t.Fatalf("source and destination: %+v", got)
211+ }
212+ case <-time.After(2 * time.Second):
213+ t.Fatal("no source and destination content")
214+ }
171 }215 }
172 216
173 func TestBufferScaleLogicalSize(t *testing.T) {217 func TestBufferScaleLogicalSize(t *testing.T) {
@@ -175,12 +219,163 @@ func TestBufferScaleLogicalSize(t *testing.T) {
175 if w, h := st.logicalSize(200, 100); w != 100 || h != 50 {219 if w, h := st.logicalSize(200, 100); w != 100 || h != 50 {
176 t.Fatal(w, h)220 t.Fatal(w, h)
177 }221 }
178- st.viewport = &viewport{surface: st, destW: 7, destH: 3, dest: true}222+ st.committedViewport = &viewport{surface: st, destW: 7, destH: 3, dest: true}
179 if w, h := st.logicalSize(200, 100); w != 7 || h != 3 {223 if w, h := st.logicalSize(200, 100); w != 7 || h != 3 {
180 t.Fatal(w, h)224 t.Fatal(w, h)
181 }225 }
182 }226 }
183 227
228+func TestViewportBadSource(t *testing.T) {
229+ s, _, _, _, dir := contentServer(t)
230+ c := protocolClient(t, s, dir)
231+ comp := bindProtocol(t, c, "wl_compositor")
232+ vpm := bindProtocol(t, c, "wp_viewporter")
233+ surf, vp := c.AllocateID(), c.AllocateID()
234+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
235+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
236+ registerProtocol(t, c, vp)
237+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(-256), int32(0), int32(256), int32(256))
238+ expectProtocolError(t, c, vp, uint32(viewporter.WpViewportErrorBadValue))
239+}
240+
241+func TestViewportApplyErrors(t *testing.T) {
242+ for _, tc := range []struct {
243+ name string
244+ width int32
245+ destination bool
246+ code viewporter.WpViewportError
247+ }{
248+ {"bad_size", 128, false, viewporter.WpViewportErrorBadSize},
249+ {"out_of_buffer", 512, true, viewporter.WpViewportErrorOutOfBuffer},
250+ } {
251+ t.Run(tc.name, func(t *testing.T) {
252+ s, _, _, _, dir := contentServer(t)
253+ c := protocolClient(t, s, dir)
254+ comp := bindProtocol(t, c, "wl_compositor")
255+ vpm := bindProtocol(t, c, "wp_viewporter")
256+ shm := bindProtocol(t, c, "wl_shm")
257+ registerProtocol(t, c, shm)
258+ surf, vp := c.AllocateID(), c.AllocateID()
259+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
260+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
261+ registerProtocol(t, c, vp)
262+ fd, err := unix.MemfdCreate("viewport", 0)
263+ if err != nil {
264+ t.Fatal(err)
265+ }
266+ defer unix.Close(fd)
267+ if err := unix.Ftruncate(fd, 4); err != nil {
268+ t.Fatal(err)
269+ }
270+ pool, buf := c.AllocateID(), c.AllocateID()
271+ registerProtocol(t, c, pool)
272+ registerProtocol(t, c, buf)
273+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
274+ t.Fatal(err)
275+ }
276+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1), int32(1), int32(4), uint32(0))
277+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), tc.width, int32(256))
278+ if tc.destination {
279+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(1), int32(1))
280+ }
281+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
282+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
283+ expectProtocolError(t, c, vp, uint32(tc.code))
284+ })
285+ }
286+}
287+
288+// A client rounds the source position and size to 1/256 independently, so
289+// their sum can pass the buffer edge by a unit: that is accepted. The
290+// values are a real rejected source (y 213.86, height 1946.14 on 2160 rows).
291+// One pixel past the edge is still out_of_buffer.
292+func TestViewportSourceEdgeRounding(t *testing.T) {
293+ for _, tc := range []struct {
294+ name string
295+ y, h int32
296+ error bool
297+ }{
298+ {"rounded edge", 54749, 498212, false},
299+ {"one pixel past", 54749, 498212 + 256, true},
300+ } {
301+ t.Run(tc.name, func(t *testing.T) {
302+ s, events, _, contents, dir := contentServer(t)
303+ c := protocolClient(t, s, dir)
304+ win, surf, _ := surfaceMapper(t, c, events)()
305+ drainContents(contents)
306+ vpm := bindProtocol(t, c, "wp_viewporter")
307+ shm := bindProtocol(t, c, "wl_shm")
308+ registerProtocol(t, c, shm)
309+ vp := c.AllocateID()
310+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
311+ registerProtocol(t, c, vp)
312+ const w, h = 4, 2160
313+ fd, err := unix.MemfdCreate("viewport-edge", 0)
314+ if err != nil {
315+ t.Fatal(err)
316+ }
317+ defer unix.Close(fd)
318+ if err := unix.Ftruncate(fd, w*h*4); err != nil {
319+ t.Fatal(err)
320+ }
321+ pool, buf := c.AllocateID(), c.AllocateID()
322+ registerProtocol(t, c, pool)
323+ registerProtocol(t, c, buf)
324+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(w*h*4)); err != nil {
325+ t.Fatal(err)
326+ }
327+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(w), int32(h), int32(w*4), uint32(0))
328+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(w), int32(100))
329+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), tc.y, int32(w*256), tc.h)
330+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
331+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
332+ if tc.error {
333+ expectProtocolError(t, c, vp, uint32(viewporter.WpViewportErrorOutOfBuffer))
334+ return
335+ }
336+ roundtrip(t, c)
337+ select {
338+ case got := <-contents:
339+ if got.ID != win.ID || got.Source[1]+got.Source[3] > h || got.Source[1] == 0 {
340+ t.Fatalf("published source outside buffer: %+v", got)
341+ }
342+ case <-time.After(2 * time.Second):
343+ t.Fatal("missing viewport publication")
344+ }
345+ })
346+ }
347+}
348+
349+// A NULL attach unmaps the surface: viewporter.xml exempts it from
350+// out_of_buffer, so a source larger than the previous buffer is not an error.
351+// A later commit that keeps the retained buffer is still validated.
352+func TestViewportNullAttachSkipsBufferBounds(t *testing.T) {
353+ s, _, _, _, dir := contentServer(t)
354+ c := protocolClient(t, s, dir)
355+ comp := bindProtocol(t, c, "wl_compositor")
356+ vpm := bindProtocol(t, c, "wp_viewporter")
357+ surf, vp := c.AllocateID(), c.AllocateID()
358+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
359+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
360+ registerProtocol(t, c, vp)
361+ buf := shmBuffer(t, c)
362+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(256), int32(256))
363+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
364+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
365+ roundtrip(t, c)
366+
367+ // Oversized source with a NULL attach: no error.
368+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(512), int32(256))
369+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
370+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
371+ roundtrip(t, c)
372+
373+ // Reattach the 1x1 buffer: the oversized source now applies to it.
374+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
375+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
376+ expectProtocolError(t, c, vp, uint32(viewporter.WpViewportErrorOutOfBuffer))
377+}
378+
184 func TestViewportBadDestination(t *testing.T) {379 func TestViewportBadDestination(t *testing.T) {
185 s, _, _, _, dir := contentServer(t)380 s, _, _, _, dir := contentServer(t)
186 c := protocolClient(t, s, dir)381 c := protocolClient(t, s, dir)
M internal/adapters/wayland/server.go +16 -9
@@ -6,7 +6,6 @@ import (
66 "fmt"
77 "github.com/bnema/purego-libwayland/protocol/relativepointer"
88 "os"
9- "slices"
109 "sync"
1110 "time"
1211
@@ -79,8 +78,12 @@ type Server struct {
7978 started time.Time
8079 // fifoSurfaces have a fifo barrier or queued commits (fifo.go);
8180 // lastFlip is each output's latest page flip.
82- fifoSurfaces map[*surface]struct{}
83- lastFlip map[string]time.Time
81+ fifoSurfaces map[*surface]struct{}
82+ applyingGraph bool
83+ graphDrawn bool
84+ graphFeedback []graphFeedback
85+ readinessGeneration uint64
86+ lastFlip map[string]time.Time
8487 // tokens are the issued xdg-activation tokens (activation.go).
8588 tokens map[string]activationToken
8689 surfaces map[*server.Resource]*surface
@@ -106,6 +109,7 @@ type Server struct {
106109 layers map[ports.WindowID]*layerSurface
107110 nextWindow ports.WindowID
108111 nextPool uint64
112+ nextSurface uint64
109113 focused ports.WindowID
110114 pointerFocus ports.WindowID
111115 wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis
@@ -456,17 +460,20 @@ func (s *Server) emitContent(c ports.SurfaceContent, d damage) {
456460 s.contentMu.Lock()
457461 s.contentSeq[c.ID]++
458462 c.Seq = s.contentSeq[c.ID]
459- // The window's damage history: its last damageHistory contents.
460- h := append(s.damage[c.ID], ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects})
461- if len(h) > damageHistory {
462- h = h[len(h)-damageHistory:]
463- }
463+ // Copy-on-write: previous publications can still be in an output's hands.
464+ old := s.damage[c.ID]
465+ if len(old) >= damageHistory {
466+ old = old[len(old)-damageHistory+1:]
467+ }
468+ h := make([]ports.SeqDamage, len(old)+1)
469+ copy(h, old)
470+ h[len(old)] = ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects}
464471 if c.Empty() {
465472 delete(s.damage, c.ID)
466473 } else {
467474 s.damage[c.ID] = h
468475 }
469- c.DamageHistory = slices.Clone(h)
476+ c.DamageHistory = h
470477 s.contents[c.ID] = c
471478 close(s.contentNotify)
472479 s.contentNotify = make(chan struct{})
@@ -6,7 +6,6 @@ import (
6 "fmt"6 "fmt"
7 "github.com/bnema/purego-libwayland/protocol/relativepointer"7 "github.com/bnema/purego-libwayland/protocol/relativepointer"
8 "os"8 "os"
9- "slices"
10 "sync"9 "sync"
11 "time"10 "time"
12 11
@@ -79,8 +78,12 @@ type Server struct {
79 started time.Time78 started time.Time
80 // fifoSurfaces have a fifo barrier or queued commits (fifo.go);79 // fifoSurfaces have a fifo barrier or queued commits (fifo.go);
81 // lastFlip is each output's latest page flip.80 // lastFlip is each output's latest page flip.
82- fifoSurfaces map[*surface]struct{}81+ fifoSurfaces map[*surface]struct{}
83- lastFlip map[string]time.Time82+ applyingGraph bool
83+ graphDrawn bool
84+ graphFeedback []graphFeedback
85+ readinessGeneration uint64
86+ lastFlip map[string]time.Time
84 // tokens are the issued xdg-activation tokens (activation.go).87 // tokens are the issued xdg-activation tokens (activation.go).
85 tokens map[string]activationToken88 tokens map[string]activationToken
86 surfaces map[*server.Resource]*surface89 surfaces map[*server.Resource]*surface
@@ -106,6 +109,7 @@ type Server struct {
106 layers map[ports.WindowID]*layerSurface109 layers map[ports.WindowID]*layerSurface
107 nextWindow ports.WindowID110 nextWindow ports.WindowID
108 nextPool uint64111 nextPool uint64
112+ nextSurface uint64
109 focused ports.WindowID113 focused ports.WindowID
110 pointerFocus ports.WindowID114 pointerFocus ports.WindowID
111 wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis115 wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis
@@ -456,17 +460,20 @@ func (s *Server) emitContent(c ports.SurfaceContent, d damage) {
456 s.contentMu.Lock()460 s.contentMu.Lock()
457 s.contentSeq[c.ID]++461 s.contentSeq[c.ID]++
458 c.Seq = s.contentSeq[c.ID]462 c.Seq = s.contentSeq[c.ID]
459- // The window's damage history: its last damageHistory contents.463+ // Copy-on-write: previous publications can still be in an output's hands.
460- h := append(s.damage[c.ID], ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects})464+ old := s.damage[c.ID]
461- if len(h) > damageHistory {465+ if len(old) >= damageHistory {
462- h = h[len(h)-damageHistory:]466+ old = old[len(old)-damageHistory+1:]
463- }467+ }
468+ h := make([]ports.SeqDamage, len(old)+1)
469+ copy(h, old)
470+ h[len(old)] = ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects}
464 if c.Empty() {471 if c.Empty() {
465 delete(s.damage, c.ID)472 delete(s.damage, c.ID)
466 } else {473 } else {
467 s.damage[c.ID] = h474 s.damage[c.ID] = h
468 }475 }
469- c.DamageHistory = slices.Clone(h)476+ c.DamageHistory = h
470 s.contents[c.ID] = c477 s.contents[c.ID] = c
471 close(s.contentNotify)478 close(s.contentNotify)
472 s.contentNotify = make(chan struct{})479 s.contentNotify = make(chan struct{})
M internal/adapters/wayland/subsurface_test.go +547 -0
@@ -5,11 +5,457 @@ import (
55 "time"
66
77 "github.com/bnema/neferwl/internal/ports"
8+ "github.com/bnema/purego-libwayland/protocol/presentationtime"
9+ "github.com/bnema/purego-libwayland/protocol/viewporter"
810 "github.com/bnema/purego-libwayland/protocol/wayland"
911 "github.com/bnema/purego-libwayland/protocol/xdgshell"
1012 "golang.org/x/sys/unix"
1113 )
1214
15+// A parent's dependency is fixed at the commit request, not when its
16+// acquire point fires. A later child update cannot slip into that graph.
17+func TestSubsurfaceAcquireCapturesChildAtRequest(t *testing.T) {
18+ h := newSyncHarness(t)
19+ h.releases = map[uint32]*syncReleaseProxy{}
20+ c := h.c
21+ comp := bindProtocol(t, c, "wl_compositor")
22+ subc := bindProtocol(t, c, "wl_subcompositor")
23+ child := c.AllocateID()
24+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
25+ registerProtocol(t, c, child)
26+ sub := c.AllocateID()
27+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, h.surf)
28+ registerProtocol(t, c, sub)
29+ a := shmBuffer(t, c)
30+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, a, int32(0), int32(0))
31+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
32+ root := h.dmabuf()
33+ h.commit(root, 1, 2)
34+ // This commit came after the parent request, while it was still waiting.
35+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
36+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
37+ if err := c.Roundtrip(); err != nil {
38+ t.Fatal(err)
39+ }
40+ if got, ok := h.content(40 * time.Millisecond); ok && len(got.Children) != 0 {
41+ t.Fatalf("child published before parent's acquire: %+v", got.Children)
42+ }
43+ h.fire(1)
44+ deadline := time.After(2 * time.Second)
45+ for {
46+ select {
47+ case got := <-h.contents:
48+ if got.ID != h.win || got.DMABuf == nil {
49+ continue
50+ }
51+ if len(got.Children) != 1 || got.Children[0].Width != 1 {
52+ t.Fatalf("parent captured later child update: %+v", got.Children)
53+ }
54+ return
55+ case <-deadline:
56+ t.Fatal("parent graph did not publish")
57+ }
58+ }
59+}
60+
61+// Desync drains cached commits in FIFO order, including an unmap.
62+func TestSubsurfaceDesyncQueuedOrderAndUnmap(t *testing.T) {
63+ s, events, _, contents, dir := contentServer(t)
64+ c := protocolClient(t, s, dir)
65+ w, root, _ := surfaceMapper(t, c, events)()
66+ drainContents(contents)
67+ comp := bindProtocol(t, c, "wl_compositor")
68+ subc := bindProtocol(t, c, "wl_subcompositor")
69+ child := c.AllocateID()
70+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
71+ registerProtocol(t, c, child)
72+ sub := c.AllocateID()
73+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
74+ registerProtocol(t, c, sub)
75+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
76+ if err := c.Roundtrip(); err != nil {
77+ t.Fatal(err)
78+ }
79+ drainContents(contents)
80+ for _, b := range []uint32{shmBuffer(t, c), shmBuffer(t, c), 0} {
81+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
82+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
83+ }
84+ if err := c.Roundtrip(); err != nil {
85+ t.Fatal(err)
86+ }
87+ select {
88+ case got := <-contents:
89+ t.Fatalf("cached commit published: %+v", got)
90+ case <-time.After(40 * time.Millisecond):
91+ }
92+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetDesync)
93+ if err := c.Roundtrip(); err != nil {
94+ t.Fatal(err)
95+ }
96+ deadline := time.After(2 * time.Second)
97+ for {
98+ select {
99+ case got := <-contents:
100+ if got.Seq < 5 {
101+ continue // the forwarder can coalesce intermediate publications
102+ }
103+ if got.ID != w.ID || got.Seq != 5 || len(got.Children) != 0 {
104+ t.Fatalf("desync did not apply all three commits: %+v", got)
105+ }
106+ return
107+ case <-deadline:
108+ t.Fatal("missing final desync publication")
109+ }
110+ }
111+}
112+
113+// Pending viewport and scale are captured by the child commit.
114+func TestSubsurfaceCachedViewportAndScale(t *testing.T) {
115+ s, events, _, contents, dir := contentServer(t)
116+ c := protocolClient(t, s, dir)
117+ w, root, _ := surfaceMapper(t, c, events)()
118+ drainContents(contents)
119+ comp := bindVersion(t, c, "wl_compositor", 4)
120+ subc := bindProtocol(t, c, "wl_subcompositor")
121+ vpm := bindProtocol(t, c, "wp_viewporter")
122+ child := c.AllocateID()
123+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
124+ registerProtocol(t, c, child)
125+ sub := c.AllocateID()
126+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
127+ registerProtocol(t, c, sub)
128+ vp := c.AllocateID()
129+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, child)
130+ registerProtocol(t, c, vp)
131+ b := shmBuffer(t, c)
132+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(128), int32(128))
133+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(3), int32(4))
134+ requestProtocol(t, c, child, wayland.SurfaceRequestSetBufferScale, int32(2))
135+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
136+ requestProtocol(t, c, child, wayland.SurfaceRequestDamage, int32(0), int32(0), int32(1), int32(1))
137+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
138+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(9), int32(9))
139+ if err := c.Roundtrip(); err != nil {
140+ t.Fatal(err)
141+ }
142+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
143+ if err := c.Roundtrip(); err != nil {
144+ t.Fatal(err)
145+ }
146+ select {
147+ case got := <-contents:
148+ if got.ID != w.ID || len(got.Children) != 1 || got.Children[0].LogicalW != 3 || got.Children[0].LogicalH != 4 || got.Children[0].Source != ([4]float32{0, 0, 1, 1}) {
149+ t.Fatalf("cached viewport: %+v", got)
150+ }
151+ case <-time.After(2 * time.Second):
152+ t.Fatal("no cached content")
153+ }
154+}
155+
156+// Only the final state of a graph is ever sampled by an output. Buffers
157+// replaced inside the graph must be released without an output acknowledgement.
158+func TestSubsurfaceSupersededBufferReleased(t *testing.T) {
159+ s, events, _, contents, dir := contentServer(t)
160+ c := protocolClient(t, s, dir)
161+ _, root, _ := surfaceMapper(t, c, events)()
162+ drainContents(contents)
163+ comp := bindProtocol(t, c, "wl_compositor")
164+ subc := bindProtocol(t, c, "wl_subcompositor")
165+ child := c.AllocateID()
166+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
167+ registerProtocol(t, c, child)
168+ sub := c.AllocateID()
169+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
170+ registerProtocol(t, c, sub)
171+ first := shmBuffer(t, c)
172+ p := &syncReleaseProxy{released: make(chan struct{}, 4)}
173+ p.SetID(first)
174+ c.Context().Register(p)
175+ for _, b := range []uint32{first, shmBuffer(t, c)} {
176+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
177+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
178+ }
179+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
180+ if err := c.Roundtrip(); err != nil {
181+ t.Fatal(err)
182+ }
183+ select {
184+ case <-p.released:
185+ case <-time.After(2 * time.Second):
186+ t.Fatal("superseded unpublished buffer held for output")
187+ }
188+}
189+
190+// A child update captured by a parent stays with that parent after desync.
191+func TestSubsurfaceBoundUpdateStaysBound(t *testing.T) {
192+ s, events, _, contents, dir := contentServer(t)
193+ c := protocolClient(t, s, dir)
194+ _, root, _ := surfaceMapper(t, c, events)()
195+ drainContents(contents)
196+ comp := bindProtocol(t, c, "wl_compositor")
197+ subc := bindProtocol(t, c, "wl_subcompositor")
198+ child := c.AllocateID()
199+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
200+ registerProtocol(t, c, child)
201+ sub := c.AllocateID()
202+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
203+ registerProtocol(t, c, sub)
204+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
205+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
206+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
207+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetDesync)
208+ if err := c.Roundtrip(); err != nil {
209+ t.Fatal(err)
210+ }
211+ select {
212+ case got := <-contents:
213+ if len(got.Children) != 1 {
214+ t.Fatalf("captured update not applied: %+v", got)
215+ }
216+ case <-time.After(2 * time.Second):
217+ t.Fatal("missing parent graph")
218+ }
219+ select {
220+ case got := <-contents:
221+ t.Fatalf("bound update replayed: %+v", got)
222+ case <-time.After(40 * time.Millisecond):
223+ }
224+}
225+
226+// Destroying the parent cannot publish a captured child dependency.
227+func TestSubsurfaceParentDestroyDropsCapturedChild(t *testing.T) {
228+ s, events, _, contents, dir := contentServer(t)
229+ c := protocolClient(t, s, dir)
230+ _, root, _ := surfaceMapper(t, c, events)()
231+ drainContents(contents)
232+ comp := bindProtocol(t, c, "wl_compositor")
233+ subc := bindProtocol(t, c, "wl_subcompositor")
234+ child := c.AllocateID()
235+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
236+ registerProtocol(t, c, child)
237+ sub := c.AllocateID()
238+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
239+ registerProtocol(t, c, sub)
240+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
241+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
242+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
243+ if err := c.Roundtrip(); err != nil {
244+ t.Fatal(err)
245+ }
246+ // That parent commit legitimately publishes the child: consume it
247+ // before destroying the parent, so the check below sees only later output.
248+ select {
249+ case <-contents:
250+ case <-time.After(2 * time.Second):
251+ t.Fatal("parent commit did not publish")
252+ }
253+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
254+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
255+ requestProtocol(t, c, root, wayland.SurfaceRequestDestroy)
256+ if err := c.Roundtrip(); err != nil {
257+ t.Fatal(err)
258+ }
259+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetDesync)
260+ if err := c.Roundtrip(); err != nil {
261+ t.Fatal(err)
262+ }
263+ select {
264+ case got := <-contents:
265+ if len(got.Children) != 0 {
266+ t.Fatalf("destroyed parent published child: %+v", got)
267+ }
268+ case <-time.After(40 * time.Millisecond):
269+ }
270+}
271+
272+// Destroying a queued child removes it from captured parent layouts and
273+// retires its unpublished buffer without publishing its content.
274+func TestSubsurfaceDestroyQueuedChild(t *testing.T) {
275+ s, events, _, contents, dir := contentServer(t)
276+ c := protocolClient(t, s, dir)
277+ w, root, _ := surfaceMapper(t, c, events)()
278+ drainContents(contents)
279+ comp := bindProtocol(t, c, "wl_compositor")
280+ subc := bindProtocol(t, c, "wl_subcompositor")
281+ child := c.AllocateID()
282+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
283+ registerProtocol(t, c, child)
284+ sub := c.AllocateID()
285+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
286+ registerProtocol(t, c, sub)
287+ b := shmBuffer(t, c)
288+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
289+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
290+ requestProtocol(t, c, child, wayland.SurfaceRequestDestroy)
291+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
292+ if err := c.Roundtrip(); err != nil {
293+ t.Fatal(err)
294+ }
295+ deadline := time.After(2 * time.Second)
296+ for {
297+ select {
298+ case got := <-contents:
299+ if got.ID == w.ID && len(got.Children) != 0 {
300+ t.Fatalf("destroyed child published: %+v", got)
301+ }
302+ if got.ID == w.ID {
303+ return
304+ }
305+ case <-deadline:
306+ t.Fatal("no parent update after child destroy")
307+ }
308+ }
309+}
310+
311+// A queued buffer destroyed before its parent commits is skipped with the
312+// crop made for it: the retained buffer keeps its own crop, without error.
313+func TestSubsurfaceDestroyedQueuedBufferKeepsCrop(t *testing.T) {
314+ s, events, _, contents, dir := contentServer(t)
315+ c := protocolClient(t, s, dir)
316+ w, root, _ := surfaceMapper(t, c, events)()
317+ drainContents(contents)
318+ comp := bindProtocol(t, c, "wl_compositor")
319+ subc := bindProtocol(t, c, "wl_subcompositor")
320+ vpm := bindProtocol(t, c, "wp_viewporter")
321+ child := c.AllocateID()
322+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
323+ registerProtocol(t, c, child)
324+ sub := c.AllocateID()
325+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
326+ registerProtocol(t, c, sub)
327+ vp := c.AllocateID()
328+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, child)
329+ registerProtocol(t, c, vp)
330+ small, wide := shmBuffer(t, c), shmBuffer(t, c)
331+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(256), int32(256))
332+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, small, int32(0), int32(0))
333+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
334+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
335+ roundtrip(t, c)
336+ drainContents(contents)
337+
338+ // A 2x1 crop meant for a wider buffer that is destroyed while queued.
339+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(512), int32(256))
340+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, wide, int32(0), int32(0))
341+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
342+ requestProtocol(t, c, wide, wayland.BufferRequestDestroy)
343+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
344+ roundtrip(t, c)
345+ deadline := time.After(2 * time.Second)
346+ for {
347+ select {
348+ case got := <-contents:
349+ if got.ID != w.ID {
350+ continue
351+ }
352+ if len(got.Children) != 1 || got.Children[0].Source != ([4]float32{0, 0, 1, 1}) {
353+ t.Fatalf("retained crop: %+v", got)
354+ }
355+ return
356+ case <-deadline:
357+ t.Fatal("no parent update")
358+ }
359+ }
360+}
361+
362+// A synchronized child cannot publish until a parent commits; position and
363+// order are part of that same parent update.
364+func TestSubsurfaceInitialSyncAndDeferredLayout(t *testing.T) {
365+ s, events, _, contents, dir := contentServer(t)
366+ c := protocolClient(t, s, dir)
367+ rootWindow, root, _ := surfaceMapper(t, c, events)()
368+ drainContents(contents)
369+ comp := bindProtocol(t, c, "wl_compositor")
370+ subc := bindProtocol(t, c, "wl_subcompositor")
371+ child := c.AllocateID()
372+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
373+ registerProtocol(t, c, child)
374+ sub := c.AllocateID()
375+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
376+ registerProtocol(t, c, sub)
377+ buf := shmBuffer(t, c)
378+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
379+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
380+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetPosition, int32(4), int32(5))
381+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestPlaceBelow, root)
382+ if err := c.Roundtrip(); err != nil {
383+ t.Fatal(err)
384+ }
385+ select {
386+ case got := <-contents:
387+ t.Fatalf("child/layout published before parent commit: %+v", got)
388+ case <-time.After(40 * time.Millisecond):
389+ }
390+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
391+ if err := c.Roundtrip(); err != nil {
392+ t.Fatal(err)
393+ }
394+ select {
395+ case got := <-contents:
396+ if got.ID != rootWindow.ID || len(got.Children) != 1 || got.Children[0].X != 4 || got.Children[0].Y != 5 || !got.Children[0].Below {
397+ t.Fatalf("parent publication: %+v", got)
398+ }
399+ case <-time.After(2 * time.Second):
400+ t.Fatal("no parent publication")
401+ }
402+ select {
403+ case got := <-contents:
404+ t.Fatalf("extra publication: %+v", got)
405+ case <-time.After(40 * time.Millisecond):
406+ }
407+}
408+
409+func TestSubsurfaceDesyncFlushesOnlyEffectiveTransition(t *testing.T) {
410+ s, events, _, contents, dir := contentServer(t)
411+ c := protocolClient(t, s, dir)
412+ _, root, _ := surfaceMapper(t, c, events)()
413+ drainContents(contents)
414+ comp := bindProtocol(t, c, "wl_compositor")
415+ subc := bindProtocol(t, c, "wl_subcompositor")
416+ parent, child := c.AllocateID(), c.AllocateID()
417+ for _, id := range []uint32{parent, child} {
418+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, id)
419+ registerProtocol(t, c, id)
420+ }
421+ parentSub, childSub := c.AllocateID(), c.AllocateID()
422+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, parentSub, parent, root)
423+ registerProtocol(t, c, parentSub)
424+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, childSub, child, parent)
425+ registerProtocol(t, c, childSub)
426+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
427+ requestProtocol(t, c, parent, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
428+ requestProtocol(t, c, parent, wayland.SurfaceRequestCommit)
429+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
430+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
431+ requestProtocol(t, c, childSub, wayland.SubsurfaceRequestSetDesync)
432+ if err := c.Roundtrip(); err != nil {
433+ t.Fatal(err)
434+ }
435+ select {
436+ case got := <-contents:
437+ if len(got.Children) != 0 {
438+ t.Fatalf("ineffective desync published: %+v", got.Children)
439+ }
440+ case <-time.After(40 * time.Millisecond):
441+ }
442+ requestProtocol(t, c, parentSub, wayland.SubsurfaceRequestSetDesync)
443+ if err := c.Roundtrip(); err != nil {
444+ t.Fatal(err)
445+ }
446+ deadline := time.After(2 * time.Second)
447+ for {
448+ select {
449+ case got := <-contents:
450+ if len(got.Children) == 2 {
451+ return
452+ }
453+ case <-deadline:
454+ t.Fatal("desync did not flush inherited child update")
455+ }
456+ }
457+}
458+
13459 // A toplevel with a subsurface (how Firefox draws with the GPU) sends one
14460 // content: its own buffer, the child at its committed position and the
15461 // window geometry.
@@ -148,3 +594,104 @@ func TestSubsurfaceRoleOutlivesParent(t *testing.T) {
148594 requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, s2, b, p)
149595 expectProtocolError(t, c, subc, uint32(wayland.SubcompositorErrorBadSurface))
150596 }
597+
598+// A parent graph with two child commits only publishes the final child.
599+func TestSubsurfaceGraphSupersededFeedback(t *testing.T) {
600+ s, events, _, contents, dir := contentServer(t)
601+ c := protocolClient(t, s, dir)
602+ w, root, _ := surfaceMapper(t, c, events)()
603+ s.display.Do(func() { s.windows[w.ID].xdg.window.last.Output = "HEADLESS-1" })
604+ drainContents(contents)
605+ comp := bindProtocol(t, c, "wl_compositor")
606+ subc := bindProtocol(t, c, "wl_subcompositor")
607+ pres := bindProtocol(t, c, "wp_presentation")
608+ registerProtocol(t, c, pres)
609+ child := c.AllocateID()
610+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
611+ registerProtocol(t, c, child)
612+ sub := c.AllocateID()
613+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
614+ registerProtocol(t, c, sub)
615+ first := &feedbackEvents{out: make(chan presentedEvent, 2)}
616+ id := c.AllocateID()
617+ first.SetID(id)
618+ c.Context().Register(first)
619+ requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, child, id)
620+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
621+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
622+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
623+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
624+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
625+ if err := c.Roundtrip(); err != nil {
626+ t.Fatal(err)
627+ }
628+ if err := c.Roundtrip(); err != nil {
629+ t.Fatal(err)
630+ }
631+ select {
632+ case ev := <-first.out:
633+ if !ev.discarded {
634+ t.Fatalf("superseded content presented: %+v", ev)
635+ }
636+ case <-time.After(2 * time.Second):
637+ t.Fatal("superseded feedback not discarded")
638+ }
639+}
640+
641+// A subsurface resource remains live after its parent surface is destroyed.
642+func TestSubsurfacePositionAfterParentDestroy(t *testing.T) {
643+ s, events, _, _, dir := contentServer(t)
644+ c := protocolClient(t, s, dir)
645+ _, root, _ := surfaceMapper(t, c, events)()
646+ comp := bindProtocol(t, c, "wl_compositor")
647+ subc := bindProtocol(t, c, "wl_subcompositor")
648+ child := c.AllocateID()
649+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
650+ registerProtocol(t, c, child)
651+ sub := c.AllocateID()
652+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
653+ registerProtocol(t, c, sub)
654+ requestProtocol(t, c, root, wayland.SurfaceRequestDestroy)
655+ roundtrip(t, c)
656+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetPosition, int32(3), int32(4))
657+ roundtrip(t, c)
658+}
659+
660+// A graph without a changed surface must not republish a mapped tree.
661+func TestSubsurfaceNoopRootCommitDoesNotPublish(t *testing.T) {
662+ s, events, _, contents, dir := contentServer(t)
663+ c := protocolClient(t, s, dir)
664+ _, root, _ := surfaceMapper(t, c, events)()
665+ comp := bindProtocol(t, c, "wl_compositor")
666+ subc := bindProtocol(t, c, "wl_subcompositor")
667+ child := c.AllocateID()
668+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
669+ registerProtocol(t, c, child)
670+ sub := c.AllocateID()
671+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
672+ registerProtocol(t, c, sub)
673+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
674+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
675+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
676+ roundtrip(t, c)
677+ drainContents(contents)
678+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
679+ roundtrip(t, c)
680+ select {
681+ case got := <-contents:
682+ t.Fatalf("no-op published: %+v", got)
683+ case <-time.After(80 * time.Millisecond):
684+ }
685+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
686+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
687+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
688+ roundtrip(t, c)
689+ select {
690+ case got := <-contents:
691+ if len(got.Children) != 1 {
692+ t.Fatalf("child change not published: %+v", got)
693+ }
694+ case <-time.After(2 * time.Second):
695+ t.Fatal("child change did not publish")
696+ }
697+}
@@ -5,11 +5,457 @@ import (
5 "time"5 "time"
6 6
7 "github.com/bnema/neferwl/internal/ports"7 "github.com/bnema/neferwl/internal/ports"
8+ "github.com/bnema/purego-libwayland/protocol/presentationtime"
9+ "github.com/bnema/purego-libwayland/protocol/viewporter"
8 "github.com/bnema/purego-libwayland/protocol/wayland"10 "github.com/bnema/purego-libwayland/protocol/wayland"
9 "github.com/bnema/purego-libwayland/protocol/xdgshell"11 "github.com/bnema/purego-libwayland/protocol/xdgshell"
10 "golang.org/x/sys/unix"12 "golang.org/x/sys/unix"
11 )13 )
12 14
15+// A parent's dependency is fixed at the commit request, not when its
16+// acquire point fires. A later child update cannot slip into that graph.
17+func TestSubsurfaceAcquireCapturesChildAtRequest(t *testing.T) {
18+ h := newSyncHarness(t)
19+ h.releases = map[uint32]*syncReleaseProxy{}
20+ c := h.c
21+ comp := bindProtocol(t, c, "wl_compositor")
22+ subc := bindProtocol(t, c, "wl_subcompositor")
23+ child := c.AllocateID()
24+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
25+ registerProtocol(t, c, child)
26+ sub := c.AllocateID()
27+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, h.surf)
28+ registerProtocol(t, c, sub)
29+ a := shmBuffer(t, c)
30+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, a, int32(0), int32(0))
31+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
32+ root := h.dmabuf()
33+ h.commit(root, 1, 2)
34+ // This commit came after the parent request, while it was still waiting.
35+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
36+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
37+ if err := c.Roundtrip(); err != nil {
38+ t.Fatal(err)
39+ }
40+ if got, ok := h.content(40 * time.Millisecond); ok && len(got.Children) != 0 {
41+ t.Fatalf("child published before parent's acquire: %+v", got.Children)
42+ }
43+ h.fire(1)
44+ deadline := time.After(2 * time.Second)
45+ for {
46+ select {
47+ case got := <-h.contents:
48+ if got.ID != h.win || got.DMABuf == nil {
49+ continue
50+ }
51+ if len(got.Children) != 1 || got.Children[0].Width != 1 {
52+ t.Fatalf("parent captured later child update: %+v", got.Children)
53+ }
54+ return
55+ case <-deadline:
56+ t.Fatal("parent graph did not publish")
57+ }
58+ }
59+}
60+
61+// Desync drains cached commits in FIFO order, including an unmap.
62+func TestSubsurfaceDesyncQueuedOrderAndUnmap(t *testing.T) {
63+ s, events, _, contents, dir := contentServer(t)
64+ c := protocolClient(t, s, dir)
65+ w, root, _ := surfaceMapper(t, c, events)()
66+ drainContents(contents)
67+ comp := bindProtocol(t, c, "wl_compositor")
68+ subc := bindProtocol(t, c, "wl_subcompositor")
69+ child := c.AllocateID()
70+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
71+ registerProtocol(t, c, child)
72+ sub := c.AllocateID()
73+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
74+ registerProtocol(t, c, sub)
75+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
76+ if err := c.Roundtrip(); err != nil {
77+ t.Fatal(err)
78+ }
79+ drainContents(contents)
80+ for _, b := range []uint32{shmBuffer(t, c), shmBuffer(t, c), 0} {
81+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
82+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
83+ }
84+ if err := c.Roundtrip(); err != nil {
85+ t.Fatal(err)
86+ }
87+ select {
88+ case got := <-contents:
89+ t.Fatalf("cached commit published: %+v", got)
90+ case <-time.After(40 * time.Millisecond):
91+ }
92+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetDesync)
93+ if err := c.Roundtrip(); err != nil {
94+ t.Fatal(err)
95+ }
96+ deadline := time.After(2 * time.Second)
97+ for {
98+ select {
99+ case got := <-contents:
100+ if got.Seq < 5 {
101+ continue // the forwarder can coalesce intermediate publications
102+ }
103+ if got.ID != w.ID || got.Seq != 5 || len(got.Children) != 0 {
104+ t.Fatalf("desync did not apply all three commits: %+v", got)
105+ }
106+ return
107+ case <-deadline:
108+ t.Fatal("missing final desync publication")
109+ }
110+ }
111+}
112+
113+// Pending viewport and scale are captured by the child commit.
114+func TestSubsurfaceCachedViewportAndScale(t *testing.T) {
115+ s, events, _, contents, dir := contentServer(t)
116+ c := protocolClient(t, s, dir)
117+ w, root, _ := surfaceMapper(t, c, events)()
118+ drainContents(contents)
119+ comp := bindVersion(t, c, "wl_compositor", 4)
120+ subc := bindProtocol(t, c, "wl_subcompositor")
121+ vpm := bindProtocol(t, c, "wp_viewporter")
122+ child := c.AllocateID()
123+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
124+ registerProtocol(t, c, child)
125+ sub := c.AllocateID()
126+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
127+ registerProtocol(t, c, sub)
128+ vp := c.AllocateID()
129+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, child)
130+ registerProtocol(t, c, vp)
131+ b := shmBuffer(t, c)
132+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(128), int32(128))
133+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(3), int32(4))
134+ requestProtocol(t, c, child, wayland.SurfaceRequestSetBufferScale, int32(2))
135+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
136+ requestProtocol(t, c, child, wayland.SurfaceRequestDamage, int32(0), int32(0), int32(1), int32(1))
137+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
138+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(9), int32(9))
139+ if err := c.Roundtrip(); err != nil {
140+ t.Fatal(err)
141+ }
142+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
143+ if err := c.Roundtrip(); err != nil {
144+ t.Fatal(err)
145+ }
146+ select {
147+ case got := <-contents:
148+ if got.ID != w.ID || len(got.Children) != 1 || got.Children[0].LogicalW != 3 || got.Children[0].LogicalH != 4 || got.Children[0].Source != ([4]float32{0, 0, 1, 1}) {
149+ t.Fatalf("cached viewport: %+v", got)
150+ }
151+ case <-time.After(2 * time.Second):
152+ t.Fatal("no cached content")
153+ }
154+}
155+
156+// Only the final state of a graph is ever sampled by an output. Buffers
157+// replaced inside the graph must be released without an output acknowledgement.
158+func TestSubsurfaceSupersededBufferReleased(t *testing.T) {
159+ s, events, _, contents, dir := contentServer(t)
160+ c := protocolClient(t, s, dir)
161+ _, root, _ := surfaceMapper(t, c, events)()
162+ drainContents(contents)
163+ comp := bindProtocol(t, c, "wl_compositor")
164+ subc := bindProtocol(t, c, "wl_subcompositor")
165+ child := c.AllocateID()
166+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
167+ registerProtocol(t, c, child)
168+ sub := c.AllocateID()
169+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
170+ registerProtocol(t, c, sub)
171+ first := shmBuffer(t, c)
172+ p := &syncReleaseProxy{released: make(chan struct{}, 4)}
173+ p.SetID(first)
174+ c.Context().Register(p)
175+ for _, b := range []uint32{first, shmBuffer(t, c)} {
176+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
177+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
178+ }
179+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
180+ if err := c.Roundtrip(); err != nil {
181+ t.Fatal(err)
182+ }
183+ select {
184+ case <-p.released:
185+ case <-time.After(2 * time.Second):
186+ t.Fatal("superseded unpublished buffer held for output")
187+ }
188+}
189+
190+// A child update captured by a parent stays with that parent after desync.
191+func TestSubsurfaceBoundUpdateStaysBound(t *testing.T) {
192+ s, events, _, contents, dir := contentServer(t)
193+ c := protocolClient(t, s, dir)
194+ _, root, _ := surfaceMapper(t, c, events)()
195+ drainContents(contents)
196+ comp := bindProtocol(t, c, "wl_compositor")
197+ subc := bindProtocol(t, c, "wl_subcompositor")
198+ child := c.AllocateID()
199+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
200+ registerProtocol(t, c, child)
201+ sub := c.AllocateID()
202+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
203+ registerProtocol(t, c, sub)
204+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
205+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
206+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
207+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetDesync)
208+ if err := c.Roundtrip(); err != nil {
209+ t.Fatal(err)
210+ }
211+ select {
212+ case got := <-contents:
213+ if len(got.Children) != 1 {
214+ t.Fatalf("captured update not applied: %+v", got)
215+ }
216+ case <-time.After(2 * time.Second):
217+ t.Fatal("missing parent graph")
218+ }
219+ select {
220+ case got := <-contents:
221+ t.Fatalf("bound update replayed: %+v", got)
222+ case <-time.After(40 * time.Millisecond):
223+ }
224+}
225+
226+// Destroying the parent cannot publish a captured child dependency.
227+func TestSubsurfaceParentDestroyDropsCapturedChild(t *testing.T) {
228+ s, events, _, contents, dir := contentServer(t)
229+ c := protocolClient(t, s, dir)
230+ _, root, _ := surfaceMapper(t, c, events)()
231+ drainContents(contents)
232+ comp := bindProtocol(t, c, "wl_compositor")
233+ subc := bindProtocol(t, c, "wl_subcompositor")
234+ child := c.AllocateID()
235+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
236+ registerProtocol(t, c, child)
237+ sub := c.AllocateID()
238+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
239+ registerProtocol(t, c, sub)
240+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
241+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
242+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
243+ if err := c.Roundtrip(); err != nil {
244+ t.Fatal(err)
245+ }
246+ // That parent commit legitimately publishes the child: consume it
247+ // before destroying the parent, so the check below sees only later output.
248+ select {
249+ case <-contents:
250+ case <-time.After(2 * time.Second):
251+ t.Fatal("parent commit did not publish")
252+ }
253+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
254+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
255+ requestProtocol(t, c, root, wayland.SurfaceRequestDestroy)
256+ if err := c.Roundtrip(); err != nil {
257+ t.Fatal(err)
258+ }
259+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetDesync)
260+ if err := c.Roundtrip(); err != nil {
261+ t.Fatal(err)
262+ }
263+ select {
264+ case got := <-contents:
265+ if len(got.Children) != 0 {
266+ t.Fatalf("destroyed parent published child: %+v", got)
267+ }
268+ case <-time.After(40 * time.Millisecond):
269+ }
270+}
271+
272+// Destroying a queued child removes it from captured parent layouts and
273+// retires its unpublished buffer without publishing its content.
274+func TestSubsurfaceDestroyQueuedChild(t *testing.T) {
275+ s, events, _, contents, dir := contentServer(t)
276+ c := protocolClient(t, s, dir)
277+ w, root, _ := surfaceMapper(t, c, events)()
278+ drainContents(contents)
279+ comp := bindProtocol(t, c, "wl_compositor")
280+ subc := bindProtocol(t, c, "wl_subcompositor")
281+ child := c.AllocateID()
282+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
283+ registerProtocol(t, c, child)
284+ sub := c.AllocateID()
285+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
286+ registerProtocol(t, c, sub)
287+ b := shmBuffer(t, c)
288+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
289+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
290+ requestProtocol(t, c, child, wayland.SurfaceRequestDestroy)
291+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
292+ if err := c.Roundtrip(); err != nil {
293+ t.Fatal(err)
294+ }
295+ deadline := time.After(2 * time.Second)
296+ for {
297+ select {
298+ case got := <-contents:
299+ if got.ID == w.ID && len(got.Children) != 0 {
300+ t.Fatalf("destroyed child published: %+v", got)
301+ }
302+ if got.ID == w.ID {
303+ return
304+ }
305+ case <-deadline:
306+ t.Fatal("no parent update after child destroy")
307+ }
308+ }
309+}
310+
311+// A queued buffer destroyed before its parent commits is skipped with the
312+// crop made for it: the retained buffer keeps its own crop, without error.
313+func TestSubsurfaceDestroyedQueuedBufferKeepsCrop(t *testing.T) {
314+ s, events, _, contents, dir := contentServer(t)
315+ c := protocolClient(t, s, dir)
316+ w, root, _ := surfaceMapper(t, c, events)()
317+ drainContents(contents)
318+ comp := bindProtocol(t, c, "wl_compositor")
319+ subc := bindProtocol(t, c, "wl_subcompositor")
320+ vpm := bindProtocol(t, c, "wp_viewporter")
321+ child := c.AllocateID()
322+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
323+ registerProtocol(t, c, child)
324+ sub := c.AllocateID()
325+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
326+ registerProtocol(t, c, sub)
327+ vp := c.AllocateID()
328+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, child)
329+ registerProtocol(t, c, vp)
330+ small, wide := shmBuffer(t, c), shmBuffer(t, c)
331+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(256), int32(256))
332+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, small, int32(0), int32(0))
333+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
334+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
335+ roundtrip(t, c)
336+ drainContents(contents)
337+
338+ // A 2x1 crop meant for a wider buffer that is destroyed while queued.
339+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(512), int32(256))
340+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, wide, int32(0), int32(0))
341+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
342+ requestProtocol(t, c, wide, wayland.BufferRequestDestroy)
343+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
344+ roundtrip(t, c)
345+ deadline := time.After(2 * time.Second)
346+ for {
347+ select {
348+ case got := <-contents:
349+ if got.ID != w.ID {
350+ continue
351+ }
352+ if len(got.Children) != 1 || got.Children[0].Source != ([4]float32{0, 0, 1, 1}) {
353+ t.Fatalf("retained crop: %+v", got)
354+ }
355+ return
356+ case <-deadline:
357+ t.Fatal("no parent update")
358+ }
359+ }
360+}
361+
362+// A synchronized child cannot publish until a parent commits; position and
363+// order are part of that same parent update.
364+func TestSubsurfaceInitialSyncAndDeferredLayout(t *testing.T) {
365+ s, events, _, contents, dir := contentServer(t)
366+ c := protocolClient(t, s, dir)
367+ rootWindow, root, _ := surfaceMapper(t, c, events)()
368+ drainContents(contents)
369+ comp := bindProtocol(t, c, "wl_compositor")
370+ subc := bindProtocol(t, c, "wl_subcompositor")
371+ child := c.AllocateID()
372+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
373+ registerProtocol(t, c, child)
374+ sub := c.AllocateID()
375+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
376+ registerProtocol(t, c, sub)
377+ buf := shmBuffer(t, c)
378+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
379+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
380+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetPosition, int32(4), int32(5))
381+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestPlaceBelow, root)
382+ if err := c.Roundtrip(); err != nil {
383+ t.Fatal(err)
384+ }
385+ select {
386+ case got := <-contents:
387+ t.Fatalf("child/layout published before parent commit: %+v", got)
388+ case <-time.After(40 * time.Millisecond):
389+ }
390+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
391+ if err := c.Roundtrip(); err != nil {
392+ t.Fatal(err)
393+ }
394+ select {
395+ case got := <-contents:
396+ if got.ID != rootWindow.ID || len(got.Children) != 1 || got.Children[0].X != 4 || got.Children[0].Y != 5 || !got.Children[0].Below {
397+ t.Fatalf("parent publication: %+v", got)
398+ }
399+ case <-time.After(2 * time.Second):
400+ t.Fatal("no parent publication")
401+ }
402+ select {
403+ case got := <-contents:
404+ t.Fatalf("extra publication: %+v", got)
405+ case <-time.After(40 * time.Millisecond):
406+ }
407+}
408+
409+func TestSubsurfaceDesyncFlushesOnlyEffectiveTransition(t *testing.T) {
410+ s, events, _, contents, dir := contentServer(t)
411+ c := protocolClient(t, s, dir)
412+ _, root, _ := surfaceMapper(t, c, events)()
413+ drainContents(contents)
414+ comp := bindProtocol(t, c, "wl_compositor")
415+ subc := bindProtocol(t, c, "wl_subcompositor")
416+ parent, child := c.AllocateID(), c.AllocateID()
417+ for _, id := range []uint32{parent, child} {
418+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, id)
419+ registerProtocol(t, c, id)
420+ }
421+ parentSub, childSub := c.AllocateID(), c.AllocateID()
422+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, parentSub, parent, root)
423+ registerProtocol(t, c, parentSub)
424+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, childSub, child, parent)
425+ registerProtocol(t, c, childSub)
426+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
427+ requestProtocol(t, c, parent, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
428+ requestProtocol(t, c, parent, wayland.SurfaceRequestCommit)
429+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
430+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
431+ requestProtocol(t, c, childSub, wayland.SubsurfaceRequestSetDesync)
432+ if err := c.Roundtrip(); err != nil {
433+ t.Fatal(err)
434+ }
435+ select {
436+ case got := <-contents:
437+ if len(got.Children) != 0 {
438+ t.Fatalf("ineffective desync published: %+v", got.Children)
439+ }
440+ case <-time.After(40 * time.Millisecond):
441+ }
442+ requestProtocol(t, c, parentSub, wayland.SubsurfaceRequestSetDesync)
443+ if err := c.Roundtrip(); err != nil {
444+ t.Fatal(err)
445+ }
446+ deadline := time.After(2 * time.Second)
447+ for {
448+ select {
449+ case got := <-contents:
450+ if len(got.Children) == 2 {
451+ return
452+ }
453+ case <-deadline:
454+ t.Fatal("desync did not flush inherited child update")
455+ }
456+ }
457+}
458+
13 // A toplevel with a subsurface (how Firefox draws with the GPU) sends one459 // A toplevel with a subsurface (how Firefox draws with the GPU) sends one
14 // content: its own buffer, the child at its committed position and the460 // content: its own buffer, the child at its committed position and the
15 // window geometry.461 // window geometry.
@@ -148,3 +594,104 @@ func TestSubsurfaceRoleOutlivesParent(t *testing.T) {
148 requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, s2, b, p)594 requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, s2, b, p)
149 expectProtocolError(t, c, subc, uint32(wayland.SubcompositorErrorBadSurface))595 expectProtocolError(t, c, subc, uint32(wayland.SubcompositorErrorBadSurface))
150 }596 }
597+
598+// A parent graph with two child commits only publishes the final child.
599+func TestSubsurfaceGraphSupersededFeedback(t *testing.T) {
600+ s, events, _, contents, dir := contentServer(t)
601+ c := protocolClient(t, s, dir)
602+ w, root, _ := surfaceMapper(t, c, events)()
603+ s.display.Do(func() { s.windows[w.ID].xdg.window.last.Output = "HEADLESS-1" })
604+ drainContents(contents)
605+ comp := bindProtocol(t, c, "wl_compositor")
606+ subc := bindProtocol(t, c, "wl_subcompositor")
607+ pres := bindProtocol(t, c, "wp_presentation")
608+ registerProtocol(t, c, pres)
609+ child := c.AllocateID()
610+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
611+ registerProtocol(t, c, child)
612+ sub := c.AllocateID()
613+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
614+ registerProtocol(t, c, sub)
615+ first := &feedbackEvents{out: make(chan presentedEvent, 2)}
616+ id := c.AllocateID()
617+ first.SetID(id)
618+ c.Context().Register(first)
619+ requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, child, id)
620+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
621+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
622+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
623+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
624+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
625+ if err := c.Roundtrip(); err != nil {
626+ t.Fatal(err)
627+ }
628+ if err := c.Roundtrip(); err != nil {
629+ t.Fatal(err)
630+ }
631+ select {
632+ case ev := <-first.out:
633+ if !ev.discarded {
634+ t.Fatalf("superseded content presented: %+v", ev)
635+ }
636+ case <-time.After(2 * time.Second):
637+ t.Fatal("superseded feedback not discarded")
638+ }
639+}
640+
641+// A subsurface resource remains live after its parent surface is destroyed.
642+func TestSubsurfacePositionAfterParentDestroy(t *testing.T) {
643+ s, events, _, _, dir := contentServer(t)
644+ c := protocolClient(t, s, dir)
645+ _, root, _ := surfaceMapper(t, c, events)()
646+ comp := bindProtocol(t, c, "wl_compositor")
647+ subc := bindProtocol(t, c, "wl_subcompositor")
648+ child := c.AllocateID()
649+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
650+ registerProtocol(t, c, child)
651+ sub := c.AllocateID()
652+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
653+ registerProtocol(t, c, sub)
654+ requestProtocol(t, c, root, wayland.SurfaceRequestDestroy)
655+ roundtrip(t, c)
656+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetPosition, int32(3), int32(4))
657+ roundtrip(t, c)
658+}
659+
660+// A graph without a changed surface must not republish a mapped tree.
661+func TestSubsurfaceNoopRootCommitDoesNotPublish(t *testing.T) {
662+ s, events, _, contents, dir := contentServer(t)
663+ c := protocolClient(t, s, dir)
664+ _, root, _ := surfaceMapper(t, c, events)()
665+ comp := bindProtocol(t, c, "wl_compositor")
666+ subc := bindProtocol(t, c, "wl_subcompositor")
667+ child := c.AllocateID()
668+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
669+ registerProtocol(t, c, child)
670+ sub := c.AllocateID()
671+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
672+ registerProtocol(t, c, sub)
673+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
674+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
675+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
676+ roundtrip(t, c)
677+ drainContents(contents)
678+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
679+ roundtrip(t, c)
680+ select {
681+ case got := <-contents:
682+ t.Fatalf("no-op published: %+v", got)
683+ case <-time.After(80 * time.Millisecond):
684+ }
685+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
686+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
687+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
688+ roundtrip(t, c)
689+ select {
690+ case got := <-contents:
691+ if len(got.Children) != 1 {
692+ t.Fatalf("child change not published: %+v", got)
693+ }
694+ case <-time.After(2 * time.Second):
695+ t.Fatal("child change did not publish")
696+ }
697+}
M internal/adapters/wayland/surface.go +122 -43
@@ -3,6 +3,7 @@ package wayland
33 import (
44 "github.com/bnema/purego-libwayland/protocol/presentationtime"
55 "image"
6+ "math"
67 "time"
78
89 "github.com/bnema/neferwl/internal/ports"
@@ -22,8 +23,9 @@ const (
2223 )
2324
2425 type surface struct {
25- wl *wayland.Surface
26- viewport *viewport
26+ wl *wayland.Surface
27+ viewport *viewport
28+ committedViewport *viewport
2729 // bufferScale is committed state; pendingScale is set by set_buffer_scale.
2830 bufferScale, pendingScale int
2931 kind roleKind
@@ -43,9 +45,17 @@ type surface struct {
4345 scale float64
4446 // content is the last content built from this surface's own buffer;
4547 // has is false while no buffer is attached.
46- content ports.SurfaceContent
47- has bool
48- sub subState
48+ content ports.SurfaceContent
49+ has bool
50+ identity uint64
51+ version uint64
52+ // cachedTree is never modified once returned to emitContent.
53+ cachedTree []ports.Subsurface
54+ treeDirty bool
55+ commitFresh, commitSkipped bool
56+ queuedScale int
57+ queuedBuffer uint32
58+ sub subState
4959 // tearing is the surface's wp_tearing_control_v1; async is the
5060 // committed hint, pendingAsync the requested one.
5161 tearing *tearingHandler
@@ -62,7 +72,7 @@ type surface struct {
6272 pendingTime time.Time
6373 barrier bool
6474 barrierAt time.Time
65- queue []update
75+ queue []*update
6676 // contentType is the wp_content_type_v1; contentKind the committed type.
6777 contentType *contentTypeHandler
6878 contentKind, pendingKind uint32
@@ -100,13 +110,11 @@ const maxDamageRects = 32
100110 // are ordered bottom to top, below marks children under their parent.
101111 type subState struct {
102112 // role is the live wl_subsurface, nil when the surface has none.
103- role *wayland.Subsurface
104- parent *surface
105- children []*surface
106- x, y int // committed position, from the parent origin
107- pendX, pendY int
108- moved bool // a position waits for the parent commit
109- below bool
113+ role *wayland.Subsurface
114+ parent *surface
115+ children []*surface // structural children
116+ pendingLayout, layout []childLayout
117+ synced bool // explicit synchronization mode
110118 }
111119
112120 // root is the top of the surface's subsurface tree.
@@ -135,19 +143,24 @@ func (s *surface) outputColor() ports.SurfaceColor {
135143
136144 func (s *surface) contentWithColor() ports.SurfaceContent {
137145 c := s.content
146+ c.Surface, c.Version = s.identity, s.version
138147 c.Color = s.outputColor()
139148 return c
140149 }
141150
142151 // tree is the root's content with its subsurfaces flattened.
143152 func (s *surface) tree(id ports.WindowID) ports.SurfaceContent {
144- c := s.content
153+ c := s.contentWithColor()
145154 c.ID = id
146- c.Color = s.outputColor()
147- c.Children = nil
148- for _, ch := range s.sub.children {
149- ch.appendTree(&c.Children, ch.sub.x, ch.sub.y, ch.sub.below)
155+ if s.treeDirty || s.cachedTree == nil {
156+ count := s.treeCount()
157+ children := make([]ports.Subsurface, 0, count)
158+ for _, item := range s.sub.layout {
159+ item.child.appendTree(&children, item.x, item.y, item.below)
160+ }
161+ s.cachedTree, s.treeDirty = children, false
150162 }
163+ c.Children = s.cachedTree
151164 if s.xdg != nil {
152165 c.Geometry = s.xdg.geometry
153166 }
@@ -155,20 +168,31 @@ func (s *surface) tree(id ports.WindowID) ports.SurfaceContent {
155168 return c
156169 }
157170
171+func (s *surface) treeCount() int {
172+ n := 0
173+ for _, item := range s.sub.layout {
174+ if item.child.has {
175+ n++
176+ }
177+ n += item.child.treeCount()
178+ }
179+ return n
180+}
181+
158182 // appendTree flattens a subsurface at (x, y) from the root: its children
159183 // below it, itself, then its children above.
160184 func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) {
161- for _, ch := range s.sub.children {
162- if ch.sub.below {
163- ch.appendTree(out, x+ch.sub.x, y+ch.sub.y, below)
185+ for _, item := range s.sub.layout {
186+ if item.below {
187+ item.child.appendTree(out, x+item.x, y+item.y, below)
164188 }
165189 }
166190 if s.has {
167191 *out = append(*out, ports.Subsurface{X: x, Y: y, Below: below, SurfaceContent: s.contentWithColor()})
168192 }
169- for _, ch := range s.sub.children {
170- if !ch.sub.below {
171- ch.appendTree(out, x+ch.sub.x, y+ch.sub.y, below)
193+ for _, item := range s.sub.layout {
194+ if !item.below {
195+ item.child.appendTree(out, x+item.x, y+item.y, below)
172196 }
173197 }
174198 }
@@ -198,7 +222,16 @@ func (s *surface) detach() {
198222 break
199223 }
200224 }
225+ p.sub.pendingLayout = removeLayout(p.sub.pendingLayout, s)
226+ p.sub.layout = removeLayout(p.sub.layout, s)
227+ p.root().treeDirty = true
228+ for _, u := range p.queue {
229+ u.layout = removeLayout(u.layout, s)
230+ }
201231 s.sub.parent = nil
232+ // Parent updates cannot make a detached child's bound commits visible.
233+ s.dropQueue()
234+ s.flushDesync()
202235 p.redraw()
203236 s.sendScale()
204237 }
@@ -221,8 +254,11 @@ func (s *surface) Destroy(*wayland.Surface) {
221254 s.detach()
222255 for _, ch := range s.sub.children {
223256 ch.sub.parent = nil
257+ ch.dropQueue()
258+ ch.flushDesync()
224259 }
225260 s.sub.children = nil
261+ s.sub.layout, s.sub.pendingLayout = nil, nil
226262 for _, cb := range s.callbacks {
227263 cb.Destroy()
228264 }
@@ -265,18 +301,25 @@ func (s *surface) Commit(*wayland.Surface) {
265301 return
266302 }
267303 s.takeSyncPoints()
268- // A commit behind a fifo barrier, a future timestamp or an acquire
269- // point without a fence yet waits (fifo.go).
270- if s.mustWait(time.Now()) {
271- s.queueUpdate()
272- return
273- }
274- s.applyCommit()
304+ // Each request creates an ordered content update. A synchronized update
305+ // can only be reached through a parent's committed dependency graph.
306+ s.queueUpdate()
307+ s.server.tickFifo(time.Now(), nil)
275308 }
276309
277310 // applyCommit makes the pending state current.
278311 func (s *surface) applyCommit() {
312+ oldW, oldH, oldSource := s.content.LogicalW, s.content.LogicalH, s.content.Source
313+ oldColor, oldRepresentation := s.color, s.representation
279314 fresh := s.attached && s.pending != nil
315+ s.commitFresh = fresh
316+ if !s.commitSkipped {
317+ s.queuedScale = s.pendingScale
318+ s.queuedBuffer = 0
319+ if s.pending != nil {
320+ s.queuedBuffer = s.pending.ID()
321+ }
322+ }
280323 if s.pendingBarrier {
281324 s.pendingBarrier = false
282325 s.setBarrier(time.Now())
@@ -295,6 +338,10 @@ func (s *surface) applyCommit() {
295338 s.representation = s.pendingRepresentation
296339 if s.viewport != nil {
297340 s.viewport.commit()
341+ v := *s.viewport
342+ s.committedViewport = &v
343+ } else {
344+ s.committedViewport = nil
298345 }
299346 cs := s.pendingSync
300347 s.pendingSync = nil
@@ -322,6 +369,10 @@ func (s *surface) applyCommit() {
322369 if fresh {
323370 if state, ok := s.server.buffers[s.current.Resource]; ok {
324371 if c, ok := state.content(0); ok {
372+ if !s.validateViewport(c.Width, c.Height) {
373+ return
374+ }
375+ c.Source, _ = s.source(c.Width, c.Height)
325376 c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height)
326377 resized := !s.has || c.Width != s.content.Width || c.Height != s.content.Height || c.LogicalW != s.content.LogicalW || c.LogicalH != s.content.LogicalH
327378 s.commitDamage(true, resized, c.Width, c.Height)
@@ -338,19 +389,34 @@ func (s *surface) applyCommit() {
338389 // replaces them.
339390 }
340391 }
392+ damaged := !fresh && s.current != nil && (len(s.pendingDamage) > 0 || len(s.pendingBufDamage) > 0)
341393 if !fresh {
342394 s.commitDamage(false, false, 0, 0)
395+ // A NULL attach is exempt from out_of_buffer: its content is
396+ // cleared below, so the retained buffer is not validated.
397+ if s.has && s.current != nil {
398+ if !s.validateViewport(s.content.Width, s.content.Height) {
399+ return
400+ }
401+ s.content.Source, _ = s.source(s.content.Width, s.content.Height)
402+ s.content.LogicalW, s.content.LogicalH = s.logicalSize(s.content.Width, s.content.Height)
403+ }
343404 }
344405 if s.current == nil {
345406 s.content, s.has = ports.SurfaceContent{}, false
346407 }
347- // Subsurface positions apply on the parent commit.
348- moved := false
349- for _, ch := range s.sub.children {
350- if ch.sub.moved {
351- ch.sub.x, ch.sub.y, ch.sub.moved = ch.sub.pendX, ch.sub.pendY, false
352- moved = true
353- }
408+ if fresh || damaged || oldW != s.content.LogicalW || oldH != s.content.LogicalH || oldSource != s.content.Source || oldColor != s.color || oldRepresentation != s.representation {
409+ s.version++
410+ s.root().treeDirty = true
411+ }
412+ // The layout belongs to this update, not to the latest requests.
413+ moved := !sameLayout(s.sub.layout, s.sub.pendingLayout)
414+ if moved {
415+ s.root().treeDirty = true
416+ }
417+ // Queued updates can still reference the previous layout. Never mutate it.
418+ if moved {
419+ s.sub.layout = s.sub.pendingLayout
354420 }
355421 geometry := false
356422 if s.xdg != nil && s.xdg.pendingGeometry != s.xdg.geometry {
@@ -360,16 +426,24 @@ func (s *surface) applyCommit() {
360426 if s.xdg != nil && s.xdg.window != nil {
361427 s.xdg.window.afterCommit()
362428 }
363- drawn := fresh || moved || geometry || hinted || s.sub.parent != nil
429+ drawn := fresh || damaged || moved || geometry || hinted || s.sub.parent != nil || (s.has && (s.content.LogicalW != oldW || s.content.LogicalH != oldH || s.content.Source != oldSource))
364430 if drawn {
365- if moved || geometry {
431+ if moved || geometry || oldW != s.content.LogicalW || oldH != s.content.LogicalH || oldSource != s.content.Source {
366432 s.committed = damage{full: true}
367433 }
368- s.redraw()
434+ if s.server.applyingGraph {
435+ s.server.graphDrawn = true
436+ } else {
437+ s.redraw()
438+ }
369439 }
370440 fb := s.pendingFeedback
371441 s.pendingFeedback = nil
372- s.commitFeedback(fb, fresh)
442+ if s.server.applyingGraph {
443+ s.server.graphFeedback = append(s.server.graphFeedback, graphFeedback{s, fb, fresh})
444+ } else {
445+ s.commitFeedback(fb, fresh)
446+ }
373447 }
374448 func (s *surface) Damage(_ *wayland.Surface, x, y, w, h int32) {
375449 if w > 0 && h > 0 && len(s.pendingDamage) <= maxDamageRects {
@@ -416,7 +490,12 @@ func (s *surface) commitDamage(fresh, resized bool, bw, bh int) {
416490 // Clients may damage INT32_MAX-sized rects: clamp before scaling.
417491 x0, y0 := max(r.X, 0), max(r.Y, 0)
418492 x1, y1 := min(r.X+min(r.W, lw), lw), min(r.Y+min(r.H, lh), lh)
419- add(image.Rect(x0*bw/lw, y0*bh/lh, (x1*bw+lw-1)/lw, (y1*bh+lh-1)/lh))
493+ if src, ok := s.source(bw, bh); ok {
494+ add(image.Rect(int(src[0]+float32(x0)*src[2]/float32(lw)), int(src[1]+float32(y0)*src[3]/float32(lh)),
495+ int(math.Ceil(float64(src[0]+float32(x1)*src[2]/float32(lw)))), int(math.Ceil(float64(src[1]+float32(y1)*src[3]/float32(lh))))))
496+ } else {
497+ add(image.Rect(x0*bw/lw, y0*bh/lh, (x1*bw+lw-1)/lw, (y1*bh+lh-1)/lh))
498+ }
420499 }
421500 s.committed = d
422501 }
@@ -3,6 +3,7 @@ package wayland
3 import (3 import (
4 "github.com/bnema/purego-libwayland/protocol/presentationtime"4 "github.com/bnema/purego-libwayland/protocol/presentationtime"
5 "image"5 "image"
6+ "math"
6 "time"7 "time"
7 8
8 "github.com/bnema/neferwl/internal/ports"9 "github.com/bnema/neferwl/internal/ports"
@@ -22,8 +23,9 @@ const (
22 )23 )
23 24
24 type surface struct {25 type surface struct {
25- wl *wayland.Surface26+ wl *wayland.Surface
26- viewport *viewport27+ viewport *viewport
28+ committedViewport *viewport
27 // bufferScale is committed state; pendingScale is set by set_buffer_scale.29 // bufferScale is committed state; pendingScale is set by set_buffer_scale.
28 bufferScale, pendingScale int30 bufferScale, pendingScale int
29 kind roleKind31 kind roleKind
@@ -43,9 +45,17 @@ type surface struct {
43 scale float6445 scale float64
44 // content is the last content built from this surface's own buffer;46 // content is the last content built from this surface's own buffer;
45 // has is false while no buffer is attached.47 // has is false while no buffer is attached.
46- content ports.SurfaceContent48+ content ports.SurfaceContent
47- has bool49+ has bool
48- sub subState50+ identity uint64
51+ version uint64
52+ // cachedTree is never modified once returned to emitContent.
53+ cachedTree []ports.Subsurface
54+ treeDirty bool
55+ commitFresh, commitSkipped bool
56+ queuedScale int
57+ queuedBuffer uint32
58+ sub subState
49 // tearing is the surface's wp_tearing_control_v1; async is the59 // tearing is the surface's wp_tearing_control_v1; async is the
50 // committed hint, pendingAsync the requested one.60 // committed hint, pendingAsync the requested one.
51 tearing *tearingHandler61 tearing *tearingHandler
@@ -62,7 +72,7 @@ type surface struct {
62 pendingTime time.Time72 pendingTime time.Time
63 barrier bool73 barrier bool
64 barrierAt time.Time74 barrierAt time.Time
65- queue []update75+ queue []*update
66 // contentType is the wp_content_type_v1; contentKind the committed type.76 // contentType is the wp_content_type_v1; contentKind the committed type.
67 contentType *contentTypeHandler77 contentType *contentTypeHandler
68 contentKind, pendingKind uint3278 contentKind, pendingKind uint32
@@ -100,13 +110,11 @@ const maxDamageRects = 32
100 // are ordered bottom to top, below marks children under their parent.110 // are ordered bottom to top, below marks children under their parent.
101 type subState struct {111 type subState struct {
102 // role is the live wl_subsurface, nil when the surface has none.112 // role is the live wl_subsurface, nil when the surface has none.
103- role *wayland.Subsurface113+ role *wayland.Subsurface
104- parent *surface114+ parent *surface
105- children []*surface115+ children []*surface // structural children
106- x, y int // committed position, from the parent origin116+ pendingLayout, layout []childLayout
107- pendX, pendY int117+ synced bool // explicit synchronization mode
108- moved bool // a position waits for the parent commit
109- below bool
110 }118 }
111 119
112 // root is the top of the surface's subsurface tree.120 // root is the top of the surface's subsurface tree.
@@ -135,19 +143,24 @@ func (s *surface) outputColor() ports.SurfaceColor {
135 143
136 func (s *surface) contentWithColor() ports.SurfaceContent {144 func (s *surface) contentWithColor() ports.SurfaceContent {
137 c := s.content145 c := s.content
146+ c.Surface, c.Version = s.identity, s.version
138 c.Color = s.outputColor()147 c.Color = s.outputColor()
139 return c148 return c
140 }149 }
141 150
142 // tree is the root's content with its subsurfaces flattened.151 // tree is the root's content with its subsurfaces flattened.
143 func (s *surface) tree(id ports.WindowID) ports.SurfaceContent {152 func (s *surface) tree(id ports.WindowID) ports.SurfaceContent {
144- c := s.content153+ c := s.contentWithColor()
145 c.ID = id154 c.ID = id
146- c.Color = s.outputColor()155+ if s.treeDirty || s.cachedTree == nil {
147- c.Children = nil156+ count := s.treeCount()
148- for _, ch := range s.sub.children {157+ children := make([]ports.Subsurface, 0, count)
149- ch.appendTree(&c.Children, ch.sub.x, ch.sub.y, ch.sub.below)158+ for _, item := range s.sub.layout {
159+ item.child.appendTree(&children, item.x, item.y, item.below)
160+ }
161+ s.cachedTree, s.treeDirty = children, false
150 }162 }
163+ c.Children = s.cachedTree
151 if s.xdg != nil {164 if s.xdg != nil {
152 c.Geometry = s.xdg.geometry165 c.Geometry = s.xdg.geometry
153 }166 }
@@ -155,20 +168,31 @@ func (s *surface) tree(id ports.WindowID) ports.SurfaceContent {
155 return c168 return c
156 }169 }
157 170
171+func (s *surface) treeCount() int {
172+ n := 0
173+ for _, item := range s.sub.layout {
174+ if item.child.has {
175+ n++
176+ }
177+ n += item.child.treeCount()
178+ }
179+ return n
180+}
181+
158 // appendTree flattens a subsurface at (x, y) from the root: its children182 // appendTree flattens a subsurface at (x, y) from the root: its children
159 // below it, itself, then its children above.183 // below it, itself, then its children above.
160 func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) {184 func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) {
161- for _, ch := range s.sub.children {185+ for _, item := range s.sub.layout {
162- if ch.sub.below {186+ if item.below {
163- ch.appendTree(out, x+ch.sub.x, y+ch.sub.y, below)187+ item.child.appendTree(out, x+item.x, y+item.y, below)
164 }188 }
165 }189 }
166 if s.has {190 if s.has {
167 *out = append(*out, ports.Subsurface{X: x, Y: y, Below: below, SurfaceContent: s.contentWithColor()})191 *out = append(*out, ports.Subsurface{X: x, Y: y, Below: below, SurfaceContent: s.contentWithColor()})
168 }192 }
169- for _, ch := range s.sub.children {193+ for _, item := range s.sub.layout {
170- if !ch.sub.below {194+ if !item.below {
171- ch.appendTree(out, x+ch.sub.x, y+ch.sub.y, below)195+ item.child.appendTree(out, x+item.x, y+item.y, below)
172 }196 }
173 }197 }
174 }198 }
@@ -198,7 +222,16 @@ func (s *surface) detach() {
198 break222 break
199 }223 }
200 }224 }
225+ p.sub.pendingLayout = removeLayout(p.sub.pendingLayout, s)
226+ p.sub.layout = removeLayout(p.sub.layout, s)
227+ p.root().treeDirty = true
228+ for _, u := range p.queue {
229+ u.layout = removeLayout(u.layout, s)
230+ }
201 s.sub.parent = nil231 s.sub.parent = nil
232+ // Parent updates cannot make a detached child's bound commits visible.
233+ s.dropQueue()
234+ s.flushDesync()
202 p.redraw()235 p.redraw()
203 s.sendScale()236 s.sendScale()
204 }237 }
@@ -221,8 +254,11 @@ func (s *surface) Destroy(*wayland.Surface) {
221 s.detach()254 s.detach()
222 for _, ch := range s.sub.children {255 for _, ch := range s.sub.children {
223 ch.sub.parent = nil256 ch.sub.parent = nil
257+ ch.dropQueue()
258+ ch.flushDesync()
224 }259 }
225 s.sub.children = nil260 s.sub.children = nil
261+ s.sub.layout, s.sub.pendingLayout = nil, nil
226 for _, cb := range s.callbacks {262 for _, cb := range s.callbacks {
227 cb.Destroy()263 cb.Destroy()
228 }264 }
@@ -265,18 +301,25 @@ func (s *surface) Commit(*wayland.Surface) {
265 return301 return
266 }302 }
267 s.takeSyncPoints()303 s.takeSyncPoints()
268- // A commit behind a fifo barrier, a future timestamp or an acquire304+ // Each request creates an ordered content update. A synchronized update
269- // point without a fence yet waits (fifo.go).305+ // can only be reached through a parent's committed dependency graph.
270- if s.mustWait(time.Now()) {306+ s.queueUpdate()
271- s.queueUpdate()307+ s.server.tickFifo(time.Now(), nil)
272- return
273- }
274- s.applyCommit()
275 }308 }
276 309
277 // applyCommit makes the pending state current.310 // applyCommit makes the pending state current.
278 func (s *surface) applyCommit() {311 func (s *surface) applyCommit() {
312+ oldW, oldH, oldSource := s.content.LogicalW, s.content.LogicalH, s.content.Source
313+ oldColor, oldRepresentation := s.color, s.representation
279 fresh := s.attached && s.pending != nil314 fresh := s.attached && s.pending != nil
315+ s.commitFresh = fresh
316+ if !s.commitSkipped {
317+ s.queuedScale = s.pendingScale
318+ s.queuedBuffer = 0
319+ if s.pending != nil {
320+ s.queuedBuffer = s.pending.ID()
321+ }
322+ }
280 if s.pendingBarrier {323 if s.pendingBarrier {
281 s.pendingBarrier = false324 s.pendingBarrier = false
282 s.setBarrier(time.Now())325 s.setBarrier(time.Now())
@@ -295,6 +338,10 @@ func (s *surface) applyCommit() {
295 s.representation = s.pendingRepresentation338 s.representation = s.pendingRepresentation
296 if s.viewport != nil {339 if s.viewport != nil {
297 s.viewport.commit()340 s.viewport.commit()
341+ v := *s.viewport
342+ s.committedViewport = &v
343+ } else {
344+ s.committedViewport = nil
298 }345 }
299 cs := s.pendingSync346 cs := s.pendingSync
300 s.pendingSync = nil347 s.pendingSync = nil
@@ -322,6 +369,10 @@ func (s *surface) applyCommit() {
322 if fresh {369 if fresh {
323 if state, ok := s.server.buffers[s.current.Resource]; ok {370 if state, ok := s.server.buffers[s.current.Resource]; ok {
324 if c, ok := state.content(0); ok {371 if c, ok := state.content(0); ok {
372+ if !s.validateViewport(c.Width, c.Height) {
373+ return
374+ }
375+ c.Source, _ = s.source(c.Width, c.Height)
325 c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height)376 c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height)
326 resized := !s.has || c.Width != s.content.Width || c.Height != s.content.Height || c.LogicalW != s.content.LogicalW || c.LogicalH != s.content.LogicalH377 resized := !s.has || c.Width != s.content.Width || c.Height != s.content.Height || c.LogicalW != s.content.LogicalW || c.LogicalH != s.content.LogicalH
327 s.commitDamage(true, resized, c.Width, c.Height)378 s.commitDamage(true, resized, c.Width, c.Height)
@@ -338,19 +389,34 @@ func (s *surface) applyCommit() {
338 // replaces them.389 // replaces them.
339 }390 }
340 }391 }
392+ damaged := !fresh && s.current != nil && (len(s.pendingDamage) > 0 || len(s.pendingBufDamage) > 0)
341 if !fresh {393 if !fresh {
342 s.commitDamage(false, false, 0, 0)394 s.commitDamage(false, false, 0, 0)
395+ // A NULL attach is exempt from out_of_buffer: its content is
396+ // cleared below, so the retained buffer is not validated.
397+ if s.has && s.current != nil {
398+ if !s.validateViewport(s.content.Width, s.content.Height) {
399+ return
400+ }
401+ s.content.Source, _ = s.source(s.content.Width, s.content.Height)
402+ s.content.LogicalW, s.content.LogicalH = s.logicalSize(s.content.Width, s.content.Height)
403+ }
343 }404 }
344 if s.current == nil {405 if s.current == nil {
345 s.content, s.has = ports.SurfaceContent{}, false406 s.content, s.has = ports.SurfaceContent{}, false
346 }407 }
347- // Subsurface positions apply on the parent commit.408+ if fresh || damaged || oldW != s.content.LogicalW || oldH != s.content.LogicalH || oldSource != s.content.Source || oldColor != s.color || oldRepresentation != s.representation {
348- moved := false409+ s.version++
349- for _, ch := range s.sub.children {410+ s.root().treeDirty = true
350- if ch.sub.moved {411+ }
351- ch.sub.x, ch.sub.y, ch.sub.moved = ch.sub.pendX, ch.sub.pendY, false412+ // The layout belongs to this update, not to the latest requests.
352- moved = true413+ moved := !sameLayout(s.sub.layout, s.sub.pendingLayout)
353- }414+ if moved {
415+ s.root().treeDirty = true
416+ }
417+ // Queued updates can still reference the previous layout. Never mutate it.
418+ if moved {
419+ s.sub.layout = s.sub.pendingLayout
354 }420 }
355 geometry := false421 geometry := false
356 if s.xdg != nil && s.xdg.pendingGeometry != s.xdg.geometry {422 if s.xdg != nil && s.xdg.pendingGeometry != s.xdg.geometry {
@@ -360,16 +426,24 @@ func (s *surface) applyCommit() {
360 if s.xdg != nil && s.xdg.window != nil {426 if s.xdg != nil && s.xdg.window != nil {
361 s.xdg.window.afterCommit()427 s.xdg.window.afterCommit()
362 }428 }
363- drawn := fresh || moved || geometry || hinted || s.sub.parent != nil429+ drawn := fresh || damaged || moved || geometry || hinted || s.sub.parent != nil || (s.has && (s.content.LogicalW != oldW || s.content.LogicalH != oldH || s.content.Source != oldSource))
364 if drawn {430 if drawn {
365- if moved || geometry {431+ if moved || geometry || oldW != s.content.LogicalW || oldH != s.content.LogicalH || oldSource != s.content.Source {
366 s.committed = damage{full: true}432 s.committed = damage{full: true}
367 }433 }
368- s.redraw()434+ if s.server.applyingGraph {
435+ s.server.graphDrawn = true
436+ } else {
437+ s.redraw()
438+ }
369 }439 }
370 fb := s.pendingFeedback440 fb := s.pendingFeedback
371 s.pendingFeedback = nil441 s.pendingFeedback = nil
372- s.commitFeedback(fb, fresh)442+ if s.server.applyingGraph {
443+ s.server.graphFeedback = append(s.server.graphFeedback, graphFeedback{s, fb, fresh})
444+ } else {
445+ s.commitFeedback(fb, fresh)
446+ }
373 }447 }
374 func (s *surface) Damage(_ *wayland.Surface, x, y, w, h int32) {448 func (s *surface) Damage(_ *wayland.Surface, x, y, w, h int32) {
375 if w > 0 && h > 0 && len(s.pendingDamage) <= maxDamageRects {449 if w > 0 && h > 0 && len(s.pendingDamage) <= maxDamageRects {
@@ -416,7 +490,12 @@ func (s *surface) commitDamage(fresh, resized bool, bw, bh int) {
416 // Clients may damage INT32_MAX-sized rects: clamp before scaling.490 // Clients may damage INT32_MAX-sized rects: clamp before scaling.
417 x0, y0 := max(r.X, 0), max(r.Y, 0)491 x0, y0 := max(r.X, 0), max(r.Y, 0)
418 x1, y1 := min(r.X+min(r.W, lw), lw), min(r.Y+min(r.H, lh), lh)492 x1, y1 := min(r.X+min(r.W, lw), lw), min(r.Y+min(r.H, lh), lh)
419- add(image.Rect(x0*bw/lw, y0*bh/lh, (x1*bw+lw-1)/lw, (y1*bh+lh-1)/lh))493+ if src, ok := s.source(bw, bh); ok {
494+ add(image.Rect(int(src[0]+float32(x0)*src[2]/float32(lw)), int(src[1]+float32(y0)*src[3]/float32(lh)),
495+ int(math.Ceil(float64(src[0]+float32(x1)*src[2]/float32(lw)))), int(math.Ceil(float64(src[1]+float32(y1)*src[3]/float32(lh))))))
496+ } else {
497+ add(image.Rect(x0*bw/lw, y0*bh/lh, (x1*bw+lw-1)/lw, (y1*bh+lh-1)/lh))
498+ }
420 }499 }
421 s.committed = d500 s.committed = d
422 }501 }
A internal/adapters/wayland/surface_perf_test.go +78 -0
@@ -0,0 +1,78 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "fmt"
6+ "testing"
7+
8+ "github.com/bnema/neferwl/internal/logging"
9+ "github.com/bnema/neferwl/internal/ports"
10+)
11+
12+// TestTiledCommitPublishAllocations exercises real content publication without
13+// a client connection, holding old snapshots while subsequent commits publish.
14+func TestTiledCommitPublishAllocations(t *testing.T) {
15+ for _, count := range []int{16, 64} {
16+ t.Run(fmt.Sprintf("%d", count), func(t *testing.T) {
17+ channels := Channels{Contents: make(chan ports.SurfaceContent, 1)}
18+ srv, err := New(Options{RuntimeDir: t.TempDir(), Outputs: testOutputs}, channels, logging.For(context.Background(), "wayland"))
19+ if err != nil {
20+ t.Fatal(err)
21+ }
22+ root := &surface{server: srv, content: ports.SurfaceContent{Width: 1, Height: 1}, has: true, identity: 1}
23+ root.xdg = &xdgSurface{window: &window{id: 1, mapped: true}}
24+ for i := range count {
25+ child := &surface{server: srv, has: true, content: ports.SurfaceContent{Width: 1, Height: 1}, identity: uint64(i + 2)}
26+ child.sub.parent = root
27+ root.sub.children = append(root.sub.children, child)
28+ root.sub.layout = append(root.sub.layout, childLayout{child: child, x: i})
29+ }
30+ publish := func() { root.redraw() }
31+ root.treeDirty = true
32+ publish()
33+ first := srv.contents[1]
34+ publish()
35+ if first.Seq != 1 || len(first.Children) != count || len(first.DamageHistory) != 1 {
36+ t.Fatalf("previous publication changed: %+v", first)
37+ }
38+ if allocs := testing.AllocsPerRun(100, publish); allocs > 4 {
39+ t.Errorf("%d children: %.1f allocs/publication; want <=4", count, allocs)
40+ } else {
41+ t.Logf("%d children: %.1f allocs/unchanged publication", count, allocs)
42+ }
43+ // A queued commit with an unchanged layout must not allocate
44+ // proportionally to the number of children.
45+ root.sub.pendingLayout = root.sub.layout
46+ queue := func() { _ = root.takePending() }
47+ if allocs := testing.AllocsPerRun(100, queue); allocs > 2 {
48+ t.Errorf("%d children: %.1f allocs/queued commit; want <=2", count, allocs)
49+ }
50+ if first.Seq != 1 || len(first.Children) != count || len(first.DamageHistory) != 1 || first.DamageHistory[0].Seq != 1 {
51+ t.Fatal("previous publication was mutated")
52+ }
53+ // An applied child change rebuilds into a distinct immutable slice.
54+ root.sub.children[count-1].version++
55+ root.treeDirty = true
56+ publish()
57+ if first.Children[count-1].Version != 0 || srv.contents[1].Children[count-1].Version != 1 {
58+ t.Fatal("child version leaked into an earlier publication")
59+ }
60+ if &first.Children[0] == &srv.contents[1].Children[0] {
61+ t.Fatal("rebuild reused previously published children storage")
62+ }
63+ // Rebuilding a changed child's immutable snapshot must have a
64+ // constant allocation count, regardless of tree size.
65+ child := root.sub.children[count-1]
66+ changed := func() {
67+ child.version++
68+ root.treeDirty = true
69+ root.redraw()
70+ }
71+ if allocs := testing.AllocsPerRun(100, changed); allocs > 4 {
72+ t.Errorf("%d children: %.1f allocs/changed publication; want <=4", count, allocs)
73+ } else {
74+ t.Logf("%d children: %.1f allocs/changed publication", count, allocs)
75+ }
76+ })
77+ }
78+}
@@ -0,0 +1,78 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "fmt"
6+ "testing"
7+
8+ "github.com/bnema/neferwl/internal/logging"
9+ "github.com/bnema/neferwl/internal/ports"
10+)
11+
12+// TestTiledCommitPublishAllocations exercises real content publication without
13+// a client connection, holding old snapshots while subsequent commits publish.
14+func TestTiledCommitPublishAllocations(t *testing.T) {
15+ for _, count := range []int{16, 64} {
16+ t.Run(fmt.Sprintf("%d", count), func(t *testing.T) {
17+ channels := Channels{Contents: make(chan ports.SurfaceContent, 1)}
18+ srv, err := New(Options{RuntimeDir: t.TempDir(), Outputs: testOutputs}, channels, logging.For(context.Background(), "wayland"))
19+ if err != nil {
20+ t.Fatal(err)
21+ }
22+ root := &surface{server: srv, content: ports.SurfaceContent{Width: 1, Height: 1}, has: true, identity: 1}
23+ root.xdg = &xdgSurface{window: &window{id: 1, mapped: true}}
24+ for i := range count {
25+ child := &surface{server: srv, has: true, content: ports.SurfaceContent{Width: 1, Height: 1}, identity: uint64(i + 2)}
26+ child.sub.parent = root
27+ root.sub.children = append(root.sub.children, child)
28+ root.sub.layout = append(root.sub.layout, childLayout{child: child, x: i})
29+ }
30+ publish := func() { root.redraw() }
31+ root.treeDirty = true
32+ publish()
33+ first := srv.contents[1]
34+ publish()
35+ if first.Seq != 1 || len(first.Children) != count || len(first.DamageHistory) != 1 {
36+ t.Fatalf("previous publication changed: %+v", first)
37+ }
38+ if allocs := testing.AllocsPerRun(100, publish); allocs > 4 {
39+ t.Errorf("%d children: %.1f allocs/publication; want <=4", count, allocs)
40+ } else {
41+ t.Logf("%d children: %.1f allocs/unchanged publication", count, allocs)
42+ }
43+ // A queued commit with an unchanged layout must not allocate
44+ // proportionally to the number of children.
45+ root.sub.pendingLayout = root.sub.layout
46+ queue := func() { _ = root.takePending() }
47+ if allocs := testing.AllocsPerRun(100, queue); allocs > 2 {
48+ t.Errorf("%d children: %.1f allocs/queued commit; want <=2", count, allocs)
49+ }
50+ if first.Seq != 1 || len(first.Children) != count || len(first.DamageHistory) != 1 || first.DamageHistory[0].Seq != 1 {
51+ t.Fatal("previous publication was mutated")
52+ }
53+ // An applied child change rebuilds into a distinct immutable slice.
54+ root.sub.children[count-1].version++
55+ root.treeDirty = true
56+ publish()
57+ if first.Children[count-1].Version != 0 || srv.contents[1].Children[count-1].Version != 1 {
58+ t.Fatal("child version leaked into an earlier publication")
59+ }
60+ if &first.Children[0] == &srv.contents[1].Children[0] {
61+ t.Fatal("rebuild reused previously published children storage")
62+ }
63+ // Rebuilding a changed child's immutable snapshot must have a
64+ // constant allocation count, regardless of tree size.
65+ child := root.sub.children[count-1]
66+ changed := func() {
67+ child.version++
68+ root.treeDirty = true
69+ root.redraw()
70+ }
71+ if allocs := testing.AllocsPerRun(100, changed); allocs > 4 {
72+ t.Errorf("%d children: %.1f allocs/changed publication; want <=4", count, allocs)
73+ } else {
74+ t.Logf("%d children: %.1f allocs/changed publication", count, allocs)
75+ }
76+ })
77+ }
78+}
M internal/adapters/wayland/syncobj_test.go +41 -0
@@ -387,6 +387,47 @@ func TestSyncobjInvalidTimeline(t *testing.T) {
387387 expectProtocolError(t, c, mgr, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline))
388388 }
389389
390+// A synchronized subsurface dropped before its acquire is ready must
391+// retire its release point without sending wl_buffer.release.
392+func TestSyncobjDroppedSubsurfaceCommit(t *testing.T) {
393+ h := newSyncHarness(t)
394+ h.releases = map[uint32]*syncReleaseProxy{}
395+ c := h.c
396+ comp := bindProtocol(t, c, "wl_compositor")
397+ subc := bindProtocol(t, c, "wl_subcompositor")
398+ mgr := bindProtocol(t, c, "wp_linux_drm_syncobj_manager_v1")
399+ child := c.AllocateID()
400+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
401+ registerProtocol(t, c, child)
402+ sub := c.AllocateID()
403+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, h.surf)
404+ registerProtocol(t, c, sub)
405+ syncChild := c.AllocateID()
406+ requestProtocol(t, c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, syncChild, child)
407+ registerProtocol(t, c, syncChild)
408+ b := h.dmabuf()
409+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
410+ requestProtocol(t, c, syncChild, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(21))
411+ requestProtocol(t, c, syncChild, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(22))
412+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
413+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestDestroy)
414+ if err := c.Roundtrip(); err != nil {
415+ t.Fatal(err)
416+ }
417+ h.fire(21) // dropping before acquire means the later signal must not release
418+ if err := c.Roundtrip(); err != nil {
419+ t.Fatal(err)
420+ }
421+ if got := h.signalled(); len(got) != 0 {
422+ t.Fatalf("premature release point: %v", got)
423+ }
424+ select {
425+ case <-h.releases[b].released:
426+ t.Fatal("explicit sync sent wl_buffer.release")
427+ default:
428+ }
429+}
430+
390431 // A commit dropped before its acquire point has a fence signals nothing
391432 // (the client has not submitted that point) and sends no wl_buffer.release;
392433 // the waiter closes its eventfd and no acquire file is left open.
@@ -387,6 +387,47 @@ func TestSyncobjInvalidTimeline(t *testing.T) {
387 expectProtocolError(t, c, mgr, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline))387 expectProtocolError(t, c, mgr, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline))
388 }388 }
389 389
390+// A synchronized subsurface dropped before its acquire is ready must
391+// retire its release point without sending wl_buffer.release.
392+func TestSyncobjDroppedSubsurfaceCommit(t *testing.T) {
393+ h := newSyncHarness(t)
394+ h.releases = map[uint32]*syncReleaseProxy{}
395+ c := h.c
396+ comp := bindProtocol(t, c, "wl_compositor")
397+ subc := bindProtocol(t, c, "wl_subcompositor")
398+ mgr := bindProtocol(t, c, "wp_linux_drm_syncobj_manager_v1")
399+ child := c.AllocateID()
400+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
401+ registerProtocol(t, c, child)
402+ sub := c.AllocateID()
403+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, h.surf)
404+ registerProtocol(t, c, sub)
405+ syncChild := c.AllocateID()
406+ requestProtocol(t, c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, syncChild, child)
407+ registerProtocol(t, c, syncChild)
408+ b := h.dmabuf()
409+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
410+ requestProtocol(t, c, syncChild, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(21))
411+ requestProtocol(t, c, syncChild, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(22))
412+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
413+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestDestroy)
414+ if err := c.Roundtrip(); err != nil {
415+ t.Fatal(err)
416+ }
417+ h.fire(21) // dropping before acquire means the later signal must not release
418+ if err := c.Roundtrip(); err != nil {
419+ t.Fatal(err)
420+ }
421+ if got := h.signalled(); len(got) != 0 {
422+ t.Fatalf("premature release point: %v", got)
423+ }
424+ select {
425+ case <-h.releases[b].released:
426+ t.Fatal("explicit sync sent wl_buffer.release")
427+ default:
428+ }
429+}
430+
390 // A commit dropped before its acquire point has a fence signals nothing431 // A commit dropped before its acquire point has a fence signals nothing
391 // (the client has not submitted that point) and sends no wl_buffer.release;432 // (the client has not submitted that point) and sends no wl_buffer.release;
392 // the waiter closes its eventfd and no acquire file is left open.433 // the waiter closes its eventfd and no acquire file is left open.
A internal/adapters/wayland/tiled_playback_test.go +156 -0
@@ -0,0 +1,156 @@
1+package wayland
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bnema/neferwl/internal/adapters/vulkan"
8+ "github.com/bnema/neferwl/internal/ports"
9+ "github.com/bnema/purego-libwayland/protocol/wayland"
10+ "github.com/bnema/wlturbo"
11+ "golang.org/x/sys/unix"
12+)
13+
14+// callbackBudgetProxy observes real wl_surface.frame replies.
15+type callbackBudgetProxy struct {
16+ wlturbo.BaseProxy
17+ done chan struct{}
18+}
19+
20+func (p *callbackBudgetProxy) Dispatch(e *wlturbo.Event) {
21+ if e.Opcode == uint16(wayland.CallbackEventDone) {
22+ p.done <- struct{}{}
23+ }
24+}
25+
26+// TestHeadlessTiledSHMCallbackAndCopyBudget guards callback delivery and
27+// the GPU copy budget across publications with many stable SHM children.
28+func TestHeadlessTiledSHMCallbackAndCopyBudget(t *testing.T) {
29+ s, events, _, contents, dir := contentServer(t)
30+ c := protocolClient(t, s, dir)
31+ win, root, _ := surfaceMapper(t, c, events)()
32+ comp := bindProtocol(t, c, "wl_compositor")
33+ subc := bindProtocol(t, c, "wl_subcompositor")
34+ shm := bindProtocol(t, c, "wl_shm")
35+ registerProtocol(t, c, shm)
36+ fd, err := unix.MemfdCreate("tiled-playback", unix.MFD_CLOEXEC)
37+ if err != nil {
38+ t.Fatal(err)
39+ }
40+ defer unix.Close(fd)
41+ const tiles = 64
42+ const edge = 8
43+ const stride = edge * 4
44+ const size = (tiles + 1) * edge * stride
45+ if err := unix.Ftruncate(fd, size); err != nil {
46+ t.Fatal(err)
47+ }
48+ pool := c.AllocateID()
49+ registerProtocol(t, c, pool)
50+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(size)); err != nil {
51+ t.Fatal(err)
52+ }
53+ children := make([]uint32, tiles+1)
54+ buffers := make([]uint32, tiles+1)
55+ for i := range children {
56+ children[i] = c.AllocateID()
57+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, children[i])
58+ registerProtocol(t, c, children[i])
59+ sub := c.AllocateID()
60+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, children[i], root)
61+ registerProtocol(t, c, sub)
62+ if i < tiles {
63+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetPosition, int32(i%8*edge), int32(i/8*edge))
64+ } else {
65+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetPosition, int32(80), int32(0))
66+ }
67+ buffers[i] = c.AllocateID()
68+ registerProtocol(t, c, buffers[i])
69+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffers[i], int32(i*edge*stride), int32(edge), int32(edge), int32(stride), uint32(0))
70+ requestProtocol(t, c, children[i], wayland.SurfaceRequestAttach, buffers[i], int32(0), int32(0))
71+ requestProtocol(t, c, children[i], wayland.SurfaceRequestCommit)
72+ }
73+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
74+ if err := c.Roundtrip(); err != nil {
75+ t.Fatal(err)
76+ }
77+ wait := func() ports.SurfaceContent {
78+ t.Helper()
79+ deadline := time.After(5 * time.Second)
80+ for {
81+ select {
82+ case got := <-contents:
83+ if got.ID == win.ID && len(got.Children) == tiles+1 {
84+ return got
85+ }
86+ case <-deadline:
87+ t.Fatal("no tiled publication")
88+ }
89+ }
90+ }
91+ previous := wait()
92+ r, err := vulkan.New(128, 128)
93+ if err != nil {
94+ t.Skipf("Vulkan unavailable: %v", err)
95+ }
96+ defer r.Close()
97+ scene := ports.Scene{Windows: []ports.SceneWindow{{ID: win.ID, Rect: ports.Rect{W: 128, H: 128}}}}
98+ render := func(content ports.SurfaceContent) {
99+ t.Helper()
100+ fence, err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{win.ID: content})
101+ if fence != nil {
102+ _ = fence.Close()
103+ }
104+ if err != nil {
105+ t.Fatal(err)
106+ }
107+ }
108+ render(previous)
109+ copied := r.CopiedBytes()
110+ for i := range 120 {
111+ done := make(chan struct{}, 1)
112+ cb := c.AllocateID()
113+ p := &callbackBudgetProxy{done: done}
114+ p.SetID(cb)
115+ c.Context().Register(p)
116+ requestProtocol(t, c, children[tiles], wayland.SurfaceRequestFrame, cb)
117+ requestProtocol(t, c, children[tiles], wayland.SurfaceRequestAttach, buffers[tiles], int32(0), int32(0))
118+ requestProtocol(t, c, children[tiles], wayland.SurfaceRequestCommit)
119+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
120+ if err := c.Roundtrip(); err != nil {
121+ t.Fatal(err)
122+ }
123+ deadline := time.After(3 * time.Second)
124+ for {
125+ select {
126+ case <-done:
127+ goto callbackDone
128+ default:
129+ }
130+ result := make(chan error, 1)
131+ go func() { result <- c.Dispatch() }()
132+ select {
133+ case err := <-result:
134+ if err != nil {
135+ t.Fatal(err)
136+ }
137+ case <-deadline:
138+ _ = c.Close()
139+ t.Fatalf("callback %d not delivered", i)
140+ }
141+ }
142+ callbackDone:
143+ next := wait()
144+ render(next)
145+ if delta := r.CopiedBytes() - copied; delta != edge*edge*4 {
146+ t.Fatalf("frame %d copied %d bytes, want only the changing child's %d (root: id=%d version=%d size=%dx%d, previous root version=%d; first tile: id=%d version=%d previous=%d)", i, delta, edge*edge*4, next.Surface, next.Version, next.Width, next.Height, previous.Version, next.Children[0].Surface, next.Children[0].Version, previous.Children[0].Version)
147+ }
148+ copied = r.CopiedBytes()
149+ for tile := range tiles {
150+ if next.Children[tile].Version != previous.Children[tile].Version {
151+ t.Fatalf("unchanged tile %d changed version", tile)
152+ }
153+ }
154+ previous = next
155+ }
156+}
@@ -0,0 +1,156 @@
1+package wayland
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bnema/neferwl/internal/adapters/vulkan"
8+ "github.com/bnema/neferwl/internal/ports"
9+ "github.com/bnema/purego-libwayland/protocol/wayland"
10+ "github.com/bnema/wlturbo"
11+ "golang.org/x/sys/unix"
12+)
13+
14+// callbackBudgetProxy observes real wl_surface.frame replies.
15+type callbackBudgetProxy struct {
16+ wlturbo.BaseProxy
17+ done chan struct{}
18+}
19+
20+func (p *callbackBudgetProxy) Dispatch(e *wlturbo.Event) {
21+ if e.Opcode == uint16(wayland.CallbackEventDone) {
22+ p.done <- struct{}{}
23+ }
24+}
25+
26+// TestHeadlessTiledSHMCallbackAndCopyBudget guards callback delivery and
27+// the GPU copy budget across publications with many stable SHM children.
28+func TestHeadlessTiledSHMCallbackAndCopyBudget(t *testing.T) {
29+ s, events, _, contents, dir := contentServer(t)
30+ c := protocolClient(t, s, dir)
31+ win, root, _ := surfaceMapper(t, c, events)()
32+ comp := bindProtocol(t, c, "wl_compositor")
33+ subc := bindProtocol(t, c, "wl_subcompositor")
34+ shm := bindProtocol(t, c, "wl_shm")
35+ registerProtocol(t, c, shm)
36+ fd, err := unix.MemfdCreate("tiled-playback", unix.MFD_CLOEXEC)
37+ if err != nil {
38+ t.Fatal(err)
39+ }
40+ defer unix.Close(fd)
41+ const tiles = 64
42+ const edge = 8
43+ const stride = edge * 4
44+ const size = (tiles + 1) * edge * stride
45+ if err := unix.Ftruncate(fd, size); err != nil {
46+ t.Fatal(err)
47+ }
48+ pool := c.AllocateID()
49+ registerProtocol(t, c, pool)
50+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(size)); err != nil {
51+ t.Fatal(err)
52+ }
53+ children := make([]uint32, tiles+1)
54+ buffers := make([]uint32, tiles+1)
55+ for i := range children {
56+ children[i] = c.AllocateID()
57+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, children[i])
58+ registerProtocol(t, c, children[i])
59+ sub := c.AllocateID()
60+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, children[i], root)
61+ registerProtocol(t, c, sub)
62+ if i < tiles {
63+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetPosition, int32(i%8*edge), int32(i/8*edge))
64+ } else {
65+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetPosition, int32(80), int32(0))
66+ }
67+ buffers[i] = c.AllocateID()
68+ registerProtocol(t, c, buffers[i])
69+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffers[i], int32(i*edge*stride), int32(edge), int32(edge), int32(stride), uint32(0))
70+ requestProtocol(t, c, children[i], wayland.SurfaceRequestAttach, buffers[i], int32(0), int32(0))
71+ requestProtocol(t, c, children[i], wayland.SurfaceRequestCommit)
72+ }
73+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
74+ if err := c.Roundtrip(); err != nil {
75+ t.Fatal(err)
76+ }
77+ wait := func() ports.SurfaceContent {
78+ t.Helper()
79+ deadline := time.After(5 * time.Second)
80+ for {
81+ select {
82+ case got := <-contents:
83+ if got.ID == win.ID && len(got.Children) == tiles+1 {
84+ return got
85+ }
86+ case <-deadline:
87+ t.Fatal("no tiled publication")
88+ }
89+ }
90+ }
91+ previous := wait()
92+ r, err := vulkan.New(128, 128)
93+ if err != nil {
94+ t.Skipf("Vulkan unavailable: %v", err)
95+ }
96+ defer r.Close()
97+ scene := ports.Scene{Windows: []ports.SceneWindow{{ID: win.ID, Rect: ports.Rect{W: 128, H: 128}}}}
98+ render := func(content ports.SurfaceContent) {
99+ t.Helper()
100+ fence, err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{win.ID: content})
101+ if fence != nil {
102+ _ = fence.Close()
103+ }
104+ if err != nil {
105+ t.Fatal(err)
106+ }
107+ }
108+ render(previous)
109+ copied := r.CopiedBytes()
110+ for i := range 120 {
111+ done := make(chan struct{}, 1)
112+ cb := c.AllocateID()
113+ p := &callbackBudgetProxy{done: done}
114+ p.SetID(cb)
115+ c.Context().Register(p)
116+ requestProtocol(t, c, children[tiles], wayland.SurfaceRequestFrame, cb)
117+ requestProtocol(t, c, children[tiles], wayland.SurfaceRequestAttach, buffers[tiles], int32(0), int32(0))
118+ requestProtocol(t, c, children[tiles], wayland.SurfaceRequestCommit)
119+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
120+ if err := c.Roundtrip(); err != nil {
121+ t.Fatal(err)
122+ }
123+ deadline := time.After(3 * time.Second)
124+ for {
125+ select {
126+ case <-done:
127+ goto callbackDone
128+ default:
129+ }
130+ result := make(chan error, 1)
131+ go func() { result <- c.Dispatch() }()
132+ select {
133+ case err := <-result:
134+ if err != nil {
135+ t.Fatal(err)
136+ }
137+ case <-deadline:
138+ _ = c.Close()
139+ t.Fatalf("callback %d not delivered", i)
140+ }
141+ }
142+ callbackDone:
143+ next := wait()
144+ render(next)
145+ if delta := r.CopiedBytes() - copied; delta != edge*edge*4 {
146+ t.Fatalf("frame %d copied %d bytes, want only the changing child's %d (root: id=%d version=%d size=%dx%d, previous root version=%d; first tile: id=%d version=%d previous=%d)", i, delta, edge*edge*4, next.Surface, next.Version, next.Width, next.Height, previous.Version, next.Children[0].Surface, next.Children[0].Version, previous.Children[0].Version)
147+ }
148+ copied = r.CopiedBytes()
149+ for tile := range tiles {
150+ if next.Children[tile].Version != previous.Children[tile].Version {
151+ t.Fatalf("unchanged tile %d changed version", tile)
152+ }
153+ }
154+ previous = next
155+ }
156+}
A internal/adapters/wayland/viewport_queue_test.go +240 -0
@@ -0,0 +1,240 @@
1+package wayland
2+
3+import (
4+ "fmt"
5+ "testing"
6+ "time"
7+
8+ "github.com/bnema/purego-libwayland/protocol/viewporter"
9+ "github.com/bnema/purego-libwayland/protocol/wayland"
10+)
11+
12+// A queued attachment that was destroyed cannot apply either its new crop or
13+// its scale to the buffer retained by a synchronized child.
14+func TestQueuedDestroyedAttachmentRetainsScaleAndCrop(t *testing.T) {
15+ s, events, _, contents, dir := contentServer(t)
16+ c := protocolClient(t, s, dir)
17+ w, root, _ := surfaceMapper(t, c, events)()
18+ drainContents(contents)
19+ comp := bindVersion(t, c, "wl_compositor", 4)
20+ subc := bindProtocol(t, c, "wl_subcompositor")
21+ vpm := bindProtocol(t, c, "wp_viewporter")
22+ child := c.AllocateID()
23+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
24+ registerProtocol(t, c, child)
25+ sub := c.AllocateID()
26+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
27+ registerProtocol(t, c, sub)
28+ vp := c.AllocateID()
29+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, child)
30+ registerProtocol(t, c, vp)
31+ retained, discarded := shmBuffer(t, c), shmBuffer(t, c)
32+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(256), int32(256))
33+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, retained, int32(0), int32(0))
34+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
35+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
36+ roundtrip(t, c)
37+ drainContents(contents)
38+ requestProtocol(t, c, child, wayland.SurfaceRequestSetBufferScale, int32(2))
39+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, discarded, int32(0), int32(0))
40+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
41+ requestProtocol(t, c, discarded, wayland.BufferRequestDestroy)
42+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
43+ roundtrip(t, c)
44+ deadline := time.After(2 * time.Second)
45+ for {
46+ select {
47+ case got := <-contents:
48+ if got.ID != w.ID {
49+ continue
50+ }
51+ if len(got.Children) != 1 || got.Children[0].Source != ([4]float32{0, 0, 1, 1}) || got.Children[0].LogicalW != 1 {
52+ t.Fatalf("retained buffer changed: %+v", got.Children)
53+ }
54+ return
55+ case <-deadline:
56+ t.Fatal("retained publication missing")
57+ }
58+ }
59+}
60+
61+// A scale-only commit validates the retained buffer; same-buffer reattaches
62+// still advance its version.
63+func TestScaleOnlyRetainedViewportAndSameBufferReattach(t *testing.T) {
64+ s, events, _, contents, dir := contentServer(t)
65+ c := protocolClient(t, s, dir)
66+ w, root, _ := surfaceMapper(t, c, events)()
67+ drainContents(contents)
68+ comp := bindVersion(t, c, "wl_compositor", 4)
69+ subc := bindProtocol(t, c, "wl_subcompositor")
70+ vpm := bindProtocol(t, c, "wp_viewporter")
71+ child := c.AllocateID()
72+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
73+ registerProtocol(t, c, child)
74+ sub := c.AllocateID()
75+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
76+ registerProtocol(t, c, sub)
77+ vp := c.AllocateID()
78+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, child)
79+ registerProtocol(t, c, vp)
80+ buf := shmBuffer(t, c)
81+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
82+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
83+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
84+ roundtrip(t, c)
85+ var first uint64
86+ deadline := time.After(2 * time.Second)
87+ for first == 0 {
88+ select {
89+ case got := <-contents:
90+ if got.ID == w.ID && len(got.Children) == 1 {
91+ first = got.Children[0].Version
92+ }
93+ case <-deadline:
94+ t.Fatal("initial child missing")
95+ }
96+ }
97+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
98+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
99+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
100+ roundtrip(t, c)
101+ deadline = time.After(2 * time.Second)
102+ for {
103+ select {
104+ case got := <-contents:
105+ if got.ID != w.ID || len(got.Children) != 1 {
106+ continue
107+ }
108+ if got.Children[0].Version != first+1 {
109+ t.Fatalf("same-buffer version %d, want %d", got.Children[0].Version, first+1)
110+ }
111+ goto reattached
112+ case <-deadline:
113+ t.Fatal("same-buffer publication missing")
114+ }
115+ }
116+reattached:
117+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(256), int32(256))
118+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
119+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
120+ roundtrip(t, c)
121+ requestProtocol(t, c, child, wayland.SurfaceRequestSetBufferScale, int32(2))
122+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
123+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
124+ expectProtocolError(t, c, vp, uint32(viewporter.WpViewportErrorOutOfBuffer))
125+}
126+
127+// A destroyed queued buffer does not suppress the later, valid attachment.
128+func TestQueuedDestroyedThenNewBuffer(t *testing.T) {
129+ s, events, _, contents, dir := contentServer(t)
130+ c := protocolClient(t, s, dir)
131+ w, root, _ := surfaceMapper(t, c, events)()
132+ drainContents(contents)
133+ comp := bindVersion(t, c, "wl_compositor", 4)
134+ subc := bindProtocol(t, c, "wl_subcompositor")
135+ vpm := bindProtocol(t, c, "wp_viewporter")
136+ child := c.AllocateID()
137+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
138+ registerProtocol(t, c, child)
139+ sub := c.AllocateID()
140+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
141+ registerProtocol(t, c, sub)
142+ vp := c.AllocateID()
143+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, child)
144+ registerProtocol(t, c, vp)
145+ first, destroyed, next := shmBuffer(t, c), shmBuffer(t, c), shmBuffer(t, c)
146+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(256), int32(256))
147+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, first, int32(0), int32(0))
148+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
149+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
150+ roundtrip(t, c)
151+ drainContents(contents)
152+ requestProtocol(t, c, child, wayland.SurfaceRequestSetBufferScale, int32(2))
153+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, destroyed, int32(0), int32(0))
154+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
155+ requestProtocol(t, c, destroyed, wayland.BufferRequestDestroy)
156+ requestProtocol(t, c, child, wayland.SurfaceRequestSetBufferScale, int32(1))
157+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, next, int32(0), int32(0))
158+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
159+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
160+ roundtrip(t, c)
161+ deadline := time.After(2 * time.Second)
162+ for {
163+ select {
164+ case got := <-contents:
165+ if got.ID != w.ID || len(got.Children) != 1 {
166+ continue
167+ }
168+ ch := got.Children[0]
169+ if ch.Version < 2 {
170+ continue
171+ }
172+ if ch.Version != 2 || ch.LogicalW != 1 || ch.Source != ([4]float32{0, 0, 1, 1}) {
173+ t.Fatalf("later buffer did not apply: %+v", ch)
174+ }
175+ return
176+ case <-deadline:
177+ t.Fatal("later buffer not published")
178+ }
179+ }
180+}
181+
182+// A destroyed viewport in an earlier queued update must not replace the new
183+// viewport or steal its protocol errors.
184+func TestQueuedViewportRecreatedErrorObject(t *testing.T) {
185+ for _, oversized := range []bool{false, true} {
186+ t.Run(fmt.Sprintf("oversized=%v", oversized), func(t *testing.T) {
187+ s, events, _, contents, dir := contentServer(t)
188+ c := protocolClient(t, s, dir)
189+ _, root, _ := surfaceMapper(t, c, events)()
190+ drainContents(contents)
191+ comp := bindVersion(t, c, "wl_compositor", 4)
192+ subc := bindProtocol(t, c, "wl_subcompositor")
193+ vpm := bindProtocol(t, c, "wp_viewporter")
194+ child := c.AllocateID()
195+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
196+ registerProtocol(t, c, child)
197+ sub := c.AllocateID()
198+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
199+ registerProtocol(t, c, sub)
200+ old := c.AllocateID()
201+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, old, child)
202+ registerProtocol(t, c, old)
203+ buf := shmBuffer(t, c)
204+ requestProtocol(t, c, old, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(256), int32(256))
205+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
206+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit) // old viewport queued
207+ requestProtocol(t, c, old, viewporter.WpViewportRequestDestroy)
208+ current := c.AllocateID()
209+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, current, child)
210+ registerProtocol(t, c, current)
211+ width := int32(256)
212+ if oversized {
213+ width = 512
214+ }
215+ requestProtocol(t, c, current, viewporter.WpViewportRequestSetSource, int32(0), int32(0), width, int32(256))
216+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
217+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
218+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
219+ if oversized {
220+ expectProtocolError(t, c, current, uint32(viewporter.WpViewportErrorOutOfBuffer))
221+ return
222+ }
223+ roundtrip(t, c)
224+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
225+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
226+ roundtrip(t, c)
227+ deadline := time.After(2 * time.Second)
228+ for {
229+ select {
230+ case got := <-contents:
231+ if len(got.Children) == 1 && got.Children[0].Source == ([4]float32{0, 0, 1, 1}) {
232+ return
233+ }
234+ case <-deadline:
235+ t.Fatal("new valid viewport not applied")
236+ }
237+ }
238+ })
239+ }
240+}
@@ -0,0 +1,240 @@
1+package wayland
2+
3+import (
4+ "fmt"
5+ "testing"
6+ "time"
7+
8+ "github.com/bnema/purego-libwayland/protocol/viewporter"
9+ "github.com/bnema/purego-libwayland/protocol/wayland"
10+)
11+
12+// A queued attachment that was destroyed cannot apply either its new crop or
13+// its scale to the buffer retained by a synchronized child.
14+func TestQueuedDestroyedAttachmentRetainsScaleAndCrop(t *testing.T) {
15+ s, events, _, contents, dir := contentServer(t)
16+ c := protocolClient(t, s, dir)
17+ w, root, _ := surfaceMapper(t, c, events)()
18+ drainContents(contents)
19+ comp := bindVersion(t, c, "wl_compositor", 4)
20+ subc := bindProtocol(t, c, "wl_subcompositor")
21+ vpm := bindProtocol(t, c, "wp_viewporter")
22+ child := c.AllocateID()
23+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
24+ registerProtocol(t, c, child)
25+ sub := c.AllocateID()
26+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
27+ registerProtocol(t, c, sub)
28+ vp := c.AllocateID()
29+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, child)
30+ registerProtocol(t, c, vp)
31+ retained, discarded := shmBuffer(t, c), shmBuffer(t, c)
32+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(256), int32(256))
33+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, retained, int32(0), int32(0))
34+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
35+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
36+ roundtrip(t, c)
37+ drainContents(contents)
38+ requestProtocol(t, c, child, wayland.SurfaceRequestSetBufferScale, int32(2))
39+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, discarded, int32(0), int32(0))
40+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
41+ requestProtocol(t, c, discarded, wayland.BufferRequestDestroy)
42+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
43+ roundtrip(t, c)
44+ deadline := time.After(2 * time.Second)
45+ for {
46+ select {
47+ case got := <-contents:
48+ if got.ID != w.ID {
49+ continue
50+ }
51+ if len(got.Children) != 1 || got.Children[0].Source != ([4]float32{0, 0, 1, 1}) || got.Children[0].LogicalW != 1 {
52+ t.Fatalf("retained buffer changed: %+v", got.Children)
53+ }
54+ return
55+ case <-deadline:
56+ t.Fatal("retained publication missing")
57+ }
58+ }
59+}
60+
61+// A scale-only commit validates the retained buffer; same-buffer reattaches
62+// still advance its version.
63+func TestScaleOnlyRetainedViewportAndSameBufferReattach(t *testing.T) {
64+ s, events, _, contents, dir := contentServer(t)
65+ c := protocolClient(t, s, dir)
66+ w, root, _ := surfaceMapper(t, c, events)()
67+ drainContents(contents)
68+ comp := bindVersion(t, c, "wl_compositor", 4)
69+ subc := bindProtocol(t, c, "wl_subcompositor")
70+ vpm := bindProtocol(t, c, "wp_viewporter")
71+ child := c.AllocateID()
72+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
73+ registerProtocol(t, c, child)
74+ sub := c.AllocateID()
75+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
76+ registerProtocol(t, c, sub)
77+ vp := c.AllocateID()
78+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, child)
79+ registerProtocol(t, c, vp)
80+ buf := shmBuffer(t, c)
81+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
82+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
83+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
84+ roundtrip(t, c)
85+ var first uint64
86+ deadline := time.After(2 * time.Second)
87+ for first == 0 {
88+ select {
89+ case got := <-contents:
90+ if got.ID == w.ID && len(got.Children) == 1 {
91+ first = got.Children[0].Version
92+ }
93+ case <-deadline:
94+ t.Fatal("initial child missing")
95+ }
96+ }
97+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
98+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
99+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
100+ roundtrip(t, c)
101+ deadline = time.After(2 * time.Second)
102+ for {
103+ select {
104+ case got := <-contents:
105+ if got.ID != w.ID || len(got.Children) != 1 {
106+ continue
107+ }
108+ if got.Children[0].Version != first+1 {
109+ t.Fatalf("same-buffer version %d, want %d", got.Children[0].Version, first+1)
110+ }
111+ goto reattached
112+ case <-deadline:
113+ t.Fatal("same-buffer publication missing")
114+ }
115+ }
116+reattached:
117+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(256), int32(256))
118+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
119+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
120+ roundtrip(t, c)
121+ requestProtocol(t, c, child, wayland.SurfaceRequestSetBufferScale, int32(2))
122+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
123+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
124+ expectProtocolError(t, c, vp, uint32(viewporter.WpViewportErrorOutOfBuffer))
125+}
126+
127+// A destroyed queued buffer does not suppress the later, valid attachment.
128+func TestQueuedDestroyedThenNewBuffer(t *testing.T) {
129+ s, events, _, contents, dir := contentServer(t)
130+ c := protocolClient(t, s, dir)
131+ w, root, _ := surfaceMapper(t, c, events)()
132+ drainContents(contents)
133+ comp := bindVersion(t, c, "wl_compositor", 4)
134+ subc := bindProtocol(t, c, "wl_subcompositor")
135+ vpm := bindProtocol(t, c, "wp_viewporter")
136+ child := c.AllocateID()
137+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
138+ registerProtocol(t, c, child)
139+ sub := c.AllocateID()
140+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
141+ registerProtocol(t, c, sub)
142+ vp := c.AllocateID()
143+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, child)
144+ registerProtocol(t, c, vp)
145+ first, destroyed, next := shmBuffer(t, c), shmBuffer(t, c), shmBuffer(t, c)
146+ requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(256), int32(256))
147+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, first, int32(0), int32(0))
148+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
149+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
150+ roundtrip(t, c)
151+ drainContents(contents)
152+ requestProtocol(t, c, child, wayland.SurfaceRequestSetBufferScale, int32(2))
153+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, destroyed, int32(0), int32(0))
154+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
155+ requestProtocol(t, c, destroyed, wayland.BufferRequestDestroy)
156+ requestProtocol(t, c, child, wayland.SurfaceRequestSetBufferScale, int32(1))
157+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, next, int32(0), int32(0))
158+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
159+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
160+ roundtrip(t, c)
161+ deadline := time.After(2 * time.Second)
162+ for {
163+ select {
164+ case got := <-contents:
165+ if got.ID != w.ID || len(got.Children) != 1 {
166+ continue
167+ }
168+ ch := got.Children[0]
169+ if ch.Version < 2 {
170+ continue
171+ }
172+ if ch.Version != 2 || ch.LogicalW != 1 || ch.Source != ([4]float32{0, 0, 1, 1}) {
173+ t.Fatalf("later buffer did not apply: %+v", ch)
174+ }
175+ return
176+ case <-deadline:
177+ t.Fatal("later buffer not published")
178+ }
179+ }
180+}
181+
182+// A destroyed viewport in an earlier queued update must not replace the new
183+// viewport or steal its protocol errors.
184+func TestQueuedViewportRecreatedErrorObject(t *testing.T) {
185+ for _, oversized := range []bool{false, true} {
186+ t.Run(fmt.Sprintf("oversized=%v", oversized), func(t *testing.T) {
187+ s, events, _, contents, dir := contentServer(t)
188+ c := protocolClient(t, s, dir)
189+ _, root, _ := surfaceMapper(t, c, events)()
190+ drainContents(contents)
191+ comp := bindVersion(t, c, "wl_compositor", 4)
192+ subc := bindProtocol(t, c, "wl_subcompositor")
193+ vpm := bindProtocol(t, c, "wp_viewporter")
194+ child := c.AllocateID()
195+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
196+ registerProtocol(t, c, child)
197+ sub := c.AllocateID()
198+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
199+ registerProtocol(t, c, sub)
200+ old := c.AllocateID()
201+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, old, child)
202+ registerProtocol(t, c, old)
203+ buf := shmBuffer(t, c)
204+ requestProtocol(t, c, old, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(256), int32(256))
205+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
206+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit) // old viewport queued
207+ requestProtocol(t, c, old, viewporter.WpViewportRequestDestroy)
208+ current := c.AllocateID()
209+ requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, current, child)
210+ registerProtocol(t, c, current)
211+ width := int32(256)
212+ if oversized {
213+ width = 512
214+ }
215+ requestProtocol(t, c, current, viewporter.WpViewportRequestSetSource, int32(0), int32(0), width, int32(256))
216+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
217+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
218+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
219+ if oversized {
220+ expectProtocolError(t, c, current, uint32(viewporter.WpViewportErrorOutOfBuffer))
221+ return
222+ }
223+ roundtrip(t, c)
224+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
225+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
226+ roundtrip(t, c)
227+ deadline := time.After(2 * time.Second)
228+ for {
229+ select {
230+ case got := <-contents:
231+ if len(got.Children) == 1 && got.Children[0].Source == ([4]float32{0, 0, 1, 1}) {
232+ return
233+ }
234+ case <-deadline:
235+ t.Fatal("new valid viewport not applied")
236+ }
237+ }
238+ })
239+ }
240+}
M internal/ports/messages.go +10 -4
@@ -776,14 +776,20 @@ func (c SurfaceColor) IsExtendedLinear() bool {
776776 // geometry), the rest being client shadows.
777777 type SurfaceContent struct {
778778 ID WindowID
779+ // Surface is a never-reused wl_surface identity; Version identifies its
780+ // applied buffer content independently of the window publication Seq.
781+ Surface, Version uint64
779782 // Seq counts the window's contents (wayland sets it), for release.
780783 Seq uint64
781784 Width, Height int
782785 LogicalW, LogicalH int
783- Opaque bool // x formats: ignore the alpha byte
784- Color SurfaceColor
785- SHM *SHMBuffer
786- DMABuf *DMABuf
786+ // Source is the committed crop in buffer pixels: x, y, width, height.
787+ // Zero width means the entire buffer is used.
788+ Source [4]float32
789+ Opaque bool // x formats: ignore the alpha byte
790+ Color SurfaceColor
791+ SHM *SHMBuffer
792+ DMABuf *DMABuf
787793 // Children are the subsurfaces, bottom to top, flattened.
788794 Children []Subsurface
789795 // Geometry is in logical pixels from the surface origin; empty means
@@ -776,14 +776,20 @@ func (c SurfaceColor) IsExtendedLinear() bool {
776 // geometry), the rest being client shadows.776 // geometry), the rest being client shadows.
777 type SurfaceContent struct {777 type SurfaceContent struct {
778 ID WindowID778 ID WindowID
779+ // Surface is a never-reused wl_surface identity; Version identifies its
780+ // applied buffer content independently of the window publication Seq.
781+ Surface, Version uint64
779 // Seq counts the window's contents (wayland sets it), for release.782 // Seq counts the window's contents (wayland sets it), for release.
780 Seq uint64783 Seq uint64
781 Width, Height int784 Width, Height int
782 LogicalW, LogicalH int785 LogicalW, LogicalH int
783- Opaque bool // x formats: ignore the alpha byte786+ // Source is the committed crop in buffer pixels: x, y, width, height.
784- Color SurfaceColor787+ // Zero width means the entire buffer is used.
785- SHM *SHMBuffer788+ Source [4]float32
786- DMABuf *DMABuf789+ Opaque bool // x formats: ignore the alpha byte
790+ Color SurfaceColor
791+ SHM *SHMBuffer
792+ DMABuf *DMABuf
787 // Children are the subsurfaces, bottom to top, flattened.793 // Children are the subsurfaces, bottom to top, flattened.
788 Children []Subsurface794 Children []Subsurface
789 // Geometry is in logical pixels from the surface origin; empty means795 // Geometry is in logical pixels from the surface origin; empty means

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