Field report (iPad → Bazzite host, Game Mode): # arrived as \, - as /, ä as '.
That set is not random — they are the US ANSI keys that sit exactly where the German ISO ones do. So the client was right and the host session was resolving key positions with a US keymap.
Why
The key wire is US-positional by design: a client sends the physical key, vk_to_evdev turns it into an evdev code, and the session's keymap picks the character. The standing contract is "host layout == the layout printed on the client's keyboard". Nothing was upholding it, for two independent reasons.
1. Nothing exports XKB_DEFAULT_*.localectl set-x11-keymap de writes /etc/X11/xorg.conf.d/00-keyboard.conf — a file only Xorg opens — and libxkbcommon's fallback chain stops at the env vars, which no session manager sets. Compiling a keymap from empty names on a properly-configured German box therefore yielded evdev/pc105/us, silently.
2. gamescope reads those env vars and then publishes the keymap to nobody. It builds the keymap onto wlserver.keyboard_group, but the seat carries wlserver.wlr.virtual_keyboard_device — a stub whose own comment says it exists "only used to set the keymap" and which never gets one. wlserver_keyboardfocus() rebinds that stub on every focus change, and the real group only reaches the seat from a libinput key event, which a --backend headless session never has.
Verified on the box, not inferred: both gamescope Xwayland servers map evdev 53/40/43 to slash/apostrophe/backslash on a de/nodeadkeys machine, and setting XKB_DEFAULT_LAYOUT=de on gamescope changed nothing.
⚠️setxkbmap -query on an Xwayland server lies — it prints a static _XKB_RULES_NAMES property that always reads evdev/pc105/us, not the live keymap. Probe with DISPLAY=:N xmodmap -pke instead.
What this does
pf_host_config::layout resolves what the box actually recorded — XKB_DEFAULT_*, then xorg.conf.d/*keyboard*.conf, then vconsole's XKBLAYOUT. It deliberately never reads vconsole's KEYMAP=: those are console keymap names whose namespace only coincides with xkb's by accident (uk is xkb gb), and a wrong guess would mistype every key instead of falling back to a visible default.
From there:
the wlroots injector compiles its uploaded keymap from that instead of from empty names — this fixes Sway/Hyprland hosts outright;
all four gamescope launch paths (bare spawn, systemd-run --setenv, both drop-ins) hand the session XKB_DEFAULT_*, behind a +pfhdr8 probe that warns when the binary predates the fix rather than leaving it unexplained;
sync_session_keyboard_layout() covers the case none of that reaches — the autologin session punktfunk attaches to, where no launch-time decision applies — by pointing each gamescope Xwayland at the box's layout on adoption. Everything in Game Mode is an X11 client of those servers, so this is the leg that actually fixes the report. Kill switch: PUNKTFUNK_SESSION_LAYOUT=0.
gamescope patch 0010 sets the keymap on the stub as well, which is what makes the env legs mean anything for Wayland-native clients. Bumps the fork stamp +pfhdr7 → +pfhdr8.
Nothing here changes which physical key does what in a game — WASD stays under the same fingers on every layout.
Verification
10 new unit tests over the resolver (precedence, both file formats, the KEYMAP= exclusion).
pf-inject + pf-host-config: clippy --all-targets -D warningsand those tests on real linux-gnu in the CI image.
pf-vdisplay: check + clippy clean on linux-gnu via scripts/xcheck.sh, confirmed non-vacuous with a planted type error in the new function.
The character mapping itself confirmed on the box with xmodmap -pke before and after applying the layout: slash→minus, apostrophe→adiaeresis, backslash→numbersign.
Caveat for review
The gamescope patch is not build-verified — no gamescope build environment was available, so it is reviewed and git am-clean against the pinned 5fb8dce4 (whole 10-patch series re-applied from a clean checkout), but CI is its first real compile. The Rust side is independent of it: the env legs are inert against an older gamescope, and the Xwayland alignment needs no gamescope change at all.
Field report (iPad → Bazzite host, Game Mode): `#` arrived as `\`, `-` as `/`, `ä` as `'`.
That set is not random — they are the US ANSI keys that sit exactly where the German ISO ones do. So the client was right and the **host session** was resolving key positions with a US keymap.
## Why
The key wire is **US-positional** by design: a client sends the physical key, `vk_to_evdev` turns it into an evdev code, and the *session's* keymap picks the character. The standing contract is "host layout == the layout printed on the client's keyboard". Nothing was upholding it, for two independent reasons.
**1. Nothing exports `XKB_DEFAULT_*`.** `localectl set-x11-keymap de` writes `/etc/X11/xorg.conf.d/00-keyboard.conf` — a file only Xorg opens — and libxkbcommon's fallback chain stops at the env vars, which no session manager sets. Compiling a keymap from empty names on a properly-configured German box therefore yielded `evdev/pc105/us`, silently.
**2. gamescope reads those env vars and then publishes the keymap to nobody.** It builds the keymap onto `wlserver.keyboard_group`, but the seat carries `wlserver.wlr.virtual_keyboard_device` — a stub whose own comment says it exists *"only used to set the keymap"* and which never gets one. `wlserver_keyboardfocus()` rebinds that stub on every focus change, and the real group only reaches the seat from a **libinput** key event, which a `--backend headless` session never has.
Verified on the box, not inferred: both gamescope Xwayland servers map evdev 53/40/43 to `slash`/`apostrophe`/`backslash` on a `de`/`nodeadkeys` machine, and setting `XKB_DEFAULT_LAYOUT=de` on gamescope changed nothing.
> ⚠️ `setxkbmap -query` on an Xwayland server **lies** — it prints a static `_XKB_RULES_NAMES` property that always reads `evdev/pc105/us`, not the live keymap. Probe with `DISPLAY=:N xmodmap -pke` instead.
## What this does
`pf_host_config::layout` resolves what the box actually recorded — `XKB_DEFAULT_*`, then `xorg.conf.d/*keyboard*.conf`, then vconsole's `XKBLAYOUT`. It deliberately never reads vconsole's `KEYMAP=`: those are console keymap names whose namespace only coincides with xkb's by accident (`uk` is xkb `gb`), and a wrong guess would mistype every key instead of falling back to a visible default.
From there:
- the **wlroots injector** compiles its uploaded keymap from that instead of from empty names — this fixes Sway/Hyprland hosts outright;
- all four **gamescope launch paths** (bare spawn, `systemd-run --setenv`, both drop-ins) hand the session `XKB_DEFAULT_*`, behind a `+pfhdr8` probe that warns when the binary predates the fix rather than leaving it unexplained;
- **`sync_session_keyboard_layout()`** covers the case none of that reaches — the autologin session punktfunk *attaches* to, where no launch-time decision applies — by pointing each gamescope Xwayland at the box's layout on adoption. Everything in Game Mode is an X11 client of those servers, so this is the leg that actually fixes the report. Kill switch: `PUNKTFUNK_SESSION_LAYOUT=0`.
**gamescope patch 0010** sets the keymap on the stub as well, which is what makes the env legs mean anything for Wayland-native clients. Bumps the fork stamp `+pfhdr7` → `+pfhdr8`.
Nothing here changes which physical key does what in a game — `WASD` stays under the same fingers on every layout.
## Verification
- 10 new unit tests over the resolver (precedence, both file formats, the `KEYMAP=` exclusion).
- `pf-inject` + `pf-host-config`: `clippy --all-targets -D warnings` **and** those tests on real `linux-gnu` in the CI image.
- `pf-vdisplay`: check + clippy clean on `linux-gnu` via `scripts/xcheck.sh`, confirmed **non-vacuous** with a planted type error in the new function.
- The character mapping itself confirmed on the box with `xmodmap -pke` before and after applying the layout: `slash`→`minus`, `apostrophe`→`adiaeresis`, `backslash`→`numbersign`.
## Caveat for review
The gamescope patch is **not build-verified** — no gamescope build environment was available, so it is reviewed and `git am`-clean against the pinned `5fb8dce4` (whole 10-patch series re-applied from a clean checkout), but CI is its first real compile. The Rust side is independent of it: the env legs are inert against an older gamescope, and the Xwayland alignment needs no gamescope change at all.
Reported from an iPad on a Bazzite host in Game Mode: `#` arrived as `\`, `-` as `/`, `ä` as `'`.
That set is not random — they are the US ANSI keys that sit where the German ISO ones do, which
says the client was right and the host session was resolving positions with a US keymap.
The key wire is US-POSITIONAL by design: a client sends the physical key, `vk_to_evdev` turns it
into an evdev code, and the session's keymap picks the character. So the contract is "host layout
== the layout on the client's keyboard", and nothing was upholding it:
- Nothing exports `XKB_DEFAULT_*`. `localectl set-x11-keymap de` writes
/etc/X11/xorg.conf.d/00-keyboard.conf — a file only Xorg reads — and libxkbcommon's fallback
chain stops at the env vars, which no session manager sets. Compiling from empty names on a
properly-configured German box therefore yielded evdev/pc105/us, silently.
- gamescope reads those env vars and then publishes the keymap to nobody. It builds the keymap
onto `keyboard_group`, but the seat carries `virtual_keyboard_device` — a stub whose own comment
says it exists "only to set the keymap" and which never gets one. `wlserver_keyboardfocus()`
rebinds that stub on every focus change, and the real group only reaches the seat from a libinput
key event, which a `--backend headless` session never has. Verified on the box: both Xwayland
servers map evdev 53/40/43 to slash/apostrophe/backslash on a de/nodeadkeys machine.
`pf_host_config::layout` resolves what the box actually recorded (`XKB_DEFAULT_*`, then
xorg.conf.d, then vconsole's XKBLAYOUT — never vconsole's KEYMAP, whose names are console names and
do not map onto xkb's). From there:
- the wlroots injector compiles its uploaded keymap from it instead of from empty names, which
fixes Sway/Hyprland hosts outright;
- all four gamescope launch paths hand the session `XKB_DEFAULT_*`, gated behind a `+pfhdr8` probe
that warns when the binary predates the fix rather than leaving it unexplained;
- `sync_session_keyboard_layout()` covers the case none of that reaches — the autologin session
punktfunk ATTACHES to, where no launch-time decision applies — by pointing each gamescope
Xwayland at the box's layout on adoption. Everything in Game Mode is an X11 client of those
servers, so this is the leg that fixes the report. `PUNKTFUNK_SESSION_LAYOUT=0` turns it off.
gamescope patch 0010 sets the keymap on the stub as well, which is what makes the env legs mean
anything for Wayland-native clients. It is NOT build-verified (no gamescope build environment
here); the series does apply `git am`-clean at the pinned 5fb8dce4.
Verified: 10 new unit tests over the resolver; pf-inject + pf-host-config clippy `-D warnings` and
those tests on real linux-gnu in the CI image; pf-vdisplay checks and clippies clean on linux-gnu
via scripts/xcheck.sh, confirmed non-vacuous with a planted error. The character mapping itself was
confirmed on the box with `xmodmap -pke` before and after applying the layout.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Field report (iPad → Bazzite host, Game Mode):
#arrived as\,-as/,äas'.That set is not random — they are the US ANSI keys that sit exactly where the German ISO ones do. So the client was right and the host session was resolving key positions with a US keymap.
Why
The key wire is US-positional by design: a client sends the physical key,
vk_to_evdevturns it into an evdev code, and the session's keymap picks the character. The standing contract is "host layout == the layout printed on the client's keyboard". Nothing was upholding it, for two independent reasons.1. Nothing exports
XKB_DEFAULT_*.localectl set-x11-keymap dewrites/etc/X11/xorg.conf.d/00-keyboard.conf— a file only Xorg opens — and libxkbcommon's fallback chain stops at the env vars, which no session manager sets. Compiling a keymap from empty names on a properly-configured German box therefore yieldedevdev/pc105/us, silently.2. gamescope reads those env vars and then publishes the keymap to nobody. It builds the keymap onto
wlserver.keyboard_group, but the seat carrieswlserver.wlr.virtual_keyboard_device— a stub whose own comment says it exists "only used to set the keymap" and which never gets one.wlserver_keyboardfocus()rebinds that stub on every focus change, and the real group only reaches the seat from a libinput key event, which a--backend headlesssession never has.Verified on the box, not inferred: both gamescope Xwayland servers map evdev 53/40/43 to
slash/apostrophe/backslashon ade/nodeadkeysmachine, and settingXKB_DEFAULT_LAYOUT=deon gamescope changed nothing.What this does
pf_host_config::layoutresolves what the box actually recorded —XKB_DEFAULT_*, thenxorg.conf.d/*keyboard*.conf, then vconsole'sXKBLAYOUT. It deliberately never reads vconsole'sKEYMAP=: those are console keymap names whose namespace only coincides with xkb's by accident (ukis xkbgb), and a wrong guess would mistype every key instead of falling back to a visible default.From there:
systemd-run --setenv, both drop-ins) hand the sessionXKB_DEFAULT_*, behind a+pfhdr8probe that warns when the binary predates the fix rather than leaving it unexplained;sync_session_keyboard_layout()covers the case none of that reaches — the autologin session punktfunk attaches to, where no launch-time decision applies — by pointing each gamescope Xwayland at the box's layout on adoption. Everything in Game Mode is an X11 client of those servers, so this is the leg that actually fixes the report. Kill switch:PUNKTFUNK_SESSION_LAYOUT=0.gamescope patch 0010 sets the keymap on the stub as well, which is what makes the env legs mean anything for Wayland-native clients. Bumps the fork stamp
+pfhdr7→+pfhdr8.Nothing here changes which physical key does what in a game —
WASDstays under the same fingers on every layout.Verification
KEYMAP=exclusion).pf-inject+pf-host-config:clippy --all-targets -D warningsand those tests on reallinux-gnuin the CI image.pf-vdisplay: check + clippy clean onlinux-gnuviascripts/xcheck.sh, confirmed non-vacuous with a planted type error in the new function.xmodmap -pkebefore and after applying the layout:slash→minus,apostrophe→adiaeresis,backslash→numbersign.Caveat for review
The gamescope patch is not build-verified — no gamescope build environment was available, so it is reviewed and
git am-clean against the pinned5fb8dce4(whole 10-patch series re-applied from a clean checkout), but CI is its first real compile. The Rust side is independent of it: the env legs are inert against an older gamescope, and the Xwayland alignment needs no gamescope change at all.