mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
master
25
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1dec1cb2fd |
feat(app): nightly builds default the emulation thread On, with the phase 3 gate (#967 3d) (#1082)
* test(app): no_threaded_limits keeps every feature on while the emulation thread runs (#967 G1a)
Fails when a threaded-only gate, its user-facing text or its limit test comes back, when the GDB stub is locked for any
reason but hardcore, when a file starts asking "am I threaded?" beyond its allowlisted count, or when tsan.supp grows.
Verified red on the pre-3a tree (
|
||
|
|
85cd3f1642 |
feat(app): debugger halt and GDB stub with the emulation thread on (#967 phase 3b) (#1068)
* feat(app): bounded halt pump on DebugCore; the UI mirrors the halt DebugCore owns the halt: pump() serves the debug lane, waits at most kPumpWait (4 ms) on a debug-lane-only wait and returns VJ_DBG_RESUME_NONE to GDBHalt's own loop, which keeps the socket service and the halt timeout. The UI writes its verdict tagged with the halt serial it saw, so a verdict for a dead halt never resumes the next one; frameEnded() closes a halt the core left on gdb's say-so. DebugSession mirrors HaltState once per UI frame (syncHalt) and fires onHaltUi / onResumeUi on serial edges; attach/detach/breakNow are ordered through the lane and a pending detach. Inline keeps the nested UI frame. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): a STEP the core never re-halts must not leave the UI mirror halted pump() returns STEP without bumping the halt serial, so the bar stays up while the core steps. If the frame ends before the core re-halts (the stepped instruction ended it, or the target had stopped), frameEnded() found nothing halted and the mirror stayed halted for good; inline, stepEmulation() is gated on halted(), so the frame that would re-halt never ran and F9 / Detach wrote verdicts nothing took. DebugCore now remembers a STEP in flight (cleared by the next halt entry or setIface) and frameEnded() closes it like a foreign resume. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): verdicts and detach only against the core's live halt After a STEP verdict the halt serial is unchanged but the core is running until it re-halts, so DebugSession::detach() offered a DETACH for a serial the re-halt then dropped, left detachPending_ set (attach / Break refused) and, with hardcore turning on in that window, left the core attached. resume / step / detachAndResume now require the core's halt to be live (parked, same serial); detach and detachAndResume fall back to the lane detach otherwise, and that post also releases the pump if it runs inside a live halt. haltedFrame() syncs the mirror first so the first nested frame meets the current halt; the inline resume hook keeps headless audio unpaused as before. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): the emulation thread pumps the halt; quitting ends it Threaded, the pump's wait is App::haltWait (serve the debug lane for at most 4 ms, refill the views when a request is pending); frameEnded() runs after every retro_run so a halt gdb ended is closed on our side (inline: from syncDebug outside a frame). beginQuit() sets quitting_, wakes the pump and waits at most 200 ms for the detach the pump answers with, before the auto-save, the SRAM flush and the 3 s unload; closing the window inside an inline halt takes the same path. haltedFrame runs only inline. The halt bar says "HALTED by gdb" for a halt the app did not ask for; a halt at the CPU window's own temporary breakpoint (run to cursor, step over) is ours (DebugSession::ownsBreakAt). Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): calls and frame advances give up when a halt begins; content changes detach first EmuThread::call and advance take a give-up predicate: a call queued behind a frame that halts returns instead of parking the UI behind the busy spinner; advance registers its budget on the debug lane and waits with the same predicate. Every UI-thread call passes one (App::emuCall). Threaded, open / close / reset / disc detach the debugger first and then proceed; save / load state and boot refuse with a toast; frame-lane posts wait for Continue and the halt bar and the Core options tab say so. Inline keeps refusing (the call would run inside retro_run). RetroAchievements replies ride the debug lane, so none is ever cancelled. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): a given-up disc follow-up retries; close reports a re-halt; deterministic give-up and re-drain tests followDiscSwap advances seenDiscSwaps_ only when its call ran, so a halt no longer loses the RA / cheats follow-up. closeContent says so when its detach predicate gave up. The netplay headless step stops re-budgeting a frame whose advance gave up. testCallGiveUpLosesToStart covers the compare-and-swap-lost branch of call(), and testAdvanceBudgetDoesNotOvertakeFramePost covers the loop's second frame-lane drain; the 300-iteration smoke loop is gone. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): the halt and the GDB stub work on the emulation thread; a silent host behind every halt; hardcore locks the stub The phase 1 limits are gone: Break / Step / breakpoints work threaded and virtualjaguar_gdb_stub is a plain option again. Whenever the stub is enabled the app attaches its host on the emulation thread inside the load (before the first frame, so a gdb_wait boot halt is seen) and again after any frame it is missing, so every gdb halt flows through the app's pump: the halt bar shows it, calls give up, quitting detaches. RetroAchievements hardcore locks the stub to disabled at load, and a hardcore flip with the stub open reloads the game so the lock reaches the core (phase 3 plan, decisions 2 and 3). Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): B4 review: pre-frame silent host, gdb halts survive headless inline, Detach clears breakpoints - emuRunOneFrame re-attaches the silent gdb host before the frame, so a gdb halt right after a Detach is hosted - headless inline haltedFrame releases only app halts and quit; gdb's halt keeps the core's socket loop serving - Detach drops the session breakpoint list with the core's table - Settings > Emulation thread hint reflects that the halt and stub work threaded - locked-option warning is generic; a hardcore restart waits out an inline halt Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(app): the halt and the GDB stub end to end on the emulation thread debug_e2e_threaded drives break / step / continue / detach / hardcore / quit from a UI thread that never blocks over 50 ms, pins the option post that applies after Continue, the save state that gives up when a halt begins, a posted lane detach run inside a live halt, the 3 s join, and a gdb_wait boot halt with no client (Detach, quit). gdb_threaded_halt finds a headless run halted at boot from a gdb client, reads memory, continues, and quits the app mid-halt, threaded and inline, plus an inline hardcore variant: SIGINT / SIGTERM end a headless run cleanly (installed after SDL_Init, headless only). The headless inline loop now closes a gdb halt after each frame (frameEnded + syncHalt), without which the hardcore reload retry stays blocked. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): B5 review: the cancelled save is proven absent after frames run; free ports; signal handler resets debug_e2e_threaded waits for frames after the halt ends before asserting the cancelled save left no file (it is held while halted, so the old check was vacuous) and saves to a different file; the gdb_wait sections probe a free port; the test also fails on ThreadSanitizer. The signal handler restores SIG_DFL after storing the flag (a second signal kills a wedged run) and g_signalQuit is asserted lock-free. gdb_threaded_halt.py's watchdog kills live children; a missing Python3 is reported at configure time. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(app): the halt and the GDB stub on the emulation thread Halt model, Detach, hardcore lock and reload, gdb_wait, headless signal quit, testing; README, testing.md, design section 6 (2026-10-22 as-shipped notes), plan 3b task list. Includes the MSVC fix: debug_e2e_threaded includes <ws2tcpip.h> for socklen_t. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): quitting while a gdb client holds a halt no longer hangs The pump answered a quit with DETACH, which drops the app's host inside the core. A gdb client's breakpoint that is still armed (a hot loop) then fires again at once, with the socket open and no host: the core parks in GDBHalt() for the client, and the quit's flush, autosave and unload wait on it forever. The pump now answers CONTINUE and the host stays attached (unloading drops it). Every wait in the quit epilogue is bounded (flush 1 s, autosave 1.5 s, skipped while the core is still halted); the unload keeps its 3 s backstop. Also: re-attach the silent gdb host before each frame (inline stepEmulation, headless inline loops), comment/doc fixes, EmuThread::stop precondition, and the three gdb tests' PASS regexes lose their ^ anchor. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): a hardcore restart that gives up stays pending and retries Achievements::pump cleared resetPending_ before the restart and ignored its result. Threaded, the restart can give up when the core halts again (a gdb client with a breakpoint still armed): hardcore was then active without a clean restart and the open GDB stub bypassed the hardcore lock. The restart now stays pending until it takes. reset() returns whether it ran. A give-up with the game still loaded (and, for the reload, the stub still open) retries at the next pump; a restart that failed to load the game, or closed the stub, is dropped. The cheat reset is posted once and the restart is counted once (--ra-expect-reset), the give-up is logged once. Tests: VJ_TEST_RESTART_GIVEUPS=N makes the first N restarts report a give-up (a headless run cannot stage the re-halt); gdb_hardcore_flip_giveup_* (reload path) and ra_hardcore_reset_giveup_* (reset path), inline and threaded. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): a signalled threaded --neuro headless run still prints the Neuro report The signal-quit block returns before the TIMING/report section; report first (it only touches UI-thread state) so a SIGTERM'd --neuro run behaves like a finished one. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(app): gdb_* tests take their port from the OS at run time (#967) The six tests with hard-coded ports 23457-23463 (gdb_enabled, gdb_enabled_threaded, gdb_hardcore_flip_*, gdb_hardcore_flip_giveup_*) all failed on the hosted Windows ARM64 runner with 'failed to open port': every one of the distinct ports was refused, which points at a reserved/excluded range, not a collision (the Python gdb tests, which already pick a free port, passed there). tests/with_free_port.py binds 127.0.0.1:0, reads the port back, substitutes {PORT} in the command and, with --config-ini, writes the saved stub setting for the hardcore tests. The assertions and ctest regexes are unchanged. Registered only with Python3 (STATUS message otherwise). RESOURCE_LOCK vj_gdb_port on every test that binds a stub port closes the bind-0 TOCTOU window. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): start Winsock before the core loads so the GDB stub can bind on Windows (#967) Every gdb_* test that opens the stub failed on hosted Windows x64 with "[GDB] stub enabled but failed to open port N", on fixed and on OS-chosen free ports alike. The core's GDBSockOpen (src/debug/gdbsock.c) calls socket() without WSAStartup, because its libretro host (RetroArch) starts Winsock. The app never did on that path (neuro and netplay only start it for themselves), so socket() failed with WSANOTINITIALISED and the stub stayed disabled. The feature was therefore broken for users on Windows too, not only in tests. platform::initNetworking() now runs WSAStartup once, right after SDL_Init and before any core load, headless runs included. A no-op on macOS and Linux. Winsock is reference counted, so neuro's and netplay's own WSAStartup stay harmless. Also corrects the comments that blamed reserved port ranges. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(app): with_free_port.py hands the app explicit stdout/stderr (#967) The Packages / Windows x64 job builds the GUI-subsystem exe (VJ_APP_WIN32_GUI). There, gdb_enabled, gdb_enabled_threaded and the gdb_hardcore_flip* tests missed their PASS regexes. Popen(cmd) left the child's standard handles to inheritance, so Windows started the GUI-subsystem app with no stdout. console_attach.cpp then attached the runner's console, and the output never reached ctest's pipe. The wrapper now passes stdin=DEVNULL and stdout/stderr explicitly, so the output stays on the pipe. Tests that ctest launches directly already get redirected handles and were unaffected. Refs #1075 Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: dry <dry@x> |
||
|
|
6ce76c1cf8 |
feat(app): Neuro on the emulation thread (#967 phase 3c) (#1066)
* feat(app): AgentPlayer and the snapshot's agent channel (#967) The Neuro agent's input becomes a program the emulation thread replays: InputSnapshot gains an agent channel (AgentStep / AgentProgram / AgentProgress; publishAgent, takeAgent, reportAgent, agentProgress) behind its own mutex and a gen atomic, so the per-event lock is untouched. InputQueue gains version, lineage, firstFrame, trim and program, and AgentPlayer is the pure emulation-thread replica (load, frame, progress), driven by pollAgent. Unit-level only: the app is wired in the next task. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): Neuro presses through the input snapshot (#967) The UI-side Agent now publishes the input it accepted as an AgentProgram (Agent::publishTo) and absorbs the replica's progress (Agent::absorb), which trims its queue and drives busy(), pressedNow() and framesPlayed(). The app's input poll runs the AgentPlayer replica one step per emulated frame, inline and threaded alike (pollAgent); Agent::frame() and InputManager::agentControls are gone (InputManager::addAgentControls keeps the inline hashes unmoved). The #1025 pins are ported to the replica (testAgentGating through a Replica helper, loopback with a frame-numbered record) and extended (testAgentProgram: busy covers older progress, release_all and hardcore clear the replica within one poll, Neuro off, append mid-play does not replay). New neuro_core_e2e runs the replica against the real core (inline, lock-step threaded, free-running at 600 Hz, disabled) and neuro_app_inline runs the headless app against a fake Neuro server. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): Neuro reads RAM watches through a DebugViews request (#967) Neuro no longer reads core RAM through the retro_get_memory_* externs. The profile's watches are one DebugViews request, a Memory request over the covering range or a Dram request when the span is wider than 64 KiB, and the emulation thread's copier fills it after each frame. neuro::WatchFeed (neuro_watch.h) owns the request on the UI thread: it gives a changed range a fresh id so the old range's buffer is never read as the new one, reads the buffer at most every 0.25 s, and adopts it only when it is ok, current and answers exactly the request asked. Anything else leaves the state text at "?". GameState::ram / ramSize become watchBytes / watchBase, watchValue gains a base argument, and Agent::watchRange gives the covering range. Headless inline runs move the refresh serial (syncDebug) and run the copier after each frame, and the end-of-run report prints the state line (neuro_watch_inline pins a cheat byte read through the Dram path). Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): Neuro works with the emulation thread on (#967) The gate that turned Neuro off while the emulation thread is on is gone: settingsFrom no longer disables it, neuroInit does not log "off for this run", and Settings > Neuro drops the hint. The program/replica design from the earlier 3c commits (AgentProgram through the input snapshot, AgentPlayer in the core's poll, watches as a DebugViews request) is what makes this safe. The threaded headless --neuro run ticks Neuro in lock step: one frame at a time, in real time, like the inline loop, and neuroHeadlessReport prints in both modes. neuro_off_threaded is replaced by its twins: neuro_on_threaded, neuro_watch_threaded and neuro_loopback_threaded (the loopback run with the poll on its own thread; its frame-numbered press record equals the inline run's). Docs: neuro.md, app/README.md, docs/agent/testing.md. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): Neuro final-review fixes: one lock-step headless loop, watch staleness (#967) - App::runHeadless: the threaded mock-mode and --neuro loops are one lock-step loop in the inline loop's order, so the 3b rebase meets two loops, not three. - WatchFeed::dropIfStale: neuroTick drops a previous game's bytes before it lends them to the state text, not one tick later. - WatchFeed cadence: a rejected buffer is not re-read every UI frame; the clock resets on release, a key change and a dropped epoch. - neuroHeadlessReport forces its read (comment now matches the code). - docs/neuro.md: the 2 MiB copy runs while Neuro is on, about 10 times a second. - Tests: dropIfStale and rejected-buffer cadence (testWatchFeed), a non-zero low byte for the Dram index, absorb on a stale lineage (testAgentProgram); the testAgentGating comment moves back above its function. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
5d0afd4b37 |
feat(app): Tools windows work with the emulation thread on (#967 phase 3a) (#1061)
* feat(app): a debug lane in the emulation thread's command queue (#967) EmuCommands gets Lane::Debug beside the frame lane; the loop drains both at each frame boundary. EmuThread::debugCall / debugPost are safe inside retro_run() once the halt pump serves the lane (phase 3b). Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): DebugViews, per-window requests the emulation thread fills (#967) A request table with a double buffer per request: the UI sets and reads, the emulation thread (inline: the same thread) fills after each frame. A changed request or a content switch invalidates its buffer; no lock is held across a core call. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): DebugCore owns the debug interface; DebugSession marshals to it (#967) DebugCore wraps every vj_debug_iface entry point (emulation thread only); DebugSession routes each action through coreCall/corePost, which run inline until App wires them to the debug lane. setHaltAvailable gates attach, Break and breakpoints so a threaded run can keep the windows and refuse the halt. Inline behaviour is unchanged. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): Memory viewer and Palette read published view buffers (#967) The Memory grid, its selection footer and the Palette ask the view copier for their bytes and draw the published buffers; a content switch (DebugViews epoch) resets them. SearchJob::step and the CRY table each take one callCore. Adds the shared counting test fixture (debug_ui_fixture.h) and the debug_no_direct_iface static guard that the other tool windows reuse. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): resized Memory window adopts its wider buffer; guard catches core() calls (#967) fetchWindow compared only serial and start address when deciding a buffer was already adopted, so a paused window that grew (bytes-per-row change at the region top) kept the shorter window. It now compares size too. CheckNoDirectIface also rejects the session's DebugCore wrappers (s.core().peek( and friends). The smoke test pins the resize case and asserts the Palette really adopted a changed CLUT byte. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): CPU debugger reads Regs and Disasm view buffers (#967) Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): CPU walks drop stale answers; rows re-adopt on every answer (#967) Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): Object list reads the OpSnapshot request; step-a-frame via advance(1) (#967) Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): Object list step bumps the serial after the frame; release posts the clear (#967) Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): Blitter log and Textures through view buffers and core calls (#967) Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): Cheat scanner on the Dram request; Audio scope available with the thread on (#967) The scan pass is one DebugSession::snapshotDram (callCore: the 2 MiB peek runs on the emulation side); the live column reads a published Dram request and copies the buffer only when a newer fill exists. The raw memoryRegion path is gone. The scope gets a framesSeen test hook and a threaded-audio gate test. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): Cheat scanner shows -- until a value was read; new rows carry the scan's own values (#967) Per-row known flag: cleared on a new result list, release and an epoch change; edit prefill and Add as cheat refuse a row with no value. doScan derives the rows from the RAM image it just read, not the older published buffer. Tests: search before the first answer, stale held buffer, collapsed window, unreadable core. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): the Tools windows work with the emulation thread on (#967) The App fills the debug views after each emulated frame (and on the UI thread inline), wires DebugSession to the debug lane, hands the core's interface over again and narrows the thread gate to "halt not available": Break, Step, breakpoints and the GDB stub stay off until phase 3b. --show-window opens a tool threaded. DebugSession::iface()/setThreadBlocked() are gone. debug_views_threaded runs the real core on the emulation thread with the session wired as App wires it. Final-review fixes folded in: - "Step a frame" refuses while the debugger is attached in the threaded case too, because advance() has no give-up. - The scanner's live column waits on its own buffer (DebugViews::serialOf) instead of on any pending view. - The -emu.csv perf trace gains a trailing fill_ns column that times the debug-view copier. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(app): correct the guard's getter list and the Step-a-frame comments (#967) CheckNoDirectIface.cmake named haltSerial() as a legal core() getter. DebugCore has no haltSerial() yet; it arrives in phase 3b. The op_view.h and op_ui.cpp comments now say that Advance also refuses while the session is attached, because emu.advance() has no give-up. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): a replaced core closes the tools like an unload (#967) Threaded openContent() on a running game publishes null and the new debug interface inside one blocking emu.call(), so the UI's syncCore() saw only the final non-null interface: closeAllTools() was skipped and the cheat scanner kept the old game's candidates, which it could then compare or write against the new game's RAM. syncCore() now closes the tools whenever it replaces a core it had (generation moved, had a core), which is what inline does when it sees null then the new interface. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): every breakpoint control greys out while the halt is unavailable (#967) With the emulation thread on, only the toolbar's Break button was gated: the margin click, the context menu's Set breakpoint and "Add at cursor" still called toggleBreakpoint(), which always refuses without the halt machine and reported the misleading "table is full / not attached" error. All three now share one predicate (canAddBp): the menu item and button grey out with haltDisabledReason() as the tooltip, the margin hover says why, and toggleBp() itself answers with that reason without touching the core. Removing a breakpoint stays allowed. The Break tooltip uses the same reason text. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(app): Tools-window actions wait for the frame in progress (#967) README: with the emulation thread on, an action that reaches the core (a Tools-window edit, a load, a state save) waits for the current frame to end, including link play's bounded peer wait. It does not run immediately. Refs #967 Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
0085c4485d |
feat(app): emulation thread phase 2 — achievements, netplay and busy calls (#967) (#1031)
* docs(app): emulation thread phase 2 plan (#967) Achievements and netplay back on with the emulation thread (core-event stream, rc_client_do_frame on the emulation thread, netplay as a UI-thread object fed by the stream), the cheat batch as a regular command, the 250 ms busy spinner, and the Phase 1 open items (MinGW waits, limits after a failed start, the vsync-off nap). Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): EmuSnapshot in its own file, with the core-event stream (#967) CoreEventQueue carries log lines, vj_events and option writes from the emulation thread to the UI thread in order; log lines are bounded, events and option writes never dropped. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): core log lines, events and owner option writes reach the UI thread through the stream (#967) CoreHost::onUiEvent gets every log line and vj_event on the UI thread (pinSnapshot), with an epoch that rides the command queue; option writes made on the emulation thread inside a call() are applied to the UI's mirror and the config there too, never on the emulation thread. A content switch drops queued lines and events. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): netplay works with the emulation thread on (#967) Netplay stays a UI-thread object: the core's link lines and events reach it through CoreHost::onUiEvent, filtered by an epoch bumped at every start/stop/bounce, and the endpoint bounce completes after the core has read "disabled" for 2 emulated frames (3 threaded), counted with frameCount(). Threaded twins of netplay_pair (both sides, voice modem, a mixed threaded/inline pair) and of netplay_no_partner, which must exit 1, never 3. Co-Authored-By: Claude Opus 5.5 <[email protected]> * refactor(app): the cheat list reaches the core as one regular command (#967) CheatManager::apply() builds one CheatBatch (reset, then every set) and hands it to commit, which the app posts to the emulation thread; the three sinks and the UI-thread batch collector are gone. Co-Authored-By: Claude Opus 5.5 <[email protected]> * refactor(app): RetroAchievements safe on two threads (#967) rc_client events are copied into AchEvents and applied in pump(); core RAM, the memory map and the cheat resets are reached on the emulation thread (call/post); read_memory off it answers address checks from the map without touching RAM; the rich-presence line is computed with the frame and published; the mock server is locked. Inline runs behave as before. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): high-resolution short waits for the emulation thread on Windows (#967) The MinGW runner paced threaded fast-forward at one frame per ~15.6 ms (63.4 vs 96.6 frames/s); develop's newest completed MinGW runs (x64 and ARM64) pass emu_thread_unit, but ctest hides a passing test's output so the timer note is not visible there, and no MinGW run on develop has yet carried the 1 ms precise-wait check. preciseSleepNs uses a high-resolution waitable timer, and every app job now prints emu_thread_unit's timer report. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(app): the precise-sleep check uses the shortest wake (#967) A starved host still has some on-time wakes, while timers that are coarse by platform (every wake ~15.6 ms on MinGW) have none, so the check asserts on the shortest of 20 preciseSleepNs(1 ms) waits; the note prints mean and min. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): RetroAchievements works with the emulation thread on (#967) rc_client_do_frame (and with it rc_client_idle) runs after each frame on the emulation thread; the UI pump posts the idle while paused. A threaded headless RA run steps one frame at a time in the inline order, so every ra_* test has a _threaded twin that lands its steps on the same frames. Only the debugger tools and the GDB stub stay off. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): precise_sleep.h includes a lean windows.h (#967) emu_commands.h reaches ~24 MinGW translation units through app.h; WIN32_LEAN_AND_MEAN keeps legacy winsock and macro collisions out of them (the waitable-timer and Sleep calls need nothing outside it). A WaitForSingleObject that does not return WAIT_OBJECT_0 now falls back to Sleep(1), so a failed wait cannot make waitUntil spin. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): quiesce the emulation thread before RetroAchievements shuts down (#967) App::shutdown destroyed the rc_client while a free-running emulation thread could still be inside rc_client_do_frame or a posted emuIdle: it now unloads and stops the thread first, then ach.shutdown() (still before video.shutdown(), images.stop() needs the GL context). AchHttp::ensureWorker runs inside request()'s locked block, and shutdown() moves the worker out under the lock and joins outside it, so request() is safe from the UI and the emulation thread (new two-thread case in ach_http_test). onContentLoaded and onReset resync framesSeen_ so frames that ran before the load are not counted as the new game's. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): a call() that takes over 250 ms shows a busy spinner (#967) EmuThread::call() runs a wait hook on the waiting thread from 250 ms on; the app's hook pumps OS events and swaps a spinner drawn with scissored clears -- no ImGui frame, no call(), no read of a field only a call() writes (CoreHost counts those while the UI waits). Headless, it logs once per call. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): a failed emulation-thread start lifts the feature limits; vsync off naps to the next refresh (#967) Phase 1 final-review minors 2 and 3. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(app): emulation thread phase 2 -- achievements, netplay, the busy spinner (#967) Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): drop the previous game's achievement trackers and indicators after a content switch (#967) Events carry the generation they were queued in; onContentLoaded and onContentClosed bump it, and drainEvents drops the stale tracker, challenge, progress, attempt started/failed and reset events (no HIDE would ever come for them). A real unlock, completions, scoreboards and the connection state are kept. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): drive rc_client's mutating calls from the emulation thread only (#967) App::reset runs ach.onReset() inside the emulation-thread call (rc_client_reset raises events); applyHardcore, setEnabled(false), logout and the pause check (rc_client_can_pause advances the pause-decay state) go through coreCall, so none of them runs while rc_client_do_frame does. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): the vsync-off present loop runs on a display-period deadline grid and handles input while it naps (#967) The nap was anchored at the swap end, so the period was refresh + build + swap (~54-57 Hz on a 60 Hz display) and no input was published during it. LateLatch::advanceGridNs / gridNapNs keep a deadline grid; App::presentNap waits for events as latchWait does and hands them to the emulation thread at once. (SDL 3.4 has no SDL_WaitEventTimeoutNS.) Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): the emulation-thread setting hint names only what is still switched off (#967) RetroAchievements and netplay work with the thread on; the Tools (debugger) menu and the GDB stub do not. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(app): pin that a reload drops the previous game's queued core log lines (#967) The block had no onUiEvent, so it only proved option survival. It now queues a log line inside the same call as the reload, and asserts it is not delivered while the option write still is. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): final-review minors -- ci report step, inline pin, display mode, RAII guards, stale docs (#967) ci: the timer-report step is informational (continue-on-error). CoreHost::pinSnapshot applies queued option events when the UI thread is the owner (thread stopped / inline). The display period is re-read on SDL_EVENT_DISPLAY_CURRENT_MODE_CHANGED. busyFrame's UI-waiting flag and call()'s wait-hook flag are RAII. netplay.h and cheats.h comments, docs/agent/testing.md (TSan recipe) and the ach_event_unit label are brought up to date. The off-thread read_memory answer notes the rcheevos v12.5 assumption. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): serialize achievement replies and synchronize scope capture (#967) Run HTTP and mock rc_client replies on the core owner; guard scope visibility and its sample ring. Add fail-first ownership/concurrency regressions to the sanitizer suite and prevent Windows GL-loader macros from breaking the busy spinner. * fix(app): synchronize achievement UI client access (#967) --------- Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
998dee625c | docs: make AGENTS.md the shared Codex and Claude guide (#1032) | ||
|
|
df0f6d271b |
feat(app): emulation on its own thread, phase 1 (#967) (#1000)
* feat(app): FrameExchange triple buffer for the emulation thread (#967) Frame moves to frame.h so CoreHost, FrameExchange and VideoOut share it. Content epochs keep a previous game's frame from ever being shown. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): EmuCommands, the emulation thread's ordered command queue (#967) Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): PacingClock mode A, the audio clock as master with an even frame grid (#967) A wall-clock grid at the core's period, trimmed at most 0.5% by the smoothed SDL queue level: true Jaguar rate on the sound card's crystal without the 10.7 ms device-pull jitter a raw queue trigger would add. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): PacingClock refill rebases the grid so it converges (#967) During refill frames start at up to 2x the core rate but the grid still advanced a full period per frame, leaving it ahead of the clock by the extra audio just queued. The loop then slept that long, drained the queue below the low-water mark and re-armed refill: bursts and ~66 ms stalls forever, with underruns. Rebase the grid on each early start while refilling, and make testRefillFromEmpty check evenness, rate and underruns after the refill. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): EmuThread, the single owner thread of the core (#967) post() at frame boundaries in order, call() that runs inline on the thread itself and after a stop, callFor() and a join timeout for a core stuck in retro_run, advance() for headless and tests, inline mode for everything else. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): InputSnapshot, the newest input for the emulation thread's poll (#967) States are newest-wins; pointer motion is summed until a frame takes it and is delivered exactly once. InputManager::poll can run on the UI thread with the keypad overlay ticking per emulated frame. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): EmuThread: pending call returns on abandon, exact advance, rate-based pacing test (#967) call() polls for abandonment instead of blocking forever on a queued task; advance(n) counts only budgeted frames so a frame already in flight cannot satisfy it; the pacing test checks rates over measured windows. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): AudioOut safe across the emulation and UI threads; mode-A update (#967) update(false) counts underruns without nudging the playback ratio: in mode A the pacer follows the audio clock and the two controllers must not fight. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): CoreHost owner thread, EmuSnapshot and queued writes (#967) Inline nothing changes. With an owner, the UI reads options, AV info and discs from a pinned EmuSnapshot, option values through a UI-side mirror (the config is still written on the UI thread), and its writes reach the core at the next frame boundary. Any other core call off the owner is refused, counted and logged once. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(app): the real core inline vs on the emulation thread, frame for frame (#967) Also pins a load while frames run (no stale frame, empty buffer inside the load call, rising serials) and the owner rule. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): CoreHost publishes the snapshot before a load call returns (#967) A load or unload made through EmuThread::call() returned before the batch ended, so the UI could pin the previous option table, AV info or disc list. finishLoad, stopSession, a failed startSession and the disc eject / index commands now publish themselves. With no post set while threaded, queued writers are counted and dropped instead of recursing into themselves. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): Settings > Interface > Emulation thread and --emu-thread (#967) Off by default in every build for now (nightlies default it on in phase 3, once nothing is switched off while threaded). Decided once per run; headless runs stay inline unless --emu-thread on. Nothing starts a thread yet. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): VJ_PERF_CSV input-latency and underrun columns, --perf-input (#967) Latency is the oldest input event's SDL timestamp to the core's poll; the first three columns are unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): content and state actions through EmuThread::call; headless --emu-thread on (#967) Load, boot BIOS, unload, reset, save and load state, SRAM flushes and the shutdown unload run where the core lives; inline they run in place as before. The save-state thumbnail is the frame the state was taken on. UI checks ask "halted?" instead of CoreHost::inFrame(). The headless smoke tests pass with --emu-thread on with the inline hashes. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): switch off what is not thread-safe yet while the emulation thread is on (#967) RetroAchievements, netplay, the Tools windows and the GDB stub option are off for a threaded run (phases 2 and 3 bring them over). Cheat changes from the Cheats window reach the core as one command, never half a list. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(app): windowed emulation-thread check for the Mac Studio (#967) Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): the interactive loop with the emulation on its own thread (#967) The UI thread polls and publishes input, takes the newest frame, drains messages and counters and presents; it never waits for the emulation. The 5 s SRAM flush is posted. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): VJ_PERF_CSV event-to-present column (#967) The swap that first shows a frame which polled an input event, timed from the event: inline from the iteration's oldest polled event, threaded through the InputSnapshot publish number each frame carries. --perf-input now injects its key from an SDL timer at a swept delay, so events land evenly over the display period in both loops. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(app): late latch for the threaded loop (#967) With the emulation thread and vsync on, the UI thread waits until just before the next vblank (the slowest recent UI build plus a pad that grows on a missed vblank), publishing input as each event arrives, then takes the newest frame. It tracks the vblank grid from swaps that block and stays off while it cannot. The fake-clock model: event -> present 25.3 ms for 5 ms core frames (inline 25.0, unlatched 46.4); 34.2 ms for 14 ms frames, which only mode B can fix. --late-latch off for A/B measurement. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): late latch backs off on persistent misses, hysteresis (#967) The latch now releases (with a 300-swap cool-down) when more than a quarter of its last 60 waits missed the vblank they aimed at, so a build whose GPU work lands late cannot keep every frame a vblank behind. Once locked it lets go on jitter only at 2 ms (locks below 1 ms), and a wait that itself overran its wake time no longer counts as a miss. The locked/released log line and the perf note show misses, releases and the pad. Co-Authored-By: Claude Opus 5.5 <[email protected]> * ci(app): ThreadSanitizer job over the emulation-thread tests (#967) VJ_APP_TSAN instruments the app, the core, SDL and the tests; the job runs ctest -L emu_thread and fails on any race report. tsan_canary proves the sanitizer is really on. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(app): the Emulation thread setting (#967) Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): route UI disc commands through call() so staged_ is never written off the UI wait (#967) A disc insert can re-stage the CD BIOS in the core (stage_cd_bios via open_disc_and_resolve_boot), which writes CoreHost::staged_ from the VJ_EVT_BIOS_STAGED/REJECTED event. Posted from the UI, that write ran on the emulation thread while the BIOS panel read stagedBios() every frame. The File menu tray/disc items and desktop.cpp toggleDiscTray/selectDisc now run inside EmuThread::call (in place when inline), like reset. core_host.h's threading comment and the plan's boundary table say so. Latent today: the app never registers disc images with the core. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(app): thread-test fail patterns, README frame-skip wording, two comments (#967) - README: the drift alone skips a frame about every 18 s, but frames finishing near the take point also repeat/skip (~1-2.5/s measured at 60 Hz); the display-locked mode planned later addresses it, hence off by default. - emu_thread_flag_rejects, emu_thread_limits, netplay_refused_threaded and gdb_locked_threaded only had a pass regex (ctest then ignores the exit code): they now also fail on "ThreadSanitizer|owner violation". - late_latch.h: kLockAfter counts tracked observations; single outliers do not reset it. emu_thread.h: a call queued before an abandon may still run later on the detached thread, so captures must be by value. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(app): emu-thread tests on CI runners: Windows timer, MSVC capture, libdbus TSan (#967) - emu_thread_unit raises the Windows timer resolution to 1 ms (timeBeginPeriod, winmm), as SDL_Init does in the app; at the default ~15.6 ms, fast-forward could not beat normal speed (63 vs 97 frames/s on the Windows runners). - Its "keeps running" check is a deadline poll instead of a fixed-window quarter-rate floor, which a starved macOS runner missed (6 of 36 frames). - emu_commands_test: kEach is static constexpr, so MSVC does not need it captured (C3493). - tsan.supp: suppress libdbus's own lock-order-inversion between SDL_Init and SDL_Quit on the main thread (system library, not instrumented); it was the only report in the Linux TSan job, on every threaded app run. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): precise emulation-thread waits on MinGW (#967) MinGW's timed condition wait (winpthreads) rounds short timeouts up to the scheduler tick (~10-16 ms) even with the 1 ms timer resolution SDL sets, so threaded fast-forward ran below normal speed on the MinGW Windows runner (67.5 vs 98.2 frames/s; MSVC was fine) -- and the shipped Windows packages are MinGW builds. EmuCommands::waitUntil now waits on the condition only while more than 20 ms remain, then polls in 1 ms sleeps until the deadline or a post()/notify(). Other platforms keep the plain condition wait. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(app): emu_thread_unit reports the host's timer precision for the fast-forward check (#967) The MinGW runner still showed fast-forward at one frame per ~15.6 ms (64 vs 98 frames/s) after the polling wait, which points at the platform's timers rather than EmuThread. The test now measures sleep_for(1 ms) and the steady_clock step and prints them with the rates. The fast-forward ratio is still asserted whenever it holds or the host can pace 2.5 ms frames; it is reported instead only when it misses on a host whose sleeps exceed 6 ms or whose clock steps exceed 1 ms (or that cannot even sustain normal speed). Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
7988dc8b6d |
test(app): pacing_trace replays a measured cost trace through the pacer; VJ_PERF_CSV per-iteration dump
pacing_trace feeds frame_cost_probe's ms column to the real Pacer on a fake clock and reports uneven image gaps, dropped frames and swap-interval toggles (plus a decoupled-emulation model for comparison). --perf-frames gains VJ_PERF_CSV=path: one row per loop iteration (ms, emulated frames, swap interval). Co-Authored-By: Claude Opus 5.5 <[email protected]> |
||
|
|
fdfbef078a |
docs(agent): corpus_ab_sweep AUDIO=1 / ARGS
Co-Authored-By: Claude Opus 5.5 <[email protected]> |
||
|
|
7b5fa04124 |
docs(timing): S2 result -- traffic census, read-derived grant-latency function, NO-GO
Doom's critical (GPU) chain gains 0.31/0.46/0.68 field per flip from the latency function x measured traffic (every bound < 1.0); injected into the emulator it moves the present rate +0.01..+0.05 (stock) / +0.16..+0.46 (all timing options). The function is assembled from JTRM rev 8 and the ARB.NET state machine (not simulated: no Verilator rig or jag_sim checkout on any host) and is written up as gpu-timing-spec.md section 15. Also corrects plan premises H-A (94% of GPU external traffic is spin polls), H-B (68K work is 0.31 field/flip) and H-C (DSP traffic is large but its chain has 1.25 fields of slack), and records the 6% 68K slice-overshoot. Co-Authored-By: Claude Opus 5.5 <[email protected]> |
||
|
|
f2a2cac78d |
docs(timing): document S0 gates; correct plan 1.3/5.3 Checkered Flag budget premise
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> |
||
|
|
4841d260f7 |
perf(netlink): wait for unprompted link bytes, add on-device hop diagnostic (#860)
A Doom link tic is 12 one-way single-byte hops (6 pairs, ping-pong). Two parts of the old behaviour made the tic rate depend on the vsync phase between the two devices, collapsing to 5 tics/s at bad phases even on a fast link: JLinkAwaitReply only engaged after we transmitted, so a side that only ANSWERS spun on ASISTAT with no wait, finished its frame in ~2 ms and sat in the frontend while the partner's byte arrived; every such hop then cost a whole video frame. uart.c now counts consecutive ASISTAT reads with nothing to send or receive and, past 24 in a frame, calls JLinkAwaitIdle(), which blocks like the reply wait (same frame budget and timeout) so the byte is seen the moment it arrives. A silent partner backs off exponentially (1..32 frames without idle waits); any received byte resets it. Real transports on the JagLink device only (not loopback, not the voice modem). Simulator, Doom 2P, tics/s averaged over 4 vsync phases, before -> after (emu 2 ms, 5 us poll_receive): 2 ms one-way 27.5 -> 27.5 2.5 ms 19.8 -> 26.0 3 ms 16.3 -> 20.8 4 ms 7.5 -> 19.2 Wi-Fi A (2 ms+3 jitter+2% spikes) 8.9 -> 16.0 Wi-Fi B (3 ms+8 jitter+3% spikes) 5.4 -> 9.0 3 ms with emu 6 ms 10 -> 15 Also: a once-per-5-s INFO log line while a link carries traffic (hops/s, mean hop time, wait per hop, slipped-hop share, timeouts, budget cuts, emulation cost, idle-wait hits) so the numbers can be read from the RetroArch log on iOS; netlink_sim gains --btrace/--log/--loss-pct/--rto-ms/--udp-rto-ms/ --corrupt and the clock patch gains a byte-event hook; netlink_btrace_report.py breaks hop time down by cause and analyses the Doom tic structure. Co-Authored-By: Claude Opus 5.5 <[email protected]> |
||
|
|
8af2132793 |
fix(crash_detect): test the raw DSP PC for dsp_pc_escape
dsp_pc_valid() canonicalised the PC to 24 bits, by symmetry with the GPU,
where GPUReadWord and the 68K bus really do alias $FD012786 to $012786.
The DSP does not: DSPExec's own PC-escape bail-out tests the UNMASKED
dsp_pc against exactly this predicate (<= $E3FFFF, or DSP local RAM
$F1B000-$F1CFFF) and drains the slice without fetching. A DSP PC that
fires the new check is one the core refuses to execute, so a fire is a
non-executing DSP by construction. The mask hid Music Demo's HLE runaway
to $C1903000 (#853); an interrupt kept vectoring the DSP back into its ISR,
so the opcode counter moved and dsp_wedge never fired either. The log line
now prints the raw PC too.
Evidence (1200 frames):
- True positive:
|
||
|
|
d316292c96 |
test(harness): --input-bitmasks to exercise the RetroArch RETRO_DEVICE_ID_JOYPAD_MASK input path (#737) (#857)
Co-authored-by: Claude Sonnet 5.5 <[email protected]> |
||
|
|
e8be4e6817 |
feat(tools): score BIOS-mode cart boot rows from the boot-ROM handoff, add BIOS_REJECT
The cart boot matrix scored real-BIOS runs from frame 0, so the identical ~490-frame boot logo animation and jingle counted as every title's "video" and "audio", and carts the boot ROM rejects (red "Jaguar" screen) read as "BIOS works". cart_boot_probe now finds the boot-ROM -> cart handoff itself (68K or any of its last 1024 instructions, via pcQueue, in $800000-$DFFFFF) and restarts scored_* counters there; --post-handoff N runs N frames past it (--frames is the cap) and stops early once the boot ROM sits on its BRA.S * halt loop. Headerless RAM-loaded .jag files never run the boot ROM (bios_ran=0): whole run scored, never a reject. The classifier moves to test/tools/cart_classify.sh (so a unit test can feed it synthetic logs) and gains BIOS_REJECT (boot ROM ran, no handoff, parked on BRA.S *) and ? (bios_trap) (cart got control, then PC fell back into the boot ROM: a crash, not a reject). Crash-watchdog signatures stay whole-run: an early escape is a real bug (#853), not excused. matrix_diff.py ranks BIOS_REJECT equal to GAME_CODE: GAME_CODE -> BIOS_REJECT is a lateral scoring correction, and HLE GAME_CODE / BIOS BIOS_REJECT is not an HLE-vs-BIOS asymmetry. BIOS runs get FRAMES+700 frames and a 150s default timeout. docs/cart-boot-matrix.md prose updated; table not regenerated. Closes #852 Co-Authored-By: Claude Opus 5.5 <[email protected]> |
||
|
|
3539fbb65b |
feat(tools): corpus gate -- matrix_diff.py, never-backward cart rule, pre-tag checklist (#749)
test/tools/matrix_diff.py OLD.md NEW.md compares two cart or CD boot matrices per row. Stage rank (low->high): -1 ?(core_error|build_mismatch| no_reg), 0 LOAD_FAIL, 1 HARNESS_HANG/?/?(pc_escape|timeout|BIOS band), 2..6 BIOS_INTRO, BOOT_STUB, GAME_CODE, MENU, IN_GAME. At equal rank a new crash-watchdog signature (from the notes/Watchdog cell) is backward. Prints regressed / still-asymmetric / improved / added / removed; exits 1 on any backward move, 2 on parse error. test/test_matrix_diff.sh (43 checks) runs in make test. The cart matrix header (generator + doc) now carries the never-backward rule; docs/release-process.md gains the pre-tag checklist (regenerate both matrices on the RC on the corpus machine, diff vs the previous tag, a backward row blocks the tag until ticketed and deferred, improved list feeds release notes). Sanity: cart v3.7.0 -> v3.7.1 shows 5 regressed cells (Music Demo BIOS GAME_CODE -> ?(pc_escape); inframe_hang newly visible on 4 homebrew). Closes #749 Co-Authored-By: Claude Opus 5.5 <[email protected]> |
||
|
|
2f17c675b1 |
fix(gpu): NORMI normalises to bit 23, not 22 (#611)
NORMI returns the right shift that normalises an unsigned integer "as an IEEE 32-bit floating point value" (JTRM v8 p.52), so the top set bit lands on bit 23, the hidden-bit position: NORMI 1 = -23, NORMI $800000 = 0, NORMI $80000000 = 8. TOM's ARITH.NET "normalisation integer generator" encodes exactly that, and gives -32 for a zero source. The old code normalised to bit 22 and returned one too many for every input (and 0 for a zero source). Club Drive clips polygons against the screen with a four-entry corner table indexed by (NORMI(outcode) << 3) & $1F, so every inserted screen corner was rotated by one. A polygon crossing two screen edges (common while steering) came out non-monotonic in Y; the edge walker bailed to the command interpreter while a span callback still borrowed its return stack (r10) as scratch, and the interpreter's return popped a garbage address into main RAM. From the user's mid-race savestate, holding accelerate and steering left escaped at frame 82; with the fix all six driving patterns run 2400 frames clean. Tests: - test_gpu_ops: NORMI cases from the netlist decode (the old expectation, 9 for $80000000, encoded the bug). - test/tools/test_club_drive_611: ROM- and savestate-gated; drives from states/Club Drive (1994).state3 and requires the GPU PC to stay in local RAM for 300 frames with the picture moving. Fails on develop at frame 82. - Retire Native Demo (bin) as a test_blitter_hung reproducer: its GPU runs into data at frame 1 on develop, and the corrected NORMI changes that garbage path so it no longer issues the never-ending blit. The non-approx path stays covered by the synthetic test on both engines; Music Demo --bios still covers the approx path. The DSP has the identical NORMI block and off-by-one; that half is fixed separately on fix/dsp-normi-off-by-one. Co-Authored-By: Claude Opus 5.5 <[email protected]> |
||
|
|
ada7055b47 |
fix(file): load $5000-linked raw binaries at $5000 (#818)
The raw-binary loader scored candidate bases only by whether absolute JSR/JMP/LEA/MOVE targets fall inside [base, base+size). $5000 was not a candidate, and a $4000 window also contains almost every target of a $5000-linked image, so such images loaded $1000 low: Chroma-Luma (bin) ran its font data from its first JSR, the 68K hit ILLEGAL and parked at $1000 (#800's guard), and the screen stayed black in HLE and BIOS mode (RAM-loaded images never take the boot-ROM path, so both modes share the failure). Add $5000 and break overlapping-window ties on entry-point consistency: a JSR/JMP abs.L target that is the image start or the word after an RTS/RTE/RTR is a subroutine start under the right base and a coincidence under a wrong one. Among bases that clear the containment bar, the most entry hits wins; with no hits anywhere the containment winner stands, so images without the signal do not move. Across the 155-title cart corpus exactly six load decisions change, all $4000 -> $5000 and all from black/parked to running video: Chroma-Luma (bin), JagMania (Jul 8), JagMarble (2000), FORCE Design Legion Force intro, Joypad-TeamTap Tester [a1], and Music Demo (ScatoLOGIC), whose #794 blitter spray in BIOS mode was this misload. Music Demo is therefore no longer a hung-blitter reproducer; it moves to the new ROM-gated test_raw_binary_boot check next to the two #818 titles. Closes #818 Co-Authored-By: Claude Opus 5.5 <[email protected]> |
||
|
|
d3bc33e001 |
fix(blitter): a blit hardware never finishes hangs the blitter, not the host
Phrase-mode blits below 8bpp never end on hardware. INNER.NET builds the inner-counter decrement (Inc0-Inc3) from the 8/16/32bpp decodes, so below 8bpp it is dstxp[0]. Phrase-mode X add truncates to a phrase boundary (JTRM v8 p.71), so after the first write the counter never moves and inner0 is never reached. The synchronous accurate engine reproduced that faithfully and never returned from retro_run (Music Demo --bios, #794; Native Demo (bin) HLE). Policy: hang the emulated blitter, never the host. - BlitterNeverEnds() (blitter_mmio.c) is the single exact predicate. The accurate engine, the dispatch path and the crash watchdog (CrashDetectBlitNeverEnds now delegates to it) all use it. - The accurate engine runs the stuck step for exactly one destination wrap period, then returns. That is 65536/ppp phrase writes (1024, 2048 or 4096 at 1/2/4bpp), 65536 with the Y-add bit, +1 when the start X is not phrase-aligned. Bounded at 65,536 writes. It sets a stop flag only after the inner0/mask logic, so no write mask changes. The collapsed pattern-fill and copy paths are bypassed for these blits. - The result is EXACT when repeating the step is idempotent on memory. Every observed homebrew hang has that shape: DEMO1B $2208, Chroma $2704, Native Demo $2718 (LFU clear, no accumulators). - Otherwise it is a defined one-pass approximation: BlitterNeverEndsApprox() flags Gouraud/Z/shade accumulators, comparator masks, ADDDSEL, LFU feeding back D, or LFU fed by a separately stepping source (Music Demo, JagMania). The watchdog line now says never_ends=1 approx=1 for those. - Dispatch then sets a sticky "blitter hung" flag, under both engines. - B_CMD status reads busy (JTRM v8 p.75): IDLE clear, outer-INNER and inner-DWRITE set, and the stuck inner count in bits 16-31. - Further B_CMD starts are ignored, because OUTER.NET leaves idle only on go (innert[0] = idle AND go). - The flag clears only in BlitterReset (reset / power-on). - Known limit: hardware still latches B_COUNT/B_CMD/pointer writes mid-blit (FDSYNC on countld/cmdld), so a B_COUNT write with inner 0 would un-stick it. JTRM p.70 calls that undefined; this model does not resume a hung blit. Savestate: v16 extended in place, no bump. A "BLH1" chunk (hung flag and stuck count, 7 bytes) is appended strictly after #814's CDX1, with a room check before writing. It is read only for version >= 16. A missing or wrong magic, or an older state, loads as not hung. Tests: - test/tools/test_blitter_hung, ROM-free, accurate and fast engines. - The synthetic never-ending blit returns. - Exactly the 8 KB wrap period is written and the next phrase is untouched. - Status reads busy with count $DC, and a second start writes nothing. - serialize -> retro_reset clears the flag -> unserialize restores it. - ROM-gated runs of Music Demo --bios and Native Demo (bin): 600 frames each, and the blitter ends hung. - test_crash_detect_inframe gains 10 approx rows. Closes #794 Refs #800 Co-Authored-By: Claude Opus 5.5 <[email protected]> |
||
|
|
c1eb2b10a0 |
fix(hle): park ILLEGAL for uploaded executables so homebrew stops hanging the accurate blitter
Seven homebrew carts froze retro_run under the default (Accurate) blitter. The blitter was the victim, not the cause: - Raw BJL binaries and .abs images carry no vector table, and many end their 68K main with ILLEGAL (the Alpine/BJL "drop into the debugger" idiom; the GPU/OP keep running the demo). HLE installed RTE stubs only for cartridges, and file.c guarded only Alpine/JagServer images, so the trap went through a zero vector to $0, ran the vector table as code and walked the stack down through $0, $FFxxxx and into TOM. - There the 68K's own exception frames land on B_COUNT/B_CMD ($F0A238 reaches $F02238 through TOMWriteWord's $FF3FFF mask): SR becomes B_CMD's low word ($2208 = S|IPL2|N for DEMO1B), the pushed PC becomes B_COUNT, and the write starts a blit with the zeroed A1 registers -> phrase mode, 1bpp, X=0. - That blit never ends on real hardware either. INNER.NET's inner-counter decrement (Inc0-Inc3) only uses the 8/16/32 bpp decodes beyond bit 0, so below 8bpp it is dstxp[0]; phrase-mode X add truncates to a phrase boundary (JTRM v8 p.71), so after the first write the counter never moves. BlitterMidsummer2 matches the netlist term for term, which is why it spun forever; the Fast engine just shrugs the garbage off. Fix: JaguarReset parks vector 4 at a BRA.S * loop at $1000 for every non-cartridge, non-CD image -- the guard file.c already used for Alpine and JagServer, now re-armed on every reset so retro_reset()'s RAM clear cannot wipe it. Only vector 4 is parked; skipped if the image covers the vector or the loop. No blitter semantics change, no savestate change. Also: - crash_detect: inframe_hang now also fires (never_ends=1) for a blit whose inner counter cannot decrement (phrase mode below 8bpp). DEMO1B's blit (14.4M pixels) was under the 2^24 pixel rule and logged nothing. The "inner 0 = one step" assumption now cites INNER.NET's zero/underflow detect. CrashDetectNoteBlit takes the destination flags and X. - cart_boot_matrix.sh: CART_MATRIX_PROBE_ARGS knob so the matrix can sweep the Accurate blitter. Folded into the row-cache id and default log dir, and refused without an explicit CART_MATRIX_OUT, so it can never reuse or publish over the Fast rows. - test/tools/test_upload_illegal_park: ROM-free synthetic raw binary that ends in ILLEGAL must park at $1000, also after retro_reset (the old core hangs on it); plus a ROM-gated DEMO1B run under the Accurate blitter. test_crash_detect_inframe gains 11 never_ends rows. Before/after, Accurate blitter, 600 frames: before, all 7 carts timed out in HLE (120 s cap) and 6 of 7 in real-BIOS mode (90 s cap; JagMania (Jul 8) completed); after, all 7 complete in both boot modes. Chroma-Luma (bin) and JagMania (Jul 8) no longer hang but stay broken: their raw-binary load address is inferred wrong (a JSR lands in font data / mid-instruction), a separate loader bug. Native Demo (bin) HLE, already broken (68K wild by frame 2, GPU runaway at frame 1), now takes a different chaotic path that reaches the same never-ending blit at frame 170, so it hangs under Accurate where it used to end on a black screen. That is the #794 class below. Music Demo (ScatoLOGIC) --bios (#794) is a cartridge in real-BIOS mode, so this guard does not apply; its hanging blit is the same never-ending 1bpp phrase-mode shape and is now named by never_ends=1. Closes #800 Refs #794 Co-Authored-By: Claude Opus 5.5 <[email protected]> |
||
|
|
0983d22c2d |
test(cd): fail test_cd_bios_boot when the CD transfer wedged (#799)
test_cd_bios_boot printed [PASS] on a run where cd_seek_wedge fired: its criteria (pc_in_ram, not_loop, not_thrash, ram_payload) are all satisfied by a 68K that keeps running its CD service loop while the transfer is dead. Ask the watchdog instead of grepping its log: crash_detect.c now keeps a sticky count of cd_seek_wedge threshold crossings (counted before the log throttle, reset with CrashDetectReset) behind CrashDetectCDSeekWedgeFires() and CrashDetectCDSeekWedgeStreak(), exported via the existing CrashDetect* test ABI. The harness reads them per disc, forces virtualjaguar_crash_detect on, prints a [CD-WEDGE] reason, and FAILs the disc. A core without the symbols FAILs rather than passing blind. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> |
||
|
|
9bcaebd4c2 |
feat(crash_detect): inframe_hang signature for absurd blits (#740)
The blitter runs synchronously inside one register write, so a garbage B_COUNT can keep retro_run from returning for hours -- the per-frame watchdog never gets a turn and the frontend sees a hung core with no log line. Check at blit dispatch (both engines, memo and compare paths): when B_COUNT asks for more than 2^24 pixels, log [CRASH-DETECT] inframe_hang with the counts, the command, A1 base and all three processor PCs, then run the blit as before. Log-only, latched like the other signatures, one multiply per blit. Pixels are counted the way the accurate (default) engine runs them: outer 0 = 65536 lines (JTRM BLIT_COUNT), inner 0 = one step (<= 64 px; BlitterMidsummer2 stops on the bit-15 crossing). Counting inner 0 as 65536 per the JTRM flagged Troy Aikman, Double Dragon V and Brutal Sports Football, which write B_COUNT=0 at boot and run fine. Reproducers: Music Demo (ScatoLOGIC) --bios writes B_COUNT=$2710826D (334M pixels) at frame 158; DEMO1 (bin) in HLE freezes at frame 400 on the accurate engine. The issue proposed sampling PCs in the JaguarExecuteNew slice loop. That loop advances emulated time on every iteration, so the frame always ends unless a single call inside it never returns -- and a sampler there never runs during such a call. Every in-frame hang sampled (Music Demo, DEMO1, DEMO1B, Painter) was inside BlitterMidsummer2. cart_boot_matrix.sh now lists inframe_hang among a timed-out row's signatures (cd_boot_matrix.sh already reports the first CRASH-DETECT line). Co-Authored-By: Claude Opus 5.5 <[email protected]> |
||
|
|
b28b89bfdd |
fix(crash_detect): cd_seek_wedge only while the drive still owes data (#741)
cd_seek_wedge counted every frame with frozen FIFO drains. Under
cd_boot_mode=bios it fired 15 times across Baldies, Battle Morph,
BrainDead 13, Highlander, Primal Rage and Myst (+ both Myst demos), all of
which go on to run: the transfer had simply finished.
Measured at each fire (temporary dump, not committed): BUTCH low byte $02
every time -- the game's CD ISR clears the master interrupt enable to
signal completion. Philia (bios) ends its transfer the other way, leaving
BUTCH $03 but clearing I2CNTRL bit 2 (FIFO data enable) while it waits for
a button (it responds to B). The real IMASK-stuck transfer wedge
(
|
||
|
|
5bf1a9741f |
test(tools): corpus_ab_sweep.sh -- bounded A/B of two cores over a ROM corpus
The tooling gap that let #641's regression through. That change clamped the object-processor window and was merged on the strength of five ROMs; run across 160 it turned out to blank the bottom three scanlines on about a third of them. A corpus A/B existed and was running the whole time -- it just never reported, because it had no per-run timeout and wedged on Chroma-Luma Color Pick (#659), which makes retro_run never return. It sat there 2h23m emitting nothing, and "still running" got read as progress. So the two things this encodes are the two mistakes, not the happy path: * Every run is bounded by timeout(1). A hang is one visible SKIP line, and a SKIP makes the script exit 1 -- an unchecked ROM must not be able to look like a clean sweep. * It compares the full hash stream, not the transition count. Super Burnout reads 143 transitions on both sides of the #632 fix while rendering different pixels; a count comparison would have called that "no change". Reported as "transitions A->B, N frames differ" so the shape of a divergence is visible without opening the CSVs. Progress prints every 25 ROMs, so an empty tail is distinguishable from a stall -- the failure mode above, made visible. Verified against the #632 work: 158 identical, 1 differing (Super Burnout, the intended change), 1 skipped (the ROM from #659). Refs #659 <!-- link-issue: #659 --> |
||
|
|
1ced42b14d |
docs: split CLAUDE.md into on-demand agent reference docs
CLAUDE.md loaded ~10K tokens on every message and into every subagent at
startup. Slim it to load-bearing always-on rules (~2K) plus an index, and
move the catalogs and how-to detail into docs/agent/*.md that load only when
read:
- docs/agent/build.md — build cmds, C89 exempt list, TEST_EXPORTS relink,
build-identity guard, stale-.o hazard
- docs/agent/hardware.md — processor model, memory map, clocks, source
layout, distilled JTRM index
- docs/agent/testing.md — harness catalog, vjtrace, audio-test requirements,
crash-watchdog signatures, acid gating, profiling
- docs/agent/github.md — PR-issue linking, Copilot/Kimi, release process,
site publishing
Safety rules (C89, branch-off-develop, DEVELOPER_DIR, audio-test pair,
PR-issue link, private-ROM / git clean danger, shell-alias trap) stay inline
so no agent can miss them without opening a doc. Removed the duplicated
build-identity-guard block; compressed language to shorthand. No content
dropped — verified the tripwire facts (MMULT fix, PR #170, closingIssues,
ROM-symlink rules) survived the move.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AiMAfypdq7ASvQWK6vQ5hW
|