diff --git a/UIX.RenderApi.OpenGL/Engine/GLInputSystem.cs b/UIX.RenderApi.OpenGL/Engine/GLInputSystem.cs index 1ac3f41..4f91a58 100644 --- a/UIX.RenderApi.OpenGL/Engine/GLInputSystem.cs +++ b/UIX.RenderApi.OpenGL/Engine/GLInputSystem.cs @@ -1,13 +1,22 @@ +using Microsoft.Iris.Input; + namespace Microsoft.Iris.Render.OpenGL { /// - /// Holds the current raw-input callback sink. The window (see ) - /// translates Silk.NET input events and dispatches them here. + /// Holds the current raw-input callback sink and mouse-capture site. The input + /// translator (see ) reads + /// each event and dispatches translated Silk.NET input to it. /// public sealed class GLInputSystem : IInputSystem { public IRawInputCallbacks? Callbacks { get; private set; } + /// + /// The visual that has grabbed the mouse via , + /// or null. When set, mouse events are routed to it regardless of hit-testing. + /// + public IRawInputSite? CaptureSite { get; set; } + public void RegisterRawInputCallbacks(IRawInputCallbacks handlers) => Callbacks = handlers; public void UnregisterRawInputCallbacks() => Callbacks = null; diff --git a/UIX.RenderApi.OpenGL/Engine/GLInputTranslator.cs b/UIX.RenderApi.OpenGL/Engine/GLInputTranslator.cs new file mode 100644 index 0000000..7026330 --- /dev/null +++ b/UIX.RenderApi.OpenGL/Engine/GLInputTranslator.cs @@ -0,0 +1,326 @@ +using System; +using System.Collections.Generic; +using Microsoft.Iris.Input; +using Silk.NET.Input; +using SilkKey = Silk.NET.Input.Key; +using SilkMouseButton = Silk.NET.Input.MouseButton; + +namespace Microsoft.Iris.Render.OpenGL +{ + /// + /// Translates Silk.NET.Input keyboard/mouse events into the render API's raw-input + /// protocol (). Message identifiers match what the + /// UIX InputManager expects: keyboard uses the + /// ordinals, mouse uses Win32 WM_* codes. + /// + public sealed class GLInputTranslator : IDisposable + { + // Win32 mouse messages (the values MouseDevice.OnRawInput switches on). + private const uint WM_MOUSEMOVE = 0x0200; + private const uint WM_LBUTTONDOWN = 0x0201, WM_LBUTTONUP = 0x0202, WM_LBUTTONDBLCLK = 0x0203; + private const uint WM_RBUTTONDOWN = 0x0204, WM_RBUTTONUP = 0x0205, WM_RBUTTONDBLCLK = 0x0206; + private const uint WM_MBUTTONDOWN = 0x0207, WM_MBUTTONUP = 0x0208, WM_MBUTTONDBLCLK = 0x0209; + private const uint WM_MOUSEWHEEL = 0x020A; + private const uint WM_XBUTTONDOWN = 0x020B, WM_XBUTTONUP = 0x020C, WM_XBUTTONDBLCLK = 0x020D; + + private readonly IInputContext m_context; + private readonly GLInputSystem m_input; + private readonly GLRenderWindow m_window; + + // Per-key repeat counters so RawKeyboardData._repCount reflects auto-repeat, + // which the InputManager uses for key coalescing. + private readonly Dictionary m_repeat = new Dictionary(); + + public GLInputTranslator(IInputContext context, GLInputSystem input, GLRenderWindow window) + { + m_context = context; + m_input = input; + m_window = window; + + foreach (IKeyboard keyboard in context.Keyboards) + Hook(keyboard); + foreach (IMouse mouse in context.Mice) + Hook(mouse); + + context.ConnectionChanged += OnConnectionChanged; + } + + private void OnConnectionChanged(IInputDevice device, bool connected) + { + if (!connected) + return; + if (device is IKeyboard keyboard) + Hook(keyboard); + else if (device is IMouse mouse) + Hook(mouse); + } + + private void Hook(IKeyboard keyboard) + { + keyboard.KeyDown += OnKeyDown; + keyboard.KeyUp += OnKeyUp; + keyboard.KeyChar += OnKeyChar; + } + + private void Hook(IMouse mouse) + { + mouse.MouseMove += OnMouseMove; + mouse.MouseDown += OnMouseDown; + mouse.MouseUp += OnMouseUp; + mouse.Scroll += OnScroll; + mouse.DoubleClick += OnDoubleClick; + } + + // ---- Keyboard ------------------------------------------------------------ + private void OnKeyDown(IKeyboard keyboard, SilkKey key, int scanCode) + { + IRawInputCallbacks? cb = m_input.Callbacks; + if (cb == null) + return; + + uint repeat = m_repeat.TryGetValue(key, out uint prev) ? prev + 1 : 1; + m_repeat[key] = repeat; + + bool system = IsAltDown(); + uint message = (uint)(system ? KeyboardMessageId.SysDown : KeyboardMessageId.Down); + var data = new RawKeyboardData(MapKey(key), scanCode, repeat, 0, InputDeviceType.Keyboard); + cb.HandleRawKeyboardInput(message, ComputeModifiers(), ref data); + } + + private void OnKeyUp(IKeyboard keyboard, SilkKey key, int scanCode) + { + m_repeat.Remove(key); + + IRawInputCallbacks? cb = m_input.Callbacks; + if (cb == null) + return; + + bool system = IsAltDown(); + uint message = (uint)(system ? KeyboardMessageId.SysUp : KeyboardMessageId.Up); + var data = new RawKeyboardData(MapKey(key), scanCode, 1, 0, InputDeviceType.Keyboard); + cb.HandleRawKeyboardInput(message, ComputeModifiers(), ref data); + } + + private void OnKeyChar(IKeyboard keyboard, char character) + { + IRawInputCallbacks? cb = m_input.Callbacks; + if (cb == null) + return; + + bool system = IsAltDown(); + uint message = (uint)(system ? KeyboardMessageId.SysChar : KeyboardMessageId.Char); + // Character messages carry the char in _virtualKey (see KeyboardDevice.OnRawKeyCharacter). + var data = new RawKeyboardData((Keys)character, 0, 1, 0, InputDeviceType.Keyboard); + cb.HandleRawKeyboardInput(message, ComputeModifiers(), ref data); + } + + // ---- Mouse --------------------------------------------------------------- + private void OnMouseMove(IMouse mouse, System.Numerics.Vector2 position) + => DispatchMouse(mouse, WM_MOUSEMOVE, MouseButtons.None, 0); + + private void OnMouseDown(IMouse mouse, SilkMouseButton button) + { + (uint message, MouseButtons iris) = button switch + { + SilkMouseButton.Left => (WM_LBUTTONDOWN, MouseButtons.Left), + SilkMouseButton.Right => (WM_RBUTTONDOWN, MouseButtons.Right), + SilkMouseButton.Middle => (WM_MBUTTONDOWN, MouseButtons.Middle), + SilkMouseButton.Button4 => (WM_XBUTTONDOWN, MouseButtons.XButton1), + SilkMouseButton.Button5 => (WM_XBUTTONDOWN, MouseButtons.XButton2), + _ => (0u, MouseButtons.None), + }; + if (message != 0) + DispatchMouse(mouse, message, iris, 0); + } + + private void OnMouseUp(IMouse mouse, SilkMouseButton button) + { + (uint message, MouseButtons iris) = button switch + { + SilkMouseButton.Left => (WM_LBUTTONUP, MouseButtons.Left), + SilkMouseButton.Right => (WM_RBUTTONUP, MouseButtons.Right), + SilkMouseButton.Middle => (WM_MBUTTONUP, MouseButtons.Middle), + SilkMouseButton.Button4 => (WM_XBUTTONUP, MouseButtons.XButton1), + SilkMouseButton.Button5 => (WM_XBUTTONUP, MouseButtons.XButton2), + _ => (0u, MouseButtons.None), + }; + if (message != 0) + DispatchMouse(mouse, message, iris, 0); + } + + private void OnDoubleClick(IMouse mouse, SilkMouseButton button, System.Numerics.Vector2 position) + { + (uint message, MouseButtons iris) = button switch + { + SilkMouseButton.Left => (WM_LBUTTONDBLCLK, MouseButtons.Left), + SilkMouseButton.Right => (WM_RBUTTONDBLCLK, MouseButtons.Right), + SilkMouseButton.Middle => (WM_MBUTTONDBLCLK, MouseButtons.Middle), + SilkMouseButton.Button4 => (WM_XBUTTONDBLCLK, MouseButtons.XButton1), + SilkMouseButton.Button5 => (WM_XBUTTONDBLCLK, MouseButtons.XButton2), + _ => (0u, MouseButtons.None), + }; + if (message != 0) + DispatchMouse(mouse, message, iris, 0, doubleClick: true); + } + + private void OnScroll(IMouse mouse, ScrollWheel wheel) + => DispatchMouse(mouse, WM_MOUSEWHEEL, MouseButtons.None, (int)(wheel.Y * 120f)); + + private void DispatchMouse(IMouse mouse, uint message, MouseButtons button, int wheelDelta, bool doubleClick = false) + { + IRawInputCallbacks? cb = m_input.Callbacks; + if (cb == null) + return; + + System.Numerics.Vector2 pos = mouse.Position; + int clientX = (int)pos.X; + int clientY = (int)pos.Y; + + IVisual? natural = m_window.HitTest(new Silk.NET.Maths.Vector2D(pos.X, pos.Y)); + IVisual? capture = m_input.CaptureSite as IVisual ?? natural; + + var data = new RawMouseData( + visCapture: capture!, + visNatural: natural!, + positionX: clientX, + positionY: clientY, + naturalX: clientX, + naturalY: clientY, + physicalX: clientX, + physicalY: clientY, + screenX: clientX + m_window.Left, + screenY: clientY + m_window.Top, + button: button, + wheelDelta: wheelDelta); + + InputModifiers modifiers = ComputeModifiers(mouse); + if (doubleClick) + modifiers |= InputModifiers.DoubleClick; + + cb.HandleRawMouseInput(message, modifiers, ref data); + } + + // ---- Modifiers ----------------------------------------------------------- + private bool IsAltDown() + { + foreach (IKeyboard k in m_context.Keyboards) + { + if (k.IsKeyPressed(SilkKey.AltLeft) || k.IsKeyPressed(SilkKey.AltRight)) + return true; + } + return false; + } + + private InputModifiers ComputeModifiers(IMouse? mouse = null) + { + InputModifiers m = InputModifiers.None; + foreach (IKeyboard k in m_context.Keyboards) + { + if (k.IsKeyPressed(SilkKey.ControlLeft) || k.IsKeyPressed(SilkKey.ControlRight)) m |= InputModifiers.ControlKey; + if (k.IsKeyPressed(SilkKey.ShiftLeft) || k.IsKeyPressed(SilkKey.ShiftRight)) m |= InputModifiers.ShiftKey; + if (k.IsKeyPressed(SilkKey.AltLeft) || k.IsKeyPressed(SilkKey.AltRight)) m |= InputModifiers.AltKey; + if (k.IsKeyPressed(SilkKey.SuperLeft) || k.IsKeyPressed(SilkKey.SuperRight)) m |= InputModifiers.WindowsKey; + } + + mouse ??= FirstMouse(); + if (mouse != null) + { + if (mouse.IsButtonPressed(SilkMouseButton.Left)) m |= InputModifiers.LeftMouse; + if (mouse.IsButtonPressed(SilkMouseButton.Right)) m |= InputModifiers.RightMouse; + if (mouse.IsButtonPressed(SilkMouseButton.Middle)) m |= InputModifiers.MiddleMouse; + if (mouse.IsButtonPressed(SilkMouseButton.Button4)) m |= InputModifiers.XMouse1; + if (mouse.IsButtonPressed(SilkMouseButton.Button5)) m |= InputModifiers.XMouse2; + } + return m; + } + + private IMouse? FirstMouse() + { + foreach (IMouse mouse in m_context.Mice) + return mouse; + return null; + } + + public void Dispose() + { + m_context.ConnectionChanged -= OnConnectionChanged; + foreach (IKeyboard keyboard in m_context.Keyboards) + { + keyboard.KeyDown -= OnKeyDown; + keyboard.KeyUp -= OnKeyUp; + keyboard.KeyChar -= OnKeyChar; + } + foreach (IMouse mouse in m_context.Mice) + { + mouse.MouseMove -= OnMouseMove; + mouse.MouseDown -= OnMouseDown; + mouse.MouseUp -= OnMouseUp; + mouse.Scroll -= OnScroll; + mouse.DoubleClick -= OnDoubleClick; + } + m_context.Dispose(); + } + + // ---- Key mapping (Silk.NET.Input.Key -> Win32 virtual key codes) --------- + private static Keys MapKey(SilkKey key) => key switch + { + >= SilkKey.A and <= SilkKey.Z => Keys.A + (key - SilkKey.A), + >= SilkKey.Number0 and <= SilkKey.Number9 => Keys.D0 + (key - SilkKey.Number0), + >= SilkKey.Keypad0 and <= SilkKey.Keypad9 => Keys.NumPad0 + (key - SilkKey.Keypad0), + >= SilkKey.F1 and <= SilkKey.F24 => Keys.F1 + (key - SilkKey.F1), + + SilkKey.Space => Keys.Space, + SilkKey.Enter => Keys.Return, + SilkKey.KeypadEnter => Keys.Return, + SilkKey.Escape => Keys.Escape, + SilkKey.Tab => Keys.Tab, + SilkKey.Backspace => Keys.Back, + SilkKey.Insert => Keys.Insert, + SilkKey.Delete => Keys.Delete, + SilkKey.Home => Keys.Home, + SilkKey.End => Keys.End, + SilkKey.PageUp => Keys.PageUp, + SilkKey.PageDown => Keys.PageDown, + SilkKey.Left => Keys.Left, + SilkKey.Right => Keys.Right, + SilkKey.Up => Keys.Up, + SilkKey.Down => Keys.Down, + + SilkKey.CapsLock => Keys.CapsLock, + SilkKey.NumLock => Keys.NumLock, + SilkKey.ScrollLock => Keys.Scroll, + SilkKey.PrintScreen => Keys.PrintScreen, + SilkKey.Pause => Keys.Pause, + SilkKey.Menu => Keys.Apps, + + SilkKey.ShiftLeft => Keys.LShiftKey, + SilkKey.ShiftRight => Keys.RShiftKey, + SilkKey.ControlLeft => Keys.LControlKey, + SilkKey.ControlRight => Keys.RControlKey, + SilkKey.AltLeft => Keys.LMenu, + SilkKey.AltRight => Keys.RMenu, + SilkKey.SuperLeft => Keys.LWin, + SilkKey.SuperRight => Keys.RWin, + + SilkKey.KeypadDivide => Keys.Divide, + SilkKey.KeypadMultiply => Keys.Multiply, + SilkKey.KeypadSubtract => Keys.Subtract, + SilkKey.KeypadAdd => Keys.Add, + SilkKey.KeypadDecimal => Keys.Decimal, + + SilkKey.Semicolon => Keys.OemSemicolon, + SilkKey.Equal => Keys.OemPlus, + SilkKey.Comma => Keys.OemComma, + SilkKey.Minus => Keys.OemMinus, + SilkKey.Period => Keys.OemPeriod, + SilkKey.Slash => Keys.OemQuestion, + SilkKey.GraveAccent => Keys.OemTilde, + SilkKey.LeftBracket => Keys.OemOpenBrackets, + SilkKey.BackSlash => Keys.OemPipe, + SilkKey.RightBracket => Keys.OemCloseBrackets, + SilkKey.Apostrophe => Keys.OemQuotes, + + _ => Keys.None, + }; + } +} diff --git a/UIX.RenderApi.OpenGL/Engine/GLRenderEngine.cs b/UIX.RenderApi.OpenGL/Engine/GLRenderEngine.cs index e99e76b..2efda96 100644 --- a/UIX.RenderApi.OpenGL/Engine/GLRenderEngine.cs +++ b/UIX.RenderApi.OpenGL/Engine/GLRenderEngine.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using Silk.NET.Input; using Silk.NET.Maths; using Silk.NET.OpenGL; using Silk.NET.Windowing; @@ -23,6 +24,8 @@ namespace Microsoft.Iris.Render.OpenGL private GL? m_gl; private SceneRenderer? m_renderer; private GLDisplayManager? m_displayManager; + private IInputContext? m_inputContext; + private GLInputTranslator? m_inputTranslator; private GraphicsRenderingQuality m_quality; private SoundDeviceType m_soundType; @@ -73,6 +76,10 @@ namespace Microsoft.Iris.Render.OpenGL m_displayManager = new GLDisplayManager(m_silkWindow); m_session.GraphicsDevice = new GLGraphicsDevice(m_gl, m_quality, RenderNow); m_session.SoundDevice = new GLSoundDevice(m_soundType); + + m_inputContext = m_silkWindow.CreateInput(); + m_inputTranslator = new GLInputTranslator(m_inputContext, (GLInputSystem)m_session.InputSystem, m_window); + m_window.RaiseLoad(); } @@ -121,6 +128,7 @@ namespace Microsoft.Iris.Render.OpenGL public void Dispose() { + m_inputTranslator?.Dispose(); m_renderer?.Dispose(); m_session.Dispose(); if (!m_silkWindow.IsClosing) diff --git a/UIX.RenderApi.OpenGL/Engine/GLRenderWindow.cs b/UIX.RenderApi.OpenGL/Engine/GLRenderWindow.cs index 218c250..0fecfa7 100644 --- a/UIX.RenderApi.OpenGL/Engine/GLRenderWindow.cs +++ b/UIX.RenderApi.OpenGL/Engine/GLRenderWindow.cs @@ -13,18 +13,24 @@ namespace Microsoft.Iris.Render.OpenGL public sealed class GLRenderWindow : IRenderWindow { private readonly SilkWindow m_window; + private readonly GLRenderSession m_session; private readonly GLVisualContainer m_root; private Size m_initialClientSize = new Size(1024, 768); public GLRenderWindow(SilkWindow window, GLRenderSession session) { m_window = window; + m_session = session; m_root = new GLVisualContainer(session, null!, isRoot: true); m_root.RegisterUsage(this); } internal GLVisualContainer Root => m_root; + /// Frontmost hittable visual under a client-space point, or null. + internal GLVisual? HitTest(Vector2D clientPoint) + => m_root.HitTest(new Vector2(clientPoint.X, clientPoint.Y), Matrix4X4.Identity); + // ---- Geometry ------------------------------------------------------------ public int Left => m_window.Position.X; public int Top => m_window.Position.Y; @@ -125,7 +131,8 @@ namespace Microsoft.Iris.Render.OpenGL public void SetEdgeImages(bool fActiveEdges, ShadowEdgePart[] edges) { /* TODO(stage 3): window shadow edges */ } public void SetWindowOptions(WindowOptions options, bool enable) { /* TODO(stage 3): map to Silk window flags */ } public void SetMouseIdleOptions(Size sizeMouseIdleTolerance, uint nMouseIdleDelay) { } - public void SetCapture(IRawInputSite captureSite, bool state) { } + public void SetCapture(IRawInputSite captureSite, bool state) + => ((GLInputSystem)m_session.InputSystem).CaptureSite = state ? captureSite : null; public void SetDragDropResult(uint nDragOverResult, uint nDragDropResult) { } public void ClientToScreen(ref Point point) diff --git a/UIX.RenderApi.OpenGL/Scene/GLSprite.cs b/UIX.RenderApi.OpenGL/Scene/GLSprite.cs index 8b19501..f7917da 100644 --- a/UIX.RenderApi.OpenGL/Scene/GLSprite.cs +++ b/UIX.RenderApi.OpenGL/Scene/GLSprite.cs @@ -57,5 +57,13 @@ namespace Microsoft.Iris.Render.OpenGL renderer.DrawColoredQuad(matrix, Size.X, Size.Y, c, alpha); } } + + internal override GLVisual? HitTest(Vector2 screenPoint, Matrix4X4 parentMatrix) + { + if (!Visible || (MouseOptions & MouseOptions.Hittable) == 0) + return null; + Matrix4X4 world = LocalMatrix * parentMatrix; + return ContainsPoint(screenPoint, world) ? this : null; + } } } diff --git a/UIX.RenderApi.OpenGL/Scene/GLVisual.cs b/UIX.RenderApi.OpenGL/Scene/GLVisual.cs index 9e308d2..0812d5b 100644 --- a/UIX.RenderApi.OpenGL/Scene/GLVisual.cs +++ b/UIX.RenderApi.OpenGL/Scene/GLVisual.cs @@ -116,5 +116,25 @@ namespace Microsoft.Iris.Render.OpenGL /// Draw this visual (and its subtree) with the accumulated parent transform. internal abstract void Render(SceneRenderer renderer, Matrix4X4 parentMatrix, float inheritedAlpha); + + /// + /// Return the frontmost hittable visual under + /// (client pixel space), or null. is the + /// accumulated transform of this visual's parent. + /// + internal abstract GLVisual? HitTest(Vector2 screenPoint, Matrix4X4 parentMatrix); + + /// Is the client-space point inside this visual's local quad? + private protected bool ContainsPoint(Vector2 screenPoint, Matrix4X4 worldMatrix) + { + if (Size.X <= 0f || Size.Y <= 0f) + return false; + if (!Matrix4X4.Invert(worldMatrix, out Matrix4X4 inverse)) + return false; + // Our world matrix maps local -> screen as (local * world) under the + // renderer's convention, so the inverse maps screen -> local the same way. + Vector3D local = Vector3D.Transform(new Vector3D(screenPoint.X, screenPoint.Y, 0f), inverse); + return local.X >= 0f && local.X <= Size.X && local.Y >= 0f && local.Y <= Size.Y; + } } } diff --git a/UIX.RenderApi.OpenGL/Scene/GLVisualContainer.cs b/UIX.RenderApi.OpenGL/Scene/GLVisualContainer.cs index 33cef07..2151808 100644 --- a/UIX.RenderApi.OpenGL/Scene/GLVisualContainer.cs +++ b/UIX.RenderApi.OpenGL/Scene/GLVisualContainer.cs @@ -81,6 +81,30 @@ namespace Microsoft.Iris.Render.OpenGL child.Render(renderer, matrix, alpha); } + internal override GLVisual? HitTest(Vector2 screenPoint, Matrix4X4 parentMatrix) + { + if (!Visible) + return null; + + Matrix4X4 world = LocalMatrix * parentMatrix; + + // Children draw ascending by layer (back-to-front), so the frontmost hit is + // found by testing in reverse order. + m_children.Sort((a, b) => a.Layer.CompareTo(b.Layer)); + for (int i = m_children.Count - 1; i >= 0; i--) + { + GLVisual? hit = m_children[i].HitTest(screenPoint, world); + if (hit != null) + return hit; + } + + // Otherwise the container itself, if it is hittable and has extent. + if ((MouseOptions & MouseOptions.Hittable) != 0 && ContainsPoint(screenPoint, world)) + return this; + + return null; + } + protected override void DisposeCore() => RemoveAllChildren(); } } diff --git a/UIX.RenderApi.OpenGL/UIX.RenderApi.OpenGL.csproj b/UIX.RenderApi.OpenGL/UIX.RenderApi.OpenGL.csproj index 63eb933..d32f262 100644 --- a/UIX.RenderApi.OpenGL/UIX.RenderApi.OpenGL.csproj +++ b/UIX.RenderApi.OpenGL/UIX.RenderApi.OpenGL.csproj @@ -8,35 +8,20 @@ true enable disable - - - net8.0 - net8.0;net48 - - - - + - - - - + + + + - all runtime; build; native; contentfiles; analyzers diff --git a/logs/UIX.RenderApi.OpenGL/Implementation.md b/logs/UIX.RenderApi.OpenGL/Implementation.md index 88396b8..5881d46 100644 --- a/logs/UIX.RenderApi.OpenGL/Implementation.md +++ b/logs/UIX.RenderApi.OpenGL/Implementation.md @@ -2,6 +2,39 @@ Reverse-chronological log (prepend new entries; never edit older ones). +## 2026-07-25 — Input-event translation (Silk.NET.Input) + +Wired real keyboard/mouse input via `GLInputTranslator`, created by the engine on +window Load from `IWindow.CreateInput()`. It hooks every keyboard/mouse (and new +devices via `ConnectionChanged`) and dispatches to the registered +`IRawInputCallbacks`. + +Message-id conventions were confirmed by reading the UIX consumer, not guessed: +- Keyboard (`KeyboardDevice.OnRawInput`): the `message` is the `KeyboardMessageId` + ordinal — 0 Down, 1 Up, 2 Char, 3 SysDown, 4 SysUp, 5 SysChar. We emit Sys* + variants while Alt is held. Character events put the char in + `RawKeyboardData._virtualKey` (matches `OnRawKeyCharacter`'s `(char)_virtualKey`). +- Mouse (`MouseDevice.OnRawInput`): the `message` is the Win32 `WM_*` code + (0x200 move, 0x201/0x202 L down/up, 0x204/5 R, 0x207/8 M, 0x20A wheel, + 0x20B-D X buttons, dblclk variants 0x203/6/9/20D). Wheel delta is `scroll.Y*120`. + +`InputModifiers` is computed from live Silk key/button state each event. Silk +`Key` → Iris `Keys` (Win32 VK) mapping uses contiguous-range arithmetic for +A–Z / 0–9 / Keypad / F-keys plus an explicit table for the rest; unmapped → None. + +Hit-testing: added `GLVisual.HitTest` (frontmost-first, honoring `Visible` and +`MouseOptions.Hittable`) so `RawMouseData._visNatural` is the visual under the +cursor. `_visCapture` is the `SetCapture` site when set (now stored on +`GLInputSystem.CaptureSite`), else the natural target. Screen<->local uses the +inverse of the visual's world matrix (same row-vector convention as rendering). + +TODOs (still stage-3): HID/AppCommand (media/remote) and drag/drop translation are +not sourced from Silk yet; `_repCount` is a simple per-key press counter; keyboard +`_flags` is 0. + +Validated the same way as before (compile against the prebuilt `UIX.RenderApi.dll` ++ Silk.NET, incl. Silk.NET.Input) — build succeeded. + ## 2026-07-25 — Initial in-process OpenGL renderer ### Goal