OXYGENHUB
◆ main enhanced-x16-emulator / src / timing.c
9.6 KiB · C · last changed chore(): initial commit 25 days ago source blame history raw
1
#ifndef __APPLE__
2
#ifndef _MSC_VER
3
#define _XOPEN_SOURCE   600
4
#define _POSIX_C_SOURCE 1
5
#endif
6
#endif
7
#include "glue.h"
8
#include "video.h"
9
#include "timing.h"
10
#include "cpu/fake6502.h"
11
#include <SDL.h>
12
#include <stdio.h>
13
#include "compat.h"
14
15
// Largest real-time deficit (in microseconds) we let the emulator try to make
16
// up by running unthrottled. Anything bigger is treated as a stall — the host
17
// was blocked in a modal window move/resize loop, was suspended, or simply
18
// can't keep up — and is dropped instead of "fast-forwarded" through once the
19
// stall ends. 250 ms comfortably exceeds normal per-frame scheduling jitter.
20
#define MAX_CATCHUP_US (250 * 1000)
21
22
// Above this much surplus, the throttle sleeps in slices instead of one block,
23
// so the host stays responsive while the machine is being run slowly. The
24
// threshold is above a frame's worth of pacing at the machine's own clock, so
25
// normal-speed behaviour is untouched.
26
#define THROTTLE_SLICE_THRESHOLD_US (25 * 1000)
27
#define THROTTLE_SLICE_US           (12 * 1000)
28
29
static void timing_update_ex(bool may_sleep);
30
31
uint32_t frames;
32
uint32_t sdlTicks_base;
33
uint32_t last_perf_update;
34
uint32_t clockticks6502_old;
35
int64_t cpu_ticks;
36
int64_t last_perf_cpu_ticks;
37
char window_title[255];
38
39
// The target clock, or 0 for "not set", meaning run at the machine's own.
40
static int emu_speed_khz = 0;
41
42
int
43
timing_native_khz(void)
44
{
45
	return (int)MHZ * 1000;
46
}
47
48
int
49
timing_get_speed_khz(void)
50
{
51
	return emu_speed_khz > 0 ? emu_speed_khz : timing_native_khz();
52
}
53
54
int
55
timing_get_speed_percent(void)
56
{
57
	const int native = timing_native_khz();
58
	return native > 0
59
	           ? (int)(((int64_t)timing_get_speed_khz() * 100 + native / 2) / native)
60
	           : 100;
61
}
62
63
// Emulated microseconds represented by `ticks`, scaled to the target clock. The
64
// throttle compares this against wall-clock microseconds, so a lower target
65
// makes each emulated cycle take proportionally longer in real time. At the
66
// machine's own clock this reduces exactly to the original ticks / MHZ.
67
static int64_t
68
scaled_us(int64_t ticks)
69
{
70
	const int khz = timing_get_speed_khz();
71
	return khz > 0 ? ticks * 1000 / (int64_t)khz : ticks / (MHZ ? MHZ : 1);
72
}
73
74
static int64_t
75
scaled_emulated_us(void)
76
{
77
	return scaled_us(cpu_ticks);
78
}
79
80
void
81
timing_set_speed_khz(int khz)
82
{
83
	if (khz < TIMING_SPEED_MIN_KHZ)
84
		khz = TIMING_SPEED_MIN_KHZ;
85
	if (khz > TIMING_SPEED_MAX_KHZ)
86
		khz = TIMING_SPEED_MAX_KHZ;
87
	if (khz == emu_speed_khz)
88
		return;
89
	emu_speed_khz = khz;
90
	// Re-base, so changing the scale is not read as a huge surplus or deficit
91
	// and answered with a sprint or a stall.
92
	timing_init();
93
}
94
95
// Preset clocks, in kHz. Anchored on speeds real hardware is run at (1 / 8 / 12
96
// MHz and neighbours) with steps between, so stepping moves through meaningful
97
// rates rather than arbitrary percentages.
98
static const int speed_ladder_khz[] = {
99
	   25,    50,   100,   250,   500,   750,
100
	 1000,  1500,  2000,  3000,  4000,  6000,
101
	 8000, 10000, 12000, 14000, 16000, 20000, 24000,
102
	// Past anything the hardware runs at, but still useful for getting through
103
	// a slow section without the all-or-nothing of warp mode.
104
	32000, 40000, 48000, 64000, 80000, 100000,
105
};
106
#define SPEED_LADDER_COUNT ((int)(sizeof(speed_ladder_khz) / sizeof(speed_ladder_khz[0])))
107
108
// Build the ladder for this machine, splicing its own clock in at the right
109
// place if it is not already a preset, so normal speed is always a rung.
110
static int
111
build_ladder(int *out, int max)
112
{
113
	const int native   = timing_native_khz();
114
	int       n        = 0;
115
	bool      inserted = false;
116
117
	for (int i = 0; i < SPEED_LADDER_COUNT && n < max; i++) {
118
		const int v = speed_ladder_khz[i];
119
		if (!inserted && v >= native) {
120
			out[n++] = native;
121
			inserted = true;
122
			if (v == native)
123
				continue;               // it *is* a preset, do not add it twice
124
			if (n >= max)
125
				break;
126
		}
127
		out[n++] = v;
128
	}
129
	if (!inserted && n < max)
130
		out[n++] = native;              // native sits above every preset
131
	return n;
132
}
133
134
void
135
timing_step_speed(int direction)
136
{
137
	int       ladder[SPEED_LADDER_COUNT + 1];
138
	const int n = build_ladder(ladder, (int)(sizeof(ladder) / sizeof(ladder[0])));
139
	if (n <= 0)
140
		return;
141
142
	// Find the nearest rung, then move from there.
143
	const int cur   = timing_get_speed_khz();
144
	int       best  = 0;
145
	int       bestd = 1 << 30;
146
	for (int i = 0; i < n; i++) {
147
		const int d = ladder[i] > cur ? ladder[i] - cur : cur - ladder[i];
148
		if (d < bestd) {
149
			bestd = d;
150
			best  = i;
151
		}
152
	}
153
154
	int idx = best;
155
	if (ladder[best] == cur) {
156
		idx = best + (direction > 0 ? 1 : (direction < 0 ? -1 : 0));
157
	} else if (direction > 0 && ladder[best] < cur) {
158
		idx = best + 1;                 // snap up past the rung we are above
159
	} else if (direction < 0 && ladder[best] > cur) {
160
		idx = best - 1;                 // snap down past the rung we are below
161
	}
162
	if (idx < 0)
163
		idx = 0;
164
	if (idx >= n)
165
		idx = n - 1;
166
	timing_set_speed_khz(ladder[idx]);
167
}
168
169
void
170
timing_reset_speed(void)
171
{
172
	timing_set_speed_khz(timing_native_khz());
173
}
174
175
void
176
timing_init() {
177
	frames = 0;
178
	sdlTicks_base = SDL_GetTicks();
179
	last_perf_update = 0;
180
	last_perf_cpu_ticks = 0;
181
	clockticks6502_old = clockticks6502;
182
	cpu_ticks = 0;
183
}
184
185
void
186
timing_update()
187
{
188
	timing_update_ex(true);
189
}
190
191
// As timing_update(), but never sleeps. The window-drag path runs inside the
192
// OS modal message loop, which cannot deliver mouse input while we are in the
193
// window procedure; sleeping there shows up directly as a jerky drag. That
194
// caller paces itself with timing_lead_us() instead.
195
void
196
timing_update_no_sleep()
197
{
198
	timing_update_ex(false);
199
}
200
201
// Microseconds of emulated time we are ahead of the wall clock; negative means
202
// behind. Lets a caller pace itself without blocking.
203
int64_t
204
timing_lead_us()
205
{
206
	int64_t ticks = cpu_ticks + (int64_t)(clockticks6502 - clockticks6502_old);
207
	uint32_t sdlTicks = SDL_GetTicks() - sdlTicks_base;
208
	// Scaled the same way the throttle is, or the two would disagree by exactly
209
	// native/target: the drag path is paced by this alone, so an unscaled lead
210
	// would let a slow target run at full speed while dragging, and freeze the
211
	// machine outright for a fast one.
212
	return scaled_us(ticks) - sdlTicks * 1000LL;
213
}
214
215
static void
216
timing_update_ex(bool may_sleep)
217
{
218
	frames++;
219
	cpu_ticks += clockticks6502 - clockticks6502_old;
220
	clockticks6502_old = clockticks6502;
221
	uint32_t sdlTicks = SDL_GetTicks() - sdlTicks_base;
222
	int64_t diff_time = scaled_emulated_us() - sdlTicks * 1000LL;
223
224
	// If we've fallen far behind real time, don't sprint to catch up (that
225
	// burst is the "fast-forward" artifact seen after dragging/resizing the
226
	// window, resuming from suspend, or on a host that can't keep up). Re-base
227
	// the wall-clock reference to now so the accumulated deficit is discarded
228
	// and emulation simply resumes at real speed.
229
	if (!warp_mode && diff_time < -(int64_t)MAX_CATCHUP_US) {
230
		uint32_t now = SDL_GetTicks();
231
		uint32_t new_base = now - (uint32_t)(scaled_emulated_us() / 1000);
232
		last_perf_update -= new_base - sdlTicks_base; // keep perf timer aligned
233
		sdlTicks_base = new_base;
234
		sdlTicks = now - sdlTicks_base;
235
		diff_time = scaled_emulated_us() - sdlTicks * 1000LL;
236
	}
237
238
	if (may_sleep && !warp_mode && diff_time > 0) {
239
		// A slower target multiplies the per-frame surplus by native/target.
240
		// At the machine's own clock that surplus is at most one frame, but at
241
		// the slowest setting it is seconds, and sleeping it in a single call
242
		// leaves the window unrepainted and unable to answer the OS -- which
243
		// marks it "not responding" -- for the whole of it. So break a long
244
		// wait into slices and let the host breathe between them.
245
		//
246
		// SDL_PumpEvents() dispatches the OS message queue without dequeuing
247
		// anything, and video_repaint_only() re-presents the existing frame
248
		// without touching the event queue or the recorder, so input still
249
		// arrives intact at the next real poll. The threshold is above one
250
		// frame's worth, so pacing
251
		// at the machine's own clock takes the single-sleep path exactly as
252
		// before.
253
		//
254
		// The wait is re-derived from the clock on every slice rather than
255
		// counted down from a snapshot. Sleeping may overshoot, the repaint
256
		// between slices costs real time, and SDL_PumpEvents() can hand the
257
		// thread to the OS -- on Windows a title-bar drag enters a modal loop
258
		// that runs the emulator itself and re-bases this very clock. Counting
259
		// down a local would then sleep off a surplus that had already been
260
		// spent, freezing the machine for seconds just after a drag: the exact
261
		// artefact this loop exists to avoid.
262
		if (diff_time > THROTTLE_SLICE_THRESHOLD_US) {
263
			for (;;) {
264
				const int64_t left = scaled_emulated_us()
265
				                   - (int64_t)(SDL_GetTicks() - sdlTicks_base) * 1000;
266
				if (left <= 0)
267
					break;
268
269
				usleep(left > THROTTLE_SLICE_US ? THROTTLE_SLICE_US : left);
270
271
				SDL_PumpEvents();
272
				video_repaint_only();
273
			}
274
			diff_time = 0;
275
		} else {
276
			if (diff_time >= 1000000) {
277
				sleep(diff_time / 1000000);
278
				diff_time %= 1000000;
279
			}
280
			usleep(diff_time);
281
		}
282
	}
283
284
	if (sdlTicks - last_perf_update > 5000) {
285
		uint32_t perf = (cpu_ticks - last_perf_cpu_ticks) / (MHZ * 50000);
286
287
		if (perf < 100 || warp_mode) {
288
			sprintf(window_title, WINDOW_TITLE " (%d%%)%s", perf, mouse_grabbed ? MOUSE_GRAB_MSG : "");
289
		} else {
290
			sprintf(window_title, WINDOW_TITLE "%s", mouse_grabbed ? MOUSE_GRAB_MSG : "");
291
		}
292
293
		video_update_title(window_title);
294
295
		last_perf_cpu_ticks = cpu_ticks;
296
		last_perf_update = sdlTicks;
297
	}
298
299
	if (log_speed) {
300
		float frames_behind = -((float)diff_time * 6e-5);
301
		int load = (int)((1 + frames_behind) * 100);
302
		printf("Load: %d%%\n", load > 100 ? 100 : load);
303
304
		if ((int)frames_behind > 0) {
305
			printf("Rendering is behind %d frames.\n", -(int)frames_behind);
306
		} else {
307
		}
308
	}
309
}
310