summaryrefslogtreecommitdiff
path: root/src/movesup.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-04 16:46:27 -0700
committerScott Gasch <[email protected]>2026-09-04 16:46:27 -0700
commit92fc41226f784b251f41eab7e75c13075e980a54 (patch)
tree201d61c44dea1925b30c1d78ce2a3ac9d98e688e /src/movesup.c
parent550ea81a5a2ee2561c3feb91dc55686f4d3cb872 (diff)
Land bitboard move generation (Part A+B) and movesup.c bitboard queries; default on
Implements the full board_representation/MOVEGEN_MIGRATION.md scope: bitboard-backed generators for all six not-in-check piece types plus the JumpTable-avoiding whole-node dispatch fork (_GenerateAllMovesBB), the in-check escape path (king flight + block/capture), and movesup.c's ExposesCheck/FasterExposesCheck/ExposesCheckEp/IsAttacked/ InCheck bitboard equivalents. Nine toggles total (GENERATE_{KNIGHT,KING,ROOK,BISHOP,QUEEN,PAWN}_BITBOARD, GENERATE_ESCAPES_{KING,BLOCK}_BITBOARD, EXPOSESCHECK_BITBOARD, ISATTACKED_BITBOARD), all now on by default in GNUmakefile -- DISABLE_BITBOARD_MOVEGEN=1 opts back into the mailbox path, which remains fully present and compiled either way. Correctness verified via perft (Kiwipete, Position 4), the move-set comparison harness across 20,000 random positions, all nine toggles combined cleanly (15/15 runs, after fixing a GenerateRandomLegalPosition en-passant-sentinel bug in the test harness), and sd10 on all three curated suites showing zero solve-count regression vs head_reference (the ecm_hard_quick delta traced to unrelated intervening commits). Speed: most individual generators land near parity by design (mailbox's per-square walk was already close to O(destination count)); the real, consistent wins are the dispatch-layer fork (up to 23% in dense positions) and IsAttackedBB (0.73x-0.93x of mailbox). Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AbHkVrm5KUyzLwWd3GHmo6
Diffstat (limited to 'src/movesup.c')
-rwxr-xr-xsrc/movesup.c399
1 files changed, 397 insertions, 2 deletions
diff --git a/src/movesup.c b/src/movesup.c
index 33deae2..22bf0c1 100755
--- a/src/movesup.c
+++ b/src/movesup.c
@@ -23,7 +23,324 @@ Revision History:
#include "chess.h"
-COOR
+// Unlike GetAttacks (a real asm symbol in a different file, so
+// chess.h's macro-swap never touches its own definition),
+// ExposesCheck/FasterExposesCheck/ExposesCheckEp's mailbox
+// implementations live in *this* file, right below their BB
+// counterparts -- chess.h's #define would otherwise rename these
+// functions' own definitions too, colliding with the real
+// ExposesCheckBB/etc. symbols. #undef restores the real names for
+// this file's own definitions; every other translation unit that
+// includes chess.h still sees the macro-renamed calls.
+#if defined(EXPOSESCHECK_BITBOARD)
+#undef ExposesCheck
+#undef FasterExposesCheck
+#undef ExposesCheckEp
+#endif
+#if defined(ISATTACKED_BITBOARD)
+#undef IsAttacked
+#undef InCheck
+#endif
+
+// board_representation/MOVEGEN_MIGRATION.md section 6b: bitboard
+// equivalents of ExposesCheck/FasterExposesCheck/ExposesCheckEp.
+// ExposesCheck is called from MakeMove (move.c) on essentially every
+// move actually played during search -- the pin-legality safety net
+// every generator's header comment references -- making it a much
+// hotter target than anything in generate.c's own Part A/B work.
+//
+// Design: keep the mailbox version's own O(1) alignment pre-check
+// (CHECK_VECTOR_WITH_INDEX -- already a table lookup, nothing to
+// improve) to bail out on the common "not even aligned" case before
+// paying for any bitboard work at all. Only when aligned: exclude the
+// hypothetically-removed square(s) from occupancy, magic-lookup the
+// attack set from cLocation against that occupancy, and mask to the
+// single ray through cRemove via g_RookRayToEdge/g_BishopRayToEdge
+// (so an unrelated attacker on a *different* ray through cLocation
+// can't falsely register) -- then apply the same enemy-color/
+// piece-type check the mailbox version does on whatever single square
+// survives.
+// Finds the single nearest occupied square along one specific
+// direction from c, or ILLEGAL_COOR if that ray is empty all the way
+// to the edge. This is *not* "magic attack bitboard ANDed with a ray
+// mask" -- that was tried first and is wrong: an unblocked ray's magic
+// attack set contains every empty square out to the edge, and
+// FastFirstBit on that intersection picks the lowest square index
+// overall, which is not necessarily the square nearest c (bit index
+// order and "distance from c" only coincide for one of the two
+// possible directions along any given ray). The correct technique,
+// mirrored exactly from _WhoAttacksSquareBB (see.c): isolate the
+// lowest set bit for a "positive" direction (bb & -bb, no bit-scan
+// needed) or the highest set bit for a "negative" direction
+// (1ULL << (FastLastBit-1)) -- g_RookRayPositiveDir/
+// g_BishopRayPositiveDir already record which is which per direction.
+static COOR
+_NearestBlockerAlongRayBB(IN COOR c, IN int iDeltaFromC,
+ IN BITBOARD bbOccupied)
+/**
+
+Routine description:
+
+ Nearest occupied square from c along the single queen-direction
+ iDeltaFromC, or ILLEGAL_COOR if none.
+
+Parameters:
+
+ COOR c
+ int iDeltaFromC : one of g_iQKDeltas's 8 values
+ BITBOARD bbOccupied
+
+Return value:
+
+ COOR
+
+**/
+{
+ BITBOARD bbRay;
+ BITBOARD bbBlockers;
+ BITBOARD bbBlockerBit;
+ FLAG fPositiveDir;
+ ULONG uBitIndex;
+
+ switch (iDeltaFromC)
+ {
+ case 16:
+ bbRay = g_RookRayToEdge[0][c];
+ fPositiveDir = g_RookRayPositiveDir[0];
+ break;
+ case -16:
+ bbRay = g_RookRayToEdge[1][c];
+ fPositiveDir = g_RookRayPositiveDir[1];
+ break;
+ case 1:
+ bbRay = g_RookRayToEdge[2][c];
+ fPositiveDir = g_RookRayPositiveDir[2];
+ break;
+ case -1:
+ bbRay = g_RookRayToEdge[3][c];
+ fPositiveDir = g_RookRayPositiveDir[3];
+ break;
+ case 17:
+ bbRay = g_BishopRayToEdge[0][c];
+ fPositiveDir = g_BishopRayPositiveDir[0];
+ break;
+ case -17:
+ bbRay = g_BishopRayToEdge[1][c];
+ fPositiveDir = g_BishopRayPositiveDir[1];
+ break;
+ case 15:
+ bbRay = g_BishopRayToEdge[2][c];
+ fPositiveDir = g_BishopRayPositiveDir[2];
+ break;
+ case -15:
+ bbRay = g_BishopRayToEdge[3][c];
+ fPositiveDir = g_BishopRayPositiveDir[3];
+ break;
+ default:
+ ASSERT(FALSE);
+ return(ILLEGAL_COOR);
+ }
+
+ bbBlockers = bbRay & bbOccupied;
+ if (0 == bbBlockers)
+ {
+ return(ILLEGAL_COOR);
+ }
+ bbBlockerBit = fPositiveDir ?
+ (bbBlockers & (0ULL - bbBlockers)) :
+ (1ULL << (FastLastBit(bbBlockers) - 1));
+ uBitIndex = FastFirstBit(bbBlockerBit) - 1;
+ return BIT_NUMBER_TO_COOR(uBitIndex);
+}
+
+// Shared tail: given a candidate blocker square, apply the same
+// enemy-color / piece-type-can-reach-us check the mailbox versions do,
+// and return it (or ILLEGAL_COOR).
+static COOR
+_ValidateExposedBlockerBB(IN POSITION *pos, IN COOR cBlocker,
+ IN COOR cLocation)
+{
+ PIECE xPiece;
+ int iIndex;
+
+ if (!IS_ON_BOARD(cBlocker))
+ {
+ return(ILLEGAL_COOR);
+ }
+ xPiece = pos->rgSquare[cBlocker].pPiece;
+ ASSERT(!IS_EMPTY(xPiece));
+
+ if (OPPOSITE_COLORS(xPiece, pos->rgSquare[cLocation].pPiece))
+ {
+ iIndex = (int)cBlocker - (int)cLocation;
+ if (0 != (CHECK_VECTOR_WITH_INDEX(iIndex, GET_COLOR(xPiece)) &
+ (1 << (PIECE_TYPE(xPiece)))))
+ {
+ return(cBlocker);
+ }
+ }
+ return(ILLEGAL_COOR);
+}
+
+COOR
+FasterExposesCheckBB(IN POSITION *pos,
+ IN COOR cRemove,
+ IN COOR cLocation)
+/**
+
+Routine description:
+
+ Bitboard equivalent of FasterExposesCheck -- see that function's
+ comment. Caller already knows exposure is geometrically possible
+ (skips the alignment pre-check FasterExposesCheck also skips).
+
+Parameters:
+
+ POSITION *pos,
+ COOR cRemove,
+ COOR cLocation
+
+Return value:
+
+ COOR
+
+**/
+{
+ int iIndex = (int)cLocation - (int)cRemove;
+ int iDelta;
+ BITBOARD bbOccupiedWithoutRemove;
+ COOR cBlocker;
+
+ ASSERT(IS_KING(pos->rgSquare[cLocation].pPiece));
+ ASSERT(IS_ON_BOARD(cRemove));
+ ASSERT(IS_ON_BOARD(cLocation));
+ ASSERT(!IS_EMPTY(pos->rgSquare[cLocation].pPiece));
+ ASSERT(0 != (CHECK_VECTOR_WITH_INDEX(iIndex, BLACK) & (1 << QUEEN)));
+
+ iDelta = CHECK_DELTA_WITH_INDEX(iIndex);
+ ASSERT(iDelta != 0);
+
+ bbOccupiedWithoutRemove = _BuildFullOccupiedBB(pos) & ~COOR_TO_BB(cRemove);
+ cBlocker = _NearestBlockerAlongRayBB(cLocation, iDelta,
+ bbOccupiedWithoutRemove);
+ return _ValidateExposedBlockerBB(pos, cBlocker, cLocation);
+}
+
+COOR
+ExposesCheckBB(IN POSITION *pos,
+ IN COOR cRemove,
+ IN COOR cLocation)
+/**
+
+Routine description:
+
+ Bitboard equivalent of ExposesCheck -- see that function's comment.
+
+Parameters:
+
+ POSITION *pos : the board
+ COOR cRemove : the square where a piece hypothetically removed from
+ COOR cLocation : the square where the attackee is sitting
+
+Return value:
+
+ COOR : the location of an attacker piece or 0x88 (!IS_ON_BOARD) if
+ the removal of cRemove does not expose check.
+
+**/
+{
+ int iIndex = (int)cLocation - (int)cRemove;
+ int iDelta;
+ BITBOARD bbOccupiedWithoutRemove;
+ COOR cBlocker;
+
+ ASSERT(IS_ON_BOARD(cRemove));
+ ASSERT(IS_ON_BOARD(cLocation));
+ ASSERT(!IS_EMPTY(pos->rgSquare[cLocation].pPiece));
+
+ //
+ // If there is no way for a queen sitting at the square removed to
+ // reach the square we are testing (i.e. the two squares are not
+ // on the same rank, file, or diagonal) then there is no way
+ // removing it can expose cLocation to check.
+ //
+ if (0 == (CHECK_VECTOR_WITH_INDEX(iIndex, BLACK) & (1 << QUEEN)))
+ {
+ return(ILLEGAL_COOR);
+ }
+ iDelta = CHECK_DELTA_WITH_INDEX(iIndex);
+
+ bbOccupiedWithoutRemove = _BuildFullOccupiedBB(pos) & ~COOR_TO_BB(cRemove);
+ cBlocker = _NearestBlockerAlongRayBB(cLocation, iDelta,
+ bbOccupiedWithoutRemove);
+ return _ValidateExposedBlockerBB(pos, cBlocker, cLocation);
+}
+
+COOR
+ExposesCheckEpBB(IN POSITION *pos,
+ IN COOR cTest,
+ IN COOR cIgnore,
+ IN COOR cBlock,
+ IN COOR cKing)
+/**
+
+Routine description:
+
+ Bitboard equivalent of ExposesCheckEp -- see that function's
+ comment. Two squares are excluded from occupancy (cTest, the
+ captured pawn; cIgnore, the capturing pawn's origin) and one is
+ forced occupied (cBlock, the capturing pawn's destination) --
+ matching the mailbox version's own "pretend the en passant capture
+ already happened" bookkeeping.
+
+Parameters:
+
+ POSITION *pos,
+ COOR cTest : the square the attack would come from
+ COOR cIgnore : ignore this square, the pawn moved
+ COOR cBlock : this square is where the pawn moved to and now blocks
+ COOR cKing : the square under attack
+
+Return value:
+
+ COOR
+
+**/
+{
+ int iIndex = (int)cKing - (int)cTest;
+ int iDelta;
+ BITBOARD bbOccupied;
+ COOR cBlocker;
+
+ ASSERT(IS_ON_BOARD(cTest));
+ ASSERT(IS_ON_BOARD(cIgnore));
+ ASSERT(IS_ON_BOARD(cBlock));
+ ASSERT(IS_ON_BOARD(cKing));
+
+ if (0 == (CHECK_VECTOR_WITH_INDEX(iIndex, BLACK) & (1 << QUEEN)))
+ {
+ return(ILLEGAL_COOR);
+ }
+ iDelta = CHECK_DELTA_WITH_INDEX(iIndex);
+
+ bbOccupied = (_BuildFullOccupiedBB(pos) &
+ ~COOR_TO_BB(cTest) & ~COOR_TO_BB(cIgnore)) |
+ COOR_TO_BB(cBlock);
+ cBlocker = _NearestBlockerAlongRayBB(cKing, iDelta, bbOccupied);
+
+ // cBlock is forced-occupied above purely to stop the ray there --
+ // it may not really hold a piece yet (pre-move state), so it must
+ // be recognized and treated as "safe" directly, not run through
+ // pos->rgSquare's real (stale) contents, exactly like the mailbox
+ // version's explicit early check.
+ if (cBlocker == cBlock)
+ {
+ return(ILLEGAL_COOR);
+ }
+ return _ValidateExposedBlockerBB(pos, cBlocker, cKing);
+}
+
+COOR
FasterExposesCheck(POSITION *pos,
COOR cRemove,
COOR cLocation)
@@ -282,7 +599,85 @@ Return value:
}
-FLAG
+//
+// board_representation/MOVEGEN_MIGRATION.md section 6b: bitboard
+// equivalent of IsAttacked/InCheck. IsAttacked's own direct callers
+// (move.c/san.c castling-through-check legality) are cold, but InCheck
+// -- a thin wrapper around it -- has ~70 call sites across
+// search.c/move.c/eval.c/root.c/dynamic.c, called constantly through
+// search; this is a much hotter target than its direct-caller count
+// alone would suggest.
+//
+// Design: _WhoAttacksSquareBB (see.c, already exposed) already answers
+// "which of uSide's knight/bishop/rook/queen/king pieces attack this
+// square" via the same magic-table substrate as everything else in
+// this migration -- IsAttackedBB is just that function reduced to a
+// boolean, plus a separate pawn-attack check via g_PawnAttackOriginBB
+// (since _WhoAttacksSquareBB deliberately excludes pawns -- see its
+// own header comment).
+//
+FLAG
+IsAttackedBB(IN COOR cTest, IN POSITION *pos, IN ULONG uSide)
+/**
+
+Routine description:
+
+ Bitboard equivalent of IsAttacked -- see that function's comment.
+
+Parameters:
+
+ COOR cTest : the square we want to determine if is under attack
+ POSITION *pos : the board
+ ULONG uSide : the side we want to see if is attacking cTest
+
+Return value:
+
+ FLAG : TRUE if uSide attacks cTest, FALSE otherwise
+
+**/
+{
+ BITBOARD bbOccupied;
+
+ ASSERT(IS_ON_BOARD(cTest));
+ ASSERT(IS_VALID_COLOR(uSide));
+
+ bbOccupied = _BuildFullOccupiedBB(pos);
+ if (0 != _WhoAttacksSquareBB(pos, cTest, uSide, bbOccupied))
+ {
+ return(TRUE);
+ }
+ return(0 != (g_PawnAttackOriginBB[uSide][cTest] & pos->bbPawns[uSide]));
+}
+
+FLAG
+InCheckBB(IN POSITION *pos, IN ULONG uSide)
+/**
+
+Routine description:
+
+ Bitboard equivalent of InCheck -- see that function's comment.
+
+Parameters:
+
+ POSITION *pos : the board
+ ULONG uSide : the side we want to determine if is in check
+
+Return value:
+
+ FLAG : TRUE if side is in check, FALSE otherwise.
+
+**/
+{
+ COOR cKingLoc = pos->cNonPawns[uSide][0];
+
+ ASSERT(IS_VALID_COLOR(uSide));
+ ASSERT(IS_KING(pos->rgSquare[cKingLoc].pPiece));
+ ASSERT(GET_COLOR(pos->rgSquare[cKingLoc].pPiece) == uSide);
+
+ return IsAttackedBB(cKingLoc, pos, FLIP(uSide));
+}
+
+FLAG
IsAttacked(COOR cTest, POSITION *pos, ULONG uSide)
/**