perf(agc): take a storage image's generation baseline only when its guest bytes are settled
An upload of a storage image up to 1 MiB reads the surface's guest bytes into a baseline (#1599), which a later write-back compares against for blocks the CPU stamped. When a unit shadow, another image's pending results or a recorded GPU store covers the surface, that read goes through the flush hook: the shadow is published and the worker waits for the GPU, although the upload itself runs on the GPU from the import. The baseline is now taken only when nothing of that kind overlaps the surface. Otherwise the image has none, and its write-back keeps the CPU's bytes for stamped blocks, the rule before #1599. TechnicalDebt's entry names the condition. agc_storage_baseline_settled creates a storage image over a watched host import with a recorded store pending over part of it and checks that the upload did not submit the open batch; without the change the baseline read syncs on that store. agc_unchanged_cpu_stamps (#1599) still passes. PPSA14632 (LEGO Horizon Adventures) refreshes such images about 870 times per 10 s from queue 0 dispatches: the hook's recorded-store syncs go from about 3400 waiting 4.0 s to about 630 waiting 0.3 s per 10 s, and a direct dispatch's device call from 356 to 107 us. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5edae35361
commit
75b540c216
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -2980,6 +2980,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();
|
||||
|
|
@ -4369,6 +4449,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);
|
||||
|
|
@ -4421,6 +4505,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);
|
||||
|
|
|
|||
|
|
@ -400,7 +400,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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue