diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c85757df6..0559567c61 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -357,6 +357,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_pages.py) add_test(NAME pr_overlap_export_cache COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_export_cache.py) + add_test(NAME pr_overlap_fail_closed COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_fail_closed.py) add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py) add_test(NAME release_package COMMAND ${Python3_EXECUTABLE} diff --git a/core/libs/prx/libSceAcm/Export.cpp b/core/libs/prx/libSceAcm/Export.cpp index c50791cc65..415bd482e3 100644 --- a/core/libs/prx/libSceAcm/Export.cpp +++ b/core/libs/prx/libSceAcm/Export.cpp @@ -398,7 +398,12 @@ int APS5_VABI sceAcm_Panner(AcmBatchInfo* batch, std::uint32_t sourceCount, cons } int APS5_VABI sceAcmBatchJobPriority(AcmBatchInfo* batch, std::uint32_t priority) { - NotImplemented_nid_no_patch(__func__); + if (!batch || priority >= 4) return SCE_ACM_ERROR_BAD_POINTER; + const auto offset = batch->offset; + if (offset > batch->buffer_size || batch->buffer_size - offset < 8) return static_cast(0x81940009); + const std::uint64_t job = 0x10 | (static_cast(priority) << 8); + std::memcpy(static_cast(batch->buffer) + offset, &job, sizeof(job)); + batch->offset = offset + sizeof(job); return 0; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index dfc0e43630..6059fa4ac9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -13,6 +13,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Dispatch/IndirectDispatch.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/WriteEvidence.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Packets/PacketTiming.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelTakes.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/WaitMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" @@ -220,6 +221,7 @@ private: void noteLabelStore(std::uint64_t address, std::span bytes, std::uint64_t stamp, std::uint32_t queue); void noteLabelPass(std::span packet, std::uint32_t queue); void noteLabelIssue(std::span packet, std::uint32_t queue); + void retireLabelTakes(std::uint32_t queue, std::uint64_t received); bool storedSince(std::span packet, std::uint64_t address, std::size_t bytes, std::uint64_t received, std::uint32_t queue, std::uint32_t* writer = nullptr); static void traceLabel(std::span packet, std::uint32_t queue); static std::array& writeHistory(); @@ -461,8 +463,7 @@ private: AgcDriver::Mutex labelStoresMutex; std::unordered_map> labelStores; - std::unordered_map labelPasses; - std::unordered_map labelTaken; + LabelTakes labelTakes; std::vector> replacedDevices; std::stop_token shutdownToken = LibcShutdownToken_nid_postfix(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelTakes.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelTakes.hpp new file mode 100644 index 0000000000..eff8338f78 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelTakes.hpp @@ -0,0 +1,88 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_SYNCHRONIZATION_LABELTAKES_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_SYNCHRONIZATION_LABELTAKES_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::DriverDetail { + +class LabelTakes { +public: + void Pass(std::uint32_t queue, std::uint64_t dword, std::uint64_t serial) { + const auto key = queueDword(queue, dword); + auto& entry = entries[key]; + if (entry.passed == 0) passCounts[slot(key)].fetch_add(1, std::memory_order_relaxed); + entry.passed = serial; + passes[queue].emplace_back(serial, key); + } + + bool MayHavePass(std::uint32_t queue, std::uint64_t dword) const { + return passCounts[slot(queueDword(queue, dword))].load(std::memory_order_relaxed) != 0; + } + + void Take(std::uint32_t queue, std::uint64_t dword) { + const auto key = queueDword(queue, dword); + const auto found = entries.find(key); + if (found == entries.end() || found->second.passed == 0) return; + found->second.taken = found->second.passed; + found->second.passed = 0; + passCounts[slot(key)].fetch_sub(1, std::memory_order_relaxed); + } + + std::uint64_t Taken(std::uint32_t queue, std::uint64_t dword) const { + const auto found = entries.find(queueDword(queue, dword)); + return found == entries.end() ? 0 : found->second.taken; + } + + void Retire(std::uint32_t queue, std::uint64_t received) { + const auto queued = passes.find(queue); + if (queued == passes.end()) return; + auto& order = queued->second; + while (!order.empty() && order.front().first <= received) { + const auto [serial, key] = order.front(); + order.pop_front(); + const auto found = entries.find(key); + if (found == entries.end()) continue; + if (found->second.passed == serial) { + found->second.passed = 0; + passCounts[slot(key)].fetch_sub(1, std::memory_order_relaxed); + } + if (found->second.taken == serial) found->second.taken = 0; + if (found->second.passed == 0 && found->second.taken == 0) entries.erase(found); + } + if (order.empty()) passes.erase(queued); + } + + bool Empty() const { + return entries.empty() && passes.empty(); + } + +private: + struct Entry { + std::uint64_t passed = 0; + std::uint64_t taken = 0; + }; + + static constexpr std::size_t PassSlotBits = 12; + + static std::uint64_t queueDword(std::uint32_t queue, std::uint64_t dword) { + return dword | (static_cast(queue) << 56u); + } + + static std::size_t slot(std::uint64_t key) { + return static_cast(((key >> 2u) * 0x9e3779b97f4a7c15ull) >> (64u - PassSlotBits)); + } + + std::unordered_map entries; + std::unordered_map>> passes; + std::array, std::size_t{1} << PassSlotBits> passCounts{}; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp index a620cb1849..aaf8455ca5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp @@ -278,6 +278,7 @@ void Driver::run(std::uint32_t id) noexcept { running.Start(); if (profile && submission.enqueuedAt != std::chrono::steady_clock::time_point{}) costs.dequeueNs += static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - submission.enqueuedAt).count()); } + retireLabelTakes(id, submission.received); execute(submission); if (submission.progress && submission.rewindTail == nullptr && !completionDeferred()) submitOpenWork(submission.progress); if (traceGpu) std::fprintf(stderr, "[gpu] %.1f done serial=%llu queue=0x%x\n", TraceMs(), static_cast(submission.serial), id); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp index 7b452b32f3..ed5af8d7e8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -362,6 +362,7 @@ void Driver::executeRewindTail(const Submission& stalled) { if (APS5_ENABLE_TIMING_LOG) tail.roomReadyAt = std::chrono::steady_clock::now(); auto* const capture = SubmissionCapture::Active(); auto captured = capture != nullptr ? SubmissionCapture::Prepare(tail.queue, SubmissionKind::Rewind, tail.commands, tail.conditionalEnds) : CapturedSubmission{}; + std::uint64_t retiredTakes = 0; { std::lock_guard lock(mutex); rethrowFailure(); @@ -370,12 +371,15 @@ void Driver::executeRewindTail(const Submission& stalled) { tail.shaders = shaders; tail.serial = stalled.serial; tail.received = ++eventSerial; + const auto& waiting = workers[tail.queue].pending; + retiredTakes = waiting.empty() ? tail.received : std::min(tail.received, waiting.front().received); if (capture != nullptr) capture->Take(captured, tail.serial, captureTime(), tail.heldAtSubmit); if (ring) RecordSubmission(tail.queue, tail.serial, tail.commands, stalled.rewindTail, true); } if (capture != nullptr) writeCaptured(*this, *capture, captured); if (APS5_ENABLE_TIMING_LOG) tail.enqueuedAt = tail.dequeuedAt = tail.orderedAt = std::chrono::steady_clock::now(); timing.Finish(); + retireLabelTakes(tail.queue, retiredTakes); execute(tail); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp index db078dae9d..8b4141c7eb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp @@ -20,14 +20,6 @@ void Driver::noteLabelStore(std::uint64_t address, std::span by } } -namespace { - -std::uint64_t queueDword(std::uint32_t queue, std::uint64_t dword) { - return dword | (static_cast(queue) << 56u); -} - -} - void Driver::noteLabelPass(std::span packet, std::uint32_t queue) { const auto opcode = (packet[0] >> 8u) & 0xffu; if ((opcode != 0x3c && opcode != 0x93) || packet.size() < 7 || ((packet[1] >> 4u) & 3u) != 1u) return; @@ -35,20 +27,28 @@ void Driver::noteLabelPass(std::span packet, std::uint32_t const std::uint64_t address = (packet[2] & (bytes == 8 ? ~7u : ~3u)) | (static_cast(packet[3]) << 32u); const auto passed = eventSerial.load(std::memory_order_acquire); std::lock_guard lock(labelStoresMutex); - for (std::size_t offset = 0; offset < bytes; offset += 4) labelPasses[queueDword(queue, address + offset)] = passed; + for (std::size_t offset = 0; offset < bytes; offset += 4) labelTakes.Pass(queue, address + offset, passed); +} + +void Driver::retireLabelTakes(std::uint32_t queue, std::uint64_t received) { + std::lock_guard lock(labelStoresMutex); + labelTakes.Retire(queue, received); } void Driver::noteLabelIssue(std::span packet, std::uint32_t queue) { - const auto take = [&](std::uint64_t address, std::uint64_t bytes) { + const auto mayTake = [&](std::uint64_t address, std::uint64_t bytes) { for (std::uint64_t offset = 0; offset < bytes; offset += 4) { - const auto key = queueDword(queue, address + offset); - if (const auto pass = labelPasses.find(key); pass != labelPasses.end()) labelTaken[key] = pass->second; + if (labelTakes.MayHavePass(queue, address + offset)) return true; } + return false; + }; + const auto take = [&](std::uint64_t address, std::uint64_t bytes) { + for (std::uint64_t offset = 0; offset < bytes; offset += 4) labelTakes.Take(queue, address + offset); }; switch ((packet[0] >> 8u) & 0xffu) { case 0x37: case 0x49: { const auto label = Pm4::DecodeLabelWrite(packet); - if (!label.has_value() || !KeepsLabelStore(label->address, label->Bytes().size())) return; + if (!label.has_value() || !KeepsLabelStore(label->address, label->Bytes().size()) || !mayTake(label->address, label->Bytes().size())) return; std::lock_guard lock(labelStoresMutex); take(label->address, label->Bytes().size()); return; @@ -57,7 +57,7 @@ void Driver::noteLabelIssue(std::span packet, std::uint32_t default: return; } auto stores = Pm4::DecodeStores(packet); - std::erase_if(stores, [](const Pm4::PacketStore& store) { return store.conditional || !KeepsLabelStore(store.address, static_cast(store.bytes)); }); + std::erase_if(stores, [&](const Pm4::PacketStore& store) { return store.conditional || !KeepsLabelStore(store.address, static_cast(store.bytes)) || !mayTake(store.address, store.bytes); }); if (stores.empty()) return; std::lock_guard lock(labelStoresMutex); for (const auto& store : stores) take(store.address, store.bytes); @@ -70,9 +70,7 @@ bool Driver::storedSince(std::span packet, std::uint64_t ad const auto high = bytes == 8 ? labelStores.find(address + 4) : labelStores.end(); if (bytes == 8 && high == labelStores.end()) return false; std::uint64_t taken = 0; - for (std::size_t offset = 0; offset < bytes; offset += 4) { - if (const auto found = labelTaken.find(queueDword(queue, address + offset)); found != labelTaken.end()) taken = std::max(taken, found->second); - } + for (std::size_t offset = 0; offset < bytes; offset += 4) taken = std::max(taken, labelTakes.Taken(queue, address + offset)); for (const auto& store : low->second) { if (store.stamp <= received || (store.queue != queue && store.stamp <= taken)) continue; std::uint64_t value = store.value; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp index 3f75b2a804..2a796be046 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp @@ -37,8 +37,6 @@ public: std::uint64_t hits = 0; std::uint64_t misses = 0; std::size_t held = 0; - // Hits served by the cached address space's serial alone (no hash, no compare; see - // GuestBufferMemory::CachedAddressTable), counted in `hits` too. std::uint64_t spaceTables = 0; // Why the most recently used table did not serve a build ([bda-table] first-differing entry // class): its owners were gone (expired weak reference), or its word hash differed, split by diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 5eb4815ffa..44c1a86246 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" +#include "BdaAbi.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include #include @@ -81,6 +82,9 @@ bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std: void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); std::shared_ptr MultisampleTargetFmaskTexture_nid_no_patch(const Context& context, const GuestTextureResource& resource, VkComponentMapping components); void ClearMultisampleTargets(VkDevice device); +std::uint64_t MultisampleTargetRangeGeneration(); +std::optional> ExcludeMultisampleTargetRanges( + VkDevice device, std::span ranges, std::uint64_t& generation); void InstallResidentImageReadGuard_nid_no_patch(); void UpdateResidentImageUnmapObserver_nid_no_patch(); void ClaimMultisampleTargetMemory(std::uint64_t address, std::uint64_t bytes); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index a8440fb7f0..3b0498e7b8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" #include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "ImageTableAbi.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include @@ -18,29 +19,11 @@ namespace AgcDriver::Graphics { namespace { -// The page tables built per device, reused by a build whose ranges (guest ranges, device -// addresses, permissions) equal one of them: consecutive address-based builds map the same registered -// set through the same imports and mirrors, so the ~1200-entry table repeats. The shader only reads -// the table (its lookup keeps its state in function variables), and a batch in flight keeps its -// BdaResources, so sharing the buffer is safe. The cache holds the tables weakly: they never outlive -// the builds that own them (a static owner would destroy the buffer after its device, at exit or on -// device replacement, and would fail the tests' leak check), and with deferred lease release the -// previous builds are still in flight when the next one asks, which is what makes the hit rate. -// One table was enough while every build repeated the last one exactly; now the misaligned -// descriptor sub-ranges the GPU copies out of imports sit in pool staging buffers whose device -// addresses cycle through the few buffers the batches in flight leave free, and the three -// address-based programs of one queue interleave with the lease draws, so a table differs from the -// previous one and comes back a few builds later (69% misses in the wave-8 run, 4% before the GPU -// copies). Hence several recent tables, most recently used first, matched by a word hash before -// the byte compare. APS5_BDA_TABLE_CACHE_ENTRIES sets how many (default 8; 1 is the previous -// behaviour); APS5_NO_BDA_TABLE_CACHE=1 builds every table. -// A table built for (or found equal to) the cached address space's ranges carries the space's -// serial: a build served by that space alone binds it by the serial, with no hash and no compare. struct TableEntry { std::uint64_t hash; std::vector ranges; std::weak_ptr buffer; - std::uint64_t spaceSerial = 0; + std::vector> spaceKeys; }; struct TableCache { @@ -54,8 +37,6 @@ struct TableCache { bool sameRanges(const std::vector& left, const std::vector& right); -// APS5_PROFILE_DRAW: why the most recently used entry did not serve this build (see -// TableCacheStats). Under the cache mutex. void classifyFirstEntry(TableCache& cache, std::uint64_t hash, const std::vector& ranges) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile) return; @@ -135,8 +116,33 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo const auto cached = memory.CachedAddressTable(); std::vector built; if (!cached.has_value()) built = memory.AddressRanges(); - const auto& ranges = cached.has_value() ? *cached->ranges : built; + const auto& addressRanges = cached.has_value() ? *cached->ranges : built; const auto serial = cached.has_value() ? cached->serial : 0; + auto guardSerial = MultisampleTargetRangeGeneration(); + if (tableCacheEnabled() && serial != 0) { + auto& cache = Tables(); + std::lock_guard lock(cache.mutex); + if (cache.device != context.device) { + cache.entries.clear(); + cache.device = context.device; + } + for (auto it = cache.entries.begin(); it != cache.entries.end(); ++it) { + const auto key = std::pair{serial, guardSerial}; + if (std::find(it->spaceKeys.begin(), it->spaceKeys.end(), key) == it->spaceKeys.end()) continue; + if (auto shared = it->buffer.lock(); shared != nullptr) { + tableBytes = shared->Bytes().size(); + ++cache.hits; + ++cache.classes.spaceTables; + table = std::move(shared); + cache.entries.splice(cache.entries.begin(), cache.entries, it); + return; + } + cache.entries.erase(it); + break; + } + } + auto filtered = ExcludeMultisampleTargetRanges(context.device, addressRanges, guardSerial); + const auto& ranges = filtered.has_value() ? *filtered : addressRanges; Require(ranges.size() <= std::numeric_limits::max(), "BDA table range count overflow"); Require(ranges.size() <= (std::numeric_limits::max() - sizeof(ShaderRecompiler::BdaAbi::Header)) / sizeof(ShaderRecompiler::BdaAbi::Range), "BDA table size overflow"); tableBytes = sizeof(ShaderRecompiler::BdaAbi::Header) + ranges.size() * sizeof(ShaderRecompiler::BdaAbi::Range); @@ -150,20 +156,6 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo cache.entries.clear(); cache.device = context.device; } - if (serial != 0) { - for (auto it = cache.entries.begin(); it != cache.entries.end(); ++it) { - if (it->spaceSerial != serial) continue; - if (auto shared = it->buffer.lock(); shared != nullptr) { - ++cache.hits; - ++cache.classes.spaceTables; - table = std::move(shared); - cache.entries.splice(cache.entries.begin(), cache.entries, it); - return; - } - cache.entries.erase(it); - break; - } - } hash = hashRanges(ranges); classifyFirstEntry(cache, hash, ranges); for (auto it = cache.entries.begin(); it != cache.entries.end();) { @@ -186,9 +178,13 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo } if (sameRanges(it->ranges, ranges)) { ++cache.hits; - // An equal table of an earlier space (a rebuild that mapped the same ranges the - // same way): the next build of this space binds it by the serial. - if (serial != 0) it->spaceSerial = serial; + if (serial != 0) { + const auto key = std::pair{serial, guardSerial}; + auto found = std::find(it->spaceKeys.begin(), it->spaceKeys.end(), key); + if (found != it->spaceKeys.end()) it->spaceKeys.erase(found); + it->spaceKeys.push_back(key); + if (it->spaceKeys.size() > tableCacheEntries()) it->spaceKeys.erase(it->spaceKeys.begin()); + } table = std::move(shared); cache.entries.splice(cache.entries.begin(), cache.entries, it); return; @@ -209,7 +205,9 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo cache.entries.clear(); cache.device = context.device; } - cache.entries.push_front({hash, cached.has_value() ? ranges : std::move(built), table, serial}); + auto cachedRanges = filtered.has_value() ? std::move(*filtered) : cached.has_value() ? *cached->ranges : std::move(built); + cache.entries.push_front({hash, std::move(cachedRanges), table, {}}); + if (serial != 0) cache.entries.front().spaceKeys.emplace_back(serial, guardSerial); while (cache.entries.size() > tableCacheEntries()) cache.entries.pop_back(); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index a89637de7c..43b8c69993 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -796,6 +796,11 @@ std::atomic& anyGuarded() { return any; } +std::atomic& guardedGeneration() { + static std::atomic generation{0}; + return generation; +} + void guardReads(std::uint64_t address, std::size_t bytes) { RequireNoMultisampleTarget(address, bytes, "a CPU read"); } @@ -834,13 +839,18 @@ void retireUnmapped(std::uintptr_t address, std::size_t bytes) { forgetSingleClears(address, bytes); RetireUnmappedDepth_nid_no_patch(address, bytes); std::unique_lock lock(guardedMutex()); - std::erase_if(guarded(), [&](const auto& range) { - if (range.metadataValid && address < range.address + range.bytes && range.address < address + bytes) range.metadataValid->store(false, std::memory_order_release); + bool metadataChanged = false; + const auto invalidated = std::erase_if(guarded(), [&](const auto& range) { + if (range.metadataValid && address < range.address + range.bytes && range.address < address + bytes) { + range.metadataValid->store(false, std::memory_order_release); + metadataChanged = true; + } if (range.address < address || range.address - address > bytes || range.bytes > bytes - (range.address - address)) return false; if (!range.metadataValid) range.active->store(false, std::memory_order_release); return true; }); - std::erase_if(guarded(), [](const auto& range) { return range.metadataValid && !range.active->load(std::memory_order_acquire); }); + const auto staleMetadata = std::erase_if(guarded(), [](const auto& range) { return range.metadataValid && !range.active->load(std::memory_order_acquire); }); + if (metadataChanged || invalidated != 0 || staleMetadata != 0) guardedGeneration().fetch_add(1, std::memory_order_release); anyGuarded().store(!guarded().empty(), std::memory_order_release); } @@ -882,6 +892,7 @@ void guard(const Context& context, const ColorTarget& target, const std::shared_ if (target.cmaskAddress != 0) guarded().push_back({context.device, target.cmaskAddress, target.cmaskBytes, target.samples, active, metadataValid, use}); if (target.samples == 8u && target.fmaskAddress != 0u && target.cmaskAddress != 0u) guarded().push_back({context.device, target.fmaskAddress, DescribeSurface(fmaskResource(target)).guestBytes, target.samples, active, metadataValid, use}); } + guardedGeneration().fetch_add(1, std::memory_order_release); anyGuarded().store(true, std::memory_order_release); InstallResidentImageReadGuard_nid_no_patch(); } @@ -1477,7 +1488,8 @@ void ClearMultisampleTargets(VkDevice device) { std::lock_guard lock(targetsMutex()); std::erase_if(targets(), [&](const auto& target) { return target->context.device == device; }); std::unique_lock guardLock(guardedMutex()); - std::erase_if(guarded(), [&](const auto& range) { return range.device == device; }); + const auto removed = std::erase_if(guarded(), [&](const auto& range) { return range.device == device; }); + if (removed != 0) guardedGeneration().fetch_add(1, std::memory_order_release); anyGuarded().store(!guarded().empty(), std::memory_order_release); UpdateResidentImageUnmapObserver_nid_no_patch(); } @@ -1499,6 +1511,71 @@ void GuardOneFragmentFmask(const Context& context, const ColorTarget& target) { InstallResidentImageReadGuard_nid_no_patch(); } +std::uint64_t MultisampleTargetRangeGeneration() { + std::shared_lock lock(guardedMutex()); + return guardedGeneration().load(std::memory_order_acquire); +} + +std::optional> ExcludeMultisampleTargetRanges( + VkDevice device, std::span ranges, std::uint64_t& generation) { + if (ranges.empty()) { + generation = MultisampleTargetRangeGeneration(); + return std::nullopt; + } + if (!anyGuarded().load(std::memory_order_acquire)) { + generation = MultisampleTargetRangeGeneration(); + return std::nullopt; + } + std::vector> excluded; + { + std::shared_lock lock(guardedMutex()); + generation = guardedGeneration().load(std::memory_order_relaxed); + for (const auto& range : guarded()) { + if (range.device != device || !range.active->load(std::memory_order_acquire)) continue; + excluded.emplace_back(range.address, range.address + range.bytes); + } + } + if (excluded.empty()) return std::nullopt; + std::sort(excluded.begin(), excluded.end()); + std::vector> merged; + merged.reserve(excluded.size()); + for (const auto& range : excluded) { + if (!merged.empty() && range.first <= merged.back().second) merged.back().second = std::max(merged.back().second, range.second); + else merged.push_back(range); + } + std::vector filtered; + filtered.reserve(ranges.size() + merged.size()); + bool changed = false; + for (const auto& range : ranges) { + auto cursor = range.begin; + for (const auto& [begin, end] : merged) { + if (end <= cursor) continue; + if (begin >= range.end) break; + const auto cutBegin = std::max(cursor, begin); + const auto cutEnd = std::min(range.end, end); + if (cutBegin > cursor) { + auto part = range; + part.begin = cursor; + part.end = cutBegin; + part.deviceAddress += cursor - range.begin; + filtered.push_back(part); + } + if (cutBegin < cutEnd) { + changed = true; + cursor = cutEnd; + } + if (cursor == range.end) break; + } + if (cursor < range.end) { + auto part = range; + part.begin = cursor; + part.deviceAddress += cursor - range.begin; + filtered.push_back(part); + } + } + return changed ? std::optional(std::move(filtered)) : std::nullopt; +} + void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access) { RequireNoResidentDepthRead_nid_no_patch(address, bytes); RequireNoMultisampleColorTarget(address, bytes, access); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 2cafbf2f1f..a00cc6e740 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -116,6 +116,7 @@ void snapshotTableTests(const Context& context, const BdaTestAccess& access) { auto a = build(first, false, true, snapshotAddress); auto plain = build(first, false, false, snapshotAddress); auto d = build(first, true, true, snapshotAddress); + const auto spaceTableHitsBefore = BdaResources::TableCacheCounters().spaceTables; auto b = build(first, false, true, snapshotAddress); auto c = build(second, false, true, snapshotAddress); Require(a.snapshot == b.snapshot && a.view == b.view, "two builds of one snapshot with the same bytes do not share its device copy"); @@ -124,6 +125,8 @@ void snapshotTableTests(const Context& context, const BdaTestAccess& access) { if (spaces) { Require(a.memory->CachedAddressTable().has_value() && c.memory->CachedAddressTable().has_value() && d.memory->CachedAddressTable().has_value(), "a build with its own regions over the address space has no cached BDA table"); Require(a.table == b.table, "two builds of the same space and snapshot do not share their BDA table"); + Require(BdaResources::TableCacheCounters().spaceTables == spaceTableHitsBefore + 1, "a repeated build with a stable address-space and MSAA guard generation missed the serial-keyed BDA table"); + Require(b.bda->Table().range == a.bda->Table().range && b.bda->Table().range != 0, "a cached BDA descriptor lost its byte range: " + std::to_string(b.bda->Table().range) + " instead of " + std::to_string(a.bda->Table().range)); Require(c.table != a.table, "a build with other snapshot bytes took another build's BDA table"); Require(a.memory->CachedAddressTable()->serial == b.memory->CachedAddressTable()->serial && c.memory->CachedAddressTable()->serial != a.memory->CachedAddressTable()->serial, "cached BDA table serials do not follow the build's ranges"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 11a1fb07f4..41f9cffe55 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -6280,7 +6280,15 @@ void descriptorPoolResetFailureTest() { const auto previous = _ZSt13set_terminatePFvvE_nid_postfix([] { std::_Exit(mock.descriptorResetResult == VK_SUCCESS ? 0 : 1); }); +#ifdef __APPLE__ + const auto previousHost = std::set_terminate([] { + std::_Exit(mock.descriptorResetResult == VK_SUCCESS ? 0 : 1); + }); +#endif cache.Free(allocation); +#ifdef __APPLE__ + std::set_terminate(previousHost); +#endif _ZSt13set_terminatePFvvE_nid_postfix(previous); } diff --git a/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp b/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp index 8befc9e582..4256def9d9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp @@ -76,6 +76,43 @@ int main() { program.Metadata().dynamicReads.clear(); if (program.ReclaimUnusedValues() != 2u) throw std::runtime_error("reclamation did not recover after an external-owner rejection"); if (builder.Constant(999u).Id() <= pinned.back()->Id()) throw std::runtime_error("reclamation reused a value ID"); + IrProgram replaced; + auto& shared = replaced.CreateValue(IrOpcode::IAdd32, IrType::U32); + std::vector readers; + for (std::uint32_t number = 0; number < 40u; ++number) { + auto& reader = replaced.CreateValue(IrOpcode::IAdd32, IrType::U32); + reader.AddArgument(&shared); + readers.push_back(&reader); + } + for (std::size_t index = 0; index < readers.size(); ++index) { + if (shared.OperandUses()[index].user != readers[index]) throw std::runtime_error("a use list lost an entry while growing"); + } + auto& target = replaced.CreateValue(IrOpcode::IAdd32, IrType::U32); + shared.ReplaceAllUsesWith(&target); + if (shared.HasUses() || target.UseCount() != readers.size() || readers.front()->Argument(0) != &target || readers.back()->Argument(0) != &target) throw std::runtime_error("moving a grown use list to another value lost a use"); + IrProgram replacement; + auto& kept = replacement.CreateValue(IrOpcode::IAdd32, IrType::U32); + auto& keptReader = replacement.CreateValue(IrOpcode::IAdd32, IrType::U32); + keptReader.AddArgument(&kept); + replaced = std::move(replacement); + if (kept.UseCount() != 1u || kept.OperandUses().front().user != &keptReader) throw std::runtime_error("move assignment invalidated the assigned program's use lists"); + IrProgram cached; + auto& source = cached.CreateValue(IrOpcode::IAdd32, IrType::U32); + std::vector users; + for (std::uint32_t number = 0; number < 40u; ++number) { + auto& user = cached.CreateValue(IrOpcode::IAdd32, IrType::U32); + user.AddArgument(&source); + users.push_back(&user); + } + cached.ReleaseUseArena(); + if (source.OperandUses().get_allocator().Arena() != nullptr || users.front()->OperandUses().get_allocator().Arena() != nullptr) throw std::runtime_error("releasing the use arena left a use list in it"); + if (source.UseCount() != users.size()) throw std::runtime_error("releasing the use arena lost a use"); + for (std::size_t index = 0; index < users.size(); ++index) { + if (source.OperandUses()[index].user != users[index] || users[index]->Argument(0) != &source) throw std::runtime_error("releasing the use arena reordered a use list"); + } + auto& late = cached.CreateValue(IrOpcode::IAdd32, IrType::U32); + late.AddArgument(&source); + if (late.OperandUses().get_allocator().Arena() == nullptr || source.UseCount() != users.size() + 1u || source.OperandUses().back().user != &late) throw std::runtime_error("a value created after releasing the use arena did not get a new one"); IrProgram moved = std::move(program); if (value.Id() != id || value.ImmediateU32() != 42u || moved.ReclaimUnusedValues() != 1u) throw std::runtime_error("moving a program invalidated its allocation owner"); IrProgram assigned; diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index 1d226750d1..482c6aad1b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -39,6 +39,18 @@ static_assert(offsetof(Packet, addr) == 0); static_assert(offsetof(Packet, dw_num) == 8); static_assert(offsetof(Packet, flags) == 12); +namespace AgcDriver::DriverDetail { + +struct DriverTestAccess { + static std::uint64_t LabelTaken(std::uint32_t queue, const volatile std::uint32_t* address) { + auto& driver = Driver::Get(); + std::lock_guard lock(driver.labelStoresMutex); + return driver.labelTakes.Taken(queue, reinterpret_cast(address)); + } +}; + +} + namespace { void check(bool condition, const char* message) { @@ -451,6 +463,56 @@ void testLabelTakenAndResetByEarlierSubmission() { AgcDriverWaitIdle_nid_postfix(); } +void testLabelTakesRetire() { + using AgcDriver::DriverDetail::LabelTakes; + auto takes = std::make_unique(); + constexpr std::uint64_t label = 0x1000, other = 0x2000; + takes->Pass(0x20, label, 10); + check(takes->MayHavePass(0x20, label) && takes->Taken(0x20, label) == 0, "a passed wait was not kept for the store that takes it"); + takes->Take(0x20, label); + check(takes->Taken(0x20, label) == 10 && !takes->MayHavePass(0x20, label), "the queue's own store did not turn its pass into a take"); + takes->Retire(0x20, 9); + check(takes->Taken(0x20, label) == 10, "a take was dropped while a submission of the queue entered before it could still wait"); + takes->Retire(0x28, 20); + check(takes->Taken(0x20, label) == 10, "another queue's submission retired a take"); + takes->Retire(0x20, 10); + check(takes->Taken(0x20, label) == 0 && takes->Empty(), "a take was kept after every submission of the queue that could wait entered after it"); + takes->Pass(0x20, label, 30); + takes->Pass(0x20, label, 40); + takes->Retire(0x20, 30); + check(takes->MayHavePass(0x20, label), "an older pass on the dword retired its newer pass"); + takes->Take(0x20, label); + takes->Retire(0x20, 39); + check(takes->Taken(0x20, label) == 40, "a take was retired by a submission entered before it"); + takes->Pass(0x20, other, 50); + takes->Pass(0x28, label, 50); + takes->Retire(0x20, 50); + check(takes->Taken(0x20, label) == 0 && !takes->MayHavePass(0x20, other), "a submission of the queue entered after its passes and take did not retire them"); + check(takes->MayHavePass(0x28, label) && !takes->Empty(), "a submission of one queue retired another queue's pass"); + takes->Retire(0x28, 50); + check(takes->Empty(), "a pass was kept after every submission of its queue entered after it"); +} + +void testLabelTakeRetiredByLaterSubmission() { + using AgcDriver::DriverDetail::DriverTestAccess; + alignas(64) static volatile std::uint32_t gate = 0, label = 0, first = 0, second = 0, third = 0; + submit(0x20, commands(waitEqual(&gate, 1), waitEqual(&label, 1), writeData(&label, 0), writeData(&first, 1))); + submit(0x20, commands(waitEqual(&label, 1), writeData(&second, 1))); + submit(0, commands(writeData(&label, 1))); + waitFor(&label, 1, "producer label never landed"); + gate = 1; + waitFor(&first, 1, "consumer never passed its first label wait"); + std::this_thread::sleep_for(std::chrono::milliseconds(1200)); + check(second == 0, "a label store an earlier submission of the queue had taken and reset satisfied a later submission's wait, or the wait expired"); + check(DriverTestAccess::LabelTaken(0x20, &label) != 0, "a take was dropped while a submission of the queue entered before it was waiting"); + submit(0, commands(writeData(&label, 1))); + waitFor(&second, 1, "the later submission did not resume after a new producer label"); + submit(0x20, commands(writeData(&third, 1))); + waitFor(&third, 1, "a submission entered after the take never ran"); + check(DriverTestAccess::LabelTaken(0x20, &label) == 0, "a take was kept after a submission of the queue entered after it started"); + AgcDriverWaitIdle_nid_postfix(); +} + void testWaitFreeSubmissionAfterEarlierQueue0Work() { alignas(64) static volatile std::uint32_t first = 0, second = 0; constexpr std::uint32_t writes = 20000; @@ -1103,6 +1165,8 @@ int main(int argc, char** argv) { testLabelHeldAtSubmission(); testLabelTakenAndResetInOneSubmission(); testLabelTakenAndResetByEarlierSubmission(); + testLabelTakesRetire(); + testLabelTakeRetiredByLaterSubmission(); testWideLabelStoredSinceSubmission(); testWaitFreeSubmissionAfterEarlierQueue0Work(); testWaitFreeSubmissionQueue0WaitsOn(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32MinMaxDenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32MinMaxDenormalModes.cpp index a9d0238d0b..c9d951d979 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F32MinMaxDenormalModes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32MinMaxDenormalModes.cpp @@ -24,9 +24,9 @@ constexpr std::uint32_t Instructions = 5; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array Code{ +alignas(256) constexpr std::array Code{ 0x34020084u, 0x34040086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x1e140b04u, 0x20160b04u, 0xd551000cu, - 0x041a0b04u, 0xd554000du, 0x041a0b04u, 0xd557000eu, 0x041a0b04u, 0xe0781000u, 0x80010a02u, 0xe0701010u, + 0x041a0b04u, 0xd554000du, 0x041a0b04u, 0xd557000eu, 0x041a0b04u, 0x201e0880u, 0xe0781000u, 0x80010a02u, 0xe0781010u, 0x80010e02u, 0xbf810000u, }; @@ -155,6 +155,10 @@ void Check(std::uint32_t denormals, bool ieee) { Hex(vector.operands[0]) + ", " + Hex(vector.operands[1]) + ", " + Hex(vector.operands[2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); } + const auto value = vector.operands[0]; + const bool tiny = denormals != 1u && (value & 0x7f800000u) == 0u; + const auto zeroMax = value <= 0x7f800000u && !tiny ? value : 0u; + Require(Output[lane * Results + 5u] == zeroMax, "v_max_f32 v15, 0, v4 denormal result for " + Hex(value)); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32MinMaxNan.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32MinMaxNan.cpp index d427b01f05..29643a5996 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F32MinMaxNan.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32MinMaxNan.cpp @@ -23,10 +23,10 @@ constexpr std::uint32_t Results = 16; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array Code{ +alignas(256) constexpr std::array Code{ 0x34020084, 0x34060086, 0xe0381000, 0x80000401, 0xbf8c3f70, 0x1e140b04, 0x20160b04, 0xd551000c, 0x041a0b04, 0xd554000d, 0x041a0b04, 0xd557000e, 0x041a0b04, 0xd50f000f, 0x02020b04, 0xd5100010, - 0x02020b04, 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xbf810000, + 0x02020b04, 0x20220880, 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xbf810000, }; constexpr std::uint32_t Rows[128][4] = { @@ -482,6 +482,10 @@ void Check() { for (std::uint32_t i = 0; i < 7; ++i) { Expect(Chunk * Threads + tid, out[i], (Ieee ? ExpectedIeee : Expected)[Chunk * Threads + tid][i], Names[i]); } + const auto magnitude = in[0] & 0x7fffffffu; + const bool signaling = magnitude > 0x7f800000u && (in[0] & 0x00400000u) == 0u; + const auto zeroMax = Ieee && signaling ? in[0] | 0x00400000u : in[0] <= 0x7f800000u ? in[0] : 0u; + Expect(Chunk * Threads + tid, out[7], zeroMax, "v_max_f32 v17, 0, v4"); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index e8127cdb0b..f9f42ce348 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -41,6 +41,11 @@ #include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif namespace AgcDriver { @@ -82,6 +87,9 @@ constexpr std::uint32_t TileD64KBX = 0x1a; constexpr std::uint32_t Threads = 32; constexpr std::uint32_t Words = 32; constexpr std::uint32_t LoadResult = 12; +constexpr std::uint32_t BdaThreads = 64; +constexpr std::uint32_t BdaLoadBase = 0x100; +constexpr std::uint32_t BdaOutputBase = 0x8000; constexpr std::array Drawn{0.25f, 0.5f, 0.125f, 1.0f}; constexpr double EdgeMargin = 1.0 / 32.0; constexpr std::uint32_t FillOnes = 0x3c003c00u; @@ -150,6 +158,11 @@ struct SampleWrite { std::uint32_t sample; }; +alignas(256) constexpr std::array BdaSpanCode{ + 0x34040084, 0x4a0404ff, BdaLoadBase, 0x34080081, 0x36080882, 0x4a040504, 0x34060083, 0x4a0606ff, + BdaOutputBase, 0xdc388ff8, 0x04000002, 0xbf8c3f70, 0xdc708000, 0x00000503, 0xdc708004, 0x00000703, 0xbf810000, +}; + constexpr std::array, 3> Triangle{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {0.83f, -1.0f, 0.5f, 1.0f}, {-1.0f, 0.71f, 0.5f, 1.0f} }}; @@ -158,6 +171,43 @@ alignas(256) std::array Buffer{}; alignas(256) constexpr std::array BufferLoadCode{0x34020082u, 0xe8001000u | (20u << 19u), 0x80000201u, 0xbf8c0000u, 0xe0701000u, 0x80010201u, 0xbf810000u}; +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + Require(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + _dup2(_fileno(file), _fileno(stderr)); +#else + saved = dup(fileno(stderr)); + dup2(fileno(file), fileno(stderr)); +#endif + } + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + struct Block { explicit Block(std::size_t bytes, bool watched = false) : bytes(bytes), watched(watched) { #ifdef _WIN32 @@ -821,6 +871,55 @@ void StoreWords(const Block& block, std::uint64_t bytes, std::uint32_t word) { for (std::uint64_t offset = 0; offset < bytes; offset += sizeof(word)) std::memcpy(block.data + offset, &word, sizeof(word)); } +ShaderRecompiler::RecompileResult CompileBdaSpan(AgcDriver::VulkanDevice& device, std::uint64_t address) { + const std::span code(BdaSpanCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const std::array userData{static_cast(address), static_cast(address >> 32u)}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{BdaThreads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + Require(std::ranges::any_of(result.bindings, [](const auto& binding) { return binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable; }), "the native multisample BDA test has no address-table binding"); + return result; +} + +void CheckBdaSpan(AgcDriver::VulkanDevice& device, const Block& block, std::size_t offset, bool refused) { + std::vector before(block.bytes); + for (std::size_t index = 0; index < before.size(); ++index) before[index] = static_cast(index * 37u + 11u); + const auto output = offset + 0x8000u; + std::fill(before.begin() + static_cast(output), before.begin() + static_cast(output + BdaThreads * 8u), 0xcdu); + std::memcpy(block.data, before.data(), before.size()); + auto expected = before; + for (std::uint32_t tid = 0; tid < BdaThreads; ++tid) { + const auto load = offset + BdaLoadBase + tid * 16u + (tid & 1u) * 2u - 8u; + std::memcpy(expected.data() + output + tid * 8u, before.data() + load + 4u, sizeof(std::uint32_t)); + std::memcpy(expected.data() + output + tid * 8u + 4u, before.data() + load + 12u, sizeof(std::uint32_t)); + } + std::string failure; + StderrCapture log; + try { + const auto address = block.Address() + offset; + device.Dispatch(CompileBdaSpan(device, address), 1, 1, 1, {}, reinterpret_cast(BdaSpanCode.data())); + device.WaitIdle(); + } catch (const std::exception& error) { + failure = error.what(); + } + const auto deferred = log.Finish(); + if (failure.empty()) failure = deferred; + if (refused) { + Require(failure.find("BDA access failed:") != std::string::npos, "BDA access to a resident native multisample range was not refused: " + failure); + Require(std::equal(before.begin(), before.end(), block.data), "a refused BDA access changed guest memory"); + } else { + Require(failure.empty(), "BDA access to an ordinary neighboring range failed: " + failure); + Require(std::equal(expected.begin(), expected.end(), block.data), "the ordinary BDA span did not preserve its reads, writes or boundaries"); + } +} + void RequireRefused(AgcDriver::VulkanDevice& device, const std::array& texture, const std::string& expected, const std::string& what) { std::string refusal; try { @@ -1260,6 +1359,18 @@ int main(int argc, char** argv) { return VulkanTestSkipped; } const auto colorBytes = AgcDriver::Graphics::MultisampledColorBytes(color); + Require(colorBytes == BlockBytes, "the native color target does not occupy exactly one BDA block"); + Block bda(4u * BlockBytes); + const auto bdaState = TargetState(DecodeTarget(bda)); + CheckBdaSpan(*device, bda, 0, false); + CheckBdaSpan(*device, bda, BlockBytes, false); + Draw(*device, bdaState); + CheckBdaSpan(*device, bda, 0, true); + CheckBdaSpan(*device, bda, BlockBytes, false); + Draw(*device, state); + CheckBdaSpan(*device, block, 0, true); + CheckBdaSpan(*device, block, BlockBytes, false); + CheckSamples(state, LoadSamples(*device, block), {}); Require(colorBytes != 0 && colorBytes <= block.bytes, "the 4x target's color memory does not fit its block"); AgcDriver::Graphics::NoteColorMetadataFill(block.Address(), static_cast(colorBytes), FillOnes); CheckUniform(LoadSamples(*device, block), Ones, "after a fill of the target's color memory with half-float ones"); @@ -1289,6 +1400,7 @@ int main(int argc, char** argv) { device->WaitIdle(); }, "recipe dispatch", missing); } + std::memset(block.data + colorBytes, 0, BlockBytes); CheckBufferLoad(*device, CompileBufferLoad(*device, block.Address() + colorBytes, output.Address()), output); CheckUniform(LoadSamples(*device, block), Ones, "after the native buffer access refusals and adjacent buffer control"); std::cout << "native buffer controls: adjacent=passed deferred=" << (prepared != nullptr) << " recipe=" << recipeReused << std::endl; @@ -1385,6 +1497,10 @@ int main(int argc, char** argv) { Block standard(BlockBytes); StoreWords(standard, colorBytes, FillOnes); RequireRefused(*device, TargetDescriptor(standard, SampleShift, Format16161616Float, TileS64KBX), "has no modeled multisampled color footprint", "loading an S swizzle multisampled texture no draw rendered to"); + device->WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(device->Device()); + CheckBdaSpan(*device, block, 0, false); + Draw(*device, TargetState(DecodeTarget(block))); std::puts("every sample of a 4x color target loaded through its multisampled texture descriptor matches the triangle drawn into it, then the fill of its color memory; a surface no draw rendered to loads the value its color memory repeats and keeps it under a later draw"); struct ReleaseIndexDevice { std::unique_ptr& device; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp index fe4ba8a092..a8c47fc2c4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp @@ -512,6 +512,10 @@ int main() { std::puts("skipped, the device lacks fragmentShaderPixelInterlock"); return VulkanTestSkipped; } + if (!TargetHasCapability(device->Target(), spv::CapabilityStorageImageReadWithoutFormat)) { + std::puts("skipped, the device lacks shaderStorageImageReadWithoutFormat"); + return VulkanTestSkipped; + } const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 4); Block block(layout.Bytes()); const auto unorm = DecodeTarget(block, Unorm8888Info); diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index 45cb44d74b..c2149823d0 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -58,6 +58,10 @@ double AudioOut2TraceSeconds() { return std::chrono::duration(Clock::now() - start).count(); } +static Clock::time_point Now(const AudioOut2Context& context) { + return context.clockForTest ? *context.clockForTest : Clock::now(); +} + static std::mutex g_contextsLock; static std::unordered_set g_contexts; @@ -285,6 +289,18 @@ static void TraceSummary(AudioOut2Context& context, Clock::time_point now) { context.summaryPeak = 0.0f; } +static Clock::time_point WaitForRoom(AudioOut2Context& context, std::unique_lock& lock) { + auto now = Now(context); + const auto waitStart = now; + while (QueueLevel(context, now) >= context.queueDepth && now - waitStart <= FULL_WAIT_TIMEOUT) { + lock.unlock(); + PreciseSleepUs(std::chrono::microseconds(FULL_WAIT_STEP).count()); + lock.lock(); + now = Now(context); + } + return now; +} + extern "C" { int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) try { @@ -368,7 +384,7 @@ int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint3 return 0; } std::lock_guard lock(context->lock); - const auto level = std::min(context->queueDepth, QueueLevel(*context, Clock::now()) + static_cast(context->advancedGrains.size())); + const auto level = std::min(context->queueDepth, QueueLevel(*context, Now(*context)) + static_cast(context->advancedGrains.size())); if (queue_level) *queue_level = level; if (available_queue) *available_queue = context->queueDepth - level; context->queueLevelPolls++; @@ -383,24 +399,17 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; std::unique_lock lock(context->lock); const QueueLevelPublisher publication{*context, lock}; - auto now = Clock::now(); - const auto waitStart = now; - while (QueueLevel(*context, now) >= context->queueDepth) { - if (!blocking) { - context->fullRejects++; - if (context->fullRejects <= CALL_TRACE_FULL) AUDIOOUT2_TRACE("t=%.3f Push ctx %p non-blocking on a full queue (%u): rejected\n", AudioOut2TraceSeconds(), static_cast(context), context->queueDepth); - return SCE_AUDIO_OUT2_ERROR_QUEUE_FULL; - } - if (now - waitStart > FULL_WAIT_TIMEOUT) break; - lock.unlock(); - PreciseSleepUs(std::chrono::microseconds(FULL_WAIT_STEP).count()); - lock.lock(); - now = Clock::now(); + auto now = Now(*context); + if (!blocking && QueueLevel(*context, now) >= context->queueDepth) { + context->fullRejects++; + if (context->fullRejects <= CALL_TRACE_FULL) AUDIOOUT2_TRACE("t=%.3f Push ctx %p non-blocking on a full queue (%u): rejected\n", AudioOut2TraceSeconds(), static_cast(context), context->queueDepth); + return SCE_AUDIO_OUT2_ERROR_QUEUE_FULL; } + if (blocking) now = WaitForRoom(*context, lock); if (context->advancedMode && context->advancedGrains.empty()) { if (!blocking) throw std::runtime_error("AudioOut2: a non-blocking push without a rendered grain is not implemented"); if (!context->grainReady.wait(lock, LibcShutdownToken_nid_postfix(), [&] { return !context->advancedGrains.empty(); })) throw ProcessShutdown{}; - now = Clock::now(); + now = Now(*context); Drain(*context, now); } context->queued++; @@ -437,6 +446,7 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block context->output.insert(context->output.end(), context->mix.begin(), context->mix.end()); } } + if (blocking) now = WaitForRoom(*context, lock); context->pushes++; context->summaryPushes++; if (blocking) context->blockingPushes++; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp index 2e315598b3..b50c512b0b 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -129,6 +130,7 @@ struct AudioOut2Context { std::chrono::steady_clock::time_point summaryStart; bool deviceBacked = false; std::atomic publishedQueueLevel{0}; + std::optional clockForTest; }; AudioOut2Context* AudioOut2FindContext(AudioOut2ContextHandle ctx); diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp index 804325eba3..691045c6b8 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -213,6 +214,104 @@ void TestLevelWithoutADeviceUsesThePlaybackClock() { session.Close(); } +void TestBlockingPushWaitsForRoomWithoutADevice() { + SetEnvironment("SDL_DISKAUDIOFILE", std::filesystem::temp_directory_path().string()); + constexpr std::uint32_t paced = 1024; + AudioOut2ContextParam params{}; + Require(sceAudioOut2ContextResetParam(¶ms) == 0); + params.num_grains = paced; + params.queue_depth = queueDepth; + AudioOut2ContextHandle handle = 0; + Require(sceAudioOut2ContextCreate(¶ms, nullptr, 0, &handle) == 0); + Require(reinterpret_cast(handle)->device == 0); + std::uint32_t level = 99; + std::uint32_t available = 99; + for (std::uint32_t round = 0; round < 4; round++) { + Require(sceAudioOut2ContextPush(handle, 1) == 0); + Require(sceAudioOut2ContextGetQueueLevel(handle, &level, &available) == 0); + Require(level < queueDepth && available > 0); + } + Require(sceAudioOut2ContextDestroy(handle) == 0); +} + +AudioOut2ContextHandle CreateContextOnATestClock() { + SetEnvironment("SDL_DISKAUDIOFILE", std::filesystem::temp_directory_path().string()); + AudioOut2ContextParam params{}; + Require(sceAudioOut2ContextResetParam(¶ms) == 0); + params.num_grains = grain; + params.queue_depth = queueDepth; + AudioOut2ContextHandle handle = 0; + Require(sceAudioOut2ContextCreate(¶ms, nullptr, 0, &handle) == 0); + auto& context = *reinterpret_cast(handle); + std::lock_guard lock(context.lock); + Require(context.device == 0); + const auto start = std::chrono::steady_clock::now(); + context.clockForTest = start; + context.playHead = start; + return handle; +} + +void PlayOneGrainOnTheTestClock(AudioOut2ContextHandle handle) { + auto& context = *reinterpret_cast(handle); + std::lock_guard lock(context.lock); + *context.clockForTest += std::chrono::duration_cast(std::chrono::duration(static_cast(grain) / frequency)) + std::chrono::microseconds(1); +} + +template +bool ContextReaches(AudioOut2ContextHandle handle, std::chrono::milliseconds limit, Predicate predicate) { + auto& context = *reinterpret_cast(handle); + const auto deadline = std::chrono::steady_clock::now() + limit; + for (;;) { + { + std::lock_guard lock(context.lock); + if (predicate(context)) return true; + } + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +std::uint32_t QueueLevelNow(AudioOut2ContextHandle handle) { + std::uint32_t level = 99; + std::uint32_t available = 99; + Require(sceAudioOut2ContextGetQueueLevel(handle, &level, &available) == 0); + Require(level + available == queueDepth); + return level; +} + +void TestBlockingPushStaysBlockedUntilPlaybackFreesASlot() { + const auto handle = CreateContextOnATestClock(); + for (std::uint32_t push = 0; push + 1 < queueDepth; push++) Require(sceAudioOut2ContextPush(handle, 1) == 0); + Require(QueueLevelNow(handle) == queueDepth - 1); + auto push = std::async(std::launch::async, [handle] { return sceAudioOut2ContextPush(handle, 1); }); + std::uint64_t pushesOnceQueued = 0; + Require(ContextReaches(handle, std::chrono::seconds(10), [&](const AudioOut2Context& context) { + pushesOnceQueued = context.pushes; + return context.queued == queueDepth; + })); + Require(pushesOnceQueued == queueDepth - 1); + Require(push.wait_for(std::chrono::seconds(0)) == std::future_status::timeout); + Require(QueueLevelNow(handle) == queueDepth); + PlayOneGrainOnTheTestClock(handle); + Require(push.get() == 0); + Require(QueueLevelNow(handle) == queueDepth - 1); + Require(sceAudioOut2ContextDestroy(handle) == 0); +} + +void TestBlockingPushOnAFullQueueQueuesOnlyOnceASlotIsFree() { + const auto handle = CreateContextOnATestClock(); + for (std::uint32_t push = 0; push < queueDepth; push++) Require(sceAudioOut2ContextPush(handle, 0) == 0); + Require(sceAudioOut2ContextPush(handle, 0) == queueFull); + auto push = std::async(std::launch::async, [handle] { return sceAudioOut2ContextPush(handle, 1); }); + Require(!ContextReaches(handle, std::chrono::seconds(1), [](const AudioOut2Context& context) { return context.queued > queueDepth; })); + Require(push.wait_for(std::chrono::seconds(0)) == std::future_status::timeout); + PlayOneGrainOnTheTestClock(handle); + PlayOneGrainOnTheTestClock(handle); + Require(push.get() == 0); + Require(QueueLevelNow(handle) == queueDepth - 1); + Require(sceAudioOut2ContextDestroy(handle) == 0); +} + void TestPrimingAfterRunningDry() { Session session("anyps5_audio_out2_priming.raw"); session.Fill(0.5f); @@ -372,6 +471,9 @@ int main() { TestDeviceLevelDoesNotWaitForTheContext(false); TestDeviceLevelDoesNotWaitForTheContext(true); TestLevelWithoutADeviceUsesThePlaybackClock(); + TestBlockingPushStaysBlockedUntilPlaybackFreesASlot(); + TestBlockingPushOnAFullQueueQueuesOnlyOnceASlotIsFree(); + TestBlockingPushWaitsForRoomWithoutADevice(); TestPrimingAfterRunningDry(); TestLowLatencyCallsRejectBadArguments(); TestLowLatencyReadsAtNextPush(); diff --git a/core/libs/prx/libSceFreeTypeOt/CMakeLists.txt b/core/libs/prx/libSceFreeTypeOt/CMakeLists.txt index 4d34176d1f..5fe4b84169 100644 --- a/core/libs/prx/libSceFreeTypeOt/CMakeLists.txt +++ b/core/libs/prx/libSceFreeTypeOt/CMakeLists.txt @@ -18,3 +18,20 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +target_sources(libSceFreeTypeOt PRIVATE src/Memory.cpp) +if(BUILD_TESTING) + add_test_executable(guest_freetype_ot_memory_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeOtMemory.cpp) + target_include_directories(guest_freetype_ot_memory_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_ot_memory_tests PRIVATE libSceFreeTypeOt) + if(WIN32) + target_link_libraries(guest_freetype_ot_memory_tests PRIVATE libc) + endif() + configure_windows_unwind(guest_freetype_ot_memory_tests) + add_test(NAME guest_freetype_ot_memory COMMAND guest_freetype_ot_memory_tests) + set_tests_properties(guest_freetype_ot_memory PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_freetype_ot_memory PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceFreeTypeOt/Export.cpp b/core/libs/prx/libSceFreeTypeOt/Export.cpp index 8669bd9fe2..da2d170fba 100644 --- a/core/libs/prx/libSceFreeTypeOt/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOt/Export.cpp @@ -590,46 +590,6 @@ int APS5_VABI sceFreeTypeOtUnknown_AEU2tkjdZ2w_nid_no_patch() { } APS5_EXPORT("AEU2tkjdZ2w", sceFreeTypeOtUnknown_AEU2tkjdZ2w_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_fPrLu1YGOaw_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("fPrLu1YGOaw", sceFreeTypeOtUnknown_fPrLu1YGOaw_nid_no_patch); - -int APS5_VABI sceFreeTypeOtUnknown_t3vCgnT5YL0_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("t3vCgnT5YL0", sceFreeTypeOtUnknown_t3vCgnT5YL0_nid_no_patch); - -int APS5_VABI sceFreeTypeOtUnknown_8bkQ_YRnpOE_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("8bkQ+YRnpOE", sceFreeTypeOtUnknown_8bkQ_YRnpOE_nid_no_patch); - -int APS5_VABI sceFreeTypeOtUnknown_q4qhGebYuGI_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("q4qhGebYuGI", sceFreeTypeOtUnknown_q4qhGebYuGI_nid_no_patch); - -int APS5_VABI sceFreeTypeOtUnknown_ni_K_leoQNg_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("ni-K-leoQNg", sceFreeTypeOtUnknown_ni_K_leoQNg_nid_no_patch); - -int APS5_VABI sceFreeTypeOtUnknown_XfcgJB6V2a0_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("XfcgJB6V2a0", sceFreeTypeOtUnknown_XfcgJB6V2a0_nid_no_patch); - -int APS5_VABI sceFreeTypeOtUnknown_TWzf5iykiGw_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("TWzf5iykiGw", sceFreeTypeOtUnknown_TWzf5iykiGw_nid_no_patch); - -int APS5_VABI sceFreeTypeOtUnknown__b4iVKArt94_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("-b4iVKArt94", sceFreeTypeOtUnknown__b4iVKArt94_nid_no_patch); - int APS5_VABI sceFreeTypeOtUnknown_uSNqxrARpEQ_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } diff --git a/core/libs/prx/libSceFreeTypeOt/include/Memory.hpp b/core/libs/prx/libSceFreeTypeOt/include/Memory.hpp new file mode 100644 index 0000000000..b0453f8844 --- /dev/null +++ b/core/libs/prx/libSceFreeTypeOt/include/Memory.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "prx/libSceFreeType/include/GuestList.hpp" + +#include + +extern "C" { + +void* APS5_VABI ft_mem_alloc_nid_postfix(FreeType::GuestMemoryRec* memory, std::int64_t size, std::int32_t* error); +void* APS5_VABI ft_mem_qalloc_nid_postfix(FreeType::GuestMemoryRec* memory, std::int64_t size, std::int32_t* error); +void* APS5_VABI ft_mem_realloc_nid_postfix(FreeType::GuestMemoryRec* memory, std::int64_t itemSize, std::int64_t currentCount, std::int64_t newCount, void* block, std::int32_t* error); +void* APS5_VABI ft_mem_qrealloc_nid_postfix(FreeType::GuestMemoryRec* memory, std::int64_t itemSize, std::int64_t currentCount, std::int64_t newCount, void* block, std::int32_t* error); +void APS5_VABI ft_mem_free_nid_postfix(FreeType::GuestMemoryRec* memory, const void* block); +void* APS5_VABI ft_mem_dup_nid_postfix(FreeType::GuestMemoryRec* memory, const void* address, std::uint64_t size, std::int32_t* error); +void* APS5_VABI ft_mem_strdup_nid_postfix(FreeType::GuestMemoryRec* memory, const char* string, std::int32_t* error); +std::int32_t APS5_VABI ft_mem_strcpyn_nid_postfix(char* destination, const char* source, std::uint64_t size); + +} diff --git a/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp b/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp index 00e2da7277..1155682fa1 100644 --- a/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp +++ b/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp @@ -3,3 +3,4 @@ #include "prx/libSceFreeTypeOt/include/Stream.hpp" #include "prx/libSceFreeType/include/Api.hpp" #include "prx/libSceFreeTypeOt/include/Calc.hpp" +#include "prx/libSceFreeTypeOt/include/Memory.hpp" diff --git a/core/libs/prx/libSceFreeTypeOt/src/Memory.cpp b/core/libs/prx/libSceFreeTypeOt/src/Memory.cpp new file mode 100644 index 0000000000..95a2712409 --- /dev/null +++ b/core/libs/prx/libSceFreeTypeOt/src/Memory.cpp @@ -0,0 +1,104 @@ +#include "prx/libSceFreeTypeOt/include/Memory.hpp" + +#include +#include +#include + +namespace { + +constexpr std::int32_t InvalidArgument = 0x06; +constexpr std::int32_t ArrayTooLarge = 0x0A; +constexpr std::int32_t OutOfMemory = 0x40; +constexpr std::int64_t IntMax = 0x7FFFFFFF; + +std::int64_t Bytes(std::int64_t count, std::int64_t itemSize) { + return static_cast(static_cast(count) * static_cast(itemSize)); +} + +} + +extern "C" { + +void* APS5_VABI ft_mem_qalloc_nid_postfix(FreeType::GuestMemoryRec* memory, std::int64_t size, std::int32_t* error) { + std::int32_t result = 0; + void* block = nullptr; + if (size > 0) { + block = memory->alloc(memory, size); + if (!block) result = OutOfMemory; + } else if (size < 0) { + result = InvalidArgument; + } + *error = result; + return block; +} + +void* APS5_VABI ft_mem_alloc_nid_postfix(FreeType::GuestMemoryRec* memory, std::int64_t size, std::int32_t* error) { + std::int32_t result = 0; + void* block = ft_mem_qalloc_nid_postfix(memory, size, &result); + if (!result && block && size > 0) std::memset(block, 0, static_cast(size)); + *error = result; + return block; +} + +void APS5_VABI ft_mem_free_nid_postfix(FreeType::GuestMemoryRec* memory, const void* block) { + if (block) memory->free(memory, const_cast(block)); +} + +void* APS5_VABI ft_mem_qrealloc_nid_postfix(FreeType::GuestMemoryRec* memory, std::int64_t itemSize, std::int64_t currentCount, + std::int64_t newCount, void* block, std::int32_t* error) { + std::int32_t result = 0; + if (currentCount < 0 || newCount < 0 || itemSize < 0) { + result = InvalidArgument; + } else if (newCount == 0 || itemSize == 0) { + ft_mem_free_nid_postfix(memory, block); + block = nullptr; + } else if (newCount > IntMax / itemSize) { + result = ArrayTooLarge; + } else if (currentCount == 0) { + block = ft_mem_alloc_nid_postfix(memory, newCount * itemSize, &result); + } else { + void* resized = memory->realloc(memory, Bytes(currentCount, itemSize), newCount * itemSize, block); + if (resized) { + block = resized; + } else { + result = OutOfMemory; + } + } + *error = result; + return block; +} + +void* APS5_VABI ft_mem_realloc_nid_postfix(FreeType::GuestMemoryRec* memory, std::int64_t itemSize, std::int64_t currentCount, + std::int64_t newCount, void* block, std::int32_t* error) { + std::int32_t result = 0; + block = ft_mem_qrealloc_nid_postfix(memory, itemSize, currentCount, newCount, block, &result); + if (!result && block && newCount > currentCount) { + std::memset(static_cast(block) + currentCount * itemSize, 0, static_cast((newCount - currentCount) * itemSize)); + } + *error = result; + return block; +} + +void* APS5_VABI ft_mem_dup_nid_postfix(FreeType::GuestMemoryRec* memory, const void* address, std::uint64_t size, std::int32_t* error) { + std::int32_t result = 0; + void* copy = ft_mem_qalloc_nid_postfix(memory, static_cast(size), &result); + if (!result && address && size > 0) std::memcpy(copy, address, static_cast(size)); + *error = result; + return copy; +} + +void* APS5_VABI ft_mem_strdup_nid_postfix(FreeType::GuestMemoryRec* memory, const char* string, std::int32_t* error) { + const std::uint64_t length = string ? std::strlen(string) + 1 : 0; + return ft_mem_dup_nid_postfix(memory, string, length, error); +} + +std::int32_t APS5_VABI ft_mem_strcpyn_nid_postfix(char* destination, const char* source, std::uint64_t size) { + while (size > 1 && *source != 0) { + *destination++ = *source++; + --size; + } + *destination = 0; + return *source != 0; +} + +} diff --git a/core/libs/prx/libSceHttp2/CMakeLists.txt b/core/libs/prx/libSceHttp2/CMakeLists.txt index 15d6efc930..45e3b07081 100644 --- a/core/libs/prx/libSceHttp2/CMakeLists.txt +++ b/core/libs/prx/libSceHttp2/CMakeLists.txt @@ -22,10 +22,13 @@ if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") endif() install(FILES ${CMAKE_SOURCE_DIR}/3rdparty/nghttp2/COPYING DESTINATION licenses/nghttp2) -add_library(anyps5_http2_transport STATIC EXCLUDE_FROM_ALL Transport.cpp) -target_include_directories(anyps5_http2_transport PRIVATE ${LIBS_INCLUDE_DIR}) +set(http2ZlibDir ${CMAKE_SOURCE_DIR}/3rdparty/libjpeg-turbo/src/spng/zlib) +add_library(anyps5_http2_transport STATIC EXCLUDE_FROM_ALL Transport.cpp + ${http2ZlibDir}/adler32.c ${http2ZlibDir}/crc32.c ${http2ZlibDir}/inffast.c + ${http2ZlibDir}/inflate.c ${http2ZlibDir}/inftrees.c ${http2ZlibDir}/zutil.c) +target_include_directories(anyps5_http2_transport PRIVATE ${LIBS_INCLUDE_DIR} ${http2ZlibDir}) target_link_libraries(anyps5_http2_transport PRIVATE libSceNet libSceSsl libkernel libc nghttp2_static) -set_target_properties(anyps5_http2_transport PROPERTIES POSITION_INDEPENDENT_CODE ON CXX_VISIBILITY_PRESET hidden) +set_target_properties(anyps5_http2_transport PROPERTIES POSITION_INDEPENDENT_CODE ON CXX_VISIBILITY_PRESET hidden C_VISIBILITY_PRESET hidden) if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(anyps5_http2_transport INTERFACE LINKER:--exclude-libs,libanyps5_http2_transport.a) endif() diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index a87518e163..da8ca45a89 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -254,7 +254,7 @@ int APS5_VABI sceHttp2SetConnectTimeOut(int id, uint32_t usec) { } int APS5_VABI sceHttp2SetInflateGZIPEnabled(int id, int enable) { - return Update(id, __func__, [&](auto&) { if (enable != 0) NotImplemented_nid_no_patch("sceHttp2SetInflateGZIPEnabled"); }); + return Update(id, __func__, [&](auto& settings) { settings.inflateGzip = enable != 0; }); } int APS5_VABI sceHttp2SetRecvTimeOut(int id, uint32_t usec) { diff --git a/core/libs/prx/libSceHttp2/Transport.cpp b/core/libs/prx/libSceHttp2/Transport.cpp index 52aab59d91..60ba971b06 100644 --- a/core/libs/prx/libSceHttp2/Transport.cpp +++ b/core/libs/prx/libSceHttp2/Transport.cpp @@ -8,6 +8,8 @@ #endif #include "Transport.hpp" + +#include "zlib.h" #include "prx/libSceNet/include/Net.hpp" #include "prx/libSceSsl/include/Ssl.hpp" #include "prx/libc/include/General.hpp" @@ -36,6 +38,7 @@ using Clock = std::chrono::steady_clock; constexpr int WantRead = static_cast(0x8095f013); constexpr int WantWrite = static_cast(0x8095f014); constexpr int InvalidValue = static_cast(0x817B11FE); +constexpr std::size_t MaxGzipBytes = 64 * 1024 * 1024; [[noreturn]] void Unknown(const char* message) { NotImplemented_nid_no_patch(message); @@ -254,6 +257,8 @@ struct Request::State { bool ready = false; bool ended = false; int status = 0; + std::string contentEncoding; + bool inflatedGzip = false; int blockStatus = 0; int networkResult = 0; int socket = -1; @@ -318,6 +323,60 @@ struct Request::State { } } + bool Inflating() const { return settings.inflateGzip && contentEncoding == "gzip"; } + + void InflateGzip() { + if (inflatedGzip || !Inflating() || body.empty()) return; + inflatedGzip = true; + const std::uint8_t* data = body.data(); + const std::size_t size = body.size(); + Require(size >= 18 && data[0] == 0x1f && data[1] == 0x8b && data[2] == 8, "sceHttp2: malformed gzip header"); + const std::uint8_t flags = data[3]; + Require((flags & 0xe0) == 0, "sceHttp2: unknown gzip header flags"); + std::size_t offset = 10; + const auto skip = [&](std::size_t count) { Require(offset + count <= size - 8, "sceHttp2: truncated gzip header"); offset += count; }; + if (flags & 4) { + Require(offset + 2 <= size - 8, "sceHttp2: truncated gzip extra field"); + const std::size_t extra = static_cast(data[offset]) | (static_cast(data[offset + 1]) << 8); + skip(2 + extra); + } + if (flags & 8) while (skip(1), data[offset - 1] != 0) {} + if (flags & 16) while (skip(1), data[offset - 1] != 0) {} + if (flags & 2) skip(2); + z_stream inflater{}; + Require(inflateInit2(&inflater, -15) == Z_OK, "sceHttp2: gzip inflate initialization failed"); + struct End { + z_stream* stream; + ~End() { inflateEnd(stream); } + } end{&inflater}; + inflater.next_in = const_cast(data + offset); + inflater.avail_in = static_cast(size - offset - 8); + std::vector output; + std::uint8_t chunk[16384]; + int result = Z_OK; + while (result != Z_STREAM_END) { + inflater.next_out = chunk; + inflater.avail_out = sizeof(chunk); + result = inflate(&inflater, Z_NO_FLUSH); + Require(result == Z_OK || result == Z_STREAM_END, "sceHttp2: gzip inflate failed"); + output.insert(output.end(), chunk, chunk + (sizeof(chunk) - inflater.avail_out)); + Require(result == Z_STREAM_END || inflater.avail_in != 0 || inflater.avail_out == 0, "sceHttp2: truncated gzip stream"); + Require(output.size() <= MaxGzipBytes, "sceHttp2: inflated response exceeds the host limit"); + } + Require(inflater.avail_in == 0, "sceHttp2: concatenated gzip members are not implemented"); + const auto readLe32 = [&](std::size_t position) { + return static_cast(data[position]) | (static_cast(data[position + 1]) << 8) | + (static_cast(data[position + 2]) << 16) | (static_cast(data[position + 3]) << 24); + }; + Require(crc32(0, output.data(), static_cast(output.size())) == readLe32(size - 8), "sceHttp2: gzip checksum mismatch"); + Require(static_cast(output.size()) == readLe32(size - 4), "sceHttp2: gzip size mismatch"); + body = std::move(output); + bodyOffset = 0; + contentLength = body.size(); + hasLength = true; + contentEncoding.clear(); + } + int Connect() { #ifdef _WIN32 const int startup = sceNetSocket("http2-resolver", 2, 1, 0); @@ -412,6 +471,7 @@ struct Request::State { state.contentLength = Number(text); state.hasLength = true; } + if (key == "content-encoding") state.contentEncoding = Lower(std::string(text)); }); } @@ -433,15 +493,21 @@ struct Request::State { (frame->hd.flags & NGHTTP2_FLAG_END_STREAM)) { Require(state.ready, "sceHttp2: response ended without final headers"); state.ended = true; + state.InflateGzip(); } }); } - static int Data(nghttp2_session*, std::uint8_t, std::int32_t streamId, const std::uint8_t* bytes, std::size_t size, void* data) { + static int Data(nghttp2_session* session, std::uint8_t, std::int32_t streamId, const std::uint8_t* bytes, std::size_t size, void* data) { auto& state = *static_cast(data); return Callback(state, [&] { if (streamId != state.stream) return; - Require(state.body.size() + size <= 65535, "sceHttp2: receive flow-control limit exceeded"); + if (state.Inflating()) { + Require(state.body.size() + size <= MaxGzipBytes, "sceHttp2: encoded gzip response exceeds the host limit"); + CheckNg(nghttp2_session_consume(session, streamId, size)); + } else { + Require(state.body.size() + size <= 65535, "sceHttp2: receive flow-control limit exceeded"); + } state.body.insert(state.body.end(), bytes, bytes + size); }); } @@ -598,7 +664,7 @@ int Request::Read(void* data, std::size_t size) { try { const auto deadline = state->Deadline(state->settings.recvTimeout); state->Check(deadline); - while (state->bodyOffset == state->body.size() && !state->ended) state->Pump(deadline); + while (!state->ended && (state->bodyOffset == state->body.size() || state->Inflating())) state->Pump(deadline); const auto count = std::min({size, state->body.size() - state->bodyOffset, static_cast(std::numeric_limits::max())}); state->Check(deadline); if (count) std::memcpy(data, state->body.data() + state->bodyOffset, count); @@ -622,6 +688,12 @@ int Request::ContentLength(int* kind, std::uint64_t* length) { auto lock = state->Lock(); if (state->networkResult) return state->networkResult; Require(kind && length && state->ready, "sceHttp2GetResponseContentLength: null output or response unavailable"); + if (state->Inflating()) { + try { + const auto deadline = state->Deadline(state->settings.recvTimeout); + while (!state->ended) state->Pump(deadline); + } catch (...) { state->Fail(); throw; } + } *kind = state->hasLength ? 0 : 1; *length = state->hasLength ? state->contentLength : 0; return 0; diff --git a/core/libs/prx/libSceHttp2/Transport.hpp b/core/libs/prx/libSceHttp2/Transport.hpp index 5131206d6c..1595ab1048 100644 --- a/core/libs/prx/libSceHttp2/Transport.hpp +++ b/core/libs/prx/libSceHttp2/Transport.hpp @@ -22,6 +22,7 @@ struct Settings { std::vector> headers; std::uint64_t contentLength = 0; bool noContentLength = false; + bool inflateGzip = false; bool auth = true; int redirect = 1; std::uint32_t verifyFlags = 0x3d; diff --git a/core/libs/prx/libScePad/include/PadState.hpp b/core/libs/prx/libScePad/include/PadState.hpp index 365627eb3a..1dfc108fcc 100644 --- a/core/libs/prx/libScePad/include/PadState.hpp +++ b/core/libs/prx/libScePad/include/PadState.hpp @@ -80,6 +80,7 @@ bool IsConnected(int port); extern "C" void PadPublishInput_nid_postfix(const PadInputState& input); extern "C" void PadPublishInputForPort_nid_postfix(int port, const PadInputState& input); +extern "C" void PadApplyInjectedInput_nid_postfix(PadInputState* state); extern "C" void PadReportInputFailure_nid_postfix(std::exception_ptr error); extern "C" bool PadFetchOutput_nid_postfix(std::uint32_t* seenSequence, PadOutputState* out); extern "C" bool PadFetchOutputForPort_nid_postfix(int port, std::uint32_t* seenSequence, PadOutputState* out); diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index 2b57276eed..65618584ee 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -505,6 +505,14 @@ extern "C" void PadPublishInput_nid_postfix(const PadInputState& input) { PadPublishInputForPort_nid_postfix(0, input); } +extern "C" void PadApplyInjectedInput_nid_postfix(PadInputState* state) { + if (state == nullptr) return; + std::lock_guard lock(stateMutex); + const auto injected = InjectedInput(sceKernelGetProcessTime()); + state->buttons |= injected.buttons; + if (injected.hasSticks) state->sticks = injected.sticks; +} + extern "C" void PadReportInputFailure_nid_postfix(std::exception_ptr error) { if (!error) throw std::invalid_argument("Pad: missing input failure"); std::lock_guard lock(stateMutex); diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index 6e7441138f..027eee925e 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -62,6 +62,7 @@ private: std::array slots{}; VideoOutTelemetry::PadChangeFilter telemetryFilter; AgcDriver::PadStatusProvider statusProvider; + bool padCaptured = false; }; #endif diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 4a3c1b8185..922b4ced42 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -357,7 +357,9 @@ void PadInput::Update(const DisplayWindow& window) { return; } const bool changed = keyboardMouse.Advance(std::chrono::steady_clock::now()); - if (slots[0].controller != nullptr || keyboardMouse.MouseMode() || changed) { + const bool captured = ImeCapturesPad_nid_postfix() || ModalDialogCapturesInput_nid_postfix(); + const bool captureChanged = std::exchange(padCaptured, captured) != captured; + if (slots[0].controller != nullptr || keyboardMouse.MouseMode() || changed || captured || captureChanged) { publish(window.FrameNumber(), 0); } for (int slot = 1; slot < PAD_MAX_CONTROLLERS; ++slot) { @@ -377,6 +379,7 @@ PadInputState PadInput::handOff(int slot, PadInputState state) { auto& held = slots[slot].heldThroughCapture; const bool imeCaptures = ImeCapturesPad_nid_postfix(); if (imeCaptures || ModalDialogCapturesInput_nid_postfix()) { + if (slot == 0) PadApplyInjectedInput_nid_postfix(&state); if (slot == 0 && imeCaptures) ImePublishPad_nid_postfix({state.buttons, state.sticks[0], state.sticks[1], state.analogButtonsL2}); else if (slot == 0) ModalDialogPublishPad_nid_postfix({state.buttons, state.sticks[0], state.sticks[1], state.analogButtonsL2}); held = state.buttons; diff --git a/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp b/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp index 894b928bf8..489eb45c5b 100644 --- a/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp +++ b/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp @@ -7,8 +7,13 @@ #include "prx/libScePad/include/PadState.hpp" #include "prx/libSceIme/include/ImeInput.hpp" #include "prx/libSceIme/include/ModalDialog.hpp" +#include #include #include +#include +#include +#include +#include #include #undef main @@ -113,6 +118,40 @@ void HandOff(PadInput& padInput, const DisplayWindow& window, int device) { ModalDialogSetPresenter_nid_postfix(false); } +void InjectedAnswer(PadInput& padInput, const DisplayWindow& window, const std::filesystem::path& commandPath) { + constexpr std::uint32_t Cross = 0x4000; + constexpr std::uint32_t Intercepted = 0x80000000; + ModalDialogSetPresenter_nid_postfix(true); + ModalDialogRequest request; + request.message = u"Create new data?"; + request.buttons = {u"OK"}; + Require(ModalDialogOpen_nid_postfix(request)); + padInput.Update(window); + Require(Pad::ReadState(0).buttons == Intercepted); + std::ofstream(commandPath) << "1 16384 400\n"; + std::size_t chosen = 9; + ModalDialogState state = ModalDialogState::Showing; + for (int frame = 0; frame < 200 && state == ModalDialogState::Showing; ++frame) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + padInput.Update(window); + Require(Pad::ReadState(0).buttons == Intercepted); + state = ModalDialogPoll_nid_postfix(chosen); + } + Require(state == ModalDialogState::Answered && chosen == 0); + padInput.Update(window); + Require((Pad::ReadState(0).buttons & Intercepted) == 0); + std::ofstream(commandPath) << "2 16384 400\n"; + bool reached = false; + for (int frame = 0; frame < 200 && !reached; ++frame) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + padInput.Update(window); + reached = Pad::ReadState(0).buttons == Cross; + } + Require(reached); + ModalDialogReset_nid_postfix(); + ModalDialogSetPresenter_nid_postfix(false); +} + } int main() { @@ -123,6 +162,17 @@ int main() { return 1; #endif + const auto commandPath = std::filesystem::temp_directory_path() / ("anyps5-pad-sdl-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + struct Cleanup { + std::filesystem::path path; + ~Cleanup() { std::error_code error; std::filesystem::remove(path, error); } + } cleanup{commandPath}; +#ifdef _WIN32 + Require(_wputenv_s(L"APS5_PAD_INPUT_FILE", commandPath.c_str()) == 0); +#else + Require(setenv("APS5_PAD_INPUT_FILE", commandPath.string().c_str(), 1) == 0); +#endif + Require(SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK) == 0); const int hostJoysticks = SDL_NumJoysticks(); @@ -199,6 +249,7 @@ int main() { padInput.HandleEvent(rem0, window); Require(!padInput.SlotOccupied(0)); Require(!padInput.SlotOccupied(1)); + InjectedAnswer(padInput, window, commandPath); Require(SDL_JoystickDetachVirtual(dev1) == 0); Require(SDL_JoystickDetachVirtual(dev0) == 0); diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 176bc416d1..a65adb325e 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -262,6 +262,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_memccpy_bounds_tests ${LIBS_INCLUDE_DIR}/tests/GuestMemccpyBounds.cpp) + target_include_directories(guest_memccpy_bounds_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_memccpy_bounds_tests PRIVATE libc) + configure_windows_unwind(guest_memccpy_bounds_tests) + add_test(NAME guest_memccpy_bounds COMMAND guest_memccpy_bounds_tests) + set_tests_properties(guest_memccpy_bounds PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_memccpy_bounds PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_allocating_strings_tests ${LIBS_INCLUDE_DIR}/tests/GuestAllocatingStrings.cpp) target_include_directories(guest_allocating_strings_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_allocating_strings_tests PRIVATE libc libkernel) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 0ddde07db8..a1e555c3aa 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -102,6 +102,7 @@ const char* CutAtQuadScanDirective(const char* format, std::string& head) { #endif std::string HostLength(const std::string& length, char conversion) { + if (std::strchr("sc[", conversion) && length != "l") return {}; #ifdef _WIN32 if (length == "l" && std::strchr("diouxXn", conversion) != nullptr) return "ll"; #else @@ -110,7 +111,46 @@ std::string HostLength(const std::string& length, char conversion) { return length; } +const char* ScanLengthEnd(const char* cursor) { + if (*cursor == 'h' || *cursor == 'l') { + const char first = *cursor++; + if (*cursor == first) ++cursor; + } else if (*cursor != '\0' && std::strchr("jztL", *cursor)) ++cursor; + return cursor; +} + +const char* HostScanFormat(const char* format, std::string& normalized) { + if (format == nullptr) return format; + const char* copied = format; + const char* cursor = format; + while ((cursor = std::strchr(cursor, '%')) != nullptr) { + ++cursor; + if (*cursor == '%') { ++cursor; continue; } + while (*cursor && std::strchr("-+ #0'0123456789.*$", *cursor)) ++cursor; + const char* const length = cursor; + cursor = ScanLengthEnd(cursor); + const bool wide = cursor == length + 1 && *length == 'l'; + if (cursor != length && !wide && *cursor && std::strchr("sc[", *cursor)) { + normalized.append(copied, length); + copied = cursor; + } + if (*cursor == '[') { + ++cursor; + if (*cursor == '^') ++cursor; + if (*cursor == ']') ++cursor; + while (*cursor && *cursor != ']') ++cursor; + } + if (*cursor) ++cursor; + else break; + } + if (copied == format) return format; + normalized.append(copied); + return normalized.c_str(); +} + int HostVsscanf(const char* input, const char* format, std::va_list args) { + std::string normalized; + format = HostScanFormat(format, normalized); #if defined(__GLIBC__) const int savedError = errno; const int result = __isoc99_vsscanf(input, format, args); @@ -131,6 +171,8 @@ int HostSscanf(const char* input, const char* format, ...) { #ifndef _WIN32 int HostVfscanf(std::FILE* stream, const char* format, std::va_list args) { + std::string normalized; + format = HostScanFormat(format, normalized); #if defined(__GLIBC__) return __isoc99_vfscanf(stream, format, args); #else diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index 597b166790..c33509fca8 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -2,6 +2,8 @@ #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include +#include #include #include #include @@ -226,6 +228,43 @@ static std::string ReadWhole(const std::string& path) { return std::string(buffer.data(), size); } +static int APS5_VABI ScanSafeList(FileStream* stream, const char* format, ...) { + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); + const int result = vfscanf_s_nid_postfix(stream, format, reinterpret_cast(args)); + __builtin_sysv_va_end(args); + return result; +} + +static void CheckCharacterScanLengths() { + using Scanner = int (APS5_VABI*)(FileStream*, const char*, ...); + const Scanner scanners[]{fscanf_nid_postfix, ScanList, fscanf_s_nid_postfix, ScanSafeList}; + for (std::size_t api = 0; api < std::size(scanners); ++api) { +#ifdef _WIN32 + if (api < 2) continue; +#endif + for (const char* length : {"hh", "h", "ll", "j", "z", "t", "L"}) { + for (const char* conversion : {"s", "c", "[a-z]"}) { + auto* native = std::tmpfile(); + Require(native != nullptr && std::fputs("ab", native) >= 0); + std::rewind(native); + FileStream stream(native); + std::array output; + output.fill('Z'); + int consumed = -1; + const std::string format = std::string("%2") + length + conversion + "%n"; + const auto scan = scanners[api]; + const int result = api >= 2 ? scan(&stream, format.c_str(), output.data(), 32u, &consumed) : scan(&stream, format.c_str(), output.data(), &consumed); + Require(result == 1 && consumed == 2, "Stream character scan ignored-length consumption"); + const bool single = conversion[0] == 'c'; + Require(output[0] == 'a' && output[1] == 'b' && (single || output[2] == 0), "Stream character scan ignored length stored wide data"); + Require(std::all_of(output.begin() + (single ? 2 : 3), output.end(), [](char value) { return value == 'Z'; })); + Require(std::fclose(native) == 0); + } + } + } +} + template static void CheckReadAfterWrite(const std::string& path, const char* mode, const char* expected, TRead read) { auto* stream = fopen_nid_postfix(path.c_str(), "wb"); @@ -307,6 +346,7 @@ static void CheckWriteAfterRead(const std::string& path, const char* mode, const } static void CheckStreamWritesAfterReads(const std::string& base) { + CheckCharacterScanLengths(); const std::string path = base + ".written"; constexpr char appended[] = "0123456789XY"; #if defined(__GLIBC__) diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 468005d4d2..87e27c19cd 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -5,6 +5,8 @@ #include "prx/libc/include/ScanQuadLength.hpp" #include #include +#include +#include #include #include #include @@ -67,6 +69,41 @@ static int APS5_VABI ScanListS(const char* buffer, const char* format, ...) { return result; } +static void CheckCharacterScanLengths() { + const auto check = [](bool condition, const char* message = "Character scan length check failed") { + if (!condition) throw std::runtime_error(message); + }; + using Scanner = int (APS5_VABI*)(const char*, const char*, ...); + const Scanner scanners[]{sscanf_nid_postfix, ScanList, sscanf_s_nid_postfix, ScanListS}; + for (std::size_t api = 0; api < std::size(scanners); ++api) { + const auto scan = scanners[api]; + const bool secure = api >= 2; +#ifdef _WIN32 + if (!secure) continue; +#endif + for (const char* length : {"hh", "h", "ll", "j", "z", "t", "L"}) { + for (const char* conversion : {"s", "c", "[a-z]"}) { + const std::string format = std::string("%2") + length + conversion + "%n"; + std::array output; + output.fill('Z'); + int consumed = -1; + const int result = secure ? scan("ab", format.c_str(), output.data(), 32u, &consumed) : scan("ab", format.c_str(), output.data(), &consumed); + check(result == 1 && consumed == 2, "Character scan ignored-length consumption"); + const bool single = conversion[0] == 'c'; + check(output[0] == 'a' && output[1] == 'b' && (single || output[2] == '\0'), "Character scan ignored length stored wide data"); + check(std::all_of(output.begin() + (single ? 2 : 3), output.end(), [](char value) { return value == 'Z'; }), "Character scan changed its destination guard"); + } + } + for (const auto& example : {std::pair{"%lls", "%5[%lls]%n"}, std::pair{"% ab", "%% %2lls%n"}}) { + std::array output{}; + int consumed = -1; + const int result = secure ? scan(example.first, example.second, output.data(), 32u, &consumed) : scan(example.first, example.second, output.data(), &consumed); + check(result == 1 && consumed == 4); + check(std::strcmp(output.data(), example.first[1] == ' ' ? "ab" : "%lls") == 0, "Character scan normalization changed a literal or scanset"); + } + } +} + static int APS5_VABI PrintList(const char* format, ...) { __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); @@ -78,6 +115,7 @@ static int APS5_VABI PrintList(const char* format, ...) { } static void CheckIgnoredLengths() { + CheckCharacterScanLengths(); char output[96]; Require(snprintf_nid_postfix(output, sizeof(output), "%Ld|%Lx|%Lu|%Lo", -1985229329, 0x89abcdefu, 0x89abcdefu, 0x89abcdefu) == 43 && std::strcmp(output, "-1985229329|89abcdef|2309737967|21152746757") == 0); diff --git a/core/libs/tests/GuestAcm.cpp b/core/libs/tests/GuestAcm.cpp index bce7fb869e..366cf3764b 100644 --- a/core/libs/tests/GuestAcm.cpp +++ b/core/libs/tests/GuestAcm.cpp @@ -288,6 +288,25 @@ int main() { outputA.channelCount = 3; Require(reverb(1, reverbFilters, reverbOutputs) == BadPointer); Require(reverbBatch.offset == 0 && reverbBatch.buffer_size == sizeof(reverbCommands)); + commandBatch = {commands, 0, sizeof(commands)}; + std::memset(commands, 0xCC, sizeof(commands)); + Require(sceAcmBatchJobPriority(nullptr, 0) == BadPointer); + Require(sceAcmBatchJobPriority(&commandBatch, 4) == BadPointer && commandBatch.offset == 0); + Require(sceAcmBatchJobPriority(&commandBatch, 0xFFFFFFFFu) == BadPointer && commandBatch.offset == 0); + Require(sceAcmBatchJobPriority(&commandBatch, 3) == 0 && commandBatch.offset == 8); + Require(sceAcmBatchJobPriority(&commandBatch, 0) == 0 && commandBatch.offset == 16 && commandBatch.buffer_size == sizeof(commands)); + std::uint64_t priorityWords[3]; + std::memcpy(priorityWords, commands, sizeof(priorityWords)); + Require(priorityWords[0] == 0x310 && priorityWords[1] == 0x10 && priorityWords[2] == 0xCCCCCCCCCCCCCCCCull); + commandBatch = {commands, sizeof(commands) - 7, sizeof(commands)}; + Require(sceAcmBatchJobPriority(&commandBatch, 1) == static_cast(0x81940009) && commandBatch.offset == sizeof(commands) - 7); + Require(sceAcmBatchJobPriority(&commandBatch, 4) == BadPointer && commandBatch.offset == sizeof(commands) - 7); + commandBatch = {commands, sizeof(commands) + 8, sizeof(commands)}; + Require(sceAcmBatchJobPriority(&commandBatch, 1) == static_cast(0x81940009) && commandBatch.offset == sizeof(commands) + 8); + commandBatch = {commands, sizeof(commands) - 8, sizeof(commands)}; + Require(sceAcmBatchJobPriority(&commandBatch, 1) == 0 && commandBatch.offset == sizeof(commands)); + std::memcpy(priorityWords, commands + sizeof(commands) - 8, sizeof(std::uint64_t)); + Require(priorityWords[0] == 0x110); Require(sceAcmContextDestroy(context1) == 0); Require(sceAcmContextDestroy(context1) == BadContext); diff --git a/core/libs/tests/GuestFreeTypeOtMemory.cpp b/core/libs/tests/GuestFreeTypeOtMemory.cpp new file mode 100644 index 0000000000..1a1d9691a8 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeOtMemory.cpp @@ -0,0 +1,258 @@ +#include "prx/libSceFreeTypeOt/include/Memory.hpp" +#include +#include +#include +#include + +namespace { + +using FreeType::GuestMemoryRec; + +struct Recorder { + GuestMemoryRec memory{}; + int allocs = 0; + int frees = 0; + int reallocs = 0; + std::int64_t lastSize = 0; + std::int64_t lastCurrent = 0; + std::int64_t lastNew = 0; + void* lastBlock = nullptr; + GuestMemoryRec* lastMemory = nullptr; + bool fail = false; +}; + +Recorder* Owner(GuestMemoryRec* memory) { + return static_cast(memory->user); +} + +void* APS5_VABI RecordAlloc(GuestMemoryRec* memory, std::int64_t size) { + auto* recorder = Owner(memory); + ++recorder->allocs; + recorder->lastMemory = memory; + recorder->lastSize = size; + if (recorder->fail) return nullptr; + void* block = std::malloc(static_cast(size)); + std::memset(block, 0xAA, static_cast(size)); + return block; +} + +void APS5_VABI RecordFree(GuestMemoryRec* memory, void* block) { + auto* recorder = Owner(memory); + ++recorder->frees; + recorder->lastMemory = memory; + recorder->lastBlock = block; + std::free(block); +} + +void* APS5_VABI RecordRealloc(GuestMemoryRec* memory, std::int64_t current, std::int64_t size, void* block) { + auto* recorder = Owner(memory); + ++recorder->reallocs; + recorder->lastMemory = memory; + recorder->lastCurrent = current; + recorder->lastNew = size; + recorder->lastBlock = block; + if (recorder->fail) return nullptr; + auto* resized = static_cast(std::realloc(block, static_cast(size))); + if (size > current) std::memset(resized + current, 0xAA, static_cast(size - current)); + return resized; +} + +void Reset(Recorder& recorder) { + recorder = Recorder{}; + recorder.memory = {&recorder, RecordAlloc, RecordFree, RecordRealloc}; +} + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "FreeType Ot memory: %s\n", message); + std::exit(1); +} + +bool Filled(const void* block, std::size_t size, unsigned char value) { + const auto* bytes = static_cast(block); + for (std::size_t i = 0; i < size; ++i) { + if (bytes[i] != value) return false; + } + return true; +} + +void QuickAllocation() { + Recorder recorder; + Reset(recorder); + std::int32_t error = -1; + void* block = ft_mem_qalloc_nid_postfix(&recorder.memory, 24, &error); + Require(block && error == 0, "qalloc of a positive size returns a block and no error"); + Require(recorder.allocs == 1 && recorder.lastSize == 24 && recorder.lastMemory == &recorder.memory, "qalloc passes the memory record and size to alloc"); + Require(Filled(block, 24, 0xAA), "qalloc leaves the block uncleared"); + ft_mem_free_nid_postfix(&recorder.memory, block); + Require(recorder.frees == 1 && recorder.lastBlock == block && recorder.lastMemory == &recorder.memory, "free passes the block to memory->free"); + + Reset(recorder); + error = -1; + Require(!ft_mem_qalloc_nid_postfix(&recorder.memory, 0, &error) && error == 0 && recorder.allocs == 0, "qalloc of size 0 returns null without calling alloc"); + error = -1; + Require(!ft_mem_qalloc_nid_postfix(&recorder.memory, -1, &error) && error == 0x06 && recorder.allocs == 0, "qalloc of a negative size is Invalid_Argument"); + recorder.fail = true; + error = -1; + Require(!ft_mem_qalloc_nid_postfix(&recorder.memory, 8, &error) && error == 0x40 && recorder.allocs == 1, "qalloc reports Out_Of_Memory when alloc fails"); +} + +void ClearedAllocation() { + Recorder recorder; + Reset(recorder); + std::int32_t error = -1; + void* block = ft_mem_alloc_nid_postfix(&recorder.memory, 16, &error); + Require(block && error == 0 && recorder.allocs == 1 && recorder.lastSize == 16, "alloc allocates through memory->alloc"); + Require(Filled(block, 16, 0), "alloc clears the block"); + ft_mem_free_nid_postfix(&recorder.memory, block); + + error = -1; + Require(!ft_mem_alloc_nid_postfix(&recorder.memory, 0, &error) && error == 0, "alloc of size 0 returns null"); + error = -1; + Require(!ft_mem_alloc_nid_postfix(&recorder.memory, -5, &error) && error == 0x06, "alloc of a negative size is Invalid_Argument"); + recorder.fail = true; + error = -1; + Require(!ft_mem_alloc_nid_postfix(&recorder.memory, 4, &error) && error == 0x40, "alloc reports Out_Of_Memory when alloc fails"); + + Reset(recorder); + ft_mem_free_nid_postfix(&recorder.memory, nullptr); + Require(recorder.frees == 0, "free of null does not call memory->free"); +} + +void QuickReallocation() { + Recorder recorder; + Reset(recorder); + std::int32_t error = -1; + void* block = ft_mem_qrealloc_nid_postfix(&recorder.memory, 4, 0, 3, nullptr, &error); + Require(block && error == 0 && recorder.allocs == 1 && recorder.lastSize == 12 && recorder.reallocs == 0, "qrealloc from zero items allocates new_count * item_size"); + Require(Filled(block, 12, 0), "qrealloc from zero items clears the new block, as ft_mem_alloc does"); + std::memset(block, 0x11, 12); + + error = -1; + void* grown = ft_mem_qrealloc_nid_postfix(&recorder.memory, 4, 3, 5, block, &error); + Require(grown && error == 0 && recorder.reallocs == 1, "qrealloc of a live block calls memory->realloc"); + Require(recorder.lastCurrent == 12 && recorder.lastNew == 20 && recorder.lastBlock == block, "qrealloc passes byte sizes and the old block"); + Require(Filled(grown, 12, 0x11) && Filled(static_cast(grown) + 12, 8, 0xAA), "qrealloc does not clear the grown tail"); + + recorder.fail = true; + error = -1; + Require(ft_mem_qrealloc_nid_postfix(&recorder.memory, 4, 5, 9, grown, &error) == grown && error == 0x40, "qrealloc keeps the old block when realloc fails"); + recorder.fail = false; + + const int callbacks = recorder.allocs + recorder.frees + recorder.reallocs; + error = -1; + Require(ft_mem_qrealloc_nid_postfix(&recorder.memory, -1, 5, 6, grown, &error) == grown && error == 0x06, "negative item_size is Invalid_Argument"); + error = -1; + Require(ft_mem_qrealloc_nid_postfix(&recorder.memory, 4, -1, 6, grown, &error) == grown && error == 0x06, "negative cur_count is Invalid_Argument"); + error = -1; + Require(ft_mem_qrealloc_nid_postfix(&recorder.memory, 4, 5, -1, grown, &error) == grown && error == 0x06, "negative new_count is Invalid_Argument"); + error = -1; + Require(ft_mem_qrealloc_nid_postfix(&recorder.memory, 2, 5, 0x40000000, grown, &error) == grown && error == 0x0A, "new_count above FT_INT_MAX / item_size is Array_Too_Large"); + Require(recorder.allocs + recorder.frees + recorder.reallocs == callbacks, "rejected qrealloc calls make no memory callback"); + + recorder.fail = true; + error = -1; + Require(!ft_mem_qrealloc_nid_postfix(&recorder.memory, 2, 0, 0x3FFFFFFF, nullptr, &error) && error == 0x40, "new_count at FT_INT_MAX / item_size is allocated"); + Require(recorder.lastSize == 0x7FFFFFFE, "the boundary allocation asks for new_count * item_size bytes"); + recorder.fail = false; + + error = -1; + Require(!ft_mem_qrealloc_nid_postfix(&recorder.memory, 4, 5, 0, grown, &error) && error == 0, "qrealloc to zero items frees and returns null"); + Require(recorder.lastBlock == grown && recorder.frees == 1, "qrealloc to zero items passes the block to memory->free"); + + block = ft_mem_qalloc_nid_postfix(&recorder.memory, 8, &error); + error = -1; + Require(!ft_mem_qrealloc_nid_postfix(&recorder.memory, 0, 2, 4, block, &error) && error == 0, "qrealloc with item_size 0 frees and returns null"); + Require(recorder.lastBlock == block && recorder.frees == 2, "qrealloc with item_size 0 passes the block to memory->free"); +} + +void ClearedReallocation() { + Recorder recorder; + Reset(recorder); + std::int32_t error = -1; + void* block = ft_mem_realloc_nid_postfix(&recorder.memory, 4, 0, 3, nullptr, &error); + Require(block && error == 0 && recorder.lastSize == 12 && Filled(block, 12, 0), "realloc from zero items allocates and clears"); + auto* bytes = static_cast(block); + for (int i = 0; i < 12; ++i) bytes[i] = static_cast(i + 1); + + error = -1; + void* grown = ft_mem_realloc_nid_postfix(&recorder.memory, 4, 3, 5, block, &error); + Require(grown && error == 0 && recorder.reallocs == 1 && recorder.lastCurrent == 12 && recorder.lastNew == 20, "realloc resizes through memory->realloc"); + bytes = static_cast(grown); + bool kept = true; + for (int i = 0; i < 12; ++i) kept = kept && bytes[i] == i + 1; + Require(kept && Filled(bytes + 12, 8, 0), "realloc keeps the old items and clears the new ones"); + + std::memset(bytes, 0x22, 20); + error = -1; + void* shrunk = ft_mem_realloc_nid_postfix(&recorder.memory, 4, 5, 2, grown, &error); + Require(shrunk && error == 0 && recorder.lastCurrent == 20 && recorder.lastNew == 8 && Filled(shrunk, 8, 0x22), "realloc shrinks without clearing"); + + error = -1; + Require(ft_mem_realloc_nid_postfix(&recorder.memory, 4, 2, -3, shrunk, &error) == shrunk && error == 0x06 && Filled(shrunk, 8, 0x22), "realloc errors keep the block unchanged"); + recorder.fail = true; + error = -1; + Require(ft_mem_realloc_nid_postfix(&recorder.memory, 4, 2, 6, shrunk, &error) == shrunk && error == 0x40 && Filled(shrunk, 8, 0x22), "realloc keeps the block when realloc fails"); + recorder.fail = false; + ft_mem_free_nid_postfix(&recorder.memory, shrunk); +} + +void Duplication() { + Recorder recorder; + Reset(recorder); + const char source[7] = {'a', 'b', 'c', 0, 'd', 'e', 'f'}; + std::int32_t error = -1; + void* copy = ft_mem_dup_nid_postfix(&recorder.memory, source, sizeof(source), &error); + Require(copy && error == 0 && recorder.allocs == 1 && recorder.lastSize == 7, "dup allocates size bytes"); + Require(std::memcmp(copy, source, sizeof(source)) == 0, "dup copies every byte, including embedded zeros"); + ft_mem_free_nid_postfix(&recorder.memory, copy); + + error = -1; + Require(!ft_mem_dup_nid_postfix(&recorder.memory, source, 0, &error) && error == 0 && recorder.allocs == 1, "dup of size 0 returns null"); + error = -1; + void* uncopied = ft_mem_dup_nid_postfix(&recorder.memory, nullptr, 4, &error); + Require(uncopied && error == 0 && recorder.allocs == 2 && Filled(uncopied, 4, 0xAA), "dup of a null address allocates without copying"); + ft_mem_free_nid_postfix(&recorder.memory, uncopied); + error = -1; + Require(!ft_mem_dup_nid_postfix(&recorder.memory, source, 0x8000000000000000ull, &error) && error == 0x06 && recorder.allocs == 2, "dup of a size above FT_Long is Invalid_Argument"); + + error = -1; + char* text = static_cast(ft_mem_strdup_nid_postfix(&recorder.memory, "hello", &error)); + Require(text && error == 0 && recorder.lastSize == 6 && std::strcmp(text, "hello") == 0, "strdup copies strlen + 1 bytes"); + ft_mem_free_nid_postfix(&recorder.memory, text); + const int allocs = recorder.allocs; + error = -1; + Require(!ft_mem_strdup_nid_postfix(&recorder.memory, nullptr, &error) && error == 0 && recorder.allocs == allocs, "strdup of null returns null without allocating"); + recorder.fail = true; + error = -1; + Require(!ft_mem_strdup_nid_postfix(&recorder.memory, "x", &error) && error == 0x40, "strdup reports Out_Of_Memory when alloc fails"); +} + +void BoundedCopy() { + char buffer[8]; + std::memset(buffer, 'x', sizeof(buffer)); + Require(ft_mem_strcpyn_nid_postfix(buffer, "abc", sizeof(buffer)) == 0 && std::strcmp(buffer, "abc") == 0, "strcpyn copies a string that fits and returns 0"); + std::memset(buffer, 'x', sizeof(buffer)); + Require(ft_mem_strcpyn_nid_postfix(buffer, "abc", 4) == 0 && std::strcmp(buffer, "abc") == 0, "strcpyn fits size - 1 characters"); + std::memset(buffer, 'x', sizeof(buffer)); + Require(ft_mem_strcpyn_nid_postfix(buffer, "abcdef", 4) == 1 && std::strcmp(buffer, "abc") == 0 && buffer[4] == 'x', "strcpyn truncates to size - 1 characters and returns 1"); + std::memset(buffer, 'x', sizeof(buffer)); + Require(ft_mem_strcpyn_nid_postfix(buffer, "a", 1) == 1 && buffer[0] == 0 && buffer[1] == 'x', "strcpyn of size 1 writes only the terminator"); + std::memset(buffer, 'x', sizeof(buffer)); + Require(ft_mem_strcpyn_nid_postfix(buffer, "a", 0) == 1 && buffer[0] == 0 && buffer[1] == 'x', "strcpyn of size 0 still writes the terminator"); + std::memset(buffer, 'x', sizeof(buffer)); + Require(ft_mem_strcpyn_nid_postfix(buffer, "", 5) == 0 && buffer[0] == 0 && buffer[1] == 'x', "strcpyn of an empty string returns 0"); +} + +} + +int main() { + QuickAllocation(); + ClearedAllocation(); + QuickReallocation(); + ClearedReallocation(); + Duplication(); + BoundedCopy(); + std::puts("Guest FreeType Ot memory tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestHttp2Transport.cpp b/core/libs/tests/GuestHttp2Transport.cpp index 1c25112829..4aab31e278 100644 --- a/core/libs/tests/GuestHttp2Transport.cpp +++ b/core/libs/tests/GuestHttp2Transport.cpp @@ -52,9 +52,41 @@ bool Throws(TCall call) { return false; } -enum class Mode { Normal, Large, NoLength, Head, NotFound, Redirect, Auth, Reset, Truncate, StallHandshake, StallHeaders, StallBody, NoAlpn, Informational, StallUpload, EarlyFinal, EarlyReset, EarlyEmpty, EarlyLarge, EarlyResetIncomplete }; +enum class Mode { Normal, Large, NoLength, Head, NotFound, Redirect, Auth, Reset, Truncate, Gzip, GzipSplit, GzipTruncated, GzipChecksum, GzipConcat, GzipLarge, StallHandshake, StallHeaders, StallBody, NoAlpn, Informational, StallUpload, EarlyFinal, EarlyReset, EarlyEmpty, EarlyLarge, EarlyResetIncomplete }; const char* Protocols[] = {"h2", nullptr}; +std::string GzipPayload() { + std::string payload(70000, '\0'); + for (std::size_t i = 0; i < payload.size(); ++i) payload[i] = static_cast((i * 31 + 7) & 0xff); + return payload; +} + +std::string StoredGzip(const std::string& payload) { + static const unsigned char header[] = {0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff}; + std::string body(reinterpret_cast(header), sizeof(header)); + std::size_t offset = 0; + while (offset < payload.size()) { + const std::size_t count = std::min(payload.size() - offset, 65535); + body.push_back(static_cast(offset + count == payload.size() ? 1 : 0)); + body.push_back(static_cast(count & 0xff)); + body.push_back(static_cast((count >> 8) & 0xff)); + body.push_back(static_cast(~count & 0xff)); + body.push_back(static_cast((~count >> 8) & 0xff)); + body.append(payload, offset, count); + offset += count; + } + std::uint32_t crc = 0xffffffffu; + for (const unsigned char byte : payload) { + crc ^= byte; + for (int bit = 0; bit < 8; ++bit) crc = (crc >> 1) ^ (0xedb88320u & (0u - (crc & 1u))); + } + crc = ~crc; + const std::uint32_t size = static_cast(payload.size()); + for (int shift = 0; shift < 32; shift += 8) body.push_back(static_cast((crc >> shift) & 0xff)); + for (int shift = 0; shift < 32; shift += 8) body.push_back(static_cast((size >> shift) & 0xff)); + return body; +} + class Server { public: explicit Server(Mode mode) : mode(mode) { @@ -137,6 +169,8 @@ private: std::size_t offset = 0; bool responded = false; bool ended = false; + bool splitSent = false; + bool splitResumed = false; void Stage(int value) { { std::lock_guard lock(mutex); stage = value; } @@ -176,7 +210,12 @@ private: std::uint32_t* flags, nghttp2_data_source*, void* data) { auto& server = *static_cast(data); if (server.mode == Mode::StallBody || server.mode == Mode::EarlyResetIncomplete) return NGHTTP2_ERR_DEFERRED; + if (server.mode == Mode::GzipSplit && server.splitSent && !server.splitResumed) return NGHTTP2_ERR_DEFERRED; size = std::min(size, server.body.size() - server.offset); + if (server.mode == Mode::GzipSplit && !server.splitSent) { + size = std::min(size, 8); + server.splitSent = true; + } if (size) std::memcpy(bytes, server.body.data() + server.offset, size); server.offset += size; if (server.offset == server.body.size()) *flags |= NGHTTP2_DATA_FLAG_EOF; @@ -194,6 +233,21 @@ private: auto length = std::to_string(body.size()); if (mode == Mode::Head) body.clear(); if (mode == Mode::Truncate) body = "short"; + if (mode == Mode::Gzip || mode == Mode::GzipSplit || mode == Mode::GzipTruncated || mode == Mode::GzipChecksum || mode == Mode::GzipConcat) { + static const unsigned char compressed[] = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x2b, 0x4a, 0x2d, 0x2e, 0xc8, 0xcf, + 0x2b, 0x4e, 0x55, 0x48, 0xca, 0x4f, 0xa9, 0x04, 0x00, 0x36, 0x76, 0x8b, 0x0c, 0x0d, 0x00, 0x00, 0x00, + }; + body.assign(reinterpret_cast(compressed), sizeof(compressed)); + if (mode == Mode::GzipTruncated) body.resize(body.size() - 4); + if (mode == Mode::GzipChecksum) body[body.size() - 8] ^= 0xff; + if (mode == Mode::GzipConcat) { const std::string member = body; body += member; } + length = std::to_string(body.size()); + } + if (mode == Mode::GzipLarge) { + body = StoredGzip(GzipPayload()); + length = std::to_string(body.size()); + } if (mode == Mode::Informational) { nghttp2_nv early{reinterpret_cast(const_cast(":status")), reinterpret_cast(const_cast("103")), 7, 3, 0}; Require(nghttp2_submit_headers(session, 0, stream, nullptr, &early, 1, nullptr) == 0, "server informational response"); @@ -201,6 +255,7 @@ private: std::vector> fields{{":status", status}, {"x-loopback", "yes"}}; if (mode != Mode::NoLength) fields.emplace_back("content-length", length); if (mode == Mode::Redirect) fields.emplace_back("location", "/another"); + if (mode == Mode::Gzip || mode == Mode::GzipSplit || mode == Mode::GzipTruncated || mode == Mode::GzipChecksum || mode == Mode::GzipConcat || mode == Mode::GzipLarge) fields.emplace_back("content-encoding", "gzip"); std::vector response; for (auto& [name, value] : fields) response.push_back({reinterpret_cast(name.data()), reinterpret_cast(value.data()), name.size(), value.size(), 0}); nghttp2_data_provider2 provider{}; @@ -259,6 +314,12 @@ private: } if (!sent) break; } + if (mode == Mode::GzipSplit && splitSent && !splitResumed) { + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(nghttp2_session_resume_data(session, 1) == 0, "resume split gzip body"); + splitResumed = true; + continue; + } if (IsEarly() && responded && sent) { Stage(2); if (!resetSent && (mode == Mode::EarlyReset || mode == Mode::EarlyResetIncomplete)) { @@ -355,6 +416,94 @@ void Success(int ssl, Mode mode, bool disableCa = false) { Require(Throws([&] { sceHttp2SendRequest(client.request, nullptr, 0); }), "unsupported reuse throws"); } +void Gzip(int ssl) { + std::fprintf(stderr, "HTTP2 gzip scenario\n"); + for (const int enabled : {1, 0}) { + Server server(Mode::Gzip); + Client client(ssl); + Require(sceHttp2SetInflateGZIPEnabled(client.tmpl, enabled) == 0, "gzip option"); + client.Create(server.Url()); + Require(sceHttp2SendRequest(client.request, nullptr, 0) == 0, "gzip send"); + int kind = -1; + std::uint64_t length = 0; + Require(sceHttp2GetResponseContentLength(client.request, &kind, &length) == 0, "gzip length"); + std::array buffer{}; + const int count = sceHttp2ReadData(client.request, buffer.data(), buffer.size()); + if (enabled) { + Require(kind == 0 && length == 13 && count == 13 && std::string(buffer.data(), 13) == "response body", "gzip inflated body"); + } else { + Require(kind == 0 && length == 33 && count == 33 && static_cast(buffer[0]) == 0x1f && + static_cast(buffer[1]) == 0x8b, "gzip passthrough body"); + } + server.Join(); + } +} + +void GzipSplit(int ssl) { + std::fprintf(stderr, "HTTP2 gzip split scenario\n"); + Server server(Mode::GzipSplit); + Client client(ssl); + Require(sceHttp2SetInflateGZIPEnabled(client.tmpl, 1) == 0, "gzip split option"); + client.Create(server.Url()); + Require(sceHttp2SendRequest(client.request, nullptr, 0) == 0, "gzip split send"); + std::string body; + std::array buffer; + while (body.size() < 12) { + const int count = sceHttp2ReadData(client.request, buffer.data(), buffer.size()); + Require(count > 0 && count <= static_cast(buffer.size()), "gzip split read count"); + body.append(buffer.data(), static_cast(count)); + } + int kind = -1; + std::uint64_t length = 0; + Require(sceHttp2GetResponseContentLength(client.request, &kind, &length) == 0, "gzip split length"); + Require(kind == 0 && length == 13, "gzip split inflated length"); + for (;;) { + const int count = sceHttp2ReadData(client.request, buffer.data(), buffer.size()); + Require(count >= 0 && count <= static_cast(buffer.size()), "gzip split read count"); + if (!count) break; + body.append(buffer.data(), static_cast(count)); + } + Require(body == "response body", "gzip split inflated body"); + server.Join(); +} + +void GzipLarge(int ssl) { + std::fprintf(stderr, "HTTP2 gzip large scenario\n"); + Server server(Mode::GzipLarge); + Client client(ssl); + Require(sceHttp2SetInflateGZIPEnabled(client.tmpl, 1) == 0, "gzip large option"); + client.Create(server.Url()); + Require(sceHttp2SendRequest(client.request, nullptr, 0) == 0, "gzip large send"); + int kind = -1; + std::uint64_t length = 0; + Require(sceHttp2GetResponseContentLength(client.request, &kind, &length) == 0, "gzip large length"); + Require(kind == 0 && length == 70000, "gzip large inflated length"); + std::string body; + std::array buffer; + for (;;) { + const int count = sceHttp2ReadData(client.request, buffer.data(), buffer.size()); + Require(count >= 0 && count <= static_cast(buffer.size()), "gzip large read count"); + if (!count) break; + body.append(buffer.data(), static_cast(count)); + } + Require(body == GzipPayload(), "gzip large inflated body"); + server.Join(); +} + +void GzipFailure(int ssl, Mode mode, const char* message) { + std::fprintf(stderr, "HTTP2 gzip failure scenario %d\n", static_cast(mode)); + Server server(mode); + Client client(ssl); + Require(sceHttp2SetInflateGZIPEnabled(client.tmpl, 1) == 0, "gzip failure option"); + client.Create(server.Url()); + Require(Throws([&] { + sceHttp2SendRequest(client.request, nullptr, 0); + std::array buffer; + sceHttp2ReadData(client.request, buffer.data(), buffer.size()); + }), message); + server.Join(); +} + void Failure(int ssl, Mode mode, bool verifyName = false) { std::fprintf(stderr, "HTTP2 failure scenario %d\n", static_cast(mode)); Server server(mode); @@ -586,6 +735,12 @@ int main(int argc, char** argv) { } else { Require(argc == 1, "test arguments"); for (const auto mode : {Mode::Normal, Mode::Large, Mode::NoLength, Mode::Head, Mode::NotFound, Mode::Redirect, Mode::Auth, Mode::Informational}) Success(ssl, mode); + Gzip(ssl); + GzipSplit(ssl); + GzipLarge(ssl); + GzipFailure(ssl, Mode::GzipTruncated, "truncated gzip throws"); + GzipFailure(ssl, Mode::GzipChecksum, "gzip checksum mismatch throws"); + GzipFailure(ssl, Mode::GzipConcat, "concatenated gzip members throw"); for (const auto mode : {Mode::NoAlpn, Mode::Reset, Mode::Truncate, Mode::Redirect, Mode::Auth}) Failure(ssl, mode); Failure(ssl, Mode::Normal, true); for (const auto mode : {Mode::StallHandshake, Mode::StallHeaders, Mode::StallBody, Mode::StallUpload}) { diff --git a/core/libs/tests/GuestMemccpyBounds.cpp b/core/libs/tests/GuestMemccpyBounds.cpp new file mode 100644 index 0000000000..cfb89a8be1 --- /dev/null +++ b/core/libs/tests/GuestMemccpyBounds.cpp @@ -0,0 +1,85 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include +#endif + +static void Check(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "memccpy check failed at line %d\n", line); + std::abort(); + } +} +#define Require(condition) Check((condition), __LINE__) + +class GuardedPage { +public: + std::size_t size; + unsigned char* data; + + GuardedPage() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + size = info.dwPageSize; + data = static_cast(VirtualAlloc(nullptr, size * 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(data != nullptr); + DWORD previous = 0; + Require(VirtualProtect(data + size, size, PAGE_NOACCESS, &previous) != 0); +#else + const long pageSize = sysconf(_SC_PAGESIZE); + Require(pageSize > 0); + size = static_cast(pageSize); + data = static_cast(mmap(nullptr, size * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(data != MAP_FAILED); + Require(mprotect(data + size, size, PROT_NONE) == 0); +#endif + } + + ~GuardedPage() { +#ifdef _WIN32 + VirtualFree(data, 0, MEM_RELEASE); +#else + munmap(data, size * 2); +#endif + } +}; + +int main() { + GuardedPage source; + GuardedPage destination; + for (std::size_t count = 0; count <= 256; ++count) { + for (const std::size_t stop : {count, count / 2, std::size_t{0}}) { + std::memset(source.data, 0x35, source.size); + std::memset(destination.data, 0xA7, destination.size); + auto* input = source.data + source.size - std::max(count, std::size_t{1}); + auto* output = destination.data + destination.size - std::max(count, std::size_t{1}); + if (stop < count) input[stop] = 0xFF; + const std::vector before(source.data, source.data + source.size); + std::vector expected(destination.data, destination.data + destination.size); + const std::size_t copied = stop < count ? stop + 1 : count; + std::copy_n(input, copied, expected.data() + (output - destination.data)); + void* result = memccpy_nid_postfix(output, input, 0x1FF, count); + Require(result == (stop < count ? output + copied : nullptr)); + Require(std::memcmp(destination.data, expected.data(), destination.size) == 0); + Require(std::memcmp(source.data, before.data(), source.size) == 0); + } + } + for (const int character : {-1, 0xFF, 0x1FF}) { + source.data[source.size - 1] = 0xFF; + destination.data[destination.size - 1] = 0; + Require(memccpy_nid_postfix(destination.data + destination.size - 1, source.data + source.size - 1, + character, 256) == destination.data + destination.size); + Require(destination.data[destination.size - 1] == 0xFF); + } + std::puts("memccpy return values, stop bytes, guards and unsigned conversion passed"); +} diff --git a/core/libs/tests/GuestNpSessionSignaling.cpp b/core/libs/tests/GuestNpSessionSignaling.cpp index d69593f7e5..cd5d5c0943 100644 --- a/core/libs/tests/GuestNpSessionSignaling.cpp +++ b/core/libs/tests/GuestNpSessionSignaling.cpp @@ -160,4 +160,5 @@ int main() { Require(sceNpSessionSignalingInitialize(reinterpret_cast(initializeParam)) == 0, "reinitialize"); Require(sceNpSessionSignalingRequestPrepare(contextId, &requestId) == ContextNotFound, "reinitialize retained a context"); Require(sceNpSessionSignalingTerminate() == 0, "final terminate"); + Require(sceNpSessionSignalingCreateContext(reinterpret_cast(1), &contextId) == NotInitialized, "create context post-terminate"); } diff --git a/core/libs/tests/GuestVdecsw.cpp b/core/libs/tests/GuestVdecsw.cpp index 0990141df2..9b2bc63990 100644 --- a/core/libs/tests/GuestVdecsw.cpp +++ b/core/libs/tests/GuestVdecsw.cpp @@ -102,6 +102,12 @@ void checkGenericPictureInfo(const OutputInfo& output, const TInfo& typed) { shorter.thisSize = sizeof(OutputInfo) - 8; auto actual = typed; check(sceVdecswGetPictureInfo(&shorter, &actual, nullptr) == 0 && std::memcmp(&actual, &typed, sizeof(actual)) == 0, "generic picture info rejected the shorter output descriptor"); + auto doubled = output; + doubled.pictureCount = 2; + auto doubledFirst = typed; + const auto doubledBefore = doubledFirst; + auto doubledSecond = typed; + check(throws([&] { sceVdecswGetPictureInfo(&doubled, &doubledFirst, &doubledSecond); }) && std::memcmp(&doubledFirst, &doubledBefore, sizeof(doubledFirst)) == 0 && std::memcmp(&doubledSecond, &doubledBefore, sizeof(doubledSecond)) == 0, "generic picture info accepted two pictures"); } void testGenericPictureInfoValidation() { diff --git a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp index 951a99b18e..648b8cbd79 100644 --- a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp +++ b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp @@ -20,6 +20,7 @@ inline constexpr std::uint8_t PrefixAddressSize = 0x67; inline constexpr std::uint8_t RexMin = 0x40; inline constexpr std::uint8_t RexMax = 0x4F; inline constexpr std::uint8_t RexWBit = 0x08; +inline constexpr std::uint8_t RexBBit = 0x01; inline constexpr std::uint8_t TwoByteOpcodeEscape = 0x0F; @@ -108,6 +109,8 @@ inline constexpr std::uint8_t OneByteLoopMax = 0xE2; inline constexpr std::uint8_t OneByteInOutImm8Min = 0xE4; inline constexpr std::uint8_t OneByteInOutImm8Max = 0xE7; inline constexpr std::uint8_t OneByteInt = 0xCD; +inline constexpr std::uint8_t OneByteInt3 = 0xCC; +inline constexpr std::uint8_t OneByteNop = 0x90; inline constexpr std::uint8_t OneByteRetImm16 = 0xC2; inline constexpr std::uint8_t OneByteRetFarImm16 = 0xCA; inline constexpr std::uint8_t OneByteEnter = 0xC8; diff --git a/core/relinker/codegen/src/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index d8d1be1731..f6817ce47e 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -112,6 +112,41 @@ private: return hit != _branchTargets.end() && *hit < end; } + [[nodiscard]] bool _int3(const std::size_t index) const { + const auto bytes = _bytes(index); + return bytes.size() == 1 && bytes[0] == X64OpcodeConstants::OneByteInt3; + } + + [[nodiscard]] bool _padding(const std::size_t index) const { + const auto info = _info(index); + if (info.IsTwoByteOpcode) + return info.Opcode == X64OpcodeConstants::TwoByteNopModRm; + if (info.Opcode != X64OpcodeConstants::OneByteNop) + return _int3(index); + const auto prefixes = _bytes(index).first(info.OpcodeOffset); + return (info.RexPrefix & X64OpcodeConstants::RexBBit) == 0 && std::find(prefixes.begin(), prefixes.end(), X64OpcodeConstants::PrefixRep) == prefixes.end(); + } + + [[nodiscard]] bool _mayBeEntered(const std::size_t index) const { + for (auto previous = index; previous > 0; --previous) { + if (_end(previous - 1) != _address(previous) || _blocksFollowing(previous - 1)) + return true; + if (!_padding(previous - 1)) + return false; + } + return true; + } + + [[nodiscard]] bool _enteredWithin(const std::size_t first, const std::size_t last) const { + if (_entered(_address(first), _end(last))) + return true; + for (auto index = first + 1; index <= last; ++index) { + if (!_int3(index) && _mayBeEntered(index)) + return true; + } + return false; + } + [[nodiscard]] bool _blocksFollowing(const std::size_t index) const { const auto kind = _info(index).FlowKind; return kind != ControlFlowKind::Sequential && kind != ControlFlowKind::ConditionalBranch; @@ -135,7 +170,7 @@ private: } [[nodiscard]] std::optional _relocate(const std::size_t index, const std::size_t first, const std::size_t last, const bool allowInPlace = false) const { - if (_entered(_address(first), _end(last))) + if (_enteredWithin(first, last)) return std::nullopt; StubBodyBuilder body; Placement placement; @@ -162,7 +197,7 @@ private: using Amd64OnlySubstitutionTable::kJmpRel32; std::size_t head = 0; for (auto last = start; last < _matches.size(); ++last) { - if (!_movable(last, true) || _entered(_address(start), _end(last))) + if (!_movable(last, true) || _enteredWithin(start, last)) return std::nullopt; if (head == 0 && _end(last) - _address(start) >= kJmpRel32.Size) head = static_cast(_end(last) - _address(start)); diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index d5823779fc..451340b35f 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -1058,6 +1058,39 @@ void converterWideStoreWindows() { requireFailure([&] { (void)convert(withStore(far, tail)); }, "A 256-bit store whose movable neighbours are all beyond a short jump was kept"); } +void converterIslandIndirectEntries() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes prologue = {0x55, 0x48, 0x89, 0xE5, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55, 0x41, 0x54, 0x53, 0xC3}; + for (const auto& padding : {Bytes{0xCC, 0xCC}, Bytes{0x66, 0x90}, Bytes{0x0F, 0x1F, 0x00}, Bytes{}}) { + Bytes text = {0xE8, 0x40, 0x00, 0x00, 0x00, 0xC5, 0xFC, 0x11, 0x07, 0xC3, 0xEB, 0xFD}; + text.insert(text.end(), padding.begin(), padding.end()); + const auto entry = static_cast(0x200 + text.size()); + text.insert(text.end(), prologue.begin(), prologue.end()); + Bytes file(0x300 + text.size(), 0xCC); + std::copy(text.begin(), text.end(), file.begin() + 0x200); + const auto result = converter->Convert(file, {segmentHeader(text.size())}); + require(result.Trampolines.size() == 2 && result.KeptCount == 0, "A 256-bit store before a function that only a pointer reaches was not lowered through an island"); + for (const auto& site : result.Trampolines) + require(site.Offset >= entry || site.Offset + site.Length <= entry, "An island covered the entry of a function that follows a jump and its padding"); + require(result.Trampolines[0].Offset == entry, "An island next to a function that follows a jump and its padding did not start at its entry"); + } +} + +void converterIslandInTrapPadding() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + Bytes text = {0xE8, 0x40, 0x00, 0x00, 0x00, 0xC5, 0xFC, 0x11, 0x07, 0xC3, 0xEB, 0xFD}; + const Bytes padding(15, 0xCC); + text.insert(text.end(), padding.begin(), padding.end()); + const auto entry = static_cast(0x200 + text.size()); + text.insert(text.end(), {0xE9, 0x00, 0x01, 0x00, 0x00}); + Bytes file(0x300 + text.size(), 0xCC); + std::copy(text.begin(), text.end(), file.begin() + 0x200); + const auto result = converter->Convert(file, {segmentHeader(text.size())}); + require(result.Trampolines.size() == 2 && result.KeptCount == 0, "A 256-bit store before int3 padding and a function that cannot move was not lowered through an island"); + for (const auto& site : result.Trampolines) + require(site.Offset + site.Length <= entry, "An island in the int3 padding after a jump ran into the next function"); +} + void converterClaimedFollower() { const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); for (const auto& [shortInstruction, name] : {std::pair{Bytes{0x0F, 0x53, 0xCA}, "RCPPS"}, std::pair{Bytes{0xF3, 0x0F, 0xC7, 0xF8}, "RDPID"}}) { @@ -2413,6 +2446,8 @@ int main() { wideStoreSplit(); converterWideStoreWindows(); converterWideStoreIslandAcrossInt64Max(); + converterIslandIndirectEntries(); + converterIslandInTrapPadding(); converterClaimedFollower(); converterJumpTableTargets(); converterReciprocal(); diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index 19da8b5235..419d0ab69a 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -482,6 +482,8 @@ bool canReachBefore(const ControlFlowGraph& graph, std::uint32_t start, std::uin } bool reachesWithinIteration(const ControlFlowGraph& graph, const NaturalLoop& loop, std::uint32_t start, std::uint32_t target) { + if (start == target) return true; + if (start == loop.continueBlock || start == loop.mergeBlock) return false; std::vector pending = {start}; std::vector visited(graph.blocks.size(), false); while (!pending.empty()) { diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp index f724becd4b..bb66d00d95 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp @@ -32,12 +32,14 @@ public: IrValue& CreateValue(IrOpcode opcode, IrType type, std::uint64_t flags); void SetEntryBlock(IrBlock& block); std::size_t ReclaimUnusedValues(); + void ReleaseUseArena(); [[nodiscard]] std::vector& BlockOrder(); [[nodiscard]] const std::vector& BlockOrder() const; private: std::shared_ptr instructionNodes; std::vector> blocks; + IrUseArena useArena; std::vector blockOrder; std::vector> values; IrResourcePlan resourcePlan; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp index d37a3156af..699436a0e3 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -16,6 +18,51 @@ namespace ShaderRecompiler { class IrBlock; class IrValue; +struct IrUse; + +template +class IrUseAllocator { +public: + using value_type = T; + using propagate_on_container_move_assignment = std::true_type; + using propagate_on_container_swap = std::true_type; + IrUseAllocator() noexcept = default; + explicit IrUseAllocator(std::pmr::memory_resource* arena) noexcept : arena(arena) {} + template + IrUseAllocator(const IrUseAllocator& other) noexcept : arena(other.Arena()) {} + T* allocate(std::size_t count) { + if (arena == nullptr) return std::allocator{}.allocate(count); + return static_cast(arena->allocate(count * sizeof(T), alignof(T))); + } + void deallocate(T* pointer, std::size_t count) noexcept { + if (arena == nullptr) std::allocator{}.deallocate(pointer, count); + } + [[nodiscard]] std::pmr::memory_resource* Arena() const noexcept { return arena; } + IrUseAllocator select_on_container_copy_construction() const noexcept { return {}; } + friend bool operator==(const IrUseAllocator& left, const IrUseAllocator& right) noexcept { return left.arena == right.arena; } + +private: + std::pmr::memory_resource* arena = nullptr; +}; + +using IrUseList = std::vector>; + +class IrUseArena { +public: + IrUseArena() = default; + IrUseArena(IrUseArena&& other) noexcept = default; + IrUseArena& operator=(IrUseArena&& other) noexcept { + resource.swap(other.resource); + return *this; + } + std::pmr::memory_resource* Resource() { + if (resource == nullptr) resource = std::make_unique(); + return resource.get(); + } + +private: + std::unique_ptr resource; +}; struct IrUse { IrValue* user = nullptr; @@ -91,7 +138,7 @@ private: class IrValue { public: - IrValue(IrOpcode opcode, IrType type, std::uint32_t id); + IrValue(IrOpcode opcode, IrType type, std::uint32_t id, std::pmr::memory_resource* useArena = nullptr); IrValue(const IrValue& other) = delete; IrValue& operator=(const IrValue& other) = delete; IrValue(IrValue&& other) = delete; @@ -136,7 +183,8 @@ public: [[nodiscard]] std::size_t PhiBlockCount() const; [[nodiscard]] IrValue* Argument(std::size_t index) const; [[nodiscard]] IrBlock* PhiBlock(std::size_t index) const; - [[nodiscard]] const std::vector& OperandUses() const; + [[nodiscard]] const IrUseList& OperandUses() const; + void MoveUsesToHeap(); [[nodiscard]] std::uint64_t ImmediateU64() const; [[nodiscard]] std::uint16_t ImmediateF16Bits() const; [[nodiscard]] float ImmediateF32() const; @@ -166,7 +214,7 @@ private: GuestRegister registerValue{}; std::uint64_t flags = 0; std::vector phiBlocks; - std::vector operandUses; + IrUseList operandUses; bool hasImmediate; IrOperands arguments; IrBlock* parent; diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp index 4c5e1fc57c..34a251932b 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp @@ -274,6 +274,11 @@ std::size_t IrProgram::ReclaimUnusedValues() { return removed; } +void IrProgram::ReleaseUseArena() { + for (const auto& value : values) value->MoveUsesToHeap(); + useArena = IrUseArena{}; +} + std::vector>& IrProgram::Blocks() { return blocks; } @@ -315,7 +320,7 @@ IrBlock& IrProgram::CreateBlock() { } IrValue& IrProgram::CreateValue(IrOpcode opcode, IrType type) { - auto value = std::make_unique(opcode, type, nextValueId); + auto value = std::make_unique(opcode, type, nextValueId, useArena.Resource()); nextValueId++; IrValue& created = *value; values.push_back(std::move(value)); diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp index 2bbfc39eda..b549aa7393 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp @@ -5,7 +5,7 @@ namespace ShaderRecompiler { -IrValue::IrValue(IrOpcode opcode, IrType type, std::uint32_t id) : opcode(opcode), type(type), id(id), hasImmediate(false), parent(nullptr) { +IrValue::IrValue(IrOpcode opcode, IrType type, std::uint32_t id, std::pmr::memory_resource* useArena) : opcode(opcode), type(type), id(id), hasImmediate(false), parent(nullptr), operandUses(IrUseAllocator(useArena)) { arguments.reserve(IrOpcodeOperandCount(opcode)); } @@ -121,10 +121,15 @@ IrBlock* IrValue::PhiBlock(std::size_t index) const { return phiBlocks[index]; } -const std::vector& IrValue::OperandUses() const { +const IrUseList& IrValue::OperandUses() const { return operandUses; } +void IrValue::MoveUsesToHeap() { + if (operandUses.get_allocator().Arena() == nullptr) return; + operandUses = IrUseList(operandUses.begin(), operandUses.end(), IrUseAllocator{}); +} + std::uint64_t IrValue::ImmediateU64() const { return immediateBits; } diff --git a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp index f5a711b969..0f17c87cd6 100644 --- a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp @@ -91,7 +91,7 @@ void CheckSimpleReplacement() { Require(!source->HasUses() && source->OperandUses().empty()); Require(first->Argument(0) == target && first->Argument(1) == target && first->Argument(2) == target); Require(second->Argument(0) == target); - const std::vector expected{{existing, 0}, {first, 2}, {first, 1}, {second, 0}, {first, 0}}; + const IrUseList expected{{existing, 0}, {first, 2}, {first, 1}, {second, 0}, {first, 0}}; Require(target->OperandUses() == expected); Require(std::ranges::equal(target->Uses(), std::vector({existing, first, first, second, first}))); first->ReplaceArgument(1, source); @@ -110,10 +110,10 @@ void CheckTailArgumentRemoval() { last->AddArgument(source); last->AddArgument(source); last->ReplaceArgument(0, nullptr); - Require(source->OperandUses() == std::vector({{first, 0}, {last, 1}})); + Require(source->OperandUses() == IrUseList({{first, 0}, {last, 1}})); last->ReplaceArgument(1, graph.Value(3)); - Require(source->OperandUses() == std::vector({{first, 0}})); - Require(graph.Value(3)->OperandUses() == std::vector({{last, 1}})); + Require(source->OperandUses() == IrUseList({{first, 0}})); + Require(graph.Value(3)->OperandUses() == IrUseList({{last, 1}})); first->Invalidate(); Require(!source->HasUses() && first->Argument(0) == nullptr); } diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index 43db702ad8..e31e25adb8 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -765,7 +765,7 @@ bool forwardsWord(const IrUse& use) { void collectReaders(IrValue& value, std::vector& readers) { std::unordered_set visited{&value}; - std::vector, std::size_t>> pending; + std::vector> pending; pending.emplace_back(value.OperandUses(), 0u); while (!pending.empty()) { auto& [uses, next] = pending.back(); @@ -819,7 +819,7 @@ void lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancil if (!isPackedAncillary(ancillary)) { return; } - const std::vector uses = ancillary.OperandUses(); + const IrUseList uses = ancillary.OperandUses(); std::vector direct; std::vector forwarded; for (const IrUse& use : uses) { diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index f4f2e38844..43e6c9a3cc 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -536,6 +536,7 @@ std::shared_ptr getSource(const RecompileRequest& request, ShaderPr ResourceProgram resource(request, preparation, reclaim); source->plan = std::move(resource.plan); if (!hasPreparedArtifact && std::ranges::none_of(source->variants, [&](const auto& variant) { return sameLayout(variant->layout, request.layout); })) { + resource.program->ReleaseUseArena(); source->program = std::move(resource.program); } } catch (...) { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index a55c6b3c49..a564f91563 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -106,7 +106,7 @@ static_assert(sizeof(BindingAllocationResult) == 184, "BindingAllocationResult c static_assert(sizeof(CompiledBindingLayout) == 88, "CompiledBindingLayout changed: update the allocation encoder"); static_assert(sizeof(BindingLayout) == 16, "BindingLayout changed: update BuildKey"); static_assert(sizeof(IrResourcePlan) == 728, "IrResourcePlan changed: update EncodePlan and DecodePlan"); -static_assert(sizeof(IrValue) == 136, "IrValue changed: update the plan value encoder"); +static_assert(sizeof(IrValue) == 144, "IrValue changed: update the plan value encoder"); static_assert(sizeof(GuestRegister) == 8, "GuestRegister changed: update the plan value encoder"); static_assert(sizeof(MemoryInfo) == 80, "MemoryInfo changed: update the plan encoder"); static_assert(sizeof(DescriptorSource) == 192, "DescriptorSource changed: update the plan encoder"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 7679b0cd6d..2d3f5f0bfb 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1660,6 +1660,10 @@ std::uint32_t EmitClassMaskF32(SpirvEmitterState& state, std::uint32_t value, st } std::uint32_t EmitMinMaxF32Value(SpirvEmitterState& state, std::uint32_t lhs, std::uint32_t rhs, bool maxValue) { + if (maxValue && lhs == ConstantF32(state, 0u)) { + const auto positive = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), rhs, lhs); + return Select(state, TypeF32(state), positive, rhs, lhs); + } const auto lhsClass = EmitClassifyF32(state, lhs); const auto rhsClass = EmitClassifyF32(state, rhs); diff --git a/docs/dev/debt/libSceAcm.md b/docs/dev/debt/libSceAcm.md index 2c1bdc3552..a559bd0bca 100644 --- a/docs/dev/debt/libSceAcm.md +++ b/docs/dev/debt/libSceAcm.md @@ -2,7 +2,7 @@ ### Silent stubs -- [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) batches execute as silent no-ops, and do not execute encoded panner, FFT and reverb commands. +- [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) batches execute as silent no-ops, and do not execute encoded panner, FFT, reverb and priority commands (the priority job layout and checks are read from `libSceAcm.sprx`, firmware 13.40, not measured on a console). ### Unknown function info diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 94fce96bf8..13dd239b2b 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -285,7 +285,7 @@ - [Host import refresh](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): after a change to the guest allocation registry, an import is checked again only when its import window, or its own range when larger, touches a range that changed since the last check, or when it was made since then; every import is checked when the window is 0 or the window or the budget changed. An unchanged import whose run could now be larger, because a remembered refusal no longer holds, is kept until its window changes or a request for the larger run retires it; before, any registry change anywhere retired it. Remembered failures and refusals are dropped the same way and stay checked where they are used. - [Sampled MSAA surface geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) supplies the fragment-aware linear size and one mip for a tiled 2D surface at two, four or eight fragments. Unsupported arrays, mip chains, extents and block sizes throw. Linear descriptors retain the existing geometry path, including its use when sampling an existing native target; a linear MSAA guest layout is not modeled. The range calculation follows AMD Addrlib's thin-block dimensions. Console layout equivalence and title effects of the additional mip metadata remain unmeasured. This does not add sample upload or readback. -- [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted. Without blending Vulkan's conversion clamps the stored value. With blending the pixel shader reads the target through its storage image under pixel interlock, evaluates the blend equation on the unclamped export, and stores the clamped result, because Vulkan clamps blend inputs to the normalized range. The result is not measured on a console. The final float to normalized rounding is Vulkan's. The path needs fragmentShaderPixelInterlock, recorded draws and a single-sample resident target, and refuses dual-source blending, multisampled, packed, reversed and alternate component order targets, a signed target whose write mask skips a component (the shader rewrites it through a float, so the code -128 would become -127), and a packed target's partial write in the same draw. +- [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted. With blending the pixel shader reads the target through its storage image under pixel interlock, evaluates the blend equation on the unclamped export, and stores the clamped result, because Vulkan clamps blend inputs to the normalized range. The result is not measured on a console. The final float to normalized rounding is Vulkan's. The path needs fragmentShaderPixelInterlock and shaderStorageImageReadWithoutFormat (refused without either), recorded draws and a single-sample resident target, and refuses dual-source blending, multisampled, packed, reversed and alternate component order targets, a signed target whose write mask skips a component (the shader rewrites it through a float, so the code -128 would become -127), and a packed target's partial write in the same draw. - [Multisample image readback](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp) fetches unsigned R8/R16/R32/R32G32/R32G32B32A32 or D32 depth samples through a 2D array view in GENERAL, in layer/y/x/fragment order. The caller owns producer completion, image transitions and resource lifetimes. It implements Vulkan per-sample fetches, not guest-memory publication, metadata expansion or PS5 layout equivalence; D32 exceptional-value preservation has not been measured. Other formats, sample counts and invalid ranges throw. - [Multisample image upload](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleUpload.cpp) does not model guest layouts, native ownership or CMASK/FMASK/DCC publication; console equivalence and large-title initialization costs remain unmeasured. It stores 1/2/4/8/16-byte elements from a storage buffer into an unsigned 2D multisample array view in layer/y/x/fragment order. It requires enabled multisample storage images and formatless image writes, 2/4/8 fragments and valid aligned buffer and dispatch ranges. The caller supplies the matching storage view in GENERAL and owns barriers, completion and resource lifetimes. diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 4134ebe72e..42248966d1 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -134,6 +134,7 @@ ### Functional +- [sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - a blocking Push waits for room before it queues its grain and again after, so it returns once fewer grains than the queue depth wait to be played (each wait ends after at most 200 ms, so on a queue that never drains a blocking Push returns after up to 400 ms). Inferred from PPSA02530, whose sound engine pushes one grain per call from a feeder thread and wakes its renderer after each push: while Push returned as soon as the grain was queued, the renderer found the queue full and slept until the next rendered frame. When the console's blocking Push returns is unmeasured. Both waits release the context lock, so destroying a context while a push is waiting on it is not supported. - [sceAudioOutMasteringInit, sceAudioOutMasteringTerm and sceAudioOutMasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - non-zero flags, a null pointer and a params id other than 1 throw; a repeated Init, a Term without Init and a SetParam or GetState before Init are unknown. There is no mastering stage, so Init and Term keep no state and SetParam reads only the id. The signatures and the default params id 1 follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut.cs#L312-L344), not a console measurement. SharpProspero documents flag bit 0 as `SwapEqCompressor`, and shadPS4's PS4 stub [rejects any non-zero Init flags](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/audio/audioout.cpp#L956-L962) with 0x80260201 - [sceAudioOutMasteringGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the states id 1 and the meter offsets 32, 128, 240, 272 and 304 in a 336-byte struct follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Audio/AudioOutMastering.cs#L97-L110), not a console measurement. With no mastering stage it zeroes the compressor input RMS and gain, and the limiter input, output and gain peaks, and leaves the other bytes, including the descriptors the AudioOut2 call writes, untouched; zero is not known to be what the console reports for an idle chain. A null pointer and a states id other than 1 throw; the console error codes are unknown - [sceAudioOut2EnableChat and sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) - both PS5 exports take no arguments, return 0x80268006 before AudioOut2 initialization, and invoke the system chat service with selector 3. A negative service result maps to 0x80268000, otherwise they return zero. The host only records a local enabled flag, without a chat service; repeated-call and no-session behavior therefore still depends on the missing service. diff --git a/docs/dev/debt/libSceFreeTypeOt.md b/docs/dev/debt/libSceFreeTypeOt.md index 7013351fc5..880f53c69e 100644 --- a/docs/dev/debt/libSceFreeTypeOt.md +++ b/docs/dev/debt/libSceFreeTypeOt.md @@ -1,7 +1,7 @@ # libSceFreeTypeOt technical debt - The FT_Outline_Get_Orientation, FT_Request_Size, FT_Sin, FT_Tan, FT_Vector_Unit, FT_New_Size, FT_Outline_Copy, FT_Outline_Decompose, FT_Stroker_New, FT_Stroker_Done, FT_Stroker_GetCounts, FT_Stroker_BeginSubPath, FT_Stroker_LineTo, FT_Stroker_EndSubPath, FT_Stroker_Set, FT_Stroker_Rewind, FT_Stroker_GetBorderCounts, FT_Stroker_ConicTo and FT_Stroker_CubicTo NIDs in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are identified, but their provider-specific prototypes and behavior remain unverified. Their named helpers retain the explicit inventory aliases and throw; the separate libSceFreeType implementations do not supply this provider. -- The 120 `sceFreeTypeOtUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. +- The 75 `sceFreeTypeOtUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. ### Unknown function info - [FT_Outline_GetInsideBorder, FT_Outline_GetOutsideBorder](../../../core/libs/prx/libSceFreeTypeOt/CMakeLists.txt) (libSceFreeTypeOl, libSceFreeTypeOt) - console prototypes, enum/layout equivalence, FreeType configuration and module/library versions are unverified. The existing LP64 outline layout and unsigned 16-bit counts follow bundled FreeType 2.13.3; border selection follows [ftstroke.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c), with the shifted polygon area from [ftoutln.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) accumulated in 64 bits. Null positive-count arrays and out-of-range or non-increasing contour endpoints throw when reached; their console behavior is unknown. Tags and flags are not read, and input arrays remain caller-owned. @@ -13,3 +13,5 @@ - [ft_corner_is_flat, ft_corner_orientation](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) (libSceFreeTypeOt) - console prototypes and FreeType version equivalence are unverified. The LP64 signatures and 64-bit coordinate calculations follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftcalc.c): orientation returns +1 for counter-clockwise turns, -1 for clockwise turns and 0 for collinear inputs using 64-bit coordinate cross-products; flatness compares hypotenuse approximations via the alpha-max-plus-beta-min metric and returns 1 when the corner angle deviation does not exceed the threshold, 0 otherwise. - [FT_MulDiv_No_Round, FT_Matrix_Multiply_Scaled, FT_Vector_Transform_Scaled, ft_highpow2](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) (libSceFreeTypeOt) - console prototypes and the FT_LONG64 build option are unverified. FT_Property_Set and ft_highpow2 date this FreeType to 2.4.11-2.5.3, whose stock [ftconfig.h](https://github.com/freetype/freetype/blob/cecf93ef90c660a8b0b45e5adbbfb5ea443fb6b9/include/freetype/config/ftconfig.h#L289-L306) drops FT_LONG64 under `__STDC__` and compiles the 32-bit [FT_MulDiv](https://github.com/freetype/freetype/blob/cecf93ef90c660a8b0b45e5adbbfb5ea443fb6b9/src/base/ftcalc.c#L411-L479). Inputs where that build, the FT_LONG64 build and bundled 2.13.3 can differ throw without writing: an operand above 0x7FFFFFFF in magnitude, a zero divisor with a zero factor, or a quotient above 0xFFFFFFFF. The scaled calls divide each term by 0x10000 * scaling, so |scaling| > 32767 throws; a null matrix or vector is a no-op unless that product overflows. + +- [ft_mem_alloc, ft_mem_qalloc, ft_mem_realloc, ft_mem_qrealloc, ft_mem_free, ft_mem_dup, ft_mem_strdup, ft_mem_strcpyn](../../../core/libs/prx/libSceFreeTypeOt/src/Memory.cpp) (libSceFreeTypeOt) - the exports carry the NIDs of these FreeType names; console prototypes and FreeType version equivalence are unverified. The LP64 signatures, the guest `FT_MemoryRec` callbacks and the error codes (Invalid_Argument 0x06, Array_Too_Large 0x0A, Out_Of_Memory 0x40) follow FreeType 2.4.12 [ftutil.c](https://github.com/freetype/freetype/blob/cecf93ef90c660a8b0b45e5adbbfb5ea443fb6b9/src/base/ftutil.c#L49-L221). Bundled FreeType 2.13.3 differs only in `ft_mem_qrealloc` from a count of 0, which it does not zero. As in a release build, `ft_mem_qrealloc` does not check the `FT_ASSERT` that a block with no current items is null. The current byte size passed to `realloc` wraps where the source's signed product would overflow. diff --git a/docs/dev/debt/libSceHttp2.md b/docs/dev/debt/libSceHttp2.md index 600830fa6e..f0f949af7c 100644 --- a/docs/dev/debt/libSceHttp2.md +++ b/docs/dev/debt/libSceHttp2.md @@ -30,9 +30,10 @@ - [libSceHttp2 transport](../../../core/libs/prx/libSceHttp2/Transport.cpp) - only synchronous HTTPS requests with version 3 and ALPN `h2` are supported. HTTP/1.x, cleartext HTTP/2, negotiation fallback, connection reuse, multiplexing across requests, CONNECT, response trailers, automatic redirects and authentication challenges throw. Version values, method strings, header overwrite/append modes, status/read/content-length signatures and timeout units follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http2.cs); wire framing and negotiation follow [RFC 9113 sections 3, 5, 6 and 8](https://www.rfc-editor.org/rfc/rfc9113.html). [prosper](https://github.com/mattias800/prosper/blob/b1bec57186a0/prosper/src/hle/net/hle_http2.cpp) instead records the method argument as an integer; the string signature follows SharpProspero. These are public compatibility contracts, not console measurements. - [libSceHttp2 error paths](../../../core/libs/prx/libSceHttp2/Transport.cpp) - cancellation, deadlines, DNS/TLS/protocol failures, malformed inputs, concurrent calls on one request and exhausted connection slots throw because their console error mappings are unknown. Socket connect errors propagate the libSceNet result, following the module-analysis report in [prosper #2894](https://github.com/mattias800/prosper/issues/2894); the report does not establish every exported error path. A request uses one connection; a context's concurrency cap counts connections until EOF, failure or deletion. Native pooling, scheduling, validation order and teardown semantics remain unverified. DNS uses two host workers and a queue of eight; cancellation/deadlines abandon the caller's wait, not libc's lookup. Library unload can wait for a host lookup to return. A resolve retry resubmits a failed lookup with a new timeout; a timed-out lookup isn't retried. The console's retry semantics are unverified. -- [libSceHttp2 TLS settings](../../../core/libs/prx/libSceHttp2/Export.cpp) - minimum SSL versions outside 2 to 5, nonnull SSL callbacks and flags outside `0x3d` throw. Supported certificate-check bits follow SharpProspero and use libSceSsl's TLS 1.2 backend and CA context; native defaults and error translation are unverified. Settings on object kinds other than templates/requests throw, except for the request options below; native object-kind restrictions are otherwise unverified. Settings become immutable when transport is created. Zero timeout values, negative resolve retries, gzip inflation and connection-slot wait timeouts throw; a failed lookup is retried up to the set count. Host HTTP allocations are not charged to the context pool, so `sceHttp2GetMemoryPoolStats` reports a live context's pool size with zero peak and current use. PPSA01494, PPSA30839 and PPSA31162 read the three sizes for memory logging without checking the result, and stopped at startup while it threw. +- [libSceHttp2 TLS settings](../../../core/libs/prx/libSceHttp2/Export.cpp) - minimum SSL versions outside 2 to 5, nonnull SSL callbacks and flags outside `0x3d` throw. Supported certificate-check bits follow SharpProspero and use libSceSsl's TLS 1.2 backend and CA context; native defaults and error translation are unverified. Settings on object kinds other than templates/requests throw, except for the request options below; native object-kind restrictions are otherwise unverified. Settings become immutable when transport is created. Zero timeout values, negative resolve retries and connection-slot wait timeouts throw; a failed lookup is retried up to the set count. Host HTTP allocations are not charged to the context pool, so `sceHttp2GetMemoryPoolStats` reports a live context's pool size with zero peak and current use. PPSA01494, PPSA30839 and PPSA31162 read the three sizes for memory logging without checking the result, and stopped at startup while it threw. - [sceHttp2SetMinSslVersion](../../../core/libs/prx/libSceHttp2/Export.cpp) - libSceHttp2's own range and validation order are not measured: versions 2 (SSL 3.0) to 5 (TLS 1.2) are accepted, the range `sceSslSetMinSslVersion` takes on a 13.40 console ([libSceSsl](libSceSsl.md)). The floor is not kept: libSceSsl negotiates TLS 1.2 only, which meets every accepted floor, so a server limited to versions older than TLS 1.2 fails the handshake whatever the floor; what a lower floor does with such a server on a console is not measured. - [libSceHttp2 request options](../../../core/libs/prx/libSceHttp2/Export.cpp) - extends the TLS settings entry: the auth and redirect flags, headers and the auth-info and pre-send callbacks live in `Http2::Settings`, are copied into each new request and become immutable once transport exists; the getters still read them then. Accepted kinds, defaults, copying, header modes, error codes and their order follow the 13.42 module run in an emulator ([#10536](https://github.com/boykopovar/AnyPS5/pull/10536)), not a console. A null out-pointer to `sceHttp2GetAuthEnabled` is written through on that module; here it throws. The pre-send callback type is unknown. The transport never calls either callback, and the auth cache flush only validates the context +- [libSceHttp2 gzip responses](../../../core/libs/prx/libSceHttp2/Transport.cpp) - `sceHttp2SetInflateGZIPEnabled` stores the flag. When it is set and the response carries `content-encoding: gzip`, the transport inflates the body with the vendored zlib before `sceHttp2ReadData` and `sceHttp2GetResponseContentLength` see it, and reports the inflated length; both calls wait for END_STREAM while the enabled response is gzip. The transport neither adds nor inspects `Accept-Encoding`, so the guest supplies that request header and an unsolicited gzip response is inflated too. Whether a console rewrites the header block (Content-Encoding and Content-Length) is unverified, so the serialized headers keep the fields the server sent; other encodings are not inflated, a malformed or truncated stream, a bad checksum or size and concatenated members throw, and the optional gzip header CRC is not verified. - [sceHttp2GetAllResponseHeaders](../../../core/libs/prx/libSceHttp2/Export.cpp) throws: the serialization, size and lifetime of a decoded HTTP/2 header block have not been established. Status and Content-Length fields are decoded; absent Content-Length reports `NotFound`, following SharpProspero. - [libSceHttp2 asynchronous requests](../../../core/libs/prx/libSceHttp2/Export.cpp) - send/read submission and `sceHttp2WaitAsync` throw. The previous offline implementation queued a fabricated network failure and used event type zero; public sources do not establish the completion event types, wait timeout updates, cancellation results or ownership of async buffers. PPSA17221's kqueue layout and PPSA15508's 24-byte wait result establish caller ABI only. No completion or equeue event is manufactured. diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index 32629a0137..62b5d6b939 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -21,4 +21,5 @@ ### Functional +- [sceRtcParseRFC3339, sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - when `second == 60`, both functions return 0 (success) but do not write the tick for the leap second; they still subtract the timezone offset from the existing output tick. `sceRtcParseDateTime` has the same behavior for digit-led (RFC 3339 style) input. Measured on PS5 firmware 13.60; see PR #1397. - The process-local clocks set by [sceRtcSetCurrentTick, sceRtcSetCurrentNetworkTick, sceRtcSetCurrentAdNetworkTick and sceRtcSetCurrentDebugNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) are visible only through libSceRtc. The system clock and libkernel clock APIs keep reporting host time; cross-library clock consistency is not modelled. The console's clock interactions are not measured diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 779bef0ae4..5517d6bc09 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -116,6 +116,7 @@ - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. - [memmem and memccpy](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `memmem` searches for needle occurrences in memory buffers and `memccpy` copies buffers until a character match, following FreeBSD and POSIX specifications; behavior is unverified on console. Neither is exported by `libSceLibcInternal.sprx` on a PS5 (firmware 13.60), so a payload process cannot call them there. - [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) round to the nearest float, where the console's `_Stof` in `libSceLibcInternal.sprx` (0x734a0) does not always: about a third of decimal inputs within a relative 2^-51 of a point halfway between two floats, and some hexadecimal inputs with more than 24 significant bits, come out one unit apart in the last place (`1e-40` is `0x1.16c3p-133` there and `0x1.16c2p-133` here). The console's own rounding is not reproduced +- [Short narrow character fields](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - on glibc-backed scan paths, `%2c` with one available input byte can return `EOF` after storing that byte, while the bundled modules in PPSA03766 and PPSA07923 count one assignment. This is a CPU-only module comparison, not a console run. - [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `ERANGE` follows the `libc.prx` bundled with PPSA08427 (J03547016) and PPSA03671 (J03848509), both run side by side with this libc on a Linux host: a decimal input whose result is a nonzero subnormal sets it only when inexact, a hexadecimal one only when it lies below the smallest subnormal and rounds up to it, and underflow to zero and overflow set it. Both builds also take a `NAN(...)` payload in `strtof` from the sequence's leading digits (`nan(0x123)` is `0x7fc00123`, `-nan(5)` is `0xffc00005`); the sign is kept here, the payload is not. `libSceLibcInternal`'s `_Stof` differs: no `ERANGE` for hexadecimal input, a positive `nan`, and after a minus sign a failed parse is `-0.0` for a lone sign and `+0.0` before an `inf`/`nan` spelling, the reverse of `libc.prx`. Decimal `strtod` sets `ERANGE` for a nonzero subnormal in J03547016 but not in J03848509 or `libSceLibcInternal`, whose result is kept. - [operator new](../../../core/libs/prx/libc/src/StdioExtras.cpp) (libc) - the `nothrow` forms return null only for `std::bad_alloc`, where the `libc.prx` bundled with PPSA21564 (build `J03093718`) catches every exception there, so a new handler that throws anything else ends the process instead of returning null. The aligned forms throw `std::bad_alloc` for an alignment of zero or one that is not a power of two, where that binary calls `abort`. diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index e6771c8831..ba0a3fc9b7 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -529,6 +529,10 @@ def write_reports(out, rows, blob, main_exports=None): (out / f"{number}.md").write_text(report(found, blob, main_exports.get(number)), encoding="utf-8") +def unverified_overlaps(rows): + return {number: set(found["uncompared"]) for number, found in rows.items() if found.get("uncompared")} + + def main_export_findings(prs, current_exports): findings = {} for number, pr in prs.items(): @@ -591,6 +595,12 @@ def main(argv=None): raise SystemExit(f"pr_overlap: {error}") if args.branch: print(report(rows[0], blob, main_exports.get(0)) or "No overlap with open pull requests.", end="") + unresolved = unverified_overlaps(rows[0]) + if unresolved: + for number, files in sorted(unresolved.items()): + progress(f"cannot confirm overlap with PR {number}: {', '.join(sorted(files))}") + progress("overlap check failed closed; manually compare every reported PR branch before opening this one") + return 2 return 0 write_reports(args.out, rows, blob, main_exports) return 0 diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py index 781bafce5c..dba19da350 100644 --- a/tools/tests/test_configure_seed.py +++ b/tools/tests/test_configure_seed.py @@ -47,7 +47,7 @@ class ConfigureSeedPathTests(unittest.TestCase): "resource.setrlimit(resource.RLIMIT_FSIZE, (64, 64)); " "sys.path.insert(0, sys.argv[1]); import configure_seed; configure_seed.main(sys.argv[2:])") result = subprocess.run([sys.executable, "-c", program, str(TOOL.parent), str(self.cache), str(seed)], - capture_output=True, text=True, timeout=5) + capture_output=True, encoding="utf-8", timeout=5) self.assertEqual(result.returncode, 2, result.stderr) self.assertNotIn("Traceback", result.stderr) self.assertEqual(result.stdout, "") @@ -62,11 +62,11 @@ class ConfigureSeedPathTests(unittest.TestCase): seed = self.root / "seed.cmake" program = "import os,sys; os.umask(0o027); sys.path.insert(0,sys.argv[1]); import configure_seed; configure_seed.main(sys.argv[2:])" arguments = [sys.executable, "-c", program, str(TOOL.parent), str(self.cache), str(seed)] - result = subprocess.run(arguments, capture_output=True, text=True, timeout=5) + result = subprocess.run(arguments, capture_output=True, encoding="utf-8", timeout=5) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(seed.stat().st_mode & 0o777, 0o640) seed.chmod(0o600) - result = subprocess.run(arguments, capture_output=True, text=True, timeout=5) + result = subprocess.run(arguments, capture_output=True, encoding="utf-8", timeout=5) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(seed.stat().st_mode & 0o777, 0o600) @@ -110,7 +110,7 @@ class ConfigureSeedPathTests(unittest.TestCase): def run_seed(self, seed, *options): return subprocess.run([sys.executable, str(TOOL), str(self.cache), str(seed), *options], - capture_output=True, text=True) + capture_output=True, encoding="utf-8") def assert_rejected(self, seed, *options): result = self.run_seed(seed, *options) @@ -124,7 +124,7 @@ class ConfigureSeedPathTests(unittest.TestCase): def test_unreadable_cache_is_reported_without_a_traceback(self): missing = self.root / "no cache dir" / "CMakeCache.txt" result = subprocess.run([sys.executable, str(TOOL), str(missing), str(self.root / "seed.cmake")], - capture_output=True, text=True) + capture_output=True, encoding="utf-8") self.assertEqual(result.returncode, 2, result.stderr) self.assertIn(str(missing), result.stderr) self.assertNotIn("Traceback", result.stderr) @@ -373,7 +373,7 @@ class ChecksParsingTests(unittest.TestCase): ("--include", "ARCH_SIZE"), ("--exclude", "ARCH_SIZE")): with self.subTest(options=options): result = subprocess.run([sys.executable, str(TOOL), str(cache), str(seed), *options], - capture_output=True, text=True) + capture_output=True, encoding="utf-8") self.assertEqual(result.returncode, 0, result.stderr) text = seed.read_text(encoding="utf-8") self.assertNotIn("ARCH_SIZE", text) diff --git a/tools/tests/test_pr_overlap_fail_closed.py b/tools/tests/test_pr_overlap_fail_closed.py new file mode 100644 index 0000000000..c8e3fb69d8 --- /dev/null +++ b/tools/tests/test_pr_overlap_fail_closed.py @@ -0,0 +1,52 @@ +import sys +import unittest +from contextlib import ExitStack +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import pr_overlap + + +class FailClosedTests(unittest.TestCase): + def run_branch_check(self, found): + argv = ["pr_overlap.py", "--repo", "owner/project", "--branch", "topic"] + patches = ( + patch.object(sys, "argv", argv), + patch.object(pr_overlap, "open_prs", return_value={7: {"ref": "refs/pr/7"}}), + patch.object(pr_overlap, "fetch"), + patch.object(pr_overlap, "own", return_value={7: {"ref": "refs/pr/7"}}), + patch.object(pr_overlap, "git", return_value="a" * 40), + patch.object(pr_overlap, "scan_all"), + patch.object(pr_overlap, "base_exports", return_value={}), + patch.object(pr_overlap, "main_export_findings", return_value={}), + patch.object(pr_overlap, "overlaps", return_value=found), + patch.object(pr_overlap, "report", return_value="review result\n"), + patch.object(pr_overlap, "progress"), + patch("builtins.print"), + ) + with ExitStack() as stack: + for item in patches: + stack.enter_context(item) + return pr_overlap.main() + + def test_uncompared_shared_files_fail_the_local_branch_check(self): + result = self.run_branch_check({"added": set(), "uncompared": {"VectorAlgorithms.cpp"}}) + self.assertEqual(result, 2) + + def test_confirmed_comparisons_keep_the_local_branch_check_green(self): + result = self.run_branch_check({"added": set(), "conflicts": {}, "exports": set()}) + self.assertEqual(result, 0) + + def test_reports_every_uncompared_path(self): + self.assertEqual( + pr_overlap.unverified_overlaps({ + 7: {"uncompared": {"b.cpp", "a.cpp"}}, + 8: {"conflicts": {"c.cpp": [(1, 2)]}}, + }), + {7: {"a.cpp", "b.cpp"}}, + ) + + +if __name__ == "__main__": + unittest.main()