summaryrefslogtreecommitdiff
path: root/src/board_representation
diff options
context:
space:
mode:
Diffstat (limited to 'src/board_representation')
-rw-r--r--src/board_representation/EVAL.md242
1 files changed, 233 insertions, 9 deletions
diff --git a/src/board_representation/EVAL.md b/src/board_representation/EVAL.md
index 697ea38..210340e 100644
--- a/src/board_representation/EVAL.md
+++ b/src/board_representation/EVAL.md
@@ -1,14 +1,32 @@
# Migration plan: bitboard-backed `Eval()` (rewrite, 2026-09-04)
-**Status: planning only, superseding the earlier draft of this
-document in full.** That draft was written before the movegen
-bitboard work (`MOVEGEN_MIGRATION.md`) landed and assumed a slider
-attack-bitboard primitive ("`_RookAttacksBB(c, bbOccupied)`... already
-built") that did not actually exist yet at the time. It now does --
-this rewrite is based on having read `_EvalBishop`/`_EvalKnight`/
-`_EvalRook`/`_EvalQueen` in full (not by memory/analogy) and the
-landed `generate.c` bitboard infrastructure (real magic tables, not a
-sketch) side by side.
+**Status as of 2026-09-05: in progress, not planning-only anymore.**
+Pawns, `bbOccupied`, the piece-dispatch loop, knight, and bishop are
+landed and committed; rook, queen, and the final `_EvalKing`/
+`_WhoControlsSquareFast` cleanup remain. See "Progress log and lessons
+learned" at the end of this document for the full, current account --
+read that section first, it supersedes some of the sequencing/toggle
+assumptions below (particularly section 4's per-piece `#define`
+toggle idea, which was not what actually got used).
+
+The rest of this document below is the original rewrite plan as
+scoped before implementation started. It's kept because the technical
+reduction for each piece type (section 1b) and the retirement
+criteria (section 8) are still accurate and worth reading in full
+before touching rook or queen -- just read the progress log first for
+what's actually true about the current state of the code.
+
+---
+
+**Original status note (2026-09-04, now historical): planning only,
+superseding the earlier draft of this document in full.** That draft
+was written before the movegen bitboard work (`MOVEGEN_MIGRATION.md`)
+landed and assumed a slider attack-bitboard primitive
+("`_RookAttacksBB(c, bbOccupied)`... already built") that did not
+actually exist yet at the time. It now does -- this rewrite is based
+on having read `_EvalBishop`/`_EvalKnight`/`_EvalRook`/`_EvalQueen` in
+full (not by memory/analogy) and the landed `generate.c` bitboard
+infrastructure (real magic tables, not a sketch) side by side.
## 0. Why this rewrite exists, and what's different now
@@ -668,3 +686,209 @@ knight's likely-clean bill of health lower the bar for bishop's
two-x-ray-category case or queen's combined-ray-family case --
each has different enough special-case surface (section 1b) to
warrant its own full pass through this list.
+
+## 9. Progress log and lessons learned (2026-09-05)
+
+This section is the authoritative account of what has actually
+happened, kept up to date as work lands. Read this before touching
+rook, queen, or `_EvalKing` -- it corrects several assumptions in the
+plan above (particularly section 4's toggle strategy, which is not
+what was actually used) and records methodology worth repeating.
+
+### What's landed (commits, in order)
+
+- `57502d6` -- Pawns retire `bvAttacks` entirely. `pos->bbPawnAttacks[2]`
+ computed via shift-and-mask from `pos->bbPawns[2]` (the same
+ technique `generate.c`'s `_GenerateAllPawnMovesBB` already uses,
+ minus the enemy-occupancy mask). Two silent-regression bugs found
+ by auditing every remaining `|8` site by hand after the fact, not
+ by any test failing: `_EvalPawns`' own pawn-duo/backward-pawn
+ detection, and `_WhoControlsSquareFast`, both read `bvAttacks` at
+ points in `Eval()`'s sequence where only pawns could have written
+ it -- once pawns stopped writing there, both went silently wrong.
+ Fixed by reading `bbPawnAttacks` directly instead.
+- `2ce3570` -- `pos->bbOccupied` added, incrementally maintained by
+ `move.c`'s existing `SlidePiece`/`SlidePawn`/`LiftPiece`/`PlacePiece`
+ choke points (same sites that already maintain `bbPieces`/`bbPawns`).
+ Replaces on-demand rebuilds in `generate.c`/`movesup.c`/`see.c`;
+ dedupes two byte-for-byte-identical builder functions
+ (`_BuildFullOccupiedBB` / `_BuildOccupiedBB`) into one. A second
+ position-construction path missed on the first pass
+ (`GenerateRandomLegalPosition` in `testsup.c`, which pokes
+ `rgSquare`/`bbPieces`/`bbPawns` directly, bypassing `move.c`) never
+ set `bbOccupied`, so the self-test suite's new consistency check
+ looped forever retrying a position that could never pass -- caught
+ because the self-test spun at 100% CPU with zero progress instead of
+ crashing outright, not because anything asserted immediately.
+- `6e86450` -- `Eval()`'s piece-dispatch loop (`cNonPawns` walk +
+ `p&0x4`/`IS_KNIGHT` branch dispatch + `cDefer`/`uDefer` bookkeeping)
+ replaced with direct `pos->bbPieces[color][TYPE]` bitboard walks,
+ one type at a time, same phase order as before. Verified
+ byte-identical search node counts against the pre-change commit on
+ `tests/ecm_ringers.ep_` at `sd10` -- not just "didn't crash."
+- `7e3e6b6` -- Knight: full mobility rewrite via `g_KnightAttacksBB[c]`
+ (already existed, built for move generation) plus bitboard masks,
+ replacing the per-square delta-walk and `NMobCaseTable` switch
+ entirely. First contributor to a new `bbMinorAttacks[2]`.
+- `de3f366` -- Bishop: mobility via `_BishopAttacksBB(c,
+ pos->bbOccupied)` (the movegen magic-bitboard slider lookup) plus
+ bitboard masks, replacing the ray-walk and `BMobCaseTable`. Adds
+ `bbMinorXrayAttacks[2]`. **Ships a deliberate behavior change from
+ the old ray-walk**, not full fidelity: the old walk's
+ `fStop=FALSE` for x-ray-triggering blockers (friendly bishop/queen,
+ enemy rook/queen/king) meant it kept walking -- and kept counting
+ mobility -- through however many such blockers were stacked
+ consecutively on one ray. The bitboard version only extends one hop
+ past the first x-ray-worthy blocker. Found by a DEBUG assert on
+ `8/1R1B4/2B1r3/5k2/2P2P2/1p6/1Kb5/7n w - -` (bishop x-raying an
+ enemy rook, then continuing to x-ray *through* an enemy king right
+ behind it), and kept as-is rather than fixed with a bounded
+ chain-following loop -- explicit user call, for speed, given how
+ rare >=2 consecutive x-ray-worthy pieces on one ray is.
+
+### What's next, and how
+
+**Rook, then queen, then `_EvalKing`/`_WhoControlsSquareFast`'s final
+cleanup -- one piece at a time, each landed and verified before the
+next starts, not in one combined change.** This is a hard-learned
+constraint, not a style preference: an earlier attempt this same
+session tried to convert knight, bishop, rook, queen, and
+`_EvalKing`/`_WhoControlsSquareFast` all at once after what started as
+a scoping question ("why do rook/queen have to write into the old
+structure too?"). It produced a real bug (a byte-scale mismatch
+between the `_XRAY_BIT` whole-word constants and the standalone-byte
+value `.uXray` actually reads as) that was hard to isolate with five
+things changed simultaneously, and the whole thing was reverted back
+to a clean commit rather than debugged further. Redone one piece at a
+time afterward, each step's own bugs (the xray-scale issue again on
+bishop, then the x-ray-chain gap) were caught and fixed/decided within
+that single step, not entangled with four other pieces' changes.
+
+Concretely, for rook and then queen:
+
+- Mobility via `_RookAttacksBB(c, pos->bbOccupied)` /
+ the rook+bishop two-pass combination for queen (per section 1b,
+ reusing the already-tested "two passes beats one combined 8-ray
+ table" finding from `MOVEGEN_MIGRATION.md`), replacing
+ `RMobCaseTable`/`QMobCaseTable` and their ray-walks.
+- New `bbRookAttacks[2]`/`bbRookXrayAttacks[2]` and
+ `bbQueenAttacks[2]`/`bbQueenXrayAttacks[2]`, same lifetime/clearing
+ discipline as the existing `bbMinorAttacks`/`bbMinorXrayAttacks`
+ pair.
+- **Decide the x-ray-chain question for rook/queen explicitly, don't
+ assume bishop's answer carries over.** Bishop's single-hop
+ simplification was judged acceptable for bishops specifically;
+ rook and queen batteries (rook-behind-rook, queen-behind-rook,
+ queen-behind-bishop) may be common enough in real play that the
+ same simplification changes behavior more than bishop's did. Worth
+ a quick sanity check (how often do the ringers/confident/hard-quick
+ suites actually exercise a 2+-deep battery?) before defaulting to
+ the same one-hop cut, not a foregone conclusion either way.
+- **The transitional helpers (`_IsSquareAttackedByMinor`,
+ `_IsSquareXrayedByMinor`) are explicitly temporary and get rewritten
+ incrementally as each piece converts, not left to accumulate special
+ cases.** Once rook converts, `UNSAFE_FOR_QUEEN`'s rook-bit
+ contribution moves from a raw `bvAttacks` read to a
+ `bbRookAttacks`-based check -- likely folded into
+ `_IsSquareAttackedByMinor`'s shape generalized to also cover rook,
+ or a new sibling helper, whichever reads cleaner at the time; decide
+ when actually doing it, not in advance. Once queen also converts,
+ `_EvalKing`/`_WhoControlsSquareFast`'s `bvAttack`/`bvDefend`/`bvXray`
+ computation stops touching `rgSquare[c|8].bvAttacks` at all, and at
+ that point the helpers themselves can very likely be deleted
+ entirely in favor of plain `pos->bbXAttacks[color] & sq` reads --
+ the whole point of calling them "transitional" from the start.
+- **This culminates in removing the `c|8`/`bvAttacks`/`ATTACK_BITV`
+ mechanism from `chess.h` entirely** (the `SQUARE` union becomes a
+ plain 2-field struct, already done in shape when pawn converted;
+ what's left is the `bvAttacks[2]` member itself and the
+ `ATTACK_BITV` type), once king is the only remaining writer and its
+ three write sites (`_EvalKing`'s two paths) move to
+ `bbKingAttacks[2]` the same way every other piece type did. Not a
+ separate "big cleanup" step -- it falls out for free once queen's
+ conversion lands, since nothing will be writing the old structure
+ anymore.
+- **Keep writing real DEBUG asserts, using the still-live `rgSquare`
+ mailbox representation as independent ground truth, for as long as
+ it exists.** Both transitional helpers already do this (a from-
+ scratch mailbox ray-walk/table lookup, deliberately not sharing code
+ with the production bitboard technique being verified) -- keep this
+ pattern for rook and queen's own conversions too. This is *the*
+ reason the bishop x-ray-chain gap was caught during routine
+ `precommit_check.sh` smoke testing instead of surfacing as an
+ unexplained node-count drift days later.
+- **Verification bar changes once a piece ships a deliberate
+ behavior change.** Byte-identical node counts (the bar for pawns,
+ `bbOccupied`, the dispatch loop, and knight) stop being the right
+ test once a step *intentionally* changes behavior, like bishop's
+ x-ray simplification. In that case: full `tests/ecm_ringers.ep_` at
+ `sd10`, checking (a) solve/no-solve parity holds exactly and (b)
+ node-count deltas stay in a bounded, unsurprising range (bishop's
+ landed at roughly -17% to +20% per position, all real, all
+ attributable to the one documented rule change) rather than
+ expecting or requiring equality.
+- **After the attack-bits work is done** (rook, queen, king all
+ converted, `bvAttacks` deleted): revisit the rest of
+ `_EvalKnight`/`_EvalBishop`/`_EvalRook`/`_EvalQueen` -- the
+ non-mobility scoring terms this plan deliberately left alone
+ throughout (outpost bonuses, good/bad-bishop scoring, rook-on-open-
+ file, queen-out-early, etc.) -- with fresh eyes on whether each is
+ worth keeping as-is, rewriting, or cutting outright, not just
+ re-implementing them faster. Not scoped further than that yet; a
+ separate pass once the attack-bitboard foundation is solid under
+ all of them.
+
+### What's been learned about `Eval()` itself along the way
+
+- **The pawn hash is unambiguously worth keeping, measured, not
+ assumed.** Hit/miss cycle counts split via `EVAL_TIME` instrumentation
+ on one `sd12` benchmark position: hits average ~90 cycles, misses
+ average ~1741 cycles (~19x), 97%+ hit rate. The miss cost was never
+ mostly attack-bit population (that's its own ~1% bucket of total
+ eval cost, down from ~3.6% before pawns converted) -- it's the
+ isolated/doubled/duo/backward-pawn scoring loops, real per-pawn work
+ that recurs across sibling/ancestor search nodes often enough to be
+ well worth caching. Also: even a pawn-hash *hit* costs ~90 cycles,
+ more than "just compare a key" should cost -- the table (9MB/thread)
+ mostly isn't L1/L2-resident, so even a hit typically pays a real
+ memory round-trip. The practical takeaway: the hash's floor cost is
+ bounded by memory latency either way, so "is bitboard math faster
+ than a cache-missing read" was the wrong question to ask here --
+ the read happens regardless, and the alternative (recompute always)
+ would cost ~19x more on every single call instead of only the ~3%
+ that miss.
+- **`EVAL_TIME`'s per-term cycle breakdown (`root.c`'s printout,
+ `chess.h`'s `u64CyclesEval*` counters) is worth re-running after
+ each piece type converts**, not just once at the start. It already
+ caught two concrete, real wins directly attributable to specific
+ commits: `_PopulatePawnAttackBits`'s own bucket dropped from ~3.6%
+ to ~1.0% of total eval cost when pawns converted (same call site,
+ clean before/after comparison); the dispatch-loop rewrite produced
+ a consistent ~3-5% NPS improvement across all 11 ringers positions
+ at identical node counts. Cheap to re-check, concrete numbers
+ instead of "should be faster" intuition.
+- **This whole effort should err on the side of speed, explicitly.**
+ Direct instruction, worth stating plainly since it's a real change
+ from the original plan's framing (section 0's "byte-identical
+ exact-score harness" bar, section 5's "must be byte-identical, any
+ mismatch is a real bug"): where a bitboard reduction can either (a)
+ preserve the old ray-walk's exact behavior at the cost of real
+ complexity (a bounded chain-following loop, multi-blocker
+ bookkeeping) or (b) ship a simpler, faster, slightly different
+ approximation of the same idea, prefer (b) once the behavior delta
+ has been measured and judged small/rare enough -- don't default to
+ fidelity as the tiebreaker. This is a deliberate trade against the
+ original plan's own stated bar, made explicitly and recorded here so
+ it's not mistaken for an oversight later. Section 5's exact-score
+ harness (never built) is now unlikely to be the right verification
+ tool for future steps that ship intentional behavior changes; the
+ ringers-suite-plus-bounded-delta check above is what's actually
+ being used instead.
+- **The piece-dispatch loop (section 0's original target) turned out
+ to be a real, separate win worth landing on its own**, distinct from
+ any single piece type's mobility algorithm -- eliminating the
+ `p&0x4`/`IS_KNIGHT` branch and the `cNonPawns` mailbox re-read in
+ the outer loop was independently profitable (~3-5% NPS) and didn't
+ require any piece type's own conversion to land first, since
+ `pos->bbPieces[color][TYPE]` already existed and was simply unused
+ by `Eval()` before this.