badline 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +167 -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 +88 -23
- 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 +131 -15
- data/lib/badline/cia/interrupt_register.rb +88 -0
- data/lib/badline/cia/serial.rb +219 -0
- data/lib/badline/cia/timer.rb +43 -31
- data/lib/badline/cia.rb +109 -74
- data/lib/badline/color_memory.rb +14 -5
- data/lib/badline/computer.rb +68 -8
- data/lib/badline/control_ports.rb +54 -9
- data/lib/badline/cpu/addressing.rb +131 -0
- data/lib/badline/cpu/microcode.rb +171 -0
- data/lib/badline/cpu/operations.rb +88 -0
- data/lib/badline/cpu/stack_operations.rb +82 -0
- data/lib/badline/cpu.rb +108 -167
- 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 +33 -0
- data/lib/badline/kernal_trap/drive.rb +239 -0
- data/lib/badline/kernal_trap/file.rb +4 -24
- data/lib/badline/kernal_trap/load.rb +2 -2
- data/lib/badline/kernal_trap/routine.rb +34 -0
- data/lib/badline/kernal_trap/save.rb +1 -1
- data/lib/badline/kernal_trap/serial.rb +139 -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/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 +32 -27
- data/lib/badline/storage/crt_file.rb +18 -2
- data/lib/badline/storage/disk_image.rb +52 -5
- data/lib/badline/storage/host_directory.rb +18 -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 +373 -49
- data/lib/badline.rb +5 -0
- data/vendor/.gitkeep +0 -0
- metadata +103 -4
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class CPU
|
|
5
|
+
class InvalidOpcodeError < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Steps that write to memory. The CPU keeps running through BA on
|
|
8
|
+
# cycles that write, so Computer#cycle! asks before each cycle whether
|
|
9
|
+
# the next step is one of these.
|
|
10
|
+
WRITE_STEPS = %i[
|
|
11
|
+
op_mem_write op_rmw_write_original op_rmw_write_result op_push_exec op_jsr_push_high op_jsr_push_low
|
|
12
|
+
op_int_push_pch op_int_push_pcl op_int_push_status
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
# The steps each addressing mode takes to work out the address an
|
|
16
|
+
# instruction operates on. They run after the opcode fetch.
|
|
17
|
+
ADDRESSING_STEPS = {
|
|
18
|
+
zeropage: %i[op_zp],
|
|
19
|
+
zeropage_x: %i[op_zp op_zp_x],
|
|
20
|
+
zeropage_y: %i[op_zp op_zp_y],
|
|
21
|
+
absolute: %i[op_abs_low op_abs_high],
|
|
22
|
+
absolute_x: %i[op_abs_low op_abs_high_x op_idx_dummy],
|
|
23
|
+
absolute_y: %i[op_abs_low op_abs_high_y op_idx_dummy],
|
|
24
|
+
indirect_x: %i[op_zp_pointer op_pointer_x op_pointer_low op_pointer_high],
|
|
25
|
+
indirect_y: %i[op_zp_pointer op_pointer_low op_pointer_high_y op_idx_dummy]
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# The steps that read or write that address, once it is known.
|
|
29
|
+
OPERATION_STEPS = {
|
|
30
|
+
read: %i[op_mem_read],
|
|
31
|
+
write: %i[op_mem_write],
|
|
32
|
+
rmw: %i[op_rmw_read op_rmw_write_original op_rmw_write_result]
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
STORES = %i[sta stx sty sax sha shx shy tas].freeze
|
|
36
|
+
READ_MODIFY_WRITES = %i[asl lsr rol ror inc dec slo sre rla rra isc dcp].freeze
|
|
37
|
+
BRANCHES = %i[bcc bcs beq bmi bne bpl bvc bvs].freeze
|
|
38
|
+
|
|
39
|
+
# The addresses a 6502 reads after a JAM opcode, before it settles into
|
|
40
|
+
# JAMMED_PLAN.
|
|
41
|
+
JAM_ADDRESSES = [0xffff, 0xfffe, 0xfffe].freeze
|
|
42
|
+
|
|
43
|
+
# The plan between instructions: the next cycle fetches an opcode.
|
|
44
|
+
FETCH_PLAN = %i[op_fetch].freeze
|
|
45
|
+
|
|
46
|
+
IMPLIED_PLAN = %i[op_fetch op_implied_exec].freeze
|
|
47
|
+
ACCUMULATOR_PLAN = %i[op_fetch op_accumulator_exec].freeze
|
|
48
|
+
IMMEDIATE_PLAN = %i[op_fetch op_immediate_read].freeze
|
|
49
|
+
|
|
50
|
+
# $10, $30, $50, $70, $90, $B0, $D0, $F0 - relative - 2+ cycles.
|
|
51
|
+
# A branch that is not taken takes 2 cycles. A taken branch takes 3,
|
|
52
|
+
# or 4 when the target is on another page.
|
|
53
|
+
BRANCH_PLAN = %i[op_fetch op_relative op_branch op_branch_fixup].freeze
|
|
54
|
+
|
|
55
|
+
# $4C - absolute - 3 cycles
|
|
56
|
+
JMP_ABSOLUTE_PLAN = %i[op_fetch op_abs_low op_jmp_high].freeze
|
|
57
|
+
|
|
58
|
+
# $6C - indirect - 5 cycles
|
|
59
|
+
JMP_INDIRECT_PLAN = %i[op_fetch op_abs_low op_abs_high op_indirect_low op_indirect_high].freeze
|
|
60
|
+
|
|
61
|
+
# $20 - absolute - 6 cycles
|
|
62
|
+
JSR_PLAN = %i[op_fetch op_jsr_low op_jsr_dummy op_jsr_push_high op_jsr_push_low op_jsr_high].freeze
|
|
63
|
+
|
|
64
|
+
# $60 - implied - 6 cycles
|
|
65
|
+
RTS_PLAN = %i[op_fetch op_implied_dummy op_pull_dummy op_pull_low op_pull_high op_rts_fixup].freeze
|
|
66
|
+
|
|
67
|
+
# $40 - implied - 6 cycles
|
|
68
|
+
RTI_PLAN = %i[op_fetch op_implied_dummy op_pull_dummy op_rti_pull_status op_pull_low op_rti_pull_high].freeze
|
|
69
|
+
|
|
70
|
+
# $08, $48 - implied - 3 cycles
|
|
71
|
+
PUSH_PLAN = %i[op_fetch op_implied_dummy op_push_exec].freeze
|
|
72
|
+
|
|
73
|
+
# $28, $68 - implied - 4 cycles
|
|
74
|
+
PULL_PLAN = %i[op_fetch op_implied_dummy op_pull_dummy op_pull_exec].freeze
|
|
75
|
+
|
|
76
|
+
# $02, $12, $22, $32, $42, $52, $62, $72, $92, $B2, $D2, $F2 - illegal
|
|
77
|
+
JAM_PLAN = [:op_fetch, :op_implied_dummy, *Array.new(JAM_ADDRESSES.length, :op_jam)].freeze
|
|
78
|
+
|
|
79
|
+
# A jammed CPU reads $FFFF on every cycle until reset. It never reaches
|
|
80
|
+
# an instruction boundary, so a pending IRQ or NMI never starts.
|
|
81
|
+
JAMMED_PLAN = %i[op_jammed].freeze
|
|
82
|
+
|
|
83
|
+
# BRK and hardware interrupts push the return address and status, then
|
|
84
|
+
# read the new PC from a vector. Only the second step differs: BRK's
|
|
85
|
+
# selects the IRQ vector and sets the break flag it pushes.
|
|
86
|
+
#
|
|
87
|
+
# $00 - implied - 7 cycles (BRK)
|
|
88
|
+
INTERRUPT_PLAN = %i[
|
|
89
|
+
op_fetch op_implied_dummy op_int_push_pch op_int_push_pcl op_int_push_status
|
|
90
|
+
op_int_vector_low op_int_vector_high
|
|
91
|
+
].freeze
|
|
92
|
+
BRK_PLAN = %i[
|
|
93
|
+
op_fetch op_brk_dummy op_int_push_pch op_int_push_pcl op_int_push_status
|
|
94
|
+
op_int_vector_low op_int_vector_high
|
|
95
|
+
].freeze
|
|
96
|
+
|
|
97
|
+
SPECIAL_PLANS = {
|
|
98
|
+
brk: BRK_PLAN, jam: JAM_PLAN, jsr: JSR_PLAN, rts: RTS_PLAN,
|
|
99
|
+
rti: RTI_PLAN, pha: PUSH_PLAN, php: PUSH_PLAN,
|
|
100
|
+
pla: PULL_PLAN, plp: PULL_PLAN
|
|
101
|
+
}.freeze
|
|
102
|
+
|
|
103
|
+
# Everything the CPU needs to run one opcode:
|
|
104
|
+
# - +operation+: the instruction method to call, such as :lda
|
|
105
|
+
# - +plan+: the step to run on each cycle
|
|
106
|
+
# - +writes+: which of those steps write to memory
|
|
107
|
+
# - +optional_dummy+: true for indexed reads, which skip the extra
|
|
108
|
+
# cycle unless indexing crosses a page
|
|
109
|
+
Microcode = Struct.new(:operation, :plan, :writes, :optional_dummy) do
|
|
110
|
+
class << self
|
|
111
|
+
def table
|
|
112
|
+
Array.new(256) do |opcode|
|
|
113
|
+
instruction = Instruction.find(opcode)
|
|
114
|
+
raise InvalidOpcodeError, format("$%02x", opcode) unless instruction
|
|
115
|
+
|
|
116
|
+
build(instruction)
|
|
117
|
+
end.freeze
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def write_mask(plan)
|
|
121
|
+
plan.map { |step| WRITE_STEPS.include?(step) }.freeze
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def build(instruction)
|
|
127
|
+
plan = plan_for(instruction)
|
|
128
|
+
new(instruction.name, plan, write_mask(plan),
|
|
129
|
+
instruction.boundary_cycle?)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def plan_for(instruction)
|
|
133
|
+
case instruction.name
|
|
134
|
+
when :jmp
|
|
135
|
+
if instruction.addressing_mode == :indirect
|
|
136
|
+
JMP_INDIRECT_PLAN
|
|
137
|
+
else
|
|
138
|
+
JMP_ABSOLUTE_PLAN
|
|
139
|
+
end
|
|
140
|
+
when *BRANCHES then BRANCH_PLAN
|
|
141
|
+
else SPECIAL_PLANS[instruction.name] || addressed_plan(instruction)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def addressed_plan(instruction)
|
|
146
|
+
mode = instruction.addressing_mode
|
|
147
|
+
case mode
|
|
148
|
+
when :implied then IMPLIED_PLAN
|
|
149
|
+
when :accumulator then ACCUMULATOR_PLAN
|
|
150
|
+
when :immediate then IMMEDIATE_PLAN
|
|
151
|
+
else
|
|
152
|
+
[:op_fetch, *ADDRESSING_STEPS.fetch(mode),
|
|
153
|
+
*OPERATION_STEPS.fetch(operation_kind(instruction.name))].freeze
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def operation_kind(name)
|
|
158
|
+
return :write if STORES.include?(name)
|
|
159
|
+
return :rmw if READ_MODIFY_WRITES.include?(name)
|
|
160
|
+
|
|
161
|
+
:read
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
FETCH_WRITES = Microcode.write_mask(FETCH_PLAN)
|
|
167
|
+
INTERRUPT_WRITES = Microcode.write_mask(INTERRUPT_PLAN)
|
|
168
|
+
JAMMED_WRITES = Microcode.write_mask(JAMMED_PLAN)
|
|
169
|
+
MICROCODE = Microcode.table
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class CPU
|
|
5
|
+
# Steps that read or write an instruction's operand once its address is
|
|
6
|
+
# known, plus the branch and JAM steps.
|
|
7
|
+
module Operations
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def op_mem_read
|
|
11
|
+
send(@operation, @address, @memory.peek(@address))
|
|
12
|
+
end_instruction
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def op_mem_write
|
|
16
|
+
send(@operation, @address, nil)
|
|
17
|
+
end_instruction
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Read-modify-write instructions (ASL, INC and the like) take three
|
|
21
|
+
# cycles here: read the value and compute the result, write the
|
|
22
|
+
# original value back, then write the result. The instruction method
|
|
23
|
+
# hands the result over through #write_modified.
|
|
24
|
+
def op_rmw_read
|
|
25
|
+
@value = @memory.peek(@address)
|
|
26
|
+
send(@operation, @address, @value)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def op_rmw_write_original
|
|
30
|
+
@memory.poke(@address, @value)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def op_rmw_write_result
|
|
34
|
+
@memory.poke(@address, @rmw_result)
|
|
35
|
+
end_instruction
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def op_relative
|
|
39
|
+
offset = @memory.peek(@program_counter)
|
|
40
|
+
@program_counter = (@program_counter + 1) & 0xffff
|
|
41
|
+
@address = (@program_counter + signed_int8(offset)) & 0xffff
|
|
42
|
+
@branch_taken = false
|
|
43
|
+
send(@operation, @address, nil)
|
|
44
|
+
if @branch_taken
|
|
45
|
+
# A taken branch within the same page doesn't poll on its last cycle.
|
|
46
|
+
@skip_poll = true if same_page?
|
|
47
|
+
else
|
|
48
|
+
end_instruction
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def op_branch
|
|
53
|
+
@memory.peek(@program_counter)
|
|
54
|
+
return unless same_page?
|
|
55
|
+
|
|
56
|
+
@program_counter = @address
|
|
57
|
+
end_instruction
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A branch to another page spends an extra cycle reading from the target's low byte on the old page.
|
|
61
|
+
def op_branch_fixup
|
|
62
|
+
@memory.peek((@program_counter & 0xff00) | (@address & 0xff))
|
|
63
|
+
@program_counter = @address
|
|
64
|
+
end_instruction
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def same_page?
|
|
68
|
+
@address >> 8 == @program_counter >> 8
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Runs once for each address in JAM_ADDRESSES. By the time a step runs, @index already points at the next one.
|
|
72
|
+
def op_jam
|
|
73
|
+
position = @index - 2
|
|
74
|
+
@memory.peek(JAM_ADDRESSES[position - 1])
|
|
75
|
+
return unless position == JAM_ADDRESSES.length
|
|
76
|
+
|
|
77
|
+
@plan = JAMMED_PLAN
|
|
78
|
+
@writes = JAMMED_WRITES
|
|
79
|
+
@index = 0
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def op_jammed
|
|
83
|
+
@index = 0
|
|
84
|
+
@memory.peek(0xffff)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class CPU
|
|
5
|
+
# Steps that use the stack: PHA, PHP, PLA, PLP, JSR, RTS and RTI.
|
|
6
|
+
module StackOperations
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def op_push_exec
|
|
10
|
+
send(@operation, nil, nil)
|
|
11
|
+
end_instruction
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def op_pull_dummy
|
|
15
|
+
@memory.peek(stack_address)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def op_pull_exec
|
|
19
|
+
send(@operation, nil, nil)
|
|
20
|
+
end_instruction
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def op_pull_low
|
|
24
|
+
@stack_pointer = (@stack_pointer + 1) & 0xff
|
|
25
|
+
@address = @memory.peek(stack_address)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def op_pull_high
|
|
29
|
+
@stack_pointer = (@stack_pointer + 1) & 0xff
|
|
30
|
+
@address |= @memory.peek(stack_address) << 8
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def op_rts_fixup
|
|
34
|
+
@memory.peek(@address)
|
|
35
|
+
@program_counter = (@address + 1) & 0xffff
|
|
36
|
+
end_instruction
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def op_rti_pull_status
|
|
40
|
+
@stack_pointer = (@stack_pointer + 1) & 0xff
|
|
41
|
+
@status.value = @memory.peek(stack_address)
|
|
42
|
+
@status.break = false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def op_rti_pull_high
|
|
46
|
+
@stack_pointer = (@stack_pointer + 1) & 0xff
|
|
47
|
+
@program_counter = @address | (@memory.peek(stack_address) << 8)
|
|
48
|
+
end_instruction
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def op_jsr_low
|
|
52
|
+
@address = @memory.peek(@program_counter)
|
|
53
|
+
@program_counter = (@program_counter + 2) & 0xffff
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def op_jsr_dummy
|
|
57
|
+
@memory.peek(stack_address)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def op_jsr_push_high
|
|
61
|
+
@memory.poke(stack_address, high_byte(return_address))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def op_jsr_push_low
|
|
65
|
+
@memory.poke(stack_address(-1), low_byte(return_address))
|
|
66
|
+
@stack_pointer = (@stack_pointer - 2) & 0xff
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# JSR reads its target's high byte after pushing the return address.
|
|
70
|
+
# A JSR running from the stack page can overwrite its own operand
|
|
71
|
+
# with that push, and then jumps to the overwritten address.
|
|
72
|
+
def op_jsr_high
|
|
73
|
+
@program_counter = @address | (@memory.peek(return_address) << 8)
|
|
74
|
+
end_instruction
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def return_address
|
|
78
|
+
(@program_counter - 1) & 0xffff
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
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
31
|
@status = Status.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
|