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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
8 files · +206 -85
M Makefile +1 -1
| @@ -61,7 +61,7 @@ arch: adapter-imports | |||
| 61 | 61 | # Adapters stay independent: production code may import only the shared | |
| 62 | 62 | # adapter libraries below. outputkit holds what both output backends (drm, | |
| 63 | 63 | # 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 | |
| 65 | 65 | adapter-imports: | |
| 66 | 66 | @edges=$$(CGO_ENABLED=0 go list -f '{{$$p := .ImportPath}}{{range .Imports}}{{$$p}} {{.}}{{"\n"}}{{end}}' ./internal/adapters/...) || exit 1; \ | |
| 67 | 67 | 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 shared | 61 | # 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|xkb | 64 | + | 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 ( | |||
| 11 | 11 | ||
| 12 | 12 | "github.com/bnema/neferwl/internal/adapters/clock" | |
| 13 | 13 | "github.com/bnema/neferwl/internal/adapters/outputkit/capture" | |
| 14 | + | "github.com/bnema/neferwl/internal/adapters/outputkit/corewake" | |
| 14 | 15 | "github.com/bnema/neferwl/internal/adapters/outputkit/presented" | |
| 15 | 16 | "github.com/bnema/neferwl/internal/adapters/outputkit/surfaces" | |
| 16 | 17 | ||
| @@ -116,9 +117,8 @@ type Output struct { | |||
| 116 | 117 | reports presented.Queue | |
| 117 | 118 | // Last immutable shows snapshot sent in a flip. Never mutate it: wayland | |
| 118 | 119 | // may still be reading a prior report on another goroutine. | |
| 119 | - | showsSnapshot map[ports.WindowID]uint64 | |
| 120 | + | shows surfaces.ShowsSnapshot | |
| 120 | 121 | directShowsSnapshot map[ports.WindowID]uint64 | |
| 121 | - | showsScratch map[ports.WindowID]uint64 | |
| 122 | 122 | // Tearing (ADR 006): async commits of scanned-out buffers that ask for | |
| 123 | 123 | // them, when the card has DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. asyncFence | |
| 124 | 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 | 1433 | stateDirty = true | |
| 1434 | 1434 | } | |
| 1435 | 1435 | 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 | |
| 1448 | 1440 | case <-stats.C(): | |
| 1449 | 1441 | now := clk.Now() | |
| 1450 | 1442 | 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 | 1656 | // wakeCore tells core the next frame of a running animation is due. A frame | |
| 1665 | 1657 | // 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) } | |
| 1675 | 1659 | ||
| 1676 | 1660 | // setCursor loads a cursor for scale and output transform t into the cursor | |
| 1677 | 1661 | // 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.Queue | 117 | reports presented.Queue | ||
| 117 | // Last immutable shows snapshot sent in a flip. Never mutate it: wayland | 118 | // 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]uint64 | 120 | + | shows surfaces.ShowsSnapshot |
| 120 | directShowsSnapshot map[ports.WindowID]uint64 | 121 | directShowsSnapshot map[ports.WindowID]uint64 | ||
| 121 | - | showsScratch map[ports.WindowID]uint64 | |||
| 122 | // Tearing (ADR 006): async commits of scanned-out buffers that ask for | 122 | // Tearing (ADR 006): async commits of scanned-out buffers that ask for | ||
| 123 | // them, when the card has DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. asyncFence | 123 | // 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 = true | 1433 | 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 goes | 1442 | 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 frame | 1656 | // 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 cursor | 1660 | // 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 @@ | |||
| 1 | 1 | package drm | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "maps" | |
| 5 | - | ||
| 6 | - | "github.com/bnema/neferwl/internal/adapters/outputkit/surfaces" | |
| 7 | 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 | 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. | |
| 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. | |
| 26 | 23 | 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) | |
| 37 | 25 | } | |
| 38 | 26 | ||
| 39 | 27 | func (o *Output) directShownBy(id ports.WindowID, seq uint64) map[ports.WindowID]uint64 { | |
| @@ -1,9 +1,6 @@ | |||||
| 1 | package drm | 1 | 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 by | 21 | + | // (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 ( | |||
| 5 | 5 | "fmt" | |
| 6 | 6 | "image" | |
| 7 | 7 | "image/png" | |
| 8 | - | "maps" | |
| 9 | 8 | "os" | |
| 10 | 9 | "path/filepath" | |
| 11 | 10 | "time" | |
| @@ -14,6 +13,7 @@ import ( | |||
| 14 | 13 | ||
| 15 | 14 | "github.com/bnema/neferwl/internal/adapters/clock" | |
| 16 | 15 | "github.com/bnema/neferwl/internal/adapters/outputkit/capture" | |
| 16 | + | "github.com/bnema/neferwl/internal/adapters/outputkit/corewake" | |
| 17 | 17 | "github.com/bnema/neferwl/internal/adapters/outputkit/presented" | |
| 18 | 18 | "github.com/bnema/neferwl/internal/adapters/outputkit/surfaces" | |
| 19 | 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 | 193 | table.Prune(scene) | |
| 194 | 194 | } | |
| 195 | 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) | |
| 196 | + | batchDirty, _ := table.Apply(b, scene, seen, returns) | |
| 197 | + | dirty = dirty || batchDirty | |
| 204 | 198 | } | |
| 205 | 199 | clk := opts.Clock | |
| 206 | 200 | if clk == nil { | |
| @@ -544,11 +538,8 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 544 | 538 | requests.List = capture.Waiting(requests.List, scene) | |
| 545 | 539 | frame++ | |
| 546 | 540 | 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) | |
| 552 | 543 | } | |
| 553 | 544 | if opts.ScreenshotDir != "" && !opts.protected() { | |
| 554 | 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 | 623 | // 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. | |
| 636 | 626 | type flipShows struct { | |
| 637 | - | scratch, snapshot map[ports.WindowID]uint64 | |
| 627 | + | snapshot surfaces.ShowsSnapshot | |
| 638 | 628 | } | |
| 639 | 629 | ||
| 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. | |
| 642 | 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 { | |
| 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) | |
| 658 | 635 | } | |
| 659 | 636 | ||
| 660 | 637 | // 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.Clock | 199 | 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 the | 623 | // 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 | 624 | + | // 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 is | 625 | + | // 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]uint64 | 627 | + | snapshot surfaces.ShowsSnapshot |
| 638 | } | 628 | } | ||
| 639 | 629 | ||||
| 640 | - | // of returns the content Seq of the windows scene showed to their client. The | 630 | + | // 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_MONOTONIC | 637 | // 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 @@ | |||
| 4 | 4 | package surfaces | |
| 5 | 5 | ||
| 6 | 6 | import ( | |
| 7 | + | "maps" | |
| 8 | + | ||
| 7 | 9 | "github.com/bnema/neferwl/internal/adapters/outputkit/capture" | |
| 8 | 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 | 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 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() | |
| 113 | 149 | for id, seq := range seen { | |
| 114 | 150 | if Shown(scene, id, kept) { | |
| 115 | - | dst[id] = seq | |
| 151 | + | 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 | } | |
| @@ -4,6 +4,8 @@ | |||||
| 4 | package surfaces | 4 | 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 scene | 111 | + | // 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] = seq | 151 | + | 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 ( | |||
| 4 | 4 | "os" | |
| 5 | 5 | "testing" | |
| 6 | 6 | ||
| 7 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |
| 7 | 8 | "github.com/bnema/neferwl/internal/ports" | |
| 8 | 9 | ) | |
| 9 | 10 | ||
| @@ -125,14 +126,72 @@ func TestKeptChildrenFencesDroppedOnACopy(t *testing.T) { | |||
| 125 | 126 | ||
| 126 | 127 | // Both backends report a window shown only when the scene draws it and its | |
| 127 | 128 | // content is not kept. | |
| 128 | - | func TestFillShown(t *testing.T) { | |
| 129 | + | func TestShowsSnapshotSkipsKept(t *testing.T) { | |
| 129 | 130 | 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) | |
| 134 | 141 | } | |
| 135 | 142 | if !Shown(shows(2), 2, nil) { | |
| 136 | 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 | + | } | |
| @@ -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 its | 127 | // 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 | + | } | |||