From 3334e41e452fc6f4b1326efd72daebd7b23a4a2e Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Tue, 13 Oct 2026 00:36:12 +0200 Subject: [PATCH] fix(shader): read the ancillary render target index as 0 No pre-rasterization stage writes Layer: vertex layer exports are ignored, so the first layer is used and the render target index field of the packed ancillary word is 0. The lowering read the Layer builtin for it instead. AMD's Windows driver drops the color writes of a fragment shader that consumes an unwritten Layer, which made agc_driver_relative_ancillary fail on AMD hardware (#4800); lavapipe returns 0 there. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/PixelAncillary.cpp | 45 +++++++++---------- .../Optimization/src/ConstantFolder.cpp | 3 +- docs/dev/debt/shader.md | 2 +- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/core/libs/tests/PixelAncillary.cpp b/core/libs/tests/PixelAncillary.cpp index 491526ba6..5bc5ba3a3 100644 --- a/core/libs/tests/PixelAncillary.cpp +++ b/core/libs/tests/PixelAncillary.cpp @@ -23,6 +23,19 @@ struct Row { }; constexpr std::array Rows {{{0u, 0u, false}, {0u, 0u, true}, {15u, 0x1FFFu, false}, {15u, 0x1FFFu, true}, {0u, 0x1FFFu, true}, {15u, 0u, false}}}; +static std::uint32_t Evaluate(const IrValue* value, const Row& row); +static void RequireField(const IrValue& user, StageInputKind kind, std::uint32_t fieldOffset, std::uint32_t count) { + if (kind == StageInputKind::Layer) { + for (const Row& row : Rows) Require(Evaluate(&user, row) == 0u); + return; + } + const IrValue* field = user.Argument(0)->Resolve(); + Require(field->Opcode() == IrOpcode::GetBuiltin); + Require(static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == kind); + Require(user.Argument(1)->Resolve()->ImmediateU32() == fieldOffset); + Require(user.Argument(2)->Resolve()->ImmediateU32() == count); +} + static IrBlock& Begin(IrProgram& program) { program.Resources().stage = IrShaderStage::Pixel; program.Resources().resourceTrackingComplete = true; @@ -80,11 +93,7 @@ static void ExtractVector(std::uint32_t offset, std::uint32_t count, StageInputK static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&user})); static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); Lower(program); - const IrValue* field = user.Argument(0)->Resolve(); - Require(field->Opcode() == IrOpcode::GetBuiltin); - Require(static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == kind); - Require(user.Argument(1)->Resolve()->ImmediateU32() == fieldOffset); - Require(user.Argument(2)->Resolve()->ImmediateU32() == count); + RequireField(user, kind, fieldOffset, count); } static void ExtractVectorMaskedCopy(std::uint32_t offset, std::uint32_t count, StageInputKind kind, std::uint32_t fieldOffset) { @@ -109,21 +118,13 @@ static void ExtractVectorMaskedCopy(std::uint32_t offset, std::uint32_t count, S static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {©})); static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); ConstantFolder().Fold(program); - const IrValue* field = user.Argument(0)->Resolve(); - Require(field->Opcode() == IrOpcode::GetBuiltin); - Require(static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == kind); - Require(user.Argument(1)->Resolve()->ImmediateU32() == fieldOffset); - Require(user.Argument(2)->Resolve()->ImmediateU32() == count); + RequireField(user, kind, fieldOffset, count); } static void Extract(IrOpcode opcode, std::uint32_t offset, std::uint32_t count, StageInputKind kind, std::uint32_t fieldOffset) { IrProgram program; auto& user = Build(program, opcode, offset, count); Lower(program); - const IrValue* field = user.Argument(0)->Resolve(); - Require(field->Opcode() == IrOpcode::GetBuiltin); - Require(static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == kind); - Require(user.Argument(1)->Resolve()->ImmediateU32() == fieldOffset); - Require(user.Argument(2)->Resolve()->ImmediateU32() == count); + RequireField(user, kind, fieldOffset, count); } static void Refused(IrOpcode opcode, std::uint32_t offset, std::uint32_t count, bool throughSelect = false) { IrProgram program; @@ -171,7 +172,7 @@ static std::uint32_t Evaluate(const IrValue* value, const Row& row) { throw std::runtime_error("packed pixel ancillary evaluation reached an unsupported value"); } static std::uint32_t PackedWord(const Row& row) { - return UnmodelledBits | (row.sample << 8u) | (row.layer << 16u); + return UnmodelledBits | (row.sample << 8u); } static void Values(IrOpcode opcode, std::uint32_t offset, std::uint32_t count, bool throughSelect) { IrProgram program; @@ -193,11 +194,7 @@ static void LateFold() { IrValue& user = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&ancillary, &offset, &count}); End(builder, user); Lower(program); - const IrValue* field = user.Argument(0)->Resolve(); - Require(field->Opcode() == IrOpcode::GetBuiltin); - Require(static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == StageInputKind::Layer); - Require(user.Argument(1)->Resolve()->ImmediateU32() == 0u); - Require(user.Argument(2)->Resolve()->ImmediateU32() == 11u); + RequireField(user, StageInputKind::Layer, 0u, 11u); for (const Row& row : Rows) { Require(Evaluate(&user, row) == BitField(PackedWord(row), 16u, 11u, false)); } @@ -329,8 +326,7 @@ static void HelperLanesOnly(std::uint32_t chain = 0u) { } } Require(built.written->Parent() == nullptr); - const IrValue* field = built.layer->Argument(0)->Resolve(); - Require(field->Opcode() == IrOpcode::GetBuiltin && static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == StageInputKind::Layer); + RequireField(*built.layer, StageInputKind::Layer, 0u, 11u); const IrValue* entryValue = built.exported->Argument(0)->Resolve(); Require(entryValue->HasImmediate() && entryValue->ImmediateU32() == SelectOther); } @@ -416,8 +412,7 @@ static void HelperLoopOnly(LoopRead read) { } } Require(built.written->Parent() == nullptr); - const IrValue* field = built.layer->Argument(0)->Resolve(); - Require(field->Opcode() == IrOpcode::GetBuiltin && static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == StageInputKind::Layer); + RequireField(*built.layer, StageInputKind::Layer, 0u, 11u); const IrValue* entryValue = built.exported->Argument(0)->Resolve(); Require(entryValue->HasImmediate() && entryValue->ImmediateU32() == SelectOther); } diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index 079df35fb..fc48ec49d 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -384,8 +384,9 @@ void lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancil } std::array fields {}; const auto field = [&](std::size_t index) -> IrValue& { + if (fields[index] == nullptr && index == 1u) fields[index] = &builder.Constant(0u); if (fields[index] == nullptr) { - fields[index] = &insertBuiltin(program, builder, ancillary, index == 0u ? StageInputKind::SampleId : StageInputKind::Layer); + fields[index] = &insertBuiltin(program, builder, ancillary, StageInputKind::SampleId); } return *fields[index]; }; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index c424adcaf..bb76f27c5 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -2,7 +2,7 @@ ### Unknown function info -- [Packed pixel ancillary word](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295, and their equality with the SampleId and Layer builtins is inferred, not measured. Mesa reads both fields from those bits, with the layer 14 bits wide only from GFX12 on ([ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c#L342-L368)). A value passed through a select, move or phi whose every read is an extract inside one of those fields is rebuilt from the two fields alone. Any other read the masked-select pass cannot drop gets the whole word: the primitive class at bits 0-1 (0 point, 1 line, 2 triangle, 3 rectangle), which the draw sets as a pipeline specialization constant ([RasterPrimitiveOf](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp)) in the shaders that read it, plus the sample field, with every other bit zero. That layout comes from the RDNA 3 and 3.5 ISA guides (prim type 1:0, VRS rates 3:2 and 5:4, eye id 7, sample 11:8, render target index 28:16). The GCN register guides give the same prim type and sample fields and put the render target index at 26:16, so the two sources agree for layers below 2048. AMD's LLPC reads the primitive class from bits 1:0 with the same encoding ([LowerGlCompatibility.cpp](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/llpc/lowering/LowerGlCompatibility.cpp#L830-L836)) and without a GFX-level check ([getPrimType](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/LowerInOut.cpp#L5307-L5315)). Nobody has checked it on gfx1013. In the whole word only bits 0-1 and 8-11 have a source. Bits 2-7, 12-15 and 29-31 are assumed zero, not measured: the VRS rates (2-5) and the eye id (7) because the driver supports neither variable-rate shading nor multiview, so a title that enables either would read other bits on the console, and bits 6, 12-15 and 29-31 because no source gives them a meaning. The sample field reads SampleId, which is always 0 here because the driver rejects MSAA and per-sample shading (`PA_SC_AA_CONFIG`, `PS_ITER_SAMPLE`). The render target index (bits 16-28) of the whole word is the constant 0 and not the Layer builtin: draws render to a single layer and guest layer exports are ignored, so no stage writes Layer, and a fragment shader that reads the unwritten builtin loses its writes on AMD's Windows driver (#4800). A change that makes draws export a layer must restore the read +- [Packed pixel ancillary word](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295. The sample index is the SampleId builtin, an equality inferred, not measured. The render target index is 0: vertex layer exports are ignored, so no pre-rasterization stage writes Layer and the first layer is used. Reading the unwritten Layer builtin instead makes AMD's Windows driver drop the fragment's writes (#4800). Mesa reads both fields from those bits, with the layer 14 bits wide only from GFX12 on ([ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c#L342-L368)). A value passed through a select, move or phi whose every read is an extract inside one of those fields is rebuilt from the two fields alone. Any other read the masked-select pass cannot drop gets the whole word: the primitive class at bits 0-1 (0 point, 1 line, 2 triangle, 3 rectangle), which the draw sets as a pipeline specialization constant ([RasterPrimitiveOf](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp)) in the shaders that read it, plus the sample field, with every other bit zero. That layout comes from the RDNA 3 and 3.5 ISA guides (prim type 1:0, VRS rates 3:2 and 5:4, eye id 7, sample 11:8, render target index 28:16). The GCN register guides give the same prim type and sample fields and put the render target index at 26:16, so the two sources agree for layers below 2048. AMD's LLPC reads the primitive class from bits 1:0 with the same encoding ([LowerGlCompatibility.cpp](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/llpc/lowering/LowerGlCompatibility.cpp#L830-L836)) and without a GFX-level check ([getPrimType](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/LowerInOut.cpp#L5307-L5315)). Nobody has checked it on gfx1013. In the whole word only bits 0-1 and 8-11 have a source. Bits 2-7, 12-15 and 29-31 are assumed zero, not measured: the VRS rates (2-5) and the eye id (7) because the driver supports neither variable-rate shading nor multiview, so a title that enables either would read other bits on the console, and bits 6, 12-15 and 29-31 because no source gives them a meaning. The sample field reads SampleId, which is always 0 here because the driver rejects MSAA and per-sample shading (`PA_SC_AA_CONFIG`, `PS_ITER_SAMPLE`). The whole word has the same 0 at bits 16-28. A change that makes draws export a layer must restore the Layer read in both places - [Color comparison descriptor forms](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): emulated color comparison supports UNORM, SNORM and float formats for 2D, 2D-array and cube views. 8-bit sRGB and UNORM (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`, `Format8UNorm`, `Format8_8UNorm`, `Format8_8_8_8UNorm`) return 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. Anisotropic filters are refused, except on those 8-bit formats, where every texel comparison is 0 (not measured with an anisotropic S#). Bilinear cube comparisons of float, snorm and the other unorm formats without `DISABLE_CUBE_WRAP` are refused; on that hardware a 4x4 R32 float cube blends across faces (46 of 90 edge rows differ from a per-face clamp; face 0, ref 0.1, u=3.9 v=2.5 is 0.6016 and u=1.5 v=0.1 is 0.3984), and with `DISABLE_CUBE_WRAP` the per-face clamp matches. An 8-bit sRGB or UNORM bilinear cube without that bit returns 0, since every texel comparison is already 0 for every compare function and the selected face cannot change the result. Cube-edge sampling behavior and console coverage on PS5's gfx1013 remain unverified. - [s_setkill](../../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified - [s_sethalt](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) (core/shader/recompiler) - with SIMM16[0] clear it is a no-op: the RDNA 2 ISA guide describes that value as resuming a halted wave, and a wave that executes it is not halted; measured on a gfx1035 (RDNA 2) wave: `s_sethalt 0` followed by a VALU op completes and gives the normal result. Only SIMM16[0] is read; with it set the instruction throws