Home / Features / Cross-platform
Six targets, four graphics APIs, one app-facing model. There is no OpenGL or WebGL fallback path: each platform gets the current API for that platform, with its own hand-written shader set. Everything that genuinely differs is pushed behind one static service object, so your scene code never contains a platform conditional.
| Platform | Target framework | Entry point | Backend |
|---|---|---|---|
| Windows | net10.0-windows10.0.19041.0 | WindowsApp.Run(app) | Direct3D 12 |
| Linux | net10.0 | LinuxApp.Run(app) | Vulkan |
| Android | net10.0-android | AndroidApp.Run(app) | Vulkan |
| iOS | net10.0-ios | iOSApp.Run(app) | Metal |
| Mac Catalyst | net10.0-maccatalyst | MacCatalystApp.Run(app) | Metal |
| Web | net10.0-browser | WebApp.Run(app, jsRuntime, httpClient, ...) | WebGPU |
The Windows target is added conditionally, only when the build host is Windows, so a Linux or macOS machine can build the other five without installing a Windows SDK. Android, iOS and Mac Catalyst use the MAUI hosting layer for their application shell; Linux and Web do not.
Vulkan is shared between Linux and Android. They live in the same
Platforms/Shared/LinuxAndroid backend, and iOS and Mac Catalyst
share Platforms/Shared/Apple. Four graphics implementations,
six platforms: the sharing is at the API level, which is the only level where sharing is
honest.
HLSL for Direct3D 12, GLSL compiled to SPIR-V for Vulkan, Metal Shading Language for Apple, WGSL for WebGPU. No cross-compiler, no shader abstraction layer, no runtime translation. The cost is that a lighting change is four edits; the benefit is that when one backend looks wrong you are reading the shader that actually runs, and a backend can use a feature the others do not have without inventing a lowest common denominator.
DeviceServices is a static set of interfaces, initialised once
by the platform entry point before your app runs. Everything platform-specific is reached
through it, which is why the engine source contains no
#if ANDROID in scene-facing code.
| Service | Interface | Covers |
|---|---|---|
Core | IDeviceCore | Platform and channel, orientation, resource loading, dark mode, permissions. |
Media | IMediaPlayer | Music and sound playback with separate volumes. |
Video | IVideoPlayerService | Video frames pushed into the engine as RGBA8. Hardware accelerated on Windows. |
Dialog | IDialogService | Native message boxes and the native keyboard. |
File | IFileService | Pickers and platform file access. |
Image | IImageService | Native image decode and encode. |
Gallery | IGalleryService | Reading from and saving to the system photo library. |
Record | IRecordService | Microphone capture. |
Recorder | IMediaRecorder | Recording the app's own rendered output. Null where unimplemented. |
Download | IDownloadService | Background downloads with progress. |
Store | IStoreService | In-app purchase and licence checks. |
Ads | IAds | Ad integration, opt-in. |
WindowsFeatures | IWindowsFeatures | Windows-only extras, isolated behind one interface rather than sprinkled through the engine. |
Where a platform has no implementation, the service is null or a documented no-op. It does
not silently return a plausible-looking value. Callers null-check
— DeviceServices.Recorder is the one you will hit
first — and in exchange you never ship a feature that appeared to work in
development and does nothing on a user's device.
Two concrete examples of the asymmetry. Screen capture splits cleanly:
CaptureSelf, which reads back the engine's own backbuffer as
RGBA8, works everywhere because it goes through the graphics API.
CaptureWindow, which grabs the whole screen, is desktop only
— mobile and Web sandboxes do not permit it, and the method returns null rather than a
black image. Resource loading splits too: every platform has a synchronous
LoadFile, but Web overrides the async path with an HTTP download,
because Blazor WebAssembly has no synchronous I/O and a task there must never be waited on
synchronously.
Your application is a BaseApp subclass with panels in it, and
it is the same class on all six targets. Each platform head is a small project whose whole
job is to create the window or canvas, construct the platform service implementations, and
hand your app to the loop.
// Windows head
WindowsApp.Run(new MyApp());
// Web head, inside a Blazor component
await WebApp.Run(new MyApp(), JS, Http, canvasId: "season-canvas");
The Web head takes more arguments because the browser gives it less: a JavaScript runtime for
interop, an HttpClient because assets arrive over the network,
and the id of the canvas to render into. Everything above that line is identical.
WebGPU is real, not emulated. The GPU objects live in JavaScript in
seasonWebGPU.js and the C# layer forwards through
[JSImport]. Shaders are WGSL, compiled by the browser, and the
JS side carries no shader source of its own. Compute effects run in the browser; global
illumination, TAA and the atmosphere model are the same code path as on the desktop.
StorageService resolves one settings and file location per
platform from a single DirectoryBase name, so an app declares
where its data lives once and gets the right answer on every target.
Database is the local persistence layer, and
Localization backs the
Texts.Translate flag — which is why text is translated
before layout rather than after, and a longer German string reflows correctly instead of
overflowing its box.