perf(core): guard publish, overview and partial-redraw allocations, reuse damage scratch
- commit
- 52e963535ab44dbb0c53325c4e4c150f68ebf91d
- author
- brice · · 2026-10-04 20:13 UTC
- committer
- brice · · 2026-10-04 20:13 UTC
- parent
- afb3cdc
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +151 -48
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|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestDueFramesAllocations|TestRenderSteadyStateAllocations|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|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestDueFramesAllocations|TestRenderSteadyStateAllocations|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|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestDueFramesAllocations|TestRenderSteadyStateAllocations|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|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestDueFramesAllocations|TestRenderSteadyStateAllocations|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/damage.go +9 -6
| @@ -22,6 +22,8 @@ type damageRegion struct { | |||
| 22 | 22 | seen map[ports.WindowID]bool | |
| 23 | 23 | bound image.Rectangle | |
| 24 | 24 | drawn map[ports.WindowID]heldWindow | |
| 25 | + | // rects is scratch for a content's damage rects (reused across frames). | |
| 26 | + | rects []ports.Rect | |
| 25 | 27 | } | |
| 26 | 28 | ||
| 27 | 29 | // heldWindow is a window a target holds: its content Seq and rect. | |
| @@ -50,7 +52,7 @@ func (r *Renderer) frameDamage(tg *target, s ports.Scene, bounds image.Rectangle | |||
| 50 | 52 | r.damageDrawn = make(map[ports.WindowID]heldWindow) | |
| 51 | 53 | } | |
| 52 | 54 | clear(r.damageDrawn) | |
| 53 | - | r.damage = damageRegion{seen: r.damageSeen, drawn: r.damageDrawn, bound: bounds} | |
| 55 | + | r.damage = damageRegion{seen: r.damageSeen, drawn: r.damageDrawn, bound: bounds, rects: r.damage.rects[:0]} | |
| 54 | 56 | if tg.valid && s.Seq != 0 && tg.sceneSeq == s.Seq { | |
| 55 | 57 | r.damage.held = tg | |
| 56 | 58 | } | |
| @@ -86,13 +88,12 @@ func (d *damageRegion) window(id ports.WindowID, c ports.SurfaceContent, rect im | |||
| 86 | 88 | d.add(rect) | |
| 87 | 89 | return | |
| 88 | 90 | } | |
| 89 | - | rects, known := c.DamageSince(held) | |
| 91 | + | var known bool | |
| 92 | + | d.rects, known = c.AppendDamageSince(d.rects[:0], held) | |
| 90 | 93 | if !known { | |
| 91 | 94 | d.add(rect) | |
| 92 | - | return | |
| 93 | 95 | } | |
| 94 | - | // Mapped by content, called next: nothing more here. | |
| 95 | - | _ = rects | |
| 96 | + | // Otherwise mapped by content, called next: nothing more here. | |
| 96 | 97 | } | |
| 97 | 98 | ||
| 98 | 99 | // content maps a root surface's buffer damage onto full (where its w×h | |
| @@ -107,10 +108,12 @@ func (d *damageRegion) content(id ports.WindowID, c *ports.SurfaceContent, full, | |||
| 107 | 108 | if !ok || held == c.Seq || len(c.Children) > 0 { | |
| 108 | 109 | return | |
| 109 | 110 | } | |
| 110 | - | rects, known := c.DamageSince(held) | |
| 111 | + | var known bool | |
| 112 | + | d.rects, known = c.AppendDamageSince(d.rects[:0], held) | |
| 111 | 113 | if !known || c.Width <= 0 || c.Height <= 0 { | |
| 112 | 114 | return | |
| 113 | 115 | } | |
| 116 | + | rects := d.rects | |
| 114 | 117 | if c.Transform != 0 { | |
| 115 | 118 | d.add(dst) | |
| 116 | 119 | return | |
| @@ -22,6 +22,8 @@ type damageRegion struct { | |||||
| 22 | seen map[ports.WindowID]bool | 22 | seen map[ports.WindowID]bool | ||
| 23 | bound image.Rectangle | 23 | bound image.Rectangle | ||
| 24 | drawn map[ports.WindowID]heldWindow | 24 | drawn map[ports.WindowID]heldWindow | ||
| 25 | + | // rects is scratch for a content's damage rects (reused across frames). | |||
| 26 | + | rects []ports.Rect | |||
| 25 | } | 27 | } | ||
| 26 | 28 | ||||
| 27 | // heldWindow is a window a target holds: its content Seq and rect. | 29 | // heldWindow is a window a target holds: its content Seq and rect. | ||
| @@ -50,7 +52,7 @@ func (r *Renderer) frameDamage(tg *target, s ports.Scene, bounds image.Rectangle | |||||
| 50 | r.damageDrawn = make(map[ports.WindowID]heldWindow) | 52 | r.damageDrawn = make(map[ports.WindowID]heldWindow) | ||
| 51 | } | 53 | } | ||
| 52 | clear(r.damageDrawn) | 54 | clear(r.damageDrawn) | ||
| 53 | - | r.damage = damageRegion{seen: r.damageSeen, drawn: r.damageDrawn, bound: bounds} | 55 | + | r.damage = damageRegion{seen: r.damageSeen, drawn: r.damageDrawn, bound: bounds, rects: r.damage.rects[:0]} |
| 54 | if tg.valid && s.Seq != 0 && tg.sceneSeq == s.Seq { | 56 | if tg.valid && s.Seq != 0 && tg.sceneSeq == s.Seq { | ||
| 55 | r.damage.held = tg | 57 | r.damage.held = tg | ||
| 56 | } | 58 | } | ||
| @@ -86,13 +88,12 @@ func (d *damageRegion) window(id ports.WindowID, c ports.SurfaceContent, rect im | |||||
| 86 | d.add(rect) | 88 | d.add(rect) | ||
| 87 | return | 89 | return | ||
| 88 | } | 90 | } | ||
| 89 | - | rects, known := c.DamageSince(held) | 91 | + | var known bool |
| 92 | + | d.rects, known = c.AppendDamageSince(d.rects[:0], held) | |||
| 90 | if !known { | 93 | if !known { | ||
| 91 | d.add(rect) | 94 | d.add(rect) | ||
| 92 | - | return | |||
| 93 | } | 95 | } | ||
| 94 | - | // Mapped by content, called next: nothing more here. | 96 | + | // Otherwise mapped by content, called next: nothing more here. |
| 95 | - | _ = rects | |||
| 96 | } | 97 | } | ||
| 97 | 98 | ||||
| 98 | // content maps a root surface's buffer damage onto full (where its w×h | 99 | // content maps a root surface's buffer damage onto full (where its w×h | ||
| @@ -107,10 +108,12 @@ func (d *damageRegion) content(id ports.WindowID, c *ports.SurfaceContent, full, | |||||
| 107 | if !ok || held == c.Seq || len(c.Children) > 0 { | 108 | if !ok || held == c.Seq || len(c.Children) > 0 { | ||
| 108 | return | 109 | return | ||
| 109 | } | 110 | } | ||
| 110 | - | rects, known := c.DamageSince(held) | 111 | + | var known bool |
| 112 | + | d.rects, known = c.AppendDamageSince(d.rects[:0], held) | |||
| 111 | if !known || c.Width <= 0 || c.Height <= 0 { | 113 | if !known || c.Width <= 0 || c.Height <= 0 { | ||
| 112 | return | 114 | return | ||
| 113 | } | 115 | } | ||
| 116 | + | rects := d.rects | |||
| 114 | if c.Transform != 0 { | 117 | if c.Transform != 0 { | ||
| 115 | d.add(dst) | 118 | d.add(dst) | ||
| 116 | return | 119 | return | ||
M internal/adapters/vulkan/scene_perf_test.go +26 -10
| @@ -62,20 +62,36 @@ func TestRenderSteadyStateAllocations(t *testing.T) { | |||
| 62 | 62 | if allocs := testing.AllocsPerRun(50, frameDraws); allocs != 0 { | |
| 63 | 63 | t.Errorf("moving window draws: %.1f allocs/frame, want 0", allocs) | |
| 64 | 64 | } | |
| 65 | - | // A content change under an unchanged scene Seq: partial redraw | |
| 66 | - | // from the window damage, clipped to the changed region. | |
| 67 | - | changed := [2]ports.SurfaceContent{content, content} | |
| 68 | - | changed[1].Seq, changed[1].Version = 2, 2 | |
| 69 | - | same := scene | |
| 70 | - | same.Seq = r.last.sceneSeq | |
| 65 | + | // A content change under an unchanged scene Seq: a partial | |
| 66 | + | // redraw clipped to the changed buffer region, replicating the | |
| 67 | + | // partial sequence of Render. The stand-in target holds the | |
| 68 | + | // previous content Seq of the window, and is never updated, so | |
| 69 | + | // every run is damaged. | |
| 70 | + | dc := content | |
| 71 | + | dc.Seq, dc.Version = 2, 2 | |
| 72 | + | dc.DamageHistory = []ports.SeqDamage{{Seq: 2, Rects: []ports.Rect{{X: 1, Y: 1, W: 2, H: 2}}}} | |
| 73 | + | pcontents := map[ports.WindowID]ports.SurfaceContent{1: dc} | |
| 74 | + | held := &target{valid: true, sceneSeq: scene.Seq, windows: map[ports.WindowID]heldWindow{1: {seq: 1, rect: image.Rect(0, 0, r.width, r.height)}}} | |
| 75 | + | var area image.Rectangle | |
| 76 | + | var whole bool | |
| 71 | 77 | partial := func() { | |
| 72 | - | n++ | |
| 73 | - | contents[1] = changed[n%2] | |
| 74 | - | _ = r.draws(same, contents, r.frameDamage(r.last, same, bounds)) | |
| 78 | + | dmg := r.frameDamage(held, scene, bounds) | |
| 79 | + | ds := r.draws(scene, pcontents, dmg) | |
| 80 | + | dmg.finish() | |
| 81 | + | ds = append(ds, draw{}) | |
| 82 | + | copy(ds[1:], ds[:len(ds)-1]) | |
| 83 | + | ds[0] = r.fillDraw(dmg.area, parseColor(scene.Background)) | |
| 84 | + | ds = dmg.clip(ds) | |
| 85 | + | r.scratchDraws = ds | |
| 86 | + | r.orient(ds, scene.Transform) | |
| 87 | + | area, whole = dmg.area, dmg.all() | |
| 75 | 88 | } | |
| 76 | 89 | partial() | |
| 90 | + | if whole || area.Empty() || area.Dx()*area.Dy() >= bounds.Dx()*bounds.Dy() { | |
| 91 | + | t.Fatalf("not a partial redraw: all %v, area %v of %v", whole, area, bounds) | |
| 92 | + | } | |
| 77 | 93 | if allocs := testing.AllocsPerRun(50, partial); allocs != 0 { | |
| 78 | - | t.Errorf("content-damage draws: %.1f allocs/frame, want 0", allocs) | |
| 94 | + | t.Errorf("partial redraw draws: %.1f allocs/frame, want 0", allocs) | |
| 79 | 95 | } | |
| 80 | 96 | }) | |
| 81 | 97 | } | |
| @@ -62,20 +62,36 @@ func TestRenderSteadyStateAllocations(t *testing.T) { | |||||
| 62 | if allocs := testing.AllocsPerRun(50, frameDraws); allocs != 0 { | 62 | if allocs := testing.AllocsPerRun(50, frameDraws); allocs != 0 { | ||
| 63 | t.Errorf("moving window draws: %.1f allocs/frame, want 0", allocs) | 63 | t.Errorf("moving window draws: %.1f allocs/frame, want 0", allocs) | ||
| 64 | } | 64 | } | ||
| 65 | - | // A content change under an unchanged scene Seq: partial redraw | 65 | + | // A content change under an unchanged scene Seq: a partial |
| 66 | - | // from the window damage, clipped to the changed region. | 66 | + | // redraw clipped to the changed buffer region, replicating the |
| 67 | - | changed := [2]ports.SurfaceContent{content, content} | 67 | + | // partial sequence of Render. The stand-in target holds the |
| 68 | - | changed[1].Seq, changed[1].Version = 2, 2 | 68 | + | // previous content Seq of the window, and is never updated, so |
| 69 | - | same := scene | 69 | + | // every run is damaged. |
| 70 | - | same.Seq = r.last.sceneSeq | 70 | + | dc := content |
| 71 | + | dc.Seq, dc.Version = 2, 2 | |||
| 72 | + | dc.DamageHistory = []ports.SeqDamage{{Seq: 2, Rects: []ports.Rect{{X: 1, Y: 1, W: 2, H: 2}}}} | |||
| 73 | + | pcontents := map[ports.WindowID]ports.SurfaceContent{1: dc} | |||
| 74 | + | held := &target{valid: true, sceneSeq: scene.Seq, windows: map[ports.WindowID]heldWindow{1: {seq: 1, rect: image.Rect(0, 0, r.width, r.height)}}} | |||
| 75 | + | var area image.Rectangle | |||
| 76 | + | var whole bool | |||
| 71 | partial := func() { | 77 | partial := func() { | ||
| 72 | - | n++ | 78 | + | dmg := r.frameDamage(held, scene, bounds) |
| 73 | - | contents[1] = changed[n%2] | 79 | + | ds := r.draws(scene, pcontents, dmg) |
| 74 | - | _ = r.draws(same, contents, r.frameDamage(r.last, same, bounds)) | 80 | + | dmg.finish() |
| 81 | + | ds = append(ds, draw{}) | |||
| 82 | + | copy(ds[1:], ds[:len(ds)-1]) | |||
| 83 | + | ds[0] = r.fillDraw(dmg.area, parseColor(scene.Background)) | |||
| 84 | + | ds = dmg.clip(ds) | |||
| 85 | + | r.scratchDraws = ds | |||
| 86 | + | r.orient(ds, scene.Transform) | |||
| 87 | + | area, whole = dmg.area, dmg.all() | |||
| 75 | } | 88 | } | ||
| 76 | partial() | 89 | partial() | ||
| 90 | + | if whole || area.Empty() || area.Dx()*area.Dy() >= bounds.Dx()*bounds.Dy() { | |||
| 91 | + | t.Fatalf("not a partial redraw: all %v, area %v of %v", whole, area, bounds) | |||
| 92 | + | } | |||
| 77 | if allocs := testing.AllocsPerRun(50, partial); allocs != 0 { | 93 | if allocs := testing.AllocsPerRun(50, partial); allocs != 0 { | ||
| 78 | - | t.Errorf("content-damage draws: %.1f allocs/frame, want 0", allocs) | 94 | + | t.Errorf("partial redraw draws: %.1f allocs/frame, want 0", allocs) |
| 79 | } | 95 | } | ||
| 80 | }) | 96 | }) | ||
| 81 | } | 97 | } | ||
M internal/core/core.go +3 -2
| @@ -588,12 +588,13 @@ func (c *Core) publish(ctx context.Context) error { | |||
| 588 | 588 | if d := c.drag; d != nil && d.target.screen == sc && d.target.kind != dropNone { | |
| 589 | 589 | scene.DropHints = slices.Clone(d.target.hints) | |
| 590 | 590 | } | |
| 591 | - | // Only the focused output lights the focused window's lines. | |
| 592 | - | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), frame, i == c.focusScreen) | |
| 593 | 591 | if sc.mon.ov.open { | |
| 594 | 592 | // Frame the selection and separate numbered and named row groups. | |
| 595 | 593 | _, scene.Separators = sc.mon.overviewRows() | |
| 596 | 594 | scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...) | |
| 595 | + | } else { | |
| 596 | + | // Only the focused output lights the focused window's lines. | |
| 597 | + | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), frame, i == c.focusScreen) | |
| 597 | 598 | } | |
| 598 | 599 | // A window alone on screen needs no pulse to show it has the focus. | |
| 599 | 600 | alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, frame) == 1 | |
| @@ -588,12 +588,13 @@ func (c *Core) publish(ctx context.Context) error { | |||||
| 588 | if d := c.drag; d != nil && d.target.screen == sc && d.target.kind != dropNone { | 588 | if d := c.drag; d != nil && d.target.screen == sc && d.target.kind != dropNone { | ||
| 589 | scene.DropHints = slices.Clone(d.target.hints) | 589 | scene.DropHints = slices.Clone(d.target.hints) | ||
| 590 | } | 590 | } | ||
| 591 | - | // Only the focused output lights the focused window's lines. | |||
| 592 | - | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), frame, i == c.focusScreen) | |||
| 593 | if sc.mon.ov.open { | 591 | if sc.mon.ov.open { | ||
| 594 | // Frame the selection and separate numbered and named row groups. | 592 | // Frame the selection and separate numbered and named row groups. | ||
| 595 | _, scene.Separators = sc.mon.overviewRows() | 593 | _, scene.Separators = sc.mon.overviewRows() | ||
| 596 | scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...) | 594 | scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...) | ||
| 595 | + | } else { | |||
| 596 | + | // Only the focused output lights the focused window's lines. | |||
| 597 | + | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), frame, i == c.focusScreen) | |||
| 597 | } | 598 | } | ||
| 598 | // A window alone on screen needs no pulse to show it has the focus. | 599 | // A window alone on screen needs no pulse to show it has the focus. | ||
| 599 | alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, frame) == 1 | 600 | alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, frame) == 1 | ||
M internal/core/publish_alloc_test.go +75 -23
| @@ -8,31 +8,41 @@ import ( | |||
| 8 | 8 | "github.com/bnema/neferwl/internal/ports" | |
| 9 | 9 | ) | |
| 10 | 10 | ||
| 11 | - | // publishAllocBudget is the allocations of one animation frame (step) on two | |
| 12 | - | // screens of six windows each, with camera and rect motions running. Every | |
| 13 | - | // scene sent is freshly built (immutable once sent), and the layouts are | |
| 14 | - | // rebuilt for every publish, so the floor is not 0. Measured: 43 before | |
| 15 | - | // presizing the scene windows and separators, 36 after. | |
| 16 | - | const publishAllocBudget = 36 | |
| 11 | + | // Allocation budgets of one animation frame (step) on two screens of six | |
| 12 | + | // windows each, with camera and rect motions running, and on one screen with | |
| 13 | + | // the overview open. Every scene sent is freshly built (immutable once sent), | |
| 14 | + | // and the layouts are rebuilt for every publish, so the floor is not 0. | |
| 15 | + | // Measured: 43 before presizing the scene windows and separators, 36 after; | |
| 16 | + | // 70 with the overview open (its rows and card layouts are rebuilt too). | |
| 17 | + | const ( | |
| 18 | + | publishAllocBudget = 36 | |
| 19 | + | publishOverviewAllocBudget = 70 | |
| 20 | + | ) | |
| 17 | 21 | ||
| 18 | - | // TestPublishAllocations pins the cost of publishing an animation frame. | |
| 19 | - | func TestPublishAllocations(t *testing.T) { | |
| 20 | - | // No Clock port: a mock clock would count its own bookkeeping, and the | |
| 21 | - | // fallback timer then is the system one. | |
| 22 | + | // publishRig is a Core on the real clock (a mock clock would count its own | |
| 23 | + | // bookkeeping; the fallback timer is then the system one) with screens | |
| 24 | + | // screens of six windows each. The Slowdown cap keeps every motion running | |
| 25 | + | // for seconds, so none settles during a measure even under -race or load. | |
| 26 | + | func publishRig(t *testing.T, screens int) *Core { | |
| 27 | + | t.Helper() | |
| 22 | 28 | var cfg ports.Config | |
| 23 | 29 | cfg.Keyboard.CmdKey = "super" | |
| 24 | 30 | cfg.Layout.MaxColumns = 2 | |
| 25 | 31 | cfg.Border.Width = 2 | |
| 26 | 32 | cfg.Animations.On = true | |
| 33 | + | cfg.Animations.Slowdown = 10 | |
| 27 | 34 | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64)}) | |
| 28 | 35 | if err != nil { | |
| 29 | 36 | t.Fatal(err) | |
| 30 | 37 | } | |
| 31 | - | c.addScreen(ports.OutputInfo{Name: "A", Width: 300, Height: 200}) | |
| 32 | - | c.addScreen(ports.OutputInfo{Name: "B", Width: 300, Height: 200}) | |
| 33 | - | t0 := time.Now() | |
| 34 | 38 | next := WindowID(1) | |
| 39 | + | for i := range screens { | |
| 40 | + | c.addScreen(ports.OutputInfo{Name: string(rune('A' + i)), Width: 300, Height: 200}) | |
| 41 | + | } | |
| 35 | 42 | for _, sc := range c.screens { | |
| 43 | + | if sc.name() == "" { | |
| 44 | + | continue | |
| 45 | + | } | |
| 36 | 46 | for range 6 { | |
| 37 | 47 | sc.mon.AddWindow(next) | |
| 38 | 48 | next++ | |
| @@ -40,9 +50,33 @@ func TestPublishAllocations(t *testing.T) { | |||
| 40 | 50 | sc.arrange() | |
| 41 | 51 | } | |
| 42 | 52 | c.refreshShown() | |
| 53 | + | return c | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | // publishStep is one animation frame whose scene is taken from the channel. | |
| 57 | + | func publishStep(t *testing.T, c *Core) func() { | |
| 58 | + | ctx := context.Background() | |
| 59 | + | return func() { | |
| 60 | + | if err := c.step(ctx, nil); err != nil { | |
| 61 | + | t.Fatal(err) | |
| 62 | + | } | |
| 63 | + | select { | |
| 64 | + | case <-c.ch.Scenes: | |
| 65 | + | default: | |
| 66 | + | t.Fatal("no scene published") | |
| 67 | + | } | |
| 68 | + | } | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | // TestPublishAllocations pins the cost of publishing an animation frame. | |
| 72 | + | func TestPublishAllocations(t *testing.T) { | |
| 73 | + | c := publishRig(t, 2) | |
| 74 | + | t0 := time.Now() | |
| 43 | 75 | for _, sc := range c.screens { | |
| 44 | - | m := sc.mon | |
| 45 | - | ws := m.Current() | |
| 76 | + | if sc.name() == "" { | |
| 77 | + | continue | |
| 78 | + | } | |
| 79 | + | ws := sc.mon.Current() | |
| 46 | 80 | ws.motion = c.spring(viewSpring(100, 0), t0) | |
| 47 | 81 | ws.shift = 100 | |
| 48 | 82 | sc.rects = make(map[WindowID]rectMotion, 6) | |
| @@ -56,15 +90,12 @@ func TestPublishAllocations(t *testing.T) { | |||
| 56 | 90 | } | |
| 57 | 91 | sc.rectsWS = ws | |
| 58 | 92 | } | |
| 59 | - | ctx := context.Background() | |
| 60 | - | step := func() { | |
| 61 | - | if err := c.step(ctx, nil); err != nil { | |
| 62 | - | t.Fatal(err) | |
| 63 | - | } | |
| 64 | - | <-c.ch.Scenes | |
| 65 | - | } | |
| 93 | + | step := publishStep(t, c) | |
| 66 | 94 | step() | |
| 67 | 95 | for _, sc := range c.screens { | |
| 96 | + | if sc.name() == "" { | |
| 97 | + | continue | |
| 98 | + | } | |
| 68 | 99 | if len(sc.rects) != 6 || !sc.mon.Current().motion.on || len(sc.shown) < 6 { | |
| 69 | 100 | t.Fatalf("setup: rects %d camera %v shown %d", len(sc.rects), sc.mon.Current().motion.on, len(sc.shown)) | |
| 70 | 101 | } | |
| @@ -72,7 +103,10 @@ func TestPublishAllocations(t *testing.T) { | |||
| 72 | 103 | n := testing.AllocsPerRun(50, step) | |
| 73 | 104 | t.Logf("allocs per step = %v", n) | |
| 74 | 105 | for _, sc := range c.screens { | |
| 75 | - | if len(sc.rects) == 0 || !sc.mon.Current().motion.on { | |
| 106 | + | if sc.name() == "" { | |
| 107 | + | continue | |
| 108 | + | } | |
| 109 | + | if len(sc.rects) != 6 || !sc.mon.Current().motion.on { | |
| 76 | 110 | t.Fatalf("motions settled during the measure: rects %d", len(sc.rects)) | |
| 77 | 111 | } | |
| 78 | 112 | } | |
| @@ -80,3 +114,21 @@ func TestPublishAllocations(t *testing.T) { | |||
| 80 | 114 | t.Errorf("publish allocs per frame = %v, budget %d", n, publishAllocBudget) | |
| 81 | 115 | } | |
| 82 | 116 | } | |
| 117 | + | ||
| 118 | + | // TestPublishOverviewAllocations pins the cost of publishing with the | |
| 119 | + | // overview open, which guards the reuse of Core.overviewReal. | |
| 120 | + | func TestPublishOverviewAllocations(t *testing.T) { | |
| 121 | + | c := publishRig(t, 1) | |
| 122 | + | sc := c.cur() | |
| 123 | + | sc.mon.ToggleOverview() | |
| 124 | + | step := publishStep(t, c) | |
| 125 | + | step() | |
| 126 | + | if !sc.mon.ov.open || len(c.overviewReal) != 6 { | |
| 127 | + | t.Fatalf("setup: overview %v, real layouts %d", sc.mon.ov.open, len(c.overviewReal)) | |
| 128 | + | } | |
| 129 | + | n := testing.AllocsPerRun(50, step) | |
| 130 | + | t.Logf("overview allocs per step = %v", n) | |
| 131 | + | if n > publishOverviewAllocBudget { | |
| 132 | + | t.Errorf("overview publish allocs per frame = %v, budget %d", n, publishOverviewAllocBudget) | |
| 133 | + | } | |
| 134 | + | } | |
| @@ -8,31 +8,41 @@ import ( | |||||
| 8 | "github.com/bnema/neferwl/internal/ports" | 8 | "github.com/bnema/neferwl/internal/ports" | ||
| 9 | ) | 9 | ) | ||
| 10 | 10 | ||||
| 11 | - | // publishAllocBudget is the allocations of one animation frame (step) on two | 11 | + | // Allocation budgets of one animation frame (step) on two screens of six |
| 12 | - | // screens of six windows each, with camera and rect motions running. Every | 12 | + | // windows each, with camera and rect motions running, and on one screen with |
| 13 | - | // scene sent is freshly built (immutable once sent), and the layouts are | 13 | + | // the overview open. Every scene sent is freshly built (immutable once sent), |
| 14 | - | // rebuilt for every publish, so the floor is not 0. Measured: 43 before | 14 | + | // and the layouts are rebuilt for every publish, so the floor is not 0. |
| 15 | - | // presizing the scene windows and separators, 36 after. | 15 | + | // Measured: 43 before presizing the scene windows and separators, 36 after; |
| 16 | - | const publishAllocBudget = 36 | 16 | + | // 70 with the overview open (its rows and card layouts are rebuilt too). |
| 17 | + | const ( | |||
| 18 | + | publishAllocBudget = 36 | |||
| 19 | + | publishOverviewAllocBudget = 70 | |||
| 20 | + | ) | |||
| 17 | 21 | ||||
| 18 | - | // TestPublishAllocations pins the cost of publishing an animation frame. | 22 | + | // publishRig is a Core on the real clock (a mock clock would count its own |
| 19 | - | func TestPublishAllocations(t *testing.T) { | 23 | + | // bookkeeping; the fallback timer is then the system one) with screens |
| 20 | - | // No Clock port: a mock clock would count its own bookkeeping, and the | 24 | + | // screens of six windows each. The Slowdown cap keeps every motion running |
| 21 | - | // fallback timer then is the system one. | 25 | + | // for seconds, so none settles during a measure even under -race or load. |
| 26 | + | func publishRig(t *testing.T, screens int) *Core { | |||
| 27 | + | t.Helper() | |||
| 22 | var cfg ports.Config | 28 | var cfg ports.Config | ||
| 23 | cfg.Keyboard.CmdKey = "super" | 29 | cfg.Keyboard.CmdKey = "super" | ||
| 24 | cfg.Layout.MaxColumns = 2 | 30 | cfg.Layout.MaxColumns = 2 | ||
| 25 | cfg.Border.Width = 2 | 31 | cfg.Border.Width = 2 | ||
| 26 | cfg.Animations.On = true | 32 | cfg.Animations.On = true | ||
| 33 | + | cfg.Animations.Slowdown = 10 | |||
| 27 | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64)}) | 34 | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64)}) | ||
| 28 | if err != nil { | 35 | if err != nil { | ||
| 29 | t.Fatal(err) | 36 | t.Fatal(err) | ||
| 30 | } | 37 | } | ||
| 31 | - | c.addScreen(ports.OutputInfo{Name: "A", Width: 300, Height: 200}) | |||
| 32 | - | c.addScreen(ports.OutputInfo{Name: "B", Width: 300, Height: 200}) | |||
| 33 | - | t0 := time.Now() | |||
| 34 | next := WindowID(1) | 38 | next := WindowID(1) | ||
| 39 | + | for i := range screens { | |||
| 40 | + | c.addScreen(ports.OutputInfo{Name: string(rune('A' + i)), Width: 300, Height: 200}) | |||
| 41 | + | } | |||
| 35 | for _, sc := range c.screens { | 42 | for _, sc := range c.screens { | ||
| 43 | + | if sc.name() == "" { | |||
| 44 | + | continue | |||
| 45 | + | } | |||
| 36 | for range 6 { | 46 | for range 6 { | ||
| 37 | sc.mon.AddWindow(next) | 47 | sc.mon.AddWindow(next) | ||
| 38 | next++ | 48 | next++ | ||
| @@ -40,9 +50,33 @@ func TestPublishAllocations(t *testing.T) { | |||||
| 40 | sc.arrange() | 50 | sc.arrange() | ||
| 41 | } | 51 | } | ||
| 42 | c.refreshShown() | 52 | c.refreshShown() | ||
| 53 | + | return c | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | // publishStep is one animation frame whose scene is taken from the channel. | |||
| 57 | + | func publishStep(t *testing.T, c *Core) func() { | |||
| 58 | + | ctx := context.Background() | |||
| 59 | + | return func() { | |||
| 60 | + | if err := c.step(ctx, nil); err != nil { | |||
| 61 | + | t.Fatal(err) | |||
| 62 | + | } | |||
| 63 | + | select { | |||
| 64 | + | case <-c.ch.Scenes: | |||
| 65 | + | default: | |||
| 66 | + | t.Fatal("no scene published") | |||
| 67 | + | } | |||
| 68 | + | } | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | // TestPublishAllocations pins the cost of publishing an animation frame. | |||
| 72 | + | func TestPublishAllocations(t *testing.T) { | |||
| 73 | + | c := publishRig(t, 2) | |||
| 74 | + | t0 := time.Now() | |||
| 43 | for _, sc := range c.screens { | 75 | for _, sc := range c.screens { | ||
| 44 | - | m := sc.mon | 76 | + | if sc.name() == "" { |
| 45 | - | ws := m.Current() | 77 | + | continue |
| 78 | + | } | |||
| 79 | + | ws := sc.mon.Current() | |||
| 46 | ws.motion = c.spring(viewSpring(100, 0), t0) | 80 | ws.motion = c.spring(viewSpring(100, 0), t0) | ||
| 47 | ws.shift = 100 | 81 | ws.shift = 100 | ||
| 48 | sc.rects = make(map[WindowID]rectMotion, 6) | 82 | sc.rects = make(map[WindowID]rectMotion, 6) | ||
| @@ -56,15 +90,12 @@ func TestPublishAllocations(t *testing.T) { | |||||
| 56 | } | 90 | } | ||
| 57 | sc.rectsWS = ws | 91 | sc.rectsWS = ws | ||
| 58 | } | 92 | } | ||
| 59 | - | ctx := context.Background() | 93 | + | step := publishStep(t, c) |
| 60 | - | step := func() { | |||
| 61 | - | if err := c.step(ctx, nil); err != nil { | |||
| 62 | - | t.Fatal(err) | |||
| 63 | - | } | |||
| 64 | - | <-c.ch.Scenes | |||
| 65 | - | } | |||
| 66 | step() | 94 | step() | ||
| 67 | for _, sc := range c.screens { | 95 | for _, sc := range c.screens { | ||
| 96 | + | if sc.name() == "" { | |||
| 97 | + | continue | |||
| 98 | + | } | |||
| 68 | if len(sc.rects) != 6 || !sc.mon.Current().motion.on || len(sc.shown) < 6 { | 99 | if len(sc.rects) != 6 || !sc.mon.Current().motion.on || len(sc.shown) < 6 { | ||
| 69 | t.Fatalf("setup: rects %d camera %v shown %d", len(sc.rects), sc.mon.Current().motion.on, len(sc.shown)) | 100 | t.Fatalf("setup: rects %d camera %v shown %d", len(sc.rects), sc.mon.Current().motion.on, len(sc.shown)) | ||
| 70 | } | 101 | } | ||
| @@ -72,7 +103,10 @@ func TestPublishAllocations(t *testing.T) { | |||||
| 72 | n := testing.AllocsPerRun(50, step) | 103 | n := testing.AllocsPerRun(50, step) | ||
| 73 | t.Logf("allocs per step = %v", n) | 104 | t.Logf("allocs per step = %v", n) | ||
| 74 | for _, sc := range c.screens { | 105 | for _, sc := range c.screens { | ||
| 75 | - | if len(sc.rects) == 0 || !sc.mon.Current().motion.on { | 106 | + | if sc.name() == "" { |
| 107 | + | continue | |||
| 108 | + | } | |||
| 109 | + | if len(sc.rects) != 6 || !sc.mon.Current().motion.on { | |||
| 76 | t.Fatalf("motions settled during the measure: rects %d", len(sc.rects)) | 110 | t.Fatalf("motions settled during the measure: rects %d", len(sc.rects)) | ||
| 77 | } | 111 | } | ||
| 78 | } | 112 | } | ||
| @@ -80,3 +114,21 @@ func TestPublishAllocations(t *testing.T) { | |||||
| 80 | t.Errorf("publish allocs per frame = %v, budget %d", n, publishAllocBudget) | 114 | t.Errorf("publish allocs per frame = %v, budget %d", n, publishAllocBudget) | ||
| 81 | } | 115 | } | ||
| 82 | } | 116 | } | ||
| 117 | + | ||||
| 118 | + | // TestPublishOverviewAllocations pins the cost of publishing with the | |||
| 119 | + | // overview open, which guards the reuse of Core.overviewReal. | |||
| 120 | + | func TestPublishOverviewAllocations(t *testing.T) { | |||
| 121 | + | c := publishRig(t, 1) | |||
| 122 | + | sc := c.cur() | |||
| 123 | + | sc.mon.ToggleOverview() | |||
| 124 | + | step := publishStep(t, c) | |||
| 125 | + | step() | |||
| 126 | + | if !sc.mon.ov.open || len(c.overviewReal) != 6 { | |||
| 127 | + | t.Fatalf("setup: overview %v, real layouts %d", sc.mon.ov.open, len(c.overviewReal)) | |||
| 128 | + | } | |||
| 129 | + | n := testing.AllocsPerRun(50, step) | |||
| 130 | + | t.Logf("overview allocs per step = %v", n) | |||
| 131 | + | if n > publishOverviewAllocBudget { | |||
| 132 | + | t.Errorf("overview publish allocs per frame = %v, budget %d", n, publishOverviewAllocBudget) | |||
| 133 | + | } | |||
| 134 | + | } | |||
M internal/ports/damage_test.go +20 -0
| @@ -35,3 +35,23 @@ func TestDamageSince(t *testing.T) { | |||
| 35 | 35 | t.Fatal("stale history used") | |
| 36 | 36 | } | |
| 37 | 37 | } | |
| 38 | + | ||
| 39 | + | func TestAppendDamageSince(t *testing.T) { | |
| 40 | + | a, b := Rect{X: 1, W: 2, H: 2}, Rect{Y: 5, W: 1, H: 1} | |
| 41 | + | c := SurfaceContent{Seq: 7, DamageHistory: []SeqDamage{{Seq: 6, Rects: []Rect{a}}, {Seq: 7, Rects: []Rect{b}}}} | |
| 42 | + | keep := []Rect{{W: 9}} | |
| 43 | + | if got, ok := c.AppendDamageSince(keep, 5); !ok || !slices.Equal(got, []Rect{{W: 9}, a, b}) { | |
| 44 | + | t.Fatalf("append %v %v", got, ok) | |
| 45 | + | } | |
| 46 | + | // A refused history leaves dst as it was. | |
| 47 | + | c.DamageHistory[1].Full = true | |
| 48 | + | if got, ok := c.AppendDamageSince(keep, 5); ok || !slices.Equal(got, keep) { | |
| 49 | + | t.Fatalf("full change: %v %v", got, ok) | |
| 50 | + | } | |
| 51 | + | // Reusing the slice allocates nothing. | |
| 52 | + | c.DamageHistory[1].Full = false | |
| 53 | + | buf := make([]Rect, 0, 4) | |
| 54 | + | if n := testing.AllocsPerRun(20, func() { buf, _ = c.AppendDamageSince(buf[:0], 5) }); n != 0 { | |
| 55 | + | t.Fatalf("allocs = %v", n) | |
| 56 | + | } | |
| 57 | + | } | |
| @@ -35,3 +35,23 @@ func TestDamageSince(t *testing.T) { | |||||
| 35 | t.Fatal("stale history used") | 35 | t.Fatal("stale history used") | ||
| 36 | } | 36 | } | ||
| 37 | } | 37 | } | ||
| 38 | + | ||||
| 39 | + | func TestAppendDamageSince(t *testing.T) { | |||
| 40 | + | a, b := Rect{X: 1, W: 2, H: 2}, Rect{Y: 5, W: 1, H: 1} | |||
| 41 | + | c := SurfaceContent{Seq: 7, DamageHistory: []SeqDamage{{Seq: 6, Rects: []Rect{a}}, {Seq: 7, Rects: []Rect{b}}}} | |||
| 42 | + | keep := []Rect{{W: 9}} | |||
| 43 | + | if got, ok := c.AppendDamageSince(keep, 5); !ok || !slices.Equal(got, []Rect{{W: 9}, a, b}) { | |||
| 44 | + | t.Fatalf("append %v %v", got, ok) | |||
| 45 | + | } | |||
| 46 | + | // A refused history leaves dst as it was. | |||
| 47 | + | c.DamageHistory[1].Full = true | |||
| 48 | + | if got, ok := c.AppendDamageSince(keep, 5); ok || !slices.Equal(got, keep) { | |||
| 49 | + | t.Fatalf("full change: %v %v", got, ok) | |||
| 50 | + | } | |||
| 51 | + | // Reusing the slice allocates nothing. | |||
| 52 | + | c.DamageHistory[1].Full = false | |||
| 53 | + | buf := make([]Rect, 0, 4) | |||
| 54 | + | if n := testing.AllocsPerRun(20, func() { buf, _ = c.AppendDamageSince(buf[:0], 5) }); n != 0 { | |||
| 55 | + | t.Fatalf("allocs = %v", n) | |||
| 56 | + | } | |||
| 57 | + | } | |||
M internal/ports/scene.go +17 -6
| @@ -302,24 +302,35 @@ type SeqDamage struct { | |||
| 302 | 302 | // DamageSince is the union of changes after content seq up to this one, | |
| 303 | 303 | // and false when the history does not reach back to seq (redraw all). | |
| 304 | 304 | func (c SurfaceContent) DamageSince(seq uint64) ([]Rect, bool) { | |
| 305 | + | out, ok := c.AppendDamageSince(nil, seq) | |
| 306 | + | if !ok { | |
| 307 | + | return nil, false | |
| 308 | + | } | |
| 309 | + | return out, true | |
| 310 | + | } | |
| 311 | + | ||
| 312 | + | // AppendDamageSince is DamageSince appending to dst, so a caller reusing | |
| 313 | + | // its slice allocates nothing. When the history does not reach back to seq | |
| 314 | + | // it returns dst unchanged and false. | |
| 315 | + | func (c SurfaceContent) AppendDamageSince(dst []Rect, seq uint64) ([]Rect, bool) { | |
| 305 | 316 | if seq >= c.Seq { | |
| 306 | - | return nil, true | |
| 317 | + | return dst, true | |
| 307 | 318 | } | |
| 308 | 319 | h := c.DamageHistory | |
| 309 | 320 | if len(h) == 0 || h[0].Seq > seq+1 || h[len(h)-1].Seq != c.Seq { | |
| 310 | - | return nil, false | |
| 321 | + | return dst, false | |
| 311 | 322 | } | |
| 312 | - | var out []Rect | |
| 323 | + | n := len(dst) | |
| 313 | 324 | for _, d := range h { | |
| 314 | 325 | if d.Seq <= seq { | |
| 315 | 326 | continue | |
| 316 | 327 | } | |
| 317 | 328 | if d.Full { | |
| 318 | - | return nil, false | |
| 329 | + | return dst[:n], false | |
| 319 | 330 | } | |
| 320 | - | out = append(out, d.Rects...) | |
| 331 | + | dst = append(dst, d.Rects...) | |
| 321 | 332 | } | |
| 322 | - | return out, true | |
| 333 | + | return dst, true | |
| 323 | 334 | } | |
| 324 | 335 | ||
| 325 | 336 | // Subsurface is a child surface at X, Y logical pixels from the root | |
| @@ -302,24 +302,35 @@ type SeqDamage struct { | |||||
| 302 | // DamageSince is the union of changes after content seq up to this one, | 302 | // DamageSince is the union of changes after content seq up to this one, | ||
| 303 | // and false when the history does not reach back to seq (redraw all). | 303 | // and false when the history does not reach back to seq (redraw all). | ||
| 304 | func (c SurfaceContent) DamageSince(seq uint64) ([]Rect, bool) { | 304 | func (c SurfaceContent) DamageSince(seq uint64) ([]Rect, bool) { | ||
| 305 | + | out, ok := c.AppendDamageSince(nil, seq) | |||
| 306 | + | if !ok { | |||
| 307 | + | return nil, false | |||
| 308 | + | } | |||
| 309 | + | return out, true | |||
| 310 | + | } | |||
| 311 | + | ||||
| 312 | + | // AppendDamageSince is DamageSince appending to dst, so a caller reusing | |||
| 313 | + | // its slice allocates nothing. When the history does not reach back to seq | |||
| 314 | + | // it returns dst unchanged and false. | |||
| 315 | + | func (c SurfaceContent) AppendDamageSince(dst []Rect, seq uint64) ([]Rect, bool) { | |||
| 305 | if seq >= c.Seq { | 316 | if seq >= c.Seq { | ||
| 306 | - | return nil, true | 317 | + | return dst, true |
| 307 | } | 318 | } | ||
| 308 | h := c.DamageHistory | 319 | h := c.DamageHistory | ||
| 309 | if len(h) == 0 || h[0].Seq > seq+1 || h[len(h)-1].Seq != c.Seq { | 320 | if len(h) == 0 || h[0].Seq > seq+1 || h[len(h)-1].Seq != c.Seq { | ||
| 310 | - | return nil, false | 321 | + | return dst, false |
| 311 | } | 322 | } | ||
| 312 | - | var out []Rect | 323 | + | n := len(dst) |
| 313 | for _, d := range h { | 324 | for _, d := range h { | ||
| 314 | if d.Seq <= seq { | 325 | if d.Seq <= seq { | ||
| 315 | continue | 326 | continue | ||
| 316 | } | 327 | } | ||
| 317 | if d.Full { | 328 | if d.Full { | ||
| 318 | - | return nil, false | 329 | + | return dst[:n], false |
| 319 | } | 330 | } | ||
| 320 | - | out = append(out, d.Rects...) | 331 | + | dst = append(dst, d.Rects...) |
| 321 | } | 332 | } | ||
| 322 | - | return out, true | 333 | + | return dst, true |
| 323 | } | 334 | } | ||
| 324 | 335 | ||||
| 325 | // Subsurface is a child surface at X, Y logical pixels from the root | 336 | // Subsurface is a child surface at X, Y logical pixels from the root | ||