From f27bdf4d73f2a44ce03d0cf2b47bbe6db6551255 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Mon, 12 Oct 2026 07:58:49 +0300 Subject: [PATCH 1/3] feat(agc): support 64 KiB display XOR color targets Map SW_64KB_D_X color surfaces through the existing source-backed texture equations and mip geometry. Keep display and render-target equation caches separate. Reject unresolved raw column bits for the new mode instead of accepting them. Cover the captured PPSA99902 descriptor, independent source offsets, pipe/bank tags, transfers, array/mip placement and resident/non-resident GPU draws. Record unmeasured console behavior in TechnicalDebt. CPU validation on main e013f8c9: captured-descriptor failure 5/5; changed passes 5/5; pinned AMD addrlib reference 536940 addresses per run, five runs. Full build and GPU validation are tracked externally. AI-assisted implementation and validation: Codex. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +++ .../Graphics/include/ColorTargetLayout.hpp | 5 +- .../Graphics/src/ColorTargetLayout.cpp | 13 ++--- .../tests/ColorTargetLayout.cpp | 51 +++++++++++++++++++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 19 +++++++ .../tests/execution/Uint8x4ColorTarget.cpp | 27 ++++++---- docs/dev/debt/libSceAgcDriver.md | 1 + 7 files changed, 103 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 24a468603..ddabb2b69 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1168,6 +1168,12 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() set_tests_properties(agc_driver_provoking_vertex PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_uint8x4_color_target_no_resident COMMAND agc_driver_uint8x4_color_target_tests) + set_tests_properties(agc_driver_uint8x4_color_target_no_resident PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_RESIDENT_TARGETS=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_uint8x4_color_target_no_resident PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() set_tests_properties(agc_driver_flat_store PROPERTIES RUN_SERIAL TRUE) set_target_properties(agc_driver_flat_apertures_tests PROPERTIES EXCLUDE_FROM_ALL OFF) foreach(flatAperturesPart RANGE 13) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp index d5a82a6ed..c58694803 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp @@ -18,11 +18,12 @@ enum class ColorTileMode : std::uint32_t { Standard64KB = 9, S4KBX = 0x15, D4KBX = 0x16, + D64KBX = 0x1a, RenderTarget = 0x1b }; constexpr bool ColorTileModeIsXor(ColorTileMode mode) { - return mode == ColorTileMode::S4KBX || mode == ColorTileMode::D4KBX || mode == ColorTileMode::RenderTarget; + return mode == ColorTileMode::S4KBX || mode == ColorTileMode::D4KBX || mode == ColorTileMode::D64KBX || mode == ColorTileMode::RenderTarget; } constexpr std::uint32_t ColorTileModeBlockBytes(ColorTileMode mode) { @@ -30,7 +31,7 @@ constexpr std::uint32_t ColorTileModeBlockBytes(ColorTileMode mode) { } constexpr TextureTileMode ColorTextureTileMode(ColorTileMode mode) { - return mode == ColorTileMode::Linear ? TextureTileMode::kLinear : mode == ColorTileMode::Standard4KB ? TextureTileMode::kStandard4KB : mode == ColorTileMode::Standard64KB ? TextureTileMode::kStandard64KB : mode == ColorTileMode::S4KBX ? TextureTileMode::kS4KBX : mode == ColorTileMode::D4KBX ? TextureTileMode::kD4KBX : TextureTileMode::kR64KBX; + return mode == ColorTileMode::Linear ? TextureTileMode::kLinear : mode == ColorTileMode::Standard4KB ? TextureTileMode::kStandard4KB : mode == ColorTileMode::Standard64KB ? TextureTileMode::kStandard64KB : mode == ColorTileMode::S4KBX ? TextureTileMode::kS4KBX : mode == ColorTileMode::D4KBX ? TextureTileMode::kD4KBX : mode == ColorTileMode::D64KBX ? TextureTileMode::kD64KBX : TextureTileMode::kR64KBX; } ColorTileMode DecodeColorTileMode(std::uint32_t attrib3); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp index 832305195..699325b2e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp @@ -47,7 +47,7 @@ ColorTileMode DecodeColorTileMode(std::uint32_t attrib3) { require((attrib3 & 0x80002000u) == 0 && ((resourceType == 0 && (attrib3 & 0x1fffu) == 0) || resourceType == 1 || resourceType == 2) && ((attrib3 >> 27u) & 7u) == 1, "AGC graphics: unsupported color depth, dimension, resource level or metadata mode"); const auto mode = (attrib3 >> 14u) & 0x1fu; const auto fmaskMode = (attrib3 >> 19u) & 0x1fu; - require(mode == 0 || mode == 5 || mode == 9 || mode == 0x15 || mode == 0x16 || mode == 0x1b, "AGC graphics: unsupported color tile mode"); + require(mode == 0 || mode == 5 || mode == 9 || mode == 0x15 || mode == 0x16 || mode == 0x1a || mode == 0x1b, "AGC graphics: unsupported color tile mode"); require(fmaskMode == 0 || fmaskMode == 0x18 || fmaskMode == (mode & ~3u), "AGC graphics: unsupported color FMASK swizzle mode"); require(resourceType != 0 || mode == 0 || mode == 0x1b, "AGC graphics: 1D color targets with a standard swizzle mode are invalid"); return static_cast(mode); @@ -74,9 +74,9 @@ struct SwizzleTables { }; const SwizzleTables& equationTables(ColorTileMode mode, std::uint32_t bytesPerElement, std::uint32_t blockWidth, std::uint32_t blockHeight) { - static std::once_flag once[3][5]; - static SwizzleTables tables[3][5]; - const auto modeIndex = mode == ColorTileMode::S4KBX ? 0u : mode == ColorTileMode::D4KBX ? 1u : 2u; + static std::once_flag once[4][5]; + static SwizzleTables tables[4][5]; + const auto modeIndex = mode == ColorTileMode::S4KBX ? 0u : mode == ColorTileMode::D4KBX ? 1u : mode == ColorTileMode::D64KBX ? 2u : 3u; const auto index = static_cast(std::countr_zero(bytesPerElement)); std::call_once(once[modeIndex][index], [&] { const auto* equation = FindTextureSwizzleEquation(static_cast(mode), bytesPerElement); @@ -118,6 +118,7 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, require(width != 0 && height != 0 && width <= 16384 && height <= 16384, "AGC graphics: invalid color surface extent"); require(std::has_single_bit(bytesPerElement) && bytesPerElement <= 16u, "AGC graphics: unsupported color element size"); require(pipeBankXor == 0 || (ColorTileModeIsXor(mode) && pipeBankXor < ColorTileModeBlockBytes(mode) && pipeBankXor % 256u == 0), "AGC graphics: a color pipe/bank XOR applies only to whole 256-byte units of XOR swizzle blocks"); + require(mode != ColorTileMode::D64KBX || (pipeBankXor & ~0xcf00u) == 0, "AGC graphics: unsupported display color pipe/bank XOR column bits"); std::uint32_t paddedHeight = height; switch (mode) { case ColorTileMode::Linear: { @@ -127,9 +128,9 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, } case ColorTileMode::S4KBX: case ColorTileMode::D4KBX: + case ColorTileMode::D64KBX: case ColorTileMode::RenderTarget: { - // SW_64KB_R_X: 64 KiB blocks of 2^(16 - log2(bpe)) elements, wider than tall for odd powers. - const auto log2Elements = (mode == ColorTileMode::RenderTarget ? 16u : 12u) - static_cast(std::countr_zero(bytesPerElement)); + const auto log2Elements = static_cast(std::countr_zero(ColorTileModeBlockBytes(mode))) - static_cast(std::countr_zero(bytesPerElement)); blockWidth = 1u << ((log2Elements + 1u) / 2u); blockHeight = 1u << (log2Elements / 2u); pitch = (width + blockWidth - 1u) / blockWidth * blockWidth; diff --git a/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp index 0aebfe320..629659f9e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp @@ -207,4 +207,55 @@ void RunColorTargetLayoutTests() { Require(back == source, "4 KiB XOR color tiling round trip lost pixels"); reject([mode] { static_cast(ColorTargetLayout(40, 20, mode, 4, 0x1000)); }); } + + Require(DecodeColorTileMode(0x0dc68000u) == ColorTileMode::D64KBX, "the observed SW_64KB_D_X color descriptor was rejected"); + Require(ColorTextureTileMode(ColorTileMode::D64KBX) == TextureTileMode::kD64KBX, "a display XOR color target did not map to its texture mode"); + Require(ColorTileModeIsXor(ColorTileMode::D64KBX) && ColorTileModeBlockBytes(ColorTileMode::D64KBX) == 65536u, "a display XOR color target lost its block alignment or XOR flag"); + reject([] { DecodeColorTileMode(0x0cc68000u); }); + reject([] { DecodeColorTileMode((0x0dc68000u & ~(0x1fu << 19u)) | (1u << 19u)); }); + reject([] { ColorTargetLayout(64, 64, ColorTileMode::D64KBX, 4, 0x80); }); + reject([] { ColorTargetLayout(64, 64, ColorTileMode::D64KBX, 4, 0x10000); }); + for (const std::uint32_t columnBits : {0x1000u, 0x2000u, 0x3000u}) reject([columnBits] { ColorTargetLayout(64, 64, ColorTileMode::D64KBX, 4, columnBits); }); + const std::array, 5> sourceOffsets{{{27647u, 66048u}, {29694u, 212992u}, {62204u, 344064u}, {45304u, 540672u}, {110832u, 933888u}}}; + for (const std::uint32_t bpe : {1u, 2u, 4u, 8u, 16u}) { + const ColorTargetLayout sourceLayout(257, 129, ColorTileMode::D64KBX, bpe, 0x4000); + const auto& offsets = sourceOffsets[std::countr_zero(bpe)]; + for (std::uint32_t xorTag = 0; xorTag < 256u; ++xorTag) { + if ((xorTag & ~0xcfu) != 0) continue; + const ColorTargetLayout xorLayout(1, 1, ColorTileMode::D64KBX, bpe, xorTag << 8u); + Require(xorLayout.Offset(0, 0) == (xorTag << 8u), "a display XOR color origin differs from its pipe/bank bits"); + } + Require(sourceLayout.Offset(127, 63) == offsets[0] && sourceLayout.Offset(256, 128) == offsets[1], "display XOR color offsets differ from the AMD GFX10 non-RB+ source reference"); + const auto block = ThinBlockLayout(TextureTileMode::kD64KBX, bpe); + const auto width = 2u * block[1] + 1u; + const auto height = 2u * block[2] + 1u; + for (const auto mode : {ColorTileMode::D64KBX, ColorTileMode::RenderTarget}) { + const auto* equation = FindTextureSwizzleEquation(static_cast(mode), bpe); + Require(equation != nullptr, "a 64 KiB color swizzle equation is missing"); + const ColorTargetLayout layout(width, height, mode, bpe, 0x4600); + Require(layout.Alignment() == block[0] && layout.BlocksPerRow() == 3u && layout.Bytes() == ComputeSurfaceSize(ComputeElementMipLayout(ColorTextureTileMode(mode), bpe, width, height, 1), 1), "a 64 KiB color layout differs from its texture geometry"); + for (const auto x : {0u, 1u, block[1] - 1u, block[1], block[1] + 1u, width - 1u}) { + for (const auto y : {0u, 1u, block[2] - 1u, block[2], block[2] + 1u, height - 1u}) { + std::size_t expected = 0; + for (std::uint32_t bit = 0; bit < 16u; ++bit) expected |= static_cast(std::popcount((x & equation->bits[bit] & 0xfffu) ^ ((y << 12u) & equation->bits[bit] & 0xfff000u)) & 1) << bit; + const auto blockIndex = (y / block[2]) * 3u + x / block[1]; + Require(layout.Offset(x, y) == blockIndex * 65536u + (expected ^ 0x4600u), "a 64 KiB color offset differs from its mode's equation across block boundaries"); + } + } + std::vector source(layout.LinearBytes()); + for (std::size_t index = 0; index < source.size(); ++index) source[index] = static_cast((index * 31u + index / 251u) & 255u); + std::vector tiled(layout.Bytes()); + std::vector restored(source.size()); + layout.Tile(source, tiled); + layout.Detile(tiled, restored); + Require(restored == source, "64 KiB display/render-target XOR color tiling lost pixels"); + } + } + std::fill(guest.begin(), guest.end(), std::byte{0x6b}); + target = {reinterpret_cast(guest.data()), {2, 2}, VK_FORMAT_R8G8B8A8_UNORM, guest.size(), 0xe4, ColorTileMode::D64KBX}; + target.pipeBankXor = 0x0200; + WriteColorTarget(target, pixels); + readback.fill(std::byte{0}); + ReadColorTarget(target, readback); + Require(readback == pixels && guest[0] == std::byte{0x6b}, "display XOR guest color transfer lost pixels or wrote the untagged block origin"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 935796066..10f88a9bb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -814,6 +814,25 @@ void ColorPipeBankXorTests() { const auto color = AgcDriver::Graphics::DecodeState(queue).color; Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::D4KBX && color.address == block + 0x3000u && color.pipeBankXor == pipeBankXor, "a SW_4KB_D_X color base did not split into its 4 KiB block base and pipe/bank XOR " + std::to_string(pipeBankXor)); } + queue.context[0x3b8] = 0x0dc68000u; + for (const std::uint32_t pipeBankXor : {0u, 0x4600u, 0xcf00u}) { + const auto address = block + pipeBankXor; + queue.context[0x318] = static_cast(address >> 8u); + queue.context[0x390] = static_cast(address >> 40u); + const auto color = AgcDriver::Graphics::DecodeState(queue).color; + Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::D64KBX && color.address == block && color.surfaceAddress == block && color.pipeBankXor == pipeBankXor, "a SW_64KB_D_X color base did not split its block address and XOR"); + } + static std::vector mipStorage(16u * blockBytes); + const auto mipBase = (reinterpret_cast(mipStorage.data()) + blockBytes - 1u) / blockBytes * blockBytes; + const auto mips = AgcDriver::Graphics::ComputeElementMipLayout(AgcDriver::Graphics::TextureTileMode::kD64KBX, 4, 256, 128, 5); + const auto sliceBytes = AgcDriver::Graphics::ComputeSurfaceSize(mips, 1); + queue.context[0x318] = static_cast((mipBase + 0x0200u) >> 8u); + queue.context[0x390] = static_cast((mipBase + 0x0200u) >> 40u); + queue.context[0x3b0] = (4u << 28u) | (255u << 14u) | 127u; + queue.context[0x3b8] |= 1u; + queue.context[0x31b] = (2u << 26u) | 1u | (1u << 13u); + const auto mipColor = AgcDriver::Graphics::DecodeColorBuffer(queue.context, 0); + Require(mipColor.mip == 2 && mipColor.mipCount == 5 && mipColor.extent.width == 64 && mipColor.extent.height == 32 && mipColor.surfaceAddress == mipBase + sliceBytes && mipColor.address == mipBase + sliceBytes + mips[2].tiledOffset && mipColor.mipTail == mips[2].tail && mipColor.pipeBankXor == 0x0200u, "a display XOR color view lost its array slice, mip placement or XOR"); } void HiddenUserWordVertexTablesTests() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Uint8x4ColorTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Uint8x4ColorTarget.cpp index f03fe5dbb..8576fd86e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Uint8x4ColorTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Uint8x4ColorTarget.cpp @@ -133,17 +133,18 @@ struct Block { std::uint8_t* data = nullptr; }; -AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t info) { +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t info, AgcDriver::Graphics::ColorTileMode mode, std::uint32_t pipeBankXor) { AgcDriver::Registers cx; - cx[0x318] = static_cast(block.Address() >> 8u); + const auto address = block.Address() + pipeBankXor; + cx[0x318] = static_cast(address >> 8u); cx[0x31b] = 0; cx[0x31c] = info; cx[0x31d] = 0; cx[0x323] = 0; cx[0x324] = 0; cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); - cx[0x3b8] = LinearAttrib3; - cx[0x390] = static_cast(block.Address() >> 40u); + cx[0x3b8] = LinearAttrib3 | (static_cast(mode) << 14u); + cx[0x390] = static_cast(address >> 40u); auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); color.exportIndex = 0; color.uintExport = true; @@ -218,15 +219,17 @@ std::vector ReadBack(AgcDriver::VulkanDevice& device, std::uint64_ return stored; } -void Expect(AgcDriver::VulkanDevice& device, std::uint32_t info, VkFormat format, std::uint32_t elementBytes, std::array exported, const std::string& what) { +void Expect(AgcDriver::VulkanDevice& device, std::uint32_t info, VkFormat format, std::uint32_t elementBytes, std::array exported, const std::string& what, AgcDriver::Graphics::ColorTileMode mode = AgcDriver::Graphics::ColorTileMode::Linear, std::uint32_t pipeBankXor = 0) { const auto bytes = static_cast(Width) * Height * elementBytes; - Require(AgcDriver::Graphics::ColorTargetLayout(Width, Height, AgcDriver::Graphics::ColorTileMode::Linear, elementBytes).Bytes() == bytes, what + ": the linear target is padded"); - Block block(bytes); - std::memset(block.data, Kept, bytes); - const auto color = DecodeTarget(block, info); - Require(color.format == format && color.elementBytes == elementBytes && color.bytes == bytes, what + ": the target decoded to the wrong format"); + const AgcDriver::Graphics::ColorTargetLayout layout(Width, Height, mode, elementBytes, pipeBankXor); + Block block(layout.Bytes()); + std::memset(block.data, Kept, block.bytes); + const auto color = DecodeTarget(block, info, mode, pipeBankXor); + Require(color.format == format && color.elementBytes == elementBytes && color.bytes == layout.Bytes() && color.tileMode == mode && color.pipeBankXor == pipeBankXor, what + ": the target decoded to the wrong format, mode or XOR"); Draw(device, color, Program(exported)); - const auto stored = ReadBack(device, block.Address(), bytes); + const auto tiled = ReadBack(device, block.Address(), block.bytes); + std::vector stored(bytes); + layout.Detile(std::as_bytes(std::span(tiled)), std::as_writable_bytes(std::span(stored))); for (std::size_t offset = 0; offset < bytes; ++offset) { const bool inside = (offset / elementBytes) % Width < ScissorWidth; const auto expected = inside ? static_cast(exported[offset % elementBytes]) : Kept; @@ -241,6 +244,8 @@ int main() { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; Expect(*device, Rgba8UintInfo, VK_FORMAT_R8G8B8A8_UINT, 4, {0x12u, 0xbeu, 0x42u, 0x7fu}, "a UINT16_ABGR export into an 8_8_8_8_UINT target"); + Expect(*device, Rgba8UintInfo, VK_FORMAT_R8G8B8A8_UINT, 4, {0x23u, 0x9au, 0x65u, 0x81u}, "a UINT16_ABGR export into a SW_64KB_D_X target", AgcDriver::Graphics::ColorTileMode::D64KBX); + Expect(*device, Rgba8UintInfo, VK_FORMAT_R8G8B8A8_UINT, 4, {0x31u, 0x7cu, 0x42u, 0x96u}, "a UINT16_ABGR export into a SW_64KB_D_X target with XOR", AgcDriver::Graphics::ColorTileMode::D64KBX, 0x4600); std::puts("8_8_8_8_UINT color target tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 0a8e13c84..4207abc6b 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -25,6 +25,7 @@ ### Functional - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. Matching depth/stencil storage access still throws. +- [SW_64KB_D_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the existing texture block/mip geometry and equations from [AMD addrlib GFX10 non-RB+](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) for 16 pipes and 256-byte interleave. The address-reference tests use legal pipe/bank XOR bits for that configuration. Raw column bits outside the source configuration's pipe/bank mask 0xcf00 throw; their console meaning is unresolved. Console address equations and render results have not been measured. - [Per-face depth bias](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): Vulkan has one depth bias per draw, so a draw that rasterizes both faces with different front and back bias (PA_SU_SC_MODE_CNTL, PA_SU_POLY_OFFSET_FRONT/BACK_*) is recorded twice, front faces with the back faces culled and then back faces with the front faces culled. The vertex stages run twice, so a vertex shader that writes memory writes twice, and all front faces land before all back faces instead of in primitive order, which differs only for blending or stencil updates that depend on the order. - [Partial rect-list primitives](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): a rect-list draw whose vertex or index count is not a multiple of 3 draws its complete rectangles and leaves out the 1 or 2 vertices after them, because rect lists are drawn as Vulkan patch lists of 3 control points, which generate [⌊vertexCount/3⌋ patches](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#drawing-patch-lists). What the console draws for the incomplete rectangle is not measured; Palworld (PPSA23126) draws 4 vertices × 9 instances as a rect list when its character creation opens. - [Separate alpha blend into 8_8 SWAP_ALT targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver): the second channel of a two-channel target with `COMP_SWAP` 1 holds the alpha export, but Vulkan blends it with the color equation of the R8G8 attachment, so a draw whose alpha equation differs from its color equation is recorded twice, the first channel with the color equation and the second with the alpha factors and operation moved into the color equation. The vertex stages run twice, so a vertex shader that writes memory writes twice, and occlusion counts include both passes. Draws that write depth or stencil, pixel shaders that write memory and factors of the destination alpha throw. That the hardware blends the second channel with the alpha equation is taken from Mesa's `si_translate_colorswap` and radeonsi's use of it for luminance-alpha formats, not measured on hardware. From 4326c7c21293cb308b9f592f45bd8b450c6c5e12 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Mon, 12 Oct 2026 08:16:24 +0300 Subject: [PATCH 2/3] fix(agc): validate display XOR bits before resident lookup Apply the source-configured D_X XOR refusal during color decoding as well as direct layout construction. Regression tests reject each raw column-bit combination before storage or rendering is selected. Existing resident-array and packed-mip refusal paths remain in force. AI-assisted implementation and validation: Codex. --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 1 + core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index a5f5d4407..f151a5f0b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1040,6 +1040,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { color.pipeBankXor = static_cast(color.surfaceAddress & (ColorTileModeBlockBytes(color.tileMode) - 1u)); color.surfaceAddress -= color.pipeBankXor; } + Require(color.tileMode != ColorTileMode::D64KBX || (color.pipeBankXor & ~0xcf00u) == 0, "unsupported display color pipe/bank XOR column bits"); color.arrayAddress = color.arrayLastSlice != 0 ? color.surfaceAddress : 0; if (slice != 0 && !volume) color.surfaceAddress += slice * ComputeSurfaceSize(ComputeElementMipLayout(ColorTextureTileMode(color.tileMode), color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount), 1); color.address = color.surfaceAddress + mipOffset; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 10f88a9bb..a66d48247 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -822,6 +822,11 @@ void ColorPipeBankXorTests() { const auto color = AgcDriver::Graphics::DecodeState(queue).color; Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::D64KBX && color.address == block && color.surfaceAddress == block && color.pipeBankXor == pipeBankXor, "a SW_64KB_D_X color base did not split its block address and XOR"); } + for (const std::uint32_t columnBits : {0x1000u, 0x2000u, 0x3000u}) { + queue.context[0x318] = static_cast((block + columnBits) >> 8u); + queue.context[0x390] = static_cast((block + columnBits) >> 40u); + expectFailure([&queue] { AgcDriver::Graphics::DecodeColorBuffer(queue.context, 0); }, "unsupported display color pipe/bank XOR column bits"); + } static std::vector mipStorage(16u * blockBytes); const auto mipBase = (reinterpret_cast(mipStorage.data()) + blockBytes - 1u) / blockBytes * blockBytes; const auto mips = AgcDriver::Graphics::ComputeElementMipLayout(AgcDriver::Graphics::TextureTileMode::kD64KBX, 4, 256, 128, 5); From b6f1a4a69dab38d0f95495a7e1e4243cae9d7dc3 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Mon, 12 Oct 2026 08:18:18 +0300 Subject: [PATCH 3/3] test(agc): verify display XOR array attachment transport Run the existing array-layer draw, recompiled shader load, untouched-layer and guest-publication checks with D_X as well as R_X. This exercises the resident array path that carries slice-dependent address XOR; a direct per-slice copy is not an equivalent model. AI-assisted implementation and validation: Codex. --- .../tests/execution/ArrayLayerTarget.cpp | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp index 53887c1b4..6b5a49e90 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp @@ -113,12 +113,12 @@ private: std::uint8_t* block = nullptr; }; -std::array ArrayDescriptor(std::uint64_t address) { +std::array ArrayDescriptor(std::uint64_t address, std::uint32_t swizzleMode) { return { static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (Format32Float << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), - 0xfacu | (TileR64KBX << 20u) | (Type2DArray << 28u), + 0xfacu | (swizzleMode << 20u) | (Type2DArray << 28u), Layers - 1u, 0u, 0u, @@ -146,13 +146,13 @@ void Dispatch(AgcDriver::VulkanDevice& device, std::span co device.WaitIdle(); } -void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer) { +void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer, std::uint32_t swizzleMode) { Output.fill(0xdeadbeefu); auto& code = LoadCodes[layer]; code = {0x7e020280, 0x7e040280u | layer, 0xf0001128, 0x00010300, 0x34080082, 0xbf8c3f70, 0xe0701000, 0x80000304, 0xbf810000}; std::vector userData(16, 0u); const auto buffer = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); - const auto texture = ArrayDescriptor(surface); + const auto texture = ArrayDescriptor(surface, swizzleMode); std::copy(buffer.begin(), buffer.end(), userData.begin()); std::copy(texture.begin(), texture.end(), userData.begin() + 4); Dispatch(device, code, userData); @@ -165,14 +165,14 @@ void RequireOutput(std::uint32_t inside, std::uint32_t outside, const std::strin } } -AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice) { +AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice, std::uint32_t swizzleMode) { AgcDriver::Registers cx; cx[0x318] = static_cast(address >> 8u); cx[0x31b] = slice | (slice << 13u); cx[0x31c] = Color32FloatInfo; cx[0x31d] = 0; cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); - cx[0x3b8] = TiledAttrib3 | (Layers - 1u); + cx[0x3b8] = (TiledAttrib3 & ~(0x1fu << 14u)) | (swizzleMode << 14u) | (Layers - 1u); cx[0x390] = static_cast(address >> 40u); const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); Require(color.format == VK_FORMAT_R32_SFLOAT && color.bytes == LayerBytes && color.address == address + slice * LayerBytes, "the COLOR_32 float view did not decode as a 128x128 R32 slice of the array"); @@ -180,7 +180,7 @@ AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32 return color; } -void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice) { +void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice, std::uint32_t swizzleMode) { const auto target = device.Target(); constexpr std::uint32_t waveSize = 64; std::size_t at = 0; @@ -207,7 +207,7 @@ void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint const auto vertexResult = ShaderRecompiler::Recompile(vertex); const auto vertexPush = static_cast(vertexResult.pushConstants.size()); - const auto color = DecodeTarget(surface, slice); + const auto color = DecodeTarget(surface, slice, swizzleMode); VkPipelineColorBlendAttachmentState blend{}; blend.colorWriteMask = 0xf; AgcDriver::Graphics::State state{}; @@ -246,20 +246,20 @@ void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint device.WaitIdle(); } -void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { +void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) { const auto surface = AddressOf(texels); std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Initial); AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); AgcDriver::GuestMemory::BumpCollectEpoch(); std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); const auto drawn = std::bit_cast(Drawn); - DrawSlice(device, surface, 1); + DrawSlice(device, surface, 1, swizzleMode); const auto pending = StorageTexture::FindPending(surface, SurfaceBytes); Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray && pending->Descriptor().baseAddress == surface && pending->Descriptor().depthOrLastArray == Layers - 1u, "the draw into slice 1 did not render into the image of its array"); - LoadLayer(device, surface, 1); + LoadLayer(device, surface, 1, swizzleMode); RequireOutput(drawn, Initial, "slice 1 read through the array after the draw"); for (const std::uint32_t other : {0u, 2u, 3u}) { - LoadLayer(device, surface, other); + LoadLayer(device, surface, other, swizzleMode); RequireOutput(Initial, Initial, "slice " + std::to_string(other) + " after the draw into slice 1"); } StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test"); @@ -267,7 +267,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { AgcDriver::Graphics::ClearCachedTextures(device.Device()); AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); AgcDriver::GuestMemory::BumpCollectEpoch(); - LoadLayer(device, surface, 1); + LoadLayer(device, surface, 1, swizzleMode); RequireOutput(drawn, Initial, "slice 1 uploaded again from guest memory"); AgcDriver::Graphics::ClearCachedTextures(device.Device()); } @@ -279,7 +279,8 @@ int main() { GuestBlock block; const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device, block.Data()); + Run(*device, block.Data(), TileR64KBX); + Run(*device, block.Data(), static_cast(AgcDriver::Graphics::ColorTileMode::D64KBX)); std::puts("array layer target tests passed"); return 0; } catch (const std::exception& error) {