uxn

Varvara Ordinator, written in ANSI C(SDL2)
git clone https://git.eamoncaddigan.net/uxn.git
Log | Files | Refs | README | LICENSE

uxnemu.c (15002B)


      1 #include <stdio.h>
      2 #include <time.h>
      3 #include <unistd.h>
      4 
      5 #include "uxn.h"
      6 
      7 #pragma GCC diagnostic push
      8 #pragma clang diagnostic push
      9 #pragma GCC diagnostic ignored "-Wpedantic"
     10 #pragma clang diagnostic ignored "-Wtypedef-redefinition"
     11 #include <SDL.h>
     12 #include "devices/system.h"
     13 #include "devices/console.h"
     14 #include "devices/screen.h"
     15 #include "devices/audio.h"
     16 #include "devices/file.h"
     17 #include "devices/controller.h"
     18 #include "devices/mouse.h"
     19 #include "devices/datetime.h"
     20 #if defined(_WIN32) && defined(_WIN32_WINNT) && _WIN32_WINNT > 0x0602
     21 #include <processthreadsapi.h>
     22 #elif defined(_WIN32)
     23 #include <windows.h>
     24 #include <string.h>
     25 #endif
     26 #ifndef __plan9__
     27 #define USED(x) (void)(x)
     28 #endif
     29 #pragma GCC diagnostic pop
     30 #pragma clang diagnostic pop
     31 
     32 /*
     33 Copyright (c) 2021-2023 Devine Lu Linvega, Andrew Alderwick
     34 
     35 Permission to use, copy, modify, and distribute this software for any
     36 purpose with or without fee is hereby granted, provided that the above
     37 copyright notice and this permission notice appear in all copies.
     38 
     39 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
     40 WITH REGARD TO THIS SOFTWARE.
     41 */
     42 
     43 #define PAD 2
     44 #define PAD2 4
     45 #define WIDTH 64 * 8
     46 #define HEIGHT 40 * 8
     47 #define TIMEOUT_MS 334
     48 
     49 static SDL_Window *emu_window;
     50 static SDL_Texture *emu_texture;
     51 static SDL_Renderer *emu_renderer;
     52 static SDL_Rect emu_viewport;
     53 static SDL_AudioDeviceID audio_id;
     54 static SDL_Thread *stdin_thread;
     55 
     56 /* devices */
     57 
     58 static int window_created, fullscreen, borderless;
     59 static Uint32 stdin_event, audio0_event, zoom = 1;
     60 static Uint64 exec_deadline, deadline_interval, ms_interval;
     61 
     62 static int
     63 clamp(int v, int min, int max)
     64 {
     65 	return v < min ? min : v > max ? max
     66 								   : v;
     67 }
     68 
     69 static void
     70 audio_deo(int instance, Uint8 *d, Uint8 port, Uxn *u)
     71 {
     72 	if(!audio_id) return;
     73 	if(port == 0xf) {
     74 		SDL_LockAudioDevice(audio_id);
     75 		audio_start(instance, d, u);
     76 		SDL_UnlockAudioDevice(audio_id);
     77 		SDL_PauseAudioDevice(audio_id, 0);
     78 	}
     79 }
     80 
     81 Uint8
     82 emu_dei(Uxn *u, Uint8 addr)
     83 {
     84 	Uint8 p = addr & 0x0f, d = addr & 0xf0;
     85 	switch(d) {
     86 	case 0x00: return system_dei(u, addr);
     87 	case 0x20: return screen_dei(u, addr);
     88 	case 0x30: return audio_dei(0, &u->dev[d], p);
     89 	case 0x40: return audio_dei(1, &u->dev[d], p);
     90 	case 0x50: return audio_dei(2, &u->dev[d], p);
     91 	case 0x60: return audio_dei(3, &u->dev[d], p);
     92 	case 0xc0: return datetime_dei(u, addr);
     93 	}
     94 	return u->dev[addr];
     95 }
     96 
     97 void
     98 emu_deo(Uxn *u, Uint8 addr, Uint8 value)
     99 {
    100 	Uint8 p = addr & 0x0f, d = addr & 0xf0;
    101 	u->dev[addr] = value;
    102 	switch(d) {
    103 	case 0x00:
    104 		system_deo(u, &u->dev[d], p);
    105 		if(p > 0x7 && p < 0xe) screen_palette(&u->dev[0x8]);
    106 		break;
    107 	case 0x10: console_deo(&u->dev[d], p); break;
    108 	case 0x20: screen_deo(u->ram, &u->dev[0x20], p); break;
    109 	case 0x30: audio_deo(0, &u->dev[d], p, u); break;
    110 	case 0x40: audio_deo(1, &u->dev[d], p, u); break;
    111 	case 0x50: audio_deo(2, &u->dev[d], p, u); break;
    112 	case 0x60: audio_deo(3, &u->dev[d], p, u); break;
    113 	case 0xa0: file_deo(0, u->ram, &u->dev[d], p); break;
    114 	case 0xb0: file_deo(1, u->ram, &u->dev[d], p); break;
    115 	}
    116 }
    117 
    118 /* Handlers */
    119 
    120 void
    121 audio_finished_handler(int instance)
    122 {
    123 	SDL_Event event;
    124 	event.type = audio0_event + instance;
    125 	SDL_PushEvent(&event);
    126 }
    127 
    128 static int
    129 stdin_handler(void *p)
    130 {
    131 	SDL_Event event;
    132 	USED(p);
    133 	event.type = stdin_event;
    134 	while(read(0, &event.cbutton.button, 1) > 0) {
    135 		while(SDL_PushEvent(&event) < 0)
    136 			SDL_Delay(25); /* slow down - the queue is most likely full */
    137 	}
    138 	return 0;
    139 }
    140 
    141 static void
    142 set_window_size(SDL_Window *window, int w, int h)
    143 {
    144 	SDL_Point win_old;
    145 	SDL_GetWindowSize(window, &win_old.x, &win_old.y);
    146 	if(w == win_old.x && h == win_old.y) return;
    147 	SDL_RenderClear(emu_renderer);
    148 	SDL_SetWindowSize(window, w, h);
    149 }
    150 
    151 static void
    152 set_zoom(Uint8 z, int win)
    153 {
    154 	if(z < 1) return;
    155 	if(win)
    156 		set_window_size(emu_window, (uxn_screen.width + PAD2) * z, (uxn_screen.height + PAD2) * z);
    157 	zoom = z;
    158 }
    159 
    160 static void
    161 set_fullscreen(int value, int win)
    162 {
    163 	Uint32 flags = 0; /* windowed mode; SDL2 has no constant for this */
    164 	fullscreen = value;
    165 	if(fullscreen)
    166 		flags = SDL_WINDOW_FULLSCREEN_DESKTOP;
    167 	if(win)
    168 		SDL_SetWindowFullscreen(emu_window, flags);
    169 }
    170 
    171 static void
    172 set_borderless(int value)
    173 {
    174 	if(fullscreen) return;
    175 	borderless = value;
    176 	SDL_SetWindowBordered(emu_window, !value);
    177 }
    178 
    179 static void
    180 set_debugger(Uxn *u, int value)
    181 {
    182 	u->dev[0x0e] = value;
    183 	screen_fill(uxn_screen.fg, 0);
    184 	screen_redraw(u);
    185 }
    186 
    187 /* emulator primitives */
    188 
    189 int
    190 emu_resize(int width, int height)
    191 {
    192 	if(!window_created)
    193 		return 0;
    194 	if(emu_texture != NULL)
    195 		SDL_DestroyTexture(emu_texture);
    196 	SDL_RenderSetLogicalSize(emu_renderer, width + PAD2, height + PAD2);
    197 	emu_texture = SDL_CreateTexture(emu_renderer, SDL_PIXELFORMAT_RGB888, SDL_TEXTUREACCESS_STATIC, width, height);
    198 	if(emu_texture == NULL || SDL_SetTextureBlendMode(emu_texture, SDL_BLENDMODE_NONE))
    199 		return system_error("SDL_SetTextureBlendMode", SDL_GetError());
    200 	if(SDL_UpdateTexture(emu_texture, NULL, uxn_screen.pixels, sizeof(Uint32)) != 0)
    201 		return system_error("SDL_UpdateTexture", SDL_GetError());
    202 	emu_viewport.x = PAD;
    203 	emu_viewport.y = PAD;
    204 	emu_viewport.w = uxn_screen.width;
    205 	emu_viewport.h = uxn_screen.height;
    206 	set_window_size(emu_window, (width + PAD2) * zoom, (height + PAD2) * zoom);
    207 	return 1;
    208 }
    209 
    210 static void
    211 emu_redraw(Uxn *u)
    212 {
    213 	screen_redraw(u);
    214 	if(SDL_UpdateTexture(emu_texture, NULL, uxn_screen.pixels, uxn_screen.width * sizeof(Uint32)) != 0)
    215 		system_error("SDL_UpdateTexture", SDL_GetError());
    216 	SDL_RenderClear(emu_renderer);
    217 	SDL_RenderCopy(emu_renderer, emu_texture, NULL, &emu_viewport);
    218 	SDL_RenderPresent(emu_renderer);
    219 }
    220 
    221 static int
    222 emu_init(Uxn *u)
    223 {
    224 	SDL_AudioSpec as;
    225 	SDL_zero(as);
    226 	as.freq = SAMPLE_FREQUENCY;
    227 	as.format = AUDIO_S16SYS;
    228 	as.channels = 2;
    229 	as.callback = audio_handler;
    230 	as.samples = AUDIO_BUFSIZE;
    231 	as.userdata = u;
    232 	if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_JOYSTICK) < 0)
    233 		return system_error("sdl", SDL_GetError());
    234 	audio_id = SDL_OpenAudioDevice(NULL, 0, &as, NULL, 0);
    235 	if(!audio_id)
    236 		system_error("sdl_audio", SDL_GetError());
    237 	if(SDL_NumJoysticks() > 0 && SDL_JoystickOpen(0) == NULL)
    238 		system_error("sdl_joystick", SDL_GetError());
    239 	stdin_event = SDL_RegisterEvents(1);
    240 	audio0_event = SDL_RegisterEvents(POLYPHONY);
    241 	SDL_DetachThread(stdin_thread = SDL_CreateThread(stdin_handler, "stdin", NULL));
    242 	SDL_StartTextInput();
    243 	SDL_ShowCursor(SDL_DISABLE);
    244 	SDL_EventState(SDL_DROPFILE, SDL_ENABLE);
    245 	SDL_SetRenderDrawColor(emu_renderer, 0x00, 0x00, 0x00, 0xff);
    246 	ms_interval = SDL_GetPerformanceFrequency() / 1000;
    247 	deadline_interval = ms_interval * TIMEOUT_MS;
    248 	exec_deadline = SDL_GetPerformanceCounter() + deadline_interval;
    249 	screen_resize(WIDTH, HEIGHT);
    250 	SDL_PauseAudioDevice(audio_id, 1);
    251 	return 1;
    252 }
    253 
    254 static void
    255 emu_restart(Uxn *u, char *rom, int soft)
    256 {
    257 	screen_resize(WIDTH, HEIGHT);
    258 	screen_fill(uxn_screen.bg, 0);
    259 	screen_fill(uxn_screen.fg, 0);
    260 	system_reboot(u, rom, soft);
    261 	SDL_SetWindowTitle(emu_window, boot_rom);
    262 }
    263 
    264 static void
    265 capture_screen(void)
    266 {
    267 	const Uint32 format = SDL_PIXELFORMAT_RGB24;
    268 #if SDL_BYTEORDER == SDL_BIG_ENDIAN
    269 	/* SDL_PIXELFORMAT_RGB24 */
    270 	Uint32 Rmask = 0x000000FF;
    271 	Uint32 Gmask = 0x0000FF00;
    272 	Uint32 Bmask = 0x00FF0000;
    273 #else
    274 	/* SDL_PIXELFORMAT_BGR24 */
    275 	Uint32 Rmask = 0x00FF0000;
    276 	Uint32 Gmask = 0x0000FF00;
    277 	Uint32 Bmask = 0x000000FF;
    278 #endif
    279 	time_t t = time(NULL);
    280 	char fname[64];
    281 	int w, h;
    282 	SDL_Surface *surface;
    283 	SDL_GetRendererOutputSize(emu_renderer, &w, &h);
    284 	if((surface = SDL_CreateRGBSurface(0, w, h, 24, Rmask, Gmask, Bmask, 0)) == NULL)
    285 		return;
    286 	SDL_RenderReadPixels(emu_renderer, NULL, format, surface->pixels, surface->pitch);
    287 	strftime(fname, sizeof(fname), "screenshot-%Y%m%d-%H%M%S.bmp", localtime(&t));
    288 	if(SDL_SaveBMP(surface, fname) == 0) {
    289 		fprintf(stderr, "Saved %s\n", fname);
    290 		fflush(stderr);
    291 	}
    292 	SDL_FreeSurface(surface);
    293 }
    294 
    295 static Uint8
    296 get_button(SDL_Event *event)
    297 {
    298 	switch(event->key.keysym.sym) {
    299 	case SDLK_LCTRL: return 0x01;
    300 	case SDLK_LALT: return 0x02;
    301 	case SDLK_LSHIFT: return 0x04;
    302 	case SDLK_HOME: return 0x08;
    303 	case SDLK_UP: return 0x10;
    304 	case SDLK_DOWN: return 0x20;
    305 	case SDLK_LEFT: return 0x40;
    306 	case SDLK_RIGHT: return 0x80;
    307 	}
    308 	return 0x00;
    309 }
    310 
    311 static Uint8
    312 get_button_joystick(SDL_Event *event)
    313 {
    314 	return 0x01 << (event->jbutton.button & 0x3);
    315 }
    316 
    317 static Uint8
    318 get_vector_joystick(SDL_Event *event)
    319 {
    320 	if(event->jaxis.value < -3200)
    321 		return 1;
    322 	if(event->jaxis.value > 3200)
    323 		return 2;
    324 	return 0;
    325 }
    326 
    327 static Uint8
    328 get_key(SDL_Event *event)
    329 {
    330 	int sym = event->key.keysym.sym;
    331 	SDL_Keymod mods = SDL_GetModState();
    332 	if(sym < 0x20 || sym == SDLK_DELETE)
    333 		return sym;
    334 	if(mods & KMOD_CTRL) {
    335 		if(sym < SDLK_a)
    336 			return sym;
    337 		else if(sym <= SDLK_z)
    338 			return sym - (mods & KMOD_SHIFT) * 0x20;
    339 	}
    340 	return 0x00;
    341 }
    342 
    343 static int
    344 handle_events(Uxn *u)
    345 {
    346 	SDL_Event event;
    347 	while(SDL_PollEvent(&event)) {
    348 		/* Window */
    349 		if(event.type == SDL_QUIT)
    350 			return 0;
    351 		else if(event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_EXPOSED)
    352 			emu_redraw(u);
    353 		else if(event.type == SDL_DROPFILE) {
    354 			emu_restart(u, event.drop.file, 0);
    355 			SDL_free(event.drop.file);
    356 		}
    357 		/* Mouse */
    358 		else if(event.type == SDL_MOUSEMOTION)
    359 			mouse_pos(u, &u->dev[0x90], clamp(event.motion.x - PAD, 0, uxn_screen.width - 1), clamp(event.motion.y - PAD, 0, uxn_screen.height - 1));
    360 		else if(event.type == SDL_MOUSEBUTTONUP)
    361 			mouse_up(u, &u->dev[0x90], SDL_BUTTON(event.button.button));
    362 		else if(event.type == SDL_MOUSEBUTTONDOWN)
    363 			mouse_down(u, &u->dev[0x90], SDL_BUTTON(event.button.button));
    364 		else if(event.type == SDL_MOUSEWHEEL)
    365 			mouse_scroll(u, &u->dev[0x90], event.wheel.x, event.wheel.y);
    366 		/* Controller */
    367 		else if(event.type == SDL_TEXTINPUT) {
    368 			char *c;
    369 			for(c = event.text.text; *c; c++)
    370 				controller_key(u, &u->dev[0x80], *c);
    371 		} else if(event.type == SDL_KEYDOWN) {
    372 			int ksym;
    373 			if(get_key(&event))
    374 				controller_key(u, &u->dev[0x80], get_key(&event));
    375 			else if(get_button(&event))
    376 				controller_down(u, &u->dev[0x80], get_button(&event));
    377 			else if(event.key.keysym.sym == SDLK_F1)
    378 				set_zoom(zoom == 3 ? 1 : zoom + 1, 1);
    379 			else if(event.key.keysym.sym == SDLK_F2)
    380 				set_debugger(u, !u->dev[0x0e]);
    381 			else if(event.key.keysym.sym == SDLK_F3)
    382 				capture_screen();
    383 			else if(event.key.keysym.sym == SDLK_F4)
    384 				emu_restart(u, boot_rom, 0);
    385 			else if(event.key.keysym.sym == SDLK_F5)
    386 				emu_restart(u, boot_rom, 1);
    387 			else if(event.key.keysym.sym == SDLK_F11)
    388 				set_fullscreen(!fullscreen, 1);
    389 			else if(event.key.keysym.sym == SDLK_F12)
    390 				set_borderless(!borderless);
    391 			ksym = event.key.keysym.sym;
    392 			if(SDL_PeepEvents(&event, 1, SDL_PEEKEVENT, SDL_KEYUP, SDL_KEYUP) == 1 && ksym == event.key.keysym.sym)
    393 				return 1;
    394 		} else if(event.type == SDL_KEYUP)
    395 			controller_up(u, &u->dev[0x80], get_button(&event));
    396 		else if(event.type == SDL_JOYAXISMOTION) {
    397 			Uint8 vec = get_vector_joystick(&event);
    398 			if(!vec)
    399 				controller_up(u, &u->dev[0x80], (3 << (!event.jaxis.axis * 2)) << 4);
    400 			else
    401 				controller_down(u, &u->dev[0x80], (1 << ((vec + !event.jaxis.axis * 2) - 1)) << 4);
    402 		} else if(event.type == SDL_JOYBUTTONDOWN)
    403 			controller_down(u, &u->dev[0x80], get_button_joystick(&event));
    404 		else if(event.type == SDL_JOYBUTTONUP)
    405 			controller_up(u, &u->dev[0x80], get_button_joystick(&event));
    406 		else if(event.type == SDL_JOYHATMOTION) {
    407 			/* NOTE: Assuming there is only one joyhat in the controller */
    408 			switch(event.jhat.value) {
    409 			case SDL_HAT_UP: controller_down(u, &u->dev[0x80], 0x10); break;
    410 			case SDL_HAT_DOWN: controller_down(u, &u->dev[0x80], 0x20); break;
    411 			case SDL_HAT_LEFT: controller_down(u, &u->dev[0x80], 0x40); break;
    412 			case SDL_HAT_RIGHT: controller_down(u, &u->dev[0x80], 0x80); break;
    413 			case SDL_HAT_LEFTDOWN: controller_down(u, &u->dev[0x80], 0x40 | 0x20); break;
    414 			case SDL_HAT_LEFTUP: controller_down(u, &u->dev[0x80], 0x40 | 0x10); break;
    415 			case SDL_HAT_RIGHTDOWN: controller_down(u, &u->dev[0x80], 0x80 | 0x20); break;
    416 			case SDL_HAT_RIGHTUP: controller_down(u, &u->dev[0x80], 0x80 | 0x10); break;
    417 			case SDL_HAT_CENTERED: controller_up(u, &u->dev[0x80], 0x10 | 0x20 | 0x40 | 0x80); break;
    418 			}
    419 		}
    420 		/* Console */
    421 		else if(event.type == stdin_event)
    422 			console_input(u, event.cbutton.button, CONSOLE_STD);
    423 	}
    424 	return 1;
    425 }
    426 
    427 static int
    428 emu_run(Uxn *u, char *rom)
    429 {
    430 	Uint64 next_refresh = 0;
    431 	Uint64 frame_interval = SDL_GetPerformanceFrequency() / 60;
    432 	Uint8 *vector_addr = &u->dev[0x20];
    433 	Uint32 window_flags = SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI;
    434 	window_created = 1;
    435 	if(fullscreen)
    436 		window_flags = window_flags | SDL_WINDOW_FULLSCREEN_DESKTOP;
    437 	emu_window = SDL_CreateWindow(rom,
    438 		SDL_WINDOWPOS_UNDEFINED,
    439 		SDL_WINDOWPOS_UNDEFINED,
    440 		(uxn_screen.width + PAD2) * zoom,
    441 		(uxn_screen.height + PAD2) * zoom,
    442 		window_flags);
    443 	if(emu_window == NULL)
    444 		return system_error("sdl_window", SDL_GetError());
    445 	emu_renderer = SDL_CreateRenderer(emu_window, -1, SDL_RENDERER_ACCELERATED);
    446 	if(emu_renderer == NULL)
    447 		return system_error("sdl_renderer", SDL_GetError());
    448 	emu_resize(uxn_screen.width, uxn_screen.height);
    449 	/* game loop */
    450 	for(;;) {
    451 		Uint16 screen_vector;
    452 		Uint64 now = SDL_GetPerformanceCounter();
    453 		/* .System/halt */
    454 		if(u->dev[0x0f])
    455 			return system_error("Run", "Ended.");
    456 		exec_deadline = now + deadline_interval;
    457 		if(!handle_events(u))
    458 			return 0;
    459 		screen_vector = PEEK2(vector_addr);
    460 		if(now >= next_refresh) {
    461 			now = SDL_GetPerformanceCounter();
    462 			next_refresh = now + frame_interval;
    463 			uxn_eval(u, screen_vector);
    464 			if(uxn_screen.x2)
    465 				emu_redraw(u);
    466 		}
    467 		if(screen_vector || uxn_screen.x2) {
    468 			Uint64 delay_ms = (next_refresh - now) / ms_interval;
    469 			if(delay_ms > 0) SDL_Delay(delay_ms);
    470 		} else
    471 			SDL_WaitEvent(NULL);
    472 	}
    473 }
    474 
    475 static int
    476 emu_end(Uxn *u)
    477 {
    478 	SDL_CloseAudioDevice(audio_id);
    479 #ifdef _WIN32
    480 #pragma GCC diagnostic ignored "-Wint-to-pointer-cast"
    481 	TerminateThread((HANDLE)SDL_GetThreadID(stdin_thread), 0);
    482 #elif !defined(__APPLE__)
    483 	close(0); /* make stdin thread exit */
    484 #endif
    485 	SDL_Quit();
    486 	free(u->ram);
    487 	return u->dev[0x0f] & 0x7f;
    488 }
    489 
    490 int
    491 main(int argc, char **argv)
    492 {
    493 	int i = 1;
    494 	Uint8 *ram;
    495 	char *rom;
    496 	Uxn u = {0};
    497 	Uint8 dev[0x100] = {0};
    498 	Uxn u_audio = {0};
    499 	u.dev = dev;
    500 	u_audio.dev = dev;
    501 	/* flags */
    502 	if(argc > 1 && argv[i][0] == '-') {
    503 		if(!strcmp(argv[i], "-v"))
    504 			return system_error("Uxnemu - Varvara Emulator(GUI)", "4 Mar 2024.");
    505 		else if(!strcmp(argv[i], "-2x"))
    506 			set_zoom(2, 0);
    507 		else if(!strcmp(argv[i], "-3x"))
    508 			set_zoom(3, 0);
    509 		else if(strcmp(argv[i], "-f") == 0)
    510 			set_fullscreen(1, 0);
    511 		i++;
    512 	}
    513 	/* start */
    514 	rom = i == argc ? "boot.rom" : argv[i++];
    515 	ram = (Uint8 *)calloc(0x10000 * RAM_PAGES, sizeof(Uint8));
    516 	if(!system_init(&u, ram, rom) || !system_init(&u_audio, ram, rom))
    517 		return system_error("usage:", "uxnemu [-v | -f | -2x | -3x] file.rom [args...]");
    518 	if(!emu_init(&u_audio))
    519 		return system_error("Init", "Failed to initialize varvara.");
    520 	/* loop */
    521 	u.dev[0x17] = argc - i;
    522 	if(uxn_eval(&u, PAGE_PROGRAM)) {
    523 		console_listen(&u, i, argc, argv);
    524 		emu_run(&u, boot_rom);
    525 	}
    526 	return emu_end(&u);
    527 }