Merge remote-tracking branch 'origin/main' into codex/fix-cpu-write-mapping-lifetime-20261025

This commit is contained in:
Romans Pokrovskis committed 2026-10-25 22:00:50 +02:00
commit 60fcbde8ca
44 files changed
+1731 -110

No files matched your search

+2
View File
@@ -252,6 +252,8 @@ if(BUILD_TESTING)
endforeach()
add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py)
add_test(NAME symbolize_samples COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_symbolize_samples.py)
add_test(NAME conventions_bom_shebang COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_bom_shebang.py)
set_tests_properties(conventions_bom_shebang PROPERTIES TIMEOUT 20)
+11
View File
@@ -536,6 +536,17 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceMove>")
endif()
add_test_executable(guest_pthread_create_suspended_tests tests/GuestPthreadCreateSuspended.cpp)
target_include_directories(guest_pthread_create_suspended_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_pthread_create_suspended_tests PRIVATE libkernel_sys libkernel libc)
configure_windows_unwind(guest_pthread_create_suspended_tests)
add_test(NAME guest_pthread_create_suspended COMMAND guest_pthread_create_suspended_tests)
set_tests_properties(guest_pthread_create_suspended PROPERTIES TIMEOUT 20)
if(WIN32)
set_tests_properties(guest_pthread_create_suspended PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
endif()
add_test_executable(guest_np_party_tests tests/GuestNpParty.cpp)
target_include_directories(guest_np_party_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_np_party_tests PRIVATE libSceNpParty libc)
@@ -163,18 +163,29 @@ struct Sampler {
return frame;
}
static void writeModules(std::FILE* file, const std::map<std::uint64_t, std::string>& loaded) {
for (const auto& [base, name] : loaded) {
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(base + dos->e_lfanew);
std::fprintf(file, "# module %s 0x%lx 0x%lx\n", name.c_str(), static_cast<unsigned long>(nt->OptionalHeader.SizeOfImage), static_cast<unsigned long>(nt->FileHeader.TimeDateStamp));
}
}
void write() {
std::lock_guard lock(mutex);
std::FILE* file = std::fopen(path.c_str(), "w");
if (file == nullptr) return;
std::fprintf(file, "# %llu samples\n", static_cast<unsigned long long>(samples));
std::map<std::uint64_t, std::string> loaded;
for (const auto& [address, count] : counts) {
HMODULE module = nullptr;
char name[MAX_PATH] = "?";
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast<LPCSTR>(address), &module)) GetModuleFileNameA(module, name, sizeof(name));
const char* base = std::strrchr(name, '\\');
if (module != nullptr) loaded.emplace(reinterpret_cast<std::uint64_t>(module), base ? base + 1 : name);
std::fprintf(file, "%s 0x%llx %llu %llu\n", base ? base + 1 : name, static_cast<unsigned long long>(address - reinterpret_cast<std::uint64_t>(module)), static_cast<unsigned long long>(count.first), static_cast<unsigned long long>(count.second));
}
writeModules(file, loaded);
std::fclose(file);
if (keepStacks) writeStacks();
}
@@ -195,11 +206,7 @@ struct Sampler {
const double tickUs = 1e6 / static_cast<double>(frequency.QuadPart);
std::fprintf(file, "# samples=%llu depth=%d truncated=%llu suspended_us_mean=%.1f suspended_us_max=%.1f\n", static_cast<unsigned long long>(samples), depth, static_cast<unsigned long long>(stacks.Truncated()),
samples != 0 ? static_cast<double>(suspendedTicks) * tickUs / static_cast<double>(samples) : 0.0, static_cast<double>(longestSuspendedTicks) * tickUs);
for (const auto& [base, name] : loaded) {
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(base + dos->e_lfanew);
std::fprintf(file, "# module %s 0x%lx 0x%lx\n", name.c_str(), static_cast<unsigned long>(nt->OptionalHeader.SizeOfImage), static_cast<unsigned long>(nt->FileHeader.TimeDateStamp));
}
writeModules(file, loaded);
const auto text = stacks.Format([&](std::uint64_t address) { return names.at(address); });
const bool written = std::fwrite(text.data(), 1, text.size(), file) == text.size();
if (std::fclose(file) != 0 || !written) return;
@@ -246,6 +253,7 @@ struct ProcessSampler {
std::FILE* file = std::fopen(path.c_str(), "w");
if (file == nullptr) return;
std::fprintf(file, "# %llu rounds\n", static_cast<unsigned long long>(rounds));
std::map<std::uint64_t, std::string> loaded;
for (auto& [id, thread] : threads) {
std::lock_guard lock(thread.mutex);
std::vector<std::pair<std::uint64_t, std::pair<std::uint64_t, std::uint64_t>>> hot(thread.counts.begin(), thread.counts.end());
@@ -260,9 +268,11 @@ struct ProcessSampler {
char name[MAX_PATH] = "?";
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast<LPCSTR>(address), &module)) GetModuleFileNameA(module, name, sizeof(name));
const char* base = std::strrchr(name, '\\');
if (module != nullptr) loaded.emplace(reinterpret_cast<std::uint64_t>(module), base ? base + 1 : name);
std::fprintf(file, " %s 0x%llx %llu %llu\n", base ? base + 1 : name, static_cast<unsigned long long>(address - reinterpret_cast<std::uint64_t>(module)), static_cast<unsigned long long>(hot[i].second.first), static_cast<unsigned long long>(hot[i].second.second));
}
}
Sampler::writeModules(file, loaded);
std::fclose(file);
}
};
@@ -70,5 +70,9 @@ int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int p
int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout, AjmBatchError* error);
int APS5_VABI sceAjmBatchCancel(uint32_t context, uint32_t batch);
int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* error);
void* APS5_VABI sceAjmBatchJobControlBufferRa(void* position, uint32_t instance, uint64_t flags, const void* sideband_input, size_t sideband_input_size, void* sideband_output, size_t sideband_output_size, void* return_address);
void* APS5_VABI sceAjmBatchJobRunBufferRa(void* position, uint32_t instance, uint64_t flags, const void* data_input, size_t data_input_size, void* data_output, size_t data_output_size, void* sideband_output, size_t sideband_output_size, void* return_address);
void* APS5_VABI sceAjmBatchJobRunSplitBufferRa(void* position, uint32_t instance, uint64_t flags, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* sideband_output, size_t sideband_output_size, void* return_address);
int APS5_VABI sceAjmBatchStartBuffer(uint32_t context, const uint8_t* batch, uint32_t batch_size, int priority, AjmBatchError* error, uint32_t* batch_id);
}
+255 -12
View File
@@ -36,6 +36,8 @@ constexpr int SCE_AJM_ERROR_INVALID_INSTANCE = static_cast<int>(0x80930003);
constexpr int SCE_AJM_ERROR_INVALID_BATCH = static_cast<int>(0x80930004);
constexpr int SCE_AJM_ERROR_INVALID_PARAMETER = static_cast<int>(0x80930005);
constexpr int SCE_AJM_ERROR_OUT_OF_RESOURCES = static_cast<int>(0x80930007);
constexpr int SCE_AJM_ERROR_MALFORMED_BATCH = static_cast<int>(0x80930011);
constexpr int SCE_AJM_ERROR_INVALID_ADDRESS = static_cast<int>(0x80930016);
constexpr std::int32_t AJM_RESULT_NOT_INITIALIZED = 0x00000001;
constexpr std::int32_t AJM_RESULT_INVALID_DATA = 0x00000002;
@@ -216,6 +218,7 @@ struct JobHeader {
std::uint32_t inputCount;
std::uint32_t outputCount;
std::uint8_t parameters[16];
bool console;
};
struct PackedJob {
@@ -691,9 +694,17 @@ void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm,
ResamplerProduce(instance, outputs, channels, encoding, sampleBytes);
}
// A PS4 reports an ATRAC9 stream's bitrate from its superframe: 448-byte superframes of 4 frames of 256
// samples at 48 kHz gave 168000 (Knowledge is Power: Decades, every run job's format sideband).
std::uint32_t At9Bitrate(const Instance& instance) {
const auto samples = static_cast<std::uint64_t>(instance.info.framesInSuperframe) * static_cast<std::uint64_t>(instance.info.frameSamples);
if (samples == 0) return 0;
return static_cast<std::uint32_t>(static_cast<std::uint64_t>(instance.info.superframeSize) * 8u * static_cast<std::uint64_t>(instance.info.samplingRate) / samples);
}
SidebandFormat CurrentFormat(const Instance& instance) {
switch (instance.codec) {
case CODEC_AT9: return {static_cast<std::uint32_t>(instance.info.channels), ChannelMask(static_cast<std::size_t>(instance.info.channels)), static_cast<std::uint32_t>(instance.info.samplingRate), PcmEncoding(instance), 0, 0};
case CODEC_AT9: return {static_cast<std::uint32_t>(instance.info.channels), ChannelMask(static_cast<std::size_t>(instance.info.channels)), static_cast<std::uint32_t>(instance.info.samplingRate), PcmEncoding(instance), At9Bitrate(instance), 0};
case CODEC_MP3: return {instance.mp3Channels, ChannelMask(instance.mp3Channels), instance.mp3SampleRate, PcmEncoding(instance), instance.mp3Bitrate, 0};
case CODEC_OPUS: return {instance.opusChannels, ChannelMask(instance.opusChannels), instance.opusSampleRate, PcmEncoding(instance), 0, 0};
case CODEC_M4AAC: return {instance.aacChannels, ChannelMask(instance.aacChannels), instance.aacSampleRate, PcmEncoding(instance), instance.aacBitrate, 0};
@@ -768,12 +779,22 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c
if (segmentEnded()) break;
if (instance.superframeRemaining == 0) consumed += RiffDataOffset(input.data() + consumed, input.size() - consumed);
const std::size_t needed = instance.superframeRemaining == 0 ? superframeSize : instance.superframeRemaining;
const bool roomForFrame = Resampling(instance) ? pcmOutputs.Room() >= channels * sampleBytes : pcmOutputs.Room() >= frameBytes;
// A PS4 reports which side ran out (probe runs of a title's ATRAC9 stream through libSceAjm's
// builders): PARTIAL_INPUT when the input ran out, on a superframe boundary or not, with room
// left; NOT_ENOUGH_ROOM when the room ran out with input left, after however many frames (and
// with none decoded for a zero room); both (0x18) when the input ended just as the output
// filled, which is every Wwise run job. A run without input (a format query) reports 0.
// Resampled runs were not measured and keep the earlier rule: a trailing partial superframe is
// partial input, a zero-output run is out of room.
const bool measured = !Resampling(instance);
if (input.size() - consumed < needed) {
if (input.size() != consumed) result |= AJM_RESULT_PARTIAL_INPUT;
if (input.size() != consumed || (measured && !input.empty())) result |= AJM_RESULT_PARTIAL_INPUT;
if (measured && !input.empty() && !roomForFrame) result |= AJM_RESULT_NOT_ENOUGH_ROOM;
break;
}
if (Resampling(instance) ? pcmOutputs.Room() < channels * sampleBytes : pcmOutputs.Room() < frameBytes) {
if (frames == 0 && pcmOutputs.produced == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM;
if (!roomForFrame) {
if (measured || (frames == 0 && pcmOutputs.produced == 0)) result |= AJM_RESULT_NOT_ENOUGH_ROOM;
break;
}
if (instance.superframeRemaining == 0) {
@@ -797,6 +818,7 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c
++frames;
EmitFrame(instance, pcmOutputs, pcm.data(), frameSamples, channels, sampleBytes);
if (job.console && (job.flags & RUN_MULTIPLE_FRAMES) == 0) break;
}
if (segmentEnded()) instance.segmentSamples = 0;
@@ -1692,6 +1714,169 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out
}
}
int RegisterBatch(std::uint32_t context, AjmBatchError* error, std::uint32_t* batch) {
if (error) std::memset(error, 0, sizeof(*error));
const auto id = g_nextBatch.fetch_add(1, std::memory_order_relaxed);
{
std::lock_guard lock(g_batchLock);
g_batches.emplace(id, context);
}
*batch = id;
return 0;
}
// The PS4 batch buffer (sceAjmBatchStartBuffer) is not private: the PS4 SDK builds it with inline code
// in the title, so it has the console's layout, taken from shadPS4's reimplementation of the PS4
// library. It is a run of chunks, each starting with a 32-bit word that holds the chunk kind in bits
// 0-5 and a payload in bits 6-25. A job chunk has the instance as payload and, in a second word, the
// byte count of the chunks after it that describe the job; an inline chunk holds that many bytes of data
// a job's control buffer may point to. A job's flags chunk has flags bits 32-51 as payload and bits 0-31
// in its second word; its buffer chunks have a 32-bit size and a 64-bit address.
enum class ConsoleChunk : std::uint32_t {
Job = 0,
InputRunBuffer = 1,
InputControlBuffer = 2,
ControlFlags = 3,
RunFlags = 4,
ReturnAddress = 6,
InlineBuffer = 7,
OutputRunBuffer = 17,
OutputControlBuffer = 18,
};
constexpr std::uint32_t CONSOLE_STATISTICS_INSTANCE = 0x80000;
constexpr std::uint64_t CONSOLE_FLAGS_VERSION_AND_CODEC = 0x7FF;
// The low header word as the PS4's libSceAjm builders write it (a trace of 32 545 batches a Wwise title
// started on a PS4): bits 0-3 the kind, bit 4 the output direction of a buffer chunk
// (0x11 output run, 0x12 output control), bits 6-25 the job's instance or the flags' bits 32-51. The
// builders leave bits 5, 26, 27 and 31 as they were, and a title that reuses its batch memory (Wwise
// does) carries the previous batch's bytes there: a 6-bit kind read job headers as 16, 32 or 48 and run
// flags as 20, 36 or 52, and a 26-bit payload read flags bit 52 set.
ConsoleChunk ConsoleChunkKind(std::uint32_t word) {
const std::uint32_t kind = word & 0xFu;
return static_cast<ConsoleChunk>(kind == 1 || kind == 2 ? kind | (word & 0x10u) : kind);
}
std::uint32_t ConsoleChunkPayload(std::uint32_t word) {
return (word >> 6) & 0xFFFFFu;
}
std::uint8_t* PutConsoleChunk(std::uint8_t* at, ConsoleChunk kind, std::uint32_t payload, std::uint32_t word) {
const std::uint32_t words[2] = {static_cast<std::uint32_t>(kind) | ((payload & 0xFFFFFu) << 6), word};
std::memcpy(at, words, sizeof(words));
return at + sizeof(words);
}
std::uint8_t* PutConsoleFlags(std::uint8_t* at, ConsoleChunk kind, std::uint64_t flags) {
return PutConsoleChunk(at, kind, static_cast<std::uint32_t>(flags >> 32), static_cast<std::uint32_t>(flags));
}
std::uint8_t* PutConsoleBuffer(std::uint8_t* at, ConsoleChunk kind, const void* address, std::size_t size) {
at = PutConsoleChunk(at, kind, 0, static_cast<std::uint32_t>(size));
std::memcpy(at, &address, sizeof(address));
return at + sizeof(address);
}
std::uint8_t* BeginConsoleJob(std::uint8_t* job, const void* returnAddress) {
std::uint8_t* at = job + 2 * sizeof(std::uint32_t);
return returnAddress ? PutConsoleBuffer(at, ConsoleChunk::ReturnAddress, returnAddress, 0) : at;
}
void* EndConsoleJob(std::uint8_t* job, std::uint8_t* end, std::uint32_t instance) {
PutConsoleChunk(job, ConsoleChunk::Job, instance, static_cast<std::uint32_t>(end - job) - 2 * sizeof(std::uint32_t));
return end;
}
struct ConsoleJob {
JobHeader header;
std::vector<AjmBuffer> buffers;
};
// What sceAjmBatchStartBuffer reports in the AjmBatchError when it refuses a batch: the error, the job
// chunk, the offending chunk's offset in the job and the job's return address, as a PS4 fills them for a
// buffer chunk with a null address and a nonzero size (INVALID_ADDRESS, nothing of the batch run). The
// other refusals here report MALFORMED_BATCH the same way; they were not measured.
struct ConsoleRefusal {
std::int32_t code = SCE_AJM_ERROR_MALFORMED_BATCH;
const std::uint8_t* job = nullptr;
std::uint32_t offset = 0;
const void* returnAddress = nullptr;
};
// Reads one console job: the chunks between the job header at `job` and `end`. A buffer chunk of size
// zero is dropped whatever its address, as a PS4 does. Returns false with the refusal filled for a null
// address with a nonzero size, for chunks that overrun the job and for a job without flags.
bool ReadConsoleJob(std::uint32_t instance, const std::uint8_t* job, const std::uint8_t* end, ConsoleJob& out, ConsoleRefusal& refusal) {
out.header = JobHeader{};
out.header.instance = instance;
out.header.console = true;
std::vector<AjmBuffer> inputs;
std::vector<AjmBuffer> outputs;
AjmBuffer controlInput{};
AjmBuffer controlOutput{};
ConsoleChunk flagsKind = ConsoleChunk::Job;
refusal = ConsoleRefusal{};
refusal.job = job;
const std::uint8_t* at = job + 2 * sizeof(std::uint32_t);
while (at < end) {
refusal.offset = static_cast<std::uint32_t>(at - job);
if (end - at < 8) return false;
std::uint32_t words[2];
std::memcpy(words, at, sizeof(words));
const auto kind = ConsoleChunkKind(words[0]);
if (kind == ConsoleChunk::ControlFlags || kind == ConsoleChunk::RunFlags) {
if (flagsKind != ConsoleChunk::Job) return false;
flagsKind = kind;
out.header.flags = (std::uint64_t{ConsoleChunkPayload(words[0])} << 32) | words[1];
at += sizeof(words);
continue;
}
if (kind != ConsoleChunk::InputRunBuffer && kind != ConsoleChunk::InputControlBuffer && kind != ConsoleChunk::ReturnAddress && kind != ConsoleChunk::OutputRunBuffer && kind != ConsoleChunk::OutputControlBuffer) {
NotImplemented_nid_no_patch(("sceAjmBatchStartBuffer (job chunk kind " + std::to_string(static_cast<unsigned>(kind)) + ")").c_str());
}
if (end - at < 16) return false;
AjmBuffer buffer{nullptr, words[1]};
std::memcpy(&buffer.ptr, at + sizeof(words), sizeof(buffer.ptr));
at += 16;
if (kind == ConsoleChunk::ReturnAddress) {
refusal.returnAddress = buffer.ptr;
continue;
}
if (buffer.size == 0) continue;
if (!buffer.ptr) {
refusal.code = SCE_AJM_ERROR_INVALID_ADDRESS;
return false;
}
if (kind == ConsoleChunk::InputRunBuffer) inputs.push_back(buffer);
if (kind == ConsoleChunk::OutputRunBuffer) outputs.push_back(buffer);
if (kind == ConsoleChunk::InputControlBuffer) controlInput = buffer;
if (kind == ConsoleChunk::OutputControlBuffer) controlOutput = buffer;
}
refusal.offset = static_cast<std::uint32_t>(end - job);
if (flagsKind == ConsoleChunk::Job) return false;
JobHeader& header = out.header;
header.sideband = controlOutput.ptr;
header.sidebandSize = controlOutput.size;
if (instance == CONSOLE_STATISTICS_INSTANCE) {
header.kind = JobKind::GetStatistics;
} else if (flagsKind == ConsoleChunk::ControlFlags) {
constexpr std::uint64_t implemented = CONSOLE_FLAGS_VERSION_AND_CODEC | CONTROL_RESET | CONTROL_INITIALIZE | SIDEBAND_GAPLESS_DECODE;
if ((header.flags & ~implemented) != 0) NotImplemented_nid_no_patch("sceAjmBatchStartBuffer (control job flags other than RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE)");
if (!inputs.empty() || !outputs.empty()) NotImplemented_nid_no_patch("sceAjmBatchStartBuffer (control job with run buffers)");
header.kind = JobKind::Control;
if (controlInput.size) inputs.push_back(controlInput);
} else {
if (controlInput.size) NotImplemented_nid_no_patch("sceAjmBatchStartBuffer (run job with an input control buffer)");
header.kind = JobKind::Run;
}
header.inputCount = static_cast<std::uint32_t>(inputs.size());
header.outputCount = static_cast<std::uint32_t>(outputs.size());
AJM_TRACE("[ajm] console job: instance %u, %s flags 0x%llx, %u input and %u output buffers, control input %llu and output %llu bytes\n", instance, flagsKind == ConsoleChunk::ControlFlags ? "control" : "run", static_cast<unsigned long long>(header.flags), header.inputCount, header.outputCount, static_cast<unsigned long long>(controlInput.size), static_cast<unsigned long long>(controlOutput.size));
out.buffers = std::move(inputs);
out.buffers.insert(out.buffers.end(), outputs.begin(), outputs.end());
return true;
}
}
extern "C" {
@@ -1923,14 +2108,7 @@ int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int p
remaining -= record.bytes;
if (remaining != 0) cursor += record.bytes;
}
if (error) std::memset(error, 0, sizeof(*error));
const auto id = g_nextBatch.fetch_add(1, std::memory_order_relaxed);
{
std::lock_guard lock(g_batchLock);
g_batches.emplace(id, context);
}
*batch = id;
return 0;
return RegisterBatch(context, error, batch);
}
int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout, AjmBatchError* error) {
@@ -1959,4 +2137,69 @@ int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* erro
return 0;
}
void* APS5_VABI sceAjmBatchJobControlBufferRa(void* position, uint32_t instance, uint64_t flags, const void* sideband_input, size_t sideband_input_size, void* sideband_output, size_t sideband_output_size, void* return_address) {
auto* job = static_cast<std::uint8_t*>(position);
auto* at = BeginConsoleJob(job, return_address);
at = PutConsoleBuffer(at, ConsoleChunk::InputControlBuffer, sideband_input, sideband_input_size);
at = PutConsoleFlags(at, ConsoleChunk::ControlFlags, flags);
at = PutConsoleBuffer(at, ConsoleChunk::OutputControlBuffer, sideband_output, sideband_output_size);
return EndConsoleJob(job, at, instance);
}
void* APS5_VABI sceAjmBatchJobRunSplitBufferRa(void* position, uint32_t instance, uint64_t flags, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* sideband_output, size_t sideband_output_size, void* return_address) {
if ((input_buffers_num && !input_buffers) || (output_buffers_num && !output_buffers)) NotImplemented_nid_no_patch("AJM batch job with a null buffer list and a nonzero buffer count");
auto* job = static_cast<std::uint8_t*>(position);
auto* at = BeginConsoleJob(job, return_address);
for (std::size_t index = 0; index < input_buffers_num; ++index) at = PutConsoleBuffer(at, ConsoleChunk::InputRunBuffer, input_buffers[index].ptr, input_buffers[index].size);
at = PutConsoleFlags(at, ConsoleChunk::RunFlags, flags);
for (std::size_t index = 0; index < output_buffers_num; ++index) at = PutConsoleBuffer(at, ConsoleChunk::OutputRunBuffer, output_buffers[index].ptr, output_buffers[index].size);
at = PutConsoleBuffer(at, ConsoleChunk::OutputControlBuffer, sideband_output, sideband_output_size);
return EndConsoleJob(job, at, instance);
}
void* APS5_VABI sceAjmBatchJobRunBufferRa(void* position, uint32_t instance, uint64_t flags, const void* data_input, size_t data_input_size, void* data_output, size_t data_output_size, void* sideband_output, size_t sideband_output_size, void* return_address) {
const AjmBuffer input{const_cast<void*>(data_input), data_input_size};
const AjmBuffer output{data_output, data_output_size};
return sceAjmBatchJobRunSplitBufferRa(position, instance, flags, &input, 1, &output, 1, sideband_output, sideband_output_size, return_address);
}
int APS5_VABI sceAjmBatchStartBuffer(uint32_t context, const uint8_t* batch, uint32_t batch_size, int priority, AjmBatchError* error, uint32_t* batch_id) {
if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT;
if (!batch_id || !batch) return SCE_AJM_ERROR_INVALID_PARAMETER;
AJM_TRACE("[ajm] batch start buffer: context %u, priority %d, %u bytes\n", context, priority, batch_size);
std::vector<ConsoleJob> jobs;
ConsoleRefusal refusal;
const std::uint8_t* at = batch;
const std::uint8_t* end = batch + batch_size;
bool accepted = batch_size % 8 == 0;
while (accepted && at < end) {
refusal = ConsoleRefusal{};
refusal.job = at;
if (end - at < 8) {
accepted = false;
break;
}
std::uint32_t words[2];
std::memcpy(words, at, sizeof(words));
const std::uint8_t* body = at + sizeof(words);
if (words[1] > static_cast<std::size_t>(end - body)) {
accepted = false;
break;
}
const auto kind = ConsoleChunkKind(words[0]);
if (kind == ConsoleChunk::Job) {
accepted = ReadConsoleJob(ConsoleChunkPayload(words[0]), at, body + words[1], jobs.emplace_back(), refusal);
} else if (kind != ConsoleChunk::InlineBuffer) {
NotImplemented_nid_no_patch(("sceAjmBatchStartBuffer (batch chunk kind " + std::to_string(static_cast<unsigned>(kind)) + ")").c_str());
}
at = body + words[1];
}
if (!accepted) {
if (error) *error = AjmBatchError{refusal.code, refusal.job, refusal.offset, refusal.returnAddress};
return SCE_AJM_ERROR_MALFORMED_BATCH;
}
for (const ConsoleJob& job : jobs) Execute(job.header, job.buffers.data(), job.buffers.data() + job.header.inputCount);
return RegisterBatch(context, error, batch_id);
}
}
+1 -14
View File
@@ -6,19 +6,6 @@
extern "C" {
int APS5_VABI sceAjmBatchJobControlBufferRa() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmBatchJobRunSplitBufferRa() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmBatchStartBuffer() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
APS5_DUMMY_FUN
}
-4
View File
@@ -4,8 +4,4 @@
extern "C" {
int APS5_VABI sceAjmBatchJobControlBufferRa();
int APS5_VABI sceAjmBatchJobRunSplitBufferRa();
int APS5_VABI sceAjmBatchStartBuffer();
}
+2 -2
View File
@@ -13,8 +13,8 @@ int APS5_VABI sceAjmBatchJobInlineBuffer(void) {
return 0;
}
int APS5_VABI sceAjmBatchJobRunBufferRa(void) {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceAjmBatchJobRunBufferRa_nid_disambig1(void) {
NotImplemented_nid_no_patch("sceAjmBatchJobRunBufferRa");
return 0;
}
+1 -1
View File
@@ -5,6 +5,6 @@
extern "C" {
int APS5_VABI sceAjmBatchJobInlineBuffer(void);
int APS5_VABI sceAjmBatchJobRunBufferRa(void);
int APS5_VABI sceAjmBatchJobRunBufferRa_nid_disambig1(void);
}
+1 -1
View File
@@ -152,7 +152,7 @@ int APS5_VABI sceAvPlayerSetAvailableBandwidth(AvPlayerInternal* h, uint32_t sta
(void)minimum_bandwidth;
(void)maximum_bandwidth;
if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS;
return SCE_OK;
return ToPlayer(h)->SetAvailableBandwidth();
}
int APS5_VABI sceAvPlayerSetLogCallback(void* callback, void* user_data) {
@@ -127,6 +127,7 @@ public:
int JumpToTime(std::uint64_t milliseconds);
int SetLooping(bool enabled);
int SetTrickSpeed(std::int32_t trickSpeed);
int SetAvailableBandwidth();
int SetAvSyncMode(std::uint32_t mode);
bool GetVideoData(AvPlayerFrameInfoEx& info);
bool GetVideoData(AvPlayerFrameInfo& info);
+8 -2
View File
@@ -334,14 +334,20 @@ int Player::SetLooping(bool enabled) {
}
int Player::SetTrickSpeed(std::int32_t trickSpeed) {
if (trickSpeed == 0) return SCE_AVPLAYER_ERROR_INVALID_PARAMS;
std::lock_guard lock(mutex);
if (!source) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
if (trickSpeed == 0) return SCE_AVPLAYER_ERROR_INVALID_PARAMS;
if (state == State::Stop || state == State::EndOfFile || state == State::Error) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
speed = trickSpeed;
if (source) source->SetSpeed(trickSpeed);
source->SetSpeed(trickSpeed);
return SCE_OK;
}
int Player::SetAvailableBandwidth() {
std::lock_guard lock(mutex);
return source ? SCE_AVPLAYER_ERROR_OPERATION_FAILED : SCE_OK;
}
int Player::SetAvSyncMode(std::uint32_t mode) {
if (mode != SyncModeDefault && mode != SyncModeNone) return SCE_AVPLAYER_ERROR_INVALID_PARAMS;
std::lock_guard lock(mutex);
+35 -2
View File
@@ -16,15 +16,30 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou
std::vector<void*> pointers;
std::vector<int> suppressedEnds(static_cast<size_t>(std::count(format, format + std::strlen(format), '%')), -1);
size_t suppressedCount = 0;
struct NarrowInteger {
void* destination;
size_t size;
bool isUnsigned;
long long value;
unsigned long long unsignedValue;
int end;
};
std::vector<NarrowInteger> narrowIntegers;
narrowIntegers.reserve(suppressedEnds.size());
FormatArguments args(source);
while (*format) {
const char value = *format++;
translated += value;
if (value != '%') continue;
if (*format == '%') { translated += *format++; continue; }
const size_t directive = translated.size() - 1;
const bool suppressed = *format == '*';
if (suppressed) translated += *format++;
while (*format >= '0' && *format <= '9') translated += *format++;
if (*format == 'q') {
translated.resize(directive);
break;
}
std::string length;
if (*format && std::strchr("hljztL", *format)) {
length += *format++;
@@ -37,7 +52,8 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou
return EOF;
}
++format;
if (std::strchr("diouxXn", conversion) && (length == "l" || length == "j" || length == "z" || length == "t"))
const bool narrow = !suppressed && std::strchr("diouxX", conversion) && (length.empty() || length == "h" || length == "hh");
if (narrow || (std::strchr("diouxXn", conversion) && (length == "l" || length == "j" || length == "z" || length == "t")))
translated += "ll";
else translated += length;
translated += conversion;
@@ -48,13 +64,30 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou
if (*format != ']') { errno = 22; return EOF; }
translated += *format++;
}
if (!suppressed) pointers.push_back(args.Next<void*>());
if (narrow) {
const bool isUnsigned = std::strchr("ouxX", conversion) != nullptr;
narrowIntegers.push_back({args.Next<void*>(), length.empty() ? sizeof(int) : length == "h" ? sizeof(short) : sizeof(char), isUnsigned, 0, 0, -1});
translated += "%n";
auto& integer = narrowIntegers.back();
pointers.push_back(isUnsigned ? static_cast<void*>(&integer.unsignedValue) : static_cast<void*>(&integer.value));
pointers.push_back(&integer.end);
} else if (!suppressed) pointers.push_back(args.Next<void*>());
else if (conversion != 'n') {
translated += "%n";
pointers.push_back(&suppressedEnds[suppressedCount++]);
}
}
const int result = scan(translated.c_str(), reinterpret_cast<char*>(pointers.data()));
for (const auto& integer : narrowIntegers) {
if (integer.end < 0) continue;
if (integer.isUnsigned) {
if (integer.size == sizeof(char)) *static_cast<unsigned char*>(integer.destination) = static_cast<unsigned char>(integer.unsignedValue);
else if (integer.size == sizeof(short)) *static_cast<unsigned short*>(integer.destination) = static_cast<unsigned short>(integer.unsignedValue);
else *static_cast<unsigned int*>(integer.destination) = static_cast<unsigned int>(integer.unsignedValue);
} else if (integer.size == sizeof(char)) *static_cast<signed char*>(integer.destination) = static_cast<signed char>(integer.value);
else if (integer.size == sizeof(short)) *static_cast<short*>(integer.destination) = static_cast<short>(integer.value);
else *static_cast<int*>(integer.destination) = static_cast<int>(integer.value);
}
if (result == EOF && std::any_of(suppressedEnds.begin(), suppressedEnds.begin() + suppressedCount, [](int end) { return end >= 0; }))
return 0;
return result;
+9
View File
@@ -2036,6 +2036,15 @@ if(BUILD_TESTING)
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)
if(WIN32 AND Python3_Interpreter_FOUND)
foreach(joinProvider IN ITEMS libkernel_sys libkernel_web)
add_test(NAME guest_timed_join_import_${joinProvider} COMMAND ${Python3_EXECUTABLE}
${LIBS_INCLUDE_DIR}/tests/test_windows_timed_join_imports.py $<TARGET_FILE:relinker> ${PATCHED_DIR}
${joinProvider}.prx $<TARGET_FILE_DIR:guest_timed_join_tests>)
set_tests_properties(guest_timed_join_import_${joinProvider} PROPERTIES TIMEOUT 25
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:guest_timed_join_tests>")
endforeach()
endif()
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)
@@ -32,6 +32,7 @@ int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr)
p->_detachstate = thread->_detached ? 1 : 0;
p->_schedpriority = thread->priority.load(std::memory_order_relaxed);
p->_schedpolicy = thread->schedPolicy.load(std::memory_order_relaxed);
p->_createSuspended = thread->createdSuspended;
p->_affinity = thread->affinity.load(std::memory_order_relaxed);
p->_affinityExplicit = true;
p->_scope = thread->scope;
@@ -110,6 +110,7 @@ struct PthreadAttrPrivate {
int _schedpriority;
int _schedpolicy;
int _inheritsched;
bool _createSuspended = false;
KernelCpumask _affinity = DEFAULT_THREAD_AFFINITY;
bool _affinityExplicit = false;
std::size_t _guardsize = 0x1000;
@@ -130,6 +131,8 @@ struct PthreadPrivate {
std::atomic<unsigned> references{2};
std::atomic<bool> inWait{false};
std::atomic<int> pendingException{0};
std::atomic<bool> suspended{false};
bool createdSuspended = false;
std::atomic<int> schedPolicy{1};
void* wakeEvent = nullptr;
void* stackAddress = nullptr;
@@ -179,6 +182,8 @@ void SetLiveGuestThreadsAffinity(KernelCpumask mask);
KernelCpumask LiveGuestThreadsAffinity();
bool GuestThreadIsLive(const PthreadPrivate* thread);
std::unique_lock<std::mutex> LockLiveGuestThread(const PthreadPrivate* thread);
extern "C" int ResumeGuestThread_nid_no_patch(PthreadPrivate* thread);
extern "C" void ResumeAllGuestThreads_nid_no_patch();
bool LiveGuestThreadExists(std::int64_t tid);
bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& mask);
bool GetLiveGuestThreadSched_nid_no_patch(std::int64_t tid, int& policy, int& priority);
@@ -98,6 +98,7 @@ int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr) {
(*attr)->_schedpriority = thread->priority.load(std::memory_order_relaxed);
(*attr)->_schedpolicy = thread->schedPolicy.load(std::memory_order_relaxed);
(*attr)->_inheritsched = 4;
(*attr)->_createSuspended = thread->createdSuspended;
(*attr)->_affinity = thread->affinity.load(std::memory_order_relaxed);
(*attr)->_affinityExplicit = true;
(*attr)->_scope = thread->scope;
@@ -555,6 +555,26 @@ bool GuestThreadIsLive(const PthreadPrivate* thread) {
return LockLiveGuestThread(thread).owns_lock();
}
extern "C" int ResumeGuestThread_nid_no_patch(PthreadPrivate* thread) {
constexpr int GuestEsrch = 3;
const auto live = LockLiveGuestThread(thread);
if (!live.owns_lock()) return GuestEsrch;
thread->suspended.store(false, std::memory_order_release);
thread->suspended.notify_all();
return 0;
}
extern "C" void ResumeAllGuestThreads_nid_no_patch() {
const auto* self = CurrentGuestState().current;
auto& registry = CpuClocks();
std::lock_guard lock(registry.lock);
for (const auto& [id, thread] : registry.threads) {
if (thread == self) continue;
thread->suspended.store(false, std::memory_order_release);
thread->suspended.notify_all();
}
}
bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& mask) {
auto& registry = CpuClocks();
std::lock_guard lock(registry.lock);
@@ -775,6 +795,7 @@ static void RunThread(std::unique_ptr<ThreadArgs> args) {
const sigset_t signalMask = args->signalMask;
#endif
args.reset();
self->suspended.wait(true, std::memory_order_acquire);
#ifndef _WIN32
pthread_sigmask(SIG_SETMASK, &signalMask, nullptr);
#endif
@@ -950,6 +971,8 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread
p->scope = CurrentGuestState().current->scope;
}
if (name) p->name = name;
p->createdSuspended = attr && (*attr)->_createSuspended;
p->suspended.store(p->createdSuspended, std::memory_order_relaxed);
std::promise<bool> start;
auto args = std::make_unique<ThreadArgs>(ThreadArgs{entry, arg, p.get()});
#ifdef _WIN32
@@ -681,6 +681,7 @@ int APS5_VABI sceKernelRaiseException(Pthread thread, int signum) {
const auto handler = Handler(signum);
if (handler == nullptr) throw std::runtime_error("sceKernelRaiseException: no handler installed for the signal");
#ifdef _WIN32
if (thread->suspended.load(std::memory_order_acquire)) throw std::runtime_error("sceKernelRaiseException: the target thread is still suspended at creation");
return RaiseOn(thread, handler, signum) ? 0 : SCE_KERNEL_ERROR_ESRCH;
#else
return RaiseOn(thread) ? 0 : SCE_KERNEL_ERROR_ESRCH;
@@ -1,3 +1,5 @@
#include "prx/libkernel/KernelErrors.hpp"
#include "prx/libkernel/Pthread/include/Pthread.hpp"
#include "prx/libkernel_sys/include/Api.hpp"
#include "prx/libkernel/Pthread/include/Atfork.hpp"
#include "prx/libkernel/include/Api.hpp"
@@ -282,8 +284,10 @@ int APS5_VABI pthread_attr_setaffinity_np_nid_postfix(void) {
return 0;
}
int APS5_VABI pthread_attr_setcreatesuspend_np_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI pthread_attr_setcreatesuspend_np_nid_postfix(PthreadAttr* attr) {
constexpr int GuestEinval = 22;
if (!attr || !*attr) return GuestEinval;
(*attr)->_createSuspended = true;
return 0;
}
@@ -318,7 +322,7 @@ int APS5_VABI pthread_getstack_np_nid_postfix(void) {
}
int APS5_VABI pthread_multi_np_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
pthread_resume_all_np_nid_postfix();
return 0;
}
@@ -352,14 +356,15 @@ int APS5_VABI pthread_mutexattr_setgen_np_nid_postfix(void) {
return 0;
}
int APS5_VABI pthread_resume_all_np_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
void APS5_VABI pthread_resume_all_np_nid_postfix(void) {
ResumeAllGuestThreads_nid_no_patch();
scePthreadTestcancel();
}
int APS5_VABI pthread_resume_np_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI pthread_resume_np_nid_postfix(Pthread thread) {
constexpr int GuestEinval = 22;
if (!thread) return GuestEinval;
return ResumeGuestThread_nid_no_patch(thread);
}
int APS5_VABI pthread_resume_user_context_np_nid_postfix(void) {
@@ -2119,9 +2124,9 @@ int APS5_VABI scePthreadAttrGetscope(const PthreadAttr* attr, int* scope) {
return SceResult(pthread_attr_getscope_nid_postfix(attr, scope));
}
int APS5_VABI scePthreadAttrSetcreatesuspend(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI scePthreadAttrSetcreatesuspend(PthreadAttr* attr) {
const int error = pthread_attr_setcreatesuspend_np_nid_postfix(attr);
return error == 0 ? 0 : SceKernelError(error);
}
int APS5_VABI scePthreadAttrSetscope(PthreadAttr* attr, int scope) {
@@ -2172,8 +2177,8 @@ int APS5_VABI scePthreadMain(void) {
}
int APS5_VABI scePthreadMulti(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
const int error = pthread_multi_np_nid_postfix();
return error == 0 ? 0 : SceKernelError(error);
}
int APS5_VABI scePthreadMutexGetprioceiling(void) {
@@ -2221,14 +2226,13 @@ int APS5_VABI scePthreadMutexattrSetkind(void) {
return 0;
}
int APS5_VABI scePthreadResume(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI scePthreadResume(Pthread thread) {
const int error = pthread_resume_np_nid_postfix(thread);
return error == 0 ? 0 : SceKernelError(error);
}
int APS5_VABI scePthreadResumeAll(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
void APS5_VABI scePthreadResumeAll(void) {
pthread_resume_all_np_nid_postfix();
}
int APS5_VABI scePthreadSetBesteffort(void) {
+6 -6
View File
@@ -123,7 +123,7 @@ int APS5_VABI profil_nid_postfix(void);
int APS5_VABI pthread_attr_getaffinity_np_nid_postfix(void);
int APS5_VABI pthread_attr_getsolosched_np_nid_postfix(void);
int APS5_VABI pthread_attr_setaffinity_np_nid_postfix(void);
int APS5_VABI pthread_attr_setcreatesuspend_np_nid_postfix(void);
int APS5_VABI pthread_attr_setcreatesuspend_np_nid_postfix(PthreadAttr* attr);
int APS5_VABI pthread_attr_setstackaddr_nid_postfix(void);
int APS5_VABI pthread_barrier_setname_np_nid_postfix(void);
int APS5_VABI pthread_cond_setname_np_nid_postfix(void);
@@ -138,8 +138,8 @@ int APS5_VABI pthread_mutex_setname_np_nid_postfix(void);
int APS5_VABI pthread_mutex_setprioceiling_nid_postfix(void);
int APS5_VABI pthread_mutexattr_getgen_np_nid_postfix(void);
int APS5_VABI pthread_mutexattr_setgen_np_nid_postfix(void);
int APS5_VABI pthread_resume_all_np_nid_postfix(void);
int APS5_VABI pthread_resume_np_nid_postfix(void);
void APS5_VABI pthread_resume_all_np_nid_postfix(void);
int APS5_VABI pthread_resume_np_nid_postfix(Pthread thread);
int APS5_VABI pthread_resume_user_context_np_nid_postfix(void);
int APS5_VABI pthread_rwlock_setname_np_nid_postfix(void);
int APS5_VABI pthread_rwlockattr_settype_np_nid_postfix(void);
@@ -496,7 +496,7 @@ int APS5_VABI sceKernelYieldCpumode(void);
int APS5_VABI sceLibcMspaceCreateForMonoMutex(void);
int APS5_VABI scePthreadAtfork(void (APS5_VABI* prepare)(), void (APS5_VABI* parent)(), void (APS5_VABI* child)());
int APS5_VABI scePthreadAttrGetscope(const PthreadAttr* attr, int* scope);
int APS5_VABI scePthreadAttrSetcreatesuspend(void);
int APS5_VABI scePthreadAttrSetcreatesuspend(PthreadAttr* attr);
int APS5_VABI scePthreadAttrSetscope(PthreadAttr* attr, int scope);
int APS5_VABI scePthreadBarrierDestroy(PthreadBarrierPrivate** barrier);
int APS5_VABI scePthreadBarrierInit(PthreadBarrierPrivate** barrier, PthreadBarrierattrPrivate* const* attr, unsigned count, const char* name);
@@ -518,8 +518,8 @@ int APS5_VABI scePthreadMutexattrGetkind(void);
int APS5_VABI scePthreadMutexattrInitForInternalLibc(void);
int APS5_VABI scePthreadMutexattrSetgen(void);
int APS5_VABI scePthreadMutexattrSetkind(void);
int APS5_VABI scePthreadResume(void);
int APS5_VABI scePthreadResumeAll(void);
int APS5_VABI scePthreadResume(Pthread thread);
void APS5_VABI scePthreadResumeAll(void);
int APS5_VABI scePthreadSetBesteffort(void);
int APS5_VABI scePthreadSetconcurrency(int level);
int APS5_VABI scePthreadSingle(void);
+48
View File
@@ -153,6 +153,53 @@ static bool CheckWidePrecision() {
return correct;
}
static void CheckIntegerScanWidths() {
int value = 7;
*__error_nid_postfix() = 0;
Require(sscanf_nid_postfix("2147483648", "%d", &value) == 1 && value == -2147483647 - 1 && *__error_nid_postfix() == 0);
Require(sscanf_nid_postfix("0xFFFFFFFF", "%i", &value) == 1 && value == -1 && *__error_nid_postfix() == 0);
Require(sscanf_nid_postfix("99999999999999999999", "%d", &value) == 1 && value == -1 && *__error_nid_postfix() == 34);
unsigned unsignedValue = 7;
Require(sscanf_nid_postfix("4294967296", "%u", &unsignedValue) == 1 && unsignedValue == 0);
Require(sscanf_nid_postfix("100000001", "%x", &unsignedValue) == 1 && unsignedValue == 1);
Require(sscanf_nid_postfix("40000000001", "%o", &unsignedValue) == 1 && unsignedValue == 1);
short shortValue = 7;
Require(sscanf_nid_postfix("5000000000", "%hd", &shortValue) == 1 && shortValue == -3584);
signed char charValue = 7;
Require(sscanf_nid_postfix("300", "%hhd", &charValue) == 1 && charValue == 44);
Require(sscanf_nid_postfix("4294967295", "%u", &unsignedValue) == 1 && unsignedValue == 4294967295u);
unsigned short unsignedShort = 7;
Require(sscanf_nid_postfix("40000", "%hu", &unsignedShort) == 1 && unsignedShort == 40000);
Require(sscanf_nid_postfix("70000", "%hu", &unsignedShort) == 1 && unsignedShort == 4464);
Require(sscanf_nid_postfix("ffff", "%hx", &unsignedShort) == 1 && unsignedShort == 0xffff);
unsigned char unsignedChar = 7;
Require(sscanf_nid_postfix("200", "%hhu", &unsignedChar) == 1 && unsignedChar == 200);
Require(sscanf_nid_postfix("300", "%hhu", &unsignedChar) == 1 && unsignedChar == 44);
Require(sscanf_nid_postfix("377", "%hho", &unsignedChar) == 1 && unsignedChar == 255);
*__error_nid_postfix() = 0;
Require(sscanf_nid_postfix("-2147483649", "%d", &value) == 1 && value == 0x7fffffff && *__error_nid_postfix() == 0);
Require(sscanf_nid_postfix("-1", "%u", &unsignedValue) == 1 && unsignedValue == 0xffffffffu && *__error_nid_postfix() == 0);
Require(sscanf_nid_postfix("99999999999", "%x", &unsignedValue) == 1 && unsignedValue == 0x99999999u && *__error_nid_postfix() == 0);
Require(sscanf_nid_postfix("70000", "%hd", &shortValue) == 1 && shortValue == 0x1170 && *__error_nid_postfix() == 0);
long long quad = 0x5555555555555555ll;
Require(sscanf_nid_postfix("9223372036854775808", "%lld", &quad) == 1 && quad == 0x7fffffffffffffffll && *__error_nid_postfix() == 34);
unsigned long long unsignedQuad = 0x5555555555555555ull;
*__error_nid_postfix() = 0;
Require(sscanf_nid_postfix("18446744073709551616", "%llu", &unsignedQuad) == 1 && unsignedQuad == 0xffffffffffffffffull && *__error_nid_postfix() == 34);
#ifdef _WIN32
quad = 0x5555555555555555ll;
*__error_nid_postfix() = 0;
Require(sscanf_nid_postfix("5", "%qd", &quad) == 0 && quad == 0x5555555555555555ll && *__error_nid_postfix() == 0);
#endif
int first = 7;
int second = 7;
int position = 0;
Require(sscanf_nid_postfix("12 34", "%d %d%n", &first, &second, &position) == 2 && first == 12 && second == 34 && position == 5);
value = 7;
Require(sscanf_nid_postfix("x", "%d", &value) == 0 && value == 7);
Require(sscanf_nid_postfix("1 x", "%d %d", &first, &value) == 1 && first == 1 && value == 7);
}
static void CheckSecureFormat() {
constexpr size_t rsizeMax = SIZE_MAX >> 1;
char buffer[16];
@@ -388,6 +435,7 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() {
Require(ScanListS("123 test", "%d %s", &scanNum, scanWord, 8u) == 2 && scanNum == 123 && std::strcmp(scanWord, "test") == 0);
char tooSmall[4] = {'x', 'x', 'x', 'x'};
Require(ScanListS("abcdef", "%s", tooSmall, 4u) == 0 && tooSmall[0] == '\0');
CheckIntegerScanWidths();
std::puts("Formatting checks passed: 10000 iterations");
}
+323 -5
View File
@@ -825,7 +825,7 @@ void TestGetInfo(std::uint32_t context) {
Require(sceAjmBatchJobGetInfo(&info, instance, &at9) == 0);
Submit(context, info);
Require(initResult[0] == 0);
Require(at9.result == 0 && at9.internalResult == 0 && at9.numChannels == 2 && at9.channelMask == 0x3 && at9.sampleRate == 48000 && at9.sampleEncoding == 0 && at9.reserved == 0);
Require(at9.result == 0 && at9.internalResult == 0 && at9.numChannels == 2 && at9.channelMask == 0x3 && at9.sampleRate == 48000 && at9.sampleEncoding == 0 && at9.bitrate == 1024 * 8 * 48000 / (4 * 256) && at9.reserved == 0);
Require(sceAjmInstanceDestroy(context, instance) == 0);
Require(sceAjmInstanceCreate(context, 0, 0, &instance) == 0);
@@ -1051,7 +1051,7 @@ std::vector<std::int16_t> DecodeAt9Superframe(std::uint32_t context, const std::
Require(sceAjmBatchJobRun(&info, instance, sidebandStream | runMultipleFrames, superframe.data(), superframe.size(), pcm.data(), pcm.size() * sizeof(std::int16_t), &decoded, sizeof(decoded)) == 0);
Submit(context, info);
Require(initResult[0] == 0);
Require(decoded.stream.result == 0 && static_cast<std::size_t>(decoded.stream.inputConsumed) == superframe.size());
Require(decoded.stream.result == 0x18 && static_cast<std::size_t>(decoded.stream.inputConsumed) == superframe.size());
Require(static_cast<std::size_t>(decoded.stream.outputWritten) == pcm.size() * sizeof(std::int16_t) && decoded.stream.totalDecodedSamples == 1024 && decoded.frames == 4);
Require(sceAjmInstanceDestroy(context, instance) == 0);
return pcm;
@@ -1578,7 +1578,7 @@ ResampledStream Stream(std::uint32_t context, std::uint32_t instance, const std:
Require(sceAjmBatchJobDecode(&info, instance, stream + offset, size - offset, pcm.data(), pcm.size() * sizeof(std::int16_t), &sideband) == 0);
Submit(context, info);
if (ratio > 0) Require(setResult[0] == 0);
Require(sideband.result == 0 || (offset == size && sideband.outputWritten == 0));
Require(sideband.result == 0 || sideband.result == 0x8 || sideband.result == 0x10 || sideband.result == 0x18 || (offset == size && sideband.outputWritten == 0));
offset += static_cast<std::size_t>(sideband.inputConsumed);
if (offset < size && static_cast<std::size_t>(sideband.outputWritten) < pcm.size() * sizeof(std::int16_t)) ++out.shortJobs;
out.pcm.insert(out.pcm.end(), pcm.begin(), pcm.begin() + sideband.outputWritten / static_cast<std::int32_t>(sizeof(std::int16_t)));
@@ -1798,7 +1798,7 @@ At9Run RunAt9Job(std::uint32_t context, std::uint32_t instance, std::uint64_t fl
const std::size_t sidebandSize = (flags & runMultipleFrames) != 0 ? sizeof(decoded) : sizeof(decoded.stream);
Require(sceAjmBatchJobRun(&info, instance, sidebandStream | flags, input, inputSize, pcm.data(), pcm.size() * sizeof(std::int16_t), &decoded, sidebandSize) == 0);
Submit(context, info);
Require(decoded.stream.result == 0);
Require(decoded.stream.result == 0 || decoded.stream.result == 0x8 || decoded.stream.result == 0x18);
return decoded;
}
@@ -1832,7 +1832,318 @@ void TestAt9RunDecodesWholeInput(std::uint32_t context) {
RunControl(context, instance, CONTROL_START, &start, sizeof(start), result);
Require(result[0] == 0);
const auto run = RunAt9Job(context, instance, 0, stream.data(), stream.size());
Require(static_cast<std::size_t>(run.stream.inputConsumed) == stream.size() && run.stream.outputWritten == 1024 * 2);
Require(run.stream.result == 0x18 && static_cast<std::size_t>(run.stream.inputConsumed) == stream.size() && run.stream.outputWritten == 1024 * 2);
Require(sceAjmInstanceDestroy(context, instance) == 0);
}
void TestAt9FlagRules(std::uint32_t context) {
constexpr std::uint64_t runMultipleFrames = 1ull << 12;
constexpr std::uint64_t sidebandStream = 1ull << 47;
std::vector<std::uint8_t> stream(2048, 0);
std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME));
std::memcpy(stream.data() + 1024, AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME));
const At9Control start{{0, 0, 0}, {0xFE, 0x70, 0x1F, 0xF0}, 0};
const auto run = [&](std::size_t inputSize, std::size_t roomSamples, At9Run& decoded) {
std::uint32_t instance = 0;
Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0);
std::int32_t result[2] = {-1, -1};
RunControl(context, instance, CONTROL_START, &start, sizeof(start), result);
Require(result[0] == 0);
std::vector<std::uint8_t> batch(4096);
std::vector<std::int16_t> pcm(roomSamples + 1);
AjmBatchInfo info{};
Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0);
Require(sceAjmBatchJobRun(&info, instance, sidebandStream | runMultipleFrames, stream.data(), inputSize, pcm.data(), roomSamples * sizeof(std::int16_t), &decoded, sizeof(decoded)) == 0);
Submit(context, info);
Require(sceAjmInstanceDestroy(context, instance) == 0);
};
At9Run inputRanOut{{-1, -1, 0, 0, 0}, 0, 0};
run(1024, 2048, inputRanOut);
Require(inputRanOut.stream.result == 0x8 && inputRanOut.stream.inputConsumed == 1024 && inputRanOut.stream.outputWritten == 1024 * 2 && inputRanOut.frames == 4);
At9Run roomRanOut{{-1, -1, 0, 0, 0}, 0, 0};
run(2048, 1024, roomRanOut);
Require(roomRanOut.stream.result == 0x10 && roomRanOut.stream.inputConsumed == 1024 && roomRanOut.stream.outputWritten == 1024 * 2 && roomRanOut.frames == 4);
At9Run halfRoom{{-1, -1, 0, 0, 0}, 0, 0};
run(1024, 512, halfRoom);
Require(halfRoom.stream.result == 0x10 && halfRoom.stream.inputConsumed > 0 && halfRoom.stream.inputConsumed < 1024 && halfRoom.stream.outputWritten == 512 * 2 && halfRoom.frames == 2);
At9Run noRoom{{-1, -1, 0, 0, 0}, 0, 0};
run(1024, 0, noRoom);
Require(noRoom.stream.result == 0x10 && noRoom.stream.inputConsumed == 0 && noRoom.stream.outputWritten == 0 && noRoom.frames == 0);
At9Run both{{-1, -1, 0, 0, 0}, 0, 0};
run(1024, 1024, both);
Require(both.stream.result == 0x18 && both.stream.inputConsumed == 1024 && both.stream.outputWritten == 1024 * 2 && both.frames == 4);
}
int StartBuffer(std::uint32_t context, const void* batch, std::size_t size) {
std::uint32_t id = 0;
AjmBatchError error{};
const int started = sceAjmBatchStartBuffer(context, static_cast<const std::uint8_t*>(batch), static_cast<std::uint32_t>(size), 0, &error, &id);
if (started == 0) Require(sceAjmBatchWait(context, id, 0, &error) == 0);
return started;
}
std::uint32_t BatchWord(const std::vector<std::uint8_t>& batch, std::size_t offset) {
std::uint32_t word = 0;
std::memcpy(&word, batch.data() + offset, sizeof(word));
return word;
}
void TestConsoleBufferJobs(std::uint32_t context) {
constexpr std::uint64_t version = 1;
constexpr std::uint64_t runMultipleFrames = 1ull << 12;
constexpr std::uint64_t sidebandStream = 1ull << 47;
std::vector<std::uint8_t> stream(2048, 0);
std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME));
std::memcpy(stream.data() + 1024, AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME));
std::uint32_t instance = 0;
Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0);
const At9Control start{{0, 0, 0}, {0xFE, 0x70, 0x1F, 0xF0}, 0};
std::int32_t result[2] = {-1, -1};
std::vector<std::int16_t> pcm(2048, 0x55);
At9Run single{{-1, -1, 0, 0, 0}, 0, 0};
At9Run split{{-1, -1, 0, 0, 0}, 0, 0};
const AjmBuffer inputs[2] = {{stream.data() + 1024, 300}, {stream.data() + 1324, 724}};
const AjmBuffer outputs[2] = {{pcm.data() + 1024, 600 * 2}, {pcm.data() + 1624, 424 * 2}};
std::vector<std::uint8_t> batch(4096, 0xCD);
void* at = sceAjmBatchJobControlBufferRa(batch.data(), instance, version | CONTROL_START, &start, sizeof(start), result, sizeof(result), &context);
Require(at == batch.data() + 64);
Require(BatchWord(batch, 0) == instance << 6 && BatchWord(batch, 4) == 56);
Require(BatchWord(batch, 8) == 6 && BatchWord(batch, 24) == 2 && BatchWord(batch, 28) == sizeof(start));
Require(BatchWord(batch, 40) == (3u | (0x2000u << 6)) && BatchWord(batch, 44) == 0x6001 && BatchWord(batch, 48) == 18);
at = sceAjmBatchJobRunBufferRa(at, instance, version | sidebandStream | runMultipleFrames, stream.data(), 1024, pcm.data(), 1024 * 2, &single, sizeof(single), nullptr);
Require(at == batch.data() + 128);
Require(BatchWord(batch, 68) == 56 && BatchWord(batch, 72) == 1 && BatchWord(batch, 88) == (4u | (0x8000u << 6)) && BatchWord(batch, 96) == 17 && BatchWord(batch, 112) == 18);
at = sceAjmBatchJobRunSplitBufferRa(at, instance, version | sidebandStream | runMultipleFrames, inputs, 2, outputs, 2, &split, sizeof(split), nullptr);
Require(at == batch.data() + 224 && BatchWord(batch, 132) == 88);
Require(StartBuffer(context, batch.data(), static_cast<std::uint8_t*>(at) - batch.data()) == 0);
Require(result[0] == 0 && result[1] == 0);
constexpr std::int32_t exactFit = 0x18;
Require(single.stream.result == exactFit && single.stream.inputConsumed == 1024 && single.stream.outputWritten == 1024 * 2 && single.frames == 4 && single.stream.totalDecodedSamples == 1024);
Require(split.stream.result == exactFit && split.stream.inputConsumed == 1024 && split.stream.outputWritten == 1024 * 2 && split.frames == 4 && split.stream.totalDecodedSamples == 2048);
Require(std::all_of(pcm.begin(), pcm.end(), [](std::int16_t sample) { return sample == 0; }));
Require(sceAjmInstanceDestroy(context, instance) == 0);
}
class ConsoleBatch {
public:
void Word(std::uint32_t word) { words.push_back(word); }
void Chunk(std::uint32_t kind, std::uint32_t payload, std::uint32_t word) {
Word(kind | (payload << 6));
Word(word);
}
void Buffer(std::uint32_t kind, const void* address, std::size_t size) {
Chunk(kind, 0, static_cast<std::uint32_t>(size));
const auto value = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(address));
Word(static_cast<std::uint32_t>(value));
Word(static_cast<std::uint32_t>(value >> 32));
}
const std::uint32_t* Data() const { return words.data(); }
std::size_t Bytes() const { return words.size() * sizeof(std::uint32_t); }
private:
std::vector<std::uint32_t> words;
};
void TestConsoleHandWrittenJobs(std::uint32_t context) {
std::vector<std::uint8_t> stream(1024, 0);
std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME));
std::uint32_t instance = 0;
Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0);
std::int32_t result[2] = {-1, -1};
std::int32_t statistics[6] = {-1, -1, -1, -1, -1, -1};
std::vector<std::int16_t> pcm(1024, 0x55);
At9Run decoded{{-1, -1, 0, 0, 0}, 0, 0};
const std::uint8_t initialize[8] = {0xFE, 0x70, 0x1F, 0xF0, 0, 0, 0, 0};
std::uint32_t inlined[2];
std::memcpy(inlined, initialize, sizeof(initialize));
ConsoleBatch batch;
batch.Chunk(7, 0, sizeof(initialize));
batch.Word(inlined[0]);
batch.Word(inlined[1]);
batch.Chunk(0, instance, 40);
batch.Buffer(2, nullptr, sizeof(initialize));
batch.Chunk(3, 0, 0x6001);
batch.Buffer(18, result, sizeof(result));
batch.Chunk(0, instance, 56);
batch.Buffer(1, stream.data(), stream.size());
batch.Chunk(4, 0x8000, 0x1001);
batch.Buffer(17, pcm.data(), pcm.size() * sizeof(std::int16_t));
batch.Buffer(18, &decoded, sizeof(decoded));
batch.Chunk(0, 0x80000, 24);
batch.Chunk(3, 0, 0x8007);
batch.Buffer(18, statistics, sizeof(statistics));
std::vector<std::uint32_t> words(batch.Data(), batch.Data() + batch.Bytes() / sizeof(std::uint32_t));
const auto inlineAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(words.data() + 2));
words[8] = static_cast<std::uint32_t>(inlineAddress);
words[9] = static_cast<std::uint32_t>(inlineAddress >> 32);
Require(StartBuffer(context, words.data(), words.size() * sizeof(std::uint32_t)) == 0);
Require(result[0] == 0 && result[1] == 0 && statistics[0] == 0 && statistics[1] == 0);
Require(decoded.stream.result == 0x18 && decoded.stream.inputConsumed == 1024 && decoded.stream.outputWritten == 1024 * 2 && decoded.frames == 4);
Require(std::all_of(pcm.begin(), pcm.end(), [](std::int16_t sample) { return sample == 0; }));
constexpr int malformedBatch = static_cast<int>(0x80930011);
Require(StartBuffer(context, words.data(), 12) == malformedBatch);
ConsoleBatch flagless;
flagless.Chunk(0, instance, 16);
flagless.Buffer(18, result, sizeof(result));
Require(StartBuffer(context, flagless.Data(), flagless.Bytes()) == malformedBatch);
ConsoleBatch overrun;
overrun.Chunk(0, instance, 32);
overrun.Chunk(3, 0, 0x2001);
overrun.Buffer(18, result, sizeof(result));
Require(StartBuffer(context, overrun.Data(), overrun.Bytes()) == malformedBatch);
ConsoleBatch unknown;
unknown.Chunk(0, instance, 16);
unknown.Chunk(3, 0, 0x2001);
unknown.Chunk(5, 0, 0);
bool unknownThrows = false;
try {
static_cast<void>(StartBuffer(context, unknown.Data(), unknown.Bytes()));
} catch (const std::runtime_error&) {
unknownThrows = true;
}
Require(unknownThrows);
Require(sceAjmInstanceDestroy(context, instance) == 0);
}
int StartBufferRefused(std::uint32_t context, const void* batch, std::size_t size, AjmBatchError& error) {
std::uint32_t id = 0;
error = AjmBatchError{};
return sceAjmBatchStartBuffer(context, static_cast<const std::uint8_t*>(batch), static_cast<std::uint32_t>(size), 0, &error, &id);
}
void TestConsoleBufferEdges(std::uint32_t context) {
constexpr int malformedBatch = static_cast<int>(0x80930011);
constexpr int invalidAddress = static_cast<int>(0x80930016);
std::vector<std::uint8_t> stream(1024, 0);
std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME));
const std::uint8_t initialize[8] = {0xFE, 0x70, 0x1F, 0xF0, 0, 0, 0, 0};
std::uint32_t instance = 0;
Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0);
std::vector<std::int16_t> pcm(1024, 0x55);
std::int32_t result[2] = {-1, -1};
const auto control = [&](ConsoleBatch& batch) {
result[0] = result[1] = -1;
batch.Chunk(0, instance, 40);
batch.Buffer(2, initialize, sizeof(initialize));
batch.Chunk(3, 0, 0x6001);
batch.Buffer(18, result, sizeof(result));
};
At9Run dropped{{-1, -1, 0, 0, 0}, 0, 0};
ConsoleBatch zeroSizes;
control(zeroSizes);
zeroSizes.Chunk(0, instance, 104);
zeroSizes.Buffer(1, stream.data(), 0);
zeroSizes.Buffer(1, stream.data(), stream.size());
zeroSizes.Chunk(4, 0x8000, 0x1001);
zeroSizes.Buffer(17, pcm.data(), 0);
zeroSizes.Buffer(17, pcm.data(), pcm.size() * sizeof(std::int16_t));
zeroSizes.Buffer(17, nullptr, 0);
zeroSizes.Buffer(18, &dropped, sizeof(dropped));
Require(StartBuffer(context, zeroSizes.Data(), zeroSizes.Bytes()) == 0);
Require(result[0] == 0 && dropped.stream.result == 0x18 && dropped.stream.inputConsumed == 1024 && dropped.stream.outputWritten == 1024 * 2 && dropped.frames == 4);
Require(std::all_of(pcm.begin(), pcm.end(), [](std::int16_t sample) { return sample == 0; }));
At9Run noRoom{{-1, -1, 0, 0, 0}, 0, 0};
std::fill(pcm.begin(), pcm.end(), std::int16_t{0x55});
ConsoleBatch zeroRoom;
control(zeroRoom);
zeroRoom.Chunk(0, instance, 56);
zeroRoom.Buffer(1, stream.data(), stream.size());
zeroRoom.Chunk(4, 0x8000, 0x1001);
zeroRoom.Buffer(17, pcm.data(), 0);
zeroRoom.Buffer(18, &noRoom, sizeof(noRoom));
Require(StartBuffer(context, zeroRoom.Data(), zeroRoom.Bytes()) == 0);
Require(result[0] == 0 && noRoom.stream.result == 0x10 && noRoom.stream.inputConsumed == 0 && noRoom.stream.outputWritten == 0 && noRoom.frames == 0);
Require(std::all_of(pcm.begin(), pcm.end(), [](std::int16_t sample) { return sample == 0x55; }));
const void* returnAddress = &stream;
for (const bool nullInput : {true, false}) {
At9Run untouched{{-1, -1, 0, 0, 0}, 0, 0};
ConsoleBatch refused;
control(refused);
refused.Chunk(0, instance, 72);
refused.Buffer(6, returnAddress, 0);
refused.Buffer(1, nullInput ? nullptr : stream.data(), stream.size());
refused.Chunk(4, 0x8000, 0x1001);
refused.Buffer(17, nullInput ? pcm.data() : nullptr, pcm.size() * sizeof(std::int16_t));
refused.Buffer(18, &untouched, sizeof(untouched));
AjmBatchError error{};
Require(StartBufferRefused(context, refused.Data(), refused.Bytes(), error) == malformedBatch);
Require(error.error_code == invalidAddress && error.job_addr == reinterpret_cast<const std::uint8_t*>(refused.Data()) + 48);
Require(error.cmd_offset == (nullInput ? 24u : 48u) && error.job_ra == returnAddress);
Require(result[0] == -1 && untouched.stream.result == -1);
}
Require(sceAjmInstanceDestroy(context, instance) == 0);
}
void TestConsoleEmptyBatch(std::uint32_t context) {
constexpr int invalidParameter = static_cast<int>(0x80930005);
constexpr int invalidBatch = static_cast<int>(0x80930004);
std::uint32_t id = 0;
AjmBatchError error{};
Require(sceAjmBatchStartBuffer(context, nullptr, 0, 0, &error, &id) == invalidParameter);
Require(sceAjmBatchStartBuffer(context, nullptr, 16, 0, &error, &id) == invalidParameter);
const std::uint64_t words[2] = {0, 0};
Require(sceAjmBatchStartBuffer(context, reinterpret_cast<const std::uint8_t*>(words), 0, 0, &error, nullptr) == invalidParameter);
error.error_code = -1;
Require(sceAjmBatchStartBuffer(context, reinterpret_cast<const std::uint8_t*>(words), 0, 0, &error, &id) == 0);
Require(error.error_code == 0 && id != 0 && sceAjmBatchWait(context, id, 0, &error) == 0);
Require(sceAjmBatchWait(context, id, 0, &error) == invalidBatch);
}
void TestConsoleSingleFrameRun(std::uint32_t context) {
constexpr std::uint64_t version = 1;
constexpr std::uint64_t sidebandStream = 1ull << 47;
std::vector<std::uint8_t> stream(1024, 0);
std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME));
std::uint32_t instance = 0;
Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0);
const At9Control start{{0, 0, 0}, {0xFE, 0x70, 0x1F, 0xF0}, 0};
std::int32_t result[2] = {-1, -1};
std::vector<std::int16_t> pcm(1024, 0x55);
DecodeSideband single{-1, -1, 0, 0, 0};
std::vector<std::uint8_t> batch(4096, 0xCD);
void* at = sceAjmBatchJobControlBufferRa(batch.data(), instance, version | CONTROL_START, &start, sizeof(start), result, sizeof(result), nullptr);
at = sceAjmBatchJobRunBufferRa(at, instance, version | sidebandStream, stream.data(), stream.size(), pcm.data(), pcm.size() * sizeof(std::int16_t), &single, sizeof(single), nullptr);
Require(StartBuffer(context, batch.data(), static_cast<std::uint8_t*>(at) - batch.data()) == 0);
Require(result[0] == 0 && single.result == 0 && single.inputConsumed > 0 && single.inputConsumed < 1024);
Require(single.outputWritten == 256 * 2 && single.totalDecodedSamples == 256);
Require(std::all_of(pcm.begin(), pcm.begin() + 256, [](std::int16_t sample) { return sample == 0; }));
Require(std::all_of(pcm.begin() + 256, pcm.end(), [](std::int16_t sample) { return sample == 0x55; }));
Require(sceAjmInstanceDestroy(context, instance) == 0);
}
void TestConsoleStaleHeaderBits(std::uint32_t context) {
std::vector<std::uint8_t> stream(1024, 0);
std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME));
std::uint32_t instance = 0;
Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0);
std::int32_t result[2] = {-1, -1};
std::vector<std::int16_t> pcm(1024, 0x55);
At9Run decoded{{-1, -1, 0, 0, 0}, 0, 0};
const std::uint8_t initialize[8] = {0xFE, 0x70, 0x1F, 0xF0, 0, 0, 0, 0};
ConsoleBatch batch;
batch.Chunk(0x8C000030u, instance, 56);
batch.Buffer(0xFFFFFFF6u, &context, 0);
batch.Buffer(0xFFFFFFE2u, initialize, sizeof(initialize));
batch.Chunk(0x04000033u, 0, 0x6001);
batch.Buffer(0xFFFFFFF2u, result, sizeof(result));
batch.Chunk(0x8C000020u, instance, 72);
batch.Buffer(0xFFFFFFF6u, &context, 0);
batch.Buffer(0xFFFFFFE1u, stream.data(), stream.size());
batch.Chunk(0x04000034u, 0x8000, 0x1001);
batch.Buffer(0xFFFFFFF1u, pcm.data(), pcm.size() * sizeof(std::int16_t));
batch.Buffer(0xFFFFFFF2u, &decoded, sizeof(decoded));
Require(StartBuffer(context, batch.Data(), batch.Bytes()) == 0);
Require(result[0] == 0 && result[1] == 0);
Require(decoded.stream.result == 0x18 && decoded.stream.inputConsumed == 1024 && decoded.stream.outputWritten == 1024 * 2 && decoded.frames == 4);
Require(std::all_of(pcm.begin(), pcm.end(), [](std::int16_t sample) { return sample == 0; }));
Require(sceAjmInstanceDestroy(context, instance) == 0);
}
@@ -2088,6 +2399,13 @@ int main() {
TestResampleAt9(context);
TestAt9GaplessSegments(context);
TestAt9RunDecodesWholeInput(context);
TestAt9FlagRules(context);
TestPcmResample(context);
TestConsoleBufferJobs(context);
TestConsoleHandWrittenJobs(context);
TestConsoleBufferEdges(context);
TestConsoleEmptyBatch(context);
TestConsoleSingleFrameRun(context);
TestConsoleStaleHeaderBits(context);
Require(sceAjmFinalize(context) == 0);
}
+1 -1
View File
@@ -98,7 +98,7 @@ std::vector<T> Decode(std::uint32_t context, unsigned channels, unsigned encodin
Require((stream.result & 2) != 0 && stream.consumed == 0 && stream.produced == 0, "invalid first-frame flag");
}
const auto stream = run(packet, packet.size());
Require(stream.result == 0 && stream.internalResult == 0, "decode vibration superframe");
Require(stream.result == 0x18 && stream.internalResult == 0, "decode vibration superframe");
Require(stream.consumed == 128 && stream.produced == static_cast<int>(pcm.size() * sizeof(T)), "consumed/produced accounting");
Require(stream.samples == static_cast<unsigned>(samples) && stream.frames == 4, "decoded sample/frame accounting");
Require(sceAjmInstanceDestroy(context, instance) == 0, "destroy instance");
+3
View File
@@ -806,6 +806,8 @@ void TestPlayback() {
Check(sceAvPlayerPostInit(player, &post) == 0, "post init failed");
Check(sceAvPlayerPostInit(player, nullptr) == InvalidParams, "post init accepted null data");
Check(sceAvPlayerStreamCount(player) == OperationFailed, "stream count without a source");
Check(sceAvPlayerSetTrickSpeed(player, 100) == OperationFailed, "trick speed accepted without a source");
Check(sceAvPlayerSetTrickSpeed(player, 0) == OperationFailed, "trick speed checked the speed before the source");
Check(!sceAvPlayerIsActive(player), "active without a source");
Check(sceAvPlayerAddSource(player, "/app0/missing.mp4") == OperationFailed, "missing file accepted");
Check(sceAvPlayerAddSource(player, "/app0/missing/../avplayer.mp4") == OperationFailed, "missing parent accepted");
@@ -1041,6 +1043,7 @@ void TestExtendedExports() {
Check(sceAvPlayerSetAvailableBandwidth(player, 4000, 1000, 8000) == 0, "bandwidth rejected");
Check(sceAvPlayerSetAvailableBandwidth(nullptr, 4000, 1000, 8000) == InvalidParams, "bandwidth accepted for a null player");
Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source failed");
Check(sceAvPlayerSetAvailableBandwidth(player, 4000, 1000, 8000) == OperationFailed, "bandwidth accepted after a source");
Check(WaitFor([&] { return events.Seen(EventReady); }), "ready event missing");
CheckStreamInfoEx(player);
Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "enable video failed");
@@ -0,0 +1,276 @@
#include "prx/libkernel/include/Api.hpp"
#include "prx/libkernel_sys/include/Api.hpp"
#include "SceTypes.hpp"
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <stdexcept>
#include <string>
#include <thread>
static constexpr int SCE_OK = 0;
static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast<int>(0x80020016u);
static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast<int>(0x80020003u);
static constexpr int GUEST_ESRCH = 3;
static constexpr int GUEST_EINVAL = 22;
static constexpr int GUEST_SIGUSR1 = 30;
static constexpr int GUEST_CREATE_DETACHED = 1;
static constexpr int DETACHED_ROUNDS = 200;
static constexpr auto SUSPENDED_WINDOW = std::chrono::milliseconds(50);
static void* const CANCELED = reinterpret_cast<void*>(std::uintptr_t{1});
static void* const FINISHED = reinterpret_cast<void*>(std::uintptr_t{7});
static void Require(bool condition, const char* message) {
if (!condition) {
std::fprintf(stderr, "%s\n", message);
std::abort();
}
}
struct Started {
std::atomic<bool> ran{false};
std::atomic<bool> returned{false};
};
static std::atomic<bool> release{false};
static std::atomic<bool> stopResuming{false};
static std::atomic<int> raised{0};
static std::atomic<Pthread> raisedOn{nullptr};
static void* APS5_VABI Record(void* opaque) {
static_cast<Started*>(opaque)->ran.store(true);
return FINISHED;
}
static void* APS5_VABI CancelAfterResume(void* opaque) {
auto* started = static_cast<Started*>(opaque);
started->ran.store(true);
scePthreadTestcancel();
started->returned.store(true);
return FINISHED;
}
static void* APS5_VABI CancelInResumeAll(void* opaque) {
auto* started = static_cast<Started*>(opaque);
started->ran.store(true);
Require(scePthreadCancel(scePthreadSelf()) == SCE_OK, "scePthreadCancel of the calling thread failed");
pthread_resume_all_np_nid_postfix();
started->returned.store(true);
return FINISHED;
}
static void* APS5_VABI ResumeAllUntilStopped(void*) {
while (!stopResuming.load()) pthread_resume_all_np_nid_postfix();
return FINISHED;
}
static void* APS5_VABI WaitForRelease(void*) {
while (!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1));
return FINISHED;
}
static void APS5_VABI Handler(int, void*) {
raisedOn.store(scePthreadSelf());
raised.fetch_add(1);
}
static void CreateSuspendedPosix(Pthread* thread, PthreadEntry entry, void* arg) {
PthreadAttr attr = nullptr;
Require(pthread_attr_init_nid_postfix(&attr) == 0, "pthread_attr_init failed");
Require(pthread_attr_setcreatesuspend_np_nid_postfix(&attr) == 0, "pthread_attr_setcreatesuspend_np failed");
Require(pthread_create_nid_postfix(thread, &attr, entry, arg) == 0, "pthread_create of a suspended thread failed");
Require(pthread_attr_destroy_nid_postfix(&attr) == 0, "pthread_attr_destroy failed");
}
static void CreateSuspendedSce(Pthread* thread, PthreadEntry entry, void* arg) {
PthreadAttr attr = nullptr;
Require(scePthreadAttrInit(&attr) == SCE_OK, "scePthreadAttrInit failed");
Require(scePthreadAttrSetcreatesuspend(&attr) == SCE_OK, "scePthreadAttrSetcreatesuspend failed");
Require(scePthreadCreate(thread, &attr, entry, arg, "suspended") == SCE_OK, "scePthreadCreate of a suspended thread failed");
Require(scePthreadAttrDestroy(&attr) == SCE_OK, "scePthreadAttrDestroy failed");
}
static void ReportsErrors() {
Require(pthread_attr_setcreatesuspend_np_nid_postfix(nullptr) == GUEST_EINVAL, "pthread_attr_setcreatesuspend_np accepted a null attribute");
Require(scePthreadAttrSetcreatesuspend(nullptr) == SCE_KERNEL_ERROR_EINVAL, "scePthreadAttrSetcreatesuspend accepted a null attribute");
PthreadAttr destroyed = nullptr;
Require(scePthreadAttrInit(&destroyed) == SCE_OK, "scePthreadAttrInit failed");
Require(scePthreadAttrDestroy(&destroyed) == SCE_OK, "scePthreadAttrDestroy failed");
Require(pthread_attr_setcreatesuspend_np_nid_postfix(&destroyed) == GUEST_EINVAL, "pthread_attr_setcreatesuspend_np accepted a destroyed attribute");
Require(scePthreadAttrSetcreatesuspend(&destroyed) == SCE_KERNEL_ERROR_EINVAL, "scePthreadAttrSetcreatesuspend accepted a destroyed attribute");
Require(pthread_resume_np_nid_postfix(nullptr) == GUEST_EINVAL, "pthread_resume_np accepted a null thread");
Require(scePthreadResume(nullptr) == SCE_KERNEL_ERROR_EINVAL, "scePthreadResume accepted a null thread");
Pthread joined = nullptr;
Started started;
Require(scePthreadCreate(&joined, nullptr, Record, &started, "joined") == SCE_OK, "scePthreadCreate failed");
Require(scePthreadJoin(joined, nullptr) == SCE_OK, "scePthreadJoin failed");
Require(pthread_resume_np_nid_postfix(joined) == GUEST_ESRCH, "pthread_resume_np found a joined thread");
Require(scePthreadResume(joined) == SCE_KERNEL_ERROR_ESRCH, "scePthreadResume found a joined thread");
}
static void ResumesOneThread(bool sce) {
Started started;
Pthread thread = nullptr;
if (sce) CreateSuspendedSce(&thread, Record, &started);
else CreateSuspendedPosix(&thread, Record, &started);
std::this_thread::sleep_for(SUSPENDED_WINDOW);
Require(!started.ran.load(), "a thread created suspended ran before it was resumed");
Require((sce ? scePthreadResume(thread) : pthread_resume_np_nid_postfix(thread)) == 0, "resuming a suspended thread failed");
void* result = nullptr;
Require((sce ? scePthreadJoin(thread, &result) : pthread_join_nid_postfix(thread, &result)) == 0, "joining a resumed thread failed");
Require(result == FINISHED && started.ran.load(), "a resumed thread did not run its start routine");
}
static void ResumesRunningAndCallingThreads() {
Require(pthread_resume_np_nid_postfix(scePthreadSelf()) == 0, "pthread_resume_np of the calling thread failed");
release.store(false);
Pthread running = nullptr;
Require(scePthreadCreate(&running, nullptr, WaitForRelease, nullptr, "running") == SCE_OK, "scePthreadCreate failed");
Require(pthread_resume_np_nid_postfix(running) == 0, "pthread_resume_np of a running thread failed");
release.store(true);
void* result = nullptr;
Require(scePthreadJoin(running, &result) == SCE_OK && result == FINISHED, "joining a running thread failed");
}
static void ResumesAllThreads(void (*resumeAll)()) {
Started first;
Started second;
Pthread firstThread = nullptr;
Pthread secondThread = nullptr;
CreateSuspendedPosix(&firstThread, Record, &first);
CreateSuspendedSce(&secondThread, Record, &second);
std::this_thread::sleep_for(SUSPENDED_WINDOW);
Require(!first.ran.load() && !second.ran.load(), "a thread created suspended ran before it was resumed");
resumeAll();
void* result = nullptr;
Require(scePthreadJoin(firstThread, &result) == SCE_OK && result == FINISHED && first.ran.load(), "resuming all threads left the first one suspended");
Require(scePthreadJoin(secondThread, &result) == SCE_OK && result == FINISHED && second.ran.load(), "resuming all threads left the second one suspended");
}
static void CancelsBeforeResume() {
Started started;
Pthread thread = nullptr;
CreateSuspendedSce(&thread, CancelAfterResume, &started);
Require(scePthreadCancel(thread) == SCE_OK, "scePthreadCancel of a suspended thread failed");
Require(pthread_resume_np_nid_postfix(thread) == 0, "pthread_resume_np of a canceled thread failed");
void* result = nullptr;
Require(scePthreadJoin(thread, &result) == SCE_OK, "joining a canceled thread failed");
Require(result == CANCELED && started.ran.load() && !started.returned.load(), "a cancel requested while suspended did not take effect at the first cancellation point");
}
static void CancelsInResumeAll() {
Started started;
Pthread thread = nullptr;
Require(scePthreadCreate(&thread, nullptr, CancelInResumeAll, &started, "resume_all") == SCE_OK, "scePthreadCreate failed");
void* result = nullptr;
Require(scePthreadJoin(thread, &result) == SCE_OK, "joining a canceled thread failed");
Require(result == CANCELED && started.ran.load() && !started.returned.load(), "pthread_resume_all_np did not act on a pending cancel");
}
static void ReportsCreateSuspendedInAttrGet(int (APS5_VABI* get)(Pthread, PthreadAttr*)) {
Started first;
Started second;
Pthread suspended = nullptr;
CreateSuspendedPosix(&suspended, Record, &first);
PthreadAttr attr = nullptr;
Require(pthread_attr_init_nid_postfix(&attr) == 0, "pthread_attr_init failed");
Require(get(suspended, &attr) == 0, "getting the attributes of a suspended thread failed");
Pthread copy = nullptr;
Require(pthread_create_nid_postfix(&copy, &attr, Record, &second) == 0, "pthread_create with the attributes of a suspended thread failed");
std::this_thread::sleep_for(SUSPENDED_WINDOW);
Require(!second.ran.load(), "a thread created with the attributes of a suspended thread ran before it was resumed");
Require(pthread_resume_np_nid_postfix(copy) == 0 && pthread_resume_np_nid_postfix(suspended) == 0, "resuming a suspended thread failed");
void* result = nullptr;
Require(pthread_join_nid_postfix(copy, &result) == 0 && result == FINISHED && second.ran.load(), "joining a resumed thread failed");
Require(pthread_join_nid_postfix(suspended, &result) == 0 && result == FINISHED && first.ran.load(), "joining a resumed thread failed");
release.store(false);
Pthread running = nullptr;
Require(scePthreadCreate(&running, nullptr, WaitForRelease, nullptr, "running") == SCE_OK, "scePthreadCreate failed");
Require(pthread_attr_setcreatesuspend_np_nid_postfix(&attr) == 0, "pthread_attr_setcreatesuspend_np failed");
Require(get(running, &attr) == 0, "getting the attributes of a running thread failed");
Started third;
Pthread plain = nullptr;
Require(pthread_create_nid_postfix(&plain, &attr, Record, &third) == 0, "pthread_create with the attributes of a running thread failed");
Require(pthread_join_nid_postfix(plain, &result) == 0 && result == FINISHED && third.ran.load(), "a thread created with the attributes of a running thread did not run");
release.store(true);
Require(scePthreadJoin(running, nullptr) == SCE_OK, "joining a running thread failed");
Require(pthread_attr_destroy_nid_postfix(&attr) == 0, "pthread_attr_destroy failed");
}
static void ResumesDetachedThreadsWhileTheyExit() {
stopResuming.store(false);
Pthread resumer = nullptr;
Require(scePthreadCreate(&resumer, nullptr, ResumeAllUntilStopped, nullptr, "resumer") == SCE_OK, "scePthreadCreate failed");
for (int round = 0; round < DETACHED_ROUNDS; ++round) {
Started started;
PthreadAttr attr = nullptr;
Require(pthread_attr_init_nid_postfix(&attr) == 0, "pthread_attr_init failed");
Require(pthread_attr_setcreatesuspend_np_nid_postfix(&attr) == 0, "pthread_attr_setcreatesuspend_np failed");
Require(pthread_attr_setdetachstate_nid_postfix(&attr, GUEST_CREATE_DETACHED) == 0, "pthread_attr_setdetachstate failed");
Pthread thread = nullptr;
Require(pthread_create_nid_postfix(&thread, &attr, Record, &started) == 0, "pthread_create of a detached suspended thread failed");
Require(pthread_attr_destroy_nid_postfix(&attr) == 0, "pthread_attr_destroy failed");
int result = 0;
while ((result = pthread_resume_np_nid_postfix(thread)) == 0) std::this_thread::yield();
Require(result == GUEST_ESRCH && started.ran.load(), "a detached thread resumed while it exited did not run its start routine");
}
stopResuming.store(true);
Require(scePthreadJoin(resumer, nullptr) == SCE_OK, "joining the resuming thread failed");
}
#ifdef _WIN32
static void RefusesRaiseWhileSuspended() {
Require(sceKernelInstallExceptionHandler(GUEST_SIGUSR1, reinterpret_cast<void*>(&Handler)) == SCE_OK, "sceKernelInstallExceptionHandler failed");
Started started;
Pthread thread = nullptr;
CreateSuspendedSce(&thread, Record, &started);
bool refused = false;
try {
sceKernelRaiseException(thread, GUEST_SIGUSR1);
} catch (const std::runtime_error& error) {
refused = std::string(error.what()).find("suspended") != std::string::npos;
}
Require(refused, "sceKernelRaiseException did not refuse a thread still suspended at creation");
Require(pthread_resume_np_nid_postfix(thread) == 0, "pthread_resume_np failed");
Require(scePthreadJoin(thread, nullptr) == SCE_OK && started.ran.load(), "joining a resumed thread failed");
Require(raised.load() == 0, "the handler ran for a refused raise");
Require(sceKernelRemoveExceptionHandler(GUEST_SIGUSR1) == SCE_OK, "sceKernelRemoveExceptionHandler failed");
}
#else
static void DefersRaiseUntilResume() {
Require(sceKernelInstallExceptionHandler(GUEST_SIGUSR1, reinterpret_cast<void*>(&Handler)) == SCE_OK, "sceKernelInstallExceptionHandler failed");
Started started;
Pthread thread = nullptr;
CreateSuspendedSce(&thread, Record, &started);
Require(sceKernelRaiseException(thread, GUEST_SIGUSR1) == SCE_OK, "sceKernelRaiseException on a suspended thread failed");
std::this_thread::sleep_for(SUSPENDED_WINDOW);
Require(raised.load() == 0, "the handler ran on a thread still suspended at creation");
Require(pthread_resume_np_nid_postfix(thread) == 0, "pthread_resume_np failed");
Require(scePthreadJoin(thread, nullptr) == SCE_OK && started.ran.load(), "joining a resumed thread failed");
Require(raised.load() == 1 && raisedOn.load() == thread, "the handler did not run on the thread once it was resumed");
Require(sceKernelRemoveExceptionHandler(GUEST_SIGUSR1) == SCE_OK, "sceKernelRemoveExceptionHandler failed");
}
#endif
int main() {
ReportsErrors();
ResumesOneThread(false);
ResumesOneThread(true);
ResumesRunningAndCallingThreads();
ResumesAllThreads([] { Require(pthread_multi_np_nid_postfix() == 0, "pthread_multi_np failed"); });
ResumesAllThreads([] { pthread_resume_all_np_nid_postfix(); });
ResumesAllThreads([] { Require(scePthreadMulti() == SCE_OK, "scePthreadMulti failed"); });
ResumesAllThreads([] { scePthreadResumeAll(); });
CancelsBeforeResume();
CancelsInResumeAll();
ReportsCreateSuspendedInAttrGet(pthread_attr_get_np_nid_postfix);
ReportsCreateSuspendedInAttrGet(scePthreadAttrGet);
ResumesDetachedThreadsWhileTheyExit();
#ifdef _WIN32
RefusesRaiseWhileSuspended();
#else
DefersRaiseUntilResume();
#endif
}
@@ -0,0 +1,86 @@
import os
import shutil
import struct
import subprocess
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / 'relinker' / 'relinker' / 'tests'))
from guest_import_fixtures import executable, fixture_directory
def call(code, slot):
code.extend(b'\xff\x15' + struct.pack('<i', slot - (0x4000 + len(code) + 6)))
def fixture(variant, label):
image = executable(variant, 'PkS44IGrDkM#A#B', extra_symbols=('OxhIB8LB-PQ#A#B',))
code = bytearray(b'\x48\x83\xec\x38')
if label == 'join':
code.extend(b'\x48\x8d\x3c\x24\x31\xf6')
code.extend(b'\x48\x8d\x15' + struct.pack('<i', 0x4300 - (0x4000 + len(code) + 7)))
code.extend(b'\x48\xb9' + struct.pack('<Q', 0x123456789abcdef))
call(code, 0x4a28)
code.extend(b'\x85\xc0\x0f\x85')
create_failure = len(code)
code.extend(bytes(4))
code.extend(b'\x48\x8b\x3c\x24\x48\x8d\x74\x24\x08')
code.extend(b'\x48\xb8' + struct.pack('<Q', 0x7fffffffffffffff))
code.extend(b'\x48\x89\x44\x24\x10')
code.extend(b'\x48\xc7\x44\x24\x18' + struct.pack('<I', 999999999))
code.extend(b'\x48\x8d\x54\x24\x10')
call(code, 0x4a20)
code.extend(b'\x85\xc0\x0f\x85')
join_failure = len(code)
code.extend(bytes(4))
code.extend(b'\x48\xba' + struct.pack('<Q', 0x123456789abcdef))
code.extend(b'\x48\x39\x54\x24\x08\x0f\x85')
value_failure = len(code)
code.extend(bytes(4))
code.extend(b'\x31\xc0\x48\x83\xc4\x38\xc3')
failure = len(code)
code.extend(b'\xb8\x63\x00\x00\x00\x48\x83\xc4\x38\xc3')
for offset in (create_failure, join_failure, value_failure):
struct.pack_into('<i', code, offset, failure - offset - 4)
image[0x4300:0x4304] = b'\x48\x89\xf8\xc3'
else:
thread = 1 if label == 'invalid-deadline' else 0
code.extend(b'\xbf' + struct.pack('<I', thread) + b'\x31\xf6\x31\xd2')
if label != 'null-deadline':
code.extend(b'\x48\xc7\x44\x24\x10' + struct.pack('<i', -1 if thread else 0))
code.extend(b'\x48\xc7\x44\x24\x18\x00\x00\x00\x00')
code.extend(b'\x48\x8d\x54\x24\x10')
call(code, 0x4a20)
code.extend(b'\x48\x83\xc4\x38\xc3')
image[0x4000:0x4000 + len(code)] = code
return image
def main():
assert os.name == 'nt'
relinker, libraries = (Path(value).resolve() for value in sys.argv[1:3])
variant = sys.argv[3]
runtime = Path(sys.argv[4]).resolve()
assert variant in ('libkernel_sys.prx', 'libkernel_web.prx')
with fixture_directory('anyps5-timed-join-') as directory:
work = Path(directory)
(work / 'libs').mkdir()
(work / 'prx').mkdir()
for name in (variant, 'libkernel.prx', 'libc.prx'):
shutil.copy2(libraries / name, work / 'libs' / name)
for name in ('libgcc_s_seh-1.dll', 'libstdc++-6.dll', 'libwinpthread-1.dll'):
shutil.copy2(runtime / name, work / 'libs' / name)
for label, expected in (('null-deadline', 22), ('invalid-deadline', 22), ('null-thread', 22), ('join', 0)):
source = work / 'input.elf'
source.write_bytes(fixture(variant, label))
output = work / (label + '.exe')
result = subprocess.run([str(relinker), '--windows', str(source), str(output)],
capture_output=True, text=True, timeout=10)
assert result.returncode == 0, (variant, label, result.stderr)
result = subprocess.run([str(output)], cwd=work, capture_output=True, text=True, timeout=10)
assert result.returncode == expected, (variant, label, result.returncode, result.stderr)
print(variant + ': scoped timed-join validation and shared thread lifecycle passed')
if __name__ == '__main__':
main()
@@ -23,6 +23,7 @@
#include <io/ByteWriter.hpp>
#include <algorithm>
#include <array>
#include <bit>
#include <cmath>
#include <cstring>
#include <cstdint>
@@ -2080,6 +2081,45 @@ void xsaveoptExecution() {
require(std::memcmp(area + 160 + 3 * 16, pattern, 16) == 0 && std::memcmp(area + 576 + 3 * 16, pattern + 16, 16) == 0, "XSAVE did not store YMM3 in the standard format");
require(std::memcmp(restored, pattern, sizeof(pattern)) == 0, "XRSTOR did not restore YMM3 from the area XSAVE wrote");
}
void scalarCompareExecution() {
const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta);
const Bytes site = {0xF3, 0x0F, 0xC2, 0xC1, 0x05};
const auto match = matcher->Match(site.data(), site.size());
require(match && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "scalar compare stub was not produced");
auto body = match->StubBody;
body[match->ReturnBranchOffset] = 0xC3;
void* code = mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
require(code != MAP_FAILED, "cannot map executable memory for the stub");
std::memcpy(code, body.data(), body.size());
struct Case {
float left;
float right;
std::uint32_t mask;
const char* name;
};
for (const auto& item : std::vector<Case>{{309.0f, 309.0f, 0xFFFFFFFFu, "309 is not less than 309"},
{309.0f, 310.0f, 0u, "309 is less than 310"}}) {
alignas(16) float left[4] = {item.left, 1.0f, 2.0f, 3.0f};
alignas(16) float right[4] = {item.right, 0.0f, 0.0f, 0.0f};
alignas(16) std::uint32_t result[4] = {};
asm volatile(
"movaps (%[left]), %%xmm0\n\t"
"movaps (%[right]), %%xmm1\n\t"
"sub $128, %%rsp\n\t"
"call *%[code]\n\t"
"add $128, %%rsp\n\t"
"movaps %%xmm0, (%[result])\n\t"
:
: [left] "r"(left), [right] "r"(right), [result] "r"(result), [code] "r"(code)
: "memory", "xmm0", "xmm1");
require(result[0] == item.mask, item.name);
require(result[1] == std::bit_cast<std::uint32_t>(1.0f) && result[2] == std::bit_cast<std::uint32_t>(2.0f)
&& result[3] == std::bit_cast<std::uint32_t>(3.0f),
"scalar compare stub changed the upper lanes of its destination");
}
munmap(code, 4096);
}
#else
void reciprocalExecution() {}
void registerFormExecution() {}
@@ -2126,6 +2166,7 @@ void rdtscpExecution() {
#else
void rdtscpExecution() {}
void wideStoreExecution() {}
void scalarCompareExecution() {}
#endif
void scannerZeroTail() {
@@ -2187,6 +2228,7 @@ int main() {
converterRosettaLegacyScalarCompare();
rdtscpExecution();
wideStoreExecution();
scalarCompareExecution();
wideStoreSplit();
converterWideStoreWindows();
converterClaimedFollower();
@@ -27,7 +27,7 @@ def main():
output = case / "out" / ("app.exe" if windows else "app.elf")
output.parent.mkdir(exist_ok=True)
result = subprocess.run([str(relinker), *(["--windows"] if windows else []), "--registry", *options, str(source), str(output)],
capture_output=True, text=True, timeout=60)
capture_output=True, text=True, encoding="utf-8", timeout=60)
return result, output
for windows in (False, True):
@@ -36,7 +36,7 @@ def main():
work = Path(directory)
def run(*arguments):
return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, timeout=60)
return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, encoding="utf-8", timeout=60)
def case(name):
root = work / name
@@ -64,7 +64,7 @@ def main():
assert f"Patch: eboot.bin [flip_bit] va={address:#x} off=" in result.stdout, result.stdout
assert f"{expected:02x} -> {expected ^ 1:02x} (flip one bit)\n" in result.stdout, result.stdout
manifest = output.parent / "app.patches.toml"
text = manifest.read_text()
text = manifest.read_text(encoding="utf-8")
assert 'main_target = "eboot.bin"' in text and 'name = "flip_bit"' in text, text
assert 'main_target = "eboot.bin"\nlinux_base = 0x400000\n' in text, text
assert f"Patch manifest: {manifest}\n" in result.stdout
@@ -154,7 +154,7 @@ def main():
def relocation_overlap(relinker, work):
def run(*arguments):
return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, timeout=60)
return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, encoding="utf-8", timeout=60)
def patch_file(path, target, address, expected):
path.write_text(f'format = 1\n\n[[patch]]\ntarget = "{target}"\nva = {address:#x}\nexpected = "{expected}"\nvalue = "ffff"\n')
@@ -196,7 +196,7 @@ def relocation_overlap(relinker, work):
def writer_overlap(relinker, work):
def run(*arguments):
return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, timeout=60)
return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, encoding="utf-8", timeout=60)
def attempt(name, module, options, address, expected, value="ff", target="sce_module/sample.prx"):
case = work / name
@@ -63,7 +63,7 @@ def check_guest_needed(relinker, work):
(case / "prx" / "consumer.prx").write_bytes(module_with_needed(names, entries))
output = case / ("output.exe" if windows else "output.elf")
result = subprocess.run([str(relinker), *(["--windows"] if windows else []), str(source), str(output)],
capture_output=True, text=True, timeout=30)
capture_output=True, text=True, encoding="utf-8", timeout=30)
assert result.returncode == 0, (label, windows, result.stdout, result.stderr)
artifact = case / "app0" / "prx" / "consumer.prx.guest.prx"
assert artifact.read_bytes().startswith(b"MZ" if windows else b"\x7fELF"), artifact
@@ -81,7 +81,7 @@ def check_guest_needed(relinker, work):
module.write_bytes(module_with_needed((first, invalid), (0, 0, 1)))
output = case / ("output.exe" if windows else "output.elf")
result = subprocess.run([str(relinker), *(["--windows"] if windows else []), str(source), str(output)],
capture_output=True, text=True, timeout=30)
capture_output=True, text=True, encoding="utf-8", timeout=30)
assert result.returncode == 2 and f"{module}: Invalid dependency: {invalid.decode()}\n" in result.stderr, result.stderr
assert not output.exists() and not (case / "app0").exists(), output
@@ -198,7 +198,7 @@ def main():
source.write_bytes(executable_with_needed(needed, repeats))
output = case / ("output.exe" if windows else "output.elf")
result = subprocess.run([str(relinker), *(["--windows"] if windows else []), str(source), str(output)],
capture_output=True, text=True, timeout=30)
capture_output=True, text=True, encoding="utf-8", timeout=30)
return result, output
for windows in (False, True):
@@ -217,7 +217,7 @@ def main():
assert layout in result.stdout, result.stdout
for spelling in (os.path.join(".", output.name), os.path.join("..", case.name, output.name)):
relative = subprocess.run([str(relinker), *(["--windows"] if windows else []), "input.elf", spelling], cwd=case,
capture_output=True, text=True, timeout=30)
capture_output=True, text=True, encoding="utf-8", timeout=30)
assert relative.returncode == 0 and layout in relative.stdout, (spelling, relative.stdout, relative.stderr)
if windows and os.name == "nt":
run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30)
@@ -302,7 +302,7 @@ def main():
source = case / "input.elf"
source.write_bytes(executable_with_needed(b"libSceVideoOut.prx", 2))
result = subprocess.run([str(relinker), "--macos", str(source), str(case / "output")],
capture_output=True, text=True, timeout=30)
capture_output=True, text=True, encoding="utf-8", timeout=30)
assert result.returncode == 0, (result.stdout, result.stderr)
print("Guest needed module tests passed")
@@ -148,7 +148,7 @@ def relink(relinker, case, executable, modules):
for name, image in modules.items():
(case / "sce_module" / name).write_bytes(image)
output = case / "output.elf"
result = subprocess.run([str(relinker), str(source), str(output)], capture_output=True, text=True, timeout=30)
result = subprocess.run([str(relinker), str(source), str(output)], capture_output=True, text=True, encoding="utf-8", timeout=30)
assert result.returncode == 0, (result.stdout, result.stderr)
return output
@@ -192,7 +192,7 @@ def main():
for name, image in twins.items():
(work / "needed-both" / "sce_module" / name).write_bytes(image)
result = subprocess.run([str(relinker), str(work / "needed-both" / "input.elf"), str(work / "needed-both" / "output.elf")],
capture_output=True, text=True, timeout=30)
capture_output=True, text=True, encoding="utf-8", timeout=30)
assert result.returncode != 0 and "Ambiguous guest import AAAAAAAAAAA" in result.stderr, (result.stdout, result.stderr)
guests = relink(relinker, work / "guest-declared", importing_executable([]), {
@@ -222,7 +222,7 @@ def main():
needed_module_import("YYYYYYYYYYY", "XXXXXXXXXXX#A#B", "libc.prx", (tag,)))
output = case / ("output.exe" if windows else "output.elf")
result = subprocess.run([str(relinker), *(["--windows"] if windows else []), str(case / "input.elf"), str(output)],
capture_output=True, text=True, timeout=30)
capture_output=True, text=True, encoding="utf-8", timeout=30)
assert result.returncode == 0, (windows, hex(tag), result.stdout, result.stderr)
if not windows:
modules = case / "app0" / "sce_module"
@@ -240,7 +240,7 @@ def main():
module = case / "sce_module" / "libc.prx"
module.write_bytes(needed_module_import("XXXXXXXXXXX", "YYYYYYYYYYY#A#B", "libkernel.prx", (0x6100000F, 0x61000045)))
result = subprocess.run([str(relinker), str(case / "input.elf"), str(case / "output.elf")],
capture_output=True, text=True, timeout=30)
capture_output=True, text=True, encoding="utf-8", timeout=30)
assert result.returncode == 2 and f"{module}: Guest module mixes PS4 and PS5 needed-module tags\n" in result.stderr, result.stderr
assert not (case / "output.elf").exists()
@@ -250,7 +250,7 @@ def main():
module = case / "sce_module" / "libc.prx"
module.write_bytes(needed_module_import("XXXXXXXXXXX", "YYYYYYYYYYY#A#C", "libkernel.prx"))
result = subprocess.run([str(relinker), str(case / "input.elf"), str(case / "output.elf")],
capture_output=True, text=True, timeout=30)
capture_output=True, text=True, encoding="utf-8", timeout=30)
assert result.returncode == 2 and "Unknown import module ID: YYYYYYYYYYY#A#C\n" in result.stderr \
and f"Input: {module}\n" in result.stderr, result.stderr
assert not (case / "output.elf").exists()
@@ -21,7 +21,7 @@ def main():
output = work / (name + ".out")
registry = work / (name + ".registry.json")
result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", *flags, str(source), str(output)],
capture_output=True, text=True, timeout=20)
capture_output=True, text=True, encoding="utf-8", timeout=20)
assert result.returncode == 2 and error in result.stderr and "bad_alloc" not in result.stderr, (name, flags, result)
assert not output.exists() and not registry.exists(), (name, flags, result)
@@ -30,7 +30,7 @@ def check_executable_hardlinks(relinker, work):
links = protected.stat().st_nlink
try:
result = subprocess.run([str(relinker), *command], cwd=case,
capture_output=True, text=True, timeout=20)
capture_output=True, text=True, encoding="utf-8", timeout=20)
assert result.returncode == 2 and "output would overwrite" in result.stderr, (
windows, output_name, input_name, readonly, result.stdout, result.stderr)
for path, original in inputs.items():
@@ -61,7 +61,7 @@ def check_executable_hardlinks(relinker, work):
path.chmod(stat.S_IREAD)
try:
result = subprocess.run([str(relinker), *command], cwd=case,
capture_output=True, text=True, timeout=20)
capture_output=True, text=True, encoding="utf-8", timeout=20)
assert result.returncode == 0, (readonly, result.stdout, result.stderr)
for name in ("executable", "secondary-one", "secondary-two"):
assert outputs[name].read_bytes().startswith(b"MZ")
@@ -111,7 +111,7 @@ def check_diagnostic_inputs(relinker, work):
options.append("--registry")
result = subprocess.run([str(relinker), "--windows", "--windows-diagnostics", *options,
str(source), str(output)], cwd=diagnostics.parent,
capture_output=True, text=True, timeout=20)
capture_output=True, text=True, encoding="utf-8", timeout=20)
owner = "an input module" if bundled else "the input executable"
assert result.returncode == 2 and f"Windows import diagnostics output would overwrite {owner}" in result.stderr, (result.stdout, result.stderr)
assert str(diagnostics) in result.stderr, result.stderr
@@ -134,7 +134,7 @@ def check_diagnostic_inputs(relinker, work):
options = ["--windows-diagnostics"] if enabled else []
result = subprocess.run([str(relinker), "--windows", "--skip-sce-module", *options,
str(source), str(output)], cwd=case,
capture_output=True, text=True, timeout=20)
capture_output=True, text=True, encoding="utf-8", timeout=20)
assert result.returncode == 0 and output.read_bytes().startswith(b"MZ"), (result.stdout, result.stderr)
assert source.read_bytes() == original
assert diagnostics.read_bytes() == (b"" if enabled else original)
@@ -177,7 +177,7 @@ def check_registry_inputs(relinker, work, windows, needed=False):
previous_output = b"previous executable"
output.write_bytes(previous_output)
result = subprocess.run([str(relinker), *options, "--registry", str(source), str(output)],
capture_output=True, text=True, timeout=20)
capture_output=True, text=True, encoding="utf-8", timeout=20)
kind = "Needed libraries" if needed else "Registry"
expected = f"{kind} output would overwrite an input module" if bundled else f"{kind} output would overwrite the input executable"
assert result.returncode == 2 and expected in result.stderr, (result.returncode, result.stdout, result.stderr)
@@ -204,7 +204,7 @@ def check_registry_after_failed_patch(relinker, work):
for registry in registries:
registry.write_bytes(previous_registry)
result = subprocess.run([str(relinker), "--windows", "--registry", str(source), str(output)],
capture_output=True, text=True, timeout=20)
capture_output=True, text=True, encoding="utf-8", timeout=20)
assert result.returncode == 2 and "Invalid Windows icon PNG" in result.stderr, (result.stdout, result.stderr)
assert output.read_bytes() == previous_output
assert all(registry.read_bytes() == previous_registry for registry in registries)
@@ -225,7 +225,7 @@ def check_output_directory(relinker, work):
output = str(target) if existing else str(target) + os.sep
options = ["--windows"] if windows else []
result = subprocess.run([str(relinker), *options, str(source), output], cwd=case,
capture_output=True, text=True, timeout=20)
capture_output=True, text=True, encoding="utf-8", timeout=20)
assert result.returncode == 2 and "Executable output is a directory" in result.stderr, (result.stdout, result.stderr)
assert not (case / "app0").exists() and not (target / "app0").exists()
assert target.exists() == existing
@@ -273,7 +273,7 @@ def check_desktop_inputs(relinker, work):
registry_path.write_bytes(previous_registry)
options = ["--registry"] if registry else []
result = subprocess.run([str(relinker), *options, str(source), str(output)],
capture_output=True, text=True, timeout=20)
capture_output=True, text=True, encoding="utf-8", timeout=20)
kind = "Desktop entry" if desktop else "Icon"
owner = "an input module" if bundled else "the input executable"
assert result.returncode == 2 and f"{kind} output would overwrite {owner}" in result.stderr, (result.stdout, result.stderr)
@@ -332,7 +332,7 @@ def main():
arguments = [] if bundled else ["--skip-sce-module"]
result = subprocess.run([str(relinker), *options, *arguments, "--registry",
str(source), str(output)],
capture_output=True, text=True, timeout=20)
capture_output=True, text=True, encoding="utf-8", timeout=20)
expected = "Executable output would overwrite an input module" if bundled else "Executable output would overwrite the input executable"
if (result.returncode != 2 or expected not in result.stderr
or source.read_bytes() != original
@@ -365,7 +365,7 @@ def main():
if existing:
output.write_bytes(b"previous output")
result = subprocess.run([str(relinker), *options, "--registry", str(source), str(output)],
capture_output=True, text=True, timeout=20)
capture_output=True, text=True, encoding="utf-8", timeout=20)
converted = case / "app0" / "sce_module" / "sample.prx.guest.prx"
magic = b"MZ" if windows else b"\x7fELF"
if (result.returncode != 0 or not output.read_bytes().startswith(magic)
@@ -375,7 +375,7 @@ def main():
raise AssertionError((windows, existing, result.returncode, result.stdout, result.stderr))
if not windows:
assert Path(f"{output}.png").read_bytes() == image
assert Path(f"{output}.desktop").read_text().startswith("[Desktop Entry]\n")
assert Path(f"{output}.desktop").read_text(encoding="utf-8").startswith("[Desktop Entry]\n")
print("Input preservation tests passed")
@@ -21,7 +21,7 @@ def main():
def check(input_path, offset, options):
result = subprocess.run([str(relinker), "--windows", *options, str(source), str(output)],
capture_output=True, text=True, timeout=30)
capture_output=True, text=True, encoding="utf-8", timeout=30)
assert result.returncode == 2, result
assert "Unsupported Windows guest TLS instruction" in result.stderr, result.stderr
assert f"(bytes: {TLS_INSTRUCTION.hex(' ')})" in result.stderr, result.stderr
+2 -2
View File
@@ -216,11 +216,11 @@ On a device that supports `VK_EXT_device_fault`, the first `VK_ERROR_DEVICE_LOST
## Worker sampler
Set `APS5_SAMPLE_WORKER=<file>` to sample queue worker 0 every millisecond (in practice every timer tick). Every 20 s and at exit it writes `<file>`, one `module offset self inclusive` line per sampled address, and `<file stem>.collapsed.txt`, the worker's whole stacks in the collapsed format that speedscope.app and flamegraph.pl read: one `frame;...;frame count` line per distinct stack, root first, each frame as `module+0xRVA`. A walk that ends before the thread's root starts with `[truncated]`. The `#` lines give the samples, the walk depth (64 frames), the truncated walks, the mean and longest time the worker was suspended, and the size and PE timestamp of each module, to check the binary a file is symbolized against. A frame with `.pdata` is stepped by `RtlVirtualUnwind`; any other frame by its DWARF record in `.ehfram`, the unwind data the libraries are built with. Those steps read only inside the worker's stack, and a module is indexed after the worker resumes, the first time a walk meets it.
Set `APS5_SAMPLE_WORKER=<file>` to sample queue worker 0 every millisecond (in practice every timer tick). Every 20 s and at exit it writes `<file>`, one `module offset self inclusive` line per sampled address, and `<file stem>.collapsed.txt`, the worker's whole stacks in the collapsed format that speedscope.app and flamegraph.pl read: one `frame;...;frame count` line per distinct stack, root first, each frame as `module+0xRVA`. A walk that ends before the thread's root starts with `[truncated]`. The `#` lines give the samples, the walk depth (64 frames), the truncated walks, the mean and longest time the worker was suspended, and the size and PE timestamp of each module, to check the binary a file is symbolized against. A frame with `.pdata` is stepped by `RtlVirtualUnwind`; any other frame by its DWARF record in `.ehfram`, the unwind data the libraries are built with. Those steps read only inside the worker's stack, and a module is indexed after the worker resumes, the first time a walk meets it. `python tools/symbolize_samples.py <file stem>.collapsed.txt --libs <libs directory>` names each frame from the COFF symbols of the module that ran, or from its export table when it has none, and leaves a module unsymbolized when its size or timestamp differs from its `# module` line or when it has none (`--unverified` symbolizes it anyway); `--since <earlier copy>` keeps only the samples taken after that copy, `--out` writes the named stacks and `--demangle` uses `c++filt`. Given `<file>`, it lists functions by self and by inclusive samples, where a function's inclusive count is the sum of its addresses' and overcounts a function that is on one stack more than once.
`APS5_SAMPLE_COMMITTER=<file>` samples queue 0's draw committer the same way and writes the same two files. The committer records the draws the worker hands over (`APS5_PIPELINED_DRAWS`), so when the worker's stacks wait in `DrawPipeline::Enqueue`, its stacks show what bounds the frame. It starts with the first pipelined draw; with `APS5_PIPELINED_DRAWS=0` nothing is sampled.
`APS5_SAMPLE_THREADS=<file>` samples every thread of the process every 2 ms and writes the 40 hottest addresses of each busy thread to `<file>`, 12 frames deep and through `.pdata` only. The samplers exist only on Windows; the addresses are symbolized offline, for example with `nm` on the `.prx`.
`APS5_SAMPLE_THREADS=<file>` samples every thread of the process every 2 ms and writes the 40 hottest addresses of each busy thread to `<file>`, 12 frames deep and through `.pdata` only, under a `thread <id> samples <n>` line per thread and with the same `# module` lines. The samplers exist only on Windows; the addresses are symbolized offline with `tools/symbolize_samples.py`, which adds up all threads or, with `--thread <id>`, keeps one.
## Performance trace
+8 -2
View File
@@ -6,7 +6,7 @@
- [sceAjmInstanceExtend, sceAjmInstanceSwitch](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjm.native) - their four-argument signatures come only from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ajm.cs#L98-L104), whose comments do not describe them; shadPS4 [declares both without parameters](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ajm/ajm.h#L222-L223). Neither is measured on a console; these throwing declarations only resolve imports.
- [sceAjmInstanceCreate](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a full 14-bit instance slot namespace returns `0x80930007` (`OUT_OF_RESOURCES`), as [shadPS4's InstanceCreate](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ajm/ajm_context.cpp) does for its own instance pool (PS4). The 16384 slots per codec prefix are this runtime's existing ID layout, not a measured console capacity; the PS5 capacity and exhaustion result are unverified.
- [ATRAC9 vibration decoding](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): channel configurations 6 (mono) and 7 (dual mono) use the pinned [vgmstream decoder](https://github.com/vgmstream/vgmstream/blob/7c520e8f9b078dfde2f84f1b5bcbad0c6c415806/src/coding/libs/atrac9_dec.c), including its 12/24/48 kHz profile limits. Synthetic fixtures cover interleaving, reset after malformed input, partial superframes and PCM16/PCM32/float output; console PCM rounding and physical haptic output are unverified. Ordinary channel configurations and discrete multichannel streams still use LibAtrac9. Decoder creation and decoding run under the existing AJM instance lock because the dependency rebuilds shared Huffman tables during initialization.
- [sceAjmBatchJobGetInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the format reports bitrate 0 for ATRAC9 as in KytyPS5, while [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it from the superframe size; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder
- [sceAjmBatchJobGetInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the ATRAC9 format's bitrate is the superframe's bits per second (superframe bytes * 8 * sample rate / samples per superframe), as [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it and as a PS4 reported it (168000 for 448-byte superframes at 48 kHz), where KytyPS5 reports 0; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder
- [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: Layer I/II headers are accepted when no original-file-length (`parse_ofl`) lookup is requested, which is supported only for Layer III; MPEG-2.5 stops at 64 kbps. The `parse_ofl` layout (Xing/Info + LAME tag after side information, with the standard fixed Xing/Info offset retained for encoders such as [LAME](https://github.com/lameproject/lame/blob/master/libmp3lame/VbrTag.c#L854-L862), VBRI at byte 36 or after side information, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description.
- [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title (the one call in PPSA04354 passes a 24-byte result buffer, the 8-byte result plus the 16-byte ATRAC9 codec info written here; the call does not show the codec or the fields); a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts
- [AJM Opus decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. NHL 27 (PPSA34063) passes `1, 48000, 255`. Family 255 with one channel is decoded as one mono stream, the same as family 0; this is assumed, since the stream count, coupled stream count and channel mapping that a family-255 Opus header carries ([RFC 7845 section 5.1.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1.1)) are not passed. Only 48000 Hz, mapping families 0 and 1, and family 255 with one channel are implemented; other rates and families, and family 255 with more channels, throw
@@ -17,6 +17,12 @@
- [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - initialize parameters are 8 bytes for ATRAC9 and M4AAC, 12 for Opus and none for MP3, as one title passes them; other codecs and sizes, and an output other than the 8-byte result, throw
- [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - SIDEBAND_GAPLESS_DECODE with RESET overwrites both counts as in [RPCSX](https://github.com/RPCSX/rpcsx); shadPS4 ignores a zero count when the other is nonzero. Without RESET they differ, so it throws
- [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty
- [sceAjmBatchJobControlBufferRa, sceAjmBatchJobRunBufferRa, sceAjmBatchJobRunSplitBufferRa, sceAjmBatchStartBuffer](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - the batch layout (chunk kinds, 20-bit payload, flags bits 32-51 as the flags chunk's payload, the builders' chunk order) follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/854b291c63360ff24e590809aff54cf5ab2dd4c9/src/core/libraries/ajm/ajm_batch.cpp) and was confirmed byte for byte for 64-byte control and 80-byte run jobs by a PS4 trace of Knowledge is Power: Decades (CUSA12435, Wwise, 32 545 batches dumped by a GoldHEN plugin). The console's builders write only bits 0-3 (kind), bit 4 (a buffer chunk's output direction) and bits 6-25 (payload) of the header word and leave bits 5, 26, 27 and 31 as the previous batch left them, so the kind is read as 4 bits plus the direction bit, not as 6 bits
- [sceAjmBatchStartBuffer](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - the statistics instance 0x80000 and the inline chunk come from shadPS4 only: the traced title builds every batch with the builders and a return address, and no title was seen writing an inline chunk. Flags are kept as passed. Unknown chunk kinds, control flags other than RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE, control jobs with run buffers and run jobs with a control input throw
- [sceAjmBatchStartBuffer refusals](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - probe batches through the console's builders on a PS4 showed that a run buffer chunk of size zero is dropped whatever its address (the job runs as without it: 0x10 and nothing consumed for no room, 0 for no input), and that a null address with a nonzero size refuses the batch before any of its jobs runs, with 0x80930011 returned and the error record holding 0x80930016, the job chunk's address, the chunk's offset in the job and the job's return address. Control buffer chunks follow the same rule here, not measured. A size not a multiple of 8, a chunk that overruns its job or the batch, or a job without flags are refused the same way with 0x80930011 in the record; the console's report for those was not measured
- [sceAjmBatchStartBuffer null and empty batches](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - a null batch address returns 0x80930005 whatever the size, as the PS5 builders' sceAjmBatchInitialize does for a null buffer, and a batch of zero bytes at a non-null address is accepted, runs nothing and completes at once (a null address with size 0 is refused like any null address); neither was measured on a console (the probe that passed a null batch froze the console before a result was written, so it is not retried on hardware)
- [ATRAC9 run results](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - probe runs of a title's ATRAC9 stream through the console's builders on a PS4 showed the run result naming the side that ran out: `PARTIAL_INPUT` (0x8) when the input ran out with room left, on a superframe boundary or not; `NOT_ENOUGH_ROOM` (0x10) when the room ran out with input left, after any number of frames, and with nothing consumed for a zero room; both (0x18) when the input ended just as the output filled, which is every Wwise run job. ATRAC9 run jobs with `RUN_MULTIPLE_FRAMES` follow those rules here, vibration streams included, which were not traced; resampled runs and the other codecs keep the earlier rule (a trailing partial frame is partial input, a run that produced nothing is out of room)
- [ATRAC9 run without RUN_MULTIPLE_FRAMES](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the same PS4 probe decoded exactly one frame and reported 0 for such a run, and a job read from a PS4 batch buffer (sceAjmBatchStartBuffer) does the same here. A job from the PS5 builders still decodes its whole input, as FMOD in PPSA29141 needs (it puts two run jobs in one batch with the second's input right after the whole of the first's); whether a PS5 stops after one frame is not measured. MP3 and Opus run jobs stop after one frame on both paths. The console also rejected the tests' synthetic silent superframe after three frames (`CODEC_ERROR | FATAL`, internal result -577) where LibAtrac9 decodes four, so that vector proves job plumbing, not decoding; on the title's stream LibAtrac9 matched the console's PCM within 4 LSB, 83.5% of the samples exactly
### Functional
@@ -24,7 +30,7 @@
- [AJM batch contexts](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): start, wait and cancel reject context IDs not allocated by initialization; batches belong to their creating context, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0ebea15c24253b60f514fc37a348dabc8d0bcb90/src/core/libraries/ajm/ajm.cpp). This is not verified on PS5. Finalization still does not invalidate contexts or release their instances and outstanding batches; jobs execute synchronously and cancellation does not interrupt them.
- [sceAjmBatchStart](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp): console results for malformed batch descriptors and corrupted jobs are unverified. A null batch buffer or an offset beyond its capacity returns the existing invalid-parameter error; a truncated or inconsistent private packed job throws before its payload is read. The packed representation belongs to this implementation.
- [ATRAC9 gapless segments](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a run job stops after the gapless total of output samples counted from the start of the segment, leaving the rest of its input unconsumed, and the next run starts the next segment with the decoder as it is and without a second skip. FMOD in PPSA29141 sets a total shorter than a sound's frames and reads the frames after it on later runs; with the output held at the total its stream reader waited for them forever and the last buffer repeated. The console's handling of the frames after the total is not documented; a reset (`SetGaplessDecode` with the reset flag, `ClearContext`, initialization) starts a new segment. An ATRAC9 run job without `RUN_MULTIPLE_FRAMES` decodes its whole input as one with it: FMOD in PPSA29141 puts two run jobs in one batch with the second's input right after the whole of the first's. Whether the console stops after one frame there is not verified; MP3 and Opus run jobs still stop after one frame
- [ATRAC9 gapless segments](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a run job stops after the gapless total of output samples counted from the start of the segment, leaving the rest of its input unconsumed, and the next run starts the next segment with the decoder as it is and without a second skip. FMOD in PPSA29141 sets a total shorter than a sound's frames and reads the frames after it on later runs; with the output held at the total its stream reader waited for them forever and the last buffer repeated. The console's handling of the frames after the total is not documented; a reset (`SetGaplessDecode` with the reset flag, `ClearContext`, initialization) starts a new segment.
- [AJM M4AAC decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 2) - Ghost of Yotei (PPSA26344) initializes it through `sceAjmBatchJobControl`. The initialize parameters (`u32` config type, 1 ADTS or 2 raw, then `u32` sampling frequency index), instance flags (channels in bits 3-6, `ENABLE_SBR_DECODE` bit 32, `ENABLE_NONDELAY_OUTPUT` bit 33, surround interleave order bits 34-35), `{heaac, reserved}` codec info and decoder error result (`CODEC_ERROR | FATAL`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/ajm/ajm_aac.cpp) (PS4, fdk-aac) and are unverified on PS5; decoding uses FFmpeg's AAC decoder. Without `ENABLE_NONDELAY_OUTPUT`, the first two frames after initialization or reset are consumed without output. Raw input takes its channel configuration from the instance (0 means stereo) and is decoded as one access unit per run job. The format reports the last frame's bitrate. Invalid config type or raw sampling frequency index above 11 returns invalid-parameter result 4. The implementation throws for ADTS without a frame header, profiles other than AAC LC, multiple raw data blocks per ADTS frame, unsupported channel configurations, more channels than the instance supports, HE-AAC without `ENABLE_SBR_DECODE`, raw input with `RUN_MULTIPLE_FRAMES`, output room smaller than the previous frame length, and unknown codec flags.
- [AJM](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix.
- [AJM Opus mapping family 255 with several channels](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): the initialization words carry no stream count or channel mapping, so the decoder opens on the first packet. It counts the self-delimited streams of that multistream packet ([RFC 6716 appendix B](https://datatracker.ietf.org/doc/html/rfc6716#appendix-B)) and takes each stream's stereo flag from its TOC byte as coupled, then decodes with an identity channel mapping. A packet whose streams do not add up to the channel count throws. The layout is inferred, not read from the console; unverified on hardware.
+1 -1
View File
@@ -2,4 +2,4 @@
### Unknown function info
- [libSceAjmi pending exports](../../../core/libs/prx/libSceAjmi/Export.cpp) - 5 named callable exports were identified in the PS5 3.20 export table and cross-checked against firmware 13.60. Signatures and behavior remain unverified; declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAjmi.c).
- [libSceAjmi pending exports](../../../core/libs/prx/libSceAjmi/Export.cpp) - of the 5 named callable exports identified in the PS5 3.20 export table and cross-checked against firmware 13.60, 2 are declared here: `sceAjmBatchJobInlineBuffer` and `sceAjmBatchJobRunBufferRa`, the latter as `sceAjmBatchJobRunBufferRa_nid_disambig1`, which the NID patcher exports under the name's NID, because libSceAjm implements the same name for PS4 titles; a title importing it from libSceAjmi still gets the not-implemented error at call time. Signatures and behavior remain unverified; the declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAjmi.c).
+2 -2
View File
@@ -2,8 +2,8 @@
### Silent stubs
- [libSceAvPlayer](../../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement.
- [sceAvPlayerSetTrickSpeed](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time.
- [libSceAvPlayer](../../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement; after a source is added it returns `OPERATION_FAILED`, as [prosper's reading of the module](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/video/avplayer.cpp#L960-L972) describes.
- [sceAvPlayerSetTrickSpeed](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. With a source, a zero speed returns `INVALID_PARAMS` before the `OPERATION_FAILED` of a stopped, finished or failed player, the order AnyPS5 had before; that order is unmeasured. In [prosper's reading of the module](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/video/avplayer.cpp#L929-L959), only the missing source is checked first. A zero speed is refused with `NOT_SUPPORTED`, with every other speed from -399 to 399 except 100, and no player state is checked.
### Unknown function info
+1
View File
@@ -147,6 +147,7 @@
- [std::bad_optional_access and std::_Throw_bad_optional_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) provide the exception type, constructor, destructor, and `what()` exports alongside `_Throw_bad_optional_access`, returning the `what()` string `bad_optional_access` matching Dinkumware and libc++ conventions; the console libc.prx string is unverified. PPSA26578 (issue #7624) and PPSA34015 import `_ZSt26_Throw_bad_optional_accessv` (`KwzEip7m6TM`) from `libc`.
- [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, decodes the input of the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) on Linux as UTF-8, mirroring the narrow printf family, where FreeBSD uses the locale's `mbrtowc` (one byte per character in the C locale); the console's locale encoding is unverified. Input that is not UTF-8, a character outside the BMP, a scanset member outside ASCII, positional arguments next to a wide conversion and wide conversions in `fscanf` throw. Each character is stored as one 16-bit unit, widths and `_s` sizes count characters, and a `%lc` field cut short by the end of the input keeps what it read, as FreeBSD's `convert_wchar` does. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits
- [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e`
- [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) on Linux reads a `q` length as `ll`, as glibc does. On Windows a `q` directive ends the scan with nothing stored, as the console's own libc does for `%qd`; FreeBSD 11.4's source reads it as `ll`.
- [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `<name>.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx`. A relinked module request that misses both the exact and the case-insensitive `<name>.guest.prx` takes the sibling whose stem (the name without its last extension, ASCII case ignored) equals the requested stem and that ends with `.guest.prx`, shortest name first: Unity IL2CPP titles load `Media/Modules/<name>.prx` and `Media/Plugins/<name>.prx`, and the relinker names the converted module after the ELF it was built from, `<name>.elf.guest.prx`. The console's module namespace is assumed to ignore case and the extension; several stem matches pick the shortest name instead of throwing, which is not measured on a console
- [Guest file names on Windows](../../../core/libs/prx/libc/src/General.cpp) (libc) are not escaped or mapped before the host call, so names Windows treats specially differ from FreeBSD, where only `/` and NUL are special. `a*b`, `a?b`, `a<b>c`, `a|b` and `a"b` fail with `EINVAL` (22) where FreeBSD creates them. `trail.` and `trail ` both create the host file `trail`, so three FreeBSD names are one file here. `NUL` and a reopen of it succeed, but no file is created. `con`, `aux.txt` and `COM1` create ordinary files. Colons are under Colons in guest names. Measured on Windows 11 (10.0.26200), MinGW-w64 GCC 15.2.0 r7, with `open` (`O_CREAT|O_EXCL|O_WRONLY`), a one-byte write and a listing of the host directory; not measured on Linux or a console. A fix needs a guest-to-host name mapping.
- [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount
+1
View File
@@ -186,6 +186,7 @@
- [pthread_sigmask](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) shares the process-wide mask of `sigprocmask` and `_sigprocmask`; FreeBSD keeps a mask per thread
- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) reaches a thread that waits in an alertable `TimedWait` wait through an APC. A thread blocked anywhere else, including all waits under `APS5_COARSE_TIMED_WAITS` and host waits inside the emulator, is redirected and runs the handler only when that wait returns
- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler wherever the target thread was interrupted. It waits only while the thread is inside winpthreads or restoring a context through `NtContinue`, and throws if that lasts 1 s, so a handler that calls into other host code the thread was interrupted in, such as the C runtime's heap or a lock of the emulator, can deadlock on it or reenter it. A thread that holds the address-wait lock of `sceKernelSyncOnAddressWait`, `sceKernelSyncOnAddressWake` and `_umtx_op`, or the park queue lock of the guest mutexes, gets the raise as an APC instead, which runs once it has released the lock or while it waits; the other locks of the emulator are not deferred this way
- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows throws for a target thread that was created suspended and is not resumed yet. FreeBSD 11.4 libthr starts such a thread with every signal but `SIGTRAP` blocked ([`thr_create.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_create.c#L177-L183)) and restores its mask only once it is resumed ([`thread_start`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_create.c#L252-L294)), so a signal sent to it stays pending until then. The console's behaviour is unverified
- [sigaltstack](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records each thread's alternate signal stack and reports it back, following FreeBSD's `kern_sigaltstack` (`EINVAL` for flags other than `SS_DISABLE`, `ENOMEM` below `MINSIGSTKSZ` 2048), but handlers never run on it, so `SS_ONSTACK` and `EPERM` are never reported
- [sigpending](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) reports the intersection of the process-wide pending set of `sigtimedwait` and the blocked mask of `_sigprocmask`, as FreeBSD's `sys_sigpending` intersects the pending list with `td_sigmask`, and returns -1 with `EFAULT` for a null set. PPSA99420 imports it
- [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - a joined thread is kept until `scePthreadCreate` publishes over the same handle storage, so one host object and its handle are retained per distinct guest storage location for the life of the process; a guest that joins into a fresh location every time keeps every one. A handle copied to a second storage and joined there retains the object under the original storage key, so a later `scePthreadCreate` over the original storage releases an object the copy still points at, and the copy reads freed memory. A detached thread is released when it ends, so joining it after it ended reads freed memory. A null entry crashed the payload and throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console. PPSA34015 once stopped with `Joining guest thread: Access is denied` before the join and detach checks followed the console
+1
View File
@@ -21,6 +21,7 @@
- [aio_read, aio_write, aio_fsync, aio_error, aio_return, aio_suspend, aio_waitcomplete, aio_cancel](../../../core/libs/prx/libkernel_sys/Aio.cpp) (libkernel_sys) - a request with `SIGEV_SIGNAL`, `SIGEV_THREAD_ID` or `SIGEV_KEVENT` completion, or on a socket, throws, because delivering a guest signal or kevent and socket aio are not modelled. One worker thread runs the process's requests one at a time in submission order, so a request is cancellable only until that thread takes it. A queued request keeps the descriptor number, not the open file: closing or reusing the descriptor before it runs changes what it reaches. Pipes fail with `EOPNOTSUPP`, as FreeBSD 11 does with `vfs.aio.enable_unsafe` 0; the console's setting is unverified. Waits are not interrupted by signals (`EINTR`), and timeouts under 1 ms round up to 1 ms (hz 1000). Otherwise the rules follow FreeBSD 11 `vfs_aio.c`
- [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified
- [link](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows resolves a symbolic link source through the host, which follows longer chains than FreeBSD: a chain of 40 links is linked, where FreeBSD 11.4 returns `ELOOP` after 32 (`MAXSYMLINKS`). A loop returns `ELOOP` as on FreeBSD
- [pthread_attr_setcreatesuspend_np, pthread_resume_np, pthread_resume_all_np, pthread_multi_np, scePthreadAttrSetcreatesuspend, scePthreadResume, scePthreadResumeAll and scePthreadMulti](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) - when a thread created suspended stops relative to its signal mask is unmeasured on a console. Return values and errors follow FreeBSD 11.4 libthr ([`thr_attr.c`](https://github.com/freebsd/freebsd-src/blob/8c639945/lib/libthr/thread/thr_attr.c#L369-L381), [`thr_resume_np.c`](https://github.com/freebsd/freebsd-src/blob/8c639945/lib/libthr/thread/thr_resume_np.c#L47-L97), [`thr_multi_np.c`](https://github.com/freebsd/freebsd-src/blob/8c639945/lib/libthr/thread/thr_multi_np.c#L40-L51)) and the code of firmware 13.42's `libkernel_sys.sprx` ([#9573](https://github.com/boykopovar/AnyPS5/pull/9573#issuecomment-6393877843)), where the sce forms add `0x80020000` to a non-zero errno
- [fstatat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `stat`, or to `lstat` with `AT_SYMLINK_NOFOLLOW` (0x200). Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory, and flags other than `AT_SYMLINK_NOFOLLOW` return `EINVAL`. PPSA74126 imports it. Console behaviour is unverified
- [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified
- [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified
+300
View File
@@ -0,0 +1,300 @@
import argparse
import bisect
import collections
import shutil
import struct
import subprocess
import sys
from pathlib import Path
DESCRIPTION = """Symbolize the output of the Windows samplers (APS5_SAMPLE_WORKER, APS5_SAMPLE_COMMITTER and
APS5_SAMPLE_THREADS) against the modules that ran. Each sampled frame is written as module+0xRVA; this reads the
COFF symbol table of each module found in --libs (the MinGW-built .prx and .dll files keep one), or its export
table when it has none, and names every frame module!function.
The '# module name size timestamp' header lines the samplers write are compared with the SizeOfImage and
TimeDateStamp of the file that is found; a module whose file does not match is left unsymbolized, since its
addresses would name the wrong functions.
The sampler rewrites its files every 20 s with totals since start-up. --since takes an earlier copy of the same
file and subtracts it, which keeps only the samples taken between the two copies (a steady-state window).
A module with no such record is also left unsymbolized, since nothing shows which build it was; --unverified
symbolizes it anyway against the file found.
Input: a collapsed-stack file (<file stem>.collapsed.txt, 'frame;...;frame count' lines) or an address file
('module offset self inclusive' lines, under 'thread <id> samples <n>' headings for APS5_SAMPLE_THREADS; --thread
keeps one thread). Output: the hottest functions by self and by inclusive samples, and with --out a symbolized
collapsed-stack file for speedscope.app or flamegraph.pl. For an address file, a function's inclusive count is the
sum of its addresses' counts, which overcounts the samples in which it is on the stack more than once."""
IMAGE_SCN_CNT_CODE = 0x20
IMAGE_SYM_CLASS_EXTERNAL = 2
IMAGE_SYM_CLASS_STATIC = 3
class PeModule:
def __init__(self, path):
data = Path(path).read_bytes()
if data[:2] != b"MZ":
raise ValueError(f"{path}: not a PE file")
pe = struct.unpack_from("<I", data, 0x3C)[0]
if data[pe:pe + 4] != b"PE\0\0":
raise ValueError(f"{path}: not a PE file")
_, sections, self.timestamp, symbol_table, symbol_count, optional_size, _ = struct.unpack_from("<HHIIIHH", data, pe + 4)
optional = pe + 24
if struct.unpack_from("<H", data, optional)[0] != 0x20B:
raise ValueError(f"{path}: not a PE32+ image")
self.image_base = struct.unpack_from("<Q", data, optional + 24)[0]
self.size = struct.unpack_from("<I", data, optional + 56)[0]
export_rva, export_size = struct.unpack_from("<II", data, optional + 112)
self.sections = []
for index in range(sections):
entry = optional + optional_size + index * 40
virtual_size, rva, raw_size, raw_pointer = struct.unpack_from("<IIII", data, entry + 8)
characteristics = struct.unpack_from("<I", data, entry + 36)[0]
self.sections.append((rva, max(virtual_size, raw_size), raw_pointer, characteristics))
symbols = self.coff_symbols(data, symbol_table, symbol_count) if symbol_table and symbol_count else []
self.exported = not symbols
if self.exported and export_rva:
symbols = self.export_symbols(data, export_rva, export_size)
symbols.sort()
self.code = []
for start, size, _, characteristics in self.sections:
if not characteristics & IMAGE_SCN_CNT_CODE:
continue
inside = [(address, name) for address, name in symbols if start <= address < start + size]
self.code.append((start, start + size, [address for address, _ in inside], [name for _, name in inside]))
self.names = [name for _, name in symbols]
def offset(self, rva):
for start, size, raw, _ in self.sections:
if start <= rva < start + size:
return raw + rva - start
raise ValueError(f"RVA {rva:#x} is in no section")
def coff_symbols(self, data, table, count):
strings = table + count * 18
result = []
index = 0
while index < count:
entry = table + index * 18
name, value, section, kind, storage, aux = struct.unpack_from("<8sIhHBB", data, entry)
index += 1 + aux
if storage not in (IMAGE_SYM_CLASS_EXTERNAL, IMAGE_SYM_CLASS_STATIC) or not 1 <= section <= len(self.sections):
continue
start, _, _, characteristics = self.sections[section - 1]
if not characteristics & IMAGE_SCN_CNT_CODE:
continue
if storage == IMAGE_SYM_CLASS_STATIC and (kind >> 4) != 2 and value == 0:
continue
if name[:4] == b"\0\0\0\0":
at = strings + struct.unpack_from("<I", name, 4)[0]
end = data.index(b"\0", at)
text = data[at:end]
else:
text = name.rstrip(b"\0")
result.append((start + value, text.decode("utf-8", "replace")))
return result
def export_symbols(self, data, rva, size):
directory = self.offset(rva)
_, names, function_rva, name_rva, ordinal_rva = struct.unpack_from("<IIIII", data, directory + 20)
function_table = self.offset(function_rva)
result = []
if names:
name_table = self.offset(name_rva)
ordinal_table = self.offset(ordinal_rva)
for index in range(names):
ordinal = struct.unpack_from("<H", data, ordinal_table + index * 2)[0]
address = struct.unpack_from("<I", data, function_table + ordinal * 4)[0]
if rva <= address < rva + size:
continue
at = self.offset(struct.unpack_from("<I", data, name_table + index * 4)[0])
result.append((address, data[at:data.index(b"\0", at)].decode("utf-8", "replace")))
return result
def name(self, rva):
for start, end, addresses, names in self.code:
if start <= rva < end:
index = bisect.bisect_right(addresses, rva) - 1
return None if index < 0 else ("~" if self.exported else "") + names[index]
return None
class Symbolizer:
def __init__(self, libs, headers, demangle=False, warn=print, unverified=False):
self.files = {}
for directory in libs:
for path in Path(directory).iterdir():
if path.is_file():
self.files.setdefault(path.name.lower(), path)
self.headers = headers
self.modules = {}
self.demangle = demangle and shutil.which("c++filt") is not None
self.demangled = {}
self.warn = warn
self.unverified = unverified
def module(self, name):
if name in self.modules:
return self.modules[name]
module = None
path = self.files.get(name.lower())
if path is not None:
try:
module = PeModule(path)
except (ValueError, struct.error) as error:
self.warn(f"{name}: {error}")
if module is not None and name not in self.headers and not self.unverified:
self.warn(f"{name}: the samples have no '# module' record for it, so {path} may be another build; its frames stay unsymbolized (--unverified symbolizes them anyway)")
module = None
if module is not None and name in self.headers and self.headers[name] != (module.size, module.timestamp):
size, timestamp = self.headers[name]
self.warn(f"{name}: the sampled module had size {size:#x} and timestamp {timestamp:#x}, but {path} has {module.size:#x} and {module.timestamp:#x}; its frames stay unsymbolized")
module = None
self.modules[name] = module
return module
def frame(self, frame):
name, separator, offset = frame.partition("+0x")
if not separator:
return frame
module = self.module(name)
if module is None:
return frame
try:
symbol = module.name(int(offset, 16))
except ValueError:
return frame
if symbol is None:
return frame
return f"{name}!{self.pretty(symbol)}"
def pretty(self, symbol):
if not self.demangle or not symbol.lstrip("~").startswith("_Z"):
return symbol
if symbol not in self.demangled:
plain = symbol.lstrip("~")
result = subprocess.run(["c++filt", plain], capture_output=True, text=True)
text = result.stdout.strip() if result.returncode == 0 and result.stdout.strip() else plain
self.demangled[symbol] = ("~" if symbol.startswith("~") else "") + text
return self.demangled[symbol]
def read_headers(lines):
headers = {}
for line in lines:
parts = line.split()
if len(parts) == 5 and parts[:2] == ["#", "module"]:
headers[parts[2]] = (int(parts[3], 16), int(parts[4], 16))
return headers
class Samples:
def __init__(self):
self.headers = {}
self.stacks = collections.Counter()
self.self_counts = collections.Counter()
self.inclusive = collections.Counter()
self.total = collections.Counter()
def load(path, thread=None):
lines = Path(path).read_text(encoding="utf-8-sig").splitlines()
samples = Samples()
samples.headers = read_headers(lines)
current = None
for number, line in enumerate(lines, 1):
parts = line.split()
if len(parts) == 3 and parts[0] == "#" and parts[2] == "samples" and parts[1].isdigit():
samples.total["samples"] += int(parts[1])
continue
if line.startswith("#") or not line.strip():
continue
if len(parts) == 4 and parts[0] == "thread" and parts[2] == "samples" and parts[1].isdigit() and parts[3].isdigit():
current = parts[1]
if thread is None or current == thread:
samples.total["threads"] += int(parts[3])
continue
if len(parts) == 4 and parts[1].startswith("0x") and parts[2].isdigit() and parts[3].isdigit():
if thread is None or current == thread:
samples.self_counts[(parts[0], parts[1])] += int(parts[2])
samples.inclusive[(parts[0], parts[1])] += int(parts[3])
continue
frames, _, count = line.rpartition(" ")
if not frames or not count.isdigit():
raise ValueError(f"{path}:{number}: expected 'frame;...;frame count', 'module offset self inclusive' or 'thread <id> samples <n>'")
samples.stacks[frames] += int(count)
return samples
def subtract(later, earlier):
result = collections.Counter(later)
result.subtract(earlier)
return collections.Counter({key: count for key, count in result.items() if count > 0})
def window(later, earlier):
result = Samples()
result.headers = {**earlier.headers, **later.headers}
for name in ("stacks", "self_counts", "inclusive", "total"):
setattr(result, name, subtract(getattr(later, name), getattr(earlier, name)))
return result
def summarize(symbolizer, samples, top):
self_counts = collections.Counter()
inclusive = collections.Counter()
total = 0
if samples.stacks:
for frames, count in samples.stacks.items():
names = [symbolizer.frame(frame) for frame in frames.split(";")]
total += count
self_counts[names[-1]] += count
for name in set(names):
inclusive[name] += count
else:
for (module, offset), count in samples.self_counts.items():
self_counts[symbolizer.frame(f"{module}+{offset}")] += count
for (module, offset), count in samples.inclusive.items():
inclusive[symbolizer.frame(f"{module}+{offset}")] += count
total = samples.total["threads"] or samples.total["samples"] or sum(samples.self_counts.values())
lines = [f"{total} samples"]
for title, counter in (("self", self_counts), ("inclusive", inclusive)):
if not counter:
continue
lines.append(f"hottest by {title}:")
for name, count in counter.most_common(top):
lines.append(f" {count:8d} {100.0 * count / total if total else 0.0:5.1f}% {name}")
return lines
def main(argv=None):
parser = argparse.ArgumentParser(description=DESCRIPTION, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("samples", help="collapsed-stack file or address file written by a Windows sampler")
parser.add_argument("--libs", action="append", required=True, help="directory holding the modules that ran (repeatable)")
parser.add_argument("--since", help="an earlier copy of the same file, subtracted to keep the samples taken after it")
parser.add_argument("--out", help="write the symbolized collapsed stacks to this file")
parser.add_argument("--top", type=int, default=30, help="functions listed per table (default 30)")
parser.add_argument("--demangle", action="store_true", help="demangle C++ names with c++filt when it is on PATH")
parser.add_argument("--thread", help="keep only this thread of an APS5_SAMPLE_THREADS file")
parser.add_argument("--unverified", action="store_true", help="also symbolize modules the samples have no '# module' record for")
args = parser.parse_args(argv)
samples = load(args.samples, args.thread)
if args.since:
samples = window(samples, load(args.since, args.thread))
symbolizer = Symbolizer(args.libs, samples.headers, args.demangle, warn=lambda text: print("warning: " + text, file=sys.stderr), unverified=args.unverified)
for line in summarize(symbolizer, samples, args.top):
print(line)
if args.out:
if not samples.stacks:
parser.error("--out needs a collapsed-stack file")
with open(args.out, "w", encoding="utf-8") as output:
for frames, count in samples.stacks.items():
output.write(";".join(symbolizer.frame(frame) for frame in frames.split(";")) + f" {count}\n")
return 0
if __name__ == "__main__":
sys.exit(main())
+202
View File
@@ -0,0 +1,202 @@
import contextlib
import io
import struct
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import symbolize_samples
TIMESTAMP = 0x6AD92AC4
SIZE_OF_IMAGE = 0x3000
def symbol(name, value, section, kind, storage, aux=0):
return struct.pack("<8sIhHBB", name, value, section, kind, storage, aux)
def build_pe(path, with_symbols=True, with_exports=False, second_code=False):
image = bytearray(0xC00)
image[0:2] = b"MZ"
struct.pack_into("<I", image, 0x3C, 0x40)
image[0x40:0x44] = b"PE\0\0"
symbols = b""
strings = b""
if with_symbols:
long_name = b"AgcDriver_long_function_name\0"
strings = struct.pack("<I", 4 + len(long_name)) + long_name
symbols = b"".join([
symbol(b".text", 0, 1, 0, 3, aux=1), b"\0" * 18,
symbol(b"short", 0x10, 1, 0x20, 2),
symbol(b"\0\0\0\0" + struct.pack("<I", 4), 0x40, 1, 0x20, 2),
symbol(b"static_f", 0x80, 1, 0x20, 3),
symbol(b"data_obj", 0x08, 2, 0, 2),
] + ([symbol(b"late_fn", 0x40, 3, 0x20, 2)] if second_code else []))
symbol_count = len(symbols) // 18
struct.pack_into("<HHIIIHH", image, 0x44, 0x8664, 3 if second_code else 2, TIMESTAMP, 0x800 if with_symbols else 0, symbol_count, 240, 0x22)
optional = 0x58
struct.pack_into("<H", image, optional, 0x20B)
struct.pack_into("<Q", image, optional + 24, 0x140000000)
struct.pack_into("<I", image, optional + 56, SIZE_OF_IMAGE)
if with_exports:
struct.pack_into("<II", image, optional + 112, 0x2000, 0x100)
sections = optional + 240
struct.pack_into("<8sIIII", image, sections, b".text", 0x100, 0x1000, 0x100, 0x400)
struct.pack_into("<I", image, sections + 36, 0x60000020)
struct.pack_into("<8sIIII", image, sections + 40, b".rdata", 0x200, 0x2000, 0x200, 0x600)
struct.pack_into("<I", image, sections + 76, 0x40000040)
if second_code:
struct.pack_into("<8sIIII", image, sections + 80, b".text2", 0x100, 0x2800, 0x100, 0xA00)
struct.pack_into("<I", image, sections + 116, 0x60000020)
if with_exports:
directory = 0x600
struct.pack_into("<IIHHIIIIIII", image, directory, 0, 0, 0, 0, 0, 1, 2, 2, 0x2040, 0x2048, 0x2050)
struct.pack_into("<II", image, 0x640, 0x1020, 0x1060)
struct.pack_into("<II", image, 0x648, 0x2060, 0x2070)
struct.pack_into("<HH", image, 0x650, 0, 1)
image[0x660:0x660 + 11] = b"first_func\0"
image[0x670:0x670 + 12] = b"second_func\0"
if with_symbols:
image[0x800:0x800 + len(symbols)] = symbols
image[0x800 + len(symbols):0x800 + len(symbols) + len(strings)] = strings
Path(path).write_bytes(bytes(image))
class SymbolizeSamplesTests(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.root = Path(self.directory.name)
self.libs = self.root / "libs"
self.libs.mkdir()
build_pe(self.libs / "libSceAgcDriver.prx")
build_pe(self.libs / "libwinpthread-1.dll", with_symbols=False, with_exports=True)
def tearDown(self):
self.directory.cleanup()
def test_coff_symbols_name_code_addresses(self):
module = symbolize_samples.PeModule(self.libs / "libSceAgcDriver.prx")
self.assertEqual((module.size, module.timestamp), (SIZE_OF_IMAGE, TIMESTAMP))
self.assertFalse(module.exported)
self.assertIsNone(module.name(0x1005))
self.assertEqual(module.name(0x1010), "short")
self.assertEqual(module.name(0x103F), "short")
self.assertEqual(module.name(0x1045), "AgcDriver_long_function_name")
self.assertEqual(module.name(0x1085), "static_f")
self.assertIsNone(module.name(0x1100))
self.assertIsNone(module.name(0x2008))
self.assertNotIn("data_obj", module.names)
self.assertNotIn(".text", module.names)
def test_export_table_names_a_module_without_symbols(self):
module = symbolize_samples.PeModule(self.libs / "libwinpthread-1.dll")
self.assertTrue(module.exported)
self.assertEqual(module.name(0x1030), "~first_func")
self.assertEqual(module.name(0x1070), "~second_func")
def test_frames_keep_unknown_modules_and_out_of_range_addresses(self):
symbolizer = symbolize_samples.Symbolizer([self.libs], {}, warn=lambda text: None, unverified=True)
self.assertEqual(symbolizer.frame("libSceAgcDriver.prx+0x1046"), "libSceAgcDriver.prx!AgcDriver_long_function_name")
self.assertEqual(symbolizer.frame("LIBSCEAGCDRIVER.PRX+0x1011"), "LIBSCEAGCDRIVER.PRX!short")
self.assertEqual(symbolizer.frame("ntdll.dll+0x10"), "ntdll.dll+0x10")
self.assertEqual(symbolizer.frame("libSceAgcDriver.prx+0x10"), "libSceAgcDriver.prx+0x10")
self.assertEqual(symbolizer.frame("[truncated]"), "[truncated]")
def test_a_module_whose_size_or_timestamp_differs_stays_unsymbolized(self):
warnings = []
headers = {"libSceAgcDriver.prx": (SIZE_OF_IMAGE, TIMESTAMP + 1)}
symbolizer = symbolize_samples.Symbolizer([self.libs], headers, warn=warnings.append)
self.assertEqual(symbolizer.frame("libSceAgcDriver.prx+0x1011"), "libSceAgcDriver.prx+0x1011")
self.assertEqual(len(warnings), 1)
self.assertIn("timestamp", warnings[0])
matching = symbolize_samples.Symbolizer([self.libs], {"libSceAgcDriver.prx": (SIZE_OF_IMAGE, TIMESTAMP)}, warn=warnings.append)
self.assertEqual(matching.frame("libSceAgcDriver.prx+0x1011"), "libSceAgcDriver.prx!short")
def test_since_subtracts_an_earlier_snapshot_and_out_writes_symbolized_stacks(self):
header = f"# samples=9 depth=64 truncated=0\n# module libSceAgcDriver.prx {SIZE_OF_IMAGE:#x} {TIMESTAMP:#x}\n"
earlier = self.root / "worker-early.collapsed.txt"
later = self.root / "worker-late.collapsed.txt"
earlier.write_text(header + "ntdll.dll+0x1;libSceAgcDriver.prx+0x1011 4\nntdll.dll+0x1;libSceAgcDriver.prx+0x1081 2\n", encoding="utf-8")
later.write_text(header + "ntdll.dll+0x1;libSceAgcDriver.prx+0x1011 7\nntdll.dll+0x1;libSceAgcDriver.prx+0x1081 2\nntdll.dll+0x1;libSceAgcDriver.prx+0x1041 3\n", encoding="utf-8")
out = self.root / "steady.collapsed.txt"
printed = io.StringIO()
with contextlib.redirect_stdout(printed):
self.assertEqual(symbolize_samples.main([str(later), "--since", str(earlier), "--libs", str(self.libs), "--out", str(out), "--top", "5"]), 0)
text = printed.getvalue()
self.assertIn("6 samples", text)
self.assertIn("libSceAgcDriver.prx!short", text)
self.assertIn("libSceAgcDriver.prx!AgcDriver_long_function_name", text)
self.assertNotIn("static_f", text)
written = sorted(out.read_text(encoding="utf-8").splitlines())
self.assertEqual(written, ["ntdll.dll+0x1;libSceAgcDriver.prx!AgcDriver_long_function_name 3", "ntdll.dll+0x1;libSceAgcDriver.prx!short 3"])
def run_main(self, *arguments):
printed = io.StringIO()
errors = io.StringIO()
with contextlib.redirect_stdout(printed), contextlib.redirect_stderr(errors):
self.assertEqual(symbolize_samples.main([str(argument) for argument in arguments]), 0)
return printed.getvalue().splitlines(), errors.getvalue()
def test_address_files_are_summarized_by_self_and_inclusive_samples(self):
samples = self.root / "worker.txt"
samples.write_text(f"# 5 samples\nlibSceAgcDriver.prx 0x1011 3 9\nlibSceAgcDriver.prx 0x1012 1 4\nlibSceAgcDriver.prx 0x1045 0 5\nntdll.dll 0x10 1 1\n# module libSceAgcDriver.prx {SIZE_OF_IMAGE:#x} {TIMESTAMP:#x}\n", encoding="utf-8")
lines, _ = self.run_main(samples, "--libs", self.libs)
self.assertEqual(lines[0], "5 samples")
self.assertIn("4 80.0% libSceAgcDriver.prx!short", lines[2])
inclusive = lines[lines.index("hottest by inclusive:") + 1:]
self.assertIn("13 260.0% libSceAgcDriver.prx!short", inclusive[0])
self.assertIn("5 100.0% libSceAgcDriver.prx!AgcDriver_long_function_name", inclusive[1])
def test_a_symbol_does_not_reach_into_a_later_code_section(self):
build_pe(self.libs / "two.prx", second_code=True)
module = symbolize_samples.PeModule(self.libs / "two.prx")
self.assertEqual(module.name(0x1085), "static_f")
self.assertIsNone(module.name(0x2810))
self.assertEqual(module.name(0x2840), "late_fn")
self.assertEqual(module.name(0x28F0), "late_fn")
def test_a_module_without_a_record_stays_unsymbolized_unless_unverified(self):
samples = self.root / "worker.txt"
samples.write_text("# 4 samples\nlibSceAgcDriver.prx 0x1011 4 4\n", encoding="utf-8")
lines, errors = self.run_main(samples, "--libs", self.libs)
self.assertIn("libSceAgcDriver.prx+0x1011", lines[2])
self.assertIn("no '# module' record", errors)
lines, errors = self.run_main(samples, "--libs", self.libs, "--unverified")
self.assertIn("libSceAgcDriver.prx!short", lines[2])
self.assertEqual(errors, "")
def test_thread_files_keep_each_heading_and_can_select_one_thread(self):
samples = self.root / "threads.txt"
record = f"# module libSceAgcDriver.prx {SIZE_OF_IMAGE:#x} {TIMESTAMP:#x}\n"
samples.write_text("# 50 rounds\nthread 7 samples 10\n libSceAgcDriver.prx 0x1011 6 8\nthread 9 samples 40\n libSceAgcDriver.prx 0x1045 20 30\n libSceAgcDriver.prx 0x1011 2 12\n" + record, encoding="utf-8")
lines, _ = self.run_main(samples, "--libs", self.libs)
self.assertEqual(lines[0], "50 samples")
self.assertIn("20 40.0% libSceAgcDriver.prx!AgcDriver_long_function_name", lines[2])
lines, _ = self.run_main(samples, "--libs", self.libs, "--thread", "7")
self.assertEqual(lines[0], "10 samples")
self.assertIn("6 60.0% libSceAgcDriver.prx!short", lines[2])
self.assertNotIn("AgcDriver_long_function_name", "\n".join(lines))
def test_since_subtracts_both_counters_of_an_address_file(self):
record = f"# module libSceAgcDriver.prx {SIZE_OF_IMAGE:#x} {TIMESTAMP:#x}\n"
earlier = self.root / "early.txt"
later = self.root / "late.txt"
earlier.write_text("# 4 samples\nlibSceAgcDriver.prx 0x1011 4 6\n" + record, encoding="utf-8")
later.write_text("# 10 samples\nlibSceAgcDriver.prx 0x1011 7 12\nlibSceAgcDriver.prx 0x1045 3 3\n" + record, encoding="utf-8")
lines, _ = self.run_main(later, "--since", earlier, "--libs", self.libs)
self.assertEqual(lines[0], "6 samples")
self.assertIn("3 50.0% libSceAgcDriver.prx!short", "\n".join(lines[2:4]))
inclusive = lines[lines.index("hottest by inclusive:") + 1:]
self.assertIn("6 100.0% libSceAgcDriver.prx!short", inclusive[0])
def test_malformed_lines_are_rejected_with_their_line_number(self):
bad = self.root / "bad.collapsed.txt"
bad.write_text("# header\nlibSceAgcDriver.prx+0x1011 many\n", encoding="utf-8")
with self.assertRaisesRegex(ValueError, r"bad\.collapsed\.txt:2:"):
symbolize_samples.load(bad)
if __name__ == "__main__":
unittest.main()