diff --git a/CHANGES b/CHANGES index 41e46b939..11f97dc2b 100644 --- a/CHANGES +++ b/CHANGES @@ -2,6 +2,7 @@ Features: - Scripting: New `input` API for getting raw keyboard/mouse/controller state - Scripting: New `storage` API for saving data for a script, e.g. settings + - Scripting: Debugger integration to allow for breakpoints and watchpoints - New unlicensed GB mappers: NT (older types 1 and 2), Li Cheng, GGB-81 - Debugger: Add range watchpoints Emulation fixes: diff --git a/src/core/scripting.c b/src/core/scripting.c index ba0549dae..a49ef4fe0 100644 --- a/src/core/scripting.c +++ b/src/core/scripting.c @@ -156,10 +156,39 @@ struct mScriptMemoryDomain { struct mCoreMemoryBlock block; }; +#ifdef USE_DEBUGGERS +struct mScriptBreakpointName { + uint32_t address; + uint32_t maxAddress; + int segment : 9; + int type : 1; + int subtype : 3; +}; + +struct mScriptBreakpoint { + ssize_t id; + struct mScriptBreakpointName name; + struct Table callbacks; +}; + +struct mScriptCoreAdapter; +struct mScriptDebugger { + struct mDebuggerModule d; + struct mScriptCoreAdapter* p; + struct Table breakpoints; + struct Table cbidMap; + struct Table bpidMap; + int64_t nextBreakpoint; +}; +#endif + struct mScriptCoreAdapter { struct mCore* core; struct mScriptContext* context; struct mScriptValue memory; +#ifdef USE_DEBUGGERS + struct mScriptDebugger debugger; +#endif }; struct mScriptConsole { @@ -659,9 +688,239 @@ static void _rebuildMemoryMap(struct mScriptContext* context, struct mScriptCore } } +#ifdef USE_DEBUGGERS +static void _freeBreakpoint(void* bp) { + struct mScriptBreakpoint* point = bp; + HashTableDeinit(&point->callbacks); + free(bp); +} + +static struct mScriptBreakpoint* _ensureBreakpoint(struct mScriptDebugger* debugger, struct mBreakpoint* breakpoint) { + struct mDebuggerModule* module = &debugger->d; + struct mScriptBreakpointName name = { + .address = breakpoint->address, + .maxAddress = 0, + .segment = breakpoint->segment, + .type = 0, + .subtype = breakpoint->type + }; + struct mScriptBreakpoint* point = HashTableLookupBinary(&debugger->breakpoints, &name, sizeof(name)); + if (point) { + return point; + } + point = calloc(1, sizeof(*point)); + point->id = module->p->platform->setBreakpoint(module->p->platform, module, breakpoint); + point->name = name; + HashTableInit(&point->callbacks, 0, (void (*)(void*)) mScriptValueDeref); + HashTableInsertBinary(&debugger->bpidMap, &point->id, sizeof(point->id), point); + HashTableInsertBinary(&debugger->breakpoints, &name, sizeof(name), point); + return point; +} + +static struct mScriptBreakpoint* _ensureWatchpoint(struct mScriptDebugger* debugger, struct mWatchpoint* watchpoint) { + struct mDebuggerModule* module = &debugger->d; + struct mScriptBreakpointName name = { + .address = watchpoint->minAddress, + .maxAddress = watchpoint->maxAddress, + .segment = watchpoint->segment, + .type = 1, + .subtype = watchpoint->type + }; + struct mScriptBreakpoint* point = HashTableLookupBinary(&debugger->breakpoints, &name, sizeof(name)); + if (point) { + return point; + } + point = calloc(1, sizeof(*point)); + point->id = module->p->platform->setWatchpoint(module->p->platform, module, watchpoint); + point->name = name; + HashTableInit(&point->callbacks, 0, (void (*)(void*)) mScriptValueDeref); + HashTableInsertBinary(&debugger->bpidMap, &point->id, sizeof(point->id), point); + HashTableInsertBinary(&debugger->breakpoints, &name, sizeof(name), point); + return point; +} + +static int64_t _addCallbackToBreakpoint(struct mScriptDebugger* debugger, struct mScriptBreakpoint* point, struct mScriptValue* callback) { + int64_t cbid = debugger->nextBreakpoint; + ++debugger->nextBreakpoint; + HashTableInsertBinary(&debugger->cbidMap, &cbid, sizeof(cbid), point); + mScriptValueRef(callback); + HashTableInsertBinary(&point->callbacks, &cbid, sizeof(cbid), callback); + return cbid; +} + +static void _runCallbacks(struct mScriptBreakpoint* point) { + struct TableIterator iter; + if (!HashTableIteratorStart(&point->callbacks, &iter)) { + return; + } + do { + struct mScriptValue* fn = HashTableIteratorGetValue(&point->callbacks, &iter); + struct mScriptFrame frame; + mScriptFrameInit(&frame); + mScriptInvoke(fn, &frame); + mScriptFrameDeinit(&frame); + } while (HashTableIteratorNext(&point->callbacks, &iter)); +} + +static void _scriptDebuggerInit(struct mDebuggerModule* debugger) { + struct mScriptDebugger* scriptDebugger = (struct mScriptDebugger*) debugger; + debugger->isPaused = false; + debugger->needsCallback = false; + + HashTableInit(&scriptDebugger->breakpoints, 0, _freeBreakpoint); + HashTableInit(&scriptDebugger->cbidMap, 0, NULL); + HashTableInit(&scriptDebugger->bpidMap, 0, NULL); +} + +static void _scriptDebuggerDeinit(struct mDebuggerModule* debugger) { + struct mScriptDebugger* scriptDebugger = (struct mScriptDebugger*) debugger; + HashTableDeinit(&scriptDebugger->cbidMap); + HashTableDeinit(&scriptDebugger->bpidMap); + HashTableDeinit(&scriptDebugger->breakpoints); +} + +static void _scriptDebuggerPaused(struct mDebuggerModule* debugger, int32_t timeoutMs) { + UNUSED(debugger); + UNUSED(timeoutMs); +} + +static void _scriptDebuggerUpdate(struct mDebuggerModule* debugger) { + UNUSED(debugger); +} + +static void _scriptDebuggerEntered(struct mDebuggerModule* debugger, enum mDebuggerEntryReason reason, struct mDebuggerEntryInfo* info) { + struct mScriptDebugger* scriptDebugger = (struct mScriptDebugger*) debugger; + struct mScriptBreakpoint* point; + switch (reason) { + case DEBUGGER_ENTER_BREAKPOINT: + case DEBUGGER_ENTER_WATCHPOINT: + point = HashTableLookupBinary(&scriptDebugger->bpidMap, &info->pointId, sizeof(info->pointId)); + break; + default: + return; + } + _runCallbacks(point); + debugger->isPaused = false; +} + +static void _scriptDebuggerCustom(struct mDebuggerModule* debugger) { + UNUSED(debugger); +} + +static void _scriptDebuggerInterrupt(struct mDebuggerModule* debugger) { + UNUSED(debugger); +} + +static bool _setupDebugger(struct mScriptCoreAdapter* adapter) { + if (!adapter->core->debugger) { + return false; + } + + if (adapter->debugger.d.p) { + return true; + } + adapter->debugger.p = adapter; + adapter->debugger.d.type = DEBUGGER_CUSTOM; + adapter->debugger.d.init = _scriptDebuggerInit; + adapter->debugger.d.deinit = _scriptDebuggerDeinit; + adapter->debugger.d.paused = _scriptDebuggerPaused; + adapter->debugger.d.update = _scriptDebuggerUpdate; + adapter->debugger.d.entered = _scriptDebuggerEntered; + adapter->debugger.d.custom = _scriptDebuggerCustom; + adapter->debugger.d.interrupt = _scriptDebuggerInterrupt; + adapter->debugger.d.isPaused = false; + adapter->debugger.d.needsCallback = false; + adapter->debugger.nextBreakpoint = 1; + mDebuggerAttachModule(adapter->core->debugger, &adapter->debugger.d); + return true; +} + +static int64_t _mScriptCoreAdapterSetBreakpoint(struct mScriptCoreAdapter* adapter, struct mScriptValue* callback, uint32_t address, int32_t segment) { + if (!_setupDebugger(adapter)) { + return -1; + } + struct mBreakpoint breakpoint = { + .address = address, + .segment = segment, + .type = BREAKPOINT_HARDWARE + }; + + struct mDebuggerModule* module = &adapter->debugger.d; + if (!module->p->platform->setBreakpoint) { + return -1; + } + struct mScriptBreakpoint* point = _ensureBreakpoint(&adapter->debugger, &breakpoint); + return _addCallbackToBreakpoint(&adapter->debugger, point, callback); +} + +static int64_t _mScriptCoreAdapterSetWatchpoint(struct mScriptCoreAdapter* adapter, struct mScriptValue* callback, uint32_t address, int type, int32_t segment) { + if (!_setupDebugger(adapter)) { + return -1; + } + + struct mWatchpoint watchpoint = { + .minAddress = address, + .maxAddress = address + 1, + .segment = segment, + .type = type, + }; + struct mDebuggerModule* module = &adapter->debugger.d; + if (!module->p->platform->setWatchpoint) { + return -1; + } + struct mScriptBreakpoint* point = _ensureWatchpoint(&adapter->debugger, &watchpoint); + return _addCallbackToBreakpoint(&adapter->debugger, point, callback); +} + +static int64_t _mScriptCoreAdapterSetRangeWatchpoint(struct mScriptCoreAdapter* adapter, struct mScriptValue* callback, uint32_t minAddress, uint32_t maxAddress, int type, int32_t segment) { + if (!_setupDebugger(adapter)) { + return -1; + } + + struct mWatchpoint watchpoint = { + .minAddress = minAddress, + .maxAddress = maxAddress, + .segment = segment, + .type = type, + }; + struct mDebuggerModule* module = &adapter->debugger.d; + if (!module->p->platform->setWatchpoint) { + return -1; + } + struct mScriptBreakpoint* point = _ensureWatchpoint(&adapter->debugger, &watchpoint); + return _addCallbackToBreakpoint(&adapter->debugger, point, callback); +} + +static bool _mScriptCoreAdapterClearBreakpoint(struct mScriptCoreAdapter* adapter, int64_t cbid) { + if (!_setupDebugger(adapter)) { + return false; + } + struct mScriptBreakpoint* point = HashTableLookupBinary(&adapter->debugger.cbidMap, &cbid, sizeof(cbid)); + if (!point) { + return false; + } + HashTableRemoveBinary(&adapter->debugger.cbidMap, &cbid, sizeof(cbid)); + HashTableRemoveBinary(&point->callbacks, &cbid, sizeof(cbid)); + + if (!HashTableSize(&point->callbacks)) { + struct mDebuggerModule* module = &adapter->debugger.d; + module->p->platform->clearBreakpoint(module->p->platform, point->id); + + struct mScriptBreakpointName name = point->name; + HashTableRemoveBinary(&adapter->debugger.breakpoints, &name, sizeof(name)); + } + return true; +} +#endif + static void _mScriptCoreAdapterDeinit(struct mScriptCoreAdapter* adapter) { _clearMemoryMap(adapter->context, adapter, false); adapter->memory.type->free(&adapter->memory); +#ifdef USE_DEBUGGERS + if (adapter->core->debugger) { + mDebuggerDetachModule(adapter->core->debugger, &adapter->debugger.d); + } +#endif } static struct mScriptValue* _mScriptCoreAdapterGet(struct mScriptCoreAdapter* adapter, const char* name) { @@ -686,6 +945,33 @@ mSCRIPT_DECLARE_STRUCT(mScriptCoreAdapter); mSCRIPT_DECLARE_STRUCT_METHOD(mScriptCoreAdapter, W(mCore), _get, _mScriptCoreAdapterGet, 1, CHARP, name); mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptCoreAdapter, _deinit, _mScriptCoreAdapterDeinit, 0); mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptCoreAdapter, reset, _mScriptCoreAdapterReset, 0); +#ifdef USE_DEBUGGERS +mSCRIPT_DECLARE_STRUCT_METHOD_WITH_DEFAULTS(mScriptCoreAdapter, S64, setBreakpoint, _mScriptCoreAdapterSetBreakpoint, 3, WRAPPER, callback, U32, address, S32, segment); +mSCRIPT_DECLARE_STRUCT_METHOD_WITH_DEFAULTS(mScriptCoreAdapter, S64, setWatchpoint, _mScriptCoreAdapterSetWatchpoint, 4, WRAPPER, callback, U32, address, S32, type, S32, segment); +mSCRIPT_DECLARE_STRUCT_METHOD_WITH_DEFAULTS(mScriptCoreAdapter, S64, setRangeWatchpoint, _mScriptCoreAdapterSetRangeWatchpoint, 5, WRAPPER, callback, U32, minAddress, U32, maxAddress, S32, type, S32, segment); +mSCRIPT_DECLARE_STRUCT_METHOD(mScriptCoreAdapter, BOOL, clearBreakpoint, _mScriptCoreAdapterClearBreakpoint, 1, S64, cbid); +#endif + +mSCRIPT_DEFINE_STRUCT_BINDING_DEFAULTS(mScriptCoreAdapter, setBreakpoint) + mSCRIPT_NO_DEFAULT, + mSCRIPT_NO_DEFAULT, + mSCRIPT_S32(-1) +mSCRIPT_DEFINE_DEFAULTS_END; + +mSCRIPT_DEFINE_STRUCT_BINDING_DEFAULTS(mScriptCoreAdapter, setWatchpoint) + mSCRIPT_NO_DEFAULT, + mSCRIPT_NO_DEFAULT, + mSCRIPT_NO_DEFAULT, + mSCRIPT_S32(-1) +mSCRIPT_DEFINE_DEFAULTS_END; + +mSCRIPT_DEFINE_STRUCT_BINDING_DEFAULTS(mScriptCoreAdapter, setRangeWatchpoint) + mSCRIPT_NO_DEFAULT, + mSCRIPT_NO_DEFAULT, + mSCRIPT_NO_DEFAULT, + mSCRIPT_NO_DEFAULT, + mSCRIPT_S32(-1) +mSCRIPT_DEFINE_DEFAULTS_END; mSCRIPT_DEFINE_STRUCT(mScriptCoreAdapter) mSCRIPT_DEFINE_CLASS_DOCSTRING( @@ -700,6 +986,19 @@ mSCRIPT_DEFINE_STRUCT(mScriptCoreAdapter) mSCRIPT_DEFINE_STRUCT_DEFAULT_GET(mScriptCoreAdapter) mSCRIPT_DEFINE_DOCSTRING("Reset the emulation. As opposed to struct::mCore.reset, this version calls the **reset** callback") mSCRIPT_DEFINE_STRUCT_METHOD(mScriptCoreAdapter, reset) +#ifdef USE_DEBUGGERS + mSCRIPT_DEFINE_DOCSTRING("Set a breakpoint at a given address") + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptCoreAdapter, setBreakpoint) + mSCRIPT_DEFINE_DOCSTRING("Clear a breakpoint or watchpoint for a given id returned by a previous call") + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptCoreAdapter, clearBreakpoint) + mSCRIPT_DEFINE_DOCSTRING("Set a watchpoint at a given address of a given type") + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptCoreAdapter, setWatchpoint) + mSCRIPT_DEFINE_DOCSTRING( + "Set a watchpoint in a given range of a given type. Note that the range is exclusive on the end, " + "as though you've added the size, i.e. a 4-byte watch would specify the maximum as the minimum address + 4" + ) + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptCoreAdapter, setRangeWatchpoint) +#endif mSCRIPT_DEFINE_STRUCT_CAST_TO_MEMBER(mScriptCoreAdapter, S(mCore), _core) mSCRIPT_DEFINE_STRUCT_CAST_TO_MEMBER(mScriptCoreAdapter, CS(mCore), _core) mSCRIPT_DEFINE_END; diff --git a/src/core/test/scripting.c b/src/core/test/scripting.c index 4ed2c2abd..c2593bf14 100644 --- a/src/core/test/scripting.c +++ b/src/core/test/scripting.c @@ -9,8 +9,7 @@ #include #include #include -#include -#include +#include #include "script/test.h" @@ -143,8 +142,8 @@ M_TEST_DEFINE(globals) { LOAD_PROGRAM("assert(emu)"); assert_true(lua->run(lua)); - TEARDOWN_CORE; mScriptContextDeinit(&context); + TEARDOWN_CORE; } M_TEST_DEFINE(infoFuncs) { @@ -161,8 +160,8 @@ M_TEST_DEFINE(infoFuncs) { TEST_VALUE(S32, "frequency", core->frequency(core)); TEST_VALUE(S32, "frameCycles", core->frameCycles(core)); - TEARDOWN_CORE; mScriptContextDeinit(&context); + TEARDOWN_CORE; } M_TEST_DEFINE(detach) { @@ -195,8 +194,8 @@ M_TEST_DEFINE(detach) { ); assert_false(lua->run(lua)); - TEARDOWN_CORE; mScriptContextDeinit(&context); + TEARDOWN_CORE; } M_TEST_DEFINE(runFrame) { @@ -215,8 +214,8 @@ M_TEST_DEFINE(runFrame) { TEST_VALUE(S32, "frame", i); } - TEARDOWN_CORE; mScriptContextDeinit(&context); + TEARDOWN_CORE; } M_TEST_DEFINE(memoryRead) { @@ -250,8 +249,8 @@ M_TEST_DEFINE(memoryRead) { TEST_VALUE(S32, "b16", 0x0807); TEST_VALUE(S32, "a32", 0x0C0B0A09); - TEARDOWN_CORE; mScriptContextDeinit(&context); + TEARDOWN_CORE; } M_TEST_DEFINE(memoryWrite) { @@ -278,8 +277,8 @@ M_TEST_DEFINE(memoryWrite) { assert_int_equal(core->busRead8(core, RAM_BASE + i), i + 1); } - TEARDOWN_CORE; mScriptContextDeinit(&context); + TEARDOWN_CORE; } M_TEST_DEFINE(logging) { @@ -323,11 +322,402 @@ M_TEST_DEFINE(screenshot) { TEST_PROGRAM("assert(im.width >= 160)"); TEST_PROGRAM("assert(im.height >= 144)"); - TEARDOWN_CORE; free(buffer); mScriptContextDeinit(&context); + TEARDOWN_CORE; } +#ifdef USE_DEBUGGERS +void _setupBp(struct mCore* core) { + switch (core->platform(core)) { +#ifdef M_CORE_GBA + case mPLATFORM_GBA: + core->busWrite32(core, 0x020000C0, 0xE0000000); // nop + core->busWrite32(core, 0x020000C4, 0xE0000000); // nop + core->busWrite32(core, 0x020000C8, 0xEAFFFFFD); // b 0x020000C4 + break; +#endif +#ifdef M_CORE_GB + case mPLATFORM_GB: + core->rawWrite8(core, 0x101, 0, 0xEE); // Jump to 0xF0 + core->rawWrite8(core, 0xF0, 0, 0x00); // nop + core->rawWrite8(core, 0xF1, 0, 0x18); // Loop forecer + core->rawWrite8(core, 0xF2, 0, 0xFD); // jr $-3 + break; +#endif + } +} + +#ifdef M_CORE_GBA +M_TEST_DEFINE(basicBreakpointGBA) { + SETUP_LUA; + struct mCore* core = mCoreCreate(mPLATFORM_GBA); + struct mDebugger debugger; + assert_non_null(core); + assert_true(core->init(core)); + mCoreInitConfig(core, NULL); + core->reset(core); + _setupBp(core); + mScriptContextAttachCore(&context, core); + + mDebuggerInit(&debugger); + mDebuggerAttach(&debugger, core); + + TEST_PROGRAM( + "hit = 0\n" + "function bkpt()\n" + " hit = hit + 1\n" + "end" + ); + TEST_PROGRAM("cbid = emu:setBreakpoint(bkpt, 0x020000C4)"); + TEST_PROGRAM("assert(cbid == 1)"); + + int i; + for (i = 0; i < 20; ++i) { + mDebuggerRun(&debugger); + } + + assert_int_equal(debugger.state, DEBUGGER_RUNNING); + TEST_PROGRAM("assert(hit >= 1)"); + + mScriptContextDeinit(&context); + TEARDOWN_CORE; + mDebuggerDeinit(&debugger); +} +#endif + +#ifdef M_CORE_GB +M_TEST_DEFINE(basicBreakpointGB) { + SETUP_LUA; + struct mCore* core = mCoreCreate(mPLATFORM_GB); + struct mDebugger debugger; + assert_non_null(core); + assert_true(core->init(core)); + mCoreInitConfig(core, NULL); + assert_true(core->loadROM(core, VFileFromConstMemory(_fakeGBROM, sizeof(_fakeGBROM)))); + core->reset(core); + _setupBp(core); + mScriptContextAttachCore(&context, core); + + mDebuggerInit(&debugger); + mDebuggerAttach(&debugger, core); + + TEST_PROGRAM( + "hit = 0\n" + "function bkpt()\n" + " hit = hit + 1\n" + "end" + ); + TEST_PROGRAM("cbid = emu:setBreakpoint(bkpt, 0xF0)"); + TEST_PROGRAM("assert(cbid == 1)"); + + int i; + for (i = 0; i < 20; ++i) { + mDebuggerRun(&debugger); + } + + assert_int_equal(debugger.state, DEBUGGER_RUNNING); + TEST_PROGRAM("assert(hit >= 1)"); + + mScriptContextDeinit(&context); + TEARDOWN_CORE; + mDebuggerDeinit(&debugger); +} +#endif + +M_TEST_DEFINE(multipleBreakpoint) { + SETUP_LUA; + struct mCore* core = mCoreCreate(TEST_PLATFORM); + struct mDebugger debugger; + assert_non_null(core); + assert_true(core->init(core)); + mCoreInitConfig(core, NULL); + core->reset(core); + _setupBp(core); + mScriptContextAttachCore(&context, core); + + mDebuggerInit(&debugger); + mDebuggerAttach(&debugger, core); + + TEST_PROGRAM( + "hit = 0\n" + "function bkpt1()\n" + " hit = hit + 1\n" + "end\n" + "function bkpt2()\n" + " hit = hit + 100\n" + "end" + ); +#ifdef M_CORE_GBA + TEST_PROGRAM("cbid1 = emu:setBreakpoint(bkpt1, 0x020000C4)"); + TEST_PROGRAM("cbid2 = emu:setBreakpoint(bkpt2, 0x020000C8)"); +#else + TEST_PROGRAM("cbid1 = emu:setBreakpoint(bkpt1, 0xF0)"); + TEST_PROGRAM("cbid2 = emu:setBreakpoint(bkpt2, 0xF1)"); +#endif + TEST_PROGRAM("assert(cbid1 == 1)"); + TEST_PROGRAM("assert(cbid2 == 2)"); + + int i; + for (i = 0; i < 20; ++i) { + mDebuggerRun(&debugger); + } + + assert_int_equal(debugger.state, DEBUGGER_RUNNING); + TEST_PROGRAM("assert(hit >= 101)"); + + mScriptContextDeinit(&context); + TEARDOWN_CORE; + mDebuggerDeinit(&debugger); +} + +M_TEST_DEFINE(basicWatchpoint) { + SETUP_LUA; + mScriptContextAttachStdlib(&context); + CREATE_CORE; + struct mDebugger debugger; + core->reset(core); + mScriptContextAttachCore(&context, core); + + mDebuggerInit(&debugger); + mDebuggerAttach(&debugger, core); + + TEST_PROGRAM( + "hit = 0\n" + "function bkpt()\n" + " hit = hit + 1\n" + "end" + ); + struct mScriptValue base = mSCRIPT_MAKE_S32(RAM_BASE); + lua->setGlobal(lua, "base", &base); + TEST_PROGRAM("assert(0 < emu:setWatchpoint(bkpt, base, C.WATCHPOINT_TYPE.READ))"); + TEST_PROGRAM("assert(0 < emu:setWatchpoint(bkpt, base + 1, C.WATCHPOINT_TYPE.WRITE))"); + TEST_PROGRAM("assert(0 < emu:setWatchpoint(bkpt, base + 2, C.WATCHPOINT_TYPE.RW))"); + TEST_PROGRAM("assert(0 < emu:setWatchpoint(bkpt, base + 3, C.WATCHPOINT_TYPE.WRITE_CHANGE))"); + TEST_PROGRAM("assert(hit == 0)"); + + uint8_t value; + + // Read + TEST_PROGRAM("hit = 0"); + value = core->rawRead8(core, RAM_BASE, -1); + TEST_PROGRAM("assert(hit == 0)"); + core->busRead8(core, RAM_BASE); + TEST_PROGRAM("assert(hit == 1)"); + core->busWrite8(core, RAM_BASE, value); + TEST_PROGRAM("assert(hit == 1)"); + core->busWrite8(core, RAM_BASE, ~value); + TEST_PROGRAM("assert(hit == 1)"); + + // Write + TEST_PROGRAM("hit = 0"); + value = core->rawRead8(core, RAM_BASE + 1, -1); + TEST_PROGRAM("assert(hit == 0)"); + core->busRead8(core, RAM_BASE + 1); + TEST_PROGRAM("assert(hit == 0)"); + core->busWrite8(core, RAM_BASE + 1, value); + TEST_PROGRAM("assert(hit == 1)"); + core->busWrite8(core, RAM_BASE + 1, ~value); + TEST_PROGRAM("assert(hit == 2)"); + + // RW + TEST_PROGRAM("hit = 0"); + value = core->rawRead8(core, RAM_BASE + 2, -1); + TEST_PROGRAM("assert(hit == 0)"); + core->busRead8(core, RAM_BASE + 2); + TEST_PROGRAM("assert(hit == 1)"); + core->busWrite8(core, RAM_BASE + 2, value); + TEST_PROGRAM("assert(hit == 2)"); + core->busWrite8(core, RAM_BASE + 2, ~value); + TEST_PROGRAM("assert(hit == 3)"); + + // Change + TEST_PROGRAM("hit = 0"); + value = core->rawRead8(core, RAM_BASE + 3, -1); + TEST_PROGRAM("assert(hit == 0)"); + core->busRead8(core, RAM_BASE + 3); + TEST_PROGRAM("assert(hit == 0)"); + core->busWrite8(core, RAM_BASE + 3, value); + TEST_PROGRAM("assert(hit == 0)"); + core->busWrite8(core, RAM_BASE + 3, ~value); + TEST_PROGRAM("assert(hit == 1)"); + + mScriptContextDeinit(&context); + TEARDOWN_CORE; + mDebuggerDeinit(&debugger); +} + +M_TEST_DEFINE(removeBreakpoint) { + SETUP_LUA; + mScriptContextAttachStdlib(&context); + CREATE_CORE; + struct mDebugger debugger; + core->reset(core); + mScriptContextAttachCore(&context, core); + + mDebuggerInit(&debugger); + mDebuggerAttach(&debugger, core); + + TEST_PROGRAM( + "hit = 0\n" + "function bkpt()\n" + " hit = hit + 1\n" + "end" + ); + struct mScriptValue base = mSCRIPT_MAKE_S32(RAM_BASE); + lua->setGlobal(lua, "base", &base); + TEST_PROGRAM("cbid = emu:setWatchpoint(bkpt, base, C.WATCHPOINT_TYPE.READ)"); + + TEST_PROGRAM("assert(hit == 0)"); + core->busRead8(core, RAM_BASE); + TEST_PROGRAM("assert(hit == 1)"); + core->busRead8(core, RAM_BASE); + TEST_PROGRAM("assert(hit == 2)"); + TEST_PROGRAM("assert(emu:clearBreakpoint(cbid))"); + core->busRead8(core, RAM_BASE); + TEST_PROGRAM("assert(hit == 2)"); + + mScriptContextDeinit(&context); + TEARDOWN_CORE; + mDebuggerDeinit(&debugger); +} + + +M_TEST_DEFINE(overlappingBreakpoint) { + SETUP_LUA; + struct mCore* core = mCoreCreate(TEST_PLATFORM); + struct mDebugger debugger; + assert_non_null(core); + assert_true(core->init(core)); + mCoreInitConfig(core, NULL); + core->reset(core); + _setupBp(core); + mScriptContextAttachCore(&context, core); + + mDebuggerInit(&debugger); + mDebuggerAttach(&debugger, core); + + TEST_PROGRAM( + "hit = 0\n" + "function bkpt1()\n" + " hit = hit + 1\n" + "end\n" + "function bkpt2()\n" + " hit = hit + 100\n" + "end" + ); +#ifdef M_CORE_GBA + TEST_PROGRAM("cbid1 = emu:setBreakpoint(bkpt1, 0x020000C4)"); + TEST_PROGRAM("cbid2 = emu:setBreakpoint(bkpt2, 0x020000C4)"); +#else + TEST_PROGRAM("cbid1 = emu:setBreakpoint(bkpt1, 0xF0)"); + TEST_PROGRAM("cbid2 = emu:setBreakpoint(bkpt2, 0xF0)"); +#endif + TEST_PROGRAM("assert(cbid1 == 1)"); + TEST_PROGRAM("assert(cbid2 == 2)"); + + int i; + for (i = 0; i < 20; ++i) { + mDebuggerRun(&debugger); + } + + assert_int_equal(debugger.state, DEBUGGER_RUNNING); + TEST_PROGRAM("assert(hit >= 101)"); + TEST_PROGRAM("oldHit = hit"); + + TEST_PROGRAM("assert(emu:clearBreakpoint(cbid2))"); + + for (i = 0; i < 10; ++i) { + mDebuggerRun(&debugger); + } + TEST_PROGRAM("assert(hit - oldHit > 0)"); + TEST_PROGRAM("assert(hit - oldHit < 100)"); + + mScriptContextDeinit(&context); + TEARDOWN_CORE; + mDebuggerDeinit(&debugger); +} + +M_TEST_DEFINE(overlappingWatchpoint) { + SETUP_LUA; + mScriptContextAttachStdlib(&context); + CREATE_CORE; + struct mDebugger debugger; + core->reset(core); + mScriptContextAttachCore(&context, core); + + mDebuggerInit(&debugger); + mDebuggerAttach(&debugger, core); + + TEST_PROGRAM( + "hit = 0\n" + "function bkpt()\n" + " hit = hit + 1\n" + "end" + ); + struct mScriptValue base = mSCRIPT_MAKE_S32(RAM_BASE); + lua->setGlobal(lua, "base", &base); + TEST_PROGRAM("assert(0 < emu:setWatchpoint(bkpt, base, C.WATCHPOINT_TYPE.READ))"); + TEST_PROGRAM("assert(0 < emu:setWatchpoint(bkpt, base, C.WATCHPOINT_TYPE.WRITE))"); + TEST_PROGRAM("assert(0 < emu:setWatchpoint(bkpt, base, C.WATCHPOINT_TYPE.RW))"); + TEST_PROGRAM("assert(0 < emu:setWatchpoint(bkpt, base, C.WATCHPOINT_TYPE.WRITE_CHANGE))"); + TEST_PROGRAM("assert(hit == 0)"); + + uint8_t value; + + // Read + TEST_PROGRAM("hit = 0"); + value = core->rawRead8(core, RAM_BASE, -1); + TEST_PROGRAM("assert(hit == 0)"); + core->busRead8(core, RAM_BASE); + TEST_PROGRAM("assert(hit == 2)"); // Read, RW + core->busWrite8(core, RAM_BASE, value); + TEST_PROGRAM("assert(hit == 4)"); // Write, RW + core->busWrite8(core, RAM_BASE, ~value); + TEST_PROGRAM("assert(hit == 7)"); // Write, RW, change + + mScriptContextDeinit(&context); + TEARDOWN_CORE; + mDebuggerDeinit(&debugger); +} + +M_TEST_DEFINE(rangeWatchpoint) { + SETUP_LUA; + mScriptContextAttachStdlib(&context); + CREATE_CORE; + struct mDebugger debugger; + core->reset(core); + mScriptContextAttachCore(&context, core); + + mDebuggerInit(&debugger); + mDebuggerAttach(&debugger, core); + + TEST_PROGRAM( + "hit = 0\n" + "function bkpt()\n" + " hit = hit + 1\n" + "end" + ); + struct mScriptValue base = mSCRIPT_MAKE_S32(RAM_BASE); + lua->setGlobal(lua, "base", &base); + TEST_PROGRAM("assert(0 < emu:setRangeWatchpoint(bkpt, base, base + 2, C.WATCHPOINT_TYPE.READ))"); + TEST_PROGRAM("assert(0 < emu:setRangeWatchpoint(bkpt, base + 1, base + 3, C.WATCHPOINT_TYPE.READ))"); + + // Read + TEST_PROGRAM("assert(hit == 0)"); + core->busRead8(core, RAM_BASE); + TEST_PROGRAM("assert(hit == 1)"); + core->busRead8(core, RAM_BASE + 1); + TEST_PROGRAM("assert(hit == 3)"); + core->busRead8(core, RAM_BASE + 2); + TEST_PROGRAM("assert(hit == 4)"); + + mScriptContextDeinit(&context); + TEARDOWN_CORE; + mDebuggerDeinit(&debugger); +} +#endif + M_TEST_SUITE_DEFINE_SETUP_TEARDOWN(mScriptCore, cmocka_unit_test(globals), cmocka_unit_test(infoFuncs), @@ -337,4 +727,18 @@ M_TEST_SUITE_DEFINE_SETUP_TEARDOWN(mScriptCore, cmocka_unit_test(memoryWrite), cmocka_unit_test(logging), cmocka_unit_test(screenshot), +#ifdef USE_DEBUGGERS +#ifdef M_CORE_GBA + cmocka_unit_test(basicBreakpointGBA), +#endif +#ifdef M_CORE_GB + cmocka_unit_test(basicBreakpointGB), +#endif + cmocka_unit_test(multipleBreakpoint), + cmocka_unit_test(basicWatchpoint), + cmocka_unit_test(removeBreakpoint), + cmocka_unit_test(overlappingBreakpoint), + cmocka_unit_test(overlappingWatchpoint), + cmocka_unit_test(rangeWatchpoint), +#endif ) diff --git a/src/script/stdlib.c b/src/script/stdlib.c index 8a64ac0c1..c6070dcb6 100644 --- a/src/script/stdlib.c +++ b/src/script/stdlib.c @@ -158,6 +158,15 @@ void mScriptContextAttachStdlib(struct mScriptContext* context) { mSCRIPT_CONSTANT_PAIR(GB_KEY, DOWN), mSCRIPT_KV_SENTINEL }); +#endif +#ifdef USE_DEBUGGERS + mScriptContextExportConstants(context, "WATCHPOINT_TYPE", (struct mScriptKVPair[]) { + mSCRIPT_CONSTANT_PAIR(WATCHPOINT, WRITE), + mSCRIPT_CONSTANT_PAIR(WATCHPOINT, READ), + mSCRIPT_CONSTANT_PAIR(WATCHPOINT, RW), + mSCRIPT_CONSTANT_PAIR(WATCHPOINT, WRITE_CHANGE), + mSCRIPT_KV_SENTINEL + }); #endif mScriptContextSetGlobal(context, "C", context->constants); mScriptContextSetDocstring(context, "C", "A table containing the [exported constants](#constants)");