@gba-kit/arm-emulator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1875 @@
1
+ import { LR, PC, SENTINEL_ADDR, SP } from './types.js';
2
+ import { addWithFlags, asr, bit, bits, isNegative, lsl, lsr, ror, signExtend, subWithFlags } from './utils.js';
3
+ // ─── CPSR Bit Positions ──────────────────────────────────────────────
4
+ const CPSR_N = 31;
5
+ const CPSR_Z = 30;
6
+ const CPSR_C = 29;
7
+ const CPSR_V = 28;
8
+ const CPSR_I = 7;
9
+ const CPSR_F = 6;
10
+ const CPSR_T = 5;
11
+ const CPSR_MODE_MASK = 0x1f;
12
+ // ─── CPU Mode Constants ──────────────────────────────────────────────
13
+ /** CPU mode codes (bits 4-0 of CPSR) */
14
+ export const MODE_USR = 0x10;
15
+ export const MODE_FIQ = 0x11;
16
+ export const MODE_IRQ = 0x12;
17
+ export const MODE_SVC = 0x13;
18
+ export const MODE_ABT = 0x17;
19
+ export const MODE_UND = 0x1b;
20
+ export const MODE_SYS = 0x1f;
21
+ /** Index into banked SP/LR arrays by mode */
22
+ const SP_LR_BANK_INDEX = {
23
+ [MODE_USR]: 0,
24
+ [MODE_SYS]: 0,
25
+ [MODE_FIQ]: 1,
26
+ [MODE_IRQ]: 2,
27
+ [MODE_SVC]: 3,
28
+ [MODE_ABT]: 4,
29
+ [MODE_UND]: 5,
30
+ };
31
+ /** Index into SPSR array by mode (only privileged modes have SPSR) */
32
+ const SPSR_BANK_INDEX = {
33
+ [MODE_FIQ]: 0,
34
+ [MODE_IRQ]: 1,
35
+ [MODE_SVC]: 2,
36
+ [MODE_ABT]: 3,
37
+ [MODE_UND]: 4,
38
+ };
39
+ /** Stub address range for external function calls */
40
+ const STUB_BASE = 0x08f00000;
41
+ /**
42
+ * Check if a CPU mode is valid.
43
+ */
44
+ function isValidMode(mode) {
45
+ return (mode === MODE_USR ||
46
+ mode === MODE_FIQ ||
47
+ mode === MODE_IRQ ||
48
+ mode === MODE_SVC ||
49
+ mode === MODE_ABT ||
50
+ mode === MODE_UND ||
51
+ mode === MODE_SYS);
52
+ }
53
+ // ─── ARM Condition Codes ─────────────────────────────────────────────
54
+ /**
55
+ * Evaluate an ARM condition code (bits 31-28 of an ARM instruction).
56
+ */
57
+ function checkCondition(cond, n, z, c, v) {
58
+ switch (cond) {
59
+ case 0x0:
60
+ return z; // EQ
61
+ case 0x1:
62
+ return !z; // NE
63
+ case 0x2:
64
+ return c; // CS/HS
65
+ case 0x3:
66
+ return !c; // CC/LO
67
+ case 0x4:
68
+ return n; // MI
69
+ case 0x5:
70
+ return !n; // PL
71
+ case 0x6:
72
+ return v; // VS
73
+ case 0x7:
74
+ return !v; // VC
75
+ case 0x8:
76
+ return c && !z; // HI
77
+ case 0x9:
78
+ return !c || z; // LS
79
+ case 0xa:
80
+ return n === v; // GE
81
+ case 0xb:
82
+ return n !== v; // LT
83
+ case 0xc:
84
+ return !z && n === v; // GT
85
+ case 0xd:
86
+ return z || n !== v; // LE
87
+ case 0xe:
88
+ return true; // AL
89
+ case 0xf:
90
+ return true; // Unconditional (ARMv5+, treat as AL)
91
+ default:
92
+ return true;
93
+ }
94
+ }
95
+ // ─── ARM7TDMI Full CPU ──────────────────────────────────────────────
96
+ /**
97
+ * Full ARM7TDMI CPU supporting ARM and Thumb instruction sets,
98
+ * with CPU modes and banked registers.
99
+ */
100
+ export class ArmCpu {
101
+ /** General-purpose registers r0-r15 (current view, mode-dependent) */
102
+ registers = new Uint32Array(16);
103
+ /**
104
+ * Current Program Status Register (full 32-bit).
105
+ *
106
+ * Bit layout:
107
+ * - 31: N (Negative)
108
+ * - 30: Z (Zero)
109
+ * - 29: C (Carry)
110
+ * - 28: V (Overflow)
111
+ * - 7: I (IRQ disable)
112
+ * - 6: F (FIQ disable)
113
+ * - 5: T (Thumb state: 0=ARM, 1=Thumb)
114
+ * - 4-0: Mode (0x10=USR, 0x11=FIQ, 0x12=IRQ, 0x13=SVC, 0x17=ABT, 0x1B=UND, 0x1F=SYS)
115
+ */
116
+ cpsr = MODE_SYS | (1 << CPSR_I) | (1 << CPSR_F);
117
+ /** Memory bus */
118
+ memory;
119
+ // ─── Banked Registers ────────────────────────────────────────────
120
+ /**
121
+ * Banked SP and LR for each mode.
122
+ * Index: 0=USR/SYS, 1=FIQ, 2=IRQ, 3=SVC, 4=ABT, 5=UND
123
+ */
124
+ #bankedSP = new Uint32Array(6);
125
+ #bankedLR = new Uint32Array(6);
126
+ /**
127
+ * FIQ banked r8-r12 (5 registers). Only FIQ has these extra banked regs.
128
+ */
129
+ #fiqBankedR8to12 = new Uint32Array(5);
130
+ /**
131
+ * USR/SYS r8-r12 saved when switching to FIQ mode.
132
+ */
133
+ #usrBankedR8to12 = new Uint32Array(5);
134
+ /**
135
+ * SPSR for each privileged mode.
136
+ * Index: 0=FIQ, 1=IRQ, 2=SVC, 3=ABT, 4=UND
137
+ */
138
+ #spsr = new Uint32Array(5);
139
+ // ─── Execution State ─────────────────────────────────────────────
140
+ /** Whether the CPU has halted (function returned to sentinel) */
141
+ #halted = false;
142
+ /** Halt flag for SWI 0x02 (Halt) */
143
+ #haltedBySWI = false;
144
+ /** Map of stub addresses to symbol names */
145
+ #stubs = new Map();
146
+ /** Next available stub address */
147
+ #nextStub = STUB_BASE;
148
+ /** Recorded external calls */
149
+ #externalCalls = [];
150
+ /** Optional debug hooks */
151
+ #hooks;
152
+ /** Platform-specific SWI handler */
153
+ #swiHandler;
154
+ constructor(memory, options) {
155
+ this.memory = memory;
156
+ this.#hooks = options?.hooks;
157
+ this.#swiHandler = options?.swiHandler;
158
+ }
159
+ /**
160
+ * Set the banked SP for a given mode without switching modes.
161
+ * Used to initialize stack pointers (e.g. GBA BIOS sets IRQ/SVC stacks).
162
+ */
163
+ setBankedSP(mode, value) {
164
+ const bankIdx = SP_LR_BANK_INDEX[mode];
165
+ if (bankIdx !== undefined) {
166
+ this.#bankedSP[bankIdx] = value;
167
+ }
168
+ }
169
+ // ─── CPSR Accessors ──────────────────────────────────────────────
170
+ /** Get condition flags from CPSR as a CpsrFlags object */
171
+ get flags() {
172
+ return {
173
+ n: this.getN(),
174
+ z: this.getZ(),
175
+ c: this.getC(),
176
+ v: this.getV(),
177
+ };
178
+ }
179
+ /** Get Negative flag */
180
+ getN() {
181
+ return (this.cpsr & (1 << CPSR_N)) !== 0;
182
+ }
183
+ /** Get Zero flag */
184
+ getZ() {
185
+ return (this.cpsr & (1 << CPSR_Z)) !== 0;
186
+ }
187
+ /** Get Carry flag */
188
+ getC() {
189
+ return (this.cpsr & (1 << CPSR_C)) !== 0;
190
+ }
191
+ /** Get Overflow flag */
192
+ getV() {
193
+ return (this.cpsr & (1 << CPSR_V)) !== 0;
194
+ }
195
+ /** Get Thumb state bit */
196
+ getT() {
197
+ return (this.cpsr & (1 << CPSR_T)) !== 0;
198
+ }
199
+ /** Get current CPU mode */
200
+ getMode() {
201
+ return this.cpsr & CPSR_MODE_MASK;
202
+ }
203
+ /** Set Negative flag */
204
+ setN(val) {
205
+ if (val) {
206
+ this.cpsr |= 1 << CPSR_N;
207
+ }
208
+ else {
209
+ this.cpsr &= ~(1 << CPSR_N);
210
+ }
211
+ }
212
+ /** Set Zero flag */
213
+ setZ(val) {
214
+ if (val) {
215
+ this.cpsr |= 1 << CPSR_Z;
216
+ }
217
+ else {
218
+ this.cpsr &= ~(1 << CPSR_Z);
219
+ }
220
+ }
221
+ /** Set Carry flag */
222
+ setC(val) {
223
+ if (val) {
224
+ this.cpsr |= 1 << CPSR_C;
225
+ }
226
+ else {
227
+ this.cpsr &= ~(1 << CPSR_C);
228
+ }
229
+ }
230
+ /** Set Overflow flag */
231
+ setV(val) {
232
+ if (val) {
233
+ this.cpsr |= 1 << CPSR_V;
234
+ }
235
+ else {
236
+ this.cpsr &= ~(1 << CPSR_V);
237
+ }
238
+ }
239
+ /** Set Thumb state bit */
240
+ setT(val) {
241
+ if (val) {
242
+ this.cpsr |= 1 << CPSR_T;
243
+ }
244
+ else {
245
+ this.cpsr &= ~(1 << CPSR_T);
246
+ }
247
+ }
248
+ /** Set all four condition flags at once from an AluResult */
249
+ setFlags(n, z, c, v) {
250
+ this.setN(n);
251
+ this.setZ(z);
252
+ this.setC(c);
253
+ this.setV(v);
254
+ }
255
+ /** Set NZ flags from a result value, leave C and V unchanged */
256
+ setNZ(result) {
257
+ const u = result >>> 0;
258
+ this.setN((u & 0x80000000) !== 0);
259
+ this.setZ(u === 0);
260
+ }
261
+ // ─── SPSR Access ─────────────────────────────────────────────────
262
+ /** Get the SPSR for the current mode. Returns 0 for USR/SYS (no SPSR). */
263
+ getSPSR() {
264
+ const mode = this.getMode();
265
+ const idx = SPSR_BANK_INDEX[mode];
266
+ if (idx === undefined) {
267
+ return 0;
268
+ }
269
+ return this.#spsr[idx];
270
+ }
271
+ /** Set the SPSR for the current mode. No-op for USR/SYS. */
272
+ setSPSR(value) {
273
+ const mode = this.getMode();
274
+ const idx = SPSR_BANK_INDEX[mode];
275
+ if (idx === undefined) {
276
+ return;
277
+ }
278
+ this.#spsr[idx] = value;
279
+ }
280
+ // ─── Mode Switching ──────────────────────────────────────────────
281
+ /**
282
+ * Switch CPU mode. Saves banked registers from the old mode
283
+ * and restores them for the new mode.
284
+ */
285
+ switchMode(newMode) {
286
+ const oldMode = this.getMode();
287
+ if (oldMode === newMode) {
288
+ return;
289
+ }
290
+ if (!isValidMode(newMode)) {
291
+ return;
292
+ }
293
+ // Save registers for old mode
294
+ this.#saveBankedRegisters(oldMode);
295
+ // Update CPSR mode bits
296
+ this.cpsr = (this.cpsr & ~CPSR_MODE_MASK) | newMode;
297
+ // Restore registers for new mode
298
+ this.#restoreBankedRegisters(newMode);
299
+ }
300
+ /**
301
+ * Save current SP/LR (and r8-r12 for FIQ) into the bank for the given mode.
302
+ */
303
+ #saveBankedRegisters(mode) {
304
+ // Save SP/LR for the old mode
305
+ const bankIdx = SP_LR_BANK_INDEX[mode];
306
+ if (bankIdx !== undefined) {
307
+ this.#bankedSP[bankIdx] = this.registers[SP];
308
+ this.#bankedLR[bankIdx] = this.registers[LR];
309
+ }
310
+ // FIQ also saves r8-r12
311
+ if (mode === MODE_FIQ) {
312
+ for (let i = 0; i < 5; i++) {
313
+ this.#fiqBankedR8to12[i] = this.registers[8 + i];
314
+ }
315
+ }
316
+ else {
317
+ // Non-FIQ modes share USR r8-r12
318
+ for (let i = 0; i < 5; i++) {
319
+ this.#usrBankedR8to12[i] = this.registers[8 + i];
320
+ }
321
+ }
322
+ }
323
+ /**
324
+ * Restore SP/LR (and r8-r12 for FIQ) from the bank for the given mode.
325
+ */
326
+ #restoreBankedRegisters(mode) {
327
+ // Restore SP/LR for the new mode
328
+ const bankIdx = SP_LR_BANK_INDEX[mode];
329
+ if (bankIdx !== undefined) {
330
+ this.registers[SP] = this.#bankedSP[bankIdx];
331
+ this.registers[LR] = this.#bankedLR[bankIdx];
332
+ }
333
+ // FIQ restores its own r8-r12
334
+ if (mode === MODE_FIQ) {
335
+ for (let i = 0; i < 5; i++) {
336
+ this.registers[8 + i] = this.#fiqBankedR8to12[i];
337
+ }
338
+ }
339
+ else {
340
+ // Non-FIQ modes restore USR r8-r12
341
+ for (let i = 0; i < 5; i++) {
342
+ this.registers[8 + i] = this.#usrBankedR8to12[i];
343
+ }
344
+ }
345
+ }
346
+ // ─── Public API ──────────────────────────────────────────────────
347
+ /** Attach or detach debug hooks */
348
+ setDebugHooks(hooks) {
349
+ this.#hooks = hooks;
350
+ }
351
+ /** Register a stub for an external function call */
352
+ registerStub(symbolName) {
353
+ const addr = this.#nextStub;
354
+ this.#stubs.set(addr, symbolName);
355
+ // Write a "bx lr" (Thumb) at the stub
356
+ this.memory.write16(addr, 0x4770);
357
+ this.#nextStub += 4;
358
+ return addr;
359
+ }
360
+ /** Serialize to a plain snapshot. */
361
+ serialize() {
362
+ return {
363
+ registers: new Uint32Array(this.registers),
364
+ cpsr: this.cpsr,
365
+ bankedSP: new Uint32Array(this.#bankedSP),
366
+ bankedLR: new Uint32Array(this.#bankedLR),
367
+ fiqBankedR8to12: new Uint32Array(this.#fiqBankedR8to12),
368
+ usrBankedR8to12: new Uint32Array(this.#usrBankedR8to12),
369
+ spsr: new Uint32Array(this.#spsr),
370
+ halted: this.#halted,
371
+ haltedBySWI: this.#haltedBySWI,
372
+ };
373
+ }
374
+ /** Restore from a snapshot. */
375
+ deserialize(snap) {
376
+ this.registers.set(snap.registers);
377
+ this.cpsr = snap.cpsr;
378
+ this.#bankedSP.set(snap.bankedSP);
379
+ this.#bankedLR.set(snap.bankedLR);
380
+ this.#fiqBankedR8to12.set(snap.fiqBankedR8to12);
381
+ this.#usrBankedR8to12.set(snap.usrBankedR8to12);
382
+ this.#spsr.set(snap.spsr);
383
+ this.#halted = snap.halted;
384
+ this.#haltedBySWI = snap.haltedBySWI;
385
+ }
386
+ /** Reset CPU state for a new execution */
387
+ resetState() {
388
+ this.registers.fill(0);
389
+ this.cpsr = MODE_SYS | (1 << CPSR_I) | (1 << CPSR_F);
390
+ this.#spsr.fill(0);
391
+ this.#fiqBankedR8to12.fill(0);
392
+ this.#usrBankedR8to12.fill(0);
393
+ this.#bankedSP.fill(0);
394
+ this.#bankedLR.fill(0);
395
+ this.#externalCalls = [];
396
+ this.#halted = false;
397
+ this.#haltedBySWI = false;
398
+ }
399
+ /** Check if IRQs are disabled (CPSR I bit set) */
400
+ irqDisabled() {
401
+ return (this.cpsr & (1 << CPSR_I)) !== 0;
402
+ }
403
+ /**
404
+ * Enter IRQ exception.
405
+ *
406
+ * Standard ARM7TDMI exception entry:
407
+ * 1. Save CPSR to SPSR_irq
408
+ * 2. Switch to IRQ mode
409
+ * 3. Set LR_irq to return address (next instruction + 4)
410
+ * 4. Set I bit (disable further IRQs)
411
+ * 5. Clear T bit (enter ARM state)
412
+ * 6. Set PC to IRQ vector (0x00000018)
413
+ *
414
+ * The BIOS stub at 0x18 (installed by Gba.#installBiosStub) handles:
415
+ * - Saving registers to IRQ stack
416
+ * - Calling the user's handler from [0x03007FFC]
417
+ * - Restoring registers and returning from IRQ
418
+ *
419
+ * The BIOS stub at 0x80 handles IE/IF acknowledgment and BIOS IF mirror
420
+ * update before calling the user handler.
421
+ */
422
+ enterIrq() {
423
+ // Save current CPSR as SPSR_irq
424
+ const savedCpsr = this.cpsr;
425
+ // LR_irq = address of next instruction + 4
426
+ // The CPU checks for IRQ between instructions, so PC points to the next instruction.
427
+ // ARM7TDMI: LR_irq = PC + 4 (return via SUBS PC, LR, #4)
428
+ const returnAddr = (this.registers[PC] + 4) >>> 0;
429
+ // Switch to IRQ mode (saves current SP/LR, restores IRQ SP/LR)
430
+ this.switchMode(MODE_IRQ);
431
+ // Set LR_irq to return address
432
+ this.registers[LR] = returnAddr;
433
+ // Set SPSR_irq
434
+ this.setSPSR(savedCpsr);
435
+ // Disable IRQs and enter ARM state
436
+ this.cpsr |= 1 << CPSR_I; // Disable IRQs
437
+ this.cpsr &= ~(1 << CPSR_T); // Enter ARM state
438
+ // Jump to BIOS IRQ vector — the stub handles IE/IF acknowledgment,
439
+ // BIOS IF mirror update, and calling the user handler
440
+ this.registers[PC] = 0x00000018;
441
+ }
442
+ /** Enter Undefined Instruction exception */
443
+ enterUnd(instrAddr) {
444
+ const savedCpsr = this.cpsr;
445
+ const isThumb = !!(this.cpsr & (1 << CPSR_T));
446
+ // LR_und = address of undefined instruction + 2 (Thumb) or + 4 (ARM)
447
+ const returnAddr = isThumb ? (instrAddr + 2) >>> 0 : (instrAddr + 4) >>> 0;
448
+ this.switchMode(MODE_UND);
449
+ this.registers[LR] = returnAddr;
450
+ this.setSPSR(savedCpsr);
451
+ this.cpsr |= 1 << CPSR_I; // Disable IRQs
452
+ this.cpsr &= ~(1 << CPSR_T); // Enter ARM state
453
+ this.registers[PC] = 0x00000004; // UND vector
454
+ }
455
+ /**
456
+ * Execute one instruction (ARM or Thumb based on T bit).
457
+ * Returns false if halted.
458
+ */
459
+ step() {
460
+ if (this.#halted || this.#haltedBySWI) {
461
+ return false;
462
+ }
463
+ const pc = this.registers[PC];
464
+ if ((pc & ~1) === SENTINEL_ADDR || (pc & ~1) === (SENTINEL_ADDR & ~1)) {
465
+ this.#halted = true;
466
+ return false;
467
+ }
468
+ // Check stubs
469
+ const pcAligned = this.getT() ? pc & ~1 : pc & ~3;
470
+ const stubName = this.#stubs.get(pcAligned);
471
+ if (stubName !== undefined) {
472
+ this.#externalCalls.push({
473
+ callSite: pcAligned,
474
+ targetAddress: pcAligned,
475
+ symbolName: stubName,
476
+ r0: this.registers[0],
477
+ r1: this.registers[1],
478
+ r2: this.registers[2],
479
+ r3: this.registers[3],
480
+ });
481
+ this.registers[0] = 0;
482
+ const returnAddr = this.registers[LR];
483
+ this.registers[PC] = returnAddr & ~1;
484
+ return true;
485
+ }
486
+ if (this.getT()) {
487
+ return this.#stepThumb();
488
+ }
489
+ else {
490
+ return this.#stepArm();
491
+ }
492
+ }
493
+ /** Run until halt or instruction limit */
494
+ run(maxInstructions) {
495
+ const trackable = this.memory;
496
+ trackable.resetWriteLog?.();
497
+ this.#externalCalls = [];
498
+ let count = 0;
499
+ while (count < maxInstructions && this.step()) {
500
+ count++;
501
+ }
502
+ return {
503
+ registers: new Uint32Array(this.registers),
504
+ cpsr: this.flags,
505
+ memoryWrites: trackable.getWriteLog?.() ?? [],
506
+ externalCalls: [...this.#externalCalls],
507
+ instructionsExecuted: count,
508
+ completed: this.#halted,
509
+ };
510
+ }
511
+ // ─── Thumb Execution ─────────────────────────────────────────────
512
+ /** Execute one Thumb instruction */
513
+ #stepThumb() {
514
+ const pc = this.registers[PC];
515
+ const instrAddr = pc & ~1;
516
+ const instr = this.memory.read16(instrAddr);
517
+ if (this.#hooks?.onInstructionPre) {
518
+ const action = this.#hooks.onInstructionPre(instrAddr, instr);
519
+ if (action === 'break') {
520
+ return false;
521
+ }
522
+ }
523
+ this.registers[PC] = (pc + 2) >>> 0;
524
+ this.#executeThumb(instr, instrAddr);
525
+ this.#hooks?.onInstructionPost?.(instrAddr, instr);
526
+ return !this.#halted;
527
+ }
528
+ /** Decode and execute a single 16-bit Thumb instruction. */
529
+ #executeThumb(instr, _instrAddr) {
530
+ const op = instr >>> 8;
531
+ // Format 19: Long Branch with Link (BL) — two-part
532
+ if ((instr & 0xf800) === 0xf000) {
533
+ this.#thumbBlPrefix(instr);
534
+ return;
535
+ }
536
+ if ((instr & 0xf800) === 0xf800) {
537
+ this.#thumbBlSuffix(instr);
538
+ return;
539
+ }
540
+ // Format 18: Unconditional Branch
541
+ if ((instr & 0xf800) === 0xe000) {
542
+ const offset11 = signExtend(instr & 0x7ff, 11);
543
+ // ARM7TDMI pipeline: PC = instrAddr+4. registers[PC] = instrAddr+2, so add +2.
544
+ this.registers[PC] = (this.registers[PC] + 2 + offset11 * 2) >>> 0;
545
+ return;
546
+ }
547
+ // Format 17: SWI
548
+ if ((instr & 0xff00) === 0xdf00) {
549
+ this.#swiHandler?.(this, instr & 0xff);
550
+ return;
551
+ }
552
+ // Format 16: Conditional Branch
553
+ if ((instr & 0xf000) === 0xd000) {
554
+ this.#thumbCondBranch(instr);
555
+ return;
556
+ }
557
+ // Format 14: Push/Pop
558
+ if ((instr & 0xf600) === 0xb400) {
559
+ this.#thumbPushPop(instr);
560
+ return;
561
+ }
562
+ // Format 13: Add offset to SP
563
+ if ((op & 0xff) === 0xb0) {
564
+ const s = bit(instr, 7);
565
+ const offset7 = (instr & 0x7f) << 2;
566
+ if (s === 0) {
567
+ this.registers[SP] = (this.registers[SP] + offset7) >>> 0;
568
+ }
569
+ else {
570
+ this.registers[SP] = (this.registers[SP] - offset7) >>> 0;
571
+ }
572
+ return;
573
+ }
574
+ // Format 11: SP-relative Load/Store
575
+ if ((instr & 0xf000) === 0x9000) {
576
+ const l = bit(instr, 11);
577
+ const rd = bits(instr, 10, 8);
578
+ const offset8 = (instr & 0xff) << 2;
579
+ const address = (this.registers[SP] + offset8) >>> 0;
580
+ if (l === 1) {
581
+ this.registers[rd] = this.memory.read32(address);
582
+ }
583
+ else {
584
+ this.memory.write32(address, this.registers[rd]);
585
+ }
586
+ return;
587
+ }
588
+ // Format 12: Load Address
589
+ if ((instr & 0xf000) === 0xa000) {
590
+ const sp = bit(instr, 11);
591
+ const rd = bits(instr, 10, 8);
592
+ const offset8 = (instr & 0xff) << 2;
593
+ if (sp === 0) {
594
+ const base = ((this.registers[PC] + 2) & ~3) >>> 0;
595
+ this.registers[rd] = (base + offset8) >>> 0;
596
+ }
597
+ else {
598
+ this.registers[rd] = (this.registers[SP] + offset8) >>> 0;
599
+ }
600
+ return;
601
+ }
602
+ // Format 10: Load/Store Halfword Imm
603
+ if ((instr & 0xf000) === 0x8000) {
604
+ const l = bit(instr, 11);
605
+ const offset5 = bits(instr, 10, 6);
606
+ const rb = bits(instr, 5, 3);
607
+ const rd = bits(instr, 2, 0);
608
+ const address = (this.registers[rb] + (offset5 << 1)) >>> 0;
609
+ if (l === 1) {
610
+ this.registers[rd] = this.memory.read16(address);
611
+ }
612
+ else {
613
+ this.memory.write16(address, this.registers[rd]);
614
+ }
615
+ return;
616
+ }
617
+ // Format 9: Load/Store Imm Offset
618
+ if ((instr & 0xe000) === 0x6000) {
619
+ this.#thumbImmOffsetLoadStore(instr);
620
+ return;
621
+ }
622
+ // Format 8: Load/Store Sign-Extended
623
+ if ((instr & 0xf200) === 0x5200) {
624
+ this.#thumbSignExtLoadStore(instr);
625
+ return;
626
+ }
627
+ // Format 7: Load/Store Register Offset
628
+ if ((instr & 0xf200) === 0x5000) {
629
+ this.#thumbRegOffsetLoadStore(instr);
630
+ return;
631
+ }
632
+ // Format 6: PC-Relative Load
633
+ if ((instr & 0xf800) === 0x4800) {
634
+ const rd = bits(instr, 10, 8);
635
+ const offset8 = (instr & 0xff) << 2;
636
+ const base = ((this.registers[PC] + 2) & ~3) >>> 0;
637
+ const address = (base + offset8) >>> 0;
638
+ this.registers[rd] = this.memory.read32(address);
639
+ return;
640
+ }
641
+ // Format 5: Hi Register Ops / BX
642
+ if ((instr & 0xfc00) === 0x4400) {
643
+ this.#thumbHiRegBx(instr);
644
+ return;
645
+ }
646
+ // Format 4: ALU Operations
647
+ if ((instr & 0xfc00) === 0x4000) {
648
+ this.#thumbAluOp(instr);
649
+ return;
650
+ }
651
+ // Format 3: Move/Compare/Add/Sub Immediate
652
+ if ((instr & 0xe000) === 0x2000) {
653
+ this.#thumbImmOp(instr);
654
+ return;
655
+ }
656
+ // Format 2: Add/Subtract
657
+ if ((instr & 0xf800) === 0x1800) {
658
+ this.#thumbAddSub(instr);
659
+ return;
660
+ }
661
+ // Format 1: Move Shifted Register
662
+ if ((instr & 0xe000) === 0x0000) {
663
+ this.#thumbShifted(instr);
664
+ return;
665
+ }
666
+ // Format 15: Multiple Load/Store
667
+ if ((instr & 0xf000) === 0xc000) {
668
+ this.#thumbBlockTransfer(instr);
669
+ return;
670
+ }
671
+ }
672
+ // ── Thumb instruction implementations ─────────────────────────────
673
+ #thumbShifted(instr) {
674
+ const op = bits(instr, 12, 11);
675
+ const offset5 = bits(instr, 10, 6);
676
+ const rs = bits(instr, 5, 3);
677
+ const rd = bits(instr, 2, 0);
678
+ const rsVal = this.registers[rs] | 0;
679
+ let result;
680
+ let carry;
681
+ switch (op) {
682
+ case 0:
683
+ [result, carry] = lsl(rsVal, offset5, this.getC());
684
+ break;
685
+ case 1:
686
+ [result, carry] = lsr(rsVal, offset5, this.getC(), true);
687
+ break;
688
+ case 2:
689
+ [result, carry] = asr(rsVal, offset5, this.getC(), true);
690
+ break;
691
+ default:
692
+ return;
693
+ }
694
+ this.registers[rd] = result >>> 0;
695
+ this.setN((result & 0x80000000) !== 0);
696
+ this.setZ(result >>> 0 === 0);
697
+ this.setC(carry);
698
+ }
699
+ #thumbAddSub(instr) {
700
+ const i = bit(instr, 10);
701
+ const op = bit(instr, 9);
702
+ const rnImm = bits(instr, 8, 6);
703
+ const rs = bits(instr, 5, 3);
704
+ const rd = bits(instr, 2, 0);
705
+ const rsVal = this.registers[rs];
706
+ const operand = i === 1 ? rnImm : this.registers[rnImm];
707
+ const alu = op === 0 ? addWithFlags(rsVal | 0, operand | 0) : subWithFlags(rsVal | 0, operand | 0);
708
+ this.registers[rd] = alu.value >>> 0;
709
+ this.setFlags(alu.n, alu.z, alu.c, alu.v);
710
+ }
711
+ #thumbImmOp(instr) {
712
+ const op = bits(instr, 12, 11);
713
+ const rd = bits(instr, 10, 8);
714
+ const imm8 = instr & 0xff;
715
+ const rdVal = this.registers[rd];
716
+ switch (op) {
717
+ case 0: // MOV
718
+ this.registers[rd] = imm8;
719
+ this.setN(false);
720
+ this.setZ(imm8 === 0);
721
+ return;
722
+ case 1: {
723
+ // CMP
724
+ const alu = subWithFlags(rdVal | 0, imm8);
725
+ this.setFlags(alu.n, alu.z, alu.c, alu.v);
726
+ return;
727
+ }
728
+ case 2: {
729
+ // ADD
730
+ const alu = addWithFlags(rdVal | 0, imm8);
731
+ this.registers[rd] = alu.value >>> 0;
732
+ this.setFlags(alu.n, alu.z, alu.c, alu.v);
733
+ return;
734
+ }
735
+ case 3: {
736
+ // SUB
737
+ const alu = subWithFlags(rdVal | 0, imm8);
738
+ this.registers[rd] = alu.value >>> 0;
739
+ this.setFlags(alu.n, alu.z, alu.c, alu.v);
740
+ return;
741
+ }
742
+ }
743
+ }
744
+ #thumbAluOp(instr) {
745
+ const op = bits(instr, 9, 6);
746
+ const rs = bits(instr, 5, 3);
747
+ const rd = bits(instr, 2, 0);
748
+ const rdVal = this.registers[rd] | 0;
749
+ const rsVal = this.registers[rs] | 0;
750
+ let result;
751
+ let carry = this.getC();
752
+ let overflow = this.getV();
753
+ let writeResult = true;
754
+ switch (op) {
755
+ case 0x0:
756
+ result = rdVal & rsVal;
757
+ break;
758
+ case 0x1:
759
+ result = rdVal ^ rsVal;
760
+ break;
761
+ case 0x2: {
762
+ const amount = rsVal & 0xff;
763
+ [result, carry] = lsl(rdVal, amount, this.getC());
764
+ break;
765
+ }
766
+ case 0x3: {
767
+ const amount = rsVal & 0xff;
768
+ [result, carry] = lsr(rdVal, amount, this.getC());
769
+ break;
770
+ }
771
+ case 0x4: {
772
+ const amount = rsVal & 0xff;
773
+ [result, carry] = asr(rdVal, amount, this.getC());
774
+ break;
775
+ }
776
+ case 0x5: {
777
+ const alu = addWithFlags(rdVal, rsVal, this.getC() ? 1 : 0);
778
+ result = alu.value;
779
+ carry = alu.c;
780
+ overflow = alu.v;
781
+ break;
782
+ }
783
+ case 0x6: {
784
+ const alu = subWithFlags(rdVal, rsVal, this.getC() ? 1 : 0);
785
+ result = alu.value;
786
+ carry = alu.c;
787
+ overflow = alu.v;
788
+ break;
789
+ }
790
+ case 0x7: {
791
+ const amount = rsVal & 0xff;
792
+ [result, carry] = ror(rdVal, amount, this.getC());
793
+ break;
794
+ }
795
+ case 0x8:
796
+ result = rdVal & rsVal;
797
+ writeResult = false;
798
+ break;
799
+ case 0x9: {
800
+ const alu = subWithFlags(0, rsVal);
801
+ result = alu.value;
802
+ carry = alu.c;
803
+ overflow = alu.v;
804
+ break;
805
+ }
806
+ case 0xa: {
807
+ const alu = subWithFlags(rdVal, rsVal);
808
+ result = alu.value;
809
+ carry = alu.c;
810
+ overflow = alu.v;
811
+ writeResult = false;
812
+ break;
813
+ }
814
+ case 0xb: {
815
+ const alu = addWithFlags(rdVal, rsVal);
816
+ result = alu.value;
817
+ carry = alu.c;
818
+ overflow = alu.v;
819
+ writeResult = false;
820
+ break;
821
+ }
822
+ case 0xc:
823
+ result = rdVal | rsVal;
824
+ break;
825
+ case 0xd:
826
+ result = Math.imul(rdVal, rsVal);
827
+ carry = false;
828
+ break;
829
+ case 0xe:
830
+ result = rdVal & ~rsVal;
831
+ break;
832
+ case 0xf:
833
+ result = ~rsVal;
834
+ break;
835
+ default:
836
+ return;
837
+ }
838
+ if (writeResult) {
839
+ this.registers[rd] = result >>> 0;
840
+ }
841
+ this.setN((result & 0x80000000) !== 0);
842
+ this.setZ(result >>> 0 === 0);
843
+ this.setC(carry);
844
+ this.setV(overflow);
845
+ }
846
+ #thumbHiRegBx(instr) {
847
+ const op = bits(instr, 9, 8);
848
+ const hd = bit(instr, 7);
849
+ const hs = bit(instr, 6);
850
+ const rs = bits(instr, 5, 3) | (hs << 3);
851
+ const rd = bits(instr, 2, 0) | (hd << 3);
852
+ let rsVal = this.registers[rs];
853
+ // Pipeline correction: PC reads as instrAddr + 4 in Thumb mode
854
+ if (rs === PC) {
855
+ rsVal = (rsVal + 2) >>> 0;
856
+ }
857
+ // Pipeline correction for Rd=PC (reads as instrAddr + 4)
858
+ let rdVal = this.registers[rd];
859
+ if (rd === PC) {
860
+ rdVal = (rdVal + 2) >>> 0;
861
+ }
862
+ switch (op) {
863
+ case 0: // ADD
864
+ this.registers[rd] = (rdVal + rsVal) >>> 0;
865
+ if (rd === PC) {
866
+ this.registers[PC] = this.registers[PC] & ~1;
867
+ }
868
+ break;
869
+ case 1: {
870
+ // CMP
871
+ const alu = subWithFlags(rdVal | 0, rsVal | 0);
872
+ this.setFlags(alu.n, alu.z, alu.c, alu.v);
873
+ break;
874
+ }
875
+ case 2: // MOV
876
+ this.registers[rd] = rsVal;
877
+ if (rd === PC) {
878
+ this.registers[PC] = this.registers[PC] & ~1;
879
+ }
880
+ break;
881
+ case 3: // BX
882
+ if ((rsVal & ~1) === SENTINEL_ADDR || (rsVal & ~1) === (SENTINEL_ADDR & ~1)) {
883
+ this.#halted = true;
884
+ return;
885
+ }
886
+ // T bit determined by bit 0
887
+ this.setT((rsVal & 1) !== 0);
888
+ this.registers[PC] = rsVal & ~1;
889
+ break;
890
+ }
891
+ }
892
+ #thumbRegOffsetLoadStore(instr) {
893
+ const l = bit(instr, 11);
894
+ const b = bit(instr, 10);
895
+ const ro = bits(instr, 8, 6);
896
+ const rb = bits(instr, 5, 3);
897
+ const rd = bits(instr, 2, 0);
898
+ const address = (this.registers[rb] + this.registers[ro]) >>> 0;
899
+ if (l === 1) {
900
+ if (b === 1) {
901
+ this.registers[rd] = this.memory.read8(address);
902
+ }
903
+ else {
904
+ // ARM7TDMI: unaligned word reads rotate
905
+ const aligned = address & ~3;
906
+ const rotation = (address & 3) * 8;
907
+ let value = this.memory.read32(aligned);
908
+ if (rotation) {
909
+ value = ((value >>> rotation) | (value << (32 - rotation))) >>> 0;
910
+ }
911
+ this.registers[rd] = value;
912
+ }
913
+ }
914
+ else {
915
+ if (b === 1) {
916
+ this.memory.write8(address, this.registers[rd]);
917
+ }
918
+ else {
919
+ this.memory.write32(address & ~3, this.registers[rd]);
920
+ }
921
+ }
922
+ }
923
+ #thumbSignExtLoadStore(instr) {
924
+ const h = bit(instr, 11);
925
+ const s = bit(instr, 10);
926
+ const ro = bits(instr, 8, 6);
927
+ const rb = bits(instr, 5, 3);
928
+ const rd = bits(instr, 2, 0);
929
+ const address = (this.registers[rb] + this.registers[ro]) >>> 0;
930
+ if (s === 0 && h === 0) {
931
+ this.memory.write16(address, this.registers[rd]);
932
+ }
933
+ else if (s === 0 && h === 1) {
934
+ this.registers[rd] = this.memory.read16(address);
935
+ }
936
+ else if (s === 1 && h === 0) {
937
+ this.registers[rd] = signExtend(this.memory.read8(address), 8) >>> 0;
938
+ }
939
+ else {
940
+ if (address & 1) {
941
+ this.registers[rd] = signExtend(this.memory.read8(address), 8) >>> 0;
942
+ }
943
+ else {
944
+ this.registers[rd] = signExtend(this.memory.read16(address), 16) >>> 0;
945
+ }
946
+ }
947
+ }
948
+ #thumbImmOffsetLoadStore(instr) {
949
+ const b = bit(instr, 12);
950
+ const l = bit(instr, 11);
951
+ const offset5 = bits(instr, 10, 6);
952
+ const rb = bits(instr, 5, 3);
953
+ const rd = bits(instr, 2, 0);
954
+ const base = this.registers[rb];
955
+ const offset = b === 0 ? offset5 << 2 : offset5;
956
+ const address = (base + offset) >>> 0;
957
+ if (l === 1) {
958
+ if (b === 1) {
959
+ this.registers[rd] = this.memory.read8(address);
960
+ }
961
+ else {
962
+ // ARM7TDMI: unaligned word reads rotate
963
+ const aligned = address & ~3;
964
+ const rotation = (address & 3) * 8;
965
+ let value = this.memory.read32(aligned);
966
+ if (rotation) {
967
+ value = ((value >>> rotation) | (value << (32 - rotation))) >>> 0;
968
+ }
969
+ this.registers[rd] = value;
970
+ }
971
+ }
972
+ else {
973
+ if (b === 1) {
974
+ this.memory.write8(address, this.registers[rd]);
975
+ }
976
+ else {
977
+ this.memory.write32(address & ~3, this.registers[rd]);
978
+ }
979
+ }
980
+ }
981
+ #thumbPushPop(instr) {
982
+ const l = bit(instr, 11);
983
+ const r = bit(instr, 8);
984
+ const rlist = instr & 0xff;
985
+ if (l === 0) {
986
+ // PUSH
987
+ let sp = this.registers[SP];
988
+ let count = 0;
989
+ for (let i = 0; i < 8; i++) {
990
+ if (rlist & (1 << i)) {
991
+ count++;
992
+ }
993
+ }
994
+ if (r) {
995
+ count++;
996
+ }
997
+ sp = (sp - count * 4) >>> 0;
998
+ this.registers[SP] = sp;
999
+ let addr = sp;
1000
+ for (let i = 0; i < 8; i++) {
1001
+ if (rlist & (1 << i)) {
1002
+ this.memory.write32(addr, this.registers[i]);
1003
+ addr = (addr + 4) >>> 0;
1004
+ }
1005
+ }
1006
+ if (r) {
1007
+ this.memory.write32(addr, this.registers[LR]);
1008
+ }
1009
+ }
1010
+ else {
1011
+ // POP
1012
+ let addr = this.registers[SP];
1013
+ for (let i = 0; i < 8; i++) {
1014
+ if (rlist & (1 << i)) {
1015
+ this.registers[i] = this.memory.read32(addr);
1016
+ addr = (addr + 4) >>> 0;
1017
+ }
1018
+ }
1019
+ if (r) {
1020
+ const val = this.memory.read32(addr);
1021
+ addr = (addr + 4) >>> 0;
1022
+ if ((val & ~1) === SENTINEL_ADDR || (val & ~1) === (SENTINEL_ADDR & ~1)) {
1023
+ this.#halted = true;
1024
+ this.registers[SP] = addr;
1025
+ return;
1026
+ }
1027
+ this.registers[PC] = val & ~1;
1028
+ }
1029
+ this.registers[SP] = addr;
1030
+ }
1031
+ }
1032
+ #thumbBlockTransfer(instr) {
1033
+ const l = bit(instr, 11);
1034
+ const rb = bits(instr, 10, 8);
1035
+ const rlist = instr & 0xff;
1036
+ let addr = this.registers[rb];
1037
+ if (l === 1) {
1038
+ // LDMIA
1039
+ for (let i = 0; i < 8; i++) {
1040
+ if (rlist & (1 << i)) {
1041
+ this.registers[i] = this.memory.read32(addr);
1042
+ addr = (addr + 4) >>> 0;
1043
+ }
1044
+ }
1045
+ // No writeback if Rb is in the register list (loaded value takes precedence)
1046
+ if (!(rlist & (1 << rb))) {
1047
+ this.registers[rb] = addr;
1048
+ }
1049
+ }
1050
+ else {
1051
+ // STMIA
1052
+ for (let i = 0; i < 8; i++) {
1053
+ if (rlist & (1 << i)) {
1054
+ this.memory.write32(addr, this.registers[i]);
1055
+ addr = (addr + 4) >>> 0;
1056
+ }
1057
+ }
1058
+ this.registers[rb] = addr;
1059
+ }
1060
+ }
1061
+ #thumbCondBranch(instr) {
1062
+ const cond = bits(instr, 11, 8);
1063
+ const offset8 = signExtend(instr & 0xff, 8);
1064
+ if (!checkCondition(cond, this.getN(), this.getZ(), this.getC(), this.getV())) {
1065
+ return;
1066
+ }
1067
+ // ARM7TDMI pipeline: PC reads as instrAddr+4 in Thumb mode.
1068
+ // registers[PC] is instrAddr+2 (pre-incremented), so add +2 for pipeline.
1069
+ this.registers[PC] = (this.registers[PC] + 2 + offset8 * 2) >>> 0;
1070
+ }
1071
+ #thumbBlPrefix(instr) {
1072
+ const offset11 = signExtend(instr & 0x7ff, 11);
1073
+ // ARM7TDMI pipeline: PC = instrAddr+4. registers[PC] = instrAddr+2, so add +2.
1074
+ this.registers[LR] = (this.registers[PC] + 2 + (offset11 << 12)) >>> 0;
1075
+ }
1076
+ #thumbBlSuffix(instr) {
1077
+ const offset11 = (instr & 0x7ff) << 1;
1078
+ const target = (this.registers[LR] + offset11) >>> 0;
1079
+ this.registers[LR] = (this.registers[PC] | 1) >>> 0;
1080
+ this.registers[PC] = target & ~1;
1081
+ }
1082
+ // ─── ARM Execution ───────────────────────────────────────────────
1083
+ /** Execute one ARM (32-bit) instruction */
1084
+ #stepArm() {
1085
+ const pc = this.registers[PC];
1086
+ const instrAddr = pc & ~3;
1087
+ const instr = this.memory.read32(instrAddr);
1088
+ if (this.#hooks?.onInstructionPre) {
1089
+ const action = this.#hooks.onInstructionPre(instrAddr, instr);
1090
+ if (action === 'break') {
1091
+ return false;
1092
+ }
1093
+ }
1094
+ // Advance PC by 4 (ARM instructions are 4 bytes)
1095
+ this.registers[PC] = (pc + 4) >>> 0;
1096
+ // Check condition code (bits 31-28)
1097
+ const cond = (instr >>> 28) & 0xf;
1098
+ if (checkCondition(cond, this.getN(), this.getZ(), this.getC(), this.getV())) {
1099
+ this.#executeArm(instr, instrAddr);
1100
+ }
1101
+ this.#hooks?.onInstructionPost?.(instrAddr, instr);
1102
+ return !this.#halted;
1103
+ }
1104
+ /**
1105
+ * Decode and execute a single 32-bit ARM instruction.
1106
+ *
1107
+ * ARM instruction categories by bits 27-25:
1108
+ * - 00x: Data Processing / Multiply / Misc
1109
+ * - 010: Load/Store Word/Byte (immediate offset)
1110
+ * - 011: Load/Store Word/Byte (register offset)
1111
+ * - 100: Block Data Transfer (LDM/STM)
1112
+ * - 101: Branch (B/BL)
1113
+ * - 110: Coprocessor (undefined for GBA)
1114
+ * - 111: SWI / Coprocessor
1115
+ */
1116
+ #executeArm(instr, instrAddr) {
1117
+ const bits27_25 = bits(instr, 27, 25);
1118
+ switch (bits27_25) {
1119
+ case 0b000:
1120
+ case 0b001:
1121
+ this.#armDataProcessingOrMisc(instr, instrAddr);
1122
+ break;
1123
+ case 0b010:
1124
+ this.#armSingleDataTransferImm(instr);
1125
+ break;
1126
+ case 0b011:
1127
+ if (bit(instr, 4) === 0) {
1128
+ this.#armSingleDataTransferReg(instr);
1129
+ }
1130
+ else {
1131
+ // Undefined instruction on ARM7TDMI
1132
+ this.enterUnd(instrAddr);
1133
+ }
1134
+ break;
1135
+ case 0b100:
1136
+ this.#armBlockDataTransfer(instr);
1137
+ break;
1138
+ case 0b101:
1139
+ this.#armBranch(instr, instrAddr);
1140
+ break;
1141
+ case 0b110:
1142
+ // Coprocessor — undefined on GBA
1143
+ break;
1144
+ case 0b111:
1145
+ if (bit(instr, 24) === 1) {
1146
+ // SWI
1147
+ this.#armSwi(instr);
1148
+ }
1149
+ // else coprocessor operations — undefined on GBA
1150
+ break;
1151
+ }
1152
+ }
1153
+ // ─── ARM Data Processing / Misc ──────────────────────────────────
1154
+ /**
1155
+ * Handle ARM data processing instructions and miscellaneous instructions
1156
+ * that share the same top bits (27-25 = 00x).
1157
+ */
1158
+ #armDataProcessingOrMisc(instr, _instrAddr) {
1159
+ // Check for multiplies: bits 27-22=000000, bits 7-4=1001
1160
+ if ((instr & 0x0fc000f0) === 0x00000090) {
1161
+ this.#armMultiply(instr);
1162
+ return;
1163
+ }
1164
+ // Check for long multiplies: bits 27-23=00001, bits 7-4=1001
1165
+ if ((instr & 0x0f8000f0) === 0x00800090) {
1166
+ this.#armMultiplyLong(instr);
1167
+ return;
1168
+ }
1169
+ // Check for single data swap: bits 27-23=00010, bits 11-4=00001001
1170
+ if ((instr & 0x0fb00ff0) === 0x01000090) {
1171
+ this.#armSwap(instr);
1172
+ return;
1173
+ }
1174
+ // Check for BX: 0001_0010_1111_1111_1111_0001
1175
+ if ((instr & 0x0ffffff0) === 0x012fff10) {
1176
+ this.#armBx(instr);
1177
+ return;
1178
+ }
1179
+ // Check for halfword/signed transfers: bits 27-25=000, bit7=1, bit4=1
1180
+ // But NOT multiply (already checked above)
1181
+ if ((instr & 0x0e000090) === 0x00000090 && (instr & 0x00000060) !== 0) {
1182
+ this.#armHalfwordTransfer(instr);
1183
+ return;
1184
+ }
1185
+ // Check for MRS: bits 27-23=00010, bits 21-20=00, bits 11-0=0000_0000_0000
1186
+ if ((instr & 0x0fbf0fff) === 0x010f0000) {
1187
+ this.#armMrs(instr);
1188
+ return;
1189
+ }
1190
+ // Check for MSR (register): bits 27-25=000, bits 24-23=10, bit 21=1, bit 20=0,
1191
+ // bits 15-12=1111, bits 11-4=00000000. Field mask (bits 19-16) varies.
1192
+ if ((instr & 0x0fb0fff0) === 0x0120f000) {
1193
+ this.#armMsrReg(instr);
1194
+ return;
1195
+ }
1196
+ // Check for MSR (immediate): bits 27-25=001, bits 24-23=10, bit 21=1, bit 20=0,
1197
+ // bits 15-12=1111. Field mask (bits 19-16) varies.
1198
+ if ((instr & 0x0fb0f000) === 0x0320f000) {
1199
+ this.#armMsrImm(instr);
1200
+ return;
1201
+ }
1202
+ // Data Processing instruction
1203
+ this.#armDataProcessing(instr);
1204
+ }
1205
+ /** ARM barrel shifter: compute the shifter operand and carry out */
1206
+ #armBarrelShifter(instr, isImmediate) {
1207
+ if (isImmediate) {
1208
+ // Immediate: 8-bit value rotated right by 2*rotate4
1209
+ const imm8 = instr & 0xff;
1210
+ const rotate = ((instr >>> 8) & 0xf) * 2;
1211
+ if (rotate === 0) {
1212
+ return [imm8, this.getC()];
1213
+ }
1214
+ const result = (imm8 >>> rotate) | (imm8 << (32 - rotate)) | 0;
1215
+ return [result, ((result >>> 31) & 1) !== 0];
1216
+ }
1217
+ // Register operand
1218
+ const rm = instr & 0xf;
1219
+ let rmVal = this.registers[rm];
1220
+ const shiftType = (instr >>> 5) & 0x3;
1221
+ const regShift = bit(instr, 4);
1222
+ // ARM7TDMI pipeline: PC reads as instrAddr+8 normally, instrAddr+12 with register shift.
1223
+ // registers[PC] = instrAddr+4, so add +4 or +8 respectively.
1224
+ if (rm === PC) {
1225
+ rmVal = (rmVal + (regShift ? 8 : 4)) >>> 0;
1226
+ }
1227
+ let amount;
1228
+ if (regShift) {
1229
+ // Register-specified shift amount (Rs)
1230
+ const rsReg = (instr >>> 8) & 0xf;
1231
+ amount = this.registers[rsReg] & 0xff;
1232
+ }
1233
+ else {
1234
+ // Immediate-specified shift amount
1235
+ amount = (instr >>> 7) & 0x1f;
1236
+ }
1237
+ switch (shiftType) {
1238
+ case 0: // LSL
1239
+ return lsl(rmVal | 0, amount, this.getC());
1240
+ case 1: // LSR
1241
+ return lsr(rmVal | 0, amount, this.getC(), !regShift);
1242
+ case 2: // ASR
1243
+ return asr(rmVal | 0, amount, this.getC(), !regShift);
1244
+ case 3: // ROR
1245
+ if (!regShift && amount === 0) {
1246
+ // RRX (rotate right extended): shift right by 1, carry in from CPSR.C
1247
+ const carry = (rmVal & 1) !== 0;
1248
+ const result = (this.getC() ? 0x80000000 : 0) | (rmVal >>> 1) | 0;
1249
+ return [result, carry];
1250
+ }
1251
+ return ror(rmVal | 0, amount, this.getC());
1252
+ default:
1253
+ return [rmVal, this.getC()];
1254
+ }
1255
+ }
1256
+ /** ARM data processing instructions */
1257
+ #armDataProcessing(instr) {
1258
+ const isImm = bit(instr, 25);
1259
+ const opcode = bits(instr, 24, 21);
1260
+ const setFlags = bit(instr, 20) === 1;
1261
+ const rn = bits(instr, 19, 16);
1262
+ const rd = bits(instr, 15, 12);
1263
+ const [op2, shifterCarry] = this.#armBarrelShifter(instr, isImm === 1);
1264
+ // ARM7TDMI pipeline: PC reads as instrAddr+8 for data processing.
1265
+ // registers[PC] = instrAddr+4, so add +4 for Rn=PC.
1266
+ // For register-shifted: PC reads as instrAddr+12, so add +8.
1267
+ const regShift = isImm === 0 && bit(instr, 4) === 1;
1268
+ let rnVal = this.registers[rn];
1269
+ if (rn === PC) {
1270
+ rnVal = (rnVal + (regShift ? 8 : 4)) >>> 0;
1271
+ }
1272
+ let result;
1273
+ let carry = shifterCarry;
1274
+ let overflow = this.getV();
1275
+ let writeResult = true;
1276
+ switch (opcode) {
1277
+ case 0x0: // AND
1278
+ result = rnVal & op2;
1279
+ break;
1280
+ case 0x1: // EOR
1281
+ result = rnVal ^ op2;
1282
+ break;
1283
+ case 0x2: {
1284
+ // SUB
1285
+ const alu = subWithFlags(rnVal | 0, op2 | 0);
1286
+ result = alu.value;
1287
+ carry = alu.c;
1288
+ overflow = alu.v;
1289
+ break;
1290
+ }
1291
+ case 0x3: {
1292
+ // RSB
1293
+ const alu = subWithFlags(op2 | 0, rnVal | 0);
1294
+ result = alu.value;
1295
+ carry = alu.c;
1296
+ overflow = alu.v;
1297
+ break;
1298
+ }
1299
+ case 0x4: {
1300
+ // ADD
1301
+ const alu = addWithFlags(rnVal | 0, op2 | 0);
1302
+ result = alu.value;
1303
+ carry = alu.c;
1304
+ overflow = alu.v;
1305
+ break;
1306
+ }
1307
+ case 0x5: {
1308
+ // ADC
1309
+ const alu = addWithFlags(rnVal | 0, op2 | 0, this.getC() ? 1 : 0);
1310
+ result = alu.value;
1311
+ carry = alu.c;
1312
+ overflow = alu.v;
1313
+ break;
1314
+ }
1315
+ case 0x6: {
1316
+ // SBC
1317
+ const alu = subWithFlags(rnVal | 0, op2 | 0, this.getC() ? 1 : 0);
1318
+ result = alu.value;
1319
+ carry = alu.c;
1320
+ overflow = alu.v;
1321
+ break;
1322
+ }
1323
+ case 0x7: {
1324
+ // RSC
1325
+ const alu = subWithFlags(op2 | 0, rnVal | 0, this.getC() ? 1 : 0);
1326
+ result = alu.value;
1327
+ carry = alu.c;
1328
+ overflow = alu.v;
1329
+ break;
1330
+ }
1331
+ case 0x8: // TST
1332
+ result = rnVal & op2;
1333
+ writeResult = false;
1334
+ break;
1335
+ case 0x9: // TEQ
1336
+ result = rnVal ^ op2;
1337
+ writeResult = false;
1338
+ break;
1339
+ case 0xa: {
1340
+ // CMP
1341
+ const alu = subWithFlags(rnVal | 0, op2 | 0);
1342
+ result = alu.value;
1343
+ carry = alu.c;
1344
+ overflow = alu.v;
1345
+ writeResult = false;
1346
+ break;
1347
+ }
1348
+ case 0xb: {
1349
+ // CMN
1350
+ const alu = addWithFlags(rnVal | 0, op2 | 0);
1351
+ result = alu.value;
1352
+ carry = alu.c;
1353
+ overflow = alu.v;
1354
+ writeResult = false;
1355
+ break;
1356
+ }
1357
+ case 0xc: // ORR
1358
+ result = rnVal | op2;
1359
+ break;
1360
+ case 0xd: // MOV
1361
+ result = op2;
1362
+ break;
1363
+ case 0xe: // BIC
1364
+ result = rnVal & ~op2;
1365
+ break;
1366
+ case 0xf: // MVN
1367
+ result = ~op2;
1368
+ break;
1369
+ default:
1370
+ return;
1371
+ }
1372
+ if (writeResult) {
1373
+ this.registers[rd] = result >>> 0;
1374
+ }
1375
+ if (setFlags) {
1376
+ if (rd === PC) {
1377
+ // Data processing with S=1 and Rd=PC: copy SPSR to CPSR (return from exception)
1378
+ const spsr = this.getSPSR();
1379
+ const newMode = spsr & CPSR_MODE_MASK;
1380
+ const oldMode = this.getMode();
1381
+ // Must switch mode BEFORE overwriting CPSR, so banked registers are
1382
+ // properly saved (old mode) and restored (new mode).
1383
+ if (newMode !== oldMode) {
1384
+ this.switchMode(newMode);
1385
+ }
1386
+ this.cpsr = spsr;
1387
+ }
1388
+ else {
1389
+ this.setN((result & 0x80000000) !== 0);
1390
+ this.setZ(result >>> 0 === 0);
1391
+ this.setC(carry);
1392
+ this.setV(overflow);
1393
+ }
1394
+ }
1395
+ // If Rd is PC and we wrote to it (non-flag instructions), flush pipeline
1396
+ if (writeResult && rd === PC) {
1397
+ // If the T bit changed, the new mode takes effect on next fetch
1398
+ }
1399
+ }
1400
+ // ─── ARM Multiply ────────────────────────────────────────────────
1401
+ /** ARM multiply: MUL, MLA */
1402
+ #armMultiply(instr) {
1403
+ const accumulate = bit(instr, 21) === 1;
1404
+ const setFlags = bit(instr, 20) === 1;
1405
+ const rd = bits(instr, 19, 16);
1406
+ const rn = bits(instr, 15, 12);
1407
+ const rs = bits(instr, 11, 8);
1408
+ const rm = instr & 0xf;
1409
+ let result = Math.imul(this.registers[rm] | 0, this.registers[rs] | 0);
1410
+ if (accumulate) {
1411
+ result = (result + (this.registers[rn] | 0)) | 0;
1412
+ }
1413
+ this.registers[rd] = result >>> 0;
1414
+ if (setFlags) {
1415
+ this.setN((result & 0x80000000) !== 0);
1416
+ this.setZ(result >>> 0 === 0);
1417
+ // C is unpredictable on ARMv4T, V is unchanged
1418
+ }
1419
+ }
1420
+ /** ARM long multiply: UMULL, UMLAL, SMULL, SMLAL */
1421
+ #armMultiplyLong(instr) {
1422
+ const isSigned = bit(instr, 22) === 1;
1423
+ const accumulate = bit(instr, 21) === 1;
1424
+ const setFlags = bit(instr, 20) === 1;
1425
+ const rdHi = bits(instr, 19, 16);
1426
+ const rdLo = bits(instr, 15, 12);
1427
+ const rs = bits(instr, 11, 8);
1428
+ const rm = instr & 0xf;
1429
+ let resultHi;
1430
+ let resultLo;
1431
+ if (isSigned) {
1432
+ // SMULL/SMLAL: signed 32x32 -> 64
1433
+ const a = this.registers[rm] | 0;
1434
+ const b = this.registers[rs] | 0;
1435
+ // Use BigInt for 64-bit precision
1436
+ const product = BigInt(a) * BigInt(b);
1437
+ resultLo = Number(product & 0xffffffffn) >>> 0;
1438
+ resultHi = Number((product >> 32n) & 0xffffffffn) >>> 0;
1439
+ }
1440
+ else {
1441
+ // UMULL/UMLAL: unsigned 32x32 -> 64
1442
+ const a = this.registers[rm] >>> 0;
1443
+ const b = this.registers[rs] >>> 0;
1444
+ const product = BigInt(a) * BigInt(b);
1445
+ resultLo = Number(product & 0xffffffffn) >>> 0;
1446
+ resultHi = Number((product >> 32n) & 0xffffffffn) >>> 0;
1447
+ }
1448
+ if (accumulate) {
1449
+ // Add to existing RdHi:RdLo
1450
+ const accLo = this.registers[rdLo] >>> 0;
1451
+ const accHi = this.registers[rdHi] >>> 0;
1452
+ const sum = BigInt(resultHi) * 0x100000000n + BigInt(resultLo) + BigInt(accHi) * 0x100000000n + BigInt(accLo);
1453
+ resultLo = Number(sum & 0xffffffffn) >>> 0;
1454
+ resultHi = Number((sum >> 32n) & 0xffffffffn) >>> 0;
1455
+ }
1456
+ this.registers[rdLo] = resultLo;
1457
+ this.registers[rdHi] = resultHi;
1458
+ if (setFlags) {
1459
+ this.setN((resultHi & 0x80000000) !== 0);
1460
+ this.setZ(resultHi === 0 && resultLo === 0);
1461
+ // C, V are unpredictable
1462
+ }
1463
+ }
1464
+ // ─── ARM Swap ────────────────────────────────────────────────────
1465
+ /** ARM SWP/SWPB: atomic swap */
1466
+ #armSwap(instr) {
1467
+ const byteMode = bit(instr, 22) === 1;
1468
+ const rn = bits(instr, 19, 16);
1469
+ const rd = bits(instr, 15, 12);
1470
+ const rm = instr & 0xf;
1471
+ const address = this.registers[rn];
1472
+ if (byteMode) {
1473
+ const temp = this.memory.read8(address);
1474
+ this.memory.write8(address, this.registers[rm] & 0xff);
1475
+ this.registers[rd] = temp;
1476
+ }
1477
+ else {
1478
+ // SWP: unaligned word reads rotate (same as LDR)
1479
+ const aligned = address & ~3;
1480
+ const rotation = (address & 3) * 8;
1481
+ let temp = this.memory.read32(aligned);
1482
+ if (rotation) {
1483
+ temp = ((temp >>> rotation) | (temp << (32 - rotation))) >>> 0;
1484
+ }
1485
+ this.memory.write32(aligned, this.registers[rm]);
1486
+ this.registers[rd] = temp;
1487
+ }
1488
+ }
1489
+ // ─── ARM Branch Exchange ─────────────────────────────────────────
1490
+ /** ARM BX: branch and exchange instruction set */
1491
+ #armBx(instr) {
1492
+ const rm = instr & 0xf;
1493
+ const target = this.registers[rm];
1494
+ if ((target & ~1) === SENTINEL_ADDR || (target & ~1) === (SENTINEL_ADDR & ~1)) {
1495
+ this.#halted = true;
1496
+ return;
1497
+ }
1498
+ this.setT((target & 1) !== 0);
1499
+ this.registers[PC] = target & ~1;
1500
+ }
1501
+ // ─── ARM Branch ──────────────────────────────────────────────────
1502
+ /** ARM B/BL: branch (with optional link) */
1503
+ #armBranch(instr, instrAddr) {
1504
+ const link = bit(instr, 24) === 1;
1505
+ const offset = signExtend(instr & 0x00ffffff, 24) << 2;
1506
+ if (link) {
1507
+ // LR = address of instruction after this one
1508
+ this.registers[LR] = (instrAddr + 4) >>> 0;
1509
+ }
1510
+ // ARM7TDMI pipeline: PC = instrAddr+8. registers[PC] = instrAddr+4, so add +4.
1511
+ this.registers[PC] = (this.registers[PC] + 4 + offset) >>> 0;
1512
+ }
1513
+ // ─── ARM Single Data Transfer (Immediate offset) ─────────────────
1514
+ /** ARM LDR/STR with immediate offset */
1515
+ #armSingleDataTransferImm(instr) {
1516
+ const pre = bit(instr, 24) === 1;
1517
+ const up = bit(instr, 23) === 1;
1518
+ const byteMode = bit(instr, 22) === 1;
1519
+ const writeback = bit(instr, 21) === 1;
1520
+ const load = bit(instr, 20) === 1;
1521
+ const rn = bits(instr, 19, 16);
1522
+ const rd = bits(instr, 15, 12);
1523
+ const offset = instr & 0xfff;
1524
+ let base = this.registers[rn];
1525
+ if (rn === PC) {
1526
+ base = (base + 4) >>> 0; // PC+8 relative to instruction address
1527
+ }
1528
+ const effectiveOffset = up ? offset : -offset;
1529
+ let address;
1530
+ if (pre) {
1531
+ address = (base + effectiveOffset) >>> 0;
1532
+ }
1533
+ else {
1534
+ address = base;
1535
+ }
1536
+ if (load) {
1537
+ if (byteMode) {
1538
+ this.registers[rd] = this.memory.read8(address);
1539
+ }
1540
+ else {
1541
+ // ARM7TDMI: unaligned word reads rotate the value
1542
+ const aligned = address & ~3;
1543
+ const rotation = (address & 3) * 8;
1544
+ let value = this.memory.read32(aligned);
1545
+ if (rotation) {
1546
+ value = ((value >>> rotation) | (value << (32 - rotation))) >>> 0;
1547
+ }
1548
+ this.registers[rd] = value;
1549
+ }
1550
+ if (rd === PC) {
1551
+ this.registers[PC] = this.registers[PC] & ~3;
1552
+ }
1553
+ }
1554
+ else {
1555
+ let value = this.registers[rd];
1556
+ if (rd === PC) {
1557
+ value = (value + 4) >>> 0; // PC+12
1558
+ }
1559
+ if (byteMode) {
1560
+ this.memory.write8(address, value & 0xff);
1561
+ }
1562
+ else {
1563
+ this.memory.write32(address & ~3, value);
1564
+ }
1565
+ }
1566
+ // Writeback
1567
+ if (pre && writeback) {
1568
+ this.registers[rn] = (base + effectiveOffset) >>> 0;
1569
+ }
1570
+ else if (!pre) {
1571
+ // Post-indexed always writes back
1572
+ this.registers[rn] = (base + effectiveOffset) >>> 0;
1573
+ }
1574
+ }
1575
+ /** ARM LDR/STR with register offset */
1576
+ #armSingleDataTransferReg(instr) {
1577
+ const pre = bit(instr, 24) === 1;
1578
+ const up = bit(instr, 23) === 1;
1579
+ const byteMode = bit(instr, 22) === 1;
1580
+ const writeback = bit(instr, 21) === 1;
1581
+ const load = bit(instr, 20) === 1;
1582
+ const rn = bits(instr, 19, 16);
1583
+ const rd = bits(instr, 15, 12);
1584
+ // Compute shifted register offset
1585
+ const rm = instr & 0xf;
1586
+ const shiftType = bits(instr, 6, 5);
1587
+ const shiftAmount = bits(instr, 11, 7);
1588
+ let offset;
1589
+ const rmVal = this.registers[rm];
1590
+ switch (shiftType) {
1591
+ case 0: // LSL
1592
+ offset = shiftAmount === 0 ? rmVal : (rmVal << shiftAmount) >>> 0;
1593
+ break;
1594
+ case 1: // LSR
1595
+ offset = shiftAmount === 0 ? 0 : rmVal >>> shiftAmount;
1596
+ break;
1597
+ case 2: // ASR
1598
+ offset = shiftAmount === 0 ? (isNegative(rmVal) ? 0xffffffff : 0) : (rmVal | 0) >> shiftAmount;
1599
+ break;
1600
+ case 3: // ROR/RRX
1601
+ if (shiftAmount === 0) {
1602
+ // RRX
1603
+ offset = ((this.getC() ? 0x80000000 : 0) | (rmVal >>> 1)) >>> 0;
1604
+ }
1605
+ else {
1606
+ offset = ((rmVal >>> shiftAmount) | (rmVal << (32 - shiftAmount))) >>> 0;
1607
+ }
1608
+ break;
1609
+ default:
1610
+ offset = rmVal;
1611
+ }
1612
+ let base = this.registers[rn];
1613
+ if (rn === PC) {
1614
+ base = (base + 4) >>> 0;
1615
+ }
1616
+ const effectiveOffset = up ? offset : -offset | 0;
1617
+ let address;
1618
+ if (pre) {
1619
+ address = (base + effectiveOffset) >>> 0;
1620
+ }
1621
+ else {
1622
+ address = base;
1623
+ }
1624
+ if (load) {
1625
+ if (byteMode) {
1626
+ this.registers[rd] = this.memory.read8(address);
1627
+ }
1628
+ else {
1629
+ // ARM7TDMI: unaligned word reads rotate the value
1630
+ const aligned = address & ~3;
1631
+ const rotation = (address & 3) * 8;
1632
+ let value = this.memory.read32(aligned);
1633
+ if (rotation) {
1634
+ value = ((value >>> rotation) | (value << (32 - rotation))) >>> 0;
1635
+ }
1636
+ this.registers[rd] = value;
1637
+ }
1638
+ if (rd === PC) {
1639
+ this.registers[PC] = this.registers[PC] & ~3;
1640
+ }
1641
+ }
1642
+ else {
1643
+ let value = this.registers[rd];
1644
+ if (rd === PC) {
1645
+ value = (value + 4) >>> 0;
1646
+ }
1647
+ if (byteMode) {
1648
+ this.memory.write8(address, value & 0xff);
1649
+ }
1650
+ else {
1651
+ this.memory.write32(address & ~3, value);
1652
+ }
1653
+ }
1654
+ if (pre && writeback) {
1655
+ this.registers[rn] = (base + effectiveOffset) >>> 0;
1656
+ }
1657
+ else if (!pre) {
1658
+ this.registers[rn] = (base + effectiveOffset) >>> 0;
1659
+ }
1660
+ }
1661
+ // ─── ARM Halfword / Signed Transfer ──────────────────────────────
1662
+ /** ARM LDRH/STRH/LDRSB/LDRSH */
1663
+ #armHalfwordTransfer(instr) {
1664
+ const pre = bit(instr, 24) === 1;
1665
+ const up = bit(instr, 23) === 1;
1666
+ const immOffset = bit(instr, 22) === 1;
1667
+ const writeback = bit(instr, 21) === 1;
1668
+ const load = bit(instr, 20) === 1;
1669
+ const rn = bits(instr, 19, 16);
1670
+ const rd = bits(instr, 15, 12);
1671
+ const sh = bits(instr, 6, 5); // S and H bits: 01=H, 10=SB, 11=SH
1672
+ let offset;
1673
+ if (immOffset) {
1674
+ // Immediate: high nibble | low nibble
1675
+ offset = ((instr >>> 4) & 0xf0) | (instr & 0xf);
1676
+ }
1677
+ else {
1678
+ // Register
1679
+ const rm = instr & 0xf;
1680
+ offset = this.registers[rm];
1681
+ }
1682
+ let base = this.registers[rn];
1683
+ if (rn === PC) {
1684
+ base = (base + 4) >>> 0;
1685
+ }
1686
+ const effectiveOffset = up ? offset : -offset;
1687
+ let address;
1688
+ if (pre) {
1689
+ address = (base + effectiveOffset) >>> 0;
1690
+ }
1691
+ else {
1692
+ address = base;
1693
+ }
1694
+ if (load) {
1695
+ switch (sh) {
1696
+ case 0b01: // LDRH (unsigned halfword)
1697
+ // ARM7TDMI: unaligned LDRH reads aligned halfword then rotates by 8
1698
+ if (address & 1) {
1699
+ const hw = this.memory.read16(address & ~1);
1700
+ this.registers[rd] = ((hw >>> 8) | (hw << 24)) >>> 0;
1701
+ }
1702
+ else {
1703
+ this.registers[rd] = this.memory.read16(address);
1704
+ }
1705
+ break;
1706
+ case 0b10: // LDRSB (signed byte)
1707
+ this.registers[rd] = signExtend(this.memory.read8(address), 8) >>> 0;
1708
+ break;
1709
+ case 0b11: // LDRSH (signed halfword)
1710
+ // ARM7TDMI: unaligned LDRSH loads byte and sign-extends (like LDRSB)
1711
+ if (address & 1) {
1712
+ this.registers[rd] = signExtend(this.memory.read8(address), 8) >>> 0;
1713
+ }
1714
+ else {
1715
+ this.registers[rd] = signExtend(this.memory.read16(address), 16) >>> 0;
1716
+ }
1717
+ break;
1718
+ }
1719
+ }
1720
+ else {
1721
+ // STRH (only sh=01 for store) — ignores bit 0 (force halfword alignment)
1722
+ if (sh === 0b01) {
1723
+ this.memory.write16(address & ~1, this.registers[rd] & 0xffff);
1724
+ }
1725
+ }
1726
+ if (pre && writeback) {
1727
+ this.registers[rn] = (base + effectiveOffset) >>> 0;
1728
+ }
1729
+ else if (!pre) {
1730
+ this.registers[rn] = (base + effectiveOffset) >>> 0;
1731
+ }
1732
+ }
1733
+ // ─── ARM Block Data Transfer (LDM/STM) ───────────────────────────
1734
+ /** ARM LDM/STM */
1735
+ #armBlockDataTransfer(instr) {
1736
+ const pre = bit(instr, 24) === 1;
1737
+ const up = bit(instr, 23) === 1;
1738
+ const sBit = bit(instr, 22) === 1; // PSR & force user mode
1739
+ const writeback = bit(instr, 21) === 1;
1740
+ const load = bit(instr, 20) === 1;
1741
+ const rn = bits(instr, 19, 16);
1742
+ const rlist = instr & 0xffff;
1743
+ let base = this.registers[rn];
1744
+ // Count registers in the list
1745
+ let regCount = 0;
1746
+ for (let i = 0; i < 16; i++) {
1747
+ if (rlist & (1 << i)) {
1748
+ regCount++;
1749
+ }
1750
+ }
1751
+ if (regCount === 0) {
1752
+ // Edge case: empty register list
1753
+ return;
1754
+ }
1755
+ // Calculate start address based on direction
1756
+ let address;
1757
+ if (up) {
1758
+ address = pre ? (base + 4) >>> 0 : base;
1759
+ }
1760
+ else {
1761
+ // Down: start from base - regCount*4
1762
+ address = pre ? (base - regCount * 4) >>> 0 : (base - regCount * 4 + 4) >>> 0;
1763
+ }
1764
+ if (load) {
1765
+ for (let i = 0; i < 16; i++) {
1766
+ if (rlist & (1 << i)) {
1767
+ this.registers[i] = this.memory.read32(address);
1768
+ address = (address + 4) >>> 0;
1769
+ }
1770
+ }
1771
+ // If PC is in the list and S bit is set, restore CPSR from SPSR
1772
+ if (rlist & (1 << PC) && sBit) {
1773
+ const spsr = this.getSPSR();
1774
+ const newMode = spsr & 0x1f;
1775
+ const oldMode = this.getMode();
1776
+ if (newMode !== oldMode) {
1777
+ this.switchMode(newMode);
1778
+ }
1779
+ this.cpsr = spsr;
1780
+ }
1781
+ // If PC was loaded, align it
1782
+ if (rlist & (1 << PC)) {
1783
+ this.registers[PC] = this.registers[PC] & ~3;
1784
+ }
1785
+ }
1786
+ else {
1787
+ for (let i = 0; i < 16; i++) {
1788
+ if (rlist & (1 << i)) {
1789
+ let value = this.registers[i];
1790
+ if (i === PC) {
1791
+ value = (value + 4) >>> 0; // PC+12
1792
+ }
1793
+ this.memory.write32(address, value);
1794
+ address = (address + 4) >>> 0;
1795
+ }
1796
+ }
1797
+ }
1798
+ // Writeback (for LDM: no writeback if Rn is in the register list)
1799
+ if (writeback && !(load && rlist & (1 << rn))) {
1800
+ if (up) {
1801
+ this.registers[rn] = (base + regCount * 4) >>> 0;
1802
+ }
1803
+ else {
1804
+ this.registers[rn] = (base - regCount * 4) >>> 0;
1805
+ }
1806
+ }
1807
+ }
1808
+ // ─── ARM MRS/MSR ─────────────────────────────────────────────────
1809
+ /** MRS: Move PSR to register */
1810
+ #armMrs(instr) {
1811
+ const useSPSR = bit(instr, 22) === 1;
1812
+ const rd = bits(instr, 15, 12);
1813
+ this.registers[rd] = useSPSR ? this.getSPSR() : this.cpsr;
1814
+ }
1815
+ /** MSR (register): Move register to PSR */
1816
+ #armMsrReg(instr) {
1817
+ const useSPSR = bit(instr, 22) === 1;
1818
+ const rm = instr & 0xf;
1819
+ const value = this.registers[rm];
1820
+ this.#writePsr(value, useSPSR, bits(instr, 19, 16));
1821
+ }
1822
+ /** MSR (immediate): Move immediate to PSR flags */
1823
+ #armMsrImm(instr) {
1824
+ const useSPSR = bit(instr, 22) === 1;
1825
+ const imm8 = instr & 0xff;
1826
+ const rotate = ((instr >>> 8) & 0xf) * 2;
1827
+ let value;
1828
+ if (rotate === 0) {
1829
+ value = imm8;
1830
+ }
1831
+ else {
1832
+ value = ((imm8 >>> rotate) | (imm8 << (32 - rotate))) >>> 0;
1833
+ }
1834
+ this.#writePsr(value, useSPSR, bits(instr, 19, 16));
1835
+ }
1836
+ /** Write to PSR based on field mask */
1837
+ #writePsr(value, useSPSR, fieldMask) {
1838
+ let mask = 0;
1839
+ if (fieldMask & 0x1) {
1840
+ mask |= 0x000000ff;
1841
+ } // control
1842
+ if (fieldMask & 0x2) {
1843
+ mask |= 0x0000ff00;
1844
+ } // extension
1845
+ if (fieldMask & 0x4) {
1846
+ mask |= 0x00ff0000;
1847
+ } // status
1848
+ if (fieldMask & 0x8) {
1849
+ mask |= 0xff000000;
1850
+ } // flags
1851
+ if (useSPSR) {
1852
+ const current = this.getSPSR();
1853
+ this.setSPSR((current & ~mask) | (value & mask));
1854
+ }
1855
+ else {
1856
+ const oldMode = this.getMode();
1857
+ this.cpsr = (this.cpsr & ~mask) | (value & mask);
1858
+ const newMode = this.getMode();
1859
+ if (newMode !== oldMode) {
1860
+ // Need to re-bank registers
1861
+ this.#saveBankedRegisters(oldMode);
1862
+ this.#restoreBankedRegisters(newMode);
1863
+ }
1864
+ }
1865
+ }
1866
+ // ─── ARM SWI ─────────────────────────────────────────────────────
1867
+ /** ARM Software Interrupt */
1868
+ #armSwi(instr) {
1869
+ // The SWI number encoding is platform-specific. On GBA it's bits 23-16.
1870
+ // We pass the full 24-bit comment field; the handler extracts what it needs.
1871
+ const swiNumber = (instr >>> 16) & 0xff;
1872
+ this.#swiHandler?.(this, swiNumber);
1873
+ }
1874
+ }
1875
+ //# sourceMappingURL=arm-cpu.js.map