Merge pull request #5596 from tibzejoker/port/pr-637-main

fix(libc): track writes to singly mapped direct memory without faults on Windows
This commit is contained in:
Dean 2026-10-14 15:53:44 +13:00 committed by GitHub
commit 3e4fe4ce31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 607 additions and 24 deletions

View File

@ -985,6 +985,9 @@ if(BUILD_TESTING)
if(WIN32)
set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only)
set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()

View File

@ -1310,6 +1310,41 @@ void hostImportRetirementTests(const Device& device) {
Require(preservedEveryRecorder, "unlocked mapping changed recorder batches");
std::cout << "Host import retirement locking and GPU readback tests passed\n";
}
#ifdef _WIN32
void secondMappingImportTests(const Device& device) {
const auto& context = device.GetContext();
if (context.hostImportAlignment == 0) {
std::cout << "host imports unavailable: imports after a second mapping not tested\n";
return;
}
constexpr std::size_t bytes = 65536;
constexpr std::size_t page = 16384;
struct Release {
void* mapping = nullptr;
void* alias = nullptr;
std::int64_t physical = -1;
~Release() {
if (alias != nullptr) sceKernelMunmap(alias, page);
if (mapping != nullptr) sceKernelMunmap(mapping, bytes);
if (physical >= 0) sceKernelReleaseDirectMemory(physical, bytes);
}
} release;
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, bytes, 0, &release.physical) == 0, "allocate the second mapping test backing");
Require(sceKernelMapDirectMemory(&release.mapping, bytes, 0x33, 0, release.physical, bytes) == 0, "map the second mapping test backing");
const auto address = reinterpret_cast<std::uint64_t>(release.mapping);
if (HostImportFor(context, address, bytes) == nullptr) {
std::cout << "host import of singly mapped direct memory refused: imports after a second mapping not tested\n";
return;
}
const auto first = HostImportSerial(context, address, bytes, true);
Require(first != 0 && HostImportSerial(context, address, bytes, true) == first, "singly mapped direct memory lost its import");
Require(sceKernelMapDirectMemory(&release.alias, page, 0x33, 0, release.physical, page) == 0, "map the first page a second time");
Require(HostImportFor(context, address, bytes) != nullptr, "direct memory was not imported again after a second mapping of its first page");
const auto second = HostImportSerial(context, address, bytes, true);
Require(second != 0 && second != first, "the import of direct memory whose pages were replaced was kept");
HostImportFor(context, address, bytes);
}
#endif
void movedMetadataTests(const Device& device, Recorder& recorder) {
const auto& base = device.GetContext();
@ -6449,6 +6484,12 @@ int main(int argc, char** argv) {
return vertexTargetRangeTests(device, recorder, std::string_view(argv[1]) == "--vertex-target-range-imported") ? 0 : 77;
}
#ifdef _WIN32
if (argc == 2 && std::string_view(argv[1]) == "--host-import-second-mapping-only") {
secondMappingImportTests(device);
return 0;
}
#endif
readTrackingTests(device, recorder);
writeSettledTests(device, recorder);
completionCountTests(device, recorder);
@ -6479,6 +6520,9 @@ int main(int argc, char** argv) {
storeRunTests(device, recorder);
queuedWriteGroupTests();
remappedImportTests(device);
#ifdef _WIN32
secondMappingImportTests(device);
#endif
movedMetadataTests(device, recorder);
viewPastLastMipTests(device, recorder);
keysFillTests(device, recorder);

View File

@ -100,6 +100,9 @@ public:
const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, BlockBytes, nullptr);
Require(section != nullptr, "cannot create shared test memory");
GuestArena::GuestArenaMap_nid_postfix(block, BlockBytes, section, 0, PAGE_READWRITE);
twin = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536));
Require(twin != nullptr, "cannot reserve the second mapping of shared test memory");
GuestArena::GuestArenaMap_nid_postfix(twin, BlockBytes, section, 0, PAGE_READWRITE);
CloseHandle(section);
} else if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes);
} else {
@ -130,6 +133,10 @@ public:
~GuestBlock() {
GuestAllocations::Mutation().Remove(block);
#ifdef _WIN32
if (twin != nullptr) {
GuestArena::GuestArenaReset_nid_postfix(twin, BlockBytes);
GuestArena::GuestArenaRelease_nid_postfix(twin, BlockBytes);
}
if (watched) {
GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes);
GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes);
@ -154,6 +161,7 @@ public:
private:
bool watched;
std::uint8_t* block = nullptr;
std::uint8_t* twin = nullptr;
};
std::array<std::uint32_t, 8> TextureDescriptor(const std::uint8_t* texels, const std::uint8_t* keys) {

View File

@ -30,6 +30,7 @@ void GuestArenaCommit_nid_postfix(void* pointer, std::size_t bytes, std::uint32_
void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes);
#ifdef _WIN32
bool GuestArenaHandleWrite_nid_postfix(std::uintptr_t address);
bool GuestArenaHandleRead_nid_postfix(std::uintptr_t address);
void GuestArenaPinWritable_nid_postfix(const void* pointer, std::size_t bytes);
void GuestArenaUnpinWritable_nid_postfix(const void* pointer, std::size_t bytes);
bool GuestArenaProtection_nid_postfix(std::uintptr_t address, std::uint32_t* protection);
@ -40,8 +41,11 @@ bool GuestArenaHostRegionOverlaps_nid_postfix(std::uintptr_t address, std::size_
std::uint64_t GuestArenaCommitGeneration_nid_postfix();
void GuestArenaSetPrivateMappingObserver_nid_postfix(void (*callback)(std::uintptr_t address, std::size_t bytes, std::uint64_t generation));
void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection);
void GuestArenaMapDirect_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection);
void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes);
void GuestArenaUnmapAlias_nid_postfix(void* alias);
bool GuestArenaResidentSpan_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uintptr_t* start, std::uintptr_t* end);
void GuestArenaForgetSection_nid_postfix(void* section);
#else
void GuestArenaSetSharedBacking_nid_postfix(SharedBackingResolver resolver);
void GuestArenaSetSharedBackingWriter_nid_no_patch(SharedBackingWriter writer);

View File

@ -8,6 +8,7 @@
#include <cstdio>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <mutex>
#include <map>
#include <memory>
@ -74,33 +75,67 @@ public:
reset(reinterpret_cast<std::uintptr_t>(address), bytes);
}
void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection) {
void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection, bool watched = false, bool resident = false) {
std::lock_guard lock(mutex);
auto cursor = reinterpret_cast<std::uintptr_t>(address);
reset(cursor, bytes);
if (resident) watchResident = watched;
HANDLE duplicate = nullptr;
if (!DuplicateHandle(GetCurrentProcess(), section, GetCurrentProcess(), &duplicate, 0, FALSE, DUPLICATE_SAME_ACCESS)) fail("keep shared guest section");
const auto owned = std::make_shared<Section>(duplicate);
for (std::size_t done = 0; done < bytes; done += pageBytes) {
split(cursor + done, pageBytes);
void* page = reinterpret_cast<void*>(cursor + done);
if (!map(section, GetCurrentProcess(), page, offset + done, pageBytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map shared guest page");
MEMORY_BASIC_INFORMATION memory{};
if (VirtualQuery(page, &memory, sizeof(memory)) != sizeof(memory)) fail("query shared guest page");
if (memory.State == MEM_RESERVE && !VirtualAlloc(page, pageBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page");
DWORD previous;
if (!VirtualProtect(page, pageBytes, protection, &previous)) fail("protect shared guest page");
const auto key = std::make_pair(reinterpret_cast<std::uintptr_t>(section), offset + done);
auto shared = physical[key].lock();
const auto slotOf = [&](std::size_t done) -> Physical& { return physical[std::make_pair(reinterpret_cast<std::uintptr_t>(section), offset + done)]; };
const auto pageOf = [](Physical& slot) {
auto shared = slot.page.lock();
if (!shared) {
shared = std::make_shared<SharedPage>();
physical[key] = shared;
slot.page = shared;
}
return shared;
};
const auto single = [&](std::size_t done) {
const auto& slot = slotOf(done);
const auto shared = slot.page.lock();
return resident && !slot.stored && (!shared || shared->aliases.empty());
};
for (std::size_t done = 0; done < bytes;) {
const auto base = cursor + done;
if (single(done)) {
auto runBytes = pageBytes;
while (done + runBytes < bytes && (base + runBytes) % chunkBytes != 0 && single(done + runBytes)) runBytes += pageBytes;
split(base, runBytes);
commitResident(base, runBytes);
residentRuns.emplace(base, Run{base + runBytes, {}, static_cast<std::uint32_t>(runBytes >> 12)});
for (std::size_t at = 0; at < runBytes; at += pageBytes) {
auto& slot = slotOf(done + at);
const auto shared = pageOf(slot);
if (slot.saved) {
std::memcpy(reinterpret_cast<void*>(base + at), slot.saved->bytes + slot.savedOffset, pageBytes);
slot.saved.reset();
}
shared->aliases.push_back(base + at);
views.emplace(base + at, View{shared, protection, {}, 0, owned, offset + done + at, 0, 0, true, &slot});
invalidate(*shared);
}
remember(freshRanges, base, base + runBytes);
DWORD previous;
if (protection != PAGE_READWRITE && !VirtualProtect(reinterpret_cast<void*>(base), runBytes, protection, &previous)) fail("protect resident guest pages");
done += runBytes;
continue;
}
auto& slot = slotOf(done);
const auto shared = pageOf(slot);
const auto aliases = shared->aliases;
for (const auto alias : aliases) {
if (views.at(alias).resident) demote(alias);
}
split(base, pageBytes);
mapShared(base, section, offset + done, protection);
slot.stored = true;
shared->aliases.push_back(base);
views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0});
views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0, 0, false, &slot});
remember(freshRanges, base, base + pageBytes);
invalidate(*shared);
done += pageBytes;
}
}
@ -143,6 +178,10 @@ public:
const auto found = views.find(base);
if (found == views.end() || !writable(found->second.protection)) return false;
auto& view = found->second;
if (view.resident) {
const auto memory = query(address);
return memory.State == MEM_COMMIT && writable(memory.Protect & 0xffu);
}
const auto written = subpagesOf(base, address, address + 1);
invalidate(*view.page, written);
protectSubpages(base, written, view.protection, "resume shared memory write");
@ -150,6 +189,36 @@ public:
return true;
}
bool HandleRead(std::uintptr_t address) {
std::lock_guard lock(mutex);
if (!views.contains(address & ~(pageBytes - 1))) return false;
const auto memory = query(address);
return memory.State == MEM_COMMIT && (memory.Protect & (PAGE_NOACCESS | PAGE_GUARD)) == 0;
}
bool ResidentSpan(std::uintptr_t address, std::size_t bytes, std::uintptr_t* start, std::uintptr_t* end) {
std::lock_guard lock(mutex);
auto run = residentRuns.upper_bound(address);
if (run != residentRuns.begin() && std::prev(run)->second.end > address) --run;
if (run == residentRuns.end() || run->first >= address + bytes) return false;
*start = run->first;
for (; run != residentRuns.end() && run->first < address + bytes; ++run) *end = run->second.end;
return true;
}
void ForgetSection(HANDLE section) {
std::lock_guard lock(mutex);
const auto key = reinterpret_cast<std::uintptr_t>(section);
for (auto slot = physical.lower_bound(std::make_pair(key, std::uint64_t{0})); slot != physical.end() && slot->first.first == key;) {
const auto shared = slot->second.page.lock();
if (shared && !shared->aliases.empty()) {
++slot;
continue;
}
slot = physical.erase(slot);
}
}
bool BeginHostWrite(std::uintptr_t address, std::size_t bytes) {
std::lock_guard lock(mutex);
const auto first = views.lower_bound(address & ~(pageBytes - 1));
@ -187,6 +256,16 @@ public:
return std::runtime_error(text);
};
if (address % pageBytes != 0 || bytes % pageBytes != 0 || bytes == 0) throw refuse("the range is not made of whole shared pages");
for (std::size_t done = 0; done < bytes; done += pageBytes) {
const auto found = views.find(address + done);
if (found == views.end() || !found->second.resident) continue;
const auto run = std::prev(residentRuns.upper_bound(address + done));
if (run->first < address || run->second.end > address + bytes) throw refuse("a resident run of direct memory extends outside the range");
}
for (std::size_t done = 0; done < bytes; done += pageBytes) {
const auto found = views.find(address + done);
if (found != views.end() && found->second.resident) demote(address + done);
}
auto view = views.find(address);
if (view == views.end()) throw refuse("the range does not start at a shared view");
const auto section = view->second.section;
@ -244,6 +323,10 @@ public:
const auto fresh = freshRanges.upper_bound(address);
if ((fresh == freshRanges.begin() || std::prev(fresh)->second <= address) && (fresh == freshRanges.end() || fresh->first >= end)) return true;
for (auto it = views.lower_bound(address & ~(pageBytes - 1)); it != views.end() && it->first < end; ++it) {
if (it->second.resident) {
if (!watchResident) return false;
continue;
}
if (it->second.protection != PAGE_NOACCESS) continue;
for (auto at = std::max(address, it->first); at < std::min(end, it->first + pageBytes); at += 4096) {
if ((it->second.collected & (1u << ((at - it->first) / 4096))) == 0) return false;
@ -261,7 +344,16 @@ public:
}
const auto base = cursor & ~(pageBytes - 1);
const auto found = views.find(base);
if (found != views.end()) {
if (found != views.end() && found->second.resident) {
if (!watchResident) return false;
const auto available = capacity - *count;
if (available == 0) return true;
const auto run = std::prev(residentRuns.upper_bound(cursor));
const auto stop = std::min({end, run->second.end, (cursor & ~std::uintptr_t{4095}) + available * 4096});
collectResident(run->first, run->second, cursor, stop, pages, count, clear, collection);
if (*count == capacity) return true;
cursor = stop;
} else if (found != views.end()) {
auto& view = found->second;
auto stop = std::min(std::min(end, base + pageBytes), nextClean == cleanRanges.end() ? end : nextClean->first);
if (collection != Collection::All) {
@ -309,6 +401,7 @@ public:
std::uint8_t untracked = 0;
for (const auto alias : view.page->aliases) {
auto& other = views.at(alias);
if (other.resident) continue;
const auto arming = static_cast<std::uint8_t>(settled & ~other.armed);
if (!writable(other.protection) || arming == 0) continue;
if (other.hostWrites != 0) {
@ -364,7 +457,7 @@ private:
for (auto cursor = address; cursor < end;) {
const auto base = cursor & ~(pageBytes - 1);
if (const auto found = views.find(base); found != views.end()) {
if (found->second.protection == PAGE_NOACCESS) return false;
if (found->second.protection == PAGE_NOACCESS || (found->second.resident && !watchResident)) return false;
cursor = std::min(end, base + pageBytes);
continue;
}
@ -380,6 +473,7 @@ private:
static_assert(subpageBytes == 4096, "Collect reports one page per subpage");
static constexpr std::size_t subpages = pageBytes / subpageBytes;
static constexpr std::uint8_t allSubpages = (1u << subpages) - 1;
static constexpr std::size_t chunkBytes = 0x200000;
struct SharedPage {
std::array<std::uint64_t, subpages> generation = [] {
std::array<std::uint64_t, subpages> initial{};
@ -396,6 +490,19 @@ private:
Section& operator=(const Section&) = delete;
~Section() { CloseHandle(handle); }
};
struct Saved {
unsigned char* bytes;
explicit Saved(unsigned char* bytes) : bytes(bytes) {}
Saved(const Saved&) = delete;
Saved& operator=(const Saved&) = delete;
~Saved() { VirtualFree(bytes, 0, MEM_RELEASE); }
};
struct Physical {
std::weak_ptr<SharedPage> page;
bool stored = false;
std::shared_ptr<Saved> saved;
std::size_t savedOffset = 0;
};
struct View {
std::shared_ptr<SharedPage> page;
DWORD protection;
@ -405,9 +512,159 @@ private:
std::uint64_t offset;
std::uint32_t hostWrites;
std::uint8_t collected = 0;
bool resident = false;
Physical* slot = nullptr;
};
struct Run {
std::uintptr_t end;
std::array<std::uint64_t, 8> collected;
std::uint32_t fresh;
};
struct Window {
void* view = nullptr;
unsigned char* bytes = nullptr;
};
using Ranges = std::map<std::uintptr_t, std::uintptr_t>;
static bool runCollected(const Run& run, std::size_t bit) {
return (run.collected[bit >> 6] >> (bit & 63) & 1) != 0;
}
void collectResident(std::uintptr_t start, Run& run, std::uintptr_t from, std::uintptr_t stop, void** pages, std::size_t* count, bool clear, Collection collection) {
const auto first = from & ~std::uintptr_t{4095};
std::vector<std::uintptr_t> written;
if (collection != Collection::Fresh) {
std::vector<void*> found((stop - first + 4095) / 4096);
ULONG_PTR reported = found.size();
DWORD granularity = 0;
if (GetWriteWatch(clear ? WRITE_WATCH_FLAG_RESET : 0, reinterpret_cast<void*>(first), stop - first, found.data(), &reported, &granularity) != 0) fail("collect resident guest writes");
for (ULONG_PTR index = 0; index < reported; ++index) written.push_back(reinterpret_cast<std::uintptr_t>(found[index]));
}
auto next = written.begin();
std::uintptr_t resetFrom = 0;
const auto flushReset = [&](std::uintptr_t to) {
if (resetFrom != 0 && ResetWriteWatch(reinterpret_cast<void*>(resetFrom), to - resetFrom) != 0) fail("reset resident guest writes");
resetFrom = 0;
};
for (auto at = first; at < stop; at += 4096) {
const auto bit = (at - start) >> 12;
const bool fresh = !runCollected(run, bit);
bool wrote = false;
while (next != written.end() && *next < at) ++next;
if (next != written.end() && *next == at) wrote = true;
const bool report = collection == Collection::Fresh ? fresh : collection == Collection::Armed ? !fresh && wrote : fresh || wrote;
if (report) pages[(*count)++] = reinterpret_cast<void*>(std::max(at, from));
const bool settle = clear && fresh && collection != Collection::Armed;
if (settle) {
run.collected[bit >> 6] |= 1ull << (bit & 63);
--run.fresh;
forget(freshRanges, at, at + 4096);
if (collection == Collection::Fresh && resetFrom == 0) resetFrom = at;
} else if (collection == Collection::Fresh) {
flushReset(at);
}
}
if (collection == Collection::Fresh) flushReset(stop);
}
Window open(HANDLE section, std::uint64_t offset, std::size_t bytes) {
const auto lead = offset % granularity;
Window window;
window.view = map(section, GetCurrentProcess(), nullptr, offset - lead, lead + bytes, 0, PAGE_READWRITE, nullptr, 0);
if (window.view == nullptr) fail("open a window on direct memory");
window.bytes = static_cast<unsigned char*>(window.view) + lead;
return window;
}
void close(Window& window) {
if (window.view != nullptr && !unmap(GetCurrentProcess(), window.view, 0)) fail("close a window on direct memory");
window = {};
}
void commitResident(std::uintptr_t base, std::size_t bytes) {
const DWORD flags = MEM_RESERVE | MEM_COMMIT | MEM_REPLACE_PLACEHOLDER | (watchResident ? MEM_WRITE_WATCH : 0);
if (!allocate(GetCurrentProcess(), reinterpret_cast<void*>(base), bytes, flags, PAGE_READWRITE, nullptr, 0)) fail("replace guest placeholder with resident direct memory");
}
void mapShared(std::uintptr_t base, HANDLE section, std::uint64_t offset, DWORD protection) {
void* page = reinterpret_cast<void*>(base);
if (!map(section, GetCurrentProcess(), page, offset, pageBytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map shared guest page");
if (query(base).State == MEM_RESERVE && !VirtualAlloc(page, pageBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page");
DWORD previous;
if (!VirtualProtect(page, pageBytes, protection, &previous)) fail("protect shared guest page");
}
static bool blank(std::uintptr_t page) {
const auto* words = reinterpret_cast<const std::uint64_t*>(page);
for (std::size_t i = 0; i < pageBytes / sizeof(std::uint64_t); ++i) {
if (words[i] != 0) return false;
}
return true;
}
void demote(std::uintptr_t address) {
const auto run = std::prev(residentRuns.upper_bound(address));
const auto start = run->first;
const auto stop = run->second.end;
DWORD previous;
if (!VirtualProtect(reinterpret_cast<void*>(start), stop - start, PAGE_READONLY, &previous)) fail("fence resident guest pages");
const auto section = views.at(start).section;
auto window = open(section->handle, views.at(start).offset, stop - start);
if (query(reinterpret_cast<std::uintptr_t>(window.bytes)).State == MEM_RESERVE && !VirtualAlloc(window.bytes, stop - start, MEM_COMMIT, PAGE_READWRITE)) fail("commit direct memory pages");
std::memcpy(window.bytes, reinterpret_cast<const void*>(start), stop - start);
close(window);
if (!VirtualFree(reinterpret_cast<void*>(start), stop - start, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release resident guest pages");
for (auto page = start; page < stop; page += pageBytes) {
auto& view = views.at(page);
split(page, pageBytes);
mapShared(page, view.section->handle, view.offset, view.protection);
view.slot->stored = true;
view.resident = false;
view.seen.fill(0);
view.armed = 0;
view.collected = 0;
for (std::size_t sub = 0; sub < subpages; ++sub) {
if (runCollected(run->second, ((page - start) >> 12) + sub)) view.collected |= static_cast<std::uint8_t>(1u << sub);
}
invalidate(*view.page);
}
residentRuns.erase(run);
}
void breakRun(std::uintptr_t at) {
const auto next = residentRuns.upper_bound(at);
if (next == residentRuns.begin()) return;
const auto run = std::prev(next);
if (at > run->first && at < run->second.end) demote(at);
}
void evict(std::map<std::uintptr_t, Run>::iterator run) {
const auto start = run->first;
const auto stop = run->second.end;
DWORD previous;
if (!VirtualProtect(reinterpret_cast<void*>(start), stop - start, PAGE_READONLY, &previous)) fail("open resident guest pages");
std::shared_ptr<Saved> saved;
for (auto page = start; page < stop; page += pageBytes) {
const auto found = views.find(page);
auto& view = found->second;
if (!blank(page)) {
if (!saved) {
auto* bytes = static_cast<unsigned char*>(VirtualAlloc(nullptr, stop - start, MEM_RESERVE, PAGE_READWRITE));
if (bytes == nullptr) fail("reserve unmapped direct memory");
saved = std::make_shared<Saved>(bytes);
}
if (!VirtualAlloc(saved->bytes + (page - start), pageBytes, MEM_COMMIT, PAGE_READWRITE)) fail("keep unmapped direct memory");
std::memcpy(saved->bytes + (page - start), reinterpret_cast<const void*>(page), pageBytes);
view.slot->saved = saved;
view.slot->savedOffset = page - start;
}
std::erase(view.page->aliases, page);
views.erase(found);
}
forget(freshRanges, start, stop);
residentRuns.erase(run);
}
static void forget(Ranges& ranges, std::uintptr_t start, std::uintptr_t end) {
auto it = ranges.lower_bound(start);
if (it != ranges.begin() && std::prev(it)->second > start) --it;
@ -490,6 +747,9 @@ private:
unmap = reinterpret_cast<UnmapFunction>(GetProcAddress(module, "UnmapViewOfFile2"));
if (!allocate || !map || !unmap) throw std::runtime_error("Windows placeholder memory APIs are required");
compare = reinterpret_cast<CompareFunction>(GetProcAddress(module, "CompareObjectHandles"));
SYSTEM_INFO system{};
GetSystemInfo(&system);
granularity = system.dwAllocationGranularity;
}
[[noreturn]] static void fail(const char* operation) {
@ -518,6 +778,8 @@ private:
void reset(std::uintptr_t address, std::size_t bytes) {
const auto end = address + bytes;
forgetClean(address, end);
breakRun(address);
breakRun(end);
for (auto cursor = address; cursor < end;) {
const auto memory = query(cursor);
if (memory.State == MEM_RESERVE) {
@ -541,6 +803,7 @@ private:
forget(freshRanges, cursor, cursor + pageBytes);
}
} else if (memory.Type == MEM_PRIVATE) {
if (const auto run = residentRuns.find(cursor); run != residentRuns.end()) evict(run);
if (!VirtualFree(reinterpret_cast<void*>(cursor), allocationEnd - cursor, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory");
} else {
throw std::runtime_error("unsupported guest mapping type");
@ -594,8 +857,11 @@ private:
Ranges cleanRanges;
Ranges freshRanges;
std::map<std::uintptr_t, View> views;
std::map<std::pair<std::uintptr_t, std::uint64_t>, std::weak_ptr<SharedPage>> physical;
std::map<std::pair<std::uintptr_t, std::uint64_t>, Physical> physical;
std::map<std::uintptr_t, Run> residentRuns;
std::map<std::uintptr_t, std::size_t> splitAliases;
std::size_t granularity = 0x10000;
bool watchResident = false;
std::mutex mutex;
AllocateFunction allocate = nullptr;
MapFunction map = nullptr;

View File

@ -320,6 +320,10 @@ bool GuestArenaHandleWrite_nid_postfix(std::uintptr_t address) {
return WindowsMappings::Get().HandleWrite(address);
}
bool GuestArenaHandleRead_nid_postfix(std::uintptr_t address) {
return WindowsMappings::Get().HandleRead(address);
}
void GuestArenaPinWritable_nid_postfix(const void* pointer, std::size_t bytes) {
if (!Arena::Get().Contains(pointer, bytes)) return;
WindowsMappings::Get().Pin(reinterpret_cast<std::uintptr_t>(pointer), bytes);
@ -374,7 +378,12 @@ void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes) {
void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection) {
if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("shared mapping", pointer, bytes);
WindowsMappings::Get().Map(pointer, bytes, section, offset, protection);
WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched(), false);
}
void GuestArenaMapDirect_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection) {
if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("direct memory mapping", pointer, bytes);
WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched(), true);
}
void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes) {
@ -384,6 +393,14 @@ void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes)
void GuestArenaUnmapAlias_nid_postfix(void* alias) {
WindowsMappings::Get().UnmapAlias(alias);
}
bool GuestArenaResidentSpan_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uintptr_t* start, std::uintptr_t* end) {
return WindowsMappings::Get().ResidentSpan(address, bytes, start, end);
}
void GuestArenaForgetSection_nid_postfix(void* section) {
WindowsMappings::Get().ForgetSection(section);
}
#endif
#if defined(__linux__)

View File

@ -246,6 +246,7 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) {
static std::atomic<bool> reported{false};
const auto* fault = info->ExceptionRecord;
if (fault->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && fault->NumberParameters >= 2 && fault->ExceptionInformation[0] == 1 && GuestArena::GuestArenaHandleWrite_nid_postfix(fault->ExceptionInformation[1])) return EXCEPTION_CONTINUE_EXECUTION;
if (fault->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && fault->NumberParameters >= 2 && fault->ExceptionInformation[0] == 0 && GuestArena::GuestArenaHandleRead_nid_postfix(fault->ExceptionInformation[1])) return EXCEPTION_CONTINUE_EXECUTION;
if (HandleWatch(info)) return EXCEPTION_CONTINUE_EXECUTION;
if (HandleSse4a(info)) return EXCEPTION_CONTINUE_EXECUTION;
const auto* record = info->ExceptionRecord;

View File

@ -302,6 +302,7 @@ public:
~PhysicalBacking() {
#ifdef _WIN32
GuestArena::GuestArenaForgetSection_nid_postfix(section);
CloseHandle(section);
#else
if (hostWriteView != nullptr) ::munmap(hostWriteView, bytes);
@ -313,7 +314,7 @@ public:
void Map(std::uintptr_t address, std::size_t bytes, std::uint64_t offset, int protection) const {
#ifdef _WIN32
GuestArena::GuestArenaMap_nid_postfix(reinterpret_cast<void*>(address), bytes, section, offset, WinProtFromPosix(protection));
GuestArena::GuestArenaMapDirect_nid_postfix(reinterpret_cast<void*>(address), bytes, section, offset, WinProtFromPosix(protection));
#else
if (::mmap(reinterpret_cast<void*>(address), bytes, protection, MAP_SHARED | MAP_FIXED, file, static_cast<off_t>(fileOffset + offset)) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map direct memory backing");
#endif
@ -552,11 +553,44 @@ void AddMapping(std::uintptr_t address, std::size_t len, std::uint64_t phys, int
#endif
}
void RequireResidentUnpinned(GuestAllocations::Mutation& mutation, std::uintptr_t address, std::size_t len) {
#ifdef _WIN32
std::uintptr_t start = 0;
std::uintptr_t end = 0;
if (GuestArena::GuestArenaResidentSpan_nid_postfix(address, len, &start, &end)) mutation.RequireUnpinned(reinterpret_cast<const void*>(start), end - start);
#else
static_cast<void>(mutation);
static_cast<void>(address);
static_cast<void>(len);
#endif
}
void RequireAliasesUnpinned(GuestAllocations::Mutation& mutation, std::uint64_t phys, std::size_t len) {
#ifdef _WIN32
std::vector<std::pair<std::uintptr_t, std::size_t>> aliases;
{
std::lock_guard lock(g_directLock);
for (const auto& [base, mapping] : g_directMappings) {
const auto first = std::max(phys, mapping.phys);
const auto last = std::min(phys + len, mapping.phys + (mapping.end - base));
if (first < last) aliases.emplace_back(base + (first - mapping.phys), last - first);
}
}
for (const auto& [address, bytes] : aliases) RequireResidentUnpinned(mutation, address, bytes);
#else
static_cast<void>(mutation);
static_cast<void>(phys);
static_cast<void>(len);
#endif
}
bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, size_t len, int prot, int flags, int64_t physStart = -1) {
constexpr int GuestMapFixed = 0x10;
constexpr int GuestMapNoOverwrite = 0x80;
if (addr == nullptr || (flags & GuestMapFixed) == 0 || (flags & GuestMapNoOverwrite) != 0 || !mutation.Covers(addr, len)) return false;
ValidateRange(addr, len, PS5_PAGE_SIZE);
RequireResidentUnpinned(mutation, reinterpret_cast<std::uintptr_t>(addr), len);
if (physStart >= 0) RequireAliasesUnpinned(mutation, static_cast<std::uint64_t>(physStart), len);
Trace("remap fixed %p+0x%zx prot=0x%x phys=0x%llx", addr, len, prot, static_cast<long long>(physStart));
const auto nativeProtection = LinuxProtFromSce(prot);
mutation.Protect(addr, len, (prot & 3) != 0, (prot & 2) != 0, (prot & GuestProtGpuReadWrite) != 0, [&] {
@ -798,6 +832,7 @@ bool Reserved(const void* addr, size_t len) {
}
void UnmapRegistered(GuestAllocations::Mutation& mutation, void* addr, size_t len) {
RequireResidentUnpinned(mutation, reinterpret_cast<std::uintptr_t>(addr), len);
mutation.Unmap(addr, len, [&](const void* piece, std::size_t pieceBytes, const void* allocation, bool last) {
auto* pieceAddress = const_cast<void*>(piece);
std::lock_guard lock(g_directLock);
@ -847,6 +882,7 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart,
return 0;
}
ReplaceFixedOverlap(mutation, *addr, len, flags);
RequireAliasesUnpinned(mutation, static_cast<std::uint64_t>(physStart), len);
std::lock_guard lock(g_directLock);
ValidatePhysicalRange(static_cast<std::uint64_t>(physStart), len);
void* mapped = MapAligned(*addr, len, PROT_NONE, flags, alignment);

View File

@ -37,6 +37,7 @@
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
#endif
extern "C" {
@ -1318,6 +1319,198 @@ static void CheckHeapAfterMappingReuse() {
GuestHeap::GuestHeapFree_nid_postfix(pointer);
}
#ifdef _WIN32
static int g_accessViolations = 0;
static LONG WINAPI CountAccessViolations(EXCEPTION_POINTERS* info) {
if (info->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) ++g_accessViolations;
return EXCEPTION_CONTINUE_SEARCH;
}
#endif
static void CheckSingleDirectMappingTracksWritesWithoutFaults() {
#ifdef _WIN32
constexpr std::size_t page = 0x4000;
std::int64_t phys = 0;
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 4, 0, 0, &phys) == 0);
void* mapped = nullptr;
Require(sceKernelMapDirectMemory(&mapped, page * 4, 3, 0, phys, 0) == 0);
const auto collect = [](void* address, std::size_t bytes) {
std::array<void*, 32> pages{};
std::size_t count = pages.size();
Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast<std::uintptr_t>(address), bytes, pages.data(), &count, true));
return count;
};
auto* bytes = static_cast<volatile unsigned char*>(mapped);
Require(collect(mapped, page * 4) == 16);
Require(collect(mapped, page * 4) == 0);
void* const handler = AddVectoredExceptionHandler(1, CountAccessViolations);
Require(handler != nullptr);
bytes[page + 0x1000] = 7;
bytes[page * 3] = 9;
Require(RemoveVectoredExceptionHandler(handler) != 0);
Require(g_accessViolations == 0);
Require(collect(mapped, page * 4) == 2);
Require(collect(mapped, page * 4) == 0);
Require(sceKernelMunmap(static_cast<unsigned char*>(mapped) + page * 2, page) == 0);
Require(bytes[page + 0x1000] == 7 && bytes[page * 3] == 9);
bytes[0] = 5;
Require(collect(mapped, page * 2) != 0);
Require(collect(mapped, page * 2) == 0);
void* second = nullptr;
Require(sceKernelMapDirectMemory(&second, page, 3, 0, phys + page, 0) == 0);
Require(static_cast<unsigned char*>(second)[0x1000] == 7);
static_cast<volatile unsigned char*>(second)[0x1001] = 11;
Require(bytes[page + 0x1001] == 11);
Require(collect(mapped, page * 2) != 0);
Require(sceKernelMunmap(second, page) == 0);
Require(sceKernelMunmap(mapped, page * 2) == 0);
Require(sceKernelMunmap(static_cast<unsigned char*>(mapped) + page * 3, page) == 0);
void* again = nullptr;
Require(sceKernelMapDirectMemory(&again, page * 4, 3, 0, phys, 0) == 0);
const auto* data = static_cast<const unsigned char*>(again);
Require(data[0] == 5 && data[page + 0x1000] == 7 && data[page + 0x1001] == 11 && data[page * 2] == 0 && data[page * 3] == 9);
Require(sceKernelMunmap(again, page * 4) == 0);
Require(sceKernelReleaseDirectMemory(phys, page * 4) == 0);
#endif
}
static void CheckSecondDirectMappingWaitsForLeases() {
#ifdef _WIN32
constexpr std::size_t page = 0x4000;
std::int64_t phys = 0;
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 4, 0, 0, &phys) == 0);
void* mapped = nullptr;
Require(sceKernelMapDirectMemory(&mapped, page * 4, 3, 0, phys, 0) == 0);
auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix();
Require(std::count_if(lease.begin(), lease.end(), [&](const auto& range) { return range->address == reinterpret_cast<std::uintptr_t>(mapped); }) == 1);
std::atomic<bool> released{false};
std::thread holder([&] {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
released = true;
lease.clear();
});
void* second = nullptr;
Require(sceKernelMapDirectMemory(&second, page, 3, 0, phys + page, 0) == 0);
Require(released);
holder.join();
static_cast<volatile unsigned char*>(second)[8] = 3;
Require(static_cast<unsigned char*>(mapped)[page + 8] == 3);
static_cast<volatile unsigned char*>(mapped)[page * 2 + 8] = 4;
Require(sceKernelMunmap(second, page) == 0);
Require(sceKernelMunmap(mapped, page * 4) == 0);
Require(sceKernelReleaseDirectMemory(phys, page * 4) == 0);
#endif
}
#ifdef _WIN32
static void RequireResidentMapping(const void* address) {
MEMORY_BASIC_INFORMATION info{};
Require(VirtualQuery(address, &info, sizeof(info)) == sizeof(info));
Require(info.State == MEM_COMMIT && info.Type == MEM_PRIVATE);
}
#endif
static void CheckBreakingAResidentRunKeepsConcurrentAccesses() {
#ifdef _WIN32
constexpr std::size_t page = 0x4000;
constexpr std::size_t chunk = 0x200000;
constexpr std::size_t chunks = 4;
for (int round = 0; round < 20; ++round) {
const bool unmap = round % 2 != 0;
std::int64_t phys = 0;
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, chunk * chunks, 0, 0, &phys) == 0);
void* mapped = nullptr;
Require(sceKernelMapDirectMemory(&mapped, chunk * chunks, 3, 0, phys, 0) == 0);
RequireResidentMapping(mapped);
auto* memory = static_cast<volatile unsigned char*>(mapped);
const auto value = [&](std::size_t at) { return static_cast<unsigned char>((at * 31 + static_cast<std::size_t>(round)) | 1); };
const auto skipped = [&](std::size_t offset) { return unmap && offset >= page && offset < page * 2; };
std::atomic<bool> go{false};
std::atomic<bool> done{false};
std::atomic<bool> consistent{true};
std::thread writer([&] {
while (!go) {}
for (std::size_t offset = 0; offset < chunk; ++offset) {
if (skipped(offset)) continue;
for (std::size_t index = 0; index < chunks; ++index) memory[index * chunk + offset] = value(index * chunk + offset);
}
});
std::thread reader([&] {
while (!go) {}
while (!done) {
for (std::size_t offset = 0; offset < chunk && !done; offset += 61) {
if (skipped(offset)) continue;
for (std::size_t index = 0; index < chunks; ++index) {
const unsigned char seen = memory[index * chunk + offset];
if (seen != 0 && seen != value(index * chunk + offset)) consistent = false;
}
}
}
});
std::array<void*, chunks> second{};
go = true;
for (std::size_t index = 0; index < chunks; ++index) {
if (unmap) Require(sceKernelMunmap(static_cast<unsigned char*>(mapped) + index * chunk + page, page) == 0);
else Require(sceKernelMapDirectMemory(&second[index], page, 3, 0, phys + static_cast<std::int64_t>(index * chunk + page), 0) == 0);
}
writer.join();
done = true;
reader.join();
Require(consistent);
for (std::size_t index = 0; index < chunks; ++index) {
for (std::size_t offset = 0; offset < chunk; ++offset) {
if (!skipped(offset)) Require(memory[index * chunk + offset] == value(index * chunk + offset));
}
if (unmap) continue;
for (std::size_t offset = 0; offset < page; ++offset) Require(static_cast<unsigned char*>(second[index])[offset] == value(index * chunk + page + offset));
Require(sceKernelMunmap(second[index], page) == 0);
}
for (std::size_t index = 0; index < chunks; ++index) {
auto* base = static_cast<unsigned char*>(mapped) + index * chunk;
if (!unmap) {
Require(sceKernelMunmap(base, chunk) == 0);
continue;
}
Require(sceKernelMunmap(base, page) == 0);
Require(sceKernelMunmap(base + page * 2, chunk - page * 2) == 0);
}
Require(sceKernelReleaseDirectMemory(phys, chunk * chunks) == 0);
}
#endif
}
static void CheckRemappedDirectMemoryIsChargedOnce() {
#ifdef _WIN32
constexpr std::size_t bytes = 0x10000000;
const auto committed = [] {
PERFORMANCE_INFORMATION info{};
info.cb = sizeof(info);
Require(K32GetPerformanceInfo(&info, sizeof(info)) != 0);
return static_cast<std::int64_t>(info.CommitTotal) * static_cast<std::int64_t>(info.PageSize);
};
bool once = false;
for (int attempt = 0; attempt < 3 && !once; ++attempt) {
const auto before = committed();
std::int64_t phys = 0;
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &phys) == 0);
void* mapped = nullptr;
Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, phys, 0) == 0);
RequireResidentMapping(mapped);
std::memset(mapped, 0xa5, bytes);
Require(sceKernelMunmap(mapped, bytes) == 0);
void* again = nullptr;
Require(sceKernelMapDirectMemory(&again, bytes, 3, 0, phys, 0) == 0);
const auto* data = static_cast<const unsigned char*>(again);
Require(data[0] == 0xa5 && data[bytes / 2] == 0xa5 && data[bytes - 1] == 0xa5);
once = committed() - before < static_cast<std::int64_t>(bytes + bytes / 2);
Require(sceKernelMunmap(again, bytes) == 0);
Require(sceKernelReleaseDirectMemory(phys, bytes) == 0);
}
Require(once);
#endif
}
static void CheckSharedWriteTracking() {
#ifdef _WIN32
constexpr std::size_t page = 0x4000;
@ -1535,7 +1728,9 @@ static void CheckPinnedSharedPages() {
std::int64_t phys = 0;
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 3, 0, 0, &phys) == 0);
void* mapped = nullptr;
void* alias = nullptr;
Require(sceKernelMapDirectMemory(&mapped, page * 3, 3, 0, phys, 0) == 0);
Require(sceKernelMapDirectMemory(&alias, page * 3, 3, 0, phys, 0) == 0);
auto* bytes = static_cast<volatile unsigned char*>(mapped);
const auto collect = [&] {
std::array<void*, 32> pages{};
@ -1567,6 +1762,7 @@ static void CheckPinnedSharedPages() {
Require(protection(page) == PAGE_READONLY);
bytes[page + 8] = 11;
Require(collect() == 1);
Require(sceKernelMunmap(alias, page * 3) == 0);
Require(sceKernelMunmap(mapped, page * 3) == 0);
Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0);
#endif
@ -2229,6 +2425,10 @@ int main() {
CheckReadsIntoSharedWriteTracking();
CheckPinnedSharedPages();
CheckFailedCollectKeepsWrites();
CheckSingleDirectMappingTracksWritesWithoutFaults();
CheckSecondDirectMappingWaitsForLeases();
CheckBreakingAResidentRunKeepsConcurrentAccesses();
CheckRemappedDirectMemoryIsChargedOnce();
#if defined(__linux__)
CheckWriteWatch();
CheckWriteWatchArmedCollect();

View File

@ -141,6 +141,8 @@ int main(int argc, char** argv) {
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &physical) == 0, "allocate direct memory");
void* mapping = nullptr;
Require(sceKernelMapDirectMemory(&mapping, page * 2, 3, 0, physical, 0) == 0, "map read/write memory");
void* twin = nullptr;
Require(sceKernelMapDirectMemory(&twin, page * 2, 3, 0, physical, 0) == 0, "map read/write memory a second time");
auto* buffer = static_cast<char*>(mapping) + page - 7;
const auto collect = [&] {
std::array<void*, 16> pages{};
@ -199,6 +201,9 @@ int main(int argc, char** argv) {
void* outputMapping = nullptr;
Require(sceKernelMapDirectMemory(&outputMapping, page * 2, 3, 0, outputPhysical, 0) == 0,
"map sender output memory");
void* outputTwin = nullptr;
Require(sceKernelMapDirectMemory(&outputTwin, page * 2, 3, 0, outputPhysical, 0) == 0,
"map sender output memory a second time");
const auto collectOutputs = [&] {
std::array<void*, 16> pages{};
std::size_t count = pages.size();
@ -238,6 +243,7 @@ int main(int argc, char** argv) {
if (mask & 1) Require(Protection(address) == PAGE_READONLY, "sender address rearmed");
if (mask & 2) Require(Protection(length) == PAGE_READONLY, "sender length rearmed");
}
Require(sceKernelMunmap(outputTwin, page * 2) == 0, "unmap the second sender output mapping");
Require(sceKernelMunmap(outputMapping, page * 2) == 0, "unmap sender outputs");
Require(sceKernelReleaseDirectMemory(outputPhysical, page * 2) == 0, "release sender output memory");
}
@ -300,6 +306,7 @@ int main(int argc, char** argv) {
Require(Protection(buffer) == PAGE_READONLY, "completed receive can be rearmed");
Require((net ? sceNetSocketClose(sender) : close_nid_postfix(sender)) == 0, "close sender");
Require((net ? sceNetSocketClose(receiver) : close_nid_postfix(receiver)) == 0, "close receiver");
Require(sceKernelMunmap(twin, page * 2) == 0, "unmap the second direct memory mapping");
Require(sceKernelMunmap(mapping, page * 2) == 0, "unmap direct memory");
Require(sceKernelReleaseDirectMemory(physical, page * 2) == 0, "release direct memory");
}

View File

@ -11,10 +11,6 @@ Entries about one library or component are in [debt](debt/), one file per librar
- libstdc++-6.dll
- libwinpthread-1.dll
### Direct memory
- Windows direct-memory backing reserves a paging-file section and commits each mapped guest page before setting its protection. Committed physical contents remain in the section across unmap/remap and are released when the backing and its remaining views are destroyed; commitment is not recovered merely by unmapping a page. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified.
### Silent stubs
Throughout the project, every function at every stage either **does exactly what it's supposed to or throws an exception**. Everywhere... except:

View File

@ -28,6 +28,7 @@
### Functional
- Windows direct-memory backing reserves a paging-file section. A [direct memory page](../../../core/libs/prx/libc/include/WindowsMappings.hpp) mapped at one guest address is private write-watched memory at that address and is not committed in the section; a page mapped at a second address moves its whole resident run into the section (committed there) and every address becomes a view of it, tracked by write protection. An unmapped resident page that is not all zeros is kept in a private reservation until it is mapped again. Committed section contents remain across unmap/remap and are released when the backing and its remaining views are destroyed. A host import alias over part of a resident run is refused. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified.
- [getc_unlocked, putc_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified.
- [ftrylockfile](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it
- [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported

View File

@ -97,7 +97,7 @@ Use the generated files printed as `Guest module:` for bundled title modules. `l
On Windows, a self-built `libs/` also needs `libgcc_s_seh-1.dll`, `libstdc++-6.dll` and `libwinpthread-1.dll` from the `mingw64/bin` directory of the toolchain that built the libraries; the release archives already contain them. The libraries are loaded without searching `PATH`, so a copy elsewhere on the system is not used, and a library that needs one of them fails with error 126 (the specified module could not be found) although its `.prx` file is present.
On Windows, direct memory (`sceKernelAllocateDirectMemory`, up to 13824 MiB per title) is committed in full when the title allocates it, not when its pages are first used. The system commit limit (installed memory plus page file size, the second value of Committed in Task Manager) must cover it together with all other committed memory. Otherwise the allocation throws `create direct memory backing of 0x<n> bytes (<m> MiB)` with the Windows error; enlarge the page file or close other applications.
On Windows, direct memory (`sceKernelAllocateDirectMemory`, up to 13824 MiB per title) is committed when the title maps it, not when the title allocates it or first uses its pages. The system commit limit (installed memory plus page file size, the second value of Committed in Task Manager) must cover the mapped direct memory together with all other committed memory. Otherwise `sceKernelMapDirectMemory` throws `replace guest placeholder with resident direct memory` with the Windows error (`commit direct memory pages` when a page is mapped at a second address, `keep unmapped direct memory` when it is unmapped); enlarge the page file or close other applications.
On Linux, when the Vulkan driver imports dma-buf memory (not the NVIDIA proprietary driver), shared direct memory is imported through `/dev/udmabuf`, and the user who runs the game needs read-write access to it. Many distributions create it as `root:kvm` with mode `0660`: add the user to the `kvm` group and log in again (an ACL such as `setfacl -m u:$USER:rw /dev/udmabuf` lasts until the next reboot). Without access, startup prints `[gpu] open /dev/udmabuf: Permission denied`, these ranges are copied instead of imported, and GPU stores to them through FLAT/GLOBAL addresses fail with `BDA access failed`. Ranges above udmabuf's `size_limit_mb` (64 MiB by default) are copied as well.