vic20: fix the text window geometry

The VIC-I draws a fixed 22x23 character window, centered in the picture.
The emulator used the VIC-II's display window registers, so the window was
placed at a garbage offset: the first character column was clipped in half
and the whole picture was shifted 24 pixels to the right to hide it.

- the new emulator core (chips) centers the window and takes its vertical
  position from the ROM, so NTSC and PAL ROMs both work
- the 24 pixel video shift is gone
- the platform loads res/vic20/vic20.bios (a real ROM set) when it is
  available, and falls back to the stub BIOS otherwise
- a regression test checks the geometry of the rendered frame
This commit is contained in:
Steven Hugg committed 2026-10-22 11:11:09 +02:00
1 parent 0063608f2a
commit 9aa2490dba
4 files changed
+118 -1

No files matched your search

+4
View File
@@ -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.
Binary file not shown.
+19 -1
View File
@@ -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);
+95
View File
@@ -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');
});
});