Merge main into feat/npcppwebapi-public-profile-singular

Keep main's argc/argv alias-vector dispatch and TestVectorAliasedGrowth together with the singular public-profile helper and its call in GuestNpCppWebApiIntrusivePtr.cpp.
This commit is contained in:
Dean Galvin committed 2026-10-29 23:35:59 +13:00
commit 04b3dd67f8
258 files changed
+7079 -578

No files matched your search

+2
View File
@@ -38,3 +38,5 @@ jobs:
node --check demo/web/worker.mjs
- name: Test browser converter helpers
run: node --test demo/web/tests/app.test.mjs demo/web/tests/batch.test.mjs demo/web/tests/failure-report.test.mjs demo/web/tests/output-directory.test.mjs
- name: Test directory writer resources
run: node --test demo/web/tests/directory-writer.test.mjs
+1 -1
+4 -2
View File
@@ -32,9 +32,9 @@ ctest --test-dir build --output-on-failure
Python 3 is optional; without it some relinker tests are not registered.
Every push to `main`, including a merged pull request, automatically runs full Linux and Windows builds and tests and the relinker matrix on Linux, Windows and macOS, using the pushed commit without contributor checks.
Every push to `main`, including a merged pull request, automatically runs full Linux and Windows builds and tests, which include the relinker tests, using the pushed commit without contributor checks. The relinker-only build and tests run separately on macOS, for arm64 and x86_64, and the relinker-only build with the address sanitizer runs on Linux, only when the push changes a file the relinker-only build uses.
For pull requests, full Linux and Windows builds and tests run only when a collaborator with write access selects Actions > Build > Run workflow on `main` and enters an open pull request number targeting `main`. A collaborator can start the same manual run from the command line, replacing `1234` with the pull request number:
For pull requests, full Linux and Windows builds and tests run only when a collaborator with write access selects Actions > Build > Run workflow on `main` and enters up to 40 numbers of open pull requests targeting `main`, separated by spaces or commas; they are merged in that order and built together. A collaborator can start the same manual run from the command line, replacing `1234` with the pull request numbers:
```
gh workflow run build.yml --repo boykopovar/AnyPS5 --ref main --field pr=1234
@@ -44,6 +44,8 @@ This manual run also builds and tests the relinker on Linux, Windows and macOS w
With `--field full=false`, the manual run skips the full builds and runs only the relinker matrix on Linux, Windows and macOS, if the pull request changes a file the relinker-only build uses, such as the Python tools and their tests in `tools/`.
With `--field linux=false`, the manual run skips the full Linux build and adds Linux to the relinker matrix, if the pull requests change a file the relinker-only build uses. With `--field select=true`, the full builds build and run only the tests whose build inputs the pull requests change; push builds of `main` always run every test.
The Conventions check runs automatically for every contributor, including first-time contributors. It uses the checker from the current tip of the base branch, so rules added after a pull request was opened apply to its next run, and reads pull request Git objects without executing pull request code. The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [technical debt](docs/dev/TechnicalDebt.md), the index or a file in `docs/dev/debt/`, and only UTF-8 text files. Run it locally before pushing:
```
+7
View File
@@ -11,6 +11,13 @@ function(add_bundled_opus)
add_subdirectory(3rdparty/opus EXCLUDE_FROM_ALL)
set_target_properties(opus PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden)
target_compile_definitions(opus PRIVATE OPUS_EXPORT=)
if(MINGW AND CMAKE_C_COMPILER_ID STREQUAL "GNU")
include(CheckCCompilerFlag)
check_c_compiler_flag("-Wa,-muse-unaligned-vector-move" ANYPS5_ASM_UNALIGNED_VECTOR_MOVE)
if(ANYPS5_ASM_UNALIGNED_VECTOR_MOVE)
target_compile_options(opus PRIVATE -Wa,-muse-unaligned-vector-move)
endif()
endif()
if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_options(opus INTERFACE LINKER:--exclude-libs,libopus.a)
endif()
+17
View File
@@ -480,6 +480,11 @@ if(BUILD_TESTING)
add_test(NAME guest_kernel_sys_threads_${case} COMMAND guest_kernel_sys_threads_tests ${UNPATCHED_DIR} unpatched ${case})
add_test(NAME guest_kernel_sys_threads_${case}_patched COMMAND guest_kernel_sys_threads_tests ${PATCHED_DIR} patched ${case})
set_tests_properties(guest_kernel_sys_threads_${case} guest_kernel_sys_threads_${case}_patched PROPERTIES TIMEOUT 20)
if(MINGW)
get_filename_component(kernelSysThreadsRuntimeDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY)
set_tests_properties(guest_kernel_sys_threads_${case} guest_kernel_sys_threads_${case}_patched PROPERTIES
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:${kernelSysThreadsRuntimeDirectory}")
endif()
endforeach()
add_test_executable(guest_gnm_neo_packets_tests tests/GuestGnmNeoPackets.cpp)
@@ -848,6 +853,17 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSsl>")
endif()
add_test_executable(guest_kernel_sys_ksem_tests tests/GuestKernelSysKsem.cpp)
target_include_directories(guest_kernel_sys_ksem_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_kernel_sys_ksem_tests PRIVATE libkernel_sys libkernel libc)
configure_windows_unwind(guest_kernel_sys_ksem_tests)
add_test(NAME guest_kernel_sys_ksem COMMAND guest_kernel_sys_ksem_tests)
set_tests_properties(guest_kernel_sys_ksem PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(guest_kernel_sys_ksem PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
endif()
add_test_executable(guest_system_gesture_tests tests/GuestSystemGesture.cpp)
target_include_directories(guest_system_gesture_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_system_gesture_tests PRIVATE libSceSystemGesture libc)
@@ -1118,3 +1134,4 @@ if(BUILD_TESTING)
endif()
include(prx/libSceFreeType/CurveBounds.cmake)
include(prx/libSceBeisobmf/Tests.cmake)
+26 -2
View File
@@ -561,6 +561,18 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
if(WIN32)
add_test_executable(agc_worker_sampler_tests tests/WorkerSampler.cpp Execution/src/WorkerSampler.cpp)
target_include_directories(agc_worker_sampler_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_worker_sampler_tests PRIVATE libc)
configure_windows_unwind(agc_worker_sampler_tests)
foreach(mode default selected unselected process)
add_test(NAME agc_worker_sampler_${mode} COMMAND agc_worker_sampler_tests ${mode})
set_tests_properties(agc_worker_sampler_${mode} PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endforeach()
endif()
add_test_executable(agc_stack_walk_tests tests/StackWalk.cpp)
target_include_directories(agc_stack_walk_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_stack_walk_tests PRIVATE libc)
@@ -2271,6 +2283,14 @@ set_target_properties(agc_driver_mesh_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_mesh_tests)
set(ANYPS5_AGC_EXECUTION_TESTS "" CACHE STRING "tests/execution sources to build, by file name without extension; the others build a stub that reports a skip. Empty builds all")
add_executable(agc_driver_registration_prepare_budget_tests tests/RegistrationPrepareBudget.cpp)
target_include_directories(agc_driver_registration_prepare_budget_tests PRIVATE ${LIBS_INCLUDE_DIR})
set_target_properties(agc_driver_registration_prepare_budget_tests PROPERTIES CXX_EXTENSIONS OFF)
if(MINGW)
target_compile_options(agc_driver_registration_prepare_budget_tests PRIVATE -fasynchronous-unwind-tables)
target_link_options(agc_driver_registration_prepare_budget_tests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
file(GLOB agcExecutionTestSources CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/*.cpp)
set(agcExecutionTests)
foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources)
@@ -2341,6 +2361,8 @@ foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS)
endif()
endforeach()
if(BUILD_TESTING)
add_test(NAME agc_driver_registration_prepare_budget COMMAND agc_driver_registration_prepare_budget_tests)
set_tests_properties(agc_driver_registration_prepare_budget PROPERTIES TIMEOUT 10)
add_test(NAME agc_driver_indirect_thread_dimensions_cpu COMMAND agc_driver_indirect_thread_dimensions_tests)
set_tests_properties(agc_driver_indirect_thread_dimensions_cpu PROPERTIES ENVIRONMENT "APS5_NO_GPU_THREAD_DIMENSIONS=1" SKIP_RETURN_CODE 77 TIMEOUT 60)
if(WIN32)
@@ -3419,7 +3441,7 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_array_layer_target_cpu PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync dispatch_ahead_prewarm abi_deferred_compute abi_deferred_pixel abi_deferred_stages abi_deferred_rectangle abi_deferred_undecodable_state abi_deferred_registration_failure abi_deferred_vertex_inputs abi_deferred_rectangle_pairs lazy lazy_sync)
foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync dispatch_ahead_prewarm abi_deferred_compute abi_deferred_pixel abi_deferred_stages abi_deferred_rectangle abi_deferred_undecodable_state abi_deferred_registration_failure abi_deferred_vertex_inputs abi_deferred_rectangle_pairs lazy lazy_sync lazy_budget_zero)
set(preparedTest "agc_driver_prepared_shaders_${preparedMode}")
if(preparedMode MATCHES "^abi_deferred_")
string(REPLACE "_" "-" preparedArgument ${preparedMode})
@@ -3430,7 +3452,7 @@ 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 "lazy" OR preparedMode STREQUAL "lazy_sync")
elseif(preparedMode STREQUAL "lazy" OR preparedMode STREQUAL "lazy_sync" OR preparedMode STREQUAL "lazy_budget_zero")
add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --lazy)
elseif(preparedMode STREQUAL "registered_workgroup" OR preparedMode STREQUAL "registered_workgroup_sync")
add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --registered-workgroup)
@@ -3454,6 +3476,8 @@ if(BUILD_TESTING)
if(preparedMode STREQUAL "lazy_sync")
set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_SYNC_SHADER_PREPARE=1")
endif()
elseif(preparedMode STREQUAL "lazy_budget_zero")
set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_REGISTRATION_PREPARE_COMMIT_GB=0" "APS5_SUSPEND_DRAIN=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()
@@ -8,5 +8,7 @@ bool AgcDriverDeliverEopInterrupt(std::uint32_t queue);
extern "C" int APS5_VABI sceAgcDriverAddEqEvent(KernelEqueue eq, int id, void* udata);
extern "C" int APS5_VABI sceAgcDriverDeleteEqEvent(KernelEqueue eq, int id);
extern "C" int AgcDriverAddQueueEqEvent_nid_postfix(KernelEqueue eq, std::uint32_t queue, std::uintptr_t ident, void* udata);
extern "C" int AgcDriverDeleteQueueEqEvent_nid_postfix(KernelEqueue eq, std::uintptr_t ident);
#endif
+52 -35
View File
@@ -18,38 +18,19 @@ constexpr int16_t EvfiltGraphicsCore = -14;
struct Registration {
KernelEqueue eq;
int id;
std::uint32_t queue;
std::uintptr_t ident;
};
AgcDriver::Mutex g_mutex;
std::vector<Registration> g_registrations;
}
bool AgcDriverDeliverEopInterrupt(std::uint32_t queue) {
std::vector<Registration> registrations;
{
std::lock_guard lock(g_mutex);
registrations = g_registrations;
}
bool delivered = false;
for (const auto& registration : registrations) {
if (registration.id == static_cast<int>(queue)) {
EqueueTriggerEvent_nid_postfix(registration.eq, static_cast<uintptr_t>(registration.id), EvfiltGraphicsCore, nullptr);
delivered = true;
}
}
return delivered;
}
extern "C" {
int APS5_VABI sceAgcDriverAddEqEvent(KernelEqueue eq, int id, void* udata) {
int AddEvent(KernelEqueue eq, std::uint32_t queue, std::uintptr_t ident, void* udata, const char* caller) {
if (eq == 0 || !EqueuePin_nid_postfix(eq)) {
throw std::runtime_error(std::string(__func__) + ": invalid event queue");
throw std::runtime_error(std::string(caller) + ": invalid event queue");
}
KernelEqueueEvent event{};
event.event.ident = static_cast<uintptr_t>(id);
event.event.ident = ident;
event.event.filter = EvfiltGraphicsCore;
event.event.flags = EV_ADD | EV_CLEAR;
event.event.udata = udata;
@@ -63,24 +44,60 @@ int APS5_VABI sceAgcDriverAddEqEvent(KernelEqueue eq, int id, void* udata) {
};
const int result = EqueueAddEvent_nid_postfix(eq, event);
if (result != EQUEUE_OK) {
throw std::runtime_error(std::string(__func__) + ": event queue rejected the event");
throw std::runtime_error(std::string(caller) + ": event queue rejected the event");
}
std::lock_guard lock(g_mutex);
if (std::none_of(g_registrations.begin(), g_registrations.end(), [&](const Registration& r) { return r.eq == eq && r.id == id; })) {
g_registrations.push_back({eq, id});
if (std::none_of(g_registrations.begin(), g_registrations.end(), [&](const Registration& r) { return r.eq == eq && r.ident == ident; })) {
g_registrations.push_back({eq, queue, ident});
}
return 0;
}
int DeleteEvent(KernelEqueue eq, std::uintptr_t ident, const char* caller) {
{
std::lock_guard lock(g_mutex);
std::erase_if(g_registrations, [&](const Registration& r) { return r.eq == eq && r.ident == ident; });
}
if (EqueueDeleteEvent_nid_postfix(eq, ident, EvfiltGraphicsCore) != EQUEUE_OK) {
throw std::runtime_error(std::string(caller) + ": event is not registered");
}
return 0;
}
}
bool AgcDriverDeliverEopInterrupt(std::uint32_t queue) {
std::vector<Registration> registrations;
{
std::lock_guard lock(g_mutex);
registrations = g_registrations;
}
bool delivered = false;
for (const auto& registration : registrations) {
if (registration.queue == queue) {
EqueueTriggerEvent_nid_postfix(registration.eq, registration.ident, EvfiltGraphicsCore, nullptr);
delivered = true;
}
}
return delivered;
}
extern "C" {
int APS5_VABI sceAgcDriverAddEqEvent(KernelEqueue eq, int id, void* udata) {
return AddEvent(eq, static_cast<std::uint32_t>(id), static_cast<std::uintptr_t>(id), udata, __func__);
}
int APS5_VABI sceAgcDriverDeleteEqEvent(KernelEqueue eq, int id) {
{
std::lock_guard lock(g_mutex);
std::erase_if(g_registrations, [&](const Registration& r) { return r.eq == eq && r.id == id; });
}
if (EqueueDeleteEvent_nid_postfix(eq, static_cast<uintptr_t>(id), EvfiltGraphicsCore) != EQUEUE_OK) {
throw std::runtime_error(std::string(__func__) + ": event is not registered");
}
return 0;
return DeleteEvent(eq, static_cast<std::uintptr_t>(id), __func__);
}
}
extern "C" int AgcDriverAddQueueEqEvent_nid_postfix(KernelEqueue eq, std::uint32_t queue, std::uintptr_t ident, void* udata) {
return AddEvent(eq, queue, ident, udata, __func__);
}
extern "C" int AgcDriverDeleteQueueEqEvent_nid_postfix(KernelEqueue eq, std::uintptr_t ident) {
return DeleteEvent(eq, ident, __func__);
}
@@ -25,6 +25,8 @@ struct DisplayBuffer {
std::uint32_t alignment = 65536;
};
inline constexpr std::uint32_t DisplayTilingGcn = 2;
std::size_t DisplayBufferSize(const DisplayBuffer& buffer);
std::size_t DisplayBufferOffset(const DisplayBuffer& buffer, std::uint32_t x, std::uint32_t y);
std::vector<std::byte> DecodeDisplayBuffer(const DisplayBuffer& buffer, std::span<const std::byte> source);
@@ -24,6 +24,8 @@ struct AwaitedWait {
extern "C" void AgcDriverPrepareCommandWrite_nid_no_patch(std::uint64_t address, std::size_t bytes);
extern "C" void AgcDriverWaitIdle_nid_postfix();
extern "C" void AgcDriverSubmitGraphics_nid_postfix(std::uint32_t* commands, std::uint32_t dwords);
extern "C" void AgcDriverUseGcnShaders_nid_postfix();
extern "C" void AgcDriverRaiseWorkerThreadPriority_nid_postfix(const char* role);
extern "C" void AgcDriverLockVulkanLoader_nid_postfix();
extern "C" void AgcDriverUnlockVulkanLoader_nid_postfix();
@@ -40,6 +40,30 @@ struct PatternFillRange {
}
};
inline constexpr std::array<std::uint32_t, 18> GcnPatternFillKernel{
0xbeeb03ffu, 0x0000000bu, 0x8f008610u, 0x4a000000u, 0xc2400d00u, 0xbf8c007fu,
0x7d880000u, 0xbe82246au, 0x36020001u, 0xbf880007u, 0xe0002000u, 0x80010101u,
0x7d88000au, 0xbe80246au, 0xbf8c0f70u, 0xe0102000u, 0x80020100u, 0xbf810000u
};
inline bool MatchesGcnPatternFillKernel(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) {
if (userData.size() != 16 || compute.numThreads != std::array<std::uint32_t, 3>{64, 1, 1} || !compute.groupIdEnable[0] || compute.threadIdComponentCount != 1) return false;
if (code.size() < GcnPatternFillKernel.size() || !std::equal(GcnPatternFillKernel.begin(), GcnPatternFillKernel.end(), code.begin())) return false;
for (const auto first : {4u, 8u}) {
if ((userData[first + 1] & 0xbfff0000u) != 0x00040000u || (userData[first + 3] & 0xc087f000u) != 0x00024000u) return false;
}
return (userData[7] & 7u) == 4u && userData[6] >= 1 && userData[14] >= 1;
}
inline std::optional<PatternFillRange> DecodeGcnPatternFillRange(std::span<const std::uint32_t> userData, std::span<const std::uint32_t> packet) {
if (userData.size() != 16 || packet.size() < 5 || packet[2] != 1 || packet[3] != 1) return std::nullopt;
const std::uint64_t invocations = (packet[4] & 0x20u) != 0 ? packet[1] : std::uint64_t{packet[1]} * 64u;
if (invocations > (std::uint64_t{1} << 32u)) return std::nullopt;
const auto base = [&](std::size_t word) { return userData[word] | (static_cast<std::uint64_t>(userData[word + 1] & 0xfffu) << 32u); };
if (base(4) % 4 != 0 || base(12) % 4 != 0) return std::nullopt;
return PatternFillRange{base(4), base(8), base(12), invocations, userData[10]};
}
inline std::optional<PatternFillRange> DecodePatternFillRange(std::span<const std::uint32_t> userData, std::span<const std::uint32_t> packet) {
if (userData.size() != 12 || packet.size() < 5 || packet[2] != 1 || packet[3] != 1) return std::nullopt;
const std::uint64_t invocations = (packet[4] & 0x20u) != 0 ? packet[1] : std::uint64_t{packet[1]} * 64u;
@@ -0,0 +1,31 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_SHADERS_REGISTRATIONPREPARATIONBUDGET_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_SHADERS_REGISTRATIONPREPARATIONBUDGET_HPP
#include <cerrno>
#include <cmath>
#include <cstdlib>
#include <cstdint>
#include <limits>
#include <stdexcept>
namespace AgcDriver::DriverDetail {
inline std::uint64_t RegistrationPreparationCommitBudgetBytes(const char* text, std::uint64_t defaultBudgetBytes) {
if (text == nullptr) return defaultBudgetBytes;
constexpr double GiB = 1024.0 * 1024.0 * 1024.0;
char* end = nullptr;
errno = 0;
const auto value = std::strtod(text, &end);
if (errno != 0 || end == text || *end != '\0' || !std::isfinite(value) || value < 0 || value >= static_cast<double>(std::numeric_limits<std::uint64_t>::max()) / GiB) {
throw std::runtime_error("APS5_REGISTRATION_PREPARE_COMMIT_GB must be a non-negative number of GiB");
}
return static_cast<std::uint64_t>(value * GiB);
}
constexpr bool RegistrationPreparationAllowed(std::uint64_t processPrivateBytes, std::uint64_t commitBudgetBytes) noexcept {
return processPrivateBytes < commitBudgetBytes;
}
}
#endif
@@ -257,6 +257,7 @@ std::optional<std::uint64_t> ReadProcessPrivateBytes();
std::optional<OverlayMemory> ParseCommitCharge(std::string_view meminfo);
std::optional<OverlayMemory> ReadCommitCharge();
std::optional<std::uint64_t> ReadPageFaults();
std::optional<std::uint64_t> ReadPhysicalMemoryBytes();
std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content);
OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner);
std::array<std::array<float, 4>, OverlayColorCount> OverlayPalette(VkFormat format, VkColorSpaceKHR colorSpace);
@@ -269,6 +269,15 @@ struct SpareRegisters {
SpareRegisters& operator=(SpareRegisters&&) noexcept = default;
};
inline Registers ClearStateContextRegisters() {
static const Registers& cleared = *new Registers([] {
auto registers = BuildInitialContextRegisters();
registers[0x1b6] = 2;
return registers;
}());
return cleared;
}
struct Predication {
std::uint64_t address = 0;
std::uint32_t operation = 0;
@@ -293,7 +302,7 @@ struct QueueState {
Predication predication;
void ClearContext() {
context = InitialContextRegisterSet();
context = ClearStateContextRegisters();
}
void PushContext() {
@@ -1,10 +1,11 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_WORKERSAMPLER_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_WORKERSAMPLER_HPP
#include <cstdint>
namespace AgcDriver {
// Starts sampling the calling thread when APS5_SAMPLE_WORKER names an output file.
void StartWorkerSampler();
void StartWorkerSampler(std::uint32_t queue = 0);
void StartCommitterSampler();
void StopWorkerSampler();
@@ -3,6 +3,7 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Graphics/include/HalfFloatScanout.hpp"
#include <algorithm>
@@ -12,6 +13,7 @@
#include <cstdlib>
#include <cstring>
#include <limits>
#include <optional>
#include <stdexcept>
#include <string>
@@ -39,7 +41,19 @@ std::uint32_t tileOffset64(std::uint32_t x, std::uint32_t y) {
return ((x << 3u) & 0x0008u) ^ ((x << 4u) & 0x0460u) ^ ((x << 5u) & 0x0300u) ^ ((x << 6u) & 0x0800u) ^ ((x << 9u) & 0x8000u) ^ ((x << 10u) & 0x2000u) ^ ((y << 4u) & 0x0010u) ^ ((y << 6u) & 0x0080u) ^ ((y << 5u) & 0x0f00u) ^ ((y << 10u) & 0x5000u);
}
const Graphics::ColorTargetLayout& gcnLayout(const DisplayBuffer& buffer, std::uint32_t texelBytes) {
thread_local std::optional<Graphics::ColorTargetLayout> layout;
thread_local std::array<std::uint32_t, 3> key{};
const std::array<std::uint32_t, 3> wanted{buffer.pitchInPixel, buffer.height, texelBytes};
if (!layout || key != wanted) {
layout.emplace(buffer.pitchInPixel, buffer.height, Graphics::ColorTileMode::GcnDisplay2D, texelBytes);
key = wanted;
}
return *layout;
}
std::size_t texelOffset(const DisplayBuffer& buffer, std::uint32_t x, std::uint32_t y, std::uint32_t texelBytes) {
if (buffer.tilingMode == DisplayTilingGcn) return gcnLayout(buffer, texelBytes).Offset(x, y);
if (buffer.tilingMode == 1) {
const auto pitch = buffer.pitchInPixel == 0 ? buffer.width : buffer.pitchInPixel;
return (static_cast<std::size_t>(y) * pitch + x) * texelBytes;
@@ -130,9 +144,14 @@ std::size_t DisplayBufferSize(const DisplayBuffer& buffer) {
throw std::runtime_error(message);
}
require(!halfFloat || buffer.dccAddress == 0, "VideoOut: DCC metadata on a 16-bit float display buffer is not implemented");
require(buffer.alignment == 65536u || (buffer.alignment == 4096u && buffer.tilingMode == 1), "VideoOut: 4 KiB display buffer alignment is only supported for linear buffers");
require(buffer.alignment == 65536u || (buffer.alignment == 4096u && (buffer.tilingMode == 1 || buffer.tilingMode == DisplayTilingGcn)), "VideoOut: 4 KiB display buffer alignment is only supported for linear and PS4 buffers");
require(buffer.address != 0 && (buffer.address & (buffer.alignment - 1u)) == 0, buffer.alignment == 65536u ? "VideoOut: display buffer requires 64 KiB alignment" : "VideoOut: display buffer requires 4 KiB alignment");
require(buffer.tilingMode <= 1, "VideoOut: unsupported display tiling mode");
require(buffer.tilingMode <= 1 || buffer.tilingMode == DisplayTilingGcn, "VideoOut: unsupported display tiling mode");
if (buffer.tilingMode == DisplayTilingGcn) {
require(buffer.dccAddress == 0 && !DisplayHalfFloat(buffer.pixelFormat), "VideoOut: PS4 display buffers carry no DCC and no half-float texels");
require(buffer.pitchInPixel >= buffer.width && buffer.pitchInPixel % 128u == 0, "VideoOut: PS4 tiled display pitch is not padded to 128 pixels");
return gcnLayout(buffer, DisplayTexelBytes(buffer.pixelFormat)).Bytes();
}
require(buffer.tilingMode != 0 || buffer.pitchInPixel == 0, "VideoOut: tiled display pitch is unsupported");
require(buffer.dccAddress == 0 || buffer.tilingMode == 0, "VideoOut: a linear display buffer cannot carry DCC metadata");
require(buffer.dccAddress != 0 || buffer.dccClearColor == 0, "VideoOut: a DCC clear color needs DCC metadata");
@@ -117,7 +117,7 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const
DrawProgram result{
{stage, address, std::span(snapshot.code).subspan(codeOffset), snapshot.headerAddress, snapshot.header},
userDataBase,
stage == Stage::Fragment ? 0u : 8u,
stage == Stage::Fragment || userDataBase == 0x04cu ? 0u : 8u,
{},
{{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}, {snapshot.headerAddress, snapshot.header}}},
program,
@@ -167,6 +167,8 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const
append(0xc8, type, Stage::Mesh, 0x8b, 0x8c, Role::Main);
InitializeMerged(queue, programs.back(), 0x82, type == 4, staticAbi);
if (type == 4) append(0x88, 6, Stage::Mesh, 0x8b, 0x8c, Role::GeometryBack);
} else if (graphics.stages.legacyVertex) {
append(0x48, 2, Stage::Vertex, 0x4b, 0x4c, Role::Main);
} else {
append(0xc8, 2, Stage::Vertex, 0x8b, 0x8c, Role::Main);
}
@@ -87,6 +87,18 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
for (const auto word : pixel.targetMerge.descriptor) mix(word);
mix(pixel.targetMerge.depthStencilWrites);
mix(pixel.targetMerge.depthStencilTests);
if (pixel.targetMerge.blend.enabled) {
const auto& blend = pixel.targetMerge.blend;
mix(blend.snorm);
mix(blend.colorSource);
mix(blend.colorDestination);
mix(blend.colorOperation);
mix(blend.alphaSource);
mix(blend.alphaDestination);
mix(blend.alphaOperation);
mix(blend.writeMask);
for (const auto word : blend.constants) mix(word);
}
mix(pixel.alphaToCoverage);
mix(static_cast<std::uint64_t>(pixel.rasterPrimitive));
mix(pixel.ditheredAlphaToCoverage);
@@ -467,7 +467,8 @@ std::uint64_t DrawPipeline::DrainRequestsByOpcode(std::uint32_t opcode) {
}
bool DrawPipeline::HeldReads(std::uint64_t address, std::size_t bytes) {
if (bytes == 0 || std::getenv("APS5_NO_QUEUED_DRAW_READ_ORDER") != nullptr) return false;
static const bool unordered = std::getenv("APS5_NO_QUEUED_DRAW_READ_ORDER") != nullptr;
if (bytes == 0 || unordered) return false;
const auto end = address + bytes;
auto& pipeline = Queue0();
std::lock_guard lock(pipeline.mutex);
@@ -12,7 +12,7 @@ namespace AgcDriver::DriverDetail {
namespace {
bool runtimeRegister(std::uint32_t offset) {
return offset - 0x00cu < 32u || offset - 0x08cu < 32u || offset - 0x10cu < 32u || offset == 0x082u || offset == 0x083u || offset == 0x102u || offset == 0x103u;
return offset - 0x00cu < 32u || offset - 0x04cu < 16u || offset - 0x08cu < 32u || offset - 0x10cu < 32u || offset == 0x082u || offset == 0x083u || offset == 0x102u || offset == 0x103u;
}
const auto& registerMasks() {
@@ -71,10 +71,11 @@ std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool
mixPart(part, it->second);
}
};
for (const auto base : {0x00cu, 0x08cu, 0x10cu}) {
for (const auto base : {0x00cu, 0x04cu, 0x08cu, 0x10cu}) {
const auto limit = base == 0x04cu ? 16u : 32u;
const auto resources = queue.shader.find(base - 1);
const auto count = resources == queue.shader.end() ? 0u : ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u);
words(base, allUserWords ? 32u : std::min(count, 32u), userData);
words(base, allUserWords ? limit : std::min(count, limit), userData);
}
words(0x082u, 2, auxiliary);
words(0x102u, 2, auxiliary);
@@ -86,7 +87,7 @@ std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool
std::uint64_t DrawShaderSetKey(const QueueState& queue, const ShaderRegistry& registry) {
std::uint64_t key = 0xcbf29ce484222325ull;
const auto mix = [&](std::uint64_t value) { key = (key ^ value) * 0x100000001b3ull; };
for (const auto base : {0x008u, 0x088u, 0x0c8u, 0x108u, 0x148u}) {
for (const auto base : {0x008u, 0x048u, 0x088u, 0x0c8u, 0x108u, 0x148u}) {
const auto low = queue.shader.find(base);
const auto high = queue.shader.find(base + 1);
if (low == queue.shader.end() || high == queue.shader.end()) { mix(0); continue; }
@@ -167,7 +168,7 @@ std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegis
(*components)[4] = DrawShaderSetKey(queue, registry);
}
mix(deviceSerial);
for (const auto base : {0x008u, 0x088u, 0x0c8u, 0x108u, 0x148u}) {
for (const auto base : {0x008u, 0x048u, 0x088u, 0x0c8u, 0x108u, 0x148u}) {
const auto low = queue.shader.find(base);
const auto high = queue.shader.find(base + 1);
if (low == queue.shader.end() || high == queue.shader.end()) {
@@ -7,6 +7,7 @@
#include "prx/libkernel/Time/include/SleepWake.hpp"
#include <algorithm>
#include <mutex>
#include "RdnaDecoder/RdnaInstructionDecoder.hpp"
namespace AgcDriver {
@@ -91,6 +92,15 @@ extern "C" void AgcDriverWaitIdle_nid_postfix() try {
LibcAwaitExit_nid_postfix();
}
extern "C" void AgcDriverSubmitGraphics_nid_postfix(std::uint32_t* commands, std::uint32_t dwords) {
const Packet packet{commands, dwords, 0, {}};
AgcDriver::Submit(&packet, 0);
}
extern "C" void AgcDriverUseGcnShaders_nid_postfix() {
ShaderRecompiler::SetGcnGuestShaders(true);
}
static std::mutex& VulkanLoaderMutex() {
static std::mutex mutex;
return mutex;
@@ -51,7 +51,7 @@ bool Driver::matchesFillKernel(std::span<const std::uint32_t> code, const std::v
static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr;
static const bool patternEnabled = std::getenv("APS5_NO_PATTERN_FILL_HLE") == nullptr;
if (!enabled || userData.size() < 8 || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return false;
if (patternEnabled && MatchesPatternFillKernel(code, userData, compute)) return true;
if (patternEnabled && (MatchesPatternFillKernel(code, userData, compute) || MatchesGcnPatternFillKernel(code, userData, compute))) return true;
static constexpr std::array<std::uint32_t, 9> fillKernel{0xd7460004u, 0x04010c08u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0xe01c2000u, 0x80000004u, 0xbf810000u};
if (!matchesBufferPatternFillKernel(code) && (code.size() < fillKernel.size() || !std::equal(fillKernel.begin(), fillKernel.end(), code.begin()))) return false;
@@ -275,8 +275,8 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons
std::fill(pattern.begin() + 1, pattern.end(), pattern[0]);
base = memoryFill->address;
bytes = memoryFill->bytes;
} else if (MatchesPatternFillKernel(code, userData, compute)) {
const auto range = DecodePatternFillRange(userData, packet);
} else if (MatchesPatternFillKernel(code, userData, compute) || MatchesGcnPatternFillKernel(code, userData, compute)) {
const auto range = MatchesGcnPatternFillKernel(code, userData, compute) ? DecodeGcnPatternFillRange(userData, packet) : DecodePatternFillRange(userData, packet);
if (!range) return false;
if (range->invocations == 0) return true;
drainPendingInputWrites(queueId, {{range->control, 8}, {range->source, 4}});
@@ -141,7 +141,7 @@ void Driver::run(std::uint32_t id) noexcept {
PinWorkerThread(role);
GuestMemory::TagGpuLockThread(id);
if (id == 0) StartWorkerSampler();
StartWorkerSampler(id);
Submission submission;
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
auto& costs = submissionCosts(id);
@@ -1,12 +1,37 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp"
#include "ControlFlow/RequestSerializer.hpp"
#include <charconv>
#include <cstdlib>
#include <functional>
#include <optional>
#include <string_view>
namespace AgcDriver::DriverDetail {
namespace {
std::optional<std::set<std::uint64_t>> selectedPrograms() {
const char* value = std::getenv("APS5_DUMP_SHADERS");
if (value == nullptr || std::string_view(value) == "1") return std::nullopt;
std::set<std::uint64_t> programs;
std::string_view rest(value);
while (!rest.empty()) {
const auto comma = rest.find(',');
auto token = rest.substr(0, comma);
rest = comma == std::string_view::npos ? std::string_view{} : rest.substr(comma + 1);
if (token.starts_with("0x") || token.starts_with("0X")) token.remove_prefix(2);
std::uint64_t address = 0;
const auto [end, error] = std::from_chars(token.data(), token.data() + token.size(), address, 16);
if (token.empty() || error != std::errc{} || end != token.data() + token.size()) return std::nullopt;
programs.insert(address);
}
if (programs.empty()) return std::nullopt;
return programs;
}
}
void Driver::reportSkip(const char* kind, const std::string& what) {
static AgcDriver::Mutex reportedMutex;
static std::set<std::size_t> reported;
@@ -60,6 +85,7 @@ std::string Driver::dumpRequest(std::uint64_t address, const ShaderRecompiler::R
char hex[32];
std::snprintf(hex, sizeof(hex), "%llx", static_cast<unsigned long long>(address));
const std::string name = "shader_" + std::string(hex) + std::string(suffix) + ".req";
if (const auto selected = selectedPrograms(); selected.has_value() && !selected->contains(address)) return {};
std::lock_guard lock(dumpMutex);
if (!dumped.insert(name).second) return name;
try {
@@ -18,6 +18,8 @@
#include <array>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cerrno>
#include <set>
#include <condition_variable>
#include <cstdio>
@@ -33,6 +35,7 @@
#include <list>
#include <map>
#include <mutex>
#include <limits>
#include <atomic>
#include <pthread.h>
#ifdef _WIN32
@@ -50,6 +53,8 @@
#include <stdexcept>
#include <thread>
#include "Optimization/ShaderStageInputInfo.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/RegistrationPreparationBudget.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp"
#include <spirv/unified1/spirv.hpp>
#include "RdnaDecoder/RdnaInstructionDecoder.hpp"
@@ -1028,7 +1033,8 @@ std::vector<ShaderRecompiler::TableCallee> ReadTableCallees(const ShaderRecompil
}
ShaderRecompiler::LinkedCalls LinkUserDataCallees(const ShaderRecompiler::RecompileRequest& request) {
if (request.shader.stage != ShaderRecompiler::ShaderStage::Compute) return {};
const bool gcnVertex = ShaderRecompiler::GcnGuestShaders() && request.shader.stage == ShaderRecompiler::ShaderStage::Vertex;
if (request.shader.stage != ShaderRecompiler::ShaderStage::Compute && !gcnVertex) return {};
const auto code = request.shader.code;
if (std::none_of(code.begin(), code.end(), [](std::uint32_t word) { return (word & 0xff80ff00u) == 0xbe802100u; })) return {};
const auto program = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(code);
@@ -1209,7 +1215,15 @@ struct RegisteredPreparation {
std::shared_ptr<VulkanDevice> prebuildDevice;
};
bool RegistrationPrepareWithinMemoryBudget();
void LogRegistrationPrepareDeferred();
std::unique_ptr<RegisteredPreparation> PlanRegistered(const ShaderSnapshot& snapshot, const VulkanDevice& device, const QueueState& state, bool registration, bool* deferred = nullptr) {
if (registration && std::getenv("APS5_LAZY_SHADER_PREPARE") == nullptr && !RegistrationPrepareWithinMemoryBudget()) {
if (deferred != nullptr) *deferred = true;
LogRegistrationPrepareDeferred();
return {};
}
using Stage = ShaderRecompiler::ShaderStage;
const auto header = ReadHeader(snapshot);
std::uint32_t programRegister;
@@ -1705,14 +1719,39 @@ bool AsyncRegistrationPrepare() {
return !sync;
}
std::uint64_t RegistrationPrepareCommitBudgetBytes() {
static const std::uint64_t budget = [] {
if (const auto* text = std::getenv("APS5_REGISTRATION_PREPARE_COMMIT_GB")) {
return RegistrationPreparationCommitBudgetBytes(text, 0);
}
const auto physicalBytes = ReadPhysicalMemoryBytes();
const auto defaultBudget = physicalBytes ? *physicalBytes / 2 : std::numeric_limits<std::uint64_t>::max();
return RegistrationPreparationCommitBudgetBytes(nullptr, defaultBudget);
}();
return budget;
}
bool RegistrationPrepareWithinMemoryBudget() {
const auto privateBytes = ReadProcessPrivateBytes();
return !privateBytes || RegistrationPreparationAllowed(*privateBytes, RegistrationPrepareCommitBudgetBytes());
}
void LogRegistrationPrepareDeferred() {
static std::atomic<bool> reported{false};
if (reported.exchange(true, std::memory_order_relaxed)) return;
APS5_LOG_OUT("%s", "process private memory reached the registered shader preparation budget; later registrations are prepared when first used (APS5_REGISTRATION_PREPARE_COMMIT_GB)");
}
void PrepareRegisteredInBackground(std::shared_ptr<const ShaderSnapshot> snapshot, std::shared_ptr<RegisteredPreparation> plan) {
const auto* preparedKey = snapshot->prepared.get();
SubmitRegistrationPreparation([snapshot = std::move(snapshot), plan = std::move(plan)] {
PerformanceContext timingContext(FrameTiming::Preparation());
std::vector<PreparedShaders::Entry> entries;
std::exception_ptr failure;
bool deferred = false;
try {
entries = PrepareRegistered(*plan);
if (RegistrationPrepareWithinMemoryBudget()) entries = PrepareRegistered(*plan);
else deferred = true;
} catch (const std::exception& error) {
APS5_LOG_ERR("Shader 0x%llx snapshot=0x%llx could not be prepared after registration: %s", static_cast<unsigned long long>(snapshot->codeAddress), static_cast<unsigned long long>(reinterpret_cast<std::uintptr_t>(snapshot.get())), error.what());
failure = std::current_exception();
@@ -1723,10 +1762,12 @@ void PrepareRegisteredInBackground(std::shared_ptr<const ShaderSnapshot> snapsho
{
std::lock_guard lock(prepared.mutex);
prepared.entries.insert(prepared.entries.end(), std::make_move_iterator(entries.begin()), std::make_move_iterator(entries.end()));
if (failure == nullptr && !plan->abiKey.empty()) prepared.registeredAbis.push_back(std::move(plan->abiKey));
if (failure == nullptr && !deferred && !plan->abiKey.empty()) prepared.registeredAbis.push_back(std::move(plan->abiKey));
prepared.deferred = prepared.deferred || deferred;
prepared.failure = failure;
prepared.pending = false;
}
if (deferred) LogRegistrationPrepareDeferred();
prepared.settled.notify_all();
}, preparedKey);
}
@@ -15,6 +15,7 @@
#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp"
#include <algorithm>
#include <atomic>
#include <cerrno>
#include <cmath>
#include <cstdio>
#include <cstdlib>
@@ -25,6 +26,9 @@
#include <mutex>
#include <stdexcept>
#include <string_view>
#ifndef _WIN32
#include <unistd.h>
#endif
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
@@ -686,6 +690,20 @@ std::optional<std::uint64_t> ReadPageFaults() {
#endif
}
std::optional<std::uint64_t> ReadPhysicalMemoryBytes() {
#ifdef _WIN32
MEMORYSTATUSEX status{};
status.dwLength = sizeof(status);
if (::GlobalMemoryStatusEx(&status) == 0) return std::nullopt;
return static_cast<std::uint64_t>(status.ullTotalPhys);
#else
const auto pages = ::sysconf(_SC_PHYS_PAGES);
const auto pageSize = ::sysconf(_SC_PAGESIZE);
if (pages <= 0 || pageSize <= 0) return std::nullopt;
return static_cast<std::uint64_t>(pages) * static_cast<std::uint64_t>(pageSize);
#endif
}
std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content) {
auto scale = std::max<std::int32_t>(1, static_cast<std::int32_t>((extent.height + 240) / 480));
while (scale > 1 && !fits(extent, content, scale)) --scale;
@@ -3297,7 +3297,9 @@ void WriteFrameBmp(int index, std::uint32_t width, std::uint32_t height, std::sp
ResidentPresent ResidentPresentable(const Graphics::Context& context, const Graphics::StorageTexture& image, const DisplayBuffer& buffer, VkFilter& filter, std::uint32_t& layer) {
const auto& descriptor = image.Descriptor();
if (descriptor.width != buffer.width || descriptor.height != buffer.height || descriptor.mipCount != 1 || image.ImageDepth() != 1) return ResidentPresent::None;
const bool gcn = buffer.tilingMode == DisplayTilingGcn;
if (gcn && (descriptor.tileMode != Graphics::TextureTileMode::kGcnDisplay2D || descriptor.width != buffer.pitchInPixel || descriptor.height < buffer.height || descriptor.mipCount != 1 || image.ImageDepth() != 1)) return ResidentPresent::None;
if (!gcn && (descriptor.width != buffer.width || descriptor.height != buffer.height || descriptor.mipCount != 1 || image.ImageDepth() != 1)) return ResidentPresent::None;
const auto slice = ResidentPresentSlice(descriptor.baseAddress, image.GuestBytes(), image.ImageLayers(), buffer.address, DisplayBufferSize(buffer));
if (!slice) return ResidentPresent::None;
layer = *slice;
@@ -326,9 +326,13 @@ void StartCallingThreadSampler(const char* path, std::jthread& slot) {
}
void StartWorkerSampler() {
StartProcessSampler();
StartCallingThreadSampler(std::getenv("APS5_SAMPLE_WORKER"), workerSamplerThread);
void StartWorkerSampler(std::uint32_t queue) {
if (queue == 0) StartProcessSampler();
static const auto sampledQueue = [] {
const char* value = std::getenv("APS5_SAMPLE_WORKER_QUEUE");
return value != nullptr ? static_cast<std::uint32_t>(std::strtoul(value, nullptr, 0)) : 0u;
}();
if (queue == sampledQueue) StartCallingThreadSampler(std::getenv("APS5_SAMPLE_WORKER"), workerSamplerThread);
}
void StartCommitterSampler() {
@@ -344,7 +348,7 @@ void StopWorkerSampler() {
if (committerSamplerThread.joinable()) committerSamplerThread.join();
}
#else
void StartWorkerSampler() {}
void StartWorkerSampler(std::uint32_t) {}
void StartCommitterSampler() {}
void StopWorkerSampler() {}
#endif
@@ -3,6 +3,7 @@
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
#include <span>
@@ -20,9 +21,17 @@ enum class ColorTileMode : std::uint32_t {
D4KBX = 0x16,
Z64KBX = 0x18,
D64KBX = 0x1a,
RenderTarget = 0x1b
RenderTarget = 0x1b,
GcnThin1D = 0x100,
GcnDisplay1D = 0x101,
GcnDisplay2D = 0x102,
GcnThin2D = 0x103
};
constexpr bool ColorTileModeIsGcn(ColorTileMode mode) {
return mode == ColorTileMode::GcnThin1D || mode == ColorTileMode::GcnDisplay1D || mode == ColorTileMode::GcnDisplay2D || mode == ColorTileMode::GcnThin2D;
}
constexpr bool ColorTileModeIsXor(ColorTileMode mode) {
return mode == ColorTileMode::S4KBX || mode == ColorTileMode::D4KBX || mode == ColorTileMode::Z64KBX || mode == ColorTileMode::D64KBX || mode == ColorTileMode::RenderTarget;
}
@@ -32,6 +41,10 @@ constexpr std::uint32_t ColorTileModeBlockBytes(ColorTileMode mode) {
}
constexpr TextureTileMode ColorTextureTileMode(ColorTileMode mode) {
if (mode == ColorTileMode::GcnThin1D) return TextureTileMode::kGcnThin1D;
if (mode == ColorTileMode::GcnDisplay1D) return TextureTileMode::kGcnDisplay1D;
if (mode == ColorTileMode::GcnDisplay2D) return TextureTileMode::kGcnDisplay2D;
if (mode == ColorTileMode::GcnThin2D) return TextureTileMode::kGcnThin2D;
return mode == ColorTileMode::Linear ? TextureTileMode::kLinear : mode == ColorTileMode::Standard4KB ? TextureTileMode::kStandard4KB : mode == ColorTileMode::Standard64KB ? TextureTileMode::kStandard64KB : mode == ColorTileMode::S4KBX ? TextureTileMode::kS4KBX : mode == ColorTileMode::D4KBX ? TextureTileMode::kD4KBX : mode == ColorTileMode::D64KBX ? TextureTileMode::kD64KBX : mode == ColorTileMode::Z64KBX ? TextureTileMode::kZ64KBX : TextureTileMode::kR64KBX;
}
@@ -43,7 +56,7 @@ public:
ColorTargetLayout(std::uint32_t width, std::uint32_t height, ColorTileMode mode, std::uint32_t bytesPerElement = 4, std::uint32_t pipeBankXor = 0);
std::size_t Bytes() const { return bytes; }
std::size_t LinearBytes() const { return static_cast<std::size_t>(width) * height * elementBytes; }
std::size_t Alignment() const { return ColorTileModeBlockBytes(mode); }
std::size_t Alignment() const { return blockBytes; }
std::uint32_t BlocksPerRow() const { return pitch / blockWidth; }
std::size_t Offset(std::uint32_t x, std::uint32_t y) const;
void Detile(std::span<const std::byte> source, std::span<std::byte> destination) const;
@@ -59,7 +72,10 @@ private:
std::uint32_t elementBytes;
std::uint32_t blockWidth = 1;
std::uint32_t blockHeight = 1;
std::size_t blockBytes = 0;
std::uint32_t pipeBankXor = 0;
bool gcn = false;
std::array<std::uint32_t, 16> gcnEquation{};
// The XOR swizzle is linear over GF(2), so a block offset is xOffsets[x] ^ yOffsets[y].
const std::uint32_t* xOffsets = nullptr;
const std::uint32_t* yOffsets = nullptr;
@@ -25,6 +25,21 @@ enum class TextureTileMode {
kD64KBT,
kS4KBX,
kD4KBX,
kGcnThin1D,
kGcnDisplay1D,
kGcnDisplay2D,
kGcnThin2D,
kGcnDepth2D64,
kGcnDepth2D128,
kGcnDepth2D256,
kGcnDepth2D512,
kGcnDepth2D1K,
kGcnDepthPrt256,
kGcnDepthPrt1K,
kGcnDisplayPrt,
kGcnDisplay2DPrt,
kGcnThinPrt,
kGcnThin2DPrt,
// Upstream's name for SW_64KB_R_X (tile mode 0x1b).
RenderTarget64KB = kR64KBX
};
@@ -91,8 +106,37 @@ struct GuestTextureResource {
std::uint32_t allocatedMipCount = 0;
std::uint32_t samples = 1;
std::uint64_t fmaskCmaskAddress = 0;
std::uint32_t gcnPitch = 0;
bool gcnPow2Pad = false;
bool gcnLinearGeneral = false;
};
constexpr bool IsGcnMicroTiled(TextureTileMode mode) {
return mode == TextureTileMode::kGcnThin1D || mode == TextureTileMode::kGcnDisplay1D;
}
constexpr bool IsGcnDepthTiled(TextureTileMode mode) {
return mode == TextureTileMode::kGcnDepth2D64 || mode == TextureTileMode::kGcnDepth2D128 || mode == TextureTileMode::kGcnDepth2D256 || mode == TextureTileMode::kGcnDepth2D512 || mode == TextureTileMode::kGcnDepth2D1K
|| mode == TextureTileMode::kGcnDepthPrt256 || mode == TextureTileMode::kGcnDepthPrt1K;
}
constexpr bool IsGcnPrtTiled(TextureTileMode mode) {
return mode == TextureTileMode::kGcnDepthPrt256 || mode == TextureTileMode::kGcnDepthPrt1K || mode == TextureTileMode::kGcnDisplayPrt || mode == TextureTileMode::kGcnDisplay2DPrt || mode == TextureTileMode::kGcnThinPrt || mode == TextureTileMode::kGcnThin2DPrt;
}
constexpr bool IsGcnMacroTiled(TextureTileMode mode) {
return mode == TextureTileMode::kGcnDisplay2D || mode == TextureTileMode::kGcnThin2D || IsGcnDepthTiled(mode) || IsGcnPrtTiled(mode);
}
constexpr bool IsGcnTiled(TextureTileMode mode) {
return IsGcnMicroTiled(mode) || IsGcnMacroTiled(mode);
}
constexpr TextureTileMode GcnMicroTileMode(TextureTileMode mode) {
if (mode == TextureTileMode::kGcnDisplay2D || mode == TextureTileMode::kGcnDisplayPrt || mode == TextureTileMode::kGcnDisplay2DPrt) return TextureTileMode::kGcnDisplay1D;
return IsGcnMacroTiled(mode) ? TextureTileMode::kGcnThin1D : mode;
}
float EffectiveMinLod(const GuestTextureResource& resource);
bool IsNullTextureResource(std::span<const std::uint32_t> words);
@@ -42,8 +42,11 @@ struct ShaderStages {
float maxTessellationLevel = 64.0f;
std::uint32_t hullWaveSize = 64;
std::optional<ShaderRecompiler::MeshConfiguration> tessellationGeometry;
bool legacyVertex = false;
};
inline constexpr std::uint16_t UnclampedBlendMask = 0xffffu;
struct ColorTarget {
std::uint64_t address;
VkExtent2D extent;
@@ -305,7 +308,7 @@ struct DrawKeyRange {
std::uint32_t first;
std::uint32_t count;
};
inline constexpr std::array<DrawKeyRange, 50> DrawKeyRegisters{{
inline constexpr std::array<DrawKeyRange, 52> DrawKeyRegisters{{
{RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 2}, {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_BARYC_CNTL, SPI_SHADER_POS/Z/COL_FORMAT,
@@ -313,13 +316,13 @@ inline constexpr std::array<DrawKeyRange, 50> DrawKeyRegisters{{
{RegisterBank::Context, 0x191, 32}, {RegisterBank::Context, 0x1b3, 2}, {RegisterBank::Context, 0x1b6, 3}, {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, 0x2af, 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},
{RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x286, 2}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2a8, 2}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2af, 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, 0x10}, {RegisterBank::Context, 0x3a8, 0x18},
// The pixel program address, RSRC2 and user words; the geometry-back user pointer and program
// address; the vertex/geometry-front RSRC1/RSRC2 and user words; the vertex program address;
// the hull user pointer, program address, RSRC2 and user words; the local program address.
{RegisterBank::Shader, 0x008, 0x24}, {RegisterBank::Shader, 0x082, 2}, {RegisterBank::Shader, 0x088, 2}, {RegisterBank::Shader, 0x08a, 0x22}, {RegisterBank::Shader, 0x0c8, 2}, {RegisterBank::Shader, 0x102, 2}, {RegisterBank::Shader, 0x108, 2}, {RegisterBank::Shader, 0x10b, 0x21}, {RegisterBank::Shader, 0x148, 2},
{RegisterBank::Shader, 0x008, 0x24}, {RegisterBank::Shader, 0x048, 0x14}, {RegisterBank::Shader, 0x082, 2}, {RegisterBank::Shader, 0x088, 2}, {RegisterBank::Shader, 0x08a, 0x22}, {RegisterBank::Shader, 0x0c8, 2}, {RegisterBank::Shader, 0x102, 2}, {RegisterBank::Shader, 0x108, 2}, {RegisterBank::Shader, 0x10b, 0x21}, {RegisterBank::Shader, 0x148, 2},
// GE_PRIM_TYPE, GE_MULTI_PRIM_IB_RESET_EN, the geometry subgroup sizes.
{RegisterBank::UserConfig, 0x242, 1}, {RegisterBank::UserConfig, 0x24b, 1}, {RegisterBank::UserConfig, 0x25b, 1},
}};
@@ -163,6 +163,7 @@ private:
VkFormat blockFormat = VK_FORMAT_UNDEFINED;
bool repackCopy = false;
std::uint32_t copyWidth = 0, copyHeight = 0, copyLevels = 0, copyLayers = 0, copyElementBytes = 0;
std::uint32_t copyTexelsPerElement = 4;
};
VkFormat AttachmentProxyFormat(const Context& context, VkFormat format);
@@ -22,6 +22,8 @@ struct TileMipLayout {
bool tail;
std::uint32_t tailX;
std::uint32_t tailY;
bool gcnMicroTiled = false;
std::uint32_t gcnSplitStride = 0;
};
std::vector<TileMipLayout> ComputeMipLayout(TextureTileMode tileMode, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t mipCount);
@@ -67,6 +69,10 @@ struct SurfaceGeometry {
std::uint32_t blockDepth = 1;
std::uint64_t layerBytes = 0;
bool levelMajor = false;
std::uint64_t GuestOffset(std::uint32_t layer, const TileMipLayout& mip) const {
return levelMajor ? mip.tiledOffset + static_cast<std::uint64_t>(layer) * mip.tiledSize : GuestLayerOffset(layer) + mip.tiledOffset;
}
std::uint64_t GuestLayerOffset(std::uint32_t layer) const { return thick ? static_cast<std::uint64_t>(layer / blockDepth) * layerBytes : static_cast<std::uint64_t>(layer) * layerBytes; }
std::uint64_t LinearLayerOffset(std::uint32_t layer) const { return static_cast<std::uint64_t>(layer) * sliceLinearBytes; }
std::uint32_t CopyLayer(std::uint32_t layer) const { return imageDepth > 1 ? 0u : layer; }
@@ -74,6 +80,17 @@ struct SurfaceGeometry {
bool HasLayer(std::uint32_t level, std::uint32_t layer) const { return imageDepth <= 1 || layer < std::max(imageDepth >> level, 1u); }
};
SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor);
std::vector<TileMipLayout> ComputeGcnMipLayout(TextureTileMode tileMode, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t pitch, bool pow2Pad, std::uint32_t mipCount, bool linearGeneral = false, std::uint32_t layers = 1);
struct GcnTileGeometry {
std::uint32_t blockBytes;
std::uint32_t blockWidth;
std::uint32_t blockHeight;
std::array<std::uint32_t, 16> equation;
std::uint32_t splitSlices = 1;
std::array<std::uint32_t, 4> splitMasks{};
};
GcnTileGeometry GcnTiling(TextureTileMode tileMode, std::uint32_t bytesPerElement);
inline TextureTileMode MipTileMode(TextureTileMode tileMode, const TileMipLayout& mip) { return mip.gcnMicroTiled ? GcnMicroTileMode(tileMode) : tileMode; }
bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t levels);
GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed, bool keysReadAsStored);
GuestTextureResource ArrayLayerWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers);
@@ -29,6 +29,10 @@ layout(constant_id = 19) const uint EQ15 = 0u;
// Nonzero: block extent in elements (thick 3D blocks are not the thin 2D shape derived below).
layout(constant_id = 20) const uint BLOCK_WIDTH = 0u;
layout(constant_id = 21) const uint BLOCK_HEIGHT = 0u;
layout(constant_id = 22) const uint SPLIT0 = 0u;
layout(constant_id = 23) const uint SPLIT1 = 0u;
layout(constant_id = 24) const uint SPLIT2 = 0u;
layout(constant_id = 25) const uint SPLIT3 = 0u;
layout(local_size_x = 8, local_size_y = 8) in;
@@ -64,6 +68,7 @@ layout(push_constant) uniform Push {
uint columnBegin;
uint rowBegin;
uint pipeBankXor;
uint splitStride;
} params;
uint equationBit(uint mask, uvec2 p) {
@@ -242,6 +247,10 @@ void main() {
uint blockIndex = block.y * params.blocksPerRow + block.x;
uint tiledOffset = blockIndex * BLOCK_BYTES + blockOffset(swizzle);
if (SPLIT0 != 0u) {
uint split = equationBit(SPLIT0, p) | (equationBit(SPLIT1, p) << 1) | (equationBit(SPLIT2, p) << 2) | (equationBit(SPLIT3, p) << 3);
tiledOffset += split * params.splitStride;
}
if (tiledOffset < params.rangeBegin || tiledOffset >= params.rangeEnd) {
return;
}
@@ -54,6 +54,7 @@ ColorTileMode DecodeColorTileMode(std::uint32_t attrib3) {
}
TileMipLayout ComputeColorTargetMipLayout(ColorTileMode mode, std::uint32_t bytesPerElement, std::uint32_t surfaceWidth, std::uint32_t surfaceHeight, std::uint32_t mipCount, std::uint32_t mipLevel) {
require(!ColorTileModeIsGcn(mode), "AGC graphics: PS4 color target mips are laid out by ComputeGcnMipLayout");
auto mips = ComputeElementMipLayout(ColorTextureTileMode(mode), bytesPerElement, surfaceWidth, surfaceHeight, mipCount);
require(mipLevel < mips.size(), "AGC graphics: color target mip is outside its surface");
auto mip = mips[mipLevel];
@@ -156,8 +157,22 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height,
yOffsets = tables.y.data();
break;
}
case ColorTileMode::GcnThin1D:
case ColorTileMode::GcnDisplay1D:
case ColorTileMode::GcnDisplay2D:
case ColorTileMode::GcnThin2D: {
const auto geometry = GcnTiling(ColorTextureTileMode(mode), bytesPerElement);
blockWidth = geometry.blockWidth;
blockHeight = geometry.blockHeight;
pitch = (width + blockWidth - 1u) / blockWidth * blockWidth;
paddedHeight = (height + blockHeight - 1u) / blockHeight * blockHeight;
gcn = true;
gcnEquation = geometry.equation;
break;
}
default: throw std::runtime_error("AGC graphics: unsupported color tile mode");
}
blockBytes = gcn ? static_cast<std::size_t>(blockWidth) * blockHeight * bytesPerElement : ColorTileModeBlockBytes(mode);
const auto size = static_cast<std::uint64_t>(pitch) * paddedHeight * bytesPerElement;
require(size <= std::numeric_limits<std::size_t>::max(), "AGC graphics: color surface size overflow");
bytes = static_cast<std::size_t>(size);
@@ -166,6 +181,11 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height,
std::size_t ColorTargetLayout::offset(std::uint32_t x, std::uint32_t y) const {
if (mode == ColorTileMode::Linear) return (static_cast<std::size_t>(y) * pitch + x) * elementBytes;
const auto block = static_cast<std::size_t>(y / blockHeight) * (pitch / blockWidth) + x / blockWidth;
if (gcn) {
std::uint32_t inBlock = 0;
for (std::uint32_t bit = 0; bit < 16u; ++bit) inBlock |= parity((x & gcnEquation[bit] & 0xfffu) ^ ((y << 12u) & gcnEquation[bit] & 0xfff000u)) << bit;
return block * blockBytes + inBlock;
}
auto inBlock = xOffsets[x % blockWidth] ^ yOffsets[y % blockHeight];
if (equation != nullptr) {
const auto highX = (x & ~(blockWidth - 1u)) & 0xfffu;
@@ -1319,6 +1319,7 @@ public:
Require(requested != 0 && requested <= layers, "depth plane copy slices exceed its layers");
Require(baseLayer < requested, "a depth plane copy view starts past the slices it was given");
const auto geometry = DescribeSurface(resource);
Require(!geometry.levelMajor, "depth planes of a PS4 layered surface with several mip levels (stored level by level) are not implemented");
const bool guestFill = std::any_of(slices.begin(), slices.end(), [](const auto& slice) { return slice.image == VK_NULL_HANDLE; });
if (guestFill && (geometry.layers != requested || geometry.sliceLinearBytes < sliceBytes() || geometry.mips.empty())) {
char text[320];
@@ -2530,6 +2531,7 @@ std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<c
}
std::vector<DepthPlaneSlice> slices;
const auto geometry = DescribeSurface(resource);
Require(!geometry.levelMajor, "depth planes of a PS4 layered surface with several mip levels (stored level by level) are not implemented");
for (std::uint32_t layer = 0; layer < layers; ++layer) {
const auto address = base.address + geometry.GuestLayerOffset(layer);
auto* slice = depthSlice(context.device, address, base);
@@ -2666,6 +2668,7 @@ std::shared_ptr<StorageTexture> layeredDepthStorage(const Context& context, Dept
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
const auto bytesPerTexel = d16 ? 2u : 4u;
surface.RequireStorable(resource);
Require(!DescribeSurface(resource).levelMajor, "depth planes of a PS4 layered surface with several mip levels (stored level by level) are not implemented");
auto& entry = layeredStorages()[{context.device, resource.baseAddress, resource.width, resource.height, layers, resource.format, static_cast<int>(resource.tileMode), static_cast<int>(resource.dimension), resource.baseArray}];
if (entry == nullptr) entry = std::make_unique<LayeredDepthStorage>(context, resource, target, bytesPerTexel, resource.baseArray, layers);
else static_cast<void>(entry->storage->Refresh());
@@ -4,6 +4,8 @@
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
#include "RdnaDecoder/RdnaDescriptorFormat.hpp"
#include "RdnaDecoder/RdnaInstructionDecoder.hpp"
#include <array>
#include <algorithm>
#include <array>
#include <bit>
@@ -40,6 +42,28 @@ TextureTileMode resolveTileMode(std::uint32_t raw) {
}
}
TextureTileMode resolveGcnTileMode(std::uint32_t index) {
switch (index) {
case 0u: return TextureTileMode::kGcnDepth2D64;
case 1u: return TextureTileMode::kGcnDepth2D128;
case 2u: return TextureTileMode::kGcnDepth2D256;
case 3u: return TextureTileMode::kGcnDepth2D512;
case 4u: return TextureTileMode::kGcnDepth2D1K;
case 6u: return TextureTileMode::kGcnDepthPrt256;
case 7u: return TextureTileMode::kGcnDepthPrt1K;
case 8u: case 31u: return TextureTileMode::kLinear;
case 11u: return TextureTileMode::kGcnDisplayPrt;
case 12u: return TextureTileMode::kGcnDisplay2DPrt;
case 16u: return TextureTileMode::kGcnThinPrt;
case 17u: return TextureTileMode::kGcnThin2DPrt;
case 5u: case 13u: return TextureTileMode::kGcnThin1D;
case 9u: return TextureTileMode::kGcnDisplay1D;
case 10u: return TextureTileMode::kGcnDisplay2D;
case 14u: return TextureTileMode::kGcnThin2D;
default: throw std::runtime_error("AGC graphics: PS4 texture tile mode index " + std::to_string(index) + " (3D or thick) is not implemented");
}
}
TextureDimension resolveDimension(std::uint32_t raw) {
switch (raw) {
case 8: return TextureDimension::k1D;
@@ -101,8 +125,14 @@ bool IsNullTextureResource(std::span<const std::uint32_t> words) {
return base40 == 0 || type <= 1;
}
GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words) {
Require(words.size() == 8, "guest texture descriptor must contain 8 dwords");
GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> descriptor) {
Require(descriptor.size() == 8, "guest texture descriptor must contain 8 dwords");
const bool gcn = ShaderRecompiler::GcnGuestShaders();
std::array<std::uint32_t, 8> local{};
std::copy(descriptor.begin(), descriptor.end(), local.begin());
const auto gcnTiling = gcn ? local[7] : 0u;
if (gcn) local[6] = local[7] = 0u;
const std::span<const std::uint32_t> words(local);
const auto base40 = (static_cast<std::uint64_t>(words[0]) | (static_cast<std::uint64_t>(words[1]) << 32u)) & 0xffffffffffull;
auto baseAddress = base40 << 8u;
@@ -191,11 +221,11 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
for (const auto selector : {dstSelX, dstSelY, dstSelZ, dstSelW}) Require(identity || selector == 0u || selector == 1u || selector == 4u, "FMASK destination channel selector is not implemented");
}
const auto describe = [&] { return " (format " + std::to_string(format) + ", " + std::to_string(width) + "x" + std::to_string(height) + "x" + std::to_string(depth + 1) + ", type " + std::to_string(typeRaw) + ", tile " + std::to_string(tileModeRaw) + ", mips " + std::to_string(baseLevel) + ".." + std::to_string(lastLevel) + " of " + std::to_string(maxMip + 1) + ")"; };
const auto describe = [&] { return " (format " + std::to_string(format) + ", " + std::to_string(width) + "x" + std::to_string(height) + "x" + std::to_string(depth + 1) + ", type " + std::to_string(typeRaw) + (gcn ? ", PS4 tile index " + std::to_string(ShaderRecompiler::GcnImageTilingIndex(gcnTiling)) : ", tile " + std::to_string(tileModeRaw)) + ", mips " + std::to_string(baseLevel) + ".." + std::to_string(lastLevel) + " of " + std::to_string(maxMip + 1) + ")"; };
TextureTileMode tileMode;
TextureDimension dimension;
try {
tileMode = resolveTileMode(tileModeRaw);
tileMode = gcn ? resolveGcnTileMode(ShaderRecompiler::GcnImageTilingIndex(gcnTiling)) : resolveTileMode(tileModeRaw);
dimension = resolveDimension(typeRaw);
} catch (const std::exception& error) {
throw std::runtime_error(std::string(error.what()) + describe());
@@ -278,6 +308,12 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
result.iterate256 = iterate256;
result.minLod = minLod;
result.samples = samples;
if (gcn) {
result.gcnPitch = ShaderRecompiler::GcnImagePitch(gcnTiling);
result.gcnPow2Pad = ShaderRecompiler::GcnImagePow2Pad(gcnTiling);
result.gcnLinearGeneral = ShaderRecompiler::GcnImageTilingIndex(gcnTiling) == 31u;
Require(result.gcnPitch >= width, "PS4 texture descriptor has a pitch below its width" + describe());
}
if (baseLevel > maxMip) {
const bool fits = LevelsFitAllocation(result, lastLevel + 1u);
if (!fits && ZeroTail(words)) {
@@ -675,6 +675,13 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
const bool alternateAlpha = SplitsAlternateAlpha(state);
std::vector<VkPipelineColorBlendAttachmentState> firstChannels(state.blends.begin(), state.blends.end());
std::vector<VkPipelineColorBlendAttachmentState> secondChannels(state.blends.begin(), state.blends.end());
for (const auto& merged : state.colors) {
if (merged.partialWriteMask != UnclampedBlendMask) continue;
for (auto* channels : {&firstChannels, &secondChannels}) {
channels->at(merged.exportIndex).blendEnable = VK_FALSE;
channels->at(merged.exportIndex).colorWriteMask = 0;
}
}
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");
@@ -38,8 +38,8 @@ static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update
static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec");
static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec");
#if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__)
static_assert(sizeof(State) == 912, "State changed: update CanonicalPipelineState and the recipe codec");
static_assert(sizeof(ShaderStages) == 160, "ShaderStages changed: update the recipe codec");
static_assert(sizeof(State) == 920, "State changed: update CanonicalPipelineState and the recipe codec");
static_assert(sizeof(ShaderStages) == 164, "ShaderStages changed: update the recipe codec");
static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 40, "MeshConfiguration changed: update the recipe codec");
static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec");
#endif
@@ -10,6 +10,7 @@
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libc/include/General.hpp"
#include "RdnaDecoder/RdnaInstructionDecoder.hpp"
#include <algorithm>
#include <bit>
#include <bitset>
@@ -449,8 +450,9 @@ DepthTarget decodeDepthPlane(const Registers& cx) {
};
DepthTarget depth{};
depth.samples = 1u << ((read(cx, 0x010) >> 2u) & 3u);
depth.depthSwizzle = (read(cx, 0x010) >> 4u) & 31u;
depth.iterate256 = (read(cx, 0x010) & (1u << 20u)) != 0;
const bool gcn = ShaderRecompiler::GcnGuestShaders();
depth.depthSwizzle = gcn ? 0u : (read(cx, 0x010) >> 4u) & 31u;
depth.iterate256 = !gcn && (read(cx, 0x010) & (1u << 20u)) != 0;
depth.firstSlice = view & 0x1fffu;
depth.lastSlice = (view >> 13u) & 0x7ffu;
depth.address = zFormat != 0 ? base(0x012, 0x01a) : 0;
@@ -464,8 +466,14 @@ DepthTarget decodeDepthPlane(const Registers& cx) {
depth.htileVrsEncoding = (surface->second >> 19u) & 3u;
}
}
const auto size = read(cx, 0x007);
depth.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u};
if (gcn) {
const auto size = read(cx, 0x016);
depth.extent = {((size & 0x7ffu) + 1u) * 8u, (((size >> 11u) & 0x7ffu) + 1u) * 8u};
} else {
const auto size = read(cx, 0x007);
depth.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u};
}
Require(!gcn || (maxMip == 0 && viewMip == 0), "PS4 depth targets with mip levels are not implemented");
if (maxMip != 0) {
char message[320];
std::snprintf(message, sizeof(message), "depth view mip %u of %u at %ux%u (DB_Z_INFO 0x%08x, DB_DEPTH_VIEW 0x%08x)", viewMip, maxMip + 1u, depth.extent.width, depth.extent.height, read(cx, 0x010), view);
@@ -1290,7 +1298,16 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) {
const auto provokingVertex = raster != queue.context.end() && (raster->second & (1u << 19u)) != 0 ? outputSize - 1u : 0u;
return ShaderRecompiler::MeshConfiguration{inputPrimitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, maxPrimitives, ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, provokingVertex, esgsItemSize, outputSize};
};
if (path == ShaderPath::Vertex) {
if (path == ShaderPath::Vertex && ShaderRecompiler::GcnGuestShaders() && (value & 0x2000u) == 0) {
validate(value == 0, "GCN vertex routing other than a plain VS is unsupported");
const auto unitStepRate = [&](std::uint32_t offset) {
const auto rate = find(queue.context, offset);
return rate == queue.context.end() || rate->second == 1u;
};
const auto components = (read(queue.shader, 0x4a, RegisterBank::Shader) >> 24u) & 3u;
validate(components == 0 || (unitStepRate(0x2a8) && (components == 1 || unitStepRate(0x2a9))), "GCN vertex instance step rates other than 1 are not implemented");
result.legacyVertex = true;
} else if (path == ShaderPath::Vertex) {
validate((value & 0x2000u) != 0, "legacy vertex routing without PRIMGEN_EN is unsupported");
validate((value & ~0x0247a010u) == 0 || value == 0x02002008u, "unsupported vertex routing, scheduling or wave-ID state");
} else if (path == ShaderPath::Tessellation || path == ShaderPath::TessellationGeometry) {
@@ -1454,6 +1471,20 @@ void ApplyDepthRemap(const Context& context, State& state) {
state.depthClamp = true;
}
static bool SignedNormalizedFormat(VkFormat format) {
return format == VK_FORMAT_R8_SNORM || format == VK_FORMAT_R16_SNORM || format == VK_FORMAT_R8G8_SNORM || format == VK_FORMAT_R16G16_SNORM || format == VK_FORMAT_R8G8B8A8_SNORM || format == VK_FORMAT_B8G8R8A8_SNORM || format == VK_FORMAT_R16G16B16A16_SNORM;
}
static std::uint32_t NormalizedChannelMask(VkFormat format) {
switch (format) {
case VK_FORMAT_R8_SNORM:
case VK_FORMAT_R16_SNORM: return 0x1u;
case VK_FORMAT_R8G8_SNORM:
case VK_FORMAT_R16G16_SNORM: return 0x3u;
default: return 0xfu;
}
}
State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
if (auto reason = DepthMaintenanceRejection(queue); !reason.empty()) throw std::runtime_error(reason);
const auto& cx = queue.context;
@@ -1809,7 +1840,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
Require(nibbles || (exportedMask & packedMask) == packedMask, "partial writes of " + packed + " color target are unsupported");
if ((exportedMask & packedMask) != packedMask) {
Require(color.mipCount == 1u && color.depth == 1u && color.layers == 1u && color.arrayLastSlice == 0u, "partial writes of a mipmapped, volume or array " + std::string(PackedColorTargetName(color.packing)) + " color target are not implemented");
Require(std::ranges::none_of(result.colors, [](const ColorTarget& other) { return other.partialWriteMask != 0; }), "partial writes of more than one packed color target are not implemented");
Require(std::ranges::none_of(result.colors, [](const ColorTarget& other) { return other.partialWriteMask != 0; }), "a draw merges into one color target at most, so partial writes of a packed color target cannot join another merged color target");
color.partialWriteMask = PackedWriteMask(mapping, exportedMask);
state.colorWriteMask = 0;
if (color.samples > 1u) {
@@ -1836,7 +1867,13 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
if (const auto info = read(cx, 0x31c + slot * 0xfu); state.blendEnable && (info & 0x8000u) == 0 && ((info >> 8u) & 7u) <= 1u) {
char detail[80];
std::snprintf(detail, sizeof(detail), " (CB_COLOR_INFO 0x%08x, CB_BLEND%u_CONTROL 0x%08x)", info, slot, blend);
throw std::runtime_error(std::string("AGC graphics: blending unclamped normalized color is not implemented") + detail);
const std::string where = detail;
Require(color.samples == 1u && color.packing == ShaderRecompiler::ColorExportPacking::None, "blending unclamped normalized color into a multisampled or packed color target is not implemented" + where);
Require(!reversed && mapping != 0xecu, "blending unclamped normalized color into a color target with a reversed or alternate component order is not implemented" + where);
Require(!(secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor)), "dual-source blending of unclamped normalized color is not implemented" + where);
Require(std::ranges::none_of(result.colors, [&](const ColorTarget& other) { return &other != &color && other.partialWriteMask != 0; }), "a draw merges into one color target at most, so blending unclamped normalized color cannot join another merged color target" + where);
Require(!SignedNormalizedFormat(color.format) || (state.colorWriteMask & NormalizedChannelMask(color.format)) == NormalizedChannelMask(color.format), "blending unclamped signed normalized color with a write mask that skips a component is not implemented, since the shader rewrites the skipped component through a float and the code -128 would become -127" + where);
color.partialWriteMask = UnclampedBlendMask;
}
result.blends[color.exportIndex] = state;
}
@@ -1906,10 +1943,11 @@ bool DepthStencilTests(const State& state) {
}
ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) {
static_assert(VK_BLEND_FACTOR_ZERO == 0 && VK_BLEND_FACTOR_SRC_ALPHA_SATURATE == 14 && VK_BLEND_OP_ADD == 0 && VK_BLEND_OP_MAX == 4);
ShaderRecompiler::TargetMerge merge;
for (const auto& color : state.colors) {
if (color.partialWriteMask == 0) continue;
Require(merge.mask == 0 && color.exportIndex < 8u && color.samples <= 8u, "a draw merges into one packed color target at most");
Require(merge.mask == 0 && color.exportIndex < 8u && color.samples <= 8u, "a draw merges into one color target at most");
Require(color.samples == 1u || state.perSampleShading, "a merged multisampled color target needs per-sample shading");
merge.target = static_cast<std::uint8_t>(color.exportIndex);
merge.mask = color.partialWriteMask;
@@ -1917,6 +1955,19 @@ ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) {
merge.descriptor = EncodeTextureResource(TargetMergeSurface(color));
merge.depthStencilWrites = DepthStencilWrites(state);
merge.depthStencilTests = DepthStencilTests(state);
if (color.partialWriteMask != UnclampedBlendMask) continue;
const auto& attachment = state.blends.at(color.exportIndex);
auto& blend = merge.blend;
blend.enabled = true;
blend.snorm = SignedNormalizedFormat(color.format);
blend.colorSource = static_cast<std::uint8_t>(attachment.srcColorBlendFactor);
blend.colorDestination = static_cast<std::uint8_t>(attachment.dstColorBlendFactor);
blend.colorOperation = static_cast<std::uint8_t>(attachment.colorBlendOp);
blend.alphaSource = static_cast<std::uint8_t>(attachment.srcAlphaBlendFactor);
blend.alphaDestination = static_cast<std::uint8_t>(attachment.dstAlphaBlendFactor);
blend.alphaOperation = static_cast<std::uint8_t>(attachment.alphaBlendOp);
blend.writeMask = static_cast<std::uint8_t>(attachment.colorWriteMask & 0xfu);
for (std::size_t i = 0; i < 4; ++i) blend.constants[i] = std::bit_cast<std::uint32_t>(state.blendConstants[i]);
}
return merge;
}
@@ -2028,6 +2079,7 @@ GuestTextureResource SurfaceForTarget(const ColorTarget& color) {
surface.dccAddress = color.dccAddress != 0 ? color.dccAddress : color.chainDccAddress;
surface.dccAlphaOnMsb = color.dccAlphaOnMsb;
surface.dccPipeAligned = color.dccPipeAligned;
if (ColorTileModeIsGcn(color.tileMode)) surface.gcnPitch = surface.width;
return surface;
}
DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, std::uint32_t swap) {
@@ -2037,7 +2089,54 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number,
}
ColorTarget DecodeGcnColorBuffer(const Registers& cx, std::uint32_t slot) {
const auto stride = slot * 0xfu;
ColorTarget color{};
color.slot = slot;
const auto info = read(cx, 0x31c + stride);
const auto number = (info >> 8u) & 7u;
const auto swap = (info >> 11u) & 3u;
const auto decoded = DecodeColorFormat((info >> 2u) & 0x1fu, number, swap);
const auto attrib = read(cx, 0x31d + stride);
color.samples = 1u << ((attrib >> 12u) & 7u);
Require(color.samples == 1u, "multisampled GCN color targets are not implemented");
Require((info & 0x3u) == 0 && (info & 0x80u) == 0, "endian-swapped or linear-general GCN color targets are not implemented");
Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported");
const auto view = read(cx, 0x31b + stride);
Require((view & 0x7ffu) == 0 && ((view >> 13u) & 0x7ffu) == 0, "GCN color array slices are not implemented");
const auto pitch = ((read(cx, 0x319 + stride) & 0x7ffu) + 1u) * 8u;
const auto sliceElements = ((read(cx, 0x31a + stride) & 0x3fffffu) + 1u) * 64u;
color.extent = {pitch, sliceElements / pitch};
color.surfaceExtent = color.extent;
color.elementBytes = decoded.elementBytes;
switch (attrib & 0x1fu) {
case 8u: color.tileMode = ColorTileMode::Linear; break;
case 5u: case 13u: color.tileMode = ColorTileMode::GcnThin1D; break;
case 9u: color.tileMode = ColorTileMode::GcnDisplay1D; break;
case 10u: color.tileMode = ColorTileMode::GcnDisplay2D; break;
case 14u: color.tileMode = ColorTileMode::GcnThin2D; break;
default: throw std::runtime_error("AGC graphics: PS4 color target tile mode index " + std::to_string(attrib & 0x1fu) + " is not implemented");
}
color.mipCount = 1;
color.surfaceAddress = static_cast<std::uint64_t>(read(cx, 0x318 + stride)) << 8u;
color.address = color.surfaceAddress;
const ColorTargetLayout layout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes);
Require(layout.Bytes() == static_cast<std::size_t>(pitch) * color.extent.height * color.elementBytes, "PS4 color target pitch or slice is not padded the way its tile mode pads them");
color.bytes = layout.Bytes();
color.format = decoded.format;
color.componentMapping = decoded.componentMapping;
color.packing = decoded.packing;
for (std::uint32_t word = 0; word < 2; ++word) {
const auto clear = find(cx, 0x323 + word + stride);
color.clearWords[word] = clear == cx.end() ? 0u : clear->second;
}
Require(((info >> 13u) & 1u) == 0, "PS4 color target fast clear (CMASK) is not implemented");
GuestMemory::CheckRange(reinterpret_cast<const void*>(color.address), color.bytes, layout.Alignment(), true);
return color;
}
ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allowMultisampleDcc) {
if (ShaderRecompiler::GcnGuestShaders()) return DecodeGcnColorBuffer(cx, slot);
const auto stride = slot * 0xfu;
ColorTarget color{};
color.slot = slot;
@@ -453,12 +453,11 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) {
const auto guestLayerOffset = geometry.GuestLayerOffset(layer);
const auto linearLayerOffset = geometry.LinearLayerOffset(layer);
for (std::uint32_t level = 0; level < mips.size(); ++level) {
if (!geometry.HasLayer(level, layer)) continue;
const auto& mip = mips[level];
detiler.Dispatch(commands, descriptor.tileMode, elementBytes, tiled->Handle(), guestLayerOffset + mip.tiledOffset, linear->Handle(), linearLayerOffset + mip.linearOffset, mip, false, layer, geometry.thick, {.pipeBankXor = descriptor.pipeBankXor});
detiler.Dispatch(commands, descriptor.tileMode, elementBytes, tiled->Handle(), geometry.GuestOffset(layer, mip), linear->Handle(), linearLayerOffset + mip.linearOffset, mip, false, layer, geometry.thick, {.pipeBankXor = descriptor.pipeBankXor});
}
}
if (IsRepackedTextureFormat(descriptor.format)) {
@@ -615,6 +614,12 @@ bool Texture::CanDepthCopyFrom(const StorageTexture& source, const GuestTextureR
bool Texture::CanBlockCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor) {
const auto& from = source.Descriptor();
if (descriptor.gcnPitch != 0 && from.gcnPitch == descriptor.gcnPitch && (descriptor.width != from.width || descriptor.height != from.height)) {
return !IsBlockCompressed(descriptor.format) && !IsBlockCompressed(from.format) && !IsSrgbTextureFormat(descriptor.format) && BytesPerElement(from.format) == BytesPerElement(descriptor.format)
&& descriptor.baseAddress == from.baseAddress && descriptor.tileMode == from.tileMode && descriptor.dimension == TextureDimension::k2D && from.dimension == TextureDimension::k2D
&& descriptor.mipCount == 1u && from.mipCount == 1u && descriptor.width <= from.width && descriptor.height <= from.height
&& source.ImageDepth() == 1 && DescribeSurface(descriptor).guestBytes == source.GuestBytes();
}
if (!IsBlockCompressed(descriptor.format) || IsBlockCompressed(from.format) || BytesPerElement(from.format) != BytesPerElement(descriptor.format)) return false;
if (descriptor.dimension != TextureDimension::k2D && descriptor.dimension != TextureDimension::k2DArray) return false;
if (descriptor.baseAddress != from.baseAddress || descriptor.pipeBankXor != from.pipeBankXor || descriptor.dimension != from.dimension || descriptor.tileMode != from.tileMode || descriptor.depthOrLastArray != from.depthOrLastArray || descriptor.mipCount > from.mipCount) return false;
@@ -701,6 +706,7 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
Require(source != nullptr && CanBlockCopyFrom(*source, descriptor), "storage image cannot serve a block copy of the sampled texture");
blockFormat = SampledTextureFormat(context, descriptor.format);
const auto geometry = DescribeSurface(descriptor);
copyTexelsPerElement = IsBlockCompressed(descriptor.format) ? 4u : 1u;
copyWidth = descriptor.width;
copyHeight = descriptor.height;
copyLevels = descriptor.mipCount;
@@ -999,7 +1005,7 @@ bool Texture::refreshBlockCopy() {
VkImageCopy region{};
region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, copyLayers};
region.dstSubresource = region.srcSubresource;
region.extent = {std::max(copyWidth >> level, 1u) / 4u, std::max(copyHeight >> level, 1u) / 4u, 1u};
region.extent = {std::max(copyWidth >> level, 1u) / copyTexelsPerElement, std::max(copyHeight >> level, 1u) / copyTexelsPerElement, 1u};
regions.push_back(region);
}
auto* recorder = Recorder::Active();
@@ -1442,6 +1448,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler,
descriptor.depthOrLastArray, descriptor.baseArray, static_cast<int>(descriptor.dimension), static_cast<int>(descriptor.tileMode), descriptor.format, static_cast<int>(vkFormat));
#endif
geometry = DescribeSurface(descriptor);
Require(!geometry.levelMajor, "a storage image of a PS4 layered surface with several mip levels (stored level by level) is not implemented");
mips = geometry.mips;
arrayLayers = geometry.layers;
const auto elementBytes = BytesPerElement(descriptor.format);
@@ -1452,7 +1459,8 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler,
const bool layered = geometry.layers >= 2 && !geometry.thick && geometry.imageDepth == 1 && geometry.layers == geometry.imageLayers && geometry.layerBytes * geometry.layers == guestBytes && geometry.layerBytes % 65536 == 0 && descriptor.baseAddress % 65536 == 0;
// Tile blocks divide the stamp blocks, so at an aligned base every unit is whole tile blocks
// of its (layer, mip) slices (sliceWindows).
blockUnits = LayerTrackingEnabled() && BlockTrackingEnabled() && !geometry.thick && descriptor.tileMode != TextureTileMode::kLinear && descriptor.baseAddress % 65536 == 0;
blockUnits = LayerTrackingEnabled() && BlockTrackingEnabled() && !geometry.thick && descriptor.tileMode != TextureTileMode::kLinear && descriptor.baseAddress % 65536 == 0
&& std::none_of(mips.begin(), mips.end(), [](const TileMipLayout& mip) { return mip.gcnSplitStride != 0; });
trackedLayers = blockUnits ? static_cast<std::uint32_t>((guestBytes + 65535) / 65536) : layered && LayerTrackingEnabled() ? geometry.layers : 1u;
trackedLayerBytes = blockUnits ? 65536 : guestBytes / trackedLayers;
layerGeneration.assign(trackedLayers, 0);
@@ -2977,12 +2985,12 @@ std::vector<std::pair<std::uint64_t, std::uint64_t>> StorageTexture::unitRuns(co
std::vector<StorageTexture::SliceWindow> StorageTexture::sliceWindows(std::span<const std::pair<std::uint64_t, std::uint64_t>> runs) const {
std::vector<SliceWindow> windows;
const auto elementBytes = BytesPerElement(descriptor.format);
const auto block = ThinBlockLayout(descriptor.tileMode, elementBytes);
for (const auto& [runBegin, runEnd] : runs) {
for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) {
for (std::uint32_t level = 0; level < descriptor.mipCount; ++level) {
if (!geometry.HasLayer(level, layer)) continue;
const auto& mip = mips[level];
const auto block = ThinBlockLayout(MipTileMode(descriptor.tileMode, mip), elementBytes);
const auto sliceBegin = geometry.GuestLayerOffset(layer) + mip.tiledOffset;
const auto sliceEnd = sliceBegin + mip.tiledSize;
if (runEnd <= sliceBegin || sliceEnd <= runBegin) continue;
@@ -37,6 +37,7 @@ struct Push {
std::uint32_t columnBegin;
std::uint32_t rowBegin;
std::uint32_t pipeBankXor;
std::uint32_t splitStride;
};
std::uint32_t BlockBytesFor(TextureTileMode tileMode) {
@@ -148,8 +149,18 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem
}
// Constants 0-3 select element size, block size, addressing family and direction; 4-19 carry the
// per-bit XOR equation for the equation family (2); 20-21 override the block extent in elements.
std::array<std::uint32_t, 22> values{elementBytes, BlockBytesFor(tileMode), tileMode == TextureTileMode::kLinear ? 0u : 1u, retile ? 1u : 0u};
if (thick) {
const bool gcn = IsGcnTiled(tileMode);
std::array<std::uint32_t, 26> values{elementBytes, gcn ? 0u : BlockBytesFor(tileMode), tileMode == TextureTileMode::kLinear ? 0u : 1u, retile ? 1u : 0u};
if (gcn) {
Require(!thick, "PS4 tiled surfaces are detiled thin");
const auto geometry = GcnTiling(tileMode, elementBytes);
values[1] = geometry.blockBytes;
values[2] = 2u;
std::copy(geometry.equation.begin(), geometry.equation.end(), values.begin() + 4);
values[20] = geometry.blockWidth;
values[21] = geometry.blockHeight;
std::copy(geometry.splitMasks.begin(), geometry.splitMasks.end(), values.begin() + 22);
} else if (thick) {
const auto thickMode = tileMode == TextureTileMode::kStandard4KB ? 0x105u : tileMode == TextureTileMode::kS4KBX ? 0x115u : tileMode == TextureTileMode::kStandard64KB ? 0x109u : tileMode == TextureTileMode::kS64KBX ? 0x119u : tileMode == TextureTileMode::kD64KBX ? 0x11au : 0u;
Require(thickMode != 0, "3D textures are only detiled thick from SW_4KB_S, SW_4KB_S_X, SW_64KB_S, SW_64KB_S_X or SW_64KB_D_X");
const auto* equation = FindTextureSwizzleEquation(thickMode, elementBytes);
@@ -168,7 +179,7 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem
if (image && imageModule == VK_NULL_HANDLE) {
imageModule = CreateShaderModule(context, TEXTURE_DETILE_IMAGE_SPV, sizeof(TEXTURE_DETILE_IMAGE_SPV), "vkCreateShaderModule");
}
std::array<VkSpecializationMapEntry, 22> entries{};
std::array<VkSpecializationMapEntry, 26> entries{};
for (std::uint32_t index = 0; index < entries.size(); ++index) entries[index] = {index, index * 4u, 4};
VkSpecializationInfo specialization{};
specialization.mapEntryCount = static_cast<std::uint32_t>(entries.size());
@@ -206,7 +217,7 @@ void TextureDetiler::Dispatch(VkCommandBuffer commands, TextureTileMode tileMode
Require(source != VK_NULL_HANDLE && destination != VK_NULL_HANDLE, "texture detiling requires source and destination buffers");
std::uint32_t columnEnd = 0, rowEnd = 0;
checkWindow(layout, window, columnEnd, rowEnd);
const auto target = pipeline(tileMode, elementBytes, retile, thick);
const auto target = pipeline(MipTileMode(tileMode, layout), elementBytes, retile, thick);
const auto alignment = std::max<VkDeviceSize>(context.limits.minStorageBufferOffsetAlignment, 4);
const auto sourceDescriptorOffset = sourceOffset - sourceOffset % alignment;
const auto destinationDescriptorOffset = destinationOffset - destinationOffset % alignment;
@@ -258,6 +269,7 @@ void TextureDetiler::Dispatch(VkCommandBuffer commands, TextureTileMode tileMode
push.columnBegin = window.columnBegin;
push.rowBegin = window.rowBegin;
push.pipeBankXor = window.pipeBankXor;
push.splitStride = layout.gcnSplitStride;
context.Function<PFN_vkCmdPushConstants>("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(Push), &push);
const auto groupsX = (columnEnd - window.columnBegin + 7u) / 8u;
const auto groupsY = (rowEnd - window.rowBegin + 7u) / 8u;
@@ -402,7 +414,7 @@ void TextureDetiler::DispatchImage(VkCommandBuffer commands, TextureTileMode til
Require(ImageElementFormat(elementBytes) != VK_FORMAT_UNDEFINED, "unsupported element size for texture detiling through an image");
std::uint32_t columnEnd = 0, rowEnd = 0;
checkWindow(layout, window, columnEnd, rowEnd);
const auto target = pipeline(tileMode, elementBytes, retile, false, true);
const auto target = pipeline(MipTileMode(tileMode, layout), elementBytes, retile, false, true);
const auto alignment = std::max<VkDeviceSize>(context.limits.minStorageBufferOffsetAlignment, 4);
const auto descriptorOffset = tiledOffset - tiledOffset % alignment;
const auto base = tiledOffset - descriptorOffset;
@@ -448,6 +460,7 @@ void TextureDetiler::DispatchImage(VkCommandBuffer commands, TextureTileMode til
push.columnBegin = window.columnBegin;
push.rowBegin = window.rowBegin;
push.pipeBankXor = window.pipeBankXor;
push.splitStride = layout.gcnSplitStride;
context.Function<PFN_vkCmdPushConstants>("vkCmdPushConstants")(commands, imagePipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(Push), &push);
const auto groupsX = (columnEnd - window.columnBegin + 7u) / 8u;
const auto groupsY = (rowEnd - window.rowBegin + 7u) / 8u;
@@ -59,6 +59,24 @@ BlockLayout GetBlockLayout(TextureTileMode tileMode, std::uint32_t bytesPerEleme
case TextureTileMode::kS64KBX:
case TextureTileMode::kD64KBX:
case TextureTileMode::kR64KBX: return {65536u, 1u << kLog2BlockThin64KB[index].width, 1u << kLog2BlockThin64KB[index].height};
case TextureTileMode::kGcnThin1D:
case TextureTileMode::kGcnDisplay1D:
case TextureTileMode::kGcnDisplay2D:
case TextureTileMode::kGcnThin2D:
case TextureTileMode::kGcnDepth2D64:
case TextureTileMode::kGcnDepth2D128:
case TextureTileMode::kGcnDepth2D256:
case TextureTileMode::kGcnDepth2D512:
case TextureTileMode::kGcnDepth2D1K:
case TextureTileMode::kGcnDepthPrt256:
case TextureTileMode::kGcnDepthPrt1K:
case TextureTileMode::kGcnDisplayPrt:
case TextureTileMode::kGcnDisplay2DPrt:
case TextureTileMode::kGcnThinPrt:
case TextureTileMode::kGcnThin2DPrt: {
const auto geometry = GcnTiling(tileMode, bytesPerElement);
return {geometry.blockBytes, geometry.blockWidth, geometry.blockHeight};
}
}
throw std::runtime_error("AGC graphics: GetBlockLayout encountered an unknown tile mode");
}
@@ -101,7 +119,22 @@ bool GetMipTailLayout(TextureTileMode tileMode, const BlockLayout& block, std::u
switch (tileMode) {
case TextureTileMode::kLinear:
case TextureTileMode::kStandard256B:
case TextureTileMode::kD256B: return false;
case TextureTileMode::kD256B:
case TextureTileMode::kGcnThin1D:
case TextureTileMode::kGcnDisplay1D:
case TextureTileMode::kGcnDisplay2D:
case TextureTileMode::kGcnThin2D:
case TextureTileMode::kGcnDepth2D64:
case TextureTileMode::kGcnDepth2D128:
case TextureTileMode::kGcnDepth2D256:
case TextureTileMode::kGcnDepth2D512:
case TextureTileMode::kGcnDepth2D1K:
case TextureTileMode::kGcnDepthPrt256:
case TextureTileMode::kGcnDepthPrt1K:
case TextureTileMode::kGcnDisplayPrt:
case TextureTileMode::kGcnDisplay2DPrt:
case TextureTileMode::kGcnThinPrt:
case TextureTileMode::kGcnThin2DPrt: return false;
case TextureTileMode::kStandard4KB:
case TextureTileMode::kD4KB:
case TextureTileMode::kS4KBX:
@@ -248,6 +281,7 @@ std::vector<TileMipLayout> ComputeMipLayout(TextureTileMode tileMode, std::uint3
Require(width != 0 && height != 0, "cannot compute mip layout for a zero-sized texture");
Require(mipCount != 0 && mipCount <= 16u, "texture mip count is out of range");
Require(!IsGcnTiled(tileMode), "PS4 surfaces are laid out by ComputeGcnMipLayout from their pitch");
const auto bytesPerElement = BytesPerElement(format);
const auto texelWidth = BlockWidth(format);
const auto texelHeight = BlockHeight(format);
@@ -279,7 +313,7 @@ std::array<std::uint32_t, 3> ThinBlockLayout(TextureTileMode tileMode, std::uint
std::vector<std::pair<std::uint64_t, std::uint64_t>> CoveredMipBytes(TextureTileMode tileMode, std::uint32_t bytesPerElement, const TileMipLayout& mip) {
std::vector<std::pair<std::uint64_t, std::uint64_t>> covered;
if (mip.tail || mip.width == 0 || mip.height == 0) return covered;
if (mip.tail || mip.width == 0 || mip.height == 0 || mip.gcnSplitStride != 0) return covered;
const auto add = [&](std::uint64_t begin, std::uint64_t end) {
end = std::min(end, mip.tiledSize);
if (begin >= end) return;
@@ -291,7 +325,7 @@ std::vector<std::pair<std::uint64_t, std::uint64_t>> CoveredMipBytes(TextureTile
for (std::uint32_t row = 0; row < mip.height; ++row) add(static_cast<std::uint64_t>(row) * mip.pitchBytes, static_cast<std::uint64_t>(row) * mip.pitchBytes + rowBytes);
return covered;
}
const auto block = ThinBlockLayout(tileMode, bytesPerElement);
const auto block = ThinBlockLayout(MipTileMode(tileMode, mip), bytesPerElement);
const auto wholeColumns = std::min(mip.width / block[1], mip.blocksPerRow);
const auto wholeRows = mip.height / block[2];
for (std::uint32_t row = 0; row < wholeRows && wholeColumns != 0; ++row) {
@@ -382,7 +416,170 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s
return result;
}
std::vector<TileMipLayout> ComputeGcnMipLayout(TextureTileMode tileMode, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t pitch, bool pow2Pad, std::uint32_t mipCount, bool linearGeneral, std::uint32_t layers) {
Require(layers != 0, "a PS4 surface has at least one slice");
Require(!linearGeneral || tileMode == TextureTileMode::kLinear, "LINEAR_GENERAL is a linear layout");
Require(width != 0 && height != 0 && pitch >= width, "cannot compute the mip layout of an empty PS4 texture");
Require(mipCount != 0 && mipCount <= 16u, "texture mip count is out of range");
Require(tileMode == TextureTileMode::kLinear || IsGcnTiled(tileMode), "PS4 texture tile mode has no modeled layout");
const auto bytes = BytesPerElement(format);
const auto texelWidth = BlockWidth(format);
const auto texelHeight = BlockHeight(format);
std::vector<TileMipLayout> mips(mipCount);
std::uint64_t offset = 0, linearOffset = 0;
for (std::uint32_t level = 0; level < mipCount; ++level) {
auto levelWidth = std::max(pitch >> level, 1u);
auto levelHeight = std::max(height >> level, 1u);
if (pow2Pad && level != 0) {
levelWidth = std::bit_ceil(levelWidth);
levelHeight = std::bit_ceil(levelHeight);
}
auto rowElements = (levelWidth + texelWidth - 1u) / texelWidth;
auto rows = (levelHeight + texelHeight - 1u) / texelHeight;
bool macro = false;
std::uint32_t macroWidth = 0;
std::uint32_t splitSlices = 1;
if (IsGcnMacroTiled(tileMode)) {
const auto geometry = GcnTiling(tileMode, bytes);
macro = level == 0 || (rowElements >= geometry.blockWidth && rows >= geometry.blockHeight);
if (macro) {
macroWidth = geometry.blockWidth;
splitSlices = geometry.splitSlices;
rowElements = AlignUp(rowElements, geometry.blockWidth);
rows = AlignUp(rows, geometry.blockHeight);
}
}
const bool micro = !macro && tileMode != TextureTileMode::kLinear;
if (micro) {
rowElements = AlignUp(rowElements, 8u);
rows = AlignUp(rows, 8u);
while (static_cast<std::uint64_t>(rowElements) * rows * bytes % 256u != 0) rowElements += 8u;
} else if (!macro && !linearGeneral) {
const auto pitchAlign = std::max(8u, 64u / bytes);
const auto sliceAlign = std::max(64u, 256u / bytes);
rowElements = AlignUp(rowElements, pitchAlign);
while (static_cast<std::uint64_t>(rowElements) * rows % sliceAlign != 0) rowElements += pitchAlign;
}
if (level == 0) Require(rowElements == (pitch + texelWidth - 1u) / texelWidth, "PS4 texture pitch " + std::to_string(pitch) + " is not padded the way its tile mode pads a row (" + std::to_string(rowElements * texelWidth) + ")");
auto& mip = mips[level];
mip.width = TexelLevelDimension(width, level, texelWidth);
mip.height = TexelLevelDimension(height, level, texelHeight);
mip.blocksPerRow = macro ? rowElements / macroWidth : micro ? rowElements / 8u : rowElements;
mip.gcnMicroTiled = micro && IsGcnMacroTiled(tileMode);
mip.gcnSplitStride = splitSlices > 1u ? static_cast<std::uint32_t>(static_cast<std::uint64_t>(rowElements) * rows * bytes / splitSlices) : 0u;
mip.pitchBytes = rowElements * bytes;
mip.tiledSize = static_cast<std::uint64_t>(rowElements) * rows * bytes;
mip.linearSize = static_cast<std::uint64_t>(mip.pitchBytes) * mip.height;
mip.tiledOffset = offset;
mip.linearOffset = linearOffset;
mip.tail = false;
mip.tailX = 0;
mip.tailY = 0;
offset += mip.tiledSize * layers;
linearOffset += mip.tiledSize;
}
return mips;
}
GcnTileGeometry GcnTiling(TextureTileMode tileMode, std::uint32_t bytesPerElement) {
Require(IsGcnTiled(tileMode), "PS4 tile geometry needs a PS4 tiled mode");
Require(std::has_single_bit(bytesPerElement) && bytesPerElement <= 16u, "unsupported bytes per element for PS4 tiling");
const auto x = [](std::uint32_t bit) { return 1u << bit; };
const auto y = [](std::uint32_t bit) { return 1u << (12u + bit); };
const std::array<std::uint32_t, 6> thin{x(0), y(0), x(1), y(1), x(2), y(2)};
const std::array<std::array<std::uint32_t, 6>, 5> display{{
{x(0), x(1), x(2), y(1), y(0), y(2)}, {x(0), x(1), x(2), y(0), y(1), y(2)}, {x(0), x(1), y(0), x(2), y(1), y(2)},
{x(0), y(0), x(1), x(2), y(1), y(2)}, {x(0), y(0), x(1), y(1), x(2), y(2)},
}};
const auto log2Bytes = static_cast<std::uint32_t>(std::countr_zero(bytesPerElement));
const bool displayOrder = tileMode == TextureTileMode::kGcnDisplay1D || tileMode == TextureTileMode::kGcnDisplay2D || tileMode == TextureTileMode::kGcnDisplayPrt || tileMode == TextureTileMode::kGcnDisplay2DPrt;
const auto& order = displayOrder ? display[log2Bytes] : thin;
const auto microBytes = 64u * bytesPerElement;
GcnTileGeometry geometry{};
if (IsGcnMicroTiled(tileMode)) {
geometry = {microBytes, 8u, 8u, {}};
for (std::uint32_t bit = 0; bit < order.size(); ++bit) geometry.equation[log2Bytes + bit] = order[bit];
return geometry;
}
struct MacroMode { std::uint32_t bankHeight; std::uint32_t banks; std::uint32_t aspect; };
constexpr std::array<MacroMode, 5> macroModes{{{4u, 16u, 4u}, {2u, 16u, 2u}, {1u, 16u, 2u}, {1u, 16u, 2u}, {1u, 8u, 1u}}};
constexpr std::array<MacroMode, 5> prtMacroModes{{{8u, 16u, 4u}, {4u, 16u, 4u}, {2u, 16u, 2u}, {1u, 16u, 2u}, {1u, 8u, 1u}}};
std::uint32_t tileSplit = std::min(1024u, std::max(256u, 2u * microBytes));
switch (tileMode) {
case TextureTileMode::kGcnDepth2D64: tileSplit = 64u; break;
case TextureTileMode::kGcnDepth2D128: tileSplit = 128u; break;
case TextureTileMode::kGcnDepth2D256:
case TextureTileMode::kGcnDepthPrt256: tileSplit = 256u; break;
case TextureTileMode::kGcnDepth2D512: tileSplit = 512u; break;
case TextureTileMode::kGcnDepth2D1K:
case TextureTileMode::kGcnDepthPrt1K: tileSplit = 1024u; break;
default: break;
}
const bool prt = IsGcnPrtTiled(tileMode);
const bool splitRotation = tileMode != TextureTileMode::kGcnDisplayPrt && tileMode != TextureTileMode::kGcnThinPrt;
const bool pipe8x16 = tileMode == TextureTileMode::kGcnDisplayPrt || tileMode == TextureTileMode::kGcnThinPrt;
const auto tileBytes = std::min(tileSplit, microBytes);
const auto log2TileBytes = static_cast<std::uint32_t>(std::countr_zero(tileBytes));
const auto& mode = (prt ? prtMacroModes : macroModes)[log2TileBytes - 6u];
constexpr std::uint32_t pipes = 8u, log2Pipes = 3u, pipeInterleaveBits = 8u;
const auto log2BankHeight = static_cast<std::uint32_t>(std::countr_zero(mode.bankHeight));
const auto bankBits = static_cast<std::uint32_t>(std::countr_zero(mode.banks));
geometry.blockWidth = 8u * pipes * mode.aspect;
geometry.blockHeight = 8u * mode.bankHeight * mode.banks / mode.aspect;
const auto macroBytes = tileBytes * mode.bankHeight;
Require(macroBytes % 256u == 0, "PS4 macro tile is not a whole number of pipe interleaves");
geometry.blockBytes = macroBytes * pipes * mode.banks;
const auto elementBits = log2TileBytes - log2Bytes;
geometry.splitSlices = microBytes / tileBytes;
for (auto bit = elementBits; bit < order.size(); ++bit) geometry.splitMasks[bit - elementBits] = order[bit];
std::array<std::uint32_t, 16> inner{};
for (std::uint32_t bit = 0; bit < elementBits; ++bit) inner[log2Bytes + bit] = order[bit];
auto innerBits = log2TileBytes;
for (std::uint32_t bit = 0; bit < log2BankHeight; ++bit) inner[innerBits++] = y(3u + bit);
const auto pipe = pipe8x16 ? std::array<std::uint32_t, 3>{x(4) | y(3) | x(5), x(3) | y(4), x(5) | y(5)} : std::array<std::uint32_t, 3>{x(3) | y(3) | x(4), x(4) | y(4), x(5) | y(5)};
const auto log2Aspect = static_cast<std::uint32_t>(std::countr_zero(mode.aspect));
const auto tx = [&](std::uint32_t bit) { return prt && bit >= log2Aspect ? 0u : x(3u + log2Pipes + bit); };
const auto ty = [&](std::uint32_t bit) { return prt && bit >= bankBits - log2Aspect ? 0u : y(3u + log2BankHeight + bit); };
std::array<std::uint32_t, 4> bank{};
switch (mode.banks) {
case 16u: bank = {tx(0) | ty(3), tx(1) | ty(2) | ty(3), tx(2) | ty(1), tx(3) | ty(0)}; break;
case 8u: bank = {tx(0) | ty(2), tx(1) | ty(1) | ty(2), tx(2) | ty(0), 0u}; break;
default: Require(false, "PS4 macro tile bank count is not modeled");
}
for (std::uint32_t split = 0; splitRotation && split < 4u && geometry.splitMasks[split] != 0; ++split) {
const auto rotation = ((mode.banks / 2u + 1u) << split) & (mode.banks - 1u);
for (std::uint32_t bit = 0; bit < bankBits; ++bit) {
if ((rotation >> bit) & 1u) bank[bit] ^= geometry.splitMasks[split];
}
}
Require(innerBits + log2Pipes + bankBits <= 16u, "PS4 macro tile exceeds 64 KiB");
for (std::uint32_t bit = 0; bit < pipeInterleaveBits; ++bit) geometry.equation[bit] = inner[bit];
for (std::uint32_t bit = 0; bit < log2Pipes; ++bit) geometry.equation[pipeInterleaveBits + bit] = pipe[bit];
for (std::uint32_t bit = 0; bit < bankBits; ++bit) geometry.equation[pipeInterleaveBits + log2Pipes + bit] = bank[bit];
for (auto bit = pipeInterleaveBits; bit < innerBits; ++bit) geometry.equation[bit + log2Pipes + bankBits] = inner[bit];
return geometry;
}
namespace {
SurfaceGeometry describeGcnSurface(const GuestTextureResource& descriptor) {
Require(descriptor.dimension != TextureDimension::k3D && descriptor.dimension != TextureDimension::k2DMsaa, "PS4 3D and multisampled textures are not implemented");
const bool layered = descriptor.dimension == TextureDimension::k2DArray || descriptor.dimension == TextureDimension::k1DArray || descriptor.dimension == TextureDimension::kCube;
SurfaceGeometry geometry;
geometry.layers = layered ? descriptor.depthOrLastArray + 1u : 1u;
geometry.levelMajor = geometry.layers > 1u && descriptor.mipCount > 1u;
geometry.mips = ComputeGcnMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, descriptor.gcnPitch, descriptor.gcnPow2Pad, descriptor.mipCount, descriptor.gcnLinearGeneral, geometry.layers);
geometry.imageLayers = geometry.layers;
geometry.guestBytes = geometry.mips.back().tiledOffset + geometry.mips.back().tiledSize * geometry.layers;
geometry.layerBytes = geometry.guestBytes / geometry.layers;
for (const auto& mip : geometry.mips) geometry.sliceLinearBytes = std::max(geometry.sliceLinearBytes, mip.linearOffset + mip.linearSize);
return geometry;
}
}
SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) {
if (descriptor.gcnPitch != 0) return describeGcnSurface(descriptor);
SurfaceGeometry geometry;
if (descriptor.samples != 1u && descriptor.tileMode != TextureTileMode::kLinear) {
Require(descriptor.samples == 2u || descriptor.samples == 4u || descriptor.samples == 8u, "multisampled surface fragment count is not implemented");
@@ -510,6 +707,7 @@ GuestTextureResource ArrayLayerWindow(const GuestTextureResource& viewed, std::u
std::vector<TileMipLayout> ComputeElementMipLayout(TextureTileMode tileMode, std::uint32_t bytesPerElement, std::uint32_t width, std::uint32_t height, std::uint32_t mipCount) {
Require(width != 0 && height != 0 && mipCount != 0 && mipCount <= 16u, "invalid surface mip chain");
Require(!IsGcnTiled(tileMode), "PS4 surfaces are laid out by ComputeGcnMipLayout from their pitch");
if (tileMode == TextureTileMode::kLinear) return ComputeLinearMipLayout(bytesPerElement, 1u, 1u, width, height, mipCount);
return ComputeTiledMipLayout(tileMode, bytesPerElement, 1u, 1u, width, height, mipCount);
}
@@ -1838,14 +1838,23 @@ void UnclampedNormalizedColorTests() {
const auto pass = AgcDriver::Graphics::DecodeColorMetadataPass(eliminate);
Require(pass.has_value() && pass->mode == AgcDriver::Graphics::ColorMetadataPass::Mode::EliminateFastClear && pass->targets.size() == 1 && pass->targets[0].format == VK_FORMAT_R8G8_UNORM, "a fast-clear eliminate over an unclamped UNORM target was refused");
queue.context[0x1e0] = blending;
expectFailure([&] { DecodeState(queue); }, "blending unclamped normalized color is not implemented (CB_COLOR_INFO 0x0000000c, CB_BLEND0_CONTROL 0x40000504)");
state = DecodeState(queue);
Require(state.colors.at(0).partialWriteMask == AgcDriver::Graphics::UnclampedBlendMask && state.blends.at(0).blendEnable && state.blends.at(0).colorWriteMask != 0, "an 8_8 UNORM target that blends with BLEND_CLAMP clear (CB_COLOR_INFO 0x0000000c, CB_BLEND0_CONTROL 0x40000504) was not routed to the pixel shader");
const auto merge = AgcDriver::Graphics::ExportTargetMerge(state);
Require(merge.blend.enabled && !merge.blend.snorm && merge.mask == AgcDriver::Graphics::UnclampedBlendMask && merge.samples == 1u && merge.blend.colorSource == VK_BLEND_FACTOR_SRC_ALPHA && merge.blend.colorDestination == VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA && merge.blend.colorOperation == VK_BLEND_OP_ADD && merge.blend.writeMask == state.blends.at(0).colorWriteMask, "the unclamped blend of an 8_8 UNORM target was not described to the pixel shader");
queue.context[0x31c] |= 0x10000u;
state = DecodeState(queue);
Require(!state.blends.at(0).blendEnable && state.blends.at(0).colorWriteMask != 0, "an unclamped UNORM target with BLEND_BYPASS was refused or blended");
queue = target(0x0000010cu);
Require(DecodeState(queue).colors.at(0).format == VK_FORMAT_R8G8_SNORM, "an unclamped SNORM target that does not blend was refused");
queue.context[0x1e0] = blending;
expectFailure([&] { DecodeState(queue); }, "blending unclamped normalized color is not implemented");
state = DecodeState(queue);
Require(state.colors.at(0).partialWriteMask == AgcDriver::Graphics::UnclampedBlendMask && AgcDriver::Graphics::ExportTargetMerge(state).blend.snorm, "an unclamped SNORM target that blends was not routed to the pixel shader as signed");
auto unsignedMasked = target(0x0000000cu);
unsignedMasked.context[0x1e0] = blending;
unsignedMasked.context[0x8e] = 0x1u;
state = DecodeState(unsignedMasked);
Require(state.colors.at(0).partialWriteMask == AgcDriver::Graphics::UnclampedBlendMask && state.blends.at(0).colorWriteMask == VK_COLOR_COMPONENT_R_BIT, "an unclamped UNORM target that blends with a write mask that skips a component was not routed to the pixel shader");
queue = target(0x00000604u);
expectFailure([&] { DecodeState(queue); }, "unclamped normalized color is unsupported");
queue = target(0x0000800cu);
@@ -689,7 +689,7 @@ void pageTwoSamplerTest(const Device& device) {
const auto present = [&](std::chrono::milliseconds at, std::uint64_t address, std::uint64_t drops) { overlay.NotePresent(start + at, VkExtent2D{640, 360}, AgcDriver::OverlayPresentInfo{address, VK_PRESENT_MODE_FIFO_KHR, drops}); };
present(0ms, 0x1000, 7);
const auto& sample = overlay.DetailSample();
Require(sample.commit.has_value() && sample.unimplementedTotal.has_value() && sample.presentMode == "fifo" && sample.pipelinesCompiling == std::optional<std::uint64_t>{0}, "page 2's first sample does not read the commit charge, the unimplemented calls, the compile backlog and the present mode");
Require(sample.commit.has_value() == AgcDriver::ReadCommitCharge().has_value() && sample.unimplementedTotal.has_value() && sample.presentMode == "fifo" && sample.pipelinesCompiling == std::optional<std::uint64_t>{0}, "page 2's first sample does not read the commit charge where the system reports one, the unimplemented calls, the compile backlog and the present mode");
Require(!sample.flipsPerSecond && !sample.pageFaultsPerSecond && !sample.cpuCopyMegabytesPerSecond && sample.busy.empty() && sample.phases.empty(), "page 2's first sample shows rates before a second has passed");
Require(sample.hostImports.has_value() == (context.hostImportAlignment != 0), "the host import line does not follow the device's host import support");
std::thread([] {
@@ -737,7 +737,11 @@ void operatingSystemTests() {
require(!AgcDriver::ParseCommitCharge("CommitLimit: 8159204 kB\nCommitted_AS: 12 MB\n"), "a value in another unit is read as kB");
require(!AgcDriver::ParseCommitCharge(""), "an empty /proc/meminfo gives a commit charge");
const auto commit = AgcDriver::ReadCommitCharge();
#ifdef __APPLE__
require(!commit, "a commit charge is read on macOS, which reports none");
#else
require(commit && commit->used != 0 && commit->budget != 0, "the system's commit charge and limit cannot be read");
#endif
static std::uint8_t* volatile touched = nullptr;
const auto before = AgcDriver::ReadPageFaults();
require(before.has_value(), "the process's page faults cannot be read");
+3 -1
View File
@@ -203,6 +203,7 @@ void testRegisterFile() {
void testContextAndBases() {
AgcDriver::QueueState state;
check(state.context.at(0x1b6) == 0, "the starting context changed SPI_PS_IN_CONTROL");
check(state.context.contains(0x280) && state.context.at(0x280) == 0x00080008u && state.context.contains(0x281) && state.context.at(0x281) == 0xffff0000u, "initial point context differs from the SDK defaults");
execute(state, makePacket(0x69, {0x280, 0x00180018u, 0x00400008u}));
check(state.context.at(0x280) == 0x00180018u && state.context.at(0x281) == 0x00400008u, "explicit point size and limits were not stored");
@@ -210,10 +211,11 @@ void testContextAndBases() {
execute(state, makePacket(0x12, {0}));
for (const auto offset : {0x010u, 0x011u, 0x10bu, 0x10cu, 0x10du}) check(state.context.at(offset) == 0, "CLEAR_STATE did not restore depth and stencil defaults");
check(state.context.at(0x280) == 0x00080008u && state.context.at(0x281) == 0xffff0000u, "CLEAR_STATE did not restore point size and limits");
check(state.context.at(0x1b6) == 2u, "CLEAR_STATE did not leave SPI_PS_IN_CONTROL at its clear-state NUM_INTERP of 2");
execute(state, makePacket(0x69, {0x10, 17}));
execute(state, makePacket(0x76, {0x20c, 2}));
execute(state, makePacket(0x10, {3, 0}, 0x68));
check(state.context == AgcDriver::InitialContextRegisters() && state.shader.at(0x20c) == 2, "push-clear reset wrong state");
check(state.context == AgcDriver::ClearStateContextRegisters() && state.context.at(0x1b6) == 2u && state.shader.at(0x20c) == 2, "push-clear reset wrong state");
expectFailure([&] { execute(state, makePacket(0x10, {1, 0}, 0x68)); }, "already pushed");
execute(state, makePacket(0x69, {0x10, 19}));
execute(state, makePacket(0x69, {0x280, 0x00100010u, 0x00800010u}));
@@ -23,16 +23,16 @@ std::uint64_t baselineRegisterStateKey(const QueueState& queue, bool allUserWord
key ^= value;
key *= 0x100000001b3ull;
};
const auto userEnd = [&](std::uint32_t base) {
if (allUserWords) return base + 32u;
const auto userEnd = [&](std::uint32_t base, std::uint32_t limit) {
if (allUserWords) return base + limit;
const auto resources = queue.shader.find(base - 1);
if (resources == queue.shader.end()) return base;
const auto count = ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u);
return base + std::min(count, 32u);
return base + std::min(count, limit);
};
const std::array<std::pair<std::uint32_t, std::uint32_t>, 3> users{{{0x00cu, userEnd(0x00cu)}, {0x08cu, userEnd(0x08cu)}, {0x10cu, userEnd(0x10cu)}}};
const std::array<std::array<std::uint32_t, 3>, 4> users{{{0x00cu, userEnd(0x00cu, 32u), 32u}, {0x04cu, userEnd(0x04cu, 16u), 16u}, {0x08cu, userEnd(0x08cu, 32u), 32u}, {0x10cu, userEnd(0x10cu, 32u), 32u}}};
const auto unread = [&](std::uint32_t offset) {
return std::any_of(users.begin(), users.end(), [&](const auto& user) { return offset >= user.second && offset < user.first + 32u; });
return std::any_of(users.begin(), users.end(), [&](const auto& user) { return offset >= user[1] && offset < user[0] + user[2]; });
};
for (const auto& range : Graphics::DrawKeyRegisters) {
const auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig;
@@ -0,0 +1,48 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/RegistrationPreparationBudget.hpp"
#include <cstdint>
#include <iostream>
#include <limits>
#include <stdexcept>
namespace {
void Require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
}
int main() {
try {
using AgcDriver::DriverDetail::RegistrationPreparationCommitBudgetBytes;
using AgcDriver::DriverDetail::RegistrationPreparationAllowed;
constexpr std::uint64_t GiB = 1024ull * 1024 * 1024;
constexpr std::uint64_t defaultBudget = 8ull * GiB;
Require(RegistrationPreparationCommitBudgetBytes(nullptr, defaultBudget) == defaultBudget, "an unset budget should preserve the host-derived default");
Require(RegistrationPreparationCommitBudgetBytes("0", defaultBudget) == 0, "zero GiB should disable registration preparation");
Require(RegistrationPreparationCommitBudgetBytes("0.5", defaultBudget) == GiB / 2, "fractional GiB should convert to bytes");
Require(RegistrationPreparationCommitBudgetBytes("2", defaultBudget) == GiB * 2, "whole GiB should convert to bytes");
for (const auto* invalid : {"", "-1", "1x", "inf", "nan", "1e1000"}) {
bool rejected = false;
try {
static_cast<void>(RegistrationPreparationCommitBudgetBytes(invalid, defaultBudget));
} catch (const std::runtime_error&) {
rejected = true;
}
Require(rejected, "invalid budget text should be rejected");
}
Require(RegistrationPreparationAllowed(0, 1), "preparation should be allowed below the budget");
Require(RegistrationPreparationAllowed(1023, 1024), "preparation should be allowed immediately below the budget");
Require(!RegistrationPreparationAllowed(1024, 1024), "preparation should defer at the budget");
Require(!RegistrationPreparationAllowed(1025, 1024), "preparation should defer above the budget");
Require(!RegistrationPreparationAllowed(0, 0), "a zero budget should defer every preparation");
const auto unlimited = std::numeric_limits<std::uint64_t>::max();
Require(RegistrationPreparationAllowed(unlimited - 1, unlimited), "the maximum budget should allow values below it");
Require(!RegistrationPreparationAllowed(unlimited, unlimited), "the maximum budget should defer at its limit");
std::cout << "registration prepare budget tests passed\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
@@ -6216,6 +6216,30 @@ void verifyPixelRequestSerialization() {
depthTestingMerge.context.pixel->targetMerge.depthStencilTests = true;
RecompileCacheKey::Build(depthTestingMerge, replayKey);
require(key != replayKey, "the merge draw's depth or stencil tests did not change the static shader identity");
auto blending = request;
blending.context.pixel->targetMerge.blend = {true, false, 6u, 7u, 0u, 1u, 0u, 0u, 0xfu, {}};
RecompileCacheKey::Build(blending, replayKey);
require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(blending), "an unclamped color target blend did not change the static shader identity");
const auto blendKey = replayKey;
for (const auto change : {0, 1, 2, 3, 4, 5, 6, 7, 8}) {
auto other = blending;
auto& blend = other.context.pixel->targetMerge.blend;
if (change == 0) blend.snorm = true;
if (change == 1) blend.colorSource = 2u;
if (change == 2) blend.colorDestination = 2u;
if (change == 3) blend.colorOperation = 1u;
if (change == 4) blend.alphaSource = 2u;
if (change == 5) blend.alphaDestination = 2u;
if (change == 6) blend.alphaOperation = 1u;
if (change == 7) blend.writeMask = 0x5u;
if (change == 8) blend.constants[2] = 0x3f800000u;
RecompileCacheKey::Build(other, replayKey);
require(blendKey != replayKey, "a field of the unclamped color target blend did not change the static shader identity");
}
auto disabled = blending;
disabled.context.pixel->targetMerge.blend.enabled = false;
RecompileCacheKey::Build(disabled, replayKey);
require(key == replayKey && RecompileCacheKey::ContextHash(request) == RecompileCacheKey::ContextHash(disabled), "a disabled color target blend changed the static shader identity");
auto withoutMerge = request;
withoutMerge.context.pixel->targetMerge = {};
RecompileCacheKey::Build(withoutMerge, replayKey);
@@ -6229,6 +6253,17 @@ void verifyPixelRequestSerialization() {
RecompileCacheKey::Build(withoutStencilRef, replayKey);
require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(withoutStencilRef), "the stencil reference export did not change the static shader identity");
}
auto blendedDepth = request;
blendedDepth.context.pixel = pixel;
blendedDepth.context.pixel->depthExportEnable = true;
blendedDepth.context.pixel->depthExportMin = -0.25f;
blendedDepth.context.pixel->depthExportMax = 0.75f;
blendedDepth.context.pixel->targetMerge.blend = {true, true, 6u, 7u, 1u, 2u, 3u, 4u, 0xau, {0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u}};
const auto blendedReplay = serializer.Deserialize(serializer.Serialize(blendedDepth));
require(blendedReplay.request.context.pixel.has_value() && fields(*blendedReplay.request.context.pixel) == fields(*blendedDepth.context.pixel), "a pixel request with a target blend and depth export bounds lost pixel fields in serialization");
require(blendedReplay.request.context.pixel->targetMerge.blend == blendedDepth.context.pixel->targetMerge.blend, "the target blend did not round-trip beside the depth export bounds");
require(blendedReplay.request.context.pixel->depthExportMin == -0.25f && blendedReplay.request.context.pixel->depthExportMax == 0.75f, "the depth export bounds did not round-trip beside the target blend");
require(blendedReplay.request.context.vertex.has_value() && blendedReplay.request.context.vertex->fetchAttribReg == 17u && blendedReplay.request.context.memory.size() == 1u && blendedReplay.request.context.memory[0].guestAddress == 0x60000u, "a target blend with depth export bounds displaced the following guest context");
request.context.pixel.reset();
request.shader.stage = ShaderStage::Vertex;
const auto withoutPixel = serializer.Deserialize(serializer.Serialize(request));
@@ -6646,6 +6681,15 @@ void verifyVertexInfoVersion19() {
const auto mergingBytes = decodeBase64(serializer.Serialize(merging));
require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 2u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && mergingBytes[mergeOffset + 38] == 1u && mergingBytes[mergeOffset + 39] == 1u && std::equal(plainPixel.begin() + mergeOffset + static_cast<std::ptrdiff_t>(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast<std::ptrdiff_t>(mergeBytes)), "the color target merge is not the 40 bytes the version 46 pixel info adds after the sample order");
require(serializer.Deserialize(serializer.Serialize(merging)).request.context.pixel->targetMerge == merging.context.pixel->targetMerge, "a serialized request lost its color target merge");
auto blendMerging = pixel;
blendMerging.context.pixel->targetMerge = {1u, 0xffffu, 1u, {1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u}, true, false, {true, true, 6u, 7u, 1u, 1u, 0u, 4u, 0xbu, {0x3fc00000u, 0xbe800000u, 0x3f000000u, 0x40000000u}}};
const auto blendMergingBytes = decodeBase64(serializer.Serialize(blendMerging));
constexpr std::size_t blendPayloadBytes = 24u;
require(blendMergingBytes.size() == plainPixel.size() + blendPayloadBytes && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, blendMergingBytes.begin()) && blendMergingBytes[mergeOffset] == 1u && blendMergingBytes[mergeOffset + 1] == 0xffu && blendMergingBytes[mergeOffset + 2] == 0xffu && blendMergingBytes[mergeOffset + 3] == 0x01u && blendMergingBytes[mergeOffset + 4] == 0x00u, "a color target blend is not flagged in the merge mask word, or changed the bytes before the merge");
require(serializer.Deserialize(serializer.Serialize(blendMerging)).request.context.pixel->targetMerge == blendMerging.context.pixel->targetMerge, "a serialized request lost its color target blend");
auto disabledBlend = merging;
disabledBlend.context.pixel->targetMerge.blend = {false, true, 6u, 7u, 1u, 1u, 0u, 4u, 0xbu, {1u, 2u, 3u, 4u}};
require(serializer.Serialize(disabledBlend) == serializer.Serialize(merging), "a disabled color target blend changed the serialized request");
// A version 43 request: no color target merge, and no fused arithmetic flag, pixel function LDS flag, storage size or barycentric built-ins flag at the end.
const auto version43Bytes = [&](std::vector<std::uint8_t> bytes) {
require(bytes.end() - bytes.begin() > mergeOffset + static_cast<std::ptrdiff_t>(mergeBytes) && std::equal(plainPixel.begin() + mergeOffset, plainPixel.begin() + mergeOffset + static_cast<std::ptrdiff_t>(mergeBytes), bytes.begin() + mergeOffset), "a replay fixture does not hold the default color target merge after the sample order");
@@ -140,12 +140,13 @@ void testClearState() {
const auto shader = graphics.shader;
const auto userConfig = graphics.userConfig;
graphics.ClearContext();
check(graphics.context == AgcDriver::InitialContextRegisters(), "CLEAR_STATE retained context registers");
check(graphics.context == AgcDriver::ClearStateContextRegisters(), "CLEAR_STATE retained context registers");
check(graphics.shader == shader && graphics.userConfig == userConfig, "CLEAR_STATE reset unrelated registers");
check(graphics.context.count(0x1b3) == 1 && graphics.context.at(0x1b3) == 0 && graphics.context.count(0x1b4) == 1 && graphics.context.at(0x1b4) == 0, "CLEAR_STATE left SPI_PS_INPUT_ENA/ADDR unset");
check(graphics.context.at(0x1b6) == 2u, "CLEAR_STATE did not leave SPI_PS_IN_CONTROL at its clear-state NUM_INTERP of 2");
graphics.context.emplace(0x10, 31);
graphics.ClearContext();
check(graphics.context == AgcDriver::InitialContextRegisters(), "repeated CLEAR_STATE retained context registers");
check(graphics.context == AgcDriver::ClearStateContextRegisters(), "repeated CLEAR_STATE retained context registers");
std::array<std::uint32_t, 3> words{0xc0001200, 0, 0};
Packet packet{words.data(), 2, 0, {}};
@@ -30,6 +30,7 @@ struct Push {
std::uint32_t columnBegin;
std::uint32_t rowBegin;
std::uint32_t pipeBankXor;
std::uint32_t splitStride;
};
Push decodePush(const std::vector<std::byte>& bytes) {
@@ -140,9 +140,144 @@ void requireSliceAddresses(const GuestTextureResource& resource, std::uint32_t b
}
}
std::uint64_t referenceGcnAddress(TextureTileMode mode, std::uint32_t bytes, std::uint32_t pitch, std::uint32_t height, std::uint32_t x, std::uint32_t y) {
const auto bit = [](std::uint32_t value, std::uint32_t index) { return (value >> index) & 1u; };
const std::uint32_t x0 = bit(x, 0), x1 = bit(x, 1), x2 = bit(x, 2), y0 = bit(y, 0), y1 = bit(y, 1), y2 = bit(y, 2);
std::array<std::uint32_t, 6> pixel{x0, y0, x1, y1, x2, y2};
if (mode == TextureTileMode::kGcnDisplay1D || mode == TextureTileMode::kGcnDisplay2D || mode == TextureTileMode::kGcnDisplayPrt || mode == TextureTileMode::kGcnDisplay2DPrt) {
switch (bytes) {
case 1u: pixel = {x0, x1, x2, y1, y0, y2}; break;
case 2u: pixel = {x0, x1, x2, y0, y1, y2}; break;
case 4u: pixel = {x0, x1, y0, x2, y1, y2}; break;
case 8u: pixel = {x0, y0, x1, x2, y1, y2}; break;
default: pixel = {x0, y0, x1, y1, x2, y2}; break;
}
}
std::uint32_t pixelIndex = 0;
for (std::uint32_t index = 0; index < 6u; ++index) pixelIndex |= pixel[index] << index;
std::uint64_t microBytes = 64u * bytes;
std::uint64_t elementOffset = static_cast<std::uint64_t>(pixelIndex) * bytes;
if (mode == TextureTileMode::kGcnThin1D || mode == TextureTileMode::kGcnDisplay1D) {
return (static_cast<std::uint64_t>(y / 8u) * (pitch / 8u) + x / 8u) * microBytes + elementOffset;
}
std::uint32_t tileSplit = std::min(1024u, std::max(256u, 2u * static_cast<std::uint32_t>(microBytes)));
if (mode >= TextureTileMode::kGcnDepth2D64 && mode <= TextureTileMode::kGcnDepth2D1K) tileSplit = 64u << (static_cast<std::uint32_t>(mode) - static_cast<std::uint32_t>(TextureTileMode::kGcnDepth2D64));
if (mode == TextureTileMode::kGcnDepthPrt256) tileSplit = 256u;
if (mode == TextureTileMode::kGcnDepthPrt1K) tileSplit = 1024u;
const bool prt = IsGcnPrtTiled(mode);
const auto macroMode = std::bit_width(std::min<std::uint64_t>(tileSplit, microBytes) / 64u) - 1u + (prt ? 8u : 0u);
std::uint64_t splitSlice = 0;
if (microBytes > tileSplit) {
splitSlice = elementOffset / tileSplit;
elementOffset %= tileSplit;
microBytes = tileSplit;
}
constexpr std::uint32_t bankHeights[] = {4u, 2u, 1u, 1u, 1u, 0u, 0u, 0u, 8u, 4u, 2u, 1u, 1u}, bankCounts[] = {16u, 16u, 16u, 16u, 8u, 0u, 0u, 0u, 16u, 16u, 16u, 16u, 8u}, aspects[] = {4u, 2u, 2u, 2u, 1u, 0u, 0u, 0u, 4u, 4u, 2u, 2u, 1u};
const std::uint32_t pipes = 8u, bankWidth = 1u, bankHeight = bankHeights[macroMode], banks = bankCounts[macroMode], aspect = aspects[macroMode];
const std::uint32_t macroPitch = 8u * bankWidth * pipes * aspect;
const std::uint32_t macroHeight = 8u * bankHeight * banks / aspect;
const std::uint64_t macroBytes = microBytes * (macroPitch / 8u) * (macroHeight / 8u) / (pipes * banks);
const std::uint64_t macroOffset = (static_cast<std::uint64_t>(y / macroHeight) * (pitch / macroPitch) + x / macroPitch) * macroBytes;
const std::uint32_t tileIndex = ((y / 8u) % bankHeight) * bankWidth + ((x / 8u) / pipes) % bankWidth;
const std::uint64_t sliceBytes = static_cast<std::uint64_t>(pitch / macroPitch) * (height / macroHeight) * macroBytes;
const std::uint64_t total = splitSlice * sliceBytes + macroOffset + elementOffset + static_cast<std::uint64_t>(tileIndex) * microBytes;
const std::uint32_t px = prt ? x % macroPitch : x, py = prt ? y % macroHeight : y;
const std::uint32_t tx = px / 8u, ty = py / 8u;
const bool pipe8x16 = mode == TextureTileMode::kGcnDisplayPrt || mode == TextureTileMode::kGcnThinPrt;
const std::uint32_t pipe = pipe8x16 ? (bit(tx, 1) ^ bit(ty, 0) ^ bit(tx, 2)) | ((bit(tx, 0) ^ bit(ty, 1)) << 1u) | ((bit(tx, 2) ^ bit(ty, 2)) << 2u)
: (bit(tx, 0) ^ bit(ty, 0) ^ bit(tx, 1)) | ((bit(tx, 1) ^ bit(ty, 1)) << 1u) | ((bit(tx, 2) ^ bit(ty, 2)) << 2u);
const std::uint32_t bx = px / 8u / (bankWidth * pipes), by = py / 8u / bankHeight;
std::uint32_t bank = 0;
if (banks == 16u) bank = (bit(bx, 0) ^ bit(by, 3)) | ((bit(bx, 1) ^ bit(by, 2) ^ bit(by, 3)) << 1u) | ((bit(bx, 2) ^ bit(by, 1)) << 2u) | ((bit(bx, 3) ^ bit(by, 0)) << 3u);
else bank = (bit(bx, 0) ^ bit(by, 2)) | ((bit(bx, 1) ^ bit(by, 1) ^ bit(by, 2)) << 1u) | ((bit(bx, 2) ^ bit(by, 0)) << 2u);
if (!pipe8x16) bank = (bank ^ static_cast<std::uint32_t>((banks / 2u + 1u) * splitSlice)) & (banks - 1u);
const std::uint32_t bankBits = static_cast<std::uint32_t>(std::countr_zero(banks));
return ((total >> 8u) << (8u + 3u + bankBits)) | (static_cast<std::uint64_t>(bank) << 11u) | (static_cast<std::uint64_t>(pipe) << 8u) | (total & 0xffu);
}
void gcnTilingTests() {
using enum TextureTileMode;
for (const auto mode : {kGcnThin1D, kGcnDisplay1D, kGcnDisplay2D, kGcnThin2D, kGcnDepth2D64, kGcnDepth2D128, kGcnDepth2D256, kGcnDepth2D512, kGcnDepth2D1K, kGcnDepthPrt256, kGcnDepthPrt1K, kGcnDisplayPrt, kGcnDisplay2DPrt, kGcnThinPrt, kGcnThin2DPrt}) {
for (std::uint32_t bytes = 1u; bytes <= 16u; bytes <<= 1u) {
const auto geometry = GcnTiling(mode, bytes);
const auto what = "PS4 tile mode " + std::to_string(static_cast<int>(mode)) + " at " + std::to_string(bytes) + " bytes";
Require(geometry.blockBytes * geometry.splitSlices == geometry.blockWidth * geometry.blockHeight * bytes, what + ": a block holds its elements exactly");
const auto splitStride = 16u * geometry.blockBytes;
const auto pitch = geometry.blockWidth * 4u;
const auto height = geometry.blockHeight * 4u;
std::vector<std::uint8_t> seen(static_cast<std::size_t>(pitch) * height, 0u);
for (std::uint32_t y = 0; y < height; ++y) {
for (std::uint32_t x = 0; x < pitch; ++x) {
std::uint32_t inBlock = 0;
for (std::uint32_t bit = 0; bit < 16u; ++bit) inBlock |= static_cast<std::uint32_t>(std::popcount((x & geometry.equation[bit] & 0xfffu) ^ ((y << 12u) & geometry.equation[bit] & 0xfff000u)) & 1) << bit;
const auto block = static_cast<std::uint64_t>(y / geometry.blockHeight) * (pitch / geometry.blockWidth) + x / geometry.blockWidth;
std::uint32_t split = 0;
for (std::uint32_t bit = 0; bit < 4u; ++bit) split |= static_cast<std::uint32_t>(std::popcount((x & geometry.splitMasks[bit] & 0xfffu) ^ ((y << 12u) & geometry.splitMasks[bit] & 0xfff000u)) & 1) << bit;
const auto address = block * geometry.blockBytes + inBlock + static_cast<std::uint64_t>(split) * splitStride;
if (address != referenceGcnAddress(mode, bytes, pitch, height, x, y)) Require(false, what + ": element " + std::to_string(x) + "," + std::to_string(y) + " is at " + std::to_string(address) + ", addrlib places it at " + std::to_string(referenceGcnAddress(mode, bytes, pitch, height, x, y)));
Require(address % bytes == 0 && address / bytes < seen.size() && seen[address / bytes]++ == 0, what + ": two elements share an address or one leaves the surface");
}
}
}
}
constexpr std::array<std::pair<std::uint32_t, std::uint32_t>, 5> display2DPrt{{{256u, 256u}, {256u, 128u}, {128u, 128u}, {128u, 64u}, {64u, 64u}}};
for (std::uint32_t log2 = 0; log2 < 5u; ++log2) {
const auto geometry = GcnTiling(TextureTileMode::kGcnDisplay2DPrt, 1u << log2);
Require(geometry.blockWidth == display2DPrt[log2].first && geometry.blockHeight == display2DPrt[log2].second, "PS4 Display2DThinPrt macro tile extent changed");
}
{
Require(ComputeGcnMipLayout(TextureTileMode::kLinear, 20u, 100u, 3u, 100u, false, 1u, true)[0].pitchBytes == 400u, "PS4 linear general pitch changed");
reject([] { ComputeGcnMipLayout(TextureTileMode::kLinear, 20u, 100u, 3u, 100u, false, 1u); }, "is not padded the way its tile mode pads a row");
}
constexpr std::array<std::pair<std::uint32_t, std::uint32_t>, 5> display2D{{{256u, 128u}, {128u, 128u}, {128u, 64u}, {128u, 64u}, {64u, 64u}}};
for (std::uint32_t log2 = 0; log2 < 5u; ++log2) {
const auto geometry = GcnTiling(TextureTileMode::kGcnDisplay2D, 1u << log2);
Require(geometry.blockWidth == display2D[log2].first && geometry.blockHeight == display2D[log2].second, "PS4 Display2DThin macro tile extent changed");
}
{
GuestTextureResource array{};
array.baseAddress = 0x10000;
array.width = 16u;
array.height = 16u;
array.depthOrLastArray = 2u;
array.mipCount = 3u;
array.lastLevel = 2u;
array.tileMode = TextureTileMode::kGcnThin1D;
array.dimension = TextureDimension::k2DArray;
array.format = 20u;
array.gcnPitch = 16u;
const auto geometry = DescribeSurface(array);
Require(geometry.levelMajor && geometry.layers == 3u && geometry.guestBytes == 4608u, "PS4 layered mip chain size changed");
Require(geometry.mips[1].tiledOffset == 3072u && geometry.mips[2].tiledOffset == 3840u && geometry.mips[1].tiledSize == 256u, "PS4 levels do not follow every slice of the level before");
Require(geometry.GuestOffset(2u, geometry.mips[1]) == 3584u && geometry.GuestOffset(1u, geometry.mips[0]) == 1024u, "PS4 slice of a level is misplaced");
Require(geometry.mips[1].linearOffset == 1024u && geometry.mips[2].linearOffset == 1280u, "PS4 linear staging is not per slice");
array.mipCount = 1u;
array.lastLevel = 0u;
Require(!DescribeSurface(array).levelMajor, "a single-level PS4 array is slice-major");
}
{
const auto mips = ComputeGcnMipLayout(TextureTileMode::kGcnDepth2D64, 20u, 2048u, 1152u, 2048u, false, 1u);
Require(mips[0].tiledSize == 2048u * 1152u * 4u && mips[0].gcnSplitStride == 2048u * 1152u, "PS4 tile-split depth layout changed");
Require(GcnTiling(TextureTileMode::kGcnDepth2D64, 4u).splitSlices == 4u && GcnTiling(TextureTileMode::kGcnDepth2D256, 4u).splitSlices == 1u, "PS4 depth split slice count changed");
}
{
const auto mips = ComputeGcnMipLayout(TextureTileMode::kGcnThin1D, 14u, 212u, 35u, 224u, false, 1u);
Require(mips[0].tiledSize == 224u * 40u * 2u && mips[0].blocksPerRow == 28u && mips[0].pitchBytes == 448u && !mips[0].gcnMicroTiled, "PS4 1D thin texture layout changed");
reject([] { ComputeGcnMipLayout(TextureTileMode::kGcnThin1D, 14u, 212u, 35u, 216u, false, 1u); }, "is not padded the way its tile mode pads a row");
}
{
const auto mips = ComputeGcnMipLayout(TextureTileMode::kGcnDisplay2D, 56u, 256u, 256u, 256u, false, 4u);
Require(!mips[0].gcnMicroTiled && !mips[1].gcnMicroTiled && mips[2].gcnMicroTiled && mips[3].gcnMicroTiled, "PS4 2D tiled mips switch to 1D tiling below one macro tile");
Require(mips[0].tiledOffset == 0u && mips[1].tiledOffset == 256u * 256u * 4u && mips[1].blocksPerRow == 1u, "PS4 mips follow level 0 in order");
Require(mips[2].tiledOffset == mips[1].tiledOffset + 128u * 128u * 4u && mips[2].blocksPerRow == 8u, "PS4 1D tiled mip after the 2D levels");
Require(MipTileMode(TextureTileMode::kGcnDisplay2D, mips[2]) == TextureTileMode::kGcnDisplay1D, "a 1D tiled level of a 2D surface uses the 1D display mode");
}
}
}
void RunTextureTilingTests() {
gcnTilingTests();
{
const auto mips = ComputeMipLayout(TextureTileMode::kLinear, 1, 4, 4, 2);
Require(mips.size() == 2, "linear mip chain must contain the requested mip count");
@@ -0,0 +1,40 @@
#include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp"
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <string>
#include <thread>
#include <windows.h>
int main(int argc, char** argv) {
if (argc != 2) return 1;
const std::string mode = argv[1];
const bool selected = mode == "selected";
const bool process = mode == "process";
const bool workerExpected = selected || mode == "default";
const auto stem = std::filesystem::temp_directory_path() / ("anyps5-sampler-" + std::to_string(GetCurrentProcessId()));
const auto workerPath = stem.string() + "-worker.txt";
const auto processPath = stem.string() + "-process.txt";
_putenv_s("APS5_SAMPLE_WORKER", workerPath.c_str());
_putenv_s("APS5_SAMPLE_THREADS", process ? processPath.c_str() : "");
_putenv_s("APS5_SAMPLE_WORKER_QUEUE", mode == "default" ? "" : "0x50");
AgcDriver::StartWorkerSampler(selected ? 0x50 : 0);
const auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(200);
while (std::chrono::steady_clock::now() < until) std::this_thread::yield();
AgcDriver::StopWorkerSampler();
const auto sampled = [](const std::string& path) {
std::ifstream file(path);
std::string marker;
std::uint64_t samples = 0;
return (file >> marker >> samples) && marker == "#" && samples != 0;
};
const bool passed = sampled(workerPath) == workerExpected && sampled(processPath) == process;
std::filesystem::remove(workerPath);
std::filesystem::remove(processPath);
std::filesystem::remove(stem.string() + "-worker.collapsed.txt");
std::filesystem::remove(stem.string() + "-worker.collapsed.txt.tmp");
std::printf("worker sampler %s: %s\n", mode.c_str(), passed ? "passed" : "failed");
return passed ? 0 : 1;
}
@@ -4,6 +4,7 @@
#include "prx/libc/include/GuestAllocations.hpp"
#include "Optimization/ResourceProgram.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
@@ -177,6 +178,77 @@ void ScalarReads(AgcDriver::VulkanDevice& device) {
GuestAllocations::Mutation().Remove(Input.data());
}
void DisabledDescriptors(AgcDriver::VulkanDevice& device) {
constexpr std::uint32_t Value = 0x12345678u;
alignas(256) static constexpr std::array<std::uint32_t, 5> formattedStore{0x7e0202ffu, Value, 0xe0102000u, 0x80000100u, 0xbf810000u};
alignas(256) static constexpr std::array<std::uint32_t, 6> rawStore{0x34020082u, 0x7e0402ffu, Value, 0xe0701000u, 0x80000201u, 0xbf810000u};
alignas(256) static constexpr std::array<std::uint32_t, 7> formattedLoad{0x34020082u, 0xe0002000u, 0x80000200u, 0xbf8c3f70u, 0xe0701000u, 0x80010201u, 0xbf810000u};
alignas(256) static constexpr std::array<std::uint32_t, 4> atomicAdd{0x34020082u, 0xe0c81000u, 0x80000201u, 0xbf810000u};
std::array<std::uint32_t, 8> userData{};
auto request = Request(userData, device.ComputeTarget(Lanes), {});
request.useCache = false;
const auto output = BufferDescriptor(Output.data(), sizeof(Output));
std::copy(output.begin(), output.end(), userData.begin() + 4);
const auto dispatch = [&](std::span<const std::uint32_t> code, const std::array<std::uint32_t, 4>& descriptor) {
request.shader.code = code;
request.shader.codeAddress = reinterpret_cast<std::uintptr_t>(code.data());
std::copy(descriptor.begin(), descriptor.end(), userData.begin());
Output.fill(Fill);
const auto result = ShaderRecompiler::Recompile(request);
device.Dispatch(result, 1, 1, 1);
device.WaitIdle();
};
const auto all = [](std::uint32_t expected) { return std::all_of(Output.begin(), Output.end(), [expected](std::uint32_t value) { return value == expected; }); };
const auto raw = [](const void* data, std::uint32_t bytes, std::uint32_t word3) {
auto descriptor = BufferDescriptor(data, bytes);
descriptor[3] = word3;
return descriptor;
};
const auto strided = [](const void* data, std::uint32_t word3) {
auto descriptor = BufferDescriptor(data, Lanes);
descriptor[1] |= 4u << 16u;
descriptor[3] = word3;
return descriptor;
};
std::array<std::uint32_t, Lanes> input{};
std::memcpy(input.data(), Input.data(), sizeof(input));
dispatch(Code, raw(Input.data(), InputBytes, 0x31016facu));
Require(Output == input, "raw load through a TYPE 0 32_FLOAT V# did not read the buffer");
dispatch(formattedLoad, strided(Input.data(), 0x31014facu));
Require(Output == input, "formatted load through a TYPE 0 32_UINT V# did not read the buffer");
dispatch(rawStore, raw(Output.data(), sizeof(Output), 0x31016facu));
Require(all(Value), "raw store through a TYPE 0 32_FLOAT V# did not write every lane");
dispatch(formattedStore, strided(Output.data(), 0x31016facu));
Require(all(Value), "formatted store through a TYPE 0 32_FLOAT V# did not write every lane");
dispatch(Code, raw(Input.data(), InputBytes, 0x31000facu));
Require(all(0u), "raw load through a TYPE 0 FORMAT 0 V# did not return 0");
dispatch(formattedLoad, strided(Input.data(), 0x31000facu));
Require(all(0u), "formatted load through a TYPE 0 FORMAT 0 V# did not return 0");
for (const auto word3 : {0x31000facu, 0x91000facu, 0x91016facu, 0x91b00facu, 0xd1000facu, 0xd1016facu}) {
dispatch(rawStore, raw(Output.data(), sizeof(Output), word3));
Require(all(Fill), "raw store through V# word 3 " + Hex(word3) + " wrote memory");
dispatch(formattedStore, strided(Output.data(), word3));
Require(all(Fill), "formatted store through V# word 3 " + Hex(word3) + " wrote memory");
}
dispatch(formattedStore, {0x02100b00u, 0xc3c00000u, 0x010dc1dfu, 0x91b00facu});
Require(all(Fill), "store through an image T# read as a V# wrote memory");
const auto rejects = [&](std::span<const std::uint32_t> code, std::uint32_t word3, const char* access) {
bool rejected = false;
try {
dispatch(code, raw(Output.data(), sizeof(Output), word3));
} catch (const std::runtime_error& error) {
Require(std::string(error.what()).find("buffer descriptor uses an unsupported type") != std::string::npos, std::string(access) + " through V# word 3 " + Hex(word3) + " failed for another reason: " + error.what());
rejected = true;
}
Require(rejected, std::string(access) + " accepted V# word 3 " + Hex(word3));
};
for (const auto word3 : {0x71000facu, 0x71016facu, 0xb107ffacu, 0xf107ffacu}) {
rejects(Code, word3, "a raw load");
rejects(rawStore, word3, "a raw store");
rejects(formattedStore, word3, "a formatted store");
}
}
}
int main() {
@@ -187,6 +259,7 @@ int main() {
Run(*device, false);
Run(*device, true);
ScalarReads(*device);
DisabledDescriptors(*device);
std::puts("buffer descriptor runtime tests passed");
return 0;
} catch (const std::exception& error) {
@@ -0,0 +1,222 @@
#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 <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 = 5;
constexpr std::uint32_t Stride = 8;
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, 20> Code{
0x34020084u, 0x34040085u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xd56d6a0au, 0x04160904u, 0xd501000bu,
0x01a90280u, 0xd56d6a0cu, 0x04160905u, 0xd501000du, 0x01a90280u, 0xd55f000eu, 0x04160906u, 0xe0781000u,
0x80010a02u, 0xe0701010u, 0x80010e02u, 0xbf810000u,
};
constexpr const char* Names[Results] = {
"v_div_scale_f32 v10, vcc_lo, v4, v4, v5",
"its vcc_lo",
"v_div_scale_f32 v12, vcc_lo, v5, v4, v5",
"its vcc_lo",
"v_div_fixup_f32 v14, v6, v4, v5",
};
constexpr std::uint32_t Rows[Threads][3] = {
{0x7e800000u, 0x00000001u, 0x00000005u},
{0x7f7fffffu, 0x807fffffu, 0x3f800000u},
{0x7e800000u, 0x00400000u, 0x80400000u},
{0xff7fffffu, 0x00000003u, 0x7fc00001u},
{0x00000001u, 0x3f800000u, 0x00000005u},
{0x007fffffu, 0x40490fdbu, 0x3f800000u},
{0x80000001u, 0xbfc00000u, 0x80400000u},
{0x00400000u, 0x7f7fffffu, 0x7fc00001u},
{0x3f800000u, 0x00000003u, 0x00000005u},
{0xbfc00000u, 0x80000001u, 0x3f800000u},
{0x40490fdbu, 0x007fffffu, 0x80400000u},
{0x5f800000u, 0x00400000u, 0x7fc00001u},
{0x00000001u, 0x00000001u, 0x00000005u},
{0x007fffffu, 0x807fffffu, 0x3f800000u},
{0x00400000u, 0x00000003u, 0x80400000u},
{0x00800000u, 0x00000001u, 0x7fc00001u},
{0x00000001u, 0x00800000u, 0x00000005u},
{0x0c000000u, 0x00000001u, 0x3f800000u},
{0x00000003u, 0x0c800000u, 0x80400000u},
{0x34000000u, 0x80000001u, 0x7fc00001u},
{0x00000001u, 0x00000000u, 0x00000005u},
{0x00000000u, 0x00000001u, 0x3f800000u},
{0x7f800000u, 0x00000001u, 0x80400000u},
{0x00000001u, 0xff800000u, 0x7fc00001u},
{0x7fc00000u, 0x00000001u, 0x00000005u},
{0x00000003u, 0x7fc00000u, 0x3f800000u},
{0x00000001u, 0x7e800000u, 0x80400000u},
{0x807fffffu, 0x5f800000u, 0x7fc00001u},
{0x3f800000u, 0x3f800000u, 0x00000005u},
{0x40490fdbu, 0xbfc00000u, 0x3f800000u},
{0x0c800000u, 0x7f7fffffu, 0x80400000u},
{0x00800000u, 0x0c000000u, 0x7fc00001u}
};
constexpr std::uint32_t Mode1Expected[Threads][Results] = {
{0x5e800000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u},
{0x5f7fffffu, 0x00000001u, 0x80000000u, 0x00000001u, 0x80000000u},
{0x5e800000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u},
{0xdf7fffffu, 0x00000001u, 0x00000000u, 0x00000001u, 0x80000000u},
{0x15000000u, 0x00000001u, 0x3f800000u, 0x00000001u, 0x00000005u},
{0x207ffffeu, 0x00000001u, 0x40490fdbu, 0x00000001u, 0x3f800000u},
{0x95000000u, 0x00000001u, 0xbfc00000u, 0x00000001u, 0x00400000u},
{0x20000000u, 0x00000001u, 0x7f7fffffu, 0x00000001u, 0x7f800000u},
{0x3f800000u, 0x00000001u, 0x15c00000u, 0x00000001u, 0x00000005u},
{0xbfc00000u, 0x00000001u, 0x95000000u, 0x00000001u, 0x3f800000u},
{0x40490fdbu, 0x00000001u, 0x207ffffeu, 0x00000001u, 0x00400000u},
{0x5f800000u, 0x00000001u, 0x20000000u, 0x00000001u, 0x00000000u},
{0x15000000u, 0x00000000u, 0x15000000u, 0x00000000u, 0x00000005u},
{0x207ffffeu, 0x00000000u, 0xa07ffffeu, 0x00000000u, 0xbf800000u},
{0x20000000u, 0x00000000u, 0x15c00000u, 0x00000000u, 0x00400000u},
{0x20800000u, 0x00000000u, 0x15000000u, 0x00000000u, 0x7f800000u},
{0x15000000u, 0x00000000u, 0x20800000u, 0x00000000u, 0x00000005u},
{0x2c000000u, 0x00000000u, 0x15000000u, 0x00000000u, 0x3f800000u},
{0x15c00000u, 0x00000000u, 0x2c800000u, 0x00000000u, 0x00400000u},
{0x34000000u, 0x00000001u, 0x95000000u, 0x00000001u, 0xff800000u},
{0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0x00000000u},
{0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0x7f800000u},
{0x7f800000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u},
{0x15000000u, 0x00000001u, 0xff800000u, 0x00000001u, 0xff800000u},
{0x7fc00000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x7fc00000u},
{0x15c00000u, 0x00000001u, 0x7fc00000u, 0x00000001u, 0x7fc00000u},
{0x15000000u, 0x00000001u, 0x7e800000u, 0x00000001u, 0x00400000u},
{0xa07ffffeu, 0x00000001u, 0x5f800000u, 0x00000001u, 0xff800000u},
{0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000005u},
{0x40490fdbu, 0x00000000u, 0xbfc00000u, 0x00000000u, 0xbf800000u},
{0x2c800000u, 0x00000001u, 0x7f7fffffu, 0x00000001u, 0x00400000u},
{0x20800000u, 0x00000000u, 0x2c000000u, 0x00000000u, 0x7f800000u}
};
constexpr std::uint32_t Mode2Expected[Threads][Results] = {
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x00000000u},
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x80000000u},
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x00000000u},
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x80000000u},
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x7f800000u},
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x7f800000u},
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x7f800000u},
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x7f800000u},
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x00000000u},
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x00000000u},
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x00000000u},
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x00000000u},
{0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0xffc00000u},
{0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0xffc00000u},
{0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0xffc00000u},
{0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0x00000000u},
{0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0x7f800000u},
{0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0x00000000u},
{0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0x7f800000u},
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x80000000u},
{0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0xffc00000u},
{0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0xffc00000u},
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x00000000u},
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0xff800000u},
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x7fc00000u},
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x7fc00000u},
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x7f800000u},
{0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0xff800000u},
{0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000005u},
{0x40490fdbu, 0x00000000u, 0xbfc00000u, 0x00000000u, 0xbf800000u},
{0x2c800000u, 0x00000001u, 0x7f7fffffu, 0x00000001u, 0x00400000u},
{0x20800000u, 0x00000000u, 0x2c000000u, 0x00000000u, 0x7f800000u}
};
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;
}
std::optional<std::string> Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode floatMode) {
Input.fill(0u);
for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy(std::begin(Rows[tid]), std::end(Rows[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));
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}, 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.Target(),
{0, 0, 0, 128}
};
request.useCache = false;
request.context.floatMode = floatMode;
std::optional<decltype(ShaderRecompiler::Recompile(request))> result;
try {
result.emplace(ShaderRecompiler::Recompile(request));
} catch (const std::exception& error) {
return std::string(error.what());
}
device.Dispatch(*result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
device.WaitIdle();
return std::nullopt;
}
void Check(std::uint32_t denormals, bool ieee) {
const auto& expected = denormals == 1u ? Mode1Expected : Mode2Expected;
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
for (std::uint32_t i = 0; i < Results; ++i) {
const std::uint32_t actual = Output[tid * Stride + i];
Require(actual == expected[tid][i], "f32 denormal mode " + std::to_string(denormals) + (ieee ? ", IEEE=1, " : ", IEEE=0, ") + Names[i] + " with v4 " + Hex(Rows[tid][0]) + ", v5 " + Hex(Rows[tid][1]) + ", v6 " + Hex(Rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected[tid][i]));
}
}
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
const bool keepsDenormals = device->Target().float32DenormPreserve;
std::uint32_t checked = 0u;
for (const std::uint32_t denormals : {1u, 2u}) {
for (const bool ieee : {false, true}) {
const ShaderRecompiler::ShaderFloatMode floatMode{0xc0u | denormals << 4u, true, ieee, false};
const auto refusal = Run(*device, floatMode);
if (refusal) {
Require(denormals == 1u && !keepsDenormals && refusal->find("needs DenormPreserve 32") != std::string::npos, "FLOAT_MODE " + Hex(floatMode.floatMode) + ": " + *refusal);
continue;
}
++checked;
Check(denormals, ieee);
}
}
std::printf("v_div_scale_f32 and v_div_fixup_f32 denormal mode 1 and 2 tests passed (%u of 4 modes run; the others need DenormPreserve 32)\n", checked);
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
@@ -700,14 +700,20 @@ bool HostFusesFma(AgcDriver::VulkanDevice& device) {
}
void CheckScaleDenormals(AgcDriver::VulkanDevice& device) {
const std::array<std::optional<ShaderRecompiler::ShaderFloatMode>, 5> modes{{
const std::array<std::optional<ShaderRecompiler::ShaderFloatMode>, 9> modes{{
std::nullopt,
ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false},
ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false},
ShaderRecompiler::ShaderFloatMode{0xd0u, true, false, false},
ShaderRecompiler::ShaderFloatMode{0xd0u, true, true, false},
ShaderRecompiler::ShaderFloatMode{0xe0u, true, false, false},
ShaderRecompiler::ShaderFloatMode{0xe0u, true, true, false},
ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false},
ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false},
}};
for (const auto& mode : modes) {
const std::uint32_t denormals = mode.has_value() ? (mode->floatMode >> 4u) & 3u : 0u;
if (denormals == 1u && !device.Target().float32DenormPreserve) continue;
Shader shader(device, ScaleCode, mode);
Input.fill(0u);
for (std::uint32_t lane = 0; lane < std::size(ScaleVectors); ++lane) {
@@ -718,20 +724,12 @@ void CheckScaleDenormals(AgcDriver::VulkanDevice& device) {
for (std::uint32_t lane = 0; lane < std::size(ScaleVectors); ++lane) {
const auto& vector = ScaleVectors[lane];
const auto* out = &Output[lane * Stride];
const auto expected = mode.has_value() && ((mode->floatMode >> 4u) & 3u) == 3u ? vector.kept : vector.flushed;
const std::uint32_t keptResult = (vector.kept & 0x7f800000u) == 0u ? vector.kept & 0x80000000u : vector.kept;
const auto expected = denormals == 3u ? vector.kept : denormals == 1u ? keptResult : vector.flushed;
const std::string where = "division scale denormals: lane " + std::to_string(lane) + " mode " + (mode.has_value() ? Hex(mode->floatMode) : "unknown");
Require(out[0] == expected && out[1] == vector.vcc, where + ": v_div_scale_f32 is " + Hex(out[0]) + " vcc " + std::to_string(out[1]) + ", expected " + Hex(expected) + " vcc " + std::to_string(vector.vcc));
}
}
for (const std::uint32_t mode : {0xd0u, 0xe0u}) {
bool refused = false;
try {
Shader shader(device, ScaleCode, ShaderRecompiler::ShaderFloatMode{mode, true, false, false});
} catch (const std::runtime_error& error) {
refused = std::string(error.what()).find("f32 denormal mode") != std::string::npos;
}
Require(refused, "division scale denormals: FLOAT_MODE " + Hex(mode) + " was not refused");
}
}
void CheckHelpers(AgcDriver::VulkanDevice& device, bool fused) {
@@ -10,7 +10,6 @@
#include <cstdio>
#include <iostream>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
#include <spirv/unified1/spirv.hpp>
@@ -555,15 +554,6 @@ int main() {
CheckTinyLegacy("round mode " + std::to_string(round));
CheckTinyDirected(round);
}
for (const std::uint32_t mode : {0xd0u, 0xe0u}) {
bool refused = false;
try {
Run(*device, ShaderRecompiler::ShaderFloatMode{mode, true, false, false});
} catch (const std::runtime_error& error) {
refused = std::string(error.what()).find("f32 denormal mode") != std::string::npos;
}
Require(refused, "f32 denormal flush: FLOAT_MODE " + Hex(mode) + " was not refused");
}
}
std::puts("f32 denormal flush tests passed");
return 0;
@@ -88,7 +88,7 @@ void Run() {
const std::string first = std::string(prefix) + ".req";
const std::string failed = std::string(prefix) + "_failed.req";
Valid = BufferDescriptor(0x00014204u);
Unsupported = BufferDescriptor(0xa1b00facu);
Unsupported = BufferDescriptor(0x61b00facu);
Output.fill(Untouched);
const auto valid = Pointer(Valid.data());
Dispatch(valid);
@@ -101,7 +101,7 @@ void Run() {
} catch (const std::exception& error) {
message = error.what();
}
Require(message.find("unsupported type") != std::string::npos, "the dispatch through a type 2 descriptor failed for another reason: " + message);
Require(message.find("unsupported type") != std::string::npos, "the dispatch through a type 1 descriptor failed for another reason: " + message);
Require(message.find("(" + failed + ")") != std::string::npos, "the failure does not name the failing dispatch's request: " + message);
const auto firstText = Read(first);
const auto failedText = Read(failed);
@@ -0,0 +1,91 @@
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "VulkanTestDevice.hpp"
#include <array>
#include <bit>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <string>
namespace AgcDriver {
struct VulkanDeviceTestAccess {
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
};
}
namespace {
using namespace AgcDriver::Graphics;
std::uint32_t parity(std::uint32_t mask, std::uint32_t x, std::uint32_t y) {
return static_cast<std::uint32_t>(std::popcount((x & mask & 0xfffu) ^ ((y << 12u) & mask & 0xfff000u))) & 1u;
}
std::uint8_t elementByte(std::uint32_t x, std::uint32_t y, std::uint32_t byte) {
return static_cast<std::uint8_t>(x * 7u + y * 13u + byte * 101u + (x >> 4u) * 3u + (y >> 3u) * 5u);
}
void detile(const Context& context, TextureDetiler& detiler, TextureTileMode mode, std::uint32_t bytes) {
constexpr std::array<std::uint32_t, 5> formats{1u, 7u, 20u, 62u, 75u};
const auto geometry = GcnTiling(mode, bytes);
const auto width = geometry.blockWidth * 2u;
const auto height = geometry.blockHeight * 2u;
const auto mip = ComputeGcnMipLayout(mode, formats[std::countr_zero(bytes)], width, height, width, false, 1u).front();
const auto what = "PS4 tile mode " + std::to_string(static_cast<int>(mode)) + " at " + std::to_string(bytes) + " bytes";
Buffer tiled(context, static_cast<std::size_t>(mip.tiledSize), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
Buffer linear(context, static_cast<std::size_t>(mip.linearSize), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
std::memset(tiled.Bytes().data(), 0, tiled.Bytes().size());
std::memset(linear.Bytes().data(), 0xcd, linear.Bytes().size());
for (std::uint32_t y = 0; y < height; ++y) {
for (std::uint32_t x = 0; x < width; ++x) {
std::uint32_t inBlock = 0, split = 0;
for (std::uint32_t bit = 0; bit < 16u; ++bit) inBlock |= parity(geometry.equation[bit], x, y) << bit;
for (std::uint32_t bit = 0; bit < 4u; ++bit) split |= parity(geometry.splitMasks[bit], x, y) << bit;
const auto block = static_cast<std::uint64_t>(y / geometry.blockHeight) * mip.blocksPerRow + x / geometry.blockWidth;
const auto offset = block * geometry.blockBytes + inBlock + static_cast<std::uint64_t>(split) * mip.gcnSplitStride;
Require(offset + bytes <= tiled.Bytes().size(), what + ": an element lies outside the tiled surface");
for (std::uint32_t byte = 0; byte < bytes; ++byte) tiled.Bytes()[offset + byte] = static_cast<std::byte>(elementByte(x, y, byte));
}
}
CommandBatch batch(context);
detiler.BeginBatch();
detiler.Dispatch(batch.Handle(), mode, bytes, tiled.Handle(), 0, linear.Handle(), 0, mip);
const VkMemoryBarrier toHost{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT};
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &toHost, 0, nullptr, 0, nullptr);
batch.SubmitAndWait();
for (std::uint32_t y = 0; y < height; ++y) {
for (std::uint32_t x = 0; x < width; ++x) {
for (std::uint32_t byte = 0; byte < bytes; ++byte) {
const auto got = static_cast<std::uint8_t>(linear.Bytes()[static_cast<std::size_t>(y) * mip.pitchBytes + static_cast<std::size_t>(x) * bytes + byte]);
if (got != elementByte(x, y, byte)) Require(false, what + ": element " + std::to_string(x) + "," + std::to_string(y) + " byte " + std::to_string(byte) + " detiled as " + std::to_string(got) + ", expected " + std::to_string(elementByte(x, y, byte)));
}
}
}
}
}
int main() {
try {
auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
{
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device);
TextureDetiler detiler(context);
using enum TextureTileMode;
for (const auto mode : {kGcnThin1D, kGcnDisplay1D, kGcnDisplay2D, kGcnThin2D, kGcnDepth2D64, kGcnDepth2D256, kGcnDepth2D1K, kGcnDisplayPrt, kGcnThin2DPrt, kGcnDepthPrt256}) {
for (std::uint32_t bytes = 1u; bytes <= 16u; bytes <<= 1u) detile(context, detiler, mode, bytes);
}
}
device.reset();
std::puts("PS4 detile tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
@@ -43,6 +43,10 @@ alignas(256) constexpr std::array<std::uint32_t, 12> StoreCode{
0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0201f08, 0x0001141e,
0x7e3e0281, 0xf0201508, 0x0001141e, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 12> StoreD16Code{
0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0201f08, 0x8001141e,
0x7e3e0281, 0xf0201508, 0x8001141e, 0xbf810000,
};
struct Format {
const char* name;
@@ -141,28 +145,35 @@ std::array<std::uint32_t, 8> TextureDescriptor(const void* data, const Format& f
((Width - 1u) >> 2u) | ((Height - 1u) << 14u), (format.components == 2u ? TwoChannelSwizzle : IdentitySwizzle) | (Type2D << 28u), 0u, 0u, 0u, 0u};
}
void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Format& format) {
std::uint16_t Half(std::uint32_t tid, std::uint32_t component) {
return HalfTowardZero(Value(tid, component));
}
void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Format& format, bool d16) {
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
for (std::uint32_t component = 0; component < 4u; ++component) Input[tid * 4u + component] = Value(tid, component);
for (std::uint32_t component = 0; component < 4u; ++component) {
Input[tid * 4u + component] = d16 ? component < 2u ? static_cast<std::uint32_t>(Half(tid, component * 2u)) | (static_cast<std::uint32_t>(Half(tid, component * 2u + 1u)) << 16u) : 0u : Value(tid, component);
}
}
const auto& storeCode = d16 ? StoreD16Code : StoreCode;
std::fill(texels, texels + TexelBytes, 0x5au);
std::vector<std::uint32_t> userData(16, 0u);
const auto buffer = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * sizeof(Input[0])));
const auto texture = TextureDescriptor(texels, format);
std::copy(buffer.begin(), buffer.end(), userData.begin());
std::copy(texture.begin(), texture.end(), userData.begin() + 4);
const std::span<const std::uint32_t> code(StoreCode);
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(StoreCode.data()), std::as_bytes(code)}}};
const std::span<const std::uint32_t> code(storeCode);
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(storeCode.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>(StoreCode.data()), code, 0, {}},
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(storeCode.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>(StoreCode.data()));
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(storeCode.data()));
device.WaitIdle();
AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast<std::uintptr_t>(texels), TexelBytes, nullptr, "test");
device.WaitIdle();
@@ -172,10 +183,10 @@ std::uint16_t ExpectedComponent(std::uint32_t tid, std::uint32_t dmask, std::uin
if (((dmask >> component) & 1u) == 0u) return 0u;
std::uint32_t source = 0u;
for (std::uint32_t lower = 0u; lower < component; ++lower) source += (dmask >> lower) & 1u;
return HalfTowardZero(Value(tid, source));
return Half(tid, source);
}
void Check(const std::uint8_t* texels, const Format& format) {
void Check(const std::uint8_t* texels, const Format& format, bool d16) {
for (std::uint32_t y = 0; y < Height; ++y) {
for (std::uint32_t x = 0; x < Width; ++x) {
const auto offset = TexelOffset(format, x, y);
@@ -184,7 +195,7 @@ void Check(const std::uint8_t* texels, const Format& format) {
std::memcpy(&actual, texels + offset + component * sizeof(actual), sizeof(actual));
const auto dmask = y == Writes[0].row ? Writes[0].dmask : y == Writes[1].row ? Writes[1].dmask : 0u;
const auto expected = dmask != 0u ? ExpectedComponent(x, dmask, component) : 0x5a5au;
Require(actual == expected, std::string("image_store of ") + format.name + ": texel (" + std::to_string(x) + ", " + std::to_string(y) + ") component " + std::to_string(component) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected));
Require(actual == expected, std::string(d16 ? "D16 image_store of " : "image_store of ") + format.name + ": texel (" + std::to_string(x) + ", " + std::to_string(y) + ") component " + std::to_string(component) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected));
}
}
}
@@ -197,9 +208,11 @@ int main() {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
GuestBlock block;
for (const auto& format : Formats) {
Store(*device, block.Data(), format);
Check(block.Data(), format);
for (const bool d16 : {false, true}) {
for (const auto& format : Formats) {
Store(*device, block.Data(), format, d16);
Check(block.Data(), format, d16);
}
}
std::puts("image store float rounding tests passed");
return 0;
@@ -82,6 +82,29 @@ void RangeTests() {
Require(!DecodePatternFillRange(data, packet), "multidimensional fill was accepted");
}
void GcnRangeTests() {
const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1};
constexpr std::array<std::uint32_t, 16> data{0, 0, 0, 0, 0x80600830u, 0x00040002u, 1, 0x20024004u, 0x40a00000u, 0x00047002u, 0x001fe000u, 0x18024004u, 0x806008acu, 0x00100002u, 1, 0x20077facu};
const auto& code = GcnPatternFillKernel;
Require(MatchesGcnPatternFillKernel(code, data, compute), "PS4 pattern fill kernel was not recognized");
Require(!MatchesPatternFillKernel(code, data, compute), "the PS4 fill matched the PS5 kernel");
for (std::size_t word = 0; word < code.size(); ++word) {
auto altered = code;
altered[word] ^= 1;
Require(!MatchesGcnPatternFillKernel(altered, data, compute), "a different shader matched the PS4 fill kernel");
}
for (const auto& [word, bits] : std::array<std::pair<std::size_t, std::uint32_t>, 4>{{{5, 0x000c0000u}, {11, 0x00003000u}, {7, 0x00800000u}, {7, 0x00000001u}}}) {
auto altered = data;
altered[word] ^= bits;
Require(!MatchesGcnPatternFillKernel(code, altered, compute), "unsupported PS4 fill descriptor matched");
}
constexpr std::array<std::uint32_t, 5> packet{0xc0031500u, 0x7f80u, 1, 1, 1};
const auto range = DecodeGcnPatternFillRange(data, packet);
Require(range && range->source == 0x280600830ull && range->destination == 0x240a00000ull && range->control == 0x2806008acull, "PS4 fill addresses kept bits above 44");
Require(range->invocations == 0x7f80u * 64u && range->records == 0x1fe000u, "PS4 fill range changed");
Require(range->UniformBytes(0x1fe000u, 0) == 8355840u, "PS4 render target clear does not cover the target");
}
std::vector<std::uint32_t> Commands(std::span<const std::uint32_t> data, std::uint32_t threads, std::uint32_t wave, bool partial, bool indirect) {
std::vector<std::uint32_t> words;
const auto registers = [&](std::uint32_t first, std::span<const std::uint32_t> values) {
@@ -209,6 +232,7 @@ void Benchmark() {
int main(int argc, char** argv) {
try {
RangeTests();
GcnRangeTests();
{
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
@@ -204,6 +204,50 @@ void Run(AgcDriver::VulkanDevice& device) {
ExpectFailure([&] { static_cast<void>(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request)); }, "does not refer to registered code");
}
void DumpSelection(AgcDriver::VulkanDevice& device) {
using namespace ShaderRecompiler;
const std::array<std::uint32_t, 1> code{0xffffffffu};
const std::vector<std::byte> header(2048, std::byte{0x5a});
RecompileRequest request{{ShaderStage::Compute, 0x12345cafeull, code, 0x20000, header},
{32, 0, {}, ShaderComputeStageInfo{{1, 1, 1}, 0, {}, false, 1, {}}, {}, {}, {}}, device.ComputeTarget(32), {0, 0, 0, 128}};
const char* previous = std::getenv("APS5_DUMP_SHADERS");
const std::string saved = previous != nullptr ? previous : "";
const auto setCapture = [](const char* value) {
#ifdef _WIN32
Require(_putenv_s("APS5_DUMP_SHADERS", value) == 0, "cannot set shader capture");
#else
Require(setenv("APS5_DUMP_SHADERS", value, 1) == 0, "cannot set shader capture");
#endif
};
const auto written = [](const std::string& name) {
const bool exists = std::ifstream(name, std::ios::binary).is_open();
std::remove(name.c_str());
return exists;
};
const std::string excludedPath = "shader_12345cafe_excluded.req";
const std::string listedPath = "shader_12345cafe_listed.req";
const std::string otherPath = "shader_feedface_unlisted.req";
std::remove(excludedPath.c_str());
std::remove(listedPath.c_str());
std::remove(otherPath.c_str());
setCapture("feedface");
Require(AgcDriver::DriverDetail::Driver::dumpRequest(request.shader.codeAddress, request, "_excluded").empty(), "a program outside the APS5_DUMP_SHADERS list was named as dumped");
Require(!written(excludedPath), "a program outside the APS5_DUMP_SHADERS list was dumped");
setCapture("feedface,0x12345CAFE");
Require(AgcDriver::DriverDetail::Driver::dumpRequest(request.shader.codeAddress, request, "_excluded") == excludedPath, "a program dropped by an earlier list was not dumped once listed");
Require(written(excludedPath), "a listed program's request was not written after an earlier list dropped it");
Require(AgcDriver::DriverDetail::Driver::dumpRequest(request.shader.codeAddress, request, "_listed") == listedPath, "a listed program was not dumped");
Require(written(listedPath), "a listed program's request was not written");
setCapture("12345cafe,not-an-address");
Require(AgcDriver::DriverDetail::Driver::dumpRequest(0xfeedfaceull, request, "_unlisted") == otherPath, "a value that is not an address list did not dump every program");
Require(written(otherPath), "a value that is not an address list did not write the request");
#ifdef _WIN32
Require(_putenv_s("APS5_DUMP_SHADERS", saved.c_str()) == 0, "cannot restore shader capture");
#else
Require((previous != nullptr ? setenv("APS5_DUMP_SHADERS", saved.c_str(), 1) : unsetenv("APS5_DUMP_SHADERS")) == 0, "cannot restore shader capture");
#endif
}
void FailureCapture(AgcDriver::VulkanDevice& device) {
using namespace ShaderRecompiler;
const std::array<std::uint32_t, 1> code{0xffffffffu};
@@ -723,7 +767,9 @@ void RegisteredVertexWorkgroup(const ShaderRecompiler::SpirvTarget& target, bool
void LazyRegistration() {
using namespace NarrowConstantStoreFixture;
Require(std::getenv("APS5_LAZY_SHADER_PREPARE") != nullptr, "lazy preparation is not enabled");
const char* registrationBudget = std::getenv("APS5_REGISTRATION_PREPARE_COMMIT_GB");
const bool budgetOnly = std::getenv("APS5_LAZY_SHADER_PREPARE") == nullptr;
Require(!budgetOnly || (registrationBudget != nullptr && std::string_view(registrationBudget) == "0"), "lazy preparation is not enabled");
struct Header {
Shader shader{};
std::array<ShaderRegister, 7> registers{};
@@ -741,15 +787,17 @@ void LazyRegistration() {
header.specials.dispatch_modifier = 0x8000;
header.registers = {{{0x20c, static_cast<std::uint32_t>(address >> 8u)}, {0x20d, static_cast<std::uint32_t>(address >> 40u)}, {0x207, Threads}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 4}}};
AgcDriverRegisterShader_nid_postfix(&header.shader);
++header.registers[0].value;
ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "entry point is outside shader code");
--header.registers[0].value;
header.registers[2].value = 0;
ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "must be nonzero");
header.registers[2].value = Threads;
header.shader.num_sh_registers = 255;
ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "truncated shader metadata");
header.shader.num_sh_registers = header.registers.size();
if (!budgetOnly) {
++header.registers[0].value;
ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "entry point is outside shader code");
--header.registers[0].value;
header.registers[2].value = 0;
ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "must be nonzero");
header.registers[2].value = Threads;
header.shader.num_sh_registers = 255;
ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "truncated shader metadata");
header.shader.num_sh_registers = header.registers.size();
}
const auto registered = [&] {
const auto registry = AgcDriver::DriverDetail::Driver::Get().RegisteredShaders();
Require(registry != nullptr, "lazy registration published no shader registry");
@@ -1615,6 +1663,7 @@ int main(int argc, char** argv) {
Run(*device);
RegisteredVertexWorkgroup(device->Target());
FailureCapture(*device);
DumpSelection(*device);
PendingRegistrationPreparation(*device);
AwaitedRegistrationPreparation();
Require(argc != 2 || std::string_view(argv[1]) != "--fail-before-registration", "injected failure before registration");
@@ -0,0 +1,498 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "prx/libc/include/GuestArena.hpp"
#include "prx/libc/include/GuestWriteWatch.hpp"
#include "Recompiler.hpp"
#include "Optimization/ResourceProgram.hpp"
#include "VulkanTestDevice.hpp"
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <sys/mman.h>
#endif
#include <array>
#include <bit>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <iostream>
#include <map>
#include <mutex>
#include <span>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::ColorTargetLayout;
using AgcDriver::Graphics::ColorTileMode;
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Width = 256;
constexpr std::uint32_t Height = 128;
constexpr std::uint32_t ScissorWidth = 128;
constexpr std::size_t BlockBytes = 65536;
constexpr std::uint32_t Unorm8888Info = 10u << 2u;
constexpr std::uint32_t Snorm8888Info = (10u << 2u) | (1u << 8u);
constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u;
constexpr std::uint32_t Float32Export = 9;
constexpr std::uint32_t PositionInputs = 0x300u;
constexpr std::uint32_t DepthInput = 0x400u;
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
};
constexpr std::array<std::array<float, 4>, 3> Triangle{{
{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}
}};
alignas(256) std::array<std::array<std::uint32_t, 64>, 64> Programs{};
std::size_t programCount = 0;
std::span<const std::uint32_t> Float32Program(std::array<float, 4> rgba) {
Require(programCount < Programs.size(), "too many test programs");
auto& code = Programs[programCount++];
std::size_t at = 0;
for (std::uint32_t vgpr = 0; vgpr < 4u; ++vgpr) {
code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u);
code[at++] = std::bit_cast<std::uint32_t>(rgba[vgpr]);
}
code[at++] = 0xf800180fu;
code[at++] = 0x07060504u;
code[at++] = 0xbf810000u;
return std::span<const std::uint32_t>(code.data(), at);
}
std::span<const std::uint32_t> DepthAsXProgram(std::array<float, 4> rgba) {
Require(programCount < Programs.size(), "too many test programs");
auto& code = Programs[programCount++];
std::size_t at = 0;
code[at++] = 0x7e080300u;
for (std::uint32_t vgpr = 1; vgpr < 4u; ++vgpr) {
code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u);
code[at++] = std::bit_cast<std::uint32_t>(rgba[vgpr]);
}
code[at++] = 0xf800180fu;
code[at++] = 0x07060504u;
code[at++] = 0xbf810000u;
return std::span<const std::uint32_t>(code.data(), at);
}
const std::array<std::array<float, 4>, 3>& TriangleAt(float z) {
static std::map<float, std::array<std::array<float, 4>, 3>> triangles;
return triangles.try_emplace(z, std::array<std::array<float, 4>, 3>{{{-1.0f, -1.0f, z, 1.0f}, {3.0f, -1.0f, z, 1.0f}, {-1.0f, 3.0f, z, 1.0f}}}).first->second;
}
struct DrawSetup {
std::uint32_t inputs = 0;
std::uint32_t shaderControl = 0;
std::span<const std::array<float, 4>> vertices = Triangle;
std::uint64_t depth = 0;
bool depthWrite = false;
bool withoutInterlock = false;
};
struct Block {
explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) {
#ifdef _WIN32
if (watched) {
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
} else {
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
}
#else
if (watched) {
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
Require(raw != MAP_FAILED, "cannot map the color target");
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
if (aligned != begin) munmap(raw, aligned - begin);
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
data = reinterpret_cast<std::uint8_t*>(aligned);
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
} else {
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
}
#endif
Require(data != nullptr, "cannot allocate the color target");
Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched");
GuestAllocations::Mutation mutation;
mutation.Add(data, bytes, true, true, true);
}
~Block() {
AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release");
{
GuestAllocations::Mutation mutation;
mutation.Remove(data);
}
#ifdef _WIN32
if (watched) {
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
} else {
VirtualFree(data, 0, MEM_RELEASE);
}
#else
if (watched) {
munmap(data, bytes);
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
} else {
std::free(data);
}
#endif
}
Block(const Block&) = delete;
Block& operator=(const Block&) = delete;
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
std::size_t bytes;
bool watched;
std::uint8_t* data = nullptr;
};
struct BlendCase {
VkBlendFactor colorSource;
VkBlendFactor colorDestination;
VkBlendOp colorOperation;
VkBlendFactor alphaSource;
VkBlendFactor alphaDestination;
VkBlendOp alphaOperation;
VkColorComponentFlags writeMask;
std::array<float, 4> constants;
bool snorm;
};
void Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTarget color, const BlendCase& blendCase, std::uint32_t exportFormat, std::span<const std::uint32_t> pixelCode, const DrawSetup& setup = {}) {
auto target = device.Target();
std::vector<std::uint32_t> capabilities(target.supportedCapabilities.begin(), target.supportedCapabilities.end());
if (setup.withoutInterlock) std::erase(capabilities, static_cast<std::uint32_t>(spv::CapabilityFragmentShaderPixelInterlockEXT));
target.supportedCapabilities = capabilities;
constexpr std::uint32_t waveSize = 64;
std::vector<std::uint32_t> vertexUserData(4, 0u);
const auto address = reinterpret_cast<std::uintptr_t>(setup.vertices.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>(setup.vertices.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());
color.partialWriteMask = AgcDriver::Graphics::UnclampedBlendMask;
AgcDriver::Graphics::State state{};
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt};
state.colors = {color};
state.color = color;
state.hasColorTarget = true;
state.renderExtent = {Width, Height};
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
state.negativeOneToOne = false;
state.scissor = {{0, 0}, {ScissorWidth, Height}};
state.cullMode = VK_CULL_MODE_NONE;
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
VkPipelineColorBlendAttachmentState blend{};
blend.blendEnable = VK_TRUE;
blend.srcColorBlendFactor = blendCase.colorSource;
blend.dstColorBlendFactor = blendCase.colorDestination;
blend.colorBlendOp = blendCase.colorOperation;
blend.srcAlphaBlendFactor = blendCase.alphaSource;
blend.dstAlphaBlendFactor = blendCase.alphaDestination;
blend.alphaBlendOp = blendCase.alphaOperation;
blend.colorWriteMask = blendCase.writeMask;
state.blends = {blend};
state.blend = blend;
state.blendConstants = blendCase.constants;
if (setup.depth != 0) {
state.depth = AgcDriver::Graphics::DepthTarget{setup.depth, 0, {Width, Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0, false};
state.depthTest = true;
state.depthWrite = setup.depthWrite;
state.depthCompare = VK_COMPARE_OP_LESS;
}
const auto codeAddress = reinterpret_cast<std::uintptr_t>(pixelCode.data());
const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, setup.inputs}, {0x1b4, setup.inputs}, {0x203, setup.shaderControl}, {0x1c5, exportFormat}};
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}};
ShaderRecompiler::RecompileRequest fragment{
{ShaderStage::Fragment, codeAddress, pixelCode, 0, {}},
{waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory},
target,
PixelPushLayout(vertexPush, target)
};
const auto handle = ShaderRecompiler::PrepareShader(fragment);
AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}};
snapshot.prepared->entries.push_back({0, handle});
fragment.shader.code = snapshot.code;
fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state));
fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state);
fragment.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(state);
Require(!fragment.context.pixel->pixelKillEnable && !fragment.context.pixel->earlyZ, "the test pixel shader does not kill");
Require(fragment.context.pixel->targetMerge.blend.enabled && fragment.context.pixel->targetMerge.mask == AgcDriver::Graphics::UnclampedBlendMask && fragment.context.pixel->targetMerge.blend.snorm == blendCase.snorm, "the draw's target merge does not describe the unclamped blend");
const ShaderRecompiler::SrtRuntime runtime{};
const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment);
const auto pixelResult = invocation.Materialize(*invocation.Capture(runtime));
const bool registered = pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId;
Require(!registered, "the blending draw reused the registered pixel shader");
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
{ShaderStage::Vertex, &vertexResult, 0},
{ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)}
}};
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(setup.vertices.size()), 0, 1, 0, false};
device.Draw(state, draw, shaders);
}
struct Case {
const char* name;
BlendCase blend;
std::array<float, 4> rgba;
bool distinguishesClamping;
};
constexpr int Tolerance = 1;
AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, bool snorm) {
AgcDriver::Registers cx;
cx[0x318] = static_cast<std::uint32_t>(block.Address() >> 8u);
cx[0x31b] = 0;
cx[0x31c] = snorm ? Snorm8888Info : Unorm8888Info;
cx[0x31d] = 0;
cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u);
cx[0x3b8] = TiledAttrib3;
cx[0x390] = static_cast<std::uint32_t>(block.Address() >> 40u);
auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
Require(color.format == (snorm ? VK_FORMAT_R8G8B8A8_SNORM : VK_FORMAT_R8G8B8A8_UNORM) && color.componentMapping == 0xe4u && color.packing == ShaderRecompiler::ColorExportPacking::None && color.tileMode == ColorTileMode::RenderTarget && color.bytes == block.bytes, "the 8_8_8_8 target did not decode as expected");
return color;
}
float Factor(VkBlendFactor kind, std::uint32_t channel, bool alpha, const std::array<float, 4>& source, const std::array<float, 4>& destination, const std::array<float, 4>& constants) {
const std::uint32_t index = alpha ? 3u : channel;
switch (kind) {
case VK_BLEND_FACTOR_ZERO: return 0.0f;
case VK_BLEND_FACTOR_ONE: return 1.0f;
case VK_BLEND_FACTOR_SRC_COLOR: return source[index];
case VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR: return 1.0f - source[index];
case VK_BLEND_FACTOR_DST_COLOR: return destination[index];
case VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR: return 1.0f - destination[index];
case VK_BLEND_FACTOR_SRC_ALPHA: return source[3];
case VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA: return 1.0f - source[3];
case VK_BLEND_FACTOR_DST_ALPHA: return destination[3];
case VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA: return 1.0f - destination[3];
case VK_BLEND_FACTOR_CONSTANT_COLOR: return constants[index];
case VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR: return 1.0f - constants[index];
case VK_BLEND_FACTOR_CONSTANT_ALPHA: return constants[3];
case VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA: return 1.0f - constants[3];
case VK_BLEND_FACTOR_SRC_ALPHA_SATURATE: return alpha ? 1.0f : std::fmin(source[3], 1.0f - destination[3]);
default: throw std::runtime_error("the oracle has no blend factor " + std::to_string(static_cast<int>(kind)));
}
}
float Equation(VkBlendOp operation, float source, float sourceFactor, float destination, float destinationFactor) {
volatile float weightedSource = source * sourceFactor;
volatile float weightedDestination = destination * destinationFactor;
switch (operation) {
case VK_BLEND_OP_ADD: return weightedSource + weightedDestination;
case VK_BLEND_OP_SUBTRACT: return weightedSource - weightedDestination;
case VK_BLEND_OP_REVERSE_SUBTRACT: return weightedDestination - weightedSource;
case VK_BLEND_OP_MIN: return std::fmin(source, destination);
case VK_BLEND_OP_MAX: return std::fmax(source, destination);
default: throw std::runtime_error("the oracle has no blend operation " + std::to_string(static_cast<int>(operation)));
}
}
float Decode(std::uint8_t byte, bool snorm) {
return snorm ? std::fmax(static_cast<float>(static_cast<std::int8_t>(byte)) / 127.0f, -1.0f) : static_cast<float>(byte) / 255.0f;
}
std::uint8_t Encode(float value, bool snorm) {
if (std::isnan(value)) return 0;
if (snorm) return static_cast<std::uint8_t>(static_cast<std::int8_t>(std::nearbyint(static_cast<double>(std::fmin(std::fmax(value, -1.0f), 1.0f)) * 127.0)));
return static_cast<std::uint8_t>(std::nearbyint(static_cast<double>(std::fmin(std::fmax(value, 0.0f), 1.0f)) * 255.0));
}
std::array<std::uint8_t, 4> Blended(const BlendCase& blend, const std::array<float, 4>& rgba, const std::array<std::uint8_t, 4>& stored, bool clampInputs) {
std::array<float, 4> source = rgba;
std::array<float, 4> destination{};
std::array<float, 4> constants = blend.constants;
const float low = blend.snorm ? -1.0f : 0.0f;
for (std::uint32_t channel = 0; channel < 4u; ++channel) destination[channel] = Decode(stored[channel], blend.snorm);
if (clampInputs) {
for (auto& value : source) value = std::fmin(std::fmax(value, low), 1.0f);
for (auto& value : constants) value = std::fmin(std::fmax(value, low), 1.0f);
}
std::array<std::uint8_t, 4> result = stored;
for (std::uint32_t channel = 0; channel < 4u; ++channel) {
if ((blend.writeMask & (1u << channel)) == 0u) continue;
const bool alpha = channel == 3u;
const auto sourceFactor = Factor(alpha ? blend.alphaSource : blend.colorSource, channel, alpha, source, destination, constants);
const auto destinationFactor = Factor(alpha ? blend.alphaDestination : blend.colorDestination, channel, alpha, source, destination, constants);
const float clampedFactorSource = clampInputs ? std::fmin(std::fmax(sourceFactor, 0.0f), 1.0f) : sourceFactor;
const float clampedFactorDestination = clampInputs ? std::fmin(std::fmax(destinationFactor, 0.0f), 1.0f) : destinationFactor;
result[channel] = Encode(Equation(alpha ? blend.alphaOperation : blend.colorOperation, source[channel], clampedFactorSource, destination[channel], clampedFactorDestination), blend.snorm);
}
return result;
}
std::vector<std::uint8_t> ReadBack(AgcDriver::VulkanDevice& device, const Block& block) {
AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back");
device.WaitIdle();
std::vector<std::uint8_t> stored(block.bytes);
AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1);
return stored;
}
std::vector<std::uint8_t> Fill(Block& block, std::uint32_t seed) {
std::vector<std::uint8_t> bytes(block.bytes);
for (std::size_t index = 0; index < bytes.size(); ++index) bytes[index] = static_cast<std::uint8_t>(((index + seed) * 0x9e3779b9u) >> 23u);
std::memcpy(block.data, bytes.data(), block.bytes);
return bytes;
}
std::array<std::uint8_t, 4> Texel(const std::vector<std::uint8_t>& bytes, const ColorTargetLayout& layout, std::uint32_t x, std::uint32_t y) {
const auto offset = layout.Offset(x, y);
return {bytes[offset], bytes[offset + 1u], bytes[offset + 2u], bytes[offset + 3u]};
}
struct Outcome {
bool distinguished = false;
};
int Difference(std::uint8_t left, std::uint8_t right, bool snorm) {
return snorm ? std::abs(static_cast<int>(static_cast<std::int8_t>(left)) - static_cast<int>(static_cast<std::int8_t>(right))) : std::abs(static_cast<int>(left) - static_cast<int>(right));
}
Outcome Expect(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector<std::uint8_t>& before, bool snorm, std::uint32_t writeMask, const std::string& what, const std::function<std::array<std::uint8_t, 4>(const std::array<std::uint8_t, 4>&)>& expected, const std::function<std::array<std::uint8_t, 4>(const std::array<std::uint8_t, 4>&)>& clamped) {
const auto stored = ReadBack(device, block);
Outcome outcome;
for (std::uint32_t y = 0; y < Height; ++y) {
for (std::uint32_t x = 0; x < Width; ++x) {
const auto was = Texel(before, layout, x, y);
const bool inside = x < ScissorWidth;
const auto want = inside ? expected(was) : was;
const auto got = Texel(stored, layout, x, y);
if (inside && clamped(was) != want) outcome.distinguished = true;
for (std::uint32_t channel = 0; channel < 4u; ++channel) {
const bool written = inside && (writeMask & (1u << channel)) != 0u;
if (Difference(got[channel], want[channel], snorm) <= (written ? Tolerance : 0)) continue;
char detail[256];
std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds (%u, %u, %u, %u), expected (%u, %u, %u, %u) from (%u, %u, %u, %u)", x, y, inside ? "inside" : "outside", got[0], got[1], got[2], got[3], want[0], want[1], want[2], want[3], was[0], was[1], was[2], was[3]);
throw std::runtime_error(what + detail);
}
}
}
return outcome;
}
void RunCase(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color, const Case& testCase, std::uint32_t seed) {
const auto before = Fill(block, seed);
Draw(device, color, testCase.blend, Float32Export, Float32Program(testCase.rgba));
const auto outcome = Expect(device, block, layout, before, testCase.blend.snorm, testCase.blend.writeMask, std::string("unclamped blend ") + testCase.name,
[&](const std::array<std::uint8_t, 4>& was) { return Blended(testCase.blend, testCase.rgba, was, false); },
[&](const std::array<std::uint8_t, 4>& was) { return Blended(testCase.blend, testCase.rgba, was, true); });
Require(outcome.distinguished == testCase.distinguishesClamping, std::string("unclamped blend ") + testCase.name + (testCase.distinguishesClamping ? " does not differ from Vulkan's clamped blend, so it cannot catch a clamping approximation" : " differs from Vulkan's clamped blend, which the case declares equal"));
}
void Overdraw(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color, float first, float last, std::uint32_t seed) {
constexpr std::uint32_t Layers = 16;
static std::map<float, std::vector<std::array<float, 4>>> stacks;
auto& vertices = stacks[last];
std::vector<float> depths;
for (std::uint32_t layer = 0; layer < Layers; ++layer) {
const float z = layer % 2u == Layers % 2u ? first : last;
depths.push_back(z);
const auto& triangle = TriangleAt(z);
vertices.insert(vertices.end(), triangle.begin(), triangle.end());
}
BlendCase blend{VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, 0xf, {}, false};
const auto before = Fill(block, seed);
Draw(device, color, blend, Float32Export, DepthAsXProgram({0.0f, 0.4f, 0.6f, 0.5f}), {DepthInput, 0u, vertices});
Expect(device, block, layout, before, false, blend.writeMask, "one draw of 16 full-screen triangles alternating z " + std::to_string(first) + " and " + std::to_string(last) + " blended into a UNORM target",
[&](const std::array<std::uint8_t, 4>& was) {
auto state = was;
for (const auto z : depths) state = Blended(blend, {z, 0.4f, 0.6f, 0.5f}, state, false);
return state;
},
[&](const std::array<std::uint8_t, 4>& was) { return was; });
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
if (!TargetHasCapability(device->Target(), spv::CapabilityFragmentShaderPixelInterlockEXT)) {
std::puts("skipped, the device lacks fragmentShaderPixelInterlock");
return VulkanTestSkipped;
}
const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 4);
Block block(layout.Bytes());
const auto unorm = DecodeTarget(block, false);
const auto snorm = DecodeTarget(block, true);
constexpr VkColorComponentFlags All = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
constexpr auto Zero = VK_BLEND_FACTOR_ZERO;
constexpr auto One = VK_BLEND_FACTOR_ONE;
constexpr auto Add = VK_BLEND_OP_ADD;
const std::array<float, 4> constants{1.5f, -0.25f, 0.5f, 2.0f};
const std::array<Case, 14> unormCases{{
{"SRC_ALPHA, ONE_MINUS_SRC_ALPHA (the Ghost of Yotei title scene setup) with an out-of-range source", {VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, Add, One, Zero, Add, All, {}, false}, {0.75f, -0.5f, 2.0f, 1.5f}, true},
{"SRC_COLOR, ONE_MINUS_SRC_COLOR", {VK_BLEND_FACTOR_SRC_COLOR, VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR, Add, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, Add, All, {}, false}, {1.5f, -0.25f, 0.5f, 0.75f}, true},
{"DST_COLOR, ZERO", {VK_BLEND_FACTOR_DST_COLOR, Zero, Add, VK_BLEND_FACTOR_DST_ALPHA, Zero, Add, All, {}, false}, {2.0f, 0.5f, 3.0f, 1.25f}, true},
{"ONE, ONE with a negative source", {One, One, Add, One, One, Add, All, {}, false}, {-0.5f, 0.25f, -2.0f, -0.75f}, true},
{"SUBTRACT ONE, ONE with a large source", {One, One, VK_BLEND_OP_SUBTRACT, One, One, VK_BLEND_OP_SUBTRACT, All, {}, false}, {1.5f, 0.25f, 3.0f, 2.0f}, true},
{"REVERSE_SUBTRACT SRC_ALPHA, ONE", {VK_BLEND_FACTOR_SRC_ALPHA, One, VK_BLEND_OP_REVERSE_SUBTRACT, One, One, VK_BLEND_OP_REVERSE_SUBTRACT, All, {}, false}, {0.5f, 2.0f, 0.25f, 2.0f}, true},
{"MIN", {One, One, VK_BLEND_OP_MIN, One, One, VK_BLEND_OP_MIN, All, {}, false}, {-0.5f, 0.5f, 2.0f, 0.25f}, false},
{"MAX", {Zero, Zero, VK_BLEND_OP_MAX, Zero, Zero, VK_BLEND_OP_MAX, All, {}, false}, {-0.5f, 0.5f, 2.0f, 0.25f}, false},
{"CONSTANT_COLOR, ONE_MINUS_CONSTANT_COLOR with out-of-range constants", {VK_BLEND_FACTOR_CONSTANT_COLOR, VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR, Add, VK_BLEND_FACTOR_CONSTANT_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA, Add, All, constants, false}, {0.5f, 0.75f, 0.25f, 0.5f}, true},
{"SRC_ALPHA_SATURATE, ONE", {VK_BLEND_FACTOR_SRC_ALPHA_SATURATE, One, Add, One, One, Add, All, {}, false}, {1.5f, 2.0f, 0.5f, 3.0f}, true},
{"DST_ALPHA, ONE_MINUS_DST_ALPHA", {VK_BLEND_FACTOR_DST_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA, Add, One, Zero, Add, All, {}, false}, {2.0f, -1.0f, 0.5f, 0.5f}, true},
{"separate alpha equation: color ADD SRC_ALPHA, alpha MAX", {VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, Add, One, One, VK_BLEND_OP_MAX, All, {}, false}, {1.25f, 0.5f, 0.75f, 1.75f}, true},
{"write mask R and B", {VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, Add, One, Zero, Add, VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_B_BIT, {}, false}, {0.75f, 2.0f, 1.5f, 1.5f}, true},
{"write mask none", {One, One, Add, One, One, Add, 0, {}, false}, {0.75f, 2.0f, 1.5f, 1.5f}, false},
}};
const std::array<Case, 4> snormCases{{
{"SNORM SRC_ALPHA, ONE_MINUS_SRC_ALPHA", {VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, Add, One, Zero, Add, All, {}, true}, {-0.75f, 1.5f, -2.0f, 1.5f}, true},
{"SNORM DST_COLOR, ZERO", {VK_BLEND_FACTOR_DST_COLOR, Zero, Add, VK_BLEND_FACTOR_DST_ALPHA, Zero, Add, All, {}, true}, {-2.0f, 0.5f, 3.0f, -1.25f}, true},
{"SNORM ONE, ONE", {One, One, Add, One, One, Add, All, {}, true}, {-1.5f, 0.25f, -0.5f, 2.0f}, true},
{"SNORM SUBTRACT ONE, ONE", {One, One, VK_BLEND_OP_SUBTRACT, One, One, VK_BLEND_OP_SUBTRACT, All, {}, true}, {1.5f, -2.0f, 0.25f, 0.5f}, true},
}};
std::uint32_t seed = 1;
for (const auto& testCase : unormCases) RunCase(*device, block, layout, unorm, testCase, seed++);
for (const auto& testCase : snormCases) RunCase(*device, block, layout, snorm, testCase, seed++);
std::string refusal;
try {
Draw(*device, unorm, unormCases[0].blend, Float32Export, Float32Program(unormCases[0].rgba), {.withoutInterlock = true});
} catch (const std::exception& error) {
refusal = error.what();
}
Require(refusal.find("blending into an unclamped normalized color target needs the fragmentShaderPixelInterlock feature, which the device lacks") != std::string::npos, "a device without fragmentShaderPixelInterlock did not refuse the unclamped blend: " + refusal);
Overdraw(*device, block, layout, unorm, 0.2f, 0.8f, 401u);
Overdraw(*device, block, layout, unorm, 0.8f, 0.2f, 402u);
std::printf("unclamped normalized color blend tests passed: %zu UNORM draws, %zu SNORM draws, 2 same-draw overdraws of 16 layers, 1 refusal without pixel interlock\n", unormCases.size(), snormCases.size());
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
@@ -194,6 +194,10 @@ int main(int argc, char** argv) {
}
int failures = 0;
for (int i = 1; i < argc; ++i) {
if (std::string(argv[i]) == "--gcn") {
ShaderRecompiler::SetGcnGuestShaders(true);
continue;
}
if (std::string(argv[i]) == "--dis") {
g_disassemble = true;
continue;
+3 -1
View File
@@ -1,7 +1,9 @@
add_library(libSceAudioIn SHARED EXCLUDE_FROM_ALL Export.cpp)
target_include_directories(libSceAudioIn PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR})
target_link_libraries(libSceAudioIn PRIVATE SDL2-static)
target_link_options(libSceAudioIn PRIVATE LINKER:--exclude-libs,ALL)
if(NOT APPLE)
target_link_options(libSceAudioIn PRIVATE LINKER:--exclude-libs,ALL)
endif()
configure_windows_unwind(libSceAudioIn)
if(BUILD_TESTING)
@@ -8,6 +8,7 @@
static constexpr std::uint32_t MAX_SUPPORTED_3D_OUTPUT = 2;
static constexpr int SCE_AUDIO_OUT2_ERROR_INVALID_PARAM = static_cast<int>(0x80268001);
static constexpr int SCE_AUDIO_OUT2_ERROR_NOT_INITIALIZED = static_cast<int>(0x80268006);
static std::atomic<bool> g_chatEnabled{false};
@@ -37,6 +38,7 @@ int APS5_VABI sceAudioOut2SetSystemDebugState(const AudioOut2SystemDebugStatePar
constexpr std::uint32_t DebugStateIdUnmodelled = 0x20;
constexpr std::size_t LoudnessParamSize = 8;
constexpr std::size_t UnmodelledParamSize = 4;
if (!AudioOut2IsInitialized_nid_no_patch()) return SCE_AUDIO_OUT2_ERROR_NOT_INITIALIZED;
if (param == nullptr) return ErrorInvalidPointer;
if (param->debug_state_id == DebugStateIdUnmodelled && param->param_size == UnmodelledParamSize) NotImplemented_nid_no_patch(__func__);
if (param->debug_state_id != DebugStateIdLoudness || param->param_size != LoudnessParamSize) return SCE_AUDIO_OUT2_ERROR_INVALID_PARAM;
@@ -46,11 +48,13 @@ int APS5_VABI sceAudioOut2SetSystemDebugState(const AudioOut2SystemDebugStatePar
}
int APS5_VABI sceAudioOut2EnableChat(void) {
if (!AudioOut2IsInitialized_nid_no_patch()) return SCE_AUDIO_OUT2_ERROR_NOT_INITIALIZED;
if (!g_chatEnabled.exchange(true)) AUDIOOUT2_TRACE("t=%.3f EnableChat: chat audio is allowed alongside the title's; the host mixes no chat audio\n", AudioOut2TraceSeconds());
return 0;
}
int APS5_VABI sceAudioOut2DisableChat(void) {
if (!AudioOut2IsInitialized_nid_no_patch()) return SCE_AUDIO_OUT2_ERROR_NOT_INITIALIZED;
if (g_chatEnabled.exchange(false)) AUDIOOUT2_TRACE("t=%.3f DisableChat: chat audio is no longer allowed alongside the title's\n", AudioOut2TraceSeconds());
return 0;
}
@@ -85,6 +85,22 @@ void TestMasteringBeforeInitialize() {
Require(sceAudioOut2MasteringSetParam(nullptr, 0, 2) == errorNotInitialized, "the AudioOut2 initialization check comes before the parameter checks");
}
void TestSystemBeforeInitialize() {
std::uint32_t loudness[2] = {1, 0};
const auto set = [](std::uint32_t id, void* data, std::size_t size) {
const AudioOut2SystemDebugStateParam param{id, 0, data, size};
return sceAudioOut2SetSystemDebugState(&param);
};
Require(set(1, loudness, 8) == errorNotInitialized, "a debug state before AudioOut2 initialization is refused");
Require(sceAudioOut2SetSystemDebugState(nullptr) == errorNotInitialized, "the AudioOut2 initialization check comes before the null debug state check");
Require(set(2, loudness, 8) == errorNotInitialized, "the AudioOut2 initialization check comes before the debug state id check");
Require(set(1, loudness, 4) == errorNotInitialized, "the AudioOut2 initialization check comes before the debug state size check");
Require(set(0x20, loudness, 4) == errorNotInitialized, "debug state id 0x20 of size 4 before AudioOut2 initialization is refused");
Require(set(1, nullptr, 8) == errorNotInitialized, "a loudness debug state without a param before AudioOut2 initialization is refused");
Require(sceAudioOut2EnableChat() == errorNotInitialized, "enabling chat before AudioOut2 initialization is refused");
Require(sceAudioOut2DisableChat() == errorNotInitialized, "disabling chat before AudioOut2 initialization is refused");
}
void TestMasteringInit() {
const AudioOut2MasteringParamsHeader params{1u};
AudioOut2MasteringStates storage{};
@@ -246,6 +262,7 @@ void TestLowLatencyService() {
int main() {
TestMasteringBeforeInitialize();
TestSystemBeforeInitialize();
Require(sceAudioOut2Initialize() == 0, "initialization must succeed");
Require(sceAudioOut2Initialize() == static_cast<int>(0x80268004), "a second AudioOut2 initialization fails");
Require(sceAudioOutInit() == 0, "the first legacy initialization succeeds after AudioOut2's");
+162 -33
View File
@@ -1,55 +1,181 @@
#include "SceTypes.hpp"
#include "prx/libSceBeisobmf/include/Api.hpp"
#include "prx/libc/include/General.hpp"
#include <cstddef>
#include <cstdint>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include <array>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <unordered_set>
namespace {
std::uintptr_t FieldAddress(const void *object, std::size_t offset) {
const auto address = reinterpret_cast<std::uintptr_t>(object);
if (address == 0) throw std::invalid_argument("isobmf object pointer is null");
if (offset > std::numeric_limits<std::uintptr_t>::max() - address)
throw std::runtime_error("isobmf field address overflows");
return address + offset;
}
template <typename TValue>
TValue ReadAt(std::uintptr_t address) {
TValue value{};
if (address == 0 ||
!GuestCopyIn_nid_no_patch(&value, reinterpret_cast<const void *>(address), sizeof(value)))
throw std::runtime_error("isobmf guest field is unreadable");
return value;
}
template <typename TValue>
TValue ReadField(const void *object, std::size_t offset) {
return ReadAt<TValue>(FieldAddress(object, offset));
}
std::uintptr_t ReadPointer(const void *object, std::size_t offset) {
return ReadField<std::uintptr_t>(object, offset);
}
void WritePointer(void *object, std::size_t offset, std::uintptr_t value) {
const auto address = FieldAddress(object, offset);
if (!GuestCopyOut_nid_no_patch(reinterpret_cast<void *>(address), &value, sizeof(value)))
throw std::runtime_error("isobmf guest cursor is not writable");
}
std::array<char, 0x40> CopyRequestedName(const char *name) {
std::array<char, 0x40> result{};
const auto address = reinterpret_cast<std::uintptr_t>(name);
if (address == 0) throw std::invalid_argument("isobmf member name is null");
for (std::size_t i = 0; i < result.size(); ++i) {
if (i > std::numeric_limits<std::uintptr_t>::max() - address ||
!GuestCopyIn_nid_no_patch(&result[i], reinterpret_cast<const void *>(address + i),
sizeof(result[i])))
throw std::runtime_error("isobmf member name is unreadable");
if (result[i] == '\0') break;
}
return result;
}
std::uintptr_t FindMember(void *box, const char *name) {
if (!box || !name)
throw std::invalid_argument("isobmf member lookup requires a box and name");
const auto requestedName = CopyRequestedName(name);
const auto boxAddress = reinterpret_cast<std::uintptr_t>(box);
WritePointer(box, 0x50, ReadPointer(box, 0x40));
std::unordered_set<std::uintptr_t> visited;
auto member = ReadPointer(box, 0x50);
while (member != 0) {
if (!visited.insert(member).second)
throw std::runtime_error("isobmf member list contains a cycle");
std::array<char, 0x40> memberName{};
const auto nameAddress = FieldAddress(reinterpret_cast<const void *>(member), 0x18);
if (!GuestCopyIn_nid_no_patch(memberName.data(),
reinterpret_cast<const void *>(nameAddress), memberName.size()))
throw std::runtime_error("isobmf member key is unreadable");
if (std::strncmp(memberName.data(), requestedName.data(), memberName.size()) == 0)
return member;
member = ReadPointer(reinterpret_cast<const void *>(member), 0x10);
WritePointer(reinterpret_cast<void *>(boxAddress), 0x50, member);
}
return 0;
}
void *MemberPointer(std::uintptr_t member) {
const auto type =
ReadField<std::uint32_t>(reinterpret_cast<const void *>(member), 0x6c);
if (type < 20 && ((0x97800u >> (type & 0x1f)) & 1u) != 0)
return reinterpret_cast<void *>(
ReadPointer(reinterpret_cast<const void *>(member), 0x60));
return reinterpret_cast<void *>(FieldAddress(reinterpret_cast<const void *>(member), 0x58));
}
void LogGuestAccessFailure(const char *function, const char *message) noexcept {
try {
APS5_LOG_ERR("%s failed safely: %s", function, message);
} catch (...) {
}
}
template <typename TValue, typename TFunction>
TValue CallSafely(const char *function, TValue fallback, TFunction operation) noexcept {
try {
return operation();
} catch (const std::exception &error) {
LogGuestAccessFailure(function, error.what());
return fallback;
} catch (...) {
LogGuestAccessFailure(function, "unknown guest memory error");
return fallback;
}
}
} // namespace
extern "C" {
APS5_DUMMY_FUN
int APS5_VABI isobmf_box_getchildbox_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
void *APS5_VABI isobmf_box_getchildbox_nid_postfix(void *box) {
return CallSafely(__func__, static_cast<void *>(nullptr), [&] {
return reinterpret_cast<void *>(ReadPointer(box, 0x10));
});
}
int APS5_VABI isobmf_box_getdiskoffset_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
std::uint64_t APS5_VABI isobmf_box_getdiskoffset_nid_postfix(void *box) {
return CallSafely(__func__, std::uint64_t{0}, [&] { return ReadField<std::uint64_t>(box, 0x80); });
}
int APS5_VABI isobmf_box_getflags32b_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
std::uint32_t APS5_VABI isobmf_box_getflags32b_nid_postfix(void *box) {
return CallSafely(__func__, std::uint32_t{0}, [&] {
if (!box) return std::uint32_t{0};
const auto member = FindMember(box, "flags");
if (!member) return std::uint32_t{0};
std::array<std::uint8_t, 3> value{};
const auto address = reinterpret_cast<std::uintptr_t>(MemberPointer(member));
if (!GuestCopyIn_nid_no_patch(value.data(), reinterpret_cast<const void *>(address),
value.size()))
throw std::runtime_error("isobmf flags payload is unreadable");
return static_cast<std::uint32_t>(value[0]) |
(static_cast<std::uint32_t>(value[1]) << 8) |
(static_cast<std::uint32_t>(value[2]) << 16);
});
}
int APS5_VABI isobmf_box_getmember_length_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
std::uint32_t APS5_VABI
isobmf_box_getmember_length_nid_postfix(void *box, const char *name) {
return CallSafely(__func__, std::uint32_t{0}, [&] {
const auto member = FindMember(box, name);
return member ? ReadField<std::uint32_t>(reinterpret_cast<const void *>(member), 0x68) : 0;
});
}
int APS5_VABI isobmf_box_getmember_ptr_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
void *APS5_VABI isobmf_box_getmember_ptr_nid_postfix(void *box,
const char *name) {
return CallSafely(__func__, static_cast<void *>(nullptr), [&] {
const auto member = FindMember(box, name);
return member ? MemberPointer(member) : nullptr;
});
}
int APS5_VABI isobmf_box_getmember_type_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
std::uint32_t APS5_VABI
isobmf_box_getmember_type_nid_postfix(void *box, const char *name) {
return CallSafely(__func__, std::uint32_t{0}, [&] {
const auto member = FindMember(box, name);
return member ? ReadField<std::uint32_t>(reinterpret_cast<const void *>(member), 0x6c) : 0;
});
}
int APS5_VABI isobmf_box_getnextbox_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
void *APS5_VABI isobmf_box_getnextbox_nid_postfix(void *box) {
return CallSafely(__func__, static_cast<void *>(nullptr), [&] {
return reinterpret_cast<void *>(ReadPointer(box, 0x20));
});
}
int APS5_VABI isobmf_box_getstate_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
std::uint32_t APS5_VABI isobmf_box_getstate_nid_postfix(void *box) {
return CallSafely(__func__, std::uint32_t{0}, [&] { return ReadField<std::uint32_t>(box, 0x8c); });
}
int APS5_VABI isobmf_box_gettype_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
std::uint32_t APS5_VABI isobmf_box_gettype_nid_postfix(void *box) {
return CallSafely(__func__, std::uint32_t{0}, [&] { return ReadField<std::uint32_t>(box, 0x90); });
}
int APS5_VABI isobmf_decoder_clear_nid_postfix(void) {
@@ -82,9 +208,12 @@ int APS5_VABI isobmf_decoder_getquerydata_nid_postfix(void) {
return 0;
}
int APS5_VABI isobmf_decoder_next_disk_offset_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
std::uint64_t APS5_VABI
isobmf_decoder_next_disk_offset_nid_postfix(void *decoder) {
return CallSafely(__func__, std::uint64_t{0xffffffff}, [&] {
if (!decoder) return std::uint64_t{0xffffffff};
return ReadField<std::uint64_t>(decoder, 0xc0);
});
}
int APS5_VABI isobmf_decoder_query_nid_postfix(void) {
+11
View File
@@ -0,0 +1,11 @@
if(BUILD_TESTING)
add_test_executable(guest_beisobmf_boxes_tests ${CMAKE_CURRENT_LIST_DIR}/../../tests/GuestBeisobmfBoxes.cpp)
target_include_directories(guest_beisobmf_boxes_tests PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../..)
target_link_libraries(guest_beisobmf_boxes_tests PRIVATE libSceBeisobmf libc)
configure_windows_unwind(guest_beisobmf_boxes_tests)
add_test(NAME guest_beisobmf_boxes COMMAND guest_beisobmf_boxes_tests)
if(WIN32)
set_tests_properties(guest_beisobmf_boxes PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceBeisobmf>")
endif()
endif()
+10 -10
View File
@@ -4,22 +4,22 @@
extern "C" {
int APS5_VABI isobmf_box_getchildbox_nid_postfix(void);
int APS5_VABI isobmf_box_getdiskoffset_nid_postfix(void);
int APS5_VABI isobmf_box_getflags32b_nid_postfix(void);
int APS5_VABI isobmf_box_getmember_length_nid_postfix(void);
int APS5_VABI isobmf_box_getmember_ptr_nid_postfix(void);
int APS5_VABI isobmf_box_getmember_type_nid_postfix(void);
int APS5_VABI isobmf_box_getnextbox_nid_postfix(void);
int APS5_VABI isobmf_box_getstate_nid_postfix(void);
int APS5_VABI isobmf_box_gettype_nid_postfix(void);
void *APS5_VABI isobmf_box_getchildbox_nid_postfix(void *box);
std::uint64_t APS5_VABI isobmf_box_getdiskoffset_nid_postfix(void *box);
std::uint32_t APS5_VABI isobmf_box_getflags32b_nid_postfix(void *box);
std::uint32_t APS5_VABI isobmf_box_getmember_length_nid_postfix(void *box, const char *name);
void *APS5_VABI isobmf_box_getmember_ptr_nid_postfix(void *box, const char *name);
std::uint32_t APS5_VABI isobmf_box_getmember_type_nid_postfix(void *box, const char *name);
void *APS5_VABI isobmf_box_getnextbox_nid_postfix(void *box);
std::uint32_t APS5_VABI isobmf_box_getstate_nid_postfix(void *box);
std::uint32_t APS5_VABI isobmf_box_gettype_nid_postfix(void *box);
int APS5_VABI isobmf_decoder_clear_nid_postfix(void);
int APS5_VABI isobmf_decoder_create_nid_postfix(void);
int APS5_VABI isobmf_decoder_decode_nid_postfix(void);
int APS5_VABI isobmf_decoder_destroy_nid_postfix(void);
int APS5_VABI isobmf_decoder_get_decoder_tree_nid_postfix(void);
int APS5_VABI isobmf_decoder_getquerydata_nid_postfix(void);
int APS5_VABI isobmf_decoder_next_disk_offset_nid_postfix(void);
std::uint64_t APS5_VABI isobmf_decoder_next_disk_offset_nid_postfix(void *decoder);
int APS5_VABI isobmf_decoder_query_nid_postfix(void);
int APS5_VABI isobmf_memmanager_set_nid_postfix(void);
+59
View File
@@ -1,3 +1,5 @@
#include "prx/libc/include/general/ExportMacros.hpp"
#include "prx/libSceBgft/include/InventoryExports.hpp"
#include "prx/libSceBgft/include/Api.hpp"
#include <cstdint>
#include <cstddef>
@@ -419,3 +421,60 @@ int APS5_VABI sceBgftServiceIntUploadSetUploadError() {
}
}
extern "C" {
int APS5_VABI sceBgftServiceIntDownloadClearAllInstalled() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceBgftServiceIntDownloadClearInstalled() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceBgftServiceIntDownloadPauseGroupTasks() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceBgftServiceIntDownloadResumeGroupTasks() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceBgftServiceIntDownloadStartGroupTasks() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceBgftServiceIntDownloadUnregisterCompleteTask() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceBgftServiceIntUploadUnregisterCompleteTask() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI libSceBgftUnknown_3cvL6QTobNg_nid_no_patch() {
NotImplemented_nid_no_patch(__func__);
}
APS5_EXPORT("3cvL6QTobNg", libSceBgftUnknown_3cvL6QTobNg_nid_no_patch);
int APS5_VABI libSceBgftUnknown_4JQKNtsVDpg_nid_no_patch() {
NotImplemented_nid_no_patch(__func__);
}
APS5_EXPORT("4JQKNtsVDpg", libSceBgftUnknown_4JQKNtsVDpg_nid_no_patch);
int APS5_VABI libSceBgftUnknown_85axuUkg9eE_nid_no_patch() {
NotImplemented_nid_no_patch(__func__);
}
APS5_EXPORT("85axuUkg9eE", libSceBgftUnknown_85axuUkg9eE_nid_no_patch);
int APS5_VABI libSceBgftUnknown_fJnnIc5sHOU_nid_no_patch() {
NotImplemented_nid_no_patch(__func__);
}
APS5_EXPORT("fJnnIc5sHOU", libSceBgftUnknown_fJnnIc5sHOU_nid_no_patch);
int APS5_VABI libSceBgftUnknown_tBOVSvi9EjY_nid_no_patch() {
NotImplemented_nid_no_patch(__func__);
}
APS5_EXPORT("tBOVSvi9EjY", libSceBgftUnknown_tBOVSvi9EjY_nid_no_patch);
}
@@ -0,0 +1,20 @@
#pragma once
#include "SceTypes.hpp"
extern "C" {
int APS5_VABI sceBgftServiceIntDownloadClearAllInstalled();
int APS5_VABI sceBgftServiceIntDownloadClearInstalled();
int APS5_VABI sceBgftServiceIntDownloadPauseGroupTasks();
int APS5_VABI sceBgftServiceIntDownloadResumeGroupTasks();
int APS5_VABI sceBgftServiceIntDownloadStartGroupTasks();
int APS5_VABI sceBgftServiceIntDownloadUnregisterCompleteTask();
int APS5_VABI sceBgftServiceIntUploadUnregisterCompleteTask();
int APS5_VABI libSceBgftUnknown_3cvL6QTobNg_nid_no_patch();
int APS5_VABI libSceBgftUnknown_4JQKNtsVDpg_nid_no_patch();
int APS5_VABI libSceBgftUnknown_85axuUkg9eE_nid_no_patch();
int APS5_VABI libSceBgftUnknown_fJnnIc5sHOU_nid_no_patch();
int APS5_VABI libSceBgftUnknown_tBOVSvi9EjY_nid_no_patch();
}
+1
View File
@@ -1,3 +1,4 @@
#pragma once
#include "prx/libSceBgft/include/Api.hpp"
#include "prx/libSceBgft/include/InventoryExports.hpp"
@@ -2431,7 +2431,14 @@ int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint16_t keycode, std::uin
}
int APS5_VABI sceConvertKeycodeGetCharacterFromKeyboardData(const KeyboardData* keyData, std::int32_t index, std::uint32_t state, std::uint32_t keyboardType, char16_t* character) {
NotImplemented_nid_no_patch(__func__);
if (keyData == nullptr || character == nullptr) return kErrInvalidAddress;
if (keyboardType == 0 || keyboardType > 39) return kErrInvalidKeyboardType;
if (keyboardType >= 31) throw std::runtime_error("sceConvertKeycodeGetCharacterFromKeyboardData: guest sdk version is unavailable");
if (index < 0 || index >= keyData->length || index >= static_cast<std::int32_t>(KEYBOARD_MAX_KEYCODES)) {
throw std::runtime_error("sceConvertKeycodeGetCharacterFromKeyboardData: result for an index outside the key data is unknown");
}
const std::uint16_t keycode = keyData->key_code[index];
*character = ConvertCharacter(keycode, state, keyboardType);
return 0;
}
+1 -3
View File
@@ -165,12 +165,10 @@ void FillWritingMetricsFromGlyph(FontWritingData& writing, const FontGlyphMetric
writing.rawGlyphMetrics = glyphMetrics;
writing.hasRawGlyphMetrics = true;
writing.step.advanceX = glyphMetrics.Horizontal.advance;
writing.step.advanceY = glyphMetrics.Vertical.advance;
writing.step.advanceY = 0.0f;
writing.step.GlyphMetrics = glyphMetrics;
writing.step.Positioning.x = writing.step.x;
writing.step.Positioning.y = writing.step.y;
writing.step.GlyphMetrics.width = writing.step.x;
writing.step.GlyphMetrics.height = writing.step.y;
}
bool PopulateWritingStep(FontWritingData& writing, std::size_t index) {
@@ -184,6 +184,11 @@ struct GuestFaceRec {
void* internal;
};
struct GuestPropIncreaseXHeight {
GuestFaceRec* face;
std::uint32_t limit;
};
static_assert(sizeof(GuestMatrix) == 32 && offsetof(GuestMatrix, xy) == 8 && offsetof(GuestMatrix, yx) == 16 && offsetof(GuestMatrix, yy) == 24);
static_assert(sizeof(GuestGeneric) == 16 && offsetof(GuestGeneric, finalizer) == 8);
static_assert(sizeof(GuestBitmapSize) == 32 && offsetof(GuestBitmapSize, size) == 8);
@@ -205,6 +210,7 @@ static_assert(offsetof(GuestFaceRec, num_fixed_sizes) == 56 && offsetof(GuestFac
static_assert(offsetof(GuestFaceRec, bbox) == 104 && offsetof(GuestFaceRec, units_per_EM) == 136 && offsetof(GuestFaceRec, underline_thickness) == 150);
static_assert(offsetof(GuestFaceRec, glyph) == 0x98 && offsetof(GuestFaceRec, size) == 0xa0 && offsetof(GuestFaceRec, charmap) == 0xa8);
static_assert(offsetof(GuestFaceRec, autohint) == 216 && offsetof(GuestFaceRec, internal) == 240 && sizeof(GuestFaceRec) == 248);
static_assert(sizeof(GuestPropIncreaseXHeight) == 16 && offsetof(GuestPropIncreaseXHeight, face) == 0 && offsetof(GuestPropIncreaseXHeight, limit) == 8);
}
@@ -3,6 +3,7 @@
#include "prx/libc/include/General.hpp"
#include FT_MODULE_H
#include FT_DRIVER_H
#include <cstdint>
#include <cstring>
@@ -16,7 +17,7 @@ static_assert(sizeof(FT_Bool) == sizeof(std::uint8_t));
namespace {
enum class PropertyType { Unsigned, Signed, Boolean, Darkening, Unsupported };
enum class PropertyType { Unsigned, Signed, Boolean, Darkening, IncreaseXHeight, Unsupported };
using DarkeningParameters = FT_Int[8];
PropertyType TypeOf(const char* moduleName, const char* propertyName) {
@@ -36,6 +37,7 @@ PropertyType TypeOf(const char* moduleName, const char* propertyName) {
std::strcmp(propertyName, "default-script") == 0)) {
return PropertyType::Unsigned;
}
if (autoHinter && std::strcmp(propertyName, "increase-x-height") == 0) return PropertyType::IncreaseXHeight;
return PropertyType::Unsupported;
}
@@ -47,6 +49,20 @@ int Read(FT_Library library, const char* moduleName, const char* propertyName, v
return error;
}
int ReadIncreaseXHeight(FT_Library library, const char* moduleName, const char* propertyName, void* output) {
GuestFaceRec* guestFace = nullptr;
std::memcpy(&guestFace, output, sizeof(guestFace));
const auto face = FaceOf(guestFace);
FT_Prop_IncreaseXHeight hostProp{};
hostProp.face = face ? face->host : nullptr;
const auto error = FT_Property_Get(library, moduleName, propertyName, &hostProp);
if (!error) {
const std::uint32_t limit = hostProp.limit;
std::memcpy(static_cast<char*>(output) + offsetof(GuestPropIncreaseXHeight, limit), &limit, sizeof(limit));
}
return error;
}
template <typename TValue>
int Write(FT_Library library, const char* moduleName, const char* propertyName, const void* input) {
TValue value{};
@@ -54,6 +70,16 @@ int Write(FT_Library library, const char* moduleName, const char* propertyName,
return FT_Property_Set(library, moduleName, propertyName, &value);
}
int WriteIncreaseXHeight(FT_Library library, const char* moduleName, const char* propertyName, const void* input) {
GuestPropIncreaseXHeight guestProp{};
std::memcpy(&guestProp, input, sizeof(guestProp));
const auto face = FaceOf(guestProp.face);
FT_Prop_IncreaseXHeight hostProp{};
hostProp.face = face ? face->host : nullptr;
hostProp.limit = guestProp.limit;
return FT_Property_Set(library, moduleName, propertyName, &hostProp);
}
}
extern "C" int APS5_VABI FT_Property_Get_nid_postfix(GuestLibrary* library, const char* moduleName,
@@ -65,6 +91,7 @@ extern "C" int APS5_VABI FT_Property_Get_nid_postfix(GuestLibrary* library, cons
case PropertyType::Signed: return Read<FT_Int32>(host, moduleName, propertyName, value);
case PropertyType::Boolean: return Read<FT_Bool>(host, moduleName, propertyName, value);
case PropertyType::Darkening: return Read<DarkeningParameters>(host, moduleName, propertyName, value);
case PropertyType::IncreaseXHeight: return ReadIncreaseXHeight(host, moduleName, propertyName, value);
default: NotImplemented_nid_no_patch(__func__);
}
}
@@ -78,6 +105,7 @@ extern "C" int APS5_VABI FT_Property_Set_nid_postfix(GuestLibrary* library, cons
case PropertyType::Signed: return Write<FT_Int32>(host, moduleName, propertyName, value);
case PropertyType::Boolean: return Write<FT_Bool>(host, moduleName, propertyName, value);
case PropertyType::Darkening: return Write<DarkeningParameters>(host, moduleName, propertyName, value);
case PropertyType::IncreaseXHeight: return WriteIncreaseXHeight(host, moduleName, propertyName, value);
default: NotImplemented_nid_no_patch(__func__);
}
}
@@ -15,6 +15,27 @@ static constexpr int ErrorInvalidArgument = static_cast<int>(0x80A00002);
static constexpr int ErrorAlreadyInitialized = static_cast<int>(0x80A00003);
static constexpr int ErrorNotInitialized = static_cast<int>(0x80A00004);
static constexpr std::size_t ProgramInfoSize = 0x134C;
static constexpr std::size_t MaxMetadataLength = 240;
static constexpr std::size_t MaxPresetDescriptionLength = 720;
static constexpr std::size_t MaxInvitationSessionIdLength = 45;
static constexpr std::size_t MaxSocialMessageLength = 360;
static constexpr std::size_t MaxSpoilerTags = 16;
static constexpr std::size_t MaxPresetCommands = 5;
static bool ValidFeedbackType(int type) {
return type >= 1 && type <= 3;
}
static bool ValidString(const char* text, std::size_t maxLength) {
return text != nullptr && std::memchr(text, 0, maxLength + 1) != nullptr;
}
static bool AllZero(const std::uint8_t* bytes, std::size_t size) {
for (std::size_t i = 0; i < size; i++) {
if (bytes[i] != 0) return false;
}
return true;
}
static_assert(sizeof(GameLiveStreamingStatus2) == 72);
@@ -87,13 +108,15 @@ int APS5_VABI sceGameLiveStreamingScreenSetMode(void) {
return 0;
}
int APS5_VABI sceGameLiveStreamingPermitLiveStreaming(void) {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceGameLiveStreamingPermitLiveStreaming(bool permit) {
std::lock_guard lock(g_mutex);
if (!g_initialized) return ErrorNotInitialized;
return 0;
}
int APS5_VABI sceGameLiveStreamingSetMaxBitrate(void) {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceGameLiveStreamingSetMaxBitrate(std::uint32_t bitrate) {
std::lock_guard lock(g_mutex);
if (!g_initialized) return ErrorNotInitialized;
return 0;
}
@@ -102,68 +125,98 @@ int APS5_VABI sceGameLiveStreamingGetCurrentStatus(GameLiveStreamingStatus* stat
return 0;
}
int APS5_VABI sceGameLiveStreamingEnableSocialFeedback(void) {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceGameLiveStreamingEnableSocialFeedback(bool enable) {
std::lock_guard lock(g_mutex);
if (!g_initialized) return ErrorNotInitialized;
return 0;
}
int APS5_VABI sceGameLiveStreamingGetSocialFeedbackMessagesCount(void) {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceGameLiveStreamingGetSocialFeedbackMessagesCount(int type, std::uint32_t* count) {
std::lock_guard lock(g_mutex);
if (!g_initialized) return ErrorNotInitialized;
if (count == nullptr || !ValidFeedbackType(type)) return ErrorInvalidArgument;
*count = 0;
return 0;
}
int APS5_VABI sceGameLiveStreamingGetSocialFeedbackMessages(void) {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceGameLiveStreamingGetSocialFeedbackMessages(int type, size_t capacity, GameLiveStreamingFeedbackMessage* messages, size_t* acquired) {
std::lock_guard lock(g_mutex);
if (!g_initialized) return ErrorNotInitialized;
if (messages == nullptr || acquired == nullptr || !ValidFeedbackType(type)) return ErrorInvalidArgument;
*acquired = 0;
return 0;
}
int APS5_VABI sceGameLiveStreamingPermitServerSideRecording(void) {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceGameLiveStreamingPermitServerSideRecording(bool permit) {
std::lock_guard lock(g_mutex);
if (!g_initialized) return ErrorNotInitialized;
return 0;
}
int APS5_VABI sceGameLiveStreamingSetMetadata(void) {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceGameLiveStreamingSetMetadata(const char* metadata) {
std::lock_guard lock(g_mutex);
if (!g_initialized) return ErrorNotInitialized;
if (!ValidString(metadata, MaxMetadataLength)) return ErrorInvalidArgument;
return 0;
}
int APS5_VABI sceGameLiveStreamingSetSpoilerTag(void) {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceGameLiveStreamingSetSpoilerTag(size_t count, const GameLiveStreamingSpoilerTag* tags) {
std::lock_guard lock(g_mutex);
if (!g_initialized) return ErrorNotInitialized;
if (tags == nullptr || count > MaxSpoilerTags) return ErrorInvalidArgument;
for (size_t i = 0; i < count; i++) {
if (!AllZero(tags[i].reserved, sizeof(tags[i].reserved))) return ErrorInvalidArgument;
}
return 0;
}
int APS5_VABI sceGameLiveStreamingClearSpoilerTag(void) {
NotImplemented_nid_no_patch(__func__);
std::lock_guard lock(g_mutex);
if (!g_initialized) return ErrorNotInitialized;
return 0;
}
int APS5_VABI sceGameLiveStreamingSetPresetSocialFeedbackCommands(void) {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceGameLiveStreamingSetPresetSocialFeedbackCommands(size_t count, const GameLiveStreamingPresetCommand* commands) {
std::lock_guard lock(g_mutex);
if (!g_initialized) return ErrorNotInitialized;
if (commands == nullptr || count > MaxPresetCommands) return ErrorInvalidArgument;
for (size_t i = 0; i < count; i++) {
if (!AllZero(commands[i].reserved, sizeof(commands[i].reserved))) return ErrorInvalidArgument;
}
return 0;
}
int APS5_VABI sceGameLiveStreamingClearPresetSocialFeedbackCommands(void) {
NotImplemented_nid_no_patch(__func__);
std::lock_guard lock(g_mutex);
if (!g_initialized) return ErrorNotInitialized;
return 0;
}
int APS5_VABI sceGameLiveStreamingSetPresetSocialFeedbackCommandsDescription(void) {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceGameLiveStreamingSetPresetSocialFeedbackCommandsDescription(const char* description) {
std::lock_guard lock(g_mutex);
if (!g_initialized) return ErrorNotInitialized;
if (!ValidString(description, MaxPresetDescriptionLength)) return ErrorInvalidArgument;
return 0;
}
int APS5_VABI sceGameLiveStreamingClearSocialFeedbackMessages(void) {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceGameLiveStreamingClearSocialFeedbackMessages(int type) {
std::lock_guard lock(g_mutex);
if (!g_initialized) return ErrorNotInitialized;
if (!ValidFeedbackType(type)) return ErrorInvalidArgument;
return 0;
}
int APS5_VABI sceGameLiveStreamingPostSocialMessage(void) {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceGameLiveStreamingPostSocialMessage(const char* message) {
std::lock_guard lock(g_mutex);
if (!g_initialized) return ErrorNotInitialized;
if (!ValidString(message, MaxSocialMessageLength)) return ErrorInvalidArgument;
return 0;
}
int APS5_VABI sceGameLiveStreamingSetCameraFrameSetting(void) {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceGameLiveStreamingSetCameraFrameSetting(const GameLiveStreamingCameraFrameSetting* setting) {
std::lock_guard lock(g_mutex);
if (!g_initialized) return ErrorNotInitialized;
if (setting == nullptr || !AllZero(setting->reserved, sizeof(setting->reserved))) return ErrorInvalidArgument;
return 0;
}
@@ -177,8 +230,10 @@ int APS5_VABI sceGameLiveStreamingSetDefaultServiceProviderPermission(void) {
return 0;
}
int APS5_VABI sceGameLiveStreamingSetInvitationSessionId(void) {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceGameLiveStreamingSetInvitationSessionId(const char* sessionId) {
std::lock_guard lock(g_mutex);
if (!g_initialized) return ErrorNotInitialized;
if (!ValidString(sessionId, MaxInvitationSessionIdLength)) return ErrorInvalidArgument;
return 0;
}
@@ -6,6 +6,31 @@
struct GameLiveStreamingProgramInfo;
struct GameLiveStreamingSpoilerTag {
char text[193];
std::uint8_t reserved[31];
};
static_assert(offsetof(GameLiveStreamingSpoilerTag, reserved) == 193);
static_assert(sizeof(GameLiveStreamingSpoilerTag) == 224);
struct GameLiveStreamingPresetCommand {
std::uint8_t body[197];
std::uint8_t reserved[31];
};
static_assert(offsetof(GameLiveStreamingPresetCommand, reserved) == 197);
static_assert(sizeof(GameLiveStreamingPresetCommand) == 228);
struct GameLiveStreamingCameraFrameSetting {
std::uint8_t head[9];
std::uint8_t reserved[31];
};
static_assert(offsetof(GameLiveStreamingCameraFrameSetting, reserved) == 9);
struct GameLiveStreamingFeedbackMessage;
struct GameLiveStreamingStatus {
std::uint8_t opaque00[12];
std::uint8_t reserved[60];
@@ -38,25 +63,25 @@ int APS5_VABI sceGameLiveStreamingScreenCloseSeparateMode(void);
int APS5_VABI sceGameLiveStreamingScreenConfigureSeparateMode(void);
int APS5_VABI sceGameLiveStreamingScreenOpenSeparateMode(void);
int APS5_VABI sceGameLiveStreamingScreenSetMode(void);
int APS5_VABI sceGameLiveStreamingPermitLiveStreaming(void);
int APS5_VABI sceGameLiveStreamingSetMaxBitrate(void);
int APS5_VABI sceGameLiveStreamingPermitLiveStreaming(bool permit);
int APS5_VABI sceGameLiveStreamingSetMaxBitrate(std::uint32_t bitrate);
int APS5_VABI sceGameLiveStreamingGetCurrentStatus(GameLiveStreamingStatus* status);
int APS5_VABI sceGameLiveStreamingEnableSocialFeedback(void);
int APS5_VABI sceGameLiveStreamingGetSocialFeedbackMessagesCount(void);
int APS5_VABI sceGameLiveStreamingGetSocialFeedbackMessages(void);
int APS5_VABI sceGameLiveStreamingPermitServerSideRecording(void);
int APS5_VABI sceGameLiveStreamingSetMetadata(void);
int APS5_VABI sceGameLiveStreamingSetSpoilerTag(void);
int APS5_VABI sceGameLiveStreamingEnableSocialFeedback(bool enable);
int APS5_VABI sceGameLiveStreamingGetSocialFeedbackMessagesCount(int type, std::uint32_t* count);
int APS5_VABI sceGameLiveStreamingGetSocialFeedbackMessages(int type, size_t capacity, GameLiveStreamingFeedbackMessage* messages, size_t* acquired);
int APS5_VABI sceGameLiveStreamingPermitServerSideRecording(bool permit);
int APS5_VABI sceGameLiveStreamingSetMetadata(const char* metadata);
int APS5_VABI sceGameLiveStreamingSetSpoilerTag(size_t count, const GameLiveStreamingSpoilerTag* tags);
int APS5_VABI sceGameLiveStreamingClearSpoilerTag(void);
int APS5_VABI sceGameLiveStreamingSetPresetSocialFeedbackCommands(void);
int APS5_VABI sceGameLiveStreamingSetPresetSocialFeedbackCommands(size_t count, const GameLiveStreamingPresetCommand* commands);
int APS5_VABI sceGameLiveStreamingClearPresetSocialFeedbackCommands(void);
int APS5_VABI sceGameLiveStreamingSetPresetSocialFeedbackCommandsDescription(void);
int APS5_VABI sceGameLiveStreamingClearSocialFeedbackMessages(void);
int APS5_VABI sceGameLiveStreamingPostSocialMessage(void);
int APS5_VABI sceGameLiveStreamingSetCameraFrameSetting(void);
int APS5_VABI sceGameLiveStreamingSetPresetSocialFeedbackCommandsDescription(const char* description);
int APS5_VABI sceGameLiveStreamingClearSocialFeedbackMessages(int type);
int APS5_VABI sceGameLiveStreamingPostSocialMessage(const char* message);
int APS5_VABI sceGameLiveStreamingSetCameraFrameSetting(const GameLiveStreamingCameraFrameSetting* setting);
int APS5_VABI sceGameLiveStreamingSetServiceProviderPermission(void);
int APS5_VABI sceGameLiveStreamingSetDefaultServiceProviderPermission(void);
int APS5_VABI sceGameLiveStreamingSetInvitationSessionId(void);
int APS5_VABI sceGameLiveStreamingSetInvitationSessionId(const char* sessionId);
int APS5_VABI sceGameLiveStreamingStartGenerateStandbyScreenResource(void);
int APS5_VABI sceGameLiveStreamingStopGenerateStandbyScreenResource(void);
int APS5_VABI sceGameLiveStreamingRegisterCallback(void);
+12 -3
View File
@@ -29,7 +29,7 @@ int Digit(char16_t value, int base) {
std::int32_t Decode(Icu::UnescapeCharacterAt characterAt, std::int32_t& position, std::int32_t length, void* context) {
const auto start = position;
if (position < 0 || position >= length) return -1;
if (!characterAt) NotImplemented_nid_no_patch("u_unescapeAt: null callback");
if (!characterAt) throw std::invalid_argument("u_unescapeAt: null callback");
const auto initial = characterAt(position++, context);
bool braces = false;
int minimum = 0;
@@ -117,17 +117,22 @@ char16_t APS5_VABI ByteAt(std::int32_t position, void* context) {
extern "C" {
std::int32_t APS5_VABI u_unescapeAt_nid_postfix(Icu::UnescapeCharacterAt characterAt, std::int32_t* offset, std::int32_t length, void* context) {
if (!offset) NotImplemented_nid_no_patch(__func__);
if (!offset) APS5_INVALID_ARG_EX;
return Decode(characterAt, *offset, length, context);
}
std::int32_t APS5_VABI u_unescape_nid_postfix(const char* source, char16_t* destination, std::int32_t capacity) {
if (!source || (destination && capacity < 0)) NotImplemented_nid_no_patch(__func__);
if (!source) APS5_INVALID_ARG_EX;
const auto bytes = std::strlen(source);
if (bytes > static_cast<std::size_t>(std::numeric_limits<std::int32_t>::max()))
NotImplemented_nid_no_patch(__func__);
const auto length = static_cast<std::int32_t>(bytes);
const auto room = [&](std::int32_t used) {
if (destination && static_cast<std::int64_t>(capacity) - used < std::numeric_limits<std::int32_t>::min())
NotImplemented_nid_no_patch("u_unescape: capacity minus the output length overflows");
};
std::int32_t written = 0;
std::int32_t segment = 0;
for (std::int32_t position = 0; position < length;) {
const auto byte = static_cast<unsigned char>(source[position++]);
if (byte != '\\') {
@@ -135,11 +140,13 @@ std::int32_t APS5_VABI u_unescape_nid_postfix(const char* source, char16_t* dest
++written;
continue;
}
if (written != segment) room(segment);
const auto value = Decode(ByteAt, position, length, const_cast<char*>(source));
if (value < 0) {
if (destination && capacity > 0) destination[0] = 0;
return 0;
}
room(written);
const int units = value > 0xffff ? 2 : 1;
if (destination && written <= capacity && units <= capacity - written) {
if (units == 1) destination[written] = static_cast<char16_t>(value);
@@ -149,7 +156,9 @@ std::int32_t APS5_VABI u_unescape_nid_postfix(const char* source, char16_t* dest
}
}
written += units;
segment = written;
}
if (written != segment) room(segment);
if (destination && written < capacity) destination[written] = 0;
return written;
}
+41 -10
View File
@@ -7,6 +7,7 @@
#include <exception>
#include <limits>
#include <source_location>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
@@ -22,9 +23,15 @@ void Require(bool condition, std::source_location location = std::source_locatio
}
template<typename TAction>
void RequireThrows(TAction action) {
void RequireThrows(TAction action, std::source_location location = std::source_location::current()) {
try { action(); } catch (const std::exception&) { return; }
Require(false);
Require(false, location);
}
template<typename TAction>
void RequireInvalidArgument(TAction action, std::source_location location = std::source_location::current()) {
try { action(); } catch (const std::invalid_argument&) { return; }
Require(false, location);
}
struct Input {
@@ -116,8 +123,9 @@ void SingleEscapes() {
}
offset = 0;
Require(u_unescapeAt_nid_postfix(nullptr, &offset, -1, nullptr) == -1 && offset == 0);
RequireThrows([&] { u_unescapeAt_nid_postfix(nullptr, &offset, 1, nullptr); });
RequireThrows([] { u_unescapeAt_nid_postfix(Read_nid_no_patch, nullptr, 1, nullptr); });
RequireInvalidArgument([&] { u_unescapeAt_nid_postfix(nullptr, &offset, 1, nullptr); });
RequireInvalidArgument([] { u_unescapeAt_nid_postfix(Read_nid_no_patch, nullptr, 1, nullptr); });
RequireInvalidArgument([] { u_unescapeAt_nid_postfix(nullptr, nullptr, -1, nullptr); });
}
void String(const char* escaped, std::u16string_view expected) {
@@ -162,12 +170,10 @@ void Strings() {
Require(u_unescape_nid_postfix("a\\u12", destination, 3) == 0 && destination[0] == 0 && destination[1] == 0xa55a);
destination[0] = 0xa55a;
Require(u_unescape_nid_postfix("abc\\u12", destination, 3) == 0 && destination[0] == 0 && destination[1] == u'b' && destination[2] == u'c');
for (auto capacity : {-1, std::numeric_limits<std::int32_t>::min()}) {
std::fill(std::begin(destination), std::end(destination), 0xa55a);
RequireThrows([&] { u_unescape_nid_postfix("a\\U0001f600", destination, capacity); });
Require(destination[0] == 0xa55a && destination[1] == 0xa55a && destination[2] == 0xa55a);
}
RequireThrows([] { u_unescape_nid_postfix(nullptr, nullptr, 0); });
std::fill(std::begin(destination), std::end(destination), 0xa55a);
RequireInvalidArgument([] { u_unescape_nid_postfix(nullptr, nullptr, 0); });
RequireInvalidArgument([&] { u_unescape_nid_postfix(nullptr, destination, 3); });
Require(destination[0] == 0xa55a && destination[1] == 0xa55a && destination[2] == 0xa55a);
for (std::uint32_t value = 0; value <= 0x10ffff; value += 17) {
char escaped[16];
std::snprintf(escaped, sizeof(escaped), "\\U%08x", value);
@@ -179,11 +185,36 @@ void Strings() {
}
}
void NegativeCapacity() {
constexpr auto minimum = std::numeric_limits<std::int32_t>::min();
struct Case {
const char* escaped;
std::int32_t capacity;
std::int32_t size;
};
for (const auto& test : {Case{"a\\U0001f600", -1, 3}, Case{"a\\U0001f600", -2, 3}, Case{"a\\U0001f600", minimum + 1, 3},
Case{"plain", -1, 5}, Case{"\\n", minimum, 1}, Case{"ab", minimum, 2}, Case{"a\\u12", minimum, 0},
Case{"a\\u12", -1, 0}, Case{"ab\\n", minimum + 2, 3}, Case{"\\nab", minimum + 1, 3}, Case{"", minimum, 0}}) {
std::array<char16_t, 8> output;
output.fill(0xa55a);
Require(u_unescape_nid_postfix(test.escaped, output.data() + 1, test.capacity) == test.size);
Require(std::all_of(output.begin(), output.end(), [](char16_t unit) { return unit == 0xa55a; }));
}
for (const auto& test : {Case{"a\\n", minimum, 0}, Case{"\\na", minimum, 0}, Case{"ab\\n", minimum + 1, 0}, Case{"\\nab", minimum, 0}}) {
std::array<char16_t, 8> output;
output.fill(0xa55a);
RequireThrows([&] { u_unescape_nid_postfix(test.escaped, output.data() + 1, test.capacity); });
Require(std::all_of(output.begin(), output.end(), [](char16_t unit) { return unit == 0xa55a; }));
Require(u_unescape_nid_postfix(test.escaped, nullptr, test.capacity) == static_cast<std::int32_t>(std::string_view(test.escaped).size()) - 1);
}
}
} // namespace
int main() {
SingleEscapes();
CallbackOrder();
Strings();
NegativeCapacity();
std::puts("ICU escape decoding tests passed");
}
@@ -111,4 +111,20 @@ int APS5_VABI sceInvitationDialogGetResult(InvitationDialogResult* result) {
return COMMON_DIALOG_OK;
}
int APS5_VABI sceInvitationDialogGetStatus_nid_postfix(void) {
DialogState& dialog = Dialog();
std::lock_guard lock(dialog.mutex);
return dialog.status;
}
int APS5_VABI sceInvitationDialogOpenA_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceInvitationDialogGetResultA_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
}
@@ -54,5 +54,8 @@ int APS5_VABI sceInvitationDialogOpen(const InvitationDialogParam* param);
int APS5_VABI sceInvitationDialogTerminate(void);
int APS5_VABI sceInvitationDialogUpdateStatus(void);
int APS5_VABI sceInvitationDialogClose(void);
int APS5_VABI sceInvitationDialogGetStatus_nid_postfix(void);
int APS5_VABI sceInvitationDialogOpenA_nid_postfix(void);
int APS5_VABI sceInvitationDialogGetResultA_nid_postfix(void);
}
+8 -6
View File
@@ -25,20 +25,21 @@ std::int64_t APS5_VABI PKFree_nid_postfix(void** memory) {
}
std::int64_t APS5_VABI readIS_nid_postfix(void*, Jxr::BitIo* io) {
if (!io) NotImplemented_nid_no_patch(__func__);
if (!io) APS5_INVALID_ARG_EX;
constexpr std::uintptr_t packetSize = 4096;
const auto start = reinterpret_cast<std::uintptr_t>(io->start);
if (((start ^ reinterpret_cast<std::uintptr_t>(io->current)) & packetSize) == 0) return WmpErrSuccess;
auto* stream = io->stream;
if (!stream || !stream->setPosition) NotImplemented_nid_no_patch(__func__);
if (!stream || !stream->setPosition) APS5_INVALID_ARG_EX;
stream->setPosition(stream, io->referenceOffset);
if (!io->start || !stream->read) NotImplemented_nid_no_patch(__func__);
if (!stream->read) APS5_INVALID_ARG_EX;
{
GuestArena::HostWrite access(io->start, packetSize);
if (!access.Open()) NotImplemented_nid_no_patch(__func__);
if (!access.Open()) APS5_INVALID_ARG_EX;
stream->read(stream, io->start, packetSize);
}
io->referenceOffset += packetSize;
if (!io->start) APS5_INVALID_ARG_EX;
std::memcpy(&io->shadow, io->start, sizeof(io->shadow));
const auto mask = static_cast<std::uintptr_t>(static_cast<std::intptr_t>(io->mask));
io->start = reinterpret_cast<std::uint8_t*>((reinterpret_cast<std::uintptr_t>(io->start) + packetSize) & mask);
@@ -46,13 +47,14 @@ std::int64_t APS5_VABI readIS_nid_postfix(void*, Jxr::BitIo* io) {
}
std::int64_t APS5_VABI writeIS_nid_postfix(void*, Jxr::BitIo* io) {
if (!io) NotImplemented_nid_no_patch(__func__);
if (!io) APS5_INVALID_ARG_EX;
constexpr std::uintptr_t packetSize = 4096;
const auto start = reinterpret_cast<std::uintptr_t>(io->start);
if (((start ^ reinterpret_cast<std::uintptr_t>(io->current)) & packetSize) == 0) return WmpErrSuccess;
if (!io->start || !io->stream || !io->stream->write) NotImplemented_nid_no_patch(__func__);
if (!io->stream || !io->stream->write) APS5_INVALID_ARG_EX;
const auto result = io->stream->write(io->stream, io->start, packetSize);
if (result < 0) return result;
if (!io->start) APS5_INVALID_ARG_EX;
const auto mask = static_cast<std::uintptr_t>(static_cast<std::intptr_t>(io->mask));
io->start = reinterpret_cast<std::uint8_t*>((reinterpret_cast<std::uintptr_t>(io->start) + packetSize) & mask);
return result;
-5
View File
@@ -1463,11 +1463,6 @@ int APS5_VABI X509_STORE_add_crl_nid_postfix(void) {
return 0;
}
int APS5_VABI X509_VERIFY_PARAM_set1_host_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI X509_check_issued_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
+1 -1
View File
@@ -541,7 +541,7 @@ LibreSslX509Lookup* APS5_VABI X509_STORE_add_lookup_nid_postfix(LibreSslX509Stor
void APS5_VABI X509_STORE_free_nid_postfix(LibreSslX509Store* store);
LibreSslX509Store* APS5_VABI X509_STORE_new_nid_postfix(void);
int APS5_VABI X509_STORE_set_flags_nid_postfix(LibreSslX509Store* store, std::uint64_t flags);
int APS5_VABI X509_VERIFY_PARAM_set1_host_nid_postfix(void);
int APS5_VABI X509_VERIFY_PARAM_set1_host_nid_postfix(LibreSslX509VerifyParam* param, const char* name, std::size_t length);
int APS5_VABI X509_VERIFY_PARAM_set1_ip_asc_nid_postfix(LibreSslX509VerifyParam* param, const char* text);
void APS5_VABI X509_VERIFY_PARAM_set_hostflags_nid_postfix(LibreSslX509VerifyParam* param, std::uint32_t flags);
int APS5_VABI X509_check_issued_nid_postfix(void);
@@ -5,6 +5,7 @@
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iterator>
static_assert(sizeof(LibreSslX509VerifyParamId) == 56);
@@ -238,4 +239,32 @@ LibreSslX509Lookup* APS5_VABI X509_STORE_add_lookup_nid_postfix(LibreSslX509Stor
return nullptr;
}
int APS5_VABI X509_VERIFY_PARAM_set1_host_nid_postfix(LibreSslX509VerifyParam* param, const char* name, std::size_t length) {
if (!param || !param->id) NotImplemented_nid_no_patch(__func__);
auto* id = param->id;
if (name && length == 0) length = std::strlen(name);
if (name && std::memchr(name, '\0', length)) return 0;
if (id->hosts) {
FreeStrings(static_cast<Stack*>(id->hosts), __func__);
id->hosts = nullptr;
}
if (!name || length == 0) return 1;
char* copy = strndup_nid_postfix(name, length);
if (!copy) return 0;
if (!id->hosts && !(id->hosts = NewStack(nullptr))) {
free_nid_postfix(copy);
return 0;
}
auto* hosts = static_cast<Stack*>(id->hosts);
if (!Push(hosts, copy, __func__)) {
free_nid_postfix(copy);
if (hosts->num == 0) {
FreeStack(hosts);
id->hosts = nullptr;
}
return 0;
}
return 1;
}
}
@@ -125,13 +125,52 @@ void Unsupported() {
RequireThrows([&] { X509_STORE_free_nid_postfix(store); }, "null element array");
}
const char* Host(const LibreSslX509VerifyParam* param, int index) {
const auto* hosts = static_cast<const Stack*>(param->id->hosts);
return hosts && index < hosts->num ? hosts->data[index] : nullptr;
}
int HostCount(const LibreSslX509VerifyParam* param) {
const auto* hosts = static_cast<const Stack*>(param->id->hosts);
return hosts ? hosts->num : -1;
}
void Hosts() {
auto* store = X509_STORE_new_nid_postfix();
auto* param = store->param;
Require(X509_VERIFY_PARAM_set1_host_nid_postfix(param, "example.com", 0) == 1, "a terminated name is set");
Require(HostCount(param) == 1 && std::strcmp(Host(param, 0), "example.com") == 0, "the name is copied into a one-name list");
const auto* hosts = static_cast<const Stack*>(param->id->hosts);
Require(hosts->comp == nullptr && hosts->numAlloc == 4 && hosts->sorted == 0, "the host list is a plain stack");
Require(X509_VERIFY_PARAM_set1_host_nid_postfix(param, "www.example.org.extra", 15) == 1, "a counted name is set");
Require(HostCount(param) == 1 && std::strcmp(Host(param, 0), "www.example.org") == 0, "set replaces the list and copies the counted bytes");
char unterminated[] = {'a', '.', 'b'};
Require(X509_VERIFY_PARAM_set1_host_nid_postfix(param, unterminated, sizeof(unterminated)) == 1 && std::strcmp(Host(param, 0), "a.b") == 0,
"a counted name needs no terminator");
const char embedded[] = {'a', '\0', 'b'};
Require(X509_VERIFY_PARAM_set1_host_nid_postfix(param, embedded, sizeof(embedded)) == 0, "an embedded NUL is refused");
Require(HostCount(param) == 1 && std::strcmp(Host(param, 0), "a.b") == 0, "a refused name keeps the list");
Require(X509_VERIFY_PARAM_set1_host_nid_postfix(param, "", 0) == 1 && param->id->hosts == nullptr, "an empty name clears the list");
Require(X509_VERIFY_PARAM_set1_host_nid_postfix(param, "c.d", 0) == 1, "a name after clearing");
Require(X509_VERIFY_PARAM_set1_host_nid_postfix(param, nullptr, 5) == 1 && param->id->hosts == nullptr, "a null name clears the list");
Require(X509_VERIFY_PARAM_set1_host_nid_postfix(param, nullptr, 0) == 1 && param->id->hosts == nullptr, "clearing an empty list");
Require(X509_VERIFY_PARAM_set1_host_nid_postfix(param, "last.example", 0) == 1, "a name that the store frees");
RequireThrows([] { X509_VERIFY_PARAM_set1_host_nid_postfix(nullptr, "a", 0); }, "null verify parameters");
auto* id = param->id;
param->id = nullptr;
RequireThrows([&] { X509_VERIFY_PARAM_set1_host_nid_postfix(param, "a", 0); }, "verify parameters without an identity");
param->id = id;
X509_STORE_free_nid_postfix(store);
}
int allocations = 0;
int callocs = 0;
int frees = 0;
int failCallocAt = 0;
int failAllocationAt = 0;
void* APS5_VABI Allocate_nid_no_patch(std::size_t size) {
++allocations;
if (++allocations == failAllocationAt) return nullptr;
return std::malloc(size);
}
@@ -166,6 +205,24 @@ void Allocator() {
Require(X509_STORE_new_nid_postfix() == nullptr, "a failed verify parameter allocation fails the store");
Require(frees == allocations + failing - 1, "a failed store releases what it allocated");
}
failCallocAt = 0;
store = X509_STORE_new_nid_postfix();
allocations = callocs = frees = 0;
Require(X509_VERIFY_PARAM_set1_host_nid_postfix(store->param, "a.example", 0) == 1 && allocations == 3 && frees == 0,
"a first name allocates its copy, the stack and its array");
Require(X509_VERIFY_PARAM_set1_host_nid_postfix(store->param, "b.example", 0) == 1 && allocations == 6 && frees == 3,
"a new name frees the old list");
allocations = frees = 0;
failAllocationAt = 1;
Require(X509_VERIFY_PARAM_set1_host_nid_postfix(store->param, "c.example", 0) == 0 && store->param->id->hosts == nullptr && frees == 3,
"a failed copy leaves the list cleared");
allocations = frees = 0;
failAllocationAt = 2;
Require(X509_VERIFY_PARAM_set1_host_nid_postfix(store->param, "d.example", 0) == 0 && store->param->id->hosts == nullptr && frees == 1,
"a failed stack frees the copy");
failAllocationAt = 0;
X509_STORE_free_nid_postfix(store);
}
} // namespace
@@ -182,4 +239,5 @@ int main(int argc, char**) {
References();
Identity();
Unsupported();
Hosts();
}
+5
View File
@@ -16,6 +16,11 @@ if(BUILD_TESTING)
endif()
add_test(NAME net_ipv6only_patched COMMAND net_ipv6only_patched_tests ${PATCHED_DIR})
set_tests_properties(net_ipv6only_patched PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77)
if(MINGW)
get_filename_component(ipv6OnlyPatchedRuntimeDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY)
set_tests_properties(net_ipv6only_patched PROPERTIES
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:${ipv6OnlyPatchedRuntimeDirectory}")
endif()
add_test_executable(net_ipv6only_tests tests/Ipv6Only.cpp)
target_include_directories(net_ipv6only_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(net_ipv6only_tests PRIVATE libSceNet libkernel libc)
+11
View File
@@ -837,6 +837,17 @@ bool SetSocketOptionForKernel(int descriptor, int level, int option, const void*
const bool g_socketOptionSetterRegistered = (KernelSetSocketOptionSetter_nid_no_patch(&SetSocketOptionForKernel), true);
bool GetSocketOptionForKernel(int descriptor, int level, int option, void* value, std::uint32_t* length, int* error) {
{
std::lock_guard lk(g_mutex);
if (g_socks.count(descriptor) == 0) return false;
}
*error = sceNetGetsockopt(descriptor, level, option, value, length) == 0 ? 0 : *errno_slot();
return true;
}
const bool g_socketOptionGetterRegistered = (KernelSetSocketOptionGetter_nid_no_patch(&GetSocketOptionForKernel), true);
constexpr int GuestFcntlGetStatus = 3;
constexpr int GuestFcntlSetStatus = 4;
constexpr int GuestReadWrite = 2;
@@ -14,8 +14,14 @@ if(BUILD_TESTING)
target_link_libraries(guest_np_cpp_web_api_string_tests PRIVATE libSceNpCppWebApi libc)
configure_windows_unwind(guest_np_cpp_web_api_string_tests)
add_test(NAME guest_np_cpp_web_api_string COMMAND guest_np_cpp_web_api_string_tests)
add_test(NAME guest_np_cpp_web_api_alias_append COMMAND guest_np_cpp_web_api_string_tests alias-append)
add_test(NAME guest_np_cpp_web_api_alias_string_vector COMMAND guest_np_cpp_web_api_string_tests alias-vector)
add_test(NAME guest_np_cpp_web_api_alias_intrusive_vector COMMAND guest_np_cpp_web_api_intrusive_ptr_tests alias-vector)
set_tests_properties(guest_np_cpp_web_api_alias_append guest_np_cpp_web_api_alias_string_vector
guest_np_cpp_web_api_alias_intrusive_vector PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(guest_np_cpp_web_api_string PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_tests_properties(guest_np_cpp_web_api_string guest_np_cpp_web_api_alias_append
guest_np_cpp_web_api_alias_string_vector guest_np_cpp_web_api_alias_intrusive_vector
PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
+10 -4
View File
@@ -931,10 +931,11 @@ static IntrusivePtr* HostVectorElement(void* self, const std::uint64_t index, co
static int VectorPushBackIntrusivePtr(IntrusivePtr* self, const IntrusivePtr* element) {
VectorBody* body = VectorBodyFor(self, 1, __func__);
if (body == nullptr) return -1;
const IntrusivePtr copy = *element;
if (!VectorGrow(body, sizeof(IntrusivePtr))) return -1;
auto* elements = static_cast<IntrusivePtr*>(body->elements);
elements[body->size] = *element;
if (element->object != nullptr) ReferenceCount(element->object, ReferenceCountOffset).fetch_add(1);
elements[body->size] = copy;
if (copy.object != nullptr) ReferenceCount(copy.object, ReferenceCountOffset).fetch_add(1);
body->size += 1;
return 0;
}
@@ -942,7 +943,6 @@ static int VectorPushBackIntrusivePtr(IntrusivePtr* self, const IntrusivePtr* el
static int VectorPushBackString(IntrusivePtr* self, const CommonString* element) {
VectorBody* body = VectorBodyFor(self, 2, __func__);
if (body == nullptr) return -1;
if (!VectorGrow(body, sizeof(CommonString))) return -1;
CommonString copy = *element;
copy.referenceCount = 0;
copy.buffer = nullptr;
@@ -954,6 +954,10 @@ static int VectorPushBackString(IntrusivePtr* self, const CommonString* element)
std::memcpy(copy.buffer, element->buffer, length);
copy.bufferSize = length;
}
if (!VectorGrow(body, sizeof(CommonString))) {
std::free(copy.buffer);
return -1;
}
static_cast<CommonString*>(body->elements)[body->size] = copy;
body->size += 1;
return 0;
@@ -1129,11 +1133,13 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common6String6appendEPKc(CommonString* self,
char* buffer = static_cast<char*>(std::malloc(capacity));
if (buffer == nullptr) return -1;
if (oldLength != 0) std::memcpy(buffer, self->buffer, oldLength);
std::memmove(buffer + oldLength, text, textLength + 1);
std::free(self->buffer);
self->buffer = buffer;
self->bufferSize = capacity;
return 0;
}
std::memcpy(self->buffer + oldLength, text, textLength + 1);
std::memmove(self->buffer + oldLength, text, textLength + 1);
return 0;
}
+5 -2
View File
@@ -887,8 +887,11 @@ int APS5_VABI sceNpPollAsync(int req_id, int* result) {
return NP_POLL_ASYNC_FINISHED;
}
int APS5_VABI sceNpWaitAsync(void) {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceNpWaitAsync(int req_id, int* result) {
if (!result) return SCE_NP_ERROR_INVALID_ARGUMENT;
std::lock_guard lock(requestMutex);
if (!liveRequests.count(req_id)) return SCE_NP_ERROR_REQUEST_NOT_FOUND;
*result = SCE_NP_ERROR_SIGNED_OUT;
return 0;
}
@@ -184,7 +184,7 @@ int APS5_VABI sceNpGetOnlineId(int userId, NpOnlineId* onlineId);
int APS5_VABI sceNpGetState(int userId, uint32_t* state);
int APS5_VABI sceNpHasSignedUp(int userId, bool* hasSignedUp);
int APS5_VABI sceNpPollAsync(int reqId, int* result);
int APS5_VABI sceNpWaitAsync(void);
int APS5_VABI sceNpWaitAsync(int reqId, int* result);
void APS5_VABI sceNpRegisterGamePresenceCallback(void* callback, void* userdata);
int APS5_VABI sceNpRegisterNpReachabilityStateCallback(void* callback, void* userdata);
int APS5_VABI sceNpUnregisterNpReachabilityStateCallback(void);
@@ -1,3 +1,4 @@
#include "prx/libSceNpSignaling/include/InventoryExports.hpp"
#include "prx/libSceNpSignaling/include/Api.hpp"
#include <cstdint>
#include <cstring>
@@ -171,3 +172,67 @@ int APS5_VABI sceNpSignalingDeactivateConnection(int ctxId, int connId) {
}
}
extern "C" {
int APS5_VABI sceNpSignalingActivateConnectionA() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceNpSignalingCancelPeerNetInfo() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceNpSignalingCreateContextA() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceNpSignalingGetConnectionFromNpId() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceNpSignalingGetConnectionFromPeerAddressA() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceNpSignalingGetConnectionFromPeerAddress() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceNpSignalingGetConnectionInfoA() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceNpSignalingGetConnectionStatistics() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceNpSignalingGetConnectionStatus() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceNpSignalingGetContextOption() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceNpSignalingGetLocalNetInfo() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceNpSignalingGetMemoryInfo() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceNpSignalingGetPeerNetInfoA() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceNpSignalingGetPeerNetInfoResult() {
NotImplemented_nid_no_patch(__func__);
}
int APS5_VABI sceNpSignalingGetPeerNetInfo() {
NotImplemented_nid_no_patch(__func__);
}
}
@@ -0,0 +1,23 @@
#pragma once
#include "SceTypes.hpp"
extern "C" {
int APS5_VABI sceNpSignalingActivateConnectionA();
int APS5_VABI sceNpSignalingCancelPeerNetInfo();
int APS5_VABI sceNpSignalingCreateContextA();
int APS5_VABI sceNpSignalingGetConnectionFromNpId();
int APS5_VABI sceNpSignalingGetConnectionFromPeerAddressA();
int APS5_VABI sceNpSignalingGetConnectionFromPeerAddress();
int APS5_VABI sceNpSignalingGetConnectionInfoA();
int APS5_VABI sceNpSignalingGetConnectionStatistics();
int APS5_VABI sceNpSignalingGetConnectionStatus();
int APS5_VABI sceNpSignalingGetContextOption();
int APS5_VABI sceNpSignalingGetLocalNetInfo();
int APS5_VABI sceNpSignalingGetMemoryInfo();
int APS5_VABI sceNpSignalingGetPeerNetInfoA();
int APS5_VABI sceNpSignalingGetPeerNetInfoResult();
int APS5_VABI sceNpSignalingGetPeerNetInfo();
}
@@ -1,3 +1,4 @@
#pragma once
#include "prx/libSceNpSignaling/include/Api.hpp"
#include "prx/libSceNpSignaling/include/InventoryExports.hpp"
Loaded 100 of 258 files, more files were not shown because too many files have changed in this diff. Show more