refactor(wayland): pace frames on the display loop via a presentation sink
- commit
- fefcf43c4307760f824ff3b4f9214464e84ce1b4
- author
- brice · · 2026-10-13 15:50 UTC
- parent
- 94ab056
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
47 files · +664 -285
M .mockery.yml +1 -0
| @@ -17,6 +17,7 @@ packages: | |||
| 17 | 17 | Clock: | |
| 18 | 18 | Notifier: | |
| 19 | 19 | OutputScaleStore: | |
| 20 | + | PresentationSink: | |
| 20 | 21 | Renderer: | |
| 21 | 22 | Seat: | |
| 22 | 23 | SessionSecurity: | |
| @@ -17,6 +17,7 @@ packages: | |||||
| 17 | Clock: | 17 | Clock: | ||
| 18 | Notifier: | 18 | Notifier: | ||
| 19 | OutputScaleStore: | 19 | OutputScaleStore: | ||
| 20 | + | PresentationSink: | |||
| 20 | Renderer: | 21 | Renderer: | ||
| 21 | Seat: | 22 | Seat: | ||
| 22 | SessionSecurity: | 23 | SessionSecurity: | ||
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)$$' -count=1 | |
| 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)$$' -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)$$' -count=1 | 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)$$' -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 | ||
M go.mod +3 -0
| @@ -24,3 +24,6 @@ require ( | |||
| 24 | 24 | go.yaml.in/yaml/v3 v3.0.5 // indirect | |
| 25 | 25 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect | |
| 26 | 26 | ) | |
| 27 | + | ||
| 28 | + | // TODO(release): drop replace, bump to tagged version | |
| 29 | + | replace github.com/bnema/purego-libwayland => ../../../purego-libwayland/.worktrees/display-post | |
| @@ -24,3 +24,6 @@ require ( | |||||
| 24 | go.yaml.in/yaml/v3 v3.0.5 // indirect | 24 | go.yaml.in/yaml/v3 v3.0.5 // indirect | ||
| 25 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect | 25 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect | ||
| 26 | ) | 26 | ) | ||
| 27 | + | ||||
| 28 | + | // TODO(release): drop replace, bump to tagged version | |||
| 29 | + | replace github.com/bnema/purego-libwayland => ../../../purego-libwayland/.worktrees/display-post | |||
M internal/adapters/drm/amdgpu_rules_test.go +1 -1
| @@ -431,7 +431,7 @@ func TestRunProtectedActivationRetriesWithoutStoppingOrEarlyProof(t *testing.T) | |||
| 431 | 431 | defer cancel() | |
| 432 | 432 | done := make(chan error, 1) | |
| 433 | 433 | go func() { | |
| 434 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, nil, nil, nil, make(chan ports.OutputPresented, 8), nil, nil) | |
| 434 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, nil, nil, nil, discardSink(t), nil, nil) | |
| 435 | 435 | }() | |
| 436 | 436 | for { | |
| 437 | 437 | select { | |
| @@ -431,7 +431,7 @@ func TestRunProtectedActivationRetriesWithoutStoppingOrEarlyProof(t *testing.T) | |||||
| 431 | defer cancel() | 431 | defer cancel() | ||
| 432 | done := make(chan error, 1) | 432 | done := make(chan error, 1) | ||
| 433 | go func() { | 433 | go func() { | ||
| 434 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, nil, nil, nil, make(chan ports.OutputPresented, 8), nil, nil) | 434 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, nil, nil, nil, discardSink(t), nil, nil) |
| 435 | }() | 435 | }() | ||
| 436 | for { | 436 | for { | ||
| 437 | select { | 437 | select { | ||
M internal/adapters/drm/capture_test.go +4 -4
| @@ -366,11 +366,11 @@ func TestRunChildHoldLiftedWhileDisplayCommitStalls(t *testing.T) { | |||
| 366 | 366 | contents := make(chan ports.SurfaceContent) // unbuffered: taken before the scene | |
| 367 | 367 | captures := make(chan ports.CaptureRequest) // unbuffered: a send returns once Run took it | |
| 368 | 368 | captured := make(chan ports.CaptureDone, 4) | |
| 369 | - | presented := make(chan ports.OutputPresented, 64) | |
| 369 | + | sink, presented := presentedSink(t, 64) | |
| 370 | 370 | ctx, cancel := context.WithCancel(context.Background()) | |
| 371 | 371 | done := make(chan error, 1) | |
| 372 | 372 | go func() { | |
| 373 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return display, nil }, nil, make(chan bool), scenes, contents, nil, presented, captures, captured) | |
| 373 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return display, nil }, nil, make(chan bool), scenes, contents, nil, sink, captures, captured) | |
| 374 | 374 | }() | |
| 375 | 375 | count := func() int { | |
| 376 | 376 | commitMu.Lock() | |
| @@ -487,7 +487,7 @@ func runCaptureWaitsForItsIndicatorScene(t *testing.T) { | |||
| 487 | 487 | ctx, cancel := context.WithCancel(context.Background()) | |
| 488 | 488 | done := make(chan error, 1) | |
| 489 | 489 | go func() { | |
| 490 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, make(chan ports.OutputPresented, 16), captures, captured) | |
| 490 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, discardSink(t), captures, captured) | |
| 491 | 491 | }() | |
| 492 | 492 | count := func() int { | |
| 493 | 493 | commitMu.Lock() | |
| @@ -563,7 +563,7 @@ func TestRunCaptureWithoutIndicatorFailsAfterTheBoundedWait(t *testing.T) { | |||
| 563 | 563 | ctx, cancel := context.WithCancel(context.Background()) | |
| 564 | 564 | done := make(chan error, 1) | |
| 565 | 565 | go func() { | |
| 566 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, make(chan ports.OutputPresented, 16), captures, captured) | |
| 566 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, discardSink(t), captures, captured) | |
| 567 | 567 | }() | |
| 568 | 568 | count := func() int { | |
| 569 | 569 | commitMu.Lock() | |
| @@ -366,11 +366,11 @@ func TestRunChildHoldLiftedWhileDisplayCommitStalls(t *testing.T) { | |||||
| 366 | contents := make(chan ports.SurfaceContent) // unbuffered: taken before the scene | 366 | contents := make(chan ports.SurfaceContent) // unbuffered: taken before the scene | ||
| 367 | captures := make(chan ports.CaptureRequest) // unbuffered: a send returns once Run took it | 367 | captures := make(chan ports.CaptureRequest) // unbuffered: a send returns once Run took it | ||
| 368 | captured := make(chan ports.CaptureDone, 4) | 368 | captured := make(chan ports.CaptureDone, 4) | ||
| 369 | - | presented := make(chan ports.OutputPresented, 64) | 369 | + | sink, presented := presentedSink(t, 64) |
| 370 | ctx, cancel := context.WithCancel(context.Background()) | 370 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 371 | done := make(chan error, 1) | 371 | done := make(chan error, 1) | ||
| 372 | go func() { | 372 | go func() { | ||
| 373 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return display, nil }, nil, make(chan bool), scenes, contents, nil, presented, captures, captured) | 373 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return display, nil }, nil, make(chan bool), scenes, contents, nil, sink, captures, captured) |
| 374 | }() | 374 | }() | ||
| 375 | count := func() int { | 375 | count := func() int { | ||
| 376 | commitMu.Lock() | 376 | commitMu.Lock() | ||
| @@ -487,7 +487,7 @@ func runCaptureWaitsForItsIndicatorScene(t *testing.T) { | |||||
| 487 | ctx, cancel := context.WithCancel(context.Background()) | 487 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 488 | done := make(chan error, 1) | 488 | done := make(chan error, 1) | ||
| 489 | go func() { | 489 | go func() { | ||
| 490 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, make(chan ports.OutputPresented, 16), captures, captured) | 490 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, discardSink(t), captures, captured) |
| 491 | }() | 491 | }() | ||
| 492 | count := func() int { | 492 | count := func() int { | ||
| 493 | commitMu.Lock() | 493 | commitMu.Lock() | ||
| @@ -563,7 +563,7 @@ func TestRunCaptureWithoutIndicatorFailsAfterTheBoundedWait(t *testing.T) { | |||||
| 563 | ctx, cancel := context.WithCancel(context.Background()) | 563 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 564 | done := make(chan error, 1) | 564 | done := make(chan error, 1) | ||
| 565 | go func() { | 565 | go func() { | ||
| 566 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, make(chan ports.OutputPresented, 16), captures, captured) | 566 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, discardSink(t), captures, captured) |
| 567 | }() | 567 | }() | ||
| 568 | count := func() int { | 568 | count := func() int { | ||
| 569 | commitMu.Lock() | 569 | commitMu.Lock() | ||
M internal/adapters/drm/commit_test.go +5 -5
| @@ -400,7 +400,7 @@ func enableAfterStartAwayModesetsOnce(t *testing.T) { | |||
| 400 | 400 | ctx, cancel := context.WithCancel(context.Background()) | |
| 401 | 401 | done := make(chan error, 1) | |
| 402 | 402 | go func() { | |
| 403 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1), nil, nil) | |
| 403 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, discardSink(t), nil, nil) | |
| 404 | 404 | }() | |
| 405 | 405 | count := func(flags uint32) int { | |
| 406 | 406 | commitMu.Lock() | |
| @@ -517,11 +517,11 @@ func runReportsSeenAfterCursorCommit(t *testing.T) { | |||
| 517 | 517 | } | |
| 518 | 518 | scenes := make(chan ports.Scene, 1) | |
| 519 | 519 | contents := make(chan ports.SurfaceContent, 1) | |
| 520 | - | presented := make(chan ports.OutputPresented, 8) | |
| 520 | + | sink, presented := presentedSink(t, 8) | |
| 521 | 521 | ctx, cancel := context.WithCancel(context.Background()) | |
| 522 | 522 | done := make(chan error, 1) | |
| 523 | 523 | go func() { | |
| 524 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, load, make(chan bool), scenes, contents, nil, presented, nil, nil) | |
| 524 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, load, make(chan bool), scenes, contents, nil, sink, nil, nil) | |
| 525 | 525 | }() | |
| 526 | 526 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1} | |
| 527 | 527 | var frame commitRec | |
| @@ -601,11 +601,11 @@ func TestRunClosesFrameFences(t *testing.T) { | |||
| 601 | 601 | before := fds() | |
| 602 | 602 | active := make(chan bool, 1) | |
| 603 | 603 | scenes := make(chan ports.Scene, 1) | |
| 604 | - | presented := make(chan ports.OutputPresented, 4*rounds) | |
| 604 | + | sink, _ := presentedSink(t, 4*rounds) | |
| 605 | 605 | ctx, cancel := context.WithCancel(context.Background()) | |
| 606 | 606 | done := make(chan error, 1) | |
| 607 | 607 | go func() { | |
| 608 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, presented, nil, nil) | |
| 608 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, sink, nil, nil) | |
| 609 | 609 | }() | |
| 610 | 610 | count := func() int { | |
| 611 | 611 | commitMu.Lock() | |
| @@ -400,7 +400,7 @@ func enableAfterStartAwayModesetsOnce(t *testing.T) { | |||||
| 400 | ctx, cancel := context.WithCancel(context.Background()) | 400 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 401 | done := make(chan error, 1) | 401 | done := make(chan error, 1) | ||
| 402 | go func() { | 402 | go func() { | ||
| 403 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1), nil, nil) | 403 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, discardSink(t), nil, nil) |
| 404 | }() | 404 | }() | ||
| 405 | count := func(flags uint32) int { | 405 | count := func(flags uint32) int { | ||
| 406 | commitMu.Lock() | 406 | commitMu.Lock() | ||
| @@ -517,11 +517,11 @@ func runReportsSeenAfterCursorCommit(t *testing.T) { | |||||
| 517 | } | 517 | } | ||
| 518 | scenes := make(chan ports.Scene, 1) | 518 | scenes := make(chan ports.Scene, 1) | ||
| 519 | contents := make(chan ports.SurfaceContent, 1) | 519 | contents := make(chan ports.SurfaceContent, 1) | ||
| 520 | - | presented := make(chan ports.OutputPresented, 8) | 520 | + | sink, presented := presentedSink(t, 8) |
| 521 | ctx, cancel := context.WithCancel(context.Background()) | 521 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 522 | done := make(chan error, 1) | 522 | done := make(chan error, 1) | ||
| 523 | go func() { | 523 | go func() { | ||
| 524 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, load, make(chan bool), scenes, contents, nil, presented, nil, nil) | 524 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, load, make(chan bool), scenes, contents, nil, sink, nil, nil) |
| 525 | }() | 525 | }() | ||
| 526 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1} | 526 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1} | ||
| 527 | var frame commitRec | 527 | var frame commitRec | ||
| @@ -601,11 +601,11 @@ func TestRunClosesFrameFences(t *testing.T) { | |||||
| 601 | before := fds() | 601 | before := fds() | ||
| 602 | active := make(chan bool, 1) | 602 | active := make(chan bool, 1) | ||
| 603 | scenes := make(chan ports.Scene, 1) | 603 | scenes := make(chan ports.Scene, 1) | ||
| 604 | - | presented := make(chan ports.OutputPresented, 4*rounds) | 604 | + | sink, _ := presentedSink(t, 4*rounds) |
| 605 | ctx, cancel := context.WithCancel(context.Background()) | 605 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 606 | done := make(chan error, 1) | 606 | done := make(chan error, 1) | ||
| 607 | go func() { | 607 | go func() { | ||
| 608 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, presented, nil, nil) | 608 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, sink, nil, nil) |
| 609 | }() | 609 | }() | ||
| 610 | count := func() int { | 610 | count := func() int { | ||
| 611 | commitMu.Lock() | 611 | commitMu.Lock() | ||
M internal/adapters/drm/output.go +6 -6
| @@ -1005,8 +1005,8 @@ func (o *Output) Close() { | |||
| 1005 | 1005 | func (o *Output) InactiveOnClose() bool { return o.inactiveOnClose } | |
| 1006 | 1006 | ||
| 1007 | 1007 | // Run renders scenes and commits them until ctx ends. active reports seat | |
| 1008 | - | // enable/disable. What the output shows and read is reported on reportsCh. | |
| 1009 | - | func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Renderer, error), loadCursor CursorLoader, active <-chan bool, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange, reportsCh chan<- ports.OutputPresented, captures <-chan ports.CaptureRequest, captured chan<- ports.CaptureDone) (runErr error) { | |
| 1008 | + | // enable/disable. What the output shows and read is pushed to reports. | |
| 1009 | + | func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Renderer, error), loadCursor CursorLoader, active <-chan bool, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange, reports ports.PresentationSink, captures <-chan ports.CaptureRequest, captured chan<- ports.CaptureDone) (runErr error) { | |
| 1010 | 1010 | defer func() { | |
| 1011 | 1011 | p := recover() | |
| 1012 | 1012 | if p != nil { | |
| @@ -1232,11 +1232,11 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1232 | 1232 | readWait = readWait || o.capped | |
| 1233 | 1233 | } | |
| 1234 | 1234 | } | |
| 1235 | - | o.reports.Flush(reportsCh) | |
| 1236 | - | // An unsent report is retried soon, not only on the next event; | |
| 1237 | - | // so is one that waits for an uncommitted frame's fence. | |
| 1235 | + | o.reports.Flush(reports) | |
| 1236 | + | // A report that waits for an uncommitted frame's fence is retried | |
| 1237 | + | // soon, not only on the next event. | |
| 1238 | 1238 | var retry <-chan time.Time | |
| 1239 | - | if o.reports.Len() > 0 || readWait { | |
| 1239 | + | if readWait { | |
| 1240 | 1240 | retryTimer.Reset(time.Millisecond) | |
| 1241 | 1241 | retry = retryTimer.C | |
| 1242 | 1242 | } else { | |
| @@ -1005,8 +1005,8 @@ func (o *Output) Close() { | |||||
| 1005 | func (o *Output) InactiveOnClose() bool { return o.inactiveOnClose } | 1005 | func (o *Output) InactiveOnClose() bool { return o.inactiveOnClose } | ||
| 1006 | 1006 | ||||
| 1007 | // Run renders scenes and commits them until ctx ends. active reports seat | 1007 | // Run renders scenes and commits them until ctx ends. active reports seat | ||
| 1008 | - | // enable/disable. What the output shows and read is reported on reportsCh. | 1008 | + | // enable/disable. What the output shows and read is pushed to reports. |
| 1009 | - | func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Renderer, error), loadCursor CursorLoader, active <-chan bool, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange, reportsCh chan<- ports.OutputPresented, captures <-chan ports.CaptureRequest, captured chan<- ports.CaptureDone) (runErr error) { | 1009 | + | func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Renderer, error), loadCursor CursorLoader, active <-chan bool, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange, reports ports.PresentationSink, captures <-chan ports.CaptureRequest, captured chan<- ports.CaptureDone) (runErr error) { |
| 1010 | defer func() { | 1010 | defer func() { | ||
| 1011 | p := recover() | 1011 | p := recover() | ||
| 1012 | if p != nil { | 1012 | if p != nil { | ||
| @@ -1232,11 +1232,11 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1232 | readWait = readWait || o.capped | 1232 | readWait = readWait || o.capped | ||
| 1233 | } | 1233 | } | ||
| 1234 | } | 1234 | } | ||
| 1235 | - | o.reports.Flush(reportsCh) | 1235 | + | o.reports.Flush(reports) |
| 1236 | - | // An unsent report is retried soon, not only on the next event; | 1236 | + | // A report that waits for an uncommitted frame's fence is retried |
| 1237 | - | // so is one that waits for an uncommitted frame's fence. | 1237 | + | // soon, not only on the next event. |
| 1238 | var retry <-chan time.Time | 1238 | var retry <-chan time.Time | ||
| 1239 | - | if o.reports.Len() > 0 || readWait { | 1239 | + | if readWait { |
| 1240 | retryTimer.Reset(time.Millisecond) | 1240 | retryTimer.Reset(time.Millisecond) | ||
| 1241 | retry = retryTimer.C | 1241 | retry = retryTimer.C | ||
| 1242 | } else { | 1242 | } else { | ||
M internal/adapters/drm/output_test.go +7 -7
| @@ -598,11 +598,11 @@ func runReportsSeenAfterFlip(t *testing.T) { | |||
| 598 | 598 | } | |
| 599 | 599 | scenes := make(chan ports.Scene, 1) | |
| 600 | 600 | contents := make(chan ports.SurfaceContent, 1) | |
| 601 | - | presented := make(chan ports.OutputPresented, 8) | |
| 601 | + | sink, presented := presentedSink(t, 8) | |
| 602 | 602 | ctx, cancel := context.WithCancel(context.Background()) | |
| 603 | 603 | done := make(chan error, 1) | |
| 604 | 604 | go func() { | |
| 605 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents, nil, presented, nil, nil) | |
| 605 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents, nil, sink, nil, nil) | |
| 606 | 606 | }() | |
| 607 | 607 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}} | |
| 608 | 608 | waitFor(t, func() bool { return frameCommits() == 1 }) | |
| @@ -679,7 +679,7 @@ func TestRunComposedFullscreenKeepsVRR(t *testing.T) { | |||
| 679 | 679 | ctx, cancel := context.WithCancel(context.Background()) | |
| 680 | 680 | done := make(chan error, 1) | |
| 681 | 681 | go func() { | |
| 682 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, make(chan ports.OutputPresented, 8), nil, nil) | |
| 682 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, discardSink(t), nil, nil) | |
| 683 | 683 | }() | |
| 684 | 684 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, | |
| 685 | 685 | Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}}, | |
| @@ -761,7 +761,7 @@ func TestRunTrimsRendererOnStatsTick(t *testing.T) { | |||
| 761 | 761 | active <- false | |
| 762 | 762 | done := make(chan error, 1) | |
| 763 | 763 | go func() { | |
| 764 | - | done <- o.Run(context.Background(), func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1), nil, nil) | |
| 764 | + | done <- o.Run(context.Background(), func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, discardSink(t), nil, nil) | |
| 765 | 765 | }() | |
| 766 | 766 | ticks <- now | |
| 767 | 767 | <-trimmed | |
| @@ -881,7 +881,7 @@ func TestCaptureForcesDRMComposition(t *testing.T) { | |||
| 881 | 881 | ctx, cancel := context.WithCancel(context.Background()) | |
| 882 | 882 | done := make(chan error, 1) | |
| 883 | 883 | go func() { | |
| 884 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, make(chan ports.OutputPresented, 8), requests, replies) | |
| 884 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, discardSink(t), requests, replies) | |
| 885 | 885 | }() | |
| 886 | 886 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Background: "#000000"} | |
| 887 | 887 | select { | |
| @@ -1001,7 +1001,7 @@ func startStampRun(t *testing.T) *stampRun { | |||
| 1001 | 1001 | ctx, cancel := context.WithCancel(context.Background()) | |
| 1002 | 1002 | done := make(chan error, 1) | |
| 1003 | 1003 | go func() { | |
| 1004 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), sr.scenes, sr.contents, nil, make(chan ports.OutputPresented, 8), nil, nil) | |
| 1004 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), sr.scenes, sr.contents, nil, discardSink(t), nil, nil) | |
| 1005 | 1005 | }() | |
| 1006 | 1006 | // stop ends Run: its state is ours to read afterwards. | |
| 1007 | 1007 | sr.stop = func() { | |
| @@ -1133,7 +1133,7 @@ func TestRunKeepsLastContentOfLeavingWindow(t *testing.T) { | |||
| 1133 | 1133 | ctx, cancel := context.WithCancel(context.Background()) | |
| 1134 | 1134 | done := make(chan error, 1) | |
| 1135 | 1135 | go func() { | |
| 1136 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents, nil, make(chan ports.OutputPresented, 64), nil, nil) | |
| 1136 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents, nil, discardSink(t), nil, nil) | |
| 1137 | 1137 | }() | |
| 1138 | 1138 | acq, acqW, _ := os.Pipe() | |
| 1139 | 1139 | defer acq.Close() | |
| @@ -598,11 +598,11 @@ func runReportsSeenAfterFlip(t *testing.T) { | |||||
| 598 | } | 598 | } | ||
| 599 | scenes := make(chan ports.Scene, 1) | 599 | scenes := make(chan ports.Scene, 1) | ||
| 600 | contents := make(chan ports.SurfaceContent, 1) | 600 | contents := make(chan ports.SurfaceContent, 1) | ||
| 601 | - | presented := make(chan ports.OutputPresented, 8) | 601 | + | sink, presented := presentedSink(t, 8) |
| 602 | ctx, cancel := context.WithCancel(context.Background()) | 602 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 603 | done := make(chan error, 1) | 603 | done := make(chan error, 1) | ||
| 604 | go func() { | 604 | go func() { | ||
| 605 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents, nil, presented, nil, nil) | 605 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents, nil, sink, nil, nil) |
| 606 | }() | 606 | }() | ||
| 607 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}} | 607 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}} | ||
| 608 | waitFor(t, func() bool { return frameCommits() == 1 }) | 608 | waitFor(t, func() bool { return frameCommits() == 1 }) | ||
| @@ -679,7 +679,7 @@ func TestRunComposedFullscreenKeepsVRR(t *testing.T) { | |||||
| 679 | ctx, cancel := context.WithCancel(context.Background()) | 679 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 680 | done := make(chan error, 1) | 680 | done := make(chan error, 1) | ||
| 681 | go func() { | 681 | go func() { | ||
| 682 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, make(chan ports.OutputPresented, 8), nil, nil) | 682 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, discardSink(t), nil, nil) |
| 683 | }() | 683 | }() | ||
| 684 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, | 684 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, | ||
| 685 | Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}}, | 685 | Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}}, | ||
| @@ -761,7 +761,7 @@ func TestRunTrimsRendererOnStatsTick(t *testing.T) { | |||||
| 761 | active <- false | 761 | active <- false | ||
| 762 | done := make(chan error, 1) | 762 | done := make(chan error, 1) | ||
| 763 | go func() { | 763 | go func() { | ||
| 764 | - | done <- o.Run(context.Background(), func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1), nil, nil) | 764 | + | done <- o.Run(context.Background(), func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, discardSink(t), nil, nil) |
| 765 | }() | 765 | }() | ||
| 766 | ticks <- now | 766 | ticks <- now | ||
| 767 | <-trimmed | 767 | <-trimmed | ||
| @@ -881,7 +881,7 @@ func TestCaptureForcesDRMComposition(t *testing.T) { | |||||
| 881 | ctx, cancel := context.WithCancel(context.Background()) | 881 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 882 | done := make(chan error, 1) | 882 | done := make(chan error, 1) | ||
| 883 | go func() { | 883 | go func() { | ||
| 884 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, make(chan ports.OutputPresented, 8), requests, replies) | 884 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, discardSink(t), requests, replies) |
| 885 | }() | 885 | }() | ||
| 886 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Background: "#000000"} | 886 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Background: "#000000"} | ||
| 887 | select { | 887 | select { | ||
| @@ -1001,7 +1001,7 @@ func startStampRun(t *testing.T) *stampRun { | |||||
| 1001 | ctx, cancel := context.WithCancel(context.Background()) | 1001 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 1002 | done := make(chan error, 1) | 1002 | done := make(chan error, 1) | ||
| 1003 | go func() { | 1003 | go func() { | ||
| 1004 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), sr.scenes, sr.contents, nil, make(chan ports.OutputPresented, 8), nil, nil) | 1004 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), sr.scenes, sr.contents, nil, discardSink(t), nil, nil) |
| 1005 | }() | 1005 | }() | ||
| 1006 | // stop ends Run: its state is ours to read afterwards. | 1006 | // stop ends Run: its state is ours to read afterwards. | ||
| 1007 | sr.stop = func() { | 1007 | sr.stop = func() { | ||
| @@ -1133,7 +1133,7 @@ func TestRunKeepsLastContentOfLeavingWindow(t *testing.T) { | |||||
| 1133 | ctx, cancel := context.WithCancel(context.Background()) | 1133 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 1134 | done := make(chan error, 1) | 1134 | done := make(chan error, 1) | ||
| 1135 | go func() { | 1135 | go func() { | ||
| 1136 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents, nil, make(chan ports.OutputPresented, 64), nil, nil) | 1136 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents, nil, discardSink(t), nil, nil) |
| 1137 | }() | 1137 | }() | ||
| 1138 | acq, acqW, _ := os.Pipe() | 1138 | acq, acqW, _ := os.Pipe() | ||
| 1139 | defer acq.Close() | 1139 | defer acq.Close() | ||
M internal/adapters/drm/power_test.go +2 -2
| @@ -50,7 +50,7 @@ func runOutputPower(t *testing.T) { | |||
| 50 | 50 | ctx, cancel := context.WithCancel(context.Background()) | |
| 51 | 51 | done := make(chan error, 1) | |
| 52 | 52 | go func() { | |
| 53 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, vt, scenes, make(chan ports.SurfaceContent), nil, make(chan ports.OutputPresented, 8), nil, nil) | |
| 53 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, vt, scenes, make(chan ports.SurfaceContent), nil, discardSink(t), nil, nil) | |
| 54 | 54 | }() | |
| 55 | 55 | on := ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1} | |
| 56 | 56 | scenes <- on | |
| @@ -174,7 +174,7 @@ func runOutputStartOff(t *testing.T) { | |||
| 174 | 174 | ctx, cancel := context.WithCancel(context.Background()) | |
| 175 | 175 | done := make(chan error, 1) | |
| 176 | 176 | go func() { | |
| 177 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, make(chan ports.SurfaceContent), nil, make(chan ports.OutputPresented, 8), nil, nil) | |
| 177 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, make(chan ports.SurfaceContent), nil, discardSink(t), nil, nil) | |
| 178 | 178 | }() | |
| 179 | 179 | if err := <-ready; err != nil { | |
| 180 | 180 | t.Fatal(err) | |
| @@ -50,7 +50,7 @@ func runOutputPower(t *testing.T) { | |||||
| 50 | ctx, cancel := context.WithCancel(context.Background()) | 50 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 51 | done := make(chan error, 1) | 51 | done := make(chan error, 1) | ||
| 52 | go func() { | 52 | go func() { | ||
| 53 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, vt, scenes, make(chan ports.SurfaceContent), nil, make(chan ports.OutputPresented, 8), nil, nil) | 53 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, vt, scenes, make(chan ports.SurfaceContent), nil, discardSink(t), nil, nil) |
| 54 | }() | 54 | }() | ||
| 55 | on := ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1} | 55 | on := ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1} | ||
| 56 | scenes <- on | 56 | scenes <- on | ||
| @@ -174,7 +174,7 @@ func runOutputStartOff(t *testing.T) { | |||||
| 174 | ctx, cancel := context.WithCancel(context.Background()) | 174 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 175 | done := make(chan error, 1) | 175 | done := make(chan error, 1) | ||
| 176 | go func() { | 176 | go func() { | ||
| 177 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, make(chan ports.SurfaceContent), nil, make(chan ports.OutputPresented, 8), nil, nil) | 177 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, make(chan ports.SurfaceContent), nil, discardSink(t), nil, nil) |
| 178 | }() | 178 | }() | ||
| 179 | if err := <-ready; err != nil { | 179 | if err := <-ready; err != nil { | ||
| 180 | t.Fatal(err) | 180 | t.Fatal(err) | ||
A internal/adapters/drm/presented_sink_test.go +32 -0
| @@ -0,0 +1,32 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |
| 8 | + | "github.com/stretchr/testify/mock" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // presentedSink is a generated PresentationSink mock that records every push | |
| 12 | + | // on the returned channel. A full channel drops the report: Push never blocks, | |
| 13 | + | // as the port requires. | |
| 14 | + | func presentedSink(t *testing.T, size int) (ports.PresentationSink, chan ports.OutputPresented) { | |
| 15 | + | t.Helper() | |
| 16 | + | ch := make(chan ports.OutputPresented, size) | |
| 17 | + | sink := portsmocks.NewMockPresentationSink(t) | |
| 18 | + | sink.EXPECT().Push(mock.Anything).RunAndReturn(func(r ports.OutputPresented) { | |
| 19 | + | select { | |
| 20 | + | case ch <- r: | |
| 21 | + | default: | |
| 22 | + | } | |
| 23 | + | }).Maybe() | |
| 24 | + | return sink, ch | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // discardSink is a PresentationSink for tests that ignore the reports. | |
| 28 | + | func discardSink(t *testing.T) ports.PresentationSink { | |
| 29 | + | t.Helper() | |
| 30 | + | sink, _ := presentedSink(t, 1) | |
| 31 | + | return sink | |
| 32 | + | } | |
| @@ -0,0 +1,32 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | + | "github.com/stretchr/testify/mock" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // presentedSink is a generated PresentationSink mock that records every push | |||
| 12 | + | // on the returned channel. A full channel drops the report: Push never blocks, | |||
| 13 | + | // as the port requires. | |||
| 14 | + | func presentedSink(t *testing.T, size int) (ports.PresentationSink, chan ports.OutputPresented) { | |||
| 15 | + | t.Helper() | |||
| 16 | + | ch := make(chan ports.OutputPresented, size) | |||
| 17 | + | sink := portsmocks.NewMockPresentationSink(t) | |||
| 18 | + | sink.EXPECT().Push(mock.Anything).RunAndReturn(func(r ports.OutputPresented) { | |||
| 19 | + | select { | |||
| 20 | + | case ch <- r: | |||
| 21 | + | default: | |||
| 22 | + | } | |||
| 23 | + | }).Maybe() | |||
| 24 | + | return sink, ch | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | // discardSink is a PresentationSink for tests that ignore the reports. | |||
| 28 | + | func discardSink(t *testing.T) ports.PresentationSink { | |||
| 29 | + | t.Helper() | |||
| 30 | + | sink, _ := presentedSink(t, 1) | |||
| 31 | + | return sink | |||
| 32 | + | } | |||
M internal/adapters/drm/protection_retry_test.go +1 -1
| @@ -43,7 +43,7 @@ func runProtectedStartup(t *testing.T, o *Output, k *mockkms, renders func() (*o | |||
| 43 | 43 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |
| 44 | 44 | done := make(chan error, 1) | |
| 45 | 45 | go func() { | |
| 46 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, nil, nil, nil, make(chan ports.OutputPresented, 8), nil, nil) | |
| 46 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, nil, nil, nil, discardSink(t), nil, nil) | |
| 47 | 47 | }() | |
| 48 | 48 | return events, done, cancel | |
| 49 | 49 | } | |
| @@ -43,7 +43,7 @@ func runProtectedStartup(t *testing.T, o *Output, k *mockkms, renders func() (*o | |||||
| 43 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | 43 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
| 44 | done := make(chan error, 1) | 44 | done := make(chan error, 1) | ||
| 45 | go func() { | 45 | go func() { | ||
| 46 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, nil, nil, nil, make(chan ports.OutputPresented, 8), nil, nil) | 46 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, nil, nil, nil, discardSink(t), nil, nil) |
| 47 | }() | 47 | }() | ||
| 48 | return events, done, cancel | 48 | return events, done, cancel | ||
| 49 | } | 49 | } | ||
M internal/adapters/drm/reacquire_test.go +1 -1
| @@ -96,7 +96,7 @@ func newRetryHarness(t *testing.T) retryHarness { | |||
| 96 | 96 | ctx, cancel := context.WithCancel(context.Background()) | |
| 97 | 97 | done := make(chan error, 1) | |
| 98 | 98 | go func() { | |
| 99 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1), nil, nil) | |
| 99 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, discardSink(t), nil, nil) | |
| 100 | 100 | }() | |
| 101 | 101 | synctest.Wait() | |
| 102 | 102 | return retryHarness{ | |
| @@ -96,7 +96,7 @@ func newRetryHarness(t *testing.T) retryHarness { | |||||
| 96 | ctx, cancel := context.WithCancel(context.Background()) | 96 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 97 | done := make(chan error, 1) | 97 | done := make(chan error, 1) | ||
| 98 | go func() { | 98 | go func() { | ||
| 99 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1), nil, nil) | 99 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, discardSink(t), nil, nil) |
| 100 | }() | 100 | }() | ||
| 101 | synctest.Wait() | 101 | synctest.Wait() | ||
| 102 | return retryHarness{ | 102 | return retryHarness{ | ||
M internal/adapters/drm/seat_disable_test.go +1 -1
| @@ -85,7 +85,7 @@ func TestRunPreparesSeatDisable(t *testing.T) { | |||
| 85 | 85 | ctx, cancel := context.WithCancel(context.Background()) | |
| 86 | 86 | done := make(chan error, 1) | |
| 87 | 87 | go func() { | |
| 88 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1), nil, nil) | |
| 88 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, discardSink(t), nil, nil) | |
| 89 | 89 | }() | |
| 90 | 90 | if !o.PrepareSeatDisable(5 * time.Second) { | |
| 91 | 91 | t.Fatal("no ack") | |
| @@ -85,7 +85,7 @@ func TestRunPreparesSeatDisable(t *testing.T) { | |||||
| 85 | ctx, cancel := context.WithCancel(context.Background()) | 85 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 86 | done := make(chan error, 1) | 86 | done := make(chan error, 1) | ||
| 87 | go func() { | 87 | go func() { | ||
| 88 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1), nil, nil) | 88 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, discardSink(t), nil, nil) |
| 89 | }() | 89 | }() | ||
| 90 | if !o.PrepareSeatDisable(5 * time.Second) { | 90 | if !o.PrepareSeatDisable(5 * time.Second) { | ||
| 91 | t.Fatal("no ack") | 91 | t.Fatal("no ack") | ||
M internal/adapters/drm/security_recovery_test.go +1 -1
| @@ -61,7 +61,7 @@ func TestSecurityStuckRecoveryInvalidatesBeforeFreshProof(t *testing.T) { | |||
| 61 | 61 | scenes := make(chan ports.Scene, 1) | |
| 62 | 62 | done := make(chan error, 1) | |
| 63 | 63 | go func() { | |
| 64 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, make(chan ports.OutputPresented, 8), nil, nil) | |
| 64 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, discardSink(t), nil, nil) | |
| 65 | 65 | }() | |
| 66 | 66 | receive := func() ports.SecurityBackendEvent { | |
| 67 | 67 | t.Helper() | |
| @@ -61,7 +61,7 @@ func TestSecurityStuckRecoveryInvalidatesBeforeFreshProof(t *testing.T) { | |||||
| 61 | scenes := make(chan ports.Scene, 1) | 61 | scenes := make(chan ports.Scene, 1) | ||
| 62 | done := make(chan error, 1) | 62 | done := make(chan error, 1) | ||
| 63 | go func() { | 63 | go func() { | ||
| 64 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, make(chan ports.OutputPresented, 8), nil, nil) | 64 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, discardSink(t), nil, nil) |
| 65 | }() | 65 | }() | ||
| 66 | receive := func() ports.SecurityBackendEvent { | 66 | receive := func() ports.SecurityBackendEvent { | ||
| 67 | t.Helper() | 67 | t.Helper() | ||
M internal/adapters/drm/security_run_test.go +1 -1
| @@ -57,7 +57,7 @@ func securityRunWakePendingResumeAndOff(t *testing.T) { | |||
| 57 | 57 | defer cancel() | |
| 58 | 58 | done := make(chan error, 1) | |
| 59 | 59 | go func() { | |
| 60 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, make(chan ports.OutputPresented, 8), nil, nil) | |
| 60 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, discardSink(t), nil, nil) | |
| 61 | 61 | }() | |
| 62 | 62 | scenes <- ports.Scene{Seq: 99, Scale: 1} | |
| 63 | 63 | select { | |
| @@ -57,7 +57,7 @@ func securityRunWakePendingResumeAndOff(t *testing.T) { | |||||
| 57 | defer cancel() | 57 | defer cancel() | ||
| 58 | done := make(chan error, 1) | 58 | done := make(chan error, 1) | ||
| 59 | go func() { | 59 | go func() { | ||
| 60 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, make(chan ports.OutputPresented, 8), nil, nil) | 60 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, discardSink(t), nil, nil) |
| 61 | }() | 61 | }() | ||
| 62 | scenes <- ports.Scene{Seq: 99, Scale: 1} | 62 | scenes <- ports.Scene{Seq: 99, Scale: 1} | ||
| 63 | select { | 63 | select { | ||
M internal/adapters/drm/stuck_test.go +4 -2
| @@ -33,6 +33,7 @@ type stuckRun struct { | |||
| 33 | 33 | contents chan ports.SurfaceContent | |
| 34 | 34 | active chan bool | |
| 35 | 35 | presented chan ports.OutputPresented | |
| 36 | + | sink ports.PresentationSink | |
| 36 | 37 | count func() int | |
| 37 | 38 | stop func() []commitRec | |
| 38 | 39 | } | |
| @@ -67,11 +68,12 @@ func startStuck(t *testing.T, render func() *os.File, errs ...error) *stuckRun { | |||
| 67 | 68 | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |
| 68 | 69 | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |
| 69 | 70 | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |
| 70 | - | h := &stuckRun{t: t, scenes: make(chan ports.Scene, 1), contents: make(chan ports.SurfaceContent, 1), active: make(chan bool, 1), presented: make(chan ports.OutputPresented, 64)} | |
| 71 | + | h := &stuckRun{t: t, scenes: make(chan ports.Scene, 1), contents: make(chan ports.SurfaceContent, 1), active: make(chan bool, 1), presented: nil} | |
| 72 | + | h.sink, h.presented = presentedSink(t, 64) | |
| 71 | 73 | ctx, cancel := context.WithCancel(context.Background()) | |
| 72 | 74 | done := make(chan error, 1) | |
| 73 | 75 | go func() { | |
| 74 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, h.active, h.scenes, h.contents, nil, h.presented, nil, nil) | |
| 76 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, h.active, h.scenes, h.contents, nil, h.sink, nil, nil) | |
| 75 | 77 | }() | |
| 76 | 78 | h.count = func() int { | |
| 77 | 79 | commitMu.Lock() | |
| @@ -33,6 +33,7 @@ type stuckRun struct { | |||||
| 33 | contents chan ports.SurfaceContent | 33 | contents chan ports.SurfaceContent | ||
| 34 | active chan bool | 34 | active chan bool | ||
| 35 | presented chan ports.OutputPresented | 35 | presented chan ports.OutputPresented | ||
| 36 | + | sink ports.PresentationSink | |||
| 36 | count func() int | 37 | count func() int | ||
| 37 | stop func() []commitRec | 38 | stop func() []commitRec | ||
| 38 | } | 39 | } | ||
| @@ -67,11 +68,12 @@ func startStuck(t *testing.T, render func() *os.File, errs ...error) *stuckRun { | |||||
| 67 | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | 68 | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | ||
| 68 | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | 69 | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | ||
| 69 | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | 70 | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | ||
| 70 | - | h := &stuckRun{t: t, scenes: make(chan ports.Scene, 1), contents: make(chan ports.SurfaceContent, 1), active: make(chan bool, 1), presented: make(chan ports.OutputPresented, 64)} | 71 | + | h := &stuckRun{t: t, scenes: make(chan ports.Scene, 1), contents: make(chan ports.SurfaceContent, 1), active: make(chan bool, 1), presented: nil} |
| 72 | + | h.sink, h.presented = presentedSink(t, 64) | |||
| 71 | ctx, cancel := context.WithCancel(context.Background()) | 73 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 72 | done := make(chan error, 1) | 74 | done := make(chan error, 1) | ||
| 73 | go func() { | 75 | go func() { | ||
| 74 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, h.active, h.scenes, h.contents, nil, h.presented, nil, nil) | 76 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, h.active, h.scenes, h.contents, nil, h.sink, nil, nil) |
| 75 | }() | 77 | }() | ||
| 76 | h.count = func() int { | 78 | h.count = func() int { | ||
| 77 | commitMu.Lock() | 79 | commitMu.Lock() | ||
M internal/adapters/drm/vrr_flip_gap_test.go +1 -1
| @@ -115,7 +115,7 @@ func TestRunWaitsVRRFlipGap(t *testing.T) { | |||
| 115 | 115 | ctx, cancel := context.WithCancel(context.Background()) | |
| 116 | 116 | done := make(chan error, 1) | |
| 117 | 117 | go func() { | |
| 118 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, make(chan ports.OutputPresented, 8), nil, nil) | |
| 118 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, discardSink(t), nil, nil) | |
| 119 | 119 | }() | |
| 120 | 120 | // A composed fullscreen window: a game frame under VRR. | |
| 121 | 121 | game := ports.Scene{OutputWidth: 200, OutputHeight: 100, | |
| @@ -115,7 +115,7 @@ func TestRunWaitsVRRFlipGap(t *testing.T) { | |||||
| 115 | ctx, cancel := context.WithCancel(context.Background()) | 115 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 116 | done := make(chan error, 1) | 116 | done := make(chan error, 1) | ||
| 117 | go func() { | 117 | go func() { | ||
| 118 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, make(chan ports.OutputPresented, 8), nil, nil) | 118 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, discardSink(t), nil, nil) |
| 119 | }() | 119 | }() | ||
| 120 | // A composed fullscreen window: a game frame under VRR. | 120 | // A composed fullscreen window: a game frame under VRR. | ||
| 121 | game := ports.Scene{OutputWidth: 200, OutputHeight: 100, | 121 | game := ports.Scene{OutputWidth: 200, OutputHeight: 100, | ||
M internal/adapters/headless/capture_test.go +2 -2
| @@ -163,9 +163,9 @@ func TestHiddenWorkspaceCaptureUsesChildRenderer(t *testing.T) { | |||
| 163 | 163 | replies := make(chan ports.CaptureDone, 2) | |
| 164 | 164 | done := make(chan error, 1) | |
| 165 | 165 | var made [2]int | |
| 166 | - | presented := make(chan ports.OutputPresented, 4) | |
| 166 | + | sink, presented := presentedSink(t, 4) | |
| 167 | 167 | go func() { | |
| 168 | - | done <- Run(ctx, Options{Width: 2, Height: 2, Captured: replies, Presented: presented, | |
| 168 | + | done <- Run(ctx, Options{Width: 2, Height: 2, Captured: replies, Presented: sink, | |
| 169 | 169 | NewRenderer: func(int, int) (ports.Renderer, error) { return display, nil }, | |
| 170 | 170 | NewCaptureRenderer: func(w, h int) (ports.Renderer, error) { made = [2]int{w, h}; return child, nil }, | |
| 171 | 171 | }, scenes, nil, nil, incoming) | |
| @@ -163,9 +163,9 @@ func TestHiddenWorkspaceCaptureUsesChildRenderer(t *testing.T) { | |||||
| 163 | replies := make(chan ports.CaptureDone, 2) | 163 | replies := make(chan ports.CaptureDone, 2) | ||
| 164 | done := make(chan error, 1) | 164 | done := make(chan error, 1) | ||
| 165 | var made [2]int | 165 | var made [2]int | ||
| 166 | - | presented := make(chan ports.OutputPresented, 4) | 166 | + | sink, presented := presentedSink(t, 4) |
| 167 | go func() { | 167 | go func() { | ||
| 168 | - | done <- Run(ctx, Options{Width: 2, Height: 2, Captured: replies, Presented: presented, | 168 | + | done <- Run(ctx, Options{Width: 2, Height: 2, Captured: replies, Presented: sink, |
| 169 | NewRenderer: func(int, int) (ports.Renderer, error) { return display, nil }, | 169 | NewRenderer: func(int, int) (ports.Renderer, error) { return display, nil }, | ||
| 170 | NewCaptureRenderer: func(w, h int) (ports.Renderer, error) { made = [2]int{w, h}; return child, nil }, | 170 | NewCaptureRenderer: func(w, h int) (ports.Renderer, error) { made = [2]int{w, h}; return child, nil }, | ||
| 171 | }, scenes, nil, nil, incoming) | 171 | }, scenes, nil, nil, incoming) | ||
M internal/adapters/headless/output.go +5 -27
| @@ -47,7 +47,7 @@ type Options struct { | |||
| 47 | 47 | // Name and Presented report what the output has read after each | |
| 48 | 48 | // frame, so wayland can release client buffers (nil: no reports). | |
| 49 | 49 | Name string | |
| 50 | - | Presented chan<- ports.OutputPresented | |
| 50 | + | Presented ports.PresentationSink | |
| 51 | 51 | Captured chan<- ports.CaptureDone | |
| 52 | 52 | // Clock paces the renderer trim (nil: system clock). | |
| 53 | 53 | Clock ports.Clock | |
| @@ -111,7 +111,8 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 111 | 111 | cursorScale := -1.0 // not loaded yet | |
| 112 | 112 | var cursorTransform ports.BufferTransform | |
| 113 | 113 | seen := map[ports.WindowID]uint64{} | |
| 114 | - | // reports holds what the channel could not take yet, retried soon. | |
| 114 | + | // reports queues what the output read; every report is pushed to the | |
| 115 | + | // sink at once (Push never blocks), so it holds one at most. | |
| 115 | 116 | var reports presented.Queue | |
| 116 | 117 | // mergedFlips counts flips merged since the last stats line. | |
| 117 | 118 | mergedFlips := 0 | |
| @@ -217,10 +218,6 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 217 | 218 | } | |
| 218 | 219 | return err | |
| 219 | 220 | } | |
| 220 | - | var retry <-chan time.Time | |
| 221 | - | if reports.Len() > 0 { | |
| 222 | - | retry = time.After(time.Millisecond) | |
| 223 | - | } | |
| 224 | 221 | // Requests wait for a scene that shows their indicator, at most | |
| 225 | 222 | // capture.HoldFor (see capture.Hold). | |
| 226 | 223 | var holdDue <-chan time.Time | |
| @@ -292,15 +289,6 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 292 | 289 | refuse = capture.ErrOutputOff | |
| 293 | 290 | } | |
| 294 | 291 | dirty = requests.Admit(q, refuse, scene, haveScene) || dirty | |
| 295 | - | case <-retry: | |
| 296 | - | if err := checkSecurity(); err != nil { | |
| 297 | - | if ctx.Err() != nil { | |
| 298 | - | return nil | |
| 299 | - | } | |
| 300 | - | return err | |
| 301 | - | } | |
| 302 | - | opts.flush(&reports) | |
| 303 | - | continue | |
| 304 | 292 | case <-trim.C(): | |
| 305 | 293 | if err := checkSecurity(); err != nil { | |
| 306 | 294 | if ctx.Err() != nil { | |
| @@ -456,9 +444,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 456 | 444 | if reports.Push(ports.OutputPresented{Output: opts.Name, Seen: seen, ChildReads: reads}) { | |
| 457 | 445 | mergedFlips++ | |
| 458 | 446 | } | |
| 459 | - | if reports.Drain(ctx, opts.Presented) != nil { | |
| 460 | - | return nil | |
| 461 | - | } | |
| 447 | + | reports.Flush(opts.Presented) | |
| 462 | 448 | } | |
| 463 | 449 | } | |
| 464 | 450 | if err := pipeline.WaitHidden(ctx); err != nil { | |
| @@ -627,8 +613,7 @@ func writePNGSecure(path string, img image.Image, security ports.SessionSecurity | |||
| 627 | 613 | } | |
| 628 | 614 | ||
| 629 | 615 | // report queues what the output read, with flip for a drawn frame, and | |
| 630 | - | // sends what the channel takes now; the rest is retried soon. | |
| 631 | - | // It reports whether a flip was merged because wayland was not reading. | |
| 616 | + | // pushes it to the sink. It reports whether a flip was merged. | |
| 632 | 617 | func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[ports.WindowID]uint64) (merged bool) { | |
| 633 | 618 | if opts.Presented == nil { | |
| 634 | 619 | return false | |
| @@ -640,13 +625,6 @@ func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[po | |||
| 640 | 625 | return merged | |
| 641 | 626 | } | |
| 642 | 627 | ||
| 643 | - | // flush retries reports the channel could not take. | |
| 644 | - | func (opts Options) flush(q *presented.Queue) { | |
| 645 | - | if opts.Presented != nil { | |
| 646 | - | q.Flush(opts.Presented) | |
| 647 | - | } | |
| 648 | - | } | |
| 649 | - | ||
| 650 | 628 | // flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC | |
| 651 | 629 | // time (software clock, refresh unknown), so presentation feedback and | |
| 652 | 630 | // frame callbacks follow headless frames. | |
| @@ -47,7 +47,7 @@ type Options struct { | |||||
| 47 | // Name and Presented report what the output has read after each | 47 | // Name and Presented report what the output has read after each | ||
| 48 | // frame, so wayland can release client buffers (nil: no reports). | 48 | // frame, so wayland can release client buffers (nil: no reports). | ||
| 49 | Name string | 49 | Name string | ||
| 50 | - | Presented chan<- ports.OutputPresented | 50 | + | Presented ports.PresentationSink |
| 51 | Captured chan<- ports.CaptureDone | 51 | Captured chan<- ports.CaptureDone | ||
| 52 | // Clock paces the renderer trim (nil: system clock). | 52 | // Clock paces the renderer trim (nil: system clock). | ||
| 53 | Clock ports.Clock | 53 | Clock ports.Clock | ||
| @@ -111,7 +111,8 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 111 | cursorScale := -1.0 // not loaded yet | 111 | cursorScale := -1.0 // not loaded yet | ||
| 112 | var cursorTransform ports.BufferTransform | 112 | var cursorTransform ports.BufferTransform | ||
| 113 | seen := map[ports.WindowID]uint64{} | 113 | seen := map[ports.WindowID]uint64{} | ||
| 114 | - | // reports holds what the channel could not take yet, retried soon. | 114 | + | // reports queues what the output read; every report is pushed to the |
| 115 | + | // sink at once (Push never blocks), so it holds one at most. | |||
| 115 | var reports presented.Queue | 116 | var reports presented.Queue | ||
| 116 | // mergedFlips counts flips merged since the last stats line. | 117 | // mergedFlips counts flips merged since the last stats line. | ||
| 117 | mergedFlips := 0 | 118 | mergedFlips := 0 | ||
| @@ -217,10 +218,6 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 217 | } | 218 | } | ||
| 218 | return err | 219 | return err | ||
| 219 | } | 220 | } | ||
| 220 | - | var retry <-chan time.Time | |||
| 221 | - | if reports.Len() > 0 { | |||
| 222 | - | retry = time.After(time.Millisecond) | |||
| 223 | - | } | |||
| 224 | // Requests wait for a scene that shows their indicator, at most | 221 | // Requests wait for a scene that shows their indicator, at most | ||
| 225 | // capture.HoldFor (see capture.Hold). | 222 | // capture.HoldFor (see capture.Hold). | ||
| 226 | var holdDue <-chan time.Time | 223 | var holdDue <-chan time.Time | ||
| @@ -292,15 +289,6 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 292 | refuse = capture.ErrOutputOff | 289 | refuse = capture.ErrOutputOff | ||
| 293 | } | 290 | } | ||
| 294 | dirty = requests.Admit(q, refuse, scene, haveScene) || dirty | 291 | dirty = requests.Admit(q, refuse, scene, haveScene) || dirty | ||
| 295 | - | case <-retry: | |||
| 296 | - | if err := checkSecurity(); err != nil { | |||
| 297 | - | if ctx.Err() != nil { | |||
| 298 | - | return nil | |||
| 299 | - | } | |||
| 300 | - | return err | |||
| 301 | - | } | |||
| 302 | - | opts.flush(&reports) | |||
| 303 | - | continue | |||
| 304 | case <-trim.C(): | 292 | case <-trim.C(): | ||
| 305 | if err := checkSecurity(); err != nil { | 293 | if err := checkSecurity(); err != nil { | ||
| 306 | if ctx.Err() != nil { | 294 | if ctx.Err() != nil { | ||
| @@ -456,9 +444,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 456 | if reports.Push(ports.OutputPresented{Output: opts.Name, Seen: seen, ChildReads: reads}) { | 444 | if reports.Push(ports.OutputPresented{Output: opts.Name, Seen: seen, ChildReads: reads}) { | ||
| 457 | mergedFlips++ | 445 | mergedFlips++ | ||
| 458 | } | 446 | } | ||
| 459 | - | if reports.Drain(ctx, opts.Presented) != nil { | 447 | + | reports.Flush(opts.Presented) |
| 460 | - | return nil | |||
| 461 | - | } | |||
| 462 | } | 448 | } | ||
| 463 | } | 449 | } | ||
| 464 | if err := pipeline.WaitHidden(ctx); err != nil { | 450 | if err := pipeline.WaitHidden(ctx); err != nil { | ||
| @@ -627,8 +613,7 @@ func writePNGSecure(path string, img image.Image, security ports.SessionSecurity | |||||
| 627 | } | 613 | } | ||
| 628 | 614 | ||||
| 629 | // report queues what the output read, with flip for a drawn frame, and | 615 | // report queues what the output read, with flip for a drawn frame, and | ||
| 630 | - | // sends what the channel takes now; the rest is retried soon. | 616 | + | // pushes it to the sink. It reports whether a flip was merged. |
| 631 | - | // It reports whether a flip was merged because wayland was not reading. | |||
| 632 | func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[ports.WindowID]uint64) (merged bool) { | 617 | func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[ports.WindowID]uint64) (merged bool) { | ||
| 633 | if opts.Presented == nil { | 618 | if opts.Presented == nil { | ||
| 634 | return false | 619 | return false | ||
| @@ -640,13 +625,6 @@ func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[po | |||||
| 640 | return merged | 625 | return merged | ||
| 641 | } | 626 | } | ||
| 642 | 627 | ||||
| 643 | - | // flush retries reports the channel could not take. | |||
| 644 | - | func (opts Options) flush(q *presented.Queue) { | |||
| 645 | - | if opts.Presented != nil { | |||
| 646 | - | q.Flush(opts.Presented) | |||
| 647 | - | } | |||
| 648 | - | } | |||
| 649 | - | ||||
| 650 | // flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC | 628 | // flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC | ||
| 651 | // time (software clock, refresh unknown), so presentation feedback and | 629 | // time (software clock, refresh unknown), so presentation feedback and | ||
| 652 | // frame callbacks follow headless frames. | 630 | // frame callbacks follow headless frames. | ||
A internal/adapters/headless/presented_sink_test.go +25 -0
| @@ -0,0 +1,25 @@ | |||
| 1 | + | package headless | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |
| 8 | + | "github.com/stretchr/testify/mock" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // presentedSink is a generated PresentationSink mock that records every push | |
| 12 | + | // on the returned channel. A full channel drops the report: Push never blocks, | |
| 13 | + | // as the port requires. | |
| 14 | + | func presentedSink(t *testing.T, size int) (ports.PresentationSink, chan ports.OutputPresented) { | |
| 15 | + | t.Helper() | |
| 16 | + | ch := make(chan ports.OutputPresented, size) | |
| 17 | + | sink := portsmocks.NewMockPresentationSink(t) | |
| 18 | + | sink.EXPECT().Push(mock.Anything).RunAndReturn(func(r ports.OutputPresented) { | |
| 19 | + | select { | |
| 20 | + | case ch <- r: | |
| 21 | + | default: | |
| 22 | + | } | |
| 23 | + | }).Maybe() | |
| 24 | + | return sink, ch | |
| 25 | + | } | |
| @@ -0,0 +1,25 @@ | |||||
| 1 | + | package headless | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | + | "github.com/stretchr/testify/mock" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // presentedSink is a generated PresentationSink mock that records every push | |||
| 12 | + | // on the returned channel. A full channel drops the report: Push never blocks, | |||
| 13 | + | // as the port requires. | |||
| 14 | + | func presentedSink(t *testing.T, size int) (ports.PresentationSink, chan ports.OutputPresented) { | |||
| 15 | + | t.Helper() | |||
| 16 | + | ch := make(chan ports.OutputPresented, size) | |||
| 17 | + | sink := portsmocks.NewMockPresentationSink(t) | |||
| 18 | + | sink.EXPECT().Push(mock.Anything).RunAndReturn(func(r ports.OutputPresented) { | |||
| 19 | + | select { | |||
| 20 | + | case ch <- r: | |||
| 21 | + | default: | |||
| 22 | + | } | |||
| 23 | + | }).Maybe() | |||
| 24 | + | return sink, ch | |||
| 25 | + | } | |||
M internal/adapters/headless/security_exclude_fence_test.go +2 -2
| @@ -48,7 +48,7 @@ func TestExcludeFenceRetainsReadsAcrossEngageAndOff(t *testing.T) { | |||
| 48 | 48 | scenes := make(chan ports.Scene, 2) | |
| 49 | 49 | contents := make(chan ports.SurfaceContent, 1) | |
| 50 | 50 | incoming := make(chan ports.CaptureRequest) | |
| 51 | - | presented := make(chan ports.OutputPresented, 8) | |
| 51 | + | sink, presented := presentedSink(t, 8) | |
| 52 | 52 | proofs := make(chan ports.SecurityBackendEvent, 2) | |
| 53 | 53 | replies := make(chan ports.CaptureDone, 2) | |
| 54 | 54 | changes := make(chan ports.SecurityState, 1) | |
| @@ -56,7 +56,7 @@ func TestExcludeFenceRetainsReadsAcrossEngageAndOff(t *testing.T) { | |||
| 56 | 56 | defer cancel() | |
| 57 | 57 | done := make(chan error, 1) | |
| 58 | 58 | go func() { | |
| 59 | - | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, SecurityEvents: proofs, Instance: 7, Width: 2, Height: 2, Presented: presented, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents, nil, incoming) | |
| 59 | + | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, SecurityEvents: proofs, Instance: 7, Width: 2, Height: 2, Presented: sink, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents, nil, incoming) | |
| 60 | 60 | }() | |
| 61 | 61 | dst := sessionCaptureFile(t) | |
| 62 | 62 | incoming <- ports.CaptureRequest{ID: 99, Exclude: true, Session: 7, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Dst: dst} | |
| @@ -48,7 +48,7 @@ func TestExcludeFenceRetainsReadsAcrossEngageAndOff(t *testing.T) { | |||||
| 48 | scenes := make(chan ports.Scene, 2) | 48 | scenes := make(chan ports.Scene, 2) | ||
| 49 | contents := make(chan ports.SurfaceContent, 1) | 49 | contents := make(chan ports.SurfaceContent, 1) | ||
| 50 | incoming := make(chan ports.CaptureRequest) | 50 | incoming := make(chan ports.CaptureRequest) | ||
| 51 | - | presented := make(chan ports.OutputPresented, 8) | 51 | + | sink, presented := presentedSink(t, 8) |
| 52 | proofs := make(chan ports.SecurityBackendEvent, 2) | 52 | proofs := make(chan ports.SecurityBackendEvent, 2) | ||
| 53 | replies := make(chan ports.CaptureDone, 2) | 53 | replies := make(chan ports.CaptureDone, 2) | ||
| 54 | changes := make(chan ports.SecurityState, 1) | 54 | changes := make(chan ports.SecurityState, 1) | ||
| @@ -56,7 +56,7 @@ func TestExcludeFenceRetainsReadsAcrossEngageAndOff(t *testing.T) { | |||||
| 56 | defer cancel() | 56 | defer cancel() | ||
| 57 | done := make(chan error, 1) | 57 | done := make(chan error, 1) | ||
| 58 | go func() { | 58 | go func() { | ||
| 59 | - | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, SecurityEvents: proofs, Instance: 7, Width: 2, Height: 2, Presented: presented, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents, nil, incoming) | 59 | + | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, SecurityEvents: proofs, Instance: 7, Width: 2, Height: 2, Presented: sink, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents, nil, incoming) |
| 60 | }() | 60 | }() | ||
| 61 | dst := sessionCaptureFile(t) | 61 | dst := sessionCaptureFile(t) | ||
| 62 | incoming <- ports.CaptureRequest{ID: 99, Exclude: true, Session: 7, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Dst: dst} | 62 | incoming <- ports.CaptureRequest{ID: 99, Exclude: true, Session: 7, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Dst: dst} | ||
M internal/adapters/headless/security_test.go +4 -4
| @@ -146,11 +146,11 @@ func TestSecurityChangesWakeIdleAndRejectQueuedDesktop(t *testing.T) { | |||
| 146 | 146 | scenes <- ports.Scene{Seq: 1, Windows: []ports.SceneWindow{{ID: 1}}} | |
| 147 | 147 | changes := make(chan ports.SecurityState, 1) | |
| 148 | 148 | proofs := make(chan ports.SecurityBackendEvent, 2) | |
| 149 | - | presented := make(chan ports.OutputPresented, 4) | |
| 149 | + | sink, presented := presentedSink(t, 4) | |
| 150 | 150 | ctx, cancel := context.WithCancel(context.Background()) | |
| 151 | 151 | done := make(chan error, 1) | |
| 152 | 152 | go func() { | |
| 153 | - | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, Instance: 3, SecurityEvents: proofs, Presented: presented, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil) | |
| 153 | + | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, Instance: 3, SecurityEvents: proofs, Presented: sink, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil) | |
| 154 | 154 | }() | |
| 155 | 155 | t.Cleanup(func() { | |
| 156 | 156 | cancel() | |
| @@ -378,13 +378,13 @@ func TestSecurityOffOutputProofDoesNotRender(t *testing.T) { | |||
| 378 | 378 | r.EXPECT().Close().Return().Once() | |
| 379 | 379 | scenes := make(chan ports.Scene, 1) | |
| 380 | 380 | scenes <- ports.Scene{Off: true} | |
| 381 | - | presented := make(chan ports.OutputPresented, 1) | |
| 381 | + | sink, presented := presentedSink(t, 1) | |
| 382 | 382 | changes := make(chan ports.SecurityState, 1) | |
| 383 | 383 | proofs := make(chan ports.SecurityBackendEvent, 1) | |
| 384 | 384 | ctx, cancel := context.WithCancel(context.Background()) | |
| 385 | 385 | done := make(chan error, 1) | |
| 386 | 386 | go func() { | |
| 387 | - | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, SecurityEvents: proofs, Instance: 12, Presented: presented, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil) | |
| 387 | + | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, SecurityEvents: proofs, Instance: 12, Presented: sink, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil) | |
| 388 | 388 | }() | |
| 389 | 389 | securityReceive(t, presented) // Off has actually been applied by the owner. | |
| 390 | 390 | locked := ports.SecurityState{Generation: 4, Protected: true} | |
| @@ -146,11 +146,11 @@ func TestSecurityChangesWakeIdleAndRejectQueuedDesktop(t *testing.T) { | |||||
| 146 | scenes <- ports.Scene{Seq: 1, Windows: []ports.SceneWindow{{ID: 1}}} | 146 | scenes <- ports.Scene{Seq: 1, Windows: []ports.SceneWindow{{ID: 1}}} | ||
| 147 | changes := make(chan ports.SecurityState, 1) | 147 | changes := make(chan ports.SecurityState, 1) | ||
| 148 | proofs := make(chan ports.SecurityBackendEvent, 2) | 148 | proofs := make(chan ports.SecurityBackendEvent, 2) | ||
| 149 | - | presented := make(chan ports.OutputPresented, 4) | 149 | + | sink, presented := presentedSink(t, 4) |
| 150 | ctx, cancel := context.WithCancel(context.Background()) | 150 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 151 | done := make(chan error, 1) | 151 | done := make(chan error, 1) | ||
| 152 | go func() { | 152 | go func() { | ||
| 153 | - | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, Instance: 3, SecurityEvents: proofs, Presented: presented, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil) | 153 | + | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, Instance: 3, SecurityEvents: proofs, Presented: sink, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil) |
| 154 | }() | 154 | }() | ||
| 155 | t.Cleanup(func() { | 155 | t.Cleanup(func() { | ||
| 156 | cancel() | 156 | cancel() | ||
| @@ -378,13 +378,13 @@ func TestSecurityOffOutputProofDoesNotRender(t *testing.T) { | |||||
| 378 | r.EXPECT().Close().Return().Once() | 378 | r.EXPECT().Close().Return().Once() | ||
| 379 | scenes := make(chan ports.Scene, 1) | 379 | scenes := make(chan ports.Scene, 1) | ||
| 380 | scenes <- ports.Scene{Off: true} | 380 | scenes <- ports.Scene{Off: true} | ||
| 381 | - | presented := make(chan ports.OutputPresented, 1) | 381 | + | sink, presented := presentedSink(t, 1) |
| 382 | changes := make(chan ports.SecurityState, 1) | 382 | changes := make(chan ports.SecurityState, 1) | ||
| 383 | proofs := make(chan ports.SecurityBackendEvent, 1) | 383 | proofs := make(chan ports.SecurityBackendEvent, 1) | ||
| 384 | ctx, cancel := context.WithCancel(context.Background()) | 384 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 385 | done := make(chan error, 1) | 385 | done := make(chan error, 1) | ||
| 386 | go func() { | 386 | go func() { | ||
| 387 | - | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, SecurityEvents: proofs, Instance: 12, Presented: presented, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil) | 387 | + | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, SecurityEvents: proofs, Instance: 12, Presented: sink, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil) |
| 388 | }() | 388 | }() | ||
| 389 | securityReceive(t, presented) // Off has actually been applied by the owner. | 389 | securityReceive(t, presented) // Off has actually been applied by the owner. | ||
| 390 | locked := ports.SecurityState{Generation: 4, Protected: true} | 390 | locked := ports.SecurityState{Generation: 4, Protected: true} | ||
M internal/adapters/outputkit/presented/queue.go +9 -27
| @@ -3,7 +3,6 @@ | |||
| 3 | 3 | package presented | |
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | - | "context" | |
| 7 | 6 | "maps" | |
| 8 | 7 | "slices" | |
| 9 | 8 | ||
| @@ -48,15 +47,15 @@ func (q *Queue) Push(r ports.OutputPresented) (merged bool) { | |||
| 48 | 47 | return false | |
| 49 | 48 | } | |
| 50 | 49 | if old, next := q.unsent[0].Flip, q.unsent[1].Flip; next != nil && old != nil { | |
| 51 | - | mergeFlip(old, next) | |
| 50 | + | Merge(old, next) | |
| 52 | 51 | } | |
| 53 | 52 | q.unsent = slices.Delete(q.unsent, 0, 1) | |
| 54 | 53 | return true | |
| 55 | 54 | } | |
| 56 | 55 | ||
| 57 | - | // mergeFlip folds old into next: next counts it as merged and shows what | |
| 56 | + | // Merge folds old into next: next counts it as merged and shows what | |
| 58 | 57 | // old showed, so feedbacks waiting on old are answered (as discarded). | |
| 59 | - | func mergeFlip(old, next *ports.FlipInfo) { | |
| 58 | + | func Merge(old, next *ports.FlipInfo) { | |
| 60 | 59 | next.Merged += 1 + old.Merged | |
| 61 | 60 | // A snapshot can be shared with previously sent flips. Clone only when | |
| 62 | 61 | // a merge actually needs to add an absent window. | |
| @@ -76,30 +75,13 @@ func mergeFlip(old, next *ports.FlipInfo) { | |||
| 76 | 75 | } | |
| 77 | 76 | } | |
| 78 | 77 | ||
| 79 | - | // Flush sends queued reports without blocking. | |
| 80 | - | func (q *Queue) Flush(ch chan<- ports.OutputPresented) { | |
| 81 | - | for len(q.unsent) > 0 { | |
| 82 | - | select { | |
| 83 | - | case ch <- q.unsent[0]: | |
| 84 | - | q.unsent = slices.Delete(q.unsent, 0, 1) | |
| 85 | - | default: | |
| 86 | - | return | |
| 87 | - | } | |
| 88 | - | } | |
| 89 | - | } | |
| 90 | - | ||
| 91 | - | // Drain sends every queued report, blocking until wayland reads them or ctx | |
| 92 | - | // ends. | |
| 93 | - | func (q *Queue) Drain(ctx context.Context, ch chan<- ports.OutputPresented) error { | |
| 94 | - | for len(q.unsent) > 0 { | |
| 95 | - | select { | |
| 96 | - | case ch <- q.unsent[0]: | |
| 97 | - | q.unsent = slices.Delete(q.unsent, 0, 1) | |
| 98 | - | case <-ctx.Done(): | |
| 99 | - | return ctx.Err() | |
| 100 | - | } | |
| 78 | + | // Flush pushes every queued report to sink, oldest first, and empties the | |
| 79 | + | // queue. Push never blocks. | |
| 80 | + | func (q *Queue) Flush(sink ports.PresentationSink) { | |
| 81 | + | for _, r := range q.unsent { | |
| 82 | + | sink.Push(r) | |
| 101 | 83 | } | |
| 102 | - | return nil | |
| 84 | + | q.Reset() | |
| 103 | 85 | } | |
| 104 | 86 | ||
| 105 | 87 | // Len is the number of queued reports. | |
| @@ -3,7 +3,6 @@ | |||||
| 3 | package presented | 3 | package presented | ||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | - | "context" | |||
| 7 | "maps" | 6 | "maps" | ||
| 8 | "slices" | 7 | "slices" | ||
| 9 | 8 | ||||
| @@ -48,15 +47,15 @@ func (q *Queue) Push(r ports.OutputPresented) (merged bool) { | |||||
| 48 | return false | 47 | return false | ||
| 49 | } | 48 | } | ||
| 50 | if old, next := q.unsent[0].Flip, q.unsent[1].Flip; next != nil && old != nil { | 49 | if old, next := q.unsent[0].Flip, q.unsent[1].Flip; next != nil && old != nil { | ||
| 51 | - | mergeFlip(old, next) | 50 | + | Merge(old, next) |
| 52 | } | 51 | } | ||
| 53 | q.unsent = slices.Delete(q.unsent, 0, 1) | 52 | q.unsent = slices.Delete(q.unsent, 0, 1) | ||
| 54 | return true | 53 | return true | ||
| 55 | } | 54 | } | ||
| 56 | 55 | ||||
| 57 | - | // mergeFlip folds old into next: next counts it as merged and shows what | 56 | + | // Merge folds old into next: next counts it as merged and shows what |
| 58 | // old showed, so feedbacks waiting on old are answered (as discarded). | 57 | // old showed, so feedbacks waiting on old are answered (as discarded). | ||
| 59 | - | func mergeFlip(old, next *ports.FlipInfo) { | 58 | + | func Merge(old, next *ports.FlipInfo) { |
| 60 | next.Merged += 1 + old.Merged | 59 | next.Merged += 1 + old.Merged | ||
| 61 | // A snapshot can be shared with previously sent flips. Clone only when | 60 | // A snapshot can be shared with previously sent flips. Clone only when | ||
| 62 | // a merge actually needs to add an absent window. | 61 | // a merge actually needs to add an absent window. | ||
| @@ -76,30 +75,13 @@ func mergeFlip(old, next *ports.FlipInfo) { | |||||
| 76 | } | 75 | } | ||
| 77 | } | 76 | } | ||
| 78 | 77 | ||||
| 79 | - | // Flush sends queued reports without blocking. | 78 | + | // Flush pushes every queued report to sink, oldest first, and empties the |
| 80 | - | func (q *Queue) Flush(ch chan<- ports.OutputPresented) { | 79 | + | // queue. Push never blocks. |
| 81 | - | for len(q.unsent) > 0 { | 80 | + | func (q *Queue) Flush(sink ports.PresentationSink) { |
| 82 | - | select { | 81 | + | for _, r := range q.unsent { |
| 83 | - | case ch <- q.unsent[0]: | 82 | + | sink.Push(r) |
| 84 | - | q.unsent = slices.Delete(q.unsent, 0, 1) | |||
| 85 | - | default: | |||
| 86 | - | return | |||
| 87 | - | } | |||
| 88 | - | } | |||
| 89 | - | } | |||
| 90 | - | ||||
| 91 | - | // Drain sends every queued report, blocking until wayland reads them or ctx | |||
| 92 | - | // ends. | |||
| 93 | - | func (q *Queue) Drain(ctx context.Context, ch chan<- ports.OutputPresented) error { | |||
| 94 | - | for len(q.unsent) > 0 { | |||
| 95 | - | select { | |||
| 96 | - | case ch <- q.unsent[0]: | |||
| 97 | - | q.unsent = slices.Delete(q.unsent, 0, 1) | |||
| 98 | - | case <-ctx.Done(): | |||
| 99 | - | return ctx.Err() | |||
| 100 | - | } | |||
| 101 | } | 83 | } | ||
| 102 | - | return nil | 84 | + | q.Reset() |
| 103 | } | 85 | } | ||
| 104 | 86 | ||||
| 105 | // Len is the number of queued reports. | 87 | // Len is the number of queued reports. | ||
M internal/adapters/outputkit/presented/queue_test.go +12 -13
| @@ -1,11 +1,12 @@ | |||
| 1 | 1 | package presented | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "context" | |
| 5 | 4 | "reflect" | |
| 6 | 5 | "testing" | |
| 7 | 6 | ||
| 7 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |
| 8 | 8 | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | "github.com/stretchr/testify/mock" | |
| 9 | 10 | "github.com/stretchr/testify/require" | |
| 10 | 11 | ) | |
| 11 | 12 | ||
| @@ -75,24 +76,22 @@ func TestQueueMergesOldestFlipPastBound(t *testing.T) { | |||
| 75 | 76 | require.Equal(t, map[ports.WindowID]uint64{2: 5}, shared, "shared snapshot untouched") | |
| 76 | 77 | } | |
| 77 | 78 | ||
| 78 | - | func TestQueueFlushStopsWhenFull(t *testing.T) { | |
| 79 | + | func TestQueueFlushPushesInOrderAndEmpties(t *testing.T) { | |
| 79 | 80 | var q Queue | |
| 80 | 81 | q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: 1}}) | |
| 81 | 82 | q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: 2}}) | |
| 82 | - | ch := make(chan ports.OutputPresented, 1) | |
| 83 | - | q.Flush(ch) | |
| 84 | - | require.Equal(t, 1, q.Len()) | |
| 85 | - | require.Equal(t, uint64(1), (<-ch).Flip.Seq) | |
| 86 | - | require.NoError(t, q.Drain(context.Background(), ch)) | |
| 83 | + | var got []ports.OutputPresented | |
| 84 | + | sink := portsmocks.NewMockPresentationSink(t) | |
| 85 | + | sink.EXPECT().Push(mock.Anything).RunAndReturn(func(r ports.OutputPresented) { got = append(got, r) }) | |
| 86 | + | q.Flush(sink) | |
| 87 | 87 | require.Zero(t, q.Len()) | |
| 88 | - | require.Equal(t, uint64(2), (<-ch).Flip.Seq) | |
| 88 | + | require.Len(t, got, 2) | |
| 89 | + | require.Equal(t, uint64(1), got[0].Flip.Seq) | |
| 90 | + | require.Equal(t, uint64(2), got[1].Flip.Seq) | |
| 91 | + | q.Flush(sink) // empty: nothing more is pushed | |
| 92 | + | require.Len(t, got, 2) | |
| 89 | 93 | ||
| 90 | 94 | q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: 3}}) | |
| 91 | - | q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: 4}}) | |
| 92 | - | ctx, cancel := context.WithCancel(context.Background()) | |
| 93 | - | cancel() | |
| 94 | - | ch <- ports.OutputPresented{} | |
| 95 | - | require.ErrorIs(t, q.Drain(ctx, ch), context.Canceled) | |
| 96 | 95 | q.Reset() | |
| 97 | 96 | require.Zero(t, q.Len()) | |
| 98 | 97 | } | |
| @@ -1,11 +1,12 @@ | |||||
| 1 | package presented | 1 | package presented | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | - | "context" | |||
| 5 | "reflect" | 4 | "reflect" | ||
| 6 | "testing" | 5 | "testing" | ||
| 7 | 6 | ||||
| 7 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |||
| 8 | "github.com/bnema/neferwl/internal/ports" | 8 | "github.com/bnema/neferwl/internal/ports" | ||
| 9 | + | "github.com/stretchr/testify/mock" | |||
| 9 | "github.com/stretchr/testify/require" | 10 | "github.com/stretchr/testify/require" | ||
| 10 | ) | 11 | ) | ||
| 11 | 12 | ||||
| @@ -75,24 +76,22 @@ func TestQueueMergesOldestFlipPastBound(t *testing.T) { | |||||
| 75 | require.Equal(t, map[ports.WindowID]uint64{2: 5}, shared, "shared snapshot untouched") | 76 | require.Equal(t, map[ports.WindowID]uint64{2: 5}, shared, "shared snapshot untouched") | ||
| 76 | } | 77 | } | ||
| 77 | 78 | ||||
| 78 | - | func TestQueueFlushStopsWhenFull(t *testing.T) { | 79 | + | func TestQueueFlushPushesInOrderAndEmpties(t *testing.T) { |
| 79 | var q Queue | 80 | var q Queue | ||
| 80 | q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: 1}}) | 81 | q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: 1}}) | ||
| 81 | q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: 2}}) | 82 | q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: 2}}) | ||
| 82 | - | ch := make(chan ports.OutputPresented, 1) | 83 | + | var got []ports.OutputPresented |
| 83 | - | q.Flush(ch) | 84 | + | sink := portsmocks.NewMockPresentationSink(t) |
| 84 | - | require.Equal(t, 1, q.Len()) | 85 | + | sink.EXPECT().Push(mock.Anything).RunAndReturn(func(r ports.OutputPresented) { got = append(got, r) }) |
| 85 | - | require.Equal(t, uint64(1), (<-ch).Flip.Seq) | 86 | + | q.Flush(sink) |
| 86 | - | require.NoError(t, q.Drain(context.Background(), ch)) | |||
| 87 | require.Zero(t, q.Len()) | 87 | require.Zero(t, q.Len()) | ||
| 88 | - | require.Equal(t, uint64(2), (<-ch).Flip.Seq) | 88 | + | require.Len(t, got, 2) |
| 89 | + | require.Equal(t, uint64(1), got[0].Flip.Seq) | |||
| 90 | + | require.Equal(t, uint64(2), got[1].Flip.Seq) | |||
| 91 | + | q.Flush(sink) // empty: nothing more is pushed | |||
| 92 | + | require.Len(t, got, 2) | |||
| 89 | 93 | ||||
| 90 | q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: 3}}) | 94 | q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: 3}}) | ||
| 91 | - | q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: 4}}) | |||
| 92 | - | ctx, cancel := context.WithCancel(context.Background()) | |||
| 93 | - | cancel() | |||
| 94 | - | ch <- ports.OutputPresented{} | |||
| 95 | - | require.ErrorIs(t, q.Drain(ctx, ch), context.Canceled) | |||
| 96 | q.Reset() | 95 | q.Reset() | ||
| 97 | require.Zero(t, q.Len()) | 96 | require.Zero(t, q.Len()) | ||
| 98 | } | 97 | } | ||
M internal/adapters/wayland/alloc_hotpath_test.go +2 -2
| @@ -24,7 +24,7 @@ func TestReleaseHeldAllocations(t *testing.T) { | |||
| 24 | 24 | } | |
| 25 | 25 | ||
| 26 | 26 | func TestCommitDamageAllocations(t *testing.T) { | |
| 27 | - | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)} | |
| 27 | + | srv := &Server{fifoSurfaces: map[*surface]struct{}{}} | |
| 28 | 28 | surf := &surface{server: srv} | |
| 29 | 29 | commit := func() { | |
| 30 | 30 | surf.Damage(nil, 0, 0, 4, 4) | |
| @@ -55,7 +55,7 @@ func TestCommitDamageAllocations(t *testing.T) { | |||
| 55 | 55 | func TestCommitSyncStorageIsPerUpdate(t *testing.T) { | |
| 56 | 56 | dev := newMocksyncobjDevice(t) | |
| 57 | 57 | dev.EXPECT().signalled(mock.Anything, mock.Anything).Return(true, nil) | |
| 58 | - | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)} | |
| 58 | + | srv := &Server{fifoSurfaces: map[*surface]struct{}{}} | |
| 59 | 59 | sw, err := newSyncWaiter(func() {}) | |
| 60 | 60 | if err != nil { | |
| 61 | 61 | t.Fatal(err) | |
| @@ -24,7 +24,7 @@ func TestReleaseHeldAllocations(t *testing.T) { | |||||
| 24 | } | 24 | } | ||
| 25 | 25 | ||||
| 26 | func TestCommitDamageAllocations(t *testing.T) { | 26 | func TestCommitDamageAllocations(t *testing.T) { | ||
| 27 | - | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)} | 27 | + | srv := &Server{fifoSurfaces: map[*surface]struct{}{}} |
| 28 | surf := &surface{server: srv} | 28 | surf := &surface{server: srv} | ||
| 29 | commit := func() { | 29 | commit := func() { | ||
| 30 | surf.Damage(nil, 0, 0, 4, 4) | 30 | surf.Damage(nil, 0, 0, 4, 4) | ||
| @@ -55,7 +55,7 @@ func TestCommitDamageAllocations(t *testing.T) { | |||||
| 55 | func TestCommitSyncStorageIsPerUpdate(t *testing.T) { | 55 | func TestCommitSyncStorageIsPerUpdate(t *testing.T) { | ||
| 56 | dev := newMocksyncobjDevice(t) | 56 | dev := newMocksyncobjDevice(t) | ||
| 57 | dev.EXPECT().signalled(mock.Anything, mock.Anything).Return(true, nil) | 57 | dev.EXPECT().signalled(mock.Anything, mock.Anything).Return(true, nil) | ||
| 58 | - | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)} | 58 | + | srv := &Server{fifoSurfaces: map[*surface]struct{}{}} |
| 59 | sw, err := newSyncWaiter(func() {}) | 59 | sw, err := newSyncWaiter(func() {}) | ||
| 60 | if err != nil { | 60 | if err != nil { | ||
| 61 | t.Fatal(err) | 61 | t.Fatal(err) | ||
M internal/adapters/wayland/commit_lifecycle_test.go +4 -4
| @@ -18,7 +18,7 @@ import ( | |||
| 18 | 18 | // Applying an older captured commit must never transiently own newer requests. | |
| 19 | 19 | // The next commit captures those requests without losing its one-shot state. | |
| 20 | 20 | func TestCapturedCommitKeepsLaterRequests(t *testing.T) { | |
| 21 | - | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1), constraints: map[*surface]*constraint{}} | |
| 21 | + | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, constraints: map[*surface]*constraint{}} | |
| 22 | 22 | surf := &surface{server: srv, xdg: &xdgSurface{}} | |
| 23 | 23 | cons := &constraint{surface: surf} | |
| 24 | 24 | srv.constraints[surf] = cons | |
| @@ -313,7 +313,7 @@ func TestQueuedDestructionDropsUpdates(t *testing.T) { | |||
| 313 | 313 | ||
| 314 | 314 | // A dropped update must be cleared before a different surface reuses it. | |
| 315 | 315 | func TestDroppedUpdateRecycledWithoutState(t *testing.T) { | |
| 316 | - | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)} | |
| 316 | + | srv := &Server{fifoSurfaces: map[*surface]struct{}{}} | |
| 317 | 317 | old := &surface{server: srv, next: pendingCommit{scale: 2, barrier: true, damage: []ports.Rect{{W: 2}}}} | |
| 318 | 318 | old.queueUpdate() | |
| 319 | 319 | old.destroyed = true | |
| @@ -329,7 +329,7 @@ func TestDroppedUpdateRecycledWithoutState(t *testing.T) { | |||
| 329 | 329 | // BenchmarkEmptySurfaceQueueApply measures an empty, roleless surface's | |
| 330 | 330 | // in-process queue/apply loop. It excludes protocol dispatch and real buffers. | |
| 331 | 331 | func BenchmarkEmptySurfaceQueueApply(b *testing.B) { | |
| 332 | - | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)} | |
| 332 | + | srv := &Server{fifoSurfaces: map[*surface]struct{}{}} | |
| 333 | 333 | surf := &surface{server: srv} | |
| 334 | 334 | surf.queueUpdate() | |
| 335 | 335 | surf.queue[0].applyGraph() | |
| @@ -381,7 +381,7 @@ func TestCapturedViewportApplyAllocations(t *testing.T) { | |||
| 381 | 381 | } | |
| 382 | 382 | ||
| 383 | 383 | func TestCapturedCommitApplyAllocations(t *testing.T) { | |
| 384 | - | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)} | |
| 384 | + | srv := &Server{fifoSurfaces: map[*surface]struct{}{}} | |
| 385 | 385 | surf := &surface{server: srv} | |
| 386 | 386 | apply := func() { | |
| 387 | 387 | surf.queueUpdate() | |
| @@ -18,7 +18,7 @@ import ( | |||||
| 18 | // Applying an older captured commit must never transiently own newer requests. | 18 | // Applying an older captured commit must never transiently own newer requests. | ||
| 19 | // The next commit captures those requests without losing its one-shot state. | 19 | // The next commit captures those requests without losing its one-shot state. | ||
| 20 | func TestCapturedCommitKeepsLaterRequests(t *testing.T) { | 20 | func TestCapturedCommitKeepsLaterRequests(t *testing.T) { | ||
| 21 | - | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1), constraints: map[*surface]*constraint{}} | 21 | + | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, constraints: map[*surface]*constraint{}} |
| 22 | surf := &surface{server: srv, xdg: &xdgSurface{}} | 22 | surf := &surface{server: srv, xdg: &xdgSurface{}} | ||
| 23 | cons := &constraint{surface: surf} | 23 | cons := &constraint{surface: surf} | ||
| 24 | srv.constraints[surf] = cons | 24 | srv.constraints[surf] = cons | ||
| @@ -313,7 +313,7 @@ func TestQueuedDestructionDropsUpdates(t *testing.T) { | |||||
| 313 | 313 | ||||
| 314 | // A dropped update must be cleared before a different surface reuses it. | 314 | // A dropped update must be cleared before a different surface reuses it. | ||
| 315 | func TestDroppedUpdateRecycledWithoutState(t *testing.T) { | 315 | func TestDroppedUpdateRecycledWithoutState(t *testing.T) { | ||
| 316 | - | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)} | 316 | + | srv := &Server{fifoSurfaces: map[*surface]struct{}{}} |
| 317 | old := &surface{server: srv, next: pendingCommit{scale: 2, barrier: true, damage: []ports.Rect{{W: 2}}}} | 317 | old := &surface{server: srv, next: pendingCommit{scale: 2, barrier: true, damage: []ports.Rect{{W: 2}}}} | ||
| 318 | old.queueUpdate() | 318 | old.queueUpdate() | ||
| 319 | old.destroyed = true | 319 | old.destroyed = true | ||
| @@ -329,7 +329,7 @@ func TestDroppedUpdateRecycledWithoutState(t *testing.T) { | |||||
| 329 | // BenchmarkEmptySurfaceQueueApply measures an empty, roleless surface's | 329 | // BenchmarkEmptySurfaceQueueApply measures an empty, roleless surface's | ||
| 330 | // in-process queue/apply loop. It excludes protocol dispatch and real buffers. | 330 | // in-process queue/apply loop. It excludes protocol dispatch and real buffers. | ||
| 331 | func BenchmarkEmptySurfaceQueueApply(b *testing.B) { | 331 | func BenchmarkEmptySurfaceQueueApply(b *testing.B) { | ||
| 332 | - | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)} | 332 | + | srv := &Server{fifoSurfaces: map[*surface]struct{}{}} |
| 333 | surf := &surface{server: srv} | 333 | surf := &surface{server: srv} | ||
| 334 | surf.queueUpdate() | 334 | surf.queueUpdate() | ||
| 335 | surf.queue[0].applyGraph() | 335 | surf.queue[0].applyGraph() | ||
| @@ -381,7 +381,7 @@ func TestCapturedViewportApplyAllocations(t *testing.T) { | |||||
| 381 | } | 381 | } | ||
| 382 | 382 | ||||
| 383 | func TestCapturedCommitApplyAllocations(t *testing.T) { | 383 | func TestCapturedCommitApplyAllocations(t *testing.T) { | ||
| 384 | - | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)} | 384 | + | srv := &Server{fifoSurfaces: map[*surface]struct{}{}} |
| 385 | surf := &surface{server: srv} | 385 | surf := &surface{server: srv} | ||
| 386 | apply := func() { | 386 | apply := func() { | ||
| 387 | surf.queueUpdate() | 387 | surf.queueUpdate() | ||
M internal/adapters/wayland/fifo.go +9 -6
| @@ -757,8 +757,7 @@ func sameBuffer(a, b *wayland.Buffer) bool { | |||
| 757 | 757 | ||
| 758 | 758 | // wakePacer makes the pacer look at its queues. Display goroutine only. | |
| 759 | 759 | // While a commit or a tick runs (paceDepth) it only records the request: the | |
| 760 | - | // outermost caller then ticks inline (paceNow), saving the round trip of | |
| 761 | - | // waking the pacer goroutine just so that it calls Do back. | |
| 760 | + | // outermost caller then ticks inline (paceNow), saving a posted tick. | |
| 762 | 761 | func (s *Server) wakePacer() { | |
| 763 | 762 | if s.paceDepth > 0 { | |
| 764 | 763 | s.paceDirty = true | |
| @@ -767,10 +766,14 @@ func (s *Server) wakePacer() { | |||
| 767 | 766 | s.signalPacer() | |
| 768 | 767 | } | |
| 769 | 768 | ||
| 770 | - | // signalPacer wakes the pacer goroutine; safe from any goroutine. | |
| 769 | + | // signalPacer posts a tick to the display loop; safe from any goroutine. | |
| 770 | + | // Signals coalesce until the tick runs. | |
| 771 | 771 | func (s *Server) signalPacer() { | |
| 772 | - | select { | |
| 773 | - | case s.frameReady <- struct{}{}: | |
| 774 | - | default: | |
| 772 | + | if !s.paceSignal.CompareAndSwap(false, true) { | |
| 773 | + | return | |
| 774 | + | } | |
| 775 | + | if s.display == nil { // unit tests that build a Server without a display | |
| 776 | + | return | |
| 775 | 777 | } | |
| 778 | + | s.display.Post(s.paceSignalFn) | |
| 776 | 779 | } | |
| @@ -757,8 +757,7 @@ func sameBuffer(a, b *wayland.Buffer) bool { | |||||
| 757 | 757 | ||||
| 758 | // wakePacer makes the pacer look at its queues. Display goroutine only. | 758 | // wakePacer makes the pacer look at its queues. Display goroutine only. | ||
| 759 | // While a commit or a tick runs (paceDepth) it only records the request: the | 759 | // While a commit or a tick runs (paceDepth) it only records the request: the | ||
| 760 | - | // outermost caller then ticks inline (paceNow), saving the round trip of | 760 | + | // outermost caller then ticks inline (paceNow), saving a posted tick. |
| 761 | - | // waking the pacer goroutine just so that it calls Do back. | |||
| 762 | func (s *Server) wakePacer() { | 761 | func (s *Server) wakePacer() { | ||
| 763 | if s.paceDepth > 0 { | 762 | if s.paceDepth > 0 { | ||
| 764 | s.paceDirty = true | 763 | s.paceDirty = true | ||
| @@ -767,10 +766,14 @@ func (s *Server) wakePacer() { | |||||
| 767 | s.signalPacer() | 766 | s.signalPacer() | ||
| 768 | } | 767 | } | ||
| 769 | 768 | ||||
| 770 | - | // signalPacer wakes the pacer goroutine; safe from any goroutine. | 769 | + | // signalPacer posts a tick to the display loop; safe from any goroutine. |
| 770 | + | // Signals coalesce until the tick runs. | |||
| 771 | func (s *Server) signalPacer() { | 771 | func (s *Server) signalPacer() { | ||
| 772 | - | select { | 772 | + | if !s.paceSignal.CompareAndSwap(false, true) { |
| 773 | - | case s.frameReady <- struct{}{}: | 773 | + | return |
| 774 | - | default: | 774 | + | } |
| 775 | + | if s.display == nil { // unit tests that build a Server without a display | |||
| 776 | + | return | |||
| 775 | } | 777 | } | ||
| 778 | + | s.display.Post(s.paceSignalFn) | |||
| 776 | } | 779 | } | ||
M internal/adapters/wayland/fifo_test.go +3 -3
| @@ -154,7 +154,7 @@ func TestMonotonicTimeClamps(t *testing.T) { | |||
| 154 | 154 | // A barrier on an output that flips clears on its next flip. | |
| 155 | 155 | func TestTickFifoClearsBarrierOnFlip(t *testing.T) { | |
| 156 | 156 | out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}} | |
| 157 | - | s := &Server{outputs: []*output{out}, fifoSurfaces: map[*surface]struct{}{}, lastFlip: map[string]time.Time{}, frameReady: make(chan struct{}, 1)} | |
| 157 | + | s := &Server{outputs: []*output{out}, fifoSurfaces: map[*surface]struct{}{}, lastFlip: map[string]time.Time{}} | |
| 158 | 158 | surf := &surface{server: s, xdg: &xdgSurface{window: &window{last: ports.ConfigureWindow{Output: "DP-2"}}}} | |
| 159 | 159 | now := time.Unix(100, 0) | |
| 160 | 160 | s.lastFlip["DP-2"] = now.Add(-time.Millisecond) | |
| @@ -217,7 +217,7 @@ func TestQueuedParentRetainsRetiredChild(t *testing.T) { | |||
| 217 | 217 | name = "dropped" | |
| 218 | 218 | } | |
| 219 | 219 | t.Run(name, func(t *testing.T) { | |
| 220 | - | srv := &Server{fifoSurfaces: make(map[*surface]struct{}), frameReady: make(chan struct{}, 1)} | |
| 220 | + | srv := &Server{fifoSurfaces: make(map[*surface]struct{})} | |
| 221 | 221 | parent := &surface{server: srv} | |
| 222 | 222 | child := &surface{server: srv} | |
| 223 | 223 | child.sub.parent, child.sub.synced = parent, true | |
| @@ -277,7 +277,7 @@ func TestGraphReadyMemoizesPredecessors(t *testing.T) { | |||
| 277 | 277 | ||
| 278 | 278 | func TestInvisibleFifoPeriod(t *testing.T) { | |
| 279 | 279 | out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}} | |
| 280 | - | s := &Server{outputs: []*output{out}, fifoSurfaces: map[*surface]struct{}{}, lastFlip: map[string]time.Time{}, frameReady: make(chan struct{}, 1)} | |
| 280 | + | s := &Server{outputs: []*output{out}, fifoSurfaces: map[*surface]struct{}{}, lastFlip: map[string]time.Time{}} | |
| 281 | 281 | w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2"}} | |
| 282 | 282 | surf := &surface{server: s, xdg: &xdgSurface{window: w}} | |
| 283 | 283 | now := time.Unix(100, 0) | |
| @@ -154,7 +154,7 @@ func TestMonotonicTimeClamps(t *testing.T) { | |||||
| 154 | // A barrier on an output that flips clears on its next flip. | 154 | // A barrier on an output that flips clears on its next flip. | ||
| 155 | func TestTickFifoClearsBarrierOnFlip(t *testing.T) { | 155 | func TestTickFifoClearsBarrierOnFlip(t *testing.T) { | ||
| 156 | out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}} | 156 | out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}} | ||
| 157 | - | s := &Server{outputs: []*output{out}, fifoSurfaces: map[*surface]struct{}{}, lastFlip: map[string]time.Time{}, frameReady: make(chan struct{}, 1)} | 157 | + | s := &Server{outputs: []*output{out}, fifoSurfaces: map[*surface]struct{}{}, lastFlip: map[string]time.Time{}} |
| 158 | surf := &surface{server: s, xdg: &xdgSurface{window: &window{last: ports.ConfigureWindow{Output: "DP-2"}}}} | 158 | surf := &surface{server: s, xdg: &xdgSurface{window: &window{last: ports.ConfigureWindow{Output: "DP-2"}}}} | ||
| 159 | now := time.Unix(100, 0) | 159 | now := time.Unix(100, 0) | ||
| 160 | s.lastFlip["DP-2"] = now.Add(-time.Millisecond) | 160 | s.lastFlip["DP-2"] = now.Add(-time.Millisecond) | ||
| @@ -217,7 +217,7 @@ func TestQueuedParentRetainsRetiredChild(t *testing.T) { | |||||
| 217 | name = "dropped" | 217 | name = "dropped" | ||
| 218 | } | 218 | } | ||
| 219 | t.Run(name, func(t *testing.T) { | 219 | t.Run(name, func(t *testing.T) { | ||
| 220 | - | srv := &Server{fifoSurfaces: make(map[*surface]struct{}), frameReady: make(chan struct{}, 1)} | 220 | + | srv := &Server{fifoSurfaces: make(map[*surface]struct{})} |
| 221 | parent := &surface{server: srv} | 221 | parent := &surface{server: srv} | ||
| 222 | child := &surface{server: srv} | 222 | child := &surface{server: srv} | ||
| 223 | child.sub.parent, child.sub.synced = parent, true | 223 | child.sub.parent, child.sub.synced = parent, true | ||
| @@ -277,7 +277,7 @@ func TestGraphReadyMemoizesPredecessors(t *testing.T) { | |||||
| 277 | 277 | ||||
| 278 | func TestInvisibleFifoPeriod(t *testing.T) { | 278 | func TestInvisibleFifoPeriod(t *testing.T) { | ||
| 279 | out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}} | 279 | out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}} | ||
| 280 | - | s := &Server{outputs: []*output{out}, fifoSurfaces: map[*surface]struct{}{}, lastFlip: map[string]time.Time{}, frameReady: make(chan struct{}, 1)} | 280 | + | s := &Server{outputs: []*output{out}, fifoSurfaces: map[*surface]struct{}{}, lastFlip: map[string]time.Time{}} |
| 281 | w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2"}} | 281 | w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2"}} | ||
| 282 | surf := &surface{server: s, xdg: &xdgSurface{window: w}} | 282 | surf := &surface{server: s, xdg: &xdgSurface{window: w}} | ||
| 283 | now := time.Unix(100, 0) | 283 | now := time.Unix(100, 0) | ||
M internal/adapters/wayland/frames.go +43 -63
| @@ -1,7 +1,6 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "context" | |
| 5 | 4 | "slices" | |
| 6 | 5 | "time" | |
| 7 | 6 | ||
| @@ -16,8 +15,10 @@ import ( | |||
| 16 | 15 | // frame. They are paced per output: on its page flip when it flips, else | |
| 17 | 16 | // at its refresh rate, so a 165 Hz output lets clients draw 165 frames a | |
| 18 | 17 | // second and a V-Sync game follows the screen. Cursors and unplaced | |
| 19 | - | // surfaces are paced at 60 Hz; invisible toplevel trees at 1 Hz. The | |
| 20 | - | // pacer sleeps while no callback waits. | |
| 18 | + | // surfaces are paced at 60 Hz; invisible toplevel trees at 1 Hz. Pacing runs | |
| 19 | + | // on the display loop: output reports arrive through the presentation sink | |
| 20 | + | // (presented.go) and deadlines through a timerfd; the timer sleeps while no | |
| 21 | + | // callback waits. | |
| 21 | 22 | ||
| 22 | 23 | // defaultFramePeriod paces surfaces without an output (60 Hz). | |
| 23 | 24 | const defaultFramePeriod = time.Second / 60 | |
| @@ -25,72 +26,51 @@ const suspendedFramePeriod = time.Second | |||
| 25 | 26 | const suspendedFrameQueue = "\x00suspended" // never a wl_output name | |
| 26 | 27 | ||
| 27 | 28 | // maxPacePasses bounds the passes one tick repeats while a pass queues | |
| 28 | - | // more work; the rest goes back to the pacer goroutine. | |
| 29 | + | // more work; the rest is posted to the display loop. | |
| 29 | 30 | const maxPacePasses = 3 | |
| 30 | 31 | ||
| 31 | - | // pace sleeps until a flip, a wake or the deadline armed by the last tick, | |
| 32 | - | // then runs a tick on the display goroutine. The timer is only armed from | |
| 33 | - | // ticks, which all run there, so a commit that ticks inline (paceNow) never | |
| 34 | - | // races the pacer for it. | |
| 35 | - | func (s *Server) pace(ctx context.Context) { | |
| 36 | - | timer := s.paceTimer // created by Run before the pacer starts | |
| 37 | - | defer timer.Stop() | |
| 38 | - | presented := s.channels.Presented | |
| 39 | - | if s.flipped == nil { | |
| 40 | - | s.flipped = make(map[string]bool) | |
| 41 | - | } | |
| 42 | - | // One method value for the whole loop: no closure per wake. | |
| 43 | - | tick := s.paceFromPacer | |
| 44 | - | for { | |
| 45 | - | clear(s.flipped) | |
| 46 | - | flipped := s.flipped | |
| 47 | - | s.frameReports = s.frameReports[:0] | |
| 48 | - | reports := s.frameReports | |
| 49 | - | select { | |
| 50 | - | case <-ctx.Done(): | |
| 51 | - | return | |
| 52 | - | case <-s.display.Stopped(): | |
| 53 | - | return | |
| 54 | - | case p := <-presented: | |
| 55 | - | flipped[p.Output] = flipped[p.Output] || paces(p) | |
| 56 | - | reports = append(reports, p) | |
| 57 | - | case <-s.frameReady: | |
| 58 | - | case <-timer.C: | |
| 59 | - | } | |
| 60 | - | // Coalesce flips queued while the display was busy: one burst of | |
| 61 | - | // callbacks per output, not one per stale flip. | |
| 62 | - | drain: | |
| 63 | - | for { | |
| 64 | - | select { | |
| 65 | - | case p := <-presented: | |
| 66 | - | flipped[p.Output] = flipped[p.Output] || paces(p) | |
| 67 | - | reports = append(reports, p) | |
| 68 | - | default: | |
| 69 | - | break drain | |
| 70 | - | } | |
| 71 | - | } | |
| 72 | - | s.frameReports = reports | |
| 73 | - | if !s.display.Do(tick) { | |
| 74 | - | return | |
| 75 | - | } | |
| 76 | - | } | |
| 77 | - | } | |
| 78 | - | ||
| 79 | - | // paceFromPacer runs the pacer's collected flips (flipped, frameReports) | |
| 80 | - | // on the display goroutine. | |
| 81 | - | func (s *Server) paceFromPacer() { s.paceTick(s.flipped, s.frameReports) } | |
| 82 | - | ||
| 83 | 32 | // paceNow runs a tick from code already on the display goroutine (a client | |
| 84 | - | // commit): the pacer goroutine would only call Do to get back here. Without | |
| 85 | - | // a running pacer (no timer) it falls back to waking it. | |
| 33 | + | // commit). Without a registered pace timer (unit tests that build a Server | |
| 34 | + | // without a display) it falls back to signalling a tick. | |
| 86 | 35 | func (s *Server) paceNow() { | |
| 87 | - | if s.paceTimer == nil { | |
| 36 | + | if !s.paceReady { | |
| 88 | 37 | s.signalPacer() | |
| 89 | 38 | return | |
| 90 | 39 | } | |
| 91 | 40 | s.paceTick(nil, nil) | |
| 92 | 41 | } | |
| 93 | 42 | ||
| 43 | + | // armPace schedules the next flip-less tick d from now on the pace timerfd | |
| 44 | + | // (nanosecond precision, one-shot). Display goroutine only. | |
| 45 | + | func (s *Server) armPace(d time.Duration) { | |
| 46 | + | if d <= 0 { | |
| 47 | + | d = time.Nanosecond | |
| 48 | + | } | |
| 49 | + | spec := unix.ItimerSpec{Value: unix.NsecToTimespec(int64(d))} | |
| 50 | + | _ = unix.TimerfdSettime(s.paceFD, 0, &spec, nil) | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // stopPace disarms the pace timerfd. Display goroutine only. | |
| 54 | + | func (s *Server) stopPace() { | |
| 55 | + | var spec unix.ItimerSpec | |
| 56 | + | _ = unix.TimerfdSettime(s.paceFD, 0, &spec, nil) | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // onPaceTimer is the display loop's callback for the pace timerfd: the | |
| 60 | + | // deadline armed by the last tick passed. | |
| 61 | + | func (s *Server) onPaceTimer() { | |
| 62 | + | var buf [8]byte | |
| 63 | + | _, _ = unix.Read(s.paceFD, buf[:]) // level-triggered: consume the expiry | |
| 64 | + | s.paceTick(nil, nil) | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | // paceSignalled is the posted tick: a wake (fence, overflow) made while the | |
| 68 | + | // display goroutine was elsewhere. | |
| 69 | + | func (s *Server) paceSignalled() { | |
| 70 | + | s.paceSignal.Store(false) | |
| 71 | + | s.paceTick(nil, nil) | |
| 72 | + | } | |
| 73 | + | ||
| 94 | 74 | // paceTick answers the flips in reports, fires the due frame callbacks, | |
| 95 | 75 | // releases buffers and applies the ready FIFO commits, then arms the timer | |
| 96 | 76 | // for the next deadline (stopped when nothing waits). Display goroutine only. | |
| @@ -109,7 +89,7 @@ func (s *Server) paceTick(flipped map[string]bool, reports []ports.OutputPresent | |||
| 109 | 89 | } | |
| 110 | 90 | // The pass queued work behind its own checks (applied commits | |
| 111 | 91 | // queue callbacks, released buffers): look again, as a flip-less | |
| 112 | - | // tick, instead of waking the pacer for it. | |
| 92 | + | // tick, instead of posting one. | |
| 113 | 93 | flipped, reports = nil, nil | |
| 114 | 94 | if pass+1 == maxPacePasses { | |
| 115 | 95 | s.paceDirty = false | |
| @@ -117,16 +97,16 @@ func (s *Server) paceTick(flipped map[string]bool, reports []ports.OutputPresent | |||
| 117 | 97 | break | |
| 118 | 98 | } | |
| 119 | 99 | } | |
| 120 | - | if s.paceTimer == nil { | |
| 100 | + | if !s.paceReady { | |
| 121 | 101 | return | |
| 122 | 102 | } | |
| 123 | 103 | if idle { | |
| 124 | 104 | // Nothing waits: sleep until a commit queues a callback | |
| 125 | 105 | // (wakePacer) or a flip. | |
| 126 | - | s.paceTimer.Stop() | |
| 106 | + | s.stopPace() | |
| 127 | 107 | return | |
| 128 | 108 | } | |
| 129 | - | s.paceTimer.Reset(wait) | |
| 109 | + | s.armPace(wait) | |
| 130 | 110 | } | |
| 131 | 111 | ||
| 132 | 112 | // pacePass is one pass of paceTick; it returns the time to the next | |
| @@ -1,7 +1,6 @@ | |||||
| 1 | package wayland | 1 | package wayland | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | - | "context" | |||
| 5 | "slices" | 4 | "slices" | ||
| 6 | "time" | 5 | "time" | ||
| 7 | 6 | ||||
| @@ -16,8 +15,10 @@ import ( | |||||
| 16 | // frame. They are paced per output: on its page flip when it flips, else | 15 | // frame. They are paced per output: on its page flip when it flips, else | ||
| 17 | // at its refresh rate, so a 165 Hz output lets clients draw 165 frames a | 16 | // at its refresh rate, so a 165 Hz output lets clients draw 165 frames a | ||
| 18 | // second and a V-Sync game follows the screen. Cursors and unplaced | 17 | // second and a V-Sync game follows the screen. Cursors and unplaced | ||
| 19 | - | // surfaces are paced at 60 Hz; invisible toplevel trees at 1 Hz. The | 18 | + | // surfaces are paced at 60 Hz; invisible toplevel trees at 1 Hz. Pacing runs |
| 20 | - | // pacer sleeps while no callback waits. | 19 | + | // on the display loop: output reports arrive through the presentation sink |
| 20 | + | // (presented.go) and deadlines through a timerfd; the timer sleeps while no | |||
| 21 | + | // callback waits. | |||
| 21 | 22 | ||||
| 22 | // defaultFramePeriod paces surfaces without an output (60 Hz). | 23 | // defaultFramePeriod paces surfaces without an output (60 Hz). | ||
| 23 | const defaultFramePeriod = time.Second / 60 | 24 | const defaultFramePeriod = time.Second / 60 | ||
| @@ -25,72 +26,51 @@ const suspendedFramePeriod = time.Second | |||||
| 25 | const suspendedFrameQueue = "\x00suspended" // never a wl_output name | 26 | const suspendedFrameQueue = "\x00suspended" // never a wl_output name | ||
| 26 | 27 | ||||
| 27 | // maxPacePasses bounds the passes one tick repeats while a pass queues | 28 | // maxPacePasses bounds the passes one tick repeats while a pass queues | ||
| 28 | - | // more work; the rest goes back to the pacer goroutine. | 29 | + | // more work; the rest is posted to the display loop. |
| 29 | const maxPacePasses = 3 | 30 | const maxPacePasses = 3 | ||
| 30 | 31 | ||||
| 31 | - | // pace sleeps until a flip, a wake or the deadline armed by the last tick, | |||
| 32 | - | // then runs a tick on the display goroutine. The timer is only armed from | |||
| 33 | - | // ticks, which all run there, so a commit that ticks inline (paceNow) never | |||
| 34 | - | // races the pacer for it. | |||
| 35 | - | func (s *Server) pace(ctx context.Context) { | |||
| 36 | - | timer := s.paceTimer // created by Run before the pacer starts | |||
| 37 | - | defer timer.Stop() | |||
| 38 | - | presented := s.channels.Presented | |||
| 39 | - | if s.flipped == nil { | |||
| 40 | - | s.flipped = make(map[string]bool) | |||
| 41 | - | } | |||
| 42 | - | // One method value for the whole loop: no closure per wake. | |||
| 43 | - | tick := s.paceFromPacer | |||
| 44 | - | for { | |||
| 45 | - | clear(s.flipped) | |||
| 46 | - | flipped := s.flipped | |||
| 47 | - | s.frameReports = s.frameReports[:0] | |||
| 48 | - | reports := s.frameReports | |||
| 49 | - | select { | |||
| 50 | - | case <-ctx.Done(): | |||
| 51 | - | return | |||
| 52 | - | case <-s.display.Stopped(): | |||
| 53 | - | return | |||
| 54 | - | case p := <-presented: | |||
| 55 | - | flipped[p.Output] = flipped[p.Output] || paces(p) | |||
| 56 | - | reports = append(reports, p) | |||
| 57 | - | case <-s.frameReady: | |||
| 58 | - | case <-timer.C: | |||
| 59 | - | } | |||
| 60 | - | // Coalesce flips queued while the display was busy: one burst of | |||
| 61 | - | // callbacks per output, not one per stale flip. | |||
| 62 | - | drain: | |||
| 63 | - | for { | |||
| 64 | - | select { | |||
| 65 | - | case p := <-presented: | |||
| 66 | - | flipped[p.Output] = flipped[p.Output] || paces(p) | |||
| 67 | - | reports = append(reports, p) | |||
| 68 | - | default: | |||
| 69 | - | break drain | |||
| 70 | - | } | |||
| 71 | - | } | |||
| 72 | - | s.frameReports = reports | |||
| 73 | - | if !s.display.Do(tick) { | |||
| 74 | - | return | |||
| 75 | - | } | |||
| 76 | - | } | |||
| 77 | - | } | |||
| 78 | - | ||||
| 79 | - | // paceFromPacer runs the pacer's collected flips (flipped, frameReports) | |||
| 80 | - | // on the display goroutine. | |||
| 81 | - | func (s *Server) paceFromPacer() { s.paceTick(s.flipped, s.frameReports) } | |||
| 82 | - | ||||
| 83 | // paceNow runs a tick from code already on the display goroutine (a client | 32 | // paceNow runs a tick from code already on the display goroutine (a client | ||
| 84 | - | // commit): the pacer goroutine would only call Do to get back here. Without | 33 | + | // commit). Without a registered pace timer (unit tests that build a Server |
| 85 | - | // a running pacer (no timer) it falls back to waking it. | 34 | + | // without a display) it falls back to signalling a tick. |
| 86 | func (s *Server) paceNow() { | 35 | func (s *Server) paceNow() { | ||
| 87 | - | if s.paceTimer == nil { | 36 | + | if !s.paceReady { |
| 88 | s.signalPacer() | 37 | s.signalPacer() | ||
| 89 | return | 38 | return | ||
| 90 | } | 39 | } | ||
| 91 | s.paceTick(nil, nil) | 40 | s.paceTick(nil, nil) | ||
| 92 | } | 41 | } | ||
| 93 | 42 | ||||
| 43 | + | // armPace schedules the next flip-less tick d from now on the pace timerfd | |||
| 44 | + | // (nanosecond precision, one-shot). Display goroutine only. | |||
| 45 | + | func (s *Server) armPace(d time.Duration) { | |||
| 46 | + | if d <= 0 { | |||
| 47 | + | d = time.Nanosecond | |||
| 48 | + | } | |||
| 49 | + | spec := unix.ItimerSpec{Value: unix.NsecToTimespec(int64(d))} | |||
| 50 | + | _ = unix.TimerfdSettime(s.paceFD, 0, &spec, nil) | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // stopPace disarms the pace timerfd. Display goroutine only. | |||
| 54 | + | func (s *Server) stopPace() { | |||
| 55 | + | var spec unix.ItimerSpec | |||
| 56 | + | _ = unix.TimerfdSettime(s.paceFD, 0, &spec, nil) | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | // onPaceTimer is the display loop's callback for the pace timerfd: the | |||
| 60 | + | // deadline armed by the last tick passed. | |||
| 61 | + | func (s *Server) onPaceTimer() { | |||
| 62 | + | var buf [8]byte | |||
| 63 | + | _, _ = unix.Read(s.paceFD, buf[:]) // level-triggered: consume the expiry | |||
| 64 | + | s.paceTick(nil, nil) | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | // paceSignalled is the posted tick: a wake (fence, overflow) made while the | |||
| 68 | + | // display goroutine was elsewhere. | |||
| 69 | + | func (s *Server) paceSignalled() { | |||
| 70 | + | s.paceSignal.Store(false) | |||
| 71 | + | s.paceTick(nil, nil) | |||
| 72 | + | } | |||
| 73 | + | ||||
| 94 | // paceTick answers the flips in reports, fires the due frame callbacks, | 74 | // paceTick answers the flips in reports, fires the due frame callbacks, | ||
| 95 | // releases buffers and applies the ready FIFO commits, then arms the timer | 75 | // releases buffers and applies the ready FIFO commits, then arms the timer | ||
| 96 | // for the next deadline (stopped when nothing waits). Display goroutine only. | 76 | // for the next deadline (stopped when nothing waits). Display goroutine only. | ||
| @@ -109,7 +89,7 @@ func (s *Server) paceTick(flipped map[string]bool, reports []ports.OutputPresent | |||||
| 109 | } | 89 | } | ||
| 110 | // The pass queued work behind its own checks (applied commits | 90 | // The pass queued work behind its own checks (applied commits | ||
| 111 | // queue callbacks, released buffers): look again, as a flip-less | 91 | // queue callbacks, released buffers): look again, as a flip-less | ||
| 112 | - | // tick, instead of waking the pacer for it. | 92 | + | // tick, instead of posting one. |
| 113 | flipped, reports = nil, nil | 93 | flipped, reports = nil, nil | ||
| 114 | if pass+1 == maxPacePasses { | 94 | if pass+1 == maxPacePasses { | ||
| 115 | s.paceDirty = false | 95 | s.paceDirty = false | ||
| @@ -117,16 +97,16 @@ func (s *Server) paceTick(flipped map[string]bool, reports []ports.OutputPresent | |||||
| 117 | break | 97 | break | ||
| 118 | } | 98 | } | ||
| 119 | } | 99 | } | ||
| 120 | - | if s.paceTimer == nil { | 100 | + | if !s.paceReady { |
| 121 | return | 101 | return | ||
| 122 | } | 102 | } | ||
| 123 | if idle { | 103 | if idle { | ||
| 124 | // Nothing waits: sleep until a commit queues a callback | 104 | // Nothing waits: sleep until a commit queues a callback | ||
| 125 | // (wakePacer) or a flip. | 105 | // (wakePacer) or a flip. | ||
| 126 | - | s.paceTimer.Stop() | 106 | + | s.stopPace() |
| 127 | return | 107 | return | ||
| 128 | } | 108 | } | ||
| 129 | - | s.paceTimer.Reset(wait) | 109 | + | s.armPace(wait) |
| 130 | } | 110 | } | ||
| 131 | 111 | ||||
| 132 | // pacePass is one pass of paceTick; it returns the time to the next | 112 | // pacePass is one pass of paceTick; it returns the time to the next | ||
M internal/adapters/wayland/frames_test.go +2 -2
| @@ -88,7 +88,7 @@ func TestDueFrames(t *testing.T) { | |||
| 88 | 88 | ||
| 89 | 89 | func TestInvisibleFramePacingAndMigration(t *testing.T) { | |
| 90 | 90 | out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}} | |
| 91 | - | s := &Server{outputs: []*output{out}, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1)} | |
| 91 | + | s := &Server{outputs: []*output{out}, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}} | |
| 92 | 92 | w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}} | |
| 93 | 93 | root := &surface{xdg: &xdgSurface{window: w}} | |
| 94 | 94 | child := &surface{sub: subState{parent: root}} | |
| @@ -128,7 +128,7 @@ func TestInvisibleFramePacingAndMigration(t *testing.T) { | |||
| 128 | 128 | // output's queue and move when Captured changes. | |
| 129 | 129 | func TestCapturedWindowKeepsOutputFramePacing(t *testing.T) { | |
| 130 | 130 | out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}} | |
| 131 | - | s := &Server{outputs: []*output{out}, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1)} | |
| 131 | + | s := &Server{outputs: []*output{out}, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}} | |
| 132 | 132 | w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}} | |
| 133 | 133 | root := &surface{xdg: &xdgSurface{window: w}} | |
| 134 | 134 | a := new(wayland.Callback) | |
| @@ -88,7 +88,7 @@ func TestDueFrames(t *testing.T) { | |||||
| 88 | 88 | ||||
| 89 | func TestInvisibleFramePacingAndMigration(t *testing.T) { | 89 | func TestInvisibleFramePacingAndMigration(t *testing.T) { | ||
| 90 | out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}} | 90 | out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}} | ||
| 91 | - | s := &Server{outputs: []*output{out}, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1)} | 91 | + | s := &Server{outputs: []*output{out}, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}} |
| 92 | w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}} | 92 | w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}} | ||
| 93 | root := &surface{xdg: &xdgSurface{window: w}} | 93 | root := &surface{xdg: &xdgSurface{window: w}} | ||
| 94 | child := &surface{sub: subState{parent: root}} | 94 | child := &surface{sub: subState{parent: root}} | ||
| @@ -128,7 +128,7 @@ func TestInvisibleFramePacingAndMigration(t *testing.T) { | |||||
| 128 | // output's queue and move when Captured changes. | 128 | // output's queue and move when Captured changes. | ||
| 129 | func TestCapturedWindowKeepsOutputFramePacing(t *testing.T) { | 129 | func TestCapturedWindowKeepsOutputFramePacing(t *testing.T) { | ||
| 130 | out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}} | 130 | out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}} | ||
| 131 | - | s := &Server{outputs: []*output{out}, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1)} | 131 | + | s := &Server{outputs: []*output{out}, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}} |
| 132 | w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}} | 132 | w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}} | ||
| 133 | root := &surface{xdg: &xdgSurface{window: w}} | 133 | root := &surface{xdg: &xdgSurface{window: w}} | ||
| 134 | a := new(wayland.Callback) | 134 | a := new(wayland.Callback) | ||
A internal/adapters/wayland/norace_test.go +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | //go:build !race | |
| 2 | + | ||
| 3 | + | package wayland | |
| 4 | + | ||
| 5 | + | const raceEnabled = false | |
| @@ -0,0 +1,5 @@ | |||||
| 1 | + | //go:build !race | |||
| 2 | + | ||||
| 3 | + | package wayland | |||
| 4 | + | ||||
| 5 | + | const raceEnabled = false | |||
M internal/adapters/wayland/pace_inline_test.go +41 -29
| @@ -6,11 +6,13 @@ import ( | |||
| 6 | 6 | ||
| 7 | 7 | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 8 | 8 | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | "golang.org/x/sys/unix" | |
| 9 | 10 | ) | |
| 10 | 11 | ||
| 11 | 12 | // paceFixture is a server whose surface waits on an output whose next | |
| 12 | 13 | // frame is an hour away: a commit queues its callback and nothing fires. | |
| 13 | - | func paceFixture(timer bool) (*Server, *surface) { | |
| 14 | + | func paceFixture(t *testing.T, timer bool) (*Server, *surface) { | |
| 15 | + | t.Helper() | |
| 14 | 16 | out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}} | |
| 15 | 17 | s := &Server{ | |
| 16 | 18 | outputs: []*output{out}, | |
| @@ -20,11 +22,14 @@ func paceFixture(timer bool) (*Server, *surface) { | |||
| 20 | 22 | lastFlip: map[string]time.Time{}, | |
| 21 | 23 | flips: map[string]ports.FlipInfo{}, | |
| 22 | 24 | reports: map[string]ports.OutputPresented{}, | |
| 23 | - | frameReady: make(chan struct{}, 1), | |
| 24 | 25 | } | |
| 25 | 26 | if timer { | |
| 26 | - | s.paceTimer = time.NewTimer(time.Hour) | |
| 27 | - | s.paceTimer.Stop() | |
| 27 | + | fd, err := unix.TimerfdCreate(unix.CLOCK_MONOTONIC, unix.TFD_NONBLOCK|unix.TFD_CLOEXEC) | |
| 28 | + | if err != nil { | |
| 29 | + | t.Fatal(err) | |
| 30 | + | } | |
| 31 | + | t.Cleanup(func() { unix.Close(fd) }) | |
| 32 | + | s.paceFD, s.paceReady = fd, true | |
| 28 | 33 | } | |
| 29 | 34 | w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}} | |
| 30 | 35 | surf := &surface{server: s, xdg: &xdgSurface{window: w}} | |
| @@ -33,16 +38,16 @@ func paceFixture(timer bool) (*Server, *surface) { | |||
| 33 | 38 | } | |
| 34 | 39 | ||
| 35 | 40 | // A commit on the display goroutine ticks the pacer inline: it arms the | |
| 36 | - | // pacer's timer itself and does not wake the pacer goroutine, which would | |
| 37 | - | // only call display.Do to get back to the display goroutine. | |
| 41 | + | // pace timer itself and posts no tick, which would only run later on the | |
| 42 | + | // display goroutine it is already on. | |
| 38 | 43 | func TestCommitTicksPacerInline(t *testing.T) { | |
| 39 | - | s, surf := paceFixture(true) | |
| 44 | + | s, surf := paceFixture(t, true) | |
| 40 | 45 | surf.queueAndTick() | |
| 41 | 46 | if len(s.awaiting["DP-2"]) != 1 { | |
| 42 | 47 | t.Fatalf("callback not queued: %+v", s.awaiting) | |
| 43 | 48 | } | |
| 44 | - | if len(s.frameReady) != 0 { | |
| 45 | - | t.Fatal("commit woke the pacer goroutine") | |
| 49 | + | if s.paceSignal.Load() { | |
| 50 | + | t.Fatal("commit posted a tick") | |
| 46 | 51 | } | |
| 47 | 52 | waitArmed(t, s, "inline tick did not arm the pacer timer for the next deadline") | |
| 48 | 53 | if s.paceDepth != 0 || s.paceDirty { | |
| @@ -50,54 +55,57 @@ func TestCommitTicksPacerInline(t *testing.T) { | |||
| 50 | 55 | } | |
| 51 | 56 | } | |
| 52 | 57 | ||
| 53 | - | // Without a running pacer (no timer) the wake falls back to the channel. | |
| 58 | + | // Without a registered pace timer the wake falls back to a posted tick. | |
| 54 | 59 | func TestCommitWakesPacerWithoutTimer(t *testing.T) { | |
| 55 | - | s, surf := paceFixture(false) | |
| 60 | + | s, surf := paceFixture(t, false) | |
| 56 | 61 | surf.queueAndTick() | |
| 57 | - | if len(s.frameReady) != 1 { | |
| 58 | - | t.Fatal("pacer not woken") | |
| 62 | + | if !s.paceSignal.Load() { | |
| 63 | + | t.Fatal("tick not signalled") | |
| 59 | 64 | } | |
| 60 | 65 | } | |
| 61 | 66 | ||
| 62 | - | // Wakes inside a tick coalesce into one more pass, not one channel wake each. | |
| 67 | + | // Wakes inside a tick coalesce into one more pass, not one posted tick each. | |
| 63 | 68 | func TestWakePacerDefersInsideTick(t *testing.T) { | |
| 64 | - | s, _ := paceFixture(true) | |
| 69 | + | s, _ := paceFixture(t, true) | |
| 65 | 70 | s.paceDepth = 1 | |
| 66 | 71 | s.wakePacer() | |
| 67 | 72 | s.wakePacer() | |
| 68 | - | if len(s.frameReady) != 0 || !s.paceDirty { | |
| 69 | - | t.Fatalf("wake inside a tick: queued %d dirty %v", len(s.frameReady), s.paceDirty) | |
| 73 | + | if s.paceSignal.Load() || !s.paceDirty { | |
| 74 | + | t.Fatalf("wake inside a tick: signalled %v dirty %v", s.paceSignal.Load(), s.paceDirty) | |
| 70 | 75 | } | |
| 71 | 76 | s.paceDepth = 0 | |
| 72 | 77 | s.wakePacer() | |
| 73 | - | if len(s.frameReady) != 1 { | |
| 78 | + | if !s.paceSignal.Load() { | |
| 74 | 79 | t.Fatal("wake outside a tick lost") | |
| 75 | 80 | } | |
| 76 | 81 | } | |
| 77 | 82 | ||
| 78 | 83 | // A commit that waits (a fifo barrier) is timed by the inline tick. | |
| 79 | 84 | func TestCommitWaitingOnBarrierArmsTimer(t *testing.T) { | |
| 80 | - | s, surf := paceFixture(true) | |
| 85 | + | s, surf := paceFixture(t, true) | |
| 81 | 86 | surf.next.callbacks = nil | |
| 82 | 87 | surf.barrier, surf.barrierAt = true, time.Now() | |
| 83 | 88 | surf.next.wait = true | |
| 84 | 89 | surf.queueAndTick() | |
| 85 | - | if len(surf.queue) != 1 || len(s.frameReady) != 0 { | |
| 86 | - | t.Fatalf("queued %d, pacer wakes %d", len(surf.queue), len(s.frameReady)) | |
| 90 | + | if len(surf.queue) != 1 || s.paceSignal.Load() { | |
| 91 | + | t.Fatalf("queued %d, tick signalled %v", len(surf.queue), s.paceSignal.Load()) | |
| 87 | 92 | } | |
| 88 | 93 | waitArmed(t, s, "timer not armed for the barrier deadline") | |
| 89 | 94 | } | |
| 90 | 95 | ||
| 91 | - | // waitArmed asserts the timer was armed: it is pending, or it already | |
| 92 | - | // fired and holds its value. | |
| 96 | + | // waitArmed asserts the timerfd was armed: it is pending, or it already | |
| 97 | + | // fired and holds its expiry. | |
| 93 | 98 | func waitArmed(t *testing.T, s *Server, msg string) { | |
| 94 | 99 | t.Helper() | |
| 95 | - | if s.paceTimer.Stop() { | |
| 100 | + | var cur unix.ItimerSpec | |
| 101 | + | if err := unix.TimerfdGettime(s.paceFD, &cur); err != nil { | |
| 102 | + | t.Fatal(err) | |
| 103 | + | } | |
| 104 | + | if cur.Value != (unix.Timespec{}) { | |
| 96 | 105 | return | |
| 97 | 106 | } | |
| 98 | - | select { | |
| 99 | - | case <-s.paceTimer.C: | |
| 100 | - | default: | |
| 107 | + | var buf [8]byte | |
| 108 | + | if n, _ := unix.Read(s.paceFD, buf[:]); n != 8 { | |
| 101 | 109 | t.Fatal(msg) | |
| 102 | 110 | } | |
| 103 | 111 | } | |
| @@ -105,14 +113,18 @@ func waitArmed(t *testing.T, s *Server, msg string) { | |||
| 105 | 113 | // A synchronized subsurface commit waits for its parent's commit: it does | |
| 106 | 114 | // not tick the pacer, so a burst of child commits costs no ticks. | |
| 107 | 115 | func TestSyncedCommitDoesNotTick(t *testing.T) { | |
| 108 | - | s, surf := paceFixture(true) | |
| 116 | + | s, surf := paceFixture(t, true) | |
| 109 | 117 | surf.sub.parent = &surface{server: s} | |
| 110 | 118 | surf.sub.synced = true | |
| 111 | 119 | surf.queueAndTick() | |
| 112 | 120 | if len(surf.queue) != 1 || !surf.queue[0].synced { | |
| 113 | 121 | t.Fatalf("synced commit not queued: %d", len(surf.queue)) | |
| 114 | 122 | } | |
| 115 | - | if len(s.frameReady) != 0 || s.paceTimer.Stop() { | |
| 123 | + | var cur unix.ItimerSpec | |
| 124 | + | if err := unix.TimerfdGettime(s.paceFD, &cur); err != nil { | |
| 125 | + | t.Fatal(err) | |
| 126 | + | } | |
| 127 | + | if s.paceSignal.Load() || cur.Value != (unix.Timespec{}) { | |
| 116 | 128 | t.Fatal("synced commit ticked the pacer") | |
| 117 | 129 | } | |
| 118 | 130 | } | |
| @@ -6,11 +6,13 @@ import ( | |||||
| 6 | 6 | ||||
| 7 | "github.com/bnema/go-wayland-bindings/server/wayland" | 7 | "github.com/bnema/go-wayland-bindings/server/wayland" | ||
| 8 | "github.com/bnema/neferwl/internal/ports" | 8 | "github.com/bnema/neferwl/internal/ports" | ||
| 9 | + | "golang.org/x/sys/unix" | |||
| 9 | ) | 10 | ) | ||
| 10 | 11 | ||||
| 11 | // paceFixture is a server whose surface waits on an output whose next | 12 | // paceFixture is a server whose surface waits on an output whose next | ||
| 12 | // frame is an hour away: a commit queues its callback and nothing fires. | 13 | // frame is an hour away: a commit queues its callback and nothing fires. | ||
| 13 | - | func paceFixture(timer bool) (*Server, *surface) { | 14 | + | func paceFixture(t *testing.T, timer bool) (*Server, *surface) { |
| 15 | + | t.Helper() | |||
| 14 | out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}} | 16 | out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}} | ||
| 15 | s := &Server{ | 17 | s := &Server{ | ||
| 16 | outputs: []*output{out}, | 18 | outputs: []*output{out}, | ||
| @@ -20,11 +22,14 @@ func paceFixture(timer bool) (*Server, *surface) { | |||||
| 20 | lastFlip: map[string]time.Time{}, | 22 | lastFlip: map[string]time.Time{}, | ||
| 21 | flips: map[string]ports.FlipInfo{}, | 23 | flips: map[string]ports.FlipInfo{}, | ||
| 22 | reports: map[string]ports.OutputPresented{}, | 24 | reports: map[string]ports.OutputPresented{}, | ||
| 23 | - | frameReady: make(chan struct{}, 1), | |||
| 24 | } | 25 | } | ||
| 25 | if timer { | 26 | if timer { | ||
| 26 | - | s.paceTimer = time.NewTimer(time.Hour) | 27 | + | fd, err := unix.TimerfdCreate(unix.CLOCK_MONOTONIC, unix.TFD_NONBLOCK|unix.TFD_CLOEXEC) |
| 27 | - | s.paceTimer.Stop() | 28 | + | if err != nil { |
| 29 | + | t.Fatal(err) | |||
| 30 | + | } | |||
| 31 | + | t.Cleanup(func() { unix.Close(fd) }) | |||
| 32 | + | s.paceFD, s.paceReady = fd, true | |||
| 28 | } | 33 | } | ||
| 29 | w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}} | 34 | w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}} | ||
| 30 | surf := &surface{server: s, xdg: &xdgSurface{window: w}} | 35 | surf := &surface{server: s, xdg: &xdgSurface{window: w}} | ||
| @@ -33,16 +38,16 @@ func paceFixture(timer bool) (*Server, *surface) { | |||||
| 33 | } | 38 | } | ||
| 34 | 39 | ||||
| 35 | // A commit on the display goroutine ticks the pacer inline: it arms the | 40 | // A commit on the display goroutine ticks the pacer inline: it arms the | ||
| 36 | - | // pacer's timer itself and does not wake the pacer goroutine, which would | 41 | + | // pace timer itself and posts no tick, which would only run later on the |
| 37 | - | // only call display.Do to get back to the display goroutine. | 42 | + | // display goroutine it is already on. |
| 38 | func TestCommitTicksPacerInline(t *testing.T) { | 43 | func TestCommitTicksPacerInline(t *testing.T) { | ||
| 39 | - | s, surf := paceFixture(true) | 44 | + | s, surf := paceFixture(t, true) |
| 40 | surf.queueAndTick() | 45 | surf.queueAndTick() | ||
| 41 | if len(s.awaiting["DP-2"]) != 1 { | 46 | if len(s.awaiting["DP-2"]) != 1 { | ||
| 42 | t.Fatalf("callback not queued: %+v", s.awaiting) | 47 | t.Fatalf("callback not queued: %+v", s.awaiting) | ||
| 43 | } | 48 | } | ||
| 44 | - | if len(s.frameReady) != 0 { | 49 | + | if s.paceSignal.Load() { |
| 45 | - | t.Fatal("commit woke the pacer goroutine") | 50 | + | t.Fatal("commit posted a tick") |
| 46 | } | 51 | } | ||
| 47 | waitArmed(t, s, "inline tick did not arm the pacer timer for the next deadline") | 52 | waitArmed(t, s, "inline tick did not arm the pacer timer for the next deadline") | ||
| 48 | if s.paceDepth != 0 || s.paceDirty { | 53 | if s.paceDepth != 0 || s.paceDirty { | ||
| @@ -50,54 +55,57 @@ func TestCommitTicksPacerInline(t *testing.T) { | |||||
| 50 | } | 55 | } | ||
| 51 | } | 56 | } | ||
| 52 | 57 | ||||
| 53 | - | // Without a running pacer (no timer) the wake falls back to the channel. | 58 | + | // Without a registered pace timer the wake falls back to a posted tick. |
| 54 | func TestCommitWakesPacerWithoutTimer(t *testing.T) { | 59 | func TestCommitWakesPacerWithoutTimer(t *testing.T) { | ||
| 55 | - | s, surf := paceFixture(false) | 60 | + | s, surf := paceFixture(t, false) |
| 56 | surf.queueAndTick() | 61 | surf.queueAndTick() | ||
| 57 | - | if len(s.frameReady) != 1 { | 62 | + | if !s.paceSignal.Load() { |
| 58 | - | t.Fatal("pacer not woken") | 63 | + | t.Fatal("tick not signalled") |
| 59 | } | 64 | } | ||
| 60 | } | 65 | } | ||
| 61 | 66 | ||||
| 62 | - | // Wakes inside a tick coalesce into one more pass, not one channel wake each. | 67 | + | // Wakes inside a tick coalesce into one more pass, not one posted tick each. |
| 63 | func TestWakePacerDefersInsideTick(t *testing.T) { | 68 | func TestWakePacerDefersInsideTick(t *testing.T) { | ||
| 64 | - | s, _ := paceFixture(true) | 69 | + | s, _ := paceFixture(t, true) |
| 65 | s.paceDepth = 1 | 70 | s.paceDepth = 1 | ||
| 66 | s.wakePacer() | 71 | s.wakePacer() | ||
| 67 | s.wakePacer() | 72 | s.wakePacer() | ||
| 68 | - | if len(s.frameReady) != 0 || !s.paceDirty { | 73 | + | if s.paceSignal.Load() || !s.paceDirty { |
| 69 | - | t.Fatalf("wake inside a tick: queued %d dirty %v", len(s.frameReady), s.paceDirty) | 74 | + | t.Fatalf("wake inside a tick: signalled %v dirty %v", s.paceSignal.Load(), s.paceDirty) |
| 70 | } | 75 | } | ||
| 71 | s.paceDepth = 0 | 76 | s.paceDepth = 0 | ||
| 72 | s.wakePacer() | 77 | s.wakePacer() | ||
| 73 | - | if len(s.frameReady) != 1 { | 78 | + | if !s.paceSignal.Load() { |
| 74 | t.Fatal("wake outside a tick lost") | 79 | t.Fatal("wake outside a tick lost") | ||
| 75 | } | 80 | } | ||
| 76 | } | 81 | } | ||
| 77 | 82 | ||||
| 78 | // A commit that waits (a fifo barrier) is timed by the inline tick. | 83 | // A commit that waits (a fifo barrier) is timed by the inline tick. | ||
| 79 | func TestCommitWaitingOnBarrierArmsTimer(t *testing.T) { | 84 | func TestCommitWaitingOnBarrierArmsTimer(t *testing.T) { | ||
| 80 | - | s, surf := paceFixture(true) | 85 | + | s, surf := paceFixture(t, true) |
| 81 | surf.next.callbacks = nil | 86 | surf.next.callbacks = nil | ||
| 82 | surf.barrier, surf.barrierAt = true, time.Now() | 87 | surf.barrier, surf.barrierAt = true, time.Now() | ||
| 83 | surf.next.wait = true | 88 | surf.next.wait = true | ||
| 84 | surf.queueAndTick() | 89 | surf.queueAndTick() | ||
| 85 | - | if len(surf.queue) != 1 || len(s.frameReady) != 0 { | 90 | + | if len(surf.queue) != 1 || s.paceSignal.Load() { |
| 86 | - | t.Fatalf("queued %d, pacer wakes %d", len(surf.queue), len(s.frameReady)) | 91 | + | t.Fatalf("queued %d, tick signalled %v", len(surf.queue), s.paceSignal.Load()) |
| 87 | } | 92 | } | ||
| 88 | waitArmed(t, s, "timer not armed for the barrier deadline") | 93 | waitArmed(t, s, "timer not armed for the barrier deadline") | ||
| 89 | } | 94 | } | ||
| 90 | 95 | ||||
| 91 | - | // waitArmed asserts the timer was armed: it is pending, or it already | 96 | + | // waitArmed asserts the timerfd was armed: it is pending, or it already |
| 92 | - | // fired and holds its value. | 97 | + | // fired and holds its expiry. |
| 93 | func waitArmed(t *testing.T, s *Server, msg string) { | 98 | func waitArmed(t *testing.T, s *Server, msg string) { | ||
| 94 | t.Helper() | 99 | t.Helper() | ||
| 95 | - | if s.paceTimer.Stop() { | 100 | + | var cur unix.ItimerSpec |
| 101 | + | if err := unix.TimerfdGettime(s.paceFD, &cur); err != nil { | |||
| 102 | + | t.Fatal(err) | |||
| 103 | + | } | |||
| 104 | + | if cur.Value != (unix.Timespec{}) { | |||
| 96 | return | 105 | return | ||
| 97 | } | 106 | } | ||
| 98 | - | select { | 107 | + | var buf [8]byte |
| 99 | - | case <-s.paceTimer.C: | 108 | + | if n, _ := unix.Read(s.paceFD, buf[:]); n != 8 { |
| 100 | - | default: | |||
| 101 | t.Fatal(msg) | 109 | t.Fatal(msg) | ||
| 102 | } | 110 | } | ||
| 103 | } | 111 | } | ||
| @@ -105,14 +113,18 @@ func waitArmed(t *testing.T, s *Server, msg string) { | |||||
| 105 | // A synchronized subsurface commit waits for its parent's commit: it does | 113 | // A synchronized subsurface commit waits for its parent's commit: it does | ||
| 106 | // not tick the pacer, so a burst of child commits costs no ticks. | 114 | // not tick the pacer, so a burst of child commits costs no ticks. | ||
| 107 | func TestSyncedCommitDoesNotTick(t *testing.T) { | 115 | func TestSyncedCommitDoesNotTick(t *testing.T) { | ||
| 108 | - | s, surf := paceFixture(true) | 116 | + | s, surf := paceFixture(t, true) |
| 109 | surf.sub.parent = &surface{server: s} | 117 | surf.sub.parent = &surface{server: s} | ||
| 110 | surf.sub.synced = true | 118 | surf.sub.synced = true | ||
| 111 | surf.queueAndTick() | 119 | surf.queueAndTick() | ||
| 112 | if len(surf.queue) != 1 || !surf.queue[0].synced { | 120 | if len(surf.queue) != 1 || !surf.queue[0].synced { | ||
| 113 | t.Fatalf("synced commit not queued: %d", len(surf.queue)) | 121 | t.Fatalf("synced commit not queued: %d", len(surf.queue)) | ||
| 114 | } | 122 | } | ||
| 115 | - | if len(s.frameReady) != 0 || s.paceTimer.Stop() { | 123 | + | var cur unix.ItimerSpec |
| 124 | + | if err := unix.TimerfdGettime(s.paceFD, &cur); err != nil { | |||
| 125 | + | t.Fatal(err) | |||
| 126 | + | } | |||
| 127 | + | if s.paceSignal.Load() || cur.Value != (unix.Timespec{}) { | |||
| 116 | t.Fatal("synced commit ticked the pacer") | 128 | t.Fatal("synced commit ticked the pacer") | ||
| 117 | } | 129 | } | ||
| 118 | } | 130 | } | ||
M internal/adapters/wayland/presentation_test.go +4 -5
| @@ -46,14 +46,13 @@ func (p *feedbackEvents) Dispatch(e *wlturbo.Event) { | |||
| 46 | 46 | } | |
| 47 | 47 | ||
| 48 | 48 | // presentServer is a server whose flips the test sends. | |
| 49 | - | func presentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, chan ports.OutputPresented, string) { | |
| 49 | + | func presentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, ports.PresentationSink, string) { | |
| 50 | 50 | t.Helper() | |
| 51 | 51 | dir := t.TempDir() | |
| 52 | 52 | events := make(chan ports.ClientEvent, 16) | |
| 53 | 53 | commands := make(chan ports.ClientCommand, 16) | |
| 54 | 54 | contents := make(chan ports.SurfaceContent, 64) | |
| 55 | - | presented := make(chan ports.OutputPresented, 16) | |
| 56 | - | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Commands: commands, Contents: contents, Presented: presented}, logging.For(context.Background(), "wayland")) | |
| 55 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Commands: commands, Contents: contents}, logging.For(context.Background(), "wayland")) | |
| 57 | 56 | if err != nil { | |
| 58 | 57 | t.Fatal(err) | |
| 59 | 58 | } | |
| @@ -61,7 +60,7 @@ func presentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.Cl | |||
| 61 | 60 | done := make(chan error, 1) | |
| 62 | 61 | go func() { done <- s.Run(ctx) }() | |
| 63 | 62 | t.Cleanup(func() { cancel(); <-done }) | |
| 64 | - | return s, events, commands, contents, presented, dir | |
| 63 | + | return s, events, commands, contents, s.Presentation(), dir | |
| 65 | 64 | } | |
| 66 | 65 | ||
| 67 | 66 | func TestPresentationFeedback(t *testing.T) { | |
| @@ -156,7 +155,7 @@ func TestPresentationFeedback(t *testing.T) { | |||
| 156 | 155 | } | |
| 157 | 156 | } | |
| 158 | 157 | flip := func(when time.Duration, seq uint64, shows uint64, zero ports.WindowID) { | |
| 159 | - | presented <- ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{When: when, Seq: seq, Refresh: time.Second / 60, ZeroCopy: zero, HardwareClock: true, Shows: map[ports.WindowID]uint64{w.ID: shows}}} | |
| 158 | + | presented.Push(ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{When: when, Seq: seq, Refresh: time.Second / 60, ZeroCopy: zero, HardwareClock: true, Shows: map[ports.WindowID]uint64{w.ID: shows}}}) | |
| 160 | 159 | } | |
| 161 | 160 | // With no flip, hiding must discard a waiting feedback immediately. | |
| 162 | 161 | hidden := feedback() | |
| @@ -46,14 +46,13 @@ func (p *feedbackEvents) Dispatch(e *wlturbo.Event) { | |||||
| 46 | } | 46 | } | ||
| 47 | 47 | ||||
| 48 | // presentServer is a server whose flips the test sends. | 48 | // presentServer is a server whose flips the test sends. | ||
| 49 | - | func presentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, chan ports.OutputPresented, string) { | 49 | + | func presentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, ports.PresentationSink, string) { |
| 50 | t.Helper() | 50 | t.Helper() | ||
| 51 | dir := t.TempDir() | 51 | dir := t.TempDir() | ||
| 52 | events := make(chan ports.ClientEvent, 16) | 52 | events := make(chan ports.ClientEvent, 16) | ||
| 53 | commands := make(chan ports.ClientCommand, 16) | 53 | commands := make(chan ports.ClientCommand, 16) | ||
| 54 | contents := make(chan ports.SurfaceContent, 64) | 54 | contents := make(chan ports.SurfaceContent, 64) | ||
| 55 | - | presented := make(chan ports.OutputPresented, 16) | 55 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Commands: commands, Contents: contents}, logging.For(context.Background(), "wayland")) |
| 56 | - | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Commands: commands, Contents: contents, Presented: presented}, logging.For(context.Background(), "wayland")) | |||
| 57 | if err != nil { | 56 | if err != nil { | ||
| 58 | t.Fatal(err) | 57 | t.Fatal(err) | ||
| 59 | } | 58 | } | ||
| @@ -61,7 +60,7 @@ func presentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.Cl | |||||
| 61 | done := make(chan error, 1) | 60 | done := make(chan error, 1) | ||
| 62 | go func() { done <- s.Run(ctx) }() | 61 | go func() { done <- s.Run(ctx) }() | ||
| 63 | t.Cleanup(func() { cancel(); <-done }) | 62 | t.Cleanup(func() { cancel(); <-done }) | ||
| 64 | - | return s, events, commands, contents, presented, dir | 63 | + | return s, events, commands, contents, s.Presentation(), dir |
| 65 | } | 64 | } | ||
| 66 | 65 | ||||
| 67 | func TestPresentationFeedback(t *testing.T) { | 66 | func TestPresentationFeedback(t *testing.T) { | ||
| @@ -156,7 +155,7 @@ func TestPresentationFeedback(t *testing.T) { | |||||
| 156 | } | 155 | } | ||
| 157 | } | 156 | } | ||
| 158 | flip := func(when time.Duration, seq uint64, shows uint64, zero ports.WindowID) { | 157 | flip := func(when time.Duration, seq uint64, shows uint64, zero ports.WindowID) { | ||
| 159 | - | presented <- ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{When: when, Seq: seq, Refresh: time.Second / 60, ZeroCopy: zero, HardwareClock: true, Shows: map[ports.WindowID]uint64{w.ID: shows}}} | 158 | + | presented.Push(ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{When: when, Seq: seq, Refresh: time.Second / 60, ZeroCopy: zero, HardwareClock: true, Shows: map[ports.WindowID]uint64{w.ID: shows}}}) |
| 160 | } | 159 | } | ||
| 161 | // With no flip, hiding must discard a waiting feedback immediately. | 160 | // With no flip, hiding must discard a waiting feedback immediately. | ||
| 162 | hidden := feedback() | 161 | hidden := feedback() | ||
A internal/adapters/wayland/presented.go +112 -0
| @@ -0,0 +1,112 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "slices" | |
| 5 | + | "sync" | |
| 6 | + | "sync/atomic" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/neferwl/internal/adapters/outputkit/presented" | |
| 9 | + | "github.com/bnema/neferwl/internal/ports" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // maxPresentRing bounds the reports waiting for the display goroutine. Each | |
| 13 | + | // output already bounds its own queue (presented.MaxUnsent), so the ring only | |
| 14 | + | // fills when the display loop is stalled. | |
| 15 | + | const maxPresentRing = 64 | |
| 16 | + | ||
| 17 | + | // presentSink is the ports.PresentationSink wayland offers to the outputs: | |
| 18 | + | // output owner goroutines Push reports, the display goroutine drains them and | |
| 19 | + | // paces frames. The ring is a transport queue between those goroutines, not | |
| 20 | + | // window state: the mutex guards only the two slices and the pending flag. | |
| 21 | + | type presentSink struct { | |
| 22 | + | s *Server | |
| 23 | + | ||
| 24 | + | mu sync.Mutex | |
| 25 | + | ring, spare []ports.OutputPresented | |
| 26 | + | pending bool // a drain is posted and has not taken the ring yet | |
| 27 | + | drainFn func() | |
| 28 | + | drains atomic.Uint64 // finished drains | |
| 29 | + | scratchFlips map[string]bool | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | func newPresentSink(s *Server) *presentSink { | |
| 33 | + | p := &presentSink{ | |
| 34 | + | s: s, | |
| 35 | + | ring: make([]ports.OutputPresented, 0, maxPresentRing), | |
| 36 | + | spare: make([]ports.OutputPresented, 0, maxPresentRing), | |
| 37 | + | scratchFlips: make(map[string]bool), | |
| 38 | + | } | |
| 39 | + | p.drainFn = p.drain | |
| 40 | + | return p | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // Presentation returns the sink outputs report to. Push never blocks. | |
| 44 | + | func (s *Server) Presentation() ports.PresentationSink { return s.sink } | |
| 45 | + | ||
| 46 | + | // Push queues a report for the display goroutine and wakes it; one wake | |
| 47 | + | // serves every report pushed before the drain runs. | |
| 48 | + | func (p *presentSink) Push(r ports.OutputPresented) { | |
| 49 | + | p.mu.Lock() | |
| 50 | + | if len(p.ring) >= maxPresentRing { | |
| 51 | + | p.dropOldestLocked() | |
| 52 | + | } | |
| 53 | + | p.ring = append(p.ring, r) | |
| 54 | + | post := !p.pending | |
| 55 | + | p.pending = true | |
| 56 | + | p.mu.Unlock() | |
| 57 | + | if post { | |
| 58 | + | p.s.display.Post(p.drainFn) | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // dropOldestLocked makes room: the oldest report that a later report of the | |
| 63 | + | // same output follows is folded into it (flips merge as in presented.Queue). | |
| 64 | + | func (p *presentSink) dropOldestLocked() { | |
| 65 | + | victim := 0 | |
| 66 | + | for i := range p.ring { | |
| 67 | + | if next := p.nextOfOutputLocked(i); next > 0 { | |
| 68 | + | if old, nf := p.ring[i].Flip, p.ring[next].Flip; old != nil && nf != nil { | |
| 69 | + | presented.Merge(old, nf) | |
| 70 | + | } | |
| 71 | + | victim = i | |
| 72 | + | break | |
| 73 | + | } | |
| 74 | + | } | |
| 75 | + | p.ring = slices.Delete(p.ring, victim, victim+1) | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | // nextOfOutputLocked returns the index of the next report of i's output, 0 | |
| 79 | + | // when none follows. | |
| 80 | + | func (p *presentSink) nextOfOutputLocked(i int) int { | |
| 81 | + | for j := i + 1; j < len(p.ring); j++ { | |
| 82 | + | if p.ring[j].Output == p.ring[i].Output { | |
| 83 | + | return j | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | return 0 | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // drain runs on the display goroutine: it paces frames for every report that | |
| 90 | + | // arrived since the last drain as one tick, so a burst of flips fires one | |
| 91 | + | // burst of frame callbacks per output. | |
| 92 | + | func (p *presentSink) drain() { | |
| 93 | + | p.mu.Lock() | |
| 94 | + | batch := p.ring | |
| 95 | + | p.ring, p.spare = p.spare[:0], nil | |
| 96 | + | p.pending = false | |
| 97 | + | p.mu.Unlock() | |
| 98 | + | flipped := p.scratchFlips | |
| 99 | + | clear(flipped) | |
| 100 | + | for _, r := range batch { | |
| 101 | + | flipped[r.Output] = flipped[r.Output] || paces(r) | |
| 102 | + | } | |
| 103 | + | if len(batch) > 0 { | |
| 104 | + | p.s.paceTick(flipped, batch) | |
| 105 | + | } | |
| 106 | + | // The reports carry maps: do not retain them in the spare batch. | |
| 107 | + | clear(batch) | |
| 108 | + | p.mu.Lock() | |
| 109 | + | p.spare = batch[:0] | |
| 110 | + | p.mu.Unlock() | |
| 111 | + | p.drains.Add(1) | |
| 112 | + | } | |
| @@ -0,0 +1,112 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "slices" | |||
| 5 | + | "sync" | |||
| 6 | + | "sync/atomic" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/neferwl/internal/adapters/outputkit/presented" | |||
| 9 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // maxPresentRing bounds the reports waiting for the display goroutine. Each | |||
| 13 | + | // output already bounds its own queue (presented.MaxUnsent), so the ring only | |||
| 14 | + | // fills when the display loop is stalled. | |||
| 15 | + | const maxPresentRing = 64 | |||
| 16 | + | ||||
| 17 | + | // presentSink is the ports.PresentationSink wayland offers to the outputs: | |||
| 18 | + | // output owner goroutines Push reports, the display goroutine drains them and | |||
| 19 | + | // paces frames. The ring is a transport queue between those goroutines, not | |||
| 20 | + | // window state: the mutex guards only the two slices and the pending flag. | |||
| 21 | + | type presentSink struct { | |||
| 22 | + | s *Server | |||
| 23 | + | ||||
| 24 | + | mu sync.Mutex | |||
| 25 | + | ring, spare []ports.OutputPresented | |||
| 26 | + | pending bool // a drain is posted and has not taken the ring yet | |||
| 27 | + | drainFn func() | |||
| 28 | + | drains atomic.Uint64 // finished drains | |||
| 29 | + | scratchFlips map[string]bool | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | func newPresentSink(s *Server) *presentSink { | |||
| 33 | + | p := &presentSink{ | |||
| 34 | + | s: s, | |||
| 35 | + | ring: make([]ports.OutputPresented, 0, maxPresentRing), | |||
| 36 | + | spare: make([]ports.OutputPresented, 0, maxPresentRing), | |||
| 37 | + | scratchFlips: make(map[string]bool), | |||
| 38 | + | } | |||
| 39 | + | p.drainFn = p.drain | |||
| 40 | + | return p | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // Presentation returns the sink outputs report to. Push never blocks. | |||
| 44 | + | func (s *Server) Presentation() ports.PresentationSink { return s.sink } | |||
| 45 | + | ||||
| 46 | + | // Push queues a report for the display goroutine and wakes it; one wake | |||
| 47 | + | // serves every report pushed before the drain runs. | |||
| 48 | + | func (p *presentSink) Push(r ports.OutputPresented) { | |||
| 49 | + | p.mu.Lock() | |||
| 50 | + | if len(p.ring) >= maxPresentRing { | |||
| 51 | + | p.dropOldestLocked() | |||
| 52 | + | } | |||
| 53 | + | p.ring = append(p.ring, r) | |||
| 54 | + | post := !p.pending | |||
| 55 | + | p.pending = true | |||
| 56 | + | p.mu.Unlock() | |||
| 57 | + | if post { | |||
| 58 | + | p.s.display.Post(p.drainFn) | |||
| 59 | + | } | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | // dropOldestLocked makes room: the oldest report that a later report of the | |||
| 63 | + | // same output follows is folded into it (flips merge as in presented.Queue). | |||
| 64 | + | func (p *presentSink) dropOldestLocked() { | |||
| 65 | + | victim := 0 | |||
| 66 | + | for i := range p.ring { | |||
| 67 | + | if next := p.nextOfOutputLocked(i); next > 0 { | |||
| 68 | + | if old, nf := p.ring[i].Flip, p.ring[next].Flip; old != nil && nf != nil { | |||
| 69 | + | presented.Merge(old, nf) | |||
| 70 | + | } | |||
| 71 | + | victim = i | |||
| 72 | + | break | |||
| 73 | + | } | |||
| 74 | + | } | |||
| 75 | + | p.ring = slices.Delete(p.ring, victim, victim+1) | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | // nextOfOutputLocked returns the index of the next report of i's output, 0 | |||
| 79 | + | // when none follows. | |||
| 80 | + | func (p *presentSink) nextOfOutputLocked(i int) int { | |||
| 81 | + | for j := i + 1; j < len(p.ring); j++ { | |||
| 82 | + | if p.ring[j].Output == p.ring[i].Output { | |||
| 83 | + | return j | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | return 0 | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | // drain runs on the display goroutine: it paces frames for every report that | |||
| 90 | + | // arrived since the last drain as one tick, so a burst of flips fires one | |||
| 91 | + | // burst of frame callbacks per output. | |||
| 92 | + | func (p *presentSink) drain() { | |||
| 93 | + | p.mu.Lock() | |||
| 94 | + | batch := p.ring | |||
| 95 | + | p.ring, p.spare = p.spare[:0], nil | |||
| 96 | + | p.pending = false | |||
| 97 | + | p.mu.Unlock() | |||
| 98 | + | flipped := p.scratchFlips | |||
| 99 | + | clear(flipped) | |||
| 100 | + | for _, r := range batch { | |||
| 101 | + | flipped[r.Output] = flipped[r.Output] || paces(r) | |||
| 102 | + | } | |||
| 103 | + | if len(batch) > 0 { | |||
| 104 | + | p.s.paceTick(flipped, batch) | |||
| 105 | + | } | |||
| 106 | + | // The reports carry maps: do not retain them in the spare batch. | |||
| 107 | + | clear(batch) | |||
| 108 | + | p.mu.Lock() | |||
| 109 | + | p.spare = batch[:0] | |||
| 110 | + | p.mu.Unlock() | |||
| 111 | + | p.drains.Add(1) | |||
| 112 | + | } | |||
A internal/adapters/wayland/presented_test.go +116 -0
| @@ -0,0 +1,116 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "runtime" | |
| 5 | + | "testing" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/go-wayland-bindings/server/fifo" | |
| 9 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 10 | + | "github.com/bnema/neferwl/internal/ports" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // Flips pushed while the display goroutine is busy are paced by one drain: | |
| 14 | + | // one tick, one burst of frame callbacks per output, not one per stale flip. | |
| 15 | + | func TestPresentationPushCoalescesWhileDisplayBusy(t *testing.T) { | |
| 16 | + | s, _, _, _, sink, _ := presentServer(t) | |
| 17 | + | release, started := make(chan struct{}), make(chan struct{}) | |
| 18 | + | finished := make(chan struct{}) | |
| 19 | + | go func() { | |
| 20 | + | s.display.Do(func() { close(started); <-release }) | |
| 21 | + | close(finished) | |
| 22 | + | }() | |
| 23 | + | <-started | |
| 24 | + | for i := range 5 { | |
| 25 | + | sink.Push(ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{Seq: uint64(i + 1), HardwareClock: true}}) | |
| 26 | + | } | |
| 27 | + | close(release) | |
| 28 | + | <-finished | |
| 29 | + | var drains, seq uint64 | |
| 30 | + | var reported bool | |
| 31 | + | s.display.Do(func() { | |
| 32 | + | drains = s.sink.drains.Load() | |
| 33 | + | seq = s.flips["HEADLESS-1"].Seq | |
| 34 | + | _, reported = s.reports["HEADLESS-1"] | |
| 35 | + | }) | |
| 36 | + | if drains != 1 { | |
| 37 | + | t.Fatalf("%d drains for 5 flips pushed while busy, want 1", drains) | |
| 38 | + | } | |
| 39 | + | if !reported || seq != 5 { | |
| 40 | + | t.Fatalf("last flip %d, reported %v: every flip must still be answered in order", seq, reported) | |
| 41 | + | } | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | // A full ring merges the oldest flip of an output into its next one. | |
| 45 | + | func TestPresentationRingMergesPastBound(t *testing.T) { | |
| 46 | + | p := &presentSink{ring: make([]ports.OutputPresented, 0, maxPresentRing)} | |
| 47 | + | for i := range maxPresentRing { | |
| 48 | + | p.ring = append(p.ring, ports.OutputPresented{Output: "A", Flip: &ports.FlipInfo{Seq: uint64(i + 1)}}) | |
| 49 | + | } | |
| 50 | + | p.mu.Lock() | |
| 51 | + | p.dropOldestLocked() | |
| 52 | + | p.mu.Unlock() | |
| 53 | + | if len(p.ring) != maxPresentRing-1 || p.ring[0].Flip.Seq != 2 || p.ring[0].Flip.Merged != 1 { | |
| 54 | + | t.Fatalf("ring %d, first seq %d merged %d", len(p.ring), p.ring[0].Flip.Seq, p.ring[0].Flip.Merged) | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // Pushing a report and draining it on the display goroutine allocates nothing | |
| 59 | + | // in steady state. The count includes the display goroutine: AllocsPerRun | |
| 60 | + | // reads the global malloc counter. | |
| 61 | + | func TestPresentationPushAllocations(t *testing.T) { | |
| 62 | + | if raceEnabled { | |
| 63 | + | t.Skip("the race detector makes the purego wake callback allocate") | |
| 64 | + | } | |
| 65 | + | s, _, _, _, sink, _ := presentServer(t) | |
| 66 | + | r := ports.OutputPresented{Output: "HEADLESS-1"} | |
| 67 | + | for range 4 { | |
| 68 | + | sink.Push(r) | |
| 69 | + | s.display.Do(func() {}) | |
| 70 | + | } | |
| 71 | + | allocs := testing.AllocsPerRun(200, func() { | |
| 72 | + | want := s.sink.drains.Load() + 1 | |
| 73 | + | sink.Push(r) | |
| 74 | + | for s.sink.drains.Load() < want { // wait for the drain without allocating | |
| 75 | + | runtime.Gosched() | |
| 76 | + | } | |
| 77 | + | }) | |
| 78 | + | if allocs != 0 { | |
| 79 | + | t.Errorf("push + drain: %.1f allocs, want 0", allocs) | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | // A fifo barrier on an output that does not flip clears at barrierAt+period, | |
| 84 | + | // driven by the pace timerfd: the next frame is not late by a timer tick. | |
| 85 | + | func TestPaceTimerfdClearsBarrierOnDeadline(t *testing.T) { | |
| 86 | + | s, events, _, contents, dir := contentServer(t) | |
| 87 | + | c := protocolClient(t, s, dir) | |
| 88 | + | w, surf, _ := surfaceMapper(t, c, events)() | |
| 89 | + | manager := bindProtocol(t, c, "wp_fifo_manager_v1") | |
| 90 | + | f := c.AllocateID() | |
| 91 | + | registerProtocol(t, c, f) | |
| 92 | + | requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, surf) | |
| 93 | + | buf := shmBuffer(t, c) | |
| 94 | + | drainContents(contents) | |
| 95 | + | ||
| 96 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier) | |
| 97 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 98 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 99 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier) | |
| 100 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 101 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 102 | + | if err := c.Roundtrip(); err != nil { | |
| 103 | + | t.Fatal(err) | |
| 104 | + | } | |
| 105 | + | var barrierAt time.Time | |
| 106 | + | var period time.Duration | |
| 107 | + | s.display.Do(func() { | |
| 108 | + | root := s.windows[w.ID].xdg.surface | |
| 109 | + | barrierAt, period = root.barrierAt, s.outputPeriod(s.fifoOutput(root)) | |
| 110 | + | }) | |
| 111 | + | nextContent(t, contents, w.ID) // the first commit | |
| 112 | + | late := nextContent(t, contents, w.ID).Sub(barrierAt.Add(period)) | |
| 113 | + | if late < -time.Millisecond || late > 2*time.Millisecond { | |
| 114 | + | t.Fatalf("queued commit applied %v after barrierAt+%v", late, period) | |
| 115 | + | } | |
| 116 | + | } | |
| @@ -0,0 +1,116 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "runtime" | |||
| 5 | + | "testing" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/go-wayland-bindings/server/fifo" | |||
| 9 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 10 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // Flips pushed while the display goroutine is busy are paced by one drain: | |||
| 14 | + | // one tick, one burst of frame callbacks per output, not one per stale flip. | |||
| 15 | + | func TestPresentationPushCoalescesWhileDisplayBusy(t *testing.T) { | |||
| 16 | + | s, _, _, _, sink, _ := presentServer(t) | |||
| 17 | + | release, started := make(chan struct{}), make(chan struct{}) | |||
| 18 | + | finished := make(chan struct{}) | |||
| 19 | + | go func() { | |||
| 20 | + | s.display.Do(func() { close(started); <-release }) | |||
| 21 | + | close(finished) | |||
| 22 | + | }() | |||
| 23 | + | <-started | |||
| 24 | + | for i := range 5 { | |||
| 25 | + | sink.Push(ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{Seq: uint64(i + 1), HardwareClock: true}}) | |||
| 26 | + | } | |||
| 27 | + | close(release) | |||
| 28 | + | <-finished | |||
| 29 | + | var drains, seq uint64 | |||
| 30 | + | var reported bool | |||
| 31 | + | s.display.Do(func() { | |||
| 32 | + | drains = s.sink.drains.Load() | |||
| 33 | + | seq = s.flips["HEADLESS-1"].Seq | |||
| 34 | + | _, reported = s.reports["HEADLESS-1"] | |||
| 35 | + | }) | |||
| 36 | + | if drains != 1 { | |||
| 37 | + | t.Fatalf("%d drains for 5 flips pushed while busy, want 1", drains) | |||
| 38 | + | } | |||
| 39 | + | if !reported || seq != 5 { | |||
| 40 | + | t.Fatalf("last flip %d, reported %v: every flip must still be answered in order", seq, reported) | |||
| 41 | + | } | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | // A full ring merges the oldest flip of an output into its next one. | |||
| 45 | + | func TestPresentationRingMergesPastBound(t *testing.T) { | |||
| 46 | + | p := &presentSink{ring: make([]ports.OutputPresented, 0, maxPresentRing)} | |||
| 47 | + | for i := range maxPresentRing { | |||
| 48 | + | p.ring = append(p.ring, ports.OutputPresented{Output: "A", Flip: &ports.FlipInfo{Seq: uint64(i + 1)}}) | |||
| 49 | + | } | |||
| 50 | + | p.mu.Lock() | |||
| 51 | + | p.dropOldestLocked() | |||
| 52 | + | p.mu.Unlock() | |||
| 53 | + | if len(p.ring) != maxPresentRing-1 || p.ring[0].Flip.Seq != 2 || p.ring[0].Flip.Merged != 1 { | |||
| 54 | + | t.Fatalf("ring %d, first seq %d merged %d", len(p.ring), p.ring[0].Flip.Seq, p.ring[0].Flip.Merged) | |||
| 55 | + | } | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // Pushing a report and draining it on the display goroutine allocates nothing | |||
| 59 | + | // in steady state. The count includes the display goroutine: AllocsPerRun | |||
| 60 | + | // reads the global malloc counter. | |||
| 61 | + | func TestPresentationPushAllocations(t *testing.T) { | |||
| 62 | + | if raceEnabled { | |||
| 63 | + | t.Skip("the race detector makes the purego wake callback allocate") | |||
| 64 | + | } | |||
| 65 | + | s, _, _, _, sink, _ := presentServer(t) | |||
| 66 | + | r := ports.OutputPresented{Output: "HEADLESS-1"} | |||
| 67 | + | for range 4 { | |||
| 68 | + | sink.Push(r) | |||
| 69 | + | s.display.Do(func() {}) | |||
| 70 | + | } | |||
| 71 | + | allocs := testing.AllocsPerRun(200, func() { | |||
| 72 | + | want := s.sink.drains.Load() + 1 | |||
| 73 | + | sink.Push(r) | |||
| 74 | + | for s.sink.drains.Load() < want { // wait for the drain without allocating | |||
| 75 | + | runtime.Gosched() | |||
| 76 | + | } | |||
| 77 | + | }) | |||
| 78 | + | if allocs != 0 { | |||
| 79 | + | t.Errorf("push + drain: %.1f allocs, want 0", allocs) | |||
| 80 | + | } | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | // A fifo barrier on an output that does not flip clears at barrierAt+period, | |||
| 84 | + | // driven by the pace timerfd: the next frame is not late by a timer tick. | |||
| 85 | + | func TestPaceTimerfdClearsBarrierOnDeadline(t *testing.T) { | |||
| 86 | + | s, events, _, contents, dir := contentServer(t) | |||
| 87 | + | c := protocolClient(t, s, dir) | |||
| 88 | + | w, surf, _ := surfaceMapper(t, c, events)() | |||
| 89 | + | manager := bindProtocol(t, c, "wp_fifo_manager_v1") | |||
| 90 | + | f := c.AllocateID() | |||
| 91 | + | registerProtocol(t, c, f) | |||
| 92 | + | requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, surf) | |||
| 93 | + | buf := shmBuffer(t, c) | |||
| 94 | + | drainContents(contents) | |||
| 95 | + | ||||
| 96 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier) | |||
| 97 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 98 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 99 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier) | |||
| 100 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 101 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 102 | + | if err := c.Roundtrip(); err != nil { | |||
| 103 | + | t.Fatal(err) | |||
| 104 | + | } | |||
| 105 | + | var barrierAt time.Time | |||
| 106 | + | var period time.Duration | |||
| 107 | + | s.display.Do(func() { | |||
| 108 | + | root := s.windows[w.ID].xdg.surface | |||
| 109 | + | barrierAt, period = root.barrierAt, s.outputPeriod(s.fifoOutput(root)) | |||
| 110 | + | }) | |||
| 111 | + | nextContent(t, contents, w.ID) // the first commit | |||
| 112 | + | late := nextContent(t, contents, w.ID).Sub(barrierAt.Add(period)) | |||
| 113 | + | if late < -time.Millisecond || late > 2*time.Millisecond { | |||
| 114 | + | t.Fatalf("queued commit applied %v after barrierAt+%v", late, period) | |||
| 115 | + | } | |||
| 116 | + | } | |||
A internal/adapters/wayland/race_test.go +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | //go:build race | |
| 2 | + | ||
| 3 | + | package wayland | |
| 4 | + | ||
| 5 | + | // raceEnabled: the race detector makes purego callbacks (the display loop wake) | |
| 6 | + | // allocate, which allocation budgets that cross the display loop cannot count. | |
| 7 | + | const raceEnabled = true | |
| @@ -0,0 +1,7 @@ | |||||
| 1 | + | //go:build race | |||
| 2 | + | ||||
| 3 | + | package wayland | |||
| 4 | + | ||||
| 5 | + | // raceEnabled: the race detector makes purego callbacks (the display loop wake) | |||
| 6 | + | // allocate, which allocation budgets that cross the display loop cannot count. | |||
| 7 | + | const raceEnabled = true | |||
M internal/adapters/wayland/server.go +42 -18
| @@ -71,9 +71,6 @@ type Channels struct { | |||
| 71 | 71 | Workspaces <-chan ports.Workspaces | |
| 72 | 72 | // Cursors receives the cursor the client under the pointer asks for. | |
| 73 | 73 | Cursors chan<- ports.CursorChange | |
| 74 | - | // Presented paces frame callbacks on the outputs' page flips; outputs | |
| 75 | - | // that do not flip (idle, headless) are paced at their refresh rate. | |
| 76 | - | Presented <-chan ports.OutputPresented | |
| 77 | 74 | // Captures carries accepted capture requests, never blocking. Each | |
| 78 | 75 | // accepted request holds one of ports.MaxCaptureInflight credits until its | |
| 79 | 76 | // CaptureDone arrives on Captured, whether or not the client is still | |
| @@ -154,18 +151,20 @@ type Server struct { | |||
| 154 | 151 | awaiting map[string][]*wayland.Callback | |
| 155 | 152 | frameOwners map[*wayland.Callback]*surface | |
| 156 | 153 | frameDue map[string]time.Time | |
| 157 | - | frameReady chan struct{} | |
| 158 | - | // paceTimer is the pacer's deadline, armed by ticks on the display | |
| 159 | - | // goroutine; paceDepth/paceDirty defer wakePacer inside a commit or | |
| 160 | - | // tick (display goroutine only). | |
| 161 | - | paceTimer *time.Timer | |
| 162 | - | paceDepth int | |
| 163 | - | paceDirty bool | |
| 164 | - | // Pacer scratch: periods is used on the display goroutine, while | |
| 165 | - | // flipped and frameReports belong to pace and its synchronous Do call. | |
| 154 | + | // paceFD is the pace deadline timerfd, armed by ticks on the display | |
| 155 | + | // goroutine (paceReady: registered with the display loop); | |
| 156 | + | // paceDepth/paceDirty defer wakePacer inside a commit or tick (display | |
| 157 | + | // goroutine only). paceSignal coalesces posted ticks (paceSignalFn). | |
| 158 | + | paceFD int | |
| 159 | + | paceReady bool | |
| 160 | + | paceSignal atomic.Bool | |
| 161 | + | paceSignalFn func() | |
| 162 | + | paceDepth int | |
| 163 | + | paceDirty bool | |
| 164 | + | // sink receives the outputs' reports (presented.go). | |
| 165 | + | sink *presentSink | |
| 166 | + | // framePeriods is the tick's scratch (display goroutine only). | |
| 166 | 167 | framePeriods map[string]time.Duration | |
| 167 | - | flipped map[string]bool | |
| 168 | - | frameReports []ports.OutputPresented | |
| 169 | 168 | // held buffers wait until no output reads them (release.go); reports | |
| 170 | 169 | // are the outputs' latest ports.OutputPresented. | |
| 171 | 170 | held []heldBuffer | |
| @@ -319,7 +318,6 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 319 | 318 | display: d, | |
| 320 | 319 | awaiting: map[string][]*wayland.Callback{}, | |
| 321 | 320 | frameDue: map[string]time.Time{}, | |
| 322 | - | frameReady: make(chan struct{}, 1), | |
| 323 | 321 | reports: map[string]ports.OutputPresented{}, | |
| 324 | 322 | env: linuxProcEnv{}, | |
| 325 | 323 | name: name, | |
| @@ -420,7 +418,12 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 420 | 418 | s.seat.keymapFD, s.seat.keymapSize, s.seat.keymapText = fd, size, opts.Keymap | |
| 421 | 419 | } | |
| 422 | 420 | s.syncWait.log = log | |
| 421 | + | s.sink = newPresentSink(s) | |
| 422 | + | s.paceSignalFn = s.paceSignalled | |
| 423 | 423 | s.cleanup = func() { | |
| 424 | + | if s.paceReady { | |
| 425 | + | unix.Close(s.paceFD) | |
| 426 | + | } | |
| 424 | 427 | if s.securityStop >= 0 { | |
| 425 | 428 | unix.Close(s.securityStop) | |
| 426 | 429 | } | |
| @@ -441,6 +444,11 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 441 | 444 | d.Close() | |
| 442 | 445 | return nil, err | |
| 443 | 446 | } | |
| 447 | + | if err = s.openPaceTimer(); err != nil { | |
| 448 | + | s.cleanup() | |
| 449 | + | d.Close() | |
| 450 | + | return nil, err | |
| 451 | + | } | |
| 444 | 452 | if err = registerGlobals(d, opts, s); err != nil { | |
| 445 | 453 | s.cleanup() | |
| 446 | 454 | d.Close() | |
| @@ -451,6 +459,24 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 451 | 459 | } | |
| 452 | 460 | return s, nil | |
| 453 | 461 | } | |
| 462 | + | ||
| 463 | + | // openPaceTimer creates the pace timerfd and registers it with the display | |
| 464 | + | // loop (before Run, as AddFD requires). | |
| 465 | + | func (s *Server) openPaceTimer() error { | |
| 466 | + | fd, err := unix.TimerfdCreate(unix.CLOCK_MONOTONIC, unix.TFD_NONBLOCK|unix.TFD_CLOEXEC) | |
| 467 | + | if err != nil { | |
| 468 | + | return err | |
| 469 | + | } | |
| 470 | + | // libwayland duplicates fd: ours stays open for TimerfdSettime and is | |
| 471 | + | // closed by cleanup; the source goes with the display. | |
| 472 | + | if _, err = s.display.AddFD(fd, s.onPaceTimer); err != nil { | |
| 473 | + | unix.Close(fd) | |
| 474 | + | return err | |
| 475 | + | } | |
| 476 | + | s.paceFD, s.paceReady = fd, true | |
| 477 | + | return nil | |
| 478 | + | } | |
| 479 | + | ||
| 454 | 480 | func (s *Server) SocketName() string { return s.name } | |
| 455 | 481 | func (s *Server) Run(ctx context.Context) error { | |
| 456 | 482 | defer s.cleanup() | |
| @@ -464,9 +490,8 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 464 | 490 | s.log.Info().Str("socket", s.name).Msg("starting wayland") | |
| 465 | 491 | s.started = time.Now() | |
| 466 | 492 | s.ctx = ctx | |
| 467 | - | s.paceTimer = time.NewTimer(defaultFramePeriod) | |
| 468 | 493 | var wg sync.WaitGroup | |
| 469 | - | wg.Add(15) | |
| 494 | + | wg.Add(14) | |
| 470 | 495 | go func() { defer wg.Done(); s.forwardBackendSecurity(ctx) }() | |
| 471 | 496 | go func() { defer wg.Done(); s.forwardSecurityEvents(ctx) }() | |
| 472 | 497 | go func() { defer wg.Done(); s.forwardLeases(ctx) }() | |
| @@ -507,7 +532,6 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 507 | 532 | } | |
| 508 | 533 | } | |
| 509 | 534 | }() | |
| 510 | - | go func() { defer wg.Done(); s.pace(ctx) }() | |
| 511 | 535 | go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }() | |
| 512 | 536 | go func() { defer wg.Done(); s.forwardOutputHeads(ctx) }() | |
| 513 | 537 | go func() { defer wg.Done(); s.forwardOutputApplied(ctx) }() | |
| @@ -71,9 +71,6 @@ type Channels struct { | |||||
| 71 | Workspaces <-chan ports.Workspaces | 71 | Workspaces <-chan ports.Workspaces | ||
| 72 | // Cursors receives the cursor the client under the pointer asks for. | 72 | // Cursors receives the cursor the client under the pointer asks for. | ||
| 73 | Cursors chan<- ports.CursorChange | 73 | Cursors chan<- ports.CursorChange | ||
| 74 | - | // Presented paces frame callbacks on the outputs' page flips; outputs | |||
| 75 | - | // that do not flip (idle, headless) are paced at their refresh rate. | |||
| 76 | - | Presented <-chan ports.OutputPresented | |||
| 77 | // Captures carries accepted capture requests, never blocking. Each | 74 | // Captures carries accepted capture requests, never blocking. Each | ||
| 78 | // accepted request holds one of ports.MaxCaptureInflight credits until its | 75 | // accepted request holds one of ports.MaxCaptureInflight credits until its | ||
| 79 | // CaptureDone arrives on Captured, whether or not the client is still | 76 | // CaptureDone arrives on Captured, whether or not the client is still | ||
| @@ -154,18 +151,20 @@ type Server struct { | |||||
| 154 | awaiting map[string][]*wayland.Callback | 151 | awaiting map[string][]*wayland.Callback | ||
| 155 | frameOwners map[*wayland.Callback]*surface | 152 | frameOwners map[*wayland.Callback]*surface | ||
| 156 | frameDue map[string]time.Time | 153 | frameDue map[string]time.Time | ||
| 157 | - | frameReady chan struct{} | 154 | + | // paceFD is the pace deadline timerfd, armed by ticks on the display |
| 158 | - | // paceTimer is the pacer's deadline, armed by ticks on the display | 155 | + | // goroutine (paceReady: registered with the display loop); |
| 159 | - | // goroutine; paceDepth/paceDirty defer wakePacer inside a commit or | 156 | + | // paceDepth/paceDirty defer wakePacer inside a commit or tick (display |
| 160 | - | // tick (display goroutine only). | 157 | + | // goroutine only). paceSignal coalesces posted ticks (paceSignalFn). |
| 161 | - | paceTimer *time.Timer | 158 | + | paceFD int |
| 162 | - | paceDepth int | 159 | + | paceReady bool |
| 163 | - | paceDirty bool | 160 | + | paceSignal atomic.Bool |
| 164 | - | // Pacer scratch: periods is used on the display goroutine, while | 161 | + | paceSignalFn func() |
| 165 | - | // flipped and frameReports belong to pace and its synchronous Do call. | 162 | + | paceDepth int |
| 163 | + | paceDirty bool | |||
| 164 | + | // sink receives the outputs' reports (presented.go). | |||
| 165 | + | sink *presentSink | |||
| 166 | + | // framePeriods is the tick's scratch (display goroutine only). | |||
| 166 | framePeriods map[string]time.Duration | 167 | framePeriods map[string]time.Duration | ||
| 167 | - | flipped map[string]bool | |||
| 168 | - | frameReports []ports.OutputPresented | |||
| 169 | // held buffers wait until no output reads them (release.go); reports | 168 | // held buffers wait until no output reads them (release.go); reports | ||
| 170 | // are the outputs' latest ports.OutputPresented. | 169 | // are the outputs' latest ports.OutputPresented. | ||
| 171 | held []heldBuffer | 170 | held []heldBuffer | ||
| @@ -319,7 +318,6 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 319 | display: d, | 318 | display: d, | ||
| 320 | awaiting: map[string][]*wayland.Callback{}, | 319 | awaiting: map[string][]*wayland.Callback{}, | ||
| 321 | frameDue: map[string]time.Time{}, | 320 | frameDue: map[string]time.Time{}, | ||
| 322 | - | frameReady: make(chan struct{}, 1), | |||
| 323 | reports: map[string]ports.OutputPresented{}, | 321 | reports: map[string]ports.OutputPresented{}, | ||
| 324 | env: linuxProcEnv{}, | 322 | env: linuxProcEnv{}, | ||
| 325 | name: name, | 323 | name: name, | ||
| @@ -420,7 +418,12 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 420 | s.seat.keymapFD, s.seat.keymapSize, s.seat.keymapText = fd, size, opts.Keymap | 418 | s.seat.keymapFD, s.seat.keymapSize, s.seat.keymapText = fd, size, opts.Keymap | ||
| 421 | } | 419 | } | ||
| 422 | s.syncWait.log = log | 420 | s.syncWait.log = log | ||
| 421 | + | s.sink = newPresentSink(s) | |||
| 422 | + | s.paceSignalFn = s.paceSignalled | |||
| 423 | s.cleanup = func() { | 423 | s.cleanup = func() { | ||
| 424 | + | if s.paceReady { | |||
| 425 | + | unix.Close(s.paceFD) | |||
| 426 | + | } | |||
| 424 | if s.securityStop >= 0 { | 427 | if s.securityStop >= 0 { | ||
| 425 | unix.Close(s.securityStop) | 428 | unix.Close(s.securityStop) | ||
| 426 | } | 429 | } | ||
| @@ -441,6 +444,11 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 441 | d.Close() | 444 | d.Close() | ||
| 442 | return nil, err | 445 | return nil, err | ||
| 443 | } | 446 | } | ||
| 447 | + | if err = s.openPaceTimer(); err != nil { | |||
| 448 | + | s.cleanup() | |||
| 449 | + | d.Close() | |||
| 450 | + | return nil, err | |||
| 451 | + | } | |||
| 444 | if err = registerGlobals(d, opts, s); err != nil { | 452 | if err = registerGlobals(d, opts, s); err != nil { | ||
| 445 | s.cleanup() | 453 | s.cleanup() | ||
| 446 | d.Close() | 454 | d.Close() | ||
| @@ -451,6 +459,24 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 451 | } | 459 | } | ||
| 452 | return s, nil | 460 | return s, nil | ||
| 453 | } | 461 | } | ||
| 462 | + | ||||
| 463 | + | // openPaceTimer creates the pace timerfd and registers it with the display | |||
| 464 | + | // loop (before Run, as AddFD requires). | |||
| 465 | + | func (s *Server) openPaceTimer() error { | |||
| 466 | + | fd, err := unix.TimerfdCreate(unix.CLOCK_MONOTONIC, unix.TFD_NONBLOCK|unix.TFD_CLOEXEC) | |||
| 467 | + | if err != nil { | |||
| 468 | + | return err | |||
| 469 | + | } | |||
| 470 | + | // libwayland duplicates fd: ours stays open for TimerfdSettime and is | |||
| 471 | + | // closed by cleanup; the source goes with the display. | |||
| 472 | + | if _, err = s.display.AddFD(fd, s.onPaceTimer); err != nil { | |||
| 473 | + | unix.Close(fd) | |||
| 474 | + | return err | |||
| 475 | + | } | |||
| 476 | + | s.paceFD, s.paceReady = fd, true | |||
| 477 | + | return nil | |||
| 478 | + | } | |||
| 479 | + | ||||
| 454 | func (s *Server) SocketName() string { return s.name } | 480 | func (s *Server) SocketName() string { return s.name } | ||
| 455 | func (s *Server) Run(ctx context.Context) error { | 481 | func (s *Server) Run(ctx context.Context) error { | ||
| 456 | defer s.cleanup() | 482 | defer s.cleanup() | ||
| @@ -464,9 +490,8 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 464 | s.log.Info().Str("socket", s.name).Msg("starting wayland") | 490 | s.log.Info().Str("socket", s.name).Msg("starting wayland") | ||
| 465 | s.started = time.Now() | 491 | s.started = time.Now() | ||
| 466 | s.ctx = ctx | 492 | s.ctx = ctx | ||
| 467 | - | s.paceTimer = time.NewTimer(defaultFramePeriod) | |||
| 468 | var wg sync.WaitGroup | 493 | var wg sync.WaitGroup | ||
| 469 | - | wg.Add(15) | 494 | + | wg.Add(14) |
| 470 | go func() { defer wg.Done(); s.forwardBackendSecurity(ctx) }() | 495 | go func() { defer wg.Done(); s.forwardBackendSecurity(ctx) }() | ||
| 471 | go func() { defer wg.Done(); s.forwardSecurityEvents(ctx) }() | 496 | go func() { defer wg.Done(); s.forwardSecurityEvents(ctx) }() | ||
| 472 | go func() { defer wg.Done(); s.forwardLeases(ctx) }() | 497 | go func() { defer wg.Done(); s.forwardLeases(ctx) }() | ||
| @@ -507,7 +532,6 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 507 | } | 532 | } | ||
| 508 | } | 533 | } | ||
| 509 | }() | 534 | }() | ||
| 510 | - | go func() { defer wg.Done(); s.pace(ctx) }() | |||
| 511 | go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }() | 535 | go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }() | ||
| 512 | go func() { defer wg.Done(); s.forwardOutputHeads(ctx) }() | 536 | go func() { defer wg.Done(); s.forwardOutputHeads(ctx) }() | ||
| 513 | go func() { defer wg.Done(); s.forwardOutputApplied(ctx) }() | 537 | go func() { defer wg.Done(); s.forwardOutputApplied(ctx) }() | ||
M internal/adapters/wayland/surface_perf_test.go +1 -1
| @@ -35,7 +35,7 @@ func TestTiledCommitPublishAllocations(t *testing.T) { | |||
| 35 | 35 | // The top-level test is listed in Makefile perf-check; this subtest guards | |
| 36 | 36 | // the per-commit update storage after its first retirement. | |
| 37 | 37 | t.Run("queueUpdateSteadyState", func(t *testing.T) { | |
| 38 | - | srv := &Server{fifoSurfaces: make(map[*surface]struct{}), frameReady: make(chan struct{}, 1)} | |
| 38 | + | srv := &Server{fifoSurfaces: make(map[*surface]struct{})} | |
| 39 | 39 | surf := &surface{server: srv} | |
| 40 | 40 | commit := func() { | |
| 41 | 41 | surf.queueUpdate() | |
| @@ -35,7 +35,7 @@ func TestTiledCommitPublishAllocations(t *testing.T) { | |||||
| 35 | // The top-level test is listed in Makefile perf-check; this subtest guards | 35 | // The top-level test is listed in Makefile perf-check; this subtest guards | ||
| 36 | // the per-commit update storage after its first retirement. | 36 | // the per-commit update storage after its first retirement. | ||
| 37 | t.Run("queueUpdateSteadyState", func(t *testing.T) { | 37 | t.Run("queueUpdateSteadyState", func(t *testing.T) { | ||
| 38 | - | srv := &Server{fifoSurfaces: make(map[*surface]struct{}), frameReady: make(chan struct{}, 1)} | 38 | + | srv := &Server{fifoSurfaces: make(map[*surface]struct{})} |
| 39 | surf := &surface{server: srv} | 39 | surf := &surface{server: srv} | ||
| 40 | commit := func() { | 40 | commit := func() { | ||
| 41 | surf.queueUpdate() | 41 | surf.queueUpdate() | ||
M internal/adapters/wayland/syncobj_test.go +4 -5
| @@ -49,7 +49,7 @@ type syncHarness struct { | |||
| 49 | 49 | c *wlturbo.Display | |
| 50 | 50 | dev *mocksyncobjDevice | |
| 51 | 51 | contents chan ports.SurfaceContent | |
| 52 | - | presented chan ports.OutputPresented | |
| 52 | + | presented ports.PresentationSink | |
| 53 | 53 | surf uint32 | |
| 54 | 54 | syncSurf uint32 | |
| 55 | 55 | timeline uint32 | |
| @@ -100,9 +100,8 @@ func newSyncHarness(t *testing.T) *syncHarness { | |||
| 100 | 100 | events := make(chan ports.ClientEvent, 16) | |
| 101 | 101 | commands := make(chan ports.ClientCommand, 16) | |
| 102 | 102 | h.contents = make(chan ports.SurfaceContent, 64) | |
| 103 | - | h.presented = make(chan ports.OutputPresented, 16) | |
| 104 | 103 | sup := ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}} | |
| 105 | - | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup, syncDev: h.dev}, Channels{Events: events, Commands: commands, Contents: h.contents, Presented: h.presented}, logging.For(context.Background(), "wayland")) | |
| 104 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup, syncDev: h.dev}, Channels{Events: events, Commands: commands, Contents: h.contents}, logging.For(context.Background(), "wayland")) | |
| 106 | 105 | if err != nil { | |
| 107 | 106 | t.Fatal(err) | |
| 108 | 107 | } | |
| @@ -118,7 +117,7 @@ func newSyncHarness(t *testing.T) *syncHarness { | |||
| 118 | 117 | } | |
| 119 | 118 | h.mu.Unlock() | |
| 120 | 119 | }) | |
| 121 | - | h.s = s | |
| 120 | + | h.s, h.presented = s, s.Presentation() | |
| 122 | 121 | h.c = protocolClient(t, s, dir) | |
| 123 | 122 | w, surf, xdg := surfaceMapper(t, h.c, events)() | |
| 124 | 123 | registerProtocol(t, h.c, xdg) | |
| @@ -275,7 +274,7 @@ func TestSyncobjAcquireAndRelease(t *testing.T) { | |||
| 275 | 274 | t.Fatal("acquire fence closed before release") | |
| 276 | 275 | } | |
| 277 | 276 | // The output read the second content: the first buffer is released. | |
| 278 | - | h.presented <- ports.OutputPresented{Output: "HEADLESS-1", Seen: map[ports.WindowID]uint64{h.win: second.Seq}} | |
| 277 | + | h.presented.Push(ports.OutputPresented{Output: "HEADLESS-1", Seen: map[ports.WindowID]uint64{h.win: second.Seq}}) | |
| 279 | 278 | deadline := time.Now().Add(2 * time.Second) | |
| 280 | 279 | for len(h.signalled()) == 0 && time.Now().Before(deadline) { | |
| 281 | 280 | if err := h.c.Roundtrip(); err != nil { | |
| @@ -49,7 +49,7 @@ type syncHarness struct { | |||||
| 49 | c *wlturbo.Display | 49 | c *wlturbo.Display | ||
| 50 | dev *mocksyncobjDevice | 50 | dev *mocksyncobjDevice | ||
| 51 | contents chan ports.SurfaceContent | 51 | contents chan ports.SurfaceContent | ||
| 52 | - | presented chan ports.OutputPresented | 52 | + | presented ports.PresentationSink |
| 53 | surf uint32 | 53 | surf uint32 | ||
| 54 | syncSurf uint32 | 54 | syncSurf uint32 | ||
| 55 | timeline uint32 | 55 | timeline uint32 | ||
| @@ -100,9 +100,8 @@ func newSyncHarness(t *testing.T) *syncHarness { | |||||
| 100 | events := make(chan ports.ClientEvent, 16) | 100 | events := make(chan ports.ClientEvent, 16) | ||
| 101 | commands := make(chan ports.ClientCommand, 16) | 101 | commands := make(chan ports.ClientCommand, 16) | ||
| 102 | h.contents = make(chan ports.SurfaceContent, 64) | 102 | h.contents = make(chan ports.SurfaceContent, 64) | ||
| 103 | - | h.presented = make(chan ports.OutputPresented, 16) | |||
| 104 | sup := ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}} | 103 | sup := ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}} | ||
| 105 | - | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup, syncDev: h.dev}, Channels{Events: events, Commands: commands, Contents: h.contents, Presented: h.presented}, logging.For(context.Background(), "wayland")) | 104 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup, syncDev: h.dev}, Channels{Events: events, Commands: commands, Contents: h.contents}, logging.For(context.Background(), "wayland")) |
| 106 | if err != nil { | 105 | if err != nil { | ||
| 107 | t.Fatal(err) | 106 | t.Fatal(err) | ||
| 108 | } | 107 | } | ||
| @@ -118,7 +117,7 @@ func newSyncHarness(t *testing.T) *syncHarness { | |||||
| 118 | } | 117 | } | ||
| 119 | h.mu.Unlock() | 118 | h.mu.Unlock() | ||
| 120 | }) | 119 | }) | ||
| 121 | - | h.s = s | 120 | + | h.s, h.presented = s, s.Presentation() |
| 122 | h.c = protocolClient(t, s, dir) | 121 | h.c = protocolClient(t, s, dir) | ||
| 123 | w, surf, xdg := surfaceMapper(t, h.c, events)() | 122 | w, surf, xdg := surfaceMapper(t, h.c, events)() | ||
| 124 | registerProtocol(t, h.c, xdg) | 123 | registerProtocol(t, h.c, xdg) | ||
| @@ -275,7 +274,7 @@ func TestSyncobjAcquireAndRelease(t *testing.T) { | |||||
| 275 | t.Fatal("acquire fence closed before release") | 274 | t.Fatal("acquire fence closed before release") | ||
| 276 | } | 275 | } | ||
| 277 | // The output read the second content: the first buffer is released. | 276 | // The output read the second content: the first buffer is released. | ||
| 278 | - | h.presented <- ports.OutputPresented{Output: "HEADLESS-1", Seen: map[ports.WindowID]uint64{h.win: second.Seq}} | 277 | + | h.presented.Push(ports.OutputPresented{Output: "HEADLESS-1", Seen: map[ports.WindowID]uint64{h.win: second.Seq}}) |
| 279 | deadline := time.Now().Add(2 * time.Second) | 278 | deadline := time.Now().Add(2 * time.Second) | ||
| 280 | for len(h.signalled()) == 0 && time.Now().Before(deadline) { | 279 | for len(h.signalled()) == 0 && time.Now().Before(deadline) { | ||
| 281 | if err := h.c.Roundtrip(); err != nil { | 280 | if err := h.c.Roundtrip(); err != nil { | ||
M internal/adapters/wayland/transform_test.go +1 -1
| @@ -234,7 +234,7 @@ func TestOpaqueRect(t *testing.T) { | |||
| 234 | 234 | // Transform and opaque region requested after a queued commit belong to | |
| 235 | 235 | // the next commit. | |
| 236 | 236 | func TestCapturedTransformAndOpaqueKeepLaterRequests(t *testing.T) { | |
| 237 | - | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1), constraints: map[*surface]*constraint{}} | |
| 237 | + | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, constraints: map[*surface]*constraint{}} | |
| 238 | 238 | surf := &surface{server: srv} | |
| 239 | 239 | surf.next.transform = 1 | |
| 240 | 240 | surf.next.opaque, surf.next.opaqueSet = []ports.Rect{{W: 4, H: 4}}, true | |
| @@ -234,7 +234,7 @@ func TestOpaqueRect(t *testing.T) { | |||||
| 234 | // Transform and opaque region requested after a queued commit belong to | 234 | // Transform and opaque region requested after a queued commit belong to | ||
| 235 | // the next commit. | 235 | // the next commit. | ||
| 236 | func TestCapturedTransformAndOpaqueKeepLaterRequests(t *testing.T) { | 236 | func TestCapturedTransformAndOpaqueKeepLaterRequests(t *testing.T) { | ||
| 237 | - | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1), constraints: map[*surface]*constraint{}} | 237 | + | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, constraints: map[*surface]*constraint{}} |
| 238 | surf := &surface{server: srv} | 238 | surf := &surface{server: srv} | ||
| 239 | surf.next.transform = 1 | 239 | surf.next.transform = 1 | ||
| 240 | surf.next.opaque, surf.next.opaqueSet = []ports.Rect{{W: 4, H: 4}}, true | 240 | surf.next.opaque, surf.next.opaqueSet = []ports.Rect{{W: 4, H: 4}}, true | ||
M internal/app/app.go +14 -20
| @@ -138,29 +138,23 @@ func relayConfig(ctx context.Context, cur ports.Config, in <-chan ports.ConfigCh | |||
| 138 | 138 | } | |
| 139 | 139 | } | |
| 140 | 140 | ||
| 141 | - | // relayPresented passes output reports to wayland in order. Each flip is | |
| 142 | - | // also a frame for core; a frame core has not read yet is enough, so | |
| 143 | - | // frames never wait. | |
| 144 | - | func relayPresented(ctx context.Context, in <-chan ports.OutputPresented, out chan<- ports.OutputPresented, frames chan<- ports.OutputFrame) { | |
| 145 | - | for { | |
| 146 | - | var p ports.OutputPresented | |
| 147 | - | select { | |
| 148 | - | case <-ctx.Done(): | |
| 149 | - | return | |
| 150 | - | case p = <-in: | |
| 151 | - | } | |
| 152 | - | if p.Flip != nil { | |
| 153 | - | select { | |
| 154 | - | case frames <- ports.OutputFrame{Output: p.Output}: | |
| 155 | - | default: | |
| 156 | - | } | |
| 157 | - | } | |
| 141 | + | // framesTap is the sink outputs report to: it passes every report to wayland | |
| 142 | + | // and tells core about each flip (a frame for running animations). A frame | |
| 143 | + | // core has not read yet is enough, so frames never wait. | |
| 144 | + | type framesTap struct { | |
| 145 | + | sink ports.PresentationSink | |
| 146 | + | frames chan<- ports.OutputFrame | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | // Push implements ports.PresentationSink; it never blocks. | |
| 150 | + | func (t framesTap) Push(p ports.OutputPresented) { | |
| 151 | + | if p.Flip != nil { | |
| 158 | 152 | select { | |
| 159 | - | case out <- p: | |
| 160 | - | case <-ctx.Done(): | |
| 161 | - | return | |
| 153 | + | case t.frames <- ports.OutputFrame{Output: p.Output}: | |
| 154 | + | default: | |
| 162 | 155 | } | |
| 163 | 156 | } | |
| 157 | + | t.sink.Push(p) | |
| 164 | 158 | } | |
| 165 | 159 | ||
| 166 | 160 | func consumeScenes(ctx context.Context, scenes <-chan []ports.Scene, configErrors <-chan error, tap chan<- []ports.Scene, renderScenes chan []ports.Scene) { | |
| @@ -138,29 +138,23 @@ func relayConfig(ctx context.Context, cur ports.Config, in <-chan ports.ConfigCh | |||||
| 138 | } | 138 | } | ||
| 139 | } | 139 | } | ||
| 140 | 140 | ||||
| 141 | - | // relayPresented passes output reports to wayland in order. Each flip is | 141 | + | // framesTap is the sink outputs report to: it passes every report to wayland |
| 142 | - | // also a frame for core; a frame core has not read yet is enough, so | 142 | + | // and tells core about each flip (a frame for running animations). A frame |
| 143 | - | // frames never wait. | 143 | + | // core has not read yet is enough, so frames never wait. |
| 144 | - | func relayPresented(ctx context.Context, in <-chan ports.OutputPresented, out chan<- ports.OutputPresented, frames chan<- ports.OutputFrame) { | 144 | + | type framesTap struct { |
| 145 | - | for { | 145 | + | sink ports.PresentationSink |
| 146 | - | var p ports.OutputPresented | 146 | + | frames chan<- ports.OutputFrame |
| 147 | - | select { | 147 | + | } |
| 148 | - | case <-ctx.Done(): | 148 | + | |
| 149 | - | return | 149 | + | // Push implements ports.PresentationSink; it never blocks. |
| 150 | - | case p = <-in: | 150 | + | func (t framesTap) Push(p ports.OutputPresented) { |
| 151 | - | } | 151 | + | if p.Flip != nil { |
| 152 | - | if p.Flip != nil { | |||
| 153 | - | select { | |||
| 154 | - | case frames <- ports.OutputFrame{Output: p.Output}: | |||
| 155 | - | default: | |||
| 156 | - | } | |||
| 157 | - | } | |||
| 158 | select { | 152 | select { | ||
| 159 | - | case out <- p: | 153 | + | case t.frames <- ports.OutputFrame{Output: p.Output}: |
| 160 | - | case <-ctx.Done(): | 154 | + | default: |
| 161 | - | return | |||
| 162 | } | 155 | } | ||
| 163 | } | 156 | } | ||
| 157 | + | t.sink.Push(p) | |||
| 164 | } | 158 | } | ||
| 165 | 159 | ||||
| 166 | func consumeScenes(ctx context.Context, scenes <-chan []ports.Scene, configErrors <-chan error, tap chan<- []ports.Scene, renderScenes chan []ports.Scene) { | 160 | func consumeScenes(ctx context.Context, scenes <-chan []ports.Scene, configErrors <-chan error, tap chan<- []ports.Scene, renderScenes chan []ports.Scene) { | ||
M internal/app/app_test.go +24 -0
| @@ -17,7 +17,10 @@ import ( | |||
| 17 | 17 | "github.com/bnema/neferwl/internal/adapters/config" | |
| 18 | 18 | "github.com/bnema/neferwl/internal/adapters/logging" | |
| 19 | 19 | "github.com/bnema/neferwl/internal/adapters/xkb" | |
| 20 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |
| 20 | 21 | "github.com/bnema/neferwl/internal/ports" | |
| 22 | + | "github.com/stretchr/testify/mock" | |
| 23 | + | "github.com/stretchr/testify/require" | |
| 21 | 24 | ) | |
| 22 | 25 | ||
| 23 | 26 | func TestHeadlessConfigReload(t *testing.T) { | |
| @@ -423,3 +426,24 @@ func TestHeadlessTwoOutputs(t *testing.T) { | |||
| 423 | 426 | } | |
| 424 | 427 | } | |
| 425 | 428 | } | |
| 429 | + | ||
| 430 | + | // framesTap passes every report to wayland and tells core about a flip only, | |
| 431 | + | // without ever blocking on a full frames channel. | |
| 432 | + | func TestFramesTapForwardsReportsAndFlipFrames(t *testing.T) { | |
| 433 | + | var got []ports.OutputPresented | |
| 434 | + | sink := portsmocks.NewMockPresentationSink(t) | |
| 435 | + | sink.EXPECT().Push(mock.Anything).RunAndReturn(func(r ports.OutputPresented) { got = append(got, r) }) | |
| 436 | + | frames := make(chan ports.OutputFrame, 1) | |
| 437 | + | tap := framesTap{sink: sink, frames: frames} | |
| 438 | + | ||
| 439 | + | tap.Push(ports.OutputPresented{Output: "DP-2"}) | |
| 440 | + | require.Empty(t, frames, "a report without a flip is not a frame") | |
| 441 | + | tap.Push(ports.OutputPresented{Output: "DP-2", Flip: &ports.FlipInfo{Seq: 1}}) | |
| 442 | + | require.Equal(t, ports.OutputFrame{Output: "DP-2"}, <-frames) | |
| 443 | + | ||
| 444 | + | tap.Push(ports.OutputPresented{Output: "DP-2", Flip: &ports.FlipInfo{Seq: 2}}) | |
| 445 | + | tap.Push(ports.OutputPresented{Output: "DP-2", Flip: &ports.FlipInfo{Seq: 3}}) // frames full: must not block | |
| 446 | + | require.Len(t, frames, 1) | |
| 447 | + | require.Len(t, got, 4, "wayland receives every report") | |
| 448 | + | require.Equal(t, uint64(3), got[3].Flip.Seq) | |
| 449 | + | } | |
| @@ -17,7 +17,10 @@ import ( | |||||
| 17 | "github.com/bnema/neferwl/internal/adapters/config" | 17 | "github.com/bnema/neferwl/internal/adapters/config" | ||
| 18 | "github.com/bnema/neferwl/internal/adapters/logging" | 18 | "github.com/bnema/neferwl/internal/adapters/logging" | ||
| 19 | "github.com/bnema/neferwl/internal/adapters/xkb" | 19 | "github.com/bnema/neferwl/internal/adapters/xkb" | ||
| 20 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |||
| 20 | "github.com/bnema/neferwl/internal/ports" | 21 | "github.com/bnema/neferwl/internal/ports" | ||
| 22 | + | "github.com/stretchr/testify/mock" | |||
| 23 | + | "github.com/stretchr/testify/require" | |||
| 21 | ) | 24 | ) | ||
| 22 | 25 | ||||
| 23 | func TestHeadlessConfigReload(t *testing.T) { | 26 | func TestHeadlessConfigReload(t *testing.T) { | ||
| @@ -423,3 +426,24 @@ func TestHeadlessTwoOutputs(t *testing.T) { | |||||
| 423 | } | 426 | } | ||
| 424 | } | 427 | } | ||
| 425 | } | 428 | } | ||
| 429 | + | ||||
| 430 | + | // framesTap passes every report to wayland and tells core about a flip only, | |||
| 431 | + | // without ever blocking on a full frames channel. | |||
| 432 | + | func TestFramesTapForwardsReportsAndFlipFrames(t *testing.T) { | |||
| 433 | + | var got []ports.OutputPresented | |||
| 434 | + | sink := portsmocks.NewMockPresentationSink(t) | |||
| 435 | + | sink.EXPECT().Push(mock.Anything).RunAndReturn(func(r ports.OutputPresented) { got = append(got, r) }) | |||
| 436 | + | frames := make(chan ports.OutputFrame, 1) | |||
| 437 | + | tap := framesTap{sink: sink, frames: frames} | |||
| 438 | + | ||||
| 439 | + | tap.Push(ports.OutputPresented{Output: "DP-2"}) | |||
| 440 | + | require.Empty(t, frames, "a report without a flip is not a frame") | |||
| 441 | + | tap.Push(ports.OutputPresented{Output: "DP-2", Flip: &ports.FlipInfo{Seq: 1}}) | |||
| 442 | + | require.Equal(t, ports.OutputFrame{Output: "DP-2"}, <-frames) | |||
| 443 | + | ||||
| 444 | + | tap.Push(ports.OutputPresented{Output: "DP-2", Flip: &ports.FlipInfo{Seq: 2}}) | |||
| 445 | + | tap.Push(ports.OutputPresented{Output: "DP-2", Flip: &ports.FlipInfo{Seq: 3}}) // frames full: must not block | |||
| 446 | + | require.Len(t, frames, 1) | |||
| 447 | + | require.Len(t, got, 4, "wayland receives every report") | |||
| 448 | + | require.Equal(t, uint64(3), got[3].Flip.Seq) | |||
| 449 | + | } | |||
M internal/app/outputrouting.go +1 -1
| @@ -16,7 +16,7 @@ type outputChannels struct { | |||
| 16 | 16 | scenes <-chan []ports.Scene | |
| 17 | 17 | contents <-chan ports.SurfaceContent | |
| 18 | 18 | cursorChanges <-chan ports.CursorChange | |
| 19 | - | presented chan<- ports.OutputPresented | |
| 19 | + | presented ports.PresentationSink | |
| 20 | 20 | captures <-chan ports.CaptureRequest | |
| 21 | 21 | captured chan<- ports.CaptureDone | |
| 22 | 22 | formats chan<- ports.OutputFormats | |
| @@ -16,7 +16,7 @@ type outputChannels struct { | |||||
| 16 | scenes <-chan []ports.Scene | 16 | scenes <-chan []ports.Scene | ||
| 17 | contents <-chan ports.SurfaceContent | 17 | contents <-chan ports.SurfaceContent | ||
| 18 | cursorChanges <-chan ports.CursorChange | 18 | cursorChanges <-chan ports.CursorChange | ||
| 19 | - | presented chan<- ports.OutputPresented | 19 | + | presented ports.PresentationSink |
| 20 | captures <-chan ports.CaptureRequest | 20 | captures <-chan ports.CaptureRequest | ||
| 21 | captured chan<- ports.CaptureDone | 21 | captured chan<- ports.CaptureDone | ||
| 22 | formats chan<- ports.OutputFormats | 22 | formats chan<- ports.OutputFormats | ||
M internal/app/session.go +1 -3
| @@ -351,12 +351,10 @@ func (s *session) run(ctx context.Context, inject func(chan<- ports.InputEvent)) | |||
| 351 | 351 | defer workers.Done() | |
| 352 | 352 | consumeScenes(ctx, p.scenes, p.configErrors, opts.testScenes, p.renderScenes) | |
| 353 | 353 | }() | |
| 354 | - | workers.Add(1) | |
| 355 | - | go func() { defer workers.Done(); relayPresented(ctx, p.flips, p.presented, p.frames) }() | |
| 356 | 354 | ||
| 357 | 355 | go func() { | |
| 358 | 356 | defer workers.Done() | |
| 359 | - | outputIO := p.outputs(security) | |
| 357 | + | outputIO := p.outputs(security, framesTap{sink: server.Presentation(), frames: p.frames}) | |
| 360 | 358 | apply := newOutputApply(core.NewOutputOverrides(opts.Config, hw == nil), logging.For(ctx, "app")) | |
| 361 | 359 | if hw != nil { | |
| 362 | 360 | done <- safe("output", func() error { | |
| @@ -351,12 +351,10 @@ func (s *session) run(ctx context.Context, inject func(chan<- ports.InputEvent)) | |||||
| 351 | defer workers.Done() | 351 | defer workers.Done() | ||
| 352 | consumeScenes(ctx, p.scenes, p.configErrors, opts.testScenes, p.renderScenes) | 352 | consumeScenes(ctx, p.scenes, p.configErrors, opts.testScenes, p.renderScenes) | ||
| 353 | }() | 353 | }() | ||
| 354 | - | workers.Add(1) | |||
| 355 | - | go func() { defer workers.Done(); relayPresented(ctx, p.flips, p.presented, p.frames) }() | |||
| 356 | 354 | ||||
| 357 | go func() { | 355 | go func() { | ||
| 358 | defer workers.Done() | 356 | defer workers.Done() | ||
| 359 | - | outputIO := p.outputs(security) | 357 | + | outputIO := p.outputs(security, framesTap{sink: server.Presentation(), frames: p.frames}) |
| 360 | apply := newOutputApply(core.NewOutputOverrides(opts.Config, hw == nil), logging.For(ctx, "app")) | 358 | apply := newOutputApply(core.NewOutputOverrides(opts.Config, hw == nil), logging.For(ctx, "app")) | ||
| 361 | if hw != nil { | 359 | if hw != nil { | ||
| 362 | done <- safe("output", func() error { | 360 | done <- safe("output", func() error { | ||
M internal/app/wiring.go +7 -13
| @@ -41,12 +41,9 @@ type pipes struct { | |||
| 41 | 41 | // config file. | |
| 42 | 42 | scales chan ports.ScaleChanged | |
| 43 | 43 | ||
| 44 | - | contents chan ports.SurfaceContent | |
| 45 | - | presented chan ports.OutputPresented | |
| 46 | - | // Outputs report on flips; relayPresented passes them to wayland | |
| 47 | - | // (presented) and tells core about each flip (frames) to move running | |
| 48 | - | // slides. | |
| 49 | - | flips chan ports.OutputPresented | |
| 44 | + | contents chan ports.SurfaceContent | |
| 45 | + | // Outputs report to wayland through its presentation sink and tell core | |
| 46 | + | // about each flip (frames) to move running slides. | |
| 50 | 47 | frames chan ports.OutputFrame | |
| 51 | 48 | ||
| 52 | 49 | captures chan ports.CaptureRequest | |
| @@ -94,10 +91,8 @@ func newPipes(realSession bool) *pipes { | |||
| 94 | 91 | configured: make(chan ports.ConfigChanged, 8), | |
| 95 | 92 | configErrors: make(chan error, 8), | |
| 96 | 93 | ||
| 97 | - | contents: make(chan ports.SurfaceContent, 64), | |
| 98 | - | presented: make(chan ports.OutputPresented, 64), | |
| 99 | - | flips: make(chan ports.OutputPresented, 64), | |
| 100 | - | frames: make(chan ports.OutputFrame, 8), | |
| 94 | + | contents: make(chan ports.SurfaceContent, 64), | |
| 95 | + | frames: make(chan ports.OutputFrame, 8), | |
| 101 | 96 | ||
| 102 | 97 | captures: make(chan ports.CaptureRequest, ports.MaxCaptureInflight), | |
| 103 | 98 | captured: make(chan ports.CaptureDone, ports.MaxCaptureInflight), | |
| @@ -149,7 +144,6 @@ func (p *pipes) wayland() wayland.Channels { | |||
| 149 | 144 | Workspaces: p.workspaces, | |
| 150 | 145 | Contents: p.contents, | |
| 151 | 146 | Cursors: p.cursorChanges, | |
| 152 | - | Presented: p.presented, | |
| 153 | 147 | Captures: p.captures, | |
| 154 | 148 | Captured: p.captured, | |
| 155 | 149 | OutputFormats: p.outputFormats, | |
| @@ -164,7 +158,7 @@ func (p *pipes) wayland() wayland.Channels { | |||
| 164 | 158 | } | |
| 165 | 159 | ||
| 166 | 160 | // outputs is the outputs' side (DRM or headless). | |
| 167 | - | func (p *pipes) outputs(security ports.SessionSecurity) outputChannels { | |
| 161 | + | func (p *pipes) outputs(security ports.SessionSecurity, presented ports.PresentationSink) outputChannels { | |
| 168 | 162 | return outputChannels{ | |
| 169 | 163 | security: security, | |
| 170 | 164 | securityChanges: p.securityChanges, | |
| @@ -173,7 +167,7 @@ func (p *pipes) outputs(security ports.SessionSecurity) outputChannels { | |||
| 173 | 167 | scenes: p.renderScenes, | |
| 174 | 168 | contents: p.contents, | |
| 175 | 169 | cursorChanges: p.cursorChanges, | |
| 176 | - | presented: p.flips, | |
| 170 | + | presented: presented, | |
| 177 | 171 | captures: p.captures, | |
| 178 | 172 | captured: p.captured, | |
| 179 | 173 | formats: p.outputFormats, | |
| @@ -41,12 +41,9 @@ type pipes struct { | |||||
| 41 | // config file. | 41 | // config file. | ||
| 42 | scales chan ports.ScaleChanged | 42 | scales chan ports.ScaleChanged | ||
| 43 | 43 | ||||
| 44 | - | contents chan ports.SurfaceContent | 44 | + | contents chan ports.SurfaceContent |
| 45 | - | presented chan ports.OutputPresented | 45 | + | // Outputs report to wayland through its presentation sink and tell core |
| 46 | - | // Outputs report on flips; relayPresented passes them to wayland | 46 | + | // about each flip (frames) to move running slides. |
| 47 | - | // (presented) and tells core about each flip (frames) to move running | |||
| 48 | - | // slides. | |||
| 49 | - | flips chan ports.OutputPresented | |||
| 50 | frames chan ports.OutputFrame | 47 | frames chan ports.OutputFrame | ||
| 51 | 48 | ||||
| 52 | captures chan ports.CaptureRequest | 49 | captures chan ports.CaptureRequest | ||
| @@ -94,10 +91,8 @@ func newPipes(realSession bool) *pipes { | |||||
| 94 | configured: make(chan ports.ConfigChanged, 8), | 91 | configured: make(chan ports.ConfigChanged, 8), | ||
| 95 | configErrors: make(chan error, 8), | 92 | configErrors: make(chan error, 8), | ||
| 96 | 93 | ||||
| 97 | - | contents: make(chan ports.SurfaceContent, 64), | 94 | + | contents: make(chan ports.SurfaceContent, 64), |
| 98 | - | presented: make(chan ports.OutputPresented, 64), | 95 | + | frames: make(chan ports.OutputFrame, 8), |
| 99 | - | flips: make(chan ports.OutputPresented, 64), | |||
| 100 | - | frames: make(chan ports.OutputFrame, 8), | |||
| 101 | 96 | ||||
| 102 | captures: make(chan ports.CaptureRequest, ports.MaxCaptureInflight), | 97 | captures: make(chan ports.CaptureRequest, ports.MaxCaptureInflight), | ||
| 103 | captured: make(chan ports.CaptureDone, ports.MaxCaptureInflight), | 98 | captured: make(chan ports.CaptureDone, ports.MaxCaptureInflight), | ||
| @@ -149,7 +144,6 @@ func (p *pipes) wayland() wayland.Channels { | |||||
| 149 | Workspaces: p.workspaces, | 144 | Workspaces: p.workspaces, | ||
| 150 | Contents: p.contents, | 145 | Contents: p.contents, | ||
| 151 | Cursors: p.cursorChanges, | 146 | Cursors: p.cursorChanges, | ||
| 152 | - | Presented: p.presented, | |||
| 153 | Captures: p.captures, | 147 | Captures: p.captures, | ||
| 154 | Captured: p.captured, | 148 | Captured: p.captured, | ||
| 155 | OutputFormats: p.outputFormats, | 149 | OutputFormats: p.outputFormats, | ||
| @@ -164,7 +158,7 @@ func (p *pipes) wayland() wayland.Channels { | |||||
| 164 | } | 158 | } | ||
| 165 | 159 | ||||
| 166 | // outputs is the outputs' side (DRM or headless). | 160 | // outputs is the outputs' side (DRM or headless). | ||
| 167 | - | func (p *pipes) outputs(security ports.SessionSecurity) outputChannels { | 161 | + | func (p *pipes) outputs(security ports.SessionSecurity, presented ports.PresentationSink) outputChannels { |
| 168 | return outputChannels{ | 162 | return outputChannels{ | ||
| 169 | security: security, | 163 | security: security, | ||
| 170 | securityChanges: p.securityChanges, | 164 | securityChanges: p.securityChanges, | ||
| @@ -173,7 +167,7 @@ func (p *pipes) outputs(security ports.SessionSecurity) outputChannels { | |||||
| 173 | scenes: p.renderScenes, | 167 | scenes: p.renderScenes, | ||
| 174 | contents: p.contents, | 168 | contents: p.contents, | ||
| 175 | cursorChanges: p.cursorChanges, | 169 | cursorChanges: p.cursorChanges, | ||
| 176 | - | presented: p.flips, | 170 | + | presented: presented, |
| 177 | captures: p.captures, | 171 | captures: p.captures, | ||
| 178 | captured: p.captured, | 172 | captured: p.captured, | ||
| 179 | formats: p.outputFormats, | 173 | formats: p.outputFormats, | ||
A internal/mocks/ports/output_events_mock.go +86 -0
| @@ -0,0 +1,86 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package portsmocks | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | mock "github.com/stretchr/testify/mock" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // NewMockPresentationSink creates a new instance of MockPresentationSink. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 13 | + | // The first argument is typically a *testing.T value. | |
| 14 | + | func NewMockPresentationSink(t interface { | |
| 15 | + | mock.TestingT | |
| 16 | + | Cleanup(func()) | |
| 17 | + | }) *MockPresentationSink { | |
| 18 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 19 | + | helper.Helper() | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | mock := &MockPresentationSink{} | |
| 23 | + | mock.Mock.Test(t) | |
| 24 | + | ||
| 25 | + | t.Cleanup(func() { | |
| 26 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 27 | + | helper.Helper() | |
| 28 | + | } | |
| 29 | + | mock.AssertExpectations(t) | |
| 30 | + | }) | |
| 31 | + | ||
| 32 | + | return mock | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // MockPresentationSink is an autogenerated mock type for the PresentationSink type | |
| 36 | + | type MockPresentationSink struct { | |
| 37 | + | mock.Mock | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | type MockPresentationSink_Expecter struct { | |
| 41 | + | mock *mock.Mock | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | func (_m *MockPresentationSink) EXPECT() *MockPresentationSink_Expecter { | |
| 45 | + | return &MockPresentationSink_Expecter{mock: &_m.Mock} | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // Push provides a mock function for the type MockPresentationSink | |
| 49 | + | func (_mock *MockPresentationSink) Push(outputPresented ports.OutputPresented) { | |
| 50 | + | _mock.Called(outputPresented) | |
| 51 | + | return | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | // MockPresentationSink_Push_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Push' | |
| 55 | + | type MockPresentationSink_Push_Call struct { | |
| 56 | + | *mock.Call | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // Push is a helper method to define mock.On call | |
| 60 | + | // - outputPresented ports.OutputPresented | |
| 61 | + | func (_e *MockPresentationSink_Expecter) Push(outputPresented any) *MockPresentationSink_Push_Call { | |
| 62 | + | return &MockPresentationSink_Push_Call{Call: _e.mock.On("Push", outputPresented)} | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | func (_c *MockPresentationSink_Push_Call) Run(run func(outputPresented ports.OutputPresented)) *MockPresentationSink_Push_Call { | |
| 66 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 67 | + | var arg0 ports.OutputPresented | |
| 68 | + | if args[0] != nil { | |
| 69 | + | arg0 = args[0].(ports.OutputPresented) | |
| 70 | + | } | |
| 71 | + | run( | |
| 72 | + | arg0, | |
| 73 | + | ) | |
| 74 | + | }) | |
| 75 | + | return _c | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | func (_c *MockPresentationSink_Push_Call) Return() *MockPresentationSink_Push_Call { | |
| 79 | + | _c.Call.Return() | |
| 80 | + | return _c | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | func (_c *MockPresentationSink_Push_Call) RunAndReturn(run func(outputPresented ports.OutputPresented)) *MockPresentationSink_Push_Call { | |
| 84 | + | _c.Run(run) | |
| 85 | + | return _c | |
| 86 | + | } | |
| @@ -0,0 +1,86 @@ | |||||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |||
| 2 | + | // github.com/vektra/mockery | |||
| 3 | + | // template: testify | |||
| 4 | + | ||||
| 5 | + | package portsmocks | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | mock "github.com/stretchr/testify/mock" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // NewMockPresentationSink creates a new instance of MockPresentationSink. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |||
| 13 | + | // The first argument is typically a *testing.T value. | |||
| 14 | + | func NewMockPresentationSink(t interface { | |||
| 15 | + | mock.TestingT | |||
| 16 | + | Cleanup(func()) | |||
| 17 | + | }) *MockPresentationSink { | |||
| 18 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 19 | + | helper.Helper() | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | mock := &MockPresentationSink{} | |||
| 23 | + | mock.Mock.Test(t) | |||
| 24 | + | ||||
| 25 | + | t.Cleanup(func() { | |||
| 26 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 27 | + | helper.Helper() | |||
| 28 | + | } | |||
| 29 | + | mock.AssertExpectations(t) | |||
| 30 | + | }) | |||
| 31 | + | ||||
| 32 | + | return mock | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // MockPresentationSink is an autogenerated mock type for the PresentationSink type | |||
| 36 | + | type MockPresentationSink struct { | |||
| 37 | + | mock.Mock | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | type MockPresentationSink_Expecter struct { | |||
| 41 | + | mock *mock.Mock | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | func (_m *MockPresentationSink) EXPECT() *MockPresentationSink_Expecter { | |||
| 45 | + | return &MockPresentationSink_Expecter{mock: &_m.Mock} | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // Push provides a mock function for the type MockPresentationSink | |||
| 49 | + | func (_mock *MockPresentationSink) Push(outputPresented ports.OutputPresented) { | |||
| 50 | + | _mock.Called(outputPresented) | |||
| 51 | + | return | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | // MockPresentationSink_Push_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Push' | |||
| 55 | + | type MockPresentationSink_Push_Call struct { | |||
| 56 | + | *mock.Call | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | // Push is a helper method to define mock.On call | |||
| 60 | + | // - outputPresented ports.OutputPresented | |||
| 61 | + | func (_e *MockPresentationSink_Expecter) Push(outputPresented any) *MockPresentationSink_Push_Call { | |||
| 62 | + | return &MockPresentationSink_Push_Call{Call: _e.mock.On("Push", outputPresented)} | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | func (_c *MockPresentationSink_Push_Call) Run(run func(outputPresented ports.OutputPresented)) *MockPresentationSink_Push_Call { | |||
| 66 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 67 | + | var arg0 ports.OutputPresented | |||
| 68 | + | if args[0] != nil { | |||
| 69 | + | arg0 = args[0].(ports.OutputPresented) | |||
| 70 | + | } | |||
| 71 | + | run( | |||
| 72 | + | arg0, | |||
| 73 | + | ) | |||
| 74 | + | }) | |||
| 75 | + | return _c | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | func (_c *MockPresentationSink_Push_Call) Return() *MockPresentationSink_Push_Call { | |||
| 79 | + | _c.Call.Return() | |||
| 80 | + | return _c | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | func (_c *MockPresentationSink_Push_Call) RunAndReturn(run func(outputPresented ports.OutputPresented)) *MockPresentationSink_Push_Call { | |||
| 84 | + | _c.Run(run) | |||
| 85 | + | return _c | |||
| 86 | + | } | |||
M internal/ports/output_events.go +4 -0
| @@ -94,6 +94,10 @@ type OutputPresented struct { | |||
| 94 | 94 | ChildReads map[WindowID]uint64 | |
| 95 | 95 | } | |
| 96 | 96 | ||
| 97 | + | // PresentationSink receives output → wayland reports. Push never blocks; | |
| 98 | + | // it wakes the consumer. Owner goroutines of outputs call it. | |
| 99 | + | type PresentationSink interface{ Push(OutputPresented) } | |
| 100 | + | ||
| 97 | 101 | // OutputFormats carries output → wayland the dmabuf formats an output can | |
| 98 | 102 | // scan out directly (its primary plane's, that the renderer also samples, | |
| 99 | 103 | // so a refused buffer can still be composed). Device is the KMS device | |
| @@ -94,6 +94,10 @@ type OutputPresented struct { | |||||
| 94 | ChildReads map[WindowID]uint64 | 94 | ChildReads map[WindowID]uint64 | ||
| 95 | } | 95 | } | ||
| 96 | 96 | ||||
| 97 | + | // PresentationSink receives output → wayland reports. Push never blocks; | |||
| 98 | + | // it wakes the consumer. Owner goroutines of outputs call it. | |||
| 99 | + | type PresentationSink interface{ Push(OutputPresented) } | |||
| 100 | + | ||||
| 97 | // OutputFormats carries output → wayland the dmabuf formats an output can | 101 | // OutputFormats carries output → wayland the dmabuf formats an output can | ||
| 98 | // scan out directly (its primary plane's, that the renderer also samples, | 102 | // scan out directly (its primary plane's, that the renderer also samples, | ||
| 99 | // so a refused buffer can still be composed). Device is the KMS device | 103 | // so a refused buffer can still be composed). Device is the KMS device | ||