-
Notifications
You must be signed in to change notification settings - Fork 34
feature: All-in-one PS/2 micro-controller and keyboard emulation once and for all #1247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
dd25081
to
06435f3
Compare
06435f3
to
72e9457
Compare
41c1686
to
8877921
Compare
d051c80
to
082e61b
Compare
src/Spice86.Core/Emulator/InterruptHandlers/Input/Keyboard/BiosKeyboardInt9Handler.cs
Outdated
Show resolved
Hide resolved
src/Spice86.Core/Emulator/InterruptHandlers/Input/Keyboard/BiosKeyboardInt9Handler.cs
Outdated
Show resolved
Hide resolved
0a9d7d2
to
a46bdae
Compare
src/Spice86.Core/Emulator/Devices/Input/Keyboard/AvalionaKeyConverter.cs
Outdated
Show resolved
Hide resolved
src/Spice86.Core/Emulator/Devices/Input/Keyboard/PS2Keyboard.cs
Outdated
Show resolved
Hide resolved
src/Spice86.Core/Emulator/InterruptHandlers/Input/Keyboard/BiosKeyboardInt9Handler.cs
Outdated
Show resolved
Hide resolved
public override void Run() { | ||
byte? scanCode = _keyboard.KeyboardEvent.ScanCode; | ||
byte ascii = _keyboard.KeyboardEvent.AsciiCode ?? 0; | ||
_keyboard.WriteByte(KeyboardPorts.Command, (byte)KeyboardCommand.DisablePortKbd); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldnt this logic to disable keyboard, read data and enable again be in keyboard class? Overrides might need it.
I wonder why doing so though, to ensure no other key gets added to the buffer? Wouldn't that solve the problem partially considering between time keyboard request irq1 and time this actually gets called, some time could already have passed? (Think about masked irq or other irq being processed)
byte scanCode = _keyboard.ReadByte(KeyboardPorts.Data); | ||
_keyboard.WriteByte(KeyboardPorts.Command, (byte)KeyboardCommand.EnablePortKbd); | ||
|
||
byte savedAl = State.AL; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ushort savedAX = State.AX since you use both al and ah?
_performanceMeasurer.UpdateValue(_cpuState.Cycles); | ||
_timer.Tick(); | ||
_dmaController.PerformDmaTransfers(); | ||
_inputEventQueue.ProcessAllPendingInputEvents(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why cant this be done on ui thread that would put events in a queue in each device? Wouldn't this be less impactful than checking that every cycle?
internal void OnMainWindowClosing() => _isAppClosing = true; | ||
|
||
internal void OnKeyUp(KeyEventArgs e) { | ||
if (IsPaused) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't this be checked in one common class that would be used to enquête events for the devices? UI would push events thrre and it would ignore them if paused
e06ebea
to
323d7dc
Compare
src/Spice86.Core/Emulator/Devices/Input/Keyboard/Intel8042PS2KeyboardMouseController.cs
Fixed
Show fixed
Hide fixed
src/Spice86.Core/Emulator/Devices/Input/Keyboard/Intel8042PS2KeyboardMouseController.cs
Fixed
Show fixed
Hide fixed
src/Spice86.Core/Emulator/Devices/Input/Keyboard/Intel8042PS2KeyboardMouseController.cs
Fixed
Show fixed
Hide fixed
src/Spice86.Core/Emulator/Devices/Input/Keyboard/Intel8042PS2KeyboardMouseController.cs
Fixed
Show fixed
Hide fixed
src/Spice86.Core/Emulator/Devices/Input/Keyboard/Intel8042PS2KeyboardMouseController.cs
Fixed
Show fixed
Hide fixed
src/Spice86.Core/Emulator/Devices/Input/Keyboard/Intel8042PS2KeyboardMouseController.cs
Fixed
Show fixed
Hide fixed
Updated the PS2Keyboard, Mouse, VgaCard, and MouseDriver classes to use more specific interfaces for handling keyboard, mouse, and video events. Removed the IGui interface and introduced IGuiKeyboardEvents, IGuiMouseEvents, and IGuiVideoPresentation for better separation of concerns. The HeadlessGui class now implements these new interfaces, and the Spice86DependencyInjection and MainWindowViewModel classes were modified accordingly to support the changes.
- Introduced `InputEventQueue` for processing keyboard and mouse events in a thread-safe manner. - Updated `EmulationLoop` to include `_inputEventQueue` and modified its constructor to accept this new parameter. - Enhanced the main execution method of `EmulationLoop` to process input events during each iteration. - Updated documentation for the `IsPaused` property for clarity. - Modified `Spice86DependencyInjection` and `DosFileManagerTests` to integrate the new input event handling functionality.
Introduced the `Intel8042PS2KeyboardMouseController` class to implement PS/2 keyboard and mouse logic, replacing the old `PS2Keyboard` class. Updated the `KeyboardCommand` enum to include a new command, `ReadKeyboardVersion`. Modified `BiosKeyboardInt9Handler` and `EmulationLoop` to utilize the new controller. Improved buffer management, command execution, and event handling throughout the application.
Replaced `AvaloniaKeyConverter` with `KeyboardScancodeConverter` for improved scancode and ASCII conversion. Introduced `PhysicalKey` and `KbdKey` enums to standardize key representation and align with modern standards. Removed the `Key` enum and its references. Refactored `Intel8042PS2KeyboardMouseController` to use the new converter and simplified typematic handling. Updated `BiosKeyboardInt9Handler` to process scancodes and update BIOS flags, mimicking DOSBox's `IRQ1_Handler`. Simplified `MainWindowViewModel` and `EmulationLoop` by removing redundant dependencies. Updated tests and dependency injection to reflect these changes. Overall, these updates enhance maintainability, extensibility, and compatibility with DOSBox-style emulation.
Refactored `Intel8042PS2KeyboardMouseController` to `Intel8042Controller` for clarity and updated its constructor to include `EmulatorEventClock` for managing timed events. Extracted `PS2Keyboard` into its own class and refactored it to use `EmulatorEventClock` for typematic/repeat handling. Improved scancode handling with support for sets 1, 2, and 3. Introduced `EmulatorEventClock` to manage timed events, integrated it into `EmulationLoop`, and updated related classes (`BiosKeyboardInt9Handler`, `Machine`, `Spice86DependencyInjection`) to use the new `Intel8042Controller`. Removed redundant code, improved modularity, and updated tests to reflect these changes.
Side by side debugging for now...
5d483bf
to
df6d6f1
Compare
src/Spice86.Core/Emulator/Devices/Input/Keyboard/PS2Keyboard.cs
Dismissed
Show dismissed
Hide dismissed
505e81b
to
0c4645e
Compare
if ((param & (1 << 0)) == 0) { | ||
if (_loggerService.IsEnabled(LogEventLevel.Warning)) { | ||
_loggerService.Warning("I8042: Clearing P2 bit 0 locks a real PC"); | ||
} | ||
// System restart is not suported. | ||
} |
Check notice
Code scanning / CodeQL
Nested 'if' statements can be combined Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
The issue occurs in src/Spice86.Core/Emulator/Devices/Input/Keyboard/Intel8042Controller.cs
at lines 630–633. The nested if
statements should be merged into a single if
statement using the &&
operator, so that both guards are combined and the block is only executed if both conditions are true. No change to the functionality will result, but the code will be easier to read.
No imports or method definitions are required for this change. Only the lines containing the nested if
statements and the logging statement should be changed.
-
Copy modified lines R630-R631 -
Copy modified lines R633-R634
@@ -627,12 +627,11 @@ | ||
case KeyboardCommand.WriteOutputPort: // 0xd1 | ||
// Writes the controller output port (P2) | ||
_a20Gate.IsEnabled = (param & (1 << 1)) != 0; | ||
if ((param & (1 << 0)) == 0) { | ||
if (_loggerService.IsEnabled(LogEventLevel.Warning)) { | ||
_loggerService.Warning("I8042: Clearing P2 bit 0 locks a real PC"); | ||
} | ||
// System restart is not suported. | ||
if ((param & (1 << 0)) == 0 && _loggerService.IsEnabled(LogEventLevel.Warning)) { | ||
_loggerService.Warning("I8042: Clearing P2 bit 0 locks a real PC"); | ||
} | ||
// System restart is not suported. | ||
|
||
break; | ||
case KeyboardCommand.SimulateInputKbd: // 0xd2 | ||
// Acts as if the byte was received from keyboard |
if (code == 0xf0 && (lines & 0b0001) == 0) { | ||
if(_loggerService.IsEnabled(LogEventLevel.Warning)) { | ||
_loggerService.Warning("System Reset is not implemented"); | ||
} | ||
} |
Check notice
Code scanning / CodeQL
Nested 'if' statements can be combined Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
The fix is to collapse the nested if
statements at lines 667–669 into a single conditional using the logical &&
operator, following C# operator precedence (which is well-defined for &&
). The new line will join the inner and outer conditions:
if (code == 0xf0 && (lines & 0b0001) == 0 && _loggerService.IsEnabled(LogEventLevel.Warning)) { ... }
Only lines 667–670 are affected. No new imports or definitions are required.
-
Copy modified lines R667-R668
@@ -664,10 +664,8 @@ | ||
(code != 0xf0 && param != 0b1111)) { | ||
WarnLinePulse(); | ||
} | ||
if (code == 0xf0 && (lines & 0b0001) == 0) { | ||
if (_loggerService.IsEnabled(LogEventLevel.Warning)) { | ||
_loggerService.Warning("System Reset is not implemented"); | ||
} | ||
if (code == 0xf0 && (lines & 0b0001) == 0 && _loggerService.IsEnabled(LogEventLevel.Warning)) { | ||
_loggerService.Warning("System Reset is not implemented"); | ||
} | ||
} else { | ||
// If we are here, than either this function |
} | ||
} | ||
|
||
private void ProcessScancode(byte scanCode, ref byte flags1, ref byte flags2, ref byte flags3, ref byte leds) { |
Check notice
Code scanning / CodeQL
Too many 'ref' parameters Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 days ago
The best solution is to encapsulate the four byte
values (flags1
, flags2
, flags3
, and leds
) into a new dedicated struct or class, for example, KeyboardState
. The ProcessScancode
method signature should then take a single KeyboardState
object (ideally by reference if mutation is needed for performance, or as a class reference type if acceptable). All internal code that previously referenced the ref
bytes should now access the properties of KeyboardState
. This change improves code readability, maintainability, and reduces the method’s complexity. Only the region encompassing the relevant method declaration and its usages within this file need updating; any usages outside this file must be disregarded as per the instructions.
-
Copy modified lines R275-R282 -
Copy modified line R286 -
Copy modified line R289 -
Copy modified line R292 -
Copy modified lines R295-R298 -
Copy modified lines R302-R305 -
Copy modified line R309 -
Copy modified line R312
@@ -272,37 +272,44 @@ | ||
} | ||
} | ||
|
||
private void ProcessScancode(byte scanCode, ref byte flags1, ref byte flags2, ref byte flags3, ref byte leds) { | ||
private class KeyboardState { | ||
public byte Flags1; | ||
public byte Flags2; | ||
public byte Flags3; | ||
public byte Leds; | ||
} | ||
|
||
private void ProcessScancode(byte scanCode, KeyboardState state) { | ||
switch (scanCode) { | ||
/* First the hard ones */ | ||
case 0xfa: /* Acknowledge */ | ||
leds |= 0x10; | ||
state.Leds |= 0x10; | ||
break; | ||
case 0xe1: /* Extended key special. Only pause uses this */ | ||
flags3 |= 0x01; | ||
state.Flags3 |= 0x01; | ||
break; | ||
case 0xe0: /* Extended key */ | ||
flags3 |= 0x02; | ||
state.Flags3 |= 0x02; | ||
break; | ||
case 0x1d: /* Ctrl Pressed */ | ||
if ((flags3 & 0x01) == 0) { | ||
flags1 |= 0x04; | ||
if ((flags3 & 0x02) != 0) flags3 |= 0x04; | ||
else flags2 |= 0x01; | ||
if ((state.Flags3 & 0x01) == 0) { | ||
state.Flags1 |= 0x04; | ||
if ((state.Flags3 & 0x02) != 0) state.Flags3 |= 0x04; | ||
else state.Flags2 |= 0x01; | ||
} /* else it's part of the pause scancodes */ | ||
break; | ||
case 0x9d: /* Ctrl Released */ | ||
if ((flags3 & 0x01) == 0) { | ||
if ((flags3 & 0x02) != 0) flags3 = (byte)(flags3 & ~0x04); | ||
else flags2 = (byte)(flags2 & ~0x01); | ||
if (!((flags3 & 0x04) != 0 || (flags2 & 0x01) != 0)) flags1 = (byte)(flags1 & ~0x04); | ||
if ((state.Flags3 & 0x01) == 0) { | ||
if ((state.Flags3 & 0x02) != 0) state.Flags3 = (byte)(state.Flags3 & ~0x04); | ||
else state.Flags2 = (byte)(state.Flags2 & ~0x01); | ||
if (!((state.Flags3 & 0x04) != 0 || (state.Flags2 & 0x01) != 0)) state.Flags1 = (byte)(state.Flags1 & ~0x04); | ||
} | ||
break; | ||
case 0x2a: /* Left Shift Pressed */ | ||
flags1 |= 0x02; | ||
state.Flags1 |= 0x02; | ||
break; | ||
case 0xaa: /* Left Shift Released */ | ||
flags1 = (byte)(flags1 & ~0x02); | ||
state.Flags1 = (byte)(state.Flags1 & ~0x02); | ||
break; | ||
case 0x36: /* Right Shift Pressed */ | ||
flags1 |= 0x01; |
if ((flags1 & 0x08) != 0) asciiscan = 0xa600; | ||
else asciiscan = (ushort)((asciiscan & 0xff) | 0xe000); |
Check notice
Code scanning / CodeQL
Missed ternary opportunity Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 days ago
To fix the problem, we should replace the if-else
assignment to asciiscan
on line 454–455 with a single ternary assignment, reducing code duplication and improving clarity. The correct change is to replace:
if ((flags1 & 0x08) != 0) asciiscan = 0xa600;
else asciiscan = (ushort)((asciiscan & 0xff) | 0xe000);
with:
asciiscan = ((flags1 & 0x08) != 0) ? (ushort)0xa600 : (ushort)((asciiscan & 0xff) | 0xe000);
This preserves the original assignment logic without altering any other part of the function. All changes should be within src/Spice86.Core/Emulator/InterruptHandlers/Input/Keyboard/BiosKeyboardInt9Handler.cs
in the given lines.
-
Copy modified line R454
@@ -451,8 +451,7 @@ | ||
if ((flags3 & 0x02) != 0) { | ||
/* extended key (numblock), return and slash need special handling */ | ||
if (scanCode == 0x1c) { /* return */ | ||
if ((flags1 & 0x08) != 0) asciiscan = 0xa600; | ||
else asciiscan = (ushort)((asciiscan & 0xff) | 0xe000); | ||
asciiscan = ((flags1 & 0x08) != 0) ? (ushort)0xa600 : (ushort)((asciiscan & 0xff) | 0xe000); | ||
} else if (scanCode == 0x35) { /* slash */ | ||
if ((flags1 & 0x08) != 0) asciiscan = 0xa400; | ||
else if ((flags1 & 0x04) != 0) asciiscan = 0x9500; |
if (_buffer[idx].IsFromAux) { | ||
_status.IsDataFromAux = true; | ||
} else { | ||
_status.IsDataFromAux = false; | ||
} |
Check notice
Code scanning / CodeQL
Missed ternary opportunity Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
To fix this problem, replace the conditional assignment to _status.IsDataFromAux
with a single-line ternary assignment.
- Original region: lines 312-315.
- Change: Replace the
if ... else ...
block assigning to_status.IsDataFromAux
with a single line:_status.IsDataFromAux = _buffer[idx].IsFromAux ? true : false;
- The ternary is strictly correct (though just assigning
_buffer[idx].IsFromAux
would suffice since it is already a boolean), as per the recommendation ("use ternary operator"), but for maximum clarity and minimal change you can use the ternary as suggested.
No imports or other definitions are needed; only edit the assignment block.
-
Copy modified lines R312-R316
@@ -309,11 +309,11 @@ | ||
|
||
// Transfer one byte of data from buffer to output port | ||
_dataByte = _buffer[idx].Data; | ||
if (_buffer[idx].IsFromAux) { | ||
_status.IsDataFromAux = true; | ||
} else { | ||
_status.IsDataFromAux = false; | ||
} | ||
_status.IsDataFromAux = _buffer[idx].IsFromAux ? true : false; | ||
|
||
|
||
|
||
|
||
_status.IsDataNew = true; | ||
RestartDelayTimer(); | ||
ActivateIrqsIfNeeded(); |
if (isFromKbd && _config.TranslationEnabled) { | ||
_buffer[idx].Data = GetTranslated(value); | ||
} else { | ||
_buffer[idx].Data = value; | ||
} |
Check notice
Code scanning / CodeQL
Missed ternary opportunity Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
To fix the issue, replace the if-else block currently used to assign a value to _buffer[idx].Data
with a single line that uses the ternary operator to assign either GetTranslated(value)
or value
based on the condition isFromKbd && _config.TranslationEnabled
. The change should be made within the method BufferAdd
on lines 337–341 in Intel8042Controller.cs
. No new imports, methods, or definitions are necessary; simply replace the block with the ternary assignment.
-
Copy modified line R337
@@ -334,11 +334,7 @@ | ||
|
||
int idx = (_bufferStartIdx + _bufferNumUsed++) % BufferSize; | ||
|
||
if (isFromKbd && _config.TranslationEnabled) { | ||
_buffer[idx].Data = GetTranslated(value); | ||
} else { | ||
_buffer[idx].Data = value; | ||
} | ||
_buffer[idx].Data = (isFromKbd && _config.TranslationEnabled) ? GetTranslated(value) : value; | ||
_buffer[idx].IsFromAux = isFromAux; | ||
_buffer[idx].IsFromKbd = isFromKbd; | ||
_buffer[idx].SkipDelay = skipDelay || (!isFromAux && !isFromKbd); |
Fixes the most glaring issues in the current keyboard implementation.
The keyboard did not have an internal buffer.
It did not have a delay to ignore key
It did not support being disabled/renabled (which is a keyboard command)
The IRQ1 handler didn't call the keyboard intercept from INT15H first before processing.
The corresponding INT15H interrupt was not implemented.
INT9H did not read the keyboard through the Data port.
We could not read the keyboard through the Data port while an emulated program read it at the same time.
I still have a lot to implement (a proper Keyboard Controller, which also links to the mouse and aux port, typematic stuff, keyboard ScanCode sets, support all keyboard commands, move the scanCode translation elsewhere, support DOS keyboard layouts on top...)
But at least the DOS Console stuff (like in text menus, for example the one in Oh No! More Lemmings, or the DUNE 2 SETUP program), are useable now.
Monkey Island 1 can also properly be paused with the space bar thanks to the delay.
I tested a lot of games without issues (Krondor, Dune, Dune II, Wolf3D, Shangai II, ...)