BugInfrastructure

RPCEmu Extended: First Modifier Key Combination Ignored on macOS at Startup (Apple Silicon)

On macOS (Apple Silicon), RPCEmu Extended 1.1.16 silently drops the first modifier-key combination (e.g., Shift+key, Ctrl+key) entered after initial application launch.

Rootlock SRE Engine 6 min read
Diagnostic brief

At a Glance

On macOS (Apple Silicon), RPCEmu Extended 1.1.16 silently drops the first modifier-key combination (e.g., Shift+key, Ctrl+key) entered after initial application launch.

Severity Not rated
Confidence Medium
Frequency Unknown
Impact See analysis

Summary

On macOS (Apple Silicon), RPCEmu Extended 1.1.16 silently drops the first modifier-key combination (e.g., Shift+key, Ctrl+key) entered after initial application launch. The modifier key registers correctly on all subsequent presses in the same session, and the problem does not recur after an in-application restart. This is a regression introduced between RPCEmu Extended 0.9.99 and 1.1.13 and likely reflects incorrect modifier-key state initialization at startup on macOS.

Root-Cause Analysis

Confirmed Evidence

  • The failure is strictly first-press-after-cold-start: subsequent modifier combinations work, meaning the modifier key tracking logic eventually reaches a correct state but starts in an incorrect one.
  • The bug is absent after an in-app restart, which re-executes initialization paths that cold start apparently skips or incorrectly sequences.
  • The regression window is between 0.9.99 and 1.1.13, bounding the change to commits introduced in that range.
  • The host is Apple Silicon macOS, where modifier key delivery differs from Intel macOS and from other platforms (see below).

Root-Cause Reasoning

Primary hypothesis — uninitialized modifier-key state at startup:

On macOS, modifier keys (Shift, Control, Option, Command, Caps Lock) are not delivered as standard keyDown/keyUp events. Instead, the OS fires NSFlagsChanged (Cocoa) or a dedicated SDL_KEYDOWN/SDL_KEYUP sequence for modifier keycodes (SDL2/SDL3). If RPCEmu's input layer initializes the modifier-state bitmask to zero (no modifiers held) but does not synthesize or read the current platform modifier state when the window first receives focus, the emulator's internal state and the OS's actual key state are out of sync.

When the user then presses, for example, Shift+A on cold start:

  1. The OS fires a flagsChanged/modifier event to signal Shift is down.
  2. RPCEmu transitions its internal Shift state from uninitialized/falsetrue — but depending on implementation, this transition may not be forwarded to the guest OS as a key-down event if the prior state was already considered "unknown" rather than explicitly "up".
  3. The subsequent A key arrives without an active Shift state in the guest, causing the modifier to be silently lost.
  4. On release, Shift is explicitly marked up, giving a clean baseline.
  5. All subsequent presses work because the state machine now has a valid prior state.

Why restart works: An in-application restart likely re-invokes the keyboard initialization routine, which resets internal state to an explicit up for all modifiers — a defined state rather than an uninitialized one — making the first subsequent press function correctly.

Why Apple Silicon may be more susceptible: macOS on Apple Silicon can deliver window-focus and input events in a different temporal order than x86 macOS, particularly with apps built using Qt or SDL frameworks running under Rosetta 2 or natively. This may expose a pre-existing race between window-focus acquisition and modifier-state polling.

Alternative causes (cannot confirm without source diff or logs):

  • A Qt or SDL version bump between 0.9.99 and 1.1.13 that changed the event delivery contract for QKeyEvent::modifiers() or SDL_GetModState() on macOS.
  • A missed call to SDL_GetModState() or [NSEvent modifierFlags] during the initial focus-in event handler — present in the older codebase, removed or moved in the newer one.
  • A one-off fence/flush issue in the HID event queue on Apple Silicon when the application first becomes key window.

What Cannot Be Confirmed Without Additional Evidence

  • The exact commit or code change that introduced the regression.
  • Whether the framework in use is SDL2, SDL3, or Qt (or a combination), which affects the correct fix location.
  • Whether the issue is limited to hardware modifier keys or also affects software-injected modifier events.

Resolution Steps

These steps apply to both end users working around the bug and developers fixing it.

User Workaround (Immediate)

  1. After RPCEmu Extended cold-starts and the RISC OS desktop is visible, press and release each modifier key individually (Shift, Ctrl, Alt/Option) once without combining them with another key.
  2. This forces the emulator's internal modifier-state machine into an explicit "up" state for each modifier, after which all combinations work normally.
  3. Alternatively, invoke the in-application restart immediately after launch to re-run initialization before attempting any keyboard input.

Developer Fix (Root Cause)

  1. Locate the keyboard/modifier initialization routine introduced or changed between 0.9.99 and 1.1.13 — focus on commits touching keyboardinput, qt_input, sdl_input, or equivalent source files.
  1. Audit the focus-in / window-activation event handler. Ensure that when the application window first becomes active (e.g., QEvent::WindowActivate, SDL_WINDOWEVENT_FOCUS_GAINED, or applicationDidBecomeActive), the code explicitly reads the current platform modifier state and synchronizes the emulator's internal bitmask:
  • SDL2/SDL3: call SDL_GetModState() and apply the result to the internal state.
  • Qt: call QGuiApplication::queryKeyboardModifiers() inside the focus-in handler.
  • Cocoa: read [NSEvent modifierFlags] in windowDidBecomeKey:.
  1. Explicitly initialize all modifier states to "up" before the first event is processed, so any unvisited modifier path starts from a known baseline rather than zero/uninitialized.
  1. Compare behavior with 0.9.99 to identify whether the older code contained an explicit SDL_GetModState() poll on startup that was subsequently removed.
  1. Test on both Intel and Apple Silicon macOS after applying any fix, as the event timing differences mean a fix that passes on Intel may not cover Apple Silicon edge cases.

CLI Commands

Capture macOS system-level key events to confirm whether the OS is delivering modifier events correctly (rules out an OS-level bug):

# Monitor HID key events system-wide for 10 seconds
# Run this in Terminal, then switch to RPCEmu and press Shift once
log stream --predicate 'subsystem == "com.apple.HIToolbox"' --level debug 2>/dev/null &
sleep 10
kill %1

If you have access to the RPCEmu Extended source, identify the regression commit:

# Clone and inspect the git log between the known-good and known-bad tags
git clone https://github.com/andrewtimmins/rpcemu-extended.git
cd rpcemu-extended
git log --oneline v0.9.99..v1.1.13 -- <keyboard-input-source-path>
# Search for modifier-state initialization sites
grep -rn "SDL_GetModState\|modifierFlags\|queryKeyboardModifiers\|KMOD_NONE" src/

Verification

Confirming the Bug Is Present

  1. Cold-start RPCEmu Extended 1.1.16.
  2. Wait for the RISC OS desktop to fully load.
  3. Open a text editor within RISC OS.
  4. Press Shift+A as the very first key input.
  5. Bug present: lowercase a is produced (Shift ignored). Bug absent: uppercase A is produced.

Confirming a Fix Works

After applying a code change or workaround:

  1. Cold-start the application (do not use in-app restart).
  2. Without pressing any key alone first, press Shift+A.
  3. Expected result: A (uppercase) is produced on the first attempt.
  4. Repeat with Ctrl and Option modifier combinations.
  5. Confirm behavior is consistent across five consecutive cold-start cycles.

Rollback Indicator

If a code fix causes modifier keys to fire duplicate events or remain "stuck," revert the change and confirm with the step above. A stuck modifier manifests as every subsequent key appearing modified even without the modifier physically held.

Prevention

  • Regression test in CI: Add an automated UI/input test that simulates a modifier+key event as the first input event after application initialization and asserts the correct key combination is delivered to the guest. Frameworks like xdotool (Linux) or cliclick (macOS) can inject synthetic key events for headless testing.
  • Explicit modifier-state synchronization on focus: Make modifier-state polling on window focus a mandatory step in the initialization checklist, guarded by a platform-specific preprocessor block (#ifdef __APPLE__) if necessary, to avoid regressions when the SDL or Qt version changes.
  • Platform-specific CI runner: Add an Apple Silicon macOS runner to the CI pipeline. The behavior difference between Intel and Apple Silicon for HID event ordering means Intel-only CI will not catch this class of bug.
  • Version-gated testing: When upgrading SDL2/SDL3 or Qt, explicitly test first-modifier-key behavior on macOS as a smoke test before release, given the historical sensitivity of this code path.
  • Changelog note: Document the known affected version range (1.1.13–1.1.16) so users can apply the press-and-release workaround while a fix is in development.
Developer FirstBuilt for engineers solving real problems
Evidence DrivenTechnical claims tied to available evidence
Automation ReadyStructured for CLI, APIs, and workflows
Privacy FocusedNo unnecessary data collection in this article UI
STAY AHEAD OF ISSUES

Get new root-cause analyses in your inbox

Engineering-focused updates. No fake subscriber counts. Unsubscribe anytime.