Merge pull request #2791 from alfalfa-star/perf/agc-storage-baseline-settled

perf(agc): take a storage image's generation baseline only when its guest bytes are settled
This commit is contained in:
Dean 2026-10-11 08:45:14 +13:00 committed by GitHub
commit 9206c3e621
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 101 additions and 2 deletions

View File

@ -699,6 +699,12 @@ if(BUILD_TESTING)
set_tests_properties(agc_unchanged_cpu_stamps PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_storage_baseline_settled COMMAND agc_driver_recorder_tests --settled-baseline-only)
set_tests_properties(agc_storage_baseline_settled PROPERTIES SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_storage_baseline_settled PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_host_import_unmap COMMAND agc_driver_recorder_tests --host-import-unmap-only)
set_tests_properties(agc_host_import_unmap PROPERTIES SKIP_RETURN_CODE 77)
if(WIN32)

View File

@ -317,6 +317,7 @@ private:
void writeBack(std::uint64_t address, std::size_t bytes);
void writeBackLayers(const std::vector<bool>& layers);
bool unchangedSinceBaseline(std::uint64_t from, std::uint64_t to) const;
bool guestBytesSettled() const;
// Tracked units as 64 KiB write-stamp blocks (`blockUnits`: a thin tiled surface at a 64 KiB
// aligned base; APS5_NO_BLOCK_TRACKING=1 tracks array layers as above instead): a fill of one
// layer, a CPU write or another image's store then costs the blocks it touched, moved through

View File

@ -1754,7 +1754,7 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
const auto now = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
const auto stampLayers = [&](bool selectedOnly) {
const bool whole = !selectedOnly || layers == nullptr;
if (guestBytes > GenerationBaselineBytes) generationBaseline.clear();
if (guestBytes > GenerationBaselineBytes || !guestBytesSettled()) generationBaseline.clear();
else if (whole) {
generationBaseline.resize(static_cast<std::size_t>(guestBytes));
GuestMemory::ReadCommitted(descriptor.baseAddress, generationBaseline);
@ -3745,6 +3745,13 @@ void StorageTexture::writeBack(std::uint64_t address, std::size_t bytes) {
writeBackLayers(layers);
}
bool StorageTexture::guestBytesSettled() const {
const std::array<std::pair<std::uint64_t, std::uint64_t>, 1> range{{{descriptor.baseAddress, descriptor.baseAddress + guestBytes}}};
if (AnyShadowedOverlaps(range) || AnyPendingOverlaps(range)) return false;
const auto* recorder = Recorder::Active();
return recorder == nullptr || !recorder->PendingWriteOverlaps(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
}
bool StorageTexture::unchangedSinceBaseline(std::uint64_t from, std::uint64_t to) const {
const auto& baseline = originalValid ? original : generationBaseline;
if (baseline.size() != guestBytes) return false;

View File

@ -3069,6 +3069,86 @@ void unchangedCpuStampTests(const Device& device, Recorder& recorder) {
std::cout << "unchanged CPU stamps: ok\n";
}
void settledBaselineTests(const Device& device, Recorder& recorder) {
const auto& base = device.GetContext();
#ifdef _WIN32
std::cout << "generation baseline over a pending store: Linux write watch only\n";
return;
#endif
if (base.hostImportAlignment == 0) {
std::cout << "host imports unavailable: generation baseline over a pending store not tested\n";
return;
}
constexpr std::uint32_t side = 256;
constexpr std::uint64_t offset = 0x2000;
constexpr std::size_t bytes = 6 * 65536;
const auto decided = PrepareImportWatch(base);
SetImportWatch(base, ImportWatch::Watch);
struct Restore {
const Context& context;
ImportWatch decided;
~Restore() { SetImportWatch(context, decided); }
} restore{base, decided};
for (const bool pending : {false, true}) {
void* block = AllocateWatched(bytes, 65536);
if (block == nullptr) {
std::cout << "no write watching: generation baseline over a pending store not tested\n";
return;
}
std::memset(block, 0x5a, bytes);
const auto blockAddress = reinterpret_cast<std::uint64_t>(block);
const auto address = blockAddress + offset;
{
GuestAllocations::Mutation mutation;
mutation.Add(block, bytes, true, true, true);
}
struct Unregister {
const Context& context;
void* block;
std::uint64_t address;
std::size_t bytes;
~Unregister() {
{
GuestAllocations::Mutation mutation;
mutation.Remove(block);
}
HostImportFor(context, address, bytes);
ReleaseWatched(block, bytes);
}
} unregister{base, block, blockAddress, bytes};
AgcDriver::GuestMemory::CollectWritesUncached(blockAddress, bytes);
TextureDetiler detiler(base);
auto context = base;
context.detiler = &detiler;
GuestTextureResource resource{};
resource.baseAddress = address;
resource.width = side;
resource.height = side;
resource.mipCount = 1;
resource.tileMode = TextureTileMode::kLinear;
resource.dimension = TextureDimension::k2D;
resource.format = 56;
resource.dstSelX = 4;
resource.dstSelY = 5;
resource.dstSelZ = 6;
resource.dstSelW = 7;
const auto surfaceBytes = static_cast<std::size_t>(DescribeSurface(resource).guestBytes);
Require(HostImportFor(context, address, surfaceBytes) != nullptr && AgcDriver::GuestMemory::Watched(address, surfaceBytes), "(b) the surface is not a watched host import");
static_cast<void>(recorder.Commands());
if (pending) recorder.NotePendingWrite(address + 4096, 4096);
const auto before = recorder.Submissions();
{
auto image = std::make_shared<StorageTexture>(context, detiler, resource, 0);
recorder.Keep(image);
if (pending) Require(recorder.Submissions() == before, "(b) an upload over a pending recorded store submitted the open batch to read its generation baseline");
}
recorder.Submit();
device.WaitQueue();
recorder.Sync();
}
std::cout << "generation baseline over a pending store: ok\n";
}
void importWindowTests(const Device& device, Recorder& recorder) {
using namespace AgcDriver::GuestMemory;
const auto& base = device.GetContext();
@ -4557,6 +4637,10 @@ int main(int argc, char** argv) {
unchangedCpuStampTests(device, recorder);
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--settled-baseline-only") {
settledBaselineTests(device, recorder);
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--completion-labels-only") {
completionCountTests(device, recorder);
afterRecordedWorkTests(device, recorder);
@ -4610,6 +4694,7 @@ int main(int argc, char** argv) {
singlePassTests(device, recorder, false);
singlePassTests(device, recorder, true);
unchangedCpuStampTests(device, recorder);
settledBaselineTests(device, recorder);
importWatchTests(device);
staleGenerationTests(device, recorder);
importWindowTests(device, recorder);

View File

@ -410,7 +410,7 @@ Throughout the project, every function at every stage either **does exactly what
- [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.
- [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.
- [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw.
- [Storage image write-back](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB; larger ones keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value.
- [Storage image write-back](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value.
- [DCC key stores](../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) cover the console's DCC extent only for pipe-aligned DCC of single-level, single-layer 4-byte SW_64KB_R_X surfaces; other surfaces, and every texture and color target key read, cover one byte per 256 bytes.
- [DCC key fills over other images](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) - a uniform fill at a live image's `dccAddress`, at least one key per 256 bytes long, is a key fill of that image even when other live images overlap it, but only while it is no longer than the image's key extent (`DccKeyCount`, the pipe-aligned count); a longer one is classified by its overlaps as before. The bound is inferred, not measured on a title: a longer fill also writes the bytes past the keys, which the overlapping image may own. Whether the title's bloom-target key fills stay within the extent was not checked (`APS5_TRACE_DCC_KEYS` prints the length of the fills classified as keys, not of the refused ones).
- [8_SRGB color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) on a device that cannot attach and blend R8_SRGB render into an RGBA8_SRGB copy of the target, so the sRGB encode, decode and blend rounding are the device's; the console's are not measured.