/* * TR8 VM Player * Copyright (C) 2023 by Juan J. Martinez * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * */ #include #include #include #include "SDL.h" #include "opl3.h" #include "vm.h" #define ARGB(r, g, b) ((uint32_t)(0xff000000|((r)<<16)|((g)<<8)|(b))) #define APP_NAME "TR8 VM Player" #define APP_VERSION "0.1-alpha" #define TR8_W 128 #define TR8_H 128 #define WINDOW_SCALE 4 #define WINDOW_W (TR8_W * WINDOW_SCALE) #define WINDOW_H (TR8_H * WINDOW_SCALE) #define TR8_SAMPLE_RATE 44100 #define TR8_SAMPLE_CHANNELS 1 #define TR8_SAMPLE_SIZE 1024 static void resize_full_screen(SDL_Window *win, SDL_Renderer *renderer, int fullscreen, SDL_Rect *s) { if (!fullscreen) { SDL_SetWindowFullscreen(win, 0); s->x = 0; s->y = 0; s->w = WINDOW_W; s->h = WINDOW_H; } else { SDL_SetWindowFullscreen(win, SDL_WINDOW_FULLSCREEN_DESKTOP); int sw, sh, w, h, scale; SDL_GetRendererOutputSize(renderer, &w, &h); sw = w / TR8_W; sh = h / TR8_H; scale = sw > sh ? sh : sw; s->x = (w - (TR8_W * scale)) / 2; s->y = (h - (TR8_H * scale)) / 2; s->w = TR8_W * scale; s->h = TR8_H * scale; } } static uint32_t palette[] = { ARGB(0x00, 0x00, 0x00), ARGB(0x00, 0x00, 0xaa), ARGB(0x00, 0xaa, 0x00), ARGB(0x00, 0xaa, 0xaa), ARGB(0xaa, 0x00, 0x00), ARGB(0xaa, 0x00, 0xaa), ARGB(0xaa, 0x55, 0x00), ARGB(0xaa, 0xaa, 0xaa), ARGB(0x55, 0x55, 0x55), ARGB(0x55, 0x55, 0xff), ARGB(0x55, 0xff, 0x55), ARGB(0x55, 0xff, 0xff), ARGB(0xff, 0x55, 0x55), ARGB(0xff, 0x55, 0xff), ARGB(0xff, 0xff, 0x55), ARGB(0xff, 0xff, 0xff), }; uint8_t ram[UINT16_MAX + 1] = { 0 }; uint32_t *fb_data = NULL; static void update_fb() { for (uint16_t addr = 0; addr < VIDEO_RAM_LEN; addr++) fb_data[addr] = palette[ram[VIDEO_RAM + addr] & 15]; } void write_m(uint16_t addr, uint8_t b) { if (addr >= VIDEO_RAM && addr < VIDEO_RAM + VIDEO_RAM_LEN) fb_data[addr - VIDEO_RAM] = palette[b & 15]; ram[addr] = b; } uint8_t read_m(uint16_t addr) { return ram[addr]; } #define CTL_FIRE1 (1 << 0) #define CTL_FIRE2 (1 << 1) #define CTL_UP (1 << 2) #define CTL_DOWN (1 << 3) #define CTL_LEFT (1 << 4) #define CTL_RIGHT (1 << 5) #define CTL_SELECT (1 << 6) #define CTL_START (1 << 7) uint8_t ctl[2] = { 0 }; static void update_ctl(SDL_Event *ev) { switch (ev->key.keysym.sym) { case SDLK_z: ctl[0] = ev->type == SDL_KEYDOWN ? ctl[0] | CTL_FIRE1 : ctl[0] & ~CTL_FIRE1; break; case SDLK_x: ctl[0] = ev->type == SDL_KEYDOWN ? ctl[0] | CTL_FIRE2 : ctl[0] & ~CTL_FIRE2; break; case SDLK_UP: ctl[0] = ev->type == SDL_KEYDOWN ? ctl[0] | CTL_UP : ctl[0] & ~CTL_UP; break; case SDLK_DOWN: ctl[0] = ev->type == SDL_KEYDOWN ? ctl[0] | CTL_DOWN : ctl[0] & ~CTL_DOWN; break; case SDLK_LEFT: ctl[0] = ev->type == SDL_KEYDOWN ? ctl[0] | CTL_LEFT : ctl[0] & ~CTL_LEFT; break; case SDLK_RIGHT: ctl[0] = ev->type == SDL_KEYDOWN ? ctl[0] | CTL_RIGHT : ctl[0] & ~CTL_RIGHT; break; case SDLK_s: ctl[0] = ev->type == SDL_KEYDOWN ? ctl[0] | CTL_START : ctl[0] & ~CTL_START; break; case SDLK_d: ctl[0] = ev->type == SDL_KEYDOWN ? ctl[0] | CTL_SELECT : ctl[0] & ~CTL_SELECT; break; default: break; } } SDL_mutex *amutex = NULL; opl3_chip opl; uint8_t snd_reg[2] = { 0 }; static void audio_callback(void *userdata, uint8_t *stream, int32_t len) { if (!SDL_LockMutex(amutex)) { SDL_memset(stream, 0, len); while (len >= 2) { OPL3_GenerateResampled((opl3_chip *)userdata, (int16_t *)stream); stream += 2; len -= 2; } SDL_UnlockMutex(amutex); } } uint8_t blt_set = 0; uint8_t blt_paramc = 0; uint8_t blt_param[6] = { 0 }; uint8_t port(uint8_t p, uint8_t v) { switch (p) { /* blitter control */ case 0xb0: /* settings mode */ if (v & 128) { blt_set = 1; blt_paramc = 0; return p; } /* draw (read or write) */ if (v & 5) { blt_set = 0; /* can't be both */ if ((v & 1) && (v & 4)) return 0xff; /* missing parameters */ if (blt_paramc != 6) return 0xff; /* write into vram */ if (v & 1) { uint16_t src = blt_param[0] | (blt_param[1] << 8); for (int16_t y = (int8_t)blt_param[3]; y < (int8_t)blt_param[3] + blt_param[5]; y++) for (int16_t x = (int8_t)blt_param[2]; x < (int8_t)blt_param[2] + blt_param[4]; x++) { uint8_t b = read_m(src++); /* skip transparent if transparent flag is set */ if ((v & 2) && (b & 128)) continue; /* clipping */ if (x < 0 || x >= TR8_W || y < 0 || y >= TR8_H) continue; write_m((uint16_t)(VIDEO_RAM + x + y * 128), b); } } /* read from vram */ else { uint16_t dst = blt_param[0] | (blt_param[1] << 8); for (int16_t y = (int8_t)blt_param[3]; y < (int8_t)blt_param[3] + blt_param[5]; y++) for (int16_t x = (int8_t)blt_param[2]; x < (int8_t)blt_param[2] + blt_param[4]; x++) { uint8_t b = 0; /* clipping */ if (x >= 0 && x < TR8_W && y >= 0 && y < TR8_H) /* skip transparent if transparent flag is set */ if (!((v & 2) && (b & 128))) b = read_m(VIDEO_RAM + x + y * 128); write_m(dst++, b); } } return p; } return 0xff; /* blitter settings */ case 0xb1: if (!blt_set || blt_paramc > 5) return 0xff; blt_param[blt_paramc++] = v; return p; /* sound register selector primary */ case 0xc0: snd_reg[0] = v & 0xff; return p; /* sound data primary */ case 0xc1: if (!SDL_LockMutex(amutex)) { OPL3_WriteReg(&opl, snd_reg[0], v); SDL_UnlockMutex(amutex); } return p; /* sound register selector secondary */ case 0xc2: snd_reg[1] = v & 0xff; return p; /* sound data secondary */ case 0xc3: if (!SDL_LockMutex(amutex)) { OPL3_WriteReg(&opl, 0x100 | snd_reg[1], v); SDL_UnlockMutex(amutex); } return p; /* controller 1 */ case 0xf0: return ctl[0]; /* controller 2 */ case 0xf1: return ctl[1]; default: return 0xff; } } int main(int argc, char *argv[]) { FILE *fd; size_t size; if (argc < 2) { fprintf(stderr, "Usage: input.prg\n"); return 1; } fd = fopen(argv[1], "rb"); if (!fd) { fclose(fd); fprintf(stderr, "Error opening input\n"); return 1; } if (fseek(fd, 0, SEEK_END) == -1) { fclose(fd); fprintf(stderr, "Error opening input\n"); return 1; } size = ftell(fd); if (size > UINT16_MAX + 1) { fclose(fd); fprintf(stderr, "Input is too large\n"); return 1; } fseek(fd, 0, SEEK_SET); if (fread(ram, 1, size, fd) != size) { fclose(fd); fprintf(stderr, "Error reading input\n"); return 1; } fclose(fd); if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER)) { fprintf(stderr, "Failed init: %s\n", SDL_GetError()); return 1; } /* init audio */ OPL3_Reset(&opl, TR8_SAMPLE_RATE); amutex = SDL_CreateMutex(); if (!amutex) { fprintf(stderr, "Failed to create mutex: %s\n", SDL_GetError()); return 1; } SDL_AudioSpec audio_spec, audio_want; SDL_memset(&audio_want, 0, sizeof(audio_want)); audio_want.freq = TR8_SAMPLE_RATE, audio_want.format = AUDIO_S16, audio_want.channels = TR8_SAMPLE_CHANNELS, audio_want.callback = audio_callback, audio_want.userdata = (void *)&opl; audio_want.samples = TR8_SAMPLE_SIZE; SDL_AudioDeviceID audio = SDL_OpenAudioDevice(NULL, 0, &audio_want, &audio_spec, 0); if (audio == 0) fprintf(stderr, "Failed to open audio: %s\n", SDL_GetError()); else SDL_PauseAudioDevice(audio, 0); SDL_Window *screen = SDL_CreateWindow(APP_NAME " " APP_VERSION, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, WINDOW_W, WINDOW_H, SDL_WINDOW_OPENGL); if (!screen) { fprintf(stderr, "Failed to create a window: %s\n", SDL_GetError()); return 1; } SDL_Renderer *renderer = SDL_CreateRenderer(screen, -1, SDL_RENDERER_TARGETTEXTURE | SDL_RENDERER_PRESENTVSYNC | SDL_RENDERER_ACCELERATED); if (!renderer) { fprintf(stderr, "Failed to create the renderer: %s\n", SDL_GetError()); return 1; } SDL_SetHint("SDL_HINT_RENDER_SCALE_QUALITY", "0"); SDL_Texture *canvas = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, TR8_W, TR8_H); if (!canvas) { fprintf(stderr, "Failed to create the canvas: %s\n", SDL_GetError()); return 1; } SDL_Texture *fb = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, TR8_W, TR8_H); if (!fb) { fprintf(stderr, "Failed to create the frame-buffer: %s\n", SDL_GetError()); return 1; } SDL_SetTextureBlendMode(fb, SDL_BLENDMODE_NONE); int fullscreen = 0; SDL_Rect dst; resize_full_screen(screen, renderer, fullscreen, &dst); SDL_SetRenderDrawColor(renderer, 18, 18, 18, 255); SDL_RenderClear(renderer); SDL_RenderPresent(renderer); Tr8 vm; tr8_init(&vm, write_m, read_m, port); int pitch = 0; uint8_t rc; /* update full fb once to sync with RAM */ pitch = 0; fb_data = NULL; SDL_LockTexture(fb, NULL, (void **)&fb_data, &pitch); update_fb(); SDL_UnlockTexture(fb); SDL_Event ev; uint8_t quit = 0; while (!quit) { while (SDL_PollEvent(&ev)) { if (ev.type == SDL_QUIT) quit = 1; if (ev.type == SDL_KEYDOWN) { if (ev.key.keysym.sym == SDLK_ESCAPE) { if (fullscreen) resize_full_screen(screen, renderer, 0, &dst); quit = 1; } if (ev.key.keysym.sym == SDLK_RETURN && (SDL_GetModState() & KMOD_LALT)) { fullscreen ^= 1; resize_full_screen(screen, renderer, fullscreen, &dst); } update_ctl(&ev); } if (ev.type == SDL_KEYUP) update_ctl(&ev); } pitch = 0; fb_data = NULL; SDL_LockTexture(fb, NULL, (void **)&fb_data, &pitch); rc = tr8_frame_int(&vm); if (rc) rc = tr8_eval(&vm); SDL_UnlockTexture(fb); if (!rc) break; /* render to the canvas */ SDL_SetRenderTarget(renderer, canvas); SDL_RenderClear(renderer); SDL_RenderCopy(renderer, fb, NULL, NULL); /* now target the screen */ SDL_SetRenderTarget(renderer, NULL); SDL_RenderClear(renderer); SDL_RenderCopy(renderer, canvas, NULL, &dst); SDL_RenderPresent(renderer); } if (canvas) SDL_DestroyTexture(canvas); if (fb) SDL_DestroyTexture(fb); if (audio > 0) SDL_CloseAudioDevice(audio); if (amutex) SDL_DestroyMutex(amutex); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(screen); return 0; }