Skip to main content

gasm_host/
session.rs

1//! A game ready to start: the module, its assets, parameters, storage and load
2//! options. [`crate::headless::run`] and `window::run` (feature `window`) drive it.
3
4use std::collections::HashMap;
5use std::time::Instant;
6
7use crate::assets::Assets;
8use crate::audio::AudioOut;
9use crate::angle::Angle;
10use crate::gfx::Gfx;
11use crate::host::{Game, Host, LoadOptions, Stop};
12use crate::net::Net;
13use crate::storage::Storage;
14
15pub struct Session {
16    /// shown in logs and the window title
17    pub name: String,
18    /// a .wasm module, or a .cwasm if `load.allow_precompiled`
19    pub wasm: Vec<u8>,
20    pub assets: Assets,
21    pub params: HashMap<String, String>,
22    pub allow_net: bool,
23    /// with `allow_net`: only these hosts (`--allow-net=a.org,*.b.org`; empty: all)
24    pub allow_hosts: Vec<String>,
25    /// gasm:fetch: live, recording or replaying responses (headless)
26    pub fetch: crate::fetch::FetchMode,
27    /// `--app-id`: the game's identity in gasm:fetch's User-Agent
28    pub app_id: Option<String>,
29    /// gasm:files: where the game's saves for the player go
30    pub save: crate::files::SaveTarget,
31    /// the window runner: ask the player about hosts and saves the command line didn't allow
32    pub consent: Option<crate::consent::Consent>,
33    /// the game's capabilities manifest (embedded, asset `gasm.manifest` or `--manifest`)
34    pub manifest: Option<crate::manifest::Manifest>,
35    /// mounted mods whose manifests ask for hosts: (file, hosts); each needs the
36    /// player's yes (or hosts --allow-net covers), else it's unmounted before the game starts
37    pub mod_requests: Vec<(String, Vec<String>)>,
38    pub storage: Storage,
39    pub load: LoadOptions,
40    /// where ANGLE is (gasm:gl games): None searches the usual places (`angle::find`)
41    pub gl_lib: Option<std::path::PathBuf>,
42    /// gasm:gl on SwiftShader (software Vulkan) instead of the GPU
43    pub gl_software: bool,
44    /// `--gl-stats`: count gasm:gl calls and print them when the game ends
45    pub gl_stats: bool,
46    /// `--profile <out.json>`: the main thread's guest stacks (`host::Profile`)
47    pub profile: Option<std::path::PathBuf>,
48    /// gasm:media: `--allow-media` (the manifest's "media" and the player's consent are
49    /// added when the game starts)
50    pub media: crate::media::MediaPolicy,
51    /// a window that can go fullscreen (the window runner), and whether it starts so:
52    /// what set_fullscreen and is_fullscreen answer from the first call (gasm_init's too)
53    pub fullscreen: Option<bool>,
54    /// `--watch-asset name=path`: files re-read as assets whenever they change
55    /// (create each watch before opening its file, so no change is missed)
56    pub watch_assets: Vec<crate::assets::AssetWatch>,
57}
58
59impl Session {
60    /// Whether the game imports gasm:gl (it then needs ANGLE to draw).
61    pub fn uses_gl(&self) -> bool {
62        crate::host::imports_module(&self.wasm, "gasm:gl", self.load.allow_precompiled)
63    }
64
65    /// ANGLE for this session's gasm:gl game: offscreen at `w`×`h`, or in a window.
66    pub fn open_gl(&self, window: Option<crate::angle::NativeWindow>, size: (u32, u32)) -> Result<Angle, String> {
67        let dir = crate::angle::find(self.gl_lib.as_deref())?;
68        let open = |software| match window {
69            Some(w) => Angle::window(&dir, w, software),
70            None => Angle::offscreen(&dir, size.0, size.1, software),
71        };
72        match open(self.gl_software) {
73            // no usable GPU (or driver): SwiftShader
74            Err(e) if !self.gl_software => {
75                eprintln!("[gasm] gl: {e}; trying SwiftShader (software)");
76                open(true)
77            }
78            r => r,
79        }
80    }
81
82    /// Instantiate the guest and run its init. `reproducible`: headless rules
83    /// (virtual time, fixed random sequence, hashing). `gl`: the GL that executes a
84    /// gasm:gl guest's calls (None: the null GL).
85    /// Compile the module on a background thread (the window runner shows the splash
86    /// meanwhile); hand the result to [`Session::start_compiled`].
87    pub fn compile_in_background(&self) -> std::thread::JoinHandle<Result<wasmtime::Module, Stop>> {
88        let (wasm, allow) = (self.wasm.clone(), self.load.allow_precompiled);
89        std::thread::spawn(move || Game::compile(&wasm, allow))
90    }
91
92    pub fn start(self, audio: Option<Box<dyn AudioOut>>, gfx: Gfx, gl: Option<Angle>, reproducible: bool, hashing: bool) -> Result<Game, Stop> {
93        self.start_with(None, audio, gfx, gl, reproducible, hashing)
94    }
95
96    /// Mods that ask for hosts: kept if --allow-net covers them or the player said yes
97    /// (asked before the game starts), else unmounted and listed in `mods.refused`.
98    fn decide_mods(&mut self, policy: &crate::net::NetPolicy) {
99        for (file, hosts) in std::mem::take(&mut self.mod_requests) {
100            let covered = policy.allowed && hosts.iter().all(|h| policy.permits(h));
101            let said = || self.consent.as_ref().and_then(|c| c.lock().unwrap().check_mod(&file, &hosts));
102            if covered || said() == Some(true) {
103                eprintln!("[gasm] mods: {file} may connect to {}", hosts.join(", "));
104                continue;
105            }
106            let why = match &self.consent {
107                Some(_) => format!("it connects to {} and the player said no", hosts.join(", ")),
108                None => format!("it connects to {}, which --allow-net doesn't cover", hosts.join(", ")),
109            };
110            eprintln!("[gasm] mods: refused {file} ({why})");
111            self.assets.refuse_mod(&file, &why);
112        }
113    }
114
115    /// [`Session::start`] with a module compiled earlier (`compile_in_background`).
116    pub fn start_compiled(self, module: wasmtime::Module, audio: Option<Box<dyn AudioOut>>, gfx: Gfx, gl: Option<Angle>) -> Result<Game, Stop> {
117        self.start_with(Some(module), audio, gfx, gl, false, false)
118    }
119
120    fn start_with(mut self, module: Option<wasmtime::Module>, audio: Option<Box<dyn AudioOut>>, gfx: Gfx, gl: Option<Angle>, reproducible: bool, hashing: bool) -> Result<Game, Stop> {
121        let policy = crate::net::NetPolicy::new(self.allow_net, self.allow_hosts.clone()).with_consent(self.consent.clone());
122        self.decide_mods(&policy);
123        // gasm:media: streams mixed into the audio output's own bus (a silent one without)
124        let bus = audio.as_ref().and_then(|a| a.media_bus());
125        let mut media = self.media.clone();
126        media.declared = self.manifest.as_ref().is_some_and(|m| m.media);
127        media.consent = self.consent.clone();
128        let mut host = Host::new(self.assets, self.params, audio, gfx, Net::with_policy(policy.clone()), self.storage);
129        host.media = crate::media::Media::new(media);
130        host.media.set_output(bus);
131        host.requires = self.manifest.map(|m| m.requires).unwrap_or_default();
132        host.fetch = crate::fetch::Fetch::new(policy, self.fetch);
133        host.fetch.set_app_id(self.app_id.as_deref());
134        host.files = crate::files::Files::new(self.save).with_consent(self.consent.clone());
135        if self.gl_stats {
136            host.gl.stats = Some(Default::default());
137        }
138        if let Some(out) = self.profile.clone() {
139            host.profile = Some(crate::host::Profile::new(out));
140        }
141        if let Some(on) = self.fullscreen {
142            host.fullscreen_capable = true;
143            host.fullscreen = on;
144        }
145        if let Some(a) = gl {
146            eprintln!("[gasm] gl: {}", a.renderer);
147            host.gl.attach(a);
148        }
149        if reproducible {
150            host.set_reproducible();
151        }
152        host.hashing = hashing;
153        let t0 = Instant::now();
154        let module = match module {
155            Some(m) => m,
156            None => Game::compile(&self.wasm, self.load.allow_precompiled)?,
157        };
158        let mut game = Game::load_module(module, host, self.load)?;
159        for w in self.watch_assets {
160            game.add_watch(w);
161        }
162        eprintln!("[gasm] loaded {} in {:.0} ms", self.name, t0.elapsed().as_secs_f64() * 1000.0);
163        Ok(game)
164    }
165}