/** Copyright (c) Scott Gasch Module Name: nnueaccum.c Abstract: Real NNUE accumulator maintenance: the embedding-table storage and the three operations (add one feature's row, remove one feature's row, recompute an entire perspective from scratch) that move.c's MakeMove/UnmakeMove hook into LiftPiece/PlacePiece/SlidePiece/ SlidePawn/SlidePieceWithoutSigs use to keep SEARCHER_THREAD_CONTEXT.sNnueAccum[2] incrementally up to date. Hot path -- called from the piece-movement primitives, which run millions of times/sec in a real search. Kept branch-light: Add/ Remove are a single feature-index computation (chess.h's NnueFeatureIndex, shared with nnuefeatures.c's reference dump) and a straight-line 256-float add/subtract loop, written so -O3 -ffast-math (this project's release profile) has the best shot at auto-vectorizing it on this box's confirmed AVX2+FMA target. Explicit SIMD intrinsics are an explicitly deferred follow-up (see docs/NNUE.md) once there's a correct baseline to profile against, not something to reach for pre-emptively here. Author: Scott Gasch (scott.gasch@gmail.com) 15 Oct 2026 **/ #include "chess.h" // Quantized embedding table -- see chess.h's extern declaration and // its own comment. Zero-initialized (C's static/global default) until // NnueLoadWeights reads a real exported checkpoint. SHORT g_rgfNnueWeight[NNUE_DIMENSIONS][NNUE_ACCUMULATOR_SIZE]; SHORT g_rgfNnueBias[NNUE_ACCUMULATOR_SIZE]; float g_fNnueFtScale = 1.0f; void NnueAccumAddFeature(NNUE_ACCUMULATOR *pAccum, ULONG uPerspective, COOR cKingOwn, COOR cPieceSquare, PIECE pPiece) /** Routine description: Adds one feature's weight row into *pAccum (in place). Hot path. **/ { int idx = NnueFeatureIndex(uPerspective, cKingOwn, cPieceSquare, pPiece); const SHORT *pRow = g_rgfNnueWeight[idx]; ULONG i; for (i = 0; i < NNUE_ACCUMULATOR_SIZE; i++) { pAccum->rgiValue[i] += pRow[i]; } } void NnueAccumRemoveFeature(NNUE_ACCUMULATOR *pAccum, ULONG uPerspective, COOR cKingOwn, COOR cPieceSquare, PIECE pPiece) /** Routine description: Subtracts one feature's weight row from *pAccum (in place). Hot path -- the exact inverse of NnueAccumAddFeature, so that applying Add then Remove (or Remove then Add) for the same arguments is an exact no-op (integer arithmetic, no float rounding to worry about). **/ { int idx = NnueFeatureIndex(uPerspective, cKingOwn, cPieceSquare, pPiece); const SHORT *pRow = g_rgfNnueWeight[idx]; ULONG i; for (i = 0; i < NNUE_ACCUMULATOR_SIZE; i++) { pAccum->rgiValue[i] -= pRow[i]; } } void NnueAccumRecompute(NNUE_ACCUMULATOR *pAccum, POSITION *pos, ULONG uPerspective) /** Routine description: Fully recomputes *pAccum from scratch against the CURRENT board in *pos, for perspective uPerspective -- the only correct way to update a perspective after its OWN king moves (every feature's king-bucket/orientation changes, not just the king's own feature; see chess.h's NNUE_ACCUMULATOR comment). Not hot in the same sense as Add/Remove -- only called on king moves, a small fraction of all moves in a tree -- so this walks the whole board rather than needing a piece list. Parameters: NNUE_ACCUMULATOR *pAccum : accumulator to overwrite POSITION *pos : current board (already reflects the move) ULONG uPerspective : which color's accumulator this is **/ { COOR cKing = pos->cKingSq[uPerspective]; int f, r; ULONG i; ASSERT(IS_ON_BOARD(cKing)); ASSERT(IS_KING(pos->rgSquare[cKing].pPiece)); ASSERT(PIECE_COLOR(pos->rgSquare[cKing].pPiece) == uPerspective); for (i = 0; i < NNUE_ACCUMULATOR_SIZE; i++) { pAccum->rgiValue[i] = g_rgfNnueBias[i]; } // 0x88 COORs aren't a simple monotonic range (A1=0x70 > H8=0x07), // so walk file/rank explicitly -- same idiom nnuefeatures.c's // ActiveFeatureIndices already uses. for (f = 0; f < 8; f++) { for (r = 1; r <= 8; r++) { COOR c = FILE_RANK_TO_COOR(f, r); PIECE p = pos->rgSquare[c].pPiece; if (IS_EMPTY(p)) { continue; } NnueAccumAddFeature(pAccum, uPerspective, cKing, c, p); } } }