ac6502 1.0.0 → 1.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.
- package/README.md +2 -2
- package/dist/components/IO/EmptyCard.js +17 -0
- package/dist/components/IO/EmptyCard.js.map +1 -0
- package/dist/components/IO/GPIOAttachments/GPIOKeypadAttachment.js +141 -0
- package/dist/components/IO/GPIOAttachments/GPIOKeypadAttachment.js.map +1 -0
- package/dist/components/IO/GPIOAttachments/GPIOLCDAttachment.js +716 -0
- package/dist/components/IO/GPIOAttachments/GPIOLCDAttachment.js.map +1 -0
- package/dist/components/IO/SerialCard.js +4 -4
- package/dist/components/IO/SerialCard.js.map +1 -1
- package/dist/components/Machine.js +84 -45
- package/dist/components/Machine.js.map +1 -1
- package/dist/index.js +175 -52
- package/dist/index.js.map +1 -1
- package/dist/tests/IO/GPIOAttachments/GPIOKeypadAttachment.test.js +323 -0
- package/dist/tests/IO/GPIOAttachments/GPIOKeypadAttachment.test.js.map +1 -0
- package/dist/tests/IO/GPIOAttachments/GPIOLCDAttachment.test.js +627 -0
- package/dist/tests/IO/GPIOAttachments/GPIOLCDAttachment.test.js.map +1 -0
- package/dist/tests/IO/SerialCard.test.js +4 -4
- package/dist/tests/IO/SerialCard.test.js.map +1 -1
- package/dist/tests/Machine.test.js +9 -3
- package/dist/tests/Machine.test.js.map +1 -1
- package/package.json +3 -3
- package/src/components/IO/EmptyCard.ts +16 -0
- package/src/components/IO/GPIOAttachments/GPIOKeypadAttachment.ts +153 -0
- package/src/components/IO/GPIOAttachments/GPIOLCDAttachment.ts +791 -0
- package/src/components/IO/SerialCard.ts +4 -4
- package/src/components/Machine.ts +107 -61
- package/src/index.ts +179 -87
- package/src/tests/IO/GPIOAttachments/GPIOKeypadAttachment.test.ts +389 -0
- package/src/tests/IO/GPIOAttachments/GPIOLCDAttachment.test.ts +795 -0
- package/src/tests/IO/SerialCard.test.ts +4 -4
- package/src/tests/Machine.test.ts +10 -3
|
@@ -49,11 +49,11 @@ export class SerialCard implements IO {
|
|
|
49
49
|
case 0x01: // Status Register
|
|
50
50
|
return this.readStatus()
|
|
51
51
|
|
|
52
|
-
case 0x02: // Command Register
|
|
53
|
-
return
|
|
52
|
+
case 0x02: // Command Register
|
|
53
|
+
return this.commandRegister
|
|
54
54
|
|
|
55
|
-
case 0x03: // Control Register
|
|
56
|
-
return
|
|
55
|
+
case 0x03: // Control Register
|
|
56
|
+
return this.controlRegister
|
|
57
57
|
|
|
58
58
|
default:
|
|
59
59
|
return 0
|
|
@@ -12,6 +12,10 @@ import { VideoCard } from './IO/VideoCard'
|
|
|
12
12
|
import { GPIOKeyboardMatrixAttachment } from './IO/GPIOAttachments/GPIOKeyboardMatrixAttachment'
|
|
13
13
|
import { GPIOKeyboardEncoderAttachment } from './IO/GPIOAttachments/GPIOKeyboardEncoderAttachment'
|
|
14
14
|
import { GPIOJoystickAttachment } from './IO/GPIOAttachments/GPIOJoystickAttachment'
|
|
15
|
+
import { GPIOLCDAttachment } from './IO/GPIOAttachments/GPIOLCDAttachment'
|
|
16
|
+
import { GPIOKeypadAttachment } from './IO/GPIOAttachments/GPIOKeypadAttachment'
|
|
17
|
+
import { EmptyCard } from './IO/EmptyCard'
|
|
18
|
+
import { IO } from './IO'
|
|
15
19
|
import { readFile } from 'fs/promises'
|
|
16
20
|
|
|
17
21
|
export class Machine {
|
|
@@ -25,21 +29,27 @@ export class Machine {
|
|
|
25
29
|
cpu: CPU
|
|
26
30
|
ram: RAM
|
|
27
31
|
rom: ROM
|
|
28
|
-
io1:
|
|
29
|
-
io2:
|
|
30
|
-
io3:
|
|
31
|
-
io4:
|
|
32
|
+
io1: IO
|
|
33
|
+
io2: IO
|
|
34
|
+
io3: IO
|
|
35
|
+
io4: IO
|
|
32
36
|
io5: SerialCard
|
|
33
|
-
io6:
|
|
34
|
-
io7:
|
|
35
|
-
io8:
|
|
37
|
+
io6: IO
|
|
38
|
+
io7: IO
|
|
39
|
+
io8: IO
|
|
36
40
|
|
|
37
41
|
cart?: Cart
|
|
42
|
+
kim: boolean
|
|
38
43
|
|
|
39
44
|
// GPIO Attachments
|
|
40
45
|
keyboardMatrixAttachment: GPIOKeyboardMatrixAttachment
|
|
41
46
|
keyboardEncoderAttachment: GPIOKeyboardEncoderAttachment
|
|
42
|
-
|
|
47
|
+
joystickAttachmentA: GPIOJoystickAttachment
|
|
48
|
+
joystickAttachmentB: GPIOJoystickAttachment
|
|
49
|
+
|
|
50
|
+
// KIM mode attachments
|
|
51
|
+
lcdAttachment?: GPIOLCDAttachment
|
|
52
|
+
keypadAttachment?: GPIOKeypadAttachment
|
|
43
53
|
|
|
44
54
|
isAlive: boolean = false
|
|
45
55
|
isRunning: boolean = false
|
|
@@ -52,73 +62,101 @@ export class Machine {
|
|
|
52
62
|
previousTime: number = performance.now()
|
|
53
63
|
|
|
54
64
|
transmit?: (data: number) => void
|
|
55
|
-
render?: (
|
|
65
|
+
render?: () => void
|
|
56
66
|
pushAudioSamples?: (samples: Float32Array) => void
|
|
57
67
|
|
|
58
68
|
//
|
|
59
69
|
// Initialization
|
|
60
70
|
//
|
|
61
71
|
|
|
62
|
-
constructor() {
|
|
72
|
+
constructor(kim: boolean = false) {
|
|
73
|
+
this.kim = kim
|
|
63
74
|
this.cpu = new CPU(this.read.bind(this), this.write.bind(this))
|
|
64
75
|
this.ram = new RAM()
|
|
65
76
|
this.rom = new ROM()
|
|
66
|
-
this.io1 = new RAMCard()
|
|
67
|
-
this.io2 = new RAMCard()
|
|
68
|
-
this.io3 = new RTCCard()
|
|
69
|
-
this.io4 = new StorageCard()
|
|
70
|
-
this.io5 = new SerialCard()
|
|
71
|
-
this.io6 = new GPIOCard()
|
|
72
|
-
this.io7 = new SoundCard()
|
|
73
|
-
this.io8 = new VideoCard()
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
this.io3.raiseIRQ = () => this.cpu.irq()
|
|
77
|
-
this.io3.raiseNMI = () => this.cpu.nmi()
|
|
78
|
+
this.io5 = new SerialCard()
|
|
78
79
|
|
|
79
80
|
// Connect SerialCard IRQ/NMI to CPU
|
|
80
81
|
this.io5.raiseIRQ = () => this.cpu.irq()
|
|
81
82
|
this.io5.raiseNMI = () => this.cpu.nmi()
|
|
82
83
|
|
|
83
|
-
// Connect SerialCard transmit callback
|
|
84
|
+
// Connect SerialCard transmit callback
|
|
84
85
|
this.io5.transmit = (data: number) => {
|
|
85
86
|
if (this.transmit) {
|
|
86
87
|
this.transmit(data)
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
//
|
|
91
|
-
this.io8.raiseIRQ = () => this.cpu.irq()
|
|
92
|
-
this.io8.raiseNMI = () => this.cpu.nmi()
|
|
93
|
-
|
|
94
|
-
// Connect SoundCard pushSamples callback (use arrow function to look up this.pushAudioSamples at call time)
|
|
95
|
-
this.io7.pushSamples = (samples: Float32Array) => {
|
|
96
|
-
if (this.pushAudioSamples) {
|
|
97
|
-
this.pushAudioSamples(samples)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Create GPIO Attachments
|
|
102
|
-
// Keyboard matrix (manual scanning) - highest priority for Port A rows (priority 10)
|
|
91
|
+
// Always create standard GPIO attachments (for type stability)
|
|
103
92
|
this.keyboardMatrixAttachment = new GPIOKeyboardMatrixAttachment(10)
|
|
104
|
-
|
|
105
|
-
// Keyboard encoder (ASCII on both Port A and Port B) - medium priority (priority 20)
|
|
106
93
|
this.keyboardEncoderAttachment = new GPIOKeyboardEncoderAttachment(20)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
94
|
+
this.joystickAttachmentA = new GPIOJoystickAttachment(false, 100)
|
|
95
|
+
this.joystickAttachmentB = new GPIOJoystickAttachment(false, 100)
|
|
96
|
+
|
|
97
|
+
if (kim) {
|
|
98
|
+
this.io1 = new EmptyCard()
|
|
99
|
+
this.io2 = new EmptyCard()
|
|
100
|
+
this.io3 = new EmptyCard()
|
|
101
|
+
this.io4 = new EmptyCard()
|
|
102
|
+
this.io6 = new EmptyCard()
|
|
103
|
+
this.io7 = new EmptyCard()
|
|
104
|
+
|
|
105
|
+
const gpioCard = new GPIOCard()
|
|
106
|
+
this.io8 = gpioCard
|
|
107
|
+
|
|
108
|
+
// Connect GPIOCard IRQ/NMI to CPU
|
|
109
|
+
gpioCard.raiseIRQ = () => this.cpu.irq()
|
|
110
|
+
gpioCard.raiseNMI = () => this.cpu.nmi()
|
|
111
|
+
|
|
112
|
+
// Create KIM GPIO Attachments
|
|
113
|
+
this.lcdAttachment = new GPIOLCDAttachment(16, 2, 10)
|
|
114
|
+
this.keypadAttachment = new GPIOKeypadAttachment(true, 20)
|
|
115
|
+
|
|
116
|
+
// Attach LCD to Port A (control: RS/RW/E on bits 5-7) and Port B (data bus)
|
|
117
|
+
gpioCard.attachToPortA(this.lcdAttachment)
|
|
118
|
+
gpioCard.attachToPortB(this.lcdAttachment)
|
|
119
|
+
|
|
120
|
+
// Attach keypad to Port A (bits 0-4)
|
|
121
|
+
gpioCard.attachToPortA(this.keypadAttachment)
|
|
122
|
+
} else {
|
|
123
|
+
const rtcCard = new RTCCard()
|
|
124
|
+
const storageCard = new StorageCard()
|
|
125
|
+
const gpioCard = new GPIOCard()
|
|
126
|
+
const soundCard = new SoundCard()
|
|
127
|
+
const videoCard = new VideoCard()
|
|
128
|
+
|
|
129
|
+
this.io1 = new RAMCard()
|
|
130
|
+
this.io2 = new RAMCard()
|
|
131
|
+
this.io3 = rtcCard
|
|
132
|
+
this.io4 = storageCard
|
|
133
|
+
this.io6 = gpioCard
|
|
134
|
+
this.io7 = soundCard
|
|
135
|
+
this.io8 = videoCard
|
|
136
|
+
|
|
137
|
+
// Connect RTCCard IRQ/NMI to CPU
|
|
138
|
+
rtcCard.raiseIRQ = () => this.cpu.irq()
|
|
139
|
+
rtcCard.raiseNMI = () => this.cpu.nmi()
|
|
140
|
+
|
|
141
|
+
// Connect VideoCard IRQ/NMI to CPU
|
|
142
|
+
videoCard.raiseIRQ = () => this.cpu.irq()
|
|
143
|
+
videoCard.raiseNMI = () => this.cpu.nmi()
|
|
144
|
+
|
|
145
|
+
// Connect SoundCard pushSamples callback
|
|
146
|
+
soundCard.pushSamples = (samples: Float32Array) => {
|
|
147
|
+
if (this.pushAudioSamples) {
|
|
148
|
+
this.pushAudioSamples(samples)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
110
151
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// Joystick attached to Port B only
|
|
121
|
-
this.io6.attachToPortB(this.joystickAttachment)
|
|
152
|
+
// Attach peripherals to GPIO Card
|
|
153
|
+
gpioCard.attachToPortA(this.keyboardMatrixAttachment)
|
|
154
|
+
gpioCard.attachToPortB(this.keyboardMatrixAttachment)
|
|
155
|
+
gpioCard.attachToPortA(this.keyboardEncoderAttachment)
|
|
156
|
+
gpioCard.attachToPortB(this.keyboardEncoderAttachment)
|
|
157
|
+
gpioCard.attachToPortA(this.joystickAttachmentA)
|
|
158
|
+
gpioCard.attachToPortB(this.joystickAttachmentB)
|
|
159
|
+
}
|
|
122
160
|
|
|
123
161
|
this.cpu.reset()
|
|
124
162
|
}
|
|
@@ -147,6 +185,7 @@ export class Machine {
|
|
|
147
185
|
}
|
|
148
186
|
|
|
149
187
|
start(): void {
|
|
188
|
+
this.cpu.reset()
|
|
150
189
|
this.startTime = Date.now()
|
|
151
190
|
this.isRunning = true
|
|
152
191
|
this.isAlive = true
|
|
@@ -213,20 +252,27 @@ export class Machine {
|
|
|
213
252
|
}
|
|
214
253
|
|
|
215
254
|
onKeyDown(scancode: number): void {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
255
|
+
if (this.kim) {
|
|
256
|
+
this.keypadAttachment?.updateKey(scancode, true)
|
|
257
|
+
} else {
|
|
258
|
+
this.keyboardMatrixAttachment.updateKey(scancode, true)
|
|
259
|
+
this.keyboardEncoderAttachment.updateKey(scancode, true)
|
|
260
|
+
}
|
|
219
261
|
}
|
|
220
262
|
|
|
221
263
|
onKeyUp(scancode: number): void {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
264
|
+
if (!this.kim) {
|
|
265
|
+
this.keyboardMatrixAttachment.updateKey(scancode, false)
|
|
266
|
+
this.keyboardEncoderAttachment.updateKey(scancode, false)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
onJoystickA(buttons: number): void {
|
|
271
|
+
this.joystickAttachmentA?.updateJoystick(buttons)
|
|
225
272
|
}
|
|
226
273
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
this.joystickAttachment.updateJoystick(buttons)
|
|
274
|
+
onJoystickB(buttons: number): void {
|
|
275
|
+
this.joystickAttachmentB?.updateJoystick(buttons)
|
|
230
276
|
}
|
|
231
277
|
|
|
232
278
|
//
|
|
@@ -274,7 +320,7 @@ export class Machine {
|
|
|
274
320
|
}
|
|
275
321
|
|
|
276
322
|
if (this.render) {
|
|
277
|
-
this.render(
|
|
323
|
+
this.render()
|
|
278
324
|
this.frames += 1
|
|
279
325
|
}
|
|
280
326
|
|
package/src/index.ts
CHANGED
|
@@ -4,9 +4,12 @@ import figlet from 'figlet'
|
|
|
4
4
|
import { Machine } from './components/Machine'
|
|
5
5
|
import { Command } from 'commander'
|
|
6
6
|
import { SerialPort } from 'serialport'
|
|
7
|
+
import { VideoCard } from './components/IO/VideoCard'
|
|
8
|
+
import { StorageCard } from './components/IO/StorageCard'
|
|
9
|
+
import { SoundCard } from './components/IO/SoundCard'
|
|
7
10
|
import sdl from '@kmamal/sdl'
|
|
8
11
|
|
|
9
|
-
const VERSION = '1.
|
|
12
|
+
const VERSION = '1.1.0'
|
|
10
13
|
const WIDTH = 320
|
|
11
14
|
const HEIGHT = 240
|
|
12
15
|
|
|
@@ -40,6 +43,7 @@ interface EmulatorOptions {
|
|
|
40
43
|
stopbits?: string
|
|
41
44
|
port?: string
|
|
42
45
|
storage?: string
|
|
46
|
+
kim?: boolean
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
class Emulator {
|
|
@@ -48,14 +52,16 @@ class Emulator {
|
|
|
48
52
|
private window?: any
|
|
49
53
|
private audioDevice?: any
|
|
50
54
|
private controllers: Map<number, any>
|
|
51
|
-
private
|
|
55
|
+
private joystickButtonStateA: number
|
|
56
|
+
private joystickButtonStateB: number
|
|
52
57
|
private options: EmulatorOptions
|
|
53
58
|
|
|
54
59
|
constructor(options: EmulatorOptions) {
|
|
55
60
|
this.options = options
|
|
56
|
-
this.machine = new Machine()
|
|
61
|
+
this.machine = new Machine(options.kim ?? false)
|
|
57
62
|
this.controllers = new Map()
|
|
58
|
-
this.
|
|
63
|
+
this.joystickButtonStateA = 0x00
|
|
64
|
+
this.joystickButtonStateB = 0x00
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
async initialize(): Promise<void> {
|
|
@@ -98,8 +104,8 @@ class Emulator {
|
|
|
98
104
|
console.log('Loaded Cart: NONE')
|
|
99
105
|
}
|
|
100
106
|
|
|
101
|
-
if (this.options.storage) {
|
|
102
|
-
await this.machine.io4.loadFromFile(this.options.storage)
|
|
107
|
+
if (this.options.storage && !this.options.kim) {
|
|
108
|
+
await (this.machine.io4 as StorageCard).loadFromFile(this.options.storage)
|
|
103
109
|
}
|
|
104
110
|
}
|
|
105
111
|
|
|
@@ -177,6 +183,7 @@ class Emulator {
|
|
|
177
183
|
}
|
|
178
184
|
|
|
179
185
|
private setupAudio(): void {
|
|
186
|
+
if (this.options.kim) return
|
|
180
187
|
try {
|
|
181
188
|
this.audioDevice = sdl.audio.openDevice({ type: 'playback' }, {
|
|
182
189
|
channels: AUDIO_CHANNELS as 1,
|
|
@@ -186,7 +193,7 @@ class Emulator {
|
|
|
186
193
|
})
|
|
187
194
|
|
|
188
195
|
// Configure SoundCard sample rate to match audio device
|
|
189
|
-
this.machine.io7.sampleRate = this.audioDevice.frequency
|
|
196
|
+
;(this.machine.io7 as SoundCard).sampleRate = this.audioDevice.frequency
|
|
190
197
|
|
|
191
198
|
// Connect the Machine's audio callback to the SDL audio device
|
|
192
199
|
this.machine.pushAudioSamples = (samples: Float32Array) => {
|
|
@@ -211,10 +218,29 @@ class Emulator {
|
|
|
211
218
|
}
|
|
212
219
|
|
|
213
220
|
private setupWindow(): void {
|
|
221
|
+
const isKIM = this.options.kim ?? false
|
|
222
|
+
const lcd = this.machine.lcdAttachment
|
|
223
|
+
|
|
224
|
+
// LCD dot-matrix rendering constants
|
|
225
|
+
const DOT_SIZE = 2 // Each LCD dot rendered as DOT_SIZE x DOT_SIZE pixels
|
|
226
|
+
const DOT_GAP = 1 // Gap between dots
|
|
227
|
+
const LCD_PADDING = 8 // Green border padding around the display
|
|
228
|
+
const CELL = DOT_SIZE + DOT_GAP
|
|
229
|
+
|
|
230
|
+
let windowWidth: number
|
|
231
|
+
let windowHeight: number
|
|
232
|
+
if (isKIM && lcd) {
|
|
233
|
+
windowWidth = LCD_PADDING * 2 + lcd.pixelsWidth * CELL
|
|
234
|
+
windowHeight = LCD_PADDING * 2 + lcd.pixelsHeight * CELL
|
|
235
|
+
} else {
|
|
236
|
+
windowWidth = WIDTH
|
|
237
|
+
windowHeight = HEIGHT
|
|
238
|
+
}
|
|
239
|
+
|
|
214
240
|
this.window = sdl.video.createWindow({
|
|
215
|
-
title: "6502 Emulator",
|
|
216
|
-
width:
|
|
217
|
-
height:
|
|
241
|
+
title: isKIM ? "6502 Emulator (KIM)" : "6502 Emulator",
|
|
242
|
+
width: windowWidth * this.machine.scale,
|
|
243
|
+
height: windowHeight * this.machine.scale,
|
|
218
244
|
accelerated: true,
|
|
219
245
|
vsync: true
|
|
220
246
|
})
|
|
@@ -229,15 +255,89 @@ class Emulator {
|
|
|
229
255
|
this.machine.onKeyUp(event.scancode)
|
|
230
256
|
})
|
|
231
257
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
258
|
+
if (isKIM && lcd) {
|
|
259
|
+
const lcdWidth = lcd.pixelsWidth
|
|
260
|
+
const lcdHeight = lcd.pixelsHeight
|
|
261
|
+
const renderWidth = windowWidth
|
|
262
|
+
const renderHeight = windowHeight
|
|
263
|
+
const rgbaBuffer = Buffer.alloc(renderWidth * renderHeight * 4)
|
|
264
|
+
|
|
265
|
+
// Pre-fill with background color (LCD green for padding + gaps)
|
|
266
|
+
for (let i = 0; i < renderWidth * renderHeight; i++) {
|
|
267
|
+
const off = i * 4
|
|
268
|
+
rgbaBuffer[off] = 0x50
|
|
269
|
+
rgbaBuffer[off + 1] = 0x88
|
|
270
|
+
rgbaBuffer[off + 2] = 0x38
|
|
271
|
+
rgbaBuffer[off + 3] = 0xFF
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
this.machine.render = () => {
|
|
275
|
+
if (!this.window) { return }
|
|
276
|
+
const buf = lcd.buffer
|
|
277
|
+
|
|
278
|
+
// Reset buffer to background color
|
|
279
|
+
for (let i = 0; i < renderWidth * renderHeight; i++) {
|
|
280
|
+
const off = i * 4
|
|
281
|
+
rgbaBuffer[off] = 0x50
|
|
282
|
+
rgbaBuffer[off + 1] = 0x88
|
|
283
|
+
rgbaBuffer[off + 2] = 0x38
|
|
284
|
+
rgbaBuffer[off + 3] = 0xFF
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Render each buffer pixel as a dot block
|
|
288
|
+
for (let by = 0; by < lcdHeight; by++) {
|
|
289
|
+
for (let bx = 0; bx < lcdWidth; bx++) {
|
|
290
|
+
const val = buf[by * lcdWidth + bx]
|
|
291
|
+
|
|
292
|
+
if (val < 0) {
|
|
293
|
+
// Gap pixel - skip, shows background color
|
|
294
|
+
continue
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let r: number, g: number, b: number
|
|
298
|
+
if (val === 0) {
|
|
299
|
+
// Pixel off - slightly brighter than background for visible dot grid
|
|
300
|
+
r = 0x60; g = 0xA0; b = 0x40
|
|
301
|
+
} else {
|
|
302
|
+
// Pixel on - dark
|
|
303
|
+
r = 0x10; g = 0x20; b = 0x10
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Draw DOT_SIZE x DOT_SIZE block
|
|
307
|
+
const screenX = LCD_PADDING + bx * CELL
|
|
308
|
+
const screenY = LCD_PADDING + by * CELL
|
|
309
|
+
for (let dy = 0; dy < DOT_SIZE; dy++) {
|
|
310
|
+
for (let dx = 0; dx < DOT_SIZE; dx++) {
|
|
311
|
+
const off = ((screenY + dy) * renderWidth + (screenX + dx)) * 4
|
|
312
|
+
rgbaBuffer[off] = r
|
|
313
|
+
rgbaBuffer[off + 1] = g
|
|
314
|
+
rgbaBuffer[off + 2] = b
|
|
315
|
+
rgbaBuffer[off + 3] = 0xFF
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this.window.render(renderWidth, renderHeight, renderWidth * 4, 'rgba32', rgbaBuffer)
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
const videoCard = this.machine.io8 as VideoCard
|
|
325
|
+
this.machine.render = () => {
|
|
326
|
+
if (!this.window) { return }
|
|
327
|
+
this.window.render(WIDTH, HEIGHT, WIDTH * 4, 'rgba32', videoCard.buffer)
|
|
328
|
+
}
|
|
235
329
|
}
|
|
236
330
|
|
|
237
331
|
this.window.on('close', () => this.shutdown())
|
|
238
332
|
}
|
|
239
333
|
|
|
334
|
+
private playerForController(deviceId: number): 'A' | 'B' {
|
|
335
|
+
const ids = Array.from(this.controllers.keys())
|
|
336
|
+
return ids.indexOf(deviceId) === 0 ? 'B' : 'A'
|
|
337
|
+
}
|
|
338
|
+
|
|
240
339
|
private setupControllers(): void {
|
|
340
|
+
if (this.options.kim) return
|
|
241
341
|
// Controller device add/remove handlers
|
|
242
342
|
(sdl.controller as any).on('deviceAdd', (device: any) => {
|
|
243
343
|
console.log(`Controller added: ${device.name || device.id}`)
|
|
@@ -246,9 +346,10 @@ class Emulator {
|
|
|
246
346
|
const controller = sdl.controller.openDevice(device)
|
|
247
347
|
this.controllers.set(device.id, controller)
|
|
248
348
|
|
|
249
|
-
this.
|
|
349
|
+
const player = this.playerForController(device.id)
|
|
350
|
+
this.setupControllerHandlers(controller, device, player)
|
|
250
351
|
|
|
251
|
-
console.log(`Controller ${device.name || device.id} opened
|
|
352
|
+
console.log(`Controller ${device.name || device.id} opened as Player ${player}`)
|
|
252
353
|
} catch (error) {
|
|
253
354
|
console.error(`Failed to open controller ${device.name || device.id}:`, error)
|
|
254
355
|
}
|
|
@@ -257,16 +358,20 @@ class Emulator {
|
|
|
257
358
|
;(sdl.controller as any).on('deviceRemove', (device: any) => {
|
|
258
359
|
console.log(`Controller removed: ${device.name || device.id}`)
|
|
259
360
|
|
|
361
|
+
const player = this.playerForController(device.id)
|
|
260
362
|
const controller = this.controllers.get(device.id)
|
|
261
363
|
if (controller && !controller.closed) {
|
|
262
364
|
controller.close()
|
|
263
365
|
}
|
|
264
366
|
this.controllers.delete(device.id)
|
|
265
367
|
|
|
266
|
-
// Clear joystick state
|
|
267
|
-
if (
|
|
268
|
-
this.
|
|
269
|
-
this.machine.
|
|
368
|
+
// Clear joystick state for the removed controller's player
|
|
369
|
+
if (player === 'A') {
|
|
370
|
+
this.joystickButtonStateA = 0x00
|
|
371
|
+
this.machine.onJoystickA(this.joystickButtonStateA)
|
|
372
|
+
} else {
|
|
373
|
+
this.joystickButtonStateB = 0x00
|
|
374
|
+
this.machine.onJoystickB(this.joystickButtonStateB)
|
|
270
375
|
}
|
|
271
376
|
})
|
|
272
377
|
|
|
@@ -280,9 +385,10 @@ class Emulator {
|
|
|
280
385
|
const controller = sdl.controller.openDevice(device)
|
|
281
386
|
this.controllers.set(device.id, controller)
|
|
282
387
|
|
|
283
|
-
this.
|
|
388
|
+
const player = this.playerForController(device.id)
|
|
389
|
+
this.setupControllerHandlers(controller, device, player)
|
|
284
390
|
|
|
285
|
-
console.log(`Controller ${device.name || device.id} opened
|
|
391
|
+
console.log(`Controller ${device.name || device.id} opened as Player ${player}`)
|
|
286
392
|
} catch (error) {
|
|
287
393
|
console.error(`Failed to open controller ${device.name || device.id}:`, error)
|
|
288
394
|
}
|
|
@@ -292,90 +398,75 @@ class Emulator {
|
|
|
292
398
|
}
|
|
293
399
|
}
|
|
294
400
|
|
|
295
|
-
private setupControllerHandlers(controller: any, device: any): void {
|
|
296
|
-
(
|
|
401
|
+
private setupControllerHandlers(controller: any, device: any, player: 'A' | 'B'): void {
|
|
402
|
+
const getState = () => player === 'A' ? this.joystickButtonStateA : this.joystickButtonStateB
|
|
403
|
+
const setState = (v: number) => {
|
|
404
|
+
if (player === 'A') this.joystickButtonStateA = v
|
|
405
|
+
else this.joystickButtonStateB = v
|
|
406
|
+
}
|
|
407
|
+
const send = () => {
|
|
408
|
+
if (player === 'A') this.machine.onJoystickA(this.joystickButtonStateA)
|
|
409
|
+
else this.machine.onJoystickB(this.joystickButtonStateB)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
;(controller as any).on('buttonDown', (button: string) => {
|
|
413
|
+
let state = getState()
|
|
297
414
|
switch (button) {
|
|
298
|
-
case 'dpadUp':
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
case '
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
case '
|
|
305
|
-
|
|
306
|
-
break
|
|
307
|
-
case 'dpadRight':
|
|
308
|
-
this.joystickButtonState |= BUTTON_RIGHT
|
|
309
|
-
break
|
|
310
|
-
case 'a':
|
|
311
|
-
this.joystickButtonState |= BUTTON_A
|
|
312
|
-
break
|
|
313
|
-
case 'b':
|
|
314
|
-
this.joystickButtonState |= BUTTON_B
|
|
315
|
-
break
|
|
316
|
-
case 'back':
|
|
317
|
-
this.joystickButtonState |= BUTTON_SELECT
|
|
318
|
-
break
|
|
319
|
-
case 'start':
|
|
320
|
-
this.joystickButtonState |= BUTTON_START
|
|
321
|
-
break
|
|
415
|
+
case 'dpadUp': state |= BUTTON_UP; break
|
|
416
|
+
case 'dpadDown': state |= BUTTON_DOWN; break
|
|
417
|
+
case 'dpadLeft': state |= BUTTON_LEFT; break
|
|
418
|
+
case 'dpadRight': state |= BUTTON_RIGHT; break
|
|
419
|
+
case 'a': state |= BUTTON_A; break
|
|
420
|
+
case 'b': state |= BUTTON_B; break
|
|
421
|
+
case 'back': state |= BUTTON_SELECT; break
|
|
422
|
+
case 'start': state |= BUTTON_START; break
|
|
322
423
|
}
|
|
323
|
-
|
|
424
|
+
setState(state)
|
|
425
|
+
send()
|
|
324
426
|
})
|
|
325
427
|
|
|
326
428
|
;(controller as any).on('buttonUp', (button: string) => {
|
|
429
|
+
let state = getState()
|
|
327
430
|
switch (button) {
|
|
328
|
-
case 'dpadUp':
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
case '
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
case '
|
|
335
|
-
|
|
336
|
-
break
|
|
337
|
-
case 'dpadRight':
|
|
338
|
-
this.joystickButtonState &= ~BUTTON_RIGHT
|
|
339
|
-
break
|
|
340
|
-
case 'a':
|
|
341
|
-
this.joystickButtonState &= ~BUTTON_A
|
|
342
|
-
break
|
|
343
|
-
case 'b':
|
|
344
|
-
this.joystickButtonState &= ~BUTTON_B
|
|
345
|
-
break
|
|
346
|
-
case 'back':
|
|
347
|
-
this.joystickButtonState &= ~BUTTON_SELECT
|
|
348
|
-
break
|
|
349
|
-
case 'start':
|
|
350
|
-
this.joystickButtonState &= ~BUTTON_START
|
|
351
|
-
break
|
|
431
|
+
case 'dpadUp': state &= ~BUTTON_UP; break
|
|
432
|
+
case 'dpadDown': state &= ~BUTTON_DOWN; break
|
|
433
|
+
case 'dpadLeft': state &= ~BUTTON_LEFT; break
|
|
434
|
+
case 'dpadRight': state &= ~BUTTON_RIGHT; break
|
|
435
|
+
case 'a': state &= ~BUTTON_A; break
|
|
436
|
+
case 'b': state &= ~BUTTON_B; break
|
|
437
|
+
case 'back': state &= ~BUTTON_SELECT; break
|
|
438
|
+
case 'start': state &= ~BUTTON_START; break
|
|
352
439
|
}
|
|
353
|
-
|
|
440
|
+
setState(state)
|
|
441
|
+
send()
|
|
354
442
|
})
|
|
355
443
|
|
|
356
444
|
controller.on('axisMotion', ({ axis, value }: { axis: string; value: number }) => {
|
|
445
|
+
let state = getState()
|
|
357
446
|
if (axis === 'leftStickX') {
|
|
358
447
|
if (value < -AXIS_THRESHOLD) {
|
|
359
|
-
|
|
360
|
-
|
|
448
|
+
state |= BUTTON_LEFT
|
|
449
|
+
state &= ~BUTTON_RIGHT
|
|
361
450
|
} else if (value > AXIS_THRESHOLD) {
|
|
362
|
-
|
|
363
|
-
|
|
451
|
+
state |= BUTTON_RIGHT
|
|
452
|
+
state &= ~BUTTON_LEFT
|
|
364
453
|
} else {
|
|
365
|
-
|
|
454
|
+
state &= ~(BUTTON_LEFT | BUTTON_RIGHT)
|
|
366
455
|
}
|
|
367
|
-
|
|
456
|
+
setState(state)
|
|
457
|
+
send()
|
|
368
458
|
} else if (axis === 'leftStickY') {
|
|
369
459
|
if (value < -AXIS_THRESHOLD) {
|
|
370
|
-
|
|
371
|
-
|
|
460
|
+
state |= BUTTON_UP
|
|
461
|
+
state &= ~BUTTON_DOWN
|
|
372
462
|
} else if (value > AXIS_THRESHOLD) {
|
|
373
|
-
|
|
374
|
-
|
|
463
|
+
state |= BUTTON_DOWN
|
|
464
|
+
state &= ~BUTTON_UP
|
|
375
465
|
} else {
|
|
376
|
-
|
|
466
|
+
state &= ~(BUTTON_UP | BUTTON_DOWN)
|
|
377
467
|
}
|
|
378
|
-
|
|
468
|
+
setState(state)
|
|
469
|
+
send()
|
|
379
470
|
}
|
|
380
471
|
})
|
|
381
472
|
|
|
@@ -422,8 +513,8 @@ class Emulator {
|
|
|
422
513
|
})
|
|
423
514
|
|
|
424
515
|
// Save storage data if path was provided
|
|
425
|
-
if (this.options.storage) {
|
|
426
|
-
this.machine.io4.saveToFile(this.options.storage).then(() => {
|
|
516
|
+
if (this.options.storage && !this.options.kim) {
|
|
517
|
+
(this.machine.io4 as StorageCard).saveToFile(this.options.storage).then(() => {
|
|
427
518
|
process.exit(0)
|
|
428
519
|
}).catch(() => {
|
|
429
520
|
process.exit(1)
|
|
@@ -455,6 +546,7 @@ program
|
|
|
455
546
|
.option('-t, --stopbits <stopbits>', 'Stop Bits (1 | 1.5 | 2)', '1')
|
|
456
547
|
.option('-p, --port <port>', 'Path to the serial port (e.g., /dev/ttyUSB0)')
|
|
457
548
|
.option('-S, --storage <path>', 'Path to storage data file for Compact Flash card persistence')
|
|
549
|
+
.option('-K, --kim', 'Configure IO for the KIM system target', false)
|
|
458
550
|
.addHelpText('beforeAll', figlet.textSync('6502 Emulator', { font: 'cricket' }) + '\n' + `Version: ${VERSION} | A.C. Wright Design\n`)
|
|
459
551
|
.parse(process.argv)
|
|
460
552
|
|