RSX: publish GET only when it has advanced

The drain fix made every path that can idle or block publish GET immediately,
which is required for correctness: a producer waiting on ring space needs to see
the progress we made before we stopped consuming.

It publishes far more often than that requires. The empty and busy cases return
straight to the run loop, so a ring that has gone quiet re-enters them once per
iteration with GET unmoved. Web of Shadows measured 137000 loop iterations per
frame against 46000 method dispatches; the remaining 91000 were republishing a
value the guest already had.

GET shares a 64-byte line with put, which the guest PPU writes from another
cluster, so each of those is a coherence miss taken against the thread feeding
the ring. The cost lands on the producer rather than on the RSX, which is why it
presented as a freeze with sound still playing: the PPU stalls on the contended
line while threads that never touch it keep running.

GET is ours to write, so tracking the last published value and skipping an
unchanged store keeps the guarantee -- progress is still announced exactly once
after the last advance -- without the repeats.
This commit is contained in:
jpolo1224
2026-08-10 22:50:14 -04:00
parent d909537f3f
commit 8041edf5bc
2 changed files with 29 additions and 4 deletions
+18 -4
View File
@@ -46,13 +46,27 @@ namespace rsx
return;
}
m_ctrl->get.release(m_internal_get);
m_ctrl->get.release(m_published_get = m_internal_get);
}
void FIFO_control::sync_get_force() const
{
m_get_sync_counter = 0;
m_ctrl->get.release(m_internal_get);
// Publish real progress only. The drain paths call this every time they are entered,
// and while the ring is empty or blocked that is once per run loop iteration with GET
// unmoved: measured at ~91000 of 137000 iterations per frame in Spider-Man: Web of
// Shadows, every one of them a coherence miss on the line holding put.
//
// The cost lands on the guest rather than on us, which is why it reads as a freeze
// with sound: the PPU feeding the ring is the thread contending for that line, so it
// stops producing while threads that never touch it carry on.
if (m_published_get == m_internal_get)
{
return;
}
m_ctrl->get.release(m_published_get = m_internal_get);
}
void FIFO_control::restore_state(u32 cmd, u32 count)
@@ -266,7 +280,7 @@ namespace rsx
}
// Update ctrl registers
m_ctrl->get.release(m_internal_get = get);
m_ctrl->get.release(m_published_get = m_internal_get = get);
m_remaining_commands = 0;
}
@@ -471,7 +485,7 @@ namespace rsx
if (!count)
{
m_ctrl->get.release(m_internal_get += 4);
m_ctrl->get.release(m_published_get = (m_internal_get += 4));
data.reg = FIFO_NOP;
return;
}
+11
View File
@@ -198,6 +198,17 @@ namespace rsx
void sync_get_force() const;
mutable u32 m_get_sync_counter = 0;
// Last value actually stored into ctrl->get. GET is ours to write -- the guest only
// writes put -- so an unchanged value need not be republished.
//
// This matters because the force paths are re-entered continuously while GET stands
// still. An empty or blocked ring returns to the run loop immediately, so a producer
// that has gone quiet leaves us taking the cross-cluster miss above tens of thousands
// of times a frame against a line the guest PPU is trying to write. Announcing
// progress once is the whole requirement; announcing it repeatedly holds up the
// producer we are waiting for.
mutable u32 m_published_get = umax;
// Snapshot of g_cfg.core.rsx_fifo_accuracy, refreshed once per packet. Reading the
// config goes through a seq_cst atomic load, an ldar on ARM64 the compiler cannot
// hoist, and it was consulted once per FIFO argument.