Add opt-in GamepadKeyRouting XAML optional change for gamepad navigation (#11853)

## 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:


<EnabledXamlOptionalChanges>GamepadKeyRouting</EnabledXamlOptionalChanges>

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

`<EnabledXamlOptionalChanges>GamepadKeyRouting</EnabledXamlOptionalChanges>`:
  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
  (`<DisabledXamlOptionalChanges>`), 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
This commit is contained in:
TomokoKwanXbox authored and GitHub committed 2026-09-29 13:29:19 -07:00
1 parent c35ac99f88
commit 5b75cd05e6
22 files changed
+363 -68

No files matched your search

@@ -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 <roapi.h>
#include <windows.foundation.metadata.h>
#include <windows.ui.input.h>
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<wf_metadata::IApiInformationStatics> apiInformation;
if (FAILED(RoGetActivationFactory(
static_cast<HSTRING>(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<HSTRING>(winrt::get_abi(contractName)),
19,
&isContractPresent)) ||
!isContractPresent)
{
return L"Unavailable";
}
auto const configurationName = RuntimeClassName(L"Windows.UI.Input.GamepadKeyRoutingConfiguration");
winrt::com_ptr<wui::IGamepadKeyRoutingConfigurationStatics> gamepadKeyRouting;
if (FAILED(RoGetActivationFactory(
static_cast<HSTRING>(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";
}
}
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#pragma once
#include <string>
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();
}
@@ -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 <iostream>
#include <sstream>
#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
@@ -20,6 +20,10 @@
</StackPanel>
<!-- Custom title bar end block -->
<TextBlock Text="WinUICppDesktopSampleApp" FontSize="20" Margin="2"/>
<StackPanel Orientation="Horizontal">
<TextBlock Text="GamepadKeyRouting:" VerticalAlignment="Center" Margin="2"/>
<TextBlock x:Name="textBlockGamepadKeyRouting" AutomationProperties.Name="textBlockGamepadKeyRouting" VerticalAlignment="Center" Margin="2"/>
</StackPanel>
<ContentDialog x:Name="contentDialogWindowChromeTest"
PrimaryButtonText="Accept" IsPrimaryButtonEnabled="False"
CloseButtonText="Cancel">
@@ -49,7 +49,7 @@
<ApplicationTypeRevision>10.0</ApplicationTypeRevision>
<!-- Test specifying Enabled/Disabled optional changes -->
<EnabledXamlOptionalChanges>_Reserved</EnabledXamlOptionalChanges>
<EnabledXamlOptionalChanges>_Reserved,GamepadKeyRouting</EnabledXamlOptionalChanges>
<DisabledXamlOptionalChanges>_Reserved</DisabledXamlOptionalChanges>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
@@ -137,6 +137,7 @@
<DependentUpon>MarkupWindow.xaml</DependentUpon>
</ClInclude>
<ClInclude Include="ShutdownOrderValidation.h" />
<ClInclude Include="GamepadKeyRoutingState.h" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml" />
@@ -186,6 +187,7 @@
</ClCompile>
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
<ClCompile Include="ShutdownOrderValidation.cpp" />
<ClCompile Include="GamepadKeyRoutingState.cpp" />
</ItemGroup>
<ItemGroup>
<Midl Include="App.idl">
@@ -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 <EnabledXamlOptionalChanges>...GamepadKeyRouting</EnabledXamlOptionalChanges>,
// 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
@@ -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;
}
+121 -60
View File
@@ -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
<PropertyGroup>
<EnabledXamlOptionalChanges>GamepadKeyRouting</EnabledXamlOptionalChanges>
</PropertyGroup>
```
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 <winrt/Windows.UI.Input.h>` and
`#include <winrt/Windows.Foundation.Metadata.h>`.
`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.
@@ -2117,6 +2117,10 @@ std::vector<std::pair<xaml_settings::XamlChangeId, bool>> 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)
@@ -2212,6 +2212,7 @@ namespace DirectUI
OptimizeApplyStyles = 61697456,
DefaultStyleOptimizations = 60995620,
DeferContextFlyoutInit = 61098986,
GamepadKeyRouting = 63117108,
};
DEFINE_ENUM_FLAG_OPERATORS(XamlChangeId);
@@ -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);
}
}
@@ -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
};
}
+40
View File
@@ -12,6 +12,9 @@
#include "DXamlCore.h"
#include <WRLHelper.h>
#include "Callback.h"
#include "GamepadKeyRoutingLightup.h"
#include <windows.foundation.metadata.h>
#include <windows.ui.input.h>
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<wf::Metadata::IApiInformationStatics> 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<wui::IGamepadKeyRoutingConfigurationStatics> 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;
}
@@ -26,6 +26,8 @@
#include <DesktopWindowImpl.h>
#include <Microsoft.UI.Dispatching.Interop.h>
#include <Microsoft.Windows.ApplicationModel.Resources.h>
#include "GamepadKeyRoutingLightup.h"
#include <OptionalChangeState.h>
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)
@@ -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();
}
@@ -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;
}
@@ -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
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.SDK.cpp.arm64" version="10.0.22621.755" />
<package id="Microsoft.Windows.SDK.cpp.arm64" version="10.0.26100.9169" />
</packages>
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.SDK.cpp.arm64" version="10.0.22621.755" />
<package id="Microsoft.Windows.SDK.cpp.arm64" version="10.0.26100.9169" />
</packages>
+1 -1
View File
@@ -5,7 +5,7 @@
<!-- For more information see docs\build-versioning.md -->
<packages>
<!-- Windows SDK as a NuGet package for c++ projects -->
<package id="Microsoft.Windows.SDK.cpp" version="10.0.22621.755" />
<package id="Microsoft.Windows.SDK.cpp" version="10.0.26100.9169" />
<!-- Windows SDK metadata for language projections, fixed at RS5 (WinAppSDK downlevel limit)-->
<package id="Microsoft.Windows.SDK.Contracts" version="10.0.17763.1000" />
<!-- Metapackage for all the necessary components of .Net Native -->
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.SDK.cpp.x64" version="10.0.22621.755" />
<package id="Microsoft.Windows.SDK.cpp.x64" version="10.0.26100.9169" />
</packages>
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.SDK.cpp.x86" version="10.0.22621.755" />
<package id="Microsoft.Windows.SDK.cpp.x86" version="10.0.26100.9169" />
</packages>