perf(headless): reuse the shows map between flips
- commit
- 4063d10ea00e15bdb5ba658233b3d14a88c0c8f2
- author
- brice · · 2026-10-13 18:10 UTC
- parent
- bc00385
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +65 -13
M Makefile +1 -1
| @@ -69,7 +69,7 @@ adapter-imports: | |||
| 69 | 69 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | |
| 70 | 70 | # Fast, mandatory guards for tiled content publications and SHM copies. | |
| 71 | 71 | perf-check: | |
| 72 | - | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations)$$' -count=1 | |
| 72 | + | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations)$$' -count=1 | |
| 73 | 73 | check: vet test arch fakes-check log-check perf-check | |
| 74 | 74 | ||
| 75 | 75 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | |
| @@ -69,7 +69,7 @@ adapter-imports: | |||||
| 69 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | 69 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | ||
| 70 | # Fast, mandatory guards for tiled content publications and SHM copies. | 70 | # Fast, mandatory guards for tiled content publications and SHM copies. | ||
| 71 | perf-check: | 71 | perf-check: | ||
| 72 | - | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations)$$' -count=1 | 72 | + | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations)$$' -count=1 |
| 73 | check: vet test arch fakes-check log-check perf-check | 73 | check: vet test arch fakes-check log-check perf-check | ||
| 74 | 74 | ||||
| 75 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | 75 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | ||
A internal/adapters/headless/flip_alloc_test.go +28 -0
| @@ -0,0 +1,28 @@ | |||
| 1 | + | package headless | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |
| 7 | + | "github.com/stretchr/testify/require" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // Steady-state flip reporting: the Shows snapshot is reused while it does not | |
| 11 | + | // change, and a changed one never mutates what an earlier flip handed out. | |
| 12 | + | func TestFlipShowsAllocations(t *testing.T) { | |
| 13 | + | var f flipShows | |
| 14 | + | scene := ports.Scene{OutputWidth: 10, OutputHeight: 10, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}} | |
| 15 | + | contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Seq: 1}, 2: {ID: 2, Seq: 2}} | |
| 16 | + | first := f.flipInfo(scene, contents, nil) | |
| 17 | + | require.Equal(t, map[ports.WindowID]uint64{1: 1}, first.Shows) | |
| 18 | + | ||
| 19 | + | require.Zero(t, testing.AllocsPerRun(100, func() { f.of(scene, contents, nil) }), "unchanged shows") | |
| 20 | + | // The FlipInfo is the one allocation left: the sink's merge writes into it. | |
| 21 | + | require.LessOrEqual(t, testing.AllocsPerRun(100, func() { f.flipInfo(scene, contents, nil) }), 1.0, "flip report") | |
| 22 | + | ||
| 23 | + | contents[1] = ports.SurfaceContent{ID: 1, Seq: 3} | |
| 24 | + | second := f.flipInfo(scene, contents, nil) | |
| 25 | + | require.Equal(t, map[ports.WindowID]uint64{1: 1}, first.Shows, "an earlier snapshot is immutable") | |
| 26 | + | require.Equal(t, map[ports.WindowID]uint64{1: 3}, second.Shows) | |
| 27 | + | require.NotSame(t, first, second) | |
| 28 | + | } | |
| @@ -0,0 +1,28 @@ | |||||
| 1 | + | package headless | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 7 | + | "github.com/stretchr/testify/require" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // Steady-state flip reporting: the Shows snapshot is reused while it does not | |||
| 11 | + | // change, and a changed one never mutates what an earlier flip handed out. | |||
| 12 | + | func TestFlipShowsAllocations(t *testing.T) { | |||
| 13 | + | var f flipShows | |||
| 14 | + | scene := ports.Scene{OutputWidth: 10, OutputHeight: 10, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}} | |||
| 15 | + | contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Seq: 1}, 2: {ID: 2, Seq: 2}} | |||
| 16 | + | first := f.flipInfo(scene, contents, nil) | |||
| 17 | + | require.Equal(t, map[ports.WindowID]uint64{1: 1}, first.Shows) | |||
| 18 | + | ||||
| 19 | + | require.Zero(t, testing.AllocsPerRun(100, func() { f.of(scene, contents, nil) }), "unchanged shows") | |||
| 20 | + | // The FlipInfo is the one allocation left: the sink's merge writes into it. | |||
| 21 | + | require.LessOrEqual(t, testing.AllocsPerRun(100, func() { f.flipInfo(scene, contents, nil) }), 1.0, "flip report") | |||
| 22 | + | ||||
| 23 | + | contents[1] = ports.SurfaceContent{ID: 1, Seq: 3} | |||
| 24 | + | second := f.flipInfo(scene, contents, nil) | |||
| 25 | + | require.Equal(t, map[ports.WindowID]uint64{1: 1}, first.Shows, "an earlier snapshot is immutable") | |||
| 26 | + | require.Equal(t, map[ports.WindowID]uint64{1: 3}, second.Shows) | |||
| 27 | + | require.NotSame(t, first, second) | |||
| 28 | + | } | |||
M internal/adapters/headless/leaving_test.go +3 -2
| @@ -10,15 +10,16 @@ import ( | |||
| 10 | 10 | // A window drawn from a content its client withdrew (closed, fading out) is | |
| 11 | 11 | // not reported shown: the empty content has no frame to present. | |
| 12 | 12 | func TestFlipInfoSkipsKeptContent(t *testing.T) { | |
| 13 | + | var shows flipShows | |
| 13 | 14 | table := surfaces.New() | |
| 14 | 15 | s := ports.Scene{OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}, {ID: 2, Rect: ports.Rect{W: 1, H: 1}}}} | |
| 15 | 16 | table.Update(ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{Pool: 1}}, s) | |
| 16 | 17 | table.Update(ports.SurfaceContent{ID: 2, Seq: 1, SHM: &ports.SHMBuffer{Pool: 2}}, s) | |
| 17 | - | if f := flipInfo(s, table.Map(), table.Kept); f.Shows[1] != 3 || f.Shows[2] != 1 { | |
| 18 | + | if f := shows.flipInfo(s, table.Map(), table.Kept); f.Shows[1] != 3 || f.Shows[2] != 1 { | |
| 18 | 19 | t.Fatalf("control: %v", f.Shows) | |
| 19 | 20 | } | |
| 20 | 21 | table.Update(ports.SurfaceContent{ID: 1, Seq: 4}, s) | |
| 21 | - | f := flipInfo(s, table.Map(), table.Kept) | |
| 22 | + | f := shows.flipInfo(s, table.Map(), table.Kept) | |
| 22 | 23 | if _, ok := f.Shows[1]; ok || f.Shows[2] != 1 { | |
| 23 | 24 | t.Fatalf("a leaving window is reported shown: %v", f.Shows) | |
| 24 | 25 | } | |
| @@ -10,15 +10,16 @@ import ( | |||||
| 10 | // A window drawn from a content its client withdrew (closed, fading out) is | 10 | // A window drawn from a content its client withdrew (closed, fading out) is | ||
| 11 | // not reported shown: the empty content has no frame to present. | 11 | // not reported shown: the empty content has no frame to present. | ||
| 12 | func TestFlipInfoSkipsKeptContent(t *testing.T) { | 12 | func TestFlipInfoSkipsKeptContent(t *testing.T) { | ||
| 13 | + | var shows flipShows | |||
| 13 | table := surfaces.New() | 14 | table := surfaces.New() | ||
| 14 | s := ports.Scene{OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}, {ID: 2, Rect: ports.Rect{W: 1, H: 1}}}} | 15 | s := ports.Scene{OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}, {ID: 2, Rect: ports.Rect{W: 1, H: 1}}}} | ||
| 15 | table.Update(ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{Pool: 1}}, s) | 16 | table.Update(ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{Pool: 1}}, s) | ||
| 16 | table.Update(ports.SurfaceContent{ID: 2, Seq: 1, SHM: &ports.SHMBuffer{Pool: 2}}, s) | 17 | table.Update(ports.SurfaceContent{ID: 2, Seq: 1, SHM: &ports.SHMBuffer{Pool: 2}}, s) | ||
| 17 | - | if f := flipInfo(s, table.Map(), table.Kept); f.Shows[1] != 3 || f.Shows[2] != 1 { | 18 | + | if f := shows.flipInfo(s, table.Map(), table.Kept); f.Shows[1] != 3 || f.Shows[2] != 1 { |
| 18 | t.Fatalf("control: %v", f.Shows) | 19 | t.Fatalf("control: %v", f.Shows) | ||
| 19 | } | 20 | } | ||
| 20 | table.Update(ports.SurfaceContent{ID: 1, Seq: 4}, s) | 21 | table.Update(ports.SurfaceContent{ID: 1, Seq: 4}, s) | ||
| 21 | - | f := flipInfo(s, table.Map(), table.Kept) | 22 | + | f := shows.flipInfo(s, table.Map(), table.Kept) |
| 22 | if _, ok := f.Shows[1]; ok || f.Shows[2] != 1 { | 23 | if _, ok := f.Shows[1]; ok || f.Shows[2] != 1 { | ||
| 23 | t.Fatalf("a leaving window is reported shown: %v", f.Shows) | 24 | t.Fatalf("a leaving window is reported shown: %v", f.Shows) | ||
| 24 | } | 25 | } | ||
M internal/adapters/headless/output.go +33 -10
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "fmt" | |
| 6 | 6 | "image" | |
| 7 | 7 | "image/png" | |
| 8 | + | "maps" | |
| 8 | 9 | "os" | |
| 9 | 10 | "path/filepath" | |
| 10 | 11 | "time" | |
| @@ -117,6 +118,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 117 | 118 | // reports queues what the output read; every report is pushed to the | |
| 118 | 119 | // sink at once (Push never blocks), so it holds one at most. | |
| 119 | 120 | var reports presented.Queue | |
| 121 | + | var shows flipShows | |
| 120 | 122 | var security ports.SecurityState | |
| 121 | 123 | securityKnown := false | |
| 122 | 124 | failRequests := func(reason error) { | |
| @@ -541,7 +543,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 541 | 543 | pipeline.EndGate(nil) | |
| 542 | 544 | requests.List = capture.Waiting(requests.List, scene) | |
| 543 | 545 | frame++ | |
| 544 | - | opts.report(&reports, flipInfo(scene, drawn, table.Kept), seen) | |
| 546 | + | opts.report(&reports, shows.flipInfo(scene, drawn, table.Kept), seen) | |
| 545 | 547 | if scene.Animating && opts.Frames != nil { | |
| 546 | 548 | select { | |
| 547 | 549 | case opts.Frames <- ports.OutputFrame{Output: opts.Name}: | |
| @@ -627,19 +629,40 @@ func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[po | |||
| 627 | 629 | q.Flush(opts.Presented) | |
| 628 | 630 | } | |
| 629 | 631 | ||
| 630 | - | // flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC | |
| 631 | - | // time (software clock, refresh unknown), so presentation feedback and | |
| 632 | - | // frame callbacks follow headless frames. | |
| 633 | - | func flipInfo(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) *ports.FlipInfo { | |
| 634 | - | var ts unix.Timespec | |
| 635 | - | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | |
| 636 | - | shows := map[ports.WindowID]uint64{} | |
| 632 | + | // flipShows builds the Shows of successive flips. Its owner goroutine is the | |
| 633 | + | // only user. A flip's Shows map is an immutable snapshot: wayland may still | |
| 634 | + | // read the one of an earlier flip on another goroutine, so the scratch map is | |
| 635 | + | // never sent and a snapshot is replaced by a clone only when it changed. | |
| 636 | + | type flipShows struct { | |
| 637 | + | scratch, snapshot map[ports.WindowID]uint64 | |
| 638 | + | } | |
| 639 | + | ||
| 640 | + | // of returns the content Seq of the windows scene showed to their client. The | |
| 641 | + | // result is shared with earlier flips of the same content: never modify it. | |
| 642 | + | func (f *flipShows) of(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) map[ports.WindowID]uint64 { | |
| 643 | + | if f.scratch == nil { | |
| 644 | + | f.scratch = make(map[ports.WindowID]uint64) | |
| 645 | + | } | |
| 646 | + | clear(f.scratch) | |
| 637 | 647 | for id, c := range contents { | |
| 638 | 648 | // A closed window drawn from its kept content is not shown to | |
| 639 | 649 | // its client. | |
| 640 | 650 | if surfaces.Shown(scene, id, kept) { | |
| 641 | - | shows[id] = c.Seq | |
| 651 | + | f.scratch[id] = c.Seq | |
| 642 | 652 | } | |
| 643 | 653 | } | |
| 644 | - | return &ports.FlipInfo{When: time.Duration(ts.Nano()), Shows: shows} | |
| 654 | + | if f.snapshot == nil || !maps.Equal(f.snapshot, f.scratch) { | |
| 655 | + | f.snapshot = maps.Clone(f.scratch) | |
| 656 | + | } | |
| 657 | + | return f.snapshot | |
| 658 | + | } | |
| 659 | + | ||
| 660 | + | // flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC | |
| 661 | + | // time (software clock, refresh unknown), so presentation feedback and | |
| 662 | + | // frame callbacks follow headless frames. The FlipInfo itself is new per | |
| 663 | + | // flip (the sink counts merges on it); only its Shows is shared. | |
| 664 | + | func (f *flipShows) flipInfo(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) *ports.FlipInfo { | |
| 665 | + | var ts unix.Timespec | |
| 666 | + | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | |
| 667 | + | return &ports.FlipInfo{When: time.Duration(ts.Nano()), Shows: f.of(scene, contents, kept)} | |
| 645 | 668 | } | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "image" | 6 | "image" | ||
| 7 | "image/png" | 7 | "image/png" | ||
| 8 | + | "maps" | |||
| 8 | "os" | 9 | "os" | ||
| 9 | "path/filepath" | 10 | "path/filepath" | ||
| 10 | "time" | 11 | "time" | ||
| @@ -117,6 +118,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 117 | // reports queues what the output read; every report is pushed to the | 118 | // reports queues what the output read; every report is pushed to the | ||
| 118 | // sink at once (Push never blocks), so it holds one at most. | 119 | // sink at once (Push never blocks), so it holds one at most. | ||
| 119 | var reports presented.Queue | 120 | var reports presented.Queue | ||
| 121 | + | var shows flipShows | |||
| 120 | var security ports.SecurityState | 122 | var security ports.SecurityState | ||
| 121 | securityKnown := false | 123 | securityKnown := false | ||
| 122 | failRequests := func(reason error) { | 124 | failRequests := func(reason error) { | ||
| @@ -541,7 +543,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 541 | pipeline.EndGate(nil) | 543 | pipeline.EndGate(nil) | ||
| 542 | requests.List = capture.Waiting(requests.List, scene) | 544 | requests.List = capture.Waiting(requests.List, scene) | ||
| 543 | frame++ | 545 | frame++ | ||
| 544 | - | opts.report(&reports, flipInfo(scene, drawn, table.Kept), seen) | 546 | + | opts.report(&reports, shows.flipInfo(scene, drawn, table.Kept), seen) |
| 545 | if scene.Animating && opts.Frames != nil { | 547 | if scene.Animating && opts.Frames != nil { | ||
| 546 | select { | 548 | select { | ||
| 547 | case opts.Frames <- ports.OutputFrame{Output: opts.Name}: | 549 | case opts.Frames <- ports.OutputFrame{Output: opts.Name}: | ||
| @@ -627,19 +629,40 @@ func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[po | |||||
| 627 | q.Flush(opts.Presented) | 629 | q.Flush(opts.Presented) | ||
| 628 | } | 630 | } | ||
| 629 | 631 | ||||
| 630 | - | // flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC | 632 | + | // flipShows builds the Shows of successive flips. Its owner goroutine is the |
| 631 | - | // time (software clock, refresh unknown), so presentation feedback and | 633 | + | // only user. A flip's Shows map is an immutable snapshot: wayland may still |
| 632 | - | // frame callbacks follow headless frames. | 634 | + | // read the one of an earlier flip on another goroutine, so the scratch map is |
| 633 | - | func flipInfo(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) *ports.FlipInfo { | 635 | + | // never sent and a snapshot is replaced by a clone only when it changed. |
| 634 | - | var ts unix.Timespec | 636 | + | type flipShows struct { |
| 635 | - | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | 637 | + | scratch, snapshot map[ports.WindowID]uint64 |
| 636 | - | shows := map[ports.WindowID]uint64{} | 638 | + | } |
| 639 | + | ||||
| 640 | + | // of returns the content Seq of the windows scene showed to their client. The | |||
| 641 | + | // result is shared with earlier flips of the same content: never modify it. | |||
| 642 | + | func (f *flipShows) of(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) map[ports.WindowID]uint64 { | |||
| 643 | + | if f.scratch == nil { | |||
| 644 | + | f.scratch = make(map[ports.WindowID]uint64) | |||
| 645 | + | } | |||
| 646 | + | clear(f.scratch) | |||
| 637 | for id, c := range contents { | 647 | for id, c := range contents { | ||
| 638 | // A closed window drawn from its kept content is not shown to | 648 | // A closed window drawn from its kept content is not shown to | ||
| 639 | // its client. | 649 | // its client. | ||
| 640 | if surfaces.Shown(scene, id, kept) { | 650 | if surfaces.Shown(scene, id, kept) { | ||
| 641 | - | shows[id] = c.Seq | 651 | + | f.scratch[id] = c.Seq |
| 642 | } | 652 | } | ||
| 643 | } | 653 | } | ||
| 644 | - | return &ports.FlipInfo{When: time.Duration(ts.Nano()), Shows: shows} | 654 | + | if f.snapshot == nil || !maps.Equal(f.snapshot, f.scratch) { |
| 655 | + | f.snapshot = maps.Clone(f.scratch) | |||
| 656 | + | } | |||
| 657 | + | return f.snapshot | |||
| 658 | + | } | |||
| 659 | + | ||||
| 660 | + | // flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC | |||
| 661 | + | // time (software clock, refresh unknown), so presentation feedback and | |||
| 662 | + | // frame callbacks follow headless frames. The FlipInfo itself is new per | |||
| 663 | + | // flip (the sink counts merges on it); only its Shows is shared. | |||
| 664 | + | func (f *flipShows) flipInfo(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) *ports.FlipInfo { | |||
| 665 | + | var ts unix.Timespec | |||
| 666 | + | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | |||
| 667 | + | return &ports.FlipInfo{When: time.Duration(ts.Nano()), Shows: f.of(scene, contents, kept)} | |||
| 645 | } | 668 | } | ||