v86 / src /browser /screen.js
peterpeter8585's picture
Upload 553 files
8df6da4 verified
import { dbg_assert } from "../log.js";
import { get_charmap } from "../lib.js";
// Draws entire buffer and visualizes the layers that would be drawn
export const DEBUG_SCREEN_LAYERS = DEBUG && false;
/**
* Adapter to use visual screen in browsers (in contrast to node)
* @constructor
* @param {Object} options
* @param {function()} screen_fill_buffer
*/
export function ScreenAdapter(options, screen_fill_buffer)
{
const screen_container = options.container;
this.screen_fill_buffer = screen_fill_buffer;
console.assert(screen_container, "options.container must be provided");
const MODE_TEXT = 0;
const MODE_GRAPHICAL = 1;
const MODE_GRAPHICAL_TEXT = 2;
const CHARACTER_INDEX = 0;
const FLAGS_INDEX = 1;
const BG_COLOR_INDEX = 2;
const FG_COLOR_INDEX = 3;
const TEXT_BUF_COMPONENT_SIZE = 4;
const FLAG_BLINKING = 0x01;
const FLAG_FONT_PAGE_B = 0x02;
this.FLAG_BLINKING = FLAG_BLINKING;
this.FLAG_FONT_PAGE_B = FLAG_FONT_PAGE_B;
var
graphic_screen = screen_container.getElementsByTagName("canvas")[0],
graphic_context = graphic_screen.getContext("2d", { alpha: false }),
text_screen = screen_container.getElementsByTagName("div")[0],
cursor_element = document.createElement("div");
var
/** @type {number} */
cursor_row,
/** @type {number} */
cursor_col,
/** @type {number} */
scale_x = options.scale !== undefined ? options.scale : 1,
/** @type {number} */
scale_y = options.scale !== undefined ? options.scale : 1,
base_scale = 1,
changed_rows,
// current display mode: MODE_GRAPHICAL or either MODE_TEXT/MODE_GRAPHICAL_TEXT
mode,
// Index 0: ASCII code
// Index 1: Flags bitset (see FLAG_...)
// Index 2: Background color
// Index 3: Foreground color
text_mode_data,
// number of columns
text_mode_width,
// number of rows
text_mode_height,
// graphical text mode's offscreen canvas contexts
offscreen_context,
offscreen_extra_context,
// fonts
font_context,
font_image_data,
font_is_visible = new Int8Array(8 * 256),
font_height,
font_width,
font_width_9px,
font_width_dbl,
font_copy_8th_col,
font_page_a = 0,
font_page_b = 0,
// blink state
blink_visible,
tm_last_update = 0,
// cursor attributes
cursor_start,
cursor_end,
cursor_enabled,
// 8-bit-text to Unicode character map
charmap = get_charmap(options.encoding),
// render loop state
timer_id = 0,
paused = false;
// 0x12345 -> "#012345"
function number_as_color(n)
{
n = n.toString(16);
return "#" + "0".repeat(6 - n.length) + n;
}
function render_font_bitmap(vga_bitmap)
{
// - Browsers impose limts on the X- and Y-axes of bitmaps (typically around 8 to 32k).
// Draw the 8 VGA font pages of 256 glyphs in 8 rows of 256 columns, this results
// in 2048, 2304 or 4096px on the X-axis (for 8, 9 or 16px VGA font width, resp.).
// This 2d layout is also convenient for glyph lookup when rendering text.
// - Font bitmap pixels are black and either fully opaque (alpha 255) or fully transparent (0).
const bitmap_width = font_width * 256;
const bitmap_height = font_height * 8;
let font_canvas = font_context ? font_context.canvas : null;
if(!font_canvas || font_canvas.width !== bitmap_width || font_canvas.height !== bitmap_height)
{
if(!font_canvas)
{
font_canvas = new OffscreenCanvas(bitmap_width, bitmap_height);
font_context = font_canvas.getContext("2d");
}
else
{
font_canvas.width = bitmap_width;
font_canvas.height = bitmap_height;
}
font_image_data = font_context.createImageData(bitmap_width, bitmap_height);
}
const font_bitmap = font_image_data.data;
let i_dst = 0, is_visible;
const put_bit = font_width_dbl ?
function(value)
{
is_visible = is_visible || value;
font_bitmap[i_dst + 3] = value;
font_bitmap[i_dst + 7] = value;
i_dst += 8;
} :
function(value)
{
is_visible = is_visible || value;
font_bitmap[i_dst + 3] = value;
i_dst += 4;
};
// move i_vga from end of glyph to start of next glyph
const vga_inc_chr = 32 - font_height;
// move i_dst from end of font page (bitmap row) to start of next font page
const dst_inc_row = bitmap_width * (font_height - 1) * 4;
// move i_dst from end of glyph (bitmap column) to start of next glyph
const dst_inc_col = (font_width - bitmap_width * font_height) * 4;
// move i_dst from end of a glyph's scanline to start of its next scanline
const dst_inc_line = font_width * 255 * 4;
for(let i_chr_all = 0, i_vga = 0; i_chr_all < 2048; ++i_chr_all, i_vga += vga_inc_chr, i_dst += dst_inc_col)
{
const i_chr = i_chr_all % 256;
if(i_chr_all && !i_chr)
{
i_dst += dst_inc_row;
}
is_visible = false;
for(let i_line = 0; i_line < font_height; ++i_line, ++i_vga, i_dst += dst_inc_line)
{
const line_bits = vga_bitmap[i_vga];
for(let i_bit = 0x80; i_bit > 0; i_bit >>= 1)
{
put_bit(line_bits & i_bit ? 255 : 0);
}
if(font_width_9px)
{
put_bit(font_copy_8th_col && i_chr >= 0xC0 && i_chr <= 0xDF && line_bits & 1 ? 255 : 0);
}
}
font_is_visible[i_chr_all] = is_visible ? 1 : 0;
}
font_context.putImageData(font_image_data, 0, 0);
}
function render_changed_rows()
{
const font_canvas = font_context.canvas;
const offscreen_extra_canvas = offscreen_extra_context.canvas;
const txt_row_size = text_mode_width * TEXT_BUF_COMPONENT_SIZE;
const gfx_width = text_mode_width * font_width;
const row_extra_1_y = 0;
const row_extra_2_y = font_height;
let n_rows_rendered = 0;
for(let row_i = 0, row_y = 0, txt_i = 0; row_i < text_mode_height; ++row_i, row_y += font_height)
{
if(!changed_rows[row_i])
{
txt_i += txt_row_size;
continue;
}
++n_rows_rendered;
// clear extra row 2
offscreen_extra_context.clearRect(0, row_extra_2_y, gfx_width, font_height);
let fg_rgba, fg_x, bg_rgba, bg_x;
for(let col_x = 0; col_x < gfx_width; col_x += font_width, txt_i += TEXT_BUF_COMPONENT_SIZE)
{
const chr = text_mode_data[txt_i + CHARACTER_INDEX];
const chr_flags = text_mode_data[txt_i + FLAGS_INDEX];
const chr_bg_rgba = text_mode_data[txt_i + BG_COLOR_INDEX];
const chr_fg_rgba = text_mode_data[txt_i + FG_COLOR_INDEX];
const chr_font_page = chr_flags & FLAG_FONT_PAGE_B ? font_page_b : font_page_a;
const chr_visible = (!(chr_flags & FLAG_BLINKING) || blink_visible) && font_is_visible[(chr_font_page << 8) + chr];
if(bg_rgba !== chr_bg_rgba)
{
if(bg_rgba !== undefined)
{
// draw opaque block of background color into offscreen_context
offscreen_context.fillStyle = number_as_color(bg_rgba);
offscreen_context.fillRect(bg_x, row_y, col_x - bg_x, font_height);
}
bg_rgba = chr_bg_rgba;
bg_x = col_x;
}
if(fg_rgba !== chr_fg_rgba)
{
if(fg_rgba !== undefined)
{
// draw opaque block of foreground color into extra row 1
offscreen_extra_context.fillStyle = number_as_color(fg_rgba);
offscreen_extra_context.fillRect(fg_x, row_extra_1_y, col_x - fg_x, font_height);
}
fg_rgba = chr_fg_rgba;
fg_x = col_x;
}
if(chr_visible)
{
// copy transparent glyphs into extra row 2
offscreen_extra_context.drawImage(font_canvas,
chr * font_width, chr_font_page * font_height, font_width, font_height,
col_x, row_extra_2_y, font_width, font_height);
}
}
// draw rightmost block of foreground color into extra row 1
offscreen_extra_context.fillStyle = number_as_color(fg_rgba);
offscreen_extra_context.fillRect(fg_x, row_extra_1_y, gfx_width - fg_x, font_height);
// combine extra row 1 (colors) and 2 (glyphs) into extra row 1 (colored glyphs)
offscreen_extra_context.globalCompositeOperation = "destination-in";
offscreen_extra_context.drawImage(offscreen_extra_canvas,
0, row_extra_2_y, gfx_width, font_height,
0, row_extra_1_y, gfx_width, font_height);
offscreen_extra_context.globalCompositeOperation = "source-over";
// draw rightmost block of background color into offscreen_context
offscreen_context.fillStyle = number_as_color(bg_rgba);
offscreen_context.fillRect(bg_x, row_y, gfx_width - bg_x, font_height);
// copy colored glyphs from extra row 1 into offscreen_context (on top of background colors)
offscreen_context.drawImage(offscreen_extra_canvas,
0, row_extra_1_y, gfx_width, font_height,
0, row_y, gfx_width, font_height);
}
if(n_rows_rendered)
{
if(blink_visible && cursor_enabled && changed_rows[cursor_row])
{
const cursor_txt_i = (cursor_row * text_mode_width + cursor_col) * TEXT_BUF_COMPONENT_SIZE;
const cursor_rgba = text_mode_data[cursor_txt_i + FG_COLOR_INDEX];
offscreen_context.fillStyle = number_as_color(cursor_rgba);
offscreen_context.fillRect(
cursor_col * font_width,
cursor_row * font_height + cursor_start,
font_width,
cursor_end - cursor_start + 1);
}
changed_rows.fill(0);
}
return n_rows_rendered;
}
function mark_blinking_rows_dirty()
{
const txt_row_size = text_mode_width * TEXT_BUF_COMPONENT_SIZE;
for(let row_i = 0, txt_i = 0; row_i < text_mode_height; ++row_i)
{
if(changed_rows[row_i])
{
txt_i += txt_row_size;
continue;
}
for(let col_i = 0; col_i < text_mode_width; ++col_i, txt_i += TEXT_BUF_COMPONENT_SIZE)
{
if(text_mode_data[txt_i + FLAGS_INDEX] & FLAG_BLINKING)
{
changed_rows[row_i] = 1;
txt_i += txt_row_size - col_i * TEXT_BUF_COMPONENT_SIZE;
break;
}
}
}
}
this.init = function()
{
// setup text mode cursor DOM element
cursor_element.classList.add("cursor");
cursor_element.style.position = "absolute";
cursor_element.style.backgroundColor = "#ccc";
cursor_element.style.width = "7px";
cursor_element.style.display = "inline-block";
// initialize display mode and size to 80x25 text with 9x16 font
this.set_mode(false);
this.set_size_text(80, 25);
if(mode === MODE_GRAPHICAL_TEXT)
{
this.set_size_graphical(720, 400, 720, 400);
}
// initialize CSS scaling
this.set_scale(scale_x, scale_y);
this.timer();
};
this.make_screenshot = function()
{
const image = new Image();
if(mode === MODE_GRAPHICAL || mode === MODE_GRAPHICAL_TEXT)
{
image.src = graphic_screen.toDataURL("image/png");
}
else
{
// Default 720x400, but can be [8, 16] at 640x400
const char_size = [9, 16];
const canvas = document.createElement("canvas");
canvas.width = text_mode_width * char_size[0];
canvas.height = text_mode_height * char_size[1];
const context = canvas.getContext("2d");
context.imageSmoothingEnabled = false;
context.font = window.getComputedStyle(text_screen).font;
context.textBaseline = "top";
for(let y = 0; y < text_mode_height; y++)
{
for(let x = 0; x < text_mode_width; x++)
{
const index = (y * text_mode_width + x) * TEXT_BUF_COMPONENT_SIZE;
const character = text_mode_data[index + CHARACTER_INDEX];
const bg_color = text_mode_data[index + BG_COLOR_INDEX];
const fg_color = text_mode_data[index + FG_COLOR_INDEX];
context.fillStyle = number_as_color(bg_color);
context.fillRect(x * char_size[0], y * char_size[1], char_size[0], char_size[1]);
context.fillStyle = number_as_color(fg_color);
context.fillText(charmap[character], x * char_size[0], y * char_size[1]);
}
}
if(cursor_element.style.display !== "none" && cursor_row < text_mode_height && cursor_col < text_mode_width)
{
context.fillStyle = cursor_element.style.backgroundColor;
context.fillRect(
cursor_col * char_size[0],
cursor_row * char_size[1] + parseInt(cursor_element.style.marginTop, 10),
parseInt(cursor_element.style.width, 10),
parseInt(cursor_element.style.height, 10)
);
}
image.src = canvas.toDataURL("image/png");
}
return image;
};
this.put_char = function(row, col, chr, flags, bg_color, fg_color)
{
dbg_assert(row >= 0 && row < text_mode_height);
dbg_assert(col >= 0 && col < text_mode_width);
dbg_assert(chr >= 0 && chr < 0x100);
const p = TEXT_BUF_COMPONENT_SIZE * (row * text_mode_width + col);
text_mode_data[p + CHARACTER_INDEX] = chr;
text_mode_data[p + FLAGS_INDEX] = flags;
text_mode_data[p + BG_COLOR_INDEX] = bg_color;
text_mode_data[p + FG_COLOR_INDEX] = fg_color;
changed_rows[row] = 1;
};
this.timer = function()
{
timer_id = requestAnimationFrame(() => this.update_screen());
};
this.update_screen = function()
{
if(!paused)
{
if(mode === MODE_TEXT)
{
this.update_text();
}
else if(mode === MODE_GRAPHICAL)
{
this.update_graphical();
}
else
{
this.update_graphical_text();
}
}
this.timer();
};
this.update_text = function()
{
for(var i = 0; i < text_mode_height; i++)
{
if(changed_rows[i])
{
this.text_update_row(i);
changed_rows[i] = 0;
}
}
};
this.update_graphical = function()
{
this.screen_fill_buffer();
};
this.update_graphical_text = function()
{
if(offscreen_context)
{
// toggle cursor and blinking character visibility at a frequency of ~3.75hz
const tm_now = performance.now();
if(tm_now - tm_last_update > 266)
{
blink_visible = !blink_visible;
if(cursor_enabled)
{
changed_rows[cursor_row] = 1;
}
mark_blinking_rows_dirty();
tm_last_update = tm_now;
}
// copy to DOM canvas only if anything new was rendered
if(render_changed_rows())
{
graphic_context.drawImage(offscreen_context.canvas, 0, 0);
}
}
};
this.destroy = function()
{
if(timer_id)
{
cancelAnimationFrame(timer_id);
timer_id = 0;
}
};
this.pause = function()
{
paused = true;
cursor_element.classList.remove("blinking-cursor");
};
this.continue = function()
{
paused = false;
cursor_element.classList.add("blinking-cursor");
};
this.set_mode = function(graphical)
{
mode = graphical ? MODE_GRAPHICAL : (options.use_graphical_text ? MODE_GRAPHICAL_TEXT : MODE_TEXT);
if(mode === MODE_TEXT)
{
text_screen.style.display = "block";
graphic_screen.style.display = "none";
}
else
{
text_screen.style.display = "none";
graphic_screen.style.display = "block";
if(mode === MODE_GRAPHICAL_TEXT && changed_rows)
{
changed_rows.fill(1);
}
}
};
this.set_font_bitmap = function(height, width_9px, width_dbl, copy_8th_col, vga_bitmap, vga_bitmap_changed)
{
const width = width_dbl ? 16 : (width_9px ? 9 : 8);
if(font_height !== height || font_width !== width || font_width_9px !== width_9px ||
font_width_dbl !== width_dbl || font_copy_8th_col !== copy_8th_col ||
vga_bitmap_changed)
{
const size_changed = font_width !== width || font_height !== height;
font_height = height;
font_width = width;
font_width_9px = width_9px;
font_width_dbl = width_dbl;
font_copy_8th_col = copy_8th_col;
if(mode === MODE_GRAPHICAL_TEXT)
{
render_font_bitmap(vga_bitmap);
changed_rows.fill(1);
if(size_changed)
{
this.set_size_graphical_text();
}
}
}
};
this.set_font_page = function(page_a, page_b)
{
if(font_page_a !== page_a || font_page_b !== page_b)
{
font_page_a = page_a;
font_page_b = page_b;
changed_rows.fill(1);
}
};
this.clear_screen = function()
{
graphic_context.fillStyle = "#000";
graphic_context.fillRect(0, 0, graphic_screen.width, graphic_screen.height);
};
this.set_size_graphical_text = function()
{
if(!font_context)
{
return;
}
const gfx_width = font_width * text_mode_width;
const gfx_height = font_height * text_mode_height;
const offscreen_extra_height = font_height * 2;
if(!offscreen_context || offscreen_context.canvas.width !== gfx_width ||
offscreen_context.canvas.height !== gfx_height ||
offscreen_extra_context.canvas.height !== offscreen_extra_height)
{
// resize offscreen canvases
if(!offscreen_context)
{
const offscreen_canvas = new OffscreenCanvas(gfx_width, gfx_height);
offscreen_context = offscreen_canvas.getContext("2d", { alpha: false });
const offscreen_extra_canvas = new OffscreenCanvas(gfx_width, offscreen_extra_height);
offscreen_extra_context = offscreen_extra_canvas.getContext("2d");
}
else
{
offscreen_context.canvas.width = gfx_width;
offscreen_context.canvas.height = gfx_height;
offscreen_extra_context.canvas.width = gfx_width;
offscreen_extra_context.canvas.height = offscreen_extra_height;
}
// resize DOM canvas graphic_screen
this.set_size_graphical(gfx_width, gfx_height, gfx_width, gfx_height);
changed_rows.fill(1);
}
};
/**
* @param {number} cols
* @param {number} rows
*/
this.set_size_text = function(cols, rows)
{
if(cols === text_mode_width && rows === text_mode_height)
{
return;
}
changed_rows = new Int8Array(rows);
text_mode_data = new Int32Array(cols * rows * TEXT_BUF_COMPONENT_SIZE);
text_mode_width = cols;
text_mode_height = rows;
if(mode === MODE_TEXT)
{
while(text_screen.childNodes.length > rows)
{
text_screen.removeChild(text_screen.firstChild);
}
while(text_screen.childNodes.length < rows)
{
text_screen.appendChild(document.createElement("div"));
}
for(var i = 0; i < rows; i++)
{
this.text_update_row(i);
}
update_scale_text();
}
else if(mode === MODE_GRAPHICAL_TEXT)
{
this.set_size_graphical_text();
}
};
this.set_size_graphical = function(width, height, buffer_width, buffer_height)
{
if(DEBUG_SCREEN_LAYERS)
{
// Draw the entire buffer. Useful for debugging
// panning / page flipping / screen splitting code for both
// v86 developers and os developers
width = buffer_width;
height = buffer_height;
}
graphic_screen.style.display = "block";
graphic_screen.width = width;
graphic_screen.height = height;
// graphic_context must be reconfigured whenever its graphic_screen is resized
graphic_context.imageSmoothingEnabled = false;
// add some scaling to tiny resolutions
if(width <= 640 &&
width * 2 < window.innerWidth * window.devicePixelRatio &&
height * 2 < window.innerHeight * window.devicePixelRatio)
{
base_scale = 2;
}
else
{
base_scale = 1;
}
update_scale_graphic();
};
this.set_scale = function(s_x, s_y)
{
scale_x = s_x;
scale_y = s_y;
update_scale_text();
update_scale_graphic();
};
function update_scale_text()
{
elem_set_scale(text_screen, scale_x, scale_y, true);
}
function update_scale_graphic()
{
elem_set_scale(graphic_screen, scale_x * base_scale, scale_y * base_scale, false);
}
function elem_set_scale(elem, scale_x, scale_y, use_scale)
{
if(!scale_x || !scale_y)
{
return;
}
elem.style.width = "";
elem.style.height = "";
if(use_scale)
{
elem.style.transform = "";
}
var rectangle = elem.getBoundingClientRect();
if(use_scale)
{
var scale_str = "";
scale_str += scale_x === 1 ? "" : " scaleX(" + scale_x + ")";
scale_str += scale_y === 1 ? "" : " scaleY(" + scale_y + ")";
elem.style.transform = scale_str;
}
else
{
// unblur non-fractional scales
if(scale_x % 1 === 0 && scale_y % 1 === 0)
{
graphic_screen.style["imageRendering"] = "crisp-edges"; // firefox
graphic_screen.style["imageRendering"] = "pixelated";
graphic_screen.style["-ms-interpolation-mode"] = "nearest-neighbor";
}
else
{
graphic_screen.style["imageRendering"] = "";
graphic_screen.style["-ms-interpolation-mode"] = "";
}
// undo fractional css-to-device pixel ratios
var device_pixel_ratio = window.devicePixelRatio || 1;
if(device_pixel_ratio % 1 !== 0)
{
scale_x /= device_pixel_ratio;
scale_y /= device_pixel_ratio;
}
}
if(scale_x !== 1)
{
elem.style.width = rectangle.width * scale_x + "px";
}
if(scale_y !== 1)
{
elem.style.height = rectangle.height * scale_y + "px";
}
}
this.update_cursor_scanline = function(start, end, enabled)
{
if(start !== cursor_start || end !== cursor_end || enabled !== cursor_enabled)
{
if(mode === MODE_TEXT)
{
if(enabled)
{
cursor_element.style.display = "inline";
cursor_element.style.height = (end - start) + "px";
cursor_element.style.marginTop = start + "px";
}
else
{
cursor_element.style.display = "none";
}
}
else if(mode === MODE_GRAPHICAL_TEXT)
{
if(cursor_row < text_mode_height)
{
changed_rows[cursor_row] = 1;
}
}
cursor_start = start;
cursor_end = end;
cursor_enabled = enabled;
}
};
this.update_cursor = function(row, col)
{
if(row !== cursor_row || col !== cursor_col)
{
if(row < text_mode_height)
{
changed_rows[row] = 1;
}
if(cursor_row < text_mode_height)
{
changed_rows[cursor_row] = 1;
}
cursor_row = row;
cursor_col = col;
}
};
this.text_update_row = function(row)
{
var offset = TEXT_BUF_COMPONENT_SIZE * row * text_mode_width,
row_element,
color_element,
fragment;
var blinking,
bg_color,
fg_color,
text;
row_element = text_screen.childNodes[row];
fragment = document.createElement("div");
for(var i = 0; i < text_mode_width; )
{
color_element = document.createElement("span");
blinking = text_mode_data[offset + FLAGS_INDEX] & FLAG_BLINKING;
bg_color = text_mode_data[offset + BG_COLOR_INDEX];
fg_color = text_mode_data[offset + FG_COLOR_INDEX];
if(blinking)
{
color_element.classList.add("blink");
}
color_element.style.backgroundColor = number_as_color(bg_color);
color_element.style.color = number_as_color(fg_color);
text = "";
// put characters of the same color in one element
while(i < text_mode_width &&
(text_mode_data[offset + FLAGS_INDEX] & FLAG_BLINKING) === blinking &&
text_mode_data[offset + BG_COLOR_INDEX] === bg_color &&
text_mode_data[offset + FG_COLOR_INDEX] === fg_color)
{
const chr = charmap[text_mode_data[offset + CHARACTER_INDEX]];
text += chr;
dbg_assert(chr);
i++;
offset += TEXT_BUF_COMPONENT_SIZE;
if(row === cursor_row)
{
if(i === cursor_col)
{
// next row will be cursor
// create new element
break;
}
else if(i === cursor_col + 1)
{
// found the cursor
cursor_element.style.backgroundColor = color_element.style.color;
fragment.appendChild(cursor_element);
break;
}
}
}
color_element.textContent = text;
fragment.appendChild(color_element);
}
row_element.parentNode.replaceChild(fragment, row_element);
};
this.update_buffer = function(layers)
{
if(DEBUG_SCREEN_LAYERS)
{
// For each visible layer that would've been drawn, draw a
// rectangle to visualise the layer instead.
graphic_context.strokeStyle = "#0F0";
graphic_context.lineWidth = 4;
for(const layer of layers)
{
graphic_context.strokeRect(
layer.buffer_x,
layer.buffer_y,
layer.buffer_width,
layer.buffer_height
);
}
graphic_context.lineWidth = 1;
return;
}
for(const layer of layers)
{
graphic_context.putImageData(
layer.image_data,
layer.screen_x - layer.buffer_x,
layer.screen_y - layer.buffer_y,
layer.buffer_x,
layer.buffer_y,
layer.buffer_width,
layer.buffer_height
);
}
};
// XXX: duplicated in DummyScreenAdapter
this.get_text_screen = function()
{
var screen = [];
for(var i = 0; i < text_mode_height; i++)
{
screen.push(this.get_text_row(i));
}
return screen;
};
this.get_text_row = function(y)
{
const begin = y * text_mode_width * TEXT_BUF_COMPONENT_SIZE + CHARACTER_INDEX;
const end = begin + text_mode_width * TEXT_BUF_COMPONENT_SIZE;
let row = "";
for(let i = begin; i < end; i += TEXT_BUF_COMPONENT_SIZE)
{
row += charmap[text_mode_data[i]];
}
return row;
};
this.init();
}