mirror of
https://github.com/copy/v86.git
synced 2025-12-31 04:23:15 +00:00
Add VGA graphical text mode
This commit is contained in:
parent
c1ef714454
commit
f92e6b4b55
8 changed files with 747 additions and 23 deletions
2
Makefile
2
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 \
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
110
src/vga.js
110
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
628
src/vga_text.js
Normal file
628
src/vga_text.js
Normal 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;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue