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
@@ -17,6 +17,16 @@ character.rom
17
17
  -------------
18
18
  The character generator ROM.
19
19
 
20
+ kernal-sx64.rom
21
+ ---------------
22
+ The SX-64's KERNAL, 251104-04. It boots with the SX-64's banner and
23
+ colours, and loads from device 8 rather than tape. Computer.new(kernal:
24
+ :sx64) fits it, as --model sx64 does.
25
+
26
+ Downloaded from
27
+ https://www.zimmers.net/anonftp/pub/cbm/firmware/computers/c64/kernal.sx.251104-04.bin
28
+ SHA-1 aa136e91ecf3c5ac64f696b3dbcbfc5ba0871c98.
29
+
20
30
  dos1541.rom
21
31
  -----------
22
32
  The 1541 disk drive's DOS 2.6, 16 KB for $C000-$FFFF: 325302-01
Binary file
@@ -22,6 +22,19 @@ module Badline
22
22
  # Cycles until the current window closes, counting the next one as 1.
23
23
  attr_reader :cycles_to_close
24
24
 
25
+ # How far the current window has got. The clock and the rate are the
26
+ # host's.
27
+ def save_state(out)
28
+ out.int(@phase).int(@sum).int(@count).int(@cycles_to_close)
29
+ end
30
+
31
+ def load_state(input)
32
+ @phase = input.int
33
+ @sum = input.int
34
+ @count = input.int
35
+ @cycles_to_close = input.int
36
+ end
37
+
25
38
  # Returns the averaged sample when these cycles close a window, and
26
39
  # nil otherwise. They must not run past the window's end.
27
40
  def push(sample, cycles = 1)
@@ -101,10 +101,13 @@ module Badline
101
101
  end
102
102
 
103
103
  # Takes the input in 1/2^Filter::FRACTION units, and reads the
104
- # output out in whole ones.
104
+ # output out in whole ones. A step of several cycles moves each pole
105
+ # at most onto its input.
105
106
  def cycle!(input, cycles = 1)
106
- delta_lowpass = ((W0_LOWPASS >> 8) * cycles * ((input << INPUT_SHIFT) - @lowpass)) >> 12
107
- delta_highpass = (W0_HIGHPASS * cycles * (@lowpass - @highpass)) >> 20
107
+ w0_lowpass = [(W0_LOWPASS >> 8) * cycles, 1 << 12].min
108
+ w0_highpass = [W0_HIGHPASS * cycles, 1 << 20].min
109
+ delta_lowpass = (w0_lowpass * ((input << INPUT_SHIFT) - @lowpass)) >> 12
110
+ delta_highpass = (w0_highpass * (@lowpass - @highpass)) >> 20
108
111
  @output = (@lowpass - @highpass) >> FRACTION
109
112
  @lowpass += delta_lowpass
110
113
  @highpass += delta_highpass
data/lib/badline/sid.rb CHANGED
@@ -127,6 +127,25 @@ module Badline
127
127
  @samples = []
128
128
  end
129
129
 
130
+ # How far the recording has got into the sample it is averaging, for
131
+ # a host that carries on recording from a saved state: it calls
132
+ # #record, then #load_recording.
133
+ def save_recording(out)
134
+ decimator = @decimator
135
+ raise ArgumentError, "the SID isn't recording" if decimator.nil?
136
+
137
+ out.marker("RECORDING")
138
+ decimator.save_state(out)
139
+ end
140
+
141
+ def load_recording(input)
142
+ decimator = @decimator
143
+ raise ArgumentError, "the SID isn't recording" if decimator.nil?
144
+
145
+ input.marker("RECORDING")
146
+ decimator.load_state(input)
147
+ end
148
+
130
149
  # Also keeps each voice's own output, before the filter and the volume,
131
150
  # as each recorded sample closes, for #drain_voice_samples.
132
151
  def record_voices!
@@ -38,6 +38,7 @@ module Badline
38
38
  def qword = take(8).unpack1("Q<").to_i
39
39
  def flag? = !byte.zero?
40
40
  def bytes(count) = take(count).bytes
41
+ def string(count) = take(count)
41
42
  def skip(count) = tap { take(count) }
42
43
 
43
44
  private
@@ -3,27 +3,32 @@
3
3
  module Badline
4
4
  module Snapshot
5
5
  # How a machine was built: its VIC, CIA and SID models, its region, its
6
- # RAM expansion and the size of its REU, the arguments Computer.new
7
- # takes. A State starts with them, so a machine to restore it into can
6
+ # RAM expansion, the size of its REU, its KERNAL and whether it has a
7
+ # datasette, the arguments Computer.new takes. A State starts with them, so a machine to restore it into can
8
8
  # be built first.
9
- Setup = Data.define(:vic_model, :cia_model, :sid_model, :region, :ram_expansion, :reu)
9
+ Setup = Data.define(:vic_model, :cia_model, :sid_model, :region, :ram_expansion, :reu, :kernal, :datasette)
10
10
 
11
11
  class Setup
12
12
  SID_MODELS = %i[mos6581 mos8580].freeze
13
- REGIONS = [Region::PAL, Region::NTSC].freeze
13
+ REGIONS = [Region::PAL, Region::NTSC, Region::NTSC_OLD, Region::DREAN].freeze
14
14
  RAM_EXPANSIONS = [:none, *RAMExpansion::TYPES.keys].freeze
15
+ KERNALS = AddressBus::ROMs::KERNALS.keys.freeze
15
16
 
16
17
  def self.of(address_bus)
17
18
  new(vic_model: address_bus.vic.model, cia_model: address_bus.cia1.model, sid_model: address_bus.sid.model,
18
- region: address_bus.region, ram_expansion: address_bus.ram_expansion.type, reu: address_bus.reu&.size_kb)
19
+ region: address_bus.region, ram_expansion: address_bus.ram_expansion.type, reu: address_bus.reu&.size_kb,
20
+ kernal: address_bus.kernal, datasette: address_bus.datasette.connected?)
19
21
  end
20
22
 
23
+ # Layout 3 has no KERNAL or datasette, and stands for the C64's.
21
24
  def self.read(input)
22
25
  new(vic_model: VIC::MODELS.fetch(input.int), cia_model: CIA::MODELS.fetch(input.int),
23
26
  sid_model: SID_MODELS.fetch(input.int), region: REGIONS.fetch(input.int),
24
- ram_expansion: ram_expansion(RAM_EXPANSIONS.fetch(input.int)), reu: reu(input.int))
27
+ ram_expansion: ram_expansion(RAM_EXPANSIONS.fetch(input.int)), reu: reu(input.int),
28
+ kernal: input.schema > 3 ? KERNALS.fetch(input.int) : :c64,
29
+ datasette: input.schema > 3 ? input.boolean? : true)
25
30
  rescue IndexError
26
- raise FormatError, "the state names a chip model, region or RAM expansion badline doesn't know"
31
+ raise FormatError, "the state names a chip model, region, RAM expansion or KERNAL badline doesn't know"
27
32
  end
28
33
 
29
34
  def self.ram_expansion(name) = name == :none ? nil : name
@@ -40,17 +45,24 @@ module Badline
40
45
  out.int(VIC::MODELS.index(vic_model)).int(CIA::MODELS.index(cia_model))
41
46
  out.int(SID_MODELS.index(sid_model)).int(REGIONS.index(region))
42
47
  out.int(RAM_EXPANSIONS.index(ram_expansion || :none)).int(reu || 0)
48
+ out.int(KERNALS.index(kernal)).boolean(datasette)
43
49
  end
44
50
 
45
51
  # A machine built this way, at power-on.
46
52
  def build
47
- Computer.new(vic_model:, cia_model:, sid_model:, region:, ram_expansion:, reu:)
53
+ Computer.new(vic_model:, cia_model:, sid_model:, region:, ram_expansion:, reu:, kernal:, datasette:)
48
54
  end
49
55
 
56
+ # The chips, led by the model's name when they make one of
57
+ # Badline::Model's.
50
58
  def to_s
59
+ model = Model.of(self)
51
60
  parts = [vic_model, cia_model, sid_model, region.name]
61
+ parts.unshift(model.name) if model
52
62
  parts << ram_expansion if ram_expansion
53
63
  parts << "a #{reu}K REU" if reu
64
+ parts << "the #{kernal} KERNAL" unless kernal == :c64
65
+ parts << "no datasette" unless datasette
54
66
  parts.join(", ")
55
67
  end
56
68
  end
@@ -13,8 +13,10 @@ module Badline
13
13
  class State
14
14
  # The layout the chips write their fields in. It goes up whenever a
15
15
  # save_state writes something else, so a State in another layout
16
- # fails before anything is read into a machine.
17
- SCHEMA = 2
16
+ # fails before anything is read into a machine. Layout 3, without the
17
+ # KERNAL and datasette in the setup, still reads.
18
+ SCHEMA = 4
19
+ READABLE = [3, SCHEMA].freeze
18
20
 
19
21
  attr_reader :values, :strings
20
22
 
@@ -101,15 +103,19 @@ module Badline
101
103
  # A detached reader restores a machine that opens no host file and
102
104
  # writes none, for a copy the machine runs on its own: a disk image
103
105
  # comes from the bytes the state holds, write-protected, a directory
104
- # or .t64 archive device 8 serves and a tape are left out, and a true
105
- # drive's disk keeps the state's tracks without its image file.
106
+ # device 8 serves is left out, and a true drive's disk keeps the
107
+ # state's tracks without its image file.
106
108
  class StateReader
109
+ # The layout check_stamp found.
110
+ attr_reader :schema
111
+
107
112
  def initialize(state, detached: false)
108
113
  @values = state.values
109
114
  @strings = state.strings
110
115
  @value = 0
111
116
  @string = 0
112
117
  @detached = detached
118
+ @schema = State::SCHEMA
113
119
  end
114
120
 
115
121
  def detached? = @detached
@@ -160,12 +166,13 @@ module Badline
160
166
  raise FormatError, "expected #{name} in the state, found #{found[0, 20].inspect}" unless found == name
161
167
  end
162
168
 
163
- # Fails unless the State was written in this layout by this badline
164
- # version.
169
+ # Fails unless the State was written in a layout this badline reads
170
+ # (State::READABLE) by this badline version.
165
171
  def check_stamp
166
172
  schema = int
167
173
  version = string
168
- return if schema == State::SCHEMA && version == VERSION
174
+ @schema = schema
175
+ return if State::READABLE.include?(schema) && version == VERSION
169
176
 
170
177
  raise FormatError, "the state was written by badline #{version.inspect} in layout #{schema}, " \
171
178
  "and this is badline #{VERSION}, layout #{State::SCHEMA}"
@@ -45,7 +45,7 @@ module Badline
45
45
  end
46
46
 
47
47
  def interrupts(computer, fields)
48
- irqs = [computer.vic.interrupted?, computer.cia1.interrupted?].count(true)
48
+ irqs = [computer.vic.interrupted?, computer.cia1.interrupted?, computer.reu&.irq? || false].count(true)
49
49
  nmis = computer.cia2.interrupted? ? 1 : 0
50
50
  pending = (irqs.positive? ? IK_IRQ : 0) | (computer.cpu.nmi ? IK_NMI : 0)
51
51
  fields.dword(irqs).dword(nmis).dword(pending)
@@ -9,7 +9,8 @@ module Badline
9
9
  # control ports and the user port. Each is written as a machine with
10
10
  # nothing attached leaves it, with the joysticks released and the
11
11
  # glue logic's VIC bank taken from CIA 2. A cartridge or tape badline
12
- # has attached is carried by the BADLINE module only.
12
+ # has attached is carried by the BADLINE module only, and an REU by
13
+ # REU1764 as well.
13
14
  module Peripherals
14
15
  # The glue logic's VIC bank, and its pending bank change.
15
16
  GLUE = "GLUE"
@@ -29,7 +30,16 @@ module Badline
29
30
 
30
31
  module_function
31
32
 
32
- def cartridge_port = section(*FIXED[0])
33
+ # C64CART 0.1 lists the REU as the one cartridge in the port when
34
+ # there is one, which REU1764 follows: no main slot cartridge, the
35
+ # lines inactive, no freeze or NMI pending, then the REU's id.
36
+ def cartridge_port(computer)
37
+ return section(*FIXED[0]) unless computer.reu
38
+
39
+ fields = FieldWriter.new.byte(1).dword(-1).byte(0).byte(0).dword(0).dword(0).byte(0).byte(0).byte(0)
40
+ fields.qword(-1).qword(-1).zeros(12 + 16).dword(REU1764::CARTRIDGE_ID)
41
+ fields.section("C64CART", 0, 1)
42
+ end
33
43
 
34
44
  def drives = section(*FIXED[1])
35
45
 
@@ -11,7 +11,7 @@ module Badline
11
11
  # MAINCPU and C64MEM hold the machine itself, so a version badline
12
12
  # doesn't read fails the restore; the others are left out and
13
13
  # reported.
14
- READ = [MainCPU::NAME, C64Mem::NAME, "CIA1", "CIA2", SIDRegisters::NAME, SIDExtended::NAME,
14
+ READ = [MainCPU::NAME, C64Mem::NAME, REU1764::NAME, "CIA1", "CIA2", SIDRegisters::NAME, SIDExtended::NAME,
15
15
  VICII::NAME].freeze
16
16
  REQUIRED = [MainCPU::NAME, C64Mem::NAME].freeze
17
17
 
@@ -83,6 +83,7 @@ module Badline
83
83
  case name
84
84
  when MainCPU::NAME then MainCPU.reads?(section)
85
85
  when C64Mem::NAME then C64Mem.reads?(section)
86
+ when REU1764::NAME then REU1764.reads?(section)
86
87
  when SIDRegisters::NAME then SIDRegisters.reads?(section)
87
88
  when SIDExtended::NAME then SIDExtended.reads?(section)
88
89
  when VICII::NAME then VICII.reads?(section)
@@ -94,6 +95,7 @@ module Badline
94
95
  case name
95
96
  when MainCPU::NAME then MainCPU.import(section, computer)
96
97
  when C64Mem::NAME then C64Mem.import(section, computer)
98
+ when REU1764::NAME then REU1764.import(section, computer)
97
99
  when SIDRegisters::NAME then SIDRegisters.import(section, computer)
98
100
  when SIDExtended::NAME then SIDExtended.import(section, computer)
99
101
  when VICII::NAME then VICII.import(section, computer)
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ # REU1764 0.0, VICE's module for an REU of any size, which x64sc
7
+ # writes straight after C64CART when the cartridge port lists the
8
+ # REU: the size in K, the sixteen bytes the REC's registers read
9
+ # from $DF00, and the RAM.
10
+ #
11
+ # Written: the registers and the RAM. VICE saves between transfers,
12
+ # so badline's copy of the machine runs a transfer in flight to its
13
+ # end first (Vice.settled).
14
+ #
15
+ # Read: the registers, as the values the counters run from and
16
+ # start from, and the RAM. The status register's interrupt, end of
17
+ # block and fault bits come back with the IRQ line, and a command
18
+ # waiting for a write to $FF00 stays armed.
19
+ module REU1764
20
+ NAME = "REU1764"
21
+ MAJOR = 0
22
+ MINOR = 0
23
+ # C64CART's id for the REU, VICE's CARTRIDGE_REU.
24
+ CARTRIDGE_ID = -105
25
+
26
+ module_function
27
+
28
+ def export(computer)
29
+ reu = computer.reu
30
+ FieldWriter.new.dword(reu.size_kb).bytes(reu.register_file).bytes(reu.ram_contents)
31
+ .section(NAME, MAJOR, MINOR)
32
+ end
33
+
34
+ def reads?(section) = section.major == MAJOR && section.minor == MINOR
35
+
36
+ # The REU's size in K, which builds the machine.
37
+ def size_kb(section)
38
+ size = FieldReader.new(section).dword
39
+ return size if REU::SIZES_KB.include?(size)
40
+
41
+ raise FormatError, "#{section}: a #{size}K REU, which badline doesn't build"
42
+ end
43
+
44
+ def import(section, computer)
45
+ reu = computer.reu
46
+ fields = FieldReader.new(section)
47
+ fields.skip(4)
48
+ registers = fields.bytes(16)
49
+ reu.restore_ram(fields.string(reu.size_kb * 1024))
50
+ reu.restore_registers(registers)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -25,9 +25,13 @@ module Badline
25
25
  MAJOR = 1
26
26
  MINOR = 3
27
27
  VERSIONS = ["VIC-II 1.3", "VIC-IISC 1.4"].freeze
28
- MODELS = %i[mos6569 mos8565].freeze
28
+ # The VIC model and region each of VICE's model numbers builds: the
29
+ # 6569, the 8565 and the 6569R1 on PAL, the 6567 and the 8562 on
30
+ # NTSC, the 6567R56A on old NTSC and the 6572 on PAL-N.
31
+ VIC_MODELS = %i[mos6569 mos8565 mos6569 mos6569 mos8565 mos6569 mos6569].freeze
32
+ REGIONS = [Region::PAL, Region::PAL, Region::PAL, Region::NTSC, Region::NTSC, Region::NTSC_OLD,
33
+ Region::DREAN].freeze
29
34
  FRAME_WIDTH = 384
30
- FRAME_HEIGHT = 312
31
35
  # The pixel pipeline x64sc keeps between cycles, in bytes.
32
36
  PIPELINE = 16 + 32 + 6 + 32 + 8 + 3 + 24 + 0x2f + 2 + 4
33
37
 
@@ -35,21 +39,61 @@ module Badline
35
39
 
36
40
  def export(computer)
37
41
  vic = computer.vic
38
- fields = FieldWriter.new.byte(MODELS.index(vic.model)).bytes(vic.register_file)
42
+ fields = FieldWriter.new.byte(number(vic)).bytes(vic.register_file)
39
43
  beam(vic, fields)
40
44
  counters(vic, fields)
41
45
  borders(vic, fields)
42
46
  fields.bytes(Array.new(0x400) { |offset| computer.address_bus.color_ram.nibble(0xd800 + offset) })
43
47
  8.times { |n| sprite(vic, n, fields) }
44
48
  pipeline(vic, fields)
45
- fields.dword(vic.rasterline).dword(FRAME_WIDTH).dword(FRAME_HEIGHT).dword(FRAME_WIDTH)
46
- fields.zeros(FRAME_WIDTH * (FRAME_HEIGHT + 4))
49
+ height = frame_height(vic.region)
50
+ fields.dword(drawn_line(vic)).dword(FRAME_WIDTH).dword(height).dword(FRAME_WIDTH)
51
+ fields.zeros(FRAME_WIDTH * (height + 4))
47
52
  fields.section(NAME, MAJOR, MINOR)
48
53
  end
49
54
 
55
+ # VICE has no HMOS chip for old NTSC or PAL-N, so an 8565 there is
56
+ # written as the NMOS one.
57
+ def number(vic)
58
+ hmos = vic.model == :mos8565
59
+ case vic.region.name
60
+ when :ntsc then hmos ? 4 : 3
61
+ when :ntscold then 5
62
+ when :drean then 6
63
+ else hmos ? 1 : 0
64
+ end
65
+ end
66
+
67
+ # x64sc's frame buffer, in lines, which it reads back at the height
68
+ # the module gives: 312 on PAL and PAL-N, 275 on either NTSC.
69
+ def frame_height(region) = region.lines_per_frame == 312 ? 312 : 275
70
+
71
+ # VICE's cycle in the line is badline's column plus one, so
72
+ # badline's last column is VICE's cycle 0 of the next line. VICE
73
+ # moves its line on in cycle 0, but line 0 a cycle later, holding
74
+ # the last line's number in cycle 0 with the start of frame flag
75
+ # set.
76
+ def last_column?(vic) = vic.column + 1 == vic.region.cycles_per_line
77
+
78
+ def vice_cycle(vic) = last_column?(vic) ? 0 : vic.column + 1
79
+
80
+ def frame_start?(vic) = last_column?(vic) && vic.rasterline + 1 == vic.region.lines_per_frame
81
+
82
+ def vice_line(vic)
83
+ line = vic.rasterline
84
+ last_column?(vic) && !frame_start?(vic) ? line + 1 : line
85
+ end
86
+
87
+ # The line VICE's frame buffer is drawing, its line but for the
88
+ # last, where it starts the next frame.
89
+ def drawn_line(vic)
90
+ line = vice_line(vic)
91
+ line >= vic.region.lines_per_frame - 1 ? 0 : line
92
+ end
93
+
50
94
  def beam(vic, fields)
51
95
  registers = vic.registers
52
- fields.dword(vic.column).dword(0).dword(vic.rasterline).byte(0)
96
+ fields.dword(vice_cycle(vic)).dword(0).dword(vice_line(vic)).flag(frame_start?(vic))
53
97
  fields.byte((registers[0x19] & 0x0f) | (vic.interrupted? ? 0x80 : 0))
54
98
  fields.dword(registers.raster_target).flag(vic.latch_bits.anybits?(1))
55
99
  [vic.character_buffer, vic.color_buffer].each { |buffer| fields.bytes(buffer.map { |value| value || 0 }) }
@@ -98,17 +142,19 @@ module Badline
98
142
 
99
143
  def reads?(section) = VERSIONS.include?(section.to_s)
100
144
 
101
- # VICE's model numbers: the 6569, the 8565 and the 6569R1 run PAL;
102
- # the 6567, 8562 and 6567R56A NTSC, and the 6572 PAL-N, which badline
103
- # doesn't run yet.
104
- def model(section)
145
+ # VICE's model number, which a snapshot of a model badline doesn't
146
+ # build fails on.
147
+ def model_number(section)
105
148
  number = FieldReader.new(section).byte
106
- return :mos8565 if number == 1
107
- return :mos6569 if [0, 2].include?(number)
149
+ return number if number < VIC_MODELS.length
108
150
 
109
- raise FormatError, "#{section}: an NTSC or PAL-N VIC-II (VICE model #{number}), which badline doesn't run"
151
+ raise FormatError, "#{section}: VICE's VIC-II model #{number}, which badline doesn't build"
110
152
  end
111
153
 
154
+ def model(section) = VIC_MODELS[model_number(section)]
155
+
156
+ def region(section) = REGIONS[model_number(section)]
157
+
112
158
  def import(section, computer)
113
159
  snapshot = Import.new(FieldReader.new(section))
114
160
  snapshot.apply(computer)
@@ -34,7 +34,7 @@ module Badline
34
34
  @cycle = f.dword
35
35
  f.skip(4)
36
36
  @line = f.dword
37
- f.skip(1)
37
+ @frame_start = f.flag?
38
38
  @irq_status = f.byte
39
39
  f.skip(4)
40
40
  @raster_match = f.flag?
@@ -95,15 +95,26 @@ module Badline
95
95
  # Clocks the VIC alone from the start of the line to the cycle,
96
96
  # so the line's fetches and sprite checks have run.
97
97
  def position(vic)
98
- vic.restore_line(@line)
98
+ region = vic.region
99
+ vic.restore_line(line(region.lines_per_frame))
99
100
  load_display_state(vic)
100
- @cycle.times { vic.cycle! }
101
+ column(region.cycles_per_line).times { vic.cycle! }
101
102
  registers = vic.registers
102
103
  registers.bytes[0x19] = @irq_status & 0x0f
103
104
  registers.bytes[0x1e], registers.bytes[0x1f] = @collisions
104
105
  registers.update_irq_line
105
106
  end
106
107
 
108
+ # badline's column, VICE's cycle less one, with VICE's cycle 0
109
+ # the last column of the line before (VICII.vice_cycle).
110
+ def column(cycles_per_line) = @cycle.zero? ? cycles_per_line - 1 : @cycle - 1
111
+
112
+ def line(lines)
113
+ return @line unless @cycle.zero? && !@frame_start
114
+
115
+ @line.zero? ? lines - 1 : @line - 1
116
+ end
117
+
107
118
  def load_display_state(vic)
108
119
  vic.display_state.restore_counters([@vc_base, @vc, @rc, @vmli], @display, @bad_lines_enabled)
109
120
  end
@@ -6,6 +6,7 @@ require "badline/snapshot/vice/cias"
6
6
  require "badline/snapshot/vice/cias_import"
7
7
  require "badline/snapshot/vice/sid_registers"
8
8
  require "badline/snapshot/vice/sid_extended"
9
+ require "badline/snapshot/vice/reu1764"
9
10
  require "badline/snapshot/vice/peripherals"
10
11
  require "badline/snapshot/vice/vicii"
11
12
  require "badline/snapshot/vice/vicii_import"
@@ -23,36 +24,32 @@ module Badline
23
24
  # boundary: the longest instruction, stretched by the VIC's longest
24
25
  # hold on the bus.
25
26
  BOUNDARY_CYCLES = 100
27
+ # Cycles more it may run for an REU transfer in flight to end: a
28
+ # swap of 64K, two cycles a byte, stretched by the VIC's holds on
29
+ # the bus.
30
+ TRANSFER_CYCLES = 0x40000
26
31
 
27
32
  # The modules a badline snapshot carries for VICE, in x64sc's order.
28
33
  # They describe a copy of the machine restored from `state`, so saving
29
34
  # leaves the machine itself alone. VICE saves between instructions,
30
35
  # and resumes by fetching an opcode at the program counter, so when
31
- # badline's CPU is inside an instruction the copy runs on to its end.
32
- # The copy's SID catches up, so its registers and voices stand as of
33
- # that cycle. badline writes no VICE module for an REU or an NTSC
34
- # VIC-II yet, and fails for a machine with either.
36
+ # badline's CPU is inside an instruction, or an REU transfer holds
37
+ # the bus, the copy runs on to the end of both. The copy's SID
38
+ # catches up, so its registers and voices stand as of that cycle.
35
39
  def self.export(state)
36
- check_exportable(Computer.setup(state))
37
40
  machine = settled(state)
38
- [MainCPU.export(machine), C64Mem.export(machine), Peripherals.cartridge_port, *CIAs.export(machine),
39
- SIDRegisters.export(machine), SIDExtended.export(machine), Peripherals.drives, VICII.export(machine),
40
- Peripherals.glue(machine), *Peripherals.rest]
41
- end
42
-
43
- def self.check_exportable(setup)
44
- raise FormatError, "badline can't save an REU in VICE's terms yet, so can't save this machine" if setup.reu
45
- return if setup.region == Region::PAL
46
-
47
- raise FormatError, "badline can't save an NTSC VIC-II in VICE's terms yet, so can't save this machine"
41
+ sections = [MainCPU.export(machine), C64Mem.export(machine), Peripherals.cartridge_port(machine)]
42
+ sections << REU1764.export(machine) if machine.reu
43
+ sections + [*CIAs.export(machine), SIDRegisters.export(machine), SIDExtended.export(machine),
44
+ Peripherals.drives, VICII.export(machine), Peripherals.glue(machine), *Peripherals.rest]
48
45
  end
49
46
 
50
47
  # The copy is detached from the host's files (Snapshot::StateReader),
51
48
  # so it reads none and its traps write none.
52
49
  def self.settled(state)
53
50
  copy = Computer.restored(state, detached: true)
54
- BOUNDARY_CYCLES.times do
55
- break if copy.cpu.boundary?
51
+ (BOUNDARY_CYCLES + TRANSFER_CYCLES).times do
52
+ break if copy.cpu.boundary? && !copy.reu&.dma?
56
53
 
57
54
  copy.cycle!
58
55
  end
@@ -60,14 +57,19 @@ module Badline
60
57
  copy
61
58
  end
62
59
 
63
- # How the machine the snapshot was saved from was built. VICE doesn't
64
- # save the CIA model; the C64C's 8565 VIC-II goes with the 6526A. An
65
- # NTSC or PAL-N VIC-II fails, as badline runs PAL only.
60
+ # How the machine the snapshot was saved from was built: the VIC
61
+ # model and the region from the VIC-II's model, and an REU from
62
+ # REU1764. VICE doesn't save the CIA model; the 8565 and 8562 go
63
+ # with the 6526A.
66
64
  def self.setup(container)
67
- vic = VICII.find(container) ? VICII.model(VICII.find(container)) : :mos6569
65
+ vic = VICII.find(container)
66
+ model = vic ? VICII.model(vic) : :mos6569
68
67
  sid = container[SIDRegisters::NAME] ? SIDRegisters.model(container[SIDRegisters::NAME]) : :mos6581
69
- Setup.new(vic_model: vic, cia_model: vic == :mos8565 ? :mos6526a : :mos6526, sid_model: sid,
70
- region: Region::PAL, ram_expansion: nil, reu: nil)
68
+ reu = container[REU1764::NAME]
69
+ reu = nil unless reu && REU1764.reads?(reu)
70
+ Setup.new(vic_model: model, cia_model: model == :mos8565 ? :mos6526a : :mos6526, sid_model: sid,
71
+ region: vic ? VICII.region(vic) : Region::PAL, ram_expansion: nil,
72
+ reu: reu ? REU1764.size_kb(reu) : nil, kernal: :c64, datasette: true)
71
73
  end
72
74
  end
73
75
  end
@@ -12,15 +12,18 @@ module Badline
12
12
  TAPE_NAME = 0x28
13
13
  ID = "T64".bytes.freeze
14
14
 
15
- def initialize(path)
15
+ # `bytes` stands in for the host file's contents, as a snapshot
16
+ # restores the archive it holds.
17
+ def initialize(path, bytes: nil)
16
18
  @path = path
17
- @bytes = File.binread(path).bytes
19
+ @bytes = bytes || File.binread(path).bytes
18
20
  raise FormatError, "Missing T64 signature" unless @bytes[0, 3] == SIGNATURE
19
21
  end
20
22
 
21
- # What Storage.reopen needs to open the archive again.
23
+ # What Storage.reopen needs to open the archive again: its path and
24
+ # its bytes.
22
25
  def save_setup(out)
23
- out.int(T64_ARCHIVE).string(::File.expand_path(@path))
26
+ out.int(T64_ARCHIVE).string(::File.expand_path(@path)).blob(@bytes)
24
27
  end
25
28
 
26
29
  def read_file(name, **)
@@ -13,13 +13,14 @@ module Badline
13
13
  HEADER_SIZE = 0x14
14
14
  OVERFLOW = 0x100 * 8
15
15
 
16
- # The file it was read from, expanded, and the offset of the next
17
- # pulse byte.
18
- attr_reader :version, :path
16
+ # The file it was read from, expanded, and the file's bytes.
17
+ attr_reader :version, :path, :bytes
19
18
 
20
- def initialize(path)
19
+ # `bytes` stands in for the host file's contents, as a snapshot
20
+ # restores the tape it holds.
21
+ def initialize(path, bytes: nil)
21
22
  @path = File.expand_path(path)
22
- @bytes = File.binread(path).bytes
23
+ @bytes = bytes || File.binread(path).bytes
23
24
  raise FormatError, "Missing TAP signature" unless @bytes[0, SIGNATURE.length] == SIGNATURE
24
25
 
25
26
  @version = @bytes[0x0c].to_i
@@ -33,6 +34,7 @@ module Badline
33
34
  @pos = HEADER_SIZE
34
35
  end
35
36
 
37
+ # The offset of the next pulse byte.
36
38
  def position = @pos
37
39
 
38
40
  def position=(offset)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Badline
4
4
  module Storage
5
- # Stands in for a directory or archive a detached restore leaves out
5
+ # Stands in for a directory a detached restore leaves out
6
6
  # (Snapshot::StateReader): it finds no files and refuses every write
7
7
  # as a write-protected disk does.
8
8
  class Unavailable
@@ -45,16 +45,16 @@ module Badline
45
45
  T64_ARCHIVE = 4
46
46
 
47
47
  class << self
48
- # Opens the storage a save_setup wrote: the same directory or archive
49
- # by its path, or a disk image with the bytes it held, writes and
50
- # all. A detached reader gets the image write-protected, and
51
- # Unavailable for a directory or archive.
48
+ # Opens the storage a save_setup wrote: the same directory by its
49
+ # path, an archive with the bytes it held, or a disk image with the
50
+ # bytes it held, writes and all. A detached reader gets the image
51
+ # write-protected, and Unavailable for a directory.
52
52
  def reopen(input)
53
53
  kind = input.int
54
54
  path = input.string
55
55
  case kind
56
56
  when HOST_DIRECTORY then input.detached? ? Unavailable.new : HostDirectory.new(path)
57
- when T64_ARCHIVE then input.detached? ? Unavailable.new : T64.new(path)
57
+ when T64_ARCHIVE then T64.new(path, bytes: input.blob)
58
58
  else reopen_image(kind, path, input)
59
59
  end
60
60
  end