1
0
mirror of https://github.com/rkujawa/rk65c02.git synced 2026-04-22 06:16:39 +00:00

Add host-control callbacks and JIT-safe polling.

Expose stop/tick/request-stop APIs, keep host control active at JIT block boundaries, and document the control model with a fuller host example plus dependency-aware builds.

Made-with: Cursor
This commit is contained in:
Radosław Kujawa
2026-03-06 19:58:55 +01:00
parent 813bc612f5
commit b980fdaae2
10 changed files with 402 additions and 17 deletions
+48 -3
View File
@@ -11,9 +11,7 @@ Currently, the following features are implemented:
- 16-bit address space.
- Minimal support for interrupts.
- JIT using GNU Lightning.
The following notable features are missing:
- Ability to execute callbacks in software utilizing this library.
- Host callbacks for stop notification and periodic execution ticks.
The only external dependencies (besides standard C library) are Boehm GC and
uthash. GNU Lightning is required for JIT support, but the library can be built
@@ -27,3 +25,50 @@ with std syntax) are also necessary.
[![Built by neckbeards](https://forthebadge.com/images/badges/built-by-neckbeards.svg)](https://forthebadge.com)
## Host control API
Typical host integration pattern is:
1. Create and configure `rk65c02emu_t`.
2. Register optional callbacks:
- `rk65c02_on_stop_set()` to be notified why execution stopped.
- `rk65c02_tick_set()` to periodically run host code while interpreter runs.
3. Start execution with `rk65c02_start()` (or bounded execution with `rk65c02_step()`).
4. Inspect or modify state (`e.regs`, bus reads/writes) and continue.
Host can request cooperative stop using:
- `rk65c02_request_stop(&e)` - asks emulator to stop at the next safe boundary.
Stop reason is reported as `HOST`.
Notes:
- Tick callback works in both execution modes:
- interpreter mode checks tick after each instruction;
- JIT mode checks tick at compiled block boundaries (coarser granularity).
- If precise per-instruction callback cadence is required, run without JIT.
- `on_stop` is called when execution stops from `rk65c02_start()` and
`rk65c02_step()` (for example: `STP`, `WAI`, `BREAKPOINT`, `HOST`,
`STEPPED`, `EMUERROR`).
- `rk65c02_stop_reason_string()` converts `emu_stop_reason_t` values to
readable strings for logs/UI.
## Examples
`examples/` contains small host programs using the library:
- `min3` - computes minimum of three values using a ROM routine.
- `mul_8bit_to_8bits` - multiplies two 8-bit values.
- `host_control` - demonstrates full host-control flow with:
- `rk65c02_on_stop_set()` callback.
- `rk65c02_tick_set()` callback.
- host-driven stop via `rk65c02_request_stop()`.
- continuing with `rk65c02_step()` after stop.
Build examples with:
```sh
make -C src
make -C examples
```
+8 -2
View File
@@ -1,6 +1,6 @@
UNAME_S := $(shell uname -s)
CFLAGS=-Wall -pedantic -I../src -g
CFLAGS=-Wall -pedantic -I../src -g -MMD -MP
LDFLAGS=-lgc -llightning
LDFLAGS_MACOSX=-L/opt/local/lib
@@ -15,7 +15,7 @@ RK6502LIB=../src/librk65c02.a
VASM=vasm6502_std
VASMFLAGS=-Fbin -wdc02
EXAMPLES=min3 mul_8bit_to_8bits
EXAMPLES=min3 mul_8bit_to_8bits host_control
EXAMPLES_ROMS:=$(addsuffix .rom,$(basename $(wildcard *.s)))
all : $(EXAMPLES) $(EXAMPLES_ROMS)
@@ -26,6 +26,9 @@ min3 : min3.o $(RK6502LIB)
mul_8bit_to_8bits : mul_8bit_to_8bits.o $(RK6502LIB)
$(CC) -o $@ $(LDFLAGS) $< $(RK6502LIB)
host_control : host_control.o $(RK6502LIB)
$(CC) -o $@ $(LDFLAGS) $< $(RK6502LIB)
%.rom : %.s
$(VASM) $(VASMFLAGS) -o $@ $<
@@ -33,5 +36,8 @@ mul_8bit_to_8bits : mul_8bit_to_8bits.o $(RK6502LIB)
$(CC) $(CFLAGS) -c $<
clean :
rm -f *.o
rm -f *.d
rm -f $(EXAMPLES) $(EXAMPLES_ROMS)
-include $(wildcard *.d)
+82
View File
@@ -0,0 +1,82 @@
#include <inttypes.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "bus.h"
#include "rk65c02.h"
static const uint16_t load_addr = 0xC000;
static const uint16_t counter_addr = 0x0200;
struct host_state {
uint32_t ticks_seen;
uint32_t tick_budget;
bool stop_notified;
};
static void
on_stop(rk65c02emu_t *e, emu_stop_reason_t reason, void *ctx)
{
struct host_state *hs = ctx;
hs->stop_notified = true;
printf("Emulation stopped: %s (PC=%#04x)\n",
rk65c02_stop_reason_string(reason),
e->regs.PC);
}
static void
on_tick(rk65c02emu_t *e, void *ctx)
{
struct host_state *hs = ctx;
hs->ticks_seen++;
if (hs->ticks_seen >= hs->tick_budget)
rk65c02_request_stop(e);
}
int
main(void)
{
struct host_state host = {
.ticks_seen = 0,
.tick_budget = 20000,
.stop_notified = false,
};
rk65c02emu_t e;
uint8_t counter_before;
uint8_t counter_after;
e = rk65c02_load_rom("host_control.rom", load_addr, NULL);
e.regs.SP = 0xFF;
e.regs.PC = load_addr;
/*
* Tick runs every 100 poll points and asks the core to stop after
* tick_budget callbacks. In JIT mode poll points are block boundaries,
* so cadence is coarser than per-instruction interpreter ticking.
*/
rk65c02_on_stop_set(&e, on_stop, &host);
rk65c02_tick_set(&e, on_tick, 100, &host);
rk65c02_jit_enable(&e, true);
counter_before = bus_read_1(e.bus, counter_addr);
rk65c02_start(&e);
counter_after = bus_read_1(e.bus, counter_addr);
printf("Tick callbacks: %" PRIu32 "\n", host.ticks_seen);
printf("Counter at $%04x before=%u after=%u\n", counter_addr,
counter_before, counter_after);
printf("on_stop callback invoked: %s\n",
host.stop_notified ? "yes" : "no");
/* Continue manually with stepping after host-initiated stop. */
rk65c02_tick_clear(&e);
host.stop_notified = false;
rk65c02_step(&e, 5);
printf("After 5-step run, stop reason is %s\n",
rk65c02_stop_reason_string(e.stopreason));
return 0;
}
+5
View File
@@ -0,0 +1,5 @@
.org 0xC000
start:
inc 0x0200
bra start
+5 -1
View File
@@ -14,7 +14,8 @@ LIB_STATIC=librk65c02.a
LDFLAGS=-shared -lgc -llightning
LDFLAGS_MACOSX=-L/opt/local/lib
CFLAGS=-Wall -fpic -ggdb -Og -DHAVE_LIGHTNING
#CFLAGS=-Wall -fpic -ggdb -Og -DHAVE_LIGHTNING -MMD -MP
CFLAGS=-Wall -fpic -O3 -DHAVE_LIGHTNING -MMD -MP
#CFLAGS=-Wall -fpic -Os -fplugin=annobin
CFLAGS_MACOSX=-I/opt/local/include/uthash -I/opt/local/include
@@ -50,6 +51,9 @@ $(EMULATION).h : $(65C02ISA).csv $(EMULATION).awk
clean :
rm -f $(65C02ISA).h $(EMULATION).h
rm -f *.d
rm -f $(LIB_OBJS) #$(CLI_OBJS)
rm -f $(LIB_SO) $(LIB_STATIC) #$(CLI)
-include $(wildcard *.d)
+11 -1
View File
@@ -1353,8 +1353,13 @@ rk65c02_run_jit(rk65c02emu_t *e)
|| e->trace || e->runtime_disassembly
|| (e->bps_head != NULL)) {
e->state = RUNNING;
while (e->state == RUNNING)
while (e->state == RUNNING) {
rk65c02_poll_host_controls(e);
if (e->state != RUNNING)
break;
rk65c02_exec(e);
rk65c02_poll_host_controls(e);
}
return;
}
@@ -1364,6 +1369,10 @@ rk65c02_run_jit(rk65c02emu_t *e)
struct rk65c02_jit_block *b;
uint16_t pc;
rk65c02_poll_host_controls(e);
if (e->state != RUNNING)
break;
/*
* Honour any runtime changes in debugging state by
* bailing out to the interpreter if needed.
@@ -1383,6 +1392,7 @@ rk65c02_run_jit(rk65c02emu_t *e)
}
b->fn(e);
rk65c02_poll_host_controls(e);
}
}
+1
View File
@@ -15,6 +15,7 @@
*/
void rk65c02_run_jit(rk65c02emu_t *e);
void rk65c02_poll_host_controls(rk65c02emu_t *e);
#ifdef HAVE_LIGHTNING
/* BCD ADC/SBC helpers: JIT calls these when P_DECIMAL is set. */
+174 -7
View File
@@ -37,6 +37,87 @@
void rk65c02_exec(rk65c02emu_t *);
static void
rk65c02_maybe_call_on_stop(rk65c02emu_t *e)
{
if (e->on_stop != NULL)
e->on_stop(e, e->stopreason, e->on_stop_ctx);
}
static void
rk65c02_apply_host_stop_request(rk65c02emu_t *e)
{
if (!(e->stop_requested))
return;
e->state = STOPPED;
e->stopreason = HOST;
e->stop_requested = false;
}
static void
rk65c02_maybe_tick(rk65c02emu_t *e)
{
if (e->tick == NULL)
return;
if ((e->state != RUNNING) && (e->state != STEPPING))
return;
if (e->tick_interval == 0) {
e->tick(e, e->tick_ctx);
return;
}
if (e->tick_countdown > 0)
e->tick_countdown--;
if (e->tick_countdown == 0) {
e->tick(e, e->tick_ctx);
e->tick_countdown = e->tick_interval;
}
}
static bool
rk65c02_can_use_jit(const rk65c02emu_t *e)
{
return e->use_jit && e->jit != NULL && !(e->trace)
&& !(e->runtime_disassembly) && (e->bps_head == NULL);
}
void
rk65c02_poll_host_controls(rk65c02emu_t *e)
{
assert(e != NULL);
rk65c02_apply_host_stop_request(e);
rk65c02_maybe_tick(e);
rk65c02_apply_host_stop_request(e);
}
const char *
rk65c02_stop_reason_string(emu_stop_reason_t reason)
{
switch (reason) {
case STP:
return "STP";
case WAI:
return "WAI";
case BREAKPOINT:
return "BREAKPOINT";
case WATCHPOINT:
return "WATCHPOINT";
case STEPPED:
return "STEPPED";
case HOST:
return "HOST";
case EMUERROR:
return "EMUERROR";
default:
return "UNKNOWN";
}
}
rk65c02emu_t
rk65c02_load_rom(const char *path, uint16_t load_addr, bus_t *b)
{
@@ -77,6 +158,13 @@ rk65c02_init(bus_t *b)
e.use_jit = false;
e.jit = NULL;
e.stop_requested = false;
e.on_stop = NULL;
e.on_stop_ctx = NULL;
e.tick = NULL;
e.tick_ctx = NULL;
e.tick_interval = 0;
e.tick_countdown = 0;
rk65c02_log(LOG_DEBUG, "Initialized new emulator.");
@@ -180,14 +268,26 @@ rk65c02_start(rk65c02emu_t *e) {
* and no debugging features that rely on per-instruction interpreter
* state are active. Otherwise fall back to the interpreter loop.
*/
if (e->use_jit && e->jit != NULL && !(e->trace) && !(e->runtime_disassembly)
&& (e->bps_head == NULL))
e->stop_requested = false;
e->tick_countdown = e->tick_interval;
if (rk65c02_can_use_jit(e))
rk65c02_run_jit(e);
else {
e->state = RUNNING;
while (e->state == RUNNING)
while (e->state == RUNNING) {
rk65c02_poll_host_controls(e);
if (e->state != RUNNING)
break;
rk65c02_exec(e);
rk65c02_poll_host_controls(e);
}
}
rk65c02_poll_host_controls(e);
if (e->state == STOPPED)
rk65c02_maybe_call_on_stop(e);
}
void
@@ -197,14 +297,76 @@ rk65c02_step(rk65c02emu_t *e, uint16_t steps) {
assert(e != NULL);
e->stop_requested = false;
e->tick_countdown = e->tick_interval;
e->state = STEPPING;
while ((e->state == STEPPING) && (i < steps)) {
rk65c02_poll_host_controls(e);
if (e->state != STEPPING)
break;
rk65c02_exec(e);
rk65c02_poll_host_controls(e);
i++;
}
e->state = STOPPED;
e->stopreason = STEPPED;
rk65c02_poll_host_controls(e);
if (e->state == STEPPING) {
e->state = STOPPED;
e->stopreason = STEPPED;
}
if (e->state == STOPPED)
rk65c02_maybe_call_on_stop(e);
}
void
rk65c02_on_stop_set(rk65c02emu_t *e, rk65c02_on_stop_cb_t cb, void *ctx)
{
assert(e != NULL);
e->on_stop = cb;
e->on_stop_ctx = ctx;
}
void
rk65c02_on_stop_clear(rk65c02emu_t *e)
{
assert(e != NULL);
e->on_stop = NULL;
e->on_stop_ctx = NULL;
}
void
rk65c02_tick_set(rk65c02emu_t *e, rk65c02_tick_cb_t cb, uint32_t interval,
void *ctx)
{
assert(e != NULL);
e->tick = cb;
e->tick_ctx = ctx;
e->tick_interval = interval;
e->tick_countdown = interval;
}
void
rk65c02_tick_clear(rk65c02emu_t *e)
{
assert(e != NULL);
e->tick = NULL;
e->tick_ctx = NULL;
e->tick_interval = 0;
e->tick_countdown = 0;
}
void
rk65c02_request_stop(rk65c02emu_t *e)
{
assert(e != NULL);
e->stop_requested = true;
}
void
@@ -268,14 +430,19 @@ void
rk65c02_panic(rk65c02emu_t *e, const char* fmt, ...)
{
va_list args;
bool was_active;
assert(e != NULL);
va_start(args, fmt);
rk65c02_logv(LOG_CRIT, fmt, args);
va_end(args);
was_active = ((e->state == RUNNING) || (e->state == STEPPING));
e->state = STOPPED;
e->stopreason = EMUERROR;
/* TODO: run some UI callback. */
e->stop_requested = false;
if (!was_active)
rk65c02_maybe_call_on_stop(e);
}
+63 -2
View File
@@ -7,6 +7,8 @@
#include "bus.h"
struct rk65c02_jit;
struct rk65c02emu;
typedef struct rk65c02emu rk65c02emu_t;
/**
* @brief State of the emulator.
@@ -30,6 +32,17 @@ typedef enum {
EMUERROR /**< Due to emulator error. */
} emu_stop_reason_t;
/**
* @brief Callback executed when emulation stops.
*/
typedef void (*rk65c02_on_stop_cb_t)(rk65c02emu_t *e, emu_stop_reason_t reason,
void *ctx);
/**
* @brief Callback executed periodically while emulation is running.
*/
typedef void (*rk65c02_tick_cb_t)(rk65c02emu_t *e, void *ctx);
/**
* @brief State of the emulated CPU registers.
*/
@@ -91,10 +104,15 @@ struct rk65c02emu {
trace_t *trace_head; /**< Pointer to linked list with trace log. */
bool use_jit; /**< Enable/disable JIT execution. */
struct rk65c02_jit *jit; /**< Opaque JIT backend state. */
bool stop_requested; /**< Host requested stop at next safe boundary. */
rk65c02_on_stop_cb_t on_stop; /**< Callback executed after stop. */
void *on_stop_ctx; /**< Host context for on_stop callback. */
rk65c02_tick_cb_t tick; /**< Interpreter tick callback. */
void *tick_ctx; /**< Host context for tick callback. */
uint32_t tick_interval; /**< Tick period in instructions (0 = every insn). */
uint32_t tick_countdown; /**< Internal countdown used by tick callback. */
};
typedef struct rk65c02emu rk65c02emu_t;
/**
* @brief Initialize the new emulator instance. Set initial CPU state.
* @param b Bus description.
@@ -115,6 +133,49 @@ void rk65c02_start(rk65c02emu_t *e);
*/
void rk65c02_step(rk65c02emu_t *e, uint16_t steps);
/**
* @brief Convert stop reason enum value to a static string.
* @param reason Stop reason value.
* @return Human-readable reason name.
*/
const char *rk65c02_stop_reason_string(emu_stop_reason_t reason);
/**
* @brief Register callback executed whenever emulation stops.
* @param e Emulator instance.
* @param cb Callback function.
* @param ctx Opaque host context passed back to callback.
*/
void rk65c02_on_stop_set(rk65c02emu_t *e, rk65c02_on_stop_cb_t cb, void *ctx);
/**
* @brief Unregister on-stop callback.
* @param e Emulator instance.
*/
void rk65c02_on_stop_clear(rk65c02emu_t *e);
/**
* @brief Register periodic interpreter tick callback.
* @param e Emulator instance.
* @param cb Callback function.
* @param interval Number of instructions between callbacks (0 = every insn).
* @param ctx Opaque host context passed back to callback.
*/
void rk65c02_tick_set(rk65c02emu_t *e, rk65c02_tick_cb_t cb,
uint32_t interval, void *ctx);
/**
* @brief Unregister tick callback.
* @param e Emulator instance.
*/
void rk65c02_tick_clear(rk65c02emu_t *e);
/**
* @brief Request stop at the next safe execution boundary.
* @param e Emulator instance.
*/
void rk65c02_request_stop(rk65c02emu_t *e);
char *rk65c02_regs_string_get(reg_state_t);
void rk65c02_dump_regs(reg_state_t);
void rk65c02_dump_stack(rk65c02emu_t *, uint8_t);
+5 -1
View File
@@ -1,6 +1,7 @@
UNAME_S := $(shell uname -s)
CFLAGS=-Wall -I../src -ggdb -Og
CFLAGS=-Wall -I../src -O3 -MMD -MP
#CFLAGS=-Wall -I../src -ggdb -Og -MMD -MP
LDFLAGS=-latf-c -lgc -llightning
LDFLAGS_MACOSX=-L/opt/local/lib
@@ -54,6 +55,9 @@ test_device_serial: test_device_serial.o $(UTILS) $(RK6502LIB)
clean :
rm -f *.o
rm -f *.d
rm -f $(TESTS) $(BENCH)
rm -f $(TESTROMS)
-include $(wildcard *.d)