diff --git a/CHANGELOG.md b/CHANGELOG.md index 6043635e..a0b0e82e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Emulators + +- **VIC-20**: the text window is drawn correctly now. The VIC-I emulated the VIC-II's display window registers, so the 22 column window was placed at a garbage offset: the first character column was clipped in half and the screen was shifted 24 pixels to the right. The VIC-I window is fixed at 22x23 characters and centered in the picture; its vertical position still comes from the ROM (the "upper border" register), so NTSC and PAL ROMs both work. + ### Editor and UI - The IDE now works in Firefox: a synchronous XHR with a `responseType` hangs in Firefox when the response is 1MB or more, so the build worker hung forever on platforms with a big toolchain package (Game Boy, Atari 8-bit, Apple II, ...). The worker now fetches those packages as ArrayBuffers. A worker that dies no longer leaves the IDE spinning: the error is shown and the build stops. diff --git a/res/vic20/vic20.wasm b/res/vic20/vic20.wasm index 34d9f1aa..c1603e4c 100755 Binary files a/res/vic20/vic20.wasm and b/res/vic20/vic20.wasm differ diff --git a/src/machine/vic20.ts b/src/machine/vic20.ts index 964d14b1..cf78ffec 100644 --- a/src/machine/vic20.ts +++ b/src/machine/vic20.ts @@ -15,7 +15,9 @@ export class VIC20_WASMMachine extends BaseWASMMachine implements Machine, Probe numTotalScanlines = 312; cpuCyclesPerLine = 71; - videoOffsetBytes = -24 * 4; + // the VIC-I draws its 22x23 character window centered in the frame, + // so no video offset is needed + videoOffsetBytes = 0; prgstart : number; joymask0 = 0; @@ -27,6 +29,22 @@ export class VIC20_WASMMachine extends BaseWASMMachine implements Machine, Probe super.loadBIOS(srcArray); } async fetchBIOS() { + // use the real VIC-20 ROM set (BASIC, character ROM and KERNAL) if it is + // available, otherwise fall back to a tiny stub BIOS that only knows how + // to start an autostart cartridge + try { + const resp = await fetch('res/vic20/vic20.bios'); + if (resp.status == 200) { + const bios = new Uint8Array(await resp.arrayBuffer()); + if (bios.length == 0x5000) { + this.allocateBIOS(bios); + this.loadBIOS(bios); + return; + } + } + } catch (e) { + // no ROM set available, use the stub BIOS below + } let bios = new Uint8Array(20480); bios.set(DEFAULT_BIOS, bios.length - DEFAULT_BIOS.length); this.allocateBIOS(bios); diff --git a/test/cli/testvic20.js b/test/cli/testvic20.js new file mode 100644 index 00000000..c7f0444d --- /dev/null +++ b/test/cli/testvic20.js @@ -0,0 +1,95 @@ +var assert = require('assert'); +var { loadPlatform } = require('../../gen/tools/emutarget.js'); + +/* + A tiny cartridge that programs the VIC-I and draws two characters, so + that the text window can be checked. + + The cartridge file has a 2 byte load address, then the cold and warm + start vectors, then the "A0CBM" signature; the code is loaded at $A009: + + $A009 A9 1B LDA #$1B ; bg color white, border color cyan + $A00B 8D 0F 90 STA $900F + $A00D A9 19 LDA #$19 ; upper border = raster line 25 + $A00F 8D 01 90 STA $9001 + $A011 A9 F0 LDA #$F0 ; screen pointer A13..A10 + $A013 8D 05 90 STA $9005 + $A015 A9 96 LDA #$96 ; screen pointer A9 + $A017 8D 02 90 STA $9002 + $A019 A9 00 LDA #$00 ; clear the screen page + $A01B A2 00 LDX #$00 + $A01D 9D 00 1E STA $1E00,X + $A020 9D 00 1F STA $1F00,X + $A023 E8 INX + $A024 D0 F7 BNE $A01D + $A026 A9 06 LDA #$06 ; clear the color RAM, blue + $A028 A2 00 LDX #$00 + $A02A 9D 00 96 STA $9600,X + $A02D E8 INX + $A02E E0 28 CPX #$28 + $A030 D0 F8 BNE $A02A + $A032 A9 48 LDA #'H' + $A034 8D 00 1E STA $1E00 + $A037 A9 49 LDA #'I' + $A039 8D 01 1E STA $1E01 + $A03C 4C 40 A0 JMP $A040 + $A03F 00 + $A040 4C 40 A0 JMP $A040 +*/ +var CART = [ + 0x00, 0xA0, 0x09, 0xA0, 0x09, 0xA0, 0x41, 0x30, 0xC3, 0xC2, 0xCD, 0xA9, + 0x1B, 0x8D, 0x0F, 0x90, 0xA9, 0x19, 0x8D, 0x01, 0x90, 0xA9, 0xF0, 0x8D, + 0x05, 0x90, 0xA9, 0x96, 0x8D, 0x02, 0x90, 0xA9, 0x00, 0xA2, 0x00, 0x9D, + 0x00, 0x1E, 0x9D, 0x00, 0x1F, 0xE8, 0xD0, 0xF7, 0xA9, 0x06, 0xA2, 0x00, + 0x9D, 0x00, 0x96, 0xE8, 0xE0, 0x28, 0xD0, 0xF8, 0xA9, 0x48, 0x8D, 0x00, + 0x1E, 0xA9, 0x49, 0x8D, 0x01, 0x1E, 0x4C, 0x40, 0xA0, +]; + +var CYAN = 0xFFFFF04D; + +describe('VIC-20 video', function () { + it('should draw the 22 column text window centered in the frame', async function () { + this.timeout(60000); + var target = await loadPlatform('vic20'); + await target.start(); + var video = target.getVideo(); + assert.equal(video.width, 232); + assert.equal(video.height, 272); + // load the cartridge that programs the VIC + await target.loadROM(new Uint8Array(CART), 'cart.bin'); + // run until the cartridge has set up the screen (with a real KERNAL + // this takes a moment, with the stub BIOS it is immediate) + var color = function () { return -1; }; + for (var i = 0; i < 300; i++) { + target.advanceFrame(); + video = target.getVideo(); + var px = video.pixels; + color = function (x, y) { return px[y * video.width + x]; }; + if (color(0, 100) == CYAN) break; + } + assert.equal(color(0, 100), CYAN, 'the cartridge screen did not appear'); + // the cartridge sets the colors before the window position, so let + // a few more frames pass before the picture is checked + for (var i = 0; i < 5; i++) { + target.advanceFrame(); + } + video = target.getVideo(); + var px = video.pixels; + color = function (x, y) { return px[y * video.width + x]; }; + // the text window is 22 columns (176 pixels) wide and centered, so + // there are 28 pixels of border color on either side. The pixels + // inside the window are the background color or a character, but + // never the border color. + assert.equal(color(0, 100), CYAN, 'left border'); + assert.equal(color(27, 100), CYAN, 'left border'); + assert.notEqual(color(28, 100), CYAN, 'first text column'); + assert.notEqual(color(203, 100), CYAN, 'last text column'); + assert.equal(color(204, 100), CYAN, 'right border'); + assert.equal(color(231, 100), CYAN, 'right border'); + // the window is 23 rows (184 lines) tall, starting at raster line 25 + assert.equal(color(100, 25), CYAN, 'above the text window'); + assert.notEqual(color(100, 26), CYAN, 'first text row'); + assert.notEqual(color(100, 209), CYAN, 'last text row'); + assert.equal(color(100, 210), CYAN, 'below the text window'); + }); +});