From 5b75cd05e674d59b2e2d005eecb2da045bedfb71 Mon Sep 17 00:00:00 2001
From: TomokoKwanXbox <130513966+TomokoKwanXbox@users.noreply.github.com>
Date: Tue, 29 Sep 2026 13:29:19 -0700
Subject: [PATCH] Add opt-in GamepadKeyRouting XAML optional change for gamepad
navigation (#11853)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Fixes
Closes #63117176
https://task.ms/63117176
## PR Type
- [ ] Bugfix
- [x] Feature
- [ ] Code style update (formatting, renaming)
- [ ] Refactoring (no functional changes, no API changes)
- [ ] Build related changes
- [ ] Documentation content changes
- [ ] Other (please describe):
## Description
Adds an opt-in `GamepadKeyRouting` XAML optional change that lets WinUI
3 apps
respond to gamepad input using XAML's existing keyboard navigation,
restoring
behavior that System XAML provided out of the box.
When enabled, `Application.Start()` calls the OS API
`Windows.UI.Input.GamepadKeyRoutingConfiguration.TrySetKeyRoutingEnabled(true)`,
which routes gamepad input into the process as keyboard virtual keys. XY
focus
navigation, Enter/Space to invoke, and Esc to dismiss then work from a
gamepad
with no app-specific code.
Because the OS API requires UniversalApiContract 19 (Windows 11, 24H2) —
newer
than the 17763 downlevel floor this repo targets — the call lives in a
new
light-up translation unit (`GamepadKeyRoutingLightup.cpp`) compiled
without the
PCH and with the contract version undefined, following the existing
`DXamlCoreLightup.cpp` pattern.
### Current Behavior
A WinUI 3 app receives no keyboard-equivalent input from a gamepad.
Gamepad
navigation that worked in System XAML requires apps to implement their
own input
handling. There is no `XamlChangeId` for gamepad routing.
### New Behavior
`Microsoft.UI.Xaml.Settings.XamlChangeId` gains a `GamepadKeyRouting`
value
(`63117108`). It is off by default. Apps opt in either via the project
property:
GamepadKeyRouting
or by calling
`XamlOptionalChanges.EnableChange(XamlChangeId.GamepadKeyRouting)`
before `Application.Start()`.
When enabled, `FrameworkApplicationFactory::StartImpl` enables routing
for the
process. Applied only when the windowing model is `ClassicDesktop` or
`Universal` — i.e. a full WinUI app. The OS setting is process-wide, so
XAML
islands deliberately never reach this call and stay at the OS default.
Apps can
opt back out at runtime via `TrySetKeyRoutingEnabled(false)` after
`Application.Start()`.
Failure is non-fatal at every step. The code gates on `ApiInformation`
(contract present, type present), then `IsSupported()`, then honors the
`TrySetKeyRoutingEnabled` return value. On downlevel OS, unsupported
hardware,
or OS refusal it returns `S_OK` and the app runs normally without
gamepad
navigation.
Also included:
- `Application.Start()` now locks the optional-changes set before
reading it, so
app code cannot alter selected behavior once startup has begun.
- Windows SDK for C++ projects bumped `10.0.22621.755` →
`10.0.26100.9169`
across all `packages*.config`, required for the contract 19 headers.
## Customer Impact
User-facing and additive. Gamepad users get working navigation with no
code which is a gap closed
versus System XAML.
Gracefully degrades: an opted-in app on Windows earlier than 11 24H2, or
on
hardware where the API is unsupported, behaves exactly as it does today.
## Regression Potential
- [ ] Low risk — isolated change, limited scope
- [x] Medium risk — touches shared components or public APIs
- [ ] High risk — architectural or breaking API change
1. **Windows SDK bump** (`10.0.22621.755` → `10.0.26100.9169`)
1. **`Application.Start()` startup path** is shared by every WinUI app.
Mitigating: the new OS call is confined to a separate TU, reached only
when
opted in, and every failure path returns `S_OK`. Test infra enables
`GamepadKeyRouting` by default for test runs, so existing suites
exercise the
enabled path.
## How Has This Been Tested?
- Manually validated end-to-end with WinUI Gallery deployed as an MSIX
with
`GamepadKeyRouting`:
gamepad navigation confirmed working.
- Tested on an older OS without
Windows.UI.Input.GamepadKeyRoutingConfiguration. WinUIGallery app works
as expected without gamepad navigation enabled.
- Built and verified the opposite configuration
(``), confirming the XAML compiler emits
`XamlOptionalChanges.DisableChange(XamlChangeId.GamepadKeyRouting)` into
the
generated `Main()` and that the app runs normally with routing off.
- [x] I have performed a self-review of my own code
- [ ] I have added tests to cover my changes
- [x] Existing tests builds, will run a test pass on the PR
---
.../GamepadKeyRoutingState.cpp | 81 ++++++++
.../GamepadKeyRoutingState.h | 13 ++
.../WinUICppDesktopSampleApp/MainWindow.cpp | 5 +-
.../WinUICppDesktopSampleApp/MainWindow.xaml | 4 +
.../WinUICppDesktopSampleApp.vcxproj | 4 +-
.../WinUICppDesktopSampleAppTests.cs | 24 +++
.../Utilities/APITestBase.cs | 15 ++
docs/design-notes/gamepad-navigation.md | 181 ++++++++++++------
dxaml/test/infra/client/lib/WindowHelper.cpp | 5 +
.../xcp/components/metadata/inc/EnumDefs.g.h | 1 +
.../inc/OptionalChangeState.h | 6 +
.../core/microsoft.ui.xaml.coretypes2.idl | 4 +-
dxaml/xcp/dxaml/lib/DXamlCoreLightup.cpp | 40 ++++
.../lib/FrameworkApplication_Partial.cpp | 12 ++
.../xcp/dxaml/lib/GamepadKeyRoutingLightup.h | 21 ++
.../dxaml/lib/XamlOptionalChanges_Partial.cpp | 2 +
.../Model/Microsoft.UI.Xaml.Settings.cs | 3 +
packages.arm64.config | 2 +-
packages.arm64ec.config | 2 +-
packages.config | 2 +-
packages.x64.config | 2 +-
packages.x86.config | 2 +-
22 files changed, 363 insertions(+), 68 deletions(-)
create mode 100644 Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/GamepadKeyRoutingState.cpp
create mode 100644 Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/GamepadKeyRoutingState.h
create mode 100644 dxaml/xcp/dxaml/lib/GamepadKeyRoutingLightup.h
diff --git a/Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/GamepadKeyRoutingState.cpp b/Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/GamepadKeyRoutingState.cpp
new file mode 100644
index 000000000..72c0d6e0e
--- /dev/null
+++ b/Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/GamepadKeyRoutingState.cpp
@@ -0,0 +1,81 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#include "pch.h"
+#include "GamepadKeyRoutingState.h"
+
+// GamepadKeyRoutingConfiguration ships in UniversalApiContract 19, which this app's C++/WinRT
+// projection does not know about: samples intentionally build against the 17763 contract metadata
+// (see eng\lightup.targets). The Windows SDK ABI headers are not subject to that pin and do
+// declare the type, so this talks to the ABI interface directly. Those headers also introduce a
+// global ::Windows namespace that collides with the using-directives in MainWindow.cpp, which is
+// why this lives in its own translation unit.
+#undef WINDOWS_FOUNDATION_UNIVERSALAPICONTRACT_VERSION
+#include
+#include
+#include
+
+namespace
+{
+ winrt::hstring RuntimeClassName(wchar_t const* name)
+ {
+ return winrt::hstring{ name };
+ }
+}
+
+namespace WinUICppDesktopSampleApp
+{
+ std::wstring GetGamepadKeyRoutingState()
+ {
+ namespace wf_metadata = ABI::Windows::Foundation::Metadata;
+ namespace wui = ABI::Windows::UI::Input;
+
+ auto const apiInformationName = RuntimeClassName(L"Windows.Foundation.Metadata.ApiInformation");
+ winrt::com_ptr apiInformation;
+ if (FAILED(RoGetActivationFactory(
+ static_cast(winrt::get_abi(apiInformationName)),
+ __uuidof(wf_metadata::IApiInformationStatics),
+ apiInformation.put_void())))
+ {
+ return L"Unavailable";
+ }
+
+ auto const contractName = RuntimeClassName(L"Windows.Foundation.UniversalApiContract");
+ boolean isContractPresent = false;
+ if (FAILED(apiInformation->IsApiContractPresentByMajor(
+ static_cast(winrt::get_abi(contractName)),
+ 19,
+ &isContractPresent)) ||
+ !isContractPresent)
+ {
+ return L"Unavailable";
+ }
+
+ auto const configurationName = RuntimeClassName(L"Windows.UI.Input.GamepadKeyRoutingConfiguration");
+ winrt::com_ptr gamepadKeyRouting;
+ if (FAILED(RoGetActivationFactory(
+ static_cast(winrt::get_abi(configurationName)),
+ __uuidof(wui::IGamepadKeyRoutingConfigurationStatics),
+ gamepadKeyRouting.put_void())))
+ {
+ return L"Unavailable";
+ }
+
+ // IsSupported() reports the same OS velocity feature that gates the routing request
+ // itself, so a false here means the machine cannot route gamepad input at all. That is
+ // not a product failure, and the test treats it as inconclusive.
+ boolean isSupported = false;
+ if (FAILED(gamepadKeyRouting->IsSupported(&isSupported)) || !isSupported)
+ {
+ return L"Unavailable";
+ }
+
+ boolean isEnabled = false;
+ if (FAILED(gamepadKeyRouting->get_IsKeyRoutingEnabled(&isEnabled)))
+ {
+ return L"Unavailable";
+ }
+
+ return isEnabled ? L"Enabled" : L"Disabled";
+ }
+}
diff --git a/Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/GamepadKeyRoutingState.h b/Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/GamepadKeyRoutingState.h
new file mode 100644
index 000000000..51d6b2c73
--- /dev/null
+++ b/Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/GamepadKeyRoutingState.h
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#pragma once
+
+#include
+
+namespace WinUICppDesktopSampleApp
+{
+ // Returns the process-wide gamepad key routing state as "Enabled", "Disabled", or
+ // "Unavailable". See GamepadKeyRoutingState.cpp for why this lives in its own file.
+ std::wstring GetGamepadKeyRoutingState();
+}
diff --git a/Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/MainWindow.cpp b/Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/MainWindow.cpp
index d2aefeb04..a9af7e7a1 100644
--- a/Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/MainWindow.cpp
+++ b/Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/MainWindow.cpp
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include "pch.h"
@@ -13,6 +13,7 @@
#include
#include
#include "ShutdownOrderValidation.h"
+#include "GamepadKeyRoutingState.h"
using namespace winrt;
using namespace Microsoft::UI::Xaml;
@@ -36,6 +37,8 @@ namespace winrt::WinUICppDesktopSampleApp::implementation
{
InitializeComponent();
+ textBlockGamepadKeyRouting().Text(::WinUICppDesktopSampleApp::GetGamepadKeyRoutingState());
+
SetBackground(m_currentBackground);
// sets up window chrome with the provided custom title bar
diff --git a/Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/MainWindow.xaml b/Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/MainWindow.xaml
index a9145a8b1..c10aabdca 100644
--- a/Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/MainWindow.xaml
+++ b/Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/MainWindow.xaml
@@ -20,6 +20,10 @@
+
+
+
+
diff --git a/Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp.vcxproj b/Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp.vcxproj
index 227e09ef8..a953dc037 100644
--- a/Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp.vcxproj
+++ b/Samples/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp/WinUICppDesktopSampleApp.vcxproj
@@ -49,7 +49,7 @@
10.0
- _Reserved
+ _Reserved,GamepadKeyRouting
_Reserved
@@ -137,6 +137,7 @@
MarkupWindow.xaml
+
@@ -186,6 +187,7 @@
+
diff --git a/controls/test/MUXControls.Test/ScenarioAppTests/WinUICppDesktopSampleAppTests.cs b/controls/test/MUXControls.Test/ScenarioAppTests/WinUICppDesktopSampleAppTests.cs
index 0daea7b0a..7c614d112 100644
--- a/controls/test/MUXControls.Test/ScenarioAppTests/WinUICppDesktopSampleAppTests.cs
+++ b/controls/test/MUXControls.Test/ScenarioAppTests/WinUICppDesktopSampleAppTests.cs
@@ -90,6 +90,30 @@ namespace Microsoft.UI.Xaml.Tests.MUXControls.InteractionTests
WinUISampleAppTestsUtils.SelectCheckbox("checkBox");
}
+ [TestMethod]
+ [Description("Validate that enabling the GamepadKeyRouting XamlOptionalChange makes the framework turn on gamepad key routing in the OS.")]
+ public void GamepadKeyRoutingIsEnabledAtStartupTest()
+ {
+ // WinUICppDesktopSampleApp sets ...GamepadKeyRouting,
+ // so Application::Start() should have called the OS gamepad key routing API before the
+ // main window was created. The app sampled the OS state at that point; here we read back
+ // what it saw. This guards against codeflow or merge issues silently dropping the call.
+ var textBlockAsUIObject = FindElement.ByName("textBlockGamepadKeyRouting");
+ Verify.IsNotNull(textBlockAsUIObject);
+ string state = new TextBlock(textBlockAsUIObject).DocumentText;
+ Log.Comment($"textBlockGamepadKeyRouting={state}");
+
+ if (state == "Unavailable")
+ {
+ // The OS on this machine cannot route gamepad input, so there is nothing for the
+ // framework to have enabled and there is nothing to verify.
+ Log.Warning("Gamepad key routing is not supported on this OS - skipping verification.");
+ return;
+ }
+
+ Verify.AreEqual("Enabled", state);
+ }
+
[TestMethod]
[Description("Basic tests of a few MUX controls: TextBlock, CheckBox, Button, ToggleButton, Flyout.")]
[TestProperty("Ignore", "True")] // http://task.ms/45771651
diff --git a/controls/test/MUXControlsTestApp/Utilities/APITestBase.cs b/controls/test/MUXControlsTestApp/Utilities/APITestBase.cs
index 367b6cc28..cae8df552 100644
--- a/controls/test/MUXControlsTestApp/Utilities/APITestBase.cs
+++ b/controls/test/MUXControlsTestApp/Utilities/APITestBase.cs
@@ -64,6 +64,7 @@ namespace MUXControlsTestApp.Utilities
Verify.IsTrue(XamlOptionalChanges.EnableChange(XamlChangeId.OptimizeApplyStyles));
Verify.IsTrue(XamlOptionalChanges.EnableChange(XamlChangeId.DefaultStyleOptimizations));
Verify.IsTrue(XamlOptionalChanges.EnableChange(XamlChangeId.DeferContextFlyoutInit));
+ Verify.IsTrue(XamlOptionalChanges.EnableChange(XamlChangeId.GamepadKeyRouting));
}
public void UpdateXamlOptionalChanges()
@@ -170,9 +171,23 @@ namespace MUXControlsTestApp.Utilities
{
changeId = XamlChangeId.DeferContextFlyoutInit;
}
+ else if (string.Equals(name, "GamepadKeyRouting", StringComparison.OrdinalIgnoreCase))
+ {
+ changeId = XamlChangeId.GamepadKeyRouting;
+ }
Verify.AreNotEqual(changeId, XamlChangeId._Reserved, "Unknown XamlChangeId: " + name);
+ if (changeId == XamlChangeId.GamepadKeyRouting && !enabled)
+ {
+ // Gamepad key routing is applied to the process by Application.Start(), which has
+ // already run by the time test setup reads this property. Clearing the optional
+ // change here only updates the bit, leaving the process still routing gamepad input
+ // as keys, so the test would silently run against the wrong configuration.
+ Verify.Fail("XamlOptionalChanges test override cannot disable GamepadKeyRouting: the " +
+ "process-wide routing setting is already applied by Application.Start().");
+ }
+
xamlOptionalChanges.Add(changeId, enabled);
pos = sepPos + 1;
}
diff --git a/docs/design-notes/gamepad-navigation.md b/docs/design-notes/gamepad-navigation.md
index f0801819f..9e0ce7585 100644
--- a/docs/design-notes/gamepad-navigation.md
+++ b/docs/design-notes/gamepad-navigation.md
@@ -13,7 +13,12 @@ In System XAML (UWP XAML), this gamepad-driven navigation was on by default —
for free without any opt-in. WinUI 3 XAML does not turn it on automatically, so today the
same app loses gamepad navigation unless it (or its host) explicitly enables it. We consider
that gap a regression from System XAML, and this spec describes closing it by having the
-WinUI framework enable gamepad key routing on by default, the way System XAML did.
+WinUI framework enable gamepad key routing during XAML startup, the way System XAML did.
+
+Because this changes existing behavior, the framework's enable is gated behind a
+**XAML optional change**, `XamlChangeId.GamepadKeyRouting`. An app opts in explicitly today;
+the change becomes the default in a later release. See
+[Opting in with XamlOptionalChanges](#opting-in-with-xamloptionalchanges).
Whether gamepad input is delivered to an app as virtual keys is controlled by an existing
operating-system API, `Windows.UI.Input.GamepadKeyRoutingConfiguration`. This is a
@@ -21,8 +26,9 @@ process-wide setting: when key routing is enabled, gamepad input arrives as virt
for the whole process; when it is disabled, it does not.
Today an app (or host) has to turn this on itself. This spec describes enabling it inside
-the WinUI framework, so that a full WinUI app gets gamepad-driven navigation on by default,
-while still allowing the app to opt out.
+the WinUI framework, so that a full WinUI app gets gamepad-driven navigation without writing
+input code — initially by enabling the `GamepadKeyRouting` optional change, and by default in
+a later release — while still allowing the app to opt out.
The framework enables key routing only for a **full WinUI app** — one that starts XAML by
calling `Application.Start()`. A process that merely embeds WinUI as a **XAML Island** is
@@ -30,15 +36,43 @@ deliberately left alone: the framework does not flip the process-wide switch for
host's non-XAML input code is unaffected. A host that wants routing can still enable it
itself.
-This spec does not introduce a new API. `GamepadKeyRoutingConfiguration` already ships
+This spec introduces no new *runtime class*. `GamepadKeyRoutingConfiguration` already ships
in the OS (`Windows.Foundation.UniversalApiContract`, version 19). The work here is a
-framework-internal call to that existing API at the right point in XAML startup, plus
-guidance for how an app opts out. The API surface is reproduced in
-[API Details](#api-details) for reference only.
+framework-internal call to that existing API at the right point in XAML startup, gated by a
+new `XamlChangeId` enum value, plus guidance for how an app opts in and out. The surfaces are
+listed in [API Details](#api-details).
# Conceptual pages (How To)
-## Gamepad navigation is on by default
+## Opting in with XamlOptionalChanges
+
+Enabling gamepad key routing changes how an existing app receives input, so it is delivered
+as a **XAML optional change** rather than switched on unconditionally. The change id is
+`XamlChangeId.GamepadKeyRouting`.
+
+An app opts in with the `EnabledXamlOptionalChanges` MSBuild property, which makes the XAML
+compiler emit the enable call into your generated `App` entry point, before
+`Application.Start()` runs:
+
+```xml
+
+ GamepadKeyRouting
+
+```
+
+Optional changes are locked when XAML initializes, so enabling the change after
+`Application.Start()` is too late — see
+[Where the framework makes the call](#where-the-framework-makes-the-call).
+
+Planned rollout:
+
+| Release | State |
+| --- | --- |
+| 2.x | Available, disabled by default (explicit opt-in as above) |
+| 3.x | Enabled by default; apps can still opt out |
+| 4.x | Always enabled; the optional change is deprecated |
+
+## Gamepad navigation once the change is enabled
When your full WinUI app starts XAML — which it does by calling `Application.Start()` from
its generated entry point — the framework enables gamepad key routing for the process.
@@ -47,7 +81,7 @@ keys) is delivered to your app as keyboard virtual keys. This means WinUI's norm
and focus behaviors — XY focus navigation, `Enter`/`Space` to invoke, `Esc` to dismiss, and
similar — respond to the gamepad automatically, with no additional code in your app.
-If your app does nothing special, this is the recommended experience: you get gamepad
+If your app does nothing further, this is the recommended experience: you get gamepad
navigation for free, and if your app has no interest in gamepad input it can simply ignore
those virtual keys.
@@ -87,9 +121,9 @@ setting. So the framework has to be deliberate about *which processes* it turns
It enables routing only from the **full-app entry point**, `Application.Start()`. A normal
WinUI app reaches that method from its generated entry point, owns its process, and gets
-gamepad navigation on by default — process-wide is exactly what it wants.
-A process that embeds WinUI only as a **XAML Island** never calls `Application.Start()`, so
-the framework does not auto-enable routing for it.
+gamepad navigation once the optional change is enabled — process-wide is exactly what it
+wants. A process that embeds WinUI only as a **XAML Island** never calls `Application.Start()`,
+so the framework does not auto-enable routing for it, regardless of the optional change.
One residual point worth stating plainly: within a full WinUI app the setting is still
process-wide, so if that app *also* hosts non-XAML UI in the same process, that UI's input
@@ -108,17 +142,18 @@ The framework makes the call once per process, from the full-app entry point
```cpp
// Inside Application.Start() (FrameworkApplicationFactory::StartImpl), on the branch that
// starts a full WinUI app. Island hosts don't reach here, so their process is left alone.
-// IsApiContractPresent checks the OS actually has the API (it ships in a newer contract than
-// WinUI's minimum OS); IsSupported checks the feature is usable on this device.
-if (Windows::Foundation::Metadata::ApiInformation::IsApiContractPresent(
- L"Windows.Foundation.UniversalApiContract", 19) &&
- Windows::UI::Input::GamepadKeyRoutingConfiguration::IsSupported())
+// The optional change must have been enabled before Application.Start(); the contract check
+// confirms the OS actually has the API, which ships in a newer contract than WinUI's minimum OS.
+if (OptionalChangeState::IsGamepadKeyRoutingEnabled())
{
- Windows::UI::Input::GamepadKeyRoutingConfiguration::TrySetKeyRoutingEnabled(true);
+ if (ApiInformation::IsApiContractPresentByMajor(L"Windows.Foundation.UniversalApiContract", 19))
+ {
+ GamepadKeyRoutingConfiguration::TrySetKeyRoutingEnabled(true);
+ }
}
```
-Your app does not need to do anything to get this behavior.
+Your app does not need to do anything beyond enabling the optional change.
## Opting out from your app
@@ -135,9 +170,8 @@ public partial class App : Application
// Opt out of gamepad-to-key routing; the framework enabled it during XAML init.
// IsApiContractPresent guards OS versions without the API (the type ships in a newer
- // contract than WinUI's minimum OS); IsSupported guards devices where it's unusable.
- if (Windows.Foundation.Metadata.ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 19) &&
- Windows.UI.Input.GamepadKeyRoutingConfiguration.IsSupported())
+ // contract than WinUI's minimum OS).
+ if (Windows.Foundation.Metadata.ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 19))
{
Windows.UI.Input.GamepadKeyRoutingConfiguration.TrySetKeyRoutingEnabled(false);
}
@@ -160,10 +194,9 @@ App::App()
{
// Opt out of gamepad-to-key routing; the framework enabled it during XAML init.
// IsApiContractPresent guards OS versions without the API (the type ships in a newer
- // contract than WinUI's minimum OS); IsSupported guards devices where it's unusable.
+ // contract than WinUI's minimum OS).
if (winrt::Windows::Foundation::Metadata::ApiInformation::IsApiContractPresent(
- L"Windows.Foundation.UniversalApiContract", 19) &&
- winrt::Windows::UI::Input::GamepadKeyRoutingConfiguration::IsSupported())
+ L"Windows.Foundation.UniversalApiContract", 19))
{
winrt::Windows::UI::Input::GamepadKeyRoutingConfiguration::TrySetKeyRoutingEnabled(false);
}
@@ -172,13 +205,30 @@ App::App()
# API Pages
-_Spec note: This feature introduces no new WinUI API. It calls the existing OS API
-[`Windows.UI.Input.GamepadKeyRoutingConfiguration`](https://learn.microsoft.com/uwp/api/windows.ui.input.gamepadkeyroutingconfiguration).
-The existing surface is listed in [API Details](#api-details) for reviewer convenience._
+_Spec note: This feature introduces no new WinUI runtime class. It adds one value to the
+existing `Microsoft.UI.Xaml.XamlChangeId` enum, and calls the existing OS API
+[`Windows.UI.Input.GamepadKeyRoutingConfiguration`](https://learn.microsoft.com/uwp/api/windows.ui.input.gamepadkeyroutingconfiguration)._
# API Details
```c# (but really MIDL3)
+namespace Microsoft.UI.Xaml
+{
+ enum XamlChangeId
+ {
+ _Reserved,
+ IconNoGridOptimization = 61276805,
+ OptimizeApplyStyles = 61697456,
+ DefaultStyleOptimizations = 60995620,
+ DeferContextFlyoutInit = 61098986,
+
+ // New in this work. Experimental until it completes API review.
+ [feature(Feature_ExperimentalApi)]
+ [contract(Microsoft.UI.Xaml.WinUIContract, 12)]
+ GamepadKeyRouting = 63117108,
+ };
+}
+
namespace Windows.UI.Input
{
// Existing OS API (Windows.Foundation.UniversalApiContract, version 19).
@@ -206,8 +256,9 @@ namespace Windows.UI.Input
The enable is placed at the **full-app entry point**: `Application.Start()`, whose
implementation is `FrameworkApplicationFactory::StartImpl` in
`/dxaml/xcp/dxaml/lib/FrameworkApplication_Partial.cpp`. `StartImpl` reads the app's
-windowing model and dispatches to `StartDesktop()` (WinUI Desktop) or `StartUWP()` (WinUI
-UWP). A full WinUI app reaches this method from its generated entry point; a process that
+windowing model and dispatches to `StartDesktop()` (WinUI Desktop) or `StartUWP()` (retained
+only for tests that have not been ported; WinUI does not support the UWP windowing model).
+A full WinUI app reaches this method from its generated entry point; a process that
only embeds WinUI as a XAML Island never does. Because the enable lives here, only full
WinUI apps turn routing on — anything that starts XAML without going through
`Application.Start()` leaves the process-wide setting untouched.
@@ -229,25 +280,18 @@ _Check_return_ HRESULT FrameworkApplicationFactory::StartImpl(_In_opt_ xaml::IAp
IFC_RETURN(E_FAIL);
}
- if (policy == AppPolicyWindowingModel_ClassicDesktop ||
- policy == AppPolicyWindowingModel_Universal)
- {
- // Full WinUI app: enable gamepad-to-key routing for the process. Island hosts never
- // reach here (they start via WindowsXamlManager.InitializeForCurrentThread()), so the
- // host process is left untouched.
- // IsApiContractPresent: the OS has the type (it ships in UniversalApiContract 19,
- // newer than WinUI's minimum OS, so on downlevel OS the type is absent and even
- // calling IsSupported() would throw). IsSupported: usable on this device.
- if (winrt::Windows::Foundation::Metadata::ApiInformation::IsApiContractPresent(
- L"Windows.Foundation.UniversalApiContract", 19) &&
- winrt::Windows::UI::Input::GamepadKeyRoutingConfiguration::IsSupported())
- {
- winrt::Windows::UI::Input::GamepadKeyRoutingConfiguration::TrySetKeyRoutingEnabled(true);
- }
- }
-
if (policy == AppPolicyWindowingModel_ClassicDesktop)
{
+ if (OptionalChangeState::IsGamepadKeyRoutingEnabled())
+ {
+ // Full WinUI app: enable gamepad-to-key routing for the process, so XAML's
+ // keyboard navigation responds to a gamepad the way it did in System XAML.
+ // Island hosts never reach here (they start via
+ // WindowsXamlManager.InitializeForCurrentThread()), so the host process is
+ // left untouched.
+ VERIFYHR(EnableGamepadKeyRouting());
+ }
+
return FrameworkApplication::StartDesktop();
}
else if (policy == AppPolicyWindowingModel_Universal)
@@ -259,25 +303,42 @@ _Check_return_ HRESULT FrameworkApplicationFactory::StartImpl(_In_opt_ xaml::IAp
}
```
-`ApiInformation::IsApiContractPresent(..., 19)` guards OS versions that predate the API
-(where the type isn't registered and calling any member — including `IsSupported()` — would
-throw), and `IsSupported()` guards devices where the feature exists but is unusable.
+## Why the call lives in a light-up translation unit
-`FrameworkApplication_Partial.cpp` will need `#include ` and
-`#include `.
+`GamepadKeyRoutingConfiguration` requires `UniversalApiContract` 19, but WinUI normally
+compiles against contract 7 (Windows build 17763, its downlevel limit), so the type is not
+even declared in a normal translation unit. `EnableGamepadKeyRouting()` therefore lives in
+`DXamlCoreLightup.cpp`, which is compiled without the PCH and with
+`WINDOWS_FOUNDATION_UNIVERSALAPICONTRACT_VERSION` undefined so the SDK headers expose their
+latest contract. It is declared in `GamepadKeyRoutingLightup.h`, which `StartImpl` includes.
+A `static_assert` in that file fails the build if the SDK in use predates contract 19.
+
+At run time the OS may still be older than the SDK, so the implementation checks
+`ApiInformation::IsApiContractPresentByMajor("Windows.Foundation.UniversalApiContract", 19)`
+before activating the type. It does **not** call `IsSupported()` first:
+`TrySetKeyRoutingEnabled` always returns `S_OK`, reporting refusal through its out-parameter,
+and internally gates on the same feature state that `IsSupported()` reports. A refusal is not
+treated as an error — gamepad navigation is an enhancement, and the app is fully usable
+without it.
## Startup ordering (why app opt-out works)
On the desktop path the sequence is:
-1. `Application.Start()` → `StartImpl` — the framework's `TrySetKeyRoutingEnabled(true)` runs
- here, before dispatching to `StartDesktop()`.
-2. `StartDesktop()` brings up the framework.
-3. Construct the app's `Application` object (`App` ctor).
-4. `OnLaunched` → app creates its first window.
-5. Run the message loop.
+1. The generated entry point calls `XamlOptionalChanges.EnableChange(GamepadKeyRouting)` (if
+ the app opted in) — this must happen before the next step.
+2. `Application.Start()` → `StartImpl` — if the optional change is enabled, the framework's
+ `TrySetKeyRoutingEnabled(true)` runs here, before dispatching to `StartDesktop()`.
+3. `StartDesktop()` brings up the framework, which locks optional changes during
+ `DXamlCore::Initialize()`.
+4. Construct the app's `Application` object (`App` ctor).
+5. `OnLaunched` → app creates its first window.
+6. Run the message loop.
-The framework's enable (step 1) runs before any app code (step 3 onward) and never runs
+The framework's enable (step 2) runs before any app code (step 4 onward) and never runs
again. So to turn gamepad navigation off, an app just calls the setter with `false` from its
`App` constructor or `OnLaunched` — that later write is the last one and sticks for the
-process.
+process. Note that by step 4 the optional changes are already locked, so opting out at that
+point means calling `GamepadKeyRoutingConfiguration.TrySetKeyRoutingEnabled(false)` directly;
+calling `XamlOptionalChanges.DisableChange()` would both fail and, even if it succeeded, have
+no effect on the OS setting that was already applied.
diff --git a/dxaml/test/infra/client/lib/WindowHelper.cpp b/dxaml/test/infra/client/lib/WindowHelper.cpp
index e61b2599d..03dd5a06b 100644
--- a/dxaml/test/infra/client/lib/WindowHelper.cpp
+++ b/dxaml/test/infra/client/lib/WindowHelper.cpp
@@ -2117,6 +2117,10 @@ std::vector> GetXamlOptionalChanges
{
changeId = xaml_settings::XamlChangeId_DeferContextFlyoutInit;
}
+ else if (_wcsicmp(name.c_str(), L"GamepadKeyRouting") == 0)
+ {
+ changeId = xaml_settings::XamlChangeId_GamepadKeyRouting;
+ }
if (changeId == xaml_settings::XamlChangeId__Reserved)
{
@@ -2197,6 +2201,7 @@ void WindowHelper::InitializeXamlCore(_In_ xaml_markup::IXamlMetadataProvider* c
optionalChangesStatics->EnableChange(xaml_settings::XamlChangeId_OptimizeApplyStyles, &mutated);
optionalChangesStatics->EnableChange(xaml_settings::XamlChangeId_DefaultStyleOptimizations, &mutated);
optionalChangesStatics->EnableChange(xaml_settings::XamlChangeId_DeferContextFlyoutInit, &mutated);
+ optionalChangesStatics->EnableChange(xaml_settings::XamlChangeId_GamepadKeyRouting, &mutated);
// Apply per-test overrides from XamlOptionalChanges test data.
for (const auto& [changeId, enabled] : changeOverrides)
diff --git a/dxaml/xcp/components/metadata/inc/EnumDefs.g.h b/dxaml/xcp/components/metadata/inc/EnumDefs.g.h
index 4307689a6..bf02f16a5 100644
--- a/dxaml/xcp/components/metadata/inc/EnumDefs.g.h
+++ b/dxaml/xcp/components/metadata/inc/EnumDefs.g.h
@@ -2212,6 +2212,7 @@ namespace DirectUI
OptimizeApplyStyles = 61697456,
DefaultStyleOptimizations = 60995620,
DeferContextFlyoutInit = 61098986,
+ GamepadKeyRouting = 63117108,
};
DEFINE_ENUM_FLAG_OPERATORS(XamlChangeId);
diff --git a/dxaml/xcp/components/runtimeEnabledFeatures/inc/OptionalChangeState.h b/dxaml/xcp/components/runtimeEnabledFeatures/inc/OptionalChangeState.h
index 767cf05ee..e44466256 100644
--- a/dxaml/xcp/components/runtimeEnabledFeatures/inc/OptionalChangeState.h
+++ b/dxaml/xcp/components/runtimeEnabledFeatures/inc/OptionalChangeState.h
@@ -28,6 +28,7 @@ namespace OptionalChangeState
constexpr int BitIndex_OptimizeApplyStyles = 1;
constexpr int BitIndex_DefaultStyleOptimizations = 2;
constexpr int BitIndex_DeferContextFlyoutInit = 3;
+ constexpr int BitIndex_GamepadKeyRouting = 4;
inline bool IsOptionalChangeEnabled(int bitIndex)
{
@@ -54,4 +55,9 @@ namespace OptionalChangeState
{
return IsOptionalChangeEnabled(BitIndex_DeferContextFlyoutInit) || IsPerfOptInEnabled();
}
+
+ inline bool IsGamepadKeyRoutingEnabled()
+ {
+ return IsOptionalChangeEnabled(BitIndex_GamepadKeyRouting);
+ }
}
diff --git a/dxaml/xcp/dxaml/idl/winrt/core/microsoft.ui.xaml.coretypes2.idl b/dxaml/xcp/dxaml/idl/winrt/core/microsoft.ui.xaml.coretypes2.idl
index 9e115db6a..91751262b 100644
--- a/dxaml/xcp/dxaml/idl/winrt/core/microsoft.ui.xaml.coretypes2.idl
+++ b/dxaml/xcp/dxaml/idl/winrt/core/microsoft.ui.xaml.coretypes2.idl
@@ -520,6 +520,9 @@ namespace Microsoft.UI.Xaml.Settings
OptimizeApplyStyles = 61697456,
DefaultStyleOptimizations = 60995620,
DeferContextFlyoutInit = 61098986,
+ [feature(Feature_ExperimentalApi)]
+ [contract(Microsoft.UI.Xaml.WinUIContract, 12)]
+ GamepadKeyRouting = 63117108,
};
}
@@ -2364,4 +2367,3 @@ namespace Microsoft.UI.Xaml.Settings
};
}
-
diff --git a/dxaml/xcp/dxaml/lib/DXamlCoreLightup.cpp b/dxaml/xcp/dxaml/lib/DXamlCoreLightup.cpp
index 813cac94f..1e1d8b7b4 100644
--- a/dxaml/xcp/dxaml/lib/DXamlCoreLightup.cpp
+++ b/dxaml/xcp/dxaml/lib/DXamlCoreLightup.cpp
@@ -12,6 +12,9 @@
#include "DXamlCore.h"
#include
#include "Callback.h"
+#include "GamepadKeyRoutingLightup.h"
+#include
+#include
using namespace WRLHelper;
using namespace DirectUI;
@@ -131,3 +134,40 @@ void DXamlCore::RemoveAnimationsEnabledChangedHandler()
m_animationsEnabledChangedToken.value = 0;
}
}
+
+// GamepadKeyRoutingConfiguration requires UniversalApiContract 19 (Windows 11, version 24H2).
+static_assert(
+ WINDOWS_FOUNDATION_UNIVERSALAPICONTRACT_VERSION >= 0x130000,
+ "The Windows SDK in use predates UniversalApiContract 19 and does not declare "
+ "Windows.UI.Input.GamepadKeyRoutingConfiguration. Update the SDK version this repo builds against.");
+
+_Check_return_ HRESULT DirectUI::EnableGamepadKeyRouting()
+{
+ ctl::ComPtr apiInformationStatics;
+ IFC_RETURN(ctl::GetActivationFactory(
+ wrl_wrappers::HStringReference(RuntimeClass_Windows_Foundation_Metadata_ApiInformation).Get(),
+ &apiInformationStatics));
+
+ boolean isContractPresent = false;
+ IFC_RETURN(apiInformationStatics->IsApiContractPresentByMajor(
+ wrl_wrappers::HStringReference(L"Windows.Foundation.UniversalApiContract").Get(),
+ 19,
+ &isContractPresent));
+
+ if (!isContractPresent)
+ {
+ return S_OK;
+ }
+
+ ctl::ComPtr gamepadKeyRoutingStatics;
+ IFC_RETURN(ctl::GetActivationFactory(
+ wrl_wrappers::HStringReference(RuntimeClass_Windows_UI_Input_GamepadKeyRoutingConfiguration).Get(),
+ &gamepadKeyRoutingStatics));
+
+ // A refusal is not an error: gamepad navigation is an enhancement, and the app remains fully
+ // usable without it.
+ boolean wasApplied = false;
+ IFC_RETURN(gamepadKeyRoutingStatics->TrySetKeyRoutingEnabled(true, &wasApplied));
+
+ return S_OK;
+}
diff --git a/dxaml/xcp/dxaml/lib/FrameworkApplication_Partial.cpp b/dxaml/xcp/dxaml/lib/FrameworkApplication_Partial.cpp
index d92beb2ae..b58e2a9ad 100644
--- a/dxaml/xcp/dxaml/lib/FrameworkApplication_Partial.cpp
+++ b/dxaml/xcp/dxaml/lib/FrameworkApplication_Partial.cpp
@@ -26,6 +26,8 @@
#include
#include
#include
+#include "GamepadKeyRoutingLightup.h"
+#include
using namespace RuntimeFeatureBehavior;
using namespace DirectUI;
@@ -180,6 +182,16 @@ _Check_return_ HRESULT FrameworkApplicationFactory::StartImpl(_In_opt_ xaml::IAp
if (policy == AppPolicyWindowingModel_ClassicDesktop)
{
+ if (OptionalChangeState::IsGamepadKeyRoutingEnabled())
+ {
+ // Full WinUI app: enable gamepad-to-key routing for the process, so XAML's
+ // keyboard navigation responds to a gamepad the way it did in System XAML.
+ // Island hosts never reach here (they start via
+ // WindowsXamlManager.InitializeForCurrentThread()), so the host process is
+ // left untouched.
+ VERIFYHR(EnableGamepadKeyRouting());
+ }
+
return FrameworkApplication::StartDesktop();
}
else if (policy == AppPolicyWindowingModel_Universal)
diff --git a/dxaml/xcp/dxaml/lib/GamepadKeyRoutingLightup.h b/dxaml/xcp/dxaml/lib/GamepadKeyRoutingLightup.h
new file mode 100644
index 000000000..9d3273f87
--- /dev/null
+++ b/dxaml/xcp/dxaml/lib/GamepadKeyRoutingLightup.h
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+#pragma once
+
+namespace DirectUI
+{
+ // Enables routing of gamepad input to this process as keyboard virtual keys, so that XAML's
+ // existing keyboard-driven navigation (XY focus, Enter/Space to invoke, Esc to dismiss) responds
+ // to a gamepad without any app-specific code. See docs\design-notes\gamepad-navigation.md.
+ //
+ // The underlying OS setting is process-wide, so this is called only for a full WinUI app, from
+ // Application.Start(). A process that embeds XAML only as an island never reaches that call and
+ // is deliberately left at the OS default. An app opts back out by calling
+ // Windows.UI.Input.GamepadKeyRoutingConfiguration.TrySetKeyRoutingEnabled(false) any time after
+ // Application.Start().
+ //
+ // Defined in a light-up translation unit because the OS API requires a newer UAP contract than
+ // the one this build normally targets.
+ _Check_return_ HRESULT EnableGamepadKeyRouting();
+}
diff --git a/dxaml/xcp/dxaml/lib/XamlOptionalChanges_Partial.cpp b/dxaml/xcp/dxaml/lib/XamlOptionalChanges_Partial.cpp
index 8dc19a31c..01b8070a3 100644
--- a/dxaml/xcp/dxaml/lib/XamlOptionalChanges_Partial.cpp
+++ b/dxaml/xcp/dxaml/lib/XamlOptionalChanges_Partial.cpp
@@ -44,6 +44,8 @@ static int GetBitIndex(xaml_settings::XamlChangeId id)
return OptionalChangeState::BitIndex_DefaultStyleOptimizations;
case xaml_settings::XamlChangeId_DeferContextFlyoutInit:
return OptionalChangeState::BitIndex_DeferContextFlyoutInit;
+ case xaml_settings::XamlChangeId_GamepadKeyRouting:
+ return OptionalChangeState::BitIndex_GamepadKeyRouting;
default:
return -1;
}
diff --git a/dxaml/xcp/tools/XCPTypesAutoGen/XamlOM/Model/Microsoft.UI.Xaml.Settings.cs b/dxaml/xcp/tools/XCPTypesAutoGen/XamlOM/Model/Microsoft.UI.Xaml.Settings.cs
index b9c2f9620..45b8d7685 100644
--- a/dxaml/xcp/tools/XCPTypesAutoGen/XamlOM/Model/Microsoft.UI.Xaml.Settings.cs
+++ b/dxaml/xcp/tools/XCPTypesAutoGen/XamlOM/Model/Microsoft.UI.Xaml.Settings.cs
@@ -9,6 +9,7 @@ namespace Microsoft.UI.Xaml.Settings
// through XamlOptionalChanges. Each value corresponds to a specific feature,
// fix, or behavioral change documented in the WinUI release notes.
[Contract(typeof(Microsoft.UI.Xaml.WinUIContract), 11)]
+ [Platform("Feature_ExperimentalApi", typeof(Microsoft.UI.Xaml.WinUIContract), Microsoft.UI.Xaml.WinUIContract.Experimental)]
[DXamlIdlGroup("coretypes2")]
[TypeTable(IsExcludedFromDXaml = true, IsExcludedFromCore = true)]
public enum XamlChangeId
@@ -20,6 +21,8 @@ namespace Microsoft.UI.Xaml.Settings
OptimizeApplyStyles = 61697456,
DefaultStyleOptimizations = 60995620,
DeferContextFlyoutInit = 61098986,
+ [VelocityFeature("Feature_ExperimentalApi")]
+ GamepadKeyRouting = 63117108,
}
// Provides static methods to opt in to or out of individual breaking or
diff --git a/packages.arm64.config b/packages.arm64.config
index 63bde295e..9316333e7 100644
--- a/packages.arm64.config
+++ b/packages.arm64.config
@@ -1,4 +1,4 @@
-
+
diff --git a/packages.arm64ec.config b/packages.arm64ec.config
index 61835153f..af88eeb95 100644
--- a/packages.arm64ec.config
+++ b/packages.arm64ec.config
@@ -1,4 +1,4 @@
-
+
diff --git a/packages.config b/packages.config
index e955ab738..b0b79c2b9 100644
--- a/packages.config
+++ b/packages.config
@@ -5,7 +5,7 @@
-
+
diff --git a/packages.x64.config b/packages.x64.config
index 288cc7062..fbd272f23 100644
--- a/packages.x64.config
+++ b/packages.x64.config
@@ -1,4 +1,4 @@
-
+
diff --git a/packages.x86.config b/packages.x86.config
index fee03ce48..9ea991f75 100644
--- a/packages.x86.config
+++ b/packages.x86.config
@@ -1,4 +1,4 @@
-
+