Merge pull request #3636 from JAB-dev/feat/agc-color-target-1-5-5-5

feat(agc): render 1_5_5_5 UNORM color targets
This commit is contained in:
Dean 2026-10-12 23:48:42 +13:00 committed by GitHub
commit e9d35d6491
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 326 additions and 0 deletions

View File

@ -990,6 +990,8 @@ target_include_directories(agc_driver_swap_alt_blend_tests PRIVATE $<TARGET_PROP
target_include_directories(agc_driver_packed_unorm_color_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_color1555_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
foreach(stage IN ITEMS vert frag)
set(agcProvokingShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/ProvokingVertex.${stage})
set(agcProvokingShaderSpv ${agcTestShaderGeneratedRoot}/ProvokingVertex_${stage}.spv)

View File

@ -541,6 +541,9 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number,
if (number == floating) return {VK_FORMAT_R32G32B32A32_SFLOAT, 16};
if (number == uint) return {VK_FORMAT_R32G32B32A32_UINT, 16};
return fail();
case 17:
if (swap != 0 || number != unorm) return fail();
return {VK_FORMAT_A1R5G5B5_UNORM_PACK16, 2, 0xc6u};
default:
return fail();
}

View File

@ -1088,6 +1088,26 @@ void ReversedComponentOrderTests() {
}
}
void Color1555TargetTests() {
auto queue = makeState();
queue.context[0x31c] = (queue.context[0x31c] & ~((0x1fu << 2u) | (7u << 8u) | (3u << 11u))) | (17u << 2u);
const auto state = AgcDriver::Graphics::DecodeState(queue);
Require(state.colors.size() == 1 && state.colors[0].format == VK_FORMAT_A1R5G5B5_UNORM_PACK16 && state.colors[0].elementBytes == 2 && state.colors[0].componentMapping == 0xc6u, "a 1_5_5_5 target did not render through A1R5G5B5 with X and Z exchanged");
Require(AgcDriver::Graphics::ExportMappings(state)[0] == 0xc6u && state.blends[0].colorWriteMask == 0xfu, "a 1_5_5_5 target did not write all four channels through its export mapping");
queue.context[0x8e] = 0x1u;
Require(AgcDriver::Graphics::DecodeState(queue).blends[0].colorWriteMask == VK_COLOR_COMPONENT_B_BIT, "a 1_5_5_5 target writing only X did not write the A1R5G5B5 channel in its low bits");
queue.context[0x8e] = 0xfu;
for (std::uint32_t i = 0; i < 4; ++i) queue.context[0x105 + i] = 0;
queue.context[0x1e0] = 0x40000504u;
const auto blended = AgcDriver::Graphics::DecodeState(queue);
Require(blended.blends[0].blendEnable && blended.blends[0].srcColorBlendFactor == VK_BLEND_FACTOR_SRC_ALPHA && blended.blends[0].dstColorBlendFactor == VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, "alpha blending into a 1_5_5_5 target was not decoded");
queue.context[0x1e0] = 0;
for (const auto& [number, swap] : {std::pair{1u, 0u}, std::pair{4u, 0u}, std::pair{0u, 1u}}) {
queue.context[0x31c] = (queue.context[0x31c] & ~((7u << 8u) | (3u << 11u))) | (number << 8u) | (swap << 11u);
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported color format 17 number type " + std::to_string(number) + " component swap " + std::to_string(swap));
}
}
void CompactedExportTests() {
alignas(256) static std::array<std::byte, 1024> slotFourMemory{};
const auto slotFour = reinterpret_cast<std::uintptr_t>(slotFourMemory.data());
@ -4940,6 +4960,7 @@ int main(int argc, char** argv) {
ConservativeRasterizationTests();
DisabledColorTests();
OpaqueDestinationAlphaTests();
Color1555TargetTests();
CompactedExportTests();
ReversedComponentOrderTests();
ShadersWriteMemoryTests();

View File

@ -0,0 +1,299 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "prx/libc/include/GuestArena.hpp"
#include "prx/libc/include/GuestWriteWatch.hpp"
#include "Recompiler.hpp"
#include "Optimization/ResourceProgram.hpp"
#include "VulkanTestDevice.hpp"
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <sys/mman.h>
#endif
#include <array>
#include <bit>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <span>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::ColorTargetLayout;
using AgcDriver::Graphics::ColorTileMode;
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Width = 256;
constexpr std::uint32_t Height = 128;
constexpr std::uint32_t ScissorWidth = 128;
constexpr std::size_t BlockBytes = 65536;
constexpr std::uint32_t Color1555Info = 0x8044u;
constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u;
constexpr std::uint32_t LinearAttrib3 = 0x09000000u;
constexpr std::uint32_t Float32Export = 9;
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
};
constexpr std::array<std::array<float, 4>, 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}
}};
alignas(256) std::array<std::array<std::uint32_t, 64>, 16> Programs{};
std::size_t programCount = 0;
using Color = std::array<float, 4>;
std::uint16_t Texel(const Color& color) {
const auto unorm = [](float value, std::uint32_t maximum) { return static_cast<std::uint32_t>(std::lround(std::fmin(std::fmax(static_cast<double>(value), 0.0), 1.0) * maximum)); };
return static_cast<std::uint16_t>(unorm(color[0], 31) | (unorm(color[1], 31) << 5u) | (unorm(color[2], 31) << 10u) | (unorm(color[3], 1) << 15u));
}
std::span<const std::uint32_t> Program(const Color& color) {
Require(programCount < Programs.size(), "too many test programs");
auto& code = Programs[programCount++];
std::size_t at = 0;
for (std::uint32_t channel = 0; channel < 4; ++channel) {
code[at++] = 0x7e0002ffu | ((4u + channel) << 17u);
code[at++] = std::bit_cast<std::uint32_t>(color[channel]);
}
code[at++] = 0xf800180fu;
code[at++] = 0x07060504u;
code[at++] = 0xbf810000u;
return std::span<const std::uint32_t>(code.data(), at);
}
struct Block {
explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) {
#ifdef _WIN32
if (watched) {
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
} else {
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
}
#else
if (watched) {
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
Require(raw != MAP_FAILED, "cannot map the color target");
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
if (aligned != begin) munmap(raw, aligned - begin);
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
data = reinterpret_cast<std::uint8_t*>(aligned);
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
} else {
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
}
#endif
Require(data != nullptr, "cannot allocate the color target");
Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched");
GuestAllocations::Mutation mutation;
mutation.Add(data, bytes, true, true, true);
}
~Block() {
AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release");
{
GuestAllocations::Mutation mutation;
mutation.Remove(data);
}
#ifdef _WIN32
if (watched) {
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
} else {
VirtualFree(data, 0, MEM_RELEASE);
}
#else
if (watched) {
munmap(data, bytes);
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
} else {
std::free(data);
}
#endif
}
Block(const Block&) = delete;
Block& operator=(const Block&) = delete;
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
std::size_t bytes;
bool watched;
std::uint8_t* data = nullptr;
};
AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t attrib3) {
AgcDriver::Registers cx;
cx[0x318] = static_cast<std::uint32_t>(block.Address() >> 8u);
cx[0x31b] = 0;
cx[0x31c] = Color1555Info;
cx[0x31d] = 0;
cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u);
cx[0x3b8] = attrib3;
cx[0x390] = static_cast<std::uint32_t>(block.Address() >> 40u);
auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
Require(color.format == VK_FORMAT_A1R5G5B5_UNORM_PACK16 && color.elementBytes == 2 && color.bytes == block.bytes, "the 1_5_5_5 unorm target did not decode as an A1R5G5B5 surface");
return color;
}
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t exportFormat, std::span<const std::uint32_t> pixelCode) {
const auto target = device.Target();
constexpr std::uint32_t waveSize = 64;
std::vector<std::uint32_t> vertexUserData(4, 0u);
const auto address = reinterpret_cast<std::uintptr_t>(Triangle.data());
const std::array<std::uint32_t, 4> vertexBuffer{static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (16u << 16u), static_cast<std::uint32_t>(Triangle.size()), 0x01016facu};
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
ShaderRecompiler::RecompileRequest vertex{
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
{waveSize, 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<std::uint32_t>(vertexResult.pushConstants.size());
AgcDriver::Graphics::State state{};
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt};
state.colors = {color};
state.color = color;
state.hasColorTarget = true;
state.renderExtent = {Width, Height};
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
state.negativeOneToOne = false;
state.scissor = {{0, 0}, {ScissorWidth, Height}};
state.cullMode = VK_CULL_MODE_NONE;
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
VkPipelineColorBlendAttachmentState blend{};
blend.colorWriteMask = 0xf;
state.blends = {blend};
state.blend = blend;
state.blendConstants = {};
const auto codeAddress = reinterpret_cast<std::uintptr_t>(pixelCode.data());
const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}};
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}};
ShaderRecompiler::RecompileRequest fragment{
{ShaderStage::Fragment, codeAddress, pixelCode, 0, {}},
{waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory},
target,
PixelPushLayout(vertexPush, target)
};
const auto handle = ShaderRecompiler::PrepareShader(fragment);
AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}};
snapshot.prepared->entries.push_back({0, handle});
fragment.shader.code = snapshot.code;
fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state));
const ShaderRecompiler::SrtRuntime runtime{};
const auto materialize = [&](const ShaderRecompiler::RecompileRequest& request) {
const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request);
return invocation.Materialize(*invocation.Capture(runtime));
};
const auto pixelResult = materialize(fragment);
Require(pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId, "the draw did not use the registered pixel shader");
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
{ShaderStage::Vertex, &vertexResult, 0},
{ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)}
}};
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangle.size()), 0, 1, 0, false};
device.Draw(state, draw, shaders);
}
std::vector<std::uint16_t> ReadBack(AgcDriver::VulkanDevice& device, const Block& block) {
AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back");
device.WaitIdle();
std::vector<std::uint16_t> stored(block.bytes / 2u);
AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1);
return stored;
}
std::vector<std::uint16_t> Fill(Block& block) {
std::vector<std::uint16_t> halves(block.bytes / 2u);
for (std::size_t index = 0; index < halves.size(); ++index) halves[index] = static_cast<std::uint16_t>((index * 0x9e3779b9u) >> 16u);
std::memcpy(block.data, halves.data(), block.bytes);
return halves;
}
void ExpectSurface(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector<std::uint16_t>& before, std::uint16_t texel, const std::string& what) {
const auto stored = ReadBack(device, block);
for (std::uint32_t y = 0; y < Height; ++y) {
for (std::uint32_t x = 0; x < Width; ++x) {
const auto index = layout.Offset(x, y) / 2u;
const bool inside = x < ScissorWidth;
const auto expected = inside ? texel : before[index];
if (stored[index] == expected) continue;
char detail[160];
std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds 0x%04x, expected 0x%04x", x, y, inside ? "inside" : "outside", stored[index], expected);
throw std::runtime_error(what + detail);
}
}
}
void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t attrib3, ColorTileMode mode, const char* kind) {
const ColorTargetLayout layout(Width, Height, mode, 2);
Require(layout.Bytes() == block.bytes, "the test surface size disagrees with its layout");
const auto color = DecodeTarget(block, attrib3);
auto before = Fill(block);
struct Case {
Color color;
std::uint16_t texel;
};
const std::array<Case, 11> cases{{
{{1.0f, 0.0f, 0.0f, 1.0f}, 0x801f},
{{0.0f, 1.0f, 0.0f, 0.0f}, 0x03e0},
{{0.0f, 0.0f, 1.0f, 0.0f}, 0x7c00},
{{0.0f, 0.0f, 0.0f, 1.0f}, 0x8000},
{{1.0f, 0.0f, 1.0f, 0.0f}, 0x7c1f},
{{1.0f, 1.0f, 1.0f, 1.0f}, 0xffff},
{{0.0f, 0.0f, 0.0f, 0.0f}, 0x0000},
{{3.0f / 31.0f, 10.0f / 31.0f, 21.0f / 31.0f, 1.0f}, (3u) | (10u << 5u) | (21u << 10u) | 0x8000u},
{{21.0f / 31.0f, 3.0f / 31.0f, 10.0f / 31.0f, 0.0f}, (21u) | (3u << 5u) | (10u << 10u)},
{{1.0f / 31.0f, 30.0f / 31.0f, 2.0f / 31.0f, 0.75f}, (1u) | (30u << 5u) | (2u << 10u) | 0x8000u},
{{2.0f, -1.0f, 10.0f / 31.0f, 0.25f}, (31u) | (0u << 5u) | (10u << 10u)},
}};
for (const auto& test : cases) {
Require(Texel(test.color) == test.texel, "the test's expected texel disagrees with the 1_5_5_5 field layout");
Draw(device, color, Float32Export, Program(test.color));
char what[160];
std::snprintf(what, sizeof(what), "a 32_ABGR export of (%.9g, %.9g, %.9g, %.9g) into a %s 1_5_5_5 unorm target", test.color[0], test.color[1], test.color[2], test.color[3], kind);
ExpectSurface(device, block, layout, before, test.texel, what);
before = ReadBack(device, block);
programCount = 0;
}
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
Block tiled(ColorTargetLayout(Width, Height, ColorTileMode::RenderTarget, 2).Bytes());
Block linear(ColorTargetLayout(Width, Height, ColorTileMode::Linear, 2).Bytes());
TargetTests(*device, tiled, TiledAttrib3, ColorTileMode::RenderTarget, "tiled");
TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, "linear");
std::puts("1_5_5_5 unorm color target tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -592,6 +592,7 @@ Throughout the project, every function at every stage either **does exactly what
- [Comp-to-single DCC keys](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - key 0x10 (`GFX9_DCC_CLEAR_SINGLE` in Mesa `ac_descriptors.h`) is expanded only by a CB DCC decompress pass over uniform keys of a non-linear target: every texel takes the element at the start of its 256-byte block, the top-left texel of the block where radv's `build_clear_dcc_comp_to_single_shader` stores the clear color. A fast-clear eliminate over such keys and mixed keys throw; textures and color targets that read them use their stored texels. "Uniform" is judged over the key bytes the driver reads, one per 256 surface bytes (see DCC key stores above), so a pipe-aligned surface whose keys differ past that range is expanded as if all were comp-to-single, as for the other clear codes. Not verified on hardware
- [8_SRGB color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) on a device that cannot attach and blend R8_SRGB render into an RGBA8_SRGB copy of the target, so the sRGB encode, decode and blend rounding are the device's; the console's are not measured.
- [8_SRGB color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none.
- [1_5_5_5 color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format.
- [COPY_DATA from the GPU clock](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect.
- [16-bit RGBA color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support the standard and the alternate (`COMP_SWAP` 1) component order. Alternate swaps red and blue in shader exports, write masks and constant-color blend factors, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c). The reverse orders (`COMP_SWAP` 2 and 3) keep throwing when blending, and simultaneous targets requiring conflicting blend constants throw. Console equivalence has not been measured on hardware.
- [UINT16_ABGR color exports](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - export format 7 is accepted only into an unsigned integer target, and each 16-bit value reaches the target unchanged; a value beyond a narrower target's range (`R8_UINT`) is undefined in Vulkan, and whether the console clamps or truncates it is not confirmed. SINT16_ABGR (8) throws