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 }