badline 0.2.0 → 0.3.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -0
  3. data/CLAUDE.md +6 -0
  4. data/README.md +24 -6
  5. data/doc/pinned-behaviour.md +18 -6
  6. data/exe/badline +5 -2
  7. data/lib/badline/address_bus.rb +14 -6
  8. data/lib/badline/addressable.rb +1 -1
  9. data/lib/badline/audio/bare_player.rb +20 -3
  10. data/lib/badline/audio/machine_player.rb +9 -6
  11. data/lib/badline/audio/renderer.rb +3 -3
  12. data/lib/badline/audio/stream.rb +117 -0
  13. data/lib/badline/audio.rb +1 -0
  14. data/lib/badline/cartridge/easy_flash.rb +6 -2
  15. data/lib/badline/cartridge/epyx_fastload.rb +1 -1
  16. data/lib/badline/cartridge/mach5.rb +1 -1
  17. data/lib/badline/cartridge/magic_desk.rb +5 -1
  18. data/lib/badline/cartridge/ocean.rb +5 -2
  19. data/lib/badline/cartridge/rex_utility.rb +1 -1
  20. data/lib/badline/cartridge/rgcd.rb +1 -1
  21. data/lib/badline/cartridge/simons_basic.rb +2 -2
  22. data/lib/badline/cartridge/westermann.rb +2 -2
  23. data/lib/badline/cartridge.rb +16 -8
  24. data/lib/badline/checkpoint.rb +98 -0
  25. data/lib/badline/cia/interrupt_register.rb +20 -6
  26. data/lib/badline/cia.rb +36 -19
  27. data/lib/badline/color_memory.rb +2 -2
  28. data/lib/badline/computer.rb +41 -11
  29. data/lib/badline/cpu/microcode.rb +41 -43
  30. data/lib/badline/cpu.rb +3 -3
  31. data/lib/badline/gui/application.rb +41 -9
  32. data/lib/badline/kernal_trap/channel.rb +118 -8
  33. data/lib/badline/kernal_trap/drive/block_commands.rb +79 -0
  34. data/lib/badline/kernal_trap/drive/channels.rb +53 -0
  35. data/lib/badline/kernal_trap/drive/memory.rb +109 -0
  36. data/lib/badline/kernal_trap/drive/parameters.rb +28 -0
  37. data/lib/badline/kernal_trap/drive/status.rb +49 -0
  38. data/lib/badline/kernal_trap/drive.rb +148 -132
  39. data/lib/badline/kernal_trap/file.rb +0 -5
  40. data/lib/badline/kernal_trap/load.rb +93 -25
  41. data/lib/badline/kernal_trap/routine.rb +17 -0
  42. data/lib/badline/kernal_trap/save.rb +48 -15
  43. data/lib/badline/kernal_trap/serial.rb +34 -2
  44. data/lib/badline/media.rb +2 -2
  45. data/lib/badline/memory.rb +7 -2
  46. data/lib/badline/rom.rb +14 -5
  47. data/lib/badline/sid/catch_up.rb +110 -0
  48. data/lib/badline/sid/decimator.rb +6 -1
  49. data/lib/badline/sid/filter.rb +5 -5
  50. data/lib/badline/sid/waveform/fast_forward.rb +10 -10
  51. data/lib/badline/sid/waveform.rb +7 -7
  52. data/lib/badline/sid.rb +16 -92
  53. data/lib/badline/status.rb +255 -32
  54. data/lib/badline/storage/d64_image.rb +6 -0
  55. data/lib/badline/storage/d71_image.rb +4 -0
  56. data/lib/badline/storage/d81_image.rb +4 -0
  57. data/lib/badline/storage/disk_image.rb +78 -17
  58. data/lib/badline/storage/host_directory.rb +26 -2
  59. data/lib/badline/storage/sid_file.rb +37 -3
  60. data/lib/badline/storage.rb +3 -1
  61. data/lib/badline/time_of_day.rb +7 -0
  62. data/lib/badline/version.rb +1 -1
  63. data/lib/badline/vic/graphics_shifter.rb +8 -2
  64. data/lib/badline/vic/registers.rb +16 -27
  65. data/lib/badline/vic.rb +23 -10
  66. data/lib/badline.rb +1 -0
  67. metadata +9 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c4f6f2a45538629646db2ddd5ff47971f36580cf1982879a6f8db7d4dc410cf4
4
- data.tar.gz: e668de00c0a93c4f7f7f9ff9494a1f875ea820bbc18ef67203382862967753e3
3
+ metadata.gz: 9348bef9831893e0aefa4d6965e1ea99a7252fb9f8f71e311f6b93b849ee1faa
4
+ data.tar.gz: '01029a95df00b30c92120e7f91c2e6dc9af247a927c41e64296a6285abdb9267'
5
5
  SHA512:
6
- metadata.gz: a4ccdf301a0d5f35e87e3ac8777bbc79b1214cbde95da1cf90db901aa40ffc05eef3dd3954fd9217137bc0892359da2123c09299a47473bf7a762a7d1754914a
7
- data.tar.gz: b16f94cd511939d43132c936047280de9114ee01a3bef13ed24af13d069a04e7d86ba1d736b544ef6957f3307de856fd625a631082a58a51deec91d2581c1204
6
+ metadata.gz: 8944a5873ed411c9ab156a5cedf537f8d7db1b0a06625c567a24b2b33f05c8ac6b20b45edbbaf7049b91a72da7bc1a5fc35b0c3f24e7029fda86a671315bfef5
7
+ data.tar.gz: bb10fd47802aadab93ed86916ce8de6a2d17e4163ecf723a0a6d4b45085eefadd2905e2eabbfbba39530f7d0e7d3be18f0e059a17bc6c71fb69aa592ea7204a3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,72 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0](https://github.com/elektronaut/badline/compare/v0.2.1...v0.3.0) (2026-09-24)
4
+
5
+ Sound is the headline: `badline --sound` plays the SID in the emulator window and paces emulation to the audio device. It is off by default, because the emulator still runs below real time on CRuby and the sound stutters there. This release also brings the virtual drive closer to a real 1541 (buffer channels in drive RAM, DOS command parsing, the BAM in buffer 4, `LOAD"*"`), cartridge banking fixes, and `Badline::Checkpoint` with `bin/machine_diff` for checking that a change leaves emulation alone.
6
+
7
+
8
+
9
+ ### Features
10
+
11
+ * add Badline::Checkpoint, a per-component digest of machine state ([183e164](https://github.com/elektronaut/badline/commit/183e16438b4b1bfae5c16b10f29f2b08d0a6b297))
12
+ * add bin/machine_diff, a differential run harness ([58f695a](https://github.com/elektronaut/badline/commit/58f695a32096fe61d7395fcec9a76d8d84b5e440))
13
+ * load ROMs from a configurable Badline.rom_path ([4fa12c8](https://github.com/elektronaut/badline/commit/4fa12c8e55004eebe0b1a14e80f03713306e3acf))
14
+ * pace the window to the audio device while sound plays ([031b001](https://github.com/elektronaut/badline/commit/031b00196ee9a881740047c726231ffc0400d03d))
15
+ * play the SID in the emulator window with --sound ([4cbd211](https://github.com/elektronaut/badline/commit/4cbd211ca986834e3d8fcbafdabc5586c87ec950))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * answer a buffer channel's first read with its buffer number ([851e0d2](https://github.com/elektronaut/badline/commit/851e0d2bb71db51cc36607da90b4f7891b32bced))
21
+ * bank BASIC out for PSID routines whose image reaches under it ([9c5bf57](https://github.com/elektronaut/badline/commit/9c5bf57646ca8de8687679accdca26ac4dd68931))
22
+ * bank sparse and odd-sized cartridge images like the hardware ([1560f63](https://github.com/elektronaut/badline/commit/1560f63678a254bfa53e8915c35851dab00dc27b))
23
+ * drop the carriage return that ends a file name, as the 1541 does ([3ca3d51](https://github.com/elektronaut/badline/commit/3ca3d518808cf87ab0e14a3b312e972883fc4699))
24
+ * fail a block write at the block it names ([4b9664c](https://github.com/elektronaut/badline/commit/4b9664ca02bd0026334ffee01d4f6a5cfc354f73))
25
+ * fail an append open on a write-protected disk ([b5c7baf](https://github.com/elektronaut/badline/commit/b5c7bafed7f189c3e7a0074d27f0b76e8ffb3b56))
26
+ * fail an unreadable host file with a READ ERROR ([8b3d8f4](https://github.com/elektronaut/badline/commit/8b3d8f4eeae28a4ef61ef72e76dcfc02cefb738f))
27
+ * fill a buffer channel with the bytes written to it ([36c3660](https://github.com/elektronaut/badline/commit/36c3660e1c170ea058b80d3a0b10ec019aa84058))
28
+ * give single-bank mappers erased ROM when the image has no chip ([07e5bf3](https://github.com/elektronaut/badline/commit/07e5bf39b26e022f814c99b0d9226cecf3114400))
29
+ * ignore whatever follows a star in a DOS file name ([b2f8831](https://github.com/elektronaut/badline/commit/b2f8831b193e496d19c03388fe0613b9f9c9abb3))
30
+ * keep the drive's buffer channels in its RAM ([9efefe4](https://github.com/elektronaut/badline/commit/9efefe44520067db27496ea2af85468228c6579f))
31
+ * keep the drive's RAM when the disk changes ([2c07d0e](https://github.com/elektronaut/badline/commit/2c07d0eb73120dfe1eda69a5fc8ad43a31d83ef2))
32
+ * leave secondary address $60 in $B9 after a trapped LOAD ([d7366a8](https://github.com/elektronaut/badline/commit/d7366a8aaf0fe13d239035178751fc8f783d7a30))
33
+ * mask Ocean and Magic Desk bank selection to the ROM size ([e6d36a0](https://github.com/elektronaut/badline/commit/e6d36a004583d18e225e4bb5cfccdd69380d523e))
34
+ * play NTSC PSID tunes on an NTSC clock on the bare rig ([006e04b](https://github.com/elektronaut/badline/commit/006e04b03985ef5d2fe57bab6c862bbe97d65f50))
35
+ * read DOS command parameters the way the 1541 does ([db5b4fc](https://github.com/elektronaut/badline/commit/db5b4fc06b82f5423453ee988d30f407a8565ed1))
36
+ * read open bus from a cartridge's empty ROM socket ([fee013d](https://github.com/elektronaut/badline/commit/fee013dcff2a062b75713b268422171e79a50375))
37
+ * read the BAM into buffer 4 when the drive initializes a disk ([c1320a5](https://github.com/elektronaut/badline/commit/c1320a5d2f7d9fcd24e800fcacaf07ba8b07b889))
38
+ * reopen the last loaded file on a LOAD of "*" ([64b3991](https://github.com/elektronaut/badline/commit/64b3991b9e1c91190e9b47e44238b93e0ea74850))
39
+ * report host SAVE and read failures on the drive's error channel ([039b6da](https://github.com/elektronaut/badline/commit/039b6da1779397e56ebf6cc25effe656ca85c1f6))
40
+ * reset the drive with the machine, as the serial bus's RESET line does ([4044db6](https://github.com/elektronaut/badline/commit/4044db6ae1bd241381c2d7651cf2a44b1ae8a61a))
41
+ * RUN BASIC RSID tunes instead of calling their first byte ([d9a51c2](https://github.com/elektronaut/badline/commit/d9a51c2eebd5092e97da576c438a647079458e78))
42
+ * show a BMM falling out of multicolour bitmap or ECM+BMM at pixel 5 ([988a0c6](https://github.com/elektronaut/badline/commit/988a0c60e40c720e8e1cd7acf0506107902a1ccb))
43
+ * **vic:** power on with every register zero, as VICE does ([d5bf757](https://github.com/elektronaut/badline/commit/d5bf75756d2edfdf3b6bf40214615262b79958a6))
44
+ * wrap a buffer channel's reads back to its second byte, as the DOS does ([9c9f0ed](https://github.com/elektronaut/badline/commit/9c9f0edb7b92b580bb2cb71668e5fa07fa8e3bd9))
45
+
46
+
47
+ ### Performance Improvements
48
+
49
+ * cut the SID's per-span catch-up overhead ([f50ede8](https://github.com/elektronaut/badline/commit/f50ede8472389c28c1dde70909794ef69ae77238))
50
+ * type the hot core for Spinel with RBS seeds ([90d31b0](https://github.com/elektronaut/badline/commit/90d31b02331982e216284f2ac533f5a6aa934bf9))
51
+
52
+ ## [0.2.1](https://github.com/elektronaut/badline/compare/v0.2.0...v0.2.1) (2026-09-23)
53
+
54
+
55
+ ### Bug Fixes
56
+
57
+ * decode U commands by the 1541's user table index ([4d37a92](https://github.com/elektronaut/badline/commit/4d37a928f9109d8214294fdac21bce25f201549b))
58
+ * detect disk image error tables by exact image size ([baa2091](https://github.com/elektronaut/badline/commit/baa20918b345723003ff61936c791f476e478fe8))
59
+ * detect the error table on 42-track D64 images ([9d8be41](https://github.com/elektronaut/badline/commit/9d8be41ffd23f0984f26e8e2c3ec0ba692629df9))
60
+ * end a SAVE the host directory can't write like a failed drive write ([4d15efe](https://github.com/elektronaut/badline/commit/4d15efe815d809fb7eba84cc9d59aebb4964f4ad))
61
+ * fail writes to a disk image like a write-protected 1541 ([c4ccb46](https://github.com/elektronaut/badline/commit/c4ccb46382166a896875a4f081c13db7841aaf7d))
62
+ * hand LOADs that reach below $0334 to the ROM's byte loop ([948b816](https://github.com/elektronaut/badline/commit/948b8163d0ba16d4eb4c71ee03ab4fe59297ba56))
63
+ * power-cycle the machine when a cartridge is attached ([2e1d7a4](https://github.com/elektronaut/badline/commit/2e1d7a472f00e0ccbd7a3245db7c685162e40786))
64
+ * print the KERNAL's SAVING message from the SAVE trap ([c912986](https://github.com/elektronaut/badline/commit/c912986e1702ba9907a097aaebd62b36e83b1fe5))
65
+ * print the KERNAL's SEARCHING and LOADING messages from the LOAD trap ([fc7e719](https://github.com/elektronaut/badline/commit/fc7e719e0856b8eddafbb96402893570cf757842))
66
+ * report the DOS version on the command channel at drive power-on ([016ad3f](https://github.com/elektronaut/badline/commit/016ad3f884a2fb1160f30372acd1ae972d0b9702))
67
+ * reset the virtual drive on the U-command reset forms ([a558314](https://github.com/elektronaut/badline/commit/a558314e7f116a28c8341ca82acce65c94469b9d))
68
+ * stop LOADs and file reads at a block the error table marks bad ([457be18](https://github.com/elektronaut/badline/commit/457be18351af7241a26f76631ec9bfad0ce2306e))
69
+
3
70
  ## [0.2.0](https://github.com/elektronaut/badline/compare/v0.1.0...v0.2.0) (2026-09-23)
4
71
 
5
72
 
data/CLAUDE.md CHANGED
@@ -52,6 +52,12 @@ timer test takes about 25 minutes.
52
52
  where that time goes. `test/baselines/README.md`
53
53
  documents the baseline format, the suites and how rows are compared.
54
54
 
55
+ `bin/machine_diff <scenario|media> --against <rev>` checks that a change
56
+ leaves emulation alone. It runs the scenario on this tree and on `<rev>`'s
57
+ `lib/` side by side, digests the CPU, RAM, VIC, CIAs and SID every million
58
+ cycles, and names the first component that differs. Use it for refactors
59
+ and speed work.
60
+
55
61
  ## Driving the emulator headlessly
56
62
 
57
63
  `exe/badline <media>` opens the SDL window, which is no use for checking
data/README.md CHANGED
@@ -10,9 +10,9 @@ are modelled at the cycle level.
10
10
 
11
11
  It runs programs, disk and tape images, cartridges and SID tunes, and
12
12
  the SDL2 front end supports the keyboard, joysticks, game controllers,
13
- paddles and a 1351 mouse. There is no live sound yet, and emulation runs
14
- slower than a real C64. See [What's emulated](#whats-emulated) for the
15
- details.
13
+ paddles and a 1351 mouse. Emulation runs slower than a real C64, so
14
+ live sound, which is off by default, stutters. See
15
+ [What's emulated](#whats-emulated) for the details.
16
16
 
17
17
  ## Requirements
18
18
 
@@ -54,6 +54,21 @@ you can type the `LOAD` yourself. `--song N` picks a subtune of a
54
54
  without YJIT, which is otherwise switched on at startup.
55
55
  `badline --help` lists the options.
56
56
 
57
+ The KERNAL, BASIC and character ROMs come with the gem. To run other
58
+ images, such as a patched KERNAL, point `BADLINE_ROM_PATH` at a
59
+ directory that holds `kernal.rom`, `basic.rom` and `character.rom`,
60
+ plus `eapi/eapi-am29f040-14` if you attach EasyFlash cartridges. From
61
+ Ruby, `Badline.rom_path = dir` does the same before a
62
+ `Badline::Computer` is built, and `nil` restores the bundled set.
63
+
64
+ `--sound` plays the SID through the host's audio device, and `F10`
65
+ mutes and unmutes it. Sound is off by default. While it plays, the
66
+ audio device sets the pace instead of the display, so the machine never
67
+ runs ahead of the sound or drifts behind it. The whole machine runs
68
+ below real time, though, so the sound stutters: it plays in bursts with
69
+ silent gaps between them, at the right pitch, and never slows the
70
+ emulation down.
71
+
57
72
  ## Media
58
73
 
59
74
  | Format | Handling |
@@ -160,6 +175,9 @@ Control port 1's fire line is also the VIC-II's light pen input, so
160
175
  joystick 1's fire button and the 1351's left button in port 1 latch the
161
176
  light pen registers.
162
177
 
178
+ With `--sound`, `F10` mutes and unmutes the sound, and the window
179
+ title shows `[MUTED]` while it's off.
180
+
163
181
  ## What's emulated
164
182
 
165
183
  - **6510**: every opcode, documented and undocumented, with per-cycle
@@ -202,9 +220,9 @@ light pen registers.
202
220
 
203
221
  Known gaps:
204
222
 
205
- - No live audio in the emulator window. The SID is emulated, but the
206
- whole machine runs below real time. `badline-sid` plays `.sid` tunes
207
- on their own.
223
+ - Live audio in the emulator window stutters, because the whole machine
224
+ runs below real time. `badline-sid` plays PSID tunes smoothly on
225
+ their own.
208
226
  - No drive emulation, so fast loaders and anything else that runs code
209
227
  on the drive won't work (see [Media](#media)). Disk images are
210
228
  read-only.
@@ -650,12 +650,24 @@ VICE x64sc's `vicii_fetch_graphics` and `draw_graphics8` for the 6569.
650
650
  - Pinned by `modesplit` (48 → 1222 px painting whole groups, 428 with
651
651
  MCM read at pixel 4), the `videomode` rows and `vicii_reg_timing`
652
652
  (pass → 274).
653
- - `videomode2` and `videomode-y` disagree on where a falling BMM shows:
654
- pixel 6 in `videomode2`, pixel 5 in `videomode-y` and in `modesplit`'s
655
- ECM+BMM → ECM split. The readme says these delays vary with the chip
656
- and its temperature. badline keeps VICE's pixel 6, which passes
657
- `videomode2` and leaves `videomode-y` 1 px off and all 48 px left in
658
- `modesplit`, one pixel on each of its first-section lines.
653
+ - A falling BMM shows at pixel 5 instead of 6 unless it leaves hi-res
654
+ bitmap: out of multicolour bitmap, and out of the invalid ECM+BMM
655
+ modes, whether ECM falls with it or stays.
656
+ - Pinned by `modesplit` (48 px → pass), whose first section drops BMM
657
+ out of ECM+BMM, by the E+B row of `vicii_reg_timing-a5` and `-ff`
658
+ (7 px each → pass), which drops ECM and BMM together, and by
659
+ `videomode-y` (1 px → pass), which drops it out of multicolour
660
+ bitmap. The hi-res exception is pinned by `videomode2` and the BMM
661
+ row of `vicii_reg_timing` (pass → 1 and 7 px without it).
662
+ - Two references disagree, and both are marked unsafe in the
663
+ testlist: `videomode-v` drops BMM out of multicolour bitmap and
664
+ `videomode-w` out of ECM+BMM, and both want pixel 6 (+1 px each).
665
+ The readme says these delays vary with the chip and its
666
+ temperature.
667
+ - Spec guard: *takes a BMM falling out of hi-res bitmap at pixel 6*,
668
+ *out of ECM+BMM at pixel 5* and *out of multicolour bitmap at
669
+ pixel 5* in
670
+ [`vic/graphics_shifter_spec.rb`](../spec/badline/vic/graphics_shifter_spec.rb).
659
671
  - An MCM that falls out of the invalid ECM+MCM text mode is a pixel
660
672
  later on both counts. The lookup stays black through pixel 4 and
661
673
  changes at pixel 5, and the pairs are read through pixel 7, with
data/exe/badline CHANGED
@@ -6,7 +6,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
6
 
7
7
  require "optparse"
8
8
 
9
- options = { autostart: true, jit: true }
9
+ options = { autostart: true, jit: true, sound: false }
10
10
 
11
11
  parser = OptionParser.new do |opts|
12
12
  opts.banner = <<~BANNER
@@ -32,6 +32,8 @@ parser = OptionParser.new do |opts|
32
32
  options[:sid_model] = model
33
33
  end
34
34
 
35
+ opts.on("--sound", "Play the SID through the host's audio device (F10 mutes)") { options[:sound] = true }
36
+
35
37
  opts.on("--disable-jit", "Run without enabling YJIT") do
36
38
  options[:jit] = false
37
39
  end
@@ -64,7 +66,8 @@ require "badline/gui"
64
66
 
65
67
  begin
66
68
  application = Badline::GUI::Application.new(media_path:, autostart: options[:autostart],
67
- song: options[:song], sid_model: options[:sid_model])
69
+ song: options[:song], sid_model: options[:sid_model],
70
+ sound: options[:sound])
68
71
  rescue Badline::Storage::SIDFile::FormatError, Badline::Storage::T64::FormatError,
69
72
  Badline::Storage::TAP::FormatError, Badline::Storage::CRTFile::FormatError,
70
73
  Badline::Cartridge::UnsupportedTypeError => e
@@ -45,12 +45,14 @@ module Badline
45
45
  PORT_FLOATING = 0b1100_1000
46
46
  TAPE_SENSE = 0b0001_0000
47
47
 
48
+ RAM_POWER_ON = [0xff, 0x07].freeze
49
+
48
50
  attr_reader :io_port, :ram, :basic_rom, :character_rom, :kernal_rom,
49
51
  :vic, :sid, :color_ram, :cia1, :cia2, :keyboard, :joystick1, :joystick2,
50
52
  :control_ports, :cartridge, :ultimax, :phi1_ultimax, :datasette
51
53
 
52
54
  def initialize(sid_model: :mos6581)
53
- @ram = Memory.new([0xff, 0x07], length: 2**16, start: 0)
55
+ @ram = Memory.new(RAM_POWER_ON, length: 2**16, start: 0)
54
56
  @cartridge = nil
55
57
  @debug_register = nil
56
58
 
@@ -79,7 +81,7 @@ module Badline
79
81
  @port_ddr = 0x00
80
82
  @port_out = 0x00
81
83
  @port_floating = 0x00
82
- @io_port = Status.new(%i[basic kernal io tape_out tape_switch tape_motor], value: port_value)
84
+ @io_port = PortStatus.new(%i[basic kernal io tape_out tape_switch tape_motor], value: port_value)
83
85
 
84
86
  @read_pages = Array.new(256)
85
87
  @write_pages = Array.new(256)
@@ -93,6 +95,10 @@ module Badline
93
95
  update_overlays!
94
96
  end
95
97
 
98
+ def power_on!
99
+ @ram.clear!(RAM_POWER_ON)
100
+ end
101
+
96
102
  # The 6510's RES line clears the port's direction and output registers.
97
103
  # The floating bits keep their charge.
98
104
  def reset_port!
@@ -174,10 +180,12 @@ module Badline
174
180
  end
175
181
  end
176
182
 
183
+ # EXROM and GAME select the cartridge whether or not it has a chip
184
+ # there, and an empty socket leaves the bus floating.
177
185
  def map_rom_overlays
178
- @read_pages.fill(@cartridge.roml, 0x80, 0x20) if roml?
186
+ @read_pages.fill(@cartridge.roml || @open_bus, 0x80, 0x20) if roml?
179
187
  if romh?
180
- @read_pages.fill(@cartridge.romh, 0xa0, 0x20)
188
+ @read_pages.fill(@cartridge.romh || @open_bus, 0xa0, 0x20)
181
189
  elsif basic?
182
190
  @read_pages.fill(basic_rom, 0xa0, 0x20)
183
191
  end
@@ -248,11 +256,11 @@ module Badline
248
256
  end
249
257
 
250
258
  def roml?
251
- @cartridge&.roml && @cartridge.exrom.zero? && io_port.kernal? && io_port.basic?
259
+ @cartridge&.exrom&.zero? && io_port.kernal? && io_port.basic?
252
260
  end
253
261
 
254
262
  def romh?
255
- @cartridge&.romh && @cartridge.exrom.zero? && @cartridge.game.zero? && io_port.kernal?
263
+ @cartridge&.exrom&.zero? && @cartridge.game.zero? && io_port.kernal?
256
264
  end
257
265
 
258
266
  # In 16K mode LORAM alone leaves $d000 as RAM, where it still maps I/O.
@@ -51,7 +51,7 @@ module Badline
51
51
 
52
52
  private
53
53
 
54
- def index(addr)
54
+ def offset_of(addr)
55
55
  i = addr - @start
56
56
  raise OutOfBoundsError, "#{addr.inspect} (#{range})" unless i >= 0 && i < @length
57
57
 
@@ -8,6 +8,10 @@ module Badline
8
8
  # which is what makes this path about three times faster than the whole
9
9
  # machine.
10
10
  #
11
+ # Nothing on the rig ties it to PAL, so an NTSC tune gets an NTSC
12
+ # machine: 60 Hz frames, the NTSC KERNAL's timer and `$02a6`, and a
13
+ # clock of 1022727 Hz for the SID's output.
14
+ #
11
15
  # Calls go through a six-byte stub whose JSR operand the dispatch trap
12
16
  # rewrites:
13
17
  #
@@ -23,6 +27,11 @@ module Badline
23
27
  # 312 raster lines of 63 cycles.
24
28
  FRAME_CYCLES = 19_656
25
29
 
30
+ # 263 raster lines of 65 cycles.
31
+ NTSC_FRAME_CYCLES = 17_095
32
+
33
+ NTSC_CLOCK_HZ = 1_022_727
34
+
26
35
  # A tune's init routine is free to unpack itself, but not forever.
27
36
  INIT_LIMIT = 10_000_000
28
37
 
@@ -30,6 +39,11 @@ module Badline
30
39
  # second.
31
40
  KERNAL_TIMER = 0x4025
32
41
 
42
+ NTSC_KERNAL_TIMER = 0x4295
43
+
44
+ # The KERNAL's PAL/NTSC flag, 1 for PAL.
45
+ VIDEO_STANDARD = 0x02a6
46
+
33
47
  attr_reader :sid
34
48
 
35
49
  def initialize(tune, song: nil, sid_model: tune.sid_model)
@@ -39,7 +53,8 @@ module Badline
39
53
  # The CPU port as the KERNAL leaves it, which a PSID tune expects.
40
54
  @bus.poke(0x00, 0x2f)
41
55
  @bus.poke(0x01, 0x37)
42
- @bus.cia1.timer_a_latch = KERNAL_TIMER
56
+ @bus.poke(VIDEO_STANDARD, tune.ntsc? ? 0 : 1)
57
+ @bus.cia1.timer_a_latch = tune.ntsc? ? NTSC_KERNAL_TIMER : KERNAL_TIMER
43
58
  @cpu = CPU.new(@bus)
44
59
  @sid = @bus.sid
45
60
  @idle = false
@@ -67,12 +82,14 @@ module Badline
67
82
  cycles
68
83
  end
69
84
 
85
+ def clock_hz = @tune.ntsc? ? NTSC_CLOCK_HZ : TimeOfDay::CLOCK_HZ
86
+
70
87
  private
71
88
 
72
- # A PAL frame, or for a CIA-timed song one period of CIA 1 timer A,
89
+ # A video frame, or for a CIA-timed song one period of CIA 1 timer A,
73
90
  # which init and play are both free to reprogram.
74
91
  def period
75
- return FRAME_CYCLES unless @tune.cia_timed?(@song + 1)
92
+ return (@tune.ntsc? ? NTSC_FRAME_CYCLES : FRAME_CYCLES) unless @tune.cia_timed?(@song + 1)
76
93
 
77
94
  @bus.cia1.timer_a_latch + 1
78
95
  end
@@ -4,8 +4,8 @@ module Badline
4
4
  module Audio
5
5
  # Runs a tune on the whole machine, for the RSID tunes that install their
6
6
  # own interrupts and expect a booted C64 underneath. The tune goes in
7
- # through the same stub `Media` uses and BASIC SYSes it; rendering starts
8
- # when the CPU reaches that stub.
7
+ # the way `Media` puts it in: BASIC SYSes the driver stub, or RUNs a
8
+ # BASIC tune. Rendering starts when the CPU gets there.
9
9
  class MachinePlayer
10
10
  FRAME_CYCLES = BarePlayer::FRAME_CYCLES
11
11
 
@@ -22,12 +22,15 @@ module Badline
22
22
 
23
23
  def start
24
24
  @computer.on_init { inject }
25
- @computer.cpu.install_trap(@tune.driver_address) { begin_playing }
25
+ @computer.cpu.install_trap(@tune.entry_address) { begin_playing }
26
26
  @computer.cycle! until @started || @computer.cycles > START_LIMIT
27
27
  end
28
28
 
29
29
  def sid = @computer.sid
30
30
 
31
+ # The machine is PAL whatever the tune asks for.
32
+ def clock_hz = TimeOfDay::CLOCK_HZ
33
+
31
34
  # Advances one PAL frame, or `budget` cycles if that is shorter, then
32
35
  # yields whatever the SID recorded over it. Returns the cycles advanced.
33
36
  def frame(budget, &)
@@ -41,13 +44,13 @@ module Badline
41
44
 
42
45
  def inject
43
46
  @computer.ram.write(@tune.load_address, @tune.data)
44
- @computer.ram.write(@tune.driver_address, @tune.driver(song: @song))
45
- @computer.type_text("sys#{@tune.driver_address}\r")
47
+ @tune.boot_memory(song: @song).each { |address, bytes| @computer.ram.write(address, bytes) }
48
+ @computer.type_text(@tune.boot_command)
46
49
  @injected = true
47
50
  end
48
51
 
49
52
  def begin_playing
50
- return unless @injected
53
+ return if @started || !@injected
51
54
 
52
55
  @computer.sid.synthesize!
53
56
  @started = true
@@ -60,7 +60,7 @@ module Badline
60
60
  # The cycles it takes to close exactly `total_samples` windows; the SID
61
61
  # records floor(cycles * rate / clock) of them.
62
62
  def total_cycles
63
- ((total_samples * TimeOfDay::CLOCK_HZ) + @rate - 1) / @rate
63
+ ((total_samples * player.clock_hz) + @rate - 1) / @rate
64
64
  end
65
65
 
66
66
  def container_for(path)
@@ -71,13 +71,13 @@ module Badline
71
71
  end
72
72
 
73
73
  def each_frame
74
- player.sid.record(rate: @rate, filter_chunk: @filter_chunk)
74
+ player.sid.record(rate: @rate, filter_chunk: @filter_chunk, clock_hz: player.clock_hz)
75
75
  total = total_cycles
76
76
  remaining = total
77
77
  while remaining.positive?
78
78
  samples = []
79
79
  remaining -= player.frame(remaining) { |sample| samples << sample }
80
- yield samples, (total - remaining).fdiv(TimeOfDay::CLOCK_HZ)
80
+ yield samples, (total - remaining).fdiv(player.clock_hz)
81
81
  end
82
82
  end
83
83
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Audio
5
+ # Plays a running machine's SID through a sink, for the window: #feed
6
+ # queues whatever the SID recorded since the last call, once a frame,
7
+ # and #pace holds the machine to the device's clock.
8
+ #
9
+ # The device starts once the queue holds `ahead` seconds. Below real time
10
+ # the queue runs dry, and the device then stops until it holds `ahead`
11
+ # again, so the sound stutters with silent gaps rather than slowing
12
+ # down; `on_underrun` hears about the first time. A frame that would
13
+ # take the queue past `limit` seconds, as an unpaced machine above real
14
+ # time makes, is dropped whole.
15
+ class Stream
16
+ AHEAD = 0.08
17
+ LIMIT = 0.25
18
+
19
+ attr_reader :underruns, :dropped
20
+
21
+ # What #pace reads the time from and sleeps on: #now and #sleep.
22
+ attr_writer :clock
23
+
24
+ def initialize(sink, sid, ahead: AHEAD, limit: LIMIT, on_underrun: nil)
25
+ @sink = sink
26
+ @sid = sid
27
+ @ahead = ahead
28
+ @limit = limit
29
+ @on_underrun = on_underrun
30
+ @clock = Clock.new
31
+ @next_frame = nil
32
+ @started = false
33
+ @muted = false
34
+ @underruns = 0
35
+ @dropped = 0
36
+ sid.record(rate: sink.rate)
37
+ end
38
+
39
+ def rate = @sink.rate
40
+
41
+ def muted? = @muted
42
+
43
+ def playing? = @started
44
+
45
+ # Muting drops the queue; unmuting fills it to `ahead` again before
46
+ # the device restarts.
47
+ def toggle_mute
48
+ @muted = !@muted
49
+ return unless @muted
50
+
51
+ halt
52
+ @sink.clear
53
+ end
54
+
55
+ def feed
56
+ samples = @sid.drain_samples
57
+ return if @muted || samples.empty?
58
+
59
+ queued = @sink.queued_seconds
60
+ return @dropped += samples.size if queued + samples.size.fdiv(rate) > @limit
61
+
62
+ underrun! if @started && queued.zero?
63
+ @sink.queue(samples)
64
+ start if @sink.queued_seconds >= @ahead
65
+ end
66
+
67
+ # Waits out the frame just run. While the device plays, that is until
68
+ # it has played the queue down to `ahead`, so the machine runs at the
69
+ # device's rate and the queue neither drifts dry nor grows. Muted or
70
+ # filling up, the clock paces frames `seconds` apart instead.
71
+ def pace(seconds)
72
+ return wait_for_device if playing?
73
+
74
+ now = @clock.now
75
+ @next_frame = now if @next_frame.nil? || @next_frame < now
76
+ @clock.sleep(@next_frame - now)
77
+ @next_frame += seconds
78
+ end
79
+
80
+ def close = @sink.close
81
+
82
+ private
83
+
84
+ def start
85
+ return if @started
86
+
87
+ @started = true
88
+ @sink.start
89
+ end
90
+
91
+ def wait_for_device
92
+ @next_frame = nil
93
+ while (excess = @sink.queued_seconds - @ahead).positive?
94
+ @clock.sleep([excess, 0.001].max)
95
+ end
96
+ end
97
+
98
+ def halt
99
+ @started = false
100
+ @sink.pause
101
+ end
102
+
103
+ def underrun!
104
+ @underruns += 1
105
+ @on_underrun&.call if @underruns == 1
106
+ halt
107
+ end
108
+
109
+ # The host's monotonic clock.
110
+ class Clock
111
+ def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
112
+
113
+ def sleep(seconds) = Kernel.sleep(seconds)
114
+ end
115
+ end
116
+ end
117
+ end
data/lib/badline/audio.rb CHANGED
@@ -9,6 +9,7 @@ require "badline/audio/machine_player"
9
9
  require "badline/audio/renderer"
10
10
  require "badline/audio/sdl_sink"
11
11
  require "badline/audio/playback"
12
+ require "badline/audio/stream"
12
13
  require "badline/audio/console"
13
14
  require "badline/audio/jukebox"
14
15
  require "badline/audio/cli"
@@ -18,7 +18,10 @@ module Badline
18
18
  class EasyFlash < Cartridge
19
19
  BANKS = 64
20
20
  EAPI_OFFSET = 0x1800
21
- EAPI = File.binread(File.expand_path("../roms/eapi/eapi-am29f040-14", __dir__)).bytes.drop(2).freeze
21
+
22
+ def self.eapi
23
+ @eapi ||= ROM.read("eapi/eapi-am29f040-14").drop(2).freeze
24
+ end
22
25
 
23
26
  attr_reader :low_flash, :high_flash
24
27
 
@@ -110,7 +113,8 @@ module Badline
110
113
  def install_eapi(high)
111
114
  return unless high[EAPI_OFFSET, 4] == "eapi".bytes
112
115
 
113
- high[EAPI_OFFSET, EAPI.length] = EAPI
116
+ eapi = EasyFlash.eapi
117
+ high[EAPI_OFFSET, eapi.length] = eapi
114
118
  end
115
119
 
116
120
  def place(flash_data, offset, bytes)
@@ -61,7 +61,7 @@ module Badline
61
61
  end
62
62
 
63
63
  def install_chips(chips)
64
- @rom = banks_from(chips).first.compact.first
64
+ @rom = banks_from(chips).first.compact.first || EMPTY_BANK
65
65
  @roml = Window.new(self, @rom)
66
66
  @discharged_at = 0
67
67
  self.mode = :rom8k
@@ -26,7 +26,7 @@ module Badline
26
26
  private
27
27
 
28
28
  def install_chips(chips)
29
- @roml = banks_from(chips).first.compact.first
29
+ @roml = banks_from(chips).first.compact.first || EMPTY_BANK
30
30
  reset
31
31
  end
32
32
  end
@@ -2,12 +2,15 @@
2
2
 
3
3
  module Badline
4
4
  class Cartridge
5
+ # Magic Desk: up to 128 8K banks. A write to I/O 1 selects the bank from
6
+ # bits 0-6, masked to the size of the ROM and at least four banks, and
7
+ # switches the ROM out with bit 7.
5
8
  class MagicDesk < Cartridge
6
9
  def poke(addr, value)
7
10
  return if addr > 0xdeff
8
11
 
9
12
  @exrom = value.anybits?(0x80) ? 1 : 0
10
- @roml = @banks[(value & 0x3f) % @banks.length]
13
+ @roml = bank(@banks, value & @bank_mask)
11
14
  changed!
12
15
  end
13
16
 
@@ -19,6 +22,7 @@ module Badline
19
22
 
20
23
  def install_chips(chips)
21
24
  @banks = banks_from(chips).first
25
+ @bank_mask = (bank_mask(@banks) | 0x03) & 0x7f
22
26
  reset
23
27
  end
24
28
  end
@@ -2,11 +2,13 @@
2
2
 
3
3
  module Badline
4
4
  class Cartridge
5
+ # Ocean: up to 64 8K banks. A write to I/O 1 selects the bank from bits
6
+ # 0-5, masked to the size of the ROM.
5
7
  class Ocean < Cartridge
6
8
  def poke(addr, value)
7
9
  return if addr > 0xdeff
8
10
 
9
- select_bank((value & 0x3f) % @banks.length)
11
+ select_bank(value & @bank_mask)
10
12
  changed!
11
13
  end
12
14
 
@@ -19,12 +21,13 @@ module Badline
19
21
 
20
22
  # In 16K mode the selected bank shows through ROMH as well.
21
23
  def select_bank(number)
22
- @roml = @banks[number]
24
+ @roml = bank(@banks, number)
23
25
  @romh = @roml if game.zero?
24
26
  end
25
27
 
26
28
  def install_chips(chips)
27
29
  @banks = banks_from(chips).first
30
+ @bank_mask = bank_mask(@banks) & 0x3f
28
31
  reset
29
32
  end
30
33
  end