detached 19a1e20

perf(vulkan): never export targets with the AMD DCC retile modifier

RADV renders a DCC_RETILE image into pipe-aligned DCC and runs a compute pass to rewrite the displayable copy: an extra full-surface GPU pass per frame. Drop those modifiers from the probed target list; plain DCC stays.
commit
19a1e20951d1da05eb1958c829d38d8d4a1faa41
author
brice · · 2026-10-05 04:19 UTC
committer
brice · · 2026-10-05 04:19 UTC
parent
cd27fce
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +80 -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** (experimental, `animations = on`; off by default). 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 6 on both the page-flip and the fallback-timer paths, 4 with closing windows and 18 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.** 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`.
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. AMD DCC modifiers with the retile bit (`DCC_RETILE`, bit 14 of the modifier, e.g. `…567b03`, which RADV picks on the Radeon 680M) are never used: such an image is rendered into pipe-aligned DCC and a compute pass rewrites its displayable copy, a GPU pass on top of the composition. Plain DCC costs no such pass: at 2560×1600 a full composed frame takes about 1.63 ms into pipe-aligned DCC (`…56bb03`) against 1.85 ms into single-plane tiled (`…401b03`) and 3.0 ms into linear on a Raphael iGPU (`go test ./internal/adapters/vulkan -bench ComposeFullTarget`, `NEFERWL_VK_DEVICE` picks the GPU; it times one frame per modifier the device exports, GPU wait included). Where the retile variant is the only DCC the GPU exports, the targets are single-plane tiled. 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** (experimental, `animations = on`; off by default). 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** (experimental, `animations = on`; off by default). 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 6 on both the page-flip and the fallback-timer paths, 4 with closing windows and 18 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 6 on both the page-flip and the fallback-timer paths, 4 with closing windows and 18 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.** 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`.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. AMD DCC modifiers with the retile bit (`DCC_RETILE`, bit 14 of the modifier, e.g. `…567b03`, which RADV picks on the Radeon 680M) are never used: such an image is rendered into pipe-aligned DCC and a compute pass rewrites its displayable copy, a GPU pass on top of the composition. Plain DCC costs no such pass: at 2560×1600 a full composed frame takes about 1.63 ms into pipe-aligned DCC (`…56bb03`) against 1.85 ms into single-plane tiled (`…401b03`) and 3.0 ms into linear on a Raphael iGPU (`go test ./internal/adapters/vulkan -bench ComposeFullTarget`, `NEFERWL_VK_DEVICE` picks the GPU; it times one frame per modifier the device exports, GPU wait included). Where the retile variant is the only DCC the GPU exports, the targets are single-plane tiled. 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/images.go +5 -2
@@ -134,8 +134,11 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima
134134 // pipe-aligned DCC, which amdgpu does not list for scanout). A
135135 // plane without IN_FORMATS lists every format with modifier 0
136136 // (readPlanes), so it gets linear images straight away rather than
137- // two refused driver attempts. Only a plane listing no modifier
138- // at all for the format (SDR) leaves the choice to the renderer.
137+ // two refused driver attempts. The renderer never picks an AMD
138+ // DCC retile modifier, whatever the plane lists: it costs an extra
139+ // GPU pass per frame (vulkan retilesDCC). Only a plane listing no
140+ // modifier at all for the format (SDR) leaves the choice to the
141+ // renderer.
139142 mods := o.primaryModifiers(o.imageFormat())
140143 if o.hdr.on {
141144 if len(mods) == 0 {
@@ -134,8 +134,11 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima
134 // pipe-aligned DCC, which amdgpu does not list for scanout). A134 // pipe-aligned DCC, which amdgpu does not list for scanout). A
135 // plane without IN_FORMATS lists every format with modifier 0135 // plane without IN_FORMATS lists every format with modifier 0
136 // (readPlanes), so it gets linear images straight away rather than136 // (readPlanes), so it gets linear images straight away rather than
137- // two refused driver attempts. Only a plane listing no modifier137+ // two refused driver attempts. The renderer never picks an AMD
138- // at all for the format (SDR) leaves the choice to the renderer.138+ // DCC retile modifier, whatever the plane lists: it costs an extra
139+ // GPU pass per frame (vulkan retilesDCC). Only a plane listing no
140+ // modifier at all for the format (SDR) leaves the choice to the
141+ // renderer.
139 mods := o.primaryModifiers(o.imageFormat())142 mods := o.primaryModifiers(o.imageFormat())
140 if o.hdr.on {143 if o.hdr.on {
141 if len(mods) == 0 {144 if len(mods) == 0 {
M internal/adapters/vulkan/targets.go +23 -4
@@ -147,7 +147,8 @@ func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {
147147 }
148148
149149 // probeModifiers lists the exportable modifiers of format (one to four memory
150-// planes: DCC adds metadata planes) that support the target usage, and
150+// planes: DCC adds metadata planes; DCC retile modifiers are left out, see
151+// retilesDCC) that support the target usage, and
151152 // records their plane counts in modPlanes. HDR targets need transfer-src only when
152153 // hdrReadback is set (GPU tests, headless --screenshot-raw; never DRM).
153154 func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 {
@@ -181,14 +182,32 @@ func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format)
181182 // maxModifierPlanes is the DRM limit of planes in a framebuffer.
182183 const maxModifierPlanes = 4
183184
185+// AMD format modifiers (drm_fourcc.h): the vendor is in bits 56-63, and a DCC
186+// modifier with the retile bit keeps a second, display-aligned copy of the
187+// DCC metadata.
188+const (
189+ modVendorShift = 56
190+ modVendorAMD = 0x02
191+ amdModRetile = 1 << 14 // AMD_FMT_MOD_DCC_RETILE
192+)
193+
194+// retilesDCC reports whether m is an AMD DCC modifier with the retile bit.
195+// RADV renders such an image into pipe-aligned DCC and runs a compute pass
196+// that rewrites the display copy: an extra full-surface GPU pass per frame,
197+// so these modifiers are never offered for targets. Plain DCC (pipe-aligned
198+// or not) costs no such pass.
199+func retilesDCC(m uint64) bool {
200+ return m>>modVendorShift == modVendorAMD && m&amdModRetile != 0
201+}
202+
184203 // filterModifiers keeps the modifiers with 1..4 memory planes whose tiling
185-// features include need and that pass exportable, and returns each kept
186-// modifier's plane count.
204+// features include need, that are not DCC retile modifiers and that pass
205+// exportable, and returns each kept modifier's plane count.
187206 func filterModifiers(mods []vk.DrmFormatModifierPropertiesEXT, need vk.FormatFeatureFlags, exportable func(uint64) bool) ([]uint64, map[uint64]uint32) {
188207 var result []uint64
189208 planes := make(map[uint64]uint32, len(mods))
190209 for _, m := range mods {
191- if m.DrmFormatModifierPlaneCount < 1 || m.DrmFormatModifierPlaneCount > maxModifierPlanes || m.DrmFormatModifierTilingFeatures&need != need || !exportable(m.DrmFormatModifier) {
210+ if m.DrmFormatModifierPlaneCount < 1 || m.DrmFormatModifierPlaneCount > maxModifierPlanes || m.DrmFormatModifierTilingFeatures&need != need || retilesDCC(m.DrmFormatModifier) || !exportable(m.DrmFormatModifier) {
192211 continue
193212 }
194213 result = append(result, m.DrmFormatModifier)
@@ -147,7 +147,8 @@ func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {
147 }147 }
148 148
149 // probeModifiers lists the exportable modifiers of format (one to four memory149 // probeModifiers lists the exportable modifiers of format (one to four memory
150-// planes: DCC adds metadata planes) that support the target usage, and150+// planes: DCC adds metadata planes; DCC retile modifiers are left out, see
151+// retilesDCC) that support the target usage, and
151 // records their plane counts in modPlanes. HDR targets need transfer-src only when152 // records their plane counts in modPlanes. HDR targets need transfer-src only when
152 // hdrReadback is set (GPU tests, headless --screenshot-raw; never DRM).153 // hdrReadback is set (GPU tests, headless --screenshot-raw; never DRM).
153 func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 {154 func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 {
@@ -181,14 +182,32 @@ func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format)
181 // maxModifierPlanes is the DRM limit of planes in a framebuffer.182 // maxModifierPlanes is the DRM limit of planes in a framebuffer.
182 const maxModifierPlanes = 4183 const maxModifierPlanes = 4
183 184
185+// AMD format modifiers (drm_fourcc.h): the vendor is in bits 56-63, and a DCC
186+// modifier with the retile bit keeps a second, display-aligned copy of the
187+// DCC metadata.
188+const (
189+ modVendorShift = 56
190+ modVendorAMD = 0x02
191+ amdModRetile = 1 << 14 // AMD_FMT_MOD_DCC_RETILE
192+)
193+
194+// retilesDCC reports whether m is an AMD DCC modifier with the retile bit.
195+// RADV renders such an image into pipe-aligned DCC and runs a compute pass
196+// that rewrites the display copy: an extra full-surface GPU pass per frame,
197+// so these modifiers are never offered for targets. Plain DCC (pipe-aligned
198+// or not) costs no such pass.
199+func retilesDCC(m uint64) bool {
200+ return m>>modVendorShift == modVendorAMD && m&amdModRetile != 0
201+}
202+
184 // filterModifiers keeps the modifiers with 1..4 memory planes whose tiling203 // filterModifiers keeps the modifiers with 1..4 memory planes whose tiling
185-// features include need and that pass exportable, and returns each kept204+// features include need, that are not DCC retile modifiers and that pass
186-// modifier's plane count.205+// exportable, and returns each kept modifier's plane count.
187 func filterModifiers(mods []vk.DrmFormatModifierPropertiesEXT, need vk.FormatFeatureFlags, exportable func(uint64) bool) ([]uint64, map[uint64]uint32) {206 func filterModifiers(mods []vk.DrmFormatModifierPropertiesEXT, need vk.FormatFeatureFlags, exportable func(uint64) bool) ([]uint64, map[uint64]uint32) {
188 var result []uint64207 var result []uint64
189 planes := make(map[uint64]uint32, len(mods))208 planes := make(map[uint64]uint32, len(mods))
190 for _, m := range mods {209 for _, m := range mods {
191- if m.DrmFormatModifierPlaneCount < 1 || m.DrmFormatModifierPlaneCount > maxModifierPlanes || m.DrmFormatModifierTilingFeatures&need != need || !exportable(m.DrmFormatModifier) {210+ if m.DrmFormatModifierPlaneCount < 1 || m.DrmFormatModifierPlaneCount > maxModifierPlanes || m.DrmFormatModifierTilingFeatures&need != need || retilesDCC(m.DrmFormatModifier) || !exportable(m.DrmFormatModifier) {
192 continue211 continue
193 }212 }
194 result = append(result, m.DrmFormatModifier)213 result = append(result, m.DrmFormatModifier)
M internal/adapters/vulkan/targets_test.go +51 -0
@@ -316,3 +316,54 @@ func TestExportTargetsStayInTheDisplayList(t *testing.T) {
316316 t.Fatalf("export with an unexportable display list gave modifier %#x", bufs[0].Modifier)
317317 }
318318 }
319+
320+// The AMD DCC retile modifiers cost a compute pass per frame: they are never
321+// targets, whatever the display lists. Plain DCC (pipe-aligned or not), the
322+// non-DCC tiled modifier and non-AMD modifiers with bit 14 set stay.
323+func TestFilterModifiersDropsDCCRetile(t *testing.T) {
324+ const (
325+ retile = 0x200000010567b03 // AMD DCC, DCC_RETILE (laptop RADV)
326+ pipeAligned = 0x20000000056bb03 // AMD DCC, pipe-aligned
327+ unaligned = 0x200000000563b03 // AMD DCC, not pipe-aligned
328+ tiled = 0x200000010401b03 // AMD tiled, no DCC
329+ foreignRetil = 0x0100000000004003 // vendor 1, bit 14 set: not AMD's
330+ )
331+ need := vk.FormatFeatureFlags(vk.FormatFeatureColorAttachmentBit)
332+ var mods []vk.DrmFormatModifierPropertiesEXT
333+ for _, m := range []uint64{retile, pipeAligned, unaligned, tiled, foreignRetil, 0} {
334+ planes := uint32(1)
335+ if m == retile || m == pipeAligned || m == unaligned {
336+ planes = 3
337+ }
338+ mods = append(mods, vk.DrmFormatModifierPropertiesEXT{DrmFormatModifier: m, DrmFormatModifierPlaneCount: planes, DrmFormatModifierTilingFeatures: need})
339+ }
340+ got, planes := filterModifiers(mods, need, func(uint64) bool { return true })
341+ if want := []uint64{pipeAligned, unaligned, tiled, foreignRetil, 0}; !slices.Equal(got, want) {
342+ t.Fatalf("kept %#x, want %#x", got, want)
343+ }
344+ if _, ok := planes[retile]; ok {
345+ t.Fatalf("retile modifier has a plane count: %v", planes)
346+ }
347+ r := &Renderer{renderMods: got, modPlanes: planes}
348+ if out := r.exportModifiers([]uint64{retile, tiled}, false); !slices.Equal(out, []uint64{tiled}) {
349+ t.Fatalf("display listing the retile modifier: %#x", out)
350+ }
351+ if out := r.exportModifiers([]uint64{retile}, false); len(out) != 0 {
352+ t.Fatalf("display listing only the retile modifier: %#x", out)
353+ }
354+}
355+
356+// The device's exportable modifiers hold no AMD DCC retile one, whatever
357+// RADV lists (it lists them on GPUs with several render backends).
358+func TestProbedModifiersHaveNoDCCRetile(t *testing.T) {
359+ r, err := New(64, 32)
360+ if err != nil {
361+ t.Skipf("Vulkan unavailable: %v", err)
362+ }
363+ defer r.Close()
364+ for _, m := range append(slices.Clone(r.renderMods), r.hdrMods...) {
365+ if retilesDCC(m) {
366+ t.Fatalf("target modifier %#x retiles DCC", m)
367+ }
368+ }
369+}
@@ -316,3 +316,54 @@ func TestExportTargetsStayInTheDisplayList(t *testing.T) {
316 t.Fatalf("export with an unexportable display list gave modifier %#x", bufs[0].Modifier)316 t.Fatalf("export with an unexportable display list gave modifier %#x", bufs[0].Modifier)
317 }317 }
318 }318 }
319+
320+// The AMD DCC retile modifiers cost a compute pass per frame: they are never
321+// targets, whatever the display lists. Plain DCC (pipe-aligned or not), the
322+// non-DCC tiled modifier and non-AMD modifiers with bit 14 set stay.
323+func TestFilterModifiersDropsDCCRetile(t *testing.T) {
324+ const (
325+ retile = 0x200000010567b03 // AMD DCC, DCC_RETILE (laptop RADV)
326+ pipeAligned = 0x20000000056bb03 // AMD DCC, pipe-aligned
327+ unaligned = 0x200000000563b03 // AMD DCC, not pipe-aligned
328+ tiled = 0x200000010401b03 // AMD tiled, no DCC
329+ foreignRetil = 0x0100000000004003 // vendor 1, bit 14 set: not AMD's
330+ )
331+ need := vk.FormatFeatureFlags(vk.FormatFeatureColorAttachmentBit)
332+ var mods []vk.DrmFormatModifierPropertiesEXT
333+ for _, m := range []uint64{retile, pipeAligned, unaligned, tiled, foreignRetil, 0} {
334+ planes := uint32(1)
335+ if m == retile || m == pipeAligned || m == unaligned {
336+ planes = 3
337+ }
338+ mods = append(mods, vk.DrmFormatModifierPropertiesEXT{DrmFormatModifier: m, DrmFormatModifierPlaneCount: planes, DrmFormatModifierTilingFeatures: need})
339+ }
340+ got, planes := filterModifiers(mods, need, func(uint64) bool { return true })
341+ if want := []uint64{pipeAligned, unaligned, tiled, foreignRetil, 0}; !slices.Equal(got, want) {
342+ t.Fatalf("kept %#x, want %#x", got, want)
343+ }
344+ if _, ok := planes[retile]; ok {
345+ t.Fatalf("retile modifier has a plane count: %v", planes)
346+ }
347+ r := &Renderer{renderMods: got, modPlanes: planes}
348+ if out := r.exportModifiers([]uint64{retile, tiled}, false); !slices.Equal(out, []uint64{tiled}) {
349+ t.Fatalf("display listing the retile modifier: %#x", out)
350+ }
351+ if out := r.exportModifiers([]uint64{retile}, false); len(out) != 0 {
352+ t.Fatalf("display listing only the retile modifier: %#x", out)
353+ }
354+}
355+
356+// The device's exportable modifiers hold no AMD DCC retile one, whatever
357+// RADV lists (it lists them on GPUs with several render backends).
358+func TestProbedModifiersHaveNoDCCRetile(t *testing.T) {
359+ r, err := New(64, 32)
360+ if err != nil {
361+ t.Skipf("Vulkan unavailable: %v", err)
362+ }
363+ defer r.Close()
364+ for _, m := range append(slices.Clone(r.renderMods), r.hdrMods...) {
365+ if retilesDCC(m) {
366+ t.Fatalf("target modifier %#x retiles DCC", m)
367+ }
368+ }
369+}

Keyboard shortcuts

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

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

Branches 25

Tags 10

↑↓ moveenter openesc close