Skip to main content

subetha_cxc/
shared_deque_urd.rs

1//! `SharedDequeUrd` - UMWAIT Rendezvous Deque, MMF-backed.
2//!
3//! Per-thief mailbox cache lines instead of a shared deque. Each
4//! mailbox is one 64-byte line carrying state (8 B) +
5//! `MAILBOX_ITEMS = 3` line items (48 B) + 8 B trailing padding.
6//! The owner picks a mailbox by round-robin (or by an explicit
7//! target index) and writes the items in; the addressed thief
8//! observes the cache-line transition and reads its items. There is
9//! **no shared head/tail counter on the steal path** - each thief
10//! has its own state byte and never CASes a contended atomic.
11//!
12//! # Wait strategy: runtime dispatch on WAITPKG
13//!
14//! Thieves idle on their mailbox's state byte. The wait primitive
15//! is chosen at runtime by [`subetha_core::has_waitpkg`]:
16//!
17//! - **WAITPKG available** (Intel Tremont / Tiger Lake+ and
18//!   AMD Zen 5+): thief uses `UMONITOR` + `UMWAIT` to halt until
19//!   the cache line transitions OR a TSC deadline fires.
20//!   Power-efficient; the thief does not burn pipeline slots
21//!   polling.
22//! - **WAITPKG not available** (most pre-2020 silicon including
23//!   AMD Zen+/2/3/4): thief uses [`std::hint::spin_loop`] (`PAUSE`
24//!   on x86) in a tight Acquire-load loop on the state byte.
25//!
26//! Both branches end the wait when `state` carries the ready bit
27//! for the expected epoch.
28//!
29//! # Why this shape vs `SharedDeque` / `SharedDequeKhpd`
30//!
31//! `SharedDeque` (Chase-Lev) and `SharedDequeKhpd` are *pull-based*:
32//! thieves CAS the deque to discover work. URD is *push-based*: the
33//! owner picks the target thief by writing its mailbox. Two
34//! architectural consequences:
35//!
36//! 1. **No CAS contention at the steal site.** N thieves on a
37//!    shared deque CAS the same head; under contention each push
38//!    racing N thieves can take O(N) failed CASes. URD's per-thief
39//!    mailbox has zero contention because the owner is the only
40//!    writer and the thief is the only reader.
41//! 2. **Owner-controlled distribution.** Round-robin /
42//!    locality-aware / variance-driven targeting is the owner's
43//!    choice, not a thief's victim-pick. The orchestrator gets
44//!    explicit say in which thief does what.
45//!
46//! Trade-offs: the thief is bound to one mailbox (no work-stealing
47//! between mailboxes), the owner pays one mailbox-spin per publish
48//! if the previous batch has not been consumed yet, and the
49//! per-mailbox slot count is fixed at [`MAILBOX_ITEMS`].
50
51#![allow(clippy::missing_errors_doc)]
52
53use std::fs::{File, OpenOptions};
54use std::io;
55use std::path::Path;
56use std::sync::atomic::{AtomicU64, Ordering};
57
58use memmap2::{MmapMut, MmapOptions};
59
60use crate::shared_deque_khpd::LineItem;
61use subetha_core::{has_movdir64b, has_waitpkg};
62
63/// Magic byte sequence marking a valid URD file. ASCII 'WURD' + ver.
64pub const URD_MAGIC: u64 = 0x5755_5244_0000_0001;
65
66/// Cache-line size; one mailbox per cache line.
67pub const URD_MAILBOX_SIZE: usize = 64;
68
69/// Items per mailbox: state (8 B) + 3 * 16 = 56 B; 8 B trailing pad.
70pub const MAILBOX_ITEMS: usize = 3;
71
72/// Wait strategy chosen at runtime per CPUID.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum WaitStrategy {
75    /// `std::hint::spin_loop` (`PAUSE` on x86). Universally
76    /// available fallback.
77    PauseSpin,
78    /// `UMONITOR` + `UMWAIT`. Available on Intel Tremont /
79    /// Tiger Lake+ and AMD Zen 5+; detected via CPUID leaf 7 ECX
80    /// bit 5.
81    Waitpkg,
82}
83
84impl WaitStrategy {
85    /// Returns the best wait strategy for this host.
86    pub fn pick() -> Self {
87        if has_waitpkg() {
88            Self::Waitpkg
89        } else {
90            Self::PauseSpin
91        }
92    }
93}
94
95/// Publish strategy chosen at runtime per CPUID.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum PublishStrategy {
98    /// Byte-by-byte store path: the publisher writes items into the
99    /// mailbox slots through normal cached stores, then Release-
100    /// stores the state word to READY. Universally available.
101    Scalar,
102    /// `MOVDIR64B` path: the publisher builds a 64-byte source line
103    /// containing the new state plus all items, then issues one
104    /// `MOVDIR64B` instruction that atomically writes the entire
105    /// 64-byte mailbox cache line as a Write-Combining store. On
106    /// cross-CCX delivery the line is written directly to LLC,
107    /// eliminating the M-state coherence transfer the byte-by-byte
108    /// path pays. Available on Intel Tremont (2019) / Tiger Lake
109    /// (2020) and later Intel cores and AMD Zen 5 (2024) and later
110    /// AMD cores; detected via CPUID leaf 7 ECX bit 28.
111    Movdir64b,
112}
113
114impl PublishStrategy {
115    /// Returns the best publish strategy for this host.
116    pub fn pick() -> Self {
117        if has_movdir64b() {
118            Self::Movdir64b
119        } else {
120            Self::Scalar
121        }
122    }
123}
124
125/// State word packed-bit layout: top 32 bits = epoch, bits 16..32 =
126/// `n_items`, bits 0..16 = claim (0 = EMPTY, 1 = READY).
127const STATE_EMPTY: u64 = 0;
128const CLAIM_READY: u64 = 1;
129
130/// File header. Cache-line aligned.
131#[repr(C, align(64))]
132pub struct UrdHeader {
133    /// Magic constant.
134    pub magic: u64,
135    /// Number of mailboxes (one per thief).
136    pub n_mailboxes: u64,
137    /// Pid of the owner process; informational. Cleared on
138    /// `close_owner()`.
139    pub owner_pid: AtomicU64,
140    /// Shutdown epoch counter.
141    pub epoch: AtomicU64,
142    /// Padding to push `rr_cursor` to its own cache line.
143    pub _pad_meta: [u8; 24],
144    /// Round-robin cursor the owner uses to pick the next target
145    /// mailbox when no explicit target is supplied.
146    pub rr_cursor: AtomicU64,
147    /// Padding to round to two cache lines.
148    pub _pad_rr: [u8; 56],
149}
150
151/// One per-thief mailbox cache line.
152#[repr(C, align(64))]
153pub struct Mailbox {
154    /// State word: `(epoch:32) << 32 | (n_items:16) << 16 | claim:16`.
155    pub state: AtomicU64,
156    /// Inline items.
157    pub items: [LineItem; MAILBOX_ITEMS],
158    /// Trailing padding.
159    pub _pad: [u8; 8],
160}
161
162/// Total file size for a URD with `n_mailboxes` mailboxes.
163pub const fn urd_file_size(n_mailboxes: usize) -> usize {
164    std::mem::size_of::<UrdHeader>() + n_mailboxes * URD_MAILBOX_SIZE
165}
166
167/// Outcome of [`SharedDequeUrd::publish_to`].
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum PublishError {
170    /// Caller-supplied target mailbox index is out of range.
171    BadTarget(usize),
172    /// More items than [`MAILBOX_ITEMS`] passed in one publish call.
173    TooManyItems,
174    /// Payload exceeded [`super::shared_deque_khpd::KHPD_ITEM_BYTES`].
175    PayloadTooLarge,
176}
177
178/// Outcome of [`SharedDequeUrd::drain_mailbox`].
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum Drain {
181    /// Got items.
182    Success(DrainResult),
183    /// Mailbox empty (no published items past this thief's last
184    /// consume).
185    Empty,
186}
187
188/// Items pulled from a mailbox.
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub struct DrainResult {
191    /// How many of the `items` slots are filled.
192    pub n_items: usize,
193    /// The items.
194    pub items: [LineItem; MAILBOX_ITEMS],
195}
196
197/// MMF-backed UMWAIT Rendezvous Deque. Single owner, N
198/// pre-configured thieves.
199pub struct SharedDequeUrd {
200    _file: File,
201    mmap: MmapMut,
202    n_mailboxes: usize,
203    wait_strategy: WaitStrategy,
204    publish_strategy: PublishStrategy,
205}
206
207// SAFETY: all fields are Send. The mmap handle is Send + Sync per
208// memmap2. Every mailbox access goes through the per-mailbox
209// state-atomic protocol.
210unsafe impl Send for SharedDequeUrd {}
211// SAFETY: same justification as the Send impl directly above.
212unsafe impl Sync for SharedDequeUrd {}
213
214impl SharedDequeUrd {
215    /// Create a fresh URD file with `n_mailboxes` mailboxes (one
216    /// per intended thief). Minimum 1. The round-robin cursor
217    /// reduces modulo `n_mailboxes` (no pow2 requirement so a
218    /// single-thief bench can use n = 1).
219    pub fn create<P: AsRef<Path>>(path: P, n_mailboxes: usize) -> io::Result<Self> {
220        let n_mailboxes = n_mailboxes.max(1);
221        let size = urd_file_size(n_mailboxes);
222
223        let file = OpenOptions::new()
224            .read(true)
225            .write(true)
226            .create(true)
227            .truncate(true)
228            .open(path.as_ref())?;
229        file.set_len(size as u64)?;
230
231        // SAFETY: `map_mut` is unsafe because the kernel cannot
232        // prevent another process from truncating the file. This
233        // call site upholds the soundness contract by writing only
234        // through the per-mailbox state-atomic protocol.
235        let mut mmap = unsafe { MmapOptions::new().len(size).map_mut(&file)? };
236
237        let header_ptr = mmap.as_mut_ptr() as *mut UrdHeader;
238        // SAFETY: mmap is page-aligned (>= 64-byte alignment); the
239        // map covers `urd_file_size(n_mailboxes)` bytes by
240        // construction.
241        unsafe {
242            (*header_ptr).magic = URD_MAGIC;
243            (*header_ptr).n_mailboxes = n_mailboxes as u64;
244            (*header_ptr).owner_pid = AtomicU64::new(std::process::id() as u64);
245            (*header_ptr).epoch = AtomicU64::new(0);
246            std::ptr::write_bytes((*header_ptr)._pad_meta.as_mut_ptr(), 0, 24);
247            (*header_ptr).rr_cursor = AtomicU64::new(0);
248            std::ptr::write_bytes((*header_ptr)._pad_rr.as_mut_ptr(), 0, 56);
249        }
250
251        // Zero all mailboxes (state == STATE_EMPTY).
252        let mailboxes_start = std::mem::size_of::<UrdHeader>();
253        // SAFETY: `write_bytes` covers the unwritten tail of the
254        // map.
255        unsafe {
256            std::ptr::write_bytes(
257                mmap.as_mut_ptr().add(mailboxes_start),
258                0,
259                n_mailboxes * URD_MAILBOX_SIZE,
260            );
261        }
262
263        mmap.flush()?;
264        let wait_strategy = WaitStrategy::pick();
265        let publish_strategy = PublishStrategy::pick();
266        Ok(Self {
267            _file: file,
268            mmap,
269            n_mailboxes,
270            wait_strategy,
271            publish_strategy,
272        })
273    }
274
275    /// Open an existing URD file.
276    pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
277        let file = OpenOptions::new()
278            .read(true)
279            .write(true)
280            .open(path.as_ref())?;
281        let size = file.metadata()?.len() as usize;
282        if size < std::mem::size_of::<UrdHeader>() {
283            return Err(io::Error::new(
284                io::ErrorKind::InvalidData,
285                "urd file too small",
286            ));
287        }
288        // SAFETY: same protocol-only-access justification as
289        // `create`.
290        let mmap = unsafe { MmapOptions::new().len(size).map_mut(&file)? };
291        let header_ptr = mmap.as_ptr() as *const UrdHeader;
292        // SAFETY: map size verified to cover header.
293        let (magic, n_mailboxes) =
294            unsafe { ((*header_ptr).magic, (*header_ptr).n_mailboxes as usize) };
295        if magic != URD_MAGIC {
296            return Err(io::Error::new(
297                io::ErrorKind::InvalidData,
298                format!("urd magic mismatch {magic:#x}"),
299            ));
300        }
301        if n_mailboxes == 0 {
302            return Err(io::Error::new(
303                io::ErrorKind::InvalidData,
304                "urd n_mailboxes must be >= 1",
305            ));
306        }
307        if size < urd_file_size(n_mailboxes) {
308            return Err(io::Error::new(
309                io::ErrorKind::InvalidData,
310                "urd file size below header expected",
311            ));
312        }
313        let wait_strategy = WaitStrategy::pick();
314        let publish_strategy = PublishStrategy::pick();
315        Ok(Self {
316            _file: file,
317            mmap,
318            n_mailboxes,
319            wait_strategy,
320            publish_strategy,
321        })
322    }
323
324    /// Number of configured mailboxes.
325    pub fn n_mailboxes(&self) -> usize {
326        self.n_mailboxes
327    }
328
329    /// Wait strategy this URD instance picked (per CPUID).
330    pub fn wait_strategy(&self) -> WaitStrategy {
331        self.wait_strategy
332    }
333
334    /// Publish strategy this URD instance picked (per CPUID).
335    pub fn publish_strategy(&self) -> PublishStrategy {
336        self.publish_strategy
337    }
338
339    /// Owner pid at create time, or 0 after `close_owner()`.
340    pub fn owner_pid(&self) -> u64 {
341        self.header().owner_pid.load(Ordering::Acquire)
342    }
343
344    /// Owner shutdown: zero pid + advance epoch.
345    pub fn close_owner(&self) {
346        self.header().owner_pid.store(0, Ordering::Release);
347        self.header().epoch.fetch_add(1, Ordering::Release);
348    }
349
350    fn header(&self) -> &UrdHeader {
351        // SAFETY: header is at the start of the map.
352        unsafe { &*(self.mmap.as_ptr() as *const UrdHeader) }
353    }
354
355    fn mailbox_ptr(&self, idx: usize) -> *mut Mailbox {
356        let off = std::mem::size_of::<UrdHeader>() + idx * URD_MAILBOX_SIZE;
357        // SAFETY: `idx < n_mailboxes` by caller contract; `off` is
358        // in-bounds + 64-byte aligned.
359        unsafe { self.mmap.as_ptr().add(off) as *mut Mailbox }
360    }
361
362    fn mailbox(&self, idx: usize) -> &Mailbox {
363        // SAFETY: same as `mailbox_ptr`.
364        unsafe { &*self.mailbox_ptr(idx) }
365    }
366
367    /// Owner-side: publish `items` to mailbox `target`. Spins until
368    /// the mailbox is EMPTY (the previous batch has been consumed),
369    /// then publishes via the per-host
370    /// [`PublishStrategy`](Self::publish_strategy):
371    ///
372    /// - [`PublishStrategy::Movdir64b`]: builds a 64-byte source
373    ///   line and atomically writes the whole mailbox cache line
374    ///   via the `MOVDIR64B` instruction (one Write-Combining
375    ///   store, no RFO).
376    /// - [`PublishStrategy::Scalar`]: writes items via cached
377    ///   stores, then Release-stores the state word to READY (two-
378    ///   step protocol).
379    ///
380    /// Returns the number of items published.
381    pub fn publish_to(
382        &self,
383        target: usize,
384        items: &[LineItem],
385    ) -> Result<usize, PublishError> {
386        if target >= self.n_mailboxes {
387            return Err(PublishError::BadTarget(target));
388        }
389        if items.len() > MAILBOX_ITEMS {
390            return Err(PublishError::TooManyItems);
391        }
392        if items.is_empty() {
393            return Ok(0);
394        }
395        let mb = self.mailbox(target);
396        // Spin-wait for the mailbox to be EMPTY (previous batch
397        // consumed). The owner is on the WRITE side so a brief
398        // PAUSE-spin is the right primitive here regardless of the
399        // thief's WAITPKG availability.
400        loop {
401            let s = mb.state.load(Ordering::Acquire);
402            if s == STATE_EMPTY {
403                break;
404            }
405            std::hint::spin_loop();
406        }
407        let epoch = self.header().epoch.load(Ordering::Relaxed);
408        let new_state = (epoch << 32) | ((items.len() as u64) << 16) | CLAIM_READY;
409
410        match self.publish_strategy {
411            PublishStrategy::Movdir64b => {
412                // SAFETY: target index is bounds-checked above;
413                // mailbox_ptr returns an in-bounds aligned pointer.
414                // The strategy is `Movdir64b` only when
415                // `has_movdir64b()` returned true, so emitting the
416                // instruction is safe.
417                unsafe {
418                    self.publish_movdir64b(target, items, new_state);
419                }
420            }
421            PublishStrategy::Scalar => {
422                // SAFETY: mailbox is in-bounds + aligned; we have
423                // exclusive access until the Release-store below
424                // transitions state to READY.
425                unsafe {
426                    let mb_ptr = self.mailbox_ptr(target);
427                    for (i, item) in items.iter().enumerate() {
428                        (*mb_ptr).items[i] = *item;
429                    }
430                }
431                mb.state.store(new_state, Ordering::Release);
432            }
433        }
434        Ok(items.len())
435    }
436
437    /// Build a 64-byte source line on the stack carrying the new
438    /// `state` plus the items, then atomically write it to the
439    /// destination mailbox via `MOVDIR64B`. `SFENCE` drains the WC
440    /// store buffer so the publish is globally observable before the
441    /// function returns.
442    ///
443    /// # Safety
444    ///
445    /// Caller must have validated that the per-host
446    /// [`PublishStrategy`] is `Movdir64b` (i.e. `has_movdir64b()`
447    /// returned true), that `target < self.n_mailboxes`, and that
448    /// `items.len() <= MAILBOX_ITEMS`.
449    #[inline(always)]
450    unsafe fn publish_movdir64b(
451        &self,
452        target: usize,
453        items: &[LineItem],
454        new_state: u64,
455    ) {
456        // Source line: layout-compatible with `Mailbox`. Built on
457        // the stack so the MOVDIR64B source is L1d-warm.
458        #[repr(C, align(64))]
459        struct SrcLine {
460            state: u64,
461            items: [LineItem; MAILBOX_ITEMS],
462            _pad: [u8; 8],
463        }
464        let mut src = SrcLine {
465            state: new_state,
466            items: [LineItem::default(); MAILBOX_ITEMS],
467            _pad: [0u8; 8],
468        };
469        for (i, item) in items.iter().enumerate() {
470            src.items[i] = *item;
471        }
472
473        let dst_ptr = self.mailbox_ptr(target) as *mut u8;
474        let src_ptr = &src as *const SrcLine as *const u8;
475
476        #[cfg(target_arch = "x86_64")]
477        {
478            // SAFETY: `MOVDIR64B` writes 64 bytes from `[src_ptr]`
479            // to `[dst_ptr]`. Both pointers are 64-byte aligned (the
480            // `Mailbox` is `#[repr(C, align(64))]` and `SrcLine` is
481            // `#[repr(C, align(64))]`). `nostack` + `preserves_flags`
482            // lets the optimizer schedule freely. `SFENCE` drains the
483            // WC store buffer.
484            unsafe {
485                core::arch::asm!(
486                    "movdir64b {dst}, [{src}]",
487                    "sfence",
488                    dst = in(reg) dst_ptr,
489                    src = in(reg) src_ptr,
490                    options(nostack, preserves_flags),
491                );
492            }
493        }
494        #[cfg(not(target_arch = "x86_64"))]
495        {
496            _ = dst_ptr;
497            _ = src_ptr;
498            unreachable!(
499                "publish_movdir64b reached on non-x86_64 host; \
500                 PublishStrategy::pick() returns Scalar there"
501            );
502        }
503    }
504
505    /// Owner-side: publish `items` to the next round-robin mailbox.
506    /// Returns `(target, n_published)`.
507    pub fn publish_round_robin(
508        &self,
509        items: &[LineItem],
510    ) -> Result<(usize, usize), PublishError> {
511        let cursor = self.header().rr_cursor.fetch_add(1, Ordering::Relaxed) as usize;
512        // Use modulo (not bit-mask) so non-pow2 mailbox counts
513        // work; the common case is `n_mailboxes` being a small
514        // constant the compiler reduces to a strength-reduced
515        // multiply.
516        let target = cursor % self.n_mailboxes;
517        let n = self.publish_to(target, items)?;
518        Ok((target, n))
519    }
520
521    /// Thief-side: drain own mailbox if it has READY items.
522    /// `mailbox_idx` is the thief's pre-assigned mailbox. Returns
523    /// [`Drain::Empty`] when the state byte is EMPTY (no work
524    /// published yet).
525    pub fn drain_mailbox(&self, mailbox_idx: usize) -> Drain {
526        if mailbox_idx >= self.n_mailboxes {
527            return Drain::Empty;
528        }
529        let mb = self.mailbox(mailbox_idx);
530        let s = mb.state.load(Ordering::Acquire);
531        if s & 0xFFFF != CLAIM_READY {
532            return Drain::Empty;
533        }
534        let n_items = ((s >> 16) & 0xFFFF) as usize;
535        let n_items = n_items.min(MAILBOX_ITEMS);
536        // SAFETY: state's READY bit is set; the publisher's
537        // Release-store synchronizes-with our Acquire-load above so
538        // item bytes are visible.
539        let result = unsafe {
540            DrainResult {
541                n_items,
542                items: (*self.mailbox_ptr(mailbox_idx)).items,
543            }
544        };
545        // Release the mailbox: state -> EMPTY. The owner's next
546        // `publish_to(target)` spin sees EMPTY and writes.
547        mb.state.store(STATE_EMPTY, Ordering::Release);
548        Drain::Success(result)
549    }
550
551    /// Thief-side: block (per the host's [`WaitStrategy`]) until
552    /// the mailbox transitions to READY, then drain it.
553    ///
554    /// On WAITPKG-capable hardware the thief uses `UMONITOR` +
555    /// `UMWAIT` to halt; otherwise it uses `PAUSE`-spin. The
556    /// deadline is expressed as the absolute TSC value at which
557    /// `UMWAIT` returns even if the line has not transitioned;
558    /// `u64::MAX` means "no deadline" (wait indefinitely - protocol
559    /// risk if the owner never publishes).
560    pub fn wait_and_drain(&self, mailbox_idx: usize, deadline_tsc: u64) -> Drain {
561        if mailbox_idx >= self.n_mailboxes {
562            return Drain::Empty;
563        }
564        let mb = self.mailbox(mailbox_idx);
565        let state_addr = (&raw const mb.state).cast::<u8>();
566        loop {
567            let s = mb.state.load(Ordering::Acquire);
568            if s & 0xFFFF == CLAIM_READY {
569                break;
570            }
571            match self.wait_strategy {
572                WaitStrategy::PauseSpin => std::hint::spin_loop(),
573                WaitStrategy::Waitpkg => {
574                    // SAFETY: WAITPKG was confirmed available by
575                    // CPUID at construction time. `state_addr` is a
576                    // valid pointer into the mmap; `UMONITOR` arms
577                    // the hardware monitor on its cache line.
578                    // `UMWAIT` suspends until the monitor fires, an
579                    // interrupt arrives, or the TSC deadline is
580                    // reached. The double-check on the next loop
581                    // iteration re-validates the state byte.
582                    unsafe { wait_with_waitpkg(state_addr, deadline_tsc) };
583                }
584            }
585        }
586        self.drain_mailbox(mailbox_idx)
587    }
588
589    /// Force any dirty pages to disk.
590    pub fn flush_to_disk(&self) -> io::Result<()> {
591        self.mmap.flush()
592    }
593}
594
595/// `UMONITOR` + `UMWAIT` wait primitive. Halts the calling logical
596/// CPU until the monitored cache line transitions OR the TSC
597/// reaches `deadline_tsc` (whichever comes first). Pass
598/// `deadline_tsc = u64::MAX` for "no deadline".
599///
600/// # Safety
601///
602/// The caller MUST have confirmed WAITPKG is available via
603/// [`subetha_core::has_waitpkg`] - executing `UMONITOR` /
604/// `UMWAIT` on hardware without WAITPKG raises an illegal-
605/// instruction trap (`#UD`).
606///
607/// `state_addr` must be a valid pointer into accessible memory;
608/// `UMONITOR` reads no payload, only the address.
609#[cfg(target_arch = "x86_64")]
610#[inline(always)]
611unsafe fn wait_with_waitpkg(state_addr: *const u8, deadline_tsc: u64) {
612    use std::arch::asm;
613    let lo = deadline_tsc as u32;
614    let hi = (deadline_tsc >> 32) as u32;
615    // Two separate asm blocks: `UMONITOR` needs the address in RAX,
616    // `UMWAIT` needs EAX (low half of RAX) for the deadline low
617    // dword. We cannot bind RAX and EAX to different values in one
618    // `asm!` call, so we split. The monitor stays armed across the
619    // second asm block; `UMWAIT` in C0.1 (hint = 1) is the light
620    // wait state with low wake latency.
621    //
622    // SAFETY: caller-asserted WAITPKG availability + valid pointer.
623    unsafe {
624        asm!(
625            "umonitor rax",
626            in("rax") state_addr,
627            options(nostack, preserves_flags),
628        );
629        asm!(
630            "umwait {hint:e}",
631            hint = in(reg) 1u32,
632            in("eax") lo,
633            in("edx") hi,
634            options(nostack),
635        );
636    }
637}
638
639#[cfg(not(target_arch = "x86_64"))]
640#[inline(always)]
641unsafe fn wait_with_waitpkg(_state_addr: *const u8, _deadline_tsc: u64) {
642    // Non-x86_64: WAITPKG cannot be available; this function is
643    // never called on those targets (the `WaitStrategy::Waitpkg`
644    // branch is gated on `has_waitpkg()` which returns false).
645    std::hint::spin_loop();
646}
647
648#[cfg(test)]
649mod tests {
650    use super::*;
651    use std::sync::Arc;
652    use std::sync::atomic::{AtomicUsize, Ordering as O};
653    use std::thread;
654
655    fn temp_path(name: &str) -> std::path::PathBuf {
656        let mut p = std::env::temp_dir();
657        let pid = std::process::id();
658        let nonce = std::time::SystemTime::now()
659            .duration_since(std::time::UNIX_EPOCH)
660            .map(|d| d.as_nanos())
661            .unwrap_or(0);
662        p.push(format!("subetha_urd_{pid}_{nonce}_{name}.bin"));
663        p
664    }
665
666    fn u32_item(id: u32) -> LineItem {
667        LineItem::new(&id.to_le_bytes()).expect("item")
668    }
669
670    fn item_id(item: &LineItem) -> u32 {
671        u32::from_le_bytes(item.payload[..4].try_into().unwrap())
672    }
673
674    #[test]
675    fn create_open_round_trips_header() {
676        let path = temp_path("create_open");
677        let _u = SharedDequeUrd::create(&path, 4).expect("create");
678        let o = SharedDequeUrd::open(&path).expect("open");
679        assert_eq!(o.n_mailboxes(), 4);
680        assert_eq!(o.owner_pid(), std::process::id() as u64);
681        std::fs::remove_file(&path).ok();
682    }
683
684    #[test]
685    fn wait_strategy_matches_host() {
686        let path = temp_path("strategy");
687        let u = SharedDequeUrd::create(&path, 2).expect("create");
688        let s = u.wait_strategy();
689        if has_waitpkg() {
690            assert_eq!(s, WaitStrategy::Waitpkg);
691        } else {
692            assert_eq!(s, WaitStrategy::PauseSpin);
693        }
694        std::fs::remove_file(&path).ok();
695    }
696
697    #[test]
698    fn publish_strategy_matches_host() {
699        let path = temp_path("publish_strategy");
700        let u = SharedDequeUrd::create(&path, 2).expect("create");
701        let s = u.publish_strategy();
702        if subetha_core::has_movdir64b() {
703            assert_eq!(s, PublishStrategy::Movdir64b);
704        } else {
705            assert_eq!(s, PublishStrategy::Scalar);
706        }
707        std::fs::remove_file(&path).ok();
708    }
709
710    #[test]
711    fn publish_then_drain_works_through_strategy_dispatch() {
712        // Round-trip a publish + drain regardless of which strategy
713        // pick() chose for this host. Exercises the dispatch site
714        // in `publish_to` so the Movdir64b arm is in the binary on
715        // capable silicon (which is where it would actually run).
716        let path = temp_path("dispatch_round_trip");
717        let urd = SharedDequeUrd::create(&path, 1).expect("create");
718        let items = [
719            LineItem::new(&1u32.to_le_bytes()).expect("item"),
720            LineItem::new(&2u32.to_le_bytes()).expect("item"),
721            LineItem::new(&3u32.to_le_bytes()).expect("item"),
722        ];
723        let n = urd.publish_to(0, &items).expect("publish");
724        assert_eq!(n, 3);
725        match urd.drain_mailbox(0) {
726            Drain::Success(r) => {
727                assert_eq!(r.n_items, 3);
728                for (i, expected) in [1u32, 2, 3].iter().enumerate() {
729                    let got = u32::from_le_bytes(
730                        r.items[i].payload[..4].try_into().unwrap(),
731                    );
732                    assert_eq!(got, *expected, "item {i}");
733                }
734            }
735            Drain::Empty => panic!("expected items, got Empty"),
736        }
737        std::fs::remove_file(&path).ok();
738    }
739
740    #[test]
741    fn publish_then_drain_round_trips() {
742        let path = temp_path("publish_drain");
743        let u = SharedDequeUrd::create(&path, 2).expect("create");
744        let items = [u32_item(1), u32_item(2), u32_item(3)];
745        let n = u.publish_to(0, &items).expect("publish");
746        assert_eq!(n, 3);
747        match u.drain_mailbox(0) {
748            Drain::Success(r) => {
749                assert_eq!(r.n_items, 3);
750                assert_eq!(item_id(&r.items[0]), 1);
751                assert_eq!(item_id(&r.items[1]), 2);
752                assert_eq!(item_id(&r.items[2]), 3);
753            }
754            Drain::Empty => panic!("expected ready mailbox"),
755        }
756        // After drain, mailbox is EMPTY.
757        assert!(matches!(u.drain_mailbox(0), Drain::Empty));
758        std::fs::remove_file(&path).ok();
759    }
760
761    #[test]
762    fn publish_round_robin_cycles_targets() {
763        let path = temp_path("rr");
764        let u = SharedDequeUrd::create(&path, 4).expect("create");
765        let items = [u32_item(1)];
766        let (t0, _) = u.publish_round_robin(&items).expect("rr 0");
767        u.drain_mailbox(t0);
768        let (t1, _) = u.publish_round_robin(&items).expect("rr 1");
769        u.drain_mailbox(t1);
770        let (t2, _) = u.publish_round_robin(&items).expect("rr 2");
771        u.drain_mailbox(t2);
772        let (t3, _) = u.publish_round_robin(&items).expect("rr 3");
773        // The four picks must cover all mailboxes (mod
774        // `n_mailboxes`).
775        let mut targets = [t0, t1, t2, t3];
776        targets.sort();
777        assert_eq!(targets, [0, 1, 2, 3]);
778        std::fs::remove_file(&path).ok();
779    }
780
781    #[test]
782    fn too_many_items_rejected() {
783        let path = temp_path("too_many");
784        let u = SharedDequeUrd::create(&path, 2).expect("create");
785        let items: Vec<LineItem> = (0..(MAILBOX_ITEMS + 1) as u32).map(u32_item).collect();
786        let err = u.publish_to(0, &items).expect_err("too many");
787        assert_eq!(err, PublishError::TooManyItems);
788        std::fs::remove_file(&path).ok();
789    }
790
791    #[test]
792    fn bad_target_rejected() {
793        let path = temp_path("bad_target");
794        let u = SharedDequeUrd::create(&path, 2).expect("create");
795        let err = u.publish_to(99, &[u32_item(1)]).expect_err("bad target");
796        assert_eq!(err, PublishError::BadTarget(99));
797        std::fs::remove_file(&path).ok();
798    }
799
800    #[test]
801    fn empty_publish_is_noop() {
802        let path = temp_path("empty");
803        let u = SharedDequeUrd::create(&path, 2).expect("create");
804        assert_eq!(u.publish_to(0, &[]).expect("publish"), 0);
805        assert!(matches!(u.drain_mailbox(0), Drain::Empty));
806        std::fs::remove_file(&path).ok();
807    }
808
809    #[test]
810    fn close_owner_zeros_pid_and_advances_epoch() {
811        let path = temp_path("close");
812        let u = SharedDequeUrd::create(&path, 2).expect("create");
813        let before = u.header().epoch.load(O::Acquire);
814        u.close_owner();
815        assert_eq!(u.owner_pid(), 0);
816        assert_eq!(u.header().epoch.load(O::Acquire), before + 1);
817        std::fs::remove_file(&path).ok();
818    }
819
820    #[test]
821    fn four_thieves_no_double_take() {
822        // Stress: owner publishes round-robin to 4 mailboxes; 4
823        // thieves each drain their own mailbox. Sum invariant.
824        let path = temp_path("stress");
825        let u = Arc::new(SharedDequeUrd::create(&path, 4).expect("create"));
826        let n = 4_000usize;
827        let consumed = Arc::new(AtomicUsize::new(0));
828        let sum = Arc::new(AtomicUsize::new(0));
829
830        let mut thieves = Vec::new();
831        for tid in 0..4 {
832            let u = Arc::clone(&u);
833            let consumed = Arc::clone(&consumed);
834            let sum = Arc::clone(&sum);
835            thieves.push(thread::spawn(move || {
836                while consumed.load(O::Relaxed) < n {
837                    match u.drain_mailbox(tid) {
838                        Drain::Success(r) => {
839                            for i in 0..r.n_items {
840                                consumed.fetch_add(1, O::Relaxed);
841                                sum.fetch_add(
842                                    item_id(&r.items[i]) as usize,
843                                    O::Relaxed,
844                                );
845                            }
846                        }
847                        Drain::Empty => std::hint::spin_loop(),
848                    }
849                }
850            }));
851        }
852
853        // Publisher: round-robin batches of `MAILBOX_ITEMS` items.
854        let mut pushed = 0usize;
855        while pushed < n {
856            let want = MAILBOX_ITEMS.min(n - pushed);
857            let mut batch = [LineItem::default(); MAILBOX_ITEMS];
858            for slot in batch.iter_mut().take(want) {
859                *slot = u32_item(pushed as u32);
860                pushed += 1;
861            }
862            u.publish_round_robin(&batch[..want]).expect("rr publish");
863        }
864
865        for t in thieves {
866            t.join().expect("thief");
867        }
868        let expected: usize = (0..n).sum();
869        assert_eq!(
870            sum.load(O::Relaxed),
871            expected,
872            "stress sum mismatch (expected every item consumed exactly once)"
873        );
874        std::fs::remove_file(&path).ok();
875    }
876}