/* * vm_screen.c * * Functions here support drawing to the virtual machine's "screen"; * exactly how that is done is an abstraction to the rest of the * program, which only knows to call the functions here. */ #include #include #include #include #include "log.h" #include "vm_event.h" #include "vm_screen.h" typedef struct { /* * The point coordinates of the change on-screen that we are looking * to make */ vm_area area; /* * The new color of the pixel */ vm_color clr; } change; /* * An array of changes, arranged in a stack, plus the position in the * stack where we currently are at. */ static change changestack[65536]; static int changepos = 0; static vm_color curcolor; struct timeval refresh_time; /* * Initialize the video of the vm_screen abstraction. This ends up being * something that depends on our third-party graphics library; in other * words, what do we need to do before we can even create a drawing * surface to work with? If we are unable to properly do so, we return * with an error. */ int vm_screen_init() { if (SDL_Init(SDL_INIT_VIDEO) < 0) { log_crit("Could not initialize video: %s", SDL_GetError()); return ERR_GFXINIT; } memset(&refresh_time, 0, sizeof(struct timeval)); return OK; } /* * Something to do in the teardown phase of the program, undoing * whatever we might have set up in the init function. */ void vm_screen_finish() { SDL_Quit(); } /* * Return a new screen. This ends up blanking out whatever we have here; * we also do _not_ create any drawing surface, which is done separately * with the add_window function. */ vm_screen * vm_screen_create() { vm_screen *screen; screen = (vm_screen *)malloc(sizeof(vm_screen)); if (screen == NULL) { log_crit("Failed to allocate vm_screen"); exit(1); } screen->xcoords = 0; screen->ycoords = 0; screen->last_key = '\0'; screen->key_pressed = false; screen->dirty = false; screen->should_exit = false; screen->window = NULL; screen->render = NULL; return screen; } /* * The logical coordinates of a screen is a grid dimension separate from * what the literal window size of our drawing surface. For instance, a * machine may assume it has 320x240 pixels, but we are drawing on a * surface that is twice the size, 640x480. In effect we aim to decouple * the presumed drawing size from the actual drawing size, so the * machine we emulate does not need to think about it. */ void vm_screen_set_logical_coords(vm_screen *screen, int xcoords, int ycoords) { screen->xcoords = xcoords; screen->ycoords = ycoords; // We allow you to set the x and y coordinates in absence of a // screen renderer, but it's kind of pointless without one. if (screen->render) { SDL_RenderSetLogicalSize(screen->render, screen->xcoords, screen->ycoords); } } /* * This function will add a physical windowed, drawing surface to the * screen object. This ends up being done through the third-party * graphics library. */ int vm_screen_add_window(vm_screen *screen, int width, int height) { // If HEADLESS is defined, we will assume we _don't_ want an actual // drawing surface, but want to pretend we've added one. #ifndef HEADLESS screen->window = SDL_CreateWindow("erc", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width, height, 0); if (screen->window == NULL) { log_crit("Could not create window: %s", SDL_GetError()); return ERR_GFXINIT; } screen->render = SDL_CreateRenderer(screen->window, -1, SDL_RENDERER_SOFTWARE); if (screen->render == NULL) { log_crit("Could not create renderer: %s", SDL_GetError()); return ERR_GFXINIT; } #endif // We plan to draw onto a surface that is xcoords x ycoords in area, // regardless of the actual size of the window. SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "nearest"); // We default to a logical coordinate system of exactly the given // width and height. For emulated systems like the Apple II, this // will not be correct, and the underlying machine system will rerun // the set_logical_coords function with different values. vm_screen_set_logical_coords(screen, width, height); vm_color clr; memset(&clr, 0, sizeof(vm_color)); vm_screen_set_color(screen, clr); vm_screen_prepare(screen); return OK; } /* * Return the limit of the x coordinates our logical size can support. * (This is not the literal width of the window, but rather the width of * the drawing surface the machine will want to work with.) */ int vm_screen_xcoords(vm_screen *screen) { return screen->xcoords; } /* * Like the xcoords function in every way and meaning; except here we * return the y coordinate limit. */ int vm_screen_ycoords(vm_screen *screen) { return screen->ycoords; } /* * Free the contents of a screen. */ void vm_screen_free(vm_screen *screen) { SDL_DestroyRenderer(screen->render); SDL_DestroyWindow(screen->window); free(screen); } /* * Is the screen active? This function will be used by machines to * determine if there's still a need to continue their run loops. */ bool vm_screen_active(vm_screen *scr) { vm_event_poll(scr); // If something happened in the event loop that caused the user to // signal an exit, then returning false here will do the trick if (scr->should_exit) { return false; } return true; } /* * Prepare the screen to be drawn and/or rendered. (Currently, this is * just a RenderClear in SDL.) */ void vm_screen_prepare(vm_screen *scr) { SDL_RenderClear(scr->render); } /* * Do whatever is required to refresh the screen with the changes we've * made recently. */ void vm_screen_refresh(vm_screen *scr) { change *chg; while (changepos >= 0) { chg = &changestack[changepos--]; SDL_SetRenderDrawColor(scr->render, chg->clr.r, chg->clr.g, chg->clr.b, SDL_ALPHA_OPAQUE); MAKE_SDL_RECT(rect, chg->area); SDL_RenderFillRect(scr->render, &rect); } SDL_RenderPresent(scr->render); scr->dirty = false; } /* * Set the color of a screen screen to a given RGBA value. */ void vm_screen_set_color(vm_screen *scr, vm_color clr) { if (scr->render) { SDL_SetRenderDrawColor(scr->render, clr.r, clr.g, clr.b, SDL_ALPHA_OPAQUE); } curcolor = clr; } /* * Draw a rectangle on the screen at a given x/y position, with a given * set of x/y dimensions, with a given screen. */ void vm_screen_draw_rect(vm_screen *screen, vm_area *area) { change *chg; // FIXME: magic number if (changepos >= 65536) { return; } chg = &changestack[changepos++]; memcpy(&chg->area, area, sizeof(vm_area)); chg->clr = curcolor; // The renderer will take care of translating the positions and // sizes into whatever the window is really at. //MAKE_SDL_RECT(rect, *area); //SDL_RenderFillRect(screen->render, &rect); screen->dirty = true; } /* * Just a simple wrapper around the key pressed state (mostly in case we * ever switch from SDL to something else, and a field stops making * sense). */ bool vm_screen_key_pressed(vm_screen *scr) { return scr->key_pressed; } /* * Similar logic as for key_pressed; this is just a dumb getter for the * last_key field. */ char vm_screen_last_key(vm_screen *scr) { return scr->last_key; } /* * Return true if the screen is considered dirty (i.e., if the screen * needs to be redrawn). */ bool vm_screen_dirty(vm_screen *scr) { struct timeval now; if (scr->dirty) { // If this returns an error, I have to assume the computer // itself may be on fire, or has grown fangs and is presently // nibbling on the user if (gettimeofday(&now, NULL) < 0) { return false; } if (now.tv_sec > refresh_time.tv_sec || (now.tv_usec > refresh_time.tv_usec + 50000) ) { memcpy(&refresh_time, &now, sizeof(struct timeval)); return true; } } return false; }