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:
- Desktop development and production builds
- Web profiles, output modes, and portal integrations
- iOS simulator and device builds
- Android setup, builds, signing, and deployment
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/.tjreferences transitively and require each linked source to share its deferred pack or live inboot. - Mouse wheel capture is controlled by
AppConfig::consumeMouseWheel(set tofalseto 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 inFULL; 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, andgameplayStop- interstitial/commercial and rewarded breaks (including a reward-ID overload)
- banner display and removal through
showBannerandhideBanner 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_TYPEselects the normal compiler optimization level.SNOWPULSE_WEB_BUILD_PROFILEcontrols which engine systems are compiled.WEB_MODEcontrols whether Emscripten emits a multi-file game or standalone playable artifact.WEB_PLATFORMselects 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 game → FULL + multi-file Poki and playable →
PLAYABLE_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.
Google Ads and MRAID Playables¶
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
UIViewsubclass backed byCAEAGLLayerprovides the default framebuffer - Frame loop driven by
CADisplayLink(replaces the desktopwhile(!shouldClose)loop and the webemscripten_set_main_loop) - Input is touch-only: multi-touch is exposed via
Input::touches(), and the primary touch is mirrored ontoKeyCode::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_ROOTprefix - Save store writes JSON files into the app's
Documentsdirectory (NSDocumentDirectory); no cloud provider by default (NullCloudSaveProvider) - Assets ship inside the
.appbundle'sResources/assets/directory; the engine setsassetRootautomatically viabundleResourcePath(). 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+ — keepCMAKE_OSX_DEPLOYMENT_TARGETat 14.0 or higher (the rootCMakeLists.txtenforces this whenCMAKE_SYSTEM_NAME=iOS). - The platform
.mmsources are compiled with-fobjc-arcso they can use__weakreferences to UIKit views;.ccsources keep the default plain-C++ flags. - Snowpulse iOS packaging excludes
.DS_Storeand preserves asset subdirectories underResources/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.