MIT License NuGet

Home / Features / Cross-platform

Cross-platform by isolation

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.

The targets

Windows Linux macOS iOS Android Direct3D 12 Vulkan Metal WebGPU
PlatformTarget frameworkEntry pointBackend
Windowsnet10.0-windows10.0.19041.0WindowsApp.Run(app)Direct3D 12
Linuxnet10.0LinuxApp.Run(app)Vulkan
Androidnet10.0-androidAndroidApp.Run(app)Vulkan
iOSnet10.0-iosiOSApp.Run(app)Metal
Mac Catalystnet10.0-maccatalystMacCatalystApp.Run(app)Metal
Webnet10.0-browserWebApp.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.

Shaders are written four times, on purpose

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.

Device services

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.

ServiceInterfaceCovers
CoreIDeviceCorePlatform and channel, orientation, resource loading, dark mode, permissions.
MediaIMediaPlayerMusic and sound playback with separate volumes.
VideoIVideoPlayerServiceVideo frames pushed into the engine as RGBA8. Hardware accelerated on Windows.
DialogIDialogServiceNative message boxes and the native keyboard.
FileIFileServicePickers and platform file access.
ImageIImageServiceNative image decode and encode.
GalleryIGalleryServiceReading from and saving to the system photo library.
RecordIRecordServiceMicrophone capture.
RecorderIMediaRecorderRecording the app's own rendered output. Null where unimplemented.
DownloadIDownloadServiceBackground downloads with progress.
StoreIStoreServiceIn-app purchase and licence checks.
AdsIAdsAd integration, opt-in.
WindowsFeaturesIWindowsFeaturesWindows-only extras, isolated behind one interface rather than sprinkled through the engine.
Coverage is not uniform, and it is not faked

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.

What a platform entry point does

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.

Storage, settings and localisation

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.

Known gaps

  • Absent No console platforms. No PlayStation, Xbox or Switch backend, and no path to one that does not involve an NDA.
  • Absent No native macOS window. Desktop macOS is reached through Mac Catalyst, not a first-class AppKit head.
  • Partial Service coverage varies by platform. Recording, store and ads are the thinnest. The table above is honest about which ones can be null; check before you design a feature around one.
  • Absent No shader cross-compilation. Adding a lighting term means editing four shader sets. This is a deliberate trade, but it is a real cost and it scales with the number of backends.