using System.Runtime.InteropServices; namespace XFESystem.NativeKernel; /// Value-only 32-byte PS/2 report ABI. Deltas are native pixels, positive Y points down. [StructLayout(LayoutKind.Sequential, Size = 32)] public struct NativeInputReport { public uint Kind, KeyCode, Unicode, Modifiers; public int DeltaX, DeltaY; public uint Buttons; public int Wheel; } /// No managed references: this state can live in the zero-library kernel's owned storage. public struct Ps2DecodeState { public ulong HeldLow, HeldHigh, ExtendedHeldLow, ExtendedHeldHigh; public uint Modifiers, MouseButtons; public byte ExtendedPrefix, PauseIndex, MouseIndex, MouseHeader, MouseX, MouseY, MouseDeviceId; } /// Project-owned set-1 and negotiated PS/2 mouse decoder. Hardware I/O and queue ownership are separate. public static unsafe class Ps2Decoder { public const uint KeyDown = 1, KeyUp = 2, Pointer = 3, Lost = 4; public const uint Shift = 1, Control = 2, Alt = 4, Super = 8, CapsLock = 16, NumLock = 32; public const uint PauseKey = 0x245; public static void ResetKeyboard(ref Ps2DecodeState state) { // Retain known held lock keys until their real break; typematic after an overrun must not toggle locks twice. state.HeldLow &= 1UL << 0x3A; state.HeldHigh &= 1UL << (0x45 - 64); state.ExtendedHeldLow = state.ExtendedHeldHigh = 0; state.ExtendedPrefix = state.PauseIndex = 0; state.Modifiers &= CapsLock | NumLock; } public static void ResetMouse(ref Ps2DecodeState state) => state.MouseIndex = 0; // Only select a format after the disabled physical device's F2 reply. // Unrecognized IDs need a hardware reset before the driver enables input. public static bool SetMouseDeviceId(ref Ps2DecodeState state, byte deviceId) { ResetMouse(ref state); state.MouseDeviceId = deviceId is 0 or 3 ? deviceId : (byte)0; return deviceId is 0 or 3; } public static bool DecodeKeyboard(ref Ps2DecodeState state, byte scan, NativeInputReport* output) { if (output == null) return false; *output = default; if (scan is 0x00 or 0xFF) { ResetKeyboard(ref state); MakeLost(ref state, output); return true; } if (state.PauseIndex != 0) { byte expected = state.PauseIndex switch { 1 => 0x1D, 2 => 0x45, 3 => 0xE1, 4 => 0x9D, _ => 0xC5 }; if (scan != expected) { ResetKeyboard(ref state); MakeLost(ref state, output); return true; } if (++state.PauseIndex < 6) return false; state.PauseIndex = 0; output->Kind = KeyDown; output->KeyCode = PauseKey; output->Modifiers = state.Modifiers; return true; } if (scan == 0xE1) { state.ExtendedPrefix = 0; state.PauseIndex = 1; return false; } if (scan == 0xE0) { state.ExtendedPrefix = 1; return false; } // Command acknowledgements are not keys. Driver initialization must consume BAT replies before normal decoding. if (scan is 0xFA or 0xFE) return false; bool extended = state.ExtendedPrefix != 0, released = (scan & 0x80) != 0; state.ExtendedPrefix = 0; uint code = (uint)(scan & 0x7F), key = code | (extended ? 0x100u : 0u); // PrintScreen's E0 2A/E0 AA fake Shift bytes must never change real modifiers. if (extended && code is 0x2A or 0x36) return false; bool previouslyHeld = IsHeld(ref state, code, extended); SetHeld(ref state, code, extended, !released); uint locks = state.Modifiers & (CapsLock | NumLock); if (!extended && !released && !previouslyHeld) { if (code == 0x3A) locks ^= CapsLock; if (code == 0x45) locks ^= NumLock; } state.Modifiers = locks; if (IsHeld(ref state, 0x2A, false) || IsHeld(ref state, 0x36, false)) state.Modifiers |= Shift; if (IsHeld(ref state, 0x1D, false) || IsHeld(ref state, 0x1D, true)) state.Modifiers |= Control; if (IsHeld(ref state, 0x38, false) || IsHeld(ref state, 0x38, true)) state.Modifiers |= Alt; if (IsHeld(ref state, 0x5B, true) || IsHeld(ref state, 0x5C, true)) state.Modifiers |= Super; output->Kind = released ? KeyUp : KeyDown; output->KeyCode = key; output->Modifiers = state.Modifiers; output->Unicode = released ? 0u : Character(key, state.Modifiers); return true; } public static bool DecodeMouse(ref Ps2DecodeState state, byte packetByte, NativeInputReport* output) { if (output == null) return false; *output = default; if (state.MouseIndex == 0) { if ((packetByte & 8) == 0) return false; state.MouseHeader = packetByte; state.MouseIndex = 1; return false; } if (state.MouseIndex == 1) { state.MouseX = packetByte; state.MouseIndex = 2; return false; } if (state.MouseIndex == 2) { state.MouseY = packetByte; if (state.MouseDeviceId == 3) { state.MouseIndex = 3; return false; } } state.MouseIndex = 0; state.MouseButtons = (uint)(state.MouseHeader & 7); output->Buttons = state.MouseButtons; output->Modifiers = state.Modifiers; if ((state.MouseHeader & 0xC0) != 0) { MakeLost(ref state, output); return true; } output->Kind = Pointer; output->DeltaX = state.MouseX - ((state.MouseHeader & 0x10) != 0 ? 256 : 0); output->DeltaY = -(state.MouseY - ((state.MouseHeader & 0x20) != 0 ? 256 : 0)); // IntelliMouse ID3 uses a signed full byte: raw positive is down, // while the shared desktop Pointer contract uses positive wheel up. if (state.MouseDeviceId == 3) output->Wheel = -unchecked((sbyte)packetByte); return true; } private static void MakeLost(ref Ps2DecodeState state, NativeInputReport* output) { *output = default; output->Kind = Lost; output->Modifiers = state.Modifiers; output->Buttons = state.MouseButtons; } private static bool IsHeld(ref Ps2DecodeState state, uint code, bool extended) { ulong bits = extended ? code < 64 ? state.ExtendedHeldLow : state.ExtendedHeldHigh : code < 64 ? state.HeldLow : state.HeldHigh; return (bits & (1UL << (int)(code & 63))) != 0; } private static void SetHeld(ref Ps2DecodeState state, uint code, bool extended, bool held) { ulong bit = 1UL << (int)(code & 63); if (extended) { if (code < 64) state.ExtendedHeldLow = held ? state.ExtendedHeldLow | bit : state.ExtendedHeldLow & ~bit; else state.ExtendedHeldHigh = held ? state.ExtendedHeldHigh | bit : state.ExtendedHeldHigh & ~bit; } else if (code < 64) state.HeldLow = held ? state.HeldLow | bit : state.HeldLow & ~bit; else state.HeldHigh = held ? state.HeldHigh | bit : state.HeldHigh & ~bit; } /// US set-1 layout, Caps XOR Shift for letters. Extended scan codes retain their distinct identity. public static char Character(uint key, uint modifiers) { bool shift = (modifiers & Shift) != 0; if (key == 0x135) return '/'; if ((key & 0x100) != 0 || key >= 0x200) return '\0'; char value = key switch { 0x10 => 'q', 0x11 => 'w', 0x12 => 'e', 0x13 => 'r', 0x14 => 't', 0x15 => 'y', 0x16 => 'u', 0x17 => 'i', 0x18 => 'o', 0x19 => 'p', 0x1E => 'a', 0x1F => 's', 0x20 => 'd', 0x21 => 'f', 0x22 => 'g', 0x23 => 'h', 0x24 => 'j', 0x25 => 'k', 0x26 => 'l', 0x2C => 'z', 0x2D => 'x', 0x2E => 'c', 0x2F => 'v', 0x30 => 'b', 0x31 => 'n', 0x32 => 'm', 0x02 => shift ? '!' : '1', 0x03 => shift ? '@' : '2', 0x04 => shift ? '#' : '3', 0x05 => shift ? '$' : '4', 0x06 => shift ? '%' : '5', 0x07 => shift ? '^' : '6', 0x08 => shift ? '&' : '7', 0x09 => shift ? '*' : '8', 0x0A => shift ? '(' : '9', 0x0B => shift ? ')' : '0', 0x0C => shift ? '_' : '-', 0x0D => shift ? '+' : '=', 0x1A => shift ? '{' : '[', 0x1B => shift ? '}' : ']', 0x27 => shift ? ':' : ';', 0x28 => shift ? '"' : '\'', 0x29 => shift ? '~' : '`', 0x2B => shift ? '|' : '\\', 0x33 => shift ? '<' : ',', 0x34 => shift ? '>' : '.', 0x35 => shift ? '?' : '/', 0x39 => ' ', 0x37 => '*', 0x4A => '-', 0x4E => '+', _ => '\0' }; if (value is >= 'a' and <= 'z') { if (shift != ((modifiers & CapsLock) != 0)) value = (char)(value - 'a' + 'A'); return value; } if (value == '\0' && (modifiers & NumLock) != 0 && !shift) value = key switch { 0x47 => '7', 0x48 => '8', 0x49 => '9', 0x4B => '4', 0x4C => '5', 0x4D => '6', 0x4F => '1', 0x50 => '2', 0x51 => '3', 0x52 => '0', 0x53 => '.', _ => '\0' }; return value; } }