Add VGA graphical text mode

This commit is contained in:
chschnell 2024-09-21 19:08:38 -06:00 committed by Fabian
parent c1ef714454
commit f92e6b4b55
8 changed files with 747 additions and 23 deletions

View file

@ -79,7 +79,7 @@ CARGO_FLAGS_SAFE=\
CARGO_FLAGS=$(CARGO_FLAGS_SAFE) -C target-feature=+bulk-memory -C target-feature=+multivalue -C target-feature=+simd128
CORE_FILES=const.js config.js io.js main.js lib.js buffer.js ide.js pci.js floppy.js \
memory.js dma.js pit.js vga.js ps2.js rtc.js uart.js \
memory.js dma.js pit.js vga.js vga_text.js ps2.js rtc.js uart.js \
acpi.js apic.js ioapic.js \
state.js ne2k.js sb16.js virtio.js virtio_console.js virtio_net.js \
bus.js log.js cpu.js debug.js \

View file

@ -20,6 +20,7 @@
<script src="src/dma.js"></script>
<script src="src/pit.js"></script>
<script src="src/vga.js"></script>
<script src="src/vga_text.js"></script>
<script src="src/ps2.js"></script>
<script src="src/rtc.js"></script>
<script src="src/uart.js"></script>

View file

@ -1694,7 +1694,10 @@
}
const emulator = new V86({
screen_container: $("screen_container"),
screen: {
container: $("screen_container"),
use_graphical_text: false,
},
net_device: {
type: settings.net_device_type || "ne2k",
relay_url: settings.relay_url,

View file

@ -37,7 +37,7 @@ function ScreenAdapter(options, screen_fill_buffer)
changed_rows,
// are we in graphical mode now?
is_graphical = false,
is_graphical = !!options.use_graphical_text,
// Index 0: ASCII code
// Index 1: Blinking
@ -134,9 +134,19 @@ function ScreenAdapter(options, screen_fill_buffer)
this.init = function()
{
// not necessary, because this gets initialized by the bios early,
// but nicer to look at
this.set_size_text(80, 25);
// initialize with mode and size presets as expected by the bios
// to avoid flickering during early startup
this.set_mode(is_graphical);
if(is_graphical)
{
// assume 80x25 with 9x16 font
this.set_size_graphical(720, 400, 720, 400);
}
else
{
this.set_size_text(80, 25);
}
this.timer();
};

View file

@ -305,6 +305,7 @@ V86.prototype.continue_init = async function(emulator, options)
settings.cpuid_level = options.cpuid_level;
settings.virtio_console = options.virtio_console;
settings.virtio_net = options.virtio_net;
settings.screen_options = options.screen_options;
const relay_url = options.network_relay_url || options.net_device && options.net_device.relay_url;
if(relay_url)
@ -345,13 +346,14 @@ V86.prototype.continue_init = async function(emulator, options)
if(screen_options.container)
{
this.screen_adapter = new ScreenAdapter(screen_options, () => this.v86.cpu.devices.vga.screen_fill_buffer());
this.screen_adapter = new ScreenAdapter(screen_options, () => this.v86.cpu.devices.vga && this.v86.cpu.devices.vga.screen_fill_buffer());
}
else
{
this.screen_adapter = new DummyScreenAdapter();
}
settings.screen = this.screen_adapter;
settings.screen_options = screen_options;
if(options.serial_container)
{

View file

@ -940,7 +940,7 @@ CPU.prototype.init = function(settings, device_bus)
this.devices.dma = new DMA(this);
this.devices.vga = new VGAScreen(this, device_bus, settings.screen, settings.vga_memory_size || 8 * 1024 * 1024);
this.devices.vga = new VGAScreen(this, device_bus, settings.screen, settings.vga_memory_size || 8 * 1024 * 1024, settings.screen_options || {});
this.devices.ps2 = new PS2(this, device_bus);

View file

@ -50,8 +50,9 @@ const VGA_HOST_MEMORY_SPACE_SIZE = Uint32Array.from([
* @param {BusConnector} bus
* @param {ScreenAdapter|DummyScreenAdapter} screen
* @param {number} vga_memory_size
* @param {Object} options
*/
function VGAScreen(cpu, bus, screen, vga_memory_size)
function VGAScreen(cpu, bus, screen, vga_memory_size, options)
{
this.cpu = cpu;
@ -166,7 +167,6 @@ function VGAScreen(cpu, bus, screen, vga_memory_size)
/** @type {boolean} */
this.graphical_mode = false;
this.screen.set_mode(this.graphical_mode);
/*
* VGA palette containing 256 colors for video mode 13, svga 8bpp, etc.
@ -377,9 +377,41 @@ function VGAScreen(cpu, bus, screen, vga_memory_size)
(addr, value) => this.vga_memory_write(addr, value),
);
if(options.use_graphical_text)
{
this.graphical_text = new GraphicalText(this);
}
cpu.devices.pci.register_device(this);
}
VGAScreen.prototype.grab_text_content = function(keep_whitespace)
{
var addr = this.start_address << 1;
const split_screen_row = this.scan_line_to_screen_row(this.line_compare);
const row_offset = Math.max(0, (this.offset_register * 2 - this.max_cols) * 2);
const text_rows = [];
for(var row = 0; row < this.max_rows; row++)
{
if(row === split_screen_row)
{
addr = 0;
}
let line = "";
for(var col = 0; col < this.max_cols; col++, addr += 2)
{
line += String.fromCodePoint(this.vga_memory[addr]);
}
text_rows.push(keep_whitespace ? line : line.trimEnd());
addr += row_offset;
}
return text_rows;
};
VGAScreen.prototype.get_state = function()
{
var state = [];
@ -519,7 +551,7 @@ VGAScreen.prototype.set_state = function(state)
this.dac_mask = state[62] === undefined ? 0xFF : state[62];
this.character_map_select = state[63] === undefined ? 0 : state[63];
this.screen.set_mode(this.graphical_mode);
this.screen.set_mode(this.graphical_mode || !!this.graphical_text);
if(this.graphical_mode)
{
@ -823,6 +855,11 @@ VGAScreen.prototype.apply_bitmask = function(data_dword, bitmask_dword)
VGAScreen.prototype.text_mode_redraw = function()
{
if(this.graphical_text)
{
return;
}
const split_screen_row = this.scan_line_to_screen_row(this.line_compare);
const row_offset = Math.max(0, (this.offset_register * 2 - this.max_cols) * 2);
const blink_flag = this.attribute_mode & 1 << 3;
@ -898,15 +935,23 @@ VGAScreen.prototype.vga_memory_write_text_mode = function(addr, value)
chr = value;
color = this.vga_memory[addr | 1];
}
const blink_flag = this.attribute_mode & 1 << 3;
const blinking = blink_flag && (color & 1 << 7);
const bg_color_mask = blink_flag ? 7 : 0xF;
this.bus.send("screen-put-char", [row, col, chr]);
this.screen.put_char(row, col, chr, blinking,
this.vga256_palette[this.dac_mask & this.dac_map[color >> 4 & bg_color_mask]],
this.vga256_palette[this.dac_mask & this.dac_map[color & 0xF]]);
if(this.graphical_text)
{
this.graphical_text.invalidate_row(row);
}
else
{
this.screen.put_char(row, col, chr, blinking,
this.vga256_palette[this.dac_mask & this.dac_map[color >> 4 & bg_color_mask]],
this.vga256_palette[this.dac_mask & this.dac_map[color & 0xF]]);
}
};
VGAScreen.prototype.update_cursor = function()
@ -927,9 +972,16 @@ VGAScreen.prototype.update_cursor = function()
}
dbg_assert(row >= 0 && col >= 0);
// NOTE: is allowed to be out of bounds
this.screen.update_cursor(row, col);
if(this.graphical_text)
{
this.graphical_text.set_cursor_pos(row, col);
}
else
{
this.screen.update_cursor(row, col);
}
};
VGAScreen.prototype.complete_redraw = function()
@ -1122,8 +1174,16 @@ VGAScreen.prototype.set_size_text = function(cols_count, rows_count)
this.max_cols = cols_count;
this.max_rows = rows_count;
this.screen.set_size_text(cols_count, rows_count);
this.bus.send("screen-set-size", [cols_count, rows_count, 0]);
if(this.graphical_text)
{
this.graphical_text.set_size(rows_count, cols_count);
}
else
{
this.screen.set_size_text(cols_count, rows_count);
}
};
VGAScreen.prototype.set_size_graphical = function(width, height, virtual_width, virtual_height, bpp)
@ -1341,7 +1401,15 @@ VGAScreen.prototype.update_cursor_scanline = function()
const start = Math.min(max, this.cursor_scanline_start & 0x1F);
const end = Math.min(max, this.cursor_scanline_end & 0x1F);
const visible = !disabled && start < end;
this.screen.update_cursor_scanline(start, end, visible);
if(this.graphical_text)
{
this.graphical_text.set_cursor_attr(start, end, visible);
}
else
{
this.screen.update_cursor_scanline(start, end, visible);
}
};
/**
@ -1388,11 +1456,11 @@ VGAScreen.prototype.port3C0_write = function(value)
var previous_mode = this.attribute_mode;
this.attribute_mode = value;
var is_graphical = (value & 0x1) > 0;
const is_graphical = (value & 0x1) !== 0;
if(!this.svga_enabled && this.graphical_mode !== is_graphical)
{
this.graphical_mode = is_graphical;
this.screen.set_mode(this.graphical_mode);
this.screen.set_mode(this.graphical_mode || !!this.graphical_text);
}
if((previous_mode ^ value) & 0x40)
@ -1528,6 +1596,7 @@ VGAScreen.prototype.port3C5_write = function(value)
if(this.graphical_text && previous_plane_write_bm !== 0xf && (previous_plane_write_bm & 0x4) && !(this.plane_write_bm & 0x4))
{
// End of font plane 2 write access (initial value of plane_write_bm assumed to be 0xf)
this.graphical_text.invalidate_font_shape();
}
break;
case 0x03:
@ -1536,6 +1605,7 @@ VGAScreen.prototype.port3C5_write = function(value)
this.character_map_select = value;
if(this.graphical_text && previous_character_map_select !== this.character_map_select)
{
this.graphical_text.set_character_map(this.character_map_select);
}
break;
case 0x04:
@ -2425,9 +2495,19 @@ VGAScreen.prototype.screen_fill_buffer = function()
if(!this.graphical_mode)
{
// text mode
// Update retrace behaviour anyway - programs waiting for signal before
// changing to graphical mode
this.update_vertical_retrace();
if(this.graphical_text)
{
const image_data = this.graphical_text.render();
this.screen.update_buffer([{
image_data: image_data,
screen_x: 0,
screen_y: 0,
buffer_x: 0,
buffer_y: 0,
buffer_width: image_data.width,
buffer_height: image_data.height
}]);
}
return;
}

628
src/vga_text.js Normal file
View file

@ -0,0 +1,628 @@
/*
vga_text.js
Renders text to image buffer using VGA fonts and attributes.
*/
"use strict";
/**
* @constructor
* @param {VGAScreen} vga
*/
function GraphicalText(vga)
{
this.vga = vga;
/**
* Number of text columns
* @type {number}
*/
this.txt_width = 80;
/**
* Number of text rows
* @type {number}
*/
this.txt_height = 25;
/**
* If true then at least one row in txt_row_dirty is marked as modified
* @type{number}
*/
this.txt_dirty = 0;
/**
* One bool per row, row was modified if its entry is != 0
*/
this.txt_row_dirty = new Uint8Array(this.txt_height);
/**
* Font bitmaps in VGA memory were changed if true
* @type{boolean}
*/
this.font_data_dirty = false;
/**
* Font width in pixel (8, 9 or 16)
* @type {number}
*/
this.font_width = 9;
/**
* Font height in pixel (0...32)
* @type {number}
*/
this.font_height = 16;
/**
* Duplicate 8th to 9th column in horizontal line drawing characters if true (Line Graphics Enable)
* @type{boolean}
*/
this.font_lge = false;
/**
* Flat bitmap of 8 fonts, array of size: 8 * 256 * font_width * font_height
* @type{Uint8ClampedArray<number>}
*/
this.font_bitmap = new Uint8ClampedArray(8 * 256 * this.font_width * this.font_height);
/**
* True: blink when msb (0x80) of text attribute is set (8 background colors)
* False: msb selects background intensity (16 background colors)
* @type{boolean}
*/
this.font_blink_enabled = false;
/**
* Active index (0...7) of font A
* @type {number}
*/
this.font_index_A = 0;
/**
* Active index (0...7) of font B (TODO)
* @type {number}
*/
this.font_index_B = 0;
/**
* If true then cursor_enabled_latch, cursor_top_latch and cursor_bottom_latch were overwritten since last call to render().
* @type{boolean}
*/
this.cursor_attr_dirty = false;
/**
* Latest value for cursor_enabled if cursor_attr_dirty is true
* @type{boolean}
*/
this.cursor_enabled_latch = false;
/**
* Latest value for cursor_top_latch if cursor_attr_dirty is true
* @type {number}
*/
this.cursor_top_latch = 0;
/**
* Latest value for cursor_bottom_latch if cursor_attr_dirty is true
* @type {number}
*/
this.cursor_bottom_latch = 0;
/**
* If true then cursor_row_latch and cursor_col_latch were overwritten since last call to render().
* @type{boolean}
*/
this.cursor_pos_dirty = false;
/**
* Latest value for cursor_row if cursor_pos_dirty is true
* @type {number}
*/
this.cursor_row_latch = 0;
/**
* Latest value for cursor_col if cursor_pos_dirty is true
* @type {number}
*/
this.cursor_col_latch = 0;
/**
* Emulate cursor if true, else disable cursor
* @type{boolean}
*/
this.cursor_enabled = false;
/**
* Cursor position's row (0...txt_height-1)
* @type {number}
*/
this.cursor_row = 0;
/**
* Cursor position's column (0...txt_width-1)
* @type {number}
*/
this.cursor_col = 0;
/**
* Cursor box's top scanline (0...font_height)
* @type {number}
*/
this.cursor_top = 0;
/**
* Cursor box's bottom scanline (0...font_height, inclusive)
* @type {number}
*/
this.cursor_bottom = 0;
/**
* Tracked value of register vga.attribute_mode
* @type {number}
*/
this.vga_attribute_mode = 0;
/**
* Tracked value of register vga.clocking_mode
* @type {number}
*/
this.vga_clocking_mode = 0;
/**
* Tracked value of register vga.max_scan_line
* @type {number}
*/
this.vga_max_scan_line = 0;
/**
* Width of graphics canvas in pixel (txt_width * font_width)
* @type {number}
*/
this.gfx_width = this.txt_width * this.font_width;
/**
* Height of graphics canvas in pixel (txt_height * font_height)
* @type {number}
*/
this.gfx_height = this.txt_height * this.font_height;
/**
* Local screen bitmap buffer, array of size: gfx_width * gfx_height * 4
* @type{Uint8ClampedArray<number>}
*/
this.gfx_data = new Uint8ClampedArray(this.gfx_width * this.gfx_height * 4);
/**
* Image container of local screen bitmap buffer gfx_data
* @type{ImageData}
*/
this.image_data = new ImageData(this.gfx_data, this.gfx_width, this.gfx_height);
/**
* Show cursor and blinking text now if true (controlled by framerate counter)
* @type{boolean}
*/
this.blink_visible = false;
/**
* Frame counter to control blink rate of type Uint32
* @type {number}
*/
this.frame_count = 0;
}
GraphicalText.prototype.rebuild_font_bitmap = function(width_9px, width_double)
{
const font_height = this.font_height;
const font_lge = this.font_lge;
const src_bitmap = this.vga.plane2;
const dst_bitmap = new Uint8ClampedArray(8 * 256 * this.font_width * font_height);
const vga_inc_chr = 32 - font_height;
let i_dst = 0;
const copy_bit = width_double ?
function(value)
{
dst_bitmap[i_dst++] = value;
dst_bitmap[i_dst++] = value;
} :
function(value)
{
dst_bitmap[i_dst++] = value;
};
let i_src = 0;
for(let i_font = 0; i_font < 8; ++i_font)
{
for(let i_chr = 0; i_chr < 256; ++i_chr, i_src += vga_inc_chr)
{
for(let i_line = 0; i_line < font_height; ++i_line)
{
const line_bits = src_bitmap[i_src++];
for(let i_bit = 0x80; i_bit > 0; i_bit >>= 1)
{
copy_bit(line_bits & i_bit ? 1 : 0);
}
if(width_9px)
{
copy_bit(font_lge && i_chr >= 0xC0 && i_chr <= 0xDF && line_bits & 1 ? 1 : 0);
}
}
}
}
return dst_bitmap;
};
GraphicalText.prototype.resize_canvas = function()
{
this.txt_dirty = 1;
this.txt_row_dirty.fill(1);
};
GraphicalText.prototype.rebuild_image_data = function()
{
const gfx_size = this.gfx_width * this.gfx_height * 4;
const gfx_data = new Uint8ClampedArray(gfx_size);
for(let i = 3; i < gfx_size; i += 4)
{
gfx_data[i] = 0xff;
}
this.gfx_data = gfx_data;
this.image_data = new ImageData(this.gfx_data, this.gfx_width, this.gfx_height);
this.resize_canvas();
};
GraphicalText.prototype.mark_blinking_rows_dirty = function()
{
const vga_memory = this.vga.vga_memory;
const txt_row_dirty = this.txt_row_dirty;
const txt_width = this.txt_width;
const txt_height = this.txt_height;
const txt_row_size = txt_width * 2;
const txt_row_step = Math.max(0, (this.vga.offset_register * 2 - txt_width) * 2);
const split_screen_row = this.vga.scan_line_to_screen_row(this.vga.line_compare);
let row, col, txt_i = this.vga.start_address << 1;
for(row = 0; row < txt_height; ++row, txt_i += txt_row_step)
{
if(row === split_screen_row)
{
txt_i = 0;
}
if(txt_row_dirty[row])
{
txt_i += txt_row_size;
continue;
}
for(col = 0; col < txt_width; ++col, txt_i += 2)
{
if(vga_memory[txt_i | 1] & 0x80)
{
txt_row_dirty[row] = this.txt_dirty = 1;
txt_i += txt_row_size - col * 2;
break;
}
}
}
};
GraphicalText.prototype.render_dirty_rows = function()
{
const vga = this.vga;
const vga_memory = vga.vga_memory;
const txt_width = this.txt_width;
const txt_height = this.txt_height;
const txt_row_dirty = this.txt_row_dirty;
const gfx_data = this.gfx_data;
const font_bitmap = this.font_bitmap;
const font_size = this.font_width * this.font_height;
const font_A_offset = this.font_index_A * 256;
const font_B_offset = this.font_index_B * 256;
const font_AB_enabled = font_A_offset !== font_B_offset;
const font_blink_enabled = this.font_blink_enabled;
//const blink_visible = this.blink_visible;
const blink_visible = true;
const cursor_visible = this.cursor_enabled && blink_visible;
const cursor_top = this.cursor_top;
const cursor_height = this.cursor_bottom - cursor_top + 1;
const split_screen_row = vga.scan_line_to_screen_row(vga.line_compare);
const bg_color_mask = font_blink_enabled ? 0x7 : 0xF;
const palette = new Int32Array(16);
for(let i = 0; i < 16; ++i)
{
palette[i] = vga.vga256_palette[vga.dac_mask & vga.dac_map[i]];
}
const txt_row_size = txt_width * 2;
const txt_row_step = Math.max(0, (vga.offset_register * 2 - txt_width) * 2);
const gfx_col_size = this.font_width * 4; // column size in gfx_data (tuple of 4 RGBA items)
const gfx_line_size = this.gfx_width * 4; // line size in gfx_data
const gfx_row_size = gfx_line_size * this.font_height; // row size in gfx_data
const gfx_col_step = (this.font_width - this.font_height * this.gfx_width) * 4; // move from end of current column to start of next in gfx_data
const gfx_line_step = (this.gfx_width - this.font_width) * 4; // move forward to start of column's next line in gfx_data
// int, current cursor linear position in canvas coordinates (top left of row/col)
const cursor_gfx_i = (this.cursor_row * this.gfx_width * this.font_height + this.cursor_col * this.font_width) * 4;
let txt_i, chr, chr_attr, chr_bg_rgba, chr_fg_rgba, chr_blinking, chr_font_ofs;
let fg, bg, fg_r=0, fg_g=0, fg_b=0, bg_r=0, bg_g=0, bg_b=0;
let gfx_i, gfx_end_y, gfx_end_x, glyph_i;
let draw_cursor, gfx_ic;
let row, col;
txt_i = vga.start_address << 1;
for(row = 0; row < txt_height; ++row, txt_i += txt_row_step)
{
if(row === split_screen_row)
{
txt_i = 0;
}
if(! txt_row_dirty[row])
{
txt_i += txt_row_size;
continue;
}
gfx_i = row * gfx_row_size;
for(col = 0; col < txt_width; ++col, txt_i += 2, gfx_i += gfx_col_step)
{
chr = vga_memory[txt_i];
chr_attr = vga_memory[txt_i | 1];
chr_blinking = font_blink_enabled && chr_attr & 0x80;
chr_font_ofs = font_AB_enabled ? (chr_attr & 0x8 ? font_A_offset : font_B_offset) : font_A_offset;
chr_bg_rgba = palette[chr_attr >> 4 & bg_color_mask];
chr_fg_rgba = palette[chr_attr & 0xF];
if(bg !== chr_bg_rgba)
{
bg = chr_bg_rgba;
bg_r = bg >> 16;
bg_g = (bg >> 8) & 0xff;
bg_b = bg & 0xff;
}
if(chr_blinking && ! blink_visible)
{
if(fg !== bg) {
fg = bg;
fg_r = bg_r;
fg_g = bg_g;
fg_b = bg_b;
}
}
else if(fg !== chr_fg_rgba)
{
fg = chr_fg_rgba;
fg_r = fg >> 16;
fg_g = (fg >> 8) & 0xff;
fg_b = fg & 0xff;
}
draw_cursor = cursor_visible && cursor_gfx_i === gfx_i;
glyph_i = (chr_font_ofs + chr) * font_size;
gfx_end_y = gfx_i + gfx_row_size;
for(; gfx_i < gfx_end_y; gfx_i += gfx_line_step)
{
gfx_end_x = gfx_i + gfx_col_size;
for(; gfx_i < gfx_end_x; gfx_i += 4)
{
if(font_bitmap[glyph_i++])
{
gfx_data[gfx_i] = fg_r;
gfx_data[gfx_i+1] = fg_g;
gfx_data[gfx_i+2] = fg_b;
}
else
{
gfx_data[gfx_i] = bg_r;
gfx_data[gfx_i+1] = bg_g;
gfx_data[gfx_i+2] = bg_b;
}
}
}
if(draw_cursor)
{
gfx_ic = cursor_gfx_i + cursor_top * gfx_line_size;
gfx_end_y = gfx_ic + cursor_height * gfx_line_size;
for(; gfx_ic < gfx_end_y; gfx_ic += gfx_line_step)
{
gfx_end_x = gfx_ic + gfx_col_size;
for(; gfx_ic < gfx_end_x; gfx_ic += 4)
{
gfx_data[gfx_ic] = fg_r;
gfx_data[gfx_ic+1] = fg_g;
gfx_data[gfx_ic+2] = fg_b;
}
}
}
}
}
};
//
// Public methods
//
GraphicalText.prototype.mark_dirty = function()
{
this.txt_row_dirty.fill(1);
this.txt_dirty = 1;
};
GraphicalText.prototype.invalidate_row = function(row)
{
if(row >= 0 && row < this.txt_height)
{
this.txt_row_dirty[row] = this.txt_dirty = 1;
}
};
GraphicalText.prototype.invalidate_font_shape = function()
{
this.font_data_dirty = true;
};
GraphicalText.prototype.set_size = function(rows, cols)
{
if(rows > 0 && rows < 256 && cols > 0 && cols < 256)
{
this.txt_width = cols;
this.txt_height = rows;
this.gfx_width = this.txt_width * this.font_width;
this.gfx_height = this.txt_height * this.font_height;
this.txt_row_dirty = new Uint8Array(this.txt_height);
this.vga.screen.set_size_graphical(this.gfx_width, this.gfx_height, this.gfx_width, this.gfx_height);
this.mark_dirty();
this.rebuild_image_data();
}
};
GraphicalText.prototype.set_character_map = function(char_map_select)
{
// bits 2, 3 and 5 (LSB to MSB): VGA font page index of font A
// bits 0, 1 and 4: VGA font page index of font B
// linear_index_map[] maps VGA's non-liner font page index to linear index
const linear_index_map = [0, 2, 4, 6, 1, 3, 5, 7];
const vga_index_A = ((char_map_select & 0b1100) >> 2) | ((char_map_select & 0b100000) >> 3);
const vga_index_B = (char_map_select & 0b11) | ((char_map_select & 0b10000) >> 2);
const font_index_A = linear_index_map[vga_index_A];
const font_index_B = linear_index_map[vga_index_B];
if(this.font_index_A !== font_index_A || this.font_index_B !== font_index_B)
{
this.font_index_A = font_index_A;
this.font_index_B = font_index_B;
this.mark_dirty();
}
};
GraphicalText.prototype.set_cursor_pos = function(row, col)
{
this.cursor_pos_dirty = true;
this.cursor_row_latch = row;
this.cursor_col_latch = col;
};
GraphicalText.prototype.set_cursor_attr = function(start, end, visible)
{
this.cursor_attr_dirty = true;
this.cursor_enabled_latch = !! visible;
this.cursor_top_latch = start;
this.cursor_bottom_latch = end;
};
GraphicalText.prototype.render = function()
{
// increment Uint32 frame counter
this.frame_count = (this.frame_count + 1) >>> 0;
// apply changes to font_width, font_height, font_lge, font_bitmap and font_blink_enabled
const curr_clocking_mode = this.vga.clocking_mode & 0b00001001;
const curr_attribute_mode = this.vga.attribute_mode & 0b00001100;
const curr_max_scan_line = this.vga.max_scan_line & 0b10011111;
if(this.font_data_dirty ||
this.vga_clocking_mode !== curr_clocking_mode ||
this.vga_attribute_mode !== curr_attribute_mode ||
this.vga_max_scan_line !== curr_max_scan_line)
{
const width_9px = ! (curr_clocking_mode & 0x01);
const width_double = !! (curr_clocking_mode & 0x08);
const curr_font_width = (width_9px ? 9 : 8) * (width_double ? 2 : 1);
const curr_font_blink_enabled = !! (curr_attribute_mode & 0b00001000);
const curr_font_lge = !! (curr_attribute_mode & 0b00000100);
const curr_font_height = (curr_max_scan_line & 0b00011111) + 1;
const font_data_changed = this.font_data_dirty || this.font_lge !== curr_font_lge;
const font_size_changed = this.font_width !== curr_font_width || this.font_height !== curr_font_height;
this.font_data_dirty = false;
this.font_width = curr_font_width;
this.font_height = curr_font_height;
this.font_blink_enabled = curr_font_blink_enabled;
this.font_lge = curr_font_lge;
this.vga_clocking_mode = curr_clocking_mode;
this.vga_attribute_mode = curr_attribute_mode;
this.vga_max_scan_line = curr_max_scan_line;
if(font_data_changed || font_size_changed)
{
if(font_size_changed)
{
this.gfx_width = this.txt_width * this.font_width;
this.gfx_height = this.txt_height * this.font_height;
this.rebuild_image_data();
}
this.font_bitmap = this.rebuild_font_bitmap(width_9px, width_double);
}
this.mark_dirty();
}
// apply changes to cursor position
if(this.cursor_pos_dirty)
{
this.cursor_pos_dirty = false;
this.cursor_row_latch = Math.min(this.cursor_row_latch, this.txt_height-1);
this.cursor_col_latch = Math.min(this.cursor_col_latch, this.txt_width-1);
if(this.cursor_row !== this.cursor_row_latch || this.cursor_col !== this.cursor_col_latch)
{
this.txt_row_dirty[this.cursor_row] = this.txt_row_dirty[this.cursor_row_latch] = this.txt_dirty = 1;
this.cursor_row = this.cursor_row_latch;
this.cursor_col = this.cursor_col_latch;
}
}
// apply changes to cursor_enabled, cursor_top and cursor_bottom
if(this.cursor_attr_dirty)
{
this.cursor_attr_dirty = false;
if(this.cursor_enabled !== this.cursor_enabled_latch ||
this.cursor_top !== this.cursor_top_latch ||
this.cursor_bottom !== this.cursor_bottom_latch)
{
this.cursor_enabled = this.cursor_enabled_latch;
this.cursor_top = this.cursor_top_latch;
this.cursor_bottom = this.cursor_bottom_latch;
this.txt_row_dirty[this.cursor_row] = this.txt_dirty = 1;
}
}
// toggle cursor and blinking character visibility at a frequency of ~3.75hz (every 16th frame at 60fps)
// TODO: make framerate independant
//if(this.frame_count % 16 === 0)
//{
// this.blink_visible = ! this.blink_visible;
// if(this.font_blink_enabled)
// {
// this.mark_blinking_rows_dirty();
// }
// if(this.cursor_enabled)
// {
// this.txt_row_dirty[this.cursor_row] = this.txt_dirty = 1;
// }
//}
// render changed rows
if(this.txt_dirty)
{
this.render_dirty_rows();
this.txt_dirty = 0;
this.txt_row_dirty.fill(0);
}
return this.image_data;
};