detached f6e232f

perf(vulkan): hide the root only under a child's opaque region

A child that is not wholly opaque but whose opaque region stops a pixel
short of its edges (Firefox's page) no longer leaves the shm root below it
drawn in full: the root draws the strips around the region only.
commit
f6e232f9e874431a503fc246607d59f34ce75b7b
author
brice · · 2026-10-05 07:07 UTC
parent
f87efce
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +388 -13
M Makefile +1 -1
@@ -60,7 +60,7 @@ arch:
6060 $(HOME)/go/bin/hexcheck -hexcheck.config .hexcheck.yaml -hexcheck.root . ./...
6161 # Fast, mandatory guards for tiled content publications and SHM copies.
6262 perf-check:
63- CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget)$$' -count=1
63+ CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget)$$' -count=1
6464 check: vet test arch fakes-check log-check perf-check
6565
6666 # On-demand SDR/HDR colour check with examples/testpattern on a headless
@@ -60,7 +60,7 @@ arch:
60 $(HOME)/go/bin/hexcheck -hexcheck.config .hexcheck.yaml -hexcheck.root . ./...60 $(HOME)/go/bin/hexcheck -hexcheck.config .hexcheck.yaml -hexcheck.root . ./...
61 # Fast, mandatory guards for tiled content publications and SHM copies.61 # Fast, mandatory guards for tiled content publications and SHM copies.
62 perf-check:62 perf-check:
63- CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget)$$' -count=163+ CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget)$$' -count=1
64 check: vet test arch fakes-check log-check perf-check64 check: vet test arch fakes-check log-check perf-check
65 65
66 # On-demand SDR/HDR colour check with examples/testpattern on a headless66 # On-demand SDR/HDR colour check with examples/testpattern on a headless
M internal/adapters/vulkan/dmabuf_test.go +1 -1
@@ -14,7 +14,7 @@ import (
1414
1515 // udmabuf turns memfd pages into a real dmabuf: a linear buffer any GPU
1616 // driver imports, like a client's.
17-func udmabuf(t *testing.T, w, h int, fill func(x, y int) [4]byte) *os.File {
17+func udmabuf(t testing.TB, w, h int, fill func(x, y int) [4]byte) *os.File {
1818 t.Helper()
1919 dev, err := os.OpenFile("/dev/udmabuf", os.O_RDWR, 0)
2020 if err != nil {
@@ -14,7 +14,7 @@ import (
14 14
15 // udmabuf turns memfd pages into a real dmabuf: a linear buffer any GPU15 // udmabuf turns memfd pages into a real dmabuf: a linear buffer any GPU
16 // driver imports, like a client's.16 // driver imports, like a client's.
17-func udmabuf(t *testing.T, w, h int, fill func(x, y int) [4]byte) *os.File {17+func udmabuf(t testing.TB, w, h int, fill func(x, y int) [4]byte) *os.File {
18 t.Helper()18 t.Helper()
19 dev, err := os.OpenFile("/dev/udmabuf", os.O_RDWR, 0)19 dev, err := os.OpenFile("/dev/udmabuf", os.O_RDWR, 0)
20 if err != nil {20 if err != nil {
M internal/adapters/vulkan/scene.go +64 -6
@@ -310,28 +310,56 @@ func (w *sceneWalk) place(id ports.WindowID, content *ports.SurfaceContent, x, y
310310 }
311311 }
312312
313-// opaqueChildren gives the physical rects shown by the opaque subsurfaces
313+// opaqueChildren gives the physical rects shown opaque by the subsurfaces
314314 // above the root (e.g. a game's GPU surface over its window's shm buffer):
315-// they hide the surfaces below them. A child whose buffer will not draw is
316-// left out, so the surface below it still shows: a dmabuf that fails to
317-// import (opaqueChildren pre-imports it) or an unreadable shm buffer.
315+// they hide the surfaces below them. A child that is not wholly opaque but
316+// has an opaque region (Firefox's page, whose region stops a pixel short of
317+// its edges) hides the surfaces below only inside that rect. A child whose
318+// buffer will not draw is left out, so the surface below it still shows: a
319+// dmabuf that fails to import (opaqueChildren pre-imports it) or an
320+// unreadable shm buffer.
318321 func (w *sceneWalk) opaqueChildren(content *ports.SurfaceContent, ox, oy float64, clip image.Rectangle) []image.Rectangle {
319322 covers := w.r.scratchCovers[:0]
320323 for i := range content.Children {
321324 ch := &content.Children[i]
322- if ch.Below || !ch.Opaque {
325+ if ch.Below || !ch.Opaque && ch.OpaqueRect.W <= 0 {
323326 continue
324327 }
325- _, dst, ok := w.surfaceRects(&ch.SurfaceContent, ox+float64(ch.X)*w.zoom, oy+float64(ch.Y)*w.zoom, clip)
328+ full, dst, ok := w.surfaceRects(&ch.SurfaceContent, ox+float64(ch.X)*w.zoom, oy+float64(ch.Y)*w.zoom, clip)
326329 if !ok || !w.drawable(&ch.SurfaceContent) {
327330 continue
328331 }
332+ if !ch.Opaque {
333+ if dst = opaqueCover(&ch.SurfaceContent, full, dst); dst.Empty() {
334+ continue
335+ }
336+ }
329337 covers = append(covers, dst)
330338 }
331339 w.r.scratchCovers = covers
332340 return covers
333341 }
334342
343+// opaqueCover is the part of dst (physical, what the surface shows of full)
344+// that its OpaqueRect maps to. Edges round inward, so every pixel of it is
345+// inside the region; an edge on the surface's own edge stays on dst's.
346+func opaqueCover(c *ports.SurfaceContent, full, dst image.Rectangle) image.Rectangle {
347+ lw, lh := c.LogicalW, c.LogicalH
348+ if lw <= 0 || lh <= 0 {
349+ lw, lh = c.Width, c.Height
350+ }
351+ o := c.OpaqueRect
352+ if lw <= 0 || lh <= 0 || o.W <= 0 || o.H <= 0 {
353+ return image.Rectangle{}
354+ }
355+ // ceil and floor of full.Min + v*size/n, v >= 0.
356+ lo := func(min, v, size, n int) int { return min + (v*size+n-1)/n }
357+ hi := func(min, v, size, n int) int { return min + v*size/n }
358+ r := image.Rect(lo(full.Min.X, max(o.X, 0), full.Dx(), lw), lo(full.Min.Y, max(o.Y, 0), full.Dy(), lh),
359+ hi(full.Min.X, min(o.X+o.W, lw), full.Dx(), lw), hi(full.Min.Y, min(o.Y+o.H, lh), full.Dy(), lh))
360+ return r.Intersect(dst)
361+}
362+
335363 // drawable reports whether the child's buffer will draw. GPU buffer
336364 // imports are cached, so pre-importing here costs nothing.
337365 func (w *sceneWalk) drawable(c *ports.SurfaceContent) bool {
@@ -383,6 +411,7 @@ func (w *sceneWalk) surfaceRects(content *ports.SurfaceContent, x, y float64, cl
383411 // surface draws one surface buffer with its origin at (x, y) logical,
384412 // clipped to clip (logical), unless an opaque surface above it (covers,
385413 // physical) hides all of it: its buffer is then neither copied nor drawn.
414+// A cover over only part of it clips the draw to the rest.
386415 func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y float64, clip image.Rectangle, key shmKey, seq uint64, covers []image.Rectangle, root bool) {
387416 full, dst, ok := w.surfaceRects(content, x, y, clip)
388417 if !ok {
@@ -396,7 +425,36 @@ func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y float64, clip im
396425 if root {
397426 w.dmg.content(key.win, content, full, dst)
398427 }
428+ n := len(w.draws)
399429 w.content(dst, full, content, key, seq, root)
430+ if len(w.draws) == n+1 && len(covers) > 0 {
431+ // The quad's mapping comes from full: only its rect shrinks.
432+ d := w.draws[n]
433+ w.draws = w.draws[:n]
434+ w.uncovered(d, dst, covers)
435+ }
436+}
437+
438+// uncovered appends d, a quad over rect, minus the covers: the strips of
439+// rect around each cover, in turn. d keeps its mapping.
440+func (w *sceneWalk) uncovered(d draw, rect image.Rectangle, covers []image.Rectangle) {
441+ for i, c := range covers {
442+ in := rect.Intersect(c)
443+ if in.Empty() {
444+ continue
445+ }
446+ rest := covers[i+1:]
447+ w.uncovered(d, image.Rect(rect.Min.X, rect.Min.Y, rect.Max.X, in.Min.Y), rest)
448+ w.uncovered(d, image.Rect(rect.Min.X, in.Max.Y, rect.Max.X, rect.Max.Y), rest)
449+ w.uncovered(d, image.Rect(rect.Min.X, in.Min.Y, in.Min.X, in.Max.Y), rest)
450+ w.uncovered(d, image.Rect(in.Max.X, in.Min.Y, rect.Max.X, in.Max.Y), rest)
451+ return
452+ }
453+ if rect.Empty() {
454+ return
455+ }
456+ d.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)}
457+ w.draws = append(w.draws, d)
400458 }
401459
402460 // content draws the part of a buffer mapped onto full (physical pixels)
@@ -310,28 +310,56 @@ func (w *sceneWalk) place(id ports.WindowID, content *ports.SurfaceContent, x, y
310 }310 }
311 }311 }
312 312
313-// opaqueChildren gives the physical rects shown by the opaque subsurfaces313+// opaqueChildren gives the physical rects shown opaque by the subsurfaces
314 // above the root (e.g. a game's GPU surface over its window's shm buffer):314 // above the root (e.g. a game's GPU surface over its window's shm buffer):
315-// they hide the surfaces below them. A child whose buffer will not draw is315+// they hide the surfaces below them. A child that is not wholly opaque but
316-// left out, so the surface below it still shows: a dmabuf that fails to316+// has an opaque region (Firefox's page, whose region stops a pixel short of
317-// import (opaqueChildren pre-imports it) or an unreadable shm buffer.317+// its edges) hides the surfaces below only inside that rect. A child whose
318+// buffer will not draw is left out, so the surface below it still shows: a
319+// dmabuf that fails to import (opaqueChildren pre-imports it) or an
320+// unreadable shm buffer.
318 func (w *sceneWalk) opaqueChildren(content *ports.SurfaceContent, ox, oy float64, clip image.Rectangle) []image.Rectangle {321 func (w *sceneWalk) opaqueChildren(content *ports.SurfaceContent, ox, oy float64, clip image.Rectangle) []image.Rectangle {
319 covers := w.r.scratchCovers[:0]322 covers := w.r.scratchCovers[:0]
320 for i := range content.Children {323 for i := range content.Children {
321 ch := &content.Children[i]324 ch := &content.Children[i]
322- if ch.Below || !ch.Opaque {325+ if ch.Below || !ch.Opaque && ch.OpaqueRect.W <= 0 {
323 continue326 continue
324 }327 }
325- _, dst, ok := w.surfaceRects(&ch.SurfaceContent, ox+float64(ch.X)*w.zoom, oy+float64(ch.Y)*w.zoom, clip)328+ full, dst, ok := w.surfaceRects(&ch.SurfaceContent, ox+float64(ch.X)*w.zoom, oy+float64(ch.Y)*w.zoom, clip)
326 if !ok || !w.drawable(&ch.SurfaceContent) {329 if !ok || !w.drawable(&ch.SurfaceContent) {
327 continue330 continue
328 }331 }
332+ if !ch.Opaque {
333+ if dst = opaqueCover(&ch.SurfaceContent, full, dst); dst.Empty() {
334+ continue
335+ }
336+ }
329 covers = append(covers, dst)337 covers = append(covers, dst)
330 }338 }
331 w.r.scratchCovers = covers339 w.r.scratchCovers = covers
332 return covers340 return covers
333 }341 }
334 342
343+// opaqueCover is the part of dst (physical, what the surface shows of full)
344+// that its OpaqueRect maps to. Edges round inward, so every pixel of it is
345+// inside the region; an edge on the surface's own edge stays on dst's.
346+func opaqueCover(c *ports.SurfaceContent, full, dst image.Rectangle) image.Rectangle {
347+ lw, lh := c.LogicalW, c.LogicalH
348+ if lw <= 0 || lh <= 0 {
349+ lw, lh = c.Width, c.Height
350+ }
351+ o := c.OpaqueRect
352+ if lw <= 0 || lh <= 0 || o.W <= 0 || o.H <= 0 {
353+ return image.Rectangle{}
354+ }
355+ // ceil and floor of full.Min + v*size/n, v >= 0.
356+ lo := func(min, v, size, n int) int { return min + (v*size+n-1)/n }
357+ hi := func(min, v, size, n int) int { return min + v*size/n }
358+ r := image.Rect(lo(full.Min.X, max(o.X, 0), full.Dx(), lw), lo(full.Min.Y, max(o.Y, 0), full.Dy(), lh),
359+ hi(full.Min.X, min(o.X+o.W, lw), full.Dx(), lw), hi(full.Min.Y, min(o.Y+o.H, lh), full.Dy(), lh))
360+ return r.Intersect(dst)
361+}
362+
335 // drawable reports whether the child's buffer will draw. GPU buffer363 // drawable reports whether the child's buffer will draw. GPU buffer
336 // imports are cached, so pre-importing here costs nothing.364 // imports are cached, so pre-importing here costs nothing.
337 func (w *sceneWalk) drawable(c *ports.SurfaceContent) bool {365 func (w *sceneWalk) drawable(c *ports.SurfaceContent) bool {
@@ -383,6 +411,7 @@ func (w *sceneWalk) surfaceRects(content *ports.SurfaceContent, x, y float64, cl
383 // surface draws one surface buffer with its origin at (x, y) logical,411 // surface draws one surface buffer with its origin at (x, y) logical,
384 // clipped to clip (logical), unless an opaque surface above it (covers,412 // clipped to clip (logical), unless an opaque surface above it (covers,
385 // physical) hides all of it: its buffer is then neither copied nor drawn.413 // physical) hides all of it: its buffer is then neither copied nor drawn.
414+// A cover over only part of it clips the draw to the rest.
386 func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y float64, clip image.Rectangle, key shmKey, seq uint64, covers []image.Rectangle, root bool) {415 func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y float64, clip image.Rectangle, key shmKey, seq uint64, covers []image.Rectangle, root bool) {
387 full, dst, ok := w.surfaceRects(content, x, y, clip)416 full, dst, ok := w.surfaceRects(content, x, y, clip)
388 if !ok {417 if !ok {
@@ -396,7 +425,36 @@ func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y float64, clip im
396 if root {425 if root {
397 w.dmg.content(key.win, content, full, dst)426 w.dmg.content(key.win, content, full, dst)
398 }427 }
428+ n := len(w.draws)
399 w.content(dst, full, content, key, seq, root)429 w.content(dst, full, content, key, seq, root)
430+ if len(w.draws) == n+1 && len(covers) > 0 {
431+ // The quad's mapping comes from full: only its rect shrinks.
432+ d := w.draws[n]
433+ w.draws = w.draws[:n]
434+ w.uncovered(d, dst, covers)
435+ }
436+}
437+
438+// uncovered appends d, a quad over rect, minus the covers: the strips of
439+// rect around each cover, in turn. d keeps its mapping.
440+func (w *sceneWalk) uncovered(d draw, rect image.Rectangle, covers []image.Rectangle) {
441+ for i, c := range covers {
442+ in := rect.Intersect(c)
443+ if in.Empty() {
444+ continue
445+ }
446+ rest := covers[i+1:]
447+ w.uncovered(d, image.Rect(rect.Min.X, rect.Min.Y, rect.Max.X, in.Min.Y), rest)
448+ w.uncovered(d, image.Rect(rect.Min.X, in.Max.Y, rect.Max.X, rect.Max.Y), rest)
449+ w.uncovered(d, image.Rect(rect.Min.X, in.Min.Y, in.Min.X, in.Max.Y), rest)
450+ w.uncovered(d, image.Rect(in.Max.X, in.Min.Y, rect.Max.X, in.Max.Y), rest)
451+ return
452+ }
453+ if rect.Empty() {
454+ return
455+ }
456+ d.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)}
457+ w.draws = append(w.draws, d)
400 }458 }
401 459
402 // content draws the part of a buffer mapped onto full (physical pixels)460 // content draws the part of a buffer mapped onto full (physical pixels)
A internal/adapters/vulkan/scene_opaque_region_test.go +251 -0
@@ -0,0 +1,251 @@
1+package vulkan
2+
3+import (
4+ "image"
5+ "image/color"
6+ "slices"
7+ "testing"
8+ "time"
9+
10+ "github.com/bnema/neferwl/internal/ports"
11+)
12+
13+// opaqueRegionContent is a 2x-scale shm root of w×h logical pixels under one
14+// child of the same size whose opaque region stops a pixel short of its right
15+// and bottom edges (Firefox's page over its shm root).
16+func opaqueRegionContent(t testing.TB, w, h int) ports.SurfaceContent {
17+ root := solidContent(t, 2*w, 2*h, color.RGBA{R: 255, A: 255})
18+ root.ID, root.Surface, root.Seq, root.Version = 1, 1, 1, 1
19+ root.LogicalW, root.LogicalH = w, h
20+ child := solidContent(t, w, h, color.RGBA{G: 255, A: 255})
21+ child.Surface, child.Version = 2, 1
22+ child.LogicalW, child.LogicalH = w, h
23+ child.OpaqueRect = ports.Rect{W: w - 1, H: h - 1}
24+ root.Children = []ports.Subsurface{{SurfaceContent: child}}
25+ return root
26+}
27+
28+// drawRects are the rects of the content draws (not the window's fills).
29+func drawRects(ds []draw) []image.Rectangle {
30+ var out []image.Rectangle
31+ for _, d := range ds {
32+ if d.pc.misc[0] == modeSolid {
33+ continue
34+ }
35+ out = append(out, image.Rect(int(d.pc.rect[0]), int(d.pc.rect[1]), int(d.pc.rect[2]), int(d.pc.rect[3])))
36+ }
37+ return out
38+}
39+
40+// A child whose opaque region leaves a thin edge out hides the root under the
41+// region only: the root draws the two strips around it, and the child draws
42+// over its own whole rect.
43+func TestSceneOpaqueRegionClipsRoot(t *testing.T) {
44+ r, err := New(64, 48)
45+ if err != nil {
46+ t.Skipf("Vulkan unavailable: %v", err)
47+ }
48+ defer r.Close()
49+ scene := ports.Scene{Seq: 1, Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 20, H: 12}}}}
50+ contents := map[ports.WindowID]ports.SurfaceContent{1: opaqueRegionContent(t, 20, 12)}
51+ ds := r.draws(scene, contents, newDamage(r.target(), scene, image.Rect(0, 0, 64, 48)))
52+ want := []image.Rectangle{
53+ // The root: the strip under the last row, then the one right of the
54+ // region; then the child over its whole rect.
55+ image.Rect(0, 11, 20, 12), image.Rect(19, 0, 20, 11),
56+ image.Rect(0, 0, 20, 12),
57+ }
58+ if got := drawRects(ds); !sameRects(got, want) {
59+ t.Fatalf("draws %v, want %v", got, want)
60+ }
61+}
62+
63+func sameRects(a, b []image.Rectangle) bool {
64+ if len(a) != len(b) {
65+ return false
66+ }
67+ for i := range a {
68+ if a[i] != b[i] {
69+ return false
70+ }
71+ }
72+ return true
73+}
74+
75+// A wholly opaque child still skips the root.
76+func TestSceneOpaqueChildSkipsRoot(t *testing.T) {
77+ r, err := New(64, 48)
78+ if err != nil {
79+ t.Skipf("Vulkan unavailable: %v", err)
80+ }
81+ defer r.Close()
82+ root := opaqueRegionContent(t, 20, 12)
83+ root.Children[0].Opaque, root.Children[0].OpaqueRect = true, ports.Rect{}
84+ scene := ports.Scene{Seq: 1, Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 20, H: 12}}}}
85+ ds := r.draws(scene, map[ports.WindowID]ports.SurfaceContent{1: root}, newDamage(r.target(), scene, image.Rect(0, 0, 64, 48)))
86+ if got, want := drawRects(ds), []image.Rectangle{image.Rect(0, 0, 20, 12)}; !sameRects(got, want) {
87+ t.Fatalf("draws %v, want %v", got, want)
88+ }
89+}
90+
91+// The strips show the root exactly as it draws alone: the child is
92+// transparent on its last row and column, outside its opaque region, and the
93+// root's pixels show through there, mapped as without the clipping. Inside
94+// the region the child shows.
95+func TestRendererOpaqueRegionStripsKeepRoot(t *testing.T) {
96+ r, err := New(64, 48)
97+ if err != nil {
98+ t.Skipf("Vulkan unavailable: %v", err)
99+ }
100+ defer r.Close()
101+ const w, h = 20, 12
102+ scene := ports.Scene{Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: w, H: h}}}}
103+ root := opaqueRegionContent(t, w, h)
104+ // A 2x root with a gradient, to see a wrong strip mapping; a child
105+ // that is green but for a transparent last row and column.
106+ px := make([]byte, 2*w*2*h*4)
107+ for y := range 2 * h {
108+ for x := range 2 * w {
109+ copy(px[(y*2*w+x)*4:], []byte{0, uint8(y * 10), uint8(x * 6), 255})
110+ }
111+ }
112+ root.SHM = shmContent(t, 2*w, 2*h, 2*w*4, px).SHM
113+ cp := make([]byte, w*h*4)
114+ for y := range h {
115+ for x := range w {
116+ if x < w-1 && y < h-1 {
117+ copy(cp[(y*w+x)*4:], []byte{0, 255, 0, 255})
118+ }
119+ }
120+ }
121+ root.Children[0].SHM = shmContent(t, w, h, w*4, cp).SHM
122+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
123+ t.Fatal(err)
124+ }
125+ with := readPixels(t, r)
126+ alone := root
127+ alone.Children = nil
128+ alone.Seq, alone.Version = 2, 2
129+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: alone}); err != nil {
130+ t.Fatal(err)
131+ }
132+ without := readPixels(t, r)
133+ green := color.RGBA{G: 255, A: 255}
134+ for y := range h {
135+ for x := range w {
136+ want := without.At(x, y)
137+ if x < w-1 && y < h-1 {
138+ want = green
139+ }
140+ if got := with.At(x, y); got != want {
141+ t.Fatalf("At(%d,%d)=%v want %v", x, y, got, want)
142+ }
143+ }
144+ }
145+}
146+
147+// opaqueCover maps the opaque rect onto the physical rect of the surface,
148+// rounding inward, clipped to what shows.
149+func TestOpaqueCover(t *testing.T) {
150+ c := ports.SurfaceContent{LogicalW: 984, LogicalH: 1231, OpaqueRect: ports.Rect{W: 983, H: 1230}}
151+ for _, tc := range []struct {
152+ name string
153+ full, dst image.Rectangle
154+ rect ports.Rect
155+ want image.Rectangle
156+ }{
157+ {"scale 1.3", image.Rect(0, 0, 1279, 1600), image.Rect(0, 0, 1279, 1600), c.OpaqueRect, image.Rect(0, 0, 1277, 1598)},
158+ {"offset", image.Rect(10, 20, 1289, 1620), image.Rect(10, 20, 1289, 1620), c.OpaqueRect, image.Rect(10, 20, 1287, 1618)},
159+ {"clipped", image.Rect(0, 0, 1279, 1600), image.Rect(0, 0, 100, 100), c.OpaqueRect, image.Rect(0, 0, 100, 100)},
160+ {"inner rect", image.Rect(0, 0, 984, 1231), image.Rect(0, 0, 984, 1231), ports.Rect{X: 4, Y: 8, W: 100, H: 200}, image.Rect(4, 8, 104, 208)},
161+ {"outside", image.Rect(0, 0, 984, 1231), image.Rect(0, 0, 3, 3), ports.Rect{X: 4, Y: 8, W: 100, H: 200}, image.Rectangle{}},
162+ } {
163+ t.Run(tc.name, func(t *testing.T) {
164+ c.OpaqueRect = tc.rect
165+ got := opaqueCover(&c, tc.full, tc.dst)
166+ if got != tc.want {
167+ t.Fatalf("opaqueCover = %v, want %v", got, tc.want)
168+ }
169+ })
170+ }
171+}
172+
173+// Building the draws of a window with a partly opaque child allocates
174+// nothing.
175+func TestSceneOpaqueRegionAllocations(t *testing.T) {
176+ r, err := New(64, 48)
177+ if err != nil {
178+ t.Skipf("Vulkan unavailable: %v", err)
179+ }
180+ defer r.Close()
181+ scene := ports.Scene{Seq: 1, Scale: 1.5, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 20, H: 12}}}}
182+ contents := map[ports.WindowID]ports.SurfaceContent{1: opaqueRegionContent(t, 20, 12)}
183+ bounds := image.Rect(0, 0, 64, 48)
184+ frame := func() { _ = r.draws(scene, contents, r.frameDamage(r.target(), scene, bounds)) }
185+ frame()
186+ if allocs := testing.AllocsPerRun(50, frame); allocs != 0 {
187+ t.Errorf("partly opaque child draws: %.1f allocs/frame, want 0", allocs)
188+ }
189+}
190+
191+// BenchmarkComposeOpaqueRegionChild composes a Firefox-like window moved every
192+// frame as a workspace swipe does, at 2560x1600 and scale 1.3: a 2x shm root
193+// of 984x1231 logical pixels under a dmabuf page of 1279x1600 pixels shown
194+// at 984x1231 (the viewport destination). "none" is a page with no opaque
195+// region (the root draws in full), "short" one with a region a logical pixel
196+// short of its right and bottom edges, "whole" one that covers it. The GPU is
197+// waited for after every frame. Select it with NEFERWL_VK_DEVICE.
198+func BenchmarkComposeOpaqueRegionChild(b *testing.B) {
199+ const w, h, lw, lh = 2560, 1600, 984, 1231
200+ for _, tc := range []struct {
201+ name string
202+ opaque bool
203+ rect ports.Rect
204+ }{
205+ {"none", false, ports.Rect{}},
206+ {"short", false, ports.Rect{W: lw - 1, H: lh - 1}},
207+ {"whole", true, ports.Rect{}},
208+ } {
209+ b.Run(tc.name, func(b *testing.B) {
210+ r, err := New(w, h)
211+ if err != nil {
212+ b.Skipf("Vulkan unavailable: %v", err)
213+ }
214+ defer r.Close()
215+ linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
216+ if !slices.Contains(r.DMABuf().Formats, linear) {
217+ b.Skipf("linear ARGB8888 not importable")
218+ }
219+ const pw, ph = 1279, 1600
220+ f := udmabuf(b, 1280, ph, func(x, y int) [4]byte { return [4]byte{byte(x), byte(y), 200, 255} })
221+ page := ports.SurfaceContent{Width: pw, Height: ph, LogicalW: lw, LogicalH: lh, Surface: 2, Version: 1,
222+ Opaque: tc.opaque, OpaqueRect: tc.rect,
223+ DMABuf: &ports.DMABuf{ID: 9, Width: pw, Height: ph, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 5120}}}}
224+ root := solidContent(b, 2*lw, 2*lh, color.RGBA{R: 240, G: 240, B: 240, A: 255})
225+ root.ID, root.Surface, root.Seq, root.Version = 1, 1, 1, 1
226+ root.LogicalW, root.LogicalH = lw, lh
227+ root.Opaque = true
228+ root.Children = []ports.Subsurface{{SurfaceContent: page}}
229+ contents := map[ports.WindowID]ports.SurfaceContent{1: root}
230+ frame := func(i int) {
231+ x := i % 64
232+ scene := ports.Scene{Seq: uint64(i + 1), Scale: 1.3, Background: "#101010", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: x, W: lw, H: lh}}}}
233+ if err := render(r, scene, contents); err != nil {
234+ b.Fatal(err)
235+ }
236+ if err := r.waitFrame(r.submitted); err != nil {
237+ b.Fatal(err)
238+ }
239+ }
240+ for i := range 8 {
241+ frame(i)
242+ }
243+ b.ResetTimer()
244+ start := time.Now()
245+ for i := range b.N {
246+ frame(i + 8)
247+ }
248+ b.ReportMetric(float64(time.Since(start).Microseconds())/1000/float64(b.N), "ms/frame")
249+ })
250+ }
251+}
@@ -0,0 +1,251 @@
1+package vulkan
2+
3+import (
4+ "image"
5+ "image/color"
6+ "slices"
7+ "testing"
8+ "time"
9+
10+ "github.com/bnema/neferwl/internal/ports"
11+)
12+
13+// opaqueRegionContent is a 2x-scale shm root of w×h logical pixels under one
14+// child of the same size whose opaque region stops a pixel short of its right
15+// and bottom edges (Firefox's page over its shm root).
16+func opaqueRegionContent(t testing.TB, w, h int) ports.SurfaceContent {
17+ root := solidContent(t, 2*w, 2*h, color.RGBA{R: 255, A: 255})
18+ root.ID, root.Surface, root.Seq, root.Version = 1, 1, 1, 1
19+ root.LogicalW, root.LogicalH = w, h
20+ child := solidContent(t, w, h, color.RGBA{G: 255, A: 255})
21+ child.Surface, child.Version = 2, 1
22+ child.LogicalW, child.LogicalH = w, h
23+ child.OpaqueRect = ports.Rect{W: w - 1, H: h - 1}
24+ root.Children = []ports.Subsurface{{SurfaceContent: child}}
25+ return root
26+}
27+
28+// drawRects are the rects of the content draws (not the window's fills).
29+func drawRects(ds []draw) []image.Rectangle {
30+ var out []image.Rectangle
31+ for _, d := range ds {
32+ if d.pc.misc[0] == modeSolid {
33+ continue
34+ }
35+ out = append(out, image.Rect(int(d.pc.rect[0]), int(d.pc.rect[1]), int(d.pc.rect[2]), int(d.pc.rect[3])))
36+ }
37+ return out
38+}
39+
40+// A child whose opaque region leaves a thin edge out hides the root under the
41+// region only: the root draws the two strips around it, and the child draws
42+// over its own whole rect.
43+func TestSceneOpaqueRegionClipsRoot(t *testing.T) {
44+ r, err := New(64, 48)
45+ if err != nil {
46+ t.Skipf("Vulkan unavailable: %v", err)
47+ }
48+ defer r.Close()
49+ scene := ports.Scene{Seq: 1, Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 20, H: 12}}}}
50+ contents := map[ports.WindowID]ports.SurfaceContent{1: opaqueRegionContent(t, 20, 12)}
51+ ds := r.draws(scene, contents, newDamage(r.target(), scene, image.Rect(0, 0, 64, 48)))
52+ want := []image.Rectangle{
53+ // The root: the strip under the last row, then the one right of the
54+ // region; then the child over its whole rect.
55+ image.Rect(0, 11, 20, 12), image.Rect(19, 0, 20, 11),
56+ image.Rect(0, 0, 20, 12),
57+ }
58+ if got := drawRects(ds); !sameRects(got, want) {
59+ t.Fatalf("draws %v, want %v", got, want)
60+ }
61+}
62+
63+func sameRects(a, b []image.Rectangle) bool {
64+ if len(a) != len(b) {
65+ return false
66+ }
67+ for i := range a {
68+ if a[i] != b[i] {
69+ return false
70+ }
71+ }
72+ return true
73+}
74+
75+// A wholly opaque child still skips the root.
76+func TestSceneOpaqueChildSkipsRoot(t *testing.T) {
77+ r, err := New(64, 48)
78+ if err != nil {
79+ t.Skipf("Vulkan unavailable: %v", err)
80+ }
81+ defer r.Close()
82+ root := opaqueRegionContent(t, 20, 12)
83+ root.Children[0].Opaque, root.Children[0].OpaqueRect = true, ports.Rect{}
84+ scene := ports.Scene{Seq: 1, Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 20, H: 12}}}}
85+ ds := r.draws(scene, map[ports.WindowID]ports.SurfaceContent{1: root}, newDamage(r.target(), scene, image.Rect(0, 0, 64, 48)))
86+ if got, want := drawRects(ds), []image.Rectangle{image.Rect(0, 0, 20, 12)}; !sameRects(got, want) {
87+ t.Fatalf("draws %v, want %v", got, want)
88+ }
89+}
90+
91+// The strips show the root exactly as it draws alone: the child is
92+// transparent on its last row and column, outside its opaque region, and the
93+// root's pixels show through there, mapped as without the clipping. Inside
94+// the region the child shows.
95+func TestRendererOpaqueRegionStripsKeepRoot(t *testing.T) {
96+ r, err := New(64, 48)
97+ if err != nil {
98+ t.Skipf("Vulkan unavailable: %v", err)
99+ }
100+ defer r.Close()
101+ const w, h = 20, 12
102+ scene := ports.Scene{Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: w, H: h}}}}
103+ root := opaqueRegionContent(t, w, h)
104+ // A 2x root with a gradient, to see a wrong strip mapping; a child
105+ // that is green but for a transparent last row and column.
106+ px := make([]byte, 2*w*2*h*4)
107+ for y := range 2 * h {
108+ for x := range 2 * w {
109+ copy(px[(y*2*w+x)*4:], []byte{0, uint8(y * 10), uint8(x * 6), 255})
110+ }
111+ }
112+ root.SHM = shmContent(t, 2*w, 2*h, 2*w*4, px).SHM
113+ cp := make([]byte, w*h*4)
114+ for y := range h {
115+ for x := range w {
116+ if x < w-1 && y < h-1 {
117+ copy(cp[(y*w+x)*4:], []byte{0, 255, 0, 255})
118+ }
119+ }
120+ }
121+ root.Children[0].SHM = shmContent(t, w, h, w*4, cp).SHM
122+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
123+ t.Fatal(err)
124+ }
125+ with := readPixels(t, r)
126+ alone := root
127+ alone.Children = nil
128+ alone.Seq, alone.Version = 2, 2
129+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: alone}); err != nil {
130+ t.Fatal(err)
131+ }
132+ without := readPixels(t, r)
133+ green := color.RGBA{G: 255, A: 255}
134+ for y := range h {
135+ for x := range w {
136+ want := without.At(x, y)
137+ if x < w-1 && y < h-1 {
138+ want = green
139+ }
140+ if got := with.At(x, y); got != want {
141+ t.Fatalf("At(%d,%d)=%v want %v", x, y, got, want)
142+ }
143+ }
144+ }
145+}
146+
147+// opaqueCover maps the opaque rect onto the physical rect of the surface,
148+// rounding inward, clipped to what shows.
149+func TestOpaqueCover(t *testing.T) {
150+ c := ports.SurfaceContent{LogicalW: 984, LogicalH: 1231, OpaqueRect: ports.Rect{W: 983, H: 1230}}
151+ for _, tc := range []struct {
152+ name string
153+ full, dst image.Rectangle
154+ rect ports.Rect
155+ want image.Rectangle
156+ }{
157+ {"scale 1.3", image.Rect(0, 0, 1279, 1600), image.Rect(0, 0, 1279, 1600), c.OpaqueRect, image.Rect(0, 0, 1277, 1598)},
158+ {"offset", image.Rect(10, 20, 1289, 1620), image.Rect(10, 20, 1289, 1620), c.OpaqueRect, image.Rect(10, 20, 1287, 1618)},
159+ {"clipped", image.Rect(0, 0, 1279, 1600), image.Rect(0, 0, 100, 100), c.OpaqueRect, image.Rect(0, 0, 100, 100)},
160+ {"inner rect", image.Rect(0, 0, 984, 1231), image.Rect(0, 0, 984, 1231), ports.Rect{X: 4, Y: 8, W: 100, H: 200}, image.Rect(4, 8, 104, 208)},
161+ {"outside", image.Rect(0, 0, 984, 1231), image.Rect(0, 0, 3, 3), ports.Rect{X: 4, Y: 8, W: 100, H: 200}, image.Rectangle{}},
162+ } {
163+ t.Run(tc.name, func(t *testing.T) {
164+ c.OpaqueRect = tc.rect
165+ got := opaqueCover(&c, tc.full, tc.dst)
166+ if got != tc.want {
167+ t.Fatalf("opaqueCover = %v, want %v", got, tc.want)
168+ }
169+ })
170+ }
171+}
172+
173+// Building the draws of a window with a partly opaque child allocates
174+// nothing.
175+func TestSceneOpaqueRegionAllocations(t *testing.T) {
176+ r, err := New(64, 48)
177+ if err != nil {
178+ t.Skipf("Vulkan unavailable: %v", err)
179+ }
180+ defer r.Close()
181+ scene := ports.Scene{Seq: 1, Scale: 1.5, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 20, H: 12}}}}
182+ contents := map[ports.WindowID]ports.SurfaceContent{1: opaqueRegionContent(t, 20, 12)}
183+ bounds := image.Rect(0, 0, 64, 48)
184+ frame := func() { _ = r.draws(scene, contents, r.frameDamage(r.target(), scene, bounds)) }
185+ frame()
186+ if allocs := testing.AllocsPerRun(50, frame); allocs != 0 {
187+ t.Errorf("partly opaque child draws: %.1f allocs/frame, want 0", allocs)
188+ }
189+}
190+
191+// BenchmarkComposeOpaqueRegionChild composes a Firefox-like window moved every
192+// frame as a workspace swipe does, at 2560x1600 and scale 1.3: a 2x shm root
193+// of 984x1231 logical pixels under a dmabuf page of 1279x1600 pixels shown
194+// at 984x1231 (the viewport destination). "none" is a page with no opaque
195+// region (the root draws in full), "short" one with a region a logical pixel
196+// short of its right and bottom edges, "whole" one that covers it. The GPU is
197+// waited for after every frame. Select it with NEFERWL_VK_DEVICE.
198+func BenchmarkComposeOpaqueRegionChild(b *testing.B) {
199+ const w, h, lw, lh = 2560, 1600, 984, 1231
200+ for _, tc := range []struct {
201+ name string
202+ opaque bool
203+ rect ports.Rect
204+ }{
205+ {"none", false, ports.Rect{}},
206+ {"short", false, ports.Rect{W: lw - 1, H: lh - 1}},
207+ {"whole", true, ports.Rect{}},
208+ } {
209+ b.Run(tc.name, func(b *testing.B) {
210+ r, err := New(w, h)
211+ if err != nil {
212+ b.Skipf("Vulkan unavailable: %v", err)
213+ }
214+ defer r.Close()
215+ linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
216+ if !slices.Contains(r.DMABuf().Formats, linear) {
217+ b.Skipf("linear ARGB8888 not importable")
218+ }
219+ const pw, ph = 1279, 1600
220+ f := udmabuf(b, 1280, ph, func(x, y int) [4]byte { return [4]byte{byte(x), byte(y), 200, 255} })
221+ page := ports.SurfaceContent{Width: pw, Height: ph, LogicalW: lw, LogicalH: lh, Surface: 2, Version: 1,
222+ Opaque: tc.opaque, OpaqueRect: tc.rect,
223+ DMABuf: &ports.DMABuf{ID: 9, Width: pw, Height: ph, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 5120}}}}
224+ root := solidContent(b, 2*lw, 2*lh, color.RGBA{R: 240, G: 240, B: 240, A: 255})
225+ root.ID, root.Surface, root.Seq, root.Version = 1, 1, 1, 1
226+ root.LogicalW, root.LogicalH = lw, lh
227+ root.Opaque = true
228+ root.Children = []ports.Subsurface{{SurfaceContent: page}}
229+ contents := map[ports.WindowID]ports.SurfaceContent{1: root}
230+ frame := func(i int) {
231+ x := i % 64
232+ scene := ports.Scene{Seq: uint64(i + 1), Scale: 1.3, Background: "#101010", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: x, W: lw, H: lh}}}}
233+ if err := render(r, scene, contents); err != nil {
234+ b.Fatal(err)
235+ }
236+ if err := r.waitFrame(r.submitted); err != nil {
237+ b.Fatal(err)
238+ }
239+ }
240+ for i := range 8 {
241+ frame(i)
242+ }
243+ b.ResetTimer()
244+ start := time.Now()
245+ for i := range b.N {
246+ frame(i + 8)
247+ }
248+ b.ReportMetric(float64(time.Since(start).Microseconds())/1000/float64(b.N), "ms/frame")
249+ })
250+ }
251+}
M internal/adapters/wayland/surface.go +24 -3
@@ -393,7 +393,7 @@ func (s *surface) applyCommit(u *update) {
393393 s.bufferScale = u.scale
394394 }
395395 s.transform = u.transform
396- oldOpaque := s.content.Opaque
396+ oldOpaque, oldOpaqueRect := s.content.Opaque, s.content.OpaqueRect
397397 if u.opaqueSet {
398398 s.opaque = u.opaque
399399 }
@@ -455,6 +455,7 @@ func (s *surface) applyCommit(u *update) {
455455 c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height)
456456 s.formatOpaque = c.Opaque
457457 c.Opaque = (c.Opaque || s.opaqueCovers(c.LogicalW, c.LogicalH)) && u.fade == 0
458+ c.OpaqueRect = s.opaqueRect(c.Opaque, c.LogicalW, c.LogicalH, u.fade)
458459 c.Fade = u.fade
459460 resized := !s.has || c.Width != s.content.Width || c.Height != s.content.Height || c.LogicalW != s.content.LogicalW || c.LogicalH != s.content.LogicalH
460461 s.commitDamage(u, true, resized, c.Width, c.Height)
@@ -484,13 +485,14 @@ func (s *surface) applyCommit(u *update) {
484485 s.content.Source, _ = s.source(s.content.Width, s.content.Height)
485486 s.content.LogicalW, s.content.LogicalH = s.logicalSize(s.content.Width, s.content.Height)
486487 s.content.Opaque = (s.formatOpaque || s.opaqueCovers(s.content.LogicalW, s.content.LogicalH)) && u.fade == 0
488+ s.content.OpaqueRect = s.opaqueRect(s.content.Opaque, s.content.LogicalW, s.content.LogicalH, u.fade)
487489 s.content.Fade = u.fade
488490 }
489491 }
490492 if s.current == nil {
491493 s.content, s.has = ports.SurfaceContent{}, false
492494 }
493- if fresh || damaged || oldW != s.content.LogicalW || oldH != s.content.LogicalH || oldSource != s.content.Source || oldColor != s.color || oldRepresentation != s.representation || oldTransform != s.content.Transform || oldOpaque != s.content.Opaque || oldFade != s.content.Fade {
495+ if fresh || damaged || oldW != s.content.LogicalW || oldH != s.content.LogicalH || oldSource != s.content.Source || oldColor != s.color || oldRepresentation != s.representation || oldTransform != s.content.Transform || oldOpaque != s.content.Opaque || oldOpaqueRect != s.content.OpaqueRect || oldFade != s.content.Fade {
494496 s.version++
495497 s.root().treeDirty = true
496498 }
@@ -519,7 +521,7 @@ func (s *surface) applyCommit(u *update) {
519521 }
520522 reshaped := s.has && (s.content.LogicalW != oldW || s.content.LogicalH != oldH || s.content.Source != oldSource || s.content.Transform != oldTransform)
521523 // Opacity changes how every pixel blends, so it repaints the whole surface.
522- opacity := s.has && (s.content.Opaque != oldOpaque || s.content.Fade != oldFade)
524+ opacity := s.has && (s.content.Opaque != oldOpaque || s.content.OpaqueRect != oldOpaqueRect || s.content.Fade != oldFade)
523525 drawn := fresh || damaged || moved || geometry || hinted || reshaped || opacity || s.sub.parent != nil
524526 if drawn {
525527 if moved || geometry || reshaped || opacity {
@@ -623,6 +625,25 @@ func (s *surface) opaqueCovers(w, h int) bool {
623625 return w > 0 && h > 0 && covers(s.opaque, ports.Rect{W: w, H: h})
624626 }
625627
628+// opaqueRect is the largest rect of the committed opaque region inside the
629+// w×h surface, for a surface that is not wholly opaque (opaque) and not
630+// faded: clients often leave an edge out of a region that covers nearly
631+// all of the surface (a 983×1230 region on a 984×1231 surface). The zero
632+// Rect means none.
633+func (s *surface) opaqueRect(opaque bool, w, h int, fade float32) ports.Rect {
634+ var best ports.Rect
635+ if opaque || fade != 0 || w <= 0 || h <= 0 {
636+ return best
637+ }
638+ for _, r := range s.opaque {
639+ x0, y0, x1, y1 := max(r.X, 0), max(r.Y, 0), min(r.X+r.W, w), min(r.Y+r.H, h)
640+ if x1 > x0 && y1 > y0 && (x1-x0)*(y1-y0) > best.W*best.H {
641+ best = ports.Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
642+ }
643+ }
644+ return best
645+}
646+
626647 // maxCoverRects bounds the region covers checks: beyond it, not covered.
627648 const maxCoverRects = 16
628649
@@ -393,7 +393,7 @@ func (s *surface) applyCommit(u *update) {
393 s.bufferScale = u.scale393 s.bufferScale = u.scale
394 }394 }
395 s.transform = u.transform395 s.transform = u.transform
396- oldOpaque := s.content.Opaque396+ oldOpaque, oldOpaqueRect := s.content.Opaque, s.content.OpaqueRect
397 if u.opaqueSet {397 if u.opaqueSet {
398 s.opaque = u.opaque398 s.opaque = u.opaque
399 }399 }
@@ -455,6 +455,7 @@ func (s *surface) applyCommit(u *update) {
455 c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height)455 c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height)
456 s.formatOpaque = c.Opaque456 s.formatOpaque = c.Opaque
457 c.Opaque = (c.Opaque || s.opaqueCovers(c.LogicalW, c.LogicalH)) && u.fade == 0457 c.Opaque = (c.Opaque || s.opaqueCovers(c.LogicalW, c.LogicalH)) && u.fade == 0
458+ c.OpaqueRect = s.opaqueRect(c.Opaque, c.LogicalW, c.LogicalH, u.fade)
458 c.Fade = u.fade459 c.Fade = u.fade
459 resized := !s.has || c.Width != s.content.Width || c.Height != s.content.Height || c.LogicalW != s.content.LogicalW || c.LogicalH != s.content.LogicalH460 resized := !s.has || c.Width != s.content.Width || c.Height != s.content.Height || c.LogicalW != s.content.LogicalW || c.LogicalH != s.content.LogicalH
460 s.commitDamage(u, true, resized, c.Width, c.Height)461 s.commitDamage(u, true, resized, c.Width, c.Height)
@@ -484,13 +485,14 @@ func (s *surface) applyCommit(u *update) {
484 s.content.Source, _ = s.source(s.content.Width, s.content.Height)485 s.content.Source, _ = s.source(s.content.Width, s.content.Height)
485 s.content.LogicalW, s.content.LogicalH = s.logicalSize(s.content.Width, s.content.Height)486 s.content.LogicalW, s.content.LogicalH = s.logicalSize(s.content.Width, s.content.Height)
486 s.content.Opaque = (s.formatOpaque || s.opaqueCovers(s.content.LogicalW, s.content.LogicalH)) && u.fade == 0487 s.content.Opaque = (s.formatOpaque || s.opaqueCovers(s.content.LogicalW, s.content.LogicalH)) && u.fade == 0
488+ s.content.OpaqueRect = s.opaqueRect(s.content.Opaque, s.content.LogicalW, s.content.LogicalH, u.fade)
487 s.content.Fade = u.fade489 s.content.Fade = u.fade
488 }490 }
489 }491 }
490 if s.current == nil {492 if s.current == nil {
491 s.content, s.has = ports.SurfaceContent{}, false493 s.content, s.has = ports.SurfaceContent{}, false
492 }494 }
493- if fresh || damaged || oldW != s.content.LogicalW || oldH != s.content.LogicalH || oldSource != s.content.Source || oldColor != s.color || oldRepresentation != s.representation || oldTransform != s.content.Transform || oldOpaque != s.content.Opaque || oldFade != s.content.Fade {495+ if fresh || damaged || oldW != s.content.LogicalW || oldH != s.content.LogicalH || oldSource != s.content.Source || oldColor != s.color || oldRepresentation != s.representation || oldTransform != s.content.Transform || oldOpaque != s.content.Opaque || oldOpaqueRect != s.content.OpaqueRect || oldFade != s.content.Fade {
494 s.version++496 s.version++
495 s.root().treeDirty = true497 s.root().treeDirty = true
496 }498 }
@@ -519,7 +521,7 @@ func (s *surface) applyCommit(u *update) {
519 }521 }
520 reshaped := s.has && (s.content.LogicalW != oldW || s.content.LogicalH != oldH || s.content.Source != oldSource || s.content.Transform != oldTransform)522 reshaped := s.has && (s.content.LogicalW != oldW || s.content.LogicalH != oldH || s.content.Source != oldSource || s.content.Transform != oldTransform)
521 // Opacity changes how every pixel blends, so it repaints the whole surface.523 // Opacity changes how every pixel blends, so it repaints the whole surface.
522- opacity := s.has && (s.content.Opaque != oldOpaque || s.content.Fade != oldFade)524+ opacity := s.has && (s.content.Opaque != oldOpaque || s.content.OpaqueRect != oldOpaqueRect || s.content.Fade != oldFade)
523 drawn := fresh || damaged || moved || geometry || hinted || reshaped || opacity || s.sub.parent != nil525 drawn := fresh || damaged || moved || geometry || hinted || reshaped || opacity || s.sub.parent != nil
524 if drawn {526 if drawn {
525 if moved || geometry || reshaped || opacity {527 if moved || geometry || reshaped || opacity {
@@ -623,6 +625,25 @@ func (s *surface) opaqueCovers(w, h int) bool {
623 return w > 0 && h > 0 && covers(s.opaque, ports.Rect{W: w, H: h})625 return w > 0 && h > 0 && covers(s.opaque, ports.Rect{W: w, H: h})
624 }626 }
625 627
628+// opaqueRect is the largest rect of the committed opaque region inside the
629+// w×h surface, for a surface that is not wholly opaque (opaque) and not
630+// faded: clients often leave an edge out of a region that covers nearly
631+// all of the surface (a 983×1230 region on a 984×1231 surface). The zero
632+// Rect means none.
633+func (s *surface) opaqueRect(opaque bool, w, h int, fade float32) ports.Rect {
634+ var best ports.Rect
635+ if opaque || fade != 0 || w <= 0 || h <= 0 {
636+ return best
637+ }
638+ for _, r := range s.opaque {
639+ x0, y0, x1, y1 := max(r.X, 0), max(r.Y, 0), min(r.X+r.W, w), min(r.Y+r.H, h)
640+ if x1 > x0 && y1 > y0 && (x1-x0)*(y1-y0) > best.W*best.H {
641+ best = ports.Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
642+ }
643+ }
644+ return best
645+}
646+
626 // maxCoverRects bounds the region covers checks: beyond it, not covered.647 // maxCoverRects bounds the region covers checks: beyond it, not covered.
627 const maxCoverRects = 16648 const maxCoverRects = 16
628 649
M internal/adapters/wayland/transform_test.go +42 -2
@@ -161,13 +161,16 @@ func TestOpaqueRegionContent(t *testing.T) {
161161 if prev.Opaque {
162162 t.Fatalf("partial region made the surface opaque: %+v", prev)
163163 }
164+ if want := (ports.Rect{W: 15, H: 20}); prev.OpaqueRect != want {
165+ t.Fatalf("partial region: OpaqueRect %v, want %v", prev.OpaqueRect, want)
166+ }
164167 // Two halves cover the whole surface, even through a larger rect.
165168 // Opacity changes blending everywhere: the commit damages it all.
166169 whole := tc.region(t, ports.Rect{W: 15, H: 20}, ports.Rect{X: 15, W: 100, H: 20})
167170 requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestSetOpaqueRegion, whole)
168171 tc.commit(t)
169172 got := tc.next(t)
170- if !got.Opaque {
173+ if !got.Opaque || got.OpaqueRect != (ports.Rect{}) {
171174 t.Fatalf("covering region: %+v", got)
172175 }
173176 if rects, ok := got.DamageSince(prev.Seq); ok {
@@ -186,9 +189,46 @@ func TestOpaqueRegionContent(t *testing.T) {
186189 }
187190 requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestSetOpaqueRegion, uint32(0))
188191 tc.commit(t)
189- if got := tc.next(t); got.Opaque {
192+ if got := tc.next(t); got.Opaque || got.OpaqueRect != (ports.Rect{}) {
190193 t.Fatalf("NULL region: %+v", got)
191194 }
195+ // A region one pixel short of the surface (what Firefox sets) is not
196+ // opaque, but its rect is reported.
197+ short := tc.region(t, ports.Rect{W: 29, H: 19})
198+ requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestSetOpaqueRegion, short)
199+ tc.commit(t)
200+ if got := tc.next(t); got.Opaque || got.OpaqueRect != (ports.Rect{W: 29, H: 19}) {
201+ t.Fatalf("short region: %+v", got)
202+ }
203+}
204+
205+// opaqueRect is the largest rect of the region inside the surface, none for
206+// an opaque or faded surface.
207+func TestOpaqueRect(t *testing.T) {
208+ for _, tc := range []struct {
209+ name string
210+ region []ports.Rect
211+ opaque bool
212+ w, h int
213+ fade float32
214+ want ports.Rect
215+ }{
216+ {"none", nil, false, 10, 10, 0, ports.Rect{}},
217+ {"one pixel short", []ports.Rect{{W: 983, H: 1230}}, false, 984, 1231, 0, ports.Rect{W: 983, H: 1230}},
218+ {"clipped to the surface", []ports.Rect{{X: -5, Y: 2, W: 100, H: 100}}, false, 10, 10, 0, ports.Rect{Y: 2, W: 10, H: 8}},
219+ {"largest of several", []ports.Rect{{W: 2, H: 2}, {X: 3, Y: 3, W: 5, H: 4}}, false, 10, 10, 0, ports.Rect{X: 3, Y: 3, W: 5, H: 4}},
220+ {"outside", []ports.Rect{{X: 10, W: 5, H: 5}}, false, 10, 10, 0, ports.Rect{}},
221+ {"wholly opaque", []ports.Rect{{W: 9, H: 9}}, true, 10, 10, 0, ports.Rect{}},
222+ {"faded", []ports.Rect{{W: 9, H: 9}}, false, 10, 10, 0.5, ports.Rect{}},
223+ {"no size", []ports.Rect{{W: 9, H: 9}}, false, 0, 0, 0, ports.Rect{}},
224+ } {
225+ t.Run(tc.name, func(t *testing.T) {
226+ s := &surface{opaque: tc.region}
227+ if got := s.opaqueRect(tc.opaque, tc.w, tc.h, tc.fade); got != tc.want {
228+ t.Fatalf("opaqueRect = %v, want %v", got, tc.want)
229+ }
230+ })
231+ }
192232 }
193233
194234 // Transform and opaque region requested after a queued commit belong to
@@ -161,13 +161,16 @@ func TestOpaqueRegionContent(t *testing.T) {
161 if prev.Opaque {161 if prev.Opaque {
162 t.Fatalf("partial region made the surface opaque: %+v", prev)162 t.Fatalf("partial region made the surface opaque: %+v", prev)
163 }163 }
164+ if want := (ports.Rect{W: 15, H: 20}); prev.OpaqueRect != want {
165+ t.Fatalf("partial region: OpaqueRect %v, want %v", prev.OpaqueRect, want)
166+ }
164 // Two halves cover the whole surface, even through a larger rect.167 // Two halves cover the whole surface, even through a larger rect.
165 // Opacity changes blending everywhere: the commit damages it all.168 // Opacity changes blending everywhere: the commit damages it all.
166 whole := tc.region(t, ports.Rect{W: 15, H: 20}, ports.Rect{X: 15, W: 100, H: 20})169 whole := tc.region(t, ports.Rect{W: 15, H: 20}, ports.Rect{X: 15, W: 100, H: 20})
167 requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestSetOpaqueRegion, whole)170 requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestSetOpaqueRegion, whole)
168 tc.commit(t)171 tc.commit(t)
169 got := tc.next(t)172 got := tc.next(t)
170- if !got.Opaque {173+ if !got.Opaque || got.OpaqueRect != (ports.Rect{}) {
171 t.Fatalf("covering region: %+v", got)174 t.Fatalf("covering region: %+v", got)
172 }175 }
173 if rects, ok := got.DamageSince(prev.Seq); ok {176 if rects, ok := got.DamageSince(prev.Seq); ok {
@@ -186,9 +189,46 @@ func TestOpaqueRegionContent(t *testing.T) {
186 }189 }
187 requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestSetOpaqueRegion, uint32(0))190 requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestSetOpaqueRegion, uint32(0))
188 tc.commit(t)191 tc.commit(t)
189- if got := tc.next(t); got.Opaque {192+ if got := tc.next(t); got.Opaque || got.OpaqueRect != (ports.Rect{}) {
190 t.Fatalf("NULL region: %+v", got)193 t.Fatalf("NULL region: %+v", got)
191 }194 }
195+ // A region one pixel short of the surface (what Firefox sets) is not
196+ // opaque, but its rect is reported.
197+ short := tc.region(t, ports.Rect{W: 29, H: 19})
198+ requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestSetOpaqueRegion, short)
199+ tc.commit(t)
200+ if got := tc.next(t); got.Opaque || got.OpaqueRect != (ports.Rect{W: 29, H: 19}) {
201+ t.Fatalf("short region: %+v", got)
202+ }
203+}
204+
205+// opaqueRect is the largest rect of the region inside the surface, none for
206+// an opaque or faded surface.
207+func TestOpaqueRect(t *testing.T) {
208+ for _, tc := range []struct {
209+ name string
210+ region []ports.Rect
211+ opaque bool
212+ w, h int
213+ fade float32
214+ want ports.Rect
215+ }{
216+ {"none", nil, false, 10, 10, 0, ports.Rect{}},
217+ {"one pixel short", []ports.Rect{{W: 983, H: 1230}}, false, 984, 1231, 0, ports.Rect{W: 983, H: 1230}},
218+ {"clipped to the surface", []ports.Rect{{X: -5, Y: 2, W: 100, H: 100}}, false, 10, 10, 0, ports.Rect{Y: 2, W: 10, H: 8}},
219+ {"largest of several", []ports.Rect{{W: 2, H: 2}, {X: 3, Y: 3, W: 5, H: 4}}, false, 10, 10, 0, ports.Rect{X: 3, Y: 3, W: 5, H: 4}},
220+ {"outside", []ports.Rect{{X: 10, W: 5, H: 5}}, false, 10, 10, 0, ports.Rect{}},
221+ {"wholly opaque", []ports.Rect{{W: 9, H: 9}}, true, 10, 10, 0, ports.Rect{}},
222+ {"faded", []ports.Rect{{W: 9, H: 9}}, false, 10, 10, 0.5, ports.Rect{}},
223+ {"no size", []ports.Rect{{W: 9, H: 9}}, false, 0, 0, 0, ports.Rect{}},
224+ } {
225+ t.Run(tc.name, func(t *testing.T) {
226+ s := &surface{opaque: tc.region}
227+ if got := s.opaqueRect(tc.opaque, tc.w, tc.h, tc.fade); got != tc.want {
228+ t.Fatalf("opaqueRect = %v, want %v", got, tc.want)
229+ }
230+ })
231+ }
192 }232 }
193 233
194 // Transform and opaque region requested after a queued commit belong to234 // Transform and opaque region requested after a queued commit belong to
M internal/ports/scene.go +5 -0
@@ -207,6 +207,11 @@ type SurfaceContent struct {
207207 // Opaque ignores the alpha byte: an x format, or an opaque region that
208208 // covers the whole surface. It is false while Fade is set.
209209 Opaque bool
210+ // OpaqueRect is the largest rect of the opaque region inside a surface
211+ // that is not wholly Opaque, in logical pixels from the surface origin:
212+ // the pixels there ignore alpha too, so the surfaces below it are hidden
213+ // by it. Empty when there is none or while Fade is set.
214+ OpaqueRect Rect
210215 // Fade is how much wp_alpha_modifier_v1 fades the surface: 0 none,
211216 // 1 invisible. Its opacity is multiplied by 1-Fade.
212217 Fade float32
@@ -207,6 +207,11 @@ type SurfaceContent struct {
207 // Opaque ignores the alpha byte: an x format, or an opaque region that207 // Opaque ignores the alpha byte: an x format, or an opaque region that
208 // covers the whole surface. It is false while Fade is set.208 // covers the whole surface. It is false while Fade is set.
209 Opaque bool209 Opaque bool
210+ // OpaqueRect is the largest rect of the opaque region inside a surface
211+ // that is not wholly Opaque, in logical pixels from the surface origin:
212+ // the pixels there ignore alpha too, so the surfaces below it are hidden
213+ // by it. Empty when there is none or while Fade is set.
214+ OpaqueRect Rect
210 // Fade is how much wp_alpha_modifier_v1 fades the surface: 0 none,215 // Fade is how much wp_alpha_modifier_v1 fades the surface: 0 none,
211 // 1 invisible. Its opacity is multiplied by 1-Fade.216 // 1 invisible. Its opacity is multiplied by 1-Fade.
212 Fade float32217 Fade float32

Keyboard shortcuts

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

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

Branches 24

Tags 10

↑↓ moveenter openesc close