Merge pull request #4603 from AlxcBTW/perf/agc-memory-value-fill

perf(agc): take the memory-value dword fill kernel as a buffer fill
This commit is contained in:
Dean 2026-10-14 13:31:43 +13:00 committed by GitHub
commit cb320fd884
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 227 additions and 15 deletions

View File

@ -2,6 +2,8 @@
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_MEMORY_DWORDPATTERNFILL_HPP
#include "Recompiler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
@ -19,6 +21,9 @@ struct DwordPatternFill {
std::optional<DwordPatternFill> MatchDwordPatternFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
std::optional<DwordPatternFill> MatchZeroDwordFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
std::optional<Graphics::MemoryValueFill> MatchMemoryValueBufferFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
bool IsByteFill(const std::array<std::uint32_t, 4>& pattern);
std::optional<Graphics::DccKeys> KeysFillMemo(const std::array<std::uint32_t, 4>& pattern, bool coversStorageKeys, bool memoryValueFill);
}

View File

@ -87,6 +87,32 @@ std::optional<DwordPatternFill> MatchZeroDwordFill(std::span<const std::uint32_t
return DwordPatternFill{base, static_cast<std::size_t>(dwords * 4u), {}};
}
std::optional<Graphics::MemoryValueFill> MatchMemoryValueBufferFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) {
static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr;
if (!enabled || userData.size() != 8 || packet.size() < 5 || (packet[4] & 0x20u) != 0 || !compute.groupIdEnable[0]) return std::nullopt;
const auto fill = Graphics::MatchMemoryValueFill(code, userData, compute.numThreads, {packet[1], packet[2], packet[3]});
if (!fill) return std::nullopt;
if (fill->address % 16u != 0 || fill->bytes % 16u != 0) return std::nullopt;
return fill;
}
bool IsByteFill(const std::array<std::uint32_t, 4>& pattern) {
return std::all_of(pattern.begin(), pattern.end(), [&](std::uint32_t word) { return word == (pattern[0] & 0xffu) * 0x01010101u; });
}
std::optional<Graphics::DccKeys> KeysFillMemo(const std::array<std::uint32_t, 4>& pattern, bool coversStorageKeys, bool memoryValueFill) {
if (!IsByteFill(pattern) || !(coversStorageKeys || memoryValueFill)) return std::nullopt;
switch (pattern[0] & 0xffu) {
case 0x00: return Graphics::DccKeys::Clear0000;
case 0x40: return Graphics::DccKeys::Clear0001;
case 0x80: return Graphics::DccKeys::Clear1110;
case 0xc0: return Graphics::DccKeys::Clear1111;
case 0x20: return Graphics::DccKeys::ClearRegister;
case 0xff: return Graphics::DccKeys::Uncompressed;
default: return std::nullopt;
}
}
bool Driver::fillClearEnabled() {
static const bool enabled = std::getenv("APS5_NO_FILL_CLEAR") == nullptr;
return enabled;
@ -154,7 +180,9 @@ void Driver::fillClearCount(const Graphics::StorageTexture::FillCoverage& covera
bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::shared_ptr<VulkanDevice>& localDevice) {
auto dwordFill = MatchDwordPatternFill(packet, code, userData, compute);
if (!dwordFill) dwordFill = MatchZeroDwordFill(packet, code, userData, compute);
if (!dwordFill && !matchesFillKernel(code, userData, compute)) return false;
std::optional<Graphics::MemoryValueFill> memoryFill;
if (!dwordFill) memoryFill = MatchMemoryValueBufferFill(packet, code, userData, compute);
if (!dwordFill && !memoryFill && !matchesFillKernel(code, userData, compute)) return false;
std::uint64_t base;
std::size_t bytes;
std::array<std::uint32_t, 4> pattern;
@ -163,6 +191,15 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons
base = dwordFill->base;
bytes = dwordFill->bytes;
pattern = dwordFill->pattern;
} else if (memoryFill) {
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill);
inputLock.lock();
recordLabelsForPacket(localDevice.get(), queueId);
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(memoryFill->valueAddress), 4)) return false;
GuestMemory::Read(memoryFill->valueAddress, std::as_writable_bytes(std::span(pattern).first(1)), 4);
std::fill(pattern.begin() + 1, pattern.end(), pattern[0]);
base = memoryFill->address;
bytes = memoryFill->bytes;
} else if (MatchesPatternFillKernel(code, userData, compute)) {
const auto range = DecodePatternFillRange(userData, packet);
if (!range) return false;
@ -240,7 +277,7 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons
static const bool traceKeys = std::getenv("APS5_TRACE_DCC_KEYS") != nullptr;
if (traceKeys && coverage.cover == Graphics::StorageTexture::FillCover::Keys) std::fprintf(stderr, "[dcc-keys] title fills keys 0x%llx+0x%zx with %08x %08x %08x %08x (queue 0x%x)\n", static_cast<unsigned long long>(base), bytes, pattern[0], pattern[1], pattern[2], pattern[3], queueId);
const bool uniformKeysFill = coverage.cover == Graphics::StorageTexture::FillCover::Keys && std::all_of(pattern.begin(), pattern.end(), [&](std::uint32_t word) { return word == (pattern[0] & 0xffu) * 0x01010101u; });
const bool uniformKeysFill = coverage.cover == Graphics::StorageTexture::FillCover::Keys && IsByteFill(pattern);
if (uniformKeysFill) {
Graphics::StorageTexture::NoteKeysFill(base, bytes, static_cast<std::uint8_t>(pattern[0]));
Graphics::StorageTexture::ClearByKeysFill(base, bytes, static_cast<std::uint8_t>(pattern[0]));
@ -273,18 +310,8 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons
phase(FillFlush);
const bool stored = cleared || localDevice->FillBuffer(base, bytes, pattern);
phase(FillDevice);
if (uniformKeysFill && stored && !cleared) {
Graphics::DccKeys filled = Graphics::DccKeys::Mixed;
switch (pattern[0] & 0xffu) {
case 0x00: filled = Graphics::DccKeys::Clear0000; break;
case 0x40: filled = Graphics::DccKeys::Clear0001; break;
case 0x80: filled = Graphics::DccKeys::Clear1110; break;
case 0xc0: filled = Graphics::DccKeys::Clear1111; break;
case 0x20: filled = Graphics::DccKeys::ClearRegister; break;
case 0xff: filled = Graphics::DccKeys::Uncompressed; break;
default: break;
}
if (filled != Graphics::DccKeys::Mixed) Graphics::NoteKeysFillOnGpu(base, bytes, filled);
if (stored && !cleared) {
if (const auto filled = KeysFillMemo(pattern, uniformKeysFill, memoryFill.has_value())) Graphics::NoteKeysFillOnGpu(base, bytes, *filled);
}
if (profile && stored && !cleared) {
const bool uniform = pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3];

View File

@ -0,0 +1,180 @@
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <span>
#include <string>
#include <utility>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using AgcDriver::DriverDetail::KeysFillMemo;
using AgcDriver::DriverDetail::MatchMemoryValueBufferFill;
alignas(256) constexpr std::array<std::uint32_t, 9> Code{0xd7460000u, 0x04010c08u, 0xf4201a82u, 0xfa000000u, 0xbf8cc07fu, 0x7e02026au, 0xe0102000u, 0x80000100u, 0xbf810000u};
constexpr std::uint32_t Untouched = 0xcafebabeu;
constexpr std::uint32_t Format32Uint = 0x00014004u;
alignas(256) std::array<std::uint32_t, 4> Value{};
alignas(256) std::array<std::uint32_t, 1028> Output{};
void KeysFillMemoTests() {
using AgcDriver::Graphics::DccKeys;
const auto bytes = [](std::uint32_t byte) { const auto word = byte * 0x01010101u; return std::array<std::uint32_t, 4>{word, word, word, word}; };
const std::array<std::pair<std::uint32_t, DccKeys>, 6> codes{{{0x00u, DccKeys::Clear0000}, {0x40u, DccKeys::Clear0001}, {0x80u, DccKeys::Clear1110}, {0xc0u, DccKeys::Clear1111}, {0x20u, DccKeys::ClearRegister}, {0xffu, DccKeys::Uncompressed}}};
for (const auto& [byte, keys] : codes) {
Require(KeysFillMemo(bytes(byte), false, true) == keys, "a memory-value fill of a key code over keys that are no storage image's was not recorded");
Require(KeysFillMemo(bytes(byte), true, false) == keys, "a fill of a key code over a storage image's keys was not recorded");
Require(KeysFillMemo(bytes(byte), true, true) == keys, "a memory-value fill over a storage image's keys was not recorded");
Require(!KeysFillMemo(bytes(byte), false, false), "a fill over keys that are no storage image's, other than a memory-value fill, was recorded");
}
Require(!KeysFillMemo(bytes(0x12u), true, true), "a byte that is no key code was recorded");
auto mixed = bytes(0x40u);
mixed[3] = 0x40404041u;
Require(!KeysFillMemo(mixed, true, true), "a pattern of two different words was recorded");
mixed = bytes(0x40u);
mixed[0] = 0x40404000u;
Require(!KeysFillMemo(mixed, true, true), "a word of different bytes was recorded");
}
std::vector<std::uint32_t> UserData(const std::uint32_t* output, std::uint32_t records, std::uint32_t valueRecords = 16, std::uint32_t valueStride = 0, std::uint32_t format = Format32Uint) {
const auto address = reinterpret_cast<std::uintptr_t>(output);
const auto value = reinterpret_cast<std::uintptr_t>(Value.data());
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (4u << 16u), records, format, static_cast<std::uint32_t>(value), static_cast<std::uint32_t>((value >> 32u) & 0xffffu) | (valueStride << 16u), valueRecords, Format32Uint};
}
void MatchTests() {
const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1};
const std::array<std::uint32_t, 5> packet{0xc0031500u, 4, 1, 1, 0x41u};
const auto data = UserData(Output.data(), 1024);
const auto fill = MatchMemoryValueBufferFill(packet, Code, data, compute);
Require(fill && fill->address == reinterpret_cast<std::uintptr_t>(Output.data()) && fill->bytes == 1024 && fill->valueAddress == reinterpret_cast<std::uintptr_t>(Value.data()), "the memory-value fill kernel was not taken as a fill of 256 dwords");
for (const std::uint32_t format : {0x00015004u, 0x00016004u}) {
Require(MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 16, 0, format), compute).has_value(), "a 32-bit output format was refused");
}
for (const std::uint32_t format : {0x0000d004u, 0x00027facu, 0x00038004u}) {
Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 16, 0, format), compute), "an output format whose elements are not one 32-bit value matched");
}
Require(MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 1, 4), compute).has_value(), "a value descriptor of one 4-byte element was refused");
Require(MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 4), compute).has_value(), "a value descriptor of 4 bytes was refused");
Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 0), compute), "a value descriptor without records matched (the load returns 0)");
Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 3), compute), "a value descriptor shorter than the loaded dword matched");
Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 1, 2), compute), "a value descriptor of one 2-byte element matched");
Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data() + 1, 1024), compute), "an unaligned destination matched");
Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 102), compute), "a record count not filling 16-byte units matched");
auto extra = data;
extra.push_back(0);
Require(!MatchMemoryValueBufferFill(packet, Code, extra, compute), "a different user SGPR count matched (the group id moves)");
auto partial = packet;
partial[4] |= 0x20u;
Require(!MatchMemoryValueBufferFill(partial, Code, data, compute), "a partial dispatch matched");
auto noGroup = compute;
noGroup.groupIdEnable[0] = false;
Require(!MatchMemoryValueBufferFill(packet, Code, data, noGroup), "a dispatch without the group id matched");
auto altered = Code;
altered[5] ^= 1u;
Require(!MatchMemoryValueBufferFill(packet, altered, data, compute), "a different shader matched");
const auto clamped = MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 64), compute);
Require(clamped && clamped->bytes == 256, "the fill exceeded the output descriptor's records");
}
std::vector<std::uint32_t> Commands(const std::vector<std::uint32_t>& data, std::uint32_t groups, std::uint32_t wave) {
std::vector<std::uint32_t> words;
const auto registers = [&](std::uint32_t first, std::span<const std::uint32_t> values) {
words.push_back(0xc0007600u | (static_cast<std::uint32_t>(values.size()) << 16u));
words.push_back(first);
words.insert(words.end(), values.begin(), values.end());
};
const std::array<std::uint32_t, 3> shape{64, 1, 1};
registers(0x207, shape);
const auto address = reinterpret_cast<std::uintptr_t>(Code.data());
const std::array<std::uint32_t, 2> program{static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>(address >> 40u)};
registers(0x20c, program);
const std::array<std::uint32_t, 1> resource{(static_cast<std::uint32_t>(data.size()) << 1u) | (1u << 7u)};
registers(0x213, resource);
registers(0x240, data);
words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u | (wave == 32 ? 0x8000u : 0u)});
return words;
}
void Label(std::vector<std::uint32_t>& words, std::uint32_t* target, std::uint32_t value) {
const auto address = reinterpret_cast<std::uintptr_t>(target);
words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), value, 0, 0});
}
void Submit(const std::vector<std::uint32_t>& words) {
Packet packet{const_cast<std::uint32_t*>(words.data()), static_cast<std::uint32_t>(words.size()), 0, {}};
AgcDriver::Submit(&packet, 0);
AgcDriverWaitIdle_nid_postfix();
AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(Output.data()), sizeof(Output));
}
void Expect(std::size_t first, std::size_t count, std::uint32_t value, const std::string& what) {
for (std::size_t i = 0; i < Output.size(); ++i) {
const auto expected = i >= first && i < first + count ? value : Untouched;
Require(Output[i] == expected, what + ": word " + std::to_string(i) + " is " + std::to_string(Output[i]) + ", expected " + std::to_string(expected));
}
}
void ExecuteTests() {
struct Case {
std::uint32_t value, first, records, groups, valueRecords, valueStride, stored;
const char* what;
};
constexpr std::array<Case, 8> cases{{
{0xffffffffu, 0, 256, 4, 16, 0, 0xffffffffu, "a 16-byte aligned fill of every launched thread"},
{0u, 0, 256, 2, 16, 0, 0u, "a fill shorter than its records"},
{0x40404040u, 0, 1000, 4, 16, 0, 0x40404040u, "a fill longer than its launched threads"},
{0x0c0c0c0cu, 0, 256, 4, 1, 4, 0x0c0c0c0cu, "a fill whose value descriptor holds one 4-byte element"},
{0x12345678u, 0, 255, 4, 16, 0, 0x12345678u, "a fill whose size is not a multiple of 16 bytes"},
{0x80808080u, 1, 256, 4, 16, 0, 0x80808080u, "a fill whose start is not 16-byte aligned"},
{0x20202020u, 0, 0, 4, 16, 0, 0x20202020u, "an empty fill"},
{0xc0c0c0c0u, 0, 256, 4, 0, 0, 0u, "a fill whose value descriptor has no records (the load returns 0)"},
}};
for (const auto wave : {32u, 64u}) {
for (const auto& item : cases) {
Output.fill(Untouched);
Value[0] = item.value;
Submit(Commands(UserData(Output.data() + item.first, item.records, item.valueRecords, item.valueStride), item.groups, wave));
Expect(item.first, std::min(item.records, item.groups * 64u), item.stored, std::string(item.what) + " (wave " + std::to_string(wave) + ")");
}
}
Output.fill(Untouched);
Value[0] = 0x11111111u;
auto words = Commands(UserData(Output.data(), 256), 4, 64);
Label(words, &Value[0], 0xc0c0c0c0u);
const auto again = Commands(UserData(Output.data(), 128), 2, 64);
words.insert(words.end(), again.begin(), again.end());
Submit(words);
for (std::size_t i = 0; i < 256; ++i) {
Require(Output[i] == (i < 128 ? 0xc0c0c0c0u : 0x11111111u), "a fill did not read the value a preceding GPU label stored, at word " + std::to_string(i));
}
}
}
int main() {
try {
KeysFillMemoTests();
if (std::getenv("APS5_NO_FILL_HLE") == nullptr) MatchTests();
{
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
}
ExecuteTests();
AgcDriverShutdown_nid_postfix();
std::puts("memory-value fill tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
AgcDriverShutdown_nid_postfix();
return 1;
}
}

View File

@ -74,7 +74,7 @@
- [PA_SC_CONSERVATIVE_RASTERIZATION_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 0x6001 uses Vulkan overestimation on devices that add at most 1/256 pixel and keep degenerate triangles; the console's uncertainty region and degenerate handling are unmeasured. A value with `OVER_RAST_ENABLE` and `UNDER_RAST_ENABLE` clear is decoded as disabled even when `UNCERTAINTY_REGION_MODE` or the `OUTER_`/`INNER_UNCERTAINTY_EDGERULE_OVERRIDE` bits (19:16) are set, as PPSA01474 writes 0xe6000; that these fields have no effect without over- or underestimation is assumed from their names, not measured.
- [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, a cleared tile of a DCC target and a fast clear of a mipmapped, 3D or array color target throw. A tile with a code other than 0 and 0xF keeps its texels like an expanded tile: PPSA18391 (engine: Clickteam Fusion) fast clears a target whose CMASK it never initialized, so some of its nibbles hold leftover values such as 1 and 10; what the console does with such codes on a single-sample target is unverified on hardware
- [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.
- [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. When the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes the dispatch instead (an output descriptor of one 32-bit value per element, a value descriptor that covers the loaded dword, a 16-byte aligned range), the value is read again when the fill is recorded, after the labels queued before it, and the HTILE fill is noted with that value.
- [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.