mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-10 03:21:27 -10:00
* 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>
346 lines
15 KiB
C++
346 lines
15 KiB
C++
// The debug views with the REAL core on the emulation thread (#967 phase 3a): what App does for the Tools windows,
|
|
// minus the pixels. usage: vj_debug_views_threaded <rom-a.j64> <rom-b.j64>
|
|
//
|
|
// The rig is wired the way App wires it (wireHost + startEmuThread + emuRunOneFrame's tail): the core hands its
|
|
// interface over on the emulation thread (onCoreIface), the session reaches the core through the debug lane
|
|
// (coreCall / corePost), and every frame ends with fillViews(). The thread stays paused, so frames run only through
|
|
// advance(), and every read goes through waitRead(): the UI's per-frame loop (wakeViews() until the buffer arrives).
|
|
// A frame run after a fill would make the buffer and a later peek differ (yarc rewrites DRAM every frame), so no case
|
|
// runs a frame between a fill and its comparison.
|
|
#include "core_host.h"
|
|
#include "debug/debug_session.h"
|
|
#include "emu_thread.h"
|
|
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <ctime>
|
|
#include <filesystem>
|
|
#include <functional>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
// The core's own entry (src/debug/gdbstub.h): what a breakpoint set over the GDB socket arms. The debug interface's
|
|
// halt_next is a no-op while no host is attached, which is exactly the state this test must reach.
|
|
extern "C" void GDBTargetHaltNext(int target);
|
|
|
|
using namespace vj;
|
|
using Id = DebugViews::Id;
|
|
|
|
static int g_fail = 0, g_checks = 0;
|
|
#define CHECK(c, ...) \
|
|
do { \
|
|
g_checks++; \
|
|
if (!(c)) { \
|
|
std::printf("FAIL %s:%d: ", __FILE__, __LINE__); \
|
|
std::printf(__VA_ARGS__); \
|
|
std::printf("\n"); \
|
|
g_fail++; \
|
|
} \
|
|
} while (0)
|
|
|
|
namespace {
|
|
|
|
uint32_t fnv(const std::vector<uint8_t>& v) {
|
|
uint32_t h = 2166136261u;
|
|
for (uint8_t b : v) {
|
|
h ^= b;
|
|
h *= 16777619u;
|
|
}
|
|
return h;
|
|
}
|
|
|
|
ViewRequest memReq(unsigned addr, unsigned len) {
|
|
ViewRequest r;
|
|
r.kind = ViewRequest::Kind::Memory;
|
|
r.addr = addr;
|
|
r.len = len;
|
|
return r;
|
|
}
|
|
|
|
ViewRequest dramReq() {
|
|
ViewRequest r;
|
|
r.kind = ViewRequest::Kind::Dram;
|
|
return r;
|
|
}
|
|
|
|
ViewRequest regsReq(int target) {
|
|
ViewRequest r;
|
|
r.kind = ViewRequest::Kind::Regs;
|
|
r.target = target;
|
|
return r;
|
|
}
|
|
|
|
struct Rig : EmuThread::Client {
|
|
CoreHost& host = CoreHost::instance();
|
|
DebugSession dbg;
|
|
EmuThread emu;
|
|
bool threaded;
|
|
bool gdbStubOn = false; // = App::gdbStubOn_: the silent debugger host is wanted
|
|
|
|
explicit Rig(bool thr) : threaded(thr) {}
|
|
|
|
// = App::attachGdbHostOnCore (emulation thread).
|
|
void attachSilentHost() {
|
|
if (gdbStubOn && host.isLoaded() && dbg.core().loaded() && !dbg.core().attached()) dbg.core().attach();
|
|
}
|
|
|
|
void threadStarted() override { host.setOwner(std::this_thread::get_id()); }
|
|
void threadStopping() override { host.setOwner(std::thread::id()); }
|
|
bool canRun() override { return host.isLoaded(); }
|
|
double framePeriod() override { return 1.0 / 600.0; }
|
|
double audioQueuedMs() override { return -1.0; }
|
|
void runFrame() override { // App::emuRunOneFrame
|
|
attachSilentHost(); // a Detach posted before this frame ran at the lane drain: the host is back before the frame
|
|
host.runFrame();
|
|
dbg.core().frameEnded();
|
|
attachSilentHost();
|
|
host.publishSnapshot();
|
|
dbg.fillViews();
|
|
}
|
|
void afterCommands() override { host.publishSnapshot(); }
|
|
|
|
void wire() { // = App::wireHost + startEmuThread
|
|
host.onDebugIface = [this](const vj_debug_iface* i) {
|
|
dbg.onCoreIface(i);
|
|
if (!host.threaded()) dbg.syncCore();
|
|
};
|
|
dbg.coreCall = [this](std::function<void()> f) { emu.debugCall(std::move(f)); };
|
|
if (threaded) dbg.corePost = [this](std::function<void()> f) { emu.debugPost(std::move(f)); };
|
|
host.post = [this](std::function<void()> f) { emu.post(std::move(f)); };
|
|
if (threaded) { // the halt pump's wait, set before the thread exists (as App::startEmuThread does)
|
|
dbg.core().waitVerdict = [this] { emu.serveDebugLane(DebugCore::kPumpWait); };
|
|
dbg.core().notifyVerdict = [this] { emu.debugPost([] {}); };
|
|
}
|
|
emu.setPaused(true); // frames only through advance()
|
|
}
|
|
void unwire() {
|
|
host.onDebugIface = nullptr;
|
|
host.post = nullptr;
|
|
}
|
|
|
|
bool start() { return emu.start(threaded ? EmuThread::Mode::Threaded : EmuThread::Mode::Inline, *this); }
|
|
bool load(const std::string& rom) {
|
|
std::string err;
|
|
const bool ok = emu.call([&] { return host.loadContent(rom, err); });
|
|
if (!ok) std::printf("load %s: %s\n", rom.c_str(), err.c_str());
|
|
return ok;
|
|
}
|
|
|
|
// The UI's per-frame loop: wakeViews() every 2 ms until the id's buffer is readable at the current refresh serial,
|
|
// up to 500 ms. (A buffer from an older serial is the previous fill, not the one asked for.)
|
|
bool waitRead(Id id, ViewBuffer& b) {
|
|
const auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
|
|
for (;;) {
|
|
dbg.wakeViews();
|
|
if (dbg.views().read(id, b) && b.serial == dbg.refreshSerial()) return true;
|
|
if (std::chrono::steady_clock::now() >= end) return false;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
}
|
|
}
|
|
|
|
// A peek of the live core on the emulation thread (the debug lane).
|
|
std::vector<uint8_t> peek(unsigned addr, unsigned n) {
|
|
std::vector<uint8_t> v(n);
|
|
const int rc = emu.debugCall([&] { return dbg.core().peek(addr, v.data(), n); });
|
|
if (rc != (int)n) v.clear();
|
|
return v;
|
|
}
|
|
};
|
|
|
|
void cases(Rig& r, const std::string& romA, const std::string& romB) {
|
|
DebugSession& dbg = r.dbg;
|
|
EmuThread& emu = r.emu;
|
|
ViewBuffer b;
|
|
|
|
CHECK(r.load(romA), "rom A loads");
|
|
dbg.setContentLoaded(true);
|
|
dbg.syncCore();
|
|
CHECK(dbg.enabled(), "the tools are enabled with the interface handed over");
|
|
|
|
// 1. The three request kinds answer what a debugCall peek says, at the same frame.
|
|
{
|
|
emu.advance(3);
|
|
const Id mid = dbg.views().set(DebugViews::kNoId, memReq(0x1000, 256));
|
|
const Id did = dbg.views().set(DebugViews::kNoId, dramReq());
|
|
const Id rid = dbg.views().set(DebugViews::kNoId, regsReq(VJ_DBG_TARGET_M68K));
|
|
dbg.requestRefresh();
|
|
const unsigned frame = emu.debugCall([&] { return dbg.core().frameNumber(); });
|
|
ViewBuffer m, d, g;
|
|
const bool okM = r.waitRead(mid, m), okD = r.waitRead(did, d), okG = r.waitRead(rid, g);
|
|
CHECK(okM && okD && okG && m.ok && d.ok && g.ok, "memory, dram and regs requests equal a debugCall peek at the same frame");
|
|
CHECK(m.frame == frame && d.frame == frame && g.frame == frame, "all three buffers carry the frame %u", frame);
|
|
CHECK(m.bytes == r.peek(0x1000, 256), "the Memory buffer equals the peek");
|
|
const std::vector<uint8_t> dram = r.peek(0, DebugSession::kDramSize);
|
|
CHECK(d.bytes.size() == DebugSession::kDramSize && fnv(d.bytes) == fnv(dram) && d.bytes == dram,
|
|
"the Dram buffer equals a peek of 2 MiB");
|
|
vj_debug_regs want{};
|
|
const bool gotRegs = emu.debugCall([&] { return dbg.core().getRegs(VJ_DBG_TARGET_M68K, want); });
|
|
CHECK(gotRegs && g.regs.pc == want.pc, "regs.pc equals getRegs(M68K).pc (%06X vs %06X)", g.regs.pc, want.pc);
|
|
dbg.views().clear(mid);
|
|
dbg.views().clear(did);
|
|
dbg.views().clear(rid);
|
|
}
|
|
|
|
// 2. A poke through the session (marshalled through debugCall) shows in the next fill.
|
|
{
|
|
CHECK(dbg.pokeU(0x1F0000, 4, 0xCAFEF00D) == 4, "the poke is accepted");
|
|
const Id id = dbg.views().set(DebugViews::kNoId, memReq(0x1F0000, 4));
|
|
const bool ok = r.waitRead(id, b);
|
|
CHECK(ok && b.bytes.size() == 4 && b.bytes[0] == 0xCA && b.bytes[1] == 0xFE && b.bytes[2] == 0xF0 && b.bytes[3] == 0x0D,
|
|
"a poke through the session shows in the next fill");
|
|
dbg.views().clear(id);
|
|
}
|
|
|
|
// 3. A new request is readable while paused, with no frame run.
|
|
{
|
|
const uint64_t frames = emu.framesRun();
|
|
const Id id = dbg.views().set(DebugViews::kNoId, memReq(0x2000, 64));
|
|
bool ok = r.waitRead(id, b);
|
|
CHECK(ok, "readable within 500 ms with no frame run");
|
|
CHECK(emu.framesRun() == frames, "a new request is readable while paused, with no advance()");
|
|
CHECK(ok && b.req.addr == 0x2000 && b.bytes == r.peek(0x2000, 64), "the first range is the peek");
|
|
const uint64_t serial = b.serial;
|
|
dbg.views().set(id, memReq(0x3000, 64)); // a scroll while paused
|
|
ok = r.waitRead(id, b);
|
|
CHECK(ok && b.req.addr == 0x3000 && b.serial == serial && b.bytes == r.peek(0x3000, 64),
|
|
"the scrolled range is the new one, at the same serial");
|
|
CHECK(emu.framesRun() == frames, "the scroll ran no frame either");
|
|
dbg.views().clear(id);
|
|
}
|
|
|
|
// 4. After a reload the Memory window's first read is the new ROM's bytes.
|
|
{
|
|
const Id id = dbg.views().set(DebugViews::kNoId, memReq(0x800000, 65536));
|
|
emu.advance(2);
|
|
CHECK(r.waitRead(id, b) && b.bytes.size() == 65536, "the cartridge window of ROM A is read");
|
|
const std::vector<uint8_t> romAbytes = b.bytes;
|
|
CHECK(r.load(romB), "rom B loads"); // onCoreIface fires on the emulation thread twice: null, then the new interface
|
|
dbg.syncCore();
|
|
ViewBuffer now;
|
|
CHECK(!dbg.views().read(id, now) || now.bytes != romAbytes, "never the previous game's bytes");
|
|
const bool ok = r.waitRead(id, b); // the epoch bump made the old buffer pending at the same serial
|
|
CHECK(ok && b.bytes == r.peek(0x800000, 65536), "after a reload the Memory window's first read is the new ROM's bytes");
|
|
CHECK(b.bytes != romAbytes, "the two ROMs differ in their first 64 KiB (pick different ROMs if this fails)");
|
|
dbg.views().clear(id);
|
|
}
|
|
|
|
// 7. Hardcore closes the tools and detaches, threaded.
|
|
{
|
|
CHECK(dbg.attach() && dbg.attached(), "the core is attached (so the hardcore detach has something to do)");
|
|
dbg.setHardcore(true);
|
|
CHECK(!dbg.enabled(), "hardcore closes the tools");
|
|
emu.debugCall([] {}); // the detach was posted to the debug lane: this runs behind it
|
|
CHECK(!dbg.attached(), "hardcore detaches the core on the emulation thread");
|
|
dbg.setHardcore(false);
|
|
CHECK(dbg.enabled(), "the tools come back with hardcore off");
|
|
}
|
|
|
|
// 7b. A user Detach is a lane command: it runs at the drain BEFORE the next frame, so the silent gdb host has to be
|
|
// put back before that frame, not after it. A halt gdb causes inside that frame (armed as the GDB socket arms
|
|
// it, not through the session) must reach the pump: the mirror sees it, and Continue ends it.
|
|
{
|
|
r.gdbStubOn = true;
|
|
CHECK(dbg.attach() && dbg.attached(), "the silent host is attached");
|
|
dbg.detach(); // running: posted to the debug lane
|
|
emu.debugPost([] { GDBTargetHaltNext(VJ_DBG_TARGET_M68K); }); // after the detach, as the lane is FIFO
|
|
std::atomic<bool> frameDone{false};
|
|
std::thread runner([&] {
|
|
emu.advance(1);
|
|
frameDone = true;
|
|
});
|
|
bool seen = false;
|
|
for (int i = 0; i < 1500 && !frameDone; i++) { // up to ~3 s
|
|
dbg.syncHalt();
|
|
if (dbg.halted()) {
|
|
seen = true;
|
|
break;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
}
|
|
CHECK(seen, "a halt in the frame right after a Detach is hosted: the mirror sees it");
|
|
CHECK(!seen || (dbg.haltByGdb() && !frameDone), "it is gdb's halt, and the frame is parked in the pump");
|
|
if (seen) dbg.resume();
|
|
runner.join();
|
|
CHECK(frameDone, "Continue ends the halt and the frame completes");
|
|
r.gdbStubOn = false;
|
|
dbg.detach();
|
|
emu.debugCall([] {});
|
|
dbg.syncHalt();
|
|
CHECK(!dbg.attached() && !dbg.halted(), "left detached and running");
|
|
}
|
|
|
|
// 8. A fill posted just before the thread stops is not lost: the stop drains the debug lane, and the next
|
|
// wakeViews() posts again (the thread is gone, so it runs where it is called).
|
|
{
|
|
const Id id = dbg.views().set(DebugViews::kNoId, memReq(0x4000, 16));
|
|
dbg.wakeViews();
|
|
CHECK(r.emu.stop(), "the thread stops with a wake post queued");
|
|
CHECK(r.waitRead(id, b) && b.bytes.size() == 16, "a wake post queued at stop does not wedge the next one");
|
|
}
|
|
}
|
|
|
|
// Inline and threaded give the same Dram view after the same 60 frames. A stopped EmuThread is not restartable: each
|
|
// call builds a fresh rig.
|
|
uint32_t dramHash(bool threaded, const std::string& rom) {
|
|
Rig r(threaded);
|
|
r.wire();
|
|
CHECK(r.start(), "the %s rig starts", threaded ? "threaded" : "inline");
|
|
CHECK(r.load(rom), "the rom loads");
|
|
r.dbg.setContentLoaded(true);
|
|
r.dbg.syncCore();
|
|
const Id id = r.dbg.views().set(DebugViews::kNoId, dramReq());
|
|
r.emu.advance(60);
|
|
r.dbg.requestRefresh();
|
|
ViewBuffer b;
|
|
const bool ok = r.waitRead(id, b);
|
|
CHECK(ok && b.ok && b.bytes.size() == DebugSession::kDramSize, "the %s Dram view arrives", threaded ? "threaded" : "inline");
|
|
const uint32_t h = ok ? fnv(b.bytes) : 0;
|
|
r.emu.call([&] { r.host.unloadContent(); });
|
|
CHECK(r.emu.stop(), "the %s rig stops", threaded ? "threaded" : "inline");
|
|
CHECK(r.host.ownerViolations() == 0, "no owner violation in the %s rig", threaded ? "threaded" : "inline");
|
|
r.unwire();
|
|
return h;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv) {
|
|
if (argc < 3) {
|
|
std::fprintf(stderr, "usage: vj_debug_views_threaded <rom-a.j64> <rom-b.j64>\n");
|
|
return 2;
|
|
}
|
|
namespace fs = std::filesystem;
|
|
std::error_code ec;
|
|
fs::path tmp = fs::temp_directory_path(ec);
|
|
if (ec) tmp = ".";
|
|
tmp /= "vj-debug-views-threaded-" + std::to_string((long long)std::time(nullptr)) + "-" + std::to_string((long long)clock());
|
|
fs::create_directories(tmp / "system", ec);
|
|
fs::create_directories(tmp / "saves", ec);
|
|
const std::string t = tmp.string() + "/";
|
|
CoreHost& host = CoreHost::instance();
|
|
host.setDirectories(t + "system/", t + "saves/");
|
|
const std::string romA = argv[1], romB = argv[2];
|
|
|
|
{
|
|
Rig r(true);
|
|
r.wire();
|
|
CHECK(r.start(), "the emulation thread starts");
|
|
cases(r, romA, romB);
|
|
// The thread has stopped (case 8): the core belongs to this thread again, as in App's shutdown.
|
|
r.host.unloadContent();
|
|
CHECK(r.host.ownerViolations() == 0, "no owner violation in the shared rig");
|
|
r.unwire();
|
|
}
|
|
|
|
const uint32_t inl = dramHash(false, romA);
|
|
const uint32_t thr = dramHash(true, romA);
|
|
CHECK(inl != 0 && inl == thr, "inline and threaded give the same Dram view (%08x vs %08x)", inl, thr);
|
|
|
|
std::error_code ec2;
|
|
fs::remove_all(tmp, ec2);
|
|
std::printf("debug_views_threaded: %d checks, %d failed\n", g_checks, g_fail);
|
|
return g_fail ? 1 : 0;
|
|
}
|