diff --git a/dotnet/DDS_Core/DDS.cs b/dotnet/DDS_Core/DDS.cs
index adbaa594..7126e4fd 100644
--- a/dotnet/DDS_Core/DDS.cs
+++ b/dotnet/DDS_Core/DDS.cs
@@ -29,7 +29,9 @@ 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
+ // 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 9ee2c487..8b9b7cf8 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
+ // 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 9f59adfc..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__)
- SetMaxThreads(0);
-#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 e9e40088..7114c1c2 100644
--- a/examples/migration_example.cpp
+++ b/examples/migration_example.cpp
@@ -17,7 +17,7 @@
void solve_legacy(const Deal& deal)
{
- SetMaxThreads(4);
+ InitializeStaticMemory();
SetResources(2000, 4);
FutureTricks fut;
diff --git a/library/src/api/dll.h b/library/src/api/dll.h
index 7fbbdd44..e1c3aa9c 100644
--- a/library/src/api/dll.h
+++ b/library/src/api/dll.h
@@ -429,20 +429,29 @@ struct DDSInfo
/**
- * @brief Set the maximum number of threads used by the solver.
+ * @brief Initialise the solver's static memory.
*
- * @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.
+ * 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 InitializeStaticMemory() -> void;
+
+/**
+ * @brief Deprecated alias of InitializeStaticMemory().
+ *
+ * @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
+ * 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 InitializeStaticMemory().
*/
EXTERN_C DLLEXPORT auto STDCALL SetMaxThreads(
int userThreads) -> void;
@@ -542,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.
*
@@ -553,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.
*
@@ -570,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.
*
@@ -587,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.
*
@@ -598,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 8003944d..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
@@ -27,7 +26,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 +109,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 +121,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)
@@ -160,9 +160,10 @@ auto calc_all_boards_n(
-int STDCALL CalcDDtable(
+int STDCALL CalcDDtableN(
DdTableDeal tableDeal,
- DdTableResults * tablep)
+ DdTableResults * tablep,
+ int maxThreads)
{
Deal dl;
Boards bo;
@@ -191,7 +192,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;
@@ -211,12 +212,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
@@ -278,7 +288,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;
@@ -316,12 +326,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++)
@@ -330,24 +352,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/dds.cpp b/library/src/dds.cpp
index 265e77dc..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)
- SetMaxThreads(0);
+ InitializeStaticMemory();
else if (ul_reason_for_call == DLL_PROCESS_DETACH)
{
FreeMemory();
@@ -61,20 +61,40 @@ void DDSInitialize(), DDSFinalize();
/**
* @brief Initialize the DDS library.
*/
-void DDSInitialize(void)
+void DDSInitialize(void)
{
- SetMaxThreads(0);
+ 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)
{
- SetMaxThreads(0);
+ InitializeStaticMemory();
}
#endif
diff --git a/library/src/init.cpp b/library/src/init.cpp
index 3e22dd11..1b206925 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 InitializeStaticMemory()
+{
+ SetResources(0, 0);
+}
+
+
+/*
+ * Deprecated alias for InitializeStaticMemory(). 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;
+ InitializeStaticMemory();
}
diff --git a/library/src/solve_board.cpp b/library/src/solve_board.cpp
index edd78abb..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(
@@ -63,7 +61,7 @@ static auto boards_from_pbn(
auto solve_all_boards_n(
Boards const& bds,
SolvedBoards& solved,
- const int worker_cap) -> int
+ int max_threads) -> int
{
const int n = bds.no_of_boards;
if (n > MAXNOOFBOARDS)
@@ -76,7 +74,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;
@@ -113,13 +111,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);
}
@@ -162,11 +160,29 @@ 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)
+{
+ return solve_all_boards_pbn_n(* bop, * solvedp, maxThreads);
+}
+
+
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 +190,7 @@ int STDCALL SolveAllBoardsBin(
Boards const * bop,
SolvedBoards * solvedp)
{
- return solve_all_boards_n(* bop, * solvedp);
+ return SolveAllBoardsBinN(bop, solvedp, 0);
}
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,
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..2f19b6de 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] 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;
+
/**
* @brief Process boards [0, count) with work-stealing parallelism.
*
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..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
- SetMaxThreads(1);
+ 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 b9c6076a..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)
{
- SetMaxThreads(1);
+ 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)
{
- SetMaxThreads(1);
+ 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)
{
- SetMaxThreads(1);
+ 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
new file mode 100644
index 00000000..52e48b9a
--- /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)
+{
+ InitializeStaticMemory();
+ 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)
+{
+ InitializeStaticMemory();
+ 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;
+ }
+ }
+}
+
+// InitializeStaticMemory leaves the library usable for a subsequent solve.
+TEST(MaxThreadsEquivalence, InitializeStaticMemoryThenSolve)
+{
+ InitializeStaticMemory();
+ 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);
+}
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 be83081a..6240e4cb 100644
--- a/python/src/bindings.cpp
+++ b/python/src/bindings.cpp
@@ -598,30 +598,51 @@ 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",
+ []() {
+ py::gil_scoped_release release;
+ InitializeStaticMemory();
+ },
+ "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) {
- 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(). "
+ "The user_threads argument is ignored.",
+ 1) != 0) {
+ throw py::error_already_set();
+ }
+ py::gil_scoped_release release;
SetMaxThreads(user_threads);
},
py::arg("user_threads") = 0,
- "Legacy thread-resource hook (wraps the deprecated SetMaxThreads C API).\n\n"
+ "DEPRECATED: use initialise_static_memory() instead.\n\n"
+ "Legacy thread-resource hook (wraps the deprecated SetMaxThreads C API,\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. 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"
- "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(
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."""