badline 0.7.0 → 0.9.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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/CLAUDE.md +3 -1
  4. data/README.md +104 -292
  5. data/doc/library.md +69 -0
  6. data/doc/media.md +78 -0
  7. data/doc/pinned-behaviour.md +53 -9
  8. data/doc/sid-player.md +136 -0
  9. data/doc/snapshots.md +102 -0
  10. data/lib/badline/address_bus/roms.rb +32 -0
  11. data/lib/badline/address_bus.rb +4 -4
  12. data/lib/badline/audio/bare_player.rb +24 -0
  13. data/lib/badline/audio/checkpoints.rb +42 -0
  14. data/lib/badline/audio/jukebox.rb +11 -4
  15. data/lib/badline/audio/machine_player.rb +16 -0
  16. data/lib/badline/audio/renderer.rb +80 -9
  17. data/lib/badline/audio/stereo.rb +3 -5
  18. data/lib/badline/audio.rb +1 -0
  19. data/lib/badline/cartridge/freezer.rb +2 -0
  20. data/lib/badline/cartridge.rb +3 -0
  21. data/lib/badline/computer/attachments.rb +5 -5
  22. data/lib/badline/computer.rb +12 -10
  23. data/lib/badline/core.rb +48 -0
  24. data/lib/badline/data_path.rb +37 -0
  25. data/lib/badline/datasette.rb +24 -9
  26. data/lib/badline/drive1541/disk.rb +3 -3
  27. data/lib/badline/drive1541/disk_state.rb +14 -5
  28. data/lib/badline/frontend/app.rb +41 -37
  29. data/lib/badline/frontend/boot.rb +16 -4
  30. data/lib/badline/frontend/buttons.rb +101 -7
  31. data/lib/badline/frontend/confirmation.rb +56 -0
  32. data/lib/badline/frontend/controls.rb +44 -32
  33. data/lib/badline/frontend/file_browser.rb +225 -0
  34. data/lib/badline/frontend/frame_report.rb +52 -0
  35. data/lib/badline/frontend/menu_dialogs.rb +160 -0
  36. data/lib/badline/frontend/menu_media.rb +157 -0
  37. data/lib/badline/frontend/menu_pages.rb +220 -0
  38. data/lib/badline/frontend/name_field.rb +88 -0
  39. data/lib/badline/frontend/painter.rb +10 -0
  40. data/lib/badline/frontend/pause_menu.rb +264 -0
  41. data/lib/badline/frontend/player_window.rb +3 -3
  42. data/lib/badline/frontend/snapshots.rb +146 -18
  43. data/lib/badline/frontend/sound.rb +11 -6
  44. data/lib/badline/frontend/timeline.rb +5 -1
  45. data/lib/badline/frontend.rb +8 -0
  46. data/lib/badline/kernal_trap/load.rb +10 -0
  47. data/lib/badline/kernal_trap/routine.rb +28 -0
  48. data/lib/badline/kernal_trap/serial.rb +19 -7
  49. data/lib/badline/media/disk_list.rb +75 -0
  50. data/lib/badline/media/disk_set.rb +118 -0
  51. data/lib/badline/media.rb +9 -3
  52. data/lib/badline/model.rb +62 -0
  53. data/lib/badline/options/event.rb +5 -3
  54. data/lib/badline/options/help.rb +16 -12
  55. data/lib/badline/options/table.rb +6 -2
  56. data/lib/badline/options/validation.rb +9 -0
  57. data/lib/badline/options.rb +10 -7
  58. data/lib/badline/region.rb +16 -4
  59. data/lib/badline/reu/ram.rb +27 -0
  60. data/lib/badline/reu/saved_state.rb +27 -0
  61. data/lib/badline/roms/README +10 -0
  62. data/lib/badline/roms/kernal-sx64.rom +0 -0
  63. data/lib/badline/sid/decimator.rb +13 -0
  64. data/lib/badline/sid/filter.rb +6 -3
  65. data/lib/badline/sid.rb +19 -0
  66. data/lib/badline/snapshot/fields.rb +1 -0
  67. data/lib/badline/snapshot/setup.rb +20 -8
  68. data/lib/badline/snapshot/state.rb +14 -7
  69. data/lib/badline/snapshot/vice/main_cpu.rb +1 -1
  70. data/lib/badline/snapshot/vice/peripherals.rb +12 -2
  71. data/lib/badline/snapshot/vice/restore.rb +3 -1
  72. data/lib/badline/snapshot/vice/reu1764.rb +55 -0
  73. data/lib/badline/snapshot/vice/vicii.rb +59 -13
  74. data/lib/badline/snapshot/vice/vicii_import.rb +14 -3
  75. data/lib/badline/snapshot/vice.rb +25 -23
  76. data/lib/badline/storage/t64.rb +7 -4
  77. data/lib/badline/storage/tap.rb +7 -5
  78. data/lib/badline/storage/unavailable.rb +1 -1
  79. data/lib/badline/storage.rb +5 -5
  80. data/lib/badline/version.rb +1 -1
  81. data/lib/badline/vic/sprite/timing.rb +5 -9
  82. data/lib/badline/vic/sprite.rb +4 -3
  83. data/lib/badline.rb +2 -38
  84. metadata +22 -1
data/doc/media.md ADDED
@@ -0,0 +1,78 @@
1
+ # Media
2
+
3
+ The [README](../README.md#media) lists the formats badline takes. This
4
+ page covers how device 8 and the true 1541 work, and how to attach media
5
+ from Ruby.
6
+
7
+ ## Device 8 without a drive
8
+
9
+ Apart from a `.g64`, which only a true drive can read, there is no 1541
10
+ unless `--true-drive` asks for one. Device 8 works by trapping the
11
+ KERNAL's `LOAD` and `SAVE` routines and its serial bus primitives, so
12
+ files open by name through `OPEN` and `CHRIN` as well. `LOAD"$",8` lists
13
+ the directory of any medium mounted there, as a 1541 does.
14
+
15
+ The command channel answers `I`, `B-P` and `U1` block reads, which
16
+ covers loaders that read blocks directly. On a writable disk image it
17
+ also takes `SAVE` (with `@0:` to replace a file), files opened for
18
+ writing or appending, `S` to scratch, `U2` and `B-W` block writes, and
19
+ `B-A` and `B-F`. It doesn't rename, copy, format or validate disks.
20
+ Loaders that upload their own code to the drive with `M-W` and `M-E`,
21
+ and copy protection that reads raw GCR, won't work there, but do on a
22
+ true drive.
23
+
24
+ A directory mounted as device 8 serves the `.prg` and `.p00` files in it
25
+ and the contents of any `.t64`, and `SAVE` writes a new `.prg`.
26
+
27
+ ## The true 1541
28
+
29
+ `--true-drive` puts an emulated 1541 on device 8 instead, running its
30
+ own DOS ROM on its own 6502 and talking to the machine over the serial
31
+ bus. It reads `.d64` and `.g64` images, which it autostarts with the
32
+ same `LOAD"*",8,1` and `RUN`, but not `.d71`, `.d81` or `.t64` images
33
+ or directories. Loading runs at the speed of a real 1541, and the
34
+ drive's red LED lights in the bottom right corner of the border.
35
+
36
+ A `.g64` holds the disk's raw GCR, half tracks and all, so copy
37
+ protection and fast loaders that read it work.
38
+
39
+ ## Write protection
40
+
41
+ Disks given on the command line, inserted from the pause menu or by an
42
+ `--at` insert go in write-protected, so the drive reports
43
+ `26,WRITE PROTECT ON` for any write and the image file stays as it was.
44
+ `--writable`, or WRITABLE on the pause menu's Drive page, lets writes go
45
+ straight back to the image file. An image the host can't write, and a
46
+ `.t64`, stay write-protected.
47
+
48
+ ## From Ruby
49
+
50
+ `Badline::Media.attach(computer, path)` attaches any medium the
51
+ command line takes, and autostarts it unless given `autostart: false`.
52
+ `Badline::Media.insert_disk(computer, path)` swaps the disk image or
53
+ directory in device 8 while the machine runs, for software that asks for
54
+ another disk. Both write to disk images unless told otherwise:
55
+ `insert_disk` takes `read_only: true`, and `attach` takes
56
+ `disk: { read_only: true }`, as the test harnesses under `bin/` do. A
57
+ `.g64` goes in the true 1541, and takes out a disk mounted through the
58
+ traps, so `LOAD` and `SAVE` reach the 1541 too. With the 1541 in device
59
+ 8, a `.d64` goes in its drive as well.
60
+
61
+ `Badline::Media::DiskSet.around(path)` lists the disks of the set a disk
62
+ image belongs to, which the pause menu's PREVIOUS DISK and NEXT DISK step
63
+ through. An `.m3u` or `.vfl` list in the disk's folder that names it
64
+ gives the set. Without one, the set is found by name in the folder.
65
+ Given a list, it returns the disks the list names.
66
+
67
+ A list names one disk image a line, as a path relative to the list or an
68
+ absolute one. Blank lines and lines starting with `#` are skipped, and
69
+ entries that aren't there are left out. In a VICE `.vfl` flip list, only
70
+ the entries after a `UNIT 8` line, or before any `UNIT` line, count.
71
+ `Media.attach` and the command line take a list as media and attach its
72
+ first disk.
73
+
74
+ ## The light pen
75
+
76
+ Control port 1's fire line is also the VIC-II's light pen input, so
77
+ joystick 1's fire button and the 1351's left button in port 1 latch the
78
+ light pen registers.
@@ -26,6 +26,7 @@ only catches the rows that happen to move.
26
26
  - [VIC light pen](#vic-light-pen)
27
27
  - [VIC-II 8565](#vic-ii-8565)
28
28
  - [VIC-II NTSC](#vic-ii-ntsc)
29
+ - [VIC-II 6572 (Drean)](#vic-ii-6572-drean)
29
30
  - [CIA 6526 timer pipeline](#cia-6526-timer-pipeline)
30
31
  - [CIA 6526A interrupt register](#cia-6526a-interrupt-register)
31
32
  - [CIA serial shift register](#cia-serial-shift-register)
@@ -293,7 +294,9 @@ only catches the rows that happen to move.
293
294
  early in the next one for 3–7.
294
295
  - A sprite still shifting at K loses the rest of its row. Its output
295
296
  holds its last pixel from K through **K+6**, then goes dark.
296
- - A comparator hit in K..K+11 shows nothing.
297
+ - A comparator hit in K..K+11 shows nothing. Where K falls in the
298
+ last 11 pixels of the line, as sprite 3's does on a 65-cycle line,
299
+ the twelve run on into the next line's first pixels.
297
300
  - A hit from K+12 on shows the row the reload brought. For sprites 0–2
298
301
  that is the *next* line's row, a line early. For sprites 3–7 it is the
299
302
  current row, and a hit ahead of their K shows the previous line's row.
@@ -307,6 +310,11 @@ only catches the rows that happen to move.
307
310
  `spritex/demusinterruptus` and `spritegap2` pass on it too.
308
311
  - Spec guard: *the reload* in
309
312
  [`vic/sprite_spec.rb`](../spec/badline/vic/sprite_spec.rb).
313
+ - The run into the next line is pinned by `spritescan_drean`'s 6572
314
+ dump, where sprite 3 shows nothing for X = $193 to $19e, the twelve
315
+ pixels from K = 515 on a 520-pixel line. Without it a hit at $198 to
316
+ $19e collides. Spec guard: *the reload near the end of a 65-cycle
317
+ line*. The `testbench-ntsc` sprite rows pass either way.
310
318
  - The hold's length is pinned by `spritefetchbug/test-136-2a`, whose
311
319
  X-expanded sprite 0 sits at X = $136 and is still shifting at 459. Its
312
320
  reference holds the last pixel through 465. Holding it for one pixel
@@ -941,14 +949,22 @@ region moves the sprites. The positions live in `Region::Profile` and
941
949
  - Spec guard: *sprite DMA cycle stealing on NTSC* and *#phi1_data on
942
950
  NTSC* in [`vic_spec.rb`](../spec/badline/vic_spec.rb), and
943
951
  [`timing_spec.rb`](../spec/badline/vic/sprite/timing_spec.rb).
944
- - **Display compare and X counter.** The 6567R8 turns a sprite's display
945
- on and off in cycle 59 (`sprite_display_cycle`), where the 6567R56A
946
- keeps the 6569's cycle 58. Its X counter holds for 8 pixels
947
- (`x_hold`), reading $184-$187 three times, so it reaches $1ff on a
948
- 520-pixel line. The 6567R56A's runs straight through 512 pixels.
949
- Neither is pinned yet: moving the 6567R8's display compare to 58, the
950
- 6567R56A's to 59, or running the 6567R8's counter straight through and
951
- wrapping it at 512 fails no row of those subsets.
952
+ - **Display compare.** The 6567R8 turns a sprite's display on and off in
953
+ cycle 59 (`sprite_display_cycle`), where the 6567R56A keeps the 6569's
954
+ cycle 58. This isn't pinned yet: moving the 6567R8's to 58 or the
955
+ 6567R56A's to 59 fails no row of those subsets.
956
+ - **X counter.** The 6567R8's X counter spends 8 pixels more on a line
957
+ (`x_hold`): after $187 it runs over $180-$187 a second time, then on
958
+ from $188, so it reaches $1ff on a 520-pixel line. The 6567R56A's runs
959
+ straight through 512 pixels.
960
+ - Pinned for the 6572, which shares the line, by `spritescan_drean`'s
961
+ dump (see [VIC-II 6572 (Drean)](#vic-ii-6572-drean)).
962
+ - The 6567R8's own lightpen dump (`split-tests/lightpen`'s
963
+ `dump6567.prg`, run by hand on `--model ntsc`) agrees: the sample the
964
+ test takes 8 pixels into the hold reads $c2 ($180-$181). Reading
965
+ $184-$187 there instead, as badline did before, gives $c4. The 6572's
966
+ `dump6572.prg` has the same values. The `testbench-ntsc` rows pass
967
+ either way.
952
968
  - **Blanking and crop.** The blanked lines are the ones VICE's NTSC view
953
969
  leaves out, 12-27 on the 6567R8 and 13-27 on the 6567R56A, so the
954
970
  `testbench-ntsc` screenshots, VICE's 247 lines from line 28 running on
@@ -970,6 +986,34 @@ What the NTSC references don't settle, and so what stays as it is:
970
986
  readme says it was matched against screenshots, and the one 6567R8
971
987
  photograph in the testprogs is of revision R01.
972
988
 
989
+ ## VIC-II 6572 (Drean)
990
+
991
+ The 6572 of the Drean C64 (`Region::DREAN`) runs the 6567R8's line, 65
992
+ cycles with its sprite fetches in cycle 59, its display compare in cycle
993
+ 59 and its X counter running over $180-$187 twice, on PAL-N's 312 lines at
994
+ 1,023,440 Hz. The oracle is `split-tests/spritescan/spritescan_drean.prg`,
995
+ which checks itself against its `dump6572.bin`, from a real 6572R1. The
996
+ testlist comments that row out, and `testbench-drean`
997
+ (`bin/testbench --drean`) runs it.
998
+
999
+ - Pinned by that dump: every byte the test compares matches, and the
1000
+ test passes.
1001
+ - The X counter's second run over $180-$187 is pinned by sprite 1,
1002
+ pattern C (a one-pixel sprite against one shifted a pixel left), which
1003
+ collides for X = $180 to $186 on the 6572. Those are inside sprite 1's
1004
+ twelve dead pixels (K = 483), so the sprite only shows from the second
1005
+ time the counter reads its X. Repeating $184-$187 in the hold instead
1006
+ misses $180-$183 (4 bytes), repeating $180-$183 misses $184-$186 (3),
1007
+ and repeating $17c-$183 or $184-$18b moves 18 bytes. Spec guard: the
1008
+ 6567R8 and 6572 examples in
1009
+ [`timing_spec.rb`](../spec/badline/vic/sprite/timing_spec.rb).
1010
+ - The display compare isn't pinned: the dump matches with it in cycle
1011
+ 58 as well as 59.
1012
+ - Not matched: `spritegap3`'s 6572 dump has sprite 0 at X = 0 miss the
1013
+ sprite at X = 1 for every pair it is in, where badline, like the
1014
+ 6567R8 dump, has them collide. The test still passes, because it
1015
+ accepts the 6567R8's dump too.
1016
+
973
1017
  ## CIA 6526 timer pipeline
974
1018
 
975
1019
  - Start and stop go through two stages. A force load lands one tick after
data/doc/sid-player.md ADDED
@@ -0,0 +1,136 @@
1
+ # The SID player
2
+
3
+ The [README](../README.md#playing-and-rendering-sid-tunes) shows how to
4
+ start it. This page covers the player in detail.
5
+
6
+ ```sh
7
+ badline sid ~/C64Music/MUSICIANS/H/Hubbard_Rob # play every tune below a directory
8
+ badline sid tune.sid other.sid # play a queue of tunes
9
+ badline --headless tune.sid # play, length from HVSC
10
+ badline --headless -s 3 tune.sid # play the third subtune
11
+ badline --headless --all-subtunes tune.sid # play on through every subtune
12
+ badline --seconds 180 tune.sid --audio-out out.aiff
13
+ badline -s 3 --rate 48000 tune.sid --audio-out out.wav
14
+ badline --headless --sid 8580 tune.sid
15
+ badline --filter-chunk 1 tune.sid --audio-out out.wav # exact filter, slower
16
+ ```
17
+
18
+ ## The queue
19
+
20
+ `badline sid FILE|DIR...` plays a queue of tunes in the SID player's
21
+ window, in the order given, and a directory adds every `.sid` tune below
22
+ it in path order. `badline sid --headless` plays the queue in the
23
+ terminal instead. `badline sid` on its own opens the window with an
24
+ empty queue. Dropping `.sid` files or folders on the window adds them
25
+ to the end of the queue, and an empty queue starts playing them. A file
26
+ that isn't a tune is skipped.
27
+
28
+ `badline tune.sid` plays the tune in the SID player too, unless an
29
+ option of the emulator's window, such as `--ntsc` or `--reu`, asks for
30
+ the machine: then a tune for one SID runs on the emulated C64, started
31
+ through a small driver after boot.
32
+
33
+ `--subtune` (or `-s`) picks the first tune's subtune, counting from 1 as
34
+ HVSC does, and defaults to the tune's own start subtune. Each tune plays
35
+ that one subtune, and when it ends the player goes on to the next tune,
36
+ stopping after the last. `a`, or `--all-subtunes`, turns on playing all
37
+ subtunes, so that a subtune that ends goes on to the tune's next
38
+ subtune, and a tune stepped to starts on its first subtune.
39
+
40
+ `--sid auto`, the default, fits each of a tune's SIDs the model its
41
+ header names. A tune written for 2 or 3 SIDs plays on as many, at the
42
+ addresses its header gives, in stereo: SID 1 on the left, SID 2 on the
43
+ right and SID 3 in the centre. A tune on one SID plays the same on both
44
+ channels.
45
+
46
+ Playback asks the device for 44.1 kHz and takes whatever rate it
47
+ offers, unless `--rate` says otherwise. Ctrl-C stops it. The emulator
48
+ window's own options, `--no-autostart`, `--writable`, `--sound`,
49
+ `--true-drive`, `--reu`, `--model`, `--ntsc` and `--verbose`, don't apply.
50
+
51
+ ## The window
52
+
53
+ The window is worked with the mouse. Its header shows the tune's name,
54
+ author and release, the tune the subtune covers at the moment when
55
+ HVSC's STIL credits one, following the times STIL gives, and buttons
56
+ that switch between three views and between the tune's own SID model,
57
+ the 6581 and the 8580, which changes the chips playing on the spot. Its
58
+ footer shows the time played on a bar you can click to seek, and buttons
59
+ that pause, step between tunes and between a tune's subtunes, and turn
60
+ shuffle, looping and all subtunes on and off.
61
+
62
+ - The visualizer shows each voice's note, and how far off it is in
63
+ cents, over a scope of the voice's output, and the mixed output below
64
+ them. A tune on more than one SID gets a row of voices for each SID,
65
+ and the mix splits into its left and right.
66
+ - The SID view shows one SID at a time, and for a tune on more than one,
67
+ a row of SID 1, 2 and 3 buttons at its top picks which. It shows each
68
+ voice's output, its control bits, pulse width, envelope settings, and
69
+ the envelope's level and stage, and the filter's modes, cutoff,
70
+ resonance, volume and its response on the chip playing.
71
+ - The INFO view shows the tune's STIL entry and the subtune's, with a
72
+ scroll bar, the mouse wheel or the up and down keys for the long ones.
73
+
74
+ The terminal's keys work in the window too, along with Tab to switch
75
+ views and `c` to step through the SID models.
76
+
77
+ ## The terminal
78
+
79
+ Played on a terminal, `--headless` and `sid` show each tune's name,
80
+ author and release as it starts, then the tune's place in the queue, the
81
+ subtune number and the time played against the subtune's length.
82
+ `--headless` queues just the one tune.
83
+
84
+ → and ← step to the tune's next and previous subtune, stopping at its
85
+ first and last. `n` and `p` step to the next and previous tune. `s`
86
+ turns shuffle on and off, which plays the queue's tunes in a random
87
+ order, and `l` turns looping on and off, so that the end of the queue
88
+ goes on to its start. `,` and `.` seek 10 seconds back and forward
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
96
+ modes are on, and they last until the player quits.
97
+
98
+ `--no-tui`, or output that isn't a terminal, gives plain progress output
99
+ instead and plays through the queue without the keys.
100
+
101
+ ## Rendering
102
+
103
+ `--audio-out` renders one subtune to a 16-bit stereo PCM file. The
104
+ file's extension picks the format, `.wav` or `.aiff`.
105
+
106
+ ## Lengths and STIL
107
+
108
+ A `.sid` file doesn't store its length, so the player looks the tune up
109
+ by MD5 in HVSC's `Songlengths.md5`. It finds the database through
110
+ `--songlengths`, in a `DOCUMENTS` directory in any of the tune's parent
111
+ directories (the layout of an HVSC collection), or under
112
+ `$HVSC_BASE/DOCUMENTS`. Without a database or `--seconds` it runs for 60
113
+ seconds, but a subtune that falls silent for 5 seconds before then ends
114
+ there, when played and when rendered alike. Silent means the output
115
+ holds within 16 steps of one level, since a 6581 idles at a DC offset
116
+ rather than at zero. A subtune with a known length plays to its length
117
+ whatever it sounds like.
118
+
119
+ A tune in an HVSC collection also gets its entry in HVSC's `STIL.txt`,
120
+ found the same way: comments, covers, and subtune names and composers.
121
+ The tune's own fields follow its header, and each subtune's print as it
122
+ starts.
123
+
124
+ ## Speed
125
+
126
+ `badline` plays PSID and RSID tunes in real time. In `badline-ruby`,
127
+ PSID tunes run on a CPU and RAM with only the SID clocked, at about
128
+ twice real time, so they play smoothly, but RSID tunes set up their own
129
+ interrupts, so they boot a full C64 first and run at about half real
130
+ time. They render fine but stutter when played, and `badline-ruby` says
131
+ so when it falls behind. Both builds run the same code, so they render
132
+ the same file sample for sample. See
133
+ [native/README.md](../native/README.md#without-the-window).
134
+
135
+ The filter steps four cycles at a time; `--filter-chunk 1` steps it
136
+ every cycle, which is exact and takes about twice as long.
data/doc/snapshots.md ADDED
@@ -0,0 +1,102 @@
1
+ # Snapshots
2
+
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.
33
+
34
+ ## From Ruby
35
+
36
+ `computer.save_snapshot(path)` saves, `computer.restore_snapshot(path)`
37
+ takes a machine back to a snapshot, and `Badline::Snapshot.load(path)`
38
+ builds a new machine as the saved one was built and restores it.
39
+ `computer.snapshot` and `computer.restore(state)` do the same in memory,
40
+ without a file. A restore that fails leaves the machine as it was.
41
+
42
+ ## What a snapshot holds
43
+
44
+ A snapshot holds the machine as it was on the cycle it was saved: the
45
+ chips down to the instruction step and the pixel pipeline, the RAM and
46
+ any +60K or +256K expansion, the cartridge with its RAM and flash, a
47
+ GEO-RAM, a true 1541 with its RAM, its VIAs and the disk under the head,
48
+ a disk or directory mounted through the traps with its open channels,
49
+ and the tape with its place on it. A restored machine runs on exactly
50
+ as the saved one would have.
51
+
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
58
+ host's: the keyboard, the joysticks, the mouse and paddles, sound, and
59
+ blocks given to `on_init` that hadn't run yet, which a restore reports.
60
+
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
+ ## VICE
70
+
71
+ Snapshots use VICE's `.vsf` format. badline writes VICE's modules for
72
+ the CPU, RAM and CPU port, both CIAs, the SID and the VIC-II, and an
73
+ REU's `REU1764`, plus the ones x64sc needs to open the file, with
74
+ nothing else attached to the cartridge port, nothing on the tape or user
75
+ ports and no true drive. The VIC-II module names the chip as VICE does:
76
+ the 6569 or 8565 on PAL, the 6567R8 or 8562 on NTSC, the 6567R56A on old
77
+ NTSC and the 6572 on PAL-N. A `BADLINE` module, which VICE skips, holds
78
+ the whole machine, and badline restores its own snapshots from it.
79
+
80
+ x64sc 3.10 opens badline's snapshots, taken at the end of the
81
+ instruction the CPU was in, and of an REU transfer under way, as long
82
+ as its VIC-II model matches: `-model c64`, `c64c`, `ntsc`, `newntsc`,
83
+ `oldntsc` or `drean`, as `--model` names the machine. It takes the REU
84
+ from the snapshot, keeps its own drives and leaves out the cartridge,
85
+ the other expansions and the tape. A program x64sc runs on from badline's
86
+ snapshot reads the raster as badline does, cycle for cycle.
87
+
88
+ Snapshots x64sc saves open in badline through the same modules: the CPU
89
+ at its instruction boundary, RAM, the CPU port, the CIAs' registers,
90
+ timers and clocks, the SID's registers and reSID voice state, the
91
+ VIC-II's registers, beam position, counters and colour RAM, and an
92
+ REU's size, registers and RAM. The VIC-II's model builds the machine's
93
+ region, and the 6569R1 runs as the 6569. The VIC-II's pixel pipeline
94
+ starts empty. badline reads the modules x64sc 3.7 to 3.10 write, and
95
+ VICE's development versions' `MAINC64CPU` and `VIC-IISC`. A module
96
+ version it doesn't know is left out, or fails the restore for the CPU
97
+ and RAM. It reports the modules it leaves out, such as the 1541 drives,
98
+ the cartridge port, the datasette and the keyboard, and carries on
99
+ without them. Restored into a running machine, a VICE snapshot
100
+ fails for a machine built another way, such as a C64C's snapshot in a
101
+ C64, and otherwise takes the cartridge out and switches the machine off
102
+ and on.
@@ -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
@@ -4,6 +4,7 @@ require "badline/address_bus/saved_state"
4
4
  require "badline/address_bus/sid_slots"
5
5
  require "badline/address_bus/extra_sids"
6
6
  require "badline/address_bus/ultimax_pages"
7
+ require "badline/address_bus/roms"
7
8
 
8
9
  module Badline
9
10
  # Memory layout:
@@ -35,6 +36,7 @@ module Badline
35
36
  include Addressable
36
37
  include UltimaxPages
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,9 +73,7 @@ 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
@@ -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.