jsbeeb 1.22.4 → 1.23.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/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "name": "jsbeeb",
8
8
  "description": "Emulate a BBC Micro",
9
9
  "repository": "git@github.com:mattgodbolt/jsbeeb.git",
10
- "version": "1.22.4",
10
+ "version": "1.23.0",
11
11
  "//engines": "If you change the version of Node, it must also be updated at the top of the Dockerfile.",
12
12
  "engines": {
13
13
  "node": ">=24.15.0"
@@ -29,6 +29,7 @@
29
29
  "@popperjs/core": "^2.11.8",
30
30
  "argparse": "^3.0.0",
31
31
  "bootstrap": "^5.3.8",
32
+ "bootstrap-icons": "^1.13.1",
32
33
  "bootswatch": "^5.3.8",
33
34
  "pako": "^3.0.1",
34
35
  "sharp": "^0.35.3",
@@ -52,7 +53,7 @@
52
53
  "vitest": "^4.0.10"
53
54
  },
54
55
  "optionalDependencies": {
55
- "electron": "^43.2.0",
56
+ "electron": "^44.0.0",
56
57
  "electron-builder": "^26.15.3",
57
58
  "electron-store": "^11.0.2"
58
59
  },
@@ -525,254 +525,269 @@ function getOp(op, arg) {
525
525
  throw new Error(`Unrecognised operation '${op}'`);
526
526
  }
527
527
 
528
- const opcodes6502 = {
528
+ // Undocumented opcodes carry a leading "*", as 6502 listings conventionally show them.
529
+ function splitUndocumented(markedOpcodes) {
530
+ const opcodes = {};
531
+ const undocumented = new Set();
532
+ for (const [op, text] of Object.entries(markedOpcodes)) {
533
+ if (text.startsWith("*")) {
534
+ undocumented.add(Number(op));
535
+ opcodes[op] = text.slice(1);
536
+ } else {
537
+ opcodes[op] = text;
538
+ }
539
+ }
540
+ return { opcodes, undocumented };
541
+ }
542
+
543
+ const { opcodes: opcodes6502, undocumented: undocumented6502 } = splitUndocumented({
529
544
  0x00: "BRK",
530
545
  0x01: "ORA (,x)",
531
- 0x03: "SLO (,x)",
532
- 0x04: "NOP zp",
546
+ 0x03: "*SLO (,x)",
547
+ 0x04: "*NOP zp",
533
548
  0x05: "ORA zp",
534
549
  0x06: "ASL zp",
535
- 0x07: "SLO zp",
550
+ 0x07: "*SLO zp",
536
551
  0x08: "PHP",
537
552
  0x09: "ORA imm",
538
553
  0x0a: "ASL A",
539
- 0x0b: "ANC imm",
540
- 0x0c: "NOP abs",
554
+ 0x0b: "*ANC imm",
555
+ 0x0c: "*NOP abs",
541
556
  0x0d: "ORA abs",
542
557
  0x0e: "ASL abs",
543
- 0x0f: "SLO abs",
558
+ 0x0f: "*SLO abs",
544
559
  0x10: "BPL branch",
545
560
  0x11: "ORA (),y",
546
- 0x13: "SLO (),y",
547
- 0x14: "NOP zp,x",
561
+ 0x13: "*SLO (),y",
562
+ 0x14: "*NOP zp,x",
548
563
  0x15: "ORA zp,x",
549
564
  0x16: "ASL zp,x",
550
- 0x17: "SLO zp,x",
565
+ 0x17: "*SLO zp,x",
551
566
  0x18: "CLC",
552
567
  0x19: "ORA abs,y",
553
- 0x1a: "NOP",
554
- 0x1b: "SLO abs,y",
555
- 0x1c: "NOP abs,x",
568
+ 0x1a: "*NOP",
569
+ 0x1b: "*SLO abs,y",
570
+ 0x1c: "*NOP abs,x",
556
571
  0x1d: "ORA abs,x",
557
572
  0x1e: "ASL abs,x",
558
- 0x1f: "SLO abs,x",
573
+ 0x1f: "*SLO abs,x",
559
574
  0x20: "JSR abs",
560
575
  0x21: "AND (,x)",
561
- 0x23: "RLA (,x)",
576
+ 0x23: "*RLA (,x)",
562
577
  0x24: "BIT zp",
563
578
  0x25: "AND zp",
564
579
  0x26: "ROL zp",
565
- 0x27: "RLA zp",
580
+ 0x27: "*RLA zp",
566
581
  0x28: "PLP",
567
582
  0x29: "AND imm",
568
583
  0x2a: "ROL A",
569
- 0x2b: "ANC imm",
584
+ 0x2b: "*ANC imm",
570
585
  0x2c: "BIT abs",
571
586
  0x2d: "AND abs",
572
587
  0x2e: "ROL abs",
573
- 0x2f: "RLA abs",
588
+ 0x2f: "*RLA abs",
574
589
  0x30: "BMI branch",
575
590
  0x31: "AND (),y",
576
- 0x33: "RLA (),y",
577
- 0x34: "NOP zp,x",
591
+ 0x33: "*RLA (),y",
592
+ 0x34: "*NOP zp,x",
578
593
  0x35: "AND zp,x",
579
594
  0x36: "ROL zp,x",
580
- 0x37: "RLA zp,x",
595
+ 0x37: "*RLA zp,x",
581
596
  0x38: "SEC",
582
597
  0x39: "AND abs,y",
583
- 0x3a: "NOP",
584
- 0x3b: "RLA abs,y",
585
- 0x3c: "NOP abs,x",
598
+ 0x3a: "*NOP",
599
+ 0x3b: "*RLA abs,y",
600
+ 0x3c: "*NOP abs,x",
586
601
  0x3d: "AND abs,x",
587
602
  0x3e: "ROL abs,x",
588
- 0x3f: "RLA abs,x",
603
+ 0x3f: "*RLA abs,x",
589
604
  0x40: "RTI",
590
605
  0x41: "EOR (,x)",
591
- 0x43: "SRE (,x)",
592
- 0x44: "NOP zp",
606
+ 0x43: "*SRE (,x)",
607
+ 0x44: "*NOP zp",
593
608
  0x45: "EOR zp",
594
609
  0x46: "LSR zp",
595
- 0x47: "SRE zp",
610
+ 0x47: "*SRE zp",
596
611
  0x48: "PHA",
597
612
  0x49: "EOR imm",
598
613
  0x4a: "LSR A",
599
- 0x4b: "ASR imm",
614
+ 0x4b: "*ASR imm",
600
615
  0x4c: "JMP abs",
601
616
  0x4d: "EOR abs",
602
617
  0x4e: "LSR abs",
603
- 0x4f: "SRE abs",
618
+ 0x4f: "*SRE abs",
604
619
  0x50: "BVC branch",
605
620
  0x51: "EOR (),y",
606
- 0x53: "SRE (),y",
607
- 0x54: "NOP zp,x",
621
+ 0x53: "*SRE (),y",
622
+ 0x54: "*NOP zp,x",
608
623
  0x55: "EOR zp,x",
609
624
  0x56: "LSR zp,x",
610
- 0x57: "SRE zp,x",
625
+ 0x57: "*SRE zp,x",
611
626
  0x58: "CLI",
612
627
  0x59: "EOR abs,y",
613
- 0x5a: "NOP",
614
- 0x5b: "SRE abs,y",
615
- 0x5c: "NOP abs,x",
628
+ 0x5a: "*NOP",
629
+ 0x5b: "*SRE abs,y",
630
+ 0x5c: "*NOP abs,x",
616
631
  0x5d: "EOR abs,x",
617
632
  0x5e: "LSR abs,x",
618
- 0x5f: "SRE abs,x",
633
+ 0x5f: "*SRE abs,x",
619
634
  0x60: "RTS",
620
635
  0x61: "ADC (,x)",
621
- 0x63: "RRA (,x)",
622
- 0x64: "NOP zp",
636
+ 0x63: "*RRA (,x)",
637
+ 0x64: "*NOP zp",
623
638
  0x65: "ADC zp",
624
639
  0x66: "ROR zp",
625
- 0x67: "RRA zp",
640
+ 0x67: "*RRA zp",
626
641
  0x68: "PLA",
627
642
  0x69: "ADC imm",
628
643
  0x6a: "ROR A",
629
- 0x6b: "ARR imm",
644
+ 0x6b: "*ARR imm",
630
645
  0x6c: "JMP (abs)",
631
646
  0x6d: "ADC abs",
632
647
  0x6e: "ROR abs",
633
- 0x6f: "RRA abs",
648
+ 0x6f: "*RRA abs",
634
649
  0x70: "BVS branch",
635
650
  0x71: "ADC (),y",
636
- 0x73: "RRA (),y",
637
- 0x74: "NOP zp,x",
651
+ 0x73: "*RRA (),y",
652
+ 0x74: "*NOP zp,x",
638
653
  0x75: "ADC zp,x",
639
654
  0x76: "ROR zp,x",
640
- 0x77: "RRA zp,x",
655
+ 0x77: "*RRA zp,x",
641
656
  0x78: "SEI",
642
657
  0x79: "ADC abs,y",
643
- 0x7a: "NOP",
644
- 0x7b: "RRA abs,y",
645
- 0x7c: "NOP abs,x",
658
+ 0x7a: "*NOP",
659
+ 0x7b: "*RRA abs,y",
660
+ 0x7c: "*NOP abs,x",
646
661
  0x7d: "ADC abs,x",
647
662
  0x7e: "ROR abs,x",
648
- 0x7f: "RRA abs,x",
649
- 0x80: "NOP imm",
663
+ 0x7f: "*RRA abs,x",
664
+ 0x80: "*NOP imm",
650
665
  0x81: "STA (,x)",
651
- 0x82: "NOP imm",
652
- 0x83: "SAX (,x)",
666
+ 0x82: "*NOP imm",
667
+ 0x83: "*SAX (,x)",
653
668
  0x84: "STY zp",
654
669
  0x85: "STA zp",
655
670
  0x86: "STX zp",
656
- 0x87: "SAX zp",
671
+ 0x87: "*SAX zp",
657
672
  0x88: "DEY",
658
- 0x89: "NOP imm",
673
+ 0x89: "*NOP imm",
659
674
  0x8a: "TXA",
660
- 0x8b: "ANE imm",
675
+ 0x8b: "*ANE imm",
661
676
  0x8c: "STY abs",
662
677
  0x8d: "STA abs",
663
678
  0x8e: "STX abs",
664
- 0x8f: "SAX abs",
679
+ 0x8f: "*SAX abs",
665
680
  0x90: "BCC branch",
666
681
  0x91: "STA (),y",
667
- 0x93: "SHA (),y",
682
+ 0x93: "*SHA (),y",
668
683
  0x94: "STY zp,x",
669
684
  0x95: "STA zp,x",
670
685
  0x96: "STX zp,y",
671
- 0x97: "SAX zp,y",
686
+ 0x97: "*SAX zp,y",
672
687
  0x98: "TYA",
673
688
  0x99: "STA abs,y",
674
689
  0x9a: "TXS",
675
- 0x9b: "SHS abs,y",
676
- 0x9c: "SHY abs,x",
690
+ 0x9b: "*SHS abs,y",
691
+ 0x9c: "*SHY abs,x",
677
692
  0x9d: "STA abs,x",
678
- 0x9e: "SHX abs,y",
679
- 0x9f: "SHA abs,y",
693
+ 0x9e: "*SHX abs,y",
694
+ 0x9f: "*SHA abs,y",
680
695
  0xa0: "LDY imm",
681
696
  0xa1: "LDA (,x)",
682
697
  0xa2: "LDX imm",
683
- 0xa3: "LAX (,x)",
698
+ 0xa3: "*LAX (,x)",
684
699
  0xa4: "LDY zp",
685
700
  0xa5: "LDA zp",
686
701
  0xa6: "LDX zp",
687
- 0xa7: "LAX zp",
702
+ 0xa7: "*LAX zp",
688
703
  0xa8: "TAY",
689
704
  0xa9: "LDA imm",
690
705
  0xaa: "TAX",
691
- 0xab: "LXA imm",
706
+ 0xab: "*LXA imm",
692
707
  0xac: "LDY abs",
693
708
  0xad: "LDA abs",
694
709
  0xae: "LDX abs",
695
- 0xaf: "LAX abs",
710
+ 0xaf: "*LAX abs",
696
711
  0xb0: "BCS branch",
697
712
  0xb1: "LDA (),y",
698
- 0xb3: "LAX (),y",
713
+ 0xb3: "*LAX (),y",
699
714
  0xb4: "LDY zp,x",
700
715
  0xb5: "LDA zp,x",
701
716
  0xb6: "LDX zp,y",
702
- 0xb7: "LAX zp,y",
717
+ 0xb7: "*LAX zp,y",
703
718
  0xb8: "CLV",
704
719
  0xb9: "LDA abs,y",
705
720
  0xba: "TSX",
706
- 0xbb: "LAS abs,y",
721
+ 0xbb: "*LAS abs,y",
707
722
  0xbc: "LDY abs,x",
708
723
  0xbd: "LDA abs,x",
709
724
  0xbe: "LDX abs,y",
710
- 0xbf: "LAX abs,y",
725
+ 0xbf: "*LAX abs,y",
711
726
  0xc0: "CPY imm",
712
727
  0xc1: "CMP (,x)",
713
- 0xc2: "NOP imm",
714
- 0xc3: "DCP (,x)",
728
+ 0xc2: "*NOP imm",
729
+ 0xc3: "*DCP (,x)",
715
730
  0xc4: "CPY zp",
716
731
  0xc5: "CMP zp",
717
732
  0xc6: "DEC zp",
718
- 0xc7: "DCP zp",
733
+ 0xc7: "*DCP zp",
719
734
  0xc8: "INY",
720
735
  0xc9: "CMP imm",
721
736
  0xca: "DEX",
722
- 0xcb: "SBX imm",
737
+ 0xcb: "*SBX imm",
723
738
  0xcc: "CPY abs",
724
739
  0xcd: "CMP abs",
725
740
  0xce: "DEC abs",
726
- 0xcf: "DCP abs",
741
+ 0xcf: "*DCP abs",
727
742
  0xd0: "BNE branch",
728
743
  0xd1: "CMP (),y",
729
- 0xd3: "DCP (),y",
730
- 0xd4: "NOP zp,x",
744
+ 0xd3: "*DCP (),y",
745
+ 0xd4: "*NOP zp,x",
731
746
  0xd5: "CMP zp,x",
732
747
  0xd6: "DEC zp,x",
733
- 0xd7: "DCP zp,x",
748
+ 0xd7: "*DCP zp,x",
734
749
  0xd8: "CLD",
735
750
  0xd9: "CMP abs,y",
736
- 0xda: "NOP",
737
- 0xdb: "DCP abs,y",
738
- 0xdc: "NOP abs,x",
751
+ 0xda: "*NOP",
752
+ 0xdb: "*DCP abs,y",
753
+ 0xdc: "*NOP abs,x",
739
754
  0xdd: "CMP abs,x",
740
755
  0xde: "DEC abs,x",
741
- 0xdf: "DCP abs,x",
756
+ 0xdf: "*DCP abs,x",
742
757
  0xe0: "CPX imm",
743
758
  0xe1: "SBC (,x)",
744
- 0xe2: "NOP imm",
745
- 0xe3: "ISB (,x)",
759
+ 0xe2: "*NOP imm",
760
+ 0xe3: "*ISB (,x)",
746
761
  0xe4: "CPX zp",
747
762
  0xe5: "SBC zp",
748
763
  0xe6: "INC zp",
749
- 0xe7: "ISB zp",
764
+ 0xe7: "*ISB zp",
750
765
  0xe8: "INX",
751
766
  0xe9: "SBC imm",
752
767
  0xea: "NOP",
753
- 0xeb: "SBC imm",
768
+ 0xeb: "*SBC imm",
754
769
  0xec: "CPX abs",
755
770
  0xed: "SBC abs",
756
771
  0xee: "INC abs",
757
- 0xef: "ISB abs",
772
+ 0xef: "*ISB abs",
758
773
  0xf0: "BEQ branch",
759
774
  0xf1: "SBC (),y",
760
- 0xf3: "ISB (),y",
761
- 0xf4: "NOP zpx",
775
+ 0xf3: "*ISB (),y",
776
+ 0xf4: "*NOP zp,x",
762
777
  0xf5: "SBC zp,x",
763
778
  0xf6: "INC zp,x",
764
- 0xf7: "ISB zp,x",
779
+ 0xf7: "*ISB zp,x",
765
780
  0xf8: "SED",
766
781
  0xf9: "SBC abs,y",
767
- 0xfa: "NOP",
768
- 0xfb: "ISB abs,y",
769
- 0xfc: "NOP abs,x",
782
+ 0xfa: "*NOP",
783
+ 0xfb: "*ISB abs,y",
784
+ 0xfc: "*NOP abs,x",
770
785
  0xfd: "SBC abs,x",
771
786
  0xfe: "INC abs,x",
772
- 0xff: "ISB abs,x",
773
- };
787
+ 0xff: "*ISB abs,x",
788
+ });
774
789
 
775
- const opcodes65c12 = {
790
+ const { opcodes: opcodes65c12, undocumented: undocumented65c12 } = splitUndocumented({
776
791
  0x00: "BRK",
777
792
  0x01: "ORA (,x)",
778
793
  0x04: "TSB zp",
@@ -952,9 +967,9 @@ const opcodes65c12 = {
952
967
  0xfa: "PLX",
953
968
  0xfd: "SBC abs,x",
954
969
  0xfe: "INC abs,x",
955
- };
970
+ });
956
971
 
957
- const opcodes65c02 = {
972
+ const { opcodes: opcodes65c02, undocumented: undocumented65c02 } = splitUndocumented({
958
973
  ...opcodes65c12,
959
974
  0x07: "RMB0 zp",
960
975
  0x17: "RMB1 zp",
@@ -988,12 +1003,20 @@ const opcodes65c02 = {
988
1003
  0xdf: "BBS5 zp,branch",
989
1004
  0xef: "BBS6 zp,branch",
990
1005
  0xff: "BBS7 zp,branch",
991
- };
1006
+ });
1007
+
1008
+ const PrevInstructionWindow = 64;
992
1009
 
993
1010
  class Disassemble6502 {
994
- constructor(cpu, opcodes) {
1011
+ constructor(cpu, opcodes, undocumented) {
995
1012
  this.cpu = cpu;
996
1013
  this.opcodes = opcodes;
1014
+ this.undocumented = undocumented;
1015
+ }
1016
+
1017
+ isDocumented(addr) {
1018
+ const op = this.cpu.peekmem(addr);
1019
+ return op in this.opcodes && !this.undocumented.has(op);
997
1020
  }
998
1021
 
999
1022
  disassemble(addr, plain) {
@@ -1003,13 +1026,15 @@ class Disassemble6502 {
1003
1026
  formatAddr = hexword;
1004
1027
  formatJumpAddr = hexword;
1005
1028
  }
1006
- const opcode = this.opcodes[this.cpu.peekmem(addr)];
1029
+ const opcodeByte = this.cpu.peekmem(addr);
1030
+ const opcode = this.opcodes[opcodeByte];
1007
1031
  if (!opcode) {
1008
1032
  return ["???", addr + 1];
1009
1033
  }
1010
- const split = opcode.split(" ");
1034
+ const [mnemonic, ...rest] = opcode.split(" ");
1035
+ const split = [(this.undocumented.has(opcodeByte) ? "*" : "") + mnemonic, ...rest];
1011
1036
  if (!split[1]) {
1012
- return [opcode, addr + 1];
1037
+ return [split[0], addr + 1];
1013
1038
  }
1014
1039
  let param = split[1] || "";
1015
1040
  let suffix = "";
@@ -1034,6 +1059,14 @@ class Disassemble6502 {
1034
1059
  }
1035
1060
  case "zp":
1036
1061
  return [`${split[0]} $${hexbyte(this.cpu.peekmem(addr + 1))}${suffix}`, addr + 2];
1062
+ case "zp,branch": {
1063
+ const destAddr = addr + signExtend(this.cpu.peekmem(addr + 2)) + 3;
1064
+ return [
1065
+ `${split[0]} $${hexbyte(this.cpu.peekmem(addr + 1))}, $${formatJumpAddr(destAddr)}`,
1066
+ addr + 3,
1067
+ destAddr,
1068
+ ];
1069
+ }
1037
1070
  case "(,x)":
1038
1071
  return [`${split[0]} ($${hexbyte(this.cpu.peekmem(addr + 1))}, X)${suffix}`, addr + 2];
1039
1072
  case "()": {
@@ -1056,11 +1089,60 @@ class Disassemble6502 {
1056
1089
  return [`${split[0]} ($${formatJumpAddr(destAddr)},x)${suffix}`, addr + 3, indDest];
1057
1090
  }
1058
1091
  }
1059
- return [opcode, addr + 1];
1092
+ return [split.join(" "), addr + 1];
1093
+ }
1094
+
1095
+ nextInstruction(address) {
1096
+ return this.disassemble(address)[1] & 0xffff;
1097
+ }
1098
+
1099
+ /**
1100
+ * Guesses the address of the instruction before the given one by scoring
1101
+ * each run of instructions in the preceding window that lands exactly on
1102
+ * it: a point per "common" instruction (loads, stores, branches, compares,
1103
+ * arithmetic and carry-set/clear that avoid unusual indexing modes) and a
1104
+ * large boost for a run that passes through pc. Runs through undocumented
1105
+ * opcodes are discarded. The highest-scoring run's last instruction wins;
1106
+ * any run beats none, since a wrong parse resynchronises within a few
1107
+ * instructions and so nearly always shares the true stream's last one.
1108
+ * Good test cases:
1109
+ * Repton 2 @ 2cbb
1110
+ * MOS @ cfc8, and the unrolled STA abs,X screen clear @ cc63
1111
+ * also, just starting from the back of ROM and going up...
1112
+ */
1113
+ prevInstruction(address, pc) {
1114
+ const commonInstructions =
1115
+ /(RTS|B..|JMP|JSR|LD[AXY]|ST[AXY]|TA[XY]|T[XY]A|AD[DC]|SUB|SBC|CLC|SEC|CMP|EOR|ORA|AND|INC|DEC).*/;
1116
+ const uncommonInstructions = /.*,\s*([XY]|X\))$/;
1117
+
1118
+ address &= 0xffff;
1119
+ let bestAddr = address - 1;
1120
+ let bestScore = -1;
1121
+ for (let startingPoint = address - PrevInstructionWindow; startingPoint !== address; startingPoint++) {
1122
+ let score = 0;
1123
+ let addr = startingPoint & 0xffff;
1124
+ while (addr < address) {
1125
+ if (!this.isDocumented(addr)) break;
1126
+ const result = this.disassemble(addr);
1127
+ if (addr === pc) score += 10;
1128
+ if (result[0].match(commonInstructions) && !result[0].match(uncommonInstructions)) {
1129
+ score++;
1130
+ }
1131
+ if (result[1] === address) {
1132
+ if (score > bestScore) {
1133
+ bestScore = score;
1134
+ bestAddr = addr;
1135
+ }
1136
+ break;
1137
+ }
1138
+ addr = result[1];
1139
+ }
1140
+ }
1141
+ return bestAddr & 0xffff;
1060
1142
  }
1061
1143
  }
1062
1144
 
1063
- function makeCpuFunctions(cpu, opcodes, is65c12, cycleAccurate = true) {
1145
+ function makeCpuFunctions(cpu, opcodes, undocumented, is65c12, cycleAccurate = true) {
1064
1146
  function getInstruction(opcodeString, needsReg) {
1065
1147
  const split = opcodeString.split(" ");
1066
1148
  const opcode = split[0];
@@ -1097,7 +1179,6 @@ function makeCpuFunctions(cpu, opcodes, is65c12, cycleAccurate = true) {
1097
1179
  return ig.render();
1098
1180
 
1099
1181
  case "zp":
1100
- case "zpx": // Seems to be enough to keep tests happy, but needs investigation.
1101
1182
  case "zp,x":
1102
1183
  case "zp,y":
1103
1184
  if (arg === "zp") {
@@ -1409,7 +1490,7 @@ ${indent}`)
1409
1490
  Runner.prototype.run = generate6502JumpTable();
1410
1491
 
1411
1492
  return {
1412
- disassembler: new Disassemble6502(cpu, opcodes),
1493
+ disassembler: new Disassemble6502(cpu, opcodes, undocumented),
1413
1494
  runInstruction: new Runner(),
1414
1495
  opcodes: opcodes,
1415
1496
  getInstruction: getInstruction,
@@ -1417,13 +1498,13 @@ ${indent}`)
1417
1498
  }
1418
1499
 
1419
1500
  export function Cpu6502(cpu, { cycleAccurate = true } = {}) {
1420
- return makeCpuFunctions(cpu, opcodes6502, false, cycleAccurate);
1501
+ return makeCpuFunctions(cpu, opcodes6502, undocumented6502, false, cycleAccurate);
1421
1502
  }
1422
1503
 
1423
1504
  export function Cpu65c12(cpu, { cycleAccurate = true } = {}) {
1424
- return makeCpuFunctions(cpu, opcodes65c12, true, cycleAccurate);
1505
+ return makeCpuFunctions(cpu, opcodes65c12, undocumented65c12, true, cycleAccurate);
1425
1506
  }
1426
1507
 
1427
1508
  export function Cpu65c02(cpu, { cycleAccurate = true } = {}) {
1428
- return makeCpuFunctions(cpu, opcodes65c02, true, cycleAccurate);
1509
+ return makeCpuFunctions(cpu, opcodes65c02, undocumented65c02, true, cycleAccurate);
1429
1510
  }
package/src/jsbeeb.css CHANGED
@@ -560,6 +560,39 @@ small {
560
560
  flex: none;
561
561
  }
562
562
 
563
+ #header-bar .navbar-brand img {
564
+ display: block;
565
+ }
566
+
567
+ #header-bar [data-icon] > svg {
568
+ width: 1.15em;
569
+ height: 1.15em;
570
+ vertical-align: -0.2em;
571
+ }
572
+
573
+ /* Shrunk, the menu list wraps onto a second row; the promo yields instead, by truncating. */
574
+ #header-bar .navbar-nav {
575
+ flex-shrink: 0;
576
+ }
577
+
578
+ #header-bar .navbar-text {
579
+ flex: 0 1 auto;
580
+ min-width: 0;
581
+ white-space: nowrap;
582
+ overflow: hidden;
583
+ text-overflow: ellipsis;
584
+ }
585
+
586
+ #paste-form {
587
+ flex: 1 0 6rem;
588
+ min-width: 0;
589
+ max-width: 14rem;
590
+ }
591
+
592
+ #paste-text {
593
+ min-width: 0;
594
+ }
595
+
563
596
  /* The only item in the header with no bound on its width, so it is the one that yields. */
564
597
  #disc-name {
565
598
  flex: 1;
package/src/main.js CHANGED
@@ -9,6 +9,7 @@ import * as utils_atom from "./utils_atom.js";
9
9
  import { GamePad } from "./gamepads.js";
10
10
  import { initialise as electron } from "./app/electron.js";
11
11
  import { AudioHandler } from "./web/audio-handler.js";
12
+ import { installIcons } from "./web/icons.js";
12
13
  import { QuickSettings } from "./web/quick-settings.js";
13
14
  import { toast } from "./web/toast.js";
14
15
  import { BuiltInImages, MediaLoader } from "./web/media-loader.js";
@@ -37,6 +38,8 @@ import { DiscVisualiser } from "./web/disc-visualiser.js";
37
38
  import { downloadDriveData } from "./dom-utils.js";
38
39
  import { parseMediaParams, processAutobootParams, processDriveTrackParams, processInputParams } from "./url-params.js";
39
40
 
41
+ installIcons();
42
+
40
43
  // ------------------------------------------------------------------------
41
44
  // What the URL asked for.
42
45
  // ------------------------------------------------------------------------
@@ -196,6 +199,7 @@ const keys = new KeyboardSetup({
196
199
  openPrinter: () => frontPanel.checkPrinterWindow(),
197
200
  pause: () => loop.stop(false),
198
201
  resume: () => loop.go(),
202
+ paste: (text) => keys.sendRawKeyboard(autoBoot.stringToMachineKeys(text), true),
199
203
  onAnyKeyDown: () => {
200
204
  audioHandler.tryResume();
201
205
  inputs.ensureMicrophoneRunning();
@@ -332,12 +336,7 @@ settings.wire({ audioHandler, display, layout, quickSettings, machine, keys, inp
332
336
 
333
337
  for (const el of document.querySelectorAll(".initially-hidden")) el.classList.remove("initially-hidden");
334
338
 
335
- const pastetext = document.getElementById("paste-text");
336
- pastetext.closest("form").addEventListener("submit", (event) => event.preventDefault());
337
- pastetext.addEventListener("paste", function (event) {
338
- const text = event.clipboardData.getData("text/plain");
339
- keys.sendRawKeyboard(autoBoot.stringToMachineKeys(text), true);
340
- });
339
+ document.getElementById("paste-form").addEventListener("submit", (event) => event.preventDefault());
341
340
 
342
341
  window.addEventListener("blur", function () {
343
342
  keys.clearKeys();
package/src/models.js CHANGED
@@ -19,9 +19,23 @@ const CpuModel = Object.freeze({
19
19
  * to any number of machines without one session's settings leaking into the next.
20
20
  */
21
21
  class Model {
22
- constructor({ name, synonyms, os, cpuModel, isMaster, isAtom, swram, fdc, cmosOverride, banks, clockMhz } = {}) {
22
+ constructor({
23
+ name,
24
+ shortName = name,
25
+ synonyms,
26
+ os,
27
+ cpuModel,
28
+ isMaster,
29
+ isAtom,
30
+ swram,
31
+ fdc,
32
+ cmosOverride,
33
+ banks,
34
+ clockMhz,
35
+ } = {}) {
23
36
  if (!(clockMhz > 0)) throw new Error(`Model ${name} has no clock speed`);
24
37
  this.name = name;
38
+ this.shortName = shortName;
25
39
  this.synonyms = synonyms;
26
40
  this.os = os;
27
41
  this.clockMhz = clockMhz;
@@ -79,6 +93,7 @@ function pickDfs(cmos) {
79
93
  function atomModel({ name, synonyms, os, banks }) {
80
94
  return new Model({
81
95
  name,
96
+ shortName: "Atom",
82
97
  synonyms,
83
98
  os,
84
99
  cpuModel: CpuModel.MOS6502,
@@ -132,6 +147,7 @@ const masterSwram = [
132
147
  export const allModels = [
133
148
  new Model({
134
149
  name: "BBC B with 8271 (DFS 1.2)",
150
+ shortName: "BBC B",
135
151
  synonyms: ["B-DFS1.2", "BBC B with DFS 1.2"],
136
152
  os: ["os.rom", "BASIC.ROM", "b/DFS-1.2.rom"],
137
153
  cpuModel: CpuModel.MOS6502,
@@ -142,6 +158,7 @@ export const allModels = [
142
158
  }),
143
159
  new Model({
144
160
  name: "BBC B with 8271 (DFS 0.9)",
161
+ shortName: "BBC B",
145
162
  synonyms: ["B-DFS0.9", "B", "BBC B with DFS 0.9"],
146
163
  os: ["os.rom", "BASIC.ROM", "b/DFS-0.9.rom"],
147
164
  cpuModel: CpuModel.MOS6502,
@@ -152,6 +169,7 @@ export const allModels = [
152
169
  }),
153
170
  new Model({
154
171
  name: "BBC B with 1770 (DFS)",
172
+ shortName: "BBC B",
155
173
  synonyms: ["B1770"],
156
174
  os: ["os.rom", "BASIC.ROM", "b1770/dfs1770.rom", "b1770/zADFS.ROM"],
157
175
  cpuModel: CpuModel.MOS6502,
@@ -163,6 +181,7 @@ export const allModels = [
163
181
  // putting ADFS in a higher ROM slot gives it priority
164
182
  new Model({
165
183
  name: "BBC B with 1770 (ADFS)",
184
+ shortName: "BBC B",
166
185
  synonyms: ["B1770A"],
167
186
  os: ["os.rom", "BASIC.ROM", "b1770/zADFS.ROM", "b1770/dfs1770.rom"],
168
187
  cpuModel: CpuModel.MOS6502,
@@ -173,6 +192,7 @@ export const allModels = [
173
192
  }),
174
193
  new Model({
175
194
  name: "BBC Master 128 (DFS)",
195
+ shortName: "Master 128",
176
196
  synonyms: ["Master"],
177
197
  os: ["master/mos3.20"],
178
198
  cpuModel: CpuModel.CMOS65C12,
@@ -184,6 +204,7 @@ export const allModels = [
184
204
  }),
185
205
  new Model({
186
206
  name: "BBC Master 128 (ADFS)",
207
+ shortName: "Master 128",
187
208
  synonyms: ["MasterADFS"],
188
209
  os: ["master/mos3.20"],
189
210
  cpuModel: CpuModel.CMOS65C12,
@@ -195,6 +216,7 @@ export const allModels = [
195
216
  }),
196
217
  new Model({
197
218
  name: "BBC Master 128 (ANFS)",
219
+ shortName: "Master 128",
198
220
  synonyms: ["MasterANFS"],
199
221
  os: ["master/mos3.20"],
200
222
  cpuModel: CpuModel.CMOS65C12,
package/src/web/config.js CHANGED
@@ -216,6 +216,9 @@ export class Config extends EventTarget {
216
216
  setModel(modelName) {
217
217
  this.model = findModel(modelName);
218
218
  for (const el of document.querySelectorAll(".bbc-model")) el.textContent = this.model.name;
219
+ for (const el of document.querySelectorAll(".bbc-model-short")) el.textContent = this.model.shortName;
220
+ const settingsLink = document.getElementById("model-settings");
221
+ if (settingsLink) settingsLink.title = `${this.model.name}: emulation settings`;
219
222
  }
220
223
 
221
224
  setKeyLayout(keyLayout) {
package/src/web/debug.js CHANGED
@@ -89,48 +89,6 @@ class MemoryView {
89
89
  }
90
90
  }
91
91
 
92
- /**
93
- * Guesses the address of the instruction before the given one by scoring each
94
- * possible run of instructions leading up to it: a point per "common"
95
- * instruction (loads, stores, branches, compares, arithmetic and
96
- * carry-set/clear that avoid unusual indexing modes) and a large boost for a
97
- * run that passes through the current program counter. The highest-scoring
98
- * run wins and its last instruction is the answer.
99
- * Good test cases:
100
- * Repton 2 @ 2cbb
101
- * MOS @ cfc8
102
- * also, just starting from the back of ROM and going up...
103
- */
104
- export function prevInstruction(disassemble, pc, address) {
105
- const commonInstructions =
106
- /(RTS|B..|JMP|JSR|LD[AXY]|ST[AXY]|TA[XY]|T[XY]A|AD[DC]|SUB|SBC|CLC|SEC|CMP|EOR|ORR|AND|INC|DEC).*/;
107
- const uncommonInstrucions = /.*,\s*([XY]|X\))$/;
108
-
109
- address &= 0xffff;
110
- let bestAddr = address - 1;
111
- let bestScore = 0;
112
- for (let startingPoint = address - 20; startingPoint !== address; startingPoint++) {
113
- let score = 0;
114
- let addr = startingPoint & 0xffff;
115
- while (addr < address) {
116
- const result = disassemble(addr);
117
- if (addr === pc) score += 10;
118
- if (result[0].match(commonInstructions) && !result[0].match(uncommonInstrucions)) {
119
- score++;
120
- }
121
- if (result[1] === address) {
122
- if (score > bestScore) {
123
- bestScore = score;
124
- bestAddr = addr;
125
- break;
126
- }
127
- }
128
- addr = result[1];
129
- }
130
- }
131
- return bestAddr;
132
- }
133
-
134
92
  export class Debugger {
135
93
  constructor() {
136
94
  this.patchInstructions = new Map();
@@ -424,7 +382,7 @@ export class Debugger {
424
382
  }
425
383
 
426
384
  prevInstruction(address) {
427
- return prevInstruction((addr) => this.disassemble(addr), this.cpu.pc, address);
385
+ return this.cpu.disassembler.prevInstruction(address, this.cpu.pc);
428
386
  }
429
387
 
430
388
  updatePrevMem() {
@@ -483,7 +441,7 @@ export class Debugger {
483
441
  }
484
442
 
485
443
  nextInstruction(address) {
486
- return this.disassemble(address)[1] & 0xffff;
444
+ return this.cpu.disassembler.nextInstruction(address);
487
445
  }
488
446
 
489
447
  instrClick(e) {
@@ -0,0 +1,42 @@
1
+ import arrowCounterclockwise from "bootstrap-icons/icons/arrow-counterclockwise.svg?raw";
2
+ import cassette from "bootstrap-icons/icons/cassette.svg?raw";
3
+ import clockHistory from "bootstrap-icons/icons/clock-history.svg?raw";
4
+ import display from "bootstrap-icons/icons/display.svg?raw";
5
+ import floppy from "bootstrap-icons/icons/floppy.svg?raw";
6
+ import gear from "bootstrap-icons/icons/gear.svg?raw";
7
+ import headphones from "bootstrap-icons/icons/headphones.svg?raw";
8
+ import pauseFill from "bootstrap-icons/icons/pause-fill.svg?raw";
9
+ import playFill from "bootstrap-icons/icons/play-fill.svg?raw";
10
+ import soundwave from "bootstrap-icons/icons/soundwave.svg?raw";
11
+ import stars from "bootstrap-icons/icons/stars.svg?raw";
12
+ import threeDots from "bootstrap-icons/icons/three-dots.svg?raw";
13
+ import tv from "bootstrap-icons/icons/tv.svg?raw";
14
+ import volumeUp from "bootstrap-icons/icons/volume-up.svg?raw";
15
+
16
+ /** The Bootstrap Icons the page uses, by their upstream names; nothing else of the set is bundled. */
17
+ const Icons = {
18
+ "arrow-counterclockwise": arrowCounterclockwise,
19
+ cassette,
20
+ "clock-history": clockHistory,
21
+ display,
22
+ floppy,
23
+ gear,
24
+ headphones,
25
+ "pause-fill": pauseFill,
26
+ "play-fill": playFill,
27
+ soundwave,
28
+ stars,
29
+ "three-dots": threeDots,
30
+ tv,
31
+ "volume-up": volumeUp,
32
+ };
33
+
34
+ export const IconNames = Object.freeze(Object.keys(Icons));
35
+
36
+ /** Puts the named SVG inside every `[data-icon]` element under `root`; an unknown name leaves them all untouched. */
37
+ export function installIcons(root = document) {
38
+ const placeholders = [...root.querySelectorAll("[data-icon]")];
39
+ const unknown = placeholders.find((el) => !(el.dataset.icon in Icons));
40
+ if (unknown) throw new Error(`No icon named ${unknown.dataset.icon}`);
41
+ for (const el of placeholders) el.innerHTML = Icons[el.dataset.icon];
42
+ }
@@ -2,19 +2,22 @@ import * as utils from "../utils.js";
2
2
  import { Keyboard } from "../keyboard.js";
3
3
  import { showNotice } from "./reporting.js";
4
4
 
5
+ const PasteBoxId = "paste-text";
6
+ const TypingTargets = 'input, textarea, select, [contenteditable]:not([contenteditable="false"])';
7
+
5
8
  /** The page's keyboard: the emulated one and the browser shortcuts around it. */
6
9
  export class KeyboardSetup {
7
10
  /**
8
11
  * @param {object} opts
9
12
  * @param {object} opts.actions what each shortcut does, supplied late-bound:
10
13
  * enterDebugger, reload, toggleFast, openRewind, openPrinter,
11
- * pause, resume, onAnyKeyDown
14
+ * pause, resume, paste, onAnyKeyDown
12
15
  * @param {import("./accessibility-switches.js").AccessibilitySwitches} opts.accessibilitySwitches
13
16
  */
14
17
  constructor({ actions, accessibilitySwitches, processor, dbgr, keyLayout }) {
15
18
  const keyboard = (this.keyboard = new Keyboard({
16
19
  processor,
17
- inputEnabledFunction: () => document.activeElement && document.activeElement.id === "paste-text",
20
+ inputEnabledFunction: () => document.activeElement && document.activeElement.id === PasteBoxId,
18
21
  keyLayout,
19
22
  dbgr,
20
23
  }));
@@ -62,6 +65,12 @@ export class KeyboardSetup {
62
65
  });
63
66
  document.addEventListener("keypress", (evt) => keyboard.keyPress(evt));
64
67
  document.addEventListener("keyup", (evt) => keyboard.keyUp(evt));
68
+ document.addEventListener("paste", (evt) => {
69
+ const target = document.activeElement;
70
+ if (target && target.id !== PasteBoxId && target.matches(TypingTargets)) return;
71
+ const text = evt.clipboardData?.getData("text/plain");
72
+ if (text) actions.paste(text);
73
+ });
65
74
  }
66
75
 
67
76
  sendRawKeyboard(keysToSend, checkCapsAndShiftLocks) {