# Makefile (for typhoon/GNU-style make). Recognized gmake variables: # # DEBUG=1: make debug version (includes asserts and sanity checks) # TEST=1: include self-test code in the binary produced # ASM=1: create assembly code, does not link final binary # EVAL_DUMP=1: make a version that can dump eval breakdowns # EVAL_TIME=1: make a version that counts cycles spent in eval # PERF_COUNTERS=1: make version with perf counters enabled # MOVE_ORDERING_EVIDENCE_COLLECTION=1: instrument the move-ordering # evidence-bucket harnesses (countermove/move-pair hit tracking, # CM/MP evidence-bucket tries/FH -- see MOVE_ORDERING_IDEAS.md) -- # a dedicated flag, independent of PERF_COUNTERS, since this # collection (per-move IS_SAME_MOVE checks + bucket lookups on every # quiet move scored) is its own cost, not bundled into the general # perf-counter set by default. # BOUNDS_CHECKING=1: make version with bounds checking enabled # MP=1: make version for multi-processor machine # TREEDUMP=1: dump a flat MOVE/RESULT search-tree trace to # treedump.log (see TREEDUMP_FORMAT.md) # OSX=1: build for an Intel-based Apple Mac (omit this for FreeBSD/Linux) # USE_READLINE=1: link against the GNU readline library # SIXTYFOUR=1: make an X64 binary # CROUTINES=1: use the C versions of the asm routines [slower] # GETATTACKS_BITBOARD=1: use the bbPieces/bbPawns-backed _GetAttacksBB # instead of the asm/CROUTINES GetAttacks -- see # board_representation/MIGRATION.md section 6 # DISABLE_BITBOARD_MOVEGEN=1: opt back into the mailbox move # generator/escapes/ExposesCheck/IsAttacked implementations -- the # nine GENERATE_*_BITBOARD/EXPOSESCHECK_BITBOARD/ISATTACKED_BITBOARD # toggles (board_representation/MOVEGEN_MIGRATION.md) are on by # default as of the 2026-09-04 correctness/no-regression sweep # (perft, move-set comparison harness, sd10 curated-suite parity, # match_play.py). The mailbox code is still fully present and # compiled either way -- this flag only flips which side of each # macro-swap wins, it doesn't remove anything. # GPROF=1: instrument with -pg for gprof(1)-based profiling (see # PROFILE.md) -- root-free on both FreeBSD and Linux, unlike # hardware-counter-based profiling (pmcstat/perf). Requires a # `gmake clean` first/after, since -pg changes codegen for every # translation unit. # EVERYTHING=1: everything everything everything everything # # $Id$ # CC = clang CXX = clang++ NASM = yasm # Default to a 64-bit build -- 32-bit clang isn't available in this # environment anyway (see CLAUDE.md). Still overridable in principle # (e.g. `gmake SIXTYFOUR=` won't un-set it since ?= only fires when # unset, but this matches how every build in this session has actually # been invoked). SIXTYFOUR ?= 1 ifdef OSX LIBRARIES = -pthread -lc ifdef SIXTYFOUR $(warning you will need to use yasm for now to get x64 on OSX) NASMFLAGS = -f macho64 -dOSX -d_X64_ else NASMFLAGS = -f macho -dOSX endif else LIBRARIES = -pthread -lc -lstdc++ ifdef SIXTYFOUR NASMFLAGS = -f elf64 -d__GNUC__ else NASMFLAGS = -f elf -d__GNUC__ endif endif RM = /bin/rm # # Setup commandline based on make defines # ifdef ASM ifdef SIXTYFOUR PROFILE = -S -fverbose-asm -D_X64_ else PROFILE = -S -fverbose-asm -D_X86_ endif else ifdef SIXTYFOUR PROFILE = -D_X64_ else PROFILE = -D_X86_ endif endif ifdef OSX PROFILE += -DOSX endif ifdef GENETIC PROFILE += -DGENETIC endif ifdef GPROF PROFILE += -pg endif # Scoped ISA opt-in for NNUE SIMD work (int8/int16 dot products, and # the FC head's existing float32 loops which clang already # autovectorizes to SSE only without this). Default ON since 2026-10-16 # -- confirmed a real, measured win for both classic HCE (~3%) and # (once real NNUE weights are loaded) the NNUE FC head (~7%, see # docs/NNUE.md), and the build box (Zen 2) has full-width 256-bit # AVX2 + FMA3. Pass AVX2=0 explicitly to disable (e.g. building on a # pre-Haswell/pre-Zen box, 2013 and earlier) -- unlike -march=native, # this doesn't assume the BUILD box matches the RUN box, but it does # assume AVX2 availability by default now rather than opt-in. ifneq ($(AVX2),0) PROFILE += -mavx2 -mfma endif ifdef DEBUG PROFILE += -g -DDEBUG BINARY = _typhoon else PROFILE += -g -O3 -ffast-math -finline-functions BINARY = typhoon endif ifdef USE_READLINE LIBRARIES += -lreadline PROFILE += -DUSE_READLINE endif ifdef SIXTYFOUR # -mpopcnt: every x86-64 chip built since ~2008 (and this box's Ryzen # 9 3900X) has the POPCNT instruction, but clang won't emit it for # __builtin_popcountll without this -- without it, the builtin lowers # to a ~15-instruction SWAR bit-trick sequence instead (still faster # than the old CDECL-call CountBits, but not the ~1-3 cycle single # instruction the "fast" eval.c wrappers were assumed to get). PROFILE += -m64 -mpopcnt NASMFLAGS += -d_X64_ else PROFILE += -m32 NASMFLAGS += -d_X86_ endif ifdef CROUTINES PROFILE += -DCROUTINES endif ifdef GETATTACKS_BITBOARD PROFILE += -DGETATTACKS_BITBOARD endif ifndef DISABLE_BITBOARD_MOVEGEN PROFILE += -DGENERATE_KNIGHT_BITBOARD -DGENERATE_KING_BITBOARD \ -DGENERATE_ROOK_BITBOARD -DGENERATE_BISHOP_BITBOARD \ -DGENERATE_QUEEN_BITBOARD -DGENERATE_PAWN_BITBOARD \ -DGENERATE_ESCAPES_KING_BITBOARD -DGENERATE_ESCAPES_BLOCK_BITBOARD \ -DEXPOSESCHECK_BITBOARD -DISATTACKED_BITBOARD \ -DUNBLOCKED_BITBOARD endif ifdef EVERYTHING PROFILE += -DEVAL_DUMP -DEVAL_TIME -DPERF_COUNTERS -DMP -DSMP -DTEST_NULL -DTREEDUMP -fbounds-checking -DMOVE_ORDERING_EVIDENCE_COLLECTION else ifdef EVAL_DUMP PROFILE += -DEVAL_DUMP endif ifdef EVAL_TIME PROFILE += -DEVAL_TIME endif ifdef PERF_COUNTERS PROFILE += -DPERF_COUNTERS endif # 2026-10-10: this flag was referenced (search.c's joint-table/marginal # collection block, #ifdef FH_TABLE_COLLECTION) but never actually wired # to a compiler define anywhere in this makefile -- every prior # collection run must have been built some other way (or this gap # predates the comment claiming it's "a live, in-progress collection # run"). Requires PERF_COUNTERS=1 too (the block this gates is nested # inside an outer #ifdef PERF_COUNTERS). # # Re-added 2026-10-20: this wiring was itself silently dropped at some # point after landing (collateral damage from the 2026-10-14 PRUNE_VERIFY/ # PRUNE_LIVE dead-flag cleanup, going by GNUmakefile.orig's surviving copy) # -- every FH_TABLE_COLLECTION=1 build in between compiled clean but # never actually included the instrumentation. Caught before launching # the NNUE dual-table re-collection run; a smoke-test build+run with # this restored confirmed -DFH_TABLE_COLLECTION actually reaches the # compiler and the (currently 4D-alpha-only, no beta side -- see # docs/NNUE.md) FOURAXISRAW dump fires. The original run's richer # ALPHARAW/BETARAW/ESTRAW dump format no longer exists in this tree at # all (not just unwired) -- that's the separate, larger restoration # this collection run is waiting on, tracked alongside this fix. ifdef FH_TABLE_COLLECTION PROFILE += -DFH_TABLE_COLLECTION endif ifdef MOVE_ORDERING_EVIDENCE_COLLECTION PROFILE += -DMOVE_ORDERING_EVIDENCE_COLLECTION endif # 2026-10-23, one-off measurement: does IS_CAPTURE(mvHash) (optionally # && pi->eNodeType==NODE_CUT) meaningfully predict a leftover move's # fail-high rate, beyond what the existing joint FH table's five axes # already capture? Neither signal is currently baked into the table at # all. Bucketed counters only (chess.h's u64HashCapTries/FH, [2][2] -- # [hash move is a capture][node type guess is CUT]), printed by # script.c's _PrintSuiteReport -- cheap enough to leave compiled in for # a --cpus 1 collection run; not wired into split.c's MP merge, same # convention as the other single-threaded-only calibration counters # (u64TotalCMEvidenceTries et al) in that same report. ifdef HASHCAP_COLLECTION PROFILE += -DHASHCAP_COLLECTION endif ifdef BOUNDS_CHECKING PROFILE += -fbounds-checking endif ifdef MP PROFILE += -DMP -DSMP endif ifdef TEST_NULL PROFILE += -DTEST_NULL endif ifdef TREEDUMP PROFILE += -DTREEDUMP endif ifdef CALIBRATE_POSITIONAL PROFILE += -DCALIBRATE_POSITIONAL endif ifdef KING_DEFECT_DUMP PROFILE += -DKING_DEFECT_DUMP endif ifdef CALIBRATE_BASE_MARGIN PROFILE += -DCALIBRATE_BASE_MARGIN endif ifdef CALIBRATE_MARGIN_SAFETY PROFILE += -DCALIBRATE_MARGIN_SAFETY endif ifdef DUMP_RESIDUAL_SAMPLES PROFILE += -DDUMP_RESIDUAL_SAMPLES endif ifdef DIAG_NO_QSEARCH_FUTILITY PROFILE += -DDIAG_NO_QSEARCH_FUTILITY endif ifdef CALIBRATE_QSEARCH_FUTILITY PROFILE += -DCALIBRATE_QSEARCH_FUTILITY endif endif # EVERYTHING # 2026-09-28: full-coverage (non-sampled) throwaway shadow re-search, # purely to measure the "silent miss" failure mode a reduced move's own # real re-search hook can't see: the extra-reduced score is < beta (no # real re-search triggered) but removing just the EXTRA plies (i.e. the # depth HEAD's own baseline -ONE_PLY reduction would have used) finds # a fail-high. Every extra-reduced move gets this check within a build # with this flag on -- doubles search cost for that subset, so # calibration-only, never part of a normal or self-play build. Opt in # explicitly, same as the other CALIBRATE_*/DUMP_* flags above. ifdef JOINT_EXTRA_VERIFY PROFILE += -DJOINT_EXTRA_VERIFY endif # 2026-09-28: audit-only EFP disable, same technique this project used # during the joint-table collection run's self-censoring fix (force the # prune decision FALSE, keep computing/logging everything around it) -- # isolates the joint-table-driven LMR's own behavior from EFP's # single-axis (gap/down only) margin-based pruning, which the # JOINT_EXTRA_VERIFY silent-miss data implicates as the dominant source # of the measured instability, not LMR's own reduction depth. Opt-in, # calibration-only. ifdef DISABLE_EFP PROFILE += -DDISABLE_EFP endif # 2026-09-28: Trace() the FEN/move/scores whenever JOINT_EXTRA_VERIFY's # shadow search catches a silent miss -- for looking at real, concrete # failing cases by hand (the methodology that actually cracked the # hanging-material bug earlier this project, rather than continuing to # guess which other search mechanism might explain the EFP-tier-window # silent-miss spike). Requires JOINT_EXTRA_VERIFY. Calibration-only. ifdef SILENT_MISS_TRACE PROFILE += -DSILENT_MISS_TRACE endif # 2026-09-29: alternate, maximally-simple extra-reduction policy for A/B # comparison against the axis-gated joint-table version -- skips the # dumb/gap/late table lookup entirely and just asks "is there enough # pre-reduction depth headroom to spare (>= 6 plies)?" If so, apply the # full extra reduction (R=3) uniformly, for ANY depth >= 6 (not just # exactly 6 -- 7, 8, 9, ... all get the same treatment). Tests whether # the axis-based table is pulling any real weight once a depth floor is # already in place, or whether depth alone does just as well. ifdef DEPTH_ONLY_EXTRA_REDUCTION PROFILE += -DDEPTH_ONLY_EXTRA_REDUCTION endif # 2026-09-29: neutralize searchsup.c's null-move "history-reduced # bailout" branch (Tord Romstad idea) for our own shadow-verification # Search() call specifically, by temporarily zeroing our own # pi->iExtensionAmount around it -- see search.c's NEUTRALIZE_NULL_ # BAILOUT comment. Correct in the real search path, that branch # fabricates a "please re-search me" placeholder score that our # bespoke, wrapper-less shadow call can't interpret and misreads as a # genuine fail-high. Build once WITHOUT this flag to measure how much # of our silent-miss data is this artifact (see # u64JointExtraVerifyNullBailoutSignature), once WITH it for the # corrected rate. # 2026-10-03: PRUNE_VERIFY -- shadow verification for the DEEP PRUNE gate # (ShouldLatePrune) was removed (2026-10-14): it never actually compiled # (fVerifyFlagged/uVerifyBand/etc. were used in search.c but never # declared -- PRUNE_VERIFY is never defined by any path in this file, so # nothing ever caught it). Dead, broken code, not a working diagnostic; # removed rather than fixed since reconstructing its intended semantics # from scratch would be inventing behavior, not restoring it. # 2026-10-03: PRUNE_LIVE= was removed (2026-10-14): PruneLivePredict/ # PRUNE_LIVE_SENTINEL (search.c) were never actually called from # anywhere, under any flag -- the wiring to make PRUNE_LIVE= actually # prune a move was never written, so this flag has never done anything # even when it compiled. It also didn't compile: PruneVerifyPredict # (the function PruneLivePredict aliased to) returns # g_uJointFHThousandths[...]'s element as ULONG, but that array is # declared USHORT in data.c -- confirmed pre-existing, unrelated to # PRUNE_VERIFY's own removal above. Dead on every axis; removed rather # than fixed for the same reason as PRUNE_VERIFY. # 2026-10-03: DISABLE_LMR -- GetLMRReduction returns 0 unconditionally, # so no move is ever reduced. Exists for ONE experiment: what is LMR # actually worth in this engine? The time-odds calibration established # that TIME-bought depth is ~95 Elo/ply here, but nothing has ever # measured whether REDUCTION-bought depth converts at that rate. Two # hypotheses fit "deeper but no stronger": (1) missed refutations, i.e. # the reduction is too aggressive and its errors cancel the gain -- # remedy is to constrain it; or (2) nominal depth under reduction is a # debased currency that never carried a time-ply's worth of information # -- in which case constraining it cannot help, since less reduction # buys less depth at the same bad rate. Removing LMR entirely and # pricing the loss against the 95 Elo/ply yardstick separates them. # Measurement only; never a shipping configuration. ifdef DISABLE_LMR PROFILE += -DDISABLE_LMR endif # 2026-10-13: override search.c's NULLMOVE_SKIP_FH_CEILING_THOUSANDTHS # default (300, validated -- see search.c's own comment) from the # command line for sweeping, e.g. `gmake ... NULLMOVE_SKIP_CEILING=1000` # -- search.c's #define is guarded with #ifndef so this wins without # editing the source. ifdef NULLMOVE_SKIP_CEILING PROFILE += -DNULLMOVE_SKIP_FH_CEILING_THOUSANDTHS=$(NULLMOVE_SKIP_CEILING) endif # 2026-10-13: override movesup.c's SelectBestWithHistory skip- # selection-sort threshold for sweeping, e.g. # `gmake ... SKIP_SELECT_N=6`. ifdef SKIP_SELECT_N PROFILE += -DSKIP_SELECT_AFTER_N_MOVES=$(SKIP_SELECT_N) endif ifdef NEUTRALIZE_NULL_BAILOUT PROFILE += -DNEUTRALIZE_NULL_BAILOUT endif # Short commit hash (plus a -dirty suffix if the working tree has # uncommitted changes) baked into the binary so a --logfile trace can # be tied back to the exact source state, not just a build timestamp -- # distinguishing rebuilds of the same day/commit during A/B testing was # previously only possible by diffing binaries. Placed here, after all # the PROFILE-overwriting `=` assignments above (not `+=`), so it # survives into CFLAGS instead of being wiped out by them. GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) # Scoped to this directory: an unscoped `git diff` has to stat (and, # with a stale index, re-hash) every tracked file in the repo, # including the multi-GB pgn/twic.pgn, on every make invocation. GIT_DIRTY := $(shell git diff --quiet -- . 2>/dev/null || echo -dirty) # Kept out of PROFILE itself (not folded in via +=): PROFILE gets # stringified whole into -DPROFILE="..." below for the "Make profile # used" trace line, and this value's embedded quotes broke that outer # string literal when it was included there. # -flto: several functions (e.g. _RookAttacksBB/_BishopAttacksBB, # generate.c) are declared FORCEINLINE but non-static, exposed # specifically so other translation units (eval.c, searchsup.c, see.c) # can call them -- __attribute__((always_inline)) only actually forces # inlining within the TU that has the function body, so every # cross-file call was paying for a real call/ret instead of inlining # without this. Measured via the `bench` command (4 reps each, # interleaved with a non-LTO baseline): a clean, reproducible ~6.6% NPS # improvement with zero overlap between the two distributions (baseline # 1643k-1661k nps, LTO 1752k-1768k nps) -- not noise. -mtune=native: # tunes instruction scheduling/costs for the actual build machine # rather than a generic x86-64 target; measured weaker and noisier # (~1.5%, some overlap with baseline) but real and free (no # portability cost the way -march=native's newer-ISA-extension # dependency would have -- see the BMI2/PEXT-on-Zen2 caveat if anyone # reaches for -march=native or PEXT-based magic bitboards later). CFLAGS = -DPROFILE="\"$(PROFILE)\"" -DGIT_COMMIT=\"$(GIT_COMMIT)$(GIT_DIRTY)\" $(PROFILE) -Wall -Wno-nan-infinity-disabled -Ifathom -flto -mtune=native HEADERS = chess.h compiler.h FATHOM_CFLAGS = -std=gnu99 -O2 -Ifathom $(filter -m32 -m64,$(PROFILE)) # # ---> .o, not .c! <--- # OBJS = main.o root.o search.o searchsup.o draw.o dynamic.o \ hash.o eval.o x86.o pawnhash.o bitboard.o \ generate.o see.o move.o movesup.o command.o script.o \ input.o vars.o util.o unix.o gamelist.o mersenne.o \ sig.o piece.o ics.o san.o fen.o book.o bench.o board.o \ data.o probe.o fathom.o recogn.o list.o x64.o \ nnuefeatures.o nnueaccum.o nnueeval.o ifdef TEST # ---> .o, not .c! <--- # OBJS += testdraw.o testmove.o testfen.o testgenerate.o \ testsan.o testics.o testeval.o testbitboard.o \ testhash.o testsee.o testsearch.o testsup.o \ testrecogn.o testnnue.o PROFILE += -DTEST else ifdef EVAL_DUMP OBJS += testeval.o testsup.o endif endif ifdef TREEDUMP OBJS += dumptree.o endif ifdef MP OBJS += split.o endif .SUFFIXES: .c .o .SUFFIXES: .cpp .o .SUFFIXES: .asm .o .PHONY: all all: $(BINARY) $(BINARY): $(OBJS) $(CXX) $(PROFILE) -flto -o $(BINARY) -Wall $(LIBRARIES) \ $(OBJS) %.o: %.c $(HEADERS) $(CC) $(CFLAGS) -c $< -o $@ fathom.o: fathom/tbprobe.c fathom/tbchess.c fathom/tbconfig.h fathom/tbprobe.h fathom/stdendian.h $(CC) $(FATHOM_CFLAGS) -c fathom/tbprobe.c -o $@ %.o: %.cpp $(HEADERS) $(CXX) $(CFLAGS) -c -w $< -o $@ %.o: %.asm $(HEADERS) $(NASM) $(NASMFLAGS) -o $@ $< .PHONY: release release: $(MAKE) -j5 MP=1 GENETIC=1 .PHONY: debug debug: $(MAKE) -j5 DEBUG=1 PERF_COUNTERS=1 GENETIC=1 .PHONY: clean clean: $(RM) -f *.o *.core *~ *.gmon \#*\# typhoon _typhoon