1use arrayvec::ArrayVec;
6use platform::{
7 thread_pool::ThreadPool, EngineCallbacks, Event, Instant, Platform, AUDIO_SAMPLE_RATE,
8};
9
10use crate::{
11 allocators::LinearAllocator,
12 input::{EventQueue, QueuedEvent},
13 mixer::Mixer,
14 multithreading::{self, parallelize},
15 resources::{FileReader, ResourceDatabase, ResourceLoader},
16};
17
18#[derive(Clone, Copy)]
26pub struct EngineLimits {
27 pub frame_arena_size: usize,
33 pub resource_database_loaded_chunks_count: u32,
40 pub resource_database_loaded_sprite_chunks_count: u32,
56 pub resource_database_read_queue_capacity: usize,
63 pub resource_database_buffer_size: usize,
70 pub audio_channel_count: usize,
81 pub audio_concurrent_sounds_count: usize,
88 pub audio_window_length: usize,
102}
103
104impl EngineLimits {
105 pub const DEFAULT: EngineLimits = EngineLimits {
107 frame_arena_size: 8 * 1024 * 1024,
108 resource_database_loaded_chunks_count: 128,
109 resource_database_loaded_sprite_chunks_count: 512,
110 resource_database_read_queue_capacity: 128,
111 resource_database_buffer_size: 8 * 1024 * 1024,
112 audio_channel_count: 1,
113 audio_concurrent_sounds_count: 64,
114 audio_window_length: (AUDIO_SAMPLE_RATE / 2) as usize,
115 };
116}
117
118impl Default for EngineLimits {
119 fn default() -> Self {
120 EngineLimits::DEFAULT
121 }
122}
123
124pub struct Engine<'a> {
127 pub resource_db: ResourceDatabase,
129 pub resource_loader: ResourceLoader,
132 pub frame_arena: LinearAllocator<'a>,
135 pub thread_pool: ThreadPool,
137 pub audio_mixer: Mixer,
139 pub event_queue: EventQueue,
144}
145
146impl Engine<'_> {
147 pub fn new(
157 platform: &dyn Platform,
158 arena: &'static LinearAllocator,
159 limits: EngineLimits,
160 ) -> Self {
161 profiling::function_scope!();
162 let mut thread_pool = multithreading::create_thread_pool(arena, platform, 1)
163 .expect("engine arena should have enough memory for the thread pool");
164
165 let dummy_slice = &mut [(); 1024][..thread_pool.thread_count()];
167 parallelize(&mut thread_pool, dummy_slice, |_, _| {
168 profiling::register_thread!("engine thread pool");
169 });
170 profiling::register_thread!("engine main");
171
172 let frame_arena = LinearAllocator::new(arena, limits.frame_arena_size)
173 .expect("should have enough memory for the frame arena");
174
175 let db_file = platform
176 .open_file("resources.db")
177 .expect("resources.db should exist and be readable");
178
179 let mut res_reader = FileReader::new(
180 arena,
181 db_file,
182 limits.resource_database_buffer_size,
183 limits.resource_database_read_queue_capacity,
184 )
185 .expect("engine arena should have enough memory for the resource db file reader");
186
187 let resource_db = ResourceDatabase::new(
188 platform,
189 arena,
190 &mut res_reader,
191 limits.resource_database_loaded_chunks_count,
192 limits.resource_database_loaded_sprite_chunks_count,
193 )
194 .expect("engine arena should have enough memory for the resource database");
195
196 let resource_loader = ResourceLoader::new(arena, res_reader, &resource_db)
197 .expect("engine arena should have enough memory for the resource loader");
198
199 let audio_mixer = Mixer::new(
200 arena,
201 limits.audio_channel_count,
202 limits.audio_concurrent_sounds_count,
203 limits.audio_window_length,
204 )
205 .expect("engine arena should have enough memory for the audio mixer");
206
207 Engine {
208 resource_db,
209 resource_loader,
210 frame_arena,
211 audio_mixer,
212 thread_pool,
213 event_queue: ArrayVec::new(),
214 }
215 }
216}
217
218impl EngineCallbacks for Engine<'_> {
219 fn run_frame(
220 &mut self,
221 platform: &dyn Platform,
222 run_game_frame: &mut dyn FnMut(Instant, &dyn Platform, &mut Self),
223 ) {
224 profiling::function_scope!();
225
226 let timestamp = platform.now();
227 self.frame_arena.reset();
228 self.resource_loader
229 .finish_reads(&mut self.resource_db, platform, 128);
230 self.resource_db.chunks.increment_ages();
231 self.resource_db.sprite_chunks.increment_ages();
232 self.audio_mixer.update_audio_sync(timestamp, platform);
233
234 run_game_frame(timestamp, platform, self);
235
236 self.audio_mixer.render_audio(
237 &mut self.thread_pool,
238 platform,
239 &self.resource_db,
240 &mut self.resource_loader,
241 );
242 self.resource_loader.dispatch_reads(platform);
243 self.event_queue
244 .retain(|queued| !queued.timed_out(timestamp));
245
246 profiling::finish_frame!();
247 }
248
249 fn event(&mut self, event: Event, timestamp: Instant) {
250 profiling::function_scope!();
251 self.event_queue.push(QueuedEvent { event, timestamp });
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use platform::{
258 ActionCategory, Button, EngineCallbacks, Event, InputDevice, Instant, Platform,
259 };
260
261 use crate::{
262 allocators::LinearAllocator,
263 geom::Rect,
264 input::{ActionKind, ActionState, InputDeviceState},
265 renderer::DrawQueue,
266 resources::{audio_clip::AudioClipHandle, sprite::SpriteHandle, ResourceDatabase},
267 static_allocator,
268 test_platform::TestPlatform,
269 };
270
271 use super::{Engine, EngineLimits};
272
273 #[repr(usize)]
274 enum TestInput {
275 Act,
276 _Count,
277 }
278
279 struct SmokeTestGame {
280 test_input: InputDeviceState<{ TestInput::_Count as usize }>,
281 test_sprite: SpriteHandle,
282 test_audio: AudioClipHandle,
283 test_counter: u32,
284 }
285
286 impl SmokeTestGame {
287 fn new(device: InputDevice, button: Button, resources: &ResourceDatabase) -> Self {
288 let test_sprite = resources.find_sprite("player").unwrap();
289 let test_audio = resources.find_audio_clip("whack").unwrap();
290 let test_input = InputDeviceState {
291 device,
292 actions: [
293 ActionState {
295 kind: ActionKind::Instant,
296 mapping: Some(button),
297 disabled: false,
298 pressed: false,
299 },
300 ],
301 };
302 Self {
303 test_input,
304 test_sprite,
305 test_audio,
306 test_counter: 0,
307 }
308 }
309
310 fn run_frame(&mut self, _: Instant, platform: &dyn Platform, engine: &mut Engine) {
311 let scale_factor = platform.draw_scale_factor();
312 let mut draw_queue =
313 DrawQueue::new(&engine.frame_arena, 100_000, scale_factor).unwrap();
314
315 self.test_input.update(&mut engine.event_queue);
316 let action_test = self.test_input.actions[TestInput::Act as usize].pressed;
317
318 if action_test {
319 engine
320 .audio_mixer
321 .play_clip(0, self.test_audio, true, &engine.resource_db);
322 self.test_counter += 1;
323 }
324
325 let test_sprite = engine.resource_db.get_sprite(self.test_sprite);
326 let mut offset = 0.0;
327 for mip in 0..9 {
328 if self.test_counter % 9 > mip {
329 continue;
330 }
331 let scale = 1. / 2i32.pow(mip) as f32;
332 let w = 319.0 * scale;
333 let h = 400.0 * scale;
334 let draw_success = test_sprite.draw(
335 Rect::xywh(offset, 0.0, w, h),
336 0,
337 &mut draw_queue,
338 &engine.resource_db,
339 &mut engine.resource_loader,
340 );
341 assert!(draw_success);
342 offset += w + 20.0;
343 }
344
345 draw_queue.dispatch_draw(&engine.frame_arena, platform);
346 }
347 }
348
349 fn run_smoke_test(platform: &TestPlatform, persistent_arena: &'static LinearAllocator) {
352 let device = platform.input_devices()[0];
353 let button = platform
354 .default_button_for_action(ActionCategory::ActPrimary, device)
355 .unwrap();
356
357 let mut engine = Engine::new(
358 platform,
359 persistent_arena,
360 EngineLimits {
361 audio_window_length: 128,
362 ..EngineLimits::DEFAULT
363 },
364 );
365
366 let mut game = SmokeTestGame::new(device, button, &engine.resource_db);
367 let mut run_frame = |timestamp: Instant, platform: &dyn Platform, engine: &mut Engine| {
368 game.run_frame(timestamp, platform, engine);
369 };
370
371 let fps = 10;
372 for current_frame in 0..(4 * fps) {
373 platform.set_elapsed_millis(current_frame * 1000 / fps);
374
375 if 2 * fps < current_frame && current_frame < 3 * fps {
376 if current_frame % 3 == 0 {
378 engine.event(
379 if current_frame % 2 == 0 {
380 Event::DigitalInputPressed(device, button)
381 } else {
382 Event::DigitalInputReleased(device, button)
383 },
384 platform.now(),
385 );
386 }
387 }
388
389 engine.run_frame(platform, &mut run_frame);
390 }
391 }
392
393 #[test]
394 #[cfg(not(target_os = "emscripten"))]
395 fn smoke_test_multithreaded() {
396 static PERSISTENT_ARENA: &LinearAllocator = static_allocator!(64 * 1024 * 1024);
397 run_smoke_test(&TestPlatform::new(true), PERSISTENT_ARENA);
398 }
399
400 #[test]
401 #[ignore = "the emscripten target doesn't support multithreading"]
402 #[cfg(target_os = "emscripten")]
403 fn smoke_test_multithreaded() {}
404
405 #[test]
406 fn smoke_test_singlethreaded() {
407 static PERSISTENT_ARENA: &LinearAllocator = static_allocator!(64 * 1024 * 1024);
408 run_smoke_test(&TestPlatform::new(false), PERSISTENT_ARENA);
409 }
410}