jsbeeb 1.12.0 → 1.13.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 +16 -2
- package/package.json +8 -8
- package/public/roms/atom/ATMMC3E.rom +0 -0
- package/public/roms/atom/Atom_Basic.rom +0 -0
- package/public/roms/atom/Atom_DOS.rom +0 -0
- package/public/roms/atom/Atom_FloatingPoint.rom +0 -0
- package/public/roms/atom/Atom_Kernel.rom +0 -0
- package/public/roms/atom/Atom_Kernel_E.rom +0 -0
- package/public/roms/atom/PCHARME.ROM +0 -0
- package/public/roms/atom/gags.rom +0 -0
- package/public/roms/atom/werom.rom +0 -0
- package/src/6502.js +344 -44
- package/src/6847.js +724 -0
- package/src/6847_fontdata.js +124 -0
- package/src/disc.js +2 -20
- package/src/fake6502.js +3 -2
- package/src/jsbeeb.css +23 -0
- package/src/keyboard.js +45 -23
- package/src/machine-session.js +85 -59
- package/src/main.js +142 -41
- package/src/mmc.js +1053 -0
- package/src/models.js +42 -1
- package/src/ppia.js +477 -0
- package/src/soundchip.js +99 -1
- package/src/tapes.js +73 -16
- package/src/url-params.js +7 -2
- package/src/utils.js +74 -1
- package/src/utils_atom.js +508 -0
- package/src/video.js +12 -1
- package/src/web/audio-handler.js +8 -3
- package/tests/test-machine.js +133 -8
package/src/6847.js
ADDED
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
import * as utils from "./utils.js";
|
|
3
|
+
import * as fontData from "./6847_fontdata.js";
|
|
4
|
+
|
|
5
|
+
const VDISPENABLE = 1 << 0,
|
|
6
|
+
HDISPENABLE = 1 << 1,
|
|
7
|
+
FRAMESKIPENABLE = 1 << 5;
|
|
8
|
+
// , EVERYTHINGENABLED = VDISPENABLE | HDISPENABLE | FRAMESKIPENABLE
|
|
9
|
+
/*
|
|
10
|
+
http://mdfs.net/Docs/Comp/Acorn/Atom/atap25.htm
|
|
11
|
+
|
|
12
|
+
25.5 Input/Output Port Allocations
|
|
13
|
+
The 8255 Programmable Peripheral Interface Adapter contains three 8-bit ports, and all but one of these lines is used by the ATOM.
|
|
14
|
+
|
|
15
|
+
Port A - #B000
|
|
16
|
+
Output bits: Function:
|
|
17
|
+
0 - 3 Keyboard row
|
|
18
|
+
4 - 7 Graphics mode
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
Hardware: PPIA 8255
|
|
23
|
+
|
|
24
|
+
output b000 0 - 3 keyboard row, 4 - 7 graphics mode
|
|
25
|
+
b002 0 cas output, 1 enable 2.4kHz, 2 buzzer, 3 colour set
|
|
26
|
+
|
|
27
|
+
input b001 0 - 5 keyboard column, 6 CTRL key, 7 SHIFT key
|
|
28
|
+
b002 4 2.4kHz input, 5 cas input, 6 REPT key, 7 60 Hz input
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
AG AS INTEXT INV GM2 GM1 GM0
|
|
33
|
+
-- -- ------ --- --- --- ---
|
|
34
|
+
0 0 0 0 X X X Internal Alphanumerics
|
|
35
|
+
0 0 0 1 X X X Internal Alphanumerics Inverted
|
|
36
|
+
0 0 1 0 X X X External Alphanumerics
|
|
37
|
+
0 0 1 1 X X X External Alphanumerics Inverted
|
|
38
|
+
0 1 0 X X X X Semigraphics 4
|
|
39
|
+
0 1 1 X X X X Semigraphics 6
|
|
40
|
+
1 X X X 0 0 0 Graphics CG1 (64x64x4) (16 bpr) #10
|
|
41
|
+
1 X X X 0 0 1 Graphics RG1 (128x64x2) (16 bpr) #30
|
|
42
|
+
1 X X X 0 1 0 Graphics CG2 (128x64x4) (32 bpr) #50
|
|
43
|
+
1 X X X 0 1 1 Graphics RG2 (128x96x2) (16 bpr) #70
|
|
44
|
+
1 X X X 1 0 0 Graphics CG3 (128x96x4) (32 bpr) #90
|
|
45
|
+
1 X X X 1 0 1 Graphics RG3 (128x192x2) (16 bpr) #b0
|
|
46
|
+
1 X X X 1 1 0 Graphics CG6 (128x192x4) (32 bpr) #d0
|
|
47
|
+
1 X X X 1 1 1 Graphics RG6 (256x192x2) (32 bpr) #f0
|
|
48
|
+
|
|
49
|
+
http://members.casema.nl/hhaydn/howel/logic/6847_clone.htm
|
|
50
|
+
|
|
51
|
+
256 = 256/8 = 32 32x1bpp = reg1:32 0x20
|
|
52
|
+
128 = 128/8 = 16 16x2bpp = reg1:32 0x20
|
|
53
|
+
|
|
54
|
+
128 = 128/8 = 16 16x1bpp = reg1:16 (xscale*2) 0x10
|
|
55
|
+
64 = 64/8 = 8 8x2bpp = reg1:16 (xscale*2) 0x10
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Data on PORTA used to set the screen mode; 0x10 means graphics mode, and then 0x20, 0x40, 0x80 are used to set the graphics mode.
|
|
59
|
+
# Data on PORTC used to set the colour select bit, which is used in conjunction with the graphics mode bits to select the colour of the screen.
|
|
60
|
+
# css = 0 means Black/Green
|
|
61
|
+
# css = 1 means Black/Orange
|
|
62
|
+
|
|
63
|
+
# PORTA is #b000 and PORTC is #b002
|
|
64
|
+
# bits 4-7 on #b000 are used to set the graphics mode, )
|
|
65
|
+
# and bit 3 on #b002 is used to set the colour select bit. (CSS connected to PC3 on 8255 via)
|
|
66
|
+
# eight bits 0-7
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
// video mode constants
|
|
70
|
+
//ppia PORTA
|
|
71
|
+
MODE_GM2 = 0x80; // only used if AG is 1 (bit 7)
|
|
72
|
+
MODE_GM1 = 0x40; // only used if AG is 1 (bit 6)
|
|
73
|
+
MODE_GM0 = 0x20; // only used if AG is 1 (bit 5)
|
|
74
|
+
MODE_AG = 0x10; // alpha or graphics (bit 4)
|
|
75
|
+
|
|
76
|
+
//ppia PORTC
|
|
77
|
+
MODE_CSS = 0x08; // colour select (bit 3)
|
|
78
|
+
|
|
79
|
+
when reading a byte from the VDG, looking at
|
|
80
|
+
// WITHIN THE VIDEO MEMORY (bit 6, 6, 7) (AS, INTEXT, INV resp.)
|
|
81
|
+
(INT/EXT & A/S connected to D6, and INV connected to D7 from CPU
|
|
82
|
+
|
|
83
|
+
This means D6 switches between internal alphanumeric, and semigraphics 6 (SG6)
|
|
84
|
+
|
|
85
|
+
// A/S-INT/EXT, INV can be changed character by character, and CSS on interrupt from CPU
|
|
86
|
+
// these not used if AG is 1, GM not used if AG is 0
|
|
87
|
+
|
|
88
|
+
for bits
|
|
89
|
+
|
|
90
|
+
CG1, CG2, CG3, CG6, RG6 are 2bpp
|
|
91
|
+
RG1, RG2, RG3 are 1bpp
|
|
92
|
+
|
|
93
|
+
?#B002=8
|
|
94
|
+
F.I=0TO255;I?#8000=I;N.
|
|
95
|
+
|
|
96
|
+
text colours are black/green (css=0) and black/orange (css=1); border black always
|
|
97
|
+
css not used for semigraphics 4 : THIS IS NOT AVAILABLE ON ATOM?
|
|
98
|
+
css is used for semigraphics 6,
|
|
99
|
+
they use 4 bits and 6 bits for luminance (0 off, 1 on)
|
|
100
|
+
and the last 2 or 3 bits are used for colour
|
|
101
|
+
(UNAVAILABLE) SG4 : 3 bits : 8 colours + black: green, yellow, blue, red , buff, cyan, magenta, orange
|
|
102
|
+
(PARTIALLY AVAILABLE) SG6 : 2 bits : 4 colours + black: css = 0, green, yellow, blue, red, css=1, buff, cyan, magenta, orange
|
|
103
|
+
only 1 bit is used of SG6 - to get yellow/red, cyan/orange
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
// const MODE_AG = 0x80,
|
|
107
|
+
// MODE_GM2 = 0x40,
|
|
108
|
+
// MODE_GM1 = 0x20,
|
|
109
|
+
// MODE_GM0 = 0x10;
|
|
110
|
+
|
|
111
|
+
// constant for the graphics mode
|
|
112
|
+
const MODE_AG = 0x10; // graphics mode
|
|
113
|
+
|
|
114
|
+
export class Video6847 {
|
|
115
|
+
constructor(video) {
|
|
116
|
+
this.video = video;
|
|
117
|
+
this.ppia = null; // set during reset
|
|
118
|
+
|
|
119
|
+
this.levelDEW = false;
|
|
120
|
+
this.levelDISPTMG = false;
|
|
121
|
+
|
|
122
|
+
// 8 colours (alpha on MSB)
|
|
123
|
+
//
|
|
124
|
+
this.collook = utils.makeFast32(
|
|
125
|
+
new Uint32Array([
|
|
126
|
+
0xff000000, // #00000000, // black
|
|
127
|
+
0xff03b91e, // #00ff00, // green
|
|
128
|
+
0xff00ffff, // #ffff00, // yellow
|
|
129
|
+
0xffff083b, // #3b08ff, // blue
|
|
130
|
+
0xff0516b9, // #b91605, // red
|
|
131
|
+
0xff018eb4, // #b48e01, // buff
|
|
132
|
+
0xffeb9200, // #0092eb, // cyan
|
|
133
|
+
0xffff1cff, // #ff1cff, // magenta
|
|
134
|
+
0xff005bbd, // #bd5b00, // orange
|
|
135
|
+
|
|
136
|
+
0xff000600, // dark green (char background) #000600
|
|
137
|
+
0xff000d1c, // dark orange (char background) #1c0d00
|
|
138
|
+
]),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// { 0, 0, 0 }, /*Black 0*/
|
|
142
|
+
// { 0, 63, 0 }, /*Green 1*/
|
|
143
|
+
// { 63, 63, 0 }, /*Yellow 2*/
|
|
144
|
+
// { 0, 0, 63 }, /*Blue 3 */
|
|
145
|
+
// { 63, 0, 0 }, /*Red 4 */
|
|
146
|
+
// { 63, 63, 63 }, /*Buff 5*/
|
|
147
|
+
// { 0, 63, 63 }, /*Cyan 6*/
|
|
148
|
+
// { 63, 0, 63 }, /*Magenta 7*/
|
|
149
|
+
// { 63, 32, 0 }, /*Orange 8 - can be red on the Atom*/
|
|
150
|
+
|
|
151
|
+
// /* dark green 9 */
|
|
152
|
+
// /* dark orange 10 */
|
|
153
|
+
|
|
154
|
+
this.interlacedSyncAndVideo = false;
|
|
155
|
+
// The 6847 blitters always write two framebuffer lines per pixel row,
|
|
156
|
+
// so Video.clearPaintBuffer() must clear both lines.
|
|
157
|
+
this.doubledScanlines = true;
|
|
158
|
+
|
|
159
|
+
this.regs = new Uint8Array(32);
|
|
160
|
+
this.bitmapX = 0;
|
|
161
|
+
this.bitmapY = 0;
|
|
162
|
+
this.frameCount = 0;
|
|
163
|
+
this.inHSync = false;
|
|
164
|
+
this.inVSync = false;
|
|
165
|
+
|
|
166
|
+
this.horizCounter = 0;
|
|
167
|
+
this.vertCounter = 0;
|
|
168
|
+
this.scanlineCounter = 0;
|
|
169
|
+
this.addr = 0;
|
|
170
|
+
this.lineStartAddr = 0;
|
|
171
|
+
this.nextLineStartAddr = 0;
|
|
172
|
+
|
|
173
|
+
this.pixelsPerChar = 8;
|
|
174
|
+
|
|
175
|
+
this.bitmapPxPerPixel = 2; // each pixel is 2 bitmap pixels wide and high
|
|
176
|
+
this.pixelsPerBit = this.bitmapPxPerPixel;
|
|
177
|
+
this.bpp = 1;
|
|
178
|
+
|
|
179
|
+
this.cpuAddr = 0;
|
|
180
|
+
this.dispEnabled = 0;
|
|
181
|
+
|
|
182
|
+
//PAL based = 312 lines
|
|
183
|
+
//NTSC based = 262 lines
|
|
184
|
+
// initialiser is outside the function to improve performance
|
|
185
|
+
this.modes = {
|
|
186
|
+
//perchar , pixpb,lines, bpp
|
|
187
|
+
0xf0: [8, 1, 1, 1], //clear4 256x192x2, pixels 1w1h MAIN MENU
|
|
188
|
+
0xb0: [16, 2, 1, 1], //clear3 128x192x2, pixels 2w1h BABIES
|
|
189
|
+
0x70: [16, 2, 2, 1], //clear2 128x96x2, pixels 2w2h 3D ASTEROIDS
|
|
190
|
+
0x30: [16, 2, 3, 1], //clear1 128x64x2 , pixels 3w4h (2w4h) 3D MAZE
|
|
191
|
+
0xd0: [8, 2, 1, 2], //?#B000=#D0 128x192x4,pixels 2w1h CHUCKIE EGG
|
|
192
|
+
0x90: [8, 2, 2, 2], //?#B000=#90 128x96x4, pixels 2w2h FLAPPY BIRD
|
|
193
|
+
0x50: [8, 2, 3, 2], //?#B000=#50 128x64x4 , pixels 3w3h (4w3h) BREAKOUT (maingame)
|
|
194
|
+
0x10: [16, 4, 3, 2], //?#B000=#10 64x64x4 , pixels 4w3h FIZZLE BRICKS
|
|
195
|
+
0x00: [8, -1, 12, 1], // clear0 //0,0 not used on Mode 0 (uses blitChar), pixelsPerBit, bpp
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// THESE MIDDLE THREE NUMBERS - THE SECOND AND THIRD ONE AFFECT THE VSYNC TIMING
|
|
199
|
+
// first is end of frame - i.e. full lines to display
|
|
200
|
+
// second is vsync start based on vertcounter
|
|
201
|
+
// third is total lines in a full frame
|
|
202
|
+
// vpulsewidth is how far through the individual scanline to end the vsync - i.e. sub scanline tine
|
|
203
|
+
|
|
204
|
+
this.lastmode = 0xff;
|
|
205
|
+
|
|
206
|
+
this.lastseconds = 0;
|
|
207
|
+
|
|
208
|
+
this.vdg_cycles = 0;
|
|
209
|
+
this.charTime = 0;
|
|
210
|
+
|
|
211
|
+
this.bordercolour = 0x00; // 0x00 black 0x01 // green or orange depending on CSS
|
|
212
|
+
|
|
213
|
+
this.init();
|
|
214
|
+
|
|
215
|
+
this.reset(null, null); // bit daft but it creates the members.
|
|
216
|
+
|
|
217
|
+
this.clearPaintBuffer();
|
|
218
|
+
this.paint();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
init() {
|
|
222
|
+
this.curGlyphs = fontData.makeCharsAtom();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
reset(cpu, ppia) {
|
|
226
|
+
this.cpu = cpu;
|
|
227
|
+
this.ppia = ppia;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// USE PAINT from VIDEO
|
|
231
|
+
paint() {
|
|
232
|
+
this.video.paint();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
clearPaintBuffer() {
|
|
236
|
+
this.video.interlacedSyncAndVideo = this.interlacedSyncAndVideo;
|
|
237
|
+
this.video.doubledScanlines = this.doubledScanlines;
|
|
238
|
+
this.video.frameCount = this.frameCount;
|
|
239
|
+
this.video.bitmapX = this.bitmapX;
|
|
240
|
+
this.video.bitmapY = this.bitmapY;
|
|
241
|
+
this.video.clearPaintBuffer();
|
|
242
|
+
}
|
|
243
|
+
// END
|
|
244
|
+
|
|
245
|
+
paintAndClear() {
|
|
246
|
+
// skip 5 frames
|
|
247
|
+
if (this.dispEnabled & FRAMESKIPENABLE) {
|
|
248
|
+
this.paint();
|
|
249
|
+
this.clearPaintBuffer();
|
|
250
|
+
}
|
|
251
|
+
this.dispEnabled &= ~FRAMESKIPENABLE;
|
|
252
|
+
let enable = FRAMESKIPENABLE;
|
|
253
|
+
if (this.frameCount % 5) enable = 0;
|
|
254
|
+
this.dispEnabled |= enable;
|
|
255
|
+
|
|
256
|
+
this.frameCount++;
|
|
257
|
+
this.bitmapY = 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/*
|
|
261
|
+
Snow is caused by the CPU changing the address bus for 500ns at the
|
|
262
|
+
same time as the VDG is expecting to get data from the address it has
|
|
263
|
+
requested, which takes 1100ns (1.1us). This only occurs if the cpu is
|
|
264
|
+
access graphics memory. So while the VDG is generating a scanline, the address
|
|
265
|
+
will change for 500ns based on the CPU memory accesses (read or write) and
|
|
266
|
+
the VDG will read 'noise' from the CPU addressed memory location instead.
|
|
267
|
+
*/
|
|
268
|
+
|
|
269
|
+
cpuAddrAccess(addr) {
|
|
270
|
+
// CPU has read from memory here
|
|
271
|
+
// VDG can read from this address for a cycle if it was video memory
|
|
272
|
+
// to generate snow
|
|
273
|
+
this.cpuAddr = addr;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// atom video memory is 0x8000->0x9fff (8k but only bottom 6k used)
|
|
277
|
+
// effecively goes up to 0x9800
|
|
278
|
+
readVideoMem() {
|
|
279
|
+
let cpuaddr = this.cpuAddr;
|
|
280
|
+
|
|
281
|
+
this.cpuAddr = 0; // reset the memory access by cpu but if it tries again it'll be set again
|
|
282
|
+
|
|
283
|
+
// during a vdg cycle, cpu might be active
|
|
284
|
+
if (this.vdg_cycles >= 0 && this.vdg_cycles < 1) {
|
|
285
|
+
if (cpuaddr >= 0x8000 && cpuaddr <= 0x9800) {
|
|
286
|
+
return this.cpu.videoRead(cpuaddr);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let memAddr = this.addr & 0x1fff;
|
|
291
|
+
memAddr |= 0x8000;
|
|
292
|
+
return this.cpu.videoRead(memAddr);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
dispEnableSet(flag) {
|
|
296
|
+
this.dispEnabled |= flag;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
dispEnableClear(flag) {
|
|
300
|
+
this.dispEnabled &= ~flag;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
setValuesFromMode(mode) {
|
|
304
|
+
mode = mode & 0xf0;
|
|
305
|
+
// In text mode (AG=0), the GM bits are ignored per MC6847 datasheet.
|
|
306
|
+
// Force mode to 0x00 so we don't index into undefined modes table entries.
|
|
307
|
+
if (!(mode & MODE_AG)) mode = 0x00;
|
|
308
|
+
|
|
309
|
+
// if no change in mode then do nothing
|
|
310
|
+
if (this.lastmode === mode) return;
|
|
311
|
+
|
|
312
|
+
this.lastmode = mode;
|
|
313
|
+
|
|
314
|
+
this.pixelsPerChar = this.modes[mode][0]; // 8 pixels per element
|
|
315
|
+
this.pixelsPerBit = this.bitmapPxPerPixel * this.modes[mode][1];
|
|
316
|
+
let linesPerRow = this.modes[mode][2]; // move to reg9
|
|
317
|
+
this.bpp = this.modes[mode][3];
|
|
318
|
+
|
|
319
|
+
this.charLinesreg9 = linesPerRow - 1; //2 - scanlines per char
|
|
320
|
+
|
|
321
|
+
//NEED TO RESET THE LINE IF
|
|
322
|
+
//MODE SWITCH MID FRAME
|
|
323
|
+
this.scanlineCounter = 0;
|
|
324
|
+
this.lineStartAddr = this.nextLineStartAddr;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// this is usually called from 'video' so 'this'
|
|
328
|
+
// is a reference to 'video'
|
|
329
|
+
polltimeFacade(clocks) {
|
|
330
|
+
if (this.video6847 != undefined) {
|
|
331
|
+
this.video6847.polltime(clocks);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
throw new Error("Video6847 not attached — polltimeFacade called before video.video6847 was initialised");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ATOM uses 6847 chip
|
|
338
|
+
polltime(clocks) {
|
|
339
|
+
const mode = this.ppia.portapins & 0xf0;
|
|
340
|
+
const css = (this.ppia.portcpins & 0x08) >>> 2;
|
|
341
|
+
this.setValuesFromMode(mode);
|
|
342
|
+
// Note: Polltime is called from the CPU many times during a frame. Once the VDG has drawn a bit of a frame
|
|
343
|
+
// it returns, and then comes back here later to continue the same frame. That's how the snow is able to work
|
|
344
|
+
// it doesn't draw the whole frame. It regularly gives control back to the CPU.
|
|
345
|
+
|
|
346
|
+
const vdgcharclock = this.pixelsPerChar / 2; // 4 or 8
|
|
347
|
+
// The MC6847 datasheet specifies 3.579545 MHz, but 3.638004 is used here
|
|
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;
|
|
352
|
+
|
|
353
|
+
const vdgframelines = 262; // 312 PAL (but no pal on standard atom) 262; // NTSC
|
|
354
|
+
const vdglinetime = 228; // vdg cycles to do a line; not 227.5
|
|
355
|
+
|
|
356
|
+
// full bordered width is 185.5 cycles
|
|
357
|
+
// (185.5+42 = 227.5)
|
|
358
|
+
const HBNK = 42; // left border start (16.5+25.5)
|
|
359
|
+
const leftborder = 29; // 29.5
|
|
360
|
+
const displayH = 128; //cycles - 186 cycles including borders - 228 for full horizontal
|
|
361
|
+
const rightborder = 29; //28.5
|
|
362
|
+
|
|
363
|
+
// total = 29+29+128+42 = 228
|
|
364
|
+
|
|
365
|
+
const vertblank = 13; //13
|
|
366
|
+
const topborder = 25; //25
|
|
367
|
+
const displayV = 192;
|
|
368
|
+
// let bottomborder = 26; // 26 + 6 = 32 = time in vsync
|
|
369
|
+
// let vertretrace = 6;
|
|
370
|
+
// ALL ADDS UP TO 262
|
|
371
|
+
|
|
372
|
+
// in vsync for 32 lines = bottomborder+vertretrace
|
|
373
|
+
// out vsync for 230 lines = vertblank+topborder+displayV
|
|
374
|
+
|
|
375
|
+
while (this.vdg_cycles >= 1) {
|
|
376
|
+
this.vdg_cycles -= 1;
|
|
377
|
+
this.charTime -= 1;
|
|
378
|
+
|
|
379
|
+
let nextChar = this.charTime <= 0;
|
|
380
|
+
|
|
381
|
+
if (nextChar) {
|
|
382
|
+
this.charTime += vdgcharclock;
|
|
383
|
+
this.bitmapX += this.pixelsPerChar * this.bitmapPxPerPixel;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (this.inHSync) {
|
|
387
|
+
// Start at -ve pos because new character is added before the pixel render
|
|
388
|
+
this.bitmapX = -this.pixelsPerChar * this.bitmapPxPerPixel;
|
|
389
|
+
|
|
390
|
+
this.bitmapY += this.bitmapPxPerPixel;
|
|
391
|
+
|
|
392
|
+
if (this.bitmapY >= 768) {
|
|
393
|
+
// Arbitrary moment when TV will give up and start flyback in the absence of an explicit VSync signal
|
|
394
|
+
this.paintAndClear();
|
|
395
|
+
}
|
|
396
|
+
this.inHSync = false;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// right border - record addr for next FULL line
|
|
400
|
+
if (this.horizCounter === HBNK + leftborder + displayH) this.nextLineStartAddr = this.addr;
|
|
401
|
+
|
|
402
|
+
// Stop drawing outside the right border
|
|
403
|
+
if (this.horizCounter === HBNK + leftborder + displayH) {
|
|
404
|
+
this.dispEnableClear(HDISPENABLE);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// left border - start the next line (not necessarily the FULL line)
|
|
408
|
+
if (this.horizCounter === HBNK + leftborder) {
|
|
409
|
+
this.dispEnableSet(HDISPENABLE);
|
|
410
|
+
this.addr = this.lineStartAddr;
|
|
411
|
+
this.charTime = 0;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// vertcounter runs from 0 to 262
|
|
415
|
+
// got to the end, start again
|
|
416
|
+
if (this.vertCounter === vertblank + topborder) {
|
|
417
|
+
this.scanlineCounter = 0;
|
|
418
|
+
this.nextLineStartAddr = 0;
|
|
419
|
+
this.lineStartAddr = this.nextLineStartAddr;
|
|
420
|
+
this.dispEnableSet(VDISPENABLE);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (this.vertCounter === vertblank + topborder + displayV) {
|
|
424
|
+
this.dispEnableClear(VDISPENABLE);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// reached end of the vdgline - start the hsync
|
|
428
|
+
if (this.horizCounter === HBNK + leftborder + displayH + rightborder && !this.inHSync) {
|
|
429
|
+
this.inHSync = true;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// image between 0 and 191 inc.
|
|
433
|
+
// vsync start (1) at line 192
|
|
434
|
+
// vsync end (0) at line 224
|
|
435
|
+
// 32 lines between inVSync and !inVSync
|
|
436
|
+
|
|
437
|
+
let vSyncEnding = false;
|
|
438
|
+
let vSyncStarting = false;
|
|
439
|
+
|
|
440
|
+
if (this.vertCounter === vertblank + topborder + displayV && !this.inVSync) {
|
|
441
|
+
vSyncStarting = true;
|
|
442
|
+
this.inVSync = true;
|
|
443
|
+
// Frame Sync is high normally and
|
|
444
|
+
// goes low when in VSync
|
|
445
|
+
// in VSync for 32 lines, and
|
|
446
|
+
// out of VSync for 262-32 lines
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (this.vertCounter === 0 && this.inVSync) {
|
|
450
|
+
vSyncEnding = true;
|
|
451
|
+
this.inVSync = false;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (!vSyncStarting && vSyncEnding) {
|
|
455
|
+
this.paintAndClear();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (vSyncStarting || vSyncEnding) {
|
|
459
|
+
this.ppia.setVBlankInt(this.inVSync);
|
|
460
|
+
|
|
461
|
+
// if (vSyncEnding)
|
|
462
|
+
// {
|
|
463
|
+
// let seconds = this.cpu.cycleSeconds+this.cpu.currentCycles/1000000.0;
|
|
464
|
+
// let diff = seconds - this.lastseconds;
|
|
465
|
+
// $("#vdg_text").html(
|
|
466
|
+
// "FPS "+(1/diff).toFixed(5)+" ("+diff.toFixed(5)+")<br>"
|
|
467
|
+
// );
|
|
468
|
+
// this.lastseconds = seconds ?? 0;
|
|
469
|
+
// }
|
|
470
|
+
|
|
471
|
+
// fix the border colour at the end/start of a frame
|
|
472
|
+
// it won't change within a frame
|
|
473
|
+
let AGM = (mode & MODE_AG) === 0;
|
|
474
|
+
|
|
475
|
+
if (AGM)
|
|
476
|
+
this.bordercolour = 0x00; // black
|
|
477
|
+
else this.bordercolour = 0x01; // green orange
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (nextChar) {
|
|
481
|
+
// once the whole of the Vertical and Horizontal is complete then do this
|
|
482
|
+
let insideBorder = (this.dispEnabled & (HDISPENABLE | VDISPENABLE)) === (HDISPENABLE | VDISPENABLE);
|
|
483
|
+
if (insideBorder) {
|
|
484
|
+
// read from video memory - uses this.addr
|
|
485
|
+
let dat = this.readVideoMem();
|
|
486
|
+
|
|
487
|
+
let offset = this.bitmapY;
|
|
488
|
+
offset = offset * 1024 + this.bitmapX;
|
|
489
|
+
// Render data depending on display enable state.
|
|
490
|
+
if (this.bitmapX >= 0 && this.bitmapX < 1024 && this.bitmapY < 625) {
|
|
491
|
+
{
|
|
492
|
+
// TODO: Add in the INTEXT modifiers to mode (if necessary)
|
|
493
|
+
// blit into the fb32 buffer which is painted by VIDEO
|
|
494
|
+
if ((mode & MODE_AG) === 0)
|
|
495
|
+
// MODE_AG - bit 4; 0x10 is the AG bit
|
|
496
|
+
this.blitChar(this.video.fb32, dat, offset, this.pixelsPerChar, css);
|
|
497
|
+
else this.blitPixels(this.video.fb32, dat, offset, css);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
} else {
|
|
501
|
+
// draw border (black, or green/buff depending on bordercolour and CSS)
|
|
502
|
+
if (this.bitmapX >= 0 && this.bitmapX < 1024 && this.bitmapY >= 0 && this.bitmapY < 625) {
|
|
503
|
+
const offset = this.bitmapY * 1024 + this.bitmapX;
|
|
504
|
+
this.blitBorder(this.video.fb32, this.bordercolour, offset, css);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
this.addr = (this.addr + 1) & 0x1fff;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// end of horizontal line
|
|
512
|
+
if (this.horizCounter === vdglinetime) {
|
|
513
|
+
let completedCharVertical = this.scanlineCounter === this.charLinesreg9; // regs9 - scanlines per char // 9 Maximum Raster Address
|
|
514
|
+
|
|
515
|
+
//keep drawing same memory addresses until end of scanlines
|
|
516
|
+
if (completedCharVertical) {
|
|
517
|
+
this.lineStartAddr = this.nextLineStartAddr;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
this.scanlineCounter += 1;
|
|
521
|
+
|
|
522
|
+
if (completedCharVertical) {
|
|
523
|
+
// this.hadVSyncThisRow = false;
|
|
524
|
+
this.scanlineCounter = 0;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// end of vertical frame was detected
|
|
528
|
+
if (this.vertCounter >= vdgframelines) {
|
|
529
|
+
this.vertCounter = 0;
|
|
530
|
+
} else {
|
|
531
|
+
this.vertCounter = (this.vertCounter + 1) & 0x1ff;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// start new horizontal line
|
|
535
|
+
this.horizCounter = 0;
|
|
536
|
+
} else {
|
|
537
|
+
this.horizCounter = (this.horizCounter + 1) & 0xff;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// // dump some data to CSV
|
|
541
|
+
// let a = this.cpu.cycleSeconds*1000000.0+this.cpu.currentCycles;
|
|
542
|
+
// let b = this.horizCounter;
|
|
543
|
+
// let c = this.vertCounter;
|
|
544
|
+
// let d = this.inVSync;
|
|
545
|
+
// let e = css;
|
|
546
|
+
|
|
547
|
+
// //using template literals for strings substitution
|
|
548
|
+
// if (this.cpu.cycleSeconds==10 && this.cpu.currentCycles<60000 && this.horizCounter == 0)
|
|
549
|
+
// {
|
|
550
|
+
// $("#csv_output").append(
|
|
551
|
+
// `<br>${a},${b},${c},${d},${e}`);
|
|
552
|
+
// }
|
|
553
|
+
} // matches while
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
blitBorder(buf, data, destOffset, css) {
|
|
557
|
+
const bpp = this.bpp;
|
|
558
|
+
const pixelsPerBit = this.pixelsPerBit / bpp;
|
|
559
|
+
const numPixels = 8 * pixelsPerBit; //per char
|
|
560
|
+
const fb32 = buf;
|
|
561
|
+
const colour = data === 0x00 ? this.collook[0] : this.collook[css ? 5 : 1];
|
|
562
|
+
for (let i = 0; i < numPixels; ++i) {
|
|
563
|
+
const n = numPixels - 1 - i; // pixels in reverse order
|
|
564
|
+
fb32[destOffset + n] = fb32[destOffset + n + 1024] = colour;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
blitPixels(buf, data, destOffset, css) {
|
|
569
|
+
// let scanline = this.scanlineCounterT;
|
|
570
|
+
// bitpattern from data is either 4 or 8 pixels in raw graphics
|
|
571
|
+
// either white and black
|
|
572
|
+
// or 3 colours and black (in pairs of bits)
|
|
573
|
+
// that is: all graphics modes are 1bpp or 2bpp giving 2 colours or 4 colours
|
|
574
|
+
|
|
575
|
+
const bitdef = data;
|
|
576
|
+
const bpp = this.bpp;
|
|
577
|
+
const pixelsPerBit = this.pixelsPerBit / bpp;
|
|
578
|
+
let colour; // see 'collook' // alpha, blue, green, red
|
|
579
|
+
|
|
580
|
+
// MODE NEED TO CHANGE THE RASTER SCAN
|
|
581
|
+
// currently 32 x 16 - 256 x 192 (with 12 lines per row)
|
|
582
|
+
|
|
583
|
+
// can get wide with 16 pixels
|
|
584
|
+
|
|
585
|
+
destOffset |= 0;
|
|
586
|
+
let fb32 = buf;
|
|
587
|
+
|
|
588
|
+
const numPixels = 8 * pixelsPerBit; //per char
|
|
589
|
+
// draw two,four pixels for each bit in the data to fill the width.
|
|
590
|
+
|
|
591
|
+
for (let i = 0; i < numPixels; i++) {
|
|
592
|
+
const n = numPixels - 1 - i; // pixels in reverse order
|
|
593
|
+
|
|
594
|
+
// get bits in pairs or singles
|
|
595
|
+
const j = Math.floor(i / pixelsPerBit);
|
|
596
|
+
|
|
597
|
+
// get just one bit
|
|
598
|
+
// RG modes
|
|
599
|
+
if (bpp === 1) {
|
|
600
|
+
// get a bit
|
|
601
|
+
const cval = (bitdef >>> j) & 0x1;
|
|
602
|
+
// CSS=0: green foreground, CSS=1: buff foreground
|
|
603
|
+
colour = cval !== 0 ? this.collook[css ? 5 : 1] : this.collook[0];
|
|
604
|
+
|
|
605
|
+
// two bitmap lines per 1 pixel
|
|
606
|
+
fb32[destOffset + n + 1024] = fb32[destOffset + n] = colour;
|
|
607
|
+
} // CG modes
|
|
608
|
+
else {
|
|
609
|
+
//let cval = (bitdef>>>(j&0xe))&0x3;
|
|
610
|
+
const cval = (bitdef >>> (j & 0xe)) & 0x3;
|
|
611
|
+
|
|
612
|
+
// 2 or 4 - green/yellow/blue/red
|
|
613
|
+
let colindex = 1 + (cval | (css << 1));
|
|
614
|
+
colour = this.collook[colindex];
|
|
615
|
+
|
|
616
|
+
// two bitmap lines per 1 pixel
|
|
617
|
+
fb32[destOffset + n + 1024] = fb32[destOffset + n] = colour;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
blitChar(buf, data, destOffset, numPixels, css) {
|
|
623
|
+
const scanline = this.scanlineCounter;
|
|
624
|
+
const chr = data & 0x7f;
|
|
625
|
+
|
|
626
|
+
// character set is just the pixel data
|
|
627
|
+
// 0 - 63 is alphachars green (0x00-0x3f) bits 0-5 for character
|
|
628
|
+
// 64-127 is 16 alphagraphics in 4 colours green/yellow/blue/red (0x40-0x7f) bit 6,7 (0xC0)
|
|
629
|
+
// bit 7 set is inverted and the alphagraphics again
|
|
630
|
+
// 128 - 191 is alphachars inverted green (0x80-0xbf)
|
|
631
|
+
// 192-255 is alphagraphics in different colours buff/cyan/magenta/orange (0xc0-0xff) bit 6,7 set (0xC0)
|
|
632
|
+
|
|
633
|
+
// in the data; bits 0-5 are the pixels
|
|
634
|
+
// bit 7 and CSS give the colour
|
|
635
|
+
// bit 6 indicates TEXT or GRAPHICS
|
|
636
|
+
// thus green and blue (css=0) alphagraphics cannot be accessed
|
|
637
|
+
// and buff and magenta (css=1) alphagraphics cannot be accessed
|
|
638
|
+
// bits 7,6
|
|
639
|
+
// [00] green text (or orange)
|
|
640
|
+
// [01] yellow graphics (or cyan)
|
|
641
|
+
// [10] inv green text (or inv orange)
|
|
642
|
+
// [11] red graphics (or orange)
|
|
643
|
+
|
|
644
|
+
// invert the char if bit 7 is set
|
|
645
|
+
const inv = !!(data & 0x80);
|
|
646
|
+
// graphics if bit 6 is set
|
|
647
|
+
const agmode = !!(data & 0x40);
|
|
648
|
+
|
|
649
|
+
//bitpattern for chars is in rows; each char is 12 rows deep
|
|
650
|
+
let chardef = this.curGlyphs[chr * 12 + scanline];
|
|
651
|
+
|
|
652
|
+
if (inv && !agmode) chardef = ~chardef;
|
|
653
|
+
|
|
654
|
+
numPixels |= 0;
|
|
655
|
+
numPixels *= this.bitmapPxPerPixel;
|
|
656
|
+
|
|
657
|
+
// can get wide with 16 pixels
|
|
658
|
+
const pixelsPerBit = numPixels / 8;
|
|
659
|
+
|
|
660
|
+
destOffset |= 0;
|
|
661
|
+
let fb32 = buf;
|
|
662
|
+
for (let i = 0; i < numPixels; ++i) {
|
|
663
|
+
const n = numPixels - 1 - i; // pixels in reverse order
|
|
664
|
+
const j = i / pixelsPerBit;
|
|
665
|
+
// text is either green/black or buff/black - nothing else
|
|
666
|
+
// css is 2 or 0 on input
|
|
667
|
+
let fgcol = this.collook[css ? 5 : 1];
|
|
668
|
+
|
|
669
|
+
if (agmode) {
|
|
670
|
+
// alphagraphics 6
|
|
671
|
+
// inv css | colour
|
|
672
|
+
// 0 0 | yellow (2) 10 + 0000
|
|
673
|
+
// 1 0 | red (4) 10 + 0010
|
|
674
|
+
// 0 2 | cyan (6) 10 + 0100
|
|
675
|
+
// 1 2 | orange (8) 10 + 0110
|
|
676
|
+
// css is 2 or 0 on input
|
|
677
|
+
fgcol = this.collook[2 + ((inv | css) << 1)];
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const luminance = (chardef >>> j) & 0x1;
|
|
681
|
+
const colour = luminance ? fgcol : this.collook[css ? 10 : 9]; //dark orange or green
|
|
682
|
+
fb32[destOffset + n] = fb32[destOffset + n + 1024] = // two lines
|
|
683
|
+
colour;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/* Video modes:
|
|
689
|
+
Alpha internal
|
|
690
|
+
Alpha external
|
|
691
|
+
SemiGraphics Four 0011 CLEAR 1
|
|
692
|
+
SemiGraphics Six 0111 CLEAR 2
|
|
693
|
+
|
|
694
|
+
0001 ?#B000=#10 1a
|
|
695
|
+
Colour Graphics 1 - 8 colours (CSS/C1/C0) - 64x64
|
|
696
|
+
1024 bytes - 2bpp (4x3) - 4 pixels x 3 rows
|
|
697
|
+
|
|
698
|
+
0011 clear 1
|
|
699
|
+
Resolution Graphics 1 - 4 colours (Lx) - 128x64
|
|
700
|
+
1024 bytes - 1bpp (3x3)
|
|
701
|
+
|
|
702
|
+
0101 ?#B000=#50 2a
|
|
703
|
+
Colour Graphics 2 - 8 colours (CSS/C1/C0) - 128x64
|
|
704
|
+
2048 bytes - 2bpp (3x3)
|
|
705
|
+
1011 CLEAR 2
|
|
706
|
+
Resolution Graphics 2 - 4 colours (Lx) - 128x96
|
|
707
|
+
1536 bytes - 1bpp (2x2)
|
|
708
|
+
|
|
709
|
+
1001 ?#B000=#90 3a
|
|
710
|
+
Colour Graphics 3 - 8 colours (CSS/C1/C0) - 128x96
|
|
711
|
+
3072 bytes - 2bpp (2x2)
|
|
712
|
+
1011 clear 3
|
|
713
|
+
Resolution Graphics 3 - 4 colours (Lx)- 128x192
|
|
714
|
+
3072 bytes - 1bpp (2x1)
|
|
715
|
+
|
|
716
|
+
110 ?#B000=#d0 4a
|
|
717
|
+
Colour Graphics 6 - 8 colours (CSS/C1/C0) - 128x192
|
|
718
|
+
6144 bytes - 2bpp (4x1)
|
|
719
|
+
1111 clear 4
|
|
720
|
+
Resolution Graphics 6 - 4 colours (Lx) 256x192
|
|
721
|
+
6144 bytes - 1bpp (8x1)
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
*/
|