From ecd657006a329aeb274a73ba744ef989c322f43e Mon Sep 17 00:00:00 2001 From: kuba Date: Fri, 31 Jul 2026 11:14:38 +0200 Subject: [PATCH] AGC: retain label producers and report failed release writes (#719) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * AGC: retain active label producer records Ported from the archived silent-hill-minimal branch (274ccfdf). RegisterLabelProducer bounded its history with RemoveRange(0, 1024) once it reached 4096 entries, which evicts the oldest records regardless of whether their label write has completed. Those records are not a diagnostic cache: a suspended DCB resolves its wait_reg_mem by finding the producer that will write the watched label. Dropping an active record hides an earlier same-submission label write, so a legitimate in-stream fence suspends forever and the graphics queue stops. Compact only completed history, and let the list exceed the soft bound when every record is still active — correctness over a diagnostic limit. Two hardening changes over the ported version, because a suspended queue is exactly when every record is active: - compaction is a single order-preserving pass instead of repeated RemoveAt, which would shift the tail per eviction (quadratic) while the label gate is held; - when a pass frees nothing the bound is raised to twice the current count, so registration does not rescan the whole list on every subsequent add, and it is reset once compaction can make progress again. Tests: 664 pass, 0 fail (SharpEmu.Libs.Tests 598 -> 600). * AGC: keep produced label values a suspended wait still needs GpuWaitRegistry.RecordProduced dropped the entire produced-value table once it reached 8192 entries: if (_lastProduced.Count >= 8192) { _lastProduced.Clear(); } Those entries are release state, not a cache. CollectDeadlockBroken is the only way out for a DCB suspended on a WAIT_REG_MEM whose label the guest has since recycled for other data: it replays the value a real producer already wrote to that label. Clearing the table wholesale erases exactly the records live waiters depend on, so every such waiter is stranded permanently — the suspended graphics queue never resumes, and with it the render thread and the whole engine thread graph park. That is what Silent Hill (PPSA10112) hits. Its final unsatisfied waits watch labels holding guest allocator metadata (each reads a pointer into its own region rather than the expected 1), i.e. recycled memory, which is precisely the case the deadlock breaker exists to handle — yet agc.deadlock_break fires 0 times across a run with 490 wait suspensions and 1068 successful release_mem writes, because the producing values had already been cleared. Prune only values no registered waiter is watching, and let the table exceed the soft bound while they are all watched. Same reasoning as the label-producer history in the preceding commit: correctness of synchronization state takes precedence over a diagnostic bound. Tests: 666 pass, 0 fail. The regression test fails against the previous wholesale Clear() and passes with the prune, verified by reverting the fix. * AGC: report release-label writes that never reach guest memory Both release_mem handlers recorded the produced label value only on success: if (wroteData && dataSelection is 1 or 2) GpuWaitRegistry.RecordProduced(...) with no else. A failed write is not benign here. That packet is the producer a suspended WAIT_REG_MEM is waiting for, so when it fails the label is neither written nor recorded, CollectDeadlockBroken has no value to replay, and the graphics queue stays suspended forever — the render thread and the engine thread graph park behind it with nothing in the log to say why. Measured on Silent Hill (PPSA10112): the fatal AccessViolation lands exactly here, on the presenter thread, in ApplySubmittedReleaseMem's label write into a protected guest page. The faulting variant kills the process; the returns-false variant wedges the GPU silently. Only the first was ever visible. Report the failure (rate-limited: first 16, then powers of two) instead of dropping it. This does not fix the underlying write failure — it makes a permanently suspended queue diagnosable rather than silent. Tests: 666 pass, 0 fail.