Merge remote-tracking branch 'origin/main' into fix/json2-special-float-modes

This commit is contained in:
Kyon committed 2026-10-23 09:58:05 +02:00
commit dfbdbfbada
45 files changed
+1714 -218

No files matched your search

+3
View File
@@ -22,6 +22,9 @@
[submodule "3rdparty/ffmpeg-core"]
path = 3rdparty/ffmpeg-core
url = https://github.com/KytyPS5/ext-ffmpeg-core.git
[submodule "3rdparty/opus"]
path = 3rdparty/opus
url = https://github.com/xiph/opus.git
[submodule "3rdparty/freetype"]
path = 3rdparty/freetype
url = https://github.com/freetype/freetype.git
Vendored Submodule
+1
Submodule 3rdparty/opus added at 22244de5a7.
+2 -1
View File
@@ -169,7 +169,7 @@ if(BUILD_TESTING)
endif()
add_test(NAME ${conventions_test_name} COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py --shard ${shard}/12)
set_tests_properties(${conventions_test_name} PROPERTIES TIMEOUT 30)
set_tests_properties(${conventions_test_name} PROPERTIES TIMEOUT 120)
endforeach()
add_test(NAME conventions_wrapped_declarations COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py)
@@ -348,6 +348,7 @@ add_subdirectory(3rdparty/libusb-cmake EXCLUDE_FROM_ALL)
set_target_properties(usb-1.0 PROPERTIES POSITION_INDEPENDENT_CODE ON)
include(cmake/ZArchive.cmake)
include(cmake/Opus.cmake)
if(WIN32)
set(MI_BUILD_SHARED ON CACHE BOOL "" FORCE)
set(MI_BUILD_STATIC OFF CACHE BOOL "" FORCE)
+19
View File
@@ -0,0 +1,19 @@
function(add_bundled_opus)
set(BUILD_SHARED_LIBS OFF)
set(BUILD_TESTING OFF)
set(OPUS_BUILD_SHARED_LIBRARY OFF)
set(OPUS_BUILD_TESTING OFF)
set(OPUS_BUILD_PROGRAMS OFF)
set(OPUS_INSTALL_PKG_CONFIG_MODULE OFF)
set(OPUS_INSTALL_CMAKE_CONFIG_MODULE OFF)
set(OPUS_STACK_PROTECTOR OFF)
set(OPUS_FORTIFY_SOURCE OFF)
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 OR CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_options(opus INTERFACE LINKER:--exclude-libs,libopus.a)
endif()
endfunction()
add_bundled_opus()
+21
View File
@@ -695,6 +695,17 @@ if(BUILD_TESTING)
configure_windows_unwind(savedata_replace_file_failure_tests)
add_test(NAME savedata_replace_file_failure COMMAND savedata_replace_file_failure_tests)
add_test_executable(guest_kernel_sys_ktimer_tests tests/GuestKernelSysKtimer.cpp)
target_include_directories(guest_kernel_sys_ktimer_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_kernel_sys_ktimer_tests PRIVATE libkernel_sys libkernel libc)
configure_windows_unwind(guest_kernel_sys_ktimer_tests)
add_test(NAME guest_kernel_sys_ktimer COMMAND guest_kernel_sys_ktimer_tests)
set_tests_properties(guest_kernel_sys_ktimer PROPERTIES TIMEOUT 20)
if(WIN32)
set_tests_properties(guest_kernel_sys_ktimer PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
endif()
add_test_executable(guest_crt_tests tests/GuestCrt.cpp)
target_include_directories(guest_crt_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_crt_tests PRIVATE libSceAmpr libc)
@@ -778,4 +789,14 @@ if(BUILD_TESTING)
set_tests_properties(guest_vorbis_dec PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceVorbisDec>")
endif()
add_test_executable(guest_gnm_neo_default_state_tests tests/GuestGnmNeoDefaultState.cpp)
target_include_directories(guest_gnm_neo_default_state_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_gnm_neo_default_state_tests PRIVATE libSceGnmDriverForNeoMode libc)
configure_windows_unwind(guest_gnm_neo_default_state_tests)
add_test(NAME guest_gnm_neo_default_state COMMAND guest_gnm_neo_default_state_tests)
if(WIN32)
set_tests_properties(guest_gnm_neo_default_state PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceGnmDriverForNeoMode>")
endif()
endif()
@@ -81,6 +81,16 @@ struct DccKeyProof {
// APS5_NO_KEY_FAST_PATH=1 scans on every call and stores nothing.
DccKeys ProvedClearKeys(const GuestTextureResource& resource, std::uint64_t guestBytes, DccKeyProof& proof);
bool KeyFastPath();
bool SingleKeysReadable(const GuestTextureResource& resource);
bool ReadSingleElement(const GuestTextureResource& resource, std::span<std::byte> element);
struct DccSingleProof {
std::uint64_t address = 0;
std::uint64_t bytes = 0;
std::uint64_t generation = 0;
std::uint32_t format = 0;
bool uniform = false;
};
bool ProvedSingleUniform(const GuestTextureResource& resource, std::uint64_t guestBytes, std::uint64_t collected, DccSingleProof& proof);
// Cumulative outcomes of ProvedClearKeys: calls answered by a proof, scans made, and scans not kept.
struct DccKeyProofCounts {
std::uint64_t proved;
@@ -580,6 +580,7 @@ private:
mutable DccKeys filledKeys = DccKeys::Uncompressed;
mutable DccKeyProof keyProof;
mutable DccRangeProof targetKeyProof;
DccSingleProof singleProof;
struct RefreshProof {
std::uint64_t generation = 0;
std::uint64_t pendingSerial = 0;
@@ -57,10 +57,6 @@ DccKeys ByteKeys(std::uint8_t key) {
}
}
bool SingleKeysReadable(const GuestTextureResource& resource) {
return KeyBytes % BytesPerElement(resource.format) == 0;
}
void ExpandSingleBlocks(std::span<std::byte> bytes, std::size_t elementBytes) {
for (std::size_t block = 0; block < bytes.size(); block += KeyBytes) {
const auto blockBytes = std::min<std::size_t>(KeyBytes, bytes.size() - block);
@@ -778,6 +774,44 @@ void ReadTextureSurface(const GuestTextureResource& resource, DccKeys keys, std:
if (keys == DccKeys::ClearSingle) ExpandSingleBlocks(bytes, BytesPerElement(resource.format));
}
bool SingleKeysReadable(const GuestTextureResource& resource) {
return KeyBytes % BytesPerElement(resource.format) == 0;
}
bool ReadSingleElement(const GuestTextureResource& resource, std::span<std::byte> element) {
if (!SingleKeysReadable(resource)) return false;
const auto bytes = BytesPerElement(resource.format);
if (element.size() < bytes) return false;
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureRead);
GuestMemory::ReadCommitted(resource.baseAddress, element.subspan(0, bytes));
return true;
}
bool ProvedSingleUniform(const GuestTextureResource& resource, std::uint64_t guestBytes, std::uint64_t collected, DccSingleProof& proof) {
if (!SingleKeysReadable(resource) || guestBytes < KeyBytes) return false;
const auto bytes = BytesPerElement(resource.format);
if (bytes == 0 || bytes > 16) return false;
const auto blocks = static_cast<std::size_t>(guestBytes / KeyBytes);
if (proof.generation != 0 && proof.address == resource.baseAddress && proof.bytes == guestBytes && proof.format == resource.format && collected != 0 && GuestMemory::UnchangedSince(resource.baseAddress, static_cast<std::size_t>(guestBytes), proof.generation)) return proof.uniform;
std::byte first[16]{};
bool uniform = true;
std::vector<std::byte> buffer(std::min<std::uint64_t>(guestBytes, std::uint64_t(1) << 20));
{
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureRead);
GuestMemory::ReadCommitted(resource.baseAddress, std::span(first, bytes));
for (std::size_t block = 1; block < blocks && uniform;) {
const auto offset = block * KeyBytes;
const auto size = static_cast<std::size_t>(std::min<std::uint64_t>(buffer.size(), guestBytes - offset));
GuestMemory::ReadCommitted(resource.baseAddress + offset, std::span(buffer).first(size));
const auto covered = size / KeyBytes;
for (std::size_t index = 0; index < covered && uniform; ++index) uniform = std::equal(first, first + bytes, buffer.begin() + index * KeyBytes);
block += covered;
}
}
proof = collected != 0 ? DccSingleProof{resource.baseAddress, guestBytes, collected, resource.format, uniform} : DccSingleProof{};
return uniform;
}
namespace {
DccKeys EncodedKeys(const GuestTextureResource& resource, DccKeys keys) {
@@ -1064,7 +1064,7 @@ void FlushForAccess(std::uint64_t address, std::size_t bytes) {
} else {
stored = StorageTexture::FlushPending(address, bytes, nullptr, "memory access", scope, &published);
}
if (stored || published) {
if (stored || published || (completionDepth == 0 && SnapshotOverlaps(address, bytes))) {
// Stores into imported memory were only recorded; the CPU is about to read them. The wait
// targets the batch holding them (the open one: SyncThrough submits it) and runs with the
// mutex released when this acquisition is the outermost; a nested hook waits locked through
@@ -2549,6 +2549,17 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes,
constexpr std::uint64_t GenerationBaselineBytes = 1ull << 20;
bool SingleKeyColorFor(const GuestTextureResource& resource, std::uint64_t guestBytes, std::uint64_t collected, DccSingleProof& proof, VkFormat format, DccKeys keys, VkClearColorValue& clear) {
if (keys != DccKeys::ClearSingle || !ProvedSingleUniform(resource, guestBytes, collected, proof)) return false;
const auto elementBytes = BytesPerElement(resource.format);
if (elementBytes == 0 || elementBytes > sizeof(std::uint32_t) * 4) return false;
std::uint32_t pattern[4]{};
auto* bytes = reinterpret_cast<std::byte*>(pattern);
if (!ReadSingleElement(resource, std::span(bytes, elementBytes))) return false;
for (std::size_t at = elementBytes; at < sizeof(pattern); at += elementBytes) std::memcpy(bytes + at, bytes, elementBytes);
return FillClearColor(format, elementBytes, pattern, clear);
}
std::vector<bool> StorageTexture::cpuUploadLayers(const std::vector<bool>& units) const {
if (descriptor.dimension != TextureDimension::k2DArray || descriptor.mipCount != 1 || descriptor.dccAddress != 0 || IsBlockCompressed(descriptor.format) || geometry.thick || arrayLayers <= 1 || geometry.imageLayers != arrayLayers || trackedLayerBytes == 0 || units.size() != trackedLayers) return {};
std::vector<bool> selected(arrayLayers, false);
@@ -2597,7 +2608,8 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
uploadedKeys = ProvedClearKeys(descriptor, guestBytes, keyProof);
filledKeys = DccKeys::Uncompressed;
VkClearColorValue clearValue{};
if (IsDccClear(uploadedKeys) && ClearColorFor(storageFormat, uploadedKeys, clearValue)) {
const bool fastClear = IsDccClear(uploadedKeys) || uploadedKeys == DccKeys::ClearSingle;
if (fastClear && (ClearColorFor(storageFormat, uploadedKeys, clearValue) || SingleKeyColorFor(descriptor, guestBytes, now, singleProof, storageFormat, uploadedKeys, clearValue))) {
// A fast-cleared surface is cleared on the GPU; its texel memory is neither read nor filled.
// The clear is recorded into the open batch like a direct upload (the image kept by it): a
// batch of its own submitted the recorder's work first and waited for all of it, 25-40 ms
@@ -47,6 +47,7 @@ struct VulkanDeviceTestAccess {
namespace {
using AgcDriver::Graphics::DccKeys;
using AgcDriver::Graphics::LookupOutcomes;
using AgcDriver::Graphics::Require;
using AgcDriver::Graphics::StorageTexture;
using ShaderRecompiler::ShaderStage;
@@ -451,6 +452,46 @@ void RunSingleKeys(AgcDriver::VulkanDevice& device, std::uint8_t* block) {
Require(wrong == 0, message);
}
void RunUniformSingleClear(AgcDriver::VulkanDevice& device, std::uint8_t* block) {
auto* texels = block;
auto* keys = block + KeysOffset;
const auto surface = AddressOf(texels);
const std::uint32_t color = 0x11223344u;
std::memset(texels, 0x55, SurfaceBytes);
for (std::size_t offset = 0; offset < SurfaceBytes; offset += 256) std::memcpy(texels + offset, &color, 4);
std::memset(keys, 0x10, KeyBytes);
AgcDriver::GuestMemory::BumpCollectEpoch();
const auto texture = TextureDescriptor(texels, keys);
std::vector<std::uint32_t> writeData(16, 0u);
std::copy(texture.begin(), texture.end(), writeData.begin() + 4);
const auto uploads = AgcDriver::Graphics::ThreadLookupOutcomes().counts;
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
Dispatch(device, WriteCode, writeData);
const auto image = StorageTexture::FindPending(surface, SurfaceBytes);
Require(image != nullptr && image->UploadedKeys() == DccKeys::Uncompressed, "uniform single-key clear: the first write did not store the keys uncompressed");
const auto& uploaded = AgcDriver::Graphics::ThreadLookupOutcomes().counts;
Require(uploaded[LookupOutcomes::UploadClear] == uploads[LookupOutcomes::UploadClear] + 1 && uploaded[LookupOutcomes::UploadCpu] == uploads[LookupOutcomes::UploadCpu], "uniform single-key clear: the first write read the surface and uploaded it instead of clearing it on the GPU");
const bool stillWatched = AgcDriver::GuestMemory::Watched(surface, SurfaceBytes);
Require(image->GuestSnapshotValid() == !stillWatched, stillWatched ? "uniform single-key clear: the upload of write-watched memory kept a guest snapshot, so the surface was read rather than cleared" : "uniform single-key clear: the upload of untracked memory kept no guest bytes to compare");
StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test uniform single-key clear");
device.WaitIdle();
RequireKeys(keys, 0xff, "uniform single-key clear: the keys after the write-back");
const AgcDriver::Graphics::ColorTargetLayout layout(Side, Side, AgcDriver::Graphics::ColorTileMode::RenderTarget, 4);
std::size_t wrong = 0;
for (std::uint32_t y = 0; y < Side; ++y) {
for (std::uint32_t x = 0; x < Side; ++x) {
const auto offset = layout.Offset(x, y);
std::uint32_t texel = 0;
std::memcpy(&texel, texels + offset, 4);
const auto expected = y == 0 && x < Threads ? 0xffffffffu : color;
if (texel != expected) ++wrong;
}
}
char message[256];
std::snprintf(message, sizeof(message), "uniform single-key clear: %zu of %u written-back texels are neither the kernel's write nor the proved clear color 0x%08x", wrong, Side * Side, color);
Require(wrong == 0, message);
}
void RunSingleKeysSampled(AgcDriver::VulkanDevice& device, std::uint8_t* block, std::uint8_t* outputBlock, const char* memory) {
auto* texels = block;
auto* keys = block + KeysOffset;
@@ -787,6 +828,11 @@ void RunSharedImport(AgcDriver::VulkanDevice& device, std::uint8_t* block) {
int main() {
try {
#ifdef _WIN32
_putenv_s("APS5_PROFILE_DRAW", "1");
#else
setenv("APS5_PROFILE_DRAW", "1", 1);
#endif
std::optional<GuestBlock> block;
std::optional<GuestBlock> watched;
std::optional<GuestBlock> untracked;
@@ -806,6 +852,9 @@ int main() {
std::optional<GuestBlock> single;
single.emplace(false);
RunSingleKeys(*device, single->Data());
std::optional<GuestBlock> uniformClear;
uniformClear.emplace(false);
RunUniformSingleClear(*device, uniformClear->Data());
std::optional<GuestBlock> sampled;
sampled.emplace(false);
std::optional<GuestBlock> sampledOutput;
@@ -828,6 +877,9 @@ int main() {
std::optional<GuestBlock> sampledWatched;
sampledWatched.emplace(true);
RunSingleKeysSampled(*device, sampledWatched->Data(), sampledOutput->Data(), "write-watched");
std::optional<GuestBlock> uniformClearWatched;
uniformClearWatched.emplace(true);
RunUniformSingleClear(*device, uniformClearWatched->Data());
#ifdef _WIN32
pendingSingle.emplace(true, true);
#else
@@ -22,6 +22,7 @@ if(BUILD_TESTING)
endif()
endif()
target_sources(libSceFreeType PRIVATE src/List.cpp)
if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_options(libSceFreeType PRIVATE LINKER:--exclude-libs,ALL)
endif()
@@ -87,3 +88,16 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceFreeType>")
endif()
endif()
if(BUILD_TESTING)
add_test_executable(guest_freetype_list_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeList.cpp)
target_include_directories(guest_freetype_list_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_freetype_list_tests PRIVATE libSceFreeType freetype libc)
configure_windows_unwind(guest_freetype_list_tests)
add_test(NAME guest_freetype_list COMMAND guest_freetype_list_tests)
set_tests_properties(guest_freetype_list PROPERTIES TIMEOUT 20)
if(WIN32)
set_tests_properties(guest_freetype_list PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceFreeType>")
endif()
endif()
@@ -0,0 +1,33 @@
#ifndef CORE_LIBS_PRX_LIBSCEFREETYPE_INCLUDE_GUESTLIST_HPP
#define CORE_LIBS_PRX_LIBSCEFREETYPE_INCLUDE_GUESTLIST_HPP
#include "prx/libc/include/general/VabiMacros.hpp"
#include <ft2build.h>
#include FT_LIST_H
#include <cstddef>
#include <cstdint>
namespace FreeType {
using GuestListRec = FT_ListRec;
using GuestListNodeRec = FT_ListNodeRec;
struct GuestMemoryRec {
void* user;
void* (APS5_VABI *alloc)(GuestMemoryRec*, std::int64_t);
void (APS5_VABI *free)(GuestMemoryRec*, void*);
void* (APS5_VABI *realloc)(GuestMemoryRec*, std::int64_t, std::int64_t, void*);
};
using GuestListIterator = int (APS5_VABI *)(GuestListNodeRec*, void*);
using GuestListDestructor = void (APS5_VABI *)(GuestMemoryRec*, void*, void*);
static_assert(sizeof(GuestListNodeRec) == 24 && offsetof(GuestListNodeRec, prev) == 0 && offsetof(GuestListNodeRec, next) == 8 && offsetof(GuestListNodeRec, data) == 16);
static_assert(sizeof(GuestListRec) == 16 && offsetof(GuestListRec, head) == 0 && offsetof(GuestListRec, tail) == 8);
static_assert(sizeof(GuestMemoryRec) == 32 && offsetof(GuestMemoryRec, alloc) == 8 && offsetof(GuestMemoryRec, free) == 16 && offsetof(GuestMemoryRec, realloc) == 24);
}
#endif
+101
View File
@@ -0,0 +1,101 @@
#include "prx/libSceFreeType/include/GuestList.hpp"
#include "prx/libc/include/GuestArena.hpp"
#include <array>
#include <stdexcept>
using namespace FreeType;
namespace {
class ListWrite {
public:
ListWrite(GuestListRec* list, std::array<GuestListNodeRec*, 4> nodes = {})
: writes{GuestArena::HostWrite(list, sizeof(*list)),
GuestArena::HostWrite(nodes[0], nodes[0] ? sizeof(*nodes[0]) : 0),
GuestArena::HostWrite(nodes[1], nodes[1] ? sizeof(*nodes[1]) : 0),
GuestArena::HostWrite(nodes[2], nodes[2] ? sizeof(*nodes[2]) : 0),
GuestArena::HostWrite(nodes[3], nodes[3] ? sizeof(*nodes[3]) : 0)} {
for (const auto& write : writes) {
if (!write.Open()) throw std::runtime_error("FT_List: a destination is not writable");
}
}
private:
std::array<GuestArena::HostWrite, 5> writes;
};
struct Iteration {
GuestListIterator iterator;
void* user;
};
int Iterate(FT_ListNode node, void* user) {
auto& call = *static_cast<Iteration*>(user);
return call.iterator(node, call.user);
}
struct Finalization {
GuestMemoryRec* memory;
GuestListDestructor destroy;
void* user;
};
void Destroy(FT_Memory memory, void* data, void*) {
auto& call = *static_cast<Finalization*>(memory->user);
call.destroy(call.memory, data, call.user);
}
void FreeNode(FT_Memory memory, void* block) {
auto& call = *static_cast<Finalization*>(memory->user);
call.memory->free(call.memory, block);
}
}
extern "C" {
GuestListNodeRec* APS5_VABI FT_List_Find_nid_postfix(GuestListRec* list, void* data) {
return FT_List_Find(list, data);
}
void APS5_VABI FT_List_Add_nid_postfix(GuestListRec* list, GuestListNodeRec* node) {
if (!list || !node) return;
const ListWrite write(list, {node, list->tail});
FT_List_Add(list, node);
}
void APS5_VABI FT_List_Insert_nid_postfix(GuestListRec* list, GuestListNodeRec* node) {
if (!list || !node) return;
const ListWrite write(list, {node, list->head});
FT_List_Insert(list, node);
}
void APS5_VABI FT_List_Remove_nid_postfix(GuestListRec* list, GuestListNodeRec* node) {
if (!list || !node) return;
const ListWrite write(list, {node->prev, node->next});
FT_List_Remove(list, node);
}
void APS5_VABI FT_List_Up_nid_postfix(GuestListRec* list, GuestListNodeRec* node) {
if (!list || !node || !node->prev) return;
const ListWrite write(list, {node, node->prev, node->next, list->head});
FT_List_Up(list, node);
}
int APS5_VABI FT_List_Iterate_nid_postfix(GuestListRec* list, GuestListIterator iterator, void* user) {
Iteration call{iterator, user};
return FT_List_Iterate(list, iterator ? Iterate : nullptr, &call);
}
void APS5_VABI FT_List_Finalize_nid_postfix(GuestListRec* list, GuestListDestructor destroy, GuestMemoryRec* memory, void* user) {
if (!list || !memory) return;
const ListWrite write(list);
Finalization call{memory, destroy, user};
FT_MemoryRec_ native{};
native.user = &call;
native.free = FreeNode;
FT_List_Finalize(list, destroy ? Destroy : nullptr, &native, user);
}
}
@@ -1,10 +1,32 @@
#include <cstdint>
#include <algorithm>
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp"
namespace {
constexpr std::uint32_t InitPacketWords = 0x100;
template <std::size_t Words>
int WriteHardwareState(std::uint32_t* cmdbuf, std::uint32_t size, const std::array<std::uint32_t, Words>& state) {
if (size < InitPacketWords) return 0;
auto* at = std::copy(GnmNeo::ClearState.begin(), GnmNeo::ClearState.end(), cmdbuf);
at = std::copy(state.begin(), state.end(), at);
const auto padding = static_cast<std::uint32_t>(cmdbuf + InitPacketWords - at - 1);
at[0] = 0xc0001000u | ((padding - 1) << 16);
at[1] = 0;
return InitPacketWords;
}
template <std::size_t Words>
int WriteContextState(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t packetWords, const std::array<std::uint32_t, Words>& state) {
if (size != packetWords) return 0;
std::copy(state.begin(), state.end(), cmdbuf);
return static_cast<int>(packetWords);
}
constexpr int Failure = static_cast<int>(0x8EEE00FFu);
constexpr int NoInterface = static_cast<int>(0x80000000u);
constexpr int ValidationNotEnabled = static_cast<int>(0x80D13FFFu);
@@ -116,29 +138,24 @@ int APS5_VABI sceGnmDrawIndirectMulti(void) {
return 0;
}
int APS5_VABI sceGnmDrawInitDefaultHardwareState(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI sceGnmDrawInitDefaultHardwareState(std::uint32_t* cmdbuf, std::uint32_t size) {
return WriteHardwareState(cmdbuf, size, GnmNeo::HardwareState);
}
int APS5_VABI sceGnmDrawInitDefaultHardwareState200(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI sceGnmDrawInitDefaultHardwareState200(std::uint32_t* cmdbuf, std::uint32_t size) {
return WriteHardwareState(cmdbuf, size, GnmNeo::HardwareState200);
}
int APS5_VABI sceGnmDrawInitDefaultHardwareState350(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI sceGnmDrawInitDefaultHardwareState350(std::uint32_t* cmdbuf, std::uint32_t size) {
return WriteHardwareState(cmdbuf, size, GnmNeo::HardwareState350);
}
int APS5_VABI sceGnmDrawInitToDefaultContextState(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI sceGnmDrawInitToDefaultContextState(std::uint32_t* cmdbuf, std::uint32_t size) {
return WriteContextState(cmdbuf, size, 0x20, GnmNeo::ContextState);
}
int APS5_VABI sceGnmDrawInitToDefaultContextState400(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI sceGnmDrawInitToDefaultContextState400(std::uint32_t* cmdbuf, std::uint32_t size) {
return WriteContextState(cmdbuf, size, 0x100, GnmNeo::ContextState400);
}
int APS5_VABI sceGnmDrawInitToDefaultContextStateInternalCommand(void) {
@@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#ifndef CORE_LIBS_PRX_LIBSCEGNMDRIVERFORNEOMODE_INCLUDE_INITSEQUENCES_HPP
#define CORE_LIBS_PRX_LIBSCEGNMDRIVERFORNEOMODE_INCLUDE_INITSEQUENCES_HPP
#include <array>
#include <cstdint>
namespace GnmNeo {
constexpr std::array<std::uint32_t, 12> ClearState{
0xc0012800u, 0x80000000u, 0x80000000u, 0xc0001200u, 0x0u, 0xc0055800u, 0x2ec47fc0u, 0xffffffffu,
0x0u, 0x0u, 0x0u, 0xau,
};
constexpr std::array<std::uint32_t, 115> HardwareState{
0xc0017600u, 0x216u, 0xffffffffu, 0xc0017600u, 0x217u, 0xffffffffu, 0xc0017600u, 0x215u,
0x0u, 0xc0016900u, 0x2f9u, 0x2du, 0xc0016900u, 0x282u, 0x8u, 0xc0016900u,
0x280u, 0x80008u, 0xc0016900u, 0x281u, 0xffff0000u, 0xc0016900u, 0x204u, 0x0u,
0xc0016900u, 0x206u, 0x43fu, 0xc0016900u, 0x83u, 0xffffu, 0xc0016900u, 0x317u,
0x10u, 0xc0016900u, 0x2fau, 0x3f800000u, 0xc0016900u, 0x2fcu, 0x3f800000u, 0xc0016900u,
0x2fbu, 0x3f800000u, 0xc0016900u, 0x2fdu, 0x3f800000u, 0xc0016900u, 0x202u, 0xcc0010u,
0xc0016900u, 0x30eu, 0xffffffffu, 0xc0016900u, 0x30fu, 0xffffffffu, 0xc0002f00u, 0x1u,
0xc0017600u, 0x7u, 0x1ffu, 0xc0017600u, 0x46u, 0x1ffu, 0xc0017600u, 0x87u,
0x1ffu, 0xc0017600u, 0xc7u, 0x1ffu, 0xc0017600u, 0x107u, 0x0u, 0xc0017600u,
0x147u, 0x1ffu, 0xc0016900u, 0x1b1u, 0x2u, 0xc0016900u, 0x101u, 0x0u,
0xc0016900u, 0x100u, 0xffffffffu, 0xc0016900u, 0x103u, 0x0u, 0xc0016900u, 0x284u,
0x0u, 0xc0016900u, 0x290u, 0x0u, 0xc0016900u, 0x2aeu, 0x0u, 0xc0016900u,
0x292u, 0x0u, 0xc0016900u, 0x293u, 0x6000000u, 0xc0016900u, 0x2f8u, 0x0u,
0xc0016900u, 0x2deu, 0x1e9u, 0xc0036900u, 0x295u, 0x100u, 0x100u, 0x4u,
0xc0017900u, 0x200u, 0xe0000000u,
};
constexpr std::array<std::uint32_t, 131> HardwareState200{
0xc0017600u, 0x216u, 0xffffffffu, 0xc0017600u, 0x217u, 0xffffffffu, 0xc0017600u, 0x219u,
0xffffffffu, 0xc0017600u, 0x21au, 0xffffffffu, 0xc0017600u, 0x215u, 0x0u, 0xc0016900u,
0x2f9u, 0x2du, 0xc0016900u, 0x282u, 0x8u, 0xc0016900u, 0x280u, 0x80008u,
0xc0016900u, 0x281u, 0xffff0000u, 0xc0016900u, 0x204u, 0x0u, 0xc0016900u, 0x206u,
0x43fu, 0xc0016900u, 0x83u, 0xffffu, 0xc0016900u, 0x317u, 0x10u, 0xc0016900u,
0x2fau, 0x3f800000u, 0xc0016900u, 0x2fcu, 0x3f800000u, 0xc0016900u, 0x2fbu, 0x3f800000u,
0xc0016900u, 0x2fdu, 0x3f800000u, 0xc0016900u, 0x202u, 0xcc0010u, 0xc0016900u, 0x30eu,
0xffffffffu, 0xc0016900u, 0x30fu, 0xffffffffu, 0xc0002f00u, 0x1u, 0xc0017600u, 0x7u,
0x1701ffu, 0xc0017600u, 0x46u, 0x1701fdu, 0xc0017600u, 0x87u, 0x1701ffu, 0xc0017600u,
0xc7u, 0x1701fdu, 0xc0017600u, 0x107u, 0x17u, 0xc0017600u, 0x147u, 0x1701fdu,
0xc0017600u, 0x47u, 0x1cu, 0xc0016900u, 0x1b1u, 0x2u, 0xc0016900u, 0x101u,
0x0u, 0xc0016900u, 0x100u, 0xffffffffu, 0xc0016900u, 0x103u, 0x0u, 0xc0016900u,
0x284u, 0x0u, 0xc0016900u, 0x290u, 0x0u, 0xc0016900u, 0x2aeu, 0x0u,
0xc0016900u, 0x292u, 0x0u, 0xc0016900u, 0x293u, 0x6020000u, 0xc0016900u, 0x2f8u,
0x0u, 0xc0016900u, 0x2deu, 0x1e9u, 0xc0026900u, 0xebu, 0xff00ff00u, 0xff00u,
0xc0036900u, 0x295u, 0x100u, 0x100u, 0x4u, 0xc0017900u, 0x200u, 0xe0000000u,
0xc0017900u, 0x40000258u, 0x6d007fu,
};
constexpr std::array<std::uint32_t, 134> HardwareState350{
0xc0017600u, 0x216u, 0xffffffffu, 0xc0017600u, 0x217u, 0xffffffffu, 0xc0017600u, 0x219u,
0xffffffffu, 0xc0017600u, 0x21au, 0xffffffffu, 0xc0017600u, 0x215u, 0x0u, 0xc0016900u,
0x2f9u, 0x2du, 0xc0016900u, 0x282u, 0x8u, 0xc0016900u, 0x280u, 0x80008u,
0xc0016900u, 0x281u, 0xffff0000u, 0xc0016900u, 0x204u, 0x0u, 0xc0016900u, 0x206u,
0x43fu, 0xc0016900u, 0x83u, 0xffffu, 0xc0016900u, 0x317u, 0x10u, 0xc0016900u,
0x2fau, 0x3f800000u, 0xc0016900u, 0x2fcu, 0x3f800000u, 0xc0016900u, 0x2fbu, 0x3f800000u,
0xc0016900u, 0x2fdu, 0x3f800000u, 0xc0016900u, 0x202u, 0xcc0010u, 0xc0016900u, 0x30eu,
0xffffffffu, 0xc0016900u, 0x30fu, 0xffffffffu, 0xc0002f00u, 0x1u, 0xc0017600u, 0x7u,
0x1701ffu, 0xc0017600u, 0x46u, 0x1701fdu, 0xc0017600u, 0x87u, 0x1701ffu, 0xc0017600u,
0xc7u, 0x1701fdu, 0xc0017600u, 0x107u, 0x17u, 0xc0017600u, 0x147u, 0x1701fdu,
0xc0017600u, 0x47u, 0x1cu, 0xc0016900u, 0x1b1u, 0x2u, 0xc0016900u, 0x101u,
0x0u, 0xc0016900u, 0x100u, 0xffffffffu, 0xc0016900u, 0x103u, 0x0u, 0xc0016900u,
0x284u, 0x0u, 0xc0016900u, 0x290u, 0x0u, 0xc0016900u, 0x2aeu, 0x0u,
0xc0016900u, 0x102u, 0x0u, 0xc0016900u, 0x292u, 0x0u, 0xc0016900u, 0x293u,
0x6020000u, 0xc0016900u, 0x2f8u, 0x0u, 0xc0016900u, 0x2deu, 0x1e9u, 0xc0026900u,
0xebu, 0xff00ff00u, 0xff00u, 0xc0036900u, 0x295u, 0x100u, 0x100u, 0x4u,
0xc0017900u, 0x200u, 0xe0000000u, 0xc0017900u, 0x40000258u, 0x6d007fu,
};
constexpr std::array<std::uint32_t, 19> ContextState{
0xc0012800u, 0x80000000u, 0x80000000u, 0xc0001200u, 0x0u, 0xc0002f00u, 0x1u, 0xc0016900u,
0x102u, 0x0u, 0xc0016900u, 0x202u, 0xcc0010u, 0xc0026900u, 0xebu, 0xff00ff00u,
0xff00u, 0xc00d1000u, 0x0u,
};
constexpr std::array<std::uint32_t, 102> ContextState400{
0xc0012800u, 0x80000000u, 0x80000000u, 0xc0001200u, 0x0u, 0xc0016900u, 0x2f9u, 0x2du,
0xc0016900u, 0x282u, 0x8u, 0xc0016900u, 0x280u, 0x80008u, 0xc0016900u, 0x281u,
0xffff0000u, 0xc0016900u, 0x204u, 0x0u, 0xc0016900u, 0x206u, 0x43fu, 0xc0016900u,
0x83u, 0xffffu, 0xc0016900u, 0x317u, 0x10u, 0xc0016900u, 0x2fau, 0x3f800000u,
0xc0016900u, 0x2fcu, 0x3f800000u, 0xc0016900u, 0x2fbu, 0x3f800000u, 0xc0016900u, 0x2fdu,
0x3f800000u, 0xc0016900u, 0x202u, 0xcc0010u, 0xc0016900u, 0x30eu, 0xffffffffu, 0xc0016900u,
0x30fu, 0xffffffffu, 0xc0002f00u, 0x1u, 0xc0016900u, 0x1b1u, 0x2u, 0xc0016900u,
0x101u, 0x0u, 0xc0016900u, 0x100u, 0xffffffffu, 0xc0016900u, 0x103u, 0x0u,
0xc0016900u, 0x284u, 0x0u, 0xc0016900u, 0x290u, 0x0u, 0xc0016900u, 0x2aeu,
0x0u, 0xc0016900u, 0x102u, 0x0u, 0xc0016900u, 0x292u, 0x0u, 0xc0016900u,
0x293u, 0x6020000u, 0xc0016900u, 0x2f8u, 0x0u, 0xc0016900u, 0x2deu, 0x1e9u,
0xc0026900u, 0xebu, 0xff00ff00u, 0xff00u, 0xc0036900u, 0x295u, 0x100u, 0x100u,
0x4u, 0xc0017900u, 0x40000258u, 0x6d007fu, 0xc09a1000u, 0x0u,
};
} // namespace GnmNeo
#endif
@@ -84,7 +84,8 @@ static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH = 0x400
static constexpr std::uint32_t SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP = 0x40020000;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER = 0x40001300;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN = 0x40001500;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX = 0x40001d00;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX = 0x40001900;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_DELAY = 0x40001d00;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 = 0x40001f00;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_MODULES = 24;
@@ -98,7 +99,8 @@ static constexpr std::uint32_t SCE_NGS2_CUSTOM_PITCH_SHIFT_STATE_SIZE = 8;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_FILTER = 0x13;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_GAIN = 0x15;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_REVERB = 0x1b;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX = 0x1d;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX = 0x19;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_DELAY = 0x1d;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2 = 0x1f;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT = 0x20;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_CONVOLUTION = 0x23;
@@ -213,6 +215,15 @@ struct Ngs2CustomModuleOption {
std::uint32_t size;
};
struct Ngs2CustomDelayModuleOption {
Ngs2CustomModuleOption custom_module_option;
std::uint32_t type;
std::uint32_t max_taps;
float max_length;
std::uint32_t reserved;
};
static_assert(sizeof(Ngs2CustomDelayModuleOption) == 20);
struct Ngs2UserFx2SetupContext {
void* common;
void* param;
+24 -2
View File
@@ -30,7 +30,7 @@ static std::uint32_t OptionWord(const Ngs2CustomModuleOption& option) {
static int CheckBuiltInModule(const Ngs2CustomRackModuleInfo& info) {
const auto stateSizeIs = [&info](std::uint32_t size) { return info.state_size == 0 || info.state_size == size; };
if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) {
if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_GAIN || info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) {
if (!stateSizeIs(0)) return SCE_NGS2_ERROR_INVALID_STATE_SIZE;
} else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_REVERB) {
if (!stateSizeIs(0)) return SCE_NGS2_ERROR_INVALID_STATE_SIZE;
@@ -45,6 +45,21 @@ static int CheckBuiltInModule(const Ngs2CustomRackModuleInfo& info) {
return SCE_NGS2_OK;
}
static void CheckDelayModule(std::uint32_t rackId, const Ngs2CustomRackModuleInfo& info) {
if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER || info.state_size != 0 || info.state_offset != 0) {
throw std::runtime_error("NGS2: this custom Delay rack or module state is not implemented");
}
if (info.option == nullptr || info.option->size != sizeof(Ngs2CustomDelayModuleOption)) {
throw std::runtime_error("NGS2: this custom Delay module option size is not implemented");
}
Ngs2CustomDelayModuleOption delay{};
std::memcpy(&delay, info.option, sizeof(delay));
if (delay.type > 1 || delay.max_taps == 0 || delay.max_taps > 8 ||
!(delay.max_length >= 1.0f && delay.max_length <= 20000.0f) || delay.reserved != 0) {
throw std::runtime_error("NGS2: these custom Delay module option values are not implemented");
}
}
int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option, std::uint32_t maxInputs) {
if (option.num_buffers == 0 || option.num_buffers > SCE_NGS2_CUSTOM_MAX_BUFFERS || option.num_buffers < maxInputs) {
return SCE_NGS2_ERROR_INVALID_NUM_BUFFERS;
@@ -58,7 +73,10 @@ int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option
} else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_FILTER) {
if (info.option != nullptr || info.state_size != 0 || info.state_offset != 0) APS5_INVALID_ARG_EX;
} else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) {
if (info.option != nullptr && info.option->size < sizeof(Ngs2CustomModuleOption)) APS5_INVALID_ARG_EX;
if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::runtime_error("NGS2: the custom UserFx module on a custom sampler rack is not implemented");
if (info.option != nullptr) throw std::runtime_error("NGS2: a custom UserFx module option is not implemented");
} else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_DELAY) {
CheckDelayModule(rackId, info);
} else if (BuiltInModuleName(info.module_id) == nullptr) {
throw std::runtime_error("NGS2: custom module id " + Ngs2Hex(info.module_id) + " is not implemented");
}
@@ -186,6 +204,9 @@ static void ProcessGain(Ngs2Voice& voice, Ngs2VoiceCustomGain& gain, const std::
void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) {
const auto rackId = voice.rack->rackId;
if ((param.id & ~0xffu) == SCE_NGS2_CUSTOM_VOICE_PARAM_DELAY) {
throw std::runtime_error("NGS2: the custom Delay module parameters are not implemented");
}
if (param.id == SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP) {
if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack");
SetupCustomSubmixer(voice, ParamAs<Ngs2CustomSubmixerVoiceSetupParam>(param));
@@ -242,6 +263,7 @@ void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t samp
std::vector<float*> channels(voice.channels);
for (std::uint32_t c = 0; c < voice.channels; c++) channels[c] = voice.samples.data() + static_cast<std::size_t>(c) * grain;
for (std::size_t m = 0; m < voice.rack->customModuleIds.size(); m++) {
if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_DELAY) continue;
if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) {
ProcessGain(voice, voice.customGain[m], channels, grain);
} else if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) {
@@ -2,7 +2,9 @@
#include <cstdint>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <string>
#include <vector>
struct Calls {
@@ -434,6 +436,7 @@ static void TestCustomRackChecks() {
option.max_channels = SCE_NGS2_CUSTOM_SUBMIXER_MAX_CHANNELS;
Require(CreateThrows<std::runtime_error>(option));
Require(create(BuiltInRackOption(userFx2, 0x19, nullptr, 4)) == SCE_NGS2_ERROR_INVALID_STATE_SIZE);
Require(!CreateThrows<std::runtime_error>(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 0)));
Require(create(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 4)) == SCE_NGS2_ERROR_INVALID_STATE_SIZE);
const ModuleOptionWords gainOption{sizeof(Ngs2CustomModuleOption), {}};
@@ -468,7 +471,7 @@ static void TestCustomRackChecks() {
option.custom_rack_option.module[1] = good.custom_rack_option.module[0];
option.custom_rack_option.module[1].dest_buffer_id = 1;
Require(create(option) == SCE_NGS2_ERROR_INVALID_BUFFER_INDEX);
Require(CreateThrows<std::runtime_error>(BuiltInRackOption(userFx2, 0x19, nullptr, 0)));
Require(CreateThrows<std::runtime_error>(BuiltInRackOption(userFx2, 0x1d, nullptr, 0)));
option = good;
option.custom_rack_option.num_buffers = 0;
@@ -500,6 +503,29 @@ static void TestCustomRackChecks() {
Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK);
sampler.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_GAIN;
Require(CreateSamplerThrows<std::runtime_error>(sampler));
Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK);
for (const auto count : {1u, 2u, 24u}) {
sampler.custom_rack_option.num_modules = count;
for (std::uint32_t index = 0; index < count; ++index) {
sampler.custom_rack_option.module[index].module_id = SCE_NGS2_CUSTOM_MODULE_ID_FILTER;
}
sampler.custom_rack_option.module[count - 1].module_id = 0x19;
Require(CreateSamplerThrows<std::runtime_error>(sampler));
for (const bool useAllocator : {false, true}) {
rack = 0;
rejected = false;
try {
if (useAllocator) {
sceNgs2RackCreateWithAllocator(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &allocator, &rack);
} else {
sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &info, &rack);
}
} catch (const std::runtime_error&) {
rejected = true;
}
Require(rejected && rack == 0 && allocCalls == 0);
}
}
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
}
@@ -512,6 +538,8 @@ struct LegacyUserFxState {
};
static std::int32_t APS5_VABI ProcessLegacy(Ngs2UserFxProcessContext* context) {
Require(context->user_data1 == 123 && context->user_data2 == 456);
Require(context->num_channels == 2 && context->num_grain_samples == Grain && context->sample_rate == 48000);
auto& state = *reinterpret_cast<LegacyUserFxState*>(context->user_data0);
state.calls++;
state.flags = context->flags;
@@ -523,7 +551,7 @@ static std::int32_t APS5_VABI ProcessLegacy(Ngs2UserFxProcessContext* context) {
return SCE_NGS2_OK;
}
static Ngs2CustomSubmixerRackOption LegacyRackOption(const Ngs2CustomModuleOption* option) {
static Ngs2CustomSubmixerRackOption LegacyRackOption(const Ngs2CustomModuleOption* option, std::uint32_t moduleIndex) {
Ngs2CustomSubmixerRackOption rack{};
auto& common = rack.custom_rack_option.rack_option;
common.size = sizeof(rack);
@@ -532,27 +560,28 @@ static Ngs2CustomSubmixerRackOption LegacyRackOption(const Ngs2CustomModuleOptio
common.max_matrices = 1;
common.max_ports = 1;
rack.custom_rack_option.num_buffers = 1;
rack.custom_rack_option.num_modules = 1;
rack.custom_rack_option.module[0].option = option;
rack.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_USER_FX;
rack.custom_rack_option.module[0].state_size = 16;
rack.custom_rack_option.num_modules = moduleIndex + 1;
for (std::uint32_t index = 0; index <= moduleIndex; ++index) {
rack.custom_rack_option.module[index].option = option;
rack.custom_rack_option.module[index].module_id = 0x19;
}
rack.max_channels = 2;
rack.max_inputs = 1;
return rack;
}
static void TestCustomLegacyUserFx() {
static void TestCustomLegacyUserFx(std::uint32_t moduleIndex) {
const auto system = CreateSystem();
const auto master = Mastering(system, 2);
const Ngs2CustomModuleOption header{sizeof(Ngs2CustomModuleOption)};
const auto rack = CustomRack(system, LegacyRackOption(&header));
const auto rack = CustomRack(system, LegacyRackOption(nullptr, moduleIndex));
const auto custom = Voice(rack);
Control(custom, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0});
Patch(custom, master);
Event(custom, SCE_NGS2_VOICE_EVENT_PLAY);
LegacyUserFxState state{};
Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 0,
Control(custom, 0x40001900 | moduleIndex,
Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, reinterpret_cast<std::uintptr_t>(&state), 123, 456});
auto out = Render(system);
@@ -568,20 +597,11 @@ static void TestCustomLegacyUserFx() {
std::uint8_t dummy[10];
};
Require(ControlThrows<std::invalid_argument>(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 0, BadSizeParam{}));
Require(ControlThrows<std::invalid_argument>(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 2, Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, 0, 0, 0}));
Require(ControlThrows<std::invalid_argument>(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 31, Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, 0, 0, 0}));
Ngs2CustomModuleOption badSize{2};
Require(CreateThrows<std::invalid_argument>(LegacyRackOption(&badSize)));
struct ExtendedOption {
std::uint32_t size = 20;
std::uint32_t reserved = 0;
std::uint32_t channels = 4;
float param0 = 1000.0f;
float param1 = 0.0f;
} extendedOption;
const auto rackExtended = CustomRack(system, LegacyRackOption(reinterpret_cast<const Ngs2CustomModuleOption*>(&extendedOption)));
Require(sceNgs2RackDestroy(rackExtended, nullptr) == SCE_NGS2_OK);
Require(CreateThrows<std::runtime_error>(LegacyRackOption(&header, moduleIndex)));
Require(ControlThrows<std::runtime_error>(custom, 0x40001d00,
Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, reinterpret_cast<std::uintptr_t>(&state), 123, 456}));
state.result = -1;
bool failed = false;
@@ -595,7 +615,7 @@ static void TestCustomLegacyUserFx() {
Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK);
const auto rackNullOpt = CustomRack(system, LegacyRackOption(nullptr));
const auto rackNullOpt = CustomRack(system, LegacyRackOption(nullptr, moduleIndex));
const auto voiceNullOpt = Voice(rackNullOpt);
Control(voiceNullOpt, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0});
Patch(voiceNullOpt, master);
@@ -604,7 +624,7 @@ static void TestCustomLegacyUserFx() {
LegacyUserFxState state2{};
state2.val0 = 0.125f;
state2.val1 = 0.75f;
Control(voiceNullOpt, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 0,
Control(voiceNullOpt, 0x40001900 | moduleIndex,
Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, reinterpret_cast<std::uintptr_t>(&state2), 123, 456});
out = Render(system);
Require(state2.calls == 1 && state2.flags == 1);
@@ -614,6 +634,92 @@ static void TestCustomLegacyUserFx() {
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
}
struct DelayOption {
std::uint32_t size = 20;
std::uint32_t type = 0;
std::uint32_t maxTaps = 4;
float maxLength = 1000.0f;
std::uint32_t reserved = 0;
};
static Ngs2CustomSubmixerRackOption DelayRackOption(const DelayOption& delay, std::uint32_t count) {
auto option = LegacyRackOption(nullptr, count - 1);
option.custom_rack_option.rack_option.max_ports = 8;
for (std::uint32_t index = 0; index < count; ++index) {
option.custom_rack_option.module[index].module_id = 0x1d;
option.custom_rack_option.module[index].option = reinterpret_cast<const Ngs2CustomModuleOption*>(&delay);
}
return option;
}
static void TestCustomDelay(std::uint32_t count, const DelayOption& delay) {
const auto system = CreateSystem();
const auto master = Mastering(system, 2);
auto option = DelayRackOption(delay, count);
const auto rack = CustomRack(system, option);
const auto custom = Voice(rack);
Control(custom, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0});
Patch(custom, master);
Event(custom, SCE_NGS2_VOICE_EVENT_PLAY);
for (const float sample : Render(system)) Require(sample == 0.0f);
std::vector<std::int16_t> pcm(Grain * 2 * 8);
for (std::uint32_t i = 0; i < pcm.size() / 2; ++i) {
pcm[i * 2] = static_cast<std::int16_t>(1000 + i * 17);
pcm[i * 2 + 1] = static_cast<std::int16_t>(-2000 + static_cast<int>(i) * 11);
}
const auto sampler = Sampler(system, pcm);
Patch(sampler, custom);
Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY);
const auto checkGrain = [&](std::uint32_t grain) {
const auto out = Render(system);
for (std::uint32_t i = 0; i < Grain * 2; ++i) {
Require(out[i] == static_cast<float>(pcm[grain * Grain * 2 + i]) / 32768.0f);
}
};
for (std::uint32_t grain = 0; grain < 3; ++grain) checkGrain(grain);
LegacyUserFxState state{};
for (std::uint32_t index = 0; index < 256; ++index) {
Ngs2CustomVoiceUserFxParam param{{sizeof(Ngs2CustomVoiceUserFxParam), 0, 0x40001d00 | index},
ProcessLegacy, reinterpret_cast<std::uintptr_t>(&state), 123, 456};
bool rejected = false;
try {
sceNgs2VoiceControl(custom, &param.header);
} catch (const std::runtime_error& error) {
const std::string message = error.what();
rejected = message.find("Delay") != std::string::npos && message.find("not implemented") != std::string::npos;
}
Require(rejected);
}
checkGrain(3);
Require(state.calls == 0);
Event(sampler, SCE_NGS2_VOICE_EVENT_STOP);
for (const float sample : Render(system)) Require(sample == 0.0f);
option.custom_rack_option.module[0].state_size = 4;
Require(CreateThrows<std::runtime_error>(option));
option.custom_rack_option.module[0].state_size = 0;
option.custom_rack_option.module[0].state_offset = 4;
Require(CreateThrows<std::runtime_error>(option));
option.custom_rack_option.module[0].state_offset = 0;
option.custom_rack_option.module[0].option = nullptr;
Require(CreateThrows<std::runtime_error>(option));
Ngs2CustomSamplerRackOption samplerOption{};
samplerOption.custom_rack_option = DelayRackOption(delay, count).custom_rack_option;
samplerOption.custom_rack_option.rack_option.size = sizeof(samplerOption);
samplerOption.max_channel_works = 1;
Require(CreateSamplerThrows<std::runtime_error>(samplerOption));
for (const auto invalid : {DelayOption{19}, DelayOption{24}, DelayOption{20, 2}, DelayOption{20, 0, 0},
DelayOption{20, 0, 9}, DelayOption{20, 0, 4, 0.0f}, DelayOption{20, 0, 4, 20001.0f},
DelayOption{20, 0, 4, std::numeric_limits<float>::quiet_NaN()},
DelayOption{20, 0, 4, std::numeric_limits<float>::infinity()}, DelayOption{20, 0, 4, 1000.0f, 1}}) {
Require(CreateThrows<std::runtime_error>(DelayRackOption(invalid, count)));
}
Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK);
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
}
static void TestCustomGain() {
const auto system = CreateSystem();
const auto master = Mastering(system, 2);
@@ -664,12 +770,17 @@ static void TestCustomGain() {
}
int main() {
for (const auto count : {1u, 2u, 24u}) {
for (const auto delay : {DelayOption{}, DelayOption{20, 1, 1, 1.0f, 0}, DelayOption{20, 1, 8, 20000.0f, 0}}) {
TestCustomDelay(count, delay);
}
}
TestUserFxChain();
TestRackOptions();
TestCustomRackChecks();
TestCustomSampler();
TestCustomFilter();
TestCustomLegacyUserFx();
for (const auto moduleIndex : {0u, 1u, 23u}) TestCustomLegacyUserFx(moduleIndex);
TestCustomGain();
return 0;
}
@@ -1,12 +1,12 @@
add_library(libSceOpusCeltDec SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp)
target_include_directories(libSceOpusCeltDec PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(libSceOpusCeltDec PRIVATE ffmpeg)
target_link_libraries(libSceOpusCeltDec PRIVATE opus)
configure_windows_unwind(libSceOpusCeltDec)
if(BUILD_TESTING)
add_test_executable(guest_opus_celt_dec_tests ${LIBS_INCLUDE_DIR}/tests/GuestOpusCeltDec.cpp)
target_include_directories(guest_opus_celt_dec_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_opus_celt_dec_tests PRIVATE libSceOpusCeltDec libc)
target_link_libraries(guest_opus_celt_dec_tests PRIVATE libSceOpusCeltDec libc opus)
configure_windows_unwind(guest_opus_celt_dec_tests)
add_test(NAME guest_opus_celt_dec COMMAND guest_opus_celt_dec_tests)
if(WIN32)
+2 -2
View File
@@ -1,12 +1,12 @@
add_library(libSceOpusDec SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp)
target_include_directories(libSceOpusDec PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(libSceOpusDec PRIVATE ffmpeg)
target_link_libraries(libSceOpusDec PRIVATE opus)
configure_windows_unwind(libSceOpusDec)
if(BUILD_TESTING)
add_test_executable(guest_opus_dec_tests ${LIBS_INCLUDE_DIR}/tests/GuestOpusDec.cpp)
target_include_directories(guest_opus_dec_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_opus_dec_tests PRIVATE libSceOpusDec libc)
target_link_libraries(guest_opus_dec_tests PRIVATE libSceOpusDec libc opus)
configure_windows_unwind(guest_opus_dec_tests)
add_test(NAME guest_opus_dec COMMAND guest_opus_dec_tests)
if(WIN32)
@@ -1,12 +1,9 @@
#ifndef CORE_LIBS_PRX_LIBSCEOPUSDEC_INCLUDE_OPUSDECODER_HPP
#define CORE_LIBS_PRX_LIBSCEOPUSDEC_INCLUDE_OPUSDECODER_HPP
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <limits>
#include <memory>
#include <mutex>
#include <stdexcept>
@@ -15,10 +12,7 @@
#include <utility>
#include <vector>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libswresample/swresample.h>
}
#include <opus.h>
namespace {
@@ -65,20 +59,10 @@ public:
decoder->context = *context;
decoder->channels = channels;
decoder->sampleRate = sampleRate;
const auto* codec = avcodec_find_decoder(AV_CODEC_ID_OPUS);
if (!codec) fail("FFmpeg Opus decoder unavailable");
decoder->codec = avcodec_alloc_context3(codec);
if (!decoder->codec) throw std::bad_alloc();
av_channel_layout_default(&decoder->codec->ch_layout, channels);
decoder->codec->sample_rate = 48000;
if (avcodec_open2(decoder->codec, codec, nullptr) < 0) fail("cannot open FFmpeg decoder");
if (sampleRate != 48000) {
AVChannelLayout layout{};
av_channel_layout_default(&layout, channels);
if (swr_alloc_set_opts2(&decoder->resampler, &layout, AV_SAMPLE_FMT_FLT, sampleRate, &layout, AV_SAMPLE_FMT_FLT, 48000, 0, nullptr) < 0 ||
swr_init(decoder->resampler) < 0)
fail("cannot open FFmpeg resampler");
}
int error = OPUS_OK;
decoder->codec.reset(opus_decoder_create(sampleRate, channels, &error));
if (error == OPUS_ALLOC_FAIL) throw std::bad_alloc();
if (!decoder->codec || error != OPUS_OK) fail("cannot create libopus decoder");
decoders.emplace(state, std::move(decoder));
return 0;
}
@@ -92,49 +76,18 @@ public:
std::lock_guard lock(mutex);
const auto found = decoders.find(state);
if (found == decoders.end()) return error(1);
if (bytes == 0 || bytes > std::numeric_limits<int>::max() - AV_INPUT_BUFFER_PADDING_SIZE) fail("packet loss concealment or oversized packet is not implemented");
if (bytes == 0) fail("packet loss concealment is not implemented");
auto& decoder = *found->second;
if (celtOnly && (data[0] >> 3) < 16) fail("SILK and hybrid packets are not implemented");
const int samples = packetSamples(data, bytes) / (48000 / decoder.sampleRate);
const int samples = opus_packet_get_nb_samples(data, bytes, decoder.sampleRate);
if (samples <= 0) fail("invalid packet duration");
const int outputBytes = samples * decoder.channels * 2;
if (capacity / 2 * 2 < outputBytes) fail("PCM buffer is too small");
std::unique_ptr<AVPacket, void (*)(AVPacket*)> packet(av_packet_alloc(), [](AVPacket* p) { av_packet_free(&p); });
std::unique_ptr<AVFrame, void (*)(AVFrame*)> frame(av_frame_alloc(), [](AVFrame* f) { av_frame_free(&f); });
if (!packet || !frame || av_new_packet(packet.get(), bytes) < 0) throw std::bad_alloc();
std::memcpy(packet->data, data, bytes);
if (avcodec_send_packet(decoder.codec, packet.get()) < 0 || avcodec_receive_frame(decoder.codec, frame.get()) < 0)
fail("FFmpeg rejected the packet");
const auto format = static_cast<AVSampleFormat>(frame->format);
if (frame->ch_layout.nb_channels != decoder.channels || frame->sample_rate != 48000 ||
(format != AV_SAMPLE_FMT_FLTP && format != AV_SAMPLE_FMT_FLT))
fail("unexpected FFmpeg PCM format");
std::vector<float> decoded;
decoded.reserve(static_cast<std::size_t>(frame->nb_samples) * decoder.channels);
for (int sample = 0; sample < frame->nb_samples; ++sample) {
for (int channel = 0; channel < decoder.channels; ++channel) {
const auto* source = reinterpret_cast<const float*>(frame->extended_data[format == AV_SAMPLE_FMT_FLTP ? channel : 0]);
decoded.push_back(source[format == AV_SAMPLE_FMT_FLTP ? sample : sample * decoder.channels + channel]);
}
}
if (decoder.resampler) {
const int capacitySamples = swr_get_out_samples(decoder.resampler, frame->nb_samples);
if (capacitySamples < 0) fail("FFmpeg resampler rejected the frame");
std::vector<float> resampled(static_cast<std::size_t>(capacitySamples) * decoder.channels);
auto* output = reinterpret_cast<std::uint8_t*>(resampled.data());
const auto* input = reinterpret_cast<const std::uint8_t*>(decoded.data());
const int converted = swr_convert(decoder.resampler, &output, capacitySamples, &input, frame->nb_samples);
if (converted < 0) fail("FFmpeg resampler rejected the frame");
resampled.resize(static_cast<std::size_t>(converted) * decoder.channels);
decoded = std::move(resampled);
}
decoder.pending.insert(decoder.pending.end(), decoded.begin(), decoded.end());
const auto count = static_cast<std::size_t>(samples) * decoder.channels;
if (decoder.pending.size() < count) decoder.pending.insert(decoder.pending.begin(), count - decoder.pending.size(), 0.0f);
for (std::size_t index = 0; index < count; ++index) {
const auto converted = static_cast<std::int16_t>(std::lrint(std::clamp(decoder.pending[index] * 32768.0f, -32768.0f, 32767.0f)));
std::memcpy(reinterpret_cast<std::uint8_t*>(pcm) + index * 2, &converted, 2);
}
decoder.pending.erase(decoder.pending.begin(), decoder.pending.begin() + static_cast<std::ptrdiff_t>(count));
if (capacity < outputBytes) fail("PCM buffer is too small");
std::vector<std::int16_t> decoded(static_cast<std::size_t>(samples) * decoder.channels);
const int result = opus_decode(decoder.codec.get(), data, bytes, decoded.data(), samples, 0);
if (result < 0) fail("libopus rejected the packet");
if (result != samples) fail("unexpected libopus PCM duration");
std::memcpy(pcm, decoded.data(), static_cast<std::size_t>(outputBytes));
return outputBytes;
}
@@ -150,13 +103,7 @@ private:
std::uint32_t context;
int channels;
int sampleRate;
AVCodecContext* codec = nullptr;
SwrContext* resampler = nullptr;
std::vector<float> pending;
~Decoder() {
swr_free(&resampler);
avcodec_free_context(&codec);
}
std::unique_ptr<OpusDecoder, void (*)(OpusDecoder*)> codec{nullptr, opus_decoder_destroy};
};
const char* name;
@@ -173,23 +120,6 @@ private:
[[noreturn]] void fail(const char* message) const {
throw std::runtime_error(std::string(name) + ": " + message);
}
int packetSamples(const std::uint8_t* packet, int size) const {
const unsigned config = packet[0] >> 3;
const int frameSamples = config < 12 ? ((config & 3) == 3 ? 2880 : 480 << (config & 3))
: config < 16 ? 480 << (config & 1) : 120 << (config & 3);
int frames = 1;
switch (packet[0] & 3) {
case 1:
case 2: frames = 2; break;
case 3:
if (size < 2) fail("truncated packet frame count");
frames = packet[1] & 63;
break;
}
if (frames == 0 || frames * frameSamples > 5760) fail("invalid packet duration");
return frames * frameSamples;
}
};
} // namespace
+16
View File
@@ -74,6 +74,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL
${CMAKE_CURRENT_SOURCE_DIR}/System/src/SignalSets.cpp
${CMAKE_CURRENT_SOURCE_DIR}/System/src/Notification.cpp
${CMAKE_CURRENT_SOURCE_DIR}/System/src/UserContext.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Time/Ktimer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Time/SleepWake.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Time/Time.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Time/TimedWait.cpp
@@ -1920,6 +1921,21 @@ if(BUILD_TESTING)
endif()
if(BUILD_TESTING)
add_test_executable(guest_timed_join_tests ${LIBS_INCLUDE_DIR}/tests/GuestTimedJoin.cpp)
target_include_directories(guest_timed_join_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_timed_join_tests PRIVATE libkernel libc ${CMAKE_DL_LIBS})
configure_windows_unwind(guest_timed_join_tests)
foreach(joinMode IN ITEMS normal coarse)
add_test(NAME guest_timed_join_${joinMode} COMMAND guest_timed_join_tests $<TARGET_FILE:libkernel>)
set_tests_properties(guest_timed_join_${joinMode} PROPERTIES TIMEOUT 15)
if(joinMode STREQUAL coarse)
set_tests_properties(guest_timed_join_${joinMode} PROPERTIES ENVIRONMENT "APS5_COARSE_TIMED_WAITS=1")
endif()
if(WIN32)
set_tests_properties(guest_timed_join_${joinMode} PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
endforeach()
add_test_executable(guest_sleep_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestSleepCancel.cpp)
target_include_directories(guest_sleep_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_sleep_cancel_tests PRIVATE libkernel libc)
@@ -18,6 +18,7 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread
int APS5_VABI scePthreadDetach(Pthread thread);
void APS5_VABI scePthreadExit(void* retval);
int APS5_VABI scePthreadJoin(Pthread thread, void** retval);
int APS5_VABI JoinThread_nid_no_patch(Pthread thread, void** retval, const KernelTimespec* abstime);
int APS5_VABI scePthreadRename(Pthread thread, const char* name);
int APS5_VABI scePthreadGetname(Pthread thread, char* name);
Pthread APS5_VABI scePthreadSelf();
@@ -241,6 +242,12 @@ int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value) {
return PosixThread::ToErrno(scePthreadJoin(thread, value));
}
int APS5_VABI pthread_timedjoin_np_nid_postfix(Pthread thread, void** value, const KernelTimespec* abstime) {
if (abstime == nullptr || abstime->tv_sec < 0 || abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000)
return PosixThread::GUEST_EINVAL;
return PosixThread::ToErrno(JoinThread_nid_no_patch(thread, value, abstime));
}
namespace {
struct RegisterPthreadOnLoad {
RegisterPthreadOnLoad() {
+22 -2
View File
@@ -2,6 +2,7 @@
#include "../include/ThreadLifecycle.hpp"
#include "../include/WindowsThreadLocal.hpp"
#include "../include/Cancel.hpp"
#include "prx/libkernel/Time/include/Time.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libc/include/general/AsmMacros.hpp"
#include "prx/libkernel/KernelErrors.hpp"
@@ -926,7 +927,7 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread
return SCE_OK;
}
int APS5_VABI scePthreadJoin(Pthread thread, void** retval) {
int APS5_VABI JoinThread_nid_no_patch(Pthread thread, void** retval, const KernelTimespec* abstime) {
if (!thread) return SCE_KERNEL_ERROR_EINVAL;
if (thread == CurrentGuestState().current) return SCE_KERNEL_ERROR_EDEADLK;
if (thread->_detached) return SCE_KERNEL_ERROR_EINVAL;
@@ -938,7 +939,22 @@ int APS5_VABI scePthreadJoin(Pthread thread, void** retval) {
if (thread->_joining) return SCE_KERNEL_ERROR_EOPNOTSUPP;
thread->_joining = true;
while (!thread->_finished.load()) {
ThreadCancel::Wait(thread->_join_cv, lock, std::nullopt);
std::optional<std::uint64_t> deadline;
if (abstime != nullptr) {
KernelTimespec now{};
if (clock_gettime_nid_postfix(0, &now) != 0) {
thread->_joining = false;
throw std::runtime_error("Joining guest thread: realtime clock unavailable");
}
if (abstime->tv_sec < now.tv_sec || (abstime->tv_sec == now.tv_sec && abstime->tv_nsec <= now.tv_nsec)) {
thread->_joining = false;
return SCE_KERNEL_ERROR_ETIMEDOUT;
}
const auto seconds = static_cast<std::uint64_t>(abstime->tv_sec) - static_cast<std::uint64_t>(now.tv_sec);
const auto nanos = seconds > 1 ? std::int64_t{1000000000} : static_cast<std::int64_t>(seconds) * 1000000000 + abstime->tv_nsec - now.tv_nsec;
deadline = TimedWait::DeadlineNanos(static_cast<std::uint64_t>((nanos + 999) / 1000));
}
ThreadCancel::Wait(thread->_join_cv, lock, deadline);
if (ThreadCancel::Requested()) {
thread->_joining = false;
lock.unlock();
@@ -964,6 +980,10 @@ int APS5_VABI scePthreadJoin(Pthread thread, void** retval) {
return SCE_OK;
}
int APS5_VABI scePthreadJoin(Pthread thread, void** retval) {
return JoinThread_nid_no_patch(thread, retval, nullptr);
}
int APS5_VABI scePthreadDetach(Pthread thread) {
if (!thread) return SCE_KERNEL_ERROR_EINVAL;
{
+165
View File
@@ -0,0 +1,165 @@
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include <cstdint>
#include <mutex>
extern "C" int* APS5_VABI __error_nid_postfix();
extern "C" int APS5_VABI clock_gettime_nid_postfix(int clockId, void* tp);
namespace {
constexpr int GuestEfault = 14;
constexpr int GuestEinval = 22;
constexpr int GuestEagain = 35;
std::int64_t Fail(int error) {
*__error_nid_postfix() = error;
return -1;
}
constexpr int ClockRealtime = 0;
constexpr int ClockMonotonic = 4;
constexpr int SigevNone = 0;
constexpr int SigevSignal = 1;
constexpr int SigevThreadId = 4;
constexpr int SignalMax = 128;
constexpr int TimerAbstime = 1;
constexpr int FirstTimer = 3;
constexpr int TimerMax = 32;
constexpr long long NanosecondsPerSecond = 1000000000;
constexpr long long TickNanoseconds = 1000000;
struct GuestSigevent {
int notify;
int signo;
};
struct GuestTimespec {
std::int64_t tv_sec;
std::int64_t tv_nsec;
};
struct GuestItimerspec {
GuestTimespec it_interval;
GuestTimespec it_value;
};
using Nanoseconds = __int128;
struct Ktimer {
bool live = false;
int clock = ClockRealtime;
Nanoseconds value = 0;
Nanoseconds interval = 0;
};
std::mutex g_ktimerLock;
Ktimer g_ktimers[TimerMax];
Nanoseconds ToNanoseconds(const GuestTimespec& time) {
return static_cast<Nanoseconds>(time.tv_sec) * NanosecondsPerSecond + time.tv_nsec;
}
GuestTimespec ToTimespec(Nanoseconds time) {
return {static_cast<std::int64_t>(time / NanosecondsPerSecond),
static_cast<std::int64_t>(time % NanosecondsPerSecond)};
}
Nanoseconds ClockNow(int clock) {
GuestTimespec now{};
clock_gettime_nid_postfix(clock, &now);
return ToNanoseconds(now);
}
bool FixTimespec(GuestTimespec& time) {
if (time.tv_sec < 0 || time.tv_nsec < 0 || time.tv_nsec >= NanosecondsPerSecond) return false;
if (time.tv_sec == 0 && time.tv_nsec != 0 && time.tv_nsec < TickNanoseconds) time.tv_nsec = TickNanoseconds;
return true;
}
Ktimer* FindTimer(int id) {
if (id < FirstTimer || id >= TimerMax || !g_ktimers[id].live) return nullptr;
return &g_ktimers[id];
}
void Expire(Ktimer& timer, Nanoseconds now) {
if (timer.value == 0 || now < timer.value) return;
if (timer.interval == 0) {
timer.value = 0;
return;
}
timer.value += ((now - timer.value) / timer.interval + 1) * timer.interval;
}
GuestItimerspec Remaining(Ktimer& timer) {
const Nanoseconds now = ClockNow(timer.clock);
Expire(timer, now);
GuestItimerspec result{ToTimespec(timer.interval), {}};
if (timer.value != 0) result.it_value = timer.value > now ? ToTimespec(timer.value - now) : GuestTimespec{0, 1};
return result;
}
}
extern "C" {
int APS5_VABI ktimer_create_nid_postfix(int clock, const GuestSigevent* event, int* timerid) {
if (clock != ClockRealtime && clock != ClockMonotonic) return static_cast<int>(Fail(GuestEinval));
if (event != nullptr) {
if (event->notify != SigevNone && event->notify != SigevSignal && event->notify != SigevThreadId)
return static_cast<int>(Fail(GuestEinval));
if (event->notify != SigevNone && (event->signo <= 0 || event->signo > SignalMax))
return static_cast<int>(Fail(GuestEinval));
}
std::lock_guard lock(g_ktimerLock);
int id = FirstTimer;
while (id < TimerMax && g_ktimers[id].live) ++id;
if (id == TimerMax) return static_cast<int>(Fail(GuestEagain));
if (timerid == nullptr) return static_cast<int>(Fail(GuestEfault));
if (event == nullptr || event->notify != SigevNone) NotImplemented_nid_no_patch("ktimer_create delivering a signal");
g_ktimers[id] = {true, clock, 0, 0};
*timerid = id;
return 0;
}
int APS5_VABI ktimer_delete_nid_postfix(int id) {
std::lock_guard lock(g_ktimerLock);
Ktimer* timer = FindTimer(id);
if (timer == nullptr) return static_cast<int>(Fail(GuestEinval));
*timer = {};
return 0;
}
int APS5_VABI ktimer_settime_nid_postfix(int id, int flags, const GuestItimerspec* value, GuestItimerspec* oldValue) {
if (value == nullptr) return static_cast<int>(Fail(GuestEfault));
std::lock_guard lock(g_ktimerLock);
Ktimer* timer = FindTimer(id);
if (timer == nullptr) return static_cast<int>(Fail(GuestEinval));
GuestItimerspec next = *value;
if (!FixTimespec(next.it_value)) return static_cast<int>(Fail(GuestEinval));
const bool armed = next.it_value.tv_sec != 0 || next.it_value.tv_nsec != 0;
if (armed && !FixTimespec(next.it_interval)) return static_cast<int>(Fail(GuestEinval));
if (!armed) next.it_interval = {};
if (oldValue != nullptr) *oldValue = Remaining(*timer);
timer->interval = ToNanoseconds(next.it_interval);
timer->value = ToNanoseconds(next.it_value);
if (armed && (flags & TimerAbstime) == 0) timer->value += ClockNow(timer->clock);
return 0;
}
int APS5_VABI ktimer_gettime_nid_postfix(int id, GuestItimerspec* value) {
std::lock_guard lock(g_ktimerLock);
Ktimer* timer = FindTimer(id);
if (timer == nullptr) return static_cast<int>(Fail(GuestEinval));
if (value == nullptr) return static_cast<int>(Fail(GuestEfault));
*value = Remaining(*timer);
return 0;
}
int APS5_VABI ktimer_getoverrun_nid_postfix(int id) {
std::lock_guard lock(g_ktimerLock);
if (FindTimer(id) == nullptr) return static_cast<int>(Fail(GuestEinval));
return 0;
}
}
@@ -297,31 +297,6 @@ int APS5_VABI ksem_wait_nid_postfix(void) {
return 0;
}
int APS5_VABI ktimer_create_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI ktimer_delete_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI ktimer_getoverrun_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI ktimer_gettime_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI ktimer_settime_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI ktrace_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
@@ -522,11 +497,6 @@ int APS5_VABI pthread_switch_delete_np_nid_postfix(void) {
return 0;
}
int APS5_VABI pthread_timedjoin_np_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI ptrace_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
+6 -4
View File
@@ -53,7 +53,8 @@ static bool Acquires(int descriptor, int mode) {
struct Fixture {
static inline unsigned serial = 0;
std::string path = (std::filesystem::temp_directory_path() / ("anyps5-flock-" + std::to_string(::GetCurrentProcessId()) + "-" + std::to_string(serial++) + ".bin")).string();
std::filesystem::path native = std::filesystem::temp_directory_path() / ("anyps5-flock-" + std::to_string(::GetCurrentProcessId()) + "-" + std::to_string(serial++) + ".bin");
std::string path = native.string();
std::set<int> descriptors;
int source = Open(true);
int other = Open(false);
@@ -76,7 +77,7 @@ struct Fixture {
}
~Fixture() {
for (const int descriptor : descriptors) Require(close_nid_postfix(descriptor) == 0);
Require(std::remove(path.c_str()) == 0);
Require(std::filesystem::remove(native));
}
};
@@ -262,7 +263,7 @@ static int ChildTry(const Fixture (&fixtures)[6], int mode) {
wchar_t executable[32768];
Require(::GetModuleFileNameW(nullptr, executable, 32768) != 0);
std::wstring command = L"\"" + std::wstring(executable) + L"\" child " + std::to_wstring(mode | 4);
for (const Fixture& fixture : fixtures) command += L" \"" + std::filesystem::path(fixture.path).wstring() + L"\"";
for (const Fixture& fixture : fixtures) command += L" \"" + fixture.native.filename().wstring() + L"\"";
STARTUPINFOW startup{};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process{};
@@ -317,7 +318,8 @@ int main(int argc, char** argv) {
if (argc >= 4 && std::string(argv[1]) == "child") {
int busy = 0;
for (int index = 3; index < argc; ++index) {
const int descriptor = open_nid_postfix(argv[index], 2, 0);
const std::string path = (std::filesystem::temp_directory_path() / argv[index]).string();
const int descriptor = open_nid_postfix(path.c_str(), 2, 0);
Require(descriptor >= 0);
if (flock_nid_postfix(descriptor, std::atoi(argv[2])) == 0) {
Require(flock_nid_postfix(descriptor, 8) == 0);
+261
View File
@@ -0,0 +1,261 @@
#include "prx/libSceFreeType/include/GuestList.hpp"
#include <array>
#include <cstdio>
#include <cstdlib>
#include <map>
#include <random>
#include <vector>
using namespace FreeType;
extern "C" {
GuestListNodeRec* APS5_VABI FT_List_Find_nid_postfix(GuestListRec*, void*);
void APS5_VABI FT_List_Add_nid_postfix(GuestListRec*, GuestListNodeRec*);
void APS5_VABI FT_List_Insert_nid_postfix(GuestListRec*, GuestListNodeRec*);
void APS5_VABI FT_List_Remove_nid_postfix(GuestListRec*, GuestListNodeRec*);
void APS5_VABI FT_List_Up_nid_postfix(GuestListRec*, GuestListNodeRec*);
int APS5_VABI FT_List_Iterate_nid_postfix(GuestListRec*, GuestListIterator, void*);
void APS5_VABI FT_List_Finalize_nid_postfix(GuestListRec*, GuestListDestructor, GuestMemoryRec*, void*);
}
static void Check(bool value, int line) {
if (!value) {
std::fprintf(stderr, "FreeType list check failed at line %d\n", line);
std::abort();
}
}
#define Require(value) Check((value), __LINE__)
constexpr std::size_t Count = 16;
using Nodes = std::array<FT_ListNodeRec, Count>;
static int Index(FT_ListNode node, const Nodes& nodes) {
if (!node) return -1;
for (std::size_t i = 0; i < nodes.size(); ++i) if (node == &nodes[i]) return static_cast<int>(i);
Require(false);
return -2;
}
struct Lists {
FT_ListRec guest{};
FT_ListRec native{};
Nodes actual{};
Nodes expected{};
std::array<int, 4> values{11, 22, 33, 44};
Lists() {
for (std::size_t i = 0; i < Count; ++i) actual[i].data = expected[i].data = i % 5 == 0 ? nullptr : &values[i % values.size()];
}
void Match() const {
Require(Index(guest.head, actual) == Index(native.head, expected));
Require(Index(guest.tail, actual) == Index(native.tail, expected));
for (std::size_t i = 0; i < Count; ++i) {
Require(Index(actual[i].prev, actual) == Index(expected[i].prev, expected));
Require(Index(actual[i].next, actual) == Index(expected[i].next, expected));
Require(actual[i].data == expected[i].data);
}
}
void Fill() {
for (std::size_t i = 0; i < Count; ++i) {
FT_List_Add_nid_postfix(&guest, &actual[i]);
FT_List_Add(&native, &expected[i]);
}
Match();
}
};
static void Mutations() {
Lists lists;
std::array<bool, Count> active{};
std::array<unsigned, 4> operations{};
std::mt19937 random(0x7157);
for (unsigned step = 0; step < 2048; ++step) {
const auto i = random() % Count;
const bool choice = (random() & 1u) != 0;
if (active[i]) {
if (choice) {
FT_List_Up_nid_postfix(&lists.guest, &lists.actual[i]);
FT_List_Up(&lists.native, &lists.expected[i]);
++operations[3];
} else {
FT_List_Remove_nid_postfix(&lists.guest, &lists.actual[i]);
FT_List_Remove(&lists.native, &lists.expected[i]);
active[i] = false;
++operations[2];
}
} else {
if (choice) {
FT_List_Insert_nid_postfix(&lists.guest, &lists.actual[i]);
FT_List_Insert(&lists.native, &lists.expected[i]);
++operations[1];
} else {
FT_List_Add_nid_postfix(&lists.guest, &lists.actual[i]);
FT_List_Add(&lists.native, &lists.expected[i]);
++operations[0];
}
active[i] = true;
}
lists.Match();
for (auto& value : lists.values) {
Require(Index(FT_List_Find_nid_postfix(&lists.guest, &value), lists.actual) == Index(FT_List_Find(&lists.native, &value), lists.expected));
}
Require(Index(FT_List_Find_nid_postfix(&lists.guest, nullptr), lists.actual) == Index(FT_List_Find(&lists.native, nullptr), lists.expected));
int absent = 123;
Require(FT_List_Find_nid_postfix(&lists.guest, &absent) == nullptr);
}
for (const auto count : operations) Require(count != 0);
for (auto operation : {FT_List_Add_nid_postfix, FT_List_Insert_nid_postfix, FT_List_Remove_nid_postfix, FT_List_Up_nid_postfix}) {
operation(nullptr, &lists.actual[0]);
operation(&lists.guest, nullptr);
operation(nullptr, nullptr);
lists.Match();
}
Require(FT_List_Find_nid_postfix(nullptr, nullptr) == nullptr);
std::printf("list mutations: 2048 states compared with native FreeType\n");
}
struct Walk {
FT_List list;
Nodes* nodes;
bool guest;
bool remove;
std::size_t stop;
int error;
std::vector<int> visits;
};
static int Visit(FT_ListNode node, void* user, bool guest) {
auto& state = *static_cast<Walk*>(user);
Require(state.guest == guest);
state.visits.push_back(Index(node, *state.nodes));
if (state.remove) {
if (guest) FT_List_Remove_nid_postfix(state.list, node);
else FT_List_Remove(state.list, node);
node->prev = nullptr;
node->next = nullptr;
}
return state.visits.size() == state.stop ? state.error : 0;
}
static int NativeIterator(FT_ListNode node, void* user) { return Visit(node, user, false); }
static int APS5_VABI GuestIterator(GuestListNodeRec* node, void* user) { return Visit(node, user, true); }
static void IterationCases() {
for (const bool remove : {false, true}) {
for (const std::size_t stop : {0u, 1u, 3u, 16u}) {
Lists lists;
lists.Fill();
const int error = stop == 3 ? -29 : 73;
Walk guest{&lists.guest, &lists.actual, true, remove, stop, error, {}};
Walk native{&lists.native, &lists.expected, false, remove, stop, error, {}};
const int result = FT_List_Iterate_nid_postfix(&lists.guest, GuestIterator, &guest);
Require(result == FT_List_Iterate(&lists.native, NativeIterator, &native));
Require(result == (stop == 0 ? 0 : error));
Require(guest.visits == native.visits && guest.visits.size() == (stop == 0 ? Count : stop));
lists.Match();
}
}
Lists empty;
Walk guest{&empty.guest, &empty.actual, true, false, 0, 0, {}};
Walk native{&empty.native, &empty.expected, false, false, 0, 0, {}};
Require(FT_List_Iterate_nid_postfix(&empty.guest, GuestIterator, &guest) == 0 && guest.visits.empty());
Require(FT_List_Iterate_nid_postfix(nullptr, GuestIterator, &guest) == FT_List_Iterate(nullptr, NativeIterator, &native));
Require(FT_List_Iterate_nid_postfix(&empty.guest, nullptr, &guest) == FT_List_Iterate(&empty.native, nullptr, &native));
Require(FT_List_Iterate_nid_postfix(nullptr, nullptr, nullptr) == FT_List_Iterate(nullptr, nullptr, nullptr));
std::printf("list iteration: 8 callback/order/error/removal cases compared\n");
}
struct Event {
int kind;
int id;
bool cleared;
bool operator==(const Event&) const = default;
};
struct Disposal {
FT_List list;
FT_Memory native;
GuestMemoryRec* guest;
void* user;
std::map<void*, int> live;
std::vector<Event> events;
};
static void Release(Disposal& state, void* block) {
const auto found = state.live.find(block);
Require(found != state.live.end());
state.events.push_back({2, found->second, state.list->head == nullptr || state.list->tail == nullptr});
state.live.erase(found);
delete static_cast<FT_ListNode>(block);
}
static void NativeFree(FT_Memory memory, void* block) {
auto& state = *static_cast<Disposal*>(memory->user);
Require(state.native == memory);
Release(state, block);
}
static void APS5_VABI GuestFree(GuestMemoryRec* memory, void* block) {
auto& state = *static_cast<Disposal*>(memory->user);
Require(state.guest == memory);
Release(state, block);
}
static void NativeDestroy(FT_Memory memory, void* data, void* user) {
auto& state = *static_cast<Disposal*>(memory->user);
Require(state.native == memory && state.user == user);
state.events.push_back({1, data ? *static_cast<int*>(data) : -1, state.list->head == nullptr || state.list->tail == nullptr});
}
static void APS5_VABI GuestDestroy(GuestMemoryRec* memory, void* data, void* user) {
auto& state = *static_cast<Disposal*>(memory->user);
Require(state.guest == memory && state.user == user);
state.events.push_back({1, data ? *static_cast<int*>(data) : -1, state.list->head == nullptr || state.list->tail == nullptr});
}
static void FinalizationCases() {
for (const bool destroy : {false, true}) {
for (const unsigned count : {0u, 1u, 8u}) {
FT_ListRec guestList{}, nativeList{};
std::array<int, 8> ids{0, 1, 2, 3, 4, 5, 6, 7};
int token = 9;
Disposal guest{&guestList, nullptr, nullptr, &token, {}, {}};
Disposal native{&nativeList, nullptr, nullptr, &token, {}, {}};
GuestMemoryRec guestMemory{&guest, nullptr, GuestFree, nullptr};
FT_MemoryRec_ nativeMemory{&native, nullptr, NativeFree, nullptr};
guest.guest = &guestMemory;
native.native = &nativeMemory;
for (unsigned i = 0; i < count; ++i) {
auto* a = new FT_ListNodeRec{};
auto* b = new FT_ListNodeRec{};
a->data = b->data = i == 0 ? nullptr : &ids[i];
guest.live.emplace(a, i);
native.live.emplace(b, i);
FT_List_Add_nid_postfix(&guestList, a);
FT_List_Add(&nativeList, b);
}
const auto savedHead = guestList.head;
FT_List_Finalize_nid_postfix(&guestList, GuestDestroy, nullptr, &token);
Require(guestList.head == savedHead && guest.events.empty());
FT_List_Finalize_nid_postfix(nullptr, GuestDestroy, &guestMemory, &token);
Require(guest.events.empty());
FT_List_Finalize_nid_postfix(&guestList, destroy ? GuestDestroy : nullptr, &guestMemory, &token);
FT_List_Finalize(&nativeList, destroy ? NativeDestroy : nullptr, &nativeMemory, &token);
Require(guestList.head == nullptr && guestList.tail == nullptr && nativeList.head == nullptr && nativeList.tail == nullptr);
Require(guest.live.empty() && native.live.empty());
Require(guest.events == native.events && guest.events.size() == count * (destroy ? 2u : 1u));
for (const auto& event : guest.events) Require(!event.cleared);
}
}
std::printf("list finalization: 6 allocator/destructor/order cases compared\n");
}
int main() {
Mutations();
IterationCases();
FinalizationCases();
}
@@ -0,0 +1,76 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <cstdint>
#include <cstdlib>
#include <initializer_list>
extern "C" {
int APS5_VABI sceGnmDrawInitDefaultHardwareState(std::uint32_t*, std::uint32_t);
int APS5_VABI sceGnmDrawInitDefaultHardwareState200(std::uint32_t*, std::uint32_t);
int APS5_VABI sceGnmDrawInitDefaultHardwareState350(std::uint32_t*, std::uint32_t);
int APS5_VABI sceGnmDrawInitToDefaultContextState(std::uint32_t*, std::uint32_t);
int APS5_VABI sceGnmDrawInitToDefaultContextState400(std::uint32_t*, std::uint32_t);
}
namespace {
using StateWriter = int (APS5_VABI *)(std::uint32_t*, std::uint32_t);
constexpr std::uint32_t Untouched = 0xa5a5a5a5u;
constexpr std::uint32_t BufferWords = 0x110;
std::uint32_t g_words[BufferWords];
void Require(bool value) { if (!value) std::abort(); }
std::uint32_t* Fresh() {
for (auto& word : g_words) word = Untouched;
return g_words;
}
void RequireAt(std::uint32_t at, std::initializer_list<std::uint32_t> expected) {
for (const auto word : expected) Require(g_words[at++] == word);
}
void RequireUntouchedFrom(std::uint32_t at) {
for (; at < BufferWords; ++at) Require(g_words[at] == Untouched);
}
void CheckHardwareState(StateWriter writer, std::uint32_t stateWords, std::initializer_list<std::uint32_t> tail) {
Require(writer(Fresh(), 0xff) == 0);
RequireUntouchedFrom(0);
Require(writer(Fresh(), 0x100) == 0x100);
RequireAt(0, {0xc0012800u, 0x80000000u, 0x80000000u, 0xc0001200u, 0, 0xc0055800u, 0x2ec47fc0u, 0xffffffffu, 0, 0, 0, 10});
RequireAt(12, {0xc0017600u, 0x216, 0xffffffffu, 0xc0017600u, 0x217, 0xffffffffu});
const std::uint32_t end = 12 + stateWords;
RequireAt(end - static_cast<std::uint32_t>(tail.size()), tail);
const std::uint32_t padding = 0x100 - end - 1;
RequireAt(end, {0xc0001000u | ((padding - 1) << 16), 0});
for (std::uint32_t at = end + 2; at < 0x100; ++at) Require(g_words[at] == Untouched);
RequireUntouchedFrom(0x100);
Require(writer(Fresh(), 0x10f) == 0x100);
RequireUntouchedFrom(0x100);
}
void CheckContextState(StateWriter writer, std::uint32_t packetWords, std::uint32_t stateWords, std::initializer_list<std::uint32_t> tail) {
Require(writer(Fresh(), packetWords - 1) == 0);
RequireUntouchedFrom(0);
Require(writer(Fresh(), packetWords + 1) == 0);
RequireUntouchedFrom(0);
Require(writer(Fresh(), packetWords) == static_cast<int>(packetWords));
RequireAt(0, {0xc0012800u, 0x80000000u, 0x80000000u, 0xc0001200u, 0});
RequireAt(stateWords - static_cast<std::uint32_t>(tail.size()), tail);
RequireUntouchedFrom(stateWords);
}
} // namespace
int main() {
CheckHardwareState(sceGnmDrawInitDefaultHardwareState, 115, {0x100, 4, 0xc0017900u, 0x200, 0xe0000000u});
CheckHardwareState(sceGnmDrawInitDefaultHardwareState200, 131, {0x200, 0xe0000000u, 0xc0017900u, 0x40000258u, 0x6d007fu});
CheckHardwareState(sceGnmDrawInitDefaultHardwareState350, 134, {0x200, 0xe0000000u, 0xc0017900u, 0x40000258u, 0x6d007fu});
CheckContextState(sceGnmDrawInitToDefaultContextState, 0x20, 19, {0xff00ff00u, 0xff00, 0xc00d1000u, 0});
CheckContextState(sceGnmDrawInitToDefaultContextState400, 0x100, 102, {0x40000258u, 0x6d007fu, 0xc09a1000u, 0});
}
+5 -1
View File
@@ -20,7 +20,11 @@ static void Require(bool value, const char* message) {
int main() {
const auto path = std::filesystem::temp_directory_path() / ("anyps5-heap-report-" + std::to_string(std::rand()) + ".txt");
Require(std::freopen(path.string().c_str(), "w", stderr) != nullptr, "stderr redirection failed");
#ifdef _WIN32
Require(_wfreopen(path.wstring().c_str(), L"w", stderr) != nullptr, "stderr redirection failed");
#else
Require(std::freopen(path.c_str(), "w", stderr) != nullptr, "stderr redirection failed");
#endif
char heapName[] = "SceLibcHeap";
sceLibcInternalBacktraceForGame_nid_postfix(heapName);
+204
View File
@@ -0,0 +1,204 @@
#include "SceTypes.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <source_location>
#include <thread>
#include <vector>
namespace {
struct Sigevent {
int notify;
int signo;
std::int64_t value;
std::int64_t extra[6];
};
struct Timespec {
std::int64_t seconds;
std::int64_t nanoseconds;
};
struct Itimerspec {
Timespec interval;
Timespec value;
};
}
extern "C" {
int* APS5_VABI __error_nid_postfix();
int APS5_VABI clock_gettime_nid_postfix(int clock, Timespec* time);
int APS5_VABI ktimer_create_nid_postfix(int clock, const Sigevent* event, int* timerid);
int APS5_VABI ktimer_delete_nid_postfix(int timerid);
int APS5_VABI ktimer_settime_nid_postfix(int timerid, int flags, const Itimerspec* value, Itimerspec* oldValue);
int APS5_VABI ktimer_gettime_nid_postfix(int timerid, Itimerspec* value);
int APS5_VABI ktimer_getoverrun_nid_postfix(int timerid);
}
namespace {
constexpr int Efault = 14;
constexpr int Einval = 22;
constexpr int Eagain = 35;
constexpr int ClockRealtime = 0;
constexpr int ClockMonotonic = 4;
constexpr int ClockUptime = 5;
constexpr int TimerAbstime = 1;
constexpr std::int64_t Millisecond = 1000000;
constexpr std::int64_t Second = 1000000000;
const Sigevent None{0, 0, 0, {}};
void Require(bool condition, std::source_location location = std::source_location::current()) {
if (condition) return;
std::fprintf(stderr, "check failed at line %u\n", static_cast<unsigned>(location.line()));
std::exit(1);
}
int Error() {
return *__error_nid_postfix();
}
std::int64_t Nanoseconds(const Timespec& time) {
return time.seconds * Second + time.nanoseconds;
}
int Create(int clock = ClockMonotonic) {
int id = -1;
Require(ktimer_create_nid_postfix(clock, &None, &id) == 0);
return id;
}
Itimerspec Get(int id) {
Itimerspec value{{-1, -1}, {-1, -1}};
Require(ktimer_gettime_nid_postfix(id, &value) == 0);
return value;
}
void Sleep(std::int64_t nanoseconds) {
std::this_thread::sleep_for(std::chrono::nanoseconds(nanoseconds));
}
void Creation() {
int id = -1;
const Sigevent badNotify{2, 0, 0, {}};
const Sigevent noSignal{1, 0, 0, {}};
const Sigevent badSignal{4, 129, 0, {}};
Require(ktimer_create_nid_postfix(ClockUptime, &None, &id) == -1 && Error() == Einval && id == -1);
Require(ktimer_create_nid_postfix(-1, &None, &id) == -1 && Error() == Einval);
Require(ktimer_create_nid_postfix(ClockMonotonic, &badNotify, &id) == -1 && Error() == Einval);
Require(ktimer_create_nid_postfix(ClockMonotonic, &noSignal, &id) == -1 && Error() == Einval);
Require(ktimer_create_nid_postfix(ClockMonotonic, &badSignal, &id) == -1 && Error() == Einval);
Require(ktimer_create_nid_postfix(ClockMonotonic, &None, nullptr) == -1 && Error() == Efault);
std::vector<int> ids;
for (int expected = 3; expected < 32; ++expected) {
ids.push_back(Create(expected % 2 == 0 ? ClockRealtime : ClockMonotonic));
Require(ids.back() == expected);
}
Require(ktimer_create_nid_postfix(ClockMonotonic, &None, &id) == -1 && Error() == Eagain);
Require(ktimer_delete_nid_postfix(7) == 0);
Require(Create() == 7);
for (const int each : ids) Require(ktimer_delete_nid_postfix(each) == 0);
}
void InvalidIds() {
Itimerspec value{};
const int id = Create();
Require(ktimer_delete_nid_postfix(id) == 0);
for (const int bad : {-1, 0, 2, id, 32}) {
Require(ktimer_delete_nid_postfix(bad) == -1 && Error() == Einval);
Require(ktimer_settime_nid_postfix(bad, 0, &value, nullptr) == -1 && Error() == Einval);
Require(ktimer_gettime_nid_postfix(bad, &value) == -1 && Error() == Einval);
Require(ktimer_getoverrun_nid_postfix(bad) == -1 && Error() == Einval);
}
Require(ktimer_settime_nid_postfix(id, 0, nullptr, nullptr) == -1 && Error() == Efault);
}
void Values() {
const int id = Create();
Itimerspec disarmed = Get(id);
Require(Nanoseconds(disarmed.value) == 0 && Nanoseconds(disarmed.interval) == 0);
for (const Itimerspec bad : {Itimerspec{{0, 0}, {-1, 0}}, Itimerspec{{0, 0}, {0, -1}}, Itimerspec{{0, 0}, {0, Second}},
Itimerspec{{-1, 0}, {1, 0}}, Itimerspec{{0, Second}, {1, 0}}})
Require(ktimer_settime_nid_postfix(id, 0, &bad, nullptr) == -1 && Error() == Einval);
const Itimerspec clear{{-1, 0}, {0, 0}};
Require(ktimer_settime_nid_postfix(id, 0, &clear, nullptr) == 0);
disarmed = Get(id);
Require(Nanoseconds(disarmed.value) == 0 && Nanoseconds(disarmed.interval) == 0);
const Itimerspec small{{0, 1}, {10, 0}};
Require(ktimer_settime_nid_postfix(id, 0, &small, nullptr) == 0);
Itimerspec old{};
const Itimerspec later{{0, 0}, {20, 0}};
Require(ktimer_settime_nid_postfix(id, 0, &later, &old) == 0);
Require(Nanoseconds(old.interval) == Millisecond);
Require(Nanoseconds(old.value) > 9 * Second && Nanoseconds(old.value) <= 10 * Second);
const Itimerspec tiny{{0, 0}, {0, 1}};
Require(ktimer_settime_nid_postfix(id, 0, &tiny, &old) == 0);
Require(Nanoseconds(old.value) > 19 * Second && Nanoseconds(old.value) <= 20 * Second);
const Itimerspec off{};
Require(ktimer_settime_nid_postfix(id, 0, &off, &old) == 0);
Require(Nanoseconds(old.interval) == 0 && Nanoseconds(old.value) <= Millisecond);
Require(ktimer_delete_nid_postfix(id) == 0);
}
void Expiry() {
const int id = Create();
const Itimerspec once{{0, 0}, {0, 30 * Millisecond}};
Require(ktimer_settime_nid_postfix(id, 0, &once, nullptr) == 0);
const auto running = Get(id);
Require(Nanoseconds(running.value) > 0 && Nanoseconds(running.value) <= 30 * Millisecond);
Sleep(60 * Millisecond);
const auto expired = Get(id);
Require(Nanoseconds(expired.value) == 0 && Nanoseconds(expired.interval) == 0);
Require(ktimer_getoverrun_nid_postfix(id) == 0);
const Itimerspec periodic{{0, 10 * Millisecond}, {0, 10 * Millisecond}};
Require(ktimer_settime_nid_postfix(id, 0, &periodic, nullptr) == 0);
Sleep(55 * Millisecond);
const auto period = Get(id);
Require(Nanoseconds(period.interval) == 10 * Millisecond);
Require(Nanoseconds(period.value) > 0 && Nanoseconds(period.value) <= 10 * Millisecond);
Require(ktimer_getoverrun_nid_postfix(id) == 0);
Require(ktimer_delete_nid_postfix(id) == 0);
}
void Absolute() {
for (const int clock : {ClockRealtime, ClockMonotonic}) {
const int id = Create(clock);
Timespec now{};
Require(clock_gettime_nid_postfix(clock, &now) == 0);
const Itimerspec future{{0, 0}, {now.seconds + 5, now.nanoseconds}};
Require(ktimer_settime_nid_postfix(id, TimerAbstime, &future, nullptr) == 0);
const auto remaining = Get(id);
Require(Nanoseconds(remaining.value) > 4 * Second && Nanoseconds(remaining.value) <= 5 * Second);
const Itimerspec past{{0, 0}, {now.seconds - 1, now.nanoseconds}};
Require(ktimer_settime_nid_postfix(id, TimerAbstime, &past, nullptr) == 0);
Require(Nanoseconds(Get(id).value) == 0);
const Itimerspec pastPeriodic{{1, 0}, {now.seconds - 10, now.nanoseconds}};
Require(ktimer_settime_nid_postfix(id, TimerAbstime, &pastPeriodic, nullptr) == 0);
const auto moved = Get(id);
Require(Nanoseconds(moved.value) > 0 && Nanoseconds(moved.value) <= Second);
Require(Nanoseconds(moved.interval) == Second);
Require(ktimer_delete_nid_postfix(id) == 0);
}
}
}
int main() {
Creation();
InvalidIds();
Values();
Expiry();
Absolute();
std::puts("guest_kernel_sys_ktimer passed");
return 0;
}
+25 -11
View File
@@ -3,7 +3,9 @@
#include <array>
#include <cstdint>
#include <cstdlib>
#include <cstdio>
#include <stdexcept>
#include <opus.h>
extern "C" {
int APS5_VABI sceOpusCeltDecInitialize(std::uint32_t*);
@@ -128,18 +130,30 @@ int main() {
for (int rate : {0, 22050, 44100, 96000})
Require(sceOpusCeltDecCreateEx(&context, unsupported.data(), rate, 2) == static_cast<int>(errorBase + 2));
for (int rate : {8000, 12000, 16000, 24000, 48000}) {
std::array<std::uint8_t, 640> toneState{};
Require(sceOpusCeltDecCreateEx(&context, toneState.data(), rate, 2) == 0);
const int samples = 960 * rate / 48000;
std::array<std::int16_t, 1922> tone{};
for (int frame = 0; frame < 4; ++frame) {
tone.fill(12345);
RequireThrows([&] { sceOpusCeltDecDecode(toneState.data(), CELT_TONE[frame], 60, tone.data() + 1, samples * 4 - 1); });
Require(sceOpusCeltDecDecode(toneState.data(), CELT_TONE[frame], 60, tone.data() + 1, samples * 4) == samples * 4);
Require(tone[0] == 12345 && tone[samples * 2 + 1] == 12345);
if (frame == 1 || frame == 2) RequireTone(tone.data() + 1, samples, 2, rate);
for (int channels : {1, 2}) {
std::array<std::uint8_t, 640> toneState{};
Require(sceOpusCeltDecCreateEx(&context, toneState.data(), rate, channels) == 0);
int error = OPUS_OK;
auto* reference = opus_decoder_create(rate, channels, &error);
Require(reference && error == OPUS_OK);
const int samples = 960 * rate / 48000;
std::array<std::int16_t, 1922> tone{};
for (int frame = 0; frame < 4; ++frame) {
tone.fill(12345);
RequireThrows([&] { sceOpusCeltDecDecode(toneState.data(), CELT_TONE[frame], 60, tone.data() + 1, samples * channels * 2 - 1); });
Require(sceOpusCeltDecDecode(toneState.data(), CELT_TONE[frame], 60, tone.data() + 1, samples * channels * 2) == samples * channels * 2);
Require(tone[0] == 12345 && tone[samples * channels + 1] == 12345);
std::array<std::int16_t, 1920> expected{};
Require(opus_decode(reference, CELT_TONE[frame], 60, expected.data(), samples, 0) == samples);
if (!std::equal(expected.begin(), expected.begin() + samples * channels, tone.begin() + 1)) {
std::fprintf(stderr, "CELT PCM differs from libopus at %d Hz, %d channels, frame %d\n", rate, channels, frame);
std::abort();
}
if (frame == 1 || frame == 2) RequireTone(tone.data() + 1, samples, channels, rate);
}
opus_decoder_destroy(reference);
Require(sceOpusCeltDecDestroy(toneState.data()) == 0);
}
Require(sceOpusCeltDecDestroy(toneState.data()) == 0);
}
Require(sceOpusCeltDecDestroy(state.data()) == 0);
+47
View File
@@ -4,7 +4,10 @@
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cstdio>
#include <span>
#include <stdexcept>
#include <opus.h>
extern "C" {
int APS5_VABI sceOpusDecInitialize(std::uint32_t*);
@@ -46,12 +49,56 @@ const std::uint8_t OPUS_NARROWBAND[] = {
10, 0, 0x08, 0x85, 0x89, 0x9D, 0x73, 0x0F, 0xC2, 0xB1, 0xE2, 0x48,
};
void CheckReference(int rate, int channels) {
std::uint32_t context = 0;
std::array<std::uint8_t, 640> state{};
Require(sceOpusDecInitialize(&context) == 0);
Require(sceOpusDecCreateEx(&context, state.data(), rate, channels) == 0);
int error = OPUS_OK;
auto* reference = opus_decoder_create(rate, channels, &error);
Require(reference && error == OPUS_OK);
std::array<std::uint8_t, 1500> longPacket{};
auto* repacketizer = opus_repacketizer_create();
Require(repacketizer != nullptr);
for (int i = 0; i < 6; ++i) Require(opus_repacketizer_cat(repacketizer, OPUS_STEREO + 2, OPUS_STEREO[0]) == OPUS_OK);
const int longSize = opus_repacketizer_out(repacketizer, longPacket.data() + 2, static_cast<opus_int32>(longPacket.size() - 2));
Require(longSize > 0);
opus_repacketizer_destroy(repacketizer);
longPacket[0] = static_cast<std::uint8_t>(longSize);
longPacket[1] = static_cast<std::uint8_t>(longSize >> 8);
for (auto packets : {std::span<const std::uint8_t>(OPUS_NARROWBAND), std::span<const std::uint8_t>(OPUS_STEREO),
std::span<const std::uint8_t>(OPUS_NARROWBAND), std::span<const std::uint8_t>(longPacket.data(), longSize + 2)}) {
for (std::size_t offset = 0; offset < packets.size();) {
const int bytes = packets[offset] | (packets[offset + 1] << 8);
const auto* packet = packets.data() + offset + 2;
std::array<std::int16_t, 11520> expected{};
const int samples = opus_decode(reference, packet, bytes, expected.data(), 5760, 0);
Require(samples > 0);
const int count = samples * channels;
std::array<std::int16_t, 11522> actual{};
actual.fill(12345);
Require(sceOpusDecDecode(state.data(), packet, bytes, actual.data() + 1, count * 2 + 1) == count * 2);
if (!std::equal(expected.begin(), expected.begin() + count, actual.begin() + 1)) {
std::fprintf(stderr, "Opus PCM differs from libopus at %d Hz, %d channels, packet offset %zu\n", rate, channels, offset);
std::abort();
}
Require(actual.front() == 12345 && std::all_of(actual.begin() + count + 1, actual.end(), [](auto value) { return value == 12345; }));
offset += bytes + 2;
}
}
opus_decoder_destroy(reference);
Require(sceOpusDecDestroy(state.data()) == 0);
Require(sceOpusDecTerminate(&context) == 0);
}
} // namespace
int main() {
constexpr std::uint32_t errorBase = 0x81740000u;
Require(sceOpusDecInitialize(nullptr) == static_cast<int>(errorBase + 13));
Require(sceOpusDecDestroy(nullptr) == static_cast<int>(errorBase));
for (int rate : {8000, 12000, 16000, 24000, 48000})
for (int channels : {1, 2}) CheckReference(rate, channels);
std::uint32_t context = 0, otherContext = 0;
Require(sceOpusDecInitialize(&context) == 0);
std::array<std::uint8_t, 640> invalidState{};
+20 -13
View File
@@ -70,14 +70,24 @@ void WINAPI UnexpectedDetach(PVOID) { TerminateProcess(GetCurrentProcess(), 4);
#endif
constexpr char RelaunchMarker[] = "ANYPS5_TEST_RELAUNCH_MARKER";
constexpr char MainExecutable[] = "ANYPS5_TEST_MAIN_EXECUTABLE";
void SetVariable(const char* name, const char* value) {
void SetVariable(const char* name, const std::filesystem::path& value) {
#ifdef _WIN32
Require(SetEnvironmentVariableA(name, value) != 0);
Require(_putenv_s(name, value) == 0);
const std::wstring wideName = std::filesystem::path(name).wstring();
Require(SetEnvironmentVariableW(wideName.c_str(), value.c_str()) != 0);
Require(_wputenv_s(wideName.c_str(), value.c_str()) == 0);
#else
Require(setenv(name, value, 1) == 0);
Require(setenv(name, value.c_str(), 1) == 0);
#endif
}
std::filesystem::path PathVariable(const char* name) {
#ifdef _WIN32
const wchar_t* value = _wgetenv(std::filesystem::path(name).wstring().c_str());
#else
const char* value = std::getenv(name);
#endif
Require(value != nullptr);
return value;
}
std::filesystem::path OwnExecutable() {
#ifdef _WIN32
std::wstring executable(32768, L'\0');
@@ -94,9 +104,7 @@ std::filesystem::path OwnExecutable() {
#endif
}
void AppendMarker(const std::string& line) {
const char* marker = std::getenv(RelaunchMarker);
Require(marker != nullptr);
std::ofstream(marker, std::ios::app) << line << '\n';
std::ofstream(PathVariable(RelaunchMarker), std::ios::app) << line << '\n';
}
void MainShutdown() {
AppendMarker("shutdown");
@@ -149,7 +157,7 @@ int RunChild(const std::filesystem::path& self, const char* mode) {
int DriveNestedExit() {
const auto self = OwnExecutable();
const auto marker = std::filesystem::temp_directory_path() / ("anyps5-nested-exit-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt");
SetVariable(RelaunchMarker, marker.string().c_str());
SetVariable(RelaunchMarker, marker);
const int code = RunChild(self, "--nested-exit");
std::string text;
{
@@ -166,7 +174,7 @@ int DriveNestedExit() {
int DriveExitOrder() {
const auto self = OwnExecutable();
const auto marker = std::filesystem::temp_directory_path() / ("anyps5-exit-order-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt");
SetVariable(RelaunchMarker, marker.string().c_str());
SetVariable(RelaunchMarker, marker);
const int code = RunChild(self, "--exit-order");
std::string text;
{
@@ -185,8 +193,8 @@ int DriveRelaunch() {
const auto secondary = self.parent_path() / ("picker.picker.bin" + self.extension().string());
std::filesystem::copy_file(self, secondary, std::filesystem::copy_options::overwrite_existing);
const auto marker = std::filesystem::temp_directory_path() / ("anyps5-relaunch-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt");
SetVariable(RelaunchMarker, marker.string().c_str());
SetVariable(MainExecutable, self.string().c_str());
SetVariable(RelaunchMarker, marker);
SetVariable(MainExecutable, self);
Require(RunChild(self, "--relaunch") == 0);
std::string text;
for (int attempt = 0; attempt < 200 && text.find("relaunched") == std::string::npos; ++attempt) {
@@ -300,8 +308,7 @@ int main(int argc, char** argv) {
return 2;
}
if (argc > firstArgument && std::strcmp(argv[firstArgument], "--relaunched") == 0) {
const char* main = std::getenv(MainExecutable);
Require(main != nullptr && std::filesystem::equivalent(OwnExecutable(), main));
Require(std::filesystem::equivalent(OwnExecutable(), PathVariable(MainExecutable)));
AppendMarker("relaunched");
return 0;
}
+183
View File
@@ -0,0 +1,183 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <limits>
#include <thread>
#ifdef _WIN32
#include <windows.h>
#else
#include <dlfcn.h>
#endif
struct Timespec { std::int64_t seconds, nanoseconds; };
static_assert(sizeof(Timespec)==16 && alignof(Timespec)==8 && offsetof(Timespec,nanoseconds)==8);
extern "C" {
int APS5_VABI pthread_create_nid_postfix(void**,const void*,void* (APS5_VABI*)(void*),void*);
int APS5_VABI pthread_join_nid_postfix(void*,void**);
int APS5_VABI pthread_detach_nid_postfix(void*);
int APS5_VABI pthread_cancel_nid_postfix(void*);
void* APS5_VABI pthread_self_nid_postfix();
int APS5_VABI clock_gettime_nid_postfix(int,Timespec*);
int* APS5_VABI __error_nid_postfix();
}
using TimedJoin=int (APS5_VABI*)(void*,void**,const Timespec*);
static TimedJoin timedJoin;
void Require(bool value,const char* message) {
if (!value) { std::fprintf(stderr,"timed join: %s\n",message); std::abort(); }
}
Timespec After(int milliseconds) {
Timespec result{};
Require(clock_gettime_nid_postfix(0,&result)==0,"read realtime");
result.nanoseconds+=static_cast<std::int64_t>(milliseconds)*1000000;
result.seconds+=result.nanoseconds/1000000000;
result.nanoseconds%=1000000000;
return result;
}
struct Gate { std::atomic<bool> release{false}, entered{false}, leaving{false}; };
void* APS5_VABI Worker(void* argument) {
auto& gate=*static_cast<Gate*>(argument);
gate.entered.store(true,std::memory_order_release);
while (!gate.release.load(std::memory_order_acquire)) std::this_thread::yield();
gate.leaving.store(true,std::memory_order_release);
return reinterpret_cast<void*>(std::uintptr_t{0x12345678});
}
void Wait(const std::atomic<bool>& flag) {
const auto deadline=std::chrono::steady_clock::now()+std::chrono::seconds(3);
while (!flag.load(std::memory_order_acquire)) {
Require(std::chrono::steady_clock::now()<deadline,"worker handshake timed out");
std::this_thread::yield();
}
}
void* Start(Gate& gate) {
void* thread=nullptr;
Require(pthread_create_nid_postfix(&thread,nullptr,Worker,&gate)==0,"create worker");
Wait(gate.entered);
return thread;
}
struct JoinTask { void* target; std::atomic<bool> entered{false}; int result=-1; void* value=reinterpret_cast<void*>(std::uintptr_t{0x55}); };
void* APS5_VABI Joiner(void* argument) {
auto& task=*static_cast<JoinTask*>(argument);
const auto deadline=After(3000);
task.entered.store(true,std::memory_order_release);
task.result=timedJoin(task.target,&task.value,&deadline);
return nullptr;
}
void AwaitClaim(void* target) {
const Timespec past{0,0};
const auto until=std::chrono::steady_clock::now()+std::chrono::seconds(2);
for (;;) {
const int result=timedJoin(target,nullptr,&past);
if (result==45) return;
Require(result==60,"claim probe returns timeout or ENOTSUP");
Require(std::chrono::steady_clock::now()<until,"joiner did not claim target");
std::this_thread::yield();
}
}
void Basic() {
const Timespec valid=After(1000);
for (const Timespec invalid: {Timespec{-1,0},Timespec{0,-1},Timespec{0,1000000000}}) {
Require(timedJoin(nullptr,nullptr,&invalid)==22,"invalid deadline precedes null thread");
Require(timedJoin(pthread_self_nid_postfix(),nullptr,&invalid)==22,"invalid deadline precedes self join");
}
Require(timedJoin(nullptr,nullptr,nullptr)==22,"null deadline");
Require(timedJoin(nullptr,nullptr,&valid)==22,"null thread");
Require(timedJoin(pthread_self_nid_postfix(),nullptr,&valid)==11,"self join");
Gate gate;
void* thread=Start(gate);
struct { std::uint64_t before=0xa5a5a5a5; void* value=reinterpret_cast<void*>(std::uintptr_t{0x55}); std::uint64_t after=0x5a5a5a5a; } result;
const Timespec past{0,0};
*__error_nid_postfix()=123;
Require(timedJoin(thread,&result.value,&past)==60,"past deadline expires");
Require(result.value==reinterpret_cast<void*>(std::uintptr_t{0x55}),"timeout preserves return slot");
Require(*__error_nid_postfix()==123,"POSIX error preserves errno");
const auto deadline=After(50);
const auto originalDeadline=deadline;
const auto started=std::chrono::steady_clock::now();
Require(timedJoin(thread,&result.value,&deadline)==60,"future deadline expires");
Require(std::chrono::steady_clock::now()-started>=std::chrono::milliseconds(25),"wait did not expire immediately");
Require(deadline.seconds==originalDeadline.seconds && deadline.nanoseconds==originalDeadline.nanoseconds,"deadline input is unchanged");
gate.release.store(true,std::memory_order_release);
const Timespec distant{std::numeric_limits<std::int64_t>::max(),999999999};
Require(timedJoin(thread,&result.value,&distant)==0,"timeout leaves target joinable; distant deadline does not overflow");
Require(result.value==reinterpret_cast<void*>(std::uintptr_t{0x12345678}),"joined return value");
Require(result.before==0xa5a5a5a5 && result.after==0x5a5a5a5a,"return-slot bounds");
Require(timedJoin(thread,nullptr,&valid)==3,"already joined target");
Require(timedJoin(thread,nullptr,nullptr)==22,"deadline validation precedes joined state");
Gate done;
void* completed=Start(done);
done.release.store(true,std::memory_order_release);
Wait(done.leaving);
const auto until=std::chrono::steady_clock::now()+std::chrono::seconds(2);
for (;;) {
const int joined=timedJoin(completed,nullptr,&past);
if (joined==0) break;
Require(joined==60 && std::chrono::steady_clock::now()<until,"completed thread joins with an expired deadline");
std::this_thread::yield();
}
Gate detached;
void* detachedThread=Start(detached);
Require(pthread_detach_nid_postfix(detachedThread)==0,"detach worker");
Require(timedJoin(detachedThread,nullptr,&valid)==22,"detached target");
detached.release.store(true,std::memory_order_release);
Wait(detached.leaving);
}
void Contention(bool cancel) {
Gate gate;
void* target=Start(gate);
JoinTask task{target};
void* joiner=nullptr;
Require(pthread_create_nid_postfix(&joiner,nullptr,Joiner,&task)==0,"create competing joiner");
Wait(task.entered);
AwaitClaim(target);
if (cancel) {
Require(pthread_cancel_nid_postfix(joiner)==0,"cancel blocked timed join");
void* canceled=nullptr;
Require(pthread_join_nid_postfix(joiner,&canceled)==0 && canceled==reinterpret_cast<void*>(std::uintptr_t{1}),"joiner is canceled");
Require(task.result==-1 && task.value==reinterpret_cast<void*>(std::uintptr_t{0x55}),"canceled join did not return or write result");
const Timespec past{0,0};
Require(timedJoin(target,nullptr,&past)==60,"cancellation releases join claim");
gate.release.store(true,std::memory_order_release);
const auto deadline=After(1000);
Require(timedJoin(target,nullptr,&deadline)==0,"cancellation preserves target joinability");
} else {
gate.release.store(true,std::memory_order_release);
Require(pthread_join_nid_postfix(joiner,nullptr)==0,"join successful joiner");
Require(task.result==0 && task.value==reinterpret_cast<void*>(std::uintptr_t{0x12345678}),"competing join succeeds once");
}
}
int main(int argc,char** argv) {
Require(argc==2,"expected kernel path");
#ifdef _WIN32
const auto library=LoadLibraryA(argv[1]);
Require(library!=nullptr,"load kernel");
timedJoin=reinterpret_cast<TimedJoin>(GetProcAddress(library,"pthread_timedjoin_np_nid_postfix"));
#else
const auto library=dlopen(argv[1],RTLD_NOW);
Require(library!=nullptr,"load kernel");
timedJoin=reinterpret_cast<TimedJoin>(dlsym(library,"pthread_timedjoin_np_nid_postfix"));
#endif
Require(timedJoin!=nullptr,"pthread_timedjoin_np export is missing");
Basic();
Contention(false);
Contention(true);
#ifdef _WIN32
Require(FreeLibrary(library)!=0,"release kernel lookup handle");
#else
Require(dlclose(library)==0,"release kernel lookup handle");
#endif
std::puts("timed join checks passed");
}
@@ -58,6 +58,7 @@ def alu_check_body(opcode, register, base, value, result, flags_masked):
emit(bytes.fromhex("0f 85 00 00 00 00"))
jumps.append(len(code) - 4)
emit(b"\x53")
emit(bytes.fromhex("64 c7 04 25 28 00 00 00") + struct.pack("<I", value & 0xffffffff))
for target in (0, 1, 3):
move_immediate(target, base)
@@ -74,8 +75,8 @@ def alu_check_body(opcode, register, base, value, result, flags_masked):
fail_unless_equal()
emit(bytes.fromhex("83 fa") + bytes([flags_masked]))
fail_unless_equal()
emit(bytes.fromhex("b8 2a 00 00 00 c3 b8 01 00 00 00 c3"))
failure = len(code) - 6
emit(bytes.fromhex("b8 2a 00 00 00 5b c3 b8 01 00 00 00 5b c3"))
failure = len(code) - 7
for position in jumps:
struct.pack_into("<i", code, position, failure - position - 4)
return bytes(code)
@@ -702,7 +703,7 @@ def main():
assert expected in stub, (case, expected.hex(), stub.hex())
if args.group in ("all", "alu_execution"):
for name, image in alu_execution_cases():
convert(name, image)
convert(name, image, tls_address=0x1241)
if args.group in ("all", "register_address_loads"):
for name, image, address, instruction, register, wide, moved in register_load_cases():
convert(name, image, tls_address=address)
@@ -719,14 +720,14 @@ def main():
head, tail = register_alu_stub(instruction, opcode, register, wide)
assert stub.startswith(head) and tail + moved in stub, (name, head.hex(), (tail + moved).hex(), stub.hex())
if args.group in ("all", "moved_branch"):
prologue = bytes.fromhex("48 c7 c3 f0 ff ff ff 48 31 c0")
rel8_body = prologue + bytes.fromhex("64 48 33 03 75 06 31 c0 c3 90 90 90 b8 2a 00 00 00 c3")
rel32_body = prologue + bytes.fromhex("64 48 33 03 0f 85") + struct.pack("<i", 0x1259 - 0x1254) + bytes.fromhex("31 c0 c3 90 90 b8 2a 00 00 00 c3")
jmp_body = prologue + bytes.fromhex("64 48 33 03 eb 06 b8 01 00 00 00 c3 90 b8 2a 00 00 00 c3")
prologue = bytes.fromhex("53 48 c7 c3 f0 ff ff ff 48 31 c0")
rel8_body = prologue + bytes.fromhex("64 48 33 03 75 07 31 c0 5b c3 90 90 90 b8 2a 00 00 00 5b c3")
rel32_body = prologue + bytes.fromhex("64 48 33 03 0f 85") + struct.pack("<i", 0x125B - 0x1255) + bytes.fromhex("31 c0 5b c3 90 90 b8 2a 00 00 00 5b c3")
jmp_body = prologue + bytes.fromhex("64 48 33 03 eb 07 b8 01 00 00 00 5b c3 90 b8 2a 00 00 00 5b c3")
for name, body, access, target, branch_opcode in (
("moved-rel8-branch", rel8_body, 0x124A, 0x1256, "0f 85"),
("moved-rel32-branch", rel32_body, 0x124A, 0x1259, "0f 85"),
("moved-rel8-jump", jmp_body, 0x124A, 0x1256, "e9"),
("moved-rel8-branch", rel8_body, 0x124B, 0x1258, "0f 85"),
("moved-rel32-branch", rel32_body, 0x124B, 0x125B, "0f 85"),
("moved-rel8-jump", jmp_body, 0x124B, 0x1258, "e9"),
):
image = make_image("register", "unwind", body=body)
convert(name, image, tls_address=access)
+2
View File
@@ -26,6 +26,8 @@
- [libSceFreeType](../../../core/libs/prx/libSceFreeType/src/GuestObjects.cpp) (libSceFreeType) - private fields (`driver`, `memory`, `stream`, `sizes_list`, `extensions`) and `subglyphs` stay null. Face, size and glyph-slot records are copies. Only edits to outline points and flags are written back before `FT_Render_Glyph`; replacing arrays or exceeding the host `FT_Pos` range throws. `FT_New_Face` reads the whole file into memory and throws for files of 2 GiB or more on Windows. On macOS, native FreeType symbols remain exported and could resolve guest imports without adaptation; Windows and Linux hide them with `--exclude-libs`.
- [FT_List_Add, FT_List_Insert, FT_List_Find, FT_List_Remove, FT_List_Up, FT_List_Iterate and FT_List_Finalize](../../../core/libs/prx/libSceFreeType/src/List.cpp) - console signatures, callback ordering and error compatibility are unverified. Names and NIDs follow the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the pointer-only 16-byte list, 24-byte node and operations follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftutil.c). The 32-byte allocator record uses LP64 guest callbacks, and finalization calls its guest `free` callback for each node. Native list-membership and callback preconditions still apply; malformed lists and callbacks that invalidate a later node are not validated.
- [FT_New_Memory_Face](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - on Windows, the host FT_Long is 32 bits, so buffer sizes of 2 GiB or more or below -2 GiB throw before the font buffer is read
- [FT_Outline_Get_Orientation](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI/version and large-outline equivalence are unverified. The adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) with the existing unsigned guest counts and an LP64 area accumulator, including on Windows.
+2 -1
View File
@@ -2,8 +2,9 @@
### Unknown function info
- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 70 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`.
- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 65 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`.
### Functional
- [retail-only answers](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - the debug resets, the resource and owner registration calls, the SDMA calls and the SQ counter calls return `0x8EEE00FF`, the five `sceGnmDriverInternalRetrieveGnmInterface*` calls return `0x80000000`, `sceGnmGetDbgGcHandle` returns -1, `sceGnmDriverTraceInProgress` and `sceGnmValidateOnSubmitEnabled` return 0 and the other validation calls return `0x80D13FFF`. These are the values [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver.cpp) returns for calls it marks as not available in retail firmware; they are not measured on a console, and the arguments are not read
- [default state packets](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - `sceGnmDrawInitDefaultHardwareState`, `sceGnmDrawInitDefaultHardwareState200`, `sceGnmDrawInitDefaultHardwareState350`, `sceGnmDrawInitToDefaultContextState` and `sceGnmDrawInitToDefaultContextState400` write the sequences in [InitSequences.hpp](../../../core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp), taken from [shadPS4's gnmdriver_init.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver_init.h): its Neo sequences, except the plain hardware state, which shadPS4 also writes unchanged in Neo mode. The size checks and returned sizes are shadPS4's. Not compared with console output, and nothing in AnyPS5 consumes a GNM command buffer yet
+4 -2
View File
@@ -21,8 +21,10 @@
- [NGS2 matrix levels](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - levels are clamped to -4..4 and NaN is kept, as the PS4 firmware 11.00 `libSceNgs2.sprx` stores them; not yet confirmed on the PS5 library. That library rejects a level count of 0 and keeps the matrix; the PS5 error code is unknown, so a count of 0 throws, except on a sampler voice without a format, where it clears the matrix (see NGS2 voices without a format). `sceNgs2VoiceRunCommands` is not in that library, so its matrix levels command is assumed to clamp the same way
- [NGS2 sampler voice state flag EMPTY](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a playing sampler voice that still accepts blocks (`CONTINUE`) and has played all queued blocks reports `INUSE|PLAYING|EMPTY` (0x20) and keeps playing, so blocks added later play on. PPSA18391 (engine: Clickteam Fusion) reads `state_flags & 0x22 != 2` from `sceNgs2VoiceGetState` as the end of a sound and only then reuses the voice. When the console sets and clears EMPTY, and whether it also reports it for other voice types, is unverified on hardware.
- [NGS2 port matrix](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a port without a matrix, or with a selected matrix whose levels were never set, mixes through a default channel map: mono reaches both stereo channels at -3 dB and the 5.1 and 7.1 centre, stereo folds to mono at -3 dB per side, and wider sources fold down with 0.5 and -3 dB gains. The map and the equivalence of an unset matrix with no matrix were measured from the PS4 firmware 11.00 `libSceNgs2.sprx`, not yet from the PS5 library. That library only accepts submixer and mastering voices with 1, 2, 6 or 8 channels, so the map into 3, 4, 5 or 7 channels throws. A matrix set with fewer levels than the destination needs still mixes only the rows it has
- [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`. UserFx2's id 0x1f, parameter id `0x40001f00 | index`, process flags (1 on the first call after setup, 2 after a param change, else 0) and call order (setup per voice at creation, process after input mixing, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5). 0x1d is the id PPSA03061 passes (#5139); its UserFx name and parameter id `0x40001d00 | index` are unsourced. Only these two modules and Gain on one buffer work: a module control handler, unequal input and output channels, setup flags and default rack options throw. Module state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` throws for this rack. As in KytyPS5, UserFx2 runs only on grains with input samples, so an effect tail stops with the input
- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - Reverb (0x1b), PitchShift (0x20) and Convolution (0x23) are not processed, nor Gain (0x15) on a custom sampler rack: a rack that passes the creation checks with one throws, as do ids besides these, 0x13, 0x1d and 0x1f. Per [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A8302` for a non-zero state size other than the module's (0 for Gain and Reverb, 8 for PitchShift), and `0x804A8013`, `0x804A8099` and `0x804A8098` for a bad Reverb option size or word and PitchShift word. Unknown: the check order, the errors for a Gain option, a missing Reverb or PitchShift option, other PitchShift and Convolution option sizes, and Convolution's state (64 bytes from SDK 3.0). fma-rc maps 0x19 to UserFx, as KytyPS5 does; it still throws, and 0x1d on PS5 is unknown. PPSA04540 uses 0x15, 0x19, 0x1b and 0x23 (#8039), PPSA35169 0x20 (#4246)
- [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - UserFx2 process flags and setup/process/cleanup order follow only [KytyPS5](https://github.com/KytyPS5/KytyPS5), not a console measurement; layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`. A module control handler, unequal input and output channels, setup flags and default rack options throw. Module state is a separate buffer, not the `state_offset` slice of voice state; `sceNgs2VoiceGetState` throws for this rack. UserFx2 runs only on grains with input samples, so an effect tail stops with the input
- [NGS2 custom UserFx](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - the 48-byte process context, process flags, callback error handling and null-handler behavior retain the existing submixer callback model and are not console-verified. Non-null module options throw because their console error is unknown. UserFx on custom sampler rack 0x4001 throws: its parameter indexing and routing are not implemented. Module id 0x19, the 40-byte voice parameter (handler and three user-data words), parameter id `0x40001900 | index`, and lack of option and module state follow [fma-rc's PS5 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943), including parameter dispatch at 0x25ec4.
- [NGS2 custom Delay](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, 0x1d) - processing and defaults remain unverified on PS5. The [agreed compatibility path](https://github.com/boykopovar/AnyPS5/pull/9337#issuecomment-6351205140) preserves main's PCM pass-through for Void Terrarium (PPSA03061) until any Delay parameter is set; every `0x40001dxx` parameter throws. Only one-buffer custom submixers with no module state and a 20-byte option are accepted. Option type 0/1, taps 1..8, length 1..20000 and reserved 0 follow [fma-rc's 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943). Other shapes and values throw because their console errors are unknown. This preserves host behavior, not console-equivalent Delay processing
- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - Reverb (0x1b), PitchShift (0x20) and Convolution (0x23) are not processed: a rack that passes their creation checks throws. Gain (0x15) on custom sampler racks also throws. Per [fma-rc's 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A8302` for a wrong non-zero state size, and `0x804A8013`, `0x804A8099` and `0x804A8098` for a bad Reverb option size or word and PitchShift word. Unknown: check order, errors for a Gain option, missing Reverb or PitchShift options, other PitchShift and Convolution option sizes, and Convolution's state (64 bytes from SDK 3.0). PPSA04540 uses 0x15, 0x19, 0x1b and 0x23 (#8039), PPSA35169 0x20 (#4246)
- [NGS2 custom Gain module](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - follows [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943): parameter `0x40001500 | module index` holds 37 linear levels, one per channel, default 0, and the module writes source × level into dest, ramping linearly over the grain from the previous level. Unverified: where the ramp starts and ends (here the grain's first sample is one step past the previous level and its last sample is at the new level), whether stopping or setting up the voice again resets the levels, and what the library does with a level outside -32 to 32 (here it throws). On a custom sampler rack, whose parameter index is one higher per fma-rc, Gain throws. A Gain module with an option throws: fma-rc reads Gain as taking none, and the error the library returns for one is unknown. The extra buffer is ignored, as fma-rc reads it
- [NGS2 custom rack buffers and channels](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, racks 0x4001 and 0x4002) - racks with 2 to 4 buffers, custom submixers with 9 to 37 channels and submixer `max_inputs` other than 1 or 2 throw: how modules and ports route several buffers is unknown, and voices hold at most 8 channels. Per [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A830B` for a buffer count outside 1 to 4 or below the submixer's `max_inputs`, `0x804A8306` for a module source, extra or dest buffer id at or above the count, and `0x804A8052` for more than 37 submixer channels; the check order is not known. `sceNgs2RackQueryBufferSize` throws for these options, as no source says whether it checks them. A port buffer id other than 0 throws. PPSA04540 creates racks with up to 4 buffers and a 16-channel custom submixer (#8039)
- [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts
+1 -1
View File
@@ -8,4 +8,4 @@
### Functional
- [libSceOpusCeltDec](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the supplied module confirms the six exported argument lists, read-only context inputs, 640-byte state allocation, mono/stereo channels, five sample rates, byte-sized decode capacity and byte-count return. The implementation still keeps decoder state outside guest memory and uses host contexts instead of AJM contexts. AJM error propagation, resource ownership and termination with live decoders are not implemented. Wrapper validation errors and their precedence are recovered.
- [sceOpusCeltDecDecode](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the original forwards packets to AJM codec 16 without a local CELT-only check. The backend response to SILK/hybrid packets is unresolved; these inputs still throw. Packet loss concealment, asynchronous AJM execution, exact PCM conversion and resampler buffering remain outside the verified behavior of the FFmpeg implementation in [OpusDecoderLibrary](../../../core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp).
- [sceOpusCeltDecDecode](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the original forwards packets to AJM codec 16 without a local CELT-only check. The backend response to SILK/hybrid packets is unresolved; packets whose TOC configuration is below 16 still throw. Decoding uses the bundled libopus decoder of [OpusDecoderLibrary](../../../core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp) and shares its limits. Packet loss concealment and asynchronous AJM execution are not implemented.
+1 -3
View File
@@ -6,6 +6,4 @@
### Functional
- [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for mono/stereo signed-16 decoding at the creation rates the research lists: 8000, 12000, 16000, 24000 and 48000 Hz. The supplied module confirms the exported arguments, read-only context inputs, 640-byte state size and creation rates. Validation errors and their precedence are recovered. Console PCM equivalence, AJM failures, context ownership and termination with live decoders remain unimplemented. Packet loss concealment still throws; decoder state is kept in host registries instead of the native guest state layout.
- [sceOpusDecDecode](../../../core/libs/prx/libSceOpusDec/Export.cpp) (libSceOpusDec) returns exactly the packet's duration, as libopus does. FFmpeg resamples narrowband SILK from 8 kHz and holds back 24 samples of the first narrowband packet after the decoder starts or after a packet of another bandwidth, releasing them with the next packet that is not narrowband. The decoder keeps FFmpeg's output in order and pads the start with silence, so such a packet begins with 24 samples (0.5 ms) of silence and the output after it is 24 samples late; libopus has no such gap
- [sceOpusDecDecode](../../../core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp) (libSceOpusDec, libSceOpusCeltDec) decodes at 48000 Hz and converts to a lower creation rate with FFmpeg's resampler, where libopus decodes at that rate directly. The resampler's filter delay is padded with silence at the start of the stream like the narrowband delay above, so the output is late by that delay and is not sample-identical to libopus; which of the two the console matches is not measured
- [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp) uses bundled libopus 1.6.1 for mono/stereo signed-16 decoding at the creation rates the research lists: 8000, 12000, 16000, 24000 and 48000 Hz. The supplied module confirms the exported arguments, read-only context inputs, 640-byte state size and creation rates. Validation errors and their precedence are recovered. Console PCM equivalence, AJM failures, context ownership and termination with live decoders remain unimplemented. Packet loss concealment still throws; decoder state is kept in host registries instead of the native guest state layout.
+3
View File
@@ -117,6 +117,8 @@
- [close, sceKernelClose](../../../core/libs/prx/libkernel/File/src/HostStreams.cpp) (libkernel) - descriptors 0 to 2 that are open when libkernel loads belong to the host process: closing one fails with `EBADF` and leaves it open. The console's result is unknown. PPSA21703 calls `sceKernelClose(0)` repeatedly; closing the host stream let the host C runtime give descriptor 0 to the next file opened, which the next call then closed
- [sceKernelPrintBacktraceWithModuleInfo](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws
- [pthread_timedjoin_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel, NID `PkS44IGrDkM`) - the signature follows [ps5-payload-dev/sdk](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/pthread_np.h#L71); deadline validation, absolute realtime deadlines, error precedence and join ownership follow [FreeBSD libthr](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libthr/thread/thr_join.c). The [public 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libkernel.c) lists this NID, but PS5 behavior and module/library versions have not been measured. Realtime is rechecked at most two seconds apart while waiting. After guest cleanup completes, final native thread teardown uses the existing unbounded join and may finish after the deadline; host teardown is not bounded by this implementation.
### Functional
- [scePthreadYield](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) on Windows calls Sleep(1) after 512 consecutive yields that find no other thread ready on the current processor and arrive less than ~10,000 TSC ticks apart (so the span varies with host CPU TSC rate); FreeBSD sched_yield never sleeps.
@@ -151,6 +153,7 @@
- [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer
- [sceKernelAioSetParam](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) writes the five values into the 20-byte scheduling struct the first argument points to, following the declarations in [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelAio.cs) and [ampr_emu](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/ps5/include/sce/_fs.h). Whether the console checks the values is unknown, so a null struct, a scheduling window or delayed count limit outside 0..128, a split flag other than 0 or 1 and a split size or chunk size above 0x1000000 (SharpProspero's maximums) throw. The scheduling settings are not used: `sceKernelAioInitializeImpl` ignores the struct and requests run synchronously on submit
- [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist, except `/gkbHtXc3Wq/common/cert/CA_LIST.cer`, which [SandboxCertificates.cpp](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) serves from the host trust store
- [ktimer_create, ktimer_settime, ktimer_gettime, ktimer_getoverrun, ktimer_delete](../../../core/libs/prx/libkernel/Time/Ktimer.cpp) (libkernel) - a `SIGEV_SIGNAL` or `SIGEV_THREAD_ID` timer, and one created with a null `sigevent` (FreeBSD defaults it to `SIGEV_SIGNAL`), throws, because delivering a guest signal from a timer is not modelled. A non-zero value under one tick rounds up to 1 ms, as FreeBSD 11 does at hz 1000; the console's hz is not measured (`kern.clockrate` is unanswered). `SIGEV_NONE` timers on `CLOCK_REALTIME` and `CLOCK_MONOTONIC` follow `kern_time.c`: ids 3 to 31 (`EAGAIN` when full), `EINVAL` for another clock, a bad notify type or signal, an id that is not live or a bad `itimerspec` field, and `TIMER_ABSTIME` (1). Expiry is worked out from the guest clock when the timer is read, and `ktimer_getoverrun` returns 0, as FreeBSD only updates it when a signal is accepted
- [The sandbox CA list](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) (libkernel), `/<sandbox word>/common/cert/CA_LIST.cer`, is a PEM bundle of the host's trust anchors, written once per process to `cert_cache/CA_LIST.cer` in the run directory with the owner write bit cleared: on Windows the current user's `ROOT` store, filtered through Disallowed and the combined EKU extension/property for TLS server authentication (Windows fetches missing roots on demand, so a root that was never fetched is absent), elsewhere `SSL_CERT_FILE` or the first of `/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt`, `/etc/ssl/ca-bundle.pem` and `/etc/ssl/cert.pem` that exists. On macOS, `/etc/ssl/cert.pem` is a static snapshot and does not reflect roots added to or distrusted in the Keychain. Existing guest files, explicit aliases and blocked mounts take precedence over the provider. The PEM snapshot does not carry Windows CTL restrictions beyond EKU, native revocation policy or later trust-store changes. Without any certificate the path does not exist. The console file's format is unverified: only that bundled OpenSSL accepts a PEM bundle there is shown (ProsperoRadio PPSA99001 and SwanStation PPSA98510, #5001). The other entries of the console's `common/cert` directory, the directory itself (it does not exist here) and the error a write open gets (`EACCES` from the host here) are unverified
- [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`)
- [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them
+2
View File
@@ -59,6 +59,8 @@ def package(platform, build, output, version):
if "libSceVorbisDec.prx" in expected:
licenses.append((ROOT / "3rdparty/ogg/COPYING", "libs/licenses/ogg.txt"))
licenses.append((ROOT / "3rdparty/vorbis/COPYING", "libs/licenses/vorbis.txt"))
if expected & {"libSceOpusDec.prx", "libSceOpusCeltDec.prx"}:
licenses.append((ROOT / "3rdparty/opus/COPYING", "libs/licenses/opus.txt"))
if platform == "windows":
runtime = windows_runtime(build)
files.extend(runtime / name for name in ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll"))
+19
View File
@@ -98,6 +98,25 @@ class ReleasePackageTests(unittest.TestCase):
self.assertEqual(archive.extractfile("libs/" + name).read(), name.encode())
self.assertEqual((self.output / "relinker-v1.exe").read_bytes(), b"Windows executable")
def test_opus_library_archives_include_the_bundled_license(self):
(self.source / "core/libs/prx/libSceOpusDec").mkdir()
(self.library.parent / "libSceOpusDec.prx").write_bytes(b"Opus library bytes")
license_file = self.source / "3rdparty/opus/COPYING"
license_file.parent.mkdir(parents=True)
license_file.write_bytes(b"Opus redistribution notice\n")
package_release.package("linux", self.build, self.output, "v1")
with zipfile.ZipFile(self.output / "prx-linux-v1.zip") as archive:
self.assertEqual(archive.read("libs/licenses/opus.txt"), license_file.read_bytes())
with tarfile.open(self.output / "prx-linux-v1.tar.gz") as archive:
self.assertEqual(archive.extractfile("libs/licenses/opus.txt").read(), license_file.read_bytes())
def test_opus_release_without_its_license_is_rejected(self):
(self.source / "core/libs/prx/libSceOpusCeltDec").mkdir()
(self.library.parent / "libSceOpusCeltDec.prx").write_bytes(b"CELT library bytes")
with self.assertRaisesRegex(RuntimeError, "COPYING"):
package_release.package("linux", self.build, self.output, "v1")
self.assertFalse(self.output.exists())
def test_windows_package_without_configured_compiler_preserves_existing_output(self):
(self.build / "CMakeCache.txt").write_text("CMAKE_CXX_COMPILER:FILEPATH=compiler-NOTFOUND\n", encoding="utf-8")
(self.build / "core/relinker/relinker.exe").write_bytes(b"Windows executable")