#[cfg(not(target_arch = "wasm32"))]
mod imp {
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Condvar, Mutex};
use std::thread::JoinHandle;
use crate::eval::EvalHash;
use crate::position::Position;
use crate::tt::TranspositionTable;
use crate::types::Depth;
use crate::search::engine::{SearchProgress, search_helper};
use crate::search::{LimitsType, SearchTuneParams, SearchWorker, TimeManagement, TimeOptions};
const SEARCH_STACK_SIZE: usize = 64 * 1024 * 1024;
pub struct ThreadPool {
threads: Vec<Thread>,
stop: Arc<AtomicBool>,
ponderhit: Arc<AtomicBool>,
increase_depth_shared: Arc<AtomicBool>,
eval_hash: Arc<EvalHash>,
search_tune_params: SearchTuneParams,
}
impl ThreadPool {
pub fn new(
num_threads: usize,
tt: Arc<TranspositionTable>,
eval_hash: Arc<EvalHash>,
stop: Arc<AtomicBool>,
ponderhit: Arc<AtomicBool>,
increase_depth_shared: Arc<AtomicBool>,
max_moves_to_draw: i32,
search_tune_params: SearchTuneParams,
) -> Self {
let mut pool = Self {
threads: Vec::new(),
stop,
ponderhit,
increase_depth_shared,
eval_hash: Arc::clone(&eval_hash),
search_tune_params,
};
pool.set_num_threads(num_threads, tt, eval_hash, max_moves_to_draw, search_tune_params);
pool
}
pub fn set_num_threads(
&mut self,
num_threads: usize,
tt: Arc<TranspositionTable>,
eval_hash: Arc<EvalHash>,
max_moves_to_draw: i32,
search_tune_params: SearchTuneParams,
) {
let helper_count = num_threads.saturating_sub(1);
if helper_count == self.threads.len() {
self.eval_hash = eval_hash;
self.search_tune_params = search_tune_params;
return;
}
self.wait_for_search_finished();
self.threads.clear();
self.eval_hash = Arc::clone(&eval_hash);
self.search_tune_params = search_tune_params;
for id in 1..=helper_count {
self.threads.push(Thread::new(
id,
Arc::clone(&tt),
Arc::clone(&eval_hash),
Arc::clone(&self.stop),
Arc::clone(&self.ponderhit),
Arc::clone(&self.increase_depth_shared),
max_moves_to_draw,
search_tune_params,
));
}
}
pub fn start_thinking(
&self,
pos: &Position,
limits: LimitsType,
max_depth: Depth,
time_options: TimeOptions,
max_moves_to_draw: i32,
draw_value_black: i32,
draw_value_white: i32,
skill_enabled: bool,
) {
if self.threads.is_empty() {
return;
}
for thread in &self.threads {
thread.start_searching(SearchTask {
pos: pos.clone(),
limits: limits.clone(),
max_depth,
time_options,
max_moves_to_draw,
draw_value_black,
draw_value_white,
search_tune_params: self.search_tune_params,
skill_enabled,
});
}
}
pub fn wait_for_search_finished(&self) {
for thread in &self.threads {
thread.wait_for_search_finished();
}
}
pub fn clear_histories(&self) {
for thread in &self.threads {
thread.clear_worker();
}
for thread in &self.threads {
thread.wait_for_search_finished();
}
}
pub fn update_tt(&mut self, tt: Arc<TranspositionTable>) {
for thread in &self.threads {
let tt = Arc::clone(&tt);
thread.with_worker(|worker| {
worker.tt = tt;
});
}
}
pub fn update_eval_hash(&mut self, eval_hash: Arc<EvalHash>) {
for thread in &self.threads {
let eval_hash = Arc::clone(&eval_hash);
thread.with_worker(|worker| {
worker.eval_hash = eval_hash;
});
}
}
pub fn update_search_tune_params(&mut self, search_tune_params: SearchTuneParams) {
self.search_tune_params = search_tune_params;
for thread in &self.threads {
thread.with_worker(|worker| {
worker.search_tune_params = search_tune_params;
});
}
}
pub fn helper_threads(&self) -> &[Thread] {
&self.threads
}
pub fn clear_helper_results(&self) {
}
}
struct ThreadInner {
worker: Mutex<Box<SearchWorker>>,
state: Mutex<ThreadState>,
condvar: Condvar,
stop: Arc<AtomicBool>,
ponderhit: Arc<AtomicBool>,
increase_depth_shared: Arc<AtomicBool>,
progress: Arc<SearchProgress>,
}
struct ThreadState {
searching: bool,
exit: bool,
task: Option<ThreadTask>,
}
enum ThreadTask {
Search(Box<SearchTask>),
ClearHistories,
}
struct SearchTask {
pos: Position,
limits: LimitsType,
max_depth: Depth,
time_options: TimeOptions,
max_moves_to_draw: i32,
draw_value_black: i32,
draw_value_white: i32,
search_tune_params: SearchTuneParams,
skill_enabled: bool,
}
pub struct Thread {
id: usize,
inner: Arc<ThreadInner>,
handle: Option<JoinHandle<()>>,
}
impl Thread {
fn new(
id: usize,
tt: Arc<TranspositionTable>,
eval_hash: Arc<EvalHash>,
stop: Arc<AtomicBool>,
ponderhit: Arc<AtomicBool>,
increase_depth_shared: Arc<AtomicBool>,
max_moves_to_draw: i32,
search_tune_params: SearchTuneParams,
) -> Self {
let worker =
SearchWorker::new(tt, eval_hash, max_moves_to_draw, id, search_tune_params);
let progress = Arc::new(SearchProgress::new());
let inner = Arc::new(ThreadInner {
worker: Mutex::new(worker),
state: Mutex::new(ThreadState {
searching: true,
exit: false,
task: None,
}),
condvar: Condvar::new(),
stop,
ponderhit,
increase_depth_shared,
progress,
});
let inner_clone = Arc::clone(&inner);
let handle = std::thread::Builder::new()
.stack_size(SEARCH_STACK_SIZE)
.spawn(move || idle_loop(inner_clone))
.expect("failed to spawn search helper thread");
let thread = Self {
id,
inner,
handle: Some(handle),
};
thread.wait_for_search_finished();
thread
}
pub fn id(&self) -> usize {
self.id
}
fn start_searching(&self, task: SearchTask) {
self.schedule_task(ThreadTask::Search(Box::new(task)));
}
fn clear_worker(&self) {
self.schedule_task(ThreadTask::ClearHistories);
}
fn schedule_task(&self, task: ThreadTask) {
let mut state = self.inner.state.lock().unwrap();
while state.searching {
state = self.inner.condvar.wait(state).unwrap();
}
state.task = Some(task);
state.searching = true;
self.inner.condvar.notify_one();
}
pub fn wait_for_search_finished(&self) {
let mut state = self.inner.state.lock().unwrap();
while state.searching {
state = self.inner.condvar.wait(state).unwrap();
}
}
pub fn with_worker<F, R>(&self, f: F) -> R
where
F: FnOnce(&mut SearchWorker) -> R,
{
let mut worker = self.inner.worker.lock().unwrap();
f(&mut worker)
}
pub fn nodes(&self) -> u64 {
self.inner.progress.nodes()
}
pub fn best_move_changes(&self) -> f64 {
self.inner.progress.best_move_changes()
}
}
impl Drop for Thread {
fn drop(&mut self) {
{
let mut state = self.inner.state.lock().unwrap();
state.exit = true;
state.searching = true;
self.inner.condvar.notify_one();
}
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
}
fn idle_loop(inner: Arc<ThreadInner>) {
loop {
let task = {
let mut state = inner.state.lock().unwrap();
state.searching = false;
inner.condvar.notify_all();
while !state.searching && !state.exit {
state = inner.condvar.wait(state).unwrap();
}
if state.exit {
return;
}
state.task.take()
};
match task {
Some(ThreadTask::Search(task)) => {
let task = *task;
inner.progress.reset();
let mut worker = inner.worker.lock().unwrap();
worker.max_moves_to_draw = task.max_moves_to_draw;
worker.search_tune_params = task.search_tune_params;
worker.draw_value_black = task.draw_value_black;
worker.draw_value_white = task.draw_value_white;
worker.prepare_search();
let mut pos = task.pos;
let mut time_manager =
TimeManagement::new(Arc::clone(&inner.stop), Arc::clone(&inner.ponderhit));
time_manager.set_options(&task.time_options);
time_manager.init(
&task.limits,
pos.side_to_move(),
pos.game_ply(),
task.max_moves_to_draw,
);
search_helper(
&mut worker,
&mut pos,
&task.limits,
&mut time_manager,
task.max_depth,
task.skill_enabled,
Some(&inner.progress),
&inner.increase_depth_shared,
);
}
Some(ThreadTask::ClearHistories) => {
inner.progress.reset();
let mut worker = inner.worker.lock().unwrap();
worker.clear();
}
None => {}
}
}
}
}
#[cfg(all(target_arch = "wasm32", not(feature = "wasm-threads")))]
mod imp {
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use crate::eval::EvalHash;
use crate::position::Position;
use crate::tt::TranspositionTable;
use crate::types::Depth;
use crate::search::{LimitsType, SearchTuneParams, TimeOptions};
pub struct ThreadPool {
_stop: Arc<AtomicBool>,
_ponderhit: Arc<AtomicBool>,
}
impl ThreadPool {
pub fn new(
_num_threads: usize,
_tt: Arc<TranspositionTable>,
_eval_hash: Arc<EvalHash>,
stop: Arc<AtomicBool>,
ponderhit: Arc<AtomicBool>,
_increase_depth_shared: Arc<AtomicBool>,
_max_moves_to_draw: i32,
_search_tune_params: SearchTuneParams,
) -> Self {
Self {
_stop: stop,
_ponderhit: ponderhit,
}
}
pub fn set_num_threads(
&mut self,
_num_threads: usize,
_tt: Arc<TranspositionTable>,
_eval_hash: Arc<EvalHash>,
_max_moves_to_draw: i32,
_search_tune_params: SearchTuneParams,
) {
}
pub fn start_thinking(
&self,
_pos: &Position,
_limits: LimitsType,
_max_depth: Depth,
_time_options: TimeOptions,
_max_moves_to_draw: i32,
_draw_value_black: i32,
_draw_value_white: i32,
_skill_enabled: bool,
) {
}
pub fn wait_for_search_finished(&self) {
}
pub fn clear_histories(&self) {
}
pub fn update_tt(&mut self, _tt: Arc<TranspositionTable>) {
}
pub fn update_eval_hash(&mut self, _eval_hash: Arc<EvalHash>) {
}
pub fn update_search_tune_params(&mut self, _search_tune_params: SearchTuneParams) {
}
pub fn helper_threads(&self) -> &[Thread] {
&[]
}
pub fn clear_helper_results(&self) {
}
}
pub struct Thread;
impl Thread {
pub fn id(&self) -> usize {
0
}
pub fn with_worker<F, R>(&self, _f: F) -> R
where
F: FnOnce(&mut crate::search::SearchWorker) -> R,
{
unreachable!("thread pool is disabled on wasm32")
}
pub fn nodes(&self) -> u64 {
0
}
pub fn best_move_changes(&self) -> f64 {
0.0
}
}
}
#[cfg(all(target_arch = "wasm32", feature = "wasm-threads"))]
mod imp {
use std::cell::RefCell;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use rayon::prelude::*;
use crate::eval::EvalHash;
use crate::position::Position;
use crate::tt::TranspositionTable;
use crate::types::{Depth, Move, Value};
use crate::search::engine::search_helper;
use crate::search::{LimitsType, SearchTuneParams, SearchWorker, TimeManagement, TimeOptions};
thread_local! {
static THREAD_WORKER: RefCell<Option<Box<SearchWorker>>> = const { RefCell::new(None) };
}
#[derive(Debug, Clone)]
pub struct HelperResult {
pub thread_id: usize,
pub nodes: u64,
pub best_move_changes: f64,
pub completed_depth: Depth,
pub best_move: Move,
pub best_score: Value,
pub top_moves: Vec<(Move, Value)>,
}
pub struct HelperProgress {
nodes: AtomicU64,
best_move_changes_bits: AtomicU64,
}
impl HelperProgress {
pub fn new() -> Self {
Self {
nodes: AtomicU64::new(0),
best_move_changes_bits: AtomicU64::new(0.0f64.to_bits()),
}
}
pub fn reset(&self) {
self.nodes.store(0, Ordering::Relaxed);
self.best_move_changes_bits.store(0.0f64.to_bits(), Ordering::Relaxed);
}
pub fn update(&self, nodes: u64, best_move_changes: f64) {
self.nodes.store(nodes, Ordering::Relaxed);
self.best_move_changes_bits
.store(best_move_changes.to_bits(), Ordering::Relaxed);
}
pub fn nodes(&self) -> u64 {
self.nodes.load(Ordering::Relaxed)
}
pub fn best_move_changes(&self) -> f64 {
f64::from_bits(self.best_move_changes_bits.load(Ordering::Relaxed))
}
}
pub struct ThreadPool {
num_threads: usize,
tt: Arc<TranspositionTable>,
eval_hash: Arc<EvalHash>,
stop: Arc<AtomicBool>,
ponderhit: Arc<AtomicBool>,
increase_depth_shared: Arc<AtomicBool>,
max_moves_to_draw: i32,
search_tune_params: SearchTuneParams,
pending_tasks: Arc<AtomicUsize>,
helper_results: Arc<Mutex<Vec<HelperResult>>>,
helper_progress: Vec<Arc<HelperProgress>>,
}
impl ThreadPool {
pub fn new(
num_threads: usize,
tt: Arc<TranspositionTable>,
eval_hash: Arc<EvalHash>,
stop: Arc<AtomicBool>,
ponderhit: Arc<AtomicBool>,
increase_depth_shared: Arc<AtomicBool>,
max_moves_to_draw: i32,
search_tune_params: SearchTuneParams,
) -> Self {
let num_threads = num_threads.max(1);
let helper_count = num_threads.saturating_sub(1);
let helper_progress =
(0..helper_count).map(|_| Arc::new(HelperProgress::new())).collect();
Self {
num_threads,
tt,
eval_hash,
stop,
ponderhit,
increase_depth_shared,
max_moves_to_draw,
search_tune_params,
pending_tasks: Arc::new(AtomicUsize::new(0)),
helper_results: Arc::new(Mutex::new(Vec::new())),
helper_progress,
}
}
pub fn set_num_threads(
&mut self,
num_threads: usize,
tt: Arc<TranspositionTable>,
eval_hash: Arc<EvalHash>,
max_moves_to_draw: i32,
search_tune_params: SearchTuneParams,
) {
let num_threads = num_threads.max(1);
let helper_count = num_threads.saturating_sub(1);
self.helper_progress
.resize_with(helper_count, || Arc::new(HelperProgress::new()));
self.helper_progress.truncate(helper_count);
self.num_threads = num_threads;
self.tt = tt;
self.eval_hash = eval_hash;
self.max_moves_to_draw = max_moves_to_draw;
self.search_tune_params = search_tune_params;
}
pub fn start_thinking(
&self,
pos: &Position,
limits: LimitsType,
max_depth: Depth,
time_options: TimeOptions,
max_moves_to_draw: i32,
draw_value_black: i32,
draw_value_white: i32,
skill_enabled: bool,
) {
match self.helper_results.lock() {
Ok(mut results) => results.clear(),
Err(e) => {
#[cfg(debug_assertions)]
eprintln!("Warning: Failed to lock helper_results for clearing: {e}");
}
}
self.pending_tasks.store(0, Ordering::Relaxed);
let helper_count = self.num_threads.saturating_sub(1);
if helper_count == 0 {
return;
}
let search_tune_params = self.search_tune_params;
self.pending_tasks.store(helper_count, Ordering::Release);
for progress in &self.helper_progress {
progress.reset();
}
for thread_id in 1..=helper_count {
let stop = Arc::clone(&self.stop);
let ponderhit = Arc::clone(&self.ponderhit);
let increase_depth = Arc::clone(&self.increase_depth_shared);
let tt = Arc::clone(&self.tt);
let eval_hash = Arc::clone(&self.eval_hash);
let pending = Arc::clone(&self.pending_tasks);
let helper_results = Arc::clone(&self.helper_results);
let progress = Arc::clone(&self.helper_progress[thread_id - 1]);
let pos_clone = pos.clone();
let limits_clone = limits.clone();
rayon::spawn_fifo(move || {
THREAD_WORKER.with(|cell| {
let mut worker_opt = cell.borrow_mut();
if worker_opt.is_none() {
*worker_opt = Some(SearchWorker::new(
Arc::clone(&tt),
Arc::clone(&eval_hash),
max_moves_to_draw,
thread_id,
search_tune_params,
));
}
let worker = worker_opt.as_mut().unwrap();
worker.thread_id = thread_id;
worker.tt = Arc::clone(&tt);
worker.eval_hash = Arc::clone(&eval_hash);
worker.max_moves_to_draw = max_moves_to_draw;
worker.draw_value_black = draw_value_black;
worker.draw_value_white = draw_value_white;
worker.search_tune_params = search_tune_params;
worker.prepare_search();
let mut search_pos = pos_clone;
let mut time_manager =
TimeManagement::new(Arc::clone(&stop), Arc::clone(&ponderhit));
time_manager.set_options(&time_options);
time_manager.init(
&limits_clone,
search_pos.side_to_move(),
search_pos.game_ply(),
max_moves_to_draw,
);
search_helper(
worker,
&mut search_pos,
&limits_clone,
&mut time_manager,
max_depth,
skill_enabled,
Some(&*progress),
&increase_depth,
);
let top_moves: Vec<(Move, Value)> = worker
.state
.root_moves
.iter()
.take(4) .map(|rm| (rm.mv(), rm.score))
.collect();
let result = HelperResult {
thread_id,
nodes: worker.state.nodes,
best_move_changes: worker.state.best_move_changes,
completed_depth: worker.state.completed_depth,
best_move: worker.state.best_move,
best_score: worker
.state
.root_moves
.get(0)
.map(|rm| rm.score)
.unwrap_or(Value::ZERO),
top_moves,
};
match helper_results.lock() {
Ok(mut results) => results.push(result),
Err(e) => {
#[cfg(debug_assertions)]
eprintln!(
"Warning: Failed to lock helper_results for pushing: {e}"
);
}
}
});
pending.fetch_sub(1, Ordering::Release);
});
}
}
pub fn wait_for_search_finished(&self) {
const SPIN_LIMIT: u32 = 100;
let mut spin_count: u32 = 0;
while self.pending_tasks.load(Ordering::Acquire) > 0 {
if spin_count < SPIN_LIMIT {
std::hint::spin_loop();
spin_count += 1;
} else {
std::thread::yield_now();
spin_count = 0;
}
}
}
pub fn clear_histories(&self) {
let helper_count = self.num_threads.saturating_sub(1);
if helper_count == 0 {
return;
}
(1..=helper_count).into_par_iter().for_each(|_| {
THREAD_WORKER.with(|cell| {
if let Some(worker) = cell.borrow_mut().as_mut() {
worker.clear();
}
});
});
}
pub fn update_tt(&mut self, tt: Arc<TranspositionTable>) {
self.tt = tt;
}
pub fn update_eval_hash(&mut self, eval_hash: Arc<EvalHash>) {
self.eval_hash = eval_hash;
}
pub fn update_search_tune_params(&mut self, search_tune_params: SearchTuneParams) {
self.search_tune_params = search_tune_params;
}
pub fn helper_threads(&self) -> &[Thread] {
&[]
}
pub fn helper_results(&self) -> Vec<HelperResult> {
self.helper_results.lock().map(|guard| guard.clone()).unwrap_or_default()
}
pub fn clear_helper_results(&self) {
match self.helper_results.lock() {
Ok(mut results) => results.clear(),
Err(e) => {
#[cfg(debug_assertions)]
eprintln!("Warning: Failed to lock helper_results for clearing: {e}");
}
}
}
pub fn helper_nodes(&self) -> u64 {
self.helper_progress.iter().fold(0u64, |acc, p| acc.saturating_add(p.nodes()))
}
pub fn helper_best_move_changes(&self) -> Vec<f64> {
self.helper_progress.iter().map(|p| p.best_move_changes()).collect()
}
}
pub struct Thread;
impl Thread {
pub fn id(&self) -> usize {
0
}
pub fn with_worker<F, R>(&self, _f: F) -> R
where
F: FnOnce(&mut SearchWorker) -> R,
{
unreachable!("rayon thread pool does not expose individual threads")
}
pub fn nodes(&self) -> u64 {
0
}
pub fn best_move_changes(&self) -> f64 {
0.0
}
}
}
pub use imp::*;