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 Unverified
B5690EEEBB952194The 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 | |
| 2 | 2 | ||
| 3 | 3 | # 0 runs until quit; set e.g. TTY_TIMEOUT=60s for a safety net. | |
| 4 | 4 | TTY_TIMEOUT ?= 0 | |
| @@ -48,7 +48,10 @@ fakes-check: | |||
| 48 | 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 | 49 | arch: | |
| 50 | 50 | $(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 | |
| 52 | 55 | ||
| 53 | 56 | # Arch package of the committed HEAD (packaging/arch/PKGBUILD). Go modules | |
| 54 | 57 | # 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 pkg | 1 | + | .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 ?= 0 | 4 | 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-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 | |||
| 52 | 55 | ||||
| 53 | # Arch package of the committed HEAD (packaging/arch/PKGBUILD). Go modules | 56 | # 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 { | |||
| 52 | 52 | rect [4]int32 | |
| 53 | 53 | mapv [4]float32 | |
| 54 | 54 | color [4]float32 | |
| 55 | + | crop [4]float32 | |
| 55 | 56 | buf [4]uint32 | |
| 56 | 57 | misc [4]uint32 | |
| 57 | 58 | } | |
| @@ -298,16 +299,20 @@ func (r *Renderer) fillDraw(rect image.Rectangle, c [3]uint8) draw { | |||
| 298 | 299 | ||
| 299 | 300 | // contentDraw draws the visible rect of a w×h buffer mapped onto full | |
| 300 | 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 | 303 | 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 | 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 | 311 | flags := uint32(0) | |
| 307 | 312 | if opaque { | |
| 308 | 313 | 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 | 316 | flags |= flagExact | |
| 312 | 317 | } | |
| 313 | 318 | dr.pc.misc = [4]uint32{mode, flags, uint32(r.width), uint32(r.height)} | |
| @@ -52,6 +52,7 @@ type pushConstants struct { | |||||
| 52 | rect [4]int32 | 52 | rect [4]int32 | ||
| 53 | mapv [4]float32 | 53 | mapv [4]float32 | ||
| 54 | color [4]float32 | 54 | color [4]float32 | ||
| 55 | + | crop [4]float32 | |||
| 55 | buf [4]uint32 | 56 | buf [4]uint32 | ||
| 56 | misc [4]uint32 | 57 | 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 full | 300 | // 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 draw | 303 | 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 |= flagOpaque | 313 | 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 |= flagExact | 316 | 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) { | |||
| 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 | 70 | // Scaled dmabufs are sampled with the linear sampler. | |
| 47 | 71 | func TestRendererScaledDMABufFiltersLinearly(t *testing.T) { | |
| 48 | 72 | r, err := New(128, 4) | |
| @@ -107,7 +131,7 @@ func TestRendererSHMCopiedOncePerContent(t *testing.T) { | |||
| 107 | 131 | } | |
| 108 | 132 | defer r.Close() | |
| 109 | 133 | 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 | |
| 111 | 135 | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}} | |
| 112 | 136 | contents := map[ports.WindowID]ports.SurfaceContent{1: c} | |
| 113 | 137 | render := func() { | |
| @@ -130,7 +154,7 @@ func TestRendererSHMCopiedOncePerContent(t *testing.T) { | |||
| 130 | 154 | if r.copied != 16*16*4 { | |
| 131 | 155 | t.Fatalf("unchanged content copied again: %d", r.copied) | |
| 132 | 156 | } | |
| 133 | - | c.Seq = 2 | |
| 157 | + | c.Seq, c.Version = 2, 2 | |
| 134 | 158 | contents[1] = c | |
| 135 | 159 | render() | |
| 136 | 160 | if r.copied != 2*16*16*4 { | |
| @@ -148,8 +172,9 @@ func TestRendererSkipsOccludedSurface(t *testing.T) { | |||
| 148 | 172 | defer r.Close() | |
| 149 | 173 | green := color.RGBA{0, 255, 0, 255} | |
| 150 | 174 | 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 | |
| 152 | 176 | game := solidContent(t, 16, 16, green) | |
| 177 | + | game.Surface, game.Version = 2, 1 | |
| 153 | 178 | game.Opaque = true | |
| 154 | 179 | root.Children = []ports.Subsurface{{SurfaceContent: game}} | |
| 155 | 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 | 190 | // A smaller opaque child leaves part of the root visible: it is copied. | |
| 166 | 191 | small := solidContent(t, 8, 8, green) | |
| 192 | + | small.Surface, small.Version = 3, 1 | |
| 167 | 193 | small.Opaque = true | |
| 168 | 194 | root.Seq = 2 | |
| 169 | 195 | root.Children = []ports.Subsurface{{SurfaceContent: small}} | |
| @@ -227,7 +253,7 @@ func TestRendererSHMDoubleBufferWaitsForReaders(t *testing.T) { | |||
| 227 | 253 | var readers []uint64 | |
| 228 | 254 | for seq := uint64(1); seq <= 3; seq++ { | |
| 229 | 255 | 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 | |
| 231 | 257 | if seq == 3 { | |
| 232 | 258 | // The buffer about to be reused was last read by frame 1. | |
| 233 | 259 | 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, 1 | 134 | + | 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 = 2 | 157 | + | c.Seq, c.Version = 2, 2 |
| 134 | contents[1] = c | 158 | 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, 1 | 175 | + | 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 = true | 178 | 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 = true | 193 | small.Opaque = true | ||
| 168 | root.Seq = 2 | 194 | 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 []uint64 | 253 | 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, seq | 256 | + | 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 | |||
| 310 | 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 | 317 | func (r *Renderer) Pixels() *image.RGBA { | |
| 314 | 318 | out := image.NewRGBA(image.Rect(0, 0, r.width, r.height)) | |
| 315 | 319 | 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) { | |||
| 321 | 321 | scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 10, Y: 10, W: 20, H: 10}}}} | |
| 322 | 322 | // A 30×20 surface whose window is the 20×10 at (5, 5): a 5px shadow. | |
| 323 | 323 | root := solidContent(t, 30, 20, red) | |
| 324 | + | root.Surface, root.Version = 1, 1 | |
| 324 | 325 | root.Geometry = ports.Rect{X: 5, Y: 5, W: 20, H: 10} | |
| 325 | 326 | // Green covers the window's top-left 4×4 corner; blue sits below the | |
| 326 | 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 | 332 | 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}, | |
| 330 | 335 | } | |
| 331 | 336 | if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil { | |
| 332 | 337 | 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 the | 326 | // 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) { | |||
| 136 | 136 | ||
| 137 | 137 | // place draws a surface tree with its window geometry at (x, y) logical, | |
| 138 | 138 | // 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. | |
| 141 | 140 | func (w *sceneWalk) place(id ports.WindowID, content *ports.SurfaceContent, x, y, width, height int) { | |
| 142 | 141 | clip := image.Rect(x, y, x+width, y+height) | |
| 143 | 142 | ox, oy := x-content.Geometry.X, y-content.Geometry.Y | |
| 144 | 143 | covers := w.opaqueChildren(content, ox, oy, clip) | |
| 145 | 144 | for i := range content.Children { | |
| 146 | 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 | 150 | for i := range content.Children { | |
| 152 | 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 | 208 | // A buffer drawn at the physical size (fractional-scale clients round | |
| 210 | 209 | // w*scale, we round per edge) is copied 1:1, never resampled for 1px. | |
| 211 | 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) { | |
| 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)) | |
| 214 | 217 | } | |
| 215 | 218 | dst = full.Intersect(w.physRect(clip.Min.X, clip.Min.Y, clip.Dx(), clip.Dy())) | |
| 216 | 219 | return full, dst, !dst.Empty() | |
| @@ -219,7 +222,7 @@ func (w *sceneWalk) surfaceRects(content *ports.SurfaceContent, x, y int, clip i | |||
| 219 | 222 | // surface draws one surface buffer with its origin at (x, y) logical, | |
| 220 | 223 | // clipped to clip (logical), unless an opaque surface above it (covers, | |
| 221 | 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 | 226 | full, dst, ok := w.surfaceRects(content, x, y, clip) | |
| 224 | 227 | if !ok { | |
| 225 | 228 | return | |
| @@ -229,15 +232,15 @@ func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y int, clip image. | |||
| 229 | 232 | return | |
| 230 | 233 | } | |
| 231 | 234 | } | |
| 232 | - | if key.index == 0 { | |
| 235 | + | if root { | |
| 233 | 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 | 241 | // content draws the part of a buffer mapped onto full (physical pixels) | |
| 239 | 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 | 244 | r := w.r | |
| 242 | 245 | rect := dst.Intersect(w.bounds) | |
| 243 | 246 | if rect.Empty() { | |
| @@ -249,7 +252,7 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon | |||
| 249 | 252 | // The window shows its background until a buffer imports. | |
| 250 | 253 | 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 | 256 | r.setContentColor(&dr, content.Color) | |
| 254 | 257 | if im.yuv { | |
| 255 | 258 | dr.pc.misc[1] |= flagYUV | |
| @@ -275,17 +278,17 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon | |||
| 275 | 278 | if err != nil { | |
| 276 | 279 | 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 | 282 | // Damage history is the root surface's: children copy in full. | |
| 280 | 283 | var damage func(uint64) ([]ports.Rect, bool) | |
| 281 | - | if key.index == 0 { | |
| 284 | + | if root { | |
| 282 | 285 | damage = content.DamageSince | |
| 283 | 286 | } | |
| 284 | 287 | c, err := r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage) | |
| 285 | 288 | if err != nil { | |
| 286 | 289 | 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 | 292 | r.setContentColor(&dr, content.Color) | |
| 290 | 293 | dr.set = c.set | |
| 291 | 294 | 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. Every | 138 | // 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 | 139 | + | // 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.Y | 142 | 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 round | 208 | // 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 | return | 228 | return | ||
| @@ -229,15 +232,15 @@ func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y int, clip image. | |||||
| 229 | return | 232 | 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.r | 244 | 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 | return | 253 | 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] |= flagYUV | 258 | 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 | return | 279 | 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.DamageSince | 285 | 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 | return | 289 | 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.set | 293 | 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 { | |||
| 9 | 9 | ivec4 rect; | |
| 10 | 10 | vec4 map; | |
| 11 | 11 | vec4 color; | |
| 12 | + | vec4 crop; | |
| 12 | 13 | uvec4 buf; | |
| 13 | 14 | uvec4 misc; | |
| 14 | 15 | } d; | |
| @@ -49,9 +50,13 @@ vec3 pqToSRGB(vec3 v, float whiteNits) { | |||
| 49 | 50 | // vec2 coordinates are normalized to the chroma image for linear filtering. | |
| 50 | 51 | vec4 sampleYUV(vec2 src) { | |
| 51 | 52 | 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); | |
| 55 | 60 | vec2 cbcr = texture(chromaTex, chromaUV).rg; | |
| 56 | 61 | bool tenBit = (d.misc.y & flagP010) != 0u; | |
| 57 | 62 | bool limited = ((d.buf.w >> 8) & 255u) == 2u; | |
| @@ -73,7 +78,7 @@ vec4 sampleYUV(vec2 src) { | |||
| 73 | 78 | ||
| 74 | 79 | // texel reads buffer pixel p, clamped to the buffer (clamp to edge). | |
| 75 | 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 | 82 | // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A). | |
| 78 | 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 | 93 | c = sampleYUV(src); | |
| 89 | 94 | } else if (d.misc.x == modeImage) { | |
| 90 | 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 | 98 | } else if (exact) { | |
| 93 | 99 | c = texel(ivec2(floor(src))); | |
| 94 | 100 | } 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 { | |||
| 5 | 5 | ivec4 rect; // visible x0, y0, x1, y1 in target pixels | |
| 6 | 6 | vec4 map; // source texel = map.xy + fragment centre * map.zw | |
| 7 | 7 | vec4 color; // solid fills: premultiplied RGBA | |
| 8 | + | vec4 crop; // source crop bounds in buffer pixels | |
| 8 | 9 | uvec4 buf; // buffer draws: first word, row width, height, unused | |
| 9 | 10 | uvec4 misc; // mode, flags, target width, target height | |
| 10 | 11 | } d; | |
| @@ -5,6 +5,7 @@ layout(push_constant) uniform Draw { | |||||
| 5 | ivec4 rect; // visible x0, y0, x1, y1 in target pixels | 5 | ivec4 rect; // visible x0, y0, x1, y1 in target pixels | ||
| 6 | vec4 map; // source texel = map.xy + fragment centre * map.zw | 6 | vec4 map; // source texel = map.xy + fragment centre * map.zw | ||
| 7 | vec4 color; // solid fills: premultiplied RGBA | 7 | 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, unused | 9 | uvec4 buf; // buffer draws: first word, row width, height, unused | ||
| 9 | uvec4 misc; // mode, flags, target width, target height | 10 | 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 { | |||
| 9 | 9 | ivec4 rect; | |
| 10 | 10 | vec4 map; | |
| 11 | 11 | vec4 color; | |
| 12 | + | vec4 crop; | |
| 12 | 13 | uvec4 buf; | |
| 13 | 14 | uvec4 misc; | |
| 14 | 15 | } d; | |
| @@ -48,9 +49,13 @@ vec3 pqToLinear709(vec3 v, float whiteNits) { | |||
| 48 | 49 | // vec2 coordinates are normalized to the chroma image for linear filtering. | |
| 49 | 50 | vec4 sampleYUV(vec2 src) { | |
| 50 | 51 | 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); | |
| 54 | 59 | vec2 cbcr = texture(chromaTex, chromaUV).rg; | |
| 55 | 60 | bool tenBit = (d.misc.y & flagP010) != 0u; | |
| 56 | 61 | bool limited = ((d.buf.w >> 8) & 255u) == 2u; | |
| @@ -72,7 +77,7 @@ vec4 sampleYUV(vec2 src) { | |||
| 72 | 77 | ||
| 73 | 78 | // texel reads buffer pixel p, clamped to the buffer (clamp to edge). | |
| 74 | 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 | 81 | // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A). | |
| 77 | 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 | 92 | c = sampleYUV(src); | |
| 88 | 93 | } else if (d.misc.x == modeImage) { | |
| 89 | 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 | 97 | } else if (exact) { | |
| 92 | 98 | c = texel(ivec2(floor(src))); | |
| 93 | 99 | } 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) { | |||
| 148 | 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 | 152 | type shmKey struct { | |
| 153 | - | win ports.WindowID | |
| 154 | - | index int | |
| 153 | + | win ports.WindowID | |
| 154 | + | surface uint64 | |
| 155 | 155 | } | |
| 156 | 156 | ||
| 157 | 157 | // shmState is what a GPU buffer holds: a w×h client buffer at a content | |
| 158 | 158 | // Seq. | |
| 159 | 159 | 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 | |
| 162 | 163 | } | |
| 163 | 164 | ||
| 164 | 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 | 191 | s.last = r.frame | |
| 191 | 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 | 194 | c.gpu.last = r.frame | |
| 194 | 195 | return c, nil | |
| 195 | 196 | } | |
| @@ -227,7 +228,7 @@ func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, st | |||
| 227 | 228 | row := st.w * 4 | |
| 228 | 229 | rects := []ports.Rect{{W: st.w, H: st.h}} | |
| 229 | 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 | 232 | rects = d | |
| 232 | 233 | } | |
| 233 | 234 | } | |
| @@ -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.WindowID | 153 | + | win ports.WindowID |
| 154 | - | index int | 154 | + | surface uint64 |
| 155 | } | 155 | } | ||
| 156 | 156 | ||||
| 157 | // shmState is what a GPU buffer holds: a w×h client buffer at a content | 157 | // 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 int | 160 | + | w, h int |
| 161 | - | seq uint64 | 161 | + | 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.frame | 191 | 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.frame | 194 | c.gpu.last = r.frame | ||
| 194 | return c, nil | 195 | 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 * 4 | 228 | 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 = d | 232 | rects = d | ||
| 232 | } | 233 | } | ||
| 233 | } | 234 | } | ||
M internal/adapters/vulkan/yuv_test.go +31 -1
| @@ -13,7 +13,7 @@ import ( | |||
| 13 | 13 | ) | |
| 14 | 14 | ||
| 15 | 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 | 17 | t.Helper() | |
| 18 | 18 | format := fourcc('N', 'V', '1', '2') | |
| 19 | 19 | if p010 { | |
| @@ -49,6 +49,13 @@ func yuvTestBuffer(t *testing.T, p010 bool, y, u, v int) *ports.DMABuf { | |||
| 49 | 49 | } | |
| 50 | 50 | for n, ch := start, 0; n < end; ch++ { | |
| 51 | 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 | 59 | if p010 { | |
| 53 | 60 | code := uint16(val << 6) | |
| 54 | 61 | data[n], data[n+1] = byte(code), byte(code>>8) | |
| @@ -159,3 +166,26 @@ func pqDecode(code float64) float64 { | |||
| 159 | 166 | p := math.Pow(code, 32.0/2523) | |
| 160 | 167 | 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 | + | } | |
| @@ -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) * 10000 | 167 | 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 { | |||
| 198 | 198 | ||
| 199 | 199 | // update is a surface's pending state taken out by a commit that waits. | |
| 200 | 200 | 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 { | |
| 229 | 237 | child *surface | |
| 230 | 238 | 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 | 265 | // takePending moves the pending state into an update: one-shot state is | |
| @@ -239,14 +271,9 @@ func (s *surface) takePending() update { | |||
| 239 | 271 | s.pendingDamage, s.pendingBufDamage = nil, nil | |
| 240 | 272 | s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{} | |
| 241 | 273 | 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 | |
| 249 | 275 | } | |
| 276 | + | u.layout = s.sub.pendingLayout | |
| 250 | 277 | if s.xdg != nil { | |
| 251 | 278 | u.xdg, u.geometry = s.xdg, s.xdg.pendingGeometry | |
| 252 | 279 | } | |
| @@ -269,14 +296,14 @@ func (s *surface) putPending(u update) { | |||
| 269 | 296 | s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at | |
| 270 | 297 | s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage | |
| 271 | 298 | 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 | 301 | 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 { | |
| 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 | |
| 280 | 307 | if u.xdg != nil && s.xdg == u.xdg { | |
| 281 | 308 | 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 | 318 | // tooEarly reports whether content applied now would show before at: | |
| 297 | 319 | // it reaches the screen one refresh later. | |
| 298 | 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 | 342 | // queueUpdate queues the pending commit until it can apply. | |
| 321 | 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 | 360 | s.server.fifoSurfaces[s] = struct{}{} | |
| 324 | 361 | s.server.wakePacer() | |
| 325 | 362 | } | |
| 326 | 363 | ||
| 327 | 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 | 366 | // A buffer destroyed while queued (a swapchain resize) is skipped: the | |
| 330 | 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 | 374 | if u.buffer != nil && !u.buffer.Resource.Alive() { | |
| 375 | + | s.commitSkipped = true | |
| 332 | 376 | 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 | 389 | later := s.takePending() | |
| 335 | - | s.putPending(u) | |
| 390 | + | originalViewport := s.viewport | |
| 391 | + | originalLayout := s.sub.pendingLayout | |
| 392 | + | s.putPending(*u) | |
| 336 | 393 | s.applyCommit() | |
| 337 | 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 | 404 | // dropQueue discards the queued updates of a destroyed surface or role. | |
| @@ -356,7 +420,7 @@ func (s *surface) dropQueue() { | |||
| 356 | 420 | if u.sync != nil { | |
| 357 | 421 | 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 | 424 | released[u.buffer.Resource] = true | |
| 361 | 425 | u.buffer.SendRelease() | |
| 362 | 426 | } | |
| @@ -365,6 +429,23 @@ func (s *surface) dropQueue() { | |||
| 365 | 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 | 449 | // setBarrier applies a committed set_barrier. | |
| 369 | 450 | func (s *surface) setBarrier(now time.Time) { | |
| 370 | 451 | 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 | 489 | wait := defaultFramePeriod | |
| 490 | + | s.readinessGeneration++ | |
| 409 | 491 | for surf := range s.fifoSurfaces { | |
| 410 | 492 | name := s.fifoOutput(surf) | |
| 411 | 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 | 502 | for len(surf.queue) > 0 && !surf.destroyed { | |
| 421 | 503 | u := surf.queue[0] | |
| 422 | - | if u.wait && surf.barrier { | |
| 504 | + | if u.synced || !u.graphReady(now) { | |
| 423 | 505 | break | |
| 424 | 506 | } | |
| 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() | |
| 427 | 516 | } | |
| 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) | |
| 431 | 534 | } | |
| 432 | - | surf.queue = surf.queue[1:] | |
| 433 | - | surf.applyUpdate(u) | |
| 535 | + | s.graphFeedback = s.graphFeedback[:0] | |
| 434 | 536 | } | |
| 435 | 537 | if surf.barrier { | |
| 436 | 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 | 544 | 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 | 614 | // sameBuffer reports whether two wrappers are the same wl_buffer. | |
| 446 | 615 | func sameBuffer(a, b *wayland.Buffer) bool { | |
| 447 | 616 | 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 bool | 201 | + | attached bool |
| 202 | - | buffer *wayland.Buffer | 202 | + | buffer *wayland.Buffer |
| 203 | - | scale int | 203 | + | scale int |
| 204 | - | async bool | 204 | + | async bool |
| 205 | - | kind uint32 | 205 | + | kind uint32 |
| 206 | - | callbacks []*wayland.Callback | 206 | + | callbacks []*wayland.Callback |
| 207 | - | vp *viewport | 207 | + | vp *viewport |
| 208 | - | vpW, vpH int32 | 208 | + | vpW, vpH int32 |
| 209 | - | vpSet bool | 209 | + | vpSet bool |
| 210 | - | moves []childMove | 210 | + | vpSrc [4]server.Fixed |
| 211 | - | xdg *xdgSurface | 211 | + | vpCrop bool |
| 212 | - | geometry ports.Rect | 212 | + | layout []childLayout |
| 213 | - | cons *constraint | 213 | + | deps []*update |
| 214 | - | region *ports.Rect | 214 | + | owner *surface |
| 215 | - | layer *layerSurface | 215 | + | prev *update |
| 216 | - | layerNext layerState | 216 | + | synced, bound bool |
| 217 | - | barrier bool | 217 | + | xdg *xdgSurface |
| 218 | - | wait bool | 218 | + | geometry ports.Rect |
| 219 | - | at time.Time | 219 | + | cons *constraint |
| 220 | - | feedback []*presentationtime.WpPresentationFeedback | 220 | + | region *ports.Rect |
| 221 | - | sync *commitSync | 221 | + | layer *layerSurface |
| 222 | - | color SurfaceColor | 222 | + | layerNext layerState |
| 223 | - | representation surfaceRepresentation | 223 | + | barrier bool |
| 224 | - | damage []ports.Rect | 224 | + | wait bool |
| 225 | - | bufDamage []ports.Rect | 225 | + | 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 *surface | 237 | child *surface | ||
| 230 | x, y int | 238 | 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 is | 265 | // 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, nil | 271 | 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.pendingSet | 274 | + | 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.pendingGeometry | 278 | 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.at | 296 | s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at | ||
| 270 | s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage | 297 | s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage | ||
| 271 | s.pendingFeedback, s.pendingSync = u.feedback, u.sync | 298 | 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.vpSet | 301 | 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.geometry | 308 | 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: the | 366 | // 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, false | 376 | 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 release | 421 | 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] = true | 424 | 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, now | 451 | 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 := defaultFramePeriod | 489 | 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 | break | 505 | break | ||
| 424 | } | 506 | } | ||
| 425 | - | if !surf.syncReady(u.sync) { | 507 | + | s.applyingGraph = true |
| 426 | - | break | 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() | |||
| 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 | - | break | 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) | |||
| 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) > 0 | 544 | 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.Resource | 616 | 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) { | |||
| 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 | + | } | |
| @@ -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 } | |||
| 75 | 75 | func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) { | |
| 76 | 76 | state := &surface{server: c.server, bufferScale: 1} | |
| 77 | 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 | 80 | state.wl = w | |
| 79 | 81 | c.server.surfaces[w.Resource] = state | |
| 80 | 82 | state.sendScale() | |
| @@ -118,8 +120,9 @@ func (c subcompositor) GetSubsurface(r *wayland.Subcompositor, id uint32, w, par | |||
| 118 | 120 | } | |
| 119 | 121 | state.kind = roleSubsurface | |
| 120 | 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 | 124 | up.sub.children = append(up.sub.children, state) | |
| 125 | + | up.sub.pendingLayout = append(append([]childLayout(nil), up.sub.pendingLayout...), childLayout{child: state}) | |
| 123 | 126 | // The child follows its root window's output and scale. | |
| 124 | 127 | state.sendScale() | |
| 125 | 128 | sub.OnDestroy = func() { | |
| @@ -138,7 +141,18 @@ type subsurface struct{ surface *surface } | |||
| 138 | 141 | ||
| 139 | 142 | func (subsurface) Destroy(*wayland.Subsurface) {} | |
| 140 | 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), 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 | + | } | |
| 142 | 156 | } | |
| 143 | 157 | func (s subsurface) PlaceAbove(r *wayland.Subsurface, sibling *wayland.Surface) { | |
| 144 | 158 | s.restack(r, sibling, true) | |
| @@ -146,11 +160,16 @@ func (s subsurface) PlaceAbove(r *wayland.Subsurface, sibling *wayland.Surface) | |||
| 146 | 160 | func (s subsurface) PlaceBelow(r *wayland.Subsurface, sibling *wayland.Surface) { | |
| 147 | 161 | s.restack(r, sibling, false) | |
| 148 | 162 | } | |
| 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 | + | } | |
| 151 | 171 | ||
| 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. | |
| 154 | 173 | func (s subsurface) restack(r *wayland.Subsurface, sibling *wayland.Surface, above bool) { | |
| 155 | 174 | me := s.surface | |
| 156 | 175 | p := me.sub.parent | |
| @@ -162,54 +181,41 @@ func (s subsurface) restack(r *wayland.Subsurface, sibling *wayland.Surface, abo | |||
| 162 | 181 | r.PostError(uint32(wayland.SubsurfaceErrorBadSurface), "not a sibling or the parent") | |
| 163 | 182 | return | |
| 164 | 183 | } | |
| 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 | |
| 169 | 189 | break | |
| 170 | 190 | } | |
| 171 | 191 | } | |
| 192 | + | list = removeLayout(list, me) | |
| 193 | + | at := len(list) | |
| 172 | 194 | if other == p { | |
| 173 | - | // Relative to the parent: just above it or just below it. | |
| 174 | - | me.sub.below = !above | |
| 195 | + | item.below = !above | |
| 175 | 196 | 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 | + | } | |
| 179 | 203 | } | |
| 180 | - | // Keep below children first so the flattening stays ordered. | |
| 181 | - | sortBelowFirst(list) | |
| 182 | 204 | } 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 | |
| 188 | 208 | if above { | |
| 189 | 209 | at++ | |
| 190 | 210 | } | |
| 191 | 211 | 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 | 221 | // 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 = w | 80 | state.wl = w | ||
| 79 | c.server.surfaces[w.Resource] = state | 81 | 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 = roleSubsurface | 121 | 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), 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 | + | } | |||
| 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 order | 172 | + | // 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.surface | 174 | me := s.surface | ||
| 156 | p := me.sub.parent | 175 | 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 | return | 182 | return | ||
| 164 | } | 183 | } | ||
| 165 | - | list := p.sub.children | 184 | + | 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 | break | 189 | 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.below | 205 | + | 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 | break | 211 | 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: pointer | 221 | // 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 | |||
| 21 | 21 | ||
| 22 | 22 | // feedbackWait is a committed feedback: presented once a flip on the | |
| 23 | 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 | 30 | type feedbackWait struct { | |
| 25 | 31 | fb *presentationtime.WpPresentationFeedback | |
| 26 | 32 | surf *surface | |
| @@ -21,6 +21,12 @@ const presentationVersion = 2 | |||||
| 21 | 21 | ||||
| 22 | // feedbackWait is a committed feedback: presented once a flip on the | 22 | // 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.WpPresentationFeedback | 31 | fb *presentationtime.WpPresentationFeedback | ||
| 26 | surf *surface | 32 | 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 | |||
| 105 | 105 | } | |
| 106 | 106 | kept := s.held[:0] | |
| 107 | 107 | for _, h := range s.held { | |
| 108 | - | if keepHeld(h, live, now) { | |
| 108 | + | if s.bufferReferenced(h.res) || keepHeld(h, live, now) { | |
| 109 | 109 | kept = append(kept, h) | |
| 110 | 110 | continue | |
| 111 | 111 | } | |
| @@ -116,6 +116,25 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo | |||
| 116 | 116 | 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 | 138 | // keepHeld reports whether an output may still read a held buffer: one | |
| 120 | 139 | // scans it out or queued it, or one has not got a later content of its | |
| 121 | 140 | // 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 | continue | 110 | continue | ||
| 111 | } | 111 | } | ||
| @@ -116,6 +116,25 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo | |||||
| 116 | return len(s.held) > 0 | 116 | 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: one | 138 | // 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 its | 139 | // 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 ( | |||
| 5 | 5 | "time" | |
| 6 | 6 | ||
| 7 | 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 | 52 | func TestKeepHeld(t *testing.T) { | |
| 11 | 53 | now := time.Unix(100, 0) | |
| 12 | 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 | + | } | |
| @@ -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 | |||
| 158 | 158 | return | |
| 159 | 159 | } | |
| 160 | 160 | surf.viewport = vp | |
| 161 | + | vp.resource = res | |
| 161 | 162 | res.OnDestroy = func() { | |
| 162 | 163 | // Destroying the viewport resets the size on the next commit. | |
| 163 | 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; the | |
| 170 | - | // source crop is accepted and ignored (clients use it for video crops). | |
| 170 | + | // viewport holds double-buffered crop and destination state. | |
| 171 | 171 | 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 | |
| 176 | 179 | } | |
| 177 | 180 | ||
| 178 | 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 | 194 | func (v *viewport) SetDestination(r *viewporter.WpViewport, w, h int32) { | |
| 182 | 195 | if v.surface.destroyed { | |
| @@ -189,13 +202,86 @@ func (v *viewport) SetDestination(r *viewporter.WpViewport, w, h int32) { | |||
| 189 | 202 | } | |
| 190 | 203 | 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 | 275 | // logicalSize is the surface size in logical pixels: the viewport | |
| 195 | 276 | // destination, else the buffer divided by its integer buffer scale. | |
| 196 | 277 | 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 | + | } | |
| 199 | 285 | } | |
| 200 | 286 | scale := max(s.bufferScale, 1) | |
| 201 | 287 | return bw / scale, bh / scale | |
| @@ -158,6 +158,7 @@ func (h viewporterHandler) GetViewport(r *viewporter.WpViewporter, id uint32, w | |||||
| 158 | return | 158 | return | ||
| 159 | } | 159 | } | ||
| 160 | surf.viewport = vp | 160 | 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; the | 170 | + | // 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 *surface | 172 | + | surface *surface |
| 173 | - | pendingW, destW int32 | 173 | + | resource *viewporter.WpViewport |
| 174 | - | pendingH, destH int32 | 174 | + | pendingW, destW int32 |
| 175 | - | pendingSet, dest bool | 175 | + | 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 != -1 | 203 | 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 viewport | 275 | // 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 / scale | 287 | return bw / scale, bh / scale | ||
M internal/adapters/wayland/scale_test.go +197 -2
| @@ -163,11 +163,55 @@ func TestFractionalScaleAndViewport(t *testing.T) { | |||
| 163 | 163 | if got.Width != 30 || got.Height != 15 || got.LogicalW != 20 || got.LogicalH != 10 { | |
| 164 | 164 | t.Fatalf("%+v", got) | |
| 165 | 165 | } | |
| 166 | - | return | |
| 166 | + | goto viewportOnly | |
| 167 | 167 | case <-deadline: | |
| 168 | 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 | 217 | func TestBufferScaleLogicalSize(t *testing.T) { | |
| @@ -175,12 +219,163 @@ func TestBufferScaleLogicalSize(t *testing.T) { | |||
| 175 | 219 | if w, h := st.logicalSize(200, 100); w != 100 || h != 50 { | |
| 176 | 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 | 223 | if w, h := st.logicalSize(200, 100); w != 7 || h != 3 { | |
| 180 | 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 | 379 | func TestViewportBadDestination(t *testing.T) { | |
| 185 | 380 | s, _, _, _, dir := contentServer(t) | |
| 186 | 381 | 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 | - | return | 166 | + | 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 ( | |||
| 6 | 6 | "fmt" | |
| 7 | 7 | "github.com/bnema/purego-libwayland/protocol/relativepointer" | |
| 8 | 8 | "os" | |
| 9 | - | "slices" | |
| 10 | 9 | "sync" | |
| 11 | 10 | "time" | |
| 12 | 11 | ||
| @@ -79,8 +78,12 @@ type Server struct { | |||
| 79 | 78 | started time.Time | |
| 80 | 79 | // fifoSurfaces have a fifo barrier or queued commits (fifo.go); | |
| 81 | 80 | // 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 | |
| 84 | 87 | // tokens are the issued xdg-activation tokens (activation.go). | |
| 85 | 88 | tokens map[string]activationToken | |
| 86 | 89 | surfaces map[*server.Resource]*surface | |
| @@ -106,6 +109,7 @@ type Server struct { | |||
| 106 | 109 | layers map[ports.WindowID]*layerSurface | |
| 107 | 110 | nextWindow ports.WindowID | |
| 108 | 111 | nextPool uint64 | |
| 112 | + | nextSurface uint64 | |
| 109 | 113 | focused ports.WindowID | |
| 110 | 114 | pointerFocus ports.WindowID | |
| 111 | 115 | 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 | 460 | s.contentMu.Lock() | |
| 457 | 461 | s.contentSeq[c.ID]++ | |
| 458 | 462 | 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} | |
| 464 | 471 | if c.Empty() { | |
| 465 | 472 | delete(s.damage, c.ID) | |
| 466 | 473 | } else { | |
| 467 | 474 | s.damage[c.ID] = h | |
| 468 | 475 | } | |
| 469 | - | c.DamageHistory = slices.Clone(h) | |
| 476 | + | c.DamageHistory = h | |
| 470 | 477 | s.contents[c.ID] = c | |
| 471 | 478 | close(s.contentNotify) | |
| 472 | 479 | 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.Time | 78 | 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.Time | 82 | + | 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]activationToken | 88 | tokens map[string]activationToken | ||
| 86 | surfaces map[*server.Resource]*surface | 89 | surfaces map[*server.Resource]*surface | ||
| @@ -106,6 +109,7 @@ type Server struct { | |||||
| 106 | layers map[ports.WindowID]*layerSurface | 109 | layers map[ports.WindowID]*layerSurface | ||
| 107 | nextWindow ports.WindowID | 110 | nextWindow ports.WindowID | ||
| 108 | nextPool uint64 | 111 | nextPool uint64 | ||
| 112 | + | nextSurface uint64 | |||
| 109 | focused ports.WindowID | 113 | focused ports.WindowID | ||
| 110 | pointerFocus ports.WindowID | 114 | pointerFocus ports.WindowID | ||
| 111 | wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis | 115 | 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] = h | 474 | s.damage[c.ID] = h | ||
| 468 | } | 475 | } | ||
| 469 | - | c.DamageHistory = slices.Clone(h) | 476 | + | c.DamageHistory = h |
| 470 | s.contents[c.ID] = c | 477 | 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 ( | |||
| 5 | 5 | "time" | |
| 6 | 6 | ||
| 7 | 7 | "github.com/bnema/neferwl/internal/ports" | |
| 8 | + | "github.com/bnema/purego-libwayland/protocol/presentationtime" | |
| 9 | + | "github.com/bnema/purego-libwayland/protocol/viewporter" | |
| 8 | 10 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 9 | 11 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| 10 | 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 | 459 | // A toplevel with a subsurface (how Firefox draws with the GPU) sends one | |
| 14 | 460 | // content: its own buffer, the child at its committed position and the | |
| 15 | 461 | // window geometry. | |
| @@ -148,3 +594,104 @@ func TestSubsurfaceRoleOutlivesParent(t *testing.T) { | |||
| 148 | 594 | requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, s2, b, p) | |
| 149 | 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 | + | } | |
| @@ -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 one | 459 | // 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 the | 460 | // 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 | |||
| 3 | 3 | import ( | |
| 4 | 4 | "github.com/bnema/purego-libwayland/protocol/presentationtime" | |
| 5 | 5 | "image" | |
| 6 | + | "math" | |
| 6 | 7 | "time" | |
| 7 | 8 | ||
| 8 | 9 | "github.com/bnema/neferwl/internal/ports" | |
| @@ -22,8 +23,9 @@ const ( | |||
| 22 | 23 | ) | |
| 23 | 24 | ||
| 24 | 25 | type surface struct { | |
| 25 | - | wl *wayland.Surface | |
| 26 | - | viewport *viewport | |
| 26 | + | wl *wayland.Surface | |
| 27 | + | viewport *viewport | |
| 28 | + | committedViewport *viewport | |
| 27 | 29 | // bufferScale is committed state; pendingScale is set by set_buffer_scale. | |
| 28 | 30 | bufferScale, pendingScale int | |
| 29 | 31 | kind roleKind | |
| @@ -43,9 +45,17 @@ type surface struct { | |||
| 43 | 45 | scale float64 | |
| 44 | 46 | // content is the last content built from this surface's own buffer; | |
| 45 | 47 | // 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 | |
| 49 | 59 | // tearing is the surface's wp_tearing_control_v1; async is the | |
| 50 | 60 | // committed hint, pendingAsync the requested one. | |
| 51 | 61 | tearing *tearingHandler | |
| @@ -62,7 +72,7 @@ type surface struct { | |||
| 62 | 72 | pendingTime time.Time | |
| 63 | 73 | barrier bool | |
| 64 | 74 | barrierAt time.Time | |
| 65 | - | queue []update | |
| 75 | + | queue []*update | |
| 66 | 76 | // contentType is the wp_content_type_v1; contentKind the committed type. | |
| 67 | 77 | contentType *contentTypeHandler | |
| 68 | 78 | contentKind, pendingKind uint32 | |
| @@ -100,13 +110,11 @@ const maxDamageRects = 32 | |||
| 100 | 110 | // are ordered bottom to top, below marks children under their parent. | |
| 101 | 111 | type subState struct { | |
| 102 | 112 | // 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 | |
| 110 | 118 | } | |
| 111 | 119 | ||
| 112 | 120 | // root is the top of the surface's subsurface tree. | |
| @@ -135,19 +143,24 @@ func (s *surface) outputColor() ports.SurfaceColor { | |||
| 135 | 143 | ||
| 136 | 144 | func (s *surface) contentWithColor() ports.SurfaceContent { | |
| 137 | 145 | c := s.content | |
| 146 | + | c.Surface, c.Version = s.identity, s.version | |
| 138 | 147 | c.Color = s.outputColor() | |
| 139 | 148 | return c | |
| 140 | 149 | } | |
| 141 | 150 | ||
| 142 | 151 | // tree is the root's content with its subsurfaces flattened. | |
| 143 | 152 | func (s *surface) tree(id ports.WindowID) ports.SurfaceContent { | |
| 144 | - | c := s.content | |
| 153 | + | c := s.contentWithColor() | |
| 145 | 154 | 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 | |
| 150 | 162 | } | |
| 163 | + | c.Children = s.cachedTree | |
| 151 | 164 | if s.xdg != nil { | |
| 152 | 165 | c.Geometry = s.xdg.geometry | |
| 153 | 166 | } | |
| @@ -155,20 +168,31 @@ func (s *surface) tree(id ports.WindowID) ports.SurfaceContent { | |||
| 155 | 168 | 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 | 182 | // appendTree flattens a subsurface at (x, y) from the root: its children | |
| 159 | 183 | // below it, itself, then its children above. | |
| 160 | 184 | 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) | |
| 164 | 188 | } | |
| 165 | 189 | } | |
| 166 | 190 | if s.has { | |
| 167 | 191 | *out = append(*out, ports.Subsurface{X: x, Y: y, Below: below, SurfaceContent: s.contentWithColor()}) | |
| 168 | 192 | } | |
| 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) | |
| 172 | 196 | } | |
| 173 | 197 | } | |
| 174 | 198 | } | |
| @@ -198,7 +222,16 @@ func (s *surface) detach() { | |||
| 198 | 222 | 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 | 231 | s.sub.parent = nil | |
| 232 | + | // Parent updates cannot make a detached child's bound commits visible. | |
| 233 | + | s.dropQueue() | |
| 234 | + | s.flushDesync() | |
| 202 | 235 | p.redraw() | |
| 203 | 236 | s.sendScale() | |
| 204 | 237 | } | |
| @@ -221,8 +254,11 @@ func (s *surface) Destroy(*wayland.Surface) { | |||
| 221 | 254 | s.detach() | |
| 222 | 255 | for _, ch := range s.sub.children { | |
| 223 | 256 | ch.sub.parent = nil | |
| 257 | + | ch.dropQueue() | |
| 258 | + | ch.flushDesync() | |
| 224 | 259 | } | |
| 225 | 260 | s.sub.children = nil | |
| 261 | + | s.sub.layout, s.sub.pendingLayout = nil, nil | |
| 226 | 262 | for _, cb := range s.callbacks { | |
| 227 | 263 | cb.Destroy() | |
| 228 | 264 | } | |
| @@ -265,18 +301,25 @@ func (s *surface) Commit(*wayland.Surface) { | |||
| 265 | 301 | return | |
| 266 | 302 | } | |
| 267 | 303 | 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) | |
| 275 | 308 | } | |
| 276 | 309 | ||
| 277 | 310 | // applyCommit makes the pending state current. | |
| 278 | 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 | 314 | 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 | 323 | if s.pendingBarrier { | |
| 281 | 324 | s.pendingBarrier = false | |
| 282 | 325 | s.setBarrier(time.Now()) | |
| @@ -295,6 +338,10 @@ func (s *surface) applyCommit() { | |||
| 295 | 338 | s.representation = s.pendingRepresentation | |
| 296 | 339 | if s.viewport != nil { | |
| 297 | 340 | s.viewport.commit() | |
| 341 | + | v := *s.viewport | |
| 342 | + | s.committedViewport = &v | |
| 343 | + | } else { | |
| 344 | + | s.committedViewport = nil | |
| 298 | 345 | } | |
| 299 | 346 | cs := s.pendingSync | |
| 300 | 347 | s.pendingSync = nil | |
| @@ -322,6 +369,10 @@ func (s *surface) applyCommit() { | |||
| 322 | 369 | if fresh { | |
| 323 | 370 | if state, ok := s.server.buffers[s.current.Resource]; ok { | |
| 324 | 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 | 376 | c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height) | |
| 326 | 377 | resized := !s.has || c.Width != s.content.Width || c.Height != s.content.Height || c.LogicalW != s.content.LogicalW || c.LogicalH != s.content.LogicalH | |
| 327 | 378 | s.commitDamage(true, resized, c.Width, c.Height) | |
| @@ -338,19 +389,34 @@ func (s *surface) applyCommit() { | |||
| 338 | 389 | // replaces them. | |
| 339 | 390 | } | |
| 340 | 391 | } | |
| 392 | + | damaged := !fresh && s.current != nil && (len(s.pendingDamage) > 0 || len(s.pendingBufDamage) > 0) | |
| 341 | 393 | if !fresh { | |
| 342 | 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 | 405 | if s.current == nil { | |
| 345 | 406 | s.content, s.has = ports.SurfaceContent{}, false | |
| 346 | 407 | } | |
| 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 | |
| 354 | 420 | } | |
| 355 | 421 | geometry := false | |
| 356 | 422 | if s.xdg != nil && s.xdg.pendingGeometry != s.xdg.geometry { | |
| @@ -360,16 +426,24 @@ func (s *surface) applyCommit() { | |||
| 360 | 426 | if s.xdg != nil && s.xdg.window != nil { | |
| 361 | 427 | s.xdg.window.afterCommit() | |
| 362 | 428 | } | |
| 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)) | |
| 364 | 430 | if drawn { | |
| 365 | - | if moved || geometry { | |
| 431 | + | if moved || geometry || oldW != s.content.LogicalW || oldH != s.content.LogicalH || oldSource != s.content.Source { | |
| 366 | 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 | 440 | fb := s.pendingFeedback | |
| 371 | 441 | 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 | 448 | func (s *surface) Damage(_ *wayland.Surface, x, y, w, h int32) { | |
| 375 | 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 | 490 | // Clients may damage INT32_MAX-sized rects: clamp before scaling. | |
| 417 | 491 | x0, y0 := max(r.X, 0), max(r.Y, 0) | |
| 418 | 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 | 500 | s.committed = d | |
| 422 | 501 | } | |
| @@ -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.Surface | 26 | + | wl *wayland.Surface |
| 26 | - | viewport *viewport | 27 | + | 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 int | 30 | bufferScale, pendingScale int | ||
| 29 | kind roleKind | 31 | kind roleKind | ||
| @@ -43,9 +45,17 @@ type surface struct { | |||||
| 43 | scale float64 | 45 | 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.SurfaceContent | 48 | + | content ports.SurfaceContent |
| 47 | - | has bool | 49 | + | has bool |
| 48 | - | sub subState | 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 | |||
| 49 | // tearing is the surface's wp_tearing_control_v1; async is the | 59 | // 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 *tearingHandler | 61 | tearing *tearingHandler | ||
| @@ -62,7 +72,7 @@ type surface struct { | |||||
| 62 | pendingTime time.Time | 72 | pendingTime time.Time | ||
| 63 | barrier bool | 73 | barrier bool | ||
| 64 | barrierAt time.Time | 74 | barrierAt time.Time | ||
| 65 | - | queue []update | 75 | + | 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 *contentTypeHandler | 77 | contentType *contentTypeHandler | ||
| 68 | contentKind, pendingKind uint32 | 78 | 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.Subsurface | 113 | + | role *wayland.Subsurface |
| 104 | - | parent *surface | 114 | + | parent *surface |
| 105 | - | children []*surface | 115 | + | children []*surface // structural children |
| 106 | - | x, y int // committed position, from the parent origin | 116 | + | pendingLayout, layout []childLayout |
| 107 | - | pendX, pendY int | 117 | + | 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.content | 145 | c := s.content | ||
| 146 | + | c.Surface, c.Version = s.identity, s.version | |||
| 138 | c.Color = s.outputColor() | 147 | c.Color = s.outputColor() | ||
| 139 | return c | 148 | 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.content | 153 | + | c := s.contentWithColor() |
| 145 | c.ID = id | 154 | c.ID = id | ||
| 146 | - | c.Color = s.outputColor() | 155 | + | if s.treeDirty || s.cachedTree == nil { |
| 147 | - | c.Children = nil | 156 | + | 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.geometry | 165 | c.Geometry = s.xdg.geometry | ||
| 153 | } | 166 | } | ||
| @@ -155,20 +168,31 @@ func (s *surface) tree(id ports.WindowID) ports.SurfaceContent { | |||||
| 155 | return c | 168 | 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 children | 182 | // 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 | break | 222 | 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 = nil | 231 | 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 = nil | 256 | ch.sub.parent = nil | ||
| 257 | + | ch.dropQueue() | |||
| 258 | + | ch.flushDesync() | |||
| 224 | } | 259 | } | ||
| 225 | s.sub.children = nil | 260 | 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 | return | 301 | return | ||
| 266 | } | 302 | } | ||
| 267 | s.takeSyncPoints() | 303 | s.takeSyncPoints() | ||
| 268 | - | // A commit behind a fifo barrier, a future timestamp or an acquire | 304 | + | // 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 != nil | 314 | 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 = false | 324 | 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.pendingRepresentation | 338 | 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.pendingSync | 346 | cs := s.pendingSync | ||
| 300 | s.pendingSync = nil | 347 | 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.LogicalH | 377 | 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{}, false | 406 | 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 := false | 409 | + | 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, false | 412 | + | // The layout belongs to this update, not to the latest requests. |
| 352 | - | moved = true | 413 | + | 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 := false | 421 | 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 != 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)) |
| 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.pendingFeedback | 440 | fb := s.pendingFeedback | ||
| 371 | s.pendingFeedback = nil | 441 | 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 = d | 500 | 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) { | |||
| 387 | 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 | 431 | // A commit dropped before its acquire point has a fence signals nothing | |
| 391 | 432 | // (the client has not submitted that point) and sends no wl_buffer.release; | |
| 392 | 433 | // 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 nothing | 431 | // 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 { | |||
| 776 | 776 | // geometry), the rest being client shadows. | |
| 777 | 777 | type SurfaceContent struct { | |
| 778 | 778 | 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 | 782 | // Seq counts the window's contents (wayland sets it), for release. | |
| 780 | 783 | Seq uint64 | |
| 781 | 784 | Width, Height int | |
| 782 | 785 | 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 | |
| 787 | 793 | // Children are the subsurfaces, bottom to top, flattened. | |
| 788 | 794 | Children []Subsurface | |
| 789 | 795 | // 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 WindowID | 778 | 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 uint64 | 783 | Seq uint64 | ||
| 781 | Width, Height int | 784 | Width, Height int | ||
| 782 | LogicalW, LogicalH int | 785 | LogicalW, LogicalH int | ||
| 783 | - | Opaque bool // x formats: ignore the alpha byte | 786 | + | // Source is the committed crop in buffer pixels: x, y, width, height. |
| 784 | - | Color SurfaceColor | 787 | + | // Zero width means the entire buffer is used. |
| 785 | - | SHM *SHMBuffer | 788 | + | Source [4]float32 |
| 786 | - | DMABuf *DMABuf | 789 | + | 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 []Subsurface | 794 | Children []Subsurface | ||
| 789 | // Geometry is in logical pixels from the surface origin; empty means | 795 | // Geometry is in logical pixels from the surface origin; empty means | ||