detached 8097a63

fix(vulkan): order client buffer acquire after its fence and fall back when sync files cannot be exported

The dmabuf acquire barrier starts at the fragment stage the frame waits on the client fence at. A device refusing exportable semaphores renders with CPU waits instead of failing, and a failed sync file export replaces the still-signalled semaphore. Tests close every frame fence and check the wl_shm double-buffer rule.
commit
8097a630b0ee03d343d280eb3dfccdaf6bdb0987
author
brice · · 2026-09-26 11:53 UTC
parent
c595af8
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +91 -26
M internal/adapters/vulkan/compose_test.go +45 -2
@@ -30,7 +30,7 @@ func TestRendererScaledSHMFiltersLinearly(t *testing.T) {
3030 c := shmContent(t, 2, 1, 8, px)
3131 c.LogicalW, c.LogicalH = 64, 16
3232 scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}, Borderless: true}}}
33- if _, err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {
33+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {
3434 t.Fatal(err)
3535 }
3636 got := r.Pixels().RGBAAt(32, 8)
@@ -63,7 +63,7 @@ func TestRendererScaledDMABufFiltersLinearly(t *testing.T) {
6363 buf := &ports.DMABuf{ID: 3, Width: 64, Height: 1, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
6464 scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 128, H: 4}, Borderless: true}}}
6565 c := ports.SurfaceContent{ID: 1, Width: 64, Height: 1, LogicalW: 128, LogicalH: 4, DMABuf: buf}
66- if _, err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
66+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
6767 t.Fatal(err)
6868 }
6969 // Target x=64 (centre 64.5) maps to buffer 32.25: between texel 31
@@ -136,3 +136,46 @@ func TestRendererSHMCopiedOncePerContent(t *testing.T) {
136136 t.Fatalf("new content copied %d", r.copied)
137137 }
138138 }
139+
140+// render draws a frame and closes its fence.
141+func render(r *Renderer, s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {
142+ done, err := r.Render(s, contents)
143+ if done != nil {
144+ done.Close()
145+ }
146+ return err
147+}
148+
149+// A wl_shm window's GPU buffer is rewritten only after every frame that
150+// read it completed: contents 1, 2, 3 back to back alternate two buffers,
151+// and the third copy waits for the frame that drew the first (the slot
152+// ring and shmCopyFor's waitFrame both guarantee it).
153+func TestRendererSHMDoubleBufferWaitsForReaders(t *testing.T) {
154+ r, err := New(32, 32)
155+ if err != nil {
156+ t.Skipf("Vulkan unavailable: %v", err)
157+ }
158+ defer r.Close()
159+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}, Borderless: true}}}
160+ var readers []uint64
161+ for seq := uint64(1); seq <= 3; seq++ {
162+ c := solidContent(t, 16, 16, color.RGBA{uint8(seq * 60), 0, 0, 255})
163+ c.ID, c.Seq = 1, seq
164+ if seq == 3 {
165+ // The buffer about to be reused was last read by frame 1.
166+ if r.completed >= readers[0] {
167+ t.Fatalf("frame %d already known done before the reuse", readers[0])
168+ }
169+ }
170+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
171+ t.Fatal(err)
172+ }
173+ readers = append(readers, r.frame)
174+ if seq == 3 && r.completed < readers[0] {
175+ t.Fatalf("buffer of frame %d rewritten with completed=%d", readers[0], r.completed)
176+ }
177+ }
178+ if got := r.Pixels().RGBAAt(4, 4); got != (color.RGBA{180, 0, 0, 255}) {
179+ t.Fatalf("pixel %v", got)
180+ }
181+}
@@ -30,7 +30,7 @@ func TestRendererScaledSHMFiltersLinearly(t *testing.T) {
30 c := shmContent(t, 2, 1, 8, px)30 c := shmContent(t, 2, 1, 8, px)
31 c.LogicalW, c.LogicalH = 64, 1631 c.LogicalW, c.LogicalH = 64, 16
32 scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}, Borderless: true}}}32 scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}, Borderless: true}}}
33- if _, err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {33+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {
34 t.Fatal(err)34 t.Fatal(err)
35 }35 }
36 got := r.Pixels().RGBAAt(32, 8)36 got := r.Pixels().RGBAAt(32, 8)
@@ -63,7 +63,7 @@ func TestRendererScaledDMABufFiltersLinearly(t *testing.T) {
63 buf := &ports.DMABuf{ID: 3, Width: 64, Height: 1, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}63 buf := &ports.DMABuf{ID: 3, Width: 64, Height: 1, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
64 scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 128, H: 4}, Borderless: true}}}64 scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 128, H: 4}, Borderless: true}}}
65 c := ports.SurfaceContent{ID: 1, Width: 64, Height: 1, LogicalW: 128, LogicalH: 4, DMABuf: buf}65 c := ports.SurfaceContent{ID: 1, Width: 64, Height: 1, LogicalW: 128, LogicalH: 4, DMABuf: buf}
66- if _, err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {66+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
67 t.Fatal(err)67 t.Fatal(err)
68 }68 }
69 // Target x=64 (centre 64.5) maps to buffer 32.25: between texel 3169 // Target x=64 (centre 64.5) maps to buffer 32.25: between texel 31
@@ -136,3 +136,46 @@ func TestRendererSHMCopiedOncePerContent(t *testing.T) {
136 t.Fatalf("new content copied %d", r.copied)136 t.Fatalf("new content copied %d", r.copied)
137 }137 }
138 }138 }
139+
140+// render draws a frame and closes its fence.
141+func render(r *Renderer, s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {
142+ done, err := r.Render(s, contents)
143+ if done != nil {
144+ done.Close()
145+ }
146+ return err
147+}
148+
149+// A wl_shm window's GPU buffer is rewritten only after every frame that
150+// read it completed: contents 1, 2, 3 back to back alternate two buffers,
151+// and the third copy waits for the frame that drew the first (the slot
152+// ring and shmCopyFor's waitFrame both guarantee it).
153+func TestRendererSHMDoubleBufferWaitsForReaders(t *testing.T) {
154+ r, err := New(32, 32)
155+ if err != nil {
156+ t.Skipf("Vulkan unavailable: %v", err)
157+ }
158+ defer r.Close()
159+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}, Borderless: true}}}
160+ var readers []uint64
161+ for seq := uint64(1); seq <= 3; seq++ {
162+ c := solidContent(t, 16, 16, color.RGBA{uint8(seq * 60), 0, 0, 255})
163+ c.ID, c.Seq = 1, seq
164+ if seq == 3 {
165+ // The buffer about to be reused was last read by frame 1.
166+ if r.completed >= readers[0] {
167+ t.Fatalf("frame %d already known done before the reuse", readers[0])
168+ }
169+ }
170+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
171+ t.Fatal(err)
172+ }
173+ readers = append(readers, r.frame)
174+ if seq == 3 && r.completed < readers[0] {
175+ t.Fatalf("buffer of frame %d rewritten with completed=%d", readers[0], r.completed)
176+ }
177+ }
178+ if got := r.Pixels().RGBAAt(4, 4); got != (color.RGBA{180, 0, 0, 255}) {
179+ t.Fatalf("pixel %v", got)
180+ }
181+}
M internal/adapters/vulkan/dmabuf_test.go +2 -2
@@ -81,7 +81,7 @@ func TestRendererImportsDMABuf(t *testing.T) {
8181 buf := &ports.DMABuf{ID: 7, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
8282 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 64, H: 16}}}}
8383 contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 16, DMABuf: buf}}
84- if _, err := r.Render(scene, contents); err != nil {
84+ if err := render(r, scene, contents); err != nil {
8585 t.Fatal(err)
8686 }
8787 px := r.Pixels()
@@ -98,7 +98,7 @@ func TestRendererImportsDMABuf(t *testing.T) {
9898 }
9999 // Frames without the buffer: its import is released after importTTL.
100100 for range importTTL + 1 {
101- if _, err := r.Render(scene, nil); err != nil {
101+ if err := render(r, scene, nil); err != nil {
102102 t.Fatal(err)
103103 }
104104 }
@@ -81,7 +81,7 @@ func TestRendererImportsDMABuf(t *testing.T) {
81 buf := &ports.DMABuf{ID: 7, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}81 buf := &ports.DMABuf{ID: 7, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
82 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 64, H: 16}}}}82 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 64, H: 16}}}}
83 contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 16, DMABuf: buf}}83 contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 16, DMABuf: buf}}
84- if _, err := r.Render(scene, contents); err != nil {84+ if err := render(r, scene, contents); err != nil {
85 t.Fatal(err)85 t.Fatal(err)
86 }86 }
87 px := r.Pixels()87 px := r.Pixels()
@@ -98,7 +98,7 @@ func TestRendererImportsDMABuf(t *testing.T) {
98 }98 }
99 // Frames without the buffer: its import is released after importTTL.99 // Frames without the buffer: its import is released after importTTL.
100 for range importTTL + 1 {100 for range importTTL + 1 {
101- if _, err := r.Render(scene, nil); err != nil {101+ if err := render(r, scene, nil); err != nil {
102 t.Fatal(err)102 t.Fatal(err)
103 }103 }
104 }104 }
M internal/adapters/vulkan/frame.go +30 -8
@@ -52,18 +52,31 @@ func (r *Renderer) createSlots() error {
5252 if err := checked("vkCreateFence", d.CreateFence(r.device, &fi, nil, &s.fence)); err != nil {
5353 return err
5454 }
55- si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo}
56- export := vk.ExportSemaphoreCreateInfo{SType: vk.StructureTypeExportSemaphoreCreateInfo, HandleTypes: vk.ExternalSemaphoreHandleTypeSyncFDBit}
57- if r.syncFD {
58- si.Next = unsafe.Pointer(&export)
59- }
60- if err := checked("vkCreateSemaphore", d.CreateSemaphore(r.device, &si, nil, &s.done)); err != nil {
55+ if err := r.newDone(s); err != nil {
6156 return err
6257 }
6358 }
6459 return nil
6560 }
6661
62+// newDone creates a slot's frame semaphore, exportable as a sync file when
63+// the device allows it; a device that refuses falls back to waiting for
64+// frames on the CPU (syncFD off).
65+func (r *Renderer) newDone(s *frameSlot) error {
66+ d := r.dd
67+ si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo}
68+ export := vk.ExportSemaphoreCreateInfo{SType: vk.StructureTypeExportSemaphoreCreateInfo, HandleTypes: vk.ExternalSemaphoreHandleTypeSyncFDBit}
69+ if r.syncFD {
70+ si.Next = unsafe.Pointer(&export)
71+ if d.CreateSemaphore(r.device, &si, nil, &s.done) == vk.Success {
72+ return nil
73+ }
74+ r.syncFD = false
75+ si.Next = nil
76+ }
77+ return checked("vkCreateSemaphore", d.CreateSemaphore(r.device, &si, nil, &s.done))
78+}
79+
6780 func (r *Renderer) destroySlots() {
6881 d := r.dd
6982 for i := range r.slots {
@@ -144,6 +157,8 @@ func (r *Renderer) idle() {
144157 // the GPU finished it; nil when the device cannot export a sync file,
145158 // then the frame is finished on return.
146159 func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) (done *os.File, err error) {
160+ // A frame that fails before its submit keeps its number: objects it
161+ // touched are freed once a later frame completes (never early).
147162 r.frame++
148163 slot := &r.slots[r.frame%frameSlots]
149164 // The slot's previous frame must be done before its command buffer
@@ -237,7 +252,12 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
237252 var fd int32
238253 get := vk.SemaphoreGetFdInfoKHR{SType: vk.StructureTypeSemaphoreGetFDInfoKHR, Semaphore: slot.done, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit}
239254 if err := checked("vkGetSemaphoreFdKHR", d.GetSemaphoreFdKHR(r.device, &get, &fd)); err != nil {
240- return nil, errors.Join(err, r.waitFrame(frame))
255+ // The semaphore stays signalled: replace it once the frame is
256+ // done, so the slot's next submit signals a fresh one.
257+ err = errors.Join(err, r.waitFrame(frame))
258+ d.DestroySemaphore(r.device, slot.done, nil)
259+ slot.done = 0
260+ return nil, errors.Join(err, r.newDone(slot))
241261 }
242262 if fd < 0 {
243263 // Already signalled: the driver may return -1.
@@ -259,7 +279,9 @@ func (r *Renderer) ownership(cmd vk.CommandBuffer, dmas []*imported, acquire boo
259279 b.OldLayout, b.NewLayout = vk.ImageLayoutGeneral, vk.ImageLayoutShaderReadOnlyOptimal
260280 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
261281 b.DstAccessMask = vk.AccessShaderReadBit
262- r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &b)
282+ // The frame waits on the client's fence at the fragment
283+ // stage: the transfer must come after that wait.
284+ r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &b)
263285 } else {
264286 b.OldLayout, b.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutGeneral
265287 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
@@ -52,18 +52,31 @@ func (r *Renderer) createSlots() error {
52 if err := checked("vkCreateFence", d.CreateFence(r.device, &fi, nil, &s.fence)); err != nil {52 if err := checked("vkCreateFence", d.CreateFence(r.device, &fi, nil, &s.fence)); err != nil {
53 return err53 return err
54 }54 }
55- si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo}55+ if err := r.newDone(s); err != nil {
56- export := vk.ExportSemaphoreCreateInfo{SType: vk.StructureTypeExportSemaphoreCreateInfo, HandleTypes: vk.ExternalSemaphoreHandleTypeSyncFDBit}
57- if r.syncFD {
58- si.Next = unsafe.Pointer(&export)
59- }
60- if err := checked("vkCreateSemaphore", d.CreateSemaphore(r.device, &si, nil, &s.done)); err != nil {
61 return err56 return err
62 }57 }
63 }58 }
64 return nil59 return nil
65 }60 }
66 61
62+// newDone creates a slot's frame semaphore, exportable as a sync file when
63+// the device allows it; a device that refuses falls back to waiting for
64+// frames on the CPU (syncFD off).
65+func (r *Renderer) newDone(s *frameSlot) error {
66+ d := r.dd
67+ si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo}
68+ export := vk.ExportSemaphoreCreateInfo{SType: vk.StructureTypeExportSemaphoreCreateInfo, HandleTypes: vk.ExternalSemaphoreHandleTypeSyncFDBit}
69+ if r.syncFD {
70+ si.Next = unsafe.Pointer(&export)
71+ if d.CreateSemaphore(r.device, &si, nil, &s.done) == vk.Success {
72+ return nil
73+ }
74+ r.syncFD = false
75+ si.Next = nil
76+ }
77+ return checked("vkCreateSemaphore", d.CreateSemaphore(r.device, &si, nil, &s.done))
78+}
79+
67 func (r *Renderer) destroySlots() {80 func (r *Renderer) destroySlots() {
68 d := r.dd81 d := r.dd
69 for i := range r.slots {82 for i := range r.slots {
@@ -144,6 +157,8 @@ func (r *Renderer) idle() {
144 // the GPU finished it; nil when the device cannot export a sync file,157 // the GPU finished it; nil when the device cannot export a sync file,
145 // then the frame is finished on return.158 // then the frame is finished on return.
146 func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) (done *os.File, err error) {159 func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) (done *os.File, err error) {
160+ // A frame that fails before its submit keeps its number: objects it
161+ // touched are freed once a later frame completes (never early).
147 r.frame++162 r.frame++
148 slot := &r.slots[r.frame%frameSlots]163 slot := &r.slots[r.frame%frameSlots]
149 // The slot's previous frame must be done before its command buffer164 // The slot's previous frame must be done before its command buffer
@@ -237,7 +252,12 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
237 var fd int32252 var fd int32
238 get := vk.SemaphoreGetFdInfoKHR{SType: vk.StructureTypeSemaphoreGetFDInfoKHR, Semaphore: slot.done, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit}253 get := vk.SemaphoreGetFdInfoKHR{SType: vk.StructureTypeSemaphoreGetFDInfoKHR, Semaphore: slot.done, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit}
239 if err := checked("vkGetSemaphoreFdKHR", d.GetSemaphoreFdKHR(r.device, &get, &fd)); err != nil {254 if err := checked("vkGetSemaphoreFdKHR", d.GetSemaphoreFdKHR(r.device, &get, &fd)); err != nil {
240- return nil, errors.Join(err, r.waitFrame(frame))255+ // The semaphore stays signalled: replace it once the frame is
256+ // done, so the slot's next submit signals a fresh one.
257+ err = errors.Join(err, r.waitFrame(frame))
258+ d.DestroySemaphore(r.device, slot.done, nil)
259+ slot.done = 0
260+ return nil, errors.Join(err, r.newDone(slot))
241 }261 }
242 if fd < 0 {262 if fd < 0 {
243 // Already signalled: the driver may return -1.263 // Already signalled: the driver may return -1.
@@ -259,7 +279,9 @@ func (r *Renderer) ownership(cmd vk.CommandBuffer, dmas []*imported, acquire boo
259 b.OldLayout, b.NewLayout = vk.ImageLayoutGeneral, vk.ImageLayoutShaderReadOnlyOptimal279 b.OldLayout, b.NewLayout = vk.ImageLayoutGeneral, vk.ImageLayoutShaderReadOnlyOptimal
260 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family280 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
261 b.DstAccessMask = vk.AccessShaderReadBit281 b.DstAccessMask = vk.AccessShaderReadBit
262- r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &b)282+ // The frame waits on the client's fence at the fragment
283+ // stage: the transfer must come after that wait.
284+ r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &b)
263 } else {285 } else {
264 b.OldLayout, b.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutGeneral286 b.OldLayout, b.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutGeneral
265 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT287 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
M internal/adapters/vulkan/renderer_test.go +12 -12
@@ -48,7 +48,7 @@ func TestRendererRender(t *testing.T) {
4848 bg := color.RGBA{16, 32, 48, 255}
4949 check := func(s ports.Scene, expected map[image.Point]color.RGBA) {
5050 t.Helper()
51- if _, err := r.Render(s, nil); err != nil {
51+ if err := render(r, s, nil); err != nil {
5252 t.Fatal(err)
5353 }
5454 for p, want := range expected {
@@ -92,7 +92,7 @@ func TestRendererContents(t *testing.T) {
9292 if c != nil {
9393 contents = map[ports.WindowID]ports.SurfaceContent{1: *c}
9494 }
95- if _, err := r.Render(scene, contents); err != nil {
95+ if err := render(r, scene, contents); err != nil {
9696 t.Fatal(err)
9797 }
9898 }
@@ -127,7 +127,7 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) {
127127 {ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}},
128128 {ID: 2, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}},
129129 }}
130- if _, err := r.Render(scene, nil); err != nil {
130+ if err := render(r, scene, nil); err != nil {
131131 t.Fatal(err)
132132 }
133133 c := windowColor(2)
@@ -143,7 +143,7 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) {
143143 opaque := shmContent(t, 16, 16, 64, px)
144144 opaque.Opaque = true
145145 contents := map[ports.WindowID]ports.SurfaceContent{1: *opaque}
146- if _, err := r.Render(scene, contents); err != nil {
146+ if err := render(r, scene, contents); err != nil {
147147 t.Fatal(err)
148148 }
149149 for _, tc := range []struct {
@@ -174,7 +174,7 @@ func TestRendererLayers(t *testing.T) {
174174 window := color.RGBA{wc[0], wc[1], wc[2], 255}
175175 check := func(want color.RGBA) {
176176 t.Helper()
177- if _, err := r.Render(scene, contents); err != nil {
177+ if err := render(r, scene, contents); err != nil {
178178 t.Fatal(err)
179179 }
180180 if got := r.Pixels().RGBAAt(5, 2); got != want {
@@ -218,7 +218,7 @@ func TestRendererScale(t *testing.T) {
218218 scaled := shmContent(t, 20, 20, 80, solid(20, 20, red))
219219 scaled.ID, scaled.LogicalW, scaled.LogicalH = 1, 10, 10
220220 content := map[ports.WindowID]ports.SurfaceContent{1: *scaled}
221- if _, err := r.Render(s, content); err != nil {
221+ if err := render(r, s, content); err != nil {
222222 t.Fatal(err)
223223 }
224224 px := r.Pixels()
@@ -236,7 +236,7 @@ func TestRendererScale(t *testing.T) {
236236 // Scale 1.5 with an integer-scale client (buffer scale 2): the 20x20
237237 // buffer is 10x10 logical and downscaled to 15x15 physical at (6,6).
238238 s.Scale = 1.5
239- if _, err := r.Render(s, content); err != nil {
239+ if err := render(r, s, content); err != nil {
240240 t.Fatal(err)
241241 }
242242 px = r.Pixels()
@@ -252,7 +252,7 @@ func TestRendererScale(t *testing.T) {
252252 odd := shmContent(t, 16, 15, 64, solid(16, 15, red))
253253 odd.ID, odd.LogicalW, odd.LogicalH = 1, 10, 10
254254 content[1] = *odd
255- if _, err := r.Render(s, content); err != nil {
255+ if err := render(r, s, content); err != nil {
256256 t.Fatal(err)
257257 }
258258 if got := r.Pixels().At(21, 10); got != (color.RGBA{0, 0, 0, 255}) {
@@ -309,7 +309,7 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) {
309309 {X: 3, Y: 3, Below: true, SurfaceContent: solidContent(t, 30, 20, blue)},
310310 {X: 5, Y: 5, SurfaceContent: solidContent(t, 4, 4, green)},
311311 }
312- if _, err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
312+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
313313 t.Fatal(err)
314314 }
315315 px := r.Pixels()
@@ -327,7 +327,7 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) {
327327 }
328328 // A root with no buffer of its own still shows its children.
329329 only := ports.SurfaceContent{Children: []ports.Subsurface{{SurfaceContent: solidContent(t, 20, 10, blue)}}}
330- if _, err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: only}); err != nil {
330+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: only}); err != nil {
331331 t.Fatal(err)
332332 }
333333 if got := r.Pixels().At(20, 15); got != blue {
@@ -344,7 +344,7 @@ func TestRendererTallFill(t *testing.T) {
344344 }
345345 defer r.Close()
346346 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 1, Y: 0, W: 6, H: h}}}}
347- if _, err := r.Render(scene, nil); err != nil {
347+ if err := render(r, scene, nil); err != nil {
348348 t.Fatal(err)
349349 }
350350 c := windowColor(1)
@@ -402,7 +402,7 @@ func TestRendererAlphaBlend(t *testing.T) {
402402 {ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 16, H: 8}},
403403 {ID: 4, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 8, W: 16, H: 8}},
404404 }}
405- if _, err := r.Render(scene, contents); err != nil {
405+ if err := render(r, scene, contents); err != nil {
406406 t.Fatal(err)
407407 }
408408 px := r.Pixels()
@@ -48,7 +48,7 @@ func TestRendererRender(t *testing.T) {
48 bg := color.RGBA{16, 32, 48, 255}48 bg := color.RGBA{16, 32, 48, 255}
49 check := func(s ports.Scene, expected map[image.Point]color.RGBA) {49 check := func(s ports.Scene, expected map[image.Point]color.RGBA) {
50 t.Helper()50 t.Helper()
51- if _, err := r.Render(s, nil); err != nil {51+ if err := render(r, s, nil); err != nil {
52 t.Fatal(err)52 t.Fatal(err)
53 }53 }
54 for p, want := range expected {54 for p, want := range expected {
@@ -92,7 +92,7 @@ func TestRendererContents(t *testing.T) {
92 if c != nil {92 if c != nil {
93 contents = map[ports.WindowID]ports.SurfaceContent{1: *c}93 contents = map[ports.WindowID]ports.SurfaceContent{1: *c}
94 }94 }
95- if _, err := r.Render(scene, contents); err != nil {95+ if err := render(r, scene, contents); err != nil {
96 t.Fatal(err)96 t.Fatal(err)
97 }97 }
98 }98 }
@@ -127,7 +127,7 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) {
127 {ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}},127 {ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}},
128 {ID: 2, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}},128 {ID: 2, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}},
129 }}129 }}
130- if _, err := r.Render(scene, nil); err != nil {130+ if err := render(r, scene, nil); err != nil {
131 t.Fatal(err)131 t.Fatal(err)
132 }132 }
133 c := windowColor(2)133 c := windowColor(2)
@@ -143,7 +143,7 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) {
143 opaque := shmContent(t, 16, 16, 64, px)143 opaque := shmContent(t, 16, 16, 64, px)
144 opaque.Opaque = true144 opaque.Opaque = true
145 contents := map[ports.WindowID]ports.SurfaceContent{1: *opaque}145 contents := map[ports.WindowID]ports.SurfaceContent{1: *opaque}
146- if _, err := r.Render(scene, contents); err != nil {146+ if err := render(r, scene, contents); err != nil {
147 t.Fatal(err)147 t.Fatal(err)
148 }148 }
149 for _, tc := range []struct {149 for _, tc := range []struct {
@@ -174,7 +174,7 @@ func TestRendererLayers(t *testing.T) {
174 window := color.RGBA{wc[0], wc[1], wc[2], 255}174 window := color.RGBA{wc[0], wc[1], wc[2], 255}
175 check := func(want color.RGBA) {175 check := func(want color.RGBA) {
176 t.Helper()176 t.Helper()
177- if _, err := r.Render(scene, contents); err != nil {177+ if err := render(r, scene, contents); err != nil {
178 t.Fatal(err)178 t.Fatal(err)
179 }179 }
180 if got := r.Pixels().RGBAAt(5, 2); got != want {180 if got := r.Pixels().RGBAAt(5, 2); got != want {
@@ -218,7 +218,7 @@ func TestRendererScale(t *testing.T) {
218 scaled := shmContent(t, 20, 20, 80, solid(20, 20, red))218 scaled := shmContent(t, 20, 20, 80, solid(20, 20, red))
219 scaled.ID, scaled.LogicalW, scaled.LogicalH = 1, 10, 10219 scaled.ID, scaled.LogicalW, scaled.LogicalH = 1, 10, 10
220 content := map[ports.WindowID]ports.SurfaceContent{1: *scaled}220 content := map[ports.WindowID]ports.SurfaceContent{1: *scaled}
221- if _, err := r.Render(s, content); err != nil {221+ if err := render(r, s, content); err != nil {
222 t.Fatal(err)222 t.Fatal(err)
223 }223 }
224 px := r.Pixels()224 px := r.Pixels()
@@ -236,7 +236,7 @@ func TestRendererScale(t *testing.T) {
236 // Scale 1.5 with an integer-scale client (buffer scale 2): the 20x20236 // Scale 1.5 with an integer-scale client (buffer scale 2): the 20x20
237 // buffer is 10x10 logical and downscaled to 15x15 physical at (6,6).237 // buffer is 10x10 logical and downscaled to 15x15 physical at (6,6).
238 s.Scale = 1.5238 s.Scale = 1.5
239- if _, err := r.Render(s, content); err != nil {239+ if err := render(r, s, content); err != nil {
240 t.Fatal(err)240 t.Fatal(err)
241 }241 }
242 px = r.Pixels()242 px = r.Pixels()
@@ -252,7 +252,7 @@ func TestRendererScale(t *testing.T) {
252 odd := shmContent(t, 16, 15, 64, solid(16, 15, red))252 odd := shmContent(t, 16, 15, 64, solid(16, 15, red))
253 odd.ID, odd.LogicalW, odd.LogicalH = 1, 10, 10253 odd.ID, odd.LogicalW, odd.LogicalH = 1, 10, 10
254 content[1] = *odd254 content[1] = *odd
255- if _, err := r.Render(s, content); err != nil {255+ if err := render(r, s, content); err != nil {
256 t.Fatal(err)256 t.Fatal(err)
257 }257 }
258 if got := r.Pixels().At(21, 10); got != (color.RGBA{0, 0, 0, 255}) {258 if got := r.Pixels().At(21, 10); got != (color.RGBA{0, 0, 0, 255}) {
@@ -309,7 +309,7 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) {
309 {X: 3, Y: 3, Below: true, SurfaceContent: solidContent(t, 30, 20, blue)},309 {X: 3, Y: 3, Below: true, SurfaceContent: solidContent(t, 30, 20, blue)},
310 {X: 5, Y: 5, SurfaceContent: solidContent(t, 4, 4, green)},310 {X: 5, Y: 5, SurfaceContent: solidContent(t, 4, 4, green)},
311 }311 }
312- if _, err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {312+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
313 t.Fatal(err)313 t.Fatal(err)
314 }314 }
315 px := r.Pixels()315 px := r.Pixels()
@@ -327,7 +327,7 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) {
327 }327 }
328 // A root with no buffer of its own still shows its children.328 // A root with no buffer of its own still shows its children.
329 only := ports.SurfaceContent{Children: []ports.Subsurface{{SurfaceContent: solidContent(t, 20, 10, blue)}}}329 only := ports.SurfaceContent{Children: []ports.Subsurface{{SurfaceContent: solidContent(t, 20, 10, blue)}}}
330- if _, err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: only}); err != nil {330+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: only}); err != nil {
331 t.Fatal(err)331 t.Fatal(err)
332 }332 }
333 if got := r.Pixels().At(20, 15); got != blue {333 if got := r.Pixels().At(20, 15); got != blue {
@@ -344,7 +344,7 @@ func TestRendererTallFill(t *testing.T) {
344 }344 }
345 defer r.Close()345 defer r.Close()
346 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 1, Y: 0, W: 6, H: h}}}}346 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 1, Y: 0, W: 6, H: h}}}}
347- if _, err := r.Render(scene, nil); err != nil {347+ if err := render(r, scene, nil); err != nil {
348 t.Fatal(err)348 t.Fatal(err)
349 }349 }
350 c := windowColor(1)350 c := windowColor(1)
@@ -402,7 +402,7 @@ func TestRendererAlphaBlend(t *testing.T) {
402 {ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 16, H: 8}},402 {ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 16, H: 8}},
403 {ID: 4, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 8, W: 16, H: 8}},403 {ID: 4, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 8, W: 16, H: 8}},
404 }}404 }}
405- if _, err := r.Render(scene, contents); err != nil {405+ if err := render(r, scene, contents); err != nil {
406 t.Fatal(err)406 t.Fatal(err)
407 }407 }
408 px := r.Pixels()408 px := r.Pixels()
M internal/adapters/vulkan/targets_test.go +2 -2
@@ -31,7 +31,7 @@ func TestExportTargets(t *testing.T) {
3131 colors := []string{"#ff0000", "#0000ff"}
3232 for i, c := range colors {
3333 r.UseTarget(i)
34- if _, err := r.Render(ports.Scene{Background: c}, nil); err != nil {
34+ if err := render(r, ports.Scene{Background: c}, nil); err != nil {
3535 t.Fatal(err)
3636 }
3737 }
@@ -55,7 +55,7 @@ func TestExportTargets(t *testing.T) {
5555 }
5656 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 32}}}}
5757 contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 32, Opaque: true, DMABuf: &b}}
58- if _, err := viewer.Render(scene, contents); err != nil {
58+ if err := render(viewer, scene, contents); err != nil {
5959 t.Fatal(err)
6060 }
6161 if got := viewer.Pixels().At(10, 10); got != (color.RGBA{255, 0, 0, 255}) {
@@ -31,7 +31,7 @@ func TestExportTargets(t *testing.T) {
31 colors := []string{"#ff0000", "#0000ff"}31 colors := []string{"#ff0000", "#0000ff"}
32 for i, c := range colors {32 for i, c := range colors {
33 r.UseTarget(i)33 r.UseTarget(i)
34- if _, err := r.Render(ports.Scene{Background: c}, nil); err != nil {34+ if err := render(r, ports.Scene{Background: c}, nil); err != nil {
35 t.Fatal(err)35 t.Fatal(err)
36 }36 }
37 }37 }
@@ -55,7 +55,7 @@ func TestExportTargets(t *testing.T) {
55 }55 }
56 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 32}}}}56 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 32}}}}
57 contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 32, Opaque: true, DMABuf: &b}}57 contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 32, Opaque: true, DMABuf: &b}}
58- if _, err := viewer.Render(scene, contents); err != nil {58+ if err := render(viewer, scene, contents); err != nil {
59 t.Fatal(err)59 t.Fatal(err)
60 }60 }
61 if got := viewer.Pixels().At(10, 10); got != (color.RGBA{255, 0, 0, 255}) {61 if got := viewer.Pixels().At(10, 10); got != (color.RGBA{255, 0, 0, 255}) {

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 25

Tags 10

↑↓ moveenter openesc close