Repository navigation
AGC: vkQuake's ray-traced shadow compute shader becomes 519,500 SPIR-V words; Intel ANV compiles it for minutes under GpuMutex and crashes (PPSA99010, Linux) #4977
Description
vkQuake (PPSA99010) with its default ray-traced shadows freezes on Linux with an Intel GPU. One compute shader of the title's run-time RADV port becomes a 519,500-word SPIR-V module. Intel's compiler works on it for minutes while the AGC queue thread holds GpuMutex, so the title's main thread blocks, and after about 5 minutes Mesa itself segfaults.
Setup: main 7e181fc, Arch, GCC 16.2.1 Release, Intel Core Ultra X7 358H (Arc B390, Mesa 26.2.2 ANV, host subgroup 32), relinked with --to-intel, Quake 1.06 shareware id1/pak0.pak (MD5 5906e5998fc3d896ddaf5e6a62e03abb), Xwayland. Same title and data as the Windows reports in #1578.
What happens
- With
APS5_PROFILE_DRAW=1the last line is[dispatch] creating a pipeline for 519500 SPIR-V words (program 0x20bf7f400). Nothing follows: in a 10-minute run the process segfaults about 5 minutes later insidelibvulkan_intel.so(kernel:segfault at 40 ... in libvulkan_intel.so). The kernel log shows no GPU hang or reset. - Interrupted in gdb while stuck, the AGC queue thread is in
Driver::run→execute→dispatch→VulkanDevice::Dispatch→libvulkan_intel.so(deep recursive frames), under theGpuMutexthatDispatch.cpptakes beforelocalDevice->Dispatch. The title's main thread waits for it insceKernelMunmap→DoMunmap→GuestAllocationsUnmap→WaitForLeasesAndImports→GpuMutexType::lock(). So the whole title stops, not just the frame. - With
r_rtshadows 0the same build plays the demo loop (2 of 2 runs of 120 s).
Where the size comes from
The program is 2,748 bytes of ISA (pc 0 to 0x2af0). Counts from APS5_DUMP_IR=20bf7f400 and from the SPIR-V in an APS5_DUMP_COMPUTE_PIPELINES capture:
| Stage | Size |
|---|---|
IR after translate |
~60,000 values (12,397 BitwiseAnd32, 8,062 SelectU32, 5,044 IEqual32, 4,579 GetVectorRegister) |
IR after resources |
~38,000 values in 185 blocks |
| SPIR-V | 519,500 words, 110,811 instructions, 6,074 OpLabel; top opcodes OpBitwiseAnd 22,722, OpSelect 15,075, OpBitcast 10,482, OpIEqual 9,591 |
spirv-opt -O of that SPIR-V |
461,429 words (−11%) |
Three things stack up:
- f32 denormal flushing. The shader's FLOAT_MODE flushes f32 denormals, so
f32DenormalFlushForwraps every add, mul, fma, min/max, compare and conversion in the exact flush emulation (flushF32Denormal,flushTinyProduct). As a local experiment only, I madef32DenormalFlushForreturn 0: the module drops to 340,562 words (−34%), and the final IR to 21,200 values. It still did not compile within 120 s. - Two lanes per invocation. The module has
LocalSize 32 1 1, and the SPIR-V repeats each operation for a pair of values (%45932/%45933, ...), consistent with a wave64 program on a 32-wide host subgroup. That roughly doubles everything after the IR. - IEEE min/max and NaN rules. Runs like
OpFOrdLessThan+ 5OpSelect+ exponent masks perv_min/max_f32, plus> 0x7f800000 → 0xffc00000canonicalisation.
The closed #3842 (Killzone: Liberation, 649,515 words, ANV stuck in brw_opt_register_coalesce) had the same symptom from a different cause (relative VGPR select chains, fixed by the vector window). #4042 proposes cheaper special-value rules for packed f16; this shader is f32.
What I don't know
- Whether NVIDIA or AMD drivers compile this module in reasonable time: on NVIDIA the title stops earlier on this shader family with the Shader float mode (IEEE_MODE, DX10_CLAMP, FLOAT_MODE) is per shader; the recompiler assumes one #981
s_round_modeerror. - Whether the denormal emulation could use SPIR-V
DenormFlushToZerowhere the device reportsshaderDenormFlushToZeroFloat32, given the before/after-rounding distinctionflushTinyProductmodels. - Whether building a pipeline should happen without
GpuMutexheld, so a slow driver compile stalls one queue instead of everymunmap.
I have the 2 MB capture.aps5cp (replays with agc_compute_pipeline_replay built with ANYPS5_ENABLE_SPIRV_TOOLS=ON) and the IR dump, and can attach them or rerun anything.
Activity
MarcZX10 commentedon Oct 13, 2026
I ran PR #4948's
agc_redundant_value_eliminationtest locally on Linux (GCC 12.2, Release); it passes (1/1). That validates the eliminator tests, but neither that test nor the PR's hosted checks exercise the 519,500-word vkQuake shader.Since you mentioned having the
capture.aps5cp, could you attach it here or share a link? We can then measure the IR/SPIR-V size and pipeline preparation time with and without #4948 on that exact capture.Faffu commentedon Oct 13, 2026
Another title hits this on Intel: Astro Bot (PPSA21559 v01.018) on
main8cf6a0b, Arc Pro B50 (Battlemage), Mesa 26.2.2 ANV, Fedora 44. Details in #357.ANV pipeline creation in
agc_compute_pipeline_replay, Mesa shader cache disabled:OpLabelOpPhiOpLoopMergeTwo data points that may help whoever picks the fix:
OpPhi; all eight share the same top opcodes (OpBitwiseAnd,OpSelect,OpBitcast,OpIEqual).I'll measure #3655, #4948 and #2837 on these captures and report.
AI-assisted: yes
MarcZX10 commentedon Oct 13, 2026
Thanks for the Astro Bot measurements. The two very slow modules having thousands of
OpPhinodes and loops is a useful lead, and the 13.9 GB peak makes the memory pressure concrete. Please share the #3655/#4948/#2837 comparison when you have it; especially useful would be whether any of those changes reduce compile time or peak RSS for the exact vkQuake capture, not only the module size.Faffu commentedon Oct 13, 2026
Follow-up with measurements on the Astro Bot program that stalls (
0x5005f6100, see my comment above), to see which lever matters on ANV.Method. One title run with
APS5_DUMP_SHADERS=1saved the recompile request. Each variant below ismain8cf6a0b with the pull request merged, andagc_shader_replay --spvregenerates the module. That module is spliced into the title'sAPS5_DUMP_COMPUTE_PIPELINEScapture (same layouts) and timed withagc_compute_pipeline_replay, Mesa shader cache disabled. The offline module uses the tool's default target, so it is smaller than the title's (362,818 words against 408,316). It has the same structure, though (2,181OpPhi, 8 loops, 256 calls), and the same cost on ANV: 831 s and 13.9 GB, against 763 s and 13.9 GB for the title's module.mainf32DenormalFlushForreturns 0The other three stalling or slow programs (
0x50064fc00,0x555f4f500,0x5007a9500) change in the same proportions: −23 to −36% without the flush, −5 to −10% with #4539.Where the size is. In the
mainmodule,mainitself is 338,446 words. The six BDA helpers are about 3,000 words of bodies, called 256 times: 205record_bda_fault, 23read_bda_span, 19probe_bda_pointer, 4get_bda_pointer, 4read_bda_dword_bytes, 1load_bda_span. Even inlined, that is about 50,000 words.mainholds 6 loops of the guest program and 2,160OpPhi. So for this program the BDA lookup changes cannot move the compile time much, and the f32 denormal flush emulation is the largest single factor: removing it cuts compile time by 4.5x and peak memory by half. Even then 183 s per pipeline is too slow for loading. Without the flush the top opcodes are stillOpSelect8,665,OpBitcast7,522 andOpBitwiseAnd4,208.The experiment is not a fix proposal: dropping the flush changes results, and I can't verify a native
DenormFlushToZeropath against RDNA without AMD hardware. I can rerun this setup on any branch that changes the recompiler and report the ANV time.AI-assisted: yes
MarcZX10 commentedon Oct 13, 2026
Thanks for the controlled measurements. The comparison points to denormal flushing as the largest cost here, but disabling it changes results, so I don’t see a safe AnyPS5 change from these numbers alone. A native or otherwise semantics-preserving lowering would need correctness evidence on AMD before I’d propose it.
milescoviello commentedon Oct 14, 2026
NVIDIA data for the open question above (whether NVIDIA compiles these modules in reasonable time). It no longer stops on #981 there, and it compiles them, but slowly.
Setup:
maind167000, Ubuntu 24.04, GCC 13.3, Release, RTX 4070 Ti (NVIDIA 580.142), relinked with--to-intel, Quake 1.06 sharewareid1/pak0.pak(MD55906e5998fc3d896ddaf5e6a62e03abb), default settings,APS5_PROFILE_DRAW=1. An earlier run onmain7a0c838 with #5464, #3925, #5523 and #5550 merged gave 30.2 s, 31.2 s and 32.1 s for the same three programs.0x21174f400(192x1x1)0x206d5d800(4x1x1)0x206d61000(4x1x1)[gpu] worker at 111 s: dispatch 107.2 s). Interrupted in gdb meanwhile, the AGC queue thread is inVulkanDevice::dispatch→libnvidia-glcore→libnvidia-gpucomp.I haven't tried
r_rtshadows 0here.AI-assisted: yes
ThatsLeo commentedon Oct 14, 2026
Correctness data for the native lowering @MarcZX100 asked about, from Intel, and a third title.
Intel's
DenormFlushToZerogives the bits #1850 measured on RDNA2. Setup: Fedora 44, Intel Arc (Meteor Lake, Mesa 26.2.3 ANV,shaderDenormFlushToZeroFloat32true,denormBehaviorIndependence32-bit only), a standalone Vulkan program outside AnyPS5. Each operation runs as its own compute shader withDenormFlushToZero 32andSignedZeroInfNanPreserve 32, over 217,600 rows: 26 values (8 denormals of both signs, zeros, min normal, 1,0x3f7fffff, max, infinities, quiet and signaling NaN) crossed three ways, plus 200,000 random rows of products, sums and differences around 2^-126. The reference applies #1850's rules: inputs and result flushed to a zero of the same sign, and tininess before rounding for products. NaN results are compared as NaN, everything else bit for bit.DenormFlushToZeroDenormPreserveOpFAdd,OpFSubOpFMul(tiny products included)Fma, zero addendFma, nonzero addendFMin,FMaxFloor,FractOpFOrdLessThan,OpFOrdEqualOpConvertFToSThe two
Fmarows are the gap #1850 left open:0x80800000 * 0x3f7fffff + 0x00ffffff, whose exact sum lies just under 2^-126 and rounds up to it. The Arc gives0x00000000(tininess before rounding), while the reference and the current emulation give0x00800000. Without an AMD run I can't say which RDNA does. ANV with no execution mode at all flushes the same way.What it saves, and what it doesn't. A local switch that leaves
f32DenormalFlushForat 0 and declaresDenormFlushToZero 32instead, onmainf265cd55. With it, the 48 compute shaders Stray (PPSA02101) dispatches up to its second scene go from 2,171,795 to 1,650,246 SPIR-V words (−24%). Its largest, program0x2010cf0000(6,060 code words, 8x8x1 threads, wave64), goes from 624,132 to 436,504 (−30%). That one is what freezes Stray on Intel after the calibration screen. Neither version of it compiles here, though: created alone withvkCreateComputePipelines(required subgroup size 32, Mesa cache off), both were still compiling after more than 10 minutes, past 13 GB of RSS, when I stopped them. It is one function with 907 blocks, 42OpPhi, no loops or calls, and about 90,000 instructions. The pair repetition of a wave64 program on a 32-wide subgroup and the min/max NaN rules account for most of the rest.Proposal. I'd open a PR that uses the hardware flush only where it is shown to match: the driver checks, once per device, the rows above for each class of flushing instruction against the emulation's rules, and lowers a class through
DenormFlushToZero 32only if every row matches. Drivers without the property (NVIDIA) or with any mismatch keep today's emulation, and the fma with a nonzero addend stays emulated everywhere. That keeps results identical to today on every driver while making the modules about a quarter smaller where the check passes. It doesn't by itself make Stray's or vkQuake's largest shader compile on ANV; that needs the wave64 pairs to shrink too. Does this sound acceptable before I write it?AI-assisted: yes.
MarcZX10 commentedon Oct 14, 2026
The per-device probe is a useful way to gate a host-specific optimization, but one SPIR-V constraint needs to be part of the design:
DenormFlushToZero 32is an entry-point execution mode, and execution modes apply to the entry point’s whole static call graph. It cannot be selected only for add/mul/min/max while leaving a nonzero-addend Fma in the same entry point under the old behavior. Since your matrix reports two Fma mismatches, checking the mode once and retaining the current Fma lowering would not by itself isolate that mismatch.I would make the mixed-operation case explicit in the regression: one shader containing a verified-safe operation and the nonzero-addend Fma counterexample, compared against the current emulator semantics. The SPIR-V spec describes the mode as applying to all operations of the target width in the entry point call graph: https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#Execution_Mode. Vulkan’s
shaderDenormFlushToZeroFloat32property permits the mode; it does not establish that its result matches the guest behavior, so your per-device semantic probe remains important.nikowolf123 commentedon Oct 14, 2026
RTX 5080 data for the per-device check @ThatsLeo proposed and for @MarcZX100's point that a mode covers the whole entry point. The driver offers neither f32 denormal mode. With no f32 mode it already flushes f32 denormals in most ops, as @SleglJan found on an RTX 3080 (#2523 (comment)). Which ops it flushes changes, though, when the module also declares
DenormPreserve 16, which main does on NVIDIA.Properties. From
vulkaninfoon an RTX 5080 (Blackwell), Windows 11, NVIDIA 616.92:VkPhysicalDeviceFloatControlsPropertiesshaderDenormFlushToZeroshaderDenormPreserveshaderSignedZeroInfNanPreserveshaderRoundingModeRTEshaderRoundingModeRTZdenormBehaviorIndependenceandroundingModeIndependenceare bothALL. The f32 denormal properties are the same as on @kr4y4's RTX 4070 Laptop (#981 (comment)) and @SleglJan's RTX 3080 (#981 (comment)).Main's modes. For f32, main declares only
SignedZeroInfNanPreserve 32(SpirvModuleSetup.cpp#L152). On NVIDIA it addsDenormPreserve 16in two cases:RoundingModeRTE 16andSignedZeroInfNanPreserve 16, when it converts f32 to f16 or does packed f16 math (SpirvAluEmitterMath.cpp#L1420-L1430).Probe. One standalone compute shader: one entry point, a switch over the ops,
NoContractionon the arithmetic results (main'sExactadds it toFma). It ran over 582,402 rows: 16 hand-picked rows, a 26-value cross product (no NaN inputs) and 560,000 random rows with exponents near 2^-126. I compared each result bit for bit against IEEE and against a flush rule. Under the rule, inputs and denormal results become a zero of the same sign, and a mul or fma result is tiny if it is below 2^-126 after rounding to 24 bits.SignedZeroInfNanPreserve 32DenormPreserve 16OpFAdd,OpFSub(100,678 / 100,677)OpFMul(100,679)Fma(137,580)FMin,FMax(20,677 each)OpFOrdLessThan,OpFOrdEqual(20,677 each)Floor(20,027)floor(0x80000001)=0xbf800000Fract(20,027)fract(0x00000001)=0,fract(0x80000001)=0x3f800000A tiny-before-rounding rule differs from the card in 9 mul and 81 fma rows.
NoContractionmade no difference, and repeat runs of these modules gave identical bits (two to four runs each). Adding main's other two f16 modes,RoundingModeRTE 16andSignedZeroInfNanPreserve 16, gave the same bits asDenormPreserve 16alone (one run).Without the f16 mode, the ops that follow the rule match the classes whose flush @SleglJan's opt-in
APS5_HOST_F32_FTZexperiment drops. With it, add, sub, mul and the compares keep f32 denormals. So a per-device probe should include the f16 modes main may add to the same entry point: on this card they change the f32 results.The counterexample. On the row from @ThatsLeo's matrix (#4977 (comment)),
Fma(0x80800000, 0x3f7fffff, 0x00ffffff)returns0x00000000in every variant, the same as the Arc withDenormFlushToZero.For a nonzero addend,
flushTinyProductkeeps the driver'sFmaresult, because the tiny test is ANDed withaddend == 0(OperandAccess.cpp#L163-L171).writeOperandthen only flushes a denormal result (#L252). So, following that path, main'sv_fma_f32gives0x00000000for this row on this card, and0x00800000on a driver whoseFmakeeps denormals. I did not run main'sv_fma_f32itself. For @MarcZX100's mixed-operation regression (#4977 (comment)), the current emulator's expected value for that row therefore already depends on the host.Not measured. I did not measure SPIR-V size or compile time with and without the flush emulation, because no capture of the vkQuake, Astro Bot or Stray programs is attached here. The closest NVIDIA numbers are @SleglJan's in the #2523 comment above. On the RTX 3080, 007's largest module (folded with spirv-opt) went from 4.90M words and 130.4 s of pipeline creation to 2.83M words and 72.2 s with the flush and the plain-multiply tiny-product check stripped for the classes NVIDIA flushed. If someone attaches a
capture.aps5cp, I can time it withagc_compute_pipeline_replayon this card. I can also attach the probe (SPIR-V assembly and C++ harness).AI-assisted: written with Claude Code; I checked the property values in both
vulkaninfoand the probe's own query, the code paths at main e34c479 and the comments linked above, and ranvulkaninfoand the probe (four module variants, all but one run more than once) on the RTX 5080.ThatsLeo commentedon Oct 14, 2026
A correction to my earlier comment, and the mixed-operation check @MarcZX100 asked for, on the Arc (Meteor Lake, Mesa 26.2.3 ANV) at
maine154f5658.Correction. I wrote that the current emulation gives
0x00800000for0x80800000 * 0x3f7fffff + 0x00ffffff. On ANV it doesn't. @nikowolf123 was right: with a nonzero addend,mainkeeps the driver'sFmaresult. ANV flushes that row even with no execution mode, somainalready returns0x00000000there.One shader, every operation.
agc_driver_f32_denormal_flushruns 19 flushing instructions in one compute entry point:v_add,v_mul,v_fma,v_fmac,v_min,v_max3,v_med3,v_floor,v_frexp_*,v_cmp_lt,v_div_fixup,v_mullitand others;I put the counterexample in its last row and ran it three ways with a local switch:
v_fma_f32main(emulation)0x00000000DenormFlushToZero 32declared, emulation kept0x00000000DenormFlushToZero 32declared,f32DenormalFlushForreturning 0 for every instruction0x00000000Mode declared, emulation kept. It also passes the other 23 float tests and the whole
libSceAgcDriversuite: 554 of 556, and the two failures fail without the mode too. So on ANV the mode doesn't change the instructions that stay emulated in the same entry point, including that fma row.Every software flush dropped. That is not an option. The hardware flush doesn't cover these instructions:
v_min,v_max3andv_med3, lowered with the NaN rules;v_frexp_*,v_mullit,v_mul_legacy,v_mad_legacyandv_mac_legacy;v_div_fixupandv_cmp_lt(one row in each mode);v_mulandv_fma_mixunder the non-default rounding modes.It also accepts a
FLOAT_MODEthat the emulation refuses.What a probe could drop. Only the classes the mixed test leaves at 0 mismatches could drop their software flush, and only in the default rounding mode:
v_add/v_sub,v_mul,v_fma/v_fmacandv_floor/v_ceil.v_cmp_ltdiffered in one row per mode. Whatever the device does with the mode, the probe has to run the recompiler's own lowering in one module, not each operation alone; my standalone probe showed 0 mismatches forFMinand the compares, but the recompiler doesn't lowerv_mintoFMin.AI-assisted: yes