refactor: use small_rng for Emscripten only, simplify platform to top-level functions only, no trait/struct

This commit is contained in:
Ryan Walters
2025-09-03 11:11:04 -05:00
parent 208ad3e733
commit 4cc5816d1f
9 changed files with 256 additions and 251 deletions

40
Cargo.lock generated
View File

@@ -722,6 +722,15 @@ dependencies = [
"portable-atomic", "portable-atomic",
] ]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]] [[package]]
name = "pretty_assertions" name = "pretty_assertions"
version = "1.4.1" version = "1.4.1"
@@ -762,6 +771,17 @@ version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [ dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core", "rand_core",
] ]
@@ -1529,3 +1549,23 @@ name = "yansi"
version = "1.0.1" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "zerocopy"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@@ -25,7 +25,6 @@ tracing-error = "0.2.0"
tracing-subscriber = {version = "0.3.20", features = ["env-filter"]} tracing-subscriber = {version = "0.3.20", features = ["env-filter"]}
thiserror = "2.0.16" thiserror = "2.0.16"
anyhow = "1.0" anyhow = "1.0"
rand = { version = "0.9.2", default-features = false, features = ["small_rng", "os_rng"] }
smallvec = "1.15.1" smallvec = "1.15.1"
bitflags = "2.9.4" bitflags = "2.9.4"
micromap = "0.1.0" micromap = "0.1.0"
@@ -48,6 +47,7 @@ windows-sys = { version = "0.60.2", features = ["Win32_System_Console"] }
[target.'cfg(not(target_os = "emscripten"))'.dependencies] [target.'cfg(not(target_os = "emscripten"))'.dependencies]
# On desktop platforms, build SDL2 with cargo-vcpkg # On desktop platforms, build SDL2 with cargo-vcpkg
sdl2 = { version = "0.38", default-features = false, features = ["image", "ttf", "gfx", "mixer", "unsafe_textures", "static-link", "use-vcpkg"] } sdl2 = { version = "0.38", default-features = false, features = ["image", "ttf", "gfx", "mixer", "unsafe_textures", "static-link", "use-vcpkg"] }
rand = { version = "0.9.2", default-features = false, features = ["thread_rng"] }
spin_sleep = "1.3.2" spin_sleep = "1.3.2"
# Browser-specific dependencies # Browser-specific dependencies
@@ -55,6 +55,7 @@ spin_sleep = "1.3.2"
# On Emscripten, we don't use cargo-vcpkg # On Emscripten, we don't use cargo-vcpkg
sdl2 = { version = "0.38", default-features = false, features = ["image", "ttf", "gfx", "mixer", "unsafe_textures"] } sdl2 = { version = "0.38", default-features = false, features = ["image", "ttf", "gfx", "mixer", "unsafe_textures"] }
# TODO: Document why Emscripten cannot use `os_rng`. # TODO: Document why Emscripten cannot use `os_rng`.
rand = { version = "0.9.2", default-features = false, features = ["small_rng", "os_rng"] }
libc = "0.2.175" # TODO: Describe why this is required. libc = "0.2.175" # TODO: Describe why this is required.
[dev-dependencies] [dev-dependencies]

View File

@@ -4,7 +4,7 @@ use crate::error::{GameError, GameResult};
use crate::constants::{CANVAS_SIZE, LOOP_TIME, SCALE}; use crate::constants::{CANVAS_SIZE, LOOP_TIME, SCALE};
use crate::game::Game; use crate::game::Game;
use crate::platform::get_platform; use crate::platform;
use sdl2::{AudioSubsystem, Sdl}; use sdl2::{AudioSubsystem, Sdl};
/// Main application wrapper that manages SDL initialization, window lifecycle, and the game loop. /// Main application wrapper that manages SDL initialization, window lifecycle, and the game loop.
@@ -101,7 +101,7 @@ impl App {
if start.elapsed() < LOOP_TIME { if start.elapsed() < LOOP_TIME {
let time = LOOP_TIME.saturating_sub(start.elapsed()); let time = LOOP_TIME.saturating_sub(start.elapsed());
if time != Duration::ZERO { if time != Duration::ZERO {
get_platform().sleep(time, self.focused); platform::sleep(time, self.focused);
} }
} }

View File

@@ -44,7 +44,7 @@ impl Asset {
mod imp { mod imp {
use super::*; use super::*;
use crate::error::AssetError; use crate::error::AssetError;
use crate::platform::get_platform; use crate::platform;
/// Loads asset bytes using the appropriate platform-specific method. /// Loads asset bytes using the appropriate platform-specific method.
/// ///
@@ -58,7 +58,7 @@ mod imp {
/// Returns `AssetError::NotFound` if the asset file cannot be located (Emscripten only), /// Returns `AssetError::NotFound` if the asset file cannot be located (Emscripten only),
/// or `AssetError::Io` for filesystem I/O failures. /// or `AssetError::Io` for filesystem I/O failures.
pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> { pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
get_platform().get_asset_bytes(asset) platform::get_asset_bytes(asset)
} }
} }

View File

@@ -22,13 +22,12 @@ mod texture;
/// This function initializes SDL, the window, the game state, and then enters /// This function initializes SDL, the window, the game state, and then enters
/// the main game loop. /// the main game loop.
pub fn main() { pub fn main() {
let platform = platform::get_platform(); if platform::requires_console() {
if platform.requires_console() {
// Setup buffered tracing subscriber that will buffer logs until console is ready // Setup buffered tracing subscriber that will buffer logs until console is ready
let switchable_writer = platform::tracing_buffer::setup_switchable_subscriber(); let switchable_writer = platform::tracing_buffer::setup_switchable_subscriber();
// Initialize platform-specific console // Initialize platform-specific console
platform.init_console().expect("Could not initialize console"); platform::init_console().expect("Could not initialize console");
// Now that console is initialized, flush buffered logs and switch to direct output // Now that console is initialized, flush buffered logs and switch to direct output
debug!("Switching to direct logging mode and flushing buffer..."); debug!("Switching to direct logging mode and flushing buffer...");

View File

@@ -1,17 +1,15 @@
//! Desktop platform implementation. //! Desktop platform implementation.
use std::borrow::Cow; use std::borrow::Cow;
use std::time::Duration; use std::time::{Duration, Instant};
use rand::rngs::ThreadRng;
use crate::asset::Asset; use crate::asset::Asset;
use crate::error::{AssetError, PlatformError}; use crate::error::{AssetError, PlatformError};
use crate::platform::CommonPlatform;
/// Desktop platform implementation. /// Desktop platform implementation.
pub struct Platform; pub fn sleep(duration: Duration, focused: bool) {
impl CommonPlatform for Platform {
fn sleep(&self, duration: Duration, focused: bool) {
if focused { if focused {
spin_sleep::sleep(duration); spin_sleep::sleep(duration);
} else { } else {
@@ -19,11 +17,11 @@ impl CommonPlatform for Platform {
} }
} }
fn get_time(&self) -> f64 { pub fn get_time() -> f64 {
std::time::Instant::now().elapsed().as_secs_f64() Instant::now().elapsed().as_secs_f64()
} }
fn init_console(&self) -> Result<(), PlatformError> { pub fn init_console() -> Result<(), PlatformError> {
#[cfg(windows)] #[cfg(windows)]
{ {
use tracing::{debug, info}; use tracing::{debug, info};
@@ -37,13 +35,13 @@ impl CommonPlatform for Platform {
debug!("No existing console window found"); debug!("No existing console window found");
} }
if let Some(file_type) = Self::is_output_setup()? { if let Some(file_type) = is_output_setup()? {
debug!(r#type = file_type, "Existing output detected"); debug!(r#type = file_type, "Existing output detected");
} else { } else {
debug!("No existing output detected"); debug!("No existing output detected");
// Try to attach to parent console for direct cargo run // Try to attach to parent console for direct cargo run
Self::attach_to_parent_console()?; attach_to_parent_console()?;
info!("Successfully attached to parent console"); info!("Successfully attached to parent console");
} }
} }
@@ -51,15 +49,15 @@ impl CommonPlatform for Platform {
Ok(()) Ok(())
} }
fn requires_console(&self) -> bool { pub fn requires_console() -> bool {
cfg!(windows) cfg!(windows)
} }
fn get_canvas_size(&self) -> Option<(u32, u32)> { pub fn get_canvas_size() -> Option<(u32, u32)> {
None // Desktop doesn't need this None // Desktop doesn't need this
} }
fn get_asset_bytes(&self, asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> { pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
match asset { match asset {
Asset::Wav1 => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/waka/1.ogg"))), Asset::Wav1 => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/waka/1.ogg"))),
Asset::Wav2 => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/waka/2.ogg"))), Asset::Wav2 => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/waka/2.ogg"))),
@@ -69,11 +67,16 @@ impl CommonPlatform for Platform {
Asset::Font => Ok(Cow::Borrowed(include_bytes!("../../assets/game/TerminalVector.ttf"))), Asset::Font => Ok(Cow::Borrowed(include_bytes!("../../assets/game/TerminalVector.ttf"))),
} }
} }
pub fn rng() -> ThreadRng {
rand::rng()
} }
#[cfg(windows)] /* Internal functions */
impl Platform {
/// Check if the output stream has been setup by a parent process /// Check if the output stream has been setup by a parent process
/// Windows-only
#[cfg(windows)]
fn is_output_setup() -> Result<Option<&'static str>, PlatformError> { fn is_output_setup() -> Result<Option<&'static str>, PlatformError> {
use tracing::{debug, warn}; use tracing::{debug, warn};
@@ -117,6 +120,8 @@ impl Platform {
} }
/// Try to attach to parent console /// Try to attach to parent console
/// Windows-only
#[cfg(windows)]
fn attach_to_parent_console() -> Result<(), PlatformError> { fn attach_to_parent_console() -> Result<(), PlatformError> {
use windows::{ use windows::{
core::PCSTR, core::PCSTR,
@@ -170,4 +175,3 @@ impl Platform {
Ok(()) Ok(())
} }
}

View File

@@ -5,35 +5,47 @@ use std::time::Duration;
use crate::asset::Asset; use crate::asset::Asset;
use crate::error::{AssetError, PlatformError}; use crate::error::{AssetError, PlatformError};
use crate::platform::CommonPlatform; use rand::{rngs::SmallRng, SeedableRng};
/// Emscripten platform implementation. // Emscripten FFI functions
pub struct Platform; extern "C" {
fn emscripten_get_now() -> f64;
fn emscripten_sleep(ms: u32);
fn emscripten_get_element_css_size(target: *const u8, width: *mut f64, height: *mut f64) -> i32;
}
impl CommonPlatform for Platform { pub fn sleep(duration: Duration, _focused: bool) {
fn sleep(&self, duration: Duration, _focused: bool) {
unsafe { unsafe {
emscripten_sleep(duration.as_millis() as u32); emscripten_sleep(duration.as_millis() as u32);
} }
} }
fn get_time(&self) -> f64 { pub fn get_time() -> f64 {
unsafe { emscripten_get_now() } unsafe { emscripten_get_now() }
} }
fn init_console(&self) -> Result<(), PlatformError> { pub fn init_console() -> Result<(), PlatformError> {
Ok(()) // No-op for Emscripten Ok(()) // No-op for Emscripten
} }
fn requires_console(&self) -> bool { pub fn requires_console() -> bool {
false false
} }
fn get_canvas_size(&self) -> Option<(u32, u32)> { pub fn get_canvas_size() -> Option<(u32, u32)> {
Some(unsafe { get_canvas_size() }) let mut width = 0.0;
let mut height = 0.0;
unsafe {
emscripten_get_element_css_size(c"canvas".as_ptr().cast(), &mut width, &mut height);
if width == 0.0 || height == 0.0 {
return None;
}
}
Some((width as u32, height as u32))
} }
fn get_asset_bytes(&self, asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> { pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
use sdl2::rwops::RWops; use sdl2::rwops::RWops;
use std::io::Read; use std::io::Read;
@@ -49,18 +61,7 @@ impl CommonPlatform for Platform {
Ok(Cow::Owned(buf)) Ok(Cow::Owned(buf))
} }
}
// Emscripten FFI functions pub fn rng() -> SmallRng {
extern "C" { SmallRng::from_os_rng()
fn emscripten_get_now() -> f64;
fn emscripten_sleep(ms: u32);
fn emscripten_get_element_css_size(target: *const u8, width: *mut f64, height: *mut f64) -> i32;
}
unsafe fn get_canvas_size() -> (u32, u32) {
let mut width = 0.0;
let mut height = 0.0;
emscripten_get_element_css_size(c"canvas".as_ptr().cast(), &mut width, &mut height);
(width as u32, height as u32)
} }

View File

@@ -1,52 +1,13 @@
//! Platform abstraction layer for cross-platform functionality. //! Platform abstraction layer for cross-platform functionality.
use crate::asset::Asset;
use crate::error::{AssetError, PlatformError};
use std::borrow::Cow;
use std::time::Duration;
#[cfg(not(target_os = "emscripten"))]
mod desktop;
#[cfg(target_os = "emscripten")]
mod emscripten;
pub mod buffered_writer; pub mod buffered_writer;
pub mod tracing_buffer; pub mod tracing_buffer;
/// Cross-platform abstraction layer providing unified APIs for platform-specific operations.
pub trait CommonPlatform {
/// Platform-specific sleep function (required due to Emscripten's non-standard sleep requirements).
///
/// Provides access to current window focus state, useful for changing sleep algorithm conditionally.
fn sleep(&self, duration: Duration, focused: bool);
#[allow(dead_code)]
fn get_time(&self) -> f64;
/// Configures platform-specific console and debugging output capabilities.
fn init_console(&self) -> Result<(), PlatformError>;
/// Retrieves the actual display canvas dimensions.
#[allow(dead_code)]
fn get_canvas_size(&self) -> Option<(u32, u32)>;
/// Loads raw asset data using the appropriate platform-specific method.
fn get_asset_bytes(&self, asset: Asset) -> Result<Cow<'static, [u8]>, AssetError>;
/// Whether the platform requires a console to be initialized.
fn requires_console(&self) -> bool;
}
/// Returns the appropriate platform implementation based on compile-time target.
#[allow(dead_code)]
pub fn get_platform() -> &'static dyn CommonPlatform {
#[cfg(not(target_os = "emscripten"))] #[cfg(not(target_os = "emscripten"))]
{ mod desktop;
&desktop::Platform #[cfg(not(target_os = "emscripten"))]
} pub use desktop::*;
#[cfg(target_os = "emscripten")] #[cfg(target_os = "emscripten")]
{ pub use emscripten::*;
&emscripten::Platform #[cfg(target_os = "emscripten")]
} mod emscripten;
}

View File

@@ -1,3 +1,4 @@
use crate::platform;
use crate::systems::components::{DirectionalAnimation, Frozen, GhostAnimation, GhostState, LastAnimationState, LinearAnimation}; use crate::systems::components::{DirectionalAnimation, Frozen, GhostAnimation, GhostState, LastAnimationState, LinearAnimation};
use crate::{ use crate::{
map::{ map::{
@@ -14,9 +15,7 @@ use crate::{
use crate::systems::GhostAnimations; use crate::systems::GhostAnimations;
use bevy_ecs::query::Without; use bevy_ecs::query::Without;
use bevy_ecs::system::{Commands, Query, Res}; use bevy_ecs::system::{Commands, Query, Res};
use rand::rngs::SmallRng;
use rand::seq::IndexedRandom; use rand::seq::IndexedRandom;
use rand::SeedableRng;
use smallvec::SmallVec; use smallvec::SmallVec;
/// Autonomous ghost AI system implementing randomized movement with backtracking avoidance. /// Autonomous ghost AI system implementing randomized movement with backtracking avoidance.
@@ -49,7 +48,7 @@ pub fn ghost_movement_system(
break; break;
} }
} else { } else {
*non_opposite_options.choose(&mut SmallRng::from_os_rng()).unwrap() *non_opposite_options.choose(&mut platform::rng()).unwrap()
}; };
velocity.direction = new_edge.direction; velocity.direction = new_edge.direction;