Skip to content

Commit 7696232

Browse files
authored
perf(linux): paint the grid subgrid and its pointer in one repaint (#1510)
1 parent c225a0d commit 7696232

29 files changed

Lines changed: 809 additions & 104 deletions

docs/adr/0003-overlay-frame-port.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,23 @@ are declarative; updates — hot, already narrow, already correct — are not.
8181
measurable time cost. That is the price this ADR said a fully declarative
8282
port would charge on every keystroke, paid only where the surface was
8383
already being repainted anyway.
84+
- **The update half widens; it does not become a frame.** #1492 is the first
85+
time the hybrid's imperative side hit the problem the declarative side solves
86+
by construction: grid mode's selection keystroke opens a subgrid *and* moves
87+
the pointer, and on Linux both are painted into one Cairo target, so two
88+
calls were two full repaints of it per key — held arrow keys inside a subgrid
89+
included. The frame path would have fixed it and is exactly what this ADR
90+
rules out here, so `ShowGridSubgrid` took the pointer as an argument instead,
91+
the way `RecursiveGridFrame` already carried it. Measured the way this ADR's
92+
previous entry was, interleaved over the simulation harness (n=8, Apple M3):
93+
the selection keystroke's grid-surface updates went 2 → 1 (p=0.000) with time
94+
and allocations unchanged (~4.74µs → ~4.79µs, p=0.65; 18 allocations either
95+
way), and grid narrowing — the keystroke this ADR exists to protect — stayed
96+
unchanged again (~5.25µs both ways, p=0.80). Widening a hot call is therefore
97+
the move when a keystroke changes two things on one surface, and the frame
98+
path is still not. The leaving half took the matching share: `ClearFrame`
99+
drops what the update calls left, because a mode resetting it by hand was a
100+
mode repainting a surface it was about to clear.
84101
- **The last two surfaces converted, and the mode handler kept one overlay
85102
reference.** Done in #1212: `MonitorSelectFrame` carries the displays on
86103
offer, and `ScrollFrame` carries nothing at all — scroll is a mode the

internal/adapter/overlay/AGENTS.md

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

internal/adapter/overlay/adapter.go

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,21 @@ func (a *Adapter) RedrawFrame(ctx context.Context, frame ports.Frame) error {
127127
}
128128

129129
// ClearFrame takes the frame on screen off it and returns the overlay to idle.
130+
//
131+
// It also drops what the grid's incremental calls left behind, which grid mode
132+
// used to reset for itself on the way out (#1492). Both run after the surface
133+
// has been cleared, which is what makes them cost nothing where the reset used
134+
// to cost a repaint: the hide-unmatched flag is a flag, and a backend that
135+
// repaints on a pointer change has already forgotten the pointer, so the hide
136+
// is the statement without the repaint. What each backend does beyond that is
137+
// its own — macOS marks its emptied view for redisplay either way — and this is
138+
// teardown, not a keystroke. The match prefix needs no reset at all: a grid
139+
// coming back up is a transition, which clears and redraws in full.
140+
//
141+
// The pair is unconditional rather than gated on grid mode having been the one
142+
// on screen. The leaving half does not ask which mode it is leaving — that is
143+
// what stops a caller from having to remember — and neither call means anything
144+
// to a surface no grid was drawn on.
130145
func (a *Adapter) ClearFrame(ctx context.Context) error {
131146
err := contextAlive(ctx)
132147
if err != nil {
@@ -138,6 +153,8 @@ func (a *Adapter) ClearFrame(ctx context.Context) error {
138153
a.manager.Clear()
139154
a.manager.ClearCache()
140155
a.hideMonitorSelect()
156+
a.manager.SetHideUnmatched(false)
157+
a.manager.HideGridPointer(ModeGrid)
141158
a.manager.Hide()
142159
a.manager.SwitchTo(ModeIdle)
143160
a.subgridDrawn.Store(false)
@@ -235,9 +252,18 @@ func (a *Adapter) SetGridHideUnmatched(hide bool) {
235252
}
236253

237254
// ShowGridSubgrid opens the finer grid inside one cell, with the grid Style
238-
// the overlay already resolved.
239-
func (a *Adapter) ShowGridSubgrid(cell *domainGrid.Cell) {
240-
a.manager.ShowSubgrid(cell, ResolvedStyle(a.styles).Grid)
255+
// the overlay already resolved and the pointer stand-in that belongs on the
256+
// same surface.
257+
//
258+
// The pointer rides the open rather than following it in a call of its own
259+
// (#1492), which is what makes the keystroke that picks a cell cost one repaint
260+
// on a backend that paints the two into one surface. Meeting it with the
261+
// resolved Style happens here, as it does for the pointer a recursive-grid
262+
// frame carries — appearance never travels from a mode.
263+
func (a *Adapter) ShowGridSubgrid(cell *domainGrid.Cell, pointer ports.GridPointer) {
264+
style := ResolvedStyle(a.styles)
265+
266+
a.manager.ShowSubgrid(cell, style.Grid, gridSurfacePointer(pointer, style.VirtualPointer))
241267
a.subgridDrawn.Store(true)
242268
}
243269

@@ -532,7 +558,7 @@ func (a *Adapter) drawRecursiveGrid(frame ports.RecursiveGridFrame, kind drawKin
532558
frame.NextLayout.Keys,
533559
frame.NextLayout.Dimensions,
534560
style.RecursiveGrid,
535-
recursiveGridPointer(frame.Pointer, style.VirtualPointer),
561+
gridSurfacePointer(frame.Pointer, style.VirtualPointer),
536562
)
537563
if drawErr != nil {
538564
if derrors.IsNotSupported(drawErr) {
@@ -637,10 +663,10 @@ func (a *Adapter) drawScroll(kind drawKind) error {
637663
return nil
638664
}
639665

640-
// recursiveGridPointer meets the pointer a frame describes with the Style the
641-
// overlay resolved. Position is the caller's; everything else is appearance,
642-
// and appearance never travels on a frame.
643-
func recursiveGridPointer(
666+
// gridSurfacePointer meets the pointer a frame or a subgrid open describes with
667+
// the Style the overlay resolved. Position is the caller's; everything else is
668+
// appearance, and appearance never travels from a mode.
669+
func gridSurfacePointer(
644670
pointer ports.GridPointer,
645671
style VirtualPointerStyle,
646672
) overlayRecursiveGrid.VirtualPointerState {

internal/adapter/overlay/adapter_test.go

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ type screenManager struct {
116116
matchPrefix string
117117
hideUnmatched bool
118118
subgridCell *domainGrid.Cell
119+
subgridPointer renderrecursivegrid.VirtualPointerState
120+
subgridOpens int
119121

120122
recursiveGrid recursiveGridDraw
121123
recursiveGridDraws int
@@ -203,8 +205,14 @@ func (m *screenManager) UpdateGridMatches(prefix string) {
203205

204206
func (m *screenManager) SetHideUnmatched(hide bool) { m.hideUnmatched = hide }
205207

206-
func (m *screenManager) ShowSubgrid(cell *domainGrid.Cell, _ rendergrid.Style) {
208+
func (m *screenManager) ShowSubgrid(
209+
cell *domainGrid.Cell,
210+
_ rendergrid.Style,
211+
virtualPointer renderrecursivegrid.VirtualPointerState,
212+
) {
207213
m.subgridCell = cell
214+
m.subgridPointer = virtualPointer
215+
m.subgridOpens++
208216
}
209217

210218
func (m *screenManager) DrawRecursiveGrid(
@@ -499,7 +507,7 @@ func TestAdapterRedrawFrame_ErasesASubgridTheGridReplaces(t *testing.T) {
499507
t.Fatal("fixture grid has no cells")
500508
}
501509

502-
adapter.ShowGridSubgrid(cells[0])
510+
adapter.ShowGridSubgrid(cells[0], ports.GridPointer{})
503511

504512
err := adapter.RedrawFrame(context.Background(), gridFrame(t, ""))
505513
if err != nil {
@@ -651,13 +659,59 @@ func TestAdapterGridUpdates_ReachTheOverlayWithoutAFrame(t *testing.T) {
651659
t.Fatal("fixture grid has no cells")
652660
}
653661

654-
adapter.ShowGridSubgrid(cells[0])
662+
adapter.ShowGridSubgrid(cells[0], ports.GridPointer{})
655663

656664
if manager.subgridCell != cells[0] {
657665
t.Error("the subgrid's cell never reached the overlay")
658666
}
659667
}
660668

669+
// TestAdapterShowGridSubgrid_CarriesThePointerIntoTheSameCall is what #1492
670+
// asks of this seam: the keystroke that picks a cell moves the selection too,
671+
// and a backend that paints both into one surface must be told both at once or
672+
// it repaints that surface twice for one key.
673+
//
674+
// The appearance is asserted with the cell, because the pointer travels as
675+
// position alone from a mode: a subgrid open that carried the position and left
676+
// the Style behind would draw the default glyph rather than the user's.
677+
func TestAdapterShowGridSubgrid_CarriesThePointerIntoTheSameCall(t *testing.T) {
678+
t.Parallel()
679+
680+
manager := newScreenManager()
681+
adapter := overlay.NewAdapter(manager, pointerStyles{}, zap.NewNop())
682+
683+
cells := gridFrame(t, "").Grid.Cells()
684+
if len(cells) == 0 {
685+
t.Fatal("fixture grid has no cells")
686+
}
687+
688+
adapter.ShowGridSubgrid(cells[0], ports.GridPointer{
689+
Visible: true,
690+
Position: image.Pt(120, 240),
691+
})
692+
693+
if manager.subgridOpens != 1 {
694+
t.Fatalf("subgrid opened %d times, want 1", manager.subgridOpens)
695+
}
696+
697+
want := renderrecursivegrid.VirtualPointerState{
698+
Visible: true,
699+
Position: image.Pt(120, 240),
700+
Size: pointerSize,
701+
FillColor: pointerFill,
702+
Char: pointerChar,
703+
FontName: pointerFontFamily,
704+
}
705+
if manager.subgridPointer != want {
706+
t.Errorf("subgrid opened with pointer %+v, want %+v", manager.subgridPointer, want)
707+
}
708+
709+
if manager.pointer.mode != "" {
710+
t.Errorf("the open also made a pointer call of its own (%+v); one call paints both",
711+
manager.pointer)
712+
}
713+
}
714+
661715
// TestAdapterUpdateGridPointer_CarriesTheWholeResolvedAppearance pins what a
662716
// backend that paints the glyph itself needs and used to be denied: the char
663717
// and the font family travel with the size and the fill, so a backend with no
@@ -790,6 +844,47 @@ func TestAdapterClearFrame_TakesTheFrameOffScreen(t *testing.T) {
790844
}
791845
}
792846

847+
// TestAdapterClearFrame_DropsWhatTheGridUpdatesLeft is the other half of "content
848+
// and all" (#1492): the hide-unmatched flag and the pointer stand-in outlive a
849+
// clear unless this call drops them, and grid mode used to drop them itself —
850+
// which meant repainting a grid on the way to taking it off the screen.
851+
//
852+
// It is also what makes that free: both run after the surface is cleared, so a
853+
// backend that repaints on a pointer change has nothing left to repaint and its
854+
// own record is already empty. Losing this call would put the flag back on the
855+
// next session's first draw.
856+
func TestAdapterClearFrame_DropsWhatTheGridUpdatesLeft(t *testing.T) {
857+
t.Parallel()
858+
859+
manager := newScreenManager()
860+
adapter := overlay.NewAdapter(manager, pointerStyles{}, zap.NewNop())
861+
862+
adapter.SetGridHideUnmatched(true)
863+
adapter.UpdateGridPointer(
864+
domain.ModeGrid,
865+
ports.GridPointer{Visible: true, Position: image.Pt(12, 34)},
866+
)
867+
868+
clearErr := adapter.ClearFrame(context.Background())
869+
if clearErr != nil {
870+
t.Fatalf("ClearFrame() error = %v", clearErr)
871+
}
872+
873+
if manager.hideUnmatched {
874+
t.Error("unmatched cells are still asked to hide after the frame was cleared")
875+
}
876+
877+
if manager.pointer.mode != overlay.ModeGrid || manager.pointer.visible {
878+
t.Errorf("grid pointer left as %+v, want it taken off the grid surface",
879+
manager.pointer)
880+
}
881+
882+
// The clear comes first, so neither reset had a surface to paint on.
883+
if manager.cleared == 0 {
884+
t.Error("the surface was never cleared, so the resets ran against a live one")
885+
}
886+
}
887+
793888
// TestAdapterFrame_ReportsACanceledContext keeps a canceled activation from
794889
// drawing over whatever replaced it.
795890
func TestAdapterFrame_ReportsACanceledContext(t *testing.T) {

internal/adapter/overlay/darwin/manager.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,12 +451,24 @@ func (m *Manager) UpdateGridMatches(prefix string) {
451451
m.GridOverlay().UpdateMatches(prefix)
452452
}
453453

454-
// ShowSubgrid shows a subgrid for the specified cell.
455-
func (m *Manager) ShowSubgrid(cell *domainGrid.Cell, style grid.Style) {
454+
// ShowSubgrid shows a subgrid for the specified cell, with the pointer
455+
// stand-in the selection that opened it asks for.
456+
//
457+
// The pointer is a layer of the overlay window here rather than something
458+
// painted into the cells, so applying it after the subgrid costs a layer
459+
// update and no redraw — which is why this platform never paid the double
460+
// repaint #1492 is about, and why taking the pointer as an argument changes
461+
// nothing it does.
462+
func (m *Manager) ShowSubgrid(
463+
cell *domainGrid.Cell,
464+
style grid.Style,
465+
virtualPointer recursivegrid.VirtualPointerState,
466+
) {
456467
if m.GridOverlay() == nil {
457468
return
458469
}
459470
m.GridOverlay().ShowSubgrid(cell, style)
471+
m.ApplyGridPointer(manager.ModeGrid, virtualPointer)
460472
}
461473

462474
// SetHideUnmatched sets whether to hide unmatched cells.

internal/adapter/overlay/headless_manager_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,12 @@ func (n *headlessManager) DrawRecursiveGrid(
159159
func (n *headlessManager) UpdateGridMatches(prefix string) {}
160160

161161
// ShowSubgrid is a no-op implementation.
162-
func (n *headlessManager) ShowSubgrid(cell *domainGrid.Cell, style rendergrid.Style) {}
162+
func (n *headlessManager) ShowSubgrid(
163+
cell *domainGrid.Cell,
164+
style rendergrid.Style,
165+
virtualPointer renderrecursivegrid.VirtualPointerState,
166+
) {
167+
}
163168

164169
// SetHideUnmatched is a no-op implementation.
165170
func (n *headlessManager) SetHideUnmatched(hide bool) {}

0 commit comments

Comments
 (0)