badline 0.2.0 → 0.2.1

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.
@@ -1,41 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
+ # A register of flag bits. +flags+ names each bit from bit 0 up: a
5
+ # symbol for a flag, or 0 or 1 for a bit that always reads as that. The
6
+ # subclasses below carry the accessors for each register layout.
4
7
  class Status
5
8
  attr_reader :value, :flags, :bitmask, :low_mask, :high_mask
6
9
 
7
- class << self
8
- # Each flag layout gets a subclass of its own, with the flag accessors
9
- # compiled as ordinary methods rather than closures.
10
- def new(flags = [], value: 0x0)
11
- return super unless equal?(Status)
12
-
13
- layout(flags).new(flags, value:)
14
- end
15
-
16
- private
17
-
18
- def layout(flags)
19
- @layouts ||= {}
20
- @layouts[flags] ||= Class.new(Status) { define_accessors(flags) }
21
- end
22
-
23
- def define_accessors(flags)
24
- flags.each_with_index do |name, i|
25
- next unless name.is_a?(Symbol)
26
-
27
- mask = 1 << i
28
- class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
29
- def #{name}=(enabled) # def carry=(enabled)
30
- @value = enabled && enabled != 0 ? @value | #{mask} : @value & #{~mask} # @value = ... | 0x01 : ... & ~0x01
31
- end # end
32
- def #{name}? = @value & #{mask} != 0 # def carry? = @value & 0x01 != 0
33
- def #{name} = @value & #{mask} == 0 ? 0 : 1 # def carry = ... ? 0 : 1
34
- RUBY
35
- end
36
- end
37
- end
38
-
39
10
  def initialize(flags = [], value: 0x0)
40
11
  @flags = flags
41
12
 
@@ -58,4 +29,256 @@ module Badline
58
29
  end
59
30
  end
60
31
  end
32
+
33
+ # The 6510 processor status register.
34
+ class CPUStatus < Status
35
+ def carry=(enabled)
36
+ @value = enabled && enabled != 0 ? @value | 0x01 : @value & ~0x01
37
+ end
38
+
39
+ def carry? = @value & 0x01 != 0
40
+
41
+ def carry = @value.nobits?(0x01) ? 0 : 1
42
+
43
+ def zero=(enabled)
44
+ @value = enabled && enabled != 0 ? @value | 0x02 : @value & ~0x02
45
+ end
46
+
47
+ def zero? = @value & 0x02 != 0
48
+
49
+ def zero = @value.nobits?(0x02) ? 0 : 1
50
+
51
+ def interrupt=(enabled)
52
+ @value = enabled && enabled != 0 ? @value | 0x04 : @value & ~0x04
53
+ end
54
+
55
+ def interrupt? = @value & 0x04 != 0
56
+
57
+ def interrupt = @value.nobits?(0x04) ? 0 : 1
58
+
59
+ def decimal=(enabled)
60
+ @value = enabled && enabled != 0 ? @value | 0x08 : @value & ~0x08
61
+ end
62
+
63
+ def decimal? = @value & 0x08 != 0
64
+
65
+ def decimal = @value.nobits?(0x08) ? 0 : 1
66
+
67
+ def break=(enabled)
68
+ @value = enabled && enabled != 0 ? @value | 0x10 : @value & ~0x10
69
+ end
70
+
71
+ def break? = @value & 0x10 != 0
72
+
73
+ def break = @value.nobits?(0x10) ? 0 : 1
74
+
75
+ def overflow=(enabled)
76
+ @value = enabled && enabled != 0 ? @value | 0x40 : @value & ~0x40
77
+ end
78
+
79
+ def overflow? = @value & 0x40 != 0
80
+
81
+ def overflow = @value.nobits?(0x40) ? 0 : 1
82
+
83
+ def negative=(enabled)
84
+ @value = enabled && enabled != 0 ? @value | 0x80 : @value & ~0x80
85
+ end
86
+
87
+ def negative? = @value & 0x80 != 0
88
+
89
+ def negative = @value.nobits?(0x80) ? 0 : 1
90
+ end
91
+
92
+ # The 6510 I/O port at $01.
93
+ class PortStatus < Status
94
+ def basic=(enabled)
95
+ @value = enabled && enabled != 0 ? @value | 0x01 : @value & ~0x01
96
+ end
97
+
98
+ def basic? = @value & 0x01 != 0
99
+
100
+ def basic = @value.nobits?(0x01) ? 0 : 1
101
+
102
+ def kernal=(enabled)
103
+ @value = enabled && enabled != 0 ? @value | 0x02 : @value & ~0x02
104
+ end
105
+
106
+ def kernal? = @value & 0x02 != 0
107
+
108
+ def kernal = @value.nobits?(0x02) ? 0 : 1
109
+
110
+ def io=(enabled)
111
+ @value = enabled && enabled != 0 ? @value | 0x04 : @value & ~0x04
112
+ end
113
+
114
+ def io? = @value & 0x04 != 0
115
+
116
+ def io = @value.nobits?(0x04) ? 0 : 1
117
+
118
+ def tape_out=(enabled)
119
+ @value = enabled && enabled != 0 ? @value | 0x08 : @value & ~0x08
120
+ end
121
+
122
+ def tape_out? = @value & 0x08 != 0
123
+
124
+ def tape_out = @value.nobits?(0x08) ? 0 : 1
125
+
126
+ def tape_switch=(enabled)
127
+ @value = enabled && enabled != 0 ? @value | 0x10 : @value & ~0x10
128
+ end
129
+
130
+ def tape_switch? = @value & 0x10 != 0
131
+
132
+ def tape_switch = @value.nobits?(0x10) ? 0 : 1
133
+
134
+ def tape_motor=(enabled)
135
+ @value = enabled && enabled != 0 ? @value | 0x20 : @value & ~0x20
136
+ end
137
+
138
+ def tape_motor? = @value & 0x20 != 0
139
+
140
+ def tape_motor = @value.nobits?(0x20) ? 0 : 1
141
+ end
142
+
143
+ # A CIA timer control register, CRA or CRB. Bits 5-7 differ between the two.
144
+ class ControlRegister < Status
145
+ def start=(enabled)
146
+ @value = enabled && enabled != 0 ? @value | 0x01 : @value & ~0x01
147
+ end
148
+
149
+ def start? = @value & 0x01 != 0
150
+
151
+ def start = @value.nobits?(0x01) ? 0 : 1
152
+
153
+ def output=(enabled)
154
+ @value = enabled && enabled != 0 ? @value | 0x02 : @value & ~0x02
155
+ end
156
+
157
+ def output? = @value & 0x02 != 0
158
+
159
+ def output = @value.nobits?(0x02) ? 0 : 1
160
+
161
+ def out_mode=(enabled)
162
+ @value = enabled && enabled != 0 ? @value | 0x04 : @value & ~0x04
163
+ end
164
+
165
+ def out_mode? = @value & 0x04 != 0
166
+
167
+ def out_mode = @value.nobits?(0x04) ? 0 : 1
168
+
169
+ def run_mode=(enabled)
170
+ @value = enabled && enabled != 0 ? @value | 0x08 : @value & ~0x08
171
+ end
172
+
173
+ def run_mode? = @value & 0x08 != 0
174
+
175
+ def run_mode = @value.nobits?(0x08) ? 0 : 1
176
+
177
+ def load=(enabled)
178
+ @value = enabled && enabled != 0 ? @value | 0x10 : @value & ~0x10
179
+ end
180
+
181
+ def load? = @value & 0x10 != 0
182
+
183
+ def load = @value.nobits?(0x10) ? 0 : 1
184
+
185
+ def in_mode=(enabled)
186
+ @value = enabled && enabled != 0 ? @value | 0x20 : @value & ~0x20
187
+ end
188
+
189
+ def in_mode? = @value & 0x20 != 0
190
+
191
+ def in_mode = @value.nobits?(0x20) ? 0 : 1
192
+
193
+ def serial_mode=(enabled)
194
+ @value = enabled && enabled != 0 ? @value | 0x40 : @value & ~0x40
195
+ end
196
+
197
+ def serial_mode? = @value & 0x40 != 0
198
+
199
+ def serial_mode = @value.nobits?(0x40) ? 0 : 1
200
+
201
+ def clock_frequency=(enabled)
202
+ @value = enabled && enabled != 0 ? @value | 0x80 : @value & ~0x80
203
+ end
204
+
205
+ def clock_frequency? = @value & 0x80 != 0
206
+
207
+ def clock_frequency = @value.nobits?(0x80) ? 0 : 1
208
+
209
+ def in_cnt=(enabled)
210
+ @value = enabled && enabled != 0 ? @value | 0x20 : @value & ~0x20
211
+ end
212
+
213
+ def in_cnt? = @value & 0x20 != 0
214
+
215
+ def in_cnt = @value.nobits?(0x20) ? 0 : 1
216
+
217
+ def in_timer_a=(enabled)
218
+ @value = enabled && enabled != 0 ? @value | 0x40 : @value & ~0x40
219
+ end
220
+
221
+ def in_timer_a? = @value & 0x40 != 0
222
+
223
+ def in_timer_a = @value.nobits?(0x40) ? 0 : 1
224
+
225
+ def alarm=(enabled)
226
+ @value = enabled && enabled != 0 ? @value | 0x80 : @value & ~0x80
227
+ end
228
+
229
+ def alarm? = @value & 0x80 != 0
230
+
231
+ def alarm = @value.nobits?(0x80) ? 0 : 1
232
+ end
233
+
234
+ # The CIA interrupt sources, as flags or as their mask.
235
+ class InterruptFlags < Status
236
+ def timer_a=(enabled)
237
+ @value = enabled && enabled != 0 ? @value | 0x01 : @value & ~0x01
238
+ end
239
+
240
+ def timer_a? = @value & 0x01 != 0
241
+
242
+ def timer_a = @value.nobits?(0x01) ? 0 : 1
243
+
244
+ def timer_b=(enabled)
245
+ @value = enabled && enabled != 0 ? @value | 0x02 : @value & ~0x02
246
+ end
247
+
248
+ def timer_b? = @value & 0x02 != 0
249
+
250
+ def timer_b = @value.nobits?(0x02) ? 0 : 1
251
+
252
+ def alarm=(enabled)
253
+ @value = enabled && enabled != 0 ? @value | 0x04 : @value & ~0x04
254
+ end
255
+
256
+ def alarm? = @value & 0x04 != 0
257
+
258
+ def alarm = @value.nobits?(0x04) ? 0 : 1
259
+
260
+ def serial=(enabled)
261
+ @value = enabled && enabled != 0 ? @value | 0x08 : @value & ~0x08
262
+ end
263
+
264
+ def serial? = @value & 0x08 != 0
265
+
266
+ def serial = @value.nobits?(0x08) ? 0 : 1
267
+
268
+ def flag=(enabled)
269
+ @value = enabled && enabled != 0 ? @value | 0x10 : @value & ~0x10
270
+ end
271
+
272
+ def flag? = @value & 0x10 != 0
273
+
274
+ def flag = @value.nobits?(0x10) ? 0 : 1
275
+
276
+ def interrupt=(enabled)
277
+ @value = enabled && enabled != 0 ? @value | 0x80 : @value & ~0x80
278
+ end
279
+
280
+ def interrupt? = @value & 0x80 != 0
281
+
282
+ def interrupt = @value.nobits?(0x80) ? 0 : 1
283
+ end
61
284
  end
@@ -3,8 +3,14 @@
3
3
  module Badline
4
4
  module Storage
5
5
  class D64Image < DiskImage
6
+ # Image sizes with an error table, mapped to its length: 35 tracks
7
+ # (174848 bytes without one), 40 tracks (196608) and 42 tracks (205312)
8
+ ERROR_TABLES = { 175_531 => 683, 197_376 => 768, 206_114 => 802 }.freeze
9
+
6
10
  private
7
11
 
12
+ def error_tables = ERROR_TABLES
13
+
8
14
  def directory_track = 18
9
15
  def directory_sector = 1
10
16
 
@@ -3,8 +3,12 @@
3
3
  module Badline
4
4
  module Storage
5
5
  class D71Image < D64Image
6
+ ERROR_TABLES = { 351_062 => 1366 }.freeze
7
+
6
8
  private
7
9
 
10
+ def error_tables = ERROR_TABLES
11
+
8
12
  def sectors_in(track)
9
13
  track > 35 ? super(track - 35) : super
10
14
  end
@@ -3,8 +3,12 @@
3
3
  module Badline
4
4
  module Storage
5
5
  class D81Image < DiskImage
6
+ ERROR_TABLES = { 822_400 => 3200 }.freeze
7
+
6
8
  private
7
9
 
10
+ def error_tables = ERROR_TABLES
11
+
8
12
  def directory_track = 40
9
13
  def directory_sector = 3
10
14
 
@@ -21,13 +21,27 @@ module Badline
21
21
  # A LOAD reads only PRG files. An OPEN names the type it wants, or
22
22
  # takes the first file of any type with a nil `type`.
23
23
  def read_file(name, type: :prg)
24
- pattern = Storage.matcher(name)
25
- entry = entries.find do |e|
26
- (type.nil? || e[:type] == type) && pattern.match?(e[:name])
27
- end
24
+ entry = find_entry(name, type)
28
25
  read_chain(entry[:track], entry[:sector]) if entry
29
26
  end
30
27
 
28
+ # The first block in a file's chain that the error table marks bad:
29
+ # its DOS error, its track and sector, and how many of the file's
30
+ # bytes come before it. Nil when the whole chain reads cleanly.
31
+ def read_error(name, type: :prg)
32
+ entry = find_entry(name, type)
33
+ return unless @errors && entry
34
+
35
+ offset = 0
36
+ each_sector(entry[:track], entry[:sector]) do |data, track, sector|
37
+ error = block_error(track, sector)
38
+ return { error:, track:, sector:, offset: } if error
39
+
40
+ offset += data[0].zero? ? data[1] - 1 : SECTOR_SIZE - 2
41
+ end
42
+ nil
43
+ end
44
+
31
45
  # Raw block access for the DOS `U1` command. Returns nil for blocks
32
46
  # outside the image's geometry.
33
47
  def read_block(track, sector)
@@ -44,15 +58,30 @@ module Badline
44
58
  DOS_ERRORS[@errors[track_offset(track) + sector]]
45
59
  end
46
60
 
61
+ # The block holding the disk name and ID, at the start of the
62
+ # directory track.
63
+ def header_block = [directory_track, 0]
64
+
65
+ # The directory block a new file's entry goes into: the first with a
66
+ # free slot, or the last one when every slot is taken.
67
+ def new_entry_block
68
+ last = nil
69
+ each_sector(directory_track, directory_sector) do |data, track, sector|
70
+ return [track, sector] if (0...ENTRIES_PER_SECTOR).any? { |i| data[(i * ENTRY_SIZE) + 2].zero? }
71
+
72
+ last = [track, sector]
73
+ end
74
+ last
75
+ end
76
+
47
77
  private
48
78
 
49
- # An error table holds one byte per block, so the image is 257 bytes
50
- # per block rather than 256.
79
+ # An error table holds one byte per block after the last one. The image
80
+ # sizes that carry one are listed exactly, since a 40-track D64 with a
81
+ # table is also a whole number of 256-byte blocks.
51
82
  def split_error_table
52
- return unless (@bytes.length % (SECTOR_SIZE + 1)).zero? &&
53
- !(@bytes.length % SECTOR_SIZE).zero?
54
-
55
- @bytes.pop(@bytes.length / (SECTOR_SIZE + 1))
83
+ blocks = error_tables[@bytes.length]
84
+ @bytes.pop(blocks) if blocks
56
85
  end
57
86
 
58
87
  def block?(track, sector)
@@ -62,9 +91,19 @@ module Badline
62
91
  sector_offset(track, sector) + SECTOR_SIZE <= @bytes.length
63
92
  end
64
93
 
94
+ def find_entry(name, type)
95
+ pattern = Storage.matcher(name)
96
+ entries.find do |e|
97
+ (type.nil? || e[:type] == type) && pattern.match?(e[:name])
98
+ end
99
+ end
100
+
65
101
  def entries
66
- @entries ||= each_sector(directory_track, directory_sector)
67
- .flat_map { |data| parse_entries(data) }
102
+ @entries ||= begin
103
+ list = []
104
+ each_sector(directory_track, directory_sector) { |data| list.concat(parse_entries(data)) }
105
+ list
106
+ end
68
107
  end
69
108
 
70
109
  def parse_entries(data)
@@ -85,19 +124,19 @@ module Badline
85
124
  end
86
125
 
87
126
  def read_chain(track, sector)
88
- each_sector(track, sector).flat_map do |data|
89
- data[0].zero? ? data[2..data[1]] : data[2..]
127
+ bytes = []
128
+ each_sector(track, sector) do |data|
129
+ bytes.concat(data[0].zero? ? data[2..data[1]] : data[2..])
90
130
  end
131
+ bytes
91
132
  end
92
133
 
93
134
  def each_sector(track, sector)
94
- return to_enum(:each_sector, track, sector) unless block_given?
95
-
96
135
  visited = {}
97
136
  while track != 0 && !visited[[track, sector]]
98
137
  visited[[track, sector]] = true
99
138
  data = sector_at(track, sector)
100
- yield data
139
+ yield data, track, sector
101
140
  track, sector = data[0, 2]
102
141
  end
103
142
  end
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Badline
4
4
  module Storage
5
+ # A host directory served as a drive: .prg files by their host name,
6
+ # .p00 and .t64 entries by their embedded names. A host file that can't
7
+ # be read is left out, and a failed write reports false.
5
8
  class HostDirectory
6
9
  def initialize(path)
7
10
  @path = path
@@ -18,6 +21,9 @@ module Badline
18
21
  def write_file(name, bytes)
19
22
  host_name = "#{name.downcase.tr('/', '_')}.prg"
20
23
  File.binwrite(File.join(@path, host_name), bytes.pack("C*"))
24
+ true
25
+ rescue SystemCallError
26
+ false
21
27
  end
22
28
 
23
29
  private
@@ -29,6 +35,8 @@ module Badline
29
35
 
30
36
  def entries
31
37
  Dir.children(@path).sort.flat_map { |f| entry_for(f) }.compact
38
+ rescue SystemCallError
39
+ []
32
40
  end
33
41
 
34
42
  def entry_for(file)
@@ -45,18 +53,22 @@ module Badline
45
53
  def p00_entry(file)
46
54
  header = File.binread(File.join(@path, file), P00::HEADER_SIZE).bytes
47
55
  { name: P00.name(header), file: } if P00.wraps?(header)
56
+ rescue SystemCallError
57
+ nil
48
58
  end
49
59
 
50
60
  def t64_entries(file)
51
61
  archive = T64.new(File.join(@path, file))
52
62
  archive.names.map { |name| { name:, archive: } }
53
- rescue T64::FormatError
63
+ rescue T64::FormatError, SystemCallError
54
64
  nil
55
65
  end
56
66
 
57
67
  def file_bytes(file)
58
68
  bytes = File.binread(File.join(@path, file)).bytes
59
69
  P00.wraps?(bytes) ? P00.data(bytes) : bytes
70
+ rescue SystemCallError
71
+ nil
60
72
  end
61
73
  end
62
74
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest/md5"
3
+ require "digest"
4
4
 
5
5
  module Badline
6
6
  module Storage
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/badline/vic.rb CHANGED
@@ -57,20 +57,33 @@ module Badline
57
57
 
58
58
  @width = 504
59
59
  @height = 312
60
+ @columns_per_line = @width / 8
61
+ @last_line = @height - 1
62
+ @display = Array.new(@width * @height, 0)
63
+ @lines = Array.new(@height) { Array.new(@width, 0) }
64
+ @dirty_lines = Array.new(@height, true)
65
+ @render = true
66
+
67
+ power_on!
68
+ super()
69
+ end
60
70
 
71
+ # The VIC has no reset pin, so only a power cycle brings back the
72
+ # registers, the raster position and the fetch and sprite state it
73
+ # starts with. The display keeps its buffers, cleared to black.
74
+ def power_on!
61
75
  @registers = VIC::Registers.new
62
76
  @register_bytes = @registers.bytes
63
77
  @display_state = VIC::DisplayState.new(@registers)
64
78
  @sequencer = VIC::Sequencer.new(@width, @registers, @vic_bank)
79
+ @sequencer.render = @render
65
80
  @sprites = VIC::Sprites.new(@registers, @vic_bank, @width)
66
- @display = Array.new(@width * @height, 0)
67
- @lines = Array.new(@height) { Array.new(@width, 0) }
68
- @dirty_lines = Array.new(@height, true)
81
+ @display.fill(0)
82
+ @lines.each { |line| line.fill(0) }
83
+ @dirty_lines.fill(true)
69
84
 
70
85
  @column = 0
71
86
  @rasterline = 0
72
- @columns_per_line = @width / 8
73
- @last_line = @height - 1
74
87
 
75
88
  @character_buffer = Array.new(40, 0)
76
89
  @color_buffer = Array.new(40, 0)
@@ -87,9 +100,6 @@ module Badline
87
100
  @lp_triggered = false
88
101
  @lp_low = false
89
102
  @raster_match = false
90
- @render = true
91
-
92
- super()
93
103
  end
94
104
 
95
105
  def cycle!
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: badline
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Inge Jørgensen
@@ -145,6 +145,7 @@ files:
145
145
  - lib/badline/kernal_trap.rb
146
146
  - lib/badline/kernal_trap/channel.rb
147
147
  - lib/badline/kernal_trap/drive.rb
148
+ - lib/badline/kernal_trap/drive/status.rb
148
149
  - lib/badline/kernal_trap/file.rb
149
150
  - lib/badline/kernal_trap/load.rb
150
151
  - lib/badline/kernal_trap/routine.rb