/* * 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 "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) 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; } } 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; /* 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.tr8\n"); return 1; } fd = fopen(argv[1], "rb"); if (!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; } 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); } /* dump vm state to stderr */ if (ev.key.keysym.sym == SDLK_F9) tr8_dump(&vm, stderr); 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); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(screen); return 0; }