summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAargh Rai <aargh.rai+git@gmail.com>2026-08-23 16:37:21 +0530
committerAargh Rai <aargh.rai+git@gmail.com>2026-08-23 16:37:21 +0530
commit0e107345a54bf987aab0495dbbcf0dcfead131a0 (patch)
tree3b425006d17f4655ab2497f7d1c0476db4ba5f24
parent99de764d1d1b936594643e34ae4d2c4a239f69db (diff)
phase F
-rw-r--r--PLAN.md8
-rw-r--r--include/engine/threads.h8
-rw-r--r--src/engine/thread.c280
-rw-r--r--src/engine/threads.c21
-rw-r--r--tests/generated.c10
5 files changed, 312 insertions, 15 deletions
diff --git a/PLAN.md b/PLAN.md
index b58db03..5808545 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -307,16 +307,16 @@ build (`just test`) green before the next.
per-thread args). Build + existing tests still pass.
### Phase B — Sequential search + eval (no threads yet)
-- [ ] **B1.** Create `include/engine/evaluate.h` + `src/engine/evaluate.c`
+- [x] **B1.** Create `include/engine/evaluate.h` + `src/engine/evaluate.c`
with `i32 evaluate(position_t)` (material + simple PST). `#include` in
`engine.c`. Add `test_eval_startpos` under `TEST_MOD`.
-- [ ] **B2.** Create `include/engine/search.h` + `src/engine/search.c` with
+- [x] **B2.** Create `include/engine/search.h` + `src/engine/search.c` with
`search_result_t` and `search(...)`. Probe/emit TT. Add `TEST_MOD`
tests: `test_search_findmate`, `test_search_respects_depth`.
-- [ ] **B3.** `just test` — all 17 original tests + new search/eval tests green.
+- [x] **B3.** `just test` — all 17 original tests + new search/eval tests green.
### Phase C — Root thread driver (still effectively single-worker)
-- [ ] **C0.** Fix required. src/engine/ttable.c — zobrist_hash scanned all 64
+- [x] **C0.** Fix required. src/engine/ttable.c — zobrist_hash scanned all 64
squares via find_piece_on_square, which asserts on empty squares.
The per-node TT probe made search hit this on any non-full-board position.
Rewrote the piece loop to iterate occupied bitboards
diff --git a/include/engine/threads.h b/include/engine/threads.h
index 43c4459..1e63a5d 100644
--- a/include/engine/threads.h
+++ b/include/engine/threads.h
@@ -56,6 +56,13 @@ struct root_thread_args {
go_args_t *go_args;
engine_message_handler_t *message;
ttable_t *ttable;
+
+ 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;
+ int workers_count;
};
typedef union {
@@ -87,6 +94,7 @@ void __threads_args_init(
// DTS primitives (§2.3–2.8): split point pool + lock-free move claiming
split_point_t *split_point_alloc(struct threads *handler);
+split_point_t *__pool_alloc(split_point_t *pool, size_t capacity);
void split_point_free(struct threads *handler, split_point_t *split_point);
void split_point_publish(split_point_t *split_point);
bool split_point_claim_move(split_point_t *split_point, move_t *move);
diff --git a/src/engine/thread.c b/src/engine/thread.c
index df4dec8..5891558 100644
--- a/src/engine/thread.c
+++ b/src/engine/thread.c
@@ -4,6 +4,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
+#include <sched.h>
#define ROOT_MAX_DEPTH 100
#define MATE_WINDOW 1000
@@ -20,8 +21,14 @@ static int __score_to_mate(i32 score) {
void threads_go(struct threads *handler) {
int running_count = handler->running_count;
+
+ // initialise every arg struct before spawning: the root hands out
+ // pointers into this array (workers_stop), so all of them must be valid
+ // and settled before any thread starts running.
for (int i = 0; i < running_count; i++) {
__threads_args_init(handler->args + i, handler, i);
+ }
+ for (int i = 0; i < running_count; i++) {
pthread_create(
handler->threads + i,
NULL,
@@ -60,10 +67,16 @@ void *child_thread(void *_args) {
args->sp_pool_capacity
);
- // nothing active anywhere: the pool is drained, so we are done helping
- // (the root re-publishes per iteration once root splitting lands)
- if (sp == NULL) break;
+ // nothing active right now: idle until the root publishes the next
+ // 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));
+ sched_yield();
+ continue;
+ }
+ fprintf(stderr,"CHILD %d found sp\n",args->id);
// register as a worker at this block so Merge knows the headcount
atomic_fetch_add(&sp->workers, 1);
@@ -136,7 +149,85 @@ 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++) {
- result = search(*args->position, depth, -SEARCH_INF, SEARCH_INF, &ctx);
+ if (atomic_load(&stop)) break;
+
+ // root move list (DTS §3.4: splitting at the root is always safe/PV)
+ moves_t moves;
+ moves_init(&moves);
+ get_legal_moves(&moves, *args->position);
+
+ bool can_split = moves.length > 1 && args->split_points != NULL;
+ split_point_t *sp = can_split
+ ? __pool_alloc(args->split_points, args->sp_pool_capacity)
+ : NULL;
+
+ fprintf(stderr,"ROOT iter %d sp=%p\n",depth,(void*)sp);
+ if (sp != NULL) {
+ // Split (§2.4): snapshot the root node and hand the whole move list to
+ // the pool; the root works the list alongside its workers.
+ sp->depth = depth;
+ sp->ply = 0;
+ sp->node_type = NODE_PV;
+ sp->position_snapshot = *args->position;
+ atomic_store(&sp->alpha, -SEARCH_INF);
+ atomic_store(&sp->beta, SEARCH_INF);
+ atomic_store(&sp->best_score, -SEARCH_INF);
+ sp->best_move = (move_t) { 0 };
+ moves_deinit(sp->moves);
+ sp->moves = moves;
+ split_point_publish(sp);
+ atomic_fetch_add(&sp->workers, 1); // the root is a worker too
+
+ i32 best_score = -SEARCH_INF;
+ move_t best_move = (move_t) { 0 };
+ move_t move;
+ while (split_point_claim_move(sp, &move)) {
+ position_t child = sp->position_snapshot;
+ position_make_move(&child, move);
+
+ int alpha = atomic_load(&sp->alpha);
+ search_result_t res = search(
+ child,
+ depth - 1,
+ -SEARCH_INF,
+ -alpha,
+ &ctx
+ );
+
+ i32 score = -res.score;
+ split_point_share(sp, score, move);
+ if (score > best_score) {
+ best_score = score;
+ best_move = move;
+ atomic_store(&sp->alpha, best_score); // Share narrowed bounds
+ }
+ }
+
+ // Merge (§2.6): leave the block; the last worker out Unsplits it.
+ // The root then waits until nobody can write to the block anymore.
+ if (split_point_merge(sp)) {
+ atomic_store(&sp->state, SPLITPOINT_STATE_FREE);
+ }
+ fprintf(stderr,"ROOT iter %d waiting w=%d\n",depth,atomic_load(&sp->workers));
+ int spins=0;
+ while (atomic_load(&sp->state) == SPLITPOINT_STATE_ACTIVE) {
+ sched_yield();
+ if (++spins==1000000) fprintf(stderr,"ROOT STUCK iter %d w=%d idx=%d\n",depth,atomic_load(&sp->workers),atomic_load(&sp->next_move_idx));
+ }
+ while (atomic_load(&sp->state) == SPLITPOINT_STATE_ACTIVE) {
+ sched_yield();
+ }
+ fprintf(stderr,"ROOT iter %d done\n",depth);
+
+ result.score = atomic_load(&sp->best_score);
+ result.best_move = sp->best_move;
+ // the move list now belongs to the pool block (released on its next
+ // reuse), so there is nothing to free here
+ } else {
+ // single reply or no pool available: search sequentially
+ result = search(*args->position, depth, -SEARCH_INF, SEARCH_INF, &ctx);
+ moves_deinit(moves);
+ }
if (atomic_load(&stop)) break;
@@ -165,7 +256,10 @@ void *root_thread(void *_args) {
message->next = NULL;
engine_message_mark_ready(message);
- // signal children to stop their loops and mark this thread as finished.
+ // 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);
+ }
atomic_store(&args->stop, 1);
atomic_store(&args->finished, 1);
@@ -377,6 +471,17 @@ static void __child_test_setup(
}
}
+static void __child_test_stop_all(__child_test_env_t *env, int worker_count) {
+ // workers idle when the pool is drained, so they must be signalled
+ for (int i = 0; i < worker_count; i++) {
+ atomic_store(&env->args[i].stop, 1);
+ }
+}
+
+static void __wait_split_point_done(split_point_t *sp) {
+ while (atomic_load(&sp->state) == SPLITPOINT_STATE_ACTIVE) sched_yield();
+}
+
static void __child_test_teardown(__child_test_env_t *env, int worker_count) {
ttable_deinit(env->handler->ttable);
for (int i = 0; i < worker_count; i++) {
@@ -410,6 +515,10 @@ bool test_child_workers_claim_and_share() {
for (int i = 0; i < WORKERS; i++) {
pthread_create(env.threads + i, NULL, child_thread, env.args + i);
}
+
+ // workers idle once the block is drained — signal them, then collect
+ __wait_split_point_done(sp);
+ __child_test_stop_all(&env, WORKERS);
for (int i = 0; i < WORKERS; i++) {
pthread_join(env.threads[i], NULL);
}
@@ -494,6 +603,9 @@ bool test_child_workers_find_mate_score() {
for (int i = 0; i < WORKERS; i++) {
pthread_create(env.threads + i, NULL, child_thread, env.args + i);
}
+
+ __wait_split_point_done(sp);
+ __child_test_stop_all(&env, WORKERS);
for (int i = 0; i < WORKERS; i++) {
pthread_join(env.threads[i], NULL);
}
@@ -507,5 +619,163 @@ bool test_child_workers_find_mate_score() {
__child_test_teardown(&env, WORKERS);
return ok;
}
+
+// ---- Phase F tests: root move-list splitting ----
+
+enum { ROOT_SPLIT_MAX = 5 };
+
+typedef struct {
+ struct threads handler;
+ go_args_t go_args;
+ position_t position;
+ engine_message_handler_t message_handlers[ROOT_SPLIT_MAX];
+ engine_message_handler_t *message_ptrs[ROOT_SPLIT_MAX];
+ engine_messages_t messages;
+ pthread_t threads[ROOT_SPLIT_MAX];
+} __root_split_env_t;
+
+static void __root_split_env_init(
+ __root_split_env_t *env,
+ int thread_count,
+ go_args_t go_args,
+ position_t position
+) {
+ threads_init(&env->handler, (size_t)thread_count);
+ ttable_init(&env->handler.ttable, 2);
+
+ env->go_args = go_args;
+ env->position = position;
+ env->messages.count = thread_count;
+ env->messages.capacity = thread_count;
+ for (int i = 0; i < thread_count; i++) {
+ env->message_handlers[i] = (engine_message_handler_t) { 0 };
+ env->message_ptrs[i] = &env->message_handlers[i];
+ engine_message_handler_init(env->message_ptrs[i]);
+ }
+ env->messages.data = env->message_ptrs;
+
+ env->handler.engine_messages = &env->messages;
+ env->handler.go_args = &env->go_args;
+ env->handler.position = &env->position;
+ env->handler.running_count = thread_count;
+
+ // args live in handler.args so that the root's workers_stop pointers
+ // target the very structs the worker threads receive.
+ for (int i = 0; i < thread_count; i++) {
+ __threads_args_init(env->handler.args + i, &env->handler, i);
+ }
+}
+
+static void __root_split_env_run(__root_split_env_t *env, int thread_count) {
+ for (int i = 1; i < thread_count; i++) {
+ pthread_create(
+ env->threads + i,
+ NULL,
+ child_thread,
+ env->handler.args + i
+ );
+ }
+ pthread_create(env->threads, NULL, root_thread, env->handler.args);
+
+ pthread_join(env->threads[0], NULL); // root stops its workers on exit
+ for (int i = 1; i < thread_count; i++) {
+ pthread_join(env->threads[i], NULL);
+ }
+}
+
+static void __root_split_env_deinit(
+ __root_split_env_t *env,
+ int thread_count
+) {
+ for (int i = 0; i < thread_count; i++) {
+ while (engine_message_handler_can_pop(&env->message_handlers[i])) {
+ engine_message_deinit(engine_message_handler_pop(&env->message_handlers[i]));
+ }
+ engine_message_handler_deinit(&env->message_handlers[i]);
+ }
+ ttable_deinit(env->handler.ttable);
+ threads_deinit(&env->handler);
+}
+
+bool test_root_split_all_moves_claimed() {
+ enum { THREADS = 4 };
+ __root_split_env_t env;
+ __root_split_env_init(
+ &env,
+ THREADS,
+ (go_args_t) { .depth = 3 },
+ position_starting()
+ );
+
+ __root_split_env_run(&env, THREADS);
+
+ bool ok = true;
+
+ // root + workers all finished cleanly
+ for (int i = 0; i < THREADS; i++) {
+ ok &= atomic_load(
+ &((thread_args_t*)env.handler.args)[i].root_thread_args.finished
+ ) == 1;
+ }
+
+ // every pool block was Unsplitten back to FREE
+ split_point_t *pool = env.handler.split_point_pool;
+ bool any_used = false;
+ for (size_t b = 0; b < env.handler.capacity; b++) {
+ ok &= atomic_load(&pool[b].state) == SPLITPOINT_STATE_FREE;
+ if (atomic_load(&pool[b].next_move_idx) > 0) any_used = true;
+ }
+ ok &= any_used; // splitting actually happened
+
+ // the root published info messages for each completed depth
+ int count = 0;
+ engine_message_t *messages =
+ __root_thread_pop_all(&env.message_handlers[0], &count);
+ ok &= count == 3;
+ ok &= messages->depth == 1 && messages->next->depth == 2;
+ ok &= messages->score_cp > -SEARCH_INF;
+
+ __root_thread_free_messages(messages);
+ __root_split_env_deinit(&env, THREADS);
+ return ok;
+}
+
+bool test_root_with_workers_reports_mate() {
+ struct fen_load r = load_fen("k7/8/KQ6/8/8/8/8/8 w - - 0 1");
+ if (r.failed) return false;
+
+ enum { THREADS = 3 };
+ __root_split_env_t env;
+ __root_split_env_init(
+ &env,
+ THREADS,
+ (go_args_t) { .depth = 3 },
+ r.position
+ );
+
+ __root_split_env_run(&env, THREADS);
+
+ bool ok = true;
+ for (int i = 0; i < THREADS; i++) {
+ ok &= atomic_load(
+ &((thread_args_t*)env.handler.args)[i].root_thread_args.finished
+ ) == 1;
+ }
+
+ // the merged root result must report the mate in 1
+ int count = 0;
+ engine_message_t *messages =
+ __root_thread_pop_all(&env.message_handlers[0], &count);
+ engine_message_t *tail = messages;
+ while (tail->next != NULL) tail = tail->next;
+ ok &= tail->mate == 1;
+ ok &= tail->score_cp == SEARCH_MATE;
+ ok &= tail->best_move.from >= 0 && tail->best_move.from < 64;
+ ok &= tail->best_move.to >= 0 && tail->best_move.to < 64;
+
+ __root_thread_free_messages(messages);
+ __root_split_env_deinit(&env, THREADS);
+ return ok;
+}
#endif
diff --git a/src/engine/threads.c b/src/engine/threads.c
index c0d1da7..fe38013 100644
--- a/src/engine/threads.c
+++ b/src/engine/threads.c
@@ -109,11 +109,17 @@ void __threads_args_init(
args->go_args = handler->go_args;
args->message = handler->engine_messages->data[i];
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->workers_count = handler->running_count - 1;
}
-split_point_t *split_point_alloc(struct threads *handler) {
- for (size_t i = 0; i < handler->capacity; i++) {
- split_point_t *sp = (split_point_t*)handler->split_point_pool + i;
+split_point_t *__pool_alloc(split_point_t *pool, size_t capacity) {
+ for (size_t i = 0; i < capacity; i++) {
+ split_point_t *sp = pool + i;
int expected = SPLITPOINT_STATE_FREE;
if (!atomic_compare_exchange_weak(&sp->state, &expected, SPLITPOINT_STATE_ACTIVE)) continue;
@@ -124,11 +130,18 @@ split_point_t *split_point_alloc(struct threads *handler) {
// workers counts only registered helpers; the creator registers itself
atomic_store(&sp->workers, 0);
sp->best_move = (move_t) { 0 };
+ // recycled block: release the previous occupant's move list
+ moves_deinit(sp->moves);
+ sp->moves = moves_empty();
return sp;
}
return NULL;
}
+split_point_t *split_point_alloc(struct threads *handler) {
+ return __pool_alloc(handler->split_point_pool, handler->capacity);
+}
+
void split_point_free(struct threads *handler, split_point_t *split_point) {
// Unsplit (§2.7): drop the block back into the pool
atomic_store(&split_point->state, SPLITPOINT_STATE_FREE);
@@ -141,6 +154,8 @@ void split_point_publish(split_point_t *split_point) {
// Select (§2.5): lock-free claim of the next move via the shared counter
bool split_point_claim_move(split_point_t *split_point, move_t *move) {
+ // a block that was already Unsplitten must not be claimed from anymore
+ if (atomic_load(&split_point->state) != SPLITPOINT_STATE_ACTIVE) return false;
int idx = atomic_fetch_add(&split_point->next_move_idx, 1);
if (idx >= (int)split_point->moves.length) return false;
*move = split_point->moves.moves[idx];
diff --git a/tests/generated.c b/tests/generated.c
index 408a8b2..608d623 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 = 37;
-bool (*tests[37])(void) = {
+int total_test_count = 39;
+bool (*tests[39])(void) = {
test_fen_no_passant,
test_fen_passant,
test_starting_position,
@@ -26,6 +26,8 @@ bool (*tests[37])(void) = {
test_child_workers_claim_and_share,
test_child_thread_stops_on_signal,
test_child_workers_find_mate_score,
+ test_root_split_all_moves_claimed,
+ test_root_with_workers_reports_mate,
test_ttable_empty,
test_ttable_insert_and_find,
test_ttable_wrong_key,
@@ -50,7 +52,7 @@ bool (*tests[37])(void) = {
};
int max_test_name_size = 35;
-char test_names[37][100] = {
+char test_names[39][100] = {
"fen_no_passant",
"fen_passant",
"starting_position",
@@ -67,6 +69,8 @@ char test_names[37][100] = {
"child_workers_claim_and_share",
"child_thread_stops_on_signal",
"child_workers_find_mate_score",
+ "root_split_all_moves_claimed",
+ "root_with_workers_reports_mate",
"ttable_empty",
"ttable_insert_and_find",
"ttable_wrong_key",