Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
153ce8a
Eval: implement 4PC king safety, endgame activity, pins, X-rays and c…
KnightStackSys Oct 23, 2026
41a2d04
Perf: make king HCE bitboard-centric with cached square masks and sha…
KnightStackSys Oct 23, 2026
f62ce3e
Bench: isolate HCE king feature groups with same-runner NPS comparisons
KnightStackSys Oct 23, 2026
f85aeff
Perf: reuse slider mobility attack maps to prefilter king pin and X-r…
KnightStackSys Oct 23, 2026
debc346
Perf: avoid full givesCheck simulation for geometrically impossible q…
KnightStackSys Oct 23, 2026
a184656
Perf: apply conservative check prefilter to non-PV LMP/HMP candidates
KnightStackSys Oct 23, 2026
843cc13
Perf: revert redundant non-PV check prefilter after same-runner bench…
KnightStackSys Oct 23, 2026
c188254
Build: add optional native-CPU Release optimization for local speed
KnightStackSys Oct 23, 2026
7479904
Perf: trial exact per-worker evaluation cache with search-local inval…
KnightStackSys Oct 23, 2026
7d77cec
Perf: remove lock construction from single-thread TT probe/store
KnightStackSys Oct 23, 2026
4412a1b
Tests: validate eval cache exactness, option bounds and epoch invalid…
KnightStackSys Oct 23, 2026
b8186f5
Perf: skip provably redundant king-in-check tests after ordinary moves
KnightStackSys Oct 23, 2026
e6567d8
Docs: describe tested eval cache, king-legality and TT hot-path optim…
KnightStackSys Oct 23, 2026
d8e35b8
Perf experiment: cache exact 4PC pawn-attack unions per worker
KnightStackSys Oct 23, 2026
663439d
Perf experiment: lazily select tactical moves in non-PV quiescence
KnightStackSys Oct 23, 2026
24469cf
Perf: revert lazy QSearch picking after identical-runner NPS regression
KnightStackSys Oct 23, 2026
193e157
Perf experiment: skip impossible pin/X-ray ray scans via precomputed …
KnightStackSys Oct 23, 2026
16b2157
Tests/docs: strengthen king ray equivalence and record measured speedups
KnightStackSys Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ set_property(GLOBAL PROPERTY USE_FOLDERS ON)
option(IRONPHOENIX_BUILD_TESTS "Build IronPhoenix correctness tests" ON)
option(IRONPHOENIX_BUILD_BENCHMARK "Build IronPhoenix microbenchmark" ON)
option(IRONPHOENIX_BUILD_DATASET_GENERATOR "Build PhoenixNet dataset generator" ON)
option(IRONPHOENIX_NATIVE_CPU "Optimize Release/RelWithDebInfo for the build host CPU; nonportable binaries" OFF)

add_library(ironphoenix_core STATIC
src/geometry.cpp
Expand All @@ -24,6 +25,7 @@ add_library(ironphoenix_core STATIC
src/tt.cpp
src/eval.cpp
src/bishop_eval.cpp
src/king_eval.cpp
src/nnue.cpp
src/tuning.cpp
src/search.cpp
Expand All @@ -34,10 +36,12 @@ add_library(ironphoenix_core STATIC
src/uci.cpp
include/ironphoenix/eval.hpp
include/ironphoenix/bishop_eval.hpp
include/ironphoenix/king_eval.hpp
include/ironphoenix/search_stack.hpp
include/ironphoenix/lmr.hpp
include/ironphoenix/nnue.hpp
include/ironphoenix/tuning.hpp
include/ironphoenix/king_legality.hpp
)

add_library(IronPhoenix::Core ALIAS ironphoenix_core)
Expand All @@ -50,6 +54,24 @@ target_include_directories(ironphoenix_core
target_compile_features(ironphoenix_core PUBLIC cxx_std_20)
target_link_libraries(ironphoenix_core PUBLIC Threads::Threads)

# Only enable for a binary that will run on the same CPU generation used to
# build it. Keep CI/distribution generic so older systems are not exposed to
# unsupported instructions. PUBLIC propagates ISA flags to tests/apps because
# Bitboard intrinsics are also header-inlined.
if(IRONPHOENIX_NATIVE_CPU)
if(MSVC)
target_compile_options(ironphoenix_core PUBLIC
$<$<CONFIG:Release>:/arch:AVX2>
$<$<CONFIG:RelWithDebInfo>:/arch:AVX2>
)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang")
target_compile_options(ironphoenix_core PUBLIC
$<$<CONFIG:Release>:-march=native>
$<$<CONFIG:RelWithDebInfo>:-march=native>
)
endif()
endif()

if(MSVC)
target_compile_options(ironphoenix_core PRIVATE
/W4
Expand Down Expand Up @@ -217,6 +239,36 @@ if(IRONPHOENIX_BUILD_TESTS)
target_compile_features(ironphoenix_knight_outpost PRIVATE cxx_std_20)
add_test(NAME ironphoenix_knight_outpost COMMAND ironphoenix_knight_outpost)

add_executable(ironphoenix_check_order tests/check_order.cpp)
target_link_libraries(ironphoenix_check_order PRIVATE IronPhoenix::Core)
target_compile_features(ironphoenix_check_order PRIVATE cxx_std_20)
add_test(NAME ironphoenix_check_order COMMAND ironphoenix_check_order)
set_target_properties(ironphoenix_check_order PROPERTIES FOLDER "Tests")

add_executable(ironphoenix_pawn_cache tests/pawn_cache.cpp)
target_link_libraries(ironphoenix_pawn_cache PRIVATE IronPhoenix::Core)
target_compile_features(ironphoenix_pawn_cache PRIVATE cxx_std_20)
add_test(NAME ironphoenix_pawn_cache COMMAND ironphoenix_pawn_cache)
set_target_properties(ironphoenix_pawn_cache PROPERTIES FOLDER "Tests")

add_executable(ironphoenix_king_legality tests/king_legality.cpp)
target_link_libraries(ironphoenix_king_legality PRIVATE IronPhoenix::Core)
target_compile_features(ironphoenix_king_legality PRIVATE cxx_std_20)
add_test(NAME ironphoenix_king_legality COMMAND ironphoenix_king_legality)
set_target_properties(ironphoenix_king_legality PROPERTIES FOLDER "Tests")

add_executable(ironphoenix_eval_cache tests/eval_cache.cpp)
target_link_libraries(ironphoenix_eval_cache PRIVATE IronPhoenix::Core)
target_compile_features(ironphoenix_eval_cache PRIVATE cxx_std_20)
add_test(NAME ironphoenix_eval_cache COMMAND ironphoenix_eval_cache)
set_target_properties(ironphoenix_eval_cache PROPERTIES FOLDER "Tests")

add_executable(ironphoenix_king_features tests/king_features.cpp)
target_link_libraries(ironphoenix_king_features PRIVATE IronPhoenix::Core)
target_compile_features(ironphoenix_king_features PRIVATE cxx_std_20)
add_test(NAME ironphoenix_king_features COMMAND ironphoenix_king_features)
set_target_properties(ironphoenix_king_features PROPERTIES FOLDER "Tests")

add_executable(ironphoenix_bishop_features tests/bishop_features.cpp)
target_link_libraries(ironphoenix_bishop_features PRIVATE IronPhoenix::Core)
target_compile_features(ironphoenix_bishop_features PRIVATE cxx_std_20)
Expand Down
62 changes: 62 additions & 0 deletions docs/king-evaluation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# 4PC Teams King Evaluation

This branch adds original HCE heuristics inspired by Chess Programming Wiki
[King Safety], [King Centralization], [Mate at a Glance], [Pin], [X-ray]
and [Castling Rights], adapted to Red+Yellow vs Blue+Green and the 14x14 board.

These are evaluation weights, not illegal move pruning or forced mate detection.

## Features
- **Middlegame shield**: three oriented forward files/ranks from each king, allowing nearby allied pawn shelter and penalizing gaps.
- **Attacked king zone**: reused team attack maps from the existing mobility stage, covering both enemy colors.
- **Enemy pawn storm**: nearby enemy pawn advances toward a home king.
- **Mate at a glance**: a *king-danger proxy* for an already attacked king with two or fewer unattacked empty adjacent escape squares. This is not a legal mate detector and **never outputs a terminal mate score**.
- **Endgame centralization**: tapered central proximity reward based on surviving enemy non-pawn material.
- **Pawnless wing**: endgame penalty for a wing king when neither teammate has pawns on that wing, with seat-orientated transverse axes.
- **Absolute pins**: a friendly piece shielding a king from an enemy bishop/rook/queen on a real uninterrupted 4PC ray.
- **X-ray pressure**: an allied piece shielding a queen from an enemy slider. No invalid cross-corner rays.
- **Castling rights**: middlegame bonus only when a configured lane has the correct live right bit and original king/rook in place. Does not assume that immediate castling is currently legal.

The middlegame fraction is based on remaining **enemy** non-pawn material;
the endgame fraction is complementary. No change to the actual rules, moves,
mate scores, TT, or search. NNUE continues to bypass HCE as before.

## New UCI parameters

| Option | Default |
|---|---:|
| EvalKingShieldPenalty | 10 |
| EvalKingZoneAttackPenalty | 6 |
| EvalKingPawnStormPenalty | 5 |
| EvalKingMateThreatPenalty | 30 |
| EvalKingCentralizationBonus | 4 |
| EvalKingPawnlessWingPenalty | 14 |
| EvalKingPinPenalty | 14 |
| EvalKingXrayPenalty | 8 |
| EvalKingCastlingRightsBonus | 8 |

Each can be set to 0 to disable a feature and tuned separately through
Tunex. These defaults are *experimental* and require independent SPRT.
All previously tuned Search/Eval parameters remain intact.

HCE king terms are deliberately computed after the shared mobility/attack
stage. Reuse of existing attack maps limits extra full-board scans, but rays,
phase calculations and pawn scans cost NPS. Profile on real 4PC positions.

## Validation

The king-feature regression test covers: all nine parameter boundaries and
UCI declarations; material-phase king shield and pawn storm; threat proxy and
zone attacks; endgame centralization and wing pawns; pins and queen X-rays;
castling right preconditions; and team-relative sign reversal.

Run CMake/CTest on Linux and Windows before testing Elo.
Compare both **NPS and paired WDL/SPRT** against \`main\`, ideally varying
one new king term at a time.

[King Safety]: https://chessprogramming.org/King_Safety
[King Centralization]: https://chessprogramming.org/King_Centralization
[Mate at a Glance]: https://chessprogramming.org/Mate_at_a_Glance
[Pin]: https://chessprogramming.org/Pin
[X-ray]: https://chessprogramming.org/X-ray
[Castling Rights]: https://chessprogramming.org/Castling_Rights
115 changes: 115 additions & 0 deletions docs/performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# IronPhoenix performance experiments

Branch: \`feature/king-evaluation\`. Keep \`main\` stable until Elo tests.

## Changes

- Precomputed immutable, seat-aware king shield/storm/wing masks.
- Hoisted four-army material phase, slider presence, team pawns and occupancy
out of the per-king evaluation path.
- Reuse the existing mobility slider-attack maps to avoid pin/X-ray probes
if no opposing slider attacks an allied screening piece.
- Fast (exact) geometric prefilter for PV quiet move check ordering. Discovered
checks and checks that already exist are handled by the full evaluator.
- UCI \`SearchFastCheckPrefilter\` defaults to 1; use 0 for A/B comparison.
A broader version in non-PV pruning was tested and **reverted** because it
showed no measurable benefit on the same runner.
- Benchmark \`ironphoenix_nps_bench\` now measures feature groups in addition
to its original fixed-position search throughput.

## Latest exact evaluation speedups

- `SearchPawnAttackCache=1` (default): Each worker caches the union
of each color's pawn attack bitboards by **all three pawn-bitboard words**.
Pawn structures often repeat even when non-pawn pieces change. Values
remain exact; the thread-local cache cannot race between SMP workers.
- `SearchKingRayPrefilter=1` (default): Precomputed geometric rank/file/
diagonal rays reject king and queen pin/X-ray probes when no opposing
slider is even aligned with the target. All four-color evaluation
scores are preserved.
- Quiescence **lazy tactical picking was tested then reverted**, because
it measured 512,945 NPS versus 516,048 NPS with ordinary full sorting.

Diagnostic same-runner Linux CI comparisons (individual experiments,
not additive or directly predictive of your CPU):

| Optimization | Disabled | Enabled | Difference |
|---|---:|---:|---:|
| Pawn attack cache | 510,898 | 524,508 | +2.7% |
| King ray prefilter | 509,880 | 537,731 | +5.5% |

Both switches accept `0` to disable and `1` to enable in UCI. These
are HCE optimizations; loaded NNUE bypasses handcrafted evaluation.

### Additional build experiment: whole-program optimization

CMake supports interprocedural optimization through its built-in option.
On MSVC, test a separate Release build with

```powershell
cmake -S . -B build-ipo -DIRONPHOENIX_NATIVE_CPU=ON -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON
cmake --build build-ipo --config Release --parallel
```

This may improve inlining across translation units, but has **not** been
benchmarked or validated for this repository's toolchain. Compare to
the native Release build with IPO disabled on the same machine.

## Native CPU Release build (opt-in)

CMake option: \`IRONPHOENIX_NATIVE_CPU=ON\` (default OFF).

**Windows (PowerShell / Visual Studio generator):**

\`\`\`powershell
cmake -S . -B build-native -DIRONPHOENIX_NATIVE_CPU=ON
cmake --build build-native --config Release --parallel
.\build-native\Release\ironphoenix_nps_bench.exe
\`\`\`

**Linux (GCC/Clang):**

\`\`\`bash
cmake -S . -B build-native -DCMAKE_BUILD_TYPE=Release -DIRONPHOENIX_NATIVE_CPU=ON
cmake --build build-native --parallel
./build-native/ironphoenix_nps_bench
\`\`\`

GCC/Clang use \`-march=native\`; MSVC uses \`/arch:AVX2\` for Release
and RelWithDebInfo. The MSVC option is a broad, predictable instruction-set
target rather than necessarily the exact highest available CPU features.

**Do not redistribute a native build to arbitrary computers**: it may use
instructions unsupported by another CPU. Keep portable builds with the
option OFF.

## How to judge speed

1. Run portable and native binaries on the **same computer** with the
same node limit and fixture.
2. Repeat each benchmark (e.g., five times), compare median elapsed time
and NPS rather than a single run.
3. Run both with \`Threads=1\` when comparing per-core changes; then compare
total throughput with \`Threads=4\`, \`8\`, etc.
4. Judge search strength separately via paired SPRT. Higher NPS alone is
not evidence that a pruning change increases Elo.

CI cloud runners have different CPU allocations, so absolute NPS between
different workflow runs is not a valid speedup measurement. The benchmark's
same-runner feature variants provide better relative evidence.

Recent **single Linux CI runner** experimental comparisons (not user-machine
NPS and not proven Elo gains):

| Switch | Disabled | Enabled | Speedup |
|---|---:|---:|---:|
| `SearchEvalCache` | 598,037 | 612,344 | about 2.4% |
| `SearchFastKingLegality` | 856,053 | 861,818 | about 0.7% |

Benchmark runs use the same fixed 4PC fixture, node budget and worker count,
but the repeated variant searches occur sequentially, so CPU variability and
hash/iteration rounding still affect short measurements. Run longer tests
locally, repeat trials, and rely on medians.

This feature branch retains all new king-evaluation terms. NNUE still
bypasses HCE; the HCE speed work helps only when NNUE is disabled.
14 changes: 14 additions & 0 deletions include/ironphoenix/king_eval.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#pragma once

#include "bitboard.hpp"

namespace ironphoenix {
class Position;
namespace Eval {
// Red+Yellow advantage minus Blue+Green advantage.
// Supplied attack maps are collected by the existing mobility evaluator.
[[nodiscard]] int kingFeatureDifference(const Position& pos,
const Bitboard (&teamAttacks)[2],
const Bitboard (&sliderAttacks)[2]) noexcept;
}
}
27 changes: 27 additions & 0 deletions include/ironphoenix/king_legality.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#pragma once
#include "position.hpp"

namespace ironphoenix {

// Only slider attacks can become newly exposed when an ordinary move vacates
// one source square. If that source is not on any rank/file/diagonal passing
// through our king, then an already-safe king cannot become checked.
// Castling/EP remove other pieces and king moves relocate the target, so they
// always require full validation. This returns true conservatively.
[[nodiscard]] IRONPHOENIX_FORCE_INLINE bool kingCheckCanChange(
const Position& pos, Move move) noexcept {
if (move.isEnPassant() || move.isCastle())
return true;
const Piece moving = pos.pieceAt(move.from());
if (moving == NO_PIECE || typeOf(moving) == KING)
return true;
const Square king = pos.kingSquare(pos.sideToMove());
if (king == SQ_NONE)
return true;
for (unsigned family = 0; family < Geometry::LINE_FAMILY_NB; ++family)
if (Geometry::LineId[family][king] == Geometry::LineId[family][move.from()])
return true;
return false;
}

} // namespace ironphoenix
17 changes: 17 additions & 0 deletions include/ironphoenix/search.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ namespace ironphoenix {
// Exposed only to the mate-stop regression test.
friend struct MateStopTestAccess;
friend struct ReverseFutilityTestAccess;
friend struct CheckOrderTestAccess;
friend struct EvalCacheTestAccess;
[[nodiscard]] bool reverseFutilitySafe(Position& pos) const;

struct TimeBudget final {
Expand All @@ -97,6 +99,21 @@ namespace ironphoenix {
bool helperMode_ = false;
int helperIndex_ = 0;
int threads_ = 1;

// Search-local, worker-private HCE cache. Lazily invalidated by epoch
// at every start, so changed tuning/network settings never leave stale
// evaluations. Avoid contention between Lazy SMP workers.
static constexpr std::size_t EVAL_CACHE_CAPACITY = 1u << 14;
struct EvalCacheEntry {
Key key = 0;
int score = 0;
std::uint32_t epoch = 0;
};
mutable std::array<EvalCacheEntry, EVAL_CACHE_CAPACITY> evalCache_{};
mutable std::uint64_t evalCacheHits_ = 0;
std::uint32_t evalCacheEpoch_ = 1;
bool evalCacheActive_ = false;

HistoryTables history_{};
std::thread worker_;
std::atomic<bool> stopRequested_{ false };
Expand Down
22 changes: 22 additions & 0 deletions include/ironphoenix/tuning.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ namespace ironphoenix {
int rfpMarginPerDepth = 141;
int rfpMinMargin = 93;
int rfpMaxImprovingDiscount = 82;
// 1 = cheaply reject geometrically impossible quiet checks.
// 0 = original full givesCheck() for A/B benchmarking.
int fastCheckPrefilter = 1;
// Exact full-evaluation cache, private to each search worker.
// Set 0 to disable for NPS/SPRT comparisons.
int evalCacheEnabled = 1;
// Geometrically skip redundant inCheck(mover) after safe moves.
// 0 forces original full move-by-move legality verification.
int fastKingLegality = 1;
// Exact union of pawn attack maps, cached per worker.
int pawnAttackCache = 1;
// Skip impossible slider rays in HCE pins/X-ray scoring.
int kingRayPrefilter = 1;
};

struct EvalParameters final {
Expand All @@ -55,6 +68,15 @@ namespace ironphoenix {
int bishopColorWeaknessPenalty = 3;
int bishopFianchettoBonus = 5;
int bishopReturningBonus = 22;
int kingShieldPenalty = 10;
int kingZoneAttackPenalty = 6;
int kingPawnStormPenalty = 5;
int kingMateThreatPenalty = 30;
int kingCentralizationBonus = 4;
int kingPawnlessWingPenalty = 14;
int kingPinPenalty = 14;
int kingXrayPenalty = 8;
int kingCastlingRightsBonus = 8;
};

[[nodiscard]] SearchParameters& search() noexcept;
Expand Down
Loading
Loading