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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +388 -13
M Makefile +1 -1
| @@ -60,7 +60,7 @@ arch: | |||
| 60 | 60 | $(HOME)/go/bin/hexcheck -hexcheck.config .hexcheck.yaml -hexcheck.root . ./... | |
| 61 | 61 | # Fast, mandatory guards for tiled content publications and SHM copies. | |
| 62 | 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=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 | |
| 64 | 64 | check: vet test arch fakes-check log-check perf-check | |
| 65 | 65 | ||
| 66 | 66 | # 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=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 |
| 64 | check: vet test arch fakes-check log-check perf-check | 64 | check: vet test arch fakes-check log-check perf-check | ||
| 65 | 65 | ||||
| 66 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | 66 | # 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 ( | |||
| 14 | 14 | ||
| 15 | 15 | // udmabuf turns memfd pages into a real dmabuf: a linear buffer any GPU | |
| 16 | 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 | 18 | t.Helper() | |
| 19 | 19 | dev, err := os.OpenFile("/dev/udmabuf", os.O_RDWR, 0) | |
| 20 | 20 | if err != nil { | |
| @@ -14,7 +14,7 @@ import ( | |||||
| 14 | 14 | ||||
| 15 | // udmabuf turns memfd pages into a real dmabuf: a linear buffer any GPU | 15 | // 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 | |||
| 310 | 310 | } | |
| 311 | 311 | } | |
| 312 | 312 | ||
| 313 | - | // opaqueChildren gives the physical rects shown by the opaque subsurfaces | |
| 313 | + | // opaqueChildren gives the physical rects shown opaque by the subsurfaces | |
| 314 | 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 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. | |
| 318 | 321 | func (w *sceneWalk) opaqueChildren(content *ports.SurfaceContent, ox, oy float64, clip image.Rectangle) []image.Rectangle { | |
| 319 | 322 | covers := w.r.scratchCovers[:0] | |
| 320 | 323 | for i := range content.Children { | |
| 321 | 324 | ch := &content.Children[i] | |
| 322 | - | if ch.Below || !ch.Opaque { | |
| 325 | + | if ch.Below || !ch.Opaque && ch.OpaqueRect.W <= 0 { | |
| 323 | 326 | 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 | 329 | if !ok || !w.drawable(&ch.SurfaceContent) { | |
| 327 | 330 | continue | |
| 328 | 331 | } | |
| 332 | + | if !ch.Opaque { | |
| 333 | + | if dst = opaqueCover(&ch.SurfaceContent, full, dst); dst.Empty() { | |
| 334 | + | continue | |
| 335 | + | } | |
| 336 | + | } | |
| 329 | 337 | covers = append(covers, dst) | |
| 330 | 338 | } | |
| 331 | 339 | w.r.scratchCovers = covers | |
| 332 | 340 | 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 | 363 | // drawable reports whether the child's buffer will draw. GPU buffer | |
| 336 | 364 | // imports are cached, so pre-importing here costs nothing. | |
| 337 | 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 | 411 | // surface draws one surface buffer with its origin at (x, y) logical, | |
| 384 | 412 | // clipped to clip (logical), unless an opaque surface above it (covers, | |
| 385 | 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 | 415 | func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y float64, clip image.Rectangle, key shmKey, seq uint64, covers []image.Rectangle, root bool) { | |
| 387 | 416 | full, dst, ok := w.surfaceRects(content, x, y, clip) | |
| 388 | 417 | if !ok { | |
| @@ -396,7 +425,36 @@ func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y float64, clip im | |||
| 396 | 425 | if root { | |
| 397 | 426 | w.dmg.content(key.win, content, full, dst) | |
| 398 | 427 | } | |
| 428 | + | n := len(w.draws) | |
| 399 | 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 | 460 | // 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 subsurfaces | 313 | + | // 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 is | 315 | + | // 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 to | 316 | + | // 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 | continue | 326 | 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 | continue | 330 | 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 = covers | 339 | w.r.scratchCovers = covers | ||
| 332 | return covers | 340 | 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 buffer | 363 | // 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) { | |||
| 393 | 393 | s.bufferScale = u.scale | |
| 394 | 394 | } | |
| 395 | 395 | s.transform = u.transform | |
| 396 | - | oldOpaque := s.content.Opaque | |
| 396 | + | oldOpaque, oldOpaqueRect := s.content.Opaque, s.content.OpaqueRect | |
| 397 | 397 | if u.opaqueSet { | |
| 398 | 398 | s.opaque = u.opaque | |
| 399 | 399 | } | |
| @@ -455,6 +455,7 @@ func (s *surface) applyCommit(u *update) { | |||
| 455 | 455 | c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height) | |
| 456 | 456 | s.formatOpaque = c.Opaque | |
| 457 | 457 | 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 | 459 | c.Fade = u.fade | |
| 459 | 460 | resized := !s.has || c.Width != s.content.Width || c.Height != s.content.Height || c.LogicalW != s.content.LogicalW || c.LogicalH != s.content.LogicalH | |
| 460 | 461 | s.commitDamage(u, true, resized, c.Width, c.Height) | |
| @@ -484,13 +485,14 @@ func (s *surface) applyCommit(u *update) { | |||
| 484 | 485 | s.content.Source, _ = s.source(s.content.Width, s.content.Height) | |
| 485 | 486 | s.content.LogicalW, s.content.LogicalH = s.logicalSize(s.content.Width, s.content.Height) | |
| 486 | 487 | 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 | 489 | s.content.Fade = u.fade | |
| 488 | 490 | } | |
| 489 | 491 | } | |
| 490 | 492 | if s.current == nil { | |
| 491 | 493 | 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 | 496 | s.version++ | |
| 495 | 497 | s.root().treeDirty = true | |
| 496 | 498 | } | |
| @@ -519,7 +521,7 @@ func (s *surface) applyCommit(u *update) { | |||
| 519 | 521 | } | |
| 520 | 522 | reshaped := s.has && (s.content.LogicalW != oldW || s.content.LogicalH != oldH || s.content.Source != oldSource || s.content.Transform != oldTransform) | |
| 521 | 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 | 525 | drawn := fresh || damaged || moved || geometry || hinted || reshaped || opacity || s.sub.parent != nil | |
| 524 | 526 | if drawn { | |
| 525 | 527 | if moved || geometry || reshaped || opacity { | |
| @@ -623,6 +625,25 @@ func (s *surface) opaqueCovers(w, h int) bool { | |||
| 623 | 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 | 647 | // maxCoverRects bounds the region covers checks: beyond it, not covered. | |
| 627 | 648 | const maxCoverRects = 16 | |
| 628 | 649 | ||
| @@ -393,7 +393,7 @@ func (s *surface) applyCommit(u *update) { | |||||
| 393 | s.bufferScale = u.scale | 393 | s.bufferScale = u.scale | ||
| 394 | } | 394 | } | ||
| 395 | s.transform = u.transform | 395 | s.transform = u.transform | ||
| 396 | - | oldOpaque := s.content.Opaque | 396 | + | oldOpaque, oldOpaqueRect := s.content.Opaque, s.content.OpaqueRect |
| 397 | if u.opaqueSet { | 397 | if u.opaqueSet { | ||
| 398 | s.opaque = u.opaque | 398 | 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.Opaque | 456 | s.formatOpaque = c.Opaque | ||
| 457 | c.Opaque = (c.Opaque || s.opaqueCovers(c.LogicalW, c.LogicalH)) && u.fade == 0 | 457 | 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.fade | 459 | 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.LogicalH | 460 | 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 == 0 | 487 | 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.fade | 489 | 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{}, false | 493 | 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 = true | 497 | 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 != nil | 525 | 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 = 16 | 648 | const maxCoverRects = 16 | ||
| 628 | 649 | ||||
M internal/adapters/wayland/transform_test.go +42 -2
| @@ -161,13 +161,16 @@ func TestOpaqueRegionContent(t *testing.T) { | |||
| 161 | 161 | if prev.Opaque { | |
| 162 | 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 | 167 | // Two halves cover the whole surface, even through a larger rect. | |
| 165 | 168 | // Opacity changes blending everywhere: the commit damages it all. | |
| 166 | 169 | whole := tc.region(t, ports.Rect{W: 15, H: 20}, ports.Rect{X: 15, W: 100, H: 20}) | |
| 167 | 170 | requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestSetOpaqueRegion, whole) | |
| 168 | 171 | tc.commit(t) | |
| 169 | 172 | got := tc.next(t) | |
| 170 | - | if !got.Opaque { | |
| 173 | + | if !got.Opaque || got.OpaqueRect != (ports.Rect{}) { | |
| 171 | 174 | t.Fatalf("covering region: %+v", got) | |
| 172 | 175 | } | |
| 173 | 176 | if rects, ok := got.DamageSince(prev.Seq); ok { | |
| @@ -186,9 +189,46 @@ func TestOpaqueRegionContent(t *testing.T) { | |||
| 186 | 189 | } | |
| 187 | 190 | requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestSetOpaqueRegion, uint32(0)) | |
| 188 | 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 | 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 | 234 | // 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 to | 234 | // 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 { | |||
| 207 | 207 | // Opaque ignores the alpha byte: an x format, or an opaque region that | |
| 208 | 208 | // covers the whole surface. It is false while Fade is set. | |
| 209 | 209 | 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 | 215 | // Fade is how much wp_alpha_modifier_v1 fades the surface: 0 none, | |
| 211 | 216 | // 1 invisible. Its opacity is multiplied by 1-Fade. | |
| 212 | 217 | Fade float32 | |
| @@ -207,6 +207,11 @@ type SurfaceContent struct { | |||||
| 207 | // Opaque ignores the alpha byte: an x format, or an opaque region that | 207 | // 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 bool | 209 | 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 float32 | 217 | Fade float32 | ||