Skip to content
Merged
15 changes: 15 additions & 0 deletions core/libs/prx/libSceAgcDriver/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,21 @@ target_link_libraries(agc_driver_tests PRIVATE
set_target_properties(agc_driver_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_tests)

add_test_executable(agc_gcn_color_target_tests EXCLUDE_FROM_ALL tests/GcnColorTarget.cpp)
target_include_directories(agc_gcn_color_target_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR})
target_link_libraries(agc_gcn_color_target_tests PRIVATE libSceAgcDriver libkernel libc)
set_target_properties(agc_gcn_color_target_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_gcn_color_target_tests)
if(BUILD_TESTING)
set_target_properties(agc_gcn_color_target_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_gcn_color_target COMMAND agc_gcn_color_target_tests)
set_tests_properties(agc_gcn_color_target PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_gcn_color_target PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()

add_test_executable(agc_driver_wait_shutdown_tests EXCLUDE_FROM_ALL tests/WaitShutdown.cpp)
target_include_directories(agc_driver_wait_shutdown_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_driver_wait_shutdown_tests PRIVATE libSceAgcDriver libkernel libc)
Expand Down
19 changes: 15 additions & 4 deletions core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1946,7 +1946,8 @@ ColorTarget DecodeGcnColorBuffer(const Registers& cx, std::uint32_t slot) {
const auto decoded = DecodeColorFormat((info >> 2u) & 0x1fu, number, swap);
const auto attrib = read(cx, 0x31d + stride);
color.samples = 1u << ((attrib >> 12u) & 7u);
Require(color.samples == 1u, "multisampled GCN color targets are not implemented");
Require(color.samples <= 8u, "GCN color targets with more than eight samples are not implemented");
Require((1u << ((attrib >> 15u) & 3u)) == color.samples, "GCN color targets with fewer fragments than samples (EQAA) are not implemented");
Require((info & 0x3u) == 0 && (info & 0x80u) == 0, "endian-swapped or linear-general GCN color targets are not implemented");
Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported");
const auto view = read(cx, 0x31b + stride);
Expand Down Expand Up @@ -1977,7 +1978,13 @@ ColorTarget DecodeGcnColorBuffer(const Registers& cx, std::uint32_t slot) {
const auto clear = find(cx, 0x323 + word + stride);
color.clearWords[word] = clear == cx.end() ? 0u : clear->second;
}
Require(((info >> 13u) & 1u) == 0, "PS4 color target fast clear (CMASK) is not implemented");
Require(color.samples > 1u || ((info >> 13u) & 1u) == 0, "PS4 color target fast clear (CMASK) is not implemented");
if (color.samples > 1u && (info & 0x6000u) != 0) {
color.cmaskAddress = static_cast<std::uint64_t>(read(cx, 0x31f + stride)) << 8u;
Require(color.cmaskAddress != 0, "a multisampled GCN color target with fast clear or FMASK compression has no CMASK address");
color.cmaskBytes = (static_cast<std::size_t>(read(cx, 0x320 + stride) & 0x3fffu) + 1u) * 128u;
color.cmaskFastClear = (info & 0x2000u) != 0;
}
GuestMemory::CheckRange(reinterpret_cast<const void*>(color.address), color.bytes, layout.Alignment(), true);
return color;
}
Expand Down Expand Up @@ -2178,7 +2185,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo

namespace {

bool drawCovers(const Registers& cx, VkExtent2D extent) {
bool drawCovers(const Registers& cx, VkExtent2D extent, const ColorTarget* gcnTarget = nullptr) {
const auto viewportControl = read(cx, 0x206);
if (viewportControl != 0x43fu) throw std::runtime_error(vteMessage(viewportControl));
const auto xs = std::fabs(readFloat(cx, 0x10f));
Expand All @@ -2190,6 +2197,10 @@ bool drawCovers(const Registers& cx, VkExtent2D extent) {
intersect(covered, cx, 0x81, false);
intersect(covered, cx, 0x90, false);
if ((read(cx, 0x292) & 2u) != 0) intersect(covered, cx, 0x94, false);
if (gcnTarget != nullptr && covered.offset.x == 0 && covered.offset.y == 0 && xo - xs <= 0.0f && yo - ys <= 0.0f) {
const VkExtent2D visible{std::min({extent.width, covered.extent.width, static_cast<std::uint32_t>(std::max(0.0f, xo + xs))}), std::min({extent.height, covered.extent.height, static_cast<std::uint32_t>(std::max(0.0f, yo + ys))})};
if (visible.width != 0 && visible.height != 0 && ColorTargetLayout(visible.width, visible.height, gcnTarget->tileMode, gcnTarget->elementBytes).Bytes() == gcnTarget->bytes) extent = visible;
}
const auto width = static_cast<float>(extent.width);
const auto height = static_cast<float>(extent.height);
const bool viewportCovers = xo - xs <= 0.0f && xo + xs >= width && yo - ys <= 0.0f && yo + ys >= height;
Expand Down Expand Up @@ -2254,7 +2265,7 @@ std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue
const auto samples = resolve && rasterizerSamples && multisampleRaster ? decodeSamples(cx) : 1u;
if (!resolve) zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion");
zero(cx, 0x80, ~0u, "window offset");
const auto requireCovered = [&](const ColorTarget& target) { Require(drawCovers(cx, target.extent), "CB metadata pass over part of a color target"); };
const auto requireCovered = [&](const ColorTarget& target) { Require(drawCovers(cx, target.extent, ColorTileModeIsGcn(target.tileMode) ? &target : nullptr), "CB metadata pass over part of a color target"); };
if (resolve) {
const auto source = DecodeColorBuffer(cx, 0);
const auto destination = DecodeColorBuffer(cx, 1);
Expand Down
153 changes: 153 additions & 0 deletions core/libs/prx/libSceAgcDriver/tests/GcnColorTarget.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include "prx/libc/include/GuestArena.hpp"
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <sys/mman.h>
#endif
#include <array>
#include <bit>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <exception>
#include <stdexcept>
#include <string>

namespace {

int failures = 0;

void Check(bool condition, const char* what) {
if (condition) return;
std::fprintf(stderr, "%s\n", what);
++failures;
}

template <typename Function>
std::string Failure(Function function) {
try {
function();
} catch (const std::exception& error) {
return error.what();
}
return {};
}

constexpr std::uint32_t Width = 64;
constexpr std::uint32_t Height = 64;
constexpr std::uint32_t Format8888 = 10u;
constexpr std::uint32_t BlendClamp = 1u << 15u;
constexpr std::uint32_t FastClear = 1u << 13u;
constexpr std::uint32_t Compression = 1u << 14u;
constexpr std::uint32_t Thin1D = 13u;

constexpr std::size_t ColorBytes = Width * Height * 4u;
constexpr std::size_t CmaskBytes = 512u;
constexpr std::size_t MemoryBytes = 65536u;
std::byte* colorMemory = nullptr;
std::byte* cmaskMemory = nullptr;

std::byte* LowMemory(std::size_t bytes) {
#ifdef _WIN32
auto* block = static_cast<std::byte*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536));
if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes);
return block;
#else
for (std::uintptr_t hint = std::uintptr_t{1} << 32u; hint < (std::uintptr_t{1} << 40u); hint += std::uintptr_t{1} << 32u) {
void* mapped = mmap(reinterpret_cast<void*>(hint), bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
if (mapped != MAP_FAILED) return static_cast<std::byte*>(mapped);
}
return nullptr;
#endif
}

std::uint64_t AddressOf(const void* data) {
return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data));
}

void SetTarget(AgcDriver::Registers& cx, std::uint32_t samples, std::uint32_t fragments, std::uint32_t flags, bool cmask = true) {
cx[0x318] = static_cast<std::uint32_t>(AddressOf(colorMemory) >> 8u);
cx[0x319] = Width / 8u - 1u;
cx[0x31a] = Width * Height / 64u - 1u;
cx[0x31b] = 0u;
cx[0x31c] = (Format8888 << 2u) | BlendClamp | flags;
cx[0x31d] = Thin1D | (static_cast<std::uint32_t>(std::countr_zero(samples)) << 12u) | (static_cast<std::uint32_t>(std::countr_zero(fragments)) << 15u);
cx[0x31f] = cmask ? static_cast<std::uint32_t>(AddressOf(cmaskMemory) >> 8u) : 0u;
cx[0x320] = static_cast<std::uint32_t>(CmaskBytes / 128u - 1u);
}

AgcDriver::Graphics::ColorTarget Decode(std::uint32_t samples, std::uint32_t fragments, std::uint32_t flags, bool cmask = true) {
AgcDriver::Registers cx;
SetTarget(cx, samples, fragments, flags, cmask);
return AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
}

void MultisampledTargetsDecode() {
const auto cleared = Decode(4u, 4u, FastClear | Compression);
Check(cleared.samples == 4u, "a 4x GCN color target did not decode with 4 samples");
Check(cleared.cmaskAddress == AddressOf(cmaskMemory) && cleared.cmaskBytes == CmaskBytes, "a 4x GCN color target lost its CMASK address or size");
Check(cleared.cmaskFastClear, "a 4x GCN color target with FAST_CLEAR is not a CMASK fast clear");
const auto compressed = Decode(8u, 8u, Compression);
Check(compressed.samples == 8u && compressed.cmaskAddress == AddressOf(cmaskMemory) && !compressed.cmaskFastClear, "an 8x FMASK-compressed GCN color target decoded wrong");
const auto plain = Decode(2u, 2u, 0u);
Check(plain.samples == 2u && plain.cmaskAddress == 0u, "a 2x GCN color target without compression took a CMASK");
Check(Failure([] { Decode(4u, 2u, Compression); }).find("fewer fragments than samples") != std::string::npos, "an EQAA GCN color target was accepted");
Check(Failure([] { Decode(1u, 1u, FastClear); }).find("fast clear (CMASK) is not implemented") != std::string::npos, "a single-sample GCN fast clear was accepted");
Check(Failure([] { Decode(4u, 4u, FastClear, false); }).find("has no CMASK address") != std::string::npos, "a 4x GCN fast clear without a CMASK address was accepted");
}

std::string FmaskDecompress(std::uint32_t visibleHeight) {
AgcDriver::QueueState queue;
auto& cx = queue.context;
SetTarget(cx, 4u, 4u, Compression);
cx[0x202] = 0xcc0000u | (5u << 4u);
cx[0x010] = 0u;
cx[0x011] = 0u;
cx[0x8e] = 0xfu;
cx[0x292] = 0u;
cx[0x10f] = std::bit_cast<std::uint32_t>(Width / 2.0f);
cx[0x110] = std::bit_cast<std::uint32_t>(Width / 2.0f);
cx[0x111] = std::bit_cast<std::uint32_t>(visibleHeight / 2.0f);
cx[0x112] = std::bit_cast<std::uint32_t>(visibleHeight / 2.0f);
cx[0x90] = 0u;
cx[0x91] = (visibleHeight << 16u) | Width;
return Failure([&] {
const auto pass = AgcDriver::Graphics::DecodeColorMetadataPass(queue);
if (!pass || pass->targets.size() != 1u || pass->targets[0].samples != 4u) throw std::runtime_error("the FMASK decompress did not decode its 4x target");
});
}

void PaddedTargetsAreCovered() {
Check(FmaskDecompress(Height).empty(), "an FMASK decompress over a whole 4x GCN target was refused");
const auto padded = FmaskDecompress(Height - 4u);
Check(padded.empty(), ("an FMASK decompress over the rows of a 4x GCN target its tile padding does not add was refused: " + padded).c_str());
Check(FmaskDecompress(Height - 16u).find("over part of a color target") != std::string::npos, "an FMASK decompress over part of a 4x GCN target was accepted");
}

}

int main() {
try {
AgcDriverUseGcnShaders_nid_postfix();
colorMemory = LowMemory(MemoryBytes);
if (colorMemory == nullptr || AddressOf(colorMemory) + MemoryBytes > (std::uint64_t{1} << 40u)) {
std::fprintf(stderr, "cannot allocate test memory below 1 TiB\n");
return 1;
}
cmaskMemory = colorMemory + ColorBytes;
MultisampledTargetsDecode();
PaddedTargetsAreCovered();
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
if (failures != 0) return 1;
std::puts("GCN multisampled color targets decode with their CMASK, and a metadata pass over the visible rows of a padded target is covered");
return 0;
}
1 change: 1 addition & 0 deletions docs/dev/debt/libSceAgcDriver.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
- [Vertex input and color target overlap](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (AGC driver) - direct draws exclude the descriptor prefix skipped by `firstVertex` or `firstInstance` from the alias check; indexed draws conservatively use index zero as the lower bound before adding `firstVertex`. Indirect draws still check the full descriptor extent. Reads of the active target's bytes remain unsupported; same-draw console ordering is not established.
- [PS4 (GCN) tiling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) (libSceAgcDriver, PS4) - not checked on a console. Surfaces with `GuestTextureResource::gcnPitch` set use the GB_TILE_MODE and GB_MACROTILE_MODE tables of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/video_core/amdgpu/tiling.cpp) and addrlib's `ComputeSurfaceAddrFromCoordMicroTiled` and `MacroTiled`: 8x8 micro tiles in thin, depth or display element order, macro tiles with pipe config P8_32x32_16x16 (P8_32x32_8x16 for the two PRT thin modes), bank width 1 and a 256-byte pipe interleave, depth micro tiles split into slices a whole surface apart with the bank rotated by (banks / 2 + 1) per slice, PRT pipe and bank taken from the coordinates inside the macro tile, and LINEAR_GENERAL rows of exactly the pitch. [TextureTiling.cpp](../../../core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp) compares it with a separate implementation of the addrlib formulas.
- [PS4 (GCN) thick and layered surfaces](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) (libSceAgcDriver, PS4) - 3D and thick modes (GB_TILE_MODE 15 and 19 to 26) throw, and so does a storage image or a depth plane of a layered surface with several mip levels. A sampled layered surface with several mip levels is stored level by level, which is not checked on a console; [GcnDetile.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/GcnDetile.cpp) runs the GPU detiler over every mode that does not throw.
- [PS4 (GCN) multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver, PS4) - a GCN color target with 2, 4 or 8 samples and as many fragments (CB_COLOR_ATTRIB NUM_SAMPLES and NUM_FRAGMENTS) becomes a host-resident multisampled target, as on the AGC path: its samples stay on the host. With FAST_CLEAR or COMPRESSION set, its CMASK (CB_COLOR_CMASK, CMASK_SLICE tile max + 1 times 128 bytes) is taken for fast clears; FMASK contents are not modeled. Fewer fragments than samples (EQAA), more than 8 samples and a single-sample fast clear throw. A CB metadata pass (FMASK decompress or resolve) over a GCN target may cover only its visible rows and columns: the extent the viewport and scissor cover counts when the tile mode pads it to the target's size. None of it is checked on a console.
- [PS4 render targets and display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, PS4) - a tiled PS4 display buffer is read as Display2DThin with rows padded to 128 elements, as shadPS4's `image_info.cpp` does. A PS4 render target spans its surface padded to whole macro tiles (1920x1080 at 32 bits is 1920x1088), the display buffer is its top-left corner, and a texture over that corner with the same pitch, tiling and texel size is a [GPU copy](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) of it. A PS4 `DB_Z_INFO` names a GB_TILE_MODE index; the depth image stays on the GPU and HTILE is kept, as for the PS5.
- [PS4 pattern fill](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) (libSceAgcDriver, PS4) - the PS4 build of the pattern-fill compute kernel seen in Limbo (CUSA01664), with user data s[4:7] a 32-bit UINT source, s[8:11] the 32-bit UINT destination and s[12:15] a control record of count and index mask, where thread i stores source[i & mask], runs as a GPU fill when the mask is 0, a single repeated dword; other masks run the kernel, since running Limbo's two-dword clears of a 64-bit target as fills made its gameplay advance more slowly per frame. The kernel is recognized by its code, not by an SDK symbol.

Expand Down
Loading