detached 9136292

perf(render): redraw an output only for content it shows

commit
9136292751b7d3429b050258feff763290f4d1cf
author
brice · · 2026-09-25 09:18 UTC
parent
086975e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +56 -7
M internal/adapters/drm/output.go +2 -1
@@ -183,7 +183,8 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
183183 } else {
184184 surfaces[c.ID] = c
185185 }
186- dirty = true
186+ // Windows on other outputs or workspaces do not need a frame.
187+ dirty = dirty || scene.Shows(c.ID)
187188 case <-stats.C:
188189 ev := o.log.Info().Int("frames", frame).Int("flips", o.flips)
189190 if o.cursor != nil {
@@ -183,7 +183,8 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
183 } else {183 } else {
184 surfaces[c.ID] = c184 surfaces[c.ID] = c
185 }185 }
186- dirty = true186+ // Windows on other outputs or workspaces do not need a frame.
187+ dirty = dirty || scene.Shows(c.ID)
187 case <-stats.C:188 case <-stats.C:
188 ev := o.log.Info().Int("frames", frame).Int("flips", o.flips)189 ev := o.log.Info().Int("frames", frame).Int("flips", o.flips)
189 if o.cursor != nil {190 if o.cursor != nil {
M internal/adapters/headless/output.go +6 -4
@@ -32,10 +32,11 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
3232 defer r.Close()
3333 surfaces := make(map[ports.WindowID]ports.SurfaceContent)
3434 var scene ports.Scene
35- haveScene := false
35+ haveScene, dirty := false, false
3636 frame := 0
3737 cursorScale := -1.0 // not loaded yet
3838 update := func(c ports.SurfaceContent) {
39+ dirty = dirty || scene.Shows(c.ID)
3940 if c.Empty() {
4041 delete(surfaces, c.ID)
4142 } else {
@@ -54,7 +55,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
5455 scenes = nil
5556 continue
5657 }
57- scene, haveScene = s, true
58+ scene, haveScene, dirty = s, true, true
5859 case c, ok := <-contents:
5960 if !ok {
6061 contents = nil
@@ -70,7 +71,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
7071 if !ok {
7172 scenes = nil
7273 } else {
73- scene, haveScene = s, true
74+ scene, haveScene, dirty = s, true, true
7475 }
7576 case c, ok := <-contents:
7677 if !ok {
@@ -82,9 +83,10 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
8283 break drain
8384 }
8485 }
85- if !haveScene {
86+ if !haveScene || !dirty {
8687 continue
8788 }
89+ dirty = false
8890 if opts.Cursor != nil && opts.LoadCursor != nil && scene.Scale != cursorScale {
8991 cursorScale = scene.Scale
9092 if img, err := opts.LoadCursor(scene.Scale, 256); err == nil {
@@ -32,10 +32,11 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
32 defer r.Close()32 defer r.Close()
33 surfaces := make(map[ports.WindowID]ports.SurfaceContent)33 surfaces := make(map[ports.WindowID]ports.SurfaceContent)
34 var scene ports.Scene34 var scene ports.Scene
35- haveScene := false35+ haveScene, dirty := false, false
36 frame := 036 frame := 0
37 cursorScale := -1.0 // not loaded yet37 cursorScale := -1.0 // not loaded yet
38 update := func(c ports.SurfaceContent) {38 update := func(c ports.SurfaceContent) {
39+ dirty = dirty || scene.Shows(c.ID)
39 if c.Empty() {40 if c.Empty() {
40 delete(surfaces, c.ID)41 delete(surfaces, c.ID)
41 } else {42 } else {
@@ -54,7 +55,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
54 scenes = nil55 scenes = nil
55 continue56 continue
56 }57 }
57- scene, haveScene = s, true58+ scene, haveScene, dirty = s, true, true
58 case c, ok := <-contents:59 case c, ok := <-contents:
59 if !ok {60 if !ok {
60 contents = nil61 contents = nil
@@ -70,7 +71,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
70 if !ok {71 if !ok {
71 scenes = nil72 scenes = nil
72 } else {73 } else {
73- scene, haveScene = s, true74+ scene, haveScene, dirty = s, true, true
74 }75 }
75 case c, ok := <-contents:76 case c, ok := <-contents:
76 if !ok {77 if !ok {
@@ -82,9 +83,10 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
82 break drain83 break drain
83 }84 }
84 }85 }
85- if !haveScene {86+ if !haveScene || !dirty {
86 continue87 continue
87 }88 }
89+ dirty = false
88 if opts.Cursor != nil && opts.LoadCursor != nil && scene.Scale != cursorScale {90 if opts.Cursor != nil && opts.LoadCursor != nil && scene.Scale != cursorScale {
89 cursorScale = scene.Scale91 cursorScale = scene.Scale
90 if img, err := opts.LoadCursor(scene.Scale, 256); err == nil {92 if img, err := opts.LoadCursor(scene.Scale, 256); err == nil {
M internal/adapters/headless/output_test.go +32 -2
@@ -69,8 +69,9 @@ func TestRun(t *testing.T) {
6969 contents := make(chan ports.SurfaceContent, 8)
7070 r, f := recordingRenderer(t, nil)
7171 contents <- ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{Pool: 1}}
72- scenes <- ports.Scene{Seq: 1}
73- scenes <- ports.Scene{Seq: 2}
72+ shown := []ports.SceneWindow{{ID: 1}, {ID: 2}}
73+ scenes <- ports.Scene{Seq: 1, Windows: shown}
74+ scenes <- ports.Scene{Seq: 2, Windows: shown}
7475 contents <- ports.SurfaceContent{ID: 2, SHM: &ports.SHMBuffer{Pool: 2}}
7576 ctx, cancel := context.WithCancel(context.Background())
7677 done := make(chan error, 1)
@@ -105,6 +106,35 @@ func TestRun(t *testing.T) {
105106 }
106107 }
107108
109+// Content of a window the output does not show draws no frame.
110+func TestRunSkipsContentNotShown(t *testing.T) {
111+ scenes := make(chan ports.Scene, 1)
112+ contents := make(chan ports.SurfaceContent, 2)
113+ scenes <- ports.Scene{Seq: 1, Windows: []ports.SceneWindow{{ID: 1}, {ID: 3, Hidden: true}}}
114+ r, f := recordingRenderer(t, nil)
115+ ctx, cancel := context.WithCancel(context.Background())
116+ done := make(chan error, 1)
117+ go func() {
118+ done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents)
119+ }()
120+ waitFrames(t, f, 1)
121+ contents <- ports.SurfaceContent{ID: 2, SHM: &ports.SHMBuffer{Pool: 2}}
122+ contents <- ports.SurfaceContent{ID: 3, SHM: &ports.SHMBuffer{Pool: 3}}
123+ time.Sleep(50 * time.Millisecond)
124+ if s, _ := f.snapshot(); len(s) != 1 {
125+ t.Fatalf("%d frames for content not shown", len(s))
126+ }
127+ contents <- ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{Pool: 1}}
128+ waitFrames(t, f, 2)
129+ if _, c := f.snapshot(); c[1][2].SHM == nil {
130+ t.Error("content not shown was dropped")
131+ }
132+ cancel()
133+ if err := <-done; err != nil {
134+ t.Fatal(err)
135+ }
136+}
137+
108138 func TestErrors(t *testing.T) {
109139 expected := errors.New("failure")
110140 scenes := make(chan ports.Scene, 1)
@@ -69,8 +69,9 @@ func TestRun(t *testing.T) {
69 contents := make(chan ports.SurfaceContent, 8)69 contents := make(chan ports.SurfaceContent, 8)
70 r, f := recordingRenderer(t, nil)70 r, f := recordingRenderer(t, nil)
71 contents <- ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{Pool: 1}}71 contents <- ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{Pool: 1}}
72- scenes <- ports.Scene{Seq: 1}72+ shown := []ports.SceneWindow{{ID: 1}, {ID: 2}}
73- scenes <- ports.Scene{Seq: 2}73+ scenes <- ports.Scene{Seq: 1, Windows: shown}
74+ scenes <- ports.Scene{Seq: 2, Windows: shown}
74 contents <- ports.SurfaceContent{ID: 2, SHM: &ports.SHMBuffer{Pool: 2}}75 contents <- ports.SurfaceContent{ID: 2, SHM: &ports.SHMBuffer{Pool: 2}}
75 ctx, cancel := context.WithCancel(context.Background())76 ctx, cancel := context.WithCancel(context.Background())
76 done := make(chan error, 1)77 done := make(chan error, 1)
@@ -105,6 +106,35 @@ func TestRun(t *testing.T) {
105 }106 }
106 }107 }
107 108
109+// Content of a window the output does not show draws no frame.
110+func TestRunSkipsContentNotShown(t *testing.T) {
111+ scenes := make(chan ports.Scene, 1)
112+ contents := make(chan ports.SurfaceContent, 2)
113+ scenes <- ports.Scene{Seq: 1, Windows: []ports.SceneWindow{{ID: 1}, {ID: 3, Hidden: true}}}
114+ r, f := recordingRenderer(t, nil)
115+ ctx, cancel := context.WithCancel(context.Background())
116+ done := make(chan error, 1)
117+ go func() {
118+ done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents)
119+ }()
120+ waitFrames(t, f, 1)
121+ contents <- ports.SurfaceContent{ID: 2, SHM: &ports.SHMBuffer{Pool: 2}}
122+ contents <- ports.SurfaceContent{ID: 3, SHM: &ports.SHMBuffer{Pool: 3}}
123+ time.Sleep(50 * time.Millisecond)
124+ if s, _ := f.snapshot(); len(s) != 1 {
125+ t.Fatalf("%d frames for content not shown", len(s))
126+ }
127+ contents <- ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{Pool: 1}}
128+ waitFrames(t, f, 2)
129+ if _, c := f.snapshot(); c[1][2].SHM == nil {
130+ t.Error("content not shown was dropped")
131+ }
132+ cancel()
133+ if err := <-done; err != nil {
134+ t.Fatal(err)
135+ }
136+}
137+
108 func TestErrors(t *testing.T) {138 func TestErrors(t *testing.T) {
109 expected := errors.New("failure")139 expected := errors.New("failure")
110 scenes := make(chan ports.Scene, 1)140 scenes := make(chan ports.Scene, 1)
M internal/ports/messages.go +16 -0
@@ -327,6 +327,22 @@ type Scene struct {
327327 Layers []SceneLayer
328328 }
329329
330+// Shows reports whether the scene draws the surface of id: only its
331+// content changes need a new frame.
332+func (s Scene) Shows(id WindowID) bool {
333+ for _, w := range s.Windows {
334+ if w.ID == id && !w.Hidden {
335+ return true
336+ }
337+ }
338+ for _, l := range s.Layers {
339+ if l.ID == id {
340+ return true
341+ }
342+ }
343+ return false
344+}
345+
330346 // Border carries the window border style; colors are #rrggbb, "" skips drawing.
331347 type Border struct {
332348 Width int
@@ -327,6 +327,22 @@ type Scene struct {
327 Layers []SceneLayer327 Layers []SceneLayer
328 }328 }
329 329
330+// Shows reports whether the scene draws the surface of id: only its
331+// content changes need a new frame.
332+func (s Scene) Shows(id WindowID) bool {
333+ for _, w := range s.Windows {
334+ if w.ID == id && !w.Hidden {
335+ return true
336+ }
337+ }
338+ for _, l := range s.Layers {
339+ if l.ID == id {
340+ return true
341+ }
342+ }
343+ return false
344+}
345+
330 // Border carries the window border style; colors are #rrggbb, "" skips drawing.346 // Border carries the window border style; colors are #rrggbb, "" skips drawing.
331 type Border struct {347 type Border struct {
332 Width int348 Width int

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 26

Tags 10

↑↓ moveenter openesc close