From 141bfeaedcb05c664d36d9db62e724fba8d02d34 Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Sat, 20 Jun 2026 20:47:25 +0100 Subject: [PATCH 01/16] Extract resolve_worker_count helper for batch parallelism Collapse the duplicated max(1, min(hardware_concurrency, count)) worker-count formula into a single resolve_worker_count helper in parallel_boards, and rewire parallel_all_boards_n to use it. Pure refactor, no behaviour change. Co-Authored-By: Claude Opus 4.8 --- library/src/system/parallel_boards.cpp | 22 +++++++++++++++------- library/src/system/parallel_boards.hpp | 9 +++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/library/src/system/parallel_boards.cpp b/library/src/system/parallel_boards.cpp index 25ded0d3..750041e7 100644 --- a/library/src/system/parallel_boards.cpp +++ b/library/src/system/parallel_boards.cpp @@ -17,6 +17,20 @@ #include +auto resolve_worker_count( + const int max_threads, + const int count) -> int +{ + int workers = max_threads; + if (workers <= 0) + { + const unsigned hw = std::thread::hardware_concurrency(); + workers = hw > 0 ? static_cast(hw) : 1; + } + return std::max(1, std::min(workers, count)); +} + + auto parallel_all_boards_n( const int count, const int worker_cap, @@ -27,13 +41,7 @@ auto parallel_all_boards_n( return RETURN_NO_FAULT; } - int workers = worker_cap; - if (workers <= 0) - { - const unsigned hw = std::thread::hardware_concurrency(); - workers = hw > 0 ? static_cast(hw) : 1; - } - workers = std::max(1, std::min(workers, count)); + const int workers = resolve_worker_count(worker_cap, count); if (workers == 1) { diff --git a/library/src/system/parallel_boards.hpp b/library/src/system/parallel_boards.hpp index 96f0baa1..763b4716 100644 --- a/library/src/system/parallel_boards.hpp +++ b/library/src/system/parallel_boards.hpp @@ -12,6 +12,15 @@ #include +/** + * @brief Resolve the number of worker threads to use. + * + * @param max_threads Requested cap; <= 0 means "auto" (use hardware concurrency). + * @param count Number of work items; the result is clamped to [1, count]. + * @return The worker count to use. + */ +auto resolve_worker_count(int max_threads, int count) -> int; + /** * @brief Process boards [0, count) with work-stealing parallelism. * From c9c746c1981aed0c0d4b4f07a12d3fe8b3d59d6f Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Sat, 20 Jun 2026 20:49:16 +0100 Subject: [PATCH 02/16] Rename SetMaxThreads to InitialiseStaticMemory with deprecated alias The function initialises static library state (TT memory pools, scheduler / thread-manager, lookup tables) and no longer controls thread count. Add InitialiseStaticMemory() as the public name and keep SetMaxThreads as a thin deprecated alias for ABI/back-compat. Repoint internal callers (dds.cpp, examples); update .NET/Python binding comments only. Co-Authored-By: Claude Opus 4.8 --- dotnet/DDS_Core/DDS.cs | 2 ++ dotnet/DDS_Core/Native/DdsNative.cs | 2 ++ examples/dd_table_for_deal.cpp | 2 +- examples/migration_example.cpp | 2 +- library/src/api/dll.h | 25 +++++++++++++++++-------- library/src/dds.cpp | 8 ++++---- library/src/init.cpp | 19 +++++++++++++++++-- python/src/bindings.cpp | 3 ++- 8 files changed, 46 insertions(+), 17 deletions(-) diff --git a/dotnet/DDS_Core/DDS.cs b/dotnet/DDS_Core/DDS.cs index adbaa594..90afffbb 100644 --- a/dotnet/DDS_Core/DDS.cs +++ b/dotnet/DDS_Core/DDS.cs @@ -30,6 +30,8 @@ public class DDS /// /// /// Maximum number of threads to use. + // The underlying C symbol SetMaxThreads was renamed to + // InitialiseStaticMemory; this P/Invoke still targets the deprecated alias. [Obsolete("Use SolverContext instead.")] public void SetMaxThreads(int userThreads) => DdsNative.SetMaxThreads(userThreads); diff --git a/dotnet/DDS_Core/Native/DdsNative.cs b/dotnet/DDS_Core/Native/DdsNative.cs index 9ee2c487..73f36d6a 100644 --- a/dotnet/DDS_Core/Native/DdsNative.cs +++ b/dotnet/DDS_Core/Native/DdsNative.cs @@ -95,6 +95,8 @@ public static extern int dds_calc_par( SolverContextHandle ctx #endregion #region ====== Configuration and Resource Management ====== + // The C symbol SetMaxThreads is deprecated and now a thin alias of + // InitialiseStaticMemory; the userThreads argument is ignored. [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] public static extern void SetMaxThreads( int userThreads); diff --git a/examples/dd_table_for_deal.cpp b/examples/dd_table_for_deal.cpp index 9f59adfc..cc537939 100644 --- a/examples/dd_table_for_deal.cpp +++ b/examples/dd_table_for_deal.cpp @@ -227,7 +227,7 @@ auto main(int argc, char * argv[]) -> int char line[80]; #if defined(__linux) || defined(__APPLE__) - SetMaxThreads(0); + InitialiseStaticMemory(); #endif const int res = CalcDDtablePBN(tableDealPBN, &table); diff --git a/examples/migration_example.cpp b/examples/migration_example.cpp index e9e40088..0b78fa56 100644 --- a/examples/migration_example.cpp +++ b/examples/migration_example.cpp @@ -17,7 +17,7 @@ void solve_legacy(const Deal& deal) { - SetMaxThreads(4); + InitialiseStaticMemory(); SetResources(2000, 4); FutureTricks fut; diff --git a/library/src/api/dll.h b/library/src/api/dll.h index 7fbbdd44..0b97b713 100644 --- a/library/src/api/dll.h +++ b/library/src/api/dll.h @@ -428,21 +428,30 @@ struct DDSInfo +/** + * @brief Initialise the solver's static memory. + * + * Allocates the transposition-table memory pools, registers scheduler and + * thread-manager state, and performs one-time lookup-table initialisation. + * This does NOT control the number of worker threads — use the + * SolveAllBoardsN / CalcAllTablesN family for per-call thread caps. + */ +EXTERN_C DLLEXPORT auto STDCALL InitialiseStaticMemory() -> void; + /** * @brief Set the maximum number of threads used by the solver. * - * @deprecated In the modern C++ API, thread count is controlled by the - * embedding application (typically one SolverContext per worker - * thread). New code should create/destroy SolverContext instances - * in the application rather than calling this function. + * @deprecated Use InitialiseStaticMemory(); the thread count argument is + * ignored (internal batch threading was removed). In the modern + * C++ API, thread count is controlled by the embedding application + * (typically one SolverContext per worker thread), or per call via + * the SolveAllBoardsN / CalcAllTablesN family. * See docs/api_migration.md for modern C++ API examples. * - * @param userThreads Maximum number of threads to use + * @param userThreads Ignored; retained for backward compatibility. * * This function is part of the legacy C API and is maintained for backward - * compatibility. It has no direct equivalent in the modern API, where both - * threading and TT memory limits are configured via SolverContext and - * SolverConfig on a per-instance basis. + * compatibility. It simply forwards to InitialiseStaticMemory(). */ EXTERN_C DLLEXPORT auto STDCALL SetMaxThreads( int userThreads) -> void; diff --git a/library/src/dds.cpp b/library/src/dds.cpp index 265e77dc..4762df7c 100644 --- a/library/src/dds.cpp +++ b/library/src/dds.cpp @@ -36,7 +36,7 @@ extern "C" BOOL APIENTRY DllMain( { if (ul_reason_for_call == DLL_PROCESS_ATTACH) - SetMaxThreads(0); + InitialiseStaticMemory(); else if (ul_reason_for_call == DLL_PROCESS_DETACH) { FreeMemory(); @@ -61,9 +61,9 @@ void DDSInitialize(), DDSFinalize(); /** * @brief Initialize the DDS library. */ -void DDSInitialize(void) +void DDSInitialize(void) { - SetMaxThreads(0); + InitialiseStaticMemory(); } @@ -84,7 +84,7 @@ void DDSFinalize(void) */ static void __attribute__ ((constructor)) libInit(void) { - SetMaxThreads(0); + InitialiseStaticMemory(); } #endif diff --git a/library/src/init.cpp b/library/src/init.cpp index 3e22dd11..0fb61e32 100644 --- a/library/src/init.cpp +++ b/library/src/init.cpp @@ -36,14 +36,29 @@ int _initialized = 0; /* - * Set the maximum number of threads used by the solver. + * Initialise the solver's static memory: TT memory pools, scheduler / + * thread-manager state, and one-time lookup-table setup. + * + * Public API documentation is maintained in the API headers. + */ +void STDCALL InitialiseStaticMemory() +{ + SetResources(0, 0); +} + + +/* + * Deprecated alias for InitialiseStaticMemory(). The thread count is no + * longer meaningful (internal batch threading was removed), so the argument + * is ignored. * * Public API documentation is maintained in the API headers. */ void STDCALL SetMaxThreads( int userThreads) { - SetResources(0, userThreads); + (void) userThreads; + InitialiseStaticMemory(); } diff --git a/python/src/bindings.cpp b/python/src/bindings.cpp index be83081a..94170ba8 100644 --- a/python/src/bindings.cpp +++ b/python/src/bindings.cpp @@ -611,7 +611,8 @@ auto register_analysis_bindings(py::module_& module) -> void SetMaxThreads(user_threads); }, py::arg("user_threads") = 0, - "Legacy thread-resource hook (wraps the deprecated SetMaxThreads C API).\n\n" + "Legacy thread-resource hook (wraps the deprecated SetMaxThreads C API,\n" + "now a thin alias of InitialiseStaticMemory).\n\n" "This does NOT control DDS's batch parallelism and is retained only for\n" "backward compatibility. solve_all_boards_* already parallelise across the\n" "machine's hardware threads automatically (see solve_boards_n); the value\n" From 89b029669ee50b4f458d8ceb5d291b9fec37da53 Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Sat, 20 Jun 2026 20:50:51 +0100 Subject: [PATCH 03/16] Plumb optional max_threads through internal batch solvers Add a trailing max_threads (default 0 = auto) to solve_all_boards_n and the legacy parallel calc_all_boards_n, forwarding it to parallel_all_boards_n and using resolve_worker_count to size the SolverContext pool. Default preserves the current hardware_concurrency auto-sizing, so no behaviour change. Deviation from task: the C++ calc_dd_table overloads use the sequential context-aware calc_all_boards_n, so threading max_threads there would be a no-op; left unchanged. The C API CalcDDtable/CalcAllTables use the legacy parallel overload, which now carries the parameter for Task 04. Co-Authored-By: Claude Opus 4.8 --- library/src/calc_tables.cpp | 11 ++++++----- library/src/solve_board.cpp | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/library/src/calc_tables.cpp b/library/src/calc_tables.cpp index 8003944d..66699101 100644 --- a/library/src/calc_tables.cpp +++ b/library/src/calc_tables.cpp @@ -27,7 +27,8 @@ extern Scheduler scheduler; // Legacy overload (creates temporary context) auto calc_all_boards_n( Boards * bop, - SolvedBoards * solvedp) -> int; + SolvedBoards * solvedp, + int max_threads = 0) -> int; auto calc_single_common_internal( @@ -109,7 +110,8 @@ auto calc_all_boards_n( // Legacy overload: parallel across boards, one SolverContext per worker. auto calc_all_boards_n( Boards * bop, - SolvedBoards * solvedp) -> int + SolvedBoards * solvedp, + int max_threads) -> int { const int n = bop->no_of_boards; if (n > MAXNOOFBOARDS) @@ -120,8 +122,7 @@ auto calc_all_boards_n( START_BLOCK_TIMER; - const int nthreads = std::max(1, - std::min(static_cast(std::thread::hardware_concurrency()), n)); + const int nthreads = resolve_worker_count(max_threads, n); int err = RETURN_NO_FAULT; if (nthreads <= 1) @@ -137,7 +138,7 @@ auto calc_all_boards_n( else { std::vector contexts(static_cast(nthreads)); - err = parallel_all_boards_n(n, nthreads, + err = parallel_all_boards_n(n, max_threads, [&](const int worker_id, const int bno) -> int { return calc_single_common_internal( contexts[static_cast(worker_id)], *bop, *solvedp, bno); diff --git a/library/src/solve_board.cpp b/library/src/solve_board.cpp index edd78abb..78ee3437 100644 --- a/library/src/solve_board.cpp +++ b/library/src/solve_board.cpp @@ -63,7 +63,7 @@ static auto boards_from_pbn( auto solve_all_boards_n( Boards const& bds, SolvedBoards& solved, - const int worker_cap) -> int + int max_threads = 0) -> int { const int n = bds.no_of_boards; if (n > MAXNOOFBOARDS) @@ -76,7 +76,7 @@ auto solve_all_boards_n( START_BLOCK_TIMER; - const int err = parallel_all_boards_n(n, worker_cap, + const int err = parallel_all_boards_n(n, max_threads, [&](const int worker_id, const int bno) -> int { (void)worker_id; From 3160a29715f67995d65f233883ea78109c5b0bab Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Sat, 20 Jun 2026 20:53:03 +0100 Subject: [PATCH 04/16] Add public *N C variants accepting maxThreads Add SolveAllBoardsN, SolveAllBoardsBinN, CalcDDtableN, CalcDDtablePBNN, CalcAllTablesN and CalcAllTablesPBNN, which forward an explicit maxThreads (<= 0 = auto) down to the internal batch solvers. The existing functions keep their signatures and now delegate to the *N variants with 0, preserving ABI and current auto-sizing behaviour. Co-Authored-By: Claude Opus 4.8 --- library/src/api/dll.h | 72 +++++++++++++++++++++++++++++++++++++ library/src/calc_tables.cpp | 66 +++++++++++++++++++++++++++------- library/src/solve_board.cpp | 47 ++++++++++++++++++++++-- 3 files changed, 171 insertions(+), 14 deletions(-) diff --git a/library/src/api/dll.h b/library/src/api/dll.h index 0b97b713..c27504a1 100644 --- a/library/src/api/dll.h +++ b/library/src/api/dll.h @@ -551,6 +551,17 @@ EXTERN_C DLLEXPORT auto STDCALL CalcDDtable( struct DdTableDeal tableDeal, struct DdTableResults * tablep) -> int; +/** + * @brief CalcDDtable with an explicit worker-thread cap. + * + * @param maxThreads Maximum worker threads; <= 0 selects the automatic + * (hardware_concurrency) default. + */ +EXTERN_C DLLEXPORT auto STDCALL CalcDDtableN( + struct DdTableDeal tableDeal, + struct DdTableResults * tablep, + int maxThreads) -> int; + /** * @brief Calculate the double dummy table for a PBN Deal. * @@ -562,6 +573,17 @@ EXTERN_C DLLEXPORT auto STDCALL CalcDDtablePBN( struct DdTableDealPBN tableDealPBN, struct DdTableResults * tablep) -> int; +/** + * @brief CalcDDtablePBN with an explicit worker-thread cap. + * + * @param maxThreads Maximum worker threads; <= 0 selects the automatic + * (hardware_concurrency) default. + */ +EXTERN_C DLLEXPORT auto STDCALL CalcDDtablePBNN( + struct DdTableDealPBN tableDealPBN, + struct DdTableResults * tablep, + int maxThreads) -> int; + /** * @brief Calculate double dummy tables for multiple deals. * @@ -579,6 +601,20 @@ EXTERN_C DLLEXPORT auto STDCALL CalcAllTables( struct DdTablesRes * resp, struct AllParResults * presp) -> int; +/** + * @brief CalcAllTables with an explicit worker-thread cap. + * + * @param maxThreads Maximum worker threads; <= 0 selects the automatic + * (hardware_concurrency) default. + */ +EXTERN_C DLLEXPORT auto STDCALL CalcAllTablesN( + struct DdTableDeals const * dealsp, + int mode, + int const trumpFilter[DDS_STRAINS], + struct DdTablesRes * resp, + struct AllParResults * presp, + int maxThreads) -> int; + /** * @brief Calculate double dummy tables for multiple PBN deals. * @@ -596,6 +632,20 @@ EXTERN_C DLLEXPORT auto STDCALL CalcAllTablesPBN( struct DdTablesRes * resp, struct AllParResults * presp) -> int; +/** + * @brief CalcAllTablesPBN with an explicit worker-thread cap. + * + * @param maxThreads Maximum worker threads; <= 0 selects the automatic + * (hardware_concurrency) default. + */ +EXTERN_C DLLEXPORT auto STDCALL CalcAllTablesPBNN( + struct DdTableDealsPBN const * dealsp, + int mode, + int const trumpFilter[DDS_STRAINS], + struct DdTablesRes * resp, + struct AllParResults * presp, + int maxThreads) -> int; + /** * @brief Solve multiple bridge deals in PBN format. * @@ -607,10 +657,32 @@ EXTERN_C DLLEXPORT auto STDCALL SolveAllBoards( struct BoardsPBN const * bop, struct SolvedBoards * solvedp) -> int; +/** + * @brief SolveAllBoards with an explicit worker-thread cap. + * + * @param maxThreads Maximum worker threads; <= 0 selects the automatic + * (hardware_concurrency) default. + */ +EXTERN_C DLLEXPORT auto STDCALL SolveAllBoardsN( + struct BoardsPBN const * bop, + struct SolvedBoards * solvedp, + int maxThreads) -> int; + EXTERN_C DLLEXPORT auto STDCALL SolveAllBoardsBin( struct Boards const * bop, struct SolvedBoards * solvedp) -> int; +/** + * @brief SolveAllBoardsBin with an explicit worker-thread cap. + * + * @param maxThreads Maximum worker threads; <= 0 selects the automatic + * (hardware_concurrency) default. + */ +EXTERN_C DLLEXPORT auto STDCALL SolveAllBoardsBinN( + struct Boards const * bop, + struct SolvedBoards * solvedp, + int maxThreads) -> int; + EXTERN_C DLLEXPORT auto STDCALL SolveAllBoardsSeq( struct BoardsPBN const * bop, struct SolvedBoards * solvedp) -> int; diff --git a/library/src/calc_tables.cpp b/library/src/calc_tables.cpp index 66699101..727753e5 100644 --- a/library/src/calc_tables.cpp +++ b/library/src/calc_tables.cpp @@ -161,9 +161,10 @@ auto calc_all_boards_n( -int STDCALL CalcDDtable( +int STDCALL CalcDDtableN( DdTableDeal tableDeal, - DdTableResults * tablep) + DdTableResults * tablep, + int maxThreads) { Deal dl; Boards bo; @@ -192,7 +193,7 @@ int STDCALL CalcDDtable( ind++; } - int res = calc_all_boards_n(&bo, &solved); + int res = calc_all_boards_n(&bo, &solved, maxThreads); if (res != 1) return res; @@ -212,12 +213,21 @@ int STDCALL CalcDDtable( } -int STDCALL CalcAllTables( +int STDCALL CalcDDtable( + DdTableDeal tableDeal, + DdTableResults * tablep) +{ + return CalcDDtableN(tableDeal, tablep, 0); +} + + +int STDCALL CalcAllTablesN( DdTableDeals const * dealsp, int mode, int const trumpFilter[5], DdTablesRes * resp, - AllParResults * presp) + AllParResults * presp, + int maxThreads) { /* mode = 0: par calculation, vulnerability None mode = 1: par calculation, vulnerability All @@ -279,7 +289,7 @@ int STDCALL CalcAllTables( bo.no_of_boards = lastIndex + 1; - int res = calc_all_boards_n(&bo, &solved); + int res = calc_all_boards_n(&bo, &solved, maxThreads); if (res != 1) return res; @@ -317,12 +327,24 @@ int STDCALL CalcAllTables( } -int STDCALL CalcAllTablesPBN( - DdTableDealsPBN const * dealsp, +int STDCALL CalcAllTables( + DdTableDeals const * dealsp, int mode, int const trumpFilter[5], DdTablesRes * resp, AllParResults * presp) +{ + return CalcAllTablesN(dealsp, mode, trumpFilter, resp, presp, 0); +} + + +int STDCALL CalcAllTablesPBNN( + DdTableDealsPBN const * dealsp, + int mode, + int const trumpFilter[5], + DdTablesRes * resp, + AllParResults * presp, + int maxThreads) { DdTableDeals dls; for (int k = 0; k < dealsp->no_of_tables; k++) @@ -331,24 +353,44 @@ int STDCALL CalcAllTablesPBN( dls.no_of_tables = dealsp->no_of_tables; - int res = CalcAllTables(&dls, mode, trumpFilter, resp, presp); + int res = CalcAllTablesN(&dls, mode, trumpFilter, resp, presp, maxThreads); return res; } -int STDCALL CalcDDtablePBN( +int STDCALL CalcAllTablesPBN( + DdTableDealsPBN const * dealsp, + int mode, + int const trumpFilter[5], + DdTablesRes * resp, + AllParResults * presp) +{ + return CalcAllTablesPBNN(dealsp, mode, trumpFilter, resp, presp, 0); +} + + +int STDCALL CalcDDtablePBNN( DdTableDealPBN tableDealPBN, - DdTableResults * tablep) + DdTableResults * tablep, + int maxThreads) { DdTableDeal tableDeal; if (convert_from_pbn(tableDealPBN.cards, tableDeal.cards) != 1) return RETURN_PBN_FAULT; - int res = CalcDDtable(tableDeal, tablep); + int res = CalcDDtableN(tableDeal, tablep, maxThreads); return res; } +int STDCALL CalcDDtablePBN( + DdTableDealPBN tableDealPBN, + DdTableResults * tablep) +{ + return CalcDDtablePBNN(tableDealPBN, tablep, 0); +} + + void detect_calc_duplicates( const Boards& bds, vector& uniques, diff --git a/library/src/solve_board.cpp b/library/src/solve_board.cpp index 78ee3437..23eedf51 100644 --- a/library/src/solve_board.cpp +++ b/library/src/solve_board.cpp @@ -162,11 +162,54 @@ int STDCALL SolveBoardPBN( * @param solvedp Pointer to results for solved Boards * @return 1 on success, error code otherwise */ +int STDCALL SolveAllBoardsN( + BoardsPBN const * bop, + SolvedBoards * solvedp, + int maxThreads) +{ + Boards bo; + bo.no_of_boards = bop->no_of_boards; + if (bo.no_of_boards > MAXNOOFBOARDS) + return RETURN_TOO_MANY_BOARDS; + + for (int k = 0; k < bop->no_of_boards; k++) + { + bo.mode[k] = bop->mode[k]; + bo.solutions[k] = bop->solutions[k]; + bo.target[k] = bop->target[k]; + bo.deals[k].first = bop->deals[k].first; + bo.deals[k].trump = bop->deals[k].trump; + + for (int i = 0; i <= 2; i++) + { + bo.deals[k].currentTrickSuit[i] = bop->deals[k].currentTrickSuit[i]; + bo.deals[k].currentTrickRank[i] = bop->deals[k].currentTrickRank[i]; + } + + if (convert_from_pbn(bop->deals[k].remainCards, bo.deals[k].remainCards) + != 1) + return RETURN_PBN_FAULT; + } + + int res = solve_all_boards_n(bo, * solvedp, maxThreads); + return res; +} + + int STDCALL SolveAllBoards( BoardsPBN const * bop, SolvedBoards * solvedp) { - return solve_all_boards_pbn_n(*bop, *solvedp, 0); + return SolveAllBoardsN(bop, solvedp, 0); +} + + +int STDCALL SolveAllBoardsBinN( + Boards const * bop, + SolvedBoards * solvedp, + int maxThreads) +{ + return solve_all_boards_n(* bop, * solvedp, maxThreads); } @@ -174,7 +217,7 @@ int STDCALL SolveAllBoardsBin( Boards const * bop, SolvedBoards * solvedp) { - return solve_all_boards_n(* bop, * solvedp); + return SolveAllBoardsBinN(bop, solvedp, 0); } From f36dc170d14cd48e04fa535bbb7298e69454eb6a Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Sat, 20 Jun 2026 21:02:46 +0100 Subject: [PATCH 05/16] Add tests for worker-count override and the initialiser rename Add worker_count_test (resolve_worker_count edge cases) and max_threads_equivalence_test (CalcDDtableN / SolveAllBoardsBinN with an explicit cap match the auto path; InitialiseStaticMemory and the deprecated SetMaxThreads alias both leave the library usable). Migrate existing SetMaxThreads(1) call sites to InitialiseStaticMemory(), keeping one deliberate alias usage in the new test. Co-Authored-By: Claude Opus 4.8 --- library/tests/system/BUILD.bazel | 24 ++++ .../tests/system/context_equivalence_test.cpp | 2 +- .../tests/system/context_tt_facade_test.cpp | 6 +- .../system/max_threads_equivalence_test.cpp | 132 ++++++++++++++++++ library/tests/system/worker_count_test.cpp | 49 +++++++ 5 files changed, 209 insertions(+), 4 deletions(-) create mode 100644 library/tests/system/max_threads_equivalence_test.cpp create mode 100644 library/tests/system/worker_count_test.cpp diff --git a/library/tests/system/BUILD.bazel b/library/tests/system/BUILD.bazel index 6d429426..5c172fd6 100644 --- a/library/tests/system/BUILD.bazel +++ b/library/tests/system/BUILD.bazel @@ -59,6 +59,30 @@ cc_test( ], ) +# Worker-count helper unit test +cc_test( + name = "worker_count_test", + size = "small", + srcs = ["worker_count_test.cpp"], + deps = [ + "//library/src:testable_dds", + "//library/src/api:api_definitions", + "@googletest//:gtest_main", + ], +) + +# max_threads override equivalence + rename/alias initialisation test +cc_test( + name = "max_threads_equivalence_test", + size = "small", + srcs = ["max_threads_equivalence_test.cpp"], + deps = [ + "//library/src:testable_dds", + "//library/src/api:api_definitions", + "@googletest//:gtest_main", + ], +) + # Utilities feature flags tests cc_test( name = "utilities_feature_flags_test", diff --git a/library/tests/system/context_equivalence_test.cpp b/library/tests/system/context_equivalence_test.cpp index a63b5a96..4396046f 100644 --- a/library/tests/system/context_equivalence_test.cpp +++ b/library/tests/system/context_equivalence_test.cpp @@ -58,7 +58,7 @@ static DdTableDeal make_known_deal() TEST(SystemContextEquivalence, LegacyVsContextReturnCode) { // Ensure DDS system and thread-local memory are initialized - SetMaxThreads(1); + InitialiseStaticMemory(); const int thr = 0; FutureTricks ft_legacy{}; FutureTricks ft_ctx{}; diff --git a/library/tests/system/context_tt_facade_test.cpp b/library/tests/system/context_tt_facade_test.cpp index b9c6076a..3ab63ecd 100644 --- a/library/tests/system/context_tt_facade_test.cpp +++ b/library/tests/system/context_tt_facade_test.cpp @@ -15,7 +15,7 @@ extern Memory memory; TEST(SystemContextTTFacades, ResetAndResizeAreNoopsWithoutTT) { - SetMaxThreads(1); + InitialiseStaticMemory(); // Some environments may compute 0 allowable threads (e.g., macOS sandbox), // so ensure we have at least one thread allocated for the test. if (memory.NumThreads() == 0) @@ -34,7 +34,7 @@ TEST(SystemContextTTFacades, ResetAndResizeAreNoopsWithoutTT) TEST(SystemContextTTFacades, ResizeCreatesWhenExisting) { - SetMaxThreads(1); + InitialiseStaticMemory(); // Ensure at least one thread exists; fall back to a small thread config. if (memory.NumThreads() == 0) memory.Resize(1, DDS_TT_SMALL, THREADMEM_SMALL_DEF_MB, THREADMEM_SMALL_MAX_MB); @@ -51,7 +51,7 @@ TEST(SystemContextTTFacades, ResizeCreatesWhenExisting) TEST(SystemContextTTFacades, Lifecycle_LookupAddClearDispose) { - SetMaxThreads(1); + InitialiseStaticMemory(); if (memory.NumThreads() == 0) memory.Resize(1, DDS_TT_SMALL, THREADMEM_SMALL_DEF_MB, THREADMEM_SMALL_MAX_MB); diff --git a/library/tests/system/max_threads_equivalence_test.cpp b/library/tests/system/max_threads_equivalence_test.cpp new file mode 100644 index 00000000..b7210603 --- /dev/null +++ b/library/tests/system/max_threads_equivalence_test.cpp @@ -0,0 +1,132 @@ +/// @file max_threads_equivalence_test.cpp +/// @brief Tests that the *N batch APIs honour maxThreads and stay equivalent to +/// the auto path, plus that the rename/alias both initialise the library. + +#include +#include +#include + +#include +#include + +namespace +{ + +// Known deal from examples/hands.cpp (hand 0), as used by context_equivalence_test. +// PBN: N:QJ6.K652.J85.T98 873.J97.AT764.Q4 K5.T83.KQ9.A7652 AT942.AQ4.32.KJ3 +DdTableDeal make_known_deal() +{ + DdTableDeal deal{}; + deal.cards[0][0] = 0x1800 | 0x0040; + deal.cards[0][1] = 0x2000 | 0x0060 | 0x0004; + deal.cards[0][2] = 0x0800 | 0x0100 | 0x0020; + deal.cards[0][3] = 0x0400 | 0x0200 | 0x0100; + deal.cards[1][0] = 0x0100 | 0x0080 | 0x0008; + deal.cards[1][1] = 0x0800 | 0x0200 | 0x0080; + deal.cards[1][2] = 0x4000 | 0x0400 | 0x0080 | 0x0040 | 0x0010; + deal.cards[1][3] = 0x1000 | 0x0010; + deal.cards[2][0] = 0x2000 | 0x0020; + deal.cards[2][1] = 0x0400 | 0x0100 | 0x0008; + deal.cards[2][2] = 0x2000 | 0x1000 | 0x0200; + deal.cards[2][3] = 0x4000 | 0x0080 | 0x0040 | 0x0020 | 0x0004; + deal.cards[3][0] = 0x4000 | 0x0400 | 0x0200 | 0x0010 | 0x0004; + deal.cards[3][1] = 0x4000 | 0x1000 | 0x0010; + deal.cards[3][2] = 0x0008 | 0x0004; + deal.cards[3][3] = 0x2000 | 0x0800 | 0x0008; + return deal; +} + +void expect_tables_equal(const DdTableResults& a, const DdTableResults& b) +{ + for (int strain = 0; strain < DDS_STRAINS; strain++) + for (int hand = 0; hand < DDS_HANDS; hand++) + EXPECT_EQ(a.res_table[strain][hand], b.res_table[strain][hand]) + << "Mismatch at strain=" << strain << " hand=" << hand; +} + +} // namespace + +// CalcDDtableN with maxThreads=1 must match the auto CalcDDtable. +TEST(MaxThreadsEquivalence, CalcDDtableNMatchesAuto) +{ + InitialiseStaticMemory(); + DdTableDeal deal = make_known_deal(); + + DdTableResults table_auto{}; + ASSERT_EQ(CalcDDtable(deal, &table_auto), RETURN_NO_FAULT); + + DdTableResults table_one{}; + ASSERT_EQ(CalcDDtableN(deal, &table_one, /*maxThreads=*/1), RETURN_NO_FAULT); + expect_tables_equal(table_auto, table_one); + + if (std::thread::hardware_concurrency() > 2) + { + DdTableResults table_two{}; + ASSERT_EQ(CalcDDtableN(deal, &table_two, /*maxThreads=*/2), RETURN_NO_FAULT); + expect_tables_equal(table_auto, table_two); + } +} + +// SolveAllBoardsBinN with maxThreads=1 must match the auto SolveAllBoardsBin. +TEST(MaxThreadsEquivalence, SolveAllBoardsBinNMatchesAuto) +{ + InitialiseStaticMemory(); + DdTableDeal table_deal = make_known_deal(); + + // Solve all five strains as separate boards. + Boards bo{}; + bo.no_of_boards = DDS_STRAINS; + for (int tr = 0; tr < DDS_STRAINS; tr++) + { + Deal dl{}; + for (int h = 0; h < DDS_HANDS; h++) + for (int s = 0; s < DDS_SUITS; s++) + dl.remainCards[h][s] = table_deal.cards[h][s]; + dl.trump = tr; + dl.first = 0; + bo.deals[tr] = dl; + bo.target[tr] = -1; + bo.solutions[tr] = 1; + bo.mode[tr] = 1; + } + + SolvedBoards solved_auto{}; + ASSERT_EQ(SolveAllBoardsBin(&bo, &solved_auto), RETURN_NO_FAULT); + + SolvedBoards solved_one{}; + ASSERT_EQ(SolveAllBoardsBinN(&bo, &solved_one, /*maxThreads=*/1), RETURN_NO_FAULT); + + ASSERT_EQ(solved_auto.no_of_boards, solved_one.no_of_boards); + for (int b = 0; b < solved_auto.no_of_boards; b++) + { + const FutureTricks& fa = solved_auto.solved_board[b]; + const FutureTricks& fo = solved_one.solved_board[b]; + ASSERT_EQ(fa.cards, fo.cards) << "card count differs at board=" << b; + // Only the first `cards` entries are meaningful; the tail is uninitialised. + for (int c = 0; c < fa.cards; c++) + { + EXPECT_EQ(fa.suit[c], fo.suit[c]) << "suit at board=" << b << " c=" << c; + EXPECT_EQ(fa.rank[c], fo.rank[c]) << "rank at board=" << b << " c=" << c; + EXPECT_EQ(fa.equals[c], fo.equals[c]) << "equals at board=" << b << " c=" << c; + EXPECT_EQ(fa.score[c], fo.score[c]) << "score at board=" << b << " c=" << c; + } + } +} + +// InitialiseStaticMemory leaves the library usable for a subsequent solve. +TEST(MaxThreadsEquivalence, InitialiseStaticMemoryThenSolve) +{ + InitialiseStaticMemory(); + DdTableDeal deal = make_known_deal(); + DdTableResults table{}; + EXPECT_EQ(CalcDDtable(deal, &table), RETURN_NO_FAULT); +} + +// The deprecated SetMaxThreads alias still initialises the library. +TEST(MaxThreadsEquivalence, DeprecatedSetMaxThreadsAliasStillWorks) +{ + SetMaxThreads(1); + DdTableDeal deal = make_known_deal(); + DdTableResults table{}; + EXPECT_EQ(CalcDDtable(deal, &table), RETURN_NO_FAULT); +} diff --git a/library/tests/system/worker_count_test.cpp b/library/tests/system/worker_count_test.cpp new file mode 100644 index 00000000..dfe6aecd --- /dev/null +++ b/library/tests/system/worker_count_test.cpp @@ -0,0 +1,49 @@ +/// @file worker_count_test.cpp +/// @brief Unit tests for resolve_worker_count (batch worker-count resolution). +/// +/// Validates the shared helper that maps an optional max_threads cap and a work +/// item count onto the number of worker threads to use. + +#include +#include +#include + +#include + +namespace +{ + +// Mirror the helper's "auto" computation so the test is host-independent. +int auto_workers(const int count) +{ + const unsigned hw = std::thread::hardware_concurrency(); + const int hw_or_1 = hw > 0 ? static_cast(hw) : 1; + return std::max(1, std::min(hw_or_1, count)); +} + +} // namespace + +TEST(ResolveWorkerCount, NonPositiveCapUsesAuto) +{ + const int count = 8; + EXPECT_EQ(resolve_worker_count(0, count), auto_workers(count)); + EXPECT_EQ(resolve_worker_count(-4, count), auto_workers(count)); +} + +TEST(ResolveWorkerCount, CapLargerThanCountClampsToCount) +{ + EXPECT_EQ(resolve_worker_count(1000, 5), 5); +} + +TEST(ResolveWorkerCount, CapSmallerThanCountAndHardwareIsHonoured) +{ + // A cap of 1 is always <= count and <= hardware_concurrency. + EXPECT_EQ(resolve_worker_count(1, 8), 1); +} + +TEST(ResolveWorkerCount, SingleItemAlwaysOneWorker) +{ + EXPECT_EQ(resolve_worker_count(0, 1), 1); + EXPECT_EQ(resolve_worker_count(16, 1), 1); + EXPECT_EQ(resolve_worker_count(1, 1), 1); +} From 3acd2e9c3056d033f7a55609a8c7c5a0295696de Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Sun, 21 Jun 2026 18:16:13 +0100 Subject: [PATCH 06/16] fix: align argument names and default values. --- library/src/solve_board.cpp | 6 +++--- library/src/solve_board.hpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/library/src/solve_board.cpp b/library/src/solve_board.cpp index 23eedf51..d484ac22 100644 --- a/library/src/solve_board.cpp +++ b/library/src/solve_board.cpp @@ -63,7 +63,7 @@ static auto boards_from_pbn( auto solve_all_boards_n( Boards const& bds, SolvedBoards& solved, - int max_threads = 0) -> int + int max_threads) -> int { const int n = bds.no_of_boards; if (n > MAXNOOFBOARDS) @@ -113,13 +113,13 @@ auto solve_all_boards_n( auto solve_all_boards_pbn_n( BoardsPBN const& bop, SolvedBoards& solved, - const int worker_cap) -> int + const int max_threads) -> int { Boards bo; const int rc = boards_from_pbn(bop, bo); if (rc != RETURN_NO_FAULT) return rc; - return solve_all_boards_n(bo, solved, worker_cap); + return solve_all_boards_n(bo, solved, max_threads); } diff --git a/library/src/solve_board.hpp b/library/src/solve_board.hpp index 400fe68a..5032aea0 100644 --- a/library/src/solve_board.hpp +++ b/library/src/solve_board.hpp @@ -17,12 +17,12 @@ auto solve_all_boards_n( Boards const& bds, SolvedBoards& solved, - int worker_cap = 0) -> int; + int max_threads = 0) -> int; auto solve_all_boards_pbn_n( BoardsPBN const& bop, SolvedBoards& solved, - int worker_cap = 0) -> int; + int max_threads = 0) -> int; auto solve_all_boards_n_seq( Boards const& bds, From 31c4cd425965f339420d96ee27ca622360cf6d19 Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Sun, 21 Jun 2026 18:26:08 +0100 Subject: [PATCH 07/16] fix: updates documenting comment. --- library/src/system/parallel_boards.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/system/parallel_boards.hpp b/library/src/system/parallel_boards.hpp index 763b4716..2f19b6de 100644 --- a/library/src/system/parallel_boards.hpp +++ b/library/src/system/parallel_boards.hpp @@ -16,7 +16,7 @@ * @brief Resolve the number of worker threads to use. * * @param max_threads Requested cap; <= 0 means "auto" (use hardware concurrency). - * @param count Number of work items; the result is clamped to [1, count]. + * @param count Number of work items; the result is clamped to [1, count] when count > 0 and to 1 when count <= 0. * @return The worker count to use. */ auto resolve_worker_count(int max_threads, int count) -> int; From 441feca9f6c67e1049d45f0cc9bf7e7df2f2719e Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Sun, 21 Jun 2026 22:59:51 +0100 Subject: [PATCH 08/16] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- dotnet/DDS_Core/DDS.cs | 2 +- library/src/api/dll.h | 2 +- library/src/solve_board.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/DDS_Core/DDS.cs b/dotnet/DDS_Core/DDS.cs index 90afffbb..53a8a2c8 100644 --- a/dotnet/DDS_Core/DDS.cs +++ b/dotnet/DDS_Core/DDS.cs @@ -29,7 +29,7 @@ public class DDS /// via SolverContext and SolverConfig. /// /// - /// Maximum number of threads to use. + /// Ignored; retained for backward compatibility. // The underlying C symbol SetMaxThreads was renamed to // InitialiseStaticMemory; this P/Invoke still targets the deprecated alias. [Obsolete("Use SolverContext instead.")] diff --git a/library/src/api/dll.h b/library/src/api/dll.h index c27504a1..c3e59574 100644 --- a/library/src/api/dll.h +++ b/library/src/api/dll.h @@ -439,7 +439,7 @@ struct DDSInfo EXTERN_C DLLEXPORT auto STDCALL InitialiseStaticMemory() -> void; /** - * @brief Set the maximum number of threads used by the solver. + * @brief Deprecated alias of InitialiseStaticMemory(). * * @deprecated Use InitialiseStaticMemory(); the thread count argument is * ignored (internal batch threading was removed). In the modern diff --git a/library/src/solve_board.cpp b/library/src/solve_board.cpp index d484ac22..8ff52f25 100644 --- a/library/src/solve_board.cpp +++ b/library/src/solve_board.cpp @@ -187,7 +187,7 @@ int STDCALL SolveAllBoardsN( } if (convert_from_pbn(bop->deals[k].remainCards, bo.deals[k].remainCards) - != 1) + != RETURN_NO_FAULT) return RETURN_PBN_FAULT; } From d5ef9761fb695726cdf7319bef8484b20f9c7b63 Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Mon, 22 Jun 2026 10:42:05 +0100 Subject: [PATCH 09/16] exports initialise static memory to python and deprecates set max threads. --- python/dds3/__init__.py | 3 +++ python/src/bindings.cpp | 43 ++++++++++++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/python/dds3/__init__.py b/python/dds3/__init__.py index 8da2bf96..dda8d813 100644 --- a/python/dds3/__init__.py +++ b/python/dds3/__init__.py @@ -7,6 +7,7 @@ from ._dds3 import calc_par from ._dds3 import calc_par_from_table from ._dds3 import dealer_par + from ._dds3 import initialise_static_memory from ._dds3 import module_name from ._dds3 import par from ._dds3 import set_max_threads @@ -25,6 +26,7 @@ from _dds3 import calc_par from _dds3 import calc_par_from_table from _dds3 import dealer_par + from _dds3 import initialise_static_memory from _dds3 import module_name from _dds3 import par from _dds3 import set_max_threads @@ -43,6 +45,7 @@ "calc_par", "calc_par_from_table", "dealer_par", + "initialise_static_memory", "module_name", "par", "set_max_threads", diff --git a/python/src/bindings.cpp b/python/src/bindings.cpp index 94170ba8..4ae723e5 100644 --- a/python/src/bindings.cpp +++ b/python/src/bindings.cpp @@ -598,8 +598,26 @@ auto register_calc_par_bindings(py::module_& module) -> void auto register_analysis_bindings(py::module_& module) -> void { - // set_max_threads: configure the worker-thread count used by the batch - // APIs (solve_all_boards_*, analyse_all_plays_pbn). 0 = auto-configure. + // initialise_static_memory: allocate the solver's static memory pools and + // perform one-time lookup-table initialisation. This does NOT control the + // worker-thread count; use solve_all_boards_* (which parallelise across the + // machine's hardware threads automatically) or one SolverContext per worker + // thread for per-board concurrency. + module.def( + "initialise_static_memory", + []() { + InitialiseStaticMemory(); + }, + "Initialise the solver's static memory.\n\n" + "Allocates the transposition-table memory pools and performs one-time\n" + "lookup-table initialisation. This does NOT control the number of worker\n" + "threads: solve_all_boards_* parallelise across the machine's hardware\n" + "threads automatically, and for per-board concurrency from Python you\n" + "create one SolverContext per worker thread and pass it to solve_board /\n" + "solve_board_pbn."); + + // set_max_threads: DEPRECATED alias of initialise_static_memory. The thread + // count argument is ignored; retained only for backward compatibility. module.def( "set_max_threads", [](const int user_threads) { @@ -608,19 +626,26 @@ auto register_analysis_bindings(py::module_& module) -> void "user_threads has invalid value " + std::to_string(user_threads) + " (expected >= 0; 0 = auto)"); } + if (PyErr_WarnEx( + PyExc_DeprecationWarning, + "set_max_threads() is deprecated; use initialise_static_memory(). " + "The user_threads argument is ignored.", + 1) != 0) { + throw py::error_already_set(); + } SetMaxThreads(user_threads); }, py::arg("user_threads") = 0, + "DEPRECATED: use initialise_static_memory() instead.\n\n" "Legacy thread-resource hook (wraps the deprecated SetMaxThreads C API,\n" - "now a thin alias of InitialiseStaticMemory).\n\n" + "now a thin alias of InitialiseStaticMemory). Calling this emits a\n" + "DeprecationWarning.\n\n" "This does NOT control DDS's batch parallelism and is retained only for\n" - "backward compatibility. solve_all_boards_* already parallelise across the\n" - "machine's hardware threads automatically (see solve_boards_n); the value\n" - "passed here does not size that pool. analyse_all_plays_pbn currently runs\n" - "sequentially. For per-board concurrency from Python, create one\n" - "SolverContext per worker thread and pass it to solve_board / solve_board_pbn.\n\n" + "backward compatibility. analyse_all_plays_pbn currently runs sequentially. For\n" + "per-board concurrency from Python, create one SolverContext per worker\n" + "thread and pass it to solve_board / solve_board_pbn.\n\n" "Args:\n" - " user_threads (int, optional): Must be >= 0; 0 = auto. Default: 0\n\n" + " user_threads (int, optional): Ignored; must be >= 0. Default: 0\n\n" "Raises:\n" " ValueError: If user_threads < 0."); From 9c831a84bb68229eb79afe83f612ed9f9f6817c9 Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Mon, 22 Jun 2026 20:38:46 +0100 Subject: [PATCH 10/16] fix: removes code duplication and uses nthreads consistently. --- library/src/calc_tables.cpp | 3 +-- library/src/solve_board.cpp | 29 +---------------------------- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/library/src/calc_tables.cpp b/library/src/calc_tables.cpp index 727753e5..b0446fe8 100644 --- a/library/src/calc_tables.cpp +++ b/library/src/calc_tables.cpp @@ -8,7 +8,6 @@ */ #include "calc_tables.hpp" -#include #include #include @@ -138,7 +137,7 @@ auto calc_all_boards_n( else { std::vector contexts(static_cast(nthreads)); - err = parallel_all_boards_n(n, max_threads, + err = parallel_all_boards_n(n, nthreads, [&](const int worker_id, const int bno) -> int { return calc_single_common_internal( contexts[static_cast(worker_id)], *bop, *solvedp, bno); diff --git a/library/src/solve_board.cpp b/library/src/solve_board.cpp index 8ff52f25..49fc7ef2 100644 --- a/library/src/solve_board.cpp +++ b/library/src/solve_board.cpp @@ -7,7 +7,6 @@ See LICENSE and README. */ -#include #include #include "solve_board.hpp" @@ -20,7 +19,6 @@ #include -extern Memory memory; extern Scheduler scheduler; auto same_board( @@ -167,32 +165,7 @@ int STDCALL SolveAllBoardsN( SolvedBoards * solvedp, int maxThreads) { - Boards bo; - bo.no_of_boards = bop->no_of_boards; - if (bo.no_of_boards > MAXNOOFBOARDS) - return RETURN_TOO_MANY_BOARDS; - - for (int k = 0; k < bop->no_of_boards; k++) - { - bo.mode[k] = bop->mode[k]; - bo.solutions[k] = bop->solutions[k]; - bo.target[k] = bop->target[k]; - bo.deals[k].first = bop->deals[k].first; - bo.deals[k].trump = bop->deals[k].trump; - - for (int i = 0; i <= 2; i++) - { - bo.deals[k].currentTrickSuit[i] = bop->deals[k].currentTrickSuit[i]; - bo.deals[k].currentTrickRank[i] = bop->deals[k].currentTrickRank[i]; - } - - if (convert_from_pbn(bop->deals[k].remainCards, bo.deals[k].remainCards) - != RETURN_NO_FAULT) - return RETURN_PBN_FAULT; - } - - int res = solve_all_boards_n(bo, * solvedp, maxThreads); - return res; + return solve_all_boards_pbn_n(* bop, * solvedp, maxThreads); } From 25477c53da7ec2f596888e01a8ab822092544f48 Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Mon, 22 Jun 2026 20:41:49 +0100 Subject: [PATCH 11/16] fix: releases the global interpreter lock before doing work in initialise static memory. --- python/src/bindings.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/src/bindings.cpp b/python/src/bindings.cpp index 4ae723e5..ab080117 100644 --- a/python/src/bindings.cpp +++ b/python/src/bindings.cpp @@ -606,6 +606,7 @@ auto register_analysis_bindings(py::module_& module) -> void module.def( "initialise_static_memory", []() { + py::gil_scoped_release release; InitialiseStaticMemory(); }, "Initialise the solver's static memory.\n\n" @@ -633,6 +634,7 @@ auto register_analysis_bindings(py::module_& module) -> void 1) != 0) { throw py::error_already_set(); } + py::gil_scoped_release release; SetMaxThreads(user_threads); }, py::arg("user_threads") = 0, From 582b1270f6475bdd227e78684f50ca141cc704b4 Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Tue, 23 Jun 2026 08:07:40 +0100 Subject: [PATCH 12/16] fix: deletes the input guard on the ignored user_threads argument. --- python/src/bindings.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/python/src/bindings.cpp b/python/src/bindings.cpp index ab080117..21333fea 100644 --- a/python/src/bindings.cpp +++ b/python/src/bindings.cpp @@ -622,11 +622,6 @@ auto register_analysis_bindings(py::module_& module) -> void module.def( "set_max_threads", [](const int user_threads) { - if (user_threads < 0) { - throw py::value_error( - "user_threads has invalid value " + std::to_string(user_threads) + - " (expected >= 0; 0 = auto)"); - } if (PyErr_WarnEx( PyExc_DeprecationWarning, "set_max_threads() is deprecated; use initialise_static_memory(). " From 600515d1782300e5abc8876826243957b292cd88 Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Tue, 23 Jun 2026 10:07:08 +0100 Subject: [PATCH 13/16] fix: removes test that is now incorrect. --- python/tests/test_analyse.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/python/tests/test_analyse.py b/python/tests/test_analyse.py index 1f589d2e..544c5b3e 100644 --- a/python/tests/test_analyse.py +++ b/python/tests/test_analyse.py @@ -61,11 +61,6 @@ def test_analyse_all_plays_missing_play(self) -> None: with self.assertRaises(KeyError): analyse_all_plays_pbn([{"remain_cards": DEAL}]) - def test_set_max_threads_rejects_negative(self) -> None: - with self.assertRaises(ValueError): - set_max_threads(-1) - set_max_threads(0) # 0 is valid (auto) - class TestDealerPar(unittest.TestCase): """Tests for dealer_par.""" From 26e2912602ace7cffd9e705ad2bf1e3c193b8e5b Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Wed, 24 Jun 2026 08:26:17 +0100 Subject: [PATCH 14/16] fix: remove mention of the guard on user_threads from the python doc string. --- python/src/bindings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/src/bindings.cpp b/python/src/bindings.cpp index 21333fea..c9399a30 100644 --- a/python/src/bindings.cpp +++ b/python/src/bindings.cpp @@ -642,7 +642,7 @@ auto register_analysis_bindings(py::module_& module) -> void "per-board concurrency from Python, create one SolverContext per worker\n" "thread and pass it to solve_board / solve_board_pbn.\n\n" "Args:\n" - " user_threads (int, optional): Ignored; must be >= 0. Default: 0\n\n" + " user_threads (int, optional): Ignored;\n\n" "Raises:\n" " ValueError: If user_threads < 0."); From 5397d23cef36155db377e9bfe46e5088a39f2bed Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Thu, 25 Jun 2026 13:13:10 +0100 Subject: [PATCH 15/16] fix: removes incorrect mention of value error. --- python/src/bindings.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/python/src/bindings.cpp b/python/src/bindings.cpp index c9399a30..6240e4cb 100644 --- a/python/src/bindings.cpp +++ b/python/src/bindings.cpp @@ -607,7 +607,7 @@ auto register_analysis_bindings(py::module_& module) -> void "initialise_static_memory", []() { py::gil_scoped_release release; - InitialiseStaticMemory(); + InitializeStaticMemory(); }, "Initialise the solver's static memory.\n\n" "Allocates the transposition-table memory pools and performs one-time\n" @@ -635,16 +635,14 @@ auto register_analysis_bindings(py::module_& module) -> void py::arg("user_threads") = 0, "DEPRECATED: use initialise_static_memory() instead.\n\n" "Legacy thread-resource hook (wraps the deprecated SetMaxThreads C API,\n" - "now a thin alias of InitialiseStaticMemory). Calling this emits a\n" + "now a thin alias of InitializeStaticMemory). Calling this emits a\n" "DeprecationWarning.\n\n" "This does NOT control DDS's batch parallelism and is retained only for\n" "backward compatibility. analyse_all_plays_pbn currently runs sequentially. For\n" "per-board concurrency from Python, create one SolverContext per worker\n" "thread and pass it to solve_board / solve_board_pbn.\n\n" "Args:\n" - " user_threads (int, optional): Ignored;\n\n" - "Raises:\n" - " ValueError: If user_threads < 0."); + " user_threads (int, optional): Ignored;\n\n"); // analyse_play_pbn: double-dummy trick count after each card of a played hand. module.def( From a0fed2075456f4a374bd6af3dd12a551b02b2e91 Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Thu, 25 Jun 2026 13:13:50 +0100 Subject: [PATCH 16/16] fix: renames InitialiseStaticMemory to use the American misspelling. --- dotnet/DDS_Core/DDS.cs | 2 +- dotnet/DDS_Core/Native/DdsNative.cs | 2 +- examples/dd_table_for_deal.cpp | 4 --- examples/migration_example.cpp | 2 +- library/src/api/dll.h | 8 +++--- library/src/dds.cpp | 28 ++++++++++++++++--- library/src/init.cpp | 6 ++-- .../tests/system/context_equivalence_test.cpp | 2 +- .../tests/system/context_tt_facade_test.cpp | 6 ++-- .../system/max_threads_equivalence_test.cpp | 10 +++---- 10 files changed, 43 insertions(+), 27 deletions(-) diff --git a/dotnet/DDS_Core/DDS.cs b/dotnet/DDS_Core/DDS.cs index 53a8a2c8..7126e4fd 100644 --- a/dotnet/DDS_Core/DDS.cs +++ b/dotnet/DDS_Core/DDS.cs @@ -31,7 +31,7 @@ public class DDS /// /// Ignored; retained for backward compatibility. // The underlying C symbol SetMaxThreads was renamed to - // InitialiseStaticMemory; this P/Invoke still targets the deprecated alias. + // InitializeStaticMemory; this P/Invoke still targets the deprecated alias. [Obsolete("Use SolverContext instead.")] public void SetMaxThreads(int userThreads) => DdsNative.SetMaxThreads(userThreads); diff --git a/dotnet/DDS_Core/Native/DdsNative.cs b/dotnet/DDS_Core/Native/DdsNative.cs index 73f36d6a..8b9b7cf8 100644 --- a/dotnet/DDS_Core/Native/DdsNative.cs +++ b/dotnet/DDS_Core/Native/DdsNative.cs @@ -96,7 +96,7 @@ public static extern int dds_calc_par( SolverContextHandle ctx #region ====== Configuration and Resource Management ====== // The C symbol SetMaxThreads is deprecated and now a thin alias of - // InitialiseStaticMemory; the userThreads argument is ignored. + // InitializeStaticMemory; the userThreads argument is ignored. [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] public static extern void SetMaxThreads( int userThreads); diff --git a/examples/dd_table_for_deal.cpp b/examples/dd_table_for_deal.cpp index cc537939..48814f1a 100644 --- a/examples/dd_table_for_deal.cpp +++ b/examples/dd_table_for_deal.cpp @@ -226,10 +226,6 @@ auto main(int argc, char * argv[]) -> int DdTableResults table; char line[80]; -#if defined(__linux) || defined(__APPLE__) - InitialiseStaticMemory(); -#endif - const int res = CalcDDtablePBN(tableDealPBN, &table); if (res != RETURN_NO_FAULT) { diff --git a/examples/migration_example.cpp b/examples/migration_example.cpp index 0b78fa56..7114c1c2 100644 --- a/examples/migration_example.cpp +++ b/examples/migration_example.cpp @@ -17,7 +17,7 @@ void solve_legacy(const Deal& deal) { - InitialiseStaticMemory(); + InitializeStaticMemory(); SetResources(2000, 4); FutureTricks fut; diff --git a/library/src/api/dll.h b/library/src/api/dll.h index c3e59574..e1c3aa9c 100644 --- a/library/src/api/dll.h +++ b/library/src/api/dll.h @@ -436,12 +436,12 @@ struct DDSInfo * This does NOT control the number of worker threads — use the * SolveAllBoardsN / CalcAllTablesN family for per-call thread caps. */ -EXTERN_C DLLEXPORT auto STDCALL InitialiseStaticMemory() -> void; +EXTERN_C DLLEXPORT auto STDCALL InitializeStaticMemory() -> void; /** - * @brief Deprecated alias of InitialiseStaticMemory(). + * @brief Deprecated alias of InitializeStaticMemory(). * - * @deprecated Use InitialiseStaticMemory(); the thread count argument is + * @deprecated Use InitializeStaticMemory(); the thread count argument is * ignored (internal batch threading was removed). In the modern * C++ API, thread count is controlled by the embedding application * (typically one SolverContext per worker thread), or per call via @@ -451,7 +451,7 @@ EXTERN_C DLLEXPORT auto STDCALL InitialiseStaticMemory() -> void; * @param userThreads Ignored; retained for backward compatibility. * * This function is part of the legacy C API and is maintained for backward - * compatibility. It simply forwards to InitialiseStaticMemory(). + * compatibility. It simply forwards to InitializeStaticMemory(). */ EXTERN_C DLLEXPORT auto STDCALL SetMaxThreads( int userThreads) -> void; diff --git a/library/src/dds.cpp b/library/src/dds.cpp index 4762df7c..64991270 100644 --- a/library/src/dds.cpp +++ b/library/src/dds.cpp @@ -36,7 +36,7 @@ extern "C" BOOL APIENTRY DllMain( { if (ul_reason_for_call == DLL_PROCESS_ATTACH) - InitialiseStaticMemory(); + InitializeStaticMemory(); else if (ul_reason_for_call == DLL_PROCESS_DETACH) { FreeMemory(); @@ -63,18 +63,38 @@ void DDSInitialize(), DDSFinalize(); */ void DDSInitialize(void) { - InitialiseStaticMemory(); + InitializeStaticMemory(); } /** * @brief Finalize and clean up the DDS library. */ -void DDSFinalize(void) +void DDSFinalize(void) { FreeMemory(); } + +/** + * @brief Library constructor/destructor for Apple platforms. + * + * Register DDSInitialize/DDSFinalize so the library's static memory is set + * up automatically when the library is loaded, matching the behaviour of the + * Windows (DllMain) and USES_CONSTRUCTOR paths. This frees callers from having + * to call InitializeStaticMemory() themselves. + */ +static void __attribute__ ((constructor)) libInit(void) +{ + DDSInitialize(); +} + + +static void __attribute__ ((destructor)) libFini(void) +{ + DDSFinalize(); +} + #elif defined(USES_CONSTRUCTOR) /** @@ -84,7 +104,7 @@ void DDSFinalize(void) */ static void __attribute__ ((constructor)) libInit(void) { - InitialiseStaticMemory(); + InitializeStaticMemory(); } #endif diff --git a/library/src/init.cpp b/library/src/init.cpp index 0fb61e32..1b206925 100644 --- a/library/src/init.cpp +++ b/library/src/init.cpp @@ -41,14 +41,14 @@ int _initialized = 0; * * Public API documentation is maintained in the API headers. */ -void STDCALL InitialiseStaticMemory() +void STDCALL InitializeStaticMemory() { SetResources(0, 0); } /* - * Deprecated alias for InitialiseStaticMemory(). The thread count is no + * Deprecated alias for InitializeStaticMemory(). The thread count is no * longer meaningful (internal batch threading was removed), so the argument * is ignored. * @@ -58,7 +58,7 @@ void STDCALL SetMaxThreads( int userThreads) { (void) userThreads; - InitialiseStaticMemory(); + InitializeStaticMemory(); } diff --git a/library/tests/system/context_equivalence_test.cpp b/library/tests/system/context_equivalence_test.cpp index 4396046f..fe586b29 100644 --- a/library/tests/system/context_equivalence_test.cpp +++ b/library/tests/system/context_equivalence_test.cpp @@ -58,7 +58,7 @@ static DdTableDeal make_known_deal() TEST(SystemContextEquivalence, LegacyVsContextReturnCode) { // Ensure DDS system and thread-local memory are initialized - InitialiseStaticMemory(); + InitializeStaticMemory(); const int thr = 0; FutureTricks ft_legacy{}; FutureTricks ft_ctx{}; diff --git a/library/tests/system/context_tt_facade_test.cpp b/library/tests/system/context_tt_facade_test.cpp index 3ab63ecd..0b6f9fac 100644 --- a/library/tests/system/context_tt_facade_test.cpp +++ b/library/tests/system/context_tt_facade_test.cpp @@ -15,7 +15,7 @@ extern Memory memory; TEST(SystemContextTTFacades, ResetAndResizeAreNoopsWithoutTT) { - InitialiseStaticMemory(); + InitializeStaticMemory(); // Some environments may compute 0 allowable threads (e.g., macOS sandbox), // so ensure we have at least one thread allocated for the test. if (memory.NumThreads() == 0) @@ -34,7 +34,7 @@ TEST(SystemContextTTFacades, ResetAndResizeAreNoopsWithoutTT) TEST(SystemContextTTFacades, ResizeCreatesWhenExisting) { - InitialiseStaticMemory(); + InitializeStaticMemory(); // Ensure at least one thread exists; fall back to a small thread config. if (memory.NumThreads() == 0) memory.Resize(1, DDS_TT_SMALL, THREADMEM_SMALL_DEF_MB, THREADMEM_SMALL_MAX_MB); @@ -51,7 +51,7 @@ TEST(SystemContextTTFacades, ResizeCreatesWhenExisting) TEST(SystemContextTTFacades, Lifecycle_LookupAddClearDispose) { - InitialiseStaticMemory(); + InitializeStaticMemory(); if (memory.NumThreads() == 0) memory.Resize(1, DDS_TT_SMALL, THREADMEM_SMALL_DEF_MB, THREADMEM_SMALL_MAX_MB); diff --git a/library/tests/system/max_threads_equivalence_test.cpp b/library/tests/system/max_threads_equivalence_test.cpp index b7210603..52e48b9a 100644 --- a/library/tests/system/max_threads_equivalence_test.cpp +++ b/library/tests/system/max_threads_equivalence_test.cpp @@ -49,7 +49,7 @@ void expect_tables_equal(const DdTableResults& a, const DdTableResults& b) // CalcDDtableN with maxThreads=1 must match the auto CalcDDtable. TEST(MaxThreadsEquivalence, CalcDDtableNMatchesAuto) { - InitialiseStaticMemory(); + InitializeStaticMemory(); DdTableDeal deal = make_known_deal(); DdTableResults table_auto{}; @@ -70,7 +70,7 @@ TEST(MaxThreadsEquivalence, CalcDDtableNMatchesAuto) // SolveAllBoardsBinN with maxThreads=1 must match the auto SolveAllBoardsBin. TEST(MaxThreadsEquivalence, SolveAllBoardsBinNMatchesAuto) { - InitialiseStaticMemory(); + InitializeStaticMemory(); DdTableDeal table_deal = make_known_deal(); // Solve all five strains as separate boards. @@ -113,10 +113,10 @@ TEST(MaxThreadsEquivalence, SolveAllBoardsBinNMatchesAuto) } } -// InitialiseStaticMemory leaves the library usable for a subsequent solve. -TEST(MaxThreadsEquivalence, InitialiseStaticMemoryThenSolve) +// InitializeStaticMemory leaves the library usable for a subsequent solve. +TEST(MaxThreadsEquivalence, InitializeStaticMemoryThenSolve) { - InitialiseStaticMemory(); + InitializeStaticMemory(); DdTableDeal deal = make_known_deal(); DdTableResults table{}; EXPECT_EQ(CalcDDtable(deal, &table), RETURN_NO_FAULT);