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 }