perf(agc-driver): join the imports of adjacent large ranges when a buffer is bound across them

8 MiB window, so a range larger than the window is always imported alone. A
buffer bound across two such ranges then has no import and is copied whole on
every use: PPSA25344 binds a 368 MiB buffer across a 320.75 MiB and a 47.3 MiB
range and copies it on almost every frame.

When UploadFinish meets such a region, it imports the run of the ranges the
region crosses, provided every one of them is larger than the window, and
retires their own imports. It does this before any region of the build
confirms an import, so the retire moves the epoch ahead of every pointer the
build would use. A range that large is registered whole, not grown in 64 KiB
pieces like a heap, so the joined import is not re-imported as a heap grows; it
stands in refreshImports while exactly its ranges stand. A later join that
reaches a joined import takes it in, so two joins that share a range never
retire each other in turn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlxcBTW 2026-10-12 06:00:43 +02:00
parent 99e4dfefe7
commit 230b5c2683
4 changed files with 152 additions and 12 deletions

View File

@ -33,6 +33,7 @@ struct HostImport {
std::uint64_t serial = 0;
bool unwatched = false;
bool dmaBuf = false;
bool joined = false;
std::shared_ptr<void> chunk;
};

View File

@ -161,6 +161,7 @@ struct HostImports {
std::map<VkDevice, std::vector<std::shared_ptr<RetiredImport>>> parked;
std::uint64_t liveBytes = 0;
std::map<std::uint64_t, ImportFailure> failed;
std::map<std::uint64_t, ImportFailure> failedJoins;
std::set<std::uint64_t> unaliasedSpans;
// Registry generation the imports were last reconciled with.
std::uint64_t refreshedGeneration = 0;
@ -210,12 +211,13 @@ struct RetiredImport {
const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end);
bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end);
bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end);
void retireImport(VkDevice device, HostImports& state, std::map<std::uint64_t, HostImport>::iterator it, const GuestAllocations::Lease& lease, bool hold = true) {
// Results shadowed for the import reach its (old) buffer first where the memory is still a
// readable registered range (the import retires because its registration vanished or changed
// size); the holder below outlives the batch that copies them.
RetireShadow(device, it->second, [&lease](std::uint64_t begin, std::uint64_t end) { return runCovers(lease, begin, end); });
RetireShadow(device, it->second, [&lease, joined = it->second.joined](std::uint64_t begin, std::uint64_t end) { return runCovers(lease, begin, end) || (joined && joinedCovers(lease, begin, end)); });
++state.epoch;
state.liveBytes -= it->second.bytes;
auto holder = std::make_shared<RetiredImport>(state.device, state.destroyBuffer, state.freeMemory, state.imports.extract(it));
@ -630,6 +632,27 @@ bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::u
return !run.ranges.empty() && end <= run.base + run.bytes;
}
ImportRun joinedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
const auto window = importWindow.load(std::memory_order_relaxed);
auto last = leasedIndexContaining(lease, begin);
if (window == 0 || last == lease.size()) return {};
const auto first = last;
for (;;) {
if (lease[last]->bytes <= window) return {};
if (lease[last]->address + lease[last]->bytes >= end) break;
if (last + 1 == lease.size() || !continuesRun(*lease[last], *lease[last + 1])) return {};
++last;
}
if (first == last) return {};
ImportRun run{lease[first]->address, lease[last]->address + lease[last]->bytes - lease[first]->address, {}};
run.ranges.assign(lease.begin() + static_cast<std::ptrdiff_t>(first), lease.begin() + static_cast<std::ptrdiff_t>(last) + 1);
return run;
}
bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
return !joinedRun(lease, begin, end).ranges.empty();
}
template<typename Entry>
bool sameRun(const Entry& entry, const ImportRun& run) {
if (run.base != entry.base || run.bytes != entry.bytes || run.ranges.size() != entry.ranges.size()) return false;
@ -652,20 +675,42 @@ bool wouldRetireImport(const HostImports& state, const ImportRun& run) {
}
bool sameRange(const HostImport& entry, const GuestAllocations::Lease& lease) {
return sameRun(entry, importRun(lease, entry.base));
return sameRun(entry, entry.joined ? joinedRun(lease, entry.base, entry.base + entry.bytes) : importRun(lease, entry.base));
}
const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end);
bool importsStale(const Context& context, const HostImports& state);
const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease);
const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t address, const GuestAllocations::Lease& lease) {
if (!importsStale(context, state)) {
if (const auto* current = findImport(state, address, address + 1)) return current;
} else if (const auto* current = findImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) {
return current;
}
auto run = importRun(lease, address);
if (run.ranges.empty()) return nullptr;
return importRunAt(context, state, std::move(run), false, address, lease);
}
const HostImport* importJoined(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) {
if (const auto* low = findImport(state, begin, begin + 1); low != nullptr && low->joined) begin = low->base;
if (const auto* high = findImport(state, end - 1, end); high != nullptr && high->joined) end = high->base + high->bytes;
auto run = joinedRun(lease, begin, end);
if (run.ranges.empty()) return nullptr;
if (const auto failed = state.failedJoins.find(run.base); failed != state.failedJoins.end() && sameRun(failed->second, run)) return nullptr;
return importRunAt(context, state, std::move(run), true, begin, lease);
}
const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease) {
const auto base = run.base;
const auto bytes = run.bytes;
auto& failures = joined ? state.failedJoins : state.failed;
const auto alignment = context.hostImportAlignment;
if (joined && (alignment == 0 || base % alignment != 0 || bytes % alignment != 0)) {
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end())});
return nullptr;
}
if (const auto found = state.imports.find(base); found != state.imports.end()) {
if (sameRun(found->second, run)) return &found->second;
retireImport(context.device, state, found, lease);
@ -676,11 +721,10 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
it = next;
}
if (auto it = state.imports.lower_bound(base); it != state.imports.begin() && std::prev(it)->second.base + std::prev(it)->second.bytes > base) retireImport(context.device, state, std::prev(it), lease);
const auto alignment = context.hostImportAlignment;
if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0) return nullptr;
if (const auto failed = state.failed.find(base); failed != state.failed.end()) {
if (const auto failed = failures.find(base); failed != failures.end()) {
if (sameRun(failed->second, run)) return nullptr;
state.failed.erase(failed);
failures.erase(failed);
}
// Pinned imports count against the driver's system memory budget; past it ordinary host
// allocations fail, so imports stop at APS5_HOST_IMPORT_MIB (default 6 GiB, which covers the
@ -713,7 +757,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
for (std::uint64_t cursor = base; cursor < base + bytes;) {
MEMORY_BASIC_INFORMATION info{};
if (VirtualQuery(reinterpret_cast<const void*>(cursor), &info, sizeof(info)) == 0 || info.State != MEM_COMMIT || (info.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) {
state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
return nullptr;
}
const auto protection = info.Protect & 0xffu;
@ -725,7 +769,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
cursor = reinterpret_cast<std::uint64_t>(info.BaseAddress) + info.RegionSize;
}
if (!writable && readOnly) {
state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
return nullptr;
}
if (!writable) {
@ -738,13 +782,17 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
entry.alias = GuestArena::GuestArenaMapAlias_nid_postfix(static_cast<std::uintptr_t>(base), static_cast<std::size_t>(bytes));
} catch (const std::runtime_error&) {
if (entry.ranges.size() == 1) throw;
if (joined) {
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
return nullptr;
}
state.unaliasedSpans.insert(base);
return importAllocation(context, state, address, lease);
}
}
#else
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(base), static_cast<std::size_t>(bytes), true)) {
state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
return nullptr;
}
#endif
@ -775,7 +823,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
#ifdef _WIN32
GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias);
#endif
state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx failed at %s (%d); falling back to copies\n", static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes), step, static_cast<int>(result));
#ifdef _WIN32
static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr;
@ -805,7 +853,8 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
importedBytes += bytes;
static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr;
state.liveBytes += bytes;
if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)\n", static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0);
if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)%s\n", static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0, joined ? ", joined at bind" : "");
entry.joined = joined;
return &state.imports.emplace(base, entry).first->second;
}
@ -839,6 +888,7 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
}
state.liveBytes = 0;
state.failed.clear();
state.failedJoins.clear();
state.device = context.device;
state.destroyBuffer = context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer");
state.freeMemory = context.Function<PFN_vkFreeMemory>("vkFreeMemory");
@ -864,6 +914,7 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
it = next;
}
for (auto it = state.failed.begin(); it != state.failed.end();) it = sameRun(it->second, importRun(lease, it->first)) ? std::next(it) : state.failed.erase(it);
for (auto it = state.failedJoins.begin(); it != state.failedJoins.end();) it = sameRun(it->second, joinedRun(lease, it->second.base, it->second.base + it->second.bytes)) ? std::next(it) : state.failedJoins.erase(it);
}
const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end) {
@ -1812,6 +1863,7 @@ void ClearHostImports(VkDevice device) {
state.imports.clear();
state.liveBytes = 0;
state.failed.clear();
state.failedJoins.clear();
state.device = VK_NULL_HANDLE;
state.refreshedGeneration = 0;
++state.epoch;
@ -2011,7 +2063,7 @@ const HostImport* HostImportFor(const Context& context, std::uint64_t address, s
}
const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix();
refreshImports(context, state, lease);
if (!runCovers(lease, address, address + bytes)) return nullptr;
if (!runCovers(lease, address, address + bytes)) return findImport(state, address, address + bytes);
const auto* entry = importAllocation(context, state, address, lease);
return entry != nullptr && findImport(state, address, address + bytes) == entry ? entry : nullptr;
}
@ -2937,6 +2989,17 @@ void GuestBufferMemory::UploadFinish(bool addressable) {
++Mirrors().subranges;
}
bool importsRefreshed = false;
if (context.hostImportAlignment != 0) {
auto& state = Imports();
std::lock_guard lock(state.mutex);
if (importsStale(context, state)) refreshImports(context, state, importRanges());
importsRefreshed = true;
for (const auto& region : regions) {
if (!region.pending || region.mirror != nullptr || region.direct != nullptr || sparseHeapCandidate(region)) continue;
if (findImport(state, region.begin, region.end) != nullptr || runCovers(importRanges(), region.begin, region.end)) continue;
static_cast<void>(importJoined(context, state, region.begin, region.end, importRanges()));
}
}
if (space != nullptr && context.hostImportAlignment != 0) {
// The space's regions were confirmed against the imports when it was built; one check that
// none was retired since (a mirror refresh or a flush between the stages can reconcile

View File

@ -207,6 +207,81 @@ void importWindowTests(const Context& context) {
::operator delete(block, std::align_val_t{window});
}
void importJoinTests(const Context& context) {
constexpr std::size_t piece = 65536;
constexpr std::size_t window = 2 * piece;
constexpr std::size_t large = 3 * piece;
void* block = ::operator new(3 * large + 5 * piece, std::align_val_t{window});
auto* guest = static_cast<std::uint8_t*>(block);
std::memset(guest, 0x11, 3 * large);
const auto first = reinterpret_cast<std::uintptr_t>(block);
const auto second = first + large;
const auto third = second + large;
auto importing = context;
importing.hostImportAlignment = piece;
SetHostImportWindow(window);
const auto change = [&](std::uint8_t* range, std::size_t bytes, bool add) {
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, bytes, true, false, true);
else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range);
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
};
const auto bind = [&](std::uintptr_t address) {
GuestBufferMemory plain(importing);
plain.AddReadable(address, 32);
plain.Upload(false);
std::uint32_t adjustment = 0;
const auto view = plain.Descriptor(address, 32, adjustment);
const auto copied = plain.CopiedBytes();
plain.WriteBack();
return std::pair{view.buffer, copied};
};
change(guest, large, true);
change(guest + large, large, true);
const auto* low = HostImportFor(importing, first, 16);
Require(low != nullptr && low->base == first && low->bytes == large, "a range larger than the import window was not imported alone");
const auto* high = HostImportFor(importing, second, 16);
Require(high != nullptr && high->base == second && high->bytes == large, "the next range larger than the import window was not imported alone");
Require(HostImportFor(importing, second - 16, 32) == nullptr, "a view across two imported ranges was imported before it was bound");
{
GuestBufferMemory leased(importing);
leased.AcquireRegistered();
leased.AddReadable(second - 16, 32);
leased.Upload(true);
const auto* joined = HostImportFor(importing, second - 16, 32);
Require(joined != nullptr && joined->base == first && joined->bytes == 2 * large, "a buffer bound across two adjacent ranges larger than the import window was not imported as one");
std::uint32_t adjustment = 0;
const auto crossing = leased.Descriptor(second - 16, 32, adjustment);
Require(crossing.buffer == joined->buffer && crossing.offset + adjustment == second - 16 - first, "a buffer bound across the joined ranges is not bound in place");
leased.WriteBack();
}
const auto joinedBuffer = HostImportFor(importing, first, 16)->buffer;
const auto [again, copied] = bind(second - 16);
Require(again == joinedBuffer && copied == 0, "a later bind across the joined ranges did not use the joined import");
auto* heap = guest + 3 * large + 2 * piece;
change(heap, piece, true);
change(heap + piece, piece, true);
Require(bind(reinterpret_cast<std::uintptr_t>(heap + piece) - 16).second != 0 && HostImportFor(importing, reinterpret_cast<std::uintptr_t>(heap + piece) - 16, 32) == nullptr, "two pieces no larger than the import window were joined");
const auto* kept = HostImportFor(importing, first, 16);
Require(kept != nullptr && kept->buffer == joinedBuffer && kept->bytes == 2 * large, "a registry change elsewhere re-imported the joined ranges");
change(guest + 2 * large, large, true);
Require(HostImportFor(importing, first, 16)->buffer == joinedBuffer, "a range registered next to the joined ranges re-imported them");
const auto [grownBuffer, grownCopied] = bind(third - 16);
const auto* grown = HostImportFor(importing, third - 16, 32);
Require(grown != nullptr && grown->base == first && grown->bytes == 3 * large && grownBuffer == grown->buffer && grownCopied == 0, "a bind across the next range did not grow the joined import");
Require(bind(second - 16).first == grownBuffer && HostImportFor(importing, first, 16)->buffer == grownBuffer, "two joins sharing a range retired each other");
change(heap, piece, false);
change(heap + piece, piece, false);
change(guest + large, large, false);
const auto* alone = HostImportFor(importing, first, 16);
Require(alone != nullptr && alone->base == first && alone->bytes == large, "the joined import outlived one of its ranges");
change(guest, large, false);
change(guest + 2 * large, large, false);
Require(HostImportFor(importing, first, 16) == nullptr && HostImportFor(importing, third, 16) == nullptr, "the host imports outlived their ranges");
SetHostImportWindow(0);
::operator delete(block, std::align_val_t{window});
}
ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) {
return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false};
}
@ -1203,6 +1278,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
importSpanTests(context);
failedImportRemapTests(context, access);
importWindowTests(context);
importJoinTests(context);
heapMirrorTests(context, access);
importedHeapMirrorTests(context, access);
importWatchTests();

View File

@ -69,7 +69,7 @@
- [Descriptor pool chain](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - each pool holds two storage buffer descriptors more than a set can take, as a workaround: RADV (Mesa 26.0 and later) lets a `FREE_DESCRIPTOR_SET` pool place a set up to 32 bytes past the pool's buffer ([`util_vma_heap_init`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_descriptor_pool.c) is given `bo_size + RADV_POOL_HEAP_OFFSET` as a size), and with a page-multiple pool size those bytes are an unmapped page
- [CMASK fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, codes other than 0 and 0xF, a cleared tile of a DCC target and a fast clear of a mipmapped, 3D or array color target throw. When the clear runs as a compute pass (a resident target whose CMASK is host-imported), an invalid code throws at the next fast-clear materialization, not at the one that read it
- [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen.
- [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import only where they fill a whole 8 MiB aligned window; ranges of a partly filled window are imported one at a time, and a buffer that crosses two imports is copied. Adjacent registered ranges are joined into one import only where they fill a whole 8 MiB aligned window; ranges of a partly filled window are imported one at a time, and a buffer that crosses two imports is copied. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV.
- [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import where they fill a whole 8 MiB aligned window; ranges of a partly filled window, and ranges larger than the window, are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV.
- [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware.
- [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size.
- [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more.