1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
|
/**
Copyright (c) Scott Gasch
Module Name:
nnuefeatures.c
Abstract:
Standalone reference implementation of the HalfKAv2_hm feature
index formula, used ONLY to cross-check against the Python
reference encoder (annotate_pgns/nnue_feature_spec.py) -- see
docs/NNUE.md's "Canonical HalfKAv2_hm feature-indexing spec"
section for the formulas this file implements, sourced verbatim
from Stockfish's src/nnue/features/half_ka_v2_hm.{h,cpp} (git tag
sf_16).
This is deliberately NOT wired into search/eval -- no accumulator,
no incremental maintenance, nothing performance-sensitive. It
exists purely so `dump nnuefeatures` can print the exact same
active-feature-index set the eventual real C inference code must
produce, so a Python/C cross-checker has a C-side ground truth to
diff against before any of that real code exists. The *real*
MakeMove/UnmakeMove accumulator plumbing (per NNUE.md's
sequencing, gated behind a dedicated branch, not started) will
reuse this same index math, not this file's dump-command
plumbing.
Author:
Scott Gasch ([email protected]) 15 Oct 2026
**/
#include "chess.h"
//
// HalfKAv2_hm constants, verbatim from Stockfish's half_ka_v2_hm.h.
// PS_NB counts 5 non-king piece types x 2 "us/them" roles x 64
// squares, plus one shared 64-square king plane (own king and enemy
// king features both land in this one plane, distinguished only by
// which square is set).
//
#define NNUE_SQUARE_NB (64)
#define NNUE_PS_W_PAWN (0 * NNUE_SQUARE_NB)
#define NNUE_PS_B_PAWN (1 * NNUE_SQUARE_NB)
#define NNUE_PS_W_KNIGHT (2 * NNUE_SQUARE_NB)
#define NNUE_PS_B_KNIGHT (3 * NNUE_SQUARE_NB)
#define NNUE_PS_W_BISHOP (4 * NNUE_SQUARE_NB)
#define NNUE_PS_B_BISHOP (5 * NNUE_SQUARE_NB)
#define NNUE_PS_W_ROOK (6 * NNUE_SQUARE_NB)
#define NNUE_PS_B_ROOK (7 * NNUE_SQUARE_NB)
#define NNUE_PS_W_QUEEN (8 * NNUE_SQUARE_NB)
#define NNUE_PS_B_QUEEN (9 * NNUE_SQUARE_NB)
#define NNUE_PS_KING (10 * NNUE_SQUARE_NB)
#define NNUE_PS_NB (11 * NNUE_SQUARE_NB) // 704
#define NNUE_DIMENSIONS ((NNUE_SQUARE_NB * NNUE_PS_NB) / 2) // 22528
//
// Convert a typhoon 0x88 COOR to Stockfish's flat 0-63 square index
// (file + 8*(rank-1), rank/file both derived from the engine's own
// RANK()/FILE() macros -- see docs/NNUE.md's spec for why this
// one-line conversion is the single highest-risk spot in the whole
// scheme).
//
static int
CoorToSq64(COOR c)
{
return (int)FILE(c) + 8 * ((int)RANK(c) - 1);
}
//
// King bucket (0..31) for a king on Stockfish-square sq64, from
// color uPerspective's (WHITE/BLACK) point of view. Verified against
// Stockfish's literal KingBuckets[][] table -- see docs/NNUE.md.
//
static int
KingBucket(ULONG uPerspective, int sq64)
{
int file = sq64 & 7;
int rank0 = sq64 >> 3; // 0..7, rank-1
int fileQuartet = (file < 4) ? file : (7 - file); // 0..3, mirrored
int rankGroup = (uPerspective == WHITE) ? (7 - rank0) : rank0;
return rankGroup * 4 + fileQuartet;
}
//
// Orientation XOR mask for perspective uPerspective whose own king
// sits on Stockfish-square ksq64. Only ksq64's file selects the
// branch (queenside half a-d vs. kingside half e-h); any rank.
//
static int
OrientMask(ULONG uPerspective, int ksq64)
{
int file = ksq64 & 7;
if (uPerspective == WHITE)
{
return (file < 4) ? 7 : 0; // SQ_H1 : SQ_A1
}
return (file < 4) ? 63 : 56; // SQ_H8 : SQ_A8
}
//
// Piece-square plane offset for a piece of color uPieceColor and
// type uPieceType (chess.h's PAWN..KING values), as seen from
// perspective uPerspective. Both kings (own and enemy) share
// NNUE_PS_KING.
//
static int
PiecePlane(ULONG uPerspective, ULONG uPieceColor, ULONG uPieceType)
{
FLAG fOwn;
if (uPieceType == KING)
{
return NNUE_PS_KING;
}
fOwn = (uPieceColor == uPerspective);
switch (uPieceType)
{
case PAWN: return fOwn ? NNUE_PS_W_PAWN : NNUE_PS_B_PAWN;
case KNIGHT: return fOwn ? NNUE_PS_W_KNIGHT : NNUE_PS_B_KNIGHT;
case BISHOP: return fOwn ? NNUE_PS_W_BISHOP : NNUE_PS_B_BISHOP;
case ROOK: return fOwn ? NNUE_PS_W_ROOK : NNUE_PS_B_ROOK;
case QUEEN: return fOwn ? NNUE_PS_W_QUEEN : NNUE_PS_B_QUEEN;
default:
ASSERT(FALSE);
return 0;
}
}
static int
CompareInts(const void *a, const void *b)
{
int x = *(const int *)a;
int y = *(const int *)b;
return (x > y) - (x < y);
}
//
// Fill pIndices (caller-allocated, at least 32 ints) with the sorted
// set of active HalfKAv2_hm feature indices for perspective
// uPerspective at position pos. Returns the count (<=32).
//
static int
ActiveFeatureIndices(POSITION *pos, ULONG uPerspective, int *pIndices)
{
int f, r;
COOR cKing = ILLEGAL_COOR;
int ksq64;
int orientMask;
int kingBucketOffset;
int n = 0;
//
// Find this perspective's own king square.
//
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) &&
(PIECE_TYPE(p) == KING) &&
(PIECE_COLOR(p) == uPerspective))
{
cKing = c;
}
}
}
ASSERT(cKing != ILLEGAL_COOR);
ksq64 = CoorToSq64(cKing);
orientMask = OrientMask(uPerspective, ksq64);
kingBucketOffset = KingBucket(uPerspective, ksq64) * NNUE_PS_NB;
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;
int sq64;
int idx;
if (IS_EMPTY(p))
{
continue;
}
sq64 = CoorToSq64(c);
idx = (sq64 ^ orientMask) +
PiecePlane(uPerspective, PIECE_COLOR(p), PIECE_TYPE(p)) +
kingBucketOffset;
ASSERT((idx >= 0) && (idx < NNUE_DIMENSIONS));
ASSERT(n < 32);
pIndices[n++] = idx;
}
}
qsort(pIndices, n, sizeof(int), CompareInts);
return n;
}
void
DumpNnueFeatures(POSITION *pos)
/**
Routine description:
Prints the sorted set of active HalfKAv2_hm feature indices for
both perspectives at the current position, one line per
perspective, in a format trivial for a Python cross-checker to
parse:
NNUE_FEATURES white <idx> <idx> ...
NNUE_FEATURES black <idx> <idx> ...
Parameters:
POSITION *pos : position to dump
Return value:
void
**/
{
int rgIndices[32];
int n;
int i;
n = ActiveFeatureIndices(pos, WHITE, rgIndices);
Trace("NNUE_FEATURES white");
for (i = 0; i < n; i++)
{
Trace(" %d", rgIndices[i]);
}
Trace("\n");
n = ActiveFeatureIndices(pos, BLACK, rgIndices);
Trace("NNUE_FEATURES black");
for (i = 0; i < n; i++)
{
Trace(" %d", rgIndices[i]);
}
Trace("\n");
}
|