Browse code

Add automated test harness (corresponding Uxntal to follow).

Andrew Alderwick authored on 29/12/2021 01:57:46
Showing 4 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,3 @@
1
+/autotest
2
+/fix_fft.c
3
+/test.ppm
0 4
new file mode 100644
... ...
@@ -0,0 +1,11 @@
1
+pcm.!default {
2
+	type file
3
+	slave.pcm null
4
+	file /proc/self/fd/3
5
+	format "raw"
6
+}
7
+
8
+pcm.null {
9
+	type null
10
+}
11
+
0 12
new file mode 100644
... ...
@@ -0,0 +1,332 @@
1
+#define _GNU_SOURCE
2
+
3
+#include <errno.h>
4
+#include <fcntl.h>
5
+#include <math.h>
6
+#include <signal.h>
7
+#include <stdarg.h>
8
+#include <stdio.h>
9
+#include <stdlib.h>
10
+#include <string.h>
11
+#include <sys/select.h>
12
+#include <sys/stat.h>
13
+#include <sys/time.h>
14
+#include <sys/types.h>
15
+#include <sys/wait.h>
16
+#include <unistd.h>
17
+
18
+#define WIDTH 512
19
+#define HEIGHT 320
20
+
21
+#define str(x) stra(x)
22
+#define stra(x) #x
23
+
24
+#define die(fnname) \
25
+	do { \
26
+		perror(fnname); \
27
+		exit(EXIT_FAILURE); \
28
+	} while(0)
29
+
30
+#define x(fn, ...) \
31
+	do { \
32
+		if(fn(__VA_ARGS__) < 0) { \
33
+			perror(#fn); \
34
+			exit(EXIT_FAILURE); \
35
+		} \
36
+	} while(0)
37
+
38
+int fix_fft(short *fr, short *fi, short m, short inverse);
39
+
40
+static pid_t
41
+launch_xvfb(void)
42
+{
43
+	char displayfd[16];
44
+	int r;
45
+	pid_t pid;
46
+	int fds[2];
47
+	x(pipe2, &fds[0], 0);
48
+	pid = fork();
49
+	if(pid < 0) {
50
+		die("fork");
51
+	} else if(!pid) {
52
+		x(snprintf, displayfd, sizeof(displayfd), "%d", fds[1]);
53
+		x(close, fds[0]);
54
+		execlp("Xvfb", "Xvfb", "-screen", "0", str(WIDTH) "x" str(HEIGHT) "x24", "-fbdir", ".", "-displayfd", displayfd, "-nolisten", "tcp", NULL);
55
+		die("execl");
56
+		exit(EXIT_FAILURE);
57
+	}
58
+	x(close, fds[1]);
59
+	r = read(fds[0], &displayfd[1], sizeof(displayfd) - 1);
60
+	if(r < 0) die("read");
61
+	x(close, fds[0]);
62
+	displayfd[r] = '\0';
63
+	displayfd[0] = ':';
64
+	x(setenv, "DISPLAY", displayfd, 1);
65
+	x(setenv, "ALSA_CONFIG_PATH", "asoundrc", 1);
66
+	return pid;
67
+}
68
+
69
+static pid_t
70
+launch_uxnemu(int *write_fd, int *read_fd, int *sound_fd)
71
+{
72
+	pid_t pid;
73
+	int fds[6];
74
+	x(pipe2, &fds[0], O_CLOEXEC);
75
+	x(pipe2, &fds[2], O_CLOEXEC);
76
+	x(pipe2, &fds[4], O_CLOEXEC);
77
+	pid = fork();
78
+	if(pid < 0) {
79
+		die("fork");
80
+	} else if(!pid) {
81
+		x(dup2, fds[0], 0);
82
+		x(dup2, fds[3], 1);
83
+		x(dup2, fds[5], 3);
84
+		execl("../../bin/uxnemu", "uxnemu", "autotest.rom", NULL);
85
+		die("execl");
86
+	}
87
+	x(close, fds[0]);
88
+	x(close, fds[3]);
89
+	x(close, fds[5]);
90
+	*write_fd = fds[1];
91
+	*read_fd = fds[2];
92
+	*sound_fd = fds[4];
93
+	return pid;
94
+}
95
+
96
+static void
97
+terminate(pid_t pid)
98
+{
99
+	int signals[] = {SIGINT, SIGTERM, SIGKILL};
100
+	int status;
101
+	size_t i;
102
+	for(i = 0; i < sizeof(signals) / sizeof(int) * 10; ++i) {
103
+		if(kill(pid, signals[i / 10])) {
104
+			break;
105
+		}
106
+		usleep(100000);
107
+		if(pid == waitpid(pid, &status, WNOHANG)) {
108
+			return;
109
+		}
110
+	}
111
+	waitpid(pid, &status, 0);
112
+}
113
+
114
+static int
115
+open_framebuffer(void)
116
+{
117
+	for(;;) {
118
+		int fd = open("Xvfb_screen0", O_RDONLY | O_CLOEXEC);
119
+		if(fd >= 0) {
120
+			return fd;
121
+		}
122
+		if(errno != ENOENT) {
123
+			perror("open");
124
+			return fd;
125
+		}
126
+		usleep(100000);
127
+	}
128
+}
129
+
130
+#define PPM_HEADER "P6\n" str(WIDTH) " " str(HEIGHT) "\n255\n"
131
+
132
+static void
133
+save_screenshot(int fb_fd, const char *filename)
134
+{
135
+	unsigned char screen[WIDTH * HEIGHT * 4 + 4];
136
+	int fd = open(filename, O_WRONLY | O_CREAT, 0666);
137
+	int i;
138
+	if(fd < 0) {
139
+		die("screenshot open");
140
+	}
141
+	x(write, fd, PPM_HEADER, strlen(PPM_HEADER));
142
+	x(lseek, fb_fd, 0xca0, SEEK_SET);
143
+	x(read, fb_fd, &screen[4], WIDTH * HEIGHT * 4);
144
+	for(i = 0; i < WIDTH * HEIGHT; ++i) {
145
+		screen[i * 3 + 2] = screen[i * 4 + 4];
146
+		screen[i * 3 + 1] = screen[i * 4 + 5];
147
+		screen[i * 3 + 0] = screen[i * 4 + 6];
148
+	}
149
+	x(write, fd, screen, WIDTH * HEIGHT * 3);
150
+	x(close, fd);
151
+}
152
+
153
+static void
154
+systemf(char *format, ...)
155
+{
156
+	char *command;
157
+	va_list ap;
158
+	va_start(ap, format);
159
+	x(vasprintf, &command, format, ap);
160
+	system(command);
161
+	free(command);
162
+}
163
+
164
+int uxn_read_fd, sound_fd;
165
+
166
+static int
167
+byte(void)
168
+{
169
+	char c;
170
+	if(read(uxn_read_fd, &c, 1) != 1) {
171
+		return 0;
172
+	}
173
+	return (unsigned char)c;
174
+}
175
+
176
+#define NEW_FFT_SIZE_POW2 10
177
+#define NEW_FFT_SIZE (1 << NEW_FFT_SIZE_POW2)
178
+#define NEW_FFT_USEC (5000 * NEW_FFT_SIZE / 441)
179
+
180
+unsigned char left_peak, right_peak;
181
+
182
+static int
183
+detect_peak(short *real, short *imag)
184
+{
185
+	int i, peak = 0, peak_i;
186
+	for(i = 0; i < NEW_FFT_SIZE; ++i) {
187
+		int v = real[i] * real[i] + imag[i] * imag[i];
188
+		if(peak < v) {
189
+			peak = v;
190
+			peak_i = i;
191
+		} else if(peak > v * 10) {
192
+			return peak_i;
193
+		}
194
+	}
195
+	return 0;
196
+}
197
+
198
+static int
199
+analyse_sound(short *samples)
200
+{
201
+	short real[NEW_FFT_SIZE], imag[NEW_FFT_SIZE];
202
+	int i;
203
+	for(i = 0; i < NEW_FFT_SIZE * 2; ++i) {
204
+		if(samples[i * 2]) break;
205
+	}
206
+	if(i == NEW_FFT_SIZE * 2) return 0;
207
+	for(i = 0; i < NEW_FFT_SIZE; ++i) {
208
+		real[i] = samples[i * 4];
209
+		imag[i] = samples[i * 4 + 2];
210
+	}
211
+	fix_fft(real, imag, NEW_FFT_SIZE_POW2, 0);
212
+	return detect_peak(real, imag);
213
+}
214
+
215
+static int
216
+read_sound(void)
217
+{
218
+	static short samples[NEW_FFT_SIZE * 4];
219
+	static size_t len = 0;
220
+	int r = read(sound_fd, ((char *)samples) + len, sizeof(samples) - len);
221
+	if(r > 0) {
222
+		len += r;
223
+		if(len == sizeof(samples)) {
224
+			left_peak = analyse_sound(&samples[0]);
225
+			right_peak = analyse_sound(&samples[1]);
226
+			len = 0;
227
+			return 1;
228
+		}
229
+	}
230
+	return 0;
231
+}
232
+
233
+static void
234
+main_loop(int uxn_write_fd, int fb_fd)
235
+{
236
+	struct timeval next_sound = {0, 0};
237
+	for(;;) {
238
+		struct timeval now;
239
+		struct timeval *timeout;
240
+		fd_set fds;
241
+		FD_ZERO(&fds);
242
+		FD_SET(uxn_read_fd, &fds);
243
+		x(gettimeofday, &now, NULL);
244
+		if(now.tv_sec > next_sound.tv_sec || (now.tv_sec == next_sound.tv_sec && now.tv_usec > next_sound.tv_usec)) {
245
+			FD_SET(sound_fd, &fds);
246
+			timeout = NULL;
247
+		} else {
248
+			now.tv_sec = 0;
249
+			now.tv_usec = NEW_FFT_USEC;
250
+			timeout = &now;
251
+		}
252
+		x(select, uxn_read_fd > sound_fd ? uxn_read_fd + 1 : sound_fd + 1, &fds, NULL, NULL, timeout);
253
+		if(FD_ISSET(uxn_read_fd, &fds)) {
254
+			int c, x, y;
255
+			unsigned char blue;
256
+			switch(c = byte()) {
257
+			case 0x00: /* also used for EOF */
258
+				printf("exiting\n");
259
+				return;
260
+			/* 01-06 mouse */
261
+			case 0x01 ... 0x05:
262
+				systemf("xdotool click %d", c);
263
+				break;
264
+			case 0x06:
265
+				x = (byte() << 8) | byte();
266
+				y = (byte() << 8) | byte();
267
+				systemf("xdotool mousemove %d %d", x, y);
268
+				break;
269
+			/* 07-08 Screen */
270
+			case 0x07:
271
+				x = (byte() << 8) | byte();
272
+				y = (byte() << 8) | byte();
273
+				lseek(fb_fd, 0xca0 + (x + y * WIDTH) * 4, SEEK_SET);
274
+				read(fb_fd, &blue, 1);
275
+				blue = blue / 0x11;
276
+				write(uxn_write_fd, &blue, 1);
277
+				break;
278
+			case 0x08:
279
+				save_screenshot(fb_fd, "test.ppm");
280
+				break;
281
+			/* 09-0a Audio */
282
+			case 0x09:
283
+				write(uxn_write_fd, &left_peak, 1);
284
+				break;
285
+			case 0x0a:
286
+				write(uxn_write_fd, &right_peak, 1);
287
+				break;
288
+			/* 11-7e Controller/key */
289
+			case 0x11 ... 0x1c:
290
+				systemf("xdotool key F%d", c - 0x10);
291
+				break;
292
+			case '0' ... '9':
293
+			case 'A' ... 'Z':
294
+			case 'a' ... 'z':
295
+				systemf("xdotool key %c", c);
296
+				break;
297
+			default:
298
+				printf("unhandled command 0x%02x\n", c);
299
+				break;
300
+			}
301
+		}
302
+		if(FD_ISSET(sound_fd, &fds)) {
303
+			if(!next_sound.tv_sec) {
304
+				x(gettimeofday, &next_sound, NULL);
305
+			}
306
+			next_sound.tv_usec += NEW_FFT_USEC * read_sound();
307
+			if(next_sound.tv_usec > 1000000) {
308
+				next_sound.tv_usec -= 1000000;
309
+				++next_sound.tv_sec;
310
+			}
311
+		}
312
+	}
313
+}
314
+
315
+int
316
+main(void)
317
+{
318
+	pid_t xvfb_pid = launch_xvfb();
319
+	int fb_fd = open_framebuffer();
320
+	if(fb_fd >= 0) {
321
+		int uxn_write_fd;
322
+		pid_t uxnemu_pid = launch_uxnemu(&uxn_write_fd, &uxn_read_fd, &sound_fd);
323
+		main_loop(uxn_write_fd, fb_fd);
324
+		terminate(uxnemu_pid);
325
+		x(close, uxn_write_fd);
326
+		x(close, uxn_read_fd);
327
+		x(close, sound_fd);
328
+		x(close, fb_fd);
329
+	}
330
+	terminate(xvfb_pid);
331
+	return 0;
332
+}
0 333
new file mode 100755
... ...
@@ -0,0 +1,20 @@
1
+#!/bin/sh -e
2
+cd "$(dirname "${0}")"
3
+if ! which Xvfb 2>/dev/null; then
4
+    echo "error: ${0} depends on Xvfb"
5
+    exit 1
6
+fi
7
+if ! which xdotool 2>/dev/null; then
8
+    echo "error: ${0} depends on xdotool"
9
+    exit 1
10
+fi
11
+if [ ! -e fix_fft.c ]; then
12
+    wget https://gist.githubusercontent.com/Tomwi/3842231/raw/67149b6ec81cfb6ac1056fd23a3bb6ce1f0a5188/fix_fft.c
13
+fi
14
+if which clang-format 2>/dev/null; then
15
+    ( cd ../.. && clang-format -i etc/autotest/main.c )
16
+fi
17
+../../bin/uxnasm autotest.tal autotest.rom
18
+gcc -std=gnu89 -Wall -Wextra -o autotest main.c fix_fft.c -lm
19
+./autotest
20
+