badline 0.1.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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +40 -0
  4. data/exe/badline +55 -0
  5. data/lib/badline/address_bus.rb +206 -0
  6. data/lib/badline/addressable.rb +61 -0
  7. data/lib/badline/cartridge/magic_desk.rb +23 -0
  8. data/lib/badline/cartridge/ocean.rb +33 -0
  9. data/lib/badline/cartridge/standard.rb +31 -0
  10. data/lib/badline/cartridge.rb +75 -0
  11. data/lib/badline/chrout_trap.rb +39 -0
  12. data/lib/badline/cia/timer.rb +122 -0
  13. data/lib/badline/cia.rb +189 -0
  14. data/lib/badline/color_memory.rb +16 -0
  15. data/lib/badline/computer.rb +100 -0
  16. data/lib/badline/control_ports.rb +24 -0
  17. data/lib/badline/cpu.rb +239 -0
  18. data/lib/badline/cycleable.rb +35 -0
  19. data/lib/badline/gui/application.rb +94 -0
  20. data/lib/badline/gui/joy_map.rb +19 -0
  21. data/lib/badline/gui/key_map.rb +34 -0
  22. data/lib/badline/gui/palette.rb +35 -0
  23. data/lib/badline/gui/pane.rb +35 -0
  24. data/lib/badline/gui/screen_pane.rb +50 -0
  25. data/lib/badline/gui/window.rb +46 -0
  26. data/lib/badline/gui.rb +11 -0
  27. data/lib/badline/instruction.rb +334 -0
  28. data/lib/badline/instruction_set/arithmetic.rb +119 -0
  29. data/lib/badline/instruction_set/bitwise.rb +131 -0
  30. data/lib/badline/instruction_set/branch.rb +78 -0
  31. data/lib/badline/instruction_set/flag.rb +63 -0
  32. data/lib/badline/instruction_set/illegal.rb +278 -0
  33. data/lib/badline/instruction_set/inc_dec.rb +71 -0
  34. data/lib/badline/instruction_set/stack.rb +104 -0
  35. data/lib/badline/instruction_set/transfer.rb +137 -0
  36. data/lib/badline/instruction_set.rb +77 -0
  37. data/lib/badline/integer_helper.rb +39 -0
  38. data/lib/badline/joystick.rb +25 -0
  39. data/lib/badline/kernal_trap/file.rb +54 -0
  40. data/lib/badline/kernal_trap/load.rb +63 -0
  41. data/lib/badline/kernal_trap/save.rb +42 -0
  42. data/lib/badline/kernal_trap.rb +5 -0
  43. data/lib/badline/keyboard.rb +58 -0
  44. data/lib/badline/keyboard_buffer.rb +33 -0
  45. data/lib/badline/media.rb +59 -0
  46. data/lib/badline/memory.rb +43 -0
  47. data/lib/badline/rom.rb +23 -0
  48. data/lib/badline/roms/README +18 -0
  49. data/lib/badline/roms/basic.rom +0 -0
  50. data/lib/badline/roms/character.rom +0 -0
  51. data/lib/badline/roms/kernal.rom +0 -0
  52. data/lib/badline/sid.rb +25 -0
  53. data/lib/badline/status.rb +56 -0
  54. data/lib/badline/storage/crt_file.rb +53 -0
  55. data/lib/badline/storage/d64_image.rb +21 -0
  56. data/lib/badline/storage/d71_image.rb +13 -0
  57. data/lib/badline/storage/d81_image.rb +14 -0
  58. data/lib/badline/storage/disk_image.rb +71 -0
  59. data/lib/badline/storage/host_directory.rb +49 -0
  60. data/lib/badline/storage/p00.rb +24 -0
  61. data/lib/badline/storage.rb +28 -0
  62. data/lib/badline/time_of_day.rb +101 -0
  63. data/lib/badline/traps.rb +15 -0
  64. data/lib/badline/version.rb +5 -0
  65. data/lib/badline/vic/bank.rb +65 -0
  66. data/lib/badline/vic/display_state.rb +78 -0
  67. data/lib/badline/vic/graphics_mode.rb +139 -0
  68. data/lib/badline/vic/registers.rb +170 -0
  69. data/lib/badline/vic/sequencer.rb +237 -0
  70. data/lib/badline/vic/sprite.rb +121 -0
  71. data/lib/badline/vic/sprites.rb +112 -0
  72. data/lib/badline/vic.rb +192 -0
  73. data/lib/badline.rb +29 -0
  74. metadata +131 -0
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CIA
5
+ class Timer
6
+ attr_accessor :counter, :latch
7
+ attr_reader :control, :underflowed
8
+
9
+ def initialize(control)
10
+ @control = control
11
+ @counter = @latch = 0x0
12
+ @pipe = 0
13
+ @load_delay = 0
14
+ @reload = false
15
+ @underflowed = false
16
+ @oneshot_linger = 0
17
+ @toggle = true
18
+ end
19
+
20
+ def toggle?
21
+ @toggle
22
+ end
23
+
24
+ def cycle!(feed, pulse)
25
+ return @counter -= 1 if feed && pulse && steady?
26
+
27
+ if (@pipe | @load_delay).zero? && !@reload
28
+ @pipe = 0b10 if feed && started?
29
+ return
30
+ end
31
+ run_tick(feed, pulse)
32
+ end
33
+
34
+ def run_tick(feed, pulse)
35
+ @underflowed = false
36
+ @oneshot_linger -= 1 if @oneshot_linger.positive?
37
+ tick(feed && started?, pulse)
38
+ end
39
+
40
+ def write_control(value)
41
+ @toggle = true if value.anybits?(0x01) && !started?
42
+ @oneshot_linger = 2 if control.run_mode? && value.nobits?(0x08)
43
+ control.value = value & ~0x10
44
+ @load_delay = 3 if value.anybits?(0x10)
45
+ end
46
+
47
+ def write_latch_low(value)
48
+ @latch = (@latch & 0xff00) | value
49
+ end
50
+
51
+ def write_latch_high(value)
52
+ @latch = (value << 8) | (@latch & 0xff)
53
+ @counter = @latch unless started?
54
+ end
55
+
56
+ private
57
+
58
+ def started?
59
+ control.value.anybits?(0x01)
60
+ end
61
+
62
+ def steady?
63
+ @counter > 1 && @pipe == 0b11 && !@reload &&
64
+ (@load_delay | @oneshot_linger).zero? && started?
65
+ end
66
+
67
+ def tick(feed, pulse)
68
+ counting = @pipe.anybits?(0b01) && pulse
69
+ @pipe = (@pipe >> 1) | (feed ? 0b10 : 0)
70
+
71
+ if premature_underflow?(pulse)
72
+ underflow
73
+ elsif apply_reload?
74
+ return
75
+ elsif counting && @load_delay != 1
76
+ count
77
+ end
78
+
79
+ forced_load
80
+ end
81
+
82
+ def forced_load
83
+ return unless @load_delay.positive?
84
+
85
+ @load_delay -= 1
86
+ @counter = @latch if @load_delay == 1
87
+ end
88
+
89
+ # the final pipeline stage, before any pending load lands
90
+ def premature_underflow?(pulse)
91
+ @counter.zero? && !@reload && pulse && @pipe.anybits?(0b01) &&
92
+ started?
93
+ end
94
+
95
+ # An underflow reload consumes the tick after the flag
96
+ def apply_reload?
97
+ return false unless @reload
98
+
99
+ @reload = false
100
+ @counter = @latch
101
+ # A zero latch underflows again on the reload tick while running
102
+ underflow if @counter.zero? && started?
103
+ true
104
+ end
105
+
106
+ def count
107
+ @counter -= 1 if @counter.positive?
108
+ underflow if @counter.zero? && @pipe.anybits?(0b01)
109
+ end
110
+
111
+ def underflow
112
+ @underflowed = true
113
+ @reload = true
114
+ @toggle = !@toggle
115
+ return unless control.run_mode? || @oneshot_linger.positive?
116
+
117
+ control.start = false
118
+ @pipe = 0
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "badline/cia/timer"
5
+
6
+ module Badline
7
+ # CIA (Complex Interface Adapter) chip
8
+ class CIA
9
+ include Addressable
10
+ extend Forwardable
11
+
12
+ attr_reader :start, :control_a, :control_b, :interrupt_status, :interrupt_control, :peripheral
13
+
14
+ def_delegator :@ta, :counter, :timer_a
15
+ def_delegator :@ta, :counter=, :timer_a=
16
+ def_delegator :@ta, :latch, :timer_a_latch
17
+ def_delegator :@ta, :latch=, :timer_a_latch=
18
+ def_delegator :@tb, :counter, :timer_b
19
+ def_delegator :@tb, :counter=, :timer_b=
20
+ def_delegator :@tb, :latch, :timer_b_latch
21
+ def_delegator :@tb, :latch=, :timer_b_latch=
22
+
23
+ def initialize(start: 0, peripheral: nil)
24
+ addressable_at(start, length: 2**8)
25
+
26
+ @peripheral = peripheral
27
+ @data_port_a = 0xff
28
+ @data_port_b = 0xff
29
+ @data_dir_a = 0xff
30
+ @data_dir_b = 0x0
31
+ @irq_pending = 0
32
+ @serial_data = 0x0
33
+ @tod = TimeOfDay.new
34
+ @interrupt_control = Status.new([:timer_a, :timer_b, :alarm, :serial,
35
+ :flag, 0, 0, 0])
36
+ @interrupt_status = Status.new([:timer_a, :timer_b, :alarm, :serial,
37
+ :flag, 0, 0, :interrupt])
38
+ @control_a = Status.new(%i[start output out_mode run_mode load
39
+ in_mode serial_mode clock_frequency])
40
+ @control_b = Status.new(%i[start output out_mode run_mode load
41
+ in_cnt in_timer_a alarm])
42
+ @ta = Timer.new(@control_a)
43
+ @tb = Timer.new(@control_b)
44
+ end
45
+
46
+ def interrupt!(delay = 1)
47
+ @irq_pending = @irq_pending.positive? ? [@irq_pending, delay].min : delay
48
+ end
49
+
50
+ def interrupted?
51
+ interrupt_status.value.anybits?(0x80)
52
+ end
53
+
54
+ def cycle!
55
+ if @irq_pending.positive?
56
+ @irq_pending -= 1
57
+ interrupt_status.interrupt = true if @irq_pending.zero?
58
+ end
59
+ update_timers
60
+ @tod.cycle! { trigger_alarm }
61
+ end
62
+
63
+ def read_port_a
64
+ pulldown = peripheral ? peripheral.read_a(@data_port_a, @data_port_b) : 0xff
65
+ driven_lines(@data_port_a, @data_dir_a) & pulldown
66
+ end
67
+
68
+ # Port A as driven by the data/direction registers alone, without
69
+ # peripheral pulldown. Cheap path for the VIC bank lookup.
70
+ def port_a_lines
71
+ driven_lines(@data_port_a, @data_dir_a)
72
+ end
73
+
74
+ def read_port_b
75
+ pulldown = peripheral ? peripheral.read_b(@data_port_a, @data_port_b) : 0xff
76
+ value = driven_lines(@data_port_b, @data_dir_b) & pulldown
77
+ apply_timer_output(value)
78
+ end
79
+
80
+ def peek(addr)
81
+ case index(addr) & 0x0f
82
+ when 0x00 then read_port_a
83
+ when 0x01 then read_port_b
84
+ when 0x02 then @data_dir_a
85
+ when 0x03 then @data_dir_b
86
+ when 0x04 then low_byte(@ta.counter)
87
+ when 0x05 then high_byte(@ta.counter)
88
+ when 0x06 then low_byte(@tb.counter)
89
+ when 0x07 then high_byte(@tb.counter)
90
+ when 0x08 then @tod.tenths
91
+ when 0x09 then @tod.seconds
92
+ when 0x0a then @tod.minutes
93
+ when 0x0b then @tod.hours
94
+ when 0x0c then @serial_data
95
+ when 0x0d
96
+ value = interrupt_status.value
97
+ interrupt_status.value = 0x0 # Burn after reading
98
+ @irq_pending = 0
99
+ value
100
+ when 0x0e then control_a.value
101
+ when 0x0f then control_b.value
102
+ end
103
+ end
104
+
105
+ def poke(addr, value)
106
+ case index(addr) & 0x0f
107
+ when 0x00 then @data_port_a = value
108
+ when 0x01 then @data_port_b = value
109
+ when 0x02 then @data_dir_a = value
110
+ when 0x03 then @data_dir_b = value
111
+ when 0x04 then @ta.write_latch_low(value)
112
+ when 0x05 then @ta.write_latch_high(value)
113
+ when 0x06 then @tb.write_latch_low(value)
114
+ when 0x07 then @tb.write_latch_high(value)
115
+ when 0x08 then @tod.write(:tenths, value, alarm: control_b.alarm?)
116
+ when 0x09 then @tod.write(:seconds, value, alarm: control_b.alarm?)
117
+ when 0x0a then @tod.write(:minutes, value, alarm: control_b.alarm?)
118
+ when 0x0b then @tod.write_hours(value, alarm: control_b.alarm?)
119
+ when 0x0c
120
+ # TODO: Serial
121
+ when 0x0d then write_interrupt_control(value)
122
+ when 0x0e then @ta.write_control(value)
123
+ when 0x0f then @tb.write_control(value)
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ def driven_lines(register, direction)
130
+ # Output bits are driven from the data register; input bits float high.
131
+ # External peripherals can still pull any line low (wired-AND).
132
+ (register & direction) | (~direction & 0xff)
133
+ end
134
+
135
+ def apply_timer_output(value)
136
+ value = with_bit(value, 6, timer_a_output?) if control_a.output?
137
+ value = with_bit(value, 7, timer_b_output?) if control_b.output?
138
+ value
139
+ end
140
+
141
+ def timer_a_output?
142
+ control_a.out_mode? ? @ta.toggle? : @ta.underflowed
143
+ end
144
+
145
+ def timer_b_output?
146
+ control_b.out_mode? ? @tb.toggle? : @tb.underflowed
147
+ end
148
+
149
+ def with_bit(value, bit, set)
150
+ set ? value | (1 << bit) : value & ~(1 << bit)
151
+ end
152
+
153
+ def trigger_alarm
154
+ interrupt_status.alarm = true
155
+ interrupt! if interrupt_control.alarm?
156
+ end
157
+
158
+ def update_timers
159
+ @ta.cycle!(@control_a.value.nobits?(0x20), true)
160
+ crb = @control_b.value
161
+ if crb.anybits?(0x40)
162
+ @tb.cycle!(true, @ta.underflowed)
163
+ else
164
+ @tb.cycle!(crb.nobits?(0x20), true)
165
+ end
166
+ if @ta.underflowed
167
+ interrupt_status.timer_a = true
168
+ interrupt! if interrupt_control.timer_a?
169
+ end
170
+ return unless @tb.underflowed
171
+
172
+ interrupt_status.timer_b = true
173
+ interrupt! if interrupt_control.timer_b?
174
+ end
175
+
176
+ def write_interrupt_control(value)
177
+ if value.nobits?(0x80)
178
+ # Clear interrupts based on bits 0-4
179
+ interrupt_control.value &= ~(value & 0x1f)
180
+ else
181
+ # Set interrupts based on bits 0-4
182
+ interrupt_control.value |= (value & 0x1f)
183
+ end
184
+ return unless interrupt_control.value.anybits?(interrupt_status.value & 0x1f)
185
+
186
+ interrupt!(2) unless interrupted?
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class ColorMemory < Memory
5
+ def poke(addr, value)
6
+ # Only write the lower 4 bits
7
+ super(addr, value & 0x0f)
8
+ end
9
+
10
+ private
11
+
12
+ def blank_value
13
+ rand(16) << 4
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Badline
6
+ class Computer
7
+ include IntegerHelper
8
+ include KeyboardBuffer
9
+ extend Forwardable
10
+
11
+ attr_reader :address_bus, :cpu, :cycles
12
+
13
+ def_delegators :address_bus, :vic, :cia1, :cia2, :sid, :ram, :keyboard, :joystick2
14
+
15
+ def initialize(debug: false)
16
+ @address_bus = AddressBus.new
17
+ @cpu = CPU.new(@address_bus, debug:)
18
+ @vic = @address_bus.vic
19
+ @cia1 = @address_bus.cia1
20
+ @cia2 = @address_bus.cia2
21
+ @cycles = 0
22
+ @nmi_asserted = false
23
+ @init_handlers = []
24
+ @pending_keys = nil
25
+ end
26
+
27
+ INIT_THRESHOLD = 2_500_000
28
+
29
+ def cycle!
30
+ handle_init if @cycles == INIT_THRESHOLD
31
+ feed_keyboard if @pending_keys
32
+
33
+ @vic.cycle!
34
+ @cia1.cycle!
35
+ @cia2.cycle!
36
+
37
+ @cpu.irq = @cia1.interrupted? || @vic.interrupted?
38
+
39
+ nmi = @cia2.interrupted?
40
+ @cpu.nmi = true if nmi && !@nmi_asserted
41
+ @nmi_asserted = nmi
42
+
43
+ @cpu.cycle! if @cpu.pending_write? || !@vic.ba_low?
44
+
45
+ @cycles += 1
46
+ end
47
+
48
+ def load_prg(data)
49
+ uint16(data[0], data[1]).tap do |load_addr|
50
+ ram.write(load_addr, data[2..])
51
+ end
52
+ end
53
+
54
+ def attach_cartridge(cartridge)
55
+ address_bus.attach_cartridge(cartridge)
56
+ cpu.reset!
57
+ end
58
+
59
+ def mount(storage)
60
+ load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, storage:)
61
+ cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
62
+ return unless storage.respond_to?(:write_file)
63
+
64
+ save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, storage:)
65
+ cpu.install_trap(KernalTrap::Save::ADDRESS) { save_trap.call }
66
+ end
67
+
68
+ def capture_output
69
+ @capture_output ||= ChroutTrap.new(cpu:, bus: address_bus).tap do |trap|
70
+ cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
71
+ end
72
+ end
73
+
74
+ def inspect
75
+ "#<#{self.class.name} cycles=#{@cycles} cpu=(#{@cpu.inspect})>"
76
+ end
77
+
78
+ def on_init(&block)
79
+ if booting?
80
+ @init_handlers << block
81
+ else
82
+ block.call
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def booting?
89
+ @cycles < init_threshold
90
+ end
91
+
92
+ def handle_init
93
+ @init_handlers.each(&:call)
94
+ end
95
+
96
+ def init_threshold
97
+ INIT_THRESHOLD
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ # CIA 1 peripheral combining the keyboard matrix with the control ports.
5
+ #
6
+ # Joystick switches share the same lines as the keyboard matrix and are
7
+ # wired-AND with it (active low). Port 2 sits on Port A.
8
+ class ControlPorts
9
+ attr_reader :keyboard, :joystick2
10
+
11
+ def initialize(keyboard:, joystick2:)
12
+ @keyboard = keyboard
13
+ @joystick2 = joystick2
14
+ end
15
+
16
+ def read_a(port_a, port_b)
17
+ keyboard.read_a(port_a, port_b) & joystick2.port_bits
18
+ end
19
+
20
+ def read_b(port_a, port_b)
21
+ keyboard.read_b(port_a, port_b)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CPU < Cycleable
5
+ STATUS_FLAGS = [:carry, :zero, :interrupt, :decimal, :break, 1,
6
+ :overflow, :negative].freeze
7
+
8
+ class InvalidOpcodeError < StandardError; end
9
+ include IntegerHelper
10
+ include InstructionSet
11
+ include Traps
12
+
13
+ attr_reader :memory, :instructions, :boundary_crossed
14
+ attr_accessor :program_counter, :stack_pointer, :status, :a, :x, :y,
15
+ :nmi, :irq
16
+
17
+ def initialize(memory = nil, debug: false)
18
+ @debug = debug
19
+ @memory = memory || Memory.new
20
+ @status = Status.new(STATUS_FLAGS, value: 0b00100000)
21
+ reset_registers
22
+
23
+ @nmi = @irq = false
24
+
25
+ @instructions = 0
26
+ @traps = nil
27
+ super()
28
+ end
29
+
30
+ def reset!
31
+ status.interrupt = true
32
+ reset_registers
33
+ end
34
+
35
+ def p
36
+ status.value
37
+ end
38
+
39
+ def p=(new_value)
40
+ status.value = new_value
41
+ end
42
+
43
+ def step!
44
+ @loop.resume unless @instruction || @interrupt
45
+ cycle! while @instruction || @interrupt
46
+ end
47
+
48
+ def inspect
49
+ "Cycles: #{@cycles}, PC: #{format16(program_counter)}, " \
50
+ "SP: #{format8(stack_pointer)}, A: #{format8(a)}, X: #{format8(x)}, " \
51
+ "Y: #{format8(y)}, P: #{format8(p)}"
52
+ end
53
+
54
+ private
55
+
56
+ def extra_cycle(instruction)
57
+ return cycle unless instruction.boundary_cycle?
58
+
59
+ boundary_crossed && cycle
60
+ end
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
67
+
68
+ write_byte(stack_address, high_byte(pc))
69
+ @stack_pointer = (@stack_pointer - 1) & 0xff
70
+ write_byte(stack_address, low_byte(pc))
71
+ @stack_pointer = (@stack_pointer - 1) & 0xff
72
+ write_byte(stack_address,
73
+ status.clone.tap { |s| s.break = brk }.value)
74
+ @stack_pointer = (@stack_pointer - 1) & 0xff
75
+ status.interrupt = true
76
+ @program_counter = read_word(vector)
77
+ end
78
+
79
+ def handle_interrupts
80
+ @interrupt = if nmi
81
+ 0xfffa
82
+ elsif irq && !status.interrupt?
83
+ 0xfffe
84
+ end
85
+
86
+ handle_interrupt(@interrupt) if @interrupt
87
+ @interrupt = nil
88
+ @nmi = @irq = false
89
+ end
90
+
91
+ def read_byte(addr)
92
+ cycle { @memory.peek(addr) }
93
+ end
94
+
95
+ def read_word(addr)
96
+ uint16(read_byte(addr),
97
+ read_byte((addr + 1) & 0xffff))
98
+ end
99
+
100
+ def read_zeropage_word(addr)
101
+ uint16(read_byte(addr & 0xff),
102
+ read_byte((addr + 1) & 0xff))
103
+ end
104
+
105
+ def read_instruction
106
+ Instruction.find(@memory.peek(@program_counter))
107
+ end
108
+
109
+ def read_operand(instruction)
110
+ return nil unless instruction.operand?
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)
116
+ end
117
+
118
+ def reset_registers
119
+ # The program counter is initialized from the reset vector
120
+ @program_counter = @memory.peek16(0xfffc)
121
+ # Stack pointer starts at 0x01ff and grows down
122
+ @stack_pointer = 0xff
123
+ @a = @x = @y = 0x0
124
+ end
125
+
126
+ def read_address(instruction, operand)
127
+ case instruction.addressing_mode
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
194
+ puts(
195
+ "#{@cycles}: " \
196
+ "PC: #{pc.to_s(16)} - " \
197
+ "#{@instruction.name.upcase} #{@instruction.addressing_mode} " \
198
+ "Operand: #{operand.inspect} Address: #{address.inspect}"
199
+ )
200
+ 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
+ end
239
+ end