rsemu 0.0.4

A multiplatform emulator in pure Rust, built bottom-up on a generic framework.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
//! The AT's system control ports — 0x61 (port B) and 0x92 (port A) — and the
//! chipset reset control register at 0xcf9.
//!
//! # Sources
//!
//! * *IBM Personal Computer AT Technical Reference* (1984), system board
//!   description and I/O address map: port 0x61's bit assignments, the refresh
//!   toggle, the timer 2 gate and output, and the parity / I/O channel check
//!   pair that drive NMI.
//! * Ralf Brown's Interrupt List, ports section, entries for 0061 and 0092 —
//!   the bit-level read/write behaviour the board, not a data sheet, defines.
//! * The PS/2 and later AT chipset convention for port 0x92 ("System Control
//!   Port A"): bit 0 fast reset, bit 1 A20 gate.
//! * Intel PIIX-family and ICH-family data sheets, the *Reset Control* register
//!   at I/O address 0xcf9: bit 1 `SYS_RST` selects the kind of reset, bit 2
//!   `RST_CPU` triggers it on a low-to-high transition, and on the later parts
//!   bit 3 `FULL_RST` asks for a power cycle rather than a reset. Ralf Brown's
//!   Interrupt List, ports section, entry 0CF9 says the same from the
//!   firmware's side.
//!
//! No emulator source was consulted (`CLAUDE.md`, provenance).
//!
//! # Why this is a device at all
//!
//! Neither port is a chip. They are latches and gates soldered onto the system
//! board: a couple of flip-flops, a gate onto the 8254's `GATE2` input, a tap
//! off the DRAM refresh request, and a second path to the two things the 8042
//! used to do alone. That is precisely why they get a device of their own
//! rather than being smuggled into the timer or the keyboard controller. Port
//! 0x61 is not part of the 8254 — it *gates* the 8254 — and port 0x92 is not
//! part of the 8042; hiding either inside a chip model would make that chip
//! answer an address it does not decode, and would put the A20 gate in two
//! places at once when the board's A20 is the wired-OR of both paths.
//!
//! Port 0xcf9 is that same argument one chipset generation later. It is a latch
//! in the south bridge rather than a register of any chip a machine file
//! instantiates, and its whole job is to pull the reset pin port 0x92 bit 0
//! already pulls. It belongs here for exactly the reason the other two do, and
//! keeping it here leaves the board's reset the wired-OR of every path to it
//! instead of something two devices each believe they own. Two ways to reboot a
//! PC becoming three is a fact about PC chipsets, not a modelling choice.
//!
//! Three ports scattered across the I/O map are three regions, not one window
//! with holes in it, so a machine file maps
//! [`region("portb")`](Device::region) at 0x61, `region("porta")` at 0x92 and
//! `region("resetctl")` at 0xcf9. `""` is port B, because it is the one every
//! AT has.
//!
//! A board with a PCI host bridge maps the third one differently, and the
//! reason is a real chipset fact: `0xcf9` sits inside the four bytes of
//! `CONFADD` at `0xcf8`, which the north bridge claims for a Dword access while
//! the south bridge claims `0xcf9` for a byte access. An address space decodes
//! by address alone, so the bridge holds all four and hands the narrow cycles
//! on. `ConfAddWindow`, below, is the view of those four bytes this device
//! publishes through [`ExportId::PORT_PASSTHROUGH`] for that purpose.
//!
//! # Port B, 0x61
//!
//! ```text
//!   bit 7  parity check status        read-only; cleared by writing bit 2
//!   bit 6  I/O channel check status   read-only; cleared by writing bit 3
//!   bit 5  timer 2 output             follows the `timer2` input pin
//!   bit 4  refresh toggle             flips on each edge of the `refresh` pin
//!   bit 3  I/O channel check enable   latched, reads back
//!   bit 2  parity check enable        latched, reads back
//!   bit 1  speaker data enable        latched, reads back
//!   bit 0  timer 2 gate               latched, reads back, drives `gate2`
//! ```
//!
//! Bits 4 and 5 are the interesting ones, and both are **inputs**, not
//! inventions of this device:
//!
//! * Bit 5 is the 8254's `OUT2` pin brought straight to the bus. Firmware
//!   calibrates loops by spinning on it, so it has to follow the wire rather
//!   than be synthesised from a counter this device does not own.
//! * Bit 4 is the DRAM refresh request, which on an AT is counter 1's output.
//!   Firmware uses it as a coarse timing reference and one well-known
//!   power-on self-test spins waiting for it to change, so a model that never
//!   moved it would hang. It is modelled as an input pin the machine file
//!   wires to the timer, and this device invents no timing of its own
//!   (`CLAUDE.md`: the scheduler owns time). It toggles on **each** edge, so
//!   the same wiring works whether counter 1 is programmed to emit a narrow
//!   pulse per refresh or a square wave.
//!
//! Writing bit 2 or bit 3 clears the matching status bit. There is no separate
//! acknowledge register on the AT: the write that re-arms the check is the
//! write that clears it, which is how a parity NMI handler gets out of its own
//! interrupt.
//!
//! # Port A, 0x92
//!
//! ```text
//!   bit 1  A20 gate     latched, drives `a20`
//!   bit 0  fast reset   write-1 pulses `reset`; always reads back clear
//! ```
//!
//! Both are the chipset's fast path to jobs the 8042 originally did with a
//! command byte and a several-microsecond handshake. The board's A20 is the
//! wired-OR of this pin and the keyboard controller's, which is a machine-file
//! wiring question, not something this device resolves.
//!
//! Bit 0 reads back clear even though every other bit is latched. It has to:
//! the canonical A20 sequence is a read-modify-write (`in al,0x92` / `or al,2`
//! / `out 0x92,al`), and a bit 0 that read back set would reset the machine on
//! the way past.
//!
//! # Reset control, 0xcf9
//!
//! ```text
//!   bit 3  FULL_RST   latched, reads back; a power cycle rather than a reset
//!   bit 2  RST_CPU    a 0->1 write transition pulses `reset`; reads back clear
//!   bit 1  SYS_RST    latched, reads back; clear is a soft reset, set a full one
//! ```
//!
//! Bit 2 is the trigger and the other two are the modifiers it acts on, which
//! is why firmware writes 0x02 and then 0x06 rather than 0x06 twice: the reset
//! happens on the transition of bit 2, so a write that leaves it already set
//! asks for nothing new. Bit 2 reads back clear for the same reason port 0x92
//! bit 0 does — a read-modify-write that saw it set would reset the machine on
//! the way past — and the level it was last written with is remembered, so the
//! next write can be told to be a transition rather than a repeat.
//!
//! Nothing here distinguishes a soft reset from a full one or from a power
//! cycle. `SYS_RST` and `FULL_RST` are latched and read back so firmware sees
//! what it wrote, but rsemu has one kind of reset on this pin, and a second
//! would mean inventing a power-cycle path the machine does not have.
//!
//! # No `speaker` property
//!
//! There is deliberately none. The only named-signal seam in the tree is
//! `dev::riscv::syscon`'s, and it does not suit: it lives behind the
//! `dev-riscv` feature, so a PC build would have to link RISC-V devices to
//! reach it, and its payload is a one-shot power request rather than an
//! observable level. Inventing a second seam for one bit is worse than having
//! none, so a test that wants to know the speaker is gated on reads port B
//! back or watches the `gate2` pin — which is the same information the 8254
//! gets.

use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt;

use crate::core::device::{Device, DeviceClass, Export, ExportId, RealizeCtx, ResetKind, SinkPin};
use crate::core::error::{BusError, Error, Result};
use crate::core::props::Props;
use crate::core::space::{AccessConstraints, MemAttrs, MemOps, MemResult, Region, RegionRef};
use crate::core::state::{ChunkReader, ChunkWriter, Sink, Source};
use crate::core::sync::{LockRank, Mutex};
use crate::core::value::{Endian, Width};
use crate::core::wire::{FanIn, Level, Resolve, WireId, WireSink, WireSource};
use crate::machine::realize::Instance;
use crate::machine::validate::ClassSchema;

/// The class name a machine description writes.
pub const CLASS_NAME: &str = "pc.sysctl";

/// The snapshot chunk version. Bump with the encoding, never on its own.
///
/// 2 appended the 0xcf9 latch and bit 2's remembered level to the chunk.
const STATE_VERSION: u32 = 2;

/// How much address space each port answers.
///
/// One byte each. The three are scattered across the I/O map, so they are
/// separate regions rather than one window — see the module docs.
pub const REGISTER_WINDOW_LEN: u64 = 1;

// -- port B (0x61) ----------------------------------------------------------

/// Timer 2 gate. Latched, and driven out on the `gate2` pin.
const B_GATE2: u8 = 0x01;
/// Speaker data enable. Latched; the 8254's output is ANDed with it on the
/// board, which is a wiring question rather than one for this latch.
const B_SPEAKER: u8 = 0x02;
/// Parity check enable. Latched, and writing it clears [`B_PARITY_STATUS`].
const B_PARITY_ENABLE: u8 = 0x04;
/// I/O channel check enable. Latched, and writing it clears [`B_IOCHK_STATUS`].
const B_IOCHK_ENABLE: u8 = 0x08;
/// The refresh toggle, driven by the `refresh` input pin.
const B_REFRESH: u8 = 0x10;
/// Timer 2 output, driven by the `timer2` input pin.
const B_TIMER2_OUT: u8 = 0x20;
/// I/O channel check status: an adapter reported a failure.
const B_IOCHK_STATUS: u8 = 0x40;
/// Parity check status: a memory board reported bad parity.
const B_PARITY_STATUS: u8 = 0x80;
/// The four bits a write latches. The top nibble is status and inputs, and a
/// write to it lands nowhere.
const B_LATCH_MASK: u8 = B_GATE2 | B_SPEAKER | B_PARITY_ENABLE | B_IOCHK_ENABLE;

// -- port A (0x92) ----------------------------------------------------------

/// Fast reset: writing it set pulses the CPU's reset line.
const A_FAST_RESET: u8 = 0x01;
/// The fast A20 gate, driven out on the `a20` pin.
const A_GATE_A20: u8 = 0x02;

// -- reset control (0xcf9) --------------------------------------------------

/// `SYS_RST`: clear asks for a soft reset, set for a full one. Latched.
const C_SYS_RST: u8 = 0x02;
/// `RST_CPU`: the trigger. A write taking it from clear to set pulses `reset`.
const C_RST_CPU: u8 = 0x04;
/// `FULL_RST`: on the later chipsets, a power cycle rather than a reset.
/// Latched and read back; the module docs say why it changes nothing else.
const C_FULL_RST: u8 = 0x08;
/// The two bits a write latches. Bit 2 is edge-sensitive and reads back clear,
/// and the rest of the byte lands nowhere.
const C_LATCH_MASK: u8 = C_SYS_RST | C_FULL_RST;

// -- input pins -------------------------------------------------------------

/// The line number [`Device::sink`] hands out for the `refresh` input.
const LINE_REFRESH: u32 = 0;
/// The line number [`Device::sink`] hands out for the `timer2` input.
const LINE_TIMER2: u32 = 1;

/// Everything a snapshot has to carry.
///
/// The wire handles are not here: they are the machine's topology, rebuilt by
/// realize on the far side of a load (`CLAUDE.md`: derived state is never
/// serialized).
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
struct State {
    /// The port B write latch, bits 0-3 only.
    port_b: u8,
    /// The port A latch. Bit 0 is always clear in here — see the module docs.
    port_a: u8,
    /// Port B bit 7: a memory board reported bad parity.
    parity_status: bool,
    /// Port B bit 6: an adapter reported an I/O channel check.
    iochk_status: bool,
    /// Port B bit 4. Not a function of the input level: it is a divide-by-two
    /// of the refresh request, so it survives across edges and is saved.
    refresh_toggle: bool,
    /// The last level seen on the `refresh` pin, so an edge can be told from a
    /// repeat after a load.
    refresh_in: bool,
    /// The last level seen on the `timer2` pin, which port B bit 5 reports.
    timer2_in: bool,
    /// The 0xcf9 write latch, bits 1 and 3 only. Bit 2 is never in here.
    reset_ctl: u8,
    /// The level bit 2 was last written with, so the next write can be told to
    /// be a transition rather than a repeat.
    rst_cpu: bool,
}

/// The latches, and the three pins they drive.
struct Registers {
    state: Mutex<State>,
    /// The 8254's `GATE2` input. At [`LockRank::LEAF`] so it can be driven
    /// with nothing else held.
    gate2: Mutex<Option<WireSource>>,
    /// The board's fast A20 path, wire-ORed with the 8042's.
    a20: Mutex<Option<WireSource>>,
    /// The CPU reset line. Pulsed, never held.
    reset: Mutex<Option<WireSource>>,
}

impl fmt::Debug for Registers {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut s = f.debug_struct("Registers");
        match self.state.try_lock() {
            Some(state) => s.field("state", &*state).finish(),
            None => s.field("state", &"<in use>").finish(),
        }
    }
}

/// Drive `pin`, if it is connected. Never called with the state lock held.
fn drive(pin: &Mutex<Option<WireSource>>, level: Level) {
    let out = pin.lock().clone();
    if let Some(out) = out {
        out.set(level);
    }
}

impl Registers {
    /// Port B as a read produces it.
    ///
    /// Deliberately free of side effects, which is what makes `MemAttrs::debug`
    /// a non-decision here. The tempting shortcut — toggling bit 4 on every
    /// read, because that makes a firmware spin loop terminate without any
    /// timing behind it — would make a debugger's window refresh change the
    /// machine, and would put this device in charge of a rate it has no clock
    /// for.
    fn read_b(&self) -> u8 {
        let s = self.state.lock();
        let mut value = s.port_b & B_LATCH_MASK;
        if s.refresh_toggle {
            value |= B_REFRESH;
        }
        if s.timer2_in {
            value |= B_TIMER2_OUT;
        }
        if s.iochk_status {
            value |= B_IOCHK_STATUS;
        }
        if s.parity_status {
            value |= B_PARITY_STATUS;
        }
        value
    }

    /// Latch a port B write and gate the timer.
    fn write_b(&self, value: u8) {
        let gate2 = {
            let mut s = self.state.lock();
            s.port_b = value & B_LATCH_MASK;
            // The write that re-arms a check is the write that clears it: the
            // AT has no separate acknowledge register, and this is how a parity
            // NMI handler stops being re-entered.
            if value & B_PARITY_ENABLE != 0 {
                s.parity_status = false;
            }
            if value & B_IOCHK_ENABLE != 0 {
                s.iochk_status = false;
            }
            Level::from_bool(s.port_b & B_GATE2 != 0)
        };
        drive(&self.gate2, gate2);
    }

    /// Port A as a read produces it. Bit 0 is never set — module docs.
    fn read_a(&self) -> u8 {
        self.state.lock().port_a
    }

    /// Pulse the CPU reset line, if it is connected.
    ///
    /// A pulse, not a level: holding reset would need somebody to release it,
    /// and nothing on the board does. Both paths to a reset — port 0x92 bit 0
    /// and 0xcf9 bit 2 — come through here, because the board has one reset pin
    /// and the net resolves the rest.
    ///
    /// Never called with the state lock held.
    fn pulse_reset(&self) {
        let out = self.reset.lock().clone();
        if let Some(out) = out {
            out.pulse(Level::High);
        }
    }

    /// Latch a port A write, move A20, and pulse reset if asked.
    fn write_a(&self, value: u8) {
        let (a20, fast_reset) = {
            let mut s = self.state.lock();
            s.port_a = value & !A_FAST_RESET;
            (
                Level::from_bool(value & A_GATE_A20 != 0),
                value & A_FAST_RESET != 0,
            )
        };
        // A20 first: a write that sets both bits is asking for a reset with the
        // gate already where it wants it, and the reset pulse re-enters this
        // device's own `reset` through the machine's reset tree.
        drive(&self.a20, a20);
        if fast_reset {
            self.pulse_reset();
        }
    }

    /// The reset control register as a read produces it. Bit 2 is never set —
    /// module docs.
    fn read_c(&self) -> u8 {
        self.state.lock().reset_ctl
    }

    /// Latch a 0xcf9 write, and pulse reset on bit 2's low-to-high transition.
    fn write_c(&self, value: u8) {
        let trigger = {
            let mut s = self.state.lock();
            s.reset_ctl = value & C_LATCH_MASK;
            let armed = value & C_RST_CPU != 0;
            // The reset is on the transition, which is why firmware writes 0x02
            // and then 0x06: a write leaving bit 2 set asks for nothing new.
            let trigger = armed && !s.rst_cpu;
            s.rst_cpu = armed;
            trigger
        };
        if trigger {
            self.pulse_reset();
        }
    }

    /// The `refresh` pin moved. Each edge flips bit 4.
    fn refresh_edge(&self, level: Level) {
        let mut s = self.state.lock();
        if s.refresh_in == level.is_high() {
            return;
        }
        s.refresh_in = level.is_high();
        s.refresh_toggle = !s.refresh_toggle;
    }

    /// The `timer2` pin moved. Bit 5 is the pin, with nothing in between.
    fn timer2_level(&self, level: Level) {
        self.state.lock().timer2_in = level.is_high();
    }

    /// Drive both level outputs from the current latches.
    fn drive_outputs(&self) {
        let (gate2, a20) = {
            let s = self.state.lock();
            (
                Level::from_bool(s.port_b & B_GATE2 != 0),
                Level::from_bool(s.port_a & A_GATE_A20 != 0),
            )
        };
        drive(&self.gate2, gate2);
        drive(&self.a20, a20);
    }
}

/// Port 0x61, as something an address space can dispatch to.
#[derive(Debug)]
struct PortB(Arc<Registers>);

/// Port 0x92, as something an address space can dispatch to.
#[derive(Debug)]
struct PortA(Arc<Registers>);

/// Port 0xcf9, as something an address space can dispatch to.
#[derive(Debug)]
struct ResetCtl(Arc<Registers>);

/// Where the reset control register sits inside the four bytes at `0xcf8`.
///
/// `0xcf9 - 0xcf8`. It is a board fact, and this device is the board.
const CONFADD_RESETCTL_OFFSET: u64 = 1;

/// The four bytes at `0xcf8`, as this board's south-bridge stand-in decodes
/// them: the reset control register at `0xcf9`, and nothing at the other three.
///
/// Published through
/// [`ExportId::PORT_PASSTHROUGH`](crate::core::device::ExportId::PORT_PASSTHROUGH)
/// so a PCI host bridge can hand on the cycles its own `CONFADD` does not
/// claim — see [`PortPassthrough`](super::PortPassthrough) for why one chip has
/// to hold all four. A board with no host bridge maps
/// [`region("resetctl")`](Device::region) at `0xcf9` instead and never asks for
/// this; both routes reach the same latch.
#[derive(Debug)]
struct ConfAddWindow(Arc<Registers>);

impl MemOps for ConfAddWindow {
    fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
        for (i, slot) in dst.iter_mut().enumerate() {
            *slot = if offset + i as u64 == CONFADD_RESETCTL_OFFSET {
                self.0.read_c()
            } else {
                // I/O space with nothing behind it. The board's pull-ups.
                0xff
            };
        }
        let _ = attrs;
        Ok(())
    }

    fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
        for (i, byte) in src.iter().enumerate() {
            if offset + i as u64 != CONFADD_RESETCTL_OFFSET {
                continue;
            }
            if attrs.debug {
                // The same refusal `ResetCtl` makes, for the same reason: a
                // debug write of bit 2 would reboot the machine somebody is
                // debugging, and one that only latched bits 1 and 3 would
                // still move bit 2's remembered level.
                return Err(BusError::BadAccess);
            }
            self.0.write_c(*byte);
        }
        Ok(())
    }

    fn constraints(&self) -> AccessConstraints {
        // Byte or word, because that is what reaches here: a Dword access to
        // these four bytes is the host bridge's own `CONFADD` and never gets
        // this far (82441FX §3.1.1).
        AccessConstraints::IO.with_widths(Width::U8, Width::U16)
    }
}

/// An 8-bit port on an 8-bit bus. Every port here is one byte at one address,
/// so a wider access is a decode this board never performs.
fn byte_port() -> AccessConstraints {
    AccessConstraints::word(Width::U8, Endian::Little)
}

/// Every port here is one byte at one address, so anything else is a decode
/// this board never performs.
fn only_byte_zero(offset: u64, len: usize) -> MemResult {
    if offset == 0 && len == 1 {
        Ok(())
    } else {
        Err(BusError::BadAccess)
    }
}

impl MemOps for PortB {
    fn read(&self, offset: u64, dst: &mut [u8], _attrs: MemAttrs) -> MemResult {
        only_byte_zero(offset, dst.len())?;
        // No `debug` branch: the read has no side effects to suppress, and in
        // particular it does not touch the refresh toggle.
        dst[0] = self.0.read_b();
        Ok(())
    }

    fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
        only_byte_zero(offset, src.len())?;
        if attrs.debug {
            // Every bit here does something: bit 0 gates the timer, bit 1 the
            // speaker, and bits 2 and 3 acknowledge an NMI. There is no
            // harmless subset to allow.
            return Err(BusError::BadAccess);
        }
        self.0.write_b(src[0]);
        Ok(())
    }

    fn constraints(&self) -> AccessConstraints {
        byte_port()
    }
}

impl MemOps for PortA {
    fn read(&self, offset: u64, dst: &mut [u8], _attrs: MemAttrs) -> MemResult {
        only_byte_zero(offset, dst.len())?;
        dst[0] = self.0.read_a();
        Ok(())
    }

    fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
        only_byte_zero(offset, src.len())?;
        if attrs.debug {
            // A debug write of bit 0 would reset the machine somebody is
            // debugging, and of bit 1 would move A20 under the guest's feet.
            return Err(BusError::BadAccess);
        }
        self.0.write_a(src[0]);
        Ok(())
    }

    fn constraints(&self) -> AccessConstraints {
        byte_port()
    }
}

impl MemOps for ResetCtl {
    fn read(&self, offset: u64, dst: &mut [u8], _attrs: MemAttrs) -> MemResult {
        only_byte_zero(offset, dst.len())?;
        // No `debug` branch: the read hands back the latches and arms nothing,
        // so a debugger's window can poll it as often as it likes.
        dst[0] = self.0.read_c();
        Ok(())
    }

    fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
        only_byte_zero(offset, src.len())?;
        if attrs.debug {
            // A debug write of bit 2 would reboot the machine somebody is
            // debugging, and one that only latched bits 1 and 3 would still
            // move bit 2's remembered level, swallowing the guest's own
            // transition or manufacturing one.
            return Err(BusError::BadAccess);
        }
        self.0.write_c(src[0]);
        Ok(())
    }

    fn constraints(&self) -> AccessConstraints {
        byte_port()
    }
}

/// One of the two input pins, with the fan-in that makes a shared net correct.
#[derive(Debug)]
struct InputPin {
    regs: Arc<Registers>,
    /// [`LINE_REFRESH`] or [`LINE_TIMER2`].
    line: u32,
    inputs: FanIn,
}

impl WireSink for InputPin {
    fn set_level(&self, src: WireId, _line: u32, level: Level) {
        self.inputs.set(src, level);
        // Wired-OR, so a second driver on the same net cannot make the pin drop
        // while the first still asserts (`core::wire`, module docs).
        let level = self.inputs.resolve(Resolve::Or);
        if self.line == LINE_REFRESH {
            self.regs.refresh_edge(level);
        } else {
            self.regs.timer2_level(level);
        }
    }
}

/// The AT's system control ports.
#[derive(Debug)]
pub struct SysCtl {
    regs: Arc<Registers>,
    port_b: RegionRef,
    port_a: RegionRef,
    reset_ctl: RegionRef,
    /// The sinks handed out by [`Device::sink`], kept alive here: a net holds
    /// only a weak reference to a sink, so the device owns the strong one.
    pins: Mutex<Vec<Arc<InputPin>>>,
}

impl SysCtl {
    /// Validate `props` and build the device.
    ///
    /// # Errors
    ///
    /// [`Error::Property`] if a property this
    /// class does not know was given.
    pub fn new(props: &Props) -> Result<SysCtl> {
        props.reader().finish()?;
        Ok(SysCtl::default_device())
    }

    /// One with no properties set.
    #[must_use]
    pub fn default_device() -> SysCtl {
        let regs = Arc::new(Registers {
            state: Mutex::with_rank(LockRank::DEVICE, State::default()),
            gate2: Mutex::with_rank(LockRank::LEAF, None),
            a20: Mutex::with_rank(LockRank::LEAF, None),
            reset: Mutex::with_rank(LockRank::LEAF, None),
        });
        let port_b: RegionRef = Arc::new(Region::io(
            "pc.sysctl.portb",
            REGISTER_WINDOW_LEN,
            Arc::new(PortB(Arc::clone(&regs))) as Arc<dyn MemOps>,
        ));
        let port_a: RegionRef = Arc::new(Region::io(
            "pc.sysctl.porta",
            REGISTER_WINDOW_LEN,
            Arc::new(PortA(Arc::clone(&regs))) as Arc<dyn MemOps>,
        ));
        let reset_ctl: RegionRef = Arc::new(Region::io(
            "pc.sysctl.resetctl",
            REGISTER_WINDOW_LEN,
            Arc::new(ResetCtl(Arc::clone(&regs))) as Arc<dyn MemOps>,
        ));
        SysCtl {
            regs,
            port_b,
            port_a,
            reset_ctl,
            pins: Mutex::with_rank(LockRank::LEAF, Vec::new()),
        }
    }

    /// Latch a parity check, as a memory board's `/PCHK` would.
    ///
    /// Port B bit 7 then reads set until the guest writes bit 2. Nothing on
    /// this board asserts it yet — the AT drives NMI from it, and the NMI path
    /// is the machine file's — so it arrives as a method rather than as a third
    /// input pin.
    pub fn raise_parity_check(&self) {
        self.regs.state.lock().parity_status = true;
    }

    /// Latch an I/O channel check, as an adapter's `/IOCHCK` would.
    ///
    /// Port B bit 6 then reads set until the guest writes bit 3.
    pub fn raise_io_channel_check(&self) {
        self.regs.state.lock().iochk_status = true;
    }
}

/// The `pc.sysctl` device class.
pub static CLASS: DeviceClass = DeviceClass {
    name: CLASS_NAME,
    version: STATE_VERSION,
    summary: "the AT system control ports: speaker gate, refresh toggle, A20 and the reset paths",
    properties: &[],
    construct: |props| Ok(Box::new(SysCtl::new(props)?)),
};

/// The error for a pin name this device does not have.
fn unknown_pin(port: &str) -> Error {
    Error::Config {
        at: port.to_string(),
        message: String::from(
            "the system control ports take `refresh` and `timer2` in, \
             and drive `gate2`, `a20` and `reset` out",
        ),
    }
}

impl Device for SysCtl {
    fn class(&self) -> &'static DeviceClass {
        &CLASS
    }

    fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
        Ok(())
    }

    fn reset(&self, _kind: ResetKind) {
        // Both a cold and a warm reset land here with everything clear, which
        // is the state the board powers up in: speaker silent, and the fast
        // A20 path idle. Idle is not the same as shut — this is one of the
        // net's two drivers and the 8042 is the other, wire-ORed, and that one
        // comes out of reset holding the gate *open* (see `kbc`'s
        // `OUTPUT_PORT_RESET`, and `docs/platforms/pc-at.md`).
        //
        // The two remembered *input* levels stay. They are not this device's to
        // clear: they are what the 8254 is driving onto `refresh` and `timer2`,
        // and a reset of these latches does not reach across a pin and change
        // that. The comment that used to stand here said realize re-announces
        // them, and it does not: `announce` re-drives a *source*, and
        // `Wire::set` delivers a change rather than a level, so a driver
        // sitting where it already sat announces nothing at all. Forgetting the
        // level therefore sticks — port 0x61 bit 5 reads 0 while OUT2 is high,
        // and `refresh_edge` swallows the next transition as a repeat, which
        // inverts bit 4's phase for good. It is also what makes the next
        // re-announcement an edge: `Wire::refresh` delivers unconditionally on
        // every snapshot load, so a stale level turns into a toggle the saved
        // machine never had.
        //
        // `refresh_toggle` is a different thing and does go: it is this
        // device's own divide-by-two flip-flop, not a level anything drives.
        let (refresh_in, timer2_in) = {
            let s = self.regs.state.lock();
            (s.refresh_in, s.timer2_in)
        };
        *self.regs.state.lock() = State {
            refresh_in,
            timer2_in,
            ..State::default()
        };
        self.regs.drive_outputs();
    }

    fn export(&self, which: ExportId) -> Option<Export> {
        // The four bytes at 0xcf8, for a host bridge that has to hold all of
        // them. See `ConfAddWindow`.
        (which == ExportId::PORT_PASSTHROUGH).then(|| {
            Export::Opaque(Arc::new(super::PortPassthrough::new(
                Arc::new(ConfAddWindow(Arc::clone(&self.regs))) as Arc<dyn MemOps>,
            )))
        })
    }

    fn region(&self, name: &str) -> Option<RegionRef> {
        match name {
            // `""` is port B: it is the one every AT has, and the one a machine
            // file that maps only a speaker gate wants.
            "" | "portb" => Some(Arc::clone(&self.port_b)),
            "porta" => Some(Arc::clone(&self.port_a)),
            "resetctl" => Some(Arc::clone(&self.reset_ctl)),
            _ => None,
        }
    }

    fn sink(&self, port: &str, sources: &[WireId]) -> Option<SinkPin> {
        let line = match port {
            "refresh" => LINE_REFRESH,
            "timer2" => LINE_TIMER2,
            _ => return None,
        };
        let pin = Arc::new(InputPin {
            regs: Arc::clone(&self.regs),
            line,
            inputs: FanIn::new(sources),
        });
        self.pins.lock().push(Arc::clone(&pin));
        Some(SinkPin { sink: pin, line })
    }

    fn connect(&self, port: &str, source: WireSource) -> Result<()> {
        let pin = match port {
            "gate2" => &self.regs.gate2,
            "a20" => &self.regs.a20,
            "reset" => &self.regs.reset,
            _ => return Err(unknown_pin(port)),
        };
        *pin.lock() = Some(source);
        Ok(())
    }

    fn announce(&self, port: &str) {
        match port {
            // Both idle low out of reset, but a snapshot loaded before the
            // sweep can leave either high, and the 8254 has to be told.
            "gate2" | "a20" => self.regs.drive_outputs(),
            // `reset` is a pulse, not a level: it has no idle level to announce
            // beyond the low a fresh net already sits at, and driving it here
            // would reset the machine as it comes up.
            _ => {}
        }
    }

    fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
        let s = *self.regs.state.lock();
        w.write_u8(s.port_b)?;
        w.write_u8(s.port_a)?;
        w.write_bool(s.parity_status)?;
        w.write_bool(s.iochk_status)?;
        w.write_bool(s.refresh_toggle)?;
        // The remembered input levels, not the wires: a `FanIn` is rebuilt from
        // the machine's topology, but whether the refresh pin was last seen
        // high decides whether the next notification is an edge.
        w.write_bool(s.refresh_in)?;
        w.write_bool(s.timer2_in)?;
        w.write_u8(s.reset_ctl)?;
        // Bit 2's remembered level, for the reason the input levels are saved:
        // whether the next write is a transition depends on it, and a snapshot
        // that dropped it would turn a repeated 0x06 into a reboot.
        w.write_bool(s.rst_cpu)
    }

    fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
        let state = State {
            port_b: r.read_u8()? & B_LATCH_MASK,
            port_a: r.read_u8()? & !A_FAST_RESET,
            parity_status: r.read_bool()?,
            iochk_status: r.read_bool()?,
            refresh_toggle: r.read_bool()?,
            refresh_in: r.read_bool()?,
            timer2_in: r.read_bool()?,
            reset_ctl: r.read_u8()? & C_LATCH_MASK,
            rst_cpu: r.read_bool()?,
        };
        *self.regs.state.lock() = state;
        // The gate and A20 are levels the rest of the machine has to agree
        // with, so they are re-driven; reset is a pulse and has nothing to
        // restore.
        self.regs.drive_outputs();
        Ok(())
    }
}

impl Instance for SysCtl {}

/// Add [`CLASS`] to a registry.
///
/// # Errors
///
/// [`Error::Config`] if the name is claimed.
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
    registry.add(&CLASS)
}

/// Bind [`CLASS`] into the machine graph.
///
/// # Errors
///
/// [`Error::Config`] if the class is bound twice.
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
    bindings.bind(CLASS_NAME, |props| Ok(Arc::new(SysCtl::new(props)?)))
}

/// What the validator should know about `pc.sysctl`.
#[must_use]
pub fn schema() -> ClassSchema {
    use crate::machine::validate::PortDir;
    ClassSchema::new(CLASS_NAME)
        .region("")
        .region("portb")
        .region("porta")
        .region("resetctl")
        .port("refresh", PortDir::In)
        .port("timer2", PortDir::In)
        .port("gate2", PortDir::Out)
        .port("a20", PortDir::Out)
        .port("reset", PortDir::Out)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::state::{MachineShape, Migrations, StateReader, StateWriter};
    use crate::core::sync::{AtomicU32, Ordering};
    use crate::core::wire::{Wire, WireIdAllocator};

    /// A probe that remembers the last level and counts rising edges, which is
    /// the only way to see a pulse after the fact.
    #[derive(Debug, Default)]
    struct Probe {
        level: AtomicU32,
        rises: AtomicU32,
    }

    impl WireSink for Probe {
        fn set_level(&self, _src: WireId, _line: u32, level: Level) {
            if level.is_high() {
                self.rises.fetch_add(1, Ordering::Relaxed);
            }
            self.level
                .store(u32::from(level.is_high()), Ordering::Relaxed);
        }
    }

    impl Probe {
        fn high(&self) -> bool {
            self.level.load(Ordering::Relaxed) == 1
        }

        fn rises(&self) -> u32 {
            self.rises.load(Ordering::Relaxed)
        }
    }

    /// Attach a probe to output pin `port`.
    fn watch(dev: &SysCtl, port: &str) -> Arc<Probe> {
        let ids = WireIdAllocator::new();
        let id = ids.alloc();
        let probe = Arc::new(Probe::default());
        let wire = Wire::builder()
            .source(id)
            .sink(Arc::clone(&probe) as Arc<dyn WireSink>, 0)
            .build_shared();
        dev.connect(port, WireSource::new(wire, id))
            .expect("the system control ports drive this pin");
        probe
    }

    /// A driver for input pin `port`.
    fn feed(dev: &SysCtl, port: &str) -> WireSource {
        let ids = WireIdAllocator::new();
        let id = ids.alloc();
        let pin = dev
            .sink(port, &[id])
            .expect("the system control ports listen on this pin");
        let wire = Wire::builder()
            .source(id)
            .sink(pin.sink, pin.line)
            .build_shared();
        WireSource::new(wire, id)
    }

    fn peek(dev: &SysCtl, region: &str) -> u8 {
        let mut byte = [0u8; 1];
        ops(dev, region)
            .read(0, &mut byte, MemAttrs::DEFAULT)
            .expect("a byte read is legal");
        byte[0]
    }

    fn poke(dev: &SysCtl, region: &str, value: u8) {
        ops(dev, region)
            .write(0, &[value], MemAttrs::DEFAULT)
            .expect("a byte write is legal");
    }

    /// The `MemOps` behind a named region, which is what a machine file maps.
    fn ops(dev: &SysCtl, region: &str) -> Arc<dyn MemOps> {
        match region {
            "porta" => Arc::new(PortA(Arc::clone(&dev.regs))) as Arc<dyn MemOps>,
            "resetctl" => Arc::new(ResetCtl(Arc::clone(&dev.regs))) as Arc<dyn MemOps>,
            _ => Arc::new(PortB(Arc::clone(&dev.regs))) as Arc<dyn MemOps>,
        }
    }

    #[test]
    fn port_b_bit_0_gates_the_timer_and_reads_back() {
        let dev = SysCtl::default_device();
        let gate2 = watch(&dev, "gate2");
        assert!(!gate2.high(), "the speaker is silent at power-on");

        poke(&dev, "portb", B_GATE2);
        assert!(gate2.high());
        assert_eq!(peek(&dev, "portb") & B_LATCH_MASK, B_GATE2);

        poke(&dev, "portb", B_GATE2 | B_SPEAKER);
        assert!(gate2.high(), "and stays gated");
        assert_eq!(peek(&dev, "portb") & B_LATCH_MASK, B_GATE2 | B_SPEAKER);

        poke(&dev, "portb", 0);
        assert!(!gate2.high());
    }

    #[test]
    fn port_b_bit_5_follows_the_timer2_pin() {
        // Firmware calibrates by spinning on this bit, so it has to be the pin
        // and not a value this device made up.
        let dev = SysCtl::default_device();
        let timer2 = feed(&dev, "timer2");
        assert_eq!(peek(&dev, "portb") & B_TIMER2_OUT, 0);
        timer2.set(Level::High);
        assert_eq!(peek(&dev, "portb") & B_TIMER2_OUT, B_TIMER2_OUT);
        timer2.set(Level::Low);
        assert_eq!(peek(&dev, "portb") & B_TIMER2_OUT, 0);
    }

    #[test]
    fn each_refresh_edge_flips_port_b_bit_4() {
        let dev = SysCtl::default_device();
        let refresh = feed(&dev, "refresh");
        let mut expected = 0u8;
        for _ in 0..4 {
            for level in [Level::High, Level::Low] {
                refresh.set(level);
                expected ^= B_REFRESH;
                assert_eq!(peek(&dev, "portb") & B_REFRESH, expected);
            }
        }
        // A repeat of the level already seen is not an edge.
        refresh.set(Level::Low);
        assert_eq!(peek(&dev, "portb") & B_REFRESH, expected);
    }

    #[test]
    fn port_a_bit_1_drives_a20_and_bit_0_pulses_reset() {
        let dev = SysCtl::default_device();
        let a20 = watch(&dev, "a20");
        let reset = watch(&dev, "reset");

        poke(&dev, "porta", A_GATE_A20);
        assert!(a20.high());
        assert_eq!(reset.rises(), 0, "A20 alone resets nothing");
        assert_eq!(peek(&dev, "porta"), A_GATE_A20);

        poke(&dev, "porta", A_GATE_A20 | A_FAST_RESET);
        assert_eq!(reset.rises(), 1);
        assert!(!reset.high(), "a pulse, not a level");
        assert!(a20.high(), "and A20 stayed where it was put");
        // Bit 0 reads back clear, or the read-modify-write every A20 routine
        // performs would reset the machine on its way past.
        assert_eq!(peek(&dev, "porta"), A_GATE_A20);

        poke(&dev, "porta", 0);
        assert!(!a20.high());
    }

    #[test]
    fn reset_control_pulses_on_bit_2s_transition_and_not_on_a_repeat() {
        // Firmware writes 0x02 and then 0x06 for exactly this reason.
        let dev = SysCtl::default_device();
        let reset = watch(&dev, "reset");

        poke(&dev, "resetctl", C_SYS_RST);
        assert_eq!(reset.rises(), 0, "arming the kind of reset is not a reset");

        poke(&dev, "resetctl", C_SYS_RST | C_RST_CPU);
        assert_eq!(reset.rises(), 1);
        assert!(!reset.high(), "a pulse, not a level");

        poke(&dev, "resetctl", C_SYS_RST | C_RST_CPU);
        assert_eq!(
            reset.rises(),
            1,
            "a write that leaves bit 2 set is not an edge"
        );

        // Dropping bit 2 and raising it again is a second transition.
        poke(&dev, "resetctl", C_SYS_RST);
        assert_eq!(reset.rises(), 1);
        poke(&dev, "resetctl", C_SYS_RST | C_RST_CPU);
        assert_eq!(reset.rises(), 2);
    }

    #[test]
    fn reset_control_reaches_the_same_reset_pin_as_port_a() {
        // The board has one reset pin and three ways to pull it; that is a fact
        // about PC chipsets, and the net resolves the rest.
        let dev = SysCtl::default_device();
        let reset = watch(&dev, "reset");
        poke(&dev, "porta", A_FAST_RESET);
        assert_eq!(reset.rises(), 1);
        poke(&dev, "resetctl", C_RST_CPU);
        assert_eq!(reset.rises(), 2);
    }

    #[test]
    fn reset_control_bit_2_reads_back_clear_and_the_others_read_back() {
        let dev = SysCtl::default_device();
        assert_eq!(peek(&dev, "resetctl"), 0, "clear at power-on");

        poke(&dev, "resetctl", C_SYS_RST | C_RST_CPU | C_FULL_RST);
        // Bit 2 reads back clear, or the read-modify-write a firmware routine
        // performs would reboot the machine on its way past.
        assert_eq!(peek(&dev, "resetctl"), C_SYS_RST | C_FULL_RST);

        poke(&dev, "resetctl", C_FULL_RST);
        assert_eq!(peek(&dev, "resetctl"), C_FULL_RST);

        // The bits outside the register land nowhere.
        poke(&dev, "resetctl", 0xf0 | C_SYS_RST);
        assert_eq!(peek(&dev, "resetctl"), C_SYS_RST);
    }

    #[test]
    fn a_debug_access_of_the_reset_control_register_neither_reboots_nor_arms() {
        let dev = SysCtl::default_device();
        let reset = watch(&dev, "reset");
        poke(&dev, "resetctl", C_SYS_RST);
        let before = peek(&dev, "resetctl");

        let mut byte = [0u8; 1];
        for _ in 0..3 {
            ops(&dev, "resetctl")
                .read(0, &mut byte, MemAttrs::DEBUG)
                .expect("a debug read is legal");
            assert_eq!(byte[0], before, "and it moved nothing");
        }

        assert!(
            ops(&dev, "resetctl")
                .write(0, &[C_SYS_RST | C_RST_CPU], MemAttrs::DEBUG)
                .is_err()
        );
        assert_eq!(reset.rises(), 0, "nothing was rebooted");
        assert_eq!(peek(&dev, "resetctl"), before, "and nothing was latched");
        // The refused write did not remember bit 2 either, so the guest's own
        // transition is still a transition.
        poke(&dev, "resetctl", C_SYS_RST | C_RST_CPU);
        assert_eq!(reset.rises(), 1);
    }

    #[test]
    fn writing_the_enable_bits_clears_the_check_status_bits() {
        // The AT has no acknowledge register: this write *is* how a parity NMI
        // handler stops being re-entered.
        let dev = SysCtl::default_device();
        dev.raise_parity_check();
        dev.raise_io_channel_check();
        assert_eq!(
            peek(&dev, "portb") & (B_PARITY_STATUS | B_IOCHK_STATUS),
            B_PARITY_STATUS | B_IOCHK_STATUS
        );

        poke(&dev, "portb", B_PARITY_ENABLE);
        assert_eq!(peek(&dev, "portb") & B_PARITY_STATUS, 0);
        assert_eq!(
            peek(&dev, "portb") & B_IOCHK_STATUS,
            B_IOCHK_STATUS,
            "the other one stands"
        );

        poke(&dev, "portb", B_IOCHK_ENABLE);
        assert_eq!(peek(&dev, "portb") & B_IOCHK_STATUS, 0);
    }

    #[test]
    fn a_debug_read_of_port_b_changes_nothing_and_a_debug_write_is_refused() {
        let dev = SysCtl::default_device();
        let refresh = feed(&dev, "refresh");
        let gate2 = watch(&dev, "gate2");
        refresh.set(Level::High);
        let before = peek(&dev, "portb");
        assert_eq!(before & B_REFRESH, B_REFRESH);

        let mut byte = [0u8; 1];
        for _ in 0..3 {
            ops(&dev, "portb")
                .read(0, &mut byte, MemAttrs::DEBUG)
                .expect("a debug read is legal");
            assert_eq!(byte[0], before, "the refresh toggle did not move");
        }

        assert!(
            ops(&dev, "portb")
                .write(0, &[B_GATE2], MemAttrs::DEBUG)
                .is_err()
        );
        assert!(!gate2.high(), "and nothing was gated");
        assert!(
            ops(&dev, "porta")
                .write(0, &[A_FAST_RESET], MemAttrs::DEBUG)
                .is_err()
        );
    }

    #[test]
    fn an_access_that_is_not_a_single_byte_at_offset_zero_is_refused() {
        let dev = SysCtl::default_device();
        for region in ["portb", "porta", "resetctl"] {
            let ops = ops(&dev, region);
            assert!(ops.read(0, &mut [0u8; 2], MemAttrs::DEFAULT).is_err());
            assert!(ops.read(1, &mut [0u8; 1], MemAttrs::DEFAULT).is_err());
            assert!(ops.write(0, &[0u8; 4], MemAttrs::DEFAULT).is_err());
            assert!(ops.write(1, &[0u8], MemAttrs::DEFAULT).is_err());
        }
    }

    #[test]
    fn the_two_ports_are_separate_regions() {
        let dev = SysCtl::default_device();
        assert!(dev.region("").is_some());
        assert!(dev.region("portb").is_some());
        assert!(dev.region("porta").is_some());
        assert!(
            dev.region("regs").is_none(),
            "one name per port, not a lump"
        );
        // A write to one is not a write to the other: they are 0x61 and 0x92.
        poke(&dev, "portb", B_GATE2 | B_SPEAKER);
        assert_eq!(peek(&dev, "porta"), 0);
        poke(&dev, "porta", A_GATE_A20);
        assert_eq!(peek(&dev, "portb") & B_LATCH_MASK, B_GATE2 | B_SPEAKER);
    }

    #[test]
    fn an_unknown_pin_is_an_error_rather_than_a_silent_no_op() {
        let dev = SysCtl::default_device();
        let ids = WireIdAllocator::new();
        let id = ids.alloc();
        let wire = Wire::builder().source(id).build_shared();
        assert!(dev.connect("speaker", WireSource::new(wire, id)).is_err());
        assert!(dev.sink("gate2", &[id]).is_none(), "gate2 is an output");
    }

    #[test]
    fn a_reset_silences_the_speaker_and_masks_a20() {
        let dev = SysCtl::default_device();
        let gate2 = watch(&dev, "gate2");
        let a20 = watch(&dev, "a20");
        poke(&dev, "portb", B_GATE2 | B_SPEAKER);
        poke(&dev, "porta", A_GATE_A20);
        dev.raise_parity_check();

        dev.reset(ResetKind::Cold);
        assert!(!gate2.high());
        assert!(!a20.high());
        assert_eq!(peek(&dev, "portb"), 0);
        assert_eq!(peek(&dev, "porta"), 0);
    }

    #[test]
    fn a_reset_does_not_forget_what_the_timer_is_driving() {
        // The two input pins are the 8254's, not this device's. A reset clears
        // the latches on this side of them; it does not reach across a pin and
        // change what the timer is doing, and nothing will ever tell this
        // device otherwise — a driver that has not moved announces nothing.
        let dev = SysCtl::default_device();
        let refresh = feed(&dev, "refresh");
        let timer2 = feed(&dev, "timer2");
        timer2.raise();
        refresh.raise();
        assert_eq!(peek(&dev, "portb") & B_TIMER2_OUT, B_TIMER2_OUT);
        let toggle = peek(&dev, "portb") & B_REFRESH;

        dev.reset(ResetKind::Cold);

        // Bit 5 is the pin, with nothing in between (module docs), so it still
        // reports the level the timer is still driving.
        assert_eq!(
            peek(&dev, "portb") & B_TIMER2_OUT,
            B_TIMER2_OUT,
            "the speaker's own waveform vanished from port 0x61 across a reset"
        );
        // The toggle itself is this device's flip-flop and does reset.
        assert_eq!(peek(&dev, "portb") & B_REFRESH, 0);
        let _ = toggle;

        // A re-announcement of an unchanged level — `Wire::refresh` on every
        // snapshot load — is not an edge and must not flip bit 4.
        refresh.set(Level::High);
        timer2.set(Level::High);
        assert_eq!(
            peek(&dev, "portb") & B_REFRESH,
            0,
            "a level that never moved was counted as a refresh edge"
        );

        // And the next real transition still is one.
        refresh.lower();
        assert_eq!(peek(&dev, "portb") & B_REFRESH, B_REFRESH);
        assert_eq!(peek(&dev, "portb") & B_TIMER2_OUT, B_TIMER2_OUT);
    }

    /// Save `dev` into a one-device snapshot.
    fn save_image(dev: &SysCtl) -> Vec<u8> {
        let mut shape = MachineShape::new();
        shape.add_device("sysctl", CLASS.name).unwrap();
        let mut w = StateWriter::new(shape);
        {
            let mut chunk = w.chunk("sysctl", CLASS.name, CLASS.version).unwrap();
            dev.save(&mut chunk).unwrap();
        }
        w.to_vec().unwrap()
    }

    #[test]
    fn a_snapshot_round_trips_every_latch_and_both_input_levels() {
        let saved = SysCtl::default_device();
        let refresh = feed(&saved, "refresh");
        let timer2 = feed(&saved, "timer2");
        poke(&saved, "portb", B_GATE2 | B_SPEAKER | B_IOCHK_ENABLE);
        poke(&saved, "porta", A_GATE_A20 | 0x40);
        // Bit 2 set as well, so the remembered trigger level is true on the far
        // side and a snapshot that dropped it would reboot on the next 0x06.
        poke(&saved, "resetctl", C_SYS_RST | C_RST_CPU | C_FULL_RST);
        saved.raise_parity_check();
        // An odd number of edges, so the toggle is set and the pin is high:
        // the two are independent and a snapshot that conflated them would
        // still pass a one-edge test.
        refresh.set(Level::High);
        timer2.set(Level::High);
        let image = save_image(&saved);

        let restored = SysCtl::default_device();
        let gate2 = watch(&restored, "gate2");
        let a20 = watch(&restored, "a20");
        let reset = watch(&restored, "reset");
        let reader = StateReader::new(&image).unwrap();
        let chunk = reader
            .load("sysctl", CLASS.name, CLASS.version, &Migrations::new())
            .unwrap();
        restored.load(&mut chunk.reader()).unwrap();

        assert_eq!(peek(&restored, "portb"), peek(&saved, "portb"));
        assert_eq!(peek(&restored, "porta"), peek(&saved, "porta"));
        assert_eq!(peek(&restored, "resetctl"), peek(&saved, "resetctl"));
        assert!(gate2.high(), "the levels were re-driven on load");
        assert!(a20.high());
        // The remembered refresh level came back, so the next notification of
        // the same level is still not an edge.
        let refresh = feed(&restored, "refresh");
        refresh.set(Level::High);
        assert_eq!(
            peek(&restored, "portb") & B_REFRESH,
            peek(&saved, "portb") & B_REFRESH
        );

        // Bit 2's remembered level came back, so the same write is still not a
        // transition. The latched bits are unchanged by it, so the image is
        // still the one that was saved.
        poke(&restored, "resetctl", C_SYS_RST | C_RST_CPU | C_FULL_RST);
        assert_eq!(
            reset.rises(),
            0,
            "a repeat is not an edge, even after a load"
        );

        assert_eq!(save_image(&restored), image, "byte-identical");
    }
}