Skip to content

drm/asahi: g17: partial renders on 25G83 - #879

Merged
RyanTheTide merged 7 commits into
aurora-silicon:integration/aurorafrom
jgoyvaerts:m4pro/g17-partial-renders-25g83
Nov 3, 2026
Merged

RyanTheTide merged 7 commits into
aurora-silicon:integration/aurorafrom
jgoyvaerts:m4pro/g17-partial-renders-25g83

Conversation

@jgoyvaerts

Copy link
Copy Markdown
Contributor

Makes partial renders work on the M4 Pro's 25G83 firmware, so a pass that overflows the tiler heap finishes instead of losing the GPU. Today every pass asks the firmware to grow the heap, and a pass that hits the 3353-block maximum fails with reason 0. Counter-Strike 2's soundpack preview is one: a depth pre-pass at 3456x2234 grows to the maximum and the client gets DEVICE_LOST.

macOS sets TA+0x892 only for memoryless passes. An ordinary pass stops, flushes a partial render when the heap runs out, and the heap grows between renders. Turning that on was not enough: partial renders hung or produced wrong pixels on 25G83. The first five patches fix what was found, and the last one turns it on, so no commit in the series enables a broken path.

  1. Tiler segment limits. 0x1c890, 0x1c918 and 0x1c9a8 were 0. Without 0x1c9a8 the pages a pass returns shrink to none and the tiler spins.
  2. Render pool, 22 to 44 MiB. This was the hang: a stopped tiler's threads keep their USC private memory and the fragment pass starves.
  3. gtp6 size. 0x600 is smaller than macOS's 0x1c00. It is now sized for the largest pool, which is what every test ran with.
  4. Depth and stencil control words. The macOS rule, with each pass's own loads kept in the first segment. A loading pass that did real work there stored background depth over the real values. This was CS2's black tiles.
  5. Last segment walks every tile. Without it, a tile only earlier segments drew to is never resolved by an in-tile resolve.
  6. Partial-render ordinary passes, on 25G83 only.

The register values come from DTrace captures of macOS on the same machine and firmware. The array word 0x80045 / 0x88045 was captured on a pass that clears depth, so keeping a loading pass's loads is inferred, and shown by the results below.

Scope. V26A428 keeps the grow request, so it shouldn't reach the arrays patches 4 and 5 change. The pool size is a shared constant, so A18 Pro pays the extra 22 MiB per context too. Patches 1 and 3 and the last patch apply to V25G83 only. The segment control value is the macOS two-cluster value, so #525 would need to check it for T6041.

Testing, M4 Pro (J616s, T6040), firmware 25G83

  • CS2, soundpack preview: no crash, 0 GPU recoveries. Before patch 4, up to 2,937 of 5,400 tiles were black. After it there were none in 48 captures across 2 soundpacks, including about 90 s watching for flicker, with the depth pass still overflowing twice a frame. Also checked by hand at 4x MSAA and in game.
  • Pixels: a Vulkan client compared byte for byte against the same scene split into passes that can't overflow, at both the starting pool and the grown pool: depth then colour at EQUAL, 1x and 2x, D32 and D32S8, stencil tested, depth loaded from a non-clear value, discard, per-draw state, occlusion queries, two colour targets, 1 to 4 stops, concentrated and spread geometry. All identical. Depth then colour at EQUAL on the grown pool was 100% wrong before patch 4. The in-tile resolve scenes differ only by resolve rounding, at most 1 level.
  • dEQP-VK renderpasses, 1/40 slice, 8 jobs: 1,201 Pass, 844 Skip, 0 Fail, the same as before the series.
  • Stress: 3M triangles 12 of 12 at 179 ms, 9M 527 ms, 20M 1,172 ms, far past the old maximum. 1,257 of 1,257 process restarts in 5 minutes at 3M on an earlier kernel of the series. 0 recoveries.
  • Speed: scenes that overflow only on their first frame are unchanged, within noise.

Not tested: A18 Pro, T6041, and frame rate in scenes that overflow every frame beyond CS2's preview.

Known issues

  • Each 4x render that overflows differs from the last in about 0.02% of pixels, with or without the stored samples, and 0.8% from a reference. No whole tile is lost. The cause is unknown, and it doesn't happen at 2x or without an overflow.
  • The pool costs lower-window space. 44 is measured, not derived: 22 lost every run, 27 passed. Refused allocations in the dEQP slice went from 0 to 15, all retried, no failures.
  • Linux has no memoryless flag, so such a pass would partial-render too. Honeykrisp always backs its attachments. GL clients were not checked.
  • In a pass with a smaller render area, loadOp DONT_CARE and an in-tile resolve, a tile entirely outside the area reloads undefined samples and resolves them over the destination. It takes an overflow as well. Layered passes with the same load op were not tested.

Tested tree. The tests ran on 9ff2b9a3761d plus our carried patches. aurora-wip has had no driver, UAPI or rust/kernel change since. Of the carried patches, four touch the driver: #837, client names in failure reports, MMU fault banks in the recovery log, and the asahi driver name. None touches this path. Patches here were reordered for this PR, and their combined driver diff is the same as the tested one except for the V25G83 condition in the last patch, which keeps the same value on 25G83. Every commit compiles.

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 <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
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 <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
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 <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
…OS 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 <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
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 <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
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 <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
@RyanTheTide
RyanTheTide changed the base branch from aurora-wip to integration/aurora November 2, 2026 23:15
…tion

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 <ryan@aurorasilicon.org>
@RyanTheTide
RyanTheTide merged commit 1ad0abb into aurora-silicon:integration/aurora Nov 3, 2026
@RyanTheTide

Copy link
Copy Markdown
Contributor

Merged. I added one fixup on top (3afdeac, Ryan): the three tiler constants sat between ta_registers_25g83()'s doc comment and the function, so rustdoc gave the function's description to VDM_SEGMENT_CONTROL. That commit only moves the constants above the comment.

Tested on the J616s as a one-shot of 7.3.0-aurora-m4t1103a (integration/aurora 19f71a1 + #837 + this series with the fixup + #884), boot c9c549d5. The tree is the same as the merged one:

  • Kernel -p3 is empty. SELinux is enforcing with 0 AVCs, and the warning set is the same as fleet14p's.
  • Mesa CTS, 21,405 cases on that boot: 19,899 targets (draw, renderpass(es), multisample, depth, glsl, texture, binding_model, robustness2 and more) plus 1,506 stride controls, at 4 jobs on the tip Mesa and two test merges. 0 Pass lost against tip, 0 Fail, 0 GPU recoveries.
  • It also fixes the M4 Pro hang from mesa#109. A synthetic client with 109's frame shape (3 depth-only passes + 4 2x MSAA passes per frame, 3 in flight, stock tip Mesa, no serialization):
    • on fleet14p: hung at frame 691,863 (recovery reason 0, the tiling side of the stuck render done, its fragment side not)
    • on this series: 1,500,000 frames with 0 hangs, at 1340 fps against 1207 before

Not run: the A18 Pro. It pays the larger render pool and keeps the grow request. A Neo one-shot is next, once the Neo is back from a reinstall.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants