@aptre/v86 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +22 -0
  2. package/LICENSE.MIT +22 -0
  3. package/Readme.md +237 -0
  4. package/dist/v86.browser.js +26666 -0
  5. package/dist/v86.browser.js.map +7 -0
  6. package/dist/v86.js +26632 -0
  7. package/dist/v86.js.map +7 -0
  8. package/gen/generate_analyzer.ts +512 -0
  9. package/gen/generate_interpreter.ts +522 -0
  10. package/gen/generate_jit.ts +624 -0
  11. package/gen/rust_ast.ts +107 -0
  12. package/gen/util.ts +35 -0
  13. package/gen/x86_table.ts +1836 -0
  14. package/lib/9p.ts +1547 -0
  15. package/lib/filesystem.ts +1879 -0
  16. package/lib/marshall.ts +168 -0
  17. package/lib/softfloat/softfloat.c +32501 -0
  18. package/lib/zstd/zstddeclib.c +13520 -0
  19. package/package.json +75 -0
  20. package/src/acpi.ts +267 -0
  21. package/src/browser/dummy_screen.ts +106 -0
  22. package/src/browser/fake_network.ts +1771 -0
  23. package/src/browser/fetch_network.ts +361 -0
  24. package/src/browser/filestorage.ts +124 -0
  25. package/src/browser/inbrowser_network.ts +57 -0
  26. package/src/browser/keyboard.ts +564 -0
  27. package/src/browser/main.ts +3415 -0
  28. package/src/browser/mouse.ts +255 -0
  29. package/src/browser/network.ts +142 -0
  30. package/src/browser/print_stats.ts +336 -0
  31. package/src/browser/screen.ts +978 -0
  32. package/src/browser/serial.ts +316 -0
  33. package/src/browser/speaker.ts +1223 -0
  34. package/src/browser/starter.ts +1688 -0
  35. package/src/browser/wisp_network.ts +332 -0
  36. package/src/browser/worker_bus.ts +64 -0
  37. package/src/buffer.ts +652 -0
  38. package/src/bus.ts +78 -0
  39. package/src/const.ts +128 -0
  40. package/src/cpu.ts +2891 -0
  41. package/src/dma.ts +474 -0
  42. package/src/elf.ts +251 -0
  43. package/src/floppy.ts +1778 -0
  44. package/src/ide.ts +3455 -0
  45. package/src/io.ts +504 -0
  46. package/src/iso9660.ts +317 -0
  47. package/src/kernel.ts +250 -0
  48. package/src/lib.ts +645 -0
  49. package/src/log.ts +149 -0
  50. package/src/main.ts +199 -0
  51. package/src/ne2k.ts +1589 -0
  52. package/src/pci.ts +815 -0
  53. package/src/pit.ts +406 -0
  54. package/src/ps2.ts +820 -0
  55. package/src/rtc.ts +537 -0
  56. package/src/rust/analysis.rs +101 -0
  57. package/src/rust/codegen.rs +2660 -0
  58. package/src/rust/config.rs +3 -0
  59. package/src/rust/control_flow.rs +425 -0
  60. package/src/rust/cpu/apic.rs +658 -0
  61. package/src/rust/cpu/arith.rs +1207 -0
  62. package/src/rust/cpu/call_indirect.rs +2 -0
  63. package/src/rust/cpu/cpu.rs +4501 -0
  64. package/src/rust/cpu/fpu.rs +923 -0
  65. package/src/rust/cpu/global_pointers.rs +112 -0
  66. package/src/rust/cpu/instructions.rs +2486 -0
  67. package/src/rust/cpu/instructions_0f.rs +5261 -0
  68. package/src/rust/cpu/ioapic.rs +316 -0
  69. package/src/rust/cpu/memory.rs +351 -0
  70. package/src/rust/cpu/misc_instr.rs +613 -0
  71. package/src/rust/cpu/mod.rs +16 -0
  72. package/src/rust/cpu/modrm.rs +133 -0
  73. package/src/rust/cpu/pic.rs +402 -0
  74. package/src/rust/cpu/sse_instr.rs +361 -0
  75. package/src/rust/cpu/string.rs +701 -0
  76. package/src/rust/cpu/vga.rs +175 -0
  77. package/src/rust/cpu_context.rs +69 -0
  78. package/src/rust/dbg.rs +98 -0
  79. package/src/rust/gen/analyzer.rs +3807 -0
  80. package/src/rust/gen/analyzer0f.rs +3992 -0
  81. package/src/rust/gen/interpreter.rs +4447 -0
  82. package/src/rust/gen/interpreter0f.rs +5404 -0
  83. package/src/rust/gen/jit.rs +5080 -0
  84. package/src/rust/gen/jit0f.rs +5547 -0
  85. package/src/rust/gen/mod.rs +14 -0
  86. package/src/rust/jit.rs +2443 -0
  87. package/src/rust/jit_instructions.rs +7881 -0
  88. package/src/rust/js_api.rs +6 -0
  89. package/src/rust/leb.rs +46 -0
  90. package/src/rust/lib.rs +29 -0
  91. package/src/rust/modrm.rs +330 -0
  92. package/src/rust/opstats.rs +249 -0
  93. package/src/rust/page.rs +15 -0
  94. package/src/rust/paging.rs +25 -0
  95. package/src/rust/prefix.rs +15 -0
  96. package/src/rust/profiler.rs +155 -0
  97. package/src/rust/regs.rs +38 -0
  98. package/src/rust/softfloat.rs +286 -0
  99. package/src/rust/state_flags.rs +27 -0
  100. package/src/rust/wasmgen/mod.rs +2 -0
  101. package/src/rust/wasmgen/wasm_builder.rs +1047 -0
  102. package/src/rust/wasmgen/wasm_opcodes.rs +221 -0
  103. package/src/rust/zstd.rs +105 -0
  104. package/src/sb16.ts +1928 -0
  105. package/src/state.ts +359 -0
  106. package/src/uart.ts +472 -0
  107. package/src/vga.ts +2791 -0
  108. package/src/virtio.ts +1756 -0
  109. package/src/virtio_balloon.ts +273 -0
  110. package/src/virtio_console.ts +372 -0
  111. package/src/virtio_net.ts +326 -0
@@ -0,0 +1,978 @@
1
+ declare let DEBUG: boolean
2
+
3
+ import { dbg_assert } from '../log.js'
4
+ import { get_charmap } from '../lib.js'
5
+
6
+ // Draws entire buffer and visualizes the layers that would be drawn
7
+ export const DEBUG_SCREEN_LAYERS = DEBUG && false
8
+
9
+ interface ScreenOptions {
10
+ container: HTMLElement
11
+ scale?: number
12
+ use_graphical_text?: boolean
13
+ encoding?: string
14
+ }
15
+
16
+ interface ScreenLayer {
17
+ image_data: ImageData
18
+ screen_x: number
19
+ screen_y: number
20
+ buffer_x: number
21
+ buffer_y: number
22
+ buffer_width: number
23
+ buffer_height: number
24
+ }
25
+
26
+ const MODE_TEXT = 0
27
+ const MODE_GRAPHICAL = 1
28
+ const MODE_GRAPHICAL_TEXT = 2
29
+
30
+ const CHARACTER_INDEX = 0
31
+ const FLAGS_INDEX = 1
32
+ const BG_COLOR_INDEX = 2
33
+ const FG_COLOR_INDEX = 3
34
+ const TEXT_BUF_COMPONENT_SIZE = 4
35
+
36
+ const FLAG_BLINKING = 0x01
37
+ const FLAG_FONT_PAGE_B = 0x02
38
+
39
+ export class ScreenAdapter {
40
+ FLAG_BLINKING = FLAG_BLINKING
41
+ FLAG_FONT_PAGE_B = FLAG_FONT_PAGE_B
42
+
43
+ screen_fill_buffer: () => void
44
+
45
+ private graphic_screen: HTMLCanvasElement
46
+ private graphic_context: CanvasRenderingContext2D
47
+ private text_screen: HTMLDivElement
48
+ private cursor_element: HTMLDivElement
49
+
50
+ private cursor_row = 0
51
+ private cursor_col = 0
52
+ private scale_x: number
53
+ private scale_y: number
54
+ private base_scale = 1
55
+ private changed_rows: Int8Array | undefined
56
+ private mode = MODE_TEXT
57
+ private text_mode_data: Int32Array | undefined
58
+ private text_mode_width = 0
59
+ private text_mode_height = 0
60
+ private offscreen_context: OffscreenCanvasRenderingContext2D | null = null
61
+ private offscreen_extra_context: OffscreenCanvasRenderingContext2D | null =
62
+ null
63
+ private font_context: OffscreenCanvasRenderingContext2D | null = null
64
+ private font_image_data: ImageData | null = null
65
+ private font_is_visible = new Int8Array(8 * 256)
66
+ private font_height = 0
67
+ private font_width = 0
68
+ private font_width_9px = false
69
+ private font_width_dbl = false
70
+ private font_copy_8th_col = false
71
+ private font_page_a = 0
72
+ private font_page_b = 0
73
+ private blink_visible = false
74
+ private tm_last_update = 0
75
+ private cursor_start = 0
76
+ private cursor_end = 0
77
+ private cursor_enabled = false
78
+ private charmap: string
79
+ private timer_id = 0
80
+ private paused = false
81
+ private options: ScreenOptions
82
+
83
+ constructor(options: ScreenOptions, screen_fill_buffer: () => void) {
84
+ const screen_container = options.container
85
+ this.screen_fill_buffer = screen_fill_buffer
86
+ this.options = options
87
+
88
+ console.assert(!!screen_container, 'options.container must be provided')
89
+
90
+ this.scale_x = options.scale !== undefined ? options.scale : 1
91
+ this.scale_y = options.scale !== undefined ? options.scale : 1
92
+ this.charmap = get_charmap(options.encoding || '')
93
+
94
+ let graphic_screen = screen_container.getElementsByTagName('canvas')[0]
95
+ if (!graphic_screen) {
96
+ graphic_screen = document.createElement('canvas')
97
+ screen_container.appendChild(graphic_screen)
98
+ }
99
+ this.graphic_screen = graphic_screen
100
+ this.graphic_context = graphic_screen.getContext('2d', {
101
+ alpha: false,
102
+ })!
103
+
104
+ let text_screen = screen_container.getElementsByTagName('div')[0] as
105
+ | HTMLDivElement
106
+ | undefined
107
+ if (!text_screen) {
108
+ text_screen = document.createElement('div')
109
+ screen_container.appendChild(text_screen)
110
+ }
111
+ this.text_screen = text_screen
112
+
113
+ this.cursor_element = document.createElement('div')
114
+
115
+ this.init()
116
+ }
117
+
118
+ private number_as_color(n: number): string {
119
+ const s = n.toString(16)
120
+ return '#' + '0'.repeat(6 - s.length) + s
121
+ }
122
+
123
+ private render_font_bitmap(vga_bitmap: Uint8Array): void {
124
+ const bitmap_width = this.font_width * 256
125
+ const bitmap_height = this.font_height * 8
126
+
127
+ let font_canvas = this.font_context ? this.font_context.canvas : null
128
+ if (
129
+ !font_canvas ||
130
+ font_canvas.width !== bitmap_width ||
131
+ font_canvas.height !== bitmap_height
132
+ ) {
133
+ if (!font_canvas) {
134
+ font_canvas = new OffscreenCanvas(bitmap_width, bitmap_height)
135
+ this.font_context = font_canvas.getContext('2d')!
136
+ } else {
137
+ font_canvas.width = bitmap_width
138
+ font_canvas.height = bitmap_height
139
+ }
140
+ this.font_image_data = this.font_context!.createImageData(
141
+ bitmap_width,
142
+ bitmap_height,
143
+ )
144
+ }
145
+
146
+ const font_bitmap = this.font_image_data!.data
147
+ let i_dst = 0
148
+ let is_visible = false
149
+ const font_width_dbl = this.font_width_dbl
150
+ const font_width = this.font_width
151
+ const font_height = this.font_height
152
+ const font_width_9px = this.font_width_9px
153
+ const font_copy_8th_col = this.font_copy_8th_col
154
+
155
+ const put_bit = font_width_dbl
156
+ ? (value: number) => {
157
+ is_visible = is_visible || !!value
158
+ font_bitmap[i_dst + 3] = value
159
+ font_bitmap[i_dst + 7] = value
160
+ i_dst += 8
161
+ }
162
+ : (value: number) => {
163
+ is_visible = is_visible || !!value
164
+ font_bitmap[i_dst + 3] = value
165
+ i_dst += 4
166
+ }
167
+
168
+ const vga_inc_chr = 32 - font_height
169
+ const dst_inc_row = bitmap_width * (font_height - 1) * 4
170
+ const dst_inc_col = (font_width - bitmap_width * font_height) * 4
171
+ const dst_inc_line = font_width * 255 * 4
172
+
173
+ for (
174
+ let i_chr_all = 0, i_vga = 0;
175
+ i_chr_all < 2048;
176
+ ++i_chr_all, i_vga += vga_inc_chr, i_dst += dst_inc_col
177
+ ) {
178
+ const i_chr = i_chr_all % 256
179
+ if (i_chr_all && !i_chr) {
180
+ i_dst += dst_inc_row
181
+ }
182
+ is_visible = false
183
+ for (
184
+ let i_line = 0;
185
+ i_line < font_height;
186
+ ++i_line, ++i_vga, i_dst += dst_inc_line
187
+ ) {
188
+ const line_bits = vga_bitmap[i_vga]
189
+ for (let i_bit = 0x80; i_bit > 0; i_bit >>= 1) {
190
+ put_bit(line_bits & i_bit ? 255 : 0)
191
+ }
192
+ if (font_width_9px) {
193
+ put_bit(
194
+ font_copy_8th_col &&
195
+ i_chr >= 0xc0 &&
196
+ i_chr <= 0xdf &&
197
+ line_bits & 1
198
+ ? 255
199
+ : 0,
200
+ )
201
+ }
202
+ }
203
+ this.font_is_visible[i_chr_all] = is_visible ? 1 : 0
204
+ }
205
+
206
+ this.font_context!.putImageData(this.font_image_data!, 0, 0)
207
+ }
208
+
209
+ private render_changed_rows(): number {
210
+ const font_context = this.font_context!
211
+ const offscreen_context = this.offscreen_context!
212
+ const offscreen_extra_context = this.offscreen_extra_context!
213
+ const font_canvas = font_context.canvas
214
+ const offscreen_extra_canvas = offscreen_extra_context.canvas
215
+ const text_mode_width = this.text_mode_width
216
+ const font_width = this.font_width
217
+ const font_height = this.font_height
218
+ const text_mode_data = this.text_mode_data!
219
+ const changed_rows = this.changed_rows!
220
+ const txt_row_size = text_mode_width * TEXT_BUF_COMPONENT_SIZE
221
+ const gfx_width = text_mode_width * font_width
222
+ const row_extra_1_y = 0
223
+ const row_extra_2_y = font_height
224
+
225
+ let n_rows_rendered = 0
226
+ for (
227
+ let row_i = 0, row_y = 0, txt_i = 0;
228
+ row_i < this.text_mode_height;
229
+ ++row_i, row_y += font_height
230
+ ) {
231
+ if (!changed_rows[row_i]) {
232
+ txt_i += txt_row_size
233
+ continue
234
+ }
235
+ ++n_rows_rendered
236
+
237
+ offscreen_extra_context.clearRect(
238
+ 0,
239
+ row_extra_2_y,
240
+ gfx_width,
241
+ font_height,
242
+ )
243
+
244
+ let fg_rgba: number | undefined
245
+ let fg_x = 0
246
+ let bg_rgba: number | undefined
247
+ let bg_x = 0
248
+ for (
249
+ let col_x = 0;
250
+ col_x < gfx_width;
251
+ col_x += font_width, txt_i += TEXT_BUF_COMPONENT_SIZE
252
+ ) {
253
+ const chr = text_mode_data[txt_i + CHARACTER_INDEX]
254
+ const chr_flags = text_mode_data[txt_i + FLAGS_INDEX]
255
+ const chr_bg_rgba = text_mode_data[txt_i + BG_COLOR_INDEX]
256
+ const chr_fg_rgba = text_mode_data[txt_i + FG_COLOR_INDEX]
257
+ const chr_font_page =
258
+ chr_flags & FLAG_FONT_PAGE_B
259
+ ? this.font_page_b
260
+ : this.font_page_a
261
+ const chr_visible =
262
+ (!(chr_flags & FLAG_BLINKING) || this.blink_visible) &&
263
+ !!this.font_is_visible[(chr_font_page << 8) + chr]
264
+
265
+ if (bg_rgba !== chr_bg_rgba) {
266
+ if (bg_rgba !== undefined) {
267
+ offscreen_context.fillStyle =
268
+ this.number_as_color(bg_rgba)
269
+ offscreen_context.fillRect(
270
+ bg_x,
271
+ row_y,
272
+ col_x - bg_x,
273
+ font_height,
274
+ )
275
+ }
276
+ bg_rgba = chr_bg_rgba
277
+ bg_x = col_x
278
+ }
279
+
280
+ if (fg_rgba !== chr_fg_rgba) {
281
+ if (fg_rgba !== undefined) {
282
+ offscreen_extra_context.fillStyle =
283
+ this.number_as_color(fg_rgba)
284
+ offscreen_extra_context.fillRect(
285
+ fg_x,
286
+ row_extra_1_y,
287
+ col_x - fg_x,
288
+ font_height,
289
+ )
290
+ }
291
+ fg_rgba = chr_fg_rgba
292
+ fg_x = col_x
293
+ }
294
+
295
+ if (chr_visible) {
296
+ offscreen_extra_context.drawImage(
297
+ font_canvas,
298
+ chr * font_width,
299
+ chr_font_page * font_height,
300
+ font_width,
301
+ font_height,
302
+ col_x,
303
+ row_extra_2_y,
304
+ font_width,
305
+ font_height,
306
+ )
307
+ }
308
+ }
309
+
310
+ offscreen_extra_context.fillStyle = this.number_as_color(fg_rgba!)
311
+ offscreen_extra_context.fillRect(
312
+ fg_x,
313
+ row_extra_1_y,
314
+ gfx_width - fg_x,
315
+ font_height,
316
+ )
317
+
318
+ offscreen_extra_context.globalCompositeOperation = 'destination-in'
319
+ offscreen_extra_context.drawImage(
320
+ offscreen_extra_canvas,
321
+ 0,
322
+ row_extra_2_y,
323
+ gfx_width,
324
+ font_height,
325
+ 0,
326
+ row_extra_1_y,
327
+ gfx_width,
328
+ font_height,
329
+ )
330
+ offscreen_extra_context.globalCompositeOperation = 'source-over'
331
+
332
+ offscreen_context.fillStyle = this.number_as_color(bg_rgba!)
333
+ offscreen_context.fillRect(
334
+ bg_x,
335
+ row_y,
336
+ gfx_width - bg_x,
337
+ font_height,
338
+ )
339
+
340
+ offscreen_context.drawImage(
341
+ offscreen_extra_canvas,
342
+ 0,
343
+ row_extra_1_y,
344
+ gfx_width,
345
+ font_height,
346
+ 0,
347
+ row_y,
348
+ gfx_width,
349
+ font_height,
350
+ )
351
+ }
352
+
353
+ if (n_rows_rendered) {
354
+ if (
355
+ this.blink_visible &&
356
+ this.cursor_enabled &&
357
+ changed_rows[this.cursor_row]
358
+ ) {
359
+ const cursor_txt_i =
360
+ (this.cursor_row * text_mode_width + this.cursor_col) *
361
+ TEXT_BUF_COMPONENT_SIZE
362
+ const cursor_rgba =
363
+ text_mode_data[cursor_txt_i + FG_COLOR_INDEX]
364
+ offscreen_context.fillStyle = this.number_as_color(cursor_rgba)
365
+ offscreen_context.fillRect(
366
+ this.cursor_col * font_width,
367
+ this.cursor_row * font_height + this.cursor_start,
368
+ font_width,
369
+ this.cursor_end - this.cursor_start + 1,
370
+ )
371
+ }
372
+ changed_rows.fill(0)
373
+ }
374
+
375
+ return n_rows_rendered
376
+ }
377
+
378
+ private mark_blinking_rows_dirty(): void {
379
+ const txt_row_size = this.text_mode_width * TEXT_BUF_COMPONENT_SIZE
380
+ const text_mode_data = this.text_mode_data!
381
+ const changed_rows = this.changed_rows!
382
+ for (let row_i = 0, txt_i = 0; row_i < this.text_mode_height; ++row_i) {
383
+ if (changed_rows[row_i]) {
384
+ txt_i += txt_row_size
385
+ continue
386
+ }
387
+ for (
388
+ let col_i = 0;
389
+ col_i < this.text_mode_width;
390
+ ++col_i, txt_i += TEXT_BUF_COMPONENT_SIZE
391
+ ) {
392
+ if (text_mode_data[txt_i + FLAGS_INDEX] & FLAG_BLINKING) {
393
+ changed_rows[row_i] = 1
394
+ txt_i += txt_row_size - col_i * TEXT_BUF_COMPONENT_SIZE
395
+ break
396
+ }
397
+ }
398
+ }
399
+ }
400
+
401
+ init(): void {
402
+ this.cursor_element.classList.add('cursor')
403
+ this.cursor_element.style.position = 'absolute'
404
+ this.cursor_element.style.backgroundColor = '#ccc'
405
+ this.cursor_element.style.width = '7px'
406
+ this.cursor_element.style.display = 'inline-block'
407
+
408
+ this.set_mode(false)
409
+ this.set_size_text(80, 25)
410
+ if (this.mode === MODE_GRAPHICAL_TEXT) {
411
+ this.set_size_graphical(720, 400, 720, 400)
412
+ }
413
+
414
+ this.set_scale(this.scale_x, this.scale_y)
415
+
416
+ this.timer()
417
+ }
418
+
419
+ make_screenshot(): HTMLImageElement {
420
+ const image = new Image()
421
+
422
+ if (this.mode === MODE_GRAPHICAL || this.mode === MODE_GRAPHICAL_TEXT) {
423
+ image.src = this.graphic_screen.toDataURL('image/png')
424
+ } else {
425
+ const char_size = [9, 16]
426
+
427
+ const canvas = document.createElement('canvas')
428
+ canvas.width = this.text_mode_width * char_size[0]
429
+ canvas.height = this.text_mode_height * char_size[1]
430
+ const context = canvas.getContext('2d')!
431
+ context.imageSmoothingEnabled = false
432
+ context.font = window.getComputedStyle(this.text_screen).font
433
+ context.textBaseline = 'top'
434
+
435
+ for (let y = 0; y < this.text_mode_height; y++) {
436
+ for (let x = 0; x < this.text_mode_width; x++) {
437
+ const index =
438
+ (y * this.text_mode_width + x) * TEXT_BUF_COMPONENT_SIZE
439
+ const character =
440
+ this.text_mode_data![index + CHARACTER_INDEX]
441
+ const bg_color =
442
+ this.text_mode_data![index + BG_COLOR_INDEX]
443
+ const fg_color =
444
+ this.text_mode_data![index + FG_COLOR_INDEX]
445
+
446
+ context.fillStyle = this.number_as_color(bg_color)
447
+ context.fillRect(
448
+ x * char_size[0],
449
+ y * char_size[1],
450
+ char_size[0],
451
+ char_size[1],
452
+ )
453
+ context.fillStyle = this.number_as_color(fg_color)
454
+ context.fillText(
455
+ this.charmap[character],
456
+ x * char_size[0],
457
+ y * char_size[1],
458
+ )
459
+ }
460
+ }
461
+
462
+ if (
463
+ this.cursor_element.style.display !== 'none' &&
464
+ this.cursor_row < this.text_mode_height &&
465
+ this.cursor_col < this.text_mode_width
466
+ ) {
467
+ context.fillStyle = this.cursor_element.style.backgroundColor
468
+ context.fillRect(
469
+ this.cursor_col * char_size[0],
470
+ this.cursor_row * char_size[1] +
471
+ parseInt(this.cursor_element.style.marginTop, 10),
472
+ parseInt(this.cursor_element.style.width, 10),
473
+ parseInt(this.cursor_element.style.height, 10),
474
+ )
475
+ }
476
+
477
+ image.src = canvas.toDataURL('image/png')
478
+ }
479
+ return image
480
+ }
481
+
482
+ put_char(
483
+ row: number,
484
+ col: number,
485
+ chr: number,
486
+ flags: number,
487
+ bg_color: number,
488
+ fg_color: number,
489
+ ): void {
490
+ dbg_assert(row >= 0 && row < this.text_mode_height)
491
+ dbg_assert(col >= 0 && col < this.text_mode_width)
492
+ dbg_assert(chr >= 0 && chr < 0x100)
493
+
494
+ const p = TEXT_BUF_COMPONENT_SIZE * (row * this.text_mode_width + col)
495
+
496
+ this.text_mode_data![p + CHARACTER_INDEX] = chr
497
+ this.text_mode_data![p + FLAGS_INDEX] = flags
498
+ this.text_mode_data![p + BG_COLOR_INDEX] = bg_color
499
+ this.text_mode_data![p + FG_COLOR_INDEX] = fg_color
500
+
501
+ this.changed_rows![row] = 1
502
+ }
503
+
504
+ timer(): void {
505
+ this.timer_id = requestAnimationFrame(() => this.update_screen())
506
+ }
507
+
508
+ update_screen(): void {
509
+ if (!this.paused) {
510
+ if (this.mode === MODE_TEXT) {
511
+ this.update_text()
512
+ } else if (this.mode === MODE_GRAPHICAL) {
513
+ this.update_graphical()
514
+ } else {
515
+ this.update_graphical_text()
516
+ }
517
+ }
518
+ this.timer()
519
+ }
520
+
521
+ update_text(): void {
522
+ for (let i = 0; i < this.text_mode_height; i++) {
523
+ if (this.changed_rows![i]) {
524
+ this.text_update_row(i)
525
+ this.changed_rows![i] = 0
526
+ }
527
+ }
528
+ }
529
+
530
+ update_graphical(): void {
531
+ this.screen_fill_buffer()
532
+ }
533
+
534
+ update_graphical_text(): void {
535
+ if (this.offscreen_context) {
536
+ const tm_now = performance.now()
537
+ if (tm_now - this.tm_last_update > 266) {
538
+ this.blink_visible = !this.blink_visible
539
+ if (this.cursor_enabled) {
540
+ this.changed_rows![this.cursor_row] = 1
541
+ }
542
+ this.mark_blinking_rows_dirty()
543
+ this.tm_last_update = tm_now
544
+ }
545
+ if (this.render_changed_rows()) {
546
+ this.graphic_context.drawImage(
547
+ this.offscreen_context.canvas,
548
+ 0,
549
+ 0,
550
+ )
551
+ }
552
+ }
553
+ }
554
+
555
+ destroy(): void {
556
+ if (this.timer_id) {
557
+ cancelAnimationFrame(this.timer_id)
558
+ this.timer_id = 0
559
+ }
560
+ }
561
+
562
+ pause(): void {
563
+ this.paused = true
564
+ this.cursor_element.classList.remove('blinking-cursor')
565
+ }
566
+
567
+ continue(): void {
568
+ this.paused = false
569
+ this.cursor_element.classList.add('blinking-cursor')
570
+ }
571
+
572
+ set_mode(graphical: boolean): void {
573
+ this.mode = graphical
574
+ ? MODE_GRAPHICAL
575
+ : this.options.use_graphical_text
576
+ ? MODE_GRAPHICAL_TEXT
577
+ : MODE_TEXT
578
+
579
+ if (this.mode === MODE_TEXT) {
580
+ this.text_screen.style.display = 'block'
581
+ this.graphic_screen.style.display = 'none'
582
+ } else {
583
+ this.text_screen.style.display = 'none'
584
+ this.graphic_screen.style.display = 'block'
585
+
586
+ if (this.mode === MODE_GRAPHICAL_TEXT && this.changed_rows) {
587
+ this.changed_rows.fill(1)
588
+ }
589
+ }
590
+ }
591
+
592
+ set_font_bitmap(
593
+ height: number,
594
+ width_9px: boolean,
595
+ width_dbl: boolean,
596
+ copy_8th_col: boolean,
597
+ vga_bitmap: Uint8Array,
598
+ vga_bitmap_changed: boolean,
599
+ ): void {
600
+ const width = width_dbl ? 16 : width_9px ? 9 : 8
601
+ if (
602
+ this.font_height !== height ||
603
+ this.font_width !== width ||
604
+ this.font_width_9px !== width_9px ||
605
+ this.font_width_dbl !== width_dbl ||
606
+ this.font_copy_8th_col !== copy_8th_col ||
607
+ vga_bitmap_changed
608
+ ) {
609
+ const size_changed =
610
+ this.font_width !== width || this.font_height !== height
611
+ this.font_height = height
612
+ this.font_width = width
613
+ this.font_width_9px = width_9px
614
+ this.font_width_dbl = width_dbl
615
+ this.font_copy_8th_col = copy_8th_col
616
+ if (this.mode === MODE_GRAPHICAL_TEXT) {
617
+ this.render_font_bitmap(vga_bitmap)
618
+ this.changed_rows!.fill(1)
619
+ if (size_changed) {
620
+ this.set_size_graphical_text()
621
+ }
622
+ }
623
+ }
624
+ }
625
+
626
+ set_font_page(page_a: number, page_b: number): void {
627
+ if (this.font_page_a !== page_a || this.font_page_b !== page_b) {
628
+ this.font_page_a = page_a
629
+ this.font_page_b = page_b
630
+ this.changed_rows!.fill(1)
631
+ }
632
+ }
633
+
634
+ clear_screen(): void {
635
+ this.graphic_context.fillStyle = '#000'
636
+ this.graphic_context.fillRect(
637
+ 0,
638
+ 0,
639
+ this.graphic_screen.width,
640
+ this.graphic_screen.height,
641
+ )
642
+ }
643
+
644
+ set_size_graphical_text(): void {
645
+ if (!this.font_context) {
646
+ return
647
+ }
648
+
649
+ const gfx_width = this.font_width * this.text_mode_width
650
+ const gfx_height = this.font_height * this.text_mode_height
651
+ const offscreen_extra_height = this.font_height * 2
652
+
653
+ if (
654
+ !this.offscreen_context ||
655
+ this.offscreen_context.canvas.width !== gfx_width ||
656
+ this.offscreen_context.canvas.height !== gfx_height ||
657
+ this.offscreen_extra_context!.canvas.height !==
658
+ offscreen_extra_height
659
+ ) {
660
+ if (!this.offscreen_context) {
661
+ const offscreen_canvas = new OffscreenCanvas(
662
+ gfx_width,
663
+ gfx_height,
664
+ )
665
+ this.offscreen_context = offscreen_canvas.getContext('2d', {
666
+ alpha: false,
667
+ })!
668
+ const offscreen_extra_canvas = new OffscreenCanvas(
669
+ gfx_width,
670
+ offscreen_extra_height,
671
+ )
672
+ this.offscreen_extra_context =
673
+ offscreen_extra_canvas.getContext('2d')!
674
+ } else {
675
+ this.offscreen_context.canvas.width = gfx_width
676
+ this.offscreen_context.canvas.height = gfx_height
677
+ this.offscreen_extra_context!.canvas.width = gfx_width
678
+ this.offscreen_extra_context!.canvas.height =
679
+ offscreen_extra_height
680
+ }
681
+
682
+ this.set_size_graphical(
683
+ gfx_width,
684
+ gfx_height,
685
+ gfx_width,
686
+ gfx_height,
687
+ )
688
+
689
+ this.changed_rows!.fill(1)
690
+ }
691
+ }
692
+
693
+ set_size_text(cols: number, rows: number): void {
694
+ if (cols === this.text_mode_width && rows === this.text_mode_height) {
695
+ return
696
+ }
697
+
698
+ this.changed_rows = new Int8Array(rows)
699
+ this.text_mode_data = new Int32Array(
700
+ cols * rows * TEXT_BUF_COMPONENT_SIZE,
701
+ )
702
+
703
+ this.text_mode_width = cols
704
+ this.text_mode_height = rows
705
+
706
+ if (this.mode === MODE_TEXT) {
707
+ while (this.text_screen.childNodes.length > rows) {
708
+ this.text_screen.removeChild(this.text_screen.firstChild!)
709
+ }
710
+
711
+ while (this.text_screen.childNodes.length < rows) {
712
+ this.text_screen.appendChild(document.createElement('div'))
713
+ }
714
+
715
+ for (let i = 0; i < rows; i++) {
716
+ this.text_update_row(i)
717
+ }
718
+
719
+ this.update_scale_text()
720
+ } else if (this.mode === MODE_GRAPHICAL_TEXT) {
721
+ this.set_size_graphical_text()
722
+ }
723
+ }
724
+
725
+ set_size_graphical(
726
+ width: number,
727
+ height: number,
728
+ buffer_width: number,
729
+ buffer_height: number,
730
+ ): void {
731
+ if (DEBUG_SCREEN_LAYERS) {
732
+ width = buffer_width
733
+ height = buffer_height
734
+ }
735
+
736
+ this.graphic_screen.style.display = 'block'
737
+
738
+ this.graphic_screen.width = width
739
+ this.graphic_screen.height = height
740
+
741
+ this.graphic_context.imageSmoothingEnabled = false
742
+
743
+ if (
744
+ width <= 640 &&
745
+ width * 2 < window.innerWidth * window.devicePixelRatio &&
746
+ height * 2 < window.innerHeight * window.devicePixelRatio
747
+ ) {
748
+ this.base_scale = 2
749
+ } else {
750
+ this.base_scale = 1
751
+ }
752
+
753
+ this.update_scale_graphic()
754
+ }
755
+
756
+ set_scale(s_x: number, s_y: number): void {
757
+ this.scale_x = s_x
758
+ this.scale_y = s_y
759
+
760
+ this.update_scale_text()
761
+ this.update_scale_graphic()
762
+ }
763
+
764
+ private update_scale_text(): void {
765
+ this.elem_set_scale(this.text_screen, this.scale_x, this.scale_y, true)
766
+ }
767
+
768
+ private update_scale_graphic(): void {
769
+ this.elem_set_scale(
770
+ this.graphic_screen,
771
+ this.scale_x * this.base_scale,
772
+ this.scale_y * this.base_scale,
773
+ false,
774
+ )
775
+ }
776
+
777
+ private elem_set_scale(
778
+ elem: HTMLElement,
779
+ sx: number,
780
+ sy: number,
781
+ use_scale: boolean,
782
+ ): void {
783
+ if (!sx || !sy) {
784
+ return
785
+ }
786
+
787
+ elem.style.width = ''
788
+ elem.style.height = ''
789
+
790
+ if (use_scale) {
791
+ elem.style.transform = ''
792
+ }
793
+
794
+ const rectangle = elem.getBoundingClientRect()
795
+
796
+ if (use_scale) {
797
+ let scale_str = ''
798
+
799
+ scale_str += sx === 1 ? '' : ' scaleX(' + sx + ')'
800
+ scale_str += sy === 1 ? '' : ' scaleY(' + sy + ')'
801
+
802
+ elem.style.transform = scale_str
803
+ } else {
804
+ if (sx % 1 === 0 && sy % 1 === 0) {
805
+ this.graphic_screen.style.imageRendering = 'crisp-edges' // firefox
806
+ this.graphic_screen.style.imageRendering = 'pixelated'
807
+ } else {
808
+ this.graphic_screen.style.imageRendering = ''
809
+ }
810
+
811
+ const device_pixel_ratio = window.devicePixelRatio || 1
812
+ if (device_pixel_ratio % 1 !== 0) {
813
+ sx /= device_pixel_ratio
814
+ sy /= device_pixel_ratio
815
+ }
816
+ }
817
+
818
+ if (sx !== 1) {
819
+ elem.style.width = rectangle.width * sx + 'px'
820
+ }
821
+ if (sy !== 1) {
822
+ elem.style.height = rectangle.height * sy + 'px'
823
+ }
824
+ }
825
+
826
+ update_cursor_scanline(start: number, end: number, enabled: boolean): void {
827
+ if (
828
+ start !== this.cursor_start ||
829
+ end !== this.cursor_end ||
830
+ enabled !== this.cursor_enabled
831
+ ) {
832
+ if (this.mode === MODE_TEXT) {
833
+ if (enabled) {
834
+ this.cursor_element.style.display = 'inline'
835
+ this.cursor_element.style.height = end - start + 'px'
836
+ this.cursor_element.style.marginTop = start + 'px'
837
+ } else {
838
+ this.cursor_element.style.display = 'none'
839
+ }
840
+ } else if (this.mode === MODE_GRAPHICAL_TEXT) {
841
+ if (this.cursor_row < this.text_mode_height) {
842
+ this.changed_rows![this.cursor_row] = 1
843
+ }
844
+ }
845
+
846
+ this.cursor_start = start
847
+ this.cursor_end = end
848
+ this.cursor_enabled = enabled
849
+ }
850
+ }
851
+
852
+ update_cursor(row: number, col: number): void {
853
+ if (row !== this.cursor_row || col !== this.cursor_col) {
854
+ if (row < this.text_mode_height) {
855
+ this.changed_rows![row] = 1
856
+ }
857
+ if (this.cursor_row < this.text_mode_height) {
858
+ this.changed_rows![this.cursor_row] = 1
859
+ }
860
+
861
+ this.cursor_row = row
862
+ this.cursor_col = col
863
+ }
864
+ }
865
+
866
+ text_update_row(row: number): void {
867
+ let offset = TEXT_BUF_COMPONENT_SIZE * row * this.text_mode_width
868
+
869
+ let blinking: number
870
+ let bg_color: number
871
+ let fg_color: number
872
+ let text: string
873
+
874
+ const row_element = this.text_screen.childNodes[row]
875
+ const fragment = document.createElement('div')
876
+
877
+ for (let i = 0; i < this.text_mode_width; ) {
878
+ const color_element = document.createElement('span')
879
+
880
+ blinking =
881
+ this.text_mode_data![offset + FLAGS_INDEX] & FLAG_BLINKING
882
+ bg_color = this.text_mode_data![offset + BG_COLOR_INDEX]
883
+ fg_color = this.text_mode_data![offset + FG_COLOR_INDEX]
884
+
885
+ if (blinking) {
886
+ color_element.classList.add('blink')
887
+ }
888
+
889
+ color_element.style.backgroundColor = this.number_as_color(bg_color)
890
+ color_element.style.color = this.number_as_color(fg_color)
891
+
892
+ text = ''
893
+
894
+ while (
895
+ i < this.text_mode_width &&
896
+ (this.text_mode_data![offset + FLAGS_INDEX] & FLAG_BLINKING) ===
897
+ blinking &&
898
+ this.text_mode_data![offset + BG_COLOR_INDEX] === bg_color &&
899
+ this.text_mode_data![offset + FG_COLOR_INDEX] === fg_color
900
+ ) {
901
+ const chr =
902
+ this.charmap[this.text_mode_data![offset + CHARACTER_INDEX]]
903
+
904
+ text += chr
905
+ dbg_assert(!!chr)
906
+
907
+ i++
908
+ offset += TEXT_BUF_COMPONENT_SIZE
909
+
910
+ if (row === this.cursor_row) {
911
+ if (i === this.cursor_col) {
912
+ break
913
+ } else if (i === this.cursor_col + 1) {
914
+ this.cursor_element.style.backgroundColor =
915
+ color_element.style.color
916
+ fragment.appendChild(this.cursor_element)
917
+ break
918
+ }
919
+ }
920
+ }
921
+
922
+ color_element.textContent = text
923
+ fragment.appendChild(color_element)
924
+ }
925
+
926
+ row_element.parentNode!.replaceChild(fragment, row_element)
927
+ }
928
+
929
+ update_buffer(layers: ScreenLayer[]): void {
930
+ if (DEBUG_SCREEN_LAYERS) {
931
+ this.graphic_context.strokeStyle = '#0F0'
932
+ this.graphic_context.lineWidth = 4
933
+ for (const layer of layers) {
934
+ this.graphic_context.strokeRect(
935
+ layer.buffer_x,
936
+ layer.buffer_y,
937
+ layer.buffer_width,
938
+ layer.buffer_height,
939
+ )
940
+ }
941
+ this.graphic_context.lineWidth = 1
942
+ return
943
+ }
944
+
945
+ for (const layer of layers) {
946
+ this.graphic_context.putImageData(
947
+ layer.image_data,
948
+ layer.screen_x - layer.buffer_x,
949
+ layer.screen_y - layer.buffer_y,
950
+ layer.buffer_x,
951
+ layer.buffer_y,
952
+ layer.buffer_width,
953
+ layer.buffer_height,
954
+ )
955
+ }
956
+ }
957
+
958
+ get_text_screen(): string[] {
959
+ const screen: string[] = []
960
+
961
+ for (let i = 0; i < this.text_mode_height; i++) {
962
+ screen.push(this.get_text_row(i))
963
+ }
964
+
965
+ return screen
966
+ }
967
+
968
+ get_text_row(y: number): string {
969
+ const begin =
970
+ y * this.text_mode_width * TEXT_BUF_COMPONENT_SIZE + CHARACTER_INDEX
971
+ const end = begin + this.text_mode_width * TEXT_BUF_COMPONENT_SIZE
972
+ let row = ''
973
+ for (let i = begin; i < end; i += TEXT_BUF_COMPONENT_SIZE) {
974
+ row += this.charmap[this.text_mode_data![i]]
975
+ }
976
+ return row
977
+ }
978
+ }