OXYGENHUB
◆ rrdc-perf enhanced-x16-emulator / tests / test_timing_speed.c
11.4 KiB · C · last changed chore(): initial commit 25 days ago source blame history raw
9120e874 Andy McCall 1
// Checks for the emulation-speed control in src/timing.c.
2
//
3
// The speed is an absolute target clock in kHz, not a percentage of something.
4
// The machine's own clock comes from -mhz and can be anything from 1 to 40, so
5
// a percentage says nothing on its own: "50%" is a different speed on a machine
6
// started with -mhz 1 than on one started with -mhz 12. That makes the
7
// arithmetic here worth pinning -- particularly that stepping always passes
8
// through the machine's own clock, so normal speed is reachable however the
9
// emulator was started.
10
//
11
// timing.c reaches for a handful of emulator globals and SDL; the test supplies
12
// its own so it links nothing else.
13
14
#include "timing.h"
15
16
#include <stdbool.h>
17
#include <stdint.h>
18
#include <stdio.h>
19
#include <string.h>
20
21
// ---- Stand-ins for the emulator and SDL ------------------------------------
22
23
uint8_t MHZ        = 8;
24
bool    warp_mode  = false;
25
bool    log_speed  = false;
26
bool    mouse_grabbed = false;
27
uint32_t clockticks6502 = 0;
28
29
void video_update_title(const char *t) { (void)t; }
30
31
// timing.c only uses these to pace; the tests here never call timing_update().
32
// Settable, so a check can advance emulated time and wall-clock time
33
// independently. Never moved backwards: timing.c subtracts it as uint32_t, so a
34
// backwards step wraps to about 4.29e9 ms rather than reading as negative.
35
static uint32_t fake_ticks_ms = 0;
36
uint32_t SDL_GetTicks(void) { return fake_ticks_ms; }
37
void     SDL_PumpEvents(void) {}
38
void     video_repaint_only(void) {}
39
40
// ---- Harness ---------------------------------------------------------------
41
42
static int failures = 0;
43
44
static void
45
check(bool cond, const char *what)
46
{
47
	if (!cond) {
48
		failures++;
49
		printf("FAIL: %s\n", what);
50
	} else {
51
		printf("ok  : %s\n", what);
52
	}
53
}
54
55
// Step `n` times in a direction and return where we land.
56
static int
57
step_n(int direction, int n)
58
{
59
	for (int i = 0; i < n; i++) {
60
		timing_step_speed(direction);
61
	}
62
	return timing_get_speed_khz();
63
}
64
65
int
66
main(void)
67
{
68
	// ── The state the emulator actually ships in ────────────────────────────
69
	// Checked before any setter runs, because nothing outside this test calls
70
	// them: unset is the only path the shipping binary ever takes, and a check
71
	// placed after a reset exercises a different one and leaves the fallback
72
	// untested.
73
	{
74
		MHZ = 8;
75
		check(timing_get_speed_khz() == timing_native_khz(),
76
		      "unset means the machine's own clock");
77
		check(timing_get_speed_percent() == 100, "which reads as 100%");
78
	}
79
80
	// ── The machine's own clock is the default ──────────────────────────────
81
	{
82
		MHZ = 8;
83
84
		timing_reset_speed();
85
		check(timing_native_khz() == 8000, "reports the machine's clock in kHz");
86
		check(timing_get_speed_khz() == 8000, "starts at the machine's own clock");
87
		check(timing_get_speed_percent() == 100, "which is 100%");
88
89
		MHZ = 1;
90
91
		timing_reset_speed();
92
		check(timing_native_khz() == 1000, "follows -mhz 1");
93
		check(timing_get_speed_percent() == 100, "and that is still 100%");
94
95
		MHZ = 12;
96
97
		timing_reset_speed();
98
		check(timing_native_khz() == 12000, "follows -mhz 12");
99
		check(timing_get_speed_percent() == 100, "and that is 100% too");
100
	}
101
102
	// ── A percentage alone would be ambiguous ───────────────────────────────
103
	// The same absolute speed is a different percentage on different machines,
104
	// which is exactly why the target is held in kHz.
105
	{
106
		MHZ = 8;
107
		timing_set_speed_khz(1000);
108
		int pct_on_8mhz = timing_get_speed_percent();
109
110
		MHZ = 1;
111
		timing_set_speed_khz(1000);
112
		int pct_on_1mhz = timing_get_speed_percent();
113
114
		check(pct_on_8mhz == 13 && pct_on_1mhz == 100,
115
		      "1 MHz reads as ~13% of an 8 MHz machine but 100% of a 1 MHz one");
116
	}
117
118
	// ── Clamping ────────────────────────────────────────────────────────────
119
	{
120
		MHZ = 8;
121
		timing_set_speed_khz(0);
122
		check(timing_get_speed_khz() == TIMING_SPEED_MIN_KHZ, "clamps zero up to the minimum");
123
		timing_set_speed_khz(-5000);
124
		check(timing_get_speed_khz() == TIMING_SPEED_MIN_KHZ, "clamps negative up to the minimum");
125
		timing_set_speed_khz(1 << 30);
126
		check(timing_get_speed_khz() == TIMING_SPEED_MAX_KHZ, "clamps absurdly high down to the maximum");
127
		timing_set_speed_khz(4000);
128
		check(timing_get_speed_khz() == 4000, "keeps a value inside the range");
129
	}
130
131
	// ── Stepping through the presets ────────────────────────────────────────
132
	{
133
		MHZ = 8;
134
135
		timing_reset_speed();
136
		int slower = step_n(-1, 1);
137
		check(slower < 8000, "stepping down leaves the machine's clock");
138
		check(step_n(1, 1) == 8000, "and stepping back up returns to it exactly");
139
140
		// Stepping down repeatedly must bottom out at the minimum, not run off
141
		// the end of the ladder.
142
143
		timing_reset_speed();
144
		check(step_n(-1, 100) == TIMING_SPEED_MIN_KHZ, "stepping down bottoms out at the minimum");
145
		check(step_n(1, 200) == TIMING_SPEED_MAX_KHZ, "stepping up tops out at the maximum");
146
147
		// Monotonic in both directions, with no repeats or reversals.
148
149
		timing_reset_speed();
150
		bool monotonic = true;
151
		int  prev      = timing_get_speed_khz();
152
		for (int i = 0; i < 40; i++) {
153
			timing_step_speed(-1);
154
			const int now = timing_get_speed_khz();
155
			if (now > prev)
156
				monotonic = false;
157
			prev = now;
158
		}
159
		check(monotonic, "stepping down never speeds up");
160
161
		prev = timing_get_speed_khz();
162
		monotonic = true;
163
		for (int i = 0; i < 60; i++) {
164
			timing_step_speed(1);
165
			const int now = timing_get_speed_khz();
166
			if (now < prev)
167
				monotonic = false;
168
			prev = now;
169
		}
170
		check(monotonic, "stepping up never slows down");
171
	}
172
173
	// ── The machine's own clock is always a rung ────────────────────────────
174
	// However the emulator was started, stepping has to be able to land back on
175
	// normal speed -- otherwise "back to normal" would be unreachable on a
176
	// machine whose clock is not one of the presets.
177
	{
178
		const uint8_t odd_clocks[] = { 1, 2, 3, 5, 7, 8, 11, 13, 17, 23, 40 };
179
		bool reachable = true;
180
		for (unsigned i = 0; i < sizeof(odd_clocks) / sizeof(odd_clocks[0]); i++) {
181
			MHZ = odd_clocks[i];
182
	
183
		timing_reset_speed();
184
			const int native = timing_native_khz();
185
186
			// Walk to the bottom, then back up, and see if we pass through it.
187
			step_n(-1, 60);
188
			bool seen = false;
189
			for (int s = 0; s < 60; s++) {
190
				timing_step_speed(1);
191
				if (timing_get_speed_khz() == native) {
192
					seen = true;
193
					break;
194
				}
195
			}
196
			if (!seen) {
197
				printf("      (native %d kHz unreachable from below on -mhz %u)\n",
198
				       native, odd_clocks[i]);
199
				reachable = false;
200
			}
201
		}
202
		check(reachable, "the machine's own clock is always reachable by stepping");
203
	}
204
205
	// ── Reset ───────────────────────────────────────────────────────────────
206
	{
207
		MHZ = 8;
208
		timing_set_speed_khz(100);
209
		check(timing_get_speed_khz() == 100, "runs slow when asked");
210
211
		timing_reset_speed();
212
		check(timing_get_speed_khz() == 8000 && timing_get_speed_percent() == 100,
213
		      "reset returns to the machine's own clock");
214
	}
215
216
	// ── The scaling itself ──────────────────────────────────────────────────
217
	// Everything above pins the ladder bookkeeping. This pins the arithmetic
218
	// that actually changes how the machine runs, which nothing else did: the
219
	// first version of this feature left timing_lead_us() unscaled and the
220
	// whole suite still passed. That value alone paces the window-drag path, so
221
	// an unscaled one lets a slow target run at full speed while a window is
222
	// being dragged, and freezes a fast one.
223
	//
224
	// timing_lead_us() is how far ahead of real time the emulation is. With
225
	// SDL_GetTicks() stubbed at zero, it is the emulated time accrued since the
226
	// clock was re-based -- so the counter is zeroed before each re-base, or the
227
	// delta it measures would be taken against the previous block's value.
228
	{
229
		MHZ = 8;
230
		timing_reset_speed();
231
		clockticks6502 = 0;
232
		timing_init();
233
		clockticks6502 = 8000;            // one millisecond at 8 MHz
234
		check(timing_lead_us() == 1000,
235
		      "lead is emulated microseconds at the machine's own clock");
236
237
		// A target 320x slower stretches the same emulated work over 320x the
238
		// real time, so the lead grows by the same factor.
239
		timing_set_speed_khz(25);
240
		clockticks6502 = 0;
241
		timing_init();
242
		clockticks6502 = 8000;
243
		check(timing_lead_us() == 320000, "and 320x that at 25 kHz");
244
245
		// Faster than the machine's own clock shrinks it.
246
		timing_set_speed_khz(16000);
247
		clockticks6502 = 0;
248
		timing_init();
249
		clockticks6502 = 8000;
250
		check(timing_lead_us() == 500, "and half that at twice the clock");
251
252
		// The scaling follows the machine's clock, not a fixed constant.
253
		MHZ = 1;
254
		timing_reset_speed();
255
		clockticks6502 = 0;
256
		timing_init();
257
		clockticks6502 = 1000;            // one millisecond at 1 MHz
258
		check(timing_lead_us() == 1000,
259
		      "a different machine clock still reads as real microseconds");
260
261
		MHZ = 8;
262
		timing_reset_speed();
263
		clockticks6502 = 0;
264
		timing_init();
265
	}
266
	// ── The wall-clock side of the lead ─────────────────────────────────────
267
	// The checks above pin the scaling of emulated time, but hold wall-clock
268
	// time at zero, so they cannot see the term that subtracts it -- nor the
269
	// accumulated cpu_ticks that the pending delta is added to. Both survive
270
	// deletion without them.
271
	//
272
	// timing_update() is deliberately never called: its slice loop re-reads the
273
	// clock to decide when to stop, and a stub that does not advance during a
274
	// sleep would spin forever. timing_update_no_sleep() skips that block.
275
	{
276
		MHZ = 8;
277
		timing_reset_speed();
278
279
		// Three milliseconds of real time against one of emulated work: the
280
		// machine is two milliseconds behind.
281
		fake_ticks_ms  = 0;
282
		clockticks6502 = 0;
283
		timing_init();
284
		fake_ticks_ms  = 3;
285
		clockticks6502 = 8000;
286
		check(timing_lead_us() == -2000, "lead goes negative when real time runs ahead");
287
288
		// Only the emulated side is scaled. At 25 kHz the same 8000 ticks are
289
		// worth 320 ms, against one second of real time.
290
		timing_set_speed_khz(25);
291
		fake_ticks_ms  = 0;
292
		clockticks6502 = 0;
293
		timing_init();
294
		fake_ticks_ms  = 1000;
295
		clockticks6502 = 8000;
296
		check(timing_lead_us() == 320000 - 1000000,
297
		      "and the wall-clock side is not scaled with it");
298
299
		// Ticks already folded into the running total still count. The wall
300
		// clock stays at zero across the update, so the catch-up re-base -- which
301
		// would silently rewrite the base and hide this -- cannot trigger.
302
		timing_reset_speed();
303
		fake_ticks_ms  = 0;
304
		clockticks6502 = 0;
305
		timing_init();
306
		clockticks6502 = 8000;
307
		timing_update_no_sleep();      // folds 8000 into the accumulated total
308
		clockticks6502 = 16000;        // another 8000 pending
309
		check(timing_lead_us() == 2000, "accumulated ticks count as well as pending ones");
310
311
		fake_ticks_ms  = 0;
312
		clockticks6502 = 0;
313
		timing_reset_speed();
314
		timing_init();
315
	}
316
317
	if (failures) {
318
		printf("\nFAILED (%d failures)\n", failures);
319
		return 1;
320
	}
321
	printf("\nPASSED (0 failures)\n");
322
	return 0;
323
}