uxn

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

uxnemu.c (13329B)


      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-2025 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 Uxn uxn;
     50 
     51 static SDL_Window *emu_window;
     52 static SDL_Texture *emu_texture;
     53 static SDL_Renderer *emu_renderer;
     54 static SDL_Rect emu_viewport;
     55 static SDL_AudioDeviceID audio_id;
     56 static SDL_Thread *stdin_thread;
     57 
     58 /* devices */
     59 
     60 static int window_created, fullscreen, borderless;
     61 static Uint32 stdin_event, audio0_event, zoom = 1;
     62 static Uint64 exec_deadline, deadline_interval, ms_interval;
     63 
     64 static int
     65 clamp(int val, int min, int max)
     66 {
     67 	return (val >= min) ? (val <= max) ? val : max : min;
     68 }
     69 
     70 static void
     71 audio_deo(int instance, Uint8 *d, Uint8 port)
     72 {
     73 	if(!audio_id) return;
     74 	if(port == 0xf) {
     75 		SDL_LockAudioDevice(audio_id);
     76 		audio_start(instance, d, &uxn);
     77 		SDL_UnlockAudioDevice(audio_id);
     78 		SDL_PauseAudioDevice(audio_id, 0);
     79 	}
     80 }
     81 
     82 Uint8
     83 emu_dei(Uint8 addr)
     84 {
     85 	Uint8 p = addr & 0x0f, d = addr & 0xf0;
     86 	switch(d) {
     87 	case 0x00: return system_dei(addr);
     88 	case 0x20: return screen_dei(addr);
     89 	case 0x30: return audio_dei(0, &uxn.dev[d], p);
     90 	case 0x40: return audio_dei(1, &uxn.dev[d], p);
     91 	case 0x50: return audio_dei(2, &uxn.dev[d], p);
     92 	case 0x60: return audio_dei(3, &uxn.dev[d], p);
     93 	case 0xc0: return datetime_dei(addr);
     94 	}
     95 	return uxn.dev[addr];
     96 }
     97 
     98 void
     99 emu_deo(Uint8 addr, Uint8 value)
    100 {
    101 	Uint8 p = addr & 0x0f, d = addr & 0xf0;
    102 	uxn.dev[addr] = value;
    103 	switch(d) {
    104 	case 0x00:
    105 		system_deo(addr);
    106 		if(p > 0x7 && p < 0xe) screen_palette();
    107 		break;
    108 	case 0x10: console_deo(addr); break;
    109 	case 0x20: screen_deo(addr); break;
    110 	case 0x30: audio_deo(0, &uxn.dev[d], p); break;
    111 	case 0x40: audio_deo(1, &uxn.dev[d], p); break;
    112 	case 0x50: audio_deo(2, &uxn.dev[d], p); break;
    113 	case 0x60: audio_deo(3, &uxn.dev[d], p); break;
    114 	case 0xa0: file_deo(addr); break;
    115 	case 0xb0: file_deo(addr); break;
    116 	}
    117 }
    118 
    119 /* Handlers */
    120 
    121 void
    122 audio_finished_handler(int instance)
    123 {
    124 	SDL_Event event;
    125 	event.type = audio0_event + instance;
    126 	SDL_PushEvent(&event);
    127 }
    128 
    129 static int
    130 stdin_handler(void *p)
    131 {
    132 	SDL_Event event;
    133 	USED(p);
    134 	event.type = stdin_event;
    135 	event.cbutton.state = CONSOLE_STD;
    136 	while(read(0, &event.cbutton.button, 1) > 0) {
    137 		while(SDL_PushEvent(&event) < 0)
    138 			SDL_Delay(25); /* slow down - the queue is most likely full */
    139 	}
    140 	/* EOF */
    141 	event.cbutton.button = 0x00;
    142 	event.cbutton.state = CONSOLE_END;
    143 	while(SDL_PushEvent(&event) < 0)
    144 		SDL_Delay(25);
    145 	return 0;
    146 }
    147 
    148 static void
    149 set_window_size(SDL_Window *window, int w, int h)
    150 {
    151 	SDL_Point win_old;
    152 	SDL_GetWindowSize(window, &win_old.x, &win_old.y);
    153 	if(w == win_old.x && h == win_old.y) return;
    154 	SDL_RenderClear(emu_renderer);
    155 	SDL_SetWindowSize(window, w, h);
    156 	screen_resize(uxn_screen.width, uxn_screen.height, 1);
    157 }
    158 
    159 static void
    160 set_zoom(Uint8 z, int win)
    161 {
    162 	if(z < 1) return;
    163 	if(win)
    164 		set_window_size(emu_window, (uxn_screen.width + PAD2) * z, (uxn_screen.height + PAD2) * z);
    165 	zoom = z;
    166 }
    167 
    168 static void
    169 set_fullscreen(int value, int win)
    170 {
    171 	Uint32 flags = 0; /* windowed mode; SDL2 has no constant for this */
    172 	fullscreen = value;
    173 	if(fullscreen)
    174 		flags = SDL_WINDOW_FULLSCREEN_DESKTOP;
    175 	if(win)
    176 		SDL_SetWindowFullscreen(emu_window, flags);
    177 }
    178 
    179 static void
    180 set_borderless(int value)
    181 {
    182 	if(fullscreen) return;
    183 	borderless = value;
    184 	SDL_SetWindowBordered(emu_window, !value);
    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(void)
    212 {
    213 	screen_redraw();
    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(void)
    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 = &uxn;
    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 	SDL_PauseAudioDevice(audio_id, 1);
    250 	screen_resize(WIDTH, HEIGHT, 1);
    251 	return 1;
    252 }
    253 
    254 static void
    255 emu_restart(int soft)
    256 {
    257 	screen_resize(WIDTH, HEIGHT, uxn_screen.scale);
    258 	screen_fill(uxn_screen.bg, 0);
    259 	screen_fill(uxn_screen.fg, 0);
    260 	system_reboot(soft);
    261 	SDL_SetWindowTitle(emu_window, "Varvara");
    262 }
    263 
    264 static Uint8
    265 get_button(SDL_Event *event)
    266 {
    267 	switch(event->key.keysym.sym) {
    268 	case SDLK_LCTRL: return 0x01;
    269 	case SDLK_LALT: return 0x02;
    270 	case SDLK_LSHIFT: return 0x04;
    271 	case SDLK_HOME: return 0x08;
    272 	case SDLK_UP: return 0x10;
    273 	case SDLK_DOWN: return 0x20;
    274 	case SDLK_LEFT: return 0x40;
    275 	case SDLK_RIGHT: return 0x80;
    276 	}
    277 	return 0x00;
    278 }
    279 
    280 static Uint8
    281 get_button_joystick(SDL_Event *event)
    282 {
    283 	return 0x01 << (event->jbutton.button & 0x3);
    284 }
    285 
    286 static Uint8
    287 get_vector_joystick(SDL_Event *event)
    288 {
    289 	if(event->jaxis.value < -3200)
    290 		return 1;
    291 	if(event->jaxis.value > 3200)
    292 		return 2;
    293 	return 0;
    294 }
    295 
    296 static Uint8
    297 get_key(SDL_Event *event)
    298 {
    299 	int sym = event->key.keysym.sym;
    300 	SDL_Keymod mods = SDL_GetModState();
    301 	if(sym < 0x20 || sym == SDLK_DELETE)
    302 		return sym;
    303 	if(mods & KMOD_CTRL) {
    304 		if(sym < SDLK_a)
    305 			return sym;
    306 		else if(sym <= SDLK_z)
    307 			return sym - (mods & KMOD_SHIFT) * 0x20;
    308 	}
    309 	return 0x00;
    310 }
    311 
    312 static int
    313 handle_events(void)
    314 {
    315 	SDL_Event event;
    316 	while(SDL_PollEvent(&event)) {
    317 		/* Window */
    318 		if(event.type == SDL_QUIT)
    319 			return 0;
    320 		else if(event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_EXPOSED)
    321 			emu_redraw();
    322 		/* Mouse */
    323 		else if(event.type == SDL_MOUSEMOTION)
    324 			mouse_pos(clamp(event.motion.x - PAD, 0, uxn_screen.width - 1), clamp(event.motion.y - PAD, 0, uxn_screen.height - 1));
    325 		else if(event.type == SDL_MOUSEBUTTONUP)
    326 			mouse_up(SDL_BUTTON(event.button.button));
    327 		else if(event.type == SDL_MOUSEBUTTONDOWN)
    328 			mouse_down(SDL_BUTTON(event.button.button));
    329 		else if(event.type == SDL_MOUSEWHEEL)
    330 			mouse_scroll(event.wheel.x, event.wheel.y);
    331 		/* Controller */
    332 		else if(event.type == SDL_TEXTINPUT) {
    333 			char *c;
    334 			for(c = event.text.text; *c; c++)
    335 				controller_key(*c);
    336 		} else if(event.type == SDL_KEYDOWN) {
    337 			int ksym;
    338 			if(get_key(&event))
    339 				controller_key(get_key(&event));
    340 			else if(get_button(&event))
    341 				controller_down(get_button(&event));
    342 			else if(event.key.keysym.sym == SDLK_F1)
    343 				set_zoom(zoom == 3 ? 1 : zoom + 1, 1);
    344 			else if(event.key.keysym.sym == SDLK_F2)
    345 				emu_deo(0xe, 0x1);
    346 			else if(event.key.keysym.sym == SDLK_F3)
    347 				uxn.dev[0x0f] = 0xff;
    348 			else if(event.key.keysym.sym == SDLK_F4)
    349 				emu_restart(0);
    350 			else if(event.key.keysym.sym == SDLK_F5)
    351 				emu_restart(1);
    352 			else if(event.key.keysym.sym == SDLK_F11)
    353 				set_fullscreen(!fullscreen, 1);
    354 			else if(event.key.keysym.sym == SDLK_F12)
    355 				set_borderless(!borderless);
    356 			ksym = event.key.keysym.sym;
    357 			if(SDL_PeepEvents(&event, 1, SDL_PEEKEVENT, SDL_KEYUP, SDL_KEYUP) == 1 && ksym == event.key.keysym.sym)
    358 				return 1;
    359 		} else if(event.type == SDL_KEYUP)
    360 			controller_up(get_button(&event));
    361 		else if(event.type == SDL_JOYAXISMOTION) {
    362 			Uint8 vec = get_vector_joystick(&event);
    363 			if(!vec)
    364 				controller_up((3 << (!event.jaxis.axis * 2)) << 4);
    365 			else
    366 				controller_down((1 << ((vec + !event.jaxis.axis * 2) - 1)) << 4);
    367 		} else if(event.type == SDL_JOYBUTTONDOWN)
    368 			controller_down(get_button_joystick(&event));
    369 		else if(event.type == SDL_JOYBUTTONUP)
    370 			controller_up(get_button_joystick(&event));
    371 		else if(event.type == SDL_JOYHATMOTION) {
    372 			/* NOTE: Assuming there is only one joyhat in the controller */
    373 			switch(event.jhat.value) {
    374 			case SDL_HAT_UP: controller_down(0x10); break;
    375 			case SDL_HAT_DOWN: controller_down(0x20); break;
    376 			case SDL_HAT_LEFT: controller_down(0x40); break;
    377 			case SDL_HAT_RIGHT: controller_down(0x80); break;
    378 			case SDL_HAT_LEFTDOWN: controller_down(0x40 | 0x20); break;
    379 			case SDL_HAT_LEFTUP: controller_down(0x40 | 0x10); break;
    380 			case SDL_HAT_RIGHTDOWN: controller_down(0x80 | 0x20); break;
    381 			case SDL_HAT_RIGHTUP: controller_down(0x80 | 0x10); break;
    382 			case SDL_HAT_CENTERED: controller_up(0x10 | 0x20 | 0x40 | 0x80); break;
    383 			}
    384 		}
    385 		/* Console */
    386 		else if(event.type == stdin_event)
    387 			console_input(event.cbutton.button, event.cbutton.state);
    388 	}
    389 	return 1;
    390 }
    391 
    392 static int
    393 emu_run(char *rom_path)
    394 {
    395 	Uint64 next_refresh = 0;
    396 	Uint64 frame_interval = SDL_GetPerformanceFrequency() / 60;
    397 	Uint32 window_flags = SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI;
    398 	window_created = 1;
    399 	if(fullscreen)
    400 		window_flags = window_flags | SDL_WINDOW_FULLSCREEN_DESKTOP;
    401 	emu_window = SDL_CreateWindow(rom_path,
    402 		SDL_WINDOWPOS_UNDEFINED,
    403 		SDL_WINDOWPOS_UNDEFINED,
    404 		(uxn_screen.width + PAD2) * zoom,
    405 		(uxn_screen.height + PAD2) * zoom,
    406 		window_flags);
    407 	if(emu_window == NULL)
    408 		return system_error("sdl_window", SDL_GetError());
    409 	emu_renderer = SDL_CreateRenderer(emu_window, -1, SDL_RENDERER_ACCELERATED);
    410 	if(emu_renderer == NULL)
    411 		return system_error("sdl_renderer", SDL_GetError());
    412 	emu_resize(uxn_screen.width, uxn_screen.height);
    413 	/* game loop */
    414 	for(;;) {
    415 		Uint64 now = SDL_GetPerformanceCounter();
    416 		/* .System/halt */
    417 		if(uxn.dev[0x0f])
    418 			return system_error("Run", "Ended.");
    419 		exec_deadline = now + deadline_interval;
    420 		if(!handle_events())
    421 			return 0;
    422 		if(now >= next_refresh) {
    423 			now = SDL_GetPerformanceCounter();
    424 			next_refresh = now + frame_interval;
    425 			uxn_eval(uxn_screen.vector);
    426 			if(screen_changed())
    427 				emu_redraw();
    428 		}
    429 		if(uxn_screen.vector) {
    430 			Uint64 delay_ms = (next_refresh - now) / ms_interval;
    431 			if(delay_ms > 0) SDL_Delay(delay_ms);
    432 		} else
    433 			SDL_WaitEvent(NULL);
    434 	}
    435 }
    436 
    437 static int
    438 emu_end(void)
    439 {
    440 	int exitcode = uxn.dev[0x0f] & 0x7f;
    441 	SDL_CloseAudioDevice(audio_id);
    442 #ifdef _WIN32
    443 #pragma GCC diagnostic ignored "-Wint-to-pointer-cast"
    444 	TerminateThread((HANDLE)SDL_GetThreadID(stdin_thread), 0);
    445 #elif !defined(__APPLE__)
    446 	close(0); /* make stdin thread exit */
    447 #endif
    448 	SDL_Quit();
    449 	return exitcode;
    450 }
    451 
    452 int
    453 main(int argc, char **argv)
    454 {
    455 	int i = 1;
    456 	char *rom_path;
    457 	/* flags */
    458 	if(argc > 1 && argv[i][0] == '-') {
    459 		if(!strcmp(argv[i], "-v"))
    460 			return system_error("Uxn(gui) - Varvara Emulator", "19 Jan 2025.");
    461 		else if(!strcmp(argv[i], "-2x"))
    462 			set_zoom(2, 0);
    463 		else if(!strcmp(argv[i], "-3x"))
    464 			set_zoom(3, 0);
    465 		else if(strcmp(argv[i], "-f") == 0)
    466 			set_fullscreen(1, 0);
    467 		i++;
    468 	}
    469 	/* start */
    470 	rom_path = i == argc ? "boot.rom" : argv[i++];
    471 	if(!system_boot((Uint8 *)calloc(0x10000 * RAM_PAGES + 1, sizeof(Uint8)), rom_path))
    472 		return system_error("usage:", "uxnemu [-v | -f | -2x | -3x] file.rom [args...]");
    473 	if(!emu_init())
    474 		return system_error("Init", "Failed to initialize varvara.");
    475 	/* loop */
    476 	uxn.dev[0x17] = argc > i;
    477 	if(uxn_eval(PAGE_PROGRAM)) {
    478 		console_listen(i, argc, argv);
    479 		emu_run(rom_path);
    480 	}
    481 	return emu_end();
    482 }