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."""