Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
015bd22
perf(agc): an untracked comparison memo holds until memory it covers …
TheAggie Oct 15, 2026
a049317
test(agc): forgetting other memory keeps an untracked image's compari…
TheAggie Oct 15, 2026
d7b2ad3
perf(agc): fast revalidation over untracked memory proves a surface b…
TheAggie Oct 15, 2026
2d03040
test(agc): a sampled template over untracked memory proves fast withi…
TheAggie Oct 15, 2026
6c1ad4b
Merge branch 'main' into perf/agc-untracked-proof-hint-key
EagleBearerYS Oct 29, 2026
d89ed07
docs(agc): list APS5_VERIFY_UNTRACKED_PROOFS in the environment refer…
EagleBearerYS Oct 29, 2026
e440b74
test(agc): finish recorded work before the untracked forget test free…
EagleBearerYS Oct 29, 2026
fdc7004
perf(agc): keep an untracked fast proof across collect epochs while i…
EagleBearerYS Oct 29, 2026
31dff93
Merge main into perf/agc-untracked-proof-hint-key-on-6192
EagleBearerYS Oct 30, 2026
2b74a6c
test(agc): store into untracked hint ranges from other threads around…
EagleBearerYS Oct 30, 2026
23ef222
Merge main into perf/agc-untracked-proof-hint-key-on-6192
EagleBearerYS Oct 30, 2026
8bee4ca
Merge main into perf/agc-untracked-proof-hint-key-on-6192
EagleBearerYS Oct 31, 2026
793fc7f
test(agc): report the untracked proof tests as skipped when they cann…
EagleBearerYS Nov 1, 2026
38dc520
Merge commit 'f6232fcfda70644495273f6b8dd88cf0603463d6' into HEAD
oneandonlydean Nov 3, 2026
8468301
Merge main into perf/agc-untracked-proof-hint-key-on-6192
EagleBearerYS Nov 3, 2026
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
8 changes: 7 additions & 1 deletion core/libs/prx/libSceAgcDriver/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1865,6 +1865,12 @@ if(BUILD_TESTING)
set_tests_properties(agc_descriptor_pool_recycling PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME agc_draw_snapshot_cap COMMAND agc_driver_recorder_tests --draw-snapshot-cap-only)
set_tests_properties(agc_draw_snapshot_cap PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME agc_untracked_forget COMMAND agc_driver_recorder_tests --untracked-forget-only)
set_tests_properties(agc_untracked_forget PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME agc_untracked_revalidate COMMAND agc_driver_recorder_tests --untracked-revalidate-only)
set_tests_properties(agc_untracked_revalidate PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME agc_untracked_hint_proof COMMAND agc_driver_recorder_tests --untracked-hint-proof-only)
set_tests_properties(agc_untracked_hint_proof PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME agc_draw_snapshot_epoch COMMAND agc_driver_recorder_tests --draw-snapshot-epoch-only)
set_tests_properties(agc_draw_snapshot_epoch PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME agc_multisample_guest_access COMMAND agc_driver_recorder_tests --multisample-only)
Expand All @@ -1891,7 +1897,7 @@ if(BUILD_TESTING)
add_test(NAME agc_metadata_pass COMMAND agc_driver_recorder_tests --metadata-pass-only)
set_tests_properties(agc_metadata_pass PROPERTIES SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_driver_recorder agc_idle_publish_wait agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_draw_snapshot_epoch agc_multisample_guest_access agc_repeat_storage_guard agc_draw_inputs_in_place agc_draw_inputs_copied agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_driver_recorder agc_idle_publish_wait agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_untracked_forget agc_untracked_revalidate agc_untracked_hint_proof agc_draw_snapshot_epoch agc_multisample_guest_access agc_repeat_storage_guard agc_draw_inputs_in_place agc_draw_inputs_copied agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only)
set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ void FlushGpuWrites(std::uint64_t address, std::size_t bytes);
// value before consulting the page states and reading directly, and the same value after, knows
// the states it consulted were current and no page it read changed its mapping meanwhile.
std::uint64_t ForgetSerial();
std::uint64_t ForgetStamp(std::uint64_t address, std::size_t bytes);

// Attribution of guest memory accesses (the [hooksync] line in Recorder.cpp and the read-site
// counts of the [guestmem] line, APS5_PROFILE_DRAW): a sync the flush hook makes for an access has
Expand Down
28 changes: 27 additions & 1 deletion core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ namespace {
std::atomic<std::uint64_t> forgetCalls{0};
// See ForgetSerial: odd while a ForgetPages call stores its page states.
std::atomic<std::uint64_t> forgetSerial{0};
constexpr std::size_t ForgetSlotCount = 65536;
constexpr std::uint64_t ForgetBlockBytes = 65536;
std::array<std::atomic<std::uint64_t>, ForgetSlotCount> forgetSlots{};
std::atomic<std::uint64_t> wideForget{0};

void raiseForgetSlot(std::atomic<std::uint64_t>& slot, std::uint64_t serial) {
auto current = slot.load(std::memory_order_relaxed);
while (current < serial && !slot.compare_exchange_weak(current, serial, std::memory_order_release, std::memory_order_relaxed)) {}
}
std::atomic<std::uint64_t> forgetBytes{0};
std::atomic<std::uint64_t> collectMemoHits{0};
std::atomic<std::uint64_t> collectEpochBumps{0};
Expand Down Expand Up @@ -539,8 +548,15 @@ std::size_t ImagePagesSize() {
void ForgetPages(std::uintptr_t address, std::size_t bytes) {
forgetCalls.fetch_add(1, std::memory_order_relaxed);
forgetBytes.fetch_add(bytes, std::memory_order_relaxed);
forgetSerial.fetch_add(1, std::memory_order_release);
const auto started = forgetSerial.fetch_add(1, std::memory_order_release) + 1;
Pages().forget(address, bytes);
const auto done = started + 1;
if (bytes != 0) {
const auto first = address / ForgetBlockBytes;
const auto last = (address + bytes - 1) / ForgetBlockBytes;
if (last - first >= ForgetSlotCount) raiseForgetSlot(wideForget, done);
else for (auto block = first; block <= last; ++block) raiseForgetSlot(forgetSlots[block % ForgetSlotCount], done);
}
forgetSerial.fetch_add(1, std::memory_order_release);
}

Expand Down Expand Up @@ -839,6 +855,16 @@ std::uint64_t ForgetSerial() {
return forgetSerial.load(std::memory_order_acquire);
}

std::uint64_t ForgetStamp(std::uint64_t address, std::size_t bytes) {
if (bytes == 0) return 0;
const auto first = address / ForgetBlockBytes;
const auto last = (address + bytes - 1) / ForgetBlockBytes;
if (last - first >= ForgetSlotCount) return forgetSerial.load(std::memory_order_acquire);
auto newest = wideForget.load(std::memory_order_acquire);
for (auto block = first; block <= last; ++block) newest = std::max(newest, forgetSlots[block % ForgetSlotCount].load(std::memory_order_acquire));
return newest;
}

Commitment DescribeCommitted(std::uint64_t address, std::size_t bytes, bool writable) {
require(bytes <= std::numeric_limits<std::uint64_t>::max() - address, "address range overflow");
const TimedAccess timed(CounterVerify, bytes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,7 @@ class ShaderResources {
const StorageTexture* source = nullptr;
bool depth = false;
bool valid = false;
std::array<std::uint64_t, 5> untracked{};
};
void captureValidation();
// A surface of this object the fast proof found a foreign image pending over: the element
Expand Down Expand Up @@ -634,6 +635,7 @@ class ShaderResources {
bool reusable = false;
std::vector<DirectRegion> directRegions;
std::vector<ValidatedSurface> validatedTextures;
std::vector<std::array<std::uint64_t, 5>> storageUntracked;
// The pending registry's serial at the last Revalidate that proved this object, taken before
// its checks and kept only when unchanged after them (0: none): while it is still the serial,
// no image other than the object's own sources has results pending over its surfaces and
Expand Down
1 change: 1 addition & 0 deletions core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ class StorageTexture : public std::enable_shared_from_this<StorageTexture> {
static std::uint64_t HandedOverSuperseded();
static void SetClearHandOver(bool enabled);
static std::uint64_t UntrackedCompares();
std::array<std::uint64_t, 4> HintedComparison(std::uint64_t address, std::size_t bytes) const;
std::uint64_t GuestBytes() const;
std::uint64_t BudgetBytes() const;
std::size_t OriginalSnapshotBytes() const { return original.capacity(); }
Expand Down
77 changes: 68 additions & 9 deletions core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ struct LookupRecord {
std::uint64_t generation;
const StorageTexture* source;
bool depth = false;
std::array<std::uint64_t, 4> untracked{};
};

thread_local std::vector<LookupRecord>* lookupLogSlot = nullptr;
Expand Down Expand Up @@ -913,7 +914,10 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
committed = std::move(ranges);
}
}
const auto untracked = source == nullptr && generation == 0 && !committed.has_value() && *keys == DccKeys::Uncompressed ? untrackedKey(address, bytes) : std::array<std::uint64_t, 4>{};
const bool keyed = source == nullptr && generation == 0 && !committed.has_value() && *keys == DccKeys::Uncompressed;
const auto forgetStamp = keyed ? GuestMemory::ForgetStamp(address, bytes) : 0;
const auto untracked = keyed ? untrackedKey(address, bytes) : std::array<std::uint64_t, 4>{};
const auto proofKey = untracked[0] != 0 ? std::array<std::uint64_t, 4>{untracked[0], untracked[1], forgetStamp, untracked[3]} : std::array<std::uint64_t, 4>{};
auto& cache = Textures();
const auto key = MakeTextureKey(context.device, words, components, depthCompare);
std::lock_guard lock(cache.mutex);
Expand Down Expand Up @@ -960,7 +964,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
it->committed = std::move(committed);
it->untracked = untracked;
touchTexture(cache, it);
logLookup({it->texture.get(), resource, guestBytes, *keys, generation, nullptr});
logLookup({it->texture.get(), resource, guestBytes, *keys, generation, nullptr, false, proofKey});
reportTextureCounters();
if (profile) LookupOutcomes::Add(LookupOutcomes::SampledHitSnapshot, start);
return it->texture;
Expand Down Expand Up @@ -1018,11 +1022,12 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
if (entry.bytes.size() > SnapshotCompareBytes && (entry.generation != 0 || entry.untracked[0] != 0)) parkSnapshot(cache, entry.bytes);
counters.snapshots.fetch_add(1, std::memory_order_relaxed);
}
const auto madeKey = entry.untracked[0] != 0 ? proofKey : std::array<std::uint64_t, 4>{};
const auto budget = sampledBudget(context, cache);
auto texture = entry.texture;
if (!admitTexture(cache, key, entry.accounted, budget)) {
noteReleased(texture);
logLookup({texture.get(), resource, guestBytes, *keys, generation, source.get()});
logLookup({texture.get(), resource, guestBytes, *keys, generation, source.get(), false, madeKey});
reportCacheCounts(cache, budget);
reportTextureCounters();
if (profile) LookupOutcomes::Add(source != nullptr ? LookupOutcomes::SampledMadeView : LookupOutcomes::SampledMadeSnapshot, start);
Expand All @@ -1031,7 +1036,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
cache.bytes += entry.accounted;
entry.present = Recorder::Presents();
++cache.counts.inserted;
logLookup({texture.get(), resource, guestBytes, *keys, generation, source.get()});
logLookup({texture.get(), resource, guestBytes, *keys, generation, source.get(), false, madeKey});
cache.entries.push_front(std::move(entry));
cache.index[key] = cache.entries.begin();
if (source != nullptr) cache.views.emplace(source.get(), cache.entries.begin());
Expand Down Expand Up @@ -3044,6 +3049,29 @@ void LastRecordIndices(std::span<const void* const> logged, std::span<const void

// Turns the lookups' records into this object's per-texture validation records (see
// ValidatedSurface); the last record of an object is the freshest.
namespace {

std::array<std::uint64_t, 4> hintedProof(std::uint64_t address, std::size_t bytes) {
const auto hint = GuestMemory::UntrackedHint(address, bytes);
if (hint == 0) return {};
const auto* recorder = Recorder::Active();
return {hint, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetStamp(address, bytes), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0};
}

std::array<std::uint64_t, 5> untrackedRangeStamp(std::uint64_t address, std::size_t bytes, const std::array<std::uint64_t, 4>& hinted) {
const auto epoch = GuestMemory::CollectEpoch();
if (epoch == 0 || bytes == 0 || (GuestMemory::ForgetSerial() & 1u) != 0) return {};
if (hinted[0] != 0) return {hinted[0], hinted[1], hinted[2], hinted[3], 1};
const auto* recorder = Recorder::Active();
return {epoch, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetStamp(address, bytes), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0, 0};
}

std::array<std::uint64_t, 5> currentUntrackedStamp(std::uint64_t address, std::size_t bytes, const std::array<std::uint64_t, 5>& recorded) {
return untrackedRangeStamp(address, bytes, recorded[4] != 0 ? hintedProof(address, bytes) : std::array<std::uint64_t, 4>{});
}

}

void ShaderResources::captureValidation() {
const auto& log = lookupLog();
thread_local std::vector<const void*>* loggedSlot = nullptr;
Expand All @@ -3062,6 +3090,16 @@ void ShaderResources::captureValidation() {
if (indices[i] == log.size()) continue;
const auto& found = log[indices[i]];
validatedTextures[i] = {found.resource, found.bytes, found.keys, found.generation, 0, found.source, found.depth, true};
auto& surface = validatedTextures[i];
if (surface.valid && !surface.depth && GuestMemory::CollectWrites(surface.resource.baseAddress, static_cast<std::size_t>(surface.bytes)) == 0) surface.untracked = untrackedRangeStamp(surface.resource.baseAddress, static_cast<std::size_t>(surface.bytes), surface.source != nullptr ? surface.source->HintedComparison(surface.resource.baseAddress, static_cast<std::size_t>(surface.bytes)) : found.untracked);
}
storageUntracked.assign(storageTextures.size(), {});
for (std::size_t i = 0; i < storageTextures.size(); ++i) {
const auto* image = storageTextures[i].get();
if (image == nullptr) continue;
const auto address = image->Descriptor().baseAddress;
const auto bytes = static_cast<std::size_t>(image->GuestBytes());
if (GuestMemory::CollectWrites(address, bytes) == 0) storageUntracked[i] = untrackedRangeStamp(address, bytes, image->HintedComparison(address, bytes));
}
lookupLog().clear();
const auto trim = [](auto& scratch) {
Expand Down Expand Up @@ -3138,7 +3176,11 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::span<const
const auto bytes = static_cast<std::size_t>(surface.bytes);
if (!FlushDepthPlanes(address, bytes).empty()) return fail(FastFail::Changed);
surface.collected = GuestMemory::CollectWrites(address, bytes);
if (surface.collected == 0) return fail(FastFail::Collect);
bool untracked = false;
if (surface.collected == 0) {
if (surface.untracked[0] == 0 || currentUntrackedStamp(address, bytes, surface.untracked) != surface.untracked) return fail(FastFail::Collect);
untracked = true;
}
const auto* source = surface.source;
if (!keyProofs) {
// Without proofs the keys are compared with the record's (a cleared view's identity
Expand Down Expand Up @@ -3176,9 +3218,9 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::span<const
}
if (source != nullptr) {
if (!source->Cached()) return fail(FastFail::Evicted);
queries.push_back({address, bytes, source->GenerationFor(address, bytes)});
if (!untracked) queries.push_back({address, bytes, source->GenerationFor(address, bytes)});
images.push_back(source);
} else {
} else if (!untracked) {
queries.push_back({address, bytes, surface.generation});
}
}
Expand All @@ -3194,7 +3236,11 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::span<const
depthGuardRefusals.fetch_add(1, std::memory_order_relaxed);
return fail(FastFail::Depth);
}
if (GuestMemory::CollectWrites(address, bytes) == 0) return fail(FastFail::Collect);
bool untracked = false;
if (GuestMemory::CollectWrites(address, bytes) == 0) {
if (i >= storageUntracked.size() || storageUntracked[i][0] == 0 || currentUntrackedStamp(address, bytes, storageUntracked[i]) != storageUntracked[i]) return fail(FastFail::Collect);
untracked = true;
}
if (own.dccAddress != 0) {
// Refresh's unchanged branch (its key compare against the keys the content was
// uploaded under, through the image's proof; the memory query below), minus its
Expand All @@ -3204,7 +3250,7 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::span<const
}
if (!unchanged) query(address, address + bytes, image, nullptr, {i, true, false});
if (!image->Cached()) return fail(FastFail::Evicted);
queries.push_back({address, bytes, image->GenerationFor(address, bytes)});
if (!untracked) queries.push_back({address, bytes, image->GenerationFor(address, bytes)});
images.push_back(image);
}
if (!pending.empty()) {
Expand Down Expand Up @@ -3604,6 +3650,19 @@ bool ShaderResources::revalidateFull(std::span<const CompiledShader> shaders, Pr
FastFail reason = FastFail::Count;
bool accepted = false;
bool fast = !noFast && fastRevalidate(serialBefore, refreshed, reason, overlapping, accepted);
{
static const bool verifyUntracked = std::getenv("APS5_VERIFY_UNTRACKED_PROOFS") != nullptr;
const bool anyUntracked = std::any_of(validatedTextures.begin(), validatedTextures.end(), [](const ValidatedSurface& surface) { return surface.untracked[0] != 0; }) || std::any_of(storageUntracked.begin(), storageUntracked.end(), [](const auto& stamp) { return stamp[0] != 0; });
if (verifyUntracked && fast && anyUntracked) {
static std::atomic<std::uint64_t> checked{0}, disagreed{0};
const auto saved = validatedTextures;
const auto savedStorage = storageUntracked;
if (!fullWalk()) disagreed.fetch_add(1, std::memory_order_relaxed);
validatedTextures = saved;
storageUntracked = savedStorage;
if (checked.fetch_add(1, std::memory_order_relaxed) % 100000 == 99999) std::fprintf(stderr, "[verify-untracked] %llu fast proofs over untracked memory checked by the full walk, %llu disagreed%c", static_cast<unsigned long long>(checked.load()), static_cast<unsigned long long>(disagreed.load()), 10);
}
}
// T1 (design_cpu_final M3, rule RT1): a Pending failure whose overlapping images are foreign
// to the surfaces is resolved by the own objects' refresh (what the walk's lookups would do to
// them) and the fast proof run again, which is then authoritative (it accepts what stays
Expand Down
Loading
Loading