Merge pull request #4209 from karanvora2599/fix/shader-cube-gradients

fix(shader): keep cube image gradients out of the face coordinate bias
This commit is contained in:
Adrià 2026-10-13 15:45:43 +02:00 committed by GitHub
commit e152ba8f69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 168 additions and 4 deletions

View File

@ -0,0 +1,163 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <bit>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Threads = 32;
constexpr std::uint32_t Words = 32;
constexpr std::uint32_t ResultWord = 8;
constexpr std::uint32_t Size = 16;
constexpr std::uint32_t Levels = 5;
constexpr std::uint32_t Faces = 6;
constexpr std::uint32_t Gradients = 4;
constexpr std::uint32_t Format8888UNorm = 56;
constexpr std::uint32_t TypeCube = 11;
alignas(256) std::array<std::uint32_t, Threads * Words> Buffer{};
alignas(256) std::array<std::uint8_t, 65536> Texels{};
alignas(256) constexpr std::array<std::uint32_t, 28> Code{
0x34020087, 0xe0301000, 0x80000501, 0xe0301004, 0x80000601, 0xe0301008, 0x80000701, 0xe030100c,
0x80000801, 0xe0301010, 0x80000901, 0xe0301014, 0x80000a01, 0xe0301018, 0x80000b01, 0xbf8c3f70,
0xf0880f18, 0x00610c05, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028,
0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000,
};
struct Sample {
std::uint32_t level;
std::uint32_t face;
std::uint32_t gradient;
std::uint32_t x;
std::uint32_t y;
};
Sample SampleOf(std::uint32_t tid) {
return {tid % Levels, (tid / Levels) % Faces, (tid / 3u) % Gradients, (tid * 7u + 3u) % Size, (tid * 5u + 1u) % Size};
}
std::uint32_t Bits(float value) {
return std::bit_cast<std::uint32_t>(value);
}
std::array<std::uint32_t, 8> TextureDescriptor(const void* data) {
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data));
return {
static_cast<std::uint32_t>(address >> 8u),
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u),
((Size - 1u) >> 2u) | ((Size - 1u) << 14u),
0xfacu | ((Levels - 1u) << 16u) | (TypeCube << 28u),
Faces - 1u,
(Levels - 1u) << 4u,
0u,
0u,
};
}
void FillInput() {
Buffer.fill(0xdeadbeefu);
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
const auto sample = SampleOf(tid);
auto* words = &Buffer[tid * Words];
for (std::uint32_t gradient = 0; gradient < Gradients; ++gradient) {
words[gradient] = Bits(gradient == sample.gradient ? std::exp2(static_cast<float>(sample.level) - 0.25f) / static_cast<float>(Size) : 0.0f);
}
words[4] = Bits(1.0f + (static_cast<float>(sample.x) + 0.5f) / static_cast<float>(Size));
words[5] = Bits(1.0f + (static_cast<float>(sample.y) + 0.5f) / static_cast<float>(Size));
words[6] = Bits(static_cast<float>(sample.face));
}
}
void FillTexture() {
Texels.fill(0xeeu);
const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(Texels.data())));
Require(surface.layers == Faces && surface.mips.size() == Levels && surface.guestBytes <= Texels.size(), "image sample cube derivatives: the cube mip chain does not fit the texel storage");
for (std::uint32_t face = 0; face < Faces; ++face) {
for (std::uint32_t level = 0; level < Levels; ++level) {
const auto& mip = surface.mips[level];
for (std::uint32_t y = 0; y < mip.height; ++y) {
for (std::uint32_t x = 0; x < mip.width; ++x) {
auto* texel = &Texels[surface.GuestLayerOffset(face) + mip.tiledOffset + static_cast<std::uint64_t>(y) * mip.pitchBytes + x * 4u];
texel[0] = static_cast<std::uint8_t>(level);
texel[1] = static_cast<std::uint8_t>(face);
texel[2] = 0u;
texel[3] = 255u;
}
}
}
}
}
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
}
std::array<std::uint32_t, 4> SamplerDescriptor() {
return {0u, 0xfffu << 12u, 1u << 26u, 0u};
}
void Run(AgcDriver::VulkanDevice& device) {
std::vector<std::uint32_t> userData(16, 0u);
const auto buffer = BufferDescriptor(Buffer.data(), static_cast<std::uint32_t>(Buffer.size() * 4u));
const auto texture = TextureDescriptor(Texels.data());
const auto sampler = SamplerDescriptor();
std::copy(buffer.begin(), buffer.end(), userData.begin());
std::copy(texture.begin(), texture.end(), userData.begin() + 4);
std::copy(sampler.begin(), sampler.end(), userData.begin() + 12);
const std::span<const std::uint32_t> code(Code);
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
ShaderRecompiler::RecompileRequest request{
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
device.Target(),
{0, 0, 0, 128}
};
request.useCache = false;
const auto result = ShaderRecompiler::Recompile(request);
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
device.WaitIdle();
}
void Check() {
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
const auto sample = SampleOf(tid);
const std::array<std::uint32_t, 4> expected{sample.level, sample.face, 0u, 255u};
for (std::uint32_t component = 0; component < 4u; ++component) {
const float value = std::bit_cast<float>(Buffer[tid * Words + ResultWord + component]) * 255.0f;
Require(std::lround(value) == static_cast<long>(expected[component]), "image_sample_d cube: thread " + std::to_string(tid) + " (face " + std::to_string(sample.face) + ", level " + std::to_string(sample.level) + ") component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component]));
}
}
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
FillInput();
FillTexture();
Run(*device);
Check();
std::puts("image sample cube derivatives tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -181,7 +181,7 @@ std::uint32_t CubeLayer(SpirvEmitterState& state, std::uint32_t value) {
return result;
}
std::uint32_t CoordF32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t first, std::uint32_t components, std::uint32_t encoded) {
std::uint32_t CoordF32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t first, std::uint32_t components, std::uint32_t encoded, bool gradient = false) {
const bool cube = access.image.cube;
const auto present = std::min(encoded, components);
if (first == NoImageComponent || (encoded < components && (cube || encoded == 0u)) || access.mem.imageAddressComponents < first + present) {
@ -193,7 +193,7 @@ std::uint32_t CoordF32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access
return x;
}
auto y = component(1u);
if (cube) {
if (cube && !gradient) {
x = CubeAxis(ctx.state, x);
y = CubeAxis(ctx.state, y);
}
@ -1416,8 +1416,8 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con
std::vector<std::uint32_t> operands;
if (HasFlag(mem, RdnaImageSampleFlagDerivative)) {
operandMask |= spv::ImageOperandsGradMask;
operands.push_back(CoordF32(ctx, access, setup.layout.gradX, setup.dimensionInfo.spatialComponents, AddressDimension(access).spatialComponents));
operands.push_back(CoordF32(ctx, access, setup.layout.gradY, setup.dimensionInfo.spatialComponents, AddressDimension(access).spatialComponents));
operands.push_back(CoordF32(ctx, access, setup.layout.gradX, setup.dimensionInfo.spatialComponents, AddressDimension(access).spatialComponents, true));
operands.push_back(CoordF32(ctx, access, setup.layout.gradY, setup.dimensionInfo.spatialComponents, AddressDimension(access).spatialComponents, true));
} else if (explicitLod) {
operandMask |= spv::ImageOperandsLodMask;
operands.push_back(HasFlag(mem, RdnaImageSampleFlagLod) ? AddressF32(ctx, access, setup.layout.lod) : ZeroF32(state));

View File

@ -30,6 +30,7 @@
- [3D image accesses bound to a 2D or 2D-array T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) follow CouchRiv's [hardware oracle](../HW_ORACLE.md) measurement on RDNA2 (gfx1036, #2023) and adriadam10's (gfx1035, #2140), not the PS5's gfx1013. On a 2D T#, `image_sample_lz` ignores r and reads the (u,v) texel, `image_load` and `image_load_mip` read the (x,y) texel when z is 0 and return zero otherwise, and `image_get_resinfo` returns (width, height, 1, levels). On a 2D-array T#, these accesses behave like dim 2D-array: r or z is the layer, and `image_get_resinfo` returns the layer count as its third component. Every other access of such an image, including samples with an LOD, bias, derivatives, offset or comparison, gathers, `image_get_lod` and stores, keeps throwing because it is not measured.
- [Unnormalized S#s](../../../core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp) are accepted only when direct and used only by explicit-LOD `image_sample` without comparison, derivatives or `_a`, on a single-level, single-layer 1D or 2D view of mip 0, even when the image is also sampled through a normalized S#.
- [Unnormalized texel offsets](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) are added to the f32 coordinates: exact on RDNA2 (gfx1035), which applies the offset before the clamp ([measured by adriadam10](https://github.com/boykopovar/AnyPS5/pull/822#issuecomment-6033192725)).
- [Cube image gradients](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (shader recompiler) - `image_sample_d` and `image_sample_cd` on a cube pass their gradients to the host sample without the -1.0 that moves the face coordinates from [1, 2] to [0, 1], as Mesa's AMD lowering builds them (`src/amd/common/nir/ac_nir_lower_tex_coords.c`, `prepare_cube_coords`: face-space derivatives with no offset, coordinates offset by 1.5). Measured on RDNA2 (gfx1035) with the [hardware oracle](../HW_ORACLE.md): on a 16x16 32_FLOAT cube with five levels, point mip filter, `image_sample_d` reads the level of `round(log2(16 * max(|ddx|, |ddy|)))` for gradients of 2^(level ± 0.3) texels on each of the four components, both signs, mixed components, every face and two coordinates (1032 rows). `image_sample_cd` takes one level per quad of lanes, from the gradients of its first lane; the host sample computes it per lane.
- [Image atomics](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) on 32_SINT and 32_FLOAT surfaces act on the raw dword through an R32_UINT storage view; their float results are assumed to match those measured on 32_UINT surfaces. Other formats throw.
- [`s_memtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on RADV with a GFX10.3 to GFX11.7 GPU (RDNA 2, RDNA 3, RDNA 3.5) reads the Vulkan device clock instead of the subgroup clock, which [ACO](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/compiler/instruction_selection/aco_select_nir_intrinsics.cpp) reads from the 20-bit `SHADER_CYCLES` register there and which wraps every 2^20 cycles. The device clock is a monotonic 64-bit counter, but it counts the 100 MHz reference clock (the same one as `s_memrealtime`), not shader cycles, so `s_memtime` deltas are about 20 times smaller than on hardware. The families are matched by the name RADV reports ([`NarrowSubgroupClock`](../../../core/libs/prx/libSceAgcDriver/Execution/src/SubgroupClock.cpp)); a family added to Mesa later reads the subgroup clock until it is listed. AMD's proprietary Windows driver also exposes a 20-bit subgroup clock on device 0x1114 (Radeon 860M, measured in #1615), device 0x744c (Radeon RX 7900 XT, measured in #1632), and device 0x747e (Radeon RX 7800 XT, measured in #1632); other AMD proprietary devices remain unverified.
- [`s_memtime` and `s_memrealtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on llvmpipe read only the low 32 bits of the host's nanosecond clock at either scope: Mesa's [`emit_clock`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/gallium/auxiliary/gallivm/lp_bld_nir_soa.c) builds the high dword with a left shift by 32, so it is always 0 and both counters wrap every 2^32 ns (about 4.3 s). The [shader clock test](../../../core/libs/prx/libSceAgcDriver/tests/execution/ShaderClock.cpp) compares them modulo 2^32 there