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/src/video.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Teletext } from "./teletext.js";
|
|
|
3
3
|
import * as utils from "./utils.js";
|
|
4
4
|
import { BbcDefaultPalette as NulaDefaultPalette } from "./bbc-palette.js";
|
|
5
5
|
import { Video6847 } from "./6847.js";
|
|
6
|
+
import { encodeLineGrid, texelsPerPixel, LineGridRows } from "./video-filters/pixel-grid.js";
|
|
6
7
|
|
|
7
8
|
export const VDISPENABLE = 1 << 0;
|
|
8
9
|
export const HDISPENABLE = 1 << 1;
|
|
@@ -16,6 +17,8 @@ export const EVERYTHINGENABLED =
|
|
|
16
17
|
export const OPAQUE_BLACK = 0xff000000;
|
|
17
18
|
export const OPAQUE_WHITE = 0xffffffff;
|
|
18
19
|
|
|
20
|
+
export const MinPaintedFrameRows = 64;
|
|
21
|
+
|
|
19
22
|
////////////////////
|
|
20
23
|
// VideoNULA - programmable 12-bit RGB palette extension (RobC hardware mod).
|
|
21
24
|
// Reference: b-em src/video.c (stardot/b-em).
|
|
@@ -127,6 +130,7 @@ class Ula {
|
|
|
127
130
|
this.video.ulaMode = newMode;
|
|
128
131
|
}
|
|
129
132
|
this.video.teletextMode = !!(val & 2);
|
|
133
|
+
this.video.updateLineGridUla();
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
// ULA palette register (&FE21).
|
|
@@ -386,6 +390,17 @@ export class Video {
|
|
|
386
390
|
this.doubledScanlines = true;
|
|
387
391
|
this.frameSkipCount = 0;
|
|
388
392
|
this.screenSubtract = 0;
|
|
393
|
+
// Describes the logical pixel grid of each framebuffer row: how many
|
|
394
|
+
// texels wide and tall one BBC pixel is on that line. Display filters
|
|
395
|
+
// need this to see the picture as pixels rather than as raster samples;
|
|
396
|
+
// see video-filters/pixel-grid.js. One byte per row, written as each
|
|
397
|
+
// character cell renders, so a mode change between rows is recorded
|
|
398
|
+
// faithfully. A row whose mode changes part way along keeps only the
|
|
399
|
+
// last mode on it — enough for a raster split, not for a mid-line one.
|
|
400
|
+
this.lineGrid = new Uint8Array(LineGridRows);
|
|
401
|
+
this.lineGridUla = 0;
|
|
402
|
+
this.lineGridUlaDoubled = 0;
|
|
403
|
+
this.updateLineGridUla();
|
|
389
404
|
|
|
390
405
|
this.topBorder = 12;
|
|
391
406
|
this.bottomBorder = 13;
|
|
@@ -524,6 +539,8 @@ export class Video {
|
|
|
524
539
|
this.halfClock = state.halfClock;
|
|
525
540
|
this.ulaMode = state.ulaMode;
|
|
526
541
|
this.teletextMode = state.teletextMode;
|
|
542
|
+
// Derived from the above, so it is recomputed rather than snapshotted.
|
|
543
|
+
this.updateLineGridUla();
|
|
527
544
|
this.displayEnableSkew = state.displayEnableSkew;
|
|
528
545
|
this.actualPal.set(state.actualPal);
|
|
529
546
|
this.cursorOn = state.cursorOn;
|
|
@@ -555,20 +572,24 @@ export class Video {
|
|
|
555
572
|
|
|
556
573
|
clearPaintBuffer() {
|
|
557
574
|
const fb32 = this.fb32;
|
|
575
|
+
// The line grid is cleared exactly where the pixels are: in interlaced
|
|
576
|
+
// modes the other field's rows survive, and so must their grid.
|
|
558
577
|
if (this.interlacedSyncAndVideo || !this.doubledScanlines) {
|
|
559
578
|
let line = this.frameCount & 1;
|
|
560
579
|
while (line < 625) {
|
|
561
580
|
const start = line * 1024;
|
|
562
581
|
fb32.fill(OPAQUE_BLACK, start, start + 1024);
|
|
582
|
+
this.lineGrid[line] = 0;
|
|
563
583
|
line += 2;
|
|
564
584
|
}
|
|
565
585
|
} else {
|
|
566
586
|
fb32.fill(OPAQUE_BLACK);
|
|
587
|
+
this.lineGrid.fill(0);
|
|
567
588
|
}
|
|
568
589
|
}
|
|
569
590
|
|
|
570
|
-
|
|
571
|
-
if (this.dispEnabled & FRAMESKIPENABLE) {
|
|
591
|
+
flyback() {
|
|
592
|
+
if (this.bitmapY >= MinPaintedFrameRows && this.dispEnabled & FRAMESKIPENABLE) {
|
|
572
593
|
this.paint();
|
|
573
594
|
this.clearPaintBuffer();
|
|
574
595
|
}
|
|
@@ -610,6 +631,21 @@ export class Video {
|
|
|
610
631
|
debugCopyFb(this.fb32, this.debugPrevScreen);
|
|
611
632
|
}
|
|
612
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Recompute the ULA-dependent half of the line grid descriptor: everything
|
|
636
|
+
* except whether this particular scanline was doubled. Called whenever the
|
|
637
|
+
* ULA control register changes, so the render loop only has to store it.
|
|
638
|
+
*
|
|
639
|
+
* MODE 7 counts as one texel per pixel: the SAA5050 emulation writes each
|
|
640
|
+
* of its 16 texels per character individually, so its output is already at
|
|
641
|
+
* the framebuffer's own resolution.
|
|
642
|
+
*/
|
|
643
|
+
updateLineGridUla() {
|
|
644
|
+
const texelsWide = this.teletextMode ? 1 : texelsPerPixel(this.ulaMode);
|
|
645
|
+
this.lineGridUla = encodeLineGrid(texelsWide, false);
|
|
646
|
+
this.lineGridUlaDoubled = encodeLineGrid(texelsWide, true);
|
|
647
|
+
}
|
|
648
|
+
|
|
613
649
|
blitFb(dat, destOffset, numPixels) {
|
|
614
650
|
destOffset |= 0;
|
|
615
651
|
const offset = table4bppOffset(this.ulaMode, dat);
|
|
@@ -816,14 +852,12 @@ export class Video {
|
|
|
816
852
|
// an approximation that works if hsyncs are spaced evenly.
|
|
817
853
|
this.bitmapY += 2;
|
|
818
854
|
|
|
819
|
-
//
|
|
820
|
-
|
|
821
|
-
// Arbitrary moment when TV will give up and start flyback in the absence of an explicit VSync signal
|
|
822
|
-
this.paintAndClear();
|
|
823
|
-
}
|
|
855
|
+
// Arbitrary moment when TV will give up and start flyback in the absence of an explicit VSync signal
|
|
856
|
+
return this.bitmapY >= 768;
|
|
824
857
|
} else if (this.hpulseCounter === (this.regs[3] & 0x0f)) {
|
|
825
858
|
this.inHSync = false;
|
|
826
859
|
}
|
|
860
|
+
return false;
|
|
827
861
|
}
|
|
828
862
|
|
|
829
863
|
cb2changed(level, output) {
|
|
@@ -871,7 +905,7 @@ export class Video {
|
|
|
871
905
|
// This emulates the Hitachi 6845SP CRTC.
|
|
872
906
|
// Other variants have different quirks.
|
|
873
907
|
// Handle HSync
|
|
874
|
-
if (this.inHSync
|
|
908
|
+
if (this.inHSync && this.handleHSync()) this.flyback();
|
|
875
909
|
|
|
876
910
|
// Handle delayed display enable due to skew
|
|
877
911
|
const displayEnablePos = this.displayEnableSkew + (this.teletextMode ? 2 : 0);
|
|
@@ -935,11 +969,7 @@ export class Video {
|
|
|
935
969
|
this.hadVSyncThisRow = true;
|
|
936
970
|
this.vpulseCounter = 0;
|
|
937
971
|
|
|
938
|
-
|
|
939
|
-
// otherwise small values.
|
|
940
|
-
if (this.regs[0] && this.regs[4]) {
|
|
941
|
-
this.paintAndClear();
|
|
942
|
-
}
|
|
972
|
+
this.flyback();
|
|
943
973
|
}
|
|
944
974
|
|
|
945
975
|
if (vSyncStarting || vSyncEnding) {
|
|
@@ -974,7 +1004,7 @@ export class Video {
|
|
|
974
1004
|
// Render data depending on display enable state.
|
|
975
1005
|
if (this.bitmapX >= 0 && this.bitmapX < 1024 && this.bitmapY < 625) {
|
|
976
1006
|
let doubledLines = false;
|
|
977
|
-
let
|
|
1007
|
+
let bitmapRow = this.bitmapY;
|
|
978
1008
|
// There's a painting subtlety here: if we're in an
|
|
979
1009
|
// interlace mode but R6>R4 then we'll get stuck
|
|
980
1010
|
// painting just an odd or even frame, so we double up
|
|
@@ -984,12 +1014,22 @@ export class Video {
|
|
|
984
1014
|
this.isEvenRender === this.lastRenderWasEven
|
|
985
1015
|
) {
|
|
986
1016
|
doubledLines = true;
|
|
987
|
-
|
|
1017
|
+
bitmapRow &= ~1;
|
|
988
1018
|
}
|
|
989
1019
|
|
|
990
|
-
offset =
|
|
1020
|
+
const offset = bitmapRow * 1024 + this.bitmapX;
|
|
991
1021
|
|
|
992
1022
|
if ((this.dispEnabled & EVERYTHINGENABLED) === EVERYTHINGENABLED) {
|
|
1023
|
+
// Note this row's logical pixel size for display
|
|
1024
|
+
// filters; see video-filters/pixel-grid.js. The ULA half
|
|
1025
|
+
// of the descriptor is precomputed on register writes so
|
|
1026
|
+
// this stays a store or two in the hottest loop we have.
|
|
1027
|
+
if (doubledLines) {
|
|
1028
|
+
this.lineGrid[bitmapRow] = this.lineGridUlaDoubled;
|
|
1029
|
+
this.lineGrid[bitmapRow + 1] = this.lineGridUlaDoubled;
|
|
1030
|
+
} else {
|
|
1031
|
+
this.lineGrid[bitmapRow] = this.lineGridUla;
|
|
1032
|
+
}
|
|
993
1033
|
if (this.teletextMode) {
|
|
994
1034
|
if (this.halfClock) {
|
|
995
1035
|
// Proper MODE 7 (1MHz clock + teletext): render SAA5050 output normally.
|
package/src/wd-fdc.js
CHANGED
|
@@ -46,6 +46,17 @@ const CommandBits = Object.freeze({
|
|
|
46
46
|
typeIIDeleted: 0x01,
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Type IV (force interrupt) condition bits, taken from the command's low nibble.
|
|
51
|
+
*
|
|
52
|
+
* @readonly
|
|
53
|
+
* @enum {Number}
|
|
54
|
+
*/
|
|
55
|
+
const ForceInterruptBits = Object.freeze({
|
|
56
|
+
indexPulse: 0x04,
|
|
57
|
+
immediate: 0x08,
|
|
58
|
+
});
|
|
59
|
+
|
|
49
60
|
/**
|
|
50
61
|
* The drive control register is documented here:
|
|
51
62
|
* https://www.cloud9.co.uk/james/BBCMicro/Documentation/wd1770.html
|
|
@@ -512,6 +523,8 @@ export class WdFdc {
|
|
|
512
523
|
// insofar as index pulse appears to be reported in the status register.
|
|
513
524
|
// - Interrupt on index pulse is only active for the current command.
|
|
514
525
|
if (this._statusRegister & Status.busy) {
|
|
526
|
+
// Any pending timer belongs to the command being aborted.
|
|
527
|
+
this._clearTimer();
|
|
515
528
|
this._commandDone(false);
|
|
516
529
|
} else {
|
|
517
530
|
if (this._state !== State.idle) throw new Error(`Unexpected state when force interrupt: ${this._state}`);
|
|
@@ -523,13 +536,10 @@ export class WdFdc {
|
|
|
523
536
|
this._currentDrive.startSpinning();
|
|
524
537
|
}
|
|
525
538
|
}
|
|
526
|
-
if (forceInterruptBits
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
} else {
|
|
531
|
-
throw new Error(`1700 force interrupt flags not handled: ${forceInterruptBits}`);
|
|
532
|
-
}
|
|
539
|
+
if (forceInterruptBits & ForceInterruptBits.immediate) this._setIntRq(true);
|
|
540
|
+
this._isInterruptOnIndexPulse = !!(forceInterruptBits & ForceInterruptBits.indexPulse);
|
|
541
|
+
// The remaining two bits select interrupts on the ready line's transitions. The BBC ties
|
|
542
|
+
// the 1770's READY input active, so neither transition can ever occur.
|
|
533
543
|
}
|
|
534
544
|
|
|
535
545
|
_timerFired() {
|
package/tests/test-machine.js
CHANGED
|
@@ -178,7 +178,7 @@ export class TestMachine {
|
|
|
178
178
|
return true;
|
|
179
179
|
}
|
|
180
180
|
});
|
|
181
|
-
await this.runFor(secs *
|
|
181
|
+
await this.runFor(secs * this.model.cyclesPerSecond);
|
|
182
182
|
hook.remove();
|
|
183
183
|
assert(hit, "Atom did not reach keyboard input in time");
|
|
184
184
|
return this.runFor(10 * 1000);
|
|
@@ -191,7 +191,7 @@ export class TestMachine {
|
|
|
191
191
|
return true;
|
|
192
192
|
}
|
|
193
193
|
});
|
|
194
|
-
await this.runFor(secs *
|
|
194
|
+
await this.runFor(secs * this.model.cyclesPerSecond);
|
|
195
195
|
hook.remove();
|
|
196
196
|
assert(hit, "did not hit appropriate breakpoint in time");
|
|
197
197
|
return this.runFor(10 * 1000);
|
|
@@ -206,7 +206,7 @@ export class TestMachine {
|
|
|
206
206
|
return true;
|
|
207
207
|
}
|
|
208
208
|
});
|
|
209
|
-
await this.runFor(secs *
|
|
209
|
+
await this.runFor(secs * this.model.cyclesPerSecond);
|
|
210
210
|
hook.remove();
|
|
211
211
|
assert(hit, "did not hit appropriate breakpoint in time");
|
|
212
212
|
}
|
|
@@ -370,7 +370,8 @@ export class TestMachine {
|
|
|
370
370
|
let nextEventCycle = 0;
|
|
371
371
|
let done = false;
|
|
372
372
|
|
|
373
|
-
const currentCycle = () =>
|
|
373
|
+
const currentCycle = () =>
|
|
374
|
+
this.processor.cycleSeconds * this.model.cyclesPerSecond + this.processor.currentCycles;
|
|
374
375
|
|
|
375
376
|
const hook = this.processor.debugInstruction.add(() => {
|
|
376
377
|
if (currentCycle() < nextEventCycle) return;
|
|
@@ -425,7 +426,8 @@ export class TestMachine {
|
|
|
425
426
|
let done = false;
|
|
426
427
|
let shiftHeld = false;
|
|
427
428
|
|
|
428
|
-
const currentCycle = () =>
|
|
429
|
+
const currentCycle = () =>
|
|
430
|
+
this.processor.cycleSeconds * this.model.cyclesPerSecond + this.processor.currentCycles;
|
|
429
431
|
|
|
430
432
|
const isShift = (entry) => entry[0] === SHIFT[0] && entry[1] === SHIFT[1];
|
|
431
433
|
|