From 988e3c5cac81e21f16e5c558b536874d3ce242da Mon Sep 17 00:00:00 2001 From: tommyb3ll Date: Thu, 8 Oct 2026 14:23:49 -0400 Subject: [PATCH 1/4] headless: peek, poke, keydown, keyup, regs, stats and lcd-dma commands For scripted tests and profiling of programs: read and write memory, hold a key while inspecting the calculator, see where a program is stuck (registers, IX frames), and read the cycle counters, flash cache misses included, which show how much time a program loses to the revision M serial flash. Co-Authored-By: Claude Opus 5.5 --- tests/autotester/headless_cli.cpp | 99 ++++++++++++++++++++++++++- tests/autotester/headless_protocol.md | 14 ++++ 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/tests/autotester/headless_cli.cpp b/tests/autotester/headless_cli.cpp index 4e286e4f..33280d16 100644 --- a/tests/autotester/headless_cli.cpp +++ b/tests/autotester/headless_cli.cpp @@ -5,6 +5,7 @@ * keep one core instance alive without pulling in Qt or another IPC library. */ +#include #include #include #include @@ -75,6 +76,17 @@ bool parseUnsigned(const std::string& text, uint32_t& result) return true; } +bool parseAddress(const std::string& text, uint32_t& result) +{ + char *end; + const unsigned long value = std::strtoul(text.c_str(), &end, 16); + if (!std::isxdigit(static_cast(text[0])) || *end || value > 0xFFFFFFu) { + return false; + } + result = static_cast(value); + return true; +} + bool fileExists(const std::string& path) { return !path.empty() && std::ifstream(path, std::ios::binary).good(); @@ -252,7 +264,9 @@ bool runCommand(const std::string& line) "key [hold-ms]; keys ; " "screenshot ; screen-hash; save-state ; " "send-file [ram|archive|auto] ; " - "usb ; reset; status; quit"); + "usb ; reset; status; " + "peek [count]; poke ; " + "keydown ; keyup ; regs; stats; lcd-dma <0|1>; quit"); return true; } if (command == "run") { @@ -410,6 +424,89 @@ bool runCommand(const std::string& line) return true; } + if (command == "peek") { + std::string address_text; + std::string count_text; + uint32_t address; + uint32_t count = 1; + if (!(input >> address_text) || !parseAddress(address_text, address) || + ((input >> count_text) && (!parseUnsigned(count_text, count) || count == 0 || count > 4096))) { + respond("ERR usage: peek [count 1-4096]"); + } else { + std::string bytes; + char hex[3]; + for (uint32_t i = 0; i < count; ++i) { + std::snprintf(hex, sizeof hex, "%02X", cemucore::mem_peek_byte((address + i) & 0xFFFFFFu)); + bytes += hex; + } + respond("OK peek " + bytes); + } + return true; + } + if (command == "poke") { + std::string address_text; + std::string bytes; + uint32_t address; + if (!(input >> address_text >> bytes) || !parseAddress(address_text, address) || bytes.size() % 2 || + bytes.find_first_not_of("0123456789abcdefABCDEF") != std::string::npos) { + respond("ERR usage: poke "); + } else { + for (size_t i = 0; i < bytes.size(); i += 2) { + cemucore::mem_poke_byte((address + static_cast(i / 2)) & 0xFFFFFFu, + static_cast(std::strtoul(bytes.substr(i, 2).c_str(), nullptr, 16))); + } + respond("OK poke " + std::to_string(bytes.size() / 2)); + } + return true; + } + if (command == "keydown" || command == "keyup") { + std::string name; + autotester::key_coord_t coord{}; + input >> name; + if (!autotester::keyCoordForName(name, coord)) { + respond("ERR unknown key " + name); + } else { + cemucore::emu_keypad_event(coord.y, coord.x, command == "keydown"); + respond("OK " + command + " " + name); + } + return true; + } + if (command == "regs") { + const auto& r = cemucore::cpu.registers; + char text[192]; + std::snprintf(text, sizeof text, + "OK regs pc=%06X sp=%06X af=%04X bc=%06X de=%06X hl=%06X ix=%06X iy=%06X adl=%d halted=%d", + static_cast(r.PC), static_cast(r.SPL), static_cast(r.AF), + static_cast(r.BC), static_cast(r.DE), static_cast(r.HL), + static_cast(r.IX), static_cast(r.IY), + static_cast(cemucore::cpu.ADL), static_cast(cemucore::cpu.halted)); + respond(text); + return true; + } + if (command == "stats") { + char text[192]; + std::snprintf(text, sizeof text, + "OK stats cycles=%llu halted=%llu dma=%llu flash-reads=%lu flash-misses=%lu flash-delay=%lld", + static_cast(cemucore::cpu.baseCycles + cemucore::cpu.cycles), + static_cast(cemucore::cpu.haltCycles), + static_cast(cemucore::cpu.dmaCycles), + static_cast(cemucore::cpu.flashTotalAccesses), + static_cast(cemucore::cpu.flashCacheMisses), + static_cast(cemucore::cpu.flashDelayCycles)); + respond(text); + return true; + } + if (command == "lcd-dma") { + std::string value; + if (!(input >> value) || (value != "0" && value != "1")) { + respond("ERR usage: lcd-dma <0|1>"); + } else { + cemucore::emu_set_lcd_dma(value == "1"); + respond("OK lcd-dma " + value); + } + return true; + } + respond("ERR unknown command " + command); return true; } diff --git a/tests/autotester/headless_protocol.md b/tests/autotester/headless_protocol.md index 2bc370b0..882aaf74 100644 --- a/tests/autotester/headless_protocol.md +++ b/tests/autotester/headless_protocol.md @@ -24,6 +24,20 @@ Supported commands: emulated host controller, or disconnects the current USB peer. - `reset` resets the emulated calculator. - `status` reports the device, ASIC revision, Python flag, and run rate. +- `peek [count]` returns `count` bytes (1 to 4096, default 1) read + from the address space without side effects, as hex: `OK peek 3F00C9`. +- `poke ` writes bytes (flash included, ignoring its + protection), for example `poke D0008E 01`. +- `keydown ` and `keyup ` press and release a key separately, without + running the emulator, to inspect the calculator while a key is held. +- `regs` reports the CPU registers (PC, SPL, AF, BC, DE, HL, IX, IY), ADL and + whether the CPU is halted. With a stack of IX frames, the caller's IX is at + (IX) and the return address at (IX+3): enough to see where a program hangs. +- `stats` reports the cycle counters since power-on: total and halted cycles, + cycles taken by the LCD's DMA, serial flash reads and cache misses (revision M + and later: an 8 KB cache, about 197 cycles per miss) and the flash wait cycles. +- `lcd-dma <0|1>` emulates the LCD's DMA, which takes RAM cycles from the CPU + while the screen refreshes (off by default here), or turns it off. - `help` lists commands. - `quit` exits cleanly. From 5d51a7c882068f66820f8ee31f6eb4a88ed2829c Mon Sep 17 00:00:00 2001 From: tommyb3ll Date: Thu, 8 Oct 2026 14:23:49 -0400 Subject: [PATCH 2/4] core: optionally print the debug console without DEBUG_SUPPORT Builds without the debugger (cemu-headless, the autotester) drop what programs write to the debug console ports. With the new opt-in define DEBUG_CONSOLE_SUPPORT (see core/Makefile), they print it through gui_console_printf / gui_console_err_printf, as the debugger build does when soft commands are on, so test scripts can read a program's dbg_printf output. It is off by default, so other consumers of the core are not affected. Co-Authored-By: Claude Opus 5.5 --- core/Makefile | 4 ++++ core/mem.c | 18 ++++++++++++++++++ tests/autotester/headless_protocol.md | 5 +++++ 3 files changed, 27 insertions(+) diff --git a/core/Makefile b/core/Makefile index 3309a356..ed9b97f0 100644 --- a/core/Makefile +++ b/core/Makefile @@ -10,6 +10,10 @@ CFLAGS = -Wall -Wextra -fPIC -O3 -std=gnu11 -static # Add debugging support, with zdis disassembler #CFLAGS += -DDEBUG_SUPPORT +# Without debugging support, print what programs write to the debug console (dbg_printf) +# through gui_console_printf / gui_console_err_printf (cemu-headless, the autotester) +#CFLAGS += -DDEBUG_CONSOLE_SUPPORT + # Add these flags if your compiler supports it #CFLAGS += -Wstack-protector -fstack-protector-strong --param=ssp-buffer-size=1 -fsanitize=address,bounds -fsanitize-undefined-trap-on-error diff --git a/core/mem.c b/core/mem.c index 1119d877..a9039103 100644 --- a/core/mem.c +++ b/core/mem.c @@ -812,6 +812,24 @@ void mem_write_cpu(uint32_t addr, uint8_t value) { /* MMIO <-> Advanced Perphrial Bus */ case 0xE: case 0xF: +#if defined(DEBUG_CONSOLE_SUPPORT) && !defined(DEBUG_SUPPORT) + if (addr >= DBGOUT_PORT_RANGE && addr < DBGEXT_PORT) { + static char console_buffer[2][SIZEOF_DBG_BUFFER]; + static unsigned console_pos[2]; + const int err = addr >= DBGERR_PORT_RANGE; + console_buffer[err][console_pos[err]] = (char)value; + console_pos[err] = (console_pos[err] + 1) % (SIZEOF_DBG_BUFFER - 1); + if (!value) { + if (err) { + gui_console_err_printf("%s", console_buffer[err]); + } else { + gui_console_printf("%s", console_buffer[err]); + } + console_pos[err] = 0; + } + break; + } +#endif #ifdef DEBUG_SUPPORT if (debug_get_flags() & DBG_SOFT_COMMANDS) { if (addr >= DBG_PORT_RANGE) { diff --git a/tests/autotester/headless_protocol.md b/tests/autotester/headless_protocol.md index 882aaf74..93a6ef97 100644 --- a/tests/autotester/headless_protocol.md +++ b/tests/autotester/headless_protocol.md @@ -41,6 +41,11 @@ Supported commands: - `help` lists commands. - `quit` exits cleanly. +If the core was built with `-DDEBUG_CONSOLE_SUPPORT` (for example `make +CPPFLAGS=-DDEBUG_CONSOLE_SUPPORT` in `core/`), what the calculator's programs print +to the debug console (the toolchain's `dbg_printf`) is written to standard error, as +the core's diagnostics are. + For example: ```text From 1be9bfbdf6901a35e662ad544b046e5659749463 Mon Sep 17 00:00:00 2001 From: tommyb3ll Date: Thu, 8 Oct 2026 14:23:49 -0400 Subject: [PATCH 3/4] Flash cache profiling: per-line counters, a line trace, a fixed flash timing Since revision M, code runs from serial flash through an 8 KB cache, and a miss costs about 197 cycles: a large program can spend a third of its time there. The core can now count, per 32-byte flash line, the entries and the misses (flash_line_profile), or report every line entered (flash_line_hook), and cemu-headless exposes both (line-profile, line-trace). flash-cycles gives every flash read a fixed cost instead, close to a pre-revision-M calculator with n=10. Measured with them on KhiCAS: misses were ~30% of its cycles, and a cache simulation of its trace showed that even a perfect code layout would remove only about a quarter of them. Co-Authored-By: Claude Opus 5.5 --- core/flash.c | 13 ++++ core/flash.h | 14 ++++ core/mem.c | 2 +- tests/autotester/headless_cli.cpp | 95 ++++++++++++++++++++++++++- tests/autotester/headless_protocol.md | 12 ++++ 5 files changed, 134 insertions(+), 2 deletions(-) diff --git a/core/flash.c b/core/flash.c index be5f8842..7a216ec3 100644 --- a/core/flash.c +++ b/core/flash.c @@ -52,6 +52,10 @@ void flash_flush_cache(void) { } } +uint32_t *flash_line_profile; +void (*flash_line_hook)(uint32_t line); +uint32_t flash_fixed_cycles; + uint32_t flash_touch_cache(uint32_t addr) { if (unlikely(++cpu.flashTotalAccesses == 0)) { cpu.flashTotalAccesses = 0x80000000; @@ -62,6 +66,12 @@ uint32_t flash_touch_cache(uint32_t addr) { return 2; } else { flash.lastCacheLine = line; + if (unlikely(flash_line_profile != NULL)) { + flash_line_profile[2 * (line & (FLASH_PROFILE_LINES - 1))]++; + } + if (unlikely(flash_line_hook != NULL)) { + flash_line_hook(line); + } flash_cache_set_t* set = &flash.cacheTags[line & (FLASH_CACHE_SETS - 1)]; uint16_t tag = (uint16_t)(line >> FLASH_CACHE_SET_BITS); if (likely(set->mru == tag)) { @@ -78,6 +88,9 @@ uint32_t flash_touch_cache(uint32_t addr) { set->lru = set->mru; set->mru = tag; cpu.flashCacheMisses++; + if (unlikely(flash_line_profile != NULL)) { + flash_line_profile[2 * (line & (FLASH_PROFILE_LINES - 1)) + 1]++; + } /* Supposedly this takes from 195-201 cycles, but typically seems to be 196-197 */ cpu.flashDelayCycles += 195; return 197; diff --git a/core/flash.h b/core/flash.h index 8dd04a19..669eb7c4 100644 --- a/core/flash.h +++ b/core/flash.h @@ -40,6 +40,20 @@ typedef struct flash_state { extern flash_state_t flash; +/* Optional profiling of serial flash reads for front ends (off when NULL). A line is + * FLASH_CACHE_LINE_SIZE bytes; it is entered when a read follows a read of another line. + * flash_line_profile: 2 counters per line (FLASH_PROFILE_LINES lines), the times it was + * entered then its cache misses. flash_line_hook: called with each line entered, in order + * (for cache simulations and code layout studies). */ +#define FLASH_PROFILE_LINES (0x1000000 >> FLASH_CACHE_LINE_BITS) +extern uint32_t *flash_line_profile; +extern void (*flash_line_hook)(uint32_t line); + +/* Nonzero: every serial flash read costs this many cycles and the cache is not modeled. + * 10 is what a calculator made before revision M spends per byte (parallel flash with the + * OS's 9 wait states), to compare timings with a revision M ROM. */ +extern uint32_t flash_fixed_cycles; + void flash_flush_cache(void); uint32_t flash_touch_cache(uint32_t addr); diff --git a/core/mem.c b/core/mem.c index a9039103..f6ad583f 100644 --- a/core/mem.c +++ b/core/mem.c @@ -598,7 +598,7 @@ static uint8_t mem_read_flash_parallel(uint32_t addr) { } static uint8_t mem_read_flash_serial(uint32_t addr) { - cpu.cycles += flash_touch_cache(addr); + cpu.cycles += unlikely(flash_fixed_cycles) ? flash_fixed_cycles : flash_touch_cache(addr); return mem.flash.block[addr & flash.mask]; } diff --git a/tests/autotester/headless_cli.cpp b/tests/autotester/headless_cli.cpp index 33280d16..e587dd17 100644 --- a/tests/autotester/headless_cli.cpp +++ b/tests/autotester/headless_cli.cpp @@ -27,6 +27,7 @@ namespace cemucore { extern "C" { + #include "../../core/flash.h" #include "../../core/usb/usb.h" void gui_console_clear() {} @@ -55,6 +56,29 @@ namespace cemucore namespace { +std::FILE *line_trace; +std::vector line_trace_buffer; + +void lineTraceHook(uint32_t line) +{ + line_trace_buffer.push_back(line); + if (line_trace_buffer.size() == 65536) { + std::fwrite(line_trace_buffer.data(), sizeof(uint32_t), line_trace_buffer.size(), line_trace); + line_trace_buffer.clear(); + } +} + +void lineTraceStop() +{ + if (line_trace) { + std::fwrite(line_trace_buffer.data(), sizeof(uint32_t), line_trace_buffer.size(), line_trace); + std::fclose(line_trace); + line_trace = nullptr; + } + line_trace_buffer.clear(); + cemucore::flash_line_hook = nullptr; +} + struct options_t { std::string rom; std::string image; @@ -266,7 +290,9 @@ bool runCommand(const std::string& line) "send-file [ram|archive|auto] ; " "usb ; reset; status; " "peek [count]; poke ; " - "keydown ; keyup ; regs; stats; lcd-dma <0|1>; quit"); + "keydown ; keyup ; regs; stats; lcd-dma <0|1>; " + "line-profile >; line-trace |off>; " + "flash-cycles ; quit"); return true; } if (command == "run") { @@ -507,6 +533,72 @@ bool runCommand(const std::string& line) return true; } + if (command == "line-profile") { + std::string what; + input >> what; + const size_t counters = 2 * FLASH_PROFILE_LINES; + if (what == "on") { + if (!cemucore::flash_line_profile) { + cemucore::flash_line_profile = static_cast(std::calloc(counters, sizeof(uint32_t))); + } else { + std::memset(cemucore::flash_line_profile, 0, counters * sizeof(uint32_t)); + } + respond(cemucore::flash_line_profile ? "OK line-profile on" : "ERR out of memory"); + } else if (what == "save" && cemucore::flash_line_profile) { + std::string path; + std::getline(input >> std::ws, path); + std::FILE *file = path.empty() ? nullptr : std::fopen(path.c_str(), "wb"); + if (!file) { + respond("ERR cannot write " + path); + } else { + const bool ok = std::fwrite(cemucore::flash_line_profile, sizeof(uint32_t), counters, file) == counters; + std::fclose(file); + respond(ok ? "OK line-profile save" : "ERR cannot write " + path); + } + } else if (what == "off") { + std::free(cemucore::flash_line_profile); + cemucore::flash_line_profile = nullptr; + respond("OK line-profile off"); + } else { + respond("ERR usage: line-profile > (save needs on)"); + } + return true; + } + if (command == "line-trace") { + std::string what; + input >> what; + if (what == "on") { + std::string path; + std::getline(input >> std::ws, path); + std::FILE *file = path.empty() ? nullptr : std::fopen(path.c_str(), "wb"); + if (!file) { + respond("ERR cannot write " + path); + } else { + lineTraceStop(); + line_trace = file; + cemucore::flash_line_hook = lineTraceHook; + respond("OK line-trace on"); + } + } else if (what == "off") { + lineTraceStop(); + respond("OK line-trace off"); + } else { + respond("ERR usage: line-trace |off>"); + } + return true; + } + if (command == "flash-cycles") { + std::string value; + uint32_t cycles; + if (!(input >> value) || !parseUnsigned(value, cycles) || cycles > 1000) { + respond("ERR usage: flash-cycles (0: the revision M cache)"); + } else { + cemucore::flash_fixed_cycles = cycles; + respond("OK flash-cycles " + value); + } + return true; + } + respond("ERR unknown command " + command); return true; } @@ -539,6 +631,7 @@ int main(int argc, char **argv) std::string line; while (std::getline(std::cin, line) && runCommand(line)) {} + lineTraceStop(); cemucore::emu_exit(); cemucore::asic_free(); return EXIT_SUCCESS; diff --git a/tests/autotester/headless_protocol.md b/tests/autotester/headless_protocol.md index 93a6ef97..8d86264b 100644 --- a/tests/autotester/headless_protocol.md +++ b/tests/autotester/headless_protocol.md @@ -38,6 +38,18 @@ Supported commands: and later: an 8 KB cache, about 197 cycles per miss) and the flash wait cycles. - `lcd-dma <0|1>` emulates the LCD's DMA, which takes RAM cycles from the CPU while the screen refreshes (off by default here), or turns it off. +- `line-profile on` counts, for each 32-byte line of flash, how many times the + CPU entered it (a read after a read of another line) and how many of those + missed the serial flash cache; `line-profile save ` writes the counters + (2 per line: entries then misses, 32-bit, host byte order, 524288 lines for + the 16 MB address space); `line-profile off` stops counting. +- `line-trace on ` writes every line entered, in order, as 32-bit line + numbers (address / 32, host byte order), for cache simulations and code layout + studies; `line-trace off` closes the file. +- `flash-cycles ` makes every serial flash read cost n cycles without the + cache; 10 is close to a calculator made before revision M (parallel flash with + the OS's 9 wait states), to compare a program's speed on older hardware with a + revision M ROM. `flash-cycles 0` restores the cache. - `help` lists commands. - `quit` exits cleanly. From fe5ab3c3cc95437e0ed7502fd8a33bdadd9eefb6 Mon Sep 17 00:00:00 2001 From: tommyb3ll Date: Mon, 12 Oct 2026 17:04:30 -0400 Subject: [PATCH 4/4] core: the debug console code also with DEBUG_SUPPORT As suggested in review: #if defined(DEBUG_CONSOLE_SUPPORT) || defined(DEBUG_SUPPORT) instead of && !defined(DEBUG_SUPPORT). A build with the debugger now prints what programs write to the debug console ports through this code too, before the debugger's soft commands are looked at, so whatever the soft commands setting; opening the debugger and clearing the console are still soft commands. --- core/Makefile | 4 ++-- core/mem.c | 2 +- tests/autotester/headless_protocol.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/Makefile b/core/Makefile index ed9b97f0..db21e6aa 100644 --- a/core/Makefile +++ b/core/Makefile @@ -10,8 +10,8 @@ CFLAGS = -Wall -Wextra -fPIC -O3 -std=gnu11 -static # Add debugging support, with zdis disassembler #CFLAGS += -DDEBUG_SUPPORT -# Without debugging support, print what programs write to the debug console (dbg_printf) -# through gui_console_printf / gui_console_err_printf (cemu-headless, the autotester) +# Print what programs write to the debug console (dbg_printf) through gui_console_printf / +# gui_console_err_printf without debugging support too (cemu-headless, the autotester) #CFLAGS += -DDEBUG_CONSOLE_SUPPORT # Add these flags if your compiler supports it diff --git a/core/mem.c b/core/mem.c index f6ad583f..c7361912 100644 --- a/core/mem.c +++ b/core/mem.c @@ -812,7 +812,7 @@ void mem_write_cpu(uint32_t addr, uint8_t value) { /* MMIO <-> Advanced Perphrial Bus */ case 0xE: case 0xF: -#if defined(DEBUG_CONSOLE_SUPPORT) && !defined(DEBUG_SUPPORT) +#if defined(DEBUG_CONSOLE_SUPPORT) || defined(DEBUG_SUPPORT) if (addr >= DBGOUT_PORT_RANGE && addr < DBGEXT_PORT) { static char console_buffer[2][SIZEOF_DBG_BUFFER]; static unsigned console_pos[2]; diff --git a/tests/autotester/headless_protocol.md b/tests/autotester/headless_protocol.md index 8d86264b..fe26e16e 100644 --- a/tests/autotester/headless_protocol.md +++ b/tests/autotester/headless_protocol.md @@ -53,7 +53,7 @@ Supported commands: - `help` lists commands. - `quit` exits cleanly. -If the core was built with `-DDEBUG_CONSOLE_SUPPORT` (for example `make +If the core was built with `-DDEBUG_CONSOLE_SUPPORT` or `-DDEBUG_SUPPORT` (for example `make CPPFLAGS=-DDEBUG_CONSOLE_SUPPORT` in `core/`), what the calculator's programs print to the debug console (the toolchain's `dbg_printf`) is written to standard error, as the core's diagnostics are.