/* control_backend.c — Commander X16 backend for the Retro Remote Debug * Controller (retro_control.h, from extern/retro-remote-debug-controller): * the control port a test harness drives, -controlport N. * * Contract 0.6: everything the plain x16emu fork's backend serves (memory, * registers, the screen, stepping, keys, reset, audio, the pointer, pads), * and GET/POST /perf, answered from the same measurements as the Performance * panel and the DAP server's x16/perfStats (perf_json.c). /perf needs cJSON, * as the DAP server does: without it the backend serves 0.5. */ #include "retro_control.h" #include "glue.h" /* regs, BRAM, num_ram_banks, machine_reset */ #include "memory.h" /* debug_read6502, write6502, memory_get_ram_bank/rom_bank */ #include "video.h" /* video_get_framebuffer, video_get_frame_count, video_space_read */ #include "keyboard.h" /* handle_keyboard */ #include "i2c.h" /* mouse_move / mouse_button_* / mouse_send_state */ #include "audio.h" /* AUDIO_SAMPLERATE */ #include "joystick.h" /* joystick_set_virtual / joystick_clear_virtual */ #include #include #include #include #include extern uint32_t host_sample_rate; /* audio.c: the real (post-SDL) output rate */ /* Debug-read into out. bank<0 => current bank. $0000-$9FFF is low RAM * (bank-independent); $A000-$BFFF is banked RAM; $C000+ is ROM. */ static uint32_t x16_read_mem(uint32_t addr, int32_t bank, uint32_t len, uint8_t *out, uint32_t cap) { int16_t x16bank = (bank >= 0) ? (int16_t)bank : USE_CURRENT_X16_BANK; uint32_t n = 0; for (uint32_t i = 0; i < len && n < cap; i++) { uint16_t a = (uint16_t)(addr + i); out[n++] = debug_read6502(a, 0, x16bank); } return n; } /* Write (poke) len bytes at addr (0.3). Banked RAM with an explicit bank= goes * straight into that bank's backing store, the mirror of x16_read_mem: routed * through write6502 it would land in whatever bank the CPU had mapped at the * pause (the KERNAL's IRQ handler maps bank 0). Without bank= the poke hits * the current bank, as a CPU write would. */ static uint32_t x16_write_mem(uint32_t addr, int32_t bank, uint32_t len, const uint8_t *in) { for (uint32_t i = 0; i < len; i++) { uint16_t a = (uint16_t)(addr + i); if (bank >= 0 && a >= 0xa000 && a < 0xc000) { if ((uint32_t)bank < (uint32_t)num_ram_banks) { BRAM[((uint32_t)bank << 13) + a - 0xa000] = in[i]; } continue; } write6502(a, 0, in[i]); } return len; } /* --- Audio drain (0.3): a ring teed from the mixer. Producer (audio_render, * via x16_control_push_audio) and consumer both run on the emulator thread. */ #define X16_AUD_RING 65536 /* int16 samples (~0.67 s stereo @ 48.8 kHz) */ static int16_t g_aud[X16_AUD_RING]; static uint32_t g_aud_head, g_aud_count, g_aud_dropped; static int g_aud_active; /* capture only once the server is up */ void x16_control_push_audio(int16_t l, int16_t r) { if (!g_aud_active) return; if (g_aud_count + 2 > X16_AUD_RING) { g_aud_dropped += 2; return; } g_aud[(g_aud_head + g_aud_count) % X16_AUD_RING] = l; g_aud_count++; g_aud[(g_aud_head + g_aud_count) % X16_AUD_RING] = r; g_aud_count++; } static uint32_t x16_capture_audio(int16_t *out, uint32_t cap, int *rate, int *channels, uint32_t *dropped) { uint32_t n = 0; if (rate) *rate = host_sample_rate ? (int)host_sample_rate : (int)AUDIO_SAMPLERATE; if (channels) *channels = 2; if (dropped) *dropped = g_aud_dropped; g_aud_dropped = 0; while (n < cap && g_aud_count) { out[n++] = g_aud[g_aud_head]; g_aud_head = (g_aud_head + 1) % X16_AUD_RING; g_aud_count--; } return n; } static void x16_get_regs_json(char *buf, size_t cap) { snprintf(buf, cap, "{\"a\":%u,\"x\":%u,\"y\":%u,\"sp\":%u,\"pc\":%u,\"status\":%u," "\"ram_bank\":%u,\"rom_bank\":%u}", (unsigned)regs.a, (unsigned)regs.xl, (unsigned)regs.yl, (unsigned)(regs.sp & 0xff), (unsigned)regs.pc, (unsigned)regs.status, (unsigned)memory_get_ram_bank(), (unsigned)memory_get_rom_bank()); } static void x16_get_framebuffer(retro_framebuffer_t *out) { int w = 0, h = 0; out->pixels = video_get_framebuffer(&w, &h); out->width = w; out->height = h; out->fmt = RETRO_PIX_BGRA8888; /* x16 framebuffer byte order */ } static uint64_t x16_get_frame_count(void) { return video_get_frame_count(); } /* Keys (0.2). text => the char's key (letters use the lowercase keycode); * code => a raw SDL scancode. */ static void x16_key_event(SDL_Keycode sym, SDL_Scancode sc, int action) { if (action == RETRO_KEY_DOWN) { handle_keyboard(true, sym, sc); } else if (action == RETRO_KEY_UP) { handle_keyboard(false, sym, sc); } else { handle_keyboard(true, sym, sc); handle_keyboard(false, sym, sc); } } static int x16_inject_key(int is_text, uint32_t value, int action) { SDL_Keycode sym; SDL_Scancode sc; if (is_text) { int c = (int)value; if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a'; sym = (SDL_Keycode)c; sc = SDL_GetScancodeFromKey(sym); } else { sc = (SDL_Scancode)value; sym = SDL_GetKeyFromScancode(sc); } if (sc == SDL_SCANCODE_UNKNOWN) { return 0; } x16_key_event(sym, sc, action); return 1; } static void x16_reset(void) { machine_reset(); } /* Pointer (0.4). The X16 mouse is a relative PS/2 device the KERNAL integrates * into sprite 0's position, so inject through mouse_move/mouse_send_state and * read the position back from sprite 0's attributes. */ #define VERA_SPRITE0_ATTR 0x1FC00U static int g_ptr_buttons; /* what this backend last pressed */ static int32_t x16_pointer_x(void) { return (int32_t)video_space_read(VERA_SPRITE0_ATTR + 2) | ((int32_t)(video_space_read(VERA_SPRITE0_ATTR + 3) & 3) << 8); } static int32_t x16_pointer_y(void) { return (int32_t)video_space_read(VERA_SPRITE0_ATTR + 4) | ((int32_t)(video_space_read(VERA_SPRITE0_ATTR + 5) & 3) << 8); } static int x16_set_pointer(int absolute, int32_t x, int32_t y, int buttons) { if (absolute) { mouse_move((int)(x - x16_pointer_x()), (int)(y - x16_pointer_y())); } else { mouse_move((int)x, (int)y); } if (buttons >= 0) { if (buttons & 1) mouse_button_down(0); else mouse_button_up(0); if (buttons & 2) mouse_button_down(1); else mouse_button_up(1); g_ptr_buttons = buttons & 3; } mouse_send_state(); return 1; } static int x16_get_pointer(int32_t *x, int32_t *y, int *buttons) { *x = x16_pointer_x(); *y = x16_pointer_y(); *buttons = g_ptr_buttons; return 1; } /* --- Pads (0.5) --------------------------------------------------------- * RRDC's canonical layout (bit 0 LEFT, 1 RIGHT, 2 UP, 3 DOWN, 4 A, 5 B, 6 X, * 7 Y, 8 START, 9 SELECT, 10 L, 11 R) to the X16's SNES serial order, active * low: bit 0 B, 1 Y, 2 SELECT, 3 START, 4 UP, 5 DOWN, 6 LEFT, 7 RIGHT, 8 A, * 9 X, 10 L, 11 R. joystick.h's JOY_BIT_* names those bits after SDL's * buttons -- its JOY_BIT_A is SDL's bottom button, SNES B -- so read the * table below against the SNES names, not those. * * Driven through the emulator's own virtual joysticks (joystick_set_virtual, * as the DAP server's x16/joystick is), which the latch reads exactly as it * reads a real pad. */ static const uint8_t pad_canon_to_x16[12] = { 6, /* LEFT */ 7, /* RIGHT */ 4, /* UP */ 5, /* DOWN */ 8, /* A */ 0, /* B */ 9, /* X */ 1, /* Y */ 3, /* START */ 2, /* SELECT */ 10, /* L */ 11, /* R */ }; static uint16_t pad_canon_to_mask(int buttons) { uint16_t mask = 0xffff; for (int b = 0; b < 12; ++b) { if (buttons & (1 << b)) { mask &= (uint16_t)~(1u << pad_canon_to_x16[b]); } } return mask; } static int pad_mask_to_canon(uint16_t mask) { int buttons = 0; for (int b = 0; b < 12; ++b) { if ((mask & (1u << pad_canon_to_x16[b])) == 0) { buttons |= (1 << b); } } return buttons; } static int x16_set_pad(int index, int buttons, int connected) { if (index < 0 || index >= NUM_JOYSTICKS) { return 0; } if (connected == 0) { joystick_clear_virtual(index); return 1; } if (buttons >= 0) { joystick_set_virtual(index, pad_canon_to_mask(buttons)); } else if (connected == 1 && !joystick_virtual_active(index)) { joystick_set_virtual(index, 0xffff); /* present, nothing held */ } return 1; } static int x16_get_pad(int index, int *buttons, int *connected) { if (index < 0 || index >= NUM_JOYSTICKS) { return 0; } joystick_debug_state_t st; joystick_debug_get_state(&st); const joystick_slot_debug_t *s = &st.slots[index]; *connected = (s->virtual_active || s->controller_bound) ? 1 : 0; *buttons = pad_mask_to_canon(*connected ? s->button_mask : 0xffff); return 1; } /* --- Performance metrics (0.6) ------------------------------------------- */ #ifdef HAS_DAP #include "perf_json.h" #include "perf_budget.h" #include "vera_bandwidth.h" #include "sprite_trace.h" /* A query string's value for `key`, or NULL. */ static const char * q_find(const char *q, const char *key, size_t *len) { const size_t kl = strlen(key); for (const char *p = q; p && *p; ) { if (!strncmp(p, key, kl) && p[kl] == '=') { const char *v = p + kl + 1; const char *e = strchr(v, '&'); *len = e ? (size_t)(e - v) : strlen(v); return v; } p = strchr(p, '&'); if (p) p++; } return NULL; } static long q_long(const char *q, const char *key, long def) { size_t n; const char *v = q_find(q, key, &n); return v ? strtol(v, NULL, 0) : def; } /* Measuring is the remote owner's to switch: a client asking for numbers is a * client that wants them collected (the SPEC lets a backend start on the * first request), and the panel or a DAP client closing does not stop it. */ static void perf_arm_remote(void) { if (!perf_budget_is_enabled()) { perf_budget_arm(PERF_OWNER_REMOTE, true); } if (!vera_bandwidth_is_enabled()) { vera_bandwidth_arm(VERA_BW_OWNER_REMOTE, true); } } static cJSON * perf_frame_record(const perf_frame_t *f) { cJSON *o = perf_json_frame(f); cJSON *z = cJSON_CreateArray(); const int nz = perf_budget_zone_count(); for (int i = 0; i < nz && i < PERF_RING_ZONES; i++) { cJSON_AddItemToArray(z, cJSON_CreateNumber(f->zone[i])); } cJSON_AddItemToObject(o, "zones", z); return o; } static cJSON * perf_zones_json(const float *secs, int nsecs) { cJSON *zones = cJSON_CreateArray(); perf_frame_t last; const bool have_last = perf_budget_last_frame(&last); const int nz = perf_budget_zone_count(); for (int i = 0; i < nz; i++) { perf_zone_t z; if (!perf_budget_zone_get(i, &z)) continue; cJSON *o = cJSON_CreateObject(); cJSON_AddNumberToObject(o, "id", i); cJSON_AddStringToObject(o, "name", z.name); cJSON_AddStringToObject(o, "kind", z.is_marker ? "marker" : "address"); if (z.is_marker) { cJSON_AddNumberToObject(o, "markerId", z.marker_id); } else { cJSON_AddNumberToObject(o, "start", z.start); cJSON_AddNumberToObject(o, "end", z.end); cJSON_AddNumberToObject(o, "bank", z.bank); } cJSON_AddBoolToObject(o, "idle", z.is_idle); cJSON_AddNumberToObject(o, "current", (have_last && i < PERF_RING_ZONES) ? last.zone[i] : 0); cJSON *w = cJSON_CreateObject(); for (int k = 0; i < PERF_RING_ZONES && k < nsecs; k++) { /* history: the first 8 only */ perf_stat_t st; if (!perf_budget_zone_window(i, secs[k], &st)) continue; char name[32]; perf_json_window_name(secs[k], name, sizeof name); cJSON_AddItemToObject(w, name, perf_json_stat(&st)); } cJSON_AddItemToObject(o, "windows", w); cJSON_AddItemToArray(zones, o); } return zones; } /* The GET /perf body. settings_only: POST /perf's answer, the settings as they * stand (no windows, zones or frames). */ static char * perf_body(const char *query, bool settings_only) { cJSON *body = cJSON_CreateObject(); cJSON_AddBoolToObject(body, "enabled", perf_budget_is_enabled()); cJSON_AddItemToObject(body, "budget", perf_json_budget()); perf_frame_t last; if (!perf_budget_last_frame(&last)) { memset(&last, 0, sizeof last); } cJSON_AddItemToObject(body, "current", perf_json_frame(&last)); const bool lines = q_long(query, "lines", 0) != 0; if (!settings_only) { float secs[16]; int nsecs = 0; size_t n; const char *v = q_find(query, "windows", &n); if (v) { char buf[256]; if (n >= sizeof buf) n = sizeof buf - 1; memcpy(buf, v, n); buf[n] = 0; for (char *tok = strtok(buf, ","); tok && nsecs < 16; tok = strtok(NULL, ",")) { cJSON *item = cJSON_CreateString(tok); secs[nsecs++] = perf_json_window_seconds(item); cJSON_Delete(item); } } else { static const float defaults[] = {1.0f, 10.0f, 30.0f, 60.0f, 0.0f}; for (; nsecs < 5; nsecs++) secs[nsecs] = defaults[nsecs]; } cJSON *windows = cJSON_CreateArray(); for (int i = 0; i < nsecs; i++) { cJSON *w = perf_json_window(secs[i]); if (w) cJSON_AddItemToArray(windows, w); } cJSON_AddItemToObject(body, "windows", windows); if (q_long(query, "zones", 1) != 0) { cJSON_AddItemToObject(body, "zones", perf_zones_json(secs, nsecs)); } /* frames=n: the last n; since=f: every frame after f still held (the * newest `frames` of them when both are given). Oldest first. */ const long want = q_long(query, "frames", 0); const long since = q_long(query, "since", -1); if (want > 0 || since >= 0) { const int have = perf_budget_frame_count(); int take = 0; for (; take < have; take++) { perf_frame_t f; if (!perf_budget_frame_at(take, &f)) break; if (since >= 0 && (long)f.frame <= since) break; if (want > 0 && take >= want) break; } cJSON *frames = cJSON_CreateArray(); for (int i = take - 1; i >= 0; i--) { perf_frame_t f; if (perf_budget_frame_at(i, &f)) { cJSON_AddItemToArray(frames, perf_frame_record(&f)); } } cJSON_AddItemToObject(body, "frames", frames); } } cJSON *platform = cJSON_CreateObject(); cJSON *bw = perf_json_bandwidth(); if (lines) { cJSON_AddItemToObject(bw, "lines", perf_json_bandwidth_lines()); } cJSON_AddItemToObject(platform, "bandwidth", bw); cJSON_AddItemToObject(platform, "sprites", perf_json_sprites(lines)); cJSON_AddItemToObject(body, "platform", platform); char *out = cJSON_PrintUnformatted(body); cJSON_Delete(body); return out; /* cJSON's allocator is malloc: the server free()s it */ } static char * x16_get_perf(const char *query) { perf_arm_remote(); return perf_body(query ? query : "", false); } static char * perf_error(int *status, const char *msg) { *status = 400; char *b = (char *)malloc(strlen(msg) + 16); if (b) sprintf(b, "{\"error\":\"%s\"}", msg); return b; } static char * x16_set_perf(const char *text, int *status) { cJSON *args = cJSON_Parse(text); if (!cJSON_IsObject(args)) { cJSON_Delete(args); return perf_error(status, "body is not a JSON object"); } /* Check every field's type before applying any, so a bad body changes * nothing. */ cJSON *en = cJSON_GetObjectItemCaseSensitive(args, "enabled"); cJSON *fps = cJSON_GetObjectItemCaseSensitive(args, "targetFps"); cJSON *idle = cJSON_GetObjectItemCaseSensitive(args, "idleMode"); cJSON *hist = cJSON_GetObjectItemCaseSensitive(args, "historyFrames"); cJSON *zones = cJSON_GetObjectItemCaseSensitive(args, "zones"); cJSON *rst = cJSON_GetObjectItemCaseSensitive(args, "reset"); cJSON *spr = cJSON_GetObjectItemCaseSensitive(args, "sprites"); const char *bad = (en && !cJSON_IsBool(en)) ? "enabled is not a boolean" : (fps && !(cJSON_IsNumber(fps) && fps->valuedouble > 0)) ? "targetFps is not a positive number" : (idle && !(cJSON_IsString(idle) && (!strcmp(idle->valuestring, "auto") || !strcmp(idle->valuestring, "markers") || !strcmp(idle->valuestring, "none")))) ? "idleMode is not auto, markers or none" : (hist && !(cJSON_IsNumber(hist) && hist->valuedouble >= 2)) ? "historyFrames is not a number of 2 or more" : (zones && !cJSON_IsArray(zones)) ? "zones is not an array" : (rst && !cJSON_IsBool(rst)) ? "reset is not a boolean" : (spr && !cJSON_IsBool(spr)) ? "sprites is not a boolean" : NULL; if (bad) { cJSON_Delete(args); return perf_error(status, bad); } if (en) { const bool want = cJSON_IsTrue(en); perf_budget_arm(PERF_OWNER_REMOTE, want); vera_bandwidth_arm(VERA_BW_OWNER_REMOTE, want); } if (fps) perf_budget_set_target_fps((float)fps->valuedouble); if (idle) { perf_budget_set_idle_mode(!strcmp(idle->valuestring, "markers") ? PERF_IDLE_MARKERS : !strcmp(idle->valuestring, "none") ? PERF_IDLE_NONE : PERF_IDLE_AUTO); } if (hist) perf_budget_set_capacity((int)hist->valuedouble); if (zones) { /* Replaces the list: address zones and marker names alike. */ perf_budget_zone_clear(); cJSON *z; cJSON_ArrayForEach(z, zones) { if (!cJSON_IsObject(z)) continue; cJSON *jn = cJSON_GetObjectItemCaseSensitive(z, "name"); const char *name = cJSON_IsString(jn) ? jn->valuestring : "zone"; cJSON *jm = cJSON_GetObjectItemCaseSensitive(z, "marker"); if (cJSON_IsNumber(jm)) { perf_budget_zone_add_marker(name, (uint8_t)jm->valuedouble); continue; } cJSON *js = cJSON_GetObjectItemCaseSensitive(z, "start"); cJSON *je = cJSON_GetObjectItemCaseSensitive(z, "end"); cJSON *jb = cJSON_GetObjectItemCaseSensitive(z, "bank"); cJSON *ji = cJSON_GetObjectItemCaseSensitive(z, "idle"); if (!cJSON_IsNumber(js) || !cJSON_IsNumber(je)) continue; perf_budget_zone_add(name, (uint16_t)js->valuedouble, (uint16_t)je->valuedouble, cJSON_IsNumber(jb) ? (int16_t)jb->valuedouble : (int16_t)-1, cJSON_IsTrue(ji)); } } if (spr) sprite_trace_set_enabled(cJSON_IsTrue(spr)); if (rst && cJSON_IsTrue(rst)) { perf_budget_reset(); vera_bandwidth_reset(); if (sprite_trace_active) sprite_trace_reset(); } cJSON_Delete(args); *status = 200; return perf_body("", true); } #endif /* HAS_DAP */ static const retro_control_backend_t x16_backend = { .platform = "x16", .emulator = "x16emu-enhanced", .read_mem = x16_read_mem, .get_regs_json = x16_get_regs_json, .get_framebuffer = x16_get_framebuffer, .get_frame_count = x16_get_frame_count, .inject_key = x16_inject_key, .reset = x16_reset, .write_mem = x16_write_mem, .capture_audio = x16_capture_audio, .set_pointer = x16_set_pointer, .get_pointer = x16_get_pointer, .set_pad = x16_set_pad, .get_pad = x16_get_pad, #ifdef HAS_DAP .get_perf = x16_get_perf, .set_perf = x16_set_perf, #endif }; bool control_port_active = false; int x16_control_start(int port) { g_aud_active = 1; const int rc = retro_control_start(port, &x16_backend); control_port_active = (rc == 0); return rc; }