diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4a6440642db..ce59adea0c6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2023,6 +2023,15 @@ if(NOT ANYPS5_AGC_EXECUTION_TESTS OR "PooledBufferRange" IN_LIST ANYPS5_AGC_EXEC endif() target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber) +target_link_libraries(agc_driver_same_queue_label_wait_tests PRIVATE libkernel) + +if(BUILD_TESTING) + add_test(NAME agc_driver_cpu_label_wake COMMAND agc_driver_same_queue_label_wait_tests --cpu-label-wake-only) + set_tests_properties(agc_driver_cpu_label_wake PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_cpu_label_wake PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() target_link_libraries(agc_driver_rect_list_corner_tests PRIVATE shader_recompiler_agc_driver) target_link_libraries(agc_driver_queue_zero_late_interrupt_tests PRIVATE libkernel) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp index d807612d5d8..16f271f0c44 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp @@ -12,7 +12,7 @@ struct SubmissionCosts { std::atomic submissions{0}, validateNs{0}, copyNs{0}, dequeueNs{0}, completeNs{0}, suspendNs{0}, suspends{0}, suspendsSkipped{0}, endSubmits{0}, endSkipped{0}, notifiesSkipped{0}; }; -extern std::atomic gpuLabels, completionLabels, noOpLabels, unlockedDrains; +extern std::atomic gpuLabels, completionLabels, noOpLabels, unlockedDrains, syncedUnimportedLabels; extern std::atomic labelFallbacks[5]; @@ -30,7 +30,7 @@ enum TrySite { TryLabel = 0, TryFlush, TryReap, TryPoll, TryIdle, TryCapture, Tr extern std::atomic triesFailed[TrySites]; -extern std::atomic storesOnGpu, storesBehindCompletions, storesOnCpu, storesDrained, dmaFills, copiesToHostMemory, copiesBehindPendingSources; +extern std::atomic storesOnGpu, storesBehindCompletions, storesOnCpu, storesSyncedUnimported, storesDrained, dmaFills, copiesToHostMemory, copiesBehindPendingSources; extern std::map drainCounts; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index d65d4bf7950..222360708ca 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Eq/include/Event.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" @@ -44,8 +45,8 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue const auto bytes = label->Bytes(); const auto stamp = ++eventSerial; reason = localDevice != nullptr ? localDevice->WriteLabelOnGpu(label->address, bytes, stamp, submission.queue) : 4; - if (reason == 1) GuestMemory::Write(label->address, bytes, 4); - if (reason == 0 || reason == 1 || reason == 5) { + if (reason == 1 || reason == 7) GuestMemory::Write(label->address, bytes, 4); + if (reason == 0 || reason == 1 || reason == 5 || reason == 7) { noteLabelStore(label->address, bytes, stamp, submission.queue); Graphics::Recorder::CloseLabelGroup(GuestMemory::TrackerGeneration()); } @@ -59,7 +60,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue interruptDeferred = localDevice->AfterRecordedWork([this, queueId] { deliverGuestEopInterrupt(queueId); }, submission.queue == 0); if (interruptDeferred && workOpen) localDevice->SubmitRecorded(submission.queue == 0); wroteOnGpu = true; - } else if (reason == 1) { + } else if (reason == 1 || reason == 7) { wroteOnGpu = true; } } @@ -94,7 +95,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue const auto stamp = ++eventSerial; const auto reason = localDevice != nullptr ? localDevice->WriteLabelOnGpu(label->address, bytes, stamp, submission.queue) : 4; wroteOnGpu = reason == 0 || reason == 5; - if (reason == 1) { + if (reason == 1 || reason == 7) { GuestMemory::Write(label->address, bytes, 4); wroteOnGpu = true; @@ -155,15 +156,16 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue recordDeferredLabels(localDevice.get(), submission.queue); const auto stamp = ++eventSerial; const auto reason = localDevice != nullptr ? localDevice->WriteLabelOnGpu(store->address, bytes, stamp, submission.queue) : 4; - if (reason == 0 || reason == 1 || reason == 5) noteLabelStore(store->address, bytes, stamp, submission.queue); + if (reason == 0 || reason == 1 || reason == 5 || reason == 7) noteLabelStore(store->address, bytes, stamp, submission.queue); if (reason == 0 || reason == 5) { if (reason == 0) ++storesOnGpu; else ++storesBehindCompletions; wroteOnGpu = true; drained = false; - } else if (reason == 1) { + } else if (reason == 1 || reason == 7) { GuestMemory::Write(store->address, bytes, 1); - ++storesOnCpu; + if (reason == 7) ++storesSyncedUnimported; + else ++storesOnCpu; wroteOnGpu = true; drained = false; } @@ -372,10 +374,11 @@ void Driver::commitDmaStore(std::uint32_t queue, std::uint64_t address, std::spa if (reason == 0) ++storesOnGpu; else ++storesBehindCompletions; } else { - if (reason != 1 && localDevice != nullptr) localDevice->WaitIdle(); + if (reason != 1 && reason != 7 && localDevice != nullptr) localDevice->WaitIdle(); GuestMemory::Write(address, bytes, 1); noteLabelStore(address, bytes, stamp != 0 ? stamp : ++eventSerial, queue); - ++storesOnCpu; + if (reason == 7) ++storesSyncedUnimported; + else ++storesOnCpu; } Graphics::Recorder::CloseLabelGroup(GuestMemory::TrackerGeneration()); } @@ -424,8 +427,9 @@ void Driver::commitLabel(std::uint32_t queue, std::uint64_t address, std::spanWriteLabelOnGpu(address, bytes, stamp, queue) : 4; if (reason != 0 && reason != 5 && reason != 6) { - if (reason != 1 && localDevice != nullptr) localDevice->WaitIdle(); + if (reason != 1 && reason != 7 && localDevice != nullptr) localDevice->WaitIdle(); GuestMemory::Write(address, bytes, 4); + KernelWakeSleepers_nid_no_patch(); } noteLabelStore(address, bytes, stamp, queue); Graphics::Recorder::CloseLabelGroup(GuestMemory::TrackerGeneration()); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp index 858cada5e4b..46ef48612ea 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include "ThreadOwned.hpp" #include @@ -118,8 +119,9 @@ void Driver::recordDeferredLabels(VulkanDevice* localDevice, std::uint32_t queue first = false; countLabelOutcome(reason); if (reason != 0 && reason != 5) { - if (reason != 1 && localDevice != nullptr) localDevice->WaitIdle(); + if (reason != 1 && reason != 7 && localDevice != nullptr) localDevice->WaitIdle(); GuestMemory::Write(label.address, bytes, 4); + KernelWakeSleepers_nid_no_patch(); } noteLabelStore(label.address, bytes, stamp, queue); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/SynchronizationStatistics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/SynchronizationStatistics.cpp index 5b1e22624b9..d24f7cd7e9e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/SynchronizationStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/SynchronizationStatistics.cpp @@ -5,7 +5,7 @@ namespace AgcDriver::DriverDetail { -std::atomic gpuLabels{0}, completionLabels{0}, noOpLabels{0}, unlockedDrains{0}; +std::atomic gpuLabels{0}, completionLabels{0}, noOpLabels{0}, unlockedDrains{0}, syncedUnimportedLabels{0}; std::atomic labelFallbacks[5] = {}; @@ -21,7 +21,7 @@ std::atomic packetLockRecordUs{0}, packetLockSubmitUs{0}; std::atomic triesFailed[TrySites] = {}; -std::atomic storesOnGpu{0}, storesBehindCompletions{0}, storesOnCpu{0}, storesDrained{0}, dmaFills{0}, copiesToHostMemory{0}, copiesBehindPendingSources{0}; +std::atomic storesOnGpu{0}, storesBehindCompletions{0}, storesOnCpu{0}, storesSyncedUnimported{0}, storesDrained{0}, dmaFills{0}, copiesToHostMemory{0}, copiesBehindPendingSources{0}; std::map drainCounts; @@ -39,12 +39,13 @@ SubmissionCosts& submissionCosts(std::uint32_t queue) { void reportSync() { std::string report; for (const auto& [code, count] : drainCounts) report += " " + (code == 0xffffu ? std::string("flip") : Pm4::Name(code << 8u)) + "=" + std::to_string(count); - AgcDriver::ProfilePrint_nid_no_patch("[sync] %llu device drains by packet:%s (%llu waited without the GPU mutex); %llu labels written on the GPU, %llu deferred behind completions, %llu no-op, fallbacks: idle %llu, completions %llu, not imported %llu, undecodable %llu; %llu labels queued per worker, recorded in %llu groups, %llu recorded at once; stores: %llu recorded on the GPU, %llu behind completions, %llu on the CPU (idle), %llu synced (drained), %llu DMA fills stored as buffer fills; %llu copies into host memory synced on their source; %llu copies of up to 64 KiB recorded on the GPU behind their source's pending stores\n", static_cast(drainTotal), report.c_str(), static_cast(unlockedDrains.load()), static_cast(gpuLabels.load()), static_cast(completionLabels.load()), static_cast(noOpLabels.load()), static_cast(labelFallbacks[1].load()), static_cast(labelFallbacks[2].load()), static_cast(labelFallbacks[3].load()), static_cast(labelFallbacks[4].load()), static_cast(queuedLabels.load()), static_cast(labelGroups.load()), static_cast(immediateLabels.load()), static_cast(storesOnGpu.load()), static_cast(storesBehindCompletions.load()), static_cast(storesOnCpu.load()), static_cast(storesDrained.load()), static_cast(dmaFills.load()), static_cast(copiesToHostMemory.load()), static_cast(copiesBehindPendingSources.load())); + AgcDriver::ProfilePrint_nid_no_patch("[sync] %llu device drains by packet:%s (%llu waited without the GPU mutex); %llu labels written on the GPU, %llu deferred behind completions, %llu no-op, fallbacks: idle %llu, no covering import (recorder synced) %llu, completions %llu, not imported %llu, undecodable %llu; %llu labels queued per worker, recorded in %llu groups, %llu recorded at once; stores: %llu recorded on the GPU, %llu behind completions, %llu on the CPU (idle), %llu on the CPU with no covering import (recorder synced), %llu synced (drained), %llu DMA fills stored as buffer fills; %llu copies into host memory synced on their source; %llu copies of up to 64 KiB recorded on the GPU behind their source's pending stores\n", static_cast(drainTotal), report.c_str(), static_cast(unlockedDrains.load()), static_cast(gpuLabels.load()), static_cast(completionLabels.load()), static_cast(noOpLabels.load()), static_cast(labelFallbacks[1].load()), static_cast(syncedUnimportedLabels.load()), static_cast(labelFallbacks[2].load()), static_cast(labelFallbacks[3].load()), static_cast(labelFallbacks[4].load()), static_cast(queuedLabels.load()), static_cast(labelGroups.load()), static_cast(immediateLabels.load()), static_cast(storesOnGpu.load()), static_cast(storesBehindCompletions.load()), static_cast(storesOnCpu.load()), static_cast(storesSyncedUnimported.load()), static_cast(storesDrained.load()), static_cast(dmaFills.load()), static_cast(copiesToHostMemory.load()), static_cast(copiesBehindPendingSources.load())); } void countLabelOutcome(int reason) { if (reason == 0) ++gpuLabels; else if (reason == 5) ++completionLabels; + else if (reason == 7) ++syncedUnimportedLabels; else ++labelFallbacks[std::min(reason, 4)]; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 45a1354a4b3..fb7f329276a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2306,8 +2306,14 @@ int VulkanDevice::WriteLabelOnGpu(std::uint64_t address, std::span progressWatcher; // Identity for a thread that released the mutex around a wait (syncThroughUnlocked): the diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 074681e607a..053fb10c010 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -3441,10 +3441,11 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& } else if (state.refreshedGeneration > generation) { return true; } - if (!adopting) current = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); for (const auto& mapped : ranges) { const auto range = mapped.lock(); - if (range == nullptr || leasedRangeOwner(current, range->address) != range) continue; + if (range == nullptr || range->bytes <= 0x10000) continue; + if (current.empty()) current = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + if (leasedRangeOwner(current, range->address) != range) continue; try { if (!GuestMemory::GpuMutex().HeldByThisThread()) { const auto run = importRun(current, range->address, std::numeric_limits::max(), effectiveBudget(context, state)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 9f03343a675..2a847dd07b0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -3517,6 +3517,10 @@ void Recorder::HoldNextSubmit(VkSemaphore semaphore, std::uint64_t value) { submitHoldValue = value; } +bool Recorder::SubmitHeld() const { + return submitHold != VK_NULL_HANDLE || (heldSerial != 0 && unsignaled(heldSerial)); +} + void Recorder::Submit() { PerformanceTimer frameTiming("Recorder.Submit"); // The work count is cleared even when nothing is open: the driver counts a dispatch after its @@ -3592,6 +3596,7 @@ void Recorder::Submit() { Check(result, "vkQueueSubmit recorder"); batch->submitted = true; batch->serial = ++submissions; + if (hold != VK_NULL_HANDLE) heldSerial = batch->serial; batch->submittedAt = std::chrono::steady_clock::now(); if (DrawProfiled()) { const auto us = std::chrono::duration(batch->submittedAt - submitStart).count(); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index e4ea60da835..0f28c680cda 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -809,7 +809,7 @@ class UnrelatedImportProbeRange { void importMappedFailureGenerationTests(const Context& context) { ClearHostImports(context.device); - WindowedRange range(context, WindowedRange::piece, 1); + WindowedRange range(context, 2 * WindowedRange::piece, 1); ImportAllocationProbe probe(range.importing); probe.failure = VK_ERROR_INVALID_EXTERNAL_HANDLE; GuestAllocations::Mapped mapped; @@ -979,7 +979,7 @@ void importNeighbouringEdgesTests(const Context& context) { // A range mapped while the imports are at the budget is refused at map time without retiring an import: the map // observer holds no GPU lock, so it must not evict (before, it evicted the least recently used import to fit). void importAtMapOverBudgetTests(const Context& context) { - constexpr std::size_t bytes = 65536; + constexpr std::size_t bytes = 2 * 65536; auto importing = context; importing.hostImportAlignment = bytes; void* kept = ::operator new(bytes, std::align_val_t{bytes}); @@ -1039,17 +1039,32 @@ void importRequesterTests(const Context& context, const BdaTestAccess& access) { const auto counted = [](ImportRequester requester) { return HostImportCounters().requesters[static_cast(requester)]; }; const auto made = [&](ImportRequester requester) { return counted(requester).made; }; Require(std::string(ImportRequesterName(ImportRequester::Map)) == "map" && std::string(ImportRequesterName(ImportRequester::Build)) == "build" && std::string(ImportRequesterName(ImportRequester::Access)) == "access" && std::string(ImportRequesterName(ImportRequester::Join)) == "join" && std::string(ImportRequesterName(ImportRequester::Segment)) == "segment", "an import requester has the wrong name"); - change(0, true); - { + const auto mappedRange = [&](std::size_t index) { GuestAllocations::Mapped ranges; const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); for (const auto& range : lease) { - if (range->address == address(0)) ranges.push_back(range); + if (range->address == address(index)) ranges.push_back(range); } Require(ranges.size() == 1, "the requester test's mapped range is not registered"); + return ranges; + }; + { + GuestAllocations::Mutation mutation; + mutation.Add(block + 6 * piece, 2 * piece, true, true, true); + } + { const auto before = made(ImportRequester::Map); - static_cast(ImportMappedRanges(importing, ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true)); - Require(made(ImportRequester::Map) == before + 1 && HostImportCovers(importing, address(0), piece), "a range imported at map time was not counted for the map requester"); + static_cast(ImportMappedRanges(importing, mappedRange(6), GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true)); + Require(made(ImportRequester::Map) == before + 1 && HostImportCovers(importing, address(6), 2 * piece), "a range imported at map time was not counted for the map requester"); + } + change(0, true); + { + const auto map = made(ImportRequester::Map); + const auto access = made(ImportRequester::Access); + static_cast(ImportMappedRanges(importing, mappedRange(0), GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true)); + Require(made(ImportRequester::Map) == map && !HostImportCovers(importing, address(0), 16), "a range of 64 KiB was imported at map time"); + Require(HostImportFor(importing, address(0), 16) != nullptr && HostImportCovers(importing, address(0), piece) && made(ImportRequester::Access) == access + 1 && made(ImportRequester::Map) == map, + "a range of 64 KiB left to its first GPU use was not imported there, or not counted for that use's requester"); } change(1, true); { @@ -1127,6 +1142,10 @@ void importRequesterTests(const Context& context, const BdaTestAccess& access) { const auto counts = HostImportCounters(); Require(counts.live >= 4 && counts.liveBytes == HostImportLiveBytes(), "the live import counts disagree with the live bytes"); for (std::size_t index = 0; index < 6; ++index) change(index, false); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block + 6 * piece); + } ClearHostImports(importing.device); ::operator delete(block, std::align_val_t{piece}); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 97f02e7f2f2..45df34ee978 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2371,7 +2371,7 @@ void remappedImportTests(const Device& device) { void hostImportRetirementTests(const Device& device) { const auto& context = device.GetContext(); - constexpr std::size_t UnitBytes = 65536; + constexpr std::size_t UnitBytes = 2 * 65536; constexpr std::size_t AllocationBytes = 4 * UnitBytes; constexpr std::size_t CopyBytes = 256; #ifdef _WIN32 diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp index e6d9bd6b307..35e4f777cdd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp @@ -21,6 +21,14 @@ #include #include +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + namespace { namespace Graphics = AgcDriver::Graphics; @@ -163,6 +171,41 @@ Outcome ResidentMappingsImportAfterCollection() { return Outcome::Passed; } +Outcome SmallMappingsImportOnFirstUse() { + GuestBlock small(GuestAlignment); + GuestBlock large(2 * GuestAlignment); + auto device = OpenVulkanTestDevice(); + if (!device) return Outcome::NoDevice; + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (context.hostImportAlignment == 0) return Outcome::NoImport; + for (auto* guest : {&small, &large}) { + Graphics::ClearHostImports(device->Device()); + GuestAllocations::Mapped ranges; + for (const auto& range : GuestAllocations::GuestAllocationsAcquire_nid_postfix()) { + if (range->address == guest->Address()) ranges.push_back(range); + } + Require(ranges.size() == 1, "mapped import test lost its registered range"); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(Graphics::ImportMappedRanges(context, ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true), "mapped import test could not adopt the device"); + } + Require(Graphics::HostImportCovers(context, guest->Address(), ProbeBytes) == (guest == &large), "mapping did not defer exactly the small import"); + Require(Fill(*device, guest->Address()), "first GPU use could not import the mapped buffer"); + Require(Graphics::HostImportCovers(context, guest->Address(), ProbeBytes), "first GPU use left the mapped buffer unimported"); + constexpr std::array pattern{0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; + for (std::size_t offset = 0; offset < ProbeBytes; offset += sizeof(std::uint32_t)) { + std::uint32_t actual = 0; + std::memcpy(&actual, guest->Data() + offset, sizeof(actual)); + Require(actual == pattern[(offset / sizeof(actual)) % pattern.size()], "first GPU use changed the fill result"); + } + for (std::size_t offset = ProbeBytes; offset < GuestAlignment; ++offset) { + Require(guest->Data()[offset] == 0, "first GPU use overwrote bytes outside the fill"); + } + } + Graphics::ClearHostImports(device->Device()); + return Outcome::Passed; +} + } int main() { @@ -176,6 +219,7 @@ int main() { const auto windows = WindowsFollowTheirRange(); Require(windows == Outcome::Passed, "host import windows: the device stopped importing guest memory between two scenarios"); Require(ResidentMappingsImportAfterCollection() == Outcome::Passed, "resident import test failed"); + Require(SmallMappingsImportOnFirstUse() == Outcome::Passed, "mapped import test lost its Vulkan device or host import support"); std::puts("host import windows tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp index d43e135c359..26a1c7d26da 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp @@ -90,7 +90,7 @@ int main() { elsewhere.fill(0); using namespace AgcDriver::DriverDetail; const auto drainedBefore = storesDrained.load(); - const auto cpuBefore = storesOnCpu.load() + storesOnGpu.load() + storesBehindCompletions.load(); + const auto cpuBefore = storesOnCpu.load() + storesSyncedUnimported.load() + storesOnGpu.load() + storesBehindCompletions.load(); std::vector words; const auto fill = DmaData(true, 0, 0x5eed1234u, reinterpret_cast(first.data()), static_cast(sizeof(first))); const auto copy = DmaData(false, reinterpret_cast(first.data()), 0, reinterpret_cast(second.data()), 512); @@ -100,7 +100,7 @@ int main() { AgcDriverWaitIdle_nid_postfix(); for (const auto word : first) check(word == 0x5eed1234u, "the immediate fill did not land"); for (std::size_t i = 0; i < second.size(); ++i) check(second[i] == (i < 128 ? 0x5eed1234u : 0u), "the copy did not see the fill before it, or wrote past its bytes"); - const auto stores = storesOnCpu.load() + storesOnGpu.load() + storesBehindCompletions.load() - cpuBefore; + const auto stores = storesOnCpu.load() + storesSyncedUnimported.load() + storesOnGpu.load() + storesBehindCompletions.load() - cpuBefore; check(stores == 2, "the DMA_DATA packets were not committed (" + std::to_string(stores) + " committed stores)"); check(storesDrained.load() == drainedBefore, "a DMA_DATA packet still drained"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SameQueueLabelWait.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SameQueueLabelWait.cpp index 607faa3dd36..f789d407d4d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SameQueueLabelWait.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SameQueueLabelWait.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Submit/include/Acb.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX @@ -11,6 +12,7 @@ #endif #include #include +#include #include #include #include @@ -19,6 +21,7 @@ #include #include #include +#include #include #include @@ -131,15 +134,54 @@ std::uint64_t Run(Slot& slot, std::uint32_t value, bool wait) { return AgcDriver::GuestMemory::CollectEpochBumps() - before; } +void CpuLabelWake(Slot& slot) { + const auto submit = [&](std::uint32_t value) { + std::vector words; + WriteData(words, &slot.done, value); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + return sceAgcDriverSubmitAcb(0x20, &packet); + }; + Require(submit(1) == 0, "CPU label wake warmup submit failed"); + AgcDriverWaitIdle_nid_postfix(); + bool woke = false; + for (std::uint32_t attempt = 0; attempt < 20 && !woke; ++attempt) { + const auto value = 1000 + attempt; + std::atomic ready{false}; + int result = -1; + std::jthread worker([&] { + while (!ready.load(std::memory_order_acquire)) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + result = submit(value); + }); + KernelNoteGpuSubmit_nid_no_patch(); + const auto began = std::chrono::steady_clock::now(); + ready.store(true, std::memory_order_release); + SleepWake::Sleep(1000000); + const auto elapsed = std::chrono::steady_clock::now() - began; + const bool landed = slot.done == value; + worker.join(); + AgcDriverWaitIdle_nid_postfix(); + Require(result == 0 && slot.done == value, "the CPU label did not reach guest memory"); + woke = landed && elapsed < std::chrono::microseconds(900); + } + Require(woke, "a completed CPU label did not wake the guest short sleep"); + std::cout << "CPU label wakes the guest only after its bytes landed\n"; +} + } -int main() { +int main(int argc, char** argv) { try { auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; device.reset(); GuestBlock block; auto& slot = block.Data(); + if (argc == 2 && std::string_view(argv[1]) == "--cpu-label-wake-only") { + CpuLabelWake(slot); + AgcDriverShutdown_nid_postfix(); + return 0; + } Run(slot, 1, false); std::uint64_t extra = ~std::uint64_t{0}; for (std::uint32_t attempt = 0; attempt < 3; ++attempt) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/UnimportedLabelOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/UnimportedLabelOrder.cpp index 2cfd3dd6592..bfd5bd45875 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/UnimportedLabelOrder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/UnimportedLabelOrder.cpp @@ -1,8 +1,11 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "VulkanTestDevice.hpp" +#include #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX @@ -10,14 +13,17 @@ #include #endif #include +#include #include #include #include #include #include #include +#include #include #include +#include #include namespace { @@ -29,13 +35,15 @@ constexpr std::uint32_t ReleaseMemHeader = 0xc0064900u; class GuestBlock { public: - GuestBlock() { + explicit GuestBlock(bool isolated = false) { + const std::size_t reserved = isolated ? 3 * BlockBytes : BlockBytes; #ifdef _WIN32 - block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + allocation = static_cast(VirtualAlloc(nullptr, reserved, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); #else - block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); + allocation = static_cast(std::aligned_alloc(BlockBytes, reserved)); #endif - Require(block != nullptr, "unimported label order: cannot allocate the guest block"); + Require(allocation != nullptr, "unimported label order: cannot allocate the guest block"); + block = isolated ? allocation + BlockBytes : allocation; std::memset(block, 0, BlockBytes); GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); } @@ -43,9 +51,9 @@ class GuestBlock { ~GuestBlock() { GuestAllocations::Mutation().Remove(block); #ifdef _WIN32 - VirtualFree(block, 0, MEM_RELEASE); + VirtualFree(allocation, 0, MEM_RELEASE); #else - std::free(block); + std::free(allocation); #endif } @@ -57,9 +65,83 @@ class GuestBlock { std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } private: + std::uint8_t* allocation = nullptr; std::uint8_t* block = nullptr; }; +class SubmitGate { +public: + explicit SubmitGate(AgcDriver::VulkanDevice& device) : device(device) { +#ifdef _WIN32 + library.reset(SDL_LoadObject("vulkan-1.dll")); +#else + library.reset(SDL_LoadObject("libvulkan.so.1")); +#endif + Require(library != nullptr, "unimported label order: cannot load Vulkan for the submit gate"); + const auto resolve = reinterpret_cast(SDL_LoadFunction(library.get(), "vkGetDeviceProcAddr")); + Require(resolve != nullptr, "unimported label order: cannot resolve vkGetDeviceProcAddr"); + const auto createSemaphore = reinterpret_cast(resolve(device.Device(), "vkCreateSemaphore")); + destroySemaphore = reinterpret_cast(resolve(device.Device(), "vkDestroySemaphore")); + signalSemaphore = reinterpret_cast(resolve(device.Device(), "vkSignalSemaphoreKHR")); + if (signalSemaphore == nullptr) signalSemaphore = reinterpret_cast(resolve(device.Device(), "vkSignalSemaphore")); + Require(createSemaphore != nullptr && destroySemaphore != nullptr && signalSemaphore != nullptr, "unimported label order: cannot resolve the timeline semaphore functions"); + VkSemaphoreTypeCreateInfo type{VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO}; + type.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE; + VkSemaphoreCreateInfo info{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, &type}; + Require(createSemaphore(device.Device(), &info, nullptr, &semaphore) == VK_SUCCESS, "unimported label order: cannot create the submit gate"); + } + + ~SubmitGate() { + if (watchdog.joinable()) { + watchdog.request_stop(); + watchdog.join(); + } + Release(); + try { + device.WaitIdle(); + } catch (...) { + } + destroySemaphore(device.Device(), semaphore, nullptr); + } + + SubmitGate(const SubmitGate&) = delete; + SubmitGate& operator=(const SubmitGate&) = delete; + + VkSemaphore Semaphore() const { return semaphore; } + + void ReleaseAfter(std::chrono::seconds timeout) { + watchdog = std::jthread([this, timeout](std::stop_token stop) { + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (!stop.stop_requested() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (!stop.stop_requested()) expired = true; + Release(); + }); + } + + bool Expired() const { return expired; } + + void Release() { + if (released.exchange(true)) return; + VkSemaphoreSignalInfo info{VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO}; + info.semaphore = semaphore; + info.value = 1; + signalResult = signalSemaphore(device.Device(), &info); + } + + bool Released() const { return signalResult == VK_SUCCESS; } + +private: + AgcDriver::VulkanDevice& device; + std::unique_ptr library{nullptr, &SDL_UnloadObject}; + PFN_vkDestroySemaphore destroySemaphore = nullptr; + PFN_vkSignalSemaphore signalSemaphore = nullptr; + VkSemaphore semaphore = VK_NULL_HANDLE; + std::jthread watchdog; + std::atomic released{false}; + std::atomic expired{false}; + VkResult signalResult = VK_NOT_READY; +}; + alignas(64) volatile std::uint32_t commandMemory[16] = {}; std::uint64_t AddressOf(volatile const std::uint32_t* word) { @@ -69,8 +151,8 @@ std::uint64_t AddressOf(volatile const std::uint32_t* word) { int StoreLabel(AgcDriver::VulkanDevice& device, std::uint64_t address, std::uint32_t value, std::uint64_t stamp) { const auto bytes = std::as_bytes(std::span(&value, 1)); const int reason = device.WriteLabelOnGpu(address, bytes, stamp, 0); - if (reason >= 1 && reason <= 4) { - if (reason != 1) device.WaitIdle(); + if ((reason >= 1 && reason <= 4) || reason == 7) { + if (reason != 1 && reason != 7) device.WaitIdle(); AgcDriver::GuestMemory::Write(address, bytes, 4); } return reason; @@ -81,6 +163,9 @@ int StoreLabel(AgcDriver::VulkanDevice& device, std::uint64_t address, std::uint int main() { try { GuestBlock labels; + GuestBlock temporary(true); + GuestBlock compute(true); + GuestBlock held(true); auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); @@ -89,6 +174,21 @@ int main() { std::printf("skipped, the device does not import guest memory\n"); return VulkanTestSkipped; } + { + const auto imported = AgcDriver::Graphics::HostImportLiveBytes(); + const std::array value{0x76543210u, 0xfedcba98u}; + const auto bytes = std::as_bytes(std::span(value)); + const auto address = temporary.Address() + 4092; + const int reason = device->WriteLabelOnGpu(address, bytes, 90, 0); + Require(reason == 7 && AgcDriver::Graphics::HostImportLiveBytes() == imported, "an eight-byte label did not select its unimported CPU path (reason " + std::to_string(reason) + ")"); + Require(labels.Words()[1024] == pattern[0], "the targeted label returned before preceding GPU work completed"); + device->WaitIdle(); + AgcDriver::GuestMemory::Write(address, bytes, 4); + Require(labels.Words()[1024] == pattern[0], "the CPU label overtook earlier GPU work"); + Require(temporary.Words()[1023] == value[0] && temporary.Words()[1024] == value[1], "the unimported eight-byte label did not land"); + Require(temporary.Words()[1022] == 0 && temporary.Words()[1025] == 0, "the CPU label changed neighboring words"); + Require(device->FillBuffer(labels.Address() + 4096, 4096, pattern), "cannot record work after the unimported label"); + } StoreLabel(*device, AddressOf(&commandMemory[8]), 1, 1); StoreLabel(*device, labels.Address() + 4, 2, 2); device->SubmitRecorded(true); @@ -104,6 +204,45 @@ int main() { std::array message{}; std::snprintf(message.data(), message.size(), "unimported label order: the label held 0x%08x when a later label had landed, and the guest's reused word holds 0x%08x instead of 0x%08x", seen, reused, ReleaseMemHeader); Require(seen == 1 && reused == ReleaseMemHeader, message.data()); + { + using namespace AgcDriver::DriverDetail; + const auto synced = syncedUnimportedLabels.load(); + const auto idle = labelFallbacks[1].load(); + const auto notImported = labelFallbacks[3].load(); + countLabelOutcome(7); + Require(syncedUnimportedLabels.load() == synced + 1 && labelFallbacks[1].load() == idle && labelFallbacks[3].load() == notImported, "an unimported label after a recorder sync was not counted on its own"); + } + { + Require(device->FillBuffer(labels.Address() + 4096, 4096, pattern), "cannot record work before the compute-queue label"); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr && !recorder->Idle(), "the compute-queue case has no recorded work before its label"); + const auto imported = AgcDriver::Graphics::HostImportLiveBytes(); + const auto submissions = recorder->Submissions(); + const std::uint32_t value = 0x600dc0deu; + const int reason = device->WriteLabelOnGpu(compute.Address() + 64, std::as_bytes(std::span(&value, 1)), 91, 0x20); + Require(reason == 0 && recorder->Submissions() == submissions, "a compute-queue label synced the recorder instead of importing its memory (reason " + std::to_string(reason) + ")"); + Require(AgcDriver::Graphics::HostImportLiveBytes() > imported, "the compute-queue label was recorded without importing its memory"); + device->WaitIdle(); + Require(compute.Words()[16] == value, "the compute-queue label did not land"); + } + if (auto* recorder = AgcDriver::Graphics::Recorder::Active(); recorder != nullptr && recorder->HasTimeline()) { + SubmitGate gate(*device); + recorder->HoldNextSubmit(gate.Semaphore(), 1); + Require(device->FillBuffer(labels.Address() + 4096, 4096, pattern), "cannot record work into the held batch"); + device->SubmitRecorded(false); + Require(recorder->SubmitHeld(), "the recorder did not report its held batch"); + gate.ReleaseAfter(std::chrono::seconds(5)); + const std::uint32_t value = 0x4e1du; + const int reason = device->WriteLabelOnGpu(held.Address() + 64, std::as_bytes(std::span(&value, 1)), 92, 0); + Require(!gate.Expired(), "a label behind a held batch waited for the batch instead of importing its memory"); + Require(reason == 0, "a label behind a held batch was not recorded on the GPU (reason " + std::to_string(reason) + ")"); + gate.Release(); + Require(gate.Released(), "unimported label order: cannot release the submit gate"); + device->WaitIdle(); + Require(!recorder->SubmitHeld() && held.Words()[16] == value, "the label behind the held batch did not land"); + } else { + std::puts("held-batch case skipped, the recorder has no timeline semaphore"); + } std::puts("unimported label order tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index d34f5f2425c..06e2a3428cc 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -235,7 +235,7 @@ - [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified; XPSemu currently stops first at the unresolved `mlockall` import (#4386) - [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 8192 entries on each side and counts them as a PS5 (firmware 13.60) counted them for a payload process: one cpu entry for each 2 MiB granule that holds a committed mapping, whatever its protection (none included), and one gpu entry for each 1 GiB region that holds a GPU-accessible mapping, plus one for each 2 MiB granule of such a mapping that is not a whole granule of direct memory at a 2 MiB-aligned physical address. A granule or region shared by several mappings counts once, and reservations do not count. On the console the first GPU mapping of a process also takes three gpu entries and one cpu entry that stay after it is unmapped, which is not modelled, and only the ranges whose protection is recorded here are counted. A null pointer returns `SCE_KERNEL_ERROR_EFAULT`, where the console faults. Resonance: A Plague Tale Legacy (PPSA30601) calls it at start-up (#4067) - [clock_settime](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) validates like FreeBSD 11.4's [`kern_clock_settime`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_time.c#L396-L412) for a root caller: `EFAULT` for a null time, `EINVAL` for any clock but `CLOCK_REALTIME`, a negative second count or a nanosecond count outside 0 to 999999999. A valid `CLOCK_REALTIME` request throws, naming the requested time, since the host clock is never set and no guest clock offset is kept. FreeBSD checks `PRIV_CLOCK_SETTIME` first, which a jailed process fails with `EPERM`; whether a title holds it on the console is unverified. PS5MC (PPSA99420) imports it; no call site is known -- [Short sleeps ended by GPU and flip completions](../../../core/libs/prx/libkernel/Time/SleepWake.cpp) (libkernel) - a `sceKernelUsleep`, `usleep`, `nanosleep` or `sceKernelNanosleep` of 1 ms or less returns before the requested time, which FreeBSD and the console do not do: it ends once the AGC driver reports a completed GPU batch or batch completion action, or VideoOut completes a flip, so a title that polls a GPU label or the flip status between sleeps does not sleep the rest. A title that paces frames with such a sleep can wake up to 1 ms early. A thread returns early at most 8 times in a run of sleeps; the run restarts when the thread submits AGC work or did not sleep the requested time since its last sleep, so a loop that counts sleeps to time out still sleeps all but 8 in full. `APS5_NO_SLEEP_WAKE=1` keeps every sleep in full +- [Short sleeps ended by GPU and flip completions](../../../core/libs/prx/libkernel/Time/SleepWake.cpp) (libkernel) - a `sceKernelUsleep`, `usleep`, `nanosleep` or `sceKernelNanosleep` of 1 ms or less returns before the requested time, which FreeBSD and the console do not do: it ends once the AGC driver reports a completed GPU batch or batch completion action or writes a label from the CPU, or VideoOut completes a flip, so a title that polls a GPU label or the flip status between sleeps does not sleep the rest. A title that paces frames with such a sleep can wake up to 1 ms early. A thread returns early at most 8 times in a run of sleeps; the run restarts when the thread submits AGC work or did not sleep the requested time since its last sleep, so a loop that counts sleeps to time out still sleeps all but 8 in full. `APS5_NO_SLEEP_WAKE=1` keeps every sleep in full - [chflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) follows FreeBSD 11.4's [`kern_chflagsat`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2366-L2383) and UFS's [`ufs_setattr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/ufs/ufs/ufs_vnops.c#L509-L556) for a root caller: the path lookup reports `EFAULT`, `ENOENT` and `ENOTDIR`, a flag outside the 17 UFS stores (`VNOVAL` included) `EOPNOTSUPP`, and `SF_SNAPSHOT` `EPERM`. Neither host keeps BSD file flags, so every file is taken to have none (`stat` reports `st_flags` 0): flags 0 succeed without touching the file, and any other flag throws, naming the flags. `UF_READONLY`, `UF_HIDDEN`, `UF_SYSTEM` and `UF_ARCHIVE` are FreeBSD's names for the Windows attributes but are not mapped to them: UFS stores them without enforcing them, `FILE_ATTRIBUTE_READONLY` already carries the owner write bit of `chmod` on Windows, and Linux has no equivalent a process can set without `CAP_LINUX_IMMUTABLE`. The console's filesystem types, `EROFS` on read-only mounts and the change-time update are not modelled - [fchflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) has the gaps of `chflags`: a valid non-zero flag throws, and the console's filesystem types, `EROFS` and the change-time update are not modelled. Character devices, the random device, other host descriptors and files read from app0.zar throw, since their console filesystems and the privilege check for devices are not modelled. On Windows, anonymous shared memory objects are temporary files that are not told apart from regular files, so they are treated as files instead of reporting `EINVAL` as FreeBSD 11.4's [`getvnode`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L3850-L3877) does. - [getpriority](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) follows FreeBSD 11.4's [`sys_getpriority`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_resource.c#L93-L165) for the only guest process, which is taken to run at nice 0: `PRIO_PROCESS` and `PRIO_PGRP` with 0 or its own pid (the guest is taken to lead its own process group, see `kill`) and `PRIO_USER` with 0 or its user id 0 return 0, any other id fails with `ESRCH` and any other `which` with `EINVAL`. As in FreeBSD's libc, where `getpriority` is the bare system call, errno is left unchanged on success. The nice value of a title process on the console is unverified, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises is not reported. `setpriority` accepts the same target ids (any other id fails with `ESRCH`, any other `which` with `EINVAL`), clamps the value to FreeBSD's `PRIO_MIN`-`PRIO_MAX` (-20 to 20) and stores it as the guest process's nice value, which `getpriority` returns; the host scheduling class is not changed, and whether a title process may lower the value without privilege is unverified