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.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +167 -0
  3. data/CLAUDE.md +217 -0
  4. data/CODE_OF_CONDUCT.md +92 -0
  5. data/CONTRIBUTING.md +51 -0
  6. data/README.md +224 -31
  7. data/doc/pinned-behaviour.md +1117 -0
  8. data/exe/badline +22 -2
  9. data/exe/badline-sid +34 -0
  10. data/lib/badline/address_bus.rb +88 -23
  11. data/lib/badline/audio/aiff.rb +36 -0
  12. data/lib/badline/audio/bare_player.rb +129 -0
  13. data/lib/badline/audio/cli.rb +133 -0
  14. data/lib/badline/audio/console.rb +75 -0
  15. data/lib/badline/audio/jukebox.rb +82 -0
  16. data/lib/badline/audio/machine_player.rb +57 -0
  17. data/lib/badline/audio/options.rb +137 -0
  18. data/lib/badline/audio/pcm_writer.rb +57 -0
  19. data/lib/badline/audio/playback.rb +108 -0
  20. data/lib/badline/audio/renderer.rb +85 -0
  21. data/lib/badline/audio/sdl_sink.rb +102 -0
  22. data/lib/badline/audio/wav.rb +23 -0
  23. data/lib/badline/audio.rb +14 -0
  24. data/lib/badline/cartridge/action_replay.rb +111 -0
  25. data/lib/badline/cartridge/atomic_power.rb +24 -0
  26. data/lib/badline/cartridge/bank.rb +81 -0
  27. data/lib/badline/cartridge/comal80.rb +48 -0
  28. data/lib/badline/cartridge/dinamic.rb +34 -0
  29. data/lib/badline/cartridge/easy_flash.rb +122 -0
  30. data/lib/badline/cartridge/epyx_fastload.rb +71 -0
  31. data/lib/badline/cartridge/final_cartridge3.rb +77 -0
  32. data/lib/badline/cartridge/flash.rb +265 -0
  33. data/lib/badline/cartridge/freezer.rb +31 -0
  34. data/lib/badline/cartridge/fun_play.rb +38 -0
  35. data/lib/badline/cartridge/g_mod2.rb +62 -0
  36. data/lib/badline/cartridge/game_system.rb +40 -0
  37. data/lib/badline/cartridge/kcs_power.rb +72 -0
  38. data/lib/badline/cartridge/mach5.rb +34 -0
  39. data/lib/badline/cartridge/magic_desk.rb +6 -3
  40. data/lib/badline/cartridge/ocean.rb +11 -12
  41. data/lib/badline/cartridge/pagefox.rb +56 -0
  42. data/lib/badline/cartridge/retro_replay.rb +276 -0
  43. data/lib/badline/cartridge/rex_utility.rb +33 -0
  44. data/lib/badline/cartridge/rgcd.rb +48 -0
  45. data/lib/badline/cartridge/simons_basic.rb +40 -0
  46. data/lib/badline/cartridge/standard.rb +3 -19
  47. data/lib/badline/cartridge/super_games.rb +36 -0
  48. data/lib/badline/cartridge/westermann.rb +34 -0
  49. data/lib/badline/cartridge/zaxxon.rb +50 -0
  50. data/lib/badline/cartridge.rb +131 -15
  51. data/lib/badline/cia/interrupt_register.rb +88 -0
  52. data/lib/badline/cia/serial.rb +219 -0
  53. data/lib/badline/cia/timer.rb +43 -31
  54. data/lib/badline/cia.rb +109 -74
  55. data/lib/badline/color_memory.rb +14 -5
  56. data/lib/badline/computer.rb +68 -8
  57. data/lib/badline/control_ports.rb +54 -9
  58. data/lib/badline/cpu/addressing.rb +131 -0
  59. data/lib/badline/cpu/microcode.rb +171 -0
  60. data/lib/badline/cpu/operations.rb +88 -0
  61. data/lib/badline/cpu/stack_operations.rb +82 -0
  62. data/lib/badline/cpu.rb +108 -167
  63. data/lib/badline/cycleable.rb +3 -0
  64. data/lib/badline/datasette.rb +81 -0
  65. data/lib/badline/debug_register.rb +28 -0
  66. data/lib/badline/gui/application.rb +79 -18
  67. data/lib/badline/gui/gamepads.rb +97 -0
  68. data/lib/badline/gui/joy_map.rb +15 -7
  69. data/lib/badline/gui.rb +1 -0
  70. data/lib/badline/input/mouse1351.rb +41 -0
  71. data/lib/badline/input/paddles.rb +36 -0
  72. data/lib/badline/input.rb +4 -0
  73. data/lib/badline/instruction_set/arithmetic.rb +16 -21
  74. data/lib/badline/instruction_set/bitwise.rb +17 -22
  75. data/lib/badline/instruction_set/branch.rb +20 -19
  76. data/lib/badline/instruction_set/flag.rb +7 -7
  77. data/lib/badline/instruction_set/illegal.rb +14 -22
  78. data/lib/badline/instruction_set/inc_dec.rb +8 -10
  79. data/lib/badline/instruction_set/stack.rb +5 -57
  80. data/lib/badline/instruction_set/transfer.rb +9 -9
  81. data/lib/badline/instruction_set.rb +9 -31
  82. data/lib/badline/interrupts.rb +104 -0
  83. data/lib/badline/joystick.rb +9 -9
  84. data/lib/badline/kernal_trap/channel.rb +33 -0
  85. data/lib/badline/kernal_trap/drive.rb +239 -0
  86. data/lib/badline/kernal_trap/file.rb +4 -24
  87. data/lib/badline/kernal_trap/load.rb +2 -2
  88. data/lib/badline/kernal_trap/routine.rb +34 -0
  89. data/lib/badline/kernal_trap/save.rb +1 -1
  90. data/lib/badline/kernal_trap/serial.rb +139 -0
  91. data/lib/badline/kernal_trap.rb +4 -0
  92. data/lib/badline/keyboard.rb +40 -16
  93. data/lib/badline/media.rb +48 -10
  94. data/lib/badline/roms/eapi/LICENSE.md +15 -0
  95. data/lib/badline/roms/eapi/README +41 -0
  96. data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
  97. data/lib/badline/sid/decimator.rb +46 -0
  98. data/lib/badline/sid/envelope.rb +232 -0
  99. data/lib/badline/sid/filter.rb +213 -0
  100. data/lib/badline/sid/voice.rb +61 -0
  101. data/lib/badline/sid/waveform/combined.rb +126 -0
  102. data/lib/badline/sid/waveform/fast_forward.rb +105 -0
  103. data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
  104. data/lib/badline/sid/waveform.rb +289 -0
  105. data/lib/badline/sid.rb +274 -7
  106. data/lib/badline/status.rb +32 -27
  107. data/lib/badline/storage/crt_file.rb +18 -2
  108. data/lib/badline/storage/disk_image.rb +52 -5
  109. data/lib/badline/storage/host_directory.rb +18 -4
  110. data/lib/badline/storage/sid_file.rb +274 -0
  111. data/lib/badline/storage/song_lengths.rb +65 -0
  112. data/lib/badline/storage/t64.rb +67 -0
  113. data/lib/badline/storage/tap.rb +68 -0
  114. data/lib/badline/storage.rb +19 -0
  115. data/lib/badline/time_of_day.rb +105 -52
  116. data/lib/badline/version.rb +1 -1
  117. data/lib/badline/vic/bank.rb +14 -6
  118. data/lib/badline/vic/border_mask.rb +76 -0
  119. data/lib/badline/vic/collisions.rb +104 -0
  120. data/lib/badline/vic/color_patches.rb +33 -0
  121. data/lib/badline/vic/display_state.rb +92 -29
  122. data/lib/badline/vic/graphics_mode.rb +78 -53
  123. data/lib/badline/vic/graphics_shifter.rb +140 -0
  124. data/lib/badline/vic/register_log.rb +63 -0
  125. data/lib/badline/vic/registers.rb +3 -0
  126. data/lib/badline/vic/sequencer.rb +163 -137
  127. data/lib/badline/vic/sequencer_output.rb +116 -0
  128. data/lib/badline/vic/sprite/internal_bus.rb +49 -0
  129. data/lib/badline/vic/sprite/shifter.rb +129 -0
  130. data/lib/badline/vic/sprite.rb +275 -65
  131. data/lib/badline/vic/sprites.rb +220 -58
  132. data/lib/badline/vic.rb +373 -49
  133. data/lib/badline.rb +5 -0
  134. data/vendor/.gitkeep +0 -0
  135. 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 < Cycleable
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
- attr_reader :memory, :instructions, :boundary_crossed
14
- attr_accessor :program_counter, :stack_pointer, :status, :a, :x, :y,
15
- :nmi, :irq
23
+ alias pending_write? pending_write
16
24
 
17
- def initialize(memory = nil, debug: false)
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
- super()
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
- @loop.resume unless @instruction || @interrupt
45
- cycle! while @instruction || @interrupt
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
- 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
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
- 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)
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 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
129
+ def end_instruction
130
+ @instructions += 1
131
+ end_sequence
89
132
  end
90
133
 
91
- def read_byte(addr)
92
- cycle { @memory.peek(addr) }
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
- def read_word(addr)
96
- uint16(read_byte(addr),
97
- read_byte((addr + 1) & 0xffff))
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
- def read_zeropage_word(addr)
101
- uint16(read_byte(addr & 0xff),
102
- read_byte((addr + 1) & 0xff))
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 read_instruction
106
- Instruction.find(@memory.peek(@program_counter))
156
+ def write_byte(addr, value)
157
+ @memory.poke(addr, value)
107
158
  end
108
159
 
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)
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 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
172
+ def log(opcode)
173
+ instruction = Instruction.find(opcode)
194
174
  puts(
195
- "#{@cycles}: " \
196
- "PC: #{pc.to_s(16)} - " \
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
@@ -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