jsbeeb 1.6.0 → 1.8.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/README.md +22 -2
- package/package.json +3 -2
- package/src/6502.js +35 -6
- package/src/6502.opcodes.js +21 -8
- package/src/app/app.js +5 -0
- package/src/disc-drive.js +33 -0
- package/src/disc.js +176 -22
- package/src/fdc.js +3 -1
- package/src/intel-fdc.js +65 -0
- package/src/jsbeeb.css +66 -0
- package/src/main.js +120 -31
- package/src/rewind-thumbnail.js +118 -0
- package/src/rewind-ui.js +230 -0
- package/src/rewind.js +13 -0
- package/src/snapshot.js +38 -5
- package/src/soundchip.js +1 -5
- package/src/wd-fdc.js +99 -2
- package/tests/test-machine.js +189 -146
package/src/wd-fdc.js
CHANGED
|
@@ -1301,9 +1301,106 @@ export class WdFdc {
|
|
|
1301
1301
|
this._dataRegister = data;
|
|
1302
1302
|
}
|
|
1303
1303
|
|
|
1304
|
-
|
|
1304
|
+
snapshotState() {
|
|
1305
|
+
const scheduler = this._timerTask.scheduler;
|
|
1306
|
+
return {
|
|
1307
|
+
controlRegister: this._controlRegister,
|
|
1308
|
+
statusRegister: this._statusRegister,
|
|
1309
|
+
trackRegister: this._trackRegister,
|
|
1310
|
+
sectorRegister: this._sectorRegister,
|
|
1311
|
+
dataRegister: this._dataRegister,
|
|
1312
|
+
isIntRq: this._isIntRq,
|
|
1313
|
+
isDrq: this._isDrq,
|
|
1314
|
+
doRaiseIntRq: this._doRaiseIntRq,
|
|
1315
|
+
isIndexPulse: this._isIndexPulse,
|
|
1316
|
+
isInterruptOnIndexPulse: this._isInterruptOnIndexPulse,
|
|
1317
|
+
isWriteTrackCrcSecondByte: this._isWriteTrackCrcSecondByte,
|
|
1318
|
+
command: this._command,
|
|
1319
|
+
commandType: this._commandType,
|
|
1320
|
+
isCommandSettle: this._isCommandSettle,
|
|
1321
|
+
isCommandWrite: this._isCommandWrite,
|
|
1322
|
+
isCommandVerify: this._isCommandVerify,
|
|
1323
|
+
isCommandMulti: this._isCommandMulti,
|
|
1324
|
+
isCommandDeleted: this._isCommandDeleted,
|
|
1325
|
+
commandStepRateMs: this._commandStepRateMs,
|
|
1326
|
+
state: this._state,
|
|
1327
|
+
timerState: this._timerState,
|
|
1328
|
+
stateCount: this._stateCount,
|
|
1329
|
+
indexPulseCount: this._indexPulseCount,
|
|
1330
|
+
// BigInt must be converted to string for JSON serialization
|
|
1331
|
+
markDetector: this._markDetector.toString(),
|
|
1332
|
+
dataShifter: this._dataShifter,
|
|
1333
|
+
dataShiftCount: this._dataShiftCount,
|
|
1334
|
+
deliverData: this._deliverData,
|
|
1335
|
+
deliverIsMarker: this._deliverIsMarker,
|
|
1336
|
+
crc: this._crc,
|
|
1337
|
+
onDiscTrack: this._onDiscTrack,
|
|
1338
|
+
onDiscSector: this._onDiscSector,
|
|
1339
|
+
onDiscLength: this._onDiscLength,
|
|
1340
|
+
onDiscCrc: this._onDiscCrc,
|
|
1341
|
+
lastMfmBit: this._lastMfmBit,
|
|
1342
|
+
timerTaskOffset: this._timerTask.scheduled() ? this._timerTask.expireEpoch - scheduler.epoch : null,
|
|
1343
|
+
drives: this._drives.map((d) => d.snapshotState()),
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
restoreState(state) {
|
|
1348
|
+
this._controlRegister = state.controlRegister;
|
|
1349
|
+
this._statusRegister = state.statusRegister;
|
|
1350
|
+
this._trackRegister = state.trackRegister;
|
|
1351
|
+
this._sectorRegister = state.sectorRegister;
|
|
1352
|
+
this._dataRegister = state.dataRegister;
|
|
1353
|
+
this._isIntRq = state.isIntRq;
|
|
1354
|
+
this._isDrq = state.isDrq;
|
|
1355
|
+
this._doRaiseIntRq = state.doRaiseIntRq;
|
|
1356
|
+
this._isIndexPulse = state.isIndexPulse;
|
|
1357
|
+
this._isInterruptOnIndexPulse = state.isInterruptOnIndexPulse;
|
|
1358
|
+
this._isWriteTrackCrcSecondByte = state.isWriteTrackCrcSecondByte;
|
|
1359
|
+
this._command = state.command;
|
|
1360
|
+
this._commandType = state.commandType;
|
|
1361
|
+
this._isCommandSettle = state.isCommandSettle;
|
|
1362
|
+
this._isCommandWrite = state.isCommandWrite;
|
|
1363
|
+
this._isCommandVerify = state.isCommandVerify;
|
|
1364
|
+
this._isCommandMulti = state.isCommandMulti;
|
|
1365
|
+
this._isCommandDeleted = state.isCommandDeleted;
|
|
1366
|
+
this._commandStepRateMs = state.commandStepRateMs;
|
|
1367
|
+
this._state = state.state;
|
|
1368
|
+
this._timerState = state.timerState;
|
|
1369
|
+
this._stateCount = state.stateCount;
|
|
1370
|
+
this._indexPulseCount = state.indexPulseCount;
|
|
1371
|
+
this._markDetector = BigInt(state.markDetector);
|
|
1372
|
+
this._dataShifter = state.dataShifter;
|
|
1373
|
+
this._dataShiftCount = state.dataShiftCount;
|
|
1374
|
+
this._deliverData = state.deliverData;
|
|
1375
|
+
this._deliverIsMarker = state.deliverIsMarker;
|
|
1376
|
+
this._crc = state.crc;
|
|
1377
|
+
this._onDiscTrack = state.onDiscTrack;
|
|
1378
|
+
this._onDiscSector = state.onDiscSector;
|
|
1379
|
+
this._onDiscLength = state.onDiscLength;
|
|
1380
|
+
this._onDiscCrc = state.onDiscCrc;
|
|
1381
|
+
this._lastMfmBit = state.lastMfmBit;
|
|
1382
|
+
|
|
1383
|
+
// Restore drives
|
|
1384
|
+
for (let i = 0; i < this._drives.length; i++) {
|
|
1385
|
+
this._drives[i].restoreState(state.drives[i]);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// Derive _currentDrive from _controlRegister
|
|
1389
|
+
this._currentDrive = null;
|
|
1390
|
+
if (this._controlRegister & Control.drive0 || this._controlRegister & Control.drive1) {
|
|
1391
|
+
this._currentDrive = this._drives[this._controlRegister & Control.drive0 ? 0 : 1];
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Restore timer
|
|
1395
|
+
this._timerTask.cancel();
|
|
1396
|
+
if (state.timerTaskOffset !== null) this._timerTask.schedule(state.timerTaskOffset);
|
|
1397
|
+
|
|
1398
|
+
// NMI level is saved/restored by the CPU snapshot directly,
|
|
1399
|
+
// so we don't reassert it here.
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
/// jsbeeb compatibility stuff
|
|
1305
1403
|
/**
|
|
1306
|
-
*
|
|
1307
1404
|
* @param {Number} drive
|
|
1308
1405
|
* @param {Disc} disc
|
|
1309
1406
|
*/
|
package/tests/test-machine.js
CHANGED
|
@@ -30,13 +30,28 @@ export class TestMachine {
|
|
|
30
30
|
if (left && !stopped) {
|
|
31
31
|
setTimeout(runAnIter, 0);
|
|
32
32
|
} else {
|
|
33
|
-
resolve();
|
|
33
|
+
resolve(stopped);
|
|
34
34
|
}
|
|
35
35
|
};
|
|
36
36
|
runAnIter();
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Run until the cursor blink reaches the desired state.
|
|
42
|
+
* This ensures deterministic screenshots regardless of how many
|
|
43
|
+
* cycles were consumed by prior type() or runFor() calls.
|
|
44
|
+
* @param {boolean} on - true for cursor visible, false for hidden
|
|
45
|
+
*/
|
|
46
|
+
async runToCursorState(on) {
|
|
47
|
+
const video = this.processor.video;
|
|
48
|
+
for (let i = 0; i < 100; i++) {
|
|
49
|
+
if (video.cursorOnThisFrame === on) return;
|
|
50
|
+
await this.runFor(40000);
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`Cursor did not reach state ${on} in time (cursorOnThisFrame=${video.cursorOnThisFrame})`);
|
|
53
|
+
}
|
|
54
|
+
|
|
40
55
|
async runUntilVblank() {
|
|
41
56
|
let hit = false;
|
|
42
57
|
if (this.processor.isMaster) throw new Error("Not yet implemented");
|
|
@@ -87,6 +102,22 @@ export class TestMachine {
|
|
|
87
102
|
this.processor.fdc.loadDisc(0, fdc.discFor(this.processor.fdc, "", data));
|
|
88
103
|
}
|
|
89
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Load a disc image from raw data (Uint8Array or Buffer).
|
|
107
|
+
* @param {Uint8Array|Buffer} data - raw disc image bytes
|
|
108
|
+
*/
|
|
109
|
+
loadDiscData(data) {
|
|
110
|
+
this.processor.fdc.loadDisc(0, fdc.discFor(this.processor.fdc, "", data));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Reset the machine.
|
|
115
|
+
* @param {boolean} hard - true for power-on reset, false for soft reset
|
|
116
|
+
*/
|
|
117
|
+
reset(hard) {
|
|
118
|
+
this.processor.reset(hard);
|
|
119
|
+
}
|
|
120
|
+
|
|
90
121
|
async loadBasic(source) {
|
|
91
122
|
const tokeniser = await Tokeniser.create();
|
|
92
123
|
const tokenised = tokeniser.tokenise(source);
|
|
@@ -105,157 +136,169 @@ export class TestMachine {
|
|
|
105
136
|
this.writebyte(0x13, endHigh);
|
|
106
137
|
}
|
|
107
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Convert an ASCII character to a {code, shift} pair for the BBC keyboard.
|
|
141
|
+
*/
|
|
142
|
+
_charToKey(ch) {
|
|
143
|
+
switch (ch) {
|
|
144
|
+
case "\n":
|
|
145
|
+
case "\r":
|
|
146
|
+
return { code: 13, shift: false };
|
|
147
|
+
case '"':
|
|
148
|
+
return { code: utils.keyCodes.K2, shift: true };
|
|
149
|
+
case "*":
|
|
150
|
+
return { code: utils.keyCodes.APOSTROPHE, shift: true };
|
|
151
|
+
case "!":
|
|
152
|
+
return { code: utils.keyCodes.K1, shift: true };
|
|
153
|
+
case ".":
|
|
154
|
+
return { code: utils.keyCodes.PERIOD, shift: false };
|
|
155
|
+
case ";":
|
|
156
|
+
return { code: utils.keyCodes.SEMICOLON, shift: false };
|
|
157
|
+
case ":":
|
|
158
|
+
return { code: utils.keyCodes.APOSTROPHE, shift: false };
|
|
159
|
+
case ",":
|
|
160
|
+
return { code: utils.keyCodes.COMMA, shift: false };
|
|
161
|
+
case "&":
|
|
162
|
+
return { code: utils.keyCodes.K6, shift: true };
|
|
163
|
+
case " ":
|
|
164
|
+
return { code: utils.keyCodes.SPACE, shift: false };
|
|
165
|
+
case "-":
|
|
166
|
+
return { code: utils.keyCodes.MINUS, shift: false };
|
|
167
|
+
case "=":
|
|
168
|
+
return { code: utils.keyCodes.MINUS, shift: true };
|
|
169
|
+
case "+":
|
|
170
|
+
return { code: utils.keyCodes.SEMICOLON, shift: true };
|
|
171
|
+
case "^":
|
|
172
|
+
return { code: utils.keyCodes.EQUALS, shift: false };
|
|
173
|
+
case "~":
|
|
174
|
+
return { code: utils.keyCodes.EQUALS, shift: true };
|
|
175
|
+
case "[":
|
|
176
|
+
return { code: utils.keyCodes.LEFT_SQUARE_BRACKET, shift: false };
|
|
177
|
+
case "]":
|
|
178
|
+
return { code: utils.keyCodes.RIGHT_SQUARE_BRACKET, shift: false };
|
|
179
|
+
case "{":
|
|
180
|
+
return { code: utils.keyCodes.LEFT_SQUARE_BRACKET, shift: true };
|
|
181
|
+
case "}":
|
|
182
|
+
return { code: utils.keyCodes.HASH, shift: true };
|
|
183
|
+
case "\\":
|
|
184
|
+
return { code: utils.keyCodes.BACKSLASH, shift: false };
|
|
185
|
+
case "/":
|
|
186
|
+
return { code: utils.keyCodes.SLASH, shift: false };
|
|
187
|
+
case "?":
|
|
188
|
+
return { code: utils.keyCodes.SLASH, shift: true };
|
|
189
|
+
case "<":
|
|
190
|
+
return { code: utils.keyCodes.COMMA, shift: true };
|
|
191
|
+
case ">":
|
|
192
|
+
return { code: utils.keyCodes.PERIOD, shift: true };
|
|
193
|
+
case "(":
|
|
194
|
+
return { code: utils.keyCodes.K8, shift: true };
|
|
195
|
+
case ")":
|
|
196
|
+
return { code: utils.keyCodes.K9, shift: true };
|
|
197
|
+
case "@":
|
|
198
|
+
return { code: utils.keyCodes.BACK_QUOTE, shift: false };
|
|
199
|
+
case "#":
|
|
200
|
+
return { code: utils.keyCodes.K3, shift: true };
|
|
201
|
+
case "$":
|
|
202
|
+
return { code: utils.keyCodes.K4, shift: true };
|
|
203
|
+
case "%":
|
|
204
|
+
return { code: utils.keyCodes.K5, shift: true };
|
|
205
|
+
default: {
|
|
206
|
+
const upper = ch.toUpperCase();
|
|
207
|
+
const isLetter = (ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z");
|
|
208
|
+
if (isLetter) {
|
|
209
|
+
const wantUpper = ch >= "A" && ch <= "Z";
|
|
210
|
+
const capsOn = this.processor.sysvia.capsLockLight;
|
|
211
|
+
// CAPS LOCK on: unshifted = upper, shifted = lower
|
|
212
|
+
// CAPS LOCK off: unshifted = lower, shifted = upper
|
|
213
|
+
const needShift = capsOn ? !wantUpper : wantUpper;
|
|
214
|
+
return { code: upper.charCodeAt(0), shift: needShift };
|
|
215
|
+
}
|
|
216
|
+
return { code: ch.charCodeAt(0), shift: false };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Type text by installing a debugInstruction hook that presses/releases
|
|
223
|
+
* keys at timed intervals during CPU execution. The hook persists across
|
|
224
|
+
* runFor calls, so breakpoints naturally coexist: if a breakpoint halts
|
|
225
|
+
* execution mid-typing, the remaining characters are typed when execution
|
|
226
|
+
* resumes.
|
|
227
|
+
*/
|
|
108
228
|
async type(text) {
|
|
109
|
-
|
|
110
|
-
const
|
|
229
|
+
const fullText = text + "\n"; // append RETURN
|
|
230
|
+
const keys = fullText.split("").map((ch) => this._charToKey(ch));
|
|
231
|
+
const holdCycles = 40000;
|
|
232
|
+
let index = 0;
|
|
233
|
+
let phase = "idle"; // "idle" → "down" → "idle"
|
|
234
|
+
let nextEventCycle = 0;
|
|
235
|
+
let done = false;
|
|
111
236
|
|
|
112
|
-
const
|
|
113
|
-
this.processor.sysvia.keyDown(ch);
|
|
114
|
-
return this.runFor(cycles).then(() => {
|
|
115
|
-
this.processor.sysvia.keyUp(ch);
|
|
116
|
-
return this.runFor(cycles);
|
|
117
|
-
});
|
|
118
|
-
};
|
|
237
|
+
const currentCycle = () => this.processor.cycleSeconds * 2000000 + this.processor.currentCycles;
|
|
119
238
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
shift = true;
|
|
133
|
-
break;
|
|
134
|
-
case "!":
|
|
135
|
-
ch = utils.keyCodes.K1;
|
|
136
|
-
shift = true;
|
|
137
|
-
break;
|
|
138
|
-
case ".":
|
|
139
|
-
ch = utils.keyCodes.PERIOD;
|
|
140
|
-
break;
|
|
141
|
-
case ";":
|
|
142
|
-
ch = utils.keyCodes.SEMICOLON;
|
|
143
|
-
break;
|
|
144
|
-
case ":":
|
|
145
|
-
ch = utils.keyCodes.APOSTROPHE;
|
|
146
|
-
break;
|
|
147
|
-
case ",":
|
|
148
|
-
ch = utils.keyCodes.COMMA;
|
|
149
|
-
break;
|
|
150
|
-
case "&":
|
|
151
|
-
ch = utils.keyCodes.K6;
|
|
152
|
-
shift = true;
|
|
153
|
-
break;
|
|
154
|
-
case " ":
|
|
155
|
-
ch = utils.keyCodes.SPACE;
|
|
156
|
-
break;
|
|
157
|
-
case "-":
|
|
158
|
-
ch = utils.keyCodes.MINUS;
|
|
159
|
-
break;
|
|
160
|
-
case "=":
|
|
161
|
-
ch = utils.keyCodes.MINUS;
|
|
162
|
-
shift = true;
|
|
163
|
-
break;
|
|
164
|
-
case "+":
|
|
165
|
-
ch = utils.keyCodes.SEMICOLON;
|
|
166
|
-
shift = true;
|
|
167
|
-
break;
|
|
168
|
-
case "^":
|
|
169
|
-
ch = utils.keyCodes.EQUALS;
|
|
170
|
-
break;
|
|
171
|
-
case "~":
|
|
172
|
-
ch = utils.keyCodes.EQUALS;
|
|
173
|
-
shift = true;
|
|
174
|
-
break;
|
|
175
|
-
case "[":
|
|
176
|
-
ch = utils.keyCodes.LEFT_SQUARE_BRACKET;
|
|
177
|
-
break;
|
|
178
|
-
case "]":
|
|
179
|
-
ch = utils.keyCodes.RIGHT_SQUARE_BRACKET;
|
|
180
|
-
break;
|
|
181
|
-
case "{":
|
|
182
|
-
ch = utils.keyCodes.LEFT_SQUARE_BRACKET;
|
|
183
|
-
shift = true;
|
|
184
|
-
break;
|
|
185
|
-
case "}":
|
|
186
|
-
ch = utils.keyCodes.HASH;
|
|
187
|
-
shift = true;
|
|
188
|
-
break;
|
|
189
|
-
case "\\":
|
|
190
|
-
ch = utils.keyCodes.BACKSLASH;
|
|
191
|
-
break;
|
|
192
|
-
case "/":
|
|
193
|
-
ch = utils.keyCodes.SLASH;
|
|
194
|
-
break;
|
|
195
|
-
case "?":
|
|
196
|
-
ch = utils.keyCodes.SLASH;
|
|
197
|
-
shift = true;
|
|
198
|
-
break;
|
|
199
|
-
case "<":
|
|
200
|
-
ch = utils.keyCodes.COMMA;
|
|
201
|
-
shift = true;
|
|
202
|
-
break;
|
|
203
|
-
case ">":
|
|
204
|
-
ch = utils.keyCodes.PERIOD;
|
|
205
|
-
shift = true;
|
|
206
|
-
break;
|
|
207
|
-
case "(":
|
|
208
|
-
ch = utils.keyCodes.K8;
|
|
209
|
-
shift = true;
|
|
210
|
-
break;
|
|
211
|
-
case ")":
|
|
212
|
-
ch = utils.keyCodes.K9;
|
|
213
|
-
shift = true;
|
|
214
|
-
break;
|
|
215
|
-
case "@":
|
|
216
|
-
ch = utils.keyCodes.BACK_QUOTE;
|
|
217
|
-
break;
|
|
218
|
-
case "#":
|
|
219
|
-
ch = utils.keyCodes.K3;
|
|
220
|
-
shift = true;
|
|
221
|
-
break;
|
|
222
|
-
case "$":
|
|
223
|
-
ch = utils.keyCodes.K4;
|
|
224
|
-
shift = true;
|
|
225
|
-
break;
|
|
226
|
-
case "%":
|
|
227
|
-
ch = utils.keyCodes.K5;
|
|
228
|
-
shift = true;
|
|
229
|
-
break;
|
|
230
|
-
default:
|
|
231
|
-
// A-Z and 0-9: ASCII value == DOM keyCode — works as-is.
|
|
232
|
-
// Anything else (e.g. '_', '£', '`') is not yet mapped.
|
|
233
|
-
ch = ch.toUpperCase().charCodeAt(0);
|
|
234
|
-
break;
|
|
239
|
+
const hook = this.processor.debugInstruction.add(() => {
|
|
240
|
+
if (currentCycle() < nextEventCycle) return;
|
|
241
|
+
|
|
242
|
+
if (phase === "down") {
|
|
243
|
+
// Release current key
|
|
244
|
+
const key = keys[index];
|
|
245
|
+
this.processor.sysvia.keyUp(key.code);
|
|
246
|
+
if (key.shift) this.processor.sysvia.keyUp(16);
|
|
247
|
+
index++;
|
|
248
|
+
phase = "idle";
|
|
249
|
+
nextEventCycle = currentCycle() + holdCycles;
|
|
250
|
+
return;
|
|
235
251
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
} else {
|
|
245
|
-
return kd(ch);
|
|
252
|
+
|
|
253
|
+
// phase === "idle"
|
|
254
|
+
if (index >= keys.length) {
|
|
255
|
+
hook.remove();
|
|
256
|
+
done = true;
|
|
257
|
+
return;
|
|
246
258
|
}
|
|
247
|
-
};
|
|
248
259
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
260
|
+
// Press next key
|
|
261
|
+
const key = keys[index];
|
|
262
|
+
if (key.shift) this.processor.sysvia.keyDown(16);
|
|
263
|
+
this.processor.sysvia.keyDown(key.code);
|
|
264
|
+
phase = "down";
|
|
265
|
+
nextEventCycle = currentCycle() + holdCycles;
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Drive execution in chunks until all characters are typed or
|
|
269
|
+
// a breakpoint halts the CPU.
|
|
270
|
+
while (!done) {
|
|
271
|
+
const stopped = await this.runFor(holdCycles);
|
|
272
|
+
if (stopped) break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Press a key on the BBC keyboard.
|
|
278
|
+
* @param {number} code - BBC key code (e.g. utils.keyCodes.SPACE)
|
|
279
|
+
*/
|
|
280
|
+
keyDown(code) {
|
|
281
|
+
this.processor.sysvia.keyDown(code);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Release a key on the BBC keyboard.
|
|
286
|
+
* @param {number} code - BBC key code
|
|
287
|
+
*/
|
|
288
|
+
keyUp(code) {
|
|
289
|
+
this.processor.sysvia.keyUp(code);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Load a ROM image directly into a sideways RAM slot.
|
|
294
|
+
* @param {number} slot - slot number (0-15, typically 4-7 for SWRAM)
|
|
295
|
+
* @param {Uint8Array|Buffer} data - ROM data (up to 16384 bytes)
|
|
296
|
+
*/
|
|
297
|
+
loadSidewaysRam(slot, data) {
|
|
298
|
+
const offset = this.processor.romOffset + slot * 16384;
|
|
299
|
+
for (let i = 0; i < data.length && i < 16384; i++) {
|
|
300
|
+
this.processor.ramRomOs[offset + i] = data[i];
|
|
301
|
+
}
|
|
259
302
|
}
|
|
260
303
|
|
|
261
304
|
writebyte(addr, val) {
|