badline 0.8.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 (134) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -0
  3. data/CLAUDE.md +6 -4
  4. data/README.md +60 -19
  5. data/doc/library.md +10 -3
  6. data/doc/media.md +11 -2
  7. data/doc/pinned-behaviour.md +199 -9
  8. data/doc/sid-player.md +8 -4
  9. data/doc/snapshots.md +79 -33
  10. data/lib/badline/address_bus/pla.rb +130 -0
  11. data/lib/badline/address_bus/roms.rb +32 -0
  12. data/lib/badline/address_bus.rb +13 -92
  13. data/lib/badline/audio/bare_player.rb +24 -0
  14. data/lib/badline/audio/checkpoints.rb +42 -0
  15. data/lib/badline/audio/jukebox.rb +11 -4
  16. data/lib/badline/audio/machine_player.rb +16 -0
  17. data/lib/badline/audio/renderer.rb +80 -9
  18. data/lib/badline/audio/stereo.rb +3 -5
  19. data/lib/badline/audio.rb +1 -0
  20. data/lib/badline/chrout_trap.rb +4 -3
  21. data/lib/badline/computer/attachments.rb +52 -12
  22. data/lib/badline/computer/saved_state.rb +1 -0
  23. data/lib/badline/computer.rb +50 -33
  24. data/lib/badline/control_ports.rb +10 -3
  25. data/lib/badline/core.rb +51 -0
  26. data/lib/badline/data_path.rb +37 -0
  27. data/lib/badline/datasette.rb +63 -10
  28. data/lib/badline/drive1541/disk.rb +3 -3
  29. data/lib/badline/drive1541/disk_state.rb +14 -5
  30. data/lib/badline/drive1541/serial_port.rb +5 -8
  31. data/lib/badline/drive1541/sleep.rb +5 -6
  32. data/lib/badline/drive1541.rb +3 -3
  33. data/lib/badline/frontend/app.rb +63 -64
  34. data/lib/badline/frontend/boot.rb +34 -4
  35. data/lib/badline/frontend/confirmation.rb +17 -11
  36. data/lib/badline/frontend/controls.rb +12 -6
  37. data/lib/badline/frontend/drive_led.rb +9 -10
  38. data/lib/badline/frontend/frame_rate.rb +4 -4
  39. data/lib/badline/frontend/machine_window.rb +87 -0
  40. data/lib/badline/frontend/menu_dialogs.rb +52 -14
  41. data/lib/badline/frontend/menu_media.rb +70 -16
  42. data/lib/badline/frontend/menu_pages.rb +47 -42
  43. data/lib/badline/frontend/menu_ports.rb +69 -0
  44. data/lib/badline/frontend/name_field.rb +88 -0
  45. data/lib/badline/frontend/pacer.rb +6 -5
  46. data/lib/badline/frontend/pause_menu.rb +52 -37
  47. data/lib/badline/frontend/player_screen.rb +1 -1
  48. data/lib/badline/frontend/player_window.rb +4 -4
  49. data/lib/badline/frontend/screen.rb +39 -37
  50. data/lib/badline/frontend/sid_view.rb +1 -1
  51. data/lib/badline/frontend/snapshots.rb +145 -17
  52. data/lib/badline/frontend/sound.rb +17 -9
  53. data/lib/badline/frontend/timeline.rb +36 -5
  54. data/lib/badline/frontend.rb +3 -0
  55. data/lib/badline/iec_bus.rb +16 -28
  56. data/lib/badline/kernal_trap/drive.rb +3 -0
  57. data/lib/badline/kernal_trap/file.rb +1 -1
  58. data/lib/badline/kernal_trap/layout.rb +112 -0
  59. data/lib/badline/kernal_trap/load.rb +24 -25
  60. data/lib/badline/kernal_trap/routine.rb +17 -5
  61. data/lib/badline/kernal_trap/save.rb +13 -23
  62. data/lib/badline/kernal_trap/serial.rb +38 -24
  63. data/lib/badline/kernal_trap.rb +1 -0
  64. data/lib/badline/keyboard.rb +17 -14
  65. data/lib/badline/machine.rb +60 -0
  66. data/lib/badline/media/disk_list.rb +75 -0
  67. data/lib/badline/media/disk_set.rb +13 -3
  68. data/lib/badline/media/vic20_basic.rb +54 -0
  69. data/lib/badline/media/vic20_cartridge.rb +74 -0
  70. data/lib/badline/media/vic20_media.rb +132 -0
  71. data/lib/badline/media.rb +39 -20
  72. data/lib/badline/model.rb +62 -0
  73. data/lib/badline/options/event.rb +10 -6
  74. data/lib/badline/options/help.rb +25 -13
  75. data/lib/badline/options/table.rb +15 -1
  76. data/lib/badline/options/validation.rb +63 -3
  77. data/lib/badline/options.rb +37 -34
  78. data/lib/badline/region.rb +16 -4
  79. data/lib/badline/reu/ram.rb +27 -0
  80. data/lib/badline/reu/saved_state.rb +27 -0
  81. data/lib/badline/roms/README +37 -0
  82. data/lib/badline/roms/kernal-sx64.rom +0 -0
  83. data/lib/badline/roms/vic20/basic.rom +0 -0
  84. data/lib/badline/roms/vic20/character.rom +0 -0
  85. data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
  86. data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
  87. data/lib/badline/sid/decimator.rb +13 -0
  88. data/lib/badline/sid/filter.rb +6 -3
  89. data/lib/badline/sid.rb +19 -0
  90. data/lib/badline/snapshot/container.rb +2 -0
  91. data/lib/badline/snapshot/fields.rb +1 -0
  92. data/lib/badline/snapshot/image.rb +15 -0
  93. data/lib/badline/snapshot/setup.rb +20 -8
  94. data/lib/badline/snapshot/state.rb +15 -7
  95. data/lib/badline/snapshot/vice/main_cpu.rb +1 -1
  96. data/lib/badline/snapshot/vice/peripherals.rb +12 -2
  97. data/lib/badline/snapshot/vice/restore.rb +3 -1
  98. data/lib/badline/snapshot/vice/reu1764.rb +55 -0
  99. data/lib/badline/snapshot/vice/vicii.rb +59 -13
  100. data/lib/badline/snapshot/vice/vicii_import.rb +14 -3
  101. data/lib/badline/snapshot/vice.rb +25 -23
  102. data/lib/badline/snapshot.rb +10 -0
  103. data/lib/badline/storage/crt_file.rb +16 -5
  104. data/lib/badline/storage/disk_image.rb +3 -0
  105. data/lib/badline/storage/host_directory.rb +3 -0
  106. data/lib/badline/storage/t64.rb +10 -4
  107. data/lib/badline/storage/tap.rb +44 -8
  108. data/lib/badline/storage/unavailable.rb +3 -1
  109. data/lib/badline/storage.rb +5 -5
  110. data/lib/badline/timing.rb +12 -0
  111. data/lib/badline/version.rb +1 -1
  112. data/lib/badline/via/shift_register.rb +43 -16
  113. data/lib/badline/vic/bank.rb +53 -16
  114. data/lib/badline/vic/registers.rb +20 -4
  115. data/lib/badline/vic/sprite/timing.rb +5 -9
  116. data/lib/badline/vic/sprite.rb +4 -3
  117. data/lib/badline/vic.rb +34 -15
  118. data/lib/badline/vic20/attachments.rb +103 -0
  119. data/lib/badline/vic20/bus.rb +308 -0
  120. data/lib/badline/vic20/cpu.rb +10 -0
  121. data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
  122. data/lib/badline/vic20/port_wiring.rb +50 -0
  123. data/lib/badline/vic20/saved_state.rb +152 -0
  124. data/lib/badline/vic20/sound/output.rb +83 -0
  125. data/lib/badline/vic20/sound.rb +233 -0
  126. data/lib/badline/vic20/user_via_ports.rb +47 -0
  127. data/lib/badline/vic20/vic/painter.rb +239 -0
  128. data/lib/badline/vic20/vic/saved_state.rb +55 -0
  129. data/lib/badline/vic20/vic/text_window.rb +71 -0
  130. data/lib/badline/vic20/vic.rb +267 -0
  131. data/lib/badline/vic20.rb +220 -0
  132. data/lib/badline.rb +2 -38
  133. metadata +37 -2
  134. data/lib/badline/address_bus/ultimax_pages.rb +0 -38
data/doc/sid-player.md CHANGED
@@ -46,7 +46,7 @@ channels.
46
46
  Playback asks the device for 44.1 kHz and takes whatever rate it
47
47
  offers, unless `--rate` says otherwise. Ctrl-C stops it. The emulator
48
48
  window's own options, `--no-autostart`, `--writable`, `--sound`,
49
- `--true-drive`, `--reu`, `--ntsc` and `--verbose`, don't apply.
49
+ `--true-drive`, `--reu`, `--model`, `--ntsc` and `--verbose`, don't apply.
50
50
 
51
51
  ## The window
52
52
 
@@ -86,9 +86,13 @@ first and last. `n` and `p` step to the next and previous tune. `s`
86
86
  turns shuffle on and off, which plays the queue's tunes in a random
87
87
  order, and `l` turns looping on and off, so that the end of the queue
88
88
  goes on to its start. `,` and `.` seek 10 seconds back and forward
89
- within the subtune: a seek forward runs on silently from where the
90
- subtune is, and a seek back plays it again from its start up to the
91
- point asked for. Space pauses and `q` quits. The status line shows which
89
+ within the subtune. As a subtune plays, the player saves its state every
90
+ 10 seconds. A seek back runs silently from the latest of those saved
91
+ states at or before the point asked for, or from the subtune's start
92
+ before the first. A seek forward runs on silently from where the
93
+ subtune is, or from a saved state further on, when an earlier seek back
94
+ left one there. The saved states last until the tune or subtune
95
+ changes. Space pauses and `q` quits. The status line shows which
92
96
  modes are on, and they last until the player quits.
93
97
 
94
98
  `--no-tui`, or output that isn't a terminal, gives plain progress output
data/doc/snapshots.md CHANGED
@@ -1,11 +1,35 @@
1
1
  # Snapshots
2
2
 
3
- In the window, `F11` saves the whole machine to a new
4
- `badline-<date>-<time>.vsf` in the working directory, and `F12` goes
5
- back to the snapshot last saved or opened, in a new machine built as the
6
- saved one was. Both executables do this, and both open a `.vsf` given as
7
- the media. A snapshot that fails to open leaves the machine running as
8
- it was.
3
+ In the window, `F11` quicksaves the whole machine to one of five slots,
4
+ `quicksave-1.vsf` to `quicksave-5.vsf` in the `quicksaves` folder of
5
+ badline's data folder. It fills the empty slots first, then replaces the
6
+ one saved longest ago. `F12` restores the newest quicksave, or a named
7
+ save in the data folder's `saves` folder if one is newer, in a new
8
+ machine built as the saved one was. Quicksaves stay after badline quits,
9
+ so `F12` finds them the next time it starts. What's printed on a save or
10
+ a restore names the slot and the file.
11
+
12
+ A run without `--frames` also autosaves every 6,000 frames the machine
13
+ has run, two minutes on PAL, to `autosave-1.vsf` to `autosave-3.vsf` in
14
+ the `autosaves` folder, replacing the oldest. Time in the pause menu
15
+ doesn't count, nothing is printed, and `F12` never picks an autosave. An
16
+ autosave that fails warns once and stops autosaving.
17
+
18
+ The pause menu's Snapshots page quicksaves, saves under a name in the
19
+ `saves` folder and loads a named save. The name starts as the game's,
20
+ from its cartridge, disk or tape, or BASIC, with the next free number,
21
+ and saving over a name already taken asks first. Below those, the
22
+ newest quicksaves and autosaves, with the time each was saved, load
23
+ with a press.
24
+
25
+ The data folder is `~/Library/Application Support/badline` on macOS,
26
+ and `$XDG_DATA_HOME/badline` or `~/.local/share/badline` elsewhere.
27
+ `BADLINE_DATA_PATH` puts it somewhere else.
28
+
29
+ Both executables do this, and both open a `.vsf` given as the media,
30
+ in a machine built as the saved one was, whatever `--model` says.
31
+ `--save-snapshot FILE` saves one to `FILE` after the last frame. A
32
+ snapshot that fails to open leaves the machine running as it was.
9
33
 
10
34
  ## From Ruby
11
35
 
@@ -25,45 +49,67 @@ a disk or directory mounted through the traps with its open channels,
25
49
  and the tape with its place on it. A restored machine runs on exactly
26
50
  as the saved one would have.
27
51
 
28
- A directory, a tape and a true drive's disk image open again from their
29
- paths when the snapshot is restored, the disk with its tracks as the
30
- drive last saw them. A disk image mounted through the traps comes back
31
- with its contents from the snapshot. What the host holds stays the
52
+ A tape, a `.t64` archive and a disk image mounted through the traps come
53
+ back with their contents from the snapshot, so they need no file to
54
+ restore. A true drive's disk opens again from its image's path, with its
55
+ tracks as the drive last saw them. When that file is gone, the disk
56
+ comes back from the tracks alone, write-protected. A directory mounted
57
+ as device 8 opens again from its path. What the host holds stays the
32
58
  host's: the keyboard, the joysticks, the mouse and paddles, sound, and
33
59
  blocks given to `on_init` that hadn't run yet, which a restore reports.
34
60
 
35
- A snapshot only restores in the badline version that wrote it, into a
36
- machine with the same chip models, RAM expansion and REU.
37
- `computer.snapshot` holds an REU's RAM, registers and transfer too, but
38
- a machine with an REU doesn't save to a file yet, as badline doesn't
39
- write VICE's REU module. Nor does an NTSC machine, as badline doesn't
40
- write an NTSC VIC-II in VICE's terms yet.
61
+ Every model saves this way, PAL, NTSC, old NTSC and Drean alike, and so
62
+ does a machine with an REU, with its RAM, its registers and a transfer
63
+ part way through. A snapshot only restores in the badline version that
64
+ wrote it, into a machine with the same chip models, region, RAM
65
+ expansion, REU, KERNAL and datasette. One saved before badline recorded
66
+ the KERNAL and the datasette restores as a machine with the C64's KERNAL
67
+ and a datasette.
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.
41
81
 
42
82
  ## VICE
43
83
 
44
84
  Snapshots use VICE's `.vsf` format. badline writes VICE's modules for
45
- the CPU, RAM and CPU port, both CIAs, the SID and the VIC-II, plus the
46
- ones x64sc needs to open the file, with nothing attached to the
47
- cartridge, tape or user ports and no true drive. A `BADLINE` module,
48
- which VICE skips, holds the whole machine, and badline restores its own
49
- snapshots from it.
85
+ the CPU, RAM and CPU port, both CIAs, the SID and the VIC-II, and an
86
+ REU's `REU1764`, plus the ones x64sc needs to open the file, with
87
+ nothing else attached to the cartridge port, nothing on the tape or user
88
+ ports and no true drive. The VIC-II module names the chip as VICE does:
89
+ the 6569 or 8565 on PAL, the 6567R8 or 8562 on NTSC, the 6567R56A on old
90
+ NTSC and the 6572 on PAL-N. A `BADLINE` module, which VICE skips, holds
91
+ the whole machine, and badline restores its own snapshots from it.
50
92
 
51
93
  x64sc 3.10 opens badline's snapshots, taken at the end of the
52
- instruction the CPU was in, as long as its VIC-II model matches
53
- (`-model c64` for the default machine). It keeps its own drives and
54
- leaves out the cartridge, the expansions and the tape.
94
+ instruction the CPU was in, and of an REU transfer under way, as long
95
+ as its VIC-II model matches: `-model c64`, `c64c`, `ntsc`, `newntsc`,
96
+ `oldntsc` or `drean`, as `--model` names the machine. It takes the REU
97
+ from the snapshot, keeps its own drives and leaves out the cartridge,
98
+ the other expansions and the tape. A program x64sc runs on from badline's
99
+ snapshot reads the raster as badline does, cycle for cycle.
55
100
 
56
101
  Snapshots x64sc saves open in badline through the same modules: the CPU
57
102
  at its instruction boundary, RAM, the CPU port, the CIAs' registers,
58
- timers and clocks, the SID's registers and reSID voice state, and the
59
- VIC-II's registers, beam position, counters and colour RAM. The VIC-II's
60
- pixel pipeline starts empty. badline reads the modules x64sc 3.7 to 3.10
61
- write, and VICE's development versions' `MAINC64CPU` and `VIC-IISC`. A
62
- module version it doesn't know is left out, or fails the restore for the
63
- CPU and RAM. It reports the modules it leaves out, such as the 1541
64
- drives, the cartridge, the datasette and the keyboard, and carries on
65
- without them. A snapshot of an NTSC machine fails, as badline reads only
66
- VICE's PAL VIC-II. Restored into a running machine, a VICE snapshot
103
+ timers and clocks, the SID's registers and reSID voice state, the
104
+ VIC-II's registers, beam position, counters and colour RAM, and an
105
+ REU's size, registers and RAM. The VIC-II's model builds the machine's
106
+ region, and the 6569R1 runs as the 6569. The VIC-II's pixel pipeline
107
+ starts empty. badline reads the modules x64sc 3.7 to 3.10 write, and
108
+ VICE's development versions' `MAINC64CPU` and `VIC-IISC`. A module
109
+ version it doesn't know is left out, or fails the restore for the CPU
110
+ and RAM. It reports the modules it leaves out, such as the 1541 drives,
111
+ the cartridge port, the datasette and the keyboard, and carries on
112
+ without them. Restored into a running machine, a VICE snapshot
67
113
  fails for a machine built another way, such as a C64C's snapshot in a
68
114
  C64, and otherwise takes the cartridge out and switches the machine off
69
115
  and on.
@@ -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
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class AddressBus
5
+ # The BASIC, character and KERNAL ROMs, and which KERNAL is fitted.
6
+ module ROMs
7
+ # The KERNAL ROMs a machine can be fitted with, by name: the C64's
8
+ # and the SX-64's.
9
+ KERNALS = { c64: "kernal.rom", sx64: "kernal-sx64.rom" }.freeze
10
+
11
+ attr_reader :basic_rom, :character_rom, :kernal_rom, :kernal
12
+
13
+ # Fits the KERNAL ROM KERNALS names `name`, in place of the one before.
14
+ def kernal=(name)
15
+ raise ArgumentError, "no KERNAL named #{name}" unless KERNALS.key?(name)
16
+
17
+ @kernal = name
18
+ @kernal_rom = ROM.load(KERNALS[name], 0xe000)
19
+ update_overlays!
20
+ end
21
+
22
+ private
23
+
24
+ def load_roms
25
+ @basic_rom = ROM.load("basic.rom", 0xa000)
26
+ @character_rom = ROM.load("character.rom", 0xd000)
27
+ @kernal = :c64
28
+ @kernal_rom = ROM.load(KERNALS[:c64], 0xe000)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -3,7 +3,8 @@
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
+ require "badline/address_bus/roms"
7
8
 
8
9
  module Badline
9
10
  # Memory layout:
@@ -33,8 +34,9 @@ module Badline
33
34
  # 0xE000-0xFFFF - KERNAL ROM / Cartridge ROM (high) - 8kb
34
35
  class AddressBus
35
36
  include Addressable
36
- include UltimaxPages
37
+ include PLA
37
38
  include ExtraSIDs
39
+ include ROMs
38
40
 
39
41
  # I/O 1 and 2, and the Ultimax holes, with nothing on the bus. A read
40
42
  # picks up the byte the VIC fetched in the preceding phi1 half-cycle,
@@ -58,7 +60,7 @@ module Badline
58
60
  # See doc/pinned-behaviour.md.
59
61
  RAM_POWER_ON = Array.new(2**16) { |addr| (((addr + 2) / 4) ^ (addr / 0x4000)).odd? ? 0xff : 0x00 }.freeze
60
62
 
61
- attr_reader :io_port, :ram, :basic_rom, :character_rom, :kernal_rom,
63
+ attr_reader :io_port, :ram,
62
64
  :vic, :sid, :color_ram, :cia1, :cia2, :keyboard, :joystick1, :joystick2,
63
65
  :control_ports, :cartridge, :ultimax, :phi1_ultimax, :datasette, :region, :video_ram, :reu
64
66
 
@@ -71,15 +73,13 @@ module Badline
71
73
  @cartridge = @reu = nil
72
74
  @debug_register = nil
73
75
 
74
- @basic_rom = ROM.load("basic.rom", 0xa000)
75
- @character_rom = ROM.load("character.rom", 0xd000)
76
- @kernal_rom = ROM.load("kernal.rom", 0xe000)
76
+ load_roms
77
77
 
78
78
  @keyboard = Keyboard.new
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
@@ -74,6 +74,30 @@ module Badline
74
74
  settle
75
75
  end
76
76
 
77
+ # The rig between frames: the bus and its chips, the extra SIDs, the
78
+ # CPU, and the call waiting for the idle loop.
79
+ def save_state(out)
80
+ out.marker("BARE PLAYER")
81
+ @bus.save_state(out)
82
+ @extras.each { |sid| sid.save_state(out) }
83
+ @cpu.save_state(out)
84
+ out.optional_int(@pending).int(@argument).boolean(@idle)
85
+ end
86
+
87
+ # Takes a rig that hasn't started to where #save_state found one, in
88
+ # place of #start.
89
+ def load_state(input)
90
+ input.marker("BARE PLAYER")
91
+ @bus.load_state(input)
92
+ @extras.each { |sid| sid.load_state(input) }
93
+ @cpu.load_state(input)
94
+ @pending = input.optional_int
95
+ @argument = input.int
96
+ @idle = input.boolean?
97
+ @stereo.synthesize!
98
+ install_dispatch
99
+ end
100
+
77
101
  # Advances one call of play, or `budget` cycles if that is shorter,
78
102
  # then yields whatever the SIDs recorded over it, in stereo. Returns
79
103
  # the cycles advanced.
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Audio
5
+ # The states a subtune's renderer saved as it played, at the first
6
+ # frame end past every `every` seconds, so that a seek starts from the
7
+ # latest one at or before where it goes rather than from the subtune's
8
+ # start. Whoever plays the subtune keeps them across its renderers, and
9
+ # drops them for another subtune.
10
+ class Checkpoints
11
+ EVERY = 10.0
12
+
13
+ def initialize(every: EVERY)
14
+ @every = every
15
+ @cycles = []
16
+ @states = []
17
+ end
18
+
19
+ # Whether the frame ending `cycles` into the subtune is the first past
20
+ # the next mark.
21
+ def due?(cycles, clock_hz) = cycles >= ((@cycles.length + 1) * @every * clock_hz).round
22
+
23
+ def keep(cycles, state)
24
+ @cycles << cycles
25
+ @states << state
26
+ end
27
+
28
+ # The state saved latest at or before `cycles`, or nil.
29
+ def latest(cycles)
30
+ index = @cycles.rindex { |at| at <= cycles }
31
+ index.nil? ? nil : @states[index]
32
+ end
33
+
34
+ # The cycles into the subtune of the state saved latest at or before
35
+ # `cycles`, or 0 for the start.
36
+ def reach(cycles)
37
+ index = @cycles.rindex { |at| at <= cycles }
38
+ index.nil? ? 0 : @cycles[index]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -7,9 +7,11 @@ module Badline
7
7
  # turn the queue's shuffle, loop and all-subtunes modes on and off, and
8
8
  # quit, and the status line follows along. Seeking runs the subtune
9
9
  # silently up to where it was asked to go: on from where it is to go
10
- # forward, and from the start again to go back. A subtune that plays to
11
- # its end moves on as Media::Queue#advance says. It stops at the end of
12
- # the queue, on q, or on Ctrl-C.
10
+ # forward, and from the latest of its Checkpoints at or before there to
11
+ # go back, or to go forward when that checkpoint is further on than
12
+ # where the subtune is. A subtune that plays to its end moves on as
13
+ # Media::Queue#advance says. It stops at the end of the queue, on q, or
14
+ # on Ctrl-C.
13
15
  #
14
16
  # `renderer` builds the renderer for an entry's subtune at the sink's
15
17
  # rate, or returns nil for an entry that can't play, which is skipped
@@ -34,6 +36,9 @@ module Badline
34
36
  @backward = false
35
37
  @below_at = -BELOW_NOTE
36
38
  @from = 0.0
39
+ @entry = nil
40
+ @subtune = 0
41
+ @checkpoints = Checkpoints.new
37
42
  end
38
43
 
39
44
  # Returns the result of the last subtune's Playback#play, or :unplayable
@@ -77,6 +82,7 @@ module Badline
77
82
  end
78
83
 
79
84
  def play(entry, subtune)
85
+ @checkpoints = Checkpoints.new unless entry.equal?(@entry) && subtune == @subtune
80
86
  @entry = entry
81
87
  @subtune = subtune
82
88
  @elapsed = 0.0
@@ -84,6 +90,7 @@ module Badline
84
90
  return :unplayable if renderer.nil?
85
91
 
86
92
  renderer.from = @from
93
+ renderer.checkpoints = @checkpoints
87
94
  @streaming = renderer
88
95
  @from = 0.0
89
96
  @backward = false
@@ -127,7 +134,7 @@ module Badline
127
134
 
128
135
  def seek(seconds)
129
136
  target = seconds.clamp(0.0, [@length.call(@entry, @subtune) - 1.0, 0.0].max)
130
- return @streaming.from = target if target > @elapsed
137
+ return @streaming.from = target if target > @elapsed && !@streaming.checkpoint_ahead?(target)
131
138
 
132
139
  @from = target
133
140
  skip
@@ -39,6 +39,22 @@ module Badline
39
39
 
40
40
  def clock_hz = @computer.region.clock_hz
41
41
 
42
+ # The whole machine, as Computer#snapshot takes it, and the extra SIDs,
43
+ # once it plays.
44
+ def save_state(out)
45
+ @computer.save_state(out)
46
+ @extras.each { |sid| sid.save_state(out) }
47
+ end
48
+
49
+ # Takes a machine that hasn't started to where #save_state found one,
50
+ # in place of #start.
51
+ def load_state(input)
52
+ @computer.load_state(input)
53
+ @extras.each { |sid| sid.load_state(input) }
54
+ @stereo.synthesize!
55
+ @started = true
56
+ end
57
+
42
58
  # Advances one frame of the machine, or `budget` cycles if that is
43
59
  # shorter, then yields whatever the SIDs recorded over it, in stereo.
44
60
  # Returns the cycles advanced.