detached 3935f90

refactor(outputkit): share batch consumption, core wake and shows snapshot

drm and headless outputs each carried the same loop over a content batch (Update, advance seen, mark dirty, reset, return), the same non-blocking OutputFrame send, and the same reuse-the-snapshot logic for flip Shows.

surfaces.Table.Apply now consumes a batch and keeps the reset-then-return rule in one place; it reports dirty and whether seen moved forward (drm's reportDirty). corewake.Send is the non-blocking OutputFrame send. surfaces.ShowsSnapshot (FromSeen for drm, FromContents for headless) replaces drm's shownBy scratch/snapshot fields and headless's flipShows map pair, and FillShown. Behavior is unchanged.
commit
3935f90bbbb6c3c8467b31555934ab5f65fc9d76
author
brice · · 2026-10-14 03:36 UTC
committer
brice · · 2026-10-14 04:34 UTC
parent
e6c3bc7
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +206 -85
M Makefile +1 -1
@@ -61,7 +61,7 @@ arch: adapter-imports
6161 # Adapters stay independent: production code may import only the shared
6262 # adapter libraries below. outputkit holds what both output backends (drm,
6363 # headless) run; the others are leaf helpers.
64-ADAPTER_LIBS := busretry|outputkit/capture|outputkit/presented|outputkit/surfaces|captureallow|clock|logging|sessionsecurity|syncfile|workspaceid|xkb
64+ADAPTER_LIBS := busretry|outputkit/capture|outputkit/corewake|outputkit/presented|outputkit/surfaces|captureallow|clock|logging|sessionsecurity|syncfile|workspaceid|xkb
6565 adapter-imports:
6666 @edges=$$(CGO_ENABLED=0 go list -f '{{$$p := .ImportPath}}{{range .Imports}}{{$$p}} {{.}}{{"\n"}}{{end}}' ./internal/adapters/...) || exit 1; \
6767 bad=$$(echo "$$edges" | grep -E ' \S+/internal/adapters/' | grep -vE '/internal/adapters/($(ADAPTER_LIBS))$$' \
@@ -61,7 +61,7 @@ arch: adapter-imports
61 # Adapters stay independent: production code may import only the shared61 # Adapters stay independent: production code may import only the shared
62 # adapter libraries below. outputkit holds what both output backends (drm,62 # adapter libraries below. outputkit holds what both output backends (drm,
63 # headless) run; the others are leaf helpers.63 # headless) run; the others are leaf helpers.
64-ADAPTER_LIBS := busretry|outputkit/capture|outputkit/presented|outputkit/surfaces|captureallow|clock|logging|sessionsecurity|syncfile|workspaceid|xkb64+ADAPTER_LIBS := busretry|outputkit/capture|outputkit/corewake|outputkit/presented|outputkit/surfaces|captureallow|clock|logging|sessionsecurity|syncfile|workspaceid|xkb
65 adapter-imports:65 adapter-imports:
66 @edges=$$(CGO_ENABLED=0 go list -f '{{$$p := .ImportPath}}{{range .Imports}}{{$$p}} {{.}}{{"\n"}}{{end}}' ./internal/adapters/...) || exit 1; \66 @edges=$$(CGO_ENABLED=0 go list -f '{{$$p := .ImportPath}}{{range .Imports}}{{$$p}} {{.}}{{"\n"}}{{end}}' ./internal/adapters/...) || exit 1; \
67 bad=$$(echo "$$edges" | grep -E ' \S+/internal/adapters/' | grep -vE '/internal/adapters/($(ADAPTER_LIBS))$$' \67 bad=$$(echo "$$edges" | grep -E ' \S+/internal/adapters/' | grep -vE '/internal/adapters/($(ADAPTER_LIBS))$$' \
M internal/adapters/drm/output.go +7 -23
@@ -11,6 +11,7 @@ import (
1111
1212 "github.com/bnema/neferwl/internal/adapters/clock"
1313 "github.com/bnema/neferwl/internal/adapters/outputkit/capture"
14+ "github.com/bnema/neferwl/internal/adapters/outputkit/corewake"
1415 "github.com/bnema/neferwl/internal/adapters/outputkit/presented"
1516 "github.com/bnema/neferwl/internal/adapters/outputkit/surfaces"
1617
@@ -116,9 +117,8 @@ type Output struct {
116117 reports presented.Queue
117118 // Last immutable shows snapshot sent in a flip. Never mutate it: wayland
118119 // may still be reading a prior report on another goroutine.
119- showsSnapshot map[ports.WindowID]uint64
120+ shows surfaces.ShowsSnapshot
120121 directShowsSnapshot map[ports.WindowID]uint64
121- showsScratch map[ports.WindowID]uint64
122122 // Tearing (ADR 006): async commits of scanned-out buffers that ask for
123123 // them, when the card has DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. asyncFence
124124 // is set when an async commit may carry IN_FENCE_FD (probed once).
@@ -1433,18 +1433,10 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
14331433 stateDirty = true
14341434 }
14351435 case b := <-contents:
1436- for i := range b.Items {
1437- c := b.Items[i]
1438- table.Update(c, scene)
1439- if c.Seq > seen[c.ID] {
1440- seen[c.ID] = c.Seq
1441- reportDirty = true
1442- }
1443- // Windows on other outputs or workspaces do not need a frame.
1444- dirty = dirty || capture.Shows(scene, c.ID)
1445- }
1446- b.Reset()
1447- returns.Return(b)
1436+ // Windows on other outputs or workspaces do not need a frame.
1437+ batchDirty, batchSeen := table.Apply(b, scene, seen, returns)
1438+ dirty = dirty || batchDirty
1439+ reportDirty = reportDirty || batchSeen
14481440 case <-stats.C():
14491441 now := clk.Now()
14501442 pipeline.Retire(scene) // an idle child of an ended session goes
@@ -1663,15 +1655,7 @@ func (o *Output) completed(ev flipEvent, seen map[ports.WindowID]uint64) bool {
16631655
16641656 // wakeCore tells core the next frame of a running animation is due. A frame
16651657 // core has not read yet is enough, so the owner never waits.
1666-func (o *Output) wakeCore() {
1667- if o.Frames == nil {
1668- return
1669- }
1670- select {
1671- case o.Frames <- ports.OutputFrame{Output: o.conn.name}:
1672- default:
1673- }
1674-}
1658+func (o *Output) wakeCore() { corewake.Send(o.Frames, o.conn.name) }
16751659
16761660 // setCursor loads a cursor for scale and output transform t into the cursor
16771661 // images.
@@ -11,6 +11,7 @@ import (
11 11
12 "github.com/bnema/neferwl/internal/adapters/clock"12 "github.com/bnema/neferwl/internal/adapters/clock"
13 "github.com/bnema/neferwl/internal/adapters/outputkit/capture"13 "github.com/bnema/neferwl/internal/adapters/outputkit/capture"
14+ "github.com/bnema/neferwl/internal/adapters/outputkit/corewake"
14 "github.com/bnema/neferwl/internal/adapters/outputkit/presented"15 "github.com/bnema/neferwl/internal/adapters/outputkit/presented"
15 "github.com/bnema/neferwl/internal/adapters/outputkit/surfaces"16 "github.com/bnema/neferwl/internal/adapters/outputkit/surfaces"
16 17
@@ -116,9 +117,8 @@ type Output struct {
116 reports presented.Queue117 reports presented.Queue
117 // Last immutable shows snapshot sent in a flip. Never mutate it: wayland118 // Last immutable shows snapshot sent in a flip. Never mutate it: wayland
118 // may still be reading a prior report on another goroutine.119 // may still be reading a prior report on another goroutine.
119- showsSnapshot map[ports.WindowID]uint64120+ shows surfaces.ShowsSnapshot
120 directShowsSnapshot map[ports.WindowID]uint64121 directShowsSnapshot map[ports.WindowID]uint64
121- showsScratch map[ports.WindowID]uint64
122 // Tearing (ADR 006): async commits of scanned-out buffers that ask for122 // Tearing (ADR 006): async commits of scanned-out buffers that ask for
123 // them, when the card has DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. asyncFence123 // them, when the card has DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. asyncFence
124 // is set when an async commit may carry IN_FENCE_FD (probed once).124 // is set when an async commit may carry IN_FENCE_FD (probed once).
@@ -1433,18 +1433,10 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
1433 stateDirty = true1433 stateDirty = true
1434 }1434 }
1435 case b := <-contents:1435 case b := <-contents:
1436- for i := range b.Items {1436+ // Windows on other outputs or workspaces do not need a frame.
1437- c := b.Items[i]1437+ batchDirty, batchSeen := table.Apply(b, scene, seen, returns)
1438- table.Update(c, scene)1438+ dirty = dirty || batchDirty
1439- if c.Seq > seen[c.ID] {1439+ reportDirty = reportDirty || batchSeen
1440- seen[c.ID] = c.Seq
1441- reportDirty = true
1442- }
1443- // Windows on other outputs or workspaces do not need a frame.
1444- dirty = dirty || capture.Shows(scene, c.ID)
1445- }
1446- b.Reset()
1447- returns.Return(b)
1448 case <-stats.C():1440 case <-stats.C():
1449 now := clk.Now()1441 now := clk.Now()
1450 pipeline.Retire(scene) // an idle child of an ended session goes1442 pipeline.Retire(scene) // an idle child of an ended session goes
@@ -1663,15 +1655,7 @@ func (o *Output) completed(ev flipEvent, seen map[ports.WindowID]uint64) bool {
1663 1655
1664 // wakeCore tells core the next frame of a running animation is due. A frame1656 // wakeCore tells core the next frame of a running animation is due. A frame
1665 // core has not read yet is enough, so the owner never waits.1657 // core has not read yet is enough, so the owner never waits.
1666-func (o *Output) wakeCore() {1658+func (o *Output) wakeCore() { corewake.Send(o.Frames, o.conn.name) }
1667- if o.Frames == nil {
1668- return
1669- }
1670- select {
1671- case o.Frames <- ports.OutputFrame{Output: o.conn.name}:
1672- default:
1673- }
1674-}
1675 1659
1676 // setCursor loads a cursor for scale and output transform t into the cursor1660 // setCursor loads a cursor for scale and output transform t into the cursor
1677 // images.1661 // images.
M internal/adapters/drm/report.go +4 -16
@@ -1,9 +1,6 @@
11 package drm
22
33 import (
4- "maps"
5-
6- "github.com/bnema/neferwl/internal/adapters/outputkit/surfaces"
74 "github.com/bnema/neferwl/internal/ports"
85 )
96
@@ -20,20 +17,11 @@ func (o *Output) report(flip *ports.FlipInfo, seen map[ports.WindowID]uint64) {
2017 o.reports.Push(ports.OutputPresented{Output: o.conn.name, Flip: flip, Shown: o.shown, Queued: o.queued, Seen: seen, ChildReads: reads})
2118 }
2219
23-// shownBy returns an immutable snapshot of the content Seq of drawn windows.
24-// The scratch map is private to the output; a snapshot may still be read by
25-// wayland after a subsequent flip, so never refill a previously sent map.
20+// shownBy returns an immutable snapshot of the content Seq of drawn windows
21+// (see surfaces.ShowsSnapshot). A closed window drawn from its kept content is
22+// not shown to its client: the empty content has no frame to present.
2623 func (o *Output) shownBy(s ports.Scene, seen map[ports.WindowID]uint64) map[ports.WindowID]uint64 {
27- if o.showsScratch == nil {
28- o.showsScratch = make(map[ports.WindowID]uint64)
29- }
30- // A closed window drawn from its kept content is not shown to its
31- // client: the empty content has no frame to present.
32- surfaces.FillShown(o.showsScratch, seen, s, o.kept)
33- if !maps.Equal(o.showsSnapshot, o.showsScratch) {
34- o.showsSnapshot = maps.Clone(o.showsScratch)
35- }
36- return o.showsSnapshot
24+ return o.shows.FromSeen(s, seen, o.kept)
3725 }
3826
3927 func (o *Output) directShownBy(id ports.WindowID, seq uint64) map[ports.WindowID]uint64 {
@@ -1,9 +1,6 @@
1 package drm1 package drm
2 2
3 import (3 import (
4- "maps"
5-
6- "github.com/bnema/neferwl/internal/adapters/outputkit/surfaces"
7 "github.com/bnema/neferwl/internal/ports"4 "github.com/bnema/neferwl/internal/ports"
8 )5 )
9 6
@@ -20,20 +17,11 @@ func (o *Output) report(flip *ports.FlipInfo, seen map[ports.WindowID]uint64) {
20 o.reports.Push(ports.OutputPresented{Output: o.conn.name, Flip: flip, Shown: o.shown, Queued: o.queued, Seen: seen, ChildReads: reads})17 o.reports.Push(ports.OutputPresented{Output: o.conn.name, Flip: flip, Shown: o.shown, Queued: o.queued, Seen: seen, ChildReads: reads})
21 }18 }
22 19
23-// shownBy returns an immutable snapshot of the content Seq of drawn windows.20+// shownBy returns an immutable snapshot of the content Seq of drawn windows
24-// The scratch map is private to the output; a snapshot may still be read by21+// (see surfaces.ShowsSnapshot). A closed window drawn from its kept content is
25-// wayland after a subsequent flip, so never refill a previously sent map.22+// not shown to its client: the empty content has no frame to present.
26 func (o *Output) shownBy(s ports.Scene, seen map[ports.WindowID]uint64) map[ports.WindowID]uint64 {23 func (o *Output) shownBy(s ports.Scene, seen map[ports.WindowID]uint64) map[ports.WindowID]uint64 {
27- if o.showsScratch == nil {24+ return o.shows.FromSeen(s, seen, o.kept)
28- o.showsScratch = make(map[ports.WindowID]uint64)
29- }
30- // A closed window drawn from its kept content is not shown to its
31- // client: the empty content has no frame to present.
32- surfaces.FillShown(o.showsScratch, seen, s, o.kept)
33- if !maps.Equal(o.showsSnapshot, o.showsScratch) {
34- o.showsSnapshot = maps.Clone(o.showsScratch)
35- }
36- return o.showsSnapshot
37 }25 }
38 26
39 func (o *Output) directShownBy(id ports.WindowID, seq uint64) map[ports.WindowID]uint64 {27 func (o *Output) directShownBy(id ports.WindowID, seq uint64) map[ports.WindowID]uint64 {
M internal/adapters/headless/output.go +12 -35
@@ -5,7 +5,6 @@ import (
55 "fmt"
66 "image"
77 "image/png"
8- "maps"
98 "os"
109 "path/filepath"
1110 "time"
@@ -14,6 +13,7 @@ import (
1413
1514 "github.com/bnema/neferwl/internal/adapters/clock"
1615 "github.com/bnema/neferwl/internal/adapters/outputkit/capture"
16+ "github.com/bnema/neferwl/internal/adapters/outputkit/corewake"
1717 "github.com/bnema/neferwl/internal/adapters/outputkit/presented"
1818 "github.com/bnema/neferwl/internal/adapters/outputkit/surfaces"
1919 "github.com/bnema/neferwl/internal/adapters/syncfile"
@@ -193,14 +193,8 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
193193 table.Prune(scene)
194194 }
195195 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)
196+ batchDirty, _ := table.Apply(b, scene, seen, returns)
197+ dirty = dirty || batchDirty
204198 }
205199 clk := opts.Clock
206200 if clk == nil {
@@ -544,11 +538,8 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
544538 requests.List = capture.Waiting(requests.List, scene)
545539 frame++
546540 opts.report(&reports, shows.flipInfo(scene, drawn, table.Kept), seen)
547- if scene.Animating && opts.Frames != nil {
548- select {
549- case opts.Frames <- ports.OutputFrame{Output: opts.Name}:
550- default:
551- }
541+ if scene.Animating {
542+ corewake.Send(opts.Frames, opts.Name)
552543 }
553544 if opts.ScreenshotDir != "" && !opts.protected() {
554545 shot := r.Pixels()
@@ -630,31 +621,17 @@ func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[po
630621 }
631622
632623 // flipShows builds the Shows of successive flips. Its owner goroutine is the
633-// only user. A flip's Shows map is an immutable snapshot: wayland may still
634-// read the one of an earlier flip on another goroutine, so the scratch map is
635-// never sent and a snapshot is replaced by a clone only when it changed.
624+// only user. The Shows snapshot is shared with earlier flips of the same
625+// content (see surfaces.ShowsSnapshot): never modify it.
636626 type flipShows struct {
637- scratch, snapshot map[ports.WindowID]uint64
627+ snapshot surfaces.ShowsSnapshot
638628 }
639629
640-// of returns the content Seq of the windows scene showed to their client. The
641-// result is shared with earlier flips of the same content: never modify it.
630+// of returns the content Seq of the windows scene showed to their client.
642631 func (f *flipShows) of(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) map[ports.WindowID]uint64 {
643- if f.scratch == nil {
644- f.scratch = make(map[ports.WindowID]uint64)
645- }
646- clear(f.scratch)
647- for id, c := range contents {
648- // A closed window drawn from its kept content is not shown to
649- // its client.
650- if surfaces.Shown(scene, id, kept) {
651- f.scratch[id] = c.Seq
652- }
653- }
654- if f.snapshot == nil || !maps.Equal(f.snapshot, f.scratch) {
655- f.snapshot = maps.Clone(f.scratch)
656- }
657- return f.snapshot
632+ // A closed window drawn from its kept content is not shown to its
633+ // client.
634+ return f.snapshot.FromContents(scene, contents, kept)
658635 }
659636
660637 // flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC
@@ -5,7 +5,6 @@ import (
5 "fmt"5 "fmt"
6 "image"6 "image"
7 "image/png"7 "image/png"
8- "maps"
9 "os"8 "os"
10 "path/filepath"9 "path/filepath"
11 "time"10 "time"
@@ -14,6 +13,7 @@ import (
14 13
15 "github.com/bnema/neferwl/internal/adapters/clock"14 "github.com/bnema/neferwl/internal/adapters/clock"
16 "github.com/bnema/neferwl/internal/adapters/outputkit/capture"15 "github.com/bnema/neferwl/internal/adapters/outputkit/capture"
16+ "github.com/bnema/neferwl/internal/adapters/outputkit/corewake"
17 "github.com/bnema/neferwl/internal/adapters/outputkit/presented"17 "github.com/bnema/neferwl/internal/adapters/outputkit/presented"
18 "github.com/bnema/neferwl/internal/adapters/outputkit/surfaces"18 "github.com/bnema/neferwl/internal/adapters/outputkit/surfaces"
19 "github.com/bnema/neferwl/internal/adapters/syncfile"19 "github.com/bnema/neferwl/internal/adapters/syncfile"
@@ -193,14 +193,8 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
193 table.Prune(scene)193 table.Prune(scene)
194 }194 }
195 update := func(b *ports.ContentBatch) {195 update := func(b *ports.ContentBatch) {
196- for i := range b.Items {196+ batchDirty, _ := table.Apply(b, scene, seen, returns)
197- c := b.Items[i]197+ dirty = dirty || batchDirty
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)
204 }198 }
205 clk := opts.Clock199 clk := opts.Clock
206 if clk == nil {200 if clk == nil {
@@ -544,11 +538,8 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
544 requests.List = capture.Waiting(requests.List, scene)538 requests.List = capture.Waiting(requests.List, scene)
545 frame++539 frame++
546 opts.report(&reports, shows.flipInfo(scene, drawn, table.Kept), seen)540 opts.report(&reports, shows.flipInfo(scene, drawn, table.Kept), seen)
547- if scene.Animating && opts.Frames != nil {541+ if scene.Animating {
548- select {542+ corewake.Send(opts.Frames, opts.Name)
549- case opts.Frames <- ports.OutputFrame{Output: opts.Name}:
550- default:
551- }
552 }543 }
553 if opts.ScreenshotDir != "" && !opts.protected() {544 if opts.ScreenshotDir != "" && !opts.protected() {
554 shot := r.Pixels()545 shot := r.Pixels()
@@ -630,31 +621,17 @@ func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[po
630 }621 }
631 622
632 // flipShows builds the Shows of successive flips. Its owner goroutine is the623 // flipShows builds the Shows of successive flips. Its owner goroutine is the
633-// only user. A flip's Shows map is an immutable snapshot: wayland may still624+// only user. The Shows snapshot is shared with earlier flips of the same
634-// read the one of an earlier flip on another goroutine, so the scratch map is625+// content (see surfaces.ShowsSnapshot): never modify it.
635-// never sent and a snapshot is replaced by a clone only when it changed.
636 type flipShows struct {626 type flipShows struct {
637- scratch, snapshot map[ports.WindowID]uint64627+ snapshot surfaces.ShowsSnapshot
638 }628 }
639 629
640-// of returns the content Seq of the windows scene showed to their client. The630+// of returns the content Seq of the windows scene showed to their client.
641-// result is shared with earlier flips of the same content: never modify it.
642 func (f *flipShows) of(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) map[ports.WindowID]uint64 {631 func (f *flipShows) of(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) map[ports.WindowID]uint64 {
643- if f.scratch == nil {632+ // A closed window drawn from its kept content is not shown to its
644- f.scratch = make(map[ports.WindowID]uint64)633+ // client.
645- }634+ return f.snapshot.FromContents(scene, contents, kept)
646- clear(f.scratch)
647- for id, c := range contents {
648- // A closed window drawn from its kept content is not shown to
649- // its client.
650- if surfaces.Shown(scene, id, kept) {
651- f.scratch[id] = c.Seq
652- }
653- }
654- if f.snapshot == nil || !maps.Equal(f.snapshot, f.scratch) {
655- f.snapshot = maps.Clone(f.scratch)
656- }
657- return f.snapshot
658 }635 }
659 636
660 // flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC637 // flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC
A internal/adapters/outputkit/corewake/corewake.go +19 -0
@@ -0,0 +1,19 @@
1+// Package corewake is how an output asks core for the next frame of a running
2+// animation.
3+package corewake
4+
5+import "github.com/bnema/neferwl/internal/ports"
6+
7+// Send tells core the next frame of a running animation of output is due.
8+// A frame core has not read yet is enough, so the owner never waits: a full
9+// channel drops the wake. A nil ch (tests, nobody to wake) does nothing.
10+// It allocates nothing.
11+func Send(ch chan<- ports.OutputFrame, output string) {
12+ if ch == nil {
13+ return
14+ }
15+ select {
16+ case ch <- ports.OutputFrame{Output: output}:
17+ default:
18+ }
19+}
@@ -0,0 +1,19 @@
1+// Package corewake is how an output asks core for the next frame of a running
2+// animation.
3+package corewake
4+
5+import "github.com/bnema/neferwl/internal/ports"
6+
7+// Send tells core the next frame of a running animation of output is due.
8+// A frame core has not read yet is enough, so the owner never waits: a full
9+// channel drops the wake. A nil ch (tests, nobody to wake) does nothing.
10+// It allocates nothing.
11+func Send(ch chan<- ports.OutputFrame, output string) {
12+ if ch == nil {
13+ return
14+ }
15+ select {
16+ case ch <- ports.OutputFrame{Output: output}:
17+ default:
18+ }
19+}
A internal/adapters/outputkit/corewake/corewake_test.go +30 -0
@@ -0,0 +1,30 @@
1+package corewake
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+ "github.com/stretchr/testify/require"
8+)
9+
10+// Send never blocks: a frame core has not read is enough.
11+func TestSend(t *testing.T) {
12+ ch := make(chan ports.OutputFrame, 1)
13+ Send(ch, "DP-1")
14+ Send(ch, "DP-1") // full: dropped, not blocked
15+ require.Equal(t, []ports.OutputFrame{{Output: "DP-1"}}, []ports.OutputFrame{<-ch})
16+ require.Empty(t, ch)
17+ require.NotPanics(t, func() { Send(nil, "DP-1") })
18+}
19+
20+func TestSendAllocations(t *testing.T) {
21+ ch := make(chan ports.OutputFrame, 1)
22+ require.Zero(t, testing.AllocsPerRun(100, func() {
23+ Send(ch, "DP-1")
24+ select {
25+ case <-ch:
26+ default:
27+ }
28+ Send(ch, "DP-1")
29+ }))
30+}
@@ -0,0 +1,30 @@
1+package corewake
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+ "github.com/stretchr/testify/require"
8+)
9+
10+// Send never blocks: a frame core has not read is enough.
11+func TestSend(t *testing.T) {
12+ ch := make(chan ports.OutputFrame, 1)
13+ Send(ch, "DP-1")
14+ Send(ch, "DP-1") // full: dropped, not blocked
15+ require.Equal(t, []ports.OutputFrame{{Output: "DP-1"}}, []ports.OutputFrame{<-ch})
16+ require.Empty(t, ch)
17+ require.NotPanics(t, func() { Send(nil, "DP-1") })
18+}
19+
20+func TestSendAllocations(t *testing.T) {
21+ ch := make(chan ports.OutputFrame, 1)
22+ require.Zero(t, testing.AllocsPerRun(100, func() {
23+ Send(ch, "DP-1")
24+ select {
25+ case <-ch:
26+ default:
27+ }
28+ Send(ch, "DP-1")
29+ }))
30+}
M internal/adapters/outputkit/surfaces/surfaces.go +69 -5
@@ -4,6 +4,8 @@
44 package surfaces
55
66 import (
7+ "maps"
8+
79 "github.com/bnema/neferwl/internal/adapters/outputkit/capture"
810 "github.com/bnema/neferwl/internal/ports"
911 )
@@ -106,13 +108,75 @@ func Shown(scene ports.Scene, id ports.WindowID, kept func(ports.WindowID) bool)
106108 return scene.Shows(id) && (kept == nil || !kept(id))
107109 }
108110
109-// FillShown clears dst and fills it with the windows of seen that scene
110-// showed to their client (Shown), at their seen Seq.
111-func FillShown(dst, seen map[ports.WindowID]uint64, scene ports.Scene, kept func(ports.WindowID) bool) {
112- clear(dst)
111+// Apply takes every content of b into the table, as the output's owner does
112+// when it receives a batch: Update per item against scene, seen advanced to
113+// the highest Seq per window, then the batch reset and handed back through
114+// ret. Reset-then-return is the ownership rule of ports.ContentBatch; b is
115+// not the caller's after Apply.
116+//
117+// dirty reports a content of a window scene draws (windows on other outputs
118+// or workspaces need no frame). seenChanged reports a window whose seen Seq
119+// moved forward: a new report is due.
120+func (t *Table) Apply(b *ports.ContentBatch, scene ports.Scene, seen map[ports.WindowID]uint64, ret ports.ContentReturn) (dirty, seenChanged bool) {
121+ for i := range b.Items {
122+ c := b.Items[i]
123+ t.Update(c, scene)
124+ if c.Seq > seen[c.ID] {
125+ seen[c.ID] = c.Seq
126+ seenChanged = true
127+ }
128+ dirty = dirty || capture.Shows(scene, c.ID)
129+ }
130+ b.Reset()
131+ ret.Return(b)
132+ return dirty, seenChanged
133+}
134+
135+// ShowsSnapshot builds the Shows of successive flips. Its owner goroutine is
136+// the only user. A flip's Shows map is an immutable snapshot: wayland may
137+// still read the one of an earlier flip on another goroutine, so the scratch
138+// map is never sent and a snapshot is replaced by a clone only when it
139+// changed. The zero value is ready; an unchanged result allocates nothing.
140+type ShowsSnapshot struct {
141+ scratch, snapshot map[ports.WindowID]uint64
142+}
143+
144+// FromSeen returns the windows of seen that scene showed to their client
145+// (Shown), at their seen Seq. The result is shared with earlier flips of the
146+// same content: never modify it.
147+func (s *ShowsSnapshot) FromSeen(scene ports.Scene, seen map[ports.WindowID]uint64, kept func(ports.WindowID) bool) map[ports.WindowID]uint64 {
148+ s.begin()
113149 for id, seq := range seen {
114150 if Shown(scene, id, kept) {
115- dst[id] = seq
151+ s.scratch[id] = seq
116152 }
117153 }
154+ return s.settle()
155+}
156+
157+// FromContents returns the windows of contents that scene showed to their
158+// client (Shown), at their content Seq. The result is shared with earlier
159+// flips of the same content: never modify it.
160+func (s *ShowsSnapshot) FromContents(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) map[ports.WindowID]uint64 {
161+ s.begin()
162+ for id, c := range contents {
163+ if Shown(scene, id, kept) {
164+ s.scratch[id] = c.Seq
165+ }
166+ }
167+ return s.settle()
168+}
169+
170+func (s *ShowsSnapshot) begin() {
171+ if s.scratch == nil {
172+ s.scratch = make(map[ports.WindowID]uint64)
173+ }
174+ clear(s.scratch)
175+}
176+
177+func (s *ShowsSnapshot) settle() map[ports.WindowID]uint64 {
178+ if s.snapshot == nil || !maps.Equal(s.snapshot, s.scratch) {
179+ s.snapshot = maps.Clone(s.scratch)
180+ }
181+ return s.snapshot
118182 }
@@ -4,6 +4,8 @@
4 package surfaces4 package surfaces
5 5
6 import (6 import (
7+ "maps"
8+
7 "github.com/bnema/neferwl/internal/adapters/outputkit/capture"9 "github.com/bnema/neferwl/internal/adapters/outputkit/capture"
8 "github.com/bnema/neferwl/internal/ports"10 "github.com/bnema/neferwl/internal/ports"
9 )11 )
@@ -106,13 +108,75 @@ func Shown(scene ports.Scene, id ports.WindowID, kept func(ports.WindowID) bool)
106 return scene.Shows(id) && (kept == nil || !kept(id))108 return scene.Shows(id) && (kept == nil || !kept(id))
107 }109 }
108 110
109-// FillShown clears dst and fills it with the windows of seen that scene111+// Apply takes every content of b into the table, as the output's owner does
110-// showed to their client (Shown), at their seen Seq.112+// when it receives a batch: Update per item against scene, seen advanced to
111-func FillShown(dst, seen map[ports.WindowID]uint64, scene ports.Scene, kept func(ports.WindowID) bool) {113+// the highest Seq per window, then the batch reset and handed back through
112- clear(dst)114+// ret. Reset-then-return is the ownership rule of ports.ContentBatch; b is
115+// not the caller's after Apply.
116+//
117+// dirty reports a content of a window scene draws (windows on other outputs
118+// or workspaces need no frame). seenChanged reports a window whose seen Seq
119+// moved forward: a new report is due.
120+func (t *Table) Apply(b *ports.ContentBatch, scene ports.Scene, seen map[ports.WindowID]uint64, ret ports.ContentReturn) (dirty, seenChanged bool) {
121+ for i := range b.Items {
122+ c := b.Items[i]
123+ t.Update(c, scene)
124+ if c.Seq > seen[c.ID] {
125+ seen[c.ID] = c.Seq
126+ seenChanged = true
127+ }
128+ dirty = dirty || capture.Shows(scene, c.ID)
129+ }
130+ b.Reset()
131+ ret.Return(b)
132+ return dirty, seenChanged
133+}
134+
135+// ShowsSnapshot builds the Shows of successive flips. Its owner goroutine is
136+// the only user. A flip's Shows map is an immutable snapshot: wayland may
137+// still read the one of an earlier flip on another goroutine, so the scratch
138+// map is never sent and a snapshot is replaced by a clone only when it
139+// changed. The zero value is ready; an unchanged result allocates nothing.
140+type ShowsSnapshot struct {
141+ scratch, snapshot map[ports.WindowID]uint64
142+}
143+
144+// FromSeen returns the windows of seen that scene showed to their client
145+// (Shown), at their seen Seq. The result is shared with earlier flips of the
146+// same content: never modify it.
147+func (s *ShowsSnapshot) FromSeen(scene ports.Scene, seen map[ports.WindowID]uint64, kept func(ports.WindowID) bool) map[ports.WindowID]uint64 {
148+ s.begin()
113 for id, seq := range seen {149 for id, seq := range seen {
114 if Shown(scene, id, kept) {150 if Shown(scene, id, kept) {
115- dst[id] = seq151+ s.scratch[id] = seq
116 }152 }
117 }153 }
154+ return s.settle()
155+}
156+
157+// FromContents returns the windows of contents that scene showed to their
158+// client (Shown), at their content Seq. The result is shared with earlier
159+// flips of the same content: never modify it.
160+func (s *ShowsSnapshot) FromContents(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) map[ports.WindowID]uint64 {
161+ s.begin()
162+ for id, c := range contents {
163+ if Shown(scene, id, kept) {
164+ s.scratch[id] = c.Seq
165+ }
166+ }
167+ return s.settle()
168+}
169+
170+func (s *ShowsSnapshot) begin() {
171+ if s.scratch == nil {
172+ s.scratch = make(map[ports.WindowID]uint64)
173+ }
174+ clear(s.scratch)
175+}
176+
177+func (s *ShowsSnapshot) settle() map[ports.WindowID]uint64 {
178+ if s.snapshot == nil || !maps.Equal(s.snapshot, s.scratch) {
179+ s.snapshot = maps.Clone(s.scratch)
180+ }
181+ return s.snapshot
118 }182 }
M internal/adapters/outputkit/surfaces/surfaces_test.go +64 -5
@@ -4,6 +4,7 @@ import (
44 "os"
55 "testing"
66
7+ portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
78 "github.com/bnema/neferwl/internal/ports"
89 )
910
@@ -125,14 +126,72 @@ func TestKeptChildrenFencesDroppedOnACopy(t *testing.T) {
125126
126127 // Both backends report a window shown only when the scene draws it and its
127128 // content is not kept.
128-func TestFillShown(t *testing.T) {
129+func TestShowsSnapshotSkipsKept(t *testing.T) {
129130 kept := func(id ports.WindowID) bool { return id == 2 }
130- dst := map[ports.WindowID]uint64{9: 9}
131- FillShown(dst, map[ports.WindowID]uint64{1: 3, 2: 4, 3: 5}, shows(1, 2), kept)
132- if len(dst) != 1 || dst[1] != 3 {
133- t.Fatalf("shown %v", dst)
131+ var snap ShowsSnapshot
132+ got := snap.FromSeen(shows(1, 2), map[ports.WindowID]uint64{1: 3, 2: 4, 3: 5}, kept)
133+ if len(got) != 1 || got[1] != 3 {
134+ t.Fatalf("shown from seen %v", got)
135+ }
136+ var fromContents ShowsSnapshot
137+ contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Seq: 3}, 2: {ID: 2, Seq: 4}, 3: {ID: 3, Seq: 5}}
138+ got = fromContents.FromContents(shows(1, 2), contents, kept)
139+ if len(got) != 1 || got[1] != 3 {
140+ t.Fatalf("shown from contents %v", got)
134141 }
135142 if !Shown(shows(2), 2, nil) {
136143 t.Fatal("nil kept counts nothing as kept")
137144 }
138145 }
146+
147+// An unchanged snapshot is reused without allocating; a changed one never
148+// mutates a map already handed out.
149+func TestShowsSnapshotImmutable(t *testing.T) {
150+ var snap ShowsSnapshot
151+ scene := shows(1)
152+ seen := map[ports.WindowID]uint64{1: 1, 2: 2}
153+ first := snap.FromSeen(scene, seen, nil)
154+ if allocs := testing.AllocsPerRun(100, func() { snap.FromSeen(scene, seen, nil) }); allocs != 0 {
155+ t.Errorf("unchanged shows: %.1f allocs, want 0", allocs)
156+ }
157+ seen[1] = 3
158+ second := snap.FromSeen(scene, seen, nil)
159+ if first[1] != 1 || second[1] != 3 {
160+ t.Fatalf("snapshot mutated: old %v new %v", first, second)
161+ }
162+}
163+
164+// Apply takes the batch into the table, advances seen, reports what needs a
165+// frame or a report, then resets the batch before giving it back.
166+func TestApply(t *testing.T) {
167+ tb := New()
168+ seen := map[ports.WindowID]uint64{1: 5}
169+ b := &ports.ContentBatch{Items: []ports.SurfaceContent{
170+ {ID: 1, Seq: 4, SHM: &ports.SHMBuffer{Pool: 1}}, // older than seen
171+ {ID: 2, Seq: 1, SHM: &ports.SHMBuffer{Pool: 2}},
172+ }}
173+ ret := portsmocks.NewMockContentReturn(t)
174+ ret.EXPECT().Return(b).Run(func(got *ports.ContentBatch) {
175+ if len(got.Items) != 0 {
176+ t.Errorf("batch returned before its reset: %d items", len(got.Items))
177+ }
178+ }).Once()
179+ dirty, changed := tb.Apply(b, shows(2), seen, ret)
180+ if !dirty || !changed {
181+ t.Fatalf("dirty %v changed %v, want both", dirty, changed)
182+ }
183+ if seen[1] != 5 || seen[2] != 1 {
184+ t.Fatalf("seen %v", seen)
185+ }
186+ if len(tb.Map()) != 2 {
187+ t.Fatalf("table %v", tb.Map())
188+ }
189+
190+ // A window no scene draws and no newer Seq: nothing to do.
191+ b = &ports.ContentBatch{Items: []ports.SurfaceContent{{ID: 1, Seq: 5, SHM: &ports.SHMBuffer{Pool: 1}}}}
192+ ret.EXPECT().Return(b).Once()
193+ dirty, changed = tb.Apply(b, shows(2), seen, ret)
194+ if dirty || changed {
195+ t.Fatalf("dirty %v changed %v, want neither", dirty, changed)
196+ }
197+}
@@ -4,6 +4,7 @@ import (
4 "os"4 "os"
5 "testing"5 "testing"
6 6
7+ portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
7 "github.com/bnema/neferwl/internal/ports"8 "github.com/bnema/neferwl/internal/ports"
8 )9 )
9 10
@@ -125,14 +126,72 @@ func TestKeptChildrenFencesDroppedOnACopy(t *testing.T) {
125 126
126 // Both backends report a window shown only when the scene draws it and its127 // Both backends report a window shown only when the scene draws it and its
127 // content is not kept.128 // content is not kept.
128-func TestFillShown(t *testing.T) {129+func TestShowsSnapshotSkipsKept(t *testing.T) {
129 kept := func(id ports.WindowID) bool { return id == 2 }130 kept := func(id ports.WindowID) bool { return id == 2 }
130- dst := map[ports.WindowID]uint64{9: 9}131+ var snap ShowsSnapshot
131- FillShown(dst, map[ports.WindowID]uint64{1: 3, 2: 4, 3: 5}, shows(1, 2), kept)132+ got := snap.FromSeen(shows(1, 2), map[ports.WindowID]uint64{1: 3, 2: 4, 3: 5}, kept)
132- if len(dst) != 1 || dst[1] != 3 {133+ if len(got) != 1 || got[1] != 3 {
133- t.Fatalf("shown %v", dst)134+ t.Fatalf("shown from seen %v", got)
135+ }
136+ var fromContents ShowsSnapshot
137+ contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Seq: 3}, 2: {ID: 2, Seq: 4}, 3: {ID: 3, Seq: 5}}
138+ got = fromContents.FromContents(shows(1, 2), contents, kept)
139+ if len(got) != 1 || got[1] != 3 {
140+ t.Fatalf("shown from contents %v", got)
134 }141 }
135 if !Shown(shows(2), 2, nil) {142 if !Shown(shows(2), 2, nil) {
136 t.Fatal("nil kept counts nothing as kept")143 t.Fatal("nil kept counts nothing as kept")
137 }144 }
138 }145 }
146+
147+// An unchanged snapshot is reused without allocating; a changed one never
148+// mutates a map already handed out.
149+func TestShowsSnapshotImmutable(t *testing.T) {
150+ var snap ShowsSnapshot
151+ scene := shows(1)
152+ seen := map[ports.WindowID]uint64{1: 1, 2: 2}
153+ first := snap.FromSeen(scene, seen, nil)
154+ if allocs := testing.AllocsPerRun(100, func() { snap.FromSeen(scene, seen, nil) }); allocs != 0 {
155+ t.Errorf("unchanged shows: %.1f allocs, want 0", allocs)
156+ }
157+ seen[1] = 3
158+ second := snap.FromSeen(scene, seen, nil)
159+ if first[1] != 1 || second[1] != 3 {
160+ t.Fatalf("snapshot mutated: old %v new %v", first, second)
161+ }
162+}
163+
164+// Apply takes the batch into the table, advances seen, reports what needs a
165+// frame or a report, then resets the batch before giving it back.
166+func TestApply(t *testing.T) {
167+ tb := New()
168+ seen := map[ports.WindowID]uint64{1: 5}
169+ b := &ports.ContentBatch{Items: []ports.SurfaceContent{
170+ {ID: 1, Seq: 4, SHM: &ports.SHMBuffer{Pool: 1}}, // older than seen
171+ {ID: 2, Seq: 1, SHM: &ports.SHMBuffer{Pool: 2}},
172+ }}
173+ ret := portsmocks.NewMockContentReturn(t)
174+ ret.EXPECT().Return(b).Run(func(got *ports.ContentBatch) {
175+ if len(got.Items) != 0 {
176+ t.Errorf("batch returned before its reset: %d items", len(got.Items))
177+ }
178+ }).Once()
179+ dirty, changed := tb.Apply(b, shows(2), seen, ret)
180+ if !dirty || !changed {
181+ t.Fatalf("dirty %v changed %v, want both", dirty, changed)
182+ }
183+ if seen[1] != 5 || seen[2] != 1 {
184+ t.Fatalf("seen %v", seen)
185+ }
186+ if len(tb.Map()) != 2 {
187+ t.Fatalf("table %v", tb.Map())
188+ }
189+
190+ // A window no scene draws and no newer Seq: nothing to do.
191+ b = &ports.ContentBatch{Items: []ports.SurfaceContent{{ID: 1, Seq: 5, SHM: &ports.SHMBuffer{Pool: 1}}}}
192+ ret.EXPECT().Return(b).Once()
193+ dirty, changed = tb.Apply(b, shows(2), seen, ret)
194+ if dirty || changed {
195+ t.Fatalf("dirty %v changed %v, want neither", dirty, changed)
196+ }
197+}

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