detached e35468a

merge: draw layer surfaces

commit
e35468a727a0b6d2c2865b2c119b007866ff0e86
author
brice · · 2026-09-23 16:08 UTC
merge of
ffa1efa + 446b47e · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +94 -0
M internal/adapters/headless/output_test.go +34 -0
@@ -141,3 +141,37 @@ func TestWritePNGAtomic(t *testing.T) {
141141 t.Fatalf("unexpected files: %v", entries)
142142 }
143143 }
144+
145+func TestRunPassesLayerContent(t *testing.T) {
146+ scenes := make(chan ports.Scene, 1)
147+ contents := make(chan ports.SurfaceContent, 1)
148+ layer := ports.SceneLayer{ID: 42, Layer: ports.LayerTop, Rect: ports.Rect{W: 2, H: 2}}
149+ scenes <- ports.Scene{Layers: []ports.SceneLayer{layer}}
150+ contents <- ports.SurfaceContent{ID: 42, Width: 1, Height: 1, Stride: 4, Pixels: []byte{0, 0, 255, 255}}
151+ r := new(fakeRenderer)
152+ ctx, cancel := context.WithCancel(context.Background())
153+ done := make(chan error, 1)
154+ go func() {
155+ done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (Renderer, error) { return r, nil }}, scenes, contents)
156+ }()
157+ deadline := time.After(3 * time.Second)
158+ for {
159+ r.mu.Lock()
160+ passed := len(r.contents) > 0 && len(r.frames[0].Layers) == 1 && r.contents[0][42].Pixels != nil
161+ r.mu.Unlock()
162+ if passed {
163+ break
164+ }
165+ select {
166+ case <-deadline:
167+ cancel()
168+ <-done
169+ t.Fatal("layer content not passed to renderer")
170+ case <-time.After(10 * time.Millisecond):
171+ }
172+ }
173+ cancel()
174+ if err := <-done; err != nil {
175+ t.Fatal(err)
176+ }
177+}
@@ -141,3 +141,37 @@ func TestWritePNGAtomic(t *testing.T) {
141 t.Fatalf("unexpected files: %v", entries)141 t.Fatalf("unexpected files: %v", entries)
142 }142 }
143 }143 }
144+
145+func TestRunPassesLayerContent(t *testing.T) {
146+ scenes := make(chan ports.Scene, 1)
147+ contents := make(chan ports.SurfaceContent, 1)
148+ layer := ports.SceneLayer{ID: 42, Layer: ports.LayerTop, Rect: ports.Rect{W: 2, H: 2}}
149+ scenes <- ports.Scene{Layers: []ports.SceneLayer{layer}}
150+ contents <- ports.SurfaceContent{ID: 42, Width: 1, Height: 1, Stride: 4, Pixels: []byte{0, 0, 255, 255}}
151+ r := new(fakeRenderer)
152+ ctx, cancel := context.WithCancel(context.Background())
153+ done := make(chan error, 1)
154+ go func() {
155+ done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (Renderer, error) { return r, nil }}, scenes, contents)
156+ }()
157+ deadline := time.After(3 * time.Second)
158+ for {
159+ r.mu.Lock()
160+ passed := len(r.contents) > 0 && len(r.frames[0].Layers) == 1 && r.contents[0][42].Pixels != nil
161+ r.mu.Unlock()
162+ if passed {
163+ break
164+ }
165+ select {
166+ case <-deadline:
167+ cancel()
168+ <-done
169+ t.Fatal("layer content not passed to renderer")
170+ case <-time.After(10 * time.Millisecond):
171+ }
172+ }
173+ cancel()
174+ if err := <-done; err != nil {
175+ t.Fatal(err)
176+ }
177+}
M internal/adapters/vulkan/renderer.go +22 -0
@@ -336,6 +336,27 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
336336 uploads = append(uploads, u)
337337 used += n
338338 }
339+ fullscreen := false
340+ for _, w := range s.Windows {
341+ fullscreen = fullscreen || (!w.Hidden && w.Fullscreen)
342+ }
343+ addLayers := func(afterWindows bool) {
344+ for _, layer := range s.Layers {
345+ if (layer.Layer >= ports.LayerTop) != afterWindows ||
346+ (fullscreen && (layer.Layer == ports.LayerBottom || layer.Layer == ports.LayerTop)) ||
347+ layer.Rect.W <= 0 || layer.Rect.H <= 0 {
348+ continue
349+ }
350+ content := contents[layer.ID]
351+ width, height := min(content.Width, layer.Rect.W), min(content.Height, layer.Rect.H)
352+ if content.Pixels == nil || width <= 0 || height <= 0 || content.Stride < width*4 || len(content.Pixels) < (height-1)*content.Stride+width*4 {
353+ continue
354+ }
355+ x, y := layer.Rect.X, layer.Rect.Y
356+ add(image.Rect(x, y, x+width, y+height), [3]uint8{}, &content, image.Pt(x, y))
357+ }
358+ }
359+ addLayers(false)
339360 for _, w := range s.Windows {
340361 if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 {
341362 continue
@@ -358,6 +379,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
358379 }
359380 }
360381 }
382+ addLayers(true)
361383 if err := r.ensureStaging(used); err != nil {
362384 return err
363385 }
@@ -336,6 +336,27 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
336 uploads = append(uploads, u)336 uploads = append(uploads, u)
337 used += n337 used += n
338 }338 }
339+ fullscreen := false
340+ for _, w := range s.Windows {
341+ fullscreen = fullscreen || (!w.Hidden && w.Fullscreen)
342+ }
343+ addLayers := func(afterWindows bool) {
344+ for _, layer := range s.Layers {
345+ if (layer.Layer >= ports.LayerTop) != afterWindows ||
346+ (fullscreen && (layer.Layer == ports.LayerBottom || layer.Layer == ports.LayerTop)) ||
347+ layer.Rect.W <= 0 || layer.Rect.H <= 0 {
348+ continue
349+ }
350+ content := contents[layer.ID]
351+ width, height := min(content.Width, layer.Rect.W), min(content.Height, layer.Rect.H)
352+ if content.Pixels == nil || width <= 0 || height <= 0 || content.Stride < width*4 || len(content.Pixels) < (height-1)*content.Stride+width*4 {
353+ continue
354+ }
355+ x, y := layer.Rect.X, layer.Rect.Y
356+ add(image.Rect(x, y, x+width, y+height), [3]uint8{}, &content, image.Pt(x, y))
357+ }
358+ }
359+ addLayers(false)
339 for _, w := range s.Windows {360 for _, w := range s.Windows {
340 if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 {361 if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 {
341 continue362 continue
@@ -358,6 +379,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
358 }379 }
359 }380 }
360 }381 }
382+ addLayers(true)
361 if err := r.ensureStaging(used); err != nil {383 if err := r.ensureStaging(used); err != nil {
362 return err384 return err
363 }385 }
M internal/adapters/vulkan/renderer_test.go +38 -0
@@ -145,3 +145,41 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) {
145145 }
146146 }
147147 }
148+
149+func TestRendererLayers(t *testing.T) {
150+ r, err := New(64, 48)
151+ if err != nil {
152+ t.Skipf("Vulkan unavailable: %v", err)
153+ }
154+ defer r.Close()
155+ red := make([]byte, 64*8*4)
156+ for i := 0; i < len(red); i += 4 {
157+ copy(red[i:i+4], []byte{0, 0, 255, 0})
158+ }
159+ contents := map[ports.WindowID]ports.SurfaceContent{2: {ID: 2, Width: 64, Height: 8, Stride: 64 * 4, Opaque: true, Pixels: red}}
160+ scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}, Layers: []ports.SceneLayer{{ID: 2, Layer: ports.LayerTop, Rect: ports.Rect{W: 64, H: 8}}}}
161+ wc := windowColor(1)
162+ window := color.RGBA{wc[0], wc[1], wc[2], 255}
163+ check := func(want color.RGBA) {
164+ t.Helper()
165+ if err := r.Render(scene, contents); err != nil {
166+ t.Fatal(err)
167+ }
168+ if got := r.Pixels().RGBAAt(5, 2); got != want {
169+ t.Errorf("pixel = %v, want %v", got, want)
170+ }
171+ }
172+ check(color.RGBA{255, 0, 0, 255})
173+ scene.Layers[0].Layer = ports.LayerBottom
174+ check(window)
175+ scene.Windows[0].Fullscreen = true
176+ scene.Layers[0].Layer = ports.LayerTop
177+ check(window)
178+ scene.Layers[0].Layer = ports.LayerOverlay
179+ check(color.RGBA{255, 0, 0, 255})
180+ scene.Windows[0].Hidden = true
181+ scene.Layers[0].Layer = ports.LayerBottom
182+ check(color.RGBA{255, 0, 0, 255})
183+ delete(contents, 2)
184+ check(color.RGBA{16, 32, 48, 255})
185+}
@@ -145,3 +145,41 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) {
145 }145 }
146 }146 }
147 }147 }
148+
149+func TestRendererLayers(t *testing.T) {
150+ r, err := New(64, 48)
151+ if err != nil {
152+ t.Skipf("Vulkan unavailable: %v", err)
153+ }
154+ defer r.Close()
155+ red := make([]byte, 64*8*4)
156+ for i := 0; i < len(red); i += 4 {
157+ copy(red[i:i+4], []byte{0, 0, 255, 0})
158+ }
159+ contents := map[ports.WindowID]ports.SurfaceContent{2: {ID: 2, Width: 64, Height: 8, Stride: 64 * 4, Opaque: true, Pixels: red}}
160+ scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}, Layers: []ports.SceneLayer{{ID: 2, Layer: ports.LayerTop, Rect: ports.Rect{W: 64, H: 8}}}}
161+ wc := windowColor(1)
162+ window := color.RGBA{wc[0], wc[1], wc[2], 255}
163+ check := func(want color.RGBA) {
164+ t.Helper()
165+ if err := r.Render(scene, contents); err != nil {
166+ t.Fatal(err)
167+ }
168+ if got := r.Pixels().RGBAAt(5, 2); got != want {
169+ t.Errorf("pixel = %v, want %v", got, want)
170+ }
171+ }
172+ check(color.RGBA{255, 0, 0, 255})
173+ scene.Layers[0].Layer = ports.LayerBottom
174+ check(window)
175+ scene.Windows[0].Fullscreen = true
176+ scene.Layers[0].Layer = ports.LayerTop
177+ check(window)
178+ scene.Layers[0].Layer = ports.LayerOverlay
179+ check(color.RGBA{255, 0, 0, 255})
180+ scene.Windows[0].Hidden = true
181+ scene.Layers[0].Layer = ports.LayerBottom
182+ check(color.RGBA{255, 0, 0, 255})
183+ delete(contents, 2)
184+ check(color.RGBA{16, 32, 48, 255})
185+}

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 23

Tags 10

↑↓ moveenter openesc close