Skip to main content

atomic_movegen/
pext.rs

1//! PEXT-based sliding piece attacks (BMI2 instruction).
2//!
3//! When BMI2 is available, the `pext` instruction replaces the magic
4//! multiplication + shift with a single instruction, giving ~2× speedup
5//! for sliding piece attacks.
6//!
7//! Tables are built at init time using a software PEXT emulation, so
8//! they work regardless of CPU support. The hot-path lookup uses hardware
9//! PEXT when available via `#[target_feature(enable = "bmi2")]`.
10//!
11//! On non-x86_64 the entire module is dead code (the caller in `attacks`
12//! is gated behind `#[cfg(target_arch = "x86_64")]`).
13
14#![cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
15
16use crate::magic::{self, BISHOP_DIRS, BISHOP_MASKS, ROOK_DIRS, ROOK_MASKS};
17use crate::types::*;
18use std::sync::OnceLock;
19
20/// Returns `true` if the CPU supports the BMI2 instruction set (PEXT).
21///
22/// On non-x86_64 architectures this always returns `false`.
23pub(crate) fn has_bmi2() -> bool {
24    #[cfg(target_arch = "x86_64")]
25    {
26        std::arch::is_x86_feature_detected!("bmi2")
27    }
28    #[cfg(not(target_arch = "x86_64"))]
29    {
30        false
31    }
32}
33
34/// Software emulation of the `pext` instruction.
35///
36/// Extracts bits from `val` at positions where `mask` has 1 bits, and
37/// compacts them into a contiguous index starting from bit 0.
38fn pext_soft(val: u64, mask: u64) -> u64 {
39    let mut result = 0u64;
40    let mut bit = 0u64;
41    let mut m = mask;
42    while m != 0 {
43        // Isolate lowest set bit.
44        let lsb = m & m.wrapping_neg();
45        if val & lsb != 0 {
46            result |= 1 << bit;
47        }
48        bit += 1;
49        // Clear the lowest set bit.
50        m ^= lsb;
51    }
52    result
53}
54
55/// Compiled-time layout for a PEXT-indexed table.
56///
57/// Stores the popcount and offset for each square, plus the total table
58/// size, allowing O(1) lookup of a square's PEXT-indexed attack range.
59struct PextLayout {
60    popcounts: [u32; 64],
61    offsets: [usize; 64],
62    total: usize,
63}
64
65/// Compute popcounts and offsets from masks at compile time.
66///
67/// For each square the occupancy mask has `p` bits set, requiring `2^p`
68/// entries in the PEXT attack table. This function computes the cumulative
69/// offset and popcount for every square.
70const fn compute_pext_layout(masks: &[Bitboard; 64]) -> PextLayout {
71    let mut popcounts = [0u32; 64];
72    let mut offsets = [0usize; 64];
73    let mut total = 0usize;
74    let mut i = 0;
75    while i < 64 {
76        let pc = masks[i].0.count_ones();
77        popcounts[i] = pc;
78        offsets[i] = total;
79        total += 1usize << pc;
80        i += 1;
81    }
82    PextLayout {
83        popcounts,
84        offsets,
85        total,
86    }
87}
88
89const ROOK_LAYOUT: PextLayout = compute_pext_layout(&ROOK_MASKS);
90const BISHOP_LAYOUT: PextLayout = compute_pext_layout(&BISHOP_MASKS);
91
92/// Build a PEXT-indexed attack table for a given piece type.
93fn build_pext_table(
94    directions: &[(i8, i8)],
95    masks: &[Bitboard; 64],
96    offsets: &[usize; 64],
97    total_size: usize,
98) -> Box<[Bitboard]> {
99    let mut table = vec![Bitboard::EMPTY; total_size].into_boxed_slice();
100
101    for sq in 0..64 {
102        let mask = masks[sq].0;
103        let offset = offsets[sq];
104        let sq_enum = Square::from_u8(sq as u8);
105
106        // Enumerate all subsets of the mask using the carry-rippler trick.
107        let mut subset = 0u64;
108        loop {
109            let attacks = magic::sliding_attack(directions, sq_enum, Bitboard(subset));
110            let idx = offset + pext_soft(subset, mask) as usize;
111            table[idx] = attacks;
112
113            // Carry-rippler: compute next subset
114            subset = subset.wrapping_sub(mask) & mask;
115            if subset == 0 {
116                break;
117            }
118        }
119    }
120
121    table
122}
123
124static ROOK_PEXT_TABLE: OnceLock<&[Bitboard]> = OnceLock::new();
125static BISHOP_PEXT_TABLE: OnceLock<&[Bitboard]> = OnceLock::new();
126
127/// Initialize the PEXT attack tables. Must be called before any lookup.
128pub(crate) fn init() {
129    _ = ROOK_PEXT_TABLE.set(Box::leak(build_pext_table(
130        &ROOK_DIRS,
131        &ROOK_MASKS,
132        &ROOK_LAYOUT.offsets,
133        ROOK_LAYOUT.total,
134    )));
135    _ = BISHOP_PEXT_TABLE.set(Box::leak(build_pext_table(
136        &BISHOP_DIRS,
137        &BISHOP_MASKS,
138        &BISHOP_LAYOUT.offsets,
139        BISHOP_LAYOUT.total,
140    )));
141}
142
143#[cfg(target_arch = "x86_64")]
144#[target_feature(enable = "bmi2")]
145unsafe fn bishop_attacks_pext_impl(sq: Square, occupied: Bitboard) -> Bitboard {
146    let table = BISHOP_PEXT_TABLE
147        .get()
148        .expect("PEXT tables not initialized — call attacks::init()");
149    let sq_idx = sq as usize;
150    let mask = BISHOP_MASKS[sq_idx];
151    let occ = occupied & mask;
152    let idx = core::arch::x86_64::_pext_u64(occ.0, mask.0) as usize;
153    table[BISHOP_LAYOUT.offsets[sq_idx] + idx]
154}
155
156#[cfg(target_arch = "x86_64")]
157#[target_feature(enable = "bmi2")]
158unsafe fn rook_attacks_pext_impl(sq: Square, occupied: Bitboard) -> Bitboard {
159    let table = ROOK_PEXT_TABLE
160        .get()
161        .expect("PEXT tables not initialized — call attacks::init()");
162    let sq_idx = sq as usize;
163    let mask = ROOK_MASKS[sq_idx];
164    let occ = occupied & mask;
165    let idx = core::arch::x86_64::_pext_u64(occ.0, mask.0) as usize;
166    table[ROOK_LAYOUT.offsets[sq_idx] + idx]
167}
168
169// Non-x86_64 stubs that should never be called (has_bmi2() returns false).
170#[cfg(not(target_arch = "x86_64"))]
171unsafe fn bishop_attacks_pext_impl(_sq: Square, _occupied: Bitboard) -> Bitboard {
172    unreachable!("PEXT not available on non-x86_64")
173}
174
175#[cfg(not(target_arch = "x86_64"))]
176unsafe fn rook_attacks_pext_impl(_sq: Square, _occupied: Bitboard) -> Bitboard {
177    unreachable!("PEXT not available on non-x86_64")
178}
179
180/// Return the attack set for a bishop on `sq` given the `occupied` board,
181/// using the BMI2 `pext` instruction.
182///
183/// # Panics
184///
185/// Panics if the PEXT tables have not been initialized (call `attacks::init()` first).
186/// On non-x86_64 platforms, this function panics unconditionally.
187/// The caller MUST ensure the CPU supports BMI2 before calling (checked during `init()`).
188#[inline(always)]
189pub(crate) fn bishop_attacks_pext(sq: Square, occupied: Bitboard) -> Bitboard {
190    // SAFETY: This function is only called after init() has verified BMI2 support
191    // via has_bmi2(), which guarantees BMI2 is available for the process lifetime.
192    unsafe { bishop_attacks_pext_impl(sq, occupied) }
193}
194
195/// Return the attack set for a rook on `sq` given the `occupied` board,
196/// using the BMI2 `pext` instruction.
197///
198/// # Panics
199///
200/// Panics if the PEXT tables have not been initialized (call `attacks::init()` first).
201/// On non-x86_64 platforms, this function panics unconditionally.
202/// The caller MUST ensure the CPU supports BMI2 before calling (checked during `init()`).
203#[inline(always)]
204pub(crate) fn rook_attacks_pext(sq: Square, occupied: Bitboard) -> Bitboard {
205    // SAFETY: This function is only called after init() has verified BMI2 support
206    // via has_bmi2(), which guarantees BMI2 is available for the process lifetime.
207    unsafe { rook_attacks_pext_impl(sq, occupied) }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_pext_soft_identity() {
216        // PEXT with all-ones mask should return the original value.
217        assert_eq!(pext_soft(0xDEADBEEF, 0xFFFFFFFF), 0xDEADBEEF);
218    }
219
220    #[test]
221    fn test_pext_soft_compact() {
222        // Extract bits 0, 2, 4 from 0b10101 = 21 -> should give 0b111 = 7
223        assert_eq!(pext_soft(0b10101, 0b10101), 0b111);
224    }
225
226    #[test]
227    fn test_pext_soft_sparse() {
228        // val = 0xFF, mask = 0x0101010101010101 (bits 0,8,16,24,32,40,48,56)
229        // Only bit 0 of val overlaps with mask bit 0 -> result = 1
230        assert_eq!(pext_soft(0xFF, 0x0101010101010101), 1);
231    }
232
233    #[test]
234    fn test_pext_soft_zero() {
235        assert_eq!(pext_soft(0, 0xFFFFFFFF), 0);
236        assert_eq!(pext_soft(0xFFFFFFFF, 0), 0);
237    }
238
239    /// Verify that the PEXT tables produce the same results as the
240    /// loop-based reference for every square and every occupancy.
241    #[test]
242    fn test_pext_vs_loop_bishop() {
243        // Initialize tables.
244        super::init();
245        let table = BISHOP_PEXT_TABLE
246            .get()
247            .expect("PEXT tables not initialized");
248        for sq_idx in 0..64 {
249            let sq = Square::from_u8(sq_idx as u8);
250            let mask = BISHOP_MASKS[sq_idx].0;
251            let size = 1usize << BISHOP_LAYOUT.popcounts[sq_idx];
252            let mut count = 0;
253            let mut subset = 0u64;
254            loop {
255                let occ = Bitboard(subset);
256                // Compute using PEXT table (software index)
257                let pext_idx = BISHOP_LAYOUT.offsets[sq_idx] + pext_soft(subset, mask) as usize;
258                let pext_atk = table[pext_idx];
259                let loop_atk = magic::sliding_attack(&BISHOP_DIRS, sq, occ);
260                assert_eq!(
261                    pext_atk, loop_atk,
262                    "Bishop PEXT mismatch at sq={:?}, occ=0x{:x}",
263                    sq, subset
264                );
265                count += 1;
266                subset = subset.wrapping_sub(mask) & mask;
267                if subset == 0 {
268                    break;
269                }
270            }
271            assert_eq!(count, size, "Bishop PEXT count mismatch at sq={:?}", sq);
272        }
273    }
274
275    #[test]
276    fn test_pext_vs_loop_rook() {
277        super::init();
278        let table = ROOK_PEXT_TABLE.get().expect("PEXT tables not initialized");
279        for sq_idx in 0..64 {
280            let sq = Square::from_u8(sq_idx as u8);
281            let mask = ROOK_MASKS[sq_idx].0;
282            let size = 1usize << ROOK_LAYOUT.popcounts[sq_idx];
283            let mut count = 0;
284            let mut subset = 0u64;
285            loop {
286                let occ = Bitboard(subset);
287                let pext_idx = ROOK_LAYOUT.offsets[sq_idx] + pext_soft(subset, mask) as usize;
288                let pext_atk = table[pext_idx];
289                let loop_atk = magic::sliding_attack(&ROOK_DIRS, sq, occ);
290                assert_eq!(
291                    pext_atk, loop_atk,
292                    "Rook PEXT mismatch at sq={:?}, occ=0x{:x}",
293                    sq, subset
294                );
295                count += 1;
296                subset = subset.wrapping_sub(mask) & mask;
297                if subset == 0 {
298                    break;
299                }
300            }
301            assert_eq!(count, size, "Rook PEXT count mismatch at sq={:?}", sq);
302        }
303    }
304
305    /// If BMI2 is available, verify the hardware PEXT path matches the
306    /// loop-based reference on a subset of positions.
307    #[test]
308    fn test_pext_hardware_vs_loop() {
309        if !has_bmi2() {
310            return;
311        }
312        // Initialize tables.
313        crate::attacks::init();
314
315        // Test a representative set of squares and occupancies.
316        for sq_idx in [0, 7, 9, 27, 36, 56, 63] {
317            let sq = Square::from_u8(sq_idx as u8);
318            for occ_val in [0u64, 0xFF, 0xFFFF, 0xDEADBEEF, 0xFFFFFFFFFFFFFFFF] {
319                let occ = Bitboard(occ_val);
320                let pext_atk = bishop_attacks_pext(sq, occ);
321                let loop_atk = magic::sliding_attack(&BISHOP_DIRS, sq, occ);
322                assert_eq!(
323                    pext_atk, loop_atk,
324                    "Bishop HW PEXT mismatch at sq={:?}, occ=0x{:x}",
325                    sq, occ_val
326                );
327
328                let pext_atk = rook_attacks_pext(sq, occ);
329                let loop_atk = magic::sliding_attack(&ROOK_DIRS, sq, occ);
330                assert_eq!(
331                    pext_atk, loop_atk,
332                    "Rook HW PEXT mismatch at sq={:?}, occ=0x{:x}",
333                    sq, occ_val
334                );
335            }
336        }
337    }
338}