apple2ix/src/video/ncvideo.c
Aaron Culliney e4e0c941d3 Refactor CPU assembly for simplicity and efficiency
- Variables needed in assembly now accessible in a struct pointer that  avoids __PIC__ nastiness
    - Reduces code size and execution time for CPU thread
2018-01-15 16:19:21 -08:00

771 lines
21 KiB
C

/*
* Apple // emulator for *ix
*
* This software package is subject to the GNU General Public License
* version 3 or later (your choice) as published by the Free Software
* Foundation.
*
* Copyright 2017 Aaron Culliney
*
*/
// ncurses renderer (for those of us who still CLI ;)
#include "common.h"
#include "video/video.h"
#include <sys/poll.h>
#if HAVE_NCURSES_H
# include <ncurses.h>
#elif HAVE_NCURSESW_NCURSES_H
# include <ncursesw/ncurses.h>
#elif HAVE_NCURSES_NCURSES_H
# include <ncurses/ncurses.h>
#elif HAVE_NCURSES_CURSES_H
# include <ncurses/curses.h>
#elif HAVE_CURSES_H
# include <curses.h>
#endif
#define THIRTYFPS NANOSECONDS_PER_SECOND / 30UL // 30FPS
#define ASCII_BACK 0x08
#define ASCII_TAB 0x09
#define ASCII_LF 0x0a
#define ASCII_CR 0x0d
#define ASCII_ESC 0x1b
//#define COLOR_BLACK 0
//#define COLOR_MAGENTA 5
#define COLOR_DARKBLUE 32
#define COLOR_PURPLE 33
#define COLOR_DARKGREEN 34
#define COLOR_DARKGREY 35
#define COLOR_MEDBLUE 36
#define COLOR_LIGHTBLUE 37
#define COLOR_BROWN 38
#define COLOR_ORANGE 39
#define COLOR_LIGHTGREY 40
#define COLOR_PINK 41
//#define COLOR_GREEN 2
//#define COLOR_YELLOW 3
#define COLOR_AQUA 42
//#define COLOR_WHITE 7
static bool ncvideo_running = true;
static WINDOW *winCurr = NULL;
static WINDOW *winMenu = NULL; // 24 x 80 text
static WINDOW *winTxt40 = NULL; // 24 x 40 text
static WINDOW *winTxt80 = NULL; // 24 x 80 text
static WINDOW *winScale = NULL; // graphics mode scaled/interpolated to terminal dimensions
// ----------------------------------------------------------------------------
// ncurses video backend helper routines
static void _nc_convertAppleGlyphs(INOUT chtype *c, OUTPARM char **s, uint8_t *cs) {
switch (*c) {
case 0x7f: // cursor ...
*c = '_';
*cs = BLACK_ON_RED;
break;
case 0x80: // closed apple ...
*c = '@';
*cs = BLACK_ON_MAGENTA;
break;
case 0x81: // open apple ...
*c = '@';
*cs = BLACK_ON_RED;
break;
case 0x82: // caret ...
*c = '^';
*cs = BLACK_ON_RED;
break;
case 0x83: // hourglass ...
*c = '%';
*cs = BLACK_ON_RED;
break;
case 0x84: // checkmark ...
#if NCURSES_UTF8
*c = '\0';
*s = "";
#else
*c = 'x';
#endif
*cs = BLACK_ON_RED;
break;
case 0x85: // reverse checkmark ...
#if NCURSES_UTF8
*c = '\0';
*s = "";
////*cs = INVERSE_CURRENT;
#else
*c = 'X';
#endif
*cs = BLACK_ON_RED;
break;
case 0x86: // runner left ...
*c = ']';
*cs = BLACK_ON_RED;
break;
case 0x87: // runner right ...
*c = '[';
*cs = BLACK_ON_RED;
break;
case 0x88: // left arrow ...
#if NCURSES_UTF8
*c = '\0';
*s = "";
#else
*c = '<';
#endif
*cs = BLACK_ON_RED;
break;
case 0x89: // ...
#if NCURSES_UTF8
*c = '\0';
*s = "";
#else
*c = '.';
#endif
*cs = BLACK_ON_RED;
break;
case 0x8a: // down arrow ...
#if NCURSES_UTF8
*c = '\0';
*s = "";
#else
*c = '!';
#endif
*cs = BLACK_ON_RED;
break;
case 0x8b: // up arrow ...
#if NCURSES_UTF8
*c = '\0';
*s = "";
#else
*c = '^';
#endif
*cs = BLACK_ON_RED;
break;
case 0x8c: // top bar ...
#if NCURSES_UTF8
*c = '\0';
*s = "¯";
#else
*c = '-';
#endif
*cs = BLACK_ON_RED;
break;
case 0x8d: // CR ...
#if NCURSES_UTF8
*c = '\0';
*s = "";
#else
*c = '^';
#endif
*cs = BLACK_ON_RED;
break;
case 0x8e: // block ...
#if NCURSES_UTF8
*c = '\0';
*s = "";
#else
*c = '#';
#endif
*cs = BLACK_ON_RED;
break;
case 0x8f: // filled left arrow ...
#if NCURSES_UTF8
*c = '\0';
*s = "";
#else
*c = '<';
#endif
*cs = BLACK_ON_RED;
break;
case 0x90: // filled right arrow ...
#if NCURSES_UTF8
*c = '\0';
*s = "";
#else
*c = '>';
#endif
*cs = BLACK_ON_RED;
break;
case 0x91: // filled down arrow ...
#if NCURSES_UTF8
*c = '\0';
*s = "";
#else
*c = '!';
#endif
*cs = BLACK_ON_RED;
break;
case 0x92: // filled up arrow ...
#if NCURSES_UTF8
*c = '\0';
*s = "";
#else
*c = '^';
#endif
*cs = BLACK_ON_RED;
break;
case 0x93: // mdash ...
#if NCURSES_UTF8
*c = '\0';
*s = "";
#else
*c = '-';
#endif
*cs = BLACK_ON_RED;
break;
case 0x94: // box bottom left ...
*c = '\\';
*cs = BLACK_ON_RED;
break;
case 0x95: // right arrow ...
#if NCURSES_UTF8
*c = '\0';
*s = "";
#else
*c = '>';
#endif
*cs = BLACK_ON_RED;
break;
case 0x96: // hash #1 ...
*c = '#';
*cs = BLACK_ON_RED;
break;
case 0x97: // hash #2 ...
*c = '#';
*cs = BLACK_ON_RED;
break;
case 0x98: // folder left ...
*c = 'f';
*cs = BLACK_ON_RED;
break;
case 0x99: // folder right ...
*c = 'f';
*cs = BLACK_ON_RED;
break;
case 0x9a: // right full bar ...
*c = '|';
*cs = BLACK_ON_RED;
break;
case 0x9b: // diamond ...
#if NCURSES_UTF8
*c = '\0';
*s = "";
#else
*c = '*';
#endif
*cs = BLACK_ON_RED;
break;
case 0x9c: // top and bottom bars ...
#if NCURSES_UTF8
*c = '\0';
*s = "";
#else
*c = '=';
#endif
*cs = BLACK_ON_RED;
break;
case 0x9d: // white plus ...
*c = '+';
*cs = BLACK_ON_RED;
break;
case 0x9e: // box and dot ...
#if NCURSES_UTF8
*c = '\0';
*s = "";
#else
*c = '#';
#endif
*cs = BLACK_ON_RED;
break;
case 0x9f: // left full bar ...
*c = '|';
*cs = BLACK_ON_RED;
break;
// interface menus ...
case 0xA0:
case 0xA3:
*c = '/';
break;
case 0xA1:
case 0xA2:
*c = '\\';
break;
case 0xA4:
*c = '|';
break;
case 0xA5:
*c = '-';
break;
case 0xA6:
case 0xA7:
case 0xA8:
case 0xA9:
case 0xAA:
*c = '+';
break;
}
}
static int _nc_keyToEmulator(int ncKey, int *isCooked) {
int a2key = ncKey;
bool isASCII = false;
switch (ncKey) {
case KEY_F(1):
a2key = SCODE_F1;
break;
case KEY_F(2):
a2key = SCODE_F2;
break;
case KEY_F(3):
a2key = SCODE_F3;
break;
case KEY_F(4):
a2key = SCODE_F4;
break;
case KEY_F(5):
a2key = SCODE_F5;
break;
case KEY_F(6):
a2key = SCODE_F6;
break;
case KEY_F(7):
a2key = SCODE_F7;
break;
case KEY_F(8):
a2key = SCODE_F8;
break;
case KEY_F(9):
a2key = SCODE_F9;
break;
case KEY_F(10):
a2key = SCODE_F10;
break;
case KEY_F(11):
a2key = SCODE_F11;
break;
case KEY_F(12):
a2key = SCODE_F12;
break;
case KEY_DOWN:
a2key = SCODE_D;
break;
case KEY_BACKSPACE:
case KEY_LEFT:
case ASCII_BACK:
a2key = SCODE_L;
break;
case KEY_RIGHT:
a2key = SCODE_R;
break;
case KEY_UP:
a2key = SCODE_U;
break;
case ASCII_LF:
case ASCII_CR:
a2key = SCODE_RET;
break;
case ASCII_ESC:
a2key = SCODE_ESC;
break;
case ASCII_TAB:
default:
if (a2key >= 0 && a2key < 256) {
isASCII = true;
} else {
a2key = -1;
}
break;
}
*isCooked = isASCII;
return a2key;
}
#define COLOR_VALUES(x) \
((NCURSES_COLOR_T)(colormap[x].red ) * 1000)/255, \
((NCURSES_COLOR_T)(colormap[x].green) * 1000)/255, \
((NCURSES_COLOR_T)(colormap[x].blue) * 1000)/255
static void _nc_initColors(void) {
if (has_colors() == FALSE) {
LOG("Your terminal does not support color, so emulated color values will not be correct...");
return;
}
if (can_change_color() == FALSE) {
LOG("Your terminal does not support changing color values, so emulated color values will not be correct...");
// drop through to start default color support ...
}
int ret = start_color();
if (ret == ERR) {
LOG("OOPS, could not initialize ncurses colors");
}
// ncurses scales colors between 0 - 1000
ret = init_color(COLOR_MAGENTA ,COLOR_VALUES(IDX_MAGENTA ));
ret = init_color(COLOR_DARKBLUE ,COLOR_VALUES(IDX_DARKBLUE ));
ret = init_color(COLOR_PURPLE ,COLOR_VALUES(IDX_PURPLE ));
ret = init_color(COLOR_DARKGREEN,COLOR_VALUES(IDX_DARKGREEN));
ret = init_color(COLOR_DARKGREY ,COLOR_VALUES(IDX_DARKGREY ));
ret = init_color(COLOR_MEDBLUE ,COLOR_VALUES(IDX_MEDBLUE ));
ret = init_color(COLOR_LIGHTBLUE,COLOR_VALUES(IDX_LIGHTBLUE));
ret = init_color(COLOR_BROWN ,COLOR_VALUES(IDX_BROWN ));
ret = init_color(COLOR_ORANGE ,COLOR_VALUES(IDX_ORANGE ));
ret = init_color(COLOR_LIGHTGREY,COLOR_VALUES(IDX_LIGHTGREY));
ret = init_color(COLOR_PINK ,COLOR_VALUES(IDX_PINK ));
ret = init_color(COLOR_GREEN ,COLOR_VALUES(IDX_GREEN ));
ret = init_color(COLOR_YELLOW ,COLOR_VALUES(IDX_YELLOW ));
ret = init_color(COLOR_AQUA ,COLOR_VALUES(IDX_AQUA ));
// interface and mousetext colors
init_pair(1+GREEN_ON_BLACK, COLOR_GREEN, COLOR_BLACK );
init_pair(1+GREEN_ON_BLUE, COLOR_GREEN, COLOR_BLUE );
init_pair(1+RED_ON_BLACK, COLOR_RED, COLOR_BLACK );
init_pair(1+BLUE_ON_BLACK, COLOR_BLUE, COLOR_BLACK );
init_pair(1+WHITE_ON_BLACK, COLOR_WHITE, COLOR_BLACK );
init_pair(1+BLACK_ON_RED, COLOR_BLACK, COLOR_RED );
// 16 COLORS:
init_pair(1+BLACK_ON_BLACK, COLOR_BLACK, COLOR_BLACK );
init_pair(1+BLACK_ON_MAGENTA, COLOR_BLACK, COLOR_MAGENTA );
init_pair(1+BLACK_ON_DARKBLUE, COLOR_BLACK, COLOR_DARKBLUE );
init_pair(1+BLACK_ON_PURPLE, COLOR_BLACK, COLOR_PURPLE );
init_pair(1+BLACK_ON_DARKGREEN, COLOR_BLACK, COLOR_DARKGREEN);
init_pair(1+BLACK_ON_DARKGREY, COLOR_BLACK, COLOR_DARKGREY );
init_pair(1+BLACK_ON_MEDBLUE, COLOR_BLACK, COLOR_MEDBLUE );
init_pair(1+BLACK_ON_LIGHTBLUE, COLOR_BLACK, COLOR_LIGHTBLUE);
init_pair(1+BLACK_ON_BROWN, COLOR_BLACK, COLOR_BROWN );
init_pair(1+BLACK_ON_ORANGE, COLOR_BLACK, COLOR_ORANGE );
init_pair(1+BLACK_ON_LIGHTGREY, COLOR_BLACK, COLOR_LIGHTGREY);
init_pair(1+BLACK_ON_PINK, COLOR_BLACK, COLOR_PINK );
init_pair(1+BLACK_ON_GREEN, COLOR_BLACK, COLOR_GREEN );
init_pair(1+BLACK_ON_YELLOW, COLOR_BLACK, COLOR_YELLOW );
init_pair(1+BLACK_ON_AQUA, COLOR_BLACK, COLOR_AQUA );
init_pair(1+BLACK_ON_WHITE, COLOR_BLACK, COLOR_WHITE );
}
static WINDOW *_nc_newwin(unsigned int height, int width, int starty, int startx) {
WINDOW *win = newwin(height+2, width+2, starty-1, startx-1);
//box(win, 0 , 0);
//meta(win, TRUE);
return win;
}
static void _nc_delwin(WINDOW **winRef) {
wborder(*winRef, ' ', ' ', ' ',' ',' ',' ',' ',' ');
wrefresh(*winRef);
delwin(*winRef);
*winRef = NULL;
}
// ----------------------------------------------------------------------------
// ncurses video backend main routines
static const char *ncvideo_name(void) {
return "ncurses";
}
static void ncvideo_init(void *context) {
// ...
}
static void ncvideo_main_loop(void) {
// start curses mode and check colors ...
initscr();
_nc_initColors();
LOG("ncurses video main loop beginning, silencing STDERR logging ...");
do_std_logging = false;
noecho(); // Do not echo output ...
raw(); // Line buffering disabled ...
keypad(stdscr, TRUE); // Special and F keys ...
nodelay(stdscr, TRUE); // getch() is non-blocking ...
int starty24 = (LINES - TEXT_ROWS) / 2;
int startx40 = (COLS - 40) / 2;
int startx80 = (COLS - 80) / 2;
winMenu = _nc_newwin(TEXT_ROWS, 80, starty24, startx80);
winTxt40 = _nc_newwin(TEXT_ROWS, 40, starty24, startx40);
winTxt80 = _nc_newwin(TEXT_ROWS, 80, starty24, startx80);
winScale = _nc_newwin(LINES, COLS, 0, 0);
////wbkgd(winTxt40, COLOR_PAIR(1)); // GREEN ON BLACK (for now)
////wbkgd(winTxt80, COLOR_PAIR(1)); // GREEN ON BLACK (for now)
static uint8_t fb[SCANWIDTH*SCANHEIGHT*sizeof(uint8_t)];
#if INTERFACE_CLASSIC
interface_setStagingFramebuffer(fb);
#endif
while (ncvideo_running) {
unsigned long wasDirty = 0UL;
if (!cpu_isPaused()) {
// check if a2 video memory is dirty
wasDirty = video_clearDirty(A2_DIRTY_FLAG);
if (wasDirty) {
display_renderStagingFramebuffer(fb);
}
}
if (interface_isShowing()) {
winCurr = winMenu;
WINDOW *winPrev = winCurr;
if (winPrev != winCurr) {
wclear(winPrev);
}
}
wasDirty = video_clearDirty(FB_DIRTY_FLAG);
if (wasDirty) {
wrefresh(winCurr);
}
// handle keyboard input
int c = getch();
int is_ascii = 0;
if (c == ERR) {
c_keys_handle_input(-1, /*pressed:*/0, /*is_ascii:*/0);
} else {
c = _nc_keyToEmulator(c, &is_ascii);
if (is_ascii) {
c_keys_handle_input(c, /*pressed:*/1, /*is_ascii:*/1);
} else {
c_keys_handle_input(c, /*pressed:*/1, /*is_ascii:*/0);
c_keys_handle_input(c, /*pressed:*/0, /*is_ascii:*/0);
}
}
// FIXME TODO ... does not account for execution time drift
struct timespec thirtyfps = { .tv_sec = 0, .tv_nsec = THIRTYFPS };
nanosleep(&thirtyfps, NULL);
}
_nc_delwin(&winMenu);
_nc_delwin(&winTxt40);
_nc_delwin(&winTxt80);
_nc_delwin(&winScale);
endwin();
}
static void ncvideo_render(void) {
// no-op ...
}
static void ncvideo_shutdown(void) {
ncvideo_running = false;
}
// ----------------------------------------------------------------------------
// plotting callbacks ...
static void _nc_graphicsUpdate(pixel_delta_t pixel) {
// TODO FIXME ... actually need to scale/plot graphics here
uint8_t cs = pixel.cs;
uint8_t row = pixel.row;
uint8_t col = pixel.col;
assert(cs == COLOR16);
WINDOW *win = winCurr;
chtype c = 0;
char *s = NULL;
uint8_t color = (pixel.b & 0x0F);
c = ' ';
switch (color) {
case 0x0: // Black
cs = BLACK_ON_BLACK;
break;
case 0x1: // Magenta
cs = BLACK_ON_MAGENTA;
break;
case 0x2: // Dark Blue
cs = BLACK_ON_DARKBLUE;
break;
case 0x3: // Purple
cs = BLACK_ON_PURPLE;
break;
case 0x4: // Dark Green
cs = BLACK_ON_DARKGREEN;
break;
case 0x5: // Dark Grey
cs = BLACK_ON_DARKGREY;
break;
case 0x6: // Medium Blue
cs = BLACK_ON_MEDBLUE;
break;
case 0x7: // Light Blue
cs = BLACK_ON_LIGHTBLUE;
break;
case 0x8: // Brown
cs = BLACK_ON_BROWN;
break;
case 0x9: // Orange
cs = BLACK_ON_ORANGE;
break;
case 0xa: // Light Grey
cs = BLACK_ON_LIGHTGREY;
break;
case 0xb: // Pink
cs = BLACK_ON_PINK;
break;
case 0xc: // Green
cs = BLACK_ON_GREEN;
break;
case 0xd: // Yellow
cs = BLACK_ON_YELLOW;
break;
case 0xe: // Aqua
cs = BLACK_ON_AQUA;
break;
case 0xf: // WHITE
cs = BLACK_ON_WHITE;
break;
}
}
static void _nc_textUpdate(pixel_delta_t pixel) {
uint8_t cs = pixel.cs;
uint8_t row = pixel.row;
uint8_t col = pixel.col;
WINDOW *win = winCurr;
chtype c = 0;
char *s = NULL;
font_mode_t fontMode = FONT_MODE_NORMAL;
do {
if (cs == COLOR16) {
// LORES/DLORES ...
_nc_graphicsUpdate(pixel);
return;
} else if (cs == INVALID_COLORSCHEME) {
// TEXT
c = keys_apple2ASCII(pixel.b, &fontMode);
_nc_convertAppleGlyphs(&c, &s, &cs);
} else {
// interface menu
c = pixel.b;
_nc_convertAppleGlyphs(&c, &s, &cs);
win = winMenu;
}
attr_t attrs = 0;
{
short ignoredPair = 0;
if (wattr_get(win, &attrs, &ignoredPair, /*opts:*/NULL) == ERR) {
break;
}
}
if (fontMode == FONT_MODE_INVERSE) {
wattron(win, A_REVERSE);
} else if (fontMode == FONT_MODE_FLASH) {
wattron(win, A_BOLD); // TODO FIXME ... A_BLINK not reliable, but maybe switch between normal and A_REVERSE at the flash rate?
}
if (cs != INVALID_COLORSCHEME) {
wattr_set(win, attrs, cs+1, /*opts:*/NULL);
}
#if NCURSES_UTF8
if (s) {
mvwaddstr(win, row+1, col+1, s);
} else
#endif
mvwaddch(win, row+1, col+1, c);
if (cs != INVALID_COLORSCHEME) {
wattr_set(win, attrs, 0, /*opts:*/NULL);
}
if (fontMode == FONT_MODE_INVERSE) {
wattroff(win, A_REVERSE);
} else if (fontMode == FONT_MODE_FLASH) {
wattroff(win, A_BOLD);
}
} while (0);
}
static void _nc_modeChange(pixel_delta_t pixel) {
#warning FIXME TODO, this copies display.c/vm.c routines somewhat, likely need to consolidate if/when other video backends implement video update callbacks
if (interface_isShowing()) {
return;
}
WINDOW *winPrev = winCurr;
uint32_t currswitches = run_args.softswitches;
if ((currswitches & SS_TEXT) && !(currswitches & SS_MIXED)) {
winCurr = (currswitches & SS_80COL) ? winTxt80 : winTxt40;
} else {
winCurr = winScale;
}
if (winPrev != winCurr) {
wclear(winPrev);
wrefresh(winPrev);
}
}
// ----------------------------------------------------------------------------
static void _init_ncvideo(void) {
LOG("Initializing ncurses renderer");
static video_backend_s ncvideo_backend = { 0 };
ncvideo_backend.name = &ncvideo_name;
ncvideo_backend.init = &ncvideo_init;
ncvideo_backend.main_loop = &ncvideo_main_loop;
ncvideo_backend.render = &ncvideo_render;
ncvideo_backend.shutdown = &ncvideo_shutdown;
static video_animation_s ncvideo_anim = {
#if 1
0
// FIXME TODO ... likely we need to follow the 'glnode' model with 'ncnode.c' and render the ncalert.c stuff over the underlying winCurr here ...
#else
.animation_showMessage = &_ncanim_showMessage;
.animation_showPaused = &_ncanim_showPaused;
.animation_showCPUSpeed = &_ncanim_showCPUSpeed;
.animation_showDiskChosen = &_ncanim_showDiskChosen;
.animation_showTrackSector = &_ncanim_showTrackSector;
#endif
};
ncvideo_backend.anim = &ncvideo_anim;
video_registerBackend(&ncvideo_backend, VID_PRIO_TERMINAL);
display_setUpdateCallback(DRAWPAGE_TEXT, &_nc_textUpdate);
display_setUpdateCallback(DRAWPAGE_MODE_CHANGE, &_nc_modeChange);
}
static __attribute__((constructor)) void __init_ncvideo(void) {
emulator_registerStartupCallback(CTOR_PRIORITY_EARLY, &_init_ncvideo);
}