jsbeeb 1.14.0 → 1.16.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 +94 -0
- package/package.json +8 -6
- package/public/roms/tube/65C102Tube.rom +0 -0
- package/src/6502.js +22 -16
- package/src/6847.js +50 -8
- package/src/acia.js +31 -12
- package/src/canvas.js +77 -18
- package/src/config.js +16 -4
- package/src/disc-drive.js +9 -0
- package/src/disc-hfe.js +1 -2
- package/src/disc-surface.js +350 -0
- package/src/disc-visualiser.js +569 -0
- package/src/disc.js +143 -49
- package/src/dom-utils.js +16 -0
- package/src/econet.js +6 -2
- package/src/fake6502.js +2 -2
- package/src/gamepads.js +11 -4
- package/src/jsbeeb.css +126 -0
- package/src/main.js +112 -55
- package/src/models.js +47 -5
- package/src/serial.js +1 -0
- package/src/snapshot-helpers.js +1 -1
- package/src/sth.js +21 -18
- package/src/tapes.js +10 -12
- package/src/teletext_adaptor.js +38 -13
- package/src/touchscreen.js +6 -6
- package/src/tube.js +89 -37
- package/src/url-params.js +23 -19
- package/src/utils.js +10 -5
- package/src/utils_atom.js +9 -5
- package/src/video-filters/pal-composite.js +14 -48
- package/src/video-filters/passthrough-filter.js +11 -39
- package/src/video-filters/pixel-grid.js +55 -0
- package/src/video-filters/shader-program.js +53 -0
- package/src/video-filters/shaders/xbr.frag.glsl +278 -0
- package/src/video-filters/shaders/xbr.vert.glsl +7 -0
- package/src/video-filters/xbr-filter.js +107 -0
- package/src/video.js +56 -16
- package/src/wd-fdc.js +17 -7
- package/tests/test-machine.js +7 -5
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ different peripherals.
|
|
|
11
11
|
## Table of Contents
|
|
12
12
|
|
|
13
13
|
- [Keyboard Mappings](#keyboard-mappings)
|
|
14
|
+
- [Remapping Keys](#remapping-keys)
|
|
14
15
|
- [Emulator Shortcuts](#emulator-shortcuts)
|
|
15
16
|
- [Save State and Rewind](#save-state-and-rewind)
|
|
16
17
|
- [Getting Set Up to Run Locally](#getting-set-up-to-run-locally)
|
|
@@ -36,6 +37,65 @@ The BBC had a somewhat different-looking keyboard to a modern PC, and so it's us
|
|
|
36
37
|
To play right now, visit [https://bbc.xania.org/](https://bbc.xania.org/). To load the default disc image (Elite in this
|
|
37
38
|
case), press shift-F12 (which is shift-Break on the BBC).
|
|
38
39
|
|
|
40
|
+
### Remapping Keys
|
|
41
|
+
|
|
42
|
+
Plenty of games use keys that are awkward on a modern keyboard: `COPY` (which is `End`, or `fn`+`→` on a Mac), or
|
|
43
|
+
`CAPS LOCK` (which on a Mac toggles rather than acting as a key you hold down). Any host key can be made to press any
|
|
44
|
+
BBC key by adding a `KEY.` parameter to the URL:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
KEY.<host key>=<BBC key>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Add one for each key you want to change. For example, Superior Software's Space Invaders fires with `COPY`; this makes
|
|
51
|
+
`Enter` fire instead:
|
|
52
|
+
|
|
53
|
+
[`https://bbc.xania.org/?disc1=sth:Superior/SpaceInvaders-Superior.zip&autoboot&KEY.ENTER=COPY`](https://bbc.xania.org/?disc1=sth%3ASuperior%2FSpaceInvaders-Superior.zip&autoboot&KEY.ENTER=COPY)
|
|
54
|
+
|
|
55
|
+
Superior's Frogger uses `A`/`Z`/`DELETE`/`COPY` to move; this puts it on the arrow keys:
|
|
56
|
+
|
|
57
|
+
[`https://bbc.xania.org/?disc1=sth:Superior/Frogger-Superior.zip&autoboot&KEY.UP=A&KEY.DOWN=Z&KEY.LEFT=DELETE&KEY.RIGHT=COPY`](https://bbc.xania.org/?disc1=sth%3ASuperior%2FFrogger-Superior.zip&autoboot&KEY.UP=A&KEY.DOWN=Z&KEY.LEFT=DELETE&KEY.RIGHT=COPY)
|
|
58
|
+
|
|
59
|
+
And Superior's Hunchback steers with `CAPS LOCK` and `CTRL`, which the arrow keys can stand in for:
|
|
60
|
+
|
|
61
|
+
[`https://bbc.xania.org/?disc1=sth:Superior/Hunchback-Superior.zip&autoboot&KEY.LEFT=CAPSLOCK&KEY.RIGHT=CTRL`](https://bbc.xania.org/?disc1=sth%3ASuperior%2FHunchback-Superior.zip&autoboot&KEY.LEFT=CAPSLOCK&KEY.RIGHT=CTRL)
|
|
62
|
+
|
|
63
|
+
The **host key** names are jsbeeb's names for the keys on your own keyboard. Most are what you'd expect, but note:
|
|
64
|
+
|
|
65
|
+
- `ENTER` (the BBC's `RETURN` key is called `ENTER` on the host side)
|
|
66
|
+
- `K0` to `K9` for the number keys, `NUMPAD0` to `NUMPAD9` for the keypad
|
|
67
|
+
- `SHIFT_LEFT` / `SHIFT_RIGHT`, `CTRL_LEFT` / `CTRL_RIGHT`, `ALT_LEFT` / `ALT_RIGHT` to distinguish the two of each
|
|
68
|
+
- `BACK_QUOTE`, `APOSTROPHE`, `SEMICOLON`, `MINUS`, `EQUALS`, `HASH`, `BACKSLASH`, `LEFT_SQUARE_BRACKET`,
|
|
69
|
+
`RIGHT_SQUARE_BRACKET` for punctuation
|
|
70
|
+
|
|
71
|
+
The **BBC key** names are:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
RETURN COPY DELETE ESCAPE TAB SPACE SHIFT SHIFTLOCK CAPSLOCK CTRL
|
|
75
|
+
LEFT RIGHT UP DOWN
|
|
76
|
+
A-Z, K0-K9 (the number keys), F0-F9 (the red function keys)
|
|
77
|
+
SEMICOLON_PLUS MINUS COMMA PERIOD SLASH AT COLON_STAR HAT_TILDE
|
|
78
|
+
UNDERSCORE_POUND PIPE_BACKSLASH LEFT_SQUARE_BRACKET RIGHT_SQUARE_BRACKET
|
|
79
|
+
|
|
80
|
+
(and, on the Master's numeric keypad only)
|
|
81
|
+
NUMPAD0-NUMPAD9 NUMPADPLUS NUMPADMINUS NUMPADSLASH NUMPADASTERISK NUMPADCOMMA
|
|
82
|
+
NUMPADHASH NUMPADENTER NUMPAD_DELETE NUMPAD_DECIMAL_POINT
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Some things to know:
|
|
86
|
+
|
|
87
|
+
- Names are case-insensitive, and a remapped key ignores the `SHIFT` state, so `KEY.ENTER=COPY` presses `COPY` whether
|
|
88
|
+
or not shift is held.
|
|
89
|
+
- Remapping replaces what that host key normally does; in the Space Invaders example above, `Enter` no longer presses
|
|
90
|
+
`RETURN`.
|
|
91
|
+
- The remapping is applied on top of whichever keyboard layout is selected, and survives changing layout or model.
|
|
92
|
+
- If a name isn't recognised the mapping is skipped, and the emulator says so on startup, naming the parameter that
|
|
93
|
+
was at fault.
|
|
94
|
+
- On the Atom, use the Atom's own key names (`LOCK`, `UP_DOWN`, `LEFT_RIGHT` and so on) rather than the BBC's.
|
|
95
|
+
|
|
96
|
+
The definitive lists are `keyCodes` (host) and `BBC` (BBC micro) in [`src/utils.js`](src/utils.js), and `ATOM` in
|
|
97
|
+
[`src/utils_atom.js`](src/utils_atom.js).
|
|
98
|
+
|
|
39
99
|
### Emulator Shortcuts
|
|
40
100
|
|
|
41
101
|
| Shortcut | Action |
|
|
@@ -63,6 +123,35 @@ jsbeeb supports both USB/Bluetooth gamepads and mouse-based analogue joystick em
|
|
|
63
123
|
- X-axis: Left = 65535, Right = 0
|
|
64
124
|
- Y-axis: Up = 65535, Down = 0
|
|
65
125
|
|
|
126
|
+
A gamepad presses BBC keys, and which key each control presses can be changed from the URL in the same way as
|
|
127
|
+
[remapping the keyboard](#remapping-keys):
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
GP.<gamepad control>=<BBC key>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
By default the D-pad presses the "Snapper" keys (`Z`, `X`, `:`, `/`), the `A` button presses `RETURN` and `Start`
|
|
134
|
+
presses `SPACE`. To play Superior's Space Invaders on a pad, where `COPY` fires:
|
|
135
|
+
|
|
136
|
+
[`https://bbc.xania.org/?disc1=sth:Superior/SpaceInvaders-Superior.zip&autoboot&GP.FIRE=COPY`](https://bbc.xania.org/?disc1=sth%3ASuperior%2FSpaceInvaders-Superior.zip&autoboot&GP.FIRE=COPY)
|
|
137
|
+
|
|
138
|
+
The gamepad control names are:
|
|
139
|
+
|
|
140
|
+
- `FIRE` — every button at once, which is usually what you want for a one-button game
|
|
141
|
+
- `UP` `DOWN` `LEFT` `RIGHT` — both analogue sticks at once, plus one face button each (`UP` is also `A`, `DOWN` is
|
|
142
|
+
`X`, `LEFT` is `Y`, `RIGHT` is `B`)
|
|
143
|
+
- `UP1` `DOWN1` `LEFT1` `RIGHT1` — the left stick only; `UP2` `DOWN2` … — the right stick only; `UP3` `DOWN3` … — the
|
|
144
|
+
face buttons only
|
|
145
|
+
- `A` `B` `X` `Y` `START` `BACK` `LB` `RB` `LT` `RT` — individual buttons, by their Xbox 360 names
|
|
146
|
+
- `FIRE1` `FIRE2` — clicking the left and right sticks
|
|
147
|
+
|
|
148
|
+
The BBC key names are the same as for the keyboard, and digits may be written either way round: `GP.A=1` and `GP.A=K1`
|
|
149
|
+
both press `1`. Unlike `KEY.`, gamepad mappings are BBC-only: there's no Atom equivalent. The D-pad's default mapping
|
|
150
|
+
can't currently be changed.
|
|
151
|
+
|
|
152
|
+
The older `LEFT=`, `RIGHT=`, `UP=`, `DOWN=` and `FIRE=` parameters (no `GP.` prefix) still work and mean the same
|
|
153
|
+
thing.
|
|
154
|
+
|
|
66
155
|
## Getting Set Up to Run Locally
|
|
67
156
|
|
|
68
157
|
### Prerequisites
|
|
@@ -157,6 +246,8 @@ sudo rpm -i out/dist/jsbeeb-1.0.1.x86_64.rpm
|
|
|
157
246
|
- `disc1=sth:ZZZ` - loads disc ZZZ from the Stairway to Hell archive
|
|
158
247
|
- `tape=XXX` - loads tape XXX (from the `tapes/` directory)
|
|
159
248
|
- `tape=sth:ZZZ` - loads tape ZZZ from the Stairway to Hell archive
|
|
249
|
+
- `KEY.X=Y` - makes host key `X` press BBC key `Y`, e.g. `KEY.ENTER=COPY`. See
|
|
250
|
+
[Remapping Keys](#remapping-keys).
|
|
160
251
|
- `patch=P` - applies a memory patch `P`. See below.
|
|
161
252
|
- `loadBasic=X` - loads 'X' (a resource on the webserver) as text, tokenises it and puts it in `PAGE` as if you'd typed
|
|
162
253
|
it in to the emulator
|
|
@@ -170,6 +261,9 @@ sudo rpm -i out/dist/jsbeeb-1.0.1.x86_64.rpm
|
|
|
170
261
|
- `cpuMultiplier=X` speeds up the CPU by a factor of `X` relative to the peripherals: video, sound and the VIAs keep
|
|
171
262
|
running at their real-world rates. May be fractional or below one to slow the CPU down. NB disc loads become
|
|
172
263
|
unreliable with a too-slow CPU, and running too fast might cause the browser to hang.
|
|
264
|
+
- `tubeCpuMultiplier=X` overclocks the second processor by a factor of `X`, which may be fractional. `1`, the default,
|
|
265
|
+
runs it at the real part's own clock: 3MHz for the 6502 second processor a BBC B takes, 4MHz for the 65C102 Turbo
|
|
266
|
+
board a Master takes. Below about 2.2MHz the MOS's unhandshaken tube transfers lose data.
|
|
173
267
|
- `sbLeft` / `sbRight` / `sbBottom` - a URL to place left of, right of, or below the cub monitor. The left and right
|
|
174
268
|
should be around 648 high and the bottom image should be around 896 wide. Left and right wider than 300 will run into
|
|
175
269
|
problems on smaller screens; bottom taller than 100 or so similarly.
|
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.16.0",
|
|
11
11
|
"//engines": "If you change the version of Node, it must also be updated at the top of the Dockerfile.",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=24.15.0"
|
|
@@ -54,10 +54,6 @@
|
|
|
54
54
|
"electron-builder": "^26.15.3",
|
|
55
55
|
"electron-store": "^11.0.2"
|
|
56
56
|
},
|
|
57
|
-
"//overrides": "brace-expansion forced to the patched version for GHSA-mh99-v99m-4gvg; the electron-builder dependency chain still pins vulnerable versions. Remove once `npm audit` is clean without it.",
|
|
58
|
-
"overrides": {
|
|
59
|
-
"brace-expansion": "^5.0.8"
|
|
60
|
-
},
|
|
61
57
|
"license": "GPL-3.0-or-later",
|
|
62
58
|
"build": {
|
|
63
59
|
"appId": "org.godbolt.bbc",
|
|
@@ -99,15 +95,21 @@
|
|
|
99
95
|
"test:unit": "vitest run --silent=true tests/unit",
|
|
100
96
|
"test:integration": "vitest run --silent=true tests/integration",
|
|
101
97
|
"test:cpu": "node tests/test-suite.js",
|
|
98
|
+
"test:shader": "vitest run --silent=true tests/shader",
|
|
102
99
|
"coverage:unit": "vitest run tests/unit --coverage",
|
|
103
100
|
"coverage:all-tests": "vitest run --coverage",
|
|
104
101
|
"benchmark": "node src/app-bench.js",
|
|
102
|
+
"mirror-sth": "node tools/mirror-sth.js --out .sth-mirror",
|
|
103
|
+
"mirror-sth:check": "node tools/mirror-sth.js --index-only",
|
|
104
|
+
"mirror-sth:upload": "npm-run-all mirror-sth:upload:*",
|
|
105
|
+
"mirror-sth:upload:blobs": "aws s3 sync .sth-mirror s3://bbc.xania.org/archive/sth/ --no-progress --exclude '*manifest.json' --exclude 'meta/*' --cache-control 'public, max-age=31536000, immutable'",
|
|
106
|
+
"mirror-sth:upload:index": "aws s3 sync .sth-mirror s3://bbc.xania.org/archive/sth/ --no-progress --exclude '*' --include '*manifest.json' --include 'meta/*' --cache-control 'public, max-age=300'",
|
|
105
107
|
"electron": "npm run build && ELECTRON_DISABLE_SANDBOX=1 electron .",
|
|
106
108
|
"electron:build": "electron-builder"
|
|
107
109
|
},
|
|
108
110
|
"lint-staged": {
|
|
109
111
|
"*.js": [
|
|
110
|
-
"vitest related --run --exclude tests/integration --no-file-parallelism",
|
|
112
|
+
"vitest related --run --exclude tests/integration --exclude tests/shader --no-file-parallelism",
|
|
111
113
|
"eslint --cache --fix",
|
|
112
114
|
"prettier --write"
|
|
113
115
|
],
|
|
Binary file
|
package/src/6502.js
CHANGED
|
@@ -15,9 +15,6 @@ import { AtomMMC2 } from "./mmc.js";
|
|
|
15
15
|
|
|
16
16
|
const signExtend = utils.signExtend;
|
|
17
17
|
|
|
18
|
-
// Speed of the second processor relative to the host, as fitted to a Master Turbo.
|
|
19
|
-
export const DefaultTubeCpuMultiplier = 2;
|
|
20
|
-
|
|
21
18
|
function _set(byte, mask, set) {
|
|
22
19
|
return (byte & ~mask) | (set ? mask : 0);
|
|
23
20
|
}
|
|
@@ -216,6 +213,9 @@ class Base6502 {
|
|
|
216
213
|
if (this._nmiLevel && !prevLevel) this._nmiEdge = true;
|
|
217
214
|
}
|
|
218
215
|
|
|
216
|
+
/** Called as the NMI vector is taken, for devices that withdraw their request at that point. */
|
|
217
|
+
nmiAcknowledged() {}
|
|
218
|
+
|
|
219
219
|
polltime() {
|
|
220
220
|
throw new Error("Must be overridden");
|
|
221
221
|
}
|
|
@@ -242,6 +242,7 @@ class Base6502 {
|
|
|
242
242
|
if ((this.model.nmos || isIrq) && this._nmiEdge) {
|
|
243
243
|
vector = 0xfffa;
|
|
244
244
|
this._nmiEdge = false;
|
|
245
|
+
this.nmiAcknowledged();
|
|
245
246
|
}
|
|
246
247
|
this.takeInt = false;
|
|
247
248
|
this.pc = this.readmem(vector) | (this.readmem(vector + 1) << 8);
|
|
@@ -410,10 +411,13 @@ class Base6502 {
|
|
|
410
411
|
}
|
|
411
412
|
|
|
412
413
|
class Tube6502 extends Base6502 {
|
|
413
|
-
constructor(model, cpu, { cpuMultiplier =
|
|
414
|
+
constructor(model, cpu, { cpuMultiplier = 1 } = {}) {
|
|
414
415
|
super(model, { cycleAccurate: false });
|
|
415
416
|
|
|
417
|
+
if (!(cpuMultiplier > 0)) throw new Error(`Tube CPU multiplier must be positive, got ${cpuMultiplier}`);
|
|
418
|
+
|
|
416
419
|
this.cycles = 0;
|
|
420
|
+
this.cyclesPerHostCycle = model.clockMhz / cpu.model.clockMhz;
|
|
417
421
|
this.cpuMultiplier = cpuMultiplier;
|
|
418
422
|
this.romPaged = true;
|
|
419
423
|
this.memory = new Uint8Array(65536);
|
|
@@ -451,6 +455,10 @@ class Tube6502 extends Base6502 {
|
|
|
451
455
|
return this.memory[offset & 0xffff];
|
|
452
456
|
}
|
|
453
457
|
|
|
458
|
+
nmiAcknowledged() {
|
|
459
|
+
this.tube.acknowledgeNmi();
|
|
460
|
+
}
|
|
461
|
+
|
|
454
462
|
writemem(addr, b) {
|
|
455
463
|
if ((addr & 0xfff8) === 0xfef8) {
|
|
456
464
|
return this.tube.parasiteWrite(addr, b);
|
|
@@ -479,7 +487,7 @@ class Tube6502 extends Base6502 {
|
|
|
479
487
|
}
|
|
480
488
|
|
|
481
489
|
execute(cycles) {
|
|
482
|
-
this.cycles +=
|
|
490
|
+
this.cycles += cycles * this.cyclesPerHostCycle * this.cpuMultiplier;
|
|
483
491
|
if (this.cycles < 3) return;
|
|
484
492
|
while (this.cycles > 0) {
|
|
485
493
|
const opcode = this.readmem(this.pc);
|
|
@@ -666,10 +674,11 @@ export class Cpu6502 extends Base6502 {
|
|
|
666
674
|
this.resetLine = true;
|
|
667
675
|
this.cpuMultiplier = this.config.cpuMultiplier;
|
|
668
676
|
this.videoCyclesBatch = this.config.videoCyclesBatch | 0;
|
|
669
|
-
this.peripheralCyclesPerSecond =
|
|
677
|
+
this.peripheralCyclesPerSecond = model.cyclesPerSecond;
|
|
670
678
|
this.hasTube = !!this.config.tube;
|
|
671
679
|
this.hasMusic5000 = !!this.config.hasMusic5000;
|
|
672
680
|
this.hasTeletextAdaptor = !!this.config.hasTeletextAdaptor;
|
|
681
|
+
this.teletextAdaptor = this.hasTeletextAdaptor ? new TeletextAdaptor(this) : null;
|
|
673
682
|
this.tube = this.hasTube
|
|
674
683
|
? new Tube6502(this.config.tube, this, { cpuMultiplier: this.config.tubeCpuMultiplier })
|
|
675
684
|
: new FakeTube();
|
|
@@ -697,7 +706,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
697
706
|
getGamepads: this.config.getGamepads,
|
|
698
707
|
});
|
|
699
708
|
this.uservia = new via.UserVia(this, this.scheduler, this.model.isMaster, this.config.userPort);
|
|
700
|
-
this.acia = new Acia(this, this.soundChip.toneGenerator, this.scheduler, this.
|
|
709
|
+
this.acia = new Acia(this, this.soundChip.toneGenerator, this.scheduler, this.relayNoise);
|
|
701
710
|
this.serial = new Serial(this.acia);
|
|
702
711
|
this.adconverter = new Adc(this.sysvia, this.scheduler);
|
|
703
712
|
this.soundChip.setScheduler(this.scheduler);
|
|
@@ -1360,8 +1369,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
1360
1369
|
this.fdc.powerOnReset();
|
|
1361
1370
|
this.adconverter.reset();
|
|
1362
1371
|
|
|
1363
|
-
this.touchScreen = new TouchScreen(this.scheduler);
|
|
1364
|
-
if (this.hasTeletextAdaptor) this.teletextAdaptor = new TeletextAdaptor(this);
|
|
1372
|
+
this.touchScreen = new TouchScreen(this.scheduler, this.model.cyclesPerSecond);
|
|
1365
1373
|
if (this.econet) this.filestore = new Filestore(this, this.econet);
|
|
1366
1374
|
}
|
|
1367
1375
|
|
|
@@ -1422,8 +1430,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
1422
1430
|
buildPolltime() {
|
|
1423
1431
|
const nop = (_cycles) => {};
|
|
1424
1432
|
const tubeStuff = this.hasTube ? (cycles) => this.tube.execute(cycles) : nop;
|
|
1425
|
-
|
|
1426
|
-
const teletextStuff = this.hasTeletextAdaptor ? (cycles) => this.teletextAdaptor.polltime(cycles) : nop;
|
|
1433
|
+
const teletextStuff = this.teletextAdaptor ? (cycles) => this.teletextAdaptor.polltime(cycles) : nop;
|
|
1427
1434
|
const musicStuff = this.music5000 ? (cycles) => this.music5000.polltime(cycles) : nop;
|
|
1428
1435
|
const econetStuff = this.econet
|
|
1429
1436
|
? (cycles) => {
|
|
@@ -1479,9 +1486,10 @@ export class Cpu6502 extends Base6502 {
|
|
|
1479
1486
|
// that from both, to keep the domain low (while accumulating seconds). Take care to preserve the bottom
|
|
1480
1487
|
// bit though; as that encodes whether we're on an even or odd bus cycle.
|
|
1481
1488
|
const smaller = Math.min(this.targetCycles, this.currentCycles) & 0xfffffffe;
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
this.
|
|
1489
|
+
const cyclesPerSecond = this.model.cyclesPerSecond;
|
|
1490
|
+
if (smaller >= cyclesPerSecond) {
|
|
1491
|
+
this.targetCycles -= cyclesPerSecond;
|
|
1492
|
+
this.currentCycles -= cyclesPerSecond;
|
|
1485
1493
|
this.cycleSeconds++;
|
|
1486
1494
|
}
|
|
1487
1495
|
// Any tracing or debugging means we need to run the potentially slower version: the debug read or
|
|
@@ -1615,8 +1623,6 @@ export class AtomCpu6502 extends Cpu6502 {
|
|
|
1615
1623
|
this.ramRomOs = expanded;
|
|
1616
1624
|
}
|
|
1617
1625
|
|
|
1618
|
-
// Atom runs at 1 MHz
|
|
1619
|
-
this.peripheralCyclesPerSecond = 1 * 1000 * 1000;
|
|
1620
1626
|
// reset() and debugger.setCpu() are called by initialise() after loadOs().
|
|
1621
1627
|
}
|
|
1622
1628
|
|
package/src/6847.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
import * as utils from "./utils.js";
|
|
3
3
|
import * as fontData from "./6847_fontdata.js";
|
|
4
|
+
import { encodeLineGrid } from "./video-filters/pixel-grid.js";
|
|
4
5
|
|
|
5
6
|
const VDISPENABLE = 1 << 0,
|
|
6
7
|
HDISPENABLE = 1 << 1,
|
|
@@ -111,6 +112,11 @@ only 1 bit is used of SG6 - to get yellow/red, cyan/orange
|
|
|
111
112
|
// constant for the graphics mode
|
|
112
113
|
const MODE_AG = 0x10; // graphics mode
|
|
113
114
|
|
|
115
|
+
// The MC6847 datasheet specifies 3.579545 MHz, but 3.638004 is used here as an
|
|
116
|
+
// empirical correction to align VSync/interrupt timing with the CPU clock.
|
|
117
|
+
// TODO: revisit once full integration is testable.
|
|
118
|
+
const VdgClockMhz = 3.638004;
|
|
119
|
+
|
|
114
120
|
export class Video6847 {
|
|
115
121
|
constructor(video) {
|
|
116
122
|
this.video = video;
|
|
@@ -175,6 +181,7 @@ export class Video6847 {
|
|
|
175
181
|
this.bitmapPxPerPixel = 2; // each pixel is 2 bitmap pixels wide and high
|
|
176
182
|
this.pixelsPerBit = this.bitmapPxPerPixel;
|
|
177
183
|
this.bpp = 1;
|
|
184
|
+
this.updateLineGrid();
|
|
178
185
|
|
|
179
186
|
this.cpuAddr = 0;
|
|
180
187
|
this.dispEnabled = 0;
|
|
@@ -225,6 +232,9 @@ export class Video6847 {
|
|
|
225
232
|
reset(cpu, ppia) {
|
|
226
233
|
this.cpu = cpu;
|
|
227
234
|
this.ppia = ppia;
|
|
235
|
+
// polltime() is handed CPU cycles, so how far the VDG advances per
|
|
236
|
+
// cycle depends on how fast the CPU it's attached to runs.
|
|
237
|
+
this.vdgCyclesPerCpuCycle = cpu ? VdgClockMhz / cpu.model.clockMhz : 0;
|
|
228
238
|
}
|
|
229
239
|
|
|
230
240
|
// USE PAINT from VIDEO
|
|
@@ -315,6 +325,7 @@ export class Video6847 {
|
|
|
315
325
|
this.pixelsPerBit = this.bitmapPxPerPixel * this.modes[mode][1];
|
|
316
326
|
let linesPerRow = this.modes[mode][2]; // move to reg9
|
|
317
327
|
this.bpp = this.modes[mode][3];
|
|
328
|
+
this.updateLineGrid();
|
|
318
329
|
|
|
319
330
|
this.charLinesreg9 = linesPerRow - 1; //2 - scanlines per char
|
|
320
331
|
|
|
@@ -344,11 +355,7 @@ export class Video6847 {
|
|
|
344
355
|
// it doesn't draw the whole frame. It regularly gives control back to the CPU.
|
|
345
356
|
|
|
346
357
|
const vdgcharclock = this.pixelsPerChar / 2; // 4 or 8
|
|
347
|
-
|
|
348
|
-
// as an empirical correction to align VSync/interrupt timing with the Atom's
|
|
349
|
-
// 1 MHz CPU clock. TODO: revisit once full integration is testable.
|
|
350
|
-
const vdgclock = 3.638004;
|
|
351
|
-
this.vdg_cycles += clocks * vdgclock;
|
|
358
|
+
this.vdg_cycles += clocks * this.vdgCyclesPerCpuCycle;
|
|
352
359
|
|
|
353
360
|
const vdgframelines = 262; // 312 PAL (but no pal on standard atom) 262; // NTSC
|
|
354
361
|
const vdglinetime = 228; // vdg cycles to do a line; not 227.5
|
|
@@ -491,9 +498,9 @@ export class Video6847 {
|
|
|
491
498
|
{
|
|
492
499
|
// TODO: Add in the INTEXT modifiers to mode (if necessary)
|
|
493
500
|
// blit into the fb32 buffer which is painted by VIDEO
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
501
|
+
const textMode = (mode & MODE_AG) === 0; // 0x10 is the AG bit
|
|
502
|
+
this.recordLineGrid(textMode);
|
|
503
|
+
if (textMode) this.blitChar(this.video.fb32, dat, offset, this.pixelsPerChar, css);
|
|
497
504
|
else this.blitPixels(this.video.fb32, dat, offset, css);
|
|
498
505
|
}
|
|
499
506
|
}
|
|
@@ -619,6 +626,41 @@ export class Video6847 {
|
|
|
619
626
|
}
|
|
620
627
|
}
|
|
621
628
|
|
|
629
|
+
/**
|
|
630
|
+
* Precompute this mode's grid descriptors, one per blitter.
|
|
631
|
+
*
|
|
632
|
+
* The two blitters derive their widths differently and cannot be unified:
|
|
633
|
+
* `blitChar` splits the character's texels across eight glyph bits, while
|
|
634
|
+
* `blitPixels` takes the bit width from the mode table — which for text
|
|
635
|
+
* mode holds -1, since it does not blit pixels at all.
|
|
636
|
+
*
|
|
637
|
+
* `pixelsPerBit` is right for graphics at 2bpp as well as 1bpp: the blitter
|
|
638
|
+
* steps its bit groups by `pixelsPerBit / bpp`, but reads the colour with
|
|
639
|
+
* `j & 0xe`, so pairs of groups share one and each pixel is twice as wide.
|
|
640
|
+
*/
|
|
641
|
+
updateLineGrid() {
|
|
642
|
+
// Every 6847 blitter writes each pixel row into two framebuffer lines.
|
|
643
|
+
// Text mode's entry is only reachable through blitChar, which is just
|
|
644
|
+
// as well: the mode table holds -1 for it and would not encode.
|
|
645
|
+
this.lineGridText = encodeLineGrid((this.pixelsPerChar * this.bitmapPxPerPixel) / 8, true);
|
|
646
|
+
this.lineGridGraphics = this.pixelsPerBit > 0 ? encodeLineGrid(this.pixelsPerBit, true) : this.lineGridText;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Note the logical pixel size of the two rows about to be written, so
|
|
651
|
+
* display filters can see the picture as pixels rather than as raster
|
|
652
|
+
* samples (see video-filters/pixel-grid.js). Only the picture records a
|
|
653
|
+
* grid: the border is a solid colour, and letting it write here would
|
|
654
|
+
* leave the row describing the border rather than the picture on it.
|
|
655
|
+
*
|
|
656
|
+
* @param {boolean} textMode whether the character blitter is about to run
|
|
657
|
+
*/
|
|
658
|
+
recordLineGrid(textMode) {
|
|
659
|
+
const grid = textMode ? this.lineGridText : this.lineGridGraphics;
|
|
660
|
+
this.video.lineGrid[this.bitmapY] = grid;
|
|
661
|
+
this.video.lineGrid[this.bitmapY + 1] = grid;
|
|
662
|
+
}
|
|
663
|
+
|
|
622
664
|
blitChar(buf, data, destOffset, numPixels, css) {
|
|
623
665
|
const scanline = this.scanlineCounter;
|
|
624
666
|
const chr = data & 0x7f;
|
package/src/acia.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
// http://www.classiccmp.org/dunfield/r/6850.pdf
|
|
6
6
|
|
|
7
7
|
export class Acia {
|
|
8
|
-
constructor(cpu, toneGen, scheduler,
|
|
8
|
+
constructor(cpu, toneGen, scheduler, relayNoise) {
|
|
9
9
|
this.cpu = cpu;
|
|
10
10
|
this.toneGen = toneGen;
|
|
11
|
-
this.rs423Handler =
|
|
11
|
+
this.rs423Handler = null;
|
|
12
12
|
this.relayNoise = relayNoise;
|
|
13
13
|
|
|
14
14
|
this.sr = 0x00;
|
|
@@ -22,17 +22,27 @@ export class Acia {
|
|
|
22
22
|
this.hadDcdHigh = false;
|
|
23
23
|
this.serialReceiveRate = 0;
|
|
24
24
|
this.serialReceiveCyclesPerByte = 0;
|
|
25
|
+
this.serialTransmitRate = 0;
|
|
26
|
+
this.serialTransmitCyclesPerByte = 0;
|
|
25
27
|
|
|
26
28
|
this.setSerialReceive(19200);
|
|
29
|
+
this.setSerialTransmit(19200);
|
|
27
30
|
this.txCompleteTask = scheduler.newTask(() => {
|
|
28
31
|
this.sr |= 0x02; // set the TDRE
|
|
32
|
+
this.updateIrq();
|
|
29
33
|
});
|
|
30
34
|
this.runTapeTask = scheduler.newTask(() => this.runTape());
|
|
31
35
|
this.runRs423Task = scheduler.newTask(() => this.runRs423());
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
// MC6850: the transmit interrupt is selected by CR6:CR5 = 01 and follows
|
|
39
|
+
// TDRE, which a high CTS inhibits.
|
|
40
|
+
txIrq() {
|
|
41
|
+
return (this.cr & 0x60) === 0x20 && (this.sr & 0x0a) === 0x02;
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
updateIrq() {
|
|
35
|
-
if (this.sr & this.cr & 0x80) {
|
|
45
|
+
if (this.sr & this.cr & 0x80 || this.txIrq()) {
|
|
36
46
|
this.cpu.interrupt |= 0x04;
|
|
37
47
|
} else {
|
|
38
48
|
this.cpu.interrupt &= ~0x04;
|
|
@@ -77,6 +87,7 @@ export class Acia {
|
|
|
77
87
|
return this.dr;
|
|
78
88
|
} else {
|
|
79
89
|
let result = (this.sr & 0x7f) | (this.sr & this.cr & 0x80);
|
|
90
|
+
if (this.txIrq()) result |= 0x80;
|
|
80
91
|
// MC6850: "A low CTS indicates that there is a Clear-to-Send
|
|
81
92
|
// from the modem. In the high state, the Transmit Data Register
|
|
82
93
|
// Empty bit is inhibited".
|
|
@@ -100,10 +111,7 @@ export class Acia {
|
|
|
100
111
|
write(addr, val) {
|
|
101
112
|
if (addr & 1) {
|
|
102
113
|
this.sr &= ~0x02;
|
|
103
|
-
|
|
104
|
-
// That could be straight away if not already tx-ing, but as we don't really tx,
|
|
105
|
-
// be conservative here.
|
|
106
|
-
this.txCompleteTask.reschedule(2000);
|
|
114
|
+
this.txCompleteTask.reschedule(this.serialTransmitCyclesPerByte);
|
|
107
115
|
this.updateIrq();
|
|
108
116
|
if (this.rs423Selected && this.rs423Handler) this.rs423Handler.onTransmit(val);
|
|
109
117
|
} else {
|
|
@@ -114,6 +122,8 @@ export class Acia {
|
|
|
114
122
|
} else {
|
|
115
123
|
this.cr = val;
|
|
116
124
|
this.setSerialReceive(this.serialReceiveRate);
|
|
125
|
+
this.setSerialTransmit(this.serialTransmitRate);
|
|
126
|
+
this.updateIrq();
|
|
117
127
|
}
|
|
118
128
|
}
|
|
119
129
|
}
|
|
@@ -212,7 +222,7 @@ export class Acia {
|
|
|
212
222
|
tapeDcdLineLevel: this.tapeDcdLineLevel,
|
|
213
223
|
hadDcdHigh: this.hadDcdHigh,
|
|
214
224
|
serialReceiveRate: this.serialReceiveRate,
|
|
215
|
-
|
|
225
|
+
serialTransmitRate: this.serialTransmitRate,
|
|
216
226
|
txCompleteTaskOffset: this.txCompleteTask.scheduled()
|
|
217
227
|
? this.txCompleteTask.expireEpoch - scheduler.epoch
|
|
218
228
|
: null,
|
|
@@ -230,8 +240,10 @@ export class Acia {
|
|
|
230
240
|
this.tapeCarrierCount = state.tapeCarrierCount;
|
|
231
241
|
this.tapeDcdLineLevel = state.tapeDcdLineLevel;
|
|
232
242
|
this.hadDcdHigh = state.hadDcdHigh;
|
|
233
|
-
|
|
234
|
-
this.
|
|
243
|
+
// The byte times are derived from the rates and the restored word format, not saved.
|
|
244
|
+
this.setSerialReceive(state.serialReceiveRate);
|
|
245
|
+
// Snapshots predating transmit timing have no saved rate.
|
|
246
|
+
this.setSerialTransmit(state.serialTransmitRate ?? 19200);
|
|
235
247
|
this.updateIrq();
|
|
236
248
|
|
|
237
249
|
this.txCompleteTask.cancel();
|
|
@@ -253,8 +265,9 @@ export class Acia {
|
|
|
253
265
|
}
|
|
254
266
|
}
|
|
255
267
|
|
|
268
|
+
// Byte times are held in CPU cycles because that's what the scheduler counts.
|
|
256
269
|
secondsToCycles(sec) {
|
|
257
|
-
return Math.floor(
|
|
270
|
+
return Math.floor(this.cpu.model.cyclesPerSecond * sec) | 0;
|
|
258
271
|
}
|
|
259
272
|
|
|
260
273
|
numBitsPerByte() {
|
|
@@ -294,7 +307,8 @@ export class Acia {
|
|
|
294
307
|
parityBits = 1;
|
|
295
308
|
break;
|
|
296
309
|
}
|
|
297
|
-
|
|
310
|
+
const startBits = 1;
|
|
311
|
+
return startBits + wordLength + stopBits + parityBits;
|
|
298
312
|
}
|
|
299
313
|
|
|
300
314
|
rts() {
|
|
@@ -307,6 +321,11 @@ export class Acia {
|
|
|
307
321
|
this.serialReceiveCyclesPerByte = this.secondsToCycles(this.numBitsPerByte() / rate);
|
|
308
322
|
}
|
|
309
323
|
|
|
324
|
+
setSerialTransmit(rate) {
|
|
325
|
+
this.serialTransmitRate = rate;
|
|
326
|
+
this.serialTransmitCyclesPerByte = this.secondsToCycles(this.numBitsPerByte() / rate);
|
|
327
|
+
}
|
|
328
|
+
|
|
310
329
|
runTape() {
|
|
311
330
|
if (this.tape) this.runTapeTask.reschedule(this.tape.poll(this));
|
|
312
331
|
}
|