refactor(wayland): hand content batches to outputs without relay goroutines
- commit
- 50ff603c2a0d92c4f24ad30f2ee6e5ee050f34e8
- author
- brice · · 2026-10-13 16:45 UTC
- committer
- brice · · 2026-10-14 04:34 UTC
- parent
- 062310a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
59 files · +1138 -411
M .mockery.yml +6 -0
| @@ -15,6 +15,7 @@ packages: | |||
| 15 | 15 | interfaces: | |
| 16 | 16 | CaptureFrame: | |
| 17 | 17 | Clock: | |
| 18 | + | ContentReturn: | |
| 18 | 19 | Notifier: | |
| 19 | 20 | OutputScaleStore: | |
| 20 | 21 | PresentationSink: | |
| @@ -97,6 +98,11 @@ packages: | |||
| 97 | 98 | dir: internal/app | |
| 98 | 99 | pkgname: app | |
| 99 | 100 | filename: leaseinventory_mock_test.go | |
| 101 | + | contentAttacher: | |
| 102 | + | config: | |
| 103 | + | dir: internal/app | |
| 104 | + | pkgname: app | |
| 105 | + | filename: contentattacher_mock_test.go | |
| 100 | 106 | github.com/bnema/neferwl/internal/adapters/statefile: | |
| 101 | 107 | interfaces: | |
| 102 | 108 | procTree: | |
| @@ -15,6 +15,7 @@ packages: | |||||
| 15 | interfaces: | 15 | interfaces: | ||
| 16 | CaptureFrame: | 16 | CaptureFrame: | ||
| 17 | Clock: | 17 | Clock: | ||
| 18 | + | ContentReturn: | |||
| 18 | Notifier: | 19 | Notifier: | ||
| 19 | OutputScaleStore: | 20 | OutputScaleStore: | ||
| 20 | PresentationSink: | 21 | PresentationSink: | ||
| @@ -97,6 +98,11 @@ packages: | |||||
| 97 | dir: internal/app | 98 | dir: internal/app | ||
| 98 | pkgname: app | 99 | pkgname: app | ||
| 99 | filename: leaseinventory_mock_test.go | 100 | filename: leaseinventory_mock_test.go | ||
| 101 | + | contentAttacher: | |||
| 102 | + | config: | |||
| 103 | + | dir: internal/app | |||
| 104 | + | pkgname: app | |||
| 105 | + | filename: contentattacher_mock_test.go | |||
| 100 | github.com/bnema/neferwl/internal/adapters/statefile: | 106 | github.com/bnema/neferwl/internal/adapters/statefile: | ||
| 101 | interfaces: | 107 | interfaces: | ||
| 102 | procTree: | 108 | procTree: | ||
M Makefile +1 -1
| @@ -69,7 +69,7 @@ adapter-imports: | |||
| 69 | 69 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | |
| 70 | 70 | # Fast, mandatory guards for tiled content publications and SHM copies. | |
| 71 | 71 | perf-check: | |
| 72 | - | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations)$$' -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|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations)$$' -count=1 | |
| 73 | 73 | check: vet test arch fakes-check log-check perf-check | |
| 74 | 74 | ||
| 75 | 75 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | |
| @@ -69,7 +69,7 @@ adapter-imports: | |||||
| 69 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | 69 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | ||
| 70 | # Fast, mandatory guards for tiled content publications and SHM copies. | 70 | # Fast, mandatory guards for tiled content publications and SHM copies. | ||
| 71 | perf-check: | 71 | perf-check: | ||
| 72 | - | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations)$$' -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|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations)$$' -count=1 |
| 73 | check: vet test arch fakes-check log-check perf-check | 73 | check: vet test arch fakes-check log-check perf-check | ||
| 74 | 74 | ||||
| 75 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | 75 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | ||
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, discardSink(t), nil, nil) | |
| 434 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, 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, discardSink(t), nil, nil) | 434 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, 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 +6 -6
| @@ -363,14 +363,14 @@ func TestRunChildHoldLiftedWhileDisplayCommitStalls(t *testing.T) { | |||
| 363 | 363 | o.NewCaptureRenderer = func(int, int) (ports.Renderer, error) { return child, nil } | |
| 364 | 364 | ||
| 365 | 365 | scenes := make(chan ports.Scene) | |
| 366 | - | contents := make(chan ports.SurfaceContent) // unbuffered: taken before the scene | |
| 366 | + | contents := newContentFeed(t, 0) // 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 | 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, sink, captures, captured) | |
| 373 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return display, nil }, nil, make(chan bool), scenes, contents.in(), contents.ret, nil, sink, captures, captured) | |
| 374 | 374 | }() | |
| 375 | 375 | count := func() int { | |
| 376 | 376 | commitMu.Lock() | |
| @@ -379,7 +379,7 @@ func TestRunChildHoldLiftedWhileDisplayCommitStalls(t *testing.T) { | |||
| 379 | 379 | } | |
| 380 | 380 | waitFor(t, func() bool { return count() == 2 }) | |
| 381 | 381 | ||
| 382 | - | contents <- ports.SurfaceContent{ID: 5, Seq: 2, Width: 1, Height: 1, SHM: &ports.SHMBuffer{}} | |
| 382 | + | contents.send(ports.SurfaceContent{ID: 5, Seq: 2, Width: 1, Height: 1, SHM: &ports.SHMBuffer{}}) | |
| 383 | 383 | // The request waits for the scene; once a display frame is pending no | |
| 384 | 384 | // request is served, so it must be queued before the scene arrives. | |
| 385 | 385 | req := sessionRequest(t, 1, false) | |
| @@ -411,7 +411,7 @@ func TestRunChildHoldLiftedWhileDisplayCommitStalls(t *testing.T) { | |||
| 411 | 411 | ||
| 412 | 412 | // A newer buffer of a display window arrives while the display commit is | |
| 413 | 413 | // pending: the GPU may still read the older one, so it is not reported. | |
| 414 | - | contents <- ports.SurfaceContent{ID: 1, Seq: 9, Width: 1, Height: 1, SHM: &ports.SHMBuffer{}} | |
| 414 | + | contents.send(ports.SurfaceContent{ID: 1, Seq: 9, Width: 1, Height: 1, SHM: &ports.SHMBuffer{}}) | |
| 415 | 415 | _, err = writeEnd.Write([]byte{1}) // the child's GPU work finished | |
| 416 | 416 | require.NoError(t, err) | |
| 417 | 417 | lifted := next(func(r ports.OutputPresented) bool { return len(r.ChildReads) == 0 }) | |
| @@ -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, discardSink(t), captures, captured) | |
| 490 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, 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, discardSink(t), captures, captured) | |
| 566 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, nil, discardSink(t), captures, captured) | |
| 567 | 567 | }() | |
| 568 | 568 | count := func() int { | |
| 569 | 569 | commitMu.Lock() | |
| @@ -363,14 +363,14 @@ func TestRunChildHoldLiftedWhileDisplayCommitStalls(t *testing.T) { | |||||
| 363 | o.NewCaptureRenderer = func(int, int) (ports.Renderer, error) { return child, nil } | 363 | o.NewCaptureRenderer = func(int, int) (ports.Renderer, error) { return child, nil } | ||
| 364 | 364 | ||||
| 365 | scenes := make(chan ports.Scene) | 365 | scenes := make(chan ports.Scene) | ||
| 366 | - | contents := make(chan ports.SurfaceContent) // unbuffered: taken before the scene | 366 | + | contents := newContentFeed(t, 0) // 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 | sink, presented := presentedSink(t, 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, sink, captures, captured) | 373 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return display, nil }, nil, make(chan bool), scenes, contents.in(), contents.ret, nil, sink, captures, captured) |
| 374 | }() | 374 | }() | ||
| 375 | count := func() int { | 375 | count := func() int { | ||
| 376 | commitMu.Lock() | 376 | commitMu.Lock() | ||
| @@ -379,7 +379,7 @@ func TestRunChildHoldLiftedWhileDisplayCommitStalls(t *testing.T) { | |||||
| 379 | } | 379 | } | ||
| 380 | waitFor(t, func() bool { return count() == 2 }) | 380 | waitFor(t, func() bool { return count() == 2 }) | ||
| 381 | 381 | ||||
| 382 | - | contents <- ports.SurfaceContent{ID: 5, Seq: 2, Width: 1, Height: 1, SHM: &ports.SHMBuffer{}} | 382 | + | contents.send(ports.SurfaceContent{ID: 5, Seq: 2, Width: 1, Height: 1, SHM: &ports.SHMBuffer{}}) |
| 383 | // The request waits for the scene; once a display frame is pending no | 383 | // The request waits for the scene; once a display frame is pending no | ||
| 384 | // request is served, so it must be queued before the scene arrives. | 384 | // request is served, so it must be queued before the scene arrives. | ||
| 385 | req := sessionRequest(t, 1, false) | 385 | req := sessionRequest(t, 1, false) | ||
| @@ -411,7 +411,7 @@ func TestRunChildHoldLiftedWhileDisplayCommitStalls(t *testing.T) { | |||||
| 411 | 411 | ||||
| 412 | // A newer buffer of a display window arrives while the display commit is | 412 | // A newer buffer of a display window arrives while the display commit is | ||
| 413 | // pending: the GPU may still read the older one, so it is not reported. | 413 | // pending: the GPU may still read the older one, so it is not reported. | ||
| 414 | - | contents <- ports.SurfaceContent{ID: 1, Seq: 9, Width: 1, Height: 1, SHM: &ports.SHMBuffer{}} | 414 | + | contents.send(ports.SurfaceContent{ID: 1, Seq: 9, Width: 1, Height: 1, SHM: &ports.SHMBuffer{}}) |
| 415 | _, err = writeEnd.Write([]byte{1}) // the child's GPU work finished | 415 | _, err = writeEnd.Write([]byte{1}) // the child's GPU work finished | ||
| 416 | require.NoError(t, err) | 416 | require.NoError(t, err) | ||
| 417 | lifted := next(func(r ports.OutputPresented) bool { return len(r.ChildReads) == 0 }) | 417 | lifted := next(func(r ports.OutputPresented) bool { return len(r.ChildReads) == 0 }) | ||
| @@ -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, discardSink(t), captures, captured) | 490 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, 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, discardSink(t), captures, captured) | 566 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, 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, discardSink(t), nil, nil) | |
| 403 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, nil, discardSink(t), nil, nil) | |
| 404 | 404 | }() | |
| 405 | 405 | count := func(flags uint32) int { | |
| 406 | 406 | commitMu.Lock() | |
| @@ -516,12 +516,12 @@ func runReportsSeenAfterCursorCommit(t *testing.T) { | |||
| 516 | 516 | return commitRec{}, false | |
| 517 | 517 | } | |
| 518 | 518 | scenes := make(chan ports.Scene, 1) | |
| 519 | - | contents := make(chan ports.SurfaceContent, 1) | |
| 519 | + | contents := newContentFeed(t, 1) | |
| 520 | 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, sink, nil, nil) | |
| 524 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, load, make(chan bool), scenes, contents.in(), contents.ret, nil, sink, nil, nil) | |
| 525 | 525 | }() | |
| 526 | 526 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1} | |
| 527 | 527 | var frame commitRec | |
| @@ -532,7 +532,7 @@ func runReportsSeenAfterCursorCommit(t *testing.T) { | |||
| 532 | 532 | var cur commitRec | |
| 533 | 533 | waitFor(t, func() bool { var ok bool; cur, ok = last(userState); return ok }) | |
| 534 | 534 | // A window this output does not show commits while the cursor flies. | |
| 535 | - | contents <- ports.SurfaceContent{ID: 7, Seq: 2, SHM: &ports.SHMBuffer{}} | |
| 535 | + | contents.send(ports.SurfaceContent{ID: 7, Seq: 2, SHM: &ports.SHMBuffer{}}) | |
| 536 | 536 | synctest.Wait() | |
| 537 | 537 | flips <- eventOf(cur) | |
| 538 | 538 | select { | |
| @@ -605,7 +605,7 @@ func TestRunClosesFrameFences(t *testing.T) { | |||
| 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, sink, nil, nil) | |
| 608 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, 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, discardSink(t), nil, nil) | 403 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, 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() | ||
| @@ -516,12 +516,12 @@ func runReportsSeenAfterCursorCommit(t *testing.T) { | |||||
| 516 | return commitRec{}, false | 516 | return commitRec{}, false | ||
| 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 := newContentFeed(t, 1) |
| 520 | sink, presented := presentedSink(t, 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, sink, nil, nil) | 524 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, load, make(chan bool), scenes, contents.in(), contents.ret, 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 | ||
| @@ -532,7 +532,7 @@ func runReportsSeenAfterCursorCommit(t *testing.T) { | |||||
| 532 | var cur commitRec | 532 | var cur commitRec | ||
| 533 | waitFor(t, func() bool { var ok bool; cur, ok = last(userState); return ok }) | 533 | waitFor(t, func() bool { var ok bool; cur, ok = last(userState); return ok }) | ||
| 534 | // A window this output does not show commits while the cursor flies. | 534 | // A window this output does not show commits while the cursor flies. | ||
| 535 | - | contents <- ports.SurfaceContent{ID: 7, Seq: 2, SHM: &ports.SHMBuffer{}} | 535 | + | contents.send(ports.SurfaceContent{ID: 7, Seq: 2, SHM: &ports.SHMBuffer{}}) |
| 536 | synctest.Wait() | 536 | synctest.Wait() | ||
| 537 | flips <- eventOf(cur) | 537 | flips <- eventOf(cur) | ||
| 538 | select { | 538 | select { | ||
| @@ -605,7 +605,7 @@ func TestRunClosesFrameFences(t *testing.T) { | |||||
| 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, sink, nil, nil) | 608 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, nil, sink, nil, nil) |
| 609 | }() | 609 | }() | ||
| 610 | count := func() int { | 610 | count := func() int { | ||
| 611 | commitMu.Lock() | 611 | commitMu.Lock() | ||
A internal/adapters/drm/content_feed_test.go +39 -0
| @@ -0,0 +1,39 @@ | |||
| 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 | + | // contentFeed is the wayland side of Run's content input in tests: batches go | |
| 12 | + | // out on ch, and the generated ContentReturn mock checks that each comes back | |
| 13 | + | // reset. | |
| 14 | + | type contentFeed struct { | |
| 15 | + | ch chan *ports.ContentBatch | |
| 16 | + | ret *portsmocks.MockContentReturn | |
| 17 | + | returned chan struct{} | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | // newContentFeed makes a feed whose channel holds buffer batches. | |
| 21 | + | func newContentFeed(t *testing.T, buffer int) *contentFeed { | |
| 22 | + | t.Helper() | |
| 23 | + | f := &contentFeed{ch: make(chan *ports.ContentBatch, buffer), ret: portsmocks.NewMockContentReturn(t), returned: make(chan struct{}, 64)} | |
| 24 | + | f.ret.EXPECT().Return(mock.Anything).Run(func(b *ports.ContentBatch) { | |
| 25 | + | if len(b.Items) != 0 { | |
| 26 | + | t.Errorf("batch returned with %d items, want it reset", len(b.Items)) | |
| 27 | + | } | |
| 28 | + | f.returned <- struct{}{} | |
| 29 | + | }).Return().Maybe() | |
| 30 | + | return f | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | // send hands the output one batch holding cs. | |
| 34 | + | func (f *contentFeed) send(cs ...ports.SurfaceContent) { | |
| 35 | + | f.ch <- &ports.ContentBatch{Items: cs} | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // in is the channel Run reads. | |
| 39 | + | func (f *contentFeed) in() <-chan *ports.ContentBatch { return f.ch } | |
| @@ -0,0 +1,39 @@ | |||||
| 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 | + | // contentFeed is the wayland side of Run's content input in tests: batches go | |||
| 12 | + | // out on ch, and the generated ContentReturn mock checks that each comes back | |||
| 13 | + | // reset. | |||
| 14 | + | type contentFeed struct { | |||
| 15 | + | ch chan *ports.ContentBatch | |||
| 16 | + | ret *portsmocks.MockContentReturn | |||
| 17 | + | returned chan struct{} | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | // newContentFeed makes a feed whose channel holds buffer batches. | |||
| 21 | + | func newContentFeed(t *testing.T, buffer int) *contentFeed { | |||
| 22 | + | t.Helper() | |||
| 23 | + | f := &contentFeed{ch: make(chan *ports.ContentBatch, buffer), ret: portsmocks.NewMockContentReturn(t), returned: make(chan struct{}, 64)} | |||
| 24 | + | f.ret.EXPECT().Return(mock.Anything).Run(func(b *ports.ContentBatch) { | |||
| 25 | + | if len(b.Items) != 0 { | |||
| 26 | + | t.Errorf("batch returned with %d items, want it reset", len(b.Items)) | |||
| 27 | + | } | |||
| 28 | + | f.returned <- struct{}{} | |||
| 29 | + | }).Return().Maybe() | |||
| 30 | + | return f | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | // send hands the output one batch holding cs. | |||
| 34 | + | func (f *contentFeed) send(cs ...ports.SurfaceContent) { | |||
| 35 | + | f.ch <- &ports.ContentBatch{Items: cs} | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // in is the channel Run reads. | |||
| 39 | + | func (f *contentFeed) in() <-chan *ports.ContentBatch { return f.ch } | |||
M internal/adapters/drm/output.go +13 -8
| @@ -1009,7 +1009,7 @@ func (o *Output) InactiveOnClose() bool { return o.inactiveOnClose } | |||
| 1009 | 1009 | ||
| 1010 | 1010 | // Run renders scenes and commits them until ctx ends. active reports seat | |
| 1011 | 1011 | // enable/disable. What the output shows and read is pushed to reports. | |
| 1012 | - | 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) { | |
| 1012 | + | 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.ContentBatch, returns ports.ContentReturn, cursor <-chan ports.CursorChange, reports ports.PresentationSink, captures <-chan ports.CaptureRequest, captured chan<- ports.CaptureDone) (runErr error) { | |
| 1013 | 1013 | defer func() { | |
| 1014 | 1014 | p := recover() | |
| 1015 | 1015 | if p != nil { | |
| @@ -1435,14 +1435,19 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1435 | 1435 | o.setCursor(r, loadCursor, want, cursorScale, cursorTransform) | |
| 1436 | 1436 | stateDirty = true | |
| 1437 | 1437 | } | |
| 1438 | - | case c := <-contents: | |
| 1439 | - | table.Update(c, scene) | |
| 1440 | - | if c.Seq > seen[c.ID] { | |
| 1441 | - | seen[c.ID] = c.Seq | |
| 1442 | - | reportDirty = true | |
| 1438 | + | case b := <-contents: | |
| 1439 | + | for i := range b.Items { | |
| 1440 | + | c := b.Items[i] | |
| 1441 | + | table.Update(c, scene) | |
| 1442 | + | if c.Seq > seen[c.ID] { | |
| 1443 | + | seen[c.ID] = c.Seq | |
| 1444 | + | reportDirty = true | |
| 1445 | + | } | |
| 1446 | + | // Windows on other outputs or workspaces do not need a frame. | |
| 1447 | + | dirty = dirty || capture.Shows(scene, c.ID) | |
| 1443 | 1448 | } | |
| 1444 | - | // Windows on other outputs or workspaces do not need a frame. | |
| 1445 | - | dirty = dirty || capture.Shows(scene, c.ID) | |
| 1449 | + | b.Reset() | |
| 1450 | + | returns.Return(b) | |
| 1446 | 1451 | case <-stats.C(): | |
| 1447 | 1452 | now := clk.Now() | |
| 1448 | 1453 | pipeline.Retire(scene) // an idle child of an ended session goes | |
| @@ -1009,7 +1009,7 @@ func (o *Output) InactiveOnClose() bool { return o.inactiveOnClose } | |||||
| 1009 | 1009 | ||||
| 1010 | // Run renders scenes and commits them until ctx ends. active reports seat | 1010 | // Run renders scenes and commits them until ctx ends. active reports seat | ||
| 1011 | // enable/disable. What the output shows and read is pushed to reports. | 1011 | // enable/disable. What the output shows and read is pushed to reports. | ||
| 1012 | - | 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) { | 1012 | + | 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.ContentBatch, returns ports.ContentReturn, cursor <-chan ports.CursorChange, reports ports.PresentationSink, captures <-chan ports.CaptureRequest, captured chan<- ports.CaptureDone) (runErr error) { |
| 1013 | defer func() { | 1013 | defer func() { | ||
| 1014 | p := recover() | 1014 | p := recover() | ||
| 1015 | if p != nil { | 1015 | if p != nil { | ||
| @@ -1435,14 +1435,19 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1435 | o.setCursor(r, loadCursor, want, cursorScale, cursorTransform) | 1435 | o.setCursor(r, loadCursor, want, cursorScale, cursorTransform) | ||
| 1436 | stateDirty = true | 1436 | stateDirty = true | ||
| 1437 | } | 1437 | } | ||
| 1438 | - | case c := <-contents: | 1438 | + | case b := <-contents: |
| 1439 | - | table.Update(c, scene) | 1439 | + | for i := range b.Items { |
| 1440 | - | if c.Seq > seen[c.ID] { | 1440 | + | c := b.Items[i] |
| 1441 | - | seen[c.ID] = c.Seq | 1441 | + | table.Update(c, scene) |
| 1442 | - | reportDirty = true | 1442 | + | if c.Seq > seen[c.ID] { |
| 1443 | + | seen[c.ID] = c.Seq | |||
| 1444 | + | reportDirty = true | |||
| 1445 | + | } | |||
| 1446 | + | // Windows on other outputs or workspaces do not need a frame. | |||
| 1447 | + | dirty = dirty || capture.Shows(scene, c.ID) | |||
| 1443 | } | 1448 | } | ||
| 1444 | - | // Windows on other outputs or workspaces do not need a frame. | 1449 | + | b.Reset() |
| 1445 | - | dirty = dirty || capture.Shows(scene, c.ID) | 1450 | + | returns.Return(b) |
| 1446 | case <-stats.C(): | 1451 | case <-stats.C(): | ||
| 1447 | now := clk.Now() | 1452 | now := clk.Now() | ||
| 1448 | pipeline.Retire(scene) // an idle child of an ended session goes | 1453 | pipeline.Retire(scene) // an idle child of an ended session goes | ||
M internal/adapters/drm/output_test.go +28 -21
| @@ -597,19 +597,26 @@ func runReportsSeenAfterFlip(t *testing.T) { | |||
| 597 | 597 | return n | |
| 598 | 598 | } | |
| 599 | 599 | scenes := make(chan ports.Scene, 1) | |
| 600 | - | contents := make(chan ports.SurfaceContent, 1) | |
| 600 | + | contents := newContentFeed(t, 1) | |
| 601 | 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, sink, nil, nil) | |
| 605 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents.in(), contents.ret, 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 }) | |
| 609 | 609 | // Frame 1 is in flight: content 3 replaces what it may be reading. | |
| 610 | - | contents <- ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{}} | |
| 610 | + | // One batch carries two windows: both are applied and the batch goes | |
| 611 | + | // back to wayland (reset) at once, whatever the frame in flight reads. | |
| 612 | + | contents.send(ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{}}, ports.SurfaceContent{ID: 2, Seq: 4, SHM: &ports.SHMBuffer{}}) | |
| 611 | 613 | synctest.Wait() | |
| 612 | 614 | select { | |
| 615 | + | case <-contents.returned: | |
| 616 | + | default: | |
| 617 | + | t.Fatal("batch not returned") | |
| 618 | + | } | |
| 619 | + | select { | |
| 613 | 620 | case p := <-presented: | |
| 614 | 621 | t.Fatalf("report while a frame is in flight: %+v", p) | |
| 615 | 622 | default: | |
| @@ -620,7 +627,7 @@ func runReportsSeenAfterFlip(t *testing.T) { | |||
| 620 | 627 | flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1} | |
| 621 | 628 | select { | |
| 622 | 629 | case p := <-presented: | |
| 623 | - | if p.Flip == nil || p.Flip.When != time.Second || p.Seen[1] != 3 { | |
| 630 | + | if p.Flip == nil || p.Flip.When != time.Second || p.Seen[1] != 3 || p.Seen[2] != 4 { | |
| 624 | 631 | t.Fatalf("report %+v", p) | |
| 625 | 632 | } | |
| 626 | 633 | case <-time.After(2 * time.Second): | |
| @@ -679,7 +686,7 @@ func TestRunComposedFullscreenKeepsVRR(t *testing.T) { | |||
| 679 | 686 | ctx, cancel := context.WithCancel(context.Background()) | |
| 680 | 687 | done := make(chan error, 1) | |
| 681 | 688 | go func() { | |
| 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) | |
| 689 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, nil, discardSink(t), nil, nil) | |
| 683 | 690 | }() | |
| 684 | 691 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, | |
| 685 | 692 | Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}}, | |
| @@ -761,7 +768,7 @@ func TestRunTrimsRendererOnStatsTick(t *testing.T) { | |||
| 761 | 768 | active <- false | |
| 762 | 769 | done := make(chan error, 1) | |
| 763 | 770 | go func() { | |
| 764 | - | done <- o.Run(context.Background(), func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, discardSink(t), nil, nil) | |
| 771 | + | done <- o.Run(context.Background(), func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, nil, discardSink(t), nil, nil) | |
| 765 | 772 | }() | |
| 766 | 773 | ticks <- now | |
| 767 | 774 | <-trimmed | |
| @@ -815,7 +822,7 @@ func TestCaptureRenderFailureClosesPendingRequest(t *testing.T) { | |||
| 815 | 822 | } | |
| 816 | 823 | done := make(chan error, 1) | |
| 817 | 824 | go func() { | |
| 818 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, nil, requests, replies) | |
| 825 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, nil, nil, requests, replies) | |
| 819 | 826 | }() | |
| 820 | 827 | // The unbuffered handoff proves Run owns the request before any scene. | |
| 821 | 828 | select { | |
| @@ -881,7 +888,7 @@ func TestCaptureForcesDRMComposition(t *testing.T) { | |||
| 881 | 888 | ctx, cancel := context.WithCancel(context.Background()) | |
| 882 | 889 | done := make(chan error, 1) | |
| 883 | 890 | go func() { | |
| 884 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, discardSink(t), requests, replies) | |
| 891 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, nil, discardSink(t), requests, replies) | |
| 885 | 892 | }() | |
| 886 | 893 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Background: "#000000"} | |
| 887 | 894 | select { | |
| @@ -941,7 +948,7 @@ func TestRunPanicReportsReadinessError(t *testing.T) { | |||
| 941 | 948 | t.Fatal("missing readiness") | |
| 942 | 949 | } | |
| 943 | 950 | }() | |
| 944 | - | o.Run(context.Background(), func(int, int) (ports.Renderer, error) { panic("renderer panic") }, nil, nil, nil, nil, nil, nil, nil, nil) | |
| 951 | + | o.Run(context.Background(), func(int, int) (ports.Renderer, error) { panic("renderer panic") }, nil, nil, nil, nil, nil, nil, nil, nil, nil) | |
| 945 | 952 | } | |
| 946 | 953 | ||
| 947 | 954 | // stampRun starts Run on an output with unbuffered scene and content | |
| @@ -951,7 +958,7 @@ func TestRunPanicReportsReadinessError(t *testing.T) { | |||
| 951 | 958 | type stampRun struct { | |
| 952 | 959 | o *Output | |
| 953 | 960 | scenes chan ports.Scene | |
| 954 | - | contents chan ports.SurfaceContent | |
| 961 | + | contents *contentFeed | |
| 955 | 962 | flips chan flipEvent | |
| 956 | 963 | wake chan ports.SecurityState | |
| 957 | 964 | state *atomic.Uint64 | |
| @@ -963,7 +970,7 @@ type stampRun struct { | |||
| 963 | 970 | func startStampRun(t *testing.T) *stampRun { | |
| 964 | 971 | o, k, commits, commitMu := testOutputMu(t) | |
| 965 | 972 | o.cursor, o.tearing = nil, false | |
| 966 | - | sr := &stampRun{o: o, scenes: make(chan ports.Scene), contents: make(chan ports.SurfaceContent), flips: make(chan flipEvent, 1), wake: make(chan ports.SecurityState, 1), state: &atomic.Uint64{}} | |
| 973 | + | sr := &stampRun{o: o, scenes: make(chan ports.Scene), contents: newContentFeed(t, 0), flips: make(chan flipEvent, 1), wake: make(chan ports.SecurityState, 1), state: &atomic.Uint64{}} | |
| 967 | 974 | o.flipped = sr.flips | |
| 968 | 975 | securityGate(t, o, sr.state) | |
| 969 | 976 | o.SecurityChanges = sr.wake | |
| @@ -1001,7 +1008,7 @@ func startStampRun(t *testing.T) *stampRun { | |||
| 1001 | 1008 | ctx, cancel := context.WithCancel(context.Background()) | |
| 1002 | 1009 | done := make(chan error, 1) | |
| 1003 | 1010 | 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, discardSink(t), nil, nil) | |
| 1011 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), sr.scenes, sr.contents.in(), sr.contents.ret, nil, discardSink(t), nil, nil) | |
| 1005 | 1012 | }() | |
| 1006 | 1013 | // stop ends Run: its state is ours to read afterwards. | |
| 1007 | 1014 | sr.stop = func() { | |
| @@ -1025,7 +1032,7 @@ func TestRunStampsWhenFrameWasWanted(t *testing.T) { | |||
| 1025 | 1032 | scene.Seq = 2 | |
| 1026 | 1033 | before := monotonic() | |
| 1027 | 1034 | sr.scenes <- scene | |
| 1028 | - | sr.contents <- ports.SurfaceContent{ID: 1, Seq: 1, SHM: &ports.SHMBuffer{}} // Run went round its loop | |
| 1035 | + | sr.contents.send(ports.SurfaceContent{ID: 1, Seq: 1, SHM: &ports.SHMBuffer{}}) // Run went round its loop | |
| 1029 | 1036 | flipSent := monotonic() | |
| 1030 | 1037 | sr.flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1} | |
| 1031 | 1038 | waitFor(t, func() bool { return sr.frameCommit() == 2 }) | |
| @@ -1048,11 +1055,11 @@ func TestRunSecurityResetClearsWantedAt(t *testing.T) { | |||
| 1048 | 1055 | waitFor(t, func() bool { return sr.frameCommit() == 1 }) | |
| 1049 | 1056 | scene.Seq = 2 | |
| 1050 | 1057 | sr.scenes <- scene | |
| 1051 | - | sr.contents <- ports.SurfaceContent{ID: 1, Seq: 1, SHM: &ports.SHMBuffer{}} // stamped by now | |
| 1058 | + | sr.contents.send(ports.SurfaceContent{ID: 1, Seq: 1, SHM: &ports.SHMBuffer{}}) // stamped by now | |
| 1052 | 1059 | // A new security generation, not protected. | |
| 1053 | 1060 | sr.state.Store(2) | |
| 1054 | 1061 | sr.wake <- ports.SecurityState{} | |
| 1055 | - | sr.contents <- ports.SurfaceContent{ID: 1, Seq: 2, SHM: &ports.SHMBuffer{}} // Run went round and reset | |
| 1062 | + | sr.contents.send(ports.SurfaceContent{ID: 1, Seq: 2, SHM: &ports.SHMBuffer{}}) // Run went round and reset | |
| 1056 | 1063 | sr.stop() | |
| 1057 | 1064 | if sr.o.wantedAt != 0 { | |
| 1058 | 1065 | t.Fatalf("stamp kept across the security reset: %s", sr.o.wantedAt) | |
| @@ -1070,8 +1077,8 @@ func TestOutputSameSeqRendersOnce(t *testing.T) { | |||
| 1070 | 1077 | sr.scenes <- scene | |
| 1071 | 1078 | // Unshown content only sends Run round its loop: the flip and the held | |
| 1072 | 1079 | // scene are handled by the time the second one is taken. | |
| 1073 | - | sr.contents <- ports.SurfaceContent{ID: 99, Seq: 1, SHM: &ports.SHMBuffer{}} | |
| 1074 | - | sr.contents <- ports.SurfaceContent{ID: 99, Seq: 2, SHM: &ports.SHMBuffer{}} | |
| 1080 | + | sr.contents.send(ports.SurfaceContent{ID: 99, Seq: 1, SHM: &ports.SHMBuffer{}}) | |
| 1081 | + | sr.contents.send(ports.SurfaceContent{ID: 99, Seq: 2, SHM: &ports.SHMBuffer{}}) | |
| 1075 | 1082 | if n := sr.frameCommit(); n != 1 { | |
| 1076 | 1083 | t.Fatalf("%d frames after a scene with the held Seq", n) | |
| 1077 | 1084 | } | |
| @@ -1129,11 +1136,11 @@ func TestRunKeepsLastContentOfLeavingWindow(t *testing.T) { | |||
| 1129 | 1136 | return c, ok | |
| 1130 | 1137 | } | |
| 1131 | 1138 | scenes := make(chan ports.Scene, 1) | |
| 1132 | - | contents := make(chan ports.SurfaceContent, 1) | |
| 1139 | + | contents := newContentFeed(t, 1) | |
| 1133 | 1140 | ctx, cancel := context.WithCancel(context.Background()) | |
| 1134 | 1141 | done := make(chan error, 1) | |
| 1135 | 1142 | go func() { | |
| 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) | |
| 1143 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents.in(), contents.ret, nil, discardSink(t), nil, nil) | |
| 1137 | 1144 | }() | |
| 1138 | 1145 | acq, acqW, _ := os.Pipe() | |
| 1139 | 1146 | defer acq.Close() | |
| @@ -1141,11 +1148,11 @@ func TestRunKeepsLastContentOfLeavingWindow(t *testing.T) { | |||
| 1141 | 1148 | win := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 10, H: 10}} | |
| 1142 | 1149 | scenes <- ports.Scene{Seq: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{win}} | |
| 1143 | 1150 | full := ports.SurfaceContent{ID: 1, Seq: 1, Width: 10, Height: 10, DMABuf: &ports.DMABuf{ID: 5, Planes: []ports.DMABufPlane{{}}}, Acquire: acq} | |
| 1144 | - | contents <- full | |
| 1151 | + | contents.send(full) | |
| 1145 | 1152 | drawn(1, func(c ports.SurfaceContent, ok bool) bool { return ok && c.Acquire == acq }) | |
| 1146 | 1153 | // The window unmaps: its empty content arrives before the scene that | |
| 1147 | 1154 | // draws it leaving (fading). The last content stays, fence dropped. | |
| 1148 | - | contents <- ports.SurfaceContent{ID: 1, Seq: 2} | |
| 1155 | + | contents.send(ports.SurfaceContent{ID: 1, Seq: 2}) | |
| 1149 | 1156 | kept, _ := drawn(1, func(c ports.SurfaceContent, ok bool) bool { return !ok || c.Acquire == nil }) | |
| 1150 | 1157 | if kept.DMABuf != full.DMABuf || kept.Seq != 1 { | |
| 1151 | 1158 | t.Fatalf("frame after the empty content drew %+v, want the last content", kept) | |
| @@ -597,19 +597,26 @@ func runReportsSeenAfterFlip(t *testing.T) { | |||||
| 597 | return n | 597 | return n | ||
| 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 := newContentFeed(t, 1) |
| 601 | sink, presented := presentedSink(t, 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, sink, nil, nil) | 605 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents.in(), contents.ret, 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 }) | ||
| 609 | // Frame 1 is in flight: content 3 replaces what it may be reading. | 609 | // Frame 1 is in flight: content 3 replaces what it may be reading. | ||
| 610 | - | contents <- ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{}} | 610 | + | // One batch carries two windows: both are applied and the batch goes |
| 611 | + | // back to wayland (reset) at once, whatever the frame in flight reads. | |||
| 612 | + | contents.send(ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{}}, ports.SurfaceContent{ID: 2, Seq: 4, SHM: &ports.SHMBuffer{}}) | |||
| 611 | synctest.Wait() | 613 | synctest.Wait() | ||
| 612 | select { | 614 | select { | ||
| 615 | + | case <-contents.returned: | |||
| 616 | + | default: | |||
| 617 | + | t.Fatal("batch not returned") | |||
| 618 | + | } | |||
| 619 | + | select { | |||
| 613 | case p := <-presented: | 620 | case p := <-presented: | ||
| 614 | t.Fatalf("report while a frame is in flight: %+v", p) | 621 | t.Fatalf("report while a frame is in flight: %+v", p) | ||
| 615 | default: | 622 | default: | ||
| @@ -620,7 +627,7 @@ func runReportsSeenAfterFlip(t *testing.T) { | |||||
| 620 | flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1} | 627 | flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1} | ||
| 621 | select { | 628 | select { | ||
| 622 | case p := <-presented: | 629 | case p := <-presented: | ||
| 623 | - | if p.Flip == nil || p.Flip.When != time.Second || p.Seen[1] != 3 { | 630 | + | if p.Flip == nil || p.Flip.When != time.Second || p.Seen[1] != 3 || p.Seen[2] != 4 { |
| 624 | t.Fatalf("report %+v", p) | 631 | t.Fatalf("report %+v", p) | ||
| 625 | } | 632 | } | ||
| 626 | case <-time.After(2 * time.Second): | 633 | case <-time.After(2 * time.Second): | ||
| @@ -679,7 +686,7 @@ func TestRunComposedFullscreenKeepsVRR(t *testing.T) { | |||||
| 679 | ctx, cancel := context.WithCancel(context.Background()) | 686 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 680 | done := make(chan error, 1) | 687 | done := make(chan error, 1) | ||
| 681 | go func() { | 688 | go func() { | ||
| 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) | 689 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, nil, discardSink(t), nil, nil) |
| 683 | }() | 690 | }() | ||
| 684 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, | 691 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, | ||
| 685 | Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}}, | 692 | Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}}, | ||
| @@ -761,7 +768,7 @@ func TestRunTrimsRendererOnStatsTick(t *testing.T) { | |||||
| 761 | active <- false | 768 | active <- false | ||
| 762 | done := make(chan error, 1) | 769 | done := make(chan error, 1) | ||
| 763 | go func() { | 770 | go func() { | ||
| 764 | - | done <- o.Run(context.Background(), func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, discardSink(t), nil, nil) | 771 | + | done <- o.Run(context.Background(), func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, nil, discardSink(t), nil, nil) |
| 765 | }() | 772 | }() | ||
| 766 | ticks <- now | 773 | ticks <- now | ||
| 767 | <-trimmed | 774 | <-trimmed | ||
| @@ -815,7 +822,7 @@ func TestCaptureRenderFailureClosesPendingRequest(t *testing.T) { | |||||
| 815 | } | 822 | } | ||
| 816 | done := make(chan error, 1) | 823 | done := make(chan error, 1) | ||
| 817 | go func() { | 824 | go func() { | ||
| 818 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, nil, requests, replies) | 825 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, nil, nil, requests, replies) |
| 819 | }() | 826 | }() | ||
| 820 | // The unbuffered handoff proves Run owns the request before any scene. | 827 | // The unbuffered handoff proves Run owns the request before any scene. | ||
| 821 | select { | 828 | select { | ||
| @@ -881,7 +888,7 @@ func TestCaptureForcesDRMComposition(t *testing.T) { | |||||
| 881 | ctx, cancel := context.WithCancel(context.Background()) | 888 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 882 | done := make(chan error, 1) | 889 | done := make(chan error, 1) | ||
| 883 | go func() { | 890 | go func() { | ||
| 884 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, discardSink(t), requests, replies) | 891 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, nil, discardSink(t), requests, replies) |
| 885 | }() | 892 | }() | ||
| 886 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Background: "#000000"} | 893 | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Background: "#000000"} | ||
| 887 | select { | 894 | select { | ||
| @@ -941,7 +948,7 @@ func TestRunPanicReportsReadinessError(t *testing.T) { | |||||
| 941 | t.Fatal("missing readiness") | 948 | t.Fatal("missing readiness") | ||
| 942 | } | 949 | } | ||
| 943 | }() | 950 | }() | ||
| 944 | - | o.Run(context.Background(), func(int, int) (ports.Renderer, error) { panic("renderer panic") }, nil, nil, nil, nil, nil, nil, nil, nil) | 951 | + | o.Run(context.Background(), func(int, int) (ports.Renderer, error) { panic("renderer panic") }, nil, nil, nil, nil, nil, nil, nil, nil, nil) |
| 945 | } | 952 | } | ||
| 946 | 953 | ||||
| 947 | // stampRun starts Run on an output with unbuffered scene and content | 954 | // stampRun starts Run on an output with unbuffered scene and content | ||
| @@ -951,7 +958,7 @@ func TestRunPanicReportsReadinessError(t *testing.T) { | |||||
| 951 | type stampRun struct { | 958 | type stampRun struct { | ||
| 952 | o *Output | 959 | o *Output | ||
| 953 | scenes chan ports.Scene | 960 | scenes chan ports.Scene | ||
| 954 | - | contents chan ports.SurfaceContent | 961 | + | contents *contentFeed |
| 955 | flips chan flipEvent | 962 | flips chan flipEvent | ||
| 956 | wake chan ports.SecurityState | 963 | wake chan ports.SecurityState | ||
| 957 | state *atomic.Uint64 | 964 | state *atomic.Uint64 | ||
| @@ -963,7 +970,7 @@ type stampRun struct { | |||||
| 963 | func startStampRun(t *testing.T) *stampRun { | 970 | func startStampRun(t *testing.T) *stampRun { | ||
| 964 | o, k, commits, commitMu := testOutputMu(t) | 971 | o, k, commits, commitMu := testOutputMu(t) | ||
| 965 | o.cursor, o.tearing = nil, false | 972 | o.cursor, o.tearing = nil, false | ||
| 966 | - | sr := &stampRun{o: o, scenes: make(chan ports.Scene), contents: make(chan ports.SurfaceContent), flips: make(chan flipEvent, 1), wake: make(chan ports.SecurityState, 1), state: &atomic.Uint64{}} | 973 | + | sr := &stampRun{o: o, scenes: make(chan ports.Scene), contents: newContentFeed(t, 0), flips: make(chan flipEvent, 1), wake: make(chan ports.SecurityState, 1), state: &atomic.Uint64{}} |
| 967 | o.flipped = sr.flips | 974 | o.flipped = sr.flips | ||
| 968 | securityGate(t, o, sr.state) | 975 | securityGate(t, o, sr.state) | ||
| 969 | o.SecurityChanges = sr.wake | 976 | o.SecurityChanges = sr.wake | ||
| @@ -1001,7 +1008,7 @@ func startStampRun(t *testing.T) *stampRun { | |||||
| 1001 | ctx, cancel := context.WithCancel(context.Background()) | 1008 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 1002 | done := make(chan error, 1) | 1009 | done := make(chan error, 1) | ||
| 1003 | go func() { | 1010 | 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, discardSink(t), nil, nil) | 1011 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), sr.scenes, sr.contents.in(), sr.contents.ret, nil, discardSink(t), nil, nil) |
| 1005 | }() | 1012 | }() | ||
| 1006 | // stop ends Run: its state is ours to read afterwards. | 1013 | // stop ends Run: its state is ours to read afterwards. | ||
| 1007 | sr.stop = func() { | 1014 | sr.stop = func() { | ||
| @@ -1025,7 +1032,7 @@ func TestRunStampsWhenFrameWasWanted(t *testing.T) { | |||||
| 1025 | scene.Seq = 2 | 1032 | scene.Seq = 2 | ||
| 1026 | before := monotonic() | 1033 | before := monotonic() | ||
| 1027 | sr.scenes <- scene | 1034 | sr.scenes <- scene | ||
| 1028 | - | sr.contents <- ports.SurfaceContent{ID: 1, Seq: 1, SHM: &ports.SHMBuffer{}} // Run went round its loop | 1035 | + | sr.contents.send(ports.SurfaceContent{ID: 1, Seq: 1, SHM: &ports.SHMBuffer{}}) // Run went round its loop |
| 1029 | flipSent := monotonic() | 1036 | flipSent := monotonic() | ||
| 1030 | sr.flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1} | 1037 | sr.flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1} | ||
| 1031 | waitFor(t, func() bool { return sr.frameCommit() == 2 }) | 1038 | waitFor(t, func() bool { return sr.frameCommit() == 2 }) | ||
| @@ -1048,11 +1055,11 @@ func TestRunSecurityResetClearsWantedAt(t *testing.T) { | |||||
| 1048 | waitFor(t, func() bool { return sr.frameCommit() == 1 }) | 1055 | waitFor(t, func() bool { return sr.frameCommit() == 1 }) | ||
| 1049 | scene.Seq = 2 | 1056 | scene.Seq = 2 | ||
| 1050 | sr.scenes <- scene | 1057 | sr.scenes <- scene | ||
| 1051 | - | sr.contents <- ports.SurfaceContent{ID: 1, Seq: 1, SHM: &ports.SHMBuffer{}} // stamped by now | 1058 | + | sr.contents.send(ports.SurfaceContent{ID: 1, Seq: 1, SHM: &ports.SHMBuffer{}}) // stamped by now |
| 1052 | // A new security generation, not protected. | 1059 | // A new security generation, not protected. | ||
| 1053 | sr.state.Store(2) | 1060 | sr.state.Store(2) | ||
| 1054 | sr.wake <- ports.SecurityState{} | 1061 | sr.wake <- ports.SecurityState{} | ||
| 1055 | - | sr.contents <- ports.SurfaceContent{ID: 1, Seq: 2, SHM: &ports.SHMBuffer{}} // Run went round and reset | 1062 | + | sr.contents.send(ports.SurfaceContent{ID: 1, Seq: 2, SHM: &ports.SHMBuffer{}}) // Run went round and reset |
| 1056 | sr.stop() | 1063 | sr.stop() | ||
| 1057 | if sr.o.wantedAt != 0 { | 1064 | if sr.o.wantedAt != 0 { | ||
| 1058 | t.Fatalf("stamp kept across the security reset: %s", sr.o.wantedAt) | 1065 | t.Fatalf("stamp kept across the security reset: %s", sr.o.wantedAt) | ||
| @@ -1070,8 +1077,8 @@ func TestOutputSameSeqRendersOnce(t *testing.T) { | |||||
| 1070 | sr.scenes <- scene | 1077 | sr.scenes <- scene | ||
| 1071 | // Unshown content only sends Run round its loop: the flip and the held | 1078 | // Unshown content only sends Run round its loop: the flip and the held | ||
| 1072 | // scene are handled by the time the second one is taken. | 1079 | // scene are handled by the time the second one is taken. | ||
| 1073 | - | sr.contents <- ports.SurfaceContent{ID: 99, Seq: 1, SHM: &ports.SHMBuffer{}} | 1080 | + | sr.contents.send(ports.SurfaceContent{ID: 99, Seq: 1, SHM: &ports.SHMBuffer{}}) |
| 1074 | - | sr.contents <- ports.SurfaceContent{ID: 99, Seq: 2, SHM: &ports.SHMBuffer{}} | 1081 | + | sr.contents.send(ports.SurfaceContent{ID: 99, Seq: 2, SHM: &ports.SHMBuffer{}}) |
| 1075 | if n := sr.frameCommit(); n != 1 { | 1082 | if n := sr.frameCommit(); n != 1 { | ||
| 1076 | t.Fatalf("%d frames after a scene with the held Seq", n) | 1083 | t.Fatalf("%d frames after a scene with the held Seq", n) | ||
| 1077 | } | 1084 | } | ||
| @@ -1129,11 +1136,11 @@ func TestRunKeepsLastContentOfLeavingWindow(t *testing.T) { | |||||
| 1129 | return c, ok | 1136 | return c, ok | ||
| 1130 | } | 1137 | } | ||
| 1131 | scenes := make(chan ports.Scene, 1) | 1138 | scenes := make(chan ports.Scene, 1) | ||
| 1132 | - | contents := make(chan ports.SurfaceContent, 1) | 1139 | + | contents := newContentFeed(t, 1) |
| 1133 | ctx, cancel := context.WithCancel(context.Background()) | 1140 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 1134 | done := make(chan error, 1) | 1141 | done := make(chan error, 1) | ||
| 1135 | go func() { | 1142 | go func() { | ||
| 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) | 1143 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents.in(), contents.ret, nil, discardSink(t), nil, nil) |
| 1137 | }() | 1144 | }() | ||
| 1138 | acq, acqW, _ := os.Pipe() | 1145 | acq, acqW, _ := os.Pipe() | ||
| 1139 | defer acq.Close() | 1146 | defer acq.Close() | ||
| @@ -1141,11 +1148,11 @@ func TestRunKeepsLastContentOfLeavingWindow(t *testing.T) { | |||||
| 1141 | win := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 10, H: 10}} | 1148 | win := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 10, H: 10}} | ||
| 1142 | scenes <- ports.Scene{Seq: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{win}} | 1149 | scenes <- ports.Scene{Seq: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{win}} | ||
| 1143 | full := ports.SurfaceContent{ID: 1, Seq: 1, Width: 10, Height: 10, DMABuf: &ports.DMABuf{ID: 5, Planes: []ports.DMABufPlane{{}}}, Acquire: acq} | 1150 | full := ports.SurfaceContent{ID: 1, Seq: 1, Width: 10, Height: 10, DMABuf: &ports.DMABuf{ID: 5, Planes: []ports.DMABufPlane{{}}}, Acquire: acq} | ||
| 1144 | - | contents <- full | 1151 | + | contents.send(full) |
| 1145 | drawn(1, func(c ports.SurfaceContent, ok bool) bool { return ok && c.Acquire == acq }) | 1152 | drawn(1, func(c ports.SurfaceContent, ok bool) bool { return ok && c.Acquire == acq }) | ||
| 1146 | // The window unmaps: its empty content arrives before the scene that | 1153 | // The window unmaps: its empty content arrives before the scene that | ||
| 1147 | // draws it leaving (fading). The last content stays, fence dropped. | 1154 | // draws it leaving (fading). The last content stays, fence dropped. | ||
| 1148 | - | contents <- ports.SurfaceContent{ID: 1, Seq: 2} | 1155 | + | contents.send(ports.SurfaceContent{ID: 1, Seq: 2}) |
| 1149 | kept, _ := drawn(1, func(c ports.SurfaceContent, ok bool) bool { return !ok || c.Acquire == nil }) | 1156 | kept, _ := drawn(1, func(c ports.SurfaceContent, ok bool) bool { return !ok || c.Acquire == nil }) | ||
| 1150 | if kept.DMABuf != full.DMABuf || kept.Seq != 1 { | 1157 | if kept.DMABuf != full.DMABuf || kept.Seq != 1 { | ||
| 1151 | t.Fatalf("frame after the empty content drew %+v, want the last content", kept) | 1158 | t.Fatalf("frame after the empty content drew %+v, want the last content", kept) | ||
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, discardSink(t), nil, nil) | |
| 53 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, vt, scenes, nil, nil, 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, discardSink(t), nil, nil) | |
| 177 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, 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, discardSink(t), nil, nil) | 53 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, vt, scenes, nil, nil, 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, discardSink(t), nil, nil) | 177 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, 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) | ||
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, discardSink(t), nil, nil) | |
| 46 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, 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, discardSink(t), nil, nil) | 46 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, 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, discardSink(t), nil, nil) | |
| 99 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, 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, discardSink(t), nil, nil) | 99 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, 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, discardSink(t), nil, nil) | |
| 88 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, 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, discardSink(t), nil, nil) | 88 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, 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, discardSink(t), nil, nil) | |
| 64 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, 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, discardSink(t), nil, nil) | 64 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, 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_resume_test.go +1 -1
| @@ -61,7 +61,7 @@ func TestSecurityUnlockedResumeUsesOwnerRetry(t *testing.T) { | |||
| 61 | 61 | defer cancel() | |
| 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, active, nil, nil, nil, nil, nil, nil) | |
| 64 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, nil, nil, nil, nil) | |
| 65 | 65 | }() | |
| 66 | 66 | active <- true | |
| 67 | 67 | if stage == "master-loss" { | |
| @@ -61,7 +61,7 @@ func TestSecurityUnlockedResumeUsesOwnerRetry(t *testing.T) { | |||||
| 61 | defer cancel() | 61 | defer cancel() | ||
| 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, active, nil, nil, nil, nil, nil, nil) | 64 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, nil, nil, nil, nil) |
| 65 | }() | 65 | }() | ||
| 66 | active <- true | 66 | active <- true | ||
| 67 | if stage == "master-loss" { | 67 | if stage == "master-loss" { | ||
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, discardSink(t), nil, nil) | |
| 60 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, 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, discardSink(t), nil, nil) | 60 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, 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/security_startup_test.go +2 -2
| @@ -43,7 +43,7 @@ func TestSecurityProtectedStartupBlackProofAndCaptureRejection(t *testing.T) { | |||
| 43 | 43 | defer cancel() | |
| 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, scenes, nil, nil, nil, captures, captured) | |
| 46 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, nil, nil, captures, captured) | |
| 47 | 47 | }() | |
| 48 | 48 | select { | |
| 49 | 49 | case ev := <-events: | |
| @@ -107,7 +107,7 @@ func TestSecurityStartupClearWaitUsesRunContext(t *testing.T) { | |||
| 107 | 107 | r.EXPECT().Close().Return().Once() | |
| 108 | 108 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil) | |
| 109 | 109 | k.EXPECT().rmFB(mock.Anything).Return(nil) | |
| 110 | - | err := o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, nil, nil, nil, nil, nil, nil) | |
| 110 | + | err := o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, nil, nil, nil, nil, nil, nil, nil) | |
| 111 | 111 | if !errors.Is(err, context.Canceled) { | |
| 112 | 112 | t.Fatalf("startup cancellation: %v", err) | |
| 113 | 113 | } | |
| @@ -43,7 +43,7 @@ func TestSecurityProtectedStartupBlackProofAndCaptureRejection(t *testing.T) { | |||||
| 43 | defer cancel() | 43 | defer cancel() | ||
| 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, scenes, nil, nil, nil, captures, captured) | 46 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, nil, nil, captures, captured) |
| 47 | }() | 47 | }() | ||
| 48 | select { | 48 | select { | ||
| 49 | case ev := <-events: | 49 | case ev := <-events: | ||
| @@ -107,7 +107,7 @@ func TestSecurityStartupClearWaitUsesRunContext(t *testing.T) { | |||||
| 107 | r.EXPECT().Close().Return().Once() | 107 | r.EXPECT().Close().Return().Once() | ||
| 108 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil) | 108 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil) | ||
| 109 | k.EXPECT().rmFB(mock.Anything).Return(nil) | 109 | k.EXPECT().rmFB(mock.Anything).Return(nil) | ||
| 110 | - | err := o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, nil, nil, nil, nil, nil, nil) | 110 | + | err := o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, nil, nil, nil, nil, nil, nil, nil) |
| 111 | if !errors.Is(err, context.Canceled) { | 111 | if !errors.Is(err, context.Canceled) { | ||
| 112 | t.Fatalf("startup cancellation: %v", err) | 112 | t.Fatalf("startup cancellation: %v", err) | ||
| 113 | } | 113 | } | ||
M internal/adapters/drm/stuck_test.go +4 -4
| @@ -30,7 +30,7 @@ func runStuck(t *testing.T, n int, errs ...error) []commitRec { | |||
| 30 | 30 | type stuckRun struct { | |
| 31 | 31 | t *testing.T | |
| 32 | 32 | scenes chan ports.Scene | |
| 33 | - | contents chan ports.SurfaceContent | |
| 33 | + | contents *contentFeed | |
| 34 | 34 | active chan bool | |
| 35 | 35 | presented chan ports.OutputPresented | |
| 36 | 36 | sink ports.PresentationSink | |
| @@ -68,12 +68,12 @@ func startStuck(t *testing.T, render func() *os.File, errs ...error) *stuckRun { | |||
| 68 | 68 | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |
| 69 | 69 | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |
| 70 | 70 | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |
| 71 | - | h := &stuckRun{t: t, scenes: make(chan ports.Scene, 1), contents: make(chan ports.SurfaceContent, 1), active: make(chan bool, 1), presented: nil} | |
| 71 | + | h := &stuckRun{t: t, scenes: make(chan ports.Scene, 1), contents: newContentFeed(t, 1), active: make(chan bool, 1), presented: nil} | |
| 72 | 72 | h.sink, h.presented = presentedSink(t, 64) | |
| 73 | 73 | ctx, cancel := context.WithCancel(context.Background()) | |
| 74 | 74 | done := make(chan error, 1) | |
| 75 | 75 | go func() { | |
| 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) | |
| 76 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, h.active, h.scenes, h.contents.in(), h.contents.ret, nil, h.sink, nil, nil) | |
| 77 | 77 | }() | |
| 78 | 78 | h.count = func() int { | |
| 79 | 79 | commitMu.Lock() | |
| @@ -196,7 +196,7 @@ func TestSeenWaitsForRefusedFrameFence(t *testing.T) { | |||
| 196 | 196 | h.scenes <- ports.Scene{Seq: 1} | |
| 197 | 197 | // EBUSY, then the retry loses DRM master: nothing is pending. | |
| 198 | 198 | waitFor(t, func() bool { return h.count() == 4 }) | |
| 199 | - | h.contents <- ports.SurfaceContent{ID: 1, Seq: 7} | |
| 199 | + | h.contents.send(ports.SurfaceContent{ID: 1, Seq: 7}) | |
| 200 | 200 | select { | |
| 201 | 201 | case p := <-h.presented: | |
| 202 | 202 | t.Fatalf("seen reported while refused frames may be read: %+v", p) | |
| @@ -30,7 +30,7 @@ func runStuck(t *testing.T, n int, errs ...error) []commitRec { | |||||
| 30 | type stuckRun struct { | 30 | type stuckRun struct { | ||
| 31 | t *testing.T | 31 | t *testing.T | ||
| 32 | scenes chan ports.Scene | 32 | scenes chan ports.Scene | ||
| 33 | - | contents chan ports.SurfaceContent | 33 | + | contents *contentFeed |
| 34 | active chan bool | 34 | active chan bool | ||
| 35 | presented chan ports.OutputPresented | 35 | presented chan ports.OutputPresented | ||
| 36 | sink ports.PresentationSink | 36 | sink ports.PresentationSink | ||
| @@ -68,12 +68,12 @@ func startStuck(t *testing.T, render func() *os.File, errs ...error) *stuckRun { | |||||
| 68 | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | 68 | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | ||
| 69 | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | 69 | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | ||
| 70 | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | 70 | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | ||
| 71 | - | h := &stuckRun{t: t, scenes: make(chan ports.Scene, 1), contents: make(chan ports.SurfaceContent, 1), active: make(chan bool, 1), presented: nil} | 71 | + | h := &stuckRun{t: t, scenes: make(chan ports.Scene, 1), contents: newContentFeed(t, 1), active: make(chan bool, 1), presented: nil} |
| 72 | h.sink, h.presented = presentedSink(t, 64) | 72 | h.sink, h.presented = presentedSink(t, 64) | ||
| 73 | ctx, cancel := context.WithCancel(context.Background()) | 73 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 74 | done := make(chan error, 1) | 74 | done := make(chan error, 1) | ||
| 75 | go func() { | 75 | go func() { | ||
| 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) | 76 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, h.active, h.scenes, h.contents.in(), h.contents.ret, nil, h.sink, nil, nil) |
| 77 | }() | 77 | }() | ||
| 78 | h.count = func() int { | 78 | h.count = func() int { | ||
| 79 | commitMu.Lock() | 79 | commitMu.Lock() | ||
| @@ -196,7 +196,7 @@ func TestSeenWaitsForRefusedFrameFence(t *testing.T) { | |||||
| 196 | h.scenes <- ports.Scene{Seq: 1} | 196 | h.scenes <- ports.Scene{Seq: 1} | ||
| 197 | // EBUSY, then the retry loses DRM master: nothing is pending. | 197 | // EBUSY, then the retry loses DRM master: nothing is pending. | ||
| 198 | waitFor(t, func() bool { return h.count() == 4 }) | 198 | waitFor(t, func() bool { return h.count() == 4 }) | ||
| 199 | - | h.contents <- ports.SurfaceContent{ID: 1, Seq: 7} | 199 | + | h.contents.send(ports.SurfaceContent{ID: 1, Seq: 7}) |
| 200 | select { | 200 | select { | ||
| 201 | case p := <-h.presented: | 201 | case p := <-h.presented: | ||
| 202 | t.Fatalf("seen reported while refused frames may be read: %+v", p) | 202 | t.Fatalf("seen reported while refused frames may be read: %+v", p) | ||
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, discardSink(t), nil, nil) | |
| 118 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, 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, discardSink(t), nil, nil) | 118 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, 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_bounds_test.go +2 -2
| @@ -27,7 +27,7 @@ func TestScreenshotReadbackFailureDoesNotWriteBlackPNG(t *testing.T) { | |||
| 27 | 27 | r.EXPECT().Close().Return().Once() | |
| 28 | 28 | done := make(chan error, 1) | |
| 29 | 29 | go func() { | |
| 30 | - | done <- Run(ctx, Options{Width: 2, Height: 2, ScreenshotDir: dir, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil) | |
| 30 | + | done <- Run(ctx, Options{Width: 2, Height: 2, ScreenshotDir: dir, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil, nil) | |
| 31 | 31 | }() | |
| 32 | 32 | select { | |
| 33 | 33 | case <-attempted: | |
| @@ -64,7 +64,7 @@ func TestCaptureBatchBoundedAndRecycledWhileIdle(t *testing.T) { | |||
| 64 | 64 | replies := make(chan ports.CaptureDone, capture.MaxRequests+1) | |
| 65 | 65 | done := make(chan error, 1) | |
| 66 | 66 | go func() { | |
| 67 | - | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, incoming) | |
| 67 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, nil, incoming) | |
| 68 | 68 | }() | |
| 69 | 69 | for i := range capture.MaxRequests + 1 { | |
| 70 | 70 | f, err := os.CreateTemp(t.TempDir(), "capture") | |
| @@ -27,7 +27,7 @@ func TestScreenshotReadbackFailureDoesNotWriteBlackPNG(t *testing.T) { | |||||
| 27 | r.EXPECT().Close().Return().Once() | 27 | r.EXPECT().Close().Return().Once() | ||
| 28 | done := make(chan error, 1) | 28 | done := make(chan error, 1) | ||
| 29 | go func() { | 29 | go func() { | ||
| 30 | - | done <- Run(ctx, Options{Width: 2, Height: 2, ScreenshotDir: dir, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil) | 30 | + | done <- Run(ctx, Options{Width: 2, Height: 2, ScreenshotDir: dir, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil, nil) |
| 31 | }() | 31 | }() | ||
| 32 | select { | 32 | select { | ||
| 33 | case <-attempted: | 33 | case <-attempted: | ||
| @@ -64,7 +64,7 @@ func TestCaptureBatchBoundedAndRecycledWhileIdle(t *testing.T) { | |||||
| 64 | replies := make(chan ports.CaptureDone, capture.MaxRequests+1) | 64 | replies := make(chan ports.CaptureDone, capture.MaxRequests+1) | ||
| 65 | done := make(chan error, 1) | 65 | done := make(chan error, 1) | ||
| 66 | go func() { | 66 | go func() { | ||
| 67 | - | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, incoming) | 67 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, nil, incoming) |
| 68 | }() | 68 | }() | ||
| 69 | for i := range capture.MaxRequests + 1 { | 69 | for i := range capture.MaxRequests + 1 { | ||
| 70 | f, err := os.CreateTemp(t.TempDir(), "capture") | 70 | f, err := os.CreateTemp(t.TempDir(), "capture") | ||
M internal/adapters/headless/capture_test.go +6 -6
| @@ -74,7 +74,7 @@ func runSession(t *testing.T, scene ports.Scene, reqs []ports.CaptureRequest, wa | |||
| 74 | 74 | replies := make(chan ports.CaptureDone, 8) | |
| 75 | 75 | done := make(chan error, 1) | |
| 76 | 76 | go func() { | |
| 77 | - | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, incoming) | |
| 77 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, nil, incoming) | |
| 78 | 78 | }() | |
| 79 | 79 | // Requests wait for a scene: all of them belong to its first frame. | |
| 80 | 80 | for _, q := range reqs { | |
| @@ -168,7 +168,7 @@ func TestHiddenWorkspaceCaptureUsesChildRenderer(t *testing.T) { | |||
| 168 | 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 | - | }, scenes, nil, nil, incoming) | |
| 171 | + | }, scenes, nil, nil, nil, incoming) | |
| 172 | 172 | }() | |
| 173 | 173 | q := ports.CaptureRequest{ID: 1, Workspace: 9, OffScreen: true, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Dst: sessionCaptureFile(t)} | |
| 174 | 174 | incoming <- q | |
| @@ -241,7 +241,7 @@ func TestRenderErrorDoesNotAnswerHandedRequestsAgain(t *testing.T) { | |||
| 241 | 241 | done := make(chan error, 1) | |
| 242 | 242 | go func() { | |
| 243 | 243 | done <- Run(ctx, Options{Width: 2, Height: 2, Captured: replies, | |
| 244 | - | NewRenderer: func(int, int) (ports.Renderer, error) { return display, nil }}, scenes, nil, nil, incoming) | |
| 244 | + | NewRenderer: func(int, int) (ports.Renderer, error) { return display, nil }}, scenes, nil, nil, nil, incoming) | |
| 245 | 245 | }() | |
| 246 | 246 | incoming <- ports.CaptureRequest{ID: 1, Exclude: true, Session: 7, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Dst: sessionCaptureFile(t)} | |
| 247 | 247 | scenes <- sessionScene() | |
| @@ -312,7 +312,7 @@ func TestCaptureWaitsForItsIndicatorScene(t *testing.T) { | |||
| 312 | 312 | replies := make(chan ports.CaptureDone, 4) | |
| 313 | 313 | done := make(chan error, 1) | |
| 314 | 314 | go func() { | |
| 315 | - | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, incoming) | |
| 315 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, nil, incoming) | |
| 316 | 316 | }() | |
| 317 | 317 | bare := ports.Scene{Seq: 2, Scale: 1, OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1}}} | |
| 318 | 318 | scenes <- bare | |
| @@ -356,7 +356,7 @@ func TestCaptureWithoutIndicatorFailsAfterTheBoundedWait(t *testing.T) { | |||
| 356 | 356 | replies := make(chan ports.CaptureDone, 4) | |
| 357 | 357 | done := make(chan error, 1) | |
| 358 | 358 | go func() { | |
| 359 | - | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, incoming) | |
| 359 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, nil, incoming) | |
| 360 | 360 | }() | |
| 361 | 361 | scenes <- ports.Scene{Seq: 2, Scale: 1, OutputWidth: 2, OutputHeight: 2} | |
| 362 | 362 | start := time.Now() | |
| @@ -406,7 +406,7 @@ func TestCaptureDeliveredOnlyWhenTheIndicatedFramePresents(t *testing.T) { | |||
| 406 | 406 | replies := make(chan ports.CaptureDone, 4) | |
| 407 | 407 | done := make(chan error, 1) | |
| 408 | 408 | go func() { | |
| 409 | - | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, incoming) | |
| 409 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, nil, incoming) | |
| 410 | 410 | }() | |
| 411 | 411 | incoming <- ports.CaptureRequest{ID: 1, Indicate: true, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Dst: sessionCaptureFile(t)} | |
| 412 | 412 | scenes <- ports.Scene{Seq: 2, Scale: 1, OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1}}, | |
| @@ -74,7 +74,7 @@ func runSession(t *testing.T, scene ports.Scene, reqs []ports.CaptureRequest, wa | |||||
| 74 | replies := make(chan ports.CaptureDone, 8) | 74 | replies := make(chan ports.CaptureDone, 8) | ||
| 75 | done := make(chan error, 1) | 75 | done := make(chan error, 1) | ||
| 76 | go func() { | 76 | go func() { | ||
| 77 | - | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, incoming) | 77 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, nil, incoming) |
| 78 | }() | 78 | }() | ||
| 79 | // Requests wait for a scene: all of them belong to its first frame. | 79 | // Requests wait for a scene: all of them belong to its first frame. | ||
| 80 | for _, q := range reqs { | 80 | for _, q := range reqs { | ||
| @@ -168,7 +168,7 @@ func TestHiddenWorkspaceCaptureUsesChildRenderer(t *testing.T) { | |||||
| 168 | done <- Run(ctx, Options{Width: 2, Height: 2, Captured: replies, Presented: sink, | 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, nil, incoming) |
| 172 | }() | 172 | }() | ||
| 173 | q := ports.CaptureRequest{ID: 1, Workspace: 9, OffScreen: true, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Dst: sessionCaptureFile(t)} | 173 | q := ports.CaptureRequest{ID: 1, Workspace: 9, OffScreen: true, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Dst: sessionCaptureFile(t)} | ||
| 174 | incoming <- q | 174 | incoming <- q | ||
| @@ -241,7 +241,7 @@ func TestRenderErrorDoesNotAnswerHandedRequestsAgain(t *testing.T) { | |||||
| 241 | done := make(chan error, 1) | 241 | done := make(chan error, 1) | ||
| 242 | go func() { | 242 | go func() { | ||
| 243 | done <- Run(ctx, Options{Width: 2, Height: 2, Captured: replies, | 243 | done <- Run(ctx, Options{Width: 2, Height: 2, Captured: replies, | ||
| 244 | - | NewRenderer: func(int, int) (ports.Renderer, error) { return display, nil }}, scenes, nil, nil, incoming) | 244 | + | NewRenderer: func(int, int) (ports.Renderer, error) { return display, nil }}, scenes, nil, nil, nil, incoming) |
| 245 | }() | 245 | }() | ||
| 246 | incoming <- ports.CaptureRequest{ID: 1, Exclude: true, Session: 7, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Dst: sessionCaptureFile(t)} | 246 | incoming <- ports.CaptureRequest{ID: 1, Exclude: true, Session: 7, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Dst: sessionCaptureFile(t)} | ||
| 247 | scenes <- sessionScene() | 247 | scenes <- sessionScene() | ||
| @@ -312,7 +312,7 @@ func TestCaptureWaitsForItsIndicatorScene(t *testing.T) { | |||||
| 312 | replies := make(chan ports.CaptureDone, 4) | 312 | replies := make(chan ports.CaptureDone, 4) | ||
| 313 | done := make(chan error, 1) | 313 | done := make(chan error, 1) | ||
| 314 | go func() { | 314 | go func() { | ||
| 315 | - | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, incoming) | 315 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, nil, incoming) |
| 316 | }() | 316 | }() | ||
| 317 | bare := ports.Scene{Seq: 2, Scale: 1, OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1}}} | 317 | bare := ports.Scene{Seq: 2, Scale: 1, OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1}}} | ||
| 318 | scenes <- bare | 318 | scenes <- bare | ||
| @@ -356,7 +356,7 @@ func TestCaptureWithoutIndicatorFailsAfterTheBoundedWait(t *testing.T) { | |||||
| 356 | replies := make(chan ports.CaptureDone, 4) | 356 | replies := make(chan ports.CaptureDone, 4) | ||
| 357 | done := make(chan error, 1) | 357 | done := make(chan error, 1) | ||
| 358 | go func() { | 358 | go func() { | ||
| 359 | - | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, incoming) | 359 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, nil, incoming) |
| 360 | }() | 360 | }() | ||
| 361 | scenes <- ports.Scene{Seq: 2, Scale: 1, OutputWidth: 2, OutputHeight: 2} | 361 | scenes <- ports.Scene{Seq: 2, Scale: 1, OutputWidth: 2, OutputHeight: 2} | ||
| 362 | start := time.Now() | 362 | start := time.Now() | ||
| @@ -406,7 +406,7 @@ func TestCaptureDeliveredOnlyWhenTheIndicatedFramePresents(t *testing.T) { | |||||
| 406 | replies := make(chan ports.CaptureDone, 4) | 406 | replies := make(chan ports.CaptureDone, 4) | ||
| 407 | done := make(chan error, 1) | 407 | done := make(chan error, 1) | ||
| 408 | go func() { | 408 | go func() { | ||
| 409 | - | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, incoming) | 409 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, nil, incoming) |
| 410 | }() | 410 | }() | ||
| 411 | incoming <- ports.CaptureRequest{ID: 1, Indicate: true, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Dst: sessionCaptureFile(t)} | 411 | incoming <- ports.CaptureRequest{ID: 1, Indicate: true, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Dst: sessionCaptureFile(t)} | ||
| 412 | scenes <- ports.Scene{Seq: 2, Scale: 1, OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1}}, | 412 | scenes <- ports.Scene{Seq: 2, Scale: 1, OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1}}, | ||
A internal/adapters/headless/content_feed_test.go +39 -0
| @@ -0,0 +1,39 @@ | |||
| 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 | + | // contentFeed is the wayland side of Run's content input in tests: batches go | |
| 12 | + | // out on ch, and the generated ContentReturn mock checks that each comes back | |
| 13 | + | // reset. | |
| 14 | + | type contentFeed struct { | |
| 15 | + | ch chan *ports.ContentBatch | |
| 16 | + | ret *portsmocks.MockContentReturn | |
| 17 | + | returned chan *ports.ContentBatch | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | // newContentFeed makes a feed whose channel holds buffer batches. | |
| 21 | + | func newContentFeed(t *testing.T, buffer int) *contentFeed { | |
| 22 | + | t.Helper() | |
| 23 | + | f := &contentFeed{ch: make(chan *ports.ContentBatch, buffer), ret: portsmocks.NewMockContentReturn(t), returned: make(chan *ports.ContentBatch, 64)} | |
| 24 | + | f.ret.EXPECT().Return(mock.Anything).Run(func(b *ports.ContentBatch) { | |
| 25 | + | if len(b.Items) != 0 { | |
| 26 | + | t.Errorf("batch returned with %d items, want it reset", len(b.Items)) | |
| 27 | + | } | |
| 28 | + | f.returned <- b | |
| 29 | + | }).Return().Maybe() | |
| 30 | + | return f | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | // send hands the output one batch holding cs. | |
| 34 | + | func (f *contentFeed) send(cs ...ports.SurfaceContent) { | |
| 35 | + | f.ch <- &ports.ContentBatch{Items: cs} | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // in is the channel Run reads. | |
| 39 | + | func (f *contentFeed) in() <-chan *ports.ContentBatch { return f.ch } | |
| @@ -0,0 +1,39 @@ | |||||
| 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 | + | // contentFeed is the wayland side of Run's content input in tests: batches go | |||
| 12 | + | // out on ch, and the generated ContentReturn mock checks that each comes back | |||
| 13 | + | // reset. | |||
| 14 | + | type contentFeed struct { | |||
| 15 | + | ch chan *ports.ContentBatch | |||
| 16 | + | ret *portsmocks.MockContentReturn | |||
| 17 | + | returned chan *ports.ContentBatch | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | // newContentFeed makes a feed whose channel holds buffer batches. | |||
| 21 | + | func newContentFeed(t *testing.T, buffer int) *contentFeed { | |||
| 22 | + | t.Helper() | |||
| 23 | + | f := &contentFeed{ch: make(chan *ports.ContentBatch, buffer), ret: portsmocks.NewMockContentReturn(t), returned: make(chan *ports.ContentBatch, 64)} | |||
| 24 | + | f.ret.EXPECT().Return(mock.Anything).Run(func(b *ports.ContentBatch) { | |||
| 25 | + | if len(b.Items) != 0 { | |||
| 26 | + | t.Errorf("batch returned with %d items, want it reset", len(b.Items)) | |||
| 27 | + | } | |||
| 28 | + | f.returned <- b | |||
| 29 | + | }).Return().Maybe() | |||
| 30 | + | return f | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | // send hands the output one batch holding cs. | |||
| 34 | + | func (f *contentFeed) send(cs ...ports.SurfaceContent) { | |||
| 35 | + | f.ch <- &ports.ContentBatch{Items: cs} | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // in is the channel Run reads. | |||
| 39 | + | func (f *contentFeed) in() <-chan *ports.ContentBatch { return f.ch } | |||
M internal/adapters/headless/frames_test.go +1 -1
| @@ -21,7 +21,7 @@ func runWithFrames(t *testing.T, size int) (scenes chan ports.Scene, frames chan | |||
| 21 | 21 | done := make(chan error, 1) | |
| 22 | 22 | go func() { | |
| 23 | 23 | done <- Run(ctx, Options{Width: 2, Height: 2, Name: "HEADLESS-1", Presented: sink, Frames: frames, | |
| 24 | - | NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil) | |
| 24 | + | NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil, nil) | |
| 25 | 25 | }() | |
| 26 | 26 | return scenes, frames, rec, func() { | |
| 27 | 27 | cancel() | |
| @@ -21,7 +21,7 @@ func runWithFrames(t *testing.T, size int) (scenes chan ports.Scene, frames chan | |||||
| 21 | done := make(chan error, 1) | 21 | done := make(chan error, 1) | ||
| 22 | go func() { | 22 | go func() { | ||
| 23 | done <- Run(ctx, Options{Width: 2, Height: 2, Name: "HEADLESS-1", Presented: sink, Frames: frames, | 23 | done <- Run(ctx, Options{Width: 2, Height: 2, Name: "HEADLESS-1", Presented: sink, Frames: frames, | ||
| 24 | - | NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil) | 24 | + | NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil, nil) |
| 25 | }() | 25 | }() | ||
| 26 | return scenes, frames, rec, func() { | 26 | return scenes, frames, rec, func() { | ||
| 27 | cancel() | 27 | cancel() | ||
M internal/adapters/headless/output.go +14 -9
| @@ -60,7 +60,7 @@ type Options struct { | |||
| 60 | 60 | // windows it no longer draws. | |
| 61 | 61 | const trimEvery = 10 * time.Second | |
| 62 | 62 | ||
| 63 | - | func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange, incoming <-chan ports.CaptureRequest) error { | |
| 63 | + | func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents <-chan *ports.ContentBatch, returns ports.ContentReturn, cursor <-chan ports.CursorChange, incoming <-chan ports.CaptureRequest) error { | |
| 64 | 64 | r, err := opts.NewRenderer(opts.Width, opts.Height) | |
| 65 | 65 | if err != nil { | |
| 66 | 66 | return fmt.Errorf("create renderer: %w", err) | |
| @@ -192,10 +192,15 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 192 | 192 | // A closed window's last content goes once no scene draws it. | |
| 193 | 193 | table.Prune(scene) | |
| 194 | 194 | } | |
| 195 | - | update := func(c ports.SurfaceContent) { | |
| 196 | - | seen[c.ID] = max(seen[c.ID], c.Seq) | |
| 197 | - | dirty = dirty || capture.Shows(scene, c.ID) | |
| 198 | - | table.Update(c, scene) | |
| 195 | + | update := func(b *ports.ContentBatch) { | |
| 196 | + | for i := range b.Items { | |
| 197 | + | c := b.Items[i] | |
| 198 | + | seen[c.ID] = max(seen[c.ID], c.Seq) | |
| 199 | + | dirty = dirty || capture.Shows(scene, c.ID) | |
| 200 | + | table.Update(c, scene) | |
| 201 | + | } | |
| 202 | + | b.Reset() | |
| 203 | + | returns.Return(b) | |
| 199 | 204 | } | |
| 200 | 205 | clk := opts.Clock | |
| 201 | 206 | if clk == nil { | |
| @@ -320,7 +325,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 320 | 325 | return err | |
| 321 | 326 | } | |
| 322 | 327 | acceptScene(s) | |
| 323 | - | case c, ok := <-contents: | |
| 328 | + | case b, ok := <-contents: | |
| 324 | 329 | if !ok { | |
| 325 | 330 | contents = nil | |
| 326 | 331 | continue | |
| @@ -331,7 +336,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 331 | 336 | } | |
| 332 | 337 | return err | |
| 333 | 338 | } | |
| 334 | - | update(c) | |
| 339 | + | update(b) | |
| 335 | 340 | case c, ok := <-cursor: | |
| 336 | 341 | if !ok { | |
| 337 | 342 | cursor = nil | |
| @@ -362,7 +367,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 362 | 367 | } | |
| 363 | 368 | acceptScene(s) | |
| 364 | 369 | } | |
| 365 | - | case c, ok := <-contents: | |
| 370 | + | case b, ok := <-contents: | |
| 366 | 371 | if !ok { | |
| 367 | 372 | contents = nil | |
| 368 | 373 | } else { | |
| @@ -372,7 +377,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 372 | 377 | } | |
| 373 | 378 | return err | |
| 374 | 379 | } | |
| 375 | - | update(c) | |
| 380 | + | update(b) | |
| 376 | 381 | } | |
| 377 | 382 | default: | |
| 378 | 383 | break drain | |
| @@ -60,7 +60,7 @@ type Options struct { | |||||
| 60 | // windows it no longer draws. | 60 | // windows it no longer draws. | ||
| 61 | const trimEvery = 10 * time.Second | 61 | const trimEvery = 10 * time.Second | ||
| 62 | 62 | ||||
| 63 | - | func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange, incoming <-chan ports.CaptureRequest) error { | 63 | + | func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents <-chan *ports.ContentBatch, returns ports.ContentReturn, cursor <-chan ports.CursorChange, incoming <-chan ports.CaptureRequest) error { |
| 64 | r, err := opts.NewRenderer(opts.Width, opts.Height) | 64 | r, err := opts.NewRenderer(opts.Width, opts.Height) | ||
| 65 | if err != nil { | 65 | if err != nil { | ||
| 66 | return fmt.Errorf("create renderer: %w", err) | 66 | return fmt.Errorf("create renderer: %w", err) | ||
| @@ -192,10 +192,15 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 192 | // A closed window's last content goes once no scene draws it. | 192 | // A closed window's last content goes once no scene draws it. | ||
| 193 | table.Prune(scene) | 193 | table.Prune(scene) | ||
| 194 | } | 194 | } | ||
| 195 | - | update := func(c ports.SurfaceContent) { | 195 | + | update := func(b *ports.ContentBatch) { |
| 196 | - | seen[c.ID] = max(seen[c.ID], c.Seq) | 196 | + | for i := range b.Items { |
| 197 | - | dirty = dirty || capture.Shows(scene, c.ID) | 197 | + | c := b.Items[i] |
| 198 | - | table.Update(c, scene) | 198 | + | seen[c.ID] = max(seen[c.ID], c.Seq) |
| 199 | + | dirty = dirty || capture.Shows(scene, c.ID) | |||
| 200 | + | table.Update(c, scene) | |||
| 201 | + | } | |||
| 202 | + | b.Reset() | |||
| 203 | + | returns.Return(b) | |||
| 199 | } | 204 | } | ||
| 200 | clk := opts.Clock | 205 | clk := opts.Clock | ||
| 201 | if clk == nil { | 206 | if clk == nil { | ||
| @@ -320,7 +325,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 320 | return err | 325 | return err | ||
| 321 | } | 326 | } | ||
| 322 | acceptScene(s) | 327 | acceptScene(s) | ||
| 323 | - | case c, ok := <-contents: | 328 | + | case b, ok := <-contents: |
| 324 | if !ok { | 329 | if !ok { | ||
| 325 | contents = nil | 330 | contents = nil | ||
| 326 | continue | 331 | continue | ||
| @@ -331,7 +336,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 331 | } | 336 | } | ||
| 332 | return err | 337 | return err | ||
| 333 | } | 338 | } | ||
| 334 | - | update(c) | 339 | + | update(b) |
| 335 | case c, ok := <-cursor: | 340 | case c, ok := <-cursor: | ||
| 336 | if !ok { | 341 | if !ok { | ||
| 337 | cursor = nil | 342 | cursor = nil | ||
| @@ -362,7 +367,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 362 | } | 367 | } | ||
| 363 | acceptScene(s) | 368 | acceptScene(s) | ||
| 364 | } | 369 | } | ||
| 365 | - | case c, ok := <-contents: | 370 | + | case b, ok := <-contents: |
| 366 | if !ok { | 371 | if !ok { | ||
| 367 | contents = nil | 372 | contents = nil | ||
| 368 | } else { | 373 | } else { | ||
| @@ -372,7 +377,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 372 | } | 377 | } | ||
| 373 | return err | 378 | return err | ||
| 374 | } | 379 | } | ||
| 375 | - | update(c) | 380 | + | update(b) |
| 376 | } | 381 | } | ||
| 377 | default: | 382 | default: | ||
| 378 | break drain | 383 | break drain | ||
M internal/adapters/headless/output_test.go +59 -23
| @@ -71,17 +71,17 @@ func waitFrames(t *testing.T, f *frames, n int) { | |||
| 71 | 71 | func TestRun(t *testing.T) { | |
| 72 | 72 | dir := t.TempDir() | |
| 73 | 73 | scenes := make(chan ports.Scene, 8) | |
| 74 | - | contents := make(chan ports.SurfaceContent, 8) | |
| 74 | + | contents := newContentFeed(t, 8) | |
| 75 | 75 | r, f := recordingRenderer(t, nil) | |
| 76 | - | contents <- ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{Pool: 1}} | |
| 76 | + | contents.send(ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{Pool: 1}}) | |
| 77 | 77 | shown := []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}, {ID: 2, Rect: ports.Rect{W: 2, H: 2}}} | |
| 78 | 78 | scenes <- ports.Scene{Seq: 1, OutputWidth: 2, OutputHeight: 2, Windows: shown} | |
| 79 | 79 | scenes <- ports.Scene{Seq: 2, OutputWidth: 2, OutputHeight: 2, Windows: shown} | |
| 80 | - | contents <- ports.SurfaceContent{ID: 2, SHM: &ports.SHMBuffer{Pool: 2}} | |
| 80 | + | contents.send(ports.SurfaceContent{ID: 2, SHM: &ports.SHMBuffer{Pool: 2}}) | |
| 81 | 81 | ctx, cancel := context.WithCancel(context.Background()) | |
| 82 | 82 | done := make(chan error, 1) | |
| 83 | 83 | go func() { | |
| 84 | - | done <- Run(ctx, Options{Width: 2, Height: 2, ScreenshotDir: dir, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents, nil, nil) | |
| 84 | + | done <- Run(ctx, Options{Width: 2, Height: 2, ScreenshotDir: dir, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents.in(), contents.ret, nil, nil) | |
| 85 | 85 | }() | |
| 86 | 86 | waitFrames(t, f, 1) | |
| 87 | 87 | if s, c := f.snapshot(); len(s) != 1 || s[0].Seq != 2 || len(c[0]) != 2 { | |
| @@ -89,7 +89,7 @@ func TestRun(t *testing.T) { | |||
| 89 | 89 | } | |
| 90 | 90 | // An empty content of a window the scene still draws (it unmapped | |
| 91 | 91 | // and fades out) keeps the last one; a scene without it drops it. | |
| 92 | - | contents <- ports.SurfaceContent{ID: 1} | |
| 92 | + | contents.send(ports.SurfaceContent{ID: 1}) | |
| 93 | 93 | waitFrames(t, f, 2) | |
| 94 | 94 | if _, c := f.snapshot(); len(c) < 2 { | |
| 95 | 95 | t.Fatal("no second frame") | |
| @@ -123,20 +123,20 @@ func TestRun(t *testing.T) { | |||
| 123 | 123 | // A scene whose Seq the output already holds is not drawn again. | |
| 124 | 124 | func TestHeadlessSameSeqRendersOnce(t *testing.T) { | |
| 125 | 125 | scenes := make(chan ports.Scene) | |
| 126 | - | contents := make(chan ports.SurfaceContent) | |
| 126 | + | contents := newContentFeed(t, 0) | |
| 127 | 127 | r, f := recordingRenderer(t, nil) | |
| 128 | 128 | ctx, cancel := context.WithCancel(context.Background()) | |
| 129 | 129 | done := make(chan error, 1) | |
| 130 | 130 | go func() { | |
| 131 | - | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents, nil, nil) | |
| 131 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents.in(), contents.ret, nil, nil) | |
| 132 | 132 | }() | |
| 133 | 133 | scene := ports.Scene{Seq: 5, OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}}} | |
| 134 | 134 | scenes <- scene | |
| 135 | 135 | waitFrames(t, f, 1) | |
| 136 | 136 | scenes <- scene | |
| 137 | 137 | // Unshown content sends Run round its loop: the held scene is handled. | |
| 138 | - | contents <- ports.SurfaceContent{ID: 99, Seq: 1, SHM: &ports.SHMBuffer{}} | |
| 139 | - | contents <- ports.SurfaceContent{ID: 99, Seq: 2, SHM: &ports.SHMBuffer{}} | |
| 138 | + | contents.send(ports.SurfaceContent{ID: 99, Seq: 1, SHM: &ports.SHMBuffer{}}) | |
| 139 | + | contents.send(ports.SurfaceContent{ID: 99, Seq: 2, SHM: &ports.SHMBuffer{}}) | |
| 140 | 140 | if s, _ := f.snapshot(); len(s) != 1 { | |
| 141 | 141 | t.Fatalf("%d frames for a scene with the held Seq", len(s)) | |
| 142 | 142 | } | |
| @@ -155,22 +155,22 @@ func TestHeadlessSameSeqRendersOnce(t *testing.T) { | |||
| 155 | 155 | // Content of a window the output does not show draws no frame. | |
| 156 | 156 | func TestRunSkipsContentNotShown(t *testing.T) { | |
| 157 | 157 | scenes := make(chan ports.Scene, 1) | |
| 158 | - | contents := make(chan ports.SurfaceContent, 2) | |
| 158 | + | contents := newContentFeed(t, 2) | |
| 159 | 159 | scenes <- ports.Scene{Seq: 1, OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}, {ID: 3, Hidden: true}}} | |
| 160 | 160 | r, f := recordingRenderer(t, nil) | |
| 161 | 161 | ctx, cancel := context.WithCancel(context.Background()) | |
| 162 | 162 | done := make(chan error, 1) | |
| 163 | 163 | go func() { | |
| 164 | - | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents, nil, nil) | |
| 164 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents.in(), contents.ret, nil, nil) | |
| 165 | 165 | }() | |
| 166 | 166 | waitFrames(t, f, 1) | |
| 167 | - | contents <- ports.SurfaceContent{ID: 2, SHM: &ports.SHMBuffer{Pool: 2}} | |
| 168 | - | contents <- ports.SurfaceContent{ID: 3, SHM: &ports.SHMBuffer{Pool: 3}} | |
| 167 | + | contents.send(ports.SurfaceContent{ID: 2, SHM: &ports.SHMBuffer{Pool: 2}}) | |
| 168 | + | contents.send(ports.SurfaceContent{ID: 3, SHM: &ports.SHMBuffer{Pool: 3}}) | |
| 169 | 169 | time.Sleep(50 * time.Millisecond) | |
| 170 | 170 | if s, _ := f.snapshot(); len(s) != 1 { | |
| 171 | 171 | t.Fatalf("%d frames for content not shown", len(s)) | |
| 172 | 172 | } | |
| 173 | - | contents <- ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{Pool: 1}} | |
| 173 | + | contents.send(ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{Pool: 1}}) | |
| 174 | 174 | waitFrames(t, f, 2) | |
| 175 | 175 | if _, c := f.snapshot(); c[1][2].SHM == nil { | |
| 176 | 176 | t.Error("content not shown was dropped") | |
| @@ -185,11 +185,11 @@ func TestErrors(t *testing.T) { | |||
| 185 | 185 | expected := errors.New("failure") | |
| 186 | 186 | scenes := make(chan ports.Scene, 1) | |
| 187 | 187 | scenes <- ports.Scene{} | |
| 188 | - | if err := Run(context.Background(), Options{NewRenderer: func(int, int) (ports.Renderer, error) { return nil, expected }}, scenes, nil, nil, nil); !errors.Is(err, expected) { | |
| 188 | + | if err := Run(context.Background(), Options{NewRenderer: func(int, int) (ports.Renderer, error) { return nil, expected }}, scenes, nil, nil, nil, nil); !errors.Is(err, expected) { | |
| 189 | 189 | t.Fatal(err) | |
| 190 | 190 | } | |
| 191 | 191 | r, _ := recordingRenderer(t, expected) | |
| 192 | - | if err := Run(context.Background(), Options{NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil); !errors.Is(err, expected) { | |
| 192 | + | if err := Run(context.Background(), Options{NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil, nil); !errors.Is(err, expected) { | |
| 193 | 193 | t.Fatal(err) | |
| 194 | 194 | } | |
| 195 | 195 | } | |
| @@ -220,7 +220,7 @@ func TestRunTrimsRendererOnTick(t *testing.T) { | |||
| 220 | 220 | var statsLog bytes.Buffer // written by Run's goroutine, read after it ends | |
| 221 | 221 | log := zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: &statsLog}) | |
| 222 | 222 | go func() { | |
| 223 | - | done <- Run(context.Background(), Options{Width: 2, Height: 2, Clock: clk, Log: log, Name: "HEADLESS-1", NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, nil, nil, nil, nil) | |
| 223 | + | done <- Run(context.Background(), Options{Width: 2, Height: 2, Clock: clk, Log: log, Name: "HEADLESS-1", NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, nil, nil, nil, nil, nil) | |
| 224 | 224 | }() | |
| 225 | 225 | ticks <- now | |
| 226 | 226 | if got := <-trimmed; !got.Equal(now) { | |
| @@ -272,15 +272,15 @@ func TestWritePNGAtomic(t *testing.T) { | |||
| 272 | 272 | ||
| 273 | 273 | func TestRunPassesLayerContent(t *testing.T) { | |
| 274 | 274 | scenes := make(chan ports.Scene, 1) | |
| 275 | - | contents := make(chan ports.SurfaceContent, 1) | |
| 275 | + | contents := newContentFeed(t, 1) | |
| 276 | 276 | layer := ports.SceneLayer{ID: 42, Layer: ports.LayerTop, Rect: ports.Rect{W: 2, H: 2}} | |
| 277 | 277 | scenes <- ports.Scene{Layers: []ports.SceneLayer{layer}} | |
| 278 | - | contents <- ports.SurfaceContent{ID: 42, Width: 1, Height: 1, SHM: &ports.SHMBuffer{Pool: 42, Stride: 4}} | |
| 278 | + | contents.send(ports.SurfaceContent{ID: 42, Width: 1, Height: 1, SHM: &ports.SHMBuffer{Pool: 42, Stride: 4}}) | |
| 279 | 279 | r, f := recordingRenderer(t, nil) | |
| 280 | 280 | ctx, cancel := context.WithCancel(context.Background()) | |
| 281 | 281 | done := make(chan error, 1) | |
| 282 | 282 | go func() { | |
| 283 | - | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents, nil, nil) | |
| 283 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents.in(), contents.ret, nil, nil) | |
| 284 | 284 | }() | |
| 285 | 285 | deadline := time.After(3 * time.Second) | |
| 286 | 286 | for { | |
| @@ -316,7 +316,7 @@ func TestCaptureForcesFreshHeadlessFrame(t *testing.T) { | |||
| 316 | 316 | ctx, cancel := context.WithCancel(context.Background()) | |
| 317 | 317 | done := make(chan error, 1) | |
| 318 | 318 | go func() { | |
| 319 | - | done <- Run(ctx, Options{Width: 2, Height: 2, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, requests) | |
| 319 | + | done <- Run(ctx, Options{Width: 2, Height: 2, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil, requests) | |
| 320 | 320 | }() | |
| 321 | 321 | scenes <- ports.Scene{Background: "#000000"} | |
| 322 | 322 | waitFrames(t, frames, 1) | |
| @@ -377,7 +377,7 @@ func TestHeadlessRawHDRScreenshot(t *testing.T) { | |||
| 377 | 377 | ctx, cancel := context.WithCancel(context.Background()) | |
| 378 | 378 | done := make(chan error, 1) | |
| 379 | 379 | go func() { | |
| 380 | - | done <- Run(ctx, Options{Width: 2, Height: 2, HDR: true, RawHDR: true, ScreenshotDir: dir, Log: logging.For(ctx, "render"), NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil) | |
| 380 | + | done <- Run(ctx, Options{Width: 2, Height: 2, HDR: true, RawHDR: true, ScreenshotDir: dir, Log: logging.For(ctx, "render"), NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil, nil) | |
| 381 | 381 | }() | |
| 382 | 382 | select { | |
| 383 | 383 | case <-rendered: | |
| @@ -452,7 +452,7 @@ func TestHeadlessHDRFormatsConfirmed(t *testing.T) { | |||
| 452 | 452 | ctx, cancel := context.WithCancel(context.Background()) | |
| 453 | 453 | done := make(chan error, 1) | |
| 454 | 454 | go func() { | |
| 455 | - | done <- Run(ctx, Options{Width: 2, Height: 2, HDR: true, Name: "HEADLESS-1", Formats: formats, Log: logging.For(ctx, "render"), NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, nil, nil, nil, nil) | |
| 455 | + | done <- Run(ctx, Options{Width: 2, Height: 2, HDR: true, Name: "HEADLESS-1", Formats: formats, Log: logging.For(ctx, "render"), NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, nil, nil, nil, nil, nil) | |
| 456 | 456 | }() | |
| 457 | 457 | select { | |
| 458 | 458 | case f := <-formats: | |
| @@ -472,3 +472,39 @@ func TestHeadlessHDRFormatsConfirmed(t *testing.T) { | |||
| 472 | 472 | }) | |
| 473 | 473 | } | |
| 474 | 474 | } | |
| 475 | + | ||
| 476 | + | // One batch with two windows applies both before the frame they show in, and | |
| 477 | + | // goes back to wayland reset and exactly once. | |
| 478 | + | func TestRunAppliesWholeBatch(t *testing.T) { | |
| 479 | + | scenes := make(chan ports.Scene, 1) | |
| 480 | + | contents := newContentFeed(t, 1) | |
| 481 | + | shown := []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}, {ID: 2, Rect: ports.Rect{W: 2, H: 2}}} | |
| 482 | + | scenes <- ports.Scene{Seq: 1, OutputWidth: 2, OutputHeight: 2, Windows: shown} | |
| 483 | + | r, f := recordingRenderer(t, nil) | |
| 484 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 485 | + | done := make(chan error, 1) | |
| 486 | + | go func() { | |
| 487 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents.in(), contents.ret, nil, nil) | |
| 488 | + | }() | |
| 489 | + | waitFrames(t, f, 1) | |
| 490 | + | contents.send(ports.SurfaceContent{ID: 1, Seq: 1, SHM: &ports.SHMBuffer{Pool: 1}}, ports.SurfaceContent{ID: 2, Seq: 1, SHM: &ports.SHMBuffer{Pool: 2}}) | |
| 491 | + | waitFrames(t, f, 2) | |
| 492 | + | if _, c := f.snapshot(); len(c[1]) != 2 || c[1][1].SHM == nil || c[1][2].SHM == nil { | |
| 493 | + | t.Errorf("batch not applied whole: %+v", c[1]) | |
| 494 | + | } | |
| 495 | + | select { | |
| 496 | + | case b := <-contents.returned: | |
| 497 | + | if len(b.Items) != 0 { | |
| 498 | + | t.Errorf("batch returned with %d items", len(b.Items)) | |
| 499 | + | } | |
| 500 | + | case <-time.After(2 * time.Second): | |
| 501 | + | t.Fatal("batch not returned") | |
| 502 | + | } | |
| 503 | + | cancel() | |
| 504 | + | if err := <-done; err != nil { | |
| 505 | + | t.Fatal(err) | |
| 506 | + | } | |
| 507 | + | if len(contents.returned) != 0 { | |
| 508 | + | t.Error("batch returned more than once") | |
| 509 | + | } | |
| 510 | + | } | |
| @@ -71,17 +71,17 @@ func waitFrames(t *testing.T, f *frames, n int) { | |||||
| 71 | func TestRun(t *testing.T) { | 71 | func TestRun(t *testing.T) { | ||
| 72 | dir := t.TempDir() | 72 | dir := t.TempDir() | ||
| 73 | scenes := make(chan ports.Scene, 8) | 73 | scenes := make(chan ports.Scene, 8) | ||
| 74 | - | contents := make(chan ports.SurfaceContent, 8) | 74 | + | contents := newContentFeed(t, 8) |
| 75 | r, f := recordingRenderer(t, nil) | 75 | r, f := recordingRenderer(t, nil) | ||
| 76 | - | contents <- ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{Pool: 1}} | 76 | + | contents.send(ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{Pool: 1}}) |
| 77 | shown := []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}, {ID: 2, Rect: ports.Rect{W: 2, H: 2}}} | 77 | shown := []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}, {ID: 2, Rect: ports.Rect{W: 2, H: 2}}} | ||
| 78 | scenes <- ports.Scene{Seq: 1, OutputWidth: 2, OutputHeight: 2, Windows: shown} | 78 | scenes <- ports.Scene{Seq: 1, OutputWidth: 2, OutputHeight: 2, Windows: shown} | ||
| 79 | scenes <- ports.Scene{Seq: 2, OutputWidth: 2, OutputHeight: 2, Windows: shown} | 79 | scenes <- ports.Scene{Seq: 2, OutputWidth: 2, OutputHeight: 2, Windows: shown} | ||
| 80 | - | contents <- ports.SurfaceContent{ID: 2, SHM: &ports.SHMBuffer{Pool: 2}} | 80 | + | contents.send(ports.SurfaceContent{ID: 2, SHM: &ports.SHMBuffer{Pool: 2}}) |
| 81 | ctx, cancel := context.WithCancel(context.Background()) | 81 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 82 | done := make(chan error, 1) | 82 | done := make(chan error, 1) | ||
| 83 | go func() { | 83 | go func() { | ||
| 84 | - | done <- Run(ctx, Options{Width: 2, Height: 2, ScreenshotDir: dir, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents, nil, nil) | 84 | + | done <- Run(ctx, Options{Width: 2, Height: 2, ScreenshotDir: dir, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents.in(), contents.ret, nil, nil) |
| 85 | }() | 85 | }() | ||
| 86 | waitFrames(t, f, 1) | 86 | waitFrames(t, f, 1) | ||
| 87 | if s, c := f.snapshot(); len(s) != 1 || s[0].Seq != 2 || len(c[0]) != 2 { | 87 | if s, c := f.snapshot(); len(s) != 1 || s[0].Seq != 2 || len(c[0]) != 2 { | ||
| @@ -89,7 +89,7 @@ func TestRun(t *testing.T) { | |||||
| 89 | } | 89 | } | ||
| 90 | // An empty content of a window the scene still draws (it unmapped | 90 | // An empty content of a window the scene still draws (it unmapped | ||
| 91 | // and fades out) keeps the last one; a scene without it drops it. | 91 | // and fades out) keeps the last one; a scene without it drops it. | ||
| 92 | - | contents <- ports.SurfaceContent{ID: 1} | 92 | + | contents.send(ports.SurfaceContent{ID: 1}) |
| 93 | waitFrames(t, f, 2) | 93 | waitFrames(t, f, 2) | ||
| 94 | if _, c := f.snapshot(); len(c) < 2 { | 94 | if _, c := f.snapshot(); len(c) < 2 { | ||
| 95 | t.Fatal("no second frame") | 95 | t.Fatal("no second frame") | ||
| @@ -123,20 +123,20 @@ func TestRun(t *testing.T) { | |||||
| 123 | // A scene whose Seq the output already holds is not drawn again. | 123 | // A scene whose Seq the output already holds is not drawn again. | ||
| 124 | func TestHeadlessSameSeqRendersOnce(t *testing.T) { | 124 | func TestHeadlessSameSeqRendersOnce(t *testing.T) { | ||
| 125 | scenes := make(chan ports.Scene) | 125 | scenes := make(chan ports.Scene) | ||
| 126 | - | contents := make(chan ports.SurfaceContent) | 126 | + | contents := newContentFeed(t, 0) |
| 127 | r, f := recordingRenderer(t, nil) | 127 | r, f := recordingRenderer(t, nil) | ||
| 128 | ctx, cancel := context.WithCancel(context.Background()) | 128 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 129 | done := make(chan error, 1) | 129 | done := make(chan error, 1) | ||
| 130 | go func() { | 130 | go func() { | ||
| 131 | - | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents, nil, nil) | 131 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents.in(), contents.ret, nil, nil) |
| 132 | }() | 132 | }() | ||
| 133 | scene := ports.Scene{Seq: 5, OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}}} | 133 | scene := ports.Scene{Seq: 5, OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}}} | ||
| 134 | scenes <- scene | 134 | scenes <- scene | ||
| 135 | waitFrames(t, f, 1) | 135 | waitFrames(t, f, 1) | ||
| 136 | scenes <- scene | 136 | scenes <- scene | ||
| 137 | // Unshown content sends Run round its loop: the held scene is handled. | 137 | // Unshown content sends Run round its loop: the held scene is handled. | ||
| 138 | - | contents <- ports.SurfaceContent{ID: 99, Seq: 1, SHM: &ports.SHMBuffer{}} | 138 | + | contents.send(ports.SurfaceContent{ID: 99, Seq: 1, SHM: &ports.SHMBuffer{}}) |
| 139 | - | contents <- ports.SurfaceContent{ID: 99, Seq: 2, SHM: &ports.SHMBuffer{}} | 139 | + | contents.send(ports.SurfaceContent{ID: 99, Seq: 2, SHM: &ports.SHMBuffer{}}) |
| 140 | if s, _ := f.snapshot(); len(s) != 1 { | 140 | if s, _ := f.snapshot(); len(s) != 1 { | ||
| 141 | t.Fatalf("%d frames for a scene with the held Seq", len(s)) | 141 | t.Fatalf("%d frames for a scene with the held Seq", len(s)) | ||
| 142 | } | 142 | } | ||
| @@ -155,22 +155,22 @@ func TestHeadlessSameSeqRendersOnce(t *testing.T) { | |||||
| 155 | // Content of a window the output does not show draws no frame. | 155 | // Content of a window the output does not show draws no frame. | ||
| 156 | func TestRunSkipsContentNotShown(t *testing.T) { | 156 | func TestRunSkipsContentNotShown(t *testing.T) { | ||
| 157 | scenes := make(chan ports.Scene, 1) | 157 | scenes := make(chan ports.Scene, 1) | ||
| 158 | - | contents := make(chan ports.SurfaceContent, 2) | 158 | + | contents := newContentFeed(t, 2) |
| 159 | scenes <- ports.Scene{Seq: 1, OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}, {ID: 3, Hidden: true}}} | 159 | scenes <- ports.Scene{Seq: 1, OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}, {ID: 3, Hidden: true}}} | ||
| 160 | r, f := recordingRenderer(t, nil) | 160 | r, f := recordingRenderer(t, nil) | ||
| 161 | ctx, cancel := context.WithCancel(context.Background()) | 161 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 162 | done := make(chan error, 1) | 162 | done := make(chan error, 1) | ||
| 163 | go func() { | 163 | go func() { | ||
| 164 | - | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents, nil, nil) | 164 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents.in(), contents.ret, nil, nil) |
| 165 | }() | 165 | }() | ||
| 166 | waitFrames(t, f, 1) | 166 | waitFrames(t, f, 1) | ||
| 167 | - | contents <- ports.SurfaceContent{ID: 2, SHM: &ports.SHMBuffer{Pool: 2}} | 167 | + | contents.send(ports.SurfaceContent{ID: 2, SHM: &ports.SHMBuffer{Pool: 2}}) |
| 168 | - | contents <- ports.SurfaceContent{ID: 3, SHM: &ports.SHMBuffer{Pool: 3}} | 168 | + | contents.send(ports.SurfaceContent{ID: 3, SHM: &ports.SHMBuffer{Pool: 3}}) |
| 169 | time.Sleep(50 * time.Millisecond) | 169 | time.Sleep(50 * time.Millisecond) | ||
| 170 | if s, _ := f.snapshot(); len(s) != 1 { | 170 | if s, _ := f.snapshot(); len(s) != 1 { | ||
| 171 | t.Fatalf("%d frames for content not shown", len(s)) | 171 | t.Fatalf("%d frames for content not shown", len(s)) | ||
| 172 | } | 172 | } | ||
| 173 | - | contents <- ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{Pool: 1}} | 173 | + | contents.send(ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{Pool: 1}}) |
| 174 | waitFrames(t, f, 2) | 174 | waitFrames(t, f, 2) | ||
| 175 | if _, c := f.snapshot(); c[1][2].SHM == nil { | 175 | if _, c := f.snapshot(); c[1][2].SHM == nil { | ||
| 176 | t.Error("content not shown was dropped") | 176 | t.Error("content not shown was dropped") | ||
| @@ -185,11 +185,11 @@ func TestErrors(t *testing.T) { | |||||
| 185 | expected := errors.New("failure") | 185 | expected := errors.New("failure") | ||
| 186 | scenes := make(chan ports.Scene, 1) | 186 | scenes := make(chan ports.Scene, 1) | ||
| 187 | scenes <- ports.Scene{} | 187 | scenes <- ports.Scene{} | ||
| 188 | - | if err := Run(context.Background(), Options{NewRenderer: func(int, int) (ports.Renderer, error) { return nil, expected }}, scenes, nil, nil, nil); !errors.Is(err, expected) { | 188 | + | if err := Run(context.Background(), Options{NewRenderer: func(int, int) (ports.Renderer, error) { return nil, expected }}, scenes, nil, nil, nil, nil); !errors.Is(err, expected) { |
| 189 | t.Fatal(err) | 189 | t.Fatal(err) | ||
| 190 | } | 190 | } | ||
| 191 | r, _ := recordingRenderer(t, expected) | 191 | r, _ := recordingRenderer(t, expected) | ||
| 192 | - | if err := Run(context.Background(), Options{NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil); !errors.Is(err, expected) { | 192 | + | if err := Run(context.Background(), Options{NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil, nil); !errors.Is(err, expected) { |
| 193 | t.Fatal(err) | 193 | t.Fatal(err) | ||
| 194 | } | 194 | } | ||
| 195 | } | 195 | } | ||
| @@ -220,7 +220,7 @@ func TestRunTrimsRendererOnTick(t *testing.T) { | |||||
| 220 | var statsLog bytes.Buffer // written by Run's goroutine, read after it ends | 220 | var statsLog bytes.Buffer // written by Run's goroutine, read after it ends | ||
| 221 | log := zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: &statsLog}) | 221 | log := zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: &statsLog}) | ||
| 222 | go func() { | 222 | go func() { | ||
| 223 | - | done <- Run(context.Background(), Options{Width: 2, Height: 2, Clock: clk, Log: log, Name: "HEADLESS-1", NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, nil, nil, nil, nil) | 223 | + | done <- Run(context.Background(), Options{Width: 2, Height: 2, Clock: clk, Log: log, Name: "HEADLESS-1", NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, nil, nil, nil, nil, nil) |
| 224 | }() | 224 | }() | ||
| 225 | ticks <- now | 225 | ticks <- now | ||
| 226 | if got := <-trimmed; !got.Equal(now) { | 226 | if got := <-trimmed; !got.Equal(now) { | ||
| @@ -272,15 +272,15 @@ func TestWritePNGAtomic(t *testing.T) { | |||||
| 272 | 272 | ||||
| 273 | func TestRunPassesLayerContent(t *testing.T) { | 273 | func TestRunPassesLayerContent(t *testing.T) { | ||
| 274 | scenes := make(chan ports.Scene, 1) | 274 | scenes := make(chan ports.Scene, 1) | ||
| 275 | - | contents := make(chan ports.SurfaceContent, 1) | 275 | + | contents := newContentFeed(t, 1) |
| 276 | layer := ports.SceneLayer{ID: 42, Layer: ports.LayerTop, Rect: ports.Rect{W: 2, H: 2}} | 276 | layer := ports.SceneLayer{ID: 42, Layer: ports.LayerTop, Rect: ports.Rect{W: 2, H: 2}} | ||
| 277 | scenes <- ports.Scene{Layers: []ports.SceneLayer{layer}} | 277 | scenes <- ports.Scene{Layers: []ports.SceneLayer{layer}} | ||
| 278 | - | contents <- ports.SurfaceContent{ID: 42, Width: 1, Height: 1, SHM: &ports.SHMBuffer{Pool: 42, Stride: 4}} | 278 | + | contents.send(ports.SurfaceContent{ID: 42, Width: 1, Height: 1, SHM: &ports.SHMBuffer{Pool: 42, Stride: 4}}) |
| 279 | r, f := recordingRenderer(t, nil) | 279 | r, f := recordingRenderer(t, nil) | ||
| 280 | ctx, cancel := context.WithCancel(context.Background()) | 280 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 281 | done := make(chan error, 1) | 281 | done := make(chan error, 1) | ||
| 282 | go func() { | 282 | go func() { | ||
| 283 | - | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents, nil, nil) | 283 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents.in(), contents.ret, nil, nil) |
| 284 | }() | 284 | }() | ||
| 285 | deadline := time.After(3 * time.Second) | 285 | deadline := time.After(3 * time.Second) | ||
| 286 | for { | 286 | for { | ||
| @@ -316,7 +316,7 @@ func TestCaptureForcesFreshHeadlessFrame(t *testing.T) { | |||||
| 316 | ctx, cancel := context.WithCancel(context.Background()) | 316 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 317 | done := make(chan error, 1) | 317 | done := make(chan error, 1) | ||
| 318 | go func() { | 318 | go func() { | ||
| 319 | - | done <- Run(ctx, Options{Width: 2, Height: 2, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, requests) | 319 | + | done <- Run(ctx, Options{Width: 2, Height: 2, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil, requests) |
| 320 | }() | 320 | }() | ||
| 321 | scenes <- ports.Scene{Background: "#000000"} | 321 | scenes <- ports.Scene{Background: "#000000"} | ||
| 322 | waitFrames(t, frames, 1) | 322 | waitFrames(t, frames, 1) | ||
| @@ -377,7 +377,7 @@ func TestHeadlessRawHDRScreenshot(t *testing.T) { | |||||
| 377 | ctx, cancel := context.WithCancel(context.Background()) | 377 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 378 | done := make(chan error, 1) | 378 | done := make(chan error, 1) | ||
| 379 | go func() { | 379 | go func() { | ||
| 380 | - | done <- Run(ctx, Options{Width: 2, Height: 2, HDR: true, RawHDR: true, ScreenshotDir: dir, Log: logging.For(ctx, "render"), NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil) | 380 | + | done <- Run(ctx, Options{Width: 2, Height: 2, HDR: true, RawHDR: true, ScreenshotDir: dir, Log: logging.For(ctx, "render"), NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil, nil) |
| 381 | }() | 381 | }() | ||
| 382 | select { | 382 | select { | ||
| 383 | case <-rendered: | 383 | case <-rendered: | ||
| @@ -452,7 +452,7 @@ func TestHeadlessHDRFormatsConfirmed(t *testing.T) { | |||||
| 452 | ctx, cancel := context.WithCancel(context.Background()) | 452 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 453 | done := make(chan error, 1) | 453 | done := make(chan error, 1) | ||
| 454 | go func() { | 454 | go func() { | ||
| 455 | - | done <- Run(ctx, Options{Width: 2, Height: 2, HDR: true, Name: "HEADLESS-1", Formats: formats, Log: logging.For(ctx, "render"), NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, nil, nil, nil, nil) | 455 | + | done <- Run(ctx, Options{Width: 2, Height: 2, HDR: true, Name: "HEADLESS-1", Formats: formats, Log: logging.For(ctx, "render"), NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, nil, nil, nil, nil, nil) |
| 456 | }() | 456 | }() | ||
| 457 | select { | 457 | select { | ||
| 458 | case f := <-formats: | 458 | case f := <-formats: | ||
| @@ -472,3 +472,39 @@ func TestHeadlessHDRFormatsConfirmed(t *testing.T) { | |||||
| 472 | }) | 472 | }) | ||
| 473 | } | 473 | } | ||
| 474 | } | 474 | } | ||
| 475 | + | ||||
| 476 | + | // One batch with two windows applies both before the frame they show in, and | |||
| 477 | + | // goes back to wayland reset and exactly once. | |||
| 478 | + | func TestRunAppliesWholeBatch(t *testing.T) { | |||
| 479 | + | scenes := make(chan ports.Scene, 1) | |||
| 480 | + | contents := newContentFeed(t, 1) | |||
| 481 | + | shown := []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}, {ID: 2, Rect: ports.Rect{W: 2, H: 2}}} | |||
| 482 | + | scenes <- ports.Scene{Seq: 1, OutputWidth: 2, OutputHeight: 2, Windows: shown} | |||
| 483 | + | r, f := recordingRenderer(t, nil) | |||
| 484 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 485 | + | done := make(chan error, 1) | |||
| 486 | + | go func() { | |||
| 487 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents.in(), contents.ret, nil, nil) | |||
| 488 | + | }() | |||
| 489 | + | waitFrames(t, f, 1) | |||
| 490 | + | contents.send(ports.SurfaceContent{ID: 1, Seq: 1, SHM: &ports.SHMBuffer{Pool: 1}}, ports.SurfaceContent{ID: 2, Seq: 1, SHM: &ports.SHMBuffer{Pool: 2}}) | |||
| 491 | + | waitFrames(t, f, 2) | |||
| 492 | + | if _, c := f.snapshot(); len(c[1]) != 2 || c[1][1].SHM == nil || c[1][2].SHM == nil { | |||
| 493 | + | t.Errorf("batch not applied whole: %+v", c[1]) | |||
| 494 | + | } | |||
| 495 | + | select { | |||
| 496 | + | case b := <-contents.returned: | |||
| 497 | + | if len(b.Items) != 0 { | |||
| 498 | + | t.Errorf("batch returned with %d items", len(b.Items)) | |||
| 499 | + | } | |||
| 500 | + | case <-time.After(2 * time.Second): | |||
| 501 | + | t.Fatal("batch not returned") | |||
| 502 | + | } | |||
| 503 | + | cancel() | |||
| 504 | + | if err := <-done; err != nil { | |||
| 505 | + | t.Fatal(err) | |||
| 506 | + | } | |||
| 507 | + | if len(contents.returned) != 0 { | |||
| 508 | + | t.Error("batch returned more than once") | |||
| 509 | + | } | |||
| 510 | + | } | |||
M internal/adapters/headless/security_capture_test.go +2 -2
| @@ -61,7 +61,7 @@ func TestScopedCaptureClosesPostRenderAdmissionGap(t *testing.T) { | |||
| 61 | 61 | incoming := make(chan ports.CaptureRequest) | |
| 62 | 62 | replies := make(chan ports.CaptureDone, 2) | |
| 63 | 63 | go func() { | |
| 64 | - | done <- Run(ctx, Options{Security: gate, SecurityEvents: proof, Instance: 8, Width: 2, Height: 2, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, incoming) | |
| 64 | + | done <- Run(ctx, Options{Security: gate, SecurityEvents: proof, Instance: 8, Width: 2, Height: 2, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil, incoming) | |
| 65 | 65 | }() | |
| 66 | 66 | dst := sessionCaptureFile(t) | |
| 67 | 67 | incoming <- ports.CaptureRequest{ID: 91, Exclude: excluded, Session: 7, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Dst: dst} | |
| @@ -124,7 +124,7 @@ func TestTransitionDuringHeadlessRenderWaitRetainsFenceAndRejectsCapture(t *test | |||
| 124 | 124 | ctx, cancel := context.WithCancel(context.Background()) | |
| 125 | 125 | done := make(chan error, 1) | |
| 126 | 126 | go func() { | |
| 127 | - | done <- Run(ctx, Options{Security: gate, SecurityEvents: proof, Instance: 8, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, incoming) | |
| 127 | + | done <- Run(ctx, Options{Security: gate, SecurityEvents: proof, Instance: 8, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil, incoming) | |
| 128 | 128 | }() | |
| 129 | 129 | dst := sessionCaptureFile(t) | |
| 130 | 130 | incoming <- ports.CaptureRequest{ID: 92, Dst: dst} | |
| @@ -61,7 +61,7 @@ func TestScopedCaptureClosesPostRenderAdmissionGap(t *testing.T) { | |||||
| 61 | incoming := make(chan ports.CaptureRequest) | 61 | incoming := make(chan ports.CaptureRequest) | ||
| 62 | replies := make(chan ports.CaptureDone, 2) | 62 | replies := make(chan ports.CaptureDone, 2) | ||
| 63 | go func() { | 63 | go func() { | ||
| 64 | - | done <- Run(ctx, Options{Security: gate, SecurityEvents: proof, Instance: 8, Width: 2, Height: 2, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, incoming) | 64 | + | done <- Run(ctx, Options{Security: gate, SecurityEvents: proof, Instance: 8, Width: 2, Height: 2, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil, incoming) |
| 65 | }() | 65 | }() | ||
| 66 | dst := sessionCaptureFile(t) | 66 | dst := sessionCaptureFile(t) | ||
| 67 | incoming <- ports.CaptureRequest{ID: 91, Exclude: excluded, Session: 7, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Dst: dst} | 67 | incoming <- ports.CaptureRequest{ID: 91, Exclude: excluded, Session: 7, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Dst: dst} | ||
| @@ -124,7 +124,7 @@ func TestTransitionDuringHeadlessRenderWaitRetainsFenceAndRejectsCapture(t *test | |||||
| 124 | ctx, cancel := context.WithCancel(context.Background()) | 124 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 125 | done := make(chan error, 1) | 125 | done := make(chan error, 1) | ||
| 126 | go func() { | 126 | go func() { | ||
| 127 | - | done <- Run(ctx, Options{Security: gate, SecurityEvents: proof, Instance: 8, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, incoming) | 127 | + | done <- Run(ctx, Options{Security: gate, SecurityEvents: proof, Instance: 8, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil, incoming) |
| 128 | }() | 128 | }() | ||
| 129 | dst := sessionCaptureFile(t) | 129 | dst := sessionCaptureFile(t) | ||
| 130 | incoming <- ports.CaptureRequest{ID: 92, Dst: dst} | 130 | incoming <- ports.CaptureRequest{ID: 92, Dst: dst} | ||
M internal/adapters/headless/security_exclude_fence_test.go +3 -3
| @@ -46,7 +46,7 @@ func TestExcludeFenceRetainsReadsAcrossEngageAndOff(t *testing.T) { | |||
| 46 | 46 | } | |
| 47 | 47 | // Native capture and Pixels have no expectations; neither is admitted. | |
| 48 | 48 | scenes := make(chan ports.Scene, 2) | |
| 49 | - | contents := make(chan ports.SurfaceContent, 1) | |
| 49 | + | contents := newContentFeed(t, 1) | |
| 50 | 50 | incoming := make(chan ports.CaptureRequest) | |
| 51 | 51 | sink, presented := presentedSink(t, 8) | |
| 52 | 52 | proofs := make(chan ports.SecurityBackendEvent, 2) | |
| @@ -56,11 +56,11 @@ 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: sink, 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.in(), contents.ret, 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} | |
| 63 | - | contents <- ports.SurfaceContent{ID: 1, Seq: 8, SHM: &ports.SHMBuffer{Pool: 1}} | |
| 63 | + | contents.send(ports.SurfaceContent{ID: 1, Seq: 8, SHM: &ports.SHMBuffer{Pool: 1}}) | |
| 64 | 64 | scene := sessionScene() | |
| 65 | 65 | scene.Security = admitted | |
| 66 | 66 | scene.Scale = 1 | |
| @@ -46,7 +46,7 @@ func TestExcludeFenceRetainsReadsAcrossEngageAndOff(t *testing.T) { | |||||
| 46 | } | 46 | } | ||
| 47 | // Native capture and Pixels have no expectations; neither is admitted. | 47 | // Native capture and Pixels have no expectations; neither is admitted. | ||
| 48 | scenes := make(chan ports.Scene, 2) | 48 | scenes := make(chan ports.Scene, 2) | ||
| 49 | - | contents := make(chan ports.SurfaceContent, 1) | 49 | + | contents := newContentFeed(t, 1) |
| 50 | incoming := make(chan ports.CaptureRequest) | 50 | incoming := make(chan ports.CaptureRequest) | ||
| 51 | sink, presented := presentedSink(t, 8) | 51 | sink, presented := presentedSink(t, 8) | ||
| 52 | proofs := make(chan ports.SecurityBackendEvent, 2) | 52 | proofs := make(chan ports.SecurityBackendEvent, 2) | ||
| @@ -56,11 +56,11 @@ 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: sink, 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.in(), contents.ret, 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} | ||
| 63 | - | contents <- ports.SurfaceContent{ID: 1, Seq: 8, SHM: &ports.SHMBuffer{Pool: 1}} | 63 | + | contents.send(ports.SurfaceContent{ID: 1, Seq: 8, SHM: &ports.SHMBuffer{Pool: 1}}) |
| 64 | scene := sessionScene() | 64 | scene := sessionScene() | ||
| 65 | scene.Security = admitted | 65 | scene.Security = admitted | ||
| 66 | scene.Scale = 1 | 66 | scene.Scale = 1 | ||
M internal/adapters/headless/security_test.go +9 -9
| @@ -58,14 +58,14 @@ func TestSecurityInitialBlackFenceBeforeLockerAndNoReadback(t *testing.T) { | |||
| 58 | 58 | scenes := make(chan ports.Scene, 3) | |
| 59 | 59 | scenes <- ports.Scene{Seq: 1, Windows: []ports.SceneWindow{{ID: 1}}} | |
| 60 | 60 | scenes <- ports.Scene{Security: state, Seq: 2, Background: "#000000", Windows: []ports.SceneWindow{{ID: 10, Fullscreen: true, Rect: ports.Rect{W: 2, H: 2}}}} | |
| 61 | - | contents := make(chan ports.SurfaceContent, 1) | |
| 61 | + | contents := newContentFeed(t, 1) | |
| 62 | 62 | acquire, acquireSignal, err := os.Pipe() | |
| 63 | 63 | if err != nil { | |
| 64 | 64 | t.Fatal(err) | |
| 65 | 65 | } | |
| 66 | 66 | defer acquire.Close() | |
| 67 | 67 | defer acquireSignal.Close() | |
| 68 | - | contents <- ports.SurfaceContent{ID: 10, SHM: &ports.SHMBuffer{Pool: 10}, Acquire: acquire} | |
| 68 | + | contents.send(ports.SurfaceContent{ID: 10, SHM: &ports.SHMBuffer{Pool: 10}, Acquire: acquire}) | |
| 69 | 69 | proof := make(chan ports.SecurityBackendEvent, 4) | |
| 70 | 70 | replies := make(chan ports.CaptureDone, 4) | |
| 71 | 71 | requests := make(chan ports.CaptureRequest, 2) | |
| @@ -78,7 +78,7 @@ func TestSecurityInitialBlackFenceBeforeLockerAndNoReadback(t *testing.T) { | |||
| 78 | 78 | done := make(chan error, 1) | |
| 79 | 79 | dir := t.TempDir() | |
| 80 | 80 | go func() { | |
| 81 | - | done <- Run(ctx, Options{Security: gate, Instance: 9, SecurityEvents: proof, Width: 2, Height: 2, Name: "H", ScreenshotDir: dir, Cursor: &Cursor{}, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents, nil, requests) | |
| 81 | + | done <- Run(ctx, Options{Security: gate, Instance: 9, SecurityEvents: proof, Width: 2, Height: 2, Name: "H", ScreenshotDir: dir, Cursor: &Cursor{}, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents.in(), contents.ret, nil, requests) | |
| 82 | 82 | }() | |
| 83 | 83 | t.Cleanup(func() { | |
| 84 | 84 | cancel() | |
| @@ -150,7 +150,7 @@ func TestSecurityChangesWakeIdleAndRejectQueuedDesktop(t *testing.T) { | |||
| 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: sink, 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, nil) | |
| 154 | 154 | }() | |
| 155 | 155 | t.Cleanup(func() { | |
| 156 | 156 | cancel() | |
| @@ -191,7 +191,7 @@ func TestSecurityDedicatedChangeWakesWithoutScene(t *testing.T) { | |||
| 191 | 191 | ctx, cancel := context.WithCancel(context.Background()) | |
| 192 | 192 | done := make(chan error, 1) | |
| 193 | 193 | go func() { | |
| 194 | - | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, Instance: 5, SecurityEvents: proof, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, nil, nil, nil, nil) | |
| 194 | + | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, Instance: 5, SecurityEvents: proof, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, nil, nil, nil, nil, nil) | |
| 195 | 195 | }() | |
| 196 | 196 | locked := ports.SecurityState{Generation: 2, Protected: true} | |
| 197 | 197 | state.Store(locked) | |
| @@ -289,7 +289,7 @@ func TestSecurityRawHDRSuppressed(t *testing.T) { | |||
| 289 | 289 | ctx, cancel := context.WithCancel(context.Background()) | |
| 290 | 290 | done := make(chan error, 1) | |
| 291 | 291 | go func() { | |
| 292 | - | done <- Run(ctx, Options{Security: gate, Width: 2, Height: 2, HDR: true, RawHDR: true, ScreenshotDir: dir, Name: "H", Log: logging.For(ctx, "render"), NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil) | |
| 292 | + | done <- Run(ctx, Options{Security: gate, Width: 2, Height: 2, HDR: true, RawHDR: true, ScreenshotDir: dir, Name: "H", Log: logging.For(ctx, "render"), NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil, nil) | |
| 293 | 293 | }() | |
| 294 | 294 | // The black protection frame follows the frame under test (or is | |
| 295 | 295 | // the only one): the raw block is over once it is rendered. | |
| @@ -336,7 +336,7 @@ func TestSecurityQueuedCaptureFailedOnceAndProofCancellation(t *testing.T) { | |||
| 336 | 336 | ctx, cancel := context.WithCancel(context.Background()) | |
| 337 | 337 | done := make(chan error, 1) | |
| 338 | 338 | go func() { | |
| 339 | - | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, SecurityEvents: proofs, Instance: 4, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, nil, nil, nil, incoming) | |
| 339 | + | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, SecurityEvents: proofs, Instance: 4, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, nil, nil, nil, nil, incoming) | |
| 340 | 340 | }() | |
| 341 | 341 | file, err := os.CreateTemp(t.TempDir(), "queued") | |
| 342 | 342 | if err != nil { | |
| @@ -384,7 +384,7 @@ func TestSecurityOffOutputProofDoesNotRender(t *testing.T) { | |||
| 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: sink, 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, 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} | |
| @@ -442,7 +442,7 @@ func TestSecurityCompletionLeaseRecycledBeforeProtectionRenderFailure(t *testing | |||
| 442 | 442 | defer cancel() | |
| 443 | 443 | done := make(chan error, 1) | |
| 444 | 444 | go func() { | |
| 445 | - | done <- Run(ctx, Options{Security: gate, Width: 2, Height: 2, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, incoming) | |
| 445 | + | done <- Run(ctx, Options{Security: gate, Width: 2, Height: 2, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil, incoming) | |
| 446 | 446 | }() | |
| 447 | 447 | file, err := os.CreateTemp(t.TempDir(), "capture") | |
| 448 | 448 | if err != nil { | |
| @@ -58,14 +58,14 @@ func TestSecurityInitialBlackFenceBeforeLockerAndNoReadback(t *testing.T) { | |||||
| 58 | scenes := make(chan ports.Scene, 3) | 58 | scenes := make(chan ports.Scene, 3) | ||
| 59 | scenes <- ports.Scene{Seq: 1, Windows: []ports.SceneWindow{{ID: 1}}} | 59 | scenes <- ports.Scene{Seq: 1, Windows: []ports.SceneWindow{{ID: 1}}} | ||
| 60 | scenes <- ports.Scene{Security: state, Seq: 2, Background: "#000000", Windows: []ports.SceneWindow{{ID: 10, Fullscreen: true, Rect: ports.Rect{W: 2, H: 2}}}} | 60 | scenes <- ports.Scene{Security: state, Seq: 2, Background: "#000000", Windows: []ports.SceneWindow{{ID: 10, Fullscreen: true, Rect: ports.Rect{W: 2, H: 2}}}} | ||
| 61 | - | contents := make(chan ports.SurfaceContent, 1) | 61 | + | contents := newContentFeed(t, 1) |
| 62 | acquire, acquireSignal, err := os.Pipe() | 62 | acquire, acquireSignal, err := os.Pipe() | ||
| 63 | if err != nil { | 63 | if err != nil { | ||
| 64 | t.Fatal(err) | 64 | t.Fatal(err) | ||
| 65 | } | 65 | } | ||
| 66 | defer acquire.Close() | 66 | defer acquire.Close() | ||
| 67 | defer acquireSignal.Close() | 67 | defer acquireSignal.Close() | ||
| 68 | - | contents <- ports.SurfaceContent{ID: 10, SHM: &ports.SHMBuffer{Pool: 10}, Acquire: acquire} | 68 | + | contents.send(ports.SurfaceContent{ID: 10, SHM: &ports.SHMBuffer{Pool: 10}, Acquire: acquire}) |
| 69 | proof := make(chan ports.SecurityBackendEvent, 4) | 69 | proof := make(chan ports.SecurityBackendEvent, 4) | ||
| 70 | replies := make(chan ports.CaptureDone, 4) | 70 | replies := make(chan ports.CaptureDone, 4) | ||
| 71 | requests := make(chan ports.CaptureRequest, 2) | 71 | requests := make(chan ports.CaptureRequest, 2) | ||
| @@ -78,7 +78,7 @@ func TestSecurityInitialBlackFenceBeforeLockerAndNoReadback(t *testing.T) { | |||||
| 78 | done := make(chan error, 1) | 78 | done := make(chan error, 1) | ||
| 79 | dir := t.TempDir() | 79 | dir := t.TempDir() | ||
| 80 | go func() { | 80 | go func() { | ||
| 81 | - | done <- Run(ctx, Options{Security: gate, Instance: 9, SecurityEvents: proof, Width: 2, Height: 2, Name: "H", ScreenshotDir: dir, Cursor: &Cursor{}, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents, nil, requests) | 81 | + | done <- Run(ctx, Options{Security: gate, Instance: 9, SecurityEvents: proof, Width: 2, Height: 2, Name: "H", ScreenshotDir: dir, Cursor: &Cursor{}, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents.in(), contents.ret, nil, requests) |
| 82 | }() | 82 | }() | ||
| 83 | t.Cleanup(func() { | 83 | t.Cleanup(func() { | ||
| 84 | cancel() | 84 | cancel() | ||
| @@ -150,7 +150,7 @@ func TestSecurityChangesWakeIdleAndRejectQueuedDesktop(t *testing.T) { | |||||
| 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: sink, 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, nil) |
| 154 | }() | 154 | }() | ||
| 155 | t.Cleanup(func() { | 155 | t.Cleanup(func() { | ||
| 156 | cancel() | 156 | cancel() | ||
| @@ -191,7 +191,7 @@ func TestSecurityDedicatedChangeWakesWithoutScene(t *testing.T) { | |||||
| 191 | ctx, cancel := context.WithCancel(context.Background()) | 191 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 192 | done := make(chan error, 1) | 192 | done := make(chan error, 1) | ||
| 193 | go func() { | 193 | go func() { | ||
| 194 | - | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, Instance: 5, SecurityEvents: proof, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, nil, nil, nil, nil) | 194 | + | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, Instance: 5, SecurityEvents: proof, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, nil, nil, nil, nil, nil) |
| 195 | }() | 195 | }() | ||
| 196 | locked := ports.SecurityState{Generation: 2, Protected: true} | 196 | locked := ports.SecurityState{Generation: 2, Protected: true} | ||
| 197 | state.Store(locked) | 197 | state.Store(locked) | ||
| @@ -289,7 +289,7 @@ func TestSecurityRawHDRSuppressed(t *testing.T) { | |||||
| 289 | ctx, cancel := context.WithCancel(context.Background()) | 289 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 290 | done := make(chan error, 1) | 290 | done := make(chan error, 1) | ||
| 291 | go func() { | 291 | go func() { | ||
| 292 | - | done <- Run(ctx, Options{Security: gate, Width: 2, Height: 2, HDR: true, RawHDR: true, ScreenshotDir: dir, Name: "H", Log: logging.For(ctx, "render"), NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil) | 292 | + | done <- Run(ctx, Options{Security: gate, Width: 2, Height: 2, HDR: true, RawHDR: true, ScreenshotDir: dir, Name: "H", Log: logging.For(ctx, "render"), NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil, nil) |
| 293 | }() | 293 | }() | ||
| 294 | // The black protection frame follows the frame under test (or is | 294 | // The black protection frame follows the frame under test (or is | ||
| 295 | // the only one): the raw block is over once it is rendered. | 295 | // the only one): the raw block is over once it is rendered. | ||
| @@ -336,7 +336,7 @@ func TestSecurityQueuedCaptureFailedOnceAndProofCancellation(t *testing.T) { | |||||
| 336 | ctx, cancel := context.WithCancel(context.Background()) | 336 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 337 | done := make(chan error, 1) | 337 | done := make(chan error, 1) | ||
| 338 | go func() { | 338 | go func() { | ||
| 339 | - | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, SecurityEvents: proofs, Instance: 4, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, nil, nil, nil, incoming) | 339 | + | done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, SecurityEvents: proofs, Instance: 4, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, nil, nil, nil, nil, incoming) |
| 340 | }() | 340 | }() | ||
| 341 | file, err := os.CreateTemp(t.TempDir(), "queued") | 341 | file, err := os.CreateTemp(t.TempDir(), "queued") | ||
| 342 | if err != nil { | 342 | if err != nil { | ||
| @@ -384,7 +384,7 @@ func TestSecurityOffOutputProofDoesNotRender(t *testing.T) { | |||||
| 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: sink, 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, 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} | ||
| @@ -442,7 +442,7 @@ func TestSecurityCompletionLeaseRecycledBeforeProtectionRenderFailure(t *testing | |||||
| 442 | defer cancel() | 442 | defer cancel() | ||
| 443 | done := make(chan error, 1) | 443 | done := make(chan error, 1) | ||
| 444 | go func() { | 444 | go func() { | ||
| 445 | - | done <- Run(ctx, Options{Security: gate, Width: 2, Height: 2, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, incoming) | 445 | + | done <- Run(ctx, Options{Security: gate, Width: 2, Height: 2, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil, incoming) |
| 446 | }() | 446 | }() | ||
| 447 | file, err := os.CreateTemp(t.TempDir(), "capture") | 447 | file, err := os.CreateTemp(t.TempDir(), "capture") | ||
| 448 | if err != nil { | 448 | if err != nil { | ||
M internal/adapters/wayland/alloc_hotpath_test.go +4 -35
| @@ -1,7 +1,6 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "context" | |
| 5 | 4 | "testing" | |
| 6 | 5 | "time" | |
| 7 | 6 | ||
| @@ -88,18 +87,16 @@ func TestCommitSyncStorageIsPerUpdate(t *testing.T) { | |||
| 88 | 87 | ||
| 89 | 88 | func TestEmitContentAllocations(t *testing.T) { | |
| 90 | 89 | s := &Server{ | |
| 91 | - | channels: Channels{Contents: make(chan ports.SurfaceContent)}, | |
| 92 | - | contents: map[ports.WindowID]ports.SurfaceContent{}, | |
| 93 | - | contentSeq: map[ports.WindowID]uint64{}, | |
| 94 | - | damage: map[ports.WindowID][]ports.SeqDamage{}, | |
| 95 | - | contentReady: make(chan struct{}, 1), | |
| 90 | + | contentSeq: map[ports.WindowID]uint64{}, | |
| 91 | + | damage: map[ports.WindowID][]ports.SeqDamage{}, | |
| 92 | + | latest: map[ports.WindowID]ports.SurfaceContent{}, | |
| 96 | 93 | } | |
| 97 | 94 | c := ports.SurfaceContent{ID: 1, Solid: &ports.SolidColor{}} | |
| 98 | 95 | var held []ports.SurfaceContent // readers retaining published histories | |
| 99 | 96 | var snapshots [][]ports.SeqDamage | |
| 100 | 97 | for range 3 * damageHistoryCap { | |
| 101 | 98 | s.emitContent(c, damage{full: true}) | |
| 102 | - | got := s.contents[1] | |
| 99 | + | got := s.latest[1] | |
| 103 | 100 | held = append(held, got) | |
| 104 | 101 | snapshots = append(snapshots, append([]ports.SeqDamage(nil), got.DamageHistory...)) | |
| 105 | 102 | } | |
| @@ -123,31 +120,3 @@ func TestEmitContentAllocations(t *testing.T) { | |||
| 123 | 120 | } | |
| 124 | 121 | t.Logf("emitContent: %.2f allocs", allocs) | |
| 125 | 122 | } | |
| 126 | - | ||
| 127 | - | // A forwarder blocked on a stale frame is woken by the next publication | |
| 128 | - | // and sends the newer frame instead. | |
| 129 | - | func TestForwardContentsSendsNewerFrame(t *testing.T) { | |
| 130 | - | contents := make(chan ports.SurfaceContent) | |
| 131 | - | s := &Server{ | |
| 132 | - | channels: Channels{Contents: contents}, | |
| 133 | - | contents: map[ports.WindowID]ports.SurfaceContent{}, | |
| 134 | - | contentSeq: map[ports.WindowID]uint64{}, | |
| 135 | - | damage: map[ports.WindowID][]ports.SeqDamage{}, | |
| 136 | - | contentReady: make(chan struct{}, 1), | |
| 137 | - | } | |
| 138 | - | ctx, cancel := context.WithCancel(context.Background()) | |
| 139 | - | done := make(chan struct{}) | |
| 140 | - | go func() { defer close(done); s.forwardContents(ctx) }() | |
| 141 | - | defer func() { cancel(); <-done }() | |
| 142 | - | s.emitContent(ports.SurfaceContent{ID: 1, Seq: 1, Solid: &ports.SolidColor{}}, damage{full: true}) | |
| 143 | - | time.Sleep(10 * time.Millisecond) // let the forwarder block on seq 1 | |
| 144 | - | s.emitContent(ports.SurfaceContent{ID: 1, Seq: 2, Solid: &ports.SolidColor{}}, damage{full: true}) | |
| 145 | - | select { | |
| 146 | - | case c := <-contents: | |
| 147 | - | if c.Seq != 2 { | |
| 148 | - | t.Fatalf("stale frame %d forwarded, want 2", c.Seq) | |
| 149 | - | } | |
| 150 | - | case <-time.After(2 * time.Second): | |
| 151 | - | t.Fatal("no frame forwarded") | |
| 152 | - | } | |
| 153 | - | } | |
| @@ -1,7 +1,6 @@ | |||||
| 1 | package wayland | 1 | package wayland | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | - | "context" | |||
| 5 | "testing" | 4 | "testing" | ||
| 6 | "time" | 5 | "time" | ||
| 7 | 6 | ||||
| @@ -88,18 +87,16 @@ func TestCommitSyncStorageIsPerUpdate(t *testing.T) { | |||||
| 88 | 87 | ||||
| 89 | func TestEmitContentAllocations(t *testing.T) { | 88 | func TestEmitContentAllocations(t *testing.T) { | ||
| 90 | s := &Server{ | 89 | s := &Server{ | ||
| 91 | - | channels: Channels{Contents: make(chan ports.SurfaceContent)}, | 90 | + | contentSeq: map[ports.WindowID]uint64{}, |
| 92 | - | contents: map[ports.WindowID]ports.SurfaceContent{}, | 91 | + | damage: map[ports.WindowID][]ports.SeqDamage{}, |
| 93 | - | contentSeq: map[ports.WindowID]uint64{}, | 92 | + | latest: map[ports.WindowID]ports.SurfaceContent{}, |
| 94 | - | damage: map[ports.WindowID][]ports.SeqDamage{}, | |||
| 95 | - | contentReady: make(chan struct{}, 1), | |||
| 96 | } | 93 | } | ||
| 97 | c := ports.SurfaceContent{ID: 1, Solid: &ports.SolidColor{}} | 94 | c := ports.SurfaceContent{ID: 1, Solid: &ports.SolidColor{}} | ||
| 98 | var held []ports.SurfaceContent // readers retaining published histories | 95 | var held []ports.SurfaceContent // readers retaining published histories | ||
| 99 | var snapshots [][]ports.SeqDamage | 96 | var snapshots [][]ports.SeqDamage | ||
| 100 | for range 3 * damageHistoryCap { | 97 | for range 3 * damageHistoryCap { | ||
| 101 | s.emitContent(c, damage{full: true}) | 98 | s.emitContent(c, damage{full: true}) | ||
| 102 | - | got := s.contents[1] | 99 | + | got := s.latest[1] |
| 103 | held = append(held, got) | 100 | held = append(held, got) | ||
| 104 | snapshots = append(snapshots, append([]ports.SeqDamage(nil), got.DamageHistory...)) | 101 | snapshots = append(snapshots, append([]ports.SeqDamage(nil), got.DamageHistory...)) | ||
| 105 | } | 102 | } | ||
| @@ -123,31 +120,3 @@ func TestEmitContentAllocations(t *testing.T) { | |||||
| 123 | } | 120 | } | ||
| 124 | t.Logf("emitContent: %.2f allocs", allocs) | 121 | t.Logf("emitContent: %.2f allocs", allocs) | ||
| 125 | } | 122 | } | ||
| 126 | - | ||||
| 127 | - | // A forwarder blocked on a stale frame is woken by the next publication | |||
| 128 | - | // and sends the newer frame instead. | |||
| 129 | - | func TestForwardContentsSendsNewerFrame(t *testing.T) { | |||
| 130 | - | contents := make(chan ports.SurfaceContent) | |||
| 131 | - | s := &Server{ | |||
| 132 | - | channels: Channels{Contents: contents}, | |||
| 133 | - | contents: map[ports.WindowID]ports.SurfaceContent{}, | |||
| 134 | - | contentSeq: map[ports.WindowID]uint64{}, | |||
| 135 | - | damage: map[ports.WindowID][]ports.SeqDamage{}, | |||
| 136 | - | contentReady: make(chan struct{}, 1), | |||
| 137 | - | } | |||
| 138 | - | ctx, cancel := context.WithCancel(context.Background()) | |||
| 139 | - | done := make(chan struct{}) | |||
| 140 | - | go func() { defer close(done); s.forwardContents(ctx) }() | |||
| 141 | - | defer func() { cancel(); <-done }() | |||
| 142 | - | s.emitContent(ports.SurfaceContent{ID: 1, Seq: 1, Solid: &ports.SolidColor{}}, damage{full: true}) | |||
| 143 | - | time.Sleep(10 * time.Millisecond) // let the forwarder block on seq 1 | |||
| 144 | - | s.emitContent(ports.SurfaceContent{ID: 1, Seq: 2, Solid: &ports.SolidColor{}}, damage{full: true}) | |||
| 145 | - | select { | |||
| 146 | - | case c := <-contents: | |||
| 147 | - | if c.Seq != 2 { | |||
| 148 | - | t.Fatalf("stale frame %d forwarded, want 2", c.Seq) | |||
| 149 | - | } | |||
| 150 | - | case <-time.After(2 * time.Second): | |||
| 151 | - | t.Fatal("no frame forwarded") | |||
| 152 | - | } | |||
| 153 | - | } | |||
A internal/adapters/wayland/contentlink.go +162 -0
| @@ -0,0 +1,162 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "sync/atomic" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // batchesPerLink is how many batches circulate between wayland and one | |
| 10 | + | // output: one wayland fills, one waits in the channel, one the output reads. | |
| 11 | + | const batchesPerLink = 3 | |
| 12 | + | ||
| 13 | + | // batchItems is the initial capacity of a batch: windows beyond it grow the | |
| 14 | + | // batch once, and it keeps the capacity. | |
| 15 | + | const batchItems = 8 | |
| 16 | + | ||
| 17 | + | // contentLink is the content path to one running output. The display | |
| 18 | + | // goroutine owns cur and the sending side of in; the output owns the batch it | |
| 19 | + | // received until it returns it. free holds the batches nobody uses: it has | |
| 20 | + | // room for all of them, so Return never blocks. | |
| 21 | + | type contentLink struct { | |
| 22 | + | name string | |
| 23 | + | in chan *ports.ContentBatch | |
| 24 | + | free chan *ports.ContentBatch | |
| 25 | + | cur *ports.ContentBatch | |
| 26 | + | // pending: cur holds contents that could not be sent yet (the channel is | |
| 27 | + | // full, or every batch is out). Return only wakes the display when it is | |
| 28 | + | // set, so a steady stream of frames costs the display no extra wake. | |
| 29 | + | pending atomic.Bool | |
| 30 | + | ret *contentReturn | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | // contentReturn is the ports.ContentReturn of one link. | |
| 34 | + | type contentReturn struct { | |
| 35 | + | s *Server | |
| 36 | + | l *contentLink | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | // Return gives a consumed (reset) batch back and, when contents wait for one, | |
| 40 | + | // has the display goroutine send them. Safe from any goroutine, also after | |
| 41 | + | // the link was detached: the batch only goes back to its own link. | |
| 42 | + | func (r *contentReturn) Return(b *ports.ContentBatch) { | |
| 43 | + | r.l.free <- b // never full: the link owns batchesPerLink batches | |
| 44 | + | // The display stores pending before it looks at free once more, so | |
| 45 | + | // either it sees this batch or this load sees pending. | |
| 46 | + | if r.l.pending.Load() && r.s.flushPosted.CompareAndSwap(false, true) { | |
| 47 | + | r.s.display.Post(r.s.flushLinksFn) | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | // add puts c in the batch being filled, replacing the window's earlier | |
| 52 | + | // content: the output only needs the newest one (see ports.ContentBatch). | |
| 53 | + | func (l *contentLink) add(c ports.SurfaceContent) { | |
| 54 | + | items := l.cur.Items | |
| 55 | + | for i := range items { | |
| 56 | + | if items[i].ID == c.ID { | |
| 57 | + | items[i] = c | |
| 58 | + | return | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | l.cur.Items = append(items, c) | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // try sends the batch being filled when the channel is empty and a batch is | |
| 65 | + | // free to fill next. Display goroutine only. | |
| 66 | + | func (l *contentLink) try() bool { | |
| 67 | + | if len(l.in) != 0 { | |
| 68 | + | return false | |
| 69 | + | } | |
| 70 | + | select { | |
| 71 | + | case next := <-l.free: | |
| 72 | + | l.in <- l.cur // empty, and only this goroutine sends | |
| 73 | + | l.cur = next | |
| 74 | + | return true | |
| 75 | + | default: | |
| 76 | + | return false | |
| 77 | + | } | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // flushLink sends l's batch when the output can take it; otherwise l keeps | |
| 81 | + | // filling it and asks Return for a wake. | |
| 82 | + | func (s *Server) flushLink(l *contentLink) { | |
| 83 | + | if len(l.cur.Items) == 0 { | |
| 84 | + | l.pending.Store(false) | |
| 85 | + | return | |
| 86 | + | } | |
| 87 | + | if l.try() { | |
| 88 | + | l.pending.Store(false) | |
| 89 | + | return | |
| 90 | + | } | |
| 91 | + | l.pending.Store(true) | |
| 92 | + | if l.try() { // a Return may have slipped in before pending was set | |
| 93 | + | l.pending.Store(false) | |
| 94 | + | } | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // flushLinks retries every link that waits for a free batch. Display | |
| 98 | + | // goroutine; posted by Return. | |
| 99 | + | func (s *Server) flushLinks() { | |
| 100 | + | s.flushPosted.Store(false) | |
| 101 | + | for _, l := range s.links { | |
| 102 | + | if l.pending.Load() { | |
| 103 | + | s.flushLink(l) | |
| 104 | + | } | |
| 105 | + | } | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | // attachOutput starts feeding the output called name; a link of the same name | |
| 109 | + | // is replaced. The first batch holds the latest content of every live window, | |
| 110 | + | // so the output starts with all of them drawn. Display goroutine, or before | |
| 111 | + | // Run. | |
| 112 | + | func (s *Server) attachOutput(name string) (<-chan *ports.ContentBatch, ports.ContentReturn) { | |
| 113 | + | s.detachOutput(name) | |
| 114 | + | l := &contentLink{ | |
| 115 | + | name: name, | |
| 116 | + | in: make(chan *ports.ContentBatch, 1), | |
| 117 | + | free: make(chan *ports.ContentBatch, batchesPerLink), | |
| 118 | + | } | |
| 119 | + | l.ret = &contentReturn{s: s, l: l} | |
| 120 | + | for range batchesPerLink - 1 { | |
| 121 | + | l.free <- &ports.ContentBatch{Items: make([]ports.SurfaceContent, 0, batchItems)} | |
| 122 | + | } | |
| 123 | + | l.cur = &ports.ContentBatch{Items: make([]ports.SurfaceContent, 0, max(batchItems, len(s.latest)))} | |
| 124 | + | for _, c := range s.latest { | |
| 125 | + | l.add(c) | |
| 126 | + | } | |
| 127 | + | s.links = append(s.links, l) | |
| 128 | + | s.flushLink(l) | |
| 129 | + | return l.in, l.ret | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // detachOutput stops feeding the output called name. Display goroutine, or | |
| 133 | + | // before Run. The output may still Return its last batch. | |
| 134 | + | func (s *Server) detachOutput(name string) { | |
| 135 | + | for i, l := range s.links { | |
| 136 | + | if l.name == name { | |
| 137 | + | s.links[i] = nil | |
| 138 | + | s.links = append(s.links[:i], s.links[i+1:]...) | |
| 139 | + | return | |
| 140 | + | } | |
| 141 | + | } | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | // AttachOutput starts feeding the output called name with the contents of | |
| 145 | + | // the windows, as batches on the returned channel, starting with the current | |
| 146 | + | // content of every window. The output hands each batch back, reset, to the | |
| 147 | + | // returned ContentReturn. Call it from outside the display goroutine. It | |
| 148 | + | // returns a nil channel when the server is stopped. | |
| 149 | + | func (s *Server) AttachOutput(name string) (<-chan *ports.ContentBatch, ports.ContentReturn) { | |
| 150 | + | var in <-chan *ports.ContentBatch | |
| 151 | + | var ret ports.ContentReturn | |
| 152 | + | if !s.display.Do(func() { in, ret = s.attachOutput(name) }) { | |
| 153 | + | return nil, nil | |
| 154 | + | } | |
| 155 | + | return in, ret | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | // DetachOutput stops the feed of the output called name. Call it from outside | |
| 159 | + | // the display goroutine, once the output no longer reads its channel. | |
| 160 | + | func (s *Server) DetachOutput(name string) { | |
| 161 | + | s.display.Do(func() { s.detachOutput(name) }) | |
| 162 | + | } | |
| @@ -0,0 +1,162 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "sync/atomic" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // batchesPerLink is how many batches circulate between wayland and one | |||
| 10 | + | // output: one wayland fills, one waits in the channel, one the output reads. | |||
| 11 | + | const batchesPerLink = 3 | |||
| 12 | + | ||||
| 13 | + | // batchItems is the initial capacity of a batch: windows beyond it grow the | |||
| 14 | + | // batch once, and it keeps the capacity. | |||
| 15 | + | const batchItems = 8 | |||
| 16 | + | ||||
| 17 | + | // contentLink is the content path to one running output. The display | |||
| 18 | + | // goroutine owns cur and the sending side of in; the output owns the batch it | |||
| 19 | + | // received until it returns it. free holds the batches nobody uses: it has | |||
| 20 | + | // room for all of them, so Return never blocks. | |||
| 21 | + | type contentLink struct { | |||
| 22 | + | name string | |||
| 23 | + | in chan *ports.ContentBatch | |||
| 24 | + | free chan *ports.ContentBatch | |||
| 25 | + | cur *ports.ContentBatch | |||
| 26 | + | // pending: cur holds contents that could not be sent yet (the channel is | |||
| 27 | + | // full, or every batch is out). Return only wakes the display when it is | |||
| 28 | + | // set, so a steady stream of frames costs the display no extra wake. | |||
| 29 | + | pending atomic.Bool | |||
| 30 | + | ret *contentReturn | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | // contentReturn is the ports.ContentReturn of one link. | |||
| 34 | + | type contentReturn struct { | |||
| 35 | + | s *Server | |||
| 36 | + | l *contentLink | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | // Return gives a consumed (reset) batch back and, when contents wait for one, | |||
| 40 | + | // has the display goroutine send them. Safe from any goroutine, also after | |||
| 41 | + | // the link was detached: the batch only goes back to its own link. | |||
| 42 | + | func (r *contentReturn) Return(b *ports.ContentBatch) { | |||
| 43 | + | r.l.free <- b // never full: the link owns batchesPerLink batches | |||
| 44 | + | // The display stores pending before it looks at free once more, so | |||
| 45 | + | // either it sees this batch or this load sees pending. | |||
| 46 | + | if r.l.pending.Load() && r.s.flushPosted.CompareAndSwap(false, true) { | |||
| 47 | + | r.s.display.Post(r.s.flushLinksFn) | |||
| 48 | + | } | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | // add puts c in the batch being filled, replacing the window's earlier | |||
| 52 | + | // content: the output only needs the newest one (see ports.ContentBatch). | |||
| 53 | + | func (l *contentLink) add(c ports.SurfaceContent) { | |||
| 54 | + | items := l.cur.Items | |||
| 55 | + | for i := range items { | |||
| 56 | + | if items[i].ID == c.ID { | |||
| 57 | + | items[i] = c | |||
| 58 | + | return | |||
| 59 | + | } | |||
| 60 | + | } | |||
| 61 | + | l.cur.Items = append(items, c) | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | // try sends the batch being filled when the channel is empty and a batch is | |||
| 65 | + | // free to fill next. Display goroutine only. | |||
| 66 | + | func (l *contentLink) try() bool { | |||
| 67 | + | if len(l.in) != 0 { | |||
| 68 | + | return false | |||
| 69 | + | } | |||
| 70 | + | select { | |||
| 71 | + | case next := <-l.free: | |||
| 72 | + | l.in <- l.cur // empty, and only this goroutine sends | |||
| 73 | + | l.cur = next | |||
| 74 | + | return true | |||
| 75 | + | default: | |||
| 76 | + | return false | |||
| 77 | + | } | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | // flushLink sends l's batch when the output can take it; otherwise l keeps | |||
| 81 | + | // filling it and asks Return for a wake. | |||
| 82 | + | func (s *Server) flushLink(l *contentLink) { | |||
| 83 | + | if len(l.cur.Items) == 0 { | |||
| 84 | + | l.pending.Store(false) | |||
| 85 | + | return | |||
| 86 | + | } | |||
| 87 | + | if l.try() { | |||
| 88 | + | l.pending.Store(false) | |||
| 89 | + | return | |||
| 90 | + | } | |||
| 91 | + | l.pending.Store(true) | |||
| 92 | + | if l.try() { // a Return may have slipped in before pending was set | |||
| 93 | + | l.pending.Store(false) | |||
| 94 | + | } | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | // flushLinks retries every link that waits for a free batch. Display | |||
| 98 | + | // goroutine; posted by Return. | |||
| 99 | + | func (s *Server) flushLinks() { | |||
| 100 | + | s.flushPosted.Store(false) | |||
| 101 | + | for _, l := range s.links { | |||
| 102 | + | if l.pending.Load() { | |||
| 103 | + | s.flushLink(l) | |||
| 104 | + | } | |||
| 105 | + | } | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | // attachOutput starts feeding the output called name; a link of the same name | |||
| 109 | + | // is replaced. The first batch holds the latest content of every live window, | |||
| 110 | + | // so the output starts with all of them drawn. Display goroutine, or before | |||
| 111 | + | // Run. | |||
| 112 | + | func (s *Server) attachOutput(name string) (<-chan *ports.ContentBatch, ports.ContentReturn) { | |||
| 113 | + | s.detachOutput(name) | |||
| 114 | + | l := &contentLink{ | |||
| 115 | + | name: name, | |||
| 116 | + | in: make(chan *ports.ContentBatch, 1), | |||
| 117 | + | free: make(chan *ports.ContentBatch, batchesPerLink), | |||
| 118 | + | } | |||
| 119 | + | l.ret = &contentReturn{s: s, l: l} | |||
| 120 | + | for range batchesPerLink - 1 { | |||
| 121 | + | l.free <- &ports.ContentBatch{Items: make([]ports.SurfaceContent, 0, batchItems)} | |||
| 122 | + | } | |||
| 123 | + | l.cur = &ports.ContentBatch{Items: make([]ports.SurfaceContent, 0, max(batchItems, len(s.latest)))} | |||
| 124 | + | for _, c := range s.latest { | |||
| 125 | + | l.add(c) | |||
| 126 | + | } | |||
| 127 | + | s.links = append(s.links, l) | |||
| 128 | + | s.flushLink(l) | |||
| 129 | + | return l.in, l.ret | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | // detachOutput stops feeding the output called name. Display goroutine, or | |||
| 133 | + | // before Run. The output may still Return its last batch. | |||
| 134 | + | func (s *Server) detachOutput(name string) { | |||
| 135 | + | for i, l := range s.links { | |||
| 136 | + | if l.name == name { | |||
| 137 | + | s.links[i] = nil | |||
| 138 | + | s.links = append(s.links[:i], s.links[i+1:]...) | |||
| 139 | + | return | |||
| 140 | + | } | |||
| 141 | + | } | |||
| 142 | + | } | |||
| 143 | + | ||||
| 144 | + | // AttachOutput starts feeding the output called name with the contents of | |||
| 145 | + | // the windows, as batches on the returned channel, starting with the current | |||
| 146 | + | // content of every window. The output hands each batch back, reset, to the | |||
| 147 | + | // returned ContentReturn. Call it from outside the display goroutine. It | |||
| 148 | + | // returns a nil channel when the server is stopped. | |||
| 149 | + | func (s *Server) AttachOutput(name string) (<-chan *ports.ContentBatch, ports.ContentReturn) { | |||
| 150 | + | var in <-chan *ports.ContentBatch | |||
| 151 | + | var ret ports.ContentReturn | |||
| 152 | + | if !s.display.Do(func() { in, ret = s.attachOutput(name) }) { | |||
| 153 | + | return nil, nil | |||
| 154 | + | } | |||
| 155 | + | return in, ret | |||
| 156 | + | } | |||
| 157 | + | ||||
| 158 | + | // DetachOutput stops the feed of the output called name. Call it from outside | |||
| 159 | + | // the display goroutine, once the output no longer reads its channel. | |||
| 160 | + | func (s *Server) DetachOutput(name string) { | |||
| 161 | + | s.display.Do(func() { s.detachOutput(name) }) | |||
| 162 | + | } | |||
A internal/adapters/wayland/contentlink_test.go +217 -0
| @@ -0,0 +1,217 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/neferwl/internal/adapters/logging" | |
| 9 | + | "github.com/bnema/neferwl/internal/ports" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // linkServer is a running server with no client, to publish contents on its | |
| 13 | + | // display goroutine. | |
| 14 | + | func linkServer(t *testing.T) *Server { | |
| 15 | + | t.Helper() | |
| 16 | + | s, err := New(Options{RuntimeDir: t.TempDir(), Outputs: testOutputs}, Channels{}, logging.For(context.Background(), "wayland")) | |
| 17 | + | if err != nil { | |
| 18 | + | t.Fatal(err) | |
| 19 | + | } | |
| 20 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 21 | + | done := make(chan error, 1) | |
| 22 | + | go func() { done <- s.Run(ctx) }() | |
| 23 | + | t.Cleanup(func() { cancel(); <-done }) | |
| 24 | + | return s | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | func solid(id ports.WindowID) ports.SurfaceContent { | |
| 28 | + | return ports.SurfaceContent{ID: id, Solid: &ports.SolidColor{}} | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | func (s *Server) publish(t *testing.T, contents ...ports.SurfaceContent) { | |
| 32 | + | t.Helper() | |
| 33 | + | if !s.display.Do(func() { | |
| 34 | + | for _, c := range contents { | |
| 35 | + | s.emitContent(c, damage{full: true}) | |
| 36 | + | } | |
| 37 | + | }) { | |
| 38 | + | t.Fatal("display stopped") | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | func nextBatch(t *testing.T, in <-chan *ports.ContentBatch) *ports.ContentBatch { | |
| 43 | + | t.Helper() | |
| 44 | + | select { | |
| 45 | + | case b := <-in: | |
| 46 | + | return b | |
| 47 | + | case <-time.After(2 * time.Second): | |
| 48 | + | t.Fatal("no batch") | |
| 49 | + | return nil | |
| 50 | + | } | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | func noBatch(t *testing.T, in <-chan *ports.ContentBatch) { | |
| 54 | + | t.Helper() | |
| 55 | + | select { | |
| 56 | + | case b := <-in: | |
| 57 | + | t.Fatalf("unexpected batch %+v", b.Items) | |
| 58 | + | case <-time.After(30 * time.Millisecond): | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // Contents published while the output has not taken the previous batch | |
| 63 | + | // replace each other per window: the output gets the first batch, then one | |
| 64 | + | // with only the newest content, once it returned the first. (Ordering per | |
| 65 | + | // window holds because a batch holds one entry per window and batches are | |
| 66 | + | // FIFO; the release accounting needs only the newest Seq.) | |
| 67 | + | func TestContentBatchKeepsLatestPerWindow(t *testing.T) { | |
| 68 | + | s := linkServer(t) | |
| 69 | + | in, ret := s.AttachOutput("A") | |
| 70 | + | s.publish(t, solid(1)) // sent at once | |
| 71 | + | s.publish(t, solid(1), solid(2), solid(1)) // wait for the output | |
| 72 | + | first := nextBatch(t, in) | |
| 73 | + | if len(first.Items) != 1 || first.Items[0].ID != 1 || first.Items[0].Seq != 1 { | |
| 74 | + | t.Fatalf("first batch %+v", first.Items) | |
| 75 | + | } | |
| 76 | + | noBatch(t, in) // nothing moves before the output returns a batch... | |
| 77 | + | first.Reset() | |
| 78 | + | ret.Return(first) // ...which sends the waiting contents | |
| 79 | + | second := nextBatch(t, in) | |
| 80 | + | if len(second.Items) != 2 { | |
| 81 | + | t.Fatalf("second batch %+v", second.Items) | |
| 82 | + | } | |
| 83 | + | for _, c := range second.Items { | |
| 84 | + | switch c.ID { | |
| 85 | + | case 1: | |
| 86 | + | if c.Seq != 3 { | |
| 87 | + | t.Errorf("window 1 seq %d, want the newest (3)", c.Seq) | |
| 88 | + | } | |
| 89 | + | case 2: | |
| 90 | + | if c.Seq != 1 { | |
| 91 | + | t.Errorf("window 2 seq %d, want 1", c.Seq) | |
| 92 | + | } | |
| 93 | + | default: | |
| 94 | + | t.Errorf("unexpected window %d", c.ID) | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // An output attached later starts with the latest content of every live | |
| 100 | + | // window and nothing for windows that were emptied. | |
| 101 | + | func TestContentBatchAttachSeedsLiveWindows(t *testing.T) { | |
| 102 | + | s := linkServer(t) | |
| 103 | + | s.publish(t, solid(1), solid(2), solid(3), ports.SurfaceContent{ID: 2}, solid(1)) | |
| 104 | + | in, _ := s.AttachOutput("A") | |
| 105 | + | b := nextBatch(t, in) | |
| 106 | + | got := map[ports.WindowID]uint64{} | |
| 107 | + | for _, c := range b.Items { | |
| 108 | + | got[c.ID] = c.Seq | |
| 109 | + | } | |
| 110 | + | if len(got) != 2 || len(b.Items) != 2 || got[1] != 2 || got[3] != 1 { | |
| 111 | + | t.Fatalf("seed batch %+v", b.Items) | |
| 112 | + | } | |
| 113 | + | // An attach with nothing to show sends nothing. | |
| 114 | + | s.display.Do(func() { clear(s.latest) }) | |
| 115 | + | in2, _ := s.AttachOutput("B") | |
| 116 | + | noBatch(t, in2) | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | // An empty content reaches the attached outputs (it releases the window's | |
| 120 | + | // buffers) and removes the window from what a later output starts with. | |
| 121 | + | func TestContentBatchDeliversEmpty(t *testing.T) { | |
| 122 | + | s := linkServer(t) | |
| 123 | + | in, ret := s.AttachOutput("A") | |
| 124 | + | s.publish(t, solid(1)) | |
| 125 | + | b := nextBatch(t, in) | |
| 126 | + | b.Reset() | |
| 127 | + | ret.Return(b) | |
| 128 | + | s.publish(t, ports.SurfaceContent{ID: 1}) | |
| 129 | + | b = nextBatch(t, in) | |
| 130 | + | if len(b.Items) != 1 || b.Items[0].ID != 1 || !b.Items[0].Empty() || b.Items[0].Seq != 2 { | |
| 131 | + | t.Fatalf("empty not delivered: %+v", b.Items) | |
| 132 | + | } | |
| 133 | + | var left int | |
| 134 | + | s.display.Do(func() { left = len(s.latest) }) | |
| 135 | + | if left != 0 { | |
| 136 | + | t.Fatalf("latest keeps %d emptied windows", left) | |
| 137 | + | } | |
| 138 | + | // Superseded by an empty before the output looked: the empty wins. | |
| 139 | + | b.Reset() | |
| 140 | + | ret.Return(b) | |
| 141 | + | s.publish(t, solid(1)) // sent | |
| 142 | + | s.publish(t, solid(1), ports.SurfaceContent{ID: 1}) // pending: last one is empty | |
| 143 | + | first := nextBatch(t, in) | |
| 144 | + | first.Reset() | |
| 145 | + | ret.Return(first) | |
| 146 | + | b = nextBatch(t, in) | |
| 147 | + | if len(b.Items) != 1 || !b.Items[0].Empty() || b.Items[0].Seq != 5 { | |
| 148 | + | t.Fatalf("emptied window: %+v", b.Items) | |
| 149 | + | } | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | // Every attached output gets its own batches; detaching one stops its feed | |
| 153 | + | // only, and its output may still Return what it holds. | |
| 154 | + | func TestContentBatchDetach(t *testing.T) { | |
| 155 | + | s := linkServer(t) | |
| 156 | + | inA, retA := s.AttachOutput("A") | |
| 157 | + | inB, _ := s.AttachOutput("B") | |
| 158 | + | s.publish(t, solid(1)) | |
| 159 | + | a, b := nextBatch(t, inA), nextBatch(t, inB) | |
| 160 | + | if len(a.Items) != 1 || len(b.Items) != 1 || a == b { | |
| 161 | + | t.Fatalf("batches %+v %+v", a.Items, b.Items) | |
| 162 | + | } | |
| 163 | + | s.DetachOutput("A") | |
| 164 | + | // Waiting contents of a detached link are never sent; Return is safe. | |
| 165 | + | a.Reset() | |
| 166 | + | retA.Return(a) | |
| 167 | + | s.publish(t, solid(1)) | |
| 168 | + | noBatch(t, inA) | |
| 169 | + | if got := nextBatch(t, inB); len(got.Items) != 1 { | |
| 170 | + | t.Fatalf("B batch %+v", got.Items) | |
| 171 | + | } | |
| 172 | + | // Attaching under a known name replaces the link. | |
| 173 | + | inA2, _ := s.AttachOutput("A") | |
| 174 | + | if got := nextBatch(t, inA2); len(got.Items) != 1 { | |
| 175 | + | t.Fatalf("re-attached batch %+v", got.Items) | |
| 176 | + | } | |
| 177 | + | var links int | |
| 178 | + | s.display.Do(func() { links = len(s.links) }) | |
| 179 | + | if links != 2 { | |
| 180 | + | t.Fatalf("%d links, want 2", links) | |
| 181 | + | } | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | // Emit, send, consume and return a batch without allocating once the | |
| 185 | + | // batches have their capacity. The consumer runs inline, like an output that | |
| 186 | + | // keeps up. | |
| 187 | + | func TestContentBatchCycleAllocations(t *testing.T) { | |
| 188 | + | s := &Server{ | |
| 189 | + | contentSeq: map[ports.WindowID]uint64{}, | |
| 190 | + | damage: map[ports.WindowID][]ports.SeqDamage{}, | |
| 191 | + | latest: map[ports.WindowID]ports.SurfaceContent{}, | |
| 192 | + | } | |
| 193 | + | in, ret := s.attachOutput("A") | |
| 194 | + | c := solid(1) | |
| 195 | + | other := solid(2) | |
| 196 | + | cycle := func() { | |
| 197 | + | for _, c := range []ports.SurfaceContent{c, other} { | |
| 198 | + | s.emitContent(c, damage{full: true}) | |
| 199 | + | select { | |
| 200 | + | case b := <-in: | |
| 201 | + | for i := range b.Items { | |
| 202 | + | _ = b.Items[i].Seq | |
| 203 | + | } | |
| 204 | + | b.Reset() | |
| 205 | + | ret.Return(b) | |
| 206 | + | default: | |
| 207 | + | panic("no batch sent") | |
| 208 | + | } | |
| 209 | + | } | |
| 210 | + | } | |
| 211 | + | for range 3 * damageHistoryCap { | |
| 212 | + | cycle() | |
| 213 | + | } | |
| 214 | + | if allocs := testing.AllocsPerRun(200, cycle); allocs != 0 { | |
| 215 | + | t.Errorf("content batch cycle: %.2f allocs, want 0", allocs) | |
| 216 | + | } | |
| 217 | + | } | |
| @@ -0,0 +1,217 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/neferwl/internal/adapters/logging" | |||
| 9 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // linkServer is a running server with no client, to publish contents on its | |||
| 13 | + | // display goroutine. | |||
| 14 | + | func linkServer(t *testing.T) *Server { | |||
| 15 | + | t.Helper() | |||
| 16 | + | s, err := New(Options{RuntimeDir: t.TempDir(), Outputs: testOutputs}, Channels{}, logging.For(context.Background(), "wayland")) | |||
| 17 | + | if err != nil { | |||
| 18 | + | t.Fatal(err) | |||
| 19 | + | } | |||
| 20 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 21 | + | done := make(chan error, 1) | |||
| 22 | + | go func() { done <- s.Run(ctx) }() | |||
| 23 | + | t.Cleanup(func() { cancel(); <-done }) | |||
| 24 | + | return s | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | func solid(id ports.WindowID) ports.SurfaceContent { | |||
| 28 | + | return ports.SurfaceContent{ID: id, Solid: &ports.SolidColor{}} | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | func (s *Server) publish(t *testing.T, contents ...ports.SurfaceContent) { | |||
| 32 | + | t.Helper() | |||
| 33 | + | if !s.display.Do(func() { | |||
| 34 | + | for _, c := range contents { | |||
| 35 | + | s.emitContent(c, damage{full: true}) | |||
| 36 | + | } | |||
| 37 | + | }) { | |||
| 38 | + | t.Fatal("display stopped") | |||
| 39 | + | } | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | func nextBatch(t *testing.T, in <-chan *ports.ContentBatch) *ports.ContentBatch { | |||
| 43 | + | t.Helper() | |||
| 44 | + | select { | |||
| 45 | + | case b := <-in: | |||
| 46 | + | return b | |||
| 47 | + | case <-time.After(2 * time.Second): | |||
| 48 | + | t.Fatal("no batch") | |||
| 49 | + | return nil | |||
| 50 | + | } | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | func noBatch(t *testing.T, in <-chan *ports.ContentBatch) { | |||
| 54 | + | t.Helper() | |||
| 55 | + | select { | |||
| 56 | + | case b := <-in: | |||
| 57 | + | t.Fatalf("unexpected batch %+v", b.Items) | |||
| 58 | + | case <-time.After(30 * time.Millisecond): | |||
| 59 | + | } | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | // Contents published while the output has not taken the previous batch | |||
| 63 | + | // replace each other per window: the output gets the first batch, then one | |||
| 64 | + | // with only the newest content, once it returned the first. (Ordering per | |||
| 65 | + | // window holds because a batch holds one entry per window and batches are | |||
| 66 | + | // FIFO; the release accounting needs only the newest Seq.) | |||
| 67 | + | func TestContentBatchKeepsLatestPerWindow(t *testing.T) { | |||
| 68 | + | s := linkServer(t) | |||
| 69 | + | in, ret := s.AttachOutput("A") | |||
| 70 | + | s.publish(t, solid(1)) // sent at once | |||
| 71 | + | s.publish(t, solid(1), solid(2), solid(1)) // wait for the output | |||
| 72 | + | first := nextBatch(t, in) | |||
| 73 | + | if len(first.Items) != 1 || first.Items[0].ID != 1 || first.Items[0].Seq != 1 { | |||
| 74 | + | t.Fatalf("first batch %+v", first.Items) | |||
| 75 | + | } | |||
| 76 | + | noBatch(t, in) // nothing moves before the output returns a batch... | |||
| 77 | + | first.Reset() | |||
| 78 | + | ret.Return(first) // ...which sends the waiting contents | |||
| 79 | + | second := nextBatch(t, in) | |||
| 80 | + | if len(second.Items) != 2 { | |||
| 81 | + | t.Fatalf("second batch %+v", second.Items) | |||
| 82 | + | } | |||
| 83 | + | for _, c := range second.Items { | |||
| 84 | + | switch c.ID { | |||
| 85 | + | case 1: | |||
| 86 | + | if c.Seq != 3 { | |||
| 87 | + | t.Errorf("window 1 seq %d, want the newest (3)", c.Seq) | |||
| 88 | + | } | |||
| 89 | + | case 2: | |||
| 90 | + | if c.Seq != 1 { | |||
| 91 | + | t.Errorf("window 2 seq %d, want 1", c.Seq) | |||
| 92 | + | } | |||
| 93 | + | default: | |||
| 94 | + | t.Errorf("unexpected window %d", c.ID) | |||
| 95 | + | } | |||
| 96 | + | } | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | // An output attached later starts with the latest content of every live | |||
| 100 | + | // window and nothing for windows that were emptied. | |||
| 101 | + | func TestContentBatchAttachSeedsLiveWindows(t *testing.T) { | |||
| 102 | + | s := linkServer(t) | |||
| 103 | + | s.publish(t, solid(1), solid(2), solid(3), ports.SurfaceContent{ID: 2}, solid(1)) | |||
| 104 | + | in, _ := s.AttachOutput("A") | |||
| 105 | + | b := nextBatch(t, in) | |||
| 106 | + | got := map[ports.WindowID]uint64{} | |||
| 107 | + | for _, c := range b.Items { | |||
| 108 | + | got[c.ID] = c.Seq | |||
| 109 | + | } | |||
| 110 | + | if len(got) != 2 || len(b.Items) != 2 || got[1] != 2 || got[3] != 1 { | |||
| 111 | + | t.Fatalf("seed batch %+v", b.Items) | |||
| 112 | + | } | |||
| 113 | + | // An attach with nothing to show sends nothing. | |||
| 114 | + | s.display.Do(func() { clear(s.latest) }) | |||
| 115 | + | in2, _ := s.AttachOutput("B") | |||
| 116 | + | noBatch(t, in2) | |||
| 117 | + | } | |||
| 118 | + | ||||
| 119 | + | // An empty content reaches the attached outputs (it releases the window's | |||
| 120 | + | // buffers) and removes the window from what a later output starts with. | |||
| 121 | + | func TestContentBatchDeliversEmpty(t *testing.T) { | |||
| 122 | + | s := linkServer(t) | |||
| 123 | + | in, ret := s.AttachOutput("A") | |||
| 124 | + | s.publish(t, solid(1)) | |||
| 125 | + | b := nextBatch(t, in) | |||
| 126 | + | b.Reset() | |||
| 127 | + | ret.Return(b) | |||
| 128 | + | s.publish(t, ports.SurfaceContent{ID: 1}) | |||
| 129 | + | b = nextBatch(t, in) | |||
| 130 | + | if len(b.Items) != 1 || b.Items[0].ID != 1 || !b.Items[0].Empty() || b.Items[0].Seq != 2 { | |||
| 131 | + | t.Fatalf("empty not delivered: %+v", b.Items) | |||
| 132 | + | } | |||
| 133 | + | var left int | |||
| 134 | + | s.display.Do(func() { left = len(s.latest) }) | |||
| 135 | + | if left != 0 { | |||
| 136 | + | t.Fatalf("latest keeps %d emptied windows", left) | |||
| 137 | + | } | |||
| 138 | + | // Superseded by an empty before the output looked: the empty wins. | |||
| 139 | + | b.Reset() | |||
| 140 | + | ret.Return(b) | |||
| 141 | + | s.publish(t, solid(1)) // sent | |||
| 142 | + | s.publish(t, solid(1), ports.SurfaceContent{ID: 1}) // pending: last one is empty | |||
| 143 | + | first := nextBatch(t, in) | |||
| 144 | + | first.Reset() | |||
| 145 | + | ret.Return(first) | |||
| 146 | + | b = nextBatch(t, in) | |||
| 147 | + | if len(b.Items) != 1 || !b.Items[0].Empty() || b.Items[0].Seq != 5 { | |||
| 148 | + | t.Fatalf("emptied window: %+v", b.Items) | |||
| 149 | + | } | |||
| 150 | + | } | |||
| 151 | + | ||||
| 152 | + | // Every attached output gets its own batches; detaching one stops its feed | |||
| 153 | + | // only, and its output may still Return what it holds. | |||
| 154 | + | func TestContentBatchDetach(t *testing.T) { | |||
| 155 | + | s := linkServer(t) | |||
| 156 | + | inA, retA := s.AttachOutput("A") | |||
| 157 | + | inB, _ := s.AttachOutput("B") | |||
| 158 | + | s.publish(t, solid(1)) | |||
| 159 | + | a, b := nextBatch(t, inA), nextBatch(t, inB) | |||
| 160 | + | if len(a.Items) != 1 || len(b.Items) != 1 || a == b { | |||
| 161 | + | t.Fatalf("batches %+v %+v", a.Items, b.Items) | |||
| 162 | + | } | |||
| 163 | + | s.DetachOutput("A") | |||
| 164 | + | // Waiting contents of a detached link are never sent; Return is safe. | |||
| 165 | + | a.Reset() | |||
| 166 | + | retA.Return(a) | |||
| 167 | + | s.publish(t, solid(1)) | |||
| 168 | + | noBatch(t, inA) | |||
| 169 | + | if got := nextBatch(t, inB); len(got.Items) != 1 { | |||
| 170 | + | t.Fatalf("B batch %+v", got.Items) | |||
| 171 | + | } | |||
| 172 | + | // Attaching under a known name replaces the link. | |||
| 173 | + | inA2, _ := s.AttachOutput("A") | |||
| 174 | + | if got := nextBatch(t, inA2); len(got.Items) != 1 { | |||
| 175 | + | t.Fatalf("re-attached batch %+v", got.Items) | |||
| 176 | + | } | |||
| 177 | + | var links int | |||
| 178 | + | s.display.Do(func() { links = len(s.links) }) | |||
| 179 | + | if links != 2 { | |||
| 180 | + | t.Fatalf("%d links, want 2", links) | |||
| 181 | + | } | |||
| 182 | + | } | |||
| 183 | + | ||||
| 184 | + | // Emit, send, consume and return a batch without allocating once the | |||
| 185 | + | // batches have their capacity. The consumer runs inline, like an output that | |||
| 186 | + | // keeps up. | |||
| 187 | + | func TestContentBatchCycleAllocations(t *testing.T) { | |||
| 188 | + | s := &Server{ | |||
| 189 | + | contentSeq: map[ports.WindowID]uint64{}, | |||
| 190 | + | damage: map[ports.WindowID][]ports.SeqDamage{}, | |||
| 191 | + | latest: map[ports.WindowID]ports.SurfaceContent{}, | |||
| 192 | + | } | |||
| 193 | + | in, ret := s.attachOutput("A") | |||
| 194 | + | c := solid(1) | |||
| 195 | + | other := solid(2) | |||
| 196 | + | cycle := func() { | |||
| 197 | + | for _, c := range []ports.SurfaceContent{c, other} { | |||
| 198 | + | s.emitContent(c, damage{full: true}) | |||
| 199 | + | select { | |||
| 200 | + | case b := <-in: | |||
| 201 | + | for i := range b.Items { | |||
| 202 | + | _ = b.Items[i].Seq | |||
| 203 | + | } | |||
| 204 | + | b.Reset() | |||
| 205 | + | ret.Return(b) | |||
| 206 | + | default: | |||
| 207 | + | panic("no batch sent") | |||
| 208 | + | } | |||
| 209 | + | } | |||
| 210 | + | } | |||
| 211 | + | for range 3 * damageHistoryCap { | |||
| 212 | + | cycle() | |||
| 213 | + | } | |||
| 214 | + | if allocs := testing.AllocsPerRun(200, cycle); allocs != 0 { | |||
| 215 | + | t.Errorf("content batch cycle: %.2f allocs, want 0", allocs) | |||
| 216 | + | } | |||
| 217 | + | } | |||
A internal/adapters/wayland/contentrelay_test.go +37 -0
| @@ -0,0 +1,37 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // relayContents attaches an output to s before Run and returns the contents it | |
| 10 | + | // receives, one by one. It consumes batches like an output does: apply, then | |
| 11 | + | // Return. The server must not be running yet. | |
| 12 | + | func relayContents(t *testing.T, s *Server) <-chan ports.SurfaceContent { | |
| 13 | + | t.Helper() | |
| 14 | + | in, ret := s.attachOutput("TEST-1") | |
| 15 | + | out := make(chan ports.SurfaceContent, 64) | |
| 16 | + | stop := make(chan struct{}) | |
| 17 | + | t.Cleanup(func() { close(stop) }) | |
| 18 | + | go func() { | |
| 19 | + | for { | |
| 20 | + | select { | |
| 21 | + | case <-stop: | |
| 22 | + | return | |
| 23 | + | case b := <-in: | |
| 24 | + | for _, c := range b.Items { | |
| 25 | + | select { | |
| 26 | + | case out <- c: | |
| 27 | + | case <-stop: | |
| 28 | + | return | |
| 29 | + | } | |
| 30 | + | } | |
| 31 | + | b.Reset() | |
| 32 | + | ret.Return(b) | |
| 33 | + | } | |
| 34 | + | } | |
| 35 | + | }() | |
| 36 | + | return out | |
| 37 | + | } | |
| @@ -0,0 +1,37 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // relayContents attaches an output to s before Run and returns the contents it | |||
| 10 | + | // receives, one by one. It consumes batches like an output does: apply, then | |||
| 11 | + | // Return. The server must not be running yet. | |||
| 12 | + | func relayContents(t *testing.T, s *Server) <-chan ports.SurfaceContent { | |||
| 13 | + | t.Helper() | |||
| 14 | + | in, ret := s.attachOutput("TEST-1") | |||
| 15 | + | out := make(chan ports.SurfaceContent, 64) | |||
| 16 | + | stop := make(chan struct{}) | |||
| 17 | + | t.Cleanup(func() { close(stop) }) | |||
| 18 | + | go func() { | |||
| 19 | + | for { | |||
| 20 | + | select { | |||
| 21 | + | case <-stop: | |||
| 22 | + | return | |||
| 23 | + | case b := <-in: | |||
| 24 | + | for _, c := range b.Items { | |||
| 25 | + | select { | |||
| 26 | + | case out <- c: | |||
| 27 | + | case <-stop: | |||
| 28 | + | return | |||
| 29 | + | } | |||
| 30 | + | } | |||
| 31 | + | b.Reset() | |||
| 32 | + | ret.Return(b) | |||
| 33 | + | } | |||
| 34 | + | } | |||
| 35 | + | }() | |||
| 36 | + | return out | |||
| 37 | + | } | |||
M internal/adapters/wayland/dmabuf_test.go +3 -3
| @@ -15,15 +15,15 @@ import ( | |||
| 15 | 15 | ||
| 16 | 16 | var linearARGB = ports.DMABufFormat{Format: 'A' | 'R'<<8 | '2'<<16 | '4'<<24} | |
| 17 | 17 | ||
| 18 | - | func dmabufServer(t *testing.T, sup ports.DMABufSupport) (*Server, chan ports.ClientEvent, chan ports.SurfaceContent, string) { | |
| 18 | + | func dmabufServer(t *testing.T, sup ports.DMABufSupport) (*Server, chan ports.ClientEvent, <-chan ports.SurfaceContent, string) { | |
| 19 | 19 | t.Helper() | |
| 20 | 20 | dir := t.TempDir() | |
| 21 | 21 | events := make(chan ports.ClientEvent, 16) | |
| 22 | - | contents := make(chan ports.SurfaceContent, 16) | |
| 23 | - | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup}, Channels{Events: events, Contents: contents}, logging.For(context.Background(), "wayland")) | |
| 22 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup}, Channels{Events: events}, logging.For(context.Background(), "wayland")) | |
| 24 | 23 | if err != nil { | |
| 25 | 24 | t.Fatal(err) | |
| 26 | 25 | } | |
| 26 | + | contents := relayContents(t, s) | |
| 27 | 27 | ctx, cancel := context.WithCancel(context.Background()) | |
| 28 | 28 | done := make(chan error, 1) | |
| 29 | 29 | go func() { done <- s.Run(ctx) }() | |
| @@ -15,15 +15,15 @@ import ( | |||||
| 15 | 15 | ||||
| 16 | var linearARGB = ports.DMABufFormat{Format: 'A' | 'R'<<8 | '2'<<16 | '4'<<24} | 16 | var linearARGB = ports.DMABufFormat{Format: 'A' | 'R'<<8 | '2'<<16 | '4'<<24} | ||
| 17 | 17 | ||||
| 18 | - | func dmabufServer(t *testing.T, sup ports.DMABufSupport) (*Server, chan ports.ClientEvent, chan ports.SurfaceContent, string) { | 18 | + | func dmabufServer(t *testing.T, sup ports.DMABufSupport) (*Server, chan ports.ClientEvent, <-chan ports.SurfaceContent, string) { |
| 19 | t.Helper() | 19 | t.Helper() | ||
| 20 | dir := t.TempDir() | 20 | dir := t.TempDir() | ||
| 21 | events := make(chan ports.ClientEvent, 16) | 21 | events := make(chan ports.ClientEvent, 16) | ||
| 22 | - | contents := make(chan ports.SurfaceContent, 16) | 22 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup}, Channels{Events: events}, logging.For(context.Background(), "wayland")) |
| 23 | - | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup}, Channels{Events: events, Contents: contents}, logging.For(context.Background(), "wayland")) | |||
| 24 | if err != nil { | 23 | if err != nil { | ||
| 25 | t.Fatal(err) | 24 | t.Fatal(err) | ||
| 26 | } | 25 | } | ||
| 26 | + | contents := relayContents(t, s) | |||
| 27 | ctx, cancel := context.WithCancel(context.Background()) | 27 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 28 | done := make(chan error, 1) | 28 | done := make(chan error, 1) | ||
| 29 | go func() { done <- s.Run(ctx) }() | 29 | go func() { done <- s.Run(ctx) }() | ||
M internal/adapters/wayland/presentation.go +0 -4
| @@ -81,9 +81,7 @@ func (s *surface) commitFeedback(pending []*presentationtime.WpPresentationFeedb | |||
| 81 | 81 | root := s.root() | |
| 82 | 82 | win := root.windowID() | |
| 83 | 83 | if win != 0 && fresh { | |
| 84 | - | srv.contentMu.Lock() | |
| 85 | 84 | seq := srv.contentSeq[win] | |
| 86 | - | srv.contentMu.Unlock() | |
| 87 | 85 | for i := range srv.feedbacks { | |
| 88 | 86 | if w := &srv.feedbacks[i]; w.surf == s && w.replaced == 0 && seq > w.seq { | |
| 89 | 87 | w.replaced = seq | |
| @@ -102,9 +100,7 @@ func (s *surface) commitFeedback(pending []*presentationtime.WpPresentationFeedb | |||
| 102 | 100 | } | |
| 103 | 101 | return | |
| 104 | 102 | } | |
| 105 | - | srv.contentMu.Lock() | |
| 106 | 103 | seq := srv.contentSeq[win] | |
| 107 | - | srv.contentMu.Unlock() | |
| 108 | 104 | for _, fb := range pending { | |
| 109 | 105 | srv.feedbacks = append(srv.feedbacks, feedbackWait{fb: fb, surf: s, win: win, seq: seq, at: time.Now()}) | |
| 110 | 106 | } | |
| @@ -81,9 +81,7 @@ func (s *surface) commitFeedback(pending []*presentationtime.WpPresentationFeedb | |||||
| 81 | root := s.root() | 81 | root := s.root() | ||
| 82 | win := root.windowID() | 82 | win := root.windowID() | ||
| 83 | if win != 0 && fresh { | 83 | if win != 0 && fresh { | ||
| 84 | - | srv.contentMu.Lock() | |||
| 85 | seq := srv.contentSeq[win] | 84 | seq := srv.contentSeq[win] | ||
| 86 | - | srv.contentMu.Unlock() | |||
| 87 | for i := range srv.feedbacks { | 85 | for i := range srv.feedbacks { | ||
| 88 | if w := &srv.feedbacks[i]; w.surf == s && w.replaced == 0 && seq > w.seq { | 86 | if w := &srv.feedbacks[i]; w.surf == s && w.replaced == 0 && seq > w.seq { | ||
| 89 | w.replaced = seq | 87 | w.replaced = seq | ||
| @@ -102,9 +100,7 @@ func (s *surface) commitFeedback(pending []*presentationtime.WpPresentationFeedb | |||||
| 102 | } | 100 | } | ||
| 103 | return | 101 | return | ||
| 104 | } | 102 | } | ||
| 105 | - | srv.contentMu.Lock() | |||
| 106 | seq := srv.contentSeq[win] | 103 | seq := srv.contentSeq[win] | ||
| 107 | - | srv.contentMu.Unlock() | |||
| 108 | for _, fb := range pending { | 104 | for _, fb := range pending { | ||
| 109 | srv.feedbacks = append(srv.feedbacks, feedbackWait{fb: fb, surf: s, win: win, seq: seq, at: time.Now()}) | 105 | srv.feedbacks = append(srv.feedbacks, feedbackWait{fb: fb, surf: s, win: win, seq: seq, at: time.Now()}) | ||
| 110 | } | 106 | } | ||
M internal/adapters/wayland/presentation_test.go +3 -3
| @@ -46,16 +46,16 @@ 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, ports.PresentationSink, 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 | - | contents := make(chan ports.SurfaceContent, 64) | |
| 55 | - | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Commands: commands, Contents: contents}, logging.For(context.Background(), "wayland")) | |
| 54 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Commands: commands}, logging.For(context.Background(), "wayland")) | |
| 56 | 55 | if err != nil { | |
| 57 | 56 | t.Fatal(err) | |
| 58 | 57 | } | |
| 58 | + | contents := relayContents(t, s) | |
| 59 | 59 | ctx, cancel := context.WithCancel(context.Background()) | |
| 60 | 60 | done := make(chan error, 1) | |
| 61 | 61 | go func() { done <- s.Run(ctx) }() | |
| @@ -46,16 +46,16 @@ 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, ports.PresentationSink, 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 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Commands: commands}, 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")) | |||
| 56 | if err != nil { | 55 | if err != nil { | ||
| 57 | t.Fatal(err) | 56 | t.Fatal(err) | ||
| 58 | } | 57 | } | ||
| 58 | + | contents := relayContents(t, s) | |||
| 59 | ctx, cancel := context.WithCancel(context.Background()) | 59 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 60 | done := make(chan error, 1) | 60 | done := make(chan error, 1) | ||
| 61 | go func() { done <- s.Run(ctx) }() | 61 | go func() { done <- s.Run(ctx) }() | ||
M internal/adapters/wayland/release.go +0 -4
| @@ -47,9 +47,7 @@ func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer, sync syncHold) | |||
| 47 | 47 | if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok { | |
| 48 | 48 | h.id = d.buf.ID | |
| 49 | 49 | } | |
| 50 | - | s.contentMu.Lock() | |
| 51 | 50 | h.after = s.contentSeq[window] | |
| 52 | - | s.contentMu.Unlock() | |
| 53 | 51 | s.held = append(s.held, h) | |
| 54 | 52 | // Wake the pacer: it releases the buffer on the next output report. | |
| 55 | 53 | s.wakePacer() | |
| @@ -78,9 +76,7 @@ func (s *Server) releaseBufferSync(surf *surface, sync syncHold) { | |||
| 78 | 76 | s.releaseSync(sync) | |
| 79 | 77 | return | |
| 80 | 78 | } | |
| 81 | - | s.contentMu.Lock() | |
| 82 | 79 | after := s.contentSeq[window] | |
| 83 | - | s.contentMu.Unlock() | |
| 84 | 80 | s.held = append(s.held, heldBuffer{sync: sync, window: window, after: after, at: time.Now()}) | |
| 85 | 81 | s.wakePacer() | |
| 86 | 82 | } | |
| @@ -47,9 +47,7 @@ func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer, sync syncHold) | |||||
| 47 | if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok { | 47 | if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok { | ||
| 48 | h.id = d.buf.ID | 48 | h.id = d.buf.ID | ||
| 49 | } | 49 | } | ||
| 50 | - | s.contentMu.Lock() | |||
| 51 | h.after = s.contentSeq[window] | 50 | h.after = s.contentSeq[window] | ||
| 52 | - | s.contentMu.Unlock() | |||
| 53 | s.held = append(s.held, h) | 51 | s.held = append(s.held, h) | ||
| 54 | // Wake the pacer: it releases the buffer on the next output report. | 52 | // Wake the pacer: it releases the buffer on the next output report. | ||
| 55 | s.wakePacer() | 53 | s.wakePacer() | ||
| @@ -78,9 +76,7 @@ func (s *Server) releaseBufferSync(surf *surface, sync syncHold) { | |||||
| 78 | s.releaseSync(sync) | 76 | s.releaseSync(sync) | ||
| 79 | return | 77 | return | ||
| 80 | } | 78 | } | ||
| 81 | - | s.contentMu.Lock() | |||
| 82 | after := s.contentSeq[window] | 79 | after := s.contentSeq[window] | ||
| 83 | - | s.contentMu.Unlock() | |||
| 84 | s.held = append(s.held, heldBuffer{sync: sync, window: window, after: after, at: time.Now()}) | 80 | s.held = append(s.held, heldBuffer{sync: sync, window: window, after: after, at: time.Now()}) | ||
| 85 | s.wakePacer() | 81 | s.wakePacer() | ||
| 86 | } | 82 | } | ||
M internal/adapters/wayland/scale_test.go +4 -4
| @@ -52,23 +52,23 @@ func (p *surfaceTransformProxy) Dispatch(e *wlturbo.Event) { | |||
| 52 | 52 | } | |
| 53 | 53 | } | |
| 54 | 54 | ||
| 55 | - | func contentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, string) { | |
| 55 | + | func contentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, <-chan ports.SurfaceContent, string) { | |
| 56 | 56 | t.Helper() | |
| 57 | 57 | return contentServerWith(t, Options{}, logging.For(context.Background(), "wayland")) | |
| 58 | 58 | } | |
| 59 | 59 | ||
| 60 | 60 | // contentServerWith is contentServer with extra options and a logger. | |
| 61 | - | func contentServerWith(t *testing.T, opts Options, log zerowrap.Logger) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, string) { | |
| 61 | + | func contentServerWith(t *testing.T, opts Options, log zerowrap.Logger) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, <-chan ports.SurfaceContent, string) { | |
| 62 | 62 | t.Helper() | |
| 63 | 63 | dir := t.TempDir() | |
| 64 | 64 | events := make(chan ports.ClientEvent, 16) | |
| 65 | 65 | commands := make(chan ports.ClientCommand, 16) | |
| 66 | - | contents := make(chan ports.SurfaceContent, 16) | |
| 67 | 66 | opts.RuntimeDir, opts.Outputs = dir, testOutputs | |
| 68 | - | s, err := New(opts, Channels{Events: events, Commands: commands, Contents: contents}, log) | |
| 67 | + | s, err := New(opts, Channels{Events: events, Commands: commands}, log) | |
| 69 | 68 | if err != nil { | |
| 70 | 69 | t.Fatal(err) | |
| 71 | 70 | } | |
| 71 | + | contents := relayContents(t, s) | |
| 72 | 72 | ctx, cancel := context.WithCancel(context.Background()) | |
| 73 | 73 | done := make(chan error, 1) | |
| 74 | 74 | go func() { done <- s.Run(ctx) }() | |
| @@ -52,23 +52,23 @@ func (p *surfaceTransformProxy) Dispatch(e *wlturbo.Event) { | |||||
| 52 | } | 52 | } | ||
| 53 | } | 53 | } | ||
| 54 | 54 | ||||
| 55 | - | func contentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, string) { | 55 | + | func contentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, <-chan ports.SurfaceContent, string) { |
| 56 | t.Helper() | 56 | t.Helper() | ||
| 57 | return contentServerWith(t, Options{}, logging.For(context.Background(), "wayland")) | 57 | return contentServerWith(t, Options{}, logging.For(context.Background(), "wayland")) | ||
| 58 | } | 58 | } | ||
| 59 | 59 | ||||
| 60 | // contentServerWith is contentServer with extra options and a logger. | 60 | // contentServerWith is contentServer with extra options and a logger. | ||
| 61 | - | func contentServerWith(t *testing.T, opts Options, log zerowrap.Logger) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, string) { | 61 | + | func contentServerWith(t *testing.T, opts Options, log zerowrap.Logger) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, <-chan ports.SurfaceContent, string) { |
| 62 | t.Helper() | 62 | t.Helper() | ||
| 63 | dir := t.TempDir() | 63 | dir := t.TempDir() | ||
| 64 | events := make(chan ports.ClientEvent, 16) | 64 | events := make(chan ports.ClientEvent, 16) | ||
| 65 | commands := make(chan ports.ClientCommand, 16) | 65 | commands := make(chan ports.ClientCommand, 16) | ||
| 66 | - | contents := make(chan ports.SurfaceContent, 16) | |||
| 67 | opts.RuntimeDir, opts.Outputs = dir, testOutputs | 66 | opts.RuntimeDir, opts.Outputs = dir, testOutputs | ||
| 68 | - | s, err := New(opts, Channels{Events: events, Commands: commands, Contents: contents}, log) | 67 | + | s, err := New(opts, Channels{Events: events, Commands: commands}, log) |
| 69 | if err != nil { | 68 | if err != nil { | ||
| 70 | t.Fatal(err) | 69 | t.Fatal(err) | ||
| 71 | } | 70 | } | ||
| 71 | + | contents := relayContents(t, s) | |||
| 72 | ctx, cancel := context.WithCancel(context.Background()) | 72 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 73 | done := make(chan error, 1) | 73 | done := make(chan error, 1) | ||
| 74 | go func() { done <- s.Run(ctx) }() | 74 | go func() { done <- s.Run(ctx) }() | ||
M internal/adapters/wayland/server.go +21 -57
| @@ -66,7 +66,6 @@ type Channels struct { | |||
| 66 | 66 | LeaseRequests chan<- ports.LeaseMessage | |
| 67 | 67 | LeaseEvents <-chan ports.LeaseMessage | |
| 68 | 68 | Events chan<- ports.ClientEvent | |
| 69 | - | Contents chan<- ports.SurfaceContent | |
| 70 | 69 | Commands <-chan ports.ClientCommand | |
| 71 | 70 | Workspaces <-chan ports.Workspaces | |
| 72 | 71 | // Cursors receives the cursor the client under the pointer asks for. | |
| @@ -212,11 +211,16 @@ type Server struct { | |||
| 212 | 211 | eventMu sync.Mutex | |
| 213 | 212 | events []ports.ClientEvent | |
| 214 | 213 | eventReady chan struct{} | |
| 215 | - | contentMu sync.Mutex | |
| 216 | - | contents map[ports.WindowID]ports.SurfaceContent | |
| 214 | + | // contentSeq counts each window's contents and damage is its recent | |
| 215 | + | // damage; latest is its current content. All three belong to the display | |
| 216 | + | // goroutine, as do the outputs' content links (contentlink.go). | |
| 217 | 217 | contentSeq map[ports.WindowID]uint64 | |
| 218 | - | // damage is each window's recent damage (contentMu). | |
| 219 | - | damage map[ports.WindowID][]ports.SeqDamage | |
| 218 | + | damage map[ports.WindowID][]ports.SeqDamage | |
| 219 | + | latest map[ports.WindowID]ports.SurfaceContent | |
| 220 | + | links []*contentLink | |
| 221 | + | // flushPosted: a flushLinks is posted and has not started (Return). | |
| 222 | + | flushPosted atomic.Bool | |
| 223 | + | flushLinksFn func() | |
| 220 | 224 | // feedbacks wait for the flip that shows their content (presentation.go). | |
| 221 | 225 | feedbacks []feedbackWait | |
| 222 | 226 | // Explicit sync (syncobj.go): the render node, imported timelines by | |
| @@ -238,7 +242,6 @@ type Server struct { | |||
| 238 | 242 | clock ports.Clock | |
| 239 | 243 | powers []*outputPower | |
| 240 | 244 | outputsOff map[string]bool | |
| 241 | - | contentReady chan struct{} | |
| 242 | 245 | outputs []*output | |
| 243 | 246 | outputManagers []*outputManager | |
| 244 | 247 | workspaceManagers []*workspaceManager | |
| @@ -331,10 +334,9 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 331 | 334 | layers: make(map[ports.WindowID]*layerSurface), | |
| 332 | 335 | nextWindow: 1, | |
| 333 | 336 | eventReady: make(chan struct{}, 1), | |
| 334 | - | contents: make(map[ports.WindowID]ports.SurfaceContent), | |
| 335 | 337 | contentSeq: make(map[ports.WindowID]uint64), | |
| 336 | 338 | damage: map[ports.WindowID][]ports.SeqDamage{}, | |
| 337 | - | contentReady: make(chan struct{}, 1), | |
| 339 | + | latest: make(map[ports.WindowID]ports.SurfaceContent), | |
| 338 | 340 | cursorReady: make(chan struct{}, 1), | |
| 339 | 341 | dataSources: map[*server.Resource]*clipSource{}, | |
| 340 | 342 | primarySources: map[*server.Resource]*clipSource{}, | |
| @@ -346,6 +348,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 346 | 348 | positioners: map[*server.Resource]*positioner{}, | |
| 347 | 349 | seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), keyStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), pointerStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}, | |
| 348 | 350 | } | |
| 351 | + | s.flushLinksFn = s.flushLinks | |
| 349 | 352 | s.leaseDevices = map[string]*leaseDevice{} | |
| 350 | 353 | s.pendingLeases = map[uint64]*leaseObject{} | |
| 351 | 354 | s.pendingLeaseCards = map[uint64]string{} | |
| @@ -491,7 +494,7 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 491 | 494 | s.started = time.Now() | |
| 492 | 495 | s.ctx = ctx | |
| 493 | 496 | var wg sync.WaitGroup | |
| 494 | - | wg.Add(14) | |
| 497 | + | wg.Add(13) | |
| 495 | 498 | go func() { defer wg.Done(); s.forwardBackendSecurity(ctx) }() | |
| 496 | 499 | go func() { defer wg.Done(); s.forwardSecurityEvents(ctx) }() | |
| 497 | 500 | go func() { defer wg.Done(); s.forwardLeases(ctx) }() | |
| @@ -499,7 +502,6 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 499 | 502 | go func() { defer wg.Done(); s.forwardWorkspaces(ctx) }() | |
| 500 | 503 | go func() { defer wg.Done(); s.forward(ctx) }() | |
| 501 | 504 | go func() { defer wg.Done(); s.forwardCursors(ctx) }() | |
| 502 | - | go func() { defer wg.Done(); s.forwardContents(ctx) }() | |
| 503 | 505 | go func() { | |
| 504 | 506 | defer wg.Done() | |
| 505 | 507 | for { | |
| @@ -665,11 +667,10 @@ func (s *Server) forward(ctx context.Context) { | |||
| 665 | 667 | } | |
| 666 | 668 | } | |
| 667 | 669 | ||
| 670 | + | // emitContent publishes a window's content on the display goroutine: it | |
| 671 | + | // becomes the window's latest content and joins the batch of every attached | |
| 672 | + | // output (contentlink.go). | |
| 668 | 673 | func (s *Server) emitContent(c ports.SurfaceContent, d damage) { | |
| 669 | - | if s.channels.Contents == nil { | |
| 670 | - | return | |
| 671 | - | } | |
| 672 | - | s.contentMu.Lock() | |
| 673 | 674 | s.contentSeq[c.ID]++ | |
| 674 | 675 | c.Seq = s.contentSeq[c.ID] | |
| 675 | 676 | // Published histories are immutable: an output may hold one for as long | |
| @@ -689,54 +690,17 @@ func (s *Server) emitContent(c ports.SurfaceContent, d damage) { | |||
| 689 | 690 | h := append(old, ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects}) | |
| 690 | 691 | if c.Empty() { | |
| 691 | 692 | delete(s.damage, c.ID) | |
| 693 | + | delete(s.latest, c.ID) | |
| 692 | 694 | } else { | |
| 693 | 695 | s.damage[c.ID] = h | |
| 694 | 696 | } | |
| 695 | 697 | c.DamageHistory = h[:len(h):len(h)] | |
| 696 | - | s.contents[c.ID] = c | |
| 697 | - | s.contentMu.Unlock() | |
| 698 | - | select { | |
| 699 | - | case s.contentReady <- struct{}{}: | |
| 700 | - | default: | |
| 698 | + | if !c.Empty() { | |
| 699 | + | s.latest[c.ID] = c | |
| 701 | 700 | } | |
| 702 | - | } | |
| 703 | - | ||
| 704 | - | func (s *Server) forwardContents(ctx context.Context) { | |
| 705 | - | for { | |
| 706 | - | s.contentMu.Lock() | |
| 707 | - | var id ports.WindowID | |
| 708 | - | var c ports.SurfaceContent | |
| 709 | - | found := false | |
| 710 | - | var seq uint64 | |
| 711 | - | for id, c = range s.contents { | |
| 712 | - | found = true | |
| 713 | - | seq = s.contentSeq[id] | |
| 714 | - | break | |
| 715 | - | } | |
| 716 | - | s.contentMu.Unlock() | |
| 717 | - | if !found { | |
| 718 | - | select { | |
| 719 | - | case <-ctx.Done(): | |
| 720 | - | return | |
| 721 | - | case <-s.contentReady: | |
| 722 | - | continue | |
| 723 | - | } | |
| 724 | - | } | |
| 725 | - | // Do not hold a stale frame while the receiver is busy: wake on newer | |
| 726 | - | // data. contentReady is signalled after every publication; a stale | |
| 727 | - | // signal only costs one more loop. | |
| 728 | - | select { | |
| 729 | - | case <-ctx.Done(): | |
| 730 | - | return | |
| 731 | - | case <-s.contentReady: | |
| 732 | - | continue | |
| 733 | - | case s.channels.Contents <- c: | |
| 734 | - | s.contentMu.Lock() | |
| 735 | - | if s.contentSeq[id] == seq { | |
| 736 | - | delete(s.contents, id) | |
| 737 | - | } | |
| 738 | - | s.contentMu.Unlock() | |
| 739 | - | } | |
| 701 | + | for _, l := range s.links { | |
| 702 | + | l.add(c) | |
| 703 | + | s.flushLink(l) | |
| 740 | 704 | } | |
| 741 | 705 | } | |
| 742 | 706 | ||
| @@ -66,7 +66,6 @@ type Channels struct { | |||||
| 66 | LeaseRequests chan<- ports.LeaseMessage | 66 | LeaseRequests chan<- ports.LeaseMessage | ||
| 67 | LeaseEvents <-chan ports.LeaseMessage | 67 | LeaseEvents <-chan ports.LeaseMessage | ||
| 68 | Events chan<- ports.ClientEvent | 68 | Events chan<- ports.ClientEvent | ||
| 69 | - | Contents chan<- ports.SurfaceContent | |||
| 70 | Commands <-chan ports.ClientCommand | 69 | Commands <-chan ports.ClientCommand | ||
| 71 | Workspaces <-chan ports.Workspaces | 70 | Workspaces <-chan ports.Workspaces | ||
| 72 | // Cursors receives the cursor the client under the pointer asks for. | 71 | // Cursors receives the cursor the client under the pointer asks for. | ||
| @@ -212,11 +211,16 @@ type Server struct { | |||||
| 212 | eventMu sync.Mutex | 211 | eventMu sync.Mutex | ||
| 213 | events []ports.ClientEvent | 212 | events []ports.ClientEvent | ||
| 214 | eventReady chan struct{} | 213 | eventReady chan struct{} | ||
| 215 | - | contentMu sync.Mutex | 214 | + | // contentSeq counts each window's contents and damage is its recent |
| 216 | - | contents map[ports.WindowID]ports.SurfaceContent | 215 | + | // damage; latest is its current content. All three belong to the display |
| 216 | + | // goroutine, as do the outputs' content links (contentlink.go). | |||
| 217 | contentSeq map[ports.WindowID]uint64 | 217 | contentSeq map[ports.WindowID]uint64 | ||
| 218 | - | // damage is each window's recent damage (contentMu). | 218 | + | damage map[ports.WindowID][]ports.SeqDamage |
| 219 | - | damage map[ports.WindowID][]ports.SeqDamage | 219 | + | latest map[ports.WindowID]ports.SurfaceContent |
| 220 | + | links []*contentLink | |||
| 221 | + | // flushPosted: a flushLinks is posted and has not started (Return). | |||
| 222 | + | flushPosted atomic.Bool | |||
| 223 | + | flushLinksFn func() | |||
| 220 | // feedbacks wait for the flip that shows their content (presentation.go). | 224 | // feedbacks wait for the flip that shows their content (presentation.go). | ||
| 221 | feedbacks []feedbackWait | 225 | feedbacks []feedbackWait | ||
| 222 | // Explicit sync (syncobj.go): the render node, imported timelines by | 226 | // Explicit sync (syncobj.go): the render node, imported timelines by | ||
| @@ -238,7 +242,6 @@ type Server struct { | |||||
| 238 | clock ports.Clock | 242 | clock ports.Clock | ||
| 239 | powers []*outputPower | 243 | powers []*outputPower | ||
| 240 | outputsOff map[string]bool | 244 | outputsOff map[string]bool | ||
| 241 | - | contentReady chan struct{} | |||
| 242 | outputs []*output | 245 | outputs []*output | ||
| 243 | outputManagers []*outputManager | 246 | outputManagers []*outputManager | ||
| 244 | workspaceManagers []*workspaceManager | 247 | workspaceManagers []*workspaceManager | ||
| @@ -331,10 +334,9 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 331 | layers: make(map[ports.WindowID]*layerSurface), | 334 | layers: make(map[ports.WindowID]*layerSurface), | ||
| 332 | nextWindow: 1, | 335 | nextWindow: 1, | ||
| 333 | eventReady: make(chan struct{}, 1), | 336 | eventReady: make(chan struct{}, 1), | ||
| 334 | - | contents: make(map[ports.WindowID]ports.SurfaceContent), | |||
| 335 | contentSeq: make(map[ports.WindowID]uint64), | 337 | contentSeq: make(map[ports.WindowID]uint64), | ||
| 336 | damage: map[ports.WindowID][]ports.SeqDamage{}, | 338 | damage: map[ports.WindowID][]ports.SeqDamage{}, | ||
| 337 | - | contentReady: make(chan struct{}, 1), | 339 | + | latest: make(map[ports.WindowID]ports.SurfaceContent), |
| 338 | cursorReady: make(chan struct{}, 1), | 340 | cursorReady: make(chan struct{}, 1), | ||
| 339 | dataSources: map[*server.Resource]*clipSource{}, | 341 | dataSources: map[*server.Resource]*clipSource{}, | ||
| 340 | primarySources: map[*server.Resource]*clipSource{}, | 342 | primarySources: map[*server.Resource]*clipSource{}, | ||
| @@ -346,6 +348,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 346 | positioners: map[*server.Resource]*positioner{}, | 348 | positioners: map[*server.Resource]*positioner{}, | ||
| 347 | seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), keyStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), pointerStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}, | 349 | seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), keyStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), pointerStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}, | ||
| 348 | } | 350 | } | ||
| 351 | + | s.flushLinksFn = s.flushLinks | |||
| 349 | s.leaseDevices = map[string]*leaseDevice{} | 352 | s.leaseDevices = map[string]*leaseDevice{} | ||
| 350 | s.pendingLeases = map[uint64]*leaseObject{} | 353 | s.pendingLeases = map[uint64]*leaseObject{} | ||
| 351 | s.pendingLeaseCards = map[uint64]string{} | 354 | s.pendingLeaseCards = map[uint64]string{} | ||
| @@ -491,7 +494,7 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 491 | s.started = time.Now() | 494 | s.started = time.Now() | ||
| 492 | s.ctx = ctx | 495 | s.ctx = ctx | ||
| 493 | var wg sync.WaitGroup | 496 | var wg sync.WaitGroup | ||
| 494 | - | wg.Add(14) | 497 | + | wg.Add(13) |
| 495 | go func() { defer wg.Done(); s.forwardBackendSecurity(ctx) }() | 498 | go func() { defer wg.Done(); s.forwardBackendSecurity(ctx) }() | ||
| 496 | go func() { defer wg.Done(); s.forwardSecurityEvents(ctx) }() | 499 | go func() { defer wg.Done(); s.forwardSecurityEvents(ctx) }() | ||
| 497 | go func() { defer wg.Done(); s.forwardLeases(ctx) }() | 500 | go func() { defer wg.Done(); s.forwardLeases(ctx) }() | ||
| @@ -499,7 +502,6 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 499 | go func() { defer wg.Done(); s.forwardWorkspaces(ctx) }() | 502 | go func() { defer wg.Done(); s.forwardWorkspaces(ctx) }() | ||
| 500 | go func() { defer wg.Done(); s.forward(ctx) }() | 503 | go func() { defer wg.Done(); s.forward(ctx) }() | ||
| 501 | go func() { defer wg.Done(); s.forwardCursors(ctx) }() | 504 | go func() { defer wg.Done(); s.forwardCursors(ctx) }() | ||
| 502 | - | go func() { defer wg.Done(); s.forwardContents(ctx) }() | |||
| 503 | go func() { | 505 | go func() { | ||
| 504 | defer wg.Done() | 506 | defer wg.Done() | ||
| 505 | for { | 507 | for { | ||
| @@ -665,11 +667,10 @@ func (s *Server) forward(ctx context.Context) { | |||||
| 665 | } | 667 | } | ||
| 666 | } | 668 | } | ||
| 667 | 669 | ||||
| 670 | + | // emitContent publishes a window's content on the display goroutine: it | |||
| 671 | + | // becomes the window's latest content and joins the batch of every attached | |||
| 672 | + | // output (contentlink.go). | |||
| 668 | func (s *Server) emitContent(c ports.SurfaceContent, d damage) { | 673 | func (s *Server) emitContent(c ports.SurfaceContent, d damage) { | ||
| 669 | - | if s.channels.Contents == nil { | |||
| 670 | - | return | |||
| 671 | - | } | |||
| 672 | - | s.contentMu.Lock() | |||
| 673 | s.contentSeq[c.ID]++ | 674 | s.contentSeq[c.ID]++ | ||
| 674 | c.Seq = s.contentSeq[c.ID] | 675 | c.Seq = s.contentSeq[c.ID] | ||
| 675 | // Published histories are immutable: an output may hold one for as long | 676 | // Published histories are immutable: an output may hold one for as long | ||
| @@ -689,54 +690,17 @@ func (s *Server) emitContent(c ports.SurfaceContent, d damage) { | |||||
| 689 | h := append(old, ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects}) | 690 | h := append(old, ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects}) | ||
| 690 | if c.Empty() { | 691 | if c.Empty() { | ||
| 691 | delete(s.damage, c.ID) | 692 | delete(s.damage, c.ID) | ||
| 693 | + | delete(s.latest, c.ID) | |||
| 692 | } else { | 694 | } else { | ||
| 693 | s.damage[c.ID] = h | 695 | s.damage[c.ID] = h | ||
| 694 | } | 696 | } | ||
| 695 | c.DamageHistory = h[:len(h):len(h)] | 697 | c.DamageHistory = h[:len(h):len(h)] | ||
| 696 | - | s.contents[c.ID] = c | 698 | + | if !c.Empty() { |
| 697 | - | s.contentMu.Unlock() | 699 | + | s.latest[c.ID] = c |
| 698 | - | select { | |||
| 699 | - | case s.contentReady <- struct{}{}: | |||
| 700 | - | default: | |||
| 701 | } | 700 | } | ||
| 702 | - | } | 701 | + | for _, l := range s.links { |
| 703 | - | 702 | + | l.add(c) | |
| 704 | - | func (s *Server) forwardContents(ctx context.Context) { | 703 | + | s.flushLink(l) |
| 705 | - | for { | |||
| 706 | - | s.contentMu.Lock() | |||
| 707 | - | var id ports.WindowID | |||
| 708 | - | var c ports.SurfaceContent | |||
| 709 | - | found := false | |||
| 710 | - | var seq uint64 | |||
| 711 | - | for id, c = range s.contents { | |||
| 712 | - | found = true | |||
| 713 | - | seq = s.contentSeq[id] | |||
| 714 | - | break | |||
| 715 | - | } | |||
| 716 | - | s.contentMu.Unlock() | |||
| 717 | - | if !found { | |||
| 718 | - | select { | |||
| 719 | - | case <-ctx.Done(): | |||
| 720 | - | return | |||
| 721 | - | case <-s.contentReady: | |||
| 722 | - | continue | |||
| 723 | - | } | |||
| 724 | - | } | |||
| 725 | - | // Do not hold a stale frame while the receiver is busy: wake on newer | |||
| 726 | - | // data. contentReady is signalled after every publication; a stale | |||
| 727 | - | // signal only costs one more loop. | |||
| 728 | - | select { | |||
| 729 | - | case <-ctx.Done(): | |||
| 730 | - | return | |||
| 731 | - | case <-s.contentReady: | |||
| 732 | - | continue | |||
| 733 | - | case s.channels.Contents <- c: | |||
| 734 | - | s.contentMu.Lock() | |||
| 735 | - | if s.contentSeq[id] == seq { | |||
| 736 | - | delete(s.contents, id) | |||
| 737 | - | } | |||
| 738 | - | s.contentMu.Unlock() | |||
| 739 | - | } | |||
| 740 | } | 704 | } | ||
| 741 | } | 705 | } | ||
| 742 | 706 | ||||
M internal/adapters/wayland/server_test.go +2 -2
| @@ -374,11 +374,11 @@ func TestWestonContents(t *testing.T) { | |||
| 374 | 374 | } | |
| 375 | 375 | dir := t.TempDir() | |
| 376 | 376 | events := make(chan ports.ClientEvent, 16) | |
| 377 | - | contents := make(chan ports.SurfaceContent, 16) | |
| 378 | - | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Contents: contents}, logging.For(context.Background(), "wayland")) | |
| 377 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events}, logging.For(context.Background(), "wayland")) | |
| 379 | 378 | if err != nil { | |
| 380 | 379 | t.Fatal(err) | |
| 381 | 380 | } | |
| 381 | + | contents := relayContents(t, s) | |
| 382 | 382 | ctx, cancel := context.WithCancel(context.Background()) | |
| 383 | 383 | done := make(chan error, 1) | |
| 384 | 384 | go func() { done <- s.Run(ctx) }() | |
| @@ -374,11 +374,11 @@ func TestWestonContents(t *testing.T) { | |||||
| 374 | } | 374 | } | ||
| 375 | dir := t.TempDir() | 375 | dir := t.TempDir() | ||
| 376 | events := make(chan ports.ClientEvent, 16) | 376 | events := make(chan ports.ClientEvent, 16) | ||
| 377 | - | contents := make(chan ports.SurfaceContent, 16) | 377 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events}, logging.For(context.Background(), "wayland")) |
| 378 | - | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Contents: contents}, logging.For(context.Background(), "wayland")) | |||
| 379 | if err != nil { | 378 | if err != nil { | ||
| 380 | t.Fatal(err) | 379 | t.Fatal(err) | ||
| 381 | } | 380 | } | ||
| 381 | + | contents := relayContents(t, s) | |||
| 382 | ctx, cancel := context.WithCancel(context.Background()) | 382 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 383 | done := make(chan error, 1) | 383 | done := make(chan error, 1) | ||
| 384 | go func() { done <- s.Run(ctx) }() | 384 | go func() { done <- s.Run(ctx) }() | ||
M internal/adapters/wayland/surface.go +1 -1
| @@ -264,7 +264,7 @@ func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) { | |||
| 264 | 264 | // for later) are not drawn, so they do not make it full. | |
| 265 | 265 | func (s *surface) redraw() { | |
| 266 | 266 | r := s.root() | |
| 267 | - | if id := r.windowID(); id != 0 && s.server.channels.Contents != nil { | |
| 267 | + | if id := r.windowID(); id != 0 { | |
| 268 | 268 | c := r.tree(id) | |
| 269 | 269 | d := damage{full: true} | |
| 270 | 270 | if s == r && len(c.Children) == 0 && !r.drewChildren { | |
| @@ -264,7 +264,7 @@ func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) { | |||||
| 264 | // for later) are not drawn, so they do not make it full. | 264 | // for later) are not drawn, so they do not make it full. | ||
| 265 | func (s *surface) redraw() { | 265 | func (s *surface) redraw() { | ||
| 266 | r := s.root() | 266 | r := s.root() | ||
| 267 | - | if id := r.windowID(); id != 0 && s.server.channels.Contents != nil { | 267 | + | if id := r.windowID(); id != 0 { |
| 268 | c := r.tree(id) | 268 | c := r.tree(id) | ||
| 269 | d := damage{full: true} | 269 | d := damage{full: true} | ||
| 270 | if s == r && len(c.Children) == 0 && !r.drewChildren { | 270 | if s == r && len(c.Children) == 0 && !r.drewChildren { | ||
M internal/adapters/wayland/surface_perf_test.go +4 -4
| @@ -48,7 +48,7 @@ func TestTiledCommitPublishAllocations(t *testing.T) { | |||
| 48 | 48 | }) | |
| 49 | 49 | for _, count := range []int{16, 64} { | |
| 50 | 50 | t.Run(fmt.Sprintf("%d", count), func(t *testing.T) { | |
| 51 | - | channels := Channels{Contents: make(chan ports.SurfaceContent, 1)} | |
| 51 | + | channels := Channels{} | |
| 52 | 52 | srv, err := New(Options{RuntimeDir: t.TempDir(), Outputs: testOutputs}, channels, logging.For(context.Background(), "wayland")) | |
| 53 | 53 | if err != nil { | |
| 54 | 54 | t.Fatal(err) | |
| @@ -64,7 +64,7 @@ func TestTiledCommitPublishAllocations(t *testing.T) { | |||
| 64 | 64 | publish := func() { root.redraw() } | |
| 65 | 65 | root.treeDirty = true | |
| 66 | 66 | publish() | |
| 67 | - | first := srv.contents[1] | |
| 67 | + | first := srv.latest[1] | |
| 68 | 68 | publish() | |
| 69 | 69 | if first.Seq != 1 || len(first.Children) != count || len(first.DamageHistory) != 1 { | |
| 70 | 70 | t.Fatalf("previous publication changed: %+v", first) | |
| @@ -99,10 +99,10 @@ func TestTiledCommitPublishAllocations(t *testing.T) { | |||
| 99 | 99 | root.sub.children[count-1].version++ | |
| 100 | 100 | root.treeDirty = true | |
| 101 | 101 | publish() | |
| 102 | - | if first.Children[count-1].Version != 0 || srv.contents[1].Children[count-1].Version != 1 { | |
| 102 | + | if first.Children[count-1].Version != 0 || srv.latest[1].Children[count-1].Version != 1 { | |
| 103 | 103 | t.Fatal("child version leaked into an earlier publication") | |
| 104 | 104 | } | |
| 105 | - | if &first.Children[0] == &srv.contents[1].Children[0] { | |
| 105 | + | if &first.Children[0] == &srv.latest[1].Children[0] { | |
| 106 | 106 | t.Fatal("rebuild reused previously published children storage") | |
| 107 | 107 | } | |
| 108 | 108 | // Rebuilding a changed child's immutable snapshot must have a | |
| @@ -48,7 +48,7 @@ func TestTiledCommitPublishAllocations(t *testing.T) { | |||||
| 48 | }) | 48 | }) | ||
| 49 | for _, count := range []int{16, 64} { | 49 | for _, count := range []int{16, 64} { | ||
| 50 | t.Run(fmt.Sprintf("%d", count), func(t *testing.T) { | 50 | t.Run(fmt.Sprintf("%d", count), func(t *testing.T) { | ||
| 51 | - | channels := Channels{Contents: make(chan ports.SurfaceContent, 1)} | 51 | + | channels := Channels{} |
| 52 | srv, err := New(Options{RuntimeDir: t.TempDir(), Outputs: testOutputs}, channels, logging.For(context.Background(), "wayland")) | 52 | srv, err := New(Options{RuntimeDir: t.TempDir(), Outputs: testOutputs}, channels, logging.For(context.Background(), "wayland")) | ||
| 53 | if err != nil { | 53 | if err != nil { | ||
| 54 | t.Fatal(err) | 54 | t.Fatal(err) | ||
| @@ -64,7 +64,7 @@ func TestTiledCommitPublishAllocations(t *testing.T) { | |||||
| 64 | publish := func() { root.redraw() } | 64 | publish := func() { root.redraw() } | ||
| 65 | root.treeDirty = true | 65 | root.treeDirty = true | ||
| 66 | publish() | 66 | publish() | ||
| 67 | - | first := srv.contents[1] | 67 | + | first := srv.latest[1] |
| 68 | publish() | 68 | publish() | ||
| 69 | if first.Seq != 1 || len(first.Children) != count || len(first.DamageHistory) != 1 { | 69 | if first.Seq != 1 || len(first.Children) != count || len(first.DamageHistory) != 1 { | ||
| 70 | t.Fatalf("previous publication changed: %+v", first) | 70 | t.Fatalf("previous publication changed: %+v", first) | ||
| @@ -99,10 +99,10 @@ func TestTiledCommitPublishAllocations(t *testing.T) { | |||||
| 99 | root.sub.children[count-1].version++ | 99 | root.sub.children[count-1].version++ | ||
| 100 | root.treeDirty = true | 100 | root.treeDirty = true | ||
| 101 | publish() | 101 | publish() | ||
| 102 | - | if first.Children[count-1].Version != 0 || srv.contents[1].Children[count-1].Version != 1 { | 102 | + | if first.Children[count-1].Version != 0 || srv.latest[1].Children[count-1].Version != 1 { |
| 103 | t.Fatal("child version leaked into an earlier publication") | 103 | t.Fatal("child version leaked into an earlier publication") | ||
| 104 | } | 104 | } | ||
| 105 | - | if &first.Children[0] == &srv.contents[1].Children[0] { | 105 | + | if &first.Children[0] == &srv.latest[1].Children[0] { |
| 106 | t.Fatal("rebuild reused previously published children storage") | 106 | t.Fatal("rebuild reused previously published children storage") | ||
| 107 | } | 107 | } | ||
| 108 | // Rebuilding a changed child's immutable snapshot must have a | 108 | // Rebuilding a changed child's immutable snapshot must have a | ||
M internal/adapters/wayland/syncobj_test.go +3 -3
| @@ -48,7 +48,7 @@ type syncHarness struct { | |||
| 48 | 48 | s *Server | |
| 49 | 49 | c *wlturbo.Display | |
| 50 | 50 | dev *mocksyncobjDevice | |
| 51 | - | contents chan ports.SurfaceContent | |
| 51 | + | contents <-chan ports.SurfaceContent | |
| 52 | 52 | presented ports.PresentationSink | |
| 53 | 53 | surf uint32 | |
| 54 | 54 | syncSurf uint32 | |
| @@ -99,12 +99,12 @@ func newSyncHarness(t *testing.T) *syncHarness { | |||
| 99 | 99 | dir := t.TempDir() | |
| 100 | 100 | events := make(chan ports.ClientEvent, 16) | |
| 101 | 101 | commands := make(chan ports.ClientCommand, 16) | |
| 102 | - | h.contents = make(chan ports.SurfaceContent, 64) | |
| 103 | 102 | sup := ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}} | |
| 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")) | |
| 103 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup, syncDev: h.dev}, Channels{Events: events, Commands: commands}, logging.For(context.Background(), "wayland")) | |
| 105 | 104 | if err != nil { | |
| 106 | 105 | t.Fatal(err) | |
| 107 | 106 | } | |
| 107 | + | h.contents = relayContents(t, s) | |
| 108 | 108 | ctx, cancel := context.WithCancel(context.Background()) | |
| 109 | 109 | done := make(chan error, 1) | |
| 110 | 110 | go func() { done <- s.Run(ctx) }() | |
| @@ -48,7 +48,7 @@ type syncHarness struct { | |||||
| 48 | s *Server | 48 | s *Server | ||
| 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 ports.PresentationSink | 52 | presented ports.PresentationSink | ||
| 53 | surf uint32 | 53 | surf uint32 | ||
| 54 | syncSurf uint32 | 54 | syncSurf uint32 | ||
| @@ -99,12 +99,12 @@ func newSyncHarness(t *testing.T) *syncHarness { | |||||
| 99 | dir := t.TempDir() | 99 | dir := t.TempDir() | ||
| 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) | |||
| 103 | sup := ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}} | 102 | sup := ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}} | ||
| 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")) | 103 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup, syncDev: h.dev}, Channels{Events: events, Commands: commands}, logging.For(context.Background(), "wayland")) |
| 105 | if err != nil { | 104 | if err != nil { | ||
| 106 | t.Fatal(err) | 105 | t.Fatal(err) | ||
| 107 | } | 106 | } | ||
| 107 | + | h.contents = relayContents(t, s) | |||
| 108 | ctx, cancel := context.WithCancel(context.Background()) | 108 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 109 | done := make(chan error, 1) | 109 | done := make(chan error, 1) | ||
| 110 | go func() { done <- s.Run(ctx) }() | 110 | go func() { done <- s.Run(ctx) }() | ||
M internal/adapters/wayland/transform_test.go +1 -1
| @@ -17,7 +17,7 @@ import ( | |||
| 17 | 17 | type transformClient struct { | |
| 18 | 18 | c *wlturbo.Display | |
| 19 | 19 | comp, surf, vp, bf uint32 | |
| 20 | - | contents chan ports.SurfaceContent | |
| 20 | + | contents <-chan ports.SurfaceContent | |
| 21 | 21 | } | |
| 22 | 22 | ||
| 23 | 23 | func newTransformClient(t *testing.T) transformClient { | |
| @@ -17,7 +17,7 @@ import ( | |||||
| 17 | type transformClient struct { | 17 | type transformClient struct { | ||
| 18 | c *wlturbo.Display | 18 | c *wlturbo.Display | ||
| 19 | comp, surf, vp, bf uint32 | 19 | comp, surf, vp, bf uint32 | ||
| 20 | - | contents chan ports.SurfaceContent | 20 | + | contents <-chan ports.SurfaceContent |
| 21 | } | 21 | } | ||
| 22 | 22 | ||||
| 23 | func newTransformClient(t *testing.T) transformClient { | 23 | func newTransformClient(t *testing.T) transformClient { | ||
M internal/app/capture_test.go +1 -1
| @@ -144,7 +144,7 @@ func TestOutputStopWithFullReplies(t *testing.T) { | |||
| 144 | 144 | replies := make(chan ports.CaptureDone, 1) | |
| 145 | 145 | replies <- ports.CaptureDone{} | |
| 146 | 146 | set := newOutputSet(ctx, replies) | |
| 147 | - | set.start(ctx, "test", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, requests <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 147 | + | set.start(ctx, "test", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, requests <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 148 | 148 | <-ctx.Done() | |
| 149 | 149 | return nil | |
| 150 | 150 | }) | |
| @@ -144,7 +144,7 @@ func TestOutputStopWithFullReplies(t *testing.T) { | |||||
| 144 | replies := make(chan ports.CaptureDone, 1) | 144 | replies := make(chan ports.CaptureDone, 1) | ||
| 145 | replies <- ports.CaptureDone{} | 145 | replies <- ports.CaptureDone{} | ||
| 146 | set := newOutputSet(ctx, replies) | 146 | set := newOutputSet(ctx, replies) | ||
| 147 | - | set.start(ctx, "test", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, requests <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | 147 | + | set.start(ctx, "test", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, requests <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { |
| 148 | <-ctx.Done() | 148 | <-ctx.Done() | ||
| 149 | return nil | 149 | return nil | ||
| 150 | }) | 150 | }) | ||
A internal/app/contentattach.go +15 -0
| @@ -0,0 +1,15 @@ | |||
| 1 | + | package app | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "github.com/bnema/neferwl/internal/adapters/wayland" | |
| 5 | + | "github.com/bnema/neferwl/internal/ports" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | // waylandContent is the contentAttacher of a wayland server. | |
| 9 | + | type waylandContent struct{ server *wayland.Server } | |
| 10 | + | ||
| 11 | + | func (w waylandContent) Attach(name string) (<-chan *ports.ContentBatch, ports.ContentReturn) { | |
| 12 | + | return w.server.AttachOutput(name) | |
| 13 | + | } | |
| 14 | + | ||
| 15 | + | func (w waylandContent) Detach(name string) { w.server.DetachOutput(name) } | |
| @@ -0,0 +1,15 @@ | |||||
| 1 | + | package app | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "github.com/bnema/neferwl/internal/adapters/wayland" | |||
| 5 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | // waylandContent is the contentAttacher of a wayland server. | |||
| 9 | + | type waylandContent struct{ server *wayland.Server } | |||
| 10 | + | ||||
| 11 | + | func (w waylandContent) Attach(name string) (<-chan *ports.ContentBatch, ports.ContentReturn) { | |||
| 12 | + | return w.server.AttachOutput(name) | |||
| 13 | + | } | |||
| 14 | + | ||||
| 15 | + | func (w waylandContent) Detach(name string) { w.server.DetachOutput(name) } | |||
A internal/app/contentattach_test.go +87 -0
| @@ -0,0 +1,87 @@ | |||
| 1 | + | package app | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // Wayland feeds a started output its contents: Attach comes after the | |
| 12 | + | // output registered and got its security state, the output runs on what | |
| 13 | + | // Attach returned, Detach follows its end, and a restarted connector attaches | |
| 14 | + | // again. | |
| 15 | + | func TestOutputSetAttachesAndDetachesContents(t *testing.T) { | |
| 16 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 17 | + | defer cancel() | |
| 18 | + | set := newOutputSet(ctx, nil) | |
| 19 | + | events := make(chan ports.SecurityBackendEvent, 4) | |
| 20 | + | set.wireSecurity(outputChannels{securityEvents: events}) | |
| 21 | + | attacher := newMockcontentAttacher(t) | |
| 22 | + | set.attach = attacher | |
| 23 | + | ret := portsmocks.NewMockContentReturn(t) | |
| 24 | + | feed := make(chan *ports.ContentBatch, 1) | |
| 25 | + | var order []string | |
| 26 | + | type seen struct { | |
| 27 | + | contents <-chan *ports.ContentBatch | |
| 28 | + | returns ports.ContentReturn | |
| 29 | + | security ports.SecurityState | |
| 30 | + | } | |
| 31 | + | started := make(chan seen, 2) | |
| 32 | + | run := func(ctx context.Context, _ <-chan ports.Scene, contents <-chan *ports.ContentBatch, returns ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, security <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 33 | + | started <- seen{contents, returns, <-security} | |
| 34 | + | <-ctx.Done() | |
| 35 | + | return nil | |
| 36 | + | } | |
| 37 | + | attacher.EXPECT().Attach("DP-1").RunAndReturn(func(string) (<-chan *ports.ContentBatch, ports.ContentReturn) { | |
| 38 | + | order = append(order, "attach") | |
| 39 | + | // The output is registered with the security backend first. | |
| 40 | + | if len(events) != 1 { | |
| 41 | + | t.Errorf("attached with %d registrations queued, want 1", len(events)) | |
| 42 | + | } | |
| 43 | + | return feed, ret | |
| 44 | + | }).Times(2) | |
| 45 | + | attacher.EXPECT().Detach("DP-1").Run(func(string) { order = append(order, "detach") }).Times(2) | |
| 46 | + | for round := range 2 { | |
| 47 | + | if !set.start(ctx, "DP-1", run) { | |
| 48 | + | t.Fatal("start failed") | |
| 49 | + | } | |
| 50 | + | got := <-started | |
| 51 | + | if got.contents != (<-chan *ports.ContentBatch)(feed) || got.returns != ports.ContentReturn(ret) { | |
| 52 | + | t.Fatalf("round %d: output got another feed", round) | |
| 53 | + | } | |
| 54 | + | <-events | |
| 55 | + | set.outs["DP-1"].stop() | |
| 56 | + | if err := set.finish("DP-1"); err != nil { | |
| 57 | + | t.Fatal(err) | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | want := []string{"attach", "detach", "attach", "detach"} | |
| 61 | + | if len(order) != len(want) { | |
| 62 | + | t.Fatalf("order %v, want %v", order, want) | |
| 63 | + | } | |
| 64 | + | for i := range want { | |
| 65 | + | if order[i] != want[i] { | |
| 66 | + | t.Fatalf("order %v, want %v", order, want) | |
| 67 | + | } | |
| 68 | + | } | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | // wait detaches the outputs it stops. | |
| 72 | + | func TestOutputSetWaitDetachesContents(t *testing.T) { | |
| 73 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 74 | + | defer cancel() | |
| 75 | + | set := newOutputSet(ctx, nil) | |
| 76 | + | attacher := newMockcontentAttacher(t) | |
| 77 | + | set.attach = attacher | |
| 78 | + | attacher.EXPECT().Attach("A").Return(nil, nil).Once() | |
| 79 | + | attacher.EXPECT().Detach("A").Return().Once() | |
| 80 | + | set.start(ctx, "A", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 81 | + | <-ctx.Done() | |
| 82 | + | return nil | |
| 83 | + | }) | |
| 84 | + | if err := set.wait(); err != nil { | |
| 85 | + | t.Fatal(err) | |
| 86 | + | } | |
| 87 | + | } | |
| @@ -0,0 +1,87 @@ | |||||
| 1 | + | package app | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // Wayland feeds a started output its contents: Attach comes after the | |||
| 12 | + | // output registered and got its security state, the output runs on what | |||
| 13 | + | // Attach returned, Detach follows its end, and a restarted connector attaches | |||
| 14 | + | // again. | |||
| 15 | + | func TestOutputSetAttachesAndDetachesContents(t *testing.T) { | |||
| 16 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 17 | + | defer cancel() | |||
| 18 | + | set := newOutputSet(ctx, nil) | |||
| 19 | + | events := make(chan ports.SecurityBackendEvent, 4) | |||
| 20 | + | set.wireSecurity(outputChannels{securityEvents: events}) | |||
| 21 | + | attacher := newMockcontentAttacher(t) | |||
| 22 | + | set.attach = attacher | |||
| 23 | + | ret := portsmocks.NewMockContentReturn(t) | |||
| 24 | + | feed := make(chan *ports.ContentBatch, 1) | |||
| 25 | + | var order []string | |||
| 26 | + | type seen struct { | |||
| 27 | + | contents <-chan *ports.ContentBatch | |||
| 28 | + | returns ports.ContentReturn | |||
| 29 | + | security ports.SecurityState | |||
| 30 | + | } | |||
| 31 | + | started := make(chan seen, 2) | |||
| 32 | + | run := func(ctx context.Context, _ <-chan ports.Scene, contents <-chan *ports.ContentBatch, returns ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, security <-chan ports.SecurityState, _ ports.OutputInstance) error { | |||
| 33 | + | started <- seen{contents, returns, <-security} | |||
| 34 | + | <-ctx.Done() | |||
| 35 | + | return nil | |||
| 36 | + | } | |||
| 37 | + | attacher.EXPECT().Attach("DP-1").RunAndReturn(func(string) (<-chan *ports.ContentBatch, ports.ContentReturn) { | |||
| 38 | + | order = append(order, "attach") | |||
| 39 | + | // The output is registered with the security backend first. | |||
| 40 | + | if len(events) != 1 { | |||
| 41 | + | t.Errorf("attached with %d registrations queued, want 1", len(events)) | |||
| 42 | + | } | |||
| 43 | + | return feed, ret | |||
| 44 | + | }).Times(2) | |||
| 45 | + | attacher.EXPECT().Detach("DP-1").Run(func(string) { order = append(order, "detach") }).Times(2) | |||
| 46 | + | for round := range 2 { | |||
| 47 | + | if !set.start(ctx, "DP-1", run) { | |||
| 48 | + | t.Fatal("start failed") | |||
| 49 | + | } | |||
| 50 | + | got := <-started | |||
| 51 | + | if got.contents != (<-chan *ports.ContentBatch)(feed) || got.returns != ports.ContentReturn(ret) { | |||
| 52 | + | t.Fatalf("round %d: output got another feed", round) | |||
| 53 | + | } | |||
| 54 | + | <-events | |||
| 55 | + | set.outs["DP-1"].stop() | |||
| 56 | + | if err := set.finish("DP-1"); err != nil { | |||
| 57 | + | t.Fatal(err) | |||
| 58 | + | } | |||
| 59 | + | } | |||
| 60 | + | want := []string{"attach", "detach", "attach", "detach"} | |||
| 61 | + | if len(order) != len(want) { | |||
| 62 | + | t.Fatalf("order %v, want %v", order, want) | |||
| 63 | + | } | |||
| 64 | + | for i := range want { | |||
| 65 | + | if order[i] != want[i] { | |||
| 66 | + | t.Fatalf("order %v, want %v", order, want) | |||
| 67 | + | } | |||
| 68 | + | } | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | // wait detaches the outputs it stops. | |||
| 72 | + | func TestOutputSetWaitDetachesContents(t *testing.T) { | |||
| 73 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 74 | + | defer cancel() | |||
| 75 | + | set := newOutputSet(ctx, nil) | |||
| 76 | + | attacher := newMockcontentAttacher(t) | |||
| 77 | + | set.attach = attacher | |||
| 78 | + | attacher.EXPECT().Attach("A").Return(nil, nil).Once() | |||
| 79 | + | attacher.EXPECT().Detach("A").Return().Once() | |||
| 80 | + | set.start(ctx, "A", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |||
| 81 | + | <-ctx.Done() | |||
| 82 | + | return nil | |||
| 83 | + | }) | |||
| 84 | + | if err := set.wait(); err != nil { | |||
| 85 | + | t.Fatal(err) | |||
| 86 | + | } | |||
| 87 | + | } | |||
A internal/app/contentattacher_mock_test.go +150 -0
| @@ -0,0 +1,150 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package app | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | mock "github.com/stretchr/testify/mock" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // newMockcontentAttacher creates a new instance of mockcontentAttacher. 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 newMockcontentAttacher(t interface { | |
| 15 | + | mock.TestingT | |
| 16 | + | Cleanup(func()) | |
| 17 | + | }) *mockcontentAttacher { | |
| 18 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 19 | + | helper.Helper() | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | mock := &mockcontentAttacher{} | |
| 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 | + | // mockcontentAttacher is an autogenerated mock type for the contentAttacher type | |
| 36 | + | type mockcontentAttacher struct { | |
| 37 | + | mock.Mock | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | type mockcontentAttacher_Expecter struct { | |
| 41 | + | mock *mock.Mock | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | func (_m *mockcontentAttacher) EXPECT() *mockcontentAttacher_Expecter { | |
| 45 | + | return &mockcontentAttacher_Expecter{mock: &_m.Mock} | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // Attach provides a mock function for the type mockcontentAttacher | |
| 49 | + | func (_mock *mockcontentAttacher) Attach(name string) (<-chan *ports.ContentBatch, ports.ContentReturn) { | |
| 50 | + | ret := _mock.Called(name) | |
| 51 | + | ||
| 52 | + | if len(ret) == 0 { | |
| 53 | + | panic("no return value specified for Attach") | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | var r0 <-chan *ports.ContentBatch | |
| 57 | + | var r1 ports.ContentReturn | |
| 58 | + | if returnFunc, ok := ret.Get(0).(func(string) (<-chan *ports.ContentBatch, ports.ContentReturn)); ok { | |
| 59 | + | return returnFunc(name) | |
| 60 | + | } | |
| 61 | + | if returnFunc, ok := ret.Get(0).(func(string) <-chan *ports.ContentBatch); ok { | |
| 62 | + | r0 = returnFunc(name) | |
| 63 | + | } else { | |
| 64 | + | if ret.Get(0) != nil { | |
| 65 | + | r0 = ret.Get(0).(<-chan *ports.ContentBatch) | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | if returnFunc, ok := ret.Get(1).(func(string) ports.ContentReturn); ok { | |
| 69 | + | r1 = returnFunc(name) | |
| 70 | + | } else { | |
| 71 | + | if ret.Get(1) != nil { | |
| 72 | + | r1 = ret.Get(1).(ports.ContentReturn) | |
| 73 | + | } | |
| 74 | + | } | |
| 75 | + | return r0, r1 | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | // mockcontentAttacher_Attach_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Attach' | |
| 79 | + | type mockcontentAttacher_Attach_Call struct { | |
| 80 | + | *mock.Call | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | // Attach is a helper method to define mock.On call | |
| 84 | + | // - name string | |
| 85 | + | func (_e *mockcontentAttacher_Expecter) Attach(name any) *mockcontentAttacher_Attach_Call { | |
| 86 | + | return &mockcontentAttacher_Attach_Call{Call: _e.mock.On("Attach", name)} | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | func (_c *mockcontentAttacher_Attach_Call) Run(run func(name string)) *mockcontentAttacher_Attach_Call { | |
| 90 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 91 | + | var arg0 string | |
| 92 | + | if args[0] != nil { | |
| 93 | + | arg0 = args[0].(string) | |
| 94 | + | } | |
| 95 | + | run( | |
| 96 | + | arg0, | |
| 97 | + | ) | |
| 98 | + | }) | |
| 99 | + | return _c | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | func (_c *mockcontentAttacher_Attach_Call) Return(contentBatchCh <-chan *ports.ContentBatch, contentReturn ports.ContentReturn) *mockcontentAttacher_Attach_Call { | |
| 103 | + | _c.Call.Return(contentBatchCh, contentReturn) | |
| 104 | + | return _c | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | func (_c *mockcontentAttacher_Attach_Call) RunAndReturn(run func(name string) (<-chan *ports.ContentBatch, ports.ContentReturn)) *mockcontentAttacher_Attach_Call { | |
| 108 | + | _c.Call.Return(run) | |
| 109 | + | return _c | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | // Detach provides a mock function for the type mockcontentAttacher | |
| 113 | + | func (_mock *mockcontentAttacher) Detach(name string) { | |
| 114 | + | _mock.Called(name) | |
| 115 | + | return | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | // mockcontentAttacher_Detach_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Detach' | |
| 119 | + | type mockcontentAttacher_Detach_Call struct { | |
| 120 | + | *mock.Call | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | // Detach is a helper method to define mock.On call | |
| 124 | + | // - name string | |
| 125 | + | func (_e *mockcontentAttacher_Expecter) Detach(name any) *mockcontentAttacher_Detach_Call { | |
| 126 | + | return &mockcontentAttacher_Detach_Call{Call: _e.mock.On("Detach", name)} | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | func (_c *mockcontentAttacher_Detach_Call) Run(run func(name string)) *mockcontentAttacher_Detach_Call { | |
| 130 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 131 | + | var arg0 string | |
| 132 | + | if args[0] != nil { | |
| 133 | + | arg0 = args[0].(string) | |
| 134 | + | } | |
| 135 | + | run( | |
| 136 | + | arg0, | |
| 137 | + | ) | |
| 138 | + | }) | |
| 139 | + | return _c | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | func (_c *mockcontentAttacher_Detach_Call) Return() *mockcontentAttacher_Detach_Call { | |
| 143 | + | _c.Call.Return() | |
| 144 | + | return _c | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | func (_c *mockcontentAttacher_Detach_Call) RunAndReturn(run func(name string)) *mockcontentAttacher_Detach_Call { | |
| 148 | + | _c.Run(run) | |
| 149 | + | return _c | |
| 150 | + | } | |
| @@ -0,0 +1,150 @@ | |||||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |||
| 2 | + | // github.com/vektra/mockery | |||
| 3 | + | // template: testify | |||
| 4 | + | ||||
| 5 | + | package app | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | mock "github.com/stretchr/testify/mock" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // newMockcontentAttacher creates a new instance of mockcontentAttacher. 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 newMockcontentAttacher(t interface { | |||
| 15 | + | mock.TestingT | |||
| 16 | + | Cleanup(func()) | |||
| 17 | + | }) *mockcontentAttacher { | |||
| 18 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 19 | + | helper.Helper() | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | mock := &mockcontentAttacher{} | |||
| 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 | + | // mockcontentAttacher is an autogenerated mock type for the contentAttacher type | |||
| 36 | + | type mockcontentAttacher struct { | |||
| 37 | + | mock.Mock | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | type mockcontentAttacher_Expecter struct { | |||
| 41 | + | mock *mock.Mock | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | func (_m *mockcontentAttacher) EXPECT() *mockcontentAttacher_Expecter { | |||
| 45 | + | return &mockcontentAttacher_Expecter{mock: &_m.Mock} | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // Attach provides a mock function for the type mockcontentAttacher | |||
| 49 | + | func (_mock *mockcontentAttacher) Attach(name string) (<-chan *ports.ContentBatch, ports.ContentReturn) { | |||
| 50 | + | ret := _mock.Called(name) | |||
| 51 | + | ||||
| 52 | + | if len(ret) == 0 { | |||
| 53 | + | panic("no return value specified for Attach") | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | var r0 <-chan *ports.ContentBatch | |||
| 57 | + | var r1 ports.ContentReturn | |||
| 58 | + | if returnFunc, ok := ret.Get(0).(func(string) (<-chan *ports.ContentBatch, ports.ContentReturn)); ok { | |||
| 59 | + | return returnFunc(name) | |||
| 60 | + | } | |||
| 61 | + | if returnFunc, ok := ret.Get(0).(func(string) <-chan *ports.ContentBatch); ok { | |||
| 62 | + | r0 = returnFunc(name) | |||
| 63 | + | } else { | |||
| 64 | + | if ret.Get(0) != nil { | |||
| 65 | + | r0 = ret.Get(0).(<-chan *ports.ContentBatch) | |||
| 66 | + | } | |||
| 67 | + | } | |||
| 68 | + | if returnFunc, ok := ret.Get(1).(func(string) ports.ContentReturn); ok { | |||
| 69 | + | r1 = returnFunc(name) | |||
| 70 | + | } else { | |||
| 71 | + | if ret.Get(1) != nil { | |||
| 72 | + | r1 = ret.Get(1).(ports.ContentReturn) | |||
| 73 | + | } | |||
| 74 | + | } | |||
| 75 | + | return r0, r1 | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | // mockcontentAttacher_Attach_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Attach' | |||
| 79 | + | type mockcontentAttacher_Attach_Call struct { | |||
| 80 | + | *mock.Call | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | // Attach is a helper method to define mock.On call | |||
| 84 | + | // - name string | |||
| 85 | + | func (_e *mockcontentAttacher_Expecter) Attach(name any) *mockcontentAttacher_Attach_Call { | |||
| 86 | + | return &mockcontentAttacher_Attach_Call{Call: _e.mock.On("Attach", name)} | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | func (_c *mockcontentAttacher_Attach_Call) Run(run func(name string)) *mockcontentAttacher_Attach_Call { | |||
| 90 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 91 | + | var arg0 string | |||
| 92 | + | if args[0] != nil { | |||
| 93 | + | arg0 = args[0].(string) | |||
| 94 | + | } | |||
| 95 | + | run( | |||
| 96 | + | arg0, | |||
| 97 | + | ) | |||
| 98 | + | }) | |||
| 99 | + | return _c | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | func (_c *mockcontentAttacher_Attach_Call) Return(contentBatchCh <-chan *ports.ContentBatch, contentReturn ports.ContentReturn) *mockcontentAttacher_Attach_Call { | |||
| 103 | + | _c.Call.Return(contentBatchCh, contentReturn) | |||
| 104 | + | return _c | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | func (_c *mockcontentAttacher_Attach_Call) RunAndReturn(run func(name string) (<-chan *ports.ContentBatch, ports.ContentReturn)) *mockcontentAttacher_Attach_Call { | |||
| 108 | + | _c.Call.Return(run) | |||
| 109 | + | return _c | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | // Detach provides a mock function for the type mockcontentAttacher | |||
| 113 | + | func (_mock *mockcontentAttacher) Detach(name string) { | |||
| 114 | + | _mock.Called(name) | |||
| 115 | + | return | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | // mockcontentAttacher_Detach_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Detach' | |||
| 119 | + | type mockcontentAttacher_Detach_Call struct { | |||
| 120 | + | *mock.Call | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | // Detach is a helper method to define mock.On call | |||
| 124 | + | // - name string | |||
| 125 | + | func (_e *mockcontentAttacher_Expecter) Detach(name any) *mockcontentAttacher_Detach_Call { | |||
| 126 | + | return &mockcontentAttacher_Detach_Call{Call: _e.mock.On("Detach", name)} | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | func (_c *mockcontentAttacher_Detach_Call) Run(run func(name string)) *mockcontentAttacher_Detach_Call { | |||
| 130 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 131 | + | var arg0 string | |||
| 132 | + | if args[0] != nil { | |||
| 133 | + | arg0 = args[0].(string) | |||
| 134 | + | } | |||
| 135 | + | run( | |||
| 136 | + | arg0, | |||
| 137 | + | ) | |||
| 138 | + | }) | |||
| 139 | + | return _c | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | func (_c *mockcontentAttacher_Detach_Call) Return() *mockcontentAttacher_Detach_Call { | |||
| 143 | + | _c.Call.Return() | |||
| 144 | + | return _c | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | func (_c *mockcontentAttacher_Detach_Call) RunAndReturn(run func(name string)) *mockcontentAttacher_Detach_Call { | |||
| 148 | + | _c.Run(run) | |||
| 149 | + | return _c | |||
| 150 | + | } | |||
M internal/app/drm.go +3 -7
| @@ -333,6 +333,7 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm | |||
| 333 | 333 | defer b.seat.Unsubscribe(active) | |
| 334 | 334 | seatActive := true // openDRM waits for the first seat enable | |
| 335 | 335 | set := newOutputSet(ctx, ch.captured) | |
| 336 | + | set.attach = ch.content | |
| 336 | 337 | set.wireSecurity(ch) | |
| 337 | 338 | protected := func() bool { return set.state.Protected || securitySnapshot(ch.security).Protected } | |
| 338 | 339 | leases := newLeasePublisher(ctx, ch.leaseEvents, func() bool { return seatActive }, protected, log) | |
| @@ -513,7 +514,7 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm | |||
| 513 | 514 | } | |
| 514 | 515 | active := b.seat.Subscribe() | |
| 515 | 516 | realtime := currentConfig.Performance.Realtime | |
| 516 | - | started := set.start(ctx, name, func(octx context.Context, sc <-chan ports.Scene, cc <-chan ports.SurfaceContent, cu <-chan ports.CursorChange, cap <-chan ports.CaptureRequest, secure <-chan ports.SecurityState, instance ports.OutputInstance) error { | |
| 517 | + | started := set.start(ctx, name, func(octx context.Context, sc <-chan ports.Scene, cc <-chan *ports.ContentBatch, rc ports.ContentReturn, cu <-chan ports.CursorChange, cap <-chan ports.CaptureRequest, secure <-chan ports.SecurityState, instance ports.OutputInstance) error { | |
| 517 | 518 | defer b.seat.Unsubscribe(active) | |
| 518 | 519 | o.Security, o.SecurityChanges, o.Instance = ch.security, secure, instance | |
| 519 | 520 | return safe("output "+name, func() error { | |
| @@ -533,7 +534,7 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm | |||
| 533 | 534 | schedLog.Warn().Err(err).Msg("output scheduling") | |
| 534 | 535 | } | |
| 535 | 536 | } | |
| 536 | - | return o.Run(octx, newRenderer, loadCursor, active, sc, cc, cu, ch.presented, cap, ch.captured) | |
| 537 | + | return o.Run(octx, newRenderer, loadCursor, active, sc, cc, rc, cu, ch.presented, cap, ch.captured) | |
| 537 | 538 | }) | |
| 538 | 539 | }, &o.SecurityEvents) | |
| 539 | 540 | if !started { | |
| @@ -595,7 +596,6 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm | |||
| 595 | 596 | configs, configNext := apply.configOut(ch.configured), apply.config | |
| 596 | 597 | replies, replyNext := apply.replyOut(ch.replies) | |
| 597 | 598 | heads, headsNext := apply.headsOut(ch.heads) | |
| 598 | - | contents, contentOut, contentNext, contentOwner := set.contentOut(ch.contents) | |
| 599 | 599 | select { | |
| 600 | 600 | case <-ctx.Done(): | |
| 601 | 601 | return set.wait() | |
| @@ -700,10 +700,6 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm | |||
| 700 | 700 | complete(progress.timeout(op)) | |
| 701 | 701 | case s := <-ch.scenes: | |
| 702 | 702 | set.scenes(s) | |
| 703 | - | case c := <-contents: | |
| 704 | - | set.content(c) | |
| 705 | - | case contentOut <- contentNext: | |
| 706 | - | contentOwner.contentSent() | |
| 707 | 703 | case q := <-ch.captures: | |
| 708 | 704 | set.routeCapture(q) | |
| 709 | 705 | case c := <-ch.cursorChanges: | |
| @@ -333,6 +333,7 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm | |||||
| 333 | defer b.seat.Unsubscribe(active) | 333 | defer b.seat.Unsubscribe(active) | ||
| 334 | seatActive := true // openDRM waits for the first seat enable | 334 | seatActive := true // openDRM waits for the first seat enable | ||
| 335 | set := newOutputSet(ctx, ch.captured) | 335 | set := newOutputSet(ctx, ch.captured) | ||
| 336 | + | set.attach = ch.content | |||
| 336 | set.wireSecurity(ch) | 337 | set.wireSecurity(ch) | ||
| 337 | protected := func() bool { return set.state.Protected || securitySnapshot(ch.security).Protected } | 338 | protected := func() bool { return set.state.Protected || securitySnapshot(ch.security).Protected } | ||
| 338 | leases := newLeasePublisher(ctx, ch.leaseEvents, func() bool { return seatActive }, protected, log) | 339 | leases := newLeasePublisher(ctx, ch.leaseEvents, func() bool { return seatActive }, protected, log) | ||
| @@ -513,7 +514,7 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm | |||||
| 513 | } | 514 | } | ||
| 514 | active := b.seat.Subscribe() | 515 | active := b.seat.Subscribe() | ||
| 515 | realtime := currentConfig.Performance.Realtime | 516 | realtime := currentConfig.Performance.Realtime | ||
| 516 | - | started := set.start(ctx, name, func(octx context.Context, sc <-chan ports.Scene, cc <-chan ports.SurfaceContent, cu <-chan ports.CursorChange, cap <-chan ports.CaptureRequest, secure <-chan ports.SecurityState, instance ports.OutputInstance) error { | 517 | + | started := set.start(ctx, name, func(octx context.Context, sc <-chan ports.Scene, cc <-chan *ports.ContentBatch, rc ports.ContentReturn, cu <-chan ports.CursorChange, cap <-chan ports.CaptureRequest, secure <-chan ports.SecurityState, instance ports.OutputInstance) error { |
| 517 | defer b.seat.Unsubscribe(active) | 518 | defer b.seat.Unsubscribe(active) | ||
| 518 | o.Security, o.SecurityChanges, o.Instance = ch.security, secure, instance | 519 | o.Security, o.SecurityChanges, o.Instance = ch.security, secure, instance | ||
| 519 | return safe("output "+name, func() error { | 520 | return safe("output "+name, func() error { | ||
| @@ -533,7 +534,7 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm | |||||
| 533 | schedLog.Warn().Err(err).Msg("output scheduling") | 534 | schedLog.Warn().Err(err).Msg("output scheduling") | ||
| 534 | } | 535 | } | ||
| 535 | } | 536 | } | ||
| 536 | - | return o.Run(octx, newRenderer, loadCursor, active, sc, cc, cu, ch.presented, cap, ch.captured) | 537 | + | return o.Run(octx, newRenderer, loadCursor, active, sc, cc, rc, cu, ch.presented, cap, ch.captured) |
| 537 | }) | 538 | }) | ||
| 538 | }, &o.SecurityEvents) | 539 | }, &o.SecurityEvents) | ||
| 539 | if !started { | 540 | if !started { | ||
| @@ -595,7 +596,6 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm | |||||
| 595 | configs, configNext := apply.configOut(ch.configured), apply.config | 596 | configs, configNext := apply.configOut(ch.configured), apply.config | ||
| 596 | replies, replyNext := apply.replyOut(ch.replies) | 597 | replies, replyNext := apply.replyOut(ch.replies) | ||
| 597 | heads, headsNext := apply.headsOut(ch.heads) | 598 | heads, headsNext := apply.headsOut(ch.heads) | ||
| 598 | - | contents, contentOut, contentNext, contentOwner := set.contentOut(ch.contents) | |||
| 599 | select { | 599 | select { | ||
| 600 | case <-ctx.Done(): | 600 | case <-ctx.Done(): | ||
| 601 | return set.wait() | 601 | return set.wait() | ||
| @@ -700,10 +700,6 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm | |||||
| 700 | complete(progress.timeout(op)) | 700 | complete(progress.timeout(op)) | ||
| 701 | case s := <-ch.scenes: | 701 | case s := <-ch.scenes: | ||
| 702 | set.scenes(s) | 702 | set.scenes(s) | ||
| 703 | - | case c := <-contents: | |||
| 704 | - | set.content(c) | |||
| 705 | - | case contentOut <- contentNext: | |||
| 706 | - | contentOwner.contentSent() | |||
| 707 | case q := <-ch.captures: | 703 | case q := <-ch.captures: | ||
| 708 | set.routeCapture(q) | 704 | set.routeCapture(q) | ||
| 709 | case c := <-ch.cursorChanges: | 705 | case c := <-ch.cursorChanges: | ||
M internal/app/headless.go +3 -7
| @@ -27,6 +27,7 @@ type headlessOptions struct { | |||
| 27 | 27 | // newRenderer is configured for virtual outputs by newVulkanRenderer. | |
| 28 | 28 | func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch outputChannels, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error { | |
| 29 | 29 | set := newOutputSet(ctx, ch.captured) | |
| 30 | + | set.attach = ch.content | |
| 30 | 31 | set.wireSecurity(ch) | |
| 31 | 32 | inventory := ports.OutputHeads{} | |
| 32 | 33 | for i, size := range o.sizes { | |
| @@ -44,9 +45,9 @@ func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch | |||
| 44 | 45 | cur := &headless.Cursor{} | |
| 45 | 46 | curs.set(name, cur) | |
| 46 | 47 | opts := headless.Options{Cursor: cur, LoadCursor: loadCursor, Width: size[0], Height: size[1], ScreenshotDir: dir, HDR: o.hdr, RawHDR: o.raw, Formats: ch.formats, Log: log, NewRenderer: newRenderer, NewCaptureRenderer: newRenderer, Name: name, Presented: ch.presented, Frames: ch.frames, Captured: ch.captured} | |
| 47 | - | started := set.start(ctx, name, func(octx context.Context, sc <-chan ports.Scene, cc <-chan ports.SurfaceContent, cu <-chan ports.CursorChange, cap <-chan ports.CaptureRequest, secure <-chan ports.SecurityState, instance ports.OutputInstance) error { | |
| 48 | + | started := set.start(ctx, name, func(octx context.Context, sc <-chan ports.Scene, cc <-chan *ports.ContentBatch, rc ports.ContentReturn, cu <-chan ports.CursorChange, cap <-chan ports.CaptureRequest, secure <-chan ports.SecurityState, instance ports.OutputInstance) error { | |
| 48 | 49 | opts.Security, opts.SecurityChanges, opts.Instance = ch.security, secure, instance | |
| 49 | - | return headless.Run(octx, opts, sc, cc, cu, cap) | |
| 50 | + | return headless.Run(octx, opts, sc, cc, rc, cu, cap) | |
| 50 | 51 | }, &opts.SecurityEvents) | |
| 51 | 52 | if !started { | |
| 52 | 53 | return set.wait() | |
| @@ -68,7 +69,6 @@ func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch | |||
| 68 | 69 | configs, configNext := apply.configOut(ch.configured), apply.config | |
| 69 | 70 | replies, replyNext := apply.replyOut(ch.replies) | |
| 70 | 71 | heads, headsNext := apply.headsOut(ch.heads) | |
| 71 | - | contents, contentOut, contentNext, contentOwner := set.contentOut(ch.contents) | |
| 72 | 72 | select { | |
| 73 | 73 | case <-ctx.Done(): | |
| 74 | 74 | return set.wait() | |
| @@ -92,10 +92,6 @@ func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch | |||
| 92 | 92 | apply.headsSent() | |
| 93 | 93 | case s := <-ch.scenes: | |
| 94 | 94 | set.scenes(s) | |
| 95 | - | case c := <-contents: | |
| 96 | - | set.content(c) | |
| 97 | - | case contentOut <- contentNext: | |
| 98 | - | contentOwner.contentSent() | |
| 99 | 95 | case q := <-ch.captures: | |
| 100 | 96 | set.routeCapture(q) | |
| 101 | 97 | case c := <-ch.cursorChanges: | |
| @@ -27,6 +27,7 @@ type headlessOptions struct { | |||||
| 27 | // newRenderer is configured for virtual outputs by newVulkanRenderer. | 27 | // newRenderer is configured for virtual outputs by newVulkanRenderer. | ||
| 28 | func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch outputChannels, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error { | 28 | func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch outputChannels, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error { | ||
| 29 | set := newOutputSet(ctx, ch.captured) | 29 | set := newOutputSet(ctx, ch.captured) | ||
| 30 | + | set.attach = ch.content | |||
| 30 | set.wireSecurity(ch) | 31 | set.wireSecurity(ch) | ||
| 31 | inventory := ports.OutputHeads{} | 32 | inventory := ports.OutputHeads{} | ||
| 32 | for i, size := range o.sizes { | 33 | for i, size := range o.sizes { | ||
| @@ -44,9 +45,9 @@ func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch | |||||
| 44 | cur := &headless.Cursor{} | 45 | cur := &headless.Cursor{} | ||
| 45 | curs.set(name, cur) | 46 | curs.set(name, cur) | ||
| 46 | opts := headless.Options{Cursor: cur, LoadCursor: loadCursor, Width: size[0], Height: size[1], ScreenshotDir: dir, HDR: o.hdr, RawHDR: o.raw, Formats: ch.formats, Log: log, NewRenderer: newRenderer, NewCaptureRenderer: newRenderer, Name: name, Presented: ch.presented, Frames: ch.frames, Captured: ch.captured} | 47 | opts := headless.Options{Cursor: cur, LoadCursor: loadCursor, Width: size[0], Height: size[1], ScreenshotDir: dir, HDR: o.hdr, RawHDR: o.raw, Formats: ch.formats, Log: log, NewRenderer: newRenderer, NewCaptureRenderer: newRenderer, Name: name, Presented: ch.presented, Frames: ch.frames, Captured: ch.captured} | ||
| 47 | - | started := set.start(ctx, name, func(octx context.Context, sc <-chan ports.Scene, cc <-chan ports.SurfaceContent, cu <-chan ports.CursorChange, cap <-chan ports.CaptureRequest, secure <-chan ports.SecurityState, instance ports.OutputInstance) error { | 48 | + | started := set.start(ctx, name, func(octx context.Context, sc <-chan ports.Scene, cc <-chan *ports.ContentBatch, rc ports.ContentReturn, cu <-chan ports.CursorChange, cap <-chan ports.CaptureRequest, secure <-chan ports.SecurityState, instance ports.OutputInstance) error { |
| 48 | opts.Security, opts.SecurityChanges, opts.Instance = ch.security, secure, instance | 49 | opts.Security, opts.SecurityChanges, opts.Instance = ch.security, secure, instance | ||
| 49 | - | return headless.Run(octx, opts, sc, cc, cu, cap) | 50 | + | return headless.Run(octx, opts, sc, cc, rc, cu, cap) |
| 50 | }, &opts.SecurityEvents) | 51 | }, &opts.SecurityEvents) | ||
| 51 | if !started { | 52 | if !started { | ||
| 52 | return set.wait() | 53 | return set.wait() | ||
| @@ -68,7 +69,6 @@ func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch | |||||
| 68 | configs, configNext := apply.configOut(ch.configured), apply.config | 69 | configs, configNext := apply.configOut(ch.configured), apply.config | ||
| 69 | replies, replyNext := apply.replyOut(ch.replies) | 70 | replies, replyNext := apply.replyOut(ch.replies) | ||
| 70 | heads, headsNext := apply.headsOut(ch.heads) | 71 | heads, headsNext := apply.headsOut(ch.heads) | ||
| 71 | - | contents, contentOut, contentNext, contentOwner := set.contentOut(ch.contents) | |||
| 72 | select { | 72 | select { | ||
| 73 | case <-ctx.Done(): | 73 | case <-ctx.Done(): | ||
| 74 | return set.wait() | 74 | return set.wait() | ||
| @@ -92,10 +92,6 @@ func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch | |||||
| 92 | apply.headsSent() | 92 | apply.headsSent() | ||
| 93 | case s := <-ch.scenes: | 93 | case s := <-ch.scenes: | ||
| 94 | set.scenes(s) | 94 | set.scenes(s) | ||
| 95 | - | case c := <-contents: | |||
| 96 | - | set.content(c) | |||
| 97 | - | case contentOut <- contentNext: | |||
| 98 | - | contentOwner.contentSent() | |||
| 99 | case q := <-ch.captures: | 95 | case q := <-ch.captures: | ||
| 100 | set.routeCapture(q) | 96 | set.routeCapture(q) | ||
| 101 | case c := <-ch.cursorChanges: | 97 | case c := <-ch.cursorChanges: | ||
M internal/app/outputrouting.go +2 -2
| @@ -4,7 +4,7 @@ import "github.com/bnema/neferwl/internal/ports" | |||
| 4 | 4 | ||
| 5 | 5 | // outputChannels is the shared app-to-backend wiring. The backend loop remains | |
| 6 | 6 | // the sole owner of outputSet and outputApply. Security producers use bounded | |
| 7 | - | // lossless FIFO forwarders; content backpressure stays inside the backend select. | |
| 7 | + | // lossless FIFO forwarders; contents are not routed here: outputs take them from wayland (contentAttacher). | |
| 8 | 8 | // heads, configured and replies are fed from the outputApply outbox. | |
| 9 | 9 | type outputChannels struct { | |
| 10 | 10 | security ports.SessionSecurity | |
| @@ -14,7 +14,7 @@ type outputChannels struct { | |||
| 14 | 14 | leaseEvents chan<- ports.LeaseMessage | |
| 15 | 15 | events chan<- ports.OutputEvent | |
| 16 | 16 | scenes <-chan []ports.Scene | |
| 17 | - | contents <-chan ports.SurfaceContent | |
| 17 | + | content contentAttacher | |
| 18 | 18 | cursorChanges <-chan ports.CursorChange | |
| 19 | 19 | presented ports.PresentationSink | |
| 20 | 20 | frames chan<- ports.OutputFrame | |
| @@ -4,7 +4,7 @@ import "github.com/bnema/neferwl/internal/ports" | |||||
| 4 | 4 | ||||
| 5 | // outputChannels is the shared app-to-backend wiring. The backend loop remains | 5 | // outputChannels is the shared app-to-backend wiring. The backend loop remains | ||
| 6 | // the sole owner of outputSet and outputApply. Security producers use bounded | 6 | // the sole owner of outputSet and outputApply. Security producers use bounded | ||
| 7 | - | // lossless FIFO forwarders; content backpressure stays inside the backend select. | 7 | + | // lossless FIFO forwarders; contents are not routed here: outputs take them from wayland (contentAttacher). |
| 8 | // heads, configured and replies are fed from the outputApply outbox. | 8 | // heads, configured and replies are fed from the outputApply outbox. | ||
| 9 | type outputChannels struct { | 9 | type outputChannels struct { | ||
| 10 | security ports.SessionSecurity | 10 | security ports.SessionSecurity | ||
| @@ -14,7 +14,7 @@ type outputChannels struct { | |||||
| 14 | leaseEvents chan<- ports.LeaseMessage | 14 | leaseEvents chan<- ports.LeaseMessage | ||
| 15 | events chan<- ports.OutputEvent | 15 | events chan<- ports.OutputEvent | ||
| 16 | scenes <-chan []ports.Scene | 16 | scenes <-chan []ports.Scene | ||
| 17 | - | contents <-chan ports.SurfaceContent | 17 | + | content contentAttacher |
| 18 | cursorChanges <-chan ports.CursorChange | 18 | cursorChanges <-chan ports.CursorChange | ||
| 19 | presented ports.PresentationSink | 19 | presented ports.PresentationSink | ||
| 20 | frames chan<- ports.OutputFrame | 20 | frames chan<- ports.OutputFrame | ||
M internal/app/outputrouting_test.go +2 -15
| @@ -14,7 +14,7 @@ func TestOutputRouting(t *testing.T) { | |||
| 14 | 14 | replies := make(chan ports.CaptureDone, 2) | |
| 15 | 15 | set := newOutputSet(ctx, replies) | |
| 16 | 16 | for _, name := range []string{"A", "B"} { | |
| 17 | - | set.start(ctx, name, func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 17 | + | set.start(ctx, name, func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 18 | 18 | <-ctx.Done() | |
| 19 | 19 | return nil | |
| 20 | 20 | }) | |
| @@ -28,13 +28,6 @@ func TestOutputRouting(t *testing.T) { | |||
| 28 | 28 | if got := <-set.outs["B"].scenes; got.Seq != 2 { | |
| 29 | 29 | t.Fatalf("B scene: %+v", got) | |
| 30 | 30 | } | |
| 31 | - | content := ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{}} | |
| 32 | - | set.content(content) | |
| 33 | - | for _, name := range []string{"A", "B"} { | |
| 34 | - | if got := <-set.outs[name].contents; got.ID != content.ID { | |
| 35 | - | t.Fatalf("%s content: %+v", name, got) | |
| 36 | - | } | |
| 37 | - | } | |
| 38 | 31 | set.setCursor(ports.CursorChange{Shape: "text"}) | |
| 39 | 32 | set.setCursor(ports.CursorChange{Shape: "pointer"}) | |
| 40 | 33 | for _, name := range []string{"A", "B"} { | |
| @@ -86,13 +79,9 @@ func TestOutputSetRemembersOff(t *testing.T) { | |||
| 86 | 79 | ||
| 87 | 80 | func TestOutputRoutingAllocations(t *testing.T) { | |
| 88 | 81 | set := newOutputSet(context.Background(), nil) | |
| 89 | - | // Populate the existing content slot before measuring, so the guard | |
| 90 | - | // measures routing rather than first-use map growth. | |
| 91 | - | content := ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{}} | |
| 92 | - | set.content(content) | |
| 93 | 82 | ctx, cancel := context.WithCancel(context.Background()) | |
| 94 | 83 | defer cancel() | |
| 95 | - | set.start(ctx, "A", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 84 | + | set.start(ctx, "A", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 96 | 85 | <-ctx.Done() | |
| 97 | 86 | return nil | |
| 98 | 87 | }) | |
| @@ -101,8 +90,6 @@ func TestOutputRoutingAllocations(t *testing.T) { | |||
| 101 | 90 | capture := ports.CaptureRequest{Output: "A"} | |
| 102 | 91 | if got := testing.AllocsPerRun(100, func() { | |
| 103 | 92 | set.scenes(scenes) | |
| 104 | - | <-set.outs["A"].contents | |
| 105 | - | set.content(content) | |
| 106 | 93 | set.setCursor(cursor) | |
| 107 | 94 | set.routeCapture(capture) | |
| 108 | 95 | <-set.outs["A"].captures | |
| @@ -14,7 +14,7 @@ func TestOutputRouting(t *testing.T) { | |||||
| 14 | replies := make(chan ports.CaptureDone, 2) | 14 | replies := make(chan ports.CaptureDone, 2) | ||
| 15 | set := newOutputSet(ctx, replies) | 15 | set := newOutputSet(ctx, replies) | ||
| 16 | for _, name := range []string{"A", "B"} { | 16 | for _, name := range []string{"A", "B"} { | ||
| 17 | - | set.start(ctx, name, func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | 17 | + | set.start(ctx, name, func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { |
| 18 | <-ctx.Done() | 18 | <-ctx.Done() | ||
| 19 | return nil | 19 | return nil | ||
| 20 | }) | 20 | }) | ||
| @@ -28,13 +28,6 @@ func TestOutputRouting(t *testing.T) { | |||||
| 28 | if got := <-set.outs["B"].scenes; got.Seq != 2 { | 28 | if got := <-set.outs["B"].scenes; got.Seq != 2 { | ||
| 29 | t.Fatalf("B scene: %+v", got) | 29 | t.Fatalf("B scene: %+v", got) | ||
| 30 | } | 30 | } | ||
| 31 | - | content := ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{}} | |||
| 32 | - | set.content(content) | |||
| 33 | - | for _, name := range []string{"A", "B"} { | |||
| 34 | - | if got := <-set.outs[name].contents; got.ID != content.ID { | |||
| 35 | - | t.Fatalf("%s content: %+v", name, got) | |||
| 36 | - | } | |||
| 37 | - | } | |||
| 38 | set.setCursor(ports.CursorChange{Shape: "text"}) | 31 | set.setCursor(ports.CursorChange{Shape: "text"}) | ||
| 39 | set.setCursor(ports.CursorChange{Shape: "pointer"}) | 32 | set.setCursor(ports.CursorChange{Shape: "pointer"}) | ||
| 40 | for _, name := range []string{"A", "B"} { | 33 | for _, name := range []string{"A", "B"} { | ||
| @@ -86,13 +79,9 @@ func TestOutputSetRemembersOff(t *testing.T) { | |||||
| 86 | 79 | ||||
| 87 | func TestOutputRoutingAllocations(t *testing.T) { | 80 | func TestOutputRoutingAllocations(t *testing.T) { | ||
| 88 | set := newOutputSet(context.Background(), nil) | 81 | set := newOutputSet(context.Background(), nil) | ||
| 89 | - | // Populate the existing content slot before measuring, so the guard | |||
| 90 | - | // measures routing rather than first-use map growth. | |||
| 91 | - | content := ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{}} | |||
| 92 | - | set.content(content) | |||
| 93 | ctx, cancel := context.WithCancel(context.Background()) | 82 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 94 | defer cancel() | 83 | defer cancel() | ||
| 95 | - | set.start(ctx, "A", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | 84 | + | set.start(ctx, "A", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { |
| 96 | <-ctx.Done() | 85 | <-ctx.Done() | ||
| 97 | return nil | 86 | return nil | ||
| 98 | }) | 87 | }) | ||
| @@ -101,8 +90,6 @@ func TestOutputRoutingAllocations(t *testing.T) { | |||||
| 101 | capture := ports.CaptureRequest{Output: "A"} | 90 | capture := ports.CaptureRequest{Output: "A"} | ||
| 102 | if got := testing.AllocsPerRun(100, func() { | 91 | if got := testing.AllocsPerRun(100, func() { | ||
| 103 | set.scenes(scenes) | 92 | set.scenes(scenes) | ||
| 104 | - | <-set.outs["A"].contents | |||
| 105 | - | set.content(content) | |||
| 106 | set.setCursor(cursor) | 93 | set.setCursor(cursor) | ||
| 107 | set.routeCapture(capture) | 94 | set.routeCapture(capture) | ||
| 108 | <-set.outs["A"].captures | 95 | <-set.outs["A"].captures | ||
M internal/app/outputs.go +36 -64
| @@ -13,16 +13,27 @@ import ( | |||
| 13 | 13 | ||
| 14 | 14 | // outputRun drives one output until ctx ends: it renders the scenes and | |
| 15 | 15 | // surface contents it receives, with the cursor the client asks for. | |
| 16 | - | type outputRun func(ctx context.Context, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange, captures <-chan ports.CaptureRequest, security <-chan ports.SecurityState, instance ports.OutputInstance) error | |
| 16 | + | type outputRun func(ctx context.Context, scenes <-chan ports.Scene, contents <-chan *ports.ContentBatch, returns ports.ContentReturn, cursor <-chan ports.CursorChange, captures <-chan ports.CaptureRequest, security <-chan ports.SecurityState, instance ports.OutputInstance) error | |
| 17 | + | ||
| 18 | + | // contentAttacher connects an output to the surface contents: wayland feeds | |
| 19 | + | // every attached output batches of window contents, starting with the latest | |
| 20 | + | // content of each window, and takes the batches back through the returned | |
| 21 | + | // ports.ContentReturn. Attach may block on the display goroutine: call it | |
| 22 | + | // from outside that goroutine. It returns a nil channel when wayland is gone. | |
| 23 | + | type contentAttacher interface { | |
| 24 | + | Attach(name string) (<-chan *ports.ContentBatch, ports.ContentReturn) | |
| 25 | + | Detach(name string) | |
| 26 | + | } | |
| 17 | 27 | ||
| 18 | 28 | // outputSet owns the running outputs and feeds them. Core sends one scene | |
| 19 | - | // per output; each goes to its output, latest first. Surface contents go to | |
| 20 | - | // every output, since windows move between outputs; the latest content of | |
| 21 | - | // each window is kept so a new output starts with every window drawn. | |
| 29 | + | // per output; each goes to its output, latest first. Surface contents reach | |
| 30 | + | // every output (windows move between outputs) straight from wayland through | |
| 31 | + | // attach, which also replays the current windows to a new output. | |
| 22 | 32 | // Only the goroutine running loop touches the set. | |
| 23 | 33 | type outputSet struct { | |
| 24 | - | outs map[string]*runningOutput | |
| 25 | - | latest map[ports.WindowID]ports.SurfaceContent | |
| 34 | + | outs map[string]*runningOutput | |
| 35 | + | // attach is nil when no wayland server feeds contents (tests). | |
| 36 | + | attach contentAttacher | |
| 26 | 37 | // off is the latest Scene.Off per output name, kept after the output | |
| 27 | 38 | // goes: it reconnects off until core's first scene (Output.StartOff). | |
| 28 | 39 | off map[string]bool | |
| @@ -50,9 +61,7 @@ type runningOutput struct { | |||
| 50 | 61 | instance ports.OutputInstance | |
| 51 | 62 | security chan ports.SecurityState | |
| 52 | 63 | securityEvents chan ports.SecurityBackendEvent | |
| 53 | - | pending []ports.SurfaceContent | |
| 54 | 64 | scenes chan ports.Scene | |
| 55 | - | contents chan ports.SurfaceContent | |
| 56 | 65 | cursor chan ports.CursorChange | |
| 57 | 66 | captures chan ports.CaptureRequest | |
| 58 | 67 | ctx context.Context | |
| @@ -62,10 +71,10 @@ type runningOutput struct { | |||
| 62 | 71 | } | |
| 63 | 72 | ||
| 64 | 73 | func newOutputSet(ctx context.Context, captured chan<- ports.CaptureDone) *outputSet { | |
| 65 | - | return &outputSet{captured: captured, ctx: ctx, outs: map[string]*runningOutput{}, latest: map[ports.WindowID]ports.SurfaceContent{}, off: map[string]bool{}, stopped: make(chan outputStopped), quit: make(chan struct{})} | |
| 74 | + | return &outputSet{captured: captured, ctx: ctx, outs: map[string]*runningOutput{}, off: map[string]bool{}, stopped: make(chan outputStopped), quit: make(chan struct{})} | |
| 66 | 75 | } | |
| 67 | 76 | ||
| 68 | - | // start runs an output in its own goroutine and replays the window contents. | |
| 77 | + | // start runs an output in its own goroutine, fed the window contents. | |
| 69 | 78 | func (s *outputSet) start(ctx context.Context, name string, run outputRun, wire ...*chan<- ports.SecurityBackendEvent) bool { | |
| 70 | 79 | // Exhaustion is permanent: never wrap to zero or reuse a lifetime. | |
| 71 | 80 | if s.nextInstance == ^ports.OutputInstance(0) { | |
| @@ -82,22 +91,21 @@ func (s *outputSet) start(ctx context.Context, name string, run outputRun, wire | |||
| 82 | 91 | s.setSecurity(s.security.Snapshot()) | |
| 83 | 92 | } | |
| 84 | 93 | octx, stop := context.WithCancel(ctx) | |
| 85 | - | r := &runningOutput{instance: instance, security: make(chan ports.SecurityState, 1), scenes: make(chan ports.Scene, 1), contents: make(chan ports.SurfaceContent, 64), cursor: make(chan ports.CursorChange, 1), captures: make(chan ports.CaptureRequest, 16), ctx: octx, stop: stop, done: make(chan struct{})} | |
| 86 | - | r.security <- s.state // available before any old scene/content replay | |
| 94 | + | r := &runningOutput{instance: instance, security: make(chan ports.SecurityState, 1), scenes: make(chan ports.Scene, 1), cursor: make(chan ports.CursorChange, 1), captures: make(chan ports.CaptureRequest, 16), ctx: octx, stop: stop, done: make(chan struct{})} | |
| 95 | + | r.security <- s.state // available before any content replay | |
| 87 | 96 | r.securityEvents = s.forwardSecurity() | |
| 88 | 97 | for _, destination := range wire { | |
| 89 | 98 | *destination = r.securityEvents | |
| 90 | 99 | } | |
| 91 | - | for _, c := range s.latest { | |
| 92 | - | select { | |
| 93 | - | case r.contents <- c: | |
| 94 | - | default: | |
| 95 | - | r.pending = append(r.pending, c) | |
| 96 | - | } | |
| 100 | + | // After the security state above: the replay of the windows starts here. | |
| 101 | + | var contents <-chan *ports.ContentBatch | |
| 102 | + | var returns ports.ContentReturn | |
| 103 | + | if s.attach != nil { | |
| 104 | + | contents, returns = s.attach.Attach(name) | |
| 97 | 105 | } | |
| 98 | 106 | s.outs[name] = r | |
| 99 | 107 | go func() { | |
| 100 | - | r.err = run(octx, r.scenes, r.contents, r.cursor, r.captures, r.security, r.instance) | |
| 108 | + | r.err = run(octx, r.scenes, contents, returns, r.cursor, r.captures, r.security, r.instance) | |
| 101 | 109 | if r.securityEvents != nil { | |
| 102 | 110 | close(r.securityEvents) | |
| 103 | 111 | } | |
| @@ -229,51 +237,6 @@ func (s *outputSet) scenes(set []ports.Scene) { | |||
| 229 | 237 | } | |
| 230 | 238 | } | |
| 231 | 239 | ||
| 232 | - | func (s *outputSet) content(c ports.SurfaceContent) { | |
| 233 | - | if c.Empty() { | |
| 234 | - | delete(s.latest, c.ID) | |
| 235 | - | } else { | |
| 236 | - | s.latest[c.ID] = c | |
| 237 | - | } | |
| 238 | - | for _, r := range s.outs { | |
| 239 | - | if r.ctx.Err() != nil { | |
| 240 | - | continue | |
| 241 | - | } | |
| 242 | - | if len(r.pending) == 0 { | |
| 243 | - | select { | |
| 244 | - | case r.contents <- c: | |
| 245 | - | continue | |
| 246 | - | default: | |
| 247 | - | } | |
| 248 | - | } | |
| 249 | - | // The backend disabled intake until the preceding broadcast drained. | |
| 250 | - | if cap(r.pending) == 0 { | |
| 251 | - | r.pending = make([]ports.SurfaceContent, 0, 1) | |
| 252 | - | } | |
| 253 | - | r.pending = append(r.pending, c) | |
| 254 | - | } | |
| 255 | - | } | |
| 256 | - | ||
| 257 | - | // At most one admitted broadcast waits behind each owner's finite replay. | |
| 258 | - | // Disable new content intake until all copies have been delivered. | |
| 259 | - | func (s *outputSet) contentOut(incoming <-chan ports.SurfaceContent) (<-chan ports.SurfaceContent, chan<- ports.SurfaceContent, ports.SurfaceContent, *runningOutput) { | |
| 260 | - | for _, r := range s.outs { | |
| 261 | - | if len(r.pending) != 0 && r.ctx.Err() == nil { | |
| 262 | - | return nil, r.contents, r.pending[0], r | |
| 263 | - | } | |
| 264 | - | } | |
| 265 | - | return incoming, nil, ports.SurfaceContent{}, nil | |
| 266 | - | } | |
| 267 | - | ||
| 268 | - | func (r *runningOutput) contentSent() { | |
| 269 | - | r.pending[0] = ports.SurfaceContent{} | |
| 270 | - | if len(r.pending) == 1 { | |
| 271 | - | r.pending = r.pending[:0] // retain the one-slot broadcast buffer | |
| 272 | - | } else { | |
| 273 | - | r.pending = r.pending[1:] | |
| 274 | - | } | |
| 275 | - | } | |
| 276 | - | ||
| 277 | 240 | // finish forgets a stopped output and returns its error. | |
| 278 | 241 | func (s *outputSet) finish(name string) error { | |
| 279 | 242 | r := s.outs[name] | |
| @@ -283,10 +246,18 @@ func (s *outputSet) finish(name string) error { | |||
| 283 | 246 | <-r.done | |
| 284 | 247 | r.stop() | |
| 285 | 248 | s.failQueued(r) | |
| 249 | + | s.detach(name) | |
| 286 | 250 | delete(s.outs, name) | |
| 287 | 251 | return r.err | |
| 288 | 252 | } | |
| 289 | 253 | ||
| 254 | + | // detach stops the content feed of an output that has stopped. | |
| 255 | + | func (s *outputSet) detach(name string) { | |
| 256 | + | if s.attach != nil { | |
| 257 | + | s.attach.Detach(name) | |
| 258 | + | } | |
| 259 | + | } | |
| 260 | + | ||
| 290 | 261 | // wait stops every output and returns the first error. | |
| 291 | 262 | func (s *outputSet) wait() error { | |
| 292 | 263 | var first error = s.startErr | |
| @@ -301,6 +272,7 @@ func (s *outputSet) wait() error { | |||
| 301 | 272 | r.stop() | |
| 302 | 273 | <-r.done | |
| 303 | 274 | s.failQueued(r) | |
| 275 | + | s.detach(name) | |
| 304 | 276 | if first == nil && r.err != nil { | |
| 305 | 277 | first = r.err | |
| 306 | 278 | } | |
| @@ -13,16 +13,27 @@ import ( | |||||
| 13 | 13 | ||||
| 14 | // outputRun drives one output until ctx ends: it renders the scenes and | 14 | // outputRun drives one output until ctx ends: it renders the scenes and | ||
| 15 | // surface contents it receives, with the cursor the client asks for. | 15 | // surface contents it receives, with the cursor the client asks for. | ||
| 16 | - | type outputRun func(ctx context.Context, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange, captures <-chan ports.CaptureRequest, security <-chan ports.SecurityState, instance ports.OutputInstance) error | 16 | + | type outputRun func(ctx context.Context, scenes <-chan ports.Scene, contents <-chan *ports.ContentBatch, returns ports.ContentReturn, cursor <-chan ports.CursorChange, captures <-chan ports.CaptureRequest, security <-chan ports.SecurityState, instance ports.OutputInstance) error |
| 17 | + | ||||
| 18 | + | // contentAttacher connects an output to the surface contents: wayland feeds | |||
| 19 | + | // every attached output batches of window contents, starting with the latest | |||
| 20 | + | // content of each window, and takes the batches back through the returned | |||
| 21 | + | // ports.ContentReturn. Attach may block on the display goroutine: call it | |||
| 22 | + | // from outside that goroutine. It returns a nil channel when wayland is gone. | |||
| 23 | + | type contentAttacher interface { | |||
| 24 | + | Attach(name string) (<-chan *ports.ContentBatch, ports.ContentReturn) | |||
| 25 | + | Detach(name string) | |||
| 26 | + | } | |||
| 17 | 27 | ||||
| 18 | // outputSet owns the running outputs and feeds them. Core sends one scene | 28 | // outputSet owns the running outputs and feeds them. Core sends one scene | ||
| 19 | - | // per output; each goes to its output, latest first. Surface contents go to | 29 | + | // per output; each goes to its output, latest first. Surface contents reach |
| 20 | - | // every output, since windows move between outputs; the latest content of | 30 | + | // every output (windows move between outputs) straight from wayland through |
| 21 | - | // each window is kept so a new output starts with every window drawn. | 31 | + | // attach, which also replays the current windows to a new output. |
| 22 | // Only the goroutine running loop touches the set. | 32 | // Only the goroutine running loop touches the set. | ||
| 23 | type outputSet struct { | 33 | type outputSet struct { | ||
| 24 | - | outs map[string]*runningOutput | 34 | + | outs map[string]*runningOutput |
| 25 | - | latest map[ports.WindowID]ports.SurfaceContent | 35 | + | // attach is nil when no wayland server feeds contents (tests). |
| 36 | + | attach contentAttacher | |||
| 26 | // off is the latest Scene.Off per output name, kept after the output | 37 | // off is the latest Scene.Off per output name, kept after the output | ||
| 27 | // goes: it reconnects off until core's first scene (Output.StartOff). | 38 | // goes: it reconnects off until core's first scene (Output.StartOff). | ||
| 28 | off map[string]bool | 39 | off map[string]bool | ||
| @@ -50,9 +61,7 @@ type runningOutput struct { | |||||
| 50 | instance ports.OutputInstance | 61 | instance ports.OutputInstance | ||
| 51 | security chan ports.SecurityState | 62 | security chan ports.SecurityState | ||
| 52 | securityEvents chan ports.SecurityBackendEvent | 63 | securityEvents chan ports.SecurityBackendEvent | ||
| 53 | - | pending []ports.SurfaceContent | |||
| 54 | scenes chan ports.Scene | 64 | scenes chan ports.Scene | ||
| 55 | - | contents chan ports.SurfaceContent | |||
| 56 | cursor chan ports.CursorChange | 65 | cursor chan ports.CursorChange | ||
| 57 | captures chan ports.CaptureRequest | 66 | captures chan ports.CaptureRequest | ||
| 58 | ctx context.Context | 67 | ctx context.Context | ||
| @@ -62,10 +71,10 @@ type runningOutput struct { | |||||
| 62 | } | 71 | } | ||
| 63 | 72 | ||||
| 64 | func newOutputSet(ctx context.Context, captured chan<- ports.CaptureDone) *outputSet { | 73 | func newOutputSet(ctx context.Context, captured chan<- ports.CaptureDone) *outputSet { | ||
| 65 | - | return &outputSet{captured: captured, ctx: ctx, outs: map[string]*runningOutput{}, latest: map[ports.WindowID]ports.SurfaceContent{}, off: map[string]bool{}, stopped: make(chan outputStopped), quit: make(chan struct{})} | 74 | + | return &outputSet{captured: captured, ctx: ctx, outs: map[string]*runningOutput{}, off: map[string]bool{}, stopped: make(chan outputStopped), quit: make(chan struct{})} |
| 66 | } | 75 | } | ||
| 67 | 76 | ||||
| 68 | - | // start runs an output in its own goroutine and replays the window contents. | 77 | + | // start runs an output in its own goroutine, fed the window contents. |
| 69 | func (s *outputSet) start(ctx context.Context, name string, run outputRun, wire ...*chan<- ports.SecurityBackendEvent) bool { | 78 | func (s *outputSet) start(ctx context.Context, name string, run outputRun, wire ...*chan<- ports.SecurityBackendEvent) bool { | ||
| 70 | // Exhaustion is permanent: never wrap to zero or reuse a lifetime. | 79 | // Exhaustion is permanent: never wrap to zero or reuse a lifetime. | ||
| 71 | if s.nextInstance == ^ports.OutputInstance(0) { | 80 | if s.nextInstance == ^ports.OutputInstance(0) { | ||
| @@ -82,22 +91,21 @@ func (s *outputSet) start(ctx context.Context, name string, run outputRun, wire | |||||
| 82 | s.setSecurity(s.security.Snapshot()) | 91 | s.setSecurity(s.security.Snapshot()) | ||
| 83 | } | 92 | } | ||
| 84 | octx, stop := context.WithCancel(ctx) | 93 | octx, stop := context.WithCancel(ctx) | ||
| 85 | - | r := &runningOutput{instance: instance, security: make(chan ports.SecurityState, 1), scenes: make(chan ports.Scene, 1), contents: make(chan ports.SurfaceContent, 64), cursor: make(chan ports.CursorChange, 1), captures: make(chan ports.CaptureRequest, 16), ctx: octx, stop: stop, done: make(chan struct{})} | 94 | + | r := &runningOutput{instance: instance, security: make(chan ports.SecurityState, 1), scenes: make(chan ports.Scene, 1), cursor: make(chan ports.CursorChange, 1), captures: make(chan ports.CaptureRequest, 16), ctx: octx, stop: stop, done: make(chan struct{})} |
| 86 | - | r.security <- s.state // available before any old scene/content replay | 95 | + | r.security <- s.state // available before any content replay |
| 87 | r.securityEvents = s.forwardSecurity() | 96 | r.securityEvents = s.forwardSecurity() | ||
| 88 | for _, destination := range wire { | 97 | for _, destination := range wire { | ||
| 89 | *destination = r.securityEvents | 98 | *destination = r.securityEvents | ||
| 90 | } | 99 | } | ||
| 91 | - | for _, c := range s.latest { | 100 | + | // After the security state above: the replay of the windows starts here. |
| 92 | - | select { | 101 | + | var contents <-chan *ports.ContentBatch |
| 93 | - | case r.contents <- c: | 102 | + | var returns ports.ContentReturn |
| 94 | - | default: | 103 | + | if s.attach != nil { |
| 95 | - | r.pending = append(r.pending, c) | 104 | + | contents, returns = s.attach.Attach(name) |
| 96 | - | } | |||
| 97 | } | 105 | } | ||
| 98 | s.outs[name] = r | 106 | s.outs[name] = r | ||
| 99 | go func() { | 107 | go func() { | ||
| 100 | - | r.err = run(octx, r.scenes, r.contents, r.cursor, r.captures, r.security, r.instance) | 108 | + | r.err = run(octx, r.scenes, contents, returns, r.cursor, r.captures, r.security, r.instance) |
| 101 | if r.securityEvents != nil { | 109 | if r.securityEvents != nil { | ||
| 102 | close(r.securityEvents) | 110 | close(r.securityEvents) | ||
| 103 | } | 111 | } | ||
| @@ -229,51 +237,6 @@ func (s *outputSet) scenes(set []ports.Scene) { | |||||
| 229 | } | 237 | } | ||
| 230 | } | 238 | } | ||
| 231 | 239 | ||||
| 232 | - | func (s *outputSet) content(c ports.SurfaceContent) { | |||
| 233 | - | if c.Empty() { | |||
| 234 | - | delete(s.latest, c.ID) | |||
| 235 | - | } else { | |||
| 236 | - | s.latest[c.ID] = c | |||
| 237 | - | } | |||
| 238 | - | for _, r := range s.outs { | |||
| 239 | - | if r.ctx.Err() != nil { | |||
| 240 | - | continue | |||
| 241 | - | } | |||
| 242 | - | if len(r.pending) == 0 { | |||
| 243 | - | select { | |||
| 244 | - | case r.contents <- c: | |||
| 245 | - | continue | |||
| 246 | - | default: | |||
| 247 | - | } | |||
| 248 | - | } | |||
| 249 | - | // The backend disabled intake until the preceding broadcast drained. | |||
| 250 | - | if cap(r.pending) == 0 { | |||
| 251 | - | r.pending = make([]ports.SurfaceContent, 0, 1) | |||
| 252 | - | } | |||
| 253 | - | r.pending = append(r.pending, c) | |||
| 254 | - | } | |||
| 255 | - | } | |||
| 256 | - | ||||
| 257 | - | // At most one admitted broadcast waits behind each owner's finite replay. | |||
| 258 | - | // Disable new content intake until all copies have been delivered. | |||
| 259 | - | func (s *outputSet) contentOut(incoming <-chan ports.SurfaceContent) (<-chan ports.SurfaceContent, chan<- ports.SurfaceContent, ports.SurfaceContent, *runningOutput) { | |||
| 260 | - | for _, r := range s.outs { | |||
| 261 | - | if len(r.pending) != 0 && r.ctx.Err() == nil { | |||
| 262 | - | return nil, r.contents, r.pending[0], r | |||
| 263 | - | } | |||
| 264 | - | } | |||
| 265 | - | return incoming, nil, ports.SurfaceContent{}, nil | |||
| 266 | - | } | |||
| 267 | - | ||||
| 268 | - | func (r *runningOutput) contentSent() { | |||
| 269 | - | r.pending[0] = ports.SurfaceContent{} | |||
| 270 | - | if len(r.pending) == 1 { | |||
| 271 | - | r.pending = r.pending[:0] // retain the one-slot broadcast buffer | |||
| 272 | - | } else { | |||
| 273 | - | r.pending = r.pending[1:] | |||
| 274 | - | } | |||
| 275 | - | } | |||
| 276 | - | ||||
| 277 | // finish forgets a stopped output and returns its error. | 240 | // finish forgets a stopped output and returns its error. | ||
| 278 | func (s *outputSet) finish(name string) error { | 241 | func (s *outputSet) finish(name string) error { | ||
| 279 | r := s.outs[name] | 242 | r := s.outs[name] | ||
| @@ -283,10 +246,18 @@ func (s *outputSet) finish(name string) error { | |||||
| 283 | <-r.done | 246 | <-r.done | ||
| 284 | r.stop() | 247 | r.stop() | ||
| 285 | s.failQueued(r) | 248 | s.failQueued(r) | ||
| 249 | + | s.detach(name) | |||
| 286 | delete(s.outs, name) | 250 | delete(s.outs, name) | ||
| 287 | return r.err | 251 | return r.err | ||
| 288 | } | 252 | } | ||
| 289 | 253 | ||||
| 254 | + | // detach stops the content feed of an output that has stopped. | |||
| 255 | + | func (s *outputSet) detach(name string) { | |||
| 256 | + | if s.attach != nil { | |||
| 257 | + | s.attach.Detach(name) | |||
| 258 | + | } | |||
| 259 | + | } | |||
| 260 | + | ||||
| 290 | // wait stops every output and returns the first error. | 261 | // wait stops every output and returns the first error. | ||
| 291 | func (s *outputSet) wait() error { | 262 | func (s *outputSet) wait() error { | ||
| 292 | var first error = s.startErr | 263 | var first error = s.startErr | ||
| @@ -301,6 +272,7 @@ func (s *outputSet) wait() error { | |||||
| 301 | r.stop() | 272 | r.stop() | ||
| 302 | <-r.done | 273 | <-r.done | ||
| 303 | s.failQueued(r) | 274 | s.failQueued(r) | ||
| 275 | + | s.detach(name) | |||
| 304 | if first == nil && r.err != nil { | 276 | if first == nil && r.err != nil { | ||
| 305 | first = r.err | 277 | first = r.err | ||
| 306 | } | 278 | } | ||
M internal/app/outputs_test.go +1 -1
| @@ -17,7 +17,7 @@ func TestOutputSetCursor(t *testing.T) { | |||
| 17 | 17 | run := func(name string) outputRun { | |
| 18 | 18 | got[name] = make(chan ports.CursorChange, 8) | |
| 19 | 19 | out := got[name] | |
| 20 | - | return func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 20 | + | return func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, cursor <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 21 | 21 | for { | |
| 22 | 22 | select { | |
| 23 | 23 | case <-ctx.Done(): | |
| @@ -17,7 +17,7 @@ func TestOutputSetCursor(t *testing.T) { | |||||
| 17 | run := func(name string) outputRun { | 17 | run := func(name string) outputRun { | ||
| 18 | got[name] = make(chan ports.CursorChange, 8) | 18 | got[name] = make(chan ports.CursorChange, 8) | ||
| 19 | out := got[name] | 19 | out := got[name] | ||
| 20 | - | return func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | 20 | + | return func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, cursor <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { |
| 21 | for { | 21 | for { | ||
| 22 | select { | 22 | select { | ||
| 23 | case <-ctx.Done(): | 23 | case <-ctx.Done(): | ||
M internal/app/security_duplicate_test.go +1 -1
| @@ -27,7 +27,7 @@ func TestSecurityDuplicatePhysicalLifetimeRequiresConfirmedInactive(t *testing.T | |||
| 27 | 27 | events := make(chan ports.SecurityBackendEvent, 4) | |
| 28 | 28 | set := newOutputSet(context.Background(), nil) | |
| 29 | 29 | set.wireSecurity(outputChannels{security: gate, securityEvents: events}) | |
| 30 | - | run := func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 30 | + | run := func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 31 | 31 | <-ctx.Done() | |
| 32 | 32 | return nil | |
| 33 | 33 | } | |
| @@ -27,7 +27,7 @@ func TestSecurityDuplicatePhysicalLifetimeRequiresConfirmedInactive(t *testing.T | |||||
| 27 | events := make(chan ports.SecurityBackendEvent, 4) | 27 | events := make(chan ports.SecurityBackendEvent, 4) | ||
| 28 | set := newOutputSet(context.Background(), nil) | 28 | set := newOutputSet(context.Background(), nil) | ||
| 29 | set.wireSecurity(outputChannels{security: gate, securityEvents: events}) | 29 | set.wireSecurity(outputChannels{security: gate, securityEvents: events}) | ||
| 30 | - | run := func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | 30 | + | run := func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { |
| 31 | <-ctx.Done() | 31 | <-ctx.Done() | ||
| 32 | return nil | 32 | return nil | ||
| 33 | } | 33 | } | ||
M internal/app/security_hotplug_test.go +4 -4
| @@ -66,7 +66,7 @@ func TestSecurityHotplugLifetimeAndProofOrdering(t *testing.T) { | |||
| 66 | 66 | commands := make(chan []ports.SecurityBackendEvent, 1) | |
| 67 | 67 | closed := make(chan bool, 1) | |
| 68 | 68 | var ownerEvents chan<- ports.SecurityBackendEvent | |
| 69 | - | if !set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, security <-chan ports.SecurityState, instance ports.OutputInstance) error { | |
| 69 | + | if !set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, security <-chan ports.SecurityState, instance ports.OutputInstance) error { | |
| 70 | 70 | defer func() { closed <- closeSuccess }() | |
| 71 | 71 | state := <-security | |
| 72 | 72 | proof := ports.SecurityOutputProof{Proof: ports.OutputProtection{Generation: state.Generation, Instance: instance, Kind: ports.ProtectionProtectedFrame}} | |
| @@ -205,7 +205,7 @@ func securityOwnerFIFOSaturationDrainsOnNormalStop(t *testing.T) { | |||
| 205 | 205 | startDone := make(chan bool, 1) | |
| 206 | 206 | var ownerEvents chan<- ports.SecurityBackendEvent | |
| 207 | 207 | go func() { | |
| 208 | - | startDone <- set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, instance ports.OutputInstance) error { | |
| 208 | + | startDone <- set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, instance ports.OutputInstance) error { | |
| 209 | 209 | for i := range 40 { | |
| 210 | 210 | var event ports.SecurityBackendEvent = ports.SecurityOutputInvalidated{Generation: generation, Instance: instance} | |
| 211 | 211 | if i%2 == 0 { | |
| @@ -295,7 +295,7 @@ func TestSecurityOwnerBufferedEvidenceDrainsAfterFinish(t *testing.T) { | |||
| 295 | 295 | startDone := make(chan bool, 1) | |
| 296 | 296 | var ownerEvents chan<- ports.SecurityBackendEvent | |
| 297 | 297 | go func() { | |
| 298 | - | startDone <- set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, instance ports.OutputInstance) error { | |
| 298 | + | startDone <- set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, instance ports.OutputInstance) error { | |
| 299 | 299 | for i := range 16 { | |
| 300 | 300 | event := ports.SecurityOutputInvalidated{Generation: ports.LockGeneration(i + 1), Instance: instance} | |
| 301 | 301 | select { | |
| @@ -350,7 +350,7 @@ func securityOwnerFIFOSaturationCancelReleasesOwnerAndForwarders(t *testing.T) { | |||
| 350 | 350 | startDone := make(chan bool, 1) | |
| 351 | 351 | var ownerEvents chan<- ports.SecurityBackendEvent | |
| 352 | 352 | go func() { | |
| 353 | - | startDone <- set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, instance ports.OutputInstance) error { | |
| 353 | + | startDone <- set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, instance ports.OutputInstance) error { | |
| 354 | 354 | defer close(ownerDone) | |
| 355 | 355 | for i := range 40 { | |
| 356 | 356 | select { | |
| @@ -66,7 +66,7 @@ func TestSecurityHotplugLifetimeAndProofOrdering(t *testing.T) { | |||||
| 66 | commands := make(chan []ports.SecurityBackendEvent, 1) | 66 | commands := make(chan []ports.SecurityBackendEvent, 1) | ||
| 67 | closed := make(chan bool, 1) | 67 | closed := make(chan bool, 1) | ||
| 68 | var ownerEvents chan<- ports.SecurityBackendEvent | 68 | var ownerEvents chan<- ports.SecurityBackendEvent | ||
| 69 | - | if !set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, security <-chan ports.SecurityState, instance ports.OutputInstance) error { | 69 | + | if !set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, security <-chan ports.SecurityState, instance ports.OutputInstance) error { |
| 70 | defer func() { closed <- closeSuccess }() | 70 | defer func() { closed <- closeSuccess }() | ||
| 71 | state := <-security | 71 | state := <-security | ||
| 72 | proof := ports.SecurityOutputProof{Proof: ports.OutputProtection{Generation: state.Generation, Instance: instance, Kind: ports.ProtectionProtectedFrame}} | 72 | proof := ports.SecurityOutputProof{Proof: ports.OutputProtection{Generation: state.Generation, Instance: instance, Kind: ports.ProtectionProtectedFrame}} | ||
| @@ -205,7 +205,7 @@ func securityOwnerFIFOSaturationDrainsOnNormalStop(t *testing.T) { | |||||
| 205 | startDone := make(chan bool, 1) | 205 | startDone := make(chan bool, 1) | ||
| 206 | var ownerEvents chan<- ports.SecurityBackendEvent | 206 | var ownerEvents chan<- ports.SecurityBackendEvent | ||
| 207 | go func() { | 207 | go func() { | ||
| 208 | - | startDone <- set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, instance ports.OutputInstance) error { | 208 | + | startDone <- set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, instance ports.OutputInstance) error { |
| 209 | for i := range 40 { | 209 | for i := range 40 { | ||
| 210 | var event ports.SecurityBackendEvent = ports.SecurityOutputInvalidated{Generation: generation, Instance: instance} | 210 | var event ports.SecurityBackendEvent = ports.SecurityOutputInvalidated{Generation: generation, Instance: instance} | ||
| 211 | if i%2 == 0 { | 211 | if i%2 == 0 { | ||
| @@ -295,7 +295,7 @@ func TestSecurityOwnerBufferedEvidenceDrainsAfterFinish(t *testing.T) { | |||||
| 295 | startDone := make(chan bool, 1) | 295 | startDone := make(chan bool, 1) | ||
| 296 | var ownerEvents chan<- ports.SecurityBackendEvent | 296 | var ownerEvents chan<- ports.SecurityBackendEvent | ||
| 297 | go func() { | 297 | go func() { | ||
| 298 | - | startDone <- set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, instance ports.OutputInstance) error { | 298 | + | startDone <- set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, instance ports.OutputInstance) error { |
| 299 | for i := range 16 { | 299 | for i := range 16 { | ||
| 300 | event := ports.SecurityOutputInvalidated{Generation: ports.LockGeneration(i + 1), Instance: instance} | 300 | event := ports.SecurityOutputInvalidated{Generation: ports.LockGeneration(i + 1), Instance: instance} | ||
| 301 | select { | 301 | select { | ||
| @@ -350,7 +350,7 @@ func securityOwnerFIFOSaturationCancelReleasesOwnerAndForwarders(t *testing.T) { | |||||
| 350 | startDone := make(chan bool, 1) | 350 | startDone := make(chan bool, 1) | ||
| 351 | var ownerEvents chan<- ports.SecurityBackendEvent | 351 | var ownerEvents chan<- ports.SecurityBackendEvent | ||
| 352 | go func() { | 352 | go func() { | ||
| 353 | - | startDone <- set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, instance ports.OutputInstance) error { | 353 | + | startDone <- set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, instance ports.OutputInstance) error { |
| 354 | defer close(ownerDone) | 354 | defer close(ownerDone) | ||
| 355 | for i := range 40 { | 355 | for i := range 40 { | ||
| 356 | select { | 356 | select { | ||
M internal/app/security_test.go +8 -69
| @@ -21,7 +21,7 @@ func TestSecurityRegistrationBeforeStart(t *testing.T) { | |||
| 21 | 21 | started := make(chan ports.OutputInstance, 1) | |
| 22 | 22 | result := make(chan bool, 1) | |
| 23 | 23 | go func() { | |
| 24 | - | result <- set.start(ctx, "DP-1", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, security <-chan ports.SecurityState, instance ports.OutputInstance) error { | |
| 24 | + | result <- set.start(ctx, "DP-1", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, security <-chan ports.SecurityState, instance ports.OutputInstance) error { | |
| 25 | 25 | state := <-security | |
| 26 | 26 | if state != (ports.SecurityState{Generation: 7, Protected: true}) { | |
| 27 | 27 | return errors.New("missing startup protection") | |
| @@ -67,7 +67,7 @@ func TestSecurityReplacementAndStaleState(t *testing.T) { | |||
| 67 | 67 | events := make(chan ports.SecurityBackendEvent, 4) | |
| 68 | 68 | set := newOutputSet(ctx, nil) | |
| 69 | 69 | set.wireSecurity(outputChannels{securityEvents: events}) | |
| 70 | - | run := func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 70 | + | run := func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 71 | 71 | <-ctx.Done() | |
| 72 | 72 | return nil | |
| 73 | 73 | } | |
| @@ -167,7 +167,7 @@ func TestSecurityInstanceExhaustion(t *testing.T) { | |||
| 167 | 167 | events := make(chan ports.SecurityBackendEvent, 2) | |
| 168 | 168 | set.wireSecurity(outputChannels{securityEvents: events}) | |
| 169 | 169 | set.nextInstance = ^ports.OutputInstance(0) - 1 | |
| 170 | - | run := func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 170 | + | run := func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 171 | 171 | <-ctx.Done() | |
| 172 | 172 | return nil | |
| 173 | 173 | } | |
| @@ -195,26 +195,22 @@ func TestSecurityInstanceExhaustion(t *testing.T) { | |||
| 195 | 195 | } | |
| 196 | 196 | } | |
| 197 | 197 | ||
| 198 | - | // The shared display event channel and an output content queue can both be | |
| 199 | - | // saturated. Routing must retain the next content without pinning the backend | |
| 200 | - | // outside its select; a lease barrier can then finish before either is drained. | |
| 201 | - | func TestSecurityFullEventsAndContentsAllowLeaseBarrier(t *testing.T) { | |
| 198 | + | // A saturated shared display event channel must not pin the backend outside | |
| 199 | + | // its select: a lease barrier can finish before the channel is drained. | |
| 200 | + | func TestSecurityFullEventsAllowLeaseBarrier(t *testing.T) { | |
| 202 | 201 | ctx, cancel := context.WithCancel(context.Background()) | |
| 203 | 202 | defer cancel() | |
| 204 | 203 | set := newOutputSet(ctx, nil) | |
| 205 | 204 | events := make(chan ports.SecurityBackendEvent, 1) | |
| 206 | 205 | set.wireSecurity(outputChannels{securityEvents: events}) | |
| 207 | 206 | var ownerEvents chan<- ports.SecurityBackendEvent | |
| 208 | - | set.start(ctx, "DP-1", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 207 | + | set.start(ctx, "DP-1", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 209 | 208 | <-ctx.Done() | |
| 210 | 209 | return nil | |
| 211 | 210 | }, &ownerEvents) | |
| 212 | 211 | <-events // ordered registration | |
| 213 | 212 | events <- ports.SecurityBackendBarrier{Generation: 99} // blocked display | |
| 214 | 213 | r := set.outs["DP-1"] | |
| 215 | - | for i := range cap(r.contents) { | |
| 216 | - | r.contents <- ports.SurfaceContent{ID: ports.WindowID(i + 1)} | |
| 217 | - | } | |
| 218 | 214 | first := ports.SecurityOutputInvalidated{Generation: 1, Instance: r.instance} | |
| 219 | 215 | second := ports.SecurityOutputProof{Proof: ports.OutputProtection{Generation: 1, Instance: r.instance, Kind: ports.ProtectionProtectedFrame}} | |
| 220 | 216 | ownerEvents <- first | |
| @@ -225,7 +221,6 @@ func TestSecurityFullEventsAndContentsAllowLeaseBarrier(t *testing.T) { | |||
| 225 | 221 | card.EXPECT().Revoke(uint32(9)).Return(nil).Once() | |
| 226 | 222 | progressed := make(chan error, 1) | |
| 227 | 223 | go func() { | |
| 228 | - | set.content(ports.SurfaceContent{ID: 70}) | |
| 229 | 224 | set.setSecurity(ports.SecurityState{Generation: 1, Protected: true}) | |
| 230 | 225 | err := revokeSecurityLeases(card, func(uint32) {}) | |
| 231 | 226 | set.securityEvent(ports.SecurityBackendBarrier{Generation: 1, Err: err}) | |
| @@ -237,23 +232,7 @@ func TestSecurityFullEventsAndContentsAllowLeaseBarrier(t *testing.T) { | |||
| 237 | 232 | t.Fatal(err) | |
| 238 | 233 | } | |
| 239 | 234 | case <-time.After(time.Second): | |
| 240 | - | t.Fatal("content/event backpressure blocked lease transition") | |
| 241 | - | } | |
| 242 | - | incoming := make(chan ports.SurfaceContent) | |
| 243 | - | contentIn, output, next, owner := set.contentOut(incoming) | |
| 244 | - | if contentIn != nil || output != r.contents || owner != r || next.ID != 70 || len(r.pending) != 1 { | |
| 245 | - | t.Fatal("pending content not bounded/preserved") | |
| 246 | - | } | |
| 247 | - | for range cap(r.contents) { | |
| 248 | - | <-r.contents | |
| 249 | - | } | |
| 250 | - | output <- next | |
| 251 | - | owner.contentSent() | |
| 252 | - | if got := <-r.contents; got.ID != 70 { | |
| 253 | - | t.Fatal("lost pending content") | |
| 254 | - | } | |
| 255 | - | if contentIn, _, _, _ := set.contentOut(incoming); contentIn != incoming { | |
| 256 | - | t.Fatal("content intake stayed disabled") | |
| 235 | + | t.Fatal("event backpressure blocked lease transition") | |
| 257 | 236 | } | |
| 258 | 237 | <-events // unblock display | |
| 259 | 238 | var ownerSequence []ports.SecurityBackendEvent | |
| @@ -275,43 +254,3 @@ func TestSecurityFullEventsAndContentsAllowLeaseBarrier(t *testing.T) { | |||
| 275 | 254 | t.Fatal(err) | |
| 276 | 255 | } | |
| 277 | 256 | } | |
| 278 | - | ||
| 279 | - | func TestSecurityLargeReplayDoesNotBlockStart(t *testing.T) { | |
| 280 | - | set := newOutputSet(context.Background(), nil) | |
| 281 | - | for i := range 100 { | |
| 282 | - | set.content(ports.SurfaceContent{ID: ports.WindowID(i + 1), SHM: &ports.SHMBuffer{}}) | |
| 283 | - | } | |
| 284 | - | if !set.start(context.Background(), "A", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |
| 285 | - | <-ctx.Done() | |
| 286 | - | return nil | |
| 287 | - | }) { | |
| 288 | - | t.Fatal("start failed") | |
| 289 | - | } | |
| 290 | - | r := set.outs["A"] | |
| 291 | - | if len(r.contents) != cap(r.contents) || len(r.pending) != 100-cap(r.contents) { | |
| 292 | - | t.Fatal("replay not preserved") | |
| 293 | - | } | |
| 294 | - | seen := map[ports.WindowID]bool{} | |
| 295 | - | for len(seen) != 100 { | |
| 296 | - | select { | |
| 297 | - | case c := <-r.contents: | |
| 298 | - | if seen[c.ID] { | |
| 299 | - | t.Fatal("duplicate replay") | |
| 300 | - | } | |
| 301 | - | seen[c.ID] = true | |
| 302 | - | default: | |
| 303 | - | _, out, c, owner := set.contentOut(nil) | |
| 304 | - | if out == nil { | |
| 305 | - | t.Fatal("replay lost") | |
| 306 | - | } | |
| 307 | - | out <- c | |
| 308 | - | owner.contentSent() | |
| 309 | - | } | |
| 310 | - | } | |
| 311 | - | if len(r.pending) != 0 { | |
| 312 | - | t.Fatal("replay remains") | |
| 313 | - | } | |
| 314 | - | if err := set.wait(); err != nil { | |
| 315 | - | t.Fatal(err) | |
| 316 | - | } | |
| 317 | - | } | |
| @@ -21,7 +21,7 @@ func TestSecurityRegistrationBeforeStart(t *testing.T) { | |||||
| 21 | started := make(chan ports.OutputInstance, 1) | 21 | started := make(chan ports.OutputInstance, 1) | ||
| 22 | result := make(chan bool, 1) | 22 | result := make(chan bool, 1) | ||
| 23 | go func() { | 23 | go func() { | ||
| 24 | - | result <- set.start(ctx, "DP-1", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, security <-chan ports.SecurityState, instance ports.OutputInstance) error { | 24 | + | result <- set.start(ctx, "DP-1", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, security <-chan ports.SecurityState, instance ports.OutputInstance) error { |
| 25 | state := <-security | 25 | state := <-security | ||
| 26 | if state != (ports.SecurityState{Generation: 7, Protected: true}) { | 26 | if state != (ports.SecurityState{Generation: 7, Protected: true}) { | ||
| 27 | return errors.New("missing startup protection") | 27 | return errors.New("missing startup protection") | ||
| @@ -67,7 +67,7 @@ func TestSecurityReplacementAndStaleState(t *testing.T) { | |||||
| 67 | events := make(chan ports.SecurityBackendEvent, 4) | 67 | events := make(chan ports.SecurityBackendEvent, 4) | ||
| 68 | set := newOutputSet(ctx, nil) | 68 | set := newOutputSet(ctx, nil) | ||
| 69 | set.wireSecurity(outputChannels{securityEvents: events}) | 69 | set.wireSecurity(outputChannels{securityEvents: events}) | ||
| 70 | - | run := func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | 70 | + | run := func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { |
| 71 | <-ctx.Done() | 71 | <-ctx.Done() | ||
| 72 | return nil | 72 | return nil | ||
| 73 | } | 73 | } | ||
| @@ -167,7 +167,7 @@ func TestSecurityInstanceExhaustion(t *testing.T) { | |||||
| 167 | events := make(chan ports.SecurityBackendEvent, 2) | 167 | events := make(chan ports.SecurityBackendEvent, 2) | ||
| 168 | set.wireSecurity(outputChannels{securityEvents: events}) | 168 | set.wireSecurity(outputChannels{securityEvents: events}) | ||
| 169 | set.nextInstance = ^ports.OutputInstance(0) - 1 | 169 | set.nextInstance = ^ports.OutputInstance(0) - 1 | ||
| 170 | - | run := func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | 170 | + | run := func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { |
| 171 | <-ctx.Done() | 171 | <-ctx.Done() | ||
| 172 | return nil | 172 | return nil | ||
| 173 | } | 173 | } | ||
| @@ -195,26 +195,22 @@ func TestSecurityInstanceExhaustion(t *testing.T) { | |||||
| 195 | } | 195 | } | ||
| 196 | } | 196 | } | ||
| 197 | 197 | ||||
| 198 | - | // The shared display event channel and an output content queue can both be | 198 | + | // A saturated shared display event channel must not pin the backend outside |
| 199 | - | // saturated. Routing must retain the next content without pinning the backend | 199 | + | // its select: a lease barrier can finish before the channel is drained. |
| 200 | - | // outside its select; a lease barrier can then finish before either is drained. | 200 | + | func TestSecurityFullEventsAllowLeaseBarrier(t *testing.T) { |
| 201 | - | func TestSecurityFullEventsAndContentsAllowLeaseBarrier(t *testing.T) { | |||
| 202 | ctx, cancel := context.WithCancel(context.Background()) | 201 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 203 | defer cancel() | 202 | defer cancel() | ||
| 204 | set := newOutputSet(ctx, nil) | 203 | set := newOutputSet(ctx, nil) | ||
| 205 | events := make(chan ports.SecurityBackendEvent, 1) | 204 | events := make(chan ports.SecurityBackendEvent, 1) | ||
| 206 | set.wireSecurity(outputChannels{securityEvents: events}) | 205 | set.wireSecurity(outputChannels{securityEvents: events}) | ||
| 207 | var ownerEvents chan<- ports.SecurityBackendEvent | 206 | var ownerEvents chan<- ports.SecurityBackendEvent | ||
| 208 | - | set.start(ctx, "DP-1", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | 207 | + | set.start(ctx, "DP-1", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { |
| 209 | <-ctx.Done() | 208 | <-ctx.Done() | ||
| 210 | return nil | 209 | return nil | ||
| 211 | }, &ownerEvents) | 210 | }, &ownerEvents) | ||
| 212 | <-events // ordered registration | 211 | <-events // ordered registration | ||
| 213 | events <- ports.SecurityBackendBarrier{Generation: 99} // blocked display | 212 | events <- ports.SecurityBackendBarrier{Generation: 99} // blocked display | ||
| 214 | r := set.outs["DP-1"] | 213 | r := set.outs["DP-1"] | ||
| 215 | - | for i := range cap(r.contents) { | |||
| 216 | - | r.contents <- ports.SurfaceContent{ID: ports.WindowID(i + 1)} | |||
| 217 | - | } | |||
| 218 | first := ports.SecurityOutputInvalidated{Generation: 1, Instance: r.instance} | 214 | first := ports.SecurityOutputInvalidated{Generation: 1, Instance: r.instance} | ||
| 219 | second := ports.SecurityOutputProof{Proof: ports.OutputProtection{Generation: 1, Instance: r.instance, Kind: ports.ProtectionProtectedFrame}} | 215 | second := ports.SecurityOutputProof{Proof: ports.OutputProtection{Generation: 1, Instance: r.instance, Kind: ports.ProtectionProtectedFrame}} | ||
| 220 | ownerEvents <- first | 216 | ownerEvents <- first | ||
| @@ -225,7 +221,6 @@ func TestSecurityFullEventsAndContentsAllowLeaseBarrier(t *testing.T) { | |||||
| 225 | card.EXPECT().Revoke(uint32(9)).Return(nil).Once() | 221 | card.EXPECT().Revoke(uint32(9)).Return(nil).Once() | ||
| 226 | progressed := make(chan error, 1) | 222 | progressed := make(chan error, 1) | ||
| 227 | go func() { | 223 | go func() { | ||
| 228 | - | set.content(ports.SurfaceContent{ID: 70}) | |||
| 229 | set.setSecurity(ports.SecurityState{Generation: 1, Protected: true}) | 224 | set.setSecurity(ports.SecurityState{Generation: 1, Protected: true}) | ||
| 230 | err := revokeSecurityLeases(card, func(uint32) {}) | 225 | err := revokeSecurityLeases(card, func(uint32) {}) | ||
| 231 | set.securityEvent(ports.SecurityBackendBarrier{Generation: 1, Err: err}) | 226 | set.securityEvent(ports.SecurityBackendBarrier{Generation: 1, Err: err}) | ||
| @@ -237,23 +232,7 @@ func TestSecurityFullEventsAndContentsAllowLeaseBarrier(t *testing.T) { | |||||
| 237 | t.Fatal(err) | 232 | t.Fatal(err) | ||
| 238 | } | 233 | } | ||
| 239 | case <-time.After(time.Second): | 234 | case <-time.After(time.Second): | ||
| 240 | - | t.Fatal("content/event backpressure blocked lease transition") | 235 | + | t.Fatal("event backpressure blocked lease transition") |
| 241 | - | } | |||
| 242 | - | incoming := make(chan ports.SurfaceContent) | |||
| 243 | - | contentIn, output, next, owner := set.contentOut(incoming) | |||
| 244 | - | if contentIn != nil || output != r.contents || owner != r || next.ID != 70 || len(r.pending) != 1 { | |||
| 245 | - | t.Fatal("pending content not bounded/preserved") | |||
| 246 | - | } | |||
| 247 | - | for range cap(r.contents) { | |||
| 248 | - | <-r.contents | |||
| 249 | - | } | |||
| 250 | - | output <- next | |||
| 251 | - | owner.contentSent() | |||
| 252 | - | if got := <-r.contents; got.ID != 70 { | |||
| 253 | - | t.Fatal("lost pending content") | |||
| 254 | - | } | |||
| 255 | - | if contentIn, _, _, _ := set.contentOut(incoming); contentIn != incoming { | |||
| 256 | - | t.Fatal("content intake stayed disabled") | |||
| 257 | } | 236 | } | ||
| 258 | <-events // unblock display | 237 | <-events // unblock display | ||
| 259 | var ownerSequence []ports.SecurityBackendEvent | 238 | var ownerSequence []ports.SecurityBackendEvent | ||
| @@ -275,43 +254,3 @@ func TestSecurityFullEventsAndContentsAllowLeaseBarrier(t *testing.T) { | |||||
| 275 | t.Fatal(err) | 254 | t.Fatal(err) | ||
| 276 | } | 255 | } | ||
| 277 | } | 256 | } | ||
| 278 | - | ||||
| 279 | - | func TestSecurityLargeReplayDoesNotBlockStart(t *testing.T) { | |||
| 280 | - | set := newOutputSet(context.Background(), nil) | |||
| 281 | - | for i := range 100 { | |||
| 282 | - | set.content(ports.SurfaceContent{ID: ports.WindowID(i + 1), SHM: &ports.SHMBuffer{}}) | |||
| 283 | - | } | |||
| 284 | - | if !set.start(context.Background(), "A", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error { | |||
| 285 | - | <-ctx.Done() | |||
| 286 | - | return nil | |||
| 287 | - | }) { | |||
| 288 | - | t.Fatal("start failed") | |||
| 289 | - | } | |||
| 290 | - | r := set.outs["A"] | |||
| 291 | - | if len(r.contents) != cap(r.contents) || len(r.pending) != 100-cap(r.contents) { | |||
| 292 | - | t.Fatal("replay not preserved") | |||
| 293 | - | } | |||
| 294 | - | seen := map[ports.WindowID]bool{} | |||
| 295 | - | for len(seen) != 100 { | |||
| 296 | - | select { | |||
| 297 | - | case c := <-r.contents: | |||
| 298 | - | if seen[c.ID] { | |||
| 299 | - | t.Fatal("duplicate replay") | |||
| 300 | - | } | |||
| 301 | - | seen[c.ID] = true | |||
| 302 | - | default: | |||
| 303 | - | _, out, c, owner := set.contentOut(nil) | |||
| 304 | - | if out == nil { | |||
| 305 | - | t.Fatal("replay lost") | |||
| 306 | - | } | |||
| 307 | - | out <- c | |||
| 308 | - | owner.contentSent() | |||
| 309 | - | } | |||
| 310 | - | } | |||
| 311 | - | if len(r.pending) != 0 { | |||
| 312 | - | t.Fatal("replay remains") | |||
| 313 | - | } | |||
| 314 | - | if err := set.wait(); err != nil { | |||
| 315 | - | t.Fatal(err) | |||
| 316 | - | } | |||
| 317 | - | } | |||
M internal/app/session.go +1 -1
| @@ -354,7 +354,7 @@ func (s *session) run(ctx context.Context, inject func(chan<- ports.InputEvent)) | |||
| 354 | 354 | ||
| 355 | 355 | go func() { | |
| 356 | 356 | defer workers.Done() | |
| 357 | - | outputIO := p.outputs(security, server.Presentation()) | |
| 357 | + | outputIO := p.outputs(security, server.Presentation(), waylandContent{server}) | |
| 358 | 358 | apply := newOutputApply(core.NewOutputOverrides(opts.Config, hw == nil), logging.For(ctx, "app")) | |
| 359 | 359 | if hw != nil { | |
| 360 | 360 | done <- safe("output", func() error { | |
| @@ -354,7 +354,7 @@ func (s *session) run(ctx context.Context, inject func(chan<- ports.InputEvent)) | |||||
| 354 | 354 | ||||
| 355 | go func() { | 355 | go func() { | ||
| 356 | defer workers.Done() | 356 | defer workers.Done() | ||
| 357 | - | outputIO := p.outputs(security, server.Presentation()) | 357 | + | outputIO := p.outputs(security, server.Presentation(), waylandContent{server}) |
| 358 | 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")) | ||
| 359 | if hw != nil { | 359 | if hw != nil { | ||
| 360 | done <- safe("output", func() error { | 360 | done <- safe("output", func() error { | ||
M internal/app/wiring.go +3 -6
| @@ -41,7 +41,6 @@ type pipes struct { | |||
| 41 | 41 | // config file. | |
| 42 | 42 | scales chan ports.ScaleChanged | |
| 43 | 43 | ||
| 44 | - | contents chan ports.SurfaceContent | |
| 45 | 44 | // Outputs report to wayland through its presentation sink and tell core | |
| 46 | 45 | // about each flip (frames) to move running slides. | |
| 47 | 46 | frames chan ports.OutputFrame | |
| @@ -91,8 +90,7 @@ func newPipes(realSession bool) *pipes { | |||
| 91 | 90 | configured: make(chan ports.ConfigChanged, 8), | |
| 92 | 91 | configErrors: make(chan error, 8), | |
| 93 | 92 | ||
| 94 | - | contents: make(chan ports.SurfaceContent, 64), | |
| 95 | - | frames: make(chan ports.OutputFrame, 8), | |
| 93 | + | frames: make(chan ports.OutputFrame, 8), | |
| 96 | 94 | ||
| 97 | 95 | captures: make(chan ports.CaptureRequest, ports.MaxCaptureInflight), | |
| 98 | 96 | captured: make(chan ports.CaptureDone, ports.MaxCaptureInflight), | |
| @@ -142,7 +140,6 @@ func (p *pipes) wayland() wayland.Channels { | |||
| 142 | 140 | Events: p.client, | |
| 143 | 141 | Commands: p.commands, | |
| 144 | 142 | Workspaces: p.workspaces, | |
| 145 | - | Contents: p.contents, | |
| 146 | 143 | Cursors: p.cursorChanges, | |
| 147 | 144 | Captures: p.captures, | |
| 148 | 145 | Captured: p.captured, | |
| @@ -158,16 +155,16 @@ func (p *pipes) wayland() wayland.Channels { | |||
| 158 | 155 | } | |
| 159 | 156 | ||
| 160 | 157 | // outputs is the outputs' side (DRM or headless). | |
| 161 | - | func (p *pipes) outputs(security ports.SessionSecurity, presented ports.PresentationSink) outputChannels { | |
| 158 | + | func (p *pipes) outputs(security ports.SessionSecurity, presented ports.PresentationSink, content contentAttacher) outputChannels { | |
| 162 | 159 | return outputChannels{ | |
| 163 | 160 | security: security, | |
| 164 | 161 | securityChanges: p.securityChanges, | |
| 165 | 162 | securityEvents: p.securityEvents, | |
| 166 | 163 | events: p.output, | |
| 167 | 164 | scenes: p.renderScenes, | |
| 168 | - | contents: p.contents, | |
| 169 | 165 | cursorChanges: p.cursorChanges, | |
| 170 | 166 | presented: presented, | |
| 167 | + | content: content, | |
| 171 | 168 | frames: p.frames, | |
| 172 | 169 | captures: p.captures, | |
| 173 | 170 | captured: p.captured, | |
| @@ -41,7 +41,6 @@ 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 | |||
| 45 | // Outputs report to wayland through its presentation sink and tell core | 44 | // Outputs report to wayland through its presentation sink and tell core | ||
| 46 | // about each flip (frames) to move running slides. | 45 | // about each flip (frames) to move running slides. | ||
| 47 | frames chan ports.OutputFrame | 46 | frames chan ports.OutputFrame | ||
| @@ -91,8 +90,7 @@ func newPipes(realSession bool) *pipes { | |||||
| 91 | configured: make(chan ports.ConfigChanged, 8), | 90 | configured: make(chan ports.ConfigChanged, 8), | ||
| 92 | configErrors: make(chan error, 8), | 91 | configErrors: make(chan error, 8), | ||
| 93 | 92 | ||||
| 94 | - | contents: make(chan ports.SurfaceContent, 64), | 93 | + | frames: make(chan ports.OutputFrame, 8), |
| 95 | - | frames: make(chan ports.OutputFrame, 8), | |||
| 96 | 94 | ||||
| 97 | captures: make(chan ports.CaptureRequest, ports.MaxCaptureInflight), | 95 | captures: make(chan ports.CaptureRequest, ports.MaxCaptureInflight), | ||
| 98 | captured: make(chan ports.CaptureDone, ports.MaxCaptureInflight), | 96 | captured: make(chan ports.CaptureDone, ports.MaxCaptureInflight), | ||
| @@ -142,7 +140,6 @@ func (p *pipes) wayland() wayland.Channels { | |||||
| 142 | Events: p.client, | 140 | Events: p.client, | ||
| 143 | Commands: p.commands, | 141 | Commands: p.commands, | ||
| 144 | Workspaces: p.workspaces, | 142 | Workspaces: p.workspaces, | ||
| 145 | - | Contents: p.contents, | |||
| 146 | Cursors: p.cursorChanges, | 143 | Cursors: p.cursorChanges, | ||
| 147 | Captures: p.captures, | 144 | Captures: p.captures, | ||
| 148 | Captured: p.captured, | 145 | Captured: p.captured, | ||
| @@ -158,16 +155,16 @@ func (p *pipes) wayland() wayland.Channels { | |||||
| 158 | } | 155 | } | ||
| 159 | 156 | ||||
| 160 | // outputs is the outputs' side (DRM or headless). | 157 | // outputs is the outputs' side (DRM or headless). | ||
| 161 | - | func (p *pipes) outputs(security ports.SessionSecurity, presented ports.PresentationSink) outputChannels { | 158 | + | func (p *pipes) outputs(security ports.SessionSecurity, presented ports.PresentationSink, content contentAttacher) outputChannels { |
| 162 | return outputChannels{ | 159 | return outputChannels{ | ||
| 163 | security: security, | 160 | security: security, | ||
| 164 | securityChanges: p.securityChanges, | 161 | securityChanges: p.securityChanges, | ||
| 165 | securityEvents: p.securityEvents, | 162 | securityEvents: p.securityEvents, | ||
| 166 | events: p.output, | 163 | events: p.output, | ||
| 167 | scenes: p.renderScenes, | 164 | scenes: p.renderScenes, | ||
| 168 | - | contents: p.contents, | |||
| 169 | cursorChanges: p.cursorChanges, | 165 | cursorChanges: p.cursorChanges, | ||
| 170 | presented: presented, | 166 | presented: presented, | ||
| 167 | + | content: content, | |||
| 171 | frames: p.frames, | 168 | frames: p.frames, | ||
| 172 | captures: p.captures, | 169 | captures: p.captures, | ||
| 173 | captured: p.captured, | 170 | captured: p.captured, | ||
A internal/mocks/ports/surface_content_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 | + | // NewMockContentReturn creates a new instance of MockContentReturn. 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 NewMockContentReturn(t interface { | |
| 15 | + | mock.TestingT | |
| 16 | + | Cleanup(func()) | |
| 17 | + | }) *MockContentReturn { | |
| 18 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 19 | + | helper.Helper() | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | mock := &MockContentReturn{} | |
| 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 | + | // MockContentReturn is an autogenerated mock type for the ContentReturn type | |
| 36 | + | type MockContentReturn struct { | |
| 37 | + | mock.Mock | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | type MockContentReturn_Expecter struct { | |
| 41 | + | mock *mock.Mock | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | func (_m *MockContentReturn) EXPECT() *MockContentReturn_Expecter { | |
| 45 | + | return &MockContentReturn_Expecter{mock: &_m.Mock} | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // Return provides a mock function for the type MockContentReturn | |
| 49 | + | func (_mock *MockContentReturn) Return(contentBatch *ports.ContentBatch) { | |
| 50 | + | _mock.Called(contentBatch) | |
| 51 | + | return | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | // MockContentReturn_Return_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Return' | |
| 55 | + | type MockContentReturn_Return_Call struct { | |
| 56 | + | *mock.Call | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // Return is a helper method to define mock.On call | |
| 60 | + | // - contentBatch *ports.ContentBatch | |
| 61 | + | func (_e *MockContentReturn_Expecter) Return(contentBatch any) *MockContentReturn_Return_Call { | |
| 62 | + | return &MockContentReturn_Return_Call{Call: _e.mock.On("Return", contentBatch)} | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | func (_c *MockContentReturn_Return_Call) Run(run func(contentBatch *ports.ContentBatch)) *MockContentReturn_Return_Call { | |
| 66 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 67 | + | var arg0 *ports.ContentBatch | |
| 68 | + | if args[0] != nil { | |
| 69 | + | arg0 = args[0].(*ports.ContentBatch) | |
| 70 | + | } | |
| 71 | + | run( | |
| 72 | + | arg0, | |
| 73 | + | ) | |
| 74 | + | }) | |
| 75 | + | return _c | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | func (_c *MockContentReturn_Return_Call) Return() *MockContentReturn_Return_Call { | |
| 79 | + | _c.Call.Return() | |
| 80 | + | return _c | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | func (_c *MockContentReturn_Return_Call) RunAndReturn(run func(contentBatch *ports.ContentBatch)) *MockContentReturn_Return_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 | + | // NewMockContentReturn creates a new instance of MockContentReturn. 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 NewMockContentReturn(t interface { | |||
| 15 | + | mock.TestingT | |||
| 16 | + | Cleanup(func()) | |||
| 17 | + | }) *MockContentReturn { | |||
| 18 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 19 | + | helper.Helper() | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | mock := &MockContentReturn{} | |||
| 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 | + | // MockContentReturn is an autogenerated mock type for the ContentReturn type | |||
| 36 | + | type MockContentReturn struct { | |||
| 37 | + | mock.Mock | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | type MockContentReturn_Expecter struct { | |||
| 41 | + | mock *mock.Mock | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | func (_m *MockContentReturn) EXPECT() *MockContentReturn_Expecter { | |||
| 45 | + | return &MockContentReturn_Expecter{mock: &_m.Mock} | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // Return provides a mock function for the type MockContentReturn | |||
| 49 | + | func (_mock *MockContentReturn) Return(contentBatch *ports.ContentBatch) { | |||
| 50 | + | _mock.Called(contentBatch) | |||
| 51 | + | return | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | // MockContentReturn_Return_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Return' | |||
| 55 | + | type MockContentReturn_Return_Call struct { | |||
| 56 | + | *mock.Call | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | // Return is a helper method to define mock.On call | |||
| 60 | + | // - contentBatch *ports.ContentBatch | |||
| 61 | + | func (_e *MockContentReturn_Expecter) Return(contentBatch any) *MockContentReturn_Return_Call { | |||
| 62 | + | return &MockContentReturn_Return_Call{Call: _e.mock.On("Return", contentBatch)} | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | func (_c *MockContentReturn_Return_Call) Run(run func(contentBatch *ports.ContentBatch)) *MockContentReturn_Return_Call { | |||
| 66 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 67 | + | var arg0 *ports.ContentBatch | |||
| 68 | + | if args[0] != nil { | |||
| 69 | + | arg0 = args[0].(*ports.ContentBatch) | |||
| 70 | + | } | |||
| 71 | + | run( | |||
| 72 | + | arg0, | |||
| 73 | + | ) | |||
| 74 | + | }) | |||
| 75 | + | return _c | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | func (_c *MockContentReturn_Return_Call) Return() *MockContentReturn_Return_Call { | |||
| 79 | + | _c.Call.Return() | |||
| 80 | + | return _c | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | func (_c *MockContentReturn_Return_Call) RunAndReturn(run func(contentBatch *ports.ContentBatch)) *MockContentReturn_Return_Call { | |||
| 84 | + | _c.Run(run) | |||
| 85 | + | return _c | |||
| 86 | + | } | |||
M internal/ports/surface_content.go +24 -0
| @@ -108,6 +108,30 @@ type SurfaceContent struct { | |||
| 108 | 108 | DamageHistory []SeqDamage | |
| 109 | 109 | } | |
| 110 | 110 | ||
| 111 | + | // ContentBatch is the window contents wayland hands to one output: at most | |
| 112 | + | // one per window, the latest published. A content superseded before the | |
| 113 | + | // output took the batch is overwritten in place; contents carry the window's | |
| 114 | + | // Seq, so the release accounting needs only the newest one. | |
| 115 | + | // | |
| 116 | + | // Ownership moves with the batch: the sender must not touch it after the | |
| 117 | + | // send, and the receiver resets it and gives it back through ContentReturn. | |
| 118 | + | // Items is a slice, not a map: SurfaceContent is large, and a map would | |
| 119 | + | // allocate its elements again after every clear. | |
| 120 | + | type ContentBatch struct { | |
| 121 | + | Items []SurfaceContent | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | // Reset empties the batch, dropping its references to buffers, and keeps its | |
| 125 | + | // capacity for reuse. | |
| 126 | + | func (b *ContentBatch) Reset() { | |
| 127 | + | clear(b.Items) | |
| 128 | + | b.Items = b.Items[:0] | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | // ContentReturn gives a consumed batch back to wayland. Return never blocks | |
| 132 | + | // and is safe after the output was detached. | |
| 133 | + | type ContentReturn interface{ Return(*ContentBatch) } | |
| 134 | + | ||
| 111 | 135 | // BufferTransform is a wl_output.transform value: bit 0 rotates 90° | |
| 112 | 136 | // counter-clockwise, bit 1 rotates 180°, bit 2 flips around the vertical | |
| 113 | 137 | // axis first. | |
| @@ -108,6 +108,30 @@ type SurfaceContent struct { | |||||
| 108 | DamageHistory []SeqDamage | 108 | DamageHistory []SeqDamage | ||
| 109 | } | 109 | } | ||
| 110 | 110 | ||||
| 111 | + | // ContentBatch is the window contents wayland hands to one output: at most | |||
| 112 | + | // one per window, the latest published. A content superseded before the | |||
| 113 | + | // output took the batch is overwritten in place; contents carry the window's | |||
| 114 | + | // Seq, so the release accounting needs only the newest one. | |||
| 115 | + | // | |||
| 116 | + | // Ownership moves with the batch: the sender must not touch it after the | |||
| 117 | + | // send, and the receiver resets it and gives it back through ContentReturn. | |||
| 118 | + | // Items is a slice, not a map: SurfaceContent is large, and a map would | |||
| 119 | + | // allocate its elements again after every clear. | |||
| 120 | + | type ContentBatch struct { | |||
| 121 | + | Items []SurfaceContent | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | // Reset empties the batch, dropping its references to buffers, and keeps its | |||
| 125 | + | // capacity for reuse. | |||
| 126 | + | func (b *ContentBatch) Reset() { | |||
| 127 | + | clear(b.Items) | |||
| 128 | + | b.Items = b.Items[:0] | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | // ContentReturn gives a consumed batch back to wayland. Return never blocks | |||
| 132 | + | // and is safe after the output was detached. | |||
| 133 | + | type ContentReturn interface{ Return(*ContentBatch) } | |||
| 134 | + | ||||
| 111 | // BufferTransform is a wl_output.transform value: bit 0 rotates 90° | 135 | // BufferTransform is a wl_output.transform value: bit 0 rotates 90° | ||
| 112 | // counter-clockwise, bit 1 rotates 180°, bit 2 flips around the vertical | 136 | // counter-clockwise, bit 1 rotates 180°, bit 2 flips around the vertical | ||
| 113 | // axis first. | 137 | // axis first. | ||