summaryrefslogtreecommitdiff
path: root/src/generate.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-07 08:17:51 -0700
committerScott Gasch <[email protected]>2026-10-07 08:17:51 -0700
commitd14dbe2a50c220b2adac758c3d4f1a1fa67ece0b (patch)
treecbf6d9928bcc41ef833b62b70479113f7d419925 /src/generate.c
parent447130db3df87b40d080dc26429d45e9213c19f7 (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.c725
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;