From aecd98f83e8fe4ceec9c18996357c4ec28ca5399 Mon Sep 17 00:00:00 2001 From: jargnar <1476125+jargnar@users.noreply.github.com> Date: Fri, 13 Nov 2026 15:33:46 +0530 Subject: [PATCH] hk: read large vertex and fragment constant tables from a blob on AGX2 On the AGX2 chips a vertex or fragment shader that indexes a large constant array stored the whole array to scratch in every invocation before it read one entry of it. nir_opt_large_constants ran only on the native AGX3 paths and the M5, so on AGX2 the scratch lowering in agx_preprocess_nir() claimed the array, and scratch is private to one invocation. DXVK 3 writes a D3D11 immediate constant buffer as such an array, a Private, NonWritable variable with a constant initializer, and on Honeykrisp does not move it to a buffer itself. Skyrim Special Edition's sun-shadow mask pass has a table of 1005 vec4. Its fragment shader compiled to 5987 instructions with 1054 spills and 16080 bytes of scratch, and its one full-screen draw took 339 ms at 1920x1080, about 165 ns per pixel. The game's world ran at 2.8 fps. Run the existing vertex and fragment branch on every chip. Nothing after it is particular to the native paths: lower_load_constant reads the blob's address from draw.vs_constant_data_addr or draw.fs_constant_data_addr, hk_compile_nir() keeps nir->constant_data with the shader, and hk_flush_shaders() uploads the bound blobs and publishes both addresses, whatever the chip. The same shader becomes 578 instructions with no spills and no scratch, the pass takes 0.49 ms and the game runs at 43 fps. Unchanged: an array the shader writes to is not constant and stays in scratch. A shader with no array above the scratch threshold is left alone, as on the native paths. AGX2 compute keeps its scratch arrays, because only the native dispatch paths publish cs.constant_data_addr. The upload reuse in hk_cmd_upload_constants() stays native-only, so AGX2 uploads a bound blob again at each shader flush. The native AGX3 and M5 conditions are the same as before. Measured on one AGX2 machine with two builds of b7b03c5b3b8a, with and without this change (debugoptimized, assertions on): - A full-screen 1920x1080 draw whose fragment shader reads one entry of a 1005-entry vec4 table per pixel: 421 ms to 0.31 ms a frame, every pixel as expected in both. The shader goes from 3680 instructions, 1893 spills and 19696 bytes of scratch to 15 instructions. With the table declared as DXVK declares it: 421 ms to 0.44 ms. - 1005 points whose vertex shader reads the table, drawn directly and through a pass-through geometry shader: every point as expected in both, the vertex shader from 3701 instructions to 27. - A fragment shader that writes one entry of the table first: the same shader hash and the same output in both. - Skyrim Special Edition, Proton Experimental (ARM64), DXVK v3.1.1-188, 1920x1080, Ultra preset: 2.8 fps to 43 fps. - The table shaders and the three CI probe shaders compile on the drm-shim profiles g13g, g13x, g14g, g15s, g15c, g15g and g17p. Not measured: Vulkan CTS, any other AGX2 chip on hardware, tessellation, and the native AGX3 and M5 paths on hardware. Closes: https://github.com/omacom/mesa/issues/44 Co-Authored-By: Claude Fable 5.2 --- src/asahi/vulkan/hk_shader.c | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/asahi/vulkan/hk_shader.c b/src/asahi/vulkan/hk_shader.c index 77d3dbdf13bc..480d27dbe860 100644 --- a/src/asahi/vulkan/hk_shader.c +++ b/src/asahi/vulkan/hk_shader.c @@ -514,21 +514,31 @@ hk_preprocess_nir_internal(struct vk_physical_device *vk_pdev, nir_shader *nir) * afterwards silently does nothing: there is no function_temp array left to * move, and nir_opt_large_constants only ever looks at function_temp * (nir_opt_large_constants.c:545-561). + * + * The AGX2 chips take the vertex and fragment branch as well. Their + * backend can encode scratch, but scratch is private to one invocation, so + * the shader stores the whole table there before it reads a single entry. + * lower_load_constant and hk_flush_shaders() already serve vertex and + * fragment blobs on every chip. AGX2 compute is left as it was: the + * classic dispatch path publishes no cs.constant_data_addr. */ - if (agx_native_graphics(&((struct hk_physical_device *)vk_pdev)->dev) || - agx_chip_g17_m5(((struct hk_physical_device *)vk_pdev)->dev.chip)) { - if (nir->info.stage == MESA_SHADER_COMPUTE) { + const bool native_constants = + agx_native_graphics(&((struct hk_physical_device *)vk_pdev)->dev) || + agx_chip_g17_m5(((struct hk_physical_device *)vk_pdev)->dev.chip); + + if (nir->info.stage == MESA_SHADER_COMPUTE) { + if (native_constants) { NIR_PASS(_, nir, nir_opt_large_constants, NULL, 32); - } else if ((nir->info.stage == MESA_SHADER_VERTEX || - nir->info.stage == MESA_SHADER_FRAGMENT) && - hk_has_scratch_sized_array(nir)) { - /* Vertex and fragment arrays within the scratch threshold keep their - * csel ladders, so leave shaders without a larger array untouched. - * nir_opt_large_constants cannot lower a whole-array copy_deref. */ - NIR_PASS(_, nir, nir_lower_var_copies); - NIR_PASS(_, nir, nir_opt_large_constants, NULL, - AGX_SCRATCH_ARRAY_THRESHOLD_B); } + } else if ((nir->info.stage == MESA_SHADER_VERTEX || + nir->info.stage == MESA_SHADER_FRAGMENT) && + hk_has_scratch_sized_array(nir)) { + /* Vertex and fragment arrays within the scratch threshold keep their + * csel ladders, so leave shaders without a larger array untouched. + * nir_opt_large_constants cannot lower a whole-array copy_deref. */ + NIR_PASS(_, nir, nir_lower_var_copies); + NIR_PASS(_, nir, nir_opt_large_constants, NULL, + AGX_SCRATCH_ARRAY_THRESHOLD_B); } /* Optimize but allow copies because we haven't lowered them yet */