detached cca9f08

fix(drm): skip the single-plane retry when driver images had one plane

Record the plane count of the last exported images; a refused one-plane
driver image goes straight to linear. TestExportTargetsPlanes now reads
each multi-plane target back, checks plane offsets and skips without a
multi-plane modifier. Clamp the plane count once in exportTarget and log
the exported target at Debug.
commit
cca9f0846ca9e9374be9097ed7d4f4fb66fdfce0
author
brice · · 2026-10-04 20:13 UTC
parent
5491ddd
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +92 -24
M internal/adapters/drm/commit_test.go +39 -2
@@ -120,14 +120,23 @@ func TestShowImagesTestsBeforeModeset(t *testing.T) {
120120 o.cursor = nil
121121 r := portsmocks.NewMockRenderer(t)
122122 buf := func() ports.DMABuf {
123+ var b ports.DMABuf
124+ for range 2 {
125+ f, w, _ := os.Pipe()
126+ w.Close()
127+ b.Planes = append(b.Planes, ports.DMABufPlane{File: f})
128+ }
129+ return b
130+ }
131+ linear := func() ports.DMABuf {
123132 f, w, _ := os.Pipe()
124133 w.Close()
125134 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
126135 }
127136 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
128137 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
129- r.EXPECT().ExportTargets(2, []uint64(nil), true).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
130- r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
138+ r.EXPECT().ExportTargets(2, []uint64(nil), true).Return([]ports.DMABuf{linear(), linear()}, nil).Once()
139+ r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{linear(), linear()}, nil).Once()
131140 r.EXPECT().UseTarget(mock.Anything).Return()
132141 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
133142 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
@@ -194,6 +203,34 @@ func TestShowImagesRetriesSinglePlaneBeforeLinear(t *testing.T) {
194203 }
195204 }
196205
206+// Refused driver images that already had one plane are not retried as
207+// single-plane images: the next export is linear.
208+func TestShowImagesSkipsSinglePlaneWhenDriverImagesHadOnePlane(t *testing.T) {
209+ o, k, commits := testOutput(t, unix.EINVAL)
210+ o.cursor = nil
211+ buf := func() ports.DMABuf {
212+ f, w, _ := os.Pipe()
213+ w.Close()
214+ return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
215+ }
216+ r := portsmocks.NewMockRenderer(t)
217+ r.EXPECT().SetHDR(float64(0)).Return().Maybe()
218+ r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
219+ r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
220+ r.EXPECT().UseTarget(mock.Anything).Return()
221+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
222+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
223+ k.EXPECT().rmFB(mock.Anything).Return(nil)
224+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
225+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
226+ if err := o.showImages(r, imagesDriver, nil); err != nil {
227+ t.Fatal(err)
228+ }
229+ if o.kind != imagesLinear || len(*commits) != 3 {
230+ t.Fatalf("kind %d, %d commits", o.kind, len(*commits))
231+ }
232+}
233+
197234 // Enabling an output whose images were not made yet (started switched
198235 // away) tests them and modesets once.
199236 func TestEnableAfterStartAwayModesetsOnce(t *testing.T) {
@@ -120,14 +120,23 @@ func TestShowImagesTestsBeforeModeset(t *testing.T) {
120 o.cursor = nil120 o.cursor = nil
121 r := portsmocks.NewMockRenderer(t)121 r := portsmocks.NewMockRenderer(t)
122 buf := func() ports.DMABuf {122 buf := func() ports.DMABuf {
123+ var b ports.DMABuf
124+ for range 2 {
125+ f, w, _ := os.Pipe()
126+ w.Close()
127+ b.Planes = append(b.Planes, ports.DMABufPlane{File: f})
128+ }
129+ return b
130+ }
131+ linear := func() ports.DMABuf {
123 f, w, _ := os.Pipe()132 f, w, _ := os.Pipe()
124 w.Close()133 w.Close()
125 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}134 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
126 }135 }
127 r.EXPECT().SetHDR(float64(0)).Return().Maybe()136 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
128 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()137 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
129- r.EXPECT().ExportTargets(2, []uint64(nil), true).Return([]ports.DMABuf{buf(), buf()}, nil).Once()138+ r.EXPECT().ExportTargets(2, []uint64(nil), true).Return([]ports.DMABuf{linear(), linear()}, nil).Once()
130- r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()139+ r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{linear(), linear()}, nil).Once()
131 r.EXPECT().UseTarget(mock.Anything).Return()140 r.EXPECT().UseTarget(mock.Anything).Return()
132 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)141 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
133 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)142 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
@@ -194,6 +203,34 @@ func TestShowImagesRetriesSinglePlaneBeforeLinear(t *testing.T) {
194 }203 }
195 }204 }
196 205
206+// Refused driver images that already had one plane are not retried as
207+// single-plane images: the next export is linear.
208+func TestShowImagesSkipsSinglePlaneWhenDriverImagesHadOnePlane(t *testing.T) {
209+ o, k, commits := testOutput(t, unix.EINVAL)
210+ o.cursor = nil
211+ buf := func() ports.DMABuf {
212+ f, w, _ := os.Pipe()
213+ w.Close()
214+ return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
215+ }
216+ r := portsmocks.NewMockRenderer(t)
217+ r.EXPECT().SetHDR(float64(0)).Return().Maybe()
218+ r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
219+ r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
220+ r.EXPECT().UseTarget(mock.Anything).Return()
221+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
222+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
223+ k.EXPECT().rmFB(mock.Anything).Return(nil)
224+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
225+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
226+ if err := o.showImages(r, imagesDriver, nil); err != nil {
227+ t.Fatal(err)
228+ }
229+ if o.kind != imagesLinear || len(*commits) != 3 {
230+ t.Fatalf("kind %d, %d commits", o.kind, len(*commits))
231+ }
232+}
233+
197 // Enabling an output whose images were not made yet (started switched234 // Enabling an output whose images were not made yet (started switched
198 // away) tests them and modesets once.235 // away) tests them and modesets once.
199 func TestEnableAfterStartAwayModesetsOnce(t *testing.T) {236 func TestEnableAfterStartAwayModesetsOnce(t *testing.T) {
M internal/adapters/drm/images.go +7 -0
@@ -104,6 +104,11 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er
104104 o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")
105105 o.freeImages()
106106 kind, cause = got+1, err
107+ if kind == imagesSinglePlane && o.planes == 1 {
108+ // The refused images already had one plane: single-plane
109+ // images would be the same ones.
110+ kind = imagesLinear
111+ }
107112 }
108113 }
109114
@@ -154,6 +159,7 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool)
154159 if err != nil {
155160 return err
156161 }
162+ o.planes = 0
157163 for i := range bufs {
158164 if len(bufs[i].Planes) == 0 || len(bufs[i].Planes) > maxFBPlanes {
159165 err = errors.Join(err, fmt.Errorf("output image has %d planes", len(bufs[i].Planes)))
@@ -194,6 +200,7 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool)
194200 _, _ = r.ExportTargets(0, nil, false)
195201 return err
196202 }
203+ o.planes = len(bufs[0].Planes)
197204 o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Msg("zero-copy output")
198205 return nil
199206 }
@@ -104,6 +104,11 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er
104 o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")104 o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")
105 o.freeImages()105 o.freeImages()
106 kind, cause = got+1, err106 kind, cause = got+1, err
107+ if kind == imagesSinglePlane && o.planes == 1 {
108+ // The refused images already had one plane: single-plane
109+ // images would be the same ones.
110+ kind = imagesLinear
111+ }
107 }112 }
108 }113 }
109 114
@@ -154,6 +159,7 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool)
154 if err != nil {159 if err != nil {
155 return err160 return err
156 }161 }
162+ o.planes = 0
157 for i := range bufs {163 for i := range bufs {
158 if len(bufs[i].Planes) == 0 || len(bufs[i].Planes) > maxFBPlanes {164 if len(bufs[i].Planes) == 0 || len(bufs[i].Planes) > maxFBPlanes {
159 err = errors.Join(err, fmt.Errorf("output image has %d planes", len(bufs[i].Planes)))165 err = errors.Join(err, fmt.Errorf("output image has %d planes", len(bufs[i].Planes)))
@@ -194,6 +200,7 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool)
194 _, _ = r.ExportTargets(0, nil, false)200 _, _ = r.ExportTargets(0, nil, false)
195 return err201 return err
196 }202 }
203+ o.planes = len(bufs[0].Planes)
197 o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Msg("zero-copy output")204 o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Msg("zero-copy output")
198 return nil205 return nil
199 }206 }
M internal/adapters/drm/output.go +2 -0
@@ -176,6 +176,8 @@ type Output struct {
176176 protectNotBefore time.Time
177177 // kind is how the images were made.
178178 kind imageKind
179+ // planes is the memory plane count of the last exported images.
180+ planes int
179181 // formats receives the direct scanout formats after each modeset;
180182 // sampled and device are what they are built from (Want).
181183 formats chan<- ports.OutputFormats
@@ -176,6 +176,8 @@ type Output struct {
176 protectNotBefore time.Time176 protectNotBefore time.Time
177 // kind is how the images were made.177 // kind is how the images were made.
178 kind imageKind178 kind imageKind
179+ // planes is the memory plane count of the last exported images.
180+ planes int
179 // formats receives the direct scanout formats after each modeset;181 // formats receives the direct scanout formats after each modeset;
180 // sampled and device are what they are built from (Want).182 // sampled and device are what they are built from (Want).
181 formats chan<- ports.OutputFormats183 formats chan<- ports.OutputFormats
M internal/adapters/vulkan/targets.go +3 -3
@@ -255,9 +255,9 @@ func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
255255 if err := checked("vkGetImageDrmFormatModifierPropertiesEXT", d.GetImageDrmFormatModifierPropertiesEXT(r.device, t.image, &props)); err != nil {
256256 return nil, ports.DMABuf{}, err
257257 }
258- n := max(r.modPlanes[props.DrmFormatModifier], 1)
258+ n := min(max(r.modPlanes[props.DrmFormatModifier], 1), maxModifierPlanes)
259259 var layouts [maxModifierPlanes]vk.SubresourceLayout
260- for i := range min(n, maxModifierPlanes) {
260+ for i := range n {
261261 sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT << i}
262262 d.GetImageSubresourceLayout(r.device, t.image, &sub, &layouts[i])
263263 }
@@ -291,7 +291,7 @@ func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
291291 }
292292 planes = append(planes, ports.DMABufPlane{File: os.NewFile(uintptr(dup), "neferwl-target"), Offset: uint32(layouts[i].Offset), Stride: uint32(layouts[i].RowPitch)})
293293 }
294- r.log.Info().Uint64("modifier", props.DrmFormatModifier).Uint32("planes", n).Msg("render target exported")
294+ r.log.Debug().Uint64("modifier", props.DrmFormatModifier).Uint32("planes", n).Msg("render target exported")
295295 buf := ports.DMABuf{Width: r.width, Height: r.height, Format: fourcc, Modifier: props.DrmFormatModifier, Planes: planes}
296296 ok = true
297297 return t, buf, nil
@@ -255,9 +255,9 @@ func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
255 if err := checked("vkGetImageDrmFormatModifierPropertiesEXT", d.GetImageDrmFormatModifierPropertiesEXT(r.device, t.image, &props)); err != nil {255 if err := checked("vkGetImageDrmFormatModifierPropertiesEXT", d.GetImageDrmFormatModifierPropertiesEXT(r.device, t.image, &props)); err != nil {
256 return nil, ports.DMABuf{}, err256 return nil, ports.DMABuf{}, err
257 }257 }
258- n := max(r.modPlanes[props.DrmFormatModifier], 1)258+ n := min(max(r.modPlanes[props.DrmFormatModifier], 1), maxModifierPlanes)
259 var layouts [maxModifierPlanes]vk.SubresourceLayout259 var layouts [maxModifierPlanes]vk.SubresourceLayout
260- for i := range min(n, maxModifierPlanes) {260+ for i := range n {
261 sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT << i}261 sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT << i}
262 d.GetImageSubresourceLayout(r.device, t.image, &sub, &layouts[i])262 d.GetImageSubresourceLayout(r.device, t.image, &sub, &layouts[i])
263 }263 }
@@ -291,7 +291,7 @@ func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
291 }291 }
292 planes = append(planes, ports.DMABufPlane{File: os.NewFile(uintptr(dup), "neferwl-target"), Offset: uint32(layouts[i].Offset), Stride: uint32(layouts[i].RowPitch)})292 planes = append(planes, ports.DMABufPlane{File: os.NewFile(uintptr(dup), "neferwl-target"), Offset: uint32(layouts[i].Offset), Stride: uint32(layouts[i].RowPitch)})
293 }293 }
294- r.log.Info().Uint64("modifier", props.DrmFormatModifier).Uint32("planes", n).Msg("render target exported")294+ r.log.Debug().Uint64("modifier", props.DrmFormatModifier).Uint32("planes", n).Msg("render target exported")
295 buf := ports.DMABuf{Width: r.width, Height: r.height, Format: fourcc, Modifier: props.DrmFormatModifier, Planes: planes}295 buf := ports.DMABuf{Width: r.width, Height: r.height, Format: fourcc, Modifier: props.DrmFormatModifier, Planes: planes}
296 ok = true296 ok = true
297 return t, buf, nil297 return t, buf, nil
M internal/adapters/vulkan/targets_test.go +41 -19
@@ -209,25 +209,44 @@ func TestFilterModifiersKeepsMultiPlane(t *testing.T) {
209209 }
210210
211211 // Every exported target carries one file per memory plane of its modifier
212-// (DCC modifiers have two to four), each with a layout, and each file is
213-// an independent descriptor of the shared memory.
212+// (DCC modifiers have two to four), each with a layout, and drawing into
213+// each target works: the frames read back through the producing renderer.
214+// A multi-plane image cannot be read back through a viewer import, which
215+// only imports one-plane buffers.
214216 func TestExportTargetsPlanes(t *testing.T) {
215217 r, err := New(64, 32)
216218 if err != nil {
217219 t.Skipf("Vulkan unavailable: %v", err)
218220 }
219221 defer r.Close()
220- bufs, err := r.ExportTargets(2, nil, false)
221- if err != nil {
222- t.Skipf("no exportable targets: %v", err)
223- }
224- defer func() {
222+ closeAll := func(bufs []ports.DMABuf) {
225223 for _, b := range bufs {
226224 for _, p := range b.Planes {
227225 p.File.Close()
228226 }
229227 }
230- }()
228+ }
229+ // Single-plane export never returns a multi-plane modifier.
230+ one, err := r.ExportTargets(1, nil, true)
231+ if err != nil {
232+ t.Skipf("no single-plane targets: %v", err)
233+ }
234+ if len(one[0].Planes) != 1 || r.modPlanes[one[0].Modifier] > 1 {
235+ t.Fatalf("single-plane export has %d planes (modifier %#x)", len(one[0].Planes), one[0].Modifier)
236+ }
237+ closeAll(one)
238+ multi := false
239+ for _, m := range r.renderMods {
240+ multi = multi || r.modPlanes[m] > 1
241+ }
242+ if !multi {
243+ t.Skip("device has no multi-plane target modifier")
244+ }
245+ bufs, err := r.ExportTargets(2, nil, false)
246+ if err != nil {
247+ t.Skipf("no exportable targets: %v", err)
248+ }
249+ defer closeAll(bufs)
231250 for _, b := range bufs {
232251 want := max(r.modPlanes[b.Modifier], 1)
233252 if uint32(len(b.Planes)) != want {
@@ -238,6 +257,10 @@ func TestExportTargetsPlanes(t *testing.T) {
238257 if p.File == nil || p.Stride == 0 {
239258 t.Fatalf("modifier %#x plane %d: %+v", b.Modifier, i, p)
240259 }
260+ // The planes share one memory object: planes 1.. lie after plane 0.
261+ if i > 0 && p.Offset == 0 {
262+ t.Fatalf("modifier %#x plane %d at offset 0", b.Modifier, i)
263+ }
241264 if seen[int(p.File.Fd())] {
242265 t.Fatalf("modifier %#x: plane %d shares a file", b.Modifier, i)
243266 }
@@ -245,17 +268,16 @@ func TestExportTargetsPlanes(t *testing.T) {
245268 }
246269 t.Logf("modifier %#x planes %d", b.Modifier, len(b.Planes))
247270 }
248- // Single-plane export never returns a multi-plane modifier.
249- one, err := r.ExportTargets(1, nil, true)
250- if err != nil {
251- t.Skipf("no single-plane targets: %v", err)
252- }
253- defer func() {
254- for _, p := range one[0].Planes {
255- p.File.Close()
271+ for i, tc := range []struct {
272+ bg string
273+ want color.RGBA
274+ }{{"#ff0000", color.RGBA{255, 0, 0, 255}}, {"#0000ff", color.RGBA{0, 0, 255, 255}}} {
275+ r.UseTarget(i)
276+ if err := render(r, ports.Scene{Background: tc.bg}, nil); err != nil {
277+ t.Fatal(err)
278+ }
279+ if got := readPixels(t, r).At(3, 3); got != tc.want {
280+ t.Fatalf("target %d (modifier %#x): read back %v, want %v", i, bufs[i].Modifier, got, tc.want)
256281 }
257- }()
258- if len(one[0].Planes) != 1 {
259- t.Fatalf("single-plane export has %d planes", len(one[0].Planes))
260282 }
261283 }
@@ -209,25 +209,44 @@ func TestFilterModifiersKeepsMultiPlane(t *testing.T) {
209 }209 }
210 210
211 // Every exported target carries one file per memory plane of its modifier211 // Every exported target carries one file per memory plane of its modifier
212-// (DCC modifiers have two to four), each with a layout, and each file is212+// (DCC modifiers have two to four), each with a layout, and drawing into
213-// an independent descriptor of the shared memory.213+// each target works: the frames read back through the producing renderer.
214+// A multi-plane image cannot be read back through a viewer import, which
215+// only imports one-plane buffers.
214 func TestExportTargetsPlanes(t *testing.T) {216 func TestExportTargetsPlanes(t *testing.T) {
215 r, err := New(64, 32)217 r, err := New(64, 32)
216 if err != nil {218 if err != nil {
217 t.Skipf("Vulkan unavailable: %v", err)219 t.Skipf("Vulkan unavailable: %v", err)
218 }220 }
219 defer r.Close()221 defer r.Close()
220- bufs, err := r.ExportTargets(2, nil, false)222+ closeAll := func(bufs []ports.DMABuf) {
221- if err != nil {
222- t.Skipf("no exportable targets: %v", err)
223- }
224- defer func() {
225 for _, b := range bufs {223 for _, b := range bufs {
226 for _, p := range b.Planes {224 for _, p := range b.Planes {
227 p.File.Close()225 p.File.Close()
228 }226 }
229 }227 }
230- }()228+ }
229+ // Single-plane export never returns a multi-plane modifier.
230+ one, err := r.ExportTargets(1, nil, true)
231+ if err != nil {
232+ t.Skipf("no single-plane targets: %v", err)
233+ }
234+ if len(one[0].Planes) != 1 || r.modPlanes[one[0].Modifier] > 1 {
235+ t.Fatalf("single-plane export has %d planes (modifier %#x)", len(one[0].Planes), one[0].Modifier)
236+ }
237+ closeAll(one)
238+ multi := false
239+ for _, m := range r.renderMods {
240+ multi = multi || r.modPlanes[m] > 1
241+ }
242+ if !multi {
243+ t.Skip("device has no multi-plane target modifier")
244+ }
245+ bufs, err := r.ExportTargets(2, nil, false)
246+ if err != nil {
247+ t.Skipf("no exportable targets: %v", err)
248+ }
249+ defer closeAll(bufs)
231 for _, b := range bufs {250 for _, b := range bufs {
232 want := max(r.modPlanes[b.Modifier], 1)251 want := max(r.modPlanes[b.Modifier], 1)
233 if uint32(len(b.Planes)) != want {252 if uint32(len(b.Planes)) != want {
@@ -238,6 +257,10 @@ func TestExportTargetsPlanes(t *testing.T) {
238 if p.File == nil || p.Stride == 0 {257 if p.File == nil || p.Stride == 0 {
239 t.Fatalf("modifier %#x plane %d: %+v", b.Modifier, i, p)258 t.Fatalf("modifier %#x plane %d: %+v", b.Modifier, i, p)
240 }259 }
260+ // The planes share one memory object: planes 1.. lie after plane 0.
261+ if i > 0 && p.Offset == 0 {
262+ t.Fatalf("modifier %#x plane %d at offset 0", b.Modifier, i)
263+ }
241 if seen[int(p.File.Fd())] {264 if seen[int(p.File.Fd())] {
242 t.Fatalf("modifier %#x: plane %d shares a file", b.Modifier, i)265 t.Fatalf("modifier %#x: plane %d shares a file", b.Modifier, i)
243 }266 }
@@ -245,17 +268,16 @@ func TestExportTargetsPlanes(t *testing.T) {
245 }268 }
246 t.Logf("modifier %#x planes %d", b.Modifier, len(b.Planes))269 t.Logf("modifier %#x planes %d", b.Modifier, len(b.Planes))
247 }270 }
248- // Single-plane export never returns a multi-plane modifier.271+ for i, tc := range []struct {
249- one, err := r.ExportTargets(1, nil, true)272+ bg string
250- if err != nil {273+ want color.RGBA
251- t.Skipf("no single-plane targets: %v", err)274+ }{{"#ff0000", color.RGBA{255, 0, 0, 255}}, {"#0000ff", color.RGBA{0, 0, 255, 255}}} {
252- }275+ r.UseTarget(i)
253- defer func() {276+ if err := render(r, ports.Scene{Background: tc.bg}, nil); err != nil {
254- for _, p := range one[0].Planes {277+ t.Fatal(err)
255- p.File.Close()278+ }
279+ if got := readPixels(t, r).At(3, 3); got != tc.want {
280+ t.Fatalf("target %d (modifier %#x): read back %v, want %v", i, bufs[i].Modifier, got, tc.want)
256 }281 }
257- }()
258- if len(one[0].Planes) != 1 {
259- t.Fatalf("single-plane export has %d planes", len(one[0].Planes))
260 }282 }
261 }283 }

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