diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleCubeDerivatives.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleCubeDerivatives.cpp new file mode 100644 index 000000000..b44e5d242 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleCubeDerivatives.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +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 Buffer{}; +alignas(256) std::array Texels{}; + +alignas(256) constexpr std::array 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(value); +} + +std::array TextureDescriptor(const void* data) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((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(sample.level) - 0.25f) / static_cast(Size) : 0.0f); + } + words[4] = Bits(1.0f + (static_cast(sample.x) + 0.5f) / static_cast(Size)); + words[5] = Bits(1.0f + (static_cast(sample.y) + 0.5f) / static_cast(Size)); + words[6] = Bits(static_cast(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(y) * mip.pitchBytes + x * 4u]; + texel[0] = static_cast(level); + texel[1] = static_cast(face); + texel[2] = 0u; + texel[3] = 255u; + } + } + } + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array SamplerDescriptor() { + return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void Run(AgcDriver::VulkanDevice& device) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(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 code(Code); + const std::array memory{{{reinterpret_cast(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(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(code.data())); + device.WaitIdle(); +} + +void Check() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sample = SampleOf(tid); + const std::array expected{sample.level, sample.face, 0u, 255u}; + for (std::uint32_t component = 0; component < 4u; ++component) { + const float value = std::bit_cast(Buffer[tid * Words + ResultWord + component]) * 255.0f; + Require(std::lround(value) == static_cast(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; + } +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index aaac7c56a..93902d26d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -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 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)); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index e0109d65c..a983e89d3 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -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