badline 0.5.0 → 0.6.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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/CLAUDE.md +23 -23
  4. data/CONTRIBUTING.md +5 -0
  5. data/README.md +59 -1
  6. data/doc/pinned-behaviour.md +4 -2
  7. data/exe/badline-ruby +2 -1
  8. data/lib/badline/address_bus/saved_state.rb +72 -0
  9. data/lib/badline/address_bus.rb +1 -0
  10. data/lib/badline/audio/stream.rb +6 -0
  11. data/lib/badline/cartridge/action_replay.rb +15 -0
  12. data/lib/badline/cartridge/comal80.rb +8 -0
  13. data/lib/badline/cartridge/dinamic.rb +8 -0
  14. data/lib/badline/cartridge/easy_flash.rb +20 -1
  15. data/lib/badline/cartridge/epyx_fastload.rb +8 -0
  16. data/lib/badline/cartridge/final_cartridge3.rb +12 -0
  17. data/lib/badline/cartridge/flash.rb +41 -0
  18. data/lib/badline/cartridge/fun_play.rb +8 -0
  19. data/lib/badline/cartridge/g_mod2.rb +12 -0
  20. data/lib/badline/cartridge/game_system.rb +8 -0
  21. data/lib/badline/cartridge/geo_ram.rb +16 -0
  22. data/lib/badline/cartridge/magic_desk.rb +8 -0
  23. data/lib/badline/cartridge/ocean.rb +8 -0
  24. data/lib/badline/cartridge/pagefox.rb +12 -0
  25. data/lib/badline/cartridge/retro_replay.rb +44 -1
  26. data/lib/badline/cartridge/rgcd.rb +10 -0
  27. data/lib/badline/cartridge/super_games.rb +10 -0
  28. data/lib/badline/cartridge/zaxxon.rb +16 -0
  29. data/lib/badline/cartridge.rb +72 -0
  30. data/lib/badline/cia/interrupt_register.rb +17 -0
  31. data/lib/badline/cia/serial.rb +44 -0
  32. data/lib/badline/cia/state.rb +47 -0
  33. data/lib/badline/cia/timer.rb +33 -0
  34. data/lib/badline/cia.rb +24 -2
  35. data/lib/badline/computer/attachments.rb +132 -0
  36. data/lib/badline/computer/saved_state.rb +116 -0
  37. data/lib/badline/computer.rb +8 -31
  38. data/lib/badline/cpu/microcode.rb +8 -0
  39. data/lib/badline/cpu/saved_state.rb +115 -0
  40. data/lib/badline/cpu.rb +2 -0
  41. data/lib/badline/datasette.rb +27 -0
  42. data/lib/badline/drive1541/bus.rb +89 -13
  43. data/lib/badline/drive1541/disk.rb +7 -2
  44. data/lib/badline/drive1541/disk_state.rb +80 -0
  45. data/lib/badline/drive1541/idle.rb +9 -57
  46. data/lib/badline/drive1541/mechanism.rb +2 -0
  47. data/lib/badline/drive1541/mechanism_state.rb +67 -0
  48. data/lib/badline/drive1541/orbit.rb +81 -0
  49. data/lib/badline/drive1541/rotation.rb +10 -0
  50. data/lib/badline/drive1541/serial_port.rb +10 -0
  51. data/lib/badline/drive1541/sleep.rb +114 -0
  52. data/lib/badline/drive1541.rb +52 -2
  53. data/lib/badline/gui/application.rb +41 -14
  54. data/lib/badline/gui/gamepads.rb +7 -0
  55. data/lib/badline/gui/snapshots.rb +67 -0
  56. data/lib/badline/gui.rb +1 -0
  57. data/lib/badline/iec_bus.rb +16 -0
  58. data/lib/badline/kernal_trap/channel.rb +27 -0
  59. data/lib/badline/kernal_trap/drive/channels.rb +25 -0
  60. data/lib/badline/kernal_trap/drive/memory.rb +9 -0
  61. data/lib/badline/kernal_trap/drive/status.rb +8 -0
  62. data/lib/badline/kernal_trap/drive/write_file.rb +21 -0
  63. data/lib/badline/kernal_trap/drive.rb +19 -0
  64. data/lib/badline/kernal_trap/save.rb +8 -0
  65. data/lib/badline/kernal_trap/serial.rb +16 -0
  66. data/lib/badline/memory.rb +11 -0
  67. data/lib/badline/options.rb +3 -2
  68. data/lib/badline/ram_expansion/plus256k.rb +16 -0
  69. data/lib/badline/ram_expansion/plus60k.rb +12 -0
  70. data/lib/badline/ram_expansion/unexpanded.rb +6 -0
  71. data/lib/badline/reu/dma.rb +36 -0
  72. data/lib/badline/reu/ram.rb +15 -0
  73. data/lib/badline/reu/saved_state.rb +52 -0
  74. data/lib/badline/reu.rb +4 -0
  75. data/lib/badline/sdl/events.rb +2 -0
  76. data/lib/badline/sid/envelope/state.rb +68 -0
  77. data/lib/badline/sid/envelope.rb +7 -0
  78. data/lib/badline/sid/filter.rb +38 -0
  79. data/lib/badline/sid/voice.rb +10 -0
  80. data/lib/badline/sid/waveform/state.rb +66 -0
  81. data/lib/badline/sid/waveform.rb +2 -0
  82. data/lib/badline/sid.rb +34 -0
  83. data/lib/badline/snapshot/container.rb +117 -0
  84. data/lib/badline/snapshot/fields.rb +54 -0
  85. data/lib/badline/snapshot/image.rb +70 -0
  86. data/lib/badline/snapshot/machine_state.rb +77 -0
  87. data/lib/badline/snapshot/setup.rb +58 -0
  88. data/lib/badline/snapshot/state.rb +178 -0
  89. data/lib/badline/snapshot/vice/c64_mem.rb +53 -0
  90. data/lib/badline/snapshot/vice/cias.rb +89 -0
  91. data/lib/badline/snapshot/vice/cias_import.rb +70 -0
  92. data/lib/badline/snapshot/vice/main_cpu.rb +76 -0
  93. data/lib/badline/snapshot/vice/peripherals.rb +47 -0
  94. data/lib/badline/snapshot/vice/restore.rb +125 -0
  95. data/lib/badline/snapshot/vice/sid_extended.rb +96 -0
  96. data/lib/badline/snapshot/vice/sid_registers.rb +46 -0
  97. data/lib/badline/snapshot/vice/vicii.rb +119 -0
  98. data/lib/badline/snapshot/vice/vicii_import.rb +131 -0
  99. data/lib/badline/snapshot/vice.rb +74 -0
  100. data/lib/badline/snapshot.rb +52 -0
  101. data/lib/badline/storage/crt_file.rb +9 -3
  102. data/lib/badline/storage/d64_image.rb +2 -0
  103. data/lib/badline/storage/d71_image.rb +2 -0
  104. data/lib/badline/storage/d81_image.rb +2 -0
  105. data/lib/badline/storage/disk_image.rb +11 -2
  106. data/lib/badline/storage/host_directory.rb +5 -0
  107. data/lib/badline/storage/t64.rb +6 -0
  108. data/lib/badline/storage/tap.rb +10 -1
  109. data/lib/badline/storage/unavailable.rb +20 -0
  110. data/lib/badline/storage.rb +34 -0
  111. data/lib/badline/time_of_day.rb +56 -0
  112. data/lib/badline/version.rb +1 -1
  113. data/lib/badline/via/control_lines.rb +15 -0
  114. data/lib/badline/via/fast_forward.rb +15 -0
  115. data/lib/badline/via/interrupt_register.rb +9 -0
  116. data/lib/badline/via/shift_register.rb +13 -0
  117. data/lib/badline/via/state.rb +42 -0
  118. data/lib/badline/via/timer1.rb +19 -0
  119. data/lib/badline/via/timer2.rb +24 -0
  120. data/lib/badline/via.rb +2 -0
  121. data/lib/badline/vic/border_mask.rb +10 -0
  122. data/lib/badline/vic/collisions.rb +11 -0
  123. data/lib/badline/vic/color_patches.rb +11 -0
  124. data/lib/badline/vic/display_state.rb +29 -1
  125. data/lib/badline/vic/graphics_shifter.rb +26 -0
  126. data/lib/badline/vic/register_log.rb +18 -0
  127. data/lib/badline/vic/registers.rb +13 -2
  128. data/lib/badline/vic/sequencer.rb +12 -0
  129. data/lib/badline/vic/sequencer_state.rb +89 -0
  130. data/lib/badline/vic/sprite/internal_bus.rb +11 -0
  131. data/lib/badline/vic/sprite/saved_state.rb +64 -0
  132. data/lib/badline/vic/sprite.rb +2 -0
  133. data/lib/badline/vic/sprites.rb +3 -0
  134. data/lib/badline/vic/sprites_state.rb +57 -0
  135. data/lib/badline/vic.rb +94 -10
  136. data/lib/badline.rb +1 -0
  137. metadata +37 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c6abfa72fe3a5e5f8e88efe9d53e04b4e6eaeb355550ef5ac23c47c1a0916fc
4
- data.tar.gz: e15d996a7121eb244ba6ad7a8870694389f59ab8139977b2cf7b9d344f35bbe7
3
+ metadata.gz: b238dd5194dd5fc7783779eced6e4b718b73890e81d78566d12c26c08a379597
4
+ data.tar.gz: 4a47225b0d7e2a055674ce9f4f61cd2bc1064413a2c4c8b6bf228ed99f4dde07
5
5
  SHA512:
6
- metadata.gz: d6fdecfc6b65d6b67d296fafb91e1cbdad7920e0326f146c5bae4bfed08fdd3fad9b4b38007a7f3bdb6a859ff5f1428ff7d94f573e13b68dfdb2e31c5c15e9fe
7
- data.tar.gz: 6843561cca4725f4d6a93cff613ea9b0a9db78d1d45aebee9c4ba7573c49205b0162c4431d4cb78b81e12ded0e84388866017e88b94e065790934e2aaa0c4e8e
6
+ metadata.gz: c636911838f2d65ae88c6b4e940085790c54573c2cc6a9d97450b68f2319eb374f3330e75243487bd7538b56d5cb695f2fe085739ead20db06eb3f24b9169c45
7
+ data.tar.gz: c48b8ecfe80b51a178a757246adba3ea267901db096ccb74c28dbc6482a9ff1f71193c26b4829e98744bcd6847f5fe3f073ff01275604d8383dd36dd91f50f69
data/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.0](https://github.com/elektronaut/badline/compare/v0.5.1...v0.6.0) (2026-09-29)
4
+
5
+
6
+ ### Features
7
+
8
+ * save and restore the machine in VICE .vsf snapshots ([07c0a26](https://github.com/elektronaut/badline/commit/07c0a26d7e73595d5cd4a5bfa90b633ee01195be))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * forget the drive's orbits after restoring it ([0238d81](https://github.com/elektronaut/badline/commit/0238d81ab12a0b0ff1e841661e10004ab3683cc1))
14
+
15
+ ## [0.5.1](https://github.com/elektronaut/badline/compare/v0.5.0...v0.5.1) (2026-09-29)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * build the native badline with a Spinel that writes .d64 images through the true drive ([97f537a](https://github.com/elektronaut/badline/commit/97f537afe69b10ae044f83ccf4dd018402ebe1be))
21
+
22
+
23
+ ### Performance Improvements
24
+
25
+ * sleep the idle true drive through its timer interrupts ([0482e43](https://github.com/elektronaut/badline/commit/0482e439cfb160ab69027b166b1059ed8fc9dfb3)), closes [#367](https://github.com/elektronaut/badline/issues/367)
26
+
3
27
  ## [0.5.0](https://github.com/elektronaut/badline/compare/v0.4.2...v0.5.0) (2026-09-29)
4
28
 
5
29
 
data/CLAUDE.md CHANGED
@@ -48,6 +48,7 @@ requires only the namespace file.
48
48
  | Wolfgang Lorenz suite | `bin/lorenz` | CPU, CIA, interrupts |
49
49
  | VICE testbench | `bin/testbench <subtree>` | `VICII/`, `CIA/`, `interrupts/`, `CPU/`, cartridges with `--carts`, and the true 1541 drive with `--drive` |
50
50
  | VICE SID testprogs | `bin/sidtests` | SID |
51
+ | Drive scenarios | `bin/drive_scenarios` | The true 1541 running the DOS ROM: save, format, the error channel, idle, write protect and autostart |
51
52
  | CIA offline grids | `bundle exec rspec --tag slow spec/badline/cia` | CIA timers and shift register, against a bare CIA in about 2 min |
52
53
 
53
54
  The CIA offline grids replay Lorenz's `cia1ta` and `cia1tb` sweeps (about
@@ -156,29 +157,29 @@ the rows your change can't reach tell you nothing about it.
156
157
  `ruby --yjit bin/lorenz [image] [start_test] [max_cycles]` (resumes the
157
158
  chain at a named test, and `--stop-after NAME` ends it once that test is
158
159
  done). These take seconds to minutes
159
- - When your change moves rows, re-record only those rows, in the same
160
- change: `rake "regression:record:<suite>[filter,...]"` (quote it, because
161
- zsh globs the brackets). It runs only the matching tests and splices
162
- their rows into the baseline, and every other row keeps its verdict.
160
+ - A pull request's CI is the verdict. It runs every suite but
161
+ `testbench-drive` on the Spinel build, compared row by row against
162
+ `test/baselines/`, and every job is a required check. A row that moved
163
+ fails it. Re-record only those rows, in the same change:
164
+ `rake "regression:record:<suite>[filter,...]"` (quote it, because zsh
165
+ globs the brackets). It runs only the matching tests on CRuby and
166
+ splices their rows into the baseline, and every other row keeps its
167
+ verdict. CI runs the same rows on Spinel, so the two have to agree.
163
168
  `lorenz` chains itself, so it takes a stretch of the chain instead:
164
169
  `rake "regression:record:lorenz[first,last]"` resumes just ahead of
165
170
  `first`, stops after `last` and leaves the `(suite)` row alone.
166
171
  `[first,(suite)]` runs on to the end of the chain and records the
167
- `(suite)` row too. Cover every test your change can reach, and explain
168
- every moved row. Never re-record just to make a diff go away
169
- - CI's nightly run owns the whole-suite verdict. The Regression workflow
170
- runs `testbench`, `lorenz` and `sid` against `main` each night, and skips
171
- the night when nothing relevant has changed since the last successful
172
- nightly run. It also runs from the Actions tab on demand. It never runs
173
- on push or on pull requests, so one verdict can cover a day's merges.
174
- The `testbench-*` and `sid-8580` suites run from the Actions tab on
175
- demand. The Spinel workflow runs the Spinel suites on pull requests
176
- that touch emulation, harness or build paths. Its jobs aren't required
177
- checks yet, and the CRuby Regression nightly runs as before
178
- - The one exception is a change whose reach you can't bound to a set of
179
- filters, such as reordering `Computer#cycle!` or changing the LOAD trap
180
- every suite loads through. Ask before running a full suite for it, and
181
- don't start one on your own judgement
172
+ `(suite)` row too. Explain every moved row in the pull request. Never
173
+ re-record just to make a diff go away
174
+ - For a change whose reach you can't bound to a set of filters, such as
175
+ reordering `Computer#cycle!` or changing the LOAD trap every suite loads
176
+ through, push and let CI run the suites
177
+ - `testbench-drive` needs the true drive, which the Spinel testbench
178
+ harness doesn't have, so CI leaves it out. Run the rows your change can
179
+ reach locally
180
+ - The Regression workflow runs the suites on CRuby, started by hand from
181
+ the Actions tab, for a person checking that CRuby and Spinel agree.
182
+ Agents don't start it
182
183
 
183
184
  Pick the filters from the suites your change can move: VIC → `testbench`,
184
185
  plus `testbench-vicii-new` (`bin/testbench --vicii-new`, the 8565) for
@@ -190,10 +191,9 @@ CPU, interrupts or timing → the matching `testbench-*` suite, plus
190
191
  the 6526A) for anything the interrupt register or the CIA model reaches; cartridge mappers, banking or power-on state →
191
192
  `testbench-carts`, plus `testbench-expansions` for banking; GEO-RAM, +60K,
192
193
  +256K, the REU or the VIC's BA line the REU follows → `testbench-expansions`; the 1541 drive, VIA or IEC bus →
193
- `testbench-drive` (`bin/testbench --drive`); SID → `sid`, plus `sid-8580` for anything the 8580
194
- model reaches (`bin/sidtests --sid 8580`). Lorenz isn't a per-change check:
195
- its full chain runs nightly, and the planner assigns any row it moves. The
196
- exception is code whose rule in `doc/pinned-behaviour.md` names Lorenz
194
+ `testbench-drive` (`bin/testbench --drive`) and `drive-scenarios` (`bin/drive_scenarios <filter>`); SID → `sid`, plus `sid-8580` for anything the 8580
195
+ model reaches (`bin/sidtests --sid 8580`). Leave the Lorenz chain to CI,
196
+ unless your code has a rule in `doc/pinned-behaviour.md` that names Lorenz
197
197
  tests: the interrupt polling, CPU port and CIA timer rules. Run just the
198
198
  tests that rule names, one at a time, with
199
199
  `ruby --yjit bin/lorenz --resume <test> --stop-after <test>`. A stretch
data/CONTRIBUTING.md CHANGED
@@ -85,6 +85,11 @@ bundle exec rubocop
85
85
  ## Tests and commits
86
86
 
87
87
  - Add tests for any behavior you change.
88
+ - CI runs rubocop, the specs and every regression suite on each pull
89
+ request, and fails on any suite row that differs from its baseline in
90
+ `test/baselines/`. When a change moves rows on purpose, re-record just
91
+ those rows as [test/baselines/README.md](test/baselines/README.md)
92
+ describes, and say why in the pull request.
88
93
  - Write commit messages using
89
94
  [Conventional Commits](https://www.conventionalcommits.org). The
90
95
  changelog and releases are generated from them, so the `feat:` and
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  [![Version](https://img.shields.io/gem/v/badline.svg?style=flat)](https://rubygems.org/gems/badline)
2
- [![Build](https://github.com/elektronaut/badline/actions/workflows/build.yml/badge.svg)](https://github.com/elektronaut/badline/actions/workflows/build.yml)
2
+ [![CI](https://github.com/elektronaut/badline/actions/workflows/ci.yml/badge.svg)](https://github.com/elektronaut/badline/actions/workflows/ci.yml)
3
3
 
4
4
  # Badline
5
5
 
@@ -57,6 +57,7 @@ badline game.d64 # Mount a disk image as device 8 and load it
57
57
  badline game.tap # Insert a tape and load it
58
58
  badline game.crt # Attach a cartridge
59
59
  badline tune.sid # Play a SID tune
60
+ badline game.vsf # Restore a snapshot, from badline or VICE
60
61
  badline ~/c64 # Mount a directory as device 8
61
62
  ```
62
63
 
@@ -107,6 +108,7 @@ slows the emulation down.
107
108
  | `.tap` | Inserted in the datasette with PLAY pressed, then `LOAD` and `RUN`. It loads at the speed of a real tape |
108
109
  | `.crt` | The hardware types listed under [Cartridges](#whats-emulated). Other types are rejected |
109
110
  | `.sid` | PSID and RSID tunes, started through a small driver after boot |
111
+ | `.vsf` | A snapshot of a running machine, badline's own or one VICE's x64sc saved. See [Snapshots](#snapshots) |
110
112
  | A directory | Mounted read-write as device 8. It serves the `.prg` and `.p00` files in it and the contents of any `.t64`, and `SAVE` writes a new `.prg` |
111
113
 
112
114
  Apart from a `.g64`, which only a true drive can read, there is no 1541
@@ -139,6 +141,62 @@ and takes out a disk mounted through the traps, so `LOAD` and `SAVE`
139
141
  reach the 1541 too. With the 1541 in device 8, a `.d64` goes in its
140
142
  drive as well.
141
143
 
144
+ ## Snapshots
145
+
146
+ In the window, `F11` saves the whole machine to a new
147
+ `badline-<date>-<time>.vsf` in the working directory, and `F12` goes
148
+ back to the snapshot last saved or opened, in a new machine built as the
149
+ saved one was. Both executables do this, and both open a `.vsf` given as
150
+ the media. A snapshot that fails to open leaves the machine running as
151
+ it was. From Ruby, `computer.save_snapshot(path)` saves,
152
+ `computer.restore_snapshot(path)` takes a machine back to a snapshot,
153
+ and `Badline::Snapshot.load(path)` builds a new machine as the saved one
154
+ was built and restores it. `computer.snapshot` and
155
+ `computer.restore(state)` do the same in memory, without a file. A
156
+ restore that fails leaves the machine as it was.
157
+
158
+ A snapshot holds the machine as it was on the cycle it was saved: the
159
+ chips down to the instruction step and the pixel pipeline, the RAM and
160
+ any +60K or +256K expansion, the cartridge with its RAM and flash, a
161
+ GEO-RAM, a true 1541 with its RAM, its VIAs and the disk under the head,
162
+ a disk or directory mounted through the traps with its open channels,
163
+ and the tape with its place on it. A restored machine runs on exactly
164
+ as the saved one would have. A directory, a tape and a true drive's
165
+ disk image open again from their paths when the snapshot is restored,
166
+ the disk with its tracks as the drive last saw them. A disk image mounted through the traps comes back
167
+ with its contents from the snapshot. What the host holds stays the host's: the keyboard, the
168
+ joysticks, the mouse and paddles, sound, and blocks given to `on_init`
169
+ that hadn't run yet, which a restore reports. A snapshot only restores
170
+ in the badline version that wrote it, into a machine with the same
171
+ chip models, RAM expansion and REU. `computer.snapshot` holds an REU's
172
+ RAM, registers and transfer too, but a machine with an REU doesn't save
173
+ to a file yet, as badline doesn't write VICE's REU module.
174
+
175
+ Snapshots use VICE's `.vsf` format. badline writes VICE's modules for
176
+ the CPU, RAM and CPU port, both CIAs, the SID and the VIC-II, plus the
177
+ ones x64sc needs to open the file, with nothing attached to the
178
+ cartridge, tape or user ports and no true drive. A `BADLINE` module,
179
+ which VICE skips, holds the whole machine, and badline restores its own
180
+ snapshots from it.
181
+
182
+ x64sc 3.10 opens badline's snapshots, taken at the end of the
183
+ instruction the CPU was in, as long as its VIC-II model matches
184
+ (`-model c64` for the default machine). It keeps its own drives and
185
+ leaves out the cartridge, the expansions and the tape. Snapshots x64sc
186
+ saves open in badline through the same modules: the CPU at its
187
+ instruction boundary, RAM, the CPU port, the CIAs' registers, timers and
188
+ clocks, the SID's registers and reSID voice state, and the VIC-II's
189
+ registers, beam position, counters and colour RAM. The VIC-II's pixel
190
+ pipeline starts empty. badline reads the modules x64sc 3.7 to 3.10
191
+ write, and VICE's development versions' `MAINC64CPU` and `VIC-IISC`. A module version
192
+ it doesn't know is left out, or fails the restore for the CPU and RAM.
193
+ It reports the modules it leaves out, such as the 1541 drives, the
194
+ cartridge, the datasette and the keyboard, and carries on without them.
195
+ An NTSC snapshot fails, as badline runs PAL only. Restored into a
196
+ running machine, a VICE snapshot fails for a machine built another way,
197
+ such as a C64C's snapshot in a C64, and otherwise takes the cartridge
198
+ out and switches the machine off and on.
199
+
142
200
  ## Playing and rendering SID tunes
143
201
 
144
202
  `badline-ruby --headless` plays a `.sid` tune on the host's audio
@@ -1203,8 +1203,10 @@ and each was knocked out: removing it fails the rows named.
1203
1203
  - Spec guard: *steps in from the stop with the first phase after the
1204
1204
  bump* in
1205
1205
  [`mechanism_spec.rb`](../spec/badline/drive1541/mechanism_spec.rb),
1206
- and *lists it through the traps* (`:slow`) in
1207
- [`drive1541_spec.rb`](../spec/badline/drive1541_spec.rb).
1206
+ and the `format/name-and-id`, `format/bam-free` and
1207
+ `format/trap-readable` rows of the `drive-scenarios` suite
1208
+ ([`drive_scenarios.rb`](../test/drive_scenarios.rb)), which read the
1209
+ formatted disk back from its image.
1208
1210
  - The disk turns at 300 rpm whatever bit rate VIA 2's PB5-6 select: a
1209
1211
  turn is 200,000 drive cycles over every track and over a half track
1210
1212
  without data. Each track's bits pass under the head at the rate they
data/exe/badline-ruby CHANGED
@@ -55,7 +55,8 @@ begin
55
55
  disk: { read_only: options.read_only? })
56
56
  rescue Badline::Media::TrueDrive::Error, Badline::Storage::SIDFile::FormatError, Badline::Storage::T64::FormatError,
57
57
  Badline::Storage::TAP::FormatError, Badline::Storage::CRTFile::FormatError,
58
- Badline::Storage::G64Image::FormatError, Badline::Cartridge::UnsupportedTypeError => e
58
+ Badline::Storage::G64Image::FormatError, Badline::Cartridge::UnsupportedTypeError,
59
+ Badline::Snapshot::FormatError => e
59
60
  warn "badline-ruby: #{options.media_path}: #{e.message}"
60
61
  exit 1
61
62
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class AddressBus
5
+ # The bus's state for a snapshot: the CPU port, the RAM, colour RAM and
6
+ # a RAM expansion, the cartridge, the datasette and the chips.
7
+ module SavedState
8
+ # The RAM expansion fitted, or an Unexpanded stand-in.
9
+ attr_reader :ram_expansion
10
+
11
+ # Takes the cartridge out of the expansion port, as a restore does for
12
+ # a state without one.
13
+ def detach_cartridge
14
+ @cartridge = nil
15
+ update_overlays!
16
+ end
17
+
18
+ # The 6510 port's direction and output registers, and the charge left
19
+ # on its floating bits.
20
+ def port_state = [@port_ddr, @port_out, @port_floating]
21
+
22
+ # Sets the port as port_state reads it, as a snapshot restores it,
23
+ # leaving the RAM under $00 and $01 alone.
24
+ def restore_port(ddr, out, floating)
25
+ @port_ddr = ddr
26
+ @port_out = out
27
+ @port_floating = floating
28
+ update_port!
29
+ end
30
+
31
+ # The machine's memory and everything on the bus but the keyboard, the
32
+ # joysticks and the pot devices, which the host holds. A cartridge is
33
+ # already in the port, built from its setup (Computer#load_state).
34
+ def save_state(out)
35
+ out.marker("BUS")
36
+ out.int(@port_ddr).int(@port_out).int(@port_floating)
37
+ @ram.save_state(out)
38
+ @color_ram.save_state(out)
39
+ @ram_expansion.save_state(out)
40
+ @cartridge&.save_state(out)
41
+ @datasette.save_state(out)
42
+ @vic.save_state(out)
43
+ @cia1.save_state(out)
44
+ @cia2.save_state(out)
45
+ @sid.save_state(out)
46
+ end
47
+
48
+ def load_state(input)
49
+ input.marker("BUS")
50
+ ddr = input.int
51
+ port_out = input.int
52
+ floating = input.int
53
+ @ram.load_state(input)
54
+ @color_ram.load_state(input)
55
+ @ram_expansion.load_state(input)
56
+ @cartridge&.load_state(input)
57
+ @datasette.load_state(input)
58
+ @port_ddr = ddr
59
+ @port_out = port_out
60
+ @port_floating = floating
61
+ @io_port.value = port_value
62
+ update_overlays!
63
+ @vic.load_state(input)
64
+ @cia1.load_state(input)
65
+ @cia2.load_state(input)
66
+ @sid.load_state(input)
67
+ end
68
+ end
69
+
70
+ include SavedState
71
+ end
72
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/address_bus/saved_state"
3
4
  require "badline/address_bus/ultimax_pages"
4
5
 
5
6
  module Badline
@@ -38,6 +38,12 @@ module Badline
38
38
 
39
39
  def rate = @sink.rate
40
40
 
41
+ # Plays another machine's SID from here on.
42
+ def sid=(sid)
43
+ @sid = sid
44
+ sid.record(rate: rate)
45
+ end
46
+
41
47
  def muted? = @muted
42
48
 
43
49
  def playing? = @started
@@ -64,6 +64,21 @@ module Badline
64
64
 
65
65
  private
66
66
 
67
+ def save_mapper(out)
68
+ save_windows(out, windows)
69
+ out.int(window_index(@rom, windows)).boolean(@active).boolean(@io2_ram).blob(@ram_data)
70
+ end
71
+
72
+ def load_mapper(input)
73
+ load_windows(input, windows)
74
+ @rom = window_at(input.int, windows)
75
+ @active = input.boolean?
76
+ @io2_ram = input.boolean?
77
+ input.blob_into(@ram_data)
78
+ end
79
+
80
+ def windows = [@through, @isolated, @contended, *@rom_banks, EMPTY_BANK]
81
+
67
82
  def io2_peek(addr)
68
83
  @io2_ram ? @ram_data[IO2_PAGE | (addr & 0xff)] : @rom.peek(addr)
69
84
  end
@@ -33,6 +33,14 @@ module Badline
33
33
 
34
34
  private
35
35
 
36
+ def save_mapper(out)
37
+ save_windows(out, [*@roml_banks, *@romh_banks, EMPTY_BANK])
38
+ end
39
+
40
+ def load_mapper(input)
41
+ load_windows(input, [*@roml_banks, *@romh_banks, EMPTY_BANK])
42
+ end
43
+
36
44
  def select(number, mode)
37
45
  @roml = bank(@roml_banks, number)
38
46
  @romh = bank(@romh_banks, number)
@@ -24,6 +24,14 @@ module Badline
24
24
 
25
25
  private
26
26
 
27
+ def save_mapper(out)
28
+ save_windows(out, [*@banks, EMPTY_BANK])
29
+ end
30
+
31
+ def load_mapper(input)
32
+ load_windows(input, [*@banks, EMPTY_BANK])
33
+ end
34
+
27
35
  def install_chips(chips)
28
36
  @banks = banks_from(chips).first
29
37
  self.mode = :rom8k
@@ -78,9 +78,28 @@ module Badline
78
78
  private
79
79
 
80
80
  def select_bank
81
+ map_bank
82
+ changed!
83
+ end
84
+
85
+ def map_bank
81
86
  @roml = @low_flash.window(@bank * BANK_SIZE)
82
87
  @romh = @high_flash.window(@bank * BANK_SIZE)
83
- changed!
88
+ end
89
+
90
+ def save_mapper(out)
91
+ out.int(@bank).int(@control).blob(@io_ram)
92
+ @low_flash.save_state(out)
93
+ @high_flash.save_state(out)
94
+ end
95
+
96
+ def load_mapper(input)
97
+ @bank = input.int
98
+ @control = input.int
99
+ input.blob_into(@io_ram)
100
+ @low_flash.load_state(input)
101
+ @high_flash.load_state(input)
102
+ map_bank
84
103
  end
85
104
 
86
105
  def apply_control
@@ -56,6 +56,14 @@ module Badline
56
56
 
57
57
  private
58
58
 
59
+ def save_mapper(out)
60
+ out.int(@discharged_at)
61
+ end
62
+
63
+ def load_mapper(input)
64
+ @discharged_at = input.int
65
+ end
66
+
59
67
  def discharge!
60
68
  @discharged_at = @clock ? @clock.call : 0
61
69
  end
@@ -62,6 +62,18 @@ module Badline
62
62
 
63
63
  private
64
64
 
65
+ def save_mapper(out)
66
+ save_windows(out, [*@roml_banks, *@romh_banks, EMPTY_BANK])
67
+ out.boolean(@register_visible).boolean(@phi1_ultimax)
68
+ end
69
+
70
+ def load_mapper(input)
71
+ load_windows(input, [*@roml_banks, *@romh_banks, EMPTY_BANK])
72
+ @io_bank = @roml
73
+ @register_visible = input.boolean?
74
+ @phi1_ultimax = input.boolean?
75
+ end
76
+
65
77
  def select(number)
66
78
  @io_bank = @roml = bank(@roml_banks, number)
67
79
  @romh = bank(@romh_banks, number)
@@ -2,6 +2,37 @@
2
2
 
3
3
  module Badline
4
4
  class Cartridge
5
+ # A Flash chip's state, for a snapshot.
6
+ module FlashState
7
+ STATES = %i[read program_setup autoselect programming erase_window erasing suspended chip_erase
8
+ program_error].freeze
9
+
10
+ # The array, the command state, a timed operation's end on the
11
+ # machine's clock, and the windows handed out so far, which load_state
12
+ # builds anew.
13
+ def save_state(out)
14
+ out.blob(@data).int(STATES.index(@state)).int(STATES.index(@base_state)).int(@cycle).int(@toggle)
15
+ out.optional_int(@done_at).optional_int(@programmed).optional_int(@remaining)
16
+ out.boolean(!@erasing_sectors.nil?)
17
+ out.ints(@erasing_sectors) if @erasing_sectors
18
+ out.ints(@windows.keys).ints(@status_windows.keys)
19
+ end
20
+
21
+ def load_state(input)
22
+ input.blob_into(@data)
23
+ @state = STATES.fetch(input.int).to_sym
24
+ @base_state = STATES.fetch(input.int).to_sym
25
+ @cycle = input.int
26
+ @toggle = input.int
27
+ @done_at = input.optional_int
28
+ @programmed = input.optional_int
29
+ @remaining = input.optional_int
30
+ @erasing_sectors = nil
31
+ @erasing_sectors = input.ints if input.boolean?
32
+ rebuild_windows(input.ints, input.ints)
33
+ end
34
+ end
35
+
5
36
  # An AMD 29F0x0 flash chip, driven through the JEDEC command set in the
6
37
  # AMD datasheets. Each command opens with two unlock cycles, $AA to the
7
38
  # first unlock address and $55 to the second, and a third write picks
@@ -66,6 +97,8 @@ module Badline
66
97
  alias [] peek
67
98
  end
68
99
 
100
+ include FlashState
101
+
69
102
  attr_reader :data, :state
70
103
  attr_writer :clock
71
104
 
@@ -95,6 +128,14 @@ module Badline
95
128
  end
96
129
  end
97
130
 
131
+ # Hands out new windows at the bases given, as #window would have.
132
+ def rebuild_windows(bases, status_bases)
133
+ @windows = {}
134
+ @status_windows = {}
135
+ bases.each { |base| @windows[base] = Window.new(self, base) }
136
+ status_bases.each { |base| @status_windows[base] = StatusWindow.new(self, base) }
137
+ end
138
+
98
139
  def array_mode?
99
140
  @state == :read || @state == :program_setup
100
141
  end
@@ -28,6 +28,14 @@ module Badline
28
28
 
29
29
  private
30
30
 
31
+ def save_mapper(out)
32
+ save_windows(out, [*@banks, EMPTY_BANK])
33
+ end
34
+
35
+ def load_mapper(input)
36
+ load_windows(input, [*@banks, EMPTY_BANK])
37
+ end
38
+
31
39
  def install_chips(chips)
32
40
  @banks = []
33
41
  chips.each { |chip| @banks[self.class.bank_number(chip.bank)] = rom_bank(chip.data) }
@@ -47,6 +47,18 @@ module Badline
47
47
  changed!
48
48
  end
49
49
 
50
+ def save_mapper(out)
51
+ out.int(@bank).boolean(@flash_writes)
52
+ @flash.save_state(out)
53
+ end
54
+
55
+ def load_mapper(input)
56
+ @bank = input.int
57
+ @flash_writes = input.boolean?
58
+ @flash.load_state(input)
59
+ @roml = @flash.window(@bank * BANK_SIZE)
60
+ end
61
+
50
62
  def install_chips(chips)
51
63
  data = Array.new(BANKS * BANK_SIZE, 0xff)
52
64
  chips.each do |chip|
@@ -25,6 +25,14 @@ module Badline
25
25
 
26
26
  private
27
27
 
28
+ def save_mapper(out)
29
+ save_windows(out, [*@banks, EMPTY_BANK])
30
+ end
31
+
32
+ def load_mapper(input)
33
+ load_windows(input, [*@banks, EMPTY_BANK])
34
+ end
35
+
28
36
  def select(addr)
29
37
  @roml = bank(@banks, addr & 0x3f)
30
38
  changed!
@@ -52,8 +52,24 @@ module Badline
52
52
  @block = @page = @window = 0
53
53
  end
54
54
 
55
+ # A GEO-RAM is built from its size alone.
56
+ def save_setup(out)
57
+ out.int(GEO_RAM_SETUP).int(@size)
58
+ end
59
+
55
60
  private
56
61
 
62
+ def save_mapper(out)
63
+ out.blob(@ram_data).int(@block).int(@page).int(@window)
64
+ end
65
+
66
+ def load_mapper(input)
67
+ input.blob_into(@ram_data)
68
+ @block = input.int
69
+ @page = input.int
70
+ @window = input.int
71
+ end
72
+
57
73
  def install_chips(_chips)
58
74
  @ram_data = Array.new(@size * 1024, 0)
59
75
  reset
@@ -20,6 +20,14 @@ module Badline
20
20
 
21
21
  private
22
22
 
23
+ def save_mapper(out)
24
+ save_windows(out, [*@banks, EMPTY_BANK])
25
+ end
26
+
27
+ def load_mapper(input)
28
+ load_windows(input, [*@banks, EMPTY_BANK])
29
+ end
30
+
23
31
  def install_chips(chips)
24
32
  @banks = banks_from(chips).first
25
33
  @bank_mask = (bank_mask(@banks) | 0x03) & 0x7f
@@ -19,6 +19,14 @@ module Badline
19
19
 
20
20
  private
21
21
 
22
+ def save_mapper(out)
23
+ save_windows(out, [*@banks, EMPTY_BANK])
24
+ end
25
+
26
+ def load_mapper(input)
27
+ load_windows(input, [*@banks, EMPTY_BANK])
28
+ end
29
+
22
30
  # In 16K mode the selected bank shows through ROMH as well.
23
31
  def select_bank(number)
24
32
  @roml = bank(@banks, number)
@@ -29,6 +29,18 @@ module Badline
29
29
 
30
30
  private
31
31
 
32
+ def save_mapper(out)
33
+ save_windows(out, windows)
34
+ @ram_banks.each { |ram| out.blob(ram.data) }
35
+ end
36
+
37
+ def load_mapper(input)
38
+ load_windows(input, windows)
39
+ @ram_banks.each { |ram| input.blob_into(ram.data) }
40
+ end
41
+
42
+ def windows = [*@roml_banks, *@romh_banks, *@ram_banks, @open_bus || EMPTY_BANK, EMPTY_BANK]
43
+
32
44
  def select(value)
33
45
  half = (value >> 1) & 0x01
34
46
  chip = (value >> 2) & 0x03