From 38b5c44899f3d93e7d57ebde7f388153de3631a9 Mon Sep 17 00:00:00 2001 From: Dylan Tague <161772172+DMDTague@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:10:02 -0400 Subject: [PATCH 01/10] Make bitboard intrinsics portable to MSVC --- bit_utils.h | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/bit_utils.h b/bit_utils.h index a3b10d8..6761049 100644 --- a/bit_utils.h +++ b/bit_utils.h @@ -3,8 +3,12 @@ #include +#if defined(_MSC_VER) +#include +#endif + // ----------------------------------------------------------------------------- -// Bit Manipulation Utilites +// Bit Manipulation Utilities // ----------------------------------------------------------------------------- // Set the bit at the given square index (0-63) to 1. @@ -23,16 +27,39 @@ inline void clear_bit(std::uint64_t& bitboard, int square) { } // Count the number of set bits (population count) in the bitboard. -// Uses highly optimized compiler intrinsics. inline int pop_count(std::uint64_t bitboard) { +#if defined(_MSC_VER) + // Portable SWAR popcount avoids requiring a particular MSVC /arch setting + // or CPU POPCNT feature while remaining branch-free. + bitboard -= (bitboard >> 1) & 0x5555555555555555ULL; + bitboard = (bitboard & 0x3333333333333333ULL) + + ((bitboard >> 2) & 0x3333333333333333ULL); + bitboard = (bitboard + (bitboard >> 4)) & 0x0F0F0F0F0F0F0F0FULL; + return static_cast((bitboard * 0x0101010101010101ULL) >> 56); +#else return __builtin_popcountll(bitboard); +#endif } // Get the index of the Least Significant 1-Bit (LSB). -// Undefined behavior for passing 0 to __builtin_ctzll natively, so we check. inline int lsb(std::uint64_t bitboard) { - if (bitboard == 0) return -1; // Or return 64; handled conventionally here + if (bitboard == 0) return -1; +#if defined(_MSC_VER) && defined(_M_X64) + unsigned long index = 0; + _BitScanForward64(&index, bitboard); + return static_cast(index); +#elif defined(_MSC_VER) + unsigned long index = 0; + const std::uint32_t low = static_cast(bitboard); + if (low != 0) { + _BitScanForward(&index, low); + return static_cast(index); + } + _BitScanForward(&index, static_cast(bitboard >> 32)); + return static_cast(index + 32); +#else return __builtin_ctzll(bitboard); +#endif } #endif // BIT_UTILS_H From 819b0b837bd48637f7c9d2e3d070f48c1ea8d1e2 Mon Sep 17 00:00:00 2001 From: Dylan Tague <161772172+DMDTague@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:10:47 -0400 Subject: [PATCH 02/10] Use compiler-specific release optimization flags --- CMakeLists.txt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index eb2078d..f67ecd2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,11 +20,19 @@ set(PICKLE_CORE zobrist.cpp ) +function(pickle_enable_release_optimizations target) + if(MSVC) + target_compile_options(${target} PRIVATE /O2) + else() + target_compile_options(${target} PRIVATE -O3) + endif() +endfunction() + if(EMSCRIPTEN) add_executable(pickle_web ${PICKLE_CORE} wasm_api.cpp) set_target_properties(pickle_web PROPERTIES SUFFIX ".js") - target_compile_options(pickle_web PRIVATE -O3) + pickle_enable_release_optimizations(pickle_web) target_link_options(pickle_web PRIVATE -O3 "-sALLOW_MEMORY_GROWTH=1" @@ -39,9 +47,9 @@ if(EMSCRIPTEN) ) else() add_executable(pickle ${PICKLE_CORE} datagen.cpp main.cpp) - target_compile_options(pickle PRIVATE -O3) + pickle_enable_release_optimizations(pickle) add_executable(pickle_regression attacks.cpp board.cpp magics.cpp movegen.cpp zobrist.cpp tests/hash_roundtrip.cpp) target_include_directories(pickle_regression PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_compile_options(pickle_regression PRIVATE -O3) + pickle_enable_release_optimizations(pickle_regression) endif() From 10232f9a6a22a441810a1d539c9e3aaf7043d8e9 Mon Sep 17 00:00:00 2001 From: Dylan Tague <161772172+DMDTague@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:10:54 -0400 Subject: [PATCH 03/10] Avoid MSVC tm type collision in time manager header --- time_manager.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/time_manager.h b/time_manager.h index 56cffdf..57df128 100644 --- a/time_manager.h +++ b/time_manager.h @@ -1,6 +1,8 @@ #ifndef TIME_MANAGER_H #define TIME_MANAGER_H +#include + // Retrieves the current time in milliseconds using the OS clock long long get_time_ms(); @@ -14,7 +16,9 @@ struct TimeManager { bool stopped; // external flag to forcefully stop searching }; -// Global TimeManger instance +// Global TimeManager instance. is included before this declaration so +// the C/C++ standard tm type is already established on MSVC before this legacy +// variable name enters the global namespace. extern TimeManager tm; // Initialize the time manager with specific limits. From de00996860f9d049f9dc7330f93eacaeeac9c129 Mon Sep 17 00:00:00 2001 From: Dylan Tague <161772172+DMDTague@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:11:38 -0400 Subject: [PATCH 04/10] Use portable LSB helper in magic initialization --- magics.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/magics.cpp b/magics.cpp index f12cc3a..59fd648 100644 --- a/magics.cpp +++ b/magics.cpp @@ -1,4 +1,5 @@ #include "magics.h" +#include "bit_utils.h" #include // ----------------------------------------------------------------------------- @@ -164,12 +165,12 @@ U64 rook_attacks_on_the_fly(int square, U64 block) { // Permutation Generator // ----------------------------------------------------------------------------- -// Utility to set occupancy for a specific index. +// Utility to set occupancy for a specific index. // Standard LSB-based permutation generator. static U64 set_occupancy(int index, int bits_in_mask, U64 attack_mask) { U64 occupancy = 0ULL; for (int count = 0; count < bits_in_mask; count++) { - int square = __builtin_ctzll(attack_mask); + int square = lsb(attack_mask); attack_mask &= ~(1ULL << square); if (index & (1 << count)) { occupancy |= (1ULL << square); From 82d8b77c2dda196670085775d6bbfc374ea3dbac Mon Sep 17 00:00:00 2001 From: Dylan Tague <161772172+DMDTague@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:11:56 -0400 Subject: [PATCH 05/10] Build Pickle with MSVC in tournament CI --- .github/workflows/tournament-tools.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/tournament-tools.yml b/.github/workflows/tournament-tools.yml index f059a9c..cb8c98d 100644 --- a/.github/workflows/tournament-tools.yml +++ b/.github/workflows/tournament-tools.yml @@ -10,6 +10,12 @@ on: paths: - 'tournament/**' - '.github/workflows/tournament-tools.yml' + - 'CMakeLists.txt' + - 'bit_utils.h' + - 'magics.cpp' + - 'time_manager.h' + - '*.cpp' + - '*.h' jobs: estimator: @@ -78,3 +84,20 @@ jobs: Write-Host "Parsing $($_.Name)" [void][ScriptBlock]::Create((Get-Content $_.FullName -Raw)) } + + windows-native-build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v5 + - name: Configure Release build + run: cmake -S . -B build-windows + - name: Build Pickle with MSVC + run: cmake --build build-windows --config Release --parallel + - name: Smoke-test UCI + shell: pwsh + run: | + $inputText = "uci`nisready`nquit`n" + $output = $inputText | & .\build-windows\Release\pickle.exe + $output | Write-Host + if ($output -notcontains 'uciok') { throw 'Missing uciok from Windows build.' } + if ($output -notcontains 'readyok') { throw 'Missing readyok from Windows build.' } From ce67cfe9669e8c034de008eebc930d92242675dc Mon Sep 17 00:00:00 2001 From: Dylan Tague <161772172+DMDTague@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:13:38 -0400 Subject: [PATCH 06/10] Rename global search timer for MSVC compatibility --- time_manager.h | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/time_manager.h b/time_manager.h index 57df128..0d7b71c 100644 --- a/time_manager.h +++ b/time_manager.h @@ -1,8 +1,6 @@ #ifndef TIME_MANAGER_H #define TIME_MANAGER_H -#include - // Retrieves the current time in milliseconds using the OS clock long long get_time_ms(); @@ -16,10 +14,9 @@ struct TimeManager { bool stopped; // external flag to forcefully stop searching }; -// Global TimeManager instance. is included before this declaration so -// the C/C++ standard tm type is already established on MSVC before this legacy -// variable name enters the global namespace. -extern TimeManager tm; +// Global search timer state. The explicit name avoids colliding with the +// standard C/C++ `tm` time structure on MSVC. +extern TimeManager search_timer; // Initialize the time manager with specific limits. // Setting elements to -1 implies "infinite" From 39713e6e707d22f95a3ddf1e969f4bbe7cb1e606 Mon Sep 17 00:00:00 2001 From: Dylan Tague <161772172+DMDTague@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:13:46 -0400 Subject: [PATCH 07/10] Use renamed global search timer --- time_manager.cpp | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/time_manager.cpp b/time_manager.cpp index 35aca13..ee45bfe 100644 --- a/time_manager.cpp +++ b/time_manager.cpp @@ -1,7 +1,7 @@ #include #include "time_manager.h" -TimeManager tm; +TimeManager search_timer; long long get_time_ms() { auto now = std::chrono::time_point_cast( @@ -11,42 +11,42 @@ long long get_time_ms() { } void set_time_limits(long long time_left, long long inc, long long movetime, int depth) { - tm.stopped = false; - tm.time_is_up = false; - tm.start_time = get_time_ms(); - tm.depth_limit = depth; + search_timer.stopped = false; + search_timer.time_is_up = false; + search_timer.start_time = get_time_ms(); + search_timer.depth_limit = depth; if (movetime > 0) { - tm.optimum_time = movetime; - tm.max_time = movetime; - tm.stop_time = tm.start_time + movetime; + search_timer.optimum_time = movetime; + search_timer.max_time = movetime; + search_timer.stop_time = search_timer.start_time + movetime; } else if (time_left > 0) { // Allocate roughly one move from a 30-move horizon plus part of increment. - tm.optimum_time = (time_left / 30) + (inc / 2); - tm.max_time = (time_left / 10) + inc; + search_timer.optimum_time = (time_left / 30) + (inc / 2); + search_timer.max_time = (time_left / 10) + inc; - if (tm.optimum_time > time_left - 50) tm.optimum_time = time_left - 50; - if (tm.max_time > time_left - 50) tm.max_time = time_left - 50; + if (search_timer.optimum_time > time_left - 50) search_timer.optimum_time = time_left - 50; + if (search_timer.max_time > time_left - 50) search_timer.max_time = time_left - 50; - if (tm.optimum_time < 0) tm.optimum_time = 0; - if (tm.max_time < 0) tm.max_time = 0; + if (search_timer.optimum_time < 0) search_timer.optimum_time = 0; + if (search_timer.max_time < 0) search_timer.max_time = 0; - tm.stop_time = tm.start_time + tm.max_time; + search_timer.stop_time = search_timer.start_time + search_timer.max_time; } else { - tm.optimum_time = -1; - tm.max_time = -1; - tm.stop_time = -1; + search_timer.optimum_time = -1; + search_timer.max_time = -1; + search_timer.stop_time = -1; } } void check_time() { - if (tm.stopped) { - tm.time_is_up = true; + if (search_timer.stopped) { + search_timer.time_is_up = true; return; } - if (tm.stop_time != -1 && get_time_ms() >= tm.stop_time) { - tm.time_is_up = true; - tm.stopped = true; + if (search_timer.stop_time != -1 && get_time_ms() >= search_timer.stop_time) { + search_timer.time_is_up = true; + search_timer.stopped = true; } } From 1e80734cb93b0d39b32e94efa5bde56c6cb7c0fc Mon Sep 17 00:00:00 2001 From: Dylan Tague <161772172+DMDTague@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:14:10 -0400 Subject: [PATCH 08/10] Use renamed search timer in UCI stop handling --- uci.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uci.cpp b/uci.cpp index 16666d8..5bcd47b 100644 --- a/uci.cpp +++ b/uci.cpp @@ -149,7 +149,7 @@ void uci_loop(Board& board) { search_position(board, depth); } else if (command == "stop") { - tm.stopped = true; + search_timer.stopped = true; } else if (command == "quit") { break; From 45e13c11f911fbf416d99e38f155b721c0811092 Mon Sep 17 00:00:00 2001 From: Dylan Tague <161772172+DMDTague@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:14:22 -0400 Subject: [PATCH 09/10] Use renamed search timer in datagen --- datagen.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/datagen.cpp b/datagen.cpp index 95f6a54..5b00685 100644 --- a/datagen.cpp +++ b/datagen.cpp @@ -27,8 +27,8 @@ void run_datagen(int games, int target_depth, const std::string& filename) { std::cout << "Starting Data Generation... (" << games << " games at depth " << target_depth << ")\n"; // Disable time management for raw algorithmic depth mapping - tm.depth_limit = target_depth; - tm.time_is_up = false; + search_timer.depth_limit = target_depth; + search_timer.time_is_up = false; for (int g = 0; g < games; g++) { Board board; @@ -101,11 +101,11 @@ void run_datagen(int games, int target_depth, const std::string& filename) { Move best = 0; clear_heuristics(); nodes_searched = 0; - tm.time_is_up = false; + search_timer.time_is_up = false; for (int d = 1; d <= target_depth; d++) { final_score = negamax(d, -50000, 50000, board, 0, true); - if (!tm.time_is_up) best = best_move; + if (!search_timer.time_is_up) best = best_move; } // To prevent mating scores distorting NN evaluations heavily, cap mate scores From ea48d7d6ee13f1364ab16bf1acae238e491f02a6 Mon Sep 17 00:00:00 2001 From: Dylan Tague <161772172+DMDTague@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:15:56 -0400 Subject: [PATCH 10/10] Use renamed search timer throughout search --- search.cpp | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/search.cpp b/search.cpp index 822d1ac..97ec89a 100644 --- a/search.cpp +++ b/search.cpp @@ -137,7 +137,7 @@ int score_move(Board& board, Move move, Move tt_move, int search_ply) { int quiescence(int alpha, int beta, Board& board, int search_ply, int qs_ply) { if ((nodes_searched & 511ULL) == 0) check_time(); - if (tm.time_is_up) return 0; + if (search_timer.time_is_up) return 0; ++nodes_searched; if (qs_ply >= MAX_QS_PLY) return evaluate(board); @@ -176,7 +176,7 @@ int quiescence(int alpha, int beta, Board& board, int search_ply, int qs_ply) { int score = -quiescence(-beta, -alpha, board, search_ply + 1, qs_ply + 1); board.unmake_move(move); - if (tm.time_is_up) return 0; + if (search_timer.time_is_up) return 0; if (score >= beta) return beta; if (score > alpha) alpha = score; } @@ -187,7 +187,7 @@ int quiescence(int alpha, int beta, Board& board, int search_ply, int qs_ply) { int negamax(int depth, int alpha, int beta, Board& board, int search_ply, bool can_null_move) { if ((nodes_searched & 511ULL) == 0) check_time(); - if (tm.time_is_up) return 0; + if (search_timer.time_is_up) return 0; ++nodes_searched; if (search_ply >= MAX_SEARCH_PLY) return evaluate(board); @@ -218,7 +218,7 @@ int negamax(int depth, int alpha, int beta, Board& board, int search_ply, bool c int score = -negamax(depth - 1 - reduction, -beta, -beta + 1, board, search_ply + 1, false); board.unmake_null_move(); - if (tm.time_is_up) return 0; + if (search_timer.time_is_up) return 0; if (score >= beta) return beta; } @@ -281,7 +281,7 @@ int negamax(int depth, int alpha, int beta, Board& board, int search_ply, bool c } board.unmake_move(move); - if (tm.time_is_up) return 0; + if (search_timer.time_is_up) return 0; if (score >= beta) { if (search_ply == 0) best_move = move; @@ -355,7 +355,7 @@ Move search_best_move(Board& board, int depth, bool print_info) { return book_move; } - int target_depth = tm.depth_limit > 0 ? tm.depth_limit : depth; + int target_depth = search_timer.depth_limit > 0 ? search_timer.depth_limit : depth; if (target_depth <= 0) target_depth = 64; auto wall_start = std::chrono::high_resolution_clock::now(); @@ -378,7 +378,7 @@ Move search_best_move(Board& board, int depth, bool print_info) { while (true) { best_move = completed_move; score = negamax(current_depth, alpha, beta, board, 0, true); - if (tm.time_is_up || tm.stopped) break; + if (search_timer.time_is_up || search_timer.stopped) break; if (score <= alpha && alpha > -INF) { window *= 2; @@ -395,7 +395,7 @@ Move search_best_move(Board& board, int depth, bool print_info) { break; } - if (tm.time_is_up || tm.stopped) break; + if (search_timer.time_is_up || search_timer.stopped) break; Move iteration_move = best_move ? best_move : completed_move; if (iteration_move == completed_move && iteration_move != 0) ++stable_best_count; @@ -426,12 +426,13 @@ Move search_best_move(Board& board, int depth, bool print_info) { std::cout << std::endl; } - if (tm.optimum_time != -1) { - long long soft_limit = tm.optimum_time; + if (search_timer.optimum_time != -1) { + long long soft_limit = search_timer.optimum_time; if (have_previous_score && std::abs(score - previous_score) >= 65) { - soft_limit = std::min(tm.max_time, tm.optimum_time + tm.optimum_time / 2); + soft_limit = std::min(search_timer.max_time, + search_timer.optimum_time + search_timer.optimum_time / 2); } else if (stable_best_count >= 3) { - soft_limit = std::max(1LL, (tm.optimum_time * 3) / 4); + soft_limit = std::max(1LL, (search_timer.optimum_time * 3) / 4); } if (elapsed >= soft_limit) break; }