chore: refresh validated graphics changes on current main

This commit is contained in:
Romans Pokrovskis committed 2026-11-04 19:39:48 +02:00
commit ef2bcde6b3
55 files changed
+1858 -227

No files matched your search

+1
View File
@@ -103,6 +103,7 @@ downloads/
!demo/web/i18n/*.json
!/docs/run-report.schema.json
!/docs/run-report-fixtures/*.json
!tools/titles.json
!/assets/*.jpg
!/assets/*.png
+3
View File
@@ -570,6 +570,9 @@ if(BUILD_TESTING)
add_test(NAME performance_compare COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_performance_compare.py)
set_tests_properties(performance_compare PROPERTIES TIMEOUT 20)
add_test(NAME title_run COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_title_run.py)
set_tests_properties(title_run PROPERTIES TIMEOUT 60)
endif()
endif()
+26
View File
@@ -488,6 +488,21 @@ if(BUILD_TESTING)
add_test(NAME nid_elf_layout COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_elf_layout.py $<TARGET_FILE:nid_patcher>)
endif()
add_test_executable(guest_kernel_sys_pwrite_tests tests/GuestKernelSysPwrite.cpp
nid/src/NidCompute.cpp nid/src/Sha1.cpp)
target_include_directories(guest_kernel_sys_pwrite_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/nid/include)
target_link_libraries(guest_kernel_sys_pwrite_tests PRIVATE ${CMAKE_DL_LIBS})
add_custom_target(guest_kernel_sys_pwrite_libraries DEPENDS
${PATCHED_DIR}/libkernel_sys.prx ${PATCHED_DIR}/libkernel.prx ${LIBC_PATCHED})
add_dependencies(guest_kernel_sys_pwrite_tests guest_kernel_sys_pwrite_libraries)
configure_windows_unwind(guest_kernel_sys_pwrite_tests)
if(MINGW)
target_compile_options(guest_kernel_sys_pwrite_tests PRIVATE -fasynchronous-unwind-tables)
target_link_options(guest_kernel_sys_pwrite_tests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
add_test(NAME guest_kernel_sys_pwrite COMMAND guest_kernel_sys_pwrite_tests ${UNPATCHED_DIR} unpatched)
add_test(NAME guest_kernel_sys_pwrite_patched COMMAND guest_kernel_sys_pwrite_tests ${PATCHED_DIR} patched)
set_tests_properties(guest_kernel_sys_pwrite guest_kernel_sys_pwrite_patched PROPERTIES TIMEOUT 20)
add_test_executable(guest_kernel_sys_tests tests/GuestKernelSys.cpp)
target_include_directories(guest_kernel_sys_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_kernel_sys_tests PRIVATE libkernel_sys libSceNet libSceAmpr libkernel libc)
@@ -915,6 +930,17 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceGameLiveStreaming>")
endif()
add_test_executable(guest_kernel_sys_revoke_tests tests/GuestKernelSysRevoke.cpp)
target_include_directories(guest_kernel_sys_revoke_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_kernel_sys_revoke_tests PRIVATE libkernel_sys libkernel libc)
configure_windows_unwind(guest_kernel_sys_revoke_tests)
add_test(NAME guest_kernel_sys_revoke COMMAND guest_kernel_sys_revoke_tests)
set_tests_properties(guest_kernel_sys_revoke PROPERTIES TIMEOUT 20)
if(WIN32)
set_tests_properties(guest_kernel_sys_revoke PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
endif()
add_test_executable(guest_netctl_ap_tests tests/GuestNetCtlAp.cpp)
target_include_directories(guest_netctl_ap_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_netctl_ap_tests PRIVATE libSceNetCtlAp libc)
@@ -4,7 +4,7 @@
#include <cstdint>
#include "SceTypes.hpp"
bool AgcDriverDeliverEopInterrupt(std::uint32_t queue);
void AgcDriverDeliverEopInterrupt(std::uint32_t queue, std::uint32_t context);
extern "C" int APS5_VABI sceAgcDriverAddEqEvent(KernelEqueue eq, int id, void* udata);
extern "C" int APS5_VABI sceAgcDriverDeleteEqEvent(KernelEqueue eq, int id);
@@ -33,9 +33,11 @@ int AddEvent(KernelEqueue eq, std::uint32_t queue, std::uintptr_t ident, void* u
event.event.ident = ident;
event.event.filter = EvfiltGraphicsCore;
event.event.flags = EV_ADD | EV_CLEAR;
event.event.data = static_cast<intptr_t>(queue);
event.event.udata = udata;
event.filter.triggerFunc = [](KernelEqueueEvent* e, void*) {
e->event.data = e->triggered ? e->event.data + 1 : 1;
event.filter.triggerFunc = [](KernelEqueueEvent* e, void* data) {
if (data == nullptr) throw std::runtime_error("graphics event triggered without its interrupt data");
e->event.data = static_cast<intptr_t>(*static_cast<const std::uint64_t*>(data));
e->triggered = true;
};
event.filter.resetFunc = [](KernelEqueueEvent* e) {
@@ -67,20 +69,17 @@ int DeleteEvent(KernelEqueue eq, std::uintptr_t ident, const char* caller) {
}
bool AgcDriverDeliverEopInterrupt(std::uint32_t queue) {
void AgcDriverDeliverEopInterrupt(std::uint32_t queue, std::uint32_t context) {
std::vector<Registration> registrations;
{
std::lock_guard lock(g_mutex);
registrations = g_registrations;
}
bool delivered = false;
for (const auto& registration : registrations) {
if (registration.queue == queue) {
EqueueTriggerEvent_nid_postfix(registration.eq, registration.ident, EvfiltGraphicsCore, nullptr);
delivered = true;
}
if (registration.queue != queue) continue;
std::uint64_t data = static_cast<std::uint64_t>(static_cast<std::uint8_t>(registration.queue)) | (static_cast<std::uint64_t>(context) << 16u);
EqueueTriggerEvent_nid_postfix(registration.eq, registration.ident, EvfiltGraphicsCore, &data);
}
return delivered;
}
extern "C" {
+2 -10
View File
@@ -17,22 +17,14 @@ uint32_t APS5_VABI sceAgcDriverGetEqContextId(const KernelEvent* ev) {
if (ev->filter != -14) {
throw std::runtime_error(std::string(__func__) + ": not a graphics event");
}
if (ev->ident > std::numeric_limits<uint32_t>::max()) {
throw std::runtime_error(std::string(__func__) + ": context id overflow");
}
return static_cast<uint32_t>(ev->ident);
return static_cast<uint32_t>(static_cast<std::uint64_t>(ev->data) >> 16u);
}
int APS5_VABI sceAgcDriverGetEqEventType(const KernelEvent* ev) {
if (ev == nullptr || reinterpret_cast<std::uintptr_t>(ev) % alignof(KernelEvent) != 0) {
throw std::runtime_error(std::string(__func__) + ": null or misaligned event");
}
if (ev->filter == -14) {
if (ev->ident > static_cast<std::uintptr_t>(std::numeric_limits<int>::max())) {
throw std::runtime_error(std::string(__func__) + ": graphics event type overflow");
}
return static_cast<int>(ev->ident);
}
if (ev->filter == -14) return static_cast<int>(static_cast<std::uint32_t>(static_cast<std::uint64_t>(ev->data)));
if (ev->data < std::numeric_limits<int>::min() || ev->data > std::numeric_limits<int>::max()) {
throw std::runtime_error(std::string(__func__) + ": event type overflow");
}
@@ -145,10 +145,13 @@ private:
static bool copySegment(Submission& submission, const std::uint32_t* guest, std::size_t words, std::size_t& budget);
static void readRegisterLists(Submission& submission);
void waitForFlipRoom(const Submission& submission);
void holdFlipBehindWorker(const Submission& submission);
void holdFlipBehindWorker(const Submission& submission, bool cpuFlip);
bool awaitsTitle(std::uint64_t awaited) const;
void noteAwaitingTitle(std::uint32_t queue, std::uint64_t awaited);
void releaseFlipHold();
void finishFlipWork(std::uint64_t workSerial);
std::uint64_t submitFlipWork(std::uint32_t queue);
void waitFlipWork(std::uint64_t workSerial);
void reserveOutputs(Submission& submission);
void executeRewindTail(const Submission& stalled);
void enqueue(Submission submission);
@@ -159,7 +162,7 @@ private:
void deferCompletion(const Submission& submission);
void submitOpenWork(const std::shared_ptr<SubmissionProgress>& progress = {});
void wakeQueueZeroForCompletions();
void completeSubmission(std::uint64_t serial, std::uint64_t received, std::span<const std::uint64_t> labelWrites, const std::shared_ptr<SubmissionProgress>& progress);
void completeSubmission(std::uint64_t serial, std::uint64_t received, std::span<const std::uint64_t> labelWrites, const std::shared_ptr<SubmissionProgress>& progress, bool holdsFlip);
static bool waitFree(const Submission& submission);
bool queue0Before(std::uint64_t received) const;
bool waitFreeBefore(std::uint64_t received) const;
@@ -298,12 +301,12 @@ private:
static std::vector<std::pair<std::uint64_t, std::uint64_t>> drawWriteRanges(const Graphics::State& graphics, std::span<const Graphics::CompiledShader> stages);
void commitDraw(std::shared_ptr<VulkanDevice> localDevice, std::uint32_t queue, const Graphics::State& graphics, const Pm4::DrawParameters& drawParameters, std::span<const Graphics::CompiledShader> stages, std::span<const Graphics::GuestMemorySnapshot> snapshots, const std::shared_ptr<const DrawRecipe>& recipe, const std::vector<std::shared_ptr<DispatchVariant>>& recipeStages, std::uint64_t drawKey, VulkanDevice::DrawTraceContext trace = {}, std::uint64_t shape = 0);
bool labelPacketQueues(std::span<const std::uint32_t> packet, std::uint32_t opcode);
bool enqueueLabelPacket(std::span<const std::uint32_t> packet, std::uint32_t opcode, std::uint32_t queue);
bool enqueueLabelPacket(std::span<const std::uint32_t> packet, std::uint32_t opcode, std::uint32_t queue, std::optional<std::uint32_t> interrupt);
bool enqueueDmaPacket(std::span<const std::uint32_t> packet, std::uint32_t opcode, std::uint32_t queue, const QueueState& state);
bool enqueueSampleDumpPacket(std::span<const std::uint32_t> packet, std::uint32_t opcode, std::uint32_t queue);
bool enqueueConstantRamDump(std::span<const std::uint32_t> packet, std::uint32_t opcode, std::uint32_t queue, const QueueState& state);
void commitStore(std::uint32_t queue, std::uint64_t address, std::span<const std::byte> bytes);
void commitLabel(std::uint32_t queue, std::uint64_t address, std::span<const std::byte> bytes, bool endOfPipeInterrupt);
void commitLabel(std::uint32_t queue, std::uint64_t address, std::span<const std::byte> bytes, std::optional<std::uint32_t> interrupt);
void submitDueAfterCommit(VulkanDevice* localDevice);
std::optional<WrittenBuffer> newestWriterLocked(std::uint64_t begin, std::uint64_t end) const;
std::optional<WrittenBuffer> newestWriter(std::uint64_t begin, std::uint64_t end);
@@ -383,15 +386,15 @@ private:
void recordQueuedLabelsByTry(std::uint32_t queue, std::atomic<std::uint64_t>& counter);
static void recordQueuedLabelsFromHook();
void flushBetweenPackets(std::uint32_t queue, std::uint32_t header, bool labelPacket);
bool preparePacketMemory(const Submission& submission, QueueState& queue, std::span<const std::uint32_t> packet, std::uint32_t header, std::uint32_t opcode, bool& wroteOnGpu, bool& endOfPipeInterrupt, bool& interruptDeferred, bool& drawPacket, bool& sampleDump);
bool preparePacketMemory(const Submission& submission, QueueState& queue, std::span<const std::uint32_t> packet, std::uint32_t header, std::uint32_t opcode, std::optional<std::uint32_t> interrupt, bool& wroteOnGpu, bool& endOfPipeInterrupt, bool& interruptDeferred, bool& drawPacket, bool& sampleDump);
void dumpSampleCounters(std::uint64_t address);
template <typename TWork>
static void timed(double WorkerProfile::*bucket, TWork&& work);
FrameTiming* frameTiming();
FrameTiming* includeTimingSubmission(const Submission& submission, bool firstSegment);
void execute(const Submission& submission);
void deliverGuestEopInterrupt(std::uint32_t queue);
void deliverFlipEopInterrupt(std::uint32_t queue);
void deliverGuestEopInterrupt(std::uint32_t queue, std::uint32_t context);
static std::optional<std::uint32_t> releaseInterrupt(std::span<const std::uint32_t> packet, std::uint32_t opcode, bool carriesFlip);
void markCompleted(std::uint64_t serial);
static const std::atomic<std::uint64_t>*& workerQueued();
void reapCompletionLabels();
@@ -417,9 +420,6 @@ private:
std::atomic<std::uint64_t> flipSerial{0};
std::atomic<std::uint64_t> flipBatchesUnsignaled{0};
std::mutex eopSelfSignalMutex;
std::unordered_map<std::uint32_t, std::uint32_t> eopSelfSignalFlips;
alignas(64) std::atomic<int> packetsInFlight{0};
std::atomic<std::uint64_t> packetsDone{0};
@@ -502,6 +502,8 @@ private:
std::atomic<std::uint64_t> queue0Awaited{0};
std::atomic<std::uint32_t> flipsAhead{0};
std::atomic<std::uint32_t> flipHolders{0};
std::atomic<std::uint32_t> flipWorkOpen{0};
std::atomic<std::uint64_t> flipWorkSerial{0};
std::atomic<bool> queue0AwaitsTitle{false};
std::atomic<std::uint64_t> evidenceReads{0};
@@ -19,13 +19,14 @@
#include <cstdlib>
#include <functional>
#include <shared_mutex>
#include <stdexcept>
#include <string>
#include <utility>
namespace AgcDriver::DriverDetail {
namespace {
constexpr std::uint32_t eopSelfSignalFlipLimit = 4;
struct RingOrder {
std::uint32_t queue = 0xffffffffu;
@@ -121,22 +122,18 @@ bool& drawnSinceSeal() {
}
void Driver::deliverGuestEopInterrupt(std::uint32_t queue) {
if (!AgcDriverDeliverEopInterrupt(queue)) return;
std::lock_guard<std::mutex> lock(eopSelfSignalMutex);
eopSelfSignalFlips[queue] = 0;
void Driver::deliverGuestEopInterrupt(std::uint32_t queue, std::uint32_t context) {
AgcDriverDeliverEopInterrupt(queue, context);
}
void Driver::deliverFlipEopInterrupt(std::uint32_t queue) {
{
std::lock_guard<std::mutex> lock(eopSelfSignalMutex);
const auto latched = eopSelfSignalFlips.find(queue);
if (latched != eopSelfSignalFlips.end()) {
if (++latched->second <= eopSelfSignalFlipLimit) return;
eopSelfSignalFlips.erase(latched);
}
}
AgcDriverDeliverEopInterrupt(queue);
std::optional<std::uint32_t> Driver::releaseInterrupt(std::span<const std::uint32_t> packet, std::uint32_t opcode, bool carriesFlip) {
if (opcode != 0x49 || carriesFlip || packet.size() < 3) return std::nullopt;
const auto interruptSelect = (packet[2] >> 24u) & 7u;
if (interruptSelect == 0 || interruptSelect == 3) return std::nullopt;
if (packet.size() < 8) throw std::runtime_error("AGC: an interrupting release has no interrupt context dword");
const auto context = packet[7] & 0x0fffffffu;
if ((context & 0x08000000u) != 0) throw std::runtime_error("AGC: a release asks for typed interrupt context " + std::to_string(context) + ", which is not delivered as a graphics end-of-pipe event; not implemented");
return context;
}
template <typename TWork>
@@ -162,14 +159,25 @@ void Driver::execute(const Submission& submission) {
auto* submissionTiming = includeTimingSubmission(submission, true);
struct FlipHold {
Driver& driver;
std::uint32_t queue;
bool held;
bool working;
void Release() {
if (!held) return;
held = false;
driver.releaseFlipHold();
}
~FlipHold() { Release(); }
} flipHold{*this, submission.holdsFlip};
void Finish() {
if (!working) return;
const auto workSerial = driver.submitFlipWork(queue);
working = false;
driver.finishFlipWork(workSerial);
}
~FlipHold() {
Release();
if (working) driver.finishFlipWork(0);
}
} flipHold{*this, submission.queue, submission.holdsFlip, submission.holdsFlip};
const bool pipelined = submission.queue == 0 && DrawPipeline::Depth() != 0;
static const bool orderedWaits = std::getenv("APS5_PIPELINE_DRAIN_LABELS") == nullptr;
DrawPipeline::Active() = pipelined;
@@ -294,7 +302,7 @@ void Driver::execute(const Submission& submission) {
if (submission.queue != 0 && Graphics::Recorder::DispatchOverlapEnabled()) noteRingPacket(submission.queue, submission.serial, opcode, header);
PerformanceTimer timing("Driver.Packet");
if (!predicated) noteLabelIssue(packet, submission.queue);
if (pipelined && !predicated && header != FlipPacketHeader && header != RenderingWaitPacketHeader && !submission.flips.contains(cursor) && (enqueueLabelPacket(packet, opcode, submission.queue) || enqueueDmaPacket(packet, opcode, submission.queue, queue) || enqueueConstantRamDump(packet, opcode, submission.queue, queue) || enqueueSampleDumpPacket(packet, opcode, submission.queue))) {
if (pipelined && !predicated && header != FlipPacketHeader && header != RenderingWaitPacketHeader && !submission.flips.contains(cursor) && (enqueueLabelPacket(packet, opcode, submission.queue, releaseInterrupt(packet, opcode, false)) || enqueueDmaPacket(packet, opcode, submission.queue, queue) || enqueueConstantRamDump(packet, opcode, submission.queue, queue) || enqueueSampleDumpPacket(packet, opcode, submission.queue))) {
traceLabel(packet, submission.queue);
recent.Record(cursor);
cursor = nextCursor;
@@ -385,7 +393,7 @@ void Driver::execute(const Submission& submission) {
if (profilePackets) packetProfile.flushMs += std::chrono::duration<double, std::milli>(packetTimer.start - flushStart).count();
bool wroteOnGpu = false, endOfPipeInterrupt = false, interruptDeferred = false, drawPacket = false, sampleDump = false;
const bool drains = preparePacketMemory(submission, queue, packet, header, opcode, wroteOnGpu, endOfPipeInterrupt, interruptDeferred, drawPacket, sampleDump);
const bool drains = preparePacketMemory(submission, queue, packet, header, opcode, releaseInterrupt(packet, opcode, submission.flips.contains(cursor)), wroteOnGpu, endOfPipeInterrupt, interruptDeferred, drawPacket, sampleDump);
traceLabel(packet, submission.queue);
timing.Mark("prepare_memory");
@@ -409,12 +417,10 @@ void Driver::execute(const Submission& submission) {
Pm4::Execute(packet, queue);
if (const auto label = Pm4::DecodeLabelWrite(packet)) noteLabelStore(label->address, label->Bytes(), ++eventSerial, submission.queue);
}
if (endOfPipeInterrupt && !interruptDeferred) deliverGuestEopInterrupt(submission.queue);
}
if (deviceUse.owns_lock()) deviceUse.unlock();
CheckFailure();
std::uint64_t batchesAtFlip = 0, unsignaledAtFlip = 0;
bool flipInterruptDeferred = false;
Graphics::StorageTexture::NoteFlip();
if (!drains) {
@@ -424,13 +430,8 @@ void Driver::execute(const Submission& submission) {
if (localDevice != nullptr) static_cast<void>(Graphics::StorageTexture::StoreDrawnLinear("flip"));
if (!recordLabelsForPacket(localDevice.get(), submission.queue) && localDevice != nullptr) localDevice->SubmitRecorded(submission.queue == 0);
if (localDevice != nullptr) {
localDevice->FlipBatches(batchesAtFlip, unsignaledAtFlip);
const auto queueId = submission.queue;
flipInterruptDeferred = localDevice->AfterRecordedWork([this, queueId] { deliverFlipEopInterrupt(queueId); }, false);
}
if (localDevice != nullptr) localDevice->FlipBatches(batchesAtFlip, unsignaledAtFlip);
}
if (!flipInterruptDeferred) deliverFlipEopInterrupt(submission.queue);
++flipsCounted;
Graphics::NoteHostImportFrame();
if (batchesAtFlip != 0) flipSerial = batchesAtFlip;
@@ -593,7 +594,7 @@ void Driver::execute(const Submission& submission) {
if (const auto label = Pm4::DecodeLabelWrite(packet)) noteLabelStore(label->address, label->Bytes(), ++eventSerial, submission.queue);
}
}
if (endOfPipeInterrupt && !interruptDeferred) deliverGuestEopInterrupt(submission.queue);
if (endOfPipeInterrupt && !interruptDeferred) deliverGuestEopInterrupt(submission.queue, packet[7] & 0x0fffffffu);
}
if (drawPacket || (sampleDump && wroteOnGpu)) Graphics::Recorder::CountRecordedWork();
timing.Mark(header == RenderingWaitPacketHeader ? "rendering_wait" : opcode == 0x3c || opcode == 0x93 ? "memory_wait" : opcode == 0x15 || opcode == 0x16 ? "dispatch" : drawPacket ? "draw" : "execute");
@@ -608,12 +609,15 @@ void Driver::execute(const Submission& submission) {
static const bool submitAtEnd = std::getenv("APS5_SUBMIT_AT_END") != nullptr;
if (pipelined && orderedCompletion && deferredLabels().labels.empty()) {
if (submission.rewindTail == nullptr) {
flipHold.working = false;
deferCompletion(submission);
return;
}
if ((std::atomic_ref<std::uint32_t>(*const_cast<std::uint32_t*>(submission.rewindTail - 1)).load(std::memory_order_acquire) & 0x80000000u) != 0) {
DrawPipeline::Queue0().Enqueue([this, progress = submission.progress] {
submitOpenWork(progress);
const bool holdsFlip = std::exchange(flipHold.working, false);
DrawPipeline::Queue0().Enqueue([this, holdsFlip, progress = submission.progress] {
if (holdsFlip) finishFlipWork(submitFlipWork(0));
else submitOpenWork(progress);
wakeQueueZeroForCompletions();
}, {});
endTiming.Finish();
@@ -626,6 +630,7 @@ void Driver::execute(const Submission& submission) {
auto& costs = submissionCosts(submission.queue);
if (!submitAtEnd && submission.rewindTail == nullptr && submission.queue == 0 && workerQueued() != nullptr && workerQueued()->load(std::memory_order_acquire) != 0) {
++costs.endSkipped;
flipHold.Finish();
return;
}
++costs.endSubmits;
@@ -636,6 +641,7 @@ void Driver::execute(const Submission& submission) {
recordDeferredLabels(localDevice.get(), submission.queue);
if (localDevice != nullptr) localDevice->SubmitRecorded(submission.queue == 0);
}
flipHold.Finish();
endTiming.Finish();
if (submission.rewindTail != nullptr) {
if (submission.progress) submitOpenWork(submission.progress);
@@ -54,15 +54,14 @@ std::optional<Pm4::MemoryCopy> decodeCopy(std::span<const std::uint32_t> packet)
}
bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue, std::span<const std::uint32_t> packet, std::uint32_t header, std::uint32_t opcode, bool& wroteOnGpu, bool& endOfPipeInterrupt, bool& interruptDeferred, bool& drawPacket, bool& sampleDump) {
bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue, std::span<const std::uint32_t> packet, std::uint32_t header, std::uint32_t opcode, std::optional<std::uint32_t> interrupt, bool& wroteOnGpu, bool& endOfPipeInterrupt, bool& interruptDeferred, bool& drawPacket, bool& sampleDump) {
static const bool drainAll = std::getenv("APS5_DRAIN_ALL") != nullptr;
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
wroteOnGpu = false;
bool orderedAlready = false;
const auto interruptSelect = opcode == 0x49 ? (packet[2] >> 24u) & 7u : 0u;
endOfPipeInterrupt = interruptSelect != 0 && interruptSelect != 3;
endOfPipeInterrupt = interrupt.has_value();
interruptDeferred = false;
if (!drainAll && endOfPipeInterrupt) {
const auto label = Pm4::DecodeLabelWrite(packet);
@@ -94,7 +93,8 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue
}
if (reason == 0 || reason == 5) {
const auto queueId = submission.queue;
interruptDeferred = localDevice->AfterRecordedWork([this, queueId] { deliverGuestEopInterrupt(queueId); }, submission.queue == 0);
const auto context = *interrupt;
interruptDeferred = localDevice->AfterRecordedWork([this, queueId, context] { deliverGuestEopInterrupt(queueId, context); }, submission.queue == 0);
if (interruptDeferred && workOpen) localDevice->SubmitRecorded(submission.queue == 0);
wroteOnGpu = true;
} else if (reason == 1) {
@@ -487,10 +487,9 @@ bool Driver::labelPacketQueues(std::span<const std::uint32_t> packet, std::uint3
return opcode == 0x49 && ((packet[2] >> 29u) == 0 || (packet[3] | (static_cast<std::uint64_t>(packet[4]) << 32u)) == 0);
}
bool Driver::enqueueLabelPacket(std::span<const std::uint32_t> packet, std::uint32_t opcode, std::uint32_t queue) {
bool Driver::enqueueLabelPacket(std::span<const std::uint32_t> packet, std::uint32_t opcode, std::uint32_t queue, std::optional<std::uint32_t> interrupt) {
if (!labelPacketQueues(packet, opcode)) return false;
const auto interruptSelect = opcode == 0x49 ? (packet[2] >> 24u) & 7u : 0u;
const bool endOfPipeInterrupt = interruptSelect != 0 && interruptSelect != 3;
const bool endOfPipeInterrupt = interrupt.has_value();
const auto label = Pm4::DecodeLabelWrite(packet);
if (!label.has_value()) {
if (opcode != 0x49) return false;
@@ -513,10 +512,10 @@ bool Driver::enqueueLabelPacket(std::span<const std::uint32_t> packet, std::uint
if (endOfPipeInterrupt) bumpEpoch(&EpochBumps::drains);
auto stored = bytes;
auto labelBytes = bytes;
auto enqueue =[this, queue, opcode, address, bytes = std::move(bytes), endOfPipeInterrupt, writes, stored = std::move(stored)]() mutable {
DrawPipeline::Queue0().Enqueue([this, queue, opcode, address, bytes = std::move(bytes), endOfPipeInterrupt] {
auto enqueue =[this, queue, opcode, address, bytes = std::move(bytes), interrupt, writes, stored = std::move(stored)]() mutable {
DrawPipeline::Queue0().Enqueue([this, queue, opcode, address, bytes = std::move(bytes), interrupt] {
GuestMemory::SetCurrentPacket(opcode, queue);
commitLabel(queue, address, bytes, endOfPipeInterrupt);
commitLabel(queue, address, bytes, interrupt);
}, std::move(writes), address, std::move(stored));
};
if (queue == 0 && DrawFinisher::Overlap() && DrawFinisher::Busy()) {
@@ -532,7 +531,7 @@ bool Driver::enqueueLabelPacket(std::span<const std::uint32_t> packet, std::uint
return true;
}
void Driver::commitLabel(std::uint32_t queue, std::uint64_t address, std::span<const std::byte> bytes, bool endOfPipeInterrupt) {
void Driver::commitLabel(std::uint32_t queue, std::uint64_t address, std::span<const std::byte> bytes, std::optional<std::uint32_t> interrupt) {
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Label);
std::lock_guard gpuLock(GuestMemory::GpuMutex());
const auto localDevice = device.Load();
@@ -554,14 +553,15 @@ void Driver::commitLabel(std::uint32_t queue, std::uint64_t address, std::span<c
} else {
++noOpLabels;
}
if (endOfPipeInterrupt) {
if (interrupt.has_value()) {
const auto context = *interrupt;
bool deferred = false;
if (bytes.empty() && localDevice != nullptr && queue == 0 && Graphics::StorageTexture::StoreDrawnLinear("interrupt") != 0) workOpen = true;
if (localDevice != nullptr && (reason == 0 || reason == 5 || reason == 6)) {
deferred = localDevice->AfterRecordedWork([this, queue] { deliverGuestEopInterrupt(queue); }, queue == 0);
deferred = localDevice->AfterRecordedWork([this, queue, context] { deliverGuestEopInterrupt(queue, context); }, queue == 0);
if (deferred && workOpen) localDevice->SubmitRecorded(queue == 0);
}
if (!deferred) deliverGuestEopInterrupt(queue);
if (!deferred) deliverGuestEopInterrupt(queue, context);
}
submitDueAfterCommit(localDevice.get());
}
@@ -70,8 +70,8 @@ void Driver::deferCompletion(const Submission& submission) {
queue0Uncommitted.push_back(received);
}
completionDeferred() = true;
DrawPipeline::Queue0().Enqueue([this, serial = submission.serial, received, labelWrites = submission.labelWrites, progress = submission.progress] {
completeSubmission(serial, received, labelWrites, progress);
DrawPipeline::Queue0().Enqueue([this, serial = submission.serial, received, labelWrites = submission.labelWrites, progress = submission.progress, holdsFlip = submission.holdsFlip] {
completeSubmission(serial, received, labelWrites, progress, holdsFlip);
}, {});
}
@@ -95,7 +95,15 @@ void Driver::wakeQueueZeroForCompletions() {
changed.notify_all();
}
void Driver::completeSubmission(std::uint64_t serial, std::uint64_t received, std::span<const std::uint64_t> labelWrites, const std::shared_ptr<SubmissionProgress>& progress) {
std::uint64_t Driver::submitFlipWork(std::uint32_t queue) {
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::End);
std::lock_guard gpuLock(GuestMemory::GpuMutex());
const auto localDevice = device.Load();
recordDeferredLabels(localDevice.get(), queue);
return localDevice != nullptr ? localDevice->SubmitAndEpoch() : 0;
}
void Driver::completeSubmission(std::uint64_t serial, std::uint64_t received, std::span<const std::uint64_t> labelWrites, const std::shared_ptr<SubmissionProgress>& progress, bool holdsFlip) {
struct Complete {
Driver& driver;
std::uint64_t serial, received;
@@ -113,7 +121,8 @@ void Driver::completeSubmission(std::uint64_t serial, std::uint64_t received, st
if (notify) driver.changed.notify_all();
}
} complete{*this, serial, received, labelWrites};
submitOpenWork(progress);
if (holdsFlip) finishFlipWork(submitFlipWork(0));
else submitOpenWork(progress);
}
const std::atomic<std::uint64_t>*& Driver::workerQueued() {
@@ -263,7 +263,7 @@ void Driver::noteAwaitingTitle(std::uint32_t queue, std::uint64_t awaited) {
changed.notify_all();
}
void Driver::holdFlipBehindWorker(const Submission& submission) {
void Driver::holdFlipBehindWorker(const Submission& submission, bool cpuFlip) {
if (submission.queue != 0 || onWorkerThread()) return;
bool candidate = false;
for (std::size_t cursor = 0; cursor < submission.commands.size() && !candidate; cursor += Pm4::PacketWords(submission.commands[cursor])) {
@@ -281,6 +281,39 @@ void Driver::holdFlipBehindWorker(const Submission& submission) {
flipHolders.fetch_add(1, std::memory_order_acq_rel);
changed.wait(lock, [&] { return failure != nullptr || stopping.load(std::memory_order_acquire) || shutdownToken.stop_requested() || flipsAhead.load(std::memory_order_acquire) == 0 || queue0AwaitsTitle.load(std::memory_order_acquire); });
flipHolders.fetch_sub(1, std::memory_order_acq_rel);
if (cpuFlip || failure != nullptr || stopping.load(std::memory_order_acquire) || shutdownToken.stop_requested() || flipsAhead.load(std::memory_order_acquire) != 0) return;
flipHolders.fetch_add(1, std::memory_order_acq_rel);
changed.wait(lock, [&] { return failure != nullptr || stopping.load(std::memory_order_acquire) || shutdownToken.stop_requested() || flipWorkOpen.load(std::memory_order_acquire) == 0 || queue0AwaitsTitle.load(std::memory_order_acquire); });
flipHolders.fetch_sub(1, std::memory_order_acq_rel);
if (failure != nullptr || stopping.load(std::memory_order_acquire) || shutdownToken.stop_requested() || flipWorkOpen.load(std::memory_order_acquire) != 0) return;
const auto workSerial = flipWorkSerial.load(std::memory_order_acquire);
lock.unlock();
waitFlipWork(workSerial);
}
void Driver::waitFlipWork(std::uint64_t workSerial) {
if (workSerial == 0) return;
auto localDevice = device.Load();
if (localDevice == nullptr) return;
if (localDevice->CanWaitUnlocked()) {
try {
localDevice->WaitRecorded(workSerial);
} catch (...) {
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Wait);
std::lock_guard gpuLock(GuestMemory::GpuMutex());
localDevice.reset();
throw;
}
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Wait);
std::lock_guard gpuLock(GuestMemory::GpuMutex());
localDevice->ReapRecorded(workSerial);
localDevice.reset();
return;
}
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Wait);
std::lock_guard gpuLock(GuestMemory::GpuMutex());
localDevice->WaitIdle();
localDevice.reset();
}
void Driver::releaseFlipHold() {
@@ -289,6 +322,13 @@ void Driver::releaseFlipHold() {
if (flipHolders.load(std::memory_order_acquire) != 0) changed.notify_all();
}
void Driver::finishFlipWork(std::uint64_t workSerial) {
std::lock_guard lock(mutex);
if (workSerial > flipWorkSerial.load(std::memory_order_relaxed)) flipWorkSerial.store(workSerial, std::memory_order_release);
flipWorkOpen.fetch_sub(1, std::memory_order_acq_rel);
if (flipHolders.load(std::memory_order_acquire) != 0) changed.notify_all();
}
void Driver::reserveOutputs(Submission& submission) {
std::set<std::uint32_t> eopTokens;
for (std::size_t cursor = 0; cursor < submission.commands.size();) {
@@ -470,7 +510,7 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue, bool cpuFlip) {
readRegisterLists(submission);
if (APS5_ENABLE_TIMING_LOG) submission.validatedAt = std::chrono::steady_clock::now();
waitForFlipRoom(submission);
holdFlipBehindWorker(submission);
holdFlipBehindWorker(submission, cpuFlip);
AwaitRegistrationPreparation();
if (LibraryPrebuildsWaiting()) PrioritizeLibraryPrebuilds(BoundGraphicsPrograms(submission));
if (APS5_ENABLE_TIMING_LOG) submission.roomReadyAt = std::chrono::steady_clock::now();
@@ -498,7 +538,6 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue, bool cpuFlip) {
submission.flips.begin()->second->GpuReady(frame);
lock.unlock();
Graphics::NoteHostImportFrame();
AgcDriverDeliverEopInterrupt(0);
return;
}
submission.shaders = shaders;
@@ -518,6 +557,7 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue, bool cpuFlip) {
if (queue == 0 && !submission.flips.empty()) {
submission.holdsFlip = true;
flipsAhead.fetch_add(1, std::memory_order_acq_rel);
flipWorkOpen.fetch_add(1, std::memory_order_acq_rel);
}
if (!submission.flips.empty()) ++flipSubmissionsOfThread();
enqueue(std::move(submission));
@@ -126,7 +126,7 @@ bool halvesRejoin() {
return rejoined == &word && fieldBack;
}
bool factorFieldKept(bool multiplied) {
bool factorFieldKept(bool multiplied, bool negated) {
IrProgram program;
auto& block = program.CreateBlock();
program.SetEntryBlock(block);
@@ -137,13 +137,14 @@ bool factorFieldKept(bool multiplied) {
auto& sum = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(1u), &lane});
auto& masked = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&sum, &builder.Constant(0xffffu)});
auto& field = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&masked, &builder.Constant(0u), &builder.Constant(24u)});
auto& used = multiplied ? builder.IMul(field, field) : field;
auto& factor = negated ? builder.Emit(IrOpcode::BitwiseXor32, IrType::U32, {&field, &builder.Constant(0x8000u)}) : field;
auto& used = multiplied ? builder.IMul(factor, factor) : factor;
auto& user = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&used});
static_cast<void>(builder.Emit(IrOpcode::Return, IrType::Void, {}));
ConstantFolder().Fold(program);
const IrValue* result = user.Argument(0)->Resolve();
if (!multiplied) return result == &masked;
return result->Opcode() == IrOpcode::IMul32 && result->Argument(0)->Resolve() == &field && field.Opcode() == IrOpcode::BitFieldUExtract && field.Argument(0)->Resolve() == &masked;
return result->Opcode() == IrOpcode::IMul32 && result->Argument(0)->Resolve() == &factor && field.Opcode() == IrOpcode::BitFieldUExtract && field.Argument(0)->Resolve() == &masked;
}
bool reversesTo(std::uint32_t word, std::uint32_t expected) {
@@ -323,8 +324,9 @@ int main() {
passed &= run("a mask under a merge of the halves reads through it", mergedHalfMask());
passed &= run("half extractions read the half that was merged in", halfExtractions());
passed &= run("the two halves of one word rejoin into the word", halvesRejoin());
passed &= run("a 24-bit field read of a value masked to 16 bits folds into the value", factorFieldKept(false));
passed &= run("a 24-bit field read that a multiply uses is kept, even for a factor masked to 16 bits", factorFieldKept(true));
passed &= run("a 24-bit field read of a value masked to 16 bits folds into the value", factorFieldKept(false, false));
passed &= run("a 24-bit field read that a multiply uses is kept, even for a factor masked to 16 bits", factorFieldKept(true, false));
passed &= run("a 24-bit field read that a multiply uses through a negating xor is kept, even for a factor masked to 16 bits", factorFieldKept(true, true));
return passed ? 0 : 1;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
+49 -2
View File
@@ -173,8 +173,11 @@ void testFlipAndBoundary() {
AgcDriverUnregisterVideoOutput_nid_postfix(7, output);
auto replacement = std::make_shared<Output>();
AgcDriverRegisterVideoOutput_nid_postfix(7, replacement);
submitFlip();
auto next = std::async(std::launch::async, [] { submitFlip(); });
check(next.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout, "a flip was accepted before the previous flip submission completed");
release.Release();
check(next.wait_for(std::chrono::seconds(5)) == std::future_status::ready, "a flip stayed held after the previous flip submission completed");
next.get();
AgcDriverWaitIdle_nid_postfix();
check(output->state->ready == 1 && replacement->state->ready == 1, "registration lifetime or FIFO was lost");
check(output->state->failed == 0, "successful request failed");
@@ -282,6 +285,50 @@ void testFlipHold() {
AgcDriverUnregisterVideoOutput_nid_postfix(7, output);
}
void testFlipHoldCoversTrailingLabel() {
auto output = std::make_shared<Output>();
AgcDriverRegisterVideoOutput_nid_postfix(7, output);
{
std::lock_guard lock(output->state->mutex);
output->state->gate = true;
output->state->waiting = false;
}
struct GateRelease {
std::shared_ptr<State> state;
~GateRelease() {
{
std::lock_guard lock(state->mutex);
state->gate = false;
}
state->changed.notify_all();
}
} gateRelease{output->state};
alignas(8) static std::atomic<std::uint32_t> label = 0;
label.store(0, std::memory_order_release);
const auto labelAddress = reinterpret_cast<std::uintptr_t>(&label);
std::array<std::uint32_t, 15> words{0xc004105c, 7, 0xfffffffeu, 1, 0, 0, 0xc0021018, 7, 0, 0, 0xc0033700, 0x04100200, static_cast<std::uint32_t>(labelAddress), static_cast<std::uint32_t>(static_cast<std::uint64_t>(labelAddress) >> 32u), 1};
Packet frame{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
check(sceAgcDriverSubmitDcb(&frame) == 0, "flip submission with a trailing label failed");
{
std::unique_lock lock(output->state->mutex);
check(output->state->changed.wait_for(lock, std::chrono::seconds(5), [&] { return output->state->waiting; }), "worker did not reach the rendering wait after the flip");
}
auto next = std::async(std::launch::async, [] { submitFlip(); });
check(next.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout, "a flip was accepted while the previous flip submission had not written its trailing label");
check(label.load(std::memory_order_acquire) == 0, "a label behind a closed rendering wait was written");
{
std::lock_guard lock(output->state->mutex);
output->state->gate = false;
}
output->state->changed.notify_all();
check(next.wait_for(std::chrono::seconds(5)) == std::future_status::ready, "a flip stayed held after the previous flip submission completed");
next.get();
check(label.load(std::memory_order_acquire) == 1, "a flip was accepted before the previous flip submission's trailing label landed");
AgcDriverWaitIdle_nid_postfix();
check(output->state->ready == 2, "flips around a trailing label were lost");
AgcDriverUnregisterVideoOutput_nid_postfix(7, output);
}
void testComputeRenderingWait() {
auto output = std::make_shared<Output>();
AgcDriverRegisterVideoOutput_nid_postfix(7, output);
@@ -457,7 +504,7 @@ int main(int argc, char** argv) {
return 0;
}
if (argc == 2) testReset(std::string(argv[1]) == "compute");
else { testFlipAndBoundary(); testFlipHold(); testComputeRenderingWait(); testComputeFlip(); testFailure(); }
else { testFlipAndBoundary(); testFlipHold(); testFlipHoldCoversTrailingLabel(); testComputeRenderingWait(); testComputeFlip(); testFailure(); }
const auto shutdown = expectFailure([] { LibcRunShutdown_nid_postfix(); });
check(shutdown.find(argc == 2 ? (std::string(argv[1]) == "compute" ? "not readable" : "required shader register") : "intentional flip failure") != std::string::npos, "shutdown lost worker failure");
std::puts("AGC flip and suspend tests passed");
+41 -52
View File
@@ -71,24 +71,20 @@ std::string expectFailure(TAction action) {
void testEvents() {
KernelEvent event{};
event.filter = -14;
event.ident = 0x40;
event.data = 123;
check(sceAgcDriverGetEqEventType(&event) == 0x40, "graphics event uses wrong field");
event.ident = 0x20;
event.data = (0x38 << 16) | 0x20;
check(sceAgcDriverGetEqEventType(&event) == ((0x38 << 16) | 0x20), "graphics event type is not the event data");
check(sceAgcDriverGetEqContextId(&event) == 0x38, "graphics event context id is not the data's upper bits");
event.data = static_cast<std::intptr_t>((std::uint64_t{0x0fffffff} << 16u) | 0x28);
check(sceAgcDriverGetEqContextId(&event) == 0x0fffffffu, "graphics event context id lost its upper bits");
check(sceAgcDriverGetEqEventType(&event) == static_cast<int>(0xffff0028u), "graphics event type is not the data's low word");
event.filter = -1;
event.data = -17;
check(sceAgcDriverGetEqEventType(&event) == -17, "non-graphics event uses wrong field");
event.data = std::numeric_limits<std::intptr_t>::max();
expectFailure([&] { sceAgcDriverGetEqEventType(&event); });
event.filter = -14;
event.ident = std::numeric_limits<std::uintptr_t>::max();
expectFailure([&] { sceAgcDriverGetEqEventType(&event); });
expectFailure([] { sceAgcDriverGetEqEventType(nullptr); });
expectFailure([&] { sceAgcDriverGetEqEventType(reinterpret_cast<const KernelEvent*>(reinterpret_cast<const std::byte*>(&event) + 1)); });
event.ident = 0x29;
check(sceAgcDriverGetEqContextId(&event) == 0x29, "graphics event context id uses wrong field");
event.ident = std::numeric_limits<std::uintptr_t>::max();
expectFailure([&] { sceAgcDriverGetEqContextId(&event); });
event.ident = 1;
event.filter = -1;
expectFailure([&] { sceAgcDriverGetEqContextId(&event); });
expectFailure([] { sceAgcDriverGetEqContextId(nullptr); });
@@ -238,58 +234,47 @@ void testEndOfPipeInterrupts() {
AgcDriverWaitIdle_nid_postfix();
check(output->ready.load() == 1, "flip-only queue flip was not handed to its output");
check(AgcDriver::Graphics::HostImportFramesEnded() == 1, "the first flip did not end a host import frame");
check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &graphicsTag && events[0].data == 1 && sceAgcDriverGetEqEventType(events.data()) == 0, "flip on a queue that never requested a release interrupt did not raise its event");
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip-only queue event was not cleared");
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "a flip raised the graphics end-of-pipe event");
std::array<std::uint32_t, 8> words{0xc0064900, 0, 1u << 24u, 0, 0, 0, 0, 0};
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
check(sceAgcDriverSubmitDcb(&packet) == 0 && sceAgcDriverSubmitDcb(&packet) == 0, "interrupt submit failed");
AgcDriverWaitIdle_nid_postfix();
check(owner->GetTriggeredEvents(events.data(), 2) == 1, "graphics end-of-pipe interrupt was not delivered to its queue only");
check(events[0].filter == -14 && events[0].udata == &graphicsTag && events[0].data == 2 && sceAgcDriverGetEqEventType(events.data()) == 0, "graphics end-of-pipe event encoding is wrong");
check(events[0].filter == -14 && events[0].udata == &graphicsTag && events[0].data == 0 && sceAgcDriverGetEqEventType(events.data()) == 0, "graphics end-of-pipe event encoding is wrong");
check(sceAgcDriverGetEqContextId(events.data()) == 0, "graphics end-of-pipe event reports another context");
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "delivered interrupt was not cleared");
words[7] = 0x38;
check(sceAgcDriverSubmitDcb(&packet) == 0, "interrupt submit with a context failed");
AgcDriverWaitIdle_nid_postfix();
check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &graphicsTag && events[0].data == (0x38 << 16), "graphics end-of-pipe event does not carry the release's interrupt context");
check(sceAgcDriverGetEqContextId(events.data()) == 0x38 && sceAgcDriverGetEqEventType(events.data()) == (0x38 << 16), "graphics end-of-pipe event reports another context");
words[7] = 0xf0000041u;
check(sceAgcDriverSubmitDcb(&packet) == 0, "interrupt submit with high context bits failed");
AgcDriverWaitIdle_nid_postfix();
check(owner->GetTriggeredEvents(events.data(), 2) == 1 && sceAgcDriverGetEqContextId(events.data()) == 0x41, "the interrupt context kept bits above its 28 bits");
words[7] = 0x38;
check(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "compute interrupt submit failed");
AgcDriverWaitIdle_nid_postfix();
check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &computeTag && sceAgcDriverGetEqEventType(events.data()) == 0x20, "compute end-of-pipe interrupt missing");
check(sceAgcDriverGetEqContextId(events.data()) == 0x20, "compute end-of-pipe event reports another context");
check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &computeTag && events[0].data == ((0x38 << 16) | 0x20) && sceAgcDriverGetEqEventType(events.data()) == ((0x38 << 16) | 0x20), "compute end-of-pipe interrupt missing");
check(sceAgcDriverGetEqContextId(events.data()) == 0x38, "compute end-of-pipe event reports another context");
words[7] = 0;
words[2] = 0;
check(sceAgcDriverSubmitDcb(&packet) == 0, "plain release submit failed");
AgcDriverWaitIdle_nid_postfix();
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "release without INT_SEL raised an interrupt");
check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip after a release interrupt submit failed");
AgcDriverWaitIdle_nid_postfix();
check(output->ready.load() == 2, "flip after a release interrupt was not handed to its output");
check(AgcDriver::Graphics::HostImportFramesEnded() == 2, "a flip packet executed on the queue did not end a host import frame");
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip on a queue that already raised its own release interrupt raised a second event");
words[2] = 1u << 24u;
check(sceAgcDriverSubmitDcb(&packet) == 0, "release before sustained flips submit failed");
AgcDriverWaitIdle_nid_postfix();
check(owner->GetTriggeredEvents(events.data(), 2) == 1, "a release interrupt did not deliver on a queue whose flip event is suppressed");
std::uint32_t sustainedFlips = 0;
std::uint32_t flipsUntilEvent = 0;
while (sustainedFlips < 8 && flipsUntilEvent == 0) {
check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "sustained flip submit failed");
for (int index = 0; index < 8; ++index) {
check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip after a release submit failed");
AgcDriverWaitIdle_nid_postfix();
++sustainedFlips;
check(output->ready.load() == static_cast<int>(2 + sustainedFlips), "sustained flip was not handed to its output");
if (owner->GetTriggeredEvents(events.data(), 2) != 0) flipsUntilEvent = sustainedFlips;
check(output->ready.load() == 2 + index, "flip after a release was not handed to its output");
check(AgcDriver::Graphics::HostImportFramesEnded() == static_cast<std::uint64_t>(2 + index), "a flip packet executed on the queue did not end a host import frame");
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "a flip after a release raised the graphics end-of-pipe event");
}
check(flipsUntilEvent != 0, "a flip never raised an event again on a queue whose own release interrupts stopped");
check(flipsUntilEvent > 2, "a flip raised an event within two flips of the queue's own release interrupt");
check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip after the latch cleared submit failed");
words[2] = 2u << 24u;
words[7] = AgcDriverReserveEopFlip_nid_postfix(AgcDriver::FlipInfo{7, 0, 1, 0});
check(sceAgcDriverSubmitDcb(&packet) == 0, "end-of-pipe flip release submit failed");
AgcDriverWaitIdle_nid_postfix();
check(output->ready.load() == static_cast<int>(3 + sustainedFlips), "flip after the latch cleared was not handed to its output");
check(owner->GetTriggeredEvents(events.data(), 2) == 1, "a flip did not raise an event after the latch cleared");
words[2] = 1u << 24u;
check(sceAgcDriverSubmitDcb(&packet) == 0, "release after a cleared latch submit failed");
AgcDriverWaitIdle_nid_postfix();
check(owner->GetTriggeredEvents(events.data(), 2) == 1, "a release interrupt did not deliver after the latch cleared");
for (int index = 0; index < 2; ++index) {
check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip paired with a release interrupt submit failed");
AgcDriverWaitIdle_nid_postfix();
check(output->ready.load() == static_cast<int>(4 + sustainedFlips + index), "flip paired with a release interrupt was not handed to its output");
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "a flip raised an event within two flips of the queue's own release interrupt");
}
check(output->ready.load() == 10, "end-of-pipe flip was not handed to its output");
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "a release that carries a flip raised the graphics end-of-pipe event");
const auto framesBefore = AgcDriver::Graphics::HostImportFramesEnded();
const auto readyBefore = output->ready.load();
std::thread([] { AgcDriverSubmitFlip_nid_postfix(AgcDriver::FlipInfo{7, -1, 1, 0}); }).join();
@@ -354,9 +339,9 @@ std::vector<std::uint32_t> commands(const std::array<std::uint32_t, N>&... packe
return words;
}
std::array<std::uint32_t, 8> endOfPipeLabel(volatile std::uint32_t* address, std::uint32_t value) {
std::array<std::uint32_t, 8> endOfPipeLabel(volatile std::uint32_t* address, std::uint32_t value, std::uint32_t context) {
const auto target = reinterpret_cast<std::uintptr_t>(address);
return {0xc0064900, 0x514, (1u << 29u) | (2u << 24u), static_cast<std::uint32_t>(target), static_cast<std::uint32_t>(static_cast<std::uint64_t>(target) >> 32u), value, 0, 0};
return {0xc0064900, 0x514, (1u << 29u) | (2u << 24u), static_cast<std::uint32_t>(target), static_cast<std::uint32_t>(static_cast<std::uint64_t>(target) >> 32u), value, 0, context};
}
void testEndOfPipeLabelsWithoutWork() {
@@ -367,12 +352,12 @@ void testEndOfPipeLabelsWithoutWork() {
int tag = 0;
check(sceAgcDriverAddEqEvent(eq, 0, &tag) == 0, "graphics event registration failed");
submit(0x20, commands(waitEqual(&second, 2), writeData(&done, 1)));
submit(0, commands(endOfPipeLabel(&first, 1), endOfPipeLabel(&second, 2)));
submit(0, commands(endOfPipeLabel(&first, 1, 0x11), endOfPipeLabel(&second, 2, 0x12)));
waitFor(&done, 1, "an end-of-pipe label with no work before it never landed");
check(first == 1 && second == 2, "end-of-pipe labels with no work before them landed wrong");
AgcDriverWaitIdle_nid_postfix();
std::array<KernelEvent, 2> events{};
check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &tag && events[0].data == 2, "end-of-pipe labels with no work before them lost their interrupts");
check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &tag && sceAgcDriverGetEqContextId(events.data()) == 0x12, "end-of-pipe labels with no work before them lost their interrupts");
check(sceAgcDriverDeleteEqEvent(eq, 0) == 0, "graphics event deletion failed");
owner.reset();
check(sceKernelDeleteEqueue(eq) == 0, "event queue deletion failed");
@@ -1121,7 +1106,11 @@ int main(int argc, char** argv) {
try {
if (argc == 2 && std::string_view(argv[1]) == "--eop-interrupts") {
testEndOfPipeInterrupts();
LibcRunShutdown_nid_postfix();
std::array<std::uint32_t, 8> typed{0xc0064900, 0, 1u << 24u, 0, 0, 0, 0, 0x08000038u};
Packet typedPacket{typed.data(), static_cast<std::uint32_t>(typed.size()), 0, {}};
check(sceAgcDriverSubmitDcb(&typedPacket) == 0, "typed interrupt context submit failed");
check(expectFailure([] { AgcDriverWaitIdle_nid_postfix(); }).find("typed interrupt context") != std::string::npos, "a release with a typed interrupt context did not fail");
check(expectFailure([] { LibcRunShutdown_nid_postfix(); }).find("typed interrupt context") != std::string::npos, "shutdown lost the typed interrupt context failure");
std::puts("AGC end-of-pipe interrupt queue mode tests passed");
return 0;
}
@@ -56,6 +56,18 @@ alignas(256) constexpr std::array<std::uint32_t, 18> PackedHighLaneCode{
0x80010a03, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 18> PackedNegatedLowLaneCode{
0x34020082, 0x34060080, 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601,
0xbf8c3f70, 0x360808ff, 0x0000ffff, 0x360a0aff, 0x0000ffff, 0xcc01400a, 0x38020b04, 0xe0702000,
0x80010a03, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 18> PackedNegatedHighLaneCode{
0x34020082, 0x34060080, 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601,
0xbf8c3f70, 0x360808ff, 0xffff0000, 0x360a0aff, 0xffff0000, 0xcc01410a, 0x18020b04, 0xe0702000,
0x80010a03, 0xbf810000,
};
constexpr std::array<std::array<std::uint32_t, 3>, 8> Edges{{
{0xffffffffu, 0xffffffffu, 0x00000000u}, {0x0000ffffu, 0x0000ffffu, 0xabcd1234u}, {0xffff0000u, 0xffff0000u, 0xffffffffu},
{0x8000ffffu, 0x7fff8000u, 0x12345678u}, {0x12345678u, 0x9abcdef0u, 0x80000000u}, {0x00010001u, 0x00010001u, 0x0000ffffu},
@@ -136,6 +148,10 @@ int main() {
Check("v_pk_mul_lo_u16 of factors whose high halves are zero", [](std::uint32_t a, std::uint32_t b, std::uint32_t) { return Low16Product(a, b); });
Run(*device, PackedHighLaneCode);
Check("v_pk_mul_lo_u16 of factors whose low halves are zero", [](std::uint32_t a, std::uint32_t b, std::uint32_t) { return Low16Product(a >> 16u, b >> 16u) << 16u; });
Run(*device, PackedNegatedLowLaneCode);
Check("v_pk_mul_lo_u16 neg_lo:[1,0] of factors whose high halves are zero", [](std::uint32_t a, std::uint32_t b, std::uint32_t) { return Low16Product(a ^ 0x8000u, b); });
Run(*device, PackedNegatedHighLaneCode);
Check("v_pk_mul_lo_u16 neg_hi:[1,0] of factors whose low halves are zero", [](std::uint32_t a, std::uint32_t b, std::uint32_t) { return Low16Product((a >> 16u) ^ 0x8000u, b >> 16u) << 16u; });
std::puts("mul lo u16 high half tests passed");
return 0;
} catch (const std::exception& error) {
+30 -3
View File
@@ -68,9 +68,36 @@ int APS5_VABI sceAjmBatchJobEncode(AjmBatchInfo* info, uint32_t instance, const
}
const char* APS5_VABI sceAjmStrError(int error) {
(void)error;
AjmStub(__func__);
return nullptr;
static const char* const messages[] = {
"context does not exist",
"instance does not exist",
"batch does not exist",
"invalid parameter",
"not enough memory",
"not enough resources",
"codec is not supported",
"codec is already registered in the context",
"codec is not registered in the context",
"wrong revision in the flags",
"flag is not supported",
"busy",
"priority is invalid",
"in progress",
"operation cannot be performed at the moment",
"malformed batch",
"error at job creation",
"invalid opcode",
"priority violation",
"buffer is too big",
"invalid address",
"batch was cancelled",
"specified memory area is already registered",
"specified memory area is not registered",
};
if (error == 0) return "ok";
const std::uint32_t index = static_cast<std::uint32_t>(error) - 0x80930002u;
if (index < sizeof(messages) / sizeof(messages[0])) return messages[index];
return "(unknown)";
}
int APS5_VABI sceAjmDecWVorbisCreateHeaderPacket(WVorbisHeaderPacketParameter* param) {
@@ -29,4 +29,13 @@ if(BUILD_TESTING)
set_tests_properties(guest_sysmodule PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSysmodule>")
endif()
add_test_executable(guest_sysmodule_invalid_ids_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/InvalidIds.cpp)
target_include_directories(guest_sysmodule_invalid_ids_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_sysmodule_invalid_ids_tests PRIVATE libSceSysmodule libkernel libc)
configure_windows_unwind(guest_sysmodule_invalid_ids_tests)
add_test(NAME guest_sysmodule-invalid-ids COMMAND guest_sysmodule_invalid_ids_tests)
if(WIN32)
set_tests_properties(guest_sysmodule-invalid-ids PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSysmodule>")
endif()
endif()
+13
View File
@@ -36,6 +36,10 @@ const char* findModuleName(const std::uint32_t id) {
return it != kModuleTable.end() ? it->second : nullptr;
}
bool isMeasuredRejectedInternalId(const std::uint32_t id) {
return id == 0x8000ffffu || id == 0x80ffff00u;
}
bool isPublicModuleId(const std::uint16_t id) {
return std::binary_search(std::begin(kPublicModuleIds), std::end(kPublicModuleIds), id);
}
@@ -280,6 +284,9 @@ int APS5_VABI sceSysmoduleLoadModuleInternal(std::uint32_t id) {
return SCE_SYSMODULE_ERROR_INVALID_VALUE;
}
if (!findModuleName(id)) {
if (isMeasuredRejectedInternalId(id)) {
return SCE_SYSMODULE_ERROR_INVALID_VALUE;
}
throw std::runtime_error(std::string("sceSysmoduleLoadModuleInternal: unknown id ") + std::to_string(id));
}
std::lock_guard<std::mutex> lock(gMutex);
@@ -292,6 +299,9 @@ int APS5_VABI sceSysmoduleUnloadModuleInternal(std::uint32_t id) {
return SCE_SYSMODULE_ERROR_INVALID_VALUE;
}
if (!findModuleName(id)) {
if (isMeasuredRejectedInternalId(id)) {
return SCE_SYSMODULE_ERROR_INVALID_VALUE;
}
throw std::runtime_error(std::string("sceSysmoduleUnloadModuleInternal: unknown id ") + std::to_string(id));
}
std::lock_guard<std::mutex> lock(gMutex);
@@ -330,6 +340,9 @@ int APS5_VABI sceSysmoduleIsLoadedInternal(std::uint32_t id) {
return SCE_SYSMODULE_ERROR_INVALID_VALUE;
}
if (!findModuleName(id)) {
if (isMeasuredRejectedInternalId(id) || id == 0x00ffffffu) {
return SCE_SYSMODULE_ERROR_INVALID_VALUE;
}
throw std::runtime_error(std::string("sceSysmoduleIsLoadedInternal: unknown id ") + std::to_string(id));
}
std::lock_guard<std::mutex> lock(gMutex);
@@ -0,0 +1,25 @@
#include "prx/libSceSysmodule/include/Api.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include <cstdint>
#include <cstdlib>
namespace {
constexpr int kInvalidValue = static_cast<int>(0x805A1000);
void Require(bool value) { if (!value) std::abort(); }
void CheckInternalIdRejected(std::uint32_t id) {
Require(sceSysmoduleIsLoadedInternal(id) == kInvalidValue);
Require(sceSysmoduleLoadModuleInternal(id) == kInvalidValue);
Require(sceSysmoduleIsLoadedInternal(id) == kInvalidValue);
Require(sceSysmoduleUnloadModuleInternal(id) == kInvalidValue);
}
}
int main() {
CheckInternalIdRejected(0x80ffff00);
CheckInternalIdRejected(0x8000ffff);
Require(sceSysmoduleIsLoadedInternal(0x00ffffff) == kInvalidValue);
}
+7 -10
View File
@@ -146,7 +146,6 @@ void testLifetime(bool reopen) {
} else {
check(expectFailure([&] { sceVideoOutWaitVblank(handle); }).find("closed") != std::string::npos, "closed port accepted a vblank wait");
}
AgcDriverUnregisterVideoOutput_nid_postfix(7, gate);
if (reopen) sceVideoOutClose(handle);
LibcRunShutdown_nid_postfix();
}
@@ -223,13 +222,12 @@ void testPs4Buffers() {
void testDeferredBuffer() {
const int handle = sceVideoOutOpen(255, 0, 0, nullptr);
auto cfg = VideoOutDriver::Get().GetConfig(handle);
auto gate = std::make_shared<Gate>();
AgcDriverRegisterVideoOutput_nid_postfix(7, gate);
std::array<uint32_t, 6> held{0xc004105c, 7, 0xfffffffeu, 1, 0, 0};
Packet heldPacket{held.data(), 6, 0, {}};
check(sceAgcDriverSubmitDcb(&heldPacket) == 0, "the gated flip was not accepted");
std::array<uint32_t, 6> flip{0xc004105c, static_cast<uint32_t>(handle), 0, 1, 3, 0};
Packet flipPacket{flip.data(), 6, 0, {}};
alignas(8) static std::uint32_t reached = 0;
reached = 0;
const auto label = reinterpret_cast<std::uintptr_t>(&reached);
std::array<uint32_t, 13> flip{0xc0053c00u, 0x13u, static_cast<uint32_t>(label), static_cast<uint32_t>(label >> 32u), 1u, 0xffffffffu, 0u,
0xc004105c, static_cast<uint32_t>(handle), 0, 1, 3, 0};
Packet flipPacket{flip.data(), 13, 0, {}};
check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "a GPU flip of a buffer registered before the GPU reaches it was rejected at submit");
std::vector<std::byte> allocation(65536 + 65535);
const auto storage = alignedBuffer(allocation);
@@ -237,7 +235,7 @@ void testDeferredBuffer() {
VideoOutBufferAttribute2 attribute{};
sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 64, 64, 0, 0, 0);
check(sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr) == 0, "buffer registration failed");
gate->Release();
std::atomic_ref<std::uint32_t>(reached).store(1u, std::memory_order_release);
{
std::unique_lock lock(cfg->mutex);
const bool done = cfg->vblankCond.wait_for(lock, std::chrono::seconds(30), [&] { return cfg->failure || cfg->flipStatus.count == 1; });
@@ -248,7 +246,6 @@ void testDeferredBuffer() {
AgcDriverWaitIdle_nid_postfix();
sceVideoOutUnregisterBuffers(handle, 0);
sceVideoOutClose(handle);
AgcDriverUnregisterVideoOutput_nid_postfix(7, gate);
LibcRunShutdown_nid_postfix();
}
+1 -1
View File
@@ -1554,7 +1554,7 @@ int APS5_VABI dl_iterate_phdr_nid_postfix(int (*callback)(dl_phdr_info*, std::si
[[noreturn]] void APS5_VABI _Exit_nid_postfix(int code);
[[noreturn]] void APS5_VABI catchReturnFromMain_nid_postfix(int status);
[[noreturn]] void APS5_VABI quick_exit_nid_postfix(int status);
[[noreturn]] void APS5_VABI _ZSt14_Throw_C_errori_nid_postfix(int code);
void APS5_VABI _ZSt14_Throw_C_errori_nid_postfix(int code);
[[noreturn]] char* APS5_VABI fgetln_nid_postfix(FileStream*, std::size_t*);
[[noreturn]] void APS5_VABI _Assert_nid_postfix(const char* message, const char* location);
[[noreturn]] void APS5_VABI _ZSt16_Throw_Cpp_errori_nid_postfix(int code);
+27 -2
View File
@@ -147,8 +147,33 @@ unsigned int APS5_VABI _ZSt14_Random_devicev_nid_postfix() {
return value;
}
[[noreturn]] void APS5_VABI _ZSt14_Throw_C_errori_nid_postfix(int code) {
throw std::runtime_error("_Throw_C_error: C11 thread error " + std::to_string(code));
static constexpr int ThrdNomem = 1;
static constexpr int ThrdTimedout = 2;
static constexpr int ThrdBusy = 3;
static constexpr int ThrdError = 4;
static constexpr int ThrdPermission = 5;
static constexpr int CppDeviceOrResourceBusy = 0;
static constexpr int CppInvalidArgument = 1;
static constexpr int CppOperationNotPermitted = 4;
static constexpr int CppResourceUnavailableTryAgain = 6;
[[noreturn]] void ThrowCppError_nid_no_patch(int code);
void APS5_VABI _ZSt14_Throw_C_errori_nid_postfix(int code) {
switch (code) {
case ThrdNomem:
case ThrdTimedout:
ThrowCppError_nid_no_patch(CppResourceUnavailableTryAgain);
case ThrdBusy:
ThrowCppError_nid_no_patch(CppDeviceOrResourceBusy);
case ThrdError:
ThrowCppError_nid_no_patch(CppInvalidArgument);
case ThrdPermission:
ThrowCppError_nid_no_patch(CppOperationNotPermitted);
default:
return;
}
}
void APS5_VABI _Lockfilelock_nid_postfix(FileStream* stream) {
+2 -11
View File
@@ -488,17 +488,8 @@ static_assert(offsetof(GuestPad, condition) == 0x8 && offsetof(GuestPad, mutex)
[[noreturn]] void ThrowCppError_nid_no_patch(int code);
static void RequireThrd(int result, const char* what) {
switch (result) {
case threadSuccess:
return;
case threadNomem:
case threadTimedout:
ThrowCppError_nid_no_patch(6);
case threadBusy:
ThrowCppError_nid_no_patch(0);
case threadError:
ThrowCppError_nid_no_patch(1);
}
if (result == threadSuccess) return;
_ZSt14_Throw_C_errori_nid_postfix(result);
throw std::runtime_error(std::string(what) + " failed with " + std::to_string(result));
}
+23 -9
View File
@@ -1675,15 +1675,29 @@ if(BUILD_TESTING)
target_include_directories(guest_named_shm_windows_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include)
target_link_libraries(guest_named_shm_windows_tests PRIVATE libkernel libc advapi32)
configure_windows_unwind(guest_named_shm_windows_tests)
add_test(NAME guest_named_shm_windows COMMAND guest_named_shm_windows_tests)
set_tests_properties(guest_named_shm_windows PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
add_test(NAME guest_named_shm_windows_short_temp COMMAND guest_named_shm_windows_tests --short-temp)
set_tests_properties(guest_named_shm_windows_short_temp PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
add_test(NAME guest_named_shm_windows_long_short_temp COMMAND guest_named_shm_windows_tests --long-short-temp)
set_tests_properties(guest_named_shm_windows_long_short_temp PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
foreach(part IN ITEMS basic reuse resize_read resize_write)
set(named_shm_suffix "_${part}")
if(part STREQUAL "basic")
set(named_shm_suffix "")
endif()
add_test(NAME guest_named_shm_windows${named_shm_suffix} COMMAND guest_named_shm_windows_tests --case ${part})
add_test(NAME guest_named_shm_windows_short_temp${named_shm_suffix} COMMAND guest_named_shm_windows_tests --short-temp ${part})
add_test(NAME guest_named_shm_windows_long_short_temp${named_shm_suffix} COMMAND guest_named_shm_windows_tests --long-short-temp ${part})
set_tests_properties(guest_named_shm_windows${named_shm_suffix}
guest_named_shm_windows_short_temp${named_shm_suffix} guest_named_shm_windows_long_short_temp${named_shm_suffix}
PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
set_tests_properties(guest_named_shm_windows_short_temp${named_shm_suffix} guest_named_shm_windows_long_short_temp${named_shm_suffix}
PROPERTIES SKIP_RETURN_CODE 77)
if(part STREQUAL "resize_write")
set_tests_properties(guest_named_shm_windows${named_shm_suffix}
guest_named_shm_windows_short_temp${named_shm_suffix} guest_named_shm_windows_long_short_temp${named_shm_suffix}
PROPERTIES PROCESSORS 3)
elseif(part STREQUAL "reuse" OR part STREQUAL "resize_read")
set_tests_properties(guest_named_shm_windows${named_shm_suffix}
guest_named_shm_windows_short_temp${named_shm_suffix} guest_named_shm_windows_long_short_temp${named_shm_suffix}
PROPERTIES PROCESSORS 2)
endif()
endforeach()
set_tests_properties(guest_shm_open PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
add_test_executable(guest_named_shm_windows_mapping_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmWindowsMapping.cpp)
@@ -13,7 +13,8 @@ int Fcntl(int fd, int command, std::intptr_t argument);
bool IoctlRequestMalformed(std::uint64_t request);
int Ioctl(int fd, std::uint64_t request, void* argument);
int DuplicateFrom(int fd, int start);
void RememberDescriptor(int fd, int openFlags);
void RememberDescriptor(int fd, int openFlags, bool fixedSizeWrites = false);
bool HasFixedSizeWrites(int fd);
void RememberPipe(int fd);
void ShareDescriptor(int from, int to);
void ForgetDescriptor(int fd);
@@ -24,6 +24,7 @@ std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence);
int APS5_VABI sceKernelStat(const char* path, FileStat* sb);
int APS5_VABI sceKernelUnlink(const char* path);
int APS5_VABI sceKernelFcntl(int d, int command, ...);
std::int64_t PosixPwrite_nid_no_patch(int d, const void* buf, std::size_t nbytes, std::int64_t offset);
#ifdef _WIN32
std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write);
#endif
@@ -15,6 +15,7 @@ std::int64_t PwriteSharedMemory_nid_no_patch(int descriptor, const void* buffer,
std::int64_t SeekSharedMemory_nid_no_patch(int descriptor, std::int64_t offset, int whence);
int TruncateSharedMemory_nid_no_patch(int descriptor, std::int64_t length);
#ifdef _WIN32
bool LimitSharedMemoryWrite_nid_no_patch(int descriptor, std::size_t& size, std::int64_t offset);
int SetSharedMemoryMode_nid_no_patch(int descriptor, int mode);
int SharedMemoryDescriptorWritable_nid_no_patch(int descriptor);
void TrackSharedMemoryDescriptor_nid_no_patch(int descriptor, bool writable);
@@ -118,6 +118,7 @@ struct Entry {
bool closeOnExec = false;
std::shared_ptr<int> status;
bool pipe = false;
bool fixedSizeWrites = false;
};
std::mutex g_mutex;
@@ -597,9 +598,9 @@ int Ioctl(int fd, std::uint64_t request, void* argument) {
Unsupported("ioctl", std::string("request ") + hex, *kind, fd);
}
void RememberDescriptor(int fd, int openFlags) {
void RememberDescriptor(int fd, int openFlags, bool fixedSizeWrites) {
std::lock_guard lock(g_mutex);
g_entries[fd] = Entry{(openFlags & OpenCloseOnExec) != 0, std::make_shared<int>(openFlags & OpenStatusFlags)};
g_entries[fd] = Entry{.closeOnExec = (openFlags & OpenCloseOnExec) != 0, .status = std::make_shared<int>(openFlags & OpenStatusFlags), .fixedSizeWrites = fixedSizeWrites};
#ifdef _WIN32
GuestFileLockForget_nid_no_patch(fd);
#endif
@@ -614,8 +615,9 @@ void RememberPipe(int fd) {
void ShareDescriptor(int from, int to) {
File::ShareArchivedFile(from, to);
std::lock_guard lock(g_mutex);
const auto& source = Materialize(from);
g_entries[to] = Entry{false, source.status, source.pipe};
auto copy = Materialize(from);
copy.closeOnExec = false;
g_entries[to] = std::move(copy);
#ifdef _WIN32
File::SharePositionedTransfer(from, to);
GuestFileLockShare_nid_no_patch(from, to);
@@ -628,6 +630,12 @@ void ForgetDescriptor(int fd) {
g_entries.erase(fd);
}
bool HasFixedSizeWrites(int fd) {
std::lock_guard lock(g_mutex);
const auto found = g_entries.find(fd);
return found != g_entries.end() && found->second.fixedSizeWrites;
}
std::uint64_t g_synchronousWrites = 0;
void SyncWrites(int fd) {
@@ -84,6 +84,11 @@ static int NativeWrite(int fd, const void* buf, std::size_t n) {
throw std::runtime_error("sceKernelWrite: nbytes exceeds platform limit");
}
const auto positioned = File::LockPositionedTransfer(fd);
if (File::HasFixedSizeWrites(fd)) {
const auto position = ::_lseeki64(fd, 0, SEEK_CUR);
if (position < 0 || !File::LimitSharedMemoryWrite_nid_no_patch(fd, n, position)) return -1;
if (n == 0) return 0;
}
const auto shared = File::WriteSharedMemory_nid_no_patch(fd, buf, n);
if (shared != -2) return static_cast<int>(shared);
if (File::Appends(fd)) {
@@ -5,6 +5,7 @@
#include "prx/libkernel/File/include/SharedMemory.hpp"
#include "prx/libc/include/CreationMask.hpp"
#include "prx/libkernel/File/include/File.hpp"
#include "prx/libkernel/File/include/DescriptorControl.hpp"
#include <cerrno>
#include <algorithm>
@@ -1050,7 +1051,12 @@ extern "C" int APS5_VABI shm_open_nid_postfix(const char* path, int flags, int m
}
if (accessMode == SCE_KERNEL_O_RDONLY) return Fail(GuestInvalid);
const int descriptor = CreateAnonymousFile();
if (descriptor >= 0) return descriptor;
if (descriptor >= 0) {
#ifdef _WIN32
File::RememberDescriptor(descriptor, flags, true);
#endif
return descriptor;
}
const int error = errno;
if (const int guest = GuestError(error)) return Fail(guest);
throw std::runtime_error("shm_open: creating the host anonymous file failed, errno=" + std::to_string(error));
@@ -303,11 +303,23 @@ void File::SharePositionedTransfer(int from, int to) {
if (!source) source = std::make_shared<PositionedLock>();
g_positionedLocks[to] = source;
}
bool File::LimitSharedMemoryWrite_nid_no_patch(int descriptor, std::size_t& size, std::int64_t offset) {
if (offset < 0) { errno = EINVAL; return false; }
const auto length = WithoutParameterHandler([descriptor] { return ::_filelengthi64(descriptor); });
if (length < 0) return false;
size = offset >= length ? 0 : static_cast<std::size_t>(std::min<std::uint64_t>(size, static_cast<std::uint64_t>(length - offset)));
return true;
}
static std::int64_t NativePositionedTransfer(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write) {
if (nbytes > static_cast<std::size_t>(std::numeric_limits<DWORD>::max())) {
throw std::runtime_error("NativePositioned: nbytes exceeds platform limit");
}
if (write) {
if (File::HasFixedSizeWrites(descriptor)) {
if (!File::LimitSharedMemoryWrite_nid_no_patch(descriptor, nbytes, offset)) return -1;
if (nbytes == 0) return 0;
}
const auto result = File::PwriteSharedMemory_nid_no_patch(descriptor, buf, nbytes, offset);
if (result != -2) return result;
}
@@ -373,6 +385,11 @@ static std::int64_t NativeTransferBuffer(int descriptor, void* buf, std::size_t
throw std::runtime_error("NativeTransfer: nbytes exceeds platform limit");
}
if (write) {
if (File::HasFixedSizeWrites(descriptor)) {
const auto position = WithoutParameterHandler([descriptor] { return ::_lseeki64(descriptor, 0, SEEK_CUR); });
if (position < 0 || !File::LimitSharedMemoryWrite_nid_no_patch(descriptor, nbytes, position)) return -1;
if (nbytes == 0) return 0;
}
const auto result = File::WriteSharedMemory_nid_no_patch(descriptor, buf, nbytes);
if (result != -2) return result;
if (File::Appends(descriptor)) {
@@ -1158,6 +1175,10 @@ int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t off
}
int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const void* buf, size_t nbytes, int64_t offset) {
return PosixPwrite_nid_no_patch(d, buf, nbytes, offset);
}
std::int64_t PosixPwrite_nid_no_patch(int d, const void* buf, std::size_t nbytes, std::int64_t offset) {
if (File::IsVirtualDirectoryDescriptor(d)) return PosixFailure(GUEST_EISDIR);
if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL);
if (const int error = PositionedArgumentError(d, true, buf == nullptr && nbytes != 0, offset)) return PosixFailure(error);
@@ -338,19 +338,22 @@ int APS5_VABI ptrace_nid_postfix(void) {
return 0;
}
int APS5_VABI pwrite_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI reboot_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI revoke_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI revoke_nid_postfix(const char* path) {
constexpr std::uint16_t typeMask = 0170000;
constexpr std::uint16_t characterDevice = 0020000;
constexpr int guestEinval = 22;
FileStat status{};
if (stat_nid_postfix(path, &status) != 0) return -1;
if ((status.st_mode & typeMask) != characterDevice) {
*__error_nid_postfix() = guestEinval;
return -1;
}
NotImplemented_nid_no_patch("revoke of a character device");
}
int APS5_VABI rfork_thread_nid_postfix(void) {
+5
View File
@@ -34,6 +34,7 @@
#include "prx/libkernel/File/include/DescriptorControl.hpp"
#include "prx/libc/include/CreationMask.hpp"
#include "prx/libkernel/File/include/FileFlags.hpp"
#include "prx/libkernel/File/include/File.hpp"
#include "prx/libkernel/Socket/include/SocketPoll.hpp"
#include "prx/libkernel/File/include/SharedMemory.hpp"
#include "prx/libkernel/Equeue/Equeue.hpp"
@@ -446,6 +447,10 @@ std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t mask) {
return File::ExchangeCreationMask_nid_no_patch(mask);
}
std::int64_t APS5_VABI pwrite_nid_postfix(int d, const void* buf, std::size_t nbytes, std::int64_t offset) {
return PosixPwrite_nid_no_patch(d, buf, nbytes, offset);
}
int APS5_VABI sceKernelGetOpenPsIdForSystem(void* id) {
if (id == nullptr) return GuestEinval;
return sceKernelGetOpenPsId(id);
+2 -2
View File
@@ -163,9 +163,9 @@ int APS5_VABI pthread_suspend_user_context_np_nid_postfix(void);
int APS5_VABI pthread_switch_add_np_nid_postfix(void);
int APS5_VABI pthread_switch_delete_np_nid_postfix(void);
int APS5_VABI ptrace_nid_postfix(void);
int APS5_VABI pwrite_nid_postfix(void);
std::int64_t APS5_VABI pwrite_nid_postfix(int d, const void* buf, std::size_t nbytes, std::int64_t offset);
int APS5_VABI reboot_nid_postfix(void);
int APS5_VABI revoke_nid_postfix(void);
int APS5_VABI revoke_nid_postfix(const char* path);
int APS5_VABI rfork_thread_nid_postfix(void);
int APS5_VABI rtld_printf_nid_postfix(void);
int APS5_VABI sceCoredumpDebugForceCoredumpOnAppClose(void);
+26
View File
@@ -1961,6 +1961,31 @@ void TestAt9RunDecodesWholeInput(std::uint32_t context) {
Require(sceAjmInstanceDestroy(context, instance) == 0);
}
void TestStrError() {
const char* const expected[] = {
"context does not exist", "instance does not exist", "batch does not exist", "invalid parameter",
"not enough memory", "not enough resources", "codec is not supported",
"codec is already registered in the context", "codec is not registered in the context",
"wrong revision in the flags", "flag is not supported", "busy", "priority is invalid", "in progress",
"operation cannot be performed at the moment", "malformed batch", "error at job creation",
"invalid opcode", "priority violation", "buffer is too big", "invalid address", "batch was cancelled",
"specified memory area is already registered", "specified memory area is not registered",
};
static_assert(sizeof(expected) / sizeof(expected[0]) == 24);
for (std::uint32_t i = 0; i < 24; ++i) {
const int code = static_cast<int>(0x80930002u + i);
const char* text = sceAjmStrError(code);
Require(text != nullptr && std::strcmp(text, expected[i]) == 0);
Require(sceAjmStrError(code) == text);
}
const char* ok = sceAjmStrError(0);
Require(ok != nullptr && std::strcmp(ok, "ok") == 0);
for (std::uint32_t code : {0x80930000u, 0x80930001u, 0x8093001au, 0x8093001bu, 0x8092ffffu, 1u, 0xffffffffu, 0x80020001u, 0x8a6e0001u, 0x7fffffffu}) {
const char* text = sceAjmStrError(static_cast<int>(code));
Require(text != nullptr && std::strcmp(text, "(unknown)") == 0);
}
}
void TestAt9FlagRules(std::uint32_t context) {
constexpr std::uint64_t runMultipleFrames = 1ull << 12;
constexpr std::uint64_t sidebandStream = 1ull << 47;
@@ -2643,6 +2668,7 @@ int main(int argc, char** argv) {
TestAt9GaplessSegments(context);
TestAt9GaplessResetRestartsTotal(context);
TestAt9RunDecodesWholeInput(context);
TestStrError();
TestAt9FlagRules(context);
TestPcmResample(context);
TestConsoleBufferJobs(context);
+2 -1
View File
@@ -17,7 +17,8 @@ GuestCatch:
.cfi_def_cfa_register %rbp
subq $16, %rsp
.Lcatch_call:
call _ZSt16_Throw_Cpp_errori_nid_postfix
movq FixtureThrow(%rip), %rax
call *%rax
.Lcatch_call_end:
ud2
.Lcatch_landing:
+31 -1
View File
@@ -8,6 +8,7 @@
#include <cstdlib>
#include <cstring>
#include <iterator>
#include <limits>
#include <stdexcept>
#include <string>
@@ -42,6 +43,7 @@ extern const unsigned char _ZTISt12system_error_nid_postfix[];
void APS5_VABI GuestCatch(int code);
void (APS5_VABI *FixtureThrow)(int) = nullptr;
const void* FixtureCatchType = nullptr;
const void* FixtureOtherType = nullptr;
const Expected* FixtureExpected = nullptr;
@@ -129,6 +131,7 @@ extern "C" void APS5_VABI FixtureInspect(void* object) {
}
static void Catch(const void* type, int code) {
FixtureThrow = _ZSt16_Throw_Cpp_errori_nid_postfix;
FixtureCatchType = type;
FixtureExpected = &Errors[code];
const auto before = FixtureInspected;
@@ -136,6 +139,15 @@ static void Catch(const void* type, int code) {
Require(FixtureInspected == before + 1, 2);
}
static void CatchCError(const void* type, int code, int cppIndex) {
FixtureThrow = _ZSt14_Throw_C_errori_nid_postfix;
FixtureCatchType = type;
FixtureExpected = &Errors[cppIndex];
const auto before = FixtureInspected;
GuestCatch(code);
Require(FixtureInspected == before + 1, 5);
}
static void CheckOutOfRange(int code) {
bool threw = false;
try {
@@ -147,6 +159,15 @@ static void CheckOutOfRange(int code) {
Require(threw, 3);
}
static void CheckCErrorReturns(int code) {
try {
_ZSt14_Throw_C_errori_nid_postfix(code);
} catch (...) {
std::fprintf(stderr, "guest _Throw_C_error unexpectedly threw for %d\n", code);
FixtureFail(6);
}
}
int main() {
const std::array<void*, 10> api {};
ApplicationHeapRegister_nid_no_patch(api.data());
@@ -156,6 +177,15 @@ int main() {
Catch(_ZTISt9exception_nid_postfix, 6);
CheckOutOfRange(-1);
CheckOutOfRange(static_cast<int>(std::size(Errors)));
std::puts("guest _Throw_Cpp_error system_error, generic category and range: PASS");
CatchCError(_ZTISt12system_error_nid_postfix, 1, 6);
CatchCError(_ZTISt12system_error_nid_postfix, 2, 6);
CatchCError(_ZTISt12system_error_nid_postfix, 3, 0);
CatchCError(_ZTISt12system_error_nid_postfix, 4, 1);
CatchCError(_ZTISt13runtime_error_nid_postfix, 3, 0);
CatchCError(_ZTISt9exception_nid_postfix, 4, 1);
const std::array returnCodes {std::numeric_limits<int>::min(), -1, 0, 6, std::numeric_limits<int>::max()};
for (const int code : returnCodes) CheckCErrorReturns(code);
CatchCError(_ZTISt12system_error_nid_postfix, 5, 4);
std::puts("guest _Throw_Cpp_error and _Throw_C_error system_error, generic category and range: PASS");
return 0;
}
+172
View File
@@ -0,0 +1,172 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libkernel/File/include/FileFlags.hpp"
#include <nid/NidCompute.hpp>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <limits>
#include <source_location>
#include <string>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <winioctl.h>
#else
#include <dlfcn.h>
#endif
static void Require(bool value, std::source_location location = std::source_location::current()) {
if (!value) {
std::fprintf(stderr, "Kernel sys pwrite check failed at line %u\n", location.line());
std::abort();
}
}
static void* LoadLibraryFile(const std::filesystem::path& path) {
#ifdef _WIN32
auto* module = reinterpret_cast<void*>(LoadLibraryExW(path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH));
#else
auto* module = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
if (!module) std::fprintf(stderr, "%s\n", dlerror());
#endif
Require(module != nullptr);
#ifdef _WIN32
std::wstring loaded(32768, L'\0');
const auto length = GetModuleFileNameW(static_cast<HMODULE>(module), loaded.data(), static_cast<DWORD>(loaded.size()));
Require(length != 0 && length < loaded.size());
loaded.resize(length);
Require(std::filesystem::equivalent(path, std::filesystem::path(loaded)));
#endif
return module;
}
template <typename Function>
static Function LoadFunction(void* module, const char* name, bool patched, const char* unpatched = nullptr) {
const auto symbol = patched ? Nid::ComputeNid(name, "") : unpatched ? std::string(unpatched) : std::string(name) + "_nid_postfix";
#ifdef _WIN32
const auto function = reinterpret_cast<Function>(reinterpret_cast<void*>(GetProcAddress(static_cast<HMODULE>(module), symbol.c_str())));
#else
const auto function = reinterpret_cast<Function>(dlsym(module, symbol.c_str()));
#endif
if (!function) std::fprintf(stderr, "Missing symbol %s\n", symbol.c_str());
Require(function != nullptr);
return function;
}
int main(int argc, char** argv) {
Require(argc == 3);
const auto directory = std::filesystem::absolute(argv[1]);
const bool patched = std::strcmp(argv[2], "patched") == 0;
auto* sys = LoadLibraryFile(directory / "libkernel_sys.prx");
auto* kernel = LoadLibraryFile(directory / "libkernel.prx");
auto* libc = LoadLibraryFile(directory / "libc.prx");
using Pwrite = std::int64_t (APS5_VABI*)(int, const void*, std::size_t, std::int64_t);
const auto writeAt = LoadFunction<Pwrite>(sys, "pwrite", patched);
const auto kernelWriteAt = LoadFunction<Pwrite>(kernel, "pwrite", patched, "pwrite_nid_disambig1_nid_postfix");
Require(writeAt != kernelWriteAt);
const auto open = LoadFunction<int (APS5_VABI*)(const char*, int, int)>(kernel, "open", patched);
const auto close = LoadFunction<int (APS5_VABI*)(int)>(kernel, "close", patched);
const auto duplicate = LoadFunction<int (APS5_VABI*)(int)>(kernel, "dup", patched);
const auto duplicateTo = LoadFunction<int (APS5_VABI*)(int, int)>(kernel, "dup2", patched);
const auto write = LoadFunction<std::int64_t (APS5_VABI*)(int, const char*, std::int64_t)>(kernel, "write", patched);
const auto seek = LoadFunction<std::int64_t (APS5_VABI*)(int, std::int64_t, int)>(kernel, "lseek", patched);
const auto readAt = LoadFunction<std::int64_t (APS5_VABI*)(int, void*, std::size_t, std::int64_t)>(kernel, "pread", patched);
const auto error = LoadFunction<int* (APS5_VABI*)()>(libc, "__error", patched);
const auto shmOpen = LoadFunction<int (APS5_VABI*)(const char*, int, int)>(kernel, "shm_open", patched);
const auto truncate = LoadFunction<int (APS5_VABI*)(int, std::int64_t)>(kernel, "ftruncate", patched);
const auto pipe = LoadFunction<int (APS5_VABI*)(int*)>(kernel, "pipe", patched);
const auto socket = LoadFunction<int (APS5_VABI*)(int, int, int)>(kernel, "socket", patched);
const auto root = std::filesystem::path("anyps5-kernel-sys-pwrite-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
Require(std::filesystem::create_directory(root));
const auto path = (root / "data").string();
{ std::ofstream stream(path, std::ios::binary); stream << "abcdefgh"; Require(stream.good()); }
#ifdef _WIN32
const auto sparseFile = CreateFileW(std::filesystem::path(path).c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
Require(sparseFile != INVALID_HANDLE_VALUE);
DWORD returned = 0;
Require(DeviceIoControl(sparseFile, FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, &returned, nullptr) != FALSE);
Require(CloseHandle(sparseFile) != FALSE);
#endif
const int descriptor = open(path.c_str(), SCE_KERNEL_O_RDWR, 0);
Require(descriptor >= 0);
Require(seek(descriptor, 3, SEEK_SET) == 3);
*error() = 13;
Require(writeAt(descriptor, "XY", 2, 5) == 2 && *error() == 13);
Require(seek(descriptor, 0, SEEK_CUR) == 3);
char contents[8]{};
Require(readAt(descriptor, contents, sizeof(contents), 0) == 8 && std::memcmp(contents, "abcdeXYh", 8) == 0);
constexpr std::int64_t largeOffset = (std::int64_t{1} << 32) + 17;
Require(writeAt(descriptor, "Z", 1, largeOffset) == 1);
Require(std::filesystem::file_size(path) == largeOffset + 1);
Require(readAt(descriptor, contents, 2, largeOffset - 1) == 2 && contents[0] == 0 && contents[1] == 'Z');
Require(readAt(descriptor, contents, 8, 0) == 8 && std::memcmp(contents, "abcdeXYh", 8) == 0);
Require(writeAt(descriptor, nullptr, 0, 0) == 0);
Require(writeAt(descriptor, nullptr, 1, 0) == -1 && *error() == 14);
Require(writeAt(descriptor, "x", 1, -1) == -1 && *error() == 22);
Require(writeAt(descriptor, nullptr, 0, -5) == -1 && *error() == 22);
Require(writeAt(descriptor, nullptr, 1, -1) == -1 && *error() == 22);
Require(writeAt(-1, nullptr, 1, -1) == -1 && *error() == 9);
Require(writeAt(-1, "x", 1, 0) == -1 && *error() == 9);
Require(writeAt(-1, nullptr, 0, 0) == -1 && *error() == 9);
const auto oversized = static_cast<std::size_t>(std::numeric_limits<std::int64_t>::max()) + 1;
Require(writeAt(descriptor, "x", oversized, 0) == -1 && *error() == 22);
Require(seek(descriptor, 0, SEEK_CUR) == 3);
Require(truncate(descriptor, 8) == 0);
Require(close(descriptor) == 0);
#ifndef _WIN32
const int append = open(path.c_str(), SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_APPEND, 0);
Require(append >= 0 && seek(append, 2, SEEK_SET) == 2);
Require(writeAt(append, "!", 1, 0) == 1 && seek(append, 0, SEEK_CUR) == 2);
Require(std::filesystem::file_size(path) == 9);
Require(close(append) == 0);
#endif
const int readOnly = open(path.c_str(), SCE_KERNEL_O_RDONLY, 0);
Require(readOnly >= 0);
Require(writeAt(readOnly, "x", 1, 0) == -1 && *error() == 9);
Require(writeAt(readOnly, nullptr, 1, -1) == -1 && *error() == 9);
#ifndef _WIN32
Require(readAt(readOnly, contents, 1, 8) == 1 && contents[0] == '!');
#endif
Require(close(readOnly) == 0);
int descriptors[2]{};
Require(pipe(descriptors) == 0);
Require(writeAt(descriptors[1], "x", 1, 0) == -1 && *error() == 29);
Require(close(descriptors[0]) == 0 && close(descriptors[1]) == 0);
const int sock = socket(2, 1, 0);
Require(sock >= 0);
Require(writeAt(sock, "x", 1, 0) == -1 && *error() == 29);
Require(close(sock) == 0);
Require(writeAt(sock, "x", 1, 0) == -1 && *error() == 9);
const int shared = shmOpen(reinterpret_cast<const char*>(1), 0x2, 0600);
Require(shared >= 0 && truncate(shared, 0x4000) == 0);
Require(seek(shared, 7, SEEK_SET) == 7);
Require(writeAt(shared, "shared", 6, 0x1000) == 6);
Require(readAt(shared, contents, 6, 0x1000) == 6 && std::memcmp(contents, "shared", 6) == 0);
Require(seek(shared, 0, SEEK_CUR) == 7);
Require(writeAt(shared, "tail", 4, 0x3ffe) == 2);
Require(readAt(shared, contents, 4, 0x3ffe) == 2 && std::memcmp(contents, "ta", 2) == 0);
Require(seek(shared, 0, SEEK_CUR) == 7);
Require(seek(shared, 0, SEEK_END) == 0x4000);
const int copy = duplicate(shared);
Require(copy >= 0 && close(shared) == 0);
Require(kernelWriteAt(copy, "edge", 4, 0x3ffe) == 2);
Require(writeAt(copy, "x", 1, 0x4000) == 0 && seek(copy, 0, SEEK_END) == 0x4000);
Require(seek(copy, 0x3fff, SEEK_SET) == 0x3fff);
Require(write(copy, "ab", 2) == 1 && write(copy, "x", 1) == 0);
Require(seek(copy, 0, SEEK_END) == 0x4000);
Require(close(copy) == 0);
const int ordinary = open(path.c_str(), SCE_KERNEL_O_RDWR, 0);
Require(ordinary >= 0 && duplicateTo(ordinary, shared) == shared);
Require(writeAt(shared, "grow", 4, 16) == 4 && seek(shared, 0, SEEK_END) == 20);
Require(close(shared) == 0);
if (ordinary != shared) Require(close(ordinary) == 0);
std::filesystem::remove_all(root);
return 0;
}
+53
View File
@@ -0,0 +1,53 @@
#include "prx/libc/include/GuestErrno.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libkernel_sys/include/Api.hpp"
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <source_location>
#include <string>
namespace {
constexpr int Enoent = 2;
constexpr int Efault = 14;
constexpr int Enotdir = 20;
constexpr int Einval = 22;
constexpr int Enametoolong = 63;
void Require(bool condition, std::source_location location = std::source_location::current()) {
if (condition) return;
std::fprintf(stderr, "check failed at line %u\n", static_cast<unsigned>(location.line()));
std::exit(1);
}
int Error() {
return *__error_nid_postfix();
}
bool Fails(const std::string& path, int error) {
return revoke_nid_postfix(path.c_str()) == -1 && Error() == error;
}
}
int main() {
const auto root = std::filesystem::path("anyps5-revoke-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
Require(std::filesystem::create_directory(root));
const auto file = (root / "file.txt").string();
std::ofstream(file).put('x');
Require(Fails(file, Einval));
Require(Fails(root.string(), Einval));
Require(Fails((root / "missing").string(), Enoent));
Require(Fails(file + "/below", Enotdir));
Require(Fails("", Enoent));
Require(Fails(std::string(1024, 'a'), Enametoolong));
Require(revoke_nid_postfix(nullptr) == -1 && Error() == Efault);
std::filesystem::remove_all(root);
std::puts("guest_kernel_sys_revoke passed");
return 0;
}
+49 -21
View File
@@ -240,9 +240,37 @@ void VerifyResizeReadPosition(const std::string& name) {
RequireNoUnlinkedFiles(name);
}
void Run() {
VerifyAnonymousDescriptorClassification();
void Run(std::string_view selection) {
if (selection == "all") {
for (const auto part : {"basic", "reuse", "resize_read", "resize_write"}) Run(part);
return;
}
const std::string base = "/anyps5-windows-named-" + std::to_string(::GetCurrentProcessId()) + "-" + std::to_string(::GetTickCount64());
if (selection == "reuse") {
const std::string name = base + "-reuse";
const int descriptor = shm_open_nid_postfix(name.c_str(), ReadWrite | Create | Exclusive, 0600);
Require(descriptor >= 0 && ftruncate_nid_postfix(descriptor, 64) == 0, "creating the descriptor-reuse object failed");
VerifyConcurrentDescriptorReuse(name);
Require(shm_unlink_nid_postfix(name.c_str()) == 0 && close_nid_postfix(descriptor) == 0, "descriptor-reuse cleanup failed");
RequireNoUnlinkedFiles(name);
return;
}
if (selection == "resize_read") {
VerifyResizeReadPosition(base + "-resize-read");
return;
}
if (selection == "resize_write") {
const std::string raceName = base + "-cross-process-race";
const int raceDescriptor = shm_open_nid_postfix(raceName.c_str(), ReadWrite | Create | Exclusive, 0600);
Require(raceDescriptor >= 0 && ftruncate_nid_postfix(raceDescriptor, RaceSize) == 0,
"cross-process race object creation failed");
VerifyCrossProcessResizeWrite(raceName, raceDescriptor);
Require(close_nid_postfix(raceDescriptor) == 0, "closing cross-process race descriptor failed");
RequireNoUnlinkedFiles(raceName);
return;
}
Require(selection == "basic", "unknown named shared-memory test selection");
VerifyAnonymousDescriptorClassification();
const std::string name = base + "-lifecycle";
Require(shm_unlink_nid_postfix(name.c_str()) == -1 && *__error_nid_postfix() == 2,
"new test name unexpectedly existed");
@@ -252,7 +280,12 @@ void Run() {
Require(original >= 0 && *__error_nid_postfix() == 73, "exclusive creation failed or changed guest errno");
VerifyUntrackedDescriptor(original);
Require(ftruncate_nid_postfix(original, 64) == 0, "sizing the named object failed");
VerifyConcurrentDescriptorReuse(name);
const int reopened = shm_open_nid_postfix(name.c_str(), ReadWrite, 0);
Require(reopened >= 0 && File::IsSharedMemoryDescriptor_nid_no_patch(reopened), "reopened named descriptor is not tracked");
Require(pwrite_nid_disambig1_nid_postfix(reopened, "x", 1, 64) == 0, "positioned write at the named-object boundary extended it");
struct _stat64 bounded{};
Require(::_fstat64(reopened, &bounded) == 0 && bounded.st_size == 64, "positioned write changed the named-object size");
Require(close_nid_postfix(reopened) == 0, "closing reopened named descriptor failed");
const char originalBytes[] = "original";
Require(pwrite_nid_disambig1_nid_postfix(original, originalBytes, sizeof(originalBytes), 0) == sizeof(originalBytes),
"write to named object failed");
@@ -338,7 +371,6 @@ void Run() {
close_nid_postfix(duplicateReplacement) == 0 && close_nid_postfix(duplicate) == 0,
"cleaning up dup2 replacement descriptors failed");
RequireNoUnlinkedFiles(replacementName);
VerifyResizeReadPosition(base + "-resize-read");
const std::string concurrentName = base + "-exclusive-race";
constexpr int contenderCount = 12;
@@ -365,13 +397,6 @@ void Run() {
Require(created == 1, "exclusive create race produced more or fewer than one winner");
Require(shm_unlink_nid_postfix(concurrentName.c_str()) == 0, "exclusive-race cleanup failed");
const std::string raceName = base + "-cross-process-race";
const int raceDescriptor = shm_open_nid_postfix(raceName.c_str(), ReadWrite | Create | Exclusive, 0600);
Require(raceDescriptor >= 0 && ftruncate_nid_postfix(raceDescriptor, RaceSize) == 0,
"cross-process race object creation failed");
VerifyCrossProcessResizeWrite(raceName, raceDescriptor);
Require(close_nid_postfix(raceDescriptor) == 0, "closing cross-process race descriptor failed");
const std::string permissionsName = base + "-permissions";
const int restricted = shm_open_nid_postfix(permissionsName.c_str(), ReadWrite | Create | Exclusive, 0400);
Require(restricted >= 0, "read-only mode creation failed");
@@ -501,7 +526,7 @@ void RemovePrivateDirectory(const std::filesystem::path& directory) {
throw std::runtime_error("removing the private temporary directory failed");
}
int RunWithShortTempPath(bool longPath) {
int RunWithShortTempPath(bool longPath, std::string_view selection) {
wchar_t tempBuffer[32768]{};
const DWORD tempLength = ::GetTempPathW(static_cast<DWORD>(std::size(tempBuffer)), tempBuffer);
if (tempLength == 0 || tempLength >= std::size(tempBuffer))
@@ -593,7 +618,7 @@ int RunWithShortTempPath(bool longPath) {
const DWORD executableLength = ::GetModuleFileNameW(nullptr, executable, static_cast<DWORD>(std::size(executable)));
if (executableLength == 0 || executableLength >= std::size(executable))
throw std::runtime_error("locating the Windows shared-memory test executable failed");
std::wstring command = L"\"" + std::wstring(executable, executableLength) + L"\" --short-temp-child";
std::wstring command = L"\"" + std::wstring(executable, executableLength) + L"\" --short-temp-child " + std::wstring(selection.begin(), selection.end());
auto environment = EnvironmentWithTempPath(DosPath(shortRoot).native());
STARTUPINFOW startup{};
startup.cb = sizeof(startup);
@@ -626,15 +651,18 @@ int main(int argumentCount, char** arguments) {
try {
if (argumentCount == 3 && std::string_view(arguments[1]) == "--race")
return RunResizeWriteRace(arguments[2]) ? 0 : 1;
if (argumentCount == 2 && std::string_view(arguments[1]) == "--short-temp")
return RunWithShortTempPath(false);
if (argumentCount == 2 && std::string_view(arguments[1]) == "--long-short-temp")
return RunWithShortTempPath(true);
if (argumentCount == 2 && std::string_view(arguments[1]) == "--short-temp-child") {
Run();
return 0;
const std::string_view selection = argumentCount == 3 ? arguments[2] : "all";
Require(selection == "all" || selection == "basic" || selection == "reuse" || selection == "resize_read" || selection == "resize_write",
"unknown named shared-memory test selection");
if (argumentCount == 2 || argumentCount == 3) {
const std::string_view option = arguments[1];
if (option == "--short-temp") return RunWithShortTempPath(false, selection);
if (option == "--long-short-temp") return RunWithShortTempPath(true, selection);
Require(option == "--case" || option == "--short-temp-child", "unknown named shared-memory test option");
} else {
Require(argumentCount == 1, "invalid named shared-memory test arguments");
}
Run();
Run(selection);
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
+2 -2
View File
@@ -121,7 +121,7 @@ void CheckUnwindInfo() {
#endif
std::uint16_t UnknownModuleId() {
for (std::uint32_t id = 0xffff; id != 0; --id) {
for (std::uint32_t id = 0xff; id != 0; --id) {
if (kModuleTable.find(id) == kModuleTable.end()) return static_cast<std::uint16_t>(id);
}
std::abort();
@@ -274,7 +274,7 @@ int main() {
Require(result == 0x5a5a5a5a);
}
Require(ThrowsRuntimeError([] { sceSysmoduleIsLoadedInternal(0x00ffffffu); }));
Require(sceSysmoduleIsLoadedInternal(0x00ffffffu) == kInvalidValue);
Require(sceSysmoduleIsLoadedInternal(kFiberModuleId) == kModuleUnloaded);
Require(sceSysmoduleIsLoaded(kFiberModuleId) == kModuleUnloaded);
Require(sceSysmoduleLoadModuleInternal(kFiberModuleId) == 0);
+5
View File
@@ -955,6 +955,11 @@ if(BUILD_TESTING)
add_asan_test_options(input_magic max_allocation_size_mb=512)
add_test(NAME input_preservation COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_preservation.py $<TARGET_FILE:relinker>)
foreach(mode linux windows controls)
add_test(NAME patch_input_preservation_${mode} COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_patch_input_preservation.py $<TARGET_FILE:relinker> ${mode})
set_tests_properties(patch_input_preservation_${mode} PROPERTIES TIMEOUT 20)
endforeach()
add_test(NAME output_collisions COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_output_collisions.py $<TARGET_FILE:relinker>)
add_test(NAME secondary_output_collisions COMMAND ${Python3_EXECUTABLE}
+3 -5
View File
@@ -382,6 +382,8 @@ int Run(const int argc, char* argv[]) {
for (const auto& artifact : guestArtifacts)
if (SameOutputFile(artifact.SourcePath, path))
throw Domain::RelinkerException(std::string(kind) + " output would overwrite an input module: " + Io::Utf8Path(path));
if (!args.noPatches && std::filesystem::is_regular_file(patchFile) && SameOutputFile(patchFile, path))
throw Domain::RelinkerException(std::string(kind) + " output would overwrite the patch file: " + Io::Utf8Path(path));
};
for (std::size_t index = 1; index < executables.size(); ++index) checkOutputInputs(Io::NativePath(executables[index].OutputPath), "Executable");
if (desktopEntry) {
@@ -411,11 +413,7 @@ int Run(const int argc, char* argv[]) {
}
checkOutputInputs(Io::NativePath(absPath), "Executable");
if (!manifestPath.empty()) {
checkOutputInputs(manifestPath, "Patch manifest");
if (std::filesystem::exists(manifestPath) && std::filesystem::exists(patchFile) && std::filesystem::equivalent(manifestPath, patchFile))
throw Domain::RelinkerException("Patch manifest output would overwrite the patch file: " + Io::Utf8Path(manifestPath));
}
if (!manifestPath.empty()) checkOutputInputs(manifestPath, "Patch manifest");
for (const auto& artifact : guestArtifacts) checkOutputInputs(artifact.Path, "Guest module");
std::vector<std::filesystem::path> outputPaths;
@@ -0,0 +1,87 @@
import os
from pathlib import Path
import subprocess
import sys
import tempfile
from test_game_patches import patch_site, patch_text
from test_guest_intel_trampolines import main_fixture
from test_secondary_output_collisions import prepare_secondary
def main():
relinker = Path(sys.argv[1]).resolve()
mode = sys.argv[2] if len(sys.argv) > 2 else "all"
formats = (False, True) if mode == "all" else (mode == "windows",)
text = patch_text(*patch_site(main_fixture()), target="input.elf").encode()
with tempfile.TemporaryDirectory(prefix="anyps5-patch-input-") as directory:
work = Path(directory)
for windows in (() if mode == "controls" else formats):
command, outputs, _ = prepare_secondary(work / f"control-{windows}", windows)
config = outputs["executable"].parent / "relinker-patches.toml"
config.write_bytes(text)
result = subprocess.run([str(relinker), *command], cwd=config.parent,
capture_output=True, text=True, timeout=20)
assert result.returncode == 0 and config.read_bytes() == text, (result.stdout, result.stderr)
outputs["manifest"] = config.parent / "output.patches.toml"
aliases = ("symlink", "direct", "hardlink")
cases = [(name, "symlink" if name == "executable" else aliases[index % len(aliases)])
for index, name in enumerate(outputs)]
cases.extend(("executable", alias) for alias in ("direct", "hardlink"))
for name, alias in cases:
case = work / f"{windows}-{name}-{alias}"
command, paths, inputs = prepare_secondary(case, windows)
paths["manifest"] = case / "output.patches.toml"
target = paths[name]
config = target if alias == "direct" else case / "relinker-patches.toml"
config.write_bytes(text)
if alias == "hardlink":
os.link(config, target)
elif alias == "symlink":
try:
target.symlink_to(config)
except OSError as error:
if os.name != "nt" or error.winerror != 1314:
raise
print("Windows symlink privilege unavailable; checking the output through its direct path")
alias = "direct"
config = target
config.write_bytes(text)
for path in paths.values():
if path != target:
path.write_bytes(b"previous output")
original = {path: path.read_bytes() for path in paths.values()}
options = ["--patches", str(config)] if alias == "direct" else []
result = subprocess.run([str(relinker), *options, *command], cwd=case,
capture_output=True, text=True, timeout=20)
assert result.returncode == 2 and "would overwrite the patch file" in result.stderr, (
windows, name, alias, result.stdout, result.stderr)
assert config.read_bytes() == text
for path, data in {**inputs, **original}.items():
assert path.read_bytes() == data, path
assert not list(case.rglob(".anyps5-output-identity-*"))
if mode in ("all", "controls"):
case = work / "empty-config"
command, outputs, inputs = prepare_secondary(case, False)
config = outputs["registry"]
config.write_text("format = 1\n")
result = subprocess.run([str(relinker), "--patches", str(config), *command], cwd=case,
capture_output=True, text=True, timeout=20)
assert result.returncode == 2 and "would overwrite the patch file" in result.stderr, (result.stdout, result.stderr)
assert config.read_text() == "format = 1\n"
assert not outputs["executable"].exists()
for path, data in inputs.items():
assert path.read_bytes() == data, path
case = work / "disabled-config"
command, _, _ = prepare_secondary(case, False)
config = case / "relinker-patches.toml"
config.write_bytes(text)
result = subprocess.run([str(relinker), "--no-patches", *command[:-1], str(config)], cwd=case,
capture_output=True, text=True, timeout=20)
assert result.returncode == 0 and config.read_bytes().startswith(b"\x7fELF"), (result.stdout, result.stderr)
print("Patch input preservation tests passed")
if __name__ == "__main__":
main()
@@ -226,7 +226,7 @@ bool foldRedundantMask(IrValue& value) {
bool feedsMultiply(const IrValue& value) {
const auto& uses = value.Uses();
return std::any_of(uses.begin(), uses.end(), [](const IrValue* use) { return use->Opcode() == IrOpcode::IMul32; });
return std::any_of(uses.begin(), uses.end(), [](const IrValue* use) { return use->Opcode() == IrOpcode::IMul32 || (use->Opcode() == IrOpcode::BitwiseXor32 && (isImmediate(*use->Argument(0)->Resolve(), IrType::U32) || isImmediate(*use->Argument(1)->Resolve(), IrType::U32)) && feedsMultiply(*use)); });
}
bool isImmediateU32(const IrValue& value, std::uint32_t immediate) {
+4
View File
@@ -76,3 +76,7 @@ See [Build](BUILD.md#performance-trace) for the trace format, summary fields and
## Shader registers
`APS5_TRACE_SHADER_REGS=all` prints register snapshots for every shader. Set it to a shader code address in hexadecimal to print one shader's registers.
## Running titles repeatably
`python tools/title_run.py --games <directory> [ID ...]` relinks and runs extracted title packages found under `--games` (a directory holding `eboot.bin` and `sce_sys/param.json`) and writes `summary.json` and a fixed-format `summary.md` under `--out` (default `build/title-runs/<commit>-<time>/`). Each title runs `--runs` times (default 2): the first with empty AnyPS5 and Mesa or NVIDIA shader caches, the next ones keeping them. AMD's Windows `VkCache` is not isolated. On Windows the title's windows open behind every other window, without focus and without being minimized, and can be brought forward from the taskbar; elsewhere run it under a virtual display if that matters. A run ends as `relink-failed`, `crashed`, `exited`, `stalled` (no present in the last 20 s, or too few presents for a frame to be dumped), `frame-unreadable` (the last dumped frame is truncated or not a 24- or 32-bit BMP), `blank` (the last frame is one colour) or `rendering`; the first, middle and last dumped frames are kept. A screen that presents only when its view changes stops after a few presents and is reported `stalled` although the title still answers input; pressing a button through `tools/pad_input.py` with `APS5_PAD_INPUT_FILE` set tells it apart from a stuck title. `--compare <summary.json>` lists regressions, improvements and missing titles against an earlier run, limited to the IDs given when there are any. Titles are prepared in `<out>/work/<ID>/`, the same path on every run, so a firewall prompt for a title that opens a port appears once; answering Cancel does not affect the run. An invocation locks `<out>/work/<ID>.lock` for each title it runs before touching anything, and refuses to start while another invocation with the same `--out` holds one of them. Per-title relink flags, run length and environment live in `tools/titles.json`.
+2 -2
View File
@@ -87,7 +87,7 @@
- [Alpha-to-coverage `0x1aa01`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `DB_ALPHA_TO_MASK = 0x1aa01` throws `dithered or rounded alpha-to-coverage is unsupported`. It differs from the accepted undithered `0xaa01` by bit 16 (`0x10000`), which the accepted dithered `0x18701` also has: [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs `OFFSET_ROUND(1)` only with the dithered offsets and `OFFSET_ROUND(0)` for `0xaa01`, so it never programs this combination. PPSA12361 reaches it on main 74acfad65b on an RTX 4080 SUPER (`run_024721.err` lines 3484-3486) and an RX 6400 (`run_025512.err` lines 2667-2671). What the console covers for this value is not measured, so it stays unsupported.
- [MSAA_ENABLE on a single-sample target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_MODE_CNTL_0.MSAA_ENABLE` with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES = 0` is drawn like the same state without the bit when sample 0 of the four `PA_SC_AA_SAMPLE_LOCS_PIXEL_*_0` registers is at the pixel center and the draw rasterizes filled triangles, lines or rectangles (`VGT_PRIMITIVE_TYPE` 2, 3, 7 or 17 as well as the triangle types) with alpha-to-coverage and conservative rasterization off. [RADV](https://gitlab.freedesktop.org/mesa/mesa/-/blob/745709a000482a4a24c91faef5c5a40bd653a863/src/amd/vulkan/radv_cmd_buffer.c#L13606-13609) programs this state for one-sample custom sample locations on GFX10 and later, where the bit makes the rasterizer use the programmed location of sample 0 ([061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)); that the bit changes nothing else for such a draw is inferred from that and is not measured on hardware. For lines the bit may also select the console's multisample (rectangular) line rule instead of the aliased one, so which pixels a line covers under this state is unmeasured. PPSA03541 reaches a rect-list draw in this state, and PPSA31246 a line list. Other locations of sample 0, absent location registers, points, alpha-to-coverage and conservative rasterization throw
- [A multisampled target rasterized without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - not measured on hardware: `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear puts every color, depth and stencil sample at the pixel center, as in GL without multisampling ([ARB_multisample](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089/extensions/ARB/ARB_multisample.txt#L185), [RADV 061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11)). Lines cover whole pixels by the host's line rule, not the console's aliased one ([D3D11.3](https://github.com/microsoft/DirectX-Specs/blob/f37b0d6033a1/d3d/archive/D3D11_3_FunctionalSpec.htm#L13705)). Depth sample tests and occlusion counts are unknown. Rasterized points, alpha-to-coverage, and Z interpolation, overrasterization or a sample-mask export with depth throw. PPSA06888 draws a 2x target, PPSA18216 a 4x depth and color pair and 4x lines.
- [sceAgcDriverGetEqContextId](../../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw
- [sceAgcDriverGetEqContextId](../../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - a graphics end-of-pipe event carries `queue | context << 16`, context being the raising `RELEASE_MEM`'s dword 7 masked to 28 bits, as the PS5 13.60 kernel builds the event; `sceAgcDriverGetEqContextId` returns `data >> 16` and `sceAgcDriverGetEqEventType` the low 32 bits, as the PS5 10.01 module reads them. The kernel also packs the pipe, ME, generation and VMID into the low bits; only the queue is filled here, and the other fields are not measured. A typed context (bit 27) is routed elsewhere by the kernel and throws. Other event filters throw
- [User data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed
- [SET_PREDICATION](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw
- [sceAgcDriverUserDataWritePacket, sceAgcDriverUserDataWritePushMarker, sceAgcDriverUserDataWriteSetMarker, sceAgcDriverUserDataWritePopMarker](../../../core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp) (libSceAgcDriver) - the packets follow firmware 13.42's libSceAgcDriver, read from disassembly, not measured on a console: a SET_UCONFIG_REG of register 0x342 (and 0x343 with up to four data bytes), then a WRITE_DATA of the remaining bytes to register 0xC343. The second argument is named `asyncCompute` because firmware libSceAgc passes 1 from its `sceAgcAcb*` marker calls and 0 from `sceAgcDcbPopMarker`; 0 sets header bit 2 in both packets. The marker kinds (0xC0000000 set, 0xC1000000 push, 0xC2000000 pop) and the colour packing are copied without names
@@ -337,7 +337,6 @@
- [Descriptor pool recycling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) frees sets individually only on lavapipe: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the reset-only path rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured.
- [16-bit depth with stencil](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is stored as `D24_UNORM_S8_UINT`, or `D32_SFLOAT_S8_UINT`, on a device without `D16_UNORM_S8_UINT` as an attachment. Depth comparisons, clears and the depth image see the wider format's precision, so results at the 16-bit quantization boundary can differ from the console; the constant depth bias is scaled by 256 for D24, and a non-zero constant on the D32 float fallback throws. Not measured on console hardware.
- [Depth surfaces created over storage writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth surface created without HTILE copies its depth plane from the storage image at the plane's address, a pending one first, when that image has the plane's extent, one level, a 2D view or a 2D array view (its layer 0 is copied) and a 16-bit format for a 16-bit plane or a 32-bit format for a 32-bit plane. A multisampled surface, a surface the device stores in another format, a surface with HTILE and a surface without such an image start from `DB_DEPTH_CLEAR`, and the stencil plane from `DB_STENCIL_CLEAR`; CPU writes to the plane's memory are not read. Not measured on the console, where the plane is that memory.
- [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing while its own release interrupts keep arriving. The latch is cleared when the queue has flipped five times without another interrupt from its own releases: the fifth flip delivers the event and unlatched the queue, so a title that waits on the flip event after latching recovers only if it submits further flips. The bound cannot be by time: the next release interrupt can arrive seconds later than a suppressed flip across loading stalls, and delivering an unexpected flip event on a queue that pairs flips with releases can crash; no fixed window short enough to unblock a waiting title keeps such a title alive. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue.
- [Heap mirror reference storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ReferenceStorage.cpp) (libSceAgcDriver): the 8-buffer/2 GiB idle cap and 2 s reuse window are unmeasured choices. Idle release is polled by a take, an eligible return, a heap mirror sweep or a present; without another poll, expired buffers remain until process shutdown. A title taking references less often than every 2 s reallocates them. The cap covers idle storage only, not buffers still held by active references.
- [Host imports across devices](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): the import registry serves one `VkDevice` at a time, so a batch recorded before a switch to another device and back can write the same host pages through its old import and a new one. A request from another device parks the current device's imports until their work retires, imports the ranges again and probes the write watch again (`decideImportWatch`). In `agc_driver_htile_mask_lifetime`, with that probe between the two imports, the pages kept the earlier `vkCmdFillBuffer`'s value instead of the later one's in about nine of ten runs on NVIDIA 580.178.04 (GTX 1050 Ti); without the probe (`APS5_WRITE_WATCH_IMPORTS=watch` or `unwatch`) and on ANV and llvmpipe they held the later one's. Only tests open a second device; that test checks the second device's pending HTILE before the first device imports again.
- [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less half that heap, kept between 2 and 4 GiB (imports, type-3 mirrors and buffers and textures placed in host memory share it; on a 16 GiB machine whose host heap is half the memory, a 2 GiB reserve left too little for the rest, and on a 32 GiB machine a reserve of half its 16 GiB heap left imports 8 GiB, less than the 10466 MiB one title holds at once). Where the device enables `VK_EXT_memory_budget`, that host heap's reported budget less its other usage and an eighth, in 64 MiB steps, read at most once a second, also bounds the default budget once an import's `vkAllocateMemory` has failed; a set `APS5_HOST_IMPORT_MIB` is never lowered by the report. The report's other usage includes the host-visible copies and heap mirrors that stand in for refused imports, so it grows with the refusals; the first time the report would lower the budget, `[gpu] host import budget: host heap ...` prints the heap's reported budget and usage and the live imports. An import's `vkAllocateMemory` failure within an eighth of the budget, or the third in a row, lowers the budget to the live imports until the device or the configured budget changes; an isolated failure further below only marks its range failed. Live imports are not capped by `maxMemoryAllocationCount`: NVIDIA's Windows driver reports 4096, while Village held 27,167 live imports without a failure. A new import beyond it retires the least recently used imports not used in the current or the previous frame (a frame ends at a flip packet or a CPU flip), so a set used in every frame keeps its imports and what does not fit is copied, as below the budget; until two frames have ended imports age by lookups, an import being evictable 256 lookups after its last use; after that, without flips (a loading stretch, compute-only work) no frame ends and nothing more becomes evictable, so imports that do not fit are copied instead of evicting by recency; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. An address-based build does not evict either, except for a range the budget refused (or remembers refusing) and no mirror serves whose copy would take the build's copies past `APS5_ADDRESS_COPY_MAX_MIB`; that eviction also takes imports the recency rule protects whose single range is releasable and write-watched, so a heap mirror can serve it, as long as the build has not taken them yet. A heap mirror, sparse or not, whose range has left the write watch is refreshed in full at each use, every block counted as written, unless the build can import its range first, evicting for it as the copy cap does, with the imports of earlier builds no longer held; a build plans its range again only once the import path takes the range. A request that may not evict (an address-based build, an import when a range is mapped) remembers a refused range or block, with the live bytes and the budget at the refusal, and is answered from that memory, still counted as refused, until live imports drop below those bytes, the budget changes or one of its ranges changes. A request that may evict remembers its refusal too, answering later requests for the same run from memory with the room the refusal left (a request whose range fits in that room steps down to a smaller block without scanning the imports again, and a refusal that stepped down is remembered for each block it tried), while no import was made or retired, the budget has not grown, the import window and the run are unchanged, and no import can have become evictable since: no frame has ended (or, before two frames have ended, 256 lookups have not passed), and no build holding an import two frames old has raised its floor. A join remembers its refusal the same way, apart from other requests for the same run, so a buffer bound across refused ranges does not try the join again at every upload. The shutdown line `[gpu] host imports at shutdown` gives the live, made, refused (and remembered) and failed counts per requester. The 64 MiB window, the two-frame protection, the 2 GiB reserve, the eighth, the 64 MiB steps and the three failures are choices, not measured.
@@ -345,6 +344,7 @@
- [Device-local staging](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written or atomic buffer element inside a host import, including one an address-based compute build reaches through its pointers, is copied to device memory before the dispatch and copied back after it. The copy-back stores only the bytes the shader changed and the import still holds at their copy-in value, so a CPU store in between survives; where both changed the same byte, the CPU's byte is kept, whichever the console would store last. The whole range is still copied back, rolling CPU stores back, at the unaligned ends of a range, for an element or import offset that is not 4-byte aligned, without a shadow copy or buffer device addresses, with `APS5_NO_CHANGED_COPY_BACK`, and when a synchronous draw stores a GPU-copied region from the CPU (no copy-back recorded, no copy-in snapshot). A CPU store made while the copy-back runs can still be overwritten by it.
- [Write-back conflicts with CPU stores](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) - a buffer region the CPU copies for a dispatch is stored back with `WriteChangedKeepingGuest`: a byte takes the shader's value only where the shader changed it and guest memory still holds the uploaded value, so a CPU store made after the upload wins where both changed the same byte. A CPU store that restores the uploaded value is indistinguishable from no store and takes the shader's byte. Writable image mirrors, heap mirrors, storage image write-backs and tiled colour targets use `WriteChanged`, which stores every byte the GPU changed: a CPU store into the same byte between the upload and the write-back is overwritten there, as before; a CPU store into a byte the GPU left alone survives on every path. Which side the console would store last is not measured.
- [Committed reads of released pages](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) - `ReadCommitted` and `EqualsCommitted` find the readable pages of a range first and read them afterwards. On Windows they read through `ReadProcessMemory`, so a page released in between reads as zero and is left out of the comparison instead of faulting: Demon's Souls (PPSA01342) samples a texture at 0x38400 of 352 MiB while it loads the tutorial level, a range that holds no guest allocation and on Windows covers host heap pages other threads release, which stopped it with an access violation. A texture outside every registered guest allocation still takes the host bytes found there, as it did before; what the console returns for it is not measured. On Linux the pages are copied directly, so a page released between the check and the copy still faults.
- [Flip submissions hold the next flip until their work completed](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - a queue-0 submission that carries a flip in its command buffer returns only after every earlier flip submission ran to its end and its recorded GPU work completed and was reaped, so the labels a frame writes after its flip are in memory when the title reads them after its next submission. A CPU flip (`sceVideoOutSubmitFlip`) does not wait, and the next flip is still released before the previous one is presented. Flips raise no graphics end-of-pipe event (from the PS5 13.60 kernel: a flip's interrupt context is always typed and goes to the display path, never to this event). Not measured: how far the console lets a title run ahead of frames its GPU has not finished.
- [Periodic pipeline cache saves skip busy intervals](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp) (libSceAgcDriver) - the saver thread snapshots the Vulkan pipeline cache only when no pipeline creation holds it (`TryLockForSnapshot`); otherwise it skips that 10 s interval. A blocking snapshot queued behind a long creation (an NVIDIA compile of a 1.5M-word compute pipeline runs for minutes) makes the writer-preferring `shared_mutex` hold every later pipeline creation on every queue until that compile ends, which froze Yōtei's (PPSA26344) loading at frame 797-799 for the whole compile. While creations overlap without a gap, no periodic save happens, so a crash in that stretch loses the pipelines built since the last save; `SaveNow` and the teardown save still wait for the lock. How often busy stretches skip saves in practice is not measured.
- [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known.
+1 -1
View File
@@ -30,7 +30,7 @@
- [sceAjmBatchJobInlineBuffer](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - the behaviour is firmware 13.42's routine, [run in an x86-64 emulator](https://github.com/boykopovar/AnyPS5/pull/11238#issuecomment-6418132093) for `libSceAjm.sprx` and `libSceAjmi.sprx`, not measured on a console: bits 5-31 of the old header word are kept with kind 7, as in [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/ajm/ps4_libsceajm.pas#L399-L410), the second word holds the low 32 bits of the size rounded up to 8, the data follows with the padding left as it was, the batch address points at the copy, and nothing is checked
- [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)
- [sceAjmStrError](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjm.native) - throws as unimplemented. The console's texts were read by calling its own export in PPSA01317 on a PS5 (firmware 13.40, reported on #3205): `ok` for 0, 24 lowercase texts without a full stop for 0x80930002 to 0x80930019 (`context does not exist` to `specified memory area is not registered`), and `(unknown)` for every other value including 0x80930000, 0x80930001 and 0x8093001a; none is implemented here. 14 titles and modules import it. Two were disassembled for this entry: the cri_ware_unity plugin of PPSA25936 calls it once (0x120929) after a negative Ajm result and passes the returned string with the code to a logging or formatting function; PPSA07426 calls it after a negative `sceAjmBatchWait` result (0xe863f0) and, after a success, for two job codes (0xe862f9 and 0xe86308) when the first is nonzero, and formats the strings into a text buffer. What those functions do with the strings was not followed, and the other importers were not read.
- [sceAjmStrError](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjm.native) - returns the console's texts, read by calling its own export in PPSA01317 on a PS5 (firmware 13.40, reported on #3205): `ok` for 0, 24 lowercase texts without a full stop for 0x80930002 to 0x80930019 (`context does not exist` to `specified memory area is not registered`), and `(unknown)` for every other value including 0x80930000, 0x80930001 and 0x8093001a. Only that firmware and title were measured; other PS5 firmware versions are unverified, and a later PS4 report differs on 0x80930018 and 0x80930019. 14 titles and modules import it. The cri_ware_unity plugin of PPSA25936 (0x120929) and PPSA07426 (0xe863f0, 0xe862f9, 0xe86308) were disassembled: they pass the strings to logging or text formatting; what those functions do with them was not followed, and the other importers were not read.
- [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
+1
View File
@@ -15,6 +15,7 @@
### Silent stubs
- [sceSysmoduleIsLoaded, sceSysmoduleLoadModule, sceSysmoduleUnloadModule](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libSceSysmodule) accept the 141 ids `sceSysmoduleIsLoaded` did not reject in a payload process on a PS5 (firmware 13.60) and return `0x805A1000` for every other id, as the console does; that includes 51 ids the module table names from older firmware, such as 0x0001 (`libSceAudioOut`) and 0x0080 (`libSceJson`). Load and unload are counted as measured. The 21 accepted ids the table does not name (0x0111, 0x0117, 0x011c to 0x011f, 0x0121 to 0x0124, 0x0129, 0x012c to 0x012e, 0x0133, 0x0134, 0x0136 and 0x013a to 0x013d) are logged and return 0 without loading anything, so `sceSysmoduleIsLoaded` reports them as loaded; which module each loads is not known. Loading id 0x00ff ended the payload process. One title (PPSA04354) calls `sceSysmoduleIsLoaded` with id 0x96 at twelve sites and loads it only when the result is 0x805a1001; any other result lets it continue.
- [sceSysmoduleIsLoadedInternal, sceSysmoduleLoadModuleInternal, sceSysmoduleUnloadModuleInternal, sceSysmoduleLoadModuleInternalWithArg, sceSysmoduleUnloadModuleInternalWithArg](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libSceSysmodule) - an internal id missing from the module table throws, except 0x8000ffff and 0x80ffff00 in `sceSysmoduleIsLoadedInternal`, `sceSysmoduleLoadModuleInternal` and `sceSysmoduleUnloadModuleInternal`, and 0x00ffffff in `sceSysmoduleIsLoadedInternal`, which return `0x805A1000` as those functions did for them in a payload process on a PS5 (firmware 12.70); the `WithArg` variants throw for them too. Which other internal ids the console rejects, and what the `WithArg` variants return for a missing id, is not measured; `sceSysmoduleIsLoadedInternal(0x800000c4)`, also missing from the table, returned `0x805A1001` there and throws here.
### Unknown function info
+1
View File
@@ -112,6 +112,7 @@
- [ungetwc, fgetwc, fputwc, fputws and fgetws](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - a null stream returns -1 (or null) with errno set to `EINVAL` (22), aligning with wide stream defensive parameter checks. In the `libc.prx` shipped by titles (measured on PPSA07923), the console does not validate the stream pointer and faults with an access violation on `ungetwc`, `fgetwc`, `fputwc`, `fputws` and `fgetws` when passed a null stream.
- [__udivti3](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc, re-exported by libSceAmpr) - the hardware traps on a zero divisor; this throws instead
- [__umodti3](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - the hardware traps on a zero divisor; this throws instead
- [std::_Throw_C_error](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) follows disassembly of a bundled PS5 `libc.prx`: maps 1 and 2 to generic error 35 (`resource_unavailable_try_again`), 3 to 16 (`device_or_resource_busy`), 4 to 22 (`invalid_argument`), and 5 to 1 (`operation_not_permitted`), throwing a guest `std::system_error` through the existing C++ error helper. Other values return without throwing. Other SDK versions have not been inspected
- [__modti3](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - the hardware traps on a zero divisor; this throws instead
- [snprintf and vsnprintf](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) accept any size up to `SIZE_MAX`, as a payload process on a PS5 (firmware 13.60) does. A null buffer with a nonzero size throws here, where the console writes through it, and a result longer than `INT_MAX` throws, which was not measured
+2
View File
@@ -335,11 +335,13 @@
- [scePthreadCondattrGetclock](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - a null clock pointer with a valid attribute throws `std::invalid_argument`, where a payload process measured on a device faults. A null or destroyed attribute with a null output returns `SCE_KERNEL_ERROR_EINVAL` (`0x80020016`) because [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/d106002967/lib/libthr/thread/thr_condattr.c#L82-L90) checks the attribute first; that pair was not measured. Inaccessible non-null pointers and stale handles are not validated
### Functional
- [revoke](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) - a character device throws, because device revocation is not modelled. Other paths follow FreeBSD 11 `sys_revoke`: path errors as `stat` reports them, and `EINVAL` for anything that is not a character device. Guest paths have no `/dev`, so `/dev/null` and the like fail with `ENOENT`, not as devices
- [sceKernelGetUpdVersion](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) - runs the same code as `sysKernelGetUpdVersion` on firmware 13.42 ([disassembly](https://github.com/boykopovar/AnyPS5/pull/10671#issuecomment-6398286973)), so it forwards there: a null output is refused with `SCE_KERNEL_ERROR_EINVAL`, and otherwise the 4 bytes come from `machdep.upd_version`, whose retail value is unmeasured (see libkernel's entry)
- [get_vm_map_timestamp](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys), firmware 13.60 `libkernel_sys.sprx`, syscall 624, returns the low 32 bits of the guest-allocation generation. That value advances whenever a guest-allocation registry scope ends, including lookups (`malloc_usable_size`) and every guest heap allocation or free, and whenever a tracked range is invalidated. FreeBSD advances a map's timestamp only when the map is write-locked, so the value changes far more often than the console's would. Host mappings made outside the tracker can be missed. Not hardware-validated.
- [kenv](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_kenv`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_environment.c#L87-L204) for a caller in a jail with the default parameters: `KENV_SET` and `KENV_UNSET` fail with `EPERM` before the name is read, as [`prison_priv_check`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_jail.c#L3060) denies `PRIV_KENV_SET` and `PRIV_KENV_UNSET` there. That a program runs jailed as uid 0 is the model agreed for the credential checks, not measured on a console. The environment therefore stays empty, since the console's boot environment is unmeasured: `KENV_GET` reads `ENOENT` and `KENV_DUMP` returns 0. A name is read with the guest path copy, which reads up to 1024 bytes, so an unterminated string followed by an unreadable page fails with `EFAULT` where FreeBSD's bounded `copyinstr` gives `ENAMETOOLONG`. Console behaviour is unverified
- [sched_rr_get_interval](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) accepts pid 0 or the host-backed guest pid and reports the FreeBSD 11 ULE default time slice for the usual AMD64 clock rates (94 ms); other pids return `ESRCH`, and a null interval returns `EFAULT`. AnyPS5 does not run a guest ULE scheduler or track a per-process configured quantum, so this reports the modeled default and does not reflect runtime scheduling. Console-specific clock rates and quantum configuration are unverified
- [pwrite](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) a one-byte write at `INT64_MAX` returns `EINVAL` here. The console table reports `EFBIG` for a maximum-offset row whose count, flags and payload are not stated, so the difference is unverified. It shares the POSIX provider and remaining limitations of [libkernel](libkernel.md); no error remapping is applied.
- [is_in_sandbox and sceKernelIsInSandbox](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) return 1 as a title process runs in a sandbox container jail, matching shadPS4 (process.cpp:15-17)
- [fchdir](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open, `EINVAL` for a pipe or socket, as FreeBSD `getvnode` does for a descriptor without a vnode, and `ENOTDIR` for any other descriptor that is not a directory. Outside Windows it checks search permission on the directory with `faccessat`, which fails with `EACCES` as FreeBSD `change_dir` does; on Windows that check is not modelled. A directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified
- [openat, _openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) pass `AT_FDCWD` (-100) and absolute paths to `open`. 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. They return `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NONBLOCK` is ignored. PPSA99003 opens directory entries through `openat`. Console behaviour is unverified
+1 -1
View File
@@ -163,7 +163,7 @@
- [f32 min and max in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_min_f32`, `v_max_f32`, `v_min3_f32`, `v_max3_f32`, `v_med3_f32`) throw in mode 1 when the module cannot declare `DenormPreserve 32` (no `shaderDenormPreserveFloat32`, or the hardware f32 flush in use), and mode 3 keeps depending on the driver there; the PS5's gfx1013 is not measured. They follow only bit 0 of the `FLOAT_MODE` f32 denormal field: mode 2 flushes denormal inputs to zeros of their sign as mode 0 does, and mode 1 keeps them and returns a selected denormal unflushed as mode 3 does. Measured on RDNA2 (gfx1030, RX 6800) over the 2,744 operand triples of ±0, ±2^-149, ±0x007fffff, ±2^-126, ±1, ±infinity, a quiet and a signaling NaN at IEEE 0 and 1 in each f32 denormal mode, round to nearest even, DX10_CLAMP=1, f16 denormals kept, FP16_OVFL=0: modes 1 and 3 give identical results, and so do modes 2 and 0.
- [v_fract_f32, v_floor_f32, v_ceil_f32 and v_frexp_mant_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) throw in mode 1, bar `v_frexp_mant_f32`, when the module cannot declare `DenormPreserve 32`; `v_sin_f32` and `v_cos_f32` still throw in both modes, and the PS5's gfx1013 is not measured. Mode 2 flushes a denormal input to a zero of its sign as mode 0 does. Mode 1 keeps it as mode 3 does (`v_floor_f32` of -2^-149 is -1) and flushes a denormal result to a zero of its sign, which only `v_fract_f32` of a positive denormal gives. Measured on RDNA2 (gfx1036, AMD Windows driver 32.0.21030.31 through HIP) over 118 inputs (zeros, denormals, minimum normals, values around 0.5, 1 and 2^23, the largest finite value, infinities, quiet and signaling NaNs, of both signs, and 64 random denormals) at IEEE 0 and 1 in every f32 denormal and round mode: modes 2 and 0 give identical results, and modes 1 and 3 differ only in those flushed results.
- [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler
- [16-bit integer multiplies](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`v_mul_lo_u16`, `v_pk_mul_lo_u16`) (shader recompiler) - a factor taken from the low half of its source is read as the low 24 bits of the register, not the low 16; the low 16 bits of the product are the same for every input. The AMD Windows driver 32.0.21045.5002 (RX 6700 XT, RDNA 2) stores the full 32-bit product when two values it knows fit in 16 bits are multiplied, the product is reduced to 16 bits and nothing else is merged into the stored value: `v_mul_lo_u16` into a register whose high half is zero, also with factors masked to 16 bits by the program, and `v_pk_mul_lo_u16` whose high lanes are zero wrote 0xfffe0001 for 0xffff * 0xffff where the GPU writes 0x00000001. Reducing the product with `v_and_b32`, `v_bfe_u32` or a shift pair loses the same bits; factors read as 24-bit fields do not. The [constant folder](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) keeps a field read that a multiply uses, even when the program already masked the factor to 16 bits, since folding it away brings the full product back (#8271). Measured against the same instructions run through the [hardware oracle](../HW_ORACLE.md) on that GPU; other drivers are not checked
- [16-bit integer multiplies](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`v_mul_lo_u16`, `v_pk_mul_lo_u16`) (shader recompiler) - a factor from the low half of its source is read as the low 24 bits, not the low 16; the product's low 16 bits do not change. The AMD Windows driver 32.0.21045.5002 (RX 6700 XT) stores the full 32-bit product when both factors are known to fit 16 bits and nothing else is merged into the stored value (0xfffe0001 for 0xffff * 0xffff, where the GPU writes 0x00000001); reducing the product with `v_and_b32`, `v_bfe_u32` or a shift pair loses the same bits, 24-bit factor reads do not. The [constant folder](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) keeps a field read that a multiply uses, directly or through an xor with a constant, even when the factor is already masked to 16 bits, since folding it away brings the full product back (#8271). Measured with the [hardware oracle](../HW_ORACLE.md) on that GPU
- [f32 compares in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_cmp_X_f32` and `v_cmpx_X_f32` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f32 denormal inputs by bit 0 of the `FLOAT_MODE` f32 denormal field: mode 1 keeps them as mode 3 does, and mode 2 flushes them as mode 0 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 484 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3, f32 round to nearest even, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0. The PS5's gfx1013 is not measured.
- [f32 conversions in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_pknorm_i16/u16_f32`, `v_cvt_pk_u8_f32`, `v_cvt_pkrtz_f16_f32` and `v_cvt_f16_f32`) flush their f32 denormal inputs by bit 0 of the `FLOAT_MODE` f32 denormal field: mode 1 keeps them as mode 3 does, and mode 2 flushes them as mode 0 does. Measured on RDNA2 (gfx1036, ROCm 7.1.1, Linux 7.2) over 148 sources and 484 operand pairs (zeros, denormals, minimum normals, f16 limits, the largest finite value, infinities, NaNs) at IEEE=0 and IEEE=1 in every f32 denormal and round mode, and for the f16 and f64 results in every f16/f64 denormal and round mode. `v_mad_mixlo/mixhi_f16`, which RDNA2 lacks, still throw in modes 1 and 2. The PS5's gfx1013 is not measured.
- [v_div_scale_f32 and v_div_fixup_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) throw with a directed f32 round mode, where an underflowing `v_div_fixup_f32` result rounds to the smallest denormal and bit 1 of the `FLOAT_MODE` f32 denormal field flushes it, and in mode 1 `v_div_scale_f32` throws when the module cannot declare `DenormPreserve 32`; the PS5's gfx1013 is not measured. At round to nearest even `v_div_fixup_f32` follows bit 0 (mode 1 as mode 3, mode 2 as mode 0), and `v_div_scale_f32` flushes its inputs in mode 2 as in mode 0 and in mode 1 keeps them and flushes a denormal result to a zero of its sign, with the VCC of mode 3. Measured on RDNA2 (gfx1036, AMD Windows driver 32.0.21030.31 through HIP) over 2,304 rows (24 x 24 operand pairs with src0 the denominator and the numerator, four quotients) at IEEE 0 and 1 in every f32 denormal and round mode.
+350
View File
@@ -0,0 +1,350 @@
import json
import os
from pathlib import Path
import struct
import subprocess
import sys
import tempfile
import time
import unittest
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import title_run
class TitlesTests(unittest.TestCase):
def setUp(self):
directory = tempfile.TemporaryDirectory()
self.addCleanup(directory.cleanup)
self.root = Path(directory.name)
def write(self, data):
path = self.root / "titles.json"
path.write_text(json.dumps(data), encoding="utf-8")
return path
def test_repository_file_is_valid(self):
titles = title_run.load_titles(title_run.ROOT / "tools/titles.json")
self.assertEqual(titles["PPSA85300"], {"relink": ["--registry"]})
def test_unknown_field_is_refused(self):
with self.assertRaisesRegex(ValueError, "PPSA00001: unknown field rlink"):
title_run.load_titles(self.write({"PPSA00001": {"rlink": []}}))
def test_wrong_type_is_refused(self):
with self.assertRaisesRegex(ValueError, "PPSA00001: seconds must be int"):
title_run.load_titles(self.write({"PPSA00001": {"seconds": "60"}}))
def test_non_string_items_are_refused(self):
with self.assertRaisesRegex(ValueError, "PPSA00001: env must hold strings"):
title_run.load_titles(self.write({"PPSA00001": {"env": {"APS5_X": 1}}}))
with self.assertRaisesRegex(ValueError, "PPSA00001: relink must hold strings"):
title_run.load_titles(self.write({"PPSA00001": {"relink": [1]}}))
def test_bad_id_is_refused(self):
with self.assertRaisesRegex(ValueError, "NOT-AN-ID is not a title ID"):
title_run.load_titles(self.write({"NOT-AN-ID": {}}))
def test_name_is_refused(self):
with self.assertRaisesRegex(ValueError, "PPSA00001: unknown field name"):
title_run.load_titles(self.write({"PPSA00001": {"name": "Light"}}))
def test_default_entry(self):
self.assertEqual(title_run.relink_args({}, "windows"), ["--windows"])
self.assertEqual(title_run.relink_args({}, "linux"), [])
def test_entry_on_windows_and_linux(self):
entry = {"relink": ["--registry"], "fixed_base": "0x400000", "executables": ["picker/picker.bin"]}
self.assertEqual(title_run.relink_args(entry, "windows"),
["--windows", "--registry", "--windows-fixed-base", "0x400000", "--executable", "picker/picker.bin"])
self.assertEqual(title_run.relink_args(entry, "linux"),
["--registry", "--linux-fixed-base", "0x400000", "--executable", "picker/picker.bin"])
def test_fixed_base_on_macos_is_refused(self):
with self.assertRaisesRegex(ValueError, "fixed_base is not supported on macos"):
title_run.relink_args({"fixed_base": "0x400000"}, "macos")
def bmp(path, width, height, color):
rows = b"".join(bytes(channel for x in range(width) for channel in (*color(x, y), 255)) for y in range(height))
header = struct.pack("<2sIHHI", b"BM", 54 + len(rows), 0, 0, 54)
info = struct.pack("<IiiHHIIiiII", 40, width, -height, 1, 32, 0, len(rows), 0, 0, 0, 0)
path.write_bytes(header + info + rows)
return path
class AnalysisTests(unittest.TestCase):
def setUp(self):
directory = tempfile.TemporaryDirectory()
self.addCleanup(directory.cleanup)
self.root = Path(directory.name)
def test_first_error(self):
text = "Transferring control\n what(): sysctl_nid_postfix: unsupported MIB 1.1\nFATAL: abort()\n"
self.assertEqual(title_run.first_error(text), "what(): sysctl_nid_postfix: unsupported MIB 1.1")
self.assertEqual(title_run.first_error("radv/ps5: the GPU has not reached submission 12 (at 11) in 10 s\nVkResult is ERROR_DEVICE_LOST\n"), "radv/ps5: the GPU has not reached submission 12 (at 11) in 10 s")
self.assertEqual(title_run.first_error("Converting\nFAIL: Forbidden syscall instruction at code offset 0x1\nInput: x\n"), "FAIL: Forbidden syscall instruction at code offset 0x1")
self.assertEqual(title_run.first_error("all fine\n"), "")
def test_warning_counts(self):
text = "[gpu] storage image 0x1 destroyed with GPU results pending\n" * 2
self.assertEqual(title_run.warning_counts(text)["destroyed with GPU results pending"], 2)
def test_read_summary(self):
path = self.root / "aps5-summary.jsonl"
lines = [{"presents": 300, "window": {"ms": 10000}, "gpu": "A", "driver": "1", "os": "W"},
{"presents": 200, "window": {"ms": 10000}, "gpu": "B", "driver": "2", "os": "W"}]
path.write_text("".join(json.dumps(line) + "\n" for line in lines), encoding="utf-8")
self.assertEqual(title_run.read_summary(path), {"presents": 500, "fps": 25.0, "gpu": "B", "driver": "2", "os": "W"})
path.write_text(path.read_text(encoding="utf-8") + '{"presents": 3', encoding="utf-8")
self.assertEqual(title_run.read_summary(path)["presents"], 500)
path.write_text(path.read_text(encoding="utf-8") + "\n[1, 2]\n", encoding="utf-8")
self.assertEqual(title_run.read_summary(path)["presents"], 500)
self.assertEqual(title_run.read_summary(self.root / "missing.jsonl")["presents"], 0)
def test_frame_is_uniform(self):
black = bmp(self.root / "black.bmp", 8, 4, lambda x, y: (0, 0, 0))
noisy = bmp(self.root / "noisy.bmp", 8, 4, lambda x, y: (3, 2, 1) if x < 7 else (200, 200, 200))
nearly = bmp(self.root / "nearly.bmp", 8, 4, lambda x, y: (x, 0, 0))
self.assertTrue(title_run.frame_is_uniform(black))
self.assertFalse(title_run.frame_is_uniform(noisy))
self.assertTrue(title_run.frame_is_uniform(nearly))
truncated = self.root / "truncated.bmp"
truncated.write_bytes(black.read_bytes()[:40])
self.assertIsNone(title_run.frame_is_uniform(truncated))
cut = self.root / "cut.bmp"
cut.write_bytes(noisy.read_bytes()[:-20])
self.assertIsNone(title_run.frame_is_uniform(cut))
def test_classify_reads_the_last_frame(self):
uniform = bmp(self.root / "uniform.bmp", 8, 4, lambda x, y: (9, 9, 9))
varied = bmp(self.root / "varied.bmp", 8, 4, lambda x, y: (x * 30, y * 60, 0))
truncated = self.root / "truncated.bmp"
truncated.write_bytes(varied.read_bytes()[:-20])
for frame, outcome in ((uniform, "blank"), (varied, "rendering"), (truncated, "frame-unreadable")):
self.assertEqual(title_run.classify(True, None, 5.0, True, title_run.frame_is_uniform(frame)), outcome, frame.name)
def test_classify(self):
cases = [
((False, None, None, False, None), "relink-failed"),
((True, 3221226505, None, False, None), "crashed"),
((True, 0, None, False, None), "exited"),
((True, None, None, False, None), "stalled"),
((True, None, 25.0, True, False), "stalled"),
((True, None, 5.0, False, None), "stalled"),
((True, None, 5.0, True, True), "blank"),
((True, None, 5.0, True, False), "rendering"),
((True, None, 5.0, True, None), "frame-unreadable"),
]
for arguments, outcome in cases:
self.assertEqual(title_run.classify(*arguments), outcome, arguments)
def run(outcome, cache="cold", fps=0.0, error=""):
return {"outcome": outcome, "cache": cache, "fps": fps, "first_error": error}
def summary(titles):
return {"build": {"commit": "abc1234", "dirty": False, "note": "main + #6814"},
"machine": {"os": "Windows 11", "gpu": "Radeon 860M", "driver": "32.0.13058.8001", "cpu": "Ryzen AI 7 350"},
"runs": 2, "seconds": 60, "titles": titles}
class ReportTests(unittest.TestCase):
def test_cell(self):
runs = [run("crashed"), run("rendering", "warm"), run("crashed", "warm")]
self.assertEqual(title_run.cell(runs, "cold"), "crashed")
self.assertEqual(title_run.cell(runs, "warm"), "crashed 1/2, rendering 1/2")
self.assertEqual(title_run.cell([run("rendering", "warm"), run("rendering", "warm")], "warm"), "rendering 2/2")
self.assertEqual(title_run.cell([run("rendering")], "warm"), "-")
def test_worst_and_compare(self):
now = summary({"PPSA85300": {"name": "Light", "version": "0.4", "runs": [run("crashed"), run("crashed", "warm")]},
"PPSA97358": {"name": "XPSemu", "version": "A2.1", "runs": [run("rendering"), run("rendering", "warm")]}})
before = summary({"PPSA85300": {"name": "Light", "version": "0.4", "runs": [run("rendering"), run("rendering", "warm")]},
"PPSA97358": {"name": "XPSemu", "version": "A2.1", "runs": [run("stalled"), run("stalled", "warm")]}})
self.assertEqual(title_run.worst(now["titles"]["PPSA85300"]["runs"]), "crashed")
self.assertEqual(title_run.compare(now, before),
([("PPSA85300", "rendering", "crashed")], [("PPSA97358", "stalled", "rendering")]))
def test_render_markdown(self):
now = summary({"PPSA85300": {"name": "Light", "version": "0.4",
"runs": [run("crashed", error="what(): unsupported MIB 1.1 | x"), run("rendering", "warm", 31.0)]}})
before = summary({"PPSA85300": {"name": "Light", "version": "0.4", "runs": [run("rendering"), run("rendering", "warm")]}})
self.assertEqual(title_run.render_markdown(now, before), "\n".join([
"AnyPS5 `abc1234`, main + #6814. Windows 11, Radeon 860M, driver 32.0.13058.8001, CPU Ryzen AI 7 350. 2 runs per title, 60 s each.",
"",
"Regressions: Light (PPSA85300) rendering -> crashed",
"",
"| Title | ID | Version | Cold | Warm | FPS | First error | Before |",
"|---|---|---|---|---|---|---|---|",
"| Light | PPSA85300 | 0.4 | crashed | rendering | 31.0 | what(): unsupported MIB 1.1 \\| x | rendering |",
"",
]))
self.assertNotIn("Before", title_run.render_markdown(now))
now["machine"] = {"os": "", "gpu": "", "driver": "", "cpu": "Ryzen AI 7 350"}
self.assertTrue(title_run.render_markdown(now).startswith("AnyPS5 `abc1234`, main + #6814. CPU Ryzen AI 7 350. 2 runs"))
def test_render_markdown_skips_runs_without_an_error(self):
now = summary({"PPSA01325": {"name": "Playroom", "version": "01.905.000",
"runs": [run("stalled"), run("crashed", "warm", error="vkQueueSubmit recorder: Vulkan result -4")]}})
self.assertIn("| stalled | crashed | 0.0 | vkQueueSubmit recorder: Vulkan result -4 |", title_run.render_markdown(now))
def test_render_markdown_lists_missing_titles(self):
now = summary({"PPSA85300": {"name": "Light", "version": "0.4", "runs": [run("rendering"), run("rendering", "warm")]}})
before = summary({"PPSA85300": {"name": "Light", "version": "0.4", "runs": [run("rendering"), run("rendering", "warm")]},
"PPSA97358": {"name": "XPSemu", "version": "A2.1", "runs": [run("stalled"), run("stalled", "warm")]}})
self.assertIn("Missing: XPSemu (PPSA97358)", title_run.render_markdown(now, before))
def package(root, title_id, version="01.000.000", name="Game"):
(root / "sce_sys").mkdir(parents=True)
(root / "eboot.bin").write_bytes(b"\x7fELF" + bytes(60))
params = {"titleId": title_id, "contentVersion": version,
"localizedParameters": {"defaultLanguage": "en-US", "en-US": {"titleName": name}}}
(root / "sce_sys/param.json").write_text(json.dumps(params), encoding="utf-8")
return root
class PreparationTests(unittest.TestCase):
def setUp(self):
directory = tempfile.TemporaryDirectory()
self.addCleanup(directory.cleanup)
self.root = Path(directory.name)
def test_a_running_title_is_left_alone(self):
package(self.root / "games/PPSA00001", "PPSA00001")
out = self.root / "out"
marker = out / "work/PPSA00001/app0/eboot.bin"
marker.parent.mkdir(parents=True)
marker.write_bytes(b"running")
held = title_run.lock_work(out / "work/PPSA00001.lock")
self.addCleanup(held.close)
self.assertIsNotNone(held)
with self.assertRaisesRegex(SystemExit, "already running in .*: PPSA00001"):
title_run.main(["--games", str(self.root / "games"), "--out", str(out), "PPSA00001"])
self.assertEqual(marker.read_bytes(), b"running")
held.close()
again = title_run.lock_work(out / "work/PPSA00001.lock")
self.assertIsNotNone(again)
again.close()
def test_discover(self):
package(self.root / "a/src/PPSA00001", "PPSA00001", "01.000.110", "Light")
package(self.root / "b/PPSA00002", "PPSA00002")
found = title_run.discover([self.root])
self.assertEqual(sorted(found), ["PPSA00001", "PPSA00002"])
self.assertEqual(found["PPSA00001"]["version"], "01.000.110")
self.assertEqual(found["PPSA00001"]["name"], "Light")
def test_source_root_follows_the_build(self):
build = self.root / "build"
build.mkdir()
self.assertEqual(title_run.source_root(build), title_run.ROOT)
(build / "CMakeCache.txt").write_text("CMAKE_BUILD_TYPE:STRING=Release\nCMAKE_HOME_DIRECTORY:INTERNAL=C:/src/AnyPS5\n", encoding="utf-8")
self.assertEqual(title_run.source_root(build), Path("C:/src/AnyPS5"))
def test_discover_refuses_duplicates(self):
package(self.root / "a/PPSA00001", "PPSA00001")
package(self.root / "b/PPSA00001", "PPSA00001")
with self.assertRaisesRegex(ValueError, "PPSA00001 found more than once"):
title_run.discover([self.root])
def test_discover_refuses_a_package_without_title_id(self):
package(self.root / "a/PPSA00001", "PPSA00001")
param = self.root / "a/PPSA00001/sce_sys/param.json"
param.write_text(json.dumps({"contentVersion": "01.000.000"}), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "param.json: no titleId"):
title_run.discover([self.root])
def test_discover_refuses_a_title_id_that_is_not_one(self):
for index, title_id in enumerate(("../../outside", str(self.root / "elsewhere"), "PPSA0001")):
package(self.root / str(index) / "PPSA00001", title_id)
with self.assertRaisesRegex(ValueError, "is not a title ID"):
title_run.discover([self.root / str(index)])
def test_unwrap_or_copy_keeps_an_elf(self):
source = self.root / "eboot.bin"
source.write_bytes(b"\x7fELF" + bytes(60))
title_run.unwrap_or_copy(source, self.root / "out/eboot.elf")
self.assertEqual((self.root / "out/eboot.elf").read_bytes(), source.read_bytes())
def test_link_tree_skips(self):
source = package(self.root / "pkg", "PPSA00001")
(source / "sce_module").mkdir()
(source / "sce_module/libc.prx").write_bytes(b"x")
title_run.link_tree(source, self.root / "app0", {"sce_module"})
self.assertTrue((self.root / "app0/sce_sys/param.json").is_file())
self.assertTrue((self.root / "app0/eboot.bin").is_file())
self.assertFalse((self.root / "app0/sce_module").exists())
def wait_until(condition, seconds=10):
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
if condition():
return True
time.sleep(0.1)
return False
class SessionTests(unittest.TestCase):
def start(self, code, **options):
session = title_run.Session([sys.executable, "-c", code], Path.cwd(), dict(os.environ), subprocess.DEVNULL, subprocess.DEVNULL, **options)
self.addCleanup(session.kill)
return session
def test_exit_code(self):
session = self.start("import sys; sys.exit(3)")
self.assertTrue(wait_until(lambda: not session.alive()))
self.assertEqual(session.exit_code(), 3)
@unittest.skipUnless(os.name == "nt", "LoadExec hand-over is Windows-specific")
def test_exit_code_of_the_last_process(self):
session = self.start("import subprocess, sys; subprocess.Popen([sys.executable, '-c', 'import time, sys; time.sleep(1); sys.exit(7)'])")
self.assertTrue(wait_until(lambda: not session.alive()))
self.assertEqual(session.exit_code(), 7)
@unittest.skipUnless(os.name == "nt", "job process list is Windows-specific")
def test_exit_code_ignores_processes_outside_owned(self):
child = "import subprocess, time; time.sleep(1); subprocess.run(['cmd.exe', '/c', 'exit 0']); raise SystemExit(7)"
session = self.start(f"import subprocess, sys; subprocess.Popen([sys.executable, '-c', {child!r}])", owned=Path(sys.executable).parent)
self.assertTrue(wait_until(lambda: not session.alive()))
self.assertEqual(session.exit_code(), 7)
@unittest.skipUnless(os.name == "nt", "window placement is Windows-specific")
def test_new_windows_go_behind_the_others_without_polling(self):
window = ("import ctypes; user32 = ctypes.WinDLL('user32'); user32.CreateWindowExW.argtypes = [ctypes.c_uint, ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint] + [ctypes.c_int] * 4 + [ctypes.c_void_p] * 4; "
"user32.CreateWindowExW(0x08000000, 'STATIC', 'title_run test', 0x10CF0000, 100, 100, 200, 200, None, None, None, None); message = ctypes.create_string_buffer(64)\n"
"while user32.GetMessageW(message, None, 0, 0) > 0: user32.DispatchMessageW(message)")
user32 = title_run.user32
if not user32.FindWindowW("Progman", None):
self.skipTest("no desktop shell in this session")
session = self.start(window)
user32.GetWindow.restype = title_run.wintypes.HWND
user32.GetWindow.argtypes = (title_run.wintypes.HWND, title_run.wintypes.UINT)
name = title_run.ctypes.create_unicode_buffer(256)
def below(window):
classes, window = set(), user32.GetWindow(window, 2)
while window:
if user32.IsWindowVisible(window) and user32.GetClassNameW(window, name, 256):
classes.add(name.value)
window = user32.GetWindow(window, 2)
return classes
self.assertTrue(wait_until(lambda: session.windows() and all(below(found) <= {"Progman", "WorkerW"} for found in session.windows())))
rect = title_run.wintypes.RECT()
user32.GetWindowRect(session.windows()[0], title_run.ctypes.byref(rect))
self.assertEqual((rect.left, rect.top), (100, 100))
def test_children_keep_it_alive_and_are_killed(self):
session = self.start("import subprocess, sys; subprocess.Popen([sys.executable, '-c', 'import time; time.sleep(60)'])")
self.assertTrue(wait_until(lambda: session.exit_code() == 0))
self.assertTrue(session.alive())
session.kill()
self.assertTrue(wait_until(lambda: not session.alive()))
if __name__ == "__main__":
unittest.main()
+575
View File
@@ -0,0 +1,575 @@
import argparse
import datetime
import json
import os
import re
import shutil
import signal
import struct
import subprocess
import sys
import threading
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "tools"))
import unwrap_self
from package_release import windows_runtime
OUTCOMES = ("relink-failed", "crashed", "exited", "stalled", "frame-unreadable", "blank", "rendering")
TITLE_ID = re.compile(r"[A-Z]{4}[0-9]{5}")
TITLE_FIELDS = {"relink": list, "fixed_base": str, "executables": list, "seconds": int, "env": dict}
FIXED_BASE_FLAGS = {"windows": "--windows-fixed-base", "linux": "--linux-fixed-base"}
def load_titles(path):
titles = json.loads(Path(path).read_text(encoding="utf-8"))
for title_id, entry in titles.items():
if not TITLE_ID.fullmatch(title_id):
raise ValueError(f"{path}: {title_id} is not a title ID")
for key, value in entry.items():
if key not in TITLE_FIELDS:
raise ValueError(f"{path}: {title_id}: unknown field {key}")
if not isinstance(value, TITLE_FIELDS[key]):
raise ValueError(f"{path}: {title_id}: {key} must be {TITLE_FIELDS[key].__name__}")
items = value.values() if isinstance(value, dict) else value if isinstance(value, list) else []
if not all(isinstance(item, str) for item in items):
raise ValueError(f"{path}: {title_id}: {key} must hold strings")
return titles
def relink_args(entry, platform):
args = ["--windows"] if platform == "windows" else []
args += entry.get("relink", [])
if "fixed_base" in entry:
if platform not in FIXED_BASE_FLAGS:
raise ValueError(f"fixed_base is not supported on {platform}")
args += [FIXED_BASE_FLAGS[platform], entry["fixed_base"]]
for executable in entry.get("executables", []):
args += ["--executable", executable]
return args
ERROR = re.compile(r"what\(\):|FAIL:|FATAL|not implemented|unhandled exception|DEVICE_LOST|has not reached submission")
WARNINGS = ("destroyed with GPU results pending", "guest memory changed while GPU results were pending")
STALL_SECONDS = 20
def first_error(text):
return next((line.strip() for line in text.splitlines() if ERROR.search(line)), "")
def warning_counts(text):
return {warning: text.count(warning) for warning in WARNINGS}
def read_summary(path):
lines = []
for line in path.read_text(encoding="utf-8", errors="replace").splitlines() if path.is_file() else []:
try:
decoded = json.loads(line)
except ValueError:
continue
if isinstance(decoded, dict):
lines.append(decoded)
presents = sum(line.get("presents", 0) for line in lines)
seconds = sum(line.get("window", {}).get("ms", 0) for line in lines) / 1000
last = lines[-1] if lines else {}
return {"presents": presents, "fps": round(presents / seconds, 1) if seconds else 0.0,
"gpu": last.get("gpu", ""), "driver": last.get("driver", ""), "os": last.get("os", "")}
def frame_is_uniform(path, tolerance=8):
data = path.read_bytes()
try:
offset = struct.unpack_from("<I", data, 10)[0]
width, height = struct.unpack_from("<ii", data, 18)
channels = struct.unpack_from("<H", data, 28)[0] // 8
except struct.error:
return None
stride = (width * channels + 3) & ~3
if channels < 3 or len(data) < offset + stride * abs(height) - (stride - width * channels):
return None
first = data[offset:offset + 3]
for row in range(abs(height)):
pixels = data[offset + row * stride:offset + row * stride + width * channels]
for index in range(0, len(pixels), channels):
if max(abs(pixels[index + channel] - first[channel]) for channel in range(3)) > tolerance:
return False
return True
def classify(relinked, exit_code, summary_age, framed, last_frame_uniform):
if not relinked:
return "relink-failed"
if exit_code is not None:
return "crashed" if exit_code != 0 else "exited"
if summary_age is None or summary_age > STALL_SECONDS or not framed:
return "stalled"
if last_frame_uniform is None:
return "frame-unreadable"
if last_frame_uniform:
return "blank"
return "rendering"
def cell(runs, cache):
outcomes = [entry["outcome"] for entry in runs if entry["cache"] == cache]
if not outcomes:
return "-"
if len(outcomes) == 1:
return outcomes[0]
return ", ".join(f"{outcome} {outcomes.count(outcome)}/{len(outcomes)}" for outcome in sorted(set(outcomes), key=OUTCOMES.index))
def worst(runs):
return min((entry["outcome"] for entry in runs), key=OUTCOMES.index)
def compare(summary, previous):
regressions, improvements = [], []
for title_id, title in summary["titles"].items():
if title_id not in previous["titles"]:
continue
before, now = worst(previous["titles"][title_id]["runs"]), worst(title["runs"])
if OUTCOMES.index(now) < OUTCOMES.index(before):
regressions.append((title_id, before, now))
elif OUTCOMES.index(now) > OUTCOMES.index(before):
improvements.append((title_id, before, now))
return regressions, improvements
def render_markdown(summary, previous=None):
build, machine = summary["build"], summary["machine"]
header = f"AnyPS5 `{build['commit']}`" + (" (dirty)" if build["dirty"] else "") + (f", {build['note']}" if build["note"] else "")
parts = [machine["os"], machine["gpu"], machine["driver"] and f"driver {machine['driver']}", machine["cpu"] and f"CPU {machine['cpu']}"]
known = ", ".join(part for part in parts if part)
header += f". {known}." if known else "."
header += f" {summary['runs']} run{'' if summary['runs'] == 1 else 's'} per title, {summary['seconds']} s each."
lines = [header, ""]
columns = ["Title", "ID", "Version", "Cold", "Warm", "FPS", "First error"] + (["Before"] if previous else [])
if previous:
regressions, improvements = compare(summary, previous)
for label, changes in (("Regressions", regressions), ("Improvements", improvements)):
if changes:
names = ", ".join(f"{summary['titles'][title_id]['name']} ({title_id}) {before} -> {now}" for title_id, before, now in changes)
lines += [f"{label}: {names}", ""]
missing = sorted(title_id for title_id in previous["titles"] if title_id not in summary["titles"])
if missing:
names = ", ".join(f"{previous['titles'][title_id]['name']} ({title_id})" for title_id in missing)
lines += [f"Missing: {names}", ""]
lines += ["| " + " | ".join(columns) + " |", "|" + "---|" * len(columns)]
for title_id, title in sorted(summary["titles"].items()):
runs = title["runs"]
error = next((entry["first_error"] for entry in runs if entry["outcome"] != "rendering" and entry["first_error"]), "")
row = [title["name"], title_id, title["version"], cell(runs, "cold"), cell(runs, "warm"), f"{runs[-1]['fps']:.1f}",
error[:120].replace("|", "\\|")]
if previous:
old = previous["titles"].get(title_id)
row.append(worst(old["runs"]) if old else "new")
lines.append("| " + " | ".join(row) + " |")
return "\n".join(lines + [""])
def discover(directories):
found = {}
for directory in directories:
for param in sorted(Path(directory).glob("**/sce_sys/param.json")):
package = param.parent.parent
if not (package / "eboot.bin").is_file():
continue
data = json.loads(param.read_text(encoding="utf-8-sig"))
title_id = data.get("titleId")
if not isinstance(title_id, str):
raise ValueError(f"{param}: no titleId")
if not TITLE_ID.fullmatch(title_id):
raise ValueError(f"{param}: titleId {title_id!r} is not a title ID")
if title_id in found:
raise ValueError(f"{title_id} found more than once: {found[title_id]['package']} and {package}")
localized = data.get("localizedParameters", {})
name = localized.get(localized.get("defaultLanguage", "en-US"), {}).get("titleName", title_id)
found[title_id] = {"package": package, "version": data.get("contentVersion", ""), "name": name}
return found
def unwrap_or_copy(source, target):
target.parent.mkdir(parents=True, exist_ok=True)
if source.read_bytes()[:4] == b"\x7fELF":
shutil.copyfile(source, target)
else:
unwrap_self.unwrap(source, target)
def link_file(source, target):
try:
os.link(source, target)
except OSError:
shutil.copy2(source, target)
def link_tree(source, target, skip):
for path in sorted(source.rglob("*")):
relative = path.relative_to(source)
if relative.parts[0] in skip:
continue
if path.is_dir():
(target / relative).mkdir(parents=True, exist_ok=True)
else:
(target / relative).parent.mkdir(parents=True, exist_ok=True)
link_file(path, target / relative)
def lock_work(path):
path.parent.mkdir(parents=True, exist_ok=True)
handle = path.open("a+b")
try:
if os.name == "nt":
import msvcrt
handle.seek(0)
msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
else:
import fcntl
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
handle.close()
return None
return handle
def prepare(title_id, found, entry, platform, build, work):
package = found["package"]
shutil.rmtree(work, ignore_errors=True)
source = work / "input"
(source / "sce_module").mkdir(parents=True)
unwrap_or_copy(package / "eboot.bin", source / "eboot.elf")
for module in sorted((package / "sce_module").glob("*.prx")):
unwrap_or_copy(module, source / "sce_module" / module.name)
for executable in entry.get("executables", []):
unwrap_or_copy(package / executable, source / executable)
executable = work / (title_id + (".exe" if platform == "windows" else ""))
relinker = build / "core/relinker" / ("relinker.exe" if platform == "windows" else "relinker")
with (work / "relink.log").open("w", encoding="utf-8") as log:
relinked = subprocess.run([str(relinker), *relink_args(entry, platform), str(source / "eboot.elf"), str(executable)],
stdout=log, stderr=subprocess.STDOUT)
if relinked.returncode != 0:
return None
link_tree(package, work / "app0", {"sce_module"})
libs = work / "libs"
libs.mkdir()
for library in sorted((build / "core/libs/libs").glob("*.prx")):
link_file(library, libs / library.name)
if platform == "windows":
runtime = windows_runtime(build)
for name in ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll"):
link_file(runtime / name, libs / name)
return executable
if os.name == "nt":
import ctypes
from ctypes import wintypes
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
user32 = ctypes.WinDLL("user32", use_last_error=True)
kernel32.CreateJobObjectW.restype = wintypes.HANDLE
kernel32.CreateJobObjectW.argtypes = (ctypes.c_void_p, wintypes.LPCWSTR)
kernel32.AssignProcessToJobObject.argtypes = (wintypes.HANDLE, wintypes.HANDLE)
kernel32.TerminateJobObject.argtypes = (wintypes.HANDLE, wintypes.UINT)
kernel32.OpenProcess.restype = wintypes.HANDLE
kernel32.OpenProcess.argtypes = (wintypes.DWORD, wintypes.BOOL, wintypes.DWORD)
kernel32.GetExitCodeProcess.argtypes = (wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD))
kernel32.QueryFullProcessImageNameW.argtypes = (wintypes.HANDLE, wintypes.DWORD, wintypes.LPWSTR, ctypes.POINTER(wintypes.DWORD))
kernel32.GetProcessTimes.argtypes = (wintypes.HANDLE, *(ctypes.POINTER(ctypes.c_ulonglong),) * 4)
kernel32.QueryInformationJobObject.argtypes = (wintypes.HANDLE, ctypes.c_int, ctypes.c_void_p, wintypes.DWORD, ctypes.c_void_p)
WindowCallback = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
user32.EnumWindows.argtypes = (WindowCallback, wintypes.LPARAM)
user32.GetWindowThreadProcessId.argtypes = (wintypes.HWND, ctypes.POINTER(wintypes.DWORD))
user32.IsWindowVisible.argtypes = (wintypes.HWND,)
user32.SetWindowPos.argtypes = (wintypes.HWND, wintypes.HWND, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, wintypes.UINT)
user32.GetSystemMetrics.argtypes = (ctypes.c_int,)
user32.PostMessageW.argtypes = (wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM)
user32.GetWindowRect.argtypes = (wintypes.HWND, ctypes.POINTER(wintypes.RECT))
user32.GetAncestor.restype = wintypes.HWND
user32.GetAncestor.argtypes = (wintypes.HWND, wintypes.UINT)
WinEventCallback = ctypes.WINFUNCTYPE(None, wintypes.HANDLE, wintypes.DWORD, wintypes.HWND, wintypes.LONG, wintypes.LONG, wintypes.DWORD, wintypes.DWORD)
user32.SetWinEventHook.restype = wintypes.HANDLE
user32.SetWinEventHook.argtypes = (wintypes.DWORD, wintypes.DWORD, wintypes.HMODULE, WinEventCallback, wintypes.DWORD, wintypes.DWORD, wintypes.DWORD)
user32.UnhookWinEvent.argtypes = (wintypes.HANDLE,)
user32.GetMessageW.argtypes = (ctypes.POINTER(wintypes.MSG), wintypes.HWND, wintypes.UINT, wintypes.UINT)
user32.PostThreadMessageW.argtypes = (wintypes.DWORD, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM)
def send_to_back(window):
user32.SetWindowPos(window, wintypes.HWND(1), 0, 0, 0, 0, 0x4013)
class ProcessIdList(ctypes.Structure):
_fields_ = [("assigned", wintypes.DWORD), ("listed", wintypes.DWORD), ("ids", ctypes.c_size_t * 256)]
class Session:
def __init__(self, command, cwd, env, stdout, stderr, owned=None):
self.owned = os.path.normcase(os.path.abspath(owned or Path(command[0]).parent))
if os.name == "nt":
self.process = subprocess.Popen(command, cwd=cwd, env=env, stdout=stdout, stderr=stderr, creationflags=subprocess.CREATE_NO_WINDOW)
self.handles, self.images = {}, {}
self.job = kernel32.CreateJobObjectW(None, None)
if not self.job or (not kernel32.AssignProcessToJobObject(self.job, int(self.process._handle)) and self.process.poll() is None):
error = ctypes.get_last_error()
self.process.kill()
raise ctypes.WinError(error)
self.placed = set()
ready = threading.Event()
threading.Thread(target=self.watch_windows, args=(ready,), daemon=True).start()
ready.wait()
else:
self.process = subprocess.Popen(command, cwd=cwd, env=env, stdout=stdout, stderr=stderr, start_new_session=True)
def process_ids(self):
listing = ProcessIdList()
kernel32.QueryInformationJobObject(self.job, 3, ctypes.byref(listing), ctypes.sizeof(listing), None)
ids = set(listing.ids[:listing.listed])
for pid in ids - self.handles.keys():
self.handles[pid] = kernel32.OpenProcess(0x1000, False, pid)
self.images[pid] = self.image_path(self.handles[pid]) if self.handles[pid] else ""
return ids
def windows(self):
ids, found = self.process_ids(), []
def visit(window, _):
owner = wintypes.DWORD()
user32.GetWindowThreadProcessId(window, ctypes.byref(owner))
if owner.value in ids and user32.IsWindowVisible(window):
found.append(window)
return True
user32.EnumWindows(WindowCallback(visit), 0)
return found
def watch_windows(self, ready):
def created(hook, event, window, kind, child, thread, at):
if kind == 0 and child == 0 and user32.GetAncestor(window, 2) == window:
send_to_back(window)
callback = WinEventCallback(created)
hook = user32.SetWinEventHook(0x8000, 0x8002, None, callback, self.process.pid, 0, 0)
self.watcher = kernel32.GetCurrentThreadId()
ready.set()
message = wintypes.MSG()
while user32.GetMessageW(ctypes.byref(message), None, 0, 0) > 0:
pass
user32.UnhookWinEvent(hook)
def alive(self):
self.process.poll()
if os.name == "nt":
return bool(self.process_ids())
try:
os.killpg(self.process.pid, 0)
return True
except PermissionError:
return True
except ProcessLookupError:
return False
def image_path(self, handle):
size = wintypes.DWORD(32768)
buffer = ctypes.create_unicode_buffer(size.value)
return os.path.normcase(os.path.abspath(buffer.value)) if kernel32.QueryFullProcessImageNameW(handle, 0, buffer, ctypes.byref(size)) else ""
def exit_code(self):
if os.name == "nt" and not self.process_ids():
last = None
for pid, handle in self.handles.items():
code, times = wintypes.DWORD(), [ctypes.c_ulonglong() for _ in range(4)]
if (handle and self.images[pid].startswith(self.owned) and kernel32.GetExitCodeProcess(handle, ctypes.byref(code)) and code.value != 259
and kernel32.GetProcessTimes(handle, *map(ctypes.byref, times)) and (last is None or times[0].value >= last[0])):
last = (times[0].value, code.value)
if last:
return last[1]
return self.process.poll()
def send_new_windows_to_back(self):
if os.name == "nt":
for window in set(self.windows()) - self.placed:
send_to_back(window)
self.placed.add(window)
def close(self):
if os.name == "nt":
for window in self.windows():
user32.PostMessageW(window, 0x10, 0, 0)
else:
try:
os.killpg(self.process.pid, signal.SIGTERM)
except (ProcessLookupError, PermissionError):
pass
def kill(self):
if os.name == "nt":
kernel32.TerminateJobObject(self.job, 1)
user32.PostThreadMessageW(self.watcher, 0x12, 0, 0)
else:
try:
os.killpg(self.process.pid, signal.SIGKILL)
except (ProcessLookupError, PermissionError):
pass
self.process.wait()
CLOSE_SECONDS = 15
FRAME_ENV = {"APS5_DUMP_FRAMES": "100000", "APS5_DUMP_FRAMES_EVERY": "30", "APS5_DUMP_SCALE": "8"}
def collect_frames(work, destination):
frames = sorted(work.glob("frame_*.bmp"), key=lambda path: int(path.stem.split("_")[1]))
keep = {frames[0], frames[len(frames) // 2], frames[-1]} if frames else set()
destination.mkdir(parents=True, exist_ok=True)
kept = []
for frame in frames:
if frame in keep:
shutil.move(frame, destination / frame.name)
kept.append(frame.name)
else:
frame.unlink()
return kept
def run_title(executable, work, run_dir, seconds, extra_env):
run_dir.mkdir(parents=True)
inherited = {key: value for key, value in os.environ.items() if key not in ("ANYPS5_SHADER_CACHE_DIR", "ANYPS5_NO_SHADER_CACHE")}
env = {**inherited, "SDL_AUDIODRIVER": "dummy", "SDL_WINDOW_NO_ACTIVATION_WHEN_SHOWN": "1",
"APS5_TRACE_SUMMARY_EVERY": "10", "APS5_TRACE_DIR": str(run_dir), **FRAME_ENV,
"MESA_SHADER_CACHE_DIR": str(work / "driver-cache"), "__GL_SHADER_DISK_CACHE_PATH": str(work / "driver-cache"), **extra_env}
started = time.monotonic()
with (run_dir / "stdout.log").open("wb") as stdout, (run_dir / "stderr.log").open("wb") as stderr:
session = Session([str(executable)], work, env, stdout, stderr, owned=work)
try:
while session.alive() and time.monotonic() - started < seconds:
session.send_new_windows_to_back()
time.sleep(0.1 if time.monotonic() - started < 10 else 0.5)
elapsed = round(time.monotonic() - started, 1)
alive, ended = session.alive(), time.time()
exit_code = None if alive else session.exit_code()
if alive:
session.close()
deadline = time.monotonic() + CLOSE_SECONDS
while session.alive() and time.monotonic() < deadline:
time.sleep(0.5)
finally:
session.kill()
text = "".join((run_dir / name).read_text(encoding="utf-8", errors="replace") for name in ("stdout.log", "stderr.log"))
summary_path = run_dir / "aps5-summary.jsonl"
summary = read_summary(summary_path)
frames = collect_frames(work, run_dir / "frames")
uniform = frame_is_uniform(run_dir / "frames" / frames[-1]) if frames else None
age = ended - summary_path.stat().st_mtime if summary_path.is_file() else None
cpu = re.search(r"host CPU (.+?): has", text)
return {"outcome": classify(True, exit_code, age, bool(frames), uniform), "exit_code": exit_code, "seconds": elapsed,
"presents": summary["presents"], "fps": summary["fps"], "first_error": first_error(text),
"warnings": warning_counts(text), "frames": frames, "gpu": summary["gpu"], "driver": summary["driver"],
"os": summary["os"], "cpu": cpu.group(1) if cpu else ""}
def source_root(build):
cache = Path(build) / "CMakeCache.txt"
if cache.is_file():
for line in cache.read_text(encoding="utf-8", errors="replace").splitlines():
if line.startswith("CMAKE_HOME_DIRECTORY:"):
return Path(line.split("=", 1)[1])
return ROOT
def git(root, *args):
return subprocess.run(["git", "-C", str(root), *args], capture_output=True, text=True).stdout.strip()
def positive(text):
try:
value = int(text)
except ValueError:
value = 0
if value < 1:
raise argparse.ArgumentTypeError(f"{text!r} is not a positive integer")
return value
def last_line(text):
return next((line.strip() for line in reversed(text.splitlines()) if line.strip()), "")
def write_summary(output, summary):
(output / "summary.json").write_text(json.dumps(summary, indent=2, default=str), encoding="utf-8")
def main(argv=None):
parser = argparse.ArgumentParser(description="Run titles repeatably and report each run's outcome.")
parser.add_argument("ids", nargs="*", help="title IDs to run (default: every title found)")
parser.add_argument("--games", action="append", type=Path, required=True, help="directory searched for extracted packages")
parser.add_argument("--build", type=Path, default=ROOT / "build")
parser.add_argument("--out", type=Path)
parser.add_argument("--runs", type=positive, default=2)
parser.add_argument("--seconds", type=positive, help="run length for every title (default: the title's, else 60)")
parser.add_argument("--note", default="", help="free text for the report, e.g. 'main + #6814'")
parser.add_argument("--compare", type=Path, help="summary.json of an earlier run")
parser.add_argument("--titles", type=Path, default=ROOT / "tools/titles.json")
args = parser.parse_args(argv)
args.out = args.out or args.build / "title-runs"
previous = json.loads(args.compare.read_text(encoding="utf-8")) if args.compare else None
platform = "windows" if os.name == "nt" else "macos" if sys.platform == "darwin" else "linux"
titles = load_titles(args.titles)
found = discover(args.games)
ids = args.ids or sorted(found)
missing = [title_id for title_id in ids if title_id not in found]
if missing:
raise SystemExit(f"not found under --games: {', '.join(missing)}")
locks = {title_id: lock_work(args.out.resolve() / "work" / f"{title_id}.lock") for title_id in ids}
busy = [title_id for title_id, handle in locks.items() if handle is None]
if busy:
raise SystemExit(f"already running in {args.out.resolve() / 'work'}: {', '.join(busy)}")
if previous and args.ids:
previous["titles"] = {title_id: title for title_id, title in previous["titles"].items() if title_id in ids}
source = source_root(args.build)
commit = git(source, "rev-parse", "--short", "HEAD")
output = args.out.resolve() / f"{commit}-{datetime.datetime.now():%Y%m%d-%H%M%S}"
summary = {"build": {"commit": commit, "dirty": bool(git(source, "status", "--porcelain", "--untracked-files=no")), "note": args.note},
"machine": {"os": "", "gpu": "", "driver": "", "cpu": ""}, "runs": args.runs, "seconds": args.seconds or 60,
"driver_cache": "isolated" if platform == "linux" else "shared (not isolated)", "titles": {}}
for title_id in ids:
entry = titles.get(title_id, {})
seconds = args.seconds or entry.get("seconds", 60)
work = args.out.resolve() / "work" / title_id
try:
executable = prepare(title_id, found[title_id], entry, platform, args.build, work)
except (unwrap_self.Refused, OSError, RuntimeError, TypeError, ValueError) as error:
work.mkdir(parents=True, exist_ok=True)
(work / "relink.log").write_text(f"{type(error).__name__}: {error}\n", encoding="utf-8")
executable = None
(output / title_id).mkdir(parents=True, exist_ok=True)
shutil.copyfile(work / "relink.log", output / title_id / "relink.log")
runs = []
for number in range(1, args.runs + 1):
if executable is None:
log = (work / "relink.log").read_text(encoding="utf-8", errors="replace")
result = {"outcome": "relink-failed", "exit_code": None, "seconds": 0, "presents": 0, "fps": 0.0,
"first_error": first_error(log) or last_line(log),
"warnings": {}, "frames": []}
else:
result = run_title(executable, work, output / title_id / f"run{number}", seconds, entry.get("env", {}))
for key in ("os", "gpu", "driver", "cpu"):
value = result.pop(key)
summary["machine"][key] = summary["machine"][key] or value
runs.append({"run": number, "cache": "cold" if number == 1 else "warm", **result})
print(f"{title_id} run{number}: {result['outcome']}", flush=True)
summary["titles"][title_id] = {"name": found[title_id]["name"], "version": found[title_id]["version"], "seconds": seconds, "runs": runs}
write_summary(output, summary)
write_summary(output, summary)
report = render_markdown(summary, previous)
(output / "summary.md").write_text(report, encoding="utf-8")
print(report)
print(f"written to {output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+14
View File
@@ -0,0 +1,14 @@
{
"PPSA01153": {"relink": ["--trap-sdk-syscall-stubs"]},
"PPSA68091": {"relink": ["--registry"]},
"PPSA69421": {"relink": ["--trap-sdk-syscall-stubs"]},
"PPSA85300": {"relink": ["--registry"]},
"PPSA97358": {"relink": ["--registry"]},
"PPSA99004": {"relink": ["--trap-sdk-syscall-stubs"]},
"PPSA99008": {"relink": ["--trap-sdk-syscall-stubs"]},
"PPSA99011": {"relink": ["--trap-sdk-syscall-stubs"]},
"PPSA99169": {"executables": ["picker/picker.bin"]},
"PPSA99203": {"relink": ["--trap-sdk-syscall-stubs"]},
"PPSA99288": {"relink": ["--trap-sdk-syscall-stubs"]},
"PPSA99360": {"relink": ["--trap-sdk-syscall-stubs"]}
}