detached 4ef9a1d

fix(vulkan): draw background instead of colored placeholder before first buffer (#50)

commit
4ef9a1d75f6579ebee1e00c2c9d7f36fbac275a7
author
bnema · · 2026-09-28 18:11 UTC
committer
GitHub · · 2026-09-28 18:11 UTC
parent
3d61f9d
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
2 files · +23 -45
M internal/adapters/vulkan/renderer_test.go +20 -17
@@ -46,9 +46,13 @@ func TestRendererRender(t *testing.T) {
4646 }
4747 defer r.Close()
4848 bg := color.RGBA{16, 32, 48, 255}
49+ wc, c2, fc := color.RGBA{200, 10, 10, 255}, color.RGBA{10, 200, 10, 255}, color.RGBA{10, 10, 200, 255}
50+ contents := map[ports.WindowID]ports.SurfaceContent{
51+ 1: solidContent(t, 64, 48, wc), 2: solidContent(t, 64, 48, c2), 3: solidContent(t, 64, 48, fc),
52+ }
4953 check := func(s ports.Scene, expected map[image.Point]color.RGBA) {
5054 t.Helper()
51- if err := render(r, s, nil); err != nil {
55+ if err := render(r, s, contents); err != nil {
5256 t.Fatal(err)
5357 }
5458 for p, want := range expected {
@@ -60,8 +64,6 @@ func TestRendererRender(t *testing.T) {
6064 // The client sits inside its inset sides; separators are drawn over
6165 // the windows in order, with the border colors.
6266 win := ports.SceneWindow{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}, Inset: ports.SideAll}
63- c := windowColor(1)
64- wc := color.RGBA{c[0], c[1], c[2], 255}
6567 gray, lit := color.RGBA{0x31, 0x32, 0x44, 255}, color.RGBA{0xb4, 0xbe, 0xfe, 255}
6668 s := ports.Scene{Background: "#102030", Border: ports.Border{Width: 2, Active: "#b4befe", Inactive: "#313244"}, Windows: []ports.SceneWindow{win}}
6769 check(s, map[image.Point]color.RGBA{{0, 0}: bg, {9, 9}: bg, {10, 10}: wc, {16, 16}: wc, {21, 21}: wc, {22, 22}: bg, {24, 24}: bg})
@@ -78,16 +80,14 @@ func TestRendererRender(t *testing.T) {
7880 {Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 2}, Active: true},
7981 {Rect: ports.Rect{X: 12, Y: 4, W: 8, H: 1}, Active: true, Window: 3},
8082 }
81- fc := windowColor(3)
82- check(s, map[image.Point]color.RGBA{{9, 9}: lit, {14, 9}: {fc[0], fc[1], fc[2], 255}, {14, 4}: lit})
83+ check(s, map[image.Point]color.RGBA{{9, 9}: lit, {14, 9}: fc, {14, 4}: lit})
8384 s.Windows = []ports.SceneWindow{win}
8485 s.Separators, s.Border.Inactive = nil, "#313244"
8586 s.Windows[0].Fullscreen = true
8687 check(s, map[image.Point]color.RGBA{{9, 9}: wc})
8788 s.Windows[0].Fullscreen = false
8889 s.Windows[0] = ports.SceneWindow{ID: 2, Rect: ports.Rect{X: -10, Y: 0, W: 20, H: 10}}
89- c = windowColor(2)
90- check(s, map[image.Point]color.RGBA{{5, 5}: {c[0], c[1], c[2], 255}, {15, 5}: bg})
90+ check(s, map[image.Point]color.RGBA{{5, 5}: c2, {15, 5}: bg})
9191 s.Windows[0].Hidden = true
9292 check(s, map[image.Point]color.RGBA{{5, 5}: bg})
9393 }
@@ -130,9 +130,9 @@ func TestRendererContents(t *testing.T) {
130130 copy(padded[8:12], []byte{7, 11, 23, 255})
131131 render(shmContent(t, 4, 4, 20, padded))
132132 check(0, 0, color.RGBA{23, 11, 7, 255})
133+ // A window without a buffer yet shows the background, not a color.
133134 render(nil)
134- c := windowColor(1)
135- check(0, 0, color.RGBA{c[0], c[1], c[2], 255})
135+ check(0, 0, bg)
136136 }
137137
138138 func TestRendererUploadOrderAndOpaque(t *testing.T) {
@@ -145,11 +145,16 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) {
145145 {ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}},
146146 {ID: 2, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}},
147147 }}
148- if err := render(r, scene, nil); err != nil {
148+ top := color.RGBA{10, 200, 10, 255}
149+ upper := map[ports.WindowID]ports.SurfaceContent{1: solidContent(t, 16, 16, color.RGBA{200, 10, 10, 255}), 2: solidContent(t, 16, 16, top)}
150+ for id, c := range upper {
151+ c.Version = 1 // the opaque buffer below is another commit
152+ upper[id] = c
153+ }
154+ if err := render(r, scene, upper); err != nil {
149155 t.Fatal(err)
150156 }
151- c := windowColor(2)
152- if got, want := r.Pixels().RGBAAt(10, 10), (color.RGBA{c[0], c[1], c[2], 255}); got != want {
157+ if got, want := r.Pixels().RGBAAt(10, 10), top; got != want {
153158 t.Errorf("overlap = %v, want %v", got, want)
154159 }
155160 scene.Border = ports.Border{Width: 4, Active: "#ffffff"}
@@ -189,8 +194,7 @@ func TestRendererLayers(t *testing.T) {
189194 layer.ID, layer.Opaque = 2, true
190195 contents := map[ports.WindowID]ports.SurfaceContent{2: *layer}
191196 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}}}}
192- wc := windowColor(1)
193- window := color.RGBA{wc[0], wc[1], wc[2], 255}
197+ window := color.RGBA{16, 32, 48, 255} // no buffer yet: background
194198 check := func(want color.RGBA) {
195199 t.Helper()
196200 if err := render(r, scene, contents); err != nil {
@@ -368,11 +372,10 @@ func TestRendererTallFill(t *testing.T) {
368372 }
369373 defer r.Close()
370374 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 1, Y: 0, W: 6, H: h}}}}
371- if err := render(r, scene, nil); err != nil {
375+ wc := color.RGBA{200, 10, 10, 255}
376+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: solidContent(t, 6, h, wc)}); err != nil {
372377 t.Fatal(err)
373378 }
374- c := windowColor(1)
375- wc := color.RGBA{c[0], c[1], c[2], 255}
376379 bg := color.RGBA{16, 32, 48, 255}
377380 px := r.Pixels()
378381 for y := 0; y < h; y++ {
@@ -46,9 +46,13 @@ func TestRendererRender(t *testing.T) {
46 }46 }
47 defer r.Close()47 defer r.Close()
48 bg := color.RGBA{16, 32, 48, 255}48 bg := color.RGBA{16, 32, 48, 255}
49+ wc, c2, fc := color.RGBA{200, 10, 10, 255}, color.RGBA{10, 200, 10, 255}, color.RGBA{10, 10, 200, 255}
50+ contents := map[ports.WindowID]ports.SurfaceContent{
51+ 1: solidContent(t, 64, 48, wc), 2: solidContent(t, 64, 48, c2), 3: solidContent(t, 64, 48, fc),
52+ }
49 check := func(s ports.Scene, expected map[image.Point]color.RGBA) {53 check := func(s ports.Scene, expected map[image.Point]color.RGBA) {
50 t.Helper()54 t.Helper()
51- if err := render(r, s, nil); err != nil {55+ if err := render(r, s, contents); err != nil {
52 t.Fatal(err)56 t.Fatal(err)
53 }57 }
54 for p, want := range expected {58 for p, want := range expected {
@@ -60,8 +64,6 @@ func TestRendererRender(t *testing.T) {
60 // The client sits inside its inset sides; separators are drawn over64 // The client sits inside its inset sides; separators are drawn over
61 // the windows in order, with the border colors.65 // the windows in order, with the border colors.
62 win := ports.SceneWindow{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}, Inset: ports.SideAll}66 win := ports.SceneWindow{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}, Inset: ports.SideAll}
63- c := windowColor(1)
64- wc := color.RGBA{c[0], c[1], c[2], 255}
65 gray, lit := color.RGBA{0x31, 0x32, 0x44, 255}, color.RGBA{0xb4, 0xbe, 0xfe, 255}67 gray, lit := color.RGBA{0x31, 0x32, 0x44, 255}, color.RGBA{0xb4, 0xbe, 0xfe, 255}
66 s := ports.Scene{Background: "#102030", Border: ports.Border{Width: 2, Active: "#b4befe", Inactive: "#313244"}, Windows: []ports.SceneWindow{win}}68 s := ports.Scene{Background: "#102030", Border: ports.Border{Width: 2, Active: "#b4befe", Inactive: "#313244"}, Windows: []ports.SceneWindow{win}}
67 check(s, map[image.Point]color.RGBA{{0, 0}: bg, {9, 9}: bg, {10, 10}: wc, {16, 16}: wc, {21, 21}: wc, {22, 22}: bg, {24, 24}: bg})69 check(s, map[image.Point]color.RGBA{{0, 0}: bg, {9, 9}: bg, {10, 10}: wc, {16, 16}: wc, {21, 21}: wc, {22, 22}: bg, {24, 24}: bg})
@@ -78,16 +80,14 @@ func TestRendererRender(t *testing.T) {
78 {Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 2}, Active: true},80 {Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 2}, Active: true},
79 {Rect: ports.Rect{X: 12, Y: 4, W: 8, H: 1}, Active: true, Window: 3},81 {Rect: ports.Rect{X: 12, Y: 4, W: 8, H: 1}, Active: true, Window: 3},
80 }82 }
81- fc := windowColor(3)83+ check(s, map[image.Point]color.RGBA{{9, 9}: lit, {14, 9}: fc, {14, 4}: lit})
82- check(s, map[image.Point]color.RGBA{{9, 9}: lit, {14, 9}: {fc[0], fc[1], fc[2], 255}, {14, 4}: lit})
83 s.Windows = []ports.SceneWindow{win}84 s.Windows = []ports.SceneWindow{win}
84 s.Separators, s.Border.Inactive = nil, "#313244"85 s.Separators, s.Border.Inactive = nil, "#313244"
85 s.Windows[0].Fullscreen = true86 s.Windows[0].Fullscreen = true
86 check(s, map[image.Point]color.RGBA{{9, 9}: wc})87 check(s, map[image.Point]color.RGBA{{9, 9}: wc})
87 s.Windows[0].Fullscreen = false88 s.Windows[0].Fullscreen = false
88 s.Windows[0] = ports.SceneWindow{ID: 2, Rect: ports.Rect{X: -10, Y: 0, W: 20, H: 10}}89 s.Windows[0] = ports.SceneWindow{ID: 2, Rect: ports.Rect{X: -10, Y: 0, W: 20, H: 10}}
89- c = windowColor(2)90+ check(s, map[image.Point]color.RGBA{{5, 5}: c2, {15, 5}: bg})
90- check(s, map[image.Point]color.RGBA{{5, 5}: {c[0], c[1], c[2], 255}, {15, 5}: bg})
91 s.Windows[0].Hidden = true91 s.Windows[0].Hidden = true
92 check(s, map[image.Point]color.RGBA{{5, 5}: bg})92 check(s, map[image.Point]color.RGBA{{5, 5}: bg})
93 }93 }
@@ -130,9 +130,9 @@ func TestRendererContents(t *testing.T) {
130 copy(padded[8:12], []byte{7, 11, 23, 255})130 copy(padded[8:12], []byte{7, 11, 23, 255})
131 render(shmContent(t, 4, 4, 20, padded))131 render(shmContent(t, 4, 4, 20, padded))
132 check(0, 0, color.RGBA{23, 11, 7, 255})132 check(0, 0, color.RGBA{23, 11, 7, 255})
133+ // A window without a buffer yet shows the background, not a color.
133 render(nil)134 render(nil)
134- c := windowColor(1)135+ check(0, 0, bg)
135- check(0, 0, color.RGBA{c[0], c[1], c[2], 255})
136 }136 }
137 137
138 func TestRendererUploadOrderAndOpaque(t *testing.T) {138 func TestRendererUploadOrderAndOpaque(t *testing.T) {
@@ -145,11 +145,16 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) {
145 {ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}},145 {ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}},
146 {ID: 2, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}},146 {ID: 2, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}},
147 }}147 }}
148- if err := render(r, scene, nil); err != nil {148+ top := color.RGBA{10, 200, 10, 255}
149+ upper := map[ports.WindowID]ports.SurfaceContent{1: solidContent(t, 16, 16, color.RGBA{200, 10, 10, 255}), 2: solidContent(t, 16, 16, top)}
150+ for id, c := range upper {
151+ c.Version = 1 // the opaque buffer below is another commit
152+ upper[id] = c
153+ }
154+ if err := render(r, scene, upper); err != nil {
149 t.Fatal(err)155 t.Fatal(err)
150 }156 }
151- c := windowColor(2)157+ if got, want := r.Pixels().RGBAAt(10, 10), top; got != want {
152- if got, want := r.Pixels().RGBAAt(10, 10), (color.RGBA{c[0], c[1], c[2], 255}); got != want {
153 t.Errorf("overlap = %v, want %v", got, want)158 t.Errorf("overlap = %v, want %v", got, want)
154 }159 }
155 scene.Border = ports.Border{Width: 4, Active: "#ffffff"}160 scene.Border = ports.Border{Width: 4, Active: "#ffffff"}
@@ -189,8 +194,7 @@ func TestRendererLayers(t *testing.T) {
189 layer.ID, layer.Opaque = 2, true194 layer.ID, layer.Opaque = 2, true
190 contents := map[ports.WindowID]ports.SurfaceContent{2: *layer}195 contents := map[ports.WindowID]ports.SurfaceContent{2: *layer}
191 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}}}}196 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}}}}
192- wc := windowColor(1)197+ window := color.RGBA{16, 32, 48, 255} // no buffer yet: background
193- window := color.RGBA{wc[0], wc[1], wc[2], 255}
194 check := func(want color.RGBA) {198 check := func(want color.RGBA) {
195 t.Helper()199 t.Helper()
196 if err := render(r, scene, contents); err != nil {200 if err := render(r, scene, contents); err != nil {
@@ -368,11 +372,10 @@ func TestRendererTallFill(t *testing.T) {
368 }372 }
369 defer r.Close()373 defer r.Close()
370 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 1, Y: 0, W: 6, H: h}}}}374 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 1, Y: 0, W: 6, H: h}}}}
371- if err := render(r, scene, nil); err != nil {375+ wc := color.RGBA{200, 10, 10, 255}
376+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: solidContent(t, 6, h, wc)}); err != nil {
372 t.Fatal(err)377 t.Fatal(err)
373 }378 }
374- c := windowColor(1)
375- wc := color.RGBA{c[0], c[1], c[2], 255}
376 bg := color.RGBA{16, 32, 48, 255}379 bg := color.RGBA{16, 32, 48, 255}
377 px := r.Pixels()380 px := r.Pixels()
378 for y := 0; y < h; y++ {381 for y := 0; y < h; y++ {
M internal/adapters/vulkan/scene.go +3 -28
@@ -97,10 +97,9 @@ func (w *sceneWalk) windows() {
9797 body := w.physRect(c.X, c.Y, c.W, c.H)
9898 content := w.contents[win.ID]
9999 w.dmg.window(win.ID, content, w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H))
100- if content.Empty() {
101- w.fill(body, windowColor(win.ID))
102- } else {
103- w.fill(body, parseColor(w.s.Background))
100+ // Until its first buffer, a window shows the background: no flash.
101+ w.fill(body, parseColor(w.s.Background))
102+ if !content.Empty() {
104103 w.place(win.ID, &content, c.X, c.Y, c.W, c.H)
105104 }
106105 if win.Floating {
@@ -335,27 +334,3 @@ func parseColor(s string) [3]uint8 {
335334 }
336335 return c
337336 }
338-
339-// windowColor is the placeholder color of a window with no content yet.
340-func windowColor(id ports.WindowID) [3]uint8 {
341- h := math.Mod(float64(id)*0.618034, 1) * 6
342- sector := int(h)
343- f := h - float64(sector)
344- p, q, t := 0.4, 0.8*(1-0.5*f), 0.8*(1-0.5*(1-f))
345- var a, b, c float64
346- switch sector {
347- case 0:
348- a, b, c = 0.8, t, p
349- case 1:
350- a, b, c = q, 0.8, p
351- case 2:
352- a, b, c = p, 0.8, t
353- case 3:
354- a, b, c = p, q, 0.8
355- case 4:
356- a, b, c = t, p, 0.8
357- default:
358- a, b, c = 0.8, p, q
359- }
360- return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)}
361-}
@@ -97,10 +97,9 @@ func (w *sceneWalk) windows() {
97 body := w.physRect(c.X, c.Y, c.W, c.H)97 body := w.physRect(c.X, c.Y, c.W, c.H)
98 content := w.contents[win.ID]98 content := w.contents[win.ID]
99 w.dmg.window(win.ID, content, w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H))99 w.dmg.window(win.ID, content, w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H))
100- if content.Empty() {100+ // Until its first buffer, a window shows the background: no flash.
101- w.fill(body, windowColor(win.ID))101+ w.fill(body, parseColor(w.s.Background))
102- } else {102+ if !content.Empty() {
103- w.fill(body, parseColor(w.s.Background))
104 w.place(win.ID, &content, c.X, c.Y, c.W, c.H)103 w.place(win.ID, &content, c.X, c.Y, c.W, c.H)
105 }104 }
106 if win.Floating {105 if win.Floating {
@@ -335,27 +334,3 @@ func parseColor(s string) [3]uint8 {
335 }334 }
336 return c335 return c
337 }336 }
338-
339-// windowColor is the placeholder color of a window with no content yet.
340-func windowColor(id ports.WindowID) [3]uint8 {
341- h := math.Mod(float64(id)*0.618034, 1) * 6
342- sector := int(h)
343- f := h - float64(sector)
344- p, q, t := 0.4, 0.8*(1-0.5*f), 0.8*(1-0.5*(1-f))
345- var a, b, c float64
346- switch sector {
347- case 0:
348- a, b, c = 0.8, t, p
349- case 1:
350- a, b, c = q, 0.8, p
351- case 2:
352- a, b, c = p, 0.8, t
353- case 3:
354- a, b, c = p, q, 0.8
355- case 4:
356- a, b, c = t, p, 0.8
357- default:
358- a, b, c = 0.8, p, q
359- }
360- return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)}
361-}

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