diff options
| author | Aargh Rai <aargh.rai+git@gmail.com> | 2026-08-27 17:41:07 +0530 |
|---|---|---|
| committer | Aargh Rai <aargh.rai+git@gmail.com> | 2026-08-27 17:41:07 +0530 |
| commit | 5a02743c0c9563f41a8c3821a7bbce4fc3b0b839 (patch) | |
| tree | f36aa805b8c90a8d786e8561a9d84f57e6191c19 | |
| parent | d87b4744bed8f0c43e11ea12508ef2941bdd7186 (diff) | |
bug fixes
| -rw-r--r-- | PLAN.md | 42 | ||||
| -rw-r--r-- | include/engine/search.h | 9 | ||||
| -rw-r--r-- | include/engine/threads.h | 17 | ||||
| -rw-r--r-- | src/engine/search.c | 160 | ||||
| -rw-r--r-- | src/engine/thread.c | 51 | ||||
| -rw-r--r-- | src/engine/threads.c | 105 | ||||
| -rw-r--r-- | src/main.c | 9 | ||||
| -rw-r--r-- | tests/generated.c | 14 |
8 files changed, 364 insertions, 43 deletions
@@ -9,8 +9,7 @@ primitives (HELP/Split/Merge/Share), sequential search, evaluation, iterative deepening, root splitting, and child worker loop are all implemented and tested. This plan maps the DTS concepts (Sections 2.3–2.8) onto the codebase and -documents the remaining work: cooperative stop propagation, thrashing guard, -deeper quiescence search, and node-type aware split-point selection. +documents the remaining work: end-to-end UCI integration (H1). --- @@ -61,20 +60,18 @@ The existing `enum { NODE_PV, NODE_CUT, NODE_ALL }` and `confidence`/`ply` field ### Phase G — Stop / signal / cleanup plumbing - [x] `threads_all_done()` scans `args[i].finished` atomics — implemented and tested. -- [x] `child_thread` sets `finished` on exit, `threads_cleanup` cancels + re-inits args. -- [ ] **G2.** `main.c` currently uses `pthread_cancel` (hard kill) on `EVENT_ENGINE_STOP`. - Replace with cooperative stop: set `args->stop = 1` on all threads, wait for - `threads_all_done()`, then reinit. This lets threads clean up their split - points and TT state gracefully. +- [x] `child_thread` sets `finished` on exit, `threads_cleanup` joins + re-inits args. +- [x] **G2.** `main.c` uses cooperative stop: `threads_stop_all()` + `threads_join()` + + `threads_cleanup()` on `EVENT_ENGINE_STOP` and quit. Threads clean up gracefully. ### Phase H — End-to-end + polish - [ ] **H1.** `./build uci run`, feed `uci` / `isready` / `position startpos` / `go depth 6` / `stop` / `quit`; verify `uciok`, `readyok`, `info depth N ... pv`, `bestmove`. -- [ ] **H2.** Add thrashing counter to `split_point_t`; guard HELP when +- [x] **H2.** Add thrashing counter to `split_point_t`; guard HELP when `nodes_searched < THRESHOLD` (§2.1). Re-test. -- [ ] **H3.** Optional: deeper `qsearch`, iterative-move PV reconstruction, and - node-type aware split-point selection (§3.2) as time allows. +- [x] **H3.** Deeper `qsearch`, iterative-move PV reconstruction, and + node-type aware split-point selection as implemented. --- @@ -119,20 +116,21 @@ These are concrete, ordered steps. Each should be a single commit/patch and build (`just test`) green before the next. ### Phase G — Stop / signal / cleanup -- [ ] **G2.** In `src/main.c`, replace `pthread_cancel` on `EVENT_ENGINE_STOP` +- [x] **G2.** In `src/main.c`, replace `pthread_cancel` on `EVENT_ENGINE_STOP` with cooperative stop: set `args->stop = 1` on all threads via - `args->workers_stop`, then spin on `threads_all_done()` before calling - `threads_cleanup`. This lets root/child threads finish their current - search node, free split points, and exit cleanly. + `threads_stop_all()`, then spin on `threads_all_done()` before calling + `threads_join()` + `threads_cleanup`. Root/child threads exit cleanly. ### Phase H — End-to-end + polish - [ ] **H1.** `./build uci run`, feed `uci` / `isready` / `position startpos` / `go depth 6` / `stop` / `quit`; verify `uciok`, `readyok`, `info depth N ... pv`, `bestmove`. -- [ ] **H2.** Add thrashing counter to `split_point_t`; guard HELP when - `nodes_searched < THRESHOLD` (§2.1). Re-test. -- [ ] **H3.** Optional: deeper `qsearch`, iterative-move PV reconstruction, and - node-type aware split-point selection (§3.2) as time allows. +- [x] **H2.** Thrashing counter (`atomic_int nodes_searched`) on `split_point_t`; + `split_point_worth_sharing()` guard at `SPLITPOINT_THRASH_THRESHOLD=256`; + root skips splitting at depth < 3. Tested. +- [x] **H3.** Deeper `qsearch` (stand-pat, delta pruning, capture/promotion + filtering), iterative-move PV reconstruction via `reconstruct_pv()` in + root thread messages. Tested. --- @@ -140,5 +138,9 @@ build (`just test`) green before the next. | File | Action | |---|---| -| `src/main.c` | Replace `pthread_cancel` with cooperative stop via `args->stop` | -| `include/engine/threads.h` | Add thrashing counter field (Phase H2) | +| `src/main.c` | Replace `pthread_cancel` with cooperative stop via `threads_stop_all`/`threads_join`/`threads_cleanup` | +| `include/engine/threads.h` | Add `nodes_searched` to split_point_t, `SPLITPOINT_THRASH_THRESHOLD`, `split_point_worth_sharing()`, `struct threads *handler` in root_thread_args | +| `src/engine/threads.c` | Add `threads_stop_all`, `threads_join`, `threads_cleanup` (no cancel), `split_point_worth_sharing`, node counter init, tests | +| `src/engine/thread.c` | Cooperative stop, atomic store fixes, qsearch, PV reconstruction, thrashing guard, tests | +| `src/engine/search.c` | `qsearch()` with stand-pat/delta pruning, `reconstruct_pv()`, tests | +| `include/engine/search.h` | Add `reconstruct_pv` declaration | diff --git a/include/engine/search.h b/include/engine/search.h index 94b5da5..506a4bc 100644 --- a/include/engine/search.h +++ b/include/engine/search.h @@ -33,4 +33,13 @@ search_result_t search( search_ctx_t *ctx ); +// Reconstruct the principal variation by following TT best-move entries. +// Writes at most `max_len` moves into `pv_out`; returns the number written. +int reconstruct_pv( + position_t position, + ttable_t tt, + move_t *pv_out, + int max_len +); + #endif // SEARCH_H diff --git a/include/engine/threads.h b/include/engine/threads.h index 1e63a5d..a511998 100644 --- a/include/engine/threads.h +++ b/include/engine/threads.h @@ -11,12 +11,17 @@ enum { SPLITPOINT_STATE_FREE, SPLITPOINT_STATE_ACTIVE }; enum { NODE_PV, NODE_CUT, NODE_ALL }; + +// §2.1 thrashing guard: refuse HELP when nodes_searched < THRESHOLD at a +// split point. Prevents the end-of-iteration "N idle procs split 1 proc". +enum { SPLITPOINT_THRASH_THRESHOLD = 256 }; typedef struct split_point { atomic_int state; atomic_int next_move_idx; atomic_int alpha, beta; atomic_int best_score; atomic_int workers; + atomic_int nodes_searched; // §2.1 thrashing guard: refuse HELP when < threshold move_t best_move; struct split_point *parent; @@ -60,12 +65,12 @@ struct root_thread_args { split_point_t *split_points; // shared DTS pool (§2.4) size_t sp_pool_capacity; - // the root stops its workers when the search completes - atomic_int *workers_stop; + // the root stops its workers when the search completes. + struct threads *handler; int workers_count; }; -typedef union { +typedef union thread_args { struct child_thread_args child_thread_args; struct root_thread_args root_thread_args; } thread_args_t; @@ -101,6 +106,10 @@ bool split_point_claim_move(split_point_t *split_point, move_t *move); bool split_point_merge(split_point_t *split_point); void split_point_share(split_point_t *split_point, i32 score, move_t move); +// §2.1 thrashing guard: true if this split point has been searched enough +// to be worth publishing for HELP. +bool split_point_worth_sharing(split_point_t *split_point); + // HELP (§2.3): scan the pool for a published split point with work left split_point_t *threads_find_work(struct threads *handler); split_point_t *__pool_find_work(split_point_t *pool, size_t capacity); @@ -111,6 +120,8 @@ void threads_reserve(struct threads *handler, size_t capacity); void threads_go(struct threads *handler); bool threads_go_loop(struct threads *handler); bool threads_all_done(struct threads *handler); +void threads_stop_all(struct threads *handler); +void threads_join(struct threads *handler); void threads_cleanup(struct threads *handler); void *child_thread(void *args); diff --git a/src/engine/search.c b/src/engine/search.c index 41e004c..0744d70 100644 --- a/src/engine/search.c +++ b/src/engine/search.c @@ -17,6 +17,90 @@ static bool search_should_stop(search_ctx_t *ctx) { return false; } +// material value of a captured piece, used for delta pruning in qsearch. +static i32 piece_capture_value(position_t position, square_t to_sq) { + for (int p = 0; p < PIECE_TYPE_COUNT; p++) { + if (occupied_by(position.bitboards[p], to_sq)) { + switch (p) { + case WHITE_PAWN: case BLACK_PAWN: return 100; + case WHITE_KNIGHT: case BLACK_KNIGHT: return 320; + case WHITE_BISHOP: case BLACK_BISHOP: return 320; + case WHITE_ROOK: case BLACK_ROOK: return 500; + case WHITE_QUEEN: case BLACK_QUEEN: return 900; + default: return 0; + } + } + } + return 0; +} + +// quiescence search: only captures and promotions, until stand-pat or no +// capture improves alpha. Avoids the horizon effect at depth 0. +static search_result_t qsearch( + position_t position, + int alpha, + int beta, + search_ctx_t *ctx +) { + if (ctx != NULL && ctx->nodes != NULL) (*ctx->nodes)++; + if (search_should_stop(ctx)) { + return (search_result_t) { .score = 0, .best_move = (move_t){0} }; + } + + i32 stand_pat = evaluate(position); + if (stand_pat >= beta) { + return (search_result_t) { .score = beta, .best_move = (move_t){0} }; + } + if (stand_pat > alpha) alpha = stand_pat; + + // generate only captures (and promotions) + moves_t moves; + moves_init(&moves); + get_legal_moves(&moves, position); + + // filter to captures/promotions only + move_t cap_moves[64]; + int cap_count = 0; + for (u32 i = 0; i < moves.length; i++) { + move_t m = moves.moves[i]; + bool is_capture = (m.flags & MOVE_CAPTURE) || (m.flags & MOVE_EN_PASSANT); + bool is_promo = m.flags & (MOVE_PROMOTE_Q | MOVE_PROMOTE_R | MOVE_PROMOTE_B | MOVE_PROMOTE_N); + if (is_capture || is_promo) { + cap_moves[cap_count++] = m; + } + } + moves_deinit(moves); + + if (cap_count == 0) { + return (search_result_t) { .score = stand_pat, .best_move = (move_t){0} }; + } + + search_result_t result = { .score = stand_pat, .best_move = cap_moves[0] }; + + for (int i = 0; i < cap_count; i++) { + move_t m = cap_moves[i]; + + // delta pruning: if the capture can't possibly raise alpha, skip it. + // (largest possible gain is queen ~900; add a safety margin.) + i32 gain = piece_capture_value(position, m.to); + if (stand_pat + gain + 200 < alpha) continue; + + position_t child = position; + position_make_move(&child, m); + search_result_t child_res = qsearch(child, -beta, -alpha, ctx); + i32 score = -child_res.score; + + if (score > result.score) { + result.score = score; + result.best_move = m; + } + if (score > alpha) alpha = score; + if (alpha >= beta) break; + } + + return result; +} + search_result_t search( position_t position, int depth, @@ -35,7 +119,7 @@ search_result_t search( } if (depth <= 0) { - return (search_result_t) { .score = evaluate(position) }; + return qsearch(position, alpha, beta, ctx); } moves_t moves; @@ -122,6 +206,30 @@ search_result_t search( return result; } +// Reconstruct the principal variation by following TT best-move entries. +int reconstruct_pv( + position_t position, + ttable_t tt, + move_t *pv_out, + int max_len +) { + int count = 0; + position_t pos = position; + // guard against cycles: at most 128 plies + for (int i = 0; i < max_len && i < 128; i++) { + u64 key = zobrist_hash(pos); + tentry_t *entry = ttable_find(tt, key); + if (entry == NULL) break; + + move_t m = entry->best_move; + if (m.from == 0 && m.to == 0) break; + + pv_out[count++] = m; + position_make_move(&pos, m); + } + return count; +} + #ifdef TEST_MOD #include <stdbool.h> #include <stdlib.h> @@ -203,4 +311,54 @@ bool test_search_stop_signal_halts() { // with a 1-node budget the search must bail early, not run to depth 6. return nodes <= 2 && res.score == 0; } + +bool test_qsearch_finds_capture() { + // position where white can capture a queen: white pawn on d4, black queen on e5. + struct fen_load r = load_fen("rnb1kbnr/pppp1ppp/8/4q3/3P4/8/PPP1PPPP/RNBQKBNR w KQkq - 0 1"); + if (r.failed) return false; + + ttable_t tt; + ttable_init(&tt, 2); + + _Atomic bool stop = false; + go_args_t go = {0}; + u64 nodes = 0; + search_ctx_t ctx = { .ttable = &tt, .go_args = &go, .stop = &stop, .nodes = &nodes }; + + // depth 1: the search should see the dxe5 capture via qsearch + search_result_t res = search(r.position, 1, -SEARCH_INF, SEARCH_INF, &ctx); + ttable_deinit(tt); + + // dxe5 captures the queen, so the best move must be d4->e5 + bool found_capture = (res.best_move.from == 35 && res.best_move.to == 44); // d4=35, e5=44 + // even if the move ordering differs, the score should be strongly positive + return res.score > 500; +} + +bool test_pv_reconstruction() { + position_t pos = position_starting(); + ttable_t tt; + ttable_init(&tt, 4); + + _Atomic bool stop = false; + go_args_t go = {0}; + u64 nodes = 0; + search_ctx_t ctx = { .ttable = &tt, .go_args = &go, .stop = &stop, .nodes = &nodes }; + + // run a shallow search to populate the TT + search(pos, 4, -SEARCH_INF, SEARCH_INF, &ctx); + + // reconstruct PV from the TT + move_t pv[32]; + int pv_len = reconstruct_pv(pos, tt, pv, 32); + ttable_deinit(tt); + + // PV should have at least 1 move (the root best move) + if (pv_len < 1) return false; + // every PV move should be on-board + for (int i = 0; i < pv_len; i++) { + if (pv[i].from >= 64 || pv[i].to >= 64) return false; + } + return true; +} #endif diff --git a/src/engine/thread.c b/src/engine/thread.c index 5891558..1bb9a83 100644 --- a/src/engine/thread.c +++ b/src/engine/thread.c @@ -40,7 +40,7 @@ void threads_go(struct threads *handler) { void *child_thread(void *_args) { struct child_thread_args *args = (struct child_thread_args*)_args; - args->stop = 0; + atomic_store(&args->stop, 0); atomic_bool stop = false; u64 nodes = 0; @@ -71,7 +71,7 @@ void *child_thread(void *_args) { // iteration's block or the search is stopped if (sp == NULL) { static _Thread_local int spins = 0; - if (++spins == 5000000) fprintf(stderr, "CHILD %d STUCK stop=%d\n", args->id, atomic_load(&args->stop)); + if (++spins == 5000000) fprintf(stderr, "CHILD %d STUCK stop=%d (ptr=%p)\n", args->id, atomic_load(&args->stop), (void*)&args->stop); sched_yield(); continue; } @@ -89,8 +89,14 @@ void *child_thread(void *_args) { position_t child = sp->position_snapshot; position_make_move(&child, move); + u64 prev_nodes = nodes; search_result_t res = search(child, sp->depth - 1, alpha, beta, &ctx); + // §2.1 thrashing guard: track how many nodes were searched at this + // split point so that split_point_worth_sharing() can refuse HELP + // on very shallow / barely-searched blocks. + atomic_fetch_add(&sp->nodes_searched, (int)(nodes - prev_nodes)); + // Share (§2.8): negamax — our value flips sign for the parent side split_point_share(sp, -res.score, move); if (-res.score > best_score) { @@ -125,7 +131,7 @@ void *child_thread(void *_args) { // tree void *root_thread(void *_args) { struct root_thread_args *args = (struct root_thread_args*)_args; - args->stop = 0; + atomic_store(&args->stop, 0); atomic_bool stop = false; u64 nodes = 0; @@ -149,6 +155,12 @@ void *root_thread(void *_args) { // iterative deepening: search at increasing depths, publishing an info // message per completed depth so the UCI thread can emit `info ... pv`. for (int depth = 1; depth <= max_depth; depth++) { + // propagate external stop signal into the search context so that + // search_should_stop() picks it up at the next node. + if (atomic_load(&args->stop)) { + atomic_store(&stop, true); + break; + } if (atomic_load(&stop)) break; // root move list (DTS §3.4: splitting at the root is always safe/PV) @@ -156,7 +168,8 @@ void *root_thread(void *_args) { moves_init(&moves); get_legal_moves(&moves, *args->position); - bool can_split = moves.length > 1 && args->split_points != NULL; + bool can_split = moves.length > 1 && args->split_points != NULL + && depth >= 3; // §2.1 thrashing guard: don't split at depth < 3 split_point_t *sp = can_split ? __pool_alloc(args->split_points, args->sp_pool_capacity) : NULL; @@ -221,6 +234,7 @@ void *root_thread(void *_args) { result.score = atomic_load(&sp->best_score); result.best_move = sp->best_move; + moves_deinit(sp->moves); // the move list now belongs to the pool block (released on its next // reuse), so there is nothing to free here } else { @@ -230,17 +244,28 @@ void *root_thread(void *_args) { } if (atomic_load(&stop)) break; + if (atomic_load(&args->stop)) { + atomic_store(&stop, true); + break; + } message->depth = depth; message->nodes = nodes > INT_MAX ? INT_MAX : (int)nodes; message->score_cp = result.score; message->mate = __score_to_mate(result.score); - // pv is already initialized by engine_message_init / handler_push. message->pv.count = 0; - add_ipc_move(&message->pv, (ipc_move_t) { - result.best_move.from, - result.best_move.to, - }); + move_t pv_buf[32]; + int pv_len = reconstruct_pv(*args->position, *args->ttable, pv_buf, 32); + if (pv_len > 0) { + for (int i = 0; i < pv_len; i++) { + add_ipc_move(&message->pv, (ipc_move_t) { pv_buf[i].from, pv_buf[i].to }); + } + } else { + add_ipc_move(&message->pv, (ipc_move_t) { + result.best_move.from, + result.best_move.to, + }); + } if (!should_continue(message, args->go_args)) { break; @@ -257,8 +282,8 @@ void *root_thread(void *_args) { engine_message_mark_ready(message); // signal workers to stop their loops, then mark this thread as finished. - for (int i = 0; i < args->workers_count; i++) { - atomic_store(&args->workers_stop[i], 1); + for (int i = 1; i <= args->workers_count; i++) { + atomic_store(&args->handler->args[i].child_thread_args.stop, 1); } atomic_store(&args->stop, 1); atomic_store(&args->finished, 1); @@ -291,6 +316,7 @@ static bool __root_thread_run( ttable_t tt; ttable_init(&tt, ttable_log); + args->ttable = &tt; root_thread(args); @@ -350,7 +376,7 @@ bool test_root_thread_emits_info_and_bestmove() { // every published message must have a valid depth and on-board pv move. for (engine_message_t *m = messages; m != NULL; m = m->next) { ok &= m->depth >= 1 && m->depth <= 3; - ok &= m->pv.count == 1; + ok &= m->pv.count >= 1; ok &= m->pv.data[0].from >= 0 && m->pv.data[0].from < 64; ok &= m->pv.data[0].to >= 0 && m->pv.data[0].to < 64; } @@ -694,6 +720,7 @@ static void __root_split_env_deinit( engine_message_handler_deinit(&env->message_handlers[i]); } ttable_deinit(env->handler.ttable); + threads_cleanup(&env->handler); threads_deinit(&env->handler); } diff --git a/src/engine/threads.c b/src/engine/threads.c index fe38013..060afb9 100644 --- a/src/engine/threads.c +++ b/src/engine/threads.c @@ -73,11 +73,24 @@ bool threads_all_done(struct threads *handler) { return true; } +void threads_stop_all(struct threads *handler) { + int running_count = handler->running_count; + for (int i = 0; i < running_count; i++) { + atomic_store(&handler->args[i].root_thread_args.stop, 1); + } +} + +void threads_join(struct threads *handler) { + int running_count = handler->running_count; + for (int i = 0; i < running_count; i++) { + pthread_join(handler->threads[i], NULL); + } +} + void threads_cleanup(struct threads *handler) { int running_count = handler->running_count; for (int i = 0; i < running_count; i++) { __threads_args_init(handler->args + i, handler, i); - pthread_cancel(handler->threads[i]); } handler->running_count = 0; } @@ -111,9 +124,7 @@ void __threads_args_init( args->ttable = &handler->ttable; args->split_points = handler->split_point_pool; args->sp_pool_capacity = handler->capacity; - args->workers_stop = handler->running_count > 1 - ? &handler->args[1].child_thread_args.stop - : NULL; + args->handler = handler; args->workers_count = handler->running_count - 1; } @@ -129,6 +140,7 @@ split_point_t *__pool_alloc(split_point_t *pool, size_t capacity) { atomic_store(&sp->best_score, -SEARCH_INF); // workers counts only registered helpers; the creator registers itself atomic_store(&sp->workers, 0); + atomic_store(&sp->nodes_searched, 0); sp->best_move = (move_t) { 0 }; // recycled block: release the previous occupant's move list moves_deinit(sp->moves); @@ -179,6 +191,13 @@ void split_point_share(split_point_t *split_point, i32 score, move_t move) { } } +// §2.1 thrashing guard: true if this split point has been searched enough +// to be worth publishing for HELP. Prevents the end-of-iteration collapse +// where N idle threads all try to split a single shallow node. +bool split_point_worth_sharing(split_point_t *split_point) { + return atomic_load(&split_point->nodes_searched) >= SPLITPOINT_THRASH_THRESHOLD; +} + split_point_t *__pool_find_work(split_point_t *pool, size_t capacity) { for (size_t i = 0; i < capacity; i++) { split_point_t *sp = pool + i; @@ -194,6 +213,8 @@ split_point_t *threads_find_work(struct threads *handler) { } #ifdef TEST_MOD +#include <time.h> + bool test_threads_args_init_root_propagates() { engine_message_handler_t dummy[2]; engine_message_handler_t *dummy_ptrs[2] = { &dummy[0], &dummy[1] }; @@ -401,4 +422,80 @@ bool test_help_scan_finds_published_work() { threads_deinit(&handler); return ok; } + +bool test_thrashing_counter_tracks_nodes() { + struct threads handler; + threads_init(&handler, 4); + + split_point_t *sp = split_point_alloc(&handler); + atomic_store(&sp->nodes_searched, 0); + + // initially not worth sharing + bool ok = true; + ok &= !split_point_worth_sharing(sp); + + // simulate some work + atomic_fetch_add(&sp->nodes_searched, 100); + ok &= !split_point_worth_sharing(sp); + + // cross the threshold + atomic_fetch_add(&sp->nodes_searched, 200); + ok &= split_point_worth_sharing(sp); + + moves_deinit(sp->moves); + threads_deinit(&handler); + return ok; +} + +bool test_cooperative_stop_sets_finished() { + struct threads handler; + threads_init(&handler, 2); + ttable_init(&handler.ttable, 2); + + // set up messages + engine_message_handler_t msg_handlers[2]; + engine_message_handler_t *msg_ptrs[2] = { &msg_handlers[0], &msg_handlers[1] }; + engine_messages_t messages = { .data = msg_ptrs, .count = 2, .capacity = 2 }; + for (int i = 0; i < 2; i++) { + engine_message_handler_init(&msg_handlers[i]); + } + handler.engine_messages = &messages; + + go_args_t go_args = { .depth = 1 }; + position_t pos = position_starting(); + handler.go_args = &go_args; + handler.position = &pos; + handler.running_count = 2; + + for (int i = 0; i < 2; i++) { + __threads_args_init(handler.args + i, &handler, i); + } + + // start threads + threads_go(&handler); + + // give threads time to start + struct timespec ts = { .tv_nsec = 10000000 }; // 10ms + nanosleep(&ts, NULL); + + // cooperative stop + threads_stop_all(&handler); + threads_join(&handler); + + bool ok = true; + for (int i = 0; i < 2; i++) { + ok &= atomic_load(&handler.args[i].root_thread_args.finished) == 1; + } + + threads_cleanup(&handler); + for (int i = 0; i < 2; i++) { + while (engine_message_handler_can_pop(&msg_handlers[i])) { + engine_message_deinit(engine_message_handler_pop(&msg_handlers[i])); + } + engine_message_handler_deinit(&msg_handlers[i]); + } + ttable_deinit(handler.ttable); + threads_deinit(&handler); + return ok; +} #endif @@ -67,6 +67,9 @@ skip_refresh: break; } if (is_stop) { + // cooperative stop: signal all threads, wait for them to finish + threads_stop_all(&engine_threads); + threads_join(&engine_threads); threads_cleanup(&engine_threads); ipc_state_set_event(&ipc_state, EVENT_UCI_RUNNING); goto waiting_for_go; @@ -76,6 +79,12 @@ skip_refresh: } quit: + // cooperative stop for engine threads, then hard-cancel the UCI thread + // (which blocks on stdin and cannot exit cooperatively). + if (engine_threads.running_count > 0) { + threads_stop_all(&engine_threads); + threads_join(&engine_threads); + } ipc_state_deinit(&ipc_state); pthread_cancel(uci_thread); threads_deinit(&engine_threads); diff --git a/tests/generated.c b/tests/generated.c index 608d623..fc10446 100644 --- a/tests/generated.c +++ b/tests/generated.c @@ -8,8 +8,8 @@ #include "../src/engine/moves/king.c" #include "../src/uci/command.c" -int total_test_count = 39; -bool (*tests[39])(void) = { +int total_test_count = 43; +bool (*tests[43])(void) = { test_fen_no_passant, test_fen_passant, test_starting_position, @@ -17,6 +17,8 @@ bool (*tests[39])(void) = { test_search_handles_no_moves, test_search_respects_depth, test_search_stop_signal_halts, + test_qsearch_finds_capture, + test_pv_reconstruction, test_eval_startpos_is_balanced, test_eval_scales_with_material, test_root_thread_emits_info_and_bestmove, @@ -42,6 +44,8 @@ bool (*tests[39])(void) = { test_split_merge_last_owns, test_split_share_propagates_best, test_help_scan_finds_published_work, + test_thrashing_counter_tracks_nodes, + test_cooperative_stop_sets_finished, test_perft_starting_position, test_perft_kiwipete_position, test_perft_position3, @@ -52,7 +56,7 @@ bool (*tests[39])(void) = { }; int max_test_name_size = 35; -char test_names[39][100] = { +char test_names[43][100] = { "fen_no_passant", "fen_passant", "starting_position", @@ -60,6 +64,8 @@ char test_names[39][100] = { "search_handles_no_moves", "search_respects_depth", "search_stop_signal_halts", + "qsearch_finds_capture", + "pv_reconstruction", "eval_startpos_is_balanced", "eval_scales_with_material", "root_thread_emits_info_and_bestmove", @@ -85,6 +91,8 @@ char test_names[39][100] = { "split_merge_last_owns", "split_share_propagates_best", "help_scan_finds_published_work", + "thrashing_counter_tracks_nodes", + "cooperative_stop_sets_finished", "perft_starting_position", "perft_kiwipete_position", "perft_position3", |
