Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/test-samples-screenshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ jobs:
DOTNET_DbgEnableMiniDump: "1"
DOTNET_DbgMiniDumpType: "1"
DOTNET_DbgMiniDumpName: "${{ github.workspace }}\\crash-dumps\\dotnet_%p.dmp"
# NativeCrashHandler (in the harness + Stride.Graphics.Regression) gates on this: without it it
# sets SEM_NOGPFAULTERRORBOX, which suppresses the WER LocalDumps + DOTNET minidump configured
# here for pure-native crashes. With it, native AVs route to WER and it writes its own SEH dump.
STRIDE_TESTS_CRASH_DUMPS: "1"
STRIDE_TESTS_CRASH_DUMP_DIR: "${{ github.workspace }}\\crash-dumps"
steps:
- uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -185,6 +190,7 @@ jobs:
name: screenshots-${{ matrix.graphics-api }}
path: |
screenshot-out/
screenshot-out-aot/
TestResults/
if-no-files-found: warn

Expand Down
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,6 @@ samplesGenerated
screenshot-regression-out
screenshot-out
ui-test-out-dpi*
# Auto-generated by samples/Directory.Build.targets when -p:StrideAutoTesting=true is passed
# (force-loads Stride.Games.AutoTesting at startup so its [ModuleInitializer] runs).
_AutoTestingBootstrap.g.cs

# VS Code files
.vscode/*
Expand Down
20 changes: 18 additions & 2 deletions build/Stride.sln
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,6 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "22-StrideRuntime.Shaders", "22-StrideRuntime.Shaders", "{7E3ACF35-0CCA-4E88-866B-0F008C5A5580}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.GameStudio", "..\sources\editor\Stride.GameStudio\Stride.GameStudio.csproj", "{2FCA2D8B-B10F-4DCA-9847-4221F74BA586}"
ProjectSection(ProjectDependencies) = postProject
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Engine", "..\sources\engine\Stride.Engine\Stride.Engine.csproj", "{C121A566-555E-42B9-9B0A-1696529A9088}"
EndProject
Expand Down Expand Up @@ -368,6 +366,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Templates.Samples",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Templates.Tests", "..\sources\tools\Stride.Templates.Tests\Stride.Templates.Tests.csproj", "{1BB0CF77-74E3-4E85-ACC3-9B3CFFCD50CA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stride.Games.AutoTesting", "..\sources\engine\Stride.Games.AutoTesting\Stride.Games.AutoTesting.csproj", "{BD5B7C55-DD46-4032-BF22-26E1789A43D6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -2198,6 +2198,22 @@ Global
{1BB0CF77-74E3-4E85-ACC3-9B3CFFCD50CA}.Release|x64.Build.0 = Release|Any CPU
{1BB0CF77-74E3-4E85-ACC3-9B3CFFCD50CA}.Release|x86.ActiveCfg = Release|Any CPU
{1BB0CF77-74E3-4E85-ACC3-9B3CFFCD50CA}.Release|x86.Build.0 = Release|Any CPU
{BD5B7C55-DD46-4032-BF22-26E1789A43D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BD5B7C55-DD46-4032-BF22-26E1789A43D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BD5B7C55-DD46-4032-BF22-26E1789A43D6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{BD5B7C55-DD46-4032-BF22-26E1789A43D6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{BD5B7C55-DD46-4032-BF22-26E1789A43D6}.Debug|x64.ActiveCfg = Debug|Any CPU
{BD5B7C55-DD46-4032-BF22-26E1789A43D6}.Debug|x64.Build.0 = Debug|Any CPU
{BD5B7C55-DD46-4032-BF22-26E1789A43D6}.Debug|x86.ActiveCfg = Debug|Any CPU
{BD5B7C55-DD46-4032-BF22-26E1789A43D6}.Debug|x86.Build.0 = Debug|Any CPU
{BD5B7C55-DD46-4032-BF22-26E1789A43D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BD5B7C55-DD46-4032-BF22-26E1789A43D6}.Release|Any CPU.Build.0 = Release|Any CPU
{BD5B7C55-DD46-4032-BF22-26E1789A43D6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{BD5B7C55-DD46-4032-BF22-26E1789A43D6}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{BD5B7C55-DD46-4032-BF22-26E1789A43D6}.Release|x64.ActiveCfg = Release|Any CPU
{BD5B7C55-DD46-4032-BF22-26E1789A43D6}.Release|x64.Build.0 = Release|Any CPU
{BD5B7C55-DD46-4032-BF22-26E1789A43D6}.Release|x86.ActiveCfg = Release|Any CPU
{BD5B7C55-DD46-4032-BF22-26E1789A43D6}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
1 change: 1 addition & 0 deletions docs/build/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,5 @@ Stride auto-selects the native toolchain: MSVC when running under `MSBuild.exe`
## Further Reading

- [SDK-GUIDE.md](SDK-GUIDE.md) — Stride build SDK internals (target structure, project SDK selection, native build modes)
- [aot.md](aot.md) — NativeAOT & trimming: publishing games, feature switches for optional subsystems, keeping the engine AOT-clean
- [../../sources/templates/README.md](../../sources/templates/README.md) — `dotnet new` template packages (Stride.Templates.Games / .Games.Starters / .Samples): end-user usage, local dev workflow, adding a new sample
92 changes: 92 additions & 0 deletions docs/build/aot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# NativeAOT & Trimming Guide

How to publish a Stride game with NativeAOT or trimming, which optional subsystems get dropped, and
how to keep the engine AOT-clean when contributing.

## Status

The Stride engine **runtime** assemblies are NativeAOT- and trim-clean: a sample publishes with
`PublishAot` and runs (renders, captures, tears down) with no ILC errors. This is verified in CI by
the test guards below. The **editor** (Game Studio) is WPF and is not AOT-published — AOT applies to
shipped games only.

## Publishing a game

```sh
# NativeAOT (single self-contained native exe). Windows needs the MSVC toolchain; the ILC link step
# shells out to vswhere.exe, so the VS Installer dir must be on PATH (a VS developer shell has this).
dotnet publish MyGame.Windows.csproj -c Release -r win-x64 -p:PublishAot=true -p:SelfContained=true

# Trimmed (still JIT). Trimmed assemblies stay on disk as DLLs, so it's the easy way to inspect what
# survived (e.g. confirm a backend was dropped).
dotnet publish MyGame.Windows.csproj -c Release -r win-x64 -p:PublishTrimmed=true -p:SelfContained=true
```

## Optional subsystems & feature switches

Optional/dev-only subsystems are guarded by `[FeatureSwitchDefinition]` switches so trimming/AOT can
drop them. Defaults are applied at the consuming game's publish via `RuntimeHostConfigurationOption`
items in each declaring assembly's `buildTransitive/<Assembly>.targets`.

| Switch | Declared in | Normal/JIT | Trim | AOT | Override |
|--------|-------------|-----------|------|-----|----------|
| `Stride.Engine.RemoteEffectCompilerEnabled` | Stride.Engine | on | **off** | **off** | `-p:StrideRemoteEffectCompilerEnabled=true` |
| `Stride.Games.WinFormsBackendEnabled` | Stride.Games | on | on | on | set the switch `false` (see below) |
| `Stride.Games.SDLBackendEnabled` | Stride.Games | on | on | on | set the switch `false` (see below) |

- **Remote effect compiler** — the Game Studio shader-compiler socket client; no shipped game uses it
(it falls back to the local compiler), so it's dropped under trim/AOT by default.
- **Windowing backends** — WinForms is the AOT-safe default Windows backend, so it stays on. To drop a
backend, set its switch off explicitly:

```xml
<ItemGroup>
<RuntimeHostConfigurationOption Include="Stride.Games.WinFormsBackendEnabled" Value="false" Trim="true" />
</ItemGroup>
```

Dropping WinForms also requires the game to use the **SDL** backend (Windows defaults to WinForms; a
`DesktopWinForms` context with the switch off throws at window creation). WPF is **not** a runtime
backend — `AppContextType.DesktopWPF` is never produced; WPF hosting goes through WinForms interop.

## Keeping the engine AOT-clean (contributor guidance)

- **Don't add AOT-incompatible dependencies** to runtime assemblies. Known offenders:
- `Microsoft.Management.Infrastructure` / `System.Management` (WMI/CIM) — native delegate marshalling
throws `NotSupportedException` under AOT. Use a P/Invoke alternative (e.g. Raw Input replaced the
WMI XInput-detection query in `Stride.Input`).
- Reflection-based `System.Text.Json` — disabled under AOT. Use `Utf8JsonWriter`/source generators.
- `Marshal.SizeOf` — use `Unsafe.SizeOf<T>()`.
- **Gate optional subsystems behind a feature switch and gate their *registration*** so the trimmer can
prove the branch is dead and remove the type (and its dependencies). Declare the switch with
`[FeatureSwitchDefinition("...")]` returning `!AppContext.TryGetSwitch(name, out var v) || v`.
- **Put the trim/AOT default** for a switch in the *declaring* assembly's `buildTransitive/<Assembly>.targets`
(conditioned on `PublishAot`/`PublishTrimmed`, guarded by an opt-in property), so it ships transitively
to consumer games. `buildTransitive/` alone covers both direct and transitive consumers.
- **Drain trim warnings** with `[DynamicallyAccessedMembers]` where the reflection is real, or a
justified `[UnconditionalSuppressMessage]` where a path is safe (name-resolved, single-file, rooted by
the assembly processor). Don't suppress blindly.

## Test guards

Two xunit facts in `Stride.Samples.Tests` guard AOT-cleanliness (Direct3D11 only; they publish, which is
slow):

- **`TopDownRPGAot`** — AOT-publishes a sample and runs it through the same capture + LPIPS compare as
the JIT lane. Catches ILC failures, reflection-serialization regressions, and native teardown crashes.
- **`TopDownRPGTrimsWinForms`** — publishes trimmed with `WinFormsBackendEnabled=false` and asserts
`System.Windows.Forms.dll` is gone from the output, proving the WinForms gating actually trims (and
catching any un-gated `System.Windows.Forms` reference).

```sh
dotnet test samples/Tests/Stride.Samples.Tests.csproj --filter "FullyQualifiedName~TopDownRPGAot"
```

CI must set `STRIDE_TESTS_CRASH_DUMPS=1` for these — see below.

## Debugging a native AOT crash

A NativeAOT crash is a native access violation — capture and analyze it like any other native crash;
see [Debugging native crashes](../debugging/native-crashes.md). One AOT-specific aid: a Debug AOT
publish emits a native `<app>.pdb` next to the exe, so point the debugger's symbol path at the publish
dir.
56 changes: 56 additions & 0 deletions docs/debugging/native-crashes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Debugging Native Crashes

Native access violations — from GPU drivers (including software renderers like WARP and Lavapipe),
native audio (XAudio2), native interop, or NativeAOT-published apps — are crashes the .NET runtime
**cannot catch via `try`/`catch`**, and Windows Error Reporting (WER) **doesn't fire for by default**
because the runtime dispatches the exception internally. Left alone they exit silently (e.g. exit code
`139`, no dump), which is especially painful for intermittent crashes and CI.

## The harness crash handler

Stride's test assemblies (`Stride.Graphics.Regression` and `Stride.Games.AutoTesting`) install a shared
`NativeCrashHandler` from a `[ModuleInitializer]` (`sources/shared/NativeCrashHandler.cs`). It:

- Calls `SetErrorMode` to hide the Windows crash dialog so a crash can't hang CI.
- **Gotcha — `SEM_NOGPFAULTERRORBOX` defeats dump capture.** That flag also suppresses WER LocalDumps
*and* the runtime minidump (`DOTNET_DbgEnableMiniDump`) for pure-native crashes. So it's **gated**: in
capture mode the handler omits it so the crash routes to WER; otherwise it keeps it so a crash can't
hang on a dialog.
- When **`STRIDE_TESTS_CRASH_DUMPS=1`**, registers a `FirstChanceException` handler that writes an SEH
minidump, and writes dumps to **`STRIDE_TESTS_CRASH_DUMP_DIR`**.

> [!IMPORTANT]
> Native dumps are only produced when `STRIDE_TESTS_CRASH_DUMPS=1`. CI sets it (plus
> `STRIDE_TESTS_CRASH_DUMP_DIR`) in the screenshot/GPU test jobs; set it locally when reproducing a
> native crash.

Note that `FirstChanceException` only sees AVs that surface through the managed/SEH layer; *pure*-native
crashes are caught by WER instead — which is why both are configured.

## Capturing a dump locally

1. `set STRIDE_TESTS_CRASH_DUMPS=1` and `set STRIDE_TESTS_CRASH_DUMP_DIR=C:\dumps`.
2. For pure-native crashes, enable WER LocalDumps once (admin):

```powershell
$wer = "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting"
reg add "$wer" /v DontShowUI /t REG_DWORD /d 1 /f
reg add "$wer\LocalDumps" /v DumpFolder /t REG_EXPAND_SZ /d "C:\dumps" /f
reg add "$wer\LocalDumps" /v DumpType /t REG_DWORD /d 2 /f # 2 = full memory
```

3. Loop the test/exe until the (often intermittent) crash fires; the dump lands in the dump folder.

## Analyzing a dump

`winget install Microsoft.WinDbg` ships `cdbX64.exe` (under `%LOCALAPPDATA%\Microsoft\WindowsApps\`). A
Debug build emits native PDBs (a Debug NativeAOT publish emits `<app>.pdb` next to the exe). Point the
debugger's symbol path at the dir holding the PDBs:

```sh
cdbX64.exe -z dump.dmp -y "<dir-with-pdbs>;srv*C:\symbols*https://msdl.microsoft.com/download/symbols" \
-c "!analyze -v; .ecxr; kb 60; q"
```

`!analyze -v` reports the faulting module/thread and a `FAILURE_BUCKET` directly; `.ecxr; kb` shows the
crashing call stack.
44 changes: 0 additions & 44 deletions samples/Directory.Build.targets

This file was deleted.

Loading
Loading