detached 8982159

fix(drm): offer the primary plane's XRGB modifiers for output images

The renderer chose its own render-target modifier and KMS refused it:
on RDNA2 RADV exports pipe-aligned DCC (0x...56bb03, 2 planes) while
amdgpu lists only non-pipe-aligned DCC (0x...563b03), so ADDFB2 returned
EINVAL because no plane advertises that format and modifier, then the
single-plane fallback was used. Pass the plane's IN_FORMATS modifiers for
the image format in SDR as in HDR, so the renderer picks the best common
one first time; document that DCC is not scanned out on this hardware.
commit
8982159d91b7a60b3b137772a99c5d18d73304d0
author
brice · · 2026-10-05 03:33 UTC
committer
brice · · 2026-10-05 03:49 UTC
parent
5361974
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +126 -7
M docs/performance.md +1 -1
@@ -10,7 +10,7 @@ How NeferWL keeps its CPU and memory use low, how to measure it, and the referen
1010 - **Game frames keep a short gap after each VRR flip.** On amdgpu, a flip committed right after the previous flip event can miss the early refresh and wait for the panel's slowest rate (~21 ms at 48 Hz); the next frame then lands at the same point, and the screen stays at 48 Hz while the game renders at 150 fps. Game frames under VRR therefore commit at least `render.vrr-flip-gap` (default 1 ms) after the previous flip. `internal/adapters/drm/vrr_flip_gap.go` has the measurements.
1111 - **Transitions.** Swipe landings, scrolling, workspace switches, moving columns and windows, consume and expel, column width and window height, maximize, fullscreen, the floating toggle, drag drops and workspaces sent to another monitor each move by one critically damped spring per moving value, advanced on the flips of the output that shows them. Windows that open, close or leave with the stash fade and scale from 90 %, and the overview zooms its cards; both reuse the per-window alpha and preview zoom of the compose pass, so no transition adds a GPU pass, a blur or a texture copy. A closing window is drawn from its last content, which the renderer already imported; the veil under a closing float goes at once, so the close redraws only the float's rect. A window that fades or zooms is never scanned out directly. Client-driven resizes, panels and hotplug never animate. Clients get one configure, to the final size. During a move or a resize the window frame moves and the content is not scaled, only clipped or padded; the content zooms with the frame only while a window opens, closes or is an overview card. A spring that moves pixels settles as soon as it can no longer leave half a pixel of its target, so its last rendered frame is already the settled one and the sub-pixel tail (about a third of a 100 px move's frames) is not drawn; fades keep their fine epsilon. The frame fallback timer is made once and reset per frame.
1212 - **Redraw only what moved.** An output whose scene is unchanged keeps its scene `Seq` and is not recomposed; a content-only change redraws only the damaged region. When only windows move, the renderer compares the new scene with the one each render target holds and redraws the union of the old and new rects of the windows that changed; anything else (layers, scale, transform, dim, capture) redraws the whole output. Resizing one of three columns at 2560×1600 redraws about 0.2 M pixels per frame instead of 4.1 M. Advancing the springs, the scene comparison (separator lists of the same length are compared index by index, as core emits them in layout order) and the draw list of a moving window cost 0 allocations; publishing the scenes of two outputs with six moving windows (position, fade, dim and zoom motions) costs a pinned 32 on both the page-flip and the fallback-timer paths, 24 with closing windows and 70 while the overview opens. All are guarded by `make perf-check`. Measured cost of a full composed frame at 2560×1600: ~0.16 ms on an RX 9070 XT, ~1.4 ms on a Raphael iGPU, ~3 ms on a Radeon 680M, against an 11.1 ms frame at 90 Hz.
13-- **Compressed render targets.** On GPUs whose display modifiers use several memory planes (AMD DCC), the exported render targets use them when the display accepts one; otherwise NeferWL falls back to a single-plane modifier, then to linear. The chosen modifier and its plane count are logged as `zero-copy output`.
13+- **Compressed render targets.** Render targets are allocated with a modifier the primary plane lists in IN_FORMATS for XRGB8888 (XRGB2101010 in HDR); KMS refuses any other (`ADDFB2: EINVAL`). A multi-plane (AMD DCC) modifier is used only when the plane lists the exact variant the GPU exports; otherwise NeferWL uses a single-plane tiled modifier, then linear. On Radeon 680M and Raphael (GFX10.3) with Mesa 26.2 RADV exports only the pipe-aligned DCC variant (`…56bb03`) while amdgpu lists only the non-pipe-aligned ones (`…563b03`), so the targets are single-plane tiled (`…401b03`): DCC is not used there. The chosen modifier and its plane count are logged as `zero-copy output`.
1414 - **Bounded capture.** Protocol requests and sync-file handles still need small allocations; pixel storage and worker batches are reused. At most 32 protocol requests are outstanding globally, with up to eight per output frame, one SHM-copy worker and two reusable GPU staging slots per output. Saturation fails requests instead of waiting or growing a queue. Two 4K slots use about 63 MiB of host-visible memory, capped at 128 MiB per output; HDR adds one GPU-local 8-bit conversion image (about 32 MiB at 4K, up to 64 MiB), outside the host-memory cap. Display capture resources are allocated on first capture and retained until the output stops. Hidden workspace capture uses a separate, demand-driven child renderer and pipeline with its own worker and two staging slots (up to 128 MiB host-visible memory), plus child render targets; each child image is capped at 256 MiB and 16384 pixels per side. The child retires once unused and idle, and is recreated for session or size changes. Copies cost GPU time and bandwidth. Capture of the displayed frame forces composition, and so does the capture indicator (red border or pill, shown while a session lives and for one second after each captured frame; see `docs/headless.md`), which also costs a second composition per capture frame because no capture holds it (a request waits at most 250 ms for the scene that shows its indicator, without drawing anything while it waits); hidden workspace capture can keep display scanout. Offscreen capture currently serves only a whole-workspace target and requests matching its exact physical dimensions, not crops. There is no per-client reservation of the shared request limit.
1515 - **Default GOGC.** The Go live heap is a few MB (client buffers live in shared and GPU memory), so the runtime default `GOGC=100` with a 256 MiB soft limit gives 2 collections on the tiled playback test where `GOGC=50` gave 13. `GOGC` and `GOMEMLIMIT` from the environment win.
1616
@@ -10,7 +10,7 @@ How NeferWL keeps its CPU and memory use low, how to measure it, and the referen
10 - **Game frames keep a short gap after each VRR flip.** On amdgpu, a flip committed right after the previous flip event can miss the early refresh and wait for the panel's slowest rate (~21 ms at 48 Hz); the next frame then lands at the same point, and the screen stays at 48 Hz while the game renders at 150 fps. Game frames under VRR therefore commit at least `render.vrr-flip-gap` (default 1 ms) after the previous flip. `internal/adapters/drm/vrr_flip_gap.go` has the measurements.10 - **Game frames keep a short gap after each VRR flip.** On amdgpu, a flip committed right after the previous flip event can miss the early refresh and wait for the panel's slowest rate (~21 ms at 48 Hz); the next frame then lands at the same point, and the screen stays at 48 Hz while the game renders at 150 fps. Game frames under VRR therefore commit at least `render.vrr-flip-gap` (default 1 ms) after the previous flip. `internal/adapters/drm/vrr_flip_gap.go` has the measurements.
11 - **Transitions.** Swipe landings, scrolling, workspace switches, moving columns and windows, consume and expel, column width and window height, maximize, fullscreen, the floating toggle, drag drops and workspaces sent to another monitor each move by one critically damped spring per moving value, advanced on the flips of the output that shows them. Windows that open, close or leave with the stash fade and scale from 90 %, and the overview zooms its cards; both reuse the per-window alpha and preview zoom of the compose pass, so no transition adds a GPU pass, a blur or a texture copy. A closing window is drawn from its last content, which the renderer already imported; the veil under a closing float goes at once, so the close redraws only the float's rect. A window that fades or zooms is never scanned out directly. Client-driven resizes, panels and hotplug never animate. Clients get one configure, to the final size. During a move or a resize the window frame moves and the content is not scaled, only clipped or padded; the content zooms with the frame only while a window opens, closes or is an overview card. A spring that moves pixels settles as soon as it can no longer leave half a pixel of its target, so its last rendered frame is already the settled one and the sub-pixel tail (about a third of a 100 px move's frames) is not drawn; fades keep their fine epsilon. The frame fallback timer is made once and reset per frame.11 - **Transitions.** Swipe landings, scrolling, workspace switches, moving columns and windows, consume and expel, column width and window height, maximize, fullscreen, the floating toggle, drag drops and workspaces sent to another monitor each move by one critically damped spring per moving value, advanced on the flips of the output that shows them. Windows that open, close or leave with the stash fade and scale from 90 %, and the overview zooms its cards; both reuse the per-window alpha and preview zoom of the compose pass, so no transition adds a GPU pass, a blur or a texture copy. A closing window is drawn from its last content, which the renderer already imported; the veil under a closing float goes at once, so the close redraws only the float's rect. A window that fades or zooms is never scanned out directly. Client-driven resizes, panels and hotplug never animate. Clients get one configure, to the final size. During a move or a resize the window frame moves and the content is not scaled, only clipped or padded; the content zooms with the frame only while a window opens, closes or is an overview card. A spring that moves pixels settles as soon as it can no longer leave half a pixel of its target, so its last rendered frame is already the settled one and the sub-pixel tail (about a third of a 100 px move's frames) is not drawn; fades keep their fine epsilon. The frame fallback timer is made once and reset per frame.
12 - **Redraw only what moved.** An output whose scene is unchanged keeps its scene `Seq` and is not recomposed; a content-only change redraws only the damaged region. When only windows move, the renderer compares the new scene with the one each render target holds and redraws the union of the old and new rects of the windows that changed; anything else (layers, scale, transform, dim, capture) redraws the whole output. Resizing one of three columns at 2560×1600 redraws about 0.2 M pixels per frame instead of 4.1 M. Advancing the springs, the scene comparison (separator lists of the same length are compared index by index, as core emits them in layout order) and the draw list of a moving window cost 0 allocations; publishing the scenes of two outputs with six moving windows (position, fade, dim and zoom motions) costs a pinned 32 on both the page-flip and the fallback-timer paths, 24 with closing windows and 70 while the overview opens. All are guarded by `make perf-check`. Measured cost of a full composed frame at 2560×1600: ~0.16 ms on an RX 9070 XT, ~1.4 ms on a Raphael iGPU, ~3 ms on a Radeon 680M, against an 11.1 ms frame at 90 Hz.12 - **Redraw only what moved.** An output whose scene is unchanged keeps its scene `Seq` and is not recomposed; a content-only change redraws only the damaged region. When only windows move, the renderer compares the new scene with the one each render target holds and redraws the union of the old and new rects of the windows that changed; anything else (layers, scale, transform, dim, capture) redraws the whole output. Resizing one of three columns at 2560×1600 redraws about 0.2 M pixels per frame instead of 4.1 M. Advancing the springs, the scene comparison (separator lists of the same length are compared index by index, as core emits them in layout order) and the draw list of a moving window cost 0 allocations; publishing the scenes of two outputs with six moving windows (position, fade, dim and zoom motions) costs a pinned 32 on both the page-flip and the fallback-timer paths, 24 with closing windows and 70 while the overview opens. All are guarded by `make perf-check`. Measured cost of a full composed frame at 2560×1600: ~0.16 ms on an RX 9070 XT, ~1.4 ms on a Raphael iGPU, ~3 ms on a Radeon 680M, against an 11.1 ms frame at 90 Hz.
13-- **Compressed render targets.** On GPUs whose display modifiers use several memory planes (AMD DCC), the exported render targets use them when the display accepts one; otherwise NeferWL falls back to a single-plane modifier, then to linear. The chosen modifier and its plane count are logged as `zero-copy output`.13+- **Compressed render targets.** Render targets are allocated with a modifier the primary plane lists in IN_FORMATS for XRGB8888 (XRGB2101010 in HDR); KMS refuses any other (`ADDFB2: EINVAL`). A multi-plane (AMD DCC) modifier is used only when the plane lists the exact variant the GPU exports; otherwise NeferWL uses a single-plane tiled modifier, then linear. On Radeon 680M and Raphael (GFX10.3) with Mesa 26.2 RADV exports only the pipe-aligned DCC variant (`…56bb03`) while amdgpu lists only the non-pipe-aligned ones (`…563b03`), so the targets are single-plane tiled (`…401b03`): DCC is not used there. The chosen modifier and its plane count are logged as `zero-copy output`.
14 - **Bounded capture.** Protocol requests and sync-file handles still need small allocations; pixel storage and worker batches are reused. At most 32 protocol requests are outstanding globally, with up to eight per output frame, one SHM-copy worker and two reusable GPU staging slots per output. Saturation fails requests instead of waiting or growing a queue. Two 4K slots use about 63 MiB of host-visible memory, capped at 128 MiB per output; HDR adds one GPU-local 8-bit conversion image (about 32 MiB at 4K, up to 64 MiB), outside the host-memory cap. Display capture resources are allocated on first capture and retained until the output stops. Hidden workspace capture uses a separate, demand-driven child renderer and pipeline with its own worker and two staging slots (up to 128 MiB host-visible memory), plus child render targets; each child image is capped at 256 MiB and 16384 pixels per side. The child retires once unused and idle, and is recreated for session or size changes. Copies cost GPU time and bandwidth. Capture of the displayed frame forces composition, and so does the capture indicator (red border or pill, shown while a session lives and for one second after each captured frame; see `docs/headless.md`), which also costs a second composition per capture frame because no capture holds it (a request waits at most 250 ms for the scene that shows its indicator, without drawing anything while it waits); hidden workspace capture can keep display scanout. Offscreen capture currently serves only a whole-workspace target and requests matching its exact physical dimensions, not crops. There is no per-client reservation of the shared request limit.14 - **Bounded capture.** Protocol requests and sync-file handles still need small allocations; pixel storage and worker batches are reused. At most 32 protocol requests are outstanding globally, with up to eight per output frame, one SHM-copy worker and two reusable GPU staging slots per output. Saturation fails requests instead of waiting or growing a queue. Two 4K slots use about 63 MiB of host-visible memory, capped at 128 MiB per output; HDR adds one GPU-local 8-bit conversion image (about 32 MiB at 4K, up to 64 MiB), outside the host-memory cap. Display capture resources are allocated on first capture and retained until the output stops. Hidden workspace capture uses a separate, demand-driven child renderer and pipeline with its own worker and two staging slots (up to 128 MiB host-visible memory), plus child render targets; each child image is capped at 256 MiB and 16384 pixels per side. The child retires once unused and idle, and is recreated for session or size changes. Copies cost GPU time and bandwidth. Capture of the displayed frame forces composition, and so does the capture indicator (red border or pill, shown while a session lives and for one second after each captured frame; see `docs/headless.md`), which also costs a second composition per capture frame because no capture holds it (a request waits at most 250 ms for the scene that shows its indicator, without drawing anything while it waits); hidden workspace capture can keep display scanout. Offscreen capture currently serves only a whole-workspace target and requests matching its exact physical dimensions, not crops. There is no per-client reservation of the shared request limit.
15 - **Default GOGC.** The Go live heap is a few MB (client buffers live in shared and GPU memory), so the runtime default `GOGC=100` with a 256 MiB soft limit gives 2 collections on the tiled playback test where `GOGC=50` gave 13. `GOGC` and `GOMEMLIMIT` from the environment win.15 - **Default GOGC.** The Go live heap is a few MB (client buffers live in shared and GPU memory), so the runtime default `GOGC=100` with a 256 MiB soft limit gives 2 collections on the tiled playback test where `GOGC=50` gave 13. `GOGC` and `GOMEMLIMIT` from the environment win.
16 16
M internal/adapters/drm/commit_test.go +66 -0
@@ -203,6 +203,72 @@ func TestShowImagesRetriesSinglePlaneBeforeLinear(t *testing.T) {
203203 }
204204 }
205205
206+// SDR images are asked for the modifiers the primary plane lists for
207+// XRGB8888, in IN_FORMATS order, never the renderer's own choice: RADV's
208+// preferred DCC variant (pipe-aligned) is not one amdgpu lists, and KMS
209+// refuses a framebuffer with a modifier the plane does not advertise
210+// (ADDFB2: EINVAL). Other formats' modifiers are not offered.
211+func TestShowImagesOffersThePrimaryPlanesXRGBModifiers(t *testing.T) {
212+ const dccListed, tiled, other = 0x200000000563b03, 0x200000000401b03, 0x200000000401903
213+ o, k, _ := testOutput(t)
214+ o.cursor = nil
215+ o.primary.formats = []ports.DMABufFormat{
216+ {Format: fourccXRGB, Modifier: dccListed}, {Format: fourccXR30, Modifier: other},
217+ {Format: fourccXRGB, Modifier: tiled}, {Format: fourccXRGB, Modifier: 0},
218+ }
219+ buf := func() ports.DMABuf {
220+ f, w, _ := os.Pipe()
221+ w.Close()
222+ return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
223+ }
224+ r := portsmocks.NewMockRenderer(t)
225+ r.EXPECT().SetHDR(float64(0)).Return().Maybe()
226+ r.EXPECT().ExportTargets(2, []uint64{dccListed, tiled, 0}, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
227+ r.EXPECT().UseTarget(mock.Anything).Return()
228+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
229+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
230+ k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
231+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
232+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
233+ if err := o.showImages(r, imagesDriver, nil); err != nil {
234+ t.Fatal(err)
235+ }
236+ if o.kind != imagesDriver {
237+ t.Fatalf("kind %d", o.kind)
238+ }
239+}
240+
241+// When the renderer has no modifier in common with the plane, the images
242+// fall back to single-plane ones with the same list, then to linear.
243+func TestShowImagesNoCommonModifierFallsBackWithThePlanesList(t *testing.T) {
244+ const listed = 0x200000000563b03
245+ o, k, _ := testOutput(t)
246+ o.cursor = nil
247+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: listed}, {Format: fourccXRGB, Modifier: 0}}
248+ buf := func() ports.DMABuf {
249+ f, w, _ := os.Pipe()
250+ w.Close()
251+ return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
252+ }
253+ r := portsmocks.NewMockRenderer(t)
254+ r.EXPECT().SetHDR(float64(0)).Return().Maybe()
255+ none := errors.New("no XRGB8888 modifier both the device and the display accept")
256+ r.EXPECT().ExportTargets(2, []uint64{listed, 0}, false).Return(nil, none).Once()
257+ r.EXPECT().ExportTargets(2, []uint64{listed, 0}, true).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
258+ r.EXPECT().UseTarget(mock.Anything).Return()
259+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
260+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
261+ k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
262+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
263+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
264+ if err := o.showImages(r, imagesDriver, nil); err != nil {
265+ t.Fatal(err)
266+ }
267+ if o.kind != imagesSinglePlane {
268+ t.Fatalf("kind %d", o.kind)
269+ }
270+}
271+
206272 // Refused driver images that already had one plane are not retried as
207273 // single-plane images: the next export is linear.
208274 func TestShowImagesSkipsSinglePlaneWhenDriverImagesHadOnePlane(t *testing.T) {
@@ -203,6 +203,72 @@ func TestShowImagesRetriesSinglePlaneBeforeLinear(t *testing.T) {
203 }203 }
204 }204 }
205 205
206+// SDR images are asked for the modifiers the primary plane lists for
207+// XRGB8888, in IN_FORMATS order, never the renderer's own choice: RADV's
208+// preferred DCC variant (pipe-aligned) is not one amdgpu lists, and KMS
209+// refuses a framebuffer with a modifier the plane does not advertise
210+// (ADDFB2: EINVAL). Other formats' modifiers are not offered.
211+func TestShowImagesOffersThePrimaryPlanesXRGBModifiers(t *testing.T) {
212+ const dccListed, tiled, other = 0x200000000563b03, 0x200000000401b03, 0x200000000401903
213+ o, k, _ := testOutput(t)
214+ o.cursor = nil
215+ o.primary.formats = []ports.DMABufFormat{
216+ {Format: fourccXRGB, Modifier: dccListed}, {Format: fourccXR30, Modifier: other},
217+ {Format: fourccXRGB, Modifier: tiled}, {Format: fourccXRGB, Modifier: 0},
218+ }
219+ buf := func() ports.DMABuf {
220+ f, w, _ := os.Pipe()
221+ w.Close()
222+ return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
223+ }
224+ r := portsmocks.NewMockRenderer(t)
225+ r.EXPECT().SetHDR(float64(0)).Return().Maybe()
226+ r.EXPECT().ExportTargets(2, []uint64{dccListed, tiled, 0}, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
227+ r.EXPECT().UseTarget(mock.Anything).Return()
228+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
229+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
230+ k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
231+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
232+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
233+ if err := o.showImages(r, imagesDriver, nil); err != nil {
234+ t.Fatal(err)
235+ }
236+ if o.kind != imagesDriver {
237+ t.Fatalf("kind %d", o.kind)
238+ }
239+}
240+
241+// When the renderer has no modifier in common with the plane, the images
242+// fall back to single-plane ones with the same list, then to linear.
243+func TestShowImagesNoCommonModifierFallsBackWithThePlanesList(t *testing.T) {
244+ const listed = 0x200000000563b03
245+ o, k, _ := testOutput(t)
246+ o.cursor = nil
247+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: listed}, {Format: fourccXRGB, Modifier: 0}}
248+ buf := func() ports.DMABuf {
249+ f, w, _ := os.Pipe()
250+ w.Close()
251+ return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
252+ }
253+ r := portsmocks.NewMockRenderer(t)
254+ r.EXPECT().SetHDR(float64(0)).Return().Maybe()
255+ none := errors.New("no XRGB8888 modifier both the device and the display accept")
256+ r.EXPECT().ExportTargets(2, []uint64{listed, 0}, false).Return(nil, none).Once()
257+ r.EXPECT().ExportTargets(2, []uint64{listed, 0}, true).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
258+ r.EXPECT().UseTarget(mock.Anything).Return()
259+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
260+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
261+ k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
262+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
263+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
264+ if err := o.showImages(r, imagesDriver, nil); err != nil {
265+ t.Fatal(err)
266+ }
267+ if o.kind != imagesSinglePlane {
268+ t.Fatalf("kind %d", o.kind)
269+ }
270+}
271+
206 // Refused driver images that already had one plane are not retried as272 // Refused driver images that already had one plane are not retried as
207 // single-plane images: the next export is linear.273 // single-plane images: the next export is linear.
208 func TestShowImagesSkipsSinglePlaneWhenDriverImagesHadOnePlane(t *testing.T) {274 func TestShowImagesSkipsSinglePlaneWhenDriverImagesHadOnePlane(t *testing.T) {
M internal/adapters/drm/images.go +24 -6
@@ -121,13 +121,19 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima
121121 if o.runContext != nil && o.runContext.Err() != nil {
122122 return kind, o.runContext.Err()
123123 }
124- mods := []uint64(nil)
124+ // The renderer is asked for the modifiers the primary plane lists
125+ // for the image format: DRM core refuses (ADDFB2: EINVAL) a
126+ // framebuffer whose format and modifier no plane advertises, and
127+ // the renderer's own preference may be such a one (RADV's
128+ // pipe-aligned DCC, which amdgpu does not list for scanout). SDR
129+ // with no modifier listed for the format leaves the choice to the
130+ // renderer.
131+ format := uint32(fourccXRGB)
132+ if o.hdr.on {
133+ format = fourccXR30
134+ }
135+ mods := o.primaryModifiers(format)
125136 if o.hdr.on {
126- for _, f := range o.primary.formats {
127- if f.Format == fourccXR30 {
128- mods = append(mods, f.Modifier)
129- }
130- }
131137 if len(mods) == 0 {
132138 return kind, fmt.Errorf("primary plane has no XRGB2101010 modifiers")
133139 }
@@ -153,6 +159,18 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima
153159 return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err)
154160 }
155161
162+// primaryModifiers lists the modifiers the primary plane accepts for
163+// format, in IN_FORMATS order.
164+func (o *Output) primaryModifiers(format uint32) []uint64 {
165+ var mods []uint64
166+ for _, f := range o.primary.formats {
167+ if f.Format == format {
168+ mods = append(mods, f.Modifier)
169+ }
170+ }
171+ return mods
172+}
173+
156174 // exportImages makes the renderer's exported targets the output images.
157175 func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error {
158176 bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane)
@@ -121,13 +121,19 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima
121 if o.runContext != nil && o.runContext.Err() != nil {121 if o.runContext != nil && o.runContext.Err() != nil {
122 return kind, o.runContext.Err()122 return kind, o.runContext.Err()
123 }123 }
124- mods := []uint64(nil)124+ // The renderer is asked for the modifiers the primary plane lists
125+ // for the image format: DRM core refuses (ADDFB2: EINVAL) a
126+ // framebuffer whose format and modifier no plane advertises, and
127+ // the renderer's own preference may be such a one (RADV's
128+ // pipe-aligned DCC, which amdgpu does not list for scanout). SDR
129+ // with no modifier listed for the format leaves the choice to the
130+ // renderer.
131+ format := uint32(fourccXRGB)
132+ if o.hdr.on {
133+ format = fourccXR30
134+ }
135+ mods := o.primaryModifiers(format)
125 if o.hdr.on {136 if o.hdr.on {
126- for _, f := range o.primary.formats {
127- if f.Format == fourccXR30 {
128- mods = append(mods, f.Modifier)
129- }
130- }
131 if len(mods) == 0 {137 if len(mods) == 0 {
132 return kind, fmt.Errorf("primary plane has no XRGB2101010 modifiers")138 return kind, fmt.Errorf("primary plane has no XRGB2101010 modifiers")
133 }139 }
@@ -153,6 +159,18 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima
153 return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err)159 return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err)
154 }160 }
155 161
162+// primaryModifiers lists the modifiers the primary plane accepts for
163+// format, in IN_FORMATS order.
164+func (o *Output) primaryModifiers(format uint32) []uint64 {
165+ var mods []uint64
166+ for _, f := range o.primary.formats {
167+ if f.Format == format {
168+ mods = append(mods, f.Modifier)
169+ }
170+ }
171+ return mods
172+}
173+
156 // exportImages makes the renderer's exported targets the output images.174 // exportImages makes the renderer's exported targets the output images.
157 func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error {175 func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error {
158 bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane)176 bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane)
M internal/adapters/vulkan/targets_test.go +35 -0
@@ -281,3 +281,38 @@ func TestExportTargetsPlanes(t *testing.T) {
281281 }
282282 }
283283 }
284+
285+// A target exported for a display modifier list carries one of its
286+// modifiers, whichever they are: KMS refuses a framebuffer whose modifier
287+// the plane does not advertise, so the intersection must hold on the real
288+// driver (RADV also exports DCC variants displays do not list).
289+func TestExportTargetsStayInTheDisplayList(t *testing.T) {
290+ r, err := New(64, 32)
291+ if err != nil {
292+ t.Skipf("Vulkan unavailable: %v", err)
293+ }
294+ defer r.Close()
295+ if len(r.renderMods) == 0 {
296+ t.Skip("device exports no render modifier")
297+ }
298+ for _, m := range r.renderMods {
299+ bufs, err := r.ExportTargets(1, []uint64{m}, false)
300+ if err != nil {
301+ t.Fatalf("modifier %#x is exportable but export failed: %v", m, err)
302+ }
303+ got := bufs[0].Modifier
304+ for _, p := range bufs[0].Planes {
305+ p.File.Close()
306+ }
307+ if got != m {
308+ t.Fatalf("display lists only %#x, target has modifier %#x", m, got)
309+ }
310+ }
311+ // A list with no exportable modifier is an error, not a guess.
312+ if bufs, err := r.ExportTargets(1, []uint64{0x1234567}, false); err == nil {
313+ for _, p := range bufs[0].Planes {
314+ p.File.Close()
315+ }
316+ t.Fatalf("export with an unexportable display list gave modifier %#x", bufs[0].Modifier)
317+ }
318+}
@@ -281,3 +281,38 @@ func TestExportTargetsPlanes(t *testing.T) {
281 }281 }
282 }282 }
283 }283 }
284+
285+// A target exported for a display modifier list carries one of its
286+// modifiers, whichever they are: KMS refuses a framebuffer whose modifier
287+// the plane does not advertise, so the intersection must hold on the real
288+// driver (RADV also exports DCC variants displays do not list).
289+func TestExportTargetsStayInTheDisplayList(t *testing.T) {
290+ r, err := New(64, 32)
291+ if err != nil {
292+ t.Skipf("Vulkan unavailable: %v", err)
293+ }
294+ defer r.Close()
295+ if len(r.renderMods) == 0 {
296+ t.Skip("device exports no render modifier")
297+ }
298+ for _, m := range r.renderMods {
299+ bufs, err := r.ExportTargets(1, []uint64{m}, false)
300+ if err != nil {
301+ t.Fatalf("modifier %#x is exportable but export failed: %v", m, err)
302+ }
303+ got := bufs[0].Modifier
304+ for _, p := range bufs[0].Planes {
305+ p.File.Close()
306+ }
307+ if got != m {
308+ t.Fatalf("display lists only %#x, target has modifier %#x", m, got)
309+ }
310+ }
311+ // A list with no exportable modifier is an error, not a guess.
312+ if bufs, err := r.ExportTargets(1, []uint64{0x1234567}, false); err == nil {
313+ for _, p := range bufs[0].Planes {
314+ p.File.Close()
315+ }
316+ t.Fatalf("export with an unexportable display list gave modifier %#x", bufs[0].Modifier)
317+ }
318+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close