badline 0.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +185 -0
- data/CLAUDE.md +217 -0
- data/CODE_OF_CONDUCT.md +92 -0
- data/CONTRIBUTING.md +51 -0
- data/README.md +224 -31
- data/doc/pinned-behaviour.md +1117 -0
- data/exe/badline +22 -2
- data/exe/badline-sid +34 -0
- data/lib/badline/address_bus.rb +96 -25
- data/lib/badline/audio/aiff.rb +36 -0
- data/lib/badline/audio/bare_player.rb +129 -0
- data/lib/badline/audio/cli.rb +133 -0
- data/lib/badline/audio/console.rb +75 -0
- data/lib/badline/audio/jukebox.rb +82 -0
- data/lib/badline/audio/machine_player.rb +57 -0
- data/lib/badline/audio/options.rb +137 -0
- data/lib/badline/audio/pcm_writer.rb +57 -0
- data/lib/badline/audio/playback.rb +108 -0
- data/lib/badline/audio/renderer.rb +85 -0
- data/lib/badline/audio/sdl_sink.rb +102 -0
- data/lib/badline/audio/wav.rb +23 -0
- data/lib/badline/audio.rb +14 -0
- data/lib/badline/cartridge/action_replay.rb +111 -0
- data/lib/badline/cartridge/atomic_power.rb +24 -0
- data/lib/badline/cartridge/bank.rb +81 -0
- data/lib/badline/cartridge/comal80.rb +48 -0
- data/lib/badline/cartridge/dinamic.rb +34 -0
- data/lib/badline/cartridge/easy_flash.rb +122 -0
- data/lib/badline/cartridge/epyx_fastload.rb +71 -0
- data/lib/badline/cartridge/final_cartridge3.rb +77 -0
- data/lib/badline/cartridge/flash.rb +265 -0
- data/lib/badline/cartridge/freezer.rb +31 -0
- data/lib/badline/cartridge/fun_play.rb +38 -0
- data/lib/badline/cartridge/g_mod2.rb +62 -0
- data/lib/badline/cartridge/game_system.rb +40 -0
- data/lib/badline/cartridge/kcs_power.rb +72 -0
- data/lib/badline/cartridge/mach5.rb +34 -0
- data/lib/badline/cartridge/magic_desk.rb +6 -3
- data/lib/badline/cartridge/ocean.rb +11 -12
- data/lib/badline/cartridge/pagefox.rb +56 -0
- data/lib/badline/cartridge/retro_replay.rb +276 -0
- data/lib/badline/cartridge/rex_utility.rb +33 -0
- data/lib/badline/cartridge/rgcd.rb +48 -0
- data/lib/badline/cartridge/simons_basic.rb +40 -0
- data/lib/badline/cartridge/standard.rb +3 -19
- data/lib/badline/cartridge/super_games.rb +36 -0
- data/lib/badline/cartridge/westermann.rb +34 -0
- data/lib/badline/cartridge/zaxxon.rb +50 -0
- data/lib/badline/cartridge.rb +132 -16
- data/lib/badline/cia/interrupt_register.rb +102 -0
- data/lib/badline/cia/serial.rb +219 -0
- data/lib/badline/cia/timer.rb +43 -31
- data/lib/badline/cia.rb +138 -88
- data/lib/badline/color_memory.rb +14 -5
- data/lib/badline/computer.rb +97 -12
- data/lib/badline/control_ports.rb +54 -9
- data/lib/badline/cpu/addressing.rb +131 -0
- data/lib/badline/cpu/microcode.rb +169 -0
- data/lib/badline/cpu/operations.rb +88 -0
- data/lib/badline/cpu/stack_operations.rb +82 -0
- data/lib/badline/cpu.rb +109 -168
- data/lib/badline/cycleable.rb +3 -0
- data/lib/badline/datasette.rb +81 -0
- data/lib/badline/debug_register.rb +28 -0
- data/lib/badline/gui/application.rb +79 -18
- data/lib/badline/gui/gamepads.rb +97 -0
- data/lib/badline/gui/joy_map.rb +15 -7
- data/lib/badline/gui.rb +1 -0
- data/lib/badline/input/mouse1351.rb +41 -0
- data/lib/badline/input/paddles.rb +36 -0
- data/lib/badline/input.rb +4 -0
- data/lib/badline/instruction_set/arithmetic.rb +16 -21
- data/lib/badline/instruction_set/bitwise.rb +17 -22
- data/lib/badline/instruction_set/branch.rb +20 -19
- data/lib/badline/instruction_set/flag.rb +7 -7
- data/lib/badline/instruction_set/illegal.rb +14 -22
- data/lib/badline/instruction_set/inc_dec.rb +8 -10
- data/lib/badline/instruction_set/stack.rb +5 -57
- data/lib/badline/instruction_set/transfer.rb +9 -9
- data/lib/badline/instruction_set.rb +9 -31
- data/lib/badline/interrupts.rb +104 -0
- data/lib/badline/joystick.rb +9 -9
- data/lib/badline/kernal_trap/channel.rb +51 -0
- data/lib/badline/kernal_trap/drive/status.rb +49 -0
- data/lib/badline/kernal_trap/drive.rb +277 -0
- data/lib/badline/kernal_trap/file.rb +5 -25
- data/lib/badline/kernal_trap/load.rb +86 -24
- data/lib/badline/kernal_trap/routine.rb +51 -0
- data/lib/badline/kernal_trap/save.rb +46 -14
- data/lib/badline/kernal_trap/serial.rb +171 -0
- data/lib/badline/kernal_trap.rb +4 -0
- data/lib/badline/keyboard.rb +40 -16
- data/lib/badline/media.rb +48 -10
- data/lib/badline/memory.rb +5 -0
- data/lib/badline/roms/eapi/LICENSE.md +15 -0
- data/lib/badline/roms/eapi/README +41 -0
- data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
- data/lib/badline/sid/decimator.rb +46 -0
- data/lib/badline/sid/envelope.rb +232 -0
- data/lib/badline/sid/filter.rb +213 -0
- data/lib/badline/sid/voice.rb +61 -0
- data/lib/badline/sid/waveform/combined.rb +126 -0
- data/lib/badline/sid/waveform/fast_forward.rb +105 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
- data/lib/badline/sid/waveform.rb +289 -0
- data/lib/badline/sid.rb +274 -7
- data/lib/badline/status.rb +251 -23
- data/lib/badline/storage/crt_file.rb +18 -2
- data/lib/badline/storage/d64_image.rb +6 -0
- data/lib/badline/storage/d71_image.rb +4 -0
- data/lib/badline/storage/d81_image.rb +4 -0
- data/lib/badline/storage/disk_image.rb +99 -13
- data/lib/badline/storage/host_directory.rb +30 -4
- data/lib/badline/storage/sid_file.rb +274 -0
- data/lib/badline/storage/song_lengths.rb +65 -0
- data/lib/badline/storage/t64.rb +67 -0
- data/lib/badline/storage/tap.rb +68 -0
- data/lib/badline/storage.rb +19 -0
- data/lib/badline/time_of_day.rb +105 -52
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/bank.rb +14 -6
- data/lib/badline/vic/border_mask.rb +76 -0
- data/lib/badline/vic/collisions.rb +104 -0
- data/lib/badline/vic/color_patches.rb +33 -0
- data/lib/badline/vic/display_state.rb +92 -29
- data/lib/badline/vic/graphics_mode.rb +78 -53
- data/lib/badline/vic/graphics_shifter.rb +140 -0
- data/lib/badline/vic/register_log.rb +63 -0
- data/lib/badline/vic/registers.rb +3 -0
- data/lib/badline/vic/sequencer.rb +163 -137
- data/lib/badline/vic/sequencer_output.rb +116 -0
- data/lib/badline/vic/sprite/internal_bus.rb +49 -0
- data/lib/badline/vic/sprite/shifter.rb +129 -0
- data/lib/badline/vic/sprite.rb +275 -65
- data/lib/badline/vic/sprites.rb +220 -58
- data/lib/badline/vic.rb +389 -55
- data/lib/badline.rb +5 -0
- data/vendor/.gitkeep +0 -0
- metadata +104 -4
data/lib/badline/cpu.rb
CHANGED
|
@@ -1,35 +1,61 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "badline/cpu/microcode"
|
|
4
|
+
require "badline/cpu/addressing"
|
|
5
|
+
require "badline/cpu/operations"
|
|
6
|
+
require "badline/cpu/stack_operations"
|
|
7
|
+
|
|
3
8
|
module Badline
|
|
4
|
-
class CPU
|
|
5
|
-
STATUS_FLAGS = [:carry, :zero, :interrupt, :decimal, :break, 1,
|
|
6
|
-
:overflow, :negative].freeze
|
|
9
|
+
class CPU
|
|
10
|
+
STATUS_FLAGS = [:carry, :zero, :interrupt, :decimal, :break, 1, :overflow, :negative].freeze
|
|
7
11
|
|
|
8
|
-
class InvalidOpcodeError < StandardError; end
|
|
9
12
|
include IntegerHelper
|
|
10
13
|
include InstructionSet
|
|
14
|
+
include Interrupts
|
|
11
15
|
include Traps
|
|
16
|
+
include Addressing
|
|
17
|
+
include Operations
|
|
18
|
+
include StackOperations
|
|
19
|
+
|
|
20
|
+
attr_reader :memory, :instructions, :boundary_crossed, :cycles, :pending_write
|
|
21
|
+
attr_accessor :program_counter, :stack_pointer, :status, :a, :x, :y, :nmi, :irq
|
|
12
22
|
|
|
13
|
-
|
|
14
|
-
attr_accessor :program_counter, :stack_pointer, :status, :a, :x, :y,
|
|
15
|
-
:nmi, :irq
|
|
23
|
+
alias pending_write? pending_write
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
# +ane_constant+ is ANE's magic constant, which varies from chip to
|
|
26
|
+
# chip. The default is the C64 6510's.
|
|
27
|
+
def initialize(memory = nil, debug: false, ane_constant: 0xef)
|
|
18
28
|
@debug = debug
|
|
29
|
+
@ane_constant = ane_constant
|
|
19
30
|
@memory = memory || Memory.new
|
|
20
|
-
@status =
|
|
31
|
+
@status = CPUStatus.new(STATUS_FLAGS, value: 0b00100000)
|
|
21
32
|
reset_registers
|
|
22
33
|
|
|
23
34
|
@nmi = @irq = false
|
|
35
|
+
@irq_sample = @irq_pending = false
|
|
36
|
+
@nmi_sample = @nmi_pending = false
|
|
37
|
+
@skip_poll = false
|
|
38
|
+
@boundary_crossed = false
|
|
39
|
+
@interrupt = nil
|
|
40
|
+
@brk = false
|
|
41
|
+
@pending_write = false
|
|
42
|
+
@stalled_at = nil
|
|
24
43
|
|
|
44
|
+
@cycles = 0
|
|
25
45
|
@instructions = 0
|
|
26
46
|
@traps = nil
|
|
27
|
-
|
|
47
|
+
end_sequence
|
|
28
48
|
end
|
|
29
49
|
|
|
30
50
|
def reset!
|
|
31
51
|
status.interrupt = true
|
|
32
52
|
reset_registers
|
|
53
|
+
@nmi = false
|
|
54
|
+
@irq_sample = @irq_pending = false
|
|
55
|
+
@nmi_sample = @nmi_pending = false
|
|
56
|
+
@skip_poll = false
|
|
57
|
+
@pending_write = false
|
|
58
|
+
end_sequence
|
|
33
59
|
end
|
|
34
60
|
|
|
35
61
|
def p
|
|
@@ -40,9 +66,37 @@ module Badline
|
|
|
40
66
|
status.value = new_value
|
|
41
67
|
end
|
|
42
68
|
|
|
69
|
+
def cycle!
|
|
70
|
+
poll # the interrupt lines
|
|
71
|
+
|
|
72
|
+
# Run the next step
|
|
73
|
+
index = @index
|
|
74
|
+
@index = index + 1
|
|
75
|
+
send(@plan[index])
|
|
76
|
+
@cycles += 1
|
|
77
|
+
|
|
78
|
+
# Record if the next step writes to memory
|
|
79
|
+
@pending_write = @writes[@index]
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Called instead of #cycle! on a cycle the VIC holds the CPU through
|
|
84
|
+
# BA. Records which cycle the CPU was stalled before, and keeps
|
|
85
|
+
# sampling the interrupt lines.
|
|
86
|
+
def stall!
|
|
87
|
+
@stalled_at = @cycles
|
|
88
|
+
sample_while_stalled
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def jammed?
|
|
92
|
+
@plan.equal?(JAMMED_PLAN)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Runs to the end of the instruction, or until the CPU jams.
|
|
43
96
|
def step!
|
|
44
|
-
|
|
45
|
-
cycle!
|
|
97
|
+
cycle!
|
|
98
|
+
cycle! until @plan.equal?(FETCH_PLAN) || jammed?
|
|
99
|
+
nil
|
|
46
100
|
end
|
|
47
101
|
|
|
48
102
|
def inspect
|
|
@@ -53,66 +107,58 @@ module Badline
|
|
|
53
107
|
|
|
54
108
|
private
|
|
55
109
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def handle_interrupt(vector, brk: false, pre_cycles: 2)
|
|
63
|
-
pre_cycles.times { cycle }
|
|
64
|
-
|
|
65
|
-
pc = program_counter
|
|
66
|
-
pc = (pc + 1) & 0xffff if brk
|
|
110
|
+
# The first cycle of every instruction. Starts an interrupt if one was
|
|
111
|
+
# pending when the previous instruction ended. Otherwise reads the
|
|
112
|
+
# opcode and switches to its plan.
|
|
113
|
+
def op_fetch
|
|
114
|
+
return start_interrupt if @boundary_nmi || @boundary_irq
|
|
67
115
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
@
|
|
75
|
-
|
|
76
|
-
@
|
|
116
|
+
run_traps
|
|
117
|
+
opcode = @memory.peek(@program_counter)
|
|
118
|
+
log(opcode) if @debug
|
|
119
|
+
micro = MICROCODE[opcode]
|
|
120
|
+
@operation = micro.operation
|
|
121
|
+
@plan = micro.plan
|
|
122
|
+
@writes = micro.writes
|
|
123
|
+
@optional_dummy = micro.optional_dummy
|
|
124
|
+
@index = 1
|
|
125
|
+
@boundary_crossed = false
|
|
126
|
+
@program_counter = (@program_counter + 1) & 0xffff
|
|
77
127
|
end
|
|
78
128
|
|
|
79
|
-
def
|
|
80
|
-
@
|
|
81
|
-
|
|
82
|
-
elsif irq && !status.interrupt?
|
|
83
|
-
0xfffe
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
handle_interrupt(@interrupt) if @interrupt
|
|
87
|
-
@interrupt = nil
|
|
88
|
-
@nmi = @irq = false
|
|
129
|
+
def end_instruction
|
|
130
|
+
@instructions += 1
|
|
131
|
+
end_sequence
|
|
89
132
|
end
|
|
90
133
|
|
|
91
|
-
|
|
92
|
-
|
|
134
|
+
# Makes the next cycle an opcode fetch, and saves the pending interrupt
|
|
135
|
+
# flags for op_fetch. The next cycle polls before op_fetch runs, which
|
|
136
|
+
# updates the live flags.
|
|
137
|
+
def end_sequence
|
|
138
|
+
@plan = FETCH_PLAN
|
|
139
|
+
@writes = FETCH_WRITES
|
|
140
|
+
@index = 0
|
|
141
|
+
@boundary_irq = @irq_pending
|
|
142
|
+
@boundary_nmi = @nmi_pending
|
|
93
143
|
end
|
|
94
144
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
145
|
+
# True when the CPU was stalled right before this cycle.
|
|
146
|
+
def stalled_before_this_cycle?
|
|
147
|
+
@stalled_at == @cycles
|
|
98
148
|
end
|
|
99
149
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
150
|
+
# True when the CPU was stalled right before the cycle that preceded
|
|
151
|
+
# this one.
|
|
152
|
+
def stalled_before_previous_cycle?
|
|
153
|
+
@stalled_at == @cycles - 1
|
|
103
154
|
end
|
|
104
155
|
|
|
105
|
-
def
|
|
106
|
-
|
|
156
|
+
def write_byte(addr, value)
|
|
157
|
+
@memory.poke(addr, value)
|
|
107
158
|
end
|
|
108
159
|
|
|
109
|
-
def
|
|
110
|
-
|
|
111
|
-
# JSR fetches its operand high byte late (see Stack#jsr).
|
|
112
|
-
return read_byte(program_counter) if instruction.name == :jsr
|
|
113
|
-
return read_word(program_counter) if instruction.operand_length == 2
|
|
114
|
-
|
|
115
|
-
read_byte(program_counter)
|
|
160
|
+
def stack_address(offset = 0)
|
|
161
|
+
0x0100 | ((@stack_pointer + offset) & 0xff)
|
|
116
162
|
end
|
|
117
163
|
|
|
118
164
|
def reset_registers
|
|
@@ -123,117 +169,12 @@ module Badline
|
|
|
123
169
|
@a = @x = @y = 0x0
|
|
124
170
|
end
|
|
125
171
|
|
|
126
|
-
def
|
|
127
|
-
|
|
128
|
-
when :implied, :immediate
|
|
129
|
-
nil
|
|
130
|
-
when :accumulator
|
|
131
|
-
:accumulator
|
|
132
|
-
when :relative
|
|
133
|
-
(@program_counter + signed_int8(operand) + 1) & 0xffff
|
|
134
|
-
when :zeropage, :absolute
|
|
135
|
-
operand
|
|
136
|
-
when :zeropage_x
|
|
137
|
-
cycle { (operand + @x) & 0xff }
|
|
138
|
-
when :zeropage_y
|
|
139
|
-
cycle { (operand + @y) & 0xff }
|
|
140
|
-
when :absolute_x
|
|
141
|
-
# Do an extra cycle if page boundary is crossed
|
|
142
|
-
@boundary_crossed = high_byte(operand + @x) != high_byte(operand)
|
|
143
|
-
extra_cycle(instruction)
|
|
144
|
-
(operand + @x) & 0xffff
|
|
145
|
-
when :absolute_y
|
|
146
|
-
@boundary_crossed = high_byte(operand + @y) != high_byte(operand)
|
|
147
|
-
# Do an extra cycle if page boundary is crossed
|
|
148
|
-
extra_cycle(instruction)
|
|
149
|
-
(operand + @y) & 0xffff
|
|
150
|
-
when :indirect
|
|
151
|
-
# This is only used for JMP. There's no carry associated, so an
|
|
152
|
-
# indirect jump to $30FF will wrap around on the same page and read
|
|
153
|
-
# from [0x30ff, 0x3000].
|
|
154
|
-
uint16(
|
|
155
|
-
read_byte(operand),
|
|
156
|
-
read_byte(uint16(
|
|
157
|
-
(low_byte(operand) + 1) & 0xff, # Wrap around low byte
|
|
158
|
-
high_byte(operand)
|
|
159
|
-
))
|
|
160
|
-
)
|
|
161
|
-
when :indirect_x
|
|
162
|
-
cycle
|
|
163
|
-
read_zeropage_word(operand + @x)
|
|
164
|
-
when :indirect_y
|
|
165
|
-
value = read_zeropage_word(operand)
|
|
166
|
-
@boundary_crossed = ((value & 0xff) + y) > 0xff
|
|
167
|
-
# Do an extra cycle if page boundary is crossed
|
|
168
|
-
extra_cycle(instruction)
|
|
169
|
-
(value + y) & 0xffff
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def realize_value(instruction, operand, address)
|
|
174
|
-
case instruction.addressing_mode
|
|
175
|
-
when :implied
|
|
176
|
-
raise "Implied value can't be realized"
|
|
177
|
-
when :accumulator
|
|
178
|
-
@a
|
|
179
|
-
when :immediate
|
|
180
|
-
operand
|
|
181
|
-
else
|
|
182
|
-
read_byte(address)
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def stack_address(offset = 0)
|
|
187
|
-
uint16((stack_pointer + offset) & 0xff, 0x01)
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def log(instruction, operand, address)
|
|
191
|
-
return unless @debug
|
|
192
|
-
|
|
193
|
-
pc = (@program_counter - 1) - instruction.operand_length
|
|
172
|
+
def log(opcode)
|
|
173
|
+
instruction = Instruction.find(opcode)
|
|
194
174
|
puts(
|
|
195
|
-
"#{@cycles}: " \
|
|
196
|
-
"
|
|
197
|
-
"#{@instruction.name.upcase} #{@instruction.addressing_mode} " \
|
|
198
|
-
"Operand: #{operand.inspect} Address: #{address.inspect}"
|
|
175
|
+
"#{@cycles}: PC: #{format16(@program_counter)} - " \
|
|
176
|
+
"#{instruction.name.upcase} #{instruction.addressing_mode}"
|
|
199
177
|
)
|
|
200
178
|
end
|
|
201
|
-
|
|
202
|
-
def main_loop
|
|
203
|
-
if nmi || (irq && !status.interrupt?)
|
|
204
|
-
handle_interrupts
|
|
205
|
-
else
|
|
206
|
-
run_traps
|
|
207
|
-
@boundary_crossed = false
|
|
208
|
-
@instruction = read_instruction
|
|
209
|
-
raise InvalidOpcodeError unless @instruction
|
|
210
|
-
|
|
211
|
-
@program_counter = (@program_counter + 1) & 0xffff
|
|
212
|
-
@cycles += 1
|
|
213
|
-
|
|
214
|
-
@operand = operand = read_operand(@instruction)
|
|
215
|
-
@address = address = read_address(@instruction, operand)
|
|
216
|
-
|
|
217
|
-
@program_counter = (@program_counter + @instruction.operand_length) &
|
|
218
|
-
0xffff
|
|
219
|
-
|
|
220
|
-
log(@instruction, operand, address)
|
|
221
|
-
|
|
222
|
-
# Run the instruction; :lazy realizes the value through #resolve
|
|
223
|
-
send(@instruction.name, address, :lazy)
|
|
224
|
-
|
|
225
|
-
@instructions += 1
|
|
226
|
-
@instruction = nil
|
|
227
|
-
end
|
|
228
|
-
Fiber.yield
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
def write_byte(addr, value)
|
|
232
|
-
if addr == :accumulator
|
|
233
|
-
@a = value
|
|
234
|
-
else
|
|
235
|
-
cycle(write: true) { @memory.poke(addr, value) }
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
179
|
end
|
|
239
180
|
end
|
data/lib/badline/cycleable.rb
CHANGED
|
@@ -22,12 +22,15 @@ module Badline
|
|
|
22
22
|
def cycle(write: false)
|
|
23
23
|
@pending_write = write
|
|
24
24
|
Fiber.yield
|
|
25
|
+
poll
|
|
25
26
|
result = yield if block_given?
|
|
26
27
|
@cycles += 1
|
|
27
28
|
@pending_write = false
|
|
28
29
|
result
|
|
29
30
|
end
|
|
30
31
|
|
|
32
|
+
def poll; end
|
|
33
|
+
|
|
31
34
|
def main_loop
|
|
32
35
|
Fiber.yield
|
|
33
36
|
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
# The 1530 datasette. Plays a pulse stream into CIA 1's FLAG pin while the
|
|
5
|
+
# motor runs with a key pressed, and reports the keys through the cassette
|
|
6
|
+
# sense line on the $01 port.
|
|
7
|
+
class Datasette
|
|
8
|
+
attr_reader :tape
|
|
9
|
+
attr_writer :motor
|
|
10
|
+
|
|
11
|
+
def initialize(tape = nil)
|
|
12
|
+
@tape = tape
|
|
13
|
+
@playing = false
|
|
14
|
+
@motor = false
|
|
15
|
+
@countdown = 0
|
|
16
|
+
@flag_handler = nil
|
|
17
|
+
@sense_handler = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Falling edges on the tape read line.
|
|
21
|
+
def on_flag(&handler)
|
|
22
|
+
@flag_handler = handler
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The cassette sense line, which the $01 port reads back.
|
|
26
|
+
def on_sense_change(&handler)
|
|
27
|
+
@sense_handler = handler
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def insert(tape)
|
|
31
|
+
@tape = tape
|
|
32
|
+
rewind
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def eject
|
|
36
|
+
stop!
|
|
37
|
+
@tape = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def rewind
|
|
41
|
+
@tape&.rewind
|
|
42
|
+
@countdown = 0
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def play! = press(true)
|
|
46
|
+
def stop! = press(false)
|
|
47
|
+
|
|
48
|
+
def playing? = @playing
|
|
49
|
+
|
|
50
|
+
# Sense reads low while a key is down on the deck.
|
|
51
|
+
def sense_low? = @playing
|
|
52
|
+
|
|
53
|
+
def motor? = @motor
|
|
54
|
+
|
|
55
|
+
def running? = @playing && @motor && !@tape.nil?
|
|
56
|
+
|
|
57
|
+
def cycle!
|
|
58
|
+
return unless running?
|
|
59
|
+
|
|
60
|
+
@countdown = @tape.next_pulse.to_i if @countdown.zero?
|
|
61
|
+
return if @countdown.zero?
|
|
62
|
+
|
|
63
|
+
@countdown -= 1
|
|
64
|
+
@flag_handler&.call if @countdown.zero?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def inspect
|
|
68
|
+
"#<#{self.class.name} tape=#{@tape ? @tape.class.name : 'none'} " \
|
|
69
|
+
"playing=#{@playing} motor=#{@motor}>"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def press(down)
|
|
75
|
+
return if down == @playing
|
|
76
|
+
|
|
77
|
+
@playing = down
|
|
78
|
+
@sense_handler&.call
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
# The VICE debug cartridge register, mapped at $D7FF on the C64. It is a
|
|
5
|
+
# VICE facility rather than real hardware, enabled there with -debugcart:
|
|
6
|
+
# writing a byte ends the run with that value as the exit code. The VICE
|
|
7
|
+
# test suites write $00 for success and $ff for failure.
|
|
8
|
+
#
|
|
9
|
+
# Wraps the SID's top mirror page, so every other access falls through.
|
|
10
|
+
class DebugRegister
|
|
11
|
+
ADDRESS = 0xd7ff
|
|
12
|
+
|
|
13
|
+
def initialize(sid, &handler)
|
|
14
|
+
@sid = sid
|
|
15
|
+
@handler = handler
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def peek(addr)
|
|
19
|
+
@sid.peek(addr)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def poke(addr, value)
|
|
23
|
+
return @handler.call(value) if addr == ADDRESS
|
|
24
|
+
|
|
25
|
+
@sid.poke(addr, value)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -6,20 +6,42 @@ module Badline
|
|
|
6
6
|
PAL_CLOCK_HZ = 985_248
|
|
7
7
|
TITLE = "Badline"
|
|
8
8
|
TOGGLE_SYM = SDL2::Key::TAB
|
|
9
|
+
REVERSE_MOD = SDL2::Key::Mod::SHIFT
|
|
9
10
|
|
|
10
|
-
SHARED_KEYS = %i[up left cursor_h cursor_v space].freeze
|
|
11
|
+
SHARED_KEYS = %i[up left cursor_h cursor_v space w a s d lshift].freeze
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
# Tab steps through the input modes and shift-Tab back; the title bar
|
|
14
|
+
# names the live one. The pot devices appear once per control port, since
|
|
15
|
+
# games disagree on which one they read.
|
|
16
|
+
MODES = {
|
|
17
|
+
keyboard: nil, joystick: "JOY",
|
|
18
|
+
mouse1: "MOUSE 1", mouse2: "MOUSE 2",
|
|
19
|
+
paddles1: "PADDLE 1", paddles2: "PADDLE 2"
|
|
20
|
+
}.freeze
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
# Both pot devices take their input from the host mouse. Motion turns the
|
|
23
|
+
# paddle knobs or steps the 1351's counters, and the host buttons go to
|
|
24
|
+
# whichever lines the device puts them on.
|
|
25
|
+
POT_DEVICES = {
|
|
26
|
+
mouse1: [Input::Mouse1351, 1], mouse2: [Input::Mouse1351, 2],
|
|
27
|
+
paddles1: [Input::Paddles, 1], paddles2: [Input::Paddles, 2]
|
|
28
|
+
}.freeze
|
|
29
|
+
MOUSE_BUTTONS = { 1 => :left, 3 => :right }.freeze
|
|
30
|
+
|
|
31
|
+
def initialize(media_path: nil, autostart: true, song: nil, sid_model: nil, debug: false)
|
|
32
|
+
@computer = Computer.new(debug:, sid_model: sid_model || Media.sid_model(media_path))
|
|
33
|
+
puts Media.attach(@computer, media_path, autostart:, song:) if media_path
|
|
34
|
+
|
|
35
|
+
@mode = :keyboard
|
|
36
|
+
@pot_device = nil
|
|
17
37
|
@panes = [ScreenPane.new(@computer)]
|
|
18
38
|
@window = Window.new(
|
|
19
39
|
title: TITLE,
|
|
20
40
|
width: canvas_width, height: canvas_height,
|
|
21
41
|
vsync: ENV["NOVSYNC"].nil?
|
|
22
42
|
)
|
|
43
|
+
@gamepads = Gamepads.new(@computer)
|
|
44
|
+
@gamepads.names.each { |name| puts "Gamepad: #{name}" }
|
|
23
45
|
|
|
24
46
|
rate = @window.refresh_rate
|
|
25
47
|
@cycles_per_frame = PAL_CLOCK_HZ / rate
|
|
@@ -30,10 +52,12 @@ module Badline
|
|
|
30
52
|
@running = true
|
|
31
53
|
while @running
|
|
32
54
|
handle_events
|
|
55
|
+
@gamepads.poll
|
|
33
56
|
@cycles_per_frame.times { @computer.cycle! }
|
|
34
57
|
@window.draw(@panes)
|
|
35
58
|
end
|
|
36
59
|
ensure
|
|
60
|
+
@gamepads.close
|
|
37
61
|
puts @computer.cpu.inspect
|
|
38
62
|
end
|
|
39
63
|
|
|
@@ -48,15 +72,22 @@ module Badline
|
|
|
48
72
|
handle_key_down(event)
|
|
49
73
|
when SDL2::Event::KeyUp
|
|
50
74
|
handle_key_up(event)
|
|
75
|
+
when SDL2::Event::MouseMotion
|
|
76
|
+
@pot_device&.move(event.xrel, event.yrel)
|
|
77
|
+
when SDL2::Event::MouseButton
|
|
78
|
+
handle_mouse_button(event)
|
|
79
|
+
when SDL2::Event::ControllerDevice
|
|
80
|
+
@gamepads.rescan
|
|
51
81
|
end
|
|
52
82
|
end
|
|
53
83
|
end
|
|
54
84
|
|
|
55
85
|
def handle_key_down(event)
|
|
56
|
-
if event.sym == TOGGLE_SYM
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
86
|
+
return cycle_mode(event.mod.anybits?(REVERSE_MOD) ? -1 : 1) if event.sym == TOGGLE_SYM
|
|
87
|
+
|
|
88
|
+
port, dir = JoyMap.parse(event) if @mode == :joystick
|
|
89
|
+
if port
|
|
90
|
+
joystick(port).press(dir)
|
|
60
91
|
else
|
|
61
92
|
@computer.keyboard.press(KeyMap.parse(event))
|
|
62
93
|
end
|
|
@@ -65,21 +96,51 @@ module Badline
|
|
|
65
96
|
def handle_key_up(event)
|
|
66
97
|
return if event.sym == TOGGLE_SYM
|
|
67
98
|
|
|
68
|
-
|
|
69
|
-
|
|
99
|
+
port, dir = JoyMap.parse(event) if @mode == :joystick
|
|
100
|
+
if port
|
|
101
|
+
joystick(port).release(dir)
|
|
70
102
|
else
|
|
71
103
|
@computer.keyboard.release(KeyMap.parse(event))
|
|
72
104
|
end
|
|
73
105
|
end
|
|
74
106
|
|
|
75
|
-
def
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
107
|
+
def handle_mouse_button(event)
|
|
108
|
+
button = MOUSE_BUTTONS[event.button]
|
|
109
|
+
return unless button && @pot_device
|
|
110
|
+
|
|
111
|
+
case event
|
|
112
|
+
when SDL2::Event::MouseButtonDown then @pot_device.press(button)
|
|
113
|
+
when SDL2::Event::MouseButtonUp then @pot_device.release(button)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def joystick(port)
|
|
118
|
+
port == 1 ? @computer.joystick1 : @computer.joystick2
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def cycle_mode(step)
|
|
122
|
+
modes = MODES.keys
|
|
123
|
+
@mode = modes[(modes.index(@mode) + step) % modes.size]
|
|
124
|
+
release_inputs
|
|
125
|
+
attach_pot_device
|
|
126
|
+
@window.title = [TITLE, MODES[@mode] && "[#{MODES[@mode]}]"].compact.join(" ")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def attach_pot_device
|
|
130
|
+
device_class, port = POT_DEVICES[@mode]
|
|
131
|
+
@pot_device = device_class&.new
|
|
132
|
+
ports = @computer.control_ports
|
|
133
|
+
ports.device1 = port == 1 ? @pot_device : nil
|
|
134
|
+
ports.device2 = port == 2 ? @pot_device : nil
|
|
135
|
+
SDL2::Mouse.relative_mode = !@pot_device.nil?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def release_inputs
|
|
139
|
+
SHARED_KEYS.each { |key| @computer.keyboard.release(key) }
|
|
140
|
+
Joystick::DIRECTIONS.each_key do |dir|
|
|
141
|
+
@computer.joystick1.release(dir)
|
|
142
|
+
@computer.joystick2.release(dir)
|
|
81
143
|
end
|
|
82
|
-
@window.title = @joystick_mode ? "#{TITLE} [JOY]" : TITLE
|
|
83
144
|
end
|
|
84
145
|
|
|
85
146
|
def canvas_width
|