From 379a03bbd993247c8de9c1f1b163fcfab2fa1d69 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Tue, 8 Sep 2026 18:47:22 -0700 Subject: Land super-lazy exit, material-based lazy floor, qsearch futility rework Brings in the last remaining piece from stash@{0}: the super-lazy exit point (material-only pre-check before the regular lazy gate), a material-bucket floor under the regular lazy exit's margin (iSwingFloorByArmy), and search.c's qsearch futility rework (FUTILITY_BASE_MARGIN_BY_SOURCE, indexed by which Eval() exit tier produced the score). Required Eval()'s signature change from a single SCORE* to SCORE(*)[2] (positional estimate per side instead of one munged magnitude) -- search.c's futility margin folds in rgiPositional[pos->uToMove], which the earlier bad-trades investigation found to be a meaningfully predictive signal. search.c and chess.h brought in wholesale from the stash (both were either completely untouched by prior commits or contained no divergence worth preserving). eval.c required hand-merging on top of this session's already-applied bad-trades fix, B-over-N removal, and xColor/reorder cleanups -- ported the super-lazy exit block, the regular-lazy material floor, the per-color piPositional writes (all three exit sites: super-lazy, regular-lazy x2, full-eval), the super-lazy calibration harness (RecordSuperLazyMarginSafetySwing, dual-regime DumpMarginSafetyCalibration), and moved uArmyScaler/ uNumTrapped initialization to match the new ordering the super-lazy exit depends on. Verified: clean release + DEBUG build (only the previously-flagged _EvalTrappedPieces warning), DEBUG smoke test pass, and a 40-game st1 match against clean 434fa04 (score 0.487, llr -0.04) -- landing this margin machinery as-is from the stash, before any retuning, does not regress strength on its own. This confirms the original regression (0.15-0.225 score seen early in this investigation) was fully explained by the bad-trades unsigned-underflow bug, not by these margins being unsound. Values are the original, as-derived-from-calibration ones (see inline comments: SUPER_LAZY_MARGIN_BY_ARMY from 100 positions/sd8 with ~15-20% headroom, iSwingFloorByArmy from 1500 positions/sd8 with ~25% headroom, FUTILITY_BASE_MARGIN_BY_SOURCE from a separate 1500-position/sd6 surprise-rate calibration). Not yet retuned -- suspected to carry more headroom than necessary, which costs real search speed (speed=depth). Next step: re-run the margin-safety calibration fresh against the current build and trim the super-lazy and regular-lazy floor headroom down from the built-in database, leaving the qsearch futility margins alone (different calibration method, already risk-tolerant by construction). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Ka9o3S2eKqh4jNfmxVZ6fH --- src/eval.c | 410 +++++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 345 insertions(+), 65 deletions(-) (limited to 'src/eval.c') diff --git a/src/eval.c b/src/eval.c index bc5f6f4..867c487 100755 --- a/src/eval.c +++ b/src/eval.c @@ -2820,6 +2820,43 @@ static UINT64 g_uMarginSwingSum[BASE_MARGIN_MAT_BUCKETS]; // moved"), this directly answers "was any real exit actually unsound". static UINT64 g_uMarginExceeded[BASE_MARGIN_MAT_BUCKETS]; +// Same four counters, kept in a fully separate set for the super-lazy +// exit (eval.c's material-only check before the regular lazy gate). +// Its own SUPER_LAZY_MARGIN was never run through this harness when +// it was added -- a much cheaper, much-larger-required-gap check than +// the regular lazy exit, so mixing its swings into the buckets above +// would wash out both regimes' signal. +static UINT64 g_uSuperLazyMarginSwingMax[BASE_MARGIN_MAT_BUCKETS]; +static UINT64 g_uSuperLazyMarginSwingCount[BASE_MARGIN_MAT_BUCKETS]; +static UINT64 g_uSuperLazyMarginSwingSum[BASE_MARGIN_MAT_BUCKETS]; +static UINT64 g_uSuperLazyMarginExceeded[BASE_MARGIN_MAT_BUCKETS]; + +static void +_RecordMarginSafetySwingInto(IN OUT UINT64 *puMax, + IN OUT UINT64 *puCount, + IN OUT UINT64 *puSum, + IN OUT UINT64 *puExceeded, + IN POSITION *pos, + IN SCORE iSwing, + IN SCORE iActualMarginUsed) +{ + ULONG uMatBucket = _MaterialBucket(pos); + + if (iSwing > iActualMarginUsed) + { + puExceeded[uMatBucket]++; + } + + ASSERT(iSwing >= 0); + puCount[uMatBucket]++; + puSum[uMatBucket] += (UINT64)iSwing; + if ((UINT64)iSwing > puMax[uMatBucket]) + { + puMax[uMatBucket] = (UINT64)iSwing; + } +} + + static void RecordMarginSafetySwing(IN POSITION *pos, IN SCORE iSwing, @@ -2846,52 +2883,58 @@ Return value: **/ { - ULONG uMatBucket = _MaterialBucket(pos); - - if (iSwing > iActualMarginUsed) - { - g_uMarginExceeded[uMatBucket]++; - } - - ASSERT(iSwing >= 0); - g_uMarginSwingCount[uMatBucket]++; - g_uMarginSwingSum[uMatBucket] += (UINT64)iSwing; - if ((UINT64)iSwing > g_uMarginSwingMax[uMatBucket]) - { - g_uMarginSwingMax[uMatBucket] = (UINT64)iSwing; - } + _RecordMarginSafetySwingInto(g_uMarginSwingMax, g_uMarginSwingCount, + g_uMarginSwingSum, g_uMarginExceeded, + pos, iSwing, iActualMarginUsed); } -void -DumpMarginSafetyCalibration(void) +static void +RecordSuperLazyMarginSafetySwing(IN POSITION *pos, + IN SCORE iSwing, + IN SCORE iActualMarginUsed) /** Routine description: - Print, per material bucket, the max and average |real - lazy| swing - observed among nodes that actually took a lazy exit this run - (command.c's "calibrate marginsafety"). The max is the number that - tells you how large LAZY_EVAL_BASE_MARGIN would need to be, at that - material level, before an exit could have been unsound. + Same as RecordMarginSafetySwing, but for the super-lazy exit's own + separate bucket set -- see the comment on the g_uSuperLazyMargin* + arrays above for why these are kept apart. Parameters: - void + POSITION *pos + SCORE iSwing : >= 0 + SCORE iActualMarginUsed : SUPER_LAZY_MARGIN Return value: void **/ +{ + _RecordMarginSafetySwingInto(g_uSuperLazyMarginSwingMax, + g_uSuperLazyMarginSwingCount, + g_uSuperLazyMarginSwingSum, + g_uSuperLazyMarginExceeded, + pos, iSwing, iActualMarginUsed); +} + + +static void +_DumpMarginSafetyCalibrationInto(IN CHAR *szLabel, + IN UINT64 *puMax, + IN UINT64 *puCount, + IN UINT64 *puSum, + IN UINT64 *puExceeded) { ULONG m; - Trace("Margin safety -- max/avg |real - lazy| swing among exits taken, " - "by material bucket:\n"); + Trace("%s -- max/avg |real - lazy| swing among exits taken, " + "by material bucket:\n", szLabel); for (m = 0; m < BASE_MARGIN_MAT_BUCKETS; m++) { - if (0 == g_uMarginSwingCount[m]) + if (0 == puCount[m]) { Trace(" material bucket %u (scaler %u-%u): no exits taken\n", m, m * BASE_MARGIN_MAT_WIDTH, @@ -2904,11 +2947,48 @@ Return value: COMPILER_LONGLONG_UNSIGNED_FORMAT "\n", m, m * BASE_MARGIN_MAT_WIDTH, (m * BASE_MARGIN_MAT_WIDTH) + BASE_MARGIN_MAT_WIDTH - 1, - g_uMarginSwingMax[m], - (double)g_uMarginSwingSum[m] / (double)g_uMarginSwingCount[m], - g_uMarginSwingCount[m], g_uMarginExceeded[m]); + puMax[m], (double)puSum[m] / (double)puCount[m], + puCount[m], puExceeded[m]); } } + + +void +DumpMarginSafetyCalibration(void) +/** + +Routine description: + + Print, per material bucket, the max and average |real - lazy| swing + observed among nodes that actually took a lazy exit this run + (command.c's "calibrate marginsafety") -- once for the regular lazy + exit (LAZY_EVAL_BASE_MARGIN / EstimatePositionalScore) and once for + the super-lazy exit (SUPER_LAZY_MARGIN), which is a different-enough + regime (cheaper check, much larger required gap) that lumping the + two together would hide whichever one is actually unsound. Either + section's max is the number that answers "how large would that + margin need to be, at that material level, before an exit there + could have been unsound." + +Parameters: + + void + +Return value: + + void + +**/ +{ + _DumpMarginSafetyCalibrationInto("Regular lazy margin safety", + g_uMarginSwingMax, g_uMarginSwingCount, + g_uMarginSwingSum, g_uMarginExceeded); + _DumpMarginSafetyCalibrationInto("Super lazy margin safety", + g_uSuperLazyMarginSwingMax, + g_uSuperLazyMarginSwingCount, + g_uSuperLazyMarginSwingSum, + g_uSuperLazyMarginExceeded); +} #endif // CALIBRATE_MARGIN_SAFETY @@ -3036,6 +3116,36 @@ Return value: *piAlphaMargin += iKingTerm + iResidualP90; *piBetaMargin += iKingTerm + iResidualP90; + + // 2026-09-08: the p90 estimate above, while within its own + // documented "~10% wrong" design tolerance, still let the regular + // lazy exit's real swing exceed the margin actually used 2.11- + // 2.38% of the time at combined army scaler 16-31 (CALIBRATE_ + // MARGIN_SAFETY, 1500 real-game positions, tests/twic_sample.ep_, + // sd 8) -- not because the swing itself is much bigger there (max + // observed 855/779, comparable to every other bucket's 603-701), + // but because REDUCED_MATERIAL_DOWN_SCALER shrinks the king term + // faster than the swing actually shrinks at that material level. + // Floor the combined margin at the measured max-per-bucket (with + // ~25% headroom) rather than re-deriving the whole p90 shape -- + // this leaves the estimate above as the primary driver wherever + // it's already wide enough (buckets 4-7, where it already clears + // these floors) and only kicks in where it wasn't. Buckets 0-1 + // never take a regular lazy exit at all (see LAZY_EVAL_MIN_ + // MATERIAL) so were never measured; use bucket 2's floor there + // defensively rather than leaving them unfloored. Provisional -- + // re-derive with `calibrate marginsafety` if this margin's shape + // changes again. + { + static const SCORE iSwingFloorByArmy[8] = + { + 1069, 1069, 1069, 974, 821, 876, 796, 754, + }; + ULONG uMatBucket = MINU( + 7, (pos->uArmyScaler[WHITE] + pos->uArmyScaler[BLACK]) / 8); + *piAlphaMargin = MAX(*piAlphaMargin, iSwingFloorByArmy[uMatBucket]); + *piBetaMargin = MAX(*piBetaMargin, iSwingFloorByArmy[uMatBucket]); + } } @@ -5033,7 +5143,7 @@ SCORE Eval(IN SEARCHER_THREAD_CONTEXT *ctx, IN SCORE iAlpha, IN SCORE iBeta, - OUT SCORE *piPositional) + OUT SCORE (*piPositional)[2]) /** Routine description: @@ -5043,11 +5153,16 @@ Parameters: SEARCHER_THREAD_CONTEXT *ctx, SCORE iAlpha, SCORE iBeta, - SCORE *piPositional : if non-NULL, filled in with a magnitude - (always >= 0) estimating the non-material component of the - score -- exact if a full eval ran, a cheap estimate otherwise. - Callers must treat it as an estimate either way; it's only - ever used to size a pruning margin, never as a hard fact. + SCORE (*piPositional)[2] : if non-NULL, filled in with an estimate + of the non-material component of the score for each side, + indexed by absolute color (WHITE/BLACK) -- exact if a full + eval ran, a cheap estimate otherwise. On a lazy exit this is + always >= 0 (an optimistic margin); on a full eval it can be + negative (a side whose positional terms net worse than its + raw material), floored at -FUTILITY_BASE_MARGIN_FULL/2 so it can + only shrink a caller's margin so far. Callers must treat it + as an estimate either way; it's only ever used to size a + pruning margin, never as a hard fact. Return value: @@ -5061,8 +5176,8 @@ Return value: PAWN_HASH_ENTRY *pHash; COOR c; ULONG u; - ULONG uColor; - ULONG xColor; + ULONG uColor = pos->uToMove; + ULONG xColor = FLIP(uColor); BITBOARD bb; FLAG fDeferred; #ifdef EVAL_TIME @@ -5079,6 +5194,12 @@ Return value: FLAG fWouldHaveExited = FALSE; SCORE iSavedLazyScore = 0; SCORE iSavedLazyPositional = 0; + // Same idea, kept separate from the pair above: the super-lazy + // exit (added 2026-09-07) is a much cheaper, much-larger-gap check + // than the regular lazy exit and was never hooked into this + // harness -- see RecordSuperLazyMarginSafetySwing below. + FLAG fWouldHaveExitedSuperLazy = FALSE; + SCORE iSavedSuperLazyScore = 0; #endif ASSERT(IS_VALID_SCORE(iAlpha)); ASSERT(IS_VALID_SCORE(iBeta)); @@ -5086,17 +5207,140 @@ Return value: ASSERT((pos->iMaterialBalance[WHITE] * -1) == pos->iMaterialBalance[BLACK]); - pos->uNumTrapped[BLACK] = pos->uNumTrapped[WHITE] = 0; - - pos->iScore[BLACK] = - (pos->uPawnMaterial[BLACK] + pos->uNonPawnMaterial[BLACK]); - pos->iScore[WHITE] = - (pos->uPawnMaterial[WHITE] + pos->uNonPawnMaterial[WHITE]); + pos->iScore[uColor] = + (pos->uPawnMaterial[uColor] + pos->uNonPawnMaterial[uColor]); + pos->iScore[xColor] = + (pos->uPawnMaterial[xColor] + pos->uNonPawnMaterial[xColor]); #ifdef EVAL_DUMP EvalTraceClear(); Trace("Material:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); #endif + // Initialize army scalers here (moved ahead of the super-lazy exit + // point below, 2026-09-08 -- their only inputs, uNonPawnMaterial, + // are already available this early, and the super-lazy margin + // table needs a material bucket before it can run). We skip lazy + // eval if we're too late into an endgame. + pos->uArmyScaler[BLACK] = pos->uNonPawnMaterial[BLACK] - VALUE_KING; + pos->uArmyScaler[WHITE] = pos->uNonPawnMaterial[WHITE] - VALUE_KING; + pos->uArmyScaler[BLACK] /= VALUE_PAWN; + pos->uArmyScaler[WHITE] /= VALUE_PAWN; + ASSERT(!(pos->uArmyScaler[BLACK] & 0x80000000)); + ASSERT(!(pos->uArmyScaler[WHITE] & 0x80000000)); + pos->uArmyScaler[BLACK] = MINU(31, pos->uArmyScaler[BLACK]); + pos->uArmyScaler[WHITE] = MINU(31, pos->uArmyScaler[WHITE]); + ASSERT(pos->uArmyScaler[BLACK] >= 0); + ASSERT(pos->uArmyScaler[BLACK] <= 31); + ASSERT(pos->uArmyScaler[WHITE] >= 0); + ASSERT(pos->uArmyScaler[WHITE] <= 31); + + // Super-lazy exit point. +#ifdef LAZY_EVAL + // 2026-09-08: was a single flat 625 for every material level. + // CALIBRATE_MARGIN_SAFETY data (100 real-game positions, sd 8, + // tests/twic_sample.ep_) showed that's badly unsound at low + // material -- up to 9.3% of super-lazy exits in bare-king-plus-a- + // little-material positions (combined army scaler 0-7) had a real + // swing exceeding 625, max observed 1710 -- while richer positions + // (combined scaler 32+) never came close (max 893, well under + // 625... actually under 900, still comfortably bounded). Table + // indexed the same way _MaterialBucket buckets the calibration + // data (combined army scaler / 8, 8 buckets), so it can be + // re-derived directly from a "calibrate marginsafety" run. Values + // here are the observed max per bucket rounded up with headroom + // (~15-20%), not a hard theoretical bound -- provisional pending a + // larger/deeper calibration run; re-check before trusting this at + // sd well beyond 8. + static const SCORE SUPER_LAZY_MARGIN_BY_ARMY[8] = + { + 2000, 1800, 1800, 1750, 1000, 850, 850, 850, + }; + ULONG uSuperLazyMatBucket = MINU( + 7, (pos->uArmyScaler[WHITE] + pos->uArmyScaler[BLACK]) / 8); + SCORE iSuperLazyMargin = SUPER_LAZY_MARGIN_BY_ARMY[uSuperLazyMatBucket]; + { +#ifdef EVAL_TIME + UINT64 uSuperLazyTimer = SystemReadTimeStampCounter(); +#endif + iScoreForSideToMove = (pos->iScore[uColor] -pos->iScore[xColor]); + ASSERT(IS_VALID_SCORE(iScoreForSideToMove)); + if (iScoreForSideToMove + iSuperLazyMargin < iAlpha) + { + ASSERT(iScoreForSideToMove < iAlpha); + INC(ctx->sCounters.tree.u64SuperLazyEvals); + if (NULL != piPositional) + { + // No positional credit here -- a super-lazy verdict is + // already a confident material-only read that we're + // well outside the window; handing back extra slack + // just re-inflates the qsearch futility margin and + // defeats the point of taking the cheap exit. Let + // FUTILITY_BASE_MARGIN_BY_SOURCE[SUPERLAZY] alone + // govern from here. + (*piPositional)[WHITE] = 0; + (*piPositional)[BLACK] = 0; + ctx->uLastPositionalSource = EVAL_POSITIONAL_SOURCE_SUPERLAZY; + } +#ifdef EVAL_TIME + ctx->sCounters.tree.u64CyclesEvalSuperLazy += + (SystemReadTimeStampCounter() - uSuperLazyTimer); +#endif +#ifdef CALIBRATE_MARGIN_SAFETY + // Don't exit yet -- let the real full eval run below so we + // can measure the true swing against SUPER_LAZY_MARGIN, + // then restore this exact (zeroed) piPositional right + // before `end:` so the caller sees what a normal build + // would have returned. + fWouldHaveExitedSuperLazy = TRUE; + iSavedSuperLazyScore = iScoreForSideToMove; +#else +#ifdef EVAL_TIME + ctx->sCounters.tree.u64CyclesEvalPreLazy += + (SystemReadTimeStampCounter() - uTimer); + ctx->sCounters.tree.u64CyclesSuperLazyExit += + (SystemReadTimeStampCounter() - uTimer); + ctx->sCounters.tree.u64CyclesInEval += + (SystemReadTimeStampCounter() - uTimer); +#endif + goto end; +#endif + } + if (iScoreForSideToMove - iSuperLazyMargin > iBeta) + { + ASSERT(iScoreForSideToMove > iBeta); + INC(ctx->sCounters.tree.u64SuperLazyEvals); + if (NULL != piPositional) + { + (*piPositional)[WHITE] = 0; + (*piPositional)[BLACK] = 0; + ctx->uLastPositionalSource = EVAL_POSITIONAL_SOURCE_SUPERLAZY; + } +#ifdef EVAL_TIME + ctx->sCounters.tree.u64CyclesEvalSuperLazy += + (SystemReadTimeStampCounter() - uSuperLazyTimer); +#endif +#ifdef CALIBRATE_MARGIN_SAFETY + fWouldHaveExitedSuperLazy = TRUE; + iSavedSuperLazyScore = iScoreForSideToMove; +#else +#ifdef EVAL_TIME + ctx->sCounters.tree.u64CyclesEvalPreLazy += + (SystemReadTimeStampCounter() - uTimer); + ctx->sCounters.tree.u64CyclesSuperLazyExit += + (SystemReadTimeStampCounter() - uTimer); + ctx->sCounters.tree.u64CyclesInEval += + (SystemReadTimeStampCounter() - uTimer); +#endif + goto end; +#endif + } +#ifdef EVAL_TIME + ctx->sCounters.tree.u64CyclesEvalSuperLazy += + (SystemReadTimeStampCounter() - uSuperLazyTimer); +#endif + } +#endif + // // Pawn eval. Note: if fDeferred comes back as TRUE then we have // neither cleared nor initialized the attack tables. This is @@ -5273,7 +5517,9 @@ Return value: INC(ctx->sCounters.tree.u64LazyEvals); if (NULL != piPositional) { - *piPositional = iAlphaMargin; + (*piPositional)[WHITE] = iAlphaMargin; + (*piPositional)[BLACK] = iAlphaMargin; + ctx->uLastPositionalSource = EVAL_POSITIONAL_SOURCE_LAZY; } #ifdef EVAL_TIME ctx->sCounters.tree.u64CyclesEvalPreLazy += @@ -5300,7 +5546,9 @@ Return value: INC(ctx->sCounters.tree.u64LazyEvals); if (NULL != piPositional) { - *piPositional = iBetaMargin; + (*piPositional)[WHITE] = iBetaMargin; + (*piPositional)[BLACK] = iBetaMargin; + ctx->uLastPositionalSource = EVAL_POSITIONAL_SOURCE_LAZY; } #ifdef EVAL_TIME ctx->sCounters.tree.u64CyclesEvalPreLazy += @@ -5375,20 +5623,7 @@ Return value: // // Pre-compute some common terms used in per-piece evals: // - // This is a scaler based on the size of the army for each side. - // - pos->uArmyScaler[BLACK] = pos->uNonPawnMaterial[BLACK] - VALUE_KING; - pos->uArmyScaler[WHITE] = pos->uNonPawnMaterial[WHITE] - VALUE_KING; - pos->uArmyScaler[BLACK] /= VALUE_PAWN; - pos->uArmyScaler[WHITE] /= VALUE_PAWN; - ASSERT(!(pos->uArmyScaler[BLACK] & 0x80000000)); - ASSERT(!(pos->uArmyScaler[WHITE] & 0x80000000)); - pos->uArmyScaler[BLACK] = MINU(31, pos->uArmyScaler[BLACK]); - pos->uArmyScaler[WHITE] = MINU(31, pos->uArmyScaler[WHITE]); - ASSERT(pos->uArmyScaler[BLACK] >= 0); - ASSERT(pos->uArmyScaler[BLACK] <= 31); - ASSERT(pos->uArmyScaler[WHITE] >= 0); - ASSERT(pos->uArmyScaler[WHITE] <= 31); + pos->uNumTrapped[BLACK] = pos->uNumTrapped[WHITE] = 0; pos->iReducedMaterialDownScaler[BLACK] = pos->iReducedMaterialDownScaler[WHITE] = 0; @@ -5695,18 +5930,61 @@ Return value: (iScoreForSideToMove * (SCORE)uDrawDist / 16); } - // - // Adjust dynamic positional component. - // - iAlphaMargin = abs(pos->iMaterialBalance[pos->uToMove] - iScoreForSideToMove); + // Adjust dynamic positional component. Unlike the lazy-exit + // margins above (always >= 0 by construction), this can go + // negative -- a side whose positional terms (king safety, pawn + // structure, hanging pieces, ...) net worse than its raw material + // gets a genuinely negative value here. search.c's futility + // margin (FUTILITY_BASE_MARGIN_BY_SOURCE[side's tier] + + // piPositional[side]) deliberately lets that shrink the margin -- + // a positionally-bad side is less likely to be saved by an + // unexamined quiet move -- but floor it so one bad eval term + // can't collapse the margin arbitrarily far; this only ever runs + // on the full-eval exit, so -FUTILITY_BASE_MARGIN_FULL/2 (not the + // lazy/super-lazy tiers) caps the damage at half of that tier's + // base. if (NULL != piPositional) { - *piPositional = iAlphaMargin; + (*piPositional)[WHITE] = + MAX(pos->iScore[WHITE] - + (SCORE)(pos->uPawnMaterial[WHITE] + pos->uNonPawnMaterial[WHITE]), + -(FUTILITY_BASE_MARGIN_FULL / 2)); + (*piPositional)[BLACK] = + MAX(pos->iScore[BLACK] - + (SCORE)(pos->uPawnMaterial[BLACK] + pos->uNonPawnMaterial[BLACK]), + -(FUTILITY_BASE_MARGIN_FULL / 2)); + ctx->uLastPositionalSource = EVAL_POSITIONAL_SOURCE_FULL; } g_Options.iLastEvalScore = iScoreForSideToMove; #ifdef CALIBRATE_MARGIN_SAFETY - if (TRUE == fWouldHaveExited) + if (TRUE == fWouldHaveExitedSuperLazy) + { + // + // Real (non-calibration) code checks super-lazy first and + // returns immediately on a hit -- so if this call would *also* + // have satisfied the regular lazy condition below, super-lazy + // is still what a normal build actually returns. Record/ + // restore this one, not the regular-lazy save in the other + // branch. + // + RecordSuperLazyMarginSafetySwing( + pos, abs(iScoreForSideToMove - iSavedSuperLazyScore), + iSuperLazyMargin); + iScoreForSideToMove = iSavedSuperLazyScore; + if (NULL != piPositional) + { + (*piPositional)[WHITE] = 0; + (*piPositional)[BLACK] = 0; + // CALIBRATE_MARGIN_SAFETY builds: the FULL tag set above + // reflects the fallthrough that just ran for measurement + // purposes, not what a normal build actually returns here + // -- fix it back up to match the restored (super-lazy) + // values. + ctx->uLastPositionalSource = EVAL_POSITIONAL_SOURCE_SUPERLAZY; + } + } + else if (TRUE == fWouldHaveExited) { // // The real swing: how far the true, full-eval score actually @@ -5725,7 +6003,9 @@ Return value: iScoreForSideToMove = iSavedLazyScore; if (NULL != piPositional) { - *piPositional = iSavedLazyPositional; + (*piPositional)[WHITE] = iSavedLazyPositional; + (*piPositional)[BLACK] = iSavedLazyPositional; + ctx->uLastPositionalSource = EVAL_POSITIONAL_SOURCE_LAZY; } } #endif -- cgit v1.3