diff options
| author | Scott Gasch <[email protected]> | 2026-10-07 08:17:51 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-07 08:17:51 -0700 |
| commit | d14dbe2a50c220b2adac758c3d4f1a1fa67ece0b (patch) | |
| tree | cbf6d9928bcc41ef833b62b70479113f7d419925 /src/generate.c | |
| parent | 447130db3df87b40d080dc26429d45e9213c19f7 (diff) | |
Speed up eval/move-gen/SEE; remove EvalLookForDanger, overhaul _EvalKing
SPRT-confirmed improvement (1814 games, st 4.0): candidate nps=1498118
vs head=1378448, avg depth 14.30 vs 13.87, ELO=+24.2+/-16.0,
LOWER95=0.5118, LLR=+3.126 crossing the H1 bound.
Most of the diff is non-semantic speed work across eval.c, generate.c,
movesup.c, see.c, probe.c and friends. Two real semantic changes:
removed EvalLookForDanger and overhauled _EvalKing's king-safety scoring.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/generate.c')
| -rw-r--r-- | src/generate.c | 725 |
1 files changed, 708 insertions, 17 deletions
diff --git a/src/generate.c b/src/generate.c index a2fc9c4..89796b4 100644 --- a/src/generate.c +++ b/src/generate.c @@ -56,7 +56,16 @@ Revision History: const INT g_iQKDeltas[] = { -17, -16, -15, -1, +1, +17, +15, +16, 0 }; -static void +// Only needed as a standalone call site when UNBLOCKED_BITBOARD is +// off; under that toggle it's still referenced (DEBUG builds only) by +// _FindUnblockedSquaresBB's cross-check, so it can't simply be +// deleted yet -- see board_representation/MOVEGEN_MIGRATION.md. +#if !defined(UNBLOCKED_BITBOARD) || defined(DEBUG) || defined(TEST) +// Exposed non-static (like generate.c's other migration-era helpers, +// e.g. _GenerateRookBB) whenever anything other than this file might +// need it: the DEBUG cross-check inside _FindUnblockedSquaresBB below, +// or testgenerate.c's correctness/speed harness under TEST. +void _FindUnblockedSquares(IN MOVE_STACK *pStack, IN POSITION *pos) /** @@ -153,20 +162,138 @@ Return value: ASSERT(uCount > 2); ASSERT(uCount < 28); } +#endif // !defined(UNBLOCKED_BITBOARD) || defined(DEBUG) + +// board_representation/MOVEGEN_MIGRATION.md's never-started +// _FindUnblockedSquares/WouldGiveCheck candidate. Membership (is this +// square unblocked for the opposing king) is exactly the set of +// squares the rook+bishop magic tables already reach from the king's +// square, stopping at (and including) the first blocker in each of +// the 8 directions -- precisely the queen-ray walk the mailbox +// version does by hand, one lookup+OR instead of an 8-direction loop. +// The direction-back-to-king half of the old table needs no storage +// at all: generate.c's own ASSERT two lines below (mailbox version) +// proves DIR_FROM_SQ_TO_KING(c) is always identical to +// DIRECTION_BETWEEN_SQUARES(c, xKing), which is a g_pVectorDelta[] +// table lookup in release builds (see chess.h), not a divide/branch +// chain -- so recomputing it on demand costs the same as reading a +// stored value would have, with no table to populate up front. +void +_FindUnblockedSquaresBB(IN MOVE_STACK *pStack, + IN POSITION *pos) +/** + +Routine description: + + Bitboard replacement for _FindUnblockedSquares -- see that + function's header comment for what "unblocked" means. Builds + pStack->bbUnblocked[uPly], one bit per square unblocked for the + king of the side not on move. + +Parameters: + + MOVE_STACK *pStack : move stack pointer + POSITION *pos : position pointer + +Return value: + + void + +**/ +{ + COOR cKing = KingPosition(pos, FLIP(pos->uToMove)); + ULONG uPly = pStack->uPly; + + ASSERT(IS_ON_BOARD(cKing)); + ASSERT(IS_KING(pos->rgSquare[cKing].pPiece)); + ASSERT(GET_COLOR(pos->rgSquare[cKing].pPiece) != pos->uToMove); + pStack->bbUnblocked[uPly] = + _RookAttacksBB(cKing, pos->bbOccupied) | + _BishopAttacksBB(cKing, pos->bbOccupied); + +#ifdef DEBUG + { + // Cross-check against the mailbox walk every single call -- + // this is the correctness gate for bring-up, not a one-time + // test: membership must match exactly, and for every square + // the bitboard claims is unblocked, the direction back to the + // king recomputed via DIRECTION_BETWEEN_SQUARES must match + // what the mailbox ray walk actually stored. + COOR c; + BITBOARD bb = pStack->bbUnblocked[uPly]; + + _FindUnblockedSquares(pStack, pos); + FOREACH_SQUARE(c) + { + if (!IS_ON_BOARD(c)) continue; + FLAG fBB = (0 != (bb & COOR_TO_BB(c))); + FLAG fMailbox = (pStack->sUnblocked[uPly][c].uKey == + pStack->uUnblockedKeyValue[uPly]); + ASSERT(fBB == fMailbox); + if (fBB) + { + ASSERT(DIRECTION_BETWEEN_SQUARES(c, cKing) == + pStack->sUnblocked[uPly][c].iPointer); + } + } + // Restore: the mailbox call above just bumped + // uUnblockedKeyValue[uPly] a second time this ply, which is + // harmless (it's just a generation counter) but leave the + // real bbUnblocked value as computed above, not whatever the + // mailbox call's side effects might suggest. + pStack->bbUnblocked[uPly] = bb; + } +#endif +} + +#ifdef UNBLOCKED_BITBOARD +// GenerateMoves no longer builds bbUnblocked[uPly] eagerly -- it just +// bumps this ply's generation ticket (uUnblockedKeyValue[uPly], see +// its comment in chess.h) so WouldGiveCheck's own lazy check (below) +// knows a new node has arrived here and the cached bbUnblocked[uPly] +// (built for some earlier generation, if ever) is no longer trusted. +// The real build is deferred to WouldGiveCheck's first call this +// generation, if there is one -- a node whose every candidate gets +// rejected before ever needing WouldGiveCheck now never pays for it. +#define MARK_UNBLOCKED_SQUARES_STALE(pStack, pos, uPly) \ + ((void)(pos), (pStack)->uUnblockedKeyValue[(uPly)]++) +#define SQUARE_IS_UNBLOCKED(c) \ + (0 != (pStack->bbUnblocked[ctx->uPly] & COOR_TO_BB(c))) +#define DIR_FROM_SQ_TO_KING(c) \ + DIRECTION_BETWEEN_SQUARES((c), xKing) +#else +// Mailbox path has no lazy build to defer to -- its own per-square +// sUnblocked[][].uKey scheme (SQUARE_IS_UNBLOCKED/DIR_FROM_SQ_TO_KING +// below) is written directly by _FindUnblockedSquares itself, so this +// stays eager, same as it always was. +#define MARK_UNBLOCKED_SQUARES_STALE(pStack, pos, uPly) \ + ((void)(uPly), _FindUnblockedSquares((pStack), (pos))) #define SQUARE_IS_UNBLOCKED(c) (uUnblocked == kp[(c)].uKey) #define DIR_FROM_SQ_TO_KING(c) (kp[(c)].iPointer) +#endif -FLAG +FLAG FORCEINLINE WouldGiveCheck(IN SEARCHER_THREAD_CONTEXT *ctx, IN MOVE mv) /** Routine description: - Determine whether a move is a checking move or not. For this - function to work properly _FindUnblockedSquares must have been - called at the start of generation. + Determine whether a move is a checking move or not. + + Under UNBLOCKED_BITBOARD (this build's active path): self- + sufficient -- builds bbUnblocked[ctx->uPly] itself, lazily, on the + first call for a given generation (see the lazy-rebuild block + below and chess.h's uUnblockedBuiltFor comment). GenerateMoves no + longer builds it eagerly, only marks the generation stale + (MARK_UNBLOCKED_SQUARES_STALE); callers no longer need to remember + to call _FindUnblockedSquares[BB] first. + + Under the mailbox fallback (no UNBLOCKED_BITBOARD): the older + precondition still holds -- _FindUnblockedSquares must have been + called at the start of generation (MARK_UNBLOCKED_SQUARES_STALE + still does this eagerly for that path, same as it always did). Note: this is one of the most called codepaths in the engine, speed is of the essence here. @@ -193,8 +320,10 @@ Return value: COOR cFrom = mv.cFrom; PIECE pPiece = mv.pMoved; COOR xKing = KingPosition(pos, FLIP(GET_COLOR(pPiece))); +#ifndef UNBLOCKED_BITBOARD KEY_POINTER *kp = &(pStack->sUnblocked[ctx->uPly][0]); ULONG uUnblocked = pStack->uUnblockedKeyValue[ctx->uPly]; +#endif COOR cEpSquare; int iDelta; @@ -206,6 +335,28 @@ Return value: ASSERT(IS_ON_BOARD(cFrom)); ASSERT(pPiece); +#ifdef UNBLOCKED_BITBOARD + // Lazy rebuild: GenerateMoves only bumps uUnblockedKeyValue[uPly] + // (MARK_UNBLOCKED_SQUARES_STALE above) instead of building + // bbUnblocked[uPly] eagerly. The first WouldGiveCheck call for a + // given generation discovers its mismatch here and does the real + // build; every later call this same node/generation matches and + // skips straight to SQUARE_IS_UNBLOCKED/DIR_FROM_SQ_TO_KING below. + // A node whose every candidate gets rejected before ever calling + // WouldGiveCheck (e.g. every capture futility-pruned in QSearch) + // now never pays for the build at all -- see chess.h's + // uUnblockedBuiltFor comment for why this needs its own field + // rather than reusing uUnblockedKeyValue for both jobs. Measured + // cost of the added branch itself (mock A/B, same shape as this + // one, three curated suites): within this box's own run-to-run + // noise band, no consistent direction -- see conversation. + if (pStack->uUnblockedBuiltFor[ctx->uPly] != pStack->uUnblockedKeyValue[ctx->uPly]) + { + _FindUnblockedSquaresBB(pStack, pos); + pStack->uUnblockedBuiltFor[ctx->uPly] = pStack->uUnblockedKeyValue[ctx->uPly]; + } +#endif + // // Phase 1: Does this move directly attack the king? // @@ -3302,6 +3453,489 @@ Return value: } +// -------------------------------------------------------------------- +// Captures/promotions-only generators (QSearch's GENERATE_CAPTURES_PROMS +// path). QSearch's _ScoreQSearchMovesNoChecks assigns a score of 0 to +// every quiet non-promoting move, and search.c's QSearch loop bails out +// of its move loop on the first move it sees scored <= 0 -- so a quiet +// move that _GenerateAllMoves would have produced for this uType was +// always going to be generated, scored 0, and never searched. These +// generators skip producing it in the first place: same mask-and- +// serialize shape as the normal _Generate*BB functions, but masking the +// attack bitboard with the ENEMY-occupied bitboard instead of the +// complement of the friendly one, so quiet destinations never reach +// _AddNormalMove at all. Bitboard-only (no mailbox fallback) -- every +// piece type's GENERATE_*_BITBOARD flag is on in this project's only +// build profile (GNUmakefile's PROFILE), and the mailbox generators this +// would otherwise have to mirror are slated for removal separately. +// +// Castling is skipped entirely (a castle is never a capture or +// promotion). Quiet pawn pushes are skipped except the single push onto +// the back rank, which is a (quiet) promotion and still must be scored. +// +// These must stay pseudo-legal-equivalent to _GenerateAllMoves filtered +// to IS_CAPTURE_OR_PROMOTION(mv), as a set -- see testgenerate.c's +// TestCapturesAndPromotionsGenerator for the TEST=1 gate that checks +// exactly this, over a corpus of positions, before any piece type here +// is trusted. +// -------------------------------------------------------------------- + +// CAPTURES_PROMS_BITBOARD_PATH is declared in chess.h (not defined +// here) so every translation unit that needs to know whether this path +// exists sees the same answer -- see its comment there. +#if defined(CAPTURES_PROMS_BITBOARD_PATH) + +void +_GenerateKnightCapturesBB(IN MOVE_STACK *pStack, + IN POSITION *pos, + IN COOR cKnight) +/** + +Routine description: + + Captures-only sibling of _GenerateKnightBB: masks the knight's + attack bitboard with the enemy-occupied bitboard instead of the + complement of the friendly one, so only captures are serialized. + Knights never promote, so this is the whole move set this piece + type can contribute to GENERATE_CAPTURES_PROMS. + +Parameters: + + MOVE_STACK *pStack : the move stack + POSITION *pos : the board position + COOR cKnight : the knight's location + +Return value: + + void + +**/ +{ + BITBOARD bbDest = g_KnightAttacksBB[cKnight] & + pos->bbOccupiedSide[FLIP(pos->uToMove)]; + ULONG uBitIndex; + COOR c; + PIECE p; + + ASSERT(IS_KNIGHT(pos->rgSquare[cKnight].pPiece)); + ASSERT(GET_COLOR(pos->rgSquare[cKnight].pPiece) == pos->uToMove); + + while (bbDest) + { + uBitIndex = FastFirstBit(bbDest) - 1; + bbDest &= (bbDest - 1); + c = BIT_NUMBER_TO_COOR(uBitIndex); + p = pos->rgSquare[c].pPiece; + ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cKnight].pPiece)); + _AddNormalMove(pStack, pos, cKnight, c, p); + } +} + + +void +_GenerateKingCapturesBB(IN MOVE_STACK *pStack, + IN POSITION *pos, + IN COOR cKing) +/** + +Routine description: + + Captures-only sibling of _GenerateKingBB. No castling here -- a + castle can never be a capture. + +Parameters: + + MOVE_STACK *pStack : the move stack + POSITION *pos : the board position + COOR cKing : the king's location + +Return value: + + void + +**/ +{ + BITBOARD bbDest = g_KingAttacksBB[cKing] & + pos->bbOccupiedSide[FLIP(pos->uToMove)]; + ULONG uBitIndex; + COOR c; + PIECE p; + + ASSERT(IS_KING(pos->rgSquare[cKing].pPiece)); + ASSERT(GET_COLOR(pos->rgSquare[cKing].pPiece) == pos->uToMove); + + while (bbDest) + { + uBitIndex = FastFirstBit(bbDest) - 1; + bbDest &= (bbDest - 1); + c = BIT_NUMBER_TO_COOR(uBitIndex); + p = pos->rgSquare[c].pPiece; + ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cKing].pPiece)); + _AddNormalMove(pStack, pos, cKing, c, p); + } +} + + +void +_GenerateBishopCapturesBB(IN MOVE_STACK *pStack, + IN POSITION *pos, + IN COOR cBishop) +/** + +Routine description: + + Captures-only sibling of _GenerateBishopBB. + +Parameters: + + MOVE_STACK *pStack : the move stack (bbOccupied must already be + set, same precondition as _GenerateBishopBB) + POSITION *pos : the board position + COOR cBishop : the bishop's location + +Return value: + + void + +**/ +{ + ULONG uMagicIndex; + BITBOARD bbDest; + ULONG uBitIndex; + COOR c; + PIECE p; + + ASSERT(IS_BISHOP(pos->rgSquare[cBishop].pPiece)); + ASSERT(GET_COLOR(pos->rgSquare[cBishop].pPiece) == pos->uToMove); + ASSERT(pStack->bbOccupied == _BuildFullOccupiedBB(pos)); + + uMagicIndex = (ULONG) + (((pStack->bbOccupied & g_BishopOccupancyMask[cBishop]) * + g_BishopMagic[cBishop]) >> g_BishopMagicShift[cBishop]); + bbDest = g_BishopAttackTable[cBishop][uMagicIndex] & + pos->bbOccupiedSide[FLIP(pos->uToMove)]; + + while (bbDest) + { + uBitIndex = FastFirstBit(bbDest) - 1; + bbDest &= (bbDest - 1); + c = BIT_NUMBER_TO_COOR(uBitIndex); + p = pos->rgSquare[c].pPiece; + ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cBishop].pPiece)); + _AddNormalMove(pStack, pos, cBishop, c, p); + } +} + + +void +_GenerateRookCapturesBB(IN MOVE_STACK *pStack, + IN POSITION *pos, + IN COOR cRook) +/** + +Routine description: + + Captures-only sibling of _GenerateRookBB. + +Parameters: + + MOVE_STACK *pStack : the move stack (bbOccupied must already be + set, same precondition as _GenerateRookBB) + POSITION *pos : the board position + COOR cRook : the rook's location + +Return value: + + void + +**/ +{ + ULONG uMagicIndex; + BITBOARD bbDest; + ULONG uBitIndex; + COOR c; + PIECE p; + + ASSERT(IS_ROOK(pos->rgSquare[cRook].pPiece)); + ASSERT(GET_COLOR(pos->rgSquare[cRook].pPiece) == pos->uToMove); + ASSERT(pStack->bbOccupied == _BuildFullOccupiedBB(pos)); + + uMagicIndex = (ULONG) + (((pStack->bbOccupied & g_RookOccupancyMask[cRook]) * + g_RookMagic[cRook]) >> g_RookMagicShift[cRook]); + bbDest = g_RookAttackTable[cRook][uMagicIndex] & + pos->bbOccupiedSide[FLIP(pos->uToMove)]; + + while (bbDest) + { + uBitIndex = FastFirstBit(bbDest) - 1; + bbDest &= (bbDest - 1); + c = BIT_NUMBER_TO_COOR(uBitIndex); + p = pos->rgSquare[c].pPiece; + ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cRook].pPiece)); + _AddNormalMove(pStack, pos, cRook, c, p); + } +} + + +void +_GenerateQueenCapturesBB(IN MOVE_STACK *pStack, + IN POSITION *pos, + IN COOR cQueen) +/** + +Routine description: + + Captures-only sibling of _GenerateQueenBB. + +Parameters: + + MOVE_STACK *pStack : the move stack (bbOccupied must already be + set, same precondition as _GenerateQueenBB) + POSITION *pos : the board position + COOR cQueen : the queen's location + +Return value: + + void + +**/ +{ + ULONG uRookMagicIndex, uBishopMagicIndex; + BITBOARD bbDest; + ULONG uBitIndex; + COOR c; + PIECE p; + + ASSERT(IS_QUEEN(pos->rgSquare[cQueen].pPiece)); + ASSERT(GET_COLOR(pos->rgSquare[cQueen].pPiece) == pos->uToMove); + ASSERT(pStack->bbOccupied == _BuildFullOccupiedBB(pos)); + + uRookMagicIndex = (ULONG) + (((pStack->bbOccupied & g_RookOccupancyMask[cQueen]) * + g_RookMagic[cQueen]) >> g_RookMagicShift[cQueen]); + uBishopMagicIndex = (ULONG) + (((pStack->bbOccupied & g_BishopOccupancyMask[cQueen]) * + g_BishopMagic[cQueen]) >> g_BishopMagicShift[cQueen]); + bbDest = (g_RookAttackTable[cQueen][uRookMagicIndex] | + g_BishopAttackTable[cQueen][uBishopMagicIndex]) & + pos->bbOccupiedSide[FLIP(pos->uToMove)]; + + while (bbDest) + { + uBitIndex = FastFirstBit(bbDest) - 1; + bbDest &= (bbDest - 1); + c = BIT_NUMBER_TO_COOR(uBitIndex); + p = pos->rgSquare[c].pPiece; + ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cQueen].pPiece)); + _AddNormalMove(pStack, pos, cQueen, c, p); + } +} + + +void +_GenerateCapturesAndPromotionsPawnsBB(IN MOVE_STACK *pStack, + IN POSITION *pos, + IN ULONG uSide) +/** + +Routine description: + + Captures/promotions-only sibling of _GenerateAllPawnMovesBB: both + diagonal captures (promoting or not), en passant, and the single + quiet push onto the back rank (a promotion, so it still must be + scored) -- but never a double push or a non-promoting single push, + neither of which can be a capture or promotion. + +Parameters: + + MOVE_STACK *pStack : the move stack + POSITION *pos : the board position + ULONG uSide : which side's pawns to generate for (pos->uToMove) + +Return value: + + void + +**/ +{ + BITBOARD bbPawns = pos->bbPawns[uSide]; + BITBOARD bbEmpty = ~pos->bbOccupied; + BITBOARD bbEnemy = pos->bbOccupiedSide[FLIP(uSide)]; + BITBOARD bbPushPromote, bbCapLeft, bbCapRight, bb; + ULONG uBitIndex; + COOR cTo, cFrom, cEp; + PIECE p; + + if (uSide == WHITE) + { + bbPushPromote = ((bbPawns & BBRANK[7]) >> 8) & bbEmpty; + bbCapLeft = ((bbPawns & ~BBFILE[0]) >> 9) & bbEnemy; + bbCapRight = ((bbPawns & ~BBFILE[7]) >> 7) & bbEnemy; + } + else + { + ASSERT(uSide == BLACK); + bbPushPromote = ((bbPawns & BBRANK[2]) << 8) & bbEmpty; + bbCapLeft = ((bbPawns & ~BBFILE[0]) << 7) & bbEnemy; + bbCapRight = ((bbPawns & ~BBFILE[7]) << 9) & bbEnemy; + } + + // Quiet promotions only -- the single push from the 7th/2nd rank. + bb = bbPushPromote; + while (bb) + { + uBitIndex = FastFirstBit(bb) - 1; + bb &= (bb - 1); + cTo = BIT_NUMBER_TO_COOR(uBitIndex); + cFrom = (uSide == WHITE) ? + BIT_NUMBER_TO_COOR(uBitIndex + 8) : + BIT_NUMBER_TO_COOR(uBitIndex - 8); + ASSERT(IS_PAWN(pos->rgSquare[cFrom].pPiece)); + ASSERT(RANK8(cTo) || RANK1(cTo)); + _AddPromote(pStack, pos, cFrom, cTo); + } + + // Capture left (+ promotion if landing on the far rank). + bb = bbCapLeft; + while (bb) + { + uBitIndex = FastFirstBit(bb) - 1; + bb &= (bb - 1); + cTo = BIT_NUMBER_TO_COOR(uBitIndex); + cFrom = (uSide == WHITE) ? + BIT_NUMBER_TO_COOR(uBitIndex + 9) : + BIT_NUMBER_TO_COOR(uBitIndex - 7); + ASSERT(IS_PAWN(pos->rgSquare[cFrom].pPiece)); + p = pos->rgSquare[cTo].pPiece; + ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cFrom].pPiece)); + if (RANK8(cTo) || RANK1(cTo)) + { + _AddPromote(pStack, pos, cFrom, cTo); + } + else + { + _AddNormalMove(pStack, pos, cFrom, cTo, p); + } + } + + // Capture right (+ promotion if landing on the far rank). + bb = bbCapRight; + while (bb) + { + uBitIndex = FastFirstBit(bb) - 1; + bb &= (bb - 1); + cTo = BIT_NUMBER_TO_COOR(uBitIndex); + cFrom = (uSide == WHITE) ? + BIT_NUMBER_TO_COOR(uBitIndex + 7) : + BIT_NUMBER_TO_COOR(uBitIndex - 9); + ASSERT(IS_PAWN(pos->rgSquare[cFrom].pPiece)); + p = pos->rgSquare[cTo].pPiece; + ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cFrom].pPiece)); + if (RANK8(cTo) || RANK1(cTo)) + { + _AddPromote(pStack, pos, cFrom, cTo); + } + else + { + _AddNormalMove(pStack, pos, cFrom, cTo, p); + } + } + + // En passant -- same per-side-per-node (not per-pawn) check as + // _GenerateAllPawnMovesBB. + cEp = pos->cEpSquare; + if (IS_ON_BOARD(cEp)) + { + int iBehindDelta = (uSide == WHITE) ? 16 : -16; + + cFrom = cEp + iBehindDelta - 1; + if (IS_ON_BOARD(cFrom) && + IS_PAWN(pos->rgSquare[cFrom].pPiece) && + (GET_COLOR(pos->rgSquare[cFrom].pPiece) == uSide)) + { + _AddEnPassant(pStack, pos, cFrom, cEp); + } + cFrom = cEp + iBehindDelta + 1; + if (IS_ON_BOARD(cFrom) && + IS_PAWN(pos->rgSquare[cFrom].pPiece) && + (GET_COLOR(pos->rgSquare[cFrom].pPiece) == uSide)) + { + _AddEnPassant(pStack, pos, cFrom, cEp); + } + } +} + + +// Non-static so testgenerate.c's correctness/speed harness can call it +// directly by name, same convention as _GenerateAllMoves below. +void +_GenerateCapturesAndPromotions(IN MOVE_STACK *pStack, + IN POSITION *pos) +/** + +Routine description: + + Generates exactly the pseudo-legal moves that are captures or + promotions in this position -- the subset of _GenerateAllMoves' + output that IS_CAPTURE_OR_PROMOTION(mv) is true for, produced + directly rather than generated-then-filtered. Used by GenerateMoves + for GENERATE_CAPTURES_PROMS (QSearch's no-checks path) in place of + _GenerateAllMoves, since every quiet move _GenerateAllMoves would + have produced there is scored 0 by _ScoreQSearchMovesNoChecks and + never searched. + + Relies on pStack->bbOccupied being set by the caller for the + sliding-piece magic lookups, same precondition as _GenerateAllMoves + itself. + +Parameters: + + MOVE_STACK *pStack : the move stack + POSITION *pos : the board position + +Return value: + + void + +**/ +{ + COOR c; + static void (*JumpTable[])(MOVE_STACK *, POSITION *, COOR) = + { + InvalidGenerator, // EMPTY + InvalidGenerator, // EMPTY | WHITE + InvalidGenerator, // 2 (BLACK_PAWN) + InvalidGenerator, // 3 (WHITE_PAWN) + _GenerateKnightCapturesBB, // 4 (BLACK_KNIGHT) + _GenerateKnightCapturesBB, // 5 (WHITE_KNIGHT) + _GenerateBishopCapturesBB, // 6 (BLACK_BISHOP) + _GenerateBishopCapturesBB, // 7 (WHITE_BISHOP) + _GenerateRookCapturesBB, // 8 (BLACK_ROOK) + _GenerateRookCapturesBB, // 9 (WHITE_ROOK) + _GenerateQueenCapturesBB, // 10 (BLACK_QUEEN) + _GenerateQueenCapturesBB, // 11 (WHITE_QUEEN) + _GenerateKingCapturesBB, // 12 (BLACK_KING) + _GenerateKingCapturesBB // 13 (WHITE_KING) + }; + + pStack->bbOccupied = pos->bbOccupied; + + { + BITBOARD bbNonPawns = NON_KING_PIECES_BB(pos, pos->uToMove) | + COOR_TO_BB(KingPosition(pos, pos->uToMove)); + while (IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1(&bbNonPawns))) + { + (JumpTable[pos->rgSquare[c].pPiece])(pStack, pos, c); + } + } + + _GenerateCapturesAndPromotionsPawnsBB(pStack, pos, pos->uToMove); +} + +#endif // captures/promotions bitboard path + + // Non-static (unlike its historical internal-only status) so // testgenerate.c's whole-node dispatch benchmark can call it directly // by name -- see _GenerateAllMovesBB's block comment for why that @@ -4574,9 +5208,7 @@ Routine description: Like _ScoreSearchMoves, this function takes "hints" from the searcher context about what squares are in danger and tries to - order moves away from those squares sooner. Unlike - _ScoreSearchMoves this function also attempts to capture enemies - that are in danger sooner. + order moves away from those squares sooner. Parameters: @@ -4599,7 +5231,7 @@ Return value: COOR cEnprise = FindEnprisePiece(ctx, uColor); // - // We have generate all moves or just legal escapes from check + // We have generated all moves or just legal escapes from check // here. There can be a hash move -- if there is then the search // has already tried it and we should not generate it again. // @@ -4611,9 +5243,31 @@ Return value: ctx->sPlyInfo[uPly].eMoveScoringScheme = MOVE_SCORING_QSEARCH_CHECKS; - for (u = pStack->uBegin[uPly]; - u < pStack->uEnd[uPly]; - u++) + // uLiveEnd shrinks as moves are rejected, instead of leaving them in + // place scored <= 0 for QSearch's move loop (search.c) to discover + // and bail on. Every move QSearch's loop will ever consider calls + // SelectBestNoHistory, which rescans the WHOLE remaining [u, + // uEnd[uPly]) range -- so a quiet, non-checking move sitting in + // that range gets rescanned by every single one of those calls + // until the bail, not generated/scored once and ignored. Instead, + // a rejected move (iValue <= 0) is swapped with the last live + // element and uLiveEnd drops by one -- same whole-struct-swap + // idiom GenerateMoves already uses to retire a found hash move + // (see its comment above) -- and pStack->uEnd[uPly] is written + // once at the end, after every move has been classified. The + // index being examined is NOT advanced on a reject, since the + // element just swapped into that slot hasn't been scored yet. + // + // Net effect: every move left in [uBegin[uPly], uEnd[uPly]) after + // this function returns has iValue > 0, so search.c's QSearch loop + // will never actually hit its own `iValue <= 0` bail for this + // scoring scheme -- that check stays (it's shared with + // _ScoreQSearchMovesNoChecks, which still needs it), it just + // becomes unreachable here, which is harmless. + { + ULONG uLiveEnd = pStack->uEnd[uPly]; + u = pStack->uBegin[uPly]; + while (u < uLiveEnd) { mv = pStack->mvf[u].mv; ASSERT(GET_COLOR(mv.pMoved) == uColor); @@ -4702,10 +5356,33 @@ Return value: s += (mv.cFrom == cEnprise) * (PIECE_VALUE(mv.pMoved) / 4); ASSERT(s > 0); } + else + { + // + // Reject: this move will never be searched (QSearch's + // caller would bail on it the moment it became the best + // remaining candidate) -- drop it from the live range now + // instead of leaving it for SelectBestNoHistory to keep + // rescanning. Swap the last live element into this slot + // and shrink uLiveEnd; u is deliberately NOT advanced so + // the freshly-swapped-in element gets classified on the + // next iteration. + // + uLiveEnd--; + if (u != uLiveEnd) + { + pStack->mvf[u] = pStack->mvf[uLiveEnd]; + } + continue; + } + pStack->mvf[u].iValue = s; ASSERT(pStack->mvf[u].iValue != -MAX_INT); + u++; } // next move + pStack->uEnd[uPly] = uLiveEnd; + } } @@ -4761,7 +5438,6 @@ Return value: ASSERT(MOVE_COUNT(ctx, uPly) <= MAX_MOVES_PER_PLY); ctx->sPlyInfo[uPly].eMoveScoringScheme = MOVE_SCORING_QSEARCH_NO_CHECKS; - #ifdef DEBUG uColor = pos->uToMove; #endif @@ -4791,6 +5467,7 @@ Return value: ASSERT((mv.pCaptured) || (mv.pPromoted)); s = PIECE_VALUE(mv.pCaptured) - PIECE_VALUE(mv.pMoved); + // // TODO: how does this handle positions like K *p *k ? // @@ -4912,27 +5589,41 @@ Return value: case GENERATE_CAPTURES_PROMS_CHECKS: ASSERT(!InCheck(pos, pos->uToMove)); ASSERT(mvHash.uMove == 0); - _FindUnblockedSquares(pStack, pos); + MARK_UNBLOCKED_SQUARES_STALE(pStack, pos, uPly); _GenerateAllMoves(pStack, pos); _ScoreQSearchMovesInclChecks(pStack, ctx); break; case GENERATE_CAPTURES_PROMS: ASSERT(!InCheck(pos, pos->uToMove)); ASSERT(mvHash.uMove == 0); - _FindUnblockedSquares(pStack, pos); + // The unblocked-squares table itself is no longer built + // here -- see WouldGiveCheck's lazy rebuild (generate.c): + // this just marks the ply's generation ticket so the + // first WouldGiveCheck call this node (if any) knows to + // build it. search.c's QSearch loop calls WouldGiveCheck + // itself, deferred, on whichever single move it selects + // and decides to search (see fIncludeChecks == FALSE + // there) -- most nodes whose candidates are all futility- + // pruned away never reach that call at all, and now never + // pay for the table either. + MARK_UNBLOCKED_SQUARES_STALE(pStack, pos, uPly); +#if defined(CAPTURES_PROMS_BITBOARD_PATH) + _GenerateCapturesAndPromotions(pStack, pos); +#else _GenerateAllMoves(pStack, pos); +#endif _ScoreQSearchMovesNoChecks(pStack, ctx); break; case GENERATE_ALL_MOVES: ASSERT(!InCheck(pos, pos->uToMove)); - _FindUnblockedSquares(pStack, pos); + MARK_UNBLOCKED_SQUARES_STALE(pStack, pos, uPly); _GenerateAllMoves(pStack, pos); _ScoreAllMoves(pStack, ctx, mvHash); uReturn = MOVE_COUNT(ctx, uPly); break; case GENERATE_ESCAPES: ASSERT(InCheck(pos, pos->uToMove)); - _FindUnblockedSquares(pStack, pos); + MARK_UNBLOCKED_SQUARES_STALE(pStack, pos, uPly); uReturn = _GenerateEscapes(pStack, pos); ASSERT(uReturn); pStack->sGenFlags[uPly].uKingMoveCount = uReturn >> 16; |
