diff --git a/.cargo/config.toml b/.cargo/config.toml index d347c053b..f440d7c7b 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -15,3 +15,15 @@ rustflags = ["-C", "target-cpu=native"] [target.x86_64-pc-windows-msvc] rustflags = ["-C", "target-cpu=native"] + +[target.wasm32-unknown-unknown] +rustflags = [ + "-C", "target-feature=+atomics,+bulk-memory,+simd128,+relaxed-simd", + "-C", "link-arg=--shared-memory", + "-C", "link-arg=--max-memory=1073741824", + "-C", "link-arg=--import-memory", + "-C", "link-arg=--export=__wasm_init_tls", + "-C", "link-arg=--export=__tls_size", + "-C", "link-arg=--export=__tls_align", + "-C", "link-arg=--export=__tls_base", +] diff --git a/.github/workflows/check_bench.yml b/.github/workflows/check_bench.yml index e3896d675..c61325dc4 100644 --- a/.github/workflows/check_bench.yml +++ b/.github/workflows/check_bench.yml @@ -63,7 +63,7 @@ jobs: shell: bash run: | echo "RUSTFLAGS:" $RUSTFLAGS - cargo +nightly -Z target-applies-to-host rustc --release -- --emit link=reckless + cargo +nightly -Z target-applies-to-host rustc --release --bin reckless -- --emit link=reckless measured_bench=$("$SDE_PATH/sde" ${{ matrix.sde_flag }} -- ./reckless bench | tail -1 | cut "--delimiter= " -f 2 -) echo "measured_bench=$measured_bench" >> $GITHUB_ENV echo "Measured Bench: $measured_bench" diff --git a/Cargo.lock b/Cargo.lock index 2a854c043..ba2ef7062 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + [[package]] name = "cc" version = "1.2.22" @@ -78,6 +84,30 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "glob" version = "0.3.2" @@ -93,6 +123,17 @@ dependencies = [ "either", ] +[[package]] +name = "js-sys" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.175" @@ -137,6 +178,18 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "prettyplease" version = "0.2.32" @@ -158,9 +211,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -171,7 +224,10 @@ version = "0.10.0-dev" dependencies = [ "bindgen", "cc", + "js-sys", "libc", + "wasm-bindgen", + "web-time", ] [[package]] @@ -209,12 +265,24 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "syn" version = "2.0.101" @@ -232,6 +300,61 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "wasm-bindgen" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index a73193b5f..c792e5cad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,9 @@ edition = "2024" build = "build/build.rs" publish = false +[lib] +crate-type = ["cdylib", "rlib"] + [features] default = ["syzygy"] syzygy = [] @@ -26,3 +29,8 @@ bindgen = "0.71.1" [dependencies] libc = "0.2.175" +web-time = "1" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" +js-sys = "0.3" diff --git a/Makefile b/Makefile index 6dec3088e..6aedf2aef 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,15 @@ endif rule: cargo rustc --release -- -C target-cpu=native --emit link=$(NAME) +wasm: + unset RUSTFLAGS && rustup run nightly \ + cargo build -Z build-std=panic_abort,std \ + --lib --target wasm32-unknown-unknown --release --no-default-features + wasm-bindgen target/wasm32-unknown-unknown/release/reckless.wasm --target web --out-dir pkg + wasm-opt -O3 --enable-simd --enable-threads --enable-relaxed-simd \ + pkg/reckless_bg.wasm -o pkg/reckless_bg.wasm + + x64-check: RUSTFLAGS="-C target-cpu=x86-64" cargo check --target x86_64-unknown-linux-gnu --no-default-features RUSTFLAGS="-C target-cpu=x86-64-v3" cargo check --target x86_64-unknown-linux-gnu --no-default-features diff --git a/build/build.rs b/build/build.rs index 364ce6fff..b1ef59700 100644 --- a/build/build.rs +++ b/build/build.rs @@ -20,7 +20,9 @@ fn main() { generate_engine_version(); #[cfg(feature = "syzygy")] - generate_syzygy_binding(); + if std::env::var("CARGO_CFG_TARGET_ARCH").as_deref() != Ok("wasm32") { + generate_syzygy_binding(); + } if !Path::new("networks").join(NETWORK_NAME).exists() && env::var("EVALFILE").is_err() { download_network(); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 000000000..09bb90cc6 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,45 @@ +#![allow(unsafe_op_in_unsafe_fn)] +#![warn(clippy::large_types_passed_by_value)] +#![warn(clippy::trivially_copy_pass_by_ref)] +#![warn(clippy::redundant_clone)] +#![cfg_attr(target_arch = "wasm32", allow(dead_code, unused_imports))] + +mod board; +mod evaluation; +mod history; +mod lookup; +mod misc; +mod movepick; +mod nnue; +mod numa; +mod parameters; +mod search; +mod setwise; +mod stack; +mod thread; +mod threadpool; +mod time; +mod transposition; +mod types; + +mod tools; + +#[cfg(not(target_arch = "wasm32"))] +mod uci; + +#[cfg(feature = "syzygy")] +mod tb; + +#[cfg(feature = "syzygy")] +#[allow(warnings)] +mod bindings; + +#[cfg(target_arch = "wasm32")] +pub mod wasm; + +#[cfg(not(target_arch = "wasm32"))] +pub fn run(buffer: std::collections::VecDeque) { + lookup::initialize(); + nnue::initialize(); + uci::message_loop(buffer); +} diff --git a/src/main.rs b/src/main.rs index ef93bccb3..9d084dad8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,40 +1,5 @@ -#![allow(unsafe_op_in_unsafe_fn)] -#![warn(clippy::large_types_passed_by_value)] -#![warn(clippy::trivially_copy_pass_by_ref)] -#![warn(clippy::redundant_clone)] - -mod board; -mod evaluation; -mod history; -mod lookup; -mod misc; -mod movepick; -mod nnue; -mod numa; -mod parameters; -mod search; -mod setwise; -mod stack; -mod thread; -mod threadpool; -mod time; -mod tools; -mod transposition; -mod types; -mod uci; - -#[cfg(feature = "syzygy")] -mod tb; - -#[cfg(feature = "syzygy")] -#[allow(warnings)] -mod bindings; - +#[cfg(not(target_arch = "wasm32"))] fn main() { - lookup::initialize(); - nnue::initialize(); - - let buffer: std::collections::VecDeque = std::env::args().skip(1).collect(); - - uci::message_loop(buffer); + let buffer = std::env::args().skip(1).collect(); + reckless::run(buffer); } diff --git a/src/nnue.rs b/src/nnue.rs index fa9c6fa26..a5066c299 100644 --- a/src/nnue.rs +++ b/src/nnue.rs @@ -15,14 +15,30 @@ use crate::{ }; mod forward { - #[cfg(any(target_feature = "avx2", target_feature = "neon"))] + #[cfg(any( + target_feature = "avx2", + target_feature = "neon", + all(target_arch = "wasm32", target_feature = "simd128"), + ))] mod vectorized; - #[cfg(any(target_feature = "avx2", target_feature = "neon"))] + #[cfg(any( + target_feature = "avx2", + target_feature = "neon", + all(target_arch = "wasm32", target_feature = "simd128"), + ))] pub use vectorized::*; - #[cfg(not(any(target_feature = "avx2", target_feature = "neon")))] + #[cfg(not(any( + target_feature = "avx2", + target_feature = "neon", + all(target_arch = "wasm32", target_feature = "simd128"), + )))] mod scalar; - #[cfg(not(any(target_feature = "avx2", target_feature = "neon")))] + #[cfg(not(any( + target_feature = "avx2", + target_feature = "neon", + all(target_arch = "wasm32", target_feature = "simd128"), + )))] pub use scalar::*; } @@ -42,9 +58,24 @@ mod simd { #[cfg(all(target_feature = "neon", not(any(target_feature = "avx2", target_feature = "avx512f"))))] pub use neon::*; - #[cfg(not(any(target_feature = "avx512f", target_feature = "avx2", target_feature = "neon")))] + #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))] + mod wasm; + #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))] + pub use wasm::*; + + #[cfg(not(any( + target_feature = "avx512f", + target_feature = "avx2", + target_feature = "neon", + all(target_arch = "wasm32", target_feature = "simd128"), + )))] mod scalar; - #[cfg(not(any(target_feature = "avx512f", target_feature = "avx2", target_feature = "neon")))] + #[cfg(not(any( + target_feature = "avx512f", + target_feature = "avx2", + target_feature = "neon", + all(target_arch = "wasm32", target_feature = "simd128"), + )))] pub use scalar::*; } diff --git a/src/nnue/forward/vectorized.rs b/src/nnue/forward/vectorized.rs index 6e16a60f2..37d713cee 100644 --- a/src/nnue/forward/vectorized.rs +++ b/src/nnue/forward/vectorized.rs @@ -156,7 +156,7 @@ pub unsafe fn propagate_l3(l2_out: &Aligned<[f32; L3_SIZE]>, bucket: usize, para simd::horizontal_sum(output) + parameters.l3_biases[bucket] } -#[cfg(all(not(target_feature = "neon"), not(target_feature = "avx512vbmi2")))] +#[cfg(all(not(target_arch = "wasm32"), not(target_feature = "neon"), not(target_feature = "avx512vbmi2")))] pub unsafe fn find_nnz( ft_out: &Aligned<[u8; L1_SIZE]>, nnz_table: &[SparseEntry], ) -> (Aligned<[u16; L1_SIZE / 4]>, usize) { @@ -255,3 +255,46 @@ pub unsafe fn find_nnz( (indexes, count) } + +#[cfg(target_arch = "wasm32")] +pub unsafe fn find_nnz( + ft_out: &Aligned<[u8; L1_SIZE]>, nnz_table: &[SparseEntry], +) -> (Aligned<[u16; L1_SIZE / 4]>, usize) { + use std::arch::wasm32::*; + + let mut indexes = Aligned::new([0u16; L1_SIZE / 4]); + let mut count = 0; + + let increment = i16x8_splat(8); + let mut base = i16x8_splat(0); + let zero = i8x16_splat(0); + + for i in (0..L1_SIZE).step_by(64) { + let v0 = *ft_out.as_ptr().add(i).cast::(); + let v1 = *ft_out.as_ptr().add(i + 16).cast::(); + let v2 = *ft_out.as_ptr().add(i + 32).cast::(); + let v3 = *ft_out.as_ptr().add(i + 48).cast::(); + + let half0 = i16x8_narrow_i32x4(v0, v1); + let half1 = i16x8_narrow_i32x4(v2, v3); + let packed = u8x16_narrow_i16x8(half0, half1); + + let mask = i8x16_bitmask(v128_not(i8x16_eq(packed, zero))) as usize; + + let base_hi = i16x8_add(base, increment); + + let entry_lo = nnz_table.get_unchecked(mask & 0xFF); + let store = indexes.as_mut_ptr().add(count) as *mut v128; + v128_store(store, i16x8_add(base, v128_load(entry_lo.indexes.as_ptr() as *const v128))); + count += entry_lo.count; + + let entry_hi = nnz_table.get_unchecked(mask >> 8); + let store = indexes.as_mut_ptr().add(count) as *mut v128; + v128_store(store, i16x8_add(base_hi, v128_load(entry_hi.indexes.as_ptr() as *const v128))); + count += entry_hi.count; + + base = i16x8_add(base_hi, increment); + } + + (indexes, count) +} diff --git a/src/nnue/simd/wasm.rs b/src/nnue/simd/wasm.rs new file mode 100644 index 000000000..0ea1380fc --- /dev/null +++ b/src/nnue/simd/wasm.rs @@ -0,0 +1,125 @@ +use std::{arch::wasm32::*, mem::size_of}; + +pub const F32_LANES: usize = size_of::() / size_of::(); +pub const I32_LANES: usize = size_of::() / size_of::(); +pub const I16_LANES: usize = size_of::() / size_of::(); +pub const MUL_HI_SHIFT: i32 = 0; + +pub fn add_i16(a: v128, b: v128) -> v128 { + i16x8_add(a, b) +} + +pub fn sub_i16(a: v128, b: v128) -> v128 { + i16x8_sub(a, b) +} + +pub unsafe fn zeroed() -> v128 { + i32x4_splat(0) +} + +pub unsafe fn splat_i16(a: i16) -> v128 { + i16x8_splat(a) +} + +pub unsafe fn clamp_i16(x: v128, min: v128, max: v128) -> v128 { + i16x8_max(i16x8_min(x, max), min) +} + +pub unsafe fn min_i16(a: v128, b: v128) -> v128 { + i16x8_min(a, b) +} + +pub unsafe fn shift_left_i16(a: v128) -> v128 { + i16x8_shl(a, SHIFT as u32) +} + +pub unsafe fn mul_high_i16(a: v128, b: v128) -> v128 { + let lo = i32x4_extmul_low_i16x8(a, b); + let hi = i32x4_extmul_high_i16x8(a, b); + i8x16_shuffle::<2, 3, 6, 7, 10, 11, 14, 15, 18, 19, 22, 23, 26, 27, 30, 31>(lo, hi) +} + +pub unsafe fn convert_i8_i16(a: u64) -> v128 { + i16x8_extend_low_i8x16(i64x2_splat(a as i64)) +} + +pub unsafe fn packus(a: v128, b: v128) -> v128 { + u8x16_narrow_i16x8(a, b) +} + +pub unsafe fn permute(a: v128) -> v128 { + a +} + +pub unsafe fn splat_i32(a: i32) -> v128 { + i32x4_splat(a) +} + +pub unsafe fn zero_f32() -> v128 { + f32x4_splat(0.0) +} + +pub unsafe fn splat_f32(a: f32) -> v128 { + f32x4_splat(a) +} + +pub unsafe fn mul_add_f32(a: v128, b: v128, c: v128) -> v128 { + #[cfg(target_feature = "relaxed-simd")] + return f32x4_relaxed_madd(a, b, c); + #[cfg(not(target_feature = "relaxed-simd"))] + return f32x4_add(f32x4_mul(a, b), c); +} + +pub unsafe fn convert_to_f32(a: v128) -> v128 { + f32x4_convert_i32x4(a) +} + +pub unsafe fn clamp_f32(x: v128, min: v128, max: v128) -> v128 { + #[cfg(target_feature = "relaxed-simd")] + return f32x4_relaxed_max(f32x4_relaxed_min(x, max), min); + #[cfg(not(target_feature = "relaxed-simd"))] + return f32x4_max(f32x4_min(x, max), min); +} + +pub unsafe fn dpbusd(i32s: v128, u8s: v128, i8s: v128) -> v128 { + #[cfg(target_feature = "relaxed-simd")] + return i32x4_relaxed_dot_i8x16_i7x16_add(i8s, u8s, i32s); + #[cfg(not(target_feature = "relaxed-simd"))] + { + let dot_lo = i32x4_dot_i16x8(u16x8_extend_low_u8x16(u8s), i16x8_extend_low_i8x16(i8s)); + let dot_hi = i32x4_dot_i16x8(u16x8_extend_high_u8x16(u8s), i16x8_extend_high_i8x16(i8s)); + let even = i8x16_shuffle::<0, 1, 2, 3, 8, 9, 10, 11, 16, 17, 18, 19, 24, 25, 26, 27>(dot_lo, dot_hi); + let odd = i8x16_shuffle::<4, 5, 6, 7, 12, 13, 14, 15, 20, 21, 22, 23, 28, 29, 30, 31>(dot_lo, dot_hi); + i32x4_add(i32s, i32x4_add(even, odd)) + } +} + +pub unsafe fn double_dpbusd(i32s: v128, u8s1: v128, i8s1: v128, u8s2: v128, i8s2: v128) -> v128 { + #[cfg(target_feature = "relaxed-simd")] + return dpbusd(dpbusd(i32s, u8s1, i8s1), u8s2, i8s2); + #[cfg(not(target_feature = "relaxed-simd"))] + { + let dot1_lo = i32x4_dot_i16x8(u16x8_extend_low_u8x16(u8s1), i16x8_extend_low_i8x16(i8s1)); + let dot2_lo = i32x4_dot_i16x8(u16x8_extend_low_u8x16(u8s2), i16x8_extend_low_i8x16(i8s2)); + let sum_lo = i32x4_add(dot1_lo, dot2_lo); + let dot1_hi = i32x4_dot_i16x8(u16x8_extend_high_u8x16(u8s1), i16x8_extend_high_i8x16(i8s1)); + let dot2_hi = i32x4_dot_i16x8(u16x8_extend_high_u8x16(u8s2), i16x8_extend_high_i8x16(i8s2)); + let sum_hi = i32x4_add(dot1_hi, dot2_hi); + let even = i8x16_shuffle::<0, 1, 2, 3, 8, 9, 10, 11, 16, 17, 18, 19, 24, 25, 26, 27>(sum_lo, sum_hi); + let odd = i8x16_shuffle::<4, 5, 6, 7, 12, 13, 14, 15, 20, 21, 22, 23, 28, 29, 30, 31>(sum_lo, sum_hi); + i32x4_add(i32s, i32x4_add(even, odd)) + } +} + +pub unsafe fn horizontal_sum(x: [v128; 4]) -> f32 { + let sum02 = f32x4_add(x[0], x[2]); + let sum13 = f32x4_add(x[1], x[3]); + let sum = f32x4_add(sum02, sum13); + let rotated = i8x16_shuffle::<8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7>(sum, sum); + let sum2 = f32x4_add(sum, rotated); + f32x4_extract_lane::<0>(sum2) + f32x4_extract_lane::<1>(sum2) +} + +pub unsafe fn nnz_bitmask(x: v128) -> u16 { + i32x4_bitmask(i32x4_gt(x, i32x4_splat(0))) as u16 +} diff --git a/src/numa.rs b/src/numa.rs index c19952c49..00c0f5d64 100644 --- a/src/numa.rs +++ b/src/numa.rs @@ -172,12 +172,21 @@ impl NumaConfig { } pub fn execute_on_numa_node(&self, n: NumaIndex, f: F) { - let cfg = self.clone(); - let handle = thread::spawn(move || { - cfg.bind_current_thread_to_numa_node(n); + #[cfg(not(target_arch = "wasm32"))] + { + let cfg = self.clone(); + let handle = thread::spawn(move || { + cfg.bind_current_thread_to_numa_node(n); + f(); + }); + handle.join().unwrap(); + } + + #[cfg(target_arch = "wasm32")] + { + let _ = n; f(); - }); - handle.join().unwrap(); + } } fn add_cpu_to_node(&mut self, node: NumaIndex, cpu: CpuIndex) { diff --git a/src/thread.rs b/src/thread.rs index 729cb1d98..43c6ecd66 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -6,6 +6,17 @@ use std::{ }, }; +#[cfg(target_arch = "wasm32")] +thread_local! { + pub static WASM_CALLBACK: std::cell::RefCell> = + const { std::cell::RefCell::new(None) }; + pub static WASM_DISPATCH: std::cell::RefCell> = + const { std::cell::RefCell::new(None) }; +} + +#[cfg(target_arch = "wasm32")] +pub static WORKERS_REMAINING: AtomicUsize = AtomicUsize::new(0); + use crate::{ board::Board, history::{ContinuationCorrectionHistory, ContinuationHistory, CorrectionHistory, NoisyHistory, QuietHistory}, @@ -18,6 +29,45 @@ use crate::{ types::{MAX_MOVES, MAX_PLY, Move, Score, normalize_to_cp}, }; +pub trait UciWriter: Send { + fn write_line(&mut self, line: &str); + #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] + fn take(&mut self) -> String { + String::new() + } +} + +pub struct StdoutWriter; + +impl UciWriter for StdoutWriter { + fn write_line(&mut self, line: &str) { + println!("{line}"); + } +} + +#[derive(Default)] +#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] +pub struct BufferWriter { + pub buffer: String, +} + +impl UciWriter for BufferWriter { + fn write_line(&mut self, line: &str) { + self.buffer.push_str(line); + self.buffer.push('\n'); + #[cfg(target_arch = "wasm32")] + WASM_CALLBACK.with(|cb| { + if let Some(f) = &*cb.borrow() { + let _ = f.call1(&wasm_bindgen::JsValue::NULL, &wasm_bindgen::JsValue::from_str(line)); + } + }); + } + + fn take(&mut self) -> String { + std::mem::take(&mut self.buffer) + } +} + #[repr(align(64))] struct AlignedAtomicU64 { inner: AtomicU64, @@ -186,6 +236,7 @@ pub struct ThreadData { pub pv_end: usize, pub cutoff_count: PlyArray, pub excluded: PlyArray, + pub writer: Box, } impl ThreadData { @@ -221,6 +272,10 @@ impl ThreadData { pv_end: 0, cutoff_count: PlyArray::default(), excluded: PlyArray::default(), + #[cfg(not(target_arch = "wasm32"))] + writer: Box::new(StdoutWriter), + #[cfg(target_arch = "wasm32")] + writer: Box::new(BufferWriter::default()), } } @@ -236,7 +291,7 @@ impl ThreadData { self.continuation_history.get(self.stack[ply - index].conthist, self.board.piece_on(mv.from()), mv.to()) } - pub fn print_uci_info(&self, depth: i32) { + pub fn print_uci_info(&mut self, depth: i32) { if self.root_moves.is_empty() { self.print_uci_no_move(); return; @@ -297,7 +352,7 @@ impl ThreadData { formatted_score.push_str(" lowerbound"); } - print!( + let mut line = format!( "info depth {depth} seldepth {} multipv {} score {formatted_score} nodes {} time {ms} nps {nps:.0} hashfull {} tbhits {} pv", root_move.sel_depth, pv_index + 1, @@ -306,20 +361,20 @@ impl ThreadData { self.shared.tb_hits.aggregate(), ); - print!(" {}", root_move.mv.to_uci(&self.board)); + line.push_str(&format!(" {}", root_move.mv.to_uci(&self.board))); for mv in root_move.pv.line() { - print!(" {}", mv.to_uci(&self.board)); + line.push_str(&format!(" {}", mv.to_uci(&self.board))); } - println!(); + self.writer.write_line(&line); } } - fn print_uci_no_move(&self) { + fn print_uci_no_move(&mut self) { if self.board.in_check() { - println!("info depth 0 score mate 0"); + self.writer.write_line("info depth 0 score mate 0"); } else { - println!("info depth 0 score cp 0"); + self.writer.write_line("info depth 0 score cp 0"); } } } diff --git a/src/threadpool.rs b/src/threadpool.rs index 8a3d166a4..7041fbada 100644 --- a/src/threadpool.rs +++ b/src/threadpool.rs @@ -1,5 +1,5 @@ use std::{ - ops::Index, + ops::{Index, IndexMut}, sync::{ Arc, Condvar, Mutex, atomic::Ordering, @@ -8,6 +8,9 @@ use std::{ thread::Scope, }; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::JsValue; + #[cfg(feature = "syzygy")] use crate::tb; use crate::{ @@ -90,6 +93,57 @@ impl ThreadPool { x.store((self.main_thread().previous_best_score + 32768) as u32, Ordering::Release); }); + #[cfg(target_arch = "wasm32")] + { + let thread_count = self.vector.len(); + + shared.root_in_tb.store(false, Ordering::Relaxed); + shared.stop_probing_tb.store(false, Ordering::Relaxed); + + { + let t1 = &mut self.vector[0]; + t1.board = (*board).clone(); + t1.root_moves = + t1.board.generate_all_moves().iter().map(|v| RootMove { mv: v.mv, ..Default::default() }).collect(); + t1.multi_pv = multi_pv; + t1.time_manager = time_manager.clone(); + } + + let root_moves = self.vector[0].root_moves.clone(); + for (index, t) in self.vector[1..].iter_mut().enumerate() { + t.id = index + 1; + t.time_manager = time_manager.clone(); + t.board = (*board).clone(); + t.root_moves = root_moves.clone(); + } + + let (t1, rest) = self.vector.split_first_mut().unwrap(); + let n_workers = rest.len(); + + if n_workers > 0 { + crate::thread::WORKERS_REMAINING.store(n_workers, Ordering::Release); + crate::thread::WASM_DISPATCH.with(|d| { + if let Some(f) = &*d.borrow() { + let ptrs = js_sys::Array::new(); + for t in rest.iter() { + ptrs.push(&JsValue::from(t as *const ThreadData as u32)); + } + let _ = f.call2(&JsValue::NULL, &ptrs.into(), &JsValue::from(thread_count as u32)); + } + }); + } + + search::start(t1, report, thread_count); + shared.status.set(Status::STOPPED); + + if n_workers > 0 { + while crate::thread::WORKERS_REMAINING.load(Ordering::Acquire) > 0 { + std::hint::spin_loop(); + } + } + } + + #[cfg(not(target_arch = "wasm32"))] std::thread::scope(|scope| { let mut handlers = Vec::new(); @@ -157,11 +211,19 @@ impl Index for ThreadPool { } } +impl IndexMut for ThreadPool { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.vector[index] + } +} + +#[cfg(not(target_arch = "wasm32"))] pub struct WorkerThread { handle: std::thread::JoinHandle<()>, comms: WorkSender, } +#[cfg(not(target_arch = "wasm32"))] impl WorkerThread { pub fn join(self) { drop(self.comms); // Drop the sender to signal the worker thread to finish @@ -224,6 +286,7 @@ pub trait ScopeExt<'scope, 'env> { F: FnOnce() + Send + 'scope; } +#[cfg(not(target_arch = "wasm32"))] impl<'scope, 'env> ScopeExt<'scope, 'env> for Scope<'scope, 'env> { fn spawn_into<'comms, F>(&'scope self, f: F, thread: &'scope WorkerThread) -> ReceiverHandle<'scope> where @@ -251,6 +314,7 @@ impl<'scope, 'env> ScopeExt<'scope, 'env> for Scope<'scope, 'env> { } } +#[cfg(not(target_arch = "wasm32"))] fn make_worker_thread() -> WorkerThread { let (sender, receiver) = make_work_channel(); @@ -268,10 +332,12 @@ fn make_worker_thread() -> WorkerThread { WorkerThread { handle, comms: sender } } +#[cfg(not(target_arch = "wasm32"))] fn make_worker_threads(num_threads: usize) -> Vec { std::iter::repeat_with(make_worker_thread).take(num_threads).collect() } +#[cfg(not(target_arch = "wasm32"))] fn make_thread_data(shared: Arc, worker_threads: &[WorkerThread]) -> Vec { std::thread::scope(|scope| -> Vec { let cfg = shared.numa_context.get_numa_config(); @@ -311,3 +377,22 @@ fn make_thread_data(shared: Arc, worker_threads: &[WorkerThread]) thread_data }) } + +#[cfg(target_arch = "wasm32")] +pub struct WorkerThread; + +#[cfg(target_arch = "wasm32")] +impl WorkerThread { + pub fn join(self) {} +} + +#[cfg(target_arch = "wasm32")] +fn make_worker_threads(num_threads: usize) -> Vec { + std::iter::repeat_with(|| WorkerThread).take(num_threads).collect() +} + +#[cfg(target_arch = "wasm32")] +fn make_thread_data(shared: Arc, worker_threads: &[WorkerThread]) -> Vec { + let token = NumaReplicatedAccessToken::new(0); + worker_threads.iter().map(|_| ThreadData::new(shared.clone(), token)).collect() +} diff --git a/src/time.rs b/src/time.rs index f069862c7..f3de19954 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,4 +1,5 @@ -use std::time::{Duration, Instant}; +use std::time::Duration; +use web_time::Instant; use crate::thread::ThreadData; diff --git a/src/transposition.rs b/src/transposition.rs index 27cf9cee8..a34398ce7 100644 --- a/src/transposition.rs +++ b/src/transposition.rs @@ -399,6 +399,7 @@ unsafe fn deallocate(ptr: *mut Cluster, len: usize) { } unsafe fn parallel_clear(threads: usize, ptr: *mut T, len: usize) { + #[cfg(not(target_arch = "wasm32"))] std::thread::scope(|scope| { let slice = std::slice::from_raw_parts_mut(ptr, len); @@ -407,4 +408,10 @@ unsafe fn parallel_clear(threads: usize, ptr: *mut T, len: scope.spawn(|| chunk.as_mut_ptr().write_bytes(0, chunk.len())); } }); + + #[cfg(target_arch = "wasm32")] + { + let _ = threads; + ptr.write_bytes(0, len); + } } diff --git a/src/uci.rs b/src/uci.rs index bcf53b844..614eb1b10 100644 --- a/src/uci.rs +++ b/src/uci.rs @@ -37,6 +37,7 @@ impl Default for Settings { } } +#[cfg(not(target_arch = "wasm32"))] pub fn message_loop(mut buffer: VecDeque) { let shared = Arc::new(SharedContext::default()); let mut settings = Settings::default(); @@ -109,6 +110,7 @@ pub fn message_loop(mut buffer: VecDeque) { } } +#[cfg(not(target_arch = "wasm32"))] fn spawn_listener(shared: Arc) -> std::sync::mpsc::Receiver { let (tx, rx) = std::sync::mpsc::channel(); @@ -240,7 +242,8 @@ fn go(threads: &mut ThreadPool, settings: &Settings, board: &Board, shared: &Arc } if best != 0 { - threads[best].print_uci_info(threads[best].completed_depth); + let depth = threads[best].completed_depth; + threads[best].print_uci_info(depth); } println!("bestmove {}", threads[best].root_moves[0].mv.to_uci(board)); diff --git a/src/wasm.rs b/src/wasm.rs new file mode 100644 index 000000000..c11fbe0a2 --- /dev/null +++ b/src/wasm.rs @@ -0,0 +1,126 @@ +use std::sync::Arc; +use std::sync::atomic::Ordering; + +use js_sys::Function as JsFunction; +use wasm_bindgen::prelude::*; + +use crate::{ + board::{Board, NullBoardObserver}, + search::Report, + thread::SharedContext, + threadpool::ThreadPool, + time::{Limits, TimeManager}, +}; + +#[wasm_bindgen] +pub struct Engine { + shared: Arc, + threads: ThreadPool, + board: Board, +} + +#[wasm_bindgen] +impl Engine { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + crate::lookup::initialize(); + crate::nnue::initialize(); + + let shared = Arc::new(SharedContext::default()); + let threads = ThreadPool::new(shared.clone()); + Self { shared, threads, board: Board::starting_position() } + } + + pub fn set_position(&mut self, fen: &str) { + self.board = Board::from_fen(fen).unwrap_or_else(|_| Board::starting_position()); + } + + pub fn make_move(&mut self, uci_move: &str) { + let moves = self.board.generate_all_moves(); + if let Some(mv) = moves.iter().map(|e| e.mv).find(|m| m.to_uci(&self.board) == uci_move) { + self.board.make_move(mv, &mut NullBoardObserver); + } + } + + pub fn set_threads(&mut self, n: u32) { + self.threads.set_count(n as usize); + } + + pub fn set_dispatch(&self, dispatch: Option) { + crate::thread::WASM_DISPATCH.with(|d| *d.borrow_mut() = dispatch); + } + + pub fn go_uci(&mut self, depth: u32, nodes: u32, multi_pv: u32, on_info: Option) { + let multi_pv = (multi_pv as usize).max(1); + let limits = if nodes > 0 { + Limits::Nodes(nodes as u64) + } else if depth > 0 { + Limits::Depth(depth as i32) + } else { + Limits::Infinite + }; + self.run_search(limits, multi_pv, on_info); + } + + pub fn go_movetime(&mut self, ms: u32, multi_pv: u32, on_info: Option) { + self.run_search(Limits::Time(ms as u64), (multi_pv as usize).max(1), on_info); + } + + pub fn take_output(&mut self) -> String { + self.threads.vector[0].writer.take() + } + + fn run_search(&mut self, limits: Limits, multi_pv: usize, on_info: Option) { + crate::thread::WASM_CALLBACK.with(|c| *c.borrow_mut() = on_info); + let tm = TimeManager::new(limits, self.board.fullmove_number(), 0); + self.threads.execute_searches(tm, Report::Full, multi_pv, &self.board, &self.shared); + crate::thread::WASM_CALLBACK.with(|c| *c.borrow_mut() = None); + } + + pub fn last_bestmove(&self) -> String { + if self.threads[0].root_moves.is_empty() { + "(none)".to_string() + } else { + self.threads[0].root_moves[0].mv.to_uci(&self.board) + } + } + + pub fn last_score(&self) -> i32 { + if self.threads[0].root_moves.is_empty() { 0 } else { self.threads[0].root_moves[0].score } + } + + pub fn last_depth(&self) -> i32 { + self.threads[0].completed_depth + } + + pub fn fen(&self) -> String { + self.board.to_fen() + } + + pub fn evaluate(&mut self) -> i32 { + self.threads.main_thread().nnue.full_refresh(&self.board); + self.threads.main_thread().nnue.evaluate(&self.board) + } + + pub fn last_nodes(&self) -> u64 { + self.shared.nodes.aggregate() + } + + pub fn reset(&mut self) { + self.threads.clear(); + self.shared.tt.clear(1); + } +} + +impl Default for Engine { + fn default() -> Self { + Self::new() + } +} + +#[wasm_bindgen] +pub fn run_helper_thread(ptr: u32, thread_count: u32) { + let t = unsafe { &mut *(ptr as *mut crate::thread::ThreadData) }; + crate::search::start(t, Report::None, thread_count as usize); + crate::thread::WORKERS_REMAINING.fetch_sub(1, Ordering::Release); +}