@@ -8,6 +8,7 @@ namespace Spice86.Core.Emulator.Devices.Input.Keyboard;
88
99using System . Collections . Generic ;
1010using System . Diagnostics ;
11+ using System . Linq ;
1112
1213/// <summary>
1314/// Emulates a PS/2 keyboard device, handling key events, scancode translation, and communication with a keyboard
@@ -33,6 +34,14 @@ public partial class PS2Keyboard {
3334 private RepeatData _repeat = new ( ) { Key = PcKeyboardKey . None , WaitMs = 0 , PauseMs = 500 , RateMs = 33 } ;
3435 private readonly Dictionary < byte , Set3CodeInfoEntry > _set3CodeInfo = new ( ) ;
3536
37+ /// <summary>
38+ /// Keys the device currently believes are held down. A host key-up can be lost (window lost focus,
39+ /// input dropped while paused, frame-buffer overflow) and the guest only ever clears a modifier on its
40+ /// BREAK scancode, so this set is the bookkeeping that lets the device reconcile and release what it
41+ /// still thinks is down.
42+ /// </summary>
43+ private readonly HashSet < PcKeyboardKey > _pressedKeys = new ( ) ;
44+
3645 /// <summary>
3746 /// State of keyboard LEDs, as requested via keyboard controller
3847 /// </summary>
@@ -130,6 +139,11 @@ private void EnqueueScanCodeFrame(List<byte>? scanCode) {
130139 if ( _keyboardFrameBuffer . Count == BufferSizeInScancodes ) {
131140 _keyboardFrameBuffer . Clear ( ) ;
132141 _bufferOverflowed = true ;
142+ // A discarded key-up would otherwise latch that key (and its modifier bits) in the guest forever.
143+ // Reconcile the held-key set so no key is believed to be down any more.
144+ _pressedKeys . Clear ( ) ;
145+ _repeat . Key = PcKeyboardKey . None ;
146+ _repeat . WaitMs = 0 ;
133147 return ;
134148 }
135149
@@ -521,6 +535,44 @@ internal void NotifyReadyForFrame() {
521535 TransferNextBufferedFrameIfReady ( ) ;
522536 }
523537
538+ /// <summary>
539+ /// Releases every key the device still believes is down, emitting a proper BREAK frame for each one,
540+ /// and stops any in-flight typematic repeat.
541+ /// </summary>
542+ /// <remarks>
543+ /// Call this when the host's key state can no longer be trusted: the window lost activation, the
544+ /// emulator was paused, or the keyboard frame buffer overflowed and discarded a key-up. The guest only
545+ /// clears a modifier on its BREAK scancode, so a lost key-up otherwise latches it forever (e.g. Alt
546+ /// left in <c>KB_FLAG</c>), silently changing how every other key is translated. This method is safe to
547+ /// call on the emulator thread only; marshal cross-thread calls onto the emulator thread first.
548+ /// </remarks>
549+ public void ReleaseAllKeys ( ) {
550+ if ( _pressedKeys . Count == 0 ) {
551+ _repeat . Key = PcKeyboardKey . None ;
552+ _repeat . WaitMs = 0 ;
553+ return ;
554+ }
555+
556+ // Emit a BREAK frame for each held key in a deterministic order so the guest sees a stable sequence.
557+ List < PcKeyboardKey > heldKeys = _pressedKeys . OrderBy ( key => key ) . ToList ( ) ;
558+ foreach ( PcKeyboardKey key in heldKeys ) {
559+ EnqueueScanCodeFrame ( GetReleaseScanCode ( key ) ) ;
560+ }
561+
562+ _pressedKeys . Clear ( ) ;
563+ _repeat . Key = PcKeyboardKey . None ;
564+ _repeat . WaitMs = 0 ;
565+ }
566+
567+ private List < byte > GetReleaseScanCode ( PcKeyboardKey keyType ) {
568+ return _codeSet switch {
569+ 1 => _scancodeConverter . GetScanCode1 ( keyType , isPressed : false ) ,
570+ 2 => _scancodeConverter . GetScanCode2 ( keyType , isPressed : false ) ,
571+ 3 => _scancodeConverter . GetScanCode3 ( keyType , isPressed : false ) ,
572+ _ => _scancodeConverter . GetScanCode1 ( keyType , isPressed : false ) ,
573+ } ;
574+ }
575+
524576 /// <summary>
525577 /// Adds a key event to be processed by the keyboard.
526578 /// </summary>
@@ -531,6 +583,16 @@ public void EnqueueKeyEvent(PcKeyboardKey keyType, bool isPressed) {
531583 return ;
532584 }
533585
586+ if ( keyType is PcKeyboardKey . None or PcKeyboardKey . Pause or PcKeyboardKey . PrintScreen ) {
587+ // Keys that are not tracked as held: Pause has no BREAK frame, PrintScreen is a momentary
588+ // key-chord, and None is an unsupported key. Keeping them out of the held-key set means a
589+ // later release-all does not try to emit a BREAK scancode that does not exist.
590+ } else if ( isPressed ) {
591+ _pressedKeys . Add ( keyType ) ;
592+ } else {
593+ _pressedKeys . Remove ( keyType ) ;
594+ }
595+
534596 List < byte > scanCode = new ( ) ;
535597
536598 switch ( _codeSet ) {
0 commit comments