From f92e6b4b55c3f6e5a327b836375df20fedd45fa4 Mon Sep 17 00:00:00 2001 From: chschnell Date: Sat, 21 Sep 2024 19:08:38 -0600 Subject: [PATCH] Add VGA graphical text mode --- Makefile | 2 +- debug.html | 1 + src/browser/main.js | 5 +- src/browser/screen.js | 18 +- src/browser/starter.js | 4 +- src/cpu.js | 2 +- src/vga.js | 110 +++++++- src/vga_text.js | 628 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 747 insertions(+), 23 deletions(-) create mode 100644 src/vga_text.js diff --git a/Makefile b/Makefile index 892b1fe07..bba402d2a 100644 --- a/Makefile +++ b/Makefile @@ -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 \ diff --git a/debug.html b/debug.html index a4e2d20c1..57594cb86 100644 --- a/debug.html +++ b/debug.html @@ -20,6 +20,7 @@ + diff --git a/src/browser/main.js b/src/browser/main.js index d48b9056b..bdabbb225 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -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, diff --git a/src/browser/screen.js b/src/browser/screen.js index df8945139..176ae429e 100644 --- a/src/browser/screen.js +++ b/src/browser/screen.js @@ -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(); }; diff --git a/src/browser/starter.js b/src/browser/starter.js index 154ae3e4f..5813c1fa9 100644 --- a/src/browser/starter.js +++ b/src/browser/starter.js @@ -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) { diff --git a/src/cpu.js b/src/cpu.js index c81759fd4..c1b6210ed 100644 --- a/src/cpu.js +++ b/src/cpu.js @@ -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); diff --git a/src/vga.js b/src/vga.js index 5b1e63656..2f34ccb73 100644 --- a/src/vga.js +++ b/src/vga.js @@ -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; } diff --git a/src/vga_text.js b/src/vga_text.js new file mode 100644 index 000000000..5b5949b03 --- /dev/null +++ b/src/vga_text.js @@ -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} + */ + 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} + */ + 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; +};