diff --git a/core/Cargo.toml b/core/Cargo.toml index e89e41600f38..74bffe7dd2a5 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -37,7 +37,7 @@ thiserror = { workspace = true } chrono = { workspace = true, features = ["clock"] } web-time = "1.1.0" encoding_rs = { workspace = true } -rand = { version = "0.9.1", features = ["std", "small_rng", "os_rng"], default-features = false } +rand = { version = "0.9.1", features = ["std", "os_rng"], default-features = false } serde = { workspace = true } serde_json = { workspace = true, features = ["preserve_order"] } nellymoser-rs = { git = "https://github.com/ruffle-rs/nellymoser", rev = "073eb48d907201f46dea0c8feb4e8d9a1d92208c", optional = true } diff --git a/core/src/avm1/activation.rs b/core/src/avm1/activation.rs index 701d06b4690e..345a988ce2f9 100644 --- a/core/src/avm1/activation.rs +++ b/core/src/avm1/activation.rs @@ -18,7 +18,6 @@ use crate::vminterface::Instantiator; use crate::{avm_error, avm_warn}; use gc_arena::{Gc, Mutation}; use indexmap::IndexMap; -use rand::Rng; use ruffle_macros::istr; use std::cell::Cell; use std::cmp::min; @@ -1820,7 +1819,7 @@ impl<'a, 'gc> Activation<'a, 'gc> { // The max value is clamped to the range [0, 2^31 - 1). let max = self.context.avm1.pop().coerce_to_f64(self)? as i32; let result = if max > 0 { - self.context.rng.random_range(0..max) + self.context.rng.generate_random_number() % max } else { 0 }; diff --git a/core/src/avm1/globals/math.rs b/core/src/avm1/globals/math.rs index 27e139b38920..8f833bef4148 100644 --- a/core/src/avm1/globals/math.rs +++ b/core/src/avm1/globals/math.rs @@ -3,7 +3,6 @@ use crate::avm1::error::Error; use crate::avm1::property_decl::{DeclContext, Declaration}; use crate::avm1::{Object, Value}; -use rand::Rng; use std::f64::consts; macro_rules! wrap_std { @@ -161,7 +160,7 @@ pub fn random<'gc>( // See https://github.com/adobe/avmplus/blob/858d034a3bd3a54d9b70909386435cf4aec81d21/core/MathUtils.cpp#L1731C24-L1731C44 // This generated a restricted set of 'f64' values, which some SWFs implicitly rely on. const MAX_VAL: u32 = 0x7FFFFFFF; - let rand = activation.context.rng.random_range(0..MAX_VAL); + let rand = activation.context.rng.generate_random_number(); Ok(((rand as f64) / (MAX_VAL as f64 + 1f64)).into()) } diff --git a/core/src/avm2/globals/math.rs b/core/src/avm2/globals/math.rs index be9fa5f126d5..c3d9a5fe3ec0 100644 --- a/core/src/avm2/globals/math.rs +++ b/core/src/avm2/globals/math.rs @@ -7,7 +7,6 @@ use crate::avm2::parameters::ParametersExt; use crate::avm2::value::Value; use crate::avm2::{ClassObject, Error}; use num_traits::ToPrimitive; -use rand::Rng; macro_rules! wrap_std { ($name:ident, $std:expr) => { @@ -159,6 +158,6 @@ pub fn random<'gc>( // See https://github.com/adobe/avmplus/blob/858d034a3bd3a54d9b70909386435cf4aec81d21/core/MathUtils.cpp#L1731C24-L1731C44 // This generated a restricted set of 'f64' values, which some SWFs implicitly rely on. const MAX_VAL: u32 = 0x7FFFFFFF; - let rand = activation.context.rng.random_range(0..MAX_VAL); + let rand = activation.context.rng.generate_random_number(); Ok(((rand as f64) / (MAX_VAL as f64 + 1f64)).into()) } diff --git a/core/src/avm_rng.rs b/core/src/avm_rng.rs new file mode 100644 index 000000000000..0e808a8744bf --- /dev/null +++ b/core/src/avm_rng.rs @@ -0,0 +1,66 @@ +use crate::locale::get_current_date_time; + +// https://github.com/adobe/avmplus/blob/858d034a3bd3a54d9b70909386435cf4aec81d21/core/MathUtils.cpp#L1546 +const C1: i32 = 1376312589; +const C2: i32 = 789221; +const C3: i32 = 15731; +const K_RANDOM_PURE_MAX: i32 = 0x7FFFFFFF; + +const U_XOR_MASK: u32 = 0x48000000; + +// This struct should not be cloned or copied. +#[derive(Debug, Default)] +pub struct AvmRng { + u_value: u32, +} + +impl AvmRng { + fn init_with_seed(&mut self, seed: u32) { + self.u_value = seed; + } + + fn random_fast_next(&mut self) -> i32 { + if (self.u_value & 1) != 0 { + self.u_value = (self.u_value >> 1) ^ U_XOR_MASK; + } else { + self.u_value >>= 1; + } + self.u_value as i32 + } + + fn random_pure_hasher(&self, mut i_seed: i32) -> i32 { + i_seed = ((i_seed << 13) ^ i_seed).wrapping_sub(i_seed >> 21); + + let mut i_result = i_seed.wrapping_mul(i_seed); + i_result = i_result.wrapping_mul(C3); + i_result = i_result.wrapping_add(C2); + i_result = i_result.wrapping_mul(i_seed); + i_result = i_result.wrapping_add(C1); + i_result &= K_RANDOM_PURE_MAX; + + i_result = i_result.wrapping_add(i_seed); + + i_result = ((i_result << 13) ^ i_result).wrapping_sub(i_result >> 21); + + i_result + } + + pub fn generate_random_number(&mut self) -> i32 { + // In avmplus, RNG is initialized on first use. + if self.u_value == 0 { + let seed = get_seed(); + self.init_with_seed(seed); + } + + let mut a_num = self.random_fast_next(); + + a_num = self.random_pure_hasher(a_num.wrapping_mul(71)); + + a_num & K_RANDOM_PURE_MAX + } +} + +// https://github.com/adobe-flash/avmplus/blob/65a05927767f3735db37823eebf7d743531f5d37/VMPI/PosixSpecificUtils.cpp#L18 +fn get_seed() -> u32 { + get_current_date_time().timestamp_micros() as u32 +} diff --git a/core/src/context.rs b/core/src/context.rs index bd04e3c23ed9..cb8784d405e8 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -6,6 +6,7 @@ use crate::avm1::{Object as Avm1Object, Value as Avm1Value}; use crate::avm2::api_version::ApiVersion; use crate::avm2::Activation as Avm2Activation; use crate::avm2::{Avm2, LoaderInfoObject, Object as Avm2Object, SoundChannelObject}; +use crate::avm_rng::AvmRng; use crate::backend::{ audio::{AudioBackend, AudioManager, SoundHandle, SoundInstanceHandle}, log::LogBackend, @@ -41,7 +42,6 @@ use crate::PlayerMode; use async_channel::Sender; use core::fmt; use gc_arena::{Collect, Mutation}; -use rand::rngs::SmallRng; use ruffle_render::backend::{BitmapCacheEntry, RenderBackend}; use ruffle_render::commands::{CommandHandler, CommandList}; use ruffle_render::transform::TransformStack; @@ -117,7 +117,7 @@ pub struct UpdateContext<'gc> { pub video: &'gc mut dyn VideoBackend, /// The RNG, used by the AVM `RandomNumber` opcode, `Math.random(),` and `random()`. - pub rng: &'gc mut SmallRng, + pub rng: &'gc mut AvmRng, /// The current player's stage (including all loaded levels) pub stage: Stage<'gc>, diff --git a/core/src/lib.rs b/core/src/lib.rs index 203f558106e5..ae5e9855c6df 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -15,6 +15,7 @@ extern crate num_derive; #[macro_use] mod avm1; mod avm2; +mod avm_rng; mod binary_data; pub mod bitmap; pub mod buffer; diff --git a/core/src/player.rs b/core/src/player.rs index 01c189c04a2d..93d45b8e3159 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -6,6 +6,7 @@ use crate::avm1::VariableDumper; use crate::avm1::{Activation, ActivationIdentifier}; use crate::avm2::object::{EventObject as Avm2EventObject, Object as Avm2Object}; use crate::avm2::{Activation as Avm2Activation, Avm2, CallStack}; +use crate::avm_rng::AvmRng; use crate::backend::ui::FontDefinition; use crate::backend::{ audio::{AudioBackend, AudioManager}, @@ -38,7 +39,6 @@ use crate::library::Library; use crate::limits::ExecutionLimit; use crate::loader::{LoadBehavior, LoadManager}; use crate::local_connection::LocalConnections; -use crate::locale::get_current_date_time; use crate::net_connection::NetConnections; use crate::orphan_manager::OrphanManager; use crate::prelude::*; @@ -54,7 +54,6 @@ use crate::DefaultFont; use async_channel::Sender; use gc_arena::lock::GcRefLock; use gc_arena::{Collect, DynamicRootSet, Mutation, Rootable}; -use rand::{rngs::SmallRng, SeedableRng}; use ruffle_macros::istr; use ruffle_render::backend::{null::NullRenderer, RenderBackend, ViewportDimensions}; use ruffle_render::commands::CommandList; @@ -316,7 +315,7 @@ pub struct Player { transform_stack: TransformStack, - rng: SmallRng, + rng: AvmRng, gc_arena: Rc>, @@ -2950,7 +2949,9 @@ impl PlayerBuilder { mouse_cursor_needs_check: false, // Misc. state - rng: SmallRng::seed_from_u64(get_current_date_time().timestamp_millis() as u64), + // TODO: AVM1 and AVM2 use separate RNGs (though algorithm is same), so this is technically incorrect. + // See: https://github.com/ruffle-rs/ruffle/issues/20244 + rng: AvmRng::default(), system: SystemProperties::new(language), page_url: self.page_url.clone(), transform_stack: TransformStack::new(), diff --git a/tests/tests/swfs/avm2/rng/output.txt b/tests/tests/swfs/avm2/rng/output.txt new file mode 100644 index 000000000000..27ba77ddaf61 --- /dev/null +++ b/tests/tests/swfs/avm2/rng/output.txt @@ -0,0 +1 @@ +true diff --git a/tests/tests/swfs/avm2/rng/test.swf b/tests/tests/swfs/avm2/rng/test.swf new file mode 100644 index 000000000000..84f97c2fab1f Binary files /dev/null and b/tests/tests/swfs/avm2/rng/test.swf differ diff --git a/tests/tests/swfs/avm2/rng/test.toml b/tests/tests/swfs/avm2/rng/test.toml new file mode 100644 index 000000000000..cf6123969a1d --- /dev/null +++ b/tests/tests/swfs/avm2/rng/test.toml @@ -0,0 +1 @@ +num_ticks = 1 diff --git a/tests/tests/swfs/avm2/stage3d_bitmap/output.expected.png b/tests/tests/swfs/avm2/stage3d_bitmap/output.expected.png index a117f05a18d0..b0e83ecd1daa 100644 Binary files a/tests/tests/swfs/avm2/stage3d_bitmap/output.expected.png and b/tests/tests/swfs/avm2/stage3d_bitmap/output.expected.png differ diff --git a/tests/tests/swfs/avm2/stage3d_texture/output.expected.png b/tests/tests/swfs/avm2/stage3d_texture/output.expected.png index bf7ab3765492..6a8ecb043775 100644 Binary files a/tests/tests/swfs/avm2/stage3d_texture/output.expected.png and b/tests/tests/swfs/avm2/stage3d_texture/output.expected.png differ