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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 30 | 30 | c := shmContent(t, 2, 1, 8, px) | |
| 31 | 31 | c.LogicalW, c.LogicalH = 64, 16 | |
| 32 | 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 | 34 | t.Fatal(err) | |
| 35 | 35 | } | |
| 36 | 36 | got := r.Pixels().RGBAAt(32, 8) | |
| @@ -63,7 +63,7 @@ func TestRendererScaledDMABufFiltersLinearly(t *testing.T) { | |||
| 63 | 63 | buf := &ports.DMABuf{ID: 3, Width: 64, Height: 1, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}} | |
| 64 | 64 | scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 128, H: 4}, Borderless: true}}} | |
| 65 | 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 | 67 | t.Fatal(err) | |
| 68 | 68 | } | |
| 69 | 69 | // Target x=64 (centre 64.5) maps to buffer 32.25: between texel 31 | |
| @@ -136,3 +136,46 @@ func TestRendererSHMCopiedOncePerContent(t *testing.T) { | |||
| 136 | 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 | + | } | |
| @@ -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, 16 | 31 | 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 31 | 69 | // 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) { | |||
| 81 | 81 | buf := &ports.DMABuf{ID: 7, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}} | |
| 82 | 82 | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 64, H: 16}}}} | |
| 83 | 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 | 85 | t.Fatal(err) | |
| 86 | 86 | } | |
| 87 | 87 | px := r.Pixels() | |
| @@ -98,7 +98,7 @@ func TestRendererImportsDMABuf(t *testing.T) { | |||
| 98 | 98 | } | |
| 99 | 99 | // Frames without the buffer: its import is released after importTTL. | |
| 100 | 100 | for range importTTL + 1 { | |
| 101 | - | if _, err := r.Render(scene, nil); err != nil { | |
| 101 | + | if err := render(r, scene, nil); err != nil { | |
| 102 | 102 | t.Fatal(err) | |
| 103 | 103 | } | |
| 104 | 104 | } | |
| @@ -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 { | |||
| 52 | 52 | if err := checked("vkCreateFence", d.CreateFence(r.device, &fi, nil, &s.fence)); err != nil { | |
| 53 | 53 | return err | |
| 54 | 54 | } | |
| 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 { | |
| 61 | 56 | return err | |
| 62 | 57 | } | |
| 63 | 58 | } | |
| 64 | 59 | 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 | 80 | func (r *Renderer) destroySlots() { | |
| 68 | 81 | d := r.dd | |
| 69 | 82 | for i := range r.slots { | |
| @@ -144,6 +157,8 @@ func (r *Renderer) idle() { | |||
| 144 | 157 | // the GPU finished it; nil when the device cannot export a sync file, | |
| 145 | 158 | // then the frame is finished on return. | |
| 146 | 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 | 162 | r.frame++ | |
| 148 | 163 | slot := &r.slots[r.frame%frameSlots] | |
| 149 | 164 | // 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 | 252 | var fd int32 | |
| 238 | 253 | get := vk.SemaphoreGetFdInfoKHR{SType: vk.StructureTypeSemaphoreGetFDInfoKHR, Semaphore: slot.done, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit} | |
| 239 | 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 | 262 | if fd < 0 { | |
| 243 | 263 | // Already signalled: the driver may return -1. | |
| @@ -259,7 +279,9 @@ func (r *Renderer) ownership(cmd vk.CommandBuffer, dmas []*imported, acquire boo | |||
| 259 | 279 | b.OldLayout, b.NewLayout = vk.ImageLayoutGeneral, vk.ImageLayoutShaderReadOnlyOptimal | |
| 260 | 280 | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family | |
| 261 | 281 | 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 | 285 | } else { | |
| 264 | 286 | b.OldLayout, b.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutGeneral | |
| 265 | 287 | 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 err | 53 | 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 err | 56 | return err | ||
| 62 | } | 57 | } | ||
| 63 | } | 58 | } | ||
| 64 | return nil | 59 | 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.dd | 81 | 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 buffer | 164 | // 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 int32 | 252 | 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.ImageLayoutShaderReadOnlyOptimal | 279 | b.OldLayout, b.NewLayout = vk.ImageLayoutGeneral, vk.ImageLayoutShaderReadOnlyOptimal | ||
| 260 | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family | 280 | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family | ||
| 261 | b.DstAccessMask = vk.AccessShaderReadBit | 281 | 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.ImageLayoutGeneral | 286 | b.OldLayout, b.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutGeneral | ||
| 265 | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT | 287 | 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) { | |||
| 48 | 48 | bg := color.RGBA{16, 32, 48, 255} | |
| 49 | 49 | check := func(s ports.Scene, expected map[image.Point]color.RGBA) { | |
| 50 | 50 | t.Helper() | |
| 51 | - | if _, err := r.Render(s, nil); err != nil { | |
| 51 | + | if err := render(r, s, nil); err != nil { | |
| 52 | 52 | t.Fatal(err) | |
| 53 | 53 | } | |
| 54 | 54 | for p, want := range expected { | |
| @@ -92,7 +92,7 @@ func TestRendererContents(t *testing.T) { | |||
| 92 | 92 | if c != nil { | |
| 93 | 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 | 96 | t.Fatal(err) | |
| 97 | 97 | } | |
| 98 | 98 | } | |
| @@ -127,7 +127,7 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) { | |||
| 127 | 127 | {ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}}, | |
| 128 | 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 | 131 | t.Fatal(err) | |
| 132 | 132 | } | |
| 133 | 133 | c := windowColor(2) | |
| @@ -143,7 +143,7 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) { | |||
| 143 | 143 | opaque := shmContent(t, 16, 16, 64, px) | |
| 144 | 144 | opaque.Opaque = true | |
| 145 | 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 | 147 | t.Fatal(err) | |
| 148 | 148 | } | |
| 149 | 149 | for _, tc := range []struct { | |
| @@ -174,7 +174,7 @@ func TestRendererLayers(t *testing.T) { | |||
| 174 | 174 | window := color.RGBA{wc[0], wc[1], wc[2], 255} | |
| 175 | 175 | check := func(want color.RGBA) { | |
| 176 | 176 | t.Helper() | |
| 177 | - | if _, err := r.Render(scene, contents); err != nil { | |
| 177 | + | if err := render(r, scene, contents); err != nil { | |
| 178 | 178 | t.Fatal(err) | |
| 179 | 179 | } | |
| 180 | 180 | if got := r.Pixels().RGBAAt(5, 2); got != want { | |
| @@ -218,7 +218,7 @@ func TestRendererScale(t *testing.T) { | |||
| 218 | 218 | scaled := shmContent(t, 20, 20, 80, solid(20, 20, red)) | |
| 219 | 219 | scaled.ID, scaled.LogicalW, scaled.LogicalH = 1, 10, 10 | |
| 220 | 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 | 222 | t.Fatal(err) | |
| 223 | 223 | } | |
| 224 | 224 | px := r.Pixels() | |
| @@ -236,7 +236,7 @@ func TestRendererScale(t *testing.T) { | |||
| 236 | 236 | // Scale 1.5 with an integer-scale client (buffer scale 2): the 20x20 | |
| 237 | 237 | // buffer is 10x10 logical and downscaled to 15x15 physical at (6,6). | |
| 238 | 238 | s.Scale = 1.5 | |
| 239 | - | if _, err := r.Render(s, content); err != nil { | |
| 239 | + | if err := render(r, s, content); err != nil { | |
| 240 | 240 | t.Fatal(err) | |
| 241 | 241 | } | |
| 242 | 242 | px = r.Pixels() | |
| @@ -252,7 +252,7 @@ func TestRendererScale(t *testing.T) { | |||
| 252 | 252 | odd := shmContent(t, 16, 15, 64, solid(16, 15, red)) | |
| 253 | 253 | odd.ID, odd.LogicalW, odd.LogicalH = 1, 10, 10 | |
| 254 | 254 | content[1] = *odd | |
| 255 | - | if _, err := r.Render(s, content); err != nil { | |
| 255 | + | if err := render(r, s, content); err != nil { | |
| 256 | 256 | t.Fatal(err) | |
| 257 | 257 | } | |
| 258 | 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 | 309 | {X: 3, Y: 3, Below: true, SurfaceContent: solidContent(t, 30, 20, blue)}, | |
| 310 | 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 | 313 | t.Fatal(err) | |
| 314 | 314 | } | |
| 315 | 315 | px := r.Pixels() | |
| @@ -327,7 +327,7 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) { | |||
| 327 | 327 | } | |
| 328 | 328 | // A root with no buffer of its own still shows its children. | |
| 329 | 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 | 331 | t.Fatal(err) | |
| 332 | 332 | } | |
| 333 | 333 | if got := r.Pixels().At(20, 15); got != blue { | |
| @@ -344,7 +344,7 @@ func TestRendererTallFill(t *testing.T) { | |||
| 344 | 344 | } | |
| 345 | 345 | defer r.Close() | |
| 346 | 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 | 348 | t.Fatal(err) | |
| 349 | 349 | } | |
| 350 | 350 | c := windowColor(1) | |
| @@ -402,7 +402,7 @@ func TestRendererAlphaBlend(t *testing.T) { | |||
| 402 | 402 | {ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 16, H: 8}}, | |
| 403 | 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 | 406 | t.Fatal(err) | |
| 407 | 407 | } | |
| 408 | 408 | 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 = true | 144 | 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, 10 | 219 | 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 20x20 | 236 | // 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.5 | 238 | 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, 10 | 253 | odd.ID, odd.LogicalW, odd.LogicalH = 1, 10, 10 | ||
| 254 | content[1] = *odd | 254 | 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) { | |||
| 31 | 31 | colors := []string{"#ff0000", "#0000ff"} | |
| 32 | 32 | for i, c := range colors { | |
| 33 | 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 | 35 | t.Fatal(err) | |
| 36 | 36 | } | |
| 37 | 37 | } | |
| @@ -55,7 +55,7 @@ func TestExportTargets(t *testing.T) { | |||
| 55 | 55 | } | |
| 56 | 56 | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 32}}}} | |
| 57 | 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 | 59 | t.Fatal(err) | |
| 60 | 60 | } | |
| 61 | 61 | 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}) { | ||