badline 0.6.0 → 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 +40 -0
- data/CLAUDE.md +10 -9
- data/README.md +24 -10
- data/doc/pinned-behaviour.md +131 -30
- data/exe/badline-ruby +20 -24
- data/lib/badline/address_bus.rb +10 -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/computer.rb +6 -3
- data/lib/badline/cpu/boundary.rb +54 -0
- data/lib/badline/cpu/core.rb +165 -0
- data/lib/badline/cpu.rb +3 -214
- data/lib/badline/drive1541/cpu.rb +29 -0
- data/lib/badline/drive1541.rb +1 -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/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 -145
- data/lib/badline/reu/dma/saved_state.rb +50 -0
- data/lib/badline/reu/dma.rb +56 -47
- data/lib/badline/reu/saved_state.rb +2 -1
- data/lib/badline/reu.rb +19 -10
- data/lib/badline/sdl.rb +123 -51
- data/lib/badline/sid/waveform/noise_writeback.rb +33 -11
- data/lib/badline/sid/waveform.rb +2 -3
- data/lib/badline/snapshot/state.rb +1 -1
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/bank.rb +21 -0
- data/lib/badline/vic.rb +15 -6
- metadata +26 -19
- 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 -123
- data/lib/badline/gui/application.rb +0 -243
- data/lib/badline/gui/drive_led_pane.rb +0 -29
- data/lib/badline/gui/gamepads.rb +0 -104
- 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/snapshots.rb +0 -67
- data/lib/badline/gui/window.rb +0 -61
- data/lib/badline/gui.rb +0 -14
- data/lib/badline/sdl/events.rb +0 -49
- 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
|
data/lib/badline/cpu.rb
CHANGED
|
@@ -1,221 +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"
|
|
7
|
-
require "badline/cpu/saved_state"
|
|
3
|
+
require "badline/cpu/core"
|
|
8
4
|
|
|
9
5
|
module Badline
|
|
6
|
+
# The C64's 6510, on the AddressBus.
|
|
10
7
|
class CPU
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
include IntegerHelper
|
|
14
|
-
include InstructionSet
|
|
15
|
-
include Interrupts
|
|
16
|
-
include Traps
|
|
17
|
-
include Addressing
|
|
18
|
-
include Operations
|
|
19
|
-
include StackOperations
|
|
20
|
-
include SavedState
|
|
21
|
-
|
|
22
|
-
attr_reader :memory, :instructions, :boundary_crossed, :cycles, :pending_write
|
|
23
|
-
attr_accessor :program_counter, :stack_pointer, :status, :a, :x, :y, :nmi, :irq
|
|
24
|
-
|
|
25
|
-
alias pending_write? pending_write
|
|
26
|
-
|
|
27
|
-
# +ane_constant+ is ANE's magic constant, which varies from chip to
|
|
28
|
-
# chip. The default is the C64 6510's.
|
|
29
|
-
def initialize(memory, debug: false, ane_constant: 0xef)
|
|
30
|
-
@debug = debug
|
|
31
|
-
@ane_constant = ane_constant
|
|
32
|
-
@memory = memory
|
|
33
|
-
@status = CPUStatus.new(STATUS_FLAGS, value: 0b00100000)
|
|
34
|
-
reset_registers
|
|
35
|
-
|
|
36
|
-
@nmi = @irq = false
|
|
37
|
-
@irq_sample = @irq_pending = false
|
|
38
|
-
@nmi_sample = @nmi_pending = false
|
|
39
|
-
@skip_poll = false
|
|
40
|
-
@boundary_crossed = false
|
|
41
|
-
@interrupt = nil
|
|
42
|
-
@brk = false
|
|
43
|
-
@pending_write = false
|
|
44
|
-
@stalled_at = nil
|
|
45
|
-
@so_high = true
|
|
46
|
-
|
|
47
|
-
@cycles = 0
|
|
48
|
-
@instructions = 0
|
|
49
|
-
@traps = nil
|
|
50
|
-
end_sequence
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def reset!
|
|
54
|
-
status.interrupt = true
|
|
55
|
-
reset_registers
|
|
56
|
-
@nmi = false
|
|
57
|
-
@irq_sample = @irq_pending = false
|
|
58
|
-
@nmi_sample = @nmi_pending = false
|
|
59
|
-
@skip_poll = false
|
|
60
|
-
@pending_write = false
|
|
61
|
-
end_sequence
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def p
|
|
65
|
-
status.value
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def p=(new_value)
|
|
69
|
-
status.value = new_value
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def cycle!
|
|
73
|
-
poll # the interrupt lines
|
|
74
|
-
|
|
75
|
-
# Run the next step
|
|
76
|
-
index = @index
|
|
77
|
-
@index = index + 1
|
|
78
|
-
send(@plan[index])
|
|
79
|
-
@cycles += 1
|
|
80
|
-
|
|
81
|
-
# Record if the next step writes to memory
|
|
82
|
-
@pending_write = @writes[@index]
|
|
83
|
-
nil
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Called instead of #cycle! on a cycle the VIC holds the CPU through
|
|
87
|
-
# BA. Records which cycle the CPU was stalled before, and keeps
|
|
88
|
-
# sampling the interrupt lines.
|
|
89
|
-
def stall!
|
|
90
|
-
@stalled_at = @cycles
|
|
91
|
-
sample_while_stalled
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# The 6502's SO (set overflow) pin, which the 6510 leaves out, so the
|
|
95
|
-
# C64 never drives it. A falling edge sets V and a held level does
|
|
96
|
-
# nothing more.
|
|
97
|
-
def so=(high)
|
|
98
|
-
@status.overflow = true if @so_high && !high
|
|
99
|
-
@so_high = high
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Pulses SO: pulls it low, which sets V, and releases it.
|
|
103
|
-
def so!
|
|
104
|
-
@status.overflow = true
|
|
105
|
-
@so_high = true
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def jammed?
|
|
109
|
-
@plan.equal?(JAMMED_PLAN)
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Runs to the end of the instruction, or until the CPU jams.
|
|
113
|
-
def step!
|
|
114
|
-
cycle!
|
|
115
|
-
cycle! until @plan.equal?(FETCH_PLAN) || jammed?
|
|
116
|
-
nil
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Whether the next cycle fetches an opcode.
|
|
120
|
-
def boundary? = @index.zero? && @plan.equal?(FETCH_PLAN)
|
|
121
|
-
|
|
122
|
-
def trapped? = !@traps.nil?
|
|
123
|
-
|
|
124
|
-
# Everything the CPU holds but its cycle and instruction counts: the
|
|
125
|
-
# registers, the interrupt lines and pipeline, and what the last
|
|
126
|
-
# instruction left in its working registers. Drive1541::Idle compares
|
|
127
|
-
# it at two instruction boundaries.
|
|
128
|
-
def idle_state
|
|
129
|
-
[@program_counter, @stack_pointer, @a, @x, @y, @status.value,
|
|
130
|
-
@irq, @nmi, @irq_sample, @irq_pending, @nmi_sample, @nmi_pending, @skip_poll,
|
|
131
|
-
@boundary_irq, @boundary_nmi, @so_high, @interrupt, @brk, @pending_write, @stalled_at, @traps,
|
|
132
|
-
@operation, @optional_dummy, @boundary_crossed, @branch_taken,
|
|
133
|
-
@address, @pointer, @dummy_address, @value, @rmw_result]
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Counts +cycles+ cycles and +instructions+ instructions that ran
|
|
137
|
-
# without changing anything idle_state holds.
|
|
138
|
-
def fast_forward(cycles, instructions)
|
|
139
|
-
@cycles += cycles
|
|
140
|
-
@instructions += instructions
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def inspect
|
|
144
|
-
"Cycles: #{@cycles}, PC: #{format16(program_counter)}, " \
|
|
145
|
-
"SP: #{format8(stack_pointer)}, A: #{format8(a)}, X: #{format8(x)}, " \
|
|
146
|
-
"Y: #{format8(y)}, P: #{format8(p)}"
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
private
|
|
150
|
-
|
|
151
|
-
# The first cycle of every instruction. Starts an interrupt if one was
|
|
152
|
-
# pending when the previous instruction ended. Otherwise reads the
|
|
153
|
-
# opcode and switches to its plan.
|
|
154
|
-
def op_fetch
|
|
155
|
-
return start_interrupt if @boundary_nmi || @boundary_irq
|
|
156
|
-
|
|
157
|
-
run_traps
|
|
158
|
-
opcode = @memory.peek(@program_counter)
|
|
159
|
-
log(opcode) if @debug
|
|
160
|
-
micro = MICROCODE[opcode]
|
|
161
|
-
@operation = micro.operation
|
|
162
|
-
@plan = micro.plan
|
|
163
|
-
@writes = micro.writes
|
|
164
|
-
@optional_dummy = micro.optional_dummy
|
|
165
|
-
@index = 1
|
|
166
|
-
@boundary_crossed = false
|
|
167
|
-
@program_counter = (@program_counter + 1) & 0xffff
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def end_instruction
|
|
171
|
-
@instructions += 1
|
|
172
|
-
end_sequence
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# Makes the next cycle an opcode fetch, and saves the pending interrupt
|
|
176
|
-
# flags for op_fetch. The next cycle polls before op_fetch runs, which
|
|
177
|
-
# updates the live flags.
|
|
178
|
-
def end_sequence
|
|
179
|
-
@plan = FETCH_PLAN
|
|
180
|
-
@writes = FETCH_WRITES
|
|
181
|
-
@index = 0
|
|
182
|
-
@boundary_irq = @irq_pending
|
|
183
|
-
@boundary_nmi = @nmi_pending
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
# True when the CPU was stalled right before this cycle.
|
|
187
|
-
def stalled_before_this_cycle?
|
|
188
|
-
@stalled_at == @cycles
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# True when the CPU was stalled right before the cycle that preceded
|
|
192
|
-
# this one.
|
|
193
|
-
def stalled_before_previous_cycle?
|
|
194
|
-
@stalled_at == @cycles - 1
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def write_byte(addr, value)
|
|
198
|
-
@memory.poke(addr, value)
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def stack_address(offset = 0)
|
|
202
|
-
0x0100 | ((@stack_pointer + offset) & 0xff)
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def reset_registers
|
|
206
|
-
# The program counter is initialized from the reset vector
|
|
207
|
-
@program_counter = @memory.peek16(0xfffc)
|
|
208
|
-
# Stack pointer starts at 0x01ff and grows down
|
|
209
|
-
@stack_pointer = 0xff
|
|
210
|
-
@a = @x = @y = 0x0
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def log(opcode)
|
|
214
|
-
instruction = Instruction.find(opcode)
|
|
215
|
-
puts(
|
|
216
|
-
"#{@cycles}: PC: #{format16(@program_counter)} - " \
|
|
217
|
-
"#{instruction.name.upcase} #{instruction.addressing_mode}"
|
|
218
|
-
)
|
|
219
|
-
end
|
|
8
|
+
include Core
|
|
220
9
|
end
|
|
221
10
|
end
|
|
@@ -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
|
data/lib/badline/drive1541.rb
CHANGED
data/lib/badline/ffi.rb
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fiddle"
|
|
4
|
+
|
|
5
|
+
module Badline
|
|
6
|
+
# The part of Spinel's FFI DSL the SDL binding uses, for CRuby, over
|
|
7
|
+
# Fiddle, so the one binding in badline/sdl serves both builds. Spinel
|
|
8
|
+
# compiles the declarations itself and never loads this file: require it
|
|
9
|
+
# before a file that declares a binding.
|
|
10
|
+
#
|
|
11
|
+
# The declarations behave as they do in Spinel. ffi_func defines
|
|
12
|
+
# Module.name(...); a pointer argument takes nil for NULL, a Fiddle
|
|
13
|
+
# pointer, a String (its bytes) or an IO::Buffer, whose contents C sees
|
|
14
|
+
# for the call's duration; a NULL pointer comes back as nil. ffi_const
|
|
15
|
+
# sets Module::NAME, ffi_buffer defines Module.name returning a zeroed
|
|
16
|
+
# static buffer, and ffi_read_<width> and ffi_write_<width> define field
|
|
17
|
+
# accessors at a fixed offset into a pointer.
|
|
18
|
+
module FFI
|
|
19
|
+
class Error < StandardError; end
|
|
20
|
+
|
|
21
|
+
TYPES = {
|
|
22
|
+
void: Fiddle::TYPE_VOID,
|
|
23
|
+
int: Fiddle::TYPE_INT, bool: Fiddle::TYPE_INT, long: Fiddle::TYPE_LONG, size_t: Fiddle::TYPE_SIZE_T,
|
|
24
|
+
int8: Fiddle::TYPE_INT8_T, uint8: Fiddle::TYPE_UINT8_T, int16: Fiddle::TYPE_INT16_T,
|
|
25
|
+
uint16: Fiddle::TYPE_UINT16_T, int32: Fiddle::TYPE_INT32_T, uint32: Fiddle::TYPE_UINT32_T,
|
|
26
|
+
float: Fiddle::TYPE_FLOAT, double: Fiddle::TYPE_DOUBLE,
|
|
27
|
+
str: Fiddle::TYPE_CONST_STRING,
|
|
28
|
+
ptr: Fiddle::TYPE_VOIDP, buffer_in: Fiddle::TYPE_VOIDP, buffer_out: Fiddle::TYPE_VOIDP,
|
|
29
|
+
buffer_inout: Fiddle::TYPE_VOIDP, int_array: Fiddle::TYPE_VOIDP, float_array: Fiddle::TYPE_VOIDP
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
# The argument specs C may write through, so an IO::Buffer passed in one
|
|
33
|
+
# gets back what C left in it.
|
|
34
|
+
WRITABLE = %i[ptr buffer_out buffer_inout].freeze
|
|
35
|
+
|
|
36
|
+
# pack directives for the accessor widths, in the host's byte order.
|
|
37
|
+
WIDTHS = {
|
|
38
|
+
u8: "C", i8: "c", u16: "S", i16: "s", u32: "L", i32: "l", u64: "Q", i64: "q", ptr: "J"
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
# Where dlopen looks for a library besides its search path, which leaves
|
|
42
|
+
# out Homebrew's, and the versioned names a runtime package installs
|
|
43
|
+
# without the development package's unversioned link.
|
|
44
|
+
DIRECTORIES = [nil, "/opt/homebrew/lib", "/usr/local/lib"].freeze
|
|
45
|
+
VERSIONED = { "SDL2" => %w[libSDL2-2.0.so.0 libSDL2-2.0.0.dylib] }.freeze
|
|
46
|
+
|
|
47
|
+
module_function
|
|
48
|
+
|
|
49
|
+
def open_library(name)
|
|
50
|
+
candidates = library_candidates(name)
|
|
51
|
+
candidates.each do |candidate|
|
|
52
|
+
return Fiddle.dlopen(candidate)
|
|
53
|
+
rescue Fiddle::DLError
|
|
54
|
+
next
|
|
55
|
+
end
|
|
56
|
+
raise Error, "can't find lib#{name} (tried #{candidates.join(', ')})"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def library_candidates(name)
|
|
60
|
+
names = ["lib#{name}.dylib", "lib#{name}.so", *VERSIONED.fetch(name, []), "#{name}.dll"]
|
|
61
|
+
DIRECTORIES.flat_map { |dir| names.map { |file| dir ? File.join(dir, file) : file } }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The value Fiddle passes for one argument, and a block that copies what
|
|
65
|
+
# C wrote back into an IO::Buffer once the call returns.
|
|
66
|
+
def argument(spec, value)
|
|
67
|
+
case value
|
|
68
|
+
when IO::Buffer then buffer_argument(spec, value)
|
|
69
|
+
when Array then [value.pack(spec == :float_array ? "d*" : "q*")]
|
|
70
|
+
when true, false then [value ? 1 : 0]
|
|
71
|
+
else [value]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def buffer_argument(spec, buffer)
|
|
76
|
+
return [nil] if buffer.null? || buffer.empty?
|
|
77
|
+
|
|
78
|
+
bytes = buffer.get_string
|
|
79
|
+
return [bytes] unless WRITABLE.include?(spec) && !buffer.readonly?
|
|
80
|
+
|
|
81
|
+
[bytes, -> { buffer.set_string(bytes) }]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def result(spec, value)
|
|
85
|
+
case spec
|
|
86
|
+
when :ptr then value.null? ? nil : value
|
|
87
|
+
when :bool then value != 0
|
|
88
|
+
else value
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def read(pointer, offset, width)
|
|
93
|
+
value = Fiddle::Pointer[pointer][offset, width_size(width)].unpack1(WIDTHS.fetch(width))
|
|
94
|
+
width == :ptr ? result(:ptr, Fiddle::Pointer.new(value)) : value
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def write(pointer, offset, width, value)
|
|
98
|
+
value = value.to_i if width == :ptr
|
|
99
|
+
Fiddle::Pointer[pointer][offset, width_size(width)] = [value || 0].pack(WIDTHS.fetch(width))
|
|
100
|
+
value
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def width_size(width) = [0].pack(WIDTHS.fetch(width)).bytesize
|
|
104
|
+
|
|
105
|
+
# The declarations, private methods of every module once this file is
|
|
106
|
+
# loaded, as they are keywords of a module body in Spinel.
|
|
107
|
+
module DSL
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def ffi_lib(name)
|
|
111
|
+
(@ffi_libraries ||= []) << FFI.open_library(name)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def ffi_func(name, arguments, returns, blocking: false)
|
|
115
|
+
function = Fiddle::Function.new(ffi_symbol(name), arguments.map { |spec| TYPES.fetch(spec) },
|
|
116
|
+
TYPES.fetch(returns), name: name.to_s, need_gvl: !blocking)
|
|
117
|
+
define_singleton_method(name) do |*values|
|
|
118
|
+
converted = arguments.zip(values).map { |spec, value| FFI.argument(spec, value) }
|
|
119
|
+
returned = function.call(*converted.map(&:first))
|
|
120
|
+
converted.each { |argument| argument[1]&.call }
|
|
121
|
+
FFI.result(returns, returned)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def ffi_const(name, value)
|
|
126
|
+
const_set(name, value)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def ffi_buffer(name, size)
|
|
130
|
+
buffer = Fiddle::Pointer.malloc(size, Fiddle::RUBY_FREE)
|
|
131
|
+
buffer[0, size] = "\0" * size
|
|
132
|
+
define_singleton_method(name) { buffer }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
WIDTHS.each_key do |width|
|
|
136
|
+
define_method(:"ffi_read_#{width}") do |name, offset|
|
|
137
|
+
define_singleton_method(name) { |pointer| FFI.read(pointer, offset, width) }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
define_method(:"ffi_write_#{width}") do |name, offset|
|
|
141
|
+
define_singleton_method(name) { |pointer, value| FFI.write(pointer, offset, width, value) }
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def ffi_symbol(name)
|
|
146
|
+
(@ffi_libraries || []).each do |library|
|
|
147
|
+
return library[name.to_s]
|
|
148
|
+
rescue Fiddle::DLError
|
|
149
|
+
next
|
|
150
|
+
end
|
|
151
|
+
Fiddle::Handle::DEFAULT[name.to_s]
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
Module.include(Badline::FFI::DSL)
|