From 6b90842359453a57a3b590cf3dd666c43a3cf803 Mon Sep 17 00:00:00 2001 From: Jonathan Goyvaerts Date: Mon, 2 Nov 2026 14:54:08 -0300 Subject: [PATCH 1/7] drm/asahi: g17: program the tiler's segment limits on 25G83 The TA arrays wrote 0 to 0x1c890, 0x1c918 and 0x1c9a8, upstream's tvb_tiling_control, tiling_control_2 and min_free_tvb_pages. macOS writes 0x200440, 4 and 0x3c on two-cluster parts, all three for every pass. Partial renders need 0x1c9a8. Without it the pages each fragment pass returns shrink geometrically, 122, 66, 37, 24, 13, 7, to none, and the tiler spins through about 140,000 empty stops until the render is killed. With it segments hold at 84-88 pages and the render finishes. 0x1c890 and 0x1c918 alone don't do that, and macOS writes them together. Signed-off-by: Jonathan Goyvaerts Assisted-by: LLM --- drivers/gpu/drm/asahi/g17/fw/render.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/render.rs b/drivers/gpu/drm/asahi/g17/fw/render.rs index a6bc1704d1418..3ab33304bc8a8 100644 --- a/drivers/gpu/drm/asahi/g17/fw/render.rs +++ b/drivers/gpu/drm/asahi/g17/fw/render.rs @@ -639,6 +639,13 @@ fn ta_registers( /// The 25G83 tiling array: the A18 Pro's without 0x1748 and 0x1740 (and 0x17d9 unless the stage /// has SCS), with the tiler parameter sub-buffer registers and 0x10131 = 0x88, in the order the /// M4 Pro has run with. Returns the array and its used entry count. +/// Tiler segment control of a two-cluster pass, what macOS writes. +const VDM_SEGMENT_CONTROL: u64 = 0x20_0440; +const VDM_SEGMENT_INDEX: u64 = 4; +/// Pages the tiler keeps out of a partial render's segments: without them the pages a pass +/// returns shrink to none and the tiler can't advance. Fifteen blocks, macOS's minimum heap. +const MIN_FREE_TVB_PAGES: u64 = 0x3c; + fn ta_registers_25g83( args: &RenderArgs<'_>, g: &Geometry, @@ -686,15 +693,15 @@ fn ta_registers_25g83( push(find(0x1c880, 0)?); push(RegisterWrite::new(0x1c948, gtp(4)?)); push(RegisterWrite::new(0x1c888, gtp(5)?)); - push(RegisterWrite::new(0x1c890, 0)); - push(RegisterWrite::new(0x1c918, 0)); + push(RegisterWrite::new(0x1c890, VDM_SEGMENT_CONTROL)); + push(RegisterWrite::new(0x1c918, VDM_SEGMENT_INDEX)); push(find(0x1c079, 0)?); push(find(0x1c9d8, 0)?); push(RegisterWrite::new(0x1c089, gtp2)); push(RegisterWrite::new(0x1c9e0, gtp2)); push(RegisterWrite::new(0x16c41, gtp6)); push(RegisterWrite::new(0x1ca40, gtp6)); - push(RegisterWrite::new(0x1c9a8, 0)); + push(RegisterWrite::new(0x1c9a8, MIN_FREE_TVB_PAGES)); push(RegisterWrite::new(0x1c920, gtp(7)?)); for reg in [0x10151, 0x1c199, 0x1c1a1, 0x1c1a9, 0x1c1b1, 0x1c1b9, 0x1c8f8, 0x1c0b1, 0x1c850] { push(find(reg, 0)?); From 8efa1323092ae140b5d517ecebe8657ce49d8166 Mon Sep 17 00:00:00 2001 From: Jonathan Goyvaerts Date: Mon, 2 Nov 2026 14:54:08 -0300 Subject: [PATCH 2/7] drm/asahi: g17: double the render pool so a partial render can't starve The render pool holds the USC private memory of a context's vertex and fragment threads. It is fixed at 22 MiB. When the tiler stops for a partial render its threads keep what they hold, and the fragment pass needs its own. At 22 blocks fragment pass 2 stalls for 2.2 ms when it is lucky, and otherwise never finishes and the firmware recovers with reason 0. With process_empty_tiles, pass 1 hangs the same way on its first thread. On a 3456x2234 render of 3M triangles, 22 blocks lost every run with the first stop at 255 pages. 27 blocks passed 12 of 12, heavy shaders included. Clearing the pool registers instead takes the 2.2 ms off pass 2, but then the tiler gets no pages back. 44 is measured, not derived: it leaves margin over the 27 that passed, for shaders heavier than the test's. It costs 22 MiB per rendering context in the 2 GiB lower window, on A18 Pro as well, which shares this constant. Refused allocations in a dEQP run at 8 jobs went from 0 to 15, all retried. Signed-off-by: Jonathan Goyvaerts Assisted-by: LLM --- drivers/gpu/drm/asahi/g17/fw/freelist.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/freelist.rs b/drivers/gpu/drm/asahi/g17/fw/freelist.rs index 2e5708dddd455..86fdc5db75a8f 100644 --- a/drivers/gpu/drm/asahi/g17/fw/freelist.rs +++ b/drivers/gpu/drm/asahi/g17/fw/freelist.rs @@ -9,7 +9,7 @@ pub(crate) const PAGE_LIST_SIZE: usize = 0x20_0000; pub(crate) const RUN_LIST_SIZE: usize = 0x1_0000; pub(crate) const TABLE_SIZE: usize = PAGE_LIST_SIZE + RUN_LIST_SIZE; pub(crate) const CONTROL_SIZE: usize = 0x4000; -pub(crate) const RENDER_BLOCKS: usize = 22; +pub(crate) const RENDER_BLOCKS: usize = 44; pub(crate) const COMPUTE_BLOCKS: usize = 21; /// Render reserves eight run words per block; compute packs runs consecutively. From 07728dcdf2db8fc6039ea25696a21f3667670dee Mon Sep 17 00:00:00 2001 From: Jonathan Goyvaerts Date: Mon, 2 Nov 2026 14:54:08 -0300 Subject: [PATCH 3/7] drm/asahi: g17: make room for the tiler's partial-render records The hardware writes a record per tile-map slot into gtp6 during a partial render, and 0x600 bytes is smaller than macOS's 0x1c00. Size it for the largest pool over all 64 slots. That is a superset of what macOS uses, not a layout claim, and it is the size every partial-render test ran with. Signed-off-by: Jonathan Goyvaerts Assisted-by: LLM --- drivers/gpu/drm/asahi/g17/buffer/target.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/buffer/target.rs b/drivers/gpu/drm/asahi/g17/buffer/target.rs index 42a2c04b40112..bd96ecc9d460e 100644 --- a/drivers/gpu/drm/asahi/g17/buffer/target.rs +++ b/drivers/gpu/drm/asahi/g17/buffer/target.rs @@ -29,7 +29,12 @@ fn aligned(value: usize, alignment: usize) -> Result { /// the default parameter-buffer sizes. Sizes are /// [TPC, tile map × 64, 0x4000 for layered targets, 0, 0xc00, 0xa00, 0x600, 0x80]. const GTP_ALIGNMENTS: [usize; 8] = [0x40, 0x40, 0x100, 1, 0x20, 0x20, 0x80, 0x40]; -const GTP_FIXED_SIZES: [usize; 4] = [0xc00, 0xa00, 0x600, 0x80]; +const GTP_FIXED_SIZES: [usize; 4] = [0xc00, 0xa00, GTP6_SIZE, 0x80]; +/// Room for the tiler's records across a partial render. macOS uses 0x1c00, so this is a superset, +/// and the size every partial-render test ran with. +const GTP_SLOTS: usize = 64; +const GTP6_SIZE: usize = GTP_SLOTS + * (((cfg::TVB_MAX_BLOCKS * cfg::TVB_BLOCK_SIZE).div_ceil(0x8000) + 0x17ff) / 24 * 8 + 8); /// Per-command geometry, independent of the capacity already allocated. #[derive(Copy, Clone, Debug)] From 02bc0af9cc6ffa1ba03d5c4c288ff89d73ff4f83 Mon Sep 17 00:00:00 2001 From: Jonathan Goyvaerts Date: Mon, 2 Nov 2026 14:54:08 -0300 Subject: [PATCH 4/7] drm/asahi: g17: write partial renders' depth and stencil words as macOS does Every array of a partial render wrote the main control word with the load and store bits of each plane set. A pass that clears its depth then loaded it in its first segment, instead of starting from the clear. macOS leaves the first segment's loads as the pass set them, stores each plane, and adds the loads for the reload arrays: 0x80045, then 0x88045. Keep the main word's loads in the first segment. A pass that loads its depth or stencil needs them there: an overflow of a grown pool with more than a few draws gives that segment real work, and a pass without them stores background depth over the real values. A colour pass at EQUAL after such a pass drew nothing at all in a test scene, and Counter-Strike 2's soundpack preview, with its depth pass overflowing twice a frame, had up to 2,937 of 5,400 tiles black. With the loads kept the scene matches a render that can't overflow byte for byte, and the preview had no black tile in 48 captures. Signed-off-by: Jonathan Goyvaerts Assisted-by: LLM --- drivers/gpu/drm/asahi/g17/fw/render.rs | 37 +++++++++++++++++++++----- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/render.rs b/drivers/gpu/drm/asahi/g17/fw/render.rs index 3ab33304bc8a8..825b4f9e50d4e 100644 --- a/drivers/gpu/drm/asahi/g17/fw/render.rs +++ b/drivers/gpu/drm/asahi/g17/fw/render.rs @@ -162,8 +162,14 @@ const UTILE_CONFIG_HEIGHT_SHIFT: u32 = 14; const UTILE_CONFIG_SIZE_MASK: u64 = 0xf000; /// The partial-render phases load and store every attached depth and stencil /// surface. -const ZLS_PARTIAL_DEPTH: u64 = 1 << 15 | 1 << 19; -const ZLS_PARTIAL_STENCIL: u64 = 1 << 14 | 1 << 18; +const ZLS_STORE_DEPTH: u64 = 1 << 19; +const ZLS_STORE_STENCIL: u64 = 1 << 18; +const ZLS_LOAD_DEPTH: u64 = 1 << 15; +const ZLS_LOAD_STENCIL: u64 = 1 << 14; +const ZLS_COMPRESS_DEPTH: u64 = 1 << 6; +const ZLS_COMPRESS_STENCIL: u64 = 1 << 8; +/// Bits of the main word a partial render sets for itself. +const ZLS_PARTIAL_CLEAR: u64 = ZLS_COMPRESS_DEPTH | ZLS_COMPRESS_STENCIL | 0xf << 56; const STENCIL_CLEAR_BASE: u32 = 0x300; /// Value of the ZLS swizzle registers and descriptor field. const ZLS_SWIZZLE: u32 = 0x0404_0404; @@ -443,6 +449,7 @@ struct PassWords { layer_mode: u64, tib_blocks: u64, partial_zls_control: u64, + partial_zls_reload: u64, aux_fb_flags: u64, bg_rsrc_spec: u64, bg_va: u64, @@ -478,12 +485,27 @@ impl PassWords { tile_config |= TILE_CONFIG_PROCESS_EMPTY_TILES; } - let mut partial_zls_control = pass.zls_control; + // The partial store keeps the main word, loads included, and stores and compresses each + // plane it has. The reload arrays add the loads, which a clearing pass doesn't have. + let mut partial_zls_control = pass.zls_control & !ZLS_PARTIAL_CLEAR; if pass.depth.va != 0 { - partial_zls_control |= ZLS_PARTIAL_DEPTH; + partial_zls_control |= ZLS_STORE_DEPTH; + } + if pass.depth.compression_va != 0 { + partial_zls_control |= ZLS_COMPRESS_DEPTH; + } + if pass.stencil.va != 0 { + partial_zls_control |= ZLS_STORE_STENCIL; + } + if pass.stencil.compression_va != 0 { + partial_zls_control |= ZLS_COMPRESS_STENCIL; + } + let mut partial_zls_reload = partial_zls_control; + if pass.depth.va != 0 { + partial_zls_reload |= ZLS_LOAD_DEPTH; } if pass.stencil.va != 0 { - partial_zls_control |= ZLS_PARTIAL_STENCIL; + partial_zls_reload |= ZLS_LOAD_STENCIL; } let mut aux_fb_flags = AUX_FB_FLAGS_BASE; @@ -511,6 +533,7 @@ impl PassWords { * samples) .div_ceil(TIB_BLOCK_SIZE), partial_zls_control, + partial_zls_reload, aux_fb_flags, bg_rsrc_spec: rsrc_spec(pass.bg.rsrc_spec), bg_va: usc(pass.bg.offset)?, @@ -937,7 +960,7 @@ fn partial_resume_registers( RegisterWrite::new(0x153c1, p.depth.compression_va), RegisterWrite::new(0x15339, p.stencil.va), RegisterWrite::new(0x153d1, p.stencil.compression_va), - RegisterWrite::new(0x15319, w.partial_zls_control), + RegisterWrite::new(0x15319, w.partial_zls_reload), RegisterWrite::new(0x15349, u64::from(ZLS_SWIZZLE)), ] } @@ -955,7 +978,7 @@ fn partial_load_registers( RegisterWrite::new(0x153c1, p.depth.compression_va), RegisterWrite::new(0x15339, p.stencil.va), RegisterWrite::new(0x153d1, p.stencil.compression_va), - RegisterWrite::new(0x15319, w.partial_zls_control), + RegisterWrite::new(0x15319, w.partial_zls_reload), RegisterWrite::new(0x15349, u64::from(ZLS_SWIZZLE)), ] } From fecc27c0075b51992d3349c9b480b1d2ecab91eb Mon Sep 17 00:00:00 2001 From: Jonathan Goyvaerts Date: Mon, 2 Nov 2026 14:54:08 -0300 Subject: [PATCH 5/7] drm/asahi: g17: walk every tile in the last segment of a partial render Tiles a segment has no geometry for are skipped, and only the main end-of-tile program runs in the last segment. A multisample colour attachment that isn't stored is resolved by that program alone, since a partial store has to keep the samples for the reload. So a tile that only earlier segments drew to is never resolved, and its part of the resolve target stays as it was: 40-47% of the pixels of a test scene with storeOp DONT_CARE and a resolve. Ask for empty tiles in the last segment. It costs one load and store of each untouched tile, only on a render that already overflowed. A tile entirely outside a smaller render area, in a pass with loadOp DONT_CARE, reloads undefined samples and resolves them over the destination. LOAD and CLEAR already walk those tiles from the first segment. Honeykrisp could take the same path for that case. Signed-off-by: Jonathan Goyvaerts Assisted-by: LLM --- drivers/gpu/drm/asahi/g17/fw/render.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/render.rs b/drivers/gpu/drm/asahi/g17/fw/render.rs index 825b4f9e50d4e..40d83b77f30a7 100644 --- a/drivers/gpu/drm/asahi/g17/fw/render.rs +++ b/drivers/gpu/drm/asahi/g17/fw/render.rs @@ -972,7 +972,9 @@ fn partial_load_registers( [ RegisterWrite::new(0x15369, w.partial_bg_rsrc_spec), RegisterWrite::new(0x15371, w.partial_bg_va), - RegisterWrite::new(0x10039, w.tile_config & !TILE_CONFIG_PROCESS_EMPTY_TILES), + // Only the main end-of-tile program resolves, and it has to reach tiles that only + // earlier segments drew to. Without this their resolve target is never written. + RegisterWrite::new(0x10039, w.tile_config | TILE_CONFIG_PROCESS_EMPTY_TILES), RegisterWrite::new(0x15309, u64::from(p.stencil_clear | STENCIL_CLEAR_BASE)), RegisterWrite::new(0x15329, p.depth.va), RegisterWrite::new(0x153c1, p.depth.compression_va), From 8ec7c05672b9dca3284149bdf2e56e7766e6354c Mon Sep 17 00:00:00 2001 From: Jonathan Goyvaerts Date: Mon, 2 Nov 2026 14:54:08 -0300 Subject: [PATCH 6/7] drm/asahi: g17: partial-render ordinary passes on 25G83, as macOS does Every TA asked the firmware to grow the parameter buffer when it overflowed. That can't go past the maximum. In Counter-Strike 2's soundpack preview a depth pre-pass at 3456x2234 grew to the 3353-block maximum and the firmware failed it. macOS sets this word only for memoryless passes. An ordinary pass stops and flushes partial renders when the heap runs out, and the heap grows between renders. Do the same on 25G83, keeping the host's automatic growth: 9M triangles take 527 ms and 20M 1,172 ms, and images match a render that can't overflow. 26A428 keeps the grow request. This has not been run there, and its partial render path may differ. Linux has no memoryless flag, so such a pass would partial-render too; Honeykrisp always backs its attachments. Signed-off-by: Jonathan Goyvaerts Assisted-by: LLM --- drivers/gpu/drm/asahi/g17/fw/render.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/render.rs b/drivers/gpu/drm/asahi/g17/fw/render.rs index 40d83b77f30a7..0d18d607f2abc 100644 --- a/drivers/gpu/drm/asahi/g17/fw/render.rs +++ b/drivers/gpu/drm/asahi/g17/fw/render.rs @@ -1108,7 +1108,6 @@ unsafe impl Zeroable for TaDescriptor {} impl TaDescriptor { const ARRAY_OFFSET: u64 = core::mem::offset_of!(Self, registers) as u64; const UNK_876: u32 = u32::MAX; - const UNK_892: u32 = 1; const UNK_932: u8 = 0x44; const UNK_93C: u8 = 1; const UNK_93E_FIRST_KICK: [u8; 2] = [0xd0, 0x91]; @@ -1157,7 +1156,9 @@ impl TaDescriptor { self.sampler_heap_va = pass.sampler_heap_va; self.sampler_count = pass.sampler_count; self.sampler_max = sampler_max; - self.unk_892 = Self::UNK_892; + // Asks the firmware to grow the heap. macOS keeps that for memoryless passes, which can't + // partial-render, and Honeykrisp has none. 26A428 keeps it: untested there. + self.unk_892 = u32::from(args.programs != RegisterPrograms::V25G83); self.stamp_va = args.ta.stamp_va; self.aux_stamp_va = args.ta.aux_stamp_va; self.stamp_value_2 = stamp_value; From 3afdeac0598c7a073c09971a854849d9cb4a7593 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 3 Nov 2026 13:17:08 +1100 Subject: [PATCH 7/7] drm/asahi: g17: keep the 25G83 tiling array's doc comment on its function The partial-render series put the tiler segment constants between ta_registers_25g83()'s doc comment and the function, so rustdoc attached the function's description to VDM_SEGMENT_CONTROL and left the function undocumented. Move the constants above the comment. No code change. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/fw/render.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/render.rs b/drivers/gpu/drm/asahi/g17/fw/render.rs index 0d18d607f2abc..d0000282cef82 100644 --- a/drivers/gpu/drm/asahi/g17/fw/render.rs +++ b/drivers/gpu/drm/asahi/g17/fw/render.rs @@ -659,9 +659,6 @@ fn ta_registers( ]) } -/// The 25G83 tiling array: the A18 Pro's without 0x1748 and 0x1740 (and 0x17d9 unless the stage -/// has SCS), with the tiler parameter sub-buffer registers and 0x10131 = 0x88, in the order the -/// M4 Pro has run with. Returns the array and its used entry count. /// Tiler segment control of a two-cluster pass, what macOS writes. const VDM_SEGMENT_CONTROL: u64 = 0x20_0440; const VDM_SEGMENT_INDEX: u64 = 4; @@ -669,6 +666,9 @@ const VDM_SEGMENT_INDEX: u64 = 4; /// returns shrink to none and the tiler can't advance. Fifteen blocks, macOS's minimum heap. const MIN_FREE_TVB_PAGES: u64 = 0x3c; +/// The 25G83 tiling array: the A18 Pro's without 0x1748 and 0x1740 (and 0x17d9 unless the stage +/// has SCS), with the tiler parameter sub-buffer registers and 0x10131 = 0x88, in the order the +/// M4 Pro has run with. Returns the array and its used entry count. fn ta_registers_25g83( args: &RenderArgs<'_>, g: &Geometry,