jsbeeb 1.19.0 → 1.19.2
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 +4 -4
- package/package.json +1 -1
- package/src/teletext.js +70 -20
- package/src/url-params.js +30 -12
- package/src/video.js +84 -23
- package/src/basic/multiline-tetris +0 -9
package/README.md
CHANGED
|
@@ -51,15 +51,15 @@ KEY.<host key>=<BBC key>
|
|
|
51
51
|
Add one for each key you want to change. For example, Superior Software's Space Invaders fires with `COPY`; this makes
|
|
52
52
|
`Enter` fire instead:
|
|
53
53
|
|
|
54
|
-
[`https://bbc.xania.org/?disc1=sth:Superior/SpaceInvaders-Superior.zip&autoboot&KEY.ENTER=COPY`](https://bbc.xania.org/?disc1=sth
|
|
54
|
+
[`https://bbc.xania.org/?disc1=sth:Superior/SpaceInvaders-Superior.zip&autoboot&KEY.ENTER=COPY`](https://bbc.xania.org/?disc1=sth:Superior/SpaceInvaders-Superior.zip&autoboot&KEY.ENTER=COPY)
|
|
55
55
|
|
|
56
56
|
Superior's Frogger uses `A`/`Z`/`DELETE`/`COPY` to move; this puts it on the arrow keys:
|
|
57
57
|
|
|
58
|
-
[`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
|
|
58
|
+
[`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:Superior/Frogger-Superior.zip&autoboot&KEY.UP=A&KEY.DOWN=Z&KEY.LEFT=DELETE&KEY.RIGHT=COPY)
|
|
59
59
|
|
|
60
60
|
And Superior's Hunchback steers with `CAPS LOCK` and `CTRL`, which the arrow keys can stand in for:
|
|
61
61
|
|
|
62
|
-
[`https://bbc.xania.org/?disc1=sth:Superior/Hunchback-Superior.zip&autoboot&KEY.LEFT=CAPSLOCK&KEY.RIGHT=CTRL`](https://bbc.xania.org/?disc1=sth
|
|
62
|
+
[`https://bbc.xania.org/?disc1=sth:Superior/Hunchback-Superior.zip&autoboot&KEY.LEFT=CAPSLOCK&KEY.RIGHT=CTRL`](https://bbc.xania.org/?disc1=sth:Superior/Hunchback-Superior.zip&autoboot&KEY.LEFT=CAPSLOCK&KEY.RIGHT=CTRL)
|
|
63
63
|
|
|
64
64
|
The **host key** names are jsbeeb's names for the keys on your own keyboard. Most are what you'd expect, but note:
|
|
65
65
|
|
|
@@ -141,7 +141,7 @@ GP.<gamepad control>=<BBC key>
|
|
|
141
141
|
By default the D-pad presses the "Snapper" keys (`Z`, `X`, `:`, `/`), the `A` button presses `RETURN` and `Start`
|
|
142
142
|
presses `SPACE`. To play Superior's Space Invaders on a pad, where `COPY` fires:
|
|
143
143
|
|
|
144
|
-
[`https://bbc.xania.org/?disc1=sth:Superior/SpaceInvaders-Superior.zip&autoboot&GP.FIRE=COPY`](https://bbc.xania.org/?disc1=sth
|
|
144
|
+
[`https://bbc.xania.org/?disc1=sth:Superior/SpaceInvaders-Superior.zip&autoboot&GP.FIRE=COPY`](https://bbc.xania.org/?disc1=sth:Superior/SpaceInvaders-Superior.zip&autoboot&GP.FIRE=COPY)
|
|
145
145
|
|
|
146
146
|
The gamepad control names are:
|
|
147
147
|
|
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.19.
|
|
10
|
+
"version": "1.19.2",
|
|
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"
|
package/src/teletext.js
CHANGED
|
@@ -16,7 +16,11 @@ export class Teletext {
|
|
|
16
16
|
this.flashTime = 0;
|
|
17
17
|
this.heldChar = 0;
|
|
18
18
|
this.holdChar = false;
|
|
19
|
-
this.
|
|
19
|
+
this.cellGlyphIndex = 0;
|
|
20
|
+
this.cellFlash = false;
|
|
21
|
+
this.cellConceal = false;
|
|
22
|
+
this.cellPalette = 0;
|
|
23
|
+
this.dataQueue = new Uint8Array(4);
|
|
20
24
|
this.scanlineCounter = 0;
|
|
21
25
|
this.levelDEW = false;
|
|
22
26
|
this.levelDISPTMG = false;
|
|
@@ -204,7 +208,7 @@ export class Teletext {
|
|
|
204
208
|
this.flashTime = state.flashTime;
|
|
205
209
|
this.heldChar = state.heldChar;
|
|
206
210
|
this.holdChar = state.holdChar;
|
|
207
|
-
this.dataQueue
|
|
211
|
+
this.dataQueue.set(state.dataQueue);
|
|
208
212
|
this.scanlineCounter = state.scanlineCounter;
|
|
209
213
|
this.levelDEW = state.levelDEW;
|
|
210
214
|
this.levelDISPTMG = state.levelDISPTMG;
|
|
@@ -308,8 +312,11 @@ export class Teletext {
|
|
|
308
312
|
}
|
|
309
313
|
|
|
310
314
|
fetchData(data) {
|
|
311
|
-
|
|
312
|
-
this.dataQueue
|
|
315
|
+
// `copyWithin` over four bytes costs more than the moves it saves.
|
|
316
|
+
this.dataQueue[0] = this.dataQueue[1];
|
|
317
|
+
this.dataQueue[1] = this.dataQueue[2];
|
|
318
|
+
this.dataQueue[2] = this.dataQueue[3];
|
|
319
|
+
this.dataQueue[3] = data & 0x7f;
|
|
313
320
|
}
|
|
314
321
|
|
|
315
322
|
setDEW(level) {
|
|
@@ -386,14 +393,13 @@ export class Teletext {
|
|
|
386
393
|
this.levelRA0 = level;
|
|
387
394
|
}
|
|
388
395
|
|
|
389
|
-
|
|
396
|
+
/**
|
|
397
|
+
* Clock one character through the chip: take the oldest byte in the pipeline, apply it
|
|
398
|
+
* to the character state, and latch how the resulting cell should look.
|
|
399
|
+
*/
|
|
400
|
+
advance() {
|
|
390
401
|
let data = this.dataQueue[0];
|
|
391
402
|
|
|
392
|
-
let scanline = this.scanlineCounter << 1;
|
|
393
|
-
if (this.levelRA0) {
|
|
394
|
-
scanline++;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
403
|
this.oldDbl = this.dbl;
|
|
398
404
|
|
|
399
405
|
this.prevCol = this.col;
|
|
@@ -412,28 +418,49 @@ export class Teletext {
|
|
|
412
418
|
this.heldChar = 32;
|
|
413
419
|
}
|
|
414
420
|
|
|
421
|
+
// Flash (code 8) is "Set After" — flashThisCell retains the pre-control-code state.
|
|
422
|
+
// Steady (code 9) is "Set At" — update so this cell stops flashing immediately.
|
|
423
|
+
if (flashThisCell && !this.flash) flashThisCell = false;
|
|
424
|
+
|
|
425
|
+
// Conceal (code 24) is "Set At", and a colour code only reveals from the cell after itself.
|
|
426
|
+
if (this.conceal) concealThisCell = true;
|
|
427
|
+
|
|
428
|
+
this.cellGlyphIndex = (data - 32) * 20;
|
|
429
|
+
this.cellFlash = flashThisCell;
|
|
430
|
+
this.cellConceal = concealThisCell;
|
|
431
|
+
this.cellPalette = ((this.bg & 7) << 5) | ((this.prevCol & 7) << 2);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
glyphScanline() {
|
|
435
|
+
let scanline = this.scanlineCounter << 1;
|
|
436
|
+
if (this.levelRA0) {
|
|
437
|
+
scanline++;
|
|
438
|
+
}
|
|
439
|
+
|
|
415
440
|
if (this.oldDbl) {
|
|
416
441
|
scanline = scanline >>> 1;
|
|
417
442
|
if (this.secondHalfOfDouble) {
|
|
418
443
|
scanline += 10;
|
|
419
444
|
}
|
|
420
445
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
// Flash (code 8) is "Set After" — flashThisCell retains the pre-control-code state.
|
|
424
|
-
// Steady (code 9) is "Set At" — update so this cell stops flashing immediately.
|
|
425
|
-
if (flashThisCell && !this.flash) flashThisCell = false;
|
|
446
|
+
return scanline;
|
|
447
|
+
}
|
|
426
448
|
|
|
427
|
-
|
|
428
|
-
|
|
449
|
+
/**
|
|
450
|
+
* Paint the cell most recently latched by `advance`. Everything else read here changes
|
|
451
|
+
* per scanline or per field, not per character.
|
|
452
|
+
*/
|
|
453
|
+
emit(buf, offset) {
|
|
454
|
+
const scanline = this.glyphScanline();
|
|
429
455
|
|
|
430
|
-
if (
|
|
431
|
-
const backgroundColour = this.colour[
|
|
456
|
+
if (this.cellConceal || (this.cellFlash && this.hideFlashing) || (this.secondHalfOfDouble && !this.dbl)) {
|
|
457
|
+
const backgroundColour = this.colour[this.cellPalette & 0xe0];
|
|
432
458
|
for (let i = 0; i < 16; ++i) {
|
|
433
459
|
buf[offset++] = backgroundColour;
|
|
434
460
|
}
|
|
435
461
|
} else {
|
|
436
|
-
|
|
462
|
+
let chardef = this.curGlyphs[this.cellGlyphIndex + scanline];
|
|
463
|
+
const paletteIndex = this.cellPalette;
|
|
437
464
|
|
|
438
465
|
for (let pixel = 0; pixel < 16; ++pixel) {
|
|
439
466
|
buf[offset + pixel] = this.colour[paletteIndex + (chardef & 3)];
|
|
@@ -441,4 +468,27 @@ export class Teletext {
|
|
|
441
468
|
}
|
|
442
469
|
}
|
|
443
470
|
}
|
|
471
|
+
|
|
472
|
+
// The second half of the cell `emit` last painted, for the ULA's 1MHz repaint.
|
|
473
|
+
emitSecondHalf(buf, offset) {
|
|
474
|
+
const scanline = this.glyphScanline();
|
|
475
|
+
if (this.cellConceal || (this.cellFlash && this.hideFlashing) || (this.secondHalfOfDouble && !this.dbl)) {
|
|
476
|
+
const backgroundColour = this.colour[this.cellPalette & 0xe0];
|
|
477
|
+
for (let pixel = 8; pixel < 16; ++pixel) {
|
|
478
|
+
buf[offset + pixel] = backgroundColour;
|
|
479
|
+
}
|
|
480
|
+
} else {
|
|
481
|
+
let chardef = this.curGlyphs[this.cellGlyphIndex + scanline] >>> 16;
|
|
482
|
+
const paletteIndex = this.cellPalette;
|
|
483
|
+
for (let pixel = 8; pixel < 16; ++pixel) {
|
|
484
|
+
buf[offset + pixel] = this.colour[paletteIndex + (chardef & 3)];
|
|
485
|
+
chardef >>>= 2;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
render(buf, offset) {
|
|
491
|
+
this.advance();
|
|
492
|
+
this.emit(buf, offset);
|
|
493
|
+
}
|
|
444
494
|
}
|
package/src/url-params.js
CHANGED
|
@@ -41,18 +41,15 @@ export const ParamTypes = {
|
|
|
41
41
|
export function parseQueryString(queryString, paramTypes = {}) {
|
|
42
42
|
if (!queryString) return {};
|
|
43
43
|
|
|
44
|
-
// workaround for shonky python web server
|
|
45
|
-
const cleanQueryString = queryString.endsWith("/") ? queryString.substring(0, queryString.length - 1) : queryString;
|
|
46
|
-
|
|
47
44
|
const parsedQuery = {};
|
|
48
45
|
|
|
49
|
-
|
|
46
|
+
queryString.split("&").forEach(function (keyval) {
|
|
50
47
|
if (!keyval) return;
|
|
51
48
|
|
|
52
49
|
const keyAndVal = keyval.split("=");
|
|
53
50
|
const key = decodeURIComponent(keyAndVal[0]);
|
|
54
51
|
let val = null;
|
|
55
|
-
if (keyAndVal.length > 1) val = decodeURIComponent(keyAndVal
|
|
52
|
+
if (keyAndVal.length > 1) val = decodeURIComponent(keyAndVal.slice(1).join("="));
|
|
56
53
|
|
|
57
54
|
const paramType = paramTypes[key] || ParamTypes.STRING;
|
|
58
55
|
|
|
@@ -90,12 +87,26 @@ export function parseQueryString(queryString, paramTypes = {}) {
|
|
|
90
87
|
}
|
|
91
88
|
|
|
92
89
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* @param {Object.<string, ParamType>} [paramTypes={}] - Object mapping parameter names to their types
|
|
97
|
-
* @returns {string} The complete URL with query parameters
|
|
90
|
+
* Characters RFC 3986 allows literally in a query that `encodeURIComponent` escapes anyway. `&`,
|
|
91
|
+
* `=`, `+`, `#`, `%` and space are left escaped because they delimit something, and `?` because a
|
|
92
|
+
* URL with a second one in it reads as broken even though the grammar permits it.
|
|
98
93
|
*/
|
|
94
|
+
const QueryLiterals = "$,/:;@";
|
|
95
|
+
|
|
96
|
+
const EscapedQueryLiterals = new RegExp(
|
|
97
|
+
[...QueryLiterals].map((char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`).join("|"),
|
|
98
|
+
"g",
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Percent-encode a key or value for a query string, escaping only what delimits something
|
|
103
|
+
* @param {string} component - The key or value to encode
|
|
104
|
+
* @returns {string} The encoded component
|
|
105
|
+
*/
|
|
106
|
+
function encodeQueryComponent(component) {
|
|
107
|
+
return encodeURIComponent(component).replace(EscapedQueryLiterals, decodeURIComponent);
|
|
108
|
+
}
|
|
109
|
+
|
|
99
110
|
/**
|
|
100
111
|
* Append a parameter to the URL
|
|
101
112
|
* @param {string} url - Current URL
|
|
@@ -105,13 +116,20 @@ export function parseQueryString(queryString, paramTypes = {}) {
|
|
|
105
116
|
* @returns {Object} Updated URL and separator
|
|
106
117
|
*/
|
|
107
118
|
function appendParam(url, sep, key, value = undefined) {
|
|
108
|
-
url += sep +
|
|
119
|
+
url += sep + encodeQueryComponent(key);
|
|
109
120
|
if (value !== undefined) {
|
|
110
|
-
url += "=" +
|
|
121
|
+
url += "=" + encodeQueryComponent(value);
|
|
111
122
|
}
|
|
112
123
|
return { url, sep: "&" };
|
|
113
124
|
}
|
|
114
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Build a URL string from base URL and query parameters
|
|
128
|
+
* @param {string} baseUrl - The base URL (without query string)
|
|
129
|
+
* @param {Object} parsedQuery - Object containing query parameters
|
|
130
|
+
* @param {Object.<string, ParamType>} [paramTypes={}] - Object mapping parameter names to their types
|
|
131
|
+
* @returns {string} The complete URL with query parameters
|
|
132
|
+
*/
|
|
115
133
|
export function buildUrlFromParams(baseUrl, parsedQuery, paramTypes = {}) {
|
|
116
134
|
let url = baseUrl;
|
|
117
135
|
let sep = "?";
|
package/src/video.js
CHANGED
|
@@ -84,6 +84,7 @@ class Ula {
|
|
|
84
84
|
this._writeNulaPalette(val);
|
|
85
85
|
break;
|
|
86
86
|
}
|
|
87
|
+
this.video.repaintSecondHalfOfCell();
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
snapshotState() {
|
|
@@ -385,7 +386,9 @@ export class Video {
|
|
|
385
386
|
this.cursorOff = false;
|
|
386
387
|
this.cursorOnThisFrame = false;
|
|
387
388
|
this.cursorDrawIndex = 0;
|
|
389
|
+
this.cursorInvertedOffset = -1;
|
|
388
390
|
this.cursorPos = 0;
|
|
391
|
+
this.cellData = 0;
|
|
389
392
|
this.interlacedSyncAndVideo = false;
|
|
390
393
|
this.doubledScanlines = true;
|
|
391
394
|
this.frameSkipCount = 0;
|
|
@@ -546,6 +549,8 @@ export class Video {
|
|
|
546
549
|
this.cursorOn = state.cursorOn;
|
|
547
550
|
this.cursorOff = state.cursorOff;
|
|
548
551
|
this.cursorOnThisFrame = state.cursorOnThisFrame;
|
|
552
|
+
this.cursorInvertedOffset = -1;
|
|
553
|
+
this.cellData = 0;
|
|
549
554
|
this.cursorDrawIndex = state.cursorDrawIndex;
|
|
550
555
|
this.cursorPos = state.cursorPos;
|
|
551
556
|
this.interlacedSyncAndVideo = state.interlacedSyncAndVideo;
|
|
@@ -599,6 +604,7 @@ export class Video {
|
|
|
599
604
|
if (this.frameCount % this.frameSkipCount) enable = 0;
|
|
600
605
|
}
|
|
601
606
|
this.dispEnabled |= enable;
|
|
607
|
+
this.cursorInvertedOffset = -1;
|
|
602
608
|
|
|
603
609
|
this.bitmapY = 0;
|
|
604
610
|
// Interlace even frame fires vsync midway through a scanline.
|
|
@@ -650,11 +656,7 @@ export class Video {
|
|
|
650
656
|
destOffset |= 0;
|
|
651
657
|
const offset = table4bppOffset(this.ulaMode, dat);
|
|
652
658
|
const fb32 = this.fb32;
|
|
653
|
-
|
|
654
|
-
// pixel colours directly from the NULA 12-bit colour table (collook).
|
|
655
|
-
// This skips the XOR-7 logical↔physical colour mapping that the
|
|
656
|
-
// standard ULA applies. Reference: b-em src/video.c lines 1083, 1117.
|
|
657
|
-
const colourLookup = this.ula.paletteMode ? this.ula.collook : this.ulaPal;
|
|
659
|
+
const colourLookup = this.pixelColours();
|
|
658
660
|
const table4bpp = this.table4bpp;
|
|
659
661
|
// Take advantage of numPixels being either 8 or 16
|
|
660
662
|
if (numPixels === 8) {
|
|
@@ -668,20 +670,70 @@ export class Video {
|
|
|
668
670
|
}
|
|
669
671
|
}
|
|
670
672
|
|
|
673
|
+
// The second half of a 16 pixel cell, for the 1MHz repaint.
|
|
674
|
+
blitFbSecondHalf(dat, destOffset) {
|
|
675
|
+
const offset = table4bppOffset(this.ulaMode, dat);
|
|
676
|
+
const colourLookup = this.pixelColours();
|
|
677
|
+
for (let i = 8; i < 16; ++i) {
|
|
678
|
+
this.fb32[destOffset + i] = colourLookup[this.table4bpp[offset + i]];
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// In NULA palette mode, bypass the ULA palette (ulaPal) and look up
|
|
683
|
+
// pixel colours directly from the NULA 12-bit colour table (collook).
|
|
684
|
+
// This skips the XOR-7 logical↔physical colour mapping that the
|
|
685
|
+
// standard ULA applies. Reference: b-em src/video.c lines 1083, 1117.
|
|
686
|
+
pixelColours() {
|
|
687
|
+
return this.ula.paletteMode ? this.ula.collook : this.ulaPal;
|
|
688
|
+
}
|
|
689
|
+
|
|
671
690
|
handleCursor(offset) {
|
|
672
691
|
if (this.cursorOnThisFrame && this.ulactrl & this.cursorTable[this.cursorDrawIndex]) {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
for (let i = 0; i < this.pixelsPerChar; ++i) {
|
|
678
|
-
this.fb32[offset + 1024 + i] ^= 0x00ffffff;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
692
|
+
this.invertForCursor(offset, 0);
|
|
693
|
+
this.cursorInvertedOffset = offset;
|
|
694
|
+
} else {
|
|
695
|
+
this.cursorInvertedOffset = -1;
|
|
681
696
|
}
|
|
682
697
|
if (++this.cursorDrawIndex === 7) this.cursorDrawIndex = 0;
|
|
683
698
|
}
|
|
684
699
|
|
|
700
|
+
invertForCursor(offset, fromPixel) {
|
|
701
|
+
for (let i = fromPixel; i < this.pixelsPerChar; ++i) {
|
|
702
|
+
this.fb32[offset + i] ^= 0x00ffffff;
|
|
703
|
+
}
|
|
704
|
+
if (this.doubledScanlines && !this.interlacedSyncAndVideo) {
|
|
705
|
+
for (let i = fromPixel; i < this.pixelsPerChar; ++i) {
|
|
706
|
+
this.fb32[offset + 1024 + i] ^= 0x00ffffff;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// The render loop paints a whole 1MHz cell on one 2MHz tick and skips the next, but the ULA
|
|
712
|
+
// output stage switches at 2MHz: a register write landing on the skipped tick changes the
|
|
713
|
+
// second half of the cell just painted. See https://github.com/mattgodbolt/jsbeeb/issues/766
|
|
714
|
+
repaintSecondHalfOfCell() {
|
|
715
|
+
if (!this.halfClock || !this.oddClock) return;
|
|
716
|
+
if ((this.dispEnabled & EVERYTHINGENABLED) !== EVERYTHINGENABLED) return;
|
|
717
|
+
if (this.bitmapX < 0 || this.bitmapX >= 1024 || this.bitmapY < 0 || this.bitmapY >= 625) return;
|
|
718
|
+
// The same line doubling decision as the render loop, which inlines it for speed.
|
|
719
|
+
const doubledLines =
|
|
720
|
+
(this.doubledScanlines && !this.interlacedSyncAndVideo) || this.isEvenRender === this.lastRenderWasEven;
|
|
721
|
+
const bitmapRow = doubledLines ? this.bitmapY & ~1 : this.bitmapY;
|
|
722
|
+
const offset = bitmapRow * 1024 + this.bitmapX;
|
|
723
|
+
const halfCell = this.pixelsPerChar >>> 1;
|
|
724
|
+
if (this.teletextMode) {
|
|
725
|
+
this.teletext.emitSecondHalf(this.fb32, offset);
|
|
726
|
+
} else {
|
|
727
|
+
this.blitFbSecondHalf(this.cellData, offset);
|
|
728
|
+
}
|
|
729
|
+
if (doubledLines) {
|
|
730
|
+
this.fb32.copyWithin(offset + 1024 + halfCell, offset + halfCell, offset + this.pixelsPerChar);
|
|
731
|
+
}
|
|
732
|
+
if (this.cursorInvertedOffset === offset) {
|
|
733
|
+
this.invertForCursor(offset, halfCell);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
685
737
|
setScreenHwScroll(viaScreenHwScroll) {
|
|
686
738
|
this.screenSubtract = this.screenAddrSubtract[viaScreenHwScroll];
|
|
687
739
|
}
|
|
@@ -984,11 +1036,15 @@ export class Video {
|
|
|
984
1036
|
// Read data from address pointer if both horizontal and vertical display enabled.
|
|
985
1037
|
const dat = this.readVideoMem();
|
|
986
1038
|
if (insideBorder) {
|
|
987
|
-
// Always
|
|
988
|
-
//
|
|
989
|
-
//
|
|
1039
|
+
// Always clock the SAA5050, whatever the ULA mode: IC15 latches the video bus
|
|
1040
|
+
// into the chip and it is MA13, not the ULA's teletext bit, that gates it. We
|
|
1041
|
+
// do not model the MA13 gate yet, so this feeds unconditionally. The chip is
|
|
1042
|
+
// clocked here and painted later, so a control code seen while the ULA shows
|
|
1043
|
+
// bitmap still takes effect.
|
|
990
1044
|
// See https://github.com/mattgodbolt/jsbeeb/issues/546
|
|
1045
|
+
// and https://github.com/mattgodbolt/jsbeeb/issues/832
|
|
991
1046
|
this.teletext.fetchData(dat);
|
|
1047
|
+
this.teletext.advance();
|
|
992
1048
|
|
|
993
1049
|
// Check cursor start.
|
|
994
1050
|
if (
|
|
@@ -1018,6 +1074,7 @@ export class Video {
|
|
|
1018
1074
|
}
|
|
1019
1075
|
|
|
1020
1076
|
const offset = bitmapRow * 1024 + this.bitmapX;
|
|
1077
|
+
this.cellData = dat;
|
|
1021
1078
|
|
|
1022
1079
|
if ((this.dispEnabled & EVERYTHINGENABLED) === EVERYTHINGENABLED) {
|
|
1023
1080
|
// Note this row's logical pixel size for display
|
|
@@ -1033,24 +1090,24 @@ export class Video {
|
|
|
1033
1090
|
if (this.teletextMode) {
|
|
1034
1091
|
if (this.halfClock) {
|
|
1035
1092
|
// Proper MODE 7 (1MHz clock + teletext): render SAA5050 output normally.
|
|
1036
|
-
this.teletext.
|
|
1093
|
+
this.teletext.emit(this.fb32, offset);
|
|
1037
1094
|
} else {
|
|
1038
|
-
// 2MHz clock + teletext bit set (the "TTX trick"): the
|
|
1039
|
-
//
|
|
1040
|
-
//
|
|
1041
|
-
// at
|
|
1095
|
+
// 2MHz clock + teletext bit set (the "TTX trick"): the SAA5050
|
|
1096
|
+
// outputs black. Behaviour confirmed by Rich Talbot-Watkins (RTW)
|
|
1097
|
+
// at ABUG 2026-03-13; the mechanism is not established, as the
|
|
1098
|
+
// Model B ULA has no connection to the SAA5050 at all.
|
|
1042
1099
|
// See https://github.com/mattgodbolt/jsbeeb/issues/546
|
|
1043
1100
|
this.fb32.fill(OPAQUE_BLACK, offset, offset + this.pixelsPerChar);
|
|
1044
1101
|
}
|
|
1045
1102
|
} else {
|
|
1046
|
-
this.blitFb(dat, offset, this.pixelsPerChar
|
|
1103
|
+
this.blitFb(dat, offset, this.pixelsPerChar);
|
|
1047
1104
|
}
|
|
1048
1105
|
if (doubledLines) {
|
|
1049
1106
|
this.fb32.copyWithin(offset + 1024, offset, offset + this.pixelsPerChar);
|
|
1050
1107
|
}
|
|
1051
1108
|
}
|
|
1052
1109
|
if (this.cursorDrawIndex) {
|
|
1053
|
-
this.handleCursor(offset
|
|
1110
|
+
this.handleCursor(offset);
|
|
1054
1111
|
}
|
|
1055
1112
|
}
|
|
1056
1113
|
}
|
|
@@ -1059,6 +1116,10 @@ export class Video {
|
|
|
1059
1116
|
// the SAA5050 pipeline with the video bus data, forcing bit 6 high.
|
|
1060
1117
|
// On real hardware IC37/IC36 operates regardless of ULA mode —
|
|
1061
1118
|
// it is wired to the CRTC DISPEN signal, not the ULA teletext bit.
|
|
1119
|
+
// Hardware also clocks the chip here, which we do not: our row reset fires
|
|
1120
|
+
// too early relative to the pipeline for that to come out right, and adding
|
|
1121
|
+
// `advance()` alone moves three of the hardware reference pages. See
|
|
1122
|
+
// https://github.com/mattgodbolt/jsbeeb/issues/874
|
|
1062
1123
|
if (!(this.dispEnabled & HDISPENABLE) && this.dispEnabled & VDISPENABLE) {
|
|
1063
1124
|
this.teletext.fetchData(this.readVideoMem() | 0x40);
|
|
1064
1125
|
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
0d=d:IFd VDUd:p=POINT(64*POS,1E3-VPOS*32):RETURN ELSEMODE2:GCOL0,-9:CLG
|
|
2
|
-
1d=9:REPEATVDU30:REPEATGOSUBFALSE:IFPOS=15VDU28,5,VPOS,14;11,26:IF0ELSEIFp=0PRINT:UNTIL0ELSEUNTILVPOS=25
|
|
3
|
-
2b=ABSRND MOD7:k=0:VDU31,9,3
|
|
4
|
-
3REPEATg=9-INKEY6MOD3
|
|
5
|
-
4FORl=TRUE TO1:o=l ANDSGNo
|
|
6
|
-
5IFo=l COLOURb-15:VDUl EORg:k=k+(g=7AND9-6*l)
|
|
7
|
-
6IF0ELSEFORf=0TO11:d=f/3OR2EORd:GOSUBFALSE
|
|
8
|
-
7IF2^((f+k)MOD12)AND975AND&C2590EC/8^b VDU2080*ABSl;:o=o+p:IF0ELSENEXT,
|
|
9
|
-
8VDU20:UNTILo*LOGg:UNTIL0
|