Repository navigation
Conversation
CnRJay
left a comment
There was a problem hiding this comment.
Per-stage relocation is a sensible next step after #4943. The verify mode (APS5_VERIFY_DRAW_ENTRIES=1, 0 differing stages) is good evidence for the demo. I found two correctness gaps that the demo probably doesn't reach, and one gap in the evidence.
1. The content hash stops at 64 words of each read. In relocatedContentKey, each (offset, words) read is hashed as count = std::min<std::size_t>(words, MaxContentWords). words is the length of a run that learnPendingRelocation derives from variant.runs, which follow the captured regions. A descriptor table reached through a relocatable base can easily be longer than 64 dwords; eight 8-dword T#s are already 64. Words past the first 64 are then never compared. A table that moved, with the same first 64 words and different descriptors after them, produces the same alias key and reuses the donor stage with stale descriptors. Either hash the whole run (in chunks of MaxContentWords), or ban the register in learnPendingRelocation when a run is longer than the limit, so such a stage stays on the capture path.
2. RecordedDrawState is one process-wide object with no lock. recordedDrawState() returns a function-local static that apply and setLibraryState read and write without synchronization. The recorded.commands == commands check only protects against a different command buffer when two command buffers are recorded one after the other. Suppose two queue workers record concurrently (the measurements say "one thread", so I assume multi-threaded recording is a supported mode). Thread A checks that recorded.commands is its own buffer, thread B then stores its own viewport and stencil values, and A compares its new values against B's. A then skips a vkCmdSet* whose value differs from what A's command buffer actually holds, so state set for one queue's draws leaks into another's. Keeping this state per command buffer (in the recorder's batch, next to commands) avoids both the race and the handle comparison. thread_local would also work if a command buffer is only ever recorded on one thread. Besides that, the filter looks right: Begin re-applies everything with known = false, and the two other graphics binds reset recorded.commands.
3. The MaxAliases limit clears both maps outright. drawStageDonors.clear() / drawDecodes.clear() at 16384 entries means a title that crosses the limit loses every donor at once. For several frames, every moved draw then goes back to full capture. That is exactly the stall pattern this PR is trying to remove, and it would show up as a periodic hitch rather than a lower average. An LRU, or evicting the oldest half, keeps the steady state. At least a counter in the profile output would make the cliff visible.
Evidence:
- The per-packet times (41.5 → 30.7 µs) and capture counts come from profiled runs.
- The unprofiled metric, audio produced as a share of real time, is a reasonable proxy for this demo. It is still not the
aps5-summary.jsonllines plus traces captured with noAPS5_PROFILE_*switch that the Performance report form asks for. - The base also includes local changes outside the PR.
The summary lines of #4943 alone and of #4943 + this PR, on the same machine, would make the claim checkable. So would a run on a second title that doesn't use a RADV-style descriptor-set layout, which would show that the extra per-draw content hashing (CopyMapped plus the hash of every relocatable read) costs nothing where relocation never hits.
Also, since only the commits after #4943's head belong to this PR, Depends on: #4943 should be filled in the checklist, if it isn't already.
…eir read ranges Two kinds of draw snapshot were always copied whole. A moved buffer is a read-only guest buffer of a draw that reuses a cached resource build whose V# pointed elsewhere (MovedReadOnlyBuffers, the draw relocation of boykopovar#5609): the snapshot copies the buffer at the draw's own address and size instead of the build's. Its read ranges are relative to the V# base, so they hold at the new address; they are now placed from the draw's own binding and V# when the move is found and used like an unmoved buffer's. A formatted load (buffer_load_format_*, tbuffer_load_*) addresses its element like any MUBUF/MTBUF load, index * stride + offset + instruction offset + soffset (RDNA 2 ISA, buffer addressing), and reads its components at byte offsets within one element of its data format, whose largest is 32_32_32_32 (16 bytes); SpirvRuntimeBuffer's formatted() reads each component through the same address(). Such a load now reads up to 16 bytes from its offset instead of the whole buffer. The "moved" and "formatted load" reasons are gone from the whole-copy counts; the shader disk cache format goes to 32. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
adriadam10
left a comment
There was a problem hiding this comment.
Reviewed against the diff; two correctness problems, and the PR still needs a rebase once #4943 lands.
DrawRelocate.cpprelocatedContentKey: each read is hashed asmin(words, MaxContentWords)(64). A descriptor table longer than 64 dwords that moved, with the same first 64 words and different descriptors after them, gets the same alias key and reuses the donor stage with stale descriptors. Hash the whole run in chunks, or stop treating the register as relocatable inlearnPendingRelocationwhen a run is longer than the limit.Pipeline.cpprecordedDrawState(): a function-local static with no lock, read and written byapply/setLibraryState. With two queue workers recording, therecorded.commands == commandscheck does not stop one worker comparing against the other's viewport/stencil values and skipping avkCmdSet*its command buffer needs. Keep this state per command buffer (next tocommandsin the recorder's batch) or make itthread_localif recording is single-threaded; please say which holds.MaxAliases:drawStageDonors.clear()/drawDecodes.clear()at 16384 drops every donor at once, so every moved draw goes back to full capture for a while. Evict the oldest part instead, or at least count it in the profile output.- Evidence: the 41.5 → 30.7 µs numbers come from profiled runs and the base has local changes outside the PR. The unprofiled
aps5-summary.jsonllines of #4943 alone vs #4943 + this PR, plus one title without a RADV-style descriptor layout showing the extra hashing costs nothing, would make it checkable. - The branch conflicts with main and is stacked on #4943, which is open and conflicting too; rebase after it merges.
Welcome back once these are addressed.
8417747 to
00816c0
Compare
adriadam10
left a comment
There was a problem hiding this comment.
Thanks for the update. The 64-word hash and the all-at-once eviction are fixed (chunked hashing, dropEveryOther with the evicted counter). One point from the last review is still open:
Pipeline.cpp:690recordedDrawState()is still a function-localstaticwith no lock. With two queue workers recording, therecorded.commands == commandscheck does not stop one worker comparing against the other's viewport/stencil values and skipping avkCmdSet*its own command buffer needs. Keep the state per command buffer (next tocommandsin the recorder's batch), or make itthread_localif recording is single-threaded; please say which one holds.- The branch still conflicts with main and is stacked on #4943, which is open and conflicting. Please rebase once #4943 lands.
00816c0 to
d6abd55
Compare
adriadam10
left a comment
There was a problem hiding this comment.
Thanks for the rebase. The 64-word hash and eviction fixes are in; one point from the last review is still open:
Pipeline.cpp:690recordedDrawState()is still a function-localstaticwith no lock. With two queue workers recording,recorded.commands == commandsdoes not stop one worker comparing against the other's viewport/stencil values and skipping avkCmdSet*its own command buffer needs (and the writes race). Keep the state per command buffer (next tocommandsin the recorder's batch), or make itthread_localif recording is single-threaded; please say which one holds.- Still stacked on #4943, which is open; please rebase once it lands.
|
@CnRJay thanks for the review. Rebased onto
The dependency is unchanged: this PR depends on #4943, and only the commits after that PR's head belong to it. This push also fixes the Linux build that @Raghav314 reported on #4943: the size asserts in AI-assisted (Claude Code). |
d6abd55 to
bf7ad56
Compare
|
Restructured, no change in behaviour: the commit that reuses draw snapshots across draws nothing wrote between is now its own PR, #5906. It helps every Windows title, not only the relocation path here. On This branch now carries #5906's two commits under its own, so it depends on #4943 and #5906. Since #5906 went up separately it has a gentler eviction (every other entry at the limit), a recorder-level test, and the debt line for #1441 updated. All three are in this branch too. Both branches are rebased onto AI-assisted (Claude Code). |
|
I prepared a Linux current-main port of this existing PR (no replacement PR): commit The 12 conflict files needed more than taking either side: the port retains main's DrawTail/DrawFinisher synchronization, mutex abstraction, mesh descriptor and user-data mask metadata, optimized-pipeline scheduling, and sample-order APIs. Main already has the pending-image index shortcut. The Linux result-size assertion grows from current main's 544 to 568 bytes for bufferBaseSlots, rather than reverting the other current ABI sizes. The new diagnostic switch now has its required environment row. One additional integration issue was reproducible: the moved-buffer regression still failed with the current SRT tape, but passed with On RX 6650 XT / RADV, Release, with
Full logs, exact pins, patch, negative controls and earlier failed-attempt evidence — SHA-256 This is focused integration/correctness evidence, not a title FPS result, full-app build, or fresh Windows validation. FAR's cache traces motivated trying this port; a matched scene comparison remains to be done. The complete main-to-port patch is in the archive if useful for rebasing your branch. AI-assisted: Codex. |
|
@JJenkx one note on the port (9592291), in case it's taken up: it changes the shader disk cache encoding ( |
|
FAR (PPSA03766) same-scene measurement of the Linux port above is complete. This single pair does not establish a reliable FPS gain. Both arms start from main
The observed +2.7% FPS / lower median is small, p99 is slightly worse, and other-host CPU estimates differ (13.5% vs 10.2% of the 32-thread machine over the sampled trace intervals). Application CPU use is 2.20 vs 2.22 cores. I would not use this to claim a reproducible title speedup. Protocol: separate private conversion/runtime/cache per arm; identical copied surface checkpoint, inputs and settings; first warm-up, then fresh measured process; official Directly compared both measured 270-second images: same surface swimmer and flooded-village camera/geometry/lighting. Candidate direct-window observations at 179/275 seconds show changing water/debris/pose; repeated warm XComposite captures alone were insufficient to assess animation. These extra readbacks were outside the timing window. Audio and new gameplay progression are unverified. Inclusive emitted shader-stage tags suggest lower materialization time (4.30 → 3.46 ms/present) but higher capture time (1.62 → 2.25); the combined shader compile/link tag is 17.04 → 17.46. These overlap and are thresholded trace slices, not independent costs or counts of new SPIR-V compilations. Exact baseline and candidate commits are available on Official summary lines, main then change: {"commit":"6d8e60339069","dirty":false,"env":{"APS5_DRAW_CACHE_ENTRIES":"4096","APS5_HOST_IMPORT_MIB":"16384","APS5_PAD_INPUT_FILE":"<path>","APS5_PIPELINED_DRAWS":"1","APS5_PLAYGO_UNLISTED_CHUNKS":"absent","APS5_TRACE_EVENTS":"1048576","APS5_TRACE_SECONDS":"250:10"},"gpu":"AMD Radeon RX 6650 XT (RADV NAVI23)","driver":"radv Mesa 26.2.4-arch1.1","os":"Linux 7.2.9-arch1-1","cpu":"AMD Ryzen 9 7950X 16-Core Processor","cpu_threads":32,"memory_mib":127901,"title_id":"PPSA03766","title":"FAR: Changing Tides","content_version":"01.002.000","resolution":"1920x1080","window":{"spec":"seconds 250:10","first_present":2311,"ms":10008.724},"presents":85,"frame_ms":{"p50":114.783,"p90":134.222,"p99":149.962,"max":149.962},"gpu_busy_ms_per_present":55.275,"worker_busy_pct":{"queue worker 0x0":94.882,"queue worker 0x38":75.145},"top_phases_ms_per_frame":{"Driver.Packet":198.669,"Driver.Packet.memory_wait":80.392,"Driver.Packet.dispatch":49.004,"Driver.Packet.draw":48.359,"Driver.Draw":47.986,"Graphics.Draw":44.087,"Graphics.Draw.draw_and_resource_release":36.727,"Driver.Dispatch":31.671},"trace_dropped":false,"trace":"aps5-trace-6d8e60339069-1325165.json.gz"}
{"commit":"cfa83360da4e","dirty":false,"env":{"APS5_DRAW_CACHE_ENTRIES":"4096","APS5_HOST_IMPORT_MIB":"16384","APS5_PAD_INPUT_FILE":"<path>","APS5_PIPELINED_DRAWS":"1","APS5_PLAYGO_UNLISTED_CHUNKS":"absent","APS5_TRACE_EVENTS":"1048576","APS5_TRACE_SECONDS":"250:10"},"gpu":"AMD Radeon RX 6650 XT (RADV NAVI23)","driver":"radv Mesa 26.2.4-arch1.1","os":"Linux 7.2.9-arch1-1","cpu":"AMD Ryzen 9 7950X 16-Core Processor","cpu_threads":32,"memory_mib":127901,"title_id":"PPSA03766","title":"FAR: Changing Tides","content_version":"01.002.000","resolution":"1920x1080","window":{"spec":"seconds 250:10","first_present":2232,"ms":10088.239},"presents":88,"frame_ms":{"p50":112.525,"p90":127.275,"p99":153.497,"max":153.497},"gpu_busy_ms_per_present":53.55,"worker_busy_pct":{"queue worker 0x0":94.789,"queue worker 0x38":75.146},"top_phases_ms_per_frame":{"Driver.Packet":193.433,"Driver.Packet.memory_wait":77.781,"Driver.Packet.dispatch":49.223,"Driver.Packet.draw":46.692,"Driver.Draw":46.349,"Graphics.Draw":41.059,"Graphics.Draw.draw_and_resource_release":34.138,"Driver.Dispatch":32.008},"trace_dropped":false,"trace":"aps5-trace-cfa83360da4e-1412121.json.gz"}AI-assisted: Codex. |
|
I refreshed the reference port as one commit on main The per-stage donor index now stores cache keys/stage indices, resolves only live entries and touches the donor LRU, preserving upstream 4119611. Updating cached stage variants publishes a copy-on-write DrawEntry snapshot so existing readers retain immutable stage vectors. The port also retains current materialization-exception tracing and forwards tracing/subdraw arguments through secondary draws. The codec correction flagged above is included: shader disk format 48, pipeline journal 11 because it embeds the changed EncodeResult/DecodeResult layout. PR #6959 independently uses those version numbers; the two branches have not been combined, so advance/reconcile versions and layout guards if either lands first. Validation on RX 6650 XT / RADV Linux:
Source patch, exact-head logs/JUnit, build receipts and negative controls — SHA-256 AI-assisted: Codex. Original feature credit remains with TheAggie; eviction/LRU correction follows upstream 4119611 and the review feedback here. |
|
@TheAggie here's a retail data point for the case this PR handles, in case it helps the next step. Metal Gear Solid 4 (PPSA18194, 1.4.1), first gameplay level. Built from
So nearly every draw moves its user-data pointers, and relocation by content recovers about 15% of them. I haven't checked whether the rest differ only in buffer base addresses, which is what this PR patches, or in other words as well. If it's useful, I can run MGS4 on a rebased head of this PR and post the same counters. AI-assisted: yes (Claude Code) |
|
Data point from Astro's Playroom (PPSA01325, 1.905), CPU Plaza, Windows 11, RTX 4080 SUPER. This is a case the PR does not reach. Measured (one run each,
* Both builds have the direct-memory constant from #12192, because plain
So this is not the PR's head. Why (from a local diagnostic counter; the interpretation is AI-generated): about 93 % of the data-only candidates that fail stop on a user word that the SRT walk reads as a value and that the stage also forwards as shader data. That word is the first of a 4-word group and changes on every draw. It looks like the base of a buffer descriptor that the title holds inline in user SGPRs. The relocation here moves descriptors reached through a pointer ( AI-assisted (Claude Code). |
|
Profile data showing the case this PR targets, from a current tree: ASTRONEER PPSA27623, intro scene, Linux, RX 6650 XT/RADV, on #8091
Other intervals are within a few percent. So 99.9% of lookups miss only because user-data words or pointers changed, and about 92% of their stages are materialized again. In a 25 s So I'd expect at most about 9 ms per frame from this kind of reuse in this scene, and less if the GPU is the limit. I haven't separated which user words are buffer base addresses and which are values. Once the branch is rebased onto current |
|
@TheAggie a data point for this PR from RESIDENT EVIL requiem (PPSA31246). I ported its commits onto a local branch of Porting onto current
requiem
I can share the port branch if a rebase would help. |
Verdictstale+rebase Findings
Freshness envelope
Cross-PR ledger
|
|
@TheAggie another title with the case this PR targets: ARMORED CORE VI FIRES OF RUBICON (PPSA06774, 01.009.000), first mission. This doesn't test this PR, which doesn't merge with current Build Frame time (
Draw cache (
So almost every miss differs only in user data, and the data-only path still compiles about 86 % of the stages it looks at. Other PRs on the same scene (same build plus one PR, one run each, standing still):
Run-to-run spread at the same spot was about 10 %. Not measured (AI-generated interpretation): this matches what QRMOE found on PPSA31246, user-data pointers that move on every draw. I haven't checked whether the moved words here are single buffer bases that this PR's AI-assisted: yes (Claude Code) |
|
A real-game case this directly addresses, as motivation. Crysis Remastered (CUSA18659) on AnyPS5 is CPU-bound at ~7-9 FPS, and the per-draw prepare dominates. Profiled with The cause is the one this PR targets. The draw key ( We ruled out table size: with (We did not test this PR on Crysis directly — it runs on an older-main integration branch, and cherry-picking the stack onto it conflicts heavily in the core draw path.) |
|
A data point for this PR from a PS4 title, Crysis Remastered (CUSA18659, GCN guest shaders): it hits this PR's case on most of its draws, and a local stand-in for it is where most of the speedup comes from. The case.
Numbers.
Three things this title needed, in case they apply to this PR's version:
The PS4 itself does not move these words between consecutive frames of a settled scene. On a PS4 Pro (FW 10.71) two consecutive flips of the same spot had identical VS and PS user data for all 1,158 draws. When the ring cycles, the fetch code and the table contents mostly change too (11 of 507 fetch subroutines and 15 of 1,439 tables kept their contents). So relocation matters while the scene changes. The PC runs above are scripted and the scene keeps changing: their draw count per frame varies between about 1,100 and 1,560. Happy to send the stand-in, or to help with the rebase if that is useful. |
|
@TheAggie I’m preparing a diagnostic-only change that shares Your head Exact-head qualification and limits: 17 positive processes across main/change (including both physical GPU vendors), plus a test-only control failing specifically on missing context. The official overlap audit remains unresolved, and I have not opened a new upstream PR. Would that separate scope/order work for you? AI-assisted: yes (Codex); author responsible. |
|
Windows/SRT tape integration feedback from Another Crab's Treasure (PPSA20423, 02.000.000), on a local rebase/extension of this work onto pinned main Windows 11 10.0.26200, Ryzen 5 5600X, RX 6700 XT, AMD Vulkan 26.8.1, WinLibs GCC 15.2.0 r7, Release. The tape needs to preserve Additional integration findings:
The current local extension also applies eligible reuse through the shape-matched/data-only path, including runtime-only pushed user words and the existing pure scalar-data refresh mechanism. The final mixed-input regression changes a memory value and a shader input together, checks the rendered color, and requires reuse in normal mode. Twelve distinct GPU cases plus three CPU contracts pass on this GPU. Vulkan is required, the shader cache disabled, Khronos validation confirmed loaded, and the compared logs have no new VUID IDs. The shader-memory capture/validation/replay suite also passes. A visually confirmed beach run with both Normal warm-scene screening of the final local composition was 11.21 FPS on baseline versus 10.73 FPS on the candidate (one run per arm; last five eligible 10-second intervals, individual frame durations, identical visually confirmed view and settings, hashes matched). This establishes no FPS benefit; the earlier buffer/table-only screen was 10.56 versus 10.57 FPS. I am sharing functional/integration evidence, not claiming that this PR or the local extension fixes this title's performance. The original runtime is restored. Game bring-up #8239. Local SRT tape origin-port diff against pinned main 5fb1747diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp
index ddda8b1fc3..916f96519e 100644
--- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp
+++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp
@@ -325,10 +325,13 @@ struct SrtTape::Frame {
std::uint8_t* states;
bool nullRoot = false;
std::uint32_t addressDepth = 0;
+ std::uint32_t operandDepth = 0;
std::size_t descriptorBytes = 0;
};
bool SrtTape::evaluate(Frame& frame, std::uint32_t index, std::uint64_t& result) const {
+ const auto& node = nodes[index];
+ if (frame.operandDepth > 0 && frame.runtime.userDataUse != nullptr && (node.op == Op::Read || node.op == Op::ReadConstBuffer)) frame.runtime.userDataUse->operandReads.push_back(node.source);
const auto state = frame.states[index];
if (state == 1) {
result = frame.values[index];
@@ -336,7 +339,10 @@ bool SrtTape::evaluate(Frame& frame, std::uint32_t index, std::uint64_t& result)
}
if (state == 2) return false;
std::uint64_t value = 0;
- if (!compute(frame, nodes[index], value)) {
+ ++frame.operandDepth;
+ const bool computed = compute(frame, node, value);
+ --frame.operandDepth;
+ if (!computed) {
frame.states[index] = 2;
return false;
}
@@ -438,7 +444,10 @@ bool SrtTape::compute(Frame& frame, const Node& node, std::uint64_t& result) con
frame.nullRoot = true;
return false;
}
- if (frame.runtime.userDataUse != nullptr) frame.runtime.userDataUse->reads.emplace_back(address, origin);
+ if (frame.runtime.userDataUse != nullptr) {
+ frame.runtime.userDataUse->reads.emplace_back(address, origin);
+ frame.runtime.userDataUse->readInstructions.push_back(node.source);
+ }
if (node.loaded && frame.runtime.accessible != nullptr && !frame.runtime.accessible(frame.runtime.userContext, address, sizeof(std::uint32_t))) return false;
if (auto* trace = frame.runtime.readTrace; trace != nullptr) {
if (node.source == trace->leaf) trace->leaves.emplace_back(trace->leafSlot, address);
@@ -556,6 +565,9 @@ bool SrtTape::Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::ve
const auto baseBefore = use != nullptr ? use->asBase : 0;
const auto valueBefore = use != nullptr ? use->asValue : 0;
const auto useReadsBefore = use != nullptr ? use->reads.size() : 0;
+ const auto useReadInstructionsBefore = use != nullptr ? use->readInstructions.size() : 0;
+ const auto useOperandReadsBefore = use != nullptr ? use->operandReads.size() : 0;
+ const auto useSourceLeadsBefore = use != nullptr ? use->sourceLeads.size() : 0;
const auto refuse = [&] {
if (trace != nullptr) {
trace->leaves.resize(leavesBefore);
@@ -566,6 +578,9 @@ bool SrtTape::Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::ve
use->asBase = baseBefore;
use->asValue = valueBefore;
use->reads.resize(useReadsBefore);
+ use->readInstructions.resize(useReadInstructionsBefore);
+ use->operandReads.resize(useOperandReadsBefore);
+ use->sourceLeads.resize(useSourceLeadsBefore);
}
return false;
};
@@ -585,6 +600,19 @@ bool SrtTape::Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::ve
std::fill_n(states, nodes.size(), std::uint8_t {0});
}
Frame frame {runtime, values, states};
+ const auto noteErasedOperand = [&](IrValue* expression, std::uint32_t root) {
+ if (use == nullptr || root == NoNode || (nodes[root].op != Op::Read && nodes[root].op != Op::ReadConstBuffer) || nodes[root].source == nullptr || expression == nullptr) return;
+ auto* value = expression->Resolve();
+ for (;;) {
+ if ((value->Opcode() == IrOpcode::BitCastU32F32 || value->Opcode() == IrOpcode::BitCastF32U32) && value->ArgumentCount() == 1) value = value->Argument(0)->Resolve();
+ else if (value->Opcode() == IrOpcode::ReadConst && value->ArgumentCount() == 2) {
+ auto* slot = value->Argument(1)->Resolve();
+ if (!slot->HasImmediate() || slot->Type() != IrType::U32 || slot->ImmediateU32() >= plan.srtReads.size() || plan.srtReads[slot->ImmediateU32()].value == nullptr) break;
+ value = plan.srtReads[slot->ImmediateU32()].value->Resolve();
+ } else break;
+ }
+ if (value != nodes[root].source) use->operandReads.push_back(nodes[root].source);
+ };
struct Scratch {
std::uint32_t attempt = 0;
@@ -639,6 +667,7 @@ bool SrtTape::Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::ve
for (const auto image : block.images) active[imageBase + image] = 1u;
for (const auto slot : block.flatSlots) activeFlat[slot] = 1u;
std::uint64_t condition = 0;
+ if (block.condition != nullptr && runtime.readSpecializationMemory != nullptr) noteErasedOperand(block.condition, conditionRoots[index]);
const bool cleanEvaluable = block.condition != nullptr && runtime.readSpecializationMemory != nullptr && conditionRoots[index] != NoNode && evaluate(frame, conditionRoots[index], condition);
if (frame.nullRoot) return refuse();
if (cleanEvaluable) {
@@ -661,12 +690,21 @@ bool SrtTape::Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::ve
for (std::uint32_t dword = 0; dword < source.dwordCount; ++dword) {
std::uint64_t word = 0;
frame.descriptorBytes = source.dwordCount > 1u ? static_cast<std::size_t>(source.dwordCount - dword) * sizeof(std::uint32_t) : 0u;
+ noteErasedOperand(source.dwords[dword], sourceDwords[sourceIndex][dword]);
const bool ok = evaluate(frame, sourceDwords[sourceIndex][dword], word);
frame.descriptorBytes = 0u;
if (!ok) return refuse();
value.dwords[dword] = static_cast<std::uint32_t>(word);
}
}
+ if (use != nullptr && source.dwordCount != 0 && source.dwords[0] != nullptr) {
+ auto* lead = source.dwords[0]->Resolve();
+ if (!lead->HasImmediate() && lead->Opcode() == IrOpcode::ReadConst && lead->ArgumentCount() == 2) {
+ auto* slot = lead->Argument(1)->Resolve();
+ if (slot->HasImmediate() && slot->Type() == IrType::U32 && slot->ImmediateU32() < plan.srtReads.size() && plan.srtReads[slot->ImmediateU32()].value != nullptr) lead = plan.srtReads[slot->ImmediateU32()].value->Resolve();
+ }
+ if (!lead->HasImmediate() && IsRawRead(plan, *lead)) use->sourceLeads.emplace_back(sourceIndex, lead);
+ }
evaluated.push_back(value);
}
auto& flattened = scratch.flattened;
@@ -681,6 +719,7 @@ bool SrtTape::Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::ve
trace->leafSlot = read.flatOffset;
}
std::uint64_t word = 0;
+ noteErasedOperand(read.value, flatRoots[slot]);
const bool ok = read.flatOffset < flattened.size() && evaluate(frame, flatRoots[slot], word);
if (pure) trace->leaf = nullptr;
if (!ok) return refuse();AI-assisted implementation and testing: yes, Codex, continuing the ZCode handover. No game files or assets attached. |
What
Second step for #4054, stacked on #4943. That PR finds a cached draw when the user-data pointers moved and the data behind them is equal. A guest driver that keeps its buffer descriptors in a table reached through such a pointer (RADV's descriptor sets) still missed on every draw whose buffers moved, because the descriptor's base address is among the words a stage is validated against.
bufferBaseSlotsfield of a result, and its Linux-only size asserts expect the grown structs.AnyPendingOverlapsconsults the pending image range index first. The state the filter compares against is kept per command buffer, in the recorder batch's open render pass: it is reset when a new pass starts and marked unknown after the back-face and second-channel splits and after stencil replay (which sets the stencil reference itself).APS5_VERIFY_DRAW_DATA_HITS=1) is left to that verification instead of being reused.APS5_NO_DRAW_RELOCATION=1turns the relocation off,APS5_NO_DRAW_STATE_FILTER=1the dynamic state filter.Only the commits after #4943's head are this PR.
Tested
Windows 11, NVIDIA GeForce RTX 5070 Ti, MinGW-w64 GCC 15.2.0, rebased on main
940eba9a2.ctest: 1270 of 1292 pass, 14 skipped.guest_call_trace_abort,windows_backtrace_reportandagc_driver_store_over_untrackedfail, and fail the same way on plain main here.agc_driver_prepared_shaders_debugfailed once in a parallel run and passes alone. Three newer tests,agc_driver_prepared_graphics_prebuild_0/_1andundeliverable_host_exception, fail under my test runner because it skips their environment changes and pass pattern; run with those, all three pass.ShaderDiskCache.cpponly compile on Linux. I have no Linux build; I compiled the structs under MinGW libstdc++ on x86_64, where all asserted sizes match.agc_shader_disk_cache's result round trip now also sets and comparesbufferBaseSlots.agc_driver_draw_cache_moved_buffer: a pixel shader loads a buffer descriptor through a user-data pointer and reads its colour from that buffer. Once the draw has been seen twice, drawing it again with the descriptor's base changed, with the same or other contents, hits the cached draw. It fails withAPS5_NO_DRAW_RELOCATION=1.main940eba9a2, this PR and perf(agc): find cached draws across moved user-data pointers #4943 against the same build without them (both with the same local changes the frontend needs to boot, immediate present, audio muted, three alternating unprofiled runs): audio produced at 34.5%, 35.7% and 35.6% of real time without, against 45.7%, 44.1% and 42.8% with.4430780bd(without perf(agc): cache a draw key the second time it is seen #5216) plus local changes outside this PR that the frontend needs to boot. perf(agc): find cached draws across moved user-data pointers #4943 alone against perf(agc): find cached draws across moved user-data pointers #4943 plus this PR, same local base, one thread,ANYPS5_PRESENT_MODE=immediate, two unprofiled runs each: audio produced at 43.6% and 43.4% of real time against 57.6% and 57.9%. One profiled run each: queue worker 41.5 µs against 30.7 µs per draw packet; stage captures per 10 s 92,728 in 185,141 draws against 14,859 in 238,439; draws with every stage reused 75% against 96.8%.APS5_VERIFY_DRAW_ENTRIES=1, which captures every hit again and compares: 0 stages differed in every report (118,722 hits in the largest).Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt.mdfiles or images added to the repository (attach them to this PR)🤖 Generated with Claude Code