diff --git a/rpcs3/Emu/Cell/lv2/lv2.cpp b/rpcs3/Emu/Cell/lv2/lv2.cpp index 05ca1df49..f7ff72267 100644 --- a/rpcs3/Emu/Cell/lv2/lv2.cpp +++ b/rpcs3/Emu/Cell/lv2/lv2.cpp @@ -7,8 +7,6 @@ #include "Emu/Cell/PPUFunction.h" #include "Emu/Cell/PPUThread.h" - -#include #include "Emu/Cell/SPUThread.h" #include "Emu/Cell/ErrorCodes.h" #include "sys_sync.h" @@ -27,7 +25,6 @@ #include "sys_ppu_thread.h" #include "sys_process.h" #include "sys_prx.h" -#include "Emu/RSX/RSXThread.h" #include "sys_rsx.h" #include "sys_rwlock.h" #include "sys_semaphore.h" @@ -1252,136 +1249,6 @@ public: // Hang watchdog. This thread is independent of the RSX thread, which is the whole // point: a hang where the RSX spins inside a method handler starves the stall check // that lives on it. Cheap -- two atomic loads and a clock read unless it fires. - // Spin detector. - // - // Five earlier attempts at catching this hang all keyed on something STOPPING -- - // frames, then lock traffic -- and all missed it, because nothing stops. Measured on - // the hung device: PPU[0x1000000] burning 4.36s of CPU across 4s of wall clock, i.e. - // more than a full core, while rsx::thread spun on NV406E_SEMAPHORE_ACQUIRE. Tales of - // Xillia 2 white-screens when its Bandai logo is skipped, and the guest's main thread - // is not blocked at all -- it is in a tight guest-side wait loop, making no syscalls, - // producing no log output, and taking no locks. That is why a frame-based detector, - // a lock-based one, and a "no syscalls at all" one each saw nothing wrong. - // - // So look for the opposite of a stall: a thread that is RUNNING (no wait flag) whose - // cia has not left a small window. A spin loop is a handful of instructions branching - // to themselves; ordinary execution walks cia across the whole binary within a second. - // Threads parked in a syscall carry cpu_flag::wait and are skipped, so a normal idle - // game cannot trip this. - { - // Count how often a thread is found at the SAME cia, and DECAY on a miss - // rather than resetting. - // - // The range-window version reset on every excursion, and the report made that - // look like success: widest_range=0x0 does not mean an identical cia, it means - // the entry had just been reset, so lo==hi. The thread mostly sits in a small - // loop -- one sample caught it inside 0x500 -- and occasionally wanders far - // enough (a helper, a syscall handler) to blow any fixed window. Anything - // all-or-nothing therefore measured nothing. - // - // Decaying instead means an occasional excursion costs one point rather than all - // of them, so a thread parked at one address 90% of the time still accumulates, - // and a thread genuinely making progress still falls to zero. - struct spin_state { u32 cia; u32 hits; }; - static std::unordered_map s_spin; - static u32 s_dumps = 0; - static u32 s_worst = 0; - static u32 s_worst_cia = 0; - - if (Emu.IsPaused() || Emu.IsStopped(true)) - { - s_spin.clear(); - s_dumps = 0; - s_worst = 0; - } - else - { - u32 worst = 0; - u32 worst_id = 0; - u32 worst_cia = 0; - - idm::select>([&](u32 id, ppu_thread& ppu) - { - // Parked in a syscall: skipped, not counted against. A thread that is - // only ever parked never accumulates hits, so idle cannot look like spin. - if (!ppu.state.load().none_of(cpu_flag::wait)) - { - return; - } - - const u32 cia = ppu.cia; - auto& st = s_spin[id]; - - if (st.hits == 0) - { - st.cia = cia; - } - - if (cia == st.cia) - { - st.hits++; - } - else if (st.hits > 0) - { - st.hits--; - - // Fully decayed: adopt wherever it is now as the new candidate. - if (st.hits == 0) - { - st.cia = cia; - } - } - - if (st.hits > worst) - { - worst = st.hits; - worst_id = id; - worst_cia = st.cia; - } - }, idm::unlocked); - - s_worst = worst; - s_worst_cia = worst_cia; - - if ((worst == 30 || worst == 45) && s_dumps < 2) - { - s_dumps++; - ppu_log.error("PPU 0x%07x has been at cia=0x%08x for %u of the last " - "samples. Dumping guest threads.", worst_id, worst_cia, worst); - rsx::dump_guest_threads_now(); - } - - if (worst == 0) - { - s_dumps = 0; - } - } - - if (i % 10 == 0) - { - // Report the widest range any tracked thread is covering. If this still does - // not fire, that number IS the answer -- it says how big the loop actually is - // and therefore what the threshold has to be, instead of costing another - // reproduction to find out. - - ppu_log.notice("spin detector: tracked=%u best=%u@0x%08x dumps=%u", - static_cast(s_spin.size()), s_worst, s_worst_cia, s_dumps); - } - } - - // IsStopped(TRUE), not the default. - // - // The default overload is `m_state <= system_state::stopping`, and the enum orders - // stopped, loading, stopping, running -- so it reports true while the game is - // LOADING. A hang during a load is precisely what this watches for (Tales of Xillia 2 - // white-screens mid-load after its logos are skipped), so the plain guard skipped the - // watchdog on every tick of the exact case it exists for, and did it silently: no - // declined decision to see, just no call at all. - if (!Emu.IsPaused() && !Emu.IsStopped(true)) - { - rsx::poll_frame_stall_watchdog(); - } - const bool is_paused = Emu.IsPaused(); // Force-print all if paused diff --git a/rpcs3/Emu/RSX/RSXThread.cpp b/rpcs3/Emu/RSX/RSXThread.cpp index bed445461..4db620d9d 100644 --- a/rpcs3/Emu/RSX/RSXThread.cpp +++ b/rpcs3/Emu/RSX/RSXThread.cpp @@ -1433,83 +1433,6 @@ namespace rsx // idm::unlocked deliberately: this runs on the RSX thread, and taking the id lock here to // diagnose a hang would add exactly the kind of dependency being diagnosed. A torn read of // a diagnostic line costs nothing. - // Independent watchdog, called from a thread that is NOT the RSX thread. - // - // check_frame_stall() runs from do_local_task, on the RSX thread's own FIFO loop. That is - // fine for a guest-side hang with the RSX idle, and useless for the opposite case: Tales of - // Xillia 2 hangs with rsx::thread spinning at 100% CPU inside NV406E_SEMAPHORE_ACQUIRE, - // waiting on a guest semaphore the stopped guest will never write. Spinning inside a method - // handler it never returns to do_local_task, so the detector that would report the hang is - // starved by the hang itself. Measured on the device: +4.99s of thread CPU across 5s of wall - // clock, with guest mutex traffic at exactly zero for minutes. - // - // So the same condition is polled from the PPU syscall usage thread, which ticks once a - // second and keeps running regardless. This half only DUMPS -- the on-screen message and the - // native-UI flip stay on the RSX side, because the overlay is not safe to drive from here. - // Dump on demand, for a caller that has decided a hang is happening by its own means. - // The frame-based checks in this file cannot see a hang whose render loop keeps flipping. - void dump_guest_threads_now() - { - dump_guest_threads_stalled(); - } - - void poll_frame_stall_watchdog() - { - const u64 now = get_system_time(); - - // Say why this declined, once every 10s. - // - // Three attempts at this detector have now failed SILENTLY on a reproducible hang -- it - // disarmed itself, then it sat on the thread that was stuck, then it would not seed its - // own clock -- and each time the only evidence was an absence, which says nothing about - // which branch below won. Reporting the decision costs one line per ten seconds and turns - // the next failure into a fact instead of another guess. - { - static atomic_t s_last_report{0}; - - if (now - s_last_report >= 10'000'000) - { - s_last_report = now; - - const u64 last = g_last_frame_time; - - rsx_log.notice("stall watchdog: progr=%d last_frame=%llu age=%lldms dumps=%u", - g_progr_text ? 1 : 0, last, - last ? static_cast(now - last) / 1000 : -1, g_frame_stall_dumps.load()); - } - } - - // SEED the timestamp, do not merely bail on it. - // - // The RSX-side check sets g_last_frame_time here, which is what starts its clock. This - // half returned instead -- and since the case it exists for is an RSX thread too stuck to - // run that check, nothing ever seeded it, g_last_frame_time stayed zero, and the watchdog - // bailed on every tick forever. Blocked in exactly the scenario it was written for. - if (g_progr_text || !g_last_frame_time) - { - g_last_frame_time = now; - g_frame_stall_reported = false; - g_frame_stall_dumps = 0; - return; - } - - if (now - g_last_frame_time < 30'000'000) - { - return; - } - - // Shares the RSX side's counter and its two-sample budget, so the two paths can never - // produce four dumps between them. - if (const u32 taken = g_frame_stall_dumps; taken < 2) - { - g_frame_stall_dumps = taken + 1; - - rsx_log.error("No frame presented in %us and the RSX thread is not polling: dumping " - "guest threads from the watchdog.", (now - g_last_frame_time) / 1'000'000); - - dump_guest_threads_stalled(); - } - } static void dump_guest_threads_stalled() { std::string out; diff --git a/rpcs3/Emu/RSX/RSXThread.h b/rpcs3/Emu/RSX/RSXThread.h index d3c78bde3..024c72a20 100644 --- a/rpcs3/Emu/RSX/RSXThread.h +++ b/rpcs3/Emu/RSX/RSXThread.h @@ -38,13 +38,6 @@ extern rsx::frame_capture_data frame_capture; namespace rsx { - // Polled once a second from the PPU syscall usage thread. The RSX-side stall check cannot - // report a hang in which the RSX thread itself is spinning; see the definition. - void poll_frame_stall_watchdog(); - - // Dump every guest thread's state now. For a caller that detected a hang some other way -- - // the frame-based checks cannot see one whose render loop is still flipping. - void dump_guest_threads_now(); class RSXDMAWriter;