Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4183,7 +4183,7 @@ std::uint64_t HostImportBudget(const VkPhysicalDeviceMemoryProperties& memory, c
if ((memory.memoryHeaps[index].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0) hostHeap = std::max<std::uint64_t>(hostHeap, memory.memoryHeaps[index].size);
}
const auto fromMemory = std::max<std::uint64_t>(6144ull << 20u, physicalBytes / 2);
const auto reserve = std::max<std::uint64_t>(2048ull << 20u, hostHeap / 2);
const auto reserve = std::max<std::uint64_t>(2048ull << 20u, std::min<std::uint64_t>(hostHeap / 2, 4096ull << 20u));
return hostHeap > reserve ? std::min(fromMemory, hostHeap - reserve) : fromMemory;
}

Expand Down
6 changes: 4 additions & 2 deletions core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7292,9 +7292,11 @@ void hostImportBudgetTests() {
memory.memoryHeaps[2] = {256 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT};
Require(HostImportBudget(memory, 16 * GiB) == 4064 * MiB, "imports on a 16 GiB machine take more than half its 8128 MiB host heap");
memory.memoryHeaps[1].size = 16 * GiB;
Require(HostImportBudget(memory, 32 * GiB) == 8 * GiB, "imports on a 32 GiB machine take more than half its 16 GiB host heap");
Require(HostImportBudget(memory, 32 * GiB) == 12 * GiB, "a 32 GiB machine does not keep 4 GiB of its 16 GiB host heap from imports");
memory.memoryHeaps[1].size = 15911 * MiB;
Require(HostImportBudget(memory, 32 * GiB) == 11815 * MiB, "imports on a 32 GiB machine leave no room for the 10466 MiB a title holds at once");
memory.memoryHeaps[1].size = 32 * GiB;
Require(HostImportBudget(memory, 64 * GiB) == 16 * GiB, "imports on a 64 GiB machine take more than half its 32 GiB host heap");
Require(HostImportBudget(memory, 64 * GiB) == 28 * GiB, "a 64 GiB machine does not keep 4 GiB of its 32 GiB host heap from imports");
memory.memoryHeaps[1].size = 3 * GiB;
Require(HostImportBudget(memory, 16 * GiB) == 1 * GiB, "a 3 GiB host heap does not keep its 2 GiB reserve");
memory.memoryHeaps[1].size = 8128 * MiB;
Expand Down
2 changes: 1 addition & 1 deletion docs/dev/debt/libSceAgcDriver.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@
- [16-bit depth with stencil](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is stored as `D24_UNORM_S8_UINT`, or `D32_SFLOAT_S8_UINT`, on a device without `D16_UNORM_S8_UINT` as an attachment. Depth comparisons, clears and the depth image see the wider format's precision, so results at the 16-bit quantization boundary can differ from the console; the constant depth bias is scaled by 256 for D24, and a non-zero constant on the D32 float fallback throws. Not measured on console hardware.
- [Depth surfaces created over storage writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth surface created without HTILE copies its depth plane from the storage image at the plane's address, a pending one first, when that image has the plane's extent, one level, a 2D view or a 2D array view (its layer 0 is copied) and a 16-bit format for a 16-bit plane or a 32-bit format for a 32-bit plane. A multisampled surface, a surface the device stores in another format, a surface with HTILE and a surface without such an image start from `DB_DEPTH_CLEAR`, and the stencil plane from `DB_STENCIL_CLEAR`; CPU writes to the plane's memory are not read. Not measured on the console, where the plane is that memory.
- [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing while its own release interrupts keep arriving. The latch is cleared when the queue has flipped five times without another interrupt from its own releases: the fifth flip delivers the event and unlatched the queue, so a title that waits on the flip event after latching recovers only if it submits further flips. The bound cannot be by time: the next release interrupt can arrive seconds later than a suppressed flip across loading stalls, and delivering an unexpected flip event on a queue that pairs flips with releases can crash; no fixed window short enough to unblock a waiting title keeps such a title alive. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue.
- [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half that heap (imports, type-3 mirrors and buffers and textures placed in host memory share it; on a 16 GiB machine whose host heap is half the memory, a 2 GiB reserve left too little for the rest). Where the device enables `VK_EXT_memory_budget`, that host heap's reported budget less its other usage and an eighth, in 64 MiB steps, read at most once a second, also bounds the default budget once an import's `vkAllocateMemory` has failed; a set `APS5_HOST_IMPORT_MIB` is never lowered by the report. The report's other usage includes the host-visible copies and heap mirrors that stand in for refused imports, so it grows with the refusals; the first time the report would lower the budget, `[gpu] host import budget: host heap ...` prints the heap's reported budget and usage and the live imports. An import's `vkAllocateMemory` failure within an eighth of the budget, or the third in a row, lowers the budget to the live imports until the device or the configured budget changes; an isolated failure further below only marks its range failed. Live imports are not capped by `maxMemoryAllocationCount`: NVIDIA's Windows driver reports 4096, while Village held 27,167 live imports without a failure. A new import beyond it retires the least recently used imports not used in the current or the previous frame (a frame ends at a flip packet or a CPU flip), so a set used in every frame keeps its imports and what does not fit is copied, as below the budget; until two frames have ended imports age by lookups, an import being evictable 256 lookups after its last use; after that, without flips (a loading stretch, compute-only work) no frame ends and nothing more becomes evictable, so imports that do not fit are copied instead of evicting by recency; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. An address-based build does not evict either, except for a range the budget refused (or remembers refusing) and no mirror serves whose copy would take the build's copies past `APS5_ADDRESS_COPY_MAX_MIB`; that eviction also takes imports the recency rule protects whose single range is releasable and write-watched, so a heap mirror can serve it, as long as the build has not taken them yet. A heap mirror, sparse or not, whose range has left the write watch is refreshed in full at each use, every block counted as written, unless the build can import its range first, evicting for it as the copy cap does, with the imports of earlier builds no longer held; a build plans its range again only once the import path takes the range. A request that may not evict (an address-based build, an import when a range is mapped) remembers a refused range or block, with the live bytes and the budget at the refusal, and is answered from that memory, still counted as refused, until live imports drop below those bytes, the budget changes or one of its ranges changes. A request that may evict remembers its refusal too, answering later requests for the same run from memory with the room the refusal left (a request whose range fits in that room steps down to a smaller block without scanning the imports again, and a refusal that stepped down is remembered for each block it tried), while no import was made or retired, the budget has not grown, the import window and the run are unchanged, and no import can have become evictable since: no frame has ended (or, before two frames have ended, 256 lookups have not passed), and no build holding an import two frames old has raised its floor. A join remembers its refusal the same way, apart from other requests for the same run, so a buffer bound across refused ranges does not try the join again at every upload. The shutdown line `[gpu] host imports at shutdown` gives the live, made, refused (and remembered) and failed counts per requester. The 64 MiB window, the two-frame protection, the 2 GiB reserve, the eighth, the 64 MiB steps and the three failures are choices, not measured.
- [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less half that heap, kept between 2 and 4 GiB (imports, type-3 mirrors and buffers and textures placed in host memory share it; on a 16 GiB machine whose host heap is half the memory, a 2 GiB reserve left too little for the rest, and on a 32 GiB machine a reserve of half its 16 GiB heap left imports 8 GiB, less than the 10466 MiB one title holds at once). Where the device enables `VK_EXT_memory_budget`, that host heap's reported budget less its other usage and an eighth, in 64 MiB steps, read at most once a second, also bounds the default budget once an import's `vkAllocateMemory` has failed; a set `APS5_HOST_IMPORT_MIB` is never lowered by the report. The report's other usage includes the host-visible copies and heap mirrors that stand in for refused imports, so it grows with the refusals; the first time the report would lower the budget, `[gpu] host import budget: host heap ...` prints the heap's reported budget and usage and the live imports. An import's `vkAllocateMemory` failure within an eighth of the budget, or the third in a row, lowers the budget to the live imports until the device or the configured budget changes; an isolated failure further below only marks its range failed. Live imports are not capped by `maxMemoryAllocationCount`: NVIDIA's Windows driver reports 4096, while Village held 27,167 live imports without a failure. A new import beyond it retires the least recently used imports not used in the current or the previous frame (a frame ends at a flip packet or a CPU flip), so a set used in every frame keeps its imports and what does not fit is copied, as below the budget; until two frames have ended imports age by lookups, an import being evictable 256 lookups after its last use; after that, without flips (a loading stretch, compute-only work) no frame ends and nothing more becomes evictable, so imports that do not fit are copied instead of evicting by recency; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. An address-based build does not evict either, except for a range the budget refused (or remembers refusing) and no mirror serves whose copy would take the build's copies past `APS5_ADDRESS_COPY_MAX_MIB`; that eviction also takes imports the recency rule protects whose single range is releasable and write-watched, so a heap mirror can serve it, as long as the build has not taken them yet. A heap mirror, sparse or not, whose range has left the write watch is refreshed in full at each use, every block counted as written, unless the build can import its range first, evicting for it as the copy cap does, with the imports of earlier builds no longer held; a build plans its range again only once the import path takes the range. A request that may not evict (an address-based build, an import when a range is mapped) remembers a refused range or block, with the live bytes and the budget at the refusal, and is answered from that memory, still counted as refused, until live imports drop below those bytes, the budget changes or one of its ranges changes. A request that may evict remembers its refusal too, answering later requests for the same run from memory with the room the refusal left (a request whose range fits in that room steps down to a smaller block without scanning the imports again, and a refusal that stepped down is remembered for each block it tried), while no import was made or retired, the budget has not grown, the import window and the run are unchanged, and no import can have become evictable since: no frame has ended (or, before two frames have ended, 256 lookups have not passed), and no build holding an import two frames old has raised its floor. A join remembers its refusal the same way, apart from other requests for the same run, so a buffer bound across refused ranges does not try the join again at every upload. The shutdown line `[gpu] host imports at shutdown` gives the live, made, refused (and remembered) and failed counts per requester. The 64 MiB window, the two-frame protection, the 2 GiB reserve, the eighth, the 64 MiB steps and the three failures are choices, not measured.

Check warning on line 304 in docs/dev/debt/libSceAgcDriver.md

View workflow job for this annotation

GitHub Actions / conventions

debt-length

Technical debt entry over 1000 characters: state the gap first (what differs from the console, what is unverified, what throws) and keep only the behaviour it needs: 4754 characters
- [Piece-split ranges over the host import budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): an address-based build imports a registered range larger than the device's largest allocation in pieces. If the budget refuses a piece, the range is mirrored or copied without the evicting retry other refused ranges get when their copy would pass `APS5_ADDRESS_COPY_MAX_MIB`, so such a copy can still exceed that cap and end the build.
- [Device-local staging](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written or atomic buffer element inside a host import, including one an address-based compute build reaches through its pointers, is copied to device memory before the dispatch and copied back whole after it, so a CPU store into that range in between is rolled back.
- [Committed reads of released pages](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) - `ReadCommitted` and `EqualsCommitted` find the readable pages of a range first and read them afterwards. On Windows they read through `ReadProcessMemory`, so a page released in between reads as zero and is left out of the comparison instead of faulting: Demon's Souls (PPSA01342) samples a texture at 0x38400 of 352 MiB while it loads the tutorial level, a range that holds no guest allocation and on Windows covers host heap pages other threads release, which stopped it with an access violation. A texture outside every registered guest allocation still takes the host bytes found there, as it did before; what the console returns for it is not measured. On Linux the pages are copied directly, so a page released between the check and the copy still faults.
Expand Down
Loading