badline 0.9.0 → 0.10.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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/CLAUDE.md +6 -4
  4. data/README.md +39 -4
  5. data/doc/pinned-behaviour.md +146 -0
  6. data/doc/snapshots.md +13 -0
  7. data/lib/badline/address_bus/pla.rb +130 -0
  8. data/lib/badline/address_bus.rb +9 -88
  9. data/lib/badline/chrout_trap.rb +4 -3
  10. data/lib/badline/computer/attachments.rb +47 -7
  11. data/lib/badline/computer/saved_state.rb +1 -0
  12. data/lib/badline/computer.rb +44 -29
  13. data/lib/badline/control_ports.rb +10 -3
  14. data/lib/badline/core.rb +3 -0
  15. data/lib/badline/datasette.rb +39 -1
  16. data/lib/badline/drive1541/serial_port.rb +5 -8
  17. data/lib/badline/drive1541/sleep.rb +5 -6
  18. data/lib/badline/drive1541.rb +3 -3
  19. data/lib/badline/frontend/app.rb +62 -64
  20. data/lib/badline/frontend/boot.rb +33 -8
  21. data/lib/badline/frontend/controls.rb +12 -6
  22. data/lib/badline/frontend/drive_led.rb +9 -10
  23. data/lib/badline/frontend/frame_rate.rb +4 -4
  24. data/lib/badline/frontend/machine_window.rb +87 -0
  25. data/lib/badline/frontend/menu_media.rb +49 -24
  26. data/lib/badline/frontend/menu_pages.rb +23 -40
  27. data/lib/badline/frontend/menu_ports.rb +69 -0
  28. data/lib/badline/frontend/pacer.rb +6 -5
  29. data/lib/badline/frontend/pause_menu.rb +30 -33
  30. data/lib/badline/frontend/player_screen.rb +1 -1
  31. data/lib/badline/frontend/player_window.rb +1 -1
  32. data/lib/badline/frontend/screen.rb +39 -37
  33. data/lib/badline/frontend/sid_view.rb +1 -1
  34. data/lib/badline/frontend/sound.rb +11 -9
  35. data/lib/badline/frontend/timeline.rb +36 -5
  36. data/lib/badline/frontend.rb +2 -0
  37. data/lib/badline/iec_bus.rb +16 -28
  38. data/lib/badline/kernal_trap/drive.rb +3 -0
  39. data/lib/badline/kernal_trap/file.rb +1 -1
  40. data/lib/badline/kernal_trap/layout.rb +112 -0
  41. data/lib/badline/kernal_trap/load.rb +16 -27
  42. data/lib/badline/kernal_trap/routine.rb +11 -27
  43. data/lib/badline/kernal_trap/save.rb +13 -23
  44. data/lib/badline/kernal_trap/serial.rb +21 -19
  45. data/lib/badline/kernal_trap.rb +1 -0
  46. data/lib/badline/keyboard.rb +17 -14
  47. data/lib/badline/machine.rb +60 -0
  48. data/lib/badline/media/vic20_basic.rb +54 -0
  49. data/lib/badline/media/vic20_cartridge.rb +74 -0
  50. data/lib/badline/media/vic20_media.rb +132 -0
  51. data/lib/badline/media.rb +32 -18
  52. data/lib/badline/options/event.rb +5 -3
  53. data/lib/badline/options/help.rb +11 -3
  54. data/lib/badline/options/table.rb +11 -1
  55. data/lib/badline/options/validation.rb +55 -4
  56. data/lib/badline/options.rb +33 -33
  57. data/lib/badline/roms/README +27 -0
  58. data/lib/badline/roms/vic20/basic.rom +0 -0
  59. data/lib/badline/roms/vic20/character.rom +0 -0
  60. data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
  61. data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
  62. data/lib/badline/snapshot/container.rb +2 -0
  63. data/lib/badline/snapshot/image.rb +15 -0
  64. data/lib/badline/snapshot/state.rb +4 -3
  65. data/lib/badline/snapshot.rb +10 -0
  66. data/lib/badline/storage/crt_file.rb +16 -5
  67. data/lib/badline/storage/disk_image.rb +3 -0
  68. data/lib/badline/storage/host_directory.rb +3 -0
  69. data/lib/badline/storage/t64.rb +3 -0
  70. data/lib/badline/storage/tap.rb +38 -4
  71. data/lib/badline/storage/unavailable.rb +2 -0
  72. data/lib/badline/timing.rb +12 -0
  73. data/lib/badline/version.rb +1 -1
  74. data/lib/badline/via/shift_register.rb +43 -16
  75. data/lib/badline/vic/bank.rb +53 -16
  76. data/lib/badline/vic/registers.rb +20 -4
  77. data/lib/badline/vic.rb +34 -15
  78. data/lib/badline/vic20/attachments.rb +103 -0
  79. data/lib/badline/vic20/bus.rb +308 -0
  80. data/lib/badline/vic20/cpu.rb +10 -0
  81. data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
  82. data/lib/badline/vic20/port_wiring.rb +50 -0
  83. data/lib/badline/vic20/saved_state.rb +152 -0
  84. data/lib/badline/vic20/sound/output.rb +83 -0
  85. data/lib/badline/vic20/sound.rb +233 -0
  86. data/lib/badline/vic20/user_via_ports.rb +47 -0
  87. data/lib/badline/vic20/vic/painter.rb +239 -0
  88. data/lib/badline/vic20/vic/saved_state.rb +55 -0
  89. data/lib/badline/vic20/vic/text_window.rb +71 -0
  90. data/lib/badline/vic20/vic.rb +267 -0
  91. data/lib/badline/vic20.rb +220 -0
  92. metadata +28 -2
  93. data/lib/badline/address_bus/ultimax_pages.rb +0 -38
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2916044b89e174698de9dd60a2a72121dc6aa91058a9de914dfbaa5e70170f29
4
- data.tar.gz: 56adae369f806a6adb31dea00b5164d6e02e4b88d6a6cc08209808abdae0392b
3
+ metadata.gz: bd764dccd3b8af3f8e0dcbb7d17debc3fceb24a9899f80871ebbf184ffefd65b
4
+ data.tar.gz: 46a62d801813476242a407c09ab3ff4fec1126d10bfee134047f18e90fd86955
5
5
  SHA512:
6
- metadata.gz: cac3c6ba20fff4bbabd7e77dd8eae4e53cb12a3785f946908407627eb29783764684c0e2cf208b6a8030ad31bcfb35811526873119d51cb5badd0f2598257b41
7
- data.tar.gz: 93cecca78dbb7f4ca6393a629d113fa80a68a573359193ae4bcc54b87aadea883fdba0f84ad3ccb8acbad54539867b676e82e9f940420b50365f0fc7b93c33ff
6
+ metadata.gz: 82a62fb4a3de3ef65e123bacf22ba300efe628f1bde38815dc950a39034d8c2b9423de79b3805fce84e19bcbaa8100a3e67488d5ef0a3ecb2de6434689886883
7
+ data.tar.gz: 595b19f034d708db60089e707507e74281059d271527fe61e0a205d9e8e0853a4f6b4d077aeed648f59a86d06bf31d3ee6e5902345fdb52d5fb36ded8de94da3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.0](https://github.com/elektronaut/badline/compare/v0.9.0...v0.10.0) (2026-10-08)
4
+
5
+
6
+ ### Features
7
+
8
+ * add the VIC-20's bus and CPU ([184f4c2](https://github.com/elektronaut/badline/commit/184f4c28c609fe0d46358c0143fabe2beba2fa70))
9
+ * name the machine family with a c64 or vic20 subcommand ([586a02f](https://github.com/elektronaut/badline/commit/586a02fbd80c6e6351d85c91263b5a39438ba13e))
10
+ * open the pause menu from the timeline and screenshot it ([63dc744](https://github.com/elektronaut/badline/commit/63dc74468432397c1ec0273a64fcd28ff46008d2))
11
+ * paint the VIC-20's display from the VIC-I's fetches ([76f67c3](https://github.com/elektronaut/badline/commit/76f67c3709363f682715681d17ea799441412f12))
12
+ * play the VIC-20's sound from the VIC-I's voices ([c490a86](https://github.com/elektronaut/badline/commit/c490a86959e18e0fecdcf21bf0139b63be25d9a9))
13
+ * read the VIC-20 .crt signature and the .tap platform byte ([fc0b130](https://github.com/elektronaut/badline/commit/fc0b130ad3a9481e7a9e61d909286281026797b1))
14
+ * run the VIC-20 in the window, in both builds ([24c056c](https://github.com/elektronaut/badline/commit/24c056c531fb356d3d47a9aad94c50ba0a0b0031))
15
+ * run the VIC-20 with its VIC-I registers and VIAs ([6089cee](https://github.com/elektronaut/badline/commit/6089ceee361e65d59c33a1eaeb3395f315eeed2d))
16
+ * save and restore the VIC-20 in badline's own snapshots ([8620849](https://github.com/elektronaut/badline/commit/8620849be5177069e16a559a35b239d75d7cc82a))
17
+ * serve the VIC-20's device 8 through the KERNAL traps and autostart its media ([356695e](https://github.com/elektronaut/badline/commit/356695e7529f999acf4bdc82951b107460ecdf39))
18
+ * wire the VIC-20's datasette and serial bus to its VIAs, with a true 1541 ([2f06e88](https://github.com/elektronaut/badline/commit/2f06e88cf5276780cc03f7d8121480e95b05480f))
19
+ * wire the VIC-20's keyboard, joystick and RESTORE to its VIAs ([32139f6](https://github.com/elektronaut/badline/commit/32139f647aeb3f5e7601928150b85973db6b9b5e))
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * show the disk device 8 holds on the menu's Drive page ([c8b0eaf](https://github.com/elektronaut/badline/commit/c8b0eaf05573246114af6b6f8404095424a1981c))
25
+ * start the VIA shift register only when idle, on the 6522's clock ([bdcb1c8](https://github.com/elektronaut/badline/commit/bdcb1c88bb729e48ffdaaffd91e1274778c4ee29))
26
+
27
+
28
+ ### Performance Improvements
29
+
30
+ * type Memory's storage as Array[Integer] for Spinel ([797c9b1](https://github.com/elektronaut/badline/commit/797c9b1cab26e6ace4f0d0a20011a3a084e73bc9))
31
+
3
32
  ## [0.9.0](https://github.com/elektronaut/badline/compare/v0.8.0...v0.9.0) (2026-10-05)
4
33
 
5
34
 
data/CLAUDE.md CHANGED
@@ -50,7 +50,7 @@ requires only the namespace file.
50
50
  | rspec (`spec/`) | `bundle exec rspec` | Unit behaviour, all of `lib/` |
51
51
  | SingleStepTests 65x02 | `rake test` (100 sampled cases per opcode) | CPU, per-cycle bus traces |
52
52
  | Wolfgang Lorenz suite | `bin/lorenz` | CPU, CIA, interrupts |
53
- | VICE testbench | `bin/testbench <subtree>` | `VICII/`, `CIA/`, `interrupts/`, `CPU/`, cartridges with `--carts`, and the true 1541 drive with `--drive` |
53
+ | VICE testbench | `bin/testbench <subtree>` | `VICII/`, `CIA/`, `interrupts/`, `CPU/`, cartridges with `--carts`, the true 1541 drive with `--drive`, and the VIC-20 with `--vic20` |
54
54
  | VICE SID testprogs | `bin/sidtests` | SID |
55
55
  | Drive scenarios | `bin/drive_scenarios` | The true 1541 running the DOS ROM: save, format, the error channel, idle, write protect and autostart |
56
56
  | CIA offline grids | `bundle exec rspec --tag slow spec/badline/cia` | CIA timers and shift register, against a bare CIA in about 2 min |
@@ -144,7 +144,7 @@ overrides that, capped by the core count, but keep the default, because
144
144
  other worktrees share the machine. Whole runs at 4 shards on an M-series
145
145
  laptop take 15 min for `testbench` (`VICII/`), 11 for `testbench-cia`, 1 for
146
146
  `testbench-interrupts`, 37 for `testbench-irqdma`, 11 for
147
- `testbench-cpu`, 2 for `testbench-carts` and about 25 for `testbench-expansions`. `sid` takes about 12 min. `lorenz` chains itself and takes
147
+ `testbench-cpu`, 2 for `testbench-carts`, about 25 for `testbench-expansions` and under 1 for `testbench-vic20`. `sid` takes about 12 min. `lorenz` chains itself and takes
148
148
  about 2.5 h on CI whole. `rake regression:lorenz-1` to `lorenz-4` run it as
149
149
  four stretches of about 40 min each, and they can run side by side.
150
150
  `test/baselines/README.md` has the full table. A killed `bin/testbench` run
@@ -187,14 +187,16 @@ the rows your change can't reach tell you nothing about it.
187
187
  Pick the filters from the suites your change can move: VIC → `testbench`,
188
188
  plus `testbench-vicii-new` (`bin/testbench --vicii-new`, the 8565) for
189
189
  anything the VIC model reaches, and `testbench-ntsc` (`bin/testbench --ntsc`,
190
- the 6567R8 and 6567R56A) for anything a region's timing reaches;
190
+ the 6567R8 and 6567R56A) and `testbench-drean` (`bin/testbench --drean`,
191
+ the 6572) for anything a region's timing reaches;
191
192
  CPU, interrupts or timing → the matching `testbench-*` suite, plus
192
193
  `rake test` for CPU; CIA → the slow CIA specs first, then the matching
193
194
  `testbench-cia` rows, plus `testbench-cia-new` (`bin/testbench --cia-new`,
194
195
  the 6526A) for anything the interrupt register or the CIA model reaches; cartridge mappers, banking or power-on state →
195
196
  `testbench-carts`, plus `testbench-expansions` for banking; GEO-RAM, +60K,
196
197
  +256K, the REU or the VIC's BA line the REU follows → `testbench-expansions`; the 1541 drive, VIA or IEC bus →
197
- `testbench-drive` (`bin/testbench --drive`) and `drive-scenarios` (`bin/drive_scenarios <filter>`); SID → `sid`, plus `sid-8580` for anything the 8580
198
+ `testbench-drive` (`bin/testbench --drive`) and `drive-scenarios` (`bin/drive_scenarios <filter>`), plus `testbench-vic20` for the VIA; the VIC-20 (`vic20/`) →
199
+ `testbench-vic20` (`bin/testbench --vic20`); SID → `sid`, plus `sid-8580` for anything the 8580
198
200
  model reaches (`bin/sidtests --sid 8580`). Leave the Lorenz chain to CI,
199
201
  unless your code has a rule in `doc/pinned-behaviour.md` that names Lorenz
200
202
  tests: the interrupt polling, CPU port and CIA timer rules. Run just the
data/README.md CHANGED
@@ -3,6 +3,8 @@
3
3
 
4
4
  # Badline
5
5
 
6
+ ![A C64 booted to BASIC in badline, its cursor blinking](doc/images/boot.gif)
7
+
6
8
  Badline is a Commodore 64 emulator written in Ruby. It emulates a PAL
7
9
  or NTSC machine one clock cycle at a time, stepping the 6510, the VIC-II, both
8
10
  CIAs and the SID together, so raster timing, bad lines and sprite DMA
@@ -10,8 +12,8 @@ are modelled at the cycle level.
10
12
 
11
13
  It runs programs, disk and tape images, cartridges and SID tunes, and
12
14
  the SDL2 front end supports the keyboard, joysticks, game controllers,
13
- paddles and a 1351 mouse. See [What's emulated](#whats-emulated) for the
14
- details.
15
+ paddles and a 1351 mouse. It also runs a PAL VIC-20, as
16
+ `badline vic20`. See [What's emulated](#whats-emulated) for the details.
15
17
 
16
18
  It comes in two builds of the same emulator:
17
19
 
@@ -65,6 +67,10 @@ Programs, disk and tape images and SID tunes start automatically, and
65
67
  a cartridge starts itself. A mounted directory waits for you to `LOAD`
66
68
  from it.
67
69
 
70
+ The first argument can name the machine, as in `badline c64 game.prg`.
71
+ The C64 is the default, and `--model` picks which C64. See
72
+ [The VIC-20](#the-vic-20) for `badline vic20`.
73
+
68
74
  `badline-ruby` takes the same media and the same options, with the
69
75
  differences noted in the table. `--help` lists the options for either.
70
76
 
@@ -77,8 +83,9 @@ differences noted in the table. `--help` lists the options for either.
77
83
  | `--true-drive` | Put an emulated 1541 on device 8 instead of the KERNAL traps. See [Media](#media) |
78
84
  | `-s`, `--subtune N` | Pick a subtune of a `.sid` file, counting from 1 |
79
85
  | `--sid 6581`, `--sid 8580` | Fit the older or newer SID. `--sid auto`, the default, takes a `.sid` tune's own |
86
+ | `--ram SIZE` | Give a VIC-20 the RAM expansion `SIZE`: `unexpanded`, `3k`, `8k`, `16k`, `24k`, `32k` or `all`. Without it, the VIC-20 gets the expansion its media needs. See [The VIC-20](#the-vic-20) |
80
87
  | `--reu SIZE` | Plug in a RAM Expansion Unit of `SIZE` K: 128, 256, 512 (a 1750) or up to 16384 |
81
- | `--model NAME` | Run another C64, named as in VICE: `c64` (the default, a PAL C64 with the 6569 VIC-II, 6526 CIAs and the 6581 SID), `c64c` (the PAL C64C, with the 8565, 6526As and the 8580), `ntsc` (the 6567R8), `newntsc` (the NTSC C64C, with the 8562, 6526As and the 8580), `oldntsc` (the first NTSC C64s' 6567R56A), `drean` (the Drean C64 of Argentina, PAL-N with the 6572) or `sx64` (the portable SX-64, with its own KERNAL and no datasette). `--sid` or a `.sid` tune's own SID takes the model's place |
88
+ | `--model NAME` | Run another model of the machine, named as in VICE. For the C64: `c64` (the default, a PAL C64 with the 6569 VIC-II, 6526 CIAs and the 6581 SID), `c64c` (the PAL C64C, with the 8565, 6526As and the 8580), `ntsc` (the 6567R8), `newntsc` (the NTSC C64C, with the 8562, 6526As and the 8580), `oldntsc` (the first NTSC C64s' 6567R56A), `drean` (the Drean C64 of Argentina, PAL-N with the 6572) or `sx64` (the portable SX-64, with its own KERNAL and no datasette). `--sid` or a `.sid` tune's own SID takes the model's place |
82
89
  | `--ntsc` | Run an NTSC C64, as `--model ntsc` does |
83
90
  | `--no-sound` | Don't play the SID (`badline` only, where sound is on by default) |
84
91
  | `--sound` | Play the SID (`badline-ruby`, where sound is off by default) |
@@ -94,7 +101,7 @@ differences noted in the table. `--help` lists the options for either.
94
101
  | --- | --- |
95
102
  | `--frames N` | Quit after N frames |
96
103
  | `--unpaced` | Run as fast as it can, without vsync or pacing |
97
- | `--at FRAME:EVENT` | Press keys, type, swap media or take a screenshot once `FRAME` frames have run |
104
+ | `--at FRAME:EVENT` | Press keys, type, swap media, open the pause menu or take a screenshot once `FRAME` frames have run |
98
105
  | `--script FILE` | Run the events in `FILE`, one `FRAME:EVENT` a line |
99
106
  | `--screenshot FILE` | Save the last frame as a `.bmp` |
100
107
  | `--save-snapshot FILE` | Save the machine as a `.vsf` snapshot after the last frame |
@@ -134,6 +141,28 @@ red LED lights in the bottom right corner of the border.
134
141
  [doc/media.md](doc/media.md) has the details, and how to attach media
135
142
  from Ruby.
136
143
 
144
+ ## The VIC-20
145
+
146
+ `badline vic20` runs a PAL VIC-20, and takes the same media as the C64
147
+ apart from SID tunes:
148
+
149
+ ```sh
150
+ badline vic20 # READY.
151
+ badline vic20 game.prg # Load and run a program
152
+ badline vic20 game.d64 # Mount a disk image as device 8 and load it
153
+ badline vic20 game.tap # Insert a tape and load it
154
+ badline vic20 game.crt # Attach a cartridge
155
+ ```
156
+
157
+ Without `--ram`, it picks the RAM expansion from where the program, or
158
+ the first program on a disk, loads: a BASIC program gets the expansion
159
+ its start of BASIC comes with, and more of BLK1 to BLK3 when it runs
160
+ past `$3FFF`. A program that loads at `$A000` is a cartridge's ROM, and
161
+ goes in as a `.crt` does. Disks go through the KERNAL traps or, with
162
+ `--true-drive`, a 1541, as on the C64. `F11`, `F12` and the pause menu
163
+ save and load the VIC-20 too, in badline's own snapshots: xvic's `.vsf`
164
+ files don't load.
165
+
137
166
  ## Snapshots
138
167
 
139
168
  `F11` quicksaves the whole machine to the next of five slots in
@@ -219,6 +248,8 @@ through the rows, Right goes into a page and Left back out, and Return or
219
248
  Space presses a row. A row with choices steps to the next. The mouse works
220
249
  too.
221
250
 
251
+ ![The pause menu's Drive 8 page, with a disk in](doc/images/pause-menu.png)
252
+
222
253
  | Page | What it holds |
223
254
  |------|---------------|
224
255
  | Snapshots | Quicksave, save under a name, load a named save, and the latest quicksaves and autosaves |
@@ -259,6 +290,10 @@ program or `.sid` dropped on it opens the menu to ask first.
259
290
  - **REU**: the 1700, 1764 and 1750 and bigger units up to 16M
260
291
  (`--reu`), with DMA timed against the VIC's bad lines and sprites.
261
292
  - **Datasette**: `.tap` playback.
293
+ - **VIC-20**: the 6502, the PAL 6561 VIC-I with its picture and sound,
294
+ both 6522 VIAs, the keyboard, the joystick and RESTORE, RAM
295
+ expansions up to 35K (`--ram`), cartridges, the datasette and the
296
+ serial bus to device 8 (`badline vic20`).
262
297
  - **1541**: an emulated drive running its own DOS (`--true-drive`).
263
298
  - **Cartridges**: standard 8K, 16K and Ultimax, Simons' BASIC, Ocean,
264
299
  Fun Play / Power Play, Super Games, Epyx FastLoad, Westermann
@@ -30,8 +30,12 @@ only catches the rows that happen to move.
30
30
  - [CIA 6526 timer pipeline](#cia-6526-timer-pipeline)
31
31
  - [CIA 6526A interrupt register](#cia-6526a-interrupt-register)
32
32
  - [CIA serial shift register](#cia-serial-shift-register)
33
+ - [VIA shift register](#via-shift-register)
33
34
  - [6510 I/O port](#6510-io-port)
34
35
  - [RAM power-on pattern](#ram-power-on-pattern)
36
+ - [VIC-20 RAM power-on pattern](#vic-20-ram-power-on-pattern)
37
+ - [VIC-I fetches and the V-bus](#vic-i-fetches-and-the-v-bus)
38
+ - [VIC-I sound](#vic-i-sound)
35
39
  - [1541 serial port](#1541-serial-port)
36
40
  - [1541 disk mechanism](#1541-disk-mechanism)
37
41
  - [REU DMA](#reu-dma)
@@ -1196,6 +1200,43 @@ and each was knocked out: removing it fails the rows named.
1196
1200
  [`cia_spec.rb`](../spec/badline/cia_spec.rb) covers the flag timing, the
1197
1201
  waiting byte, both mode-change reports and the zero-latch stall.
1198
1202
 
1203
+ ## VIA shift register
1204
+
1205
+ - A read or write of SR starts a byte only while the register is idle
1206
+ and enabled. An access in mode 0 starts nothing, even once the mode
1207
+ changes, and an access before the eighth bit is in leaves the count
1208
+ alone. The byte in progress runs on, and the first access after it
1209
+ ends starts the next.
1210
+ - In the φ2 modes (2 and 6) CB1 falls on the second cycle after the
1211
+ access, rises on the third, and so on. Mode 6 puts a bit out on cycles
1212
+ +2, +4 … +16, mode 2 takes one in on cycles +3, +5 … +17, and the flag
1213
+ rises with the eighth rising edge, on cycle +17.
1214
+ - In the timer 2 modes (1, 4 and 5) each clock edge comes two cycles
1215
+ after the timer 2 low byte underflows, on the cycle after the low byte
1216
+ reloads from its latch. The half period stays N + 2 cycles.
1217
+ - Pinned by the `VIC20/via_sr` rows, each checked by breaking the rule and
1218
+ rerunning all 32. The plain and `exp` rows dump SR every 7 cycles, the
1219
+ `ifr` and `iex` rows dump IFR, and the SR write comes before the ACR
1220
+ write that sets the mode:
1221
+ - Mode 0 starts nothing: `viasr08` and `viasr18`, and the `ifr`/`iex`
1222
+ rows of modes 04, 08, 14 and 18, which would see the SR flag.
1223
+ - No restart before the eighth bit: `viasr14` and `viasr18`. Their
1224
+ reads come every 7 cycles, faster than a byte.
1225
+ - The φ2 start a cycle late: `viasr08` and `viasr18`. Ticking from the
1226
+ first cycle, or from the third, fails both.
1227
+ - The timer 2 edge two cycles late: `viasr04`, `viasr10` and `viasr14`.
1228
+ A delay of 0, 1 or 3 cycles fails all three.
1229
+ - Every rule also fails the row's `exp` twin, the same test on an
1230
+ expanded VIC-20.
1231
+ - Nothing pins whether the timer 2 delay keeps running while the register
1232
+ is idle. It does, and freezing it passes every row too. Nothing pins
1233
+ the flag's cycle in the φ2 modes either: no row starts a byte and then
1234
+ reads IFR.
1235
+ - Spec guard: the mode blocks in
1236
+ [`shift_register_spec.rb`](../spec/badline/via/shift_register_spec.rb)
1237
+ and *the shift register* in
1238
+ [`via_spec.rb`](../spec/badline/via_spec.rb).
1239
+
1199
1240
  ## 6510 I/O port
1200
1241
 
1201
1242
  - DDR at `$00`/`$01`: inputs are pulled up, bit 5 reads low, and bits 3, 6
@@ -1239,6 +1280,111 @@ and each was knocked out: removing it fails the rows named.
1239
1280
  [`address_bus_spec.rb`](../spec/badline/address_bus_spec.rb).
1240
1281
  - The RAM expansions' extra banks still power on at `$00`.
1241
1282
 
1283
+ ## VIC-20 RAM power-on pattern
1284
+
1285
+ - The VIC-20's RAM powers on in alternating bytes, `$ff` at the even
1286
+ addresses and `$00` at the odd ones, on every power cycle. The internal
1287
+ RAM and the expansion blocks (RAM1-3, BLK1-3, BLK5) all follow it.
1288
+ Colour RAM powers on at zero. `Vic20::Bus::RAM_POWER_ON` holds it.
1289
+ - Pinned by `VIC20/raminitpattern`: `ae.crt` fails unless `$0288` reads
1290
+ `$ff` and `jellymonsters.crt` unless `$1046` does, read from a
1291
+ cartridge before the KERNAL clears RAM. The readme says AE and Jelly
1292
+ Monsters need `$ff` there. Its dumps of real machines' `$1000-$1fff`
1293
+ show bytes alternating `$00` and `$ff` on most machines, with either
1294
+ phase, and noise or other patterns on the rest. They show nothing
1295
+ for colour RAM or expansion RAM that a pattern could be read from.
1296
+ - Spec guard: *RAM at power-on* in
1297
+ [`vic20/bus_spec.rb`](../spec/badline/vic20/bus_spec.rb).
1298
+
1299
+ ## VIC-I fetches and the V-bus
1300
+
1301
+ - On a line of the text window the 6561 fetches the first character code
1302
+ 4 cycles after the column in `$9000`, 16 cycles after the raster count
1303
+ moves with the KERNAL's origin of 12, then the code's pattern byte in
1304
+ the next cycle, two cycles a character. Outside the window it fetches
1305
+ nothing.
1306
+ - A read where no chip answers in I/O 0 (`$9100`, `$9200`) returns the
1307
+ V-bus's last byte: the VIC's fetch in a cycle it fetches in, and the
1308
+ CPU's own last byte on the V-bus in the other cycles.
1309
+ - Pinned by `VIC20/split-tests/timing`: all 1,024 bytes of its readings
1310
+ of `$9003`, `$9004`, `$9100` and `$9200` over 256 cycles match
1311
+ `dumps/dump6561e.prg`, a real 6561E. The later 6561-101's dump reads
1312
+ `$20` in the idle cycles instead; badline follows the 6561E.
1313
+ - Spec guard:
1314
+ [`vic20/vic_split_tests_timing_spec.rb`](../spec/badline/vic20/vic_split_tests_timing_spec.rb)
1315
+ (`:slow`), and *the fetches* in
1316
+ [`vic20/vic_video_spec.rb`](../spec/badline/vic20/vic_video_spec.rb).
1317
+ - A write to `$900F` or `$900E` takes hold from the second of the four
1318
+ pixels of the cycle it lands in, the reverse bit two pixels later, and
1319
+ a character's pixels come out in the two cycles after its pattern
1320
+ fetch. These match xvic's screenshots of a test of cycle-timed writes
1321
+ pixel for pixel; no testprog or hardware capture checks them.
1322
+ - Spec guard: *a colour write* in
1323
+ [`vic20/vic_video_spec.rb`](../spec/badline/vic20/vic_video_spec.rb).
1324
+
1325
+ ## VIC-I sound
1326
+
1327
+ No testprog checks the VIC-I's sound, so these rules are pinned by xvic
1328
+ (VICE 3.10) recordings, run as a black box: `-sounddev wav` at 44.1 kHz of
1329
+ programs that set the registers, the same programs run on badline, and
1330
+ the two compared by period, harmonics and spectrum. xvic's own source
1331
+ wasn't read.
1332
+
1333
+ - A voice's counter comes round every 128 - ((value + 1) & 127) ticks of
1334
+ 16, 8, 4 and 2 cycles from bass to noise, and a tone voice's shift
1335
+ register plays 16 shifts a period, as Marko Mäkelä's `VIC-I.txt`
1336
+ (revision 1.2, Levente Hársfalvi's measurements) gives the frequencies.
1337
+ - Pinned by xvic: `$900A` = `$FE`, `$80` and `$FF` measure 4329.70,
1338
+ 34.0922 and 33.8256 Hz, `$900B` = `$F0` 577.29 Hz, and `$900C` = `$F0`
1339
+ and `$A0` 1154.59 and 182.30 Hz, each within 0.001 Hz of the formula
1340
+ on the PAL clock.
1341
+ - Spec guard: *a tone voice's counter* in
1342
+ [`vic20/sound_spec.rb`](../spec/badline/vic20/sound_spec.rb).
1343
+ - A tone voice's shift register takes bit 7 back into bit 0 inverted while
1344
+ the voice is on and a zero while it's off, and keeps shifting either way.
1345
+ Bit 0 is the output, not gated by the enable bit. This is the die-shot
1346
+ reading of the 6561 on the Denial forum (Lance Ewing: the feedback is a
1347
+ NOR of bit 7 and the inverted enable bit, and the counter keeps shifting
1348
+ a voice that's off).
1349
+ - Pinned by xvic: a bass voice loaded with 10101010, 11000000, 10000000
1350
+ and 11101000 by turning it on and off a shift at a time, then left
1351
+ on, has the same harmonics in xvic's recording and badline's to
1352
+ 0.1 dB through the 12th. 10101010 puts the 7th and 9th harmonics
1353
+ above the fundamental, which a register cleared while off couldn't.
1354
+ - Spec guard: *a tone voice* in
1355
+ [`vic20/sound_spec.rb`](../spec/badline/vic20/sound_spec.rb).
1356
+ - The noise voice's counter steps a 16-bit LFSR: bit 0 takes the XOR of
1357
+ bits 3, 12, 14 and 15 while the voice is on and a one while it's off.
1358
+ Each rise of the LFSR's bit 0 shifts the noise voice's own shift
1359
+ register, bit 7 back into bit 0, inverted while the voice is on, and
1360
+ its bit 0 is the output while the voice is on. These are the die-shot
1361
+ readings on the Denial forum (Lance Ewing and nippur72), which also
1362
+ say the voice is silent while off.
1363
+ - Pinned by xvic: noise at `$FE` and `$FD` repeats every 0.11825 s and
1364
+ 0.2365 s (autocorrelation 0.98 at those lags, in both xvic's
1365
+ recording and badline's), 65535 steps of 2 and 4 cycles. The octave
1366
+ band levels of noise at `$FE`, `$FD`, `$F0`, `$C0` and `$80` match
1367
+ xvic's within 0.5 dB up to 8 kHz.
1368
+ - Spec guard: *the noise voice* in
1369
+ [`vic20/sound_spec.rb`](../spec/badline/vic20/sound_spec.rb).
1370
+ - The output is the voices that are high plus 2/9 of a voice, times the
1371
+ volume, so a volume write steps the output with every voice off, as
1372
+ 4-bit samples played through `$900E` need.
1373
+ - Pinned by xvic: a volume step from 0 to 15 with every voice off peaks
1374
+ at 0.218 of a voice turned on at volume 15.
1375
+ - Spec guard: *the volume* in
1376
+ [`vic20/sound_spec.rb`](../spec/badline/vic20/sound_spec.rb).
1377
+ - Not pinned: the volume scales the output linearly. xvic's tone level
1378
+ per volume step isn't linear (volume 1 is 0.099 of volume 15 and 14
1379
+ is 1.015), and nothing here says whether the hardware is.
1380
+ - The output stage is a one-pole low-pass at 1,420 Hz and a one-pole
1381
+ high-pass at 158 Hz.
1382
+ - Pinned by xvic: a bass voice turned on at volume 15 fits those two RC
1383
+ stages at 1,417 and 158.0 Hz, with an RMS error of 6 against a peak of
1384
+ 11,280. badline's own recording fits 1,420 and 157.6 Hz.
1385
+ - Spec guard: *a step* in
1386
+ [`vic20/sound/output_spec.rb`](../spec/badline/vic20/sound/output_spec.rb).
1387
+
1242
1388
  ## 1541 serial port
1243
1389
 
1244
1390
  - The drive sees the C64's side of the serial bus a host cycle late. The
data/doc/snapshots.md CHANGED
@@ -66,6 +66,19 @@ expansion, REU, KERNAL and datasette. One saved before badline recorded
66
66
  the KERNAL and the datasette restores as a machine with the C64's KERNAL
67
67
  and a datasette.
68
68
 
69
+ ## The VIC-20
70
+
71
+ `badline vic20` saves and restores the same way, with the same keys,
72
+ slots and pause menu page. A VIC-20 snapshot holds its RAM and colour
73
+ RAM, a cartridge's ROM, the VIC with its picture and sound, both VIAs,
74
+ the CPU, the tape with its place on it, the disk or directory device 8
75
+ serves through the traps and a true 1541, and restores into a VIC-20
76
+ with the same RAM expansion. Its file names its machine `VIC20`, as
77
+ xvic's do, and holds only the `BADLINE` module, so xvic doesn't open
78
+ it, and badline doesn't open xvic's. Restoring a
79
+ VIC-20 snapshot while a C64 runs, or a C64 one while a VIC-20 runs,
80
+ swaps the machine in the window.
81
+
69
82
  ## VICE
70
83
 
71
84
  Snapshots use VICE's `.vsf` format. badline writes VICE's modules for
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class AddressBus
5
+ # The C64's PLA: what each page of the CPU's view maps to, from the
6
+ # CPU port's LORAM, HIRAM and CHAREN lines and the cartridge's EXROM
7
+ # and GAME. The bus that includes it supplies the page tables, the
8
+ # ROMs, the chips, and #map_ram_pages, which lays out the RAM that the
9
+ # ROMs, the cartridge and I/O map over.
10
+ module PLA
11
+ private
12
+
13
+ def map_pla_pages
14
+ map_ram_pages
15
+ @ultimax ? map_ultimax_pages : map_banked_pages
16
+ end
17
+
18
+ def map_banked_pages
19
+ map_rom_overlays
20
+ map_cartridge_ram
21
+ @write_pages.fill(@cartridge.romh_writes, 0xe0, 0x20) if @cartridge&.romh_writes
22
+
23
+ if io?
24
+ map_io_pages
25
+ elsif character?
26
+ @read_pages.fill(character_rom, 0xd0, 0x10)
27
+ end
28
+ end
29
+
30
+ # Ultimax cartridges ignore the $01 lines: 4K of RAM, ROML/ROMH windows,
31
+ # I/O always visible and open address space everywhere else. The ROML
32
+ # and ROMH selects fire on writes as well, so cartridge RAM or flash in
33
+ # either window takes the writes there.
34
+ def map_ultimax_pages
35
+ @read_pages.fill(@open_bus, 0x10, 0xf0)
36
+ @write_pages.fill(@open_bus, 0x10, 0xf0)
37
+ @read_pages.fill(@cartridge.roml, 0x80, 0x20) if @cartridge.roml
38
+ map_ultimax_writes(@cartridge.roml, 0x80)
39
+ @read_pages.fill(@cartridge.romh, 0xe0, 0x20) if @cartridge.romh
40
+ map_ultimax_writes(@cartridge.romh, 0xe0)
41
+ map_ultimax_a000
42
+ map_io_pages
43
+ end
44
+
45
+ def map_ultimax_writes(bank, first_page)
46
+ @write_pages.fill(bank, first_page, 0x20) if bank.respond_to?(:poke)
47
+ end
48
+
49
+ def map_ultimax_a000
50
+ return unless (window = @cartridge.ultimax_a000)
51
+
52
+ @read_pages.fill(window, 0xa0, 0x20)
53
+ @write_pages.fill(window, 0xa0, 0x20)
54
+ end
55
+
56
+ # EXROM and GAME select the cartridge whether or not it has a chip
57
+ # there, and an empty socket leaves the bus floating.
58
+ def map_rom_overlays
59
+ @read_pages.fill(@cartridge.roml || @open_bus, 0x80, 0x20) if roml?
60
+ if romh?
61
+ @read_pages.fill(@cartridge.romh || @open_bus, 0xa0, 0x20)
62
+ elsif basic?
63
+ @read_pages.fill(basic_rom, 0xa0, 0x20)
64
+ end
65
+ @read_pages.fill(kernal_rom, 0xe0, 0x20) if kernal?
66
+ end
67
+
68
+ # Cartridge RAM in the ROML or ROMH window decodes writes itself,
69
+ # whatever the $01 lines say.
70
+ def map_cartridge_ram
71
+ return unless @cartridge&.exrom&.zero?
72
+
73
+ map_cartridge_ram_bank(@cartridge.roml, 0x80)
74
+ map_cartridge_ram_bank(@cartridge.romh, 0xa0) if @cartridge.game.zero?
75
+ end
76
+
77
+ def map_cartridge_ram_bank(bank, first_page)
78
+ @write_pages.fill(bank, first_page, 0x20) if bank.is_a?(Cartridge::RAMBank)
79
+ end
80
+
81
+ def map_io_pages
82
+ {
83
+ vic => 0xd0..0xd3, sid => 0xd4..0xd7, color_ram => 0xd8..0xdb,
84
+ cia1 => 0xdc..0xdc, cia2 => 0xdd..0xdd, @open_bus => 0xde..0xdf
85
+ }.each do |chip, pages|
86
+ pages.each { |p| @read_pages[p] = @write_pages[p] = chip }
87
+ end
88
+ @ram_expansion.map_io(@read_pages, @write_pages)
89
+ @read_pages[0xd7] = @write_pages[0xd7] = @debug_register if @debug_register
90
+ @read_pages[0xdf] = @write_pages[0xdf] = @reu if @reu
91
+ map_cartridge_io if @cartridge
92
+ map_extra_sids unless @sid_slots.empty?
93
+ end
94
+
95
+ def map_cartridge_io
96
+ @write_pages.fill(@cartridge, 0xde, 2)
97
+ @cartridge.readable_io_pages.each { |p| @read_pages[p] = @cartridge }
98
+ end
99
+
100
+ def basic?
101
+ io_port.kernal? && io_port.basic? && game_high?
102
+ end
103
+
104
+ def game_high?
105
+ @cartridge.nil? || @cartridge.game == 1
106
+ end
107
+
108
+ def roml?
109
+ @cartridge&.exrom&.zero? && io_port.kernal? && io_port.basic?
110
+ end
111
+
112
+ def romh?
113
+ @cartridge&.exrom&.zero? && @cartridge.game.zero? && io_port.kernal?
114
+ end
115
+
116
+ # In 16K mode LORAM alone leaves $d000 as RAM, where it still maps I/O.
117
+ def character?
118
+ (io_port.kernal? || (io_port.basic? && game_high?)) && !io_port.io?
119
+ end
120
+
121
+ def io?
122
+ (io_port.basic? || io_port.kernal?) && io_port.io?
123
+ end
124
+
125
+ def kernal?
126
+ io_port.kernal?
127
+ end
128
+ end
129
+ end
130
+ end
@@ -3,7 +3,7 @@
3
3
  require "badline/address_bus/saved_state"
4
4
  require "badline/address_bus/sid_slots"
5
5
  require "badline/address_bus/extra_sids"
6
- require "badline/address_bus/ultimax_pages"
6
+ require "badline/address_bus/pla"
7
7
  require "badline/address_bus/roms"
8
8
 
9
9
  module Badline
@@ -34,7 +34,7 @@ module Badline
34
34
  # 0xE000-0xFFFF - KERNAL ROM / Cartridge ROM (high) - 8kb
35
35
  class AddressBus
36
36
  include Addressable
37
- include UltimaxPages
37
+ include PLA
38
38
  include ExtraSIDs
39
39
  include ROMs
40
40
 
@@ -79,7 +79,7 @@ module Badline
79
79
  @joystick1 = Joystick.new
80
80
  @joystick2 = Joystick.new
81
81
  @control_ports = ControlPorts.new(keyboard: @keyboard, joystick1: @joystick1, joystick2: @joystick2)
82
- @vic = VIC.new(self, model: vic_model, region:)
82
+ @vic = VIC.new(model: vic_model, region:)
83
83
  @cia1 = CIA.new(start: 0xdc00, peripheral: @control_ports, model: cia_model, region:)
84
84
  @cia2 = CIA.new(start: 0xdd00, model: cia_model, region:)
85
85
  @control_ports.port_a_source = @cia1
@@ -92,6 +92,8 @@ module Badline
92
92
  @datasette.on_sense_change { @io_port.value = port_value }
93
93
 
94
94
  @color_ram = ColorMemory.new(@vic)
95
+ @vic.vic_bank.connect(cia2: @cia2, color_ram: @color_ram)
96
+ @vic.vic_bank.map_character_rom(character_rom)
95
97
  @open_bus = OpenBus.new(@vic)
96
98
  @cpu_off_bus = false
97
99
 
@@ -201,96 +203,15 @@ module Badline
201
203
  def update_overlays!
202
204
  @ultimax = @cartridge ? @cartridge.ultimax? : false
203
205
  @phi1_ultimax = @cartridge ? @cartridge.phi1_ultimax? : false
204
- @ram_expansion.map(@read_pages, @write_pages)
205
206
  @video_ram = @ram_expansion.video_ram
207
+ @vic.vic_bank.map(@video_ram, phi1_ultimax: @phi1_ultimax, ultimax: @ultimax, romh: @cartridge&.romh)
206
208
 
207
- @ultimax ? map_ultimax_pages : map_banked_pages
209
+ map_pla_pages
208
210
  @write_pages[0xff] = @reu.trigger.wrap(@write_pages[0xff]) if @reu
209
211
  end
210
212
 
211
- def map_banked_pages
212
- map_rom_overlays
213
- map_cartridge_ram
214
- @write_pages.fill(@cartridge.romh_writes, 0xe0, 0x20) if @cartridge&.romh_writes
215
-
216
- if io?
217
- map_io_pages
218
- elsif character?
219
- @read_pages.fill(character_rom, 0xd0, 0x10)
220
- end
221
- end
222
-
223
- # EXROM and GAME select the cartridge whether or not it has a chip
224
- # there, and an empty socket leaves the bus floating.
225
- def map_rom_overlays
226
- @read_pages.fill(@cartridge.roml || @open_bus, 0x80, 0x20) if roml?
227
- if romh?
228
- @read_pages.fill(@cartridge.romh || @open_bus, 0xa0, 0x20)
229
- elsif basic?
230
- @read_pages.fill(basic_rom, 0xa0, 0x20)
231
- end
232
- @read_pages.fill(kernal_rom, 0xe0, 0x20) if kernal?
233
- end
234
-
235
- # Cartridge RAM in the ROML or ROMH window decodes writes itself,
236
- # whatever the $01 lines say.
237
- def map_cartridge_ram
238
- return unless @cartridge&.exrom&.zero?
239
-
240
- map_cartridge_ram_bank(@cartridge.roml, 0x80)
241
- map_cartridge_ram_bank(@cartridge.romh, 0xa0) if @cartridge.game.zero?
242
- end
243
-
244
- def map_cartridge_ram_bank(bank, first_page)
245
- @write_pages.fill(bank, first_page, 0x20) if bank.is_a?(Cartridge::RAMBank)
246
- end
247
-
248
- def map_io_pages
249
- {
250
- vic => 0xd0..0xd3, sid => 0xd4..0xd7, color_ram => 0xd8..0xdb,
251
- cia1 => 0xdc..0xdc, cia2 => 0xdd..0xdd, @open_bus => 0xde..0xdf
252
- }.each do |chip, pages|
253
- pages.each { |p| @read_pages[p] = @write_pages[p] = chip }
254
- end
255
- @ram_expansion.map_io(@read_pages, @write_pages)
256
- @read_pages[0xd7] = @write_pages[0xd7] = @debug_register if @debug_register
257
- @read_pages[0xdf] = @write_pages[0xdf] = @reu if @reu
258
- map_cartridge_io if @cartridge
259
- map_extra_sids unless @sid_slots.empty?
260
- end
261
-
262
- def map_cartridge_io
263
- @write_pages.fill(@cartridge, 0xde, 2)
264
- @cartridge.readable_io_pages.each { |p| @read_pages[p] = @cartridge }
265
- end
266
-
267
- def basic?
268
- io_port.kernal? && io_port.basic? && game_high?
269
- end
270
-
271
- def game_high?
272
- @cartridge.nil? || @cartridge.game == 1
273
- end
274
-
275
- def roml?
276
- @cartridge&.exrom&.zero? && io_port.kernal? && io_port.basic?
277
- end
278
-
279
- def romh?
280
- @cartridge&.exrom&.zero? && @cartridge.game.zero? && io_port.kernal?
281
- end
282
-
283
- # In 16K mode LORAM alone leaves $d000 as RAM, where it still maps I/O.
284
- def character?
285
- (io_port.kernal? || (io_port.basic? && game_high?)) && !io_port.io?
286
- end
287
-
288
- def io?
289
- (io_port.basic? || io_port.kernal?) && io_port.io?
290
- end
291
-
292
- def kernal?
293
- io_port.kernal?
213
+ def map_ram_pages
214
+ @ram_expansion.map(@read_pages, @write_pages)
294
215
  end
295
216
  end
296
217
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Badline
4
4
  # Observe-only PC trap on the KERNAL CHROUT routine ($FFD2). Records each
5
- # character written while the KERNAL is banked in, then lets execution fall
5
+ # character written while the layout finds the KERNAL mapped in, then lets execution fall
6
6
  # through to the ROM, so the screen output is unaffected. Used for headless
7
7
  # capture of program output.
8
8
  class ChroutTrap
@@ -10,14 +10,15 @@ module Badline
10
10
 
11
11
  attr_reader :output
12
12
 
13
- def initialize(cpu:, bus:)
13
+ def initialize(cpu:, bus:, layout:)
14
14
  @cpu = cpu
15
15
  @bus = bus
16
+ @layout = layout
16
17
  @output = +""
17
18
  end
18
19
 
19
20
  def call
20
- @output << ascii(@cpu.a) if @bus.io_port.kernal?
21
+ @output << ascii(@cpu.a) if @layout.kernal_mapped?(@bus)
21
22
  end
22
23
 
23
24
  def inspect