feat: the RRDC control port (-controlport), contract 0.6: GET/POST /perf from the Performance panel's measurements, built by perf_json.c with DAP's x16/perfStats; a guest marker region named in advance; a framed guest's regions carried across the frame boundary and an interrupt after $F1 counted as work (#1) 2980b7b6d7cd
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-10 20:57 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
2980b7b6d7cdf3928225cc12b965cc5c497b02b98a2ab5cf271549a7dfbe0c1e- Depends on
- 9120e87480a6
- Depended on by
- nothing yet
- In bookmarks
- main rrdc-perf
- Edits
- 55 atoms, 5 tree operations, 17 files
Browse the tree at this change
added .oxymodules +4 −0
| 1 | + | [src/extern/retro-remote-debug-controller] |
|
| 2 | + | url = https://oxygenhub.io/doomsdayone/retro-remote-debug-controller |
|
| 3 | + | bookmark = perf-6 |
|
| 4 | + |
added src/control_backend.c +589 −0
| 1 | + | /* control_backend.c — Commander X16 backend for the Retro Remote Debug |
|
| 2 | + | * Controller (retro_control.h, from extern/retro-remote-debug-controller): |
|
| 3 | + | * the control port a test harness drives, -controlport N. |
|
| 4 | + | * |
|
| 5 | + | * Contract 0.6: everything the plain x16emu fork's backend serves (memory, |
|
| 6 | + | * registers, the screen, stepping, keys, reset, audio, the pointer, pads), |
|
| 7 | + | * and GET/POST /perf, answered from the same measurements as the Performance |
|
| 8 | + | * panel and the DAP server's x16/perfStats (perf_json.c). /perf needs cJSON, |
|
| 9 | + | * as the DAP server does: without it the backend serves 0.5. */ |
|
| 10 | + | ||
| 11 | + | #include "retro_control.h" |
|
| 12 | + | ||
| 13 | + | #include "glue.h" /* regs, BRAM, num_ram_banks, machine_reset */ |
|
| 14 | + | #include "memory.h" /* debug_read6502, write6502, memory_get_ram_bank/rom_bank */ |
|
| 15 | + | #include "video.h" /* video_get_framebuffer, video_get_frame_count, video_space_read */ |
|
| 16 | + | #include "keyboard.h" /* handle_keyboard */ |
|
| 17 | + | #include "i2c.h" /* mouse_move / mouse_button_* / mouse_send_state */ |
|
| 18 | + | #include "audio.h" /* AUDIO_SAMPLERATE */ |
|
| 19 | + | #include "joystick.h" /* joystick_set_virtual / joystick_clear_virtual */ |
|
| 20 | + | ||
| 21 | + | #include <SDL.h> |
|
| 22 | + | #include <stdbool.h> |
|
| 23 | + | #include <stdio.h> |
|
| 24 | + | #include <stdlib.h> |
|
| 25 | + | #include <string.h> |
|
| 26 | + | ||
| 27 | + | extern uint32_t host_sample_rate; /* audio.c: the real (post-SDL) output rate */ |
|
| 28 | + | ||
| 29 | + | /* Debug-read into out. bank<0 => current bank. $0000-$9FFF is low RAM |
|
| 30 | + | * (bank-independent); $A000-$BFFF is banked RAM; $C000+ is ROM. */ |
|
| 31 | + | static uint32_t |
|
| 32 | + | x16_read_mem(uint32_t addr, int32_t bank, uint32_t len, uint8_t *out, uint32_t cap) |
|
| 33 | + | { |
|
| 34 | + | int16_t x16bank = (bank >= 0) ? (int16_t)bank : USE_CURRENT_X16_BANK; |
|
| 35 | + | uint32_t n = 0; |
|
| 36 | + | for (uint32_t i = 0; i < len && n < cap; i++) { |
|
| 37 | + | uint16_t a = (uint16_t)(addr + i); |
|
| 38 | + | out[n++] = debug_read6502(a, 0, x16bank); |
|
| 39 | + | } |
|
| 40 | + | return n; |
|
| 41 | + | } |
|
| 42 | + | ||
| 43 | + | /* Write (poke) len bytes at addr (0.3). Banked RAM with an explicit bank= goes |
|
| 44 | + | * straight into that bank's backing store, the mirror of x16_read_mem: routed |
|
| 45 | + | * through write6502 it would land in whatever bank the CPU had mapped at the |
|
| 46 | + | * pause (the KERNAL's IRQ handler maps bank 0). Without bank= the poke hits |
|
| 47 | + | * the current bank, as a CPU write would. */ |
|
| 48 | + | static uint32_t |
|
| 49 | + | x16_write_mem(uint32_t addr, int32_t bank, uint32_t len, const uint8_t *in) |
|
| 50 | + | { |
|
| 51 | + | for (uint32_t i = 0; i < len; i++) { |
|
| 52 | + | uint16_t a = (uint16_t)(addr + i); |
|
| 53 | + | if (bank >= 0 && a >= 0xa000 && a < 0xc000) { |
|
| 54 | + | if ((uint32_t)bank < (uint32_t)num_ram_banks) { |
|
| 55 | + | BRAM[((uint32_t)bank << 13) + a - 0xa000] = in[i]; |
|
| 56 | + | } |
|
| 57 | + | continue; |
|
| 58 | + | } |
|
| 59 | + | write6502(a, 0, in[i]); |
|
| 60 | + | } |
|
| 61 | + | return len; |
|
| 62 | + | } |
|
| 63 | + | ||
| 64 | + | /* --- Audio drain (0.3): a ring teed from the mixer. Producer (audio_render, |
|
| 65 | + | * via x16_control_push_audio) and consumer both run on the emulator thread. */ |
|
| 66 | + | #define X16_AUD_RING 65536 /* int16 samples (~0.67 s stereo @ 48.8 kHz) */ |
|
| 67 | + | static int16_t g_aud[X16_AUD_RING]; |
|
| 68 | + | static uint32_t g_aud_head, g_aud_count, g_aud_dropped; |
|
| 69 | + | static int g_aud_active; /* capture only once the server is up */ |
|
| 70 | + | ||
| 71 | + | void |
|
| 72 | + | x16_control_push_audio(int16_t l, int16_t r) |
|
| 73 | + | { |
|
| 74 | + | if (!g_aud_active) return; |
|
| 75 | + | if (g_aud_count + 2 > X16_AUD_RING) { g_aud_dropped += 2; return; } |
|
| 76 | + | g_aud[(g_aud_head + g_aud_count) % X16_AUD_RING] = l; g_aud_count++; |
|
| 77 | + | g_aud[(g_aud_head + g_aud_count) % X16_AUD_RING] = r; g_aud_count++; |
|
| 78 | + | } |
|
| 79 | + | ||
| 80 | + | static uint32_t |
|
| 81 | + | x16_capture_audio(int16_t *out, uint32_t cap, int *rate, int *channels, uint32_t *dropped) |
|
| 82 | + | { |
|
| 83 | + | uint32_t n = 0; |
|
| 84 | + | if (rate) *rate = host_sample_rate ? (int)host_sample_rate : (int)AUDIO_SAMPLERATE; |
|
| 85 | + | if (channels) *channels = 2; |
|
| 86 | + | if (dropped) *dropped = g_aud_dropped; |
|
| 87 | + | g_aud_dropped = 0; |
|
| 88 | + | while (n < cap && g_aud_count) { |
|
| 89 | + | out[n++] = g_aud[g_aud_head]; |
|
| 90 | + | g_aud_head = (g_aud_head + 1) % X16_AUD_RING; |
|
| 91 | + | g_aud_count--; |
|
| 92 | + | } |
|
| 93 | + | return n; |
|
| 94 | + | } |
|
| 95 | + | ||
| 96 | + | static void |
|
| 97 | + | x16_get_regs_json(char *buf, size_t cap) |
|
| 98 | + | { |
|
| 99 | + | snprintf(buf, cap, |
|
| 100 | + | "{\"a\":%u,\"x\":%u,\"y\":%u,\"sp\":%u,\"pc\":%u,\"status\":%u," |
|
| 101 | + | "\"ram_bank\":%u,\"rom_bank\":%u}", |
|
| 102 | + | (unsigned)regs.a, (unsigned)regs.xl, (unsigned)regs.yl, |
|
| 103 | + | (unsigned)(regs.sp & 0xff), (unsigned)regs.pc, (unsigned)regs.status, |
|
| 104 | + | (unsigned)memory_get_ram_bank(), (unsigned)memory_get_rom_bank()); |
|
| 105 | + | } |
|
| 106 | + | ||
| 107 | + | static void |
|
| 108 | + | x16_get_framebuffer(retro_framebuffer_t *out) |
|
| 109 | + | { |
|
| 110 | + | int w = 0, h = 0; |
|
| 111 | + | out->pixels = video_get_framebuffer(&w, &h); |
|
| 112 | + | out->width = w; |
|
| 113 | + | out->height = h; |
|
| 114 | + | out->fmt = RETRO_PIX_BGRA8888; /* x16 framebuffer byte order */ |
|
| 115 | + | } |
|
| 116 | + | ||
| 117 | + | static uint64_t |
|
| 118 | + | x16_get_frame_count(void) |
|
| 119 | + | { |
|
| 120 | + | return video_get_frame_count(); |
|
| 121 | + | } |
|
| 122 | + | ||
| 123 | + | /* Keys (0.2). text => the char's key (letters use the lowercase keycode); |
|
| 124 | + | * code => a raw SDL scancode. */ |
|
| 125 | + | static void |
|
| 126 | + | x16_key_event(SDL_Keycode sym, SDL_Scancode sc, int action) |
|
| 127 | + | { |
|
| 128 | + | if (action == RETRO_KEY_DOWN) { |
|
| 129 | + | handle_keyboard(true, sym, sc); |
|
| 130 | + | } else if (action == RETRO_KEY_UP) { |
|
| 131 | + | handle_keyboard(false, sym, sc); |
|
| 132 | + | } else { |
|
| 133 | + | handle_keyboard(true, sym, sc); |
|
| 134 | + | handle_keyboard(false, sym, sc); |
|
| 135 | + | } |
|
| 136 | + | } |
|
| 137 | + | ||
| 138 | + | static int |
|
| 139 | + | x16_inject_key(int is_text, uint32_t value, int action) |
|
| 140 | + | { |
|
| 141 | + | SDL_Keycode sym; |
|
| 142 | + | SDL_Scancode sc; |
|
| 143 | + | if (is_text) { |
|
| 144 | + | int c = (int)value; |
|
| 145 | + | if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a'; |
|
| 146 | + | sym = (SDL_Keycode)c; |
|
| 147 | + | sc = SDL_GetScancodeFromKey(sym); |
|
| 148 | + | } else { |
|
| 149 | + | sc = (SDL_Scancode)value; |
|
| 150 | + | sym = SDL_GetKeyFromScancode(sc); |
|
| 151 | + | } |
|
| 152 | + | if (sc == SDL_SCANCODE_UNKNOWN) { |
|
| 153 | + | return 0; |
|
| 154 | + | } |
|
| 155 | + | x16_key_event(sym, sc, action); |
|
| 156 | + | return 1; |
|
| 157 | + | } |
|
| 158 | + | ||
| 159 | + | static void |
|
| 160 | + | x16_reset(void) |
|
| 161 | + | { |
|
| 162 | + | machine_reset(); |
|
| 163 | + | } |
|
| 164 | + | ||
| 165 | + | /* Pointer (0.4). The X16 mouse is a relative PS/2 device the KERNAL integrates |
|
| 166 | + | * into sprite 0's position, so inject through mouse_move/mouse_send_state and |
|
| 167 | + | * read the position back from sprite 0's attributes. */ |
|
| 168 | + | #define VERA_SPRITE0_ATTR 0x1FC00U |
|
| 169 | + | static int g_ptr_buttons; /* what this backend last pressed */ |
|
| 170 | + | ||
| 171 | + | static int32_t |
|
| 172 | + | x16_pointer_x(void) |
|
| 173 | + | { |
|
| 174 | + | return (int32_t)video_space_read(VERA_SPRITE0_ATTR + 2) |
|
| 175 | + | | ((int32_t)(video_space_read(VERA_SPRITE0_ATTR + 3) & 3) << 8); |
|
| 176 | + | } |
|
| 177 | + | static int32_t |
|
| 178 | + | x16_pointer_y(void) |
|
| 179 | + | { |
|
| 180 | + | return (int32_t)video_space_read(VERA_SPRITE0_ATTR + 4) |
|
| 181 | + | | ((int32_t)(video_space_read(VERA_SPRITE0_ATTR + 5) & 3) << 8); |
|
| 182 | + | } |
|
| 183 | + | ||
| 184 | + | static int |
|
| 185 | + | x16_set_pointer(int absolute, int32_t x, int32_t y, int buttons) |
|
| 186 | + | { |
|
| 187 | + | if (absolute) { |
|
| 188 | + | mouse_move((int)(x - x16_pointer_x()), (int)(y - x16_pointer_y())); |
|
| 189 | + | } else { |
|
| 190 | + | mouse_move((int)x, (int)y); |
|
| 191 | + | } |
|
| 192 | + | if (buttons >= 0) { |
|
| 193 | + | if (buttons & 1) mouse_button_down(0); else mouse_button_up(0); |
|
| 194 | + | if (buttons & 2) mouse_button_down(1); else mouse_button_up(1); |
|
| 195 | + | g_ptr_buttons = buttons & 3; |
|
| 196 | + | } |
|
| 197 | + | mouse_send_state(); |
|
| 198 | + | return 1; |
|
| 199 | + | } |
|
| 200 | + | ||
| 201 | + | static int |
|
| 202 | + | x16_get_pointer(int32_t *x, int32_t *y, int *buttons) |
|
| 203 | + | { |
|
| 204 | + | *x = x16_pointer_x(); |
|
| 205 | + | *y = x16_pointer_y(); |
|
| 206 | + | *buttons = g_ptr_buttons; |
|
| 207 | + | return 1; |
|
| 208 | + | } |
|
| 209 | + | ||
| 210 | + | /* --- Pads (0.5) --------------------------------------------------------- |
|
| 211 | + | * RRDC's canonical layout (bit 0 LEFT, 1 RIGHT, 2 UP, 3 DOWN, 4 A, 5 B, 6 X, |
|
| 212 | + | * 7 Y, 8 START, 9 SELECT, 10 L, 11 R) to the X16's SNES serial order, active |
|
| 213 | + | * low: bit 0 B, 1 Y, 2 SELECT, 3 START, 4 UP, 5 DOWN, 6 LEFT, 7 RIGHT, 8 A, |
|
| 214 | + | * 9 X, 10 L, 11 R. joystick.h's JOY_BIT_* names those bits after SDL's |
|
| 215 | + | * buttons -- its JOY_BIT_A is SDL's bottom button, SNES B -- so read the |
|
| 216 | + | * table below against the SNES names, not those. |
|
| 217 | + | * |
|
| 218 | + | * Driven through the emulator's own virtual joysticks (joystick_set_virtual, |
|
| 219 | + | * as the DAP server's x16/joystick is), which the latch reads exactly as it |
|
| 220 | + | * reads a real pad. */ |
|
| 221 | + | static const uint8_t pad_canon_to_x16[12] = { |
|
| 222 | + | 6, /* LEFT */ 7, /* RIGHT */ 4, /* UP */ 5, /* DOWN */ |
|
| 223 | + | 8, /* A */ 0, /* B */ 9, /* X */ 1, /* Y */ |
|
| 224 | + | 3, /* START */ 2, /* SELECT */ 10, /* L */ 11, /* R */ |
|
| 225 | + | }; |
|
| 226 | + | ||
| 227 | + | static uint16_t |
|
| 228 | + | pad_canon_to_mask(int buttons) |
|
| 229 | + | { |
|
| 230 | + | uint16_t mask = 0xffff; |
|
| 231 | + | for (int b = 0; b < 12; ++b) { |
|
| 232 | + | if (buttons & (1 << b)) { |
|
| 233 | + | mask &= (uint16_t)~(1u << pad_canon_to_x16[b]); |
|
| 234 | + | } |
|
| 235 | + | } |
|
| 236 | + | return mask; |
|
| 237 | + | } |
|
| 238 | + | ||
| 239 | + | static int |
|
| 240 | + | pad_mask_to_canon(uint16_t mask) |
|
| 241 | + | { |
|
| 242 | + | int buttons = 0; |
|
| 243 | + | for (int b = 0; b < 12; ++b) { |
|
| 244 | + | if ((mask & (1u << pad_canon_to_x16[b])) == 0) { |
|
| 245 | + | buttons |= (1 << b); |
|
| 246 | + | } |
|
| 247 | + | } |
|
| 248 | + | return buttons; |
|
| 249 | + | } |
|
| 250 | + | ||
| 251 | + | static int |
|
| 252 | + | x16_set_pad(int index, int buttons, int connected) |
|
| 253 | + | { |
|
| 254 | + | if (index < 0 || index >= NUM_JOYSTICKS) { |
|
| 255 | + | return 0; |
|
| 256 | + | } |
|
| 257 | + | if (connected == 0) { |
|
| 258 | + | joystick_clear_virtual(index); |
|
| 259 | + | return 1; |
|
| 260 | + | } |
|
| 261 | + | if (buttons >= 0) { |
|
| 262 | + | joystick_set_virtual(index, pad_canon_to_mask(buttons)); |
|
| 263 | + | } else if (connected == 1 && !joystick_virtual_active(index)) { |
|
| 264 | + | joystick_set_virtual(index, 0xffff); /* present, nothing held */ |
|
| 265 | + | } |
|
| 266 | + | return 1; |
|
| 267 | + | } |
|
| 268 | + | ||
| 269 | + | static int |
|
| 270 | + | x16_get_pad(int index, int *buttons, int *connected) |
|
| 271 | + | { |
|
| 272 | + | if (index < 0 || index >= NUM_JOYSTICKS) { |
|
| 273 | + | return 0; |
|
| 274 | + | } |
|
| 275 | + | joystick_debug_state_t st; |
|
| 276 | + | joystick_debug_get_state(&st); |
|
| 277 | + | const joystick_slot_debug_t *s = &st.slots[index]; |
|
| 278 | + | *connected = (s->virtual_active || s->controller_bound) ? 1 : 0; |
|
| 279 | + | *buttons = pad_mask_to_canon(*connected ? s->button_mask : 0xffff); |
|
| 280 | + | return 1; |
|
| 281 | + | } |
|
| 282 | + | ||
| 283 | + | /* --- Performance metrics (0.6) ------------------------------------------- */ |
|
| 284 | + | #ifdef HAS_DAP |
|
| 285 | + | #include "perf_json.h" |
|
| 286 | + | #include "perf_budget.h" |
|
| 287 | + | #include "vera_bandwidth.h" |
|
| 288 | + | #include "sprite_trace.h" |
|
| 289 | + | ||
| 290 | + | /* A query string's value for `key`, or NULL. */ |
|
| 291 | + | static const char * |
|
| 292 | + | q_find(const char *q, const char *key, size_t *len) |
|
| 293 | + | { |
|
| 294 | + | const size_t kl = strlen(key); |
|
| 295 | + | for (const char *p = q; p && *p; ) { |
|
| 296 | + | if (!strncmp(p, key, kl) && p[kl] == '=') { |
|
| 297 | + | const char *v = p + kl + 1; |
|
| 298 | + | const char *e = strchr(v, '&'); |
|
| 299 | + | *len = e ? (size_t)(e - v) : strlen(v); |
|
| 300 | + | return v; |
|
| 301 | + | } |
|
| 302 | + | p = strchr(p, '&'); |
|
| 303 | + | if (p) p++; |
|
| 304 | + | } |
|
| 305 | + | return NULL; |
|
| 306 | + | } |
|
| 307 | + | ||
| 308 | + | static long |
|
| 309 | + | q_long(const char *q, const char *key, long def) |
|
| 310 | + | { |
|
| 311 | + | size_t n; |
|
| 312 | + | const char *v = q_find(q, key, &n); |
|
| 313 | + | return v ? strtol(v, NULL, 0) : def; |
|
| 314 | + | } |
|
| 315 | + | ||
| 316 | + | /* Measuring is the remote owner's to switch: a client asking for numbers is a |
|
| 317 | + | * client that wants them collected (the SPEC lets a backend start on the |
|
| 318 | + | * first request), and the panel or a DAP client closing does not stop it. */ |
|
| 319 | + | static void |
|
| 320 | + | perf_arm_remote(void) |
|
| 321 | + | { |
|
| 322 | + | if (!perf_budget_is_enabled()) { |
|
| 323 | + | perf_budget_arm(PERF_OWNER_REMOTE, true); |
|
| 324 | + | } |
|
| 325 | + | if (!vera_bandwidth_is_enabled()) { |
|
| 326 | + | vera_bandwidth_arm(VERA_BW_OWNER_REMOTE, true); |
|
| 327 | + | } |
|
| 328 | + | } |
|
| 329 | + | ||
| 330 | + | static cJSON * |
|
| 331 | + | perf_frame_record(const perf_frame_t *f) |
|
| 332 | + | { |
|
| 333 | + | cJSON *o = perf_json_frame(f); |
|
| 334 | + | cJSON *z = cJSON_CreateArray(); |
|
| 335 | + | const int nz = perf_budget_zone_count(); |
|
| 336 | + | for (int i = 0; i < nz && i < PERF_RING_ZONES; i++) { |
|
| 337 | + | cJSON_AddItemToArray(z, cJSON_CreateNumber(f->zone[i])); |
|
| 338 | + | } |
|
| 339 | + | cJSON_AddItemToObject(o, "zones", z); |
|
| 340 | + | return o; |
|
| 341 | + | } |
|
| 342 | + | ||
| 343 | + | static cJSON * |
|
| 344 | + | perf_zones_json(const float *secs, int nsecs) |
|
| 345 | + | { |
|
| 346 | + | cJSON *zones = cJSON_CreateArray(); |
|
| 347 | + | perf_frame_t last; |
|
| 348 | + | const bool have_last = perf_budget_last_frame(&last); |
|
| 349 | + | const int nz = perf_budget_zone_count(); |
|
| 350 | + | for (int i = 0; i < nz; i++) { |
|
| 351 | + | perf_zone_t z; |
|
| 352 | + | if (!perf_budget_zone_get(i, &z)) continue; |
|
| 353 | + | cJSON *o = cJSON_CreateObject(); |
|
| 354 | + | cJSON_AddNumberToObject(o, "id", i); |
|
| 355 | + | cJSON_AddStringToObject(o, "name", z.name); |
|
| 356 | + | cJSON_AddStringToObject(o, "kind", z.is_marker ? "marker" : "address"); |
|
| 357 | + | if (z.is_marker) { |
|
| 358 | + | cJSON_AddNumberToObject(o, "markerId", z.marker_id); |
|
| 359 | + | } else { |
|
| 360 | + | cJSON_AddNumberToObject(o, "start", z.start); |
|
| 361 | + | cJSON_AddNumberToObject(o, "end", z.end); |
|
| 362 | + | cJSON_AddNumberToObject(o, "bank", z.bank); |
|
| 363 | + | } |
|
| 364 | + | cJSON_AddBoolToObject(o, "idle", z.is_idle); |
|
| 365 | + | cJSON_AddNumberToObject(o, "current", (have_last && i < PERF_RING_ZONES) ? last.zone[i] : 0); |
|
| 366 | + | cJSON *w = cJSON_CreateObject(); |
|
| 367 | + | for (int k = 0; i < PERF_RING_ZONES && k < nsecs; k++) { /* history: the first 8 only */ |
|
| 368 | + | perf_stat_t st; |
|
| 369 | + | if (!perf_budget_zone_window(i, secs[k], &st)) continue; |
|
| 370 | + | char name[32]; |
|
| 371 | + | perf_json_window_name(secs[k], name, sizeof name); |
|
| 372 | + | cJSON_AddItemToObject(w, name, perf_json_stat(&st)); |
|
| 373 | + | } |
|
| 374 | + | cJSON_AddItemToObject(o, "windows", w); |
|
| 375 | + | cJSON_AddItemToArray(zones, o); |
|
| 376 | + | } |
|
| 377 | + | return zones; |
|
| 378 | + | } |
|
| 379 | + | ||
| 380 | + | /* The GET /perf body. settings_only: POST /perf's answer, the settings as they |
|
| 381 | + | * stand (no windows, zones or frames). */ |
|
| 382 | + | static char * |
|
| 383 | + | perf_body(const char *query, bool settings_only) |
|
| 384 | + | { |
|
| 385 | + | cJSON *body = cJSON_CreateObject(); |
|
| 386 | + | cJSON_AddBoolToObject(body, "enabled", perf_budget_is_enabled()); |
|
| 387 | + | cJSON_AddItemToObject(body, "budget", perf_json_budget()); |
|
| 388 | + | ||
| 389 | + | perf_frame_t last; |
|
| 390 | + | if (!perf_budget_last_frame(&last)) { |
|
| 391 | + | memset(&last, 0, sizeof last); |
|
| 392 | + | } |
|
| 393 | + | cJSON_AddItemToObject(body, "current", perf_json_frame(&last)); |
|
| 394 | + | ||
| 395 | + | const bool lines = q_long(query, "lines", 0) != 0; |
|
| 396 | + | if (!settings_only) { |
|
| 397 | + | float secs[16]; |
|
| 398 | + | int nsecs = 0; |
|
| 399 | + | size_t n; |
|
| 400 | + | const char *v = q_find(query, "windows", &n); |
|
| 401 | + | if (v) { |
|
| 402 | + | char buf[256]; |
|
| 403 | + | if (n >= sizeof buf) n = sizeof buf - 1; |
|
| 404 | + | memcpy(buf, v, n); |
|
| 405 | + | buf[n] = 0; |
|
| 406 | + | for (char *tok = strtok(buf, ","); tok && nsecs < 16; tok = strtok(NULL, ",")) { |
|
| 407 | + | cJSON *item = cJSON_CreateString(tok); |
|
| 408 | + | secs[nsecs++] = perf_json_window_seconds(item); |
|
| 409 | + | cJSON_Delete(item); |
|
| 410 | + | } |
|
| 411 | + | } else { |
|
| 412 | + | static const float defaults[] = {1.0f, 10.0f, 30.0f, 60.0f, 0.0f}; |
|
| 413 | + | for (; nsecs < 5; nsecs++) secs[nsecs] = defaults[nsecs]; |
|
| 414 | + | } |
|
| 415 | + | cJSON *windows = cJSON_CreateArray(); |
|
| 416 | + | for (int i = 0; i < nsecs; i++) { |
|
| 417 | + | cJSON *w = perf_json_window(secs[i]); |
|
| 418 | + | if (w) cJSON_AddItemToArray(windows, w); |
|
| 419 | + | } |
|
| 420 | + | cJSON_AddItemToObject(body, "windows", windows); |
|
| 421 | + | ||
| 422 | + | if (q_long(query, "zones", 1) != 0) { |
|
| 423 | + | cJSON_AddItemToObject(body, "zones", perf_zones_json(secs, nsecs)); |
|
| 424 | + | } |
|
| 425 | + | ||
| 426 | + | /* frames=n: the last n; since=f: every frame after f still held (the |
|
| 427 | + | * newest `frames` of them when both are given). Oldest first. */ |
|
| 428 | + | const long want = q_long(query, "frames", 0); |
|
| 429 | + | const long since = q_long(query, "since", -1); |
|
| 430 | + | if (want > 0 || since >= 0) { |
|
| 431 | + | const int have = perf_budget_frame_count(); |
|
| 432 | + | int take = 0; |
|
| 433 | + | for (; take < have; take++) { |
|
| 434 | + | perf_frame_t f; |
|
| 435 | + | if (!perf_budget_frame_at(take, &f)) break; |
|
| 436 | + | if (since >= 0 && (long)f.frame <= since) break; |
|
| 437 | + | if (want > 0 && take >= want) break; |
|
| 438 | + | } |
|
| 439 | + | cJSON *frames = cJSON_CreateArray(); |
|
| 440 | + | for (int i = take - 1; i >= 0; i--) { |
|
| 441 | + | perf_frame_t f; |
|
| 442 | + | if (perf_budget_frame_at(i, &f)) { |
|
| 443 | + | cJSON_AddItemToArray(frames, perf_frame_record(&f)); |
|
| 444 | + | } |
|
| 445 | + | } |
|
| 446 | + | cJSON_AddItemToObject(body, "frames", frames); |
|
| 447 | + | } |
|
| 448 | + | } |
|
| 449 | + | ||
| 450 | + | cJSON *platform = cJSON_CreateObject(); |
|
| 451 | + | cJSON *bw = perf_json_bandwidth(); |
|
| 452 | + | if (lines) { |
|
| 453 | + | cJSON_AddItemToObject(bw, "lines", perf_json_bandwidth_lines()); |
|
| 454 | + | } |
|
| 455 | + | cJSON_AddItemToObject(platform, "bandwidth", bw); |
|
| 456 | + | cJSON_AddItemToObject(platform, "sprites", perf_json_sprites(lines)); |
|
| 457 | + | cJSON_AddItemToObject(body, "platform", platform); |
|
| 458 | + | ||
| 459 | + | char *out = cJSON_PrintUnformatted(body); |
|
| 460 | + | cJSON_Delete(body); |
|
| 461 | + | return out; /* cJSON's allocator is malloc: the server free()s it */ |
|
| 462 | + | } |
|
| 463 | + | ||
| 464 | + | static char * |
|
| 465 | + | x16_get_perf(const char *query) |
|
| 466 | + | { |
|
| 467 | + | perf_arm_remote(); |
|
| 468 | + | return perf_body(query ? query : "", false); |
|
| 469 | + | } |
|
| 470 | + | ||
| 471 | + | static char * |
|
| 472 | + | perf_error(int *status, const char *msg) |
|
| 473 | + | { |
|
| 474 | + | *status = 400; |
|
| 475 | + | char *b = (char *)malloc(strlen(msg) + 16); |
|
| 476 | + | if (b) sprintf(b, "{\"error\":\"%s\"}", msg); |
|
| 477 | + | return b; |
|
| 478 | + | } |
|
| 479 | + | ||
| 480 | + | static char * |
|
| 481 | + | x16_set_perf(const char *text, int *status) |
|
| 482 | + | { |
|
| 483 | + | cJSON *args = cJSON_Parse(text); |
|
| 484 | + | if (!cJSON_IsObject(args)) { |
|
| 485 | + | cJSON_Delete(args); |
|
| 486 | + | return perf_error(status, "body is not a JSON object"); |
|
| 487 | + | } |
|
| 488 | + | /* Check every field's type before applying any, so a bad body changes |
|
| 489 | + | * nothing. */ |
|
| 490 | + | cJSON *en = cJSON_GetObjectItemCaseSensitive(args, "enabled"); |
|
| 491 | + | cJSON *fps = cJSON_GetObjectItemCaseSensitive(args, "targetFps"); |
|
| 492 | + | cJSON *idle = cJSON_GetObjectItemCaseSensitive(args, "idleMode"); |
|
| 493 | + | cJSON *hist = cJSON_GetObjectItemCaseSensitive(args, "historyFrames"); |
|
| 494 | + | cJSON *zones = cJSON_GetObjectItemCaseSensitive(args, "zones"); |
|
| 495 | + | cJSON *rst = cJSON_GetObjectItemCaseSensitive(args, "reset"); |
|
| 496 | + | cJSON *spr = cJSON_GetObjectItemCaseSensitive(args, "sprites"); |
|
| 497 | + | const char *bad = |
|
| 498 | + | (en && !cJSON_IsBool(en)) ? "enabled is not a boolean" : |
|
| 499 | + | (fps && !(cJSON_IsNumber(fps) && fps->valuedouble > 0)) ? "targetFps is not a positive number" : |
|
| 500 | + | (idle && !(cJSON_IsString(idle) && (!strcmp(idle->valuestring, "auto") || |
|
| 501 | + | !strcmp(idle->valuestring, "markers") || !strcmp(idle->valuestring, "none")))) |
|
| 502 | + | ? "idleMode is not auto, markers or none" : |
|
| 503 | + | (hist && !(cJSON_IsNumber(hist) && hist->valuedouble >= 2)) ? "historyFrames is not a number of 2 or more" : |
|
| 504 | + | (zones && !cJSON_IsArray(zones)) ? "zones is not an array" : |
|
| 505 | + | (rst && !cJSON_IsBool(rst)) ? "reset is not a boolean" : |
|
| 506 | + | (spr && !cJSON_IsBool(spr)) ? "sprites is not a boolean" : NULL; |
|
| 507 | + | if (bad) { |
|
| 508 | + | cJSON_Delete(args); |
|
| 509 | + | return perf_error(status, bad); |
|
| 510 | + | } |
|
| 511 | + | ||
| 512 | + | if (en) { |
|
| 513 | + | const bool want = cJSON_IsTrue(en); |
|
| 514 | + | perf_budget_arm(PERF_OWNER_REMOTE, want); |
|
| 515 | + | vera_bandwidth_arm(VERA_BW_OWNER_REMOTE, want); |
|
| 516 | + | } |
|
| 517 | + | if (fps) perf_budget_set_target_fps((float)fps->valuedouble); |
|
| 518 | + | if (idle) { |
|
| 519 | + | perf_budget_set_idle_mode(!strcmp(idle->valuestring, "markers") ? PERF_IDLE_MARKERS : |
|
| 520 | + | !strcmp(idle->valuestring, "none") ? PERF_IDLE_NONE |
|
| 521 | + | : PERF_IDLE_AUTO); |
|
| 522 | + | } |
|
| 523 | + | if (hist) perf_budget_set_capacity((int)hist->valuedouble); |
|
| 524 | + | if (zones) { |
|
| 525 | + | /* Replaces the list: address zones and marker names alike. */ |
|
| 526 | + | perf_budget_zone_clear(); |
|
| 527 | + | cJSON *z; |
|
| 528 | + | cJSON_ArrayForEach(z, zones) { |
|
| 529 | + | if (!cJSON_IsObject(z)) continue; |
|
| 530 | + | cJSON *jn = cJSON_GetObjectItemCaseSensitive(z, "name"); |
|
| 531 | + | const char *name = cJSON_IsString(jn) ? jn->valuestring : "zone"; |
|
| 532 | + | cJSON *jm = cJSON_GetObjectItemCaseSensitive(z, "marker"); |
|
| 533 | + | if (cJSON_IsNumber(jm)) { |
|
| 534 | + | perf_budget_zone_add_marker(name, (uint8_t)jm->valuedouble); |
|
| 535 | + | continue; |
|
| 536 | + | } |
|
| 537 | + | cJSON *js = cJSON_GetObjectItemCaseSensitive(z, "start"); |
|
| 538 | + | cJSON *je = cJSON_GetObjectItemCaseSensitive(z, "end"); |
|
| 539 | + | cJSON *jb = cJSON_GetObjectItemCaseSensitive(z, "bank"); |
|
| 540 | + | cJSON *ji = cJSON_GetObjectItemCaseSensitive(z, "idle"); |
|
| 541 | + | if (!cJSON_IsNumber(js) || !cJSON_IsNumber(je)) continue; |
|
| 542 | + | perf_budget_zone_add(name, (uint16_t)js->valuedouble, (uint16_t)je->valuedouble, |
|
| 543 | + | cJSON_IsNumber(jb) ? (int16_t)jb->valuedouble : (int16_t)-1, |
|
| 544 | + | cJSON_IsTrue(ji)); |
|
| 545 | + | } |
|
| 546 | + | } |
|
| 547 | + | if (spr) sprite_trace_set_enabled(cJSON_IsTrue(spr)); |
|
| 548 | + | if (rst && cJSON_IsTrue(rst)) { |
|
| 549 | + | perf_budget_reset(); |
|
| 550 | + | vera_bandwidth_reset(); |
|
| 551 | + | if (sprite_trace_active) sprite_trace_reset(); |
|
| 552 | + | } |
|
| 553 | + | cJSON_Delete(args); |
|
| 554 | + | *status = 200; |
|
| 555 | + | return perf_body("", true); |
|
| 556 | + | } |
|
| 557 | + | #endif /* HAS_DAP */ |
|
| 558 | + | ||
| 559 | + | static const retro_control_backend_t x16_backend = { |
|
| 560 | + | .platform = "x16", |
|
| 561 | + | .emulator = "x16emu-enhanced", |
|
| 562 | + | .read_mem = x16_read_mem, |
|
| 563 | + | .get_regs_json = x16_get_regs_json, |
|
| 564 | + | .get_framebuffer = x16_get_framebuffer, |
|
| 565 | + | .get_frame_count = x16_get_frame_count, |
|
| 566 | + | .inject_key = x16_inject_key, |
|
| 567 | + | .reset = x16_reset, |
|
| 568 | + | .write_mem = x16_write_mem, |
|
| 569 | + | .capture_audio = x16_capture_audio, |
|
| 570 | + | .set_pointer = x16_set_pointer, |
|
| 571 | + | .get_pointer = x16_get_pointer, |
|
| 572 | + | .set_pad = x16_set_pad, |
|
| 573 | + | .get_pad = x16_get_pad, |
|
| 574 | + | #ifdef HAS_DAP |
|
| 575 | + | .get_perf = x16_get_perf, |
|
| 576 | + | .set_perf = x16_set_perf, |
|
| 577 | + | #endif |
|
| 578 | + | }; |
|
| 579 | + | ||
| 580 | + | bool control_port_active = false; |
|
| 581 | + | ||
| 582 | + | int |
|
| 583 | + | x16_control_start(int port) |
|
| 584 | + | { |
|
| 585 | + | g_aud_active = 1; |
|
| 586 | + | const int rc = retro_control_start(port, &x16_backend); |
|
| 587 | + | control_port_active = (rc == 0); |
|
| 588 | + | return rc; |
|
| 589 | + | } |
added src/extern/retro-remote-debug-controller +0 −0
No line changes.
added src/perf_json.c +228 −0
| 1 | + | // perf_json.c -- see perf_json.h. The builders that were debug_server.c's, |
|
| 2 | + | // moved so the RRDC control port answers with the same fields. |
|
| 3 | + | ||
| 4 | + | #include "perf_json.h" |
|
| 5 | + | ||
| 6 | + | #include <stdio.h> |
|
| 7 | + | #include <stdlib.h> |
|
| 8 | + | #include <string.h> |
|
| 9 | + | ||
| 10 | + | #include "glue.h" // MHZ, warp_mode |
|
| 11 | + | #include "vera_bandwidth.h" |
|
| 12 | + | #include "sprite_trace.h" |
|
| 13 | + | ||
| 14 | + | cJSON *perf_json_stat(const perf_stat_t *s) { |
|
| 15 | + | cJSON *o = cJSON_CreateObject(); |
|
| 16 | + | cJSON_AddNumberToObject(o, "min", s->min); |
|
| 17 | + | cJSON_AddNumberToObject(o, "mean", s->mean); |
|
| 18 | + | cJSON_AddNumberToObject(o, "p50", s->p50); |
|
| 19 | + | cJSON_AddNumberToObject(o, "p95", s->p95); |
|
| 20 | + | cJSON_AddNumberToObject(o, "p99", s->p99); |
|
| 21 | + | cJSON_AddNumberToObject(o, "max", s->max); |
|
| 22 | + | return o; |
|
| 23 | + | } |
|
| 24 | + | ||
| 25 | + | void perf_json_window_name(float seconds, char *out, size_t outsz) { |
|
| 26 | + | if (seconds <= 0.0f) snprintf(out, outsz, "session"); |
|
| 27 | + | else snprintf(out, outsz, "%gs", (double)seconds); |
|
| 28 | + | } |
|
| 29 | + | ||
| 30 | + | // Accepts either a number of seconds or a name ("30s", "session"), so a client |
|
| 31 | + | // can send back the same window labels it was given. |
|
| 32 | + | float perf_json_window_seconds(const cJSON *item) { |
|
| 33 | + | if (cJSON_IsNumber(item)) return (float)item->valuedouble; |
|
| 34 | + | if (cJSON_IsString(item) && item->valuestring) { |
|
| 35 | + | if (!strcmp(item->valuestring, "session")) return 0.0f; |
|
| 36 | + | return (float)atof(item->valuestring); // "30s" -> 30 |
|
| 37 | + | } |
|
| 38 | + | return 0.0f; |
|
| 39 | + | } |
|
| 40 | + | ||
| 41 | + | cJSON *perf_json_budget(void) { |
|
| 42 | + | cJSON *b = cJSON_CreateObject(); |
|
| 43 | + | cJSON_AddBoolToObject(b, "enabled", perf_budget_is_enabled()); |
|
| 44 | + | cJSON_AddNumberToObject(b, "targetFps", perf_budget_get_target_fps()); |
|
| 45 | + | cJSON_AddNumberToObject(b, "vsyncHz", perf_budget_vsync_hz()); |
|
| 46 | + | cJSON_AddNumberToObject(b, "machineKhz", (double)MHZ * 1000.0); |
|
| 47 | + | cJSON_AddNumberToObject(b, "cyclesPerFrame", perf_budget_cycles_per_frame()); |
|
| 48 | + | cJSON_AddNumberToObject(b, "cyclesPerScanline", perf_budget_cycles_per_scanline()); |
|
| 49 | + | cJSON_AddNumberToObject(b, "scanlinesPerFrame", |
|
| 50 | + | perf_budget_cycles_per_scanline() |
|
| 51 | + | ? perf_budget_cycles_per_frame() / perf_budget_cycles_per_scanline() : 0); |
|
| 52 | + | cJSON_AddNumberToObject(b, "budgetCycles", perf_budget_budget_cycles()); |
|
| 53 | + | cJSON_AddNumberToObject(b, "framesPerBudget", perf_budget_frames_per_budget()); |
|
| 54 | + | cJSON_AddNumberToObject(b, "historyFrames", perf_budget_capacity()); |
|
| 55 | + | cJSON_AddNumberToObject(b, "totalFrames", perf_budget_total_frames()); |
|
| 56 | + | const char *mode = "auto"; |
|
| 57 | + | switch (perf_budget_get_idle_mode()) { |
|
| 58 | + | case PERF_IDLE_MARKERS: mode = "markers"; break; |
|
| 59 | + | case PERF_IDLE_NONE: mode = "none"; break; |
|
| 60 | + | default: mode = "auto"; break; |
|
| 61 | + | } |
|
| 62 | + | cJSON_AddStringToObject(b, "idleMode", mode); |
|
| 63 | + | cJSON_AddBoolToObject(b, "warpMode", warp_mode); |
|
| 64 | + | return b; |
|
| 65 | + | } |
|
| 66 | + | ||
| 67 | + | cJSON *perf_json_frame(const perf_frame_t *f) { |
|
| 68 | + | cJSON *o = cJSON_CreateObject(); |
|
| 69 | + | const uint32_t budget = perf_budget_budget_cycles(); |
|
| 70 | + | cJSON_AddNumberToObject(o, "frame", f->frame); |
|
| 71 | + | cJSON_AddNumberToObject(o, "totalCycles", f->total); |
|
| 72 | + | cJSON_AddNumberToObject(o, "workCycles", perf_frame_work(f)); |
|
| 73 | + | cJSON_AddNumberToObject(o, "idleCycles", f->idle); |
|
| 74 | + | cJSON_AddNumberToObject(o, "irqCycles", f->irq); |
|
| 75 | + | cJSON_AddNumberToObject(o, "mainCycles", |
|
| 76 | + | perf_frame_work(f) > f->irq ? perf_frame_work(f) - f->irq : 0); |
|
| 77 | + | cJSON_AddNumberToObject(o, "budgetWorkCycles", f->period_work); |
|
| 78 | + | cJSON_AddNumberToObject(o, "instructions", f->instructions); |
|
| 79 | + | cJSON_AddNumberToObject(o, "hostUs", f->host_us); |
|
| 80 | + | cJSON_AddNumberToObject(o, "utilizationPct", |
|
| 81 | + | budget ? 100.0 * (double)f->period_work / (double)budget : 0.0); |
|
| 82 | + | cJSON_AddBoolToObject(o, "overBudget", budget && f->period_work > budget); |
|
| 83 | + | return o; |
|
| 84 | + | } |
|
| 85 | + | ||
| 86 | + | // VERA's side of the same question. Deliberately a sub-object of perfStats |
|
| 87 | + | // rather than a request of its own: a developer asking "what is eating my |
|
| 88 | + | // frame?" should get the CPU answer and the bus answer in one round trip, |
|
| 89 | + | // because the interesting cases are the ones where the two disagree. |
|
| 90 | + | // |
|
| 91 | + | // The headline is peakLineClocks against lineClocks (800), NOT a percentage of |
|
| 92 | + | // VERA's total bandwidth. A frame uses maybe a fifth of the chip's 100 MB/s and |
|
| 93 | + | // reporting that would be true and useless -- the scanline is the window that |
|
| 94 | + | // actually runs out. See src/vera_bandwidth.h. |
|
| 95 | + | cJSON *perf_json_bandwidth(void) { |
|
| 96 | + | cJSON *o = cJSON_CreateObject(); |
|
| 97 | + | cJSON_AddBoolToObject(o, "enabled", vera_bandwidth_is_enabled()); |
|
| 98 | + | cJSON_AddNumberToObject(o, "lineClocks", VERA_BW_LINE_CLOCKS); |
|
| 99 | + | cJSON_AddNumberToObject(o, "bytesPerAccess", VERA_BW_BYTES_PER_ACCESS); |
|
| 100 | + | cJSON_AddNumberToObject(o, "accessClocks", VERA_BW_ACCESS_CLOCKS); |
|
| 101 | + | cJSON_AddNumberToObject(o, "scanlinesPerFrame", vera_bandwidth_scanlines_per_frame()); |
|
| 102 | + | ||
| 103 | + | vera_bw_frame_t f; |
|
| 104 | + | if (!vera_bandwidth_last_frame(&f)) { |
|
| 105 | + | return o; |
|
| 106 | + | } |
|
| 107 | + | ||
| 108 | + | cJSON_AddNumberToObject(o, "frame", f.frame); |
|
| 109 | + | cJSON_AddNumberToObject(o, "linesActive", f.lines_active); |
|
| 110 | + | ||
| 111 | + | cJSON *layers = cJSON_CreateArray(); |
|
| 112 | + | for (int i = 0; i < 2; i++) { |
|
| 113 | + | cJSON *l = cJSON_CreateObject(); |
|
| 114 | + | cJSON_AddNumberToObject(l, "fetches", f.layer_fetches[i]); |
|
| 115 | + | cJSON_AddNumberToObject(l, "bytes", f.layer_bytes[i]); |
|
| 116 | + | cJSON_AddNumberToObject(l, "peakLine", f.layer_peak_line[i]); |
|
| 117 | + | cJSON_AddNumberToObject(l, "peakFetches", f.layer_peak_fetches[i]); |
|
| 118 | + | cJSON_AddItemToArray(layers, l); |
|
| 119 | + | } |
|
| 120 | + | cJSON_AddItemToObject(o, "layers", layers); |
|
| 121 | + | ||
| 122 | + | // Per-scanline pressure: the headline. |
|
| 123 | + | cJSON_AddNumberToObject(o, "peakLine", f.peak_line); |
|
| 124 | + | cJSON_AddNumberToObject(o, "peakLineClocks", f.peak_clocks); |
|
| 125 | + | cJSON_AddNumberToObject(o, "p95LineClocks", f.p95_clocks); |
|
| 126 | + | cJSON_AddNumberToObject(o, "meanLineClocks", f.mean_clocks); |
|
| 127 | + | cJSON_AddNumberToObject(o, "linesOverBudget", f.lines_over); |
|
| 128 | + | cJSON_AddNumberToObject(o, "peakLinePct", |
|
| 129 | + | 100.0 * (double)f.peak_clocks / (double)VERA_BW_LINE_CLOCKS); |
|
| 130 | + | cJSON_AddNumberToObject(o, "totalFetches", f.total_fetches); |
|
| 131 | + | cJSON_AddNumberToObject(o, "totalClocks", f.total_clocks); |
|
| 132 | + | ||
| 133 | + | // How much of each scanline was still free when the layers and the CPU had |
|
| 134 | + | // taken their share. Sprites are last on the bus (vram_if.v:142-157), so |
|
| 135 | + | // this is what they were really competing for. |
|
| 136 | + | // |
|
| 137 | + | // Named "bus" deliberately. Every clock figure in this object is BUS |
|
| 138 | + | // OCCUPANCY, which is not the clock the sprite views report: sprite render |
|
| 139 | + | // time is wall clock and runs about five times higher, so a client that |
|
| 140 | + | // subtracted one from the other would conclude a comfortable line was |
|
| 141 | + | // oversubscribed. Convert sprite width and colour depth to fetches first. |
|
| 142 | + | cJSON_AddNumberToObject(o, "spriteBusHeadroomClocksAtPeak", |
|
| 143 | + | f.peak_clocks < VERA_BW_LINE_CLOCKS |
|
| 144 | + | ? VERA_BW_LINE_CLOCKS - f.peak_clocks : 0); |
|
| 145 | + | cJSON_AddStringToObject(o, "clockUnits", "bus"); |
|
| 146 | + | ||
| 147 | + | cJSON *port = cJSON_CreateObject(); |
|
| 148 | + | cJSON_AddNumberToObject(port, "readBytes", f.port_reads); |
|
| 149 | + | cJSON_AddNumberToObject(port, "writeBytes", f.port_writes); |
|
| 150 | + | cJSON_AddNumberToObject(port, "accesses", f.port_accesses); |
|
| 151 | + | cJSON_AddNumberToObject(port, "vramBytes", f.port_vram_bytes); |
|
| 152 | + | cJSON_AddNumberToObject(port, "fxCacheWrites", f.fx_cache_writes); |
|
| 153 | + | cJSON_AddNumberToObject(port, "fxAffineFetches", f.fx_affine_fetches); |
|
| 154 | + | // Bytes that reached VRAM per byte the CPU pushed. Above 1.0 means FX is |
|
| 155 | + | // doing work the CPU did not pay for, which is the point of using it. |
|
| 156 | + | const uint32_t stores = f.port_reads + f.port_writes; |
|
| 157 | + | cJSON_AddNumberToObject(port, "amplification", |
|
| 158 | + | stores ? (double)f.port_vram_bytes / (double)stores : 0.0); |
|
| 159 | + | cJSON_AddItemToObject(o, "port", port); |
|
| 160 | + | ||
| 161 | + | return o; |
|
| 162 | + | } |
|
| 163 | + | ||
| 164 | + | ||
| 165 | + | cJSON *perf_json_window(float secs) { |
|
| 166 | + | perf_window_t w; |
|
| 167 | + | if (!perf_budget_window(secs, &w)) return NULL; |
|
| 168 | + | char name[32]; |
|
| 169 | + | perf_json_window_name(secs, name, sizeof name); |
|
| 170 | + | cJSON *o = cJSON_CreateObject(); |
|
| 171 | + | cJSON_AddStringToObject(o, "name", name); |
|
| 172 | + | cJSON_AddNumberToObject(o, "seconds", secs); |
|
| 173 | + | cJSON_AddNumberToObject(o, "frames", w.frames); |
|
| 174 | + | cJSON_AddNumberToObject(o, "budgetCycles", w.budget_cycles); |
|
| 175 | + | cJSON_AddItemToObject(o, "work", perf_json_stat(&w.work)); |
|
| 176 | + | cJSON_AddItemToObject(o, "budgetWork", perf_json_stat(&w.budget_work)); |
|
| 177 | + | cJSON_AddItemToObject(o, "total", perf_json_stat(&w.total)); |
|
| 178 | + | cJSON_AddItemToObject(o, "irq", perf_json_stat(&w.irq)); |
|
| 179 | + | cJSON_AddItemToObject(o, "hostUs", perf_json_stat(&w.host_us)); |
|
| 180 | + | cJSON_AddNumberToObject(o, "overruns", w.overruns); |
|
| 181 | + | cJSON_AddNumberToObject(o, "overrunPct", w.overrun_pct); |
|
| 182 | + | cJSON_AddNumberToObject(o, "worstOverrunCycles", w.worst_overrun); |
|
| 183 | + | cJSON_AddNumberToObject(o, "worstFrame", w.worst_frame); |
|
| 184 | + | cJSON_AddNumberToObject(o, "meanUtilizationPct", w.mean_utilization); |
|
| 185 | + | cJSON_AddNumberToObject(o, "p95UtilizationPct", w.p95_utilization); |
|
| 186 | + | cJSON_AddNumberToObject(o, "maxUtilizationPct", w.max_utilization); |
|
| 187 | + | return o; |
|
| 188 | + | } |
|
| 189 | + | ||
| 190 | + | cJSON *perf_json_bandwidth_lines(void) { |
|
| 191 | + | uint16_t n = 0; |
|
| 192 | + | const vera_bw_line_t *lines = vera_bandwidth_last_lines(&n); |
|
| 193 | + | cJSON *a = cJSON_CreateArray(); |
|
| 194 | + | for (uint16_t i = 0; lines && i < n; i++) |
|
| 195 | + | cJSON_AddItemToArray(a, cJSON_CreateNumber(vera_bw_line_clocks(&lines[i]))); |
|
| 196 | + | return a; |
|
| 197 | + | } |
|
| 198 | + | ||
| 199 | + | // The sprite renderer is the other half of VERA's scanline: what it spent |
|
| 200 | + | // drawing sprites on each line against the 798 clocks it has (wall clock, not |
|
| 201 | + | // the bus clocks perf_json_bandwidth counts -- see vera_bandwidth.h). |
|
| 202 | + | cJSON *perf_json_sprites(bool lines) { |
|
| 203 | + | cJSON *o = cJSON_CreateObject(); |
|
| 204 | + | const sprite_trace_frame_t *f = sprite_trace_active ? sprite_trace_last_frame() : NULL; |
|
| 205 | + | cJSON_AddBoolToObject(o, "enabled", sprite_trace_active); |
|
| 206 | + | cJSON_AddNumberToObject(o, "lineBudget", SPRITE_TRACE_LINE_BUDGET); |
|
| 207 | + | if (!f) return o; |
|
| 208 | + | const sprite_frame_summary_t *s = &f->summary; |
|
| 209 | + | cJSON_AddNumberToObject(o, "frame", s->frame); |
|
| 210 | + | cJSON_AddNumberToObject(o, "peakLine", s->peak_line); |
|
| 211 | + | cJSON_AddNumberToObject(o, "peakBudget", s->peak_budget); |
|
| 212 | + | cJSON_AddNumberToObject(o, "peakDemand", s->peak_demand); |
|
| 213 | + | cJSON_AddNumberToObject(o, "linesExhausted", s->lines_exhausted); |
|
| 214 | + | cJSON_AddNumberToObject(o, "spritesDropped", s->sprites_dropped); |
|
| 215 | + | cJSON_AddNumberToObject(o, "budgetUsedTotal", s->budget_used_total); |
|
| 216 | + | cJSON_AddNumberToObject(o, "slotsUsed", s->slots_used); |
|
| 217 | + | cJSON_AddNumberToObject(o, "slotsMultiplexed", s->slots_multiplexed); |
|
| 218 | + | cJSON_AddNumberToObject(o, "effectiveSprites", s->effective_sprites); |
|
| 219 | + | cJSON_AddNumberToObject(o, "cpuCycles", s->cpu_cycles); |
|
| 220 | + | cJSON_AddBoolToObject(o, "multiplexing", s->multiplexing); |
|
| 221 | + | if (lines && f->lines) { |
|
| 222 | + | cJSON *a = cJSON_CreateArray(); |
|
| 223 | + | for (int i = 0; i < SPRITE_TRACE_LINES; i++) |
|
| 224 | + | cJSON_AddItemToArray(a, cJSON_CreateNumber(f->lines[i].budget_used)); |
|
| 225 | + | cJSON_AddItemToObject(o, "lines", a); |
|
| 226 | + | } |
|
| 227 | + | return o; |
|
| 228 | + | } |
added src/perf_json.h +41 −0
| 1 | + | // perf_json.h -- the performance measurements as JSON, shared by the two |
|
| 2 | + | // remote surfaces that report them: the DAP server's x16/perfStats and |
|
| 3 | + | // x16/perfConfig (debug_server.c) and the RRDC control port's GET and POST |
|
| 4 | + | // /perf (control_backend_perf.c). One builder each, so a field means the same |
|
| 5 | + | // thing whichever way a client asked. Needs cJSON: built with HAS_DAP. |
|
| 6 | + | #ifndef PERF_JSON_H |
|
| 7 | + | #define PERF_JSON_H |
|
| 8 | + | ||
| 9 | + | #include <stdbool.h> |
|
| 10 | + | #include <stddef.h> |
|
| 11 | + | #include <cjson/cJSON.h> |
|
| 12 | + | ||
| 13 | + | #include "perf_budget.h" |
|
| 14 | + | ||
| 15 | + | #ifdef __cplusplus |
|
| 16 | + | extern "C" { |
|
| 17 | + | #endif |
|
| 18 | + | ||
| 19 | + | cJSON *perf_json_stat(const perf_stat_t *s); |
|
| 20 | + | // "30s", "session" (seconds <= 0). |
|
| 21 | + | void perf_json_window_name(float seconds, char *out, size_t outsz); |
|
| 22 | + | // A number of seconds, or a name as perf_json_window_name gives it. |
|
| 23 | + | float perf_json_window_seconds(const cJSON *item); |
|
| 24 | + | // One statistics window, as perfStats' windows[] entry; NULL if the window |
|
| 25 | + | // could not be computed (profiling off). |
|
| 26 | + | cJSON *perf_json_window(float seconds); |
|
| 27 | + | cJSON *perf_json_budget(void); |
|
| 28 | + | cJSON *perf_json_frame(const perf_frame_t *f); |
|
| 29 | + | // VERA's bus for the last completed frame; `lines` adds each active line's |
|
| 30 | + | // clocks (480 numbers). |
|
| 31 | + | cJSON *perf_json_bandwidth(void); |
|
| 32 | + | cJSON *perf_json_bandwidth_lines(void); |
|
| 33 | + | // The sprite renderer's time for the last completed frame (sprite_trace); |
|
| 34 | + | // {"enabled":false} while tracing is off. `lines` adds each line's budget used. |
|
| 35 | + | cJSON *perf_json_sprites(bool lines); |
|
| 36 | + | ||
| 37 | + | #ifdef __cplusplus |
|
| 38 | + | } |
|
| 39 | + | #endif |
|
| 40 | + | ||
| 41 | + | #endif |
modified CMakeLists.txt +11 −1
| … | 371 unchanged lines |
||
| 372 | 372 | endif() |
|
| 373 | 373 | ||
| 374 | 374 | if(USE_DAP) |
|
| 375 | - | list(APPEND X16_SOURCES src/debug_server.c) |
|
| 375 | + | list(APPEND X16_SOURCES src/debug_server.c src/perf_json.c) |
|
| 376 | 376 | endif() |
|
| 377 | 377 | ||
| 378 | + | # The Retro Remote Debug Controller's control port (-controlport): the HTTP |
|
| 379 | + | # contract a test harness drives, served by the vendored core over |
|
| 380 | + | # control_backend.c. /perf, its performance metrics, shares perf_json.c with |
|
| 381 | + | # the DAP server and so comes with it (USE_DAP); without cJSON the port serves |
|
| 382 | + | # contract 0.5. The core is POSIX: on Windows its entry points are no-ops. |
|
| 383 | + | list(APPEND X16_SOURCES |
|
| 384 | + | src/control_backend.c |
|
| 385 | + | src/extern/retro-remote-debug-controller/core/retro_control.c) |
|
| 386 | + | ||
| 378 | 387 | # --- Main Executable Target --- |
|
| 379 | 388 | # Guarded so a tests-only build skips the emulator and its SDL dependency. The |
|
| 380 | 389 | # body is left un-indented deliberately: CMake ignores whitespace, and |
|
| … | 13 unchanged lines |
||
| 394 | 403 | src |
|
| 395 | 404 | src/extern/include |
|
| 396 | 405 | src/extern/ymfm/src |
|
| 406 | + | src/extern/retro-remote-debug-controller/core |
|
| 397 | 407 | ${GIT_REV_HEADER_DIR} |
|
| 398 | 408 | ) |
|
| 399 | 409 | ||
| … | 576 unchanged lines |
modified README.md +24 −0
| … | 1094 unchanged lines |
||
| 1095 | 1095 | panel is open by default, so `-imgui` profiles from boot and closing the tab stops it; without the |
|
| 1096 | 1096 | panel, nothing is collected until a DAP client asks. |
|
| 1097 | 1097 | ||
| 1098 | + | #### The control port: the same numbers to a test harness |
|
| 1099 | + | ||
| 1100 | + | `-controlport <port>` serves the [Retro Remote Debug Controller](https://oxygenhub.io/doomsdayone/retro-remote-debug-controller) |
|
| 1101 | + | HTTP contract on `127.0.0.1:<port>`: the surface a pytest harness drives to read memory, step |
|
| 1102 | + | frames deterministically, take screenshots and inject keys, pads and the pointer. It is the plain |
|
| 1103 | + | fork's control port with one more level, **contract 0.6**, which adds the profiler: |
|
| 1104 | + | ||
| 1105 | + | | Request | Effect | |
|
| 1106 | + | | --- | --- | |
|
| 1107 | + | | `GET /perf?windows=1,30,session&frames=n&since=f` | The budget, the last completed frame (total, work, idle, IRQ, main), the statistics windows, the zones with their own windows, raw per-frame records (`since=` the last frame you saw, so none is missed), and `platform`: VERA's `bandwidth` (with `lines=1`, each scanline's clocks) and the sprite renderer's time per line (`sprites`). | |
|
| 1108 | + | | `POST /perf` | A JSON body: `enabled`, `targetFps`, `idleMode`, `historyFrames`, `reset`, `zones` (address ranges, or `{"name", "marker"}` to name a guest's `$9FBC` region), and `sprites` (sprite render timing, off until asked). | |
|
| 1109 | + | ||
| 1110 | + | The fields are `x16/perfStats`'s, from the same builders, so a number means the same thing |
|
| 1111 | + | whichever way a client asked. `/perf` needs cJSON, as DAP does; without it the port serves 0.5. |
|
| 1112 | + | ||
| 1113 | + | ``` |
|
| 1114 | + | SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy ./x16emu -prg GAME.PRG -run -controlport 8391 & |
|
| 1115 | + | curl -s -XPOST localhost:8391/perf -d '{"enabled":true,"zones":[{"name":"logic","marker":1}]}' |
|
| 1116 | + | curl -s 'localhost:8391/perf?windows=1,session&frames=60' |
|
| 1117 | + | ``` |
|
| 1118 | + | ||
| 1119 | + | Run it with a display driver, dummy or real, not `-headless`: the port's `/step` counts video |
|
| 1120 | + | frames, which a headless run never completes. |
|
| 1121 | + | ||
| 1098 | 1122 | #### x16dbg, the bundled command-line client |
|
| 1099 | 1123 | ||
| 1100 | 1124 | `tools/x16dbg` is a small .NET 8 console DAP client, useful for poking at a running emulator |
|
| … | 98 unchanged lines |
modified docs/perf-budget.md +21 −4
| … | 79 unchanged lines |
||
| 80 | 80 | lose automatic detection for the rest of the session. |
|
| 81 | 81 | ||
| 82 | 82 | A region id first seen here claims a zone slot named `region <n>`, so marker |
|
| 83 | - | regions and address zones appear in one breakdown rather than two. |
|
| 83 | + | regions and address zones appear in one breakdown rather than two. A client can |
|
| 84 | + | name a region before the guest first enters it — RRDC's `POST /perf` with |
|
| 85 | + | `{"name": "sound", "marker": 7}` — so the breakdown reads in the program's own |
|
| 86 | + | words. |
|
| 84 | 87 | ||
| 88 | + | **A frame of the guest's own.** The module's frame runs vsync to vsync; a game's |
|
| 89 | + | frame starts wherever it waits for the vertical blank, so its work routinely runs |
|
| 90 | + | across the module's boundary. A guest that frames its work with `$F0` … `$F1` |
|
| 91 | + | keeps its open regions across that boundary — the phase running there keeps its |
|
| 92 | + | cycles — until its own next `$F0` or `$F1`. (Closing them at the boundary handed |
|
| 93 | + | the rest of whichever phase was running to no zone at all: Metal Gear's "common" |
|
| 94 | + | phase read 2.8 scanlines where the guest's own cycle counter said 4.3.) |
|
| 95 | + | ||
| 85 | 96 | Unbalanced writes are tolerated rather than rejected — leaving a region that is |
|
| 86 | - | not open is ignored, and a region still open when the frame ends is closed by the |
|
| 87 | - | frame boundary. A guest that forgets a leave mis-attributes one frame, not every |
|
| 88 | - | frame after it. |
|
| 97 | + | not open is ignored. A framed guest that forgets a leave mis-attributes the rest |
|
| 98 | + | of one of its frames, up to its next `$F0`; a guest that never writes `$F0` has a |
|
| 99 | + | region still open at the frame boundary closed there. Either way it costs one |
|
| 100 | + | frame, not every frame after it. |
|
| 101 | + | ||
| 102 | + | The cycles after `$F1` are waiting — except an interrupt handler's, which are |
|
| 103 | + | work in marker mode as in every other (see *Interrupts*). |
|
| 89 | 104 | ||
| 90 | 105 | ### 2. Zones — exact, no guest changes |
|
| 91 | 106 | ||
| … | 255 unchanged lines |
||
| 347 | 362 | | `src/memory.c` | The store signal the spin detector needs, and the `$9FBC` register. | |
|
| 348 | 363 | | `src/video.c` | Per-scanline layer modes, data-port traffic, and the VERA frame boundary. | |
|
| 349 | 364 | | `src/debug_ui/panels/perf_panel.cpp` | The Performance panel, CPU and bandwidth in one place. | |
|
| 365 | + | | `src/perf_json.{c,h}` | The measurements as JSON, one builder each for the two remote surfaces below. | |
|
| 350 | 366 | | `src/debug_server.c` | `x16/perfStats`, `x16/perfConfig`, and the `x16/perfBudgetOverrun` event. | |
|
| 367 | + | | `src/control_backend.c` | The RRDC control port's `GET`/`POST /perf` (contract 0.6), beside its memory, stepping and input. | |
|
| 351 | 368 | | `tests/test_perf_budget.c` | The classification and statistics, driven directly. | |
|
| 352 | 369 | | `tests/test_vera_bandwidth.c` | The fetch model, checked against the RTL's decode tables. | |
|
| 353 | 370 | | `testbench/test_dap.py` | Both DAP surfaces against a real running machine. | |
|
| … | 1 unchanged lines |
modified src/audio.c +3 −0
| … | 13 unchanged lines |
||
| 14 | 14 | #include <string.h> |
|
| 15 | 15 | #include <stdlib.h> |
|
| 16 | 16 | ||
| 17 | + | void x16_control_push_audio(int16_t l, int16_t r); /* control_backend.c */ |
|
| 18 | + | ||
| 17 | 19 | #ifdef __EMSCRIPTEN__ |
|
| 18 | 20 | #define SAMPLES_PER_BUFFER (1024) |
|
| 19 | 21 | #define SAMP_POS_FRAC_BITS (22) |
|
| … | 370 unchanged lines |
||
| 390 | 392 | } |
|
| 391 | 393 | buffer[wridx++] = (int16_t)((mix_l * limiter_amp) >> 16); |
|
| 392 | 394 | buffer[wridx++] = (int16_t)((mix_r * limiter_amp) >> 16); |
|
| 395 | + | x16_control_push_audio(buffer[wridx - 2], buffer[wridx - 1]); /* the control port's /audio */ |
|
| 393 | 396 | if (limiter_amp < (1 << 16)) limiter_amp++; |
|
| 394 | 397 | vera_samp_pos_rd = (vera_samp_pos_rd + vera_samps_per_host_samps) & SAMP_POS_MASK_FRAC; |
|
| 395 | 398 | ym_samp_pos_rd = (ym_samp_pos_rd + ym_samps_per_host_samps) & SAMP_POS_MASK_FRAC; |
|
| … | 52 unchanged lines |
modified src/debug_server.c +15 −161
| … | 9 unchanged lines |
||
| 10 | 10 | #include "code_map.h" |
|
| 11 | 11 | #include "perf_budget.h" |
|
| 12 | 12 | #include "vera_bandwidth.h" |
|
| 13 | + | #include "perf_json.h" |
|
| 13 | 14 | #include "glue.h" |
|
| 14 | 15 | #include "memory.h" |
|
| 15 | 16 | #include "source_view.h" |
|
| … | 2068 unchanged lines |
||
| 2084 | 2085 | // and the server is polled from the emulation thread, so the numbers cannot be |
|
| 2085 | 2086 | // torn by a concurrent update. |
|
| 2086 | 2087 | ||
| 2087 | - | static cJSON *perf_stat_json(const perf_stat_t *s) { |
|
| 2088 | - | cJSON *o = cJSON_CreateObject(); |
|
| 2089 | - | cJSON_AddNumberToObject(o, "min", s->min); |
|
| 2090 | - | cJSON_AddNumberToObject(o, "mean", s->mean); |
|
| 2091 | - | cJSON_AddNumberToObject(o, "p50", s->p50); |
|
| 2092 | - | cJSON_AddNumberToObject(o, "p95", s->p95); |
|
| 2093 | - | cJSON_AddNumberToObject(o, "p99", s->p99); |
|
| 2094 | - | cJSON_AddNumberToObject(o, "max", s->max); |
|
| 2095 | - | return o; |
|
| 2096 | - | } |
|
| 2097 | - | ||
| 2098 | - | static void perf_window_name(float seconds, char *out, size_t outsz) { |
|
| 2099 | - | if (seconds <= 0.0f) snprintf(out, outsz, "session"); |
|
| 2100 | - | else snprintf(out, outsz, "%gs", (double)seconds); |
|
| 2101 | - | } |
|
| 2102 | - | ||
| 2103 | - | // Accepts either a number of seconds or a name ("30s", "session"), so a client |
|
| 2104 | - | // can send back the same window labels it was given. |
|
| 2105 | - | static float perf_window_seconds(const cJSON *item) { |
|
| 2106 | - | if (cJSON_IsNumber(item)) return (float)item->valuedouble; |
|
| 2107 | - | if (cJSON_IsString(item) && item->valuestring) { |
|
| 2108 | - | if (!strcmp(item->valuestring, "session")) return 0.0f; |
|
| 2109 | - | return (float)atof(item->valuestring); // "30s" -> 30 |
|
| 2110 | - | } |
|
| 2111 | - | return 0.0f; |
|
| 2112 | - | } |
|
| 2113 | - | ||
| 2114 | - | static cJSON *perf_budget_json(void) { |
|
| 2115 | - | cJSON *b = cJSON_CreateObject(); |
|
| 2116 | - | cJSON_AddBoolToObject(b, "enabled", perf_budget_is_enabled()); |
|
| 2117 | - | cJSON_AddNumberToObject(b, "targetFps", perf_budget_get_target_fps()); |
|
| 2118 | - | cJSON_AddNumberToObject(b, "vsyncHz", perf_budget_vsync_hz()); |
|
| 2119 | - | cJSON_AddNumberToObject(b, "machineKhz", (double)MHZ * 1000.0); |
|
| 2120 | - | cJSON_AddNumberToObject(b, "cyclesPerFrame", perf_budget_cycles_per_frame()); |
|
| 2121 | - | cJSON_AddNumberToObject(b, "cyclesPerScanline", perf_budget_cycles_per_scanline()); |
|
| 2122 | - | cJSON_AddNumberToObject(b, "budgetCycles", perf_budget_budget_cycles()); |
|
| 2123 | - | cJSON_AddNumberToObject(b, "framesPerBudget", perf_budget_frames_per_budget()); |
|
| 2124 | - | cJSON_AddNumberToObject(b, "historyFrames", perf_budget_capacity()); |
|
| 2125 | - | cJSON_AddNumberToObject(b, "totalFrames", perf_budget_total_frames()); |
|
| 2126 | - | const char *mode = "auto"; |
|
| 2127 | - | switch (perf_budget_get_idle_mode()) { |
|
| 2128 | - | case PERF_IDLE_MARKERS: mode = "markers"; break; |
|
| 2129 | - | case PERF_IDLE_NONE: mode = "none"; break; |
|
| 2130 | - | default: mode = "auto"; break; |
|
| 2131 | - | } |
|
| 2132 | - | cJSON_AddStringToObject(b, "idleMode", mode); |
|
| 2133 | - | cJSON_AddBoolToObject(b, "warpMode", warp_mode); |
|
| 2134 | - | return b; |
|
| 2135 | - | } |
|
| 2136 | - | ||
| 2137 | - | static cJSON *perf_frame_json(const perf_frame_t *f) { |
|
| 2138 | - | cJSON *o = cJSON_CreateObject(); |
|
| 2139 | - | const uint32_t budget = perf_budget_budget_cycles(); |
|
| 2140 | - | cJSON_AddNumberToObject(o, "frame", f->frame); |
|
| 2141 | - | cJSON_AddNumberToObject(o, "totalCycles", f->total); |
|
| 2142 | - | cJSON_AddNumberToObject(o, "workCycles", perf_frame_work(f)); |
|
| 2143 | - | cJSON_AddNumberToObject(o, "idleCycles", f->idle); |
|
| 2144 | - | cJSON_AddNumberToObject(o, "irqCycles", f->irq); |
|
| 2145 | - | cJSON_AddNumberToObject(o, "mainCycles", |
|
| 2146 | - | perf_frame_work(f) > f->irq ? perf_frame_work(f) - f->irq : 0); |
|
| 2147 | - | cJSON_AddNumberToObject(o, "budgetWorkCycles", f->period_work); |
|
| 2148 | - | cJSON_AddNumberToObject(o, "instructions", f->instructions); |
|
| 2149 | - | cJSON_AddNumberToObject(o, "hostUs", f->host_us); |
|
| 2150 | - | cJSON_AddNumberToObject(o, "utilizationPct", |
|
| 2151 | - | budget ? 100.0 * (double)f->period_work / (double)budget : 0.0); |
|
| 2152 | - | cJSON_AddBoolToObject(o, "overBudget", budget && f->period_work > budget); |
|
| 2153 | - | return o; |
|
| 2154 | - | } |
|
| 2155 | - | ||
| 2156 | - | // VERA's side of the same question. Deliberately a sub-object of perfStats |
|
| 2157 | - | // rather than a request of its own: a developer asking "what is eating my |
|
| 2158 | - | // frame?" should get the CPU answer and the bus answer in one round trip, |
|
| 2159 | - | // because the interesting cases are the ones where the two disagree. |
|
| 2160 | - | // |
|
| 2161 | - | // The headline is peakLineClocks against lineClocks (800), NOT a percentage of |
|
| 2162 | - | // VERA's total bandwidth. A frame uses maybe a fifth of the chip's 100 MB/s and |
|
| 2163 | - | // reporting that would be true and useless -- the scanline is the window that |
|
| 2164 | - | // actually runs out. See src/vera_bandwidth.h. |
|
| 2165 | - | static cJSON *vera_bandwidth_json(void) { |
|
| 2166 | - | cJSON *o = cJSON_CreateObject(); |
|
| 2167 | - | cJSON_AddBoolToObject(o, "enabled", vera_bandwidth_is_enabled()); |
|
| 2168 | - | cJSON_AddNumberToObject(o, "lineClocks", VERA_BW_LINE_CLOCKS); |
|
| 2169 | - | cJSON_AddNumberToObject(o, "bytesPerAccess", VERA_BW_BYTES_PER_ACCESS); |
|
| 2170 | - | cJSON_AddNumberToObject(o, "accessClocks", VERA_BW_ACCESS_CLOCKS); |
|
| 2171 | - | cJSON_AddNumberToObject(o, "scanlinesPerFrame", vera_bandwidth_scanlines_per_frame()); |
|
| 2172 | - | ||
| 2173 | - | vera_bw_frame_t f; |
|
| 2174 | - | if (!vera_bandwidth_last_frame(&f)) { |
|
| 2175 | - | return o; |
|
| 2176 | - | } |
|
| 2177 | - | ||
| 2178 | - | cJSON_AddNumberToObject(o, "frame", f.frame); |
|
| 2179 | - | cJSON_AddNumberToObject(o, "linesActive", f.lines_active); |
|
| 2180 | - | ||
| 2181 | - | cJSON *layers = cJSON_CreateArray(); |
|
| 2182 | - | for (int i = 0; i < 2; i++) { |
|
| 2183 | - | cJSON *l = cJSON_CreateObject(); |
|
| 2184 | - | cJSON_AddNumberToObject(l, "fetches", f.layer_fetches[i]); |
|
| 2185 | - | cJSON_AddNumberToObject(l, "bytes", f.layer_bytes[i]); |
|
| 2186 | - | cJSON_AddNumberToObject(l, "peakLine", f.layer_peak_line[i]); |
|
| 2187 | - | cJSON_AddNumberToObject(l, "peakFetches", f.layer_peak_fetches[i]); |
|
| 2188 | - | cJSON_AddItemToArray(layers, l); |
|
| 2189 | - | } |
|
| 2190 | - | cJSON_AddItemToObject(o, "layers", layers); |
|
| 2191 | - | ||
| 2192 | - | // Per-scanline pressure: the headline. |
|
| 2193 | - | cJSON_AddNumberToObject(o, "peakLine", f.peak_line); |
|
| 2194 | - | cJSON_AddNumberToObject(o, "peakLineClocks", f.peak_clocks); |
|
| 2195 | - | cJSON_AddNumberToObject(o, "p95LineClocks", f.p95_clocks); |
|
| 2196 | - | cJSON_AddNumberToObject(o, "meanLineClocks", f.mean_clocks); |
|
| 2197 | - | cJSON_AddNumberToObject(o, "linesOverBudget", f.lines_over); |
|
| 2198 | - | cJSON_AddNumberToObject(o, "peakLinePct", |
|
| 2199 | - | 100.0 * (double)f.peak_clocks / (double)VERA_BW_LINE_CLOCKS); |
|
| 2200 | - | cJSON_AddNumberToObject(o, "totalFetches", f.total_fetches); |
|
| 2201 | - | cJSON_AddNumberToObject(o, "totalClocks", f.total_clocks); |
|
| 2202 | - | ||
| 2203 | - | // How much of each scanline was still free when the layers and the CPU had |
|
| 2204 | - | // taken their share. Sprites are last on the bus (vram_if.v:142-157), so |
|
| 2205 | - | // this is what they were really competing for. |
|
| 2206 | - | // |
|
| 2207 | - | // Named "bus" deliberately. Every clock figure in this object is BUS |
|
| 2208 | - | // OCCUPANCY, which is not the clock the sprite views report: sprite render |
|
| 2209 | - | // time is wall clock and runs about five times higher, so a client that |
|
| 2210 | - | // subtracted one from the other would conclude a comfortable line was |
|
| 2211 | - | // oversubscribed. Convert sprite width and colour depth to fetches first. |
|
| 2212 | - | cJSON_AddNumberToObject(o, "spriteBusHeadroomClocksAtPeak", |
|
| 2213 | - | f.peak_clocks < VERA_BW_LINE_CLOCKS |
|
| 2214 | - | ? VERA_BW_LINE_CLOCKS - f.peak_clocks : 0); |
|
| 2215 | - | cJSON_AddStringToObject(o, "clockUnits", "bus"); |
|
| 2216 | - | ||
| 2217 | - | cJSON *port = cJSON_CreateObject(); |
|
| 2218 | - | cJSON_AddNumberToObject(port, "readBytes", f.port_reads); |
|
| 2219 | - | cJSON_AddNumberToObject(port, "writeBytes", f.port_writes); |
|
| 2220 | - | cJSON_AddNumberToObject(port, "accesses", f.port_accesses); |
|
| 2221 | - | cJSON_AddNumberToObject(port, "vramBytes", f.port_vram_bytes); |
|
| 2222 | - | cJSON_AddNumberToObject(port, "fxCacheWrites", f.fx_cache_writes); |
|
| 2223 | - | cJSON_AddNumberToObject(port, "fxAffineFetches", f.fx_affine_fetches); |
|
| 2224 | - | // Bytes that reached VRAM per byte the CPU pushed. Above 1.0 means FX is |
|
| 2225 | - | // doing work the CPU did not pay for, which is the point of using it. |
|
| 2226 | - | const uint32_t stores = f.port_reads + f.port_writes; |
|
| 2227 | - | cJSON_AddNumberToObject(port, "amplification", |
|
| 2228 | - | stores ? (double)f.port_vram_bytes / (double)stores : 0.0); |
|
| 2229 | - | cJSON_AddItemToObject(o, "port", port); |
|
| 2230 | - | ||
| 2231 | - | return o; |
|
| 2232 | - | } |
|
| 2233 | - | ||
| 2234 | 2088 | static int handle_x16_perf_stats(int seq, cJSON *args) { |
|
| 2235 | 2089 | // A client asking for statistics is a client that wants them collected. |
|
| 2236 | 2090 | // Arming here rather than making it a separate round trip means the common |
|
| … | 6 unchanged lines |
||
| 2243 | 2097 | } |
|
| 2244 | 2098 | ||
| 2245 | 2099 | cJSON *body = cJSON_CreateObject(); |
|
| 2246 | - | cJSON_AddItemToObject(body, "budget", perf_budget_json()); |
|
| 2247 | - | cJSON_AddItemToObject(body, "bandwidth", vera_bandwidth_json()); |
|
| 2100 | + | cJSON_AddItemToObject(body, "budget", perf_json_budget()); |
|
| 2101 | + | cJSON_AddItemToObject(body, "bandwidth", perf_json_bandwidth()); |
|
| 2248 | 2102 | ||
| 2249 | 2103 | perf_frame_t last; |
|
| 2250 | 2104 | if (perf_budget_last_frame(&last)) { |
|
| 2251 | - | cJSON_AddItemToObject(body, "current", perf_frame_json(&last)); |
|
| 2105 | + | cJSON_AddItemToObject(body, "current", perf_json_frame(&last)); |
|
| 2252 | 2106 | } |
|
| 2253 | 2107 | ||
| 2254 | 2108 | // Windows: whatever the client asked for, else a useful default spread. |
|
| … | 3 unchanged lines |
||
| 2258 | 2112 | const int nreq = (req && cJSON_IsArray(req)) ? cJSON_GetArraySize(req) : 0; |
|
| 2259 | 2113 | const int nwin = nreq > 0 ? nreq : (int)(sizeof defaults / sizeof defaults[0]); |
|
| 2260 | 2114 | for (int i = 0; i < nwin; i++) { |
|
| 2261 | - | const float secs = nreq > 0 ? perf_window_seconds(cJSON_GetArrayItem(req, i)) |
|
| 2115 | + | const float secs = nreq > 0 ? perf_json_window_seconds(cJSON_GetArrayItem(req, i)) |
|
| 2262 | 2116 | : defaults[i]; |
|
| 2263 | 2117 | perf_window_t w; |
|
| 2264 | 2118 | if (!perf_budget_window(secs, &w)) continue; |
|
| 2265 | 2119 | ||
| 2266 | 2120 | char name[32]; |
|
| 2267 | - | perf_window_name(secs, name, sizeof name); |
|
| 2121 | + | perf_json_window_name(secs, name, sizeof name); |
|
| 2268 | 2122 | cJSON *o = cJSON_CreateObject(); |
|
| 2269 | 2123 | cJSON_AddStringToObject(o, "name", name); |
|
| 2270 | 2124 | cJSON_AddNumberToObject(o, "seconds", secs); |
|
| 2271 | 2125 | cJSON_AddNumberToObject(o, "frames", w.frames); |
|
| 2272 | 2126 | cJSON_AddNumberToObject(o, "budgetCycles", w.budget_cycles); |
|
| 2273 | - | cJSON_AddItemToObject(o, "work", perf_stat_json(&w.work)); |
|
| 2274 | - | cJSON_AddItemToObject(o, "budgetWork", perf_stat_json(&w.budget_work)); |
|
| 2275 | - | cJSON_AddItemToObject(o, "total", perf_stat_json(&w.total)); |
|
| 2276 | - | cJSON_AddItemToObject(o, "irq", perf_stat_json(&w.irq)); |
|
| 2277 | - | cJSON_AddItemToObject(o, "hostUs", perf_stat_json(&w.host_us)); |
|
| 2127 | + | cJSON_AddItemToObject(o, "work", perf_json_stat(&w.work)); |
|
| 2128 | + | cJSON_AddItemToObject(o, "budgetWork", perf_json_stat(&w.budget_work)); |
|
| 2129 | + | cJSON_AddItemToObject(o, "total", perf_json_stat(&w.total)); |
|
| 2130 | + | cJSON_AddItemToObject(o, "irq", perf_json_stat(&w.irq)); |
|
| 2131 | + | cJSON_AddItemToObject(o, "hostUs", perf_json_stat(&w.host_us)); |
|
| 2278 | 2132 | cJSON_AddNumberToObject(o, "overruns", w.overruns); |
|
| 2279 | 2133 | cJSON_AddNumberToObject(o, "overrunPct", w.overrun_pct); |
|
| 2280 | 2134 | cJSON_AddNumberToObject(o, "worstOverrunCycles", w.worst_overrun); |
|
| … | 31 unchanged lines |
||
| 2312 | 2166 | } |
|
| 2313 | 2167 | perf_stat_t st; |
|
| 2314 | 2168 | if (perf_budget_zone_window(i, 30.0f, &st)) |
|
| 2315 | - | cJSON_AddItemToObject(o, "window30s", perf_stat_json(&st)); |
|
| 2169 | + | cJSON_AddItemToObject(o, "window30s", perf_json_stat(&st)); |
|
| 2316 | 2170 | cJSON_AddItemToArray(zones, o); |
|
| 2317 | 2171 | } |
|
| 2318 | 2172 | cJSON_AddItemToObject(body, "zones", zones); |
|
| … | 9 unchanged lines |
||
| 2328 | 2182 | for (int i = want - 1; i >= 0; i--) { |
|
| 2329 | 2183 | perf_frame_t f; |
|
| 2330 | 2184 | if (perf_budget_frame_at(i, &f)) |
|
| 2331 | - | cJSON_AddItemToArray(frames, perf_frame_json(&f)); |
|
| 2185 | + | cJSON_AddItemToArray(frames, perf_json_frame(&f)); |
|
| 2332 | 2186 | } |
|
| 2333 | 2187 | cJSON_AddItemToObject(body, "frames", frames); |
|
| 2334 | 2188 | } |
|
| … | 83 unchanged lines |
||
| 2418 | 2272 | } |
|
| 2419 | 2273 | ||
| 2420 | 2274 | cJSON *body = cJSON_CreateObject(); |
|
| 2421 | - | cJSON_AddItemToObject(body, "budget", perf_budget_json()); |
|
| 2422 | - | cJSON_AddItemToObject(body, "bandwidth", vera_bandwidth_json()); |
|
| 2275 | + | cJSON_AddItemToObject(body, "budget", perf_json_budget()); |
|
| 2276 | + | cJSON_AddItemToObject(body, "bandwidth", perf_json_bandwidth()); |
|
| 2423 | 2277 | cJSON_AddBoolToObject(body, "overrunEvents", perf_overrun_events); |
|
| 2424 | 2278 | send_dap_response(seq, "x16/perfConfig", true, body); |
|
| 2425 | 2279 | return 0; |
|
| … | 901 unchanged lines |
modified src/main.c +40 −1
| … | 39 unchanged lines |
||
| 40 | 40 | #include "source_view.h" |
|
| 41 | 41 | #include "code_map.h" |
|
| 42 | 42 | #include "perf_budget.h" |
|
| 43 | + | #include "retro_control.h" // the RRDC control port (-controlport) |
|
| 43 | 44 | #include "vera_bandwidth.h" |
|
| 44 | 45 | #include "cpu/irq_ctx.h" |
|
| 45 | 46 | #include "utf8.h" |
|
| … | 58 unchanged lines |
||
| 104 | 105 | // the two UIs can be used together or separately. Always defined, even in a |
|
| 105 | 106 | // build without the UI, so the emulator loop needs no #ifdef. |
|
| 106 | 107 | bool imgui_debugger_enabled = false; |
|
| 108 | + | // The Retro Remote Debug Controller's HTTP port (-controlport), 0 = off. |
|
| 109 | + | // control_backend.c maps it onto the machine; extern/retro-remote-debug- |
|
| 110 | + | // controller serves it. |
|
| 111 | + | int control_port = 0; |
|
| 112 | + | int x16_control_start(int port); |
|
| 113 | + | extern bool control_port_active; |
|
| 107 | 114 | // True when a local interactive debugger was asked for (-debug/-bp/-wp), as |
|
| 108 | 115 | // distinct from the machinery merely being switched on. A DAP client uses this |
|
| 109 | 116 | // to decide whether anyone is left to resume the machine if it disconnects. |
|
| … | 513 unchanged lines |
||
| 623 | 630 | printf("-srcpath <dir>\n"); |
|
| 624 | 631 | printf("\tAdd a directory to search for the source files a .dbg file names.\n"); |
|
| 625 | 632 | printf("\tCan be repeated.\n"); |
|
| 633 | + | printf("-controlport <port>\n"); |
|
| 634 | + | printf("\tServe the Retro Remote Debug Controller's HTTP contract on\n"); |
|
| 635 | + | printf("\t127.0.0.1:<port>, so a test harness can read memory, step frames,\n"); |
|
| 636 | + | printf("\tinject input and read the performance metrics (GET /perf).\n"); |
|
| 626 | 637 | printf("-debugport [port]\n"); |
|
| 627 | 638 | printf("\tServe the Debug Adapter Protocol on <port> (default %d), so an\n", |
|
| 628 | 639 | DEBUG_SERVER_DEFAULT_PORT); |
|
| … | 505 unchanged lines |
||
| 1134 | 1145 | argc--; |
|
| 1135 | 1146 | argv++; |
|
| 1136 | 1147 | dbg_auto_opt = 0; |
|
| 1148 | + | } else if (!strcmp(argv[0], "-controlport")) { |
|
| 1149 | + | argc--; |
|
| 1150 | + | argv++; |
|
| 1151 | + | if (!argc || argv[0][0] == '-') { |
|
| 1152 | + | fprintf(stderr, "-controlport requires a port number\n"); |
|
| 1153 | + | return 1; |
|
| 1154 | + | } |
|
| 1155 | + | control_port = (int)strtol(argv[0], NULL, 0); |
|
| 1156 | + | argc--; |
|
| 1157 | + | argv++; |
|
| 1137 | 1158 | } else if (!strcmp(argv[0], "-debugport")) { |
|
| 1138 | 1159 | argc--; |
|
| 1139 | 1160 | argv++; |
|
| … | 490 unchanged lines |
||
| 1630 | 1651 | ||
| 1631 | 1652 | instruction_counter = 0; |
|
| 1632 | 1653 | ||
| 1654 | + | if (control_port > 0 && x16_control_start(control_port) != 0) { |
|
| 1655 | + | fprintf(stderr, "Failed to start the control port on %d\n", control_port); |
|
| 1656 | + | return 1; |
|
| 1657 | + | } |
|
| 1658 | + | ||
| 1633 | 1659 | #ifdef __EMSCRIPTEN__ |
|
| 1634 | 1660 | emscripten_cancel_main_loop(); |
|
| 1635 | 1661 | emscripten_set_main_loop(emscripten_main_loop, 0, 1); |
|
| … | 537 unchanged lines |
||
| 2173 | 2199 | // breakpoints are the same breakpoints. The guest can clear |
|
| 2174 | 2200 | // debugger_enabled by writing $9FB0, so testing that alone would let a drag |
|
| 2175 | 2201 | // step past a breakpoint a client is waiting on. |
|
| 2176 | - | if (debugger_enabled || imgui_debugger_enabled || debug_server_is_enabled()) { |
|
| 2202 | + | // And the control port: a harness stepping frames owns the machine's time. |
|
| 2203 | + | if (debugger_enabled || imgui_debugger_enabled || debug_server_is_enabled() || control_port_active) { |
|
| 2177 | 2204 | return; |
|
| 2178 | 2205 | } |
|
| 2179 | 2206 | ||
| … | 78 unchanged lines |
||
| 2258 | 2285 | if (emulator_stop_requested) break; |
|
| 2259 | 2286 | ||
| 2260 | 2287 | if (smc_requested_reset) machine_reset(); |
|
| 2288 | + | ||
| 2289 | + | // The control port's requests are served here, between instructions, |
|
| 2290 | + | // so a read sees a whole machine; and its pause holds the CPU here. |
|
| 2291 | + | retro_control_service(); |
|
| 2292 | + | if (!retro_control_running()) { |
|
| 2293 | + | SDL_Delay(1); |
|
| 2294 | + | continue; |
|
| 2295 | + | } |
|
| 2261 | 2296 | ||
| 2262 | 2297 | if (testbench && regs.pc == 0xfffd){ |
|
| 2263 | 2298 | testbench_init(); |
|
| … | 196 unchanged lines |
||
| 2460 | 2495 | // Before timing_update(), which sleeps off whatever surplus the |
|
| 2461 | 2496 | // frame left: the budget wants the cost of the work, not the wait. |
|
| 2462 | 2497 | perf_budget_close_frame(); |
|
| 2498 | + | ||
| 2499 | + | // After the budget closes the frame, so a /step that ends here |
|
| 2500 | + | // finds /perf's current frame is this one. |
|
| 2501 | + | retro_control_on_frame(); |
|
| 2463 | 2502 | ||
| 2464 | 2503 | timing_update(); |
|
| 2465 | 2504 | #ifdef __EMSCRIPTEN__ |
|
| … | 24 unchanged lines |
modified src/perf_budget.c +40 −10
| … | 83 unchanged lines |
||
| 84 | 84 | ||
| 85 | 85 | static bool marker_mode = false; // a marker has been seen this session |
|
| 86 | 86 | static bool marker_idle = false; // $F1 seen; work resumes at $F0 |
|
| 87 | + | static bool marker_framed = false; // $F0 seen and no $F1 since: the guest's frame is open |
|
| 87 | 88 | static int8_t marker_stack[PERF_MARKER_STACK]; |
|
| 88 | 89 | static int marker_depth = 0; |
|
| 89 | 90 | ||
| … | 265 unchanged lines |
||
| 355 | 356 | // by anything short of restarting the emulator. Since a guest that is still |
|
| 356 | 357 | // driving markers re-asserts the mode on its very next write, clearing it |
|
| 357 | 358 | // costs that guest nothing and gives every other one its detector back. |
|
| 358 | - | marker_mode = false; |
|
| 359 | - | marker_idle = false; |
|
| 360 | - | marker_depth = 0; |
|
| 359 | + | marker_mode = false; |
|
| 360 | + | marker_idle = false; |
|
| 361 | + | marker_framed = false; |
|
| 362 | + | marker_depth = 0; |
|
| 361 | 363 | } |
|
| 362 | 364 | ||
| 363 | 365 | // ─── The hot path ─────────────────────────────────────────────────────────── |
|
| … | 101 unchanged lines |
||
| 465 | 467 | if (marker_mode) { |
|
| 466 | 468 | // The guest is telling us. Do not also guess: an explicit statement |
|
| 467 | 469 | // that beats the heuristic is the whole point of the marker layer. |
|
| 468 | - | if (marker_idle || z_idle) |
|
| 470 | + | // Except inside an interrupt handler: the IRQ that lands in the wait |
|
| 471 | + | // after $F1 is the CPU working, not waiting, and counting it idle |
|
| 472 | + | // would make main (work - irq) come up short by the whole handler. |
|
| 473 | + | if ((marker_idle || z_idle) && irq_depth == 0) |
|
| 469 | 474 | cur.idle += clocks; |
|
| 470 | 475 | spin_reset(pc, false); |
|
| 471 | 476 | } else if (z_idle) { |
|
| … | 24 unchanged lines |
||
| 496 | 501 | return; |
|
| 497 | 502 | ||
| 498 | 503 | // A region left open by the guest is closed here rather than carried, so a |
|
| 499 | - | // missing leave costs one frame instead of every frame after it. |
|
| 500 | - | marker_depth = 0; |
|
| 504 | + | // missing leave costs one frame instead of every frame after it -- unless |
|
| 505 | + | // the guest frames its work with $F0 ... $F1 and is inside that frame. |
|
| 506 | + | // Then the region is the guest's own word that its work is still going: |
|
| 507 | + | // a game's frame begins where IT waits, not at this boundary, so its work |
|
| 508 | + | // routinely runs across it, and closing the region here would hand the |
|
| 509 | + | // rest of whichever phase was running to no zone at all. Its next $F0 or |
|
| 510 | + | // $F1 still closes everything, so a missing leave costs one of the |
|
| 511 | + | // guest's frames. |
|
| 512 | + | if (!marker_framed) |
|
| 513 | + | marker_depth = 0; |
|
| 501 | 514 | ||
| 502 | 515 | const uint32_t work = cur.total - cur.idle; |
|
| 503 | 516 | ||
| … | 34 unchanged lines |
||
| 538 | 551 | } |
|
| 539 | 552 | ||
| 540 | 553 | // ─── Markers ──────────────────────────────────────────────────────────────── |
|
| 554 | + | ||
| 555 | + | static int marker_zone(uint8_t id); |
|
| 556 | + | ||
| 557 | + | int |
|
| 558 | + | perf_budget_zone_add_marker(const char *name, uint8_t id) |
|
| 559 | + | { |
|
| 560 | + | if (id < 1 || id > 0x3F) |
|
| 561 | + | return -1; |
|
| 562 | + | const int zi = marker_zone(id); |
|
| 563 | + | if (zi < 0) |
|
| 564 | + | return -1; |
|
| 565 | + | if (name && *name) |
|
| 566 | + | snprintf(zones[zi].name, sizeof zones[zi].name, "%s", name); |
|
| 567 | + | return zi; |
|
| 568 | + | } |
|
| 541 | 569 | ||
| 542 | 570 | // Find (or claim) the zone slot standing for marker region `id`. |
|
| 543 | 571 | static int |
|
| … | 31 unchanged lines |
||
| 575 | 603 | return; |
|
| 576 | 604 | } |
|
| 577 | 605 | if (value == 0xF0) { |
|
| 578 | - | marker_idle = false; |
|
| 579 | - | marker_depth = 0; |
|
| 606 | + | marker_idle = false; |
|
| 607 | + | marker_framed = true; |
|
| 608 | + | marker_depth = 0; |
|
| 580 | 609 | return; |
|
| 581 | 610 | } |
|
| 582 | 611 | if (value == 0xF1) { |
|
| 583 | - | marker_idle = true; |
|
| 584 | - | marker_depth = 0; |
|
| 612 | + | marker_idle = true; |
|
| 613 | + | marker_framed = false; |
|
| 614 | + | marker_depth = 0; |
|
| 585 | 615 | return; |
|
| 586 | 616 | } |
|
| 587 | 617 | if (value == 0x00) { |
|
| … | 347 unchanged lines |
modified src/perf_budget.h +6 −0
| … | 231 unchanged lines |
||
| 232 | 232 | typedef enum { |
|
| 233 | 233 | PERF_OWNER_UI = 0, // the ImGui Performance panel |
|
| 234 | 234 | PERF_OWNER_DAP, // a debug adapter client |
|
| 235 | + | PERF_OWNER_REMOTE, // an RRDC control-port client (GET/POST /perf) |
|
| 235 | 236 | PERF_OWNER_COUNT |
|
| 236 | 237 | } perf_owner_t; |
|
| 237 | 238 | ||
| … | 56 unchanged lines |
||
| 294 | 295 | // Returns the zone index, or -1 if the table is full or the range is empty. |
|
| 295 | 296 | int perf_budget_zone_add(const char *name, uint16_t start, uint16_t end, |
|
| 296 | 297 | int16_t bank, bool is_idle); |
|
| 298 | + | // A zone standing for the guest's marker region `id` ($01-$3F), named now -- |
|
| 299 | + | // before the guest has entered it -- rather than "region <n>" when it does. |
|
| 300 | + | // Returns the zone index (an existing one for `id`, renamed), or -1 if the |
|
| 301 | + | // table is full or `id` is out of range. |
|
| 302 | + | int perf_budget_zone_add_marker(const char *name, uint8_t id); |
|
| 297 | 303 | bool perf_budget_zone_remove(int index); |
|
| 298 | 304 | void perf_budget_zone_clear(void); |
|
| 299 | 305 | int perf_budget_zone_count(void); // highest index in use + 1 |
|
| … | 32 unchanged lines |
modified src/vera_bandwidth.h +1 −0
| … | 308 unchanged lines |
||
| 309 | 309 | typedef enum { |
|
| 310 | 310 | VERA_BW_OWNER_UI = 0, |
|
| 311 | 311 | VERA_BW_OWNER_DAP, |
|
| 312 | + | VERA_BW_OWNER_REMOTE, // an RRDC control-port client (GET/POST /perf) |
|
| 312 | 313 | VERA_BW_OWNER_COUNT |
|
| 313 | 314 | } vera_bw_owner_t; |
|
| 314 | 315 | ||
| … | 34 unchanged lines |
modified src/video.c +15 −0
| … | 1840 unchanged lines |
||
| 1841 | 1841 | ||
| 1842 | 1842 | return new_frame; |
|
| 1843 | 1843 | } |
|
| 1844 | + | /* Control-port accessors (control_backend.c, the RRDC contract). */ |
|
| 1845 | + | const uint8_t * |
|
| 1846 | + | video_get_framebuffer(int *width, int *height) |
|
| 1847 | + | { |
|
| 1848 | + | if (width) *width = SCREEN_WIDTH; |
|
| 1849 | + | if (height) *height = SCREEN_HEIGHT; |
|
| 1850 | + | return framebuffer; |
|
| 1851 | + | } |
|
| 1852 | + | ||
| 1853 | + | uint32_t |
|
| 1854 | + | video_get_frame_count(void) |
|
| 1855 | + | { |
|
| 1856 | + | return (uint32_t)frame_count; |
|
| 1857 | + | } |
|
| 1858 | + | ||
| 1844 | 1859 | ||
| 1845 | 1860 | bool |
|
| 1846 | 1861 | video_get_irq_out() |
|
| … | 1677 unchanged lines |
modified src/video.h +4 −0
| … | 84 unchanged lines |
||
| 85 | 85 | void video_get_irq_state(uint8_t *out_ien, uint8_t *out_isr, uint16_t *out_irq_line); |
|
| 86 | 86 | void video_end(void); |
|
| 87 | 87 | bool video_get_irq_out(void); |
|
| 88 | + | // For the control port (control_backend.c): the last whole frame, BGRA8888, |
|
| 89 | + | // and the count of frames completed. |
|
| 90 | + | const uint8_t *video_get_framebuffer(int *width, int *height); |
|
| 91 | + | uint32_t video_get_frame_count(void); |
|
| 88 | 92 | void video_save(SDL_RWops *f); |
|
| 89 | 93 | uint8_t video_read(uint8_t reg, bool debugOn); |
|
| 90 | 94 | void video_write(uint8_t reg, uint8_t value); |
|
| … | 27 unchanged lines |
modified tests/test_perf_budget.c +81 −1
| … | 278 unchanged lines |
||
| 279 | 279 | check(strcmp(z.name, "region 1") == 0, "and named after it"); |
|
| 280 | 280 | } |
|
| 281 | 281 | ||
| 282 | - | // ── Unbalanced markers cost one frame, not every frame ────────────────── |
|
| 282 | + | // ── A framed guest's work may run across the emulator's frame boundary ── |
|
| 283 | 283 | { |
|
| 284 | + | // A game's frame starts where it waits for the vertical blank, which |
|
| 285 | + | // is not where this module's frame ends. Its phases run straight |
|
| 286 | + | // across the boundary, and the one running there keeps its cycles. |
|
| 284 | 287 | configure(); |
|
| 285 | 288 | perf_budget_marker_write(0xF0); |
|
| 289 | + | perf_budget_marker_write(0x01); // phase 1 |
|
| 290 | + | run_work(4, 10); |
|
| 291 | + | perf_budget_marker_write(0x00); |
|
| 292 | + | perf_budget_marker_write(0x02); // phase 2, running at the boundary |
|
| 293 | + | run_work(4, 10); |
|
| 294 | + | perf_budget_frame_end(0); |
|
| 295 | + | run_work(4, 10); // phase 2, after it |
|
| 296 | + | perf_budget_marker_write(0x00); |
|
| 297 | + | perf_budget_marker_write(0xF1); |
|
| 298 | + | run_work(4, 10); // the wait |
|
| 299 | + | perf_budget_frame_end(0); |
|
| 300 | + | ||
| 301 | + | perf_frame_t f; |
|
| 302 | + | perf_budget_frame_at(1, &f); |
|
| 303 | + | check_eq(f.zone[0], 40, "phase 1 before the boundary"); |
|
| 304 | + | check_eq(f.zone[1], 40, "phase 2's cycles before the boundary"); |
|
| 305 | + | perf_budget_last_frame(&f); |
|
| 306 | + | check_eq(f.zone[1], 40, "and phase 2's after it, carried across"); |
|
| 307 | + | check_eq(f.idle, 40, "the wait after $F1 idle"); |
|
| 308 | + | } |
|
| 309 | + | ||
| 310 | + | // ── An interrupt during the wait is work ──────────────────────────────── |
|
| 311 | + | { |
|
| 312 | + | // After $F1 the guest is waiting; the IRQ handler that runs in that |
|
| 313 | + | // wait is not, and main (work less irq) must not come out short of it. |
|
| 314 | + | configure(); |
|
| 315 | + | perf_budget_marker_write(0xF0); |
|
| 316 | + | run_work(4, 10); |
|
| 317 | + | perf_budget_marker_write(0xF1); |
|
| 318 | + | for (int i = 0; i < 10; i++) perf_budget_step(4, 0x2000, 0, false, 0); // waiting |
|
| 319 | + | for (int i = 0; i < 5; i++) perf_budget_step(6, 0xE000, 0, false, 1); // the IRQ |
|
| 320 | + | perf_budget_frame_end(0); |
|
| 321 | + | perf_frame_t f; |
|
| 322 | + | perf_budget_last_frame(&f); |
|
| 323 | + | check_eq(f.irq, 30, "the handler's cycles are IRQ"); |
|
| 324 | + | check_eq(f.idle, 40, "only the waiting is idle"); |
|
| 325 | + | check_eq(perf_frame_work(&f), 70, "the handler counted as work"); |
|
| 326 | + | } |
|
| 327 | + | ||
| 328 | + | // ── A marker region can be named before the guest enters it ───────────── |
|
| 329 | + | { |
|
| 330 | + | configure(); |
|
| 331 | + | const int zi = perf_budget_zone_add_marker("sound", 7); |
|
| 332 | + | check(zi >= 0, "a named marker region claims a zone slot"); |
|
| 333 | + | perf_zone_t z; |
|
| 334 | + | check(perf_budget_zone_get(zi, &z) && z.is_marker && z.marker_id == 7 && |
|
| 335 | + | !strcmp(z.name, "sound"), "under its name, standing for region 7"); |
|
| 336 | + | perf_budget_marker_write(0xF0); |
|
| 337 | + | perf_budget_marker_write(0x07); |
|
| 338 | + | run_work(4, 5); |
|
| 339 | + | perf_budget_marker_write(0xF1); |
|
| 340 | + | perf_budget_frame_end(0); |
|
| 341 | + | perf_frame_t f; |
|
| 342 | + | perf_budget_last_frame(&f); |
|
| 343 | + | check_eq(f.zone[zi], 20, "and the guest's region 7 lands in it"); |
|
| 344 | + | check(perf_budget_zone_add_marker("x", 0) < 0 && perf_budget_zone_add_marker("x", 0x40) < 0, |
|
| 345 | + | "ids outside $01-$3F are refused"); |
|
| 346 | + | } |
|
| 347 | + | ||
| 348 | + | // ── Unbalanced markers cost one frame, not every frame ────────────────── |
|
| 349 | + | { |
|
| 350 | + | // A guest that does not frame its work ($F0 ... $F1): the boundary is |
|
| 351 | + | // the only thing that can close a region it forgot to leave. |
|
| 352 | + | configure(); |
|
| 286 | 353 | perf_budget_marker_write(0x05); // entered and never left |
|
| 287 | 354 | run_work(4, 10); |
|
| 288 | 355 | perf_budget_frame_end(0); |
|
| … | 4 unchanged lines |
||
| 293 | 360 | perf_frame_t f; |
|
| 294 | 361 | perf_budget_last_frame(&f); |
|
| 295 | 362 | check_eq(f.zone[0], 0, "a region left open is closed by the frame boundary"); |
|
| 363 | + | ||
| 364 | + | // A guest that does frame its work closes it itself, at its next $F0: |
|
| 365 | + | // the missing leave costs one of ITS frames. |
|
| 366 | + | configure(); |
|
| 367 | + | perf_budget_marker_write(0xF0); |
|
| 368 | + | perf_budget_marker_write(0x05); // entered and never left |
|
| 369 | + | run_work(4, 10); |
|
| 370 | + | perf_budget_frame_end(0); |
|
| 371 | + | perf_budget_marker_write(0xF0); // the guest's next frame |
|
| 372 | + | run_work(4, 10); |
|
| 373 | + | perf_budget_frame_end(0); |
|
| 374 | + | perf_budget_last_frame(&f); |
|
| 375 | + | check_eq(f.zone[0], 0, "a framed guest's open region ends at its next $F0"); |
|
| 296 | 376 | ||
| 297 | 377 | // Leaving a region that was never entered is ignored rather than |
|
| 298 | 378 | // corrupting the stack. |
|
| … | 314 unchanged lines |