Merge remote-tracking branch 'upstream/main' into fix/export-ppsa30601-imports

# Conflicts:
#	core/libs/prx/libSceJson2/Export.cpp
#	docs/dev/TechnicalDebt.md
This commit is contained in:
xXPBSXx 2026-10-12 01:19:36 +02:00
commit 35a6df818f
381 changed files with 18879 additions and 1427 deletions

View File

@ -215,8 +215,8 @@ jobs:
key: linux
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
evict-old-files: job
- name: Install lavapipe for the GPU execution tests
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers
- name: Install lavapipe for the GPU execution tests and the audio development files
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev
- run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON
- name: Check that every test executable is registered with ctest
run: python3 tools/check_test_registration.py build

View File

@ -60,9 +60,9 @@ jobs:
with:
key: ${{ matrix.platform }}
save: false
- name: Install lavapipe for the GPU execution tests
- name: Install lavapipe for the GPU execution tests and the audio development files
if: matrix.platform == 'linux'
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev
- name: Configure Linux
if: matrix.platform == 'linux'
run: cmake -S . -B build $CONFIGURE

View File

@ -24,6 +24,7 @@ if(BUILD_TESTING)
endif()
option(ANYPS5_ENABLE_SPIRV_TOOLS "Enable SPIRV-Tools validation and optimization" OFF)
option(ANYPS5_ENABLE_ASAN "Build Linux targets with AddressSanitizer and use an ASan-compatible guest arena range" OFF)
option(APS5_ENABLE_TIMING_LOG "Write frame timings asynchronously to frame-timing.log" OFF)
option(ANYPS5_USE_SYSTEM_DEPS "Resolve SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools from the system instead of the bundled submodules" OFF)
add_compile_definitions(APS5_ENABLE_TIMING_LOG=$<BOOL:${APS5_ENABLE_TIMING_LOG}>)
@ -32,11 +33,27 @@ include(cmake/SystemDeps.cmake)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(NOT APPLE)
if(ANYPS5_ENABLE_ASAN)
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
message(FATAL_ERROR "ANYPS5_ENABLE_ASAN is supported only on Linux")
endif()
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang)$")
message(FATAL_ERROR "ANYPS5_ENABLE_ASAN requires GCC or Clang")
endif()
add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
add_link_options(-fsanitize=address)
add_compile_definitions(ANYPS5_ENABLE_ASAN=1)
elseif(NOT APPLE)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++")
endif()
if(BUILD_TESTING)
add_test_executable(shader_cache_file_read_tests
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/CacheFileReadTests.cpp)
target_include_directories(shader_cache_file_read_tests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler)
add_test(NAME shader_cache_file_read COMMAND shader_cache_file_read_tests)
set_tests_properties(shader_cache_file_read PROPERTIES TIMEOUT 20)
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE}
@ -65,6 +82,8 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_exports.py)
add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py)
add_test(NAME release_package COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_package_release.py)
endif()
endif()
@ -93,6 +112,16 @@ if(NOT ANYPS5_USE_SYSTEM_DEPS)
set(CMAKE_EXE_LINKER_FLAGS "")
add_subdirectory(3rdparty/SDL2)
set(CMAKE_EXE_LINKER_FLAGS "${ANYPS5_EXE_LINKER_FLAGS}")
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
get_directory_property(ANYPS5_SDL_PIPEWIRE DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_PIPEWIRE)
get_directory_property(ANYPS5_SDL_PULSEAUDIO DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_PULSEAUDIO)
get_directory_property(ANYPS5_SDL_ALSA DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_ALSA)
if(NOT ANYPS5_SDL_PIPEWIRE AND NOT ANYPS5_SDL_PULSEAUDIO AND NOT ANYPS5_SDL_ALSA)
message(FATAL_ERROR "SDL found no PipeWire, PulseAudio or ALSA development files, so audio output would be silent. "
"Install libpipewire-0.3-dev, libpulse-dev and libasound2-dev (Debian, Ubuntu) "
"or pipewire-devel, pulseaudio-libs-devel and alsa-lib-devel (Fedora), then rerun configuration.")
endif()
endif()
endif()
set(ARCHITECTURE x86_64)
add_subdirectory(3rdparty/ffmpeg-core)

View File

@ -55,6 +55,7 @@ target_include_directories(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
configure_windows_unwind(libc)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_options(libc PRIVATE LINKER:-z,now)
target_link_options(libc INTERFACE LINKER:--no-as-needed)
endif()
set(UNPATCHED_DIR ${CMAKE_CURRENT_BINARY_DIR}/libs/unpatched)
@ -101,9 +102,9 @@ foreach(LIB_DIR ${LIBS_DIRS})
endif()
if(LIB_NAME STREQUAL "libkernel")
target_link_libraries(${LIB_NAME} PRIVATE libc)
target_link_libraries(${LIB_NAME} PUBLIC libc)
else()
target_link_libraries(${LIB_NAME} PRIVATE libkernel libc)
target_link_libraries(${LIB_NAME} PRIVATE libkernel PUBLIC libc)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_options(${LIB_NAME} PRIVATE LINKER:-z,now)
@ -188,6 +189,16 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceHttp>")
endif()
add_test_executable(guest_signin_dialog_tests tests/GuestSigninDialog.cpp)
target_include_directories(guest_signin_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_signin_dialog_tests PRIVATE libSceSigninDialog libc)
configure_windows_unwind(guest_signin_dialog_tests)
add_test(NAME guest_signin_dialog COMMAND guest_signin_dialog_tests)
if(WIN32)
set_tests_properties(guest_signin_dialog PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSigninDialog>")
endif()
add_test_executable(guest_rudp_status_tests tests/GuestRudpStatus.cpp)
target_include_directories(guest_rudp_status_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_rudp_status_tests PRIVATE libSceRudp libc)
@ -208,6 +219,16 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceRudp>")
endif()
add_test_executable(guest_rudp_pump_tests tests/GuestRudpPump.cpp)
target_include_directories(guest_rudp_pump_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_rudp_pump_tests PRIVATE libSceRudp libc)
configure_windows_unwind(guest_rudp_pump_tests)
add_test(NAME guest_rudp_pump COMMAND guest_rudp_pump_tests)
if(WIN32)
set_tests_properties(guest_rudp_pump PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceRudp>")
endif()
add_test_executable(guest_memory_pool_stats_tests tests/GuestMemoryPoolStats.cpp)
target_include_directories(guest_memory_pool_stats_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_memory_pool_stats_tests PRIVATE libSceSsl libSceHttp2 libSceNet libkernel libc)
@ -329,4 +350,14 @@ if(BUILD_TESTING)
set_tests_properties(guest_posix_random PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libScePosixForWebKit>")
endif()
add_test_executable(guest_acm_tests tests/GuestAcm.cpp)
target_include_directories(guest_acm_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_acm_tests PRIVATE libSceAcm libc)
configure_windows_unwind(guest_acm_tests)
add_test(NAME guest_acm COMMAND guest_acm_tests)
if(WIN32)
set_tests_properties(guest_acm PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAcm>")
endif()
endif()

View File

@ -95,6 +95,14 @@ struct KernelAioRwRequest {
std::int32_t fd;
};
struct KernelAioSchedulingParam {
std::int32_t schedulingWindowSize;
std::int32_t delayedCountLimit;
std::uint32_t enableSplit;
std::uint32_t splitSize;
std::uint32_t splitChunkSize;
};
struct KernelBatchMapEntry {
void* start;
std::uint64_t offset;
@ -1253,7 +1261,7 @@ union NetCtlInfo { std::uint8_t opaque[256]; };
using NetCtlCallback = void (*)(int, void*);
struct HttpEpoll {};
struct HttpEpoll;
using HttpEpollHandle = HttpEpoll*;
using HttpsCallback = int (*)(int, unsigned int, void* const*, int, void*);
using HttpRedirectCallback = int (*)(int, std::int32_t, std::int32_t*, const char*, void*);
@ -1563,6 +1571,13 @@ struct SaveDataBackup {
std::uint8_t reserved[32];
};
struct SaveDataConvert {
std::int32_t user_id;
std::int32_t pad;
const SceSaveDataDirName* dir_name;
std::uint8_t unknown[48];
};
struct AppContentInitParam { char reserved[32]; };
struct AppContentBootParam {
@ -1582,6 +1597,19 @@ struct ContentExportInitParam2 {
std::int64_t reserved1;
};
struct ContentExportParam {
char title[257];
char comment[257];
char content_type[65];
};
static_assert(sizeof(ContentExportParam) == 579);
struct ContentExportCallbackParam {
std::size_t content_length;
void* func;
void* user_data;
};
struct ContentSearchInitParam { std::size_t memory_size; };
struct PngEncCreateParam {

View File

@ -53,13 +53,11 @@ int APS5_VABI sceAcm_ConvReverb_SharedInput(void) {
return 0;
}
int APS5_VABI sceAcm_FFT() {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceAcm_FFT(void) {
return 0;
}
int APS5_VABI sceAcm_Panner() {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceAcm_Panner(void) {
return 0;
}

View File

@ -11,10 +11,15 @@
extern "C" {
// unknown signature
void* APS5_VABI sceAgcAcbAtomicGds_0900(void) {
NotImplemented_nid_no_patch(__func__);
return nullptr;
std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData) {
const auto operation = (control & 0xffu) | ((control & 0x200u) << 7u) |
((control & 0x20u) << 13u) | ((control & 0x40u) << 14u) |
((control & 0x20u) == 0 && mode == 1 ? 0x20000u : 0u);
return Agc::Command::Emit(buf, 0x1du, {
operation, ((gdsOp & 1u) << 8u) | (src & 0x3fu), offset, index,
mask & 0x00ff00ffu, data, static_cast<std::uint32_t>(compareData),
static_cast<std::uint32_t>(compareData >> 32u), static_cast<std::uint32_t>(extraData),
static_cast<std::uint32_t>(extraData >> 32u)}, __func__);
}
std::uint32_t* APS5_VABI sceAgcAcbJump(CommandBuffer* buf, std::uint8_t cachePolicy, const std::uint32_t* target, std::uint32_t sizeInDwords) {

View File

@ -13,8 +13,7 @@
extern "C" {
// unknown signature
APS5_EXPORT("zARR5aCmkoY", sceAgcDcbA_zARR5aCmkoY);
void* APS5_VABI sceAgcDcbA_zARR5aCmkoY(void) {
void* APS5_VABI sceAgcDcbAtomicGds_0900(void) {
NotImplemented_nid_no_patch(__func__);
return nullptr;
}

View File

@ -19,6 +19,7 @@ constexpr std::uint32_t VGT_SHADER_STAGES_EN = 0x2D5u;
constexpr std::uint32_t VGT_PRIMITIVE_TYPE = 0x242u;
constexpr std::uint32_t GE_CNTL = 0x25Bu;
constexpr std::uint32_t GE_USER_VGPR1 = 0x25Cu;
constexpr std::uint32_t GE_USER_VGPR_EN = 0x262u;
constexpr std::uint32_t GE_PC_ALLOC = 0x260u;

View File

@ -53,10 +53,21 @@ std::uint32_t CreateInterpolantValueSplitF16(std::uint32_t psWord, const ShaderS
: CreateInterpolantValue(psWord, gsSemantic);
}
int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue) {
std::uint32_t UpdateInterpolantValue(std::uint32_t psWord, const ShaderSemantic* gsSemantic) {
const std::uint32_t f16 = (psWord >> 20u) & 0x3u;
if (f16 != 0 && f16 != InterpolantF16HiOnly && ((psWord >> 28u) & 0x3u) != ((psWord >> 30u) & 0x3u)) {
throw std::runtime_error(std::string(__func__) + ": DEFAULT_VAL of an f16 input with different low and high defaults is unknown");
}
return CreateInterpolantValueSplitF16(psWord, gsSemantic);
}
int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue, bool fillIdentityTail) {
if (regs == nullptr) {
throw std::runtime_error(std::string(fn) + ": regs is null");
}
if (ps == nullptr && !fillIdentityTail) {
throw std::runtime_error(std::string(fn) + ": ps is null");
}
if (ps != nullptr && ps->num_input_semantics > 32) {
throw std::runtime_error(std::string(fn) + ": input semantic count exceeds 32");
}
@ -66,13 +77,14 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader*
auto* output = regs;
regs = values.data();
if (ps == nullptr || ps->num_input_semantics == 0) {
FillIdentityInterpolants(regs, 0);
const std::uint32_t count = fillIdentityTail ? InterpolantRegisterCount : 0;
if (fillIdentityTail) FillIdentityInterpolants(regs, 0);
if (ps != nullptr) {
AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, 32}, {});
AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, count}, {});
if (gs != nullptr) AgcDriverResolveGraphicsAbi_nid_postfix(gs, ps, 0);
}
transaction.Commit();
std::copy(values.begin(), values.end(), output);
std::copy_n(values.begin(), count, output);
return 0;
}
@ -98,11 +110,12 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader*
SetInterpolantRegister(regs, i, createValue(ShaderSemanticWord(psSemantic), gsSemantic));
}
FillIdentityInterpolants(regs, ps->num_input_semantics);
AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, 32}, {});
const std::uint32_t count = fillIdentityTail ? InterpolantRegisterCount : ps->num_input_semantics;
if (fillIdentityTail) FillIdentityInterpolants(regs, ps->num_input_semantics);
AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, count}, {});
AgcDriverResolveGraphicsAbi_nid_postfix(gs, ps, 0);
transaction.Commit();
std::copy(values.begin(), values.end(), output);
std::copy_n(values.begin(), count, output);
return 0;
}
@ -111,12 +124,17 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader*
extern "C" {
int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValue);
return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValue, true);
}
APS5_EXPORT("dbOlWdppb4o", sceAgcUnknownCreateInterpolantMapping);
int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16);
return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16, true);
}
APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw);
int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
return CreateInterpolantMapping(__func__, regs, gs, ps, UpdateInterpolantValue, false);
}
}

View File

@ -11,6 +11,18 @@
#include "prx/libSceAgc/Shader/include/ShaderUtils.hpp"
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
namespace {
void RequireSpecialOffset(const ShaderRegister& reg, std::uint32_t expected, const char* name) {
if (reg.offset != expected) {
char message[160];
std::snprintf(message, sizeof(message), "sceAgcCreatePrimState: %s special register at offset 0x%x, expected 0x%x", name, static_cast<unsigned>(reg.offset), static_cast<unsigned>(expected));
throw std::runtime_error(message);
}
}
}
extern "C" {
int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_regs, const Shader* hs, const Shader* gs, std::uint32_t prim_type) {
@ -25,21 +37,17 @@ int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_
throw std::runtime_error(std::string(__func__) + ": invalid shader type or missing special registers");
}
(void)GraphicsPrimTypeToGsOut(prim_type);
const auto validStages = [](const Shader* shader) { return shader->specials->vgt_shader_stages_en.offset == ShaderRegs::VGT_SHADER_STAGES_EN; };
if (!validStages(gs) || (hs != nullptr && !validStages(hs))) {
throw std::runtime_error(std::string(__func__) + ": invalid context register offsets");
}
if (gs->specials->ge_cntl.offset != ShaderRegs::GE_CNTL || gs->specials->ge_user_vgpr_en.offset != ShaderRegs::GE_USER_VGPR_EN || (hs != nullptr && hs->specials->ge_user_vgpr_en.offset != ShaderRegs::GE_USER_VGPR_EN)) {
throw std::runtime_error(std::string(__func__) + ": invalid user configuration register offsets");
}
RequireSpecialOffset(gs->specials->vgt_shader_stages_en, ShaderRegs::VGT_SHADER_STAGES_EN, "VGT_SHADER_STAGES_EN");
if (hs != nullptr) RequireSpecialOffset(hs->specials->vgt_shader_stages_en, ShaderRegs::VGT_SHADER_STAGES_EN, "VGT_SHADER_STAGES_EN");
RequireSpecialOffset(gs->specials->ge_cntl, ShaderRegs::GE_CNTL, "GE_CNTL");
std::array<ShaderRegister, 2> contextValues{gs->specials->vgt_shader_stages_en, gs->specials->vgt_gs_out_prim_type};
if (hs != nullptr) {
contextValues[0].value |= hs->specials->vgt_shader_stages_en.value;
}
if ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_GS_BIT) != 0) {
if (gs->specials->vgt_gs_out_prim_type.offset != ShaderRegs::VGT_GS_OUT_PRIM_TYPE) throw std::runtime_error(std::string(__func__) + ": invalid context register offsets");
RequireSpecialOffset(gs->specials->vgt_gs_out_prim_type, ShaderRegs::VGT_GS_OUT_PRIM_TYPE, "VGT_GS_OUT_PRIM_TYPE");
} else if (hs != nullptr) {
if (hs->specials->vgt_gs_out_prim_type.offset != ShaderRegs::VGT_GS_OUT_PRIM_TYPE) throw std::runtime_error(std::string(__func__) + ": invalid context register offsets");
RequireSpecialOffset(hs->specials->vgt_gs_out_prim_type, ShaderRegs::VGT_GS_OUT_PRIM_TYPE, "VGT_GS_OUT_PRIM_TYPE");
contextValues[1] = hs->specials->vgt_gs_out_prim_type;
} else {
contextValues[1] = {ShaderRegs::VGT_GS_OUT_PRIM_TYPE, GraphicsPrimTypeToGsOut(prim_type)};

View File

@ -90,10 +90,4 @@ int APS5_VABI sceAgcGetShaderInstrumentation() {
return 0;
}
APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw);
int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
}

View File

@ -37,6 +37,7 @@ extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexBuffer(CommandBuffer* buf,
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetBaseIndirectArgs(CommandBuffer* buf, std::uint32_t shaderType, const volatile void* indirectBaseAddr);
extern "C" std::uint32_t* APS5_VABI sceAgcSetNop(CommandBuffer* buf, std::uint32_t sizeDw);
extern "C" int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId);
extern "C" std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData);
namespace {
@ -404,6 +405,101 @@ void testMemory() {
check(storage.words == beforeMask, "invalid mask patch modified packet memory");
}
struct AtomicGdsAllocation {
Storage storage;
std::uint32_t capacity = 15;
std::uint32_t calls = 0;
bool accepted = true;
};
bool APS5_VABI growAtomicGds(CommandBuffer* buffer, std::uint32_t count, void* userData) {
auto& allocation = *static_cast<AtomicGdsAllocation*>(userData);
check(count == 15, "atomic GDS allocation omitted packet or reserved words");
++allocation.calls;
if (!allocation.accepted) return false;
buffer->bottom = allocation.storage.words.data();
buffer->top = buffer->bottom + allocation.capacity;
buffer->cursor_up = buffer->bottom;
buffer->cursor_down = buffer->top;
return true;
}
void testAtomicGds() {
Storage storage;
storage.words.fill(0xdeadbeefu);
storage.buffer.cursor_up += 2;
auto* packet = sceAgcAcbAtomicGds_0900(&storage.buffer, 0xffffffffu, 1, 0xffffffffu, 0xffffffffu,
0xabcd, 0xfedc, 0x12345678u, 0x87654321u, 0xfedcba9876543210ull, 0x0123456789abcdefull);
const std::array<std::uint32_t, 11> expected{
0xc0091d00u, 0x001500ffu, 0x13fu, 0xabcdu, 0xfedcu, 0x00340078u,
0x87654321u, 0x76543210u, 0xfedcba98u, 0x89abcdefu, 0x01234567u};
check(packet == storage.words.data() + 2, "atomic GDS returned the wrong packet address");
check(std::equal(expected.begin(), expected.end(), packet), "atomic GDS packet or guest ABI mismatch");
check(storage.buffer.cursor_up == packet + expected.size(), "atomic GDS did not advance by eleven words");
check(storage.words[0] == 0xdeadbeefu && storage.words[1] == 0xdeadbeefu &&
std::all_of(storage.words.begin() + 13, storage.words.end(), [](auto word) { return word == 0xdeadbeefu; }),
"atomic GDS wrote outside its packet");
const std::array<std::array<std::uint32_t, 3>, 9> controls{{
{0, 0, 0}, {0, 1, 0x00020000u}, {0, 2, 0}, {0, 0xffffffffu, 0},
{0x20u, 1, 0x00040020u}, {0x40u, 1, 0x00120040u},
{0x200u, 1, 0x00030000u}, {0xfffffd00u, 0, 0}, {0xffu, 1, 0x001400ffu}}};
for (const auto& entry : controls) {
Storage controlStorage;
auto* controlPacket = sceAgcAcbAtomicGds_0900(&controlStorage.buffer, entry[0], entry[1],
0xfffffffeu, 0xffffffc0u, 0, 0, 0xff00ff00u, 0, 0, 0);
std::array<std::uint32_t, 11> controlExpected{};
controlExpected[0] = 0xc0091d00u;
controlExpected[1] = entry[2];
check(std::equal(controlExpected.begin(), controlExpected.end(), controlPacket), "atomic GDS control or field masking mismatch");
}
auto emit = [](CommandBuffer* buffer) {
return sceAgcAcbAtomicGds_0900(buffer, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
};
Storage exact;
exact.buffer.cursor_down = exact.words.data() + 15;
exact.buffer.reserved_dw = 4;
check(emit(&exact.buffer) == exact.words.data() && exact.buffer.cursor_up == exact.words.data() + 11,
"atomic GDS rejected an exact fit with reserved space");
const auto exactBefore = exact.words;
expectFailure([&] { emit(&exact.buffer); });
check(exact.words == exactBefore && exact.buffer.cursor_up == exact.words.data() + 11,
"atomic GDS overwrote reserved space");
Storage exhausted;
exhausted.buffer.cursor_down = exhausted.words.data() + 10;
const auto before = exhausted.words;
expectFailure([&] { emit(&exhausted.buffer); });
check(exhausted.words == before && exhausted.buffer.cursor_up == exhausted.words.data(),
"failed atomic GDS allocation modified the buffer");
expectFailure([&] { emit(nullptr); });
Storage noDown;
noDown.buffer.top = noDown.words.data() + 11;
noDown.buffer.cursor_down = nullptr;
check(emit(&noDown.buffer) == noDown.words.data() && noDown.buffer.cursor_up == noDown.buffer.top,
"atomic GDS did not honor a buffer with no down cursor");
AtomicGdsAllocation allocation;
CommandBuffer growing{nullptr, nullptr, nullptr, nullptr, growAtomicGds, &allocation, 4};
check(emit(&growing) == allocation.storage.words.data() && allocation.calls == 1 &&
growing.cursor_up == allocation.storage.words.data() + 11, "atomic GDS did not use the guest allocation callback");
const std::array<std::uint32_t, 11> zeroPacket{0xc0091d00u};
check(std::equal(zeroPacket.begin(), zeroPacket.end(), allocation.storage.words.begin()),
"atomic GDS callback allocation produced the wrong packet");
for (const bool accepted : {false, true}) {
AtomicGdsAllocation failure;
failure.accepted = accepted;
failure.capacity = 14;
const auto failureBefore = failure.storage.words;
CommandBuffer failed{nullptr, nullptr, nullptr, nullptr, growAtomicGds, &failure, 4};
expectFailure([&] { emit(&failed); });
check(failure.calls == 1 && failure.storage.words == failureBefore &&
failed.cursor_up == (accepted ? failure.storage.words.data() : nullptr),
"atomic GDS wrote a packet after failed or insufficient allocation");
}
}
void testDebugRaiseException() {
int result = -1;
try {
@ -459,6 +555,7 @@ int main(int argc, char** argv) {
testRegisterRange();
testPacketPayloadAddress();
testMemory();
testAtomicGds();
testDebugRaiseException();
testDefaults();
LibcRunShutdown_nid_postfix();

View File

@ -13,12 +13,14 @@
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps);
extern "C" int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps);
extern "C" int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps);
namespace {
using Registers = std::array<ShaderRegister, 32>;
const Shader* mappedPixel = nullptr;
Registers preparedMapping{};
std::size_t preparedCount = 0;
unsigned mappings = 0;
unsigned links = 0;
@ -65,6 +67,11 @@ void testIdentity() {
regs = filled();
check(sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps) == 0, "mapping without inputs failed");
checkIdentity(regs, 0, "mapping without inputs is not the identity");
regs = filled();
check(sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps) == 0, "update without inputs failed");
check(preparedCount == 0u, "update without inputs prepared registers");
const auto untouched = filled();
check(std::memcmp(regs.data(), untouched.data(), sizeof(regs)) == 0, "update without inputs wrote registers");
}
void testMapping() {
@ -99,6 +106,15 @@ void testMapping() {
check(split[4].value == 0x03180002u, "full f16 interpolant mapping changed");
checkIdentity(split, static_cast<std::uint32_t>(inputs.size()), "unused interpolants are not the identity");
auto update = filled();
const auto untouched = filled();
const std::size_t written = inputs.size() * sizeof(ShaderRegister);
check(sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps) == 0, "update mapping failed");
check(preparedCount == inputs.size(), "update prepared registers past its inputs");
check(std::memcmp(update.data(), preparedMapping.data(), written) == 0, "prepared update differs from published registers");
check(std::memcmp(update.data(), split.data(), written) == 0, "update mapping differs from the split f16 mapping");
check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "update wrote past its inputs");
auto regular = filled();
check(sceAgcCreateInterpolantMapping_0100(regular.data(), &gs, &ps) == 0, "mapping failed");
check(std::memcmp(regular.data(), preparedMapping.data(), sizeof(regular)) == 0, "prepared mapping differs from published registers");
@ -106,6 +122,51 @@ void testMapping() {
checkIdentity(regular, static_cast<std::uint32_t>(inputs.size()), "unused interpolants are not the identity");
}
void testUpdateMapping() {
std::array<ShaderSemantic, 4> inputs{semantic(1, 0, 0), semantic(2, 0, 1), semantic(3, 0, 2), semantic(4, 0, 3)};
inputs[0].is_custom = 1;
inputs[0].default_value = 2;
inputs[1].default_value = 1;
inputs[1].default_value_hi = 1;
inputs[2].default_value_hi = 2;
inputs[3].is_custom = 1;
inputs[3].default_value = 3;
inputs[3].default_value_hi = 3;
std::array<ShaderSemantic, 3> outputs{semantic(1, 5, 0), semantic(2, 7, 1), semantic(4, 9, 3)};
Shader gs{};
gs.output_semantics = outputs.data();
gs.num_output_semantics = static_cast<std::uint16_t>(outputs.size());
Shader ps{};
ps.input_semantics = inputs.data();
ps.num_input_semantics = static_cast<std::uint32_t>(inputs.size());
auto update = filled();
const auto untouched = filled();
check(sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps) == 0, "update mapping failed");
check(update[0].value == 0x00000625u, "custom interpolant update changed");
check(update[1].value == 0x01380107u, "low f16 half update changed");
check(update[2].value == 0x02480220u, "unmatched high f16 half update changed");
check(update[3].value == 0x03680709u, "full f16 interpolant update changed");
for (std::uint32_t i = 0; i < inputs.size(); ++i) {
check(update[i].offset == ShaderRegs::SPI_PS_INPUT_CNTL_0 + i, "updated register offset changed");
}
const std::size_t written = inputs.size() * sizeof(ShaderRegister);
check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "update wrote past its inputs");
auto create = filled();
check(sceAgcUnknownCreateInterpolantMapping(create.data(), &gs, &ps) == 0, "split f16 mapping failed");
check(std::memcmp(create.data(), update.data(), written) == 0, "update differs from the split f16 mapping");
checkIdentity(create, static_cast<std::uint32_t>(inputs.size()), "unused interpolants are not the identity");
for (const std::uint32_t index : {1u, 3u}) {
inputs[index].default_value = 0;
update = filled();
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps); });
check(std::memcmp(update.data(), untouched.data(), sizeof(update)) == 0, "rejected update wrote registers");
inputs[index].default_value = inputs[index].default_value_hi;
}
}
void testRejections() {
std::array<ShaderSemantic, 33> inputs{};
Shader gs{};
@ -117,18 +178,23 @@ void testRejections() {
const auto saved = regs;
expectFailure([&] { sceAgcCreateInterpolantMapping_0100(regs.data(), &gs, &ps); });
expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), &gs, &ps); });
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, &ps); });
check(std::memcmp(regs.data(), saved.data(), sizeof(regs)) == 0, "rejected mapping wrote registers");
ps.num_input_semantics = 1;
expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps); });
expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(nullptr, &gs, &ps); });
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps); });
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(nullptr, &gs, &ps); });
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, nullptr); });
}
}
extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
check(shader != nullptr && context.size() == preparedMapping.size() && primitive.empty(), "invalid mapping preparation call");
check(shader != nullptr && context.size() <= preparedMapping.size() && primitive.empty(), "invalid mapping preparation call");
mappedPixel = shader;
std::copy(context.begin(), context.end(), preparedMapping.begin());
preparedCount = context.size();
++mappings;
}
@ -141,8 +207,9 @@ int main() {
try {
testIdentity();
testMapping();
testUpdateMapping();
testRejections();
check(mappings == 3u && links == 2u, "mapping preparation call count changed");
check(mappings == 7u && links == 5u, "mapping preparation call count changed");
LibcRunShutdown_nid_postfix();
std::puts("AGC interpolant mapping tests passed");
return 0;

View File

@ -7,6 +7,7 @@
#include <array>
#include <span>
#include <stdexcept>
#include <string>
#include <iostream>
extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister*, ShaderRegister*, const void*, const Shader*, const Shader*, std::uint32_t);
@ -48,6 +49,30 @@ void OptionalGeometryOutputRegister(ShaderSpecialRegs& special, const Shader& ve
special.vgt_shader_stages_en.value = 0;
special.vgt_gs_out_prim_type = {VGT_GS_OUT_PRIM_TYPE, 2};
}
void HandBuiltUserVgprRegister() {
using namespace ShaderRegs;
ShaderSpecialRegs handBuilt{};
handBuilt.ge_cntl = {GE_CNTL, 0x100u};
handBuilt.vgt_shader_stages_en = {VGT_SHADER_STAGES_EN, 0x2412010u};
handBuilt.vgt_gs_out_prim_type = {0x2ceu, 2};
handBuilt.ge_user_vgpr_en = {GE_USER_VGPR1, 0};
Shader preRaster{};
preRaster.type = static_cast<std::uint8_t>(ShaderBinaryType::Gs);
preRaster.specials = &handBuilt;
std::array<ShaderRegister, 2> context{};
std::array<ShaderRegister, 3> primitive{};
preparations = 0;
Require(sceAgcCreatePrimState(context.data(), primitive.data(), nullptr, &preRaster, 4) == 0);
Require(context[0].offset == VGT_SHADER_STAGES_EN && context[0].value == 0x2412010u && context[1].offset == VGT_GS_OUT_PRIM_TYPE && context[1].value == static_cast<std::uint32_t>(GsOutputPrimitiveType::Triangles));
Require(primitive[0].offset == GE_CNTL && primitive[0].value == 0x100u && primitive[1].offset == GE_USER_VGPR1 && primitive[1].value == 0 && primitive[2].offset == VGT_PRIMITIVE_TYPE && primitive[2].value == 4);
Require(preparations == 1 && stageCount == 1 && preparedPrimitive[1].offset == GE_USER_VGPR1 && preparedPrimitive[1].value == 0);
handBuilt.ge_cntl.offset = 0x25cu;
std::string message;
try { static_cast<void>(sceAgcCreatePrimState(context.data(), primitive.data(), nullptr, &preRaster, 4)); }
catch (const std::runtime_error& error) { message = error.what(); }
Require(message.find("GE_CNTL") != std::string::npos && message.find("0x25c") != std::string::npos);
}
}
extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
@ -168,6 +193,7 @@ int main() {
Require(sceAgcCreateInterpolantMapping_0100(interpolants.data(), &vertex, nullptr) == 0);
Require(preparations == 0 && mappings == 0 && links == 0);
OptionalGeometryOutputRegister(special, vertex, hull);
HandBuiltUserVgprRegister();
std::cout << "shader helper preparation tests passed\n";
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';

View File

@ -111,6 +111,7 @@ add_custom_command(
set(agcDriverSources
${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ComputePipelineCapture.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/DisplayBuffer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/DisplayFormat.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/DeviceAccess.cpp
@ -173,6 +174,7 @@ set(agcDriverSources
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DepthSurface.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTarget.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetLayout.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetTransfer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Pipeline.cpp
@ -313,8 +315,10 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_wait_shutdown PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_GPU_WAIT_TIMEOUT_MS=20"
TIMEOUT 10 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_driver_resolution_shutdown COMMAND agc_driver_wait_shutdown_tests --resolution-shutdown)
set_tests_properties(agc_driver_resolution_shutdown PROPERTIES TIMEOUT 10 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_wait_shutdown PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_driver_wait_shutdown agc_driver_resolution_shutdown PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
@ -485,6 +489,11 @@ target_link_libraries(agc_masked_select_tests PRIVATE shader_recompiler_agc_driv
set_target_properties(agc_masked_select_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_masked_select_tests)
add_test_executable(agc_vector_window_tests EXCLUDE_FROM_ALL tests/VectorWindow.cpp)
target_link_libraries(agc_vector_window_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_vector_window_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_vector_window_tests)
add_test_executable(agc_constant_bit_test_tests EXCLUDE_FROM_ALL tests/ConstantBitTest.cpp)
target_link_libraries(agc_constant_bit_test_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_constant_bit_test_tests PROPERTIES CXX_EXTENSIONS OFF)
@ -539,6 +548,19 @@ if(BUILD_TESTING)
endif()
endif()
add_test_executable(agc_vop3_interp_decode_tests EXCLUDE_FROM_ALL tests/Vop3InterpDecode.cpp)
target_link_libraries(agc_vop3_interp_decode_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_vop3_interp_decode_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_vop3_interp_decode_tests)
if(BUILD_TESTING)
set_target_properties(agc_vop3_interp_decode_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_vop3_interp_decode COMMAND agc_vop3_interp_decode_tests)
if(WIN32)
set_tests_properties(agc_vop3_interp_decode PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_pass_hazards_tests EXCLUDE_FROM_ALL tests/PassHazards.cpp Graphics/src/PassHazards.cpp)
target_include_directories(agc_pass_hazards_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
target_link_libraries(agc_pass_hazards_tests PRIVATE libc)
@ -775,6 +797,13 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_storage_baseline_settled COMMAND agc_driver_recorder_tests --settled-baseline-only)
add_test(NAME agc_storage_upload_lifetime COMMAND agc_driver_recorder_tests --storage-upload-lifetime-only)
add_test(NAME agc_storage_upload_failure COMMAND agc_driver_recorder_tests --storage-upload-failure-only)
set_tests_properties(agc_storage_upload_lifetime agc_storage_upload_failure PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_storage_upload_lifetime agc_storage_upload_failure PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
set_tests_properties(agc_storage_baseline_settled PROPERTIES SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_storage_baseline_settled PROPERTIES ENVIRONMENT_MODIFICATION
@ -786,8 +815,12 @@ if(BUILD_TESTING)
set_tests_properties(agc_unit_shadow PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000")
add_test(NAME agc_descriptor_pool_recycling COMMAND agc_driver_recorder_tests --descriptor-pool-only)
set_tests_properties(agc_descriptor_pool_recycling PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME agc_draw_snapshot_cap COMMAND agc_driver_recorder_tests --draw-snapshot-cap-only)
set_tests_properties(agc_draw_snapshot_cap PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME agc_multisample_guest_access COMMAND agc_driver_recorder_tests --multisample-only)
set_tests_properties(agc_multisample_guest_access PROPERTIES SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_host_import_unmap agc_unit_shadow agc_descriptor_pool_recycling PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_host_import_unmap agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
@ -837,6 +870,7 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL
tests/DepthSurfaceReuse.cpp
Graphics/src/Draw.cpp
Graphics/src/DepthSurface.cpp
Graphics/src/MultisampleTarget.cpp
Graphics/src/Pipeline.cpp
Graphics/src/PipelineLibrary.cpp
Graphics/src/State.cpp
@ -994,15 +1028,17 @@ if(BUILD_TESTING)
endif()
add_test(NAME agc_shader_bindless COMMAND agc_shader_memory_tests --bindless)
set_tests_properties(agc_shader_bindless PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_shader_srt_read_patching COMMAND agc_shader_memory_tests --srt-read-patching)
set_tests_properties(agc_shader_srt_read_patching PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_shader_frontend_pair COMMAND agc_shader_memory_tests --frontend-pair)
add_test(NAME agc_shader_frontend_pair_off COMMAND agc_shader_memory_tests --frontend-pair)
set_tests_properties(agc_shader_frontend_pair PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
set_tests_properties(agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_FRONTEND_PAIR=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_shader_bindless agc_shader_frontend_pair agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_frontend_pair agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
foreach(agcTest IN ITEMS agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_constant_bit_test_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests ${agcExecutionTests})
if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests")
foreach(agcTest IN ITEMS agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vector_window_tests agc_constant_bit_test_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests ${agcExecutionTests})
if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests")
continue()
endif()
string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}")
@ -1014,6 +1050,39 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
add_test(NAME agc_driver_emulated_color_compare_srgb_decode COMMAND agc_driver_emulated_color_compare_tests)
set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SRGB_SHADER_DECODE=1"
SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
set_tests_properties(agc_driver_emulated_color_compare agc_driver_emulated_color_compare_srgb_decode PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_shared_write_partial_collect COMMAND agc_shared_write_tests --partial-collect)
set_tests_properties(agc_shared_write_partial_collect PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30)
if(WIN32)
add_test(NAME agc_shared_write_partial_collect_two_pass COMMAND agc_shared_write_tests --partial-collect)
set_tests_properties(agc_shared_write_partial_collect_two_pass PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_SINGLE_PASS_COLLECT=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30)
set_tests_properties(agc_shared_write_partial_collect agc_shared_write_partial_collect_two_pass PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
set_target_properties(agc_driver_pipeline_optimized_readback_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
foreach(pipelineReadbackCase IN ITEMS d32 d16 color32 color64)
foreach(pipelineReadbackMode IN ITEMS default fast)
set(pipelineReadbackTest agc_driver_pipeline_optimized_readback_${pipelineReadbackCase})
set(pipelineReadbackEnvironment "ANYPS5_NO_SHADER_CACHE=1")
if(pipelineReadbackMode STREQUAL "fast")
string(APPEND pipelineReadbackTest "_fast")
list(APPEND pipelineReadbackEnvironment "APS5_NO_PIPELINE_LTO=1")
endif()
add_test(NAME ${pipelineReadbackTest} COMMAND agc_driver_pipeline_optimized_readback_tests ${pipelineReadbackCase})
set_tests_properties(${pipelineReadbackTest} PROPERTIES ENVIRONMENT "${pipelineReadbackEnvironment}" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(${pipelineReadbackTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
endforeach()
add_test(NAME agc_driver_f64_denorm_preserve_spirv COMMAND agc_driver_f64_fma_subnormals_tests --verify-spv-denorm-mode)
set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
@ -1021,8 +1090,12 @@ if(BUILD_TESTING)
endif()
add_test(NAME agc_structurizer_intersection_off COMMAND agc_structurizer_tests)
set_tests_properties(agc_structurizer_intersection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_DOM_INTERSECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_structurizer_iteration_selection_off COMMAND agc_structurizer_tests)
set_tests_properties(agc_structurizer_iteration_selection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_ITERATION_SELECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_shader_control_flow_iteration_selection_off COMMAND agc_shader_control_flow_tests)
set_tests_properties(agc_shader_control_flow_iteration_selection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_ITERATION_SELECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_structurizer_intersection_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_structurizer_intersection_off agc_structurizer_iteration_selection_off agc_shader_control_flow_iteration_selection_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_gated_dispatch_capture_short_timeout COMMAND agc_driver_gated_dispatch_capture_tests)
set_tests_properties(agc_driver_gated_dispatch_capture_short_timeout PROPERTIES
@ -1035,8 +1108,8 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_provoking_vertex PROPERTIES TIMEOUT 30)
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 5)
add_test(NAME agc_driver_flat_apertures_${flatAperturesPart} COMMAND agc_driver_flat_apertures_tests --part ${flatAperturesPart}/6)
foreach(flatAperturesPart RANGE 13)
add_test(NAME agc_driver_flat_apertures_${flatAperturesPart} COMMAND agc_driver_flat_apertures_tests --part ${flatAperturesPart}/14)
set_tests_properties(agc_driver_flat_apertures_${flatAperturesPart} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_flat_apertures_${flatAperturesPart} PROPERTIES ENVIRONMENT_MODIFICATION
@ -1046,8 +1119,12 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_storage_cache_budget PROPERTIES TIMEOUT 30)
add_test(NAME agc_driver_sampled_cache_budget_memory COMMAND agc_driver_sampled_cache_budget_tests memory)
set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77)
add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission)
add_test(NAME agc_driver_resource_cache_oversized_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-oversized)
add_test(NAME agc_driver_resource_cache_storage_clear COMMAND agc_driver_sampled_cache_budget_tests resource-clear)
set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_pm4_failure COMMAND agc_driver_pm4_tests failure)
@ -1105,7 +1182,7 @@ if(BUILD_TESTING)
endif()
endforeach()
endforeach()
foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable)
foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync)
set(preparedTest "agc_driver_prepared_shaders_${preparedMode}")
if(preparedMode STREQUAL "indirect")
add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --indirect)
@ -1113,6 +1190,8 @@ if(BUILD_TESTING)
add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --deferred)
elseif(preparedMode STREQUAL "deferred_undecodable")
add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --deferred-undecodable)
elseif(preparedMode STREQUAL "registered_workgroup" OR preparedMode STREQUAL "registered_workgroup_sync")
add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --registered-workgroup)
elseif(preparedMode STREQUAL "debug_failure")
add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --fail-before-registration)
set_tests_properties(${preparedTest} PROPERTIES WILL_FAIL TRUE TIMEOUT 30)
@ -1122,6 +1201,8 @@ if(BUILD_TESTING)
set_tests_properties(${preparedTest} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(preparedMode STREQUAL "uncached")
set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_NO_DISPATCH_CACHE=1")
elseif(preparedMode STREQUAL "registered_workgroup_sync")
set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_SYNC_SHADER_PREPARE=1")
elseif(preparedMode STREQUAL "debug" OR preparedMode STREQUAL "debug_failure")
set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_PROFILE_DRAW=1" "APS5_NO_UNLOCKED_DEVICE=1")
endif()
@ -1241,6 +1322,60 @@ endif()
set_target_properties(agc_shader_replay PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_shader_replay)
add_executable(agc_compute_pipeline_replay EXCLUDE_FROM_ALL tools/ComputePipelineReplay.cpp)
target_include_directories(agc_compute_pipeline_replay PRIVATE
${LIBS_INCLUDE_DIR}
${CMAKE_SOURCE_DIR}/core/shader/recompiler
${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}
${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR}
)
target_link_libraries(agc_compute_pipeline_replay PRIVATE libSceAgcDriver libc)
set_target_properties(agc_compute_pipeline_replay PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_compute_pipeline_replay)
if(BUILD_TESTING)
add_test_executable(agc_compute_pipeline_capture_tests tests/ComputePipelineCapture.cpp Execution/src/ComputePipelineCapture.cpp)
target_include_directories(agc_compute_pipeline_capture_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
target_link_libraries(agc_compute_pipeline_capture_tests PRIVATE libc)
set_target_properties(agc_compute_pipeline_capture_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_compute_pipeline_capture_tests)
add_test(NAME agc_compute_descriptor_layout_flags COMMAND agc_driver_graphics_tests --descriptor-layout-flags)
set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests)
set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_compute_pipeline_capture PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
if(BUILD_TESTING AND ANYPS5_ENABLE_SPIRV_TOOLS)
add_test_executable(agc_compute_pipeline_replay_tests tests/ComputePipelineReplay.cpp)
target_include_directories(agc_compute_pipeline_replay_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_compute_pipeline_replay_tests PRIVATE libSceAgcDriver libc)
set_target_properties(agc_compute_pipeline_replay_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_compute_pipeline_replay_tests)
add_test(NAME agc_compute_pipeline_replay COMMAND agc_compute_pipeline_replay_tests)
set_tests_properties(agc_compute_pipeline_replay PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77)
add_dependencies(agc_compute_pipeline_replay_tests agc_compute_pipeline_replay agc_driver_branch_past_endpgm_tests)
add_test(NAME agc_compute_pipeline_capture_run COMMAND ${CMAKE_COMMAND}
-DFIXTURE=$<TARGET_FILE:agc_driver_branch_past_endpgm_tests>
-DREPLAY=$<TARGET_FILE:agc_compute_pipeline_replay>
-DOUTPUT_ROOT=${CMAKE_CURRENT_BINARY_DIR}/capture-tests
-P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ComputePipelineCaptureRun.cmake)
set_tests_properties(agc_compute_pipeline_capture_run PROPERTIES TIMEOUT 30 SKIP_REGULAR_EXPRESSION "compute pipeline capture run: skipped, no usable Vulkan device")
if(WIN32)
set_tests_properties(agc_compute_pipeline_replay agc_compute_pipeline_capture_run PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
if(BUILD_TESTING)
add_test_executable(texture_format_lookup_tests ${LIBS_INCLUDE_DIR}/tests/TextureFormatLookup.cpp)
target_include_directories(texture_format_lookup_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})

View File

@ -0,0 +1,77 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_COMPUTEPIPELINECAPTURE_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_COMPUTEPIPELINECAPTURE_HPP
#include <vulkan/vulkan.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <span>
#include <string>
#include <vector>
namespace AgcDriver {
struct ComputeDescriptorBinding {
std::uint32_t binding = 0;
VkDescriptorType type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
std::uint32_t count = 1;
VkShaderStageFlags stages = VK_SHADER_STAGE_COMPUTE_BIT;
VkDescriptorBindingFlags flags = 0;
};
struct ComputeDescriptorSetLayout {
VkDescriptorSetLayoutCreateFlags flags = 0;
std::vector<ComputeDescriptorBinding> bindings;
};
struct ComputeSpecialization {
std::uint32_t id = 0;
std::uint32_t value = 0;
};
struct ComputePipelineCapture {
static constexpr std::uint32_t Version = 1;
static constexpr std::uint32_t MaxSpirvWords = 8u * 1024u * 1024u;
static constexpr std::size_t MaxFileBytes = 36u * 1024u * 1024u;
std::vector<std::uint32_t> spirv;
std::vector<ComputeDescriptorSetLayout> sets;
std::vector<VkPushConstantRange> pushConstants;
std::vector<ComputeSpecialization> specialization;
std::uint32_t requiredSubgroupSize = 0;
VkPipelineCreateFlags flags = 0;
VkPipelineShaderStageCreateFlags stageFlags = 0;
std::string deviceName;
std::uint32_t vendorId = 0;
std::uint32_t deviceId = 0;
std::uint32_t driverVersion = 0;
std::uint32_t apiVersion = 0;
std::array<std::uint8_t, VK_UUID_SIZE> pipelineCacheUuid{};
void Validate() const;
std::vector<std::byte> Encode() const;
static ComputePipelineCapture Decode(std::span<const std::byte> bytes);
static ComputePipelineCapture Read(const std::filesystem::path& path);
std::filesystem::path Write(const std::filesystem::path& directory) const;
std::string Identity() const;
};
struct ComputePipelineReplayResult {
double moduleMilliseconds = 0;
double layoutMilliseconds = 0;
double pipelineMilliseconds = 0;
double cacheMilliseconds = 0;
double totalMilliseconds = 0;
std::string deviceName;
std::uint32_t vendorId = 0;
std::uint32_t deviceId = 0;
std::uint32_t driverVersion = 0;
std::uint32_t apiVersion = 0;
std::array<std::uint8_t, VK_UUID_SIZE> pipelineCacheUuid{};
std::size_t initialCacheBytes = 0;
std::vector<std::byte> cache;
};
}
#endif

View File

@ -343,6 +343,7 @@ private:
std::uint64_t queue0Executing = 0;
std::atomic<std::uint32_t> orderHolders{0};
std::atomic<std::uint32_t> runningWorkers{0};
std::atomic<std::uint64_t> idleEpoch{0};
std::atomic<std::uint64_t> queue0Awaited{0};
std::atomic<std::uint32_t> flipsAhead{0};
std::atomic<std::uint32_t> flipHolders{0};

View File

@ -92,6 +92,7 @@ std::shared_ptr<const ShaderSnapshot> ProgramSnapshot(const ShaderRegistry& shad
void NoteRawGraphicsDraw();
bool RawGraphicsDrawn();
bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset);
bool VertexWorkgroupRequired(const QueueState& queue, const ShaderRegistry& registry);
std::shared_ptr<const ShaderRecompiler::SourceHandle> PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation = nullptr);

View File

@ -183,6 +183,7 @@ unsigned long long CodeOffset(const void* address);
// every read and store through this interface calls first. Code that reads guest memory directly
// calls FlushGpuWrites itself.
void SetFlushHook(void (*hook)(std::uint64_t address, std::size_t bytes));
void SetReadGuard(void (*guard)(std::uint64_t address, std::size_t bytes));
void FlushGpuWrites(std::uint64_t address, std::size_t bytes);
// Moves with every ForgetPages call (the libc invalidator: memory unmapped or re-registered): odd
// while the call stores its page states, moved past it after, so a reader that loads an even

View File

@ -16,6 +16,7 @@ public:
ShaderPreparationTransaction& operator=(const ShaderPreparationTransaction&) = delete;
PreparedShaderState& Edit(const ShaderSnapshot& snapshot);
const PreparedShaderState& Read(const ShaderSnapshot& snapshot) const;
void Hold();
void Commit();
private:

View File

@ -7,6 +7,7 @@
#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp"
#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Recipe.hpp"
#include "prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
@ -31,6 +32,9 @@ struct RecordedDispatch;
class VulkanDevice {
public:
explicit VulkanDevice(const PresentationWindow* window = nullptr);
struct CompileOnly {};
explicit VulkanDevice(CompileOnly);
ComputePipelineReplayResult ReplayComputePipeline(const ComputePipelineCapture& capture, std::span<const std::byte> initialCache = {});
~VulkanDevice();
VulkanDevice(const VulkanDevice&) = delete;
VulkanDevice& operator=(const VulkanDevice&) = delete;
@ -57,6 +61,7 @@ public:
// mutex through the flush hook, so a thread submitting on another queue's behalf (a WAIT_REG_MEM
// poller, a compute worker's between-packet flush) passes false: it must only submit, never wait.
void SubmitRecorded(bool reapFirst = true);
void StoreIdleResults(bool reapFirst = true);
// Unlocked drain: SubmitAndEpoch (under the mutex) returns the serial covering every recorded
// batch (0: nothing in flight); WaitRecorded(serial) waits for it WITHOUT the mutex (the caller
// holds a shared_ptr to this device); ReapRecorded(serial) (under the mutex) runs the completions
@ -296,6 +301,8 @@ public:
void EnqueueDraw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots = {});
private:
VulkanDevice(const PresentationWindow* window, bool compileOnly);
friend struct VulkanDeviceTestAccess;
// The device's Graphics::Context: a copy of the one built at setup (its instance functions
// resolved then, its function table filled then), or with APS5_NO_CONTEXT_CACHE=1 built anew.
Graphics::Context graphicsContext() const;

View File

@ -0,0 +1,265 @@
#include "prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp"
#include <algorithm>
#include <fstream>
#include <iomanip>
#include <limits>
#include <mutex>
#include <set>
#include <sstream>
#include <stdexcept>
namespace AgcDriver {
namespace {
void RequireCapture(bool condition, const char* message) {
if (!condition) throw std::runtime_error(std::string("compute pipeline capture: ") + message);
}
std::string Hash(std::span<const std::byte> bytes) {
std::uint64_t hash = 14695981039346656037ull;
for (const auto value : bytes) {
hash ^= std::to_integer<std::uint8_t>(value);
hash *= 1099511628211ull;
}
std::ostringstream result;
result << std::hex << std::setfill('0') << std::setw(16) << hash;
return result.str();
}
class Encoder {
public:
void Word(std::uint32_t value) {
for (unsigned shift = 0; shift < 32; shift += 8) bytes.push_back(static_cast<std::byte>((value >> shift) & 0xffu));
}
void String(const std::string& value) {
Word(static_cast<std::uint32_t>(value.size()));
for (const auto character : value) bytes.push_back(static_cast<std::byte>(static_cast<unsigned char>(character)));
}
std::vector<std::byte> bytes;
};
class Decoder {
public:
explicit Decoder(std::span<const std::byte> bytes) : bytes(bytes) {}
std::uint32_t Word() {
RequireCapture(bytes.size() - cursor >= 4, "truncated integer");
std::uint32_t value = 0;
for (unsigned shift = 0; shift < 32; shift += 8) value |= std::to_integer<std::uint32_t>(bytes[cursor++]) << shift;
return value;
}
std::uint32_t Count(std::uint32_t maximum, std::size_t minimumBytes) {
const auto count = Word();
RequireCapture(count <= maximum && count <= (bytes.size() - cursor) / minimumBytes, "count exceeds record bounds");
return count;
}
std::string String() {
const auto count = Count(VK_MAX_PHYSICAL_DEVICE_NAME_SIZE - 1, 1);
std::string result;
for (std::uint32_t index = 0; index < count; ++index) result.push_back(static_cast<char>(std::to_integer<unsigned char>(bytes[cursor++])));
return result;
}
bool Finished() const { return cursor == bytes.size(); }
private:
std::span<const std::byte> bytes;
std::size_t cursor = 0;
};
std::vector<std::byte> ReadBytes(const std::filesystem::path& path) {
const auto size = std::filesystem::file_size(path);
RequireCapture(size <= ComputePipelineCapture::MaxFileBytes, "file exceeds 36 MiB");
std::ifstream input(path, std::ios::binary);
RequireCapture(input.good(), "cannot open capture");
std::vector<std::byte> bytes(static_cast<std::size_t>(size));
input.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
RequireCapture(input.good() && input.peek() == std::char_traits<char>::eof(), "capture changed while reading");
return bytes;
}
}
void ComputePipelineCapture::Validate() const {
RequireCapture(spirv.size() >= 5 && spirv.size() <= MaxSpirvWords, "SPIR-V word count out of bounds");
RequireCapture(spirv[0] == 0x07230203u && spirv[1] >= 0x00010000u && spirv[1] <= 0x00010600u && (spirv[1] & 0xffu) == 0 && spirv[3] != 0 && spirv[3] <= 0x003fffffu && spirv[4] == 0, "invalid SPIR-V header");
bool mainEntry = false;
for (std::size_t cursor = 5; cursor < spirv.size();) {
const auto count = spirv[cursor] >> 16u;
RequireCapture(count != 0 && count <= spirv.size() - cursor, "truncated SPIR-V instruction");
if ((spirv[cursor] & 0xffffu) == 15u) {
RequireCapture(count >= 5 && spirv[cursor + 1] == 5u && spirv[cursor + 2] != 0 && spirv[cursor + 2] < spirv[3] && spirv[cursor + 3] == 0x6e69616du && spirv[cursor + 4] == 0, "only a compute main entry point is supported");
RequireCapture(!mainEntry, "multiple entry points are unsupported");
mainEntry = true;
}
cursor += count;
}
RequireCapture(mainEntry, "compute main entry point missing");
RequireCapture(deviceName.size() < VK_MAX_PHYSICAL_DEVICE_NAME_SIZE && deviceName.find('\0') == std::string::npos, "invalid device name");
RequireCapture((flags & ~(VK_PIPELINE_CREATE_DISPATCH_BASE_BIT | VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR)) == 0, "unsupported pipeline flags");
RequireCapture(stageFlags == 0, "unsupported shader stage flags");
RequireCapture(requiredSubgroupSize == 0 || (requiredSubgroupSize <= 128 && (requiredSubgroupSize & (requiredSubgroupSize - 1u)) == 0), "invalid required subgroup size");
RequireCapture(sets.size() <= 8, "too many descriptor sets");
std::size_t totalBindings = 0;
for (const auto& layout : sets) {
RequireCapture((layout.flags & ~VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) == 0, "unsupported descriptor layout flags");
RequireCapture(layout.bindings.size() <= 4096, "too many descriptor bindings");
totalBindings += layout.bindings.size();
std::set<std::uint32_t> used;
const auto highest = layout.bindings.empty() ? 0u : std::ranges::max(layout.bindings, {}, &ComputeDescriptorBinding::binding).binding;
for (const auto& binding : layout.bindings) {
RequireCapture(used.insert(binding.binding).second, "duplicate descriptor binding");
RequireCapture(binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER || binding.type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE || binding.type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE || binding.type == VK_DESCRIPTOR_TYPE_SAMPLER, "unsupported descriptor type");
RequireCapture(binding.count != 0 && binding.count <= 1048576, "invalid descriptor count");
RequireCapture(binding.stages == VK_SHADER_STAGE_COMPUTE_BIT, "unsupported descriptor stage flags");
constexpr VkDescriptorBindingFlags supported = VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT | VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT;
RequireCapture((binding.flags & ~supported) == 0, "unsupported descriptor binding flags");
RequireCapture((binding.flags & VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT) == 0 || (binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER && (layout.flags & VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) != 0), "unsupported update-after-bind descriptor");
RequireCapture((binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) == 0 || binding.binding == highest, "variable descriptor count must use the highest binding");
}
}
RequireCapture(totalBindings <= 4096, "too many total descriptor bindings");
RequireCapture(pushConstants.size() <= 1, "too many compute push constant ranges");
for (const auto& range : pushConstants) RequireCapture(range.stageFlags == VK_SHADER_STAGE_COMPUTE_BIT && range.size != 0 && range.size <= 4096 && range.offset <= 4096 - range.size && (range.offset % 4) == 0 && (range.size % 4) == 0, "invalid push constant range");
RequireCapture(specialization.size() <= 4096, "too many specialization values");
std::set<std::uint32_t> specializationIds;
for (const auto& value : specialization) RequireCapture(specializationIds.insert(value.id).second, "duplicate specialization ID");
}
std::vector<std::byte> ComputePipelineCapture::Encode() const {
Validate();
Encoder output;
output.Word(0x35535041u);
output.Word(0x504d4f43u);
output.Word(Version);
output.Word(flags);
output.Word(stageFlags);
output.Word(requiredSubgroupSize);
output.Word(vendorId);
output.Word(deviceId);
output.Word(driverVersion);
output.Word(apiVersion);
for (const auto value : pipelineCacheUuid) output.Word(value);
output.String(deviceName);
output.Word(static_cast<std::uint32_t>(sets.size()));
for (const auto& layout : sets) {
output.Word(layout.flags);
output.Word(static_cast<std::uint32_t>(layout.bindings.size()));
for (const auto& binding : layout.bindings) {
output.Word(binding.binding);
output.Word(static_cast<std::uint32_t>(binding.type));
output.Word(binding.count);
output.Word(binding.stages);
output.Word(binding.flags);
}
}
output.Word(static_cast<std::uint32_t>(pushConstants.size()));
for (const auto& range : pushConstants) {
output.Word(range.stageFlags);
output.Word(range.offset);
output.Word(range.size);
}
output.Word(static_cast<std::uint32_t>(specialization.size()));
for (const auto& value : specialization) {
output.Word(value.id);
output.Word(value.value);
}
output.Word(static_cast<std::uint32_t>(spirv.size()));
for (const auto word : spirv) output.Word(word);
RequireCapture(output.bytes.size() <= MaxFileBytes, "encoded capture exceeds size bound");
return std::move(output.bytes);
}
ComputePipelineCapture ComputePipelineCapture::Decode(std::span<const std::byte> bytes) {
RequireCapture(bytes.size() <= MaxFileBytes, "record exceeds 36 MiB");
Decoder input(bytes);
RequireCapture(input.Word() == 0x35535041u && input.Word() == 0x504d4f43u, "invalid file signature");
RequireCapture(input.Word() == Version, "unsupported file version");
ComputePipelineCapture result;
result.flags = input.Word();
result.stageFlags = input.Word();
result.requiredSubgroupSize = input.Word();
result.vendorId = input.Word();
result.deviceId = input.Word();
result.driverVersion = input.Word();
result.apiVersion = input.Word();
for (auto& value : result.pipelineCacheUuid) {
const auto word = input.Word();
RequireCapture(word <= 255, "invalid cache UUID byte");
value = static_cast<std::uint8_t>(word);
}
result.deviceName = input.String();
result.sets.resize(input.Count(8, 8));
std::size_t totalBindings = 0;
for (auto& layout : result.sets) {
layout.flags = input.Word();
const auto count = input.Count(4096, 20);
totalBindings += count;
RequireCapture(totalBindings <= 4096, "too many total descriptor bindings");
layout.bindings.resize(count);
for (auto& binding : layout.bindings) {
binding.binding = input.Word();
binding.type = static_cast<VkDescriptorType>(input.Word());
binding.count = input.Word();
binding.stages = input.Word();
binding.flags = input.Word();
}
}
result.pushConstants.resize(input.Count(1, 12));
for (auto& range : result.pushConstants) {
range.stageFlags = input.Word();
range.offset = input.Word();
range.size = input.Word();
}
result.specialization.resize(input.Count(4096, 8));
for (auto& value : result.specialization) {
value.id = input.Word();
value.value = input.Word();
}
result.spirv.resize(input.Count(MaxSpirvWords, 4));
for (auto& word : result.spirv) word = input.Word();
RequireCapture(input.Finished(), "trailing bytes after capture");
result.Validate();
return result;
}
ComputePipelineCapture ComputePipelineCapture::Read(const std::filesystem::path& path) {
return Decode(ReadBytes(path));
}
std::string ComputePipelineCapture::Identity() const {
return Hash(Encode());
}
std::filesystem::path ComputePipelineCapture::Write(const std::filesystem::path& directory) const {
const auto bytes = Encode();
const auto recordDirectory = directory / ("compute-" + Hash(bytes));
const auto path = recordDirectory / "capture.aps5cp";
static std::mutex mutex;
std::lock_guard lock(mutex);
std::filesystem::create_directories(directory);
RequireCapture(!std::filesystem::is_symlink(std::filesystem::symlink_status(directory)), "capture directory is a symbolic link");
if (std::filesystem::exists(recordDirectory)) {
RequireCapture(std::filesystem::is_directory(recordDirectory) && !std::filesystem::is_symlink(std::filesystem::symlink_status(recordDirectory)) && !std::filesystem::is_symlink(std::filesystem::symlink_status(path)), "existing capture path is not a regular capture");
RequireCapture(ReadBytes(path) == bytes, "existing capture differs or is incomplete");
return path;
}
std::uintmax_t totalBytes = 0;
std::size_t count = 0;
for (const auto& entry : std::filesystem::directory_iterator(directory)) {
RequireCapture(++count < 128, "capture directory reached 128 entries");
RequireCapture(entry.is_directory() && !entry.is_symlink(), "capture directory contains an unexpected entry");
const auto capture = entry.path() / "capture.aps5cp";
RequireCapture(std::filesystem::is_regular_file(capture) && !std::filesystem::is_symlink(std::filesystem::symlink_status(capture)), "capture directory contains an incomplete record");
const auto size = std::filesystem::file_size(capture);
RequireCapture(size <= MaxFileBytes, "existing capture exceeds size bound");
totalBytes += size;
}
RequireCapture(totalBytes + bytes.size() <= 256u * 1024u * 1024u, "capture directory exceeds 256 MiB");
RequireCapture(std::filesystem::create_directory(recordDirectory), "capture directory was concurrently created");
std::ofstream output(path, std::ios::binary);
RequireCapture(output.good(), "cannot create capture");
output.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
output.close();
RequireCapture(output.good(), "cannot finish capture");
return path;
}
}

View File

@ -92,7 +92,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
resolveDrawDecode(queue, submission, decode, registerKey, drawKey, profile);
const auto& graphics = decode->state;
const auto& pixel = decode->pixel;
auto pixel = decode->pixel;
std::vector<DrawProgram> programs = decode->programs;
const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) {
if (!graphics.stages.mesh) return;
@ -144,7 +144,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
const auto& program = programs[i];
if (program.binary.stage == Stage::Fragment || roles[i] == Role::GeometryBack) return;
decodeReads[i].clear();
vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i]);
vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr);
};
if (!registerKey) {
for (std::size_t i = 0; i < programs.size(); ++i) decodeVertexInfo(i);
@ -204,6 +204,13 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
memory.insert(memory.end(), matchedRegions[i].begin(), matchedRegions[i].end());
} else {
resultIndex[i] = results.size();
if (program.binary.stage == ShaderRecompiler::ShaderStage::Fragment) {
for (auto previous = i; previous-- > 0;) {
if (programResults[previous] == nullptr) continue;
pixel = Graphics::DefaultUnexportedInputs(decode->pixel, programResults[previous]->parameterExports);
break;
}
}
results.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected));
if (!rejected.empty()) return DrawVerdict::Rejected;
programResults[i] = &results.back();

View File

@ -109,16 +109,7 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const
std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Submission& submission) {
auto product = std::make_shared<DrawDecode>();
bool vertexWorkgroup = false;
const auto stages = queue.context.find(0x2d5);
const auto low = queue.shader.find(0xc8);
const auto high = queue.shader.find(0xc9);
if (stages != queue.context.end() && (stages->second & 0x02002024u) == 0x2000u && low != queue.shader.end() && high != queue.shader.end()) {
const auto address = (static_cast<std::uint64_t>(low->second) << 8u) | (static_cast<std::uint64_t>(high->second & 0xffu) << 40u);
const auto front = ProgramSnapshot(*submission.shaders, address, {2});
vertexWorkgroup = front->header.empty() && ProgramUsesWorkgroup(*front, static_cast<std::size_t>((address - front->codeAddress) / sizeof(std::uint32_t)));
}
product->state = Graphics::DecodeState(queue, vertexWorkgroup);
product->state = Graphics::DecodeState(queue, VertexWorkgroupRequired(queue, *submission.shaders));
DecodeGraphicsPrograms(*product, queue, *submission.shaders, false, true);
product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(product->state), Graphics::PixelProgramSkipped(queue));
product->pixel.targetExportPacking = Graphics::ExportPackings(product->state);

View File

@ -123,13 +123,19 @@ extern "C" void AgcDriverReportFailure_nid_postfix(std::exception_ptr error) {
AgcDriver::ReportFailure(error);
}
extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) try {
AgcDriver::DriverDetail::Driver::Get().ResolveShaderAbi(shader, context, primitive);
} catch (const ProcessShutdown&) {
LibcAwaitExit_nid_postfix();
}
extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) {
extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) try {
AgcDriver::DriverDetail::Driver::Get().ResolveGraphicsAbi(vertex, pixel, primitiveType);
} catch (const ProcessShutdown&) {
LibcAwaitExit_nid_postfix();
}
extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) try {
AgcDriver::DriverDetail::Driver::Get().ResolveGraphicsStagesAbi(stages, context, primitive);
} catch (const ProcessShutdown&) {
LibcAwaitExit_nid_postfix();
}

View File

@ -6,6 +6,7 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include <algorithm>
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include <bit>
#include <cstdlib>
#include <optional>
@ -265,7 +266,10 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons
for (std::size_t done = 0; done < bytes; done += chunk) GuestMemory::Write(base + done, std::span<const std::byte>(block).first(std::min(chunk, bytes - done)), 16);
phase(FillCpu);
}
if (pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3]) Graphics::NoteDepthMetadataFill(base, bytes, pattern[0]);
if (pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3]) {
Graphics::NoteDepthMetadataFill(base, bytes, pattern[0]);
Graphics::NoteColorMetadataFill(base, bytes, pattern[0]);
}
if (profile) {
const auto now = std::chrono::steady_clock::now();
const auto cover = static_cast<std::size_t>(coverage.cover);

View File

@ -8,6 +8,7 @@
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include <cstdlib>
#include <cstring>
#include <limits>
@ -60,7 +61,12 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue
}
}
if (opcode == 0x50 && packet.size() >= 7) {
if (Pm4::DmaSource(packet) == 2 && Pm4::DmaDestination(packet) != 1) Graphics::NoteDepthMetadataFill(packet[4] | (static_cast<std::uint64_t>(packet[5]) << 32u), packet[6] & 0x3ffffffu, packet[2]);
if (Pm4::DmaSource(packet) == 0 || Pm4::DmaSource(packet) == 3) Graphics::RequireNoMultisampleTarget(packet[2] | (static_cast<std::uint64_t>(packet[3]) << 32u), packet[6] & 0x3ffffffu, "a DMA copy");
if (Pm4::DmaSource(packet) == 2 && Pm4::DmaDestination(packet) != 1) {
const auto address = packet[4] | (static_cast<std::uint64_t>(packet[5]) << 32u);
Graphics::NoteDepthMetadataFill(address, packet[6] & 0x3ffffffu, packet[2]);
Graphics::NoteColorMetadataFill(address, packet[6] & 0x3ffffffu, packet[2]);
}
}
if (!drainAll && !endOfPipeInterrupt && (opcode == 0x49 || opcode == 0x37)) {
if (const auto label = Pm4::DecodeLabelWrite(packet)) {

View File

@ -6,6 +6,7 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include <algorithm>
#include <cstdlib>
namespace AgcDriver::DriverDetail {
@ -80,6 +81,24 @@ void Driver::run(std::uint32_t id) noexcept {
try {
for (;;) {
submission = Submission{};
struct Running {
std::atomic<std::uint32_t>& count;
std::atomic<std::uint64_t>& idleEpoch;
std::condition_variable& changed;
bool active = false;
Running(std::atomic<std::uint32_t>& count, std::atomic<std::uint64_t>& idleEpoch, std::condition_variable& changed)
: count(count), idleEpoch(idleEpoch), changed(changed) {}
void Start() {
count.fetch_add(1, std::memory_order_acq_rel);
active = true;
}
~Running() {
if (active && count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
idleEpoch.fetch_add(1, std::memory_order_release);
changed.notify_all();
}
}
} running{runningWorkers, idleEpoch, changed};
static const bool traceGpu = std::getenv("APS5_TRACE_GPU") != nullptr;
{
PerformanceContext timingContext(id == 0 ? frameTiming() : FrameTiming::Async());
@ -91,6 +110,10 @@ void Driver::run(std::uint32_t id) noexcept {
workerQueued() = &worker.queued;
if (traceGpu && pending.empty()) std::fprintf(stderr, "[gpu] %.1f idle queue=0x%x\n", TraceMs(), id);
const auto ready = [&] { return failure || stopping || !pending.empty(); };
const auto allQueuesIdle = [&] {
return runningWorkers.load(std::memory_order_acquire) == 0
&& std::all_of(workers.begin(), workers.end(), [](const auto& item) { return item.second.pending.empty(); });
};
const auto poll = [&](const auto& done) {
#ifdef _WIN32
lock.unlock();
@ -104,9 +127,21 @@ void Driver::run(std::uint32_t id) noexcept {
while (!ready()) {
if (!completionsPending() || (id != 0 && Graphics::Recorder::PendingCompletionLabels() == 0)) {
const auto idleEpochAtCheck = idleEpoch.load(std::memory_order_acquire);
const bool storeIdleResults = id == 0 && allQueuesIdle() && !completionsPending();
lock.unlock();
if (storeIdleResults) {
if (const auto localDevice = device.Load()) localDevice->StoreIdleResults();
}
lock.lock();
if (ready()) break;
if (id == 0 && allQueuesIdle() && !completionsPending()
&& (!storeIdleResults || idleEpoch.load(std::memory_order_acquire) != idleEpochAtCheck)) continue;
if (id == 0) queue0Dormant.store(true, std::memory_order_relaxed);
changed.wait(lock, ready);
const auto idleEpochAtWait = idleEpoch.load(std::memory_order_acquire);
changed.wait(lock, [&] { return ready() || (id == 0 && idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait); });
if (id == 0) queue0Dormant.store(false, std::memory_order_relaxed);
if (id == 0 && !ready() && idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait) continue;
break;
}
if (poll(ready)) break;
@ -133,16 +168,10 @@ void Driver::run(std::uint32_t id) noexcept {
if (APS5_ENABLE_TIMING_LOG) submission.orderedAt = std::chrono::steady_clock::now();
timing.Mark("dequeue_order");
worker.queued.fetch_sub(1, std::memory_order_acq_rel);
running.Start();
if (profile && submission.enqueuedAt != std::chrono::steady_clock::time_point{}) costs.dequeueNs += static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - submission.enqueuedAt).count());
}
{
struct Running {
std::atomic<std::uint32_t>& count;
explicit Running(std::atomic<std::uint32_t>& count) : count(count) { count.fetch_add(1, std::memory_order_acq_rel); }
~Running() { count.fetch_sub(1, std::memory_order_acq_rel); }
} running{runningWorkers};
execute(submission);
}
execute(submission);
if (traceGpu) std::fprintf(stderr, "[gpu] %.1f done serial=%llu queue=0x%x\n", TraceMs(), static_cast<unsigned long long>(submission.serial), id);
const auto completeStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
PerformanceContext completionContext(id == 0 ? frameTiming() : FrameTiming::Async());

View File

@ -332,8 +332,12 @@ bool Driver::orderReleased(std::uint32_t queue, std::uint64_t received) const {
}
void Driver::noteWaitBlocked(std::uint32_t queue, std::uint64_t awaited, bool blocked) {
if (blocked) runningWorkers.fetch_sub(1, std::memory_order_acq_rel);
else runningWorkers.fetch_add(1, std::memory_order_acq_rel);
if (blocked) {
if (runningWorkers.fetch_sub(1, std::memory_order_acq_rel) == 1) {
idleEpoch.fetch_add(1, std::memory_order_release);
changed.notify_all();
}
} else runningWorkers.fetch_add(1, std::memory_order_acq_rel);
if (queue == 0) queue0Awaited.store(blocked ? awaited : 0, std::memory_order_release);
if (queue == 0 && !blocked) queue0AwaitsTitle.store(false, std::memory_order_release);
if (blocked && orderHolders.load(std::memory_order_acquire) != 0) {

View File

@ -130,6 +130,12 @@ void AwaitRegisteredPreparation(PreparedShaders& prepared, std::unique_lock<std:
if (prepared.failure != nullptr) std::rethrow_exception(prepared.failure);
}
bool CommittedAbi(const ShaderSnapshot& snapshot, std::span<const std::uint64_t> key) {
std::unique_lock lock(snapshot.prepared->mutex);
AwaitRegisteredPreparation(*snapshot.prepared, lock);
return std::ranges::any_of(snapshot.prepared->registeredAbis, [&](const auto& abi) { return std::ranges::equal(abi, key); });
}
}
bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset) {
@ -147,6 +153,22 @@ bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset
}
bool VertexWorkgroupRequired(const QueueState& queue, const ShaderRegistry& registry) {
Graphics::NoteRegisterRead(Graphics::RegisterBank::Context, 0x2d5);
const auto routing = queue.context.find(0x2d5);
if (routing == queue.context.end() || (routing->second & 0x02002024u) != 0x2000u) return false;
Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, 0xc8);
Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, 0xc9);
const auto low = queue.shader.find(0xc8);
const auto high = queue.shader.find(0xc9);
if (low == queue.shader.end() || high == queue.shader.end()) return false;
require((high->second & ~0xffu) == 0, "reserved graphics program address bits are set");
const auto address = (static_cast<std::uint64_t>(low->second) << 8u) | (static_cast<std::uint64_t>(high->second) << 40u);
const auto snapshot = ProgramSnapshot(registry, address, {2});
require((address - snapshot->codeAddress) % sizeof(std::uint32_t) == 0, "graphics entry point is not dword aligned");
return (snapshot->header.empty() || snapshot->type == 2) && ProgramUsesWorkgroup(*snapshot, (address - snapshot->codeAddress) / sizeof(std::uint32_t));
}
struct ShaderPreparationTransaction::State {
struct Change {
std::shared_ptr<PreparedShaders> destination;
@ -157,9 +179,13 @@ struct ShaderPreparationTransaction::State {
};
static std::mutex writers;
static thread_local State* active;
std::unique_lock<std::mutex> lock{writers};
std::unique_lock<std::mutex> lock{writers, std::defer_lock};
std::map<PreparedShaders*, Change> changes;
bool failed = false;
void Hold() {
if (!lock.owns_lock()) lock.lock();
}
};
std::mutex ShaderPreparationTransaction::State::writers;
@ -180,6 +206,7 @@ ShaderPreparationTransaction::~ShaderPreparationTransaction() {
PreparedShaderState& ShaderPreparationTransaction::Edit(const ShaderSnapshot& snapshot) {
require(!committed, "shader preparation transaction is already committed");
root->Hold();
auto& changes = root->changes;
const auto found = changes.find(snapshot.prepared.get());
if (found != changes.end()) {
@ -193,6 +220,7 @@ PreparedShaderState& ShaderPreparationTransaction::Edit(const ShaderSnapshot& sn
const PreparedShaderState& ShaderPreparationTransaction::Read(const ShaderSnapshot& snapshot) const {
require(!committed, "shader preparation transaction is already committed");
root->Hold();
auto& changes = root->changes;
const auto found = changes.find(snapshot.prepared.get());
if (found != changes.end()) return found->second.prepared;
@ -201,6 +229,11 @@ const PreparedShaderState& ShaderPreparationTransaction::Read(const ShaderSnapsh
return changes.emplace(snapshot.prepared.get(), State::Change{snapshot.prepared, *snapshot.prepared, false, snapshot.prepared->entries.size(), snapshot.prepared->registeredAbis.size()}).first->second.prepared;
}
void ShaderPreparationTransaction::Hold() {
require(!committed, "shader preparation transaction is already committed");
root->Hold();
}
void ShaderPreparationTransaction::Commit() {
require(!committed && !root->failed, "shader preparation transaction was aborted");
if (state != nullptr) {
@ -294,6 +327,7 @@ std::shared_ptr<const ShaderSnapshot> RegisteredProgram(const ShaderRegistry& re
void PublishRegisteredShader(std::shared_ptr<ShaderRegistry>& registry, const std::shared_ptr<const ShaderSnapshot>& snapshot) {
ShaderPreparationTransaction transaction;
transaction.Hold();
auto published = snapshot;
if (registry != nullptr) {
const auto found = registry->find(snapshot->codeAddress);
@ -606,6 +640,7 @@ std::unique_ptr<RegisteredPreparation> PlanRegistered(const ShaderSnapshot& snap
} else {
const auto routing = RegisterValue(state.context, 0x2d5);
wave = (routing & 0x00400000u) != 0 ? 32u : 64u;
if (registration && stage == Stage::Vertex && (routing & 0x02002024u) == 0x2000u && ProgramUsesWorkgroup(snapshot, codeOffset)) return nullptr;
if ((routing & 0x20u) != 0 || stage == Stage::Mesh || (routing & 4u) != 0 || ((routing >> 3u) & 3u) == 1u) {
if (registration) return {};
auto stageState = state;
@ -618,7 +653,7 @@ std::unique_ptr<RegisteredPreparation> PlanRegistered(const ShaderSnapshot& snap
if (firstUser == 0) userCount += 8;
}
plan->userData.resize(userCount);
if (stage != Stage::Compute && stage != Stage::Fragment && snapshot.type != 6) vertex = Graphics::DecodeVertexStageInfo(snapshot.header, snapshot.headerAddress, plan->userData, nullptr, true);
if (stage != Stage::Compute && stage != Stage::Fragment && snapshot.type != 6) vertex = Graphics::DecodeVertexStageInfo(snapshot.header, snapshot.headerAddress, plan->userData, nullptr, true, 8u - firstUser);
plan->swappc = {vertex.has_value(), firstUser, userCount};
if (deferred != nullptr) {
if (const auto counter = ShaderRecompiler::UnresolvableSwappcTarget(plan->decoded, &plan->swappc)) {
@ -805,7 +840,7 @@ std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decod
const bool fragment = program.binary.stage == ShaderRecompiler::ShaderStage::Fragment;
const auto wave = fragment ? decoded.state.stages.fragmentWaveSize : decoded.state.stages.vertexWaveSize;
std::optional<ShaderRecompiler::ShaderVertexStageInfo> vertex;
if (!fragment) vertex = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true);
if (!fragment) vertex = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true, 8u - program.firstUserSgpr);
pushOffset = Graphics::StagePushOffset(pushOffset, program.binary.stage, fixedSlots);
const auto offset = pushOffset;
const auto room = (fixedSlots && fragment ? Graphics::PipelinePushSlotBytes : capacity) - pushOffset % Graphics::PipelinePushSlotBytes;
@ -883,7 +918,7 @@ void Driver::ResolveGraphicsStagesAbi(std::span<const Shader* const> stages, std
return;
}
DrawDecode decoded{};
decoded.state.stages = Graphics::DecodeShaderStages(state);
decoded.state.stages = Graphics::DecodeShaderStages(state, VertexWorkgroupRequired(state, *registry));
DecodeGraphicsPrograms(decoded, state, *registry, true, false);
auto prepared = PrepareGraphicsStages(decoded, localDevice->Target());
for (auto& stage : prepared) {
@ -935,12 +970,14 @@ void Driver::ResolveShaderAbi(const Shader* shader, std::span<const ShaderRegist
struct RegisteredAbiKeyStorage {};
auto& key = HostThreadLocal<std::vector<std::uint64_t>, RegisteredAbiKeyStorage>();
BuildRegisteredAbiKey(state, *localDevice, key);
const bool committedAbi = CommittedAbi(*snapshot, key);
auto entries = committedAbi ? std::vector<PreparedShaders::Entry>{} : PrepareRegistered(*snapshot, *localDevice, state, false);
const auto& registeredAbis = transaction.Read(*snapshot).registeredAbis;
if (std::ranges::find(registeredAbis, key) != registeredAbis.end()) {
transaction.Commit();
return;
}
auto entries = PrepareRegistered(*snapshot, *localDevice, state, false);
if (committedAbi) entries = PrepareRegistered(*snapshot, *localDevice, state, false);
auto& prepared = transaction.Edit(*snapshot);
for (auto& entry : entries) {
const auto duplicate = std::ranges::any_of(prepared.entries, [&](const auto& existing) { return existing.handle->artifact == entry.handle->artifact; });
@ -1094,6 +1131,7 @@ void Driver::RegisterShader(const Shader* shader) {
snapshot.header.resize(fields.header_size);
std::memcpy(snapshot.header.data(), static_cast<const void*>(shader), fields.header_size);
transaction.Hold();
{
std::lock_guard lock(mutex);
rethrowFailure();

View File

@ -1058,6 +1058,10 @@ bool walkWrites(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop,
}
std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoized) {
if (bytes == 0 || bytes > std::numeric_limits<std::uint64_t>::max() - address) return 0;
#ifdef _WIN32
if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return 0;
#endif
auto& tracker = Tracker();
// Whole pages, so a page shared with the next range is collected with either.
constexpr std::uint64_t page = 4096;
@ -1185,6 +1189,10 @@ bool WriteWatched() {
}
bool Watched(std::uint64_t address, std::size_t bytes) {
if (bytes == 0 || bytes > std::numeric_limits<std::uint64_t>::max() - address) return false;
#ifdef _WIN32
if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return false;
#endif
auto& tracker = Tracker();
const auto lock = lockTracker(tracker);
tracker.initialize();
@ -1192,6 +1200,10 @@ bool Watched(std::uint64_t address, std::size_t bytes) {
}
bool UnchangedSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation) {
if (bytes == 0 || bytes > std::numeric_limits<std::uint64_t>::max() - address) return false;
#ifdef _WIN32
if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return false;
#endif
auto& tracker = Tracker();
const auto lock = lockTracker(tracker);
tracker.initialize();
@ -1208,6 +1220,9 @@ bool UnchangedSinceAll(std::span<const UnchangedQuery> queries) {
if (!tracker.watched) return false;
for (const auto& [address, bytes, generation] : queries) {
if (generation == 0 || bytes == 0 || !tracker.covers(address, bytes)) return false;
#ifdef _WIN32
if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return false;
#endif
const auto first = tracker.blockOf(address);
const auto last = tracker.blockOf(address + bytes - 1);
if (!tracker.unchanged(first, last, generation)) return false;
@ -1736,12 +1751,19 @@ void SetFlushHook(void (*hook)(std::uint64_t, std::size_t)) {
flushHook.store(hook, std::memory_order_release);
}
std::atomic<void (*)(std::uint64_t, std::size_t)> readGuard{nullptr};
void SetReadGuard(void (*guard)(std::uint64_t address, std::size_t bytes)) {
readGuard.store(guard, std::memory_order_release);
}
void FlushGpuWrites(std::uint64_t address, std::size_t bytes) {
if (const auto hook = flushHook.load(std::memory_order_acquire)) hook(address, bytes);
}
void Read(std::uint64_t address, std::span<std::byte> destination, std::size_t alignment) {
if (destination.empty()) return;
if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size());
FlushGpuWrites(address, destination.size());
const TimedAccess timed(CounterRead, destination.size());
CountReadCaller(__builtin_return_address(0));
@ -1758,6 +1780,7 @@ void Read(std::uint64_t address, std::span<std::byte> destination, std::size_t a
void ReadCommitted(std::uint64_t address, std::span<std::byte> destination) {
if (destination.empty()) return;
if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size());
FlushGpuWrites(address, destination.size());
const TimedAccess timed(CounterRead, destination.size());
CountReadCaller(__builtin_return_address(0));

View File

@ -453,7 +453,7 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
require(packet.size() >= 5, "WRITE_DATA has no data");
require((packet[1] & ~(0x40110f00u | WriteDataCachePolicy)) == 0, "WRITE_DATA engine or reserved fields are not implemented");
const auto destination = (packet[1] >> 8u) & 0xfu;
require(destination == 1 || destination == 2 || destination == 5, "WRITE_DATA register or GDS destination is not implemented");
require(destination == 1 || destination == 2 || destination == 3 || destination == 5, "WRITE_DATA register destination is not implemented");
require((packet[2] & 3u) == 0, "misaligned WRITE_DATA destination");
break;
}

View File

@ -2,6 +2,9 @@
#include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp"
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
#include "BdaAbi.hpp"
#if ANYPS5_ENABLE_SPIRV_TOOLS
#include "SpirvBackend/SpirvOptimizer.hpp"
#endif
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
@ -15,6 +18,7 @@
#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp"
#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp"
@ -70,6 +74,50 @@ void require(bool condition, const char* reason) {
if (!condition) throw std::runtime_error(std::string("Vulkan presentation: ") + reason);
}
bool CoreVulkanCapability(std::uint32_t capability) {
switch (capability) {
case spv::CapabilityMatrix:
case spv::CapabilityShader:
case spv::CapabilityInputAttachment:
case spv::CapabilitySampled1D:
case spv::CapabilityImage1D:
case spv::CapabilitySampledBuffer:
case spv::CapabilityImageBuffer:
case spv::CapabilityImageQuery:
case spv::CapabilityDerivativeControl:
case spv::CapabilityStorageImageExtendedFormats:
return true;
default:
return false;
}
}
void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, VkPipelineCache cache, VkShaderModule module, VkPipelineLayout layout, VkPipelineCreateFlags flags, VkPipelineShaderStageCreateFlags stageFlags, const VkSpecializationInfo* specialization, std::uint32_t subgroupSize, VkPipeline* pipeline) {
VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
info.flags = flags;
info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
info.stage.flags = stageFlags;
info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
info.stage.module = module;
info.stage.pName = "main";
info.stage.pSpecializationInfo = specialization;
VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT subgroup{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT};
subgroup.requiredSubgroupSize = subgroupSize;
if (subgroupSize != 0) info.stage.pNext = &subgroup;
info.layout = layout;
check(create(device, cache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines");
}
bool CaptureComputePipeline(std::size_t words) {
const char* minimum = std::getenv("APS5_DUMP_COMPUTE_MIN_WORDS");
if (minimum == nullptr) return true;
std::uint32_t threshold = 0;
const auto end = minimum + std::strlen(minimum);
const auto parsed = std::from_chars(minimum, end, threshold);
require(parsed.ec == std::errc{} && parsed.ptr == end && threshold <= ComputePipelineCapture::MaxSpirvWords, "APS5_DUMP_COMPUTE_MIN_WORDS must be an integer from 0 through 8388608");
return words >= threshold;
}
// The ShaderResources content cache dispatches share with recorded draws: Graphics::ResourceCache,
// one process-wide instance (see SharedResourceCache) that the State references so this file keeps
// its Find/Insert/Remove/Clear calls; the device clears it at teardown before its descriptor caches
@ -178,6 +226,7 @@ struct WorkgroupMemory {
};
struct VulkanDevice::State {
bool compileOnly = false;
void* library = nullptr;
PFN_vkGetInstanceProcAddr instanceProc = nullptr;
PFN_vkGetDeviceProcAddr deviceProc = nullptr;
@ -220,6 +269,9 @@ struct VulkanDevice::State {
// graphics stages, and compute workgroups wider than a wave).
bool descriptorIndexing = false;
bool storageBufferUpdateAfterBind = false;
bool descriptorBindingPartiallyBound = false;
bool descriptorBindingVariableDescriptorCount = false;
bool runtimeDescriptorArray = false;
VkPhysicalDeviceDescriptorIndexingPropertiesEXT descriptorIndexingProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_PROPERTIES_EXT};
bool imageInt64Atomics = false;
bool bufferInt64Atomics = false;
@ -567,6 +619,10 @@ struct VulkanDevice::State {
~State() {
contextReady = false;
if (compileOnly && device != VK_NULL_HANDLE) {
reinterpret_cast<PFN_vkDestroyDevice>(deviceProc(device, "vkDestroyDevice"))(device, nullptr);
device = VK_NULL_HANDLE;
}
if (device != VK_NULL_HANDLE) {
const auto idle = reinterpret_cast<PFN_vkDeviceWaitIdle>(deviceProc(device, "vkDeviceWaitIdle"))(device);
if (idle != VK_SUCCESS && idle != VK_ERROR_DEVICE_LOST) std::terminate();
@ -598,6 +654,7 @@ struct VulkanDevice::State {
// belong to this device and must be destroyed while it lives.
Graphics::ClearCachedPipelines(device);
Graphics::ClearDepthSurfaces(device);
Graphics::ClearMultisampleTargets(device);
{
std::lock_guard pipelines(computePipelinesMutex);
computePipelines.clear();
@ -656,7 +713,165 @@ struct VulkanDevice::State {
}
};
VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_unique<State>()), serial([] { static std::atomic<std::uint64_t> serials{0}; return serials.fetch_add(1, std::memory_order_relaxed) + 1; }()) {
ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePipelineCapture& capture, std::span<const std::byte> initialCache) {
require(state->compileOnly, "pipeline replay requires a compile-only device");
const auto start = std::chrono::steady_clock::now();
const auto elapsed = [](auto since) { return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - since).count(); };
capture.Validate();
require(initialCache.size() <= 64u * 1024u * 1024u, "replay pipeline cache exceeds 64 MiB");
if (!initialCache.empty()) {
require(initialCache.size() >= 32, "replay pipeline cache header is truncated");
const auto word = [&](std::size_t offset) {
std::uint32_t result = 0;
for (unsigned shift = 0; shift < 32; shift += 8) result |= std::to_integer<std::uint32_t>(initialCache[offset++]) << shift;
return result;
};
require(word(0) >= 32 && word(0) <= initialCache.size() && word(4) == VK_PIPELINE_CACHE_HEADER_VERSION_ONE, "unsupported replay pipeline cache header");
require(word(8) == state->properties.vendorID && word(12) == state->properties.deviceID, "replay pipeline cache belongs to another device");
for (std::size_t index = 0; index < VK_UUID_SIZE; ++index) require(std::to_integer<std::uint8_t>(initialCache[16 + index]) == state->properties.pipelineCacheUUID[index], "replay pipeline cache UUID differs from the driver");
}
require((capture.flags & VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR) == 0 || state->pipelineExecutableInfo, "captured pipeline statistics feature is not enabled");
if (capture.requiredSubgroupSize != 0) {
VkPhysicalDeviceSubgroupSizeControlPropertiesEXT subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_PROPERTIES_EXT};
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &subgroup};
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(state->physical, &properties);
require(state->computeWave32 && capture.requiredSubgroupSize >= subgroup.minSubgroupSize && capture.requiredSubgroupSize <= subgroup.maxSubgroupSize && (subgroup.requiredSubgroupSizeStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0, "captured required subgroup size is unsupported");
}
const auto target = Target();
require(capture.spirv[1] <= target.spirvVersion, "captured SPIR-V version exceeds enabled device support");
for (std::size_t cursor = 5; cursor < capture.spirv.size(); cursor += capture.spirv[cursor] >> 16u) {
const auto count = capture.spirv[cursor] >> 16u;
if ((capture.spirv[cursor] & 0xffffu) == spv::OpCapability) {
require(count == 2 && (CoreVulkanCapability(capture.spirv[cursor + 1]) || std::ranges::find(target.supportedCapabilities, capture.spirv[cursor + 1]) != target.supportedCapabilities.end()), "captured SPIR-V capability is not enabled");
}
}
#if ANYPS5_ENABLE_SPIRV_TOOLS
static_cast<void>(ShaderRecompiler::ValidateAndOptimizeSpirv(capture.spirv, target.vulkanVersion, target.spirvVersion, target.nonConstantImageOffsets, false));
#else
throw std::runtime_error("compute pipeline replay requires ANYPS5_ENABLE_SPIRV_TOOLS=ON");
#endif
ShaderRecompiler::RecompileResult shader;
shader.spirv = capture.spirv;
for (const auto& value : capture.specialization) shader.specialization.push_back({value.id, value.value});
const Graphics::PipelineSpecialization specialization(shader);
const auto& limits = state->properties.limits;
require(capture.sets.size() <= limits.maxBoundDescriptorSets, "captured descriptor sets exceed device limits");
for (const auto& range : capture.pushConstants) require(range.offset + range.size <= limits.maxPushConstantsSize, "captured push constants exceed device limits");
struct ReplayObjects {
VkDevice device;
PFN_vkDestroyDescriptorSetLayout destroySetLayout;
PFN_vkDestroyPipelineCache destroyCache;
std::vector<VkDescriptorSetLayout> sets;
VkPipelineCache cache = VK_NULL_HANDLE;
~ReplayObjects() {
for (auto layout : sets) destroySetLayout(device, layout, nullptr);
if (cache != VK_NULL_HANDLE) destroyCache(device, cache, nullptr);
}
} replay{state->device, state->DeviceFunction<PFN_vkDestroyDescriptorSetLayout>("vkDestroyDescriptorSetLayout"), state->DeviceFunction<PFN_vkDestroyPipelineCache>("vkDestroyPipelineCache"), {}};
ComputePipelineObjects objects;
objects.device = state->device;
objects.destroyModule = state->DeviceFunction<PFN_vkDestroyShaderModule>("vkDestroyShaderModule");
objects.destroyLayout = state->DeviceFunction<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout");
objects.destroyPipeline = state->DeviceFunction<PFN_vkDestroyPipeline>("vkDestroyPipeline");
ComputePipelineReplayResult result;
result.deviceName = state->properties.deviceName;
result.vendorId = state->properties.vendorID;
result.deviceId = state->properties.deviceID;
result.driverVersion = state->properties.driverVersion;
result.apiVersion = state->properties.apiVersion;
std::copy_n(state->properties.pipelineCacheUUID, VK_UUID_SIZE, result.pipelineCacheUuid.begin());
result.initialCacheBytes = initialCache.size();
auto phase = std::chrono::steady_clock::now();
VkPipelineCacheCreateInfo cacheInfo{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO};
cacheInfo.initialDataSize = initialCache.size();
cacheInfo.pInitialData = initialCache.data();
check(state->DeviceFunction<PFN_vkCreatePipelineCache>("vkCreatePipelineCache")(state->device, &cacheInfo, nullptr, &replay.cache), "vkCreatePipelineCache replay");
result.cacheMilliseconds = elapsed(phase);
phase = std::chrono::steady_clock::now();
VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
moduleInfo.codeSize = capture.spirv.size() * sizeof(std::uint32_t);
moduleInfo.pCode = capture.spirv.data();
check(state->DeviceFunction<PFN_vkCreateShaderModule>("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects.module), "vkCreateShaderModule replay");
result.moduleMilliseconds = elapsed(phase);
phase = std::chrono::steady_clock::now();
std::array<std::array<std::uint64_t, 4>, 2> descriptorCounts{};
bool hasUpdateAfterBindLayout = false;
for (const auto& layout : capture.sets) {
const bool updateAfterBind = (layout.flags & VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) != 0;
hasUpdateAfterBindLayout |= updateAfterBind;
require(!updateAfterBind || state->storageBufferUpdateAfterBind, "captured update-after-bind descriptors are unsupported");
std::vector<VkDescriptorSetLayoutBinding> bindings;
std::vector<VkDescriptorBindingFlags> flags;
std::uint32_t variableCount = 0;
for (const auto& binding : layout.bindings) {
require((binding.flags & VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT) == 0 || state->descriptorBindingPartiallyBound, "captured partially-bound descriptors are unsupported");
require((binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) == 0 || state->descriptorBindingVariableDescriptorCount, "captured variable descriptor count is unsupported");
if ((binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) != 0) variableCount = binding.count;
bindings.push_back({binding.binding, binding.type, binding.count, binding.stages, nullptr});
flags.push_back(binding.flags);
const std::size_t kind = binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? 0 : binding.type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE ? 1 : binding.type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE ? 2 : 3;
descriptorCounts[updateAfterBind ? 1 : 0][kind] += binding.count;
}
VkDescriptorSetLayoutBindingFlagsCreateInfo bindingFlags{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO};
bindingFlags.bindingCount = static_cast<std::uint32_t>(flags.size());
bindingFlags.pBindingFlags = flags.data();
VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
info.pNext = std::ranges::any_of(flags, [](auto value) { return value != 0; }) ? &bindingFlags : nullptr;
info.flags = layout.flags;
info.bindingCount = static_cast<std::uint32_t>(bindings.size());
info.pBindings = bindings.data();
VkDescriptorSetVariableDescriptorCountLayoutSupport variableSupport{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_LAYOUT_SUPPORT};
VkDescriptorSetLayoutSupport support{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_SUPPORT, variableCount != 0 ? &variableSupport : nullptr};
state->DeviceFunction<PFN_vkGetDescriptorSetLayoutSupport>("vkGetDescriptorSetLayoutSupport")(state->device, &info, &support);
require(support.supported == VK_TRUE, "captured descriptor set layout is unsupported");
require(variableCount <= variableSupport.maxVariableDescriptorCount, "captured variable descriptor count exceeds layout support");
VkDescriptorSetLayout handle = VK_NULL_HANDLE;
check(state->DeviceFunction<PFN_vkCreateDescriptorSetLayout>("vkCreateDescriptorSetLayout")(state->device, &info, nullptr, &handle), "vkCreateDescriptorSetLayout replay");
replay.sets.push_back(handle);
}
const std::array<std::uint32_t, 4> regularLimits{std::min(limits.maxDescriptorSetStorageBuffers, limits.maxPerStageDescriptorStorageBuffers), std::min(limits.maxDescriptorSetSampledImages, limits.maxPerStageDescriptorSampledImages), std::min(limits.maxDescriptorSetStorageImages, limits.maxPerStageDescriptorStorageImages), std::min(limits.maxDescriptorSetSamplers, limits.maxPerStageDescriptorSamplers)};
const auto& indexed = state->descriptorIndexingProperties;
const std::array<std::uint32_t, 4> indexedLimits{std::min(indexed.maxDescriptorSetUpdateAfterBindStorageBuffers, indexed.maxPerStageDescriptorUpdateAfterBindStorageBuffers), std::min(indexed.maxDescriptorSetUpdateAfterBindSampledImages, indexed.maxPerStageDescriptorUpdateAfterBindSampledImages), std::min(indexed.maxDescriptorSetUpdateAfterBindStorageImages, indexed.maxPerStageDescriptorUpdateAfterBindStorageImages), std::min(indexed.maxDescriptorSetUpdateAfterBindSamplers, indexed.maxPerStageDescriptorUpdateAfterBindSamplers)};
std::uint64_t regularResources = 0;
std::uint64_t indexedResources = 0;
for (std::size_t kind = 0; kind < regularLimits.size(); ++kind) {
require(descriptorCounts[0][kind] <= regularLimits[kind], "captured pipeline descriptors exceed device limits");
require(!hasUpdateAfterBindLayout || descriptorCounts[0][kind] + descriptorCounts[1][kind] <= indexedLimits[kind], "captured update-after-bind pipeline descriptors exceed device limits");
if (kind != 3) {
regularResources += descriptorCounts[0][kind];
indexedResources += descriptorCounts[0][kind] + descriptorCounts[1][kind];
}
}
require(regularResources <= limits.maxPerStageResources && (!hasUpdateAfterBindLayout || indexedResources <= indexed.maxPerStageUpdateAfterBindResources), "captured pipeline resources exceed device limits");
VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
layoutInfo.setLayoutCount = static_cast<std::uint32_t>(replay.sets.size());
layoutInfo.pSetLayouts = replay.sets.data();
layoutInfo.pushConstantRangeCount = static_cast<std::uint32_t>(capture.pushConstants.size());
layoutInfo.pPushConstantRanges = capture.pushConstants.data();
check(state->DeviceFunction<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(state->device, &layoutInfo, nullptr, &objects.layout), "vkCreatePipelineLayout replay");
result.layoutMilliseconds = elapsed(phase);
phase = std::chrono::steady_clock::now();
CreateComputePipeline(state->device, state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines"), replay.cache, objects.module, objects.layout, capture.flags, capture.stageFlags, specialization.Info(), capture.requiredSubgroupSize, &objects.pipeline);
result.pipelineMilliseconds = elapsed(phase);
phase = std::chrono::steady_clock::now();
const auto getCache = state->DeviceFunction<PFN_vkGetPipelineCacheData>("vkGetPipelineCacheData");
std::size_t cacheBytes = 0;
check(getCache(state->device, replay.cache, &cacheBytes, nullptr), "vkGetPipelineCacheData size replay");
require(cacheBytes <= 64u * 1024u * 1024u, "exported replay pipeline cache exceeds 64 MiB");
result.cache.resize(cacheBytes);
check(getCache(state->device, replay.cache, &cacheBytes, result.cache.data()), "vkGetPipelineCacheData replay");
result.cache.resize(cacheBytes);
result.cacheMilliseconds += elapsed(phase);
result.totalMilliseconds = elapsed(start);
return result;
}
VulkanDevice::VulkanDevice(const PresentationWindow* window) : VulkanDevice(window, false) {}
VulkanDevice::VulkanDevice(CompileOnly) : VulkanDevice(nullptr, true) {}
VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state(std::make_unique<State>()), serial([] { static std::atomic<std::uint64_t> serials{0}; return serials.fetch_add(1, std::memory_order_relaxed) + 1; }()) {
state->compileOnly = compileOnly;
APS5_LOG_OUT("VulkanDevice constructor window=%p", static_cast<const void*>(window));
#ifdef _WIN32
state->library = SDL_LoadObject("vulkan-1.dll");
@ -966,10 +1181,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &hostProperties};
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &properties);
state->hostImportAlignment = hostProperties.minImportedHostPointerAlignment;
Graphics::SetHostImportWindow(8ull << 20u);
if (!compileOnly) Graphics::SetHostImportWindow(8ull << 20u);
#ifdef _WIN32
// Imported pages are locked; locking is bounded by the process working-set minimum.
if (const char* value = std::getenv("APS5_WORKING_SET_MIB")) {
if (const char* value = std::getenv("APS5_WORKING_SET_MIB"); value != nullptr && !compileOnly) {
const SIZE_T bytes = static_cast<SIZE_T>(std::strtoull(value, nullptr, 10)) << 20u;
if (!SetProcessWorkingSetSizeEx(GetCurrentProcess(), bytes, bytes * 2, QUOTA_LIMITS_HARDWS_MIN_DISABLE | QUOTA_LIMITS_HARDWS_MAX_DISABLE)) std::fprintf(stderr, "[gpu] SetProcessWorkingSetSizeEx failed: %lu\n", GetLastError());
}
@ -1147,13 +1362,16 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
state->descriptorIndexing = descriptorIndexingFeatures.shaderSampledImageArrayNonUniformIndexing == VK_TRUE && descriptorIndexingFeatures.shaderStorageImageArrayNonUniformIndexing == VK_TRUE;
state->storageBufferUpdateAfterBind = descriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind == VK_TRUE;
state->descriptorBindingPartiallyBound = compileOnly && descriptorIndexingFeatures.descriptorBindingPartiallyBound == VK_TRUE;
state->descriptorBindingVariableDescriptorCount = compileOnly && descriptorIndexingFeatures.descriptorBindingVariableDescriptorCount == VK_TRUE;
state->runtimeDescriptorArray = compileOnly && descriptorIndexingFeatures.runtimeDescriptorArray == VK_TRUE;
if (state->storageBufferUpdateAfterBind) {
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &state->descriptorIndexingProperties};
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &properties);
}
}
descriptorIndexingFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT};
if (state->descriptorIndexing || state->storageBufferUpdateAfterBind) deviceExtensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME);
if (state->descriptorIndexing || state->storageBufferUpdateAfterBind || state->descriptorBindingPartiallyBound || state->descriptorBindingVariableDescriptorCount || state->runtimeDescriptorArray) deviceExtensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME);
if (state->descriptorIndexing) {
descriptorIndexingFeatures.shaderSampledImageArrayNonUniformIndexing = VK_TRUE;
descriptorIndexingFeatures.shaderStorageImageArrayNonUniformIndexing = VK_TRUE;
@ -1163,6 +1381,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->spirvExtensions.push_back("SPV_EXT_descriptor_indexing");
}
descriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind = state->storageBufferUpdateAfterBind ? VK_TRUE : VK_FALSE;
descriptorIndexingFeatures.descriptorBindingPartiallyBound = state->descriptorBindingPartiallyBound ? VK_TRUE : VK_FALSE;
descriptorIndexingFeatures.descriptorBindingVariableDescriptorCount = state->descriptorBindingVariableDescriptorCount ? VK_TRUE : VK_FALSE;
descriptorIndexingFeatures.runtimeDescriptorArray = state->runtimeDescriptorArray ? VK_TRUE : VK_FALSE;
if (state->runtimeDescriptorArray) {
state->capabilities.push_back(spv::CapabilityRuntimeDescriptorArray);
if (!state->descriptorIndexing) state->spirvExtensions.push_back("SPV_EXT_descriptor_indexing");
}
state->samplerAnisotropy = true;
state->textureCompressionBC = true;
deviceInfo.pEnabledFeatures = &enabled;
@ -1230,7 +1455,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
imageRobustnessFeatures.pNext = byteFeatures.pNext;
byteFeatures.pNext = &imageRobustnessFeatures;
}
if (state->descriptorIndexing || state->storageBufferUpdateAfterBind) {
if (state->descriptorIndexing || state->storageBufferUpdateAfterBind || state->descriptorBindingPartiallyBound || state->descriptorBindingVariableDescriptorCount || state->runtimeDescriptorArray) {
descriptorIndexingFeatures.pNext = byteFeatures.pNext;
byteFeatures.pNext = &descriptorIndexingFeatures;
}
@ -1310,6 +1535,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
auto shaderProfile = std::make_unique<const ShaderDeviceProfile>(buildTarget(), deviceInfo, state->properties.limits);
check(state->InstanceFunction<PFN_vkCreateDevice>("vkCreateDevice")(selected, &deviceInfo, nullptr, &state->device), "vkCreateDevice");
state->shaderProfile = std::move(shaderProfile);
if (compileOnly) return;
state->DeviceFunction<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(state->device, family, 0, &state->queue);
APS5_LOG_OUT("Vulkan device ready device=%p queue=%p family=%u", reinterpret_cast<void*>(state->device), reinterpret_cast<void*>(state->queue), family);
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
@ -1443,6 +1669,14 @@ bool OpportunisticReap() {
}
void VulkanDevice::StoreIdleResults(bool reapFirst) {
if (!Graphics::StorageTexture::AnyPending()) return;
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush);
std::lock_guard gpuLock(GuestMemory::GpuMutex());
Graphics::StorageTexture::FlushAllPending("idle");
SubmitRecorded(reapFirst);
}
void VulkanDevice::SubmitRecorded(bool reapFirst) {
if (!state->recorder) return;
// Finished batches are retired first (a non-blocking fence status check on the front of the
@ -3833,20 +4067,39 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
layoutInfo.pushConstantRangeCount = pushStages != 0 ? 1 : 0;
layoutInfo.pPushConstantRanges = pushStages != 0 ? &push : nullptr;
check(state->DeviceFunction<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(state->device, &layoutInfo, nullptr, &objects->layout), "vkCreatePipelineLayout");
VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
pipelineInfo.flags = (context.pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0) | VK_PIPELINE_CREATE_DISPATCH_BASE_BIT;
pipelineInfo.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
pipelineInfo.stage.module = objects->module;
pipelineInfo.stage.pName = "main";
const VkPipelineCreateFlags pipelineFlags = (context.pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0) | VK_PIPELINE_CREATE_DISPATCH_BASE_BIT;
const Graphics::PipelineSpecialization specialization(shader);
pipelineInfo.stage.pSpecializationInfo = specialization.Info();
VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT requiredSubgroup{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT};
requiredSubgroup.requiredSubgroupSize = std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize);
if (state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize)) pipelineInfo.stage.pNext = &requiredSubgroup;
pipelineInfo.layout = objects->layout;
const auto subgroupSize = state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize) ? std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize) : 0u;
if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && CaptureComputePipeline(shader.spirv.size())) {
ComputePipelineCapture capture;
capture.spirv = shader.spirv;
ComputeDescriptorSetLayout resourcesLayout;
resourcesLayout.flags = resources->LayoutUpdateAfterBind() ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0;
const auto layoutBindings = resources->LayoutBindings();
const auto bindingFlags = resources->LayoutBindingFlags();
require(layoutBindings.size() == bindingFlags.size(), "compute capture binding flag count differs from layout");
for (std::size_t index = 0; index < layoutBindings.size(); ++index) {
const auto& binding = layoutBindings[index];
require(binding.pImmutableSamplers == nullptr, "compute capture does not support immutable samplers");
resourcesLayout.bindings.push_back({binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags, bindingFlags[index]});
}
capture.sets.push_back(std::move(resourcesLayout));
if (shader.workgroupMemoryDwords != 0) capture.sets.push_back({0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, 0}}});
if (pushStages != 0) capture.pushConstants.push_back(push);
for (const auto& value : shader.specialization) capture.specialization.push_back({value.id, value.value});
capture.requiredSubgroupSize = subgroupSize;
capture.flags = pipelineFlags;
capture.deviceName = state->properties.deviceName;
capture.vendorId = state->properties.vendorID;
capture.deviceId = state->properties.deviceID;
capture.driverVersion = state->properties.driverVersion;
capture.apiVersion = state->properties.apiVersion;
std::copy_n(state->properties.pipelineCacheUUID, VK_UUID_SIZE, capture.pipelineCacheUuid.begin());
const auto path = capture.Write(directory);
std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str());
}
if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast<unsigned long long>(programAddress));
check(state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(state->device, context.pipelineCache, 1, &pipelineInfo, nullptr, &objects->pipeline), "vkCreateComputePipelines");
CreateComputePipeline(state->device, state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline);
objects->destroyModule(objects->device, objects->module, nullptr);
objects->module = VK_NULL_HANDLE;
Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline);

View File

@ -61,12 +61,13 @@ public:
// The size a buffer for `bytes` is created with: its size class, or `bytes` itself when large.
static std::size_t Capacity(std::size_t bytes, VkMemoryPropertyFlags properties);
static VkBufferUsageFlags Usage(VkBufferUsageFlags usage, VkMemoryPropertyFlags properties);
VkDeviceMemory AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation);
std::optional<BufferAllocation> Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
void Put(const BufferAllocation& allocation) noexcept;
static bool SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom);
static VkDeviceSize SlabBlockBytes(std::size_t capacity);
SlabSlot TakeSlot(const Context& context, std::uint32_t memoryType, std::size_t capacity, bool addressable);
void PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noexcept;
VkDeviceSize PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noexcept;
std::size_t SlabBlocks();
private:
@ -117,7 +118,8 @@ private:
static bool DeviceTierEnabled();
static bool DeviceTiered(VkMemoryPropertyFlags properties);
static std::size_t NextCapacity(std::size_t capacity);
void destroy(const BufferAllocation& allocation) noexcept;
VkDeviceSize destroy(const BufferAllocation& allocation) noexcept;
VkDeviceSize trim();
// Moves the tier's least recently used slot (the oldest front of its lists) to `evicted`; the
// caller destroys those after releasing the mutex, so builds taking buffers on other threads
// do not wait behind the Vulkan destroy calls. Nothing changes when the vector cannot grow.

View File

@ -0,0 +1,41 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETARGET_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETARGET_HPP
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include <cstddef>
#include <cstdint>
#include <string>
namespace AgcDriver::Graphics {
class StorageTexture;
class MultisampledCmask {
public:
MultisampledCmask() = default;
MultisampledCmask(std::uint64_t cmaskAddress, std::uint64_t cmaskBytes) : address(cmaskAddress), bytes(cmaskBytes) {}
void NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern);
void SeedKeys(DccKeys seeded);
void CheckAddress(std::uint64_t cmaskAddress) const;
bool TakeClear();
DccKeys Keys() const { return keys; }
const std::string& Refusal() const { return refusal; }
private:
std::uint64_t address = 0;
std::uint64_t bytes = 0;
DccKeys keys = DccKeys::Uncompressed;
std::string refusal;
};
VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target);
void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView);
void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern);
void ClearMultisampleTargets(VkDevice device);
void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access);
}
#endif

View File

@ -45,7 +45,7 @@ public:
Pipeline(const Pipeline&) = delete;
Pipeline& operator=(const Pipeline&) = delete;
VkPipelineLayout Layout() const;
std::shared_ptr<Framebuffer> AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent);
std::shared_ptr<Framebuffer> AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent, std::span<const std::uint8_t> persistent = {});
// Begins the render pass on the framebuffer, binds the pipeline and sets its dynamic state.
void Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, VkExtent2D extent, const State& state) const;
// The same inside a render pass another pipeline of the same attachments began (compatible by
@ -64,6 +64,7 @@ private:
struct CachedFramebuffer {
std::vector<VkImageView> views;
std::vector<std::weak_ptr<StorageTexture>> owners;
std::vector<bool> owned;
VkExtent2D extent;
std::shared_ptr<Framebuffer> framebuffer;
};
@ -107,6 +108,7 @@ void ValidateDepthBounds(const Context& context, const State& state);
void ValidateProvokingVertex(const Context& context, const State& state, std::span<const CompiledShader> shaders = {});
void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment);
bool ShadersWriteMemory(std::span<const CompiledShader> shaders);
// Returns the color attachment locations the pixel shader writes.
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false);

View File

@ -11,6 +11,7 @@
namespace AgcDriver::Graphics {
ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array<std::uint8_t, 8>& exportMappings, bool nullProgram = false);
ShaderRecompiler::ShaderPixelStageInfo DefaultUnexportedInputs(ShaderRecompiler::ShaderPixelStageInfo pixel, std::span<const std::uint32_t> parameterExports);
ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& shader, std::span<const std::byte> header);
// A guest range DecodeVertexStageInfo read (an attribute word, a vertex V#) with the bytes as read:
// the draw cache validates them by value with the stage's capture (design_cpu_final rule RD).
@ -18,7 +19,7 @@ struct DecodeRead {
std::uint64_t address;
std::vector<std::byte> bytes;
};
ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const std::byte> header, std::uint64_t headerAddress, std::span<const std::uint32_t> userData, std::vector<DecodeRead>* reads = nullptr, bool staticAbi = false);
ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const std::byte> header, std::uint64_t headerAddress, std::span<const std::uint32_t> userData, std::vector<DecodeRead>* reads = nullptr, bool staticAbi = false, std::uint32_t hiddenUserWords = 0);
}

View File

@ -60,7 +60,7 @@ public:
// The layout for `key` (binding, type, count, stage flags per binding, as ShaderResources builds
// it from `bindings`), created on first use.
VkDescriptorSetLayout Layout(std::span<const std::uint32_t> key, std::span<const VkDescriptorSetLayoutBinding> bindings, bool updateAfterBind = false);
VkDescriptorSetLayout Layout(std::span<const std::uint32_t> key, std::span<const VkDescriptorSetLayoutBinding> bindings, bool updateAfterBind = false, std::span<const VkDescriptorBindingFlags> flags = {});
struct SetAllocation {
VkDescriptorSet set = VK_NULL_HANDLE;
VkDescriptorPool pool = VK_NULL_HANDLE;
@ -114,6 +114,9 @@ public:
ShaderResources(const ShaderResources&) = delete;
ShaderResources& operator=(const ShaderResources&) = delete;
VkDescriptorSetLayout Layout() const;
std::vector<VkDescriptorSetLayoutBinding> LayoutBindings() const;
bool LayoutUpdateAfterBind() const;
std::span<const VkDescriptorBindingFlags> LayoutBindingFlags() const { return layoutBindingFlags; }
void Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const;
struct DrawBindings {
DrawBindings() = default;
@ -136,6 +139,7 @@ public:
};
std::shared_ptr<DrawBindings> PrepareDrawBindings(Recorder& recorder, std::span<const MovedBuffer> moved = {}) const;
std::optional<std::vector<MovedBuffer>> MovedReadOnlyBuffers(std::span<const CompiledShader> shaders, Recorder& recorder) const;
static std::size_t MaxDrawSnapshotBytes();
void WriteBack();
// Deferred completion: MarkGpuWrites registers the results the recorded work leaves on the GPU
// (storage images stay there; buffer ranges are noted so CPU reads wait); WriteBackBuffers runs
@ -159,6 +163,7 @@ public:
std::vector<std::pair<VkImage, bool>> StorageImages() const;
std::vector<std::pair<std::uint64_t, std::uint64_t>> DeviceReads() const { return guestMemory.DeviceReads(); }
bool ReadsImage(const StorageTexture* image) const;
bool HasCachedStorageImages() const;
const std::vector<std::uint32_t>& LayoutKey() const { return layoutKey; }
// Debug aid: each bound guest resource with the fraction of sampled bytes that are nonzero.
std::string Describe() const;
@ -473,6 +478,7 @@ private:
// still to look up (index into `bindings`; the DescriptorBinding lives in the compiled shader),
// the descriptor counts the set was sized for, and the compute stage of a deferred build.
std::vector<Binding> bindings;
std::vector<VkDescriptorBindingFlags> layoutBindingFlags;
std::vector<std::uint32_t> refreshResourceKey;
struct DeferredImages {
const ShaderRecompiler::DescriptorBinding* binding;

View File

@ -45,6 +45,7 @@ struct ColorTarget {
bool dccPipeAligned = false;
std::uint64_t cmaskAddress = 0;
std::size_t cmaskBytes = 0;
bool cmaskFastClear = false;
std::uint64_t surfaceAddress = 0;
VkExtent2D surfaceExtent{};
std::uint32_t mipCount = 1;
@ -54,9 +55,13 @@ struct ColorTarget {
std::uint32_t slot = 0;
std::uint32_t depth = 1;
std::uint32_t depthSlice = 0;
std::uint64_t arrayAddress = 0;
std::uint32_t arrayLastSlice = 0;
std::uint32_t arraySlice = 0;
std::uint32_t exportIndex = 0;
bool uintExport = false;
std::uint32_t pipeBankXor = 0;
std::uint32_t samples = 1;
};
struct DepthTarget {
@ -68,6 +73,7 @@ struct DepthTarget {
std::uint8_t clearStencil;
std::uint64_t htileAddress = 0;
bool htileStencil = false;
std::uint32_t samples = 1;
};
struct State {
@ -109,6 +115,7 @@ struct State {
VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
VkPipelineColorBlendAttachmentState blend;
std::array<float, 4> blendConstants;
std::uint32_t samples = 1;
};
ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false);
@ -120,9 +127,10 @@ std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height);
std::uint32_t ColorWriteMask(const Registers& context);
struct ColorMetadataPass {
enum class Mode { EliminateFastClear, DccDecompress };
enum class Mode { EliminateFastClear, DccDecompress, Resolve };
Mode mode;
std::vector<ColorTarget> targets;
std::optional<ColorTarget> source;
};
std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue);
std::string DepthMaintenanceRejection(const QueueState& queue);
@ -163,7 +171,7 @@ struct DrawKeyRange {
std::uint32_t first;
std::uint32_t count;
};
inline constexpr std::array<DrawKeyRange, 48> DrawKeyRegisters{{
inline constexpr std::array<DrawKeyRange, 49> DrawKeyRegisters{{
{RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 1}, {RegisterBank::Context, 0x005, 1}, {RegisterBank::Context, 0x007, 7}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 5},
{RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10b, 3}, {RegisterBank::Context, 0x10f, 6},
// SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL, SPI_SHADER_POS/Z/COL_FORMAT,
@ -171,7 +179,7 @@ inline constexpr std::array<DrawKeyRange, 48> DrawKeyRegisters{{
{RegisterBank::Context, 0x191, 32}, {RegisterBank::Context, 0x1b3, 2}, {RegisterBank::Context, 0x1b6, 1}, {RegisterBank::Context, 0x1c3, 3}, {RegisterBank::Context, 0x1e0, 8}, {RegisterBank::Context, 0x1ff, 1},
// DB_DEPTH_CONTROL .. PA_CL_VS_OUT_CNTL, PA_SC_MODE_CNTL_0/1, VGT_GS_MODE, VGT_GS_VERT_ITEMSIZE,
// PA_SU_VTX_CNTL, the sample masks, PA_SC_CONSERVATIVE_RASTERIZATION_CNTL.
{RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x286, 2}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2de, 6}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1},
{RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x286, 2}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2de, 6}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x2fe, 16}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1},
// CB_COLOR0..7_BASE .. DCC_BASE (15 words a slot), CB_COLOR0..7_BASE_EXT, DCC_BASE_EXT, ATTRIB2, ATTRIB3.
{RegisterBank::Context, 0x318, 0x78}, {RegisterBank::Context, 0x390, 8}, {RegisterBank::Context, 0x398, 8}, {RegisterBank::Context, 0x3a8, 0x18},
// The pixel program address, RSRC2 and user words; the geometry-back user pointer and program

View File

@ -38,6 +38,7 @@ bool StorageFormatAvailable(const Context& context, std::uint32_t guestFormat);
bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, DccKeys keys);
std::uint64_t SampledTextureMemory();
bool ClearKeepsDenormals(const Context& context, VkFormat format);
bool ClearColorForTexel(VkFormat format, std::uint32_t elementBytes, std::span<const std::uint32_t, 4> pattern, VkClearColorValue& clear);
// A sampled texture's own VkImage with its memory, shared with the recorder while a recorded upload
// still writes it (see the snapshot constructor), so the texture may go before the batch completes.
@ -142,7 +143,7 @@ VkFormat AttachmentProxyFormat(const Context& context, VkFormat format);
// or it leaves the cache. Dispatches reusing the image meanwhile skip the round trip entirely.
class StorageTexture : public std::enable_shared_from_this<StorageTexture> {
public:
StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel);
static std::shared_ptr<StorageTexture> Create(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel);
~StorageTexture();
StorageTexture(const StorageTexture&) = delete;
StorageTexture& operator=(const StorageTexture&) = delete;
@ -159,7 +160,7 @@ public:
// Render targets live in the same images: draws attach mip 0 through a view of the color
// buffer's format and mark the image dirty like a storage write.
bool Attachable() const { return attachable; }
VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0, std::uint32_t depthSlice = 0);
VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0, std::uint32_t depthSlice = 0, std::uint32_t layer = 0);
VkImageView AttachmentProxyView();
void RecordAttachmentProxyLoad(VkCommandBuffer commands, VkImageLayout attachmentLayout) const;
void RecordAttachmentProxyStore(VkCommandBuffer commands, VkImageLayout attachmentLayout) const;
@ -176,6 +177,7 @@ public:
// overlapping pending image, as before).
static bool FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except = nullptr, const char* reason = "memory access", PublishScope scope = PublishScope::Whole, bool* published = nullptr);
static void FlushAllPending(const char* reason);
static bool AnyPending();
static bool StoreAtFlipRequested(const char* value);
// See PendingSerial: a change of a surface's source outside the registry (a unit shadow
// retile) moves it too.
@ -326,6 +328,7 @@ public:
VkDeviceSize AllocationBytes() const { return memoryBytes; }
private:
StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel);
// The regions of every array layer, or of the tracked layers `layers` selects.
std::vector<VkBufferImageCopy> CopyRegions(const std::vector<bool>* layers = nullptr) const;
// Uploads the surface, or only the tracked layers `layers` selects (the direct path; the others
@ -529,7 +532,7 @@ private:
std::map<std::uint32_t, VkImageView> elementLayerViews;
std::int8_t singlePassState = -1;
bool attachable = false;
std::map<std::tuple<VkFormat, std::uint32_t, std::uint32_t>, VkImageView> attachmentViews;
std::map<std::tuple<VkFormat, std::uint32_t, std::uint32_t, std::uint32_t>, VkImageView> attachmentViews;
VkImage proxyImage = VK_NULL_HANDLE;
VkDeviceMemory proxyMemory = VK_NULL_HANDLE;
VkImageView proxyView = VK_NULL_HANDLE;

View File

@ -41,6 +41,30 @@ BufferPool::~BufferPool() {
for (auto& [memory, block] : slabBlocks) freeBlock(*block);
}
VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation) {
VkDeviceMemory memory = VK_NULL_HANDLE;
auto result = allocateMemory(device, &allocation, nullptr, &memory);
const auto initialResult = result;
unsigned attempts = 1;
VkDeviceSize reclaimed = 0;
if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY || result == VK_ERROR_OUT_OF_HOST_MEMORY) {
reclaimed = trim();
if (reclaimed != 0) {
memory = VK_NULL_HANDLE;
result = allocateMemory(device, &allocation, nullptr, &memory);
++attempts;
}
}
if (result == VK_SUCCESS) {
if (attempts == 2) std::fprintf(stderr, "[bufferpool] %s recovered: allocation=%llu reclaimed=%llu initialResult=%d attempts=%u\n", operation, static_cast<unsigned long long>(allocation.allocationSize), static_cast<unsigned long long>(reclaimed), static_cast<int>(initialResult), attempts);
return memory;
}
char details[512];
std::snprintf(details, sizeof(details), "%s: allocation=%llu memoryType=%u reclaimed=%llu initialResult=%d attempts=%u", operation, static_cast<unsigned long long>(allocation.allocationSize), allocation.memoryTypeIndex, static_cast<unsigned long long>(reclaimed), static_cast<int>(initialResult), attempts);
Check(result, details);
return VK_NULL_HANDLE;
}
bool BufferPool::SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom) {
if (capacity >= classLimit || size > capacity || !std::has_single_bit(capacity)) return false;
const auto alignmentOk = alignment == 0 || (std::has_single_bit(alignment) && alignment <= capacity);
@ -80,7 +104,7 @@ SlabSlot BufferPool::TakeSlot(const Context& context, std::uint32_t memoryType,
if (addressable) allocation.pNext = &flags;
allocation.allocationSize = blockBytes;
allocation.memoryTypeIndex = memoryType;
Check(allocateMemory(device, &allocation, nullptr, &block->memory), "vkAllocateMemory buffer slab");
block->memory = AllocateMemory(allocation, "vkAllocateMemory buffer slab");
if ((context.memory.memoryTypes[memoryType].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0) {
void* mapping = nullptr;
const auto mapped = mapMemory(device, block->memory, 0, VK_WHOLE_SIZE, 0, &mapping);
@ -105,26 +129,27 @@ void BufferPool::freeBlock(SlabBlock& block) noexcept {
freeMemory(device, block.memory, nullptr);
}
void BufferPool::PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noexcept {
VkDeviceSize BufferPool::PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noexcept {
std::unique_ptr<SlabBlock> released;
{
std::lock_guard lock(slabMutex);
const auto found = slabBlocks.find(memory);
if (found == slabBlocks.end()) return;
if (found == slabBlocks.end()) return 0;
auto& block = *found->second;
auto& slab = slabs[block.slab];
if (block.free.empty()) slab.available.push_back(&block);
block.free.push_back(static_cast<std::uint32_t>(offset / block.slotBytes));
if (--block.used != 0) return;
if (--block.used != 0) return 0;
if (slab.emptyBlocks == 0) {
++slab.emptyBlocks;
return;
return 0;
}
slab.available.erase(std::find(slab.available.begin(), slab.available.end(), &block));
released = std::move(found->second);
slabBlocks.erase(found);
}
freeBlock(*released);
return SlabBlockBytes(released->slotBytes);
}
std::size_t BufferPool::SlabBlocks() {
@ -150,16 +175,16 @@ VkDeviceSize BufferPool::DeviceBudget(const VkPhysicalDeviceMemoryProperties& me
return std::max<VkDeviceSize>(VkDeviceSize{512} << 20u, largest / 8u);
}
void BufferPool::destroy(const BufferAllocation& allocation) noexcept {
VkDeviceSize BufferPool::destroy(const BufferAllocation& allocation) noexcept {
if (allocation.slab) {
destroyBuffer(device, allocation.buffer, nullptr);
PutSlot(allocation.memory, allocation.offset);
return;
return PutSlot(allocation.memory, allocation.offset);
}
// Device-local allocations (see DeviceBuffer) are never mapped.
if (allocation.mapping != nullptr) unmap(device, allocation.memory);
destroyBuffer(device, allocation.buffer, nullptr);
freeMemory(device, allocation.memory, nullptr);
return allocation.allocationBytes;
}
bool BufferPool::DeviceTiered(VkMemoryPropertyFlags properties) {
@ -240,6 +265,43 @@ std::size_t BufferPool::MaxSlots() {
return slots;
}
VkDeviceSize BufferPool::trim() {
Tier retiredSmall;
Tier retiredLarge;
Tier retiredDevice;
{
std::lock_guard lock(mutex);
for (const auto& [tier, retired] : {std::pair{&smallTier, &retiredSmall}, std::pair{&largeTier, &retiredLarge}, std::pair{&deviceTier, &retiredDevice}}) {
tier->free.swap(retired->free);
tier->evictions += tier->slots;
tier->retainedBytes = 0;
tier->slots = 0;
}
}
VkDeviceSize bytes = 0;
for (const auto* tier : {&retiredSmall, &retiredLarge, &retiredDevice}) {
for (const auto& [key, slots] : tier->free) {
for (const auto& slot : slots) bytes += destroy(slot.allocation);
}
}
for (;;) {
std::unique_ptr<SlabBlock> released;
{
std::lock_guard lock(slabMutex);
const auto found = std::find_if(slabBlocks.begin(), slabBlocks.end(), [](const auto& entry) { return entry.second->used == 0; });
if (found == slabBlocks.end()) break;
auto& slab = slabs.at(found->second->slab);
slab.available.erase(std::find(slab.available.begin(), slab.available.end(), found->second.get()));
--slab.emptyBlocks;
released = std::move(found->second);
slabBlocks.erase(found);
}
freeBlock(*released);
bytes += SlabBlockBytes(released->slotBytes);
}
return bytes;
}
void BufferPool::Put(const BufferAllocation& allocation) noexcept {
// Evicted allocations are destroyed after the mutex is released (see evictOldest). The vector
// may throw on growth; a Put that cannot retain simply destroys, as noexcept requires.

View File

@ -39,9 +39,9 @@ public:
info.extent = {target.extent.width, target.extent.height, 1};
info.mipLevels = 1;
info.arrayLayers = 1;
info.samples = VK_SAMPLE_COUNT_1_BIT;
info.samples = static_cast<VkSampleCountFlagBits>(target.samples);
info.tiling = VK_IMAGE_TILING_OPTIMAL;
info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | (target.samples == 1 ? VK_IMAGE_USAGE_SAMPLED_BIT : 0u);
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage depth");
@ -113,6 +113,7 @@ public:
key[10] = components.b;
key[11] = components.a;
if (const auto found = textures.find(key); found != textures.end()) return found->second;
Require(target.samples == 1, "sampling a multisampled depth surface is not implemented");
const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress;
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT;
@ -158,7 +159,7 @@ private:
};
bool sameSurface(const DepthTarget& a, const DepthTarget& b) {
return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format;
return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format && a.samples == b.samples;
}
std::mutex& surfacesMutex() {

View File

@ -1,4 +1,5 @@
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp"
#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp"
@ -87,6 +88,17 @@ GuestTextureResource SurfaceForTarget(const ColorTarget& color) {
return surface;
}
GuestTextureResource ArraySurfaceForTarget(const ColorTarget& color) {
Require(color.arrayLastSlice != 0, "the color target is not a view of a 2D array");
auto surface = SurfaceForTarget(color);
surface.baseAddress = color.arrayAddress;
surface.width = color.surfaceExtent.width;
surface.height = color.surfaceExtent.height;
surface.depthOrLastArray = color.arrayLastSlice;
surface.dimension = TextureDimension::k2DArray;
return surface;
}
}
namespace {
@ -1025,7 +1037,6 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
if (draw.indexed) GuestMemory::CheckRange(reinterpret_cast<const void*>(draw.indexAddress), static_cast<std::size_t>(indexBytes), draw.indexSize);
APS5_LOG_CHARS_OUT_DEBUG("Index buffer range OK");
Require(!draw.indexed || !state.hasColorTarget || draw.indexAddress + indexBytes <= state.color.address || state.color.address + state.color.bytes <= draw.indexAddress, "index buffer aliases the render target");
if (state.rectList) Require(draw.indexCount % 3 == 0, "incomplete rect-list primitive");
APS5_LOG_CHARS_OUT_DEBUG("ValidateShaders");
if (recipe != nullptr) {
inputs.fragmentOutputs = recipe->fragmentOutputs;
@ -1690,7 +1701,9 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
if (args->countIndirect) reads.emplace_back(args->countAddress, args->countAddress + 4);
}
auto images = resources.StorageImages();
for (const auto& target : record.targets) images.emplace_back(target->Image(), true);
for (const auto& target : record.targets) {
if (target != nullptr) images.emplace_back(target->Image(), true);
}
recorder->NoteAccess(CommandClass::Draw, Recorder::Access{reads, resources.GpuWrites(), images, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, resources.HoldsLease()});
}
std::unique_ptr<DeviceBuffer> scratch;
@ -1855,6 +1868,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
std::vector<std::byte> original;
TileMipLayout mip{};
std::unique_ptr<RenderTarget> target;
bool multisampled = false;
// APS5_DUMP_TARGETS: the rendered linear pixels, read back for target_<address>_<n>.raw.
std::unique_ptr<Buffer> dump;
};
@ -1868,6 +1882,13 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
binding.color = state.colors[index];
const auto& color = binding.color;
binding.gpuTiling = color.tileMode == ColorTileMode::RenderTarget && context.detiler != nullptr;
if (color.samples > 1) {
binding.multisampled = true;
timer.phase(PhaseSetup);
targetViews.push_back(MultisampleTargetView(context, color));
timer.phase(PhaseReadTarget);
continue;
}
APS5_LOG_OUT_DEBUG("Creating color target %zu address=0x%llx bytes=%llu extent=%ux%u", index, static_cast<unsigned long long>(color.address), static_cast<unsigned long long>(color.bytes), color.extent.width, color.extent.height);
const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes);
constexpr VkBufferUsageFlags copies = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
@ -1882,6 +1903,14 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
// APS5_PACKET_EPOCH=1); the key scan runs on every lookup regardless. Lookups of a
// millisecond or more are counted apart; the [texture] line says how many uploaded.
binding.resident = refreshResidentTarget(context, state, color, outcome, profile, [&] {
if (color.arrayLastSlice != 0) {
Require(color.dccAddress == 0 && color.cmaskAddress == 0 && AttachmentProxyFormat(context, color.format) == VK_FORMAT_UNDEFINED, "a slice of a 2D array color target with DCC, CMASK or an attachment proxy has no resident array image");
const auto array = ArraySurfaceForTarget(color);
Require(color.surfaceAddress == color.arrayAddress + color.arraySlice * DescribeSurface(array).layerBytes, "the array image's layer pitch differs from the color view's slice offset");
auto resident = CachedStorageSurface(context, array);
Require(resident->Attachable(), "storage format cannot be a color attachment");
return resident;
}
auto resident = CachedStorageSurface(context, SurfaceForTarget(color));
Require(resident->Attachable(), "storage format cannot be a color attachment");
Require(color.mipCount > 1 || color.depth > 1 || resident->GuestBytes() == colorLayout.Bytes(), "resident image layout differs from the color layout");
@ -1891,12 +1920,13 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
if (binding.resident != nullptr) {
timer.phase(PhaseReadTarget);
binding.proxied = AttachmentProxyFormat(context, color.format) != VK_FORMAT_UNDEFINED;
targetViews.push_back(binding.proxied ? binding.resident->AttachmentProxyView() : binding.resident->AttachmentView(color.format, color.mip, color.depthSlice));
targetViews.push_back(binding.proxied ? binding.resident->AttachmentProxyView() : binding.resident->AttachmentView(color.format, color.mip, color.depthSlice, color.arraySlice));
continue;
}
materializeCmaskClear(context, color, nullptr);
Require(!color.mipTail, "rendering into a packed mip tail needs the resident image of its surface");
Require(color.depth == 1, "rendering into a 3D color target needs the resident image of its surface");
Require(color.arraySlice == 0 || !ColorTileModeIsXor(color.tileMode), "rendering into a slice of a 2D array color target with an XOR tile mode needs the resident image of its array");
if (binding.gpuTiling) {
binding.mip = ColorTargetMip(color, colorLayout);
binding.tiled = std::make_unique<Buffer>(context, colorLayout.Bytes(), copies);
@ -1933,7 +1963,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
timer.phase(PhasePrepare);
const bool recordDraws = RecordDraws();
auto* recorder = Recorder::Active();
const bool recordable = recordDraws && recorder != nullptr && dumpLimit == 0 && std::all_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.resident != nullptr; });
const bool recordable = recordDraws && recorder != nullptr && dumpLimit == 0 && std::all_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.resident != nullptr || binding.multisampled; });
auto resolved = resolveDrawResources(context, state, draw, shaders, snapshots, indexBytes, recordable, outcome, timer);
auto& resources = resolved.resources;
const auto& contentKey = resolved.contentKey;
@ -2044,6 +2074,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
// transitions of every target and one pass per draw, as before.
static const bool drawTransitions = std::getenv("APS5_DRAW_TRANSITIONS") != nullptr;
const bool lean = recorded && !drawTransitions;
const bool multisampled = std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.multisampled; });
Require(lean || !multisampled, "multisampled draws are only rendered as recorded draws");
if (!lean && !resolved.moved.empty()) {
resolved.resources = std::make_shared<ShaderResources>(context, shaders, state.color, static_cast<std::uint32_t>(state.colors.size()), draw.indexAddress, static_cast<std::size_t>(indexBytes), snapshots);
resolved.moved.clear();
@ -2064,8 +2096,13 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
// reused with the pipeline; a per-draw RenderTarget gets a framebuffer of its own.
std::vector<std::shared_ptr<StorageTexture>> owners;
owners.reserve(targets.size());
for (const auto& binding : targets) owners.push_back(binding.resident);
auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent);
std::vector<std::uint8_t> persistent;
persistent.reserve(targets.size());
for (const auto& binding : targets) {
owners.push_back(binding.resident);
persistent.push_back(binding.multisampled ? 1u : 0u);
}
auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent, persistent);
timer.phase(PhasePipeline);
APS5_LOG_CHARS_OUT_DEBUG("Pipeline created");
if (lean) {
@ -2089,7 +2126,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
// only a template the resource cache serves under this content key (reusable: no lease,
// no copied writes, every direct region import- or mirror-served) of a direct draw, so a
// hit's proof is the template's ProveCurrent and nothing needs completion work.
if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth && resolved.moved.empty()) {
if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth && resolved.moved.empty() && !multisampled) {
auto recipe = std::make_shared<DrawRecipe>();
recipe->device = context.device;
recipe->templateRef = resources;
@ -2478,6 +2515,15 @@ std::shared_ptr<StorageTexture> metadataPassResident(const Context& context, con
}
void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass) {
if (pass.mode == ColorMetadataPass::Mode::Resolve) {
Require(pass.source.has_value() && pass.targets.size() == 1, "CB resolve without its source and destination");
const auto& destination = pass.targets.front();
Require(destination.tileMode == ColorTileMode::RenderTarget && context.detiler != nullptr, "CB resolve into a target without a resident image is not implemented");
const auto resident = CachedStorageSurface(context, SurfaceForTarget(destination));
Require(resident->Attachable(), "storage format cannot be a color attachment");
ResolveMultisampleTarget(context, *pass.source, *resident, resident->AttachmentView(destination.format, destination.mip));
return;
}
for (const auto& color : pass.targets) {
if (color.cmaskAddress != 0) {
const auto resident = metadataPassResident(context, color);

View File

@ -113,6 +113,8 @@ struct GuestBufferMemory::AddressSpace {
namespace {
struct RetiredImport;
struct AddressSpaceCache {
std::atomic<std::shared_ptr<const GuestBufferMemory::AddressSpace>> current;
std::atomic<std::uint64_t> serials{0};
@ -148,6 +150,7 @@ struct HostImports {
PFN_vkDestroyBuffer destroyBuffer = nullptr;
PFN_vkFreeMemory freeMemory = nullptr;
std::map<std::uint64_t, HostImport> imports;
std::map<VkDevice, std::vector<std::shared_ptr<RetiredImport>>> parked;
std::uint64_t liveBytes = 0;
std::set<std::uint64_t> failed;
std::set<std::uint64_t> unaliasedSpans;
@ -763,8 +766,8 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
if (state.device != context.device) {
for (const auto& [address, entry] : state.imports) {
if (state.device != VK_NULL_HANDLE && state.destroyBuffer != nullptr && state.freeMemory != nullptr) {
state.destroyBuffer(state.device, entry.buffer, nullptr);
state.freeMemory(state.device, entry.memory, nullptr);
state.parked[state.device].push_back(std::make_shared<RetiredImport>(state.device, state.destroyBuffer, state.freeMemory, entry));
continue;
}
#ifdef _WIN32
GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias);
@ -1731,6 +1734,7 @@ void ClearImageMirrors(VkDevice device) {
void ClearHostImports(VkDevice device) {
auto& state = Imports();
std::lock_guard lock(state.mutex);
state.parked.erase(device);
if (state.device != device) return;
for (const auto& [address, entry] : state.imports) {
state.destroyBuffer(state.device, entry.buffer, nullptr);

View File

@ -0,0 +1,332 @@
#include <shared_mutex>
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include <algorithm>
#include <array>
#include <atomic>
#include <cstdio>
#include <cstring>
#include <memory>
#include <mutex>
#include <vector>
namespace AgcDriver::Graphics {
namespace {
bool sameTarget(const ColorTarget& a, const ColorTarget& b) {
return a.surfaceAddress == b.surfaceAddress && a.format == b.format && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.samples == b.samples;
}
VkClearColorValue fastClearColor(const ColorTarget& target) {
Require(target.elementBytes != 0 && target.elementBytes <= sizeof(target.clearWords) && 16u % target.elementBytes == 0, "the fast clear of a multisampled target over 64-bit texels is not modeled");
std::array<std::byte, 16> texels{};
for (std::size_t offset = 0; offset < texels.size(); offset += target.elementBytes) std::memcpy(texels.data() + offset, target.clearWords.data(), target.elementBytes);
std::array<std::uint32_t, 4> pattern{};
std::memcpy(pattern.data(), texels.data(), texels.size());
VkClearColorValue clear{};
Require(ClearColorForTexel(target.format, target.elementBytes, pattern, clear), "the fast-clear color of a multisampled target has no exact clear value in its format");
return clear;
}
template<typename Record>
void recordCommands(const Context& context, Record&& record) {
auto* recorder = Recorder::Active();
std::unique_ptr<CommandBatch> batch;
if (recorder == nullptr) batch = std::make_unique<CommandBatch>(context);
record(recorder != nullptr ? recorder->Commands() : batch->Handle(), recorder);
if (batch) batch->SubmitAndWait();
}
class MultisampleTarget {
public:
MultisampleTarget(const Context& context, const ColorTarget& target) : context(context), target(target), cmask(target.cmaskAddress, target.cmaskBytes) {
this->context.bufferPool.reset();
const auto samples = static_cast<VkSampleCountFlagBits>(target.samples);
Require((context.limits.framebufferColorSampleCounts & samples) != 0, "the device cannot render color with the target's sample count");
VkFormatProperties properties{};
context.formatProperties(context.physical, target.format, &properties);
Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0, "multisampled render-target format cannot be a color attachment");
constexpr VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
VkImageFormatProperties supported{};
Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target");
Require((supported.sampleCounts & samples) != 0 && target.extent.width <= supported.maxExtent.width && target.extent.height <= supported.maxExtent.height, "multisampled render target exceeds device image limits");
try {
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
info.imageType = VK_IMAGE_TYPE_2D;
info.format = target.format;
info.extent = {target.extent.width, target.extent.height, 1};
info.mipLevels = 1;
info.arrayLayers = 1;
info.samples = samples;
info.tiling = VK_IMAGE_TILING_OPTIMAL;
info.usage = usage;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage multisampled target");
VkMemoryRequirements requirements{};
context.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(context.device, image, &requirements);
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocation.allocationSize = requirements.size;
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory multisampled target");
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory multisampled target");
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.image = image;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = target.format;
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView multisampled target");
recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) {
VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL;
toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toGeneral.image = image;
toGeneral.subresourceRange = viewInfo.subresourceRange;
context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral);
const VkClearColorValue zero{};
context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &zero, 1, &toGeneral.subresourceRange);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2);
});
} catch (...) {
release();
throw;
}
}
~MultisampleTarget() { release(); }
MultisampleTarget(const MultisampleTarget&) = delete;
MultisampleTarget& operator=(const MultisampleTarget&) = delete;
void ApplyFastClear(const ColorTarget& use) {
if (!cmask.TakeClear()) return;
const auto clear = fastClearColor(use);
recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) {
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT);
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2);
});
}
VkRenderPass ResolvePass() {
if (resolvePass != VK_NULL_HANDLE) return resolvePass;
std::array<VkAttachmentDescription, 2> attachments{};
attachments[0].format = target.format;
attachments[0].samples = static_cast<VkSampleCountFlagBits>(target.samples);
attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[1].format = target.format;
attachments[1].samples = VK_SAMPLE_COUNT_1_BIT;
attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
for (auto& attachment : attachments) {
attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachment.initialLayout = VK_IMAGE_LAYOUT_GENERAL;
attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL;
}
const VkAttachmentReference color{0, VK_IMAGE_LAYOUT_GENERAL};
const VkAttachmentReference resolve{1, VK_IMAGE_LAYOUT_GENERAL};
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &color;
subpass.pResolveAttachments = &resolve;
VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
info.attachmentCount = static_cast<std::uint32_t>(attachments.size());
info.pAttachments = attachments.data();
info.subpassCount = 1;
info.pSubpasses = &subpass;
Check(context.Function<PFN_vkCreateRenderPass>("vkCreateRenderPass")(context.device, &info, nullptr, &resolvePass), "vkCreateRenderPass resolve");
return resolvePass;
}
Context context;
ColorTarget target;
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
VkRenderPass resolvePass = VK_NULL_HANDLE;
MultisampledCmask cmask;
private:
void release() noexcept {
if (resolvePass) context.Function<PFN_vkDestroyRenderPass>("vkDestroyRenderPass")(context.device, resolvePass, nullptr);
if (view) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, view, nullptr);
if (image) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, image, nullptr);
if (memory) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, memory, nullptr);
}
};
struct GuardedRange {
VkDevice device;
std::uint64_t address;
std::uint64_t bytes;
std::uint32_t samples;
};
std::shared_mutex& guardedMutex() {
static std::shared_mutex mutex;
return mutex;
}
std::vector<GuardedRange>& guarded() {
static std::vector<GuardedRange> ranges;
return ranges;
}
std::atomic<bool>& anyGuarded() {
static std::atomic<bool> any{false};
return any;
}
void guardReads(std::uint64_t address, std::size_t bytes) {
RequireNoMultisampleTarget(address, bytes, "a CPU read");
}
void guard(const Context& context, const ColorTarget& target) {
const auto bytes = std::max<std::uint64_t>(target.bytes * target.samples, static_cast<std::uint64_t>(target.extent.width) * target.extent.height * target.elementBytes * target.samples);
std::unique_lock lock(guardedMutex());
guarded().push_back({context.device, target.address, bytes, target.samples});
anyGuarded().store(true, std::memory_order_release);
static std::once_flag installed;
std::call_once(installed, [] { GuestMemory::SetReadGuard(&guardReads); });
}
std::mutex& targetsMutex() {
static std::mutex mutex;
return mutex;
}
std::vector<std::unique_ptr<MultisampleTarget>>& targets() {
static std::vector<std::unique_ptr<MultisampleTarget>> list;
return list;
}
MultisampleTarget& acquire(const Context& context, const ColorTarget& use) {
for (const auto& target : targets()) {
if (target->context.device != context.device || !sameTarget(target->target, use)) continue;
target->cmask.CheckAddress(use.cmaskAddress);
target->ApplyFastClear(use);
return *target;
}
targets().push_back(std::make_unique<MultisampleTarget>(context, use));
guard(context, use);
auto& created = *targets().back();
if (use.cmaskFastClear) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast<std::uint64_t>(use.cmaskBytes) * 256u));
created.ApplyFastClear(use);
return created;
}
}
void MultisampledCmask::NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern) {
if (address == 0 || fillAddress >= address + bytes || address >= fillAddress + fillBytes) return;
if (fillAddress > address || fillAddress + fillBytes < address + bytes) {
char text[192];
std::snprintf(text, sizeof(text), "a fill of 0x%llx+0x%zx partly covers the CMASK 0x%llx+0x%llx of a multisampled color target, leaving its tiles mixed", static_cast<unsigned long long>(fillAddress), fillBytes, static_cast<unsigned long long>(address), static_cast<unsigned long long>(bytes));
refusal = text;
return;
}
refusal.clear();
if (pattern == 0) {
keys = DccKeys::Clear0000;
} else if (pattern == 0xffffffffu) {
keys = DccKeys::Uncompressed;
} else {
char text[192];
std::snprintf(text, sizeof(text), "a fill of the CMASK 0x%llx+0x%llx of a multisampled color target with 0x%08x leaves its tiles mixed (neither fast-cleared 0 nor expanded 0xffffffff)", static_cast<unsigned long long>(address), static_cast<unsigned long long>(bytes), pattern);
refusal = text;
}
}
void MultisampledCmask::SeedKeys(DccKeys seeded) {
if (seeded == DccKeys::Clear0000 || seeded == DccKeys::Uncompressed) {
keys = seeded;
return;
}
char text[192];
std::snprintf(text, sizeof(text), "the CMASK 0x%llx of a multisampled color target reads as %s, which is not modeled", static_cast<unsigned long long>(address), DccKeysName(seeded));
refusal = text;
}
void MultisampledCmask::CheckAddress(std::uint64_t cmaskAddress) const {
if (cmaskAddress == address) return;
char text[192];
std::snprintf(text, sizeof(text), "a multisampled color target used CMASK 0x%llx after its first use with CMASK 0x%llx, which is not modeled", static_cast<unsigned long long>(cmaskAddress), static_cast<unsigned long long>(address));
Require(false, std::string(text));
}
bool MultisampledCmask::TakeClear() {
Require(refusal.empty(), refusal);
if (keys != DccKeys::Clear0000) return false;
keys = DccKeys::Uncompressed;
return true;
}
VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target) {
std::lock_guard lock(targetsMutex());
return acquire(context, target).view;
}
void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView) {
std::lock_guard lock(targetsMutex());
auto& target = acquire(context, source);
const std::array<VkImageView, 2> views{target.view, destinationView};
auto framebuffer = std::make_shared<Framebuffer>(context, target.ResolvePass(), views, source.extent);
recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) {
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
begin.renderPass = target.ResolvePass();
begin.framebuffer = framebuffer->Handle();
begin.renderArea = {{0, 0}, source.extent};
context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE);
context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT);
if (recorder != nullptr) {
Recorder::CountBarriers(Recorder::CommandClass::Draw, 2);
recorder->Keep(framebuffer);
}
});
destination.MarkDirty();
}
void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) {
std::lock_guard lock(targetsMutex());
for (const auto& target : targets()) {
const auto before = target->cmask.Keys();
target->cmask.NoteFill(address, bytes, pattern);
if (before == DccKeys::Clear0000 || target->cmask.Keys() != DccKeys::Clear0000) continue;
static std::atomic<int> reports{0};
if (reports.fetch_add(1) < 8) std::fprintf(stderr, "[gpu] multisampled color target 0x%llx fast-cleared through its CMASK 0x%llx (fill 0x%llx+0x%zx)\n", static_cast<unsigned long long>(target->target.surfaceAddress), static_cast<unsigned long long>(target->target.cmaskAddress), static_cast<unsigned long long>(address), bytes);
}
}
void ClearMultisampleTargets(VkDevice device) {
std::lock_guard lock(targetsMutex());
std::erase_if(targets(), [&](const auto& target) { return target->context.device == device; });
std::unique_lock guardLock(guardedMutex());
std::erase_if(guarded(), [&](const auto& range) { return range.device == device; });
anyGuarded().store(!guarded().empty(), std::memory_order_release);
}
void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access) {
if (bytes == 0 || !anyGuarded().load(std::memory_order_acquire)) return;
std::shared_lock lock(guardedMutex());
for (const auto& range : guarded()) {
if (address >= range.address + range.bytes || range.address >= address + bytes) continue;
char text[256];
std::snprintf(text, sizeof(text), "AGC graphics: %s of 0x%llx+0x%zx overlaps multisampled color target 0x%llx+0x%llx (%u samples), whose samples are only on the host: guest access to it is not implemented", access, static_cast<unsigned long long>(address), bytes, static_cast<unsigned long long>(range.address), static_cast<unsigned long long>(range.bytes), range.samples);
throw std::runtime_error(text);
}
}
}

View File

@ -115,23 +115,10 @@ void ValidateViewport(const Context& context, const VkViewport& viewport) {
Require(std::min(viewport.y, viewport.y + viewport.height) >= context.limits.viewportBoundsRange[0] && std::max(viewport.y, viewport.y + viewport.height) <= context.limits.viewportBoundsRange[1], "viewport Y exceeds device bounds");
}
namespace {
VkShaderStageFlags pushStagesOf(const Context& context) {
return PreRasterizationPushStages(context.meshShader, context.tessellationShader, context.geometryShader);
}
template<typename TValue>
void appendKey(std::vector<std::byte>& key, const TValue& value) {
static_assert(std::is_trivially_copyable_v<TValue>);
const auto bytes = std::as_bytes(std::span(&value, 1));
key.insert(key.end(), bytes.begin(), bytes.end());
}
bool fragmentWritesMemory(std::span<const CompiledShader> shaders) {
bool ShadersWriteMemory(std::span<const CompiledShader> shaders) {
const auto any = [](const std::vector<bool>& written, std::uint32_t count) { return written.size() < count || std::ranges::any_of(written, [](bool element) { return element; }); };
for (const auto& shader : shaders) {
if (shader.stage != ShaderRecompiler::ShaderStage::Fragment || shader.program == nullptr) continue;
if (shader.program == nullptr) continue;
for (const auto& binding : shader.program->bindings) {
switch (binding.role) {
case ShaderRecompiler::DescriptorRole::GuestBuffers:
@ -154,6 +141,19 @@ bool fragmentWritesMemory(std::span<const CompiledShader> shaders) {
return false;
}
namespace {
VkShaderStageFlags pushStagesOf(const Context& context) {
return PreRasterizationPushStages(context.meshShader, context.tessellationShader, context.geometryShader);
}
template<typename TValue>
void appendKey(std::vector<std::byte>& key, const TValue& value) {
static_assert(std::is_trivially_copyable_v<TValue>);
const auto bytes = std::as_bytes(std::span(&value, 1));
key.insert(key.end(), bytes.begin(), bytes.end());
}
bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span<const CompiledShader> shaders) {
using Stage = ShaderRecompiler::ShaderStage;
if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || SplitsAlternateAlpha(state)) return false;
@ -186,6 +186,8 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State&
appendKey(keys.preRasterization, state.depth.has_value() && state.depthBias);
appendKey(keys.preRasterization, state.stages.tessellation.has_value());
if (state.stages.tessellation) appendKey(keys.preRasterization, state.stages.tessellation->inputControlPoints);
appendKey(keys.fragmentShader, state.samples);
appendKey(keys.fragmentOutput, state.samples);
for (const auto& blend : state.blends) appendKey(keys.fragmentOutput, blend);
for (const auto value : state.blendConstants) appendKey(keys.fragmentOutput, value);
appendKey(keys.renderPass, state.blends.size());
@ -209,7 +211,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
this->context.bufferPool.reset();
Require(state.blends.size() == (state.colors.empty() ? 0u : state.colors.back().exportIndex + 1u) && state.colors.size() <= state.blends.size(), "blend states do not match decoded color state");
Require(state.blends.size() <= context.limits.maxColorAttachments, "color targets exceed device attachment limits");
Require(state.hasColorTarget || (context.limits.framebufferNoAttachmentsSampleCounts & VK_SAMPLE_COUNT_1_BIT) != 0, "device does not support single-sample rendering without attachments");
Require(state.hasColorTarget || (context.limits.framebufferNoAttachmentsSampleCounts & static_cast<VkSampleCountFlagBits>(state.samples)) != 0, "device does not support rendering without attachments at the draw's sample count");
Require(state.samples == 1 || !state.depth.has_value() || (context.limits.framebufferDepthSampleCounts & static_cast<VkSampleCountFlagBits>(state.samples)) != 0, "device does not support depth targets with the draw's sample count");
Require(!depthBounds || context.depthBounds, "device does not support the depth bounds test");
Require(!depthBias || state.depthBiasClamp == 0.0f || context.depthBiasClamp, "device does not support depth bias clamping");
Require(!state.dualSourceBlend || (context.dualSrcBlend && state.blends.size() <= context.limits.maxFragmentDualSrcAttachments), "device does not support dual-source blending into this many attachments");
@ -270,7 +273,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
for (std::uint32_t index = 0; index < state.colors.size(); ++index) {
VkAttachmentDescription color{};
color.format = state.colors[index].format;
color.samples = VK_SAMPLE_COUNT_1_BIT;
color.samples = static_cast<VkSampleCountFlagBits>(state.samples);
color.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
@ -288,7 +291,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
if (state.depth) {
VkAttachmentDescription depth{};
depth.format = state.depth->format;
depth.samples = VK_SAMPLE_COUNT_1_BIT;
depth.samples = static_cast<VkSampleCountFlagBits>(state.samples);
depth.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
depth.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
depth.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
@ -363,7 +366,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
raster.pNext = &provoking;
}
VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
samples.rasterizationSamples = static_cast<VkSampleCountFlagBits>(state.samples);
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
depthStencil.depthTestEnable = state.depthTest;
depthStencil.depthWriteEnable = state.depthWrite;
@ -379,7 +382,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
if (alternateAlpha) {
const auto keepsStencil = [](const VkStencilOpState& face) { return face.writeMask == 0 || (face.failOp == VK_STENCIL_OP_KEEP && face.passOp == VK_STENCIL_OP_KEEP && face.depthFailOp == VK_STENCIL_OP_KEEP); };
Require(!(state.depth && (state.depthWrite || (state.stencilTest && (!keepsStencil(state.stencilFront) || !keepsStencil(state.stencilBack))))) && !state.depthBiasPerFace, "blending into an 8_8 color target with the alternate component order and a separate alpha blend from a draw that writes depth or stencil is not implemented");
Require(!fragmentWritesMemory(shaders), "blending into an 8_8 color target with the alternate component order and a separate alpha blend from a pixel shader that writes memory is not implemented");
Require(!ShadersWriteMemory(shaders), "blending into an 8_8 color target with the alternate component order and a separate alpha blend from a draw whose shaders write memory is not implemented");
for (std::size_t index = 0; index < secondChannels.size(); ++index) {
const bool split = index < state.colors.size() && state.colors[index].componentMapping == 0xecu && state.blends[index].blendEnable;
if (!split) {
@ -482,21 +485,26 @@ VkPipelineLayout Pipeline::Layout() const {
return layout;
}
std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent) {
std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent, std::span<const std::uint8_t> persistent) {
Require(targets.size() == attachments && owners.size() == colorAttachments, "render targets do not match the pipeline's attachments");
const bool resident = std::all_of(owners.begin(), owners.end(), [](const auto& owner) { return owner != nullptr; });
bool resident = true;
for (std::size_t i = 0; i < owners.size(); ++i) resident = resident && (owners[i] != nullptr || (i < persistent.size() && persistent[i] != 0));
if (resident) {
// Entries whose views are gone can never match again and go as soon as no recorded draw holds
// them (Kept keeps its framebuffer until the batch completes).
std::erase_if(framebuffers, [](const CachedFramebuffer& entry) {
return entry.framebuffer.use_count() == 1 && std::any_of(entry.owners.begin(), entry.owners.end(), [](const auto& owner) { return owner.expired(); });
if (entry.framebuffer.use_count() != 1) return false;
for (std::size_t i = 0; i < entry.owners.size(); ++i) {
if (entry.owned[i] && entry.owners[i].expired()) return true;
}
return false;
});
for (auto it = framebuffers.begin(); it != framebuffers.end(); ++it) {
if (it->extent.width != extent.width || it->extent.height != extent.height || !std::equal(it->views.begin(), it->views.end(), targets.begin(), targets.end())) continue;
// View handles are recycled once a StorageTexture is destroyed, so the owners must be the
// very objects the views were made for.
bool same = true;
for (std::size_t i = 0; i < owners.size() && same; ++i) same = it->owners[i].lock().get() == owners[i].get();
for (std::size_t i = 0; i < owners.size() && same; ++i) same = it->owned[i] == (owners[i] != nullptr) && it->owners[i].lock().get() == owners[i].get();
if (!same) continue;
std::rotate(it, std::next(it), framebuffers.end());
return framebuffers.back().framebuffer;
@ -514,6 +522,7 @@ std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImag
CachedFramebuffer entry;
entry.views.assign(targets.begin(), targets.end());
entry.owners.assign(owners.begin(), owners.end());
for (const auto& owner : owners) entry.owned.push_back(owner != nullptr);
entry.extent = extent;
entry.framebuffer = framebuffer;
framebuffers.push_back(std::move(entry));
@ -677,6 +686,7 @@ void pipelineKey(std::vector<std::byte>& key, const Context& context, const Stat
append(key, state.blends.size());
for (const auto& blend : state.blends) append(key, blend);
for (const auto value : state.blendConstants) append(key, value);
append(key, state.samples);
append(key, state.colors.size());
for (const auto& color : state.colors) append(key, color.format);
if (state.blends.size() != state.colors.size()) {

View File

@ -103,7 +103,7 @@ Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usag
ready = true;
return;
}
Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory buffer");
memory = cache->AllocateMemory(allocation, "vkAllocateMemory buffer");
Check(context.Function<PFN_vkBindBufferMemory>("vkBindBufferMemory")(context.device, buffer, memory, 0), "vkBindBufferMemory");
initializeAddress(usage);
if ((properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0) Check(context.Function<PFN_vkMapMemory>("vkMapMemory")(context.device, memory, 0, VK_WHOLE_SIZE, 0, &mapping), "vkMapMemory");
@ -182,7 +182,7 @@ DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsa
allocation.allocationSize = ClassAllocationBytes(requirements, capacity);
allocationBytes = allocation.allocationSize;
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory device buffer");
memory = cache->AllocateMemory(allocation, "vkAllocateMemory device buffer");
Check(context.Function<PFN_vkBindBufferMemory>("vkBindBufferMemory")(context.device, buffer, memory, 0), "vkBindBufferMemory device");
} catch (...) {
release();

View File

@ -93,6 +93,16 @@ ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers&
};
}
ShaderRecompiler::ShaderPixelStageInfo DefaultUnexportedInputs(ShaderRecompiler::ShaderPixelStageInfo pixel, std::span<const std::uint32_t> parameterExports) {
for (std::uint32_t input = 0; input < pixel.interpolatorCount && input < pixel.interpolatorSettings.size(); ++input) {
auto& setting = pixel.interpolatorSettings[input];
if ((setting & 0x20u) != 0u || std::ranges::find(parameterExports, setting & 0x1fu) != parameterExports.end()) continue;
setting = (setting & ~0x700u) | 0x20u;
if ((setting & 0x80000u) != 0u) setting = (setting & ~0x600000u) | 0x100000u;
}
return pixel;
}
ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array<std::uint8_t, 8>& exportMappings, bool nullProgram) {
const auto inControl = nullProgram ? 0u : read(context, spiPsInControl, RegisterBank::Context);
const auto inputNum = inControl & 0x3Fu;
@ -162,7 +172,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
};
}
ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const std::byte> header, std::uint64_t headerAddress, std::span<const std::uint32_t> userData, std::vector<DecodeRead>* reads, bool staticAbi) {
ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const std::byte> header, std::uint64_t headerAddress, std::span<const std::uint32_t> userData, std::vector<DecodeRead>* reads, bool staticAbi, std::uint32_t hiddenUserWords) {
if (header.empty()) return {};
if (header.size() < sizeof(Shader)) throw std::runtime_error("AGC graphics: shader header is smaller than the fixed AGC header");
Shader shader;
@ -185,14 +195,16 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const st
}
if (vertexAttribReg < 0) return info;
if (vertexBufferReg < 0) throw std::runtime_error("AGC graphics: vertex attribute table requires a vertex buffer table");
if (static_cast<std::uint32_t>(vertexBufferReg) + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex buffer table pointer exceeds the user-SGPR domain");
if (static_cast<std::uint32_t>(vertexAttribReg) + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex attribute table pointer exceeds the user-SGPR domain");
const auto bufferWord = static_cast<std::uint32_t>(vertexBufferReg) + hiddenUserWords;
const auto attribWord = static_cast<std::uint32_t>(vertexAttribReg) + hiddenUserWords;
if (bufferWord + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex buffer table pointer exceeds the user-SGPR domain");
if (attribWord + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex attribute table pointer exceeds the user-SGPR domain");
if (shader.num_input_semantics == 0 || shader.num_input_semantics > ShaderRecompiler::ShaderVertexStageInfo::MaxResources) throw std::runtime_error("AGC graphics: vertex semantic count is outside the supported domain");
if (shader.input_semantics == nullptr) throw std::runtime_error("AGC graphics: missing vertex input semantics");
std::array<ShaderSemantic, ShaderRecompiler::ShaderVertexStageInfo::MaxResources> semantics{};
_readHeaderArray(header, headerAddress, shader.input_semantics, shader.num_input_semantics, semantics.data());
const auto attribTableAddr = static_cast<std::uint64_t>(userData[static_cast<std::uint32_t>(vertexAttribReg)]) | (static_cast<std::uint64_t>(userData[static_cast<std::uint32_t>(vertexAttribReg) + 1u]) << 32u);
const auto bufferTableAddr = static_cast<std::uint64_t>(userData[static_cast<std::uint32_t>(vertexBufferReg)]) | (static_cast<std::uint64_t>(userData[static_cast<std::uint32_t>(vertexBufferReg) + 1u]) << 32u);
const auto attribTableAddr = static_cast<std::uint64_t>(userData[attribWord]) | (static_cast<std::uint64_t>(userData[attribWord + 1u]) << 32u);
const auto bufferTableAddr = static_cast<std::uint64_t>(userData[bufferWord]) | (static_cast<std::uint64_t>(userData[bufferWord + 1u]) << 32u);
if (!staticAbi && attribTableAddr == 0) throw std::runtime_error("AGC graphics: null vertex attribute table address");
if (!staticAbi && bufferTableAddr == 0) throw std::runtime_error("AGC graphics: null vertex buffer table address");
info.fetchEmbedded = true;

View File

@ -2,6 +2,7 @@
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
#include "ThreadOwned.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
@ -513,6 +514,7 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) {
// `guestBytes` is the surface size when the caller described the surface already (0: described here).
std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false) {
CaptureTrace::Log("sampled-lookup address=%llx width=%u height=%u dcc=%llx", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, static_cast<unsigned long long>(resource.dccAddress));
RequireNoMultisampleTarget(resource.baseAddress, guestBytes != 0 ? static_cast<std::size_t>(guestBytes) : 1u, "a texture read");
if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes();
if (auto depth = DepthSurfaceTexture(context, words, resource, components)) {
logLookup({depth.get(), resource, 0, DccKeys::Uncompressed, 0, nullptr, true});
@ -831,13 +833,14 @@ GuestTextureResource StorageSurface(const Context& context, const GuestTextureRe
// `guestBytes` is the surface size when the caller described the surface already (0: described here).
std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& viewed, std::uint32_t mip, std::uint64_t guestBytes) {
RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast<std::size_t>(guestBytes) : 1u, "a storage image access");
if (DepthSurfaceAt(viewed.baseAddress)) {
char text[112];
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented", static_cast<unsigned long long>(viewed.baseAddress));
throw std::runtime_error(text);
}
static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr;
if (disabled) return std::make_shared<StorageTexture>(context, *context.detiler, viewed, mip);
if (disabled) return StorageTexture::Create(context, *context.detiler, viewed, mip);
static_cast<void>(words);
if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes();
const bool profile = LookupOutcomes::Profiled();
@ -869,7 +872,7 @@ std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std
} else {
if (guestBytes == 0) guestBytes = DescribeSurface(resource).guestBytes;
if (StorageTexture::FlushPending(resource.baseAddress, static_cast<std::size_t>(guestBytes), nullptr, "storage image creation", PublishScope::None) && profile) start = LookupOutcomes::Add(LookupOutcomes::PendingFlush, start);
CachedStorageTexture entry{key, mip, std::make_shared<StorageTexture>(context, *context.detiler, resource, mip)};
CachedStorageTexture entry{key, mip, StorageTexture::Create(context, *context.detiler, resource, mip)};
static const bool keepNew = std::getenv("APS5_NO_KEEP_NEW_STORAGE") == nullptr;
if (auto* recorder = Recorder::Active(); keepNew && recorder != nullptr && GuestMemory::GpuMutex().HeldByThisThread() && recorder->Recording()) recorder->Keep(entry.texture);
entry.accounted = heldBytes(*entry.texture);
@ -969,18 +972,29 @@ void ClearCachedTextures(VkDevice device) {
}
}
auto& storage = StorageTextures();
std::lock_guard lock(storage.mutex);
for (auto it = storage.entries.begin(); it != storage.entries.end();) {
if (it->key.device != device) {
++it;
continue;
std::vector<std::shared_ptr<StorageTexture>> cleared;
{
std::lock_guard lock(storage.mutex);
for (auto it = storage.entries.begin(); it != storage.entries.end();) {
if (it->key.device != device) {
++it;
continue;
}
cleared.push_back(it->texture);
it->texture->SetCached(false);
noteUncached(it->texture);
storage.bytes -= it->accounted;
storage.index.erase(it->key);
storage.byImage.erase(it->texture.get());
it = storage.entries.erase(it);
}
}
for (const auto& image : cleared) {
std::vector<std::shared_ptr<ShaderResources>> resources;
SharedResourceCache().RemoveImage(image.get(), &resources);
if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) {
for (auto& resource : resources) recorder->Keep(std::move(resource));
}
it->texture->SetCached(false);
noteUncached(it->texture);
storage.bytes -= it->accounted;
storage.index.erase(it->key);
storage.byImage.erase(it->texture.get());
it = storage.entries.erase(it);
}
}
@ -1135,14 +1149,13 @@ void ShaderResources::build(std::span<const CompiledShader> shaders, const Color
namespace {
VkDescriptorSetLayout CreateSetLayout(const Context& context, std::span<const VkDescriptorSetLayoutBinding> bindings, bool updateAfterBind) {
std::vector<VkDescriptorBindingFlags> flags;
for (const auto& binding : bindings) flags.push_back(updateAfterBind && binding.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0);
VkDescriptorSetLayout CreateSetLayout(const Context& context, std::span<const VkDescriptorSetLayoutBinding> bindings, bool updateAfterBind, std::span<const VkDescriptorBindingFlags> flags) {
Require(flags.size() == bindings.size(), "descriptor binding flag count differs from layout");
VkDescriptorSetLayoutBindingFlagsCreateInfo bindingFlags{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO};
bindingFlags.bindingCount = static_cast<std::uint32_t>(flags.size());
bindingFlags.pBindingFlags = flags.data();
VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
info.pNext = updateAfterBind ? &bindingFlags : nullptr;
info.pNext = std::ranges::any_of(flags, [](auto value) { return value != 0; }) ? &bindingFlags : nullptr;
info.flags = updateAfterBind ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0;
info.bindingCount = static_cast<std::uint32_t>(bindings.size());
info.pBindings = bindings.data();
@ -1404,16 +1417,17 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
std::vector<VkDescriptorSetLayoutBinding> description;
for (const auto& binding : bindings) {
description.push_back(binding.layout);
layoutBindingFlags.push_back(updateAfterBind && binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0);
layoutKey.insert(layoutKey.end(), {binding.layout.binding, static_cast<std::uint32_t>(binding.layout.descriptorType), binding.layout.descriptorCount, binding.layout.stageFlags});
}
if (updateAfterBind) layoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT);
static const bool noLayoutCache = std::getenv("APS5_NO_LAYOUT_CACHE") != nullptr;
static const bool noPoolCache = std::getenv("APS5_NO_POOL_CACHE") != nullptr;
if (context.descriptorCache != nullptr && !noLayoutCache) {
_layout = context.descriptorCache->Layout(layoutKey, description, updateAfterBind);
_layout = context.descriptorCache->Layout(layoutKey, description, updateAfterBind, layoutBindingFlags);
} else {
ValidateDescriptorLayout(context, description, updateAfterBind);
_layout = CreateSetLayout(context, description, updateAfterBind);
_layout = CreateSetLayout(context, description, updateAfterBind, layoutBindingFlags);
ownsLayout = true;
}
if (!bindings.empty()) {
@ -2524,6 +2538,7 @@ void ResourceCache::noteMiss(const Key& key) {
void ResourceCache::Insert(const Key& key, std::shared_ptr<ShaderResources> resources, std::vector<std::shared_ptr<ShaderResources>>* evicted) {
std::lock_guard lock(mutex);
if (!resources->HasCachedStorageImages()) return;
if (const auto found = index.find(key); found != index.end()) {
if (evicted != nullptr) evicted->push_back(std::move(found->second->second));
entries.erase(found->second);
@ -2531,9 +2546,6 @@ void ResourceCache::Insert(const Key& key, std::shared_ptr<ShaderResources> reso
}
entries.emplace_front(key, std::move(resources));
index.emplace(key, entries.begin());
// Entries pin their textures and storage images past the texture caches' budgets, so the bound
// stays modest: APS5_RESOURCE_CACHE_ENTRIES (default 1024) covers several frames of distinct
// dispatch and draw content.
static const std::size_t capacity = [] {
const char* value = std::getenv("APS5_RESOURCE_CACHE_ENTRIES");
const auto parsed = value ? std::strtoull(value, nullptr, 10) : 1024ull;
@ -2617,16 +2629,24 @@ DescriptorCache::~DescriptorCache() {
for (const auto& [key, layout] : layouts) destroyLayout(context.device, layout, nullptr);
}
VkDescriptorSetLayout DescriptorCache::Layout(std::span<const std::uint32_t> key, std::span<const VkDescriptorSetLayoutBinding> bindings, bool updateAfterBind) {
VkDescriptorSetLayout DescriptorCache::Layout(std::span<const std::uint32_t> key, std::span<const VkDescriptorSetLayoutBinding> bindings, bool updateAfterBind, std::span<const VkDescriptorBindingFlags> flags) {
std::vector<VkDescriptorBindingFlags> defaults;
if (flags.empty()) {
for (const auto& binding : bindings) defaults.push_back(updateAfterBind && binding.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0);
flags = defaults;
}
Require(flags.size() == bindings.size(), "descriptor binding flag count differs from layout");
std::lock_guard lock(mutex);
std::vector<std::uint32_t> keyCopy(key.begin(), key.end());
keyCopy.push_back(static_cast<std::uint32_t>(flags.size()));
keyCopy.insert(keyCopy.end(), flags.begin(), flags.end());
if (const auto found = layouts.find(keyCopy); found != layouts.end()) {
++stats.layoutHits;
return found->second;
}
ValidateDescriptorLayout(context, bindings, updateAfterBind);
++stats.layoutMisses;
const auto layout = CreateSetLayout(context, bindings, updateAfterBind);
const auto layout = CreateSetLayout(context, bindings, updateAfterBind, flags);
layouts.emplace(std::move(keyCopy), layout);
return layout;
}
@ -3144,9 +3164,9 @@ void ShaderResources::applyAnisoOverride() {
if (!variant.has_value()) continue;
const auto& words = *variant;
if (context.samplerCache != nullptr && !SamplerCacheDisabled()) {
samplers[index] = context.samplerCache->Get(context, words, source.compareEnable, source.unnormalized, false, source.integerBorder);
samplers[index] = context.samplerCache->Get(context, words, source.compareEnable, source.unnormalized, true, source.integerBorder);
} else {
auto resource = DecodeSamplerResource(words, source.unnormalized, false, source.integerBorder);
auto resource = DecodeSamplerResource(words, source.unnormalized, true, source.integerBorder);
resource.compareEnable = source.compareEnable;
samplers[index] = std::make_shared<Sampler>(context, resource);
}
@ -3322,6 +3342,16 @@ VkDescriptorSetLayout ShaderResources::Layout() const {
return _layout;
}
std::vector<VkDescriptorSetLayoutBinding> ShaderResources::LayoutBindings() const {
std::vector<VkDescriptorSetLayoutBinding> result;
for (const auto& binding : bindings) result.push_back(binding.layout);
return result;
}
bool ShaderResources::LayoutUpdateAfterBind() const {
return updateAfterBind;
}
ShaderResources::DrawBindings::~DrawBindings() {
if (cache != nullptr && allocation.set != VK_NULL_HANDLE) cache->Free(allocation);
}
@ -3374,9 +3404,17 @@ std::optional<std::vector<ShaderResources::MovedBuffer>> ShaderResources::MovedR
return moved;
}
std::size_t ShaderResources::MaxDrawSnapshotBytes() {
const char* text = std::getenv("APS5_DRAW_SNAPSHOT_MAX_MIB");
const auto mib = text != nullptr ? std::strtoull(text, nullptr, 10) : 64ull;
return static_cast<std::size_t>(mib * 1024ull * 1024ull);
}
std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span<const MovedBuffer> moved) const {
if (_set == VK_NULL_HANDLE || usesBda) return {};
const auto reads = guestMemory.InPlaceReads();
const auto maxSnapshotBytes = MaxDrawSnapshotBytes();
std::size_t totalSnapshotBytes = 0;
auto result = std::make_shared<DrawBindings>();
std::vector<std::size_t> selected;
for (std::size_t index = 0; index < allocations.size(); ++index) {
@ -3404,6 +3442,7 @@ std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindi
}
const auto begin = address - item.adjustment;
const auto bytes = static_cast<std::size_t>(GuestBufferMemory::ViewBytes(size, item.adjustment));
if (override == moved.end() && (bytes > maxSnapshotBytes || totalSnapshotBytes + bytes > maxSnapshotBytes)) continue;
const auto registryGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix();
const auto generation = GuestMemory::CollectWrites(begin, bytes);
auto buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation);
@ -3414,6 +3453,7 @@ std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindi
}
selected.push_back(index);
result->snapshots.push_back({begin, std::move(buffer)});
totalSnapshotBytes += bytes;
CaptureTrace::Log("draw-snapshot batch=%llu address=%llx bytes=%zu", static_cast<unsigned long long>(recorder.Submissions() + 1), static_cast<unsigned long long>(begin), bytes);
}
if (selected.empty()) return {};
@ -3526,6 +3566,11 @@ std::vector<std::pair<VkImage, bool>> ShaderResources::StorageImages() const {
return images;
}
bool ShaderResources::HasCachedStorageImages() const {
if (std::any_of(storageTextures.begin(), storageTextures.end(), [](const auto& image) { return image != nullptr && !image->Cached(); })) return false;
return std::none_of(textures.begin(), textures.end(), [](const auto& texture) { return texture != nullptr && texture->StorageSource() != nullptr && !texture->StorageSource()->Cached(); });
}
bool ShaderResources::ReadsImage(const StorageTexture* image) const {
if (image == nullptr) return false;
for (const auto& texture : textures) {

View File

@ -11,6 +11,8 @@
#include <cmath>
#include <cstdlib>
#include <limits>
#include <span>
#include <array>
#include <sstream>
namespace AgcDriver::Graphics {
@ -138,6 +140,14 @@ bool depthSurfaceBound(const Registers& cx) {
return (z != cx.end() && (z->second & 3u) != 0) || (stencil != cx.end() && (stencil->second & 1u) != 0);
}
std::uint32_t planeTests(const Registers& cx, std::uint32_t depthControl) {
const auto z = find(cx, 0x010);
const auto stencil = find(cx, 0x011);
if (z != cx.end() && (z->second & 3u) == 0) depthControl &= ~2u;
if (stencil != cx.end() && (stencil->second & 1u) == 0) depthControl &= ~1u;
return depthControl & 0xbu;
}
bool depthPlanesAbsent(const Registers& cx) {
const auto z = find(cx, 0x010);
const auto stencil = find(cx, 0x011);
@ -199,7 +209,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result)
const bool stencilClear = (read(cx, 0x000) & 2u) != 0;
const auto view = read(cx, 0x002);
zero(cx, 0x002, 0x3c000000u, "depth mips (DB_DEPTH_VIEW MIP_LEVEL)");
zero(cx, 0x010, 0x000f100cu, "multisampled, partially resident or mipmapped depth (DB_Z_INFO)");
zero(cx, 0x010, 0x000f1000u, "partially resident or mipmapped depth (DB_Z_INFO)");
zero(cx, 0x011, 0x00001000u, "partially resident stencil (DB_STENCIL_INFO)");
const auto zFormat = read(cx, 0x010) & 3u;
const bool stencil = (read(cx, 0x011) & 1u) != 0;
@ -215,6 +225,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result)
Require(!stencilClear || (stencil && !stencilReadOnly), "stencil clear requires a writable stencil plane");
Require(!depthClear || (zFormat != 0 && !depthReadOnly), "depth clear requires a writable depth plane");
DepthTarget depth{};
depth.samples = 1u << ((read(cx, 0x010) >> 2u) & 3u);
depth.address = zFormat != 0 ? base(0x012, 0x01a) : 0;
depth.stencilAddress = stencil ? base(0x013, 0x01b) : 0;
Require(zFormat == 0 || depthReadOnly || base(0x014, 0x01c) == depth.address, "depth read and written at different addresses is unsupported");
@ -302,6 +313,34 @@ std::uint32_t effectiveDepthControl(std::uint32_t depthControl) {
return (depthControl & 3u) == 0 ? depthControl & ~4u : depthControl;
}
constexpr std::array<std::array<std::int8_t, 2>, 2> StandardLocations2{{{4, 4}, {-4, -4}}};
constexpr std::array<std::array<std::int8_t, 2>, 4> StandardLocations4{{{-2, -6}, {6, -2}, {-6, 2}, {2, 6}}};
constexpr std::array<std::array<std::int8_t, 2>, 8> StandardLocations8{{{1, -3}, {-1, 3}, {5, 1}, {-3, -5}, {-5, 5}, {-7, -1}, {3, 7}, {7, -7}}};
std::uint32_t decodeSamples(const Registers& cx) {
const auto config = read(cx, 0x2f8);
zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling");
const auto log2 = config & 7u;
if (log2 == 0) {
Require(((config >> 20u) & 7u) == 0, "exposed samples without multisampling are unsupported");
return 1;
}
Require(log2 <= 3u, "more than eight samples are unsupported");
Require(((config >> 20u) & 7u) == log2, "exposing fewer samples than are rasterized is unsupported");
const auto samples = 1u << log2;
const std::span<const std::array<std::int8_t, 2>> standard = samples == 2 ? std::span<const std::array<std::int8_t, 2>>(StandardLocations2) : samples == 4 ? std::span<const std::array<std::int8_t, 2>>(StandardLocations4) : std::span<const std::array<std::int8_t, 2>>(StandardLocations8);
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) {
for (std::uint32_t sample = 0; sample < samples; ++sample) {
const auto word = read(cx, 0x2fe + pixel * 4u + sample / 4u);
const auto bits = (word >> (8u * (sample % 4u))) & 0xffu;
const auto x = static_cast<std::int8_t>(static_cast<std::uint8_t>(bits << 4u)) >> 4;
const auto y = static_cast<std::int8_t>(static_cast<std::uint8_t>(bits & 0xf0u)) >> 4;
Require(x == standard[sample][0] && y == standard[sample][1], "nonstandard sample locations are unsupported");
}
}
return samples;
}
bool colorControlSupported(std::uint32_t colorControl, bool hasColorTarget) {
colorControl &= ~1u;
return colorControl == 0xcc0010u || ((colorControl >> 4u) & 7u) == 0u || (!hasColorTarget && (colorControl & ~0x70u) == 0xcc0000u);
@ -579,13 +618,15 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
}
}
zero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports");
result.samples = decodeSamples(cx);
{
const auto depthControl = read(cx, 0x200);
const auto renderControl = find(cx, 0x000);
const bool clear = renderControl != cx.end() && (renderControl->second & 3u) != 0;
if (clear || ((depthControl & 0xbu) != 0 && depthSurfaceBound(cx))) {
const auto tests = planeTests(cx, depthControl);
if (clear || (tests != 0 && depthSurfaceBound(cx))) {
decodeDepth(cx, depthControl, result);
} else if (depthPassThrough(depthControl) || ((depthControl & 0xbu) != 0 && depthPlanesAbsent(cx))) {
} else if (depthPassThrough(depthControl) || ((depthControl & 0xbu) != 0 && (tests == 0 || depthPlanesAbsent(cx)))) {
static bool reported = false;
if (!reported) {
reported = true;
@ -606,8 +647,9 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
zero(cx, 0x203, shaderControlMask(read(cx, 0x1c4)), "depth export, shader coverage or ordered fragment execution");
result.alphaToCoverage = read(cx, 0x2dc) == AlphaToCoverageUndithered;
if (!result.alphaToCoverage) zero(cx, 0x2dc, AlphaToCoverageMask, "dithered or rounded alpha-to-coverage");
zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion");
zero(cx, 0x292, ScanModeMask, "scan conversion mode");
Require(!result.depth || result.depth->samples == result.samples, "the depth target's sample count differs from the rasterizer's");
zero(cx, 0x292, result.samples > 1 ? ScanModeMask & ~1u : ScanModeMask, "scan conversion mode");
Require(result.samples == 1 || (read(cx, 0x292) & 1u) != 0, "multisampled targets rasterized without MSAA_ENABLE are unsupported");
zero(cx, 0x293, ScanControlMask, "sample iteration, primitive discard or out-of-order rasterization");
zero(cx, 0x80, ~0u, "window offset");
zero(cx, 0x8d, ScreenOffsetMask, "reserved PA_SU_HARDWARE_SCREEN_OFFSET bits");
@ -669,6 +711,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
if (slotExport == 7 && ((read(cx, 0x31c + slot * 0xfu) >> 8u) & 7u) != 4u) throw std::runtime_error("AGC graphics: color export format 7 (UINT16_ABGR) into a target that is not unsigned integer is unsupported");
color.exportIndex = index;
color.uintExport = slotExport == 7;
Require(color.samples == result.samples, "a color target's sample count differs from the rasterizer's");
APS5_LOG_OUT_DEBUG("Color %u address=0x%llx extent=%ux%u bytes=%llu VkFormat=%u", slot, static_cast<unsigned long long>(color.address), color.extent.width, color.extent.height, static_cast<unsigned long long>(color.bytes), static_cast<unsigned>(color.format));
if (result.colors.empty()) {
result.renderExtent = color.extent;
@ -771,6 +814,8 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
if (mapping == 0xecu) {
const auto destinationAlpha = [](VkBlendFactor factor) { return factor == VK_BLEND_FACTOR_DST_ALPHA || factor == VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA || factor == VK_BLEND_FACTOR_SRC_ALPHA_SATURATE; };
if (destinationAlpha(state.srcColorBlendFactor) || destinationAlpha(state.dstColorBlendFactor) || destinationAlpha(state.srcAlphaBlendFactor) || destinationAlpha(state.dstAlphaBlendFactor)) throw std::runtime_error("AGC graphics: blending into an 8_8 color target with the alternate component order with a factor of the destination alpha, which the target keeps in its second channel, is not implemented");
const auto weighted = [](VkBlendOp operation) { return operation != VK_BLEND_OP_MIN && operation != VK_BLEND_OP_MAX; };
if ((weighted(state.colorBlendOp) && (constantColor(state.srcColorBlendFactor) || constantColor(state.dstColorBlendFactor))) || (weighted(state.alphaBlendOp) && (constantColor(state.srcAlphaBlendFactor) || constantColor(state.dstAlphaBlendFactor)))) throw std::runtime_error("AGC graphics: blending into an 8_8 color target with the alternate component order with a constant color factor, which Vulkan applies to the second channel with the green blend constant instead of the alpha one, is not implemented");
}
}
if (color.packing != ShaderRecompiler::ColorExportPacking::None) {
@ -849,17 +894,24 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
APS5_LOG_OUT_DEBUG("Color %u info=0x%x number=%u swap=%u", slot, info, number, swap);
const auto format = (info >> 2u) & 0x1fu;
const auto decoded = DecodeColorFormat(format, number, swap);
const auto attrib = read(cx, 0x31d + stride);
zero(cx, 0x31d + stride, ~0x3f000u, "color destination alpha override or other CB_COLOR_ATTRIB modes");
color.samples = 1u << ((attrib >> 12u) & 7u);
Require(color.samples <= 8u, "color targets with more than eight samples are unsupported");
Require((1u << ((attrib >> 15u) & 3u)) == color.samples, "color targets with fewer fragments than samples (EQAA) are unsupported");
const auto multisampleInfo = color.samples > 1 ? 0x0c006000u : 0u;
Require(color.samples == 1 || (info & 0x10000000u) == 0, "DCC over a multisampled color target is unsupported");
// ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written
if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")");
if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u | multisampleInfo)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")");
Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported");
const auto view = read(cx, 0x31b + stride);
Require((view & ~0x3fffffffu) == 0, "reserved CB_COLOR_VIEW bits are set");
const auto slice = view & 0x1fffu;
Require(slice == ((view >> 13u) & 0x1fffu), "color views of several array slices are unsupported");
const auto viewMip = (view >> 26u) & 0xfu;
zero(cx, 0x31d + stride, ~0x20000u, "color samples, fragments or destination alpha override");
const auto attrib2 = read(cx, 0x3b0 + slot);
const auto maxMip = attrib2 >> 28u;
Require(color.samples == 1 || maxMip == 0, "multisampled color mips are unsupported");
Require(viewMip <= maxMip, "color view mip exceeds the surface");
const auto attrib3 = read(cx, 0x3b8 + slot);
color.tileMode = DecodeColorTileMode(attrib3);
@ -872,6 +924,8 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
color.depthSlice = slice;
} else if ((attrib3 & 0x1fffu) != 0) {
Require(slice <= (attrib3 & 0x1fffu), "the color view slice is beyond the array surface");
color.arrayLastSlice = attrib3 & 0x1fffu;
color.arraySlice = slice;
}
color.extent = {((attrib2 >> 14u) & 0x3fffu) + 1u, (attrib2 & 0x3fffu) + 1u};
color.elementBytes = decoded.elementBytes;
@ -895,6 +949,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
color.pipeBankXor = static_cast<std::uint32_t>(color.surfaceAddress & (ColorTileModeBlockBytes(color.tileMode) - 1u));
color.surfaceAddress -= color.pipeBankXor;
}
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;
color.bytes = colorLayout.Bytes();
@ -908,15 +963,19 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
const auto clear = find(cx, 0x323 + word + stride);
color.clearWords[word] = clear == cx.end() ? 0u : clear->second;
}
if ((info & 0x2000u) != 0) {
Require(maxMip == 0 && !volume && slice == 0, "CMASK fast clears of a mipmapped, 3D or array color target are unsupported");
if ((info & 0x2000u) != 0 || (color.samples > 1 && (info & 0x4000u) != 0)) {
Require(color.samples > 1 || (maxMip == 0 && !volume && slice == 0), "CMASK fast clears of a mipmapped, 3D or array color target are unsupported");
Require(((attrib3 >> 19u) & 0x1fu) == 0x18u && (attrib3 & 0x4000000u) != 0, "CMASK fast clears need pipe-aligned SW_64KB_Z_X metadata (CB_COLOR_ATTRIB3 FMASK_SW_MODE 24, CMASK_PIPE_ALIGNED)");
Require(color.elementBytes <= 8, "CMASK fast clears of texels over 64 bits are unsupported");
if (color.samples == 1) Require(color.elementBytes <= 8, "CMASK fast clears of texels over 64 bits are unsupported");
const auto cmaskHigh = find(cx, 0x398 + slot);
color.cmaskAddress = ((cmaskHigh == cx.end() ? 0ull : static_cast<std::uint64_t>(cmaskHigh->second & 0xffu)) << 40u) | (static_cast<std::uint64_t>(read(cx, 0x31f + stride)) << 8u);
Require(color.cmaskAddress != 0, "CMASK fast clears without a CMASK address are unsupported");
color.cmaskBytes = CmaskBytes(color.extent.width, color.extent.height);
GuestMemory::CheckRange(reinterpret_cast<const void*>(color.cmaskAddress), color.cmaskBytes, CmaskLayout::Alignment, true);
const auto cmask = ((cmaskHigh == cx.end() ? 0ull : static_cast<std::uint64_t>(cmaskHigh->second & 0xffu)) << 40u) | (static_cast<std::uint64_t>(read(cx, 0x31f + stride)) << 8u);
Require((info & 0x2000u) == 0 || cmask != 0, "CMASK fast clears without a CMASK address are unsupported");
if (cmask != 0) {
color.cmaskAddress = cmask;
color.cmaskBytes = CmaskBytes(color.extent.width, color.extent.height);
if (color.samples == 1) GuestMemory::CheckRange(reinterpret_cast<const void*>(color.cmaskAddress), color.cmaskBytes, CmaskLayout::Alignment, true);
}
color.cmaskFastClear = (info & 0x2000u) != 0;
}
if ((info & 0x10000000u) != 0) {
if (maxMip == 0) {
@ -940,11 +999,14 @@ std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue
const auto control = find(cx, 0x202);
if (control == cx.end()) return std::nullopt;
const auto mode = (control->second >> 4u) & 7u;
if (mode != 2u && mode != 6u) return std::nullopt;
ColorMetadataPass pass{mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}};
const auto aaConfig = find(cx, 0x2f8);
const bool resolve = mode == 3u && aaConfig != cx.end() && (aaConfig->second & 7u) != 0;
if (mode != 2u && mode != 6u && !resolve) return std::nullopt;
ColorMetadataPass pass{resolve ? ColorMetadataPass::Mode::Resolve : mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}, std::nullopt};
Require((control->second & ~0x70u) == 0xcc0000u, "CB metadata pass with a nonstandard ROP, dual quads disabled or degamma");
Require((read(cx, 0x200) & 0xfu) == 0 && (read(cx, 0x0) & 0xfu) == 0, "CB metadata pass with depth or stencil work");
zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion");
const auto samples = resolve ? decodeSamples(cx) : 1u;
if (!resolve) zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion");
zero(cx, 0x80, ~0u, "window offset");
const auto viewportControl = read(cx, 0x206);
if (viewportControl != 0x43fu) throw std::runtime_error(vteMessage(viewportControl));
@ -957,16 +1019,32 @@ std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue
intersect(covered, cx, 0x81, false);
intersect(covered, cx, 0x90, false);
if ((read(cx, 0x292) & 2u) != 0) intersect(covered, cx, 0x94, false);
const auto targetMask = read(cx, 0x8e);
for (std::uint32_t slot = 0; slot < 8; ++slot) {
if (((targetMask >> (4u * slot)) & 0xfu) == 0 || ((read(cx, 0x31c + slot * 0xfu) >> 2u) & 0x1fu) == 0) continue;
const auto target = DecodeColorBuffer(cx, slot);
Require((read(cx, 0x31c + slot * 0xfu) & 0x10000000u) == 0 || target.dccAddress != 0, "CB metadata pass over a mipmapped DCC color target, whose keys are not modeled");
const auto requireCovered = [&](const ColorTarget& target) {
const auto width = static_cast<float>(target.extent.width);
const auto height = static_cast<float>(target.extent.height);
const bool viewportCovers = xo - xs <= 0.0f && xo + xs >= width && yo - ys <= 0.0f && yo + ys >= height;
const bool scissorCovers = covered.offset.x == 0 && covered.offset.y == 0 && covered.extent.width >= target.extent.width && covered.extent.height >= target.extent.height;
Require(viewportCovers && scissorCovers, "CB metadata pass over part of a color target");
};
if (resolve) {
const auto source = DecodeColorBuffer(cx, 0);
const auto destination = DecodeColorBuffer(cx, 1);
Require(source.samples == samples, "CB resolve from a target whose sample count differs from the rasterizer's");
Require(destination.samples == 1, "CB resolve into a multisampled target is unsupported");
Require(destination.cmaskAddress == 0, "CB resolve into a fast-clear color target is unsupported (the resolve does not update its CMASK)");
Require(source.format == destination.format && source.extent.width == destination.extent.width && source.extent.height == destination.extent.height, "CB resolve between different formats or extents is unsupported");
requireCovered(destination);
pass.source = source;
pass.targets.push_back(destination);
return pass;
}
const auto targetMask = read(cx, 0x8e);
for (std::uint32_t slot = 0; slot < 8; ++slot) {
if (((targetMask >> (4u * slot)) & 0xfu) == 0 || ((read(cx, 0x31c + slot * 0xfu) >> 2u) & 0x1fu) == 0) continue;
const auto target = DecodeColorBuffer(cx, slot);
Require(target.samples == 1, "CB metadata pass over a multisampled color target, whose texels are on the host, is unsupported (only a CB resolve is modeled)");
Require((read(cx, 0x31c + slot * 0xfu) & 0x10000000u) == 0 || target.dccAddress != 0, "CB metadata pass over a mipmapped DCC color target, whose keys are not modeled");
requireCovered(target);
pass.targets.push_back(target);
}
return pass;
@ -1008,8 +1086,9 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
std::uint32_t z = 0, view = 0;
if ((value(cx, 0x010, z) && (z & 3u) == 0) || (value(cx, 0x002, view) && (view & 0x01000000u) != 0)) return require(false, "depth clear requires a writable depth plane");
}
const bool surface = ((word & 0xbu) != 0 || depthClear || stencilClear) && depthSurfaceBound(cx);
if (!surface && !((word & 0xbu) != 0 && depthPlanesAbsent(cx)) && !depthPassThrough(word) && !IgnoreDepthTest() && (effectiveDepthControl(word) & DepthControlMask) != 0) return zeroMessage(0x200, word, "depth, stencil or conditional color writes");
const auto tests = planeTests(cx, word);
const bool surface = (tests != 0 || depthClear || stencilClear) && depthSurfaceBound(cx);
if (!surface && !((word & 0xbu) != 0 && (tests == 0 || depthPlanesAbsent(cx))) && !depthPassThrough(word) && !IgnoreDepthTest() && (effectiveDepthControl(word) & DepthControlMask) != 0) return zeroMessage(0x200, word, "depth, stencil or conditional color writes");
if (auto reason = require((word & 8u) == 0 || surface || depthPlanesAbsent(cx), "depth bounds without a depth surface"); !reason.empty()) return reason;
if (auto reason = require((word & 0xc0000000u) == 0, "depth-conditional color writes are unsupported"); !reason.empty()) return reason;
}
@ -1019,8 +1098,9 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
if (value(cx, 0x2dc, word) && word != AlphaToCoverageUndithered) {
if (auto reason = nonzero(cx, 0x2dc, AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); !reason.empty()) return reason;
}
if (auto reason = nonzero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); !reason.empty()) return reason;
if (auto reason = nonzero(cx, 0x292, ScanModeMask, "scan conversion mode"); !reason.empty()) return reason;
if (auto reason = nonzero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling"); !reason.empty()) return reason;
const bool multisampled = value(cx, 0x2f8, word) && (word & 7u) != 0;
if (auto reason = nonzero(cx, 0x292, multisampled ? ScanModeMask & ~1u : ScanModeMask, "scan conversion mode"); !reason.empty()) return reason;
if (auto reason = nonzero(cx, 0x293, ScanControlMask, "sample iteration, primitive discard or out-of-order rasterization"); !reason.empty()) return reason;
if (auto reason = nonzero(cx, 0x80, ~0u, "window offset"); !reason.empty()) return reason;
if (auto reason = nonzero(cx, 0x8d, ScreenOffsetMask, "reserved PA_SU_HARDWARE_SCREEN_OFFSET bits"); !reason.empty()) return reason;

View File

@ -866,9 +866,29 @@ VkFormat AttachmentProxyFormat(const Context& context, VkFormat format) {
return proxy;
}
StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel) : context(context), detiler(detiler), descriptor(descriptor) {
std::shared_ptr<StorageTexture> StorageTexture::Create(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel) {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
PhaseTimer timer;
auto texture = std::shared_ptr<StorageTexture>(new StorageTexture(context, detiler, descriptor, mipLevel));
uploadReason = "first";
texture->upload();
texture->defaultMip = mipLevel;
texture->view = texture->createView(mipLevel, false, texture->storageFormat);
{
auto& live = Live();
std::lock_guard lock(live.mutex);
live.textures.push_back(texture.get());
}
if (profile) {
auto& totals = Profile();
totals.storageCreate += timer.lap();
totals.storageBytes += texture->guestBytes;
if (++totals.storageCount % 50 == 0) std::fprintf(stderr, "[texture] %llu storage images (%.0f MiB): create %.0f ms, write-back %.0f ms (alloc %.0f, host copy %.0f, gpu %.0f, store %.0f), %llu reused, %llu direct uploads, %llu direct write-backs, clears %llu recorded / %llu waited\n", static_cast<unsigned long long>(totals.storageCount), totals.storageBytes / 1048576.0, totals.storageCreate, totals.storageWriteBack, totals.storageAlloc, totals.storageHostCopy, totals.storageGpu, totals.storageStore, static_cast<unsigned long long>(totals.storageReused), static_cast<unsigned long long>(totals.storageDirectUploads), static_cast<unsigned long long>(totals.storageDirectWriteBacks), static_cast<unsigned long long>(totals.storageRecordedClears), static_cast<unsigned long long>(totals.storageWaitedClears));
}
return texture;
}
StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel) : context(context), detiler(detiler), descriptor(descriptor) {
try {
Require(!IsBlockCompressed(descriptor.format), "block-compressed textures cannot be storage images");
Require(mipLevel < descriptor.mipCount, "storage texture mip level is outside the texture");
@ -909,7 +929,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler,
{
VkFormatProperties properties{};
context.formatProperties(context.physical, vkFormat, &properties);
attachable = (descriptor.dimension == TextureDimension::k2D || descriptor.dimension == TextureDimension::k3D) && (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
attachable = (descriptor.dimension == TextureDimension::k2D || descriptor.dimension == TextureDimension::k3D || descriptor.dimension == TextureDimension::k2DArray) && (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
if (attachable) imageInfo.usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
}
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
@ -923,21 +943,6 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler,
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory storage texture");
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory storage");
uploadReason = "first";
upload();
defaultMip = mipLevel;
view = createView(mipLevel, false, storageFormat);
{
auto& live = Live();
std::lock_guard lock(live.mutex);
live.textures.push_back(this);
}
if (profile) {
auto& totals = Profile();
totals.storageCreate += timer.lap();
totals.storageBytes += guestBytes;
if (++totals.storageCount % 50 == 0) std::fprintf(stderr, "[texture] %llu storage images (%.0f MiB): create %.0f ms, write-back %.0f ms (alloc %.0f, host copy %.0f, gpu %.0f, store %.0f), %llu reused, %llu direct uploads, %llu direct write-backs, clears %llu recorded / %llu waited\n", static_cast<unsigned long long>(totals.storageCount), totals.storageBytes / 1048576.0, totals.storageCreate, totals.storageWriteBack, totals.storageAlloc, totals.storageHostCopy, totals.storageGpu, totals.storageStore, static_cast<unsigned long long>(totals.storageReused), static_cast<unsigned long long>(totals.storageDirectUploads), static_cast<unsigned long long>(totals.storageDirectWriteBacks), static_cast<unsigned long long>(totals.storageRecordedClears), static_cast<unsigned long long>(totals.storageWaitedClears));
}
} catch (...) {
release();
throw;
@ -1265,6 +1270,10 @@ bool ClearKeepsDenormals(const Context& context, VkFormat format) {
return keeps;
}
bool ClearColorForTexel(VkFormat format, std::uint32_t elementBytes, std::span<const std::uint32_t, 4> pattern, VkClearColorValue& clear) {
return FillClearColor(format, elementBytes, pattern, clear);
}
VkImageView StorageTexture::createView(std::uint32_t mip, bool firstLayer, VkFormat format) const {
Require(mip < descriptor.mipCount, "storage texture mip level is outside the texture");
Require(!firstLayer || descriptor.dimension == TextureDimension::k2DArray, "a first-layer storage view needs a 2D array surface");
@ -1317,12 +1326,13 @@ VkImageView StorageTexture::elementLayerView(std::uint32_t level, std::uint32_t
return created;
}
VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, std::uint32_t depthSlice) {
VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, std::uint32_t depthSlice, std::uint32_t layer) {
Require(attachable, "storage image cannot be a color attachment");
Require(mip < descriptor.mipCount, "attachment mip exceeds the storage image");
const bool volume = descriptor.dimension == TextureDimension::k3D;
Require(depthSlice == 0 || (volume && mip == 0 && depthSlice <= descriptor.depthOrLastArray), "attachment slice is outside the storage image");
const auto found = attachmentViews.find({format, mip, depthSlice});
Require(layer == 0 || (!volume && descriptor.dimension == TextureDimension::k2DArray && descriptor.baseArray + layer <= descriptor.depthOrLastArray), "attachment layer is outside the storage image");
const auto found = attachmentViews.find({format, mip, depthSlice, layer});
if (found != attachmentViews.end()) return found->second;
VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO};
usage.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
@ -1332,10 +1342,10 @@ VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, s
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = format;
viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY};
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 1u, volume ? depthSlice : descriptor.baseArray, 1u};
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 1u, volume ? depthSlice : descriptor.baseArray + layer, 1u};
VkImageView created = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &created), "vkCreateImageView attachment");
attachmentViews.emplace(std::tuple{format, mip, depthSlice}, created);
attachmentViews.emplace(std::tuple{format, mip, depthSlice, layer}, created);
return created;
}
@ -2970,6 +2980,12 @@ void StorageTexture::FlushAllPending(const char* reason) {
static_cast<void>(FlushPending(0, std::numeric_limits<std::size_t>::max(), nullptr, reason));
}
bool StorageTexture::AnyPending() {
auto& pending = Pending();
std::lock_guard lock(pending.mutex);
return pending.textures.begin() != pending.textures.end();
}
bool StorageTexture::StoreAtFlipRequested(const char* value) {
if (value == nullptr) return false;
if (std::strcmp(value, "1") == 0) return true;

View File

@ -162,10 +162,12 @@ int main(int argc, char** argv) {
buffer.Bytes()[255] = std::byte{0x5a};
Require(buffer.Bytes()[255] == std::byte{0x5a}, "real BDA buffer mapping failed");
if (device->RunsOnCpu()) {
std::cout << "CPU Vulkan device: BDA execution not tested\n";
std::cout << "CPU Vulkan device: BDA read and multi-dword write execution not tested\n";
} else {
RunBdaExecutionTests(device->GetContext());
}
RunBdaStoreExecutionTests(device->GetContext());
std::cout << "BDA store boundary tests passed\n";
RunColorTransferTests(device->GetContext());
std::cout << "Vulkan BDA allocation and execution tests passed\n";
return 0;

View File

@ -1,10 +1,13 @@
#include "BdaShader.hpp"
#include "BdaAbi.hpp"
#include "IntermediateRepresentation/IrMetadata/ShaderStage.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include <algorithm>
#include <array>
#include <cstring>
#include <initializer_list>
#include <limits>
#include <set>
#include <string>
#include <vector>
@ -330,3 +333,73 @@ void RunBdaExecutionTests(const Context& context) {
run(guest, 8, 0, Abi::FaultReason::Permission);
RunBdaDwordWriteTests(context);
}
void RunBdaStoreExecutionTests(const Context& context) {
namespace Abi = ShaderRecompiler::BdaAbi;
Buffer first(context, 2, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
Buffer second(context, 3, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
Buffer table(context, sizeof(Abi::Header) + 2 * sizeof(Abi::Range), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
Buffer fault(context, Abi::FaultBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
Buffer output(context, 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
constexpr std::uint64_t guest = 0x7fff1235fffdULL;
constexpr std::uint32_t writable = Abi::Read | Abi::Write;
const Abi::Header header{Abi::Version, 2, sizeof(Abi::Range), 0};
std::array<Abi::Range, 2> ranges{};
const auto run = [&](const char* name, std::uint64_t address, std::uint32_t bits, bool coherent, const std::array<std::uint8_t, 5>& expected, std::initializer_list<std::uint64_t> writtenAddresses, Abi::FaultReason reason = {}, std::uint64_t faultAddress = 0, std::uint32_t faultBytes = 0) {
const std::string label = std::string("BDA store ") + name + (coherent ? " coherent: " : ": ");
std::memset(first.Bytes().data(), 0xcd, first.Bytes().size());
std::memset(second.Bytes().data(), 0xcd, second.Bytes().size());
std::memcpy(table.Bytes().data(), &header, sizeof(header));
std::memcpy(table.Bytes().data() + sizeof(header), ranges.data(), sizeof(ranges));
std::memset(fault.Bytes().data(), 0, fault.Bytes().size());
std::memset(output.Bytes().data(), 0, output.Bytes().size());
Pipeline pipeline(context, MakeBdaStoreTestShader(address, 0xa1b2c3d4u, bits, coherent), {&table, &fault, &output});
pipeline.Run(1);
std::array<std::uint8_t, 5> actual{};
std::memcpy(actual.data(), first.Bytes().data(), 2);
std::memcpy(actual.data() + 2, second.Bytes().data(), 3);
Require(actual == expected, label + "incorrect stored bytes or writes outside the mapped span");
Abi::Fault report{};
std::memcpy(&report, fault.Bytes().data(), sizeof(report));
if (reason == Abi::FaultReason{}) {
Require(report.state == Abi::FaultState::Empty && report.reason == Abi::FaultReason{} && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.reserved == 0, label + "mapped store published a fault");
} else {
Require(report.state == Abi::FaultState::Ready && report.reason == reason && report.address == faultAddress && report.bytes == faultBytes && report.stage == static_cast<std::uint32_t>(ShaderRecompiler::IrShaderStage::Compute) && report.instruction == 0x1234 && report.reserved == 0, label + "incorrect first fault metadata");
}
std::array<std::uint32_t, Abi::FaultBufferBytes / sizeof(std::uint32_t)> words{};
std::memcpy(words.data(), fault.Bytes().data(), sizeof(words));
Require(words[Abi::WrittenOverflowWord] == 0, label + "written-page table overflowed");
std::set<std::uint64_t> recordedPages;
Require(Abi::ForEachWrittenRange(words.data(), [&](std::uint64_t address, std::size_t bytes) {
for (std::size_t offset = 0; offset < bytes; offset += std::size_t{1} << Abi::WrittenPageShift) {
Require(recordedPages.insert((address + offset) >> Abi::WrittenPageShift).second, label + "written-page table contains duplicate pages");
}
}), label + "invalid written-page record");
std::set<std::uint64_t> expectedPages;
std::set<std::uint32_t> expectedChunks;
for (const auto writtenAddress : writtenAddresses) {
expectedPages.insert(writtenAddress >> Abi::WrittenPageShift);
expectedChunks.insert(Abi::WrittenChunkKey(writtenAddress));
}
Require(recordedPages == expectedPages, label + "incorrect written-page set");
Require(words[Abi::WrittenCountWord] == expectedChunks.size(), label + "incorrect written-chunk claim count");
std::uint32_t completed = 0;
std::memcpy(&completed, output.Bytes().data(), sizeof(completed));
Require(completed == 1, label + "store did not finish its per-byte accesses");
};
for (const bool coherent : {false, true}) {
ranges = {{{guest, guest + 2, first.DeviceAddress(), writable, 0}, {guest + 2, guest + 5, second.DeviceAddress(), writable, 0}}};
run("byte", guest + 1, 8, coherent, {0xcd, 0xd4, 0xcd, 0xcd, 0xcd}, {guest + 1});
run("split short", guest + 1, 16, coherent, {0xcd, 0xd4, 0xc3, 0xcd, 0xcd}, {guest + 1, guest + 2});
run("split unaligned dword", guest, 32, coherent, {0xd4, 0xc3, 0xb2, 0xa1, 0xcd}, {guest, guest + 3});
ranges[1].permissions = Abi::Read;
run("read-only byte", guest + 2, 8, coherent, {0xcd, 0xcd, 0xcd, 0xcd, 0xcd}, {}, Abi::FaultReason::Permission, guest + 2, 1);
run("read-only short suffix", guest + 1, 16, coherent, {0xcd, 0xd4, 0xcd, 0xcd, 0xcd}, {guest + 1}, Abi::FaultReason::Permission, guest + 2, 1);
run("read-only suffix", guest, 32, coherent, {0xd4, 0xc3, 0xcd, 0xcd, 0xcd}, {guest}, Abi::FaultReason::Permission, guest + 2, 1);
ranges[0].end = guest + 1;
ranges[1].permissions = writable;
run("unmapped middle byte", guest, 32, coherent, {0xd4, 0xcd, 0xb2, 0xa1, 0xcd}, {guest, guest + 3}, Abi::FaultReason::Unmapped, guest + 1, 1);
ranges = {{{guest + 3, guest + 5, first.DeviceAddress(), writable, 0}, {guest + 5, guest + 8, second.DeviceAddress(), writable, 0}}};
run("split aligned dword", guest + 3, 32, coherent, {0xcd, 0xcd, 0xcd, 0xcd, 0xcd}, {}, Abi::FaultReason::Unmapped, guest + 3, 4);
}
}

View File

@ -746,6 +746,42 @@ void sparseHeapTests(const Context& context, const BdaTestAccess& access) {
releaseSparse(block, reserved);
}
void hostWriteWatchExclusionTests() {
#ifdef _WIN32
if (AgcDriver::GuestMemory::WriteWatched()) {
std::uintptr_t arena = 0;
std::size_t arenaBytes = 0;
GuestArena::GuestArenaRange_nid_postfix(&arena, &arenaBytes);
bool found = false;
for (auto cursor = arena; cursor < arena + arenaBytes;) {
MEMORY_BASIC_INFORMATION info{};
Require(VirtualQuery(reinterpret_cast<void*>(cursor), &info, sizeof(info)) == sizeof(info), "cannot enumerate arena host holes");
if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(cursor, 1)) {
Require(!AgcDriver::GuestMemory::Watched(cursor, 1), "an arena host hole was reported as write-watched");
Require(AgcDriver::GuestMemory::CollectWritesUncached(cursor, 1) == 0, "an arena host hole returned a write generation");
Require(!AgcDriver::GuestMemory::UnchangedSince(cursor, 1, 1), "an arena host hole passed an unchanged proof");
const std::array queries{AgcDriver::GuestMemory::UnchangedQuery{cursor, 1, 1}};
Require(!AgcDriver::GuestMemory::UnchangedSinceAll(queries), "an arena host hole passed a bulk unchanged proof");
found = true;
break;
}
const auto next = reinterpret_cast<std::uintptr_t>(info.BaseAddress) + info.RegionSize;
Require(next > cursor, "arena enumeration did not advance");
cursor = next;
}
Require(found, "write-watched arena contained no host exclusion to test");
}
std::vector<std::uint8_t> host(64, 0x31);
const auto address = reinterpret_cast<std::uintptr_t>(host.data());
Require(!AgcDriver::GuestMemory::Watched(address, host.size()), "ordinary host heap memory was reported as write-watched");
Require(AgcDriver::GuestMemory::CollectWritesUncached(address, host.size()) == 0, "ordinary host heap memory returned a guest write generation");
Require(!AgcDriver::GuestMemory::UnchangedSince(address, host.size(), 1), "ordinary host heap memory was proven unchanged");
const std::array queries{AgcDriver::GuestMemory::UnchangedQuery{address, host.size(), 1}};
Require(!AgcDriver::GuestMemory::UnchangedSinceAll(queries), "a host heap range passed a bulk unchanged proof");
Require(std::all_of(host.begin(), host.end(), [](std::uint8_t value) { return value == 0x31; }), "write-watch eligibility changed host bytes");
#endif
}
void sparseHeapMirrorTests(const Context& context, const BdaTestAccess& access) {
#ifdef _WIN32
if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) {
@ -1077,6 +1113,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
gpuMappingTests(context);
stridedOverhangTests(context, access);
sparseHeapTests(context, access);
hostWriteWatchExclusionTests();
sparseHeapMirrorTests(context, access);
Require(AddressCopyOverflow({{0x1000, 0x3000, 0x2000, "uncommitted pages"}}, 0x2000).empty(), "copies within the limit were refused");
const auto copies = AddressCopyOverflow({{0x1000, 0x2000, 0x1000, "not mirrored"}, {0x10000, 0x30000, 0x18000, "uncommitted pages"}}, 0x2000);

View File

@ -3,6 +3,9 @@
#include "SpirvBackend/SpirvMemory/SpirvTypes.hpp"
#include "SpirvBackend/SpirvMemory/SpirvConstants.hpp"
#include "IntermediateRepresentation/IrBuilder.hpp"
#if ANYPS5_ENABLE_SPIRV_TOOLS
#include "SpirvBackend/SpirvOptimizer.hpp"
#endif
namespace {
@ -40,7 +43,7 @@ std::vector<std::uint32_t> MakeShader(std::uint64_t address, bool coherent, bool
state.module.AddFunction(spv::OpFunction, TypeVoid(state), main, spv::FunctionControlMaskNone, TypeFunction(state));
EmitLabel(state, state.module.AllocateId());
SpirvValueEmitContext ctx(state);
auto& instruction = extracted != 0u ? program.CreateValue(IrOpcode::LoadAddressU32x4, IrType::U32x4) : program.CreateValue(IrOpcode::LoadAddressU32, IrType::U32);
auto& instruction = writes ? program.CreateValue(IrOpcode::StoreAddressU32, IrType::Void) : extracted != 0u ? program.CreateValue(IrOpcode::LoadAddressU32x4, IrType::U32x4) : program.CreateValue(IrOpcode::LoadAddressU32, IrType::U32);
IrBuilder ir(program);
for (std::uint32_t dword = 0; dword < 4u; ++dword) {
if ((extracted & (1u << dword)) == 0u) continue;
@ -106,3 +109,15 @@ std::vector<std::uint32_t> MakeBdaDwordReadTestShader(std::uint64_t address, std
return std::vector<std::uint32_t>(values.begin(), values.begin() + dwords);
});
}
std::vector<std::uint32_t> MakeBdaStoreTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent) {
using namespace ShaderRecompiler;
auto shader = MakeShader(address, coherent, true, [&](SpirvValueEmitContext& ctx, const IrValue& instruction, std::uint32_t base) {
EmitBdaStore(ctx, instruction, base, ConstantU32(ctx.state, value), bits);
return std::vector<std::uint32_t>{ConstantU32(ctx.state, 1u)};
}, 0u, true);
#if ANYPS5_ENABLE_SPIRV_TOOLS
shader = ValidateAndOptimizeSpirv(shader, VK_API_VERSION_1_1, shader[1], false, false);
#endif
return shader;
}

View File

@ -9,6 +9,8 @@ std::vector<std::uint32_t> MakeBdaTestShader(std::uint64_t address, std::uint32_
std::vector<std::uint32_t> MakeBdaDwordReadTestShader(std::uint64_t address, std::uint32_t dwords, bool coherent, bool stops);
std::vector<std::uint32_t> MakeBdaSpanReadTestShader(std::uint64_t address, std::uint32_t offset, std::uint32_t extracted, bool coherent, bool stops);
std::vector<std::uint32_t> MakeBdaDwordWriteTestShader(std::uint64_t address, std::uint32_t dwords, const std::uint32_t* values);
std::vector<std::uint32_t> MakeBdaStoreTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent);
void RunBdaStoreExecutionTests(const AgcDriver::Graphics::Context& context);
void RunBdaExecutionTests(const AgcDriver::Graphics::Context& context);
void RunBdaContractTests();

View File

@ -4,6 +4,7 @@
#include <cstdint>
#include <cstring>
#include <cstdio>
#include <deque>
#include <map>
#include <memory>
#include <set>
@ -37,6 +38,11 @@ struct MockDevice {
VkDeviceSize liveBytes = 0;
std::map<VkDeviceMemory, VkDeviceSize> memoryBytes;
std::map<VkBuffer, std::pair<VkDeviceMemory, VkDeviceSize>> bound;
std::set<VkBuffer> liveBuffers;
std::deque<VkResult> results;
VkDeviceSize limit = 0;
VkResult pressureResult = VK_ERROR_OUT_OF_DEVICE_MEMORY;
std::uint64_t attempts = 0;
};
MockDevice mock;
@ -45,6 +51,7 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreateBuffer(VkDevice, const VkBufferCreateIn
*buffer = reinterpret_cast<VkBuffer>(static_cast<std::uintptr_t>(mock.next++));
mock.sizes[*buffer] = info->size;
mock.usages[*buffer] = info->usage;
mock.liveBuffers.insert(*buffer);
return VK_SUCCESS;
}
@ -53,6 +60,17 @@ VKAPI_ATTR void VKAPI_CALL mockGetBufferMemoryRequirements(VkDevice, VkBuffer bu
}
VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks*, VkDeviceMemory* memory) {
++mock.attempts;
auto result = VK_SUCCESS;
if (!mock.results.empty()) {
result = mock.results.front();
mock.results.pop_front();
}
if (result == VK_SUCCESS && mock.limit != 0 && mock.liveBytes + info->allocationSize > mock.limit) result = mock.pressureResult;
if (result != VK_SUCCESS) {
*memory = reinterpret_cast<VkDeviceMemory>(std::uintptr_t{0xdeadbeef});
return result;
}
*memory = reinterpret_cast<VkDeviceMemory>(static_cast<std::uintptr_t>(mock.next++));
if (info->memoryTypeIndex == 1) mock.hostMemory[*memory].resize(info->allocationSize);
mock.memoryBytes[*memory] = info->allocationSize;
@ -74,11 +92,16 @@ VKAPI_ATTR VkResult VKAPI_CALL mockMapMemory(VkDevice, VkDeviceMemory memory, Vk
VKAPI_ATTR void VKAPI_CALL mockUnmapMemory(VkDevice, VkDeviceMemory) {}
VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice, VkBuffer, const VkAllocationCallbacks*) {
VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice, VkBuffer buffer, const VkAllocationCallbacks*) {
Expect(mock.liveBuffers.erase(buffer) == 1, "an unknown buffer was destroyed");
++mock.destroyedBuffers;
}
VKAPI_ATTR void VKAPI_CALL mockFreeMemory(VkDevice, VkDeviceMemory memory, const VkAllocationCallbacks*) {
if (!mock.memoryBytes.contains(memory)) {
Expect(false, "an undefined failed allocation output was freed");
return;
}
mock.liveBytes -= mock.memoryBytes.at(memory);
mock.memoryBytes.erase(memory);
mock.hostMemory.erase(memory);
@ -319,6 +342,149 @@ void Slabs() {
Expect(mock.liveBytes == 0, "slab blocks outlived their pool");
}
void AllocationRecovery(VkResult pressureResult, bool host) {
mock = MockDevice{};
{
auto context = mockContext();
const auto make = [&](std::size_t bytes) -> std::shared_ptr<void> {
if (host) return std::make_shared<Buffer>(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
return std::make_shared<DeviceBuffer>(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
};
auto active = make(2 * MiB);
const auto activeBuffer = *mock.liveBuffers.begin();
const auto activeMemory = mock.memoryBytes.begin()->first;
{ auto idle = make(2 * MiB); }
mock.limit = 6 * MiB;
mock.pressureResult = pressureResult;
const auto before = mock.attempts;
try {
auto recovered = make(4 * MiB);
Expect(mock.attempts == before + 2, "pool pressure did not trigger exactly one allocation retry");
Expect(mock.frees == 1 && mock.liveBytes == 6 * MiB, "recovery did not release the idle allocation");
Expect(mock.liveBuffers.contains(activeBuffer) && mock.memoryBytes.contains(activeMemory), "recovery destroyed an active allocation");
} catch (const std::exception& error) {
Expect(false, std::string("idle memory did not permit recovery: ") + error.what());
}
}
Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "recovery leaked a resource");
}
void SlabAllocationRecovery() {
mock = MockDevice{};
{
auto context = mockContext();
Buffer active(context, 300, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
const auto activeMemory = mock.bound.at(active.Handle()).first;
{ DeviceBuffer idle(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); }
mock.limit = 4 * MiB;
const auto before = mock.attempts;
try {
Buffer recovered(context, 600, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
Expect(mock.attempts == before + 2 && mock.frees == 1, "a new slab block did not retry after reclaiming an idle allocation");
Expect(context.bufferPool->SlabBlocks() == 2 && mock.memoryBytes.contains(activeMemory), "slab allocation recovery discarded its live block");
} catch (const std::exception& error) {
Expect(false, std::string("slab allocation did not recover: ") + error.what());
}
}
Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "slab recovery leaked a resource");
}
void ReclaimSlabs() {
mock = MockDevice{};
{
auto context = mockContext();
Buffer active(context, 300, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
const auto activeMemory = mock.bound.at(active.Handle()).first;
std::memset(active.Bytes().data(), 0x5a, active.Bytes().size());
{
Buffer sameBlock(context, 260, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
Buffer otherBlock(context, 600, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
}
mock.limit = 6 * MiB;
const auto before = mock.attempts;
try {
Buffer recovered(context, 4 * MiB, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
Expect(mock.attempts == before + 2 && mock.frees == 1, "reclaim did not free the unused slab block");
Expect(context.bufferPool->SlabBlocks() == 1 && mock.memoryBytes.contains(activeMemory), "reclaim freed a slab with a live slot");
Expect(std::all_of(active.Bytes().begin(), active.Bytes().end(), [](std::byte value) { return value == std::byte{0x5a}; }), "reclaim changed a live slab buffer");
} catch (const std::exception& error) {
Expect(false, std::string("empty slab did not permit recovery: ") + error.what());
}
}
Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "slab reclaim leaked a resource");
mock = MockDevice{};
{
auto context = mockContext();
BufferPool pool(context);
const auto idle = pool.TakeSlot(context, 1, 512 * 1024, false);
pool.PutSlot(idle.memory, idle.offset);
mock.limit = 4 * MiB;
const auto before = mock.attempts;
try {
const auto recovered = pool.TakeSlot(context, 1, 256 * 1024, false);
Expect(mock.attempts == before + 2 && mock.frees == 1 && pool.SlabBlocks() == 1, "a spare slab without cached buffers was not reclaimed");
pool.PutSlot(recovered.memory, recovered.offset);
} catch (const std::exception& error) {
Expect(false, std::string("spare slab did not permit recovery: ") + error.what());
}
}
Expect(mock.liveBytes == 0, "spare slab recovery leaked device memory");
}
void AllocationFailure(VkResult initial, VkResult final, bool cached) {
mock = MockDevice{};
{
auto context = mockContext();
if (cached) { DeviceBuffer idle(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); }
mock.results = {initial, final};
const auto before = mock.attempts;
std::string failure;
try {
DeviceBuffer buffer(context, 4 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
} catch (const std::exception& error) {
failure = error.what();
}
const bool reclaimed = cached && (initial == VK_ERROR_OUT_OF_DEVICE_MEMORY || initial == VK_ERROR_OUT_OF_HOST_MEMORY);
Expect(mock.attempts == before + (reclaimed ? 2 : 1), "allocation retries were not bounded or a non-memory failure was retried");
Expect(failure.find("Vulkan result " + std::to_string(reclaimed ? final : initial)) != std::string::npos, "allocation failure lost its final Vulkan result");
Expect(mock.liveBuffers.size() == (cached && !reclaimed ? 1u : 0u), "a failed allocation retained its buffer");
Expect(mock.liveBytes == (cached && !reclaimed ? 2 * MiB : 0), "a failed allocation mishandled cached memory");
}
Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "failure cleanup leaked a resource");
}
void SuccessfulAllocationKeepsPool() {
mock = MockDevice{};
{
auto context = mockContext();
{ DeviceBuffer idle(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); }
DeviceBuffer buffer(context, 4 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
Expect(mock.attempts == 2 && mock.frees == 0 && mock.liveBytes == 6 * MiB, "a successful first allocation reclaimed its cache");
}
Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "normal allocation leaked a resource");
}
void PartialSlabDoesNotRetry() {
mock = MockDevice{};
{
auto context = mockContext();
Buffer active(context, 300, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
{ Buffer idle(context, 260, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); }
mock.limit = 2 * MiB;
const auto before = mock.attempts;
std::string failure;
try {
Buffer buffer(context, 2 * MiB, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
} catch (const std::exception& error) {
failure = error.what();
}
Expect(failure.find("Vulkan result -2") != std::string::npos, "a live slab's memory was treated as reclaimable");
Expect(mock.attempts == before + 1 && mock.frees == 0, "an allocation retried without freeing device memory");
Expect(mock.liveBuffers.size() == 1 && mock.liveBuffers.contains(active.Handle()) && context.bufferPool->SlabBlocks() == 1, "reclaim changed a live slab allocation");
}
Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "partial slab failure leaked a resource");
}
}
int main() {
@ -330,6 +496,18 @@ int main() {
AddressAndHostUnchanged();
SmallDeviceClasses();
Slabs();
for (const auto result : {VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_OUT_OF_HOST_MEMORY}) {
AllocationRecovery(result, false);
AllocationRecovery(result, true);
AllocationFailure(result, result, true);
AllocationFailure(result, result, false);
}
SlabAllocationRecovery();
ReclaimSlabs();
AllocationFailure(VK_ERROR_DEVICE_LOST, VK_ERROR_DEVICE_LOST, true);
AllocationFailure(VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_DEVICE_LOST, true);
SuccessfulAllocationKeepsPool();
PartialSlabDoesNotRetry();
} catch (const std::exception& error) {
std::fprintf(stderr, "FAIL: %s\n", error.what());
return 1;

View File

@ -0,0 +1,172 @@
#include "../tools/ComputePipelineFixture.hpp"
#include <chrono>
#include <cstdio>
#include <fstream>
#include <stdexcept>
namespace {
void Require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
template<typename TAction>
void Reject(TAction action, const char* message) {
bool rejected = false;
try {
action();
} catch (const std::exception&) {
rejected = true;
}
Require(rejected, message);
}
class Directory {
public:
Directory() {
path = std::filesystem::temp_directory_path() / ("anyps5-compute-capture-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
Require(std::filesystem::create_directory(path), "could not create test directory");
}
~Directory() {
std::error_code error;
std::filesystem::remove_all(path, error);
}
std::filesystem::path path;
};
void CheckRoundTrip() {
auto capture = AgcDriver::ComputePipelineFixture();
capture.deviceName = "test GPU \"name\"";
capture.vendorId = 0x10de;
capture.deviceId = 0x2b85;
capture.driverVersion = 0x12345678;
capture.apiVersion = VK_API_VERSION_1_3;
capture.pipelineCacheUuid[0] = 0x12;
capture.pipelineCacheUuid[15] = 0xab;
capture.requiredSubgroupSize = 32;
capture.flags = VK_PIPELINE_CREATE_DISPATCH_BASE_BIT;
capture.sets = {{VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT,
{{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 5, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT},
{7, VK_DESCRIPTOR_TYPE_SAMPLER, 2, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}};
capture.pushConstants = {{VK_SHADER_STAGE_COMPUTE_BIT, 16, 32}};
const auto encoded = capture.Encode();
const auto decoded = AgcDriver::ComputePipelineCapture::Decode(encoded);
Require(decoded.Encode() == encoded, "capture round trip changed pipeline inputs");
Require(decoded.Identity() == capture.Identity(), "capture identity changed after decoding");
auto changed = decoded;
++changed.specialization[0].value;
Require(changed.Identity() != capture.Identity(), "specialization did not affect capture identity");
changed = decoded;
++changed.sets[0].bindings[0].count;
Require(changed.Identity() != capture.Identity(), "descriptor layout did not affect capture identity");
changed = decoded;
changed.requiredSubgroupSize = 64;
Require(changed.Identity() != capture.Identity(), "subgroup size did not affect capture identity");
Directory directory;
const auto file = capture.Write(directory.path);
Require(AgcDriver::ComputePipelineCapture::Read(file).Encode() == encoded, "capture file changed pipeline inputs");
Require(capture.Write(directory.path) == file, "identical capture was not deduplicated");
std::ofstream(file, std::ios::binary | std::ios::trunc).put('x');
Reject([&] { capture.Write(directory.path); }, "existing damaged capture was overwritten");
Require(std::filesystem::file_size(file) == 1, "damaged capture was changed");
}
void CheckLargeRecord() {
auto capture = AgcDriver::ComputePipelineFixture();
constexpr std::size_t words = 5500000;
capture.spirv.insert(capture.spirv.end() - 2, words - capture.spirv.size(), 0x00010000u);
const auto encoded = capture.Encode();
Require(encoded.size() > 20u * 1024u * 1024u, "large fixture did not exceed the former file bound");
Require(AgcDriver::ComputePipelineCapture::Decode(encoded).spirv == capture.spirv, "large module changed during decoding");
Directory directory;
const auto file = capture.Write(directory.path);
Require(AgcDriver::ComputePipelineCapture::Read(file).spirv == capture.spirv, "large module changed on disk");
capture.spirv.resize(AgcDriver::ComputePipelineCapture::MaxSpirvWords + 1u, 0x00010000u);
Reject([&] { capture.Encode(); }, "module beyond the word bound accepted");
std::vector<std::byte> oversized(AgcDriver::ComputePipelineCapture::MaxFileBytes + 1u);
Reject([&] { AgcDriver::ComputePipelineCapture::Decode(oversized); }, "record beyond the file bound accepted");
const auto oversizedFile = directory.path / "oversized.aps5cp";
{
std::ofstream output(oversizedFile, std::ios::binary);
output.seekp(static_cast<std::streamoff>(AgcDriver::ComputePipelineCapture::MaxFileBytes));
output.put('x');
Require(output.good(), "cannot write oversized capture fixture");
}
Reject([&] { AgcDriver::ComputePipelineCapture::Read(oversizedFile); }, "file beyond the file bound accepted");
}
void CheckBindingFlags() {
auto capture = AgcDriver::ComputePipelineFixture();
capture.sets = {{0, {{7, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 4, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT},
{3, VK_DESCRIPTOR_TYPE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT}}}};
const auto encoded = capture.Encode();
Require(AgcDriver::ComputePipelineCapture::Decode(encoded).Encode() == encoded, "binding flags changed without update-after-bind");
auto changed = capture;
changed.sets[0].bindings[0].flags = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT;
Require(changed.Identity() != capture.Identity(), "binding flags did not affect capture identity");
changed = capture;
changed.sets[0].bindings[1].flags |= VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT;
Reject([&] { changed.Encode(); }, "non-highest variable descriptor binding accepted");
changed = capture;
changed.sets[0].bindings[0].flags |= VK_DESCRIPTOR_BINDING_UPDATE_UNUSED_WHILE_PENDING_BIT;
Reject([&] { changed.Encode(); }, "unsupported binding flags accepted");
capture.sets[0].flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT;
capture.sets[0].bindings[0].type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
capture.sets[0].bindings[0].flags |= VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT;
Require(AgcDriver::ComputePipelineCapture::Decode(capture.Encode()).Encode() == capture.Encode(), "combined binding flags changed");
}
void CheckMalformedRecords() {
const auto capture = AgcDriver::ComputePipelineFixture();
const auto encoded = capture.Encode();
for (std::size_t length = 0; length < encoded.size(); ++length) {
Reject([&] { AgcDriver::ComputePipelineCapture::Decode(std::span(encoded).first(length)); }, "truncated capture accepted");
}
auto bytes = encoded;
bytes.push_back(std::byte{0});
Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "trailing capture bytes accepted");
bytes = encoded;
bytes[0] ^= std::byte{1};
Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "invalid signature accepted");
bytes = encoded;
bytes[8] = std::byte{0xff};
Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "unknown capture version accepted");
bytes = encoded;
for (std::size_t index = 104; index < 108; ++index) bytes[index] = std::byte{0xff};
Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "unbounded device name accepted");
const auto invalid = [&](auto change) {
auto copy = capture;
change(copy);
Reject([&] { copy.Encode(); }, "unsupported pipeline input accepted");
};
invalid([](auto& copy) { copy.spirv[0] = 0; });
invalid([](auto& copy) { copy.spirv[5] = 0; });
invalid([](auto& copy) { copy.spirv[11] = 4; });
invalid([](auto& copy) { copy.spirv[13] = 0; });
invalid([](auto& copy) { copy.flags = VK_PIPELINE_CREATE_DERIVATIVE_BIT; });
invalid([](auto& copy) { copy.stageFlags = VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT; });
invalid([](auto& copy) { copy.requiredSubgroupSize = 3; });
invalid([](auto& copy) { copy.specialization.push_back(copy.specialization[0]); });
invalid([](auto& copy) { copy.pushConstants = {{VK_SHADER_STAGE_VERTEX_BIT, 0, 4}}; });
invalid([](auto& copy) { copy.pushConstants = {{VK_SHADER_STAGE_COMPUTE_BIT, 1, 4}}; });
invalid([](auto& copy) { copy.sets = {{0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 0, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}}; });
invalid([](auto& copy) { copy.sets = {{0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_VERTEX_BIT, 0}}}}; });
invalid([](auto& copy) { copy.sets = {{0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT}}}}; });
invalid([](auto& copy) { copy.sets = {{0, {{}, {}}}}; });
}
}
int main() {
try {
CheckRoundTrip();
CheckMalformedRecords();
CheckBindingFlags();
CheckLargeRecord();
std::puts("compute pipeline capture: round trip, 5500000-word module, binding flags, immutable writes and bounded inputs passed");
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "compute pipeline capture test: %s\n", error.what());
return 1;
}
}

View File

@ -0,0 +1,68 @@
foreach(required IN ITEMS FIXTURE REPLAY OUTPUT_ROOT)
if(NOT DEFINED ${required})
message(FATAL_ERROR "Missing ${required}")
endif()
endforeach()
string(RANDOM LENGTH 16 ALPHABET 0123456789abcdef runId)
set(runDirectory "${OUTPUT_ROOT}/compute-capture-${runId}")
set(captureDirectory "${runDirectory}/captures")
file(MAKE_DIRECTORY "${runDirectory}")
execute_process(
COMMAND "${CMAKE_COMMAND}" -E env "ANYPS5_NO_SHADER_CACHE=1" "APS5_PIPELINE_STATS=0" "APS5_DUMP_COMPUTE_PIPELINES=" "APS5_DUMP_COMPUTE_MIN_WORDS=0" "${FIXTURE}"
WORKING_DIRECTORY "${runDirectory}"
RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20
)
file(WRITE "${runDirectory}/normal.log" "${output}\n${error}")
if(result STREQUAL "77" AND NOT DEFINED ENV{ANYPS5_REQUIRE_VULKAN})
message(STATUS "compute pipeline capture run: skipped, no usable Vulkan device")
file(REMOVE_RECURSE "${runDirectory}")
return()
endif()
if(NOT result STREQUAL "0" OR NOT output MATCHES "branch past endpgm tests passed")
message(FATAL_ERROR "Normal compute fixture failed: ${result}\n${output}\n${error}")
endif()
execute_process(
COMMAND "${CMAKE_COMMAND}" -E env "ANYPS5_REQUIRE_VULKAN=1" "ANYPS5_NO_SHADER_CACHE=1" "APS5_PIPELINE_STATS=0" "APS5_DUMP_COMPUTE_PIPELINES=${captureDirectory}" "APS5_DUMP_COMPUTE_MIN_WORDS=0" "${FIXTURE}"
WORKING_DIRECTORY "${runDirectory}"
RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20
)
file(WRITE "${runDirectory}/capture.log" "${output}\n${error}")
if(NOT result STREQUAL "0" OR NOT output MATCHES "branch past endpgm tests passed")
message(FATAL_ERROR "Captured compute fixture failed: ${result}\n${output}\n${error}")
endif()
file(GLOB captures "${captureDirectory}/compute-*/capture.aps5cp")
list(LENGTH captures count)
if(count LESS 1 OR count GREATER 12)
message(FATAL_ERROR "Expected 1 to 12 compute captures, found ${count}")
endif()
foreach(capture IN LISTS captures)
get_filename_component(recordDirectory "${capture}" DIRECTORY)
get_filename_component(recordName "${recordDirectory}" NAME)
string(REPLACE "compute-" "" identity "${recordName}")
set(report "${runDirectory}/${recordName}.json")
execute_process(
COMMAND "${CMAKE_COMMAND}" -E env "APS5_PIPELINE_STATS=0" "APS5_DUMP_COMPUTE_PIPELINES=" "${REPLAY}" "${capture}" --output "${report}"
WORKING_DIRECTORY "${runDirectory}"
RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20
)
file(WRITE "${runDirectory}/${recordName}.log" "${output}\n${error}")
if(NOT result STREQUAL "0")
message(FATAL_ERROR "Captured pipeline replay failed: ${result}\n${output}\n${error}")
endif()
file(READ "${report}" reportJson)
string(JSON reportIdentity GET "${reportJson}" capture_identity)
string(JSON words GET "${reportJson}" spirv_words)
string(JSON sets GET "${reportJson}" descriptor_sets)
string(JSON ranges GET "${reportJson}" push_constant_ranges)
string(JSON dispatches GET "${reportJson}" dispatch_count)
string(JSON policy GET "${reportJson}" application_cache_policy)
string(JSON capturedDevice GET "${reportJson}" captured_device device_id)
string(JSON replayDevice GET "${reportJson}" replay_device device_id)
string(JSON capturedDriver GET "${reportJson}" captured_device driver_version_raw)
string(JSON replayDriver GET "${reportJson}" replay_device driver_version_raw)
if(NOT reportIdentity STREQUAL identity OR words LESS 54 OR sets LESS 1 OR ranges LESS 1 OR NOT dispatches EQUAL 0 OR NOT policy STREQUAL "fresh" OR NOT capturedDevice STREQUAL replayDevice OR NOT capturedDriver STREQUAL replayDriver)
message(FATAL_ERROR "Captured pipeline metadata mismatch: ${reportJson}")
endif()
endforeach()
message(STATUS "Normal and captured compute output verified; ${count} captured pipelines replayed without dispatch")
file(REMOVE_RECURSE "${runDirectory}")

View File

@ -0,0 +1,122 @@
#include "../tools/ComputePipelineFixture.hpp"
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <stdexcept>
#include <string_view>
namespace {
void Require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
void CheckResult(const AgcDriver::ComputePipelineReplayResult& result) {
Require(!result.deviceName.empty() && result.vendorId != 0 && result.apiVersion != 0, "replay did not report its device");
Require(result.cache.size() >= 32, "replay did not export a pipeline cache header");
for (const auto duration : {result.moduleMilliseconds, result.layoutMilliseconds, result.pipelineMilliseconds, result.cacheMilliseconds, result.totalMilliseconds}) {
Require(std::isfinite(duration) && duration >= 0, "replay reported an invalid duration");
}
Require(result.totalMilliseconds >= result.pipelineMilliseconds, "pipeline duration exceeds replay duration");
}
}
int main() {
try {
auto capture = AgcDriver::ComputePipelineFixture();
const auto encoded = capture.Encode();
const auto decoded = AgcDriver::ComputePipelineCapture::Decode(encoded);
std::unique_ptr<AgcDriver::VulkanDevice> device;
try {
device = std::make_unique<AgcDriver::VulkanDevice>(AgcDriver::VulkanDevice::CompileOnly{});
} catch (const std::exception& error) {
const std::string_view message(error.what());
const bool absent = (message.starts_with("Vulkan loader: ") && message != "Vulkan loader: vkGetInstanceProcAddr missing") || message == "Vulkan: no Vulkan 1.1 graphics and compute queue" || message == "vkCreateInstance: Vulkan result -9";
if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr || !absent) throw;
std::printf("skipped, no usable Vulkan device: %s\n", error.what());
return 77;
}
auto sampled1D = decoded;
sampled1D.spirv.insert(sampled1D.spirv.begin() + 7, {0x00020011u, 43u});
CheckResult(device->ReplayComputePipeline(sampled1D));
std::puts("compute pipeline replay: core Vulkan Sampled1D capability compiled");
for (const auto capability : {6u, 0xffffffffu}) {
auto unsupported = decoded;
unsupported.spirv.insert(unsupported.spirv.begin() + 7, {0x00020011u, capability});
bool refused = false;
try {
device->ReplayComputePipeline(unsupported);
} catch (const std::exception& error) {
Require(std::string_view(error.what()) == "Vulkan presentation: captured SPIR-V capability is not enabled", "unsupported capability failed outside the replay capability guard");
refused = true;
}
Require(refused, "non-Vulkan or unknown SPIR-V capability accepted");
}
std::puts("compute pipeline replay: non-Vulkan Kernel and unknown capabilities rejected");
const auto fresh = device->ReplayComputePipeline(decoded);
CheckResult(fresh);
Require(fresh.initialCacheBytes == 0, "fresh replay imported cache bytes");
const auto imported = device->ReplayComputePipeline(decoded, fresh.cache);
CheckResult(imported);
Require(imported.initialCacheBytes == fresh.cache.size(), "replay did not import the supplied cache");
Require(fresh.deviceId == imported.deviceId && fresh.driverVersion == imported.driverVersion && fresh.pipelineCacheUuid == imported.pipelineCacheUuid, "replay device identity changed");
Require(capture.Encode() == encoded, "replay modified the captured pipeline");
auto corrupt = fresh.cache;
corrupt[8] ^= std::byte{0xff};
bool rejected = false;
try {
device->ReplayComputePipeline(decoded, corrupt);
} catch (const std::exception&) {
rejected = true;
}
Require(rejected, "cache from another vendor was accepted");
corrupt = fresh.cache;
corrupt[16] ^= std::byte{0xff};
rejected = false;
try {
device->ReplayComputePipeline(decoded, corrupt);
} catch (const std::exception&) {
rejected = true;
}
Require(rejected, "cache with another pipeline UUID was accepted");
for (const auto flags : {VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT, VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT, static_cast<VkDescriptorBindingFlagBits>(VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT)}) {
auto indexed = decoded;
indexed.sets[0].bindings[0].flags = flags;
try {
CheckResult(device->ReplayComputePipeline(indexed));
std::printf("compute pipeline replay: descriptor binding flags 0x%x compiled without update-after-bind\n", static_cast<unsigned>(flags));
} catch (const std::exception& error) {
if (std::getenv("ANYPS5_REQUIRE_DESCRIPTOR_BINDING_FLAGS") != nullptr) throw;
const std::string_view message(error.what());
Require(message == "Vulkan presentation: captured partially-bound descriptors are unsupported" || message == "Vulkan presentation: captured variable descriptor count is unsupported", "binding flag replay failed without an explicit feature refusal");
std::printf("compute pipeline replay: descriptor binding flags 0x%x explicitly refused: %s\n", static_cast<unsigned>(flags), error.what());
}
}
auto runtimeArray = decoded;
runtimeArray.spirv.insert(runtimeArray.spirv.begin() + 7, {0x00020011u, 5302u, 0x0008000au, 0x5f565053u, 0x5f545845u, 0x63736564u, 0x74706972u, 0x695f726fu, 0x7865646eu, 0x00676e69u});
try {
CheckResult(device->ReplayComputePipeline(runtimeArray));
std::puts("compute pipeline replay: runtime descriptor array capability compiled");
} catch (const std::exception& error) {
if (std::getenv("ANYPS5_REQUIRE_DESCRIPTOR_BINDING_FLAGS") != nullptr) throw;
Require(std::string_view(error.what()) == "Vulkan presentation: captured SPIR-V capability is not enabled", "runtime descriptor array replay failed without an explicit feature refusal");
std::puts("compute pipeline replay: device explicitly refused runtime descriptor array capability");
}
capture.requiredSubgroupSize = 32;
try {
const auto subgroup = device->ReplayComputePipeline(capture);
CheckResult(subgroup);
std::puts("compute pipeline replay: required subgroup size 32 compiled");
} catch (const std::exception& error) {
Require(std::string_view(error.what()).find("captured required subgroup size is unsupported") != std::string_view::npos, "required subgroup replay failed without an explicit support refusal");
std::puts("compute pipeline replay: device explicitly refused required subgroup size 32");
}
std::printf("compute pipeline replay: fresh %.6f ms, imported %.6f ms, GPU %s; no dispatch\n", fresh.pipelineMilliseconds, imported.pipelineMilliseconds, fresh.deviceName.c_str());
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "compute pipeline replay test: %s\n", error.what());
return 1;
}
}

View File

@ -295,6 +295,97 @@ std::size_t recompile(const std::string& name, std::span<const std::uint32_t> co
return result.spirv.size();
}
void verifyLargeLoopExitFallback() {
const std::vector<std::uint32_t> code{
0x060202ffu, 0x3f800000u, 0x060202ffu, 0x3f000000u, 0x060202ffu, 0x3f001000u, 0x060202ffu, 0x3f002000u,
0x060202ffu, 0x3f003000u, 0x060202ffu, 0x3f004000u, 0x060202ffu, 0x3f005000u, 0x060202ffu, 0x3f006000u,
0x060202ffu, 0x3f007000u, 0x060202ffu, 0x3f008000u, 0x060202ffu, 0x3f009000u, 0x060202ffu, 0x3f00a000u,
0x060202ffu, 0x3f00b000u, 0x060202ffu, 0x3f00c000u, 0x060202ffu, 0x3f00d000u, 0x060202ffu, 0x3f00e000u,
0x060202ffu, 0x3f00f000u, 0x060202ffu, 0x3f010000u, 0x060202ffu, 0x3f810000u, 0x060202ffu, 0x3f000010u,
0x060202ffu, 0x3f001010u, 0x060202ffu, 0x3f002010u, 0x060202ffu, 0x3f003010u, 0x060202ffu, 0x3f004010u,
0x060202ffu, 0x3f005010u, 0x060202ffu, 0x3f006010u, 0x060202ffu, 0x3f007010u, 0x060202ffu, 0x3f008010u,
0x060202ffu, 0x3f009010u, 0x060202ffu, 0x3f00a010u, 0x060202ffu, 0x3f00b010u, 0x060202ffu, 0x3f00c010u,
0x060202ffu, 0x3f00d010u, 0x060202ffu, 0x3f00e010u, 0x060202ffu, 0x3f00f010u, 0x060202ffu, 0x3f010010u,
0x7d880084u, 0xbf870025u, 0x060202ffu, 0x3f830000u, 0x060202ffu, 0x3f000030u, 0x060202ffu, 0x3f001030u,
0x060202ffu, 0x3f002030u, 0x060202ffu, 0x3f003030u, 0x060202ffu, 0x3f004030u, 0x060202ffu, 0x3f005030u,
0x060202ffu, 0x3f006030u, 0x060202ffu, 0x3f007030u, 0x060202ffu, 0x3f008030u, 0x060202ffu, 0x3f009030u,
0x060202ffu, 0x3f00a030u, 0x060202ffu, 0x3f00b030u, 0x060202ffu, 0x3f00c030u, 0x060202ffu, 0x3f00d030u,
0x060202ffu, 0x3f00e030u, 0x060202ffu, 0x3f00f030u, 0x060202ffu, 0x3f010030u, 0xbf820099u, 0x060202ffu,
0x3f820000u, 0x060202ffu, 0x3f000020u, 0x060202ffu, 0x3f001020u, 0x060202ffu, 0x3f002020u, 0x060202ffu,
0x3f003020u, 0x060202ffu, 0x3f004020u, 0x060202ffu, 0x3f005020u, 0x060202ffu, 0x3f006020u, 0x060202ffu,
0x3f007020u, 0x060202ffu, 0x3f008020u, 0x060202ffu, 0x3f009020u, 0x060202ffu, 0x3f00a020u, 0x060202ffu,
0x3f00b020u, 0x060202ffu, 0x3f00c020u, 0x060202ffu, 0x3f00d020u, 0x060202ffu, 0x3f00e020u, 0x060202ffu,
0x3f00f020u, 0x060202ffu, 0x3f010020u, 0x7d880085u, 0xbf87004cu, 0x060202ffu, 0x3f850000u, 0x060202ffu,
0x3f000050u, 0x060202ffu, 0x3f001050u, 0x060202ffu, 0x3f002050u, 0x060202ffu, 0x3f003050u, 0x060202ffu,
0x3f004050u, 0x060202ffu, 0x3f005050u, 0x060202ffu, 0x3f006050u, 0x060202ffu, 0x3f007050u, 0x060202ffu,
0x3f008050u, 0x060202ffu, 0x3f009050u, 0x060202ffu, 0x3f00a050u, 0x060202ffu, 0x3f00b050u, 0x060202ffu,
0x3f00c050u, 0x060202ffu, 0x3f00d050u, 0x060202ffu, 0x3f00e050u, 0x060202ffu, 0x3f00f050u, 0x060202ffu,
0x3f010050u, 0x7d880088u, 0xbf870001u, 0xbf82ff68u, 0x060202ffu, 0x3f870000u, 0x060202ffu, 0x3f000070u,
0x060202ffu, 0x3f001070u, 0x060202ffu, 0x3f002070u, 0x060202ffu, 0x3f003070u, 0x060202ffu, 0x3f004070u,
0x060202ffu, 0x3f005070u, 0x060202ffu, 0x3f006070u, 0x060202ffu, 0x3f007070u, 0x060202ffu, 0x3f008070u,
0x060202ffu, 0x3f009070u, 0x060202ffu, 0x3f00a070u, 0x060202ffu, 0x3f00b070u, 0x060202ffu, 0x3f00c070u,
0x060202ffu, 0x3f00d070u, 0x060202ffu, 0x3f00e070u, 0x060202ffu, 0x3f00f070u, 0x060202ffu, 0x3f010070u,
0xbf820027u, 0x060202ffu, 0x3f840000u, 0x060202ffu, 0x3f000040u, 0x060202ffu, 0x3f001040u, 0x060202ffu,
0x3f002040u, 0x060202ffu, 0x3f003040u, 0x060202ffu, 0x3f004040u, 0x060202ffu, 0x3f005040u, 0x060202ffu,
0x3f006040u, 0x060202ffu, 0x3f007040u, 0x060202ffu, 0x3f008040u, 0x060202ffu, 0x3f009040u, 0x060202ffu,
0x3f00a040u, 0x060202ffu, 0x3f00b040u, 0x060202ffu, 0x3f00c040u, 0x060202ffu, 0x3f00d040u, 0x060202ffu,
0x3f00e040u, 0x060202ffu, 0x3f00f040u, 0x060202ffu, 0x3f010040u, 0x7d880087u, 0xbf87ff43u, 0xbf82ff8du,
0x060202ffu, 0x3f860000u, 0x060202ffu, 0x3f000060u, 0x060202ffu, 0x3f001060u, 0x060202ffu, 0x3f002060u,
0x060202ffu, 0x3f003060u, 0x060202ffu, 0x3f004060u, 0x060202ffu, 0x3f005060u, 0x060202ffu, 0x3f006060u,
0x060202ffu, 0x3f007060u, 0x060202ffu, 0x3f008060u, 0x060202ffu, 0x3f009060u, 0x060202ffu, 0x3f00a060u,
0x060202ffu, 0x3f00b060u, 0x060202ffu, 0x3f00c060u, 0x060202ffu, 0x3f00d060u, 0x060202ffu, 0x3f00e060u,
0x060202ffu, 0x3f00f060u, 0x060202ffu, 0x3f010060u, 0xe0700000u, 0x80010100u, 0xbf810000u,
};
const std::string name("large loop-exit shader preserves the original guarded fallback");
static_cast<void>(verifyGraph(name, code));
static_cast<void>(recompile(name, code, 32u));
static_cast<void>(recompile(name, code, 64u));
}
void verifyNestedExitForwarders() {
constexpr auto none = InvalidControlFlowId;
constexpr std::array<std::array<std::uint32_t, 2>, 21> targets{{
{1, none}, {2, none}, {2, 3}, {2, 4}, {5, 6}, {1, 7}, {8, 9}, {10, none},
{11, 12}, {13, 14}, {1, 15}, {16, none}, {9, none}, {17, 18}, {19, none},
{16, none}, {none, none}, {16, none}, {15, 20}, {10, none}, {19, none},
}};
constexpr std::array<std::uint32_t, 21> order{0, 1, 2, 3, 4, 6, 9, 14, 19, 10, 15, 13, 18, 20, 17, 8, 12, 11, 5, 7, 16};
std::vector<std::uint32_t> code;
std::array<std::size_t, targets.size()> starts{};
std::vector<std::pair<std::size_t, std::uint32_t>> patches;
for (std::size_t at = 0; at < order.size(); ++at) {
const auto block = order[at];
starts[block] = code.size();
code.insert(code.end(), {0x060202ffu, 0x3f800000u + block * 0x10000u});
for (std::uint32_t padding = 0; padding < 17; ++padding) code.insert(code.end(), {0x060202ffu, 0x3f000000u + padding * 0x1000u + block * 0x10u});
const auto next = at + 1 < order.size() ? order[at + 1] : none;
const auto [first, second] = targets[block];
if (first == none) {
code.insert(code.end(), {0xe0700000u, 0x80010100u, 0xbf810000u});
continue;
}
if (second != none) {
code.push_back(0x7d880080u + block + 3u);
patches.emplace_back(code.size(), first);
code.push_back(0xbf870000u);
}
const auto branch = second != none ? second : first;
if (branch != next) {
patches.emplace_back(code.size(), branch);
code.push_back(0xbf820000u);
}
}
for (const auto& [instruction, target] : patches) {
const auto displacement = static_cast<std::int64_t>(starts[target]) - static_cast<std::int64_t>(instruction) - 1;
require(displacement >= -32768 && displacement <= 32767, "nested exit forwarder test branch is out of range");
code[instruction] |= static_cast<std::uint32_t>(displacement) & 0xffffu;
}
const std::string name("nested exit forwarders keep conditional selections structured");
static_cast<void>(verifyGraph(name, code));
static_cast<void>(recompile(name, code, 32u));
static_cast<void>(recompile(name, code, 64u));
}
void verifyProgram(const Program& program) {
const std::string name(program.name);
const auto result = verifyGraph(name, program.code);
@ -409,6 +500,38 @@ int main(int argc, char** argv) {
return failures == 0 ? 0 : 1;
}
const std::vector<Program> programs{
{"nested selections share a loop continuation", "Synthetic loop with two nested selections and a shared continue; the inner selection does not dominate the continuation.",
{0x060202ffu, 0x3f800000u, 0x060202ffu, 0x3f810000u, 0x7d880084u, 0xbf870001u, 0xbf820018u, 0x060202ffu, 0x3f820000u, 0x7d880085u, 0xbf87000cu, 0x060202ffu, 0x3f850000u, 0x7d880088u, 0xbf870003u, 0x060202ffu, 0x3f870000u, 0xbf82fff0u, 0x060202ffu, 0x3f860000u, 0x7d880089u, 0xbf87fff9u, 0xbf820008u, 0x060202ffu, 0x3f830000u, 0x7d880086u, 0xbf870001u, 0xbf820003u, 0x060202ffu, 0x3f840000u, 0xbf82fff0u, 0x060202ffu, 0x3f880000u, 0xe0700000u, 0x80000100u, 0xbf810000u}, Split::None},
{"reduced shared exit junction from the public CFG fuzz report", "Public reduced word stream from #2762; synthetic regression, not title data.",
{0x060202f2u, 0x060202f4u, 0x7d880084u, 0xbf870001u, 0xbf820011u, 0x060202ffu, 0x3f820000u, 0x7d880085u, 0xbf870005u,
0x060202ffu, 0x3f850000u, 0x7d880088u, 0xbf870009u, 0xbf82fff3u, 0x060202ffu, 0x3f840000u, 0x7d880087u, 0xbf870001u,
0xbf820003u, 0x060202ffu, 0x3f860000u, 0xbf82ffebu, 0x060202f6u, 0xe0700000u, 0x80010100u, 0xbf810000u}, Split::None},
{"both selection arms leave through a shared loop exit tail", R"(
v_mov_b32 v1, 0
s_mov_b32 s8, 0
loop:
s_cmp_lt_u32 s8, 4
s_cbranch_scc0 done
s_cmp_eq_u32 s8, s2
s_cbranch_scc1 right
v_cmp_gt_u32 vcc, 8, v0
s_cbranch_vccz tail
v_add_nc_u32 v1, 1, v1
s_branch latch
right:
v_cmp_gt_u32 vcc, 16, v0
s_cbranch_vccz tail
v_add_nc_u32 v1, 2, v1
latch:
s_add_u32 s8, s8, 1
s_branch loop
tail:
v_add_nc_u32 v1, 3, v1
done:
buffer_store_dword v1, off, s[0:3], 0
s_endpgm)",
Store({0x7e020280u, 0xbe880380u, 0xbf0a8408u, 0xbf84000cu, 0xbf060208u, 0xbf850004u, 0x7d880088u, 0xbf860007u, 0x4a020281u,
0xbf820003u, 0x7d880090u, 0xbf860003u, 0x4a020282u, 0x80088108u, 0xbf82fff3u, 0x4a020283u}), Split::None},
{"shared tail", R"(
v_cmp_gt_u32 vcc, 16, v0
s_cbranch_vccz outer_else
@ -520,6 +643,69 @@ done:
buffer_store_dword v1, off, s[0:3], 0
s_endpgm)",
Store({0xbe880380u, 0x7e020280u, 0x7d880008u, 0xbf860005u, 0x7d880088u, 0xbf870002u, 0x7d880084u, 0xbf870004u, 0x4a020281u, 0x4a020282u, 0xbf09c008u, 0xbf850002u, 0x80089008u, 0xbf82fff4u}), Split::None},
{"nested selections in a loop whose second latch continues from inside them", R"(
s_mov_b32 s8, 0
v_mov_b32 v1, 0
s_branch loop
again:
v_add_nc_u32 v1, 2, v1
s_add_u32 s8, s8, 16
loop:
s_cmp_eq_u32 s8, s2
s_cbranch_scc1 latch
v_cmp_gt_u32 vcc, 8, v0
s_cbranch_vccz latch
v_cmp_gt_u32 vcc, 4, v0
s_cbranch_vccnz again
latch:
v_add_nc_u32 v1, 1, v1
s_add_u32 s8, s8, 16
s_cmp_lt_u32 s8, 64
s_cbranch_scc1 loop
buffer_store_dword v1, off, s[0:3], 0
s_endpgm)",
Store({0xbe880380u, 0x7e020280u, 0xbf820002u, 0x4a020282u, 0x80089008u, 0xbf060208u, 0xbf850004u, 0x7d880088u, 0xbf860002u, 0x7d880084u, 0xbf87fff8u, 0x4a020281u, 0x80089008u, 0xbf0ac008u, 0xbf85fff6u}), Split::None},
{"an inner loop that ends the program or continues its outer loop", R"(
v_mov_b32 v1, 0
s_mov_b32 s8, 0
outer:
v_add_nc_u32 v1, 2, v1
inner:
v_cmp_gt_u32 vcc, 8, v0
s_cbranch_vccz done
v_add_nc_u32 v1, 1, v1
s_add_u32 s8, s8, 16
s_cmp_lt_u32 s8, 64
s_cbranch_scc1 inner
s_mov_b32 s8, 0
s_branch outer
done:
buffer_store_dword v1, off, s[0:3], 0
s_endpgm)",
Store({0x7e020280u, 0xbe880380u, 0x4a020282u, 0x7d880088u, 0xbf860006u, 0x4a020281u, 0x80089008u, 0xbf0ac008u, 0xbf85fffau, 0xbe880380u, 0xbf82fff7u}), Split::Route, 0},
{"an inner loop whose body leaves both loops", R"(
v_mov_b32 v1, 0
s_mov_b32 s8, 0
outer:
s_cmp_lt_u32 s8, 64
s_cbranch_scc0 done
inner:
v_cmp_gt_u32 vcc, 8, v0
s_cbranch_vccz latch
v_add_nc_u32 v1, 1, v1
s_add_u32 s8, s8, 16
s_cmp_eq_u32 s8, s2
s_cbranch_scc1 done
s_branch inner
latch:
v_add_nc_u32 v1, 2, v1
s_add_u32 s8, s8, 16
s_branch outer
done:
buffer_store_dword v1, off, s[0:3], 0
s_endpgm)",
Store({0x7e020280u, 0xbe880380u, 0xbf0ac008u, 0xbf84000au, 0x7d880088u, 0xbf860005u, 0x4a020281u, 0x80089008u, 0xbf060208u, 0xbf850004u,
0xbf82fff9u, 0x4a020282u, 0x80089008u, 0xbf82fff4u}), Split::Route, 0},
{"loop exit to the end of the program beside a kill exit", R"(
v_mov_b32 v1, 0
s_mov_b64 s[20:21], exec
@ -1060,6 +1246,8 @@ done:
}
try {
verifyNullSwappc();
verifyLargeLoopExitFallback();
verifyNestedExitForwarders();
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
++failures;

View File

@ -7,12 +7,15 @@
#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "SceShaders.hpp"
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
#include "ControlFlow/RequestSerializer.hpp"
#include "CacheKey.hpp"
#include "Optimization/ResourceProgram.hpp"
@ -275,6 +278,29 @@ void stateTests() {
queue.context[0x200] = 0x007007bbu;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "depth bounds without a depth plane");
queue = makeState();
queue.context[0x010] = 0x80000183;
queue.context[0x011] = 0x20000180;
queue.context[0x012] = 0x00001000;
queue.context[0x014] = 0x00001000;
queue.context[0x007] = 0x0001001f;
queue.context[0x000] = 0;
queue.context[0x002] = 0;
queue.context[0x00a] = 0;
queue.context[0x00b] = 0x3f800000;
queue.context[0x200] = 0x007007b1;
queue.context[0x1b3] = 2;
queue.context[0x1b4] = 2;
state = AgcDriver::Graphics::DecodeState(queue);
Require(!state.depth.has_value() && !state.stencilTest && state.renderExtent.width == state.color.extent.width && state.renderExtent.height == state.color.extent.height, "a stencil test without a stencil plane bound the depth plane it does not test");
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a stencil test without a stencil plane beside an untested depth plane was rejected");
queue.context[0x200] = 0x00700231;
state = AgcDriver::Graphics::DecodeState(queue);
Require(!state.depth.has_value() && !state.stencilTest && state.renderExtent.width == state.color.extent.width && state.renderExtent.height == state.color.extent.height, "a failing stencil test without a stencil plane bound the depth plane it does not test");
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a failing stencil test without a stencil plane beside an untested depth plane was rejected");
queue.context[0x200] = 0x007007b3;
state = AgcDriver::Graphics::DecodeState(queue);
Require(state.depth.has_value() && state.depthTest && !state.stencilTest && state.renderExtent.width == 32 && state.renderExtent.height == 2, "a depth test on a bound depth plane lost its surface");
queue = makeState();
queue.context[0x10f] = 0x7fc00000;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "non-finite");
// The register facade: every register the decoders read is in DrawKeyRegisters (a wrong hit of
@ -723,6 +749,43 @@ void ColorPipeBankXorTests() {
}
}
void HiddenUserWordVertexTablesTests() {
constexpr std::size_t userDataOffset = 288;
constexpr std::size_t directOffset = 352;
constexpr std::size_t semanticOffset = 384;
static std::vector<std::byte> header(semanticOffset + sizeof(ShaderSemantic));
static std::array<std::uint32_t, 4> bufferTable{0x1000u, 0x20000u, 64u, 0x3000u};
static std::array<std::uint32_t, 1> attribTable{(0x2cu << 5u)};
std::array<std::uint16_t, ShaderRegs::AGC_DIRECT_RESOURCE_TYPE_COUNT> direct{};
direct.fill(ShaderRegs::AGC_ILLEGAL_DIRECT_OFFSET);
direct[static_cast<std::size_t>(ShaderRegs::AgcDirectResourceType::PtrVertexBufferTable)] = 4;
direct[static_cast<std::size_t>(ShaderRegs::AgcDirectResourceType::PtrVertexAttribDescTable)] = 6;
std::memcpy(header.data() + directOffset, direct.data(), sizeof(direct));
ShaderSemantic semantic{};
semantic.size_in_elements = 1;
std::memcpy(header.data() + semanticOffset, &semantic, sizeof(semantic));
ShaderUserData userData{};
userData.direct_resource_offset = reinterpret_cast<std::uint16_t*>(header.data() + directOffset);
userData.direct_resource_count = ShaderRegs::AGC_DIRECT_RESOURCE_TYPE_COUNT;
std::memcpy(header.data() + userDataOffset, &userData, sizeof(userData));
Shader shader{};
shader.user_data = reinterpret_cast<ShaderUserData*>(header.data() + userDataOffset);
shader.input_semantics = reinterpret_cast<ShaderSemantic*>(header.data() + semanticOffset);
shader.num_input_semantics = 1;
std::memcpy(header.data(), &shader, sizeof(shader));
const auto headerAddress = reinterpret_cast<std::uintptr_t>(header.data());
const auto low = [](const void* pointer) { return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(pointer)); };
const auto high = [](const void* pointer) { return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(pointer) >> 32u); };
const std::array<std::uint32_t, 8> registers{0x31016facu, 0x7efff390u, 0x100002u, 0x49u, low(bufferTable.data()), high(bufferTable.data()), low(attribTable.data()), high(attribTable.data())};
std::vector<std::uint32_t> merged(8, 0u);
merged.insert(merged.end(), registers.begin(), registers.end());
const auto info = AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, merged, nullptr, false, 8);
Require(info.fetchEmbedded && info.fetchBufferReg == 4u && info.fetchAttribReg == 6u && info.resourcesNum == 1 && info.resources[0].fields[0] == 0x1000u && info.resources[0].fields[2] == 64u, "a merged program's vertex tables were not read past its eight hidden user words");
expectFailure([&] { static_cast<void>(AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, merged, nullptr, false, 0)); }, "null vertex attribute table");
const auto plain = AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, registers, nullptr, false, 0);
Require(plain.fetchEmbedded && plain.fetchBufferReg == 4u && plain.resourcesNum == 1 && plain.resources[0].fields[0] == 0x1000u, "a vertex program's tables moved");
}
void HeaderlessVertexStageTests() {
const std::array<std::uint32_t, 4> userData{1, 2, 3, 4};
const auto info = AgcDriver::Graphics::DecodeVertexStageInfo({}, 0, userData);
@ -731,6 +794,28 @@ void HeaderlessVertexStageTests() {
expectFailure([&] { static_cast<void>(AgcDriver::Graphics::DecodeVertexStageInfo(truncated, 0x1000, userData)); }, "smaller than the fixed AGC header");
}
void UnexportedPixelInputTests() {
ShaderRecompiler::ShaderPixelStageInfo pixel{};
pixel.interpolatorCount = 5;
pixel.interpolatorSettings[0] = 0x000u;
pixel.interpolatorSettings[1] = 0x401u;
pixel.interpolatorSettings[2] = 0x302u;
pixel.interpolatorSettings[3] = 0x123u;
pixel.interpolatorSettings[4] = 0x680004u;
pixel.interpolatorSettings[5] = 0x005u;
const std::array<std::uint32_t, 1> exported{0u};
const auto adjusted = AgcDriver::Graphics::DefaultUnexportedInputs(pixel, exported);
Require(adjusted.interpolatorSettings[0] == 0x000u, "an input of an exported parameter was changed");
Require(adjusted.interpolatorSettings[1] == 0x021u, "a flat input of an unexported parameter did not read zero");
Require(adjusted.interpolatorSettings[2] == 0x022u, "an input of an unexported parameter did not read zero");
Require(adjusted.interpolatorSettings[3] == 0x123u, "an input that already reads its default value was changed");
Require(adjusted.interpolatorSettings[4] == 0x180024u, "an FP16 input of an unexported parameter did not read zero in both halves");
Require(adjusted.interpolatorSettings[5] == 0x005u, "a setting past SPI_PS_IN_CONTROL's input count was changed");
const std::array<std::uint32_t, 5> all{0u, 1u, 2u, 4u, 5u};
const auto unchanged = AgcDriver::Graphics::DefaultUnexportedInputs(pixel, all);
Require(std::ranges::equal(unchanged.interpolatorSettings, pixel.interpolatorSettings), "inputs of exported parameters were changed");
}
void PixelInputLayoutTests() {
using ShaderRecompiler::PixelInput;
using ShaderRecompiler::PixelInputVgpr;
@ -878,6 +963,29 @@ void TuningFieldTests() {
}
}
void ShadersWriteMemoryTests() {
const auto binding = [](ShaderRecompiler::DescriptorKind kind, ShaderRecompiler::DescriptorRole role, std::vector<bool> written) {
ShaderRecompiler::DescriptorBinding result{};
result.kind = kind;
result.role = role;
result.count = 1;
if (role == ShaderRecompiler::DescriptorRole::GuestImages) result.imageWritten = std::move(written);
else result.bufferWritten = std::move(written);
return result;
};
ShaderRecompiler::RecompileResult reads;
reads.bindings.push_back(binding(ShaderRecompiler::DescriptorKind::StorageBuffer, ShaderRecompiler::DescriptorRole::GuestBuffers, {false}));
reads.bindings.push_back(binding(ShaderRecompiler::DescriptorKind::StorageImage, ShaderRecompiler::DescriptorRole::GuestImages, {false}));
ShaderRecompiler::RecompileResult stores;
stores.bindings.push_back(binding(ShaderRecompiler::DescriptorKind::StorageBuffer, ShaderRecompiler::DescriptorRole::GuestBuffers, {true}));
using Stage = ShaderRecompiler::ShaderStage;
const auto writes = [](std::initializer_list<AgcDriver::Graphics::CompiledShader> shaders) { return AgcDriver::Graphics::ShadersWriteMemory(std::vector<AgcDriver::Graphics::CompiledShader>(shaders)); };
Require(!writes({{Stage::Vertex, &reads, 0}, {Stage::Fragment, &reads, 0}}), "shaders that only read memory were reported as writing it");
Require(writes({{Stage::Vertex, &reads, 0}, {Stage::Fragment, &stores, 0}}), "a pixel shader that stores to a buffer was not reported");
Require(writes({{Stage::Vertex, &stores, 0}, {Stage::Fragment, &reads, 0}}), "a vertex shader that stores to a buffer was not reported");
Require(writes({{Stage::Geometry, &stores, 0}, {Stage::Fragment, &reads, 0}}), "a geometry shader that stores to a buffer was not reported");
}
void ReversedComponentOrderTests() {
{
auto queue = makeState();
@ -891,6 +999,12 @@ void ReversedComponentOrderTests() {
Require(!AgcDriver::Graphics::SplitsAlternateAlpha(blended), "one blend equation for color and alpha split an 8_8 SWAP_ALT draw");
queue.context[0x1e0] = 0x60000001u;
Require(AgcDriver::Graphics::SplitsAlternateAlpha(AgcDriver::Graphics::DecodeState(queue)), "a separate alpha blend into an 8_8 SWAP_ALT target did not split its second channel");
queue.context[0x1e0] = (1u << 30u) | 13u;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "constant color factor");
queue.context[0x1e0] = (1u << 30u) | (1u << 29u) | 1u | (1u << 16u) | (14u << 24u);
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "constant color factor");
queue.context[0x1e0] = (1u << 30u) | (2u << 5u) | 13u;
Require(AgcDriver::Graphics::DecodeState(queue).blends[0].colorBlendOp == VK_BLEND_OP_MIN, "a constant color factor of a MIN blend into an 8_8 SWAP_ALT target was rejected");
}
for (const auto& [swap, mapping] : {std::pair{2u, 0x1bu}, std::pair{3u, 0x93u}}) {
auto queue = makeState();
@ -1607,6 +1721,132 @@ void uint16ExportTests() {
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 8");
}
void multisampleTests() {
using AgcDriver::Graphics::ColorMetadataPass;
using AgcDriver::Graphics::DecodeColorMetadataPass;
using AgcDriver::Graphics::DecodeState;
const auto multisampled = [] {
auto queue = makeState();
queue.context[0x2f8] = 0x00100001;
queue.context[0x292] = 3;
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44;
queue.context[0x31d] = 0x9000;
queue.context[0x3b8] |= (0x18u << 19u) | 0x4000000u;
queue.context[0x1b3] = 2;
queue.context[0x1b4] = 2;
return queue;
};
auto queue = multisampled();
auto state = DecodeState(queue);
Require(state.samples == 2 && state.colors.size() == 1 && state.colors[0].samples == 2 && state.colors[0].cmaskAddress == 0, "2x multisampling did not decode");
const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false);
Require(rejection.empty(), "the precheck refused 2x multisampling: " + rejection);
queue.context[0x302] = 0x44cc;
expectFailure([&] { DecodeState(queue); }, "nonstandard sample locations");
queue = multisampled();
queue.context[0x2f8] = 0x00300003;
queue.context[0x31d] = 0x1b000;
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) {
queue.context[0x2fe + pixel * 4u] = 0xbd153fd1u;
queue.context[0x2ff + pixel * 4u] = 0x9773f95bu;
}
state = DecodeState(queue);
Require(state.samples == 8 && state.colors.size() == 1 && state.colors[0].samples == 8, "the standard 8x pattern (sample 7 at (7, -7), PPSA99902's registers) did not decode as standard");
queue = multisampled();
queue.context[0x31d] = 0x1000;
expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples");
queue.context[0x31d] = 0;
expectFailure([&] { DecodeState(queue); }, "sample count differs");
queue = multisampled();
queue.context[0x292] = 2;
expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE");
queue = multisampled();
queue.context[0x2f8] |= 0x10;
expectFailure([&] { DecodeState(queue); }, "coverage conversion");
queue = multisampled();
queue.context[0x31c] |= 0x6000;
queue.context[0x31f] = 0x1234;
Require(DecodeState(queue).colors[0].cmaskAddress == 0x123400, "the CMASK address of a multisampled target did not decode");
queue = makeState();
queue.context[0x31c] |= 0x4000;
expectFailure([&] { DecodeState(queue); }, "color compression");
queue = multisampled();
queue.context[0x31c] |= 0x2000;
queue.context[0x31f] = 0x1234;
const auto fastCleared = DecodeState(queue).colors[0];
Require(fastCleared.cmaskAddress == 0x123400 && fastCleared.cmaskFastClear && fastCleared.cmaskBytes == 0x1000, "a multisampled FAST_CLEAR target did not decode its CMASK at the metablock size");
queue = multisampled();
queue.context[0x31c] |= 0x4000;
queue.context[0x31f] = 0x1234;
const auto compressed = DecodeState(queue).colors[0];
Require(compressed.cmaskAddress == 0x123400 && !compressed.cmaskFastClear && compressed.cmaskBytes == 0x1000, "a multisampled COMPRESSION target did not decode its CMASK at the metablock size");
queue = multisampled();
queue.context[0x31c] |= 0x2000;
queue.context[0x31f] = 0;
expectFailure([&] { DecodeState(queue); }, "without a CMASK address");
queue = multisampled();
queue.context[0x31c] |= 0x2000;
queue.context[0x31f] = 0x1234;
std::vector<AgcDriver::Graphics::RegisterRead> reads;
AgcDriver::Graphics::RegisterReadLog() = &reads;
static_cast<void>(DecodeState(queue));
AgcDriver::Graphics::RegisterReadLog() = nullptr;
for (const auto read : reads) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a CMASK register the decoder reads: " + std::to_string(read.offset));
queue = multisampled();
queue.context[0x0] = 0;
queue.context[0x202] = 0xcc0030;
for (const auto offset : {0x318u, 0x31bu, 0x31cu, 0x390u}) queue.context[offset + (offset >= 0x390u ? 1u : 0xfu)] = queue.context[offset];
queue.context[0x32c] = 0;
queue.context[0x3b1] = queue.context[0x3b0];
queue.context[0x3b9] = queue.context[0x3b8];
auto pass = DecodeColorMetadataPass(queue);
Require(pass.has_value() && pass->mode == ColorMetadataPass::Mode::Resolve && pass->source.has_value() && pass->source->samples == 2 && pass->targets.size() == 1 && pass->targets[0].samples == 1, "a CB resolve of a multisampled target did not decode");
queue.context[0x32b] = queue.context[0x31c] ^ (1u << 8u);
expectFailure([&] { DecodeColorMetadataPass(queue); }, "different formats or extents");
queue = multisampled();
queue.context[0x0] = 0;
queue.context[0x202] = 0xcc0030;
for (const auto offset : {0x318u, 0x31bu, 0x31cu, 0x390u}) queue.context[offset + (offset >= 0x390u ? 1u : 0xfu)] = queue.context[offset];
queue.context[0x32c] = 0;
queue.context[0x3b1] = queue.context[0x3b0];
queue.context[0x3b9] = queue.context[0x3b8];
queue.context[0x32b] = queue.context[0x31c] | 0x2000u;
queue.context[0x32e] = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(cmaskMemory.data()) >> 8u);
queue.context[0x399] = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(cmaskMemory.data()) >> 40u);
expectFailure([&] { DecodeColorMetadataPass(queue); }, "fast-clear color target");
queue = multisampled();
queue.context[0x2f8] = 0;
queue.context[0x0] = 0;
queue.context[0x202] = 0xcc0020;
expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled color target");
using AgcDriver::Graphics::DccKeys;
using AgcDriver::Graphics::MultisampledCmask;
MultisampledCmask cmask(0x10000, 0x1000);
cmask.NoteFill(0x10000, 0x1000, 0x11111111u);
expectFailure([&] { cmask.TakeClear(); }, "leaves its tiles mixed");
cmask.NoteFill(0x10800, 0x1000, 0);
expectFailure([&] { cmask.TakeClear(); }, "partly covers");
cmask.NoteFill(0x10000, 0x1000, 0);
Require(cmask.TakeClear() && !cmask.TakeClear(), "a zero fill of the whole CMASK did not clear once");
cmask.NoteFill(0x10000, 0x1000, 0xffffffffu);
Require(!cmask.TakeClear(), "an expanded CMASK cleared");
cmask.NoteFill(0x20000, 0x1000, 0x11111111u);
Require(!cmask.TakeClear() && cmask.Keys() == DccKeys::Uncompressed, "a fill of another range changed the CMASK");
cmask.CheckAddress(0x10000);
expectFailure([&] { cmask.CheckAddress(0x20000); }, "used CMASK 0x20000");
MultisampledCmask seeded(0x30000, 0x1000);
seeded.SeedKeys(DccKeys::Clear0000);
Require(seeded.TakeClear(), "a fast-cleared seed did not clear its first use");
MultisampledCmask mixedSeed(0x30000, 0x1000);
mixedSeed.SeedKeys(DccKeys::Mixed);
expectFailure([&] { mixedSeed.TakeClear(); }, "not modeled");
MultisampledCmask unaddressed;
unaddressed.NoteFill(0, 0x1000, 0);
Require(!unaddressed.TakeClear(), "a target without a CMASK cleared");
}
void DepthClipTests() {
auto queue = makeState();
const auto direct = AgcDriver::Graphics::DecodeState(queue);
@ -2149,6 +2389,42 @@ void expectSingleAccepted(const ShaderRecompiler::DescriptorBinding& binding, st
Require(mock.live == 0, std::string(what) + " leaked Vulkan objects");
}
void descriptorLayoutFlagTests() {
constexpr VkDescriptorBindingFlags indexed = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT;
mock = MockVulkan{};
{
AgcDriver::Graphics::DescriptorCache cache(mockContext());
const VkDescriptorSetLayoutBinding binding{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
const std::array<std::uint32_t, 4> key{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2, VK_SHADER_STAGE_COMPUTE_BIT};
const std::array<VkDescriptorBindingFlags, 1> flags{indexed};
const auto layout = cache.Layout(key, std::span(&binding, 1), false, flags);
Require(mock.layoutFlags == 0 && mock.layoutBindingFlags == std::vector<VkDescriptorBindingFlags>{indexed}, "native layout lost binding flags without update-after-bind");
Require(cache.Layout(key, std::span(&binding, 1), false, flags) == layout, "identical binding flags missed the layout cache");
Require(cache.Layout(key, std::span(&binding, 1)) != layout && mock.layoutBindingFlags.empty(), "layout cache ignored binding flags");
}
Require(mock.live == 0, "binding flag layout cache leaked Vulkan objects");
for (const bool updateAfterBind : {false, true}) {
mock = MockVulkan{};
auto context = mockContext();
context.emptyBuffer = reinterpret_cast<VkBuffer>(std::uintptr_t{0x1000});
context.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindStorageBuffers = 17;
context.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources = 128;
context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers = 32;
{
ShaderRecompiler::RecompileResult program;
const std::uint32_t count = updateAfterBind ? 17 : 1;
program.bindings.push_back(makeBinding(Role::GuestBuffers, 0, count, std::vector<std::uint32_t>(4 * count)));
const AgcDriver::Graphics::CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0};
AgcDriver::Graphics::ShaderResources resources(context, compute);
const auto flags = resources.LayoutBindingFlags();
Require(flags.size() == resources.LayoutBindings().size() && flags.size() == 1, "exported binding flag count differs from native layout");
Require(flags[0] == (updateAfterBind ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0), "exported binding flags differ from native layout");
Require(updateAfterBind ? mock.layoutBindingFlags == std::vector<VkDescriptorBindingFlags>(flags.begin(), flags.end()) : mock.layoutBindingFlags.empty(), "native layout did not use the exported binding flag vector");
}
Require(mock.live == 0, "exported binding flag fixture leaked Vulkan objects");
}
}
void descriptorPoolTests() {
using namespace AgcDriver::Graphics;
mock = MockVulkan{};
@ -3025,8 +3301,20 @@ void rectListTests() {
queue.userConfig[0x242] = 17;
const auto state = DecodeState(queue);
const Context context{};
const AgcDriver::Pm4::DrawParameters draw{0, 4, 0, 1, 0, false};
expectFailure([&] { Draw(context, state, draw, {}); }, "incomplete rect-list");
std::array<std::uint16_t, 8> indices{};
const auto drawFailure = [&](std::uint32_t count, bool indexed) {
const AgcDriver::Pm4::DrawParameters draw = indexed ? AgcDriver::Pm4::DrawParameters{reinterpret_cast<std::uint64_t>(indices.data()), count, 2, 1, 0, true} : AgcDriver::Pm4::DrawParameters{0, count, 0, 1, 0, false};
try {
Draw(context, state, draw, {});
} catch (const std::exception& error) {
return std::string(error.what());
}
return std::string{};
};
for (const auto indexed : {false, true}) {
const auto complete = drawFailure(3, indexed);
for (const auto count : {4u, 5u}) Require(drawFailure(count, indexed) == complete, "a rect-list draw with a partial last rectangle was not drawn like its complete rectangles");
}
}
ShaderRecompiler::RecompileResult recompilePixel(std::initializer_list<std::uint32_t> controls, std::span<const std::uint32_t> code) {
@ -4248,6 +4536,11 @@ int main(int argc, char** argv) {
setenv("APS5_HEAP_MIRROR_MIB", "4", 1);
#endif
try {
if (argc == 2 && std::string_view(argv[1]) == "--descriptor-layout-flags") {
descriptorLayoutFlagTests();
std::cout << "Descriptor layout native and captured binding flags passed\n";
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--descriptor-pool-reset-failure") {
descriptorPoolResetFailureTest();
std::cerr << "descriptor pool reset failure did not terminate\n";
@ -4305,17 +4598,21 @@ int main(int argc, char** argv) {
OpaqueDestinationAlphaTests();
CompactedExportTests();
ReversedComponentOrderTests();
ShadersWriteMemoryTests();
DualSourceBlendTests();
metadataPassTests();
cmaskTests();
uint16ExportTests();
uint8x4TargetTests();
multisampleTests();
ShaderStageTests();
NggVertexWorkgroupTests();
TuningFieldTests();
PixelInputLayoutTests();
UnexportedPixelInputTests();
ColorPipeBankXorTests();
HeaderlessVertexStageTests();
HiddenUserWordVertexTablesTests();
ComputeScratchTests();
shaderUserDataTailPaddingTests();
InitialContextTests();

View File

@ -34,6 +34,12 @@ struct Builder {
return created;
}
IrValue& truth() {
auto& created = program.CreateValue(IrOpcode::Void, IrType::Bool);
created.SetImmediateBool(true);
return created;
}
IrValue& emit(IrOpcode opcode, IrType type, std::initializer_list<IrValue*> arguments, std::uint64_t flags = 0) {
auto& created = program.CreateValue(opcode, type, flags);
for (auto* argument : arguments) created.AddArgument(argument);
@ -592,14 +598,24 @@ int main() {
return b.eliminate() == 0u && !removed(written);
});
const auto sample = [](IrShaderStage stage, std::uint32_t sampleFlags, IrOpcode opcode = IrOpcode::ImageSampleRaw) {
const auto wholeQuads = [](Builder& b, IrValue& live) -> IrValue& {
auto& ballot = b.emit(IrOpcode::Ballot, IrType::U32x4, {&live});
auto& allLow = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.truth()});
auto& allHigh = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.truth()});
auto& low = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &b.constant(0u)}), &b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&allLow, &b.constant(0u)})});
auto& high = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &b.constant(1u)}), &b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&allHigh, &b.constant(1u)})});
auto& wqm = b.emit(IrOpcode::WqmU64, IrType::U64, {&b.emit(IrOpcode::CompositeConstructU64, IrType::U64, {&low, &high})});
return b.threadBit(b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(0u)}), b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(1u)}));
};
const auto sample = [&](IrShaderStage stage, std::uint32_t sampleFlags, IrOpcode opcode = IrOpcode::ImageSampleRaw, bool wholeQuadExec = false) {
Builder b;
b.program.Resources().stage = stage;
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.imageSampleFlags = sampleFlags;
b.program.Resources().memoryInfo.push_back(memory);
auto& exec = b.mask(16u);
auto& exec = wholeQuadExec ? wholeQuads(b, b.mask(16u)) : b.mask(16u);
auto& old = b.lane();
auto& written = b.select(exec, b.add(old, 1u), old);
auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&written, &old});
@ -618,6 +634,9 @@ int main() {
return !sample(IrShaderStage::Pixel, 0u);
});
passed &= run("an implicit-LOD pixel sample under whole quad mode reads no masked quad and drops the select", [&] {
return sample(IrShaderStage::Pixel, 0u, IrOpcode::ImageSampleRaw, true);
});
passed &= run("an explicit-LOD sample reads only its lane and drops the select", [&] {
return sample(IrShaderStage::Pixel, RdnaImageSampleFlagLevelZero) && sample(IrShaderStage::Mesh, 0u);
});
@ -697,5 +716,54 @@ int main() {
return !guarded(IrOpcode::SharedAtomicIAdd32, true, false);
});
passed &= run("a write under whole quad mode read only by the lanes it was widened from drops the select in every stage", [&] {
const auto widened = [&](IrShaderStage stage) {
Builder b;
b.program.Resources().stage = stage;
auto& live = b.mask(16u);
auto& quads = wholeQuads(b, live);
auto& old = b.lane();
auto& written = b.select(quads, b.add(old, 1u), old);
auto& next = b.select(b.logicalAnd(live, b.mask(8u)), b.add(written, 2u), old);
b.keep(next);
return b.eliminate() != 0u && removed(written) && !removed(next);
};
return widened(IrShaderStage::Pixel) && widened(IrShaderStage::Compute);
});
passed &= run("a write under whole quad mode read by a lane outside the widened mask keeps the select in compute", [&] {
Builder b;
b.program.Resources().stage = IrShaderStage::Compute;
auto& live = b.mask(16u);
auto& quads = wholeQuads(b, live);
auto& old = b.lane();
auto& written = b.select(quads, b.add(old, 1u), old);
auto& next = b.select(b.mask(32u), b.add(written, 2u), old);
b.keep(next);
return b.eliminate() == 0u && !removed(written);
});
passed &= run("a write under whole quad mode read only by the lanes it was widened from drops the select", [&] {
Builder b;
auto& live = b.mask(16u);
auto& quads = wholeQuads(b, live);
auto& old = b.lane();
auto& written = b.select(quads, b.add(old, 1u), old);
auto& next = b.select(b.logicalAnd(live, b.mask(8u)), b.add(written, 2u), old);
b.keep(next);
return b.eliminate() != 0u && removed(written) && !removed(next);
});
passed &= run("a write under whole quad mode read by other lanes keeps the select", [&] {
Builder b;
auto& live = b.mask(16u);
auto& quads = wholeQuads(b, live);
auto& old = b.lane();
auto& written = b.select(quads, b.add(old, 1u), old);
auto& next = b.select(b.mask(32u), b.add(written, 2u), old);
b.keep(next);
return b.eliminate() == 0u && !removed(written);
});
return passed ? 0 : 1;
}

View File

@ -362,6 +362,12 @@ void testMemory() {
check(data[0] == 61 && data[1] == 62, "WRITE_DATA with a cache policy failed");
execute(state, makePacket(0x37, {0x40100500, low(data.data()), high(data.data()), 81, 82}));
check(data[0] == 81 && data[1] == 82, "WRITE_DATA to memory from the graphics queue failed");
execute(state, makePacket(0x37, {0x06100300, low(data.data()), high(data.data()), 91, 92}));
check(data[0] == 91 && data[1] == 92, "WRITE_DATA with destination 3 did not write memory");
const auto fence = makePacket(0x37, {0x06100300, low(data.data()), high(data.data()), 0});
const auto fenceLabel = AgcDriver::Pm4::DecodeLabelWrite(fence);
check(fenceLabel.has_value() && fenceLabel->address == reinterpret_cast<std::uint64_t>(data.data()), "WRITE_DATA with destination 3 did not resolve as a label");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x37, {0x100000, low(data.data()), high(data.data()), 1}), 0); }, "register destination");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x37, {0x08000100, low(data.data()), high(data.data()), 71}), 0); }, "reserved");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x37, {0x80000100, low(data.data()), high(data.data()), 51}), 0); }, "engine");
execute(state, makePacket(0x83, {0xbff8, 2, low(data.data()), high(data.data())}));

View File

@ -8,6 +8,7 @@
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
@ -38,6 +39,7 @@
#else
#include <sys/mman.h>
#endif
#include <functional>
#include <algorithm>
#include <array>
#include <atomic>
@ -250,6 +252,7 @@ public:
function<PFN_vkGetPhysicalDeviceMemoryProperties>("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory);
VkPhysicalDeviceProperties properties{};
function<PFN_vkGetPhysicalDeviceProperties>("vkGetPhysicalDeviceProperties")(context.physical, &properties);
std::cout << "Vulkan device: " << properties.deviceName << '\n';
context.limits = properties.limits;
context.bufferDeviceAddress = true;
context.formatProperties = function<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties");
@ -1764,6 +1767,150 @@ void drawSnapshotPatchTests(const Device& device) {
}
}
void drawSnapshotCapTests(const Device& device, Recorder& recorder) {
using namespace AgcDriver::GuestMemory;
const auto& context = device.GetContext();
const char* previous = std::getenv("APS5_DRAW_SNAPSHOT_MAX_MIB");
const std::string saved = previous != nullptr ? previous : "";
#ifdef _WIN32
Require(_putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", "1") == 0, "cannot set snapshot cap");
#else
Require(setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", "1", 1) == 0, "cannot set snapshot cap");
#endif
struct RestoreCap {
const char* previous;
std::string saved;
~RestoreCap() {
#ifdef _WIN32
_putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", saved.c_str());
#else
if (previous != nullptr) {
setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", saved.c_str(), 1);
} else {
unsetenv("APS5_DRAW_SNAPSHOT_MAX_MIB");
}
#endif
}
} restore{previous, saved};
constexpr std::size_t bytes = 3 * 1024 * 1024;
void* block = context.hostImportAlignment != 0 ? AllocateWatched(bytes, 65536) : nullptr;
if (block == nullptr) {
std::cout << "host imports or write watching unavailable: draw snapshot cap not tested\n";
return;
}
struct Release {
void* block;
~Release() { ReleaseWatched(block, bytes); }
} release{block};
std::memset(block, 0x11, bytes);
const auto address = reinterpret_cast<std::uint64_t>(block);
{
GuestAllocations::Mutation mutation;
mutation.Add(block, bytes, true, true, true);
}
struct Unregister {
const Context& context;
void* block;
~Unregister() {
{
GuestAllocations::Mutation mutation;
mutation.Remove(block);
}
HostImportFor(context, reinterpret_cast<std::uint64_t>(block), bytes);
}
} unregister{context, block};
if (HostImportFor(context, address, bytes) == nullptr) {
std::cout << "host import of the watched block refused: draw snapshot cap not tested\n";
return;
}
const auto smallAddress = address + 4096;
constexpr std::size_t smallBytes = 1024 * 1024 - 4096;
const auto largeAddress = address + 1024 * 1024 + 8192;
constexpr std::size_t largeBytes = 1024 * 1024 + 4096;
ShaderRecompiler::RecompileResult program;
ShaderRecompiler::DescriptorBinding smallBinding;
smallBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer;
smallBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers;
smallBinding.descriptorSet = 0;
smallBinding.binding = 0;
smallBinding.count = 1;
smallBinding.guestDescriptor = {static_cast<std::uint32_t>(smallAddress), static_cast<std::uint32_t>(smallAddress >> 32u) & 0xffffu, static_cast<std::uint32_t>(smallBytes), 0x31000000u};
smallBinding.bufferWritten = {false};
program.bindings.push_back(smallBinding);
ShaderRecompiler::DescriptorBinding largeBinding;
largeBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer;
largeBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers;
largeBinding.descriptorSet = 0;
largeBinding.binding = 1;
largeBinding.count = 1;
largeBinding.guestDescriptor = {static_cast<std::uint32_t>(largeAddress), static_cast<std::uint32_t>(largeAddress >> 32u) & 0xffffu, static_cast<std::uint32_t>(largeBytes), 0x31000000u};
largeBinding.bufferWritten = {false};
program.bindings.push_back(largeBinding);
const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0};
{
auto snapshotContext = context;
DescriptorCache cache(snapshotContext);
snapshotContext.descriptorCache = &cache;
Recorder snapshotRecorder(snapshotContext);
snapshotRecorder.Activate();
ShaderResources resources(snapshotContext, compute);
const auto bindings = resources.PrepareDrawBindings(snapshotRecorder);
Require(bindings != nullptr, "read-only draw bindings were not prepared");
Require(bindings->snapshots.size() == 1, "an input exceeding the draw snapshot cap was snapshotted");
Require(bindings->snapshots[0].address == smallAddress, "the snapshotted input does not match the smaller buffer below the cap");
Require(snapshotRecorder.ReusableDrawSnapshot(largeAddress, largeBytes) == nullptr, "an input exceeding the draw snapshot cap was kept in the snapshot cache");
if (Watched(address, bytes)) {
Require(snapshotRecorder.ReusableDrawSnapshot(smallAddress, smallBytes) != nullptr, "an input below the draw snapshot cap was not kept in the snapshot cache");
}
const auto contents = bindings->snapshots[0].buffer->Bytes();
Require(contents.size() == smallBytes && std::all_of(contents.begin(), contents.end(), [](std::byte value) { return value == std::byte{0x11}; }), "a draw snapshot does not hold the guest bytes of its draw");
std::memset(reinterpret_cast<void*>(smallAddress), 0x22, smallBytes);
std::memset(reinterpret_cast<void*>(largeAddress), 0x33, largeBytes);
Require(std::all_of(contents.begin(), contents.end(), [](std::byte value) { return value == std::byte{0x11}; }), "snapshotted input observed CPU overwrite");
const auto* inPlace = reinterpret_cast<const std::byte*>(largeAddress);
Require(inPlace[0] == std::byte{0x33} && std::all_of(inPlace, inPlace + largeBytes, [](std::byte value) { return value == std::byte{0x33}; }), "in-place bound input did not observe CPU write");
std::memset(block, 0x11, bytes);
snapshotRecorder.Sync();
}
const auto firstAddress = address + 4096;
constexpr std::size_t firstBytes = 600 * 1024;
const auto secondAddress = address + 600 * 1024 + 8192;
constexpr std::size_t secondBytes = 600 * 1024;
ShaderRecompiler::RecompileResult cumulativeProgram;
ShaderRecompiler::DescriptorBinding firstBinding;
firstBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer;
firstBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers;
firstBinding.descriptorSet = 0;
firstBinding.binding = 0;
firstBinding.count = 1;
firstBinding.guestDescriptor = {static_cast<std::uint32_t>(firstAddress), static_cast<std::uint32_t>(firstAddress >> 32u) & 0xffffu, static_cast<std::uint32_t>(firstBytes), 0x31000000u};
firstBinding.bufferWritten = {false};
cumulativeProgram.bindings.push_back(firstBinding);
ShaderRecompiler::DescriptorBinding secondBinding;
secondBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer;
secondBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers;
secondBinding.descriptorSet = 0;
secondBinding.binding = 1;
secondBinding.count = 1;
secondBinding.guestDescriptor = {static_cast<std::uint32_t>(secondAddress), static_cast<std::uint32_t>(secondAddress >> 32u) & 0xffffu, static_cast<std::uint32_t>(secondBytes), 0x31000000u};
secondBinding.bufferWritten = {false};
cumulativeProgram.bindings.push_back(secondBinding);
const CompiledShader cumulativeCompute{ShaderRecompiler::ShaderStage::Compute, &cumulativeProgram, 0};
{
auto snapshotContext = context;
DescriptorCache cache(snapshotContext);
snapshotContext.descriptorCache = &cache;
Recorder snapshotRecorder(snapshotContext);
snapshotRecorder.Activate();
ShaderResources resources(snapshotContext, cumulativeCompute);
const auto bindings = resources.PrepareDrawBindings(snapshotRecorder);
Require(bindings != nullptr, "read-only draw bindings were not prepared for cumulative test");
Require(bindings->snapshots.size() == 1, "cumulative cap permitted both inputs to snapshot");
Require(bindings->snapshots[0].address == firstAddress, "the earlier binding in the layout was not prioritised for the snapshot budget");
snapshotRecorder.Sync();
}
recorder.Activate();
}
void drawSnapshotEvictionTests(const Device& device) {
using namespace AgcDriver::GuestMemory;
@ -2201,7 +2348,7 @@ void storageRefreshTests(const Device& device, Recorder& recorder, bool watched)
DccKeyProof proof;
Require(ProvedClearKeys(resource, surfaceBytes, proof) == DccKeys::Clear0000, "0x00 keys are not a 0000 clear");
{
auto image = std::make_shared<StorageTexture>(context, detiler, resource, 0);
auto image = StorageTexture::Create(context, detiler, resource, 0);
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
const auto draw = [&](VkClearColorValue value) {
const auto commands = recorder.Commands();
@ -2555,7 +2702,7 @@ void staleGenerationTests(const Device& device, Recorder& recorder) {
resource.dccAddress = address + surfaceBytes;
{
Require(AgcDriver::GuestMemory::Watched(address, bytes), "(s) the block is not watched before the storage image takes its generation");
auto image = std::make_shared<StorageTexture>(context, detiler, resource, 0);
auto image = StorageTexture::Create(context, detiler, resource, 0);
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
const auto commands = recorder.Commands();
recorder.Keep(image);
@ -2697,7 +2844,7 @@ void writeBackPaddingTests(const Device& device, Recorder& recorder, bool watche
context.detiler = &detiler;
resource.baseAddress = address;
{
auto image = std::make_shared<StorageTexture>(context, detiler, resource, 0);
auto image = StorageTexture::Create(context, detiler, resource, 0);
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, test.mipCount, 0, 1};
const auto commands = recorder.Commands();
recorder.Keep(image);
@ -2874,7 +3021,7 @@ void singlePassTests(const Device& device, Recorder& recorder, bool watched) {
for (auto& value : texels) value = random();
const auto movesBefore = StorageTexture::SinglePassMoves();
{
auto image = std::make_shared<StorageTexture>(context, detiler, resource, 0);
auto image = StorageTexture::Create(context, detiler, resource, 0);
recorder.Keep(image);
results[mode].uploaded = readImage(*image);
Buffer staging(context, linearBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
@ -2898,7 +3045,7 @@ void singlePassTests(const Device& device, Recorder& recorder, bool watched) {
recorder.Submit();
device.WaitQueue();
recorder.Sync();
auto again = std::make_shared<StorageTexture>(context, detiler, resource, 0);
auto again = StorageTexture::Create(context, detiler, resource, 0);
recorder.Keep(again);
results[mode].reuploaded = readImage(*again);
std::vector<std::byte> read(bytes);
@ -2929,6 +3076,145 @@ void singlePassTests(const Device& device, Recorder& recorder, bool watched) {
std::cout << "single-pass storage moves" << (watched ? " (watched)" : "") << ": ok (" << tested << " surfaces)\n";
}
class StorageUploadProbe {
public:
StorageUploadProbe(Context& context, bool failView) : context(context), resolver(context.deviceProc), failView(failView) {
Require(active == nullptr, "nested storage upload probe");
active = this;
context.deviceProc = resolve;
}
~StorageUploadProbe() {
for (const auto view : destroyedViews) function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, view, nullptr);
if (imageDestroyed) function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, image, nullptr);
if (memoryFreed) function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, memory, nullptr);
context.deviceProc = resolver;
active = nullptr;
}
VkImage Image() const { return image; }
bool Destroyed() const { return imageDestroyed || memoryFreed; }
bool Released() const { return imageDestroyed && memoryFreed; }
bool FailedView() const { return failedView; }
std::uint32_t Clears() const { return clears; }
private:
template<typename TFunction>
TFunction function(const char* name) const { return reinterpret_cast<TFunction>(resolver(context.device, name)); }
static VKAPI_ATTR VkResult VKAPI_CALL createImage(VkDevice device, const VkImageCreateInfo* info, const VkAllocationCallbacks* callbacks, VkImage* created) {
const auto result = active->function<PFN_vkCreateImage>("vkCreateImage")(device, info, callbacks, created);
if (result == VK_SUCCESS) active->image = *created;
return result;
}
static VKAPI_ATTR VkResult VKAPI_CALL bindImageMemory(VkDevice device, VkImage image, VkDeviceMemory memory, VkDeviceSize offset) {
const auto result = active->function<PFN_vkBindImageMemory>("vkBindImageMemory")(device, image, memory, offset);
if (result == VK_SUCCESS && image == active->image) active->memory = memory;
return result;
}
static VKAPI_ATTR VkResult VKAPI_CALL createView(VkDevice device, const VkImageViewCreateInfo* info, const VkAllocationCallbacks* callbacks, VkImageView* created) {
if (info->image == active->image && active->failView && active->clears != 0) {
active->failedView = true;
return VK_ERROR_OUT_OF_DEVICE_MEMORY;
}
const auto result = active->function<PFN_vkCreateImageView>("vkCreateImageView")(device, info, callbacks, created);
if (result == VK_SUCCESS && info->image == active->image) active->views.push_back(*created);
return result;
}
static VKAPI_ATTR void VKAPI_CALL destroyView(VkDevice device, VkImageView view, const VkAllocationCallbacks* callbacks) {
if (std::find(active->views.begin(), active->views.end(), view) != active->views.end()) active->destroyedViews.push_back(view);
else active->function<PFN_vkDestroyImageView>("vkDestroyImageView")(device, view, callbacks);
}
static VKAPI_ATTR void VKAPI_CALL destroyImage(VkDevice device, VkImage image, const VkAllocationCallbacks* callbacks) {
if (image == active->image) active->imageDestroyed = true;
else active->function<PFN_vkDestroyImage>("vkDestroyImage")(device, image, callbacks);
}
static VKAPI_ATTR void VKAPI_CALL freeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* callbacks) {
if (memory == active->memory) active->memoryFreed = true;
else active->function<PFN_vkFreeMemory>("vkFreeMemory")(device, memory, callbacks);
}
static VKAPI_ATTR void VKAPI_CALL clearImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, const VkClearColorValue* value, std::uint32_t count, const VkImageSubresourceRange* ranges) {
if (image == active->image) ++active->clears;
active->function<PFN_vkCmdClearColorImage>("vkCmdClearColorImage")(commands, image, layout, value, count, ranges);
}
static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL resolve(VkDevice device, const char* name) {
if (std::strcmp(name, "vkCreateImage") == 0) return reinterpret_cast<PFN_vkVoidFunction>(createImage);
if (std::strcmp(name, "vkBindImageMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(bindImageMemory);
if (std::strcmp(name, "vkCreateImageView") == 0) return reinterpret_cast<PFN_vkVoidFunction>(createView);
if (std::strcmp(name, "vkDestroyImageView") == 0) return reinterpret_cast<PFN_vkVoidFunction>(destroyView);
if (std::strcmp(name, "vkDestroyImage") == 0) return reinterpret_cast<PFN_vkVoidFunction>(destroyImage);
if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(freeMemory);
if (std::strcmp(name, "vkCmdClearColorImage") == 0) return reinterpret_cast<PFN_vkVoidFunction>(clearImage);
return active->resolver(device, name);
}
Context& context;
PFN_vkGetDeviceProcAddr resolver;
bool failView;
bool failedView = false;
bool imageDestroyed = false;
bool memoryFreed = false;
std::uint32_t clears = 0;
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
std::vector<VkImageView> views;
std::vector<VkImageView> destroyedViews;
static inline StorageUploadProbe* active = nullptr;
};
void storageUploadLifetimeTests(const Device& device, Recorder& previous, bool failView) {
auto context = device.GetContext();
context.hostImportAlignment = 0;
TextureDetiler detiler(context);
context.detiler = &detiler;
GuestTextureResource resource{};
resource.width = 64;
resource.height = 64;
resource.mipCount = 1;
resource.tileMode = TextureTileMode::kLinear;
resource.dimension = TextureDimension::k2D;
resource.format = 56;
const auto guestBytes = static_cast<std::size_t>(DescribeSurface(resource).guestBytes);
std::vector<std::byte> texels(guestBytes + 256, std::byte{0x55});
std::vector<std::byte> keys(guestBytes / 256, std::byte{0});
resource.baseAddress = (reinterpret_cast<std::uintptr_t>(texels.data()) + 255) & ~std::uintptr_t{255};
resource.dccAddress = reinterpret_cast<std::uintptr_t>(keys.data());
DccKeyProof proof;
Require(ProvedClearKeys(resource, guestBytes, proof) == DccKeys::Clear0000, "storage lifetime fixture has no DCC clear");
StorageUploadProbe probe(context, failView);
Buffer readback(device.GetContext(), resource.width * resource.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
std::memset(readback.Bytes().data(), 0x55, readback.Bytes().size());
std::weak_ptr<StorageTexture> lifetime;
bool premature = false;
bool retained = true;
bool threw = false;
{
Recorder recorder(context);
recorder.Activate();
try {
auto image = StorageTexture::Create(context, detiler, resource, 0);
lifetime = image;
image.reset();
retained = !lifetime.expired();
} catch (const std::exception& error) {
if (!probe.FailedView() || std::string_view(error.what()).find("vkCreateImageView storage") == std::string_view::npos) throw;
threw = true;
}
premature = probe.Destroyed();
Require(probe.Clears() == 1 && recorder.Recording(), "storage creation did not record its initial clear");
const auto commands = recorder.Commands();
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
VkBufferImageCopy copy{};
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
copy.imageExtent = {resource.width, resource.height, 1};
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, probe.Image(), VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, &copy);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
recorder.Sync();
}
previous.Activate();
Require(threw == failView && probe.FailedView() == failView, "storage view failure injection did not take the requested path");
Require(std::all_of(readback.Bytes().begin(), readback.Bytes().end(), [](std::byte value) { return value == std::byte{0}; }), "recorded storage clear did not reach every texel");
Require(lifetime.expired() && probe.Released(), "storage upload resources outlived recorder teardown");
Require(!premature && retained, failView ? "failed storage creation released an image still referenced by its recorded clear" : "storage creation did not retain its image through the recorded clear");
std::cout << "Storage upload lifetime" << (failView ? " after view failure" : " after caller release") << ": passed\n";
}
void refreshProofTests(const Device& device, Recorder& recorder) {
const auto& base = device.GetContext();
if (base.hostImportAlignment == 0) {
@ -2987,7 +3273,7 @@ void refreshProofTests(const Device& device, Recorder& recorder) {
resource.dccAddress = address + surfaceBytes;
const auto proved = [] { return StorageTexture::RefreshesProved(); };
{
auto image = std::make_shared<StorageTexture>(context, detiler, resource, 0);
auto image = StorageTexture::Create(context, detiler, resource, 0);
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
const auto draw = [&](VkClearColorValue value) {
const auto commands = recorder.Commands();
@ -3032,7 +3318,7 @@ void refreshProofTests(const Device& device, Recorder& recorder) {
expectProved("the proof was not taken again after a key store");
auto other = resource;
other.dccAddress = 0;
auto pending = std::make_shared<StorageTexture>(context, detiler, other, 0);
auto pending = StorageTexture::Create(context, detiler, other, 0);
recorder.Keep(pending);
pending->MarkDirty();
expectFull("another image's pending results over the surface were answered from the proof");
@ -3041,7 +3327,7 @@ void refreshProofTests(const Device& device, Recorder& recorder) {
pending->MarkDirty();
expectFull("another image's pending results were answered from the proof");
Require(!AgcDriver::Graphics::PendingStorageOverlaps(address, surfaceBytes, image.get()), "the full refresh left another image's results pending");
auto fresh = std::make_shared<StorageTexture>(context, detiler, resource, 0);
auto fresh = StorageTexture::Create(context, detiler, resource, 0);
const auto before = proved();
fresh->Refresh();
Require(proved() == before, "a new image of the surface was answered from another image's proof");
@ -3127,7 +3413,7 @@ void unchangedCpuStampTests(const Device& device, Recorder& recorder) {
resource.dstSelW = 7;
Require(DescribeSurface(resource).guestBytes == surfaceBytes, "(c) unexpected surface size");
{
auto image = std::make_shared<StorageTexture>(context, detiler, resource, 0);
auto image = StorageTexture::Create(context, detiler, resource, 0);
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
const auto commands = recorder.Commands();
recorder.Keep(image);
@ -3240,7 +3526,7 @@ void settledBaselineTests(const Device& device, Recorder& recorder) {
if (pending) recorder.NotePendingWrite(address + 4096, 4096);
const auto before = recorder.Submissions();
{
auto image = std::make_shared<StorageTexture>(context, detiler, resource, 0);
auto image = StorageTexture::Create(context, detiler, resource, 0);
recorder.Keep(image);
if (pending) Require(recorder.Submissions() == before, "(b) an upload over a pending recorded store submitted the open batch to read its generation baseline");
}
@ -3330,7 +3616,7 @@ void importWindowTests(const Device& device, Recorder& recorder) {
Require(!HostImportCovers(base, address, bytes), "(w) the block is imported before the cache takes its generation");
const auto cached = CollectWritesUncached(address, surfaceBytes);
Require(cached != 0 && UnchangedSince(address, surfaceBytes, cached), "(w) the cache's generation is not current before the import");
auto image = std::make_shared<StorageTexture>(context, detiler, resource, 0);
auto image = StorageTexture::Create(context, detiler, resource, 0);
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
const auto commands = recorder.Commands();
recorder.Keep(image);
@ -4168,6 +4454,109 @@ void dataRefreshTests(const Device& device, Recorder& recorder) {
recorder.Sync();
}
void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
const auto& base = device.GetContext();
if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0 || !device.NullDescriptors()) {
std::cout << "4x color targets or nullDescriptor unavailable: multisampled target guest access not tested\n";
return;
}
constexpr std::uint32_t side = 64;
constexpr std::size_t targetBytes = 65536u;
constexpr std::size_t bytes = 5u * 65536u;
#ifdef _WIN32
void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
#else
void* block = std::aligned_alloc(65536, bytes);
#endif
Require(block != nullptr, "cannot allocate the multisampled target block");
std::memset(block, 0, bytes);
const auto address = reinterpret_cast<std::uint64_t>(block);
{
GuestAllocations::Mutation mutation;
mutation.Add(block, bytes, true, true, true);
}
struct Unregister {
const Context& context;
void* block;
~Unregister() {
ClearMultisampleTargets(context.device);
ClearCachedTextures(context.device);
{
GuestAllocations::Mutation mutation;
mutation.Remove(block);
}
#ifdef _WIN32
VirtualFree(block, 0, MEM_RELEASE);
#else
std::free(block);
#endif
}
} unregister{base, block};
TextureDetiler detiler(base);
auto context = base;
context.detiler = &detiler;
context.nullDescriptors = true;
TextureCache cache(context);
context.textureCache = &cache;
const auto refused = [](const std::function<void()>& access, const std::string& expected, const std::string& what) {
std::string message;
try {
access();
} catch (const std::exception& error) {
message = error.what();
}
Require(message.find(expected) != std::string::npos && message.find("multisampled color target") != std::string::npos, what + " of a resolved-only multisampled target's memory did not throw: " + message);
};
ColorTarget target{};
target.address = address;
target.surfaceAddress = address;
target.extent = {side, side};
target.surfaceExtent = target.extent;
target.format = VK_FORMAT_R8G8B8A8_UNORM;
target.bytes = targetBytes;
target.samples = 4;
{
std::lock_guard gpu(GpuMutex());
Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the multisampled target has no view");
recorder.Submit();
device.WaitQueue();
recorder.Sync();
}
std::array<std::byte, 64> texels{};
refused([&] { AgcDriver::GuestMemory::Read(address + 256u, texels); }, "a CPU read", "a CPU read");
refused([&] { RequireNoMultisampleTarget(address, 64u, "a DMA copy"); }, "a DMA copy", "a DMA copy");
ShaderRecompiler::RecompileResult program;
ShaderRecompiler::DescriptorBinding binding;
binding.kind = ShaderRecompiler::DescriptorKind::SampledImage;
binding.role = ShaderRecompiler::DescriptorRole::GuestImages;
binding.descriptorSet = 0;
binding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding;
binding.count = 1;
binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D;
binding.imageSamplers = {0};
binding.guestDescriptor = {
static_cast<std::uint32_t>(address >> 8u),
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (56u << 20u) | (((side - 1u) & 3u) << 30u),
((side - 1u) >> 2u) | ((side - 1u) << 14u),
0xfacu | (9u << 28u),
0u,
0u,
0u,
0u,
};
program.bindings.push_back(binding);
const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0};
refused([&] {
std::lock_guard gpu(GpuMutex());
ShaderResources resources(context, compute);
}, "a texture read", "a texture read");
refused([&] { AgcDriver::GuestMemory::Read(address + 3u * 65536u + 256u, texels); }, "a CPU read", "a CPU read in the padding of a later sample plane");
AgcDriver::GuestMemory::Read(address + 4u * 65536u, texels);
ClearMultisampleTargets(context.device);
AgcDriver::GuestMemory::Read(address + 256u, texels);
std::cout << "Multisampled target guest access tests passed\n";
}
void depthSurfaceProofTests(const Device& device, Recorder& recorder) {
const auto& base = device.GetContext();
if (!device.NullDescriptors()) {
@ -4440,7 +4829,7 @@ void singleCubeTests(const Device& device, Recorder& recorder) {
resource.baseAddress = reinterpret_cast<std::uint64_t>(surface);
const std::span<const std::byte> snapshot(surface, static_cast<std::size_t>(geometry.guestBytes));
Texture texture(context, detiler, resource, identity, snapshot);
auto storage = std::make_shared<StorageTexture>(context, detiler, resource, 0);
auto storage = StorageTexture::Create(context, detiler, resource, 0);
recorder.Keep(storage);
Texture storageView(context, storage, resource, identity);
for (std::uint32_t face = 0; face < 6; ++face) {
@ -4474,7 +4863,7 @@ void firstLayerViewTests(const Device& device, Recorder& recorder) {
auto* surface = reinterpret_cast<std::uint8_t*>((reinterpret_cast<std::uintptr_t>(memory.data()) + 255) & ~std::uintptr_t{255});
for (std::uint32_t layer = 0; layer < 3; ++layer) std::memset(surface + geometry.GuestLayerOffset(layer), 0x20 * (layer + 1), static_cast<std::size_t>(geometry.layerBytes));
resource.baseAddress = reinterpret_cast<std::uint64_t>(surface);
auto image = std::make_shared<StorageTexture>(withDetiler, detiler, resource, 0);
auto image = StorageTexture::Create(withDetiler, detiler, resource, 0);
recorder.Keep(image);
SampleProgram program(context, recorder);
expectRed(program.Red(image->FirstLayerView(0), VK_IMAGE_LAYOUT_GENERAL, 0.0f), 0x40 / 255.0f, "a first-layer view does not read the BASE_ARRAY layer");
@ -4515,7 +4904,7 @@ void atomicViewTests(const Device& device, Recorder& recorder) {
const auto geometry = DescribeSurface(resource);
std::vector<std::uint8_t> memory(static_cast<std::size_t>(geometry.guestBytes) + 256);
resource.baseAddress = (reinterpret_cast<std::uintptr_t>(memory.data()) + 255) & ~std::uintptr_t{255};
auto image = std::make_shared<StorageTexture>(withDetiler, detiler, resource, 0);
auto image = StorageTexture::Create(withDetiler, detiler, resource, 0);
recorder.Keep(image);
if (format == 56u) {
bool refused = false;
@ -4685,7 +5074,7 @@ void keysFillTests(const Device& device, Recorder& recorder) {
resource.dccAddress = keysAddress;
Require(DescribeSurface(resource).guestBytes == surfaceBytes, "the key fill surface has an unexpected size");
{
auto image = std::make_shared<StorageTexture>(context, detiler, resource, 0);
auto image = StorageTexture::Create(context, detiler, resource, 0);
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
const auto draw = [&](VkClearColorValue value) {
const auto commands = recorder.Commands();
@ -4798,7 +5187,7 @@ void denormalClearTests(const Device& device, Recorder& recorder) {
Require(ClearKeepsDenormals(context, VK_FORMAT_R16G16_SFLOAT) == keeps, "the denormal clear probe changed its answer");
Require(!ClearKeepsDenormals(context, VK_FORMAT_R32G32B32A32_SFLOAT), "a format without small floats probed as keeping denormals");
{
auto image = std::make_shared<StorageTexture>(context, detiler, resource, 0);
auto image = StorageTexture::Create(context, detiler, resource, 0);
const auto holds = [&](std::uint32_t texel) {
Buffer readback(context, side * side * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
const auto commands = recorder.Commands();
@ -4896,6 +5285,13 @@ void pipelineLibraryTests(const Device& device) {
state.colors.front().format = VK_FORMAT_R16G16B16A16_SFLOAT;
static_cast<void>(lookup());
Require(counters().built == std::array<std::uint64_t, 4>{2, 2, 3, 3}, "another target format rebuilt more than the fragment output library");
state.samples = 4;
state.colors.front().samples = 4;
const auto multisampled = lookup();
Require(counters().built == std::array<std::uint64_t, 4>{2, 2, 4, 4}, "a 4x pipeline shared the 1x fragment shader or fragment output library");
state.samples = 1;
state.colors.front().samples = 1;
Require(lookup() != multisampled && counters().built == std::array<std::uint64_t, 4>{2, 2, 4, 4}, "the 1x pipeline after a 4x one built its libraries again");
WaitForOptimizedPipelines(context.device);
const auto lto = std::getenv("APS5_NO_PIPELINE_LTO") == nullptr && context.pipelineLinkTimeOptimization;
Require(counters().optimized == (lto ? counters().linked : 0u), "a linked pipeline was not optimized in the background");
@ -4933,6 +5329,10 @@ int main(int argc, char** argv) {
std::cout << "host imports " << (PrepareImportWatch(device.GetContext()) == ImportWatch::Unwatch ? "are compared" : "stay watched") << '\n';
Recorder recorder(device.GetContext());
recorder.Activate();
if (argc == 2 && (std::string_view(argv[1]) == "--storage-upload-lifetime-only" || std::string_view(argv[1]) == "--storage-upload-failure-only")) {
storageUploadLifetimeTests(device, recorder, std::string_view(argv[1]) == "--storage-upload-failure-only");
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--cube-only") {
singleCubeTests(device, recorder);
std::cout << "Single cube snapshot and storage sampling tests passed\n";
@ -4974,11 +5374,20 @@ int main(int argc, char** argv) {
std::cout << "CMASK copied clear, write-back and failure tests passed\n";
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--multisample-only") {
multisampleGuestAccessTests(device, recorder);
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--draw-descriptors-only") {
drawDescriptorWriteTests(device, recorder);
std::cout << "Draw descriptor writes and snapshot overrides passed\n";
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--draw-snapshot-cap-only") {
drawSnapshotCapTests(device, recorder);
std::cout << "Draw snapshot cap tests passed\n";
return 0;
}
readTrackingTests(device, recorder);
writeSettledTests(device, recorder);
@ -4997,10 +5406,12 @@ int main(int argc, char** argv) {
resourceReadTests(device, recorder);
readWrittenStagingTests(device, recorder);
depthSurfaceProofTests(device, recorder);
multisampleGuestAccessTests(device, recorder);
misalignedSnapshotTests(device, recorder);
misalignedRegionTests(device, recorder);
drawSnapshotReuseTests(device, recorder);
drawDescriptorWriteTests(device, recorder);
drawSnapshotCapTests(device, recorder);
drawSnapshotEvictionTests(device);
drawInputReuseTests(device, recorder);
drawInputInPlaceTests(device, recorder);

View File

@ -23,6 +23,15 @@
#include <string>
#include <string_view>
#include <vector>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#endif
namespace {
@ -487,6 +496,63 @@ void verifyFloatModeSerialization() {
require(back.request.context.floatMode == mode, "the float mode did not survive serialization");
}
void verifyAtomicReplacement(const std::filesystem::path& directory) {
struct Reader {
#ifdef _WIN32
HANDLE file;
explicit Reader(const std::filesystem::path& path)
: file(CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)) {
require(file != INVALID_HANDLE_VALUE, "cannot retain the cache file");
}
~Reader() { CloseHandle(file); }
std::vector<std::byte> Read() {
require(SetFilePointerEx(file, {}, nullptr, FILE_BEGIN), "cannot seek the retained cache file");
std::array<std::byte, 32> bytes{};
DWORD count = 0;
require(ReadFile(file, bytes.data(), static_cast<DWORD>(bytes.size()), &count, nullptr), "cannot read the retained cache file");
return {bytes.begin(), bytes.begin() + count};
}
#else
std::ifstream file;
explicit Reader(const std::filesystem::path& path) : file(path, std::ios::binary) {
require(file.is_open(), "cannot retain the cache file");
}
std::vector<std::byte> Read() {
file.clear();
file.seekg(0);
require(!file.fail(), "cannot seek the retained cache file");
std::array<std::byte, 32> bytes{};
file.read(reinterpret_cast<char*>(bytes.data()), bytes.size());
require(file.eof() && !file.bad(), "cannot read the retained cache file");
return {bytes.begin(), bytes.begin() + file.gcount()};
}
#endif
Reader(const Reader&) = delete;
Reader& operator=(const Reader&) = delete;
};
const auto root = directory / "atomic-replacement";
const auto path = root / "entry.bin";
const std::vector<std::byte> original{std::byte{0x12}, std::byte{0x00}, std::byte{0x34}};
const std::vector<std::byte> replacement{std::byte{0xab}, std::byte{0xcd}, std::byte{0x00}, std::byte{0xef}};
require(WriteFileAtomically(path, original), "cannot write the initial cache entry");
{
Reader old(path);
require(old.Read() == original, "the retained reader did not read the original entry");
require(WriteFileAtomically(path, replacement), "cannot replace cache entry with a retained reader");
require(old.Read() == original, "replacement changed the retained original entry");
std::vector<std::byte> fresh;
require(ReadWholeFile(path, fresh) && fresh == replacement, "a fresh reader did not read the replacement entry");
Reader current(path);
require(current.Read() == replacement, "the replacement reader did not retain its entry");
require(std::filesystem::remove(path), "cannot delete the replacement while readers retain it");
require(!ReadWholeFile(path, fresh), "the deleted cache entry remains readable by path");
require(old.Read() == original && current.Read() == replacement, "deletion invalidated retained cache readers");
}
require(std::filesystem::is_empty(root), "atomic replacement left a temporary file behind");
require(std::filesystem::remove(root), "cannot remove the empty replacement directory");
}
void verifyStore() {
require(ShaderDiskCache::Enabled(), "the disk cache is not enabled");
SampleRequest sample;
@ -1034,6 +1100,7 @@ int main(int argc, char** argv) {
verifyDefaultDirectory(argv[0]);
setEnvironment("ANYPS5_NO_SHADER_CACHE", "0");
setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string());
verifyAtomicReplacement(directory);
verifyResultRoundTrip();
verifyEntryRoundTrip();
verifyArtifactStorageIsolation();

View File

@ -5,10 +5,13 @@
#include "Optimization/ResourceMaterializer.hpp"
#include "Optimization/ResourceProgram.hpp"
#include "Optimization/ShaderStageInputInfo.hpp"
#include "Optimization/SrtWalker.hpp"
#include "Optimization/SrtWalker/SrtDescriptorEvaluation.hpp"
#include "Optimization/SrtWalker/SrtEvaluator.hpp"
#include "Optimization/SrtWalker/SrtInstructionPredicates.hpp"
#include "Optimization/SrtWalker/SrtFlatSlotClasses.hpp"
#include "Optimization/SrtWalker.hpp"
#include "IntermediateRepresentation/IrBuilder.hpp"
#include "SpirvBackend/SpirvAnalysis.hpp"
#if ANYPS5_ENABLE_SPIRV_TOOLS
#include "SpirvBackend/SpirvOptimizer.hpp"
@ -443,6 +446,9 @@ void verifySignedSrtComparison() {
source.dwords[1] = &make(IrOpcode::SelectU32, IrType::U32, {&aboveMin, &greaterThanMin, &equalMin});
source.dwords[2] = &make(IrOpcode::SelectU32, IrType::U32, {&belowMaxValue, &belowMax, &equalMax});
plan.descriptorSources.push_back(source);
for (std::uint32_t dword = 0; dword < source.dwordCount; ++dword) {
require(SrtWalker().ValidateRuntimeValue(plan, source.dwords[dword]), "signed SRT comparison: the runtime validator rejects a descriptor dword the evaluator computes");
}
const std::array<std::uint32_t, 1> sourceIndex{0u};
const std::array<std::pair<std::uint32_t, std::array<std::uint32_t, 3>>, 4> cases{{
{0x80000000u, {0xau, 0xdu, 0xeu}},
@ -1356,6 +1362,101 @@ void verifyHalfWaveReduction() {
expectFailure([&] { static_cast<void>(pixel(umin, 8u, true)); }, "v_permlanex16_b32 from lanes 16-31 is outside the 8-lane host subgroup", "half-wave reduction: the scan built on an 8-lane subgroup");
}
void verifyNarrowVertexWave() {
using namespace ShaderRecompiler;
static constexpr std::array<std::uint32_t, 4> capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic};
static constexpr std::array<std::uint32_t, 4> userData{0u, 0u, 0u, 0x2020u};
const auto vertex = [](std::span<const std::uint32_t> code, std::uint32_t waveSize, std::uint32_t subgroupSize, std::uint32_t userDataBase) {
RecompileRequest request{};
request.shader = {ShaderStage::Vertex, 0x50000u, code, 0, {}};
request.context.waveSize = waveSize;
request.context.userDataBaseRegister = userDataBase;
if (userDataBase == 0u) request.context.userData = userData;
request.context.vertex = ShaderVertexStageInfo{};
request.target.vulkanVersion = 0x00401000u;
request.target.spirvVersion = 0x00010300u;
request.target.subgroupSize = subgroupSize;
request.target.supportedCapabilities = capabilities;
request.target.fragmentShaderBarycentricEnabled = false;
request.layout.pushConstantSizeBytes = 128;
return request;
};
const auto prepared = [&vertex](std::span<const std::uint32_t> code, std::uint32_t subgroupSize, std::uint32_t userDataBase = 8u) {
return GetPreparedArtifact(*PrepareShader(vertex(code, 64u, subgroupSize, userDataBase))).spirv;
};
const auto prepares = [&prepared](std::span<const std::uint32_t> code, std::uint32_t subgroupSize, std::uint32_t userDataBase = 8u) {
return !prepared(code, subgroupSize, userDataBase).empty();
};
const auto reductions = [](const std::vector<std::uint32_t>& spirv) {
std::uint32_t count = 0;
for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) {
if ((spirv[cursor] & 0xffffu) == spv::OpGroupNonUniformBitwiseOr && (spirv[cursor] >> 16u) == 6u && spirv[cursor + 4u] == spv::GroupOperationReduce) ++count;
}
return count;
};
const std::vector<std::uint32_t> culling{
0xd7650015u, 0x000100c1u, 0xd7660015u, 0x00022ac1u, 0x7e0e0280u, 0x7da206f9u, 0x81060015u, 0x360e0a81u,
0xbefe04c1u, 0xd4c50002u, 0x00020e80u, 0xd763000fu, 0x00022a81u, 0xd5010000u, 0x000a1e80u,
0x380000fau, 0xff091100u, 0x380000fau, 0xff091200u, 0x380000fau, 0xff091400u, 0x380000fau, 0xff091800u,
0xd778000fu, 0x03058300u, 0x38001f00u, 0xd7600002u, 0x00013f00u, 0xd760006bu, 0x00017f00u, 0x88026b02u,
0x7e020202u, 0xf80008cfu, 0x01010101u, 0xbf810000u,
};
const auto patched = [&culling](std::initializer_list<std::pair<std::size_t, std::uint32_t>> words) {
auto code = culling;
for (const auto& [index, word] : words) code[index] = word;
return code;
};
const char* lane63 = "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup";
require(reductions(prepared(culling, 32u)) == 1u, "narrow vertex wave: the NGG culling scan, keyed on lanes below the primitive count in s3, did not read lane 31 as a subgroup OR on 32 lanes");
const auto wide64 = prepared(culling, 64u);
require(!wide64.empty() && reductions(wide64) == 0u, "narrow vertex wave: the NGG culling scan, keyed on lanes below 64 on 64 lanes, did not keep its DPP scan");
require(prepares(patched({{6, 0x80060015u}}), 32u), "narrow vertex wave: the scan keyed on lanes below the vertex count in s3 did not prepare on 32 lanes");
static constexpr std::array<std::uint32_t, 3> noArithmetic{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle};
const auto otherKeys = patched({{7, 0x360e0a82u}});
auto withoutArithmetic = vertex(otherKeys, 64u, 32u, 8u);
withoutArithmetic.target.supportedCapabilities = noArithmetic;
const auto shuffled = GetPreparedArtifact(*PrepareShader(withoutArithmetic)).spirv;
require(!shuffled.empty() && reductions(shuffled) == 0u, "narrow vertex wave: without subgroup arithmetic, the scan did not keep lane 31 as a shuffle with lane 63 read as 0");
require(prepares(patched({{5, 0x7da82aa0u}, {6, 0xbf800000u}}), 32u), "narrow vertex wave: the scan keyed on lanes below 32 did not prepare on 32 lanes");
expectFailure([&] { static_cast<void>(prepares(patched({{5, 0x7da82aa1u}, {6, 0xbf800000u}}), 32u)); }, lane63, "narrow vertex wave: a scan keyed on lanes below 33 read lane 63");
expectFailure([&] { static_cast<void>(prepares(culling, 32u, 0u)); }, lane63, "narrow vertex wave: a scan keyed on a count from user data read lane 63");
expectFailure([&] { static_cast<void>(prepares(patched({{6, 0x81060005u}}), 32u)); }, lane63, "narrow vertex wave: a scan keyed on the vertex index instead of the lane read lane 63");
auto otherRegister = patched({{21, 0x380200fau}, {24, 0x03058301u}, {25, 0x38001f01u}});
otherRegister.insert(otherRegister.begin(), 0x7e020305u);
require(reductions(prepared(otherRegister, 32u)) == 1u, "narrow vertex wave: a scan whose bound_ctrl:1 step is written to another register did not read lane 31 as a subgroup OR on 32 lanes");
otherRegister[23] = 0xff011800u;
expectFailure([&] { static_cast<void>(prepares(otherRegister, 32u)); }, lane63, "narrow vertex wave: a scan step written to another register without bound_ctrl:1, which keeps that register's lanes outside the row, read lane 63");
auto threeSteps = culling;
threeSteps.erase(threeSteps.begin() + 21, threeSteps.begin() + 23);
const auto partialScan = prepared(threeSteps, 32u);
require(!partialScan.empty() && reductions(partialScan) == 0u, "narrow vertex wave: lane 63 of row_shr 1/2/4 steps over keys of 0 in lanes 32-63 did not read 0, or lane 31 became a subgroup OR");
threeSteps[5] = 0x7da82aa1u;
threeSteps[6] = 0xbf800000u;
expectFailure([&] { static_cast<void>(prepares(threeSteps, 32u)); }, lane63, "narrow vertex wave: lane 63 of row_shr 1/2/4 steps over keys of lanes below 33 read 0");
std::array<std::vector<std::uint32_t>, 2> narrow;
std::array<std::vector<std::uint32_t>, 2> wide;
for (const bool narrowFirst : {false, true}) {
const std::vector<std::uint32_t> code{0xbf800001u + static_cast<std::uint32_t>(narrowFirst), 0x7e020203u, 0xf80008cfu, 0x01010101u, 0xbf810000u};
const auto narrowRequest = vertex(code, 64u, 32u, 8u);
const auto wideRequest = vertex(code, 64u, 64u, 8u);
ShaderPreparationContext preparation;
const auto first = PrepareShader(narrowFirst ? narrowRequest : wideRequest, &preparation);
const auto second = PrepareShader(narrowFirst ? wideRequest : narrowRequest, &preparation);
const auto& narrowHandle = narrowFirst ? *first : *second;
const auto& wideHandle = narrowFirst ? *second : *first;
require(first->source != second->source, "narrow vertex wave: the program prepared for 32 and for 64 lanes shares one translated source");
require(MatchesPreparedShader(narrowRequest, narrowHandle) && MatchesPreparedShader(wideRequest, wideHandle), "narrow vertex wave: a prepared program did not match its own request");
require(!MatchesPreparedShader(narrowRequest, wideHandle) && !MatchesPreparedShader(wideRequest, narrowHandle), "narrow vertex wave: a program prepared for one subgroup width matched a request for the other");
narrow[narrowFirst ? 1u : 0u] = GetPreparedArtifact(narrowHandle).spirv;
wide[narrowFirst ? 1u : 0u] = GetPreparedArtifact(wideHandle).spirv;
}
require(narrow[0] == narrow[1] && wide[0] == wide[1], "narrow vertex wave: preparing for one subgroup width after the other gave another program than preparing it first");
require(narrow[0] != wide[0], "narrow vertex wave: s3 of a wave64 vertex program was the same on 32 and 64 lanes");
const std::vector<std::uint32_t> wave32{0x7e020203u, 0xf80008cfu, 0x01010101u, 0xbf810000u};
require(GetPreparedArtifact(*PrepareShader(vertex(wave32, 32u, 32u, 8u))).spirv == GetPreparedArtifact(*PrepareShader(vertex(wave32, 32u, 64u, 8u))).spirv, "narrow vertex wave: s3 of a wave32 vertex program depended on the subgroup width");
}
void verifyMeshConfiguration() {
using namespace ShaderRecompiler;
ShaderMeshInputInfo list;
@ -2655,6 +2756,36 @@ void verifyFunctionLdsBound() {
require(unsized == FunctionLdsDwordLimit, "function LDS: an access without a known width must keep the full array");
}
void verifySrtReadPatching() {
using namespace ShaderRecompiler;
IrProgram program;
program.Resources().stage = IrShaderStage::Compute;
MemoryInfo memory;
memory.kind = ResourceKind::ScalarAddress;
program.Resources().memoryInfo.push_back(memory);
auto& block = program.CreateBlock();
program.SetEntryBlock(block);
program.BlockOrder().push_back(&block);
IrBuilder builder(program);
builder.SetInsertionPoint(block);
auto& address = builder.Emit(IrOpcode::GetAddressResource, IrType::AddressResource, {&builder.Constant(0u), &builder.Constant(0u)});
auto& active = program.CreateValue(IrOpcode::Void, IrType::U1);
active.SetImmediateBool(true);
auto& read = builder.Emit(IrOpcode::LoadAddressU32, IrType::U32, {&address, &builder.Constant(0u), &builder.Constant(0u), &active}, MemoryFlags{0u, 0u});
auto& repeatedUse = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&read, &read});
auto& secondUser = builder.Emit(IrOpcode::IMul32, IrType::U32, {&read, &builder.Constant(2u)});
SrtWalker{}.BuildPlan(program);
require(program.Resources().srtReads.size() == 1u, "SRT read patching: the scalar read was not planned");
const auto* replacement = repeatedUse.Argument(0);
require(replacement->Opcode() == IrOpcode::ReadConst, "SRT read patching: the uses do not reference a flat read");
require(repeatedUse.Argument(1) == replacement && secondUser.Argument(0) == replacement, "SRT read patching: not every operand uses the same replacement");
const auto& remainingUses = read.OperandUses();
require(remainingUses.size() == 1u && remainingUses.front().user->Opcode() == IrOpcode::ReferenceU32, "SRT read patching: the original read retains non-planning uses");
}
struct NestedRequest {
std::vector<std::uint32_t> code;
std::array<std::uint32_t, 2> userData{};
@ -2912,6 +3043,24 @@ void verifyCubeDepthCompareMode() {
require(modes[selected].cube && (modes[selected].emulatedCompare & EmulatedCompare::Enabled) != 0u, "cube color comparison did not select its emulated runtime mode");
}
void verifySrgbDepthCompareModes() {
using namespace ShaderRecompiler;
for (const auto decodeFormats : {0u, 1u, 2u, 3u}) {
ImageResource image{};
image.resourceClass = ImageResourceClass::Sampled;
image.dimension = RdnaImageDimension::Dim2D;
image.depthCompare = true;
image.srgbDecodeCompatible = false;
image.srgbDecodeFormats = decodeFormats;
const auto modes = ResourceMaterializer::RuntimeImageModes(image);
for (const auto format : {128u, 129u, 130u}) {
const DescriptorValue descriptor{{0x100u, format << 20u, 0u, 0x90000facu, 0u, 0u, 0u, 0u}, 8u};
const auto selected = ResourceMaterializer::RuntimeImageMode(image, descriptor, modes);
require(!modes[selected].srgbDecode && (modes[selected].emulatedCompare & EmulatedCompare::Enabled) != 0u, "sRGB color comparison did not select its emulated runtime mode independently of decode support");
}
}
}
int main(int argc, char** argv) {
try {
using namespace ShaderRecompiler;
@ -2925,6 +3074,11 @@ int main(int argc, char** argv) {
std::cout << "Bindless mapping, indexing capabilities and strict validation passed\n";
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--srt-read-patching") {
verifySrtReadPatching();
std::cout << "SRT read patching replaces repeated and multiple uses\n";
return 0;
}
require(argc == 1, "unknown shader memory test arguments");
verifyRegisterSources();
verifyEvaluatedValues();
@ -2939,6 +3093,7 @@ int main(int argc, char** argv) {
verifyProgramCounterRelativeData();
verifyLanesOutsideHostSubgroup();
verifyHalfWaveReduction();
verifyNarrowVertexWave();
verifyMeshConfiguration();
verifyPixelInputs();
verifyPixelRequestSerialization();
@ -2956,8 +3111,10 @@ int main(int argc, char** argv) {
verifyTwoLaneUniformValues();
verifyBdaReadFallbackFunctions();
verifyFunctionLdsBound();
verifySrtReadPatching();
verifyGuardedNullPointers();
verifyCubeDepthCompareMode();
verifySrgbDepthCompareModes();
#if ANYPS5_ENABLE_SPIRV_TOOLS
const std::vector<std::uint32_t> minimalSpirv{
0x07230203u, 0x00010000u, 0u, 5u, 0u,

View File

@ -118,6 +118,59 @@ void smallStores() {
require(StoreOwnBytes(0, std::span<const std::byte>{}) == 0, "empty store produced a version");
}
void partialCollect() {
constexpr std::size_t page = 4096;
Mapping mapping(page * 4);
auto* data = static_cast<volatile std::byte*>(mapping.data);
#ifdef _WIN32
void* alias = nullptr;
require(sceKernelMapDirectMemory(&alias, mapping.size, 3, 0, mapping.physical, 65536) == 0, "partial collect alias failed");
#endif
require(WriteWatched(), "write tracking unavailable for partial collect");
const auto before = CollectWritesUncached(mapping.Address(), mapping.size);
require(before != 0, "partial collect range is not watched");
data[page * 3] = std::byte{91};
require(CollectWritesUncached(mapping.Address(), page) != 0, "prefix collect failed");
require(!UnchangedSinceCollected(mapping.Address() + page * 3, page, before), "partial collect lost a neighboring guest write");
const auto settled = CollectWritesUncached(mapping.Address(), mapping.size);
require(UnchangedSinceCollected(mapping.Address(), mapping.size, settled), "fully collected shared range remained dirty");
#ifdef _WIN32
const auto collect = [&](void* view, std::size_t offset, std::size_t bytes, std::size_t capacity, bool clear) {
std::array<void*, 4> pages{};
std::size_t count = capacity;
const auto address = reinterpret_cast<std::uintptr_t>(view);
require(GuestArena::GuestArenaCollectWrites_nid_postfix(address + offset, bytes, pages.data(), &count, clear), "shared subpage collection failed");
std::vector<std::size_t> offsets;
for (std::size_t i = 0; i < count; ++i) offsets.push_back(reinterpret_cast<std::uintptr_t>(pages[i]) - address);
return offsets;
};
const std::vector<std::size_t> all{0, page, page * 2, page * 3};
collect(alias, 0, mapping.size, 4, true);
data[page * 3] = std::byte{92};
require(collect(mapping.data, 0, page, 4, false) == std::vector<std::size_t>{0}, "probe missed a shared write");
require(collect(mapping.data, 0, page, 4, false) == std::vector<std::size_t>{0}, "probe consumed a shared write");
require(collect(mapping.data, 0, page, 4, true) == std::vector<std::size_t>{0}, "prefix reset missed a shared write");
require(collect(alias, 0, mapping.size, 4, false) == all, "prefix reset consumed an alias write");
require(collect(mapping.data, page * 3, page, 4, true) == std::vector<std::size_t>{page * 3}, "prefix reset consumed a neighboring write");
require(collect(mapping.data, page * 3, page, 4, false).empty(), "reset subpage remained dirty");
require(collect(mapping.data, page, page * 2, 4, true) == std::vector<std::size_t>{page, page * 2}, "partial resets consumed intervening writes");
require(collect(mapping.data, 0, mapping.size, 4, false).empty(), "collected subpages remained dirty");
require(collect(alias, 0, mapping.size, 4, true) == all, "collecting one view consumed an alias write");
static_cast<volatile std::byte*>(alias)[page] = std::byte{93};
require(collect(mapping.data, page * 2, page, 4, true) == std::vector<std::size_t>{page * 2}, "alias write did not invalidate the other view");
require(collect(mapping.data, 0, mapping.size, 4, true) == std::vector<std::size_t>{0, page, page * 3}, "full collect after an interior reset did not preserve pending subpages");
require(collect(mapping.data, 0, mapping.size, 4, false).empty(), "full collect left pending subpages");
static_cast<volatile std::byte*>(alias)[page * 3] = std::byte{94};
require(collect(mapping.data, 0, mapping.size, 1, true) == std::vector<std::size_t>{0}, "limited collect returned the wrong prefix");
require(collect(mapping.data, 0, mapping.size, 4, false) == all, "limited collect consumed unreported writes");
require(collect(mapping.data, page, page * 3, 3, true) == std::vector<std::size_t>{page, page * 2, page * 3}, "limited collect continuation lost writes");
require(collect(mapping.data, 0, page, 1, true) == std::vector<std::size_t>{0}, "limited collect consumed its unreset prefix");
require(collect(mapping.data, 0, mapping.size, 4, false).empty(), "limited collection did not settle");
require(sceKernelMunmap(alias, mapping.size) == 0, "partial collect alias unmap failed");
#endif
std::cout << "Partial shared write collection tests passed\n";
}
template<class TAction> void fails(TAction&& fn) {
bool failed = false;
try { fn(); } catch (const std::runtime_error&) { failed = true; }
@ -208,7 +261,14 @@ void run() {
int main(int argc, char** argv) {
try {
if (argc == 2 && std::string_view(argv[1]) == "--benchmark") benchmark();
if (argc == 2 && std::string_view(argv[1]) == "--partial-collect") {
if (!WriteWatched()) {
std::cout << "Partial shared write collection skipped: write tracking unavailable\n";
return 77;
}
partialCollect();
}
else if (argc == 2 && std::string_view(argv[1]) == "--benchmark") benchmark();
else if (argc == 2 && std::string_view(argv[1]) == "--benchmark-labels") benchmark(true);
else if (argc == 2 && std::string_view(argv[1]) == "--benchmark-fallback") benchmark(false, false);
else if (argc == 2 && std::string_view(argv[1]) == "--benchmark-labels-fallback") benchmark(true, false);

View File

@ -3,6 +3,7 @@
#include <cstdio>
#include <exception>
#include <iterator>
#include <map>
#include <stdexcept>
#include <string>
#include <utility>
@ -104,6 +105,75 @@ void requireExactPostDominators(const ControlFlowGraph& graph, const char* name)
}
}
void requireDominatedMerges(const ControlFlowGraph& graph, const char* name) {
for (const auto& block : graph.blocks) {
const auto merge = block.terminator.mergeBlock;
if (merge != InvalidControlFlowId && !graph.Dominates(block.id, merge)) {
throw std::runtime_error(std::string(name) + ": header " + std::to_string(block.id) + " does not dominate its merge " + std::to_string(merge));
}
}
}
using Routes = std::map<std::uint32_t, bool>;
std::uint32_t originalBlock(const ControlFlowGraph& graph, std::uint32_t blockId, Routes& routes) {
for (std::size_t step = 0; step <= graph.blocks.size(); ++step) {
const auto& block = graph.FindBlock(blockId);
const auto& terminator = block.terminator;
if (block.instructionBegin != block.instructionEnd) return blockId;
if (terminator.gotoValue >= 0) routes[terminator.gotoVariable] = terminator.gotoValue != 0;
if (terminator.kind == TerminatorKind::Branch) {
blockId = terminator.trueBlock;
} else if (terminator.kind == TerminatorKind::ConditionalBranch && terminator.condition == BranchCondition::GotoVariable && routes.contains(terminator.gotoVariable)) {
blockId = routes.at(terminator.gotoVariable) ? terminator.trueBlock : terminator.falseBlock;
} else {
break;
}
}
throw std::runtime_error("block " + std::to_string(blockId) + " does not lead to an original block");
}
void requireOriginalBranches(const ControlFlowGraph& graph, const std::vector<std::vector<std::uint32_t>>& successors, const char* name) {
std::vector<bool> seen(successors.size(), false);
Routes entryRoutes;
std::vector<std::pair<std::uint32_t, Routes>> pending{{originalBlock(graph, graph.entryBlock, entryRoutes), entryRoutes}};
std::vector<std::pair<std::uint32_t, Routes>> visited;
while (!pending.empty()) {
auto state = std::move(pending.back());
pending.pop_back();
if (std::find(visited.begin(), visited.end(), state) != visited.end()) continue;
visited.push_back(state);
const auto& block = graph.FindBlock(state.first);
const auto original = block.instructionBegin / 2;
std::vector<std::uint32_t> branchTargets;
if (block.terminator.kind == TerminatorKind::Branch) branchTargets = {block.terminator.trueBlock};
if (block.terminator.kind == TerminatorKind::ConditionalBranch) branchTargets = {block.terminator.trueBlock, block.terminator.falseBlock};
std::vector<std::uint32_t> targets;
for (const auto target : branchTargets) {
auto routes = state.second;
const auto reached = originalBlock(graph, target, routes);
targets.push_back(graph.FindBlock(reached).instructionBegin / 2);
pending.emplace_back(reached, std::move(routes));
}
if (targets != successors[original]) {
throw std::runtime_error(std::string(name) + ": block " + std::to_string(original) + " no longer branches to its original targets");
}
seen[original] = true;
}
if (std::find(seen.begin(), seen.end(), false) != seen.end()) {
throw std::runtime_error(std::string(name) + ": an original block is missing");
}
}
void requireStructuredLoop(const std::vector<std::vector<std::uint32_t>>& successors, const char* name) {
auto graph = makeGraph(successors);
Structurizer{}.Structurize(graph);
requireExactPostDominators(graph, name);
requireStructuredBranches(graph, name);
requireDominatedMerges(graph, name);
requireOriginalBranches(graph, successors, name);
}
std::vector<std::vector<std::uint32_t>> originalPostDominators(const ControlFlowGraph& graph) {
std::vector<std::uint32_t> all;
for (std::uint32_t id = 0; id < graph.blocks.size(); ++id) all.push_back(id);
@ -210,6 +280,13 @@ void requireOriginalDominanceSets(ControlFlowGraph graph, const char* name) {
int main() {
try {
auto largeBlocks = makeGraph({{1}, {2, 3}, {1, 4}, {5, 6}, {1, 7}, {8, 7}, {9, 4}, {10}, {6, 10}, {10}, {}});
for (auto& block : largeBlocks.blocks) block.estimatedSpirvWords = 1000u;
Structurizer{}.Structurize(largeBlocks);
requireStructuredBranches(largeBlocks, "large loop exit blocks retain the baseline result");
auto exitJunction = makeGraph({{1}, {2, 3}, {9}, {4, 6}, {5, 9}, {1}, {7, 8}, {9}, {1}, {}});
Structurizer{}.Structurize(exitJunction);
requireStructuredBranches(exitJunction, "shared loop exit junctions retain structured branches");
auto nested = makeGraph({{1}, {2}, {5, 3}, {5, 4}, {7}, {6, 7}, {}, {1}});
Structurizer{}.Structurize(nested);
requireExactPostDominators(nested, "nested selections");
@ -238,17 +315,19 @@ int main() {
Structurizer{}.Structurize(innerEndingExit);
requireExactPostDominators(innerEndingExit, "an inner loop exit that ends the program");
requireStructuredBranches(innerEndingExit, "an inner loop exit that ends the program");
requireStructuredLoop({{1}, {2, 4}, {3, 4}, {4, 5}, {1, 6}, {1}, {}}, "nested selections in a loop with two latches");
requireStructuredLoop({{1}, {5, 2}, {5, 3}, {6, 4}, {7, 5}, {8, 1}, {1}, {1}, {}}, "nested selections on the false side in a loop with three latches");
requireStructuredLoop({{1}, {2, 5}, {3, 4}, {5, 6}, {5}, {1, 7}, {1}, {}}, "an if-else in a loop whose one arm continues");
requireStructuredLoop({{1}, {2}, {3, 4}, {2, 5}, {}, {1}}, "an inner loop that returns or continues its outer loop");
requireStructuredLoop({{1}, {2, 3}, {4, 5}, {}, {3, 6}, {1}, {2}}, "an inner loop that leaves both loops or continues its outer loop");
requireStructuredLoop({{1, 3}, {2, 4}, {3, 1}, {4}, {}}, "a loop whose latch exits into a block the branch before the loop also reaches");
requireStructuredLoop({{1, 2}, {3, 2}, {4}, {4, 1}, {}}, "a loop whose header exits into a block the branch before the loop also reaches");
requireStructuredLoop({{1, 2}, {4}, {3, 1}, {4, 2}, {}}, "a loop whose header exits into a block the branch before the loop reaches first");
auto earlyReturn = makeGraph({{2, 1}, {4, 2}, {3, 5}, {5}, {}, {}});
Structurizer{}.Structurize(earlyReturn);
requireExactPostDominators(earlyReturn, "an early return inside a selection that joins its parent's merge");
requireStructuredBranches(earlyReturn, "an early return inside a selection that joins its parent's merge");
for (const auto& block : earlyReturn.blocks) {
const auto merge = block.terminator.mergeBlock;
if (merge != InvalidControlFlowId && !earlyReturn.Dominates(block.id, merge)) {
std::fprintf(stderr, "an early return inside a selection: header %u does not dominate its merge %u\n", block.id, merge);
return 1;
}
}
requireDominatedMerges(earlyReturn, "an early return inside a selection that joins its parent's merge");
std::vector<std::vector<std::uint32_t>> diamonds;
for (std::uint32_t diamond = 0; diamond < 50u; ++diamond) {
const auto head = diamond * 3u;
@ -260,6 +339,7 @@ int main() {
auto chain = makeGraph(diamonds);
Structurizer{}.Structurize(chain);
requireExactPostDominators(chain, "a chain of fifty selections");
requireStructuredLoop({{1}, {2, 8}, {3, 5}, {4, 8}, {7}, {6, 7}, {7, 8}, {1}, {}}, "nested selections sharing a loop continuation");
auto sharedDiscard = makeGraph({{5, 1}, {4, 2}, {7, 3}, {7, 4}, {5}, {7, 6}, {}, {}});
const std::vector<std::uint32_t> sharedDiscardWords{444, 1944, 68, 1128, 8, 2084, 148, 28};
for (auto& block : sharedDiscard.blocks) {

View File

@ -629,6 +629,30 @@ void testProgramWorkgroupUse() {
check(!AgcDriver::DriverDetail::ProgramUsesWorkgroup(entry, 2) && AgcDriver::DriverDetail::ProgramUsesWorkgroup(entry, 0), "the workgroup check did not start at the program's entry");
}
void testRegisteredVertexWorkgroupRouting() {
using namespace AgcDriver::DriverDetail;
ShaderSnapshot snapshot{0x20000, 0x30000, 2, {0xd8340000u, 0x00000100u, 0xbf810000u, 0xbf810000u}, {}};
snapshot.header.resize(sizeof(Shader));
const ShaderRegistry registry{{snapshot.codeAddress, {std::make_shared<const ShaderSnapshot>(std::move(snapshot))}}};
AgcDriver::QueueState queue{};
queue.shader = {{0xc8, 0x200}, {0xc9, 0}};
queue.context[0x2d5] = 0x2000;
check(VertexWorkgroupRequired(queue, registry), "a registered NGG vertex program using LDS did not need a workgroup");
for (const auto routing : {0u, 0x2020u, 0x2004u, 0x02002000u}) {
queue.context[0x2d5] = routing;
check(!VertexWorkgroupRequired(queue, registry), "legacy, geometry, tessellation or passthrough routing changed to an NGG vertex workgroup");
}
queue.context[0x2d5] = 0x2000;
auto unaligned = registry;
const auto aligned = unaligned.begin()->second.front();
ShaderSnapshot shifted{0x1ffff, 0x30000, 2, aligned->code, aligned->header};
unaligned.clear();
unaligned[shifted.codeAddress].push_back(std::make_shared<const ShaderSnapshot>(std::move(shifted)));
check(expectFailure([&] { static_cast<void>(VertexWorkgroupRequired(queue, unaligned)); }).find("not dword aligned") != std::string::npos, "an unaligned registered entry was accepted");
queue.shader[0xc9] = 0x100;
check(expectFailure([&] { static_cast<void>(VertexWorkgroupRequired(queue, registry)); }).find("reserved graphics program address") != std::string::npos, "reserved NGG program address bits were accepted");
}
void testWorkerFailure() {
std::array<std::uint32_t, 5> words{0xc0031500, 1, 1, 1, 0x41};
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
@ -722,6 +746,7 @@ int main() {
#endif
testProgramSnapshots();
testProgramWorkgroupUse();
testRegisteredVertexWorkgroupRouting();
testEvents();
testValidation();
testClearState();

View File

@ -0,0 +1,123 @@
#include "Recompiler.hpp"
#include <spirv/unified1/spirv.hpp>
#include <array>
#include <cstdint>
#include <cstdio>
#include <exception>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
namespace {
using namespace ShaderRecompiler;
constexpr std::uint32_t MoveM0Two = 0xbefc0382u;
constexpr std::uint32_t MoveV4One = 0x7e0802f2u;
constexpr std::uint32_t MoveV5Two = 0x7e0a02f4u;
constexpr std::uint32_t MoveV6Four = 0x7e0c02f6u;
constexpr std::uint32_t MovrelsV1V4 = 0x7e028704u;
constexpr std::uint32_t MovreldV4V1 = 0x7e088501u;
constexpr std::uint32_t MoveV1V5 = 0x7e020305u;
constexpr std::uint32_t EndProgram = 0xbf810000u;
struct Shape {
std::size_t functionArrays = 0;
std::uint32_t arrayLength = 0;
std::size_t accessChains = 0;
std::size_t selects = 0;
};
void Require(bool condition, const std::string& message) {
if (!condition) throw std::runtime_error(message);
}
std::vector<std::uint32_t> RecompileProgram(std::span<const std::uint32_t> code) {
const std::array<std::uint32_t, 7> capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformVote, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses};
const std::array<std::uint32_t, 4> userData{};
const std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
RecompileRequest request{};
request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}};
request.context.waveSize = 32;
request.context.userDataBaseRegister = 0;
request.context.userData = userData;
request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 4u, {false, false, false}, false, 1u};
request.target.vulkanVersion = 0x00403000u;
request.target.spirvVersion = 0x00010600u;
request.target.subgroupSize = 32;
request.target.bdaAbiVersion = 1;
request.target.supportedCapabilities = capabilities;
request.target.supportedExtensions = extensions;
request.target.maxWorkgroupSize = {1024u, 1024u, 64u};
request.target.maxWorkgroupInvocations = 1024;
request.target.maxWorkgroupSharedMemoryBytes = 49152;
request.layout = {0, 0, 0, 128};
request.useCache = false;
auto result = Recompile(request);
Require(!result.spirv.empty(), "the relative-addressing program did not recompile");
return std::vector<std::uint32_t>(result.spirv.Words().begin(), result.spirv.Words().end());
}
Shape Scan(std::span<const std::uint32_t> words) {
Shape shape;
std::vector<std::pair<std::uint32_t, std::uint32_t>> constants;
std::vector<std::pair<std::uint32_t, std::uint32_t>> arrays;
std::vector<std::pair<std::uint32_t, std::uint32_t>> functionPointers;
for (std::size_t cursor = 5; cursor < words.size();) {
const auto count = words[cursor] >> spv::WordCountShift;
const auto op = static_cast<spv::Op>(words[cursor] & spv::OpCodeMask);
Require(count != 0 && cursor + count <= words.size(), "truncated SPIR-V instruction");
if (op == spv::OpConstant && count == 4) constants.emplace_back(words[cursor + 2], words[cursor + 3]);
if (op == spv::OpTypeArray) arrays.emplace_back(words[cursor + 1], words[cursor + 3]);
if (op == spv::OpTypePointer && words[cursor + 2] == spv::StorageClassFunction) functionPointers.emplace_back(words[cursor + 1], words[cursor + 3]);
if (op == spv::OpVariable && words[cursor + 3] == spv::StorageClassFunction) {
for (const auto& [pointer, pointee] : functionPointers) {
if (pointer != words[cursor + 1]) continue;
for (const auto& [array, length] : arrays) {
if (array != pointee) continue;
++shape.functionArrays;
for (const auto& [constant, value] : constants) {
if (constant == length) shape.arrayLength = value;
}
}
}
}
if (op == spv::OpAccessChain) ++shape.accessChains;
if (op == spv::OpSelect) ++shape.selects;
cursor += count;
}
return shape;
}
bool run(const char* name, bool (*test)()) {
try {
if (test()) return true;
std::fprintf(stderr, "%s\n", name);
} catch (const std::exception& error) {
std::fprintf(stderr, "%s: %s\n", name, error.what());
}
return false;
}
}
int main() {
bool passed = true;
passed &= run("a relative read and write index a function-scope window instead of selecting over the registers", [] {
const std::array<std::uint32_t, 8> code{MoveM0Two, MoveV4One, MoveV5Two, MoveV6Four, MovrelsV1V4, MovreldV4V1, MoveV1V5, EndProgram};
const auto shape = Scan(RecompileProgram(code));
if (shape.functionArrays != 1 || shape.arrayLength != 3 || shape.accessChains < 2 || shape.selects > 4) std::fprintf(stderr, " window shape: arrays %zu length %u access chains %zu selects %zu\n", shape.functionArrays, shape.arrayLength, shape.accessChains, shape.selects);
return shape.functionArrays == 1 && shape.arrayLength == 3 && shape.accessChains >= 2 && shape.selects <= 4;
});
passed &= run("a program without relative addressing keeps its registers in SSA values", [] {
const std::array<std::uint32_t, 5> code{MoveV4One, MoveV5Two, MoveV6Four, MoveV1V5, EndProgram};
const auto shape = Scan(RecompileProgram(code));
return shape.functionArrays == 0 && shape.accessChains == 0;
});
return passed ? 0 : 1;
}

View File

@ -0,0 +1,108 @@
#include "RdnaDecoder/RdnaInstructionDecoder.hpp"
#include <cstdint>
#include <cstdio>
#include <exception>
#include <string>
#include <vector>
using namespace ShaderRecompiler;
namespace {
struct Accepted {
std::vector<std::uint32_t> words;
RdnaOpcode op;
const char* name;
RdnaOperandKind source0Kind;
std::uint32_t source0Value;
std::uint32_t attribute;
std::uint32_t channel;
bool absolute;
bool negate;
bool clamp;
std::uint32_t omod;
};
struct Refused {
std::vector<std::uint32_t> words;
const char* reason;
const char* name;
};
}
int main() {
constexpr std::uint32_t Destination = 5;
const std::vector<Accepted> accepted{
{{0xd6000005u, 0x02020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 0u},
{{0xd6010005u, 0x02020440u}, RdnaOpcode::VInterpP2F32, "v_interp_p2_f32_e64 v5, v2, attr0.y", RdnaOperandKind::VectorRegister, 2u, 0u, 1u, false, false, false, 0u},
{{0xd6020005u, 0x02000200u}, RdnaOpcode::VInterpMovF32, "v_interp_mov_f32_e64 v5, p20, attr0.x", RdnaOperandKind::IntegerInlineConstant, 1u, 0u, 0u, false, false, false, 0u},
{{0xd6000005u, 0x0202041fu}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr31.x", RdnaOperandKind::VectorRegister, 2u, 31u, 0u, false, false, false, 0u},
{{0xd6000005u, 0x02020420u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr32.x", RdnaOperandKind::VectorRegister, 2u, 32u, 0u, false, false, false, 0u},
{{0xd6000005u, 0x0203fe00u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v255, attr0.x", RdnaOperandKind::VectorRegister, 255u, 0u, 0u, false, false, false, 0u},
{{0xd6000205u, 0x02020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, |v2|, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, true, false, false, 0u},
{{0xd6000005u, 0x42020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, -v2, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, true, false, 0u},
{{0xd6008005u, 0x02020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x clamp", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, true, 0u},
{{0xd6000005u, 0x0a020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x mul:2", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 1u},
{{0xd6000005u, 0x12020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x mul:4", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 2u},
{{0xd6000005u, 0x1a020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x div:2", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 3u},
{{0xd6000205u, 0x42020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, -|v2|, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, true, true, false, 0u},
{{0xd6000205u, 0x5a020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, -|v2|, attr0.x div:2", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, true, true, false, 3u},
{{0xd6010005u, 0x42020481u}, RdnaOpcode::VInterpP2F32, "v_interp_p2_f32_e64 v5, -v2, attr1.z", RdnaOperandKind::VectorRegister, 2u, 1u, 2u, false, true, false, 0u},
{{0xd6018205u, 0x02020400u}, RdnaOpcode::VInterpP2F32, "v_interp_p2_f32_e64 v5, |v2|, attr0.x clamp", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, true, false, true, 0u},
{{0xd6028005u, 0x02000000u}, RdnaOpcode::VInterpMovF32, "v_interp_mov_f32_e64 v5, p10, attr0.x clamp", RdnaOperandKind::IntegerInlineConstant, 0u, 0u, 0u, false, false, true, 0u},
{{0xd6020005u, 0x0a000000u}, RdnaOpcode::VInterpMovF32, "v_interp_mov_f32_e64 v5, p10, attr0.x mul:2", RdnaOperandKind::IntegerInlineConstant, 0u, 0u, 0u, false, false, false, 1u},
};
const std::vector<Refused> refused{
{{0xd6000005u, 0x02000400u}, "not a vector register", "v_interp_p1_f32_e64 with a non-VGPR coordinate"},
{{0xd6020005u, 0x02000600u}, "not an interpolation parameter", "v_interp_mov_f32_e64 with a reserved parameter"},
{{0xd6000005u, 0x02060400u}, "third source is not the inline constant 0", "v_interp_p1_f32_e64 with a third source other than the inline constant 0"},
{{0xd6000805u, 0x02020400u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with op_sel"},
{{0xd6000005u, 0x02020500u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with the packed-half selector"},
{{0xd6000105u, 0x02020400u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with an absolute source 0"},
{{0xd6020005u, 0x22000200u}, "modifiers are not implemented", "v_interp_mov_f32_e64 with a negated parameter"},
};
int failures = 0;
for (const auto& test : accepted) {
auto code = test.words;
code.push_back(0xbf810000u);
try {
const auto decoded = RdnaInstructionDecoder{}.Decode(code);
if (decoded.instructions.empty()) {
std::fprintf(stderr, "%s decoded no instruction\n", test.name);
++failures;
continue;
}
const auto& instruction = decoded.instructions[0];
const auto source0Value = test.source0Kind == RdnaOperandKind::VectorRegister ? instruction.source0.reg : instruction.source0.value;
if (instruction.op != test.op || instruction.wordCount != test.words.size() || instruction.family != RdnaInstructionFamily::VOP3 ||
instruction.destination.kind != RdnaOperandKind::VectorRegister || instruction.destination.reg != Destination ||
instruction.destination.clamp != test.clamp || instruction.destination.omod != test.omod || instruction.sourceCount != 3u ||
instruction.source0.kind != test.source0Kind || source0Value != test.source0Value ||
instruction.source0.absolute != test.absolute || instruction.source0.negate != test.negate ||
instruction.source1.kind != RdnaOperandKind::IntegerInlineConstant || instruction.source1.value != test.attribute ||
instruction.source2.kind != RdnaOperandKind::IntegerInlineConstant || instruction.source2.value != test.channel) {
std::fprintf(stderr, "%s decoded to the wrong opcode or operands\n", test.name);
++failures;
}
} catch (const std::exception& error) {
std::fprintf(stderr, "%s was refused: %s\n", test.name, error.what());
++failures;
}
}
for (const auto& test : refused) {
auto code = test.words;
code.push_back(0xbf810000u);
std::string refusal;
try {
static_cast<void>(RdnaInstructionDecoder{}.Decode(code));
} catch (const std::exception& error) {
refusal = error.what();
}
if (refusal.find(test.reason) == std::string::npos) {
std::fprintf(stderr, "%s was not refused\n", test.name);
++failures;
}
}
return failures == 0 ? 0 : 1;
}

View File

@ -1,9 +1,11 @@
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include <array>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <cstdio>
#include <stdexcept>
#include <thread>
@ -22,6 +24,10 @@ void Require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
void DelayExit() {
std::this_thread::sleep_for(std::chrono::seconds(1));
}
std::array<std::uint32_t, 5> WriteData(volatile std::uint32_t* destination) {
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(destination));
return {0xc0033700, 0x00100200, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), 1};
@ -59,7 +65,7 @@ void CheckShutdownCancellation() {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::this_thread::sleep_for(std::chrono::seconds(1));
for (const auto& state : states) {
Require(state.label == 0, "unsatisfied label was unexpectedly signalled");
Require(state.after == 0 && state.queued == 0, "unsatisfied wait completed before shutdown");
@ -77,7 +83,14 @@ void CheckShutdownCancellation() {
}
int main() {
int main(int argc, char** argv) {
if (argc == 2 && std::strcmp(argv[1], "--resolution-shutdown") == 0) {
AgcDriverWaitIdle_nid_postfix();
LibcRegisterShutdown_nid_postfix(DelayExit);
LibcRequestExit_nid_postfix(0);
AgcDriverResolveShaderAbi_nid_postfix(nullptr, {}, {});
return 1;
}
try {
CheckShutdownCancellation();
std::puts("unsatisfied label waits cancelled cleanly at shutdown");

View File

@ -0,0 +1,289 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.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 "VulkanTestDevice.hpp"
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <sys/mman.h>
#endif
#include <algorithm>
#include <array>
#include <bit>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <span>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using AgcDriver::Graphics::StorageTexture;
using AgcDriver::Graphics::TextureDimension;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Threads = 32;
constexpr std::uint32_t Side = 128;
constexpr std::uint32_t Layers = 4;
constexpr std::size_t LayerBytes = 65536;
constexpr std::size_t SurfaceBytes = LayerBytes * Layers;
constexpr std::uint32_t Format32Float = 22;
constexpr std::uint32_t TileR64KBX = 0x1b;
constexpr std::uint32_t Type2DArray = 13;
constexpr std::uint32_t Color32FloatInfo = (4u << 2u) | (7u << 8u);
constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u;
constexpr std::uint32_t Float32Export = 9;
constexpr std::uint32_t Initial = 0x5a5a5a5au;
constexpr std::uint32_t ScissorWidth = 16;
constexpr float Drawn = 0.25f;
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, 9>, Layers> LoadCodes{};
alignas(256) std::array<std::uint32_t, 16> PixelCode{};
alignas(256) std::array<std::uint32_t, Threads> Output{};
std::uint64_t AddressOf(const void* data) {
return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data));
}
std::string Hex(std::uint32_t value) {
char text[16];
std::snprintf(text, sizeof(text), "0x%08x", value);
return text;
}
class GuestBlock {
public:
GuestBlock() {
#ifdef _WIN32
block = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, LayerBytes));
if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes);
#else
void* mapped = mmap(nullptr, SurfaceBytes + LayerBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mapped != MAP_FAILED) {
const auto begin = reinterpret_cast<std::uintptr_t>(mapped);
const auto aligned = (begin + LayerBytes - 1) & ~(static_cast<std::uintptr_t>(LayerBytes) - 1);
if (aligned != begin) munmap(mapped, aligned - begin);
if (aligned + SurfaceBytes != begin + SurfaceBytes + LayerBytes) munmap(reinterpret_cast<void*>(aligned + SurfaceBytes), begin + LayerBytes - aligned);
block = reinterpret_cast<std::uint8_t*>(aligned);
if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes);
}
#endif
Require(block != nullptr, "array layer target: cannot allocate the guest block");
GuestAllocations::Mutation().Add(block, SurfaceBytes, true, true, true);
}
~GuestBlock() {
GuestAllocations::Mutation().Remove(block);
#ifdef _WIN32
GuestArena::GuestArenaReset_nid_postfix(block, SurfaceBytes);
GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes);
#else
munmap(block, SurfaceBytes);
if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, SurfaceBytes);
#endif
}
GuestBlock(const GuestBlock&) = delete;
GuestBlock& operator=(const GuestBlock&) = delete;
std::uint8_t* Data() { return block; }
private:
std::uint8_t* block = nullptr;
};
std::array<std::uint32_t, 8> ArrayDescriptor(std::uint64_t address) {
return {
static_cast<std::uint32_t>(address >> 8u),
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format32Float << 20u) | (((Side - 1u) & 3u) << 30u),
((Side - 1u) >> 2u) | ((Side - 1u) << 14u),
0xfacu | (TileR64KBX << 20u) | (Type2DArray << 28u),
Layers - 1u,
0u,
0u,
0u,
};
}
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
const auto address = AddressOf(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
}
void Dispatch(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData) {
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(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<std::uintptr_t>(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<std::uintptr_t>(code.data()));
device.WaitIdle();
}
void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer) {
Output.fill(0xdeadbeefu);
auto& code = LoadCodes[layer];
code = {0x7e020280, 0x7e040280u | layer, 0xf0001128, 0x00010300, 0x34080082, 0xbf8c3f70, 0xe0701000, 0x80000304, 0xbf810000};
std::vector<std::uint32_t> userData(16, 0u);
const auto buffer = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
const auto texture = ArrayDescriptor(surface);
std::copy(buffer.begin(), buffer.end(), userData.begin());
std::copy(texture.begin(), texture.end(), userData.begin() + 4);
Dispatch(device, code, userData);
}
void RequireOutput(std::uint32_t inside, std::uint32_t outside, const std::string& what) {
for (std::uint32_t x = 0; x < Threads; ++x) {
const auto expected = x < ScissorWidth ? inside : outside;
Require(Output[x] == expected, what + ": texel (" + std::to_string(x) + ", 0) reads " + Hex(Output[x]) + ", expected " + Hex(expected));
}
}
AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice) {
AgcDriver::Registers cx;
cx[0x318] = static_cast<std::uint32_t>(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[0x390] = static_cast<std::uint32_t>(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");
Require(color.arrayAddress == address && color.arrayLastSlice == Layers - 1u && color.arraySlice == slice, "the color view did not keep its array and slice");
return color;
}
void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice) {
const auto target = device.Target();
constexpr std::uint32_t waveSize = 64;
std::size_t at = 0;
for (std::uint32_t channel = 0; channel < 4; ++channel) {
PixelCode[at++] = 0x7e0002ffu | ((4u + channel) << 17u);
PixelCode[at++] = std::bit_cast<std::uint32_t>(Drawn);
}
PixelCode[at++] = 0xf800180fu;
PixelCode[at++] = 0x07060504u;
PixelCode[at++] = 0xbf810000u;
const std::span<const std::uint32_t> pixelCode(PixelCode.data(), at);
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());
const auto color = DecodeTarget(surface, slice);
VkPipelineColorBlendAttachmentState blend{};
blend.colorWriteMask = 0xf;
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 = {Side, Side};
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
state.viewport = {0, static_cast<float>(Side), static_cast<float>(Side), -static_cast<float>(Side), 0, 1};
state.negativeOneToOne = false;
state.scissor = {{0, 0}, {ScissorWidth, Side}};
state.cullMode = VK_CULL_MODE_NONE;
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
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, Float32Export}};
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, AgcDriver::Graphics::ExportMappings(state)), std::nullopt, pixelMemory},
target,
PixelPushLayout(vertexPush, target)
};
fragment.useCache = false;
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
{ShaderStage::Vertex, &vertexResult, 0},
{ShaderStage::Fragment, &pixelResult, 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);
device.WaitIdle();
}
void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels) {
const auto surface = AddressOf(texels);
std::fill_n(reinterpret_cast<std::uint32_t*>(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<std::uint32_t>(Drawn);
DrawSlice(device, surface, 1);
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);
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);
RequireOutput(Initial, Initial, "slice " + std::to_string(other) + " after the draw into slice 1");
}
StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test");
device.WaitIdle();
AgcDriver::Graphics::ClearCachedTextures(device.Device());
AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes);
AgcDriver::GuestMemory::BumpCollectEpoch();
LoadLayer(device, surface, 1);
RequireOutput(drawn, Initial, "slice 1 uploaded again from guest memory");
AgcDriver::Graphics::ClearCachedTextures(device.Device());
}
}
int main() {
try {
GuestBlock block;
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
Run(*device, block.Data());
std::puts("array layer target tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -0,0 +1,113 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <array>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <span>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t MaxThreads = 64;
constexpr std::uint32_t Stride = 4;
alignas(256) std::array<std::uint32_t, MaxThreads * Stride> Input{};
alignas(256) std::array<std::uint32_t, MaxThreads * Stride> Output{};
std::array<std::uint32_t, 17> Code(std::uint32_t selectors) {
const auto dpp8 = [&](std::uint32_t source) { return (selectors << 8u) | source; };
return {
0x34080084u,
0xe03c1000u, 0x80000104u,
0xbf8c3f70u,
0x7e140301u,
0x781406e9u, dpp8(2u),
0x7e1a02e9u, dpp8(2u),
0x7e160301u,
0x7816070du,
0x7e180301u,
0x78180702u,
0xe07c1000u, 0x80010a04u,
0xbf810000u,
};
}
std::uint32_t Selectors(const std::array<std::uint32_t, 8>& lanes) {
std::uint32_t value = 0;
for (std::uint32_t lane = 0; lane < 8u; ++lane) value |= lanes[lane] << (lane * 3u);
return value;
}
std::uint16_t Half(std::uint32_t seed) {
const auto sign = (seed >> 15u) & 1u;
const auto exponent = 10u + seed % 10u;
const auto mantissa = (seed >> 4u) & 0x3ffu;
return static_cast<std::uint16_t>((sign << 15u) | (exponent << 10u) | mantissa);
}
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::size_t bytes) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), static_cast<std::uint32_t>(bytes), 0x31016facu};
}
void Run(AgcDriver::VulkanDevice& device, std::uint32_t wave, const std::array<std::uint32_t, 8>& lanes, const std::string& what) {
for (std::uint32_t lane = 0; lane < MaxThreads; ++lane) {
for (std::uint32_t word = 0; word < 3u; ++word) {
const auto seed = lane * 2654435761u + word * 40503u + 12345u;
Input[lane * Stride + word] = static_cast<std::uint32_t>(Half(seed)) | (static_cast<std::uint32_t>(Half(seed * 7u + 3u)) << 16u);
}
}
Output.fill(0xdeadbeefu);
const auto code = Code(Selectors(lanes));
std::vector<std::uint32_t> userData;
for (const auto& descriptor : {BufferDescriptor(Input.data(), sizeof(Input)), BufferDescriptor(Output.data(), sizeof(Output))}) {
userData.insert(userData.end(), descriptor.begin(), descriptor.end());
}
const std::span<const std::uint32_t> program(code);
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(program)}}};
const ShaderRecompiler::ShaderComputeStageInfo compute{{wave, 1, 1}, 0u, {false, false, false}, false, 1};
ShaderRecompiler::RecompileRequest request{
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), program, 0, {}},
{wave, 0, userData, compute, std::nullopt, std::nullopt, memory},
device.ComputeTarget(wave),
{0, 0, 0, 128}
};
request.useCache = false;
const auto result = ShaderRecompiler::Recompile(request);
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
device.WaitIdle();
bool permuted = false;
for (std::uint32_t lane = 0; lane < wave; ++lane) {
const auto fused = Output[lane * Stride];
const auto separate = Output[lane * Stride + 1u];
const auto plain = Output[lane * Stride + 2u];
Require(fused == separate, what + " (wave" + std::to_string(wave) + "): lane " + std::to_string(lane) + " v_pk_fmac_f16 dpp8 gave " + std::to_string(fused) +
", v_mov_b32 dpp8 then v_pk_fmac_f16 gave " + std::to_string(separate));
permuted = permuted || fused != plain;
}
Require(permuted, what + " (wave" + std::to_string(wave) + "): the dpp8 result equals the unpermuted one in every lane");
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
for (const std::uint32_t wave : {32u, 64u}) {
Run(*device, wave, {7, 6, 5, 4, 3, 2, 1, 0}, "reversed groups of eight");
Run(*device, wave, {1, 0, 3, 2, 5, 4, 7, 6}, "swapped neighbours");
}
std::puts("dpp8 v_pk_fmac_f16 tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -9,6 +9,7 @@
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <span>
#include <stdexcept>
@ -236,10 +237,11 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::
return ShaderRecompiler::Recompile(request);
}
void Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name, bool offsets = false, bool useCache = false, bool array = false) {
std::uint64_t Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name, bool offsets = false, bool useCache = false, bool array = false, std::uint64_t expectedVariant = 0) {
Output.fill(-1.0f);
const std::span<const std::uint32_t> code = offsets ? std::span<const std::uint32_t>(OffsetCode) : array ? std::span<const std::uint32_t>(ArrayCode) : std::span<const std::uint32_t>(Code);
const auto result = Compile(device, Format8888UNorm, sampler, code, useCache);
if (expectedVariant != 0) Require(result.cacheHit && result.variantId == expectedVariant, std::string(name) + " did not reuse the compiled comparison artifact");
for (const auto& binding : result.bindings) {
Require(binding.role != ShaderRecompiler::DescriptorRole::GuestSamplers, "emulated comparison retained a sampler binding");
Require(std::all_of(binding.imageSamplers.begin(), binding.imageSamplers.end(), [](auto mask) { return mask == 0u; }), "emulated comparison retained an unused sampler association");
@ -251,6 +253,7 @@ void Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* na
const float tolerance = sampler.filter == FilterPoint ? 0.0f : 1e-4f;
Require(std::fabs(Output[tid] - expected) <= tolerance, std::string(name) + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected));
}
return result.variantId;
}
constexpr std::uint32_t Select(std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint32_t w) {
@ -306,9 +309,28 @@ bool BindsDepthCompare(const ShaderRecompiler::RecompileResult& result) {
});
}
void RunSrgbZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name) {
void RunSrgbCubeZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name) {
FillCubeTexels();
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
CubeInput[tid * 4u + 0u] = 0.5f;
CubeInput[tid * 4u + 1u] = 1.0f;
CubeInput[tid * 4u + 2u] = 0.0f;
CubeInput[tid * 4u + 3u] = 0.0f;
}
Output.fill(-1.0f);
const auto result = Compile(device, format, sampler);
const auto result = Compile(device, format, sampler, CubeCode, false, true, true);
Require(!BindsDepthCompare(result), std::string(name) + " kept a depth-compare binding");
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(CubeCode.data()));
device.WaitIdle();
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
Require(Output[tid] == 0.0f, std::string(name) + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected 0");
}
}
void RunSrgbZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name, bool useCache, std::uint64_t expectedVariant) {
Output.fill(-1.0f);
const auto result = Compile(device, format, sampler, Code, useCache);
if (useCache) Require(result.cacheHit && result.variantId == expectedVariant, std::string(name) + " did not reuse the compiled comparison artifact");
Require(!BindsDepthCompare(result), std::string(name) + " kept a depth-compare binding");
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(Code.data()));
device.WaitIdle();
@ -333,10 +355,18 @@ int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
const auto* forced = std::getenv("APS5_SRGB_SHADER_DECODE");
if (forced != nullptr && std::string_view(forced) == "1") Require((device->Target().srgbDecodeFormats & 3u) == 3u, "forced R8 and RG8 sRGB shader decoding is unavailable");
FillTexels();
FillInput();
RunCube(*device, {ClampEdge, FilterPoint}, "point cube, clamp to edge");
Reject(*device, Format8888UNorm, {ClampEdge, FilterBilinear}, "bilinear cube comparison requires DISABLE_CUBE_WRAP", CubeCode, true, true);
for (std::uint32_t function = 0; function < 8u; ++function) {
const auto name = "bilinear 8_8_8_8 srgb cube function " + std::to_string(function);
RunSrgbCubeZero(*device, Format8888Srgb, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str());
}
RunSrgbCubeZero(*device, Format8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8 srgb cube without DISABLE_CUBE_WRAP");
RunSrgbCubeZero(*device, Format8_8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8_8 srgb cube without DISABLE_CUBE_WRAP");
RunCube(*device, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, true}, "bilinear cube with DISABLE_CUBE_WRAP");
Reject(*device, Format9995Float, {ClampEdge, FilterPoint}, "implemented only for float, unorm and snorm formats");
Reject(*device, Format8888UNorm, {ClampEdge, FilterAnisoBilinear}, "point or bilinear");
@ -344,16 +374,19 @@ int main() {
FillTexels();
Require(BindsDepthCompare(Compile(*device, Format32Float, {ClampEdge, FilterBilinear})), "an R32 float texture left the native comparison path");
Require(!BindsDepthCompare(Compile(*device, Format8888UNorm, {ClampEdge, FilterBilinear})), "a color texture kept a depth-compare binding");
for (std::uint32_t function = 0; function < 8u; ++function) {
const auto point = "point 8_8_8_8 srgb function " + std::to_string(function);
const auto bilinear = "bilinear 8_8_8_8 srgb function " + std::to_string(function);
RunSrgbZero(*device, Format8888Srgb, {ClampEdge, FilterPoint, BorderBlack, 0u, 0u, false, function}, point.c_str());
RunSrgbZero(*device, Format8888Srgb, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, bilinear.c_str());
for (const bool useCache : {false, true}) {
const auto variant = useCache ? Run(*device, {ClampEdge, FilterPoint}, "cached UNORM before sRGB", false, true) : 0u;
if (useCache) Require(variant != 0, "the cached comparison has no compiled artifact identity");
for (const auto format : {Format8Srgb, Format8_8Srgb, Format8888Srgb}) {
for (std::uint32_t function = 0; function < 8u; ++function) {
for (const auto filter : {FilterPoint, FilterBilinear}) {
const auto name = std::string(useCache ? "cached" : "uncached") + " sRGB format " + std::to_string(format) + " function " + std::to_string(function) + (filter == FilterPoint ? " point" : " bilinear");
RunSrgbZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), useCache, variant);
}
}
}
if (useCache) Run(*device, {ClampEdge, FilterPoint}, "cached UNORM after sRGB", false, true, false, variant);
}
RunSrgbZero(*device, Format8Srgb, {ClampEdge, FilterPoint}, "point 8 srgb");
RunSrgbZero(*device, Format8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8 srgb");
RunSrgbZero(*device, Format8_8Srgb, {ClampEdge, FilterPoint}, "point 8_8 srgb");
RunSrgbZero(*device, Format8_8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8_8 srgb");
Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge");
Run(*device, {ClampWrap, FilterPoint}, "point, wrap");
Run(*device, {ClampWrap, FilterPoint}, "point, wrap with negative offsets", true);

View File

@ -23,6 +23,8 @@ constexpr std::uint32_t Threads = 32;
constexpr std::uint32_t Inputs = 4;
constexpr std::uint32_t Results = 19;
constexpr std::uint32_t TinyResults = 16;
constexpr std::uint32_t TinyProductLane = 20;
constexpr std::uint32_t SplitTinyProductSum = 0x00800000u;
alignas(256) std::array<std::uint32_t, Threads * Inputs> Input{};
alignas(256) std::array<std::uint32_t, Threads * Results> Output{};
@ -411,9 +413,15 @@ void Run(AgcDriver::VulkanDevice& device, const std::optional<ShaderRecompiler::
}
void Check(const char* mode) {
const std::uint32_t* tinyProduct = &Output[TinyProductLane * Results];
const bool hostSplits = tinyProduct[4] == SplitTinyProductSum && tinyProduct[5] == SplitTinyProductSum;
if (hostSplits) {
std::printf("the device splits fma into a multiply and an add, so the fused sum of lane %u is not checked (%s)\n", TinyProductLane, mode);
}
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
const std::uint32_t* out = &Output[tid * Results];
for (std::uint32_t i = 0; i < Results; ++i) {
if (hostSplits && tid == TinyProductLane && (i == 4u || i == 5u)) continue;
const bool floatResult = i < 12u || i >= 15u;
if (floatResult && IsNan(Expected[tid][i])) {
Require(IsNan(out[i]), std::string("f32 denormal flush: lane ") + std::to_string(tid) + " " + mode + " " + Names[i] + " is " + Hex(out[i]) + ", expected a NaN");

View File

@ -17,6 +17,7 @@
#include <cstring>
#include <exception>
#include <iostream>
#include <map>
#include <optional>
#include <span>
#include <stdexcept>
@ -497,9 +498,10 @@ int main(int argc, char** argv) {
const bool int64Atomics = TargetHasCapability(device->Target(), spv::CapabilityInt64Atomics);
GuestBlock guest;
std::size_t checked = 0;
const auto rows = Rows();
for (std::size_t index = part; index < rows.size(); index += parts) {
const Row& row = rows[index];
std::map<std::pair<Words, std::uint32_t>, std::size_t> shaders;
for (const Row& row : Rows()) {
const auto shader = shaders.emplace(std::pair{Kernel(row), row.waveSize}, shaders.size()).first->second;
if (shader % parts != part) continue;
if (row.waveSize == 64u && !wave64) continue;
if (row.atomic && row.width == 64u && !int64Atomics) continue;
Run(*device, row, guest);

View File

@ -0,0 +1,305 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <optional>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Threads = 64;
constexpr std::uint32_t Inputs = 4;
constexpr std::uint32_t Stride = 8;
constexpr std::uint32_t Batches = 32;
alignas(256) std::array<std::uint32_t, Threads * Inputs> Input{};
alignas(256) std::array<std::uint32_t, Threads * Stride> Output{};
alignas(256) constexpr std::array<std::uint32_t, 28> ScalarCode{
0x34020084, 0x34060085, 0xe0381000, 0x80000401, 0xbf8c3f70,
0x74140b04,
0x72160b04,
0xd539810c, 0x20020d04,
0xd53a800d, 0x40020d05,
0xd751000e, 0x041a0b04,
0xd754280f, 0x441a0b04,
0xd7570010, 0x041a0b04,
0xd7579111, 0x841e0b04,
0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 16> PackedCode{
0x34020084, 0x34060085, 0xe0381000, 0x80000401, 0xbf8c3f70,
0xcc11400a, 0x18020b04,
0xcc12400b, 0x18020b04,
0xcc12ca0c, 0x30020d04,
0xcc11c00d, 0x58020f05,
0xe0781000, 0x80010a03, 0xbf810000,
};
enum class Kind { Min, Max, Min3, Max3, Med3 };
struct Source {
std::uint32_t reg;
bool opSel = false;
bool opSelHi = true;
bool absolute = false;
bool negate = false;
bool negateHi = false;
};
struct Check {
const char* name;
Kind kind;
std::array<Source, 3> sources;
bool clamp = false;
};
const std::array<Check, 8> ScalarChecks{{
{"v_min_f16 v10, v4, v5", Kind::Min, {{{0u}, {1u}, {}}}},
{"v_max_f16 v11, v4, v5", Kind::Max, {{{0u}, {1u}, {}}}},
{"v_max_f16 v12, -|v4|, v6 clamp", Kind::Max, {{{0u, false, true, true, true}, {2u}, {}}}, true},
{"v_min_f16 v13, v5, -v6 clamp", Kind::Min, {{{1u}, {2u, false, true, false, true}, {}}}, true},
{"v_min3_f16 v14, v4, v5, v6", Kind::Min3, {{{0u}, {1u}, {2u}}}},
{"v_max3_f16 v15, v4, -v5, v6 op_sel:[1,0,1,0]", Kind::Max3, {{{0u, true}, {1u, false, true, false, true}, {2u, true}}}},
{"v_med3_f16 v16, v4, v5, v6", Kind::Med3, {{{0u}, {1u}, {2u}}}},
{"v_med3_f16 v17, |v4|, v5, -v7 op_sel:[0,1,0,0] clamp", Kind::Med3, {{{0u, false, true, true}, {1u, true}, {3u, false, true, false, true}}}, true},
}};
const std::array<Check, 4> PackedChecks{{
{"v_pk_min_f16 v10, v4, v5", Kind::Min, {{{0u}, {1u}, {}}}},
{"v_pk_max_f16 v11, v4, v5", Kind::Max, {{{0u}, {1u}, {}}}},
{"v_pk_max_f16 v12, v4, v6 op_sel:[1,0] op_sel_hi:[0,1] neg_lo:[1,0] neg_hi:[0,1] clamp", Kind::Max, {{{0u, true, false, false, true, false}, {2u, false, true, false, false, true}, {}}}, true},
{"v_pk_min_f16 v13, v5, v7 neg_lo:[0,1] clamp", Kind::Min, {{{1u}, {3u, false, true, false, true}, {}}}, true},
}};
struct Mode {
const char* name;
std::optional<ShaderRecompiler::ShaderFloatMode> floatMode;
bool packed = true;
};
const std::array<Mode, 7> Modes{{
{"no FLOAT_MODE", std::nullopt},
{"dx10 ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}},
{"dx10", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}},
{"ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, false}},
{"none", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, false}},
{"f16 denormals flushed, dx10 ieee", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, false},
{"f16 input denormals flushed", ShaderRecompiler::ShaderFloatMode{0xb0u, false, false, false}, false},
}};
std::uint64_t Seed = 0x2545f4914f6cdd1dull;
std::uint32_t Next() {
Seed += 0x9e3779b97f4a7c15ull;
std::uint64_t z = Seed;
z = (z ^ (z >> 30u)) * 0xbf58476d1ce4e5b9ull;
z = (z ^ (z >> 27u)) * 0x94d049bb133111ebull;
return static_cast<std::uint32_t>(z ^ (z >> 31u));
}
std::uint32_t Half() {
const std::uint32_t bits = Next();
const std::uint32_t sign = (bits >> 31u) << 15u;
const std::uint32_t mantissa = bits & 0x3ffu;
switch ((bits >> 16u) % 9u) {
case 0u: return sign;
case 1u: return sign | std::max(mantissa, 1u);
case 2u: return sign | 0x7c00u;
case 3u: return sign | 0x7c00u | std::max(mantissa, 1u);
case 4u: return sign | 0x3c00u | (bits & 0x3u);
default: return bits & 0xffffu;
}
}
bool IsNan(std::uint32_t bits) { return (bits & 0x7fffu) > 0x7c00u; }
bool IsSnan(std::uint32_t bits) { return IsNan(bits) && (bits & 0x0200u) == 0u; }
double Value(std::uint32_t bits) {
const double sign = (bits & 0x8000u) != 0u ? -1.0 : 1.0;
const std::uint32_t exponent = (bits >> 10u) & 0x1fu;
const std::uint32_t mantissa = bits & 0x3ffu;
if (exponent == 0u) return sign * std::ldexp(static_cast<double>(mantissa), -24);
if (exponent == 31u) return mantissa != 0u ? std::nan("") : sign * INFINITY;
return sign * std::ldexp(static_cast<double>(mantissa | 0x400u), static_cast<int>(exponent) - 25);
}
struct Model {
bool ieee;
bool dx10;
bool flushInputs;
struct Half {
double value;
std::uint32_t bits;
};
std::uint32_t Quiet(std::uint32_t bits) const { return ieee ? bits | 0x0200u : bits; }
Half Numeric(bool maxValue, const Half& lhs, const Half& rhs) const {
const bool ordered = maxValue ? lhs.value >= rhs.value : lhs.value < rhs.value;
const bool bothZero = ((lhs.bits | rhs.bits) & 0x7fffu) == 0u;
const std::uint32_t zero = maxValue ? lhs.bits & rhs.bits : lhs.bits | rhs.bits;
return {ordered ? lhs.value : rhs.value, bothZero ? zero : (ordered ? lhs.bits : rhs.bits)};
}
Half Pair(bool maxValue, const Half& lhs, const Half& rhs) const {
const Half numeric = Numeric(maxValue, lhs, rhs);
std::uint32_t bits = IsNan(lhs.bits) ? rhs.bits : numeric.bits;
bits = IsNan(rhs.bits) ? lhs.bits : bits;
if (ieee) {
bits = IsSnan(rhs.bits) ? Quiet(rhs.bits) : bits;
bits = IsSnan(lhs.bits) ? Quiet(lhs.bits) : bits;
}
return {IsNan(rhs.bits) ? lhs.value : numeric.value, bits};
}
std::uint32_t Clamp(bool clamp, std::uint32_t bits) const {
if (!clamp) return bits;
const std::uint32_t magnitude = bits & 0x7fffu;
const bool zero = bits > 0x7fffu || magnitude > 0x7c00u;
const std::uint32_t clamped = zero ? 0u : (magnitude > 0x3c00u ? 0x3c00u : bits);
return dx10 || magnitude <= 0x7c00u ? clamped : bits;
}
Half Read(std::uint32_t bits) const {
if (flushInputs && (bits & 0x7fffu) < 0x0400u) bits &= 0x8000u;
return {Value(bits), bits};
}
std::uint32_t Scalar(const Check& check, const std::uint32_t* registers) const {
std::array<Half, 3> args{};
for (std::uint32_t index = 0; index < 3u; ++index) {
const Source& source = check.sources[index];
std::uint32_t bits = (registers[source.reg] >> (source.opSel ? 16u : 0u)) & 0xffffu;
if (source.absolute) bits &= 0x7fffu;
if (source.negate) bits ^= 0x8000u;
args[index] = Read(bits);
}
std::uint32_t result = 0u;
switch (check.kind) {
case Kind::Min: result = Pair(false, args[0], args[1]).bits; break;
case Kind::Max: result = Pair(true, args[0], args[1]).bits; break;
case Kind::Min3: result = Pair(false, Pair(false, args[0], args[1]), args[2]).bits; break;
case Kind::Max3: result = Pair(true, Pair(true, args[0], args[1]), args[2]).bits; break;
case Kind::Med3: {
const bool anyNan = IsNan(args[0].bits) || IsNan(args[1].bits) || IsNan(args[2].bits);
const Half minimum = Pair(false, Pair(false, args[0], args[1]), args[2]);
const Half median = Numeric(true, Numeric(false, args[0], args[1]), Numeric(false, Numeric(true, args[0], args[1]), args[2]));
result = anyNan ? minimum.bits : median.bits;
break;
}
}
return Clamp(check.clamp, result);
}
std::uint32_t Packed(const Check& check, const std::uint32_t* registers) const {
std::uint32_t result = 0u;
for (std::uint32_t high = 0; high < 2u; ++high) {
std::array<std::uint32_t, 2> bits{};
for (std::uint32_t index = 0; index < 2u; ++index) {
const Source& source = check.sources[index];
const bool selectHigh = high != 0u ? source.opSelHi : source.opSel;
bits[index] = (registers[source.reg] >> (selectHigh ? 16u : 0u)) & 0xffffu;
if (high != 0u ? source.negateHi : source.negate) bits[index] ^= 0x8000u;
}
std::uint32_t lane = Numeric(check.kind == Kind::Max, Read(bits[0]), Read(bits[1])).bits;
lane = IsNan(bits[0]) ? bits[1] : lane;
lane = IsNan(bits[1]) ? bits[0] : lane;
if (ieee) {
lane = IsSnan(bits[1]) ? Quiet(bits[1]) : lane;
lane = IsSnan(bits[0]) ? Quiet(bits[0]) : lane;
}
result |= Clamp(check.clamp, lane) << (16u * high);
}
return result;
}
};
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
}
std::string Hex(std::uint32_t value) {
char text[16];
std::snprintf(text, sizeof(text), "0x%08x", value);
return text;
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
std::vector<std::uint32_t> userData(8, 0u);
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u));
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
std::copy(input.begin(), input.end(), userData.begin());
std::copy(output.begin(), output.end(), userData.begin() + 4);
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
const auto compile = [&](std::span<const std::uint32_t> code, const Mode& mode) {
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
ShaderRecompiler::RecompileRequest request{
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
device->Target(),
{0, 0, 0, 128}
};
request.useCache = false;
request.context.floatMode = mode.floatMode;
return ShaderRecompiler::Recompile(request);
};
std::uint64_t compared = 0u;
for (const auto& mode : Modes) {
const bool flushInputs = mode.floatMode.has_value() && (mode.floatMode->floatMode & 0x40u) == 0u;
const Model model{mode.floatMode.has_value() && mode.floatMode->ieeeMode, !mode.floatMode.has_value() || mode.floatMode->dx10Clamp, flushInputs};
const auto scalar = compile(ScalarCode, mode);
std::optional<ShaderRecompiler::RecompileResult> packed;
if (mode.packed) packed = compile(PackedCode, mode);
for (std::uint32_t batch = 0; batch < Batches; ++batch) {
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
std::uint32_t* row = &Input[tid * Inputs];
for (std::uint32_t index = 0; index < Inputs; ++index) row[index] = Half() | (Half() << 16u);
if (tid % 8u == 0u) row[1] = row[0];
if (tid % 8u == 1u) row[1] = row[0] ^ 0x80008000u;
if (tid % 8u == 2u) row[2] = row[1];
if (tid % 8u == 3u) row[2] = row[0] ^ 0x80008000u;
}
const auto run = [&](const ShaderRecompiler::RecompileResult& result, std::span<const std::uint32_t> code, std::span<const Check> checks, bool isPacked) {
Output.fill(0xdeadbeefu);
device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
device->WaitIdle();
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
const std::uint32_t* row = &Input[tid * Inputs];
for (std::uint32_t index = 0; index < checks.size(); ++index) {
const std::uint32_t actual = Output[tid * Stride + index];
const std::uint32_t expected = isPacked ? model.Packed(checks[index], row) : model.Scalar(checks[index], row);
Require(actual == expected, std::string("f16 min/max: ") + mode.name + ", " + checks[index].name + " with v4=" + Hex(row[0]) + " v5=" + Hex(row[1]) + " v6=" + Hex(row[2]) + " v7=" + Hex(row[3]) + " is " + Hex(actual) + ", expected " + Hex(expected));
++compared;
}
}
};
run(scalar, ScalarCode, ScalarChecks, false);
if (packed) run(*packed, PackedCode, PackedChecks, true);
}
}
std::printf("f16 min/max tests passed (%llu results)\n", static_cast<unsigned long long>(compared));
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -23,6 +23,7 @@ alignas(256) std::array<std::uint32_t, 12> ReaderCode{0xf4040100u, 0xfa000000u,
constexpr std::uint32_t StaleValue = 0x57a1e000u;
constexpr std::uint32_t FreshValue = 0xf7e54000u;
constexpr std::uint32_t SpinIterationsPerDispatch = 1u << 20u;
struct Slot {
alignas(256) std::array<std::uint32_t, 4> stale{};
@ -81,14 +82,14 @@ void Submit(std::uint32_t queue, std::vector<std::uint32_t>& words) {
enum class Outcome { Fresh, Stale, Inconclusive };
Outcome Run(Slot& slot, std::uint32_t iterations) {
Outcome Run(Slot& slot, std::uint32_t spinDispatches) {
slot.stale[0] = StaleValue;
slot.fresh[0] = FreshValue;
slot.table = {Low(slot.stale.data()), High(slot.stale.data())};
slot.outputDescriptor = Descriptor(slot.output.data(), sizeof(slot.output));
slot.spinDescriptor = Descriptor(slot.spinOutput.data(), sizeof(slot.spinOutput));
std::vector<std::uint32_t> producer;
Dispatch(producer, SpinCode.data(), {Low(slot.spinDescriptor.data()), High(slot.spinDescriptor.data()), iterations});
for (std::uint32_t index = 0; index < spinDispatches; ++index) Dispatch(producer, SpinCode.data(), {Low(slot.spinDescriptor.data()), High(slot.spinDescriptor.data()), SpinIterationsPerDispatch});
WriteData(producer, &slot.label, 1);
std::vector<std::uint32_t> consumer;
WaitEqual(consumer, &slot.label, 1);
@ -118,16 +119,16 @@ int main() {
auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
device.reset();
std::uint32_t iterations = 1u << 26u;
std::uint32_t spinDispatches = (1u << 26u) / SpinIterationsPerDispatch;
for (auto& slot : Slots) {
const auto outcome = Run(slot, iterations);
const auto outcome = Run(slot, spinDispatches);
Require(outcome != Outcome::Stale, "a dispatch behind a cross-queue WAIT_REG_MEM read its table before the producer's label reached memory, so it missed the table the CPU filled after submitting");
if (outcome == Outcome::Fresh) {
std::cout << "gated dispatch capture test passed (" << iterations << " producer iterations)\n";
std::cout << "gated dispatch capture test passed (" << spinDispatches << " producer dispatches of " << SpinIterationsPerDispatch << " iterations)\n";
AgcDriverShutdown_nid_postfix();
return 0;
}
iterations *= 2u;
spinDispatches *= 2u;
}
std::cout << "the producer's label always reached memory before the table was filled: gated dispatch capture not tested\n";
AgcDriverShutdown_nid_postfix();

View File

@ -32,8 +32,8 @@ alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 9> PixelCode{
0x10040010, 0x06060300, 0x10060611, 0xf0800f08, 0x00610402, 0xbf8c3f70, 0xf800180f, 0x07060504, 0xbf810000,
alignas(256) constexpr std::array<std::uint32_t, 10> PixelCode{
0xbefe0a7e, 0x10040010, 0x06060300, 0x10060611, 0xf0800f08, 0x00610402, 0xbf8c3f70, 0xf800180f, 0x07060504, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 9> BiasPixelCode{

View File

@ -27,6 +27,8 @@ constexpr std::uint32_t Format8888Srgb = 130;
constexpr std::uint32_t Type2D = 9;
constexpr std::uint32_t FilterPoint = 0;
constexpr std::uint32_t FilterBilinear = 1;
constexpr std::uint32_t FilterAnisoBilinear = 3;
constexpr std::uint32_t AnisoOverride = 1u << 29u;
constexpr std::uint32_t ForceDegamma = 1u << 20u;
alignas(256) std::array<float, Threads * 3> Input{};
@ -57,8 +59,8 @@ std::array<std::uint32_t, 8> TextureDescriptor(std::uint32_t format) {
};
}
std::array<std::uint32_t, 4> SamplerDescriptor(std::uint32_t filter, std::uint32_t forced) {
return {0x92u | forced, 0x00fff000u, (filter << 20u) | (filter << 22u) | (1u << 24u), 0u};
std::array<std::uint32_t, 4> SamplerDescriptor(std::uint32_t filter, std::uint32_t forced, std::uint32_t filters) {
return {0x92u | forced, 0x00fff000u, (filter << 20u) | (filter << 22u) | (1u << 24u) | filters, 0u};
}
std::uint8_t Code8(std::uint32_t x, std::uint32_t y, std::uint32_t channel) {
@ -92,12 +94,12 @@ float SrgbToLinear(std::uint8_t code) {
return c <= 0.04045f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f);
}
std::array<float, Threads * 4> Sample(AgcDriver::VulkanDevice& device, std::uint32_t format, std::uint32_t filter, std::uint32_t forced) {
std::array<float, Threads * 4> Sample(AgcDriver::VulkanDevice& device, std::uint32_t format, std::uint32_t filter, std::uint32_t forced, std::uint32_t filters = 0u) {
std::vector<std::uint32_t> userData(20, 0u);
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(sizeof(Input)));
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(sizeof(Output)));
const auto texture = TextureDescriptor(format);
const auto sampler = SamplerDescriptor(filter, forced);
const auto sampler = SamplerDescriptor(filter, forced, filters);
std::copy(input.begin(), input.end(), userData.begin());
std::copy(output.begin(), output.end(), userData.begin() + 4);
std::copy(texture.begin(), texture.end(), userData.begin() + 8);
@ -118,9 +120,9 @@ std::array<float, Threads * 4> Sample(AgcDriver::VulkanDevice& device, std::uint
return Output;
}
void RequireSame(const std::array<float, Threads * 4>& forced, const std::array<float, Threads * 4>& plain, const char* name) {
void RequireSame(const std::array<float, Threads * 4>& forced, const std::array<float, Threads * 4>& plain, const char* name, const char* first = "through FORCE_DEGAMMA", const char* second = "without it") {
for (std::uint32_t index = 0; index < forced.size(); ++index) {
Require(std::bit_cast<std::uint32_t>(forced[index]) == std::bit_cast<std::uint32_t>(plain[index]), std::string(name) + ": thread " + std::to_string(index / 4u) + " component " + std::to_string(index % 4u) + " sampled " + std::to_string(forced[index]) + " through FORCE_DEGAMMA, " + std::to_string(plain[index]) + " without it");
Require(std::bit_cast<std::uint32_t>(forced[index]) == std::bit_cast<std::uint32_t>(plain[index]), std::string(name) + ": thread " + std::to_string(index / 4u) + " component " + std::to_string(index % 4u) + " sampled " + std::to_string(forced[index]) + " " + first + ", " + std::to_string(plain[index]) + " " + second);
}
}
@ -161,6 +163,7 @@ int main() {
FillInput(false);
const auto forcedBilinear = Sample(*device, Format8888Srgb, FilterBilinear, ForceDegamma);
RequireSame(forcedBilinear, Sample(*device, Format8888Srgb, FilterBilinear, 0u), "8_8_8_8_SRGB, bilinear");
RequireSame(Sample(*device, Format8888Srgb, FilterAnisoBilinear, ForceDegamma, AnisoOverride), forcedBilinear, "8_8_8_8_SRGB, single level", "anisotropically under ANISO_OVERRIDE", "bilinearly");
Reject(*device, Format8888UNorm, "guest format 56, which is not sRGB, is sampled through a sampler that forces sRGB decoding");
std::puts("image sample force degamma tests passed");
return 0;

View File

@ -66,12 +66,21 @@ constexpr std::array<std::uint32_t, 9> SamplerCode(std::uint32_t word0) {
};
}
constexpr std::array<std::uint32_t, 15> MixedSamplerCode(std::uint32_t word0) {
return {
0x34060084u, 0x7e280280u, 0x7e2a0280u, 0x7e2c0280u, 0xf0bc0108u, 0x00610a14u, 0xbf8c3f70u,
0xe0701000u, 0x80000a03u, word0, 0x00610a14u, 0xbf8c3f70u, 0xe0701004u, 0x80000a03u, 0xbf810000u,
};
}
alignas(256) constexpr auto LoadXyzw = LoadCode(0xfu);
alignas(256) constexpr auto LoadXy = LoadCode(0x3u);
alignas(256) constexpr auto StoreX = StoreCode(0x1u);
alignas(256) constexpr auto StoreXy = StoreCode(0x3u);
alignas(256) constexpr auto SampleLz = SamplerCode(0xf09c0f08u);
alignas(256) constexpr auto Gather4Lz = SamplerCode(0xf11c0108u);
alignas(256) constexpr auto MixedSampleLz = MixedSamplerCode(0xf09c0f08u);
alignas(256) constexpr auto MixedGather4Lz = MixedSamplerCode(0xf11c0108u);
class GuestBlock {
public:
@ -235,6 +244,8 @@ int main() {
CheckStoredLoad(*device, block, LoadXyzw, 0xfu, Srgb8, SwizzleXXX1, "8_SRGB image_load dmask:0xf X X X 1 of 8_UNORM image_stores");
RequireRefused(*device, SampleLz, "image_sample_lz");
RequireRefused(*device, Gather4Lz, "image_gather4_lz");
RequireRefused(*device, MixedSampleLz, "image_sample_c_lz followed by image_sample_lz");
RequireRefused(*device, MixedGather4Lz, "image_sample_c_lz followed by image_gather4_lz");
std::puts("image sRGB load tests passed");
return 0;
} catch (const std::exception& error) {

View File

@ -8,6 +8,7 @@
#include <cmath>
#include <cstdio>
#include <iostream>
#include <optional>
#include <span>
#include <string>
#include <vector>
@ -115,6 +116,30 @@ constexpr std::array<RoundingRow, Threads> RoundingRows{{
{0x00003c01u, 0x00004200u, 0x00800000u, 3u, 0x4202u},
}};
struct DirectedRow {
std::uint32_t a, b, c;
std::array<std::uint32_t, 4> expected;
};
constexpr std::array<DirectedRow, 16> DirectedRows{{
{0xbfc00000u, 0x33800000u, 0x00800000u, {0x8001u, 0x8001u, 0x8002u, 0x8001u}},
{0x3fc00000u, 0x33800000u, 0x00800000u, {0x0002u, 0x0002u, 0x0001u, 0x0001u}},
{0x3f800000u, 0x3f801400u, 0x00000000u, {0x3c01u, 0x3c01u, 0x3c00u, 0x3c00u}},
{0xbf800000u, 0x3f801400u, 0x00000000u, {0xbc01u, 0xbc00u, 0xbc01u, 0xbc00u}},
{0x477ff000u, 0x3f800000u, 0x00000000u, {0x7c00u, 0x7c00u, 0x7bffu, 0x7bffu}},
{0xc77ff000u, 0x3f800000u, 0x00000000u, {0xfc00u, 0xfbffu, 0xfc00u, 0xfbffu}},
{0x49742400u, 0x3f800000u, 0x00000000u, {0x7c00u, 0x7c00u, 0x7bffu, 0x7bffu}},
{0x3fc00000u, 0x40000000u, 0x3e800000u, {0x4280u, 0x4280u, 0x4280u, 0x4280u}},
{0x3f800000u, 0x3f800000u, 0xbf7fffffu, {0x0001u, 0x0001u, 0x0001u, 0x0001u}},
{0xbf800000u, 0x3f800000u, 0x3f7fffffu, {0x8001u, 0x8001u, 0x8001u, 0x8001u}},
{0x3f800000u, 0x3f800000u, 0xbf800000u, {0x0000u, 0x0000u, 0x8000u, 0x0000u}},
{0x40000000u, 0x3f800000u, 0xc0000000u, {0x0000u, 0x0000u, 0x8000u, 0x0000u}},
{0x00000000u, 0x3f800000u, 0x80000000u, {0x0000u, 0x0000u, 0x8000u, 0x0000u}},
{0x80000000u, 0x3f800000u, 0x80000000u, {0x8000u, 0x8000u, 0x8000u, 0x8000u}},
{0x33000000u, 0x3f000000u, 0x00000000u, {0x0000u, 0x0001u, 0x0000u, 0x0000u}},
{0xb3000000u, 0x3f000000u, 0x00000000u, {0x8000u, 0x8000u, 0x8001u, 0x8000u}},
}};
float Quarter(std::uint32_t index) {
return static_cast<float>(static_cast<int>(index % 17u) - 8) / 4.0f;
}
@ -176,7 +201,7 @@ void FillRoundingInput() {
}
}
void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code) {
void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const std::optional<ShaderRecompiler::ShaderFloatMode>& floatMode = std::nullopt) {
Output.fill(0xdeadbeefu);
std::vector<std::uint32_t> userData(8, 0u);
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size()));
@ -192,6 +217,7 @@ void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code) {
{0, 0, 0, 128}
};
request.useCache = false;
request.context.floatMode = floatMode;
const auto result = ShaderRecompiler::Recompile(request);
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
device.WaitIdle();
@ -235,6 +261,25 @@ void CheckRounding() {
}
}
void FillDirectedInput() {
Input.fill(0u);
for (std::uint32_t tid = 0; tid < DirectedRows.size(); ++tid) {
const auto& row = DirectedRows[tid];
std::copy_n(std::array<std::uint32_t, 3>{row.a, row.b, row.c}.begin(), 3, Input.begin() + tid * Inputs);
}
}
void CheckDirected(std::uint32_t rounding) {
for (std::uint32_t tid = 0; tid < DirectedRows.size(); ++tid) {
const auto& row = DirectedRows[tid];
const auto* out = &Output[tid * Results];
const std::uint32_t expected = row.expected[rounding];
const auto where = "mix f16 rounding " + std::to_string(rounding) + ": thread " + std::to_string(tid) + " (" + Hex(row.a) + ", " + Hex(row.b) + ", " + Hex(row.c) + ")";
Require(out[0] == (0xabcd0000u | expected), where + " v_fma_mixlo_f16 is " + Hex(out[0]) + ", expected " + Hex(0xabcd0000u | expected));
Require(out[1] == ((expected << 16u) | 0xabcdu), where + " v_fma_mixhi_f16 is " + Hex(out[1]) + ", expected " + Hex((expected << 16u) | 0xabcdu));
}
}
}
int main() {
@ -252,6 +297,11 @@ int main() {
FillRoundingInput();
Run(*device, RoundingCode);
CheckRounding();
for (std::uint32_t rounding = 0; rounding < 4u; ++rounding) {
FillDirectedInput();
Run(*device, RoundingCode, ShaderRecompiler::ShaderFloatMode{0xc0u | (rounding << 2u), true, false, false});
CheckDirected(rounding);
}
std::puts("mix precision tests passed");
return 0;
} catch (const std::exception& error) {

View File

@ -0,0 +1,149 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <cstdio>
#include <initializer_list>
#include <iostream>
#include <span>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Width = 64;
constexpr std::uint32_t Height = 32;
constexpr std::uint32_t Columns = 7;
constexpr std::uint32_t Rows = 5;
alignas(256) std::array<std::byte, Width * Height * 4> Pixels{};
alignas(256) constexpr std::array<std::uint32_t, 44> VertexCode{
0xd7650015, 0x000100c1, 0xd7660015, 0x00022ac1, 0x7e0e0280, 0x7da206f9, 0x81060015, 0x360e0a81,
0xbefe04c1, 0xd4c50002, 0x00020e80, 0xd763000f, 0x00022a81, 0xd5010000, 0x000a1e80, 0x380000fa,
0xff091100, 0x380000fa, 0xff091200, 0x380000fa, 0xff091400, 0x380000fa, 0xff091800, 0xd778000f,
0x03058300, 0x38001f00, 0x88040302, 0xd7600002, 0x00013f00, 0xd760006b, 0x00017f00, 0x88026b02,
0xbf060402, 0x850580f2, 0xe0382000, 0x80020005, 0xbf8c3f70, 0x10000005, 0x10020205, 0x10040405,
0x10060605, 0xf80008cf, 0x03020100, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 4> PixelCode{
0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000,
};
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu};
}
std::vector<std::array<float, 4>> Triangles() {
std::vector<std::array<float, 4>> vertices;
for (std::uint32_t cell = 0; cell < Columns * Rows; ++cell) {
const float left = -1.0f + 2.0f * static_cast<float>(cell % Columns) / Columns;
const float right = -1.0f + 2.0f * static_cast<float>(cell % Columns + 1u) / Columns;
const float bottom = -1.0f + 2.0f * static_cast<float>(cell / Columns) / Rows;
const float top = -1.0f + 2.0f * static_cast<float>(cell / Columns + 1u) / Rows;
for (const auto& corner : {std::array{left, bottom}, std::array{right, bottom}, std::array{right, top}, std::array{left, bottom}, std::array{right, top}, std::array{left, top}}) {
vertices.push_back({corner[0], corner[1], 0.5f, 1.0f});
}
}
return vertices;
}
void Draw(AgcDriver::VulkanDevice& device) {
static const auto triangles = Triangles();
Pixels.fill(std::byte{0});
const auto target = device.Target();
std::vector<std::uint32_t> vertexUserData(4, 0u);
const auto vertexBuffer = BufferDescriptor(triangles.data(), 16u, static_cast<std::uint32_t>(triangles.size()));
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, {}},
{64u, 8, 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());
ShaderRecompiler::ShaderPixelStageInfo pixel{};
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);
const std::vector<std::uint32_t> pixelUserData(8, 0u);
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}};
ShaderRecompiler::RecompileRequest fragment{
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(PixelCode.data()), PixelCode, 0, {}},
{64u, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
target,
PixelPushLayout(vertexPush, target)
};
fragment.useCache = false;
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
const std::array<AgcDriver::Graphics::CompiledShader, 2> 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<std::uintptr_t>(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<float>(Height), static_cast<float>(Width), -static_cast<float>(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<std::uint32_t>(triangles.size()), 0, 1, 0, false};
device.Draw(state, draw, shaders);
device.WaitIdle();
}
void Check() {
for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) {
const auto actual = std::to_integer<unsigned>(Pixels[pixel * 4u]);
Require(actual == 255u, "NGG vertex wave reduction: pixel " + std::to_string(pixel) + " is " + std::to_string(actual) + ", expected 255 (a vertex whose wave OR of its lanes' bits, read from lanes 31 and 63, differs from its wave's ballot collapses its triangle)");
}
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
const auto subgroupSize = device->Target().subgroupSize;
if (subgroupSize != 32u) {
std::printf("skipped, s3 describes a 32-lane wave only on 32-lane subgroups, and the device has %u lanes\n", subgroupSize);
return VulkanTestSkipped;
}
if ((device->SubgroupStages() & VK_SHADER_STAGE_VERTEX_BIT) == 0u || !TargetHasCapability(device->Target(), spv::CapabilityGroupNonUniformArithmetic)) {
std::puts("skipped, the device has no subgroup arithmetic in vertex shaders");
return VulkanTestSkipped;
}
for (std::uint32_t pass = 0; pass < 3; ++pass) {
Draw(*device);
Check();
}
std::puts("NGG vertex wave reduction tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -0,0 +1,215 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <iterator>
#include <optional>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Threads = 64;
constexpr std::uint32_t Inputs = 4;
constexpr std::uint32_t Results = 8;
constexpr std::uint32_t Batches = 32;
alignas(256) std::array<std::uint32_t, Threads * Inputs> Input{};
alignas(256) std::array<std::uint32_t, Threads * Results> Output{};
alignas(256) constexpr std::array<std::uint32_t, 26> Code{
0x34020084, 0x34060085, 0xe0381000, 0x80000401, 0xbf8c3f70,
0xcc0f400a, 0x18020b04,
0xcc10400b, 0x18020b04,
0xcc0e400c, 0x1c1a0b04,
0xcc0f4a0d, 0x30020b04,
0xcc10c00e, 0x18020d04,
0xcc0e330f, 0x8c1a0b04,
0x7e200306, 0x78200b04,
0xcc0ec011, 0x3c1a0f04,
0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xbf810000,
};
constexpr const char* Names[Results] = {
"v_pk_add_f16 v10, v4, v5",
"v_pk_mul_f16 v11, v4, v5",
"v_pk_fma_f16 v12, v4, v5, v6",
"v_pk_add_f16 v13, v4, v5 op_sel:[1,0] op_sel_hi:[0,1] neg_lo:[1,0] neg_hi:[0,1]",
"v_pk_mul_f16 v14, v4, v6 clamp",
"v_pk_fma_f16 v15, v4, v5, v6 op_sel:[0,1,1] op_sel_hi:[1,0,0] neg_lo:[0,0,1] neg_hi:[1,1,0]",
"v_pk_fmac_f16 v16, v4, v5",
"v_pk_fma_f16 v17, v4, v7, v6 neg_lo:[1,0,0] clamp",
};
struct Mode {
const char* name;
std::optional<ShaderRecompiler::ShaderFloatMode> floatMode;
};
const std::array<Mode, 9> Modes{{
{"no FLOAT_MODE", std::nullopt},
{"dx10 ieee ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, true}},
{"dx10 ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}},
{"dx10 ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, true}},
{"dx10", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}},
{"ieee ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, true}},
{"ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, false}},
{"ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, true}},
{"none", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, false}},
}};
std::uint64_t Seed = 0x9e3779b97f4a7c15ull;
std::uint32_t Next() {
Seed += 0x9e3779b97f4a7c15ull;
std::uint64_t z = Seed;
z = (z ^ (z >> 30u)) * 0xbf58476d1ce4e5b9ull;
z = (z ^ (z >> 27u)) * 0x94d049bb133111ebull;
return static_cast<std::uint32_t>(z ^ (z >> 31u));
}
std::uint32_t Half() {
const std::uint32_t bits = Next();
const std::uint32_t sign = (bits >> 31u) << 15u;
const std::uint32_t mantissa = bits & 0x3ffu;
switch ((bits >> 16u) % 10u) {
case 0u: return sign;
case 1u: return sign | std::max(mantissa, 1u);
case 2u: return sign | 0x7c00u;
case 3u: return sign | 0x7c00u | std::max(mantissa, 1u);
case 4u: return sign | ((0x1du + (bits >> 20u) % 2u) << 10u) | mantissa;
case 5u: return sign | ((1u + (bits >> 20u) % 4u) << 10u) | mantissa;
case 6u: return sign | ((0x0du + (bits >> 20u) % 5u) << 10u) | mantissa;
default: return bits & 0xffffu;
}
}
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
}
std::string Hex(std::uint32_t value) {
char text[16];
std::snprintf(text, sizeof(text), "0x%08x", value);
return text;
}
struct F16x2Math {
bool add = false;
bool mul = false;
bool fma = false;
};
F16x2Math FindF16x2Math(std::span<const std::uint32_t> spirv) {
std::optional<std::uint32_t> half;
std::optional<std::uint32_t> pair;
F16x2Math math;
for (std::size_t offset = 5u; offset < spirv.size();) {
const std::uint32_t words = spirv[offset] >> 16u;
const std::uint32_t opcode = spirv[offset] & 0xffffu;
Require(words != 0u && offset + words <= spirv.size(), "packed f16 native: malformed SPIR-V");
if (opcode == spv::OpTypeFloat) {
Require(words >= 3u, "packed f16 native: malformed float type");
if (spirv[offset + 2u] == 16u) half = spirv[offset + 1u];
}
if (opcode == spv::OpTypeVector) {
Require(words >= 4u, "packed f16 native: malformed vector type");
if (half && spirv[offset + 2u] == *half && spirv[offset + 3u] == 2u) pair = spirv[offset + 1u];
}
if (opcode == spv::OpFAdd || opcode == spv::OpFMul || opcode == spv::OpExtInst) {
Require(words >= 5u, "packed f16 native: malformed arithmetic instruction");
if (pair && spirv[offset + 1u] == *pair) {
math.add = math.add || opcode == spv::OpFAdd;
math.mul = math.mul || opcode == spv::OpFMul;
math.fma = math.fma || opcode == spv::OpExtInst;
}
}
offset += words;
}
return math;
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
const auto target = device->Target();
const auto supports = [&](spv::Capability capability) {
return std::ranges::find(target.supportedCapabilities, static_cast<std::uint32_t>(capability)) != target.supportedCapabilities.end();
};
if (!supports(spv::CapabilityFloat16) || !supports(spv::CapabilityRoundingModeRTE) || !supports(spv::CapabilityDenormPreserve)) {
std::puts("packed f16 native: the device has no native f16 with RTE rounding and kept denormals, skipped");
return VulkanTestSkipped;
}
std::vector<std::uint32_t> withoutFloat16;
std::ranges::copy_if(target.supportedCapabilities, std::back_inserter(withoutFloat16), [](std::uint32_t capability) { return capability != spv::CapabilityFloat16; });
auto emulatedTarget = target;
emulatedTarget.supportedCapabilities = withoutFloat16;
const std::span<const std::uint32_t> code(Code);
std::vector<std::uint32_t> userData(8, 0u);
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u));
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
std::copy(input.begin(), input.end(), userData.begin());
std::copy(output.begin(), output.end(), userData.begin() + 4);
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
const auto compile = [&](const ShaderRecompiler::SpirvTarget& shaderTarget, const Mode& mode) {
ShaderRecompiler::RecompileRequest request{
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
shaderTarget,
{0, 0, 0, 128}
};
request.useCache = false;
request.context.floatMode = mode.floatMode;
return ShaderRecompiler::Recompile(request);
};
const auto run = [&](const auto& result) {
Output.fill(0xdeadbeefu);
device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
device->WaitIdle();
return Output;
};
std::uint64_t compared = 0u;
for (const auto& mode : Modes) {
const auto native = compile(target, mode);
const auto emulated = compile(emulatedTarget, mode);
const F16x2Math nativeMath = FindF16x2Math(native.spirv.Words());
const F16x2Math emulatedMath = FindF16x2Math(emulated.spirv.Words());
Require(nativeMath.add && nativeMath.mul, std::string("packed f16 native: ") + mode.name + " does not use f16vec2 add and mul");
Require(!nativeMath.fma, std::string("packed f16 native: ") + mode.name + " uses an f16vec2 Fma, which Vulkan does not require to be fused");
Require(!emulatedMath.add && !emulatedMath.mul && !emulatedMath.fma, std::string("packed f16 native: ") + mode.name + " uses f16vec2 math without Float16");
for (std::uint32_t batch = 0; batch < Batches; ++batch) {
for (auto& word : Input) word = Half() | (Half() << 16u);
const auto nativeOutput = run(native);
const auto emulatedOutput = run(emulated);
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
for (std::uint32_t index = 0; index < Results; ++index) {
const std::uint32_t actual = nativeOutput[tid * Results + index];
const std::uint32_t expected = emulatedOutput[tid * Results + index];
const std::uint32_t* row = &Input[tid * Inputs];
Require(actual == expected, std::string("packed f16 native: ") + mode.name + ", " + Names[index] + " with v4=" + Hex(row[0]) + " v5=" + Hex(row[1]) + " v6=" + Hex(row[2]) + " v7=" + Hex(row[3]) + " is " + Hex(actual) + ", f32 path gives " + Hex(expected));
}
}
compared += Threads * Results * 2u;
}
}
std::printf("packed f16 native tests passed (%llu halves)\n", static_cast<unsigned long long>(compared));
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -0,0 +1,276 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <condition_variable>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <mutex>
#include <span>
#include <string>
#include <string_view>
#include <vector>
namespace AgcDriver {
struct VulkanDeviceTestAccess {
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
};
}
namespace {
using namespace AgcDriver::Graphics;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Width = 64;
constexpr std::uint32_t Height = 16;
constexpr std::byte Kept{0x40};
alignas(256) std::array<std::byte, Width * Height * 4> Pixels{};
alignas(256) std::array<float, Width * Height> DepthMemory{};
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 7> MaskedPixelCode{
0x7e040f00, 0x36060481, 0x7da40680, 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 4> FullPixelCode{
0x7e0e02f2, 0xf800180f, 0x07070707, 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}
}};
constexpr std::array<std::array<float, 4>, 12> DepthTriangles{{
{-1.0f, -1.0f, 0.25f, 1.0f}, {0.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 1.0f, 0.25f, 1.0f},
{-1.0f, 1.0f, 0.25f, 1.0f}, {0.0f, -1.0f, 0.25f, 1.0f}, {0.0f, 1.0f, 0.25f, 1.0f},
{0.0f, -1.0f, 0.75f, 1.0f}, {1.0f, -1.0f, 0.75f, 1.0f}, {0.0f, 1.0f, 0.75f, 1.0f},
{0.0f, 1.0f, 0.75f, 1.0f}, {1.0f, -1.0f, 0.75f, 1.0f}, {1.0f, 1.0f, 0.75f, 1.0f}
}};
struct PipelineTrace {
PFN_vkGetDeviceProcAddr resolve = nullptr;
PFN_vkCreateGraphicsPipelines create = nullptr;
PFN_vkCmdBindPipeline bind = nullptr;
PFN_vkCmdDraw draw = nullptr;
DeviceFunctions functions{};
std::mutex mutex;
std::condition_variable released;
bool allowOptimization = false;
std::vector<VkPipeline> fast;
std::vector<VkPipeline> optimized;
VkCommandBuffer boundCommands = VK_NULL_HANDLE;
VkPipeline bound = VK_NULL_HANDLE;
std::vector<VkPipeline> drawn;
void ReleaseOptimization() {
{
std::lock_guard lock(mutex);
allowOptimization = true;
}
released.notify_all();
}
};
PipelineTrace* Trace = nullptr;
VKAPI_ATTR VkResult VKAPI_CALL CreatePipelines(VkDevice device, VkPipelineCache cache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks* allocator, VkPipeline* pipelines) {
const bool optimizing = std::any_of(infos, infos + count, [](const auto& info) { return (info.flags & VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT) != 0; });
if (optimizing) {
std::unique_lock lock(Trace->mutex);
Trace->released.wait(lock, [] { return Trace->allowOptimization; });
}
const auto result = Trace->create(device, cache, count, infos, allocator, pipelines);
std::lock_guard lock(Trace->mutex);
for (std::uint32_t index = 0; index < count; ++index) {
if (pipelines[index] == VK_NULL_HANDLE || (infos[index].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) != 0) continue;
auto& handles = (infos[index].flags & VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT) != 0 ? Trace->optimized : Trace->fast;
handles.push_back(pipelines[index]);
}
return result;
}
VKAPI_ATTR void VKAPI_CALL BindPipeline(VkCommandBuffer commands, VkPipelineBindPoint point, VkPipeline pipeline) {
if (point == VK_PIPELINE_BIND_POINT_GRAPHICS) {
Trace->boundCommands = commands;
Trace->bound = pipeline;
}
Trace->bind(commands, point, pipeline);
}
VKAPI_ATTR void VKAPI_CALL DrawCommands(VkCommandBuffer commands, std::uint32_t vertices, std::uint32_t instances, std::uint32_t firstVertex, std::uint32_t firstInstance) {
Trace->drawn.push_back(Trace->boundCommands == commands ? Trace->bound : VK_NULL_HANDLE);
Trace->draw(commands, vertices, instances, firstVertex, firstInstance);
}
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice device, const char* name) {
if (std::strcmp(name, "vkCreateGraphicsPipelines") == 0) return reinterpret_cast<PFN_vkVoidFunction>(CreatePipelines);
if (std::strcmp(name, "vkCmdBindPipeline") == 0) return reinterpret_cast<PFN_vkVoidFunction>(BindPipeline);
if (std::strcmp(name, "vkCmdDraw") == 0) return reinterpret_cast<PFN_vkVoidFunction>(DrawCommands);
return Trace->resolve(device, name);
}
struct OptimizationDrain {
VkDevice device;
~OptimizationDrain() {
Trace->ReleaseOptimization();
WaitForOptimizedPipelines(device);
}
};
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu};
}
void CheckPixels(bool masked, std::string_view phase) {
for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) {
const bool drawn = masked ? (pixel % Width) % 2u == 0u : pixel % Width < Width / 2u;
const auto expected = drawn ? std::byte{255} : Kept;
for (std::uint32_t channel = 0; channel < 4; ++channel) {
const auto actual = Pixels[pixel * 4u + channel];
Require(actual == expected, std::string(phase) + ": pixel " + std::to_string(pixel) + " channel " + std::to_string(channel) + " is " + std::to_string(static_cast<unsigned>(actual)) + ", expected " + std::to_string(static_cast<unsigned>(expected)));
}
}
}
void CheckDrawn(PipelineTrace& trace, bool optimized) {
std::lock_guard lock(trace.mutex);
const auto& expected = optimized ? trace.optimized : trace.fast;
Require(trace.drawn.size() == 1, "pipeline readback did not observe exactly one real draw");
Require(std::find(expected.begin(), expected.end(), trace.drawn.front()) != expected.end(), optimized ? "readback draw did not bind an optimized pipeline" : "readback draw did not bind a fast-linked pipeline");
}
void Run(AgcDriver::VulkanDevice& device, Context context, PipelineTrace& trace, std::uint32_t waveSize, VkFormat depthFormat, bool optimize) {
const bool masked = depthFormat == VK_FORMAT_UNDEFINED;
const auto target = device.Target();
const auto vertices = masked ? std::span<const std::array<float, 4>>(Triangle) : std::span<const std::array<float, 4>>(DepthTriangles);
const auto vertexBuffer = BufferDescriptor(vertices.data(), 16u, static_cast<std::uint32_t>(vertices.size()));
const std::vector<std::uint32_t> vertexUserData(vertexBuffer.begin(), vertexBuffer.end());
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());
const auto pixelCode = masked ? std::span<const std::uint32_t>(MaskedPixelCode) : std::span<const std::uint32_t>(FullPixelCode);
ShaderRecompiler::ShaderPixelStageInfo pixel{};
pixel.wave32 = waveSize == 32u;
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);
const std::vector<std::uint32_t> pixelUserData(8, 0u);
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(pixelCode.data()), std::as_bytes(pixelCode)}}};
ShaderRecompiler::RecompileRequest fragment{
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}},
{waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
target,
PixelPushLayout(vertexPush, target)
};
fragment.useCache = false;
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
const std::array<CompiledShader, 2> shaders{{
{ShaderStage::Vertex, &vertexResult, 0},
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}
}};
State state{};
state.stages = {ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt};
state.color = {reinterpret_cast<std::uintptr_t>(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<float>(Height), static_cast<float>(Width), -static_cast<float>(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};
if (!masked) {
state.depth = DepthTarget{reinterpret_cast<std::uintptr_t>(DepthMemory.data()), 0, {Width, Height}, depthFormat, 0.5f, 0};
state.depthTest = true;
state.depthCompare = VK_COMPARE_OP_LESS;
}
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(vertices.size()), 0, 1, 0, false};
const auto render = [&](bool optimized, std::string_view phase) {
Pixels.fill(Kept);
trace.drawn.clear();
trace.bound = VK_NULL_HANDLE;
trace.boundCommands = VK_NULL_HANDLE;
AgcDriver::Graphics::Draw(context, state, draw, shaders);
device.WaitIdle();
CheckDrawn(trace, optimized);
CheckPixels(masked, phase);
};
render(false, "fast link");
trace.ReleaseOptimization();
WaitForOptimizedPipelines(context.device);
Require(PipelineLibraryCountersOf(context.device).linked == 1, "the readback pipeline was not linked from graphics libraries");
Require(trace.optimized.size() == (optimize ? 1u : 0u), "the optimized pipeline creation count differs from the requested mode");
render(optimize, "completed link");
std::printf("pipeline readback passed: fast draws %u, optimized draws %u, optimized creations %zu\n", optimize ? 1u : 2u, optimize ? 1u : 0u, trace.optimized.size());
}
}
int main(int argc, char** argv) {
try {
std::string_view mode = "d32";
bool force = false;
for (int index = 1; index < argc; ++index) {
const std::string_view argument = argv[index];
if (argument == "--force-lto") force = true;
else {
Require(argument == "color32" || argument == "color64" || argument == "d32" || argument == "d16", "unknown pipeline readback case");
mode = argument;
}
}
PipelineTrace trace;
Trace = &trace;
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device);
if (!context.graphicsPipelineLibrary) {
std::puts("skipped, graphics pipeline libraries are unavailable or disabled");
return VulkanTestSkipped;
}
const bool masked = mode == "color32" || mode == "color64";
if (masked && device->Target().subgroupSize < 32u) {
std::puts("skipped, the masked pixel shader requires at least 32 subgroup lanes");
return VulkanTestSkipped;
}
if (force) context.pipelineLinkTimeOptimization = true;
const bool optimize = context.pipelineLinkTimeOptimization && std::getenv("APS5_NO_PIPELINE_LTO") == nullptr;
std::printf("pipeline readback: %s, %.*s, LTO %s%s\n", device->DeviceName().c_str(), static_cast<int>(mode.size()), mode.data(), optimize ? "enabled" : "disabled", force ? " (explicit test override)" : " (production policy)");
trace.resolve = context.deviceProc;
trace.create = context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines");
trace.bind = context.Function<PFN_vkCmdBindPipeline>("vkCmdBindPipeline");
trace.draw = context.Function<PFN_vkCmdDraw>("vkCmdDraw");
trace.functions = context.functions != nullptr ? *context.functions : DeviceFunctions{};
trace.functions.cmdBindPipeline = BindPipeline;
trace.functions.cmdDraw = DrawCommands;
context.functions = &trace.functions;
context.deviceProc = Resolve;
OptimizationDrain drain{context.device};
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
Run(*device, context, trace, mode == "color32" ? 32u : 64u, masked ? VK_FORMAT_UNDEFINED : (mode == "d16" ? VK_FORMAT_D16_UNORM : VK_FORMAT_D32_SFLOAT), optimize);
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -10,6 +10,7 @@
#include <future>
#include <cstring>
#include <iostream>
#include <optional>
#include <thread>
namespace {
@ -277,6 +278,28 @@ void Registration(AgcDriver::VulkanDevice& device) {
Require(registry->at(0x10000).back() == metadata, "changed shader metadata was treated as identical");
}
void TransactionLock() {
ShaderSnapshot shader{};
const auto publish = [&] {
ShaderPreparationTransaction other;
other.Edit(shader).rectangleRequested = true;
other.Commit();
};
std::optional<ShaderPreparationTransaction> open(std::in_place);
auto unread = std::async(std::launch::async, publish);
const bool unblocked = unread.wait_for(std::chrono::seconds(10)) == std::future_status::ready;
if (!unblocked) open.reset();
unread.get();
Require(unblocked, "an open transaction that read nothing blocked another one");
static_cast<void>(open->Read(shader));
auto waiting = std::async(std::launch::async, publish);
const bool excluded = waiting.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout;
open->Commit();
open.reset();
waiting.get();
Require(excluded, "a transaction ran beside one that had read prepared state");
}
}
int main() {
@ -284,6 +307,7 @@ int main() {
Transactions();
HeaderSelection();
FusedHullHeaderSelection();
TransactionLock();
auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
Registration(*device);

View File

@ -525,6 +525,54 @@ void Registration(bool indirect) {
Require(destination == 0, "failed dispatch executed a subsequent memory write");
}
void RegisteredVertexWorkgroup(const ShaderRecompiler::SpirvTarget& target, bool registrationOnly = false) {
if (!registrationOnly && ((target.subgroupSize != 32u && target.subgroupSize != 64u) || std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityMeshShadingEXT) == target.supportedCapabilities.end())) {
std::cout << "registered NGG wave64 preparation not run: requires mesh shading and a 32- or 64-lane host subgroup; subgroup=" << target.subgroupSize << '\n';
return;
}
alignas(256) static constexpr std::array<std::uint32_t, 8> code{
0xd8340000u, 0u, 0xd7600000u, 0x00017f00u, 0x7e020200u, 0xf80008cfu, 0x04030201u, 0xbf810000u
};
struct Header {
Shader shader{};
std::array<ShaderRegister, 4> registers{};
std::array<ShaderRegister, 3> context{{{0x1ff, 64}, {0x2ab, 4}, {0x2ce, 0}}};
ShaderSpecialRegs specials{{0x25b, 0x8040}, {0x2d5, 0x2000}, 0, {}, {}, {0x29b, 2}, {0x260, 0}};
ShaderUserData userData{};
} header;
const auto address = reinterpret_cast<std::uintptr_t>(code.data());
header.shader.file_header = 0x34333231u;
header.shader.version = 0x18;
header.shader.header_size = sizeof(header);
header.shader.shader_size = sizeof(code);
header.shader.code = code.data();
header.shader.type = 2;
header.shader.user_data = &header.userData;
header.shader.sh_registers = header.registers.data();
header.shader.num_sh_registers = header.registers.size();
header.shader.cx_registers = header.context.data();
header.shader.num_cx_registers = header.context.size();
header.shader.specials = &header.specials;
header.registers = {{{0xc8, static_cast<std::uint32_t>(address >> 8u)}, {0xc9, static_cast<std::uint32_t>(address >> 40u)}, {0x8a, 0x622c0187}, {0x8b, 5u << 19u}}};
AgcDriverRegisterShader_nid_postfix(&header.shader);
if (registrationOnly) {
header.shader.user_data = nullptr;
AgcDriverRegisterShader_nid_postfix(&header.shader);
header.specials.vgt_shader_stages_en.value = 0;
ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "missing AGC user-data header");
header.shader.user_data = &header.userData;
if (target.subgroupSize < 64u && std::getenv("APS5_SYNC_SHADER_PREPARE") != nullptr) {
ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "v_readlane_b32 of lane 63 is outside");
}
std::cout << "registered NGG preparation deferral passed; subgroup=" << target.subgroupSize << '\n';
return;
}
const std::array<ShaderRegister, 1> primitive{{{0x242, 4}}};
AgcDriverResolveShaderAbi_nid_postfix(&header.shader, {}, primitive);
AgcDriverResolveShaderAbi_nid_postfix(&header.shader, {}, primitive);
std::cout << "registered NGG wave64 preparation passed; subgroup=" << target.subgroupSize << '\n';
}
void DeferredRegistration() {
alignas(256) std::array<std::uint32_t, 2> code{0xbe842104u, 0xbf810000u};
struct Header {
@ -585,7 +633,14 @@ void DeferredUndecodableRegistration() {
int main(int argc, char** argv) {
try {
Require(argc == 1 || (argc == 2 && (std::string_view(argv[1]) == "--indirect" || std::string_view(argv[1]) == "--fail-before-registration" || std::string_view(argv[1]) == "--deferred" || std::string_view(argv[1]) == "--deferred-undecodable")), "invalid test arguments");
Require(argc == 1 || (argc == 2 && (std::string_view(argv[1]) == "--indirect" || std::string_view(argv[1]) == "--fail-before-registration" || std::string_view(argv[1]) == "--deferred" || std::string_view(argv[1]) == "--deferred-undecodable" || std::string_view(argv[1]) == "--registered-workgroup")), "invalid test arguments");
if (argc == 2 && std::string_view(argv[1]) == "--registered-workgroup") {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
RegisteredVertexWorkgroup(device->Target(), true);
AgcDriverShutdown_nid_postfix();
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--deferred") {
if (!OpenVulkanTestDevice()) return VulkanTestSkipped;
DeferredRegistration();
@ -601,6 +656,7 @@ int main(int argc, char** argv) {
auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
Run(*device);
RegisteredVertexWorkgroup(device->Target());
FailureCapture(*device);
PendingRegistrationPreparation(*device);
Require(argc != 2 || std::string_view(argv[1]) != "--fail-before-registration", "injected failure before registration");

View File

@ -17,14 +17,15 @@ namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Threads = 32;
constexpr std::uint32_t MaxThreads = 64;
constexpr std::uint32_t Inputs = 4;
constexpr std::uint32_t Results = 16;
alignas(256) std::array<std::uint32_t, Threads * Inputs> Input{};
alignas(256) std::array<std::uint32_t, Threads * Results> Output{};
constexpr std::uint32_t Results = 32;
constexpr std::uint32_t Checked = 19;
alignas(256) std::array<std::uint32_t, MaxThreads * Inputs> Input{};
alignas(256) std::array<std::uint32_t, MaxThreads * Results> Output{};
alignas(256) constexpr std::array<std::uint32_t, 81> Code{
0x34020084, 0x34060086, 0xe0301000, 0x80000401, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280,
alignas(256) constexpr std::array<std::uint32_t, 108> Code{
0x34020084, 0x34060087, 0xe0301000, 0x80000401, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280,
0x7e1c0280, 0x7e1e0280, 0x7e200280, 0x7e220280, 0x7e240280, 0x7e260280, 0x7e280280, 0xbf8c3f70,
0x7e3c0304, 0xd525001f, 0x00010304, 0xd5250020, 0x00010504, 0xd5250021, 0x00010704, 0xd5250022,
0x00010904, 0xd5250023, 0x00010b04, 0xd5250024, 0x00010d04, 0xd5250025, 0x00010f04, 0xbefc0382,
@ -34,44 +35,21 @@ alignas(256) constexpr std::array<std::uint32_t, 81> Code{
0x0000011f, 0x7e280323, 0xe0701000, 0x80010a03, 0xe0701004, 0x80010b03, 0xe0701008, 0x80010c03,
0xe070100c, 0x80010d03, 0xe0701010, 0x80010e03, 0xe0701014, 0x80010f03, 0xe0701018, 0x80011003,
0xe070101c, 0x80011103, 0xe0701020, 0x80011203, 0xe0701024, 0x80011303, 0xe0701028, 0x80011403,
0xbf810000,
0xbefc03c1, 0x7e2a8720, 0xe070102c, 0x80011503, 0x7e428504, 0xe0701030, 0x80012003, 0x7e468922,
0xe0701034, 0x80012203, 0xe0701038, 0x80012103, 0xbefc03ff, 0x000103ff, 0x7e3ed120, 0xe070103c,
0x80012003, 0x7e48911e, 0xe0701040, 0x80012503, 0x7e2c0327, 0xe0701044, 0x80011603, 0xbefc0388,
0x7e2e871e, 0xe0701048, 0x80011703, 0xbf810000,
};
constexpr std::uint32_t Expected[32][11] = {
{0x00001002u, 0x00001002u, 0x00001001u, 0x00001030u, 0x00001020u, 0x00001000u, 0x00001007u, 0x00001006u, 0x00001007u, 0x00001005u, 0x00001005u},
{0x00002002u, 0x00002002u, 0x00002001u, 0x00002030u, 0x00002020u, 0x00002000u, 0x00002007u, 0x00002006u, 0x00002007u, 0x00002005u, 0x00002005u},
{0x00003002u, 0x00003002u, 0x00003001u, 0x00003030u, 0x00003020u, 0x00003000u, 0x00003007u, 0x00003006u, 0x00003007u, 0x00003005u, 0x00003005u},
{0x00004002u, 0x00004002u, 0x00004001u, 0x00004030u, 0x00004020u, 0x00004000u, 0x00004007u, 0x00004006u, 0x00004007u, 0x00004005u, 0x00004005u},
{0x00005002u, 0x00005002u, 0x00005001u, 0x00005030u, 0x00005020u, 0x00005000u, 0x00005007u, 0x00005006u, 0x00005007u, 0x00005005u, 0x00005005u},
{0x00006002u, 0x00006002u, 0x00006001u, 0x00006030u, 0x00006020u, 0x00006000u, 0x00006007u, 0x00006006u, 0x00006007u, 0x00006005u, 0x00006005u},
{0x00007002u, 0x00007002u, 0x00007001u, 0x00007030u, 0x00007020u, 0x00007000u, 0x00007007u, 0x00007006u, 0x00007007u, 0x00007005u, 0x00007005u},
{0x00008002u, 0x00008002u, 0x00008001u, 0x00008030u, 0x00008020u, 0x00008000u, 0x00008007u, 0x00008006u, 0x00008007u, 0x00008005u, 0x00008005u},
{0x00009002u, 0x00009002u, 0x00009001u, 0x00009030u, 0x00009020u, 0x00009000u, 0x00009007u, 0x00009006u, 0x00009007u, 0x00009005u, 0x00009005u},
{0x0000a002u, 0x0000a002u, 0x0000a001u, 0x0000a030u, 0x0000a020u, 0x0000a000u, 0x0000a007u, 0x0000a006u, 0x0000a007u, 0x0000a005u, 0x0000a005u},
{0x0000b002u, 0x0000b002u, 0x0000b001u, 0x0000b030u, 0x0000b020u, 0x0000b000u, 0x0000b007u, 0x0000b006u, 0x0000b007u, 0x0000b005u, 0x0000b005u},
{0x0000c002u, 0x0000c002u, 0x0000c001u, 0x0000c030u, 0x0000c020u, 0x0000c000u, 0x0000c007u, 0x0000c006u, 0x0000c007u, 0x0000c005u, 0x0000c005u},
{0x0000d002u, 0x0000d002u, 0x0000d001u, 0x0000d030u, 0x0000d020u, 0x0000d000u, 0x0000d007u, 0x0000d006u, 0x0000d007u, 0x0000d005u, 0x0000d005u},
{0x0000e002u, 0x0000e002u, 0x0000e001u, 0x0000e030u, 0x0000e020u, 0x0000e000u, 0x0000e007u, 0x0000e006u, 0x0000e007u, 0x0000e005u, 0x0000e005u},
{0x0000f002u, 0x0000f002u, 0x0000f001u, 0x0000f030u, 0x0000f020u, 0x0000f000u, 0x0000f007u, 0x0000f006u, 0x0000f007u, 0x0000f005u, 0x0000f005u},
{0x00010002u, 0x00010002u, 0x00010001u, 0x00010030u, 0x00010020u, 0x00010000u, 0x00010007u, 0x00010006u, 0x00010007u, 0x00010005u, 0x00010005u},
{0x00011002u, 0x00011002u, 0x00011001u, 0x00011030u, 0x00011020u, 0x00011000u, 0x00011007u, 0x00011006u, 0x00011007u, 0x00011005u, 0x00011005u},
{0x00012002u, 0x00012002u, 0x00012001u, 0x00012030u, 0x00012020u, 0x00012000u, 0x00012007u, 0x00012006u, 0x00012007u, 0x00012005u, 0x00012005u},
{0x00013002u, 0x00013002u, 0x00013001u, 0x00013030u, 0x00013020u, 0x00013000u, 0x00013007u, 0x00013006u, 0x00013007u, 0x00013005u, 0x00013005u},
{0x00014002u, 0x00014002u, 0x00014001u, 0x00014030u, 0x00014020u, 0x00014000u, 0x00014007u, 0x00014006u, 0x00014007u, 0x00014005u, 0x00014005u},
{0x00015002u, 0x00015002u, 0x00015001u, 0x00015030u, 0x00015020u, 0x00015000u, 0x00015007u, 0x00015006u, 0x00015007u, 0x00015005u, 0x00015005u},
{0x00016002u, 0x00016002u, 0x00016001u, 0x00016030u, 0x00016020u, 0x00016000u, 0x00016007u, 0x00016006u, 0x00016007u, 0x00016005u, 0x00016005u},
{0x00017002u, 0x00017002u, 0x00017001u, 0x00017030u, 0x00017020u, 0x00017000u, 0x00017007u, 0x00017006u, 0x00017007u, 0x00017005u, 0x00017005u},
{0x00018002u, 0x00018002u, 0x00018001u, 0x00018030u, 0x00018020u, 0x00018000u, 0x00018007u, 0x00018006u, 0x00018007u, 0x00018005u, 0x00018005u},
{0x00019002u, 0x00019002u, 0x00019001u, 0x00019030u, 0x00019020u, 0x00019000u, 0x00019007u, 0x00019006u, 0x00019007u, 0x00019005u, 0x00019005u},
{0x0001a002u, 0x0001a002u, 0x0001a001u, 0x0001a030u, 0x0001a020u, 0x0001a000u, 0x0001a007u, 0x0001a006u, 0x0001a007u, 0x0001a005u, 0x0001a005u},
{0x0001b002u, 0x0001b002u, 0x0001b001u, 0x0001b030u, 0x0001b020u, 0x0001b000u, 0x0001b007u, 0x0001b006u, 0x0001b007u, 0x0001b005u, 0x0001b005u},
{0x0001c002u, 0x0001c002u, 0x0001c001u, 0x0001c030u, 0x0001c020u, 0x0001c000u, 0x0001c007u, 0x0001c006u, 0x0001c007u, 0x0001c005u, 0x0001c005u},
{0x0001d002u, 0x0001d002u, 0x0001d001u, 0x0001d030u, 0x0001d020u, 0x0001d000u, 0x0001d007u, 0x0001d006u, 0x0001d007u, 0x0001d005u, 0x0001d005u},
{0x0001e002u, 0x0001e002u, 0x0001e001u, 0x0001e030u, 0x0001e020u, 0x0001e000u, 0x0001e007u, 0x0001e006u, 0x0001e007u, 0x0001e005u, 0x0001e005u},
{0x0001f002u, 0x0001f002u, 0x0001f001u, 0x0001f030u, 0x0001f020u, 0x0001f000u, 0x0001f007u, 0x0001f006u, 0x0001f007u, 0x0001f005u, 0x0001f005u},
{0x00020002u, 0x00020002u, 0x00020001u, 0x00020030u, 0x00020020u, 0x00020000u, 0x00020007u, 0x00020006u, 0x00020007u, 0x00020005u, 0x00020005u},
};
constexpr const char* Names[11] = {"movrelsd", "swapped_2", "sd2_untouched", "swaprel_dst", "swaprel_src", "swaprel_other", "swap_dst", "swap_src", "swaprel_same", "swaprel_same_other", "movrelsd_e64_out_of_range"};
constexpr const char* Names[Checked] = {"movrelsd", "swapped_2", "sd2_untouched", "swaprel_dst", "swaprel_src", "swaprel_other", "swap_dst", "swap_src", "swaprel_same", "swaprel_same_other", "movrelsd_e64_dst",
"movrels_out_of_range", "movreld_out_of_range_neighbour", "movrelsd_out_of_range_source", "movrelsd_out_of_range_other", "swaprel_one_side_out_of_range", "movrelsd_2_source_out_of_range",
"unwritten_direct", "unwritten_relative"};
std::array<std::uint32_t, Checked> ExpectedFor(std::uint32_t tid) {
const std::uint32_t x = 0x1000u * (tid + 1u);
return {x + 2u, x + 2u, x + 1u, x + 0x30u, x + 0x20u, x, x + 7u, x + 6u, x + 7u, x + 5u, x + 5u,
tid, x + 0x30u, x + 2u, x + 0x20u, x + 0x30u, tid, 0u, 0u};
}
void Fill(std::uint32_t tid, std::uint32_t* words) {
words[0] = 0x1000u * (tid + 1u);
@ -88,12 +66,12 @@ std::string Hex(std::uint32_t value) {
return text;
}
void Expect(std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, const char* name) {
Require(actual == expected, std::string("relative indexing: lane ") + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected));
void Expect(std::uint32_t waveSize, std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, const char* name) {
Require(actual == expected, std::string("relative indexing: wave") + std::to_string(waveSize) + " lane " + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected));
}
void Run(AgcDriver::VulkanDevice& device) {
for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]);
void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::uint32_t threads) {
for (std::uint32_t tid = 0; tid < threads; ++tid) Fill(tid, &Input[tid * Inputs]);
Output.fill(0xdeadbeefu);
std::vector<std::uint32_t> userData(8, 0u);
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u));
@ -102,10 +80,10 @@ void Run(AgcDriver::VulkanDevice& device) {
std::copy(output.begin(), output.end(), userData.begin() + 4);
const std::span<const std::uint32_t> code(Code);
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
const ShaderRecompiler::ShaderComputeStageInfo compute{{threads, 1, 1}, 0u, {false, false, false}, false, 1};
ShaderRecompiler::RecompileRequest request{
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
{waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory},
device.Target(),
{0, 0, 0, 128}
};
@ -115,11 +93,11 @@ void Run(AgcDriver::VulkanDevice& device) {
device.WaitIdle();
}
void Check() {
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
const std::uint32_t* in = &Input[tid * Inputs];
void Check(std::uint32_t waveSize, std::uint32_t threads) {
for (std::uint32_t tid = 0; tid < threads; ++tid) {
const std::uint32_t* out = &Output[tid * Results];
for (std::uint32_t index = 0; index < 11; ++index) Expect(tid, out[index], Expected[tid][index], Names[index]);
const auto expected = ExpectedFor(tid);
for (std::uint32_t index = 0; index < Checked; ++index) Expect(waveSize, tid, out[index], expected[index], Names[index]);
}
}
@ -129,8 +107,10 @@ int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
Run(*device);
Check();
Run(*device, 32, 32);
Check(32, 32);
Run(*device, 64, 64);
Check(64, 64);
std::puts("relative indexing tests passed");
return 0;
} catch (const std::exception& error) {

View File

@ -5,6 +5,8 @@
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureCache.hpp"
#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "VulkanTestDevice.hpp"
#include <SDL_loadso.h>
@ -45,7 +47,7 @@ constexpr std::uint64_t Ceiling = Budget + Budget / 4u;
class Device {
public:
Device() {
explicit Device(bool resourceImages = false) {
#ifdef _WIN32
library = SDL_LoadObject("vulkan-1.dll");
#else
@ -93,6 +95,11 @@ public:
queue.pQueuePriorities = &priority;
VkPhysicalDeviceFeatures enabled{};
enabled.shaderInt64 = VK_TRUE;
if (resourceImages) {
VkPhysicalDeviceFeatures supported{};
function<PFN_vkGetPhysicalDeviceFeatures>("vkGetPhysicalDeviceFeatures")(context.physical, &supported);
enabled.shaderStorageImageMultisample = supported.shaderStorageImageMultisample;
}
address.pNext = &bytes;
std::vector<const char*> extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME};
if (hasExtension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME)) {
@ -207,6 +214,115 @@ GuestTextureResource Resource(std::uint64_t address, std::uint32_t side = Side)
return DecodeTextureResource(words);
}
std::shared_ptr<ShaderResources> ImageResources(const Context& context, const Block& block, std::size_t first, std::uint32_t count, bool sampled) {
ShaderRecompiler::DescriptorBinding binding{};
binding.kind = sampled ? ShaderRecompiler::DescriptorKind::SampledImage : ShaderRecompiler::DescriptorKind::StorageImage;
binding.role = ShaderRecompiler::DescriptorRole::GuestImages;
binding.descriptorSet = 0;
binding.binding = (sampled ? ShaderRecompiler::RuntimeAbi::FirstImageBinding : ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding) + PlaceholderDimensionsPerClass + 2u;
binding.count = count;
binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D;
binding.imageWritten.assign(count, false);
binding.imageSamplers.assign(count, 0u);
for (std::uint32_t element = 0; element < count; ++element) {
const auto words = Descriptor(block.Surface(first + element));
binding.guestDescriptor.insert(binding.guestDescriptor.end(), words.begin(), words.end());
}
ShaderRecompiler::RecompileResult program;
program.bindings.push_back(std::move(binding));
const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0};
return std::make_shared<ShaderResources>(context, shader);
}
bool resourceCacheTests(Context context, const std::string& mode) {
PlaceholderImages placeholders(context);
context.placeholderImages = &placeholders;
TextureCache textures(context);
context.textureCache = &textures;
Block block(context);
if (context.hostImportAlignment == 0 || HostImportFor(context, block.Surface(0), BlockBytes) == nullptr) {
Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "resource cache tests require host imports");
std::puts("skipped, resource cache tests require host imports");
return false;
}
auto& cache = SharedResourceCache();
cache.Clear();
try {
if (mode == "resource-clear") {
auto storage = ImageResources(context, block, 0, 1, false);
auto sampled = ImageResources(context, block, 0, 1, true);
Require(sampled->SampledTextures().front()->ViewsStorageImage(), "the sampled resource is not a storage-image view");
const std::weak_ptr<StorageTexture> image = StorageTexture::FindLive(block.Surface(0), SurfaceBytes);
const std::weak_ptr<ShaderResources> storageSet = storage;
const std::weak_ptr<ShaderResources> sampledSet = sampled;
cache.Insert({1}, storage);
cache.Insert({2}, sampled);
Require(cache.Size() == 2, "the resource cache did not accept the live descriptor sets");
sampled.reset();
ClearCachedTextures(context.device);
Require(cache.Find({1}) == nullptr && cache.Find({2}) == nullptr && cache.Size() == 0, "clearing storage images left their resources cached");
Require(sampledSet.expired(), "cache clear kept the sampled descriptor set alive");
Require(!image.expired() && !storageSet.expired(), "cache clear destroyed an active caller's resources");
storage.reset();
Require(image.expired() && storageSet.expired(), "cleared storage image outlived its last active resource");
} else if (mode == "resource-oversized") {
for (const bool sampledImages : {false, true}) {
const std::weak_ptr<StorageTexture> early = CachedStorageSurface(context, Resource(block.Surface(0)));
auto oversized = ImageResources(context, block, 0, static_cast<std::uint32_t>(Surfaces), sampledImages);
const std::weak_ptr<ShaderResources> rejected = oversized;
auto image = early.lock();
Require(oversized->Reusable(), "the oversized descriptor set is not eligible for resource caching");
Require(image != nullptr && !image->Cached(), "the oversized descriptor set did not evict an early image while building");
if (sampledImages) Require(oversized->SampledTextures().size() == Surfaces && oversized->SampledTextures().front()->StorageSource() == image.get(), "the oversized sampled descriptor set does not retain the early storage image");
std::vector<std::shared_ptr<ShaderResources>> displaced;
cache.Insert({1}, oversized, &displaced);
Require(cache.Size() == 0 && displaced.empty(), sampledImages ? "an oversized sampled descriptor set entered the resource cache" : "an oversized storage descriptor set entered the resource cache");
image.reset();
oversized.reset();
TextureCacheUsage();
Require(rejected.expired() && early.expired(), "the rejected oversized descriptor set retained an evicted storage image");
std::puts(sampledImages ? "resource cache oversized sampled lifetime passed" : "resource cache oversized storage lifetime passed");
ClearCachedTextures(context.device);
}
} else {
auto storage = ImageResources(context, block, 0, 1, false);
auto sampled = ImageResources(context, block, 0, 1, true);
Require(storage->Reusable() && sampled->Reusable(), "the stale descriptor sets are not eligible for resource caching");
Require(sampled->SampledTextures().front()->ViewsStorageImage(), "the sampled resource is not a storage-image view");
auto image = StorageTexture::FindLive(block.Surface(0), SurfaceBytes);
const std::weak_ptr<StorageTexture> staleImage = image;
const std::weak_ptr<ShaderResources> storageSet = storage;
const std::weak_ptr<ShaderResources> sampledSet = sampled;
Require(image != nullptr && image->Cached(), "the resource has no cached storage image");
for (std::size_t surface = 1; surface < Surfaces && image->Cached(); ++surface) CachedStorageSurface(context, Resource(block.Surface(surface)));
Require(!image->Cached(), "the test never evicted the oldest storage image");
auto live = ImageResources(context, block, Surfaces - 1, 1, false);
cache.Insert({1}, live);
Require(cache.Find({1}) == live, "the resource cache rejected a live descriptor set");
std::vector<std::shared_ptr<ShaderResources>> displaced;
for (const auto& stale : {storage, sampled}) {
cache.Insert({2}, stale, &displaced);
Require(cache.Find({2}) == nullptr && cache.Size() == 1 && displaced.empty(), "an uncached image was admitted to the resource cache");
cache.Insert({1}, stale, &displaced);
Require(cache.Find({1}) == live && cache.Size() == 1 && displaced.empty(), "an uncached image replaced a live resource-cache entry");
}
image.reset();
storage.reset();
Require(storageSet.expired() && !sampledSet.expired() && !staleImage.expired(), "stale storage release did not preserve the sampled caller's image");
std::puts("resource cache stale storage caller release passed");
sampled.reset();
TextureCacheUsage();
Require(sampledSet.expired() && staleImage.expired(), "the rejected descriptor sets retained an evicted storage image");
std::puts("resource cache stale sampled caller release passed");
}
} catch (...) {
cache.Clear();
throw;
}
cache.Clear();
return true;
}
void viewTests(const Context& context) {
if (context.hostImportAlignment == 0) {
std::puts("host imports unavailable: views of storage images not tested");
@ -482,6 +598,8 @@ void memoryTests(Context context) {
int main(int argc, char** argv) {
try {
const bool memory = argc > 1 && std::string(argv[1]) == "memory";
const std::string mode = argc > 1 ? argv[1] : "";
const bool resources = mode == "resource-admission" || mode == "resource-oversized" || mode == "resource-clear";
#ifdef _WIN32
if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", "1");
#else
@ -489,7 +607,7 @@ int main(int argc, char** argv) {
#endif
std::unique_ptr<Device> device;
try {
device = std::make_unique<Device>();
device = std::make_unique<Device>(resources);
} catch (const std::exception& error) {
if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw;
std::printf("skipped, no usable Vulkan device: %s\n", error.what());
@ -498,6 +616,12 @@ int main(int argc, char** argv) {
auto context = device->GetContext();
TextureDetiler detiler(context);
context.detiler = &detiler;
if (resources) {
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
if (!resourceCacheTests(context, mode)) return VulkanTestSkipped;
std::puts("resource cache tests passed");
return 0;
}
if (memory) {
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
memoryTests(context);

View File

@ -0,0 +1,116 @@
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <thread>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
constexpr std::uint32_t Threads = 8;
constexpr std::uint32_t Side = 256;
constexpr std::uint32_t Format32UInt = 20;
constexpr std::uint32_t TileR64KBX = 0x1b;
constexpr std::uint32_t Type2D = 9;
constexpr std::size_t BlockBytes = 0x60000;
constexpr std::uint32_t Untouched = 0xdeadbeefu;
alignas(256) constexpr std::array<std::uint32_t, 7> StoreCode{
0x7e040300, 0x7e060280, 0x7e0202ff, 0x12345678u, 0xf0200108, 0x00020102, 0xbf810000,
};
std::uint64_t AddressOf(const void* data) {
return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data));
}
std::array<std::uint32_t, 8> TextureDescriptor(const std::uint32_t* texels) {
const auto address = AddressOf(texels);
return {
static_cast<std::uint32_t>(address >> 8u),
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u),
((Side - 1u) >> 2u) | ((Side - 1u) << 14u),
0xfacu | (TileR64KBX << 20u) | (Type2D << 28u),
0u,
0u,
0u,
0u,
};
}
std::vector<std::uint32_t> Commands(const std::uint32_t* texels) {
std::vector<std::uint32_t> userData(16, 0u);
const auto texture = TextureDescriptor(texels);
std::copy(texture.begin(), texture.end(), userData.begin() + 8);
std::vector<std::uint32_t> words;
const auto registers = [&](const std::uint32_t first, const std::span<const std::uint32_t> values) {
words.push_back(0xc0007600u | (static_cast<std::uint32_t>(values.size()) << 16u));
words.push_back(first);
words.insert(words.end(), values.begin(), values.end());
};
const std::array<std::uint32_t, 3> shape{Threads, 1, 1};
registers(0x207, shape);
const auto address = reinterpret_cast<std::uintptr_t>(StoreCode.data());
const std::array<std::uint32_t, 2> program{static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>(address >> 40u)};
registers(0x20c, program);
const std::array<std::uint32_t, 1> resource{static_cast<std::uint32_t>(userData.size()) << 1u};
registers(0x213, resource);
registers(0x240, userData);
words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u});
return words;
}
void Store(const std::uint32_t* texels) {
auto words = Commands(texels);
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
AgcDriver::Submit(&packet, 0);
}
}
int main() {
try {
{
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
}
std::vector<std::uint32_t> storage((BlockBytes + 0x10000u) / 4u);
auto* texels = reinterpret_cast<std::uint32_t*>((reinterpret_cast<std::uintptr_t>(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu});
std::fill(texels, texels + BlockBytes / 4u, Untouched);
{
GuestAllocations::Mutation mutation;
mutation.Add(texels, BlockBytes, true, true, true);
}
const auto bytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels))).guestBytes;
const volatile std::uint32_t* cpu = texels;
const auto stored = [&] {
for (std::size_t index = 0; index < bytes / 4u; ++index) {
if (cpu[index] == 0x12345678u) return true;
}
return false;
};
Store(texels);
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
while (!stored() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1));
Require(stored(), "the queue-idle store did not bring the result to guest memory without a direct flush or further submission");
AgcDriverShutdown_nid_postfix();
{
GuestAllocations::Mutation mutation;
mutation.Remove(texels);
}
std::puts("store idle results tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -0,0 +1,209 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Threads = 64;
constexpr std::uint32_t Columns = 9;
constexpr std::uint32_t Ones = 0xffffffffu;
constexpr std::uint32_t Marker = 0x5a5a5a5au;
alignas(256) std::array<std::uint32_t, Threads> Input{};
alignas(256) std::array<std::uint32_t, Threads * Columns> Output{};
alignas(256) constexpr std::array<std::uint32_t, 49> ExecKeptCode{
0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280,
0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000a, 0x80148114, 0x4a141481,
0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850001, 0xbe26fff6,
0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000, 0x80010a01,
0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400, 0x80010e01,
0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800, 0x80011201,
0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 50> ExecResetCode{
0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280,
0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000b, 0x80148114, 0x4a141481,
0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850002, 0xbefe04c1,
0xbe26fff5, 0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000,
0x80010a01, 0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400,
0x80010e01, 0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800,
0x80011201, 0xbf810000,
};
constexpr std::array<const char*, Columns> Names{"passes", "last pass", "s38 in the pass", "exec lo in the pass", "exec hi in the pass", "s38 after the loop", "exec lo after the loop", "exec hi after the loop", "total passes"};
struct Group {
std::uint32_t first;
std::uint32_t last;
std::array<std::uint32_t, Columns> values;
};
struct Case {
const char* name;
std::uint64_t mask;
std::vector<Group> groups;
};
const std::vector<Case> ExecKeptCases{
{"all lanes", ~0ull, {
{0, 31, {1, 1, Ones, Ones, 0, Ones, Ones, Ones, 2}},
{32, 63, {1, 2, Ones, 0, Ones, Ones, Ones, Ones, 2}},
}},
{"low half", 0xffffffffull, {
{0, 31, {1, 1, 0, Ones, 0, 0, Ones, 0, 1}},
{32, 63, {0, 0, 0, 0, 0, 0, Ones, 0, 1}},
}},
{"high half", 0xffffffffull << 32, {
{0, 31, {0, 0, 0, 0, 0, 0, 0, Ones, 1}},
{32, 63, {1, 1, 0, 0, Ones, 0, 0, Ones, 1}},
}},
{"no lane", 0ull, {
{0, 63, {0, 0, 0, 0, 0, Marker, 0, 0, 0}},
}},
{"lanes 3 and 40", (1ull << 3) | (1ull << 40), {
{0, 63, {0, 0, 0, 0, 0, 8, 8, 0x100u, 2}},
{3, 3, {1, 1, 0x100u, 8, 0, 8, 8, 0x100u, 2}},
{40, 40, {1, 2, 8, 0, 0x100u, 8, 8, 0x100u, 2}},
}},
{"lane 3", 1ull << 3, {
{0, 63, {0, 0, 0, 0, 0, 0, 8, 0, 1}},
{3, 3, {1, 1, 0, 8, 0, 0, 8, 0, 1}},
}},
{"lane 40", 1ull << 40, {
{0, 63, {0, 0, 0, 0, 0, 0, 0, 0x100u, 1}},
{40, 40, {1, 1, 0, 0, 0x100u, 0, 0, 0x100u, 1}},
}},
};
const std::vector<Case> ExecResetCases{
{"all lanes", ~0ull, {
{0, 63, {0, 0, 0, 0, 0, Ones, Ones, Ones, 1}},
{0, 31, {1, 1, Ones, Ones, 0, Ones, Ones, Ones, 1}},
}},
{"low half", 0xffffffffull, {
{0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}},
{0, 31, {1, 1, 0, Ones, 0, 0, 0, Ones, 1}},
}},
{"high half", 0xffffffffull << 32, {
{0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}},
{32, 63, {1, 1, 0, 0, Ones, 0, 0, Ones, 1}},
}},
{"no lane", 0ull, {
{0, 63, {0, 0, 0, 0, 0, Marker, 0, 0, 0}},
}},
{"lanes 3 and 40", (1ull << 3) | (1ull << 40), {
{0, 63, {0, 0, 0, 0, 0, 0x100u, 0x100u, Ones, 1}},
{3, 3, {1, 1, 0x100u, 8, 0, 0x100u, 0x100u, Ones, 1}},
}},
{"lane 3", 1ull << 3, {
{0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}},
{3, 3, {1, 1, 0, 8, 0, 0, 0, Ones, 1}},
}},
{"lane 40", 1ull << 40, {
{0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}},
{40, 40, {1, 1, 0, 0, 0x100u, 0, 0, Ones, 1}},
}},
};
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
}
void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, std::uint64_t mask) {
for (std::uint32_t lane = 0; lane < Threads; ++lane) Input[lane] = ((mask >> lane) & 1u) != 0u ? 1u : 0u;
Output.fill(0xdeadbeefu);
std::vector<std::uint32_t> userData(8, 0u);
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u));
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
std::copy(input.begin(), input.end(), userData.begin());
std::copy(output.begin(), output.end(), userData.begin() + 4);
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1};
ShaderRecompiler::RecompileRequest request{
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
{64, 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<std::uintptr_t>(code.data()));
device.WaitIdle();
}
void Check(const char* variant, const Case& testCase) {
std::array<std::array<std::uint32_t, Columns>, Threads> expected{};
for (const auto& group : testCase.groups) {
for (std::uint32_t lane = group.first; lane <= group.last; ++lane) expected[lane] = group.values;
}
for (std::uint32_t lane = 0; lane < Threads; ++lane) {
for (std::uint32_t column = 0; column < Columns; ++column) {
const auto actual = Output[column * Threads + lane];
Require(actual == expected[lane][column], std::string("subvector loop ") + variant + ", initial exec " + testCase.name + ": lane " + std::to_string(lane) + " " + Names[column] + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[lane][column]));
}
}
}
void CheckWave32Throws(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code) {
std::vector<std::uint32_t> userData(8, 0u);
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
const ShaderRecompiler::ShaderComputeStageInfo compute{{32, 1, 1}, 0, {false, false, false}, false, 1};
ShaderRecompiler::RecompileRequest request{
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
device.ComputeTarget(32),
{0, 0, 0, 128}
};
request.useCache = false;
try {
static_cast<void>(ShaderRecompiler::Recompile(request));
} catch (const std::runtime_error& error) {
Require(std::string(error.what()).find("s_subvector_loop_begin in a wave32 shader is not measured") != std::string::npos, std::string("wave32 subvector loop threw an unexpected error: ") + error.what());
return;
}
Require(false, "a wave32 subvector loop was recompiled");
}
void RunCases(AgcDriver::VulkanDevice& device, const char* variant, std::span<const std::uint32_t> code, const std::vector<Case>& cases) {
for (const auto& testCase : cases) {
Run(device, code, testCase.mask);
Check(variant, testCase);
}
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
CheckWave32Throws(*device, ExecKeptCode);
if (device->Target().subgroupSize < 32u) {
std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize);
return VulkanTestSkipped;
}
RunCases(*device, "exec kept by the body", ExecKeptCode, ExecKeptCases);
RunCases(*device, "body resets exec", ExecResetCode, ExecResetCases);
std::puts("subvector loop tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -31,6 +31,7 @@
#include <iostream>
#include <span>
#include <string>
#include <string_view>
#include <vector>
namespace {
@ -53,6 +54,12 @@ alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 9> StoringVertexCode{
0xe0382000, 0x80000005, 0xbf8c3f70, 0xe0700000, 0x80010000, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
};
alignas(256) std::array<std::uint32_t, 64> Stored{};
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}
}};
@ -163,16 +170,19 @@ VkPipelineColorBlendAttachmentState Blend(VkBlendFactor source, VkBlendFactor de
return blend;
}
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const VkPipelineColorBlendAttachmentState& blend, std::span<const std::uint32_t> pixelCode) {
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const VkPipelineColorBlendAttachmentState& blend, std::span<const std::uint32_t> pixelCode, std::span<const std::uint32_t> vertexCode = VertexCode) {
const auto target = device.Target();
constexpr std::uint32_t waveSize = 64;
std::vector<std::uint32_t> vertexUserData(4, 0u);
std::vector<std::uint32_t> vertexUserData(8, 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))}}};
const auto stored = reinterpret_cast<std::uintptr_t>(Stored.data());
const std::array<std::uint32_t, 4> storeBuffer{static_cast<std::uint32_t>(stored), static_cast<std::uint32_t>((stored >> 32u) & 0xffffu), static_cast<std::uint32_t>(sizeof(Stored)), 0x01016facu};
std::copy(storeBuffer.begin(), storeBuffer.end(), vertexUserData.begin() + 4);
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(vertexCode.data()), std::as_bytes(vertexCode)}}};
ShaderRecompiler::RecompileRequest vertex{
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
{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}
@ -281,6 +291,18 @@ int main() {
{"ONE + ONE_MINUS_SRC_ALPHA for color and alpha", Blend(VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD), false,
[&](float r, float g) { return std::array<float, 2>{x + r * (1.0f - w), w + g * (1.0f - w)}; }},
}};
{
Fill(block, layout);
const auto color = DecodeTarget(block);
bool rejected = false;
try {
Draw(*device, color, cases[0].blend, Program(exported), StoringVertexCode);
} catch (const std::exception& error) {
rejected = std::string_view(error.what()).find("a draw whose shaders write memory") != std::string_view::npos;
if (!rejected) throw;
}
Require(rejected, "a split 8_8 SWAP_ALT draw whose vertex shader stores to memory was not rejected");
}
for (const auto& test : cases) {
const auto before = Fill(block, layout);
const auto color = DecodeTarget(block);

View File

@ -0,0 +1,143 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Threads = 32;
constexpr std::uint32_t Inputs = 4;
constexpr std::uint32_t Results = 4;
constexpr std::uint32_t VccLow = 106u;
constexpr std::uint32_t VectorSourceBase = 256u;
constexpr std::uint32_t ShaderCmpEqF16 = 0xcau;
constexpr std::uint32_t ShaderCmpLeF64 = 0x23u;
constexpr std::uint32_t ShaderCmpNgtF64 = 0x2bu;
alignas(256) std::array<std::uint32_t, Threads * Inputs> Input{};
alignas(256) std::array<std::uint32_t, Threads * Results> Output{};
struct Row {
std::array<std::uint32_t, 4> sources;
std::uint32_t expected;
};
struct Vop3CompareWords {
std::uint32_t word0;
std::uint32_t word1;
};
Vop3CompareWords Vop3Compare(std::uint32_t opcode, std::uint32_t source0, std::uint32_t source1) {
const std::uint32_t encoded0 = VectorSourceBase + source0;
const std::uint32_t encoded1 = VectorSourceBase + source1;
return {0xd4000000u | (opcode << 16u) | VccLow | 0x8000u, encoded0 | (encoded1 << 9u)};
}
std::vector<std::uint32_t> ClampCode(const Vop3CompareWords& compare) {
return {
0x34020082u, 0x34060082u,
0xe0302000u, 0x80000401u,
0xe0302004u, 0x80000501u,
0xe0302008u, 0x80000601u,
0xe030200cu, 0x80000701u,
0xbf8c3f70u,
compare.word0, compare.word1,
0xd5010014u, 0x01a90280u,
0xe0702000u, 0x80011403u,
0xbf810000u,
};
}
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t count) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (4u << 16u), count, 0x11016facu};
}
std::string Hex(std::uint32_t value) {
char text[16];
std::snprintf(text, sizeof(text), "0x%08x", value);
return text;
}
void RunClampRows(AgcDriver::VulkanDevice& device, const char* name, const Vop3CompareWords& compare,
const ShaderRecompiler::ShaderFloatMode& floatMode, const std::vector<Row>& rows) {
Input.fill(0u);
for (std::size_t index = 0; index < rows.size(); ++index) {
std::copy(rows[index].sources.begin(), rows[index].sources.end(),
Input.begin() + static_cast<std::ptrdiff_t>(index * Inputs));
}
Output.fill(0xdeadbeefu);
std::vector<std::uint32_t> userData(8, 0u);
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size()));
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size()));
std::copy(input.begin(), input.end(), userData.begin());
std::copy(output.begin(), output.end(), userData.begin() + 4);
const auto code = ClampCode(compare);
const std::span<const std::uint32_t> words(code);
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(words.data()), std::as_bytes(words)}}};
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
ShaderRecompiler::RecompileRequest request{
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(words.data()), words, 0, {}},
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
device.Target(),
{0, 0, 0, 128}
};
request.context.floatMode = floatMode;
request.useCache = false;
const auto result = ShaderRecompiler::Recompile(request);
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(words.data()));
device.WaitIdle();
for (std::size_t index = 0; index < rows.size(); ++index) {
const auto actual = Output[index * Results];
Require(actual == rows[index].expected, std::string(name) + " clamp row " + std::to_string(index) + " is " + Hex(actual) + ", expected " + Hex(rows[index].expected));
}
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
const ShaderRecompiler::ShaderFloatMode denormalsKept{0xf0u, true, true, false};
const ShaderRecompiler::ShaderFloatMode denorm32Flushed{0xc0u, true, false, false};
RunClampRows(*device, "v_cmp_eq_f16_e64 vcc_lo, v4, v5", Vop3Compare(ShaderCmpEqF16, 4u, 5u), denormalsKept, {
{{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000001u},
{{0x0000bc00u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000000u},
{{0x00003555u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000000u},
{{0x00000001u, 0x00008000u, 0x00000000u, 0x00000000u}, 0x00000000u},
{{0x0000fc00u, 0x00008000u, 0x00000000u, 0x00000000u}, 0x00000000u},
{{0x00007c01u, 0x00003c00u, 0x00000000u, 0x00000000u}, 0x00000000u},
{{0x00006400u, 0x00003c00u, 0x00000000u, 0x00000000u}, 0x00000000u},
{{0xbeef8000u, 0xbeefbc00u, 0xbeef0000u, 0x00000000u}, 0x00000000u},
});
RunClampRows(*device, "v_cmp_le_f64_e64 vcc_lo, v[4:5], v[6:7]", Vop3Compare(ShaderCmpLeF64, 4u, 6u), denorm32Flushed, {
{{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000001u},
{{0x00000000u, 0xbff00000u, 0x00000000u, 0xfff00000u}, 0x00000000u},
{{0x00000000u, 0xc0040000u, 0x00000000u, 0xbff00000u}, 0x00000001u},
{{0x00000000u, 0x00100000u, 0x00000001u, 0x7ff00000u}, 0x00000000u},
{{0x00000000u, 0xfff00000u, 0x00000000u, 0x40040000u}, 0x00000001u},
{{0x00000001u, 0x7ff00000u, 0x00000000u, 0x41e00000u}, 0x00000000u},
});
RunClampRows(*device, "v_cmp_ngt_f64_e64 vcc_lo, v[4:5], v[6:7]", Vop3Compare(ShaderCmpNgtF64, 4u, 6u), denormalsKept, {
{{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000001u},
{{0x00000000u, 0xbff00000u, 0x00000000u, 0xfff00000u}, 0x00000000u},
{{0x00000000u, 0xc0040000u, 0x00000000u, 0xbff00000u}, 0x00000001u},
{{0x00000000u, 0x00100000u, 0x00000001u, 0x7ff00000u}, 0x00000001u},
});
std::puts("v_cmp clamp tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -0,0 +1,136 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <optional>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Threads = 16;
constexpr std::uint32_t Inputs = 4;
constexpr std::uint32_t Results = 4;
constexpr std::uint32_t RowCount = 16;
alignas(256) std::array<std::uint32_t, Threads * Inputs> Input{};
alignas(256) std::array<std::uint32_t, Threads * Results> Output{};
alignas(256) std::array<std::uint32_t, 12> Code{
0x34020084u, 0x34060084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u,
0x7e1402ffu, 0x5a5a5a5au, 0xd5d3000au, 0x00000104u,
0xe0781000u, 0x80010a03u, 0xbf810000u,
};
struct Variant {
const char* modifiers;
std::uint32_t clamp;
std::uint32_t omod;
};
constexpr Variant Variants[] = {
{"mul:2", 0, 1},
{"mul:4", 0, 2},
{"div:2", 0, 3},
{"clamp", 1, 0},
{"clamp mul:2", 1, 1},
};
constexpr std::uint32_t Rows[RowCount] = {
0x00003c00u,
0x0000bc00u,
0x0000c100u,
0x00004200u,
0x00006400u,
0x00005640u,
0x0000d640u,
0x00007bffu,
0x0000fbffu,
0x00007e00u,
0xbeef3c00u,
0xbeefc100u,
0x00007c00u,
0x0000fc00u,
0x00007c01u,
0x00003e00u,
};
constexpr std::uint32_t Expected[RowCount] = {
0x5a5a0001u, 0x5a5affffu, 0x5a5afffeu, 0x5a5a0003u,
0x5a5a0400u, 0x5a5a0064u, 0x5a5aff9cu, 0x5a5a7fffu,
0x5a5a8000u, 0x5a5a0000u, 0x5a5a0001u, 0x5a5afffeu,
0x5a5a7fffu, 0x5a5a8000u, 0x5a5a0000u, 0x5a5a0001u,
};
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
}
std::string Hex(std::uint32_t value) {
char text[16];
std::snprintf(text, sizeof(text), "0x%08x", value);
return text;
}
void Run(AgcDriver::VulkanDevice& device, const ShaderRecompiler::ShaderFloatMode& floatMode, const Variant& variant) {
Code[7] = 0xd5d3000au | variant.clamp << 15u;
Code[8] = 0x00000104u | variant.omod << 27u;
Input.fill(0u);
for (std::uint32_t tid = 0; tid < RowCount; ++tid) Input[tid * Inputs] = Rows[tid];
Output.fill(0xdeadbeefu);
std::vector<std::uint32_t> userData(8, 0u);
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u));
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
std::copy(input.begin(), input.end(), userData.begin());
std::copy(output.begin(), output.end(), userData.begin() + 4);
const std::span<const std::uint32_t> code(Code);
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(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<std::uintptr_t>(code.data()), code, 0, {}},
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
device.Target(),
{0, 0, 0, 128}
};
request.useCache = false;
request.context.floatMode = floatMode;
const auto result = ShaderRecompiler::Recompile(request);
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
device.WaitIdle();
}
void Check(const char* mode, const Variant& variant) {
for (std::uint32_t tid = 0; tid < RowCount; ++tid) {
const std::uint32_t actual = Output[tid * Results];
Require(actual == Expected[tid], std::string("v_cvt_i16_f16 ") + variant.modifiers + " " + mode + ": lane " + std::to_string(tid) +
" of " + Hex(Rows[tid]) + " is " + Hex(actual) + ", expected " + Hex(Expected[tid]));
}
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
for (const Variant& variant : Variants) {
Run(*device, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, variant);
Check("M1", variant);
Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, variant);
Check("M2", variant);
}
std::puts("v_cvt_i16_f16 output modifiers tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -0,0 +1,174 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Threads = 32;
constexpr std::uint32_t Inputs = 4;
constexpr std::uint32_t Results = 2;
constexpr std::uint32_t RowCount = 8;
constexpr std::uint32_t VariantCount = 8;
alignas(256) std::array<std::uint32_t, Threads * Inputs> Input{};
alignas(256) std::array<std::uint32_t, Threads * Results> Output{};
alignas(256) std::array<std::uint32_t, 14> Code{
0x34020084u, 0x34060083u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xd712000au, 0x00020b04u,
0xd713000bu, 0x00020b04u, 0xe0701000u, 0x80010a03u, 0xe0701004u, 0x80010b03u, 0xbf810000u,
};
struct Variant {
const char* operands;
std::uint32_t abs;
std::uint32_t neg;
std::uint32_t opSel;
};
constexpr Variant Variants[VariantCount] = {
{"|v4|, v5", 1, 0, 0},
{"v4, |v5|", 2, 0, 0},
{"-v4, v5", 0, 1, 0},
{"v4, -v5", 0, 2, 0},
{"-|v4|, v5", 1, 1, 0},
{"v4, -|v5|", 2, 2, 0},
{"v4, v5 op_sel:[1,0,0]", 0, 0, 1},
{"v4, v5 op_sel:[0,1,0]", 0, 0, 2},
};
constexpr std::uint32_t Rows[RowCount][2] = {
{0x00000400u, 0x00000400u},
{0x000083ffu, 0x000083ffu},
{0x00003555u, 0x0000d640u},
{0x0000bc00u, 0x00003c00u},
{0x00007bffu, 0x00000400u},
{0xbeef0000u, 0xbeef3c00u},
{0xbeef8000u, 0xbeef0000u},
{0xbeef0000u, 0xbeeffc00u},
};
constexpr std::uint32_t Expected[VariantCount][Results][RowCount] = {
{
{0x00020002u, 0xfffe0002u, 0x80012aa8u, 0x7fff7fffu, 0x00027fffu, 0x7fff0000u, 0x00000000u, 0x80010000u},
{0x00040004u, 0x00000004u, 0x00005550u, 0xffffffffu, 0x0004ffffu, 0xffff0000u, 0x00000000u, 0x00000000u},
},
{
{0x00020002u, 0x0002fffeu, 0x7fff2aa8u, 0x7fff8001u, 0x00027fffu, 0x7fff0000u, 0x00000000u, 0x7fff0000u},
{0x00040004u, 0x00040000u, 0xffff5550u, 0xffff0000u, 0x0004ffffu, 0xffff0000u, 0x00000000u, 0xffff0000u},
},
{
{0x0002fffeu, 0xfffe0002u, 0x8001d558u, 0x7fff7fffu, 0x00028001u, 0x7fff0000u, 0x00000000u, 0x80010000u},
{0x00040000u, 0x00000004u, 0x00000000u, 0xffffffffu, 0x00040000u, 0xffff0000u, 0x00000000u, 0x00000000u},
},
{
{0xfffe0002u, 0x0002fffeu, 0x7fff2aa8u, 0x80018001u, 0xfffe7fffu, 0x80010000u, 0x00000000u, 0x7fff0000u},
{0x00000004u, 0x00040000u, 0xffff5550u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0xffff0000u},
},
{
{0x0002fffeu, 0xfffefffeu, 0x8001d558u, 0x7fff8001u, 0x00028001u, 0x7fff0000u, 0x00000000u, 0x80010000u},
{0x00040000u, 0x00000000u, 0x00000000u, 0xffff0000u, 0x00040000u, 0xffff0000u, 0x00000000u, 0x00000000u},
},
{
{0xfffe0002u, 0xfffefffeu, 0x80012aa8u, 0x80018001u, 0xfffe7fffu, 0x80010000u, 0x00000000u, 0x80010000u},
{0x00000004u, 0x00000000u, 0x00005550u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u},
},
{
{0x00020000u, 0xfffe0000u, 0x80010000u, 0x7fff0000u, 0x00020000u, 0x7fff8001u, 0x00008001u, 0x80018001u},
{0x00040000u, 0x00000000u, 0x00000000u, 0xffff0000u, 0x00040000u, 0xffff0000u, 0x00000000u, 0x00000000u},
},
{
{0x00000002u, 0x0000fffeu, 0x00002aa8u, 0x00008001u, 0x00007fffu, 0x80010000u, 0x80010000u, 0x80010000u},
{0x00000004u, 0x00000000u, 0x00005550u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u},
},
};
constexpr const char* Opcodes[Results] = {"v_cvt_pknorm_i16_f16_e64 v10, ", "v_cvt_pknorm_u16_f16_e64 v11, "};
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
}
std::string Hex(std::uint32_t value) {
char text[16];
std::snprintf(text, sizeof(text), "0x%08x", value);
return text;
}
void Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode mode, const Variant& variant) {
const std::uint32_t first = 0xd7120000u | variant.opSel << 11u | variant.abs << 8u | 10u;
const std::uint32_t second = 0xd7130000u | variant.opSel << 11u | variant.abs << 8u | 11u;
const std::uint32_t sources = 0x00020b04u | variant.neg << 29u;
Code[5] = first;
Code[6] = sources;
Code[7] = second;
Code[8] = sources;
for (std::uint32_t index = 0; index < RowCount; ++index) {
std::copy(std::begin(Rows[index]), std::end(Rows[index]), &Input[index * Inputs]);
}
for (std::uint32_t index = RowCount * Inputs; index < Input.size(); ++index) Input[index] = 0u;
Output.fill(0xdeadbeefu);
std::vector<std::uint32_t> userData(8, 0u);
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u));
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
std::copy(input.begin(), input.end(), userData.begin());
std::copy(output.begin(), output.end(), userData.begin() + 4);
const std::span<const std::uint32_t> code(Code);
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(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<std::uintptr_t>(code.data()), code, 0, {}},
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
device.Target(),
{0, 0, 0, 128}
};
request.useCache = false;
request.context.floatMode = mode;
const auto result = ShaderRecompiler::Recompile(request);
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
device.WaitIdle();
}
void Check(const char* modeName, std::uint32_t variantIndex) {
for (std::uint32_t slot = 0; slot < Results; ++slot) {
for (std::uint32_t index = 0; index < RowCount; ++index) {
const std::uint32_t actual = Output[index * Results + slot];
const std::uint32_t expected = Expected[variantIndex][slot][index];
Require(actual == expected,
std::string("v_cvt_pknorm f16 source modifiers: ") + modeName + " row " + std::to_string(index) + " " +
Opcodes[slot] + Variants[variantIndex].operands + " of " + Hex(Rows[index][0]) + " and " +
Hex(Rows[index][1]) + " is " + Hex(actual) + ", expected " + Hex(expected));
}
}
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
for (std::uint32_t index = 0; index < VariantCount; ++index) {
Run(*device, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, Variants[index]);
Check("FLOAT_MODE 0xf0", index);
Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, Variants[index]);
Check("FLOAT_MODE 0xc0", index);
}
std::puts("v_cvt_pknorm f16 source modifiers tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -0,0 +1,35 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TOOLS_COMPUTEPIPELINEFIXTURE_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TOOLS_COMPUTEPIPELINEFIXTURE_HPP
#include "prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp"
namespace AgcDriver {
inline ComputePipelineCapture ComputePipelineFixture() {
ComputePipelineCapture capture;
capture.spirv = {
0x07230203u, 0x00010000u, 0u, 7u, 0u,
0x00020011u, 1u,
0x0003000eu, 0u, 1u,
0x0005000fu, 5u, 3u, 0x6e69616du, 0u,
0x00060010u, 3u, 17u, 1u, 1u, 1u,
0x00040047u, 6u, 1u, 7u,
0x00020013u, 1u,
0x00030021u, 2u, 1u,
0x00040015u, 5u, 32u, 0u,
0x00040032u, 5u, 6u, 1u,
0x00050036u, 1u, 3u, 0u, 2u,
0x000200f8u, 4u,
0x000100fdu,
0x00010038u
};
capture.specialization.push_back({7u, 3u});
capture.sets = {{0, {{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}};
capture.pushConstants = {{VK_SHADER_STAGE_COMPUTE_BIT, 16, 16}};
capture.flags = VK_PIPELINE_CREATE_DISPATCH_BASE_BIT;
return capture;
}
}
#endif

View File

@ -0,0 +1,154 @@
#include "ComputePipelineFixture.hpp"
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include <cstdlib>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string_view>
namespace {
struct Options {
std::filesystem::path source;
std::filesystem::path cacheIn;
std::filesystem::path cacheOut;
std::filesystem::path output;
std::filesystem::path fixtureDirectory;
bool fixture = false;
};
Options Parse(int argc, char** argv) {
Options result;
for (int index = 1; index < argc; ++index) {
const std::string_view argument(argv[index]);
const auto value = [&](std::filesystem::path& destination) {
if (!destination.empty() || ++index == argc || *argv[index] == '\0') throw std::runtime_error("missing or repeated option: " + std::string(argument));
destination = argv[index];
};
if (argument == "--cache-in") value(result.cacheIn);
else if (argument == "--cache-out") value(result.cacheOut);
else if (argument == "--output") value(result.output);
else if (argument == "--write-fixture") value(result.fixtureDirectory);
else if (argument == "--fixture" && !result.fixture) result.fixture = true;
else if (argument.starts_with("--") || !result.source.empty()) throw std::runtime_error("unknown or repeated argument: " + std::string(argument));
else result.source = argument;
}
const auto sources = static_cast<int>(!result.source.empty()) + static_cast<int>(result.fixture) + static_cast<int>(!result.fixtureDirectory.empty());
if (sources != 1) throw std::runtime_error("select a capture file, --fixture, or --write-fixture DIRECTORY");
if (!result.fixtureDirectory.empty() && (!result.cacheIn.empty() || !result.cacheOut.empty() || !result.output.empty())) throw std::runtime_error("--write-fixture cannot be combined with replay options");
for (const auto& path : {result.cacheOut, result.output}) {
if (!path.empty() && std::filesystem::exists(path)) throw std::runtime_error("output already exists: " + path.string());
}
if (!result.cacheOut.empty() && !result.output.empty() && std::filesystem::absolute(result.cacheOut).lexically_normal() == std::filesystem::absolute(result.output).lexically_normal()) throw std::runtime_error("cache and report must use different paths");
return result;
}
std::vector<std::byte> ReadCache(const std::filesystem::path& path) {
if (path.empty()) return {};
const auto bytes = std::filesystem::file_size(path);
if (bytes == 0 || bytes > 64u * 1024u * 1024u) throw std::runtime_error("imported pipeline cache must contain 1 byte to 64 MiB");
std::vector<std::byte> result(static_cast<std::size_t>(bytes));
std::ifstream input(path, std::ios::binary);
input.read(reinterpret_cast<char*>(result.data()), static_cast<std::streamsize>(result.size()));
if (!input || input.peek() != std::char_traits<char>::eof()) throw std::runtime_error("could not read complete pipeline cache: " + path.string());
return result;
}
void WriteBytes(const std::filesystem::path& path, std::span<const std::byte> bytes) {
if (std::filesystem::exists(path)) throw std::runtime_error("output already exists: " + path.string());
std::ofstream output(path, std::ios::binary);
output.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
output.close();
if (!output) throw std::runtime_error("could not write complete output: " + path.string());
}
std::string Quote(std::string_view text) {
std::ostringstream result;
result << '"';
for (const unsigned char character : text) {
if (character == '\\' || character == '"') result << '\\' << character;
else if (character < 0x20u) result << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast<unsigned>(character);
else result << character;
}
result << '"';
return result.str();
}
std::string Uuid(std::span<const std::uint8_t> bytes) {
std::ostringstream result;
for (const auto byte : bytes) result << std::hex << std::setw(2) << std::setfill('0') << static_cast<unsigned>(byte);
return result.str();
}
template<typename TDevice>
void WriteDevice(std::ostream& output, const TDevice& device) {
output << "{\"name\":" << Quote(device.deviceName)
<< ",\"vendor_id\":" << device.vendorId << ",\"device_id\":" << device.deviceId
<< ",\"driver_version_raw\":" << device.driverVersion << ",\"api_version_raw\":" << device.apiVersion
<< ",\"pipeline_cache_uuid\":" << Quote(Uuid(device.pipelineCacheUuid)) << '}';
}
std::string Report(const Options& options, const AgcDriver::ComputePipelineCapture& capture, const AgcDriver::ComputePipelineReplayResult& replay) {
std::ostringstream output;
output << std::setprecision(12)
<< "{\n \"schema_version\":1,\n \"source\":" << Quote(options.fixture ? "synthetic-no-op-compute" : options.source.string())
<< ",\n \"capture_identity\":" << Quote(capture.Identity())
<< ",\n \"capture_identity_algorithm\":\"fnv1a64\""
<< ",\n \"spirv_words\":" << capture.spirv.size()
<< ",\n \"descriptor_sets\":" << capture.sets.size()
<< ",\n \"push_constant_ranges\":" << capture.pushConstants.size()
<< ",\n \"specialization_constants\":" << capture.specialization.size()
<< ",\n \"required_subgroup_size\":" << capture.requiredSubgroupSize
<< ",\n \"pipeline_flags\":" << capture.flags
<< ",\n \"stage_flags\":" << capture.stageFlags
<< ",\n \"captured_device\":";
WriteDevice(output, capture);
output << ",\n \"replay_device\":";
WriteDevice(output, replay);
output << ",\n \"application_cache_policy\":" << Quote(options.cacheIn.empty() ? "fresh" : "imported")
<< ",\n \"cache_import_path\":" << Quote(options.cacheIn.string())
<< ",\n \"cache_import_bytes\":" << replay.initialCacheBytes
<< ",\n \"cache_export_path\":" << Quote(options.cacheOut.string())
<< ",\n \"cache_export_bytes\":" << (options.cacheOut.empty() ? 0u : replay.cache.size())
<< ",\n \"driver_cache_state\":\"uncontrolled\""
<< ",\n \"dispatch_count\":0,\n \"pipeline_statistics\":false"
<< ",\n \"milliseconds\":{\"shader_module\":" << replay.moduleMilliseconds
<< ",\"layout\":" << replay.layoutMilliseconds << ",\"pipeline_create\":" << replay.pipelineMilliseconds
<< ",\"cache\":" << replay.cacheMilliseconds << ",\"replay_total\":" << replay.totalMilliseconds << "}\n}\n";
return output.str();
}
}
int main(int argc, char** argv) {
if (argc == 2 && std::string_view(argv[1]) == "--help") {
std::cout << "agc_compute_pipeline_replay CAPTURE|--fixture [--cache-in FILE] [--cache-out FILE] [--output FILE]\n"
"agc_compute_pipeline_replay --write-fixture DIRECTORY\n"
"Compiles one compute pipeline without dispatch. Fresh application cache does not clear driver caches.\n";
return 0;
}
try {
const auto options = Parse(argc, argv);
if (!options.fixtureDirectory.empty()) {
std::cout << AgcDriver::ComputePipelineFixture().Write(options.fixtureDirectory).string() << '\n';
return 0;
}
const auto* statistics = std::getenv("APS5_PIPELINE_STATS");
if (statistics != nullptr && std::string_view(statistics) != "0" && *statistics != '\0') throw std::runtime_error("unset APS5_PIPELINE_STATS before benchmarking");
const auto capture = options.fixture ? AgcDriver::ComputePipelineFixture() : AgcDriver::ComputePipelineCapture::Read(options.source);
if ((capture.flags & VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR) != 0) throw std::runtime_error("capture requests pipeline statistics; capture again with APS5_PIPELINE_STATS disabled");
const auto cache = ReadCache(options.cacheIn);
AgcDriver::VulkanDevice device(AgcDriver::VulkanDevice::CompileOnly{});
const auto replay = device.ReplayComputePipeline(capture, cache);
if (!options.cacheOut.empty()) WriteBytes(options.cacheOut, replay.cache);
const auto report = Report(options, capture, replay);
if (!options.output.empty()) WriteBytes(options.output, std::as_bytes(std::span(report.data(), report.size())));
std::cout << report;
return 0;
} catch (const std::exception& error) {
std::cerr << "compute pipeline replay: " << error.what() << '\n';
return 1;
}
}

Some files were not shown because too many files have changed in this diff Show More