detached 50ff603

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
59 files · +1138 -411
M .mockery.yml +6 -0
@@ -15,6 +15,7 @@ packages:
1515 interfaces:
1616 CaptureFrame:
1717 Clock:
18+ ContentReturn:
1819 Notifier:
1920 OutputScaleStore:
2021 PresentationSink:
@@ -97,6 +98,11 @@ packages:
9798 dir: internal/app
9899 pkgname: app
99100 filename: leaseinventory_mock_test.go
101+ contentAttacher:
102+ config:
103+ dir: internal/app
104+ pkgname: app
105+ filename: contentattacher_mock_test.go
100106 github.com/bnema/neferwl/internal/adapters/statefile:
101107 interfaces:
102108 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/app98 dir: internal/app
98 pkgname: app99 pkgname: app
99 filename: leaseinventory_mock_test.go100 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:
6969 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }
7070 # Fast, mandatory guards for tiled content publications and SHM copies.
7171 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
7373 check: vet test arch fakes-check log-check perf-check
7474
7575 # 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=172+ 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-check73 check: vet test arch fakes-check log-check perf-check
74 74
75 # On-demand SDR/HDR colour check with examples/testpattern on a headless75 # 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)
431431 defer cancel()
432432 done := make(chan error, 1)
433433 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)
435435 }()
436436 for {
437437 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) {
363363 o.NewCaptureRenderer = func(int, int) (ports.Renderer, error) { return child, nil }
364364
365365 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
367367 captures := make(chan ports.CaptureRequest) // unbuffered: a send returns once Run took it
368368 captured := make(chan ports.CaptureDone, 4)
369369 sink, presented := presentedSink(t, 64)
370370 ctx, cancel := context.WithCancel(context.Background())
371371 done := make(chan error, 1)
372372 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)
374374 }()
375375 count := func() int {
376376 commitMu.Lock()
@@ -379,7 +379,7 @@ func TestRunChildHoldLiftedWhileDisplayCommitStalls(t *testing.T) {
379379 }
380380 waitFor(t, func() bool { return count() == 2 })
381381
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{}})
383383 // The request waits for the scene; once a display frame is pending no
384384 // request is served, so it must be queued before the scene arrives.
385385 req := sessionRequest(t, 1, false)
@@ -411,7 +411,7 @@ func TestRunChildHoldLiftedWhileDisplayCommitStalls(t *testing.T) {
411411
412412 // A newer buffer of a display window arrives while the display commit is
413413 // 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{}})
415415 _, err = writeEnd.Write([]byte{1}) // the child's GPU work finished
416416 require.NoError(t, err)
417417 lifted := next(func(r ports.OutputPresented) bool { return len(r.ChildReads) == 0 })
@@ -487,7 +487,7 @@ func runCaptureWaitsForItsIndicatorScene(t *testing.T) {
487487 ctx, cancel := context.WithCancel(context.Background())
488488 done := make(chan error, 1)
489489 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)
491491 }()
492492 count := func() int {
493493 commitMu.Lock()
@@ -563,7 +563,7 @@ func TestRunCaptureWithoutIndicatorFailsAfterTheBoundedWait(t *testing.T) {
563563 ctx, cancel := context.WithCancel(context.Background())
564564 done := make(chan error, 1)
565565 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)
567567 }()
568568 count := func() int {
569569 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 scene366+ contents := newContentFeed(t, 0) // unbuffered: taken before the scene
367 captures := make(chan ports.CaptureRequest) // unbuffered: a send returns once Run took it367 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 no383 // 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 is412 // 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 finished415 _, 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) {
400400 ctx, cancel := context.WithCancel(context.Background())
401401 done := make(chan error, 1)
402402 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)
404404 }()
405405 count := func(flags uint32) int {
406406 commitMu.Lock()
@@ -516,12 +516,12 @@ func runReportsSeenAfterCursorCommit(t *testing.T) {
516516 return commitRec{}, false
517517 }
518518 scenes := make(chan ports.Scene, 1)
519- contents := make(chan ports.SurfaceContent, 1)
519+ contents := newContentFeed(t, 1)
520520 sink, presented := presentedSink(t, 8)
521521 ctx, cancel := context.WithCancel(context.Background())
522522 done := make(chan error, 1)
523523 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)
525525 }()
526526 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1}
527527 var frame commitRec
@@ -532,7 +532,7 @@ func runReportsSeenAfterCursorCommit(t *testing.T) {
532532 var cur commitRec
533533 waitFor(t, func() bool { var ok bool; cur, ok = last(userState); return ok })
534534 // 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{}})
536536 synctest.Wait()
537537 flips <- eventOf(cur)
538538 select {
@@ -605,7 +605,7 @@ func TestRunClosesFrameFences(t *testing.T) {
605605 ctx, cancel := context.WithCancel(context.Background())
606606 done := make(chan error, 1)
607607 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)
609609 }()
610610 count := func() int {
611611 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{}, false516 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 commitRec527 var frame commitRec
@@ -532,7 +532,7 @@ func runReportsSeenAfterCursorCommit(t *testing.T) {
532 var cur commitRec532 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 }
10091009
10101010 // Run renders scenes and commits them until ctx ends. active reports seat
10111011 // 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) {
10131013 defer func() {
10141014 p := recover()
10151015 if p != nil {
@@ -1435,14 +1435,19 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
14351435 o.setCursor(r, loadCursor, want, cursorScale, cursorTransform)
14361436 stateDirty = true
14371437 }
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)
14431448 }
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)
14461451 case <-stats.C():
14471452 now := clk.Now()
14481453 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 seat1010 // 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 = true1436 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.Seq1441+ table.Update(c, scene)
1442- reportDirty = true1442+ 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 goes1453 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) {
597597 return n
598598 }
599599 scenes := make(chan ports.Scene, 1)
600- contents := make(chan ports.SurfaceContent, 1)
600+ contents := newContentFeed(t, 1)
601601 sink, presented := presentedSink(t, 8)
602602 ctx, cancel := context.WithCancel(context.Background())
603603 done := make(chan error, 1)
604604 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)
606606 }()
607607 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
608608 waitFor(t, func() bool { return frameCommits() == 1 })
609609 // 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{}})
611613 synctest.Wait()
612614 select {
615+ case <-contents.returned:
616+ default:
617+ t.Fatal("batch not returned")
618+ }
619+ select {
613620 case p := <-presented:
614621 t.Fatalf("report while a frame is in flight: %+v", p)
615622 default:
@@ -620,7 +627,7 @@ func runReportsSeenAfterFlip(t *testing.T) {
620627 flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
621628 select {
622629 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 {
624631 t.Fatalf("report %+v", p)
625632 }
626633 case <-time.After(2 * time.Second):
@@ -679,7 +686,7 @@ func TestRunComposedFullscreenKeepsVRR(t *testing.T) {
679686 ctx, cancel := context.WithCancel(context.Background())
680687 done := make(chan error, 1)
681688 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)
683690 }()
684691 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100,
685692 Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}},
@@ -761,7 +768,7 @@ func TestRunTrimsRendererOnStatsTick(t *testing.T) {
761768 active <- false
762769 done := make(chan error, 1)
763770 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)
765772 }()
766773 ticks <- now
767774 <-trimmed
@@ -815,7 +822,7 @@ func TestCaptureRenderFailureClosesPendingRequest(t *testing.T) {
815822 }
816823 done := make(chan error, 1)
817824 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)
819826 }()
820827 // The unbuffered handoff proves Run owns the request before any scene.
821828 select {
@@ -881,7 +888,7 @@ func TestCaptureForcesDRMComposition(t *testing.T) {
881888 ctx, cancel := context.WithCancel(context.Background())
882889 done := make(chan error, 1)
883890 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)
885892 }()
886893 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Background: "#000000"}
887894 select {
@@ -941,7 +948,7 @@ func TestRunPanicReportsReadinessError(t *testing.T) {
941948 t.Fatal("missing readiness")
942949 }
943950 }()
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)
945952 }
946953
947954 // stampRun starts Run on an output with unbuffered scene and content
@@ -951,7 +958,7 @@ func TestRunPanicReportsReadinessError(t *testing.T) {
951958 type stampRun struct {
952959 o *Output
953960 scenes chan ports.Scene
954- contents chan ports.SurfaceContent
961+ contents *contentFeed
955962 flips chan flipEvent
956963 wake chan ports.SecurityState
957964 state *atomic.Uint64
@@ -963,7 +970,7 @@ type stampRun struct {
963970 func startStampRun(t *testing.T) *stampRun {
964971 o, k, commits, commitMu := testOutputMu(t)
965972 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{}}
967974 o.flipped = sr.flips
968975 securityGate(t, o, sr.state)
969976 o.SecurityChanges = sr.wake
@@ -1001,7 +1008,7 @@ func startStampRun(t *testing.T) *stampRun {
10011008 ctx, cancel := context.WithCancel(context.Background())
10021009 done := make(chan error, 1)
10031010 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)
10051012 }()
10061013 // stop ends Run: its state is ours to read afterwards.
10071014 sr.stop = func() {
@@ -1025,7 +1032,7 @@ func TestRunStampsWhenFrameWasWanted(t *testing.T) {
10251032 scene.Seq = 2
10261033 before := monotonic()
10271034 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
10291036 flipSent := monotonic()
10301037 sr.flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
10311038 waitFor(t, func() bool { return sr.frameCommit() == 2 })
@@ -1048,11 +1055,11 @@ func TestRunSecurityResetClearsWantedAt(t *testing.T) {
10481055 waitFor(t, func() bool { return sr.frameCommit() == 1 })
10491056 scene.Seq = 2
10501057 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
10521059 // A new security generation, not protected.
10531060 sr.state.Store(2)
10541061 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
10561063 sr.stop()
10571064 if sr.o.wantedAt != 0 {
10581065 t.Fatalf("stamp kept across the security reset: %s", sr.o.wantedAt)
@@ -1070,8 +1077,8 @@ func TestOutputSameSeqRendersOnce(t *testing.T) {
10701077 sr.scenes <- scene
10711078 // Unshown content only sends Run round its loop: the flip and the held
10721079 // 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{}})
10751082 if n := sr.frameCommit(); n != 1 {
10761083 t.Fatalf("%d frames after a scene with the held Seq", n)
10771084 }
@@ -1129,11 +1136,11 @@ func TestRunKeepsLastContentOfLeavingWindow(t *testing.T) {
11291136 return c, ok
11301137 }
11311138 scenes := make(chan ports.Scene, 1)
1132- contents := make(chan ports.SurfaceContent, 1)
1139+ contents := newContentFeed(t, 1)
11331140 ctx, cancel := context.WithCancel(context.Background())
11341141 done := make(chan error, 1)
11351142 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)
11371144 }()
11381145 acq, acqW, _ := os.Pipe()
11391146 defer acq.Close()
@@ -1141,11 +1148,11 @@ func TestRunKeepsLastContentOfLeavingWindow(t *testing.T) {
11411148 win := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 10, H: 10}}
11421149 scenes <- ports.Scene{Seq: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{win}}
11431150 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)
11451152 drawn(1, func(c ports.SurfaceContent, ok bool) bool { return ok && c.Acquire == acq })
11461153 // The window unmaps: its empty content arrives before the scene that
11471154 // 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})
11491156 kept, _ := drawn(1, func(c ports.SurfaceContent, ok bool) bool { return !ok || c.Acquire == nil })
11501157 if kept.DMABuf != full.DMABuf || kept.Seq != 1 {
11511158 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 n597 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 <- false768 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 <- now773 ticks <- now
767 <-trimmed774 <-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 content954 // 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 *Output959 o *Output
953 scenes chan ports.Scene960 scenes chan ports.Scene
954- contents chan ports.SurfaceContent961+ contents *contentFeed
955 flips chan flipEvent962 flips chan flipEvent
956 wake chan ports.SecurityState963 wake chan ports.SecurityState
957 state *atomic.Uint64964 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, false972 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.flips974 o.flipped = sr.flips
968 securityGate(t, o, sr.state)975 securityGate(t, o, sr.state)
969 o.SecurityChanges = sr.wake976 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 = 21032 scene.Seq = 2
1026 before := monotonic()1033 before := monotonic()
1027 sr.scenes <- scene1034 sr.scenes <- scene
1028- sr.contents <- ports.SurfaceContent{ID: 1, Seq: 1, SHM: &ports.SHMBuffer{}} // Run went round its loop1035+ 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 = 21056 scene.Seq = 2
1050 sr.scenes <- scene1057 sr.scenes <- scene
1051- sr.contents <- ports.SurfaceContent{ID: 1, Seq: 1, SHM: &ports.SHMBuffer{}} // stamped by now1058+ 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 reset1062+ 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 <- scene1077 sr.scenes <- scene
1071 // Unshown content only sends Run round its loop: the flip and the held1078 // 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, ok1136 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 <- full1151+ 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 that1153 // 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) {
5050 ctx, cancel := context.WithCancel(context.Background())
5151 done := make(chan error, 1)
5252 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)
5454 }()
5555 on := ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1}
5656 scenes <- on
@@ -174,7 +174,7 @@ func runOutputStartOff(t *testing.T) {
174174 ctx, cancel := context.WithCancel(context.Background())
175175 done := make(chan error, 1)
176176 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)
178178 }()
179179 if err := <-ready; err != nil {
180180 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 <- on56 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
4343 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
4444 done := make(chan error, 1)
4545 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)
4747 }()
4848 return events, done, cancel
4949 }
@@ -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, cancel48 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 {
9696 ctx, cancel := context.WithCancel(context.Background())
9797 done := make(chan error, 1)
9898 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)
100100 }()
101101 synctest.Wait()
102102 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) {
8585 ctx, cancel := context.WithCancel(context.Background())
8686 done := make(chan error, 1)
8787 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)
8989 }()
9090 if !o.PrepareSeatDisable(5 * time.Second) {
9191 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) {
6161 scenes := make(chan ports.Scene, 1)
6262 done := make(chan error, 1)
6363 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)
6565 }()
6666 receive := func() ports.SecurityBackendEvent {
6767 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) {
6161 defer cancel()
6262 done := make(chan error, 1)
6363 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)
6565 }()
6666 active <- true
6767 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 <- true66 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) {
5757 defer cancel()
5858 done := make(chan error, 1)
5959 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)
6161 }()
6262 scenes <- ports.Scene{Seq: 99, Scale: 1}
6363 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) {
4343 defer cancel()
4444 done := make(chan error, 1)
4545 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)
4747 }()
4848 select {
4949 case ev := <-events:
@@ -107,7 +107,7 @@ func TestSecurityStartupClearWaitUsesRunContext(t *testing.T) {
107107 r.EXPECT().Close().Return().Once()
108108 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
109109 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)
111111 if !errors.Is(err, context.Canceled) {
112112 t.Fatalf("startup cancellation: %v", err)
113113 }
@@ -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 {
3030 type stuckRun struct {
3131 t *testing.T
3232 scenes chan ports.Scene
33- contents chan ports.SurfaceContent
33+ contents *contentFeed
3434 active chan bool
3535 presented chan ports.OutputPresented
3636 sink ports.PresentationSink
@@ -68,12 +68,12 @@ func startStuck(t *testing.T, render func() *os.File, errs ...error) *stuckRun {
6868 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
6969 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
7070 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}
7272 h.sink, h.presented = presentedSink(t, 64)
7373 ctx, cancel := context.WithCancel(context.Background())
7474 done := make(chan error, 1)
7575 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)
7777 }()
7878 h.count = func() int {
7979 commitMu.Lock()
@@ -196,7 +196,7 @@ func TestSeenWaitsForRefusedFrameFence(t *testing.T) {
196196 h.scenes <- ports.Scene{Seq: 1}
197197 // EBUSY, then the retry loses DRM master: nothing is pending.
198198 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})
200200 select {
201201 case p := <-h.presented:
202202 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.T31 t *testing.T
32 scenes chan ports.Scene32 scenes chan ports.Scene
33- contents chan ports.SurfaceContent33+ contents *contentFeed
34 active chan bool34 active chan bool
35 presented chan ports.OutputPresented35 presented chan ports.OutputPresented
36 sink ports.PresentationSink36 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) {
115115 ctx, cancel := context.WithCancel(context.Background())
116116 done := make(chan error, 1)
117117 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)
119119 }()
120120 // A composed fullscreen window: a game frame under VRR.
121121 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) {
2727 r.EXPECT().Close().Return().Once()
2828 done := make(chan error, 1)
2929 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)
3131 }()
3232 select {
3333 case <-attempted:
@@ -64,7 +64,7 @@ func TestCaptureBatchBoundedAndRecycledWhileIdle(t *testing.T) {
6464 replies := make(chan ports.CaptureDone, capture.MaxRequests+1)
6565 done := make(chan error, 1)
6666 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)
6868 }()
6969 for i := range capture.MaxRequests + 1 {
7070 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
7474 replies := make(chan ports.CaptureDone, 8)
7575 done := make(chan error, 1)
7676 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)
7878 }()
7979 // Requests wait for a scene: all of them belong to its first frame.
8080 for _, q := range reqs {
@@ -168,7 +168,7 @@ func TestHiddenWorkspaceCaptureUsesChildRenderer(t *testing.T) {
168168 done <- Run(ctx, Options{Width: 2, Height: 2, Captured: replies, Presented: sink,
169169 NewRenderer: func(int, int) (ports.Renderer, error) { return display, nil },
170170 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)
172172 }()
173173 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)}
174174 incoming <- q
@@ -241,7 +241,7 @@ func TestRenderErrorDoesNotAnswerHandedRequestsAgain(t *testing.T) {
241241 done := make(chan error, 1)
242242 go func() {
243243 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)
245245 }()
246246 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)}
247247 scenes <- sessionScene()
@@ -312,7 +312,7 @@ func TestCaptureWaitsForItsIndicatorScene(t *testing.T) {
312312 replies := make(chan ports.CaptureDone, 4)
313313 done := make(chan error, 1)
314314 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)
316316 }()
317317 bare := ports.Scene{Seq: 2, Scale: 1, OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1}}}
318318 scenes <- bare
@@ -356,7 +356,7 @@ func TestCaptureWithoutIndicatorFailsAfterTheBoundedWait(t *testing.T) {
356356 replies := make(chan ports.CaptureDone, 4)
357357 done := make(chan error, 1)
358358 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)
360360 }()
361361 scenes <- ports.Scene{Seq: 2, Scale: 1, OutputWidth: 2, OutputHeight: 2}
362362 start := time.Now()
@@ -406,7 +406,7 @@ func TestCaptureDeliveredOnlyWhenTheIndicatedFramePresents(t *testing.T) {
406406 replies := make(chan ports.CaptureDone, 4)
407407 done := make(chan error, 1)
408408 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)
410410 }()
411411 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)}
412412 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 <- q174 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 <- bare318 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
2121 done := make(chan error, 1)
2222 go func() {
2323 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)
2525 }()
2626 return scenes, frames, rec, func() {
2727 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 {
6060 // windows it no longer draws.
6161 const trimEvery = 10 * time.Second
6262
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 {
6464 r, err := opts.NewRenderer(opts.Width, opts.Height)
6565 if err != nil {
6666 return fmt.Errorf("create renderer: %w", err)
@@ -192,10 +192,15 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
192192 // A closed window's last content goes once no scene draws it.
193193 table.Prune(scene)
194194 }
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)
199204 }
200205 clk := opts.Clock
201206 if clk == nil {
@@ -320,7 +325,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
320325 return err
321326 }
322327 acceptScene(s)
323- case c, ok := <-contents:
328+ case b, ok := <-contents:
324329 if !ok {
325330 contents = nil
326331 continue
@@ -331,7 +336,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
331336 }
332337 return err
333338 }
334- update(c)
339+ update(b)
335340 case c, ok := <-cursor:
336341 if !ok {
337342 cursor = nil
@@ -362,7 +367,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
362367 }
363368 acceptScene(s)
364369 }
365- case c, ok := <-contents:
370+ case b, ok := <-contents:
366371 if !ok {
367372 contents = nil
368373 } else {
@@ -372,7 +377,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
372377 }
373378 return err
374379 }
375- update(c)
380+ update(b)
376381 }
377382 default:
378383 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.Second61 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.Clock205 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 err325 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 = nil330 contents = nil
326 continue331 continue
@@ -331,7 +336,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
331 }336 }
332 return err337 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 = nil342 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 = nil372 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 err378 return err
374 }379 }
375- update(c)380+ update(b)
376 }381 }
377 default:382 default:
378 break drain383 break drain
M internal/adapters/headless/output_test.go +59 -23
@@ -71,17 +71,17 @@ func waitFrames(t *testing.T, f *frames, n int) {
7171 func TestRun(t *testing.T) {
7272 dir := t.TempDir()
7373 scenes := make(chan ports.Scene, 8)
74- contents := make(chan ports.SurfaceContent, 8)
74+ contents := newContentFeed(t, 8)
7575 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}})
7777 shown := []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}, {ID: 2, Rect: ports.Rect{W: 2, H: 2}}}
7878 scenes <- ports.Scene{Seq: 1, OutputWidth: 2, OutputHeight: 2, Windows: shown}
7979 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}})
8181 ctx, cancel := context.WithCancel(context.Background())
8282 done := make(chan error, 1)
8383 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)
8585 }()
8686 waitFrames(t, f, 1)
8787 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) {
8989 }
9090 // An empty content of a window the scene still draws (it unmapped
9191 // 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})
9393 waitFrames(t, f, 2)
9494 if _, c := f.snapshot(); len(c) < 2 {
9595 t.Fatal("no second frame")
@@ -123,20 +123,20 @@ func TestRun(t *testing.T) {
123123 // A scene whose Seq the output already holds is not drawn again.
124124 func TestHeadlessSameSeqRendersOnce(t *testing.T) {
125125 scenes := make(chan ports.Scene)
126- contents := make(chan ports.SurfaceContent)
126+ contents := newContentFeed(t, 0)
127127 r, f := recordingRenderer(t, nil)
128128 ctx, cancel := context.WithCancel(context.Background())
129129 done := make(chan error, 1)
130130 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)
132132 }()
133133 scene := ports.Scene{Seq: 5, OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}}}
134134 scenes <- scene
135135 waitFrames(t, f, 1)
136136 scenes <- scene
137137 // 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{}})
140140 if s, _ := f.snapshot(); len(s) != 1 {
141141 t.Fatalf("%d frames for a scene with the held Seq", len(s))
142142 }
@@ -155,22 +155,22 @@ func TestHeadlessSameSeqRendersOnce(t *testing.T) {
155155 // Content of a window the output does not show draws no frame.
156156 func TestRunSkipsContentNotShown(t *testing.T) {
157157 scenes := make(chan ports.Scene, 1)
158- contents := make(chan ports.SurfaceContent, 2)
158+ contents := newContentFeed(t, 2)
159159 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}}}
160160 r, f := recordingRenderer(t, nil)
161161 ctx, cancel := context.WithCancel(context.Background())
162162 done := make(chan error, 1)
163163 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)
165165 }()
166166 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}})
169169 time.Sleep(50 * time.Millisecond)
170170 if s, _ := f.snapshot(); len(s) != 1 {
171171 t.Fatalf("%d frames for content not shown", len(s))
172172 }
173- contents <- ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{Pool: 1}}
173+ contents.send(ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{Pool: 1}})
174174 waitFrames(t, f, 2)
175175 if _, c := f.snapshot(); c[1][2].SHM == nil {
176176 t.Error("content not shown was dropped")
@@ -185,11 +185,11 @@ func TestErrors(t *testing.T) {
185185 expected := errors.New("failure")
186186 scenes := make(chan ports.Scene, 1)
187187 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) {
189189 t.Fatal(err)
190190 }
191191 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) {
193193 t.Fatal(err)
194194 }
195195 }
@@ -220,7 +220,7 @@ func TestRunTrimsRendererOnTick(t *testing.T) {
220220 var statsLog bytes.Buffer // written by Run's goroutine, read after it ends
221221 log := zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: &statsLog})
222222 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)
224224 }()
225225 ticks <- now
226226 if got := <-trimmed; !got.Equal(now) {
@@ -272,15 +272,15 @@ func TestWritePNGAtomic(t *testing.T) {
272272
273273 func TestRunPassesLayerContent(t *testing.T) {
274274 scenes := make(chan ports.Scene, 1)
275- contents := make(chan ports.SurfaceContent, 1)
275+ contents := newContentFeed(t, 1)
276276 layer := ports.SceneLayer{ID: 42, Layer: ports.LayerTop, Rect: ports.Rect{W: 2, H: 2}}
277277 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}})
279279 r, f := recordingRenderer(t, nil)
280280 ctx, cancel := context.WithCancel(context.Background())
281281 done := make(chan error, 1)
282282 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)
284284 }()
285285 deadline := time.After(3 * time.Second)
286286 for {
@@ -316,7 +316,7 @@ func TestCaptureForcesFreshHeadlessFrame(t *testing.T) {
316316 ctx, cancel := context.WithCancel(context.Background())
317317 done := make(chan error, 1)
318318 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)
320320 }()
321321 scenes <- ports.Scene{Background: "#000000"}
322322 waitFrames(t, frames, 1)
@@ -377,7 +377,7 @@ func TestHeadlessRawHDRScreenshot(t *testing.T) {
377377 ctx, cancel := context.WithCancel(context.Background())
378378 done := make(chan error, 1)
379379 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)
381381 }()
382382 select {
383383 case <-rendered:
@@ -452,7 +452,7 @@ func TestHeadlessHDRFormatsConfirmed(t *testing.T) {
452452 ctx, cancel := context.WithCancel(context.Background())
453453 done := make(chan error, 1)
454454 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)
456456 }()
457457 select {
458458 case f := <-formats:
@@ -472,3 +472,39 @@ func TestHeadlessHDRFormatsConfirmed(t *testing.T) {
472472 })
473473 }
474474 }
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 unmapped90 // 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 <- scene134 scenes <- scene
135 waitFrames(t, f, 1)135 waitFrames(t, f, 1)
136 scenes <- scene136 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 ends220 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 <- now225 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) {
6161 incoming := make(chan ports.CaptureRequest)
6262 replies := make(chan ports.CaptureDone, 2)
6363 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)
6565 }()
6666 dst := sessionCaptureFile(t)
6767 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
124124 ctx, cancel := context.WithCancel(context.Background())
125125 done := make(chan error, 1)
126126 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)
128128 }()
129129 dst := sessionCaptureFile(t)
130130 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) {
4646 }
4747 // Native capture and Pixels have no expectations; neither is admitted.
4848 scenes := make(chan ports.Scene, 2)
49- contents := make(chan ports.SurfaceContent, 1)
49+ contents := newContentFeed(t, 1)
5050 incoming := make(chan ports.CaptureRequest)
5151 sink, presented := presentedSink(t, 8)
5252 proofs := make(chan ports.SecurityBackendEvent, 2)
@@ -56,11 +56,11 @@ func TestExcludeFenceRetainsReadsAcrossEngageAndOff(t *testing.T) {
5656 defer cancel()
5757 done := make(chan error, 1)
5858 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)
6060 }()
6161 dst := sessionCaptureFile(t)
6262 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}})
6464 scene := sessionScene()
6565 scene.Security = admitted
6666 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 = admitted65 scene.Security = admitted
66 scene.Scale = 166 scene.Scale = 1
M internal/adapters/headless/security_test.go +9 -9
@@ -58,14 +58,14 @@ func TestSecurityInitialBlackFenceBeforeLockerAndNoReadback(t *testing.T) {
5858 scenes := make(chan ports.Scene, 3)
5959 scenes <- ports.Scene{Seq: 1, Windows: []ports.SceneWindow{{ID: 1}}}
6060 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)
6262 acquire, acquireSignal, err := os.Pipe()
6363 if err != nil {
6464 t.Fatal(err)
6565 }
6666 defer acquire.Close()
6767 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})
6969 proof := make(chan ports.SecurityBackendEvent, 4)
7070 replies := make(chan ports.CaptureDone, 4)
7171 requests := make(chan ports.CaptureRequest, 2)
@@ -78,7 +78,7 @@ func TestSecurityInitialBlackFenceBeforeLockerAndNoReadback(t *testing.T) {
7878 done := make(chan error, 1)
7979 dir := t.TempDir()
8080 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)
8282 }()
8383 t.Cleanup(func() {
8484 cancel()
@@ -150,7 +150,7 @@ func TestSecurityChangesWakeIdleAndRejectQueuedDesktop(t *testing.T) {
150150 ctx, cancel := context.WithCancel(context.Background())
151151 done := make(chan error, 1)
152152 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)
154154 }()
155155 t.Cleanup(func() {
156156 cancel()
@@ -191,7 +191,7 @@ func TestSecurityDedicatedChangeWakesWithoutScene(t *testing.T) {
191191 ctx, cancel := context.WithCancel(context.Background())
192192 done := make(chan error, 1)
193193 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)
195195 }()
196196 locked := ports.SecurityState{Generation: 2, Protected: true}
197197 state.Store(locked)
@@ -289,7 +289,7 @@ func TestSecurityRawHDRSuppressed(t *testing.T) {
289289 ctx, cancel := context.WithCancel(context.Background())
290290 done := make(chan error, 1)
291291 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)
293293 }()
294294 // The black protection frame follows the frame under test (or is
295295 // the only one): the raw block is over once it is rendered.
@@ -336,7 +336,7 @@ func TestSecurityQueuedCaptureFailedOnceAndProofCancellation(t *testing.T) {
336336 ctx, cancel := context.WithCancel(context.Background())
337337 done := make(chan error, 1)
338338 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)
340340 }()
341341 file, err := os.CreateTemp(t.TempDir(), "queued")
342342 if err != nil {
@@ -384,7 +384,7 @@ func TestSecurityOffOutputProofDoesNotRender(t *testing.T) {
384384 ctx, cancel := context.WithCancel(context.Background())
385385 done := make(chan error, 1)
386386 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)
388388 }()
389389 securityReceive(t, presented) // Off has actually been applied by the owner.
390390 locked := ports.SecurityState{Generation: 4, Protected: true}
@@ -442,7 +442,7 @@ func TestSecurityCompletionLeaseRecycledBeforeProtectionRenderFailure(t *testing
442442 defer cancel()
443443 done := make(chan error, 1)
444444 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)
446446 }()
447447 file, err := os.CreateTemp(t.TempDir(), "capture")
448448 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 is294 // 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 @@
11 package wayland
22
33 import (
4- "context"
54 "testing"
65 "time"
76
@@ -88,18 +87,16 @@ func TestCommitSyncStorageIsPerUpdate(t *testing.T) {
8887
8988 func TestEmitContentAllocations(t *testing.T) {
9089 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{},
9693 }
9794 c := ports.SurfaceContent{ID: 1, Solid: &ports.SolidColor{}}
9895 var held []ports.SurfaceContent // readers retaining published histories
9996 var snapshots [][]ports.SeqDamage
10097 for range 3 * damageHistoryCap {
10198 s.emitContent(c, damage{full: true})
102- got := s.contents[1]
99+ got := s.latest[1]
103100 held = append(held, got)
104101 snapshots = append(snapshots, append([]ports.SeqDamage(nil), got.DamageHistory...))
105102 }
@@ -123,31 +120,3 @@ func TestEmitContentAllocations(t *testing.T) {
123120 }
124121 t.Logf("emitContent: %.2f allocs", allocs)
125122 }
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 wayland1 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 histories95 var held []ports.SurfaceContent // readers retaining published histories
99 var snapshots [][]ports.SeqDamage96 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 (
1515
1616 var linearARGB = ports.DMABufFormat{Format: 'A' | 'R'<<8 | '2'<<16 | '4'<<24}
1717
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) {
1919 t.Helper()
2020 dir := t.TempDir()
2121 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"))
2423 if err != nil {
2524 t.Fatal(err)
2625 }
26+ contents := relayContents(t, s)
2727 ctx, cancel := context.WithCancel(context.Background())
2828 done := make(chan error, 1)
2929 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
8181 root := s.root()
8282 win := root.windowID()
8383 if win != 0 && fresh {
84- srv.contentMu.Lock()
8584 seq := srv.contentSeq[win]
86- srv.contentMu.Unlock()
8785 for i := range srv.feedbacks {
8886 if w := &srv.feedbacks[i]; w.surf == s && w.replaced == 0 && seq > w.seq {
8987 w.replaced = seq
@@ -102,9 +100,7 @@ func (s *surface) commitFeedback(pending []*presentationtime.WpPresentationFeedb
102100 }
103101 return
104102 }
105- srv.contentMu.Lock()
106103 seq := srv.contentSeq[win]
107- srv.contentMu.Unlock()
108104 for _, fb := range pending {
109105 srv.feedbacks = append(srv.feedbacks, feedbackWait{fb: fb, surf: s, win: win, seq: seq, at: time.Now()})
110106 }
@@ -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 = seq87 w.replaced = seq
@@ -102,9 +100,7 @@ func (s *surface) commitFeedback(pending []*presentationtime.WpPresentationFeedb
102 }100 }
103 return101 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) {
4646 }
4747
4848 // 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) {
5050 t.Helper()
5151 dir := t.TempDir()
5252 events := make(chan ports.ClientEvent, 16)
5353 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"))
5655 if err != nil {
5756 t.Fatal(err)
5857 }
58+ contents := relayContents(t, s)
5959 ctx, cancel := context.WithCancel(context.Background())
6060 done := make(chan error, 1)
6161 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)
4747 if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok {
4848 h.id = d.buf.ID
4949 }
50- s.contentMu.Lock()
5150 h.after = s.contentSeq[window]
52- s.contentMu.Unlock()
5351 s.held = append(s.held, h)
5452 // Wake the pacer: it releases the buffer on the next output report.
5553 s.wakePacer()
@@ -78,9 +76,7 @@ func (s *Server) releaseBufferSync(surf *surface, sync syncHold) {
7876 s.releaseSync(sync)
7977 return
8078 }
81- s.contentMu.Lock()
8279 after := s.contentSeq[window]
83- s.contentMu.Unlock()
8480 s.held = append(s.held, heldBuffer{sync: sync, window: window, after: after, at: time.Now()})
8581 s.wakePacer()
8682 }
@@ -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.ID48 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 return77 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) {
5252 }
5353 }
5454
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) {
5656 t.Helper()
5757 return contentServerWith(t, Options{}, logging.For(context.Background(), "wayland"))
5858 }
5959
6060 // 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) {
6262 t.Helper()
6363 dir := t.TempDir()
6464 events := make(chan ports.ClientEvent, 16)
6565 commands := make(chan ports.ClientCommand, 16)
66- contents := make(chan ports.SurfaceContent, 16)
6766 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)
6968 if err != nil {
7069 t.Fatal(err)
7170 }
71+ contents := relayContents(t, s)
7272 ctx, cancel := context.WithCancel(context.Background())
7373 done := make(chan error, 1)
7474 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, testOutputs66 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 {
6666 LeaseRequests chan<- ports.LeaseMessage
6767 LeaseEvents <-chan ports.LeaseMessage
6868 Events chan<- ports.ClientEvent
69- Contents chan<- ports.SurfaceContent
7069 Commands <-chan ports.ClientCommand
7170 Workspaces <-chan ports.Workspaces
7271 // Cursors receives the cursor the client under the pointer asks for.
@@ -212,11 +211,16 @@ type Server struct {
212211 eventMu sync.Mutex
213212 events []ports.ClientEvent
214213 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).
217217 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()
220224 // feedbacks wait for the flip that shows their content (presentation.go).
221225 feedbacks []feedbackWait
222226 // Explicit sync (syncobj.go): the render node, imported timelines by
@@ -238,7 +242,6 @@ type Server struct {
238242 clock ports.Clock
239243 powers []*outputPower
240244 outputsOff map[string]bool
241- contentReady chan struct{}
242245 outputs []*output
243246 outputManagers []*outputManager
244247 workspaceManagers []*workspaceManager
@@ -331,10 +334,9 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
331334 layers: make(map[ports.WindowID]*layerSurface),
332335 nextWindow: 1,
333336 eventReady: make(chan struct{}, 1),
334- contents: make(map[ports.WindowID]ports.SurfaceContent),
335337 contentSeq: make(map[ports.WindowID]uint64),
336338 damage: map[ports.WindowID][]ports.SeqDamage{},
337- contentReady: make(chan struct{}, 1),
339+ latest: make(map[ports.WindowID]ports.SurfaceContent),
338340 cursorReady: make(chan struct{}, 1),
339341 dataSources: map[*server.Resource]*clipSource{},
340342 primarySources: map[*server.Resource]*clipSource{},
@@ -346,6 +348,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
346348 positioners: map[*server.Resource]*positioner{},
347349 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},
348350 }
351+ s.flushLinksFn = s.flushLinks
349352 s.leaseDevices = map[string]*leaseDevice{}
350353 s.pendingLeases = map[uint64]*leaseObject{}
351354 s.pendingLeaseCards = map[uint64]string{}
@@ -491,7 +494,7 @@ func (s *Server) Run(ctx context.Context) error {
491494 s.started = time.Now()
492495 s.ctx = ctx
493496 var wg sync.WaitGroup
494- wg.Add(14)
497+ wg.Add(13)
495498 go func() { defer wg.Done(); s.forwardBackendSecurity(ctx) }()
496499 go func() { defer wg.Done(); s.forwardSecurityEvents(ctx) }()
497500 go func() { defer wg.Done(); s.forwardLeases(ctx) }()
@@ -499,7 +502,6 @@ func (s *Server) Run(ctx context.Context) error {
499502 go func() { defer wg.Done(); s.forwardWorkspaces(ctx) }()
500503 go func() { defer wg.Done(); s.forward(ctx) }()
501504 go func() { defer wg.Done(); s.forwardCursors(ctx) }()
502- go func() { defer wg.Done(); s.forwardContents(ctx) }()
503505 go func() {
504506 defer wg.Done()
505507 for {
@@ -665,11 +667,10 @@ func (s *Server) forward(ctx context.Context) {
665667 }
666668 }
667669
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).
668673 func (s *Server) emitContent(c ports.SurfaceContent, d damage) {
669- if s.channels.Contents == nil {
670- return
671- }
672- s.contentMu.Lock()
673674 s.contentSeq[c.ID]++
674675 c.Seq = s.contentSeq[c.ID]
675676 // 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) {
689690 h := append(old, ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects})
690691 if c.Empty() {
691692 delete(s.damage, c.ID)
693+ delete(s.latest, c.ID)
692694 } else {
693695 s.damage[c.ID] = h
694696 }
695697 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
701700 }
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)
740704 }
741705 }
742706
@@ -66,7 +66,6 @@ type Channels struct {
66 LeaseRequests chan<- ports.LeaseMessage66 LeaseRequests chan<- ports.LeaseMessage
67 LeaseEvents <-chan ports.LeaseMessage67 LeaseEvents <-chan ports.LeaseMessage
68 Events chan<- ports.ClientEvent68 Events chan<- ports.ClientEvent
69- Contents chan<- ports.SurfaceContent
70 Commands <-chan ports.ClientCommand69 Commands <-chan ports.ClientCommand
71 Workspaces <-chan ports.Workspaces70 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.Mutex211 eventMu sync.Mutex
213 events []ports.ClientEvent212 events []ports.ClientEvent
214 eventReady chan struct{}213 eventReady chan struct{}
215- contentMu sync.Mutex214+ // contentSeq counts each window's contents and damage is its recent
216- contents map[ports.WindowID]ports.SurfaceContent215+ // 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]uint64217 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.SeqDamage219+ 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 []feedbackWait225 feedbacks []feedbackWait
222 // Explicit sync (syncobj.go): the render node, imported timelines by226 // Explicit sync (syncobj.go): the render node, imported timelines by
@@ -238,7 +242,6 @@ type Server struct {
238 clock ports.Clock242 clock ports.Clock
239 powers []*outputPower243 powers []*outputPower
240 outputsOff map[string]bool244 outputsOff map[string]bool
241- contentReady chan struct{}
242 outputs []*output245 outputs []*output
243 outputManagers []*outputManager246 outputManagers []*outputManager
244 workspaceManagers []*workspaceManager247 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 = ctx495 s.ctx = ctx
493 var wg sync.WaitGroup496 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 long676 // 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] = h695 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] = c698+ 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) {
374374 }
375375 dir := t.TempDir()
376376 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"))
379378 if err != nil {
380379 t.Fatal(err)
381380 }
381+ contents := relayContents(t, s)
382382 ctx, cancel := context.WithCancel(context.Background())
383383 done := make(chan error, 1)
384384 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) {
264264 // for later) are not drawn, so they do not make it full.
265265 func (s *surface) redraw() {
266266 r := s.root()
267- if id := r.windowID(); id != 0 && s.server.channels.Contents != nil {
267+ if id := r.windowID(); id != 0 {
268268 c := r.tree(id)
269269 d := damage{full: true}
270270 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) {
4848 })
4949 for _, count := range []int{16, 64} {
5050 t.Run(fmt.Sprintf("%d", count), func(t *testing.T) {
51- channels := Channels{Contents: make(chan ports.SurfaceContent, 1)}
51+ channels := Channels{}
5252 srv, err := New(Options{RuntimeDir: t.TempDir(), Outputs: testOutputs}, channels, logging.For(context.Background(), "wayland"))
5353 if err != nil {
5454 t.Fatal(err)
@@ -64,7 +64,7 @@ func TestTiledCommitPublishAllocations(t *testing.T) {
6464 publish := func() { root.redraw() }
6565 root.treeDirty = true
6666 publish()
67- first := srv.contents[1]
67+ first := srv.latest[1]
6868 publish()
6969 if first.Seq != 1 || len(first.Children) != count || len(first.DamageHistory) != 1 {
7070 t.Fatalf("previous publication changed: %+v", first)
@@ -99,10 +99,10 @@ func TestTiledCommitPublishAllocations(t *testing.T) {
9999 root.sub.children[count-1].version++
100100 root.treeDirty = true
101101 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 {
103103 t.Fatal("child version leaked into an earlier publication")
104104 }
105- if &first.Children[0] == &srv.contents[1].Children[0] {
105+ if &first.Children[0] == &srv.latest[1].Children[0] {
106106 t.Fatal("rebuild reused previously published children storage")
107107 }
108108 // 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 = true65 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 = true100 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 a108 // 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 {
4848 s *Server
4949 c *wlturbo.Display
5050 dev *mocksyncobjDevice
51- contents chan ports.SurfaceContent
51+ contents <-chan ports.SurfaceContent
5252 presented ports.PresentationSink
5353 surf uint32
5454 syncSurf uint32
@@ -99,12 +99,12 @@ func newSyncHarness(t *testing.T) *syncHarness {
9999 dir := t.TempDir()
100100 events := make(chan ports.ClientEvent, 16)
101101 commands := make(chan ports.ClientCommand, 16)
102- h.contents = make(chan ports.SurfaceContent, 64)
103102 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"))
105104 if err != nil {
106105 t.Fatal(err)
107106 }
107+ h.contents = relayContents(t, s)
108108 ctx, cancel := context.WithCancel(context.Background())
109109 done := make(chan error, 1)
110110 go func() { done <- s.Run(ctx) }()
@@ -48,7 +48,7 @@ type syncHarness struct {
48 s *Server48 s *Server
49 c *wlturbo.Display49 c *wlturbo.Display
50 dev *mocksyncobjDevice50 dev *mocksyncobjDevice
51- contents chan ports.SurfaceContent51+ contents <-chan ports.SurfaceContent
52 presented ports.PresentationSink52 presented ports.PresentationSink
53 surf uint3253 surf uint32
54 syncSurf uint3254 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 (
1717 type transformClient struct {
1818 c *wlturbo.Display
1919 comp, surf, vp, bf uint32
20- contents chan ports.SurfaceContent
20+ contents <-chan ports.SurfaceContent
2121 }
2222
2323 func newTransformClient(t *testing.T) transformClient {
@@ -17,7 +17,7 @@ import (
17 type transformClient struct {17 type transformClient struct {
18 c *wlturbo.Display18 c *wlturbo.Display
19 comp, surf, vp, bf uint3219 comp, surf, vp, bf uint32
20- contents chan ports.SurfaceContent20+ 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) {
144144 replies := make(chan ports.CaptureDone, 1)
145145 replies <- ports.CaptureDone{}
146146 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 {
148148 <-ctx.Done()
149149 return nil
150150 })
@@ -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 nil149 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
333333 defer b.seat.Unsubscribe(active)
334334 seatActive := true // openDRM waits for the first seat enable
335335 set := newOutputSet(ctx, ch.captured)
336+ set.attach = ch.content
336337 set.wireSecurity(ch)
337338 protected := func() bool { return set.state.Protected || securitySnapshot(ch.security).Protected }
338339 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
513514 }
514515 active := b.seat.Subscribe()
515516 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 {
517518 defer b.seat.Unsubscribe(active)
518519 o.Security, o.SecurityChanges, o.Instance = ch.security, secure, instance
519520 return safe("output "+name, func() error {
@@ -533,7 +534,7 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm
533534 schedLog.Warn().Err(err).Msg("output scheduling")
534535 }
535536 }
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)
537538 })
538539 }, &o.SecurityEvents)
539540 if !started {
@@ -595,7 +596,6 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm
595596 configs, configNext := apply.configOut(ch.configured), apply.config
596597 replies, replyNext := apply.replyOut(ch.replies)
597598 heads, headsNext := apply.headsOut(ch.heads)
598- contents, contentOut, contentNext, contentOwner := set.contentOut(ch.contents)
599599 select {
600600 case <-ctx.Done():
601601 return set.wait()
@@ -700,10 +700,6 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm
700700 complete(progress.timeout(op))
701701 case s := <-ch.scenes:
702702 set.scenes(s)
703- case c := <-contents:
704- set.content(c)
705- case contentOut <- contentNext:
706- contentOwner.contentSent()
707703 case q := <-ch.captures:
708704 set.routeCapture(q)
709705 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 enable334 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.Realtime516 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, instance519 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.config596 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 {
2727 // newRenderer is configured for virtual outputs by newVulkanRenderer.
2828 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 {
2929 set := newOutputSet(ctx, ch.captured)
30+ set.attach = ch.content
3031 set.wireSecurity(ch)
3132 inventory := ports.OutputHeads{}
3233 for i, size := range o.sizes {
@@ -44,9 +45,9 @@ func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch
4445 cur := &headless.Cursor{}
4546 curs.set(name, cur)
4647 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 {
4849 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)
5051 }, &opts.SecurityEvents)
5152 if !started {
5253 return set.wait()
@@ -68,7 +69,6 @@ func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch
6869 configs, configNext := apply.configOut(ch.configured), apply.config
6970 replies, replyNext := apply.replyOut(ch.replies)
7071 heads, headsNext := apply.headsOut(ch.heads)
71- contents, contentOut, contentNext, contentOwner := set.contentOut(ch.contents)
7272 select {
7373 case <-ctx.Done():
7474 return set.wait()
@@ -92,10 +92,6 @@ func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch
9292 apply.headsSent()
9393 case s := <-ch.scenes:
9494 set.scenes(s)
95- case c := <-contents:
96- set.content(c)
97- case contentOut <- contentNext:
98- contentOwner.contentSent()
9995 case q := <-ch.captures:
10096 set.routeCapture(q)
10197 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, instance49 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.config69 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"
44
55 // outputChannels is the shared app-to-backend wiring. The backend loop remains
66 // 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).
88 // heads, configured and replies are fed from the outputApply outbox.
99 type outputChannels struct {
1010 security ports.SessionSecurity
@@ -14,7 +14,7 @@ type outputChannels struct {
1414 leaseEvents chan<- ports.LeaseMessage
1515 events chan<- ports.OutputEvent
1616 scenes <-chan []ports.Scene
17- contents <-chan ports.SurfaceContent
17+ content contentAttacher
1818 cursorChanges <-chan ports.CursorChange
1919 presented ports.PresentationSink
2020 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 remains5 // outputChannels is the shared app-to-backend wiring. The backend loop remains
6 // the sole owner of outputSet and outputApply. Security producers use bounded6 // 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.SessionSecurity10 security ports.SessionSecurity
@@ -14,7 +14,7 @@ type outputChannels struct {
14 leaseEvents chan<- ports.LeaseMessage14 leaseEvents chan<- ports.LeaseMessage
15 events chan<- ports.OutputEvent15 events chan<- ports.OutputEvent
16 scenes <-chan []ports.Scene16 scenes <-chan []ports.Scene
17- contents <-chan ports.SurfaceContent17+ content contentAttacher
18 cursorChanges <-chan ports.CursorChange18 cursorChanges <-chan ports.CursorChange
19 presented ports.PresentationSink19 presented ports.PresentationSink
20 frames chan<- ports.OutputFrame20 frames chan<- ports.OutputFrame
M internal/app/outputrouting_test.go +2 -15
@@ -14,7 +14,7 @@ func TestOutputRouting(t *testing.T) {
1414 replies := make(chan ports.CaptureDone, 2)
1515 set := newOutputSet(ctx, replies)
1616 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 {
1818 <-ctx.Done()
1919 return nil
2020 })
@@ -28,13 +28,6 @@ func TestOutputRouting(t *testing.T) {
2828 if got := <-set.outs["B"].scenes; got.Seq != 2 {
2929 t.Fatalf("B scene: %+v", got)
3030 }
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- }
3831 set.setCursor(ports.CursorChange{Shape: "text"})
3932 set.setCursor(ports.CursorChange{Shape: "pointer"})
4033 for _, name := range []string{"A", "B"} {
@@ -86,13 +79,9 @@ func TestOutputSetRemembersOff(t *testing.T) {
8679
8780 func TestOutputRoutingAllocations(t *testing.T) {
8881 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)
9382 ctx, cancel := context.WithCancel(context.Background())
9483 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 {
9685 <-ctx.Done()
9786 return nil
9887 })
@@ -101,8 +90,6 @@ func TestOutputRoutingAllocations(t *testing.T) {
10190 capture := ports.CaptureRequest{Output: "A"}
10291 if got := testing.AllocsPerRun(100, func() {
10392 set.scenes(scenes)
104- <-set.outs["A"].contents
105- set.content(content)
10693 set.setCursor(cursor)
10794 set.routeCapture(capture)
10895 <-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 nil19 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 nil86 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"].captures95 <-set.outs["A"].captures
M internal/app/outputs.go +36 -64
@@ -13,16 +13,27 @@ import (
1313
1414 // outputRun drives one output until ctx ends: it renders the scenes and
1515 // 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+}
1727
1828 // 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.
2232 // Only the goroutine running loop touches the set.
2333 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
2637 // off is the latest Scene.Off per output name, kept after the output
2738 // goes: it reconnects off until core's first scene (Output.StartOff).
2839 off map[string]bool
@@ -50,9 +61,7 @@ type runningOutput struct {
5061 instance ports.OutputInstance
5162 security chan ports.SecurityState
5263 securityEvents chan ports.SecurityBackendEvent
53- pending []ports.SurfaceContent
5464 scenes chan ports.Scene
55- contents chan ports.SurfaceContent
5665 cursor chan ports.CursorChange
5766 captures chan ports.CaptureRequest
5867 ctx context.Context
@@ -62,10 +71,10 @@ type runningOutput struct {
6271 }
6372
6473 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{})}
6675 }
6776
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.
6978 func (s *outputSet) start(ctx context.Context, name string, run outputRun, wire ...*chan<- ports.SecurityBackendEvent) bool {
7079 // Exhaustion is permanent: never wrap to zero or reuse a lifetime.
7180 if s.nextInstance == ^ports.OutputInstance(0) {
@@ -82,22 +91,21 @@ func (s *outputSet) start(ctx context.Context, name string, run outputRun, wire
8291 s.setSecurity(s.security.Snapshot())
8392 }
8493 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
8796 r.securityEvents = s.forwardSecurity()
8897 for _, destination := range wire {
8998 *destination = r.securityEvents
9099 }
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)
97105 }
98106 s.outs[name] = r
99107 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)
101109 if r.securityEvents != nil {
102110 close(r.securityEvents)
103111 }
@@ -229,51 +237,6 @@ func (s *outputSet) scenes(set []ports.Scene) {
229237 }
230238 }
231239
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-
277240 // finish forgets a stopped output and returns its error.
278241 func (s *outputSet) finish(name string) error {
279242 r := s.outs[name]
@@ -283,10 +246,18 @@ func (s *outputSet) finish(name string) error {
283246 <-r.done
284247 r.stop()
285248 s.failQueued(r)
249+ s.detach(name)
286250 delete(s.outs, name)
287251 return r.err
288252 }
289253
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+
290261 // wait stops every output and returns the first error.
291262 func (s *outputSet) wait() error {
292263 var first error = s.startErr
@@ -301,6 +272,7 @@ func (s *outputSet) wait() error {
301272 r.stop()
302273 <-r.done
303274 s.failQueued(r)
275+ s.detach(name)
304276 if first == nil && r.err != nil {
305277 first = r.err
306278 }
@@ -13,16 +13,27 @@ import (
13 13
14 // outputRun drives one output until ctx ends: it renders the scenes and14 // 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) error16+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 scene28 // 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 to29+// per output; each goes to its output, latest first. Surface contents reach
20-// every output, since windows move between outputs; the latest content of30+// 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]*runningOutput34+ outs map[string]*runningOutput
25- latest map[ports.WindowID]ports.SurfaceContent35+ // 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 output37 // 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]bool39 off map[string]bool
@@ -50,9 +61,7 @@ type runningOutput struct {
50 instance ports.OutputInstance61 instance ports.OutputInstance
51 security chan ports.SecurityState62 security chan ports.SecurityState
52 securityEvents chan ports.SecurityBackendEvent63 securityEvents chan ports.SecurityBackendEvent
53- pending []ports.SurfaceContent
54 scenes chan ports.Scene64 scenes chan ports.Scene
55- contents chan ports.SurfaceContent
56 cursor chan ports.CursorChange65 cursor chan ports.CursorChange
57 captures chan ports.CaptureRequest66 captures chan ports.CaptureRequest
58 ctx context.Context67 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 replay95+ 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.securityEvents98 *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] = r106 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.done246 <-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.err251 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.startErr263 var first error = s.startErr
@@ -301,6 +272,7 @@ func (s *outputSet) wait() error {
301 r.stop()272 r.stop()
302 <-r.done273 <-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.err277 first = r.err
306 }278 }
M internal/app/outputs_test.go +1 -1
@@ -17,7 +17,7 @@ func TestOutputSetCursor(t *testing.T) {
1717 run := func(name string) outputRun {
1818 got[name] = make(chan ports.CursorChange, 8)
1919 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 {
2121 for {
2222 select {
2323 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
2727 events := make(chan ports.SecurityBackendEvent, 4)
2828 set := newOutputSet(context.Background(), nil)
2929 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 {
3131 <-ctx.Done()
3232 return nil
3333 }
@@ -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 nil32 return nil
33 }33 }
M internal/app/security_hotplug_test.go +4 -4
@@ -66,7 +66,7 @@ func TestSecurityHotplugLifetimeAndProofOrdering(t *testing.T) {
6666 commands := make(chan []ports.SecurityBackendEvent, 1)
6767 closed := make(chan bool, 1)
6868 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 {
7070 defer func() { closed <- closeSuccess }()
7171 state := <-security
7272 proof := ports.SecurityOutputProof{Proof: ports.OutputProtection{Generation: state.Generation, Instance: instance, Kind: ports.ProtectionProtectedFrame}}
@@ -205,7 +205,7 @@ func securityOwnerFIFOSaturationDrainsOnNormalStop(t *testing.T) {
205205 startDone := make(chan bool, 1)
206206 var ownerEvents chan<- ports.SecurityBackendEvent
207207 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 {
209209 for i := range 40 {
210210 var event ports.SecurityBackendEvent = ports.SecurityOutputInvalidated{Generation: generation, Instance: instance}
211211 if i%2 == 0 {
@@ -295,7 +295,7 @@ func TestSecurityOwnerBufferedEvidenceDrainsAfterFinish(t *testing.T) {
295295 startDone := make(chan bool, 1)
296296 var ownerEvents chan<- ports.SecurityBackendEvent
297297 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 {
299299 for i := range 16 {
300300 event := ports.SecurityOutputInvalidated{Generation: ports.LockGeneration(i + 1), Instance: instance}
301301 select {
@@ -350,7 +350,7 @@ func securityOwnerFIFOSaturationCancelReleasesOwnerAndForwarders(t *testing.T) {
350350 startDone := make(chan bool, 1)
351351 var ownerEvents chan<- ports.SecurityBackendEvent
352352 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 {
354354 defer close(ownerDone)
355355 for i := range 40 {
356356 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.SecurityBackendEvent68 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 := <-security71 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.SecurityBackendEvent206 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.SecurityBackendEvent296 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.SecurityBackendEvent351 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) {
2121 started := make(chan ports.OutputInstance, 1)
2222 result := make(chan bool, 1)
2323 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 {
2525 state := <-security
2626 if state != (ports.SecurityState{Generation: 7, Protected: true}) {
2727 return errors.New("missing startup protection")
@@ -67,7 +67,7 @@ func TestSecurityReplacementAndStaleState(t *testing.T) {
6767 events := make(chan ports.SecurityBackendEvent, 4)
6868 set := newOutputSet(ctx, nil)
6969 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 {
7171 <-ctx.Done()
7272 return nil
7373 }
@@ -167,7 +167,7 @@ func TestSecurityInstanceExhaustion(t *testing.T) {
167167 events := make(chan ports.SecurityBackendEvent, 2)
168168 set.wireSecurity(outputChannels{securityEvents: events})
169169 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 {
171171 <-ctx.Done()
172172 return nil
173173 }
@@ -195,26 +195,22 @@ func TestSecurityInstanceExhaustion(t *testing.T) {
195195 }
196196 }
197197
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) {
202201 ctx, cancel := context.WithCancel(context.Background())
203202 defer cancel()
204203 set := newOutputSet(ctx, nil)
205204 events := make(chan ports.SecurityBackendEvent, 1)
206205 set.wireSecurity(outputChannels{securityEvents: events})
207206 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 {
209208 <-ctx.Done()
210209 return nil
211210 }, &ownerEvents)
212211 <-events // ordered registration
213212 events <- ports.SecurityBackendBarrier{Generation: 99} // blocked display
214213 r := set.outs["DP-1"]
215- for i := range cap(r.contents) {
216- r.contents <- ports.SurfaceContent{ID: ports.WindowID(i + 1)}
217- }
218214 first := ports.SecurityOutputInvalidated{Generation: 1, Instance: r.instance}
219215 second := ports.SecurityOutputProof{Proof: ports.OutputProtection{Generation: 1, Instance: r.instance, Kind: ports.ProtectionProtectedFrame}}
220216 ownerEvents <- first
@@ -225,7 +221,6 @@ func TestSecurityFullEventsAndContentsAllowLeaseBarrier(t *testing.T) {
225221 card.EXPECT().Revoke(uint32(9)).Return(nil).Once()
226222 progressed := make(chan error, 1)
227223 go func() {
228- set.content(ports.SurfaceContent{ID: 70})
229224 set.setSecurity(ports.SecurityState{Generation: 1, Protected: true})
230225 err := revokeSecurityLeases(card, func(uint32) {})
231226 set.securityEvent(ports.SecurityBackendBarrier{Generation: 1, Err: err})
@@ -237,23 +232,7 @@ func TestSecurityFullEventsAndContentsAllowLeaseBarrier(t *testing.T) {
237232 t.Fatal(err)
238233 }
239234 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")
257236 }
258237 <-events // unblock display
259238 var ownerSequence []ports.SecurityBackendEvent
@@ -275,43 +254,3 @@ func TestSecurityFullEventsAndContentsAllowLeaseBarrier(t *testing.T) {
275254 t.Fatal(err)
276255 }
277256 }
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 := <-security25 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 nil72 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) - 1169 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 nil172 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 be198+// A saturated shared display event channel must not pin the backend outside
199-// saturated. Routing must retain the next content without pinning the backend199+// 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.SecurityBackendEvent206 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 nil209 return nil
211 }, &ownerEvents)210 }, &ownerEvents)
212 <-events // ordered registration211 <-events // ordered registration
213 events <- ports.SecurityBackendBarrier{Generation: 99} // blocked display212 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 <- first216 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 display237 <-events // unblock display
259 var ownerSequence []ports.SecurityBackendEvent238 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))
354354
355355 go func() {
356356 defer workers.Done()
357- outputIO := p.outputs(security, server.Presentation())
357+ outputIO := p.outputs(security, server.Presentation(), waylandContent{server})
358358 apply := newOutputApply(core.NewOutputOverrides(opts.Config, hw == nil), logging.For(ctx, "app"))
359359 if hw != nil {
360360 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 {
4141 // config file.
4242 scales chan ports.ScaleChanged
4343
44- contents chan ports.SurfaceContent
4544 // Outputs report to wayland through its presentation sink and tell core
4645 // about each flip (frames) to move running slides.
4746 frames chan ports.OutputFrame
@@ -91,8 +90,7 @@ func newPipes(realSession bool) *pipes {
9190 configured: make(chan ports.ConfigChanged, 8),
9291 configErrors: make(chan error, 8),
9392
94- contents: make(chan ports.SurfaceContent, 64),
95- frames: make(chan ports.OutputFrame, 8),
93+ frames: make(chan ports.OutputFrame, 8),
9694
9795 captures: make(chan ports.CaptureRequest, ports.MaxCaptureInflight),
9896 captured: make(chan ports.CaptureDone, ports.MaxCaptureInflight),
@@ -142,7 +140,6 @@ func (p *pipes) wayland() wayland.Channels {
142140 Events: p.client,
143141 Commands: p.commands,
144142 Workspaces: p.workspaces,
145- Contents: p.contents,
146143 Cursors: p.cursorChanges,
147144 Captures: p.captures,
148145 Captured: p.captured,
@@ -158,16 +155,16 @@ func (p *pipes) wayland() wayland.Channels {
158155 }
159156
160157 // 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 {
162159 return outputChannels{
163160 security: security,
164161 securityChanges: p.securityChanges,
165162 securityEvents: p.securityEvents,
166163 events: p.output,
167164 scenes: p.renderScenes,
168- contents: p.contents,
169165 cursorChanges: p.cursorChanges,
170166 presented: presented,
167+ content: content,
171168 frames: p.frames,
172169 captures: p.captures,
173170 captured: p.captured,
@@ -41,7 +41,6 @@ type pipes struct {
41 // config file.41 // config file.
42 scales chan ports.ScaleChanged42 scales chan ports.ScaleChanged
43 43
44- contents chan ports.SurfaceContent
45 // Outputs report to wayland through its presentation sink and tell core44 // 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.OutputFrame46 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 {
108108 DamageHistory []SeqDamage
109109 }
110110
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+
111135 // BufferTransform is a wl_output.transform value: bit 0 rotates 90°
112136 // counter-clockwise, bit 1 rotates 180°, bit 2 flips around the vertical
113137 // axis first.
@@ -108,6 +108,30 @@ type SurfaceContent struct {
108 DamageHistory []SeqDamage108 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 vertical136 // counter-clockwise, bit 1 rotates 180°, bit 2 flips around the vertical
113 // axis first.137 // axis first.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close