badline 0.5.1 → 0.6.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 +52 -0
- data/CLAUDE.md +10 -9
- data/README.md +82 -10
- data/doc/pinned-behaviour.md +131 -30
- data/exe/badline-ruby +20 -23
- data/lib/badline/address_bus/saved_state.rb +72 -0
- data/lib/badline/address_bus.rb +11 -1
- data/lib/badline/audio/cli.rb +18 -5
- data/lib/badline/audio/console.rb +2 -1
- data/lib/badline/audio.rb +0 -3
- data/lib/badline/cartridge/action_replay.rb +15 -0
- data/lib/badline/cartridge/comal80.rb +8 -0
- data/lib/badline/cartridge/dinamic.rb +8 -0
- data/lib/badline/cartridge/easy_flash.rb +20 -1
- data/lib/badline/cartridge/epyx_fastload.rb +8 -0
- data/lib/badline/cartridge/final_cartridge3.rb +12 -0
- data/lib/badline/cartridge/flash.rb +41 -0
- data/lib/badline/cartridge/fun_play.rb +8 -0
- data/lib/badline/cartridge/g_mod2.rb +12 -0
- data/lib/badline/cartridge/game_system.rb +8 -0
- data/lib/badline/cartridge/geo_ram.rb +16 -0
- data/lib/badline/cartridge/magic_desk.rb +8 -0
- data/lib/badline/cartridge/ocean.rb +8 -0
- data/lib/badline/cartridge/pagefox.rb +12 -0
- data/lib/badline/cartridge/retro_replay.rb +44 -1
- data/lib/badline/cartridge/rgcd.rb +10 -0
- data/lib/badline/cartridge/super_games.rb +10 -0
- data/lib/badline/cartridge/zaxxon.rb +16 -0
- data/lib/badline/cartridge.rb +72 -0
- data/lib/badline/cia/interrupt_register.rb +17 -0
- data/lib/badline/cia/serial.rb +44 -0
- data/lib/badline/cia/state.rb +47 -0
- data/lib/badline/cia/timer.rb +33 -0
- data/lib/badline/cia.rb +8 -0
- data/lib/badline/computer/attachments.rb +132 -0
- data/lib/badline/computer/saved_state.rb +116 -0
- data/lib/badline/computer.rb +13 -33
- data/lib/badline/cpu/boundary.rb +54 -0
- data/lib/badline/cpu/core.rb +165 -0
- data/lib/badline/cpu/microcode.rb +8 -0
- data/lib/badline/cpu/saved_state.rb +115 -0
- data/lib/badline/cpu.rb +3 -212
- data/lib/badline/datasette.rb +27 -0
- data/lib/badline/drive1541/bus.rb +13 -0
- data/lib/badline/drive1541/cpu.rb +29 -0
- data/lib/badline/drive1541/disk.rb +7 -2
- data/lib/badline/drive1541/disk_state.rb +80 -0
- data/lib/badline/drive1541/mechanism.rb +2 -0
- data/lib/badline/drive1541/mechanism_state.rb +67 -0
- data/lib/badline/drive1541/rotation.rb +10 -0
- data/lib/badline/drive1541/serial_port.rb +10 -0
- data/lib/badline/drive1541.rb +35 -0
- data/lib/badline/ffi.rb +157 -0
- data/lib/badline/frontend/app.rb +225 -0
- data/lib/badline/frontend/audio_sink.rb +72 -0
- data/lib/badline/frontend/boot.rb +52 -0
- data/lib/badline/frontend/controls.rb +163 -0
- data/lib/badline/frontend/drive_led.rb +51 -0
- data/lib/badline/frontend/frame_rate.rb +83 -0
- data/lib/badline/frontend/gamepads.rb +76 -0
- data/lib/badline/frontend/keys.rb +48 -0
- data/lib/badline/frontend/pacer.rb +100 -0
- data/lib/badline/frontend/pad_port.rb +61 -0
- data/lib/badline/frontend/screen.rb +65 -0
- data/lib/badline/frontend/screenshot.rb +22 -0
- data/lib/badline/frontend/snapshots.rb +60 -0
- data/lib/badline/frontend/sound.rb +153 -0
- data/lib/badline/frontend/timeline.rb +171 -0
- data/lib/badline/frontend.rb +18 -0
- data/lib/badline/kernal_trap/channel.rb +27 -0
- data/lib/badline/kernal_trap/drive/channels.rb +25 -0
- data/lib/badline/kernal_trap/drive/memory.rb +9 -0
- data/lib/badline/kernal_trap/drive/status.rb +8 -0
- data/lib/badline/kernal_trap/drive/write_file.rb +21 -0
- data/lib/badline/kernal_trap/drive.rb +19 -0
- data/lib/badline/kernal_trap/save.rb +8 -0
- data/lib/badline/kernal_trap/serial.rb +16 -0
- data/lib/badline/memory.rb +8 -0
- data/lib/badline/options/event.rb +94 -0
- data/lib/badline/options/help.rb +78 -0
- data/lib/badline/options/option.rb +29 -0
- data/lib/badline/options/table.rb +62 -0
- data/lib/badline/options.rb +181 -144
- data/lib/badline/ram_expansion/plus256k.rb +16 -0
- data/lib/badline/ram_expansion/plus60k.rb +12 -0
- data/lib/badline/ram_expansion/unexpanded.rb +6 -0
- data/lib/badline/reu/dma/saved_state.rb +50 -0
- data/lib/badline/reu/dma.rb +56 -11
- data/lib/badline/reu/ram.rb +15 -0
- data/lib/badline/reu/saved_state.rb +53 -0
- data/lib/badline/reu.rb +23 -10
- data/lib/badline/sdl.rb +123 -51
- data/lib/badline/sid/envelope/state.rb +68 -0
- data/lib/badline/sid/envelope.rb +7 -0
- data/lib/badline/sid/filter.rb +38 -0
- data/lib/badline/sid/voice.rb +10 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +33 -11
- data/lib/badline/sid/waveform/state.rb +66 -0
- data/lib/badline/sid/waveform.rb +4 -3
- data/lib/badline/sid.rb +34 -0
- data/lib/badline/snapshot/container.rb +117 -0
- data/lib/badline/snapshot/fields.rb +54 -0
- data/lib/badline/snapshot/image.rb +70 -0
- data/lib/badline/snapshot/machine_state.rb +77 -0
- data/lib/badline/snapshot/setup.rb +58 -0
- data/lib/badline/snapshot/state.rb +178 -0
- data/lib/badline/snapshot/vice/c64_mem.rb +53 -0
- data/lib/badline/snapshot/vice/cias.rb +89 -0
- data/lib/badline/snapshot/vice/cias_import.rb +70 -0
- data/lib/badline/snapshot/vice/main_cpu.rb +76 -0
- data/lib/badline/snapshot/vice/peripherals.rb +47 -0
- data/lib/badline/snapshot/vice/restore.rb +125 -0
- data/lib/badline/snapshot/vice/sid_extended.rb +96 -0
- data/lib/badline/snapshot/vice/sid_registers.rb +46 -0
- data/lib/badline/snapshot/vice/vicii.rb +119 -0
- data/lib/badline/snapshot/vice/vicii_import.rb +131 -0
- data/lib/badline/snapshot/vice.rb +74 -0
- data/lib/badline/snapshot.rb +52 -0
- data/lib/badline/storage/crt_file.rb +9 -3
- data/lib/badline/storage/d64_image.rb +2 -0
- data/lib/badline/storage/d71_image.rb +2 -0
- data/lib/badline/storage/d81_image.rb +2 -0
- data/lib/badline/storage/disk_image.rb +11 -2
- data/lib/badline/storage/host_directory.rb +5 -0
- data/lib/badline/storage/t64.rb +6 -0
- data/lib/badline/storage/tap.rb +10 -1
- data/lib/badline/storage/unavailable.rb +20 -0
- data/lib/badline/storage.rb +34 -0
- data/lib/badline/time_of_day.rb +56 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/control_lines.rb +15 -0
- data/lib/badline/via/interrupt_register.rb +9 -0
- data/lib/badline/via/shift_register.rb +13 -0
- data/lib/badline/via/state.rb +42 -0
- data/lib/badline/via/timer1.rb +13 -0
- data/lib/badline/via/timer2.rb +18 -0
- data/lib/badline/via.rb +2 -0
- data/lib/badline/vic/bank.rb +21 -0
- data/lib/badline/vic/border_mask.rb +10 -0
- data/lib/badline/vic/collisions.rb +11 -0
- data/lib/badline/vic/color_patches.rb +11 -0
- data/lib/badline/vic/display_state.rb +29 -1
- data/lib/badline/vic/graphics_shifter.rb +26 -0
- data/lib/badline/vic/register_log.rb +18 -0
- data/lib/badline/vic/registers.rb +13 -2
- data/lib/badline/vic/sequencer.rb +12 -0
- data/lib/badline/vic/sequencer_state.rb +89 -0
- data/lib/badline/vic/sprite/internal_bus.rb +11 -0
- data/lib/badline/vic/sprite/saved_state.rb +64 -0
- data/lib/badline/vic/sprite.rb +2 -0
- data/lib/badline/vic/sprites.rb +3 -0
- data/lib/badline/vic/sprites_state.rb +57 -0
- data/lib/badline/vic.rb +109 -16
- data/lib/badline.rb +1 -0
- metadata +59 -18
- data/.claude/hooks/session-start.sh +0 -57
- data/.claude/settings.json +0 -14
- data/lib/badline/audio/sdl_sink.rb +0 -70
- data/lib/badline/audio/stream.rb +0 -117
- data/lib/badline/gui/application.rb +0 -216
- data/lib/badline/gui/drive_led_pane.rb +0 -29
- data/lib/badline/gui/gamepads.rb +0 -97
- data/lib/badline/gui/joy_map.rb +0 -27
- data/lib/badline/gui/key_map.rb +0 -39
- data/lib/badline/gui/palette.rb +0 -35
- data/lib/badline/gui/pane.rb +0 -36
- data/lib/badline/gui/screen_pane.rb +0 -46
- data/lib/badline/gui/window.rb +0 -61
- data/lib/badline/gui.rb +0 -13
- data/lib/badline/sdl/events.rb +0 -47
- data/lib/badline/sdl/functions.rb +0 -74
- data/vendor/.gitkeep +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/cpu/microcode"
|
|
4
|
+
require "badline/cpu/addressing"
|
|
5
|
+
require "badline/cpu/operations"
|
|
6
|
+
require "badline/cpu/stack_operations"
|
|
7
|
+
require "badline/cpu/boundary"
|
|
8
|
+
require "badline/cpu/saved_state"
|
|
9
|
+
|
|
10
|
+
module Badline
|
|
11
|
+
class CPU
|
|
12
|
+
STATUS_FLAGS = [:carry, :zero, :interrupt, :decimal, :break, 1, :overflow, :negative].freeze
|
|
13
|
+
|
|
14
|
+
# The 6502 core, on whatever bus +memory+ is. The C64's 6510 (CPU) and
|
|
15
|
+
# the 1541's 6502 (Drive1541::CPU) each include it.
|
|
16
|
+
module Core
|
|
17
|
+
include IntegerHelper
|
|
18
|
+
include InstructionSet
|
|
19
|
+
include Interrupts
|
|
20
|
+
include Traps
|
|
21
|
+
include Addressing
|
|
22
|
+
include Operations
|
|
23
|
+
include StackOperations
|
|
24
|
+
include Boundary
|
|
25
|
+
include SavedState
|
|
26
|
+
|
|
27
|
+
attr_reader :memory, :instructions, :boundary_crossed, :cycles, :pending_write
|
|
28
|
+
attr_accessor :program_counter, :stack_pointer, :status, :a, :x, :y, :nmi, :irq
|
|
29
|
+
|
|
30
|
+
alias pending_write? pending_write
|
|
31
|
+
|
|
32
|
+
# +ane_constant+ is ANE's magic constant, which varies from chip to
|
|
33
|
+
# chip. The default is the C64 6510's.
|
|
34
|
+
def initialize(memory, debug: false, ane_constant: 0xef)
|
|
35
|
+
@debug = debug
|
|
36
|
+
@ane_constant = ane_constant
|
|
37
|
+
@memory = memory
|
|
38
|
+
@status = CPUStatus.new(STATUS_FLAGS, value: 0b00100000)
|
|
39
|
+
reset_registers
|
|
40
|
+
|
|
41
|
+
@nmi = @irq = false
|
|
42
|
+
@irq_sample = @irq_pending = false
|
|
43
|
+
@nmi_sample = @nmi_pending = false
|
|
44
|
+
@skip_poll = false
|
|
45
|
+
@boundary_crossed = false
|
|
46
|
+
@interrupt = nil
|
|
47
|
+
@brk = false
|
|
48
|
+
@pending_write = false
|
|
49
|
+
@stalled_at = nil
|
|
50
|
+
@so_high = true
|
|
51
|
+
|
|
52
|
+
@cycles = 0
|
|
53
|
+
@instructions = 0
|
|
54
|
+
@traps = nil
|
|
55
|
+
end_sequence
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def reset!
|
|
59
|
+
status.interrupt = true
|
|
60
|
+
reset_registers
|
|
61
|
+
@nmi = false
|
|
62
|
+
@irq_sample = @irq_pending = false
|
|
63
|
+
@nmi_sample = @nmi_pending = false
|
|
64
|
+
@skip_poll = false
|
|
65
|
+
@pending_write = false
|
|
66
|
+
end_sequence
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def p
|
|
70
|
+
status.value
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def p=(new_value)
|
|
74
|
+
status.value = new_value
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def cycle!
|
|
78
|
+
poll # the interrupt lines
|
|
79
|
+
|
|
80
|
+
# Run the next step
|
|
81
|
+
index = @index
|
|
82
|
+
@index = index + 1
|
|
83
|
+
send(@plan[index])
|
|
84
|
+
@cycles += 1
|
|
85
|
+
|
|
86
|
+
# Record if the next step writes to memory
|
|
87
|
+
@pending_write = @writes[@index]
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Called instead of #cycle! on a cycle the VIC holds the CPU through
|
|
92
|
+
# BA. Records which cycle the CPU was stalled before, and keeps
|
|
93
|
+
# sampling the interrupt lines.
|
|
94
|
+
def stall!
|
|
95
|
+
@stalled_at = @cycles
|
|
96
|
+
sample_while_stalled
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# The 6502's SO (set overflow) pin, which the 6510 leaves out, so the
|
|
100
|
+
# C64 never drives it. A falling edge sets V and a held level does
|
|
101
|
+
# nothing more.
|
|
102
|
+
def so=(high)
|
|
103
|
+
@status.overflow = true if @so_high && !high
|
|
104
|
+
@so_high = high
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Pulses SO: pulls it low, which sets V, and releases it.
|
|
108
|
+
def so!
|
|
109
|
+
@status.overflow = true
|
|
110
|
+
@so_high = true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def jammed?
|
|
114
|
+
@plan.equal?(JAMMED_PLAN)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Runs to the end of the instruction, or until the CPU jams.
|
|
118
|
+
def step!
|
|
119
|
+
cycle!
|
|
120
|
+
cycle! until @plan.equal?(FETCH_PLAN) || jammed?
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Whether the next cycle fetches an opcode.
|
|
125
|
+
def boundary? = @index.zero? && @plan.equal?(FETCH_PLAN)
|
|
126
|
+
|
|
127
|
+
def trapped? = !@traps.nil?
|
|
128
|
+
|
|
129
|
+
def inspect
|
|
130
|
+
"Cycles: #{@cycles}, PC: #{format16(program_counter)}, " \
|
|
131
|
+
"SP: #{format8(stack_pointer)}, A: #{format8(a)}, X: #{format8(x)}, " \
|
|
132
|
+
"Y: #{format8(y)}, P: #{format8(p)}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
# True when the CPU was stalled right before this cycle.
|
|
138
|
+
def stalled_before_this_cycle?
|
|
139
|
+
@stalled_at == @cycles
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# True when the CPU was stalled right before the cycle that preceded
|
|
143
|
+
# this one.
|
|
144
|
+
def stalled_before_previous_cycle?
|
|
145
|
+
@stalled_at == @cycles - 1
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def write_byte(addr, value)
|
|
149
|
+
@memory.poke(addr, value)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def stack_address(offset = 0)
|
|
153
|
+
0x0100 | ((@stack_pointer + offset) & 0xff)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def reset_registers
|
|
157
|
+
# The program counter is initialized from the reset vector
|
|
158
|
+
@program_counter = @memory.peek16(0xfffc)
|
|
159
|
+
# Stack pointer starts at 0x01ff and grows down
|
|
160
|
+
@stack_pointer = 0xff
|
|
161
|
+
@a = @x = @y = 0x0
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -165,5 +165,13 @@ module Badline
|
|
|
165
165
|
INTERRUPT_WRITES = Microcode.write_mask(INTERRUPT_PLAN)
|
|
166
166
|
JAMMED_WRITES = Microcode.write_mask(JAMMED_PLAN)
|
|
167
167
|
MICROCODE = Microcode.table
|
|
168
|
+
|
|
169
|
+
# The instruction methods, in a fixed order a snapshot names them by.
|
|
170
|
+
OPERATIONS = MICROCODE.map(&:operation).uniq.freeze
|
|
171
|
+
|
|
172
|
+
# The plans a snapshot names that aren't an opcode's.
|
|
173
|
+
FETCH = -1
|
|
174
|
+
INTERRUPT = -2
|
|
175
|
+
JAMMED = -3
|
|
168
176
|
end
|
|
169
177
|
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class CPU
|
|
5
|
+
# The CPU's state for a snapshot: the registers, the interrupt lines and
|
|
6
|
+
# their pipeline, and the step of the instruction in flight, named by
|
|
7
|
+
# where its plan comes from.
|
|
8
|
+
module SavedState
|
|
9
|
+
# Writes everything the CPU holds, down to the step of the instruction
|
|
10
|
+
# it is in, for load_state to put back. The interrupt lines, BA stalls
|
|
11
|
+
# and SO are state too; the traps and the debug flag belong to the host.
|
|
12
|
+
def save_state(out)
|
|
13
|
+
out.marker("CPU")
|
|
14
|
+
out.int(@program_counter).int(@stack_pointer).int(@a).int(@x).int(@y).int(@status.value)
|
|
15
|
+
[@irq, @nmi, @irq_sample, @irq_pending, @nmi_sample, @nmi_pending, @skip_poll, @boundary_irq,
|
|
16
|
+
@boundary_nmi, @boundary_crossed, @brk, @pending_write, @so_high, @optional_dummy,
|
|
17
|
+
@branch_taken].each { |flag| out.boolean(flag) }
|
|
18
|
+
out.optional_int(@interrupt).optional_int(@stalled_at).int(@cycles).int(@instructions)
|
|
19
|
+
out.int(plan_source).int(@index).int(OPERATIONS.index(@operation) || -1)
|
|
20
|
+
[@address, @dummy_address, @value, @rmw_result, @pointer].each { |register| out.optional_int(register) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def load_state(input)
|
|
24
|
+
input.marker("CPU")
|
|
25
|
+
@program_counter = input.int
|
|
26
|
+
@stack_pointer = input.int
|
|
27
|
+
@a = input.int
|
|
28
|
+
@x = input.int
|
|
29
|
+
@y = input.int
|
|
30
|
+
@status.value = input.int
|
|
31
|
+
load_flags(input)
|
|
32
|
+
@interrupt = input.optional_int
|
|
33
|
+
@stalled_at = input.optional_int
|
|
34
|
+
@cycles = input.int
|
|
35
|
+
@instructions = input.int
|
|
36
|
+
load_plan(input.int)
|
|
37
|
+
@index = input.int
|
|
38
|
+
operation = input.int
|
|
39
|
+
@operation = operation.negative? ? nil : OPERATIONS.fetch(operation)
|
|
40
|
+
load_working_registers(input)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Drops the instruction or interrupt sequence in flight and the
|
|
44
|
+
# interrupt lines' pipeline, so the next cycle fetches an opcode at the
|
|
45
|
+
# program counter, and sets the cycle count. A snapshot taken between
|
|
46
|
+
# instructions, as VICE's are, restores through here.
|
|
47
|
+
def resume(cycles)
|
|
48
|
+
@irq_sample = @irq_pending = false
|
|
49
|
+
@nmi_sample = @nmi_pending = false
|
|
50
|
+
@skip_poll = false
|
|
51
|
+
@interrupt = nil
|
|
52
|
+
@brk = false
|
|
53
|
+
@stalled_at = nil
|
|
54
|
+
@cycles = cycles
|
|
55
|
+
end_sequence
|
|
56
|
+
@pending_write = false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Where the plan running comes from: an opcode's microcode, or FETCH,
|
|
62
|
+
# INTERRUPT or JAMMED below it.
|
|
63
|
+
def plan_source
|
|
64
|
+
return FETCH if @plan.equal?(FETCH_PLAN)
|
|
65
|
+
return INTERRUPT if @plan.equal?(INTERRUPT_PLAN)
|
|
66
|
+
return JAMMED if @plan.equal?(JAMMED_PLAN)
|
|
67
|
+
|
|
68
|
+
MICROCODE.index { |micro| micro.plan.equal?(@plan) && micro.operation == @operation } ||
|
|
69
|
+
MICROCODE.index { |micro| micro.plan.equal?(@plan) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def load_plan(source)
|
|
73
|
+
case source
|
|
74
|
+
when FETCH then use_plan(FETCH_PLAN, FETCH_WRITES)
|
|
75
|
+
when INTERRUPT then use_plan(INTERRUPT_PLAN, INTERRUPT_WRITES)
|
|
76
|
+
when JAMMED then use_plan(JAMMED_PLAN, JAMMED_WRITES)
|
|
77
|
+
else
|
|
78
|
+
micro = MICROCODE.fetch(source)
|
|
79
|
+
use_plan(micro.plan, micro.writes)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def use_plan(plan, writes)
|
|
84
|
+
@plan = plan
|
|
85
|
+
@writes = writes
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def load_flags(input)
|
|
89
|
+
@irq = input.boolean?
|
|
90
|
+
@nmi = input.boolean?
|
|
91
|
+
@irq_sample = input.boolean?
|
|
92
|
+
@irq_pending = input.boolean?
|
|
93
|
+
@nmi_sample = input.boolean?
|
|
94
|
+
@nmi_pending = input.boolean?
|
|
95
|
+
@skip_poll = input.boolean?
|
|
96
|
+
@boundary_irq = input.boolean?
|
|
97
|
+
@boundary_nmi = input.boolean?
|
|
98
|
+
@boundary_crossed = input.boolean?
|
|
99
|
+
@brk = input.boolean?
|
|
100
|
+
@pending_write = input.boolean?
|
|
101
|
+
@so_high = input.boolean?
|
|
102
|
+
@optional_dummy = input.boolean?
|
|
103
|
+
@branch_taken = input.boolean?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def load_working_registers(input)
|
|
107
|
+
@address = input.optional_int
|
|
108
|
+
@dummy_address = input.optional_int
|
|
109
|
+
@value = input.optional_int
|
|
110
|
+
@rmw_result = input.optional_int
|
|
111
|
+
@pointer = input.optional_int
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
data/lib/badline/cpu.rb
CHANGED
|
@@ -1,219 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "badline/cpu/
|
|
4
|
-
require "badline/cpu/addressing"
|
|
5
|
-
require "badline/cpu/operations"
|
|
6
|
-
require "badline/cpu/stack_operations"
|
|
3
|
+
require "badline/cpu/core"
|
|
7
4
|
|
|
8
5
|
module Badline
|
|
6
|
+
# The C64's 6510, on the AddressBus.
|
|
9
7
|
class CPU
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
include IntegerHelper
|
|
13
|
-
include InstructionSet
|
|
14
|
-
include Interrupts
|
|
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
|
|
22
|
-
|
|
23
|
-
alias pending_write? pending_write
|
|
24
|
-
|
|
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, debug: false, ane_constant: 0xef)
|
|
28
|
-
@debug = debug
|
|
29
|
-
@ane_constant = ane_constant
|
|
30
|
-
@memory = memory
|
|
31
|
-
@status = CPUStatus.new(STATUS_FLAGS, value: 0b00100000)
|
|
32
|
-
reset_registers
|
|
33
|
-
|
|
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
|
|
43
|
-
@so_high = true
|
|
44
|
-
|
|
45
|
-
@cycles = 0
|
|
46
|
-
@instructions = 0
|
|
47
|
-
@traps = nil
|
|
48
|
-
end_sequence
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def reset!
|
|
52
|
-
status.interrupt = true
|
|
53
|
-
reset_registers
|
|
54
|
-
@nmi = false
|
|
55
|
-
@irq_sample = @irq_pending = false
|
|
56
|
-
@nmi_sample = @nmi_pending = false
|
|
57
|
-
@skip_poll = false
|
|
58
|
-
@pending_write = false
|
|
59
|
-
end_sequence
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def p
|
|
63
|
-
status.value
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def p=(new_value)
|
|
67
|
-
status.value = new_value
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def cycle!
|
|
71
|
-
poll # the interrupt lines
|
|
72
|
-
|
|
73
|
-
# Run the next step
|
|
74
|
-
index = @index
|
|
75
|
-
@index = index + 1
|
|
76
|
-
send(@plan[index])
|
|
77
|
-
@cycles += 1
|
|
78
|
-
|
|
79
|
-
# Record if the next step writes to memory
|
|
80
|
-
@pending_write = @writes[@index]
|
|
81
|
-
nil
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Called instead of #cycle! on a cycle the VIC holds the CPU through
|
|
85
|
-
# BA. Records which cycle the CPU was stalled before, and keeps
|
|
86
|
-
# sampling the interrupt lines.
|
|
87
|
-
def stall!
|
|
88
|
-
@stalled_at = @cycles
|
|
89
|
-
sample_while_stalled
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# The 6502's SO (set overflow) pin, which the 6510 leaves out, so the
|
|
93
|
-
# C64 never drives it. A falling edge sets V and a held level does
|
|
94
|
-
# nothing more.
|
|
95
|
-
def so=(high)
|
|
96
|
-
@status.overflow = true if @so_high && !high
|
|
97
|
-
@so_high = high
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# Pulses SO: pulls it low, which sets V, and releases it.
|
|
101
|
-
def so!
|
|
102
|
-
@status.overflow = true
|
|
103
|
-
@so_high = true
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def jammed?
|
|
107
|
-
@plan.equal?(JAMMED_PLAN)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
# Runs to the end of the instruction, or until the CPU jams.
|
|
111
|
-
def step!
|
|
112
|
-
cycle!
|
|
113
|
-
cycle! until @plan.equal?(FETCH_PLAN) || jammed?
|
|
114
|
-
nil
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Whether the next cycle fetches an opcode.
|
|
118
|
-
def boundary? = @index.zero? && @plan.equal?(FETCH_PLAN)
|
|
119
|
-
|
|
120
|
-
def trapped? = !@traps.nil?
|
|
121
|
-
|
|
122
|
-
# Everything the CPU holds but its cycle and instruction counts: the
|
|
123
|
-
# registers, the interrupt lines and pipeline, and what the last
|
|
124
|
-
# instruction left in its working registers. Drive1541::Idle compares
|
|
125
|
-
# it at two instruction boundaries.
|
|
126
|
-
def idle_state
|
|
127
|
-
[@program_counter, @stack_pointer, @a, @x, @y, @status.value,
|
|
128
|
-
@irq, @nmi, @irq_sample, @irq_pending, @nmi_sample, @nmi_pending, @skip_poll,
|
|
129
|
-
@boundary_irq, @boundary_nmi, @so_high, @interrupt, @brk, @pending_write, @stalled_at, @traps,
|
|
130
|
-
@operation, @optional_dummy, @boundary_crossed, @branch_taken,
|
|
131
|
-
@address, @pointer, @dummy_address, @value, @rmw_result]
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# Counts +cycles+ cycles and +instructions+ instructions that ran
|
|
135
|
-
# without changing anything idle_state holds.
|
|
136
|
-
def fast_forward(cycles, instructions)
|
|
137
|
-
@cycles += cycles
|
|
138
|
-
@instructions += instructions
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def inspect
|
|
142
|
-
"Cycles: #{@cycles}, PC: #{format16(program_counter)}, " \
|
|
143
|
-
"SP: #{format8(stack_pointer)}, A: #{format8(a)}, X: #{format8(x)}, " \
|
|
144
|
-
"Y: #{format8(y)}, P: #{format8(p)}"
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
private
|
|
148
|
-
|
|
149
|
-
# The first cycle of every instruction. Starts an interrupt if one was
|
|
150
|
-
# pending when the previous instruction ended. Otherwise reads the
|
|
151
|
-
# opcode and switches to its plan.
|
|
152
|
-
def op_fetch
|
|
153
|
-
return start_interrupt if @boundary_nmi || @boundary_irq
|
|
154
|
-
|
|
155
|
-
run_traps
|
|
156
|
-
opcode = @memory.peek(@program_counter)
|
|
157
|
-
log(opcode) if @debug
|
|
158
|
-
micro = MICROCODE[opcode]
|
|
159
|
-
@operation = micro.operation
|
|
160
|
-
@plan = micro.plan
|
|
161
|
-
@writes = micro.writes
|
|
162
|
-
@optional_dummy = micro.optional_dummy
|
|
163
|
-
@index = 1
|
|
164
|
-
@boundary_crossed = false
|
|
165
|
-
@program_counter = (@program_counter + 1) & 0xffff
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def end_instruction
|
|
169
|
-
@instructions += 1
|
|
170
|
-
end_sequence
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# Makes the next cycle an opcode fetch, and saves the pending interrupt
|
|
174
|
-
# flags for op_fetch. The next cycle polls before op_fetch runs, which
|
|
175
|
-
# updates the live flags.
|
|
176
|
-
def end_sequence
|
|
177
|
-
@plan = FETCH_PLAN
|
|
178
|
-
@writes = FETCH_WRITES
|
|
179
|
-
@index = 0
|
|
180
|
-
@boundary_irq = @irq_pending
|
|
181
|
-
@boundary_nmi = @nmi_pending
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# True when the CPU was stalled right before this cycle.
|
|
185
|
-
def stalled_before_this_cycle?
|
|
186
|
-
@stalled_at == @cycles
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
# True when the CPU was stalled right before the cycle that preceded
|
|
190
|
-
# this one.
|
|
191
|
-
def stalled_before_previous_cycle?
|
|
192
|
-
@stalled_at == @cycles - 1
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def write_byte(addr, value)
|
|
196
|
-
@memory.poke(addr, value)
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def stack_address(offset = 0)
|
|
200
|
-
0x0100 | ((@stack_pointer + offset) & 0xff)
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def reset_registers
|
|
204
|
-
# The program counter is initialized from the reset vector
|
|
205
|
-
@program_counter = @memory.peek16(0xfffc)
|
|
206
|
-
# Stack pointer starts at 0x01ff and grows down
|
|
207
|
-
@stack_pointer = 0xff
|
|
208
|
-
@a = @x = @y = 0x0
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def log(opcode)
|
|
212
|
-
instruction = Instruction.find(opcode)
|
|
213
|
-
puts(
|
|
214
|
-
"#{@cycles}: PC: #{format16(@program_counter)} - " \
|
|
215
|
-
"#{instruction.name.upcase} #{instruction.addressing_mode}"
|
|
216
|
-
)
|
|
217
|
-
end
|
|
8
|
+
include Core
|
|
218
9
|
end
|
|
219
10
|
end
|
data/lib/badline/datasette.rb
CHANGED
|
@@ -54,6 +54,33 @@ module Badline
|
|
|
54
54
|
|
|
55
55
|
def running? = @playing && @motor && !@tape.nil?
|
|
56
56
|
|
|
57
|
+
# The keys, the motor, the countdown to the next pulse and the tape: its
|
|
58
|
+
# path and how far it has played.
|
|
59
|
+
def save_state(out)
|
|
60
|
+
out.marker("DATASETTE")
|
|
61
|
+
out.boolean(@playing).boolean(@motor).int(@countdown).boolean(!@tape.nil?)
|
|
62
|
+
return unless @tape
|
|
63
|
+
|
|
64
|
+
out.string(@tape.path).int(@tape.position)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# A tape from another file goes in from its path, and a detached
|
|
68
|
+
# reader leaves the tape out. The sense and flag handlers don't fire.
|
|
69
|
+
def load_state(input)
|
|
70
|
+
input.marker("DATASETTE")
|
|
71
|
+
@playing = input.boolean?
|
|
72
|
+
@motor = input.boolean?
|
|
73
|
+
@countdown = input.int
|
|
74
|
+
return @tape = nil unless input.boolean?
|
|
75
|
+
|
|
76
|
+
path = input.string
|
|
77
|
+
position = input.int
|
|
78
|
+
return @tape = nil if input.detached?
|
|
79
|
+
|
|
80
|
+
@tape = Storage::TAP.new(path) unless @tape&.path&.b == path
|
|
81
|
+
@tape.position = position
|
|
82
|
+
end
|
|
83
|
+
|
|
57
84
|
def cycle!
|
|
58
85
|
return unless running?
|
|
59
86
|
|
|
@@ -94,6 +94,19 @@ module Badline
|
|
|
94
94
|
watching!
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
+
# The last byte on the data bus and the RAM. What a watched stretch
|
|
98
|
+
# touched is Idle's, and a restored bus isn't watching.
|
|
99
|
+
def save_state(out)
|
|
100
|
+
out.int(@data)
|
|
101
|
+
@ram.save_state(out)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def load_state(input)
|
|
105
|
+
@data = input.int
|
|
106
|
+
@ram.load_state(input)
|
|
107
|
+
@watching = false
|
|
108
|
+
end
|
|
109
|
+
|
|
97
110
|
def peek(addr)
|
|
98
111
|
return @data = @rom.peek(addr | 0x4000) if addr >= 0x8000
|
|
99
112
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1541
|
|
5
|
+
# The drive's 6502, on the drive's Bus.
|
|
6
|
+
class CPU
|
|
7
|
+
include Badline::CPU::Core
|
|
8
|
+
|
|
9
|
+
# Everything the CPU holds but its cycle and instruction counts: the
|
|
10
|
+
# registers, the interrupt lines and pipeline, and what the last
|
|
11
|
+
# instruction left in its working registers. Drive1541::Idle compares
|
|
12
|
+
# it at two instruction boundaries.
|
|
13
|
+
def idle_state
|
|
14
|
+
[@program_counter, @stack_pointer, @a, @x, @y, @status.value,
|
|
15
|
+
@irq, @nmi, @irq_sample, @irq_pending, @nmi_sample, @nmi_pending, @skip_poll,
|
|
16
|
+
@boundary_irq, @boundary_nmi, @so_high, @interrupt, @brk, @pending_write, @stalled_at, @traps,
|
|
17
|
+
@operation, @optional_dummy, @boundary_crossed, @branch_taken,
|
|
18
|
+
@address, @pointer, @dummy_address, @value, @rmw_result]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Counts +cycles+ cycles and +instructions+ instructions that ran
|
|
22
|
+
# without changing anything idle_state holds.
|
|
23
|
+
def fast_forward(cycles, instructions)
|
|
24
|
+
@cycles += cycles
|
|
25
|
+
@instructions += instructions
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "badline/drive1541/sector_reader"
|
|
4
|
+
require "badline/drive1541/disk_state"
|
|
4
5
|
|
|
5
6
|
module Badline
|
|
6
7
|
class Drive1541
|
|
@@ -16,6 +17,8 @@ module Badline
|
|
|
16
17
|
# host file. The disk is write-protected when the image won't take
|
|
17
18
|
# writes.
|
|
18
19
|
class Disk
|
|
20
|
+
include State
|
|
21
|
+
|
|
19
22
|
MAX_HALF_TRACK = 84
|
|
20
23
|
|
|
21
24
|
# GCR bytes around a track in each speed zone: 200 ms of rotation at
|
|
@@ -52,9 +55,9 @@ module Badline
|
|
|
52
55
|
# anything else as a .d64, formatted. `read_only` opens the image
|
|
53
56
|
# write-protected.
|
|
54
57
|
def self.open(path, read_only: false)
|
|
55
|
-
return from_g64(Storage::G64Image.new(path, read_only:))
|
|
58
|
+
return from_g64(Storage::G64Image.new(path, read_only:)).opened(path, read_only) if State.g64?(path)
|
|
56
59
|
|
|
57
|
-
from_d64(Storage::D64Image.new(path, read_only:))
|
|
60
|
+
from_d64(Storage::D64Image.new(path, read_only:)).opened(path, read_only)
|
|
58
61
|
end
|
|
59
62
|
|
|
60
63
|
# A disk from a G64 image, each half track as the image stores it,
|
|
@@ -145,6 +148,8 @@ module Badline
|
|
|
145
148
|
@image = image
|
|
146
149
|
@tracks = Array.new(MAX_HALF_TRACK + 1)
|
|
147
150
|
@written = {}
|
|
151
|
+
@path = nil
|
|
152
|
+
@read_only = false
|
|
148
153
|
end
|
|
149
154
|
|
|
150
155
|
# The Track at the half track, or nil where there's no data.
|