#include "video.h"
#include "codepage.h"
#include "dbg.h"
#include "emu.h"
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
// Simulated character screen.
// This is a copy of the currently displayed output in the terminal window.
// We have a max of 128 rows of up to 256 columns, total of 32KB.
static uint16_t term_screen[64][256];
// Current line in output, lines bellow this are not currently displayed.
// This allows using only part of the terminal.
static int output_row;
// Current cursor row/column position in the terminal.
static unsigned term_posx, term_posy, term_color, term_cursor;
// Current terminal sizes
static unsigned term_sx, term_sy;
// Current emulated video sizes and cursor position
static unsigned vid_posx[8], vid_posy[8], vid_cursor;
static unsigned vid_sx, vid_sy, vid_color, vid_page;
static unsigned vid_font_lines, vid_no_blank;
// Signals that the terminal size needs updating
static volatile int term_needs_update;
// Terminal FD, allows video output even with redirection.
static FILE *tty_file;
// Video is already initialized
static int video_initialized;
// Forward
static void term_goto_xy(unsigned x, unsigned y);
// Signal handler - terminal size changed
// TODO: not used yet.
#if 0
static void sigwinch_handler(int sig)
{
// term_needs_upodate = 1;
}
#endif
static void term_get_size(void)
{
struct winsize ws;
if(ioctl(fileno(tty_file), TIOCGWINSZ, &ws) != -1)
{
// TODO: perhaps restrict to "known" values
term_sx = ws.ws_col;
term_sy = ws.ws_row;
if(term_sx < 40)
term_sx = 40;
else if(term_sx > 240)
term_sx = 240;
if(term_sy < 25)
term_sy = 25;
else if(term_sy > 64)
term_sy = 64;
}
else
{
term_sx = 80;
term_sy = 25;
}
}
// Update posx/posy in BIOS memory
static void update_posxy(void)
{
int vid_size = vid_sy > 25 ? 0x20 : 0x10;
memory[0x44C] = 0x00;
memory[0x44D] = vid_size;
memory[0x44E] = 0x00;
memory[0x44F] = (vid_size * vid_page) & 0x7F;
for(int i = 0; i < 8; i++)
{
memory[0x450 + i * 2] = vid_posx[i];
memory[0x451 + i * 2] = vid_posy[i];
}
memory[0x462] = vid_page;
}
// Clears the terminal data - not the actual terminal screen
static void clear_terminal(void)
{
debug(debug_video, "clear terminal shadow\n");
// Clear screen terminal:
for(int y = 0; y < 64; y++)
for(int x = 0; x < 256; x++)
term_screen[y][x] = 0x0720;
output_row = -1;
term_posx = 0;
term_posy = 0;
// Get current terminal size
term_get_size();
putc('\r', tty_file); // Go to column 0
}
static void set_text_mode(int clear)
{
debug(debug_video, "set text mode%s\n", clear ? " and clear" : "");
// Clear video screen
if(clear)
{
uint16_t *vm = (uint16_t *)(memory + 0xB8000);
for(int i = 0; i < 16384; i++)
vm[i] = 0x0720;
}
for(int i = 0; i < 8; i++)
{
vid_posx[i] = 0;
vid_posy[i] = 0;
}
vid_page = 0;
vid_color = 0x07;
vid_cursor = 1;
// TODO: support other video modes
vid_sx = 80;
vid_sy = 25;
vid_font_lines = 16;
memory[0x449] = 0x03; // video mode
memory[0x44A] = vid_sx;
memory[0x484] = vid_sy - 1;
update_posxy();
}
static unsigned get_last_used_row(void)
{
unsigned max = 0;
for(unsigned y = 0; y < vid_sy; y++)
for(unsigned x = 0; x < vid_sx; x++)
if(term_screen[y][x] != 0x700 && term_screen[y][x] != 0x720)
max = y + 1;
return max;
}
static void exit_video(void)
{
vid_cursor = 1;
check_screen();
unsigned max = get_last_used_row();
term_goto_xy(0, max);
fputs("\x1b[m", tty_file);
fclose(tty_file);
debug(debug_video, "exit video - row %d\n", max);
}
static void init_video(void)
{
debug(debug_video, "starting video emulation.\n");
int tty_fd = open("/dev/tty", O_NOCTTY | O_WRONLY);
if(tty_fd < 0)
{
print_error("error at open TTY, %s\n", strerror(errno));
exit(1);
}
tty_file = fdopen(tty_fd, "w");
atexit(exit_video);
video_initialized = 1;
// Fill the functionality table
memory[0xC0100] = 0x08; // Only mode 3 supported
memory[0xC0101] = 0x00;
memory[0xC0102] = 0x00;
memory[0xC0107] = 0x07; // Support 300, 350 and 400 scanlines
memory[0xC0108] = 0x00; // Active character blocks?
memory[0xC0109] = 0x00; // MAximum character blocks?
memory[0xC0108] = 0xFF; // Support functions
// Set video mode
set_text_mode(1);
clear_terminal();
term_needs_update = 0;
term_cursor = 1;
term_color = 0x07;
}
int video_active(void)
{
return video_initialized;
}
static void set_color(uint8_t c)
{
if(term_color != c)
{
static char cn[8] = "04261537";
fprintf(tty_file, "\x1b[%c;3%c;4%cm", (c & 0x08) ? '1' : '0', cn[c & 7],
cn[(c >> 4) & 7]);
term_color = c;
}
}
// Writes a DOS character to the current terminal position
static void put_vc(uint8_t c)
{
uint16_t uc = get_unicode(c);
if(uc < 128)
putc(uc, tty_file);
else if(uc < 0x800)
{
putc(0xC0 | (uc >> 6), tty_file);
putc(0x80 | (uc & 0x3F), tty_file);
}
else
{
putc(0xE0 | (uc >> 12), tty_file);
putc(0x80 | ((uc >> 6) & 0x3F), tty_file);
putc(0x80 | (uc & 0x3F), tty_file);
}
}
// Move terminal cursor to the position
static void term_goto_xy(unsigned x, unsigned y)
{
if(term_posy < y && (int)term_posy < output_row)
{
int inc = (int)y < output_row ? y - term_posy : output_row - term_posy;
fprintf(tty_file, "\x1b[%dB", inc);
term_posy += inc;
}
if(term_posy < y)
{
putc('\r', tty_file);
// Set background color to black, as some terminals insert lines with
// the current background color.
set_color(term_color & 0x0F);
// TODO: Draw new line with background color from video screen
for(unsigned i = term_posy; i < y; i++)
putc('\n', tty_file);
term_posx = 0;
term_posy = y;
}
if(term_posy > y)
{
fprintf(tty_file, "\x1b[%dA", term_posy - y);
term_posy = y;
}
if(x == 0 && term_posx != 0)
{
putc('\r', tty_file);
term_posx = 0;
}
if(term_posx < x)
{
fprintf(tty_file, "\x1b[%dC", x - term_posx);
term_posx = x;
}
if(term_posx > x)
{
fprintf(tty_file, "\x1b[%dD", term_posx - x);
term_posx = x;
}
}
// Outputs a character with the given attributes at the given position
static void put_vc_xy(uint8_t vc, uint8_t color, unsigned x, unsigned y)
{
term_goto_xy(x, y);
set_color(color);
put_vc(vc);
term_posx++;
if(term_posx >= term_sx)
{
term_posx = 0;
term_posy++;
}
if(output_row < (int)term_posy)
output_row = term_posy;
}
// Compares current screen with memory data
void check_screen(void)
{
// Exit if not in video mode
if(!video_initialized)
return;
uint16_t memp = (vid_page & 7) * (vid_sy > 25 ? 0x2000 : 0x1000);
uint16_t *vm = (uint16_t *)(memory + 0xB8000 + memp);
unsigned max = output_row + 1;
for(unsigned y = output_row + 1; y < vid_sy; y++)
for(unsigned x = 0; x < vid_sx; x++)
if(vm[x + y * vid_sx] != term_screen[y][x])
max = y + 1;
for(unsigned y = 0; y < max; y++)
for(unsigned x = 0; x < vid_sx; x++)
{
int16_t vc = vm[x + y * vid_sx];
if(vc != term_screen[y][x])
{
// Output character
term_screen[y][x] = vc;
put_vc_xy(vc & 0xFF, vc >> 8, x, y);
}
}
if(term_cursor != vid_cursor)
{
term_cursor = vid_cursor;
if(vid_cursor)
fputs("\x1b[?25h", tty_file);
else
fputs("\x1b[?25l", tty_file);
}
if(term_cursor)
// Move cursor
term_goto_xy(vid_posx[vid_page], vid_posy[vid_page]);
fflush(tty_file);
}
static void vid_scroll_up(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, int n,
int page)
{
debug(debug_video, "scroll up %d: (%d, %d) - (%d, %d)\n", n, x0, y0, x1, y1);
// Check parameters
if(x1 >= vid_sx)
x1 = vid_sx - 1;
if(y1 >= vid_sy)
y1 = vid_sy - 1;
if(y0 > y1 || x0 > x1)
return;
if(n > y1 - y0 + 1 || !n)
n = y1 + 1 - y0;
// Scroll TERMINAL if we are scrolling (almost) the entire screen
if(y0 == 0 && y1 >= vid_sy - 2 && x0 < 2 && x1 >= vid_sx - 2)
{
// Update screen before
check_screen();
int m = n > output_row + 1 ? output_row + 1 : n;
if(term_posy < m)
term_goto_xy(0, m);
output_row -= m;
term_posy -= m;
for(unsigned y = 0; y + m < term_sy; y++)
for(unsigned x = 0; x < term_sx; x++)
term_screen[y][x] = term_screen[y + m][x];
for(unsigned y = term_sy - m; y < term_sy; y++)
for(unsigned x = 0; x < term_sx; x++)
term_screen[y][x] = 0x0720;
}
// Scroll VIDEO
uint16_t memp = (page & 7) * (vid_sy > 25 ? 0x2000 : 0x1000);
uint16_t *vm = (uint16_t *)(memory + 0xB8000 + memp);
for(unsigned y = y0; y + n <= y1; y++)
for(unsigned x = x0; x <= x1; x++)
vm[x + y * vid_sx] = vm[x + y * vid_sx + n * vid_sx];
// Set last rows
for(unsigned y = y1 - (n - 1); y <= y1; y++)
for(unsigned x = x0; x <= x1; x++)
vm[x + y * vid_sx] = (vid_color << 8) + 0x20;
}
static void vid_scroll_dwn(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, unsigned n,
int page)
{
debug(debug_video, "scroll down %d: (%d, %d) - (%d, %d)\n", n, x0, y0, x1, y1);
// Check parameters
if(x1 >= vid_sx)
x1 = vid_sx - 1;
if(y1 >= vid_sy)
y1 = vid_sy - 1;
if(y0 > y1 || x0 > x1)
return;
if(n > y1 - y0 + 1 || !n)
n = y1 + 1 - y0;
// TODO: try to scroll TERMINAL
// Scroll VIDEO
uint16_t memp = (page & 7) * (vid_sy > 25 ? 0x2000 : 0x1000);
uint16_t *vm = (uint16_t *)(memory + 0xB8000 + memp);
for(unsigned y = y1; y >= y0 + n; y--)
for(unsigned x = x0; x <= x1; x++)
vm[x + y * vid_sx] = vm[x + y * vid_sx - n * vid_sx];
// Set first rows
for(unsigned y = y0; y < y0 + n; y++)
for(unsigned x = x0; x <= x1; x++)
vm[x + y * vid_sx] = (vid_color << 8) + 0x20;
}
static void set_xy(unsigned x, unsigned y, uint16_t c, uint16_t mask, int page)
{
uint16_t mem = (page & 7) * (vid_sy > 25 ? 0x2000 : 0x1000);
uint16_t *vm = (uint16_t *)(memory + 0xB8000 + mem);
vm[x + y * vid_sx] = (vm[x + y * vid_sx] & mask) | c;
}
static uint16_t get_xy(unsigned x, unsigned y, int page)
{
uint16_t mem = (page & 7) * (vid_sy > 25 ? 0x2000 : 0x1000);
uint16_t *vm = (uint16_t *)(memory + 0xB8000 + mem);
return vm[x + y * vid_sx];
}
static void video_putchar(uint8_t ch, uint16_t at, int page)
{
page = page & 7;
if(ch == 0x0A)
{
vid_posy[page]++;
while(vid_posy[page] >= vid_sy)
{
vid_posy[page] = vid_sy - 1;
vid_scroll_up(0, 0, vid_sx - 1, vid_sy - 1, 1, page);
}
}
else if(ch == 0x0D)
vid_posx[page] = 0;
else if(ch == 0x08)
{
if(vid_posx[page] > 0)
vid_posx[page]--;
}
else
{
if(at & 0xFFFF)
set_xy(vid_posx[page], vid_posy[page], ch, 0xFF00, page);
else
set_xy(vid_posx[page], vid_posy[page], ch + (at << 8), 0, page);
vid_posx[page]++;
if(vid_posx[page] >= vid_sx)
{
vid_posx[page] = 0;
vid_posy[page]++;
while(vid_posy[page] >= vid_sy)
{
vid_posy[page] = vid_sy - 1;
vid_scroll_up(0, 0, vid_sx - 1, vid_sy - 1, 1, page);
}
}
}
update_posxy();
}
void video_putch(char ch)
{
debug(debug_video, "putchar %02x at (%d,%d)\n", ch & 0xFF, vid_posx[vid_page],
vid_posy[vid_page]);
video_putchar(ch, 0xFF00, vid_page);
}
// VIDEO int
void int10()
{
debug(debug_int, "V-10%04X: BX=%04X\n", cpuGetAX(), cpuGetBX());
debug(debug_video, "V-10%04X: BX=%04X\n", cpuGetAX(), cpuGetBX());
if(!video_initialized)
init_video();
unsigned ax = cpuGetAX();
switch(ax >> 8)
{
case 0x00: // SET VIDEO MODE
if((ax & 0x7F) > 3)
debug(debug_video, "-> SET GRAPHICS MODE %x<-\n", ax & 0xFF);
else
{
set_text_mode((ax & 0x80) == 0);
vid_no_blank = ax & 0x80;
}
break;
case 0x01: // SET CURSOR SHAPE
if((cpuGetCX() & 0x6000) == 0x2000) // Hide cursor
vid_cursor = 0;
else
vid_cursor = 1;
break;
case 0x02: // SET CURSOR POS
{
int page = (cpuGetBX() >> 8) & 7;
vid_posx[page] = cpuGetDX() & 0xFF;
vid_posy[page] = cpuGetDX() >> 8;
if(vid_posx[page] >= vid_sx)
vid_posx[page] = vid_sx - 1;
if(vid_posy[page] >= vid_sy)
vid_posy[page] = vid_sy - 1;
update_posxy();
break;
}
case 0x03: // GET CURSOR POS
{
int page = (cpuGetBX() >> 8) & 7;
cpuSetDX(vid_posx[page] + (vid_posy[page] << 8));
cpuSetCX(0x0010);
break;
}
case 0x05: // SELECT DISPLAY PAGE
if((ax & 0xFF) > 7)
debug(debug_video, "WARN: Select display page > 7!\n");
else
{
vid_page = ax & 7;
update_posxy();
}
break;
case 0x06: // SCROLL UP WINDOW
{
uint16_t cx = cpuGetCX(), dx = cpuGetDX();
vid_color = cpuGetBX() >> 8;
vid_scroll_up(cx, cx >> 8, dx, dx >> 8, ax & 0xFF, vid_page);
break;
}
case 0x07: // SCROLL DOWN WINDOW
{
uint16_t cx = cpuGetCX(), dx = cpuGetDX();
vid_color = cpuGetBX() >> 8;
vid_scroll_dwn(cx, cx >> 8, dx, dx >> 8, ax & 0xFF, vid_page);
break;
}
case 0x08: // READ CHAR AT CURSOR
{
int page = (cpuGetBX() >> 8) & 7;
cpuSetAX(get_xy(vid_posx[page], vid_posy[page], page));
break;
}
case 0x09: // WRITE CHAR AT CURSOR
case 0x0A: // WRITE CHAR ONLY AT CURSOR
{
int page = (cpuGetBX() >> 8) & 7;
uint16_t px = vid_posx[page];
uint16_t py = vid_posy[page];
uint16_t mask = (ax & 0x0100) ? 0 : 0xFF00;
uint16_t ch = ((ax & 0xFF) | (cpuGetBX() << 8)) & ~mask;
for(int i = cpuGetCX(); i > 0; i--)
{
set_xy(px, py, ch, mask, page);
px++;
if(px >= vid_sx)
{
px = 0;
py++;
if(py >= vid_sy)
py = 0;
}
}
break;
}
case 0x0E: // TELETYPE OUTPUT
video_putchar(ax, 0xFF00, (cpuGetBX() >> 8) & 7);
break;
case 0x0F: // GET CURRENT VIDEO MODE
cpuSetAX((vid_sx << 8) | 0x0003 | vid_no_blank); // 80x25 mode
cpuSetBX((vid_page << 8) | (0xFF & cpuGetBX()));
break;
case 0x10:
if(ax == 0x1002) // TODO: Set pallete registers - ignore
break;
else if(ax == 0x1003) // TODO: Set blinking state
break;
debug(debug_video, "UNHANDLED INT 10, AX=%04x\n", ax);
break;
case 0x11:
if(ax == 0x1130)
{
cpuSetDX((vid_sy - 1) & 0xFF);
cpuSetCX(vid_font_lines);
}
else if(ax == 0x1104 || ax == 0x1111 || ax == 0x1114)
{
// Clear end-of-screen
unsigned max = get_last_used_row();
debug(debug_video, "set 25 lines mode %d\n", max);
if(max > 25)
{
term_goto_xy(0, 24);
set_color(0x07);
fputs("\x1b[J", tty_file);
for(int y = 25; y < 64; y++)
for(int x = 0; x < 256; x++)
term_screen[y][x] = 0x0720;
if(output_row > 24)
output_row = 24;
}
// Set 8x16 font - 80x25 mode:
vid_sy = 25;
vid_font_lines = 16;
memory[0x484] = vid_sy - 1;
update_posxy();
}
else if(ax == 0x1102 || ax == 0x1112)
{
// Set 8x8 font - 80x43 or 80x50 mode:
debug(debug_video, "set 43/50 lines mode\n");
// Hack - QBASIC.EXE assumes that the mode is always 50 lines on VGA,
// and *sets* the height into the BIOS area!
if(memory[0x484] > 42)
vid_sy = 50;
else
vid_sy = 43;
vid_font_lines = 8;
memory[0x484] = vid_sy - 1;
update_posxy();
}
break;
case 0x12: // ALT FUNCTION SELECT
{
int bl = cpuGetBX() & 0xFF;
if(bl == 0x10) // GET EGA INFO
{
cpuSetBX(0x0003);
cpuSetCX(0x0000);
cpuSetAX(0);
}
else if(bl == 0x30) // SET VERTICAL RESOLUTION
{
// TODO: select 25/28 lines
cpuSetAX(0x1212);
}
else
debug(debug_video, "UNHANDLED INT 10, AH=12 BL=%02x\n", bl);
}
break;
case 0x13: // WRITE STRING
{
int page = (cpuGetBX() >> 8) & 7;
vid_posx[page] = cpuGetDX() & 0xFF;
vid_posy[page] = cpuGetDX() >> 8;
if(vid_posx[page] >= vid_sx)
vid_posx[page] = vid_sx - 1;
if(vid_posy[page] >= vid_sy)
vid_posy[page] = vid_sy - 1;
int save_posx = vid_posx[page];
int save_posy = vid_posy[page];
int addr = cpuGetAddrES(cpuGetBP());
int cnt = cpuGetCX();
if(ax & 2)
{
while(cnt && addr < 0xFFFFF)
{
video_putchar(memory[addr], memory[addr + 1], page);
addr += 2;
cnt--;
}
}
else
{
uint8_t at = cpuGetBX() >> 8;
while(cnt && addr <= 0xFFFFF)
{
video_putchar(memory[addr], at, page);
addr++;
cnt--;
}
}
if(!(ax & 1))
{
vid_posx[page] = save_posx;
vid_posy[page] = save_posy;
}
update_posxy();
}
break;
case 0x1A: // GET/SET DISPLAY COMBINATION CODE
cpuSetAX(0x001A);
cpuSetBX(0x0008); // VGA + analog color display
break;
case 0x1B: // STATE INFO
if(cpuGetBX() == 0x0000)
{
int addr = cpuGetAddrES(cpuGetDI());
if(addr < 0xFFF00)
{
// Store state information
memset(memory + addr, 0, 64);
memory[addr + 0] = 0x00;
memory[addr + 1] = 0x01;
memory[addr + 2] = 0x00;
memory[addr + 3] = 0xC0; // static-func table at C000:0000
memory[addr + 4] = 0x03; // Video mode
memory[addr + 5] = vid_sx;
memory[addr + 6] = vid_sx >> 8;
for(int i = 0; i < 8; i++)
{
memory[addr + 11 + i * 2] = vid_posx[i];
memory[addr + 12 + i * 2] = vid_posy[i];
}
memory[addr + 27] = vid_cursor * 6; // cursor start scanline
memory[addr + 28] = vid_cursor * 7; // cursor end scanline
memory[addr + 29] = 0; // current page
memory[addr + 30] = 0xD4;
memory[addr + 31] = 0x03; // CRTC port: 03D4
memory[addr + 34] = vid_sy;
memory[addr + 35] = vid_font_lines;
memory[addr + 36] = 0x00; // font lines: 0010
memory[addr + 39] = 0x10;
memory[addr + 40] = 0x00; // # of colors: 0010
memory[addr + 41] = vid_sy > 25 ? 4 : 8; // # of pages
memory[addr + 42] = 2; // # of scan-lines - get from vid_sy
memory[addr + 49] = 3; // 256k memory
cpuSetAX(0x1B1B);
}
}
break;
case 0xEF: // TEST MSHERC.COM DISPLAY TYPE
// Ignored
break;
default:
debug(debug_video, "UNHANDLED INT 10, AX=%04x\n", ax);
}
}
// CRTC port emulation, some software use it to fix "snow" in CGA modes.
static uint8_t crtc_port;
static uint16_t crtc_cursor_loc;
uint8_t video_crtc_read(int port)
{
if(port & 1)
{
if(crtc_port == 0x0E)
return crtc_cursor_loc >> 8;
if(crtc_port == 0x0F)
return crtc_cursor_loc;
else
return 0;
}
else
return crtc_port;
}
void video_crtc_write(int port, uint8_t value)
{
if(port & 1)
{
if(crtc_port == 0x0E)
crtc_cursor_loc = (crtc_cursor_loc & 0xFF) | (value << 8);
if(crtc_port == 0x0F)
crtc_cursor_loc = (crtc_cursor_loc & 0xFF00) | (value);
else
debug(debug_video, "CRTC port write [%02x] <- %02x\n", crtc_port, value);
}
else
crtc_port = value;
}
int video_get_col(void)
{
return vid_posx[vid_page];
}