blockyard 0.0.9 → 0.1.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 (58) hide show
  1. package/CHANGELOG.md +251 -1
  2. package/README.md +42 -23
  3. package/bin/blockyard.js +2 -1
  4. package/docs/API.md +16 -14
  5. package/docs/ARCHITECTURE.md +92 -5
  6. package/docs/CONFIGURATION.md +33 -26
  7. package/docs/GETTING-STARTED.md +5 -2
  8. package/docs/INSTALL.md +90 -33
  9. package/docs/MEASUREMENTS.md +147 -0
  10. package/docs/SECURITY.md +32 -15
  11. package/docs/TROUBLESHOOTING.md +35 -1
  12. package/docs/USER-GUIDE.md +266 -26
  13. package/package.json +1 -1
  14. package/public/404.html +1 -1
  15. package/public/css/app.css +306 -82
  16. package/public/donate-qr.png +0 -0
  17. package/public/index.html +295 -103
  18. package/public/js/agents.js +228 -51
  19. package/public/js/app.js +82 -8
  20. package/public/js/blockscene3d.js +179 -27
  21. package/public/js/charts.js +21 -21
  22. package/public/js/depthchart.js +31 -27
  23. package/public/js/details3d.js +1456 -71
  24. package/public/js/doom.js +31 -0
  25. package/public/js/dosaudio.js +48 -0
  26. package/public/js/dosgame.js +389 -0
  27. package/public/js/dosio.js +186 -0
  28. package/public/js/dospc.js +1353 -0
  29. package/public/js/dosworker.js +196 -0
  30. package/public/js/login.js +5 -0
  31. package/public/js/markets.js +46 -8
  32. package/public/js/mining.js +310 -32
  33. package/public/js/panels.js +14 -10
  34. package/public/js/pricechart.js +14 -13
  35. package/public/js/quake.js +20 -0
  36. package/public/js/settings.js +103 -21
  37. package/public/js/soundcard.js +459 -0
  38. package/public/js/theme.js +235 -0
  39. package/public/js/wolf3d.js +22 -0
  40. package/public/js/x86.js +1978 -0
  41. package/scripts/donate-qr.py +12 -9
  42. package/scripts/dos-bench.js +56 -0
  43. package/scripts/setup.js +34 -12
  44. package/scripts/shots.mjs +6 -0
  45. package/scripts/smoke.sh +1 -1
  46. package/scripts/tls.js +31 -0
  47. package/server/chain/index/build.js +21 -4
  48. package/server/collect/monitor.js +30 -1
  49. package/server/collect/network.js +295 -0
  50. package/server/config.js +46 -22
  51. package/server/http/api.js +49 -5
  52. package/server/http/games.js +77 -0
  53. package/server/http/server.js +8 -0
  54. package/server/main.js +53 -8
  55. package/server/tls/selfsigned.js +160 -0
  56. package/systemd/blockyard.service +7 -5
  57. package/docs/PRIVATE-LEADERBOARD.md +0 -230
  58. package/docs/STATE-2026-09-09.md +0 -200
@@ -0,0 +1,1353 @@
1
+ // DOSPC: the PC the DOOM Diversion's DOOM.EXE thinks it is running on (operator, 2026-09-15: "Get
2
+ // DOOM working as a diversion inside blockyard with zero dependancies").
3
+ //
4
+ // Our own implementation. The CPU is x86.js; everything else a DOS/4GW program touches is here,
5
+ // emulated at the level the program sees it rather than as the silicon underneath:
6
+ //
7
+ // - the LE executable inside the bound DOS/4GW stub, loaded above 1 MB with its fixups applied
8
+ // (the extender itself -- its real-mode loader and protected-mode kernel -- never runs: this
9
+ // file IS the extender, answering INT 21h, INT 31h and the hardware interrupts it reflects)
10
+ // - DOS: files from an in-memory, case-insensitive directory; console output into text VRAM
11
+ // - DPMI: descriptors, memory blocks, protected-mode interrupt vectors
12
+ // - the BIOS calls a game makes: video mode, cursor, keyboard, mouse
13
+ // - hardware: the 8259 PICs, the 8254 timer, the keyboard controller, the VGA (planar memory,
14
+ // unchained mode, CRTC page flipping, the DAC), the PC speaker gate, and -- through `sound` --
15
+ // a Sound Blaster and an OPL
16
+ //
17
+ // Nothing in here knows it is DOOM: every behaviour is what the hardware or the DOS call does.
18
+ import { createCpu, EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI, ES, CS, SS, DS, FS, GS } from './x86.js';
19
+
20
+ const MB = 1 << 20;
21
+ export const MEM_SIZE = 32 * MB; // 32 MB, a power of two: the CPU masks every address with it
22
+ const LOAD_DELTA = 0x100000; // the LE's objects move up a megabyte, clear of conventional memory
23
+ const HEAP_BASE = 0x300000; // DPMI memory blocks from here to the top
24
+ const PSP_SEG = 0x0100; // real-mode segments in conventional memory
25
+ const ENV_SEG = 0x0110;
26
+ const DOS_HEAP_SEG = 0x0200; // DOS memory (INT 21h 48h / DPMI 0100) from here to 0x9F00
27
+ const ROM_STUBS = 0xf0000; // the "BIOS" handlers a chained vector returns to
28
+
29
+ // selectors: flat code and data at 0, then the ones the start-up hands the program
30
+ const SEL_CODE = 0x08, SEL_DATA = 0x10, SEL_PSP = 0x18, SEL_ENV = 0x20, SEL_CODE16 = 0x28, SEL_LOL = 0x30, SEL_FIRST_FREE = 0x38;
31
+
32
+ const PIT_HZ = 1193182;
33
+
34
+ // ------------------------------------------------------------------ the LE loader
35
+ /**
36
+ * Find the LE image in a bound DOS/4GW executable, copy its pages into `mem` at `delta` above the
37
+ * addresses it was linked for, and apply its fixups. Returns the objects, entry point and stack.
38
+ */
39
+ /** Whether a file carries an LE image behind its MZ stub (a DOS/4GW program). */
40
+ export function hasLE(buf) {
41
+ const dv = new DataView(buf.buffer, buf.byteOffset, buf.length);
42
+ for (let i = 0; i + 0x40 < buf.length; i++) {
43
+ if (buf[i] !== 0x4d || buf[i + 1] !== 0x5a) continue;
44
+ const l = dv.getUint32(i + 0x3c, true);
45
+ if (i + l + 2 < buf.length && buf[i + l] === 0x4c && buf[i + l + 1] === 0x45) return true;
46
+ }
47
+ return false;
48
+ }
49
+
50
+ export function loadLE(buf, mem, delta = LOAD_DELTA) {
51
+ const dv = new DataView(buf.buffer, buf.byteOffset, buf.length);
52
+ let stub = -1;
53
+ for (let i = 0; i + 0x40 < buf.length; i++) {
54
+ if (buf[i] !== 0x4d || buf[i + 1] !== 0x5a) continue;
55
+ const l = dv.getUint32(i + 0x3c, true);
56
+ if (i + l + 2 < buf.length && buf[i + l] === 0x4c && buf[i + l + 1] === 0x45) { stub = i; break; }
57
+ }
58
+ if (stub < 0) throw new Error('no LE executable in this file');
59
+ const le = stub + dv.getUint32(stub + 0x3c, true);
60
+ const u32 = (o) => dv.getUint32(le + o, true);
61
+ const pageSize = u32(0x28), nPages = u32(0x14), lastPage = u32(0x2c);
62
+ const objs = [];
63
+ for (let i = 0; i < u32(0x44); i++) {
64
+ const o = le + u32(0x40) + i * 24;
65
+ objs.push({
66
+ size: dv.getUint32(o, true), base: dv.getUint32(o + 4, true) + delta, flags: dv.getUint32(o + 8, true),
67
+ pageIdx: dv.getUint32(o + 12, true), nPages: dv.getUint32(o + 16, true),
68
+ });
69
+ }
70
+ const dataPages = stub + u32(0x80);
71
+ const opt = le + u32(0x48), fpt = le + u32(0x68), frt = le + u32(0x6c);
72
+ const pageAddr = [];
73
+ for (const ob of objs) {
74
+ for (let p = 0; p < ob.nPages; p++) {
75
+ const e = opt + (ob.pageIdx - 1 + p) * 4;
76
+ const num = (buf[e] << 16) | (buf[e + 1] << 8) | buf[e + 2];
77
+ const src = dataPages + (num - 1) * pageSize;
78
+ const len = num === nPages ? lastPage : pageSize;
79
+ mem.set(buf.subarray(src, src + len), ob.base + p * pageSize);
80
+ pageAddr[num] = ob.base + p * pageSize;
81
+ }
82
+ }
83
+ const w32 = (a, v) => { mem[a] = v; mem[a + 1] = v >> 8; mem[a + 2] = v >> 16; mem[a + 3] = v >>> 24; };
84
+ let fixups = 0;
85
+ for (let p = 1; p <= nPages; p++) {
86
+ let r = frt + dv.getUint32(fpt + (p - 1) * 4, true);
87
+ const end = frt + dv.getUint32(fpt + p * 4, true);
88
+ while (r < end) {
89
+ const st = buf[r], fl = buf[r + 1]; r += 2;
90
+ let list = null, single = 0;
91
+ if (st & 0x20) list = buf[r++]; else { single = dv.getInt16(r, true); r += 2; }
92
+ if ((fl & 3) !== 0) throw new Error('imported fixups are not supported');
93
+ let obj;
94
+ if (fl & 0x40) { obj = dv.getUint16(r, true); r += 2; } else obj = buf[r++];
95
+ let toff = 0;
96
+ if ((st & 0xf) !== 2) { if (fl & 0x10) { toff = dv.getUint32(r, true); r += 4; } else { toff = dv.getUint16(r, true); r += 2; } }
97
+ const srcs = [];
98
+ if (list !== null) for (let k = 0; k < list; k++) { srcs.push(dv.getInt16(r, true)); r += 2; } else srcs.push(single);
99
+ const target = objs[obj - 1].base + toff;
100
+ for (const so of srcs) {
101
+ const a = pageAddr[p] + so;
102
+ switch (st & 0xf) {
103
+ case 7: w32(a, target); break;
104
+ case 8: w32(a, target - (a + 4)); break;
105
+ case 2: mem[a] = objSelector(objs[obj - 1]); mem[a + 1] = 0; break;
106
+ case 5: mem[a] = target; mem[a + 1] = target >> 8; break;
107
+ case 6: w32(a, target); mem[a + 4] = objSelector(objs[obj - 1]); mem[a + 5] = 0; break;
108
+ default: throw new Error(`LE fixup type ${st}`);
109
+ }
110
+ fixups++;
111
+ }
112
+ }
113
+ }
114
+ return {
115
+ objs,
116
+ entry: objs[u32(0x18) - 1].base + u32(0x1c),
117
+ esp: objs[u32(0x20) - 1].base + u32(0x24),
118
+ fixups,
119
+ };
120
+ }
121
+ // ------------------------------------------------------------------ the COFF loader
122
+ /**
123
+ * A DJGPP v2 program: a go32 stub (an MZ executable that finds a DPMI host and loads the rest), then
124
+ * a COFF image. Returns the image's sections, entry point and the size its memory block must have,
125
+ * or null when `buf` is not one. Nothing is copied: DJGPP programs are position-independent of their
126
+ * block (every address is an offset from a selector based at it), so the caller loads them there.
127
+ */
128
+ export function parseCoff(buf) {
129
+ if (buf.length < 0x40 || buf[0] !== 0x4d || buf[1] !== 0x5a) return null;
130
+ const dv = new DataView(buf.buffer, buf.byteOffset, buf.length);
131
+ const lastPage = dv.getUint16(2, true), pages = dv.getUint16(4, true);
132
+ const coff = lastPage ? (pages - 1) * 512 + lastPage : pages * 512;
133
+ if (coff + 20 > buf.length || dv.getUint16(coff, true) !== 0x14c) return null;
134
+ const nsec = dv.getUint16(coff + 2, true), optSize = dv.getUint16(coff + 16, true);
135
+ if (optSize < 28) return null;
136
+ const entry = dv.getUint32(coff + 20 + 16, true);
137
+ const sections = [];
138
+ let end = 0;
139
+ for (let i = 0; i < nsec; i++) {
140
+ const h = coff + 20 + optSize + i * 40;
141
+ const name = String.fromCharCode(...buf.subarray(h, h + 8)).replace(/\0.*$/, '');
142
+ const vaddr = dv.getUint32(h + 12, true), size = dv.getUint32(h + 16, true), fileOff = dv.getUint32(h + 20, true), flags = dv.getUint32(h + 36, true);
143
+ sections.push({ name, vaddr, size, fileOff: coff + fileOff, bss: (flags & 0x80) !== 0 });
144
+ end = Math.max(end, vaddr + size);
145
+ }
146
+ // the stub's own parameters live in its image: "go32stub" then the size, stack and transfer buffer
147
+ const at = String.fromCharCode(...buf.subarray(0, Math.min(coff, 0x800))).indexOf('go32stub');
148
+ const minstack = at >= 0 ? dv.getUint32(at + 0x14, true) : 0x40000;
149
+ const minkeep = at >= 0 ? dv.getUint16(at + 0x20, true) : 0x4000;
150
+ return { coff, entry, sections, size: (end + 0xfff) & ~0xfff, minstack, minkeep: minkeep || 0x4000 };
151
+ }
152
+
153
+ function objSelector(ob) {
154
+ if (ob.flags & 0x2000) return ob.flags & 0x4 ? SEL_CODE : SEL_DATA; // 32-bit objects are flat
155
+ return SEL_CODE16;
156
+ }
157
+
158
+ // ------------------------------------------------------------------ a tiny PNG-free framebuffer
159
+ /** The DAC's 6-bit colours as 8-bit, the way a VGA's 18-bit palette reaches a screen. */
160
+ const DAC8 = new Uint8Array(64);
161
+ for (let i = 0; i < 64; i++) DAC8[i] = Math.round((i * 255) / 63);
162
+
163
+ // ------------------------------------------------------------------ the PC
164
+ /**
165
+ * A PC with DOOM's world around it.
166
+ * files: { NAME: Uint8Array } -- the working directory (names are case-insensitive)
167
+ * args: the command tail, e.g. '-nosound'
168
+ * now(): milliseconds, the clock the timer chip counts against (performance.now in a browser)
169
+ * onWrite(name, bytes): a file was written and closed (savegames, the config)
170
+ * onExit(code): the program ended
171
+ * sound: (mem) => a card with portIn/portOut/tick (soundcard.js), or null for no sound hardware
172
+ */
173
+ export function createPC({ files = {}, args = '', now = () => 0, onWrite = null, onExit = null, sound = null, log = null, programName = 'GAME.EXE' } = {}) {
174
+ const mem = new Uint8Array(MEM_SIZE);
175
+ if (typeof sound === 'function') sound = sound(mem); // a card built on this machine's memory
176
+ const selectors = new Map([[0, 0], [SEL_CODE, 0], [SEL_DATA, 0], [SEL_PSP, PSP_SEG * 16], [SEL_ENV, ENV_SEG * 16], [SEL_LOL, 0x500]]);
177
+ // where the running program's PSP is: 0100h for DOS/4GW, just below the transfer buffer for DJGPP
178
+ let pspSeg = PSP_SEG;
179
+ let realMode = 0; // inside a DPMI 0300 call: DOS answers with segments
180
+ let nextSelector = SEL_FIRST_FREE;
181
+ const seg16 = new Set(); // selectors whose descriptor is 16-bit (D bit clear)
182
+
183
+ // ---------------------------------------------------------------- VGA
184
+ const vga = {
185
+ planes: [new Uint8Array(0x10000), new Uint8Array(0x10000), new Uint8Array(0x10000), new Uint8Array(0x10000)],
186
+ mode: 3,
187
+ seqIndex: 0, seq: new Uint8Array(8),
188
+ gcIndex: 0, gc: new Uint8Array(16),
189
+ crtcIndex: 0, crtc: new Uint8Array(32),
190
+ attrFlip: false, attrIndex: 0, attr: new Uint8Array(32),
191
+ dacWrite: 0, dacRead: 0, dacSub: 0, dacReadSub: 0,
192
+ pal: new Uint8Array(768),
193
+ misc: 0x63,
194
+ frames: 0, // bumped on every CRTC start change: a page was flipped
195
+ palSeq: 0, // bumped on every DAC write
196
+ writes: 0, // bumped on every write into the graphics window
197
+ latch: new Uint8Array(4), // the byte of each plane the last read left (write mode 1)
198
+ };
199
+ vga.seq[2] = 0x0f; vga.seq[4] = 0x0e;
200
+ const chain4 = () => (vga.seq[4] & 0x08) !== 0;
201
+ function vgaWrite(a, v) {
202
+ const off = a - 0xa0000;
203
+ // the text buffer at B8000 is plain memory in either mode; the graphics window is planes
204
+ if (vga.mode !== 0x13 || off >= 0x10000) { if (a >= 0xb8000) mem[a] = v; return; }
205
+ if (chain4()) { vga.planes[off & 3][off >> 2] = v; vga.writes++; return; }
206
+ vga.writes++;
207
+ const mask = vga.seq[2];
208
+ // WRITE MODE 1 copies the bytes the last read latched from every plane, whatever is written:
209
+ // Wolfenstein 3D copies between its pages with it (a read from one, a write to the other)
210
+ if ((vga.gc[5] & 3) === 1) {
211
+ for (let pl = 0; pl < 4; pl++) if (mask & (1 << pl)) vga.planes[pl][off] = vga.latch[pl];
212
+ return;
213
+ }
214
+ if (mask & 1) vga.planes[0][off] = v;
215
+ if (mask & 2) vga.planes[1][off] = v;
216
+ if (mask & 4) vga.planes[2][off] = v;
217
+ if (mask & 8) vga.planes[3][off] = v;
218
+ }
219
+ function vgaRead(a) {
220
+ if (vga.mode !== 0x13) return mem[a];
221
+ const off = a - 0xa0000;
222
+ if (off >= 0x10000) return 0;
223
+ if (chain4()) return vga.planes[off & 3][off >> 2];
224
+ for (let pl = 0; pl < 4; pl++) vga.latch[pl] = vga.planes[pl][off];
225
+ return vga.planes[vga.gc[4] & 3][off];
226
+ }
227
+ function setVideoMode(m) {
228
+ vga.mode = m & 0x7f;
229
+ vga.palSeq++;
230
+ // the BIOS keeps the mode in its data area, and programs read it back from there: DOOM's
231
+ // shutdown only returns to text mode (and so only shows ENDOOM) if 0x449 says 13h
232
+ mem[0x449] = vga.mode === 0x13 ? 0x13 : 3;
233
+ if (vga.mode === 0x13) {
234
+ vga.seq[2] = 0x0f; vga.seq[4] = 0x0e; vga.crtc[0x13] = 0x28; vga.crtc[0x14] = 0x40; vga.crtc[0x17] = 0xa3;
235
+ vga.crtc[0x0c] = 0; vga.crtc[0x0d] = 0;
236
+ if (!(m & 0x80)) for (const p of vga.planes) p.fill(0);
237
+ defaultPalette();
238
+ } else {
239
+ vga.mode = 3;
240
+ if (!(m & 0x80)) for (let i = 0; i < 4000; i += 2) { mem[0xb8000 + i] = 0x20; mem[0xb8001 + i] = 0x07; }
241
+ text.x = 0; text.y = 0;
242
+ }
243
+ }
244
+ function defaultPalette() {
245
+ // the first sixteen are the EGA colours; the rest a grey ramp is enough before a game sets its own
246
+ const ega = [0, 0, 0, 0, 0, 42, 0, 42, 0, 0, 42, 42, 42, 0, 0, 42, 0, 42, 42, 21, 0, 42, 42, 42,
247
+ 21, 21, 21, 21, 21, 63, 21, 63, 21, 21, 63, 63, 63, 21, 21, 63, 21, 63, 63, 63, 21, 63, 63, 63];
248
+ for (let i = 0; i < 256; i++) {
249
+ if (i < 16) { vga.pal[i * 3] = ega[i * 3]; vga.pal[i * 3 + 1] = ega[i * 3 + 1]; vga.pal[i * 3 + 2] = ega[i * 3 + 2]; }
250
+ else { const g = (i * 63 / 255) | 0; vga.pal[i * 3] = g; vga.pal[i * 3 + 1] = g; vga.pal[i * 3 + 2] = g; }
251
+ }
252
+ }
253
+ /**
254
+ * The picture on the monitor in mode 13h: 320x200 palette indices into `out`, read the way the
255
+ * CRTC scans them -- from the start address, through whichever planes the memory mode says.
256
+ */
257
+ function renderIndexed(out) {
258
+ const start = (vga.crtc[0x0c] << 8) | vga.crtc[0x0d];
259
+ const P = vga.planes;
260
+ if (chain4()) {
261
+ const base = start * 4;
262
+ for (let i = 0; i < 64000; i++) { const off = base + i; out[i] = P[off & 3][(off >> 2) & 0xffff]; }
263
+ return out;
264
+ }
265
+ const perLine = (vga.crtc[0x13] * 2) || 80;
266
+ let o = 0;
267
+ for (let y = 0; y < 200; y++) {
268
+ const row = start + y * perLine;
269
+ for (let x = 0; x < 320; x++) out[o++] = P[x & 3][(row + (x >> 2)) & 0xffff];
270
+ }
271
+ return out;
272
+ }
273
+ /** The same picture as RGBA through the DAC (tests and screenshots; the page uses the indices). */
274
+ const indexScratch = new Uint8Array(64000);
275
+ function renderGraphics(out) {
276
+ renderIndexed(indexScratch);
277
+ const pal = vga.pal;
278
+ for (let i = 0, o = 0; i < 64000; i++) {
279
+ const c = indexScratch[i] * 3;
280
+ out[o++] = DAC8[pal[c]]; out[o++] = DAC8[pal[c + 1]]; out[o++] = DAC8[pal[c + 2]]; out[o++] = 255;
281
+ }
282
+ return out;
283
+ }
284
+
285
+ // ---------------------------------------------------------------- text console
286
+ const text = { x: 0, y: 0, attr: 0x07 };
287
+ function scrollText() {
288
+ mem.copyWithin(0xb8000, 0xb8000 + 160, 0xb8000 + 4000);
289
+ for (let i = 0; i < 160; i += 2) { mem[0xb8000 + 3840 + i] = 0x20; mem[0xb8000 + 3841 + i] = 0x07; }
290
+ }
291
+ function putChar(ch, attr = null) {
292
+ stdout.push(ch);
293
+ if (ch === 13) { text.x = 0; return; }
294
+ if (ch === 10) { text.y++; if (text.y >= 25) { scrollText(); text.y = 24; } return; }
295
+ if (ch === 8) { if (text.x > 0) text.x--; return; }
296
+ if (ch === 7) return;
297
+ const a = 0xb8000 + (text.y * 80 + text.x) * 2;
298
+ mem[a] = ch;
299
+ if (attr !== null) mem[a + 1] = attr;
300
+ text.x++;
301
+ if (text.x >= 80) { text.x = 0; text.y++; if (text.y >= 25) { scrollText(); text.y = 24; } }
302
+ }
303
+ const stdout = [];
304
+
305
+ // ---------------------------------------------------------------- PIC, PIT, keyboard, CMOS
306
+ const pic = { mask: [0xb8, 0xff], isr: [0, 0], irr: [0, 0], readIsr: [false, false], init: [0, 0] };
307
+ const pit = {
308
+ reload: [65536, 65536, 65536], mode: [3, 3, 3], access: [3, 3, 3], lowNext: [true, true, true],
309
+ latch: [-1, -1, -1], readLow: [true, true, true], start: [0, 0, 0], nextIrq: 0, gate2: 0, tickBase: 0,
310
+ };
311
+ const kbd = { queue: [], data: 0, port61: 0 };
312
+ function pitPeriodMs() { return (pit.reload[0] * 1000) / PIT_HZ; }
313
+ function pitCount(ch, t) {
314
+ const periodTicks = pit.reload[ch];
315
+ const elapsed = ((t - pit.start[ch]) * PIT_HZ) / 1000;
316
+ const c = periodTicks - (Math.floor(elapsed) % periodTicks);
317
+ return c & 0xffff;
318
+ }
319
+ function raiseIrq(n) { pic.irr[n >> 3] |= 1 << (n & 7); }
320
+
321
+ function portIn(port, size) {
322
+ if (size === 2) return portIn(port, 1) | (portIn(port + 1, 1) << 8) | (portIn(port + 2, 1) << 16) | (portIn(port + 3, 1) << 24);
323
+ if (size === 1) return portIn(port, 0) | (portIn(port + 1, 0) << 8);
324
+ switch (port) {
325
+ case 0x20: return pic.readIsr[0] ? pic.isr[0] : pic.irr[0];
326
+ case 0x21: return pic.mask[0];
327
+ case 0xa0: return pic.readIsr[1] ? pic.isr[1] : pic.irr[1];
328
+ case 0xa1: return pic.mask[1];
329
+ case 0x40: case 0x41: case 0x42: {
330
+ const ch = port - 0x40;
331
+ const v = pit.latch[ch] >= 0 ? pit.latch[ch] : pitCount(ch, now());
332
+ const acc = pit.access[ch];
333
+ if (acc === 1) { pit.latch[ch] = -1; return v & 0xff; }
334
+ if (acc === 2) { pit.latch[ch] = -1; return (v >> 8) & 0xff; }
335
+ if (pit.readLow[ch]) { pit.readLow[ch] = false; if (pit.latch[ch] < 0) pit.latch[ch] = v; return v & 0xff; }
336
+ pit.readLow[ch] = true; pit.latch[ch] = -1; return (v >> 8) & 0xff;
337
+ }
338
+ case 0x60: {
339
+ const v = kbd.data;
340
+ return v;
341
+ }
342
+ case 0x61: return (kbd.port61 & 0x0f) | ((now() * 0.066 | 0) & 1 ? 0x10 : 0) | 0x20;
343
+ case 0x64: return kbd.queue.length ? 0x1d : 0x1c;
344
+ case 0x71: return 0;
345
+ case 0x3c1: return vga.attr[vga.attrIndex & 0x1f];
346
+ case 0x3c4: return vga.seqIndex;
347
+ case 0x3c5: return vga.seq[vga.seqIndex & 7];
348
+ case 0x3c7: return 3;
349
+ case 0x3c8: return vga.dacWrite;
350
+ case 0x3c9: {
351
+ const v = vga.pal[vga.dacRead * 3 + vga.dacReadSub];
352
+ if (++vga.dacReadSub === 3) { vga.dacReadSub = 0; vga.dacRead = (vga.dacRead + 1) & 0xff; }
353
+ return v;
354
+ }
355
+ case 0x3cc: return vga.misc;
356
+ case 0x3ce: return vga.gcIndex;
357
+ case 0x3cf: return vga.gc[vga.gcIndex & 0xf];
358
+ case 0x3d4: return vga.crtcIndex;
359
+ case 0x3d5: return vga.crtc[vga.crtcIndex & 0x1f];
360
+ case 0x3da: {
361
+ vga.attrFlip = false;
362
+ // retrace for 1.2 ms of every 14.3 (70 Hz), display-enable toggling fast inside it
363
+ const t = now() % (1000 / 70);
364
+ return (t < 1.2 ? 0x08 : 0) | ((t * 31) & 1);
365
+ }
366
+ case 0x201: return 0xff; // no joystick: every axis timed out
367
+ }
368
+ if (sound) { const v = sound.portIn?.(port); if (v !== undefined) return v; }
369
+ return 0xff;
370
+ }
371
+
372
+ function portOut(port, size, v) {
373
+ if (size === 2) { portOut(port, 0, v & 0xff); portOut(port + 1, 0, (v >> 8) & 0xff); portOut(port + 2, 0, (v >> 16) & 0xff); portOut(port + 3, 0, (v >>> 24) & 0xff); return; }
374
+ if (size === 1) {
375
+ // a word to an index port is index then data -- the idiom every VGA program uses
376
+ if (port === 0x3c4 || port === 0x3ce || port === 0x3d4) { portOut(port, 0, v & 0xff); portOut(port + 1, 0, (v >> 8) & 0xff); return; }
377
+ portOut(port, 0, v & 0xff); portOut(port + 1, 0, (v >> 8) & 0xff); return;
378
+ }
379
+ v &= 0xff;
380
+ switch (port) {
381
+ case 0x20: case 0xa0: {
382
+ const i = port === 0x20 ? 0 : 1;
383
+ if (v & 0x10) { pic.init[i] = 1; pic.mask[i] = 0; pic.isr[i] = 0; return; }
384
+ if ((v & 0x18) === 0x08) { if ((v & 3) === 2) pic.readIsr[i] = false; if ((v & 3) === 3) pic.readIsr[i] = true; return; }
385
+ if ((v & 0xe0) === 0x20) { // non-specific EOI: the highest in service
386
+ const s = pic.isr[i];
387
+ if (s) pic.isr[i] = s & ~(s & -s);
388
+ } else if ((v & 0xe0) === 0x60) pic.isr[i] &= ~(1 << (v & 7));
389
+ return;
390
+ }
391
+ case 0x21: case 0xa1: {
392
+ const i = port === 0x21 ? 0 : 1;
393
+ if (pic.init[i]) { pic.init[i] = pic.init[i] === 3 ? 0 : pic.init[i] + 1; return; }
394
+ pic.mask[i] = v;
395
+ return;
396
+ }
397
+ case 0x40: case 0x41: case 0x42: {
398
+ const ch = port - 0x40;
399
+ const acc = pit.access[ch];
400
+ let r = pit.reload[ch] & 0xffff;
401
+ if (acc === 1) r = (r & 0xff00) | v;
402
+ else if (acc === 2) r = (r & 0xff) | (v << 8);
403
+ else if (pit.lowNext[ch]) { pit.lowNext[ch] = false; pit.pending = v; return; }
404
+ else { pit.lowNext[ch] = true; r = pit.pending | (v << 8); }
405
+ pit.reload[ch] = r || 65536;
406
+ if (ch === 0) pit.tickBase += Math.floor(((now() - pit.start[0]) * PIT_HZ) / 1000 / 65536);
407
+ pit.start[ch] = now();
408
+ if (ch === 0) pit.nextIrq = now() + pitPeriodMs();
409
+ if (ch === 2) sound?.speaker?.(pit.reload[2], kbd.port61);
410
+ return;
411
+ }
412
+ case 0x43: {
413
+ const ch = (v >> 6) & 3;
414
+ if (ch === 3) return;
415
+ const acc = (v >> 4) & 3;
416
+ if (acc === 0) { pit.latch[ch] = pitCount(ch, now()); pit.readLow[ch] = true; return; }
417
+ pit.access[ch] = acc; pit.mode[ch] = (v >> 1) & 7; pit.lowNext[ch] = true; pit.readLow[ch] = true;
418
+ return;
419
+ }
420
+ case 0x61: kbd.port61 = v; sound?.speaker?.(pit.reload[2], v); return;
421
+ case 0x3c0:
422
+ if (!vga.attrFlip) vga.attrIndex = v; else vga.attr[vga.attrIndex & 0x1f] = v;
423
+ vga.attrFlip = !vga.attrFlip;
424
+ return;
425
+ case 0x3c2: vga.misc = v; return;
426
+ case 0x3c4: vga.seqIndex = v; return;
427
+ case 0x3c5: vga.seq[vga.seqIndex & 7] = v; return;
428
+ case 0x3c7: vga.dacRead = v; vga.dacReadSub = 0; return;
429
+ case 0x3c8: vga.dacWrite = v; vga.dacSub = 0; return;
430
+ case 0x3c9:
431
+ vga.pal[vga.dacWrite * 3 + vga.dacSub] = v & 0x3f;
432
+ vga.palSeq++;
433
+ if (++vga.dacSub === 3) { vga.dacSub = 0; vga.dacWrite = (vga.dacWrite + 1) & 0xff; }
434
+ return;
435
+ case 0x3ce: vga.gcIndex = v; return;
436
+ case 0x3cf: vga.gc[vga.gcIndex & 0xf] = v; return;
437
+ case 0x3d4: vga.crtcIndex = v; return;
438
+ case 0x3d5:
439
+ vga.crtc[vga.crtcIndex & 0x1f] = v;
440
+ if (vga.crtcIndex === 0x0c || vga.crtcIndex === 0x0d) vga.frames++;
441
+ return;
442
+ }
443
+ sound?.portOut?.(port, v);
444
+ }
445
+
446
+ // ---------------------------------------------------------------- memory allocation
447
+ // DPMI blocks: first fit over a sorted list, from HEAP_BASE to the top of memory
448
+ const blocks = []; // { base, size, handle }
449
+ let nextHandle = 1;
450
+ function allocBlock(size) {
451
+ size = (size + 0xfff) & ~0xfff;
452
+ let at = HEAP_BASE;
453
+ for (let i = 0; i <= blocks.length; i++) {
454
+ const limit = i < blocks.length ? blocks[i].base : MEM_SIZE;
455
+ if (limit - at >= size) {
456
+ const b = { base: at, size, handle: nextHandle++ };
457
+ blocks.splice(i, 0, b);
458
+ mem.fill(0, at, at + size);
459
+ cpu.invalidate(at, size);
460
+ return b;
461
+ }
462
+ if (i < blocks.length) at = blocks[i].base + blocks[i].size;
463
+ }
464
+ return null;
465
+ }
466
+ function freeMemory() {
467
+ let free = 0, largest = 0, at = HEAP_BASE;
468
+ for (let i = 0; i <= blocks.length; i++) {
469
+ const limit = i < blocks.length ? blocks[i].base : MEM_SIZE;
470
+ free += limit - at; largest = Math.max(largest, limit - at);
471
+ if (i < blocks.length) at = blocks[i].base + blocks[i].size;
472
+ }
473
+ return { free, largest };
474
+ }
475
+ // DOS CONVENTIONAL MEMORY: blocks of paragraphs, first fit, that can be freed and resized in
476
+ // place. A bump pointer did for the extenders, which ask once; a real-mode Borland program is
477
+ // handed all of memory at start, gives most back with AH=4Ah, and grows its heap by resizing its
478
+ // own block again.
479
+ const DOS_TOP = 0x9f00;
480
+ const dosBlocks = []; // { seg, paras }, sorted by segment
481
+ function dosFreeAfter(i) {
482
+ const end = i + 1 < dosBlocks.length ? dosBlocks[i + 1].seg : DOS_TOP;
483
+ return end - (dosBlocks[i].seg + dosBlocks[i].paras);
484
+ }
485
+ function dosLargest() {
486
+ let at = DOS_HEAP_SEG, best = 0;
487
+ for (const b of dosBlocks) { best = Math.max(best, b.seg - at); at = b.seg + b.paras; }
488
+ return Math.max(best, DOS_TOP - at);
489
+ }
490
+ function dosAlloc(paras) {
491
+ let at = DOS_HEAP_SEG;
492
+ for (let i = 0; i <= dosBlocks.length; i++) {
493
+ const limit = i < dosBlocks.length ? dosBlocks[i].seg : DOS_TOP;
494
+ if (limit - at >= paras) { dosBlocks.splice(i, 0, { seg: at, paras }); return at; }
495
+ if (i < dosBlocks.length) at = dosBlocks[i].seg + dosBlocks[i].paras;
496
+ }
497
+ return -1;
498
+ }
499
+ function dosFree(seg) {
500
+ const i = dosBlocks.findIndex((b) => b.seg === seg);
501
+ if (i < 0) return false;
502
+ dosBlocks.splice(i, 1);
503
+ return true;
504
+ }
505
+ /** Resize the block at `seg` in place: true, or the largest size it could have. */
506
+ function dosResize(seg, paras) {
507
+ const i = dosBlocks.findIndex((b) => b.seg === seg);
508
+ if (i < 0) return -1;
509
+ const most = dosBlocks[i].paras + dosFreeAfter(i);
510
+ if (paras > most) return most;
511
+ dosBlocks[i].paras = paras;
512
+ return true;
513
+ }
514
+
515
+ // ---------------------------------------------------------------- files
516
+ // PATHS, NOT JUST NAMES: the drive is the program's directory, and a file is its upper-case path
517
+ // under it ("ID1/PAK0.PAK"). DOOM keeps everything beside its executable; Quake keeps its data a
518
+ // directory down and makes more of them.
519
+ const dir = new Map(); // PATH -> Uint8Array
520
+ const dirs = new Set(); // directories made by the program, beside those files imply
521
+ for (const [k, v] of Object.entries(files)) dir.set(normPath(k), v);
522
+ const handles = new Map(); // n -> { name, data, pos, dirty, size }
523
+ function normPath(raw) {
524
+ const parts = [];
525
+ for (const p of String(raw).replace(/^[a-zA-Z]:/, '').replace(/\\/g, '/').split('/')) {
526
+ if (!p || p === '.') continue;
527
+ if (p === '..') parts.pop(); else parts.push(p.toUpperCase());
528
+ }
529
+ return parts.join('/');
530
+ }
531
+ function dosName(a) {
532
+ let s = '';
533
+ for (let i = 0; i < 128; i++) { const c = mem[a + i]; if (!c) break; s += String.fromCharCode(c); }
534
+ return normPath(s);
535
+ }
536
+ const parentOf = (path) => (path.includes('/') ? path.slice(0, path.lastIndexOf('/')) : '');
537
+ function isDir(path) {
538
+ if (path === '' || dirs.has(path)) return true;
539
+ for (const k of dir.keys()) if (k.startsWith(`${path}/`)) return true;
540
+ return false;
541
+ }
542
+ function openHandle(name, data) {
543
+ let h = 5;
544
+ while (handles.has(h)) h++;
545
+ if (h >= SFT_ENTRIES) return -1;
546
+ handles.set(h, { name, data, size: data.length, pos: 0, dirty: false });
547
+ return h;
548
+ }
549
+ function closeHandle(h) {
550
+ const f = handles.get(h);
551
+ if (!f) return false;
552
+ if (f.dirty) {
553
+ const bytes = f.data.slice(0, f.size);
554
+ dir.set(f.name, bytes);
555
+ onWrite?.(f.name, bytes);
556
+ }
557
+ handles.delete(h);
558
+ return true;
559
+ }
560
+ // THE SYSTEM FILE TABLE. DOS keeps one entry per open file in its own memory, and a program that
561
+ // wants more than a handle can give -- DJGPP's fstat() wants the size, the date and a number to
562
+ // use as an inode -- asks INT 21h AH=52h for the "list of lists", follows its pointer to the SFT,
563
+ // and reads the entry the PSP's job file table names for the handle. So there is one, at 0050:0000
564
+ // below the PSP, kept in step with every open, read, write, seek and close (syncSft).
565
+ const LOL = 0x500, SFT = 0x510, SFT_ENTRIES = 20, SFT_SIZE = 0x3b;
566
+ function initSft() {
567
+ mem.fill(0, LOL, SFT + 6 + SFT_ENTRIES * SFT_SIZE);
568
+ mem[LOL + 4] = SFT - LOL; mem[LOL + 5] = 0; mem[LOL + 6] = (LOL >> 4) & 0xff; mem[LOL + 7] = LOL >> 12;
569
+ mem[SFT] = 0xff; mem[SFT + 1] = 0xff; mem[SFT + 2] = 0xff; mem[SFT + 3] = 0xff; // no next table
570
+ mem[SFT + 4] = SFT_ENTRIES;
571
+ const psp = pspSeg * 16;
572
+ mem[psp + 0x32] = SFT_ENTRIES;
573
+ mem[psp + 0x34] = 0x18; mem[psp + 0x35] = 0; mem[psp + 0x36] = pspSeg & 0xff; mem[psp + 0x37] = pspSeg >> 8;
574
+ syncSft();
575
+ }
576
+ function syncSft() {
577
+ const psp = pspSeg * 16;
578
+ for (let h = 0; h < SFT_ENTRIES; h++) {
579
+ const e = SFT + 6 + h * SFT_SIZE, f = handles.get(h);
580
+ const put16 = (o, v) => { mem[e + o] = v; mem[e + o + 1] = v >> 8; };
581
+ const put32 = (o, v) => { mem[e + o] = v; mem[e + o + 1] = v >> 8; mem[e + o + 2] = v >> 16; mem[e + o + 3] = v >>> 24; };
582
+ if (h < 5) { // the standard devices
583
+ mem[psp + 0x18 + h] = h;
584
+ put16(0, 1); put16(5, 0x80d3);
585
+ for (let i = 0; i < 11; i++) mem[e + 0x20 + i] = 'CON '.charCodeAt(i);
586
+ continue;
587
+ }
588
+ if (!f) { mem[psp + 0x18 + h] = 0xff; put16(0, 0); continue; }
589
+ mem[psp + 0x18 + h] = h;
590
+ put16(0, 1); put16(2, f.mode ?? 2); mem[e + 4] = 0x20; put16(5, 0x02 | (f.dirty ? 0 : 0x40));
591
+ put16(0x0b, 2 + h); put16(0x0d, 0); put16(0x0f, 0x21); put32(0x11, f.size); put32(0x15, f.pos);
592
+ const base = f.name.slice(f.name.lastIndexOf('/') + 1), [stem, ext = ''] = base.split('.');
593
+ const fcb = stem.padEnd(8).slice(0, 8) + ext.padEnd(3).slice(0, 3);
594
+ for (let i = 0; i < 11; i++) mem[e + 0x20 + i] = fcb.charCodeAt(i);
595
+ }
596
+ }
597
+
598
+ // find first / find next over the directory, matching DOS wildcards
599
+ let findList = [];
600
+ function wildcard(pat) {
601
+ const [pn, pe = ''] = pat.split('.');
602
+ const rx = (p) => p.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.?');
603
+ return new RegExp(`^${rx(pn)}(\\.${rx(pe)})?$`, 'i');
604
+ }
605
+ /** The entries of one directory matching a DOS pattern: files, and subdirectories when asked. */
606
+ function listDir(pattern, attrs) {
607
+ const parent = parentOf(pattern), rx = wildcard(pattern.slice(parent ? parent.length + 1 : 0) || '*.*');
608
+ const seen = new Map();
609
+ const consider = (path, isDirectory) => {
610
+ if (parentOf(path) !== parent) return;
611
+ const name = path.slice(parent ? parent.length + 1 : 0);
612
+ if (rx.test(name) && !seen.has(name)) seen.set(name, { name, isDirectory, size: isDirectory ? 0 : dir.get(path).length });
613
+ };
614
+ for (const k of dir.keys()) {
615
+ consider(k, false);
616
+ if (attrs & 0x10) { // directories implied by the paths under them
617
+ let d = parentOf(k);
618
+ while (d && d !== parent) { consider(d, true); d = parentOf(d); }
619
+ }
620
+ }
621
+ if (attrs & 0x10) for (const d of dirs) consider(d, true);
622
+ return [...seen.values()];
623
+ }
624
+ function fillDta(dta, entry) {
625
+ mem[dta + 0x15] = entry.isDirectory ? 0x10 : 0x20;
626
+ mem[dta + 0x16] = 0; mem[dta + 0x17] = 0; mem[dta + 0x18] = 0x21; mem[dta + 0x19] = 0x1f;
627
+ const n = entry.size;
628
+ mem[dta + 0x1a] = n; mem[dta + 0x1b] = n >> 8; mem[dta + 0x1c] = n >> 16; mem[dta + 0x1d] = n >> 24;
629
+ for (let i = 0; i < 13; i++) mem[dta + 0x1e + i] = i < entry.name.length ? entry.name.charCodeAt(i) : 0;
630
+ }
631
+ let dta = PSP_SEG * 16 + 0x80;
632
+
633
+ // ---------------------------------------------------------------- interrupt vectors
634
+ // the program's own protected-mode handlers; unset ones fall back to the ROM stubs
635
+ const pmVectors = new Array(256).fill(null);
636
+ const romStub = (n) => ({ sel: SEL_CODE, off: ROM_STUBS + n * 16 });
637
+ function writeStubs() {
638
+ for (let n = 0; n < 256; n++) {
639
+ const a = ROM_STUBS + n * 16;
640
+ let code;
641
+ if (n === 8) code = [0x50, 0xb0, 0x20, 0xe6, 0x20, 0x58, 0xcf]; // EOI, IRET
642
+ else if (n === 9) code = [0x50, 0xe4, 0x60, 0xb0, 0x20, 0xe6, 0x20, 0x58, 0xcf]; // read the key, EOI, IRET
643
+ else if (n >= 0x70 && n <= 0x77) code = [0x50, 0xb0, 0x20, 0xe6, 0xa0, 0xe6, 0x20, 0x58, 0xcf];
644
+ else if ((n >= 0x0a && n <= 0x0f)) code = [0x50, 0xb0, 0x20, 0xe6, 0x20, 0x58, 0xcf];
645
+ else code = [0xcd, n, 0xcf]; // INT n (the machine's), IRET
646
+ mem.set(code, a);
647
+ }
648
+ }
649
+
650
+ // ---------------------------------------------------------------- the CPU
651
+ let exited = false, exitCode = 0;
652
+ const bus = {
653
+ mem,
654
+ vgaWrite, vgaRead, portIn, portOut,
655
+ selectorBase: (sel) => selectors.get(sel & 0xfff8 | (sel & 0)) ?? selectors.get(sel) ?? 0,
656
+ selectorIs16: (sel) => seg16.has(sel & 0xfff8),
657
+ // real mode: the vector table at 0:0, as DOS and the BIOS leave it (each entry a ROM stub until
658
+ // the program sets its own); protected mode: the vectors set through DPMI
659
+ vector: (n) => (cpu.realMode ? { sel: mem[n * 4 + 2] | (mem[n * 4 + 3] << 8), off: mem[n * 4] | (mem[n * 4 + 1] << 8) } : pmVectors[n] ?? romStub(n)),
660
+ softInt: (cpu, n) => {
661
+ // a vector the program installed takes the call, unless it is the one the stub is making
662
+ const fromStub = cpu.eip - 2 === ROM_STUBS + n * 16;
663
+ if (cpu.realMode) {
664
+ const off = mem[n * 4] | (mem[n * 4 + 1] << 8), segv = mem[n * 4 + 2] | (mem[n * 4 + 3] << 8);
665
+ if (!(segv === 0xf000 && off === n * 16) && !fromStub) return false;
666
+ } else if (pmVectors[n] && !fromStub) return false;
667
+ return service(cpu, n);
668
+ },
669
+ };
670
+ const cpu = createCpu(bus);
671
+ const R = cpu.R;
672
+ writeStubs();
673
+
674
+ const u16 = (v) => v & 0xffff;
675
+ const setAX = (v) => { R[EAX] = (R[EAX] & ~0xffff) | (v & 0xffff); };
676
+ const ok = () => cpu.setCF(false);
677
+ const fail = (code) => { setAX(code); cpu.setCF(true); };
678
+ const linDS = (off) => (cpu.segBase[DS] + off) >>> 0;
679
+ const linES = (off) => (cpu.segBase[ES] + off) >>> 0;
680
+ const unhandled = new Set();
681
+ function note(what) {
682
+ if (unhandled.has(what)) return;
683
+ unhandled.add(what);
684
+ log?.(`unhandled ${what}`);
685
+ }
686
+
687
+ function service(c, n) {
688
+ switch (n) {
689
+ case 0x21: { const r = dos(c); syncSft(); return r; }
690
+ case 0x20: exited = true; exitCode = 0; c.stop(); onExit?.(0); return true;
691
+ case 0x31: return dpmi(c);
692
+ case 0x10: return video(c);
693
+ case 0x16: return biosKey(c);
694
+ case 0x33: return mouse(c);
695
+ case 0x1a: {
696
+ const ah = (R[EAX] >> 8) & 0xff;
697
+ if (ah === 0) { const t = Math.floor(now() * 18.2065 / 1000); R[ECX] = (R[ECX] & ~0xffff) | ((t >> 16) & 0xffff); R[EDX] = (R[EDX] & ~0xffff) | (t & 0xffff); setAX(0); }
698
+ else if (ah === 2 || ah === 4) { R[ECX] &= ~0xffff; R[EDX] &= ~0xffff; ok(); }
699
+ return true;
700
+ }
701
+ case 0x11: setAX(0x0027); return true;
702
+ case 0x12: setAX(640); return true;
703
+ case 0x15: fail(0x8600); return true;
704
+ case 0x2f:
705
+ if (u16(R[EAX]) === 0x1500) { R[EBX] &= ~0xffff; return true; } // MSCDEX: no CD-ROM drives
706
+ setAX(R[EAX] & 0xff00);
707
+ return true;
708
+ case 0x08: case 0x09: case 0x0a: case 0x0b: case 0x0c: case 0x0d: case 0x0e: case 0x0f:
709
+ case 0x1c: case 0x23: case 0x24: case 0x1b: return true;
710
+ case 0x70: case 0x71: case 0x72: case 0x73: case 0x74: case 0x75: case 0x76: case 0x77: return true;
711
+ default:
712
+ note(`INT ${n.toString(16)} AX=${u16(R[EAX]).toString(16)}`);
713
+ return true;
714
+ }
715
+ }
716
+
717
+ // ---------------------------------------------------------------- INT 21h
718
+ function dos(c) {
719
+ const ah = (R[EAX] >> 8) & 0xff, al = R[EAX] & 0xff;
720
+ switch (ah) {
721
+ case 0x02: putChar(R[EDX] & 0xff); return true;
722
+ case 0x06: if ((R[EDX] & 0xff) !== 0xff) putChar(R[EDX] & 0xff); else { c.setZF(true); R[EAX] &= ~0xff; } return true;
723
+ case 0x07: case 0x08: R[EAX] = (R[EAX] & ~0xff) | (kbd.queue.length ? 13 : 13); return true;
724
+ case 0x09: { let a = linDS(R[EDX]); for (let i = 0; i < 4096 && mem[a] !== 0x24; i++) putChar(mem[a++]); return true; }
725
+ case 0x0b: R[EAX] &= ~0xff; return true;
726
+ case 0x0e: R[EAX] = (R[EAX] & ~0xff) | 26; return true;
727
+ case 0x19: R[EAX] &= ~0xff | 2; R[EAX] = (R[EAX] & ~0xff) | 2; return true;
728
+ case 0x1a: dta = linDS(R[EDX]); return true;
729
+ case 0x25:
730
+ if (c.realMode) { mem[al * 4] = R[EDX]; mem[al * 4 + 1] = R[EDX] >> 8; mem[al * 4 + 2] = c.seg[DS]; mem[al * 4 + 3] = c.seg[DS] >> 8; return true; }
731
+ pmVectors[al] = { sel: c.seg[DS], off: R[EDX] >>> 0 };
732
+ return true;
733
+ case 0x2a: { const d = new Date(); R[ECX] = (R[ECX] & ~0xffff) | d.getFullYear(); R[EDX] = (R[EDX] & ~0xffff) | ((d.getMonth() + 1) << 8) | d.getDate(); R[EAX] = (R[EAX] & ~0xff) | d.getDay(); return true; }
734
+ case 0x2c: { const d = new Date(); R[ECX] = (R[ECX] & ~0xffff) | (d.getHours() << 8) | d.getMinutes(); R[EDX] = (R[EDX] & ~0xffff) | (d.getSeconds() << 8) | Math.floor(d.getMilliseconds() / 10); return true; }
735
+ case 0x2f: c.loadSeg(ES, SEL_DATA); R[EBX] = dta; return true;
736
+ case 0x30: R[EAX] = 0x1606; R[EBX] = 0; R[ECX] = 0; return true; // DOS 6.22, and no Phar Lap signature
737
+ case 0x33:
738
+ if (al === 0) R[EDX] &= ~0xff;
739
+ // the true version, which DJGPP reads before it trusts the SFT's layout: DOS 6, in the HMA
740
+ if (al === 6) { R[EBX] = (R[EBX] & ~0xffff) | 0x0006; R[EDX] = (R[EDX] & ~0xffff) | 0x1000; }
741
+ ok();
742
+ return true;
743
+ case 0x35: {
744
+ if (c.realMode) { c.loadSeg(ES, mem[al * 4 + 2] | (mem[al * 4 + 3] << 8)); R[EBX] = (R[EBX] & ~0xffff) | mem[al * 4] | (mem[al * 4 + 1] << 8); return true; }
745
+ const v = pmVectors[al] ?? romStub(al); c.loadSeg(ES, v.sel); R[EBX] = v.off;
746
+ return true;
747
+ }
748
+ case 0x36: setAX(4); R[EBX] = (R[EBX] & ~0xffff) | 0x4000; R[ECX] = (R[ECX] & ~0xffff) | 512; R[EDX] = (R[EDX] & ~0xffff) | 0xffff; return true;
749
+ case 0x39: { const d = dosName(linDS(R[EDX])); if (dir.has(d) || isDir(d)) { fail(5); return true; } dirs.add(d); ok(); return true; }
750
+ case 0x3a: { const d = dosName(linDS(R[EDX])); if (!isDir(d)) { fail(3); return true; } dirs.delete(d); ok(); return true; }
751
+ case 0x3b: if (isDir(dosName(linDS(R[EDX])))) ok(); else fail(3); return true;
752
+ case 0x3c: case 0x5b: { // create
753
+ const name = dosName(linDS(R[EDX]));
754
+ if (ah === 0x5b && dir.has(name)) { fail(80); return true; }
755
+ if (isDir(name) || !isDir(parentOf(name))) { fail(isDir(name) ? 5 : 3); return true; }
756
+ const data = new Uint8Array(4096);
757
+ const h = openHandle(name, data);
758
+ if (h < 0) { fail(4); return true; }
759
+ const f = handles.get(h); f.size = 0; f.dirty = true; f.mode = 2;
760
+ dir.set(name, new Uint8Array(0));
761
+ R[EAX] = h; ok();
762
+ return true;
763
+ }
764
+ case 0x3d: { // open
765
+ const name = dosName(linDS(R[EDX]));
766
+ const data = dir.get(name);
767
+ if (!data) { fail(2); return true; }
768
+ const h = openHandle(name, (al & 3) ? data.slice() : data);
769
+ if (h < 0) { fail(4); return true; }
770
+ handles.get(h).mode = al;
771
+ R[EAX] = h; ok();
772
+ return true;
773
+ }
774
+ case 0x3e: {
775
+ const h = u16(R[EBX]);
776
+ if (h < 5) { ok(); return true; }
777
+ if (closeHandle(h)) ok(); else fail(6);
778
+ return true;
779
+ }
780
+ case 0x3f: { // read
781
+ const h = u16(R[EBX]), n = R[ECX] >>> 0, a = linDS(R[EDX]);
782
+ if (h === 0) { R[EAX] = 0; ok(); return true; }
783
+ const f = handles.get(h);
784
+ if (!f) { fail(6); return true; }
785
+ const k = Math.max(0, Math.min(n, f.size - f.pos));
786
+ mem.set(f.data.subarray(f.pos, f.pos + k), a);
787
+ cpu.invalidate(a, k);
788
+ f.pos += k; R[EAX] = k; ok();
789
+ return true;
790
+ }
791
+ case 0x40: { // write
792
+ const h = u16(R[EBX]), n = R[ECX] >>> 0, a = linDS(R[EDX]);
793
+ if (h === 1 || h === 2) { for (let i = 0; i < n; i++) putChar(mem[a + i]); R[EAX] = n; ok(); return true; }
794
+ const f = handles.get(h);
795
+ if (!f) { fail(6); return true; }
796
+ if (n === 0) { f.size = f.pos; f.dirty = true; R[EAX] = 0; ok(); return true; } // truncate
797
+ if (f.pos + n > f.data.length) {
798
+ const grown = new Uint8Array(Math.max(f.pos + n, f.data.length * 2));
799
+ grown.set(f.data.subarray(0, f.size)); f.data = grown;
800
+ }
801
+ f.data.set(mem.subarray(a, a + n), f.pos);
802
+ f.pos += n; f.size = Math.max(f.size, f.pos); f.dirty = true;
803
+ R[EAX] = n; ok();
804
+ return true;
805
+ }
806
+ case 0x41: { const name = dosName(linDS(R[EDX])); if (dir.delete(name)) { onWrite?.(name, null); ok(); } else fail(2); return true; }
807
+ case 0x42: { // seek
808
+ const h = u16(R[EBX]);
809
+ const f = handles.get(h);
810
+ if (!f) { if (h < 5) { R[EAX] = 0; R[EDX] &= ~0xffff; ok(); } else fail(6); return true; }
811
+ const off = ((u16(R[ECX]) << 16) | u16(R[EDX])) | 0;
812
+ const base = al === 0 ? 0 : al === 1 ? f.pos : f.size;
813
+ f.pos = Math.max(0, base + off);
814
+ R[EAX] = f.pos; R[EDX] = (R[EDX] & ~0xffff) | ((f.pos >>> 16) & 0xffff);
815
+ ok();
816
+ return true;
817
+ }
818
+ case 0x43: {
819
+ const name = dosName(linDS(R[EDX]));
820
+ const directory = !dir.has(name) && isDir(name);
821
+ if (!dir.has(name) && !directory) { fail(2); return true; }
822
+ if (al === 0) R[ECX] = (R[ECX] & ~0xffff) | (directory ? 0x10 : 0x20);
823
+ ok();
824
+ return true;
825
+ }
826
+ case 0x44: {
827
+ const h = u16(R[EBX]);
828
+ if (al === 0) { R[EDX] = (R[EDX] & ~0xffff) | (h < 5 ? (h === 0 ? 0x80d3 : 0x80d3) : 0x0002); ok(); return true; }
829
+ if (al === 1) { ok(); return true; }
830
+ if (al === 8) { setAX(1); ok(); return true; }
831
+ if (al === 9) { R[EDX] &= ~0xffff; ok(); return true; }
832
+ fail(1);
833
+ return true;
834
+ }
835
+ case 0x45: { const f = handles.get(u16(R[EBX])); if (!f) { if (u16(R[EBX]) < 5) { R[EAX] = u16(R[EBX]); ok(); } else fail(6); return true; } const h = openHandle(f.name, f.data); Object.assign(handles.get(h), { size: f.size, pos: f.pos }); R[EAX] = h; ok(); return true; }
836
+ case 0x47: mem[linDS(R[ESI])] = 0; ok(); return true;
837
+ case 0x48: {
838
+ const paras = u16(R[EBX]);
839
+ const s = dosAlloc(paras);
840
+ if (s < 0) { fail(8); R[EBX] = (R[EBX] & ~0xffff) | dosLargest(); return true; }
841
+ R[EAX] = s; ok();
842
+ return true;
843
+ }
844
+ case 0x49: if (dosFree(c.seg[ES] & 0xffff) || !c.realMode) ok(); else fail(9); return true;
845
+ case 0x4a: {
846
+ if (!c.realMode) { ok(); return true; }
847
+ const r = dosResize(c.seg[ES] & 0xffff, u16(R[EBX]));
848
+ if (r === true) ok(); else if (r < 0) fail(9); else { fail(8); R[EBX] = (R[EBX] & ~0xffff) | r; }
849
+ return true;
850
+ }
851
+ case 0x4c: exited = true; exitCode = al; c.stop(); onExit?.(al); return true;
852
+ case 0x4e: {
853
+ findList = listDir(dosName(linDS(R[EDX])), u16(R[ECX]));
854
+ if (!findList.length) { fail(18); return true; }
855
+ fillDta(dta, findList.shift()); ok();
856
+ return true;
857
+ }
858
+ case 0x4f: if (!findList.length) { fail(18); return true; } fillDta(dta, findList.shift()); ok(); return true;
859
+ case 0x56: {
860
+ const from = dosName(linDS(R[EDX])), to = dosName(linES(R[EDI]));
861
+ const d = dir.get(from);
862
+ if (!d) { fail(2); return true; }
863
+ dir.delete(from); dir.set(to, d); onWrite?.(from, null); onWrite?.(to, d); ok();
864
+ return true;
865
+ }
866
+ case 0x57: R[ECX] &= ~0xffff; R[EDX] = (R[EDX] & ~0xffff) | 0x21; ok(); return true;
867
+ case 0x58: if (al === 0) setAX(0); ok(); return true;
868
+ case 0x59: setAX(0); R[EBX] &= ~0xffff; R[ECX] &= ~0xffff; return true;
869
+ case 0x60: { // canonical name: C:\ and the path
870
+ const out = `C:\\${dosName(linDS(R[ESI])).replace(/\//g, '\\')}\0`, a = linES(R[EDI]);
871
+ for (let i = 0; i < out.length; i++) mem[a + i] = out.charCodeAt(i);
872
+ ok();
873
+ return true;
874
+ }
875
+ case 0x67: case 0x68: case 0x6a: ok(); return true;
876
+ case 0x71: setAX(0x7100); cpu.setCF(true); return true;
877
+ case 0x5d: fail(1); return true; // the swappable data area: not offered, which DOS 6 may also say // no long file names: the answer programs expect
878
+ case 0x51: case 0x62: R[EBX] = (R[EBX] & ~0xffff) | (realMode || c.realMode ? pspSeg : SEL_PSP); return true;
879
+ case 0x52: c.loadSeg(ES, SEL_LOL); R[EBX] &= ~0xffff; return true; // the list of lists (syncSft)
880
+ case 0xff:
881
+ // DOS/4GW's own presence check (DX = 0x78): answer "yes, DOS/4G", and hand over the
882
+ // selector for the first megabyte in GS the way the extender does
883
+ if (al === 0 && u16(R[EDX]) === 0x78) { R[EAX] = 0x4734ff01; c.loadSeg(GS, SEL_DATA); return true; }
884
+ R[EAX] &= ~0xff;
885
+ return true;
886
+ }
887
+ note(`INT 21 AH=${ah.toString(16)} AL=${al.toString(16)}`);
888
+ cpu.setCF(true);
889
+ return true;
890
+ }
891
+
892
+ // ---------------------------------------------------------------- INT 31h, DPMI 0.9
893
+ function dpmi(c) {
894
+ const ax = u16(R[EAX]);
895
+ const bxcx = () => ((u16(R[EBX]) << 16) | u16(R[ECX])) >>> 0;
896
+ const setBXCX = (v) => { R[EBX] = (R[EBX] & ~0xffff) | ((v >>> 16) & 0xffff); R[ECX] = (R[ECX] & ~0xffff) | (v & 0xffff); };
897
+ const setSIDI = (v) => { R[ESI] = (R[ESI] & ~0xffff) | ((v >>> 16) & 0xffff); R[EDI] = (R[EDI] & ~0xffff) | (v & 0xffff); };
898
+ const sidi = () => ((u16(R[ESI]) << 16) | u16(R[EDI])) >>> 0;
899
+ switch (ax) {
900
+ case 0x0000: {
901
+ const n = Math.max(1, u16(R[ECX]));
902
+ const first = nextSelector;
903
+ for (let i = 0; i < n; i++) { selectors.set(nextSelector, 0); nextSelector += 8; }
904
+ setAX(first); ok();
905
+ return true;
906
+ }
907
+ case 0x0001: selectors.delete(u16(R[EBX])); ok(); return true;
908
+ case 0x0002: { const sel = nextSelector; nextSelector += 8; selectors.set(sel, u16(R[EBX]) * 16); setAX(sel); ok(); return true; }
909
+ case 0x0003: setAX(8); ok(); return true;
910
+ case 0x0006: { const b = selectors.get(u16(R[EBX])) ?? 0; R[ECX] = (R[ECX] & ~0xffff) | (b >>> 16); R[EDX] = (R[EDX] & ~0xffff) | (b & 0xffff); ok(); return true; }
911
+ case 0x0007: selectors.set(u16(R[EBX]), ((u16(R[ECX]) << 16) | u16(R[EDX])) >>> 0); refreshSegs(c); ok(); return true;
912
+ case 0x0008: ok(); return true;
913
+ case 0x0009: if (u16(R[ECX]) & 0x4000) seg16.delete(u16(R[EBX]) & 0xfff8); else if (u16(R[ECX]) & 0x08) seg16.add(u16(R[EBX]) & 0xfff8); refreshSegs(c); ok(); return true;
914
+ case 0x000a: { const sel = nextSelector; nextSelector += 8; selectors.set(sel, selectors.get(u16(R[EBX])) ?? 0); setAX(sel); ok(); return true; }
915
+ case 0x000b: {
916
+ const a = linES(R[EDI]), b = selectors.get(u16(R[EBX])) ?? 0;
917
+ mem.set([0xff, 0xff, b & 0xff, (b >> 8) & 0xff, (b >> 16) & 0xff, 0xf2, 0xcf, (b >>> 24) & 0xff], a);
918
+ ok();
919
+ return true;
920
+ }
921
+ case 0x000c: {
922
+ const a = linES(R[EDI]);
923
+ selectors.set(u16(R[EBX]), (mem[a + 2] | (mem[a + 3] << 8) | (mem[a + 4] << 16) | (mem[a + 7] << 24)) >>> 0);
924
+ // a code descriptor with the D bit clear is 16-bit code
925
+ if ((mem[a + 5] & 0x08) && !(mem[a + 6] & 0x40)) seg16.add(u16(R[EBX]) & 0xfff8); else seg16.delete(u16(R[EBX]) & 0xfff8);
926
+ refreshSegs(c); ok();
927
+ return true;
928
+ }
929
+ case 0x0100: {
930
+ const s = dosAlloc(u16(R[EBX]));
931
+ if (s < 0) { fail(8); R[EBX] = (R[EBX] & ~0xffff) | dosLargest(); return true; }
932
+ const sel = nextSelector; nextSelector += 8; selectors.set(sel, s * 16);
933
+ setAX(s); R[EDX] = (R[EDX] & ~0xffff) | sel; ok();
934
+ return true;
935
+ }
936
+ case 0x0101: case 0x0102: ok(); return true;
937
+ case 0x0200: R[ECX] &= ~0xffff; R[EDX] &= ~0xffff; ok(); return true;
938
+ case 0x0201: ok(); return true;
939
+ case 0x0202: { const v = romStub(R[EBX] & 0xff); R[ECX] = (R[ECX] & ~0xffff) | v.sel; R[EDX] = v.off; ok(); return true; }
940
+ case 0x0203: ok(); return true;
941
+ case 0x0204: { const v = pmVectors[R[EBX] & 0xff] ?? romStub(R[EBX] & 0xff); R[ECX] = (R[ECX] & ~0xffff) | v.sel; R[EDX] = v.off; ok(); return true; }
942
+ case 0x0205: {
943
+ const n = R[EBX] & 0xff, sel = u16(R[ECX]), off = R[EDX] >>> 0;
944
+ pmVectors[n] = sel === SEL_CODE && off === ROM_STUBS + n * 16 ? null : { sel, off };
945
+ ok();
946
+ return true;
947
+ }
948
+ case 0x0300: case 0x0301: case 0x0302: return realModeCall(c, ax);
949
+ case 0x0303: R[ECX] = (R[ECX] & ~0xffff) | 0xf000; R[EDX] = (R[EDX] & ~0xffff) | 0x0100; ok(); return true;
950
+ case 0x0304: ok(); return true;
951
+ case 0x0400: setAX(0x005a); R[EBX] = (R[EBX] & ~0xffff) | 0x0005; R[ECX] = (R[ECX] & ~0xff) | 4; R[EDX] = (R[EDX] & ~0xffff) | 0x0870; ok(); return true;
952
+ case 0x0500: {
953
+ const a = linES(R[EDI]), { free, largest } = freeMemory();
954
+ const put = (o, v) => { mem[a + o] = v; mem[a + o + 1] = v >> 8; mem[a + o + 2] = v >> 16; mem[a + o + 3] = v >>> 24; };
955
+ for (let i = 0; i < 48; i++) mem[a + i] = 0xff;
956
+ put(0, largest); put(4, largest >>> 12); put(8, largest >>> 12); put(0x0c, free >>> 12);
957
+ put(0x10, free >>> 12); put(0x14, free >>> 12); put(0x18, MEM_SIZE >>> 12); put(0x1c, free >>> 12); put(0x20, 0);
958
+ ok();
959
+ return true;
960
+ }
961
+ case 0x0501: {
962
+ const b = allocBlock(bxcx());
963
+ if (!b) { fail(0x8013); return true; }
964
+ setBXCX(b.base); setSIDI(b.handle); ok();
965
+ return true;
966
+ }
967
+ case 0x0502: {
968
+ const i = blocks.findIndex((b) => b.handle === sidi());
969
+ if (i < 0) { fail(0x8023); return true; }
970
+ blocks.splice(i, 1); ok();
971
+ return true;
972
+ }
973
+ case 0x0503: {
974
+ const i = blocks.findIndex((b) => b.handle === sidi());
975
+ if (i < 0) { fail(0x8023); return true; }
976
+ const old = blocks[i], want = (bxcx() + 0xfff) & ~0xfff;
977
+ const nextBase = i + 1 < blocks.length ? blocks[i + 1].base : MEM_SIZE;
978
+ if (old.base + want <= nextBase) { old.size = want; setBXCX(old.base); setSIDI(old.handle); ok(); return true; }
979
+ blocks.splice(i, 1);
980
+ const nb = allocBlock(want);
981
+ if (!nb) { blocks.splice(i, 0, old); fail(0x8013); return true; }
982
+ mem.copyWithin(nb.base, old.base, old.base + old.size);
983
+ cpu.invalidate(nb.base, old.size);
984
+ setBXCX(nb.base); setSIDI(nb.handle); ok();
985
+ return true;
986
+ }
987
+ case 0x0600: case 0x0601: case 0x0602: case 0x0603: case 0x0702: case 0x0703: ok(); return true;
988
+ case 0x0604: R[EBX] &= ~0xffff; R[ECX] = (R[ECX] & ~0xffff) | 0x1000; ok(); return true;
989
+ case 0x0800: ok(); return true; // physical address mapping: identity
990
+ case 0x0801: ok(); return true;
991
+ case 0x0900: case 0x0901: {
992
+ const was = c.IF ? 1 : 0;
993
+ c.flags = ax === 0x0901 ? c.flags | 0x200 : c.flags & ~0x200;
994
+ R[EAX] = (R[EAX] & ~0xff) | was;
995
+ return true;
996
+ }
997
+ case 0x0902: R[EAX] = (R[EAX] & ~0xff) | (c.IF ? 1 : 0); return true;
998
+ case 0x0a00: fail(0x8001); return true;
999
+ case 0x0507: case 0x0506: fail(0x8001); return true; // DPMI 1.0 page attributes: a 0.9 host has none, and DJGPP expects that
1000
+ case 0x0e00: setAX(0x004d); ok(); return true;
1001
+ case 0x0e01: ok(); return true;
1002
+ }
1003
+ note(`INT 31 AX=${ax.toString(16)}`);
1004
+ fail(0x8001);
1005
+ return true;
1006
+ }
1007
+ function refreshSegs(c) {
1008
+ for (let i = 0; i < 6; i++) c.loadSeg(i, c.seg[i]);
1009
+ }
1010
+
1011
+ // DPMI 0300: a real-mode interrupt with registers in a table at ES:EDI. The services a
1012
+ // protected-mode program asks for this way are the same BIOS and DOS calls, so they run through
1013
+ // the same handlers with the table's registers swapped in, then swapped back out.
1014
+ function realModeCall(c, ax) {
1015
+ const t = linES(R[EDI]);
1016
+ const g32 = (o) => mem[t + o] | (mem[t + o + 1] << 8) | (mem[t + o + 2] << 16) | (mem[t + o + 3] << 24);
1017
+ const g16 = (o) => mem[t + o] | (mem[t + o + 1] << 8);
1018
+ const p32 = (o, v) => { mem[t + o] = v; mem[t + o + 1] = v >> 8; mem[t + o + 2] = v >> 16; mem[t + o + 3] = v >> 24; };
1019
+ const p16 = (o, v) => { mem[t + o] = v; mem[t + o + 1] = v >> 8; };
1020
+ if (ax !== 0x0300) { note(`DPMI ${ax.toString(16)} (real-mode far call)`); ok(); return true; }
1021
+ const n = R[EBX] & 0xff;
1022
+ const saved = Array.from(R), savedSeg = Array.from(c.seg), savedFlags = c.flags;
1023
+ // real mode is 16-bit: a caller that fills the table through a union leaves stale upper halves,
1024
+ // and a pointer in DS:DX must not carry them
1025
+ R[EDI] = g16(0); R[ESI] = g16(4); R[EBP] = g16(8); R[EBX] = g16(16); R[EDX] = g16(20); R[ECX] = g16(24); R[EAX] = g16(28);
1026
+ // real-mode segments: selectors whose base is the segment times sixteen
1027
+ const rmSel = (segv, which) => { const sel = 0xf000 + which * 8; selectors.set(sel, segv * 16); c.loadSeg(which, sel); };
1028
+ rmSel(g16(0x22), ES); rmSel(g16(0x24), DS);
1029
+ c.flags = g16(0x20);
1030
+ realMode++;
1031
+ try { service(c, n); } finally { realMode--; }
1032
+ const outFlags = c.flags;
1033
+ // a service that returns a pointer in ES or DS (the list of lists, a vector) hands back a segment
1034
+ p16(0x22, (c.segBase[ES] >>> 4) & 0xffff); p16(0x24, (c.segBase[DS] >>> 4) & 0xffff);
1035
+ p32(0, R[EDI]); p32(4, R[ESI]); p32(8, R[EBP]); p32(16, R[EBX]); p32(20, R[EDX]); p32(24, R[ECX]); p32(28, R[EAX]);
1036
+ p16(0x20, outFlags);
1037
+ for (let i = 0; i < 8; i++) R[i] = saved[i];
1038
+ for (let i = 0; i < 6; i++) c.loadSeg(i, savedSeg[i]);
1039
+ c.flags = savedFlags;
1040
+ ok();
1041
+ return true;
1042
+ }
1043
+
1044
+ // ---------------------------------------------------------------- INT 10h
1045
+ function video(c) {
1046
+ const ah = (R[EAX] >> 8) & 0xff, al = R[EAX] & 0xff;
1047
+ switch (ah) {
1048
+ case 0x00: setVideoMode(al); return true;
1049
+ case 0x01: return true;
1050
+ case 0x02: text.y = Math.min(24, (R[EDX] >> 8) & 0xff); text.x = Math.min(79, R[EDX] & 0xff); return true;
1051
+ case 0x03: R[EDX] = (R[EDX] & ~0xffff) | (text.y << 8) | text.x; R[ECX] = (R[ECX] & ~0xffff) | 0x0607; return true;
1052
+ case 0x05: return true;
1053
+ case 0x06: case 0x07: {
1054
+ const lines = al, attr = (R[EBX] >> 8) & 0xff;
1055
+ const top = (R[ECX] >> 8) & 0xff, left = R[ECX] & 0xff, bottom = Math.min(24, (R[EDX] >> 8) & 0xff), right = Math.min(79, R[EDX] & 0xff);
1056
+ const cell = (x, y) => 0xb8000 + (y * 80 + x) * 2;
1057
+ const h = bottom - top + 1;
1058
+ for (let y = 0; y < h; y++) {
1059
+ for (let x = left; x <= right; x++) {
1060
+ const dst = ah === 0x06 ? top + y : bottom - y;
1061
+ const srcY = ah === 0x06 ? dst + lines : dst - lines;
1062
+ const inside = lines !== 0 && srcY >= top && srcY <= bottom;
1063
+ mem[cell(x, dst)] = inside ? mem[cell(x, srcY)] : 0x20;
1064
+ mem[cell(x, dst) + 1] = inside ? mem[cell(x, srcY) + 1] : attr;
1065
+ }
1066
+ }
1067
+ return true;
1068
+ }
1069
+ case 0x08: { const a = 0xb8000 + (text.y * 80 + text.x) * 2; setAX((mem[a + 1] << 8) | mem[a]); return true; }
1070
+ case 0x09: case 0x0a: {
1071
+ const n = u16(R[ECX]), attr = R[EBX] & 0xff;
1072
+ for (let i = 0; i < n; i++) {
1073
+ const p = text.y * 80 + text.x + i;
1074
+ if (p >= 2000) break;
1075
+ mem[0xb8000 + p * 2] = al;
1076
+ if (ah === 0x09) mem[0xb8001 + p * 2] = attr;
1077
+ }
1078
+ return true;
1079
+ }
1080
+ case 0x0e: putChar(al); return true;
1081
+ case 0x0f: setAX((80 << 8) | vga.mode); R[EBX] &= ~0xff00; return true;
1082
+ case 0x10:
1083
+ if (al === 0x12 || al === 0x10) {
1084
+ const first = u16(R[EBX]), count = al === 0x10 ? 1 : u16(R[ECX]);
1085
+ if (al === 0x10) { vga.pal[first * 3] = (R[EDX] >> 8) & 0x3f; vga.pal[first * 3 + 1] = (R[ECX] >> 8) & 0x3f; vga.pal[first * 3 + 2] = R[ECX] & 0x3f; return true; }
1086
+ const a = linES(R[EDX]);
1087
+ for (let i = 0; i < count * 3; i++) vga.pal[(first * 3 + i) % 768] = mem[a + i] & 0x3f;
1088
+ vga.palSeq++;
1089
+ }
1090
+ return true;
1091
+ case 0x11: case 0x12: if (ah === 0x12 && (R[EBX] & 0xff) === 0x10) R[EBX] = (R[EBX] & ~0xffff) | 0x0003; return true;
1092
+ case 0x1a: if (al === 0) { R[EAX] = (R[EAX] & ~0xff) | 0x1a; R[EBX] = (R[EBX] & ~0xffff) | 0x0008; } return true;
1093
+ case 0x4f: setAX(0x0100); return true; // VESA BIOS extensions: not here (AL != 4Fh), VGA modes only
1094
+ }
1095
+ note(`INT 10 AH=${ah.toString(16)}`);
1096
+ return true;
1097
+ }
1098
+
1099
+ // ---------------------------------------------------------------- INT 16h, INT 33h
1100
+ const biosKeys = []; // [scan, ascii] typed while a program reads the BIOS
1101
+ function biosKey(c) {
1102
+ const ah = (R[EAX] >> 8) & 0xff;
1103
+ if (ah === 0x00 || ah === 0x10) { const k = biosKeys.shift() ?? [0x1c, 13]; setAX((k[0] << 8) | k[1]); return true; }
1104
+ if (ah === 0x01 || ah === 0x11) { if (biosKeys.length) { const k = biosKeys[0]; setAX((k[0] << 8) | k[1]); c.setZF(false); } else c.setZF(true); return true; }
1105
+ if (ah === 0x02 || ah === 0x12) { R[EAX] &= ~0xff; return true; }
1106
+ return true;
1107
+ }
1108
+ const mouseState = { dx: 0, dy: 0, buttons: 0, present: true };
1109
+ function mouse(c) {
1110
+ const ax = u16(R[EAX]);
1111
+ switch (ax) {
1112
+ case 0x0001: case 0x0002: case 0x0004: case 0x0007: case 0x0008: case 0x000f: case 0x001a: case 0x001d: return true;
1113
+ case 0x0000: case 0x0021:
1114
+ if (!mouseState.present) { setAX(0); return true; }
1115
+ setAX(0xffff); R[EBX] = (R[EBX] & ~0xffff) | 3; return true;
1116
+ case 0x0003: R[EBX] = (R[EBX] & ~0xffff) | mouseState.buttons; R[ECX] &= ~0xffff; R[EDX] &= ~0xffff; return true;
1117
+ case 0x000b: {
1118
+ const dx = Math.max(-32768, Math.min(32767, Math.round(mouseState.dx)));
1119
+ const dy = Math.max(-32768, Math.min(32767, Math.round(mouseState.dy)));
1120
+ mouseState.dx -= dx; mouseState.dy -= dy;
1121
+ R[ECX] = (R[ECX] & ~0xffff) | (dx & 0xffff); R[EDX] = (R[EDX] & ~0xffff) | (dy & 0xffff);
1122
+ return true;
1123
+ }
1124
+ }
1125
+ note(`INT 33 AX=${ax.toString(16)}`);
1126
+ return true;
1127
+ }
1128
+
1129
+ // ---------------------------------------------------------------- start
1130
+ function writeEnvironment(programPath) {
1131
+ const env = ENV_SEG * 16;
1132
+ const envText = 'PATH=C:\\\0COMSPEC=C:\\COMMAND.COM\0BLASTER=A220 I7 D1 T4\0\0';
1133
+ let p = env;
1134
+ for (const ch of envText) mem[p++] = ch.charCodeAt(0);
1135
+ mem[p++] = 1; mem[p++] = 0;
1136
+ for (const ch of `${programPath}\0`) mem[p++] = ch.charCodeAt(0);
1137
+ return p - env;
1138
+ }
1139
+ function writeCommandTail(psp) {
1140
+ const tail = args ? ` ${args}` : '';
1141
+ mem[psp + 0x80] = Math.min(126, tail.length);
1142
+ for (let i = 0; i < tail.length && i < 126; i++) mem[psp + 0x81 + i] = tail.charCodeAt(i);
1143
+ mem[psp + 0x81 + Math.min(126, tail.length)] = 13;
1144
+ }
1145
+ function biosDataArea() {
1146
+ // what a program might peek at: 80 columns, a colour card, the timer count (updateTimers)
1147
+ mem[0x449] = 3; mem[0x44a] = 80; mem[0x463] = 0xd4; mem[0x464] = 0x03; mem[0x484] = 24;
1148
+ }
1149
+ const newSelector = (base) => { const sel = nextSelector; nextSelector += 8; selectors.set(sel, base >>> 0); return sel; };
1150
+
1151
+ /**
1152
+ * Start a program: a DOS/4GW LE (DOOM) or a DJGPP COFF (Quake), told apart by what is in the file.
1153
+ */
1154
+ function boot(exe) {
1155
+ const coff = parseCoff(exe);
1156
+ if (coff) return bootCoff(exe, coff);
1157
+ if (!hasLE(exe)) return bootMZ(exe);
1158
+ const img = loadLE(exe, mem, LOAD_DELTA);
1159
+ // PSP: the command tail at 80h, the environment's selector at 2Ch
1160
+ const psp = PSP_SEG * 16;
1161
+ mem[psp] = 0xcd; mem[psp + 1] = 0x20;
1162
+ mem[psp + 2] = 0x00; mem[psp + 3] = 0xa0;
1163
+ mem[psp + 0x2c] = SEL_ENV; mem[psp + 0x2d] = 0;
1164
+ writeCommandTail(psp);
1165
+ initSft();
1166
+ // the environment, then the program's own path after a count of one
1167
+ writeEnvironment('C:\\DOOM\\DOOM.EXE');
1168
+ selectors.set(SEL_CODE16, img.objs.find((o) => !(o.flags & 0x2000))?.base ?? 0);
1169
+ biosDataArea();
1170
+ R[ESP] = img.esp;
1171
+ cpu.loadSeg(CS, SEL_CODE); cpu.loadSeg(DS, SEL_DATA); cpu.loadSeg(SS, SEL_DATA);
1172
+ cpu.loadSeg(ES, SEL_PSP); cpu.loadSeg(FS, 0); cpu.loadSeg(GS, 0);
1173
+ cpu.eip = img.entry;
1174
+ cpu.flags = 0x202;
1175
+ setVideoMode(3);
1176
+ pit.nextIrq = now() + pitPeriodMs();
1177
+ return img;
1178
+ }
1179
+
1180
+ /**
1181
+ * A REAL-MODE PROGRAM (Wolfenstein 3D): what DOS's own EXEC does. The program gets the largest free
1182
+ * block of conventional memory with its PSP at the front, the image is copied in after the PSP and
1183
+ * its segment relocations fixed up, DS and ES point at the PSP, and the CPU starts in real mode at
1184
+ * the header's CS:IP with its SS:SP. The vector table at 0:0 points every interrupt at the ROM
1185
+ * stubs until the program hooks one.
1186
+ */
1187
+ function bootMZ(exe) {
1188
+ const dv = new DataView(exe.buffer, exe.byteOffset, exe.length);
1189
+ const u = (o) => dv.getUint16(o, true);
1190
+ const lastPage = u(2), pages = u(4);
1191
+ const fileSize = lastPage ? (pages - 1) * 512 + lastPage : pages * 512;
1192
+ const hdr = u(8) * 16;
1193
+ const image = exe.subarray(hdr, Math.min(exe.length, fileSize));
1194
+ const imageParas = (image.length + 15) >> 4;
1195
+ const want = Math.min(dosLargest(), Math.max(0x10 + imageParas + u(0x0a), Math.min(0xffff, 0x10 + imageParas + u(0x0c))));
1196
+ pspSeg = dosAlloc(want);
1197
+ if (pspSeg < 0) throw new Error('not enough conventional memory for this program');
1198
+ const loadSeg = pspSeg + 0x10;
1199
+ mem.set(image, loadSeg * 16);
1200
+ for (let i = 0; i < u(6); i++) {
1201
+ const e = u(0x18) + i * 4;
1202
+ const at = (u(e + 2) + loadSeg) * 16 + u(e);
1203
+ const v = (mem[at] | (mem[at + 1] << 8)) + loadSeg;
1204
+ mem[at] = v; mem[at + 1] = v >> 8;
1205
+ }
1206
+ const psp = pspSeg * 16;
1207
+ mem.fill(0, psp, psp + 0x100);
1208
+ mem[psp] = 0xcd; mem[psp + 1] = 0x20;
1209
+ const top = pspSeg + want;
1210
+ mem[psp + 2] = top; mem[psp + 3] = top >> 8;
1211
+ mem[psp + 0x2c] = ENV_SEG & 0xff; mem[psp + 0x2d] = ENV_SEG >> 8;
1212
+ selectors.set(SEL_PSP, psp);
1213
+ dta = psp + 0x80;
1214
+ writeCommandTail(psp);
1215
+ initSft();
1216
+ writeEnvironment(`C:\\${programName}`);
1217
+ for (let n = 0; n < 256; n++) { mem[n * 4] = n * 16; mem[n * 4 + 1] = (n * 16) >> 8; mem[n * 4 + 2] = 0x00; mem[n * 4 + 3] = 0xf0; }
1218
+ biosDataArea();
1219
+ cpu.realMode = true;
1220
+ cpu.loadSeg(CS, u(0x16) + loadSeg); cpu.loadSeg(SS, u(0x0e) + loadSeg);
1221
+ cpu.loadSeg(DS, pspSeg); cpu.loadSeg(ES, pspSeg); cpu.loadSeg(FS, 0); cpu.loadSeg(GS, 0);
1222
+ R[ESP] = u(0x10);
1223
+ cpu.eip = (u(0x16) + loadSeg) * 16 + u(0x14);
1224
+ cpu.flags = 0x202;
1225
+ setVideoMode(3);
1226
+ pit.nextIrq = now() + pitPeriodMs();
1227
+ return { kind: 'mz', loadSeg, psp: pspSeg };
1228
+ }
1229
+
1230
+ /**
1231
+ * THE GO32 STUB, IMPERSONATED. The real stub is 16-bit code that finds a DPMI host (loading
1232
+ * CWSDPMI.EXE if it must), switches to protected mode, allocates the program's memory block,
1233
+ * reads the COFF sections into it and jumps to the entry with FS on a copy of its "stubinfo". The
1234
+ * CPU here has no real mode, so this does what the stub leaves behind: the block with the sections
1235
+ * in it, code and data selectors based at the block, a transfer buffer in conventional memory, and
1236
+ * the stubinfo that crt0 reads -- and then CWSDPMI's part is played by this file's INT 31h.
1237
+ */
1238
+ function bootCoff(exe, img) {
1239
+ const block = allocBlock(img.size);
1240
+ for (const sec of img.sections) {
1241
+ if (sec.bss) continue;
1242
+ mem.set(exe.subarray(sec.fileOff, sec.fileOff + sec.size), block.base + sec.vaddr);
1243
+ }
1244
+ const csSel = newSelector(block.base), dsSel = newSelector(block.base);
1245
+ // the transfer buffer DOS calls go through, and the PSP with the environment's real-mode segment
1246
+ // the real stub is loaded at PSP+100h and its transfer buffer is inside it, and DJGPP's libc
1247
+ // finds the PSP by subtracting 100h from the buffer's address: so the PSP sits right below it
1248
+ pspSeg = dosAlloc(0x10 + ((img.minkeep + 15) >> 4));
1249
+ selectors.set(SEL_PSP, pspSeg * 16);
1250
+ dta = pspSeg * 16 + 0x80;
1251
+ const tbSeg = pspSeg + 0x10;
1252
+ const tbSel = newSelector(tbSeg * 16);
1253
+ const psp = pspSeg * 16;
1254
+ mem[psp] = 0xcd; mem[psp + 1] = 0x20; mem[psp + 2] = 0x00; mem[psp + 3] = 0xa0;
1255
+ // a DPMI host swaps the environment's segment at PSP:2Ch for a selector on entry to protected
1256
+ // mode, and DJGPP's libc reads it as one
1257
+ mem[psp + 0x2c] = SEL_ENV; mem[psp + 0x2d] = 0;
1258
+ writeCommandTail(psp);
1259
+ initSft();
1260
+ const envSize = writeEnvironment('C:\\QUAKE.EXE');
1261
+ // stubinfo (djgpp stub.asm): magic, size, minstack, memory handle, initial size, minkeep, the
1262
+ // transfer buffer's selector and segment, the PSP selector, the stub's CS, env size, names
1263
+ const siSeg = dosAlloc(8), si = siSeg * 16;
1264
+ const put32 = (o, v) => { mem[si + o] = v; mem[si + o + 1] = v >> 8; mem[si + o + 2] = v >> 16; mem[si + o + 3] = v >>> 24; };
1265
+ const put16 = (o, v) => { mem[si + o] = v; mem[si + o + 1] = v >> 8; };
1266
+ const putStr = (o, str, n) => { for (let i = 0; i < n; i++) mem[si + o + i] = i < str.length ? str.charCodeAt(i) : 0; };
1267
+ putStr(0, 'go32stub, v 2.00', 16);
1268
+ put32(0x10, 0x54); put32(0x14, img.minstack); put32(0x18, block.handle); put32(0x1c, img.size);
1269
+ // the stub's own code selector: 16-bit, based where the stub (and so the transfer buffer) is.
1270
+ // crt0's exit copies its last few instructions -- free the program's memory, INT 21h 4Ch -- into
1271
+ // the buffer and jumps to them through this
1272
+ const stubCs = newSelector(tbSeg * 16);
1273
+ seg16.add(stubCs);
1274
+ put16(0x20, img.minkeep); put16(0x22, tbSel); put16(0x24, tbSeg); put16(0x26, SEL_PSP); put16(0x28, stubCs);
1275
+ put16(0x2a, envSize);
1276
+ putStr(0x2c, 'QUAKE', 8); putStr(0x34, 'QUAKE.EXE', 16); putStr(0x44, 'CWSDPMI', 16);
1277
+ const siSel = newSelector(si);
1278
+ biosDataArea();
1279
+ // registers as the stub hands over: a scratch stack below the video memory until crt0 makes its own
1280
+ cpu.loadSeg(CS, csSel); cpu.loadSeg(DS, dsSel); cpu.loadSeg(ES, dsSel);
1281
+ cpu.loadSeg(SS, SEL_DATA); cpu.loadSeg(FS, siSel); cpu.loadSeg(GS, 0);
1282
+ R[ESP] = 0x9ff00;
1283
+ cpu.eip = block.base + img.entry;
1284
+ cpu.flags = 0x202;
1285
+ setVideoMode(3);
1286
+ pit.nextIrq = now() + pitPeriodMs();
1287
+ return { ...img, base: block.base, kind: 'djgpp' };
1288
+ }
1289
+
1290
+ // ---------------------------------------------------------------- running
1291
+ function deliverIrq() {
1292
+ if (!cpu.IF) return false;
1293
+ for (let chip = 0; chip < 2; chip++) {
1294
+ const pending = pic.irr[chip] & ~pic.mask[chip];
1295
+ if (!pending) continue;
1296
+ const bit = pending & -pending;
1297
+ const higherInService = pic.isr[chip] & (bit - 1 | bit);
1298
+ if (higherInService) continue;
1299
+ if (chip === 1 && (pic.mask[0] & 4)) continue;
1300
+ pic.irr[chip] &= ~bit;
1301
+ pic.isr[chip] |= bit;
1302
+ const irq = 31 - Math.clz32(bit);
1303
+ if (irq === 1 && chip === 0) kbd.data = kbd.queue.shift() ?? kbd.data;
1304
+ cpu.interrupt(chip === 0 ? 8 + irq : 0x70 + irq);
1305
+ return true;
1306
+ }
1307
+ return false;
1308
+ }
1309
+ function updateTimers(t) {
1310
+ // THE BIOS TICK COUNT at 0040:006C, which the BIOS's own IRQ 0 handler keeps. Programs read it
1311
+ // beside the timer's counter to tell the time finely (DJGPP's uclock), so it advances exactly
1312
+ // when counter 0 wraps -- a wrap per 65,536 input clocks -- and is carried across reprogramming
1313
+ const wraps = Math.floor(((t - pit.start[0]) * PIT_HZ) / 1000 / 65536);
1314
+ const ticks = (pit.tickBase + wraps) >>> 0;
1315
+ mem[0x46c] = ticks; mem[0x46d] = ticks >> 8; mem[0x46e] = ticks >> 16; mem[0x46f] = ticks >>> 24;
1316
+ const period = pitPeriodMs();
1317
+ if (t >= pit.nextIrq) {
1318
+ raiseIrq(0);
1319
+ pit.nextIrq += period;
1320
+ if (t - pit.nextIrq > period * 4) pit.nextIrq = t + period; // fell behind by more than a few: do not replay them all
1321
+ }
1322
+ if (kbd.queue.length && !(pic.irr[0] & 2) && !(pic.isr[0] & 2)) raiseIrq(1);
1323
+ sound?.tick?.(t, raiseIrq);
1324
+ }
1325
+
1326
+ /** Run the machine for `n` instructions, in slices, servicing the hardware between them. */
1327
+ function run(n, slice = 8000) {
1328
+ let done = 0;
1329
+ while (done < n && !exited) {
1330
+ updateTimers(now());
1331
+ deliverIrq();
1332
+ if (cpu.halted) {
1333
+ // nothing to do until an interrupt: let the clock run
1334
+ if (!(pic.irr[0] & ~pic.mask[0]) && !(pic.irr[1] & ~pic.mask[1])) return done;
1335
+ cpu.halted = false;
1336
+ continue;
1337
+ }
1338
+ done += cpu.run(Math.min(slice, n - done));
1339
+ }
1340
+ return done;
1341
+ }
1342
+
1343
+ return {
1344
+ mem, cpu, vga, text, pic, pit, kbd, mouse: mouseState, dir, stdout,
1345
+ boot, run, renderGraphics, renderIndexed,
1346
+ get exited() { return exited; }, get exitCode() { return exitCode; },
1347
+ /** A key: a set-1 scancode (the break code has bit 7 set; extended keys arrive after 0xE0). */
1348
+ key(scancode) { kbd.queue.push(scancode & 0xff); },
1349
+ biosKey(scan, ascii) { biosKeys.push([scan, ascii]); },
1350
+ raiseIrq,
1351
+ unhandled,
1352
+ };
1353
+ }