microsoft / microsoft/microsoft-ui-xaml
XamlCompiler.exe is a .NET Framework AnyCPU binary and runs under x64 emulation on Windows on ARM, dominating WinUI build time
- Dominant language
- C++
- Stars
- 8.4k
- Forks
- 942
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 105
Description
### Describe the bug
`XamlCompiler.exe`, which the WinUI MSBuild targets shell out to for every XAML compile, ships **only** as a .NET Framework 4.7.2 **AnyCPU** binary (`tools/net472/XamlCompiler.exe`). On an ARM64 Windows host there is no ARM64-native way to run it, so it launches as an **emulated x64** process, even when everything around it (the `dotnet` SDK, MSBuild, `cl.exe`, the host itself) is ARM64-native.
On my ARM64 machine this single emulated process accounts for **~76% of a clean WinUI build**: 72.0s of a 94.8s build, in an app with just 8 `.xaml` files.
This is **not** the usual "AnyCPU is fine, it'll pick the native architecture" case, and it is **not** blocked by .NET Framework's ARM64 support, since 4.8.1 has had native ARM64 for years and the ARM64 CLR is present on the box. I measured both (see Additional context): an AnyCPU .NET Framework exe still runs **x64-emulated** on Windows on ARM, while the *same code* built `/platform:arm64` runs **ARM64-native**. So the gap is purely in how `XamlCompiler.exe` is built and shipped, and a fix needs no port off .NET Framework.
### Why is this important?
Every XAML compile, on every ARM64 dev machine and every ARM64 CI runner, pays x64 emulation on the single step that dominates a WinUI build. It is also the step developers hit most often, since it is the inner loop of editing XAML: each edit-build-run cycle burns most of its wall clock inside an emulated process.
ARM64 is now a first-class Windows dev target and WinUI is the flagship UI stack for it, so this is precisely the part of the toolchain that should not be the one piece that cannot run natively. There is also no way out: the in-proc escape hatch (`-p:UseXamlCompilerExecutable=false`) fails outright (see Additional context), so ARM64 users cannot opt into a native XAML compile at all.
### Steps to reproduce the bug
1. On an ARM64 Windows machine, create or open any WinUI 3 / Windows App SDK app with at least one `.xaml` file.
2. Touch the `.xaml` files so the XAML compile cannot be skipped, then run `dotnet build -r win-arm64 -p:Platform=ARM64`.
3. While it builds, open **Task Manager**, go to **Details**, and add the **Architecture** column.
4. Watch for `XamlCompiler.exe`.
### Actual behavior
`XamlCompiler.exe` runs as an **x64** (emulated) process, while every other tool in the build (`cl.exe`, MSBuild, `dotnet`) is `Arm64`. It is the slowest step of the build by a wide margin.
Measured with `GetProcessInformation(ProcessMachineTypeInfo)` (the API behind Task Manager's *Architecture* column) against the live process during `dotnet build -r win-arm64 -p:Platform=ARM64`:
```
=== dotnet build -r win-arm64 -p:Platform=ARM64 (8 .xaml files touched) ===
[ 3.2s] START XamlCompiler.exe PID 22164 -> Architecture: x64 (EMULATED on an ARM64 host)
[ 24.2s] EXIT XamlCompiler.exe PID 22164 -- ran for 21.0s
[ 30.6s] START XamlCompiler.exe PID 13132 -> Architecture: x64 (EMULATED on an ARM64 host)
[ 81.6s] EXIT XamlCompiler.exe PID 13132 -- ran for 51.1s
=== build exit=0 | wall clock 94.8s | XamlCompiler.exe 72.0s across 2 run(s) = 76% of the build ===
```
### Expected behavior
On an ARM64 host, the XAML compiler runs as an ARM64-native process, like the rest of the toolchain.
### Screenshots
Task Manager during a build. `XamlCompiler.exe` is the only **x64** process in an otherwise all-ARM64 build:
| Name | PID | Architecture | Description |
|---|---|---|---|
| cl.exe | 21760 | Arm64 | Microsoft® C-C++ Compiler Driver |
| cl.exe | 19072 | Arm64 | Microsoft® C-C++ Compiler Driver |
| **XamlCompiler.exe** | 332 | **x64** | Microsoft.UI.Xaml.Markup.Compiler |
### NuGet package version
2.2.0 (`Microsoft.WindowsAppSDK`); the WinUI package it resolves to is `microsoft.windowsappsdk.winui` 2.2.1, carrying `XamlCompiler.exe` 3.0.0.2606.
### Windows version
Windows 11 (25H2): Build 26200
### Additional context
### 1. The compiler that is executed is always the .NET Framework one
From `Microsoft.UI.Xaml.Markup.Compiler.interop.targets` (WindowsAppSDK 2.2.1):
```xml
<_MuxPackageToolsFolder Condition=" '$(MSbuildRuntimeType)' == 'Core' ">$(MSBuildThisFileDirectory)..\tools\net6.0\
<_MuxPackageToolsFolder Condition=" '$(_MuxPackageToolsFolder)' == '' ">$(MSBuildThisFileDirectory)..\tools\net472\
true
$(_MuxPackageToolsFolder)..\net472\XamlCompiler.exe
```
Under `dotnet build` (`MSBuildRuntimeType == Core`) the targets select the **`net6.0`** tools folder *and* force `UseXamlCompilerExecutable=true`, but `XamlCompilerExePath` then walks straight back out of it (`$(_MuxPackageToolsFolder)..\net472\XamlCompiler.exe`) to the **net472** executable. So the `net6.0` folder is used for the in-proc helper tasks (`Microsoft.UI.Xaml.Markup.Compiler.IO.dll`), while the actual compile is `Exec`'d as a .NET Framework process.
The package already contains a **.NET build of the compiler itself** (`tools/net6.0/Microsoft.UI.Xaml.Markup.Compiler.dll`). It just has no executable host, so it is never used for the compile.
### 2. `XamlCompiler.exe` is an AnyCPU .NET Framework image
Parsing the PE header of `tools/net472/XamlCompiler.exe` (v3.0.0.2606):
| Field | Value |
|---|---|
| PE format | PE32 |
| Machine | `IMAGE_FILE_MACHINE_I386` (0x14C) |
| COR flags | `0x9` (ILONLY, no 32BITREQUIRED, no 32BITPREFERRED), i.e. **AnyCPU** |
| `supportedRuntime` (`.exe.config`) | `.NETFramework,Version=v4.7.2` |
### 3. AnyCPU is not enough, but an ARM64 target works today
This is the part I most want to flag, because it makes the fix small. It would be easy to assume "AnyCPU + .NET Framework 4.8.1 (which has native ARM64) = native", but that is not what happens. I compiled one trivial .NET Framework program twice and ran both on the same box:
| Build | `RuntimeInformation.ProcessArchitecture` | Architecture (Task Manager API) |
|---|---|---|
| `csc /platform:anycpu` | `X64` | **x64 (emulated)** |
| `csc /platform:arm64` | `Arm64` | **ARM64 (native)** |
Same source, same installed runtime (.NET Framework 4.8.1, Release `533509`; `%WINDIR%\Microsoft.NET\FrameworkArm64` is present). An AnyCPU .NET Framework exe is launched as emulated x64 on Windows on ARM; an ARM64-targeted one runs native. **The .NET Framework runtime is not the blocker; the build configuration of `XamlCompiler.exe` is.**
### 4. Suggested fixes (either would resolve it)
1. **Ship an ARM64-native host for the compiler.** The `net6.0` build of `Microsoft.UI.Xaml.Markup.Compiler.dll` is already in the package. Giving it an executable host (a .NET apphost, published per-RID including `win-arm64`) and pointing `XamlCompilerExePath` at the RID-matched exe would make the compile native on ARM64, and would modernize the `dotnet build` path, which already selects `tools/net6.0/` for everything *except* the compile. As a smaller variant of the same idea: under `MSBuildRuntimeType == Core`, run the `CompileXaml` task **in-proc** from the `net6.0` assembly, which is already native because `dotnet`/MSBuild are.
2. **Or, at minimum, add an ARM64-targeted `XamlCompiler.exe`** (`/platform:arm64`, e.g. `tools/net472-arm64/`) and have the targets select it on an ARM64 host. As shown in §3, this alone is sufficient to get a native process on the current .NET Framework, with no port required.
Fix (1) is the better long-term shape; fix (2) is a build flag and a copy, and could ship immediately.
### 5. Workaround attempted (does not work)
Since a `net6.0` compiler assembly is present and the targets *do* have an in-proc path, the obvious escape hatch is to force it. It fails:
```
dotnet build ... -p:UseXamlCompilerExecutable=false
error MSB4216: Could not run the "CompileXaml" task because MSBuild could not create or connect to a task host
with runtime "NET" and architecture "*". Please ensure that (1) the requested runtime and/or architecture are
available on the machine, and (2) that the required executable
"C:\Program Files\dotnet\sdk\10.0.301\MSBuild.exe" exists and can be run.
error MSB4027: The "CompileXaml" task generated invalid items from the "GeneratedCodeFiles" output parameter.
This object is no longer valid because the MetadataLoadContext that created it has been disposed.
```
So there is currently **no supported way to avoid the emulated compiler** on ARM64. (The `MSB4216` failure may be worth a look on its own: the in-proc path appears to request a task host that cannot be created under the .NET SDK's MSBuild.)
### 6. Environment
| | |
|---|---|
| OS | Windows 11 Pro 26200 (25H2), ARM64 |
| CPU | Microsoft SQ1 (ARM64) |
| Windows App SDK | 2.2.0; WinUI package `microsoft.windowsappsdk.winui` 2.2.1 |
| `XamlCompiler.exe` | 3.0.0.2606 (`tools/net472/`) |
| .NET SDK | 10.0.301 (`win-arm64`, ARM64-native) |
| Target framework | `net10.0-windows10.0.26100.0` |
| .NET Framework installed | 4.8.1 (Release 533509); `FrameworkArm64` present |
| Visual Studio | 2026 (18.x) |
Build times vary with load (I saw the same build stretch to ~270s), but the *share* spent inside the emulated compiler is consistently the dominant cost. My host is a first-gen SQ1, where the emulation tax is at its worst; a Snapdragon X box will hurt less, but it is still emulation, and still avoidable.
Contributor guide
Research direction
Start with Microsoft.UI.Xaml.Markup.Compiler.interop.targets, especially the _MuxPackageToolsFolder and XamlCompilerExePath entries, and inspect tools/net6.0/Microsoft.UI.Xaml.Markup.Compiler.dll alongside tools/net472/XamlCompiler.exe. Reproduce with dotnet build -r win-arm64 -p:Platform=ARM64 and verify that the selected compiler runs ARM64-native rather than as an emulated x64 process.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- build-system, operating-systems, performance
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100