jsbeeb 1.3.3 → 1.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.
- package/package.json +1 -1
- package/src/6502.js +26 -21
- package/src/6502.opcodes.js +20 -15
- package/src/acia.js +21 -7
- package/src/bbc-palette.js +24 -0
- package/src/cmos.js +12 -1
- package/src/config.js +23 -0
- package/src/fake6502.js +9 -2
- package/src/fdc.js +51 -54
- package/src/keyboard.js +7 -4
- package/src/machine-session.js +37 -3
- package/src/main.js +93 -32
- package/src/models.js +111 -83
- package/src/soundchip.js +78 -0
- package/src/speech-output.js +85 -0
- package/src/teletext.js +31 -16
- package/src/via.js +1 -1
- package/src/video.js +206 -34
- package/src/wd-fdc.js +6 -1
- package/src/web/audio-handler.js +1 -1
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"name": "jsbeeb",
|
|
8
8
|
"description": "Emulate a BBC Micro",
|
|
9
9
|
"repository": "git@github.com:mattgodbolt/jsbeeb.git",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.5.0",
|
|
11
11
|
"//": "If you change the version of Node, it must also be updated at the top of the Dockerfile.",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": "22"
|
package/src/6502.js
CHANGED
|
@@ -101,12 +101,12 @@ class Flags {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
class Base6502 {
|
|
104
|
-
constructor(model) {
|
|
104
|
+
constructor(model, { cycleAccurate = true } = {}) {
|
|
105
105
|
this.model = model;
|
|
106
106
|
this.a = this.x = this.y = this.s = 0;
|
|
107
107
|
this.p = new Flags();
|
|
108
108
|
this.pc = 0;
|
|
109
|
-
this.opcodes = model.opcodesFactory(this);
|
|
109
|
+
this.opcodes = model.opcodesFactory(this, { cycleAccurate });
|
|
110
110
|
this.disassembler = this.opcodes.disassembler;
|
|
111
111
|
this.forceTracing = false;
|
|
112
112
|
this.runner = this.opcodes.runInstruction;
|
|
@@ -405,9 +405,10 @@ class Base6502 {
|
|
|
405
405
|
|
|
406
406
|
class Tube6502 extends Base6502 {
|
|
407
407
|
constructor(model, cpu) {
|
|
408
|
-
super(model);
|
|
408
|
+
super(model, { cycleAccurate: false });
|
|
409
409
|
|
|
410
410
|
this.cycles = 0;
|
|
411
|
+
this.cpuMultiplier = 2;
|
|
411
412
|
this.romPaged = true;
|
|
412
413
|
this.memory = new Uint8Array(65536);
|
|
413
414
|
this.rom = new Uint8Array(4096);
|
|
@@ -468,7 +469,7 @@ class Tube6502 extends Base6502 {
|
|
|
468
469
|
}
|
|
469
470
|
|
|
470
471
|
execute(cycles) {
|
|
471
|
-
this.cycles += cycles *
|
|
472
|
+
this.cycles += (cycles * this.cpuMultiplier) | 0;
|
|
472
473
|
if (this.cycles < 3) return;
|
|
473
474
|
while (this.cycles > 0) {
|
|
474
475
|
const opcode = this.readmem(this.pc);
|
|
@@ -570,19 +571,22 @@ function is1MHzAccess(addr) {
|
|
|
570
571
|
}
|
|
571
572
|
|
|
572
573
|
export class Cpu6502 extends Base6502 {
|
|
573
|
-
constructor(
|
|
574
|
-
|
|
574
|
+
constructor(
|
|
575
|
+
model,
|
|
576
|
+
{ dbgr, video, soundChip, ddNoise, music5000, cmos, config, econet, cycleAccurate = true } = {},
|
|
577
|
+
) {
|
|
578
|
+
super(model, { cycleAccurate });
|
|
575
579
|
this.config = fixUpConfig(config);
|
|
576
580
|
this.debugFlags = this.config.debugFlags;
|
|
577
581
|
this.cmos = cmos;
|
|
578
582
|
this.debugger = dbgr;
|
|
579
583
|
|
|
580
|
-
this.video =
|
|
584
|
+
this.video = video;
|
|
581
585
|
this.crtc = this.video.crtc;
|
|
582
586
|
this.ula = this.video.ula;
|
|
583
|
-
this.soundChip =
|
|
584
|
-
this.music5000 =
|
|
585
|
-
this.ddNoise =
|
|
587
|
+
this.soundChip = soundChip;
|
|
588
|
+
this.music5000 = music5000;
|
|
589
|
+
this.ddNoise = ddNoise;
|
|
586
590
|
this.memStatOffsetByIFetchBank = 0;
|
|
587
591
|
this.memStatOffset = 0;
|
|
588
592
|
this.memStat = new Uint8Array(512);
|
|
@@ -602,8 +606,11 @@ export class Cpu6502 extends Base6502 {
|
|
|
602
606
|
this.videoCyclesBatch = this.config.videoCyclesBatch | 0;
|
|
603
607
|
this.peripheralCyclesPerSecond = 2 * 1000 * 1000;
|
|
604
608
|
this.tube = model.tube ? new Tube6502(model.tube, this) : new FakeTube();
|
|
609
|
+
if (model.tube && this.config.tubeCpuMultiplier) {
|
|
610
|
+
this.tube.cpuMultiplier = this.config.tubeCpuMultiplier;
|
|
611
|
+
}
|
|
605
612
|
this.music5000PageSel = 0;
|
|
606
|
-
this.econet =
|
|
613
|
+
this.econet = econet;
|
|
607
614
|
|
|
608
615
|
this.peripheralCycles = 0;
|
|
609
616
|
this.videoCycles = 0;
|
|
@@ -616,16 +623,14 @@ export class Cpu6502 extends Base6502 {
|
|
|
616
623
|
this.debugWrite = new DebugHook(this, "_debugWrite");
|
|
617
624
|
|
|
618
625
|
this.scheduler = new Scheduler();
|
|
619
|
-
this.sysvia = new via.SysVia(
|
|
620
|
-
this,
|
|
621
|
-
this.
|
|
622
|
-
this.
|
|
623
|
-
this.
|
|
624
|
-
this.
|
|
625
|
-
this.
|
|
626
|
-
|
|
627
|
-
this.config.getGamepads,
|
|
628
|
-
);
|
|
626
|
+
this.sysvia = new via.SysVia(this, this.scheduler, {
|
|
627
|
+
video: this.video,
|
|
628
|
+
soundChip: this.soundChip,
|
|
629
|
+
cmos: this.cmos,
|
|
630
|
+
isMaster: this.model.isMaster,
|
|
631
|
+
initialLayout: this.config.keyLayout,
|
|
632
|
+
getGamepads: this.config.getGamepads,
|
|
633
|
+
});
|
|
629
634
|
this.uservia = new via.UserVia(this, this.scheduler, this.model.isMaster, this.config.userPort);
|
|
630
635
|
this.acia = new Acia(this, this.soundChip.toneGenerator, this.scheduler, this.touchScreen);
|
|
631
636
|
this.serial = new Serial(this.acia);
|
package/src/6502.opcodes.js
CHANGED
|
@@ -41,8 +41,9 @@ function push(reg) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
class InstructionGen {
|
|
44
|
-
constructor(is65c12) {
|
|
44
|
+
constructor(is65c12, cycleAccurate = true) {
|
|
45
45
|
this.is65c12 = is65c12;
|
|
46
|
+
this.cycleAccurate = cycleAccurate;
|
|
46
47
|
this.ops = {};
|
|
47
48
|
this.cycle = 0;
|
|
48
49
|
}
|
|
@@ -106,6 +107,10 @@ class InstructionGen {
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
spuriousOp(addr, reg) {
|
|
110
|
+
if (!this.cycleAccurate) {
|
|
111
|
+
this.cycle++;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
109
114
|
if (this.is65c12) {
|
|
110
115
|
this.readOp(addr, "", true);
|
|
111
116
|
} else {
|
|
@@ -122,7 +127,7 @@ class InstructionGen {
|
|
|
122
127
|
toSkip++;
|
|
123
128
|
continue;
|
|
124
129
|
}
|
|
125
|
-
if (toSkip && this.ops[i].exact) {
|
|
130
|
+
if (this.cycleAccurate && toSkip && this.ops[i].exact) {
|
|
126
131
|
if (this.ops[i].addr) {
|
|
127
132
|
out.push(`cpu.polltimeAddr(${toSkip}, ${this.ops[i].addr});`);
|
|
128
133
|
} else {
|
|
@@ -134,7 +139,7 @@ class InstructionGen {
|
|
|
134
139
|
toSkip++;
|
|
135
140
|
}
|
|
136
141
|
if (toSkip) {
|
|
137
|
-
if (this.ops[this.cycle] && this.ops[this.cycle].addr) {
|
|
142
|
+
if (this.cycleAccurate && this.ops[this.cycle] && this.ops[this.cycle].addr) {
|
|
138
143
|
out.push(`cpu.polltimeAddr(${toSkip}, ${this.ops[this.cycle].addr});`);
|
|
139
144
|
} else {
|
|
140
145
|
out.push(`cpu.polltime(${toSkip});`);
|
|
@@ -145,17 +150,17 @@ class InstructionGen {
|
|
|
145
150
|
}
|
|
146
151
|
|
|
147
152
|
split(condition) {
|
|
148
|
-
return new SplitInstruction(this, condition, this.is65c12);
|
|
153
|
+
return new SplitInstruction(this, condition, this.is65c12, this.cycleAccurate);
|
|
149
154
|
}
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
class SplitInstruction {
|
|
153
|
-
constructor(preamble, condition, is65c12) {
|
|
158
|
+
constructor(preamble, condition, is65c12, cycleAccurate = true) {
|
|
154
159
|
this.preamble = preamble;
|
|
155
160
|
this.condition = condition;
|
|
156
|
-
this.ifTrue = new InstructionGen(is65c12);
|
|
161
|
+
this.ifTrue = new InstructionGen(is65c12, cycleAccurate);
|
|
157
162
|
this.ifTrue.tick(preamble.cycle);
|
|
158
|
-
this.ifFalse = new InstructionGen(is65c12);
|
|
163
|
+
this.ifFalse = new InstructionGen(is65c12, cycleAccurate);
|
|
159
164
|
this.ifFalse.tick(preamble.cycle);
|
|
160
165
|
|
|
161
166
|
["append", "prepend", "readOp", "writeOp", "spuriousOp"].forEach((op) => {
|
|
@@ -1052,7 +1057,7 @@ class Disassemble6502 {
|
|
|
1052
1057
|
}
|
|
1053
1058
|
}
|
|
1054
1059
|
|
|
1055
|
-
function makeCpuFunctions(cpu, opcodes, is65c12) {
|
|
1060
|
+
function makeCpuFunctions(cpu, opcodes, is65c12, cycleAccurate = true) {
|
|
1056
1061
|
function getInstruction(opcodeString, needsReg) {
|
|
1057
1062
|
const split = opcodeString.split(" ");
|
|
1058
1063
|
const opcode = split[0];
|
|
@@ -1060,7 +1065,7 @@ function makeCpuFunctions(cpu, opcodes, is65c12) {
|
|
|
1060
1065
|
const op = getOp(opcode, arg);
|
|
1061
1066
|
if (!op) return null;
|
|
1062
1067
|
|
|
1063
|
-
let ig = new InstructionGen(is65c12);
|
|
1068
|
+
let ig = new InstructionGen(is65c12, cycleAccurate);
|
|
1064
1069
|
if (needsReg) ig.append("let REG = 0|0;");
|
|
1065
1070
|
|
|
1066
1071
|
switch (arg) {
|
|
@@ -1398,14 +1403,14 @@ ${indent}`)
|
|
|
1398
1403
|
};
|
|
1399
1404
|
}
|
|
1400
1405
|
|
|
1401
|
-
export function Cpu6502(cpu) {
|
|
1402
|
-
return makeCpuFunctions(cpu, opcodes6502, false);
|
|
1406
|
+
export function Cpu6502(cpu, { cycleAccurate = true } = {}) {
|
|
1407
|
+
return makeCpuFunctions(cpu, opcodes6502, false, cycleAccurate);
|
|
1403
1408
|
}
|
|
1404
1409
|
|
|
1405
|
-
export function Cpu65c12(cpu) {
|
|
1406
|
-
return makeCpuFunctions(cpu, opcodes65c12, true);
|
|
1410
|
+
export function Cpu65c12(cpu, { cycleAccurate = true } = {}) {
|
|
1411
|
+
return makeCpuFunctions(cpu, opcodes65c12, true, cycleAccurate);
|
|
1407
1412
|
}
|
|
1408
1413
|
|
|
1409
|
-
export function Cpu65c02(cpu) {
|
|
1410
|
-
return makeCpuFunctions(cpu, opcodes65c02, true);
|
|
1414
|
+
export function Cpu65c02(cpu, { cycleAccurate = true } = {}) {
|
|
1415
|
+
return makeCpuFunctions(cpu, opcodes65c02, true, cycleAccurate);
|
|
1411
1416
|
}
|
package/src/acia.js
CHANGED
|
@@ -118,20 +118,34 @@ export class Acia {
|
|
|
118
118
|
selectRs423(selected) {
|
|
119
119
|
this.rs423Selected = !!selected;
|
|
120
120
|
if (this.rs423Selected) {
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
121
|
+
// When a handler is present it acts as the remote device and is
|
|
122
|
+
// always ready to receive, so CTS is low (active = clear to send).
|
|
123
|
+
// Without a handler there is nothing on the other end and CTS
|
|
124
|
+
// stays high, which inhibits TDRE so the OS output buffer drains
|
|
125
|
+
// silently rather than hanging.
|
|
126
|
+
if (this.rs423Handler) {
|
|
127
|
+
this.sr &= ~0x08; // CTS low — clear to send
|
|
128
|
+
} else {
|
|
129
|
+
this.sr |= 0x08; // CTS high — not connected
|
|
130
|
+
}
|
|
126
131
|
} else {
|
|
127
|
-
// Cassette selected.
|
|
128
|
-
// CTS is always low, meaning actually Clear To Send.
|
|
132
|
+
// Cassette selected — CTS is always low (clear to send).
|
|
129
133
|
this.sr &= ~0x08;
|
|
130
134
|
}
|
|
131
135
|
this.dcdLineUpdated();
|
|
132
136
|
this.runRs423Task.ensureScheduled(this.rs423Selected, this.serialReceiveCyclesPerByte);
|
|
133
137
|
}
|
|
134
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Attach or detach an RS-423 peripheral handler at runtime.
|
|
141
|
+
* The handler must implement onTransmit(byte) and tryReceive(rts).
|
|
142
|
+
* If RS-423 is already selected, the CTS line is updated immediately.
|
|
143
|
+
*/
|
|
144
|
+
setRs423Handler(handler) {
|
|
145
|
+
this.rs423Handler = handler;
|
|
146
|
+
if (this.rs423Selected) this.selectRs423(true);
|
|
147
|
+
}
|
|
148
|
+
|
|
135
149
|
dcdLineUpdated() {
|
|
136
150
|
// AUG: "It will always be low when the RS423 interface is selected".
|
|
137
151
|
const level = this.rs423Selected ? false : this.tapeDcdLineLevel;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// The 16 standard BBC Micro colours in ABGR format (0xffBBGGRR, little-endian canvas RGBA).
|
|
4
|
+
// Colours 0-7 are the primary palette; 8-15 duplicate them as the default solid/non-flash set.
|
|
5
|
+
// Imported by both video.js (NulaDefaultPalette) and teletext.js (BbcDefaultCollook) to avoid
|
|
6
|
+
// circular imports between those two modules.
|
|
7
|
+
export const BbcDefaultPalette = new Uint32Array([
|
|
8
|
+
0xff000000, // 0: black
|
|
9
|
+
0xff0000ff, // 1: red
|
|
10
|
+
0xff00ff00, // 2: green
|
|
11
|
+
0xff00ffff, // 3: yellow
|
|
12
|
+
0xffff0000, // 4: blue
|
|
13
|
+
0xffff00ff, // 5: magenta
|
|
14
|
+
0xffffff00, // 6: cyan
|
|
15
|
+
0xffffffff, // 7: white
|
|
16
|
+
0xff000000, // 8: black (solid duplicate)
|
|
17
|
+
0xff0000ff, // 9: red
|
|
18
|
+
0xff00ff00, // 10: green
|
|
19
|
+
0xff00ffff, // 11: yellow
|
|
20
|
+
0xffff0000, // 12: blue
|
|
21
|
+
0xffff00ff, // 13: magenta
|
|
22
|
+
0xffffff00, // 14: cyan
|
|
23
|
+
0xffffffff, // 15: white
|
|
24
|
+
]);
|
package/src/cmos.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
// CMOS layout: bytes 0-13 are RTC internal registers; bytes 14-63 are the 50
|
|
4
|
+
// bytes of user storage. Storage byte 11 (= CMOS address 25 = 0x19) holds
|
|
5
|
+
// the BBC Master DFS configuration byte whose bits [1:0] are the WD1770 step
|
|
6
|
+
// rate (*CONFIGURE FDRIVE): 0=6ms 1=12ms 2=20ms 3=30ms.
|
|
7
|
+
// The correct default is FDRIVE 0 (6ms, value 0xc8). The previous default
|
|
8
|
+
// 0xca had bits[1:0]=0b10 (20ms), which caused disc-streaming demos such as
|
|
9
|
+
// STNICC-beeb to hang on the Master 128 because the WD1770 seek overran the
|
|
10
|
+
// vsync window. Confirmed by @tom-seddon (b2 emulator):
|
|
11
|
+
// https://github.com/mattgodbolt/jsbeeb/issues/576
|
|
3
12
|
const defaultCmos = [
|
|
4
13
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00, 0xeb, 0x00,
|
|
5
|
-
0xc9, 0xff, 0xff, 0x12, 0x00, 0x17,
|
|
14
|
+
0xc9, 0xff, 0xff, 0x12, 0x00, 0x17, 0xc8, 0x1e, 0x05, 0x00, 0x35, 0xa6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
6
15
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
7
16
|
];
|
|
8
17
|
|
|
@@ -22,6 +31,8 @@ function fromBcd(value) {
|
|
|
22
31
|
return parseInt(value.toString(16), 10);
|
|
23
32
|
}
|
|
24
33
|
|
|
34
|
+
export { defaultCmos };
|
|
35
|
+
|
|
25
36
|
export class Cmos {
|
|
26
37
|
constructor(persistence, cmosOverride, econet) {
|
|
27
38
|
this.store = persistence ? persistence.load() : null;
|
package/src/config.js
CHANGED
|
@@ -17,6 +17,7 @@ export class Config extends EventEmitter {
|
|
|
17
17
|
this.changed = {};
|
|
18
18
|
this.setDropdownText(this.model.name);
|
|
19
19
|
this.set65c02(this.model.tube);
|
|
20
|
+
this.setTubeCpuMultiplier(this.tubeCpuMultiplier);
|
|
20
21
|
this.setTeletext(this.model.hasTeletextAdaptor);
|
|
21
22
|
this.setMusic5000(this.model.hasMusic5000);
|
|
22
23
|
this.setEconet(this.model.hasEconet);
|
|
@@ -36,6 +37,13 @@ export class Config extends EventEmitter {
|
|
|
36
37
|
|
|
37
38
|
$("#65c02").on("click", () => {
|
|
38
39
|
this.changed.coProcessor = $("#65c02").prop("checked");
|
|
40
|
+
$("#tubeCpuMultiplier").prop("disabled", !$("#65c02").prop("checked"));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
$("#tubeCpuMultiplier").on("input", () => {
|
|
44
|
+
const val = parseInt($("#tubeCpuMultiplier").val(), 10);
|
|
45
|
+
$("#tubeCpuMultiplierValue").text(val);
|
|
46
|
+
this.changed.tubeCpuMultiplier = val;
|
|
39
47
|
});
|
|
40
48
|
|
|
41
49
|
$("#hasTeletextAdaptor").on("click", () => {
|
|
@@ -67,6 +75,10 @@ export class Config extends EventEmitter {
|
|
|
67
75
|
this.changed.mouseJoystickEnabled = $("#mouseJoystickEnabled").prop("checked");
|
|
68
76
|
});
|
|
69
77
|
|
|
78
|
+
$("#speechOutput").on("click", () => {
|
|
79
|
+
this.changed.speechOutput = $("#speechOutput").prop("checked");
|
|
80
|
+
});
|
|
81
|
+
|
|
70
82
|
$(".display-mode-option").on("click", (e) => {
|
|
71
83
|
const mode = $(e.target).data("mode");
|
|
72
84
|
this.changed.displayMode = mode;
|
|
@@ -87,6 +99,10 @@ export class Config extends EventEmitter {
|
|
|
87
99
|
$("#mouseJoystickEnabled").prop("checked", !!enabled);
|
|
88
100
|
}
|
|
89
101
|
|
|
102
|
+
setSpeechOutput(enabled) {
|
|
103
|
+
$("#speechOutput").prop("checked", !!enabled);
|
|
104
|
+
}
|
|
105
|
+
|
|
90
106
|
setDisplayMode(mode) {
|
|
91
107
|
const config = getFilterForMode(mode).getDisplayConfig();
|
|
92
108
|
$(".display-mode-text").text(config.name);
|
|
@@ -105,6 +121,13 @@ export class Config extends EventEmitter {
|
|
|
105
121
|
enabled = !!enabled;
|
|
106
122
|
$("#65c02").prop("checked", enabled);
|
|
107
123
|
this.model.tube = enabled ? findModel("Tube65c02") : null;
|
|
124
|
+
$("#tubeCpuMultiplier").prop("disabled", !enabled);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
setTubeCpuMultiplier(value) {
|
|
128
|
+
this.tubeCpuMultiplier = value;
|
|
129
|
+
$("#tubeCpuMultiplier").val(value);
|
|
130
|
+
$("#tubeCpuMultiplierValue").text(value);
|
|
108
131
|
}
|
|
109
132
|
|
|
110
133
|
setEconet(enabled) {
|
package/src/fake6502.js
CHANGED
|
@@ -17,10 +17,17 @@ const dbgr = {
|
|
|
17
17
|
|
|
18
18
|
export function fake6502(model, opts) {
|
|
19
19
|
opts = opts || {};
|
|
20
|
-
const video = opts.video || fakeVideo;
|
|
21
20
|
model = model || TEST_6502;
|
|
22
21
|
if (opts.tube) model.tube = findModel("Tube65c02");
|
|
23
|
-
return new Cpu6502(model,
|
|
22
|
+
return new Cpu6502(model, {
|
|
23
|
+
dbgr,
|
|
24
|
+
video: opts.video || fakeVideo,
|
|
25
|
+
soundChip: opts.soundChip || soundChip,
|
|
26
|
+
ddNoise: new FakeDdNoise(),
|
|
27
|
+
music5000: new FakeMusic5000(),
|
|
28
|
+
cmos: new Cmos(),
|
|
29
|
+
cycleAccurate: opts.cycleAccurate,
|
|
30
|
+
});
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
export function fake65C02() {
|
package/src/fdc.js
CHANGED
|
@@ -13,16 +13,17 @@ export function load(name) {
|
|
|
13
13
|
*/
|
|
14
14
|
export class DiscType {
|
|
15
15
|
/**
|
|
16
|
-
* Create a new disc type
|
|
17
|
-
* @param {
|
|
18
|
-
* @param {
|
|
19
|
-
* @param {function(Disc):
|
|
20
|
-
* @param {function(
|
|
21
|
-
* @param {
|
|
22
|
-
* @param {boolean}
|
|
23
|
-
* @param {
|
|
16
|
+
* Create a new disc type.
|
|
17
|
+
* @param {Object} [options] - Configuration options for this disc type.
|
|
18
|
+
* @param {string} options.extension - File extension for this disc type (e.g. ".ssd", ".hfe").
|
|
19
|
+
* @param {function(Disc, Uint8Array, function?): Disc} options.loader - Function to load this disc type.
|
|
20
|
+
* @param {function(Disc): Uint8Array} options.saver - Function to save this disc type.
|
|
21
|
+
* @param {function(Uint8Array, string): void|null} [options.nameSetter] - Function to set the name/label in the disc image, or null if not supported.
|
|
22
|
+
* @param {boolean} [options.isDoubleSided] - Whether the disc format is double-sided.
|
|
23
|
+
* @param {boolean} [options.isDoubleDensity] - Whether the disc format is double density.
|
|
24
|
+
* @param {number|undefined} [options.byteSize] - The size in bytes of this disc format, or undefined if variable.
|
|
24
25
|
*/
|
|
25
|
-
constructor(extension, loader, saver, nameSetter, isDoubleSided, isDoubleDensity, byteSize) {
|
|
26
|
+
constructor({ extension, loader, saver, nameSetter, isDoubleSided, isDoubleDensity, byteSize } = {}) {
|
|
26
27
|
this._extension = extension;
|
|
27
28
|
this._loader = loader;
|
|
28
29
|
this._saver = saver;
|
|
@@ -128,69 +129,65 @@ function setDfsDiscName(data, name) {
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
// HFE disc type - variable size
|
|
131
|
-
const hfeDiscType = new DiscType(
|
|
132
|
-
".hfe",
|
|
133
|
-
loadHfe,
|
|
134
|
-
toHfe,
|
|
135
|
-
|
|
136
|
-
true,
|
|
137
|
-
|
|
138
|
-
undefined, // variable size
|
|
139
|
-
);
|
|
132
|
+
const hfeDiscType = new DiscType({
|
|
133
|
+
extension: ".hfe",
|
|
134
|
+
loader: loadHfe,
|
|
135
|
+
saver: toHfe,
|
|
136
|
+
isDoubleSided: true,
|
|
137
|
+
isDoubleDensity: true,
|
|
138
|
+
});
|
|
140
139
|
|
|
141
140
|
// ADFS (Large) discs are double density, double sided
|
|
142
|
-
const adlDiscType = new DiscType(
|
|
143
|
-
".adl",
|
|
144
|
-
(disc, data, _onChange) => {
|
|
141
|
+
const adlDiscType = new DiscType({
|
|
142
|
+
extension: ".adl",
|
|
143
|
+
loader: (disc, data, _onChange) => {
|
|
145
144
|
// TODO handle onChange
|
|
146
145
|
return loadAdf(disc, data, true);
|
|
147
146
|
},
|
|
148
|
-
(_data) => {
|
|
147
|
+
saver: (_data) => {
|
|
149
148
|
throw new Error("ADL unsupported");
|
|
150
149
|
},
|
|
151
|
-
|
|
152
|
-
true,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
);
|
|
150
|
+
isDoubleSided: true,
|
|
151
|
+
isDoubleDensity: true,
|
|
152
|
+
byteSize: AdfsLargeByteSize,
|
|
153
|
+
});
|
|
156
154
|
|
|
157
155
|
// ADFS (Small) discs are standard ADFS (non-double) density, single sided
|
|
158
|
-
const adfDiscType = new DiscType(
|
|
159
|
-
".adf",
|
|
160
|
-
(disc, data, _onChange) => {
|
|
156
|
+
const adfDiscType = new DiscType({
|
|
157
|
+
extension: ".adf",
|
|
158
|
+
loader: (disc, data, _onChange) => {
|
|
161
159
|
// TODO handle onChange
|
|
162
160
|
return loadAdf(disc, data, false);
|
|
163
161
|
},
|
|
164
|
-
(_data) => {
|
|
162
|
+
saver: (_data) => {
|
|
165
163
|
throw new Error("ADF unsupported");
|
|
166
164
|
},
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
);
|
|
165
|
+
isDoubleSided: false,
|
|
166
|
+
isDoubleDensity: true,
|
|
167
|
+
byteSize: AdfsSmallByteSize,
|
|
168
|
+
});
|
|
172
169
|
|
|
173
170
|
// DSD (Double-sided disc)
|
|
174
|
-
const dsdDiscType = new DiscType(
|
|
175
|
-
".dsd",
|
|
176
|
-
(disc, data, onChange) => loadSsd(disc, data, true, onChange),
|
|
177
|
-
toSsdOrDsd,
|
|
178
|
-
setDfsDiscName,
|
|
179
|
-
true,
|
|
180
|
-
false,
|
|
181
|
-
DsdByteSize,
|
|
182
|
-
);
|
|
171
|
+
const dsdDiscType = new DiscType({
|
|
172
|
+
extension: ".dsd",
|
|
173
|
+
loader: (disc, data, onChange) => loadSsd(disc, data, true, onChange),
|
|
174
|
+
saver: toSsdOrDsd,
|
|
175
|
+
nameSetter: setDfsDiscName,
|
|
176
|
+
isDoubleSided: true,
|
|
177
|
+
isDoubleDensity: false,
|
|
178
|
+
byteSize: DsdByteSize,
|
|
179
|
+
});
|
|
183
180
|
|
|
184
181
|
// SSD (Single-sided disc)
|
|
185
|
-
const ssdDiscType = new DiscType(
|
|
186
|
-
".ssd",
|
|
187
|
-
(disc, data, onChange) => loadSsd(disc, data, false, onChange),
|
|
188
|
-
toSsdOrDsd,
|
|
189
|
-
setDfsDiscName,
|
|
190
|
-
false,
|
|
191
|
-
false,
|
|
192
|
-
SsdByteSize,
|
|
193
|
-
);
|
|
182
|
+
const ssdDiscType = new DiscType({
|
|
183
|
+
extension: ".ssd",
|
|
184
|
+
loader: (disc, data, onChange) => loadSsd(disc, data, false, onChange),
|
|
185
|
+
saver: toSsdOrDsd,
|
|
186
|
+
nameSetter: setDfsDiscName,
|
|
187
|
+
isDoubleSided: false,
|
|
188
|
+
isDoubleDensity: false,
|
|
189
|
+
byteSize: SsdByteSize,
|
|
190
|
+
});
|
|
194
191
|
/**
|
|
195
192
|
* Determine the disc type based on the file name extension
|
|
196
193
|
* @param {string} name - The file name with extension
|
package/src/keyboard.js
CHANGED
|
@@ -214,14 +214,17 @@ export class Keyboard extends EventEmitter {
|
|
|
214
214
|
const isSpecialHandled = this._handleSpecialKeys(code);
|
|
215
215
|
if (isSpecialHandled) return;
|
|
216
216
|
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
// Check for registered handlers
|
|
217
|
+
// Check for registered handlers first; if one fires, don't also send to the BBC.
|
|
218
|
+
// This lets Alt+key and Ctrl+key handlers cleanly own their keys without the
|
|
219
|
+
// underlying key leaking through to the emulated machine.
|
|
221
220
|
const handler = this._findKeyHandler(code, evt.altKey, evt.ctrlKey);
|
|
222
221
|
if (handler) {
|
|
223
222
|
handler.handler(true, code);
|
|
223
|
+
return;
|
|
224
224
|
}
|
|
225
|
+
|
|
226
|
+
// No handler claimed the key — pass it to the BBC Micro.
|
|
227
|
+
this.processor.sysvia.keyDown(code, evt.shiftKey);
|
|
225
228
|
}
|
|
226
229
|
|
|
227
230
|
/**
|
package/src/machine-session.js
CHANGED
|
@@ -9,6 +9,7 @@ import { readFileSync } from "fs";
|
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
10
|
import path from "path";
|
|
11
11
|
import { TestMachine } from "../tests/test-machine.js";
|
|
12
|
+
import { InstrumentedSoundChip } from "./soundchip.js";
|
|
12
13
|
|
|
13
14
|
// Resolve the jsbeeb package root from our own location (src/machine-session.js
|
|
14
15
|
// → go up one level). Passed to setNodeBasePath() so the ROM loader resolves
|
|
@@ -27,9 +28,12 @@ const FB_HEIGHT = 625;
|
|
|
27
28
|
export class MachineSession {
|
|
28
29
|
/**
|
|
29
30
|
* @param {string} modelName - e.g. "B-DFS1.2", "Master"
|
|
31
|
+
* @param {Object} [opts]
|
|
32
|
+
* @param {string} [opts.discImage] - path to an .ssd or .dsd disc image to load on boot
|
|
30
33
|
*/
|
|
31
|
-
constructor(modelName = "B-DFS1.2") {
|
|
34
|
+
constructor(modelName = "B-DFS1.2", opts = {}) {
|
|
32
35
|
this.modelName = modelName;
|
|
36
|
+
this._opts = opts;
|
|
33
37
|
|
|
34
38
|
// Raw RGBA framebuffer — the Video chip renders into _fb32 (cleared each frame).
|
|
35
39
|
// _completeFb8 is a snapshot taken at paint time (the equivalent of the browser canvas)
|
|
@@ -50,8 +54,11 @@ export class MachineSession {
|
|
|
50
54
|
this._completeFb8.set(this._fb8);
|
|
51
55
|
});
|
|
52
56
|
|
|
53
|
-
//
|
|
54
|
-
this.
|
|
57
|
+
// Use a real (instrumented) sound chip so we can read registers and capture writes
|
|
58
|
+
this._soundChip = new InstrumentedSoundChip();
|
|
59
|
+
|
|
60
|
+
// TestMachine forwards opts.video and opts.soundChip to fake6502
|
|
61
|
+
this._machine = new TestMachine(modelName, { video: this._video, soundChip: this._soundChip });
|
|
55
62
|
|
|
56
63
|
// Accumulated VDU text output — drained by callers
|
|
57
64
|
this._pendingOutput = [];
|
|
@@ -61,6 +68,9 @@ export class MachineSession {
|
|
|
61
68
|
async initialise() {
|
|
62
69
|
setNodeBasePath(_jsbeebRoot);
|
|
63
70
|
await this._machine.initialise();
|
|
71
|
+
if (this._opts.discImage) {
|
|
72
|
+
this.loadDisc(this._opts.discImage);
|
|
73
|
+
}
|
|
64
74
|
this._installCaptureHook();
|
|
65
75
|
}
|
|
66
76
|
|
|
@@ -201,6 +211,30 @@ export class MachineSession {
|
|
|
201
211
|
});
|
|
202
212
|
}
|
|
203
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Press a key (by browser keyCode).
|
|
216
|
+
* Use utils.keyCodes for named keys, or ASCII charCode for letters/digits.
|
|
217
|
+
*/
|
|
218
|
+
keyDown(keyCode, shiftDown = false) {
|
|
219
|
+
this._machine.processor.sysvia.keyDown(keyCode, shiftDown);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Release a key (by browser keyCode).
|
|
224
|
+
*/
|
|
225
|
+
keyUp(keyCode) {
|
|
226
|
+
this._machine.processor.sysvia.keyUp(keyCode);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Reset the machine.
|
|
231
|
+
* @param {boolean} [hard=true] - true for power-on reset, false for soft reset
|
|
232
|
+
*/
|
|
233
|
+
reset(hard = true) {
|
|
234
|
+
this._machine.processor.reset(hard);
|
|
235
|
+
this._pendingOutput = [];
|
|
236
|
+
}
|
|
237
|
+
|
|
204
238
|
/** Tokenise BBC BASIC source and write it into PAGE */
|
|
205
239
|
async loadBasic(source) {
|
|
206
240
|
await this._machine.loadBasic(source);
|