handoff: VSync/event-wedge fixes + iterate 2.A–2.BC research notes

Source changes (dormant parity infra, retained from iterate 2.AI/2.AO):
- xenia-kernel/exports.rs: nt_create_event manual_reset polarity +
  related event wiring
- xenia-gpu/mmio_region.rs: D1MODE_VBLANK_VLINE_STATUS hardcode parity

Also lands the audit-runs/ analysis notes (.md/.txt/.json digests) for the
iterate 2.x VSync/0x10e8/0x1004 wedge investigation. Raw trace dumps
(.jsonl/.gz/.csv/.stdout) and agent worktrees (.claude/) are gitignored as
regenerable local artifacts — see memory + HANDOFF for the running findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-05 07:19:08 +02:00
parent acd1656753
commit ef93a4fa14
620 changed files with 108303 additions and 1 deletions

View File

@@ -4848,6 +4848,44 @@ pub(crate) fn handle_consume(state: &mut KernelState, handle: u32) {
}
}
/// 2.AJ — reciprocal-shadow clear for guest-pointer auto-reset dispatchers.
///
/// `handle_consume` only updates the kernel-side shadow's `signaled` /
/// `count`. For guest-pointer dispatchers (handle is a guest VA into a
/// `KEVENT` / `KSEMAPHORE` struct in title memory), the **source of truth
/// for the next wait's `refresh_pkevent_shadow_from_guest` is the guest
/// memory `SignalState` at `[ptr + 4]`**. Without clearing that on
/// consume, a wait that fast-paths re-signals the shadow from stale
/// guest memory on its next iteration → spin-forever loops (e.g.
/// Sylpheed tid=7 polling its VSync-signaled dispatcher 6.5 M times in
/// 208 s post-2.AI). Canary doesn't need this because `XEvent::Set` /
/// auto-reset host-event `Wait` consume on a single host OS object —
/// there is no shadow/guest split to bridge.
///
/// Mirrors the rising-edge `signal_state != 0 → shadow.signaled = true`
/// in `refresh_pkevent_shadow_from_guest`. We only mutate the falling
/// edge — i.e. only clear guest memory when we just cleared our shadow.
/// NT-handle objects (small int handles) are unaffected because they
/// have no guest-memory dispatcher to bridge to.
pub(crate) fn handle_consume_reciprocal_clear(
state: &KernelState,
mem: &GuestMemory,
handle: u32,
) {
if handle < 0x1_0000 {
return;
}
match state.objects.get(&handle) {
Some(KernelObject::Event { manual_reset, signaled, .. })
| Some(KernelObject::Timer { manual_reset, signaled, .. }) => {
if !*manual_reset && !*signaled {
mem.write_u32(handle + 4, 0);
}
}
_ => {}
}
}
/// Register a guest thread as a waiter on a handle (for later wake).
pub(crate) fn handle_enqueue_waiter(state: &mut KernelState, handle: u32, r: ThreadRef) {
match state.objects.get_mut(&handle) {
@@ -5560,6 +5598,9 @@ fn do_wait_single(ctx: &mut PpcContext, state: &mut KernelState, handle: u32, ti
state.audit_wait(handle, ctx.lr as u32, "do_wait_single", 0);
if handle_signaled(state, handle) {
handle_consume(state, handle);
// 2.AJ — clear guest-memory SignalState for guest-pointer auto-reset
// dispatchers so the next refresh doesn't re-signal from stale memory.
handle_consume_reciprocal_clear(state, mem, handle);
ctx.gpr[3] = STATUS_SUCCESS;
return;
}
@@ -5628,6 +5669,8 @@ fn do_wait_multiple(
if wait_all {
for &h in &handles {
handle_consume(state, h);
// 2.AJ — reciprocal guest-memory clear (see do_wait_single).
handle_consume_reciprocal_clear(state, mem, h);
}
ctx.gpr[3] = STATUS_SUCCESS;
} else if let Some((idx, &h)) = handles
@@ -5636,6 +5679,8 @@ fn do_wait_multiple(
.find(|&(_, &h)| handle_signaled(state, h))
{
handle_consume(state, h);
// 2.AJ — reciprocal guest-memory clear (see do_wait_single).
handle_consume_reciprocal_clear(state, mem, h);
ctx.gpr[3] = idx as u64; // STATUS_WAIT_0 + idx
} else {
ctx.gpr[3] = STATUS_SUCCESS;