Skip to content

Platforms

Snowpulse supports:

  • Windows (desktop)
  • macOS (desktop)
  • Web via Emscripten (WebGL 2)
  • iOS (iPhone and iPad)
  • Android 9/API 28+ (phones and tablets)

Choose the guide that matches the next build you want to produce:

Start with a desktop build from Create Your First Game, then add only the toolchain for the platform you plan to ship.

Capability Matrix

Capability Windows macOS Web (FULL) Web (PLAYABLE_AD) iOS Android
Renderer OpenGL 3 OpenGL 3 WebGL 2 Direct WebGL 2/GLES calls OpenGL ES 3.0 EGL/OpenGL ES 3 + Swappy
Window/input GLFW GLFW Emscripten GLFW/browser canvas Emscripten GLFW/browser canvas UIKit touch GameActivity multi-touch
Audio OpenAL OpenAL Emscripten OpenAL Emscripten OpenAL Vendored OpenAL Soft Vendored OpenAL Soft + Oboe
HTTP libcurl/Schannel libcurl/OpenSSL + Apple SecTrust Emscripten Fetch Compiled out libcurl, HTTP-only OkHttp / Android trust store
MIDI Unavailable manager CoreMIDI Web MIDI Compiled out Unavailable manager Unavailable manager
Local saves Filesystem Filesystem localStorage Compiled out App Documents directory App-private filesDir
Portal cloud saves None None Poki, Playgama, YouTube Compiled out None None
MP4 playback Unavailable AVFoundation Browser <video> Compiled out AVFoundation Media3 ExoPlayer
ImGui / Effekseer Available Available Available Compiled out Available Available
Analytics SDK GameAnalytics 5.3.1; ByteBrew unavailable GameAnalytics 5.3.1; ByteBrew unavailable ByteBrew 1.0.1 + GameAnalytics 5.0.0 SDKs compiled out; facade is a safe null service GameAnalytics 5.0.2; optional vendored ByteBrew GameAnalytics 7.0.2; optional vendored ByteBrew
Orientation query Logical viewport Logical viewport Screen orientation on mobile; logical viewport otherwise Same Logical viewport Logical viewport

FULL and PLAYABLE_AD are compile-time profiles. Their public class layouts differ through SNOWPULSE_HAS_* definitions, so use separate build directories. The analytics facade is the exception: its public API remains available in both profiles, while PLAYABLE_AD removes provider bridges and SDK artifacts.

Desktop

  • Windowing and input via GLFW
  • Rendering via OpenGL
  • Audio via OpenAL (vendored OpenAL Soft preferred when available)
  • HTTP via libcurl
  • MIDI via CoreMIDI on macOS; unavailable no-op manager on Windows
  • Save system writes to a per-bundle-id folder under a writable OS path
  • Cloud save provider defaults to NullCloudSaveProvider
  • MP4 game recording is disabled by default. A separate macOS marketing build opts in with SNOWPULSE_MACOS_MARKETING_RECORDING=ON; Windows recording is unavailable.

Windows production ZIPs and signed/notarized universal macOS DMGs are generated by explicit package targets. See Desktop Production Builds for metadata, assets, static dependency policy, symbols, credentials, and artifact verification.

Desktop MP4 Recording

In an opt-in macOS marketing build, press Control + R to start or stop recording. The shortcut is latched until the toggle key is released, so one key press can only perform one transition. Files are written under ~/Movies/<bundle-id>/ by default as recording_YYYYMMDD_HHMMSS_mmm.mp4.

The recorder captures game content only, before ImGui/debug UI is drawn. Output frames use AppContext::targetResolution(), not the current window or retina framebuffer size. When the window has a different aspect ratio, capture stays centered on the window's logical view. Presentation timestamps follow elapsed real time at the configured output rate, so 120/144 Hz displays do not produce slow-motion video. The default capture rate is 60 fps and the default bitrate is 40 Mbps.

On macOS 13 or newer, recordings include the game's final OpenAL mix as 48 kHz stereo AAC at 192 Kbps by default. The first audio-enabled recording requests macOS Screen Recording permission. Snowpulse waits for that request to resolve before beginning the video timeline, keeping audio and video synchronized. If permission is denied, or on macOS 11-12, recording continues as video-only and prints a warning. Microphone input and audio from other applications are never included.

Ordinary player builds leave recording disabled. The marketing build option enables it by default; it can still be disabled per game through DesktopRecorderConfig:

auto recorder = context()->desktopRecorderConfig();
recorder.enabled = false;
context()->desktopRecorderConfig(recorder);

To keep recording enabled but customize the shortcut or output:

auto recorder = context()->desktopRecorderConfig();
recorder.toggleKey = snowpulse::KeyCode::F;
recorder.requireControl = true;
recorder.frameRate = 60;
recorder.bitrateMbps = 40;
recorder.captureAudio = true;
recorder.audioBitrateKbps = 192;
recorder.outputDirectory = "recordings";
context()->desktopRecorderConfig(recorder);

Set captureAudio to false to produce video-only MP4 files without requesting Screen Recording permission.

Call this from game setup before snowpulse::run(app) starts the desktop loop, or early in App::init() before users can press the shortcut. See Desktop Production for the separate marketing-build workflow.

Web

  • WebGL 2 renderer
  • Emscripten fetch for HTTP
  • WebMIDI for basic non-SysEx MIDI input/output; access is requested explicitly by game code
  • Local save store uses localStorage
  • Asset files are typically preloaded via Emscripten. Optional lazy packs validate Tiled .tmj/.tsj/.tj references transitively and require each linked source to share its deferred pack or live in boot.
  • Mouse wheel capture is controlled by AppConfig::consumeMouseWheel (set to false to allow page scroll)
  • The game target's web-platform preset selects the portal adapter and default save policy before App::init()
  • MP4 playback uses a browser <video> overlay in FULL; see Video Playback
  • AppContext::screenOrientation() follows device/browser-screen orientation on mobile and tablet browsers. Desktop browsers use the canvas-derived logical viewport aspect ratio instead.

Web Portal Integrations

WEB_PLATFORM selects a registry preset that pairs the HTML shell, runtime portal adapter, platform linker behavior, and automatic save policy. The generated target-config header applies the portal and save policy before App::init(); game code should not separately select a stock portal.

WebPortal Adapter status Profile Bundled shell Implemented portal surface Remote saves
Default Generic no-SDK adapter Both shell.html.in, snowblink-basic.html.in, snowblink-plain.html.in, or a custom shell After initialization, loading/gameplay and ad calls report success as no-ops; CTA is unavailable None
Spiktar Implemented FULL spiktar.html.in Init, loading finished, gameplay start/stop, commercial and rewarded breaks None
Poki Implemented FULL poki.html.in Init, loading finished, gameplay start/stop, commercial and rewarded breaks Namespaced web localStorage provider
CrazyGames Implemented, SDK v3 bridge FULL crazygames.html.in Init, loading/gameplay events, video and banner ads, portal audio mute, account changes, reload SDK Data with legacy localStorage migration; requires Remote
GamePix Placeholder; logs and falls back to Default Both None No GamePix SDK calls; inherits the generic successful no-op lifecycle/ad behavior None
GoogleAds Implemented playable bridge Both googleads.html.in or mraid.html.in CTA exit; loading/gameplay are no-ops; ads are unavailable None
MetaAds Placeholder; logs and falls back to Default Both None No Meta SDK calls; inherits the generic successful no-op lifecycle/ad behavior None
ApplovinAds Placeholder; logs and falls back to Default Both None No AppLovin SDK calls; inherits the generic successful no-op lifecycle/ad behavior None
Playgama Implemented, Bridge SDK v2 FULL playgama.html.in Init, loading/gameplay events, interstitial/rewarded ads, language, audio and pause state Bridge storage; requires Remote for moderation-compliant builds
YouTube Implemented Playables bridge FULL youtube.html.in Init, readiness, interstitial/rewarded ads, language, audio and pause state Playables save envelope; requires Remote for certification-compliant builds

Use one of these WEB_PLATFORM values in snowpulse_add_game():

Platform preset Portal Required profile AUTO saves Other behavior
SNOWBLINK_PLAIN Default Either Local Plain Snowblink shell
POKI Poki FULL Local Poki shell
CRAZYGAMES CrazyGames FULL Remote Disables HTML minification
SPIKTAR Spiktar FULL Local Spiktar shell
YOUTUBE YouTube FULL Remote Forces index.html; disables HTML minification
PLAYGAMA Playgama FULL Remote Playgama Bridge shell
GOOGLE_ADS GoogleAds Either Local Google Ads playable shell
MRAID GoogleAds Either Local MRAID shell and optional CTA URL
CUSTOM Explicit Depends on adapter Local Requires WEB_CUSTOM_SHELL and WEB_CUSTOM_PORTAL

Names are case-insensitive; hyphens and underscores normalize equivalently. WEB_SAVE_POLICY LOCAL|REMOTE can override a preset, but CMake warns when the override differs from its known-safe AUTO value.

mraid.html.in exposes the same SnowpulsePlayableAdsBridge consumed by the GoogleAds adapter. It waits for MRAID readiness/viewability and resolves CTA URLs from common click-tag globals, SNOWPULSE_CTA_URL, or the configured CTA_URL. Selecting MetaAds or ApplovinAds does not activate that bridge; those enum values remain unimplemented and use WebPortalAPIDefault instead. That fallback reports its ad operations as successful no-ops after initialization; it does not display an ad or implement CTA exit.

snowblink-reddit.html.in and its bootstrap script are specialized host shells, but Reddit is not a WebPortal enum value and has no dedicated IWebPortalAPI adapter.

The common IWebPortalAPI surface is:

  • initialize, gameLoadingFinished, gameplayStart, and gameplayStop
  • interstitial/commercial and rewarded breaks (including a reward-ID overload)
  • banner display and removal through showBanner and hideBanner
  • exitToCTA
  • optional portal language, audio-mute, pause, and account-change callbacks
  • optional page reload through reloadGame

A method returning false means the selected adapter could not start that operation. Ad completion callbacks report whether the break or reward completed successfully.

Web Build Profiles and Platforms

Goal Build type and effective optimization Engine profile Target mode Result
Lean hosted game (for example, Spiktar, Poki, CrazyGames, Playgama, or YouTube Playables) MinSizeRel (-Os -DNDEBUG) FULL GAME Full features, multi-file output; Assimp optional
Performance-focused hosted game Release (-O3 -DNDEBUG) FULL GAME Full features, multi-file output; Assimp optional
2D playable ad MinSizeRel with -Oz -flto profile overrides PLAYABLE_AD PLAYABLE_AD Reduced features, Closure, single-file output; Assimp off
3D/FBX playable ad MinSizeRel with -Oz -flto profile overrides PLAYABLE_AD PLAYABLE_AD Reduced features, Closure, single-file output; Assimp on

These are four independent controls:

  • CMAKE_BUILD_TYPE selects the normal compiler optimization level.
  • SNOWPULSE_WEB_BUILD_PROFILE controls which engine systems are compiled.
  • WEB_MODE controls whether Emscripten emits a multi-file game or standalone playable artifact.
  • WEB_PLATFORM selects the shell, portal, automatic save policy, and platform-specific output rules.

Target mode and platform do not select the build type or engine profile. Profiles control compile-time engine APIs and must use separate build directories. A platform that needs a compiled-out adapter is rejected.

Projects generated by sdk/bin/snowpulse add a convenience layer over these independent engine controls. Generated CMake keeps GAME_WEB_MODE, GAME_WEB_PLATFORM, and the engine-wide profile together in its top settings block. The CLI maps game to FULL + GAME + POKI and playable to PLAYABLE_AD + PLAYABLE_AD + GOOGLE_ADS:

snowpulse new --name MyGame --id com.example.MyGame --dir . --web-mode playable
snowpulse update --dir MyGame --web-mode game

emcmake cmake -S MyGame -B MyGame/cmake-build-web-playable \
    -DCMAKE_BUILD_TYPE=MinSizeRel

The generated mapping is gameFULL + multi-file Poki and playablePLAYABLE_AD + standalone Google Ads. Assimp, Box2D, and the selected Spine runtime remain independent per-project or per-target options.

Full Web Games

Use FULL for hosted games that need Spiktar, Poki, CrazyGames, Playgama, YouTube Playables, HTTP/Fetch, saves and cloud providers, MIDI, video, ImGui, Effekseer, or the full GL loader. MinSizeRel is the recommended default for a lean download and compiles with -Os -DNDEBUG:

emcmake cmake -S . -B cmake-build-web-poki \
    -DCMAKE_BUILD_TYPE=MinSizeRel \
    -DSNOWPULSE_WEB_BUILD_PROFILE=FULL

cmake --build cmake-build-web-poki --target mygame -j4

Use -DCMAKE_BUILD_TYPE=Release instead when runtime performance matters more than download size. It uses -O3 -DNDEBUG; target packaging does not add or replace either optimization level.

Configure a normal multi-file target declaratively:

snowpulse_add_game(mygame
    SOURCES ${MYGAME_SOURCES}
    DISPLAY_NAME "My Game"
    BUNDLE_ID "com.example.mygame"
    ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets"
    ASSIMP OFF
    SPINE_VERSION "4.2"
    WEB_MODE GAME
    WEB_PLATFORM POKI
    WEB_SAVE_POLICY AUTO
    WEB_OUTPUT_NAME "mygame"
)

The helper emits separate HTML, JavaScript, Wasm, and asset-data files. It preloads real files from assets/ at /assets, excludes .DS_Store, uses EXIT_RUNTIME=1, and intentionally does not enable Closure, single-file embedding, or the playable size checker. Change WEB_PLATFORM to SPIKTAR, CRAZYGAMES, PLAYGAMA, or YOUTUBE; the registry selects the matching shell and adapter. YouTube also forces the output name to index and disables HTML minification.

The Spiktar adapter initializes SpiktarSDK from the shell-provided /assets/js/spiktar-sdk.js and forwards loading, gameplay, commercial-break, and rewarded-break calls. It does not provide CTA, language, audio/pause, or cloud-save integration.

The Poki adapter loads the v2 Poki SDK, forwards loading/gameplay and ad-break calls, and uses the namespaced web local-save key format for its remote save provider. Calling SaveSyncPolicy::Remote with Poki therefore selects the Poki provider, but the storage remains browser localStorage rather than a Poki account service.

The CrazyGames adapter uses the v3 SDK and covers initialization, loading/gameplay events, midgame/rewarded video ads, SDK-controlled audio muting, account-change notification, and remote saves through SDK Data. The auth listener only notifies the engine; it never reloads the page on login. The initial game.settings.muteAudio value and later settings changes force the engine audio mute state without overriding application mute state. Video-ad requests do not mute audio until the SDK invokes adStarted; adFinished and adError then restore the settings-controlled mute state. Missing SDK/ad surfaces, synchronous request errors, and adError callbacks (including adblock and unfilled) resolve the ad request as unsuccessful instead of throwing or leaving gameplay blocked. Applications must continue after a failed midgame request and must not grant a failed rewarded request. A failed rewarded control should also be disabled or hidden with an inline unavailable notice instead of remaining clickable without an effect, as required by the CrazyGames advertisement requirements.

CloudSaveProviderCrazyGames prefers account-scoped SDK Data; if the slot is absent, it copies the old raw localStorage value, verifies the Data write, and only then removes the old value. If the entire SDK is unavailable, gameplay can continue but remote save operations remain paused until the provider recovers. Account profile UI, purchases, and leaderboards are not exposed. Enable Progress Save for the game in the CrazyGames submission settings; SDK Data is intentionally the only storage used by SaveSyncPolicy::Remote.

CrazyGames can switch the SDK Data profile underneath a running page. Its auth notification therefore advances the SaveSystem remote profile generation, invalidating all slot handles opened before the switch. The game is responsible for opening the active slot and deciding how to hand off live progress.

Banner requests use a DOM container supplied by the matching shell. The bundled CrazyGames shell includes crazygames-banner; a custom container ID must name an element that already exists. Choose one of the five SDK-supported sizes:

auto* portal = context()->webPortalAPI();
portal->showBanner(
    "crazygames-banner",
    snowpulse::WebBannerSize::Mobile320x50,
    [](const bool visible) {
        // Hide or replace the surrounding layout when no banner was filled.
    });

// Later, before removing or covering the container:
portal->hideBanner();

Leaderboard728x90, Medium300x250, Mobile320x50, Main468x60, and LargeMobile320x100 map to their dimensions. An accepted C++ request returns true; its callback reports whether the asynchronous SDK request succeeded. The shell refreshes a displayed banner after 35 seconds, defers refreshes while the document is hidden or a video ad is active, clears the old container when a different banner replaces it, and stops on terminal SDK errors. Destruction of the adapter hides its active banner.

Account switches and an explicit recovery reload are available when an application needs to reopen account-scoped state:

portal->setAccountChangedCallback([this] {
    reopenRemoteProfile();
});

if (!reopenRemoteProfile()) {
    portal->reloadGame();
}

Treat reload as a last-resort recovery action: it discards the current page state. See Save System for profile-generation handling.

Select WEB_PLATFORM GOOGLE_ADS or WEB_PLATFORM MRAID. Both presets use the GoogleAds adapter, which implements exitToCTA() through the shared SnowpulsePlayableAdsBridge; initialization, loading, and gameplay calls are successful no-ops, while interstitial and rewarded methods return false.

context()->webPortalAPI()->exitToCTA([](const bool opened) {
    // opened reports whether ExitApi, MRAID, or the configured fallback opened.
});

The Google Ads shell loads Google's Exit API. The MRAID shell prefers mraid.open(), then tries common Exit API globals and a normal browser window as fallbacks. Supply CTA_URL while configuring the shell or set window.SNOWPULSE_CTA_URL before the bridge initializes.

Playgama Bridge SDK v2

The Playgama shell loads the official v2 stable CDN and lets Bridge own the loading overlay. Emscripten download progress is sent through setGameLoadingProgress(), and game_ready is queued until Bridge and the Emscripten runtime are both ready.

Playgama moderation builds must use remote saves. WEB_PLATFORM PLAYGAMA with WEB_SAVE_POLICY AUTO selects the adapter and remote policy before App::init() initializes the save system:

class GameApp final : public snowpulse::App {
public:
    GameApp()
        : App(snowpulse::target_config::makeAppConfig()) {}

    bool init() override {
        if (!App::init()) {
            return false;
        }

        auto* portal = context()->webPortalAPI();
        portal->initialize([portal](const bool success) {
            if (!success) {
                return;
            }
            const std::string language = portal->platformLanguage();
            // Apply the portal language to the game's localization system.
        });
        return true;
    }
};

SaveSyncPolicy::Remote routes save JSON through Bridge storage. Snowpulse preserves the raw JSON string and namespaces sanitized keys as snowpulse.<bundle-prefix>.<slot>. A missing Bridge value maps to NotFound, an operation rejection maps to Failed, and unavailable or uninitialized Bridge storage maps to Unavailable. The provider also reports bridge.player.isAuthorized through isLoggedIn() when that value is available. Selecting Playgama with SaveSyncPolicy::Local remains supported for backward compatibility but emits a diagnostic and is not moderation compliant.

The adapter supports initialization, portal language, game_ready, gameplay_started, gameplay_stopped, initial and live audio/pause state, interstitial ads, rewarded ads, and Bridge storage. Portal pause suspends scene activation and gameplay/component/UI/physics/effect updates while save and audio servicing continue. A playing video is paused and resumes only when it was paused by the portal. Interstitial completion succeeds on closed; rewarded completion succeeds only after the rewarded state.

Bridge optionally reads playgama-bridge-config.json from the output root. Include it with the emitted HTML/JavaScript/Wasm files when platform or advertisement settings need customization. Banners, payments, authorization UI, leaderboards, achievements, and other optional Bridge modules are not exposed through IWebPortalAPI.

See Playgama's official Bridge setup, required API steps, platform API, storage API, and advertisement API.

YouTube Playables SDK

The YouTube shell loads https://www.youtube.com/game_api/v1 as the first script, before the Emscripten runtime or any other game code. It reports firstFrameReady() once while the Snowpulse loading overlay is visible. Calling gameLoadingFinished() fades out and removes that overlay, then reports gameReady() once on the following browser frame. Games must wait until save hydration and interactable scene/UI setup are complete, and should call it only after the first ready scene frame has been rendered.

The YOUTUBE preset keeps Emscripten's HTML minifier disabled and forces the output name to index. Release minification removes the quotes around the SDK src, which some certification scanners do not recognize even though browsers accept the resulting HTML:

snowpulse_add_game(mygame
    SOURCES ${MYGAME_SOURCES}
    DISPLAY_NAME "My Game"
    BUNDLE_ID "com.example.mygame"
    ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets"
    ASSIMP OFF
    SPINE_VERSION "4.2"
    WEB_MODE GAME
    WEB_PLATFORM YOUTUBE
    WEB_SAVE_POLICY AUTO
    WEB_OUTPUT_NAME "index"
)

The preset selects the portal and remote policy before App::init():

class GameApp final : public snowpulse::App {
public:
    GameApp()
        : App(snowpulse::target_config::makeAppConfig()) {}
};

CloudSaveProviderYouTube calls ytgame.game.loadData() before exposing any slots or accepting writes, and retries initialization after temporary failure. It stores all namespaced Snowpulse slots in the versioned envelope {"version":1,"slots":{"snowpulse.<bundle-prefix>.<slot>":"<raw-json>"}}. Writes and removals are serialized so a slower earlier saveData() cannot overwrite newer state. Unsupported or malformed envelopes make remote storage unavailable and are never overwritten. When ytgame.IN_PLAYABLES_ENV is false, the same envelope uses namespaced local storage for development only.

The adapter applies initial and live YouTube audio settings, language, and pause/resume events. YouTube portal pause freezes engine frame execution and rendering and uses its own audio mute source, so resume does not override a disabled YouTube audio preference. Interstitials map to commercialBreak(). YouTube rewarded ads require a stable, non-empty reward ID:

portal->rewardedBreak("double-coins", [](const bool rewardEarned) {
    if (rewardEarned) {
        grantDoubleCoins();
    }
});

The legacy rewarded overload without an ID returns false on YouTube. Snowpulse gameplay start/stop calls are successful no-ops because the YouTube SDK has no corresponding APIs. Optional score, content-opening, and health APIs are not currently exposed.

Package the generated index.html, JavaScript, Wasm, data, and local asset files together, preserving their relative paths. Validate the result with YouTube's official SDK Test Suite and review the SDK reference, integration requirements, and stability requirements before submission.

Before submitting a Poki build, check Poki's current requirements and external-resource policy. Keep the initial download within the current portal budget and package resources in the artifact unless Poki explicitly permits an external request.

Playable-Ad Web Builds

Use a dedicated build directory for the size-optimized playable-ad profile. CMake caches the selected profile during configuration, so building an older directory configured with the default FULL profile will retain the full engine and produce a much larger artifact.

Selecting WEB_MODE PLAYABLE_AD alone is not enough to remove unused engine systems. If the build directory uses the FULL profile, the target still creates playable-style packaging, but HTTP, saves, video, ImGui, MIDI, Effekseer, Spiktar, Poki, CrazyGames, Playgama, YouTube, and the other full-engine systems remain compiled. Maximum size optimization requires all three choices shown below: MinSizeRel, the PLAYABLE_AD profile, and WEB_MODE PLAYABLE_AD.

Example playable build for an application target named sampleapp:

emcmake cmake -S . -B cmake-build-playable-emscripten \
    -DCMAKE_BUILD_TYPE=MinSizeRel \
    -DSNOWPULSE_WEB_BUILD_PROFILE=PLAYABLE_AD \
    -DSNOWPULSE_WEB_UTF8_BINARY_ENCODING=OFF \
    -DSNOWPULSE_WEB_LAZY_ASSET_LOADING=OFF

cmake --build cmake-build-playable-emscripten \
    --target sampleapp -j4

The relevant configuration options are:

Option Recommended value Purpose
CMAKE_BUILD_TYPE MinSizeRel Optimizes the build for minimum size.
SNOWPULSE_WEB_BUILD_PROFILE PLAYABLE_AD Excludes engine systems that playable ads do not use. The project-wide default remains FULL.
SNOWPULSE_WEB_UTF8_BINARY_ENCODING OFF Uses compatibility-first base64 Wasm embedding. UTF-8 embedding is experimental and has failed on asset-heavy Spine builds.
SNOWPULSE_WEB_LAZY_ASSET_LOADING Start with OFF Enables manifest-driven boot and deferred asset packs. It affects asset staging, not core Wasm size.
SNOWPULSE_PLAYABLE_SIZE_BUDGET_BYTES 2000000 Sets the raw standalone HTML target. Exceeding it warns without failing the build.

PLAYABLE_AD sets SNOWPULSE_HAS_HTTP, SNOWPULSE_HAS_MIDI, SNOWPULSE_HAS_VIDEO, SNOWPULSE_HAS_SAVES, SNOWPULSE_HAS_IMGUI, and SNOWPULSE_HAS_EFFEKSEER to 0. It also removes non-Google portal adapters and uses direct WebGL 2/GLES exports instead of the full GLAD loader. The generic/default and Google Ads playable adapters remain available.

The post-build size report repeats these arguments, along with the effective Emscripten binary-encoding value and exact HTML byte count. Check that it says SNOWPULSE_WEB_BUILD_PROFILE=PLAYABLE_AD; a report showing FULL came from a full-engine build directory.

For the staged-asset workflow, manifest format, runtime request API, and single-HTML behavior, see Optional Web Asset Packs.

Assimp and 3D Playables

Assimp is selected per target and is independent of the web profile. Set ASSIMP ON in the game declaration to retain Snowpulse's FBX runtime path:

snowpulse_add_game(mygame
    SOURCES ${MYGAME_SOURCES}
    DISPLAY_NAME "My Game"
    BUNDLE_ID "com.example.mygame"
    ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets"
    ASSIMP ON
    SPINE_VERSION "4.2"
    WEB_MODE PLAYABLE_AD
    WEB_PLATFORM GOOGLE_ADS
)

Build it in its own playable directory:

emcmake cmake -S . -B cmake-build-playable-3d \
    -DCMAKE_BUILD_TYPE=MinSizeRel \
    -DSNOWPULSE_WEB_BUILD_PROFILE=PLAYABLE_AD \
    -DSNOWPULSE_WEB_UTF8_BINARY_ENCODING=OFF

cmake --build cmake-build-playable-3d --target mygame -j4

The web dependency configuration keeps only the FBX importer and disables Assimp tools, tests, samples, exporters, and other importers. Assimp and model assets still add real weight, so a sub-2 MB artifact is not guaranteed; the playable size report remains the source of truth and warns without failing.

Android

Android is a first-class GameActivity target for touch-first Android 9+ phones and tablets. It produces debug/release APKs and signed Play-ready AABs, keeps assets packaged behind AAssetManager, uses Android-native networking and video services, and ships arm64-only release binaries. See the dedicated Android guide for toolchain pins, project generation, Gradle commands, signing, artifact checks, lifecycle behavior, dependency policy, and the device test matrix.

iOS

  • Rendering via OpenGL ES 3.0 (<OpenGLES/ES3/gl.h> + EAGLContext)
  • Windowing via UIKit: a UIView subclass backed by CAEAGLLayer provides the default framebuffer
  • Frame loop driven by CADisplayLink (replaces the desktop while(!shouldClose) loop and the web emscripten_set_main_loop)
  • Input is touch-only: multi-touch is exposed via Input::touches(), and the primary touch is mirrored onto KeyCode::MouseLeft + mousePosition() so existing UI / mouse code keeps working unchanged
  • Audio via OpenAL Soft (vendored, cross-compiled for iOS)
  • HTTP via libcurl; the current iOS build is HTTP-only and does not consume the macOS SNOWPULSE_OPENSSL_ROOT prefix
  • Save store writes JSON files into the app's Documents directory (NSDocumentDirectory); no cloud provider by default (NullCloudSaveProvider)
  • Assets ship inside the .app bundle's Resources/assets/ directory; the engine sets assetRoot automatically via bundleResourcePath(). Tiled maps, tilesets, templates, and images retain their relative subdirectory layout.
  • MP4 overlay playback uses AVFoundation when SNOWPULSE_ENABLE_MP4_VIDEO=ON
  • The deployment target should be iOS 14.0 or newer

OpenGL ES on iOS

Apple deprecated OpenGL ES on iOS in iOS 12 but the current Snowpulse backend still targets OpenGL ES 3.0. The implementation reuses the GLES-compatible shader path used by WebGL 2 and defines GLES_SILENCE_DEPRECATION to suppress SDK warnings. A future renderer can be added behind IRenderer without changing game code.

Building for iOS

iOS builds use CMake's Xcode generator. From the repo root:

# Simulator (arm64, configure once)
cmake -G Xcode -B cmake-build-ios-sim \
    -DCMAKE_SYSTEM_NAME=iOS \
    -DCMAKE_OSX_SYSROOT=iphonesimulator \
    -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 \
    -DCMAKE_OSX_ARCHITECTURES=arm64

# Build all iOS app targets
cmake --build cmake-build-ios-sim --config Debug -- -sdk iphonesimulator

# Build one target only
cmake --build cmake-build-ios-sim --config Debug --target sampleapp -- -sdk iphonesimulator

# Device (arm64)
cmake -G Xcode -B cmake-build-ios \
    -DCMAKE_SYSTEM_NAME=iOS \
    -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 \
    -DCMAKE_OSX_ARCHITECTURES=arm64
cmake --build cmake-build-ios --config Release

Open the generated .xcodeproj to configure code signing for device builds. For a target named sampleapp, the generated bundle is typically under cmake-build-ios-sim/path/to/target/Debug-iphonesimulator/sampleapp.app.

To install and launch that bundle on a booted simulator:

SIM_ID=$(xcrun simctl list devices available | awk -F '[()]' '/iPhone/{print $2; exit}')
xcrun simctl boot "$SIM_ID" 2>/dev/null
APP_BUNDLE="cmake-build-ios-sim/path/to/target/Debug-iphonesimulator/sampleapp.app"
xcrun simctl install "$SIM_ID" "$APP_BUNDLE"
xcrun simctl launch "$SIM_ID" com.example.sampleapp

The root CMakeLists.txt skips the desktop-only particle and UI editor targets on iOS and adds the examples that currently opt into iOS app configuration. Consumer projects should use snowpulse_add_game() so the engine creates the bundle and applies the shared target settings.

Per-target iOS Notes

  • The engine uses std::filesystem, which Apple ships on iOS 13+ — keep CMAKE_OSX_DEPLOYMENT_TARGET at 14.0 or higher (the root CMakeLists.txt enforces this when CMAKE_SYSTEM_NAME=iOS).
  • The platform .mm sources are compiled with -fobjc-arc so they can use __weak references to UIKit views; .cc sources keep the default plain-C++ flags.
  • Snowpulse iOS packaging excludes .DS_Store and preserves asset subdirectories under Resources/assets.
  • Tiled references must remain relative to their declaring JSON source and use exact-case / paths. Do not replace them with bundle filesystem paths.

iOS App Bundle Configuration

Use the same declaration as desktop, web, and Android:

snowpulse_add_game(mygame
    SOURCES ${MYGAME_SOURCES}
    DISPLAY_NAME "My Game"
    BUNDLE_ID "com.example.mygame"
    ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets"
    ASSIMP OFF
    SPINE_VERSION "4.2"
    WEB_MODE GAME
    WEB_PLATFORM SNOWBLINK_PLAIN
    VERSION "1.0.0"
    BUILD "1"
)

The game helper configures src/platform/ios/Info.plist.in, sets bundle and device family attributes, and places resources beneath Resources/assets while preserving their directory structure.

Simulator Install Troubleshooting

If xcrun simctl install fails with missing or invalid CFBundleExecutable in its Info.plist, inspect the app bundle plist and verify CFBundleExecutable is non-empty and matches the executable file name:

APP_BUNDLE="cmake-build-ios-sim/path/to/target/Debug-iphonesimulator/sampleapp.app"
plutil -p "$APP_BUNDLE/Info.plist" | rg CFBundleExecutable
ls -la "$APP_BUNDLE"

Snowpulse's iOS plist template should use CMake's bundle executable token ${MACOSX_BUNDLE_EXECUTABLE_NAME} so this key is populated correctly during generation.

Cloud Provider Matrix

Portal Provider Status SaveSyncPolicy::Remote Behavior
Default Stub (NullCloudSaveProvider) Unavailable
Spiktar Stub (NullCloudSaveProvider) Unavailable
Poki Web localStorage (CloudSaveProviderPoki) Available when storage is available
CrazyGames SDK Data (CloudSaveProviderCrazyGames) Guest/local and signed-in account persistence; migrates the legacy local key once
GamePix Placeholder portal + stub cloud provider Unavailable
GoogleAds Stub (NullCloudSaveProvider) Unavailable
MetaAds Placeholder portal + stub cloud provider Unavailable
ApplovinAds Placeholder portal + stub cloud provider Unavailable
Playgama Bridge storage (CloudSaveProviderPlaygama) Available after Bridge initialization; preserves raw JSON strings
YouTube Versioned Playables envelope (CloudSaveProviderYouTube) Available after loadData() hydration; serialized through saveData()

Portal-specific cloud backends are implemented for Poki, CrazyGames, Playgama, and YouTube. The engine routes save I/O according to policy:

  • SaveSyncPolicy::Local: local store only.
  • SaveSyncPolicy::Remote: cloud provider only, no local fallback.

Remote startup readiness is released on initialization success, failure, or timeout. Unavailable providers are retried once per second, and queued remote operations resume after recovery. Login state is still reported by OnSaveCloudAvailabilityChanged but is not used as a readiness gate.

For Poki on web, remote save keys intentionally reuse the same localStorage key format as local saves: <persistentDataPath>.<sanitizedSlot>.

For Playgama on web, SaveSyncPolicy::Remote is required and keys use the Bridge-backed snowpulse.<sanitizedPersistentDataPath>.<sanitizedSlot> format.

For YouTube on web, SaveSyncPolicy::Remote is required. The same namespaced slot keys are entries in the single versioned Playables save envelope.

Sample Usage

context()->targetResolution(540, 960);

if (context()->isPortrait()) {
    // Choose portrait layout.
}