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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 10 | 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 | 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 | 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 | 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 | 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 | ||
| @@ -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) { | |||
| 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 | 272 | // Refused driver images that already had one plane are not retried as | |
| 207 | 273 | // single-plane images: the next export is linear. | |
| 208 | 274 | 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 as | 272 | // 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 | |||
| 121 | 121 | if o.runContext != nil && o.runContext.Err() != nil { | |
| 122 | 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 | 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 | 137 | if len(mods) == 0 { | |
| 132 | 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 | 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 | 174 | // exportImages makes the renderer's exported targets the output images. | |
| 157 | 175 | func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error { | |
| 158 | 176 | 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) { | |||
| 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 | + | } | |
| @@ -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 | + | } | |||