From eb2c43a2ebbc237d15e8785dda877a6026cc6c4e Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:54:50 +0200 Subject: [PATCH] fix(shader): sample a 1D T# through a 2D implicit-LOD image_sample A plain image_sample that says DIM 2D on a 1D T# takes its texel and its LOD from the first coordinate alone, measured on RDNA2 (gfx1036) with the hardware oracle. Astro's Playroom draws with such a pair. --- .../ImagePlaneAddressOnLineImplicitLod.cpp | 196 ++++++++++++++++++ .../Optimization/src/ResourceTracker.cpp | 2 +- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLineImplicitLod.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLineImplicitLod.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLineImplicitLod.cpp new file mode 100644 index 000000000..cc39b8471 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLineImplicitLod.cpp @@ -0,0 +1,196 @@ +#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 Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Mips = 5; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type1D = 8; +constexpr float SecondCoordinateScale = 3.0f; +alignas(256) std::array Pixels{}; +alignas(256) std::array Line{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x10040010, 0x06060300, 0x10060611, 0xf0800f08, 0x00610402, 0xbf8c3f70, 0xf800180f, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array BiasPixelCode{ + 0x10040010, 0x06060300, 0x10060611, 0xf0940f08, 0x00610401, 0xbf8c3f70, 0xf800180f, 0x07060504, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}}}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::array LineDescriptor() { + const auto address = static_cast(reinterpret_cast(Line.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + (Size - 1u) >> 2u, + 0xfacu | ((Mips - 1u) << 16u) | (Type1D << 28u), + 0u, + (Mips - 1u) << 4u, + 0u, + 0u, + }; +} + +std::array SamplerDescriptor() { + return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +std::uint8_t LevelMark(std::uint32_t level) { + return static_cast(10u + 40u * level); +} + +void FillTexture() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(LineDescriptor())); + Require(geometry.guestBytes <= Line.size(), "implicit LOD through a 1D texture: the texture does not fit the texel storage"); + Require(geometry.mips.size() == Mips, "implicit LOD through a 1D texture: the texture has " + std::to_string(geometry.mips.size()) + " mips"); + std::fill(Line.begin(), Line.end(), 0xeeu); + for (std::uint32_t level = 0; level < Mips; ++level) { + const auto& mip = geometry.mips[level]; + for (std::uint32_t x = 0; x < (Size >> level); ++x) { + auto* texel = &Line[geometry.GuestLayerOffset(0) + mip.tiledOffset + x * 4u]; + texel[0] = LevelMark(level); + texel[1] = static_cast(x * 8u); + texel[2] = 99u; + texel[3] = 255u; + } + } +} + +void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, float firstCoordinateScale) { + const auto& target = device.Target(); + Pixels.fill(std::byte{0}); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {64, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = false; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + std::vector pixelUserData(18, 0u); + const auto texture = LineDescriptor(); + const auto sampler = SamplerDescriptor(); + std::copy(texture.begin(), texture.end(), pixelUserData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), pixelUserData.begin() + 12); + pixelUserData[16] = std::bit_cast(firstCoordinateScale); + pixelUserData[17] = std::bit_cast(SecondCoordinateScale); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {64, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(std::uint32_t level) { + const auto what = "image_sample 2d on a 1D texture with the first coordinate advancing " + std::to_string(1u << level) + " texels per pixel"; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const std::array expected{LevelMark(level), (x % (Size >> level)) * 8u, 99u, 255u}; + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto value = std::to_integer(Pixels[(y * Width + x) * 4u + component]); + Require(value == expected[component], what + ": pixel " + std::to_string(x) + "," + std::to_string(y) + " component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + } + } + } +} + +void RequireRefused(AgcDriver::VulkanDevice& device) { + std::string refusal; + try { + Draw(device, BiasPixelCode, 1.0f / static_cast(Size)); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find("incompatible with the static runtime image interface") != std::string::npos, "image_sample_b 2d on a 1D texture was not refused: " + refusal); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexture(); + for (std::uint32_t level = 0; level < 3u; ++level) { + Draw(*device, PixelCode, static_cast(1u << level) / static_cast(Size)); + Check(level); + } + RequireRefused(*device); + std::puts("image plane address on line implicit LOD tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 20be15285..16807e7d1 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -1108,7 +1108,7 @@ private: const bool flatVolumeSample = op == IrOpcode::ImageSampleRaw && memory.imageSampleFlags == RdnaImageSampleFlagLevelZero; image.flatVolumeCompatible = image.flatVolumeCompatible && (flatVolumeLoad || flatVolumeSample) && !memory.imagePacked && memory.imageByElements == 0u; const bool flatLineLoad = op == IrOpcode::ImageRead && !memory.imageHasMip && memory.imageSampleFlags == 0u; - const bool flatLineSample = op == IrOpcode::ImageSampleRaw && memory.imageSampleFlags == RdnaImageSampleFlagLevelZero; + const bool flatLineSample = op == IrOpcode::ImageSampleRaw && (memory.imageSampleFlags == RdnaImageSampleFlagLevelZero || memory.imageSampleFlags == 0u); image.flatLineCompatible = image.flatLineCompatible && (flatLineLoad || flatLineSample) && !memory.imagePacked && memory.imageByElements == 0u; if ((memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0u) { constexpr auto unsupported = RdnaImageSampleFlagLod | RdnaImageSampleFlagDerivative; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 237d82bed..44b985f26 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -381,7 +381,7 @@ Throughout the project, every function at every stage either **does exactly what - A value read through a null or unmapped pointer still throws at record time if a [branch condition, uniform fill or table base](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) needs it, or if the pointer comes from user data and is not null, even if the console skips the read. - A [V# or flat SRT word](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) read through a null pointer taken from user data is zero and counts as a [poisoned SRT read](../../core/shader/recompiler/Recompiler.cpp), so a dispatch that reads it is not cached. The console faults if the program executes the read; PPSA28416 branches around it with `s_cbranch_execz`. The fault is not modeled. - A [V#, T# or S#](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) loaded through a null or unmapped pointer read from memory is zero when the work is recorded, and the capture throws if no flat SRT slot records that read. -- [2D accesses through a 1D T#](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read the texel the 1D addressing selects and ignore the second coordinate, as simonbbby measured on RDNA2 (gfx1036) in #2237 for `image_sample_lz` and mip-less `image_load` with point filtering and wrap, clamp and border addressing; the PS5's gfx1013 is not measured. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips, derivatives, comparison) still throw. +- [2D accesses through a 1D T#](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read the texel the 1D addressing selects and ignore the second coordinate, as simonbbby measured on RDNA2 (gfx1036) in #2237 for `image_sample_lz` and mip-less `image_load` with point filtering and wrap, clamp and border addressing; the PS5's gfx1013 is not measured. A plain `image_sample` (implicit LOD) through such a pair takes its LOD from the derivatives of the first coordinate alone, measured on RDNA2 (gfx1036) with the [hardware oracle](HW_ORACLE.md) on the quads of a compute kernel (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): on a 16-texel, 5-mip 32_UINT T# with point filters it picks the same mip and texel as DIM 1D for first-coordinate steps of 0 to 8 texels per lane in either direction, whatever the second coordinate does (steps up to 16 per lane), while the same rows on a 16x16 2D T# move the mip with the second coordinate. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips, explicit LOD, bias, offsets, derivatives, comparison) still throw. - [Emulated comparison of a 2D-array access through a 2D T#](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) samples the 2D image and ignores the slice, as the native modes of a 2D-array access through a 2D T# already do. This follows the T# type deciding the addressing; it is not measured on hardware for comparison samples - [Scalar aperture base sources](../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler) - `src_shared_base`, `src_shared_limit`, `src_private_base` and `src_private_limit` (scalar source codes 235 to 238) are not modelled: decoding throws `unsupported scalar source operand code` with the code and the program counter, so a shader that reads one does not compile. Operand 235 (`src_shared_base`) was reported on [#1182](https://github.com/boykopovar/AnyPS5/pull/1182) from PPSA26344; the flat aperture routing does not read the base, so a shader that builds its own flat addresses from it stops at decode. - [Flat aperture bases](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035: `src_shared_base` 0x00010000_00000000, `src_shared_limit` 0x00010000_ffffffff). The PS5's base values, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), are inferred from GTA V's traversal addresses, not measured on a console or read from SH_MEM_BASES