-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmspacman_featheresp32.ino
More file actions
256 lines (237 loc) · 12.4 KB
/
Copy pathmspacman_featheresp32.ino
File metadata and controls
256 lines (237 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
// SPDX-FileCopyrightText: 2026 John Park for Adafruit Industries
//
// SPDX-License-Identifier: MIT
// mspacman_featheresp32 -- Ms. Pac-Man on an Adafruit Feather ESP32 V2 with
// the 2.4" TFT FeatherWing. The SAMP composition root for this pair: the
// only place that knows both "this game" and "this board".
//
// Sibling of pacman_featheresp32.ino and otherwise a copy of it: Ms.
// Pac-Man runs on Pac-Man's board with the same controls, so the
// board-to-game wiring is identical. What differs is the machine behind it
// -- an encrypted, banked program ROM on an aux daughterboard.
//
// ~32fps, not 60, and the reason is arithmetic rather than anything
// fixable in this sketch. The wing's SPI pins are not the ESP32's IOMUX
// pins, so the GPIO matrix caps the bus at 40MHz; a 320x240 RGB565 frame is
// 153,600 bytes, which is 30.7ms of clocking that has to happen no matter
// what the CPU is doing. Measured frame time is 31.4ms -- within 2% of that
// ceiling, because the scanlines go out by DMA and the Z80 emulation now
// runs underneath the transfer instead of after it (see
// src/arch/esp32/arch_spi_dma.h). Emulation is 15.1ms of that 31.4 and is
// essentially free now; the only lever left is the clock.
//
// For scale: the same code on the FIFO path the Arduino core provides ran
// 59.7ms/frame at 16.8fps, because a 64-byte poll loop added 14ms of pure
// overhead AND could not overlap with anything.
//
// Audio is a Stereo I2S 3W amp, dual MAX98357A (Adafruit #6513): BCLK to
// IO27, LRC to IO12, DIN to IO13, Vin to VBUS. It runs on its own FreeRTOS
// task pinned to core 0, so it does not compete with the emulator and the
// video path on core 1.
//
// Differences from pacman_fruitjam.ino, all forced by the hardware:
// - no setup1()/loop1(): there is no second-core display pump here, the
// panel is written from submit_scanline() on this core.
// - no set_sys_clock_khz(): that is a Pico SDK call. The ESP32 runs at
// 240MHz from the board config.
// - HAL_BTN_MIRROR and HAL_BTN_STRETCH are not wired and read false.
#include <Adafruit_Arcade_Machines.h>
#include <hal/arcade_hal_video.h>
#include <hal/arcade_hal_input.h>
#include <boards/feather_esp32/board_config_feather_esp32.h>
#include <machines/mspacman/mspacman_machine.h>
#include <machines/mspacman/mspacman_video.h>
#include <machines/mspacman/mspacman_input.h>
// Two emulated frames per painted frame -- see mspacman_run_frames(). The
// panel cannot reach 60Hz, so the game would otherwise run in slow motion.
#define EMULATED_FRAMES_PER_PAINT 2u
// Ms. Pac-Man's real refresh is 60.606Hz, so one emulated frame is 16,500us.
// The budget covers however many frames are emulated per paint.
#define FRAME_BUDGET_US (16500u * EMULATED_FRAMES_PER_PAINT)
// Set to 1 to time the transport in isolation at boot. See setup().
#define MSPACMAN_ESP32_BENCH 0
// THE ROM LIVES IN PSRAM. EVERYTHING ELSE DOES NOT, AND THAT SPLIT IS THE
// WHOLE POINT.
//
// Ms. Pac-Man is Pac-Man's board plus an aux daughterboard, so it carries
// two full 48K program banks -- 98,304 bytes -- where Pac-Man carries one
// 16K ROM. This chip's static-data segment (dram0_0_seg) is 124,580 bytes,
// NOT the 327,680 arduino-cli reports as "maximum", and that overflowed it
// by 65,776.
//
// The first attempt put the ENTIRE system struct in PSRAM. It worked and it
// cost 2.9ms per frame: 92% of arcade speed instead of 100%. The reason is
// that the other 3,188 bytes are the hottest data in the machine -- the
// renderer reads video_ram and color_ram for every tile of every scanline,
// and work_ram is written constantly -- so they were both slow themselves
// AND evicting ROM from the 32KB PSRAM cache.
//
// So only the ROM goes to PSRAM, where its size actually is the problem,
// and the rest stays in internal RAM where its speed is.
static mspacman_system g_system;
static mspacman_rom_bank_t *g_rom = NULL;
static bool g_assets_ok = false;
static uint16_t g_error_color = 0;
void setup() {
Serial.begin(115200);
delay(1500);
Serial.println("[mspacman-esp32] boot: serial up");
g_rom = (mspacman_rom_bank_t *)ps_malloc(sizeof(mspacman_rom_bank_t) *
MSPACMAN_ROM_BANKS);
if (!g_rom) {
Serial.println("[mspacman-esp32] FATAL: no PSRAM for the program ROM");
while (1) delay(1000);
}
Serial.printf("[mspacman-esp32] program ROM: %u bytes in PSRAM, "
"%u bytes of machine state in internal RAM\n",
(unsigned)(sizeof(mspacman_rom_bank_t) * MSPACMAN_ROM_BANKS),
(unsigned)sizeof(mspacman_system));
mspacman_init(&g_system, g_rom);
Serial.printf("[mspacman-esp32] boot: pacman_init done, rotation %u\n",
(unsigned)g_system.rotation);
g_assets_ok = mspacman_load_assets(&g_system, &g_error_color);
Serial.printf("[mspacman-esp32] boot: assets %s\n",
g_assets_ok ? "loaded OK" : "FAILED");
if (!g_assets_ok) {
Serial.printf("[mspacman-esp32] error colour 0x%04X -- red means no SD "
"card / would not mount, yellow means mounted but the "
"required ROM files were missing\n", g_error_color);
}
// Hand the SPI bus to the IDF driver. Must come AFTER asset loading --
// SdFat reads the ROMs over SPIClass, and the two cannot both own the
// peripheral. See arch_spi_dma.h.
hal_video_run();
// TRANSPORT BENCHMARK, one shot. Off by default: it costs ~0.7s of boot
// and is a tool, not a feature. Turn it on when changing anything about
// the transport -- it is what found that per-transfer overhead, not
// pixel throughput, was the ceiling (DEVNOTES #111).
#if MSPACMAN_ESP32_BENCH
// Original note: Pushes frames with NO rendering and no
// emulation, so what is left is the transport alone: the byte swap plus
// whatever the driver costs per transfer. Compared against the wire
// floor -- 153,600 bytes at 40MHz is 30,720us -- this says how much of
// the frame is overhead rather than physics.
{
const uint32_t t0 = micros();
for (int f = 0; f < 20; f++) {
for (uint32_t y = 0; y < HAL_VIDEO_HEIGHT; y++) {
uint16_t *b = hal_video_acquire_scanline();
hal_video_submit_scanline(b);
}
}
const uint32_t per = (micros() - t0) / 20u;
Serial.printf("[bench] transport only: %lu us/frame "
"(40MHz wire floor 30720us, overhead %ld us)\n",
(unsigned long)per, (long)per - 30720L);
}
#endif
Serial.printf("[mspacman-esp32] heap free %u, largest block %u, PSRAM %u\n",
(unsigned)ESP.getFreeHeap(), (unsigned)ESP.getMaxAllocHeap(),
(unsigned)ESP.getPsramSize());
}
void loop() {
if (!g_assets_ok) {
static uint32_t last = 0;
if (millis() - last > 1000) {
last = millis();
Serial.println("[mspacman-esp32] asset load failed -- halted");
}
mspacman_draw_error_frame(g_error_color);
return;
}
bool coin = hal_input_read(HAL_BTN_COIN);
bool start1 = hal_input_read(HAL_BTN_START1);
bool start2 = hal_input_read(HAL_BTN_START2);
bool up = hal_input_read(HAL_BTN_UP);
bool down = hal_input_read(HAL_BTN_DOWN);
bool left = hal_input_read(HAL_BTN_LEFT);
bool right = hal_input_read(HAL_BTN_RIGHT);
bool rotate = hal_input_read(HAL_BTN_ROTATE);
bool mirror = hal_input_read(HAL_BTN_MIRROR); // always false here
mspacman_input_update(&g_system, coin, start1, start2,
up, down, left, right, rotate, mirror);
// Whole-frame time: emulation and pixel-pushing together, because they
// are no longer separable. submit_scanline() hands the row to DMA and
// returns, so the next scanline's Z80 cycles run while it is still on
// the wire -- the same overlap the Fruit Jam gets from a second core,
// bought here with a second buffer instead. A number close to 30.7ms
// means the transfer is the whole cost and the emulator is hidden
// inside it; a number well above that means something stopped fitting.
static uint32_t frame = 0, emul_us = 0, push_us = 0, t_prev = 0;
uint32_t t0 = micros();
// TWO EMULATED FRAMES PER PAINTED FRAME.
//
// This board's panel cannot reach 60Hz -- 320x240 RGB565 at 40MHz is
// 30.7ms of unavoidable clocking -- so the game would otherwise run in
// slow motion, advancing one frame per display frame. Decoupling them
// keeps the Z80 at the interrupt rate the real cabinet produced and
// decimates only the picture, which is what galagino does on the same
// class of hardware for the same reason.
//
// It is nearly free: most of a frame is already spent waiting for the
// SPI transfer, and the second frame's cycles fit inside that wait.
mspacman_run_frames(&g_system, EMULATED_FRAMES_PER_PAINT);
// WALL-CLOCK LIMITER. Without it the game runs at whatever rate the
// panel happens to allow, which measured 61.4 emulated fps against a
// real Pac-Man cabinet's 60.606Hz -- 1.6% fast, and it would drift with
// scene complexity. Waiting out the remainder of the budget makes speed
// exact and content-independent.
//
// IT REPAYS DEBT RATHER THAN FORGIVING IT, and it used to do the
// opposite. The old version reset the deadline on any overrun, so a
// heavy scene degraded to "as fast as possible" rather than
// accumulating a debt it could never repay. That sounds safe and is
// subtly lossy: the time is not merely deferred, it is GONE, so every
// hiccup permanently shortens the game's clock against real time.
//
// Measured on hardware, the cost was visible without any hiccup at all.
// The once-per-30-frames heartbeat below is ~17ms of serial at 115200
// baud against a ~33ms budget; forgiven, it held these sketches at
// 99% of their cabinet rate instead of 100%. A whole class of "this
// board is slightly slow" readings was this line.
//
// So the deadline stays monotonic and a short overrun is repaid out of
// the following frames' idle time. The escape hatch survives, just with
// a threshold: only an overrun of MORE THAN A WHOLE BUDGET -- a genuine
// inability to keep up rather than a one-off -- resyncs and forgives
// the time. That bounds the debt at one frame, so a game that truly
// cannot keep up still degrades gracefully instead of spiralling, while
// the long-run rate stays exact.
//
// lrescue_featheresp32.ino adds a closed loop on top of this, because
// that game times its speaker waveform against the emulated clock and
// needs the two to track. Nothing here does, so nothing here needs it.
// DEVNOTES #120.
static uint32_t deadline = 0;
if (deadline == 0) deadline = micros();
deadline += FRAME_BUDGET_US;
int32_t slack = (int32_t)(deadline - micros());
if (slack > 0) delayMicroseconds((uint32_t)slack);
else if (slack < -(int32_t)FRAME_BUDGET_US) deadline = micros();
uint32_t total = micros() - t0;
emul_us += total;
if (++frame % 30u == 0) {
uint32_t now = millis();
// The first window spans boot, so its rate is meaningless -- it
// printed "18% of 60.6Hz", which reads like a fault. Prime the
// clock and skip it.
if (t_prev == 0) { t_prev = now; emul_us = 0; push_us = 0; return; }
float fps = 30000.0f / (float)(now - t_prev);
t_prev = now;
// Rotation is in the heartbeat because it is not otherwise
// observable and it changes what the geometry code is doing: 1 and
// 3 are tate (the picture fills all 320x240), 0 and 2 are yoko (180
// columns pillarboxed inside 320). It also makes a stray ROTATE
// press visible -- GPIO 37 is input-only with no internal pull, so
// an unwired or floating button line cycles this silently.
Serial.printf("[mspacman-esp32] frame %lu %.1f fps display "
"%.1f fps emulated (%.0f%% of 60.6Hz) frame %lu us "
"rot %u\n",
(unsigned long)frame, fps,
fps * EMULATED_FRAMES_PER_PAINT,
100.0f * fps * EMULATED_FRAMES_PER_PAINT / 60.606f,
(unsigned long)(emul_us / 30u),
(unsigned)g_system.rotation);
emul_us = 0; push_us = 0;
}
}