OXYGENHUB
# 8e8d9cad1884 x16-emulator / src / control_backend.c
10.5 KiB · C · last changed chore(): initial commit 15 days ago source blame history raw
1
/* control_backend.c — Commander X16 backend for the retro remote debug
2
 * controller (retro_control.h, from the retro-remote-debug-controller submodule
3
 * at extern/). Maps the portable read-only backend onto x16emu internals. */
4
5
#include "retro_control.h"
6
7
#include "glue.h"     /* struct regs regs; machine_reset() */
8
#include "memory.h"   /* debug_read6502, write6502, memory_get_ram_bank/rom_bank */
9
#include "video.h"    /* video_get_framebuffer, video_get_frame_count, video_space_read */
10
#include "keyboard.h" /* handle_keyboard (input injection) */
11
#include "i2c.h"      /* mouse_move / mouse_button_* / mouse_send_state (pointer) */
12
#include "audio.h"    /* AUDIO_SAMPLERATE */
13
#include "joystick.h" /* joystick_virtual_set / joystick_virtual_get (pads) */
14
15
#include <SDL.h>
16
#include <stdbool.h>
17
#include <stdio.h>
18
19
extern uint32_t host_sample_rate;   /* audio.c: the real (post-SDL) output rate */
20
21
/* Debug-read into out. bank<0 => current bank. $0000-$9FFF is low RAM
22
 * (bank-independent); $A000-$BFFF is banked RAM; $C000+ is ROM. */
23
static uint32_t
24
x16_read_mem(uint32_t addr, int32_t bank, uint32_t len, uint8_t *out, uint32_t cap)
25
{
26
	int16_t x16bank = (bank >= 0) ? (int16_t)bank : USE_CURRENT_X16_BANK;
27
	uint32_t n = 0;
28
	for (uint32_t i = 0; i < len && n < cap; i++) {
29
		uint16_t a = (uint16_t)(addr + i);
30
		out[n++] = debug_read6502(a, 0, x16bank);
31
	}
32
	return n;
33
}
34
35
/* Write (poke) len bytes at addr (0.3). Low RAM ($0000-$9EFF) goes through the
36
 * same bus a CPU write uses. Banked RAM ($A000-$BFFF) with an explicit bank= is
37
 * written into that bank's backing store directly, the mirror of what
38
 * x16_read_mem does through debug_read6502: write6502's bank argument is the
39
 * gen2 flat bank and is forced to 0 on gen1, so routing a banked poke through
40
 * it wrote whatever bank the CPU had mapped at the pause — the KERNAL's IRQ
41
 * handler maps bank 0, and a harness write for a tenant vanished into it.
42
 * Without bank= the poke still hits the current bank, as a CPU write would. */
43
static uint32_t
44
x16_write_mem(uint32_t addr, int32_t bank, uint32_t len, const uint8_t *in)
45
{
46
	for (uint32_t i = 0; i < len; i++) {
47
		uint16_t a = (uint16_t)(addr + i);
48
		if (bank >= 0 && a >= 0xa000 && a < 0xc000) {
49
			if ((uint32_t)bank < (uint32_t)num_ram_banks) {
50
				BRAM[((uint32_t)bank << 13) + a - 0xa000] = in[i];
51
			}
52
			continue;
53
		}
54
		write6502(a, 0, in[i]);
55
	}
56
	return len;
57
}
58
59
/* --- Audio drain (0.3): a lock-free ring teed from the mixer. The producer
60
 * (audio_render, via x16_control_push_audio) and the consumer (capture_audio)
61
 * both run on the emulator thread, so no locking is needed. --- */
62
#define X16_AUD_RING 65536              /* int16 samples (~0.67 s stereo @ 48.8 kHz) */
63
static int16_t  g_aud[X16_AUD_RING];
64
static uint32_t g_aud_head, g_aud_count, g_aud_dropped;
65
static int      g_aud_active;           /* start capturing only once the server is up */
66
67
/* Called from audio_render() for every mixed stereo pair. */
68
void
69
x16_control_push_audio(int16_t l, int16_t r)
70
{
71
	if (!g_aud_active) return;
72
	if (g_aud_count + 2 > X16_AUD_RING) { g_aud_dropped += 2; return; }
73
	g_aud[(g_aud_head + g_aud_count) % X16_AUD_RING] = l; g_aud_count++;
74
	g_aud[(g_aud_head + g_aud_count) % X16_AUD_RING] = r; g_aud_count++;
75
}
76
77
static uint32_t
78
x16_capture_audio(int16_t *out, uint32_t cap, int *rate, int *channels, uint32_t *dropped)
79
{
80
	uint32_t n = 0;
81
	if (rate)     *rate     = host_sample_rate ? (int)host_sample_rate : (int)AUDIO_SAMPLERATE;
82
	if (channels) *channels = 2;
83
	if (dropped)  *dropped  = g_aud_dropped;
84
	g_aud_dropped = 0;
85
	while (n < cap && g_aud_count) {
86
		out[n++] = g_aud[g_aud_head];
87
		g_aud_head = (g_aud_head + 1) % X16_AUD_RING;
88
		g_aud_count--;
89
	}
90
	return n;
91
}
92
93
static void
94
x16_get_regs_json(char *buf, size_t cap)
95
{
96
	snprintf(buf, cap,
97
		"{\"a\":%u,\"x\":%u,\"y\":%u,\"sp\":%u,\"pc\":%u,\"status\":%u,"
98
		"\"ram_bank\":%u,\"rom_bank\":%u}",
99
		(unsigned)regs.a, (unsigned)regs.xl, (unsigned)regs.yl,
100
		(unsigned)(regs.sp & 0xff), (unsigned)regs.pc, (unsigned)regs.status,
101
		(unsigned)memory_get_ram_bank(), (unsigned)memory_get_rom_bank());
102
}
103
104
static void
105
x16_get_framebuffer(retro_framebuffer_t *out)
106
{
107
	int w = 0, h = 0;
108
	out->pixels = video_get_framebuffer(&w, &h);
109
	out->width = w;
110
	out->height = h;
111
	out->fmt = RETRO_PIX_BGRA8888;   /* x16 framebuffer byte order */
112
}
113
114
static uint64_t
115
x16_get_frame_count(void)
116
{
117
	return video_get_frame_count();
118
}
119
120
/* Input injection (0.2). text => the char's key (letters use the lowercase
121
 * keycode); code => a raw SDL scancode. A tap presses then releases; hold with
122
 * down=1 across /step frames for a program that samples input each frame. */
123
static void
124
x16_key_event(SDL_Keycode sym, SDL_Scancode sc, int action)
125
{
126
	if (action == RETRO_KEY_DOWN) {
127
		handle_keyboard(true, sym, sc);
128
	} else if (action == RETRO_KEY_UP) {
129
		handle_keyboard(false, sym, sc);
130
	} else {                             /* RETRO_KEY_TAP */
131
		handle_keyboard(true, sym, sc);
132
		handle_keyboard(false, sym, sc);
133
	}
134
}
135
136
static int
137
x16_inject_key(int is_text, uint32_t value, int action)
138
{
139
	SDL_Keycode sym;
140
	SDL_Scancode sc;
141
	if (is_text) {
142
		int c = (int)value;
143
		if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a';   /* keycodes are lowercase */
144
		sym = (SDL_Keycode)c;
145
		sc = SDL_GetScancodeFromKey(sym);
146
	} else {
147
		sc = (SDL_Scancode)value;                      /* raw SDL scancode */
148
		sym = SDL_GetKeyFromScancode(sc);
149
	}
150
	if (sc == SDL_SCANCODE_UNKNOWN) {
151
		return 0;
152
	}
153
	x16_key_event(sym, sc, action);
154
	return 1;
155
}
156
157
static void
158
x16_reset(void)
159
{
160
	machine_reset();
161
}
162
163
/* Pointer injection (0.4). The X16 mouse is a relative PS/2 device: the KERNAL
164
 * integrates the deltas into an absolute position and writes it to VERA sprite 0
165
 * (the pointer). So we inject through the same mouse_move/mouse_send_state path a
166
 * host mouse uses (device level, not driver state), and read the position back
167
 * out of sprite 0's attributes. Absolute is closed-loop: read sprite 0, inject
168
 * the delta to the target. The move lands once the KERNAL's next VSYNC consumes
169
 * the packet, so a /step must follow before it is visible — and the guest must
170
 * have the mouse enabled (KERNAL MOUSE / VERA sprite 0) for it to mean anything. */
171
#define VERA_SPRITE0_ATTR 0x1FC00U   /* sprite 0 attribute base in VERA VRAM */
172
173
static int32_t
174
x16_pointer_x(void)
175
{
176
	return (int32_t)video_space_read(VERA_SPRITE0_ATTR + 2)
177
	     | ((int32_t)(video_space_read(VERA_SPRITE0_ATTR + 3) & 3) << 8);
178
}
179
static int32_t
180
x16_pointer_y(void)
181
{
182
	return (int32_t)video_space_read(VERA_SPRITE0_ATTR + 4)
183
	     | ((int32_t)(video_space_read(VERA_SPRITE0_ATTR + 5) & 3) << 8);
184
}
185
186
static int
187
x16_set_pointer(int absolute, int32_t x, int32_t y, int buttons)
188
{
189
	if (absolute) {
190
		mouse_move((int)(x - x16_pointer_x()), (int)(y - x16_pointer_y()));
191
	} else {
192
		mouse_move((int)x, (int)y);            /* relative deltas, screen pixels */
193
	}
194
	if (buttons >= 0) {                        /* -1 => leave held buttons as-is */
195
		if (buttons & 1) mouse_button_down(0); else mouse_button_up(0); /* primary */
196
		if (buttons & 2) mouse_button_down(1); else mouse_button_up(1); /* secondary */
197
	}
198
	mouse_send_state();                        /* enqueue the PS/2 packet(s) */
199
	return 1;                                  /* the mouse device is always present */
200
}
201
202
static int
203
x16_get_pointer(int32_t *x, int32_t *y, int *buttons)
204
{
205
	*x = x16_pointer_x();
206
	*y = x16_pointer_y();
207
	*buttons = (int)(mouse_get_buttons() & 3);  /* bit0 primary, bit1 secondary */
208
	return 1;
209
}
210
211
/* --- Pads (contract 0.5) ---------------------------------------------
212
 *
213
 * RRDC fixes ONE canonical layout across every platform, so a test
214
 * written against one machine reads the same on another:
215
 *
216
 *   bit  0    1     2   3     4  5  6  7  8      9       10  11
217
 *        LEFT RIGHT UP  DOWN  A  B  X  Y  START  SELECT  L   R
218
 *
219
 * The X16's SNES pad disagrees on both counts, and its mask is ACTIVE
220
 * LOW. Converting here is exactly what the spec asks of a backend whose
221
 * hardware differs; the harness never learns about it.
222
 *
223
 * The X16 side is the SNES SERIAL order — the order bits leave the
224
 * shift register, which is what the KERNAL reassembles:
225
 *
226
 *   bit  0  1  2       3      4   5     6     7      8  9  10 11
227
 *        B  Y  SELECT  START  UP  DOWN  LEFT  RIGHT  A  X  L  R
228
 *
229
 * Read that off joystick.c's button_map[] with care: its comments name
230
 * SDL buttons, not SNES ones, and the two disagree where it matters.
231
 * SDL's A is the BOTTOM button, which is SNES B — so button_map's
232
 * `1 << 0 //SDL_CONTROLLER_BUTTON_A` is bit 0 = SNES **B**. Taking
233
 * those comments at face value swaps A<->B and X<->Y, which is exactly
234
 * the bug this table shipped with until it was checked against the
235
 * game's own joystick word rather than against a read-back. */
236
static const uint8_t pad_canon_to_x16[12] = {
237
	6,  /* LEFT   */ 7,  /* RIGHT  */ 4,  /* UP     */ 5,  /* DOWN   */
238
	8,  /* A      */ 0,  /* B      */ 9,  /* X      */ 1,  /* Y      */
239
	3,  /* START  */ 2,  /* SELECT */ 10, /* L      */ 11, /* R      */
240
};
241
242
static uint16_t
243
pad_canon_to_mask(int buttons)
244
{
245
	uint16_t mask = 0xffff;                  /* active low: nothing held */
246
	for (int b = 0; b < 12; ++b) {
247
		if (buttons & (1 << b)) {
248
			mask &= (uint16_t)~(1u << pad_canon_to_x16[b]);
249
		}
250
	}
251
	return mask;
252
}
253
254
static int
255
pad_mask_to_canon(uint16_t mask)
256
{
257
	int buttons = 0;
258
	for (int b = 0; b < 12; ++b) {
259
		if ((mask & (1u << pad_canon_to_x16[b])) == 0) {   /* 0 == held */
260
			buttons |= (1 << b);
261
		}
262
	}
263
	return buttons;
264
}
265
266
static int
267
x16_set_pad(int index, int buttons, int connected)
268
{
269
	/* buttons < 0 => presence-only change, leave the held mask alone. */
270
	return joystick_virtual_set(index, connected != 0, buttons >= 0,
271
	                            pad_canon_to_mask(buttons < 0 ? 0 : buttons))
272
	           ? 1
273
	           : 0;
274
}
275
276
static int
277
x16_get_pad(int index, int *buttons, int *connected)
278
{
279
	bool     present = false;
280
	uint16_t mask    = 0xffff;
281
282
	if (!joystick_virtual_get(index, &present, &mask)) {
283
		return 0;
284
	}
285
	*connected = present ? 1 : 0;
286
	*buttons   = pad_mask_to_canon(mask);
287
	return 1;
288
}
289
290
static const retro_control_backend_t x16_backend = {
291
	.platform        = "x16",
292
	.emulator        = "x16emu",
293
	.read_mem        = x16_read_mem,
294
	.get_regs_json   = x16_get_regs_json,
295
	.get_framebuffer = x16_get_framebuffer,
296
	.get_frame_count = x16_get_frame_count,
297
	.inject_key      = x16_inject_key,
298
	.reset           = x16_reset,
299
	.write_mem       = x16_write_mem,
300
	.capture_audio   = x16_capture_audio,
301
	.set_pointer     = x16_set_pointer,
302
	.get_pointer     = x16_get_pointer,
303
	.set_pad         = x16_set_pad,
304
	.get_pad         = x16_get_pad,
305
};
306
307
int
308
x16_control_start(int port)
309
{
310
	g_aud_active = 1;                 /* begin teeing mixer output into the ring */
311
	return retro_control_start(port, &x16_backend);
312
}