Merge pull request #3596 from Lorkydey/feat/libc-fstatvfs
feat(libc): implement guest fstatvfs volume statistics
This commit is contained in:
commit
d956de8425
|
|
@ -2,8 +2,24 @@ add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp FileTime.cpp U
|
|||
target_include_directories(libSceLibcInternal PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS})
|
||||
configure_windows_unwind(libSceLibcInternal)
|
||||
target_sources(libSceLibcInternal PRIVATE FilesystemStatistics.cpp)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(guest_fstatvfs_conversion_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatvfsConversion.cpp FilesystemStatistics.cpp)
|
||||
target_include_directories(guest_fstatvfs_conversion_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
configure_windows_unwind(guest_fstatvfs_conversion_tests)
|
||||
add_test(NAME guest_fstatvfs_conversion COMMAND guest_fstatvfs_conversion_tests)
|
||||
set_tests_properties(guest_fstatvfs_conversion PROPERTIES TIMEOUT 20)
|
||||
add_test_executable(guest_fstatvfs_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatvfs.cpp)
|
||||
target_include_directories(guest_fstatvfs_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_fstatvfs_tests PRIVATE libSceLibcInternal libkernel libc)
|
||||
configure_windows_unwind(guest_fstatvfs_tests)
|
||||
add_test(NAME guest_fstatvfs COMMAND guest_fstatvfs_tests)
|
||||
set_tests_properties(guest_fstatvfs PROPERTIES TIMEOUT 20)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_fstatvfs PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibcInternal>")
|
||||
endif()
|
||||
add_test_executable(guest_utime_forwarding_tests ${LIBS_INCLUDE_DIR}/tests/GuestUtimeForwarding.cpp FileTime.cpp)
|
||||
target_include_directories(guest_utime_forwarding_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
configure_windows_unwind(guest_utime_forwarding_tests)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include "prx/libkernel/File/include/KernelStatfs.hpp"
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
struct LibcStatvfs {
|
||||
std::uint64_t f_bavail;
|
||||
std::uint64_t f_bfree;
|
||||
std::uint64_t f_blocks;
|
||||
std::uint64_t f_favail;
|
||||
std::uint64_t f_ffree;
|
||||
std::uint64_t f_files;
|
||||
std::uint64_t f_bsize;
|
||||
std::uint64_t f_flag;
|
||||
std::uint64_t f_frsize;
|
||||
std::uint64_t f_fsid;
|
||||
std::uint64_t f_namemax;
|
||||
};
|
||||
static_assert(sizeof(LibcStatvfs) == 88);
|
||||
static_assert(offsetof(LibcStatvfs, f_bsize) == 48);
|
||||
static_assert(offsetof(LibcStatvfs, f_namemax) == 80);
|
||||
|
||||
extern "C" int APS5_VABI _fstatfs_nid_postfix(int, KernelStatfs*);
|
||||
extern "C" int* APS5_VABI __error_nid_postfix();
|
||||
|
||||
extern "C" int APS5_VABI fstatvfs_nid_postfix(int descriptor, LibcStatvfs* output) {
|
||||
int* const error = __error_nid_postfix();
|
||||
const int saved = *error;
|
||||
KernelStatfs volume{};
|
||||
const int status = _fstatfs_nid_postfix(descriptor, &volume);
|
||||
if (status != 0) return status;
|
||||
if (output == nullptr) {
|
||||
*error = 14;
|
||||
return -1;
|
||||
}
|
||||
const LibcStatvfs result{
|
||||
static_cast<std::uint64_t>(volume.f_bavail), volume.f_bfree, volume.f_blocks,
|
||||
static_cast<std::uint64_t>(volume.f_ffree), static_cast<std::uint64_t>(volume.f_ffree), volume.f_files,
|
||||
volume.f_iosize, (volume.f_flags & 1) | ((volume.f_flags & 8) >> 2), volume.f_bsize,
|
||||
0, volume.f_namemax
|
||||
};
|
||||
std::memcpy(output, &result, sizeof(result));
|
||||
*error = saved;
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
struct KernelStatfs {
|
||||
std::uint32_t f_version;
|
||||
std::uint32_t f_type;
|
||||
std::uint64_t f_flags;
|
||||
std::uint64_t f_bsize;
|
||||
std::uint64_t f_iosize;
|
||||
std::uint64_t f_blocks;
|
||||
std::uint64_t f_bfree;
|
||||
std::int64_t f_bavail;
|
||||
std::uint64_t f_files;
|
||||
std::int64_t f_ffree;
|
||||
std::uint64_t f_syncwrites;
|
||||
std::uint64_t f_asyncwrites;
|
||||
std::uint64_t f_syncreads;
|
||||
std::uint64_t f_asyncreads;
|
||||
std::uint64_t f_spare[10];
|
||||
std::uint32_t f_namemax;
|
||||
std::uint32_t f_owner;
|
||||
std::int32_t f_fsid[2];
|
||||
char f_charspare[80];
|
||||
char f_fstypename[16];
|
||||
char f_mntfromname[88];
|
||||
char f_mntonname[88];
|
||||
};
|
||||
static_assert(sizeof(KernelStatfs) == 472);
|
||||
static_assert(offsetof(KernelStatfs, f_bavail) == 48);
|
||||
static_assert(offsetof(KernelStatfs, f_namemax) == 184);
|
||||
static_assert(offsetof(KernelStatfs, f_fstypename) == 280);
|
||||
static_assert(offsetof(KernelStatfs, f_mntonname) == 384);
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
#include "prx/libkernel/File/include/File.hpp"
|
||||
#include "prx/libkernel/File/include/FileFlags.hpp"
|
||||
#include "prx/libkernel/File/include/NativeStat.hpp"
|
||||
#include "prx/libkernel/File/include/KernelStatfs.hpp"
|
||||
#include "prx/libkernel/File/include/DirectoryDescriptor.hpp"
|
||||
#include "prx/libkernel/File/include/FileLock.hpp"
|
||||
#include "prx/libkernel/File/include/RandomDevice.hpp"
|
||||
|
|
@ -32,36 +33,6 @@ static_assert(sizeof(KernelIovec) == sizeof(GuestSockets::Iovec));
|
|||
|
||||
static constexpr int KERNEL_IOV_MAX = 1024;
|
||||
|
||||
struct KernelStatfs {
|
||||
std::uint32_t f_version;
|
||||
std::uint32_t f_type;
|
||||
std::uint64_t f_flags;
|
||||
std::uint64_t f_bsize;
|
||||
std::uint64_t f_iosize;
|
||||
std::uint64_t f_blocks;
|
||||
std::uint64_t f_bfree;
|
||||
std::int64_t f_bavail;
|
||||
std::uint64_t f_files;
|
||||
std::int64_t f_ffree;
|
||||
std::uint64_t f_syncwrites;
|
||||
std::uint64_t f_asyncwrites;
|
||||
std::uint64_t f_syncreads;
|
||||
std::uint64_t f_asyncreads;
|
||||
std::uint64_t f_spare[10];
|
||||
std::uint32_t f_namemax;
|
||||
std::uint32_t f_owner;
|
||||
std::int32_t f_fsid[2];
|
||||
char f_charspare[80];
|
||||
char f_fstypename[16];
|
||||
char f_mntfromname[88];
|
||||
char f_mntonname[88];
|
||||
};
|
||||
static_assert(sizeof(KernelStatfs) == 472);
|
||||
static_assert(offsetof(KernelStatfs, f_bavail) == 48);
|
||||
static_assert(offsetof(KernelStatfs, f_namemax) == 184);
|
||||
static_assert(offsetof(KernelStatfs, f_fstypename) == 280);
|
||||
static_assert(offsetof(KernelStatfs, f_mntonname) == 384);
|
||||
|
||||
static constexpr std::uint32_t KERNEL_STATFS_VERSION = 0x20030518;
|
||||
static constexpr std::uint64_t KERNEL_MNT_RDONLY = 0x1;
|
||||
static constexpr std::uint64_t KERNEL_MNT_NOEXEC = 0x4;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
#include "prx/libc/include/General.hpp"
|
||||
#include "prx/libkernel/File/include/FileFlags.hpp"
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
extern "C" {
|
||||
int APS5_VABI fstatvfs_nid_postfix(int, void*);
|
||||
int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t);
|
||||
std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int);
|
||||
int APS5_VABI close_nid_postfix(int);
|
||||
int APS5_VABI pipe_nid_postfix(int*);
|
||||
int APS5_VABI socketpair_nid_postfix(int, int, int, int*);
|
||||
int* APS5_VABI __error_nid_postfix();
|
||||
}
|
||||
|
||||
static void Check(bool value, int line) {
|
||||
if (!value) {
|
||||
std::fprintf(stderr, "fstatvfs check failed at line %d\n", line);
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
#define Require(value) Check((value), __LINE__)
|
||||
|
||||
static void RequireFailure(int descriptor, int error) {
|
||||
std::array<std::uint64_t, 11> output;
|
||||
output.fill(0x5A5A5A5A5A5A5A5Aull);
|
||||
const auto saved = output;
|
||||
*__error_nid_postfix() = 0;
|
||||
Require(fstatvfs_nid_postfix(descriptor, output.data()) == -1);
|
||||
Require(*__error_nid_postfix() == error);
|
||||
Require(output == saved);
|
||||
}
|
||||
|
||||
static void RequireVolume(int descriptor, const std::filesystem::path& path) {
|
||||
std::array<std::uint64_t, 13> guarded;
|
||||
guarded.fill(0xA5A5A5A5A5A5A5A5ull);
|
||||
*__error_nid_postfix() = 12345;
|
||||
Require(fstatvfs_nid_postfix(descriptor, guarded.data() + 1) == 0);
|
||||
Require(*__error_nid_postfix() == 12345);
|
||||
Require(guarded.front() == 0xA5A5A5A5A5A5A5A5ull && guarded.back() == guarded.front());
|
||||
const auto* fields = guarded.data() + 1;
|
||||
const auto capacity = std::filesystem::space(path).capacity;
|
||||
Require(fields[6] > 0 && fields[8] > 0);
|
||||
Require(fields[2] > 0 && fields[2] == capacity / fields[8]);
|
||||
Require(fields[0] <= fields[1] && fields[1] <= fields[2]);
|
||||
Require(fields[3] == fields[4]);
|
||||
Require((fields[7] & ~std::uint64_t{3}) == 0);
|
||||
Require(fields[9] == 0 && fields[10] >= 255);
|
||||
}
|
||||
|
||||
int main() {
|
||||
const auto root = std::filesystem::temp_directory_path() /
|
||||
("anyps5-fstatvfs-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
Require(std::filesystem::create_directory(root));
|
||||
const auto filePath = root / "file.txt";
|
||||
{
|
||||
std::ofstream file(filePath, std::ios::binary);
|
||||
file << "fstatvfs";
|
||||
Require(static_cast<bool>(file));
|
||||
}
|
||||
AddPathAlias_nid_no_patch("/anyps5-fstatvfs-test", root.string().c_str());
|
||||
const int file = sceKernelOpen("/anyps5-fstatvfs-test/file.txt", SCE_KERNEL_O_RDONLY, 0);
|
||||
Require(file >= 0);
|
||||
Require(sceKernelLseek(file, 3, 0) == 3);
|
||||
RequireVolume(file, filePath);
|
||||
Require(sceKernelLseek(file, 0, 1) == 3);
|
||||
Require(fstatvfs_nid_postfix(file, nullptr) == -1 && *__error_nid_postfix() == 14);
|
||||
|
||||
const int directory = sceKernelOpen("/anyps5-fstatvfs-test", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0);
|
||||
Require(directory >= 0);
|
||||
RequireVolume(directory, root);
|
||||
Require(close_nid_postfix(directory) == 0);
|
||||
|
||||
int pipes[2];
|
||||
Require(pipe_nid_postfix(pipes) == 0);
|
||||
RequireFailure(pipes[0], 22);
|
||||
RequireFailure(pipes[1], 22);
|
||||
for (int fd : pipes) Require(close_nid_postfix(fd) == 0);
|
||||
int sockets[2];
|
||||
Require(socketpair_nid_postfix(1, 1, 0, sockets) == 0);
|
||||
RequireFailure(sockets[0], 22);
|
||||
for (int fd : sockets) Require(close_nid_postfix(fd) == 0);
|
||||
RequireFailure(sockets[0], 9);
|
||||
RequireFailure(-1, 9);
|
||||
Require(close_nid_postfix(file) == 0);
|
||||
RemovePathAlias_nid_no_patch("/anyps5-fstatvfs-test");
|
||||
Require(std::filesystem::remove(filePath));
|
||||
Require(std::filesystem::remove(root));
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include "prx/libkernel/File/include/KernelStatfs.hpp"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
extern "C" int APS5_VABI fstatvfs_nid_postfix(int, void*);
|
||||
|
||||
namespace {
|
||||
KernelStatfs backend;
|
||||
int backendError;
|
||||
int guestError;
|
||||
int calls;
|
||||
|
||||
void Check(bool value, int line) {
|
||||
if (!value) {
|
||||
std::fprintf(stderr, "fstatvfs conversion check failed at line %d\n", line);
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
#define Require(value) Check((value), __LINE__)
|
||||
|
||||
extern "C" int* APS5_VABI __error_nid_postfix() { return &guestError; }
|
||||
|
||||
extern "C" int APS5_VABI _fstatfs_nid_postfix(int descriptor, KernelStatfs* output) {
|
||||
Require(descriptor == 42);
|
||||
++calls;
|
||||
*output = backend;
|
||||
guestError = backendError ? backendError : 71;
|
||||
return backendError ? -1 : 0;
|
||||
}
|
||||
|
||||
int main() {
|
||||
backend.f_blocks = 0x100000002ull;
|
||||
backend.f_bfree = 0x100000001ull;
|
||||
backend.f_bavail = 0x100000000ll;
|
||||
backend.f_files = 0x200000002ull;
|
||||
backend.f_ffree = 0x200000001ll;
|
||||
backend.f_iosize = 65536;
|
||||
backend.f_bsize = 4096;
|
||||
backend.f_namemax = 255;
|
||||
backend.f_fsid[0] = 123;
|
||||
for (std::uint64_t flags = 0; flags != 16; ++flags) {
|
||||
backend.f_flags = flags;
|
||||
std::array<std::uint64_t, 13> output;
|
||||
output.fill(0xA5A5A5A5A5A5A5A5ull);
|
||||
const std::array<std::uint64_t, 11> expected{
|
||||
0x100000000ull, 0x100000001ull, 0x100000002ull,
|
||||
0x200000001ull, 0x200000001ull, 0x200000002ull,
|
||||
65536, (flags & 1) | ((flags & 8) ? 2u : 0u), 4096, 0, 255
|
||||
};
|
||||
const int before = calls;
|
||||
guestError = 12345;
|
||||
Require(fstatvfs_nid_postfix(42, output.data() + 1) == 0);
|
||||
Require(calls == before + 1 && guestError == 12345);
|
||||
Require(output.front() == 0xA5A5A5A5A5A5A5A5ull && output.back() == output.front());
|
||||
Require(std::memcmp(output.data() + 1, expected.data(), sizeof(expected)) == 0);
|
||||
}
|
||||
backend.f_bavail = -1;
|
||||
backend.f_ffree = -2;
|
||||
std::array<std::uint64_t, 11> output{};
|
||||
Require(fstatvfs_nid_postfix(42, output.data()) == 0);
|
||||
Require(output[0] == std::numeric_limits<std::uint64_t>::max());
|
||||
Require(output[3] == std::numeric_limits<std::uint64_t>::max() - 1 && output[4] == output[3]);
|
||||
Require(fstatvfs_nid_postfix(42, nullptr) == -1 && guestError == 14);
|
||||
for (int failure : {5, 9, 22, 45}) {
|
||||
backendError = failure;
|
||||
output.fill(0x5A5A5A5A5A5A5A5Aull);
|
||||
const auto saved = output;
|
||||
Require(fstatvfs_nid_postfix(42, output.data()) == -1 && guestError == failure);
|
||||
Require(output == saved);
|
||||
Require(fstatvfs_nid_postfix(42, nullptr) == -1 && guestError == failure);
|
||||
}
|
||||
}
|
||||
|
|
@ -619,6 +619,7 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- [Null vertex buffer V#](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware.
|
||||
- [Descriptor pool recycling](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured.
|
||||
- [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields
|
||||
- [fstatvfs](../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run)
|
||||
- [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert
|
||||
- [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not.
|
||||
- [sceAgcCreatePrimState](../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well
|
||||
|
|
|
|||
Loading…
Reference in New Issue