#define ENGINE_THREADS_INTERNAL #include "engine/threads.h" #include "engine/search.h" #include #include #include #include #define ROOT_MAX_DEPTH 100 #define MATE_WINDOW 1000 // convert a mate score into a "mate in N plies" distance (negative when we // are getting mated), 0 when the score isn't near a mate. static int __score_to_mate(i32 score) { int abs_score = score < 0 ? -score : score; if (abs_score < SEARCH_MATE - MATE_WINDOW) return 0; int plies = SEARCH_MATE - abs_score + 1; int mate_in = (plies + 1) / 2; return score > 0 ? mate_in : -mate_in; } 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, i == 0 ? root_thread : child_thread, handler->args + i ); } } void *child_thread(void *_args) { struct child_thread_args *args = (struct child_thread_args*)_args; args->stop = 0; atomic_bool stop = false; u64 nodes = 0; search_ctx_t ctx = { .ttable = args->ttable, .go_args = args->go_args, .stop = &stop, .nodes = &nodes, }; engine_message_t *message = args->message->count > 0 ? args->message->newest_valid : engine_message_handler_push(args->message); message->multipv = args->id; i32 best_score = -SEARCH_INF; move_t best_move = (move_t) { 0 }; // DTS worker (§2.3–2.8): HELP peers by claiming moves from published // split points, searching them, and Sharing the result back. while (!atomic_load(&args->stop) && !atomic_load(&stop)) { split_point_t *sp = __pool_find_work( args->split_points, args->sp_pool_capacity ); // 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); int alpha, beta; move_t move; while (split_point_claim_move(sp, &move)) { alpha = -atomic_load(&sp->beta); beta = -atomic_load(&sp->alpha); position_t child = sp->position_snapshot; position_make_move(&child, move); search_result_t res = search(child, sp->depth - 1, alpha, beta, &ctx); // Share (§2.8): negamax — our value flips sign for the parent side split_point_share(sp, -res.score, move); if (-res.score > best_score) { best_score = -res.score; best_move = move; } message->depth = sp->depth; message->nodes = nodes > INT_MAX ? INT_MAX : (int)nodes; message->score_cp = best_score; message->pv.count = 0; add_ipc_move(&message->pv, (ipc_move_t) { best_move.from, best_move.to }); engine_message_mark_ready(message); } // Merge (§2.6): when the last worker leaves, Unsplit the block if (split_point_merge(sp)) { atomic_store(&sp->state, SPLITPOINT_STATE_FREE); } } message->best_move = (ipc_move_t) { best_move.from, best_move.to }; message->next = NULL; engine_message_mark_ready(message); atomic_store(&args->finished, 1); return NULL; } // DTS requires a root thread which is initialize the search tree, and the // others who use that search tree. it's as if root_thread is the owner of the // tree void *root_thread(void *_args) { struct root_thread_args *args = (struct root_thread_args*)_args; args->stop = 0; atomic_bool stop = false; u64 nodes = 0; search_ctx_t ctx = { .ttable = args->ttable, .go_args = args->go_args, .stop = &stop, .nodes = &nodes, }; int max_depth = args->go_args != NULL && args->go_args->depth > 0 ? args->go_args->depth : ROOT_MAX_DEPTH; // reuse the handler's pre-created message when present, otherwise push one. engine_message_t *message = args->message->count > 0 ? args->message->newest_valid : engine_message_handler_push(args->message); search_result_t result = { .score = 0, .best_move = (move_t) { 0 } }; // 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++) { 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; 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, }); if (!should_continue(message, args->go_args)) { break; } engine_message_mark_ready(message); message = engine_message_handler_push(args->message); } // final message carries the best move of the last completed iteration. message->best_move = (ipc_move_t) { result.best_move.from, result.best_move.to }; message->ponder = (ipc_move_t) { 0, 0 }; message->next = NULL; 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); } atomic_store(&args->stop, 1); atomic_store(&args->finished, 1); return NULL; } #ifdef TEST_MOD #include #include #include "fen.h" // the root_thread driver + tests need the IPC implementation (messages, // moves, should_continue) which lives outside of engine.c's aggregation. #include "../src/ipc/go_args.c" #include "../src/ipc/moves.c" #include "../src/ipc/engine_message.c" static bool __root_thread_run( struct root_thread_args *args, go_args_t *go_args, position_t position, int ttable_log ) { args->message = malloc(sizeof(*args->message)); engine_message_handler_init(args->message); args->go_args = go_args; args->position = &position; ttable_t tt; ttable_init(&tt, ttable_log); root_thread(args); bool ok = true; ok &= atomic_load(&args->finished) == 1; ok &= atomic_load(&args->stop) == 1; ttable_deinit(tt); return ok; } static engine_message_t *__root_thread_pop_all( engine_message_handler_t *handler, int *count_out ) { engine_message_t *head = NULL; engine_message_t *tail = NULL; int count = 0; while (engine_message_handler_can_pop(handler)) { engine_message_t *m = engine_message_handler_pop(handler); m->next = NULL; if (head == NULL) head = m; else tail->next = m; tail = m; count++; } if (count_out != NULL) *count_out = count; return head; } static void __root_thread_free_messages(engine_message_t *head) { while (head != NULL) { engine_message_t *next = head->next; engine_message_deinit(head); head = next; } } bool test_root_thread_emits_info_and_bestmove() { struct root_thread_args *args = calloc(sizeof(*args), 1); go_args_t go_args = { .depth = 3 }; position_t position = position_starting(); bool ok = __root_thread_run( args, &go_args, position, 2 ); engine_message_handler_t *handler = args->message; int count = 0; engine_message_t *messages = __root_thread_pop_all(handler, &count); ok &= count >= 1; // 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.data[0].from >= 0 && m->pv.data[0].from < 64; ok &= m->pv.data[0].to >= 0 && m->pv.data[0].to < 64; } // the final message must carry the best move. engine_message_t *tail = messages; while (tail->next != NULL) tail = tail->next; ok &= tail->best_move.from >= 0 && tail->best_move.from < 64; ok &= tail->best_move.to >= 0 && tail->best_move.to < 64; ok &= tail->depth == 3; __root_thread_free_messages(messages); engine_message_handler_deinit(handler); free(handler); free(args); return ok; } bool test_root_thread_respects_depth_limit() { struct root_thread_args *args = calloc(sizeof(*args), 1); go_args_t go_args = { .depth = 2 }; position_t position = position_starting(); bool ok = __root_thread_run(args, &go_args, position, 2); int count = 0; engine_message_t *messages = __root_thread_pop_all(args->message, &count); // depth 2 -> exactly two info messages, depths 1 then 2. ok &= count == 2; ok &= messages->depth == 1; ok &= messages->next->depth == 2; __root_thread_free_messages(messages); engine_message_handler_deinit(args->message); free(args->message); free(args); return ok; } bool test_root_thread_stops_on_node_limit() { struct root_thread_args *args = calloc(sizeof(*args), 1); go_args_t go_args = { .nodes = 1 }; position_t position = position_starting(); bool ok = __root_thread_run(args, &go_args, position, 2); int count = 0; engine_message_t *messages = __root_thread_pop_all(args->message, &count); // node budget of 1 exhausts should_continue after the first iteration. ok &= count == 1; ok &= messages->node_limit; __root_thread_free_messages(messages); engine_message_handler_deinit(args->message); free(args->message); free(args); return ok; } bool test_root_thread_reports_mate() { struct root_thread_args *args = calloc(sizeof(*args), 1); go_args_t go_args = { .depth = 4 }; struct fen_load r = load_fen("k7/8/KQ6/8/8/8/8/8 w - - 0 1"); if (r.failed) return false; bool ok = __root_thread_run(args, &go_args, r.position, 2); int count = 0; engine_message_t *messages = __root_thread_pop_all(args->message, &count); // mate in 1 must be reported via the `mate` field. engine_message_t *tail = messages; while (tail->next != NULL) tail = tail->next; ok &= tail->mate == 1; ok &= tail->score_cp == SEARCH_MATE; __root_thread_free_messages(messages); engine_message_handler_deinit(args->message); free(args->message); free(args); return ok; } // ---- Phase E tests: DTS child worker loop ---- #include typedef struct { struct threads *handler; struct child_thread_args args[4]; pthread_t threads[4]; } __child_test_env_t; static void __child_test_setup( __child_test_env_t *env, split_point_t *sp, int worker_count ) { threads_init(env->handler, (size_t)worker_count); ttable_init(&env->handler->ttable, 2); for (int i = 0; i < worker_count; i++) { env->args[i].id = i + 1; atomic_init(&env->args[i].stop, 0); atomic_init(&env->args[i].finished, 0); env->args[i].go_args = NULL; env->args[i].ttable = &env->handler->ttable; env->args[i].split_points = env->handler->split_point_pool; env->args[i].sp_pool_capacity = env->handler->capacity; env->args[i].message = malloc(sizeof(engine_message_handler_t)); engine_message_handler_init(env->args[i].message); } if (sp != NULL) { sp->moves.length = 0; // filled by caller } } 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++) { while (engine_message_handler_can_pop(env->args[i].message)) { engine_message_deinit(engine_message_handler_pop(env->args[i].message)); } engine_message_handler_deinit(env->args[i].message); free(env->args[i].message); } threads_deinit(env->handler); } bool test_child_workers_claim_and_share() { enum { WORKERS = 4 }; __child_test_env_t env; struct threads handler; env.handler = &handler; __child_test_setup(&env, NULL, WORKERS); split_point_t *sp = split_point_alloc(&handler); sp->depth = 3; sp->ply = 0; atomic_store(&sp->alpha, -SEARCH_INF); atomic_store(&sp->beta, SEARCH_INF); sp->position_snapshot = position_starting(); moves_init(&sp->moves); get_legal_moves(&sp->moves, sp->position_snapshot); split_point_publish(sp); 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); } bool ok = true; // every worker finished cleanly for (int i = 0; i < WORKERS; i++) { ok &= atomic_load(&env.args[i].finished) == 1; ok &= atomic_load(&env.args[i].message->newest_valid->ready) == 1; } // the whole move list was consumed and the block was Unsplitten ok &= atomic_load(&sp->next_move_idx) >= (int)sp->moves.length; ok &= atomic_load(&sp->state) == SPLITPOINT_STATE_FREE; ok &= atomic_load(&sp->workers) == 0; // real search happened: nodes were counted and a sane best was shared ok &= atomic_load(&sp->best_score) > -SEARCH_INF; ok &= atomic_load(&sp->best_score) <= SEARCH_MATE; moves_deinit(sp->moves); __child_test_teardown(&env, WORKERS); return ok; } bool test_child_thread_stops_on_signal() { __child_test_env_t env; struct threads handler; enum { WORKERS = 1 }; env.handler = &handler; __child_test_setup(&env, NULL, WORKERS); split_point_t *sp = split_point_alloc(&handler); sp->depth = 5; atomic_store(&sp->alpha, -SEARCH_INF); atomic_store(&sp->beta, SEARCH_INF); sp->position_snapshot = position_starting(); moves_init(&sp->moves); get_legal_moves(&sp->moves, sp->position_snapshot); split_point_publish(sp); pthread_create(env.threads, NULL, child_thread, env.args); // wait until the worker has started claiming, then signal stop while (atomic_load(&sp->next_move_idx) == 0) sched_yield(); atomic_store(&env.args[0].stop, 1); pthread_join(env.threads[0], NULL); bool ok = true; ok &= atomic_load(&env.args[0].finished) == 1; ok &= atomic_load(&env.args[0].message->newest_valid->ready) == 1; moves_deinit(sp->moves); __child_test_teardown(&env, WORKERS); return ok; } bool test_child_workers_find_mate_score() { // Ka6/Qb6 vs Ka8: white mates in 1 with Qa7#/Qb7#. Children searching the // mate move must Share a mate score back to the split point. enum { WORKERS = 2 }; __child_test_env_t env; struct threads handler; env.handler = &handler; __child_test_setup(&env, NULL, WORKERS); struct fen_load r = load_fen("k7/8/KQ6/8/8/8/8/8 w - - 0 1"); if (r.failed) return false; split_point_t *sp = split_point_alloc(&handler); sp->depth = 2; atomic_store(&sp->alpha, -SEARCH_INF); atomic_store(&sp->beta, SEARCH_INF); sp->position_snapshot = r.position; moves_init(&sp->moves); get_legal_moves(&sp->moves, r.position); split_point_publish(sp); 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); } bool ok = true; ok &= atomic_load(&sp->best_score) == SEARCH_MATE; ok &= sp->best_move.from >= 0 && sp->best_move.from < 64; ok &= sp->best_move.to >= 0 && sp->best_move.to < 64; moves_deinit(sp->moves); __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