diff --git a/jni/exported_symbols.lds b/jni/exported_symbols.lds index 1e4e0980..66d30184 100644 --- a/jni/exported_symbols.lds +++ b/jni/exported_symbols.lds @@ -6,6 +6,8 @@ _CalcAllTables _CalcAllTablesN _CalcAllTablesPBN _CalcAllTablesPBNN +_CalcAllTablesPBNX +_CalcAllTablesX _CalcDDtable _CalcDDtableN _CalcDDtablePBN diff --git a/jni/version_script.lds b/jni/version_script.lds index d02f1be6..ab0e82b2 100644 --- a/jni/version_script.lds +++ b/jni/version_script.lds @@ -8,6 +8,8 @@ CalcAllTablesN; CalcAllTablesPBN; CalcAllTablesPBNN; + CalcAllTablesPBNX; + CalcAllTablesX; CalcDDtable; CalcDDtableN; CalcDDtablePBN; diff --git a/library/src/api/dll.h b/library/src/api/dll.h index e1c3aa9c..f6073a35 100644 --- a/library/src/api/dll.h +++ b/library/src/api/dll.h @@ -646,6 +646,43 @@ EXTERN_C DLLEXPORT auto STDCALL CalcAllTablesPBNN( struct AllParResults * presp, int maxThreads) -> int; +/** + * @brief Unbounded CalcAllTables: any number of deals, one parallel board job. + * + * Legacy CalcAllTablesN remains capped at MAXNOOFTABLES. This entry point + * expands all deal×strain boards and solves them in a single + * parallel_all_boards_n dispatch (heap-backed), matching the ddss large-batch + * shape while preserving the fixed-size ABI of the legacy structs. + * + * @param numDeals Number of deals (may exceed MAXNOOFTABLES) + * @param deals Flat array of numDeals deals + * @param mode Par mode (-1 = no par); par requires all strains and non-null par + * @param trumpFilter Per-strain filter (0 = include) + * @param results Output array of numDeals tables + * @param par Optional par output (numDeals); required when mode requests par + * @param maxThreads Worker cap; <= 0 means auto + */ +EXTERN_C DLLEXPORT auto STDCALL CalcAllTablesX( + int numDeals, + struct DdTableDeal const * deals, + int mode, + int const trumpFilter[DDS_STRAINS], + struct DdTableResults * results, + struct ParResults * par, + int maxThreads) -> int; + +/** + * @brief PBN variant of CalcAllTablesX. + */ +EXTERN_C DLLEXPORT auto STDCALL CalcAllTablesPBNX( + int numDeals, + struct DdTableDealPBN const * deals, + int mode, + int const trumpFilter[DDS_STRAINS], + struct DdTableResults * results, + struct ParResults * par, + int maxThreads) -> int; + /** * @brief Solve multiple bridge deals in PBN format. * diff --git a/library/src/calc_tables.cpp b/library/src/calc_tables.cpp index 72c6cf78..90116387 100644 --- a/library/src/calc_tables.cpp +++ b/library/src/calc_tables.cpp @@ -9,6 +9,7 @@ #include "calc_tables.hpp" #include +#include #include #include @@ -249,7 +250,7 @@ int STDCALL CalcDDtable( int STDCALL CalcAllTablesN( DdTableDeals const * dealsp, int mode, - int const trumpFilter[5], + int const trumpFilter[DDS_STRAINS], DdTablesRes * resp, AllParResults * presp, int maxThreads) @@ -355,7 +356,7 @@ int STDCALL CalcAllTablesN( int STDCALL CalcAllTables( DdTableDeals const * dealsp, int mode, - int const trumpFilter[5], + int const trumpFilter[DDS_STRAINS], DdTablesRes * resp, AllParResults * presp) { @@ -366,7 +367,7 @@ int STDCALL CalcAllTables( int STDCALL CalcAllTablesPBNN( DdTableDealsPBN const * dealsp, int mode, - int const trumpFilter[5], + int const trumpFilter[DDS_STRAINS], DdTablesRes * resp, AllParResults * presp, int maxThreads) @@ -386,7 +387,7 @@ int STDCALL CalcAllTablesPBNN( int STDCALL CalcAllTablesPBN( DdTableDealsPBN const * dealsp, int mode, - int const trumpFilter[5], + int const trumpFilter[DDS_STRAINS], DdTablesRes * resp, AllParResults * presp) { @@ -394,6 +395,206 @@ int STDCALL CalcAllTablesPBN( } +namespace +{ + +// Solve one strain board (all four declarers) into scores[DDS_HANDS]. +auto calc_single_deal_scores( + SolverContext& ctx, + Deal deal, + const int target, + const int solutions, + const int mode, + int scores[DDS_HANDS]) -> int +{ + FutureTricks fut{}; + deal.first = 0; + int res = solve_board(ctx, deal, target, solutions, mode, &fut); + if (res != RETURN_NO_FAULT) + return res; + scores[0] = fut.score[0]; + + for (int k = 1; k < DDS_HANDS; k++) + { + const int hint = (k == 2 ? fut.score[0] : 13 - fut.score[0]); + deal.first = k; + res = solve_same_board(ctx, deal, &fut, hint); + if (res != RETURN_NO_FAULT) + return res; + scores[k] = fut.score[0]; + } + return RETURN_NO_FAULT; +} + +} // namespace + + +int STDCALL CalcAllTablesX( + int numDeals, + DdTableDeal const * deals, + int mode, + int const trumpFilter[DDS_STRAINS], + DdTableResults * results, + ParResults * par, + int maxThreads) +{ + // C ABI: exceptions must not unwind into a foreign caller (UB). Heap + // allocations below (and parallel_all_boards_n) may throw; map any throw + // to RETURN_UNKNOWN_FAULT. + try + { + if (numDeals < 0) + return RETURN_TOO_MANY_TABLES; + if (numDeals == 0) + return RETURN_NO_FAULT; + if (deals == nullptr || results == nullptr || trumpFilter == nullptr) + return RETURN_UNKNOWN_FAULT; + + int included = 0; + for (int k = 0; k < DDS_STRAINS; k++) + { + if (!trumpFilter[k]) + included++; + } + if (included == 0) + return RETURN_NO_SUIT; + + const bool want_par = (mode > -1) && (mode < 4) && (included == DDS_STRAINS); + if (want_par && par == nullptr) + return RETURN_UNKNOWN_FAULT; + + // Expand every deal×included-strain into one board list and solve in a + // single parallel_all_boards_n job (heap-backed). This is the ddss-style + // large-batch shape; legacy CalcAllTablesN remains capped at MAXNOOFTABLES. + const int nboards = numDeals * included; + std::vector boards(static_cast(nboards)); + std::vector> scores(static_cast(nboards)); + + int ind = 0; + for (int m = 0; m < numDeals; m++) + { + for (int tr = DDS_STRAINS - 1; tr >= 0; tr--) + { + if (trumpFilter[tr]) + continue; + + Deal& dl = boards[static_cast(ind)]; + for (int h = 0; h < DDS_HANDS; h++) + for (int s = 0; s < DDS_SUITS; s++) + dl.remainCards[h][s] = deals[m].cards[h][s]; + dl.trump = tr; + dl.first = 0; + for (int k = 0; k <= 2; k++) + { + dl.currentTrickRank[k] = 0; + dl.currentTrickSuit[k] = 0; + } + ind++; + } + } + + const int nthreads = resolve_worker_count(maxThreads, nboards); + + // Hardest-first dispatch only helps with multiple workers (same idea as + // calc_all_boards_n). Skip fanout+sort on the single-thread path so large + // single-worker batches avoid O(n log n) overhead. + std::vector order; + if (nthreads > 1) + { + order.resize(static_cast(nboards)); + std::iota(order.begin(), order.end(), 0); + std::vector fanout(static_cast(nboards)); + for (int i = 0; i < nboards; i++) + fanout[static_cast(i)] = + dds::internal::deal_fanout(boards[static_cast(i)]); + std::stable_sort(order.begin(), order.end(), + [&](const int a, const int b) { + return fanout[static_cast(a)] > fanout[static_cast(b)]; + }); + } + + const int err = parallel_all_boards_n(nboards, nthreads, + [&](const int worker_id, const int bno) -> int { + (void)worker_id; + return calc_single_deal_scores( + dds::internal::worker_solver_context(), + boards[static_cast(bno)], + -1, 1, 1, + scores[static_cast(bno)].data()); + }, + order.empty() ? nullptr : &order); + if (err != RETURN_NO_FAULT) + return err; + + for (int m = 0; m < numDeals; m++) + { + for (int strainIndex = 0; strainIndex < included; strainIndex++) + { + const int index = m * included + strainIndex; + const int strain = boards[static_cast(index)].trump; + for (int first = 0; first < DDS_HANDS; first++) + { + results[m].res_table[strain][rho[first]] = + 13 - scores[static_cast(index)][static_cast(first)]; + } + } + } + + if (want_par) + { + for (int k = 0; k < numDeals; k++) + { + const int res = Par(&results[k], &par[k], mode); + if (res != RETURN_NO_FAULT) + return res; + } + } + + return RETURN_NO_FAULT; + } + catch (...) + { + return RETURN_UNKNOWN_FAULT; + } +} + + +int STDCALL CalcAllTablesPBNX( + int numDeals, + DdTableDealPBN const * deals, + int mode, + int const trumpFilter[DDS_STRAINS], + DdTableResults * results, + ParResults * par, + int maxThreads) +{ + // C ABI: same catch-all contract as CalcAllTablesX / dds_c_api.cpp. + try + { + if (numDeals < 0) + return RETURN_TOO_MANY_TABLES; + if (numDeals == 0) + return RETURN_NO_FAULT; + if (deals == nullptr || results == nullptr || trumpFilter == nullptr) + return RETURN_UNKNOWN_FAULT; + + std::vector binary(static_cast(numDeals)); + for (int i = 0; i < numDeals; ++i) + { + if (convert_from_pbn(deals[i].cards, binary[static_cast(i)].cards) != 1) + return RETURN_PBN_FAULT; + } + + return CalcAllTablesX( + numDeals, binary.data(), mode, trumpFilter, results, par, maxThreads); + } + catch (...) + { + return RETURN_UNKNOWN_FAULT; + } +} + + int STDCALL CalcDDtablePBNN( DdTableDealPBN tableDealPBN, DdTableResults * tablep, diff --git a/library/src/system/deal_fanout.cpp b/library/src/system/deal_fanout.cpp index 68b143a1..79ede289 100644 --- a/library/src/system/deal_fanout.cpp +++ b/library/src/system/deal_fanout.cpp @@ -9,6 +9,8 @@ #include "deal_fanout.hpp" +#include + #include namespace dds @@ -16,8 +18,20 @@ namespace dds namespace internal { +namespace +{ +std::atomic g_deal_fanout_call_count{0}; +} // namespace + +auto deal_fanout_call_count() -> int +{ + return g_deal_fanout_call_count.load(std::memory_order_relaxed); +} + auto deal_fanout(const Deal& dl) -> int { + g_deal_fanout_call_count.fetch_add(1, std::memory_order_relaxed); + // The fanout for a given suit and a given player is the number // of bit groups, so KT982 has 3 groups. In a given suit the // maximum number over all four players is 13. diff --git a/library/src/system/deal_fanout.hpp b/library/src/system/deal_fanout.hpp index 1ade73ea..48fc5956 100644 --- a/library/src/system/deal_fanout.hpp +++ b/library/src/system/deal_fanout.hpp @@ -15,5 +15,8 @@ namespace internal */ auto deal_fanout(const Deal& dl) -> int; +/** Test seam: how many times deal_fanout() has been called process-wide. */ +auto deal_fanout_call_count() -> int; + } // namespace internal } // namespace dds diff --git a/library/src/system/parallel_boards.cpp b/library/src/system/parallel_boards.cpp index ce69cb36..7bb468e8 100644 --- a/library/src/system/parallel_boards.cpp +++ b/library/src/system/parallel_boards.cpp @@ -25,6 +25,7 @@ namespace { std::atomic g_threads_created{0}; +std::atomic g_last_job_board_count{0}; class BoardWorkerPool @@ -274,6 +275,10 @@ auto parallel_boards_worker_threads_created() -> std::uint64_t return g_threads_created.load(std::memory_order_relaxed); } +auto parallel_boards_last_job_board_count() -> int +{ + return g_last_job_board_count.load(std::memory_order_relaxed); +} void shutdown_parallel_boards_pool() { @@ -297,6 +302,10 @@ auto parallel_all_boards_n( const std::function& process_board, const std::vector* order) -> int { + // Always record the requested count, including early-return paths, so the + // test seam cannot report a stale prior job. + g_last_job_board_count.store(count, std::memory_order_relaxed); + if (count <= 0) { return RETURN_NO_FAULT; diff --git a/library/src/system/parallel_boards.hpp b/library/src/system/parallel_boards.hpp index 551c7af9..faa651a7 100644 --- a/library/src/system/parallel_boards.hpp +++ b/library/src/system/parallel_boards.hpp @@ -64,6 +64,11 @@ namespace dds::internal // Cumulative number of OS worker threads created by the board pool (test seam). auto parallel_boards_worker_threads_created() -> std::uint64_t; +// Board count of the most recent parallel_all_boards_n call (test seam), +// including calls that return early for count <= 0. Used to assert unbounded +// calc submits one job covering the full batch. +auto parallel_boards_last_job_board_count() -> int; + // Join and destroy the process-local board worker pool. Safe to call with no // pool, and again after a later parallel_all_boards_n recreates it. void shutdown_parallel_boards_pool(); diff --git a/library/tests/loop.cpp b/library/tests/loop.cpp index 7dbeb6a8..1e6ef1eb 100644 --- a/library/tests/loop.cpp +++ b/library/tests/loop.cpp @@ -105,55 +105,58 @@ void loop_solve( bool loop_calc( - DdTableDealsPBN * dealsp, - DdTablesRes * resp, - AllParResults * parp, DealPBN * deal_list, DdTableResults * table_list, - const int number, - const int stepsize) + const int number) { #ifdef BATCHTIMES - cout << setw(8) << left << "Hand no." << + cout << setw(8) << left << "Hand no." << setw(25) << right << "Time" << "\n"; #endif + // One CalcAllTablesPBNX call for the whole file: expands to number×strains + // boards and solves them in a single parallel job (ddss large-batch shape). int filter[DDS_STRAINS] = {0, 0, 0, 0, 0}; const int strain_count = DDS_STRAINS; - for (int i = 0; i < number; i += stepsize) + if (number <= 0) + return true; + std::vector deals(static_cast(number)); + std::vector results(static_cast(number)); + for (int i = 0; i < number; i++) { - int count = (i + stepsize > number ? number - i : stepsize); - dealsp->no_of_tables = count; - for (int j = 0; j < count; j++) - strcpy(dealsp->deals[j].cards, deal_list[i+j].remainCards); + std::strncpy( + deals[static_cast(i)].cards, + deal_list[i].remainCards, + sizeof(deals[0].cards)); + deals[static_cast(i)].cards[sizeof(deals[0].cards) - 1] = '\0'; + } - timer.start(count); - const int workload = count * strain_count; - const int threads = dtest_effective_threads(options.num_threads_, workload); - const int ret = CalcAllTablesPBNN(dealsp, -1, filter, resp, parp, threads); - if (ret != RETURN_NO_FAULT) - { - cout << "loop_calc: i " << i << ", return " << ret << "\n"; - exit(0); - } - timer.end(); + timer.start(number); + const int workload = number * strain_count; + const int threads = dtest_effective_threads(options.num_threads_, workload); + const int ret = CalcAllTablesPBNX( + number, deals.data(), -1, filter, results.data(), nullptr, threads); + timer.end(); + if (ret != RETURN_NO_FAULT) + { + cout << "loop_calc: CalcAllTablesPBNX return " << ret << "\n"; + exit(0); + } #ifdef BATCHTIMES - timer.print_running(i+count, number); + timer.print_running(number, number); #endif - for (int j = 0; j < count; j++) - { - if (compare_TABLE(resp->results[j], table_list[i + j])) - continue; + for (int j = 0; j < number; j++) + { + if (compare_TABLE(results[static_cast(j)], table_list[j])) + continue; - cout << "loop_calc: i " << i << ", j " << j << ": " << - "Difference\n\n"; - print_TABLE(resp->results[j] ); - cout << "\n"; - print_TABLE(table_list[i + j]) ; - cout << "\n"; - } + cout << "loop_calc: j " << j << ": Difference\n\n"; + print_TABLE(results[static_cast(j)]); + cout << "\n"; + print_TABLE(table_list[j]); + cout << "\n"; } #ifdef BATCHTIMES diff --git a/library/tests/loop.hpp b/library/tests/loop.hpp index f65708ad..a40f3e19 100644 --- a/library/tests/loop.hpp +++ b/library/tests/loop.hpp @@ -32,15 +32,16 @@ void loop_solve( const int number, const int stepsize); -/// Calculate loop: execute calc_dd_table for multiple deals. +/// Calculate loop: CalcAllTablesPBNX for the full deal list in one parallel job. +/// Allocates its own flat deal/result buffers (unbounded X API); no legacy +/// DdTableDealsPBN / DdTablesRes batch structs. +/// @param deal_list Input deals in PBN format +/// @param table_list Expected DD table results +/// @param number Number of deals in the test set bool loop_calc( - DdTableDealsPBN * dealsp, - DdTablesRes * resp, - AllParResults * parp, DealPBN * deal_list, DdTableResults * table_list, - const int number, - const int stepsize); + const int number); /// PAR loop: calculate PAR scores for multiple deals. bool loop_par( diff --git a/library/tests/system/BUILD.bazel b/library/tests/system/BUILD.bazel index aa1f52a3..09ec0079 100644 --- a/library/tests/system/BUILD.bazel +++ b/library/tests/system/BUILD.bazel @@ -85,6 +85,17 @@ cc_test( ], ) +cc_test( + name = "calc_all_tables_x_test", + size = "medium", + srcs = ["calc_all_tables_x_test.cpp"], + deps = [ + "//library/src:testable_dds", + "//library/src/api:api_definitions", + "@googletest//:gtest_main", + ], +) + cc_test( name = "worker_context_reuse_test", size = "small", diff --git a/library/tests/system/calc_all_tables_x_test.cpp b/library/tests/system/calc_all_tables_x_test.cpp new file mode 100644 index 00000000..8c9e4b36 --- /dev/null +++ b/library/tests/system/calc_all_tables_x_test.cpp @@ -0,0 +1,267 @@ +/// @file calc_all_tables_x_test.cpp +/// @brief Tests for unbounded CalcAllTablesX / CalcAllTablesPBNX. +/// +/// The unbounded APIs must accept more than MAXNOOFTABLES deals and run all +/// deal×strain boards as a single parallel job (ddss-style), not as repeated +/// MAXNOOFTABLES-sized chunks. + +#include +#include +#include + +#include +#include +#include +#include + +namespace +{ + +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; +} + +// strncpy does not guarantee NUL-termination when src length >= dest size. +void copy_pbn_cards(char* dest, const std::size_t dest_size, const char* src) +{ + std::strncpy(dest, src, dest_size - 1); + dest[dest_size - 1] = '\0'; +} + +} // namespace + +TEST(CalcAllTablesX, NullPointersReturnUnknownFault) +{ + InitializeStaticMemory(); + const DdTableDeal known = make_known_deal(); + DdTableDeal deal = known; + DdTableResults result{}; + int filter[DDS_STRAINS] = {0, 0, 0, 0, 0}; + + EXPECT_EQ( + CalcAllTablesX(1, nullptr, -1, filter, &result, nullptr, 1), + RETURN_UNKNOWN_FAULT); + EXPECT_EQ( + CalcAllTablesX(1, &deal, -1, filter, nullptr, nullptr, 1), + RETURN_UNKNOWN_FAULT); + EXPECT_EQ( + CalcAllTablesX(1, &deal, -1, nullptr, &result, nullptr, 1), + RETURN_UNKNOWN_FAULT); + // mode in [0, 3] with all strains included requests par output. + EXPECT_EQ( + CalcAllTablesX(1, &deal, /*mode=*/0, filter, &result, nullptr, 1), + RETURN_UNKNOWN_FAULT); +} + +TEST(CalcAllTablesPBNX, NullPointersReturnUnknownFault) +{ + // Also documents the C ABI contract: heap allocation failure (or any throw) + // inside CalcAllTablesPBNX must surface as RETURN_UNKNOWN_FAULT, never as an + // exception crossing the C boundary. + InitializeStaticMemory(); + DdTableDealPBN deal{}; + copy_pbn_cards( + deal.cards, + sizeof(deal.cards), + "N:QJ6.K652.J85.T98 873.J97.AT764.Q4 K5.T83.KQ9.A7652 AT942.AQ4.32.KJ3"); + DdTableResults result{}; + int filter[DDS_STRAINS] = {0, 0, 0, 0, 0}; + + EXPECT_EQ( + CalcAllTablesPBNX(1, nullptr, -1, filter, &result, nullptr, 1), + RETURN_UNKNOWN_FAULT); + EXPECT_EQ( + CalcAllTablesPBNX(1, &deal, -1, filter, nullptr, nullptr, 1), + RETURN_UNKNOWN_FAULT); + EXPECT_EQ( + CalcAllTablesPBNX(1, &deal, -1, nullptr, &result, nullptr, 1), + RETURN_UNKNOWN_FAULT); +} + +TEST(CalcAllTablesPBNX, InvalidPbnReturnsPbnFault) +{ + InitializeStaticMemory(); + DdTableDealPBN deal{}; + // Must not start with N/E/S/W (case-insensitive) or convert_from_pbn may + // attempt a partial parse and fail later with a different code. + copy_pbn_cards(deal.cards, sizeof(deal.cards), "ZZZ:not-a-deal"); + DdTableResults result{}; + int filter[DDS_STRAINS] = {0, 0, 0, 0, 0}; + + EXPECT_EQ( + CalcAllTablesPBNX(1, &deal, -1, filter, &result, nullptr, 1), + RETURN_PBN_FAULT); +} + +TEST(CalcAllTablesX, LegacyRejectsMoreThanMaxTables) +{ + InitializeStaticMemory(); + DdTableDeals deals{}; + deals.no_of_tables = MAXNOOFTABLES + 1; + const DdTableDeal known = make_known_deal(); + // Fill only the in-bounds slots; CalcAllTablesN must reject on count alone. + for (int i = 0; i < MAXNOOFTABLES; i++) + deals.deals[i] = known; + + int filter[DDS_STRAINS] = {0, 0, 0, 0, 0}; + DdTablesRes results{}; + AllParResults par{}; + EXPECT_EQ( + CalcAllTablesN(&deals, -1, filter, &results, &par, /*maxThreads=*/1), + RETURN_TOO_MANY_TABLES); +} + +TEST(CalcAllTablesX, AcceptsMoreThanMaxTablesAndMatchesLegacy) +{ + InitializeStaticMemory(); + const DdTableDeal known = make_known_deal(); + // One strain keeps the >MAXNOOFTABLES path cheap enough for TSAN CI. + int filter[DDS_STRAINS] = {1, 1, 1, 1, 0}; + + DdTableDeals legacy{}; + legacy.no_of_tables = 2; + legacy.deals[0] = known; + legacy.deals[1] = known; + DdTablesRes legacy_results{}; + AllParResults par{}; + ASSERT_EQ( + CalcAllTablesN(&legacy, -1, filter, &legacy_results, &par, 1), + RETURN_NO_FAULT); + + constexpr int kNum = MAXNOOFTABLES + 1; + std::vector deals(static_cast(kNum), known); + std::vector results(static_cast(kNum)); + ASSERT_EQ( + CalcAllTablesX( + kNum, deals.data(), -1, filter, results.data(), nullptr, 1), + RETURN_NO_FAULT); + + expect_tables_equal(legacy_results.results[0], results[0]); + expect_tables_equal(legacy_results.results[0], results[static_cast(kNum - 1)]); +} + +// Single-worker path must not pay for hardest-first fanout+sort (mirrors +// calc_all_boards_n). Multi-worker still estimates every board once. +TEST(CalcAllTablesX, SingleWorkerSkipsFanoutSort) +{ + InitializeStaticMemory(); + const DdTableDeal known = make_known_deal(); + constexpr int kNum = MAXNOOFTABLES + 1; + constexpr int kIncludedStrains = 1; + int filter[DDS_STRAINS] = {1, 1, 1, 1, 0}; + const int expected_boards = kNum * kIncludedStrains; + + std::vector deals(static_cast(kNum), known); + std::vector results(static_cast(kNum)); + + const int before_single = dds::internal::deal_fanout_call_count(); + ASSERT_EQ( + CalcAllTablesX( + kNum, deals.data(), -1, filter, results.data(), nullptr, + /*maxThreads=*/1), + RETURN_NO_FAULT); + EXPECT_EQ(dds::internal::deal_fanout_call_count(), before_single) + << "single-worker CalcAllTablesX must skip deal_fanout/sort"; + + const int before_multi = dds::internal::deal_fanout_call_count(); + ASSERT_EQ( + CalcAllTablesX( + kNum, deals.data(), -1, filter, results.data(), nullptr, + /*maxThreads=*/2), + RETURN_NO_FAULT); + EXPECT_EQ( + dds::internal::deal_fanout_call_count() - before_multi, + expected_boards) + << "multi-worker CalcAllTablesX must fanout-sort every board once"; +} + +// The performance point of the X APIs: a batch larger than MAXNOOFTABLES must +// be one parallel_all_boards_n job covering every deal×strain board, not N +// chunked jobs of MAXNOOFBOARDS each. +TEST(CalcAllTablesX, LargeBatchIsSingleParallelJob) +{ + InitializeStaticMemory(); + const DdTableDeal known = make_known_deal(); + constexpr int kNum = MAXNOOFTABLES + 1; + // One strain: still >MAXNOOFTABLES boards, without a 5× TSAN timeout. + constexpr int kIncludedStrains = 1; + int filter[DDS_STRAINS] = {1, 1, 1, 1, 0}; + const int expected_boards = kNum * kIncludedStrains; + + std::vector deals(static_cast(kNum), known); + std::vector results(static_cast(kNum)); + + (void)dds::internal::parallel_boards_last_job_board_count(); + ASSERT_EQ( + CalcAllTablesX( + kNum, deals.data(), -1, filter, results.data(), nullptr, + /*maxThreads=*/2), + RETURN_NO_FAULT); + + EXPECT_EQ( + dds::internal::parallel_boards_last_job_board_count(), + expected_boards) + << "unbounded calc must dispatch all boards in one parallel job"; +} + +TEST(CalcAllTablesX, PbnVariantMatchesBinary) +{ + InitializeStaticMemory(); + const DdTableDeal known = make_known_deal(); + constexpr int kNum = 3; + std::vector binary(static_cast(kNum), known); + std::vector binary_results(static_cast(kNum)); + int filter[DDS_STRAINS] = {0, 0, 0, 0, 0}; + ASSERT_EQ( + CalcAllTablesX( + kNum, binary.data(), -1, filter, binary_results.data(), nullptr, 1), + RETURN_NO_FAULT); + + // PBN for the known deal (examples/hands.cpp hand 0). + const char* pbn = + "N:QJ6.K652.J85.T98 873.J97.AT764.Q4 K5.T83.KQ9.A7652 AT942.AQ4.32.KJ3"; + std::vector pbn_deals(static_cast(kNum)); + for (int i = 0; i < kNum; i++) + { + copy_pbn_cards( + pbn_deals[static_cast(i)].cards, + sizeof(pbn_deals[0].cards), + pbn); + } + std::vector pbn_results(static_cast(kNum)); + ASSERT_EQ( + CalcAllTablesPBNX( + kNum, pbn_deals.data(), -1, filter, pbn_results.data(), nullptr, 1), + RETURN_NO_FAULT); + + for (int i = 0; i < kNum; i++) + expect_tables_equal( + binary_results[static_cast(i)], + pbn_results[static_cast(i)]); +} diff --git a/library/tests/system/parallel_boards_test.cpp b/library/tests/system/parallel_boards_test.cpp index 8254cd08..6c88a5a8 100644 --- a/library/tests/system/parallel_boards_test.cpp +++ b/library/tests/system/parallel_boards_test.cpp @@ -432,6 +432,21 @@ TEST(ParallelAllBoards, ReusesWorkerThreadsAcrossConsecutiveCalls) EXPECT_EQ(created_after_reuse, created_after_warm); } +TEST(ParallelAllBoards, LastJobBoardCountReflectsZeroBoardCall) +{ + // The test seam must record the most recent call's board count, including + // early-return paths (count <= 0), so a prior job cannot leave a stale value. + const auto noop = [](const int, const int) { return RETURN_NO_FAULT; }; + + ASSERT_EQ(parallel_all_boards_n(4, 1, noop), RETURN_NO_FAULT); + ASSERT_EQ(dds::internal::parallel_boards_last_job_board_count(), 4); + + ASSERT_EQ(parallel_all_boards_n(0, 1, noop), RETURN_NO_FAULT); + EXPECT_EQ(dds::internal::parallel_boards_last_job_board_count(), 0); + + ASSERT_EQ(parallel_all_boards_n(-3, 1, noop), RETURN_NO_FAULT); + EXPECT_EQ(dds::internal::parallel_boards_last_job_board_count(), -3); +} TEST(ParallelAllBoards, ShutdownJoinsPoolAndAllowsRecreation) { diff --git a/library/tests/testcommon.cpp b/library/tests/testcommon.cpp index 7d03b242..f28bbb3b 100644 --- a/library/tests/testcommon.cpp +++ b/library/tests/testcommon.cpp @@ -109,9 +109,6 @@ int real_main([[maybe_unused]] int argc, [[maybe_unused]] char * argv[]) BoardsPBN bop; SolvedBoards solvedbdp; - DdTableDealsPBN dealsp; - DdTablesRes resp; - AllParResults parp; PlayTracesPBN playsp; SolvedPlays solvedplp; @@ -121,8 +118,7 @@ int real_main([[maybe_unused]] int argc, [[maybe_unused]] char * argv[]) } else if (options.solver_ == Solver::DTEST_SOLVER_CALC) { - loop_calc(&dealsp, &resp, &parp, deal_list, table_list, - number, stepsize); + loop_calc(deal_list, table_list, number); } else if (options.solver_ == Solver::DTEST_SOLVER_PLAY) {