OXYGENHUB

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