Skip to main content

hrx/fabric/
xdna.rs

1use super::{
2    executable::{bridge_check, ffi::*},
3    memory::DeviceUse,
4    *,
5};
6use std::{
7    collections::VecDeque,
8    ffi::CString,
9    sync::{
10        Mutex,
11        atomic::{AtomicU64, Ordering},
12    },
13    time::Duration,
14};
15
16/// A bounded logical range bound to an XDNA entry.
17pub struct XdnaBinding<'a> {
18    /// Shared native backing, admitted for the selected device.
19    pub buffer: &'a Buffer,
20    /// First byte of the logical range.
21    pub offset: usize,
22    /// Logical length, excluding allocation padding.
23    pub length: usize,
24}
25/// Loaded, bound XDNA entry with a private context and establishing command.
26/// Clones share one queue and up to 128 accepted invocations. Bindings remain
27/// leased until every outstanding invocation has retired.
28#[derive(Clone)]
29pub struct XdnaProgram(Arc<Program>);
30struct Program {
31    bridge: Arc<Bridge>,
32    device: Device,
33    raw: *mut hrx_fabric_xdna_program,
34    buffers: Vec<Buffer>,
35    state: Mutex<State>,
36    retired: AtomicU64,
37    charges: Vec<Arc<crate::residency::MemoryReservation>>,
38}
39#[derive(Default)]
40struct State {
41    pending: VecDeque<u64>,
42    leases: Vec<DeviceUse>,
43    retired: u64,
44}
45// Every native operation is serialized by state. Binding and code storage are
46// immutable after preparation and remain owned until checked retirement.
47unsafe impl Send for Program {}
48unsafe impl Sync for Program {}
49impl Program {
50    fn wait(&self, state: &mut State, submission: u64, timeout: u64) -> Result<bool> {
51        if submission <= state.retired {
52            return Ok(true);
53        }
54        let status = unsafe {
55            self.bridge
56                .hrx_fabric_xdna_wait(self.raw, submission, timeout)
57        };
58        if status == u64::from(AMDF_STATUS_CODE_DEADLINE_EXCEEDED) {
59            return Ok(false);
60        }
61        check("XDNA retirement", status)?;
62        state.retired = submission;
63        while state
64            .pending
65            .front()
66            .is_some_and(|point| *point <= submission)
67        {
68            state.pending.pop_front();
69        }
70        if state.pending.is_empty() {
71            state.leases.clear();
72        }
73        self.retired.store(submission, Ordering::Release);
74        Ok(true)
75    }
76}
77impl Drop for Program {
78    fn drop(&mut self) {
79        let mut state = self
80            .state
81            .lock()
82            .unwrap_or_else(std::sync::PoisonError::into_inner);
83        let retired = match state.pending.back() {
84            Some(submission) => {
85                let submission = *submission;
86                self.wait(&mut state, submission, 10_000_000_000)
87                    .unwrap_or(false)
88            }
89            None => true,
90        };
91        if !retired || unsafe { self.bridge.hrx_fabric_xdna_close(self.raw) } != 0 {
92            // No retirement/detach proof: quarantine the full ownership chain.
93            std::mem::forget(std::mem::take(&mut state.leases));
94            std::mem::forget(self.buffers.clone());
95            std::mem::forget(std::mem::take(&mut self.charges));
96            std::mem::forget(self.device.clone());
97            std::mem::forget(self.bridge.clone());
98        }
99    }
100}
101/// A checked native completion point retaining the program and all its bindings.
102#[derive(Clone)]
103pub struct XdnaCompletion {
104    program: XdnaProgram,
105    submission: u64,
106}
107impl XdnaCompletion {
108    /// Read cached retirement without locking, native queries, or progress work.
109    pub fn is_complete(&self) -> Result<bool> {
110        Ok(self.program.0.retired.load(Ordering::Acquire) >= self.submission)
111    }
112    /// Inspect native command results and advance retirement without waiting.
113    pub fn refresh(&self) -> Result<bool> {
114        self.wait_timeout(Duration::ZERO)
115    }
116    /// Wait under one native deadline. A timeout keeps all resources retained.
117    pub fn wait_timeout(&self, timeout: Duration) -> Result<bool> {
118        let mut state = self
119            .program
120            .0
121            .state
122            .lock()
123            .map_err(|_| Error::DeviceLost("XDNA queue poisoned".into()))?;
124        self.program.0.wait(
125            &mut state,
126            self.submission,
127            timeout.as_nanos().min(u128::from(u64::MAX - 1)) as u64,
128        )
129    }
130    /// Wait until checked retirement; failure does not permit storage reuse.
131    pub fn wait(&self) -> Result<()> {
132        let mut state = self
133            .program
134            .0
135            .state
136            .lock()
137            .map_err(|_| Error::DeviceLost("XDNA queue poisoned".into()))?;
138        self.program.0.wait(&mut state, self.submission, u64::MAX)?;
139        Ok(())
140    }
141}
142impl XdnaProgram {
143    /// Compose trusted native transaction records around the bound invocation.
144    /// This consumes an exclusive, never-submitted program. The original command
145    /// body is preserved; only its transaction size/count header is updated.
146    /// Additional addressed buffers are retained through checked retirement.
147    /// # Safety
148    /// Records must own disjoint native resources, preserve the establishing
149    /// invocation, and drain every added DMA/worker before command completion.
150    /// Every address they embed must belong to `retained` or existing bindings.
151    pub unsafe fn wrap_transaction(
152        mut self,
153        prefix: &[u8],
154        prefix_operations: u32,
155        suffix: &[u8],
156        suffix_operations: u32,
157        retained: &[Buffer],
158        budget: Option<&crate::residency::MemoryBudget>,
159    ) -> Result<Self> {
160        let program = Arc::get_mut(&mut self.0)
161            .ok_or_else(|| Error::Busy("XDNA command is shared".into()))?;
162        let state = program
163            .state
164            .get_mut()
165            .map_err(|_| Error::DeviceLost("XDNA queue poisoned".into()))?;
166        if !state.pending.is_empty() || state.retired != 0 {
167            return Err(Error::Busy(
168                "XDNA command has already been submitted".into(),
169            ));
170        }
171        for buffer in retained {
172            buffer.device_address(&program.device)?;
173        }
174        let mut length = 0;
175        bridge_check(&program.bridge, unsafe {
176            program.bridge.hrx_fabric_xdna_command_copy(
177                program.raw,
178                ptr::null_mut(),
179                0,
180                &mut length,
181            )
182        })?;
183        let mut original = vec![0; length];
184        bridge_check(&program.bridge, unsafe {
185            program.bridge.hrx_fabric_xdna_command_copy(
186                program.raw,
187                original.as_mut_ptr(),
188                length,
189                &mut length,
190            )
191        })?;
192        let bytes = compose_transaction(
193            &original,
194            prefix,
195            prefix_operations,
196            suffix,
197            suffix_operations,
198        )?;
199        if let Some(budget) = budget {
200            program.charges.push(Arc::new(budget.reserve(bytes.len())?));
201        }
202        // Retain before native mutation: any failure still owns every dependency.
203        program.buffers.extend_from_slice(retained);
204        program
205            .buffers
206            .sort_by_key(|buffer| Arc::as_ptr(&buffer.0) as usize);
207        program.buffers.dedup_by(|a, b| Arc::ptr_eq(&a.0, &b.0));
208        state.leases.reserve(program.buffers.len());
209        bridge_check(&program.bridge, unsafe {
210            program
211                .bridge
212                .hrx_fabric_xdna_command_replace(program.raw, bytes.as_ptr(), bytes.len())
213        })?;
214        Ok(self)
215    }
216    /// Publish the complete establishing command and retain all external backing.
217    ///
218    /// # Safety
219    /// The caller orders conflicting accesses by other queues. The compiled
220    /// program must obey each binding's declared range and access contract.
221    pub unsafe fn dispatch(&self) -> Result<XdnaCompletion> {
222        let mut state = self
223            .0
224            .state
225            .lock()
226            .map_err(|_| Error::DeviceLost("XDNA queue poisoned".into()))?;
227        if state.pending.len() == 128 {
228            let oldest = *state.pending.front().unwrap();
229            if !self.0.wait(&mut state, oldest, 0)? {
230                return Err(Error::Busy(
231                    "XDNA pending invocation capacity exhausted".into(),
232                ));
233            }
234        }
235        if state.leases.is_empty() {
236            for buffer in &self.0.buffers {
237                match buffer.retain_use() {
238                    Ok(lease) => state.leases.push(lease),
239                    Err(error) => {
240                        state.leases.clear();
241                        return Err(error);
242                    }
243                }
244            }
245        }
246        let mut submission = 0;
247        if let Err(error) = check("XDNA submit", unsafe {
248            self.0
249                .bridge
250                .hrx_fabric_xdna_submit(self.0.raw, &mut submission)
251        }) {
252            if state.pending.is_empty() {
253                state.leases.clear();
254            }
255            return Err(error);
256        }
257        state.pending.push_back(submission);
258        Ok(XdnaCompletion {
259            program: self.clone(),
260            submission,
261        })
262    }
263}
264impl Device {
265    /// Load and bind a canonical XDNA image for the selected logical width.
266    ///
267    /// # Safety
268    /// The native executable must be trusted and obey its binding contracts.
269    pub unsafe fn prepare_xdna(
270        &self,
271        artifact: &crate::loom::Artifact,
272        columns: u16,
273        bindings: &[XdnaBinding<'_>],
274    ) -> Result<XdnaProgram> {
275        if self.endpoint().engine() != Engine::Xdna || artifact.target() != self.target().as_str() {
276            return Err(Error::Unsupported("artifact and XDNA target differ".into()));
277        }
278        if !(1..=8).contains(&columns) || bindings.len() > u32::MAX as usize {
279            return Err(Error::Message(
280                "invalid XDNA context width or binding count".into(),
281            ));
282        }
283        let mut native = Vec::with_capacity(bindings.len());
284        let mut buffers = Vec::with_capacity(bindings.len());
285        for binding in bindings {
286            if binding.length == 0
287                || binding
288                    .offset
289                    .checked_add(binding.length)
290                    .is_none_or(|end| end > binding.buffer.len())
291            {
292                return Err(Error::Message("XDNA binding range is out of bounds".into()));
293            }
294            let address = binding
295                .buffer
296                .device_address(self)?
297                .checked_add(binding.offset as u64)
298                .ok_or_else(|| Error::Message("XDNA binding address overflow".into()))?;
299            native.push(hrx_fabric_xdna_binding {
300                memory: binding.buffer.0.raw.cast(),
301                host_pointer: binding.buffer.host_pointer().cast(),
302                allocation_length: binding.buffer.len() as u64,
303                offset: binding.offset as u64,
304                length: binding.length as u64,
305                address,
306            });
307            buffers.push(binding.buffer.clone());
308        }
309        buffers.sort_by_key(|buffer| Arc::as_ptr(&buffer.0) as usize);
310        buffers.dedup_by(|a, b| Arc::ptr_eq(&a.0, &b.0));
311        let api = &self.0.endpoint.0.instance.api;
312        let bridge = api.load_bridge()?;
313        let symbol = CString::new(artifact.symbol())
314            .map_err(|_| Error::Message("symbol contains NUL".into()))?;
315        let mut raw = ptr::null_mut();
316        let status = unsafe {
317            bridge.hrx_fabric_xdna_open(
318                std::ptr::from_ref(api.core).cast(),
319                std::ptr::from_ref(api.xdna).cast(),
320                self.0.endpoint.0.instance.raw.cast(),
321                self.0.endpoint.0.raw.cast(),
322                self.0.raw.cast(),
323                artifact.bytes().as_ptr(),
324                artifact.bytes().len(),
325                symbol.as_ptr(),
326                columns,
327                native.len() as u32,
328                native.as_ptr(),
329                &mut raw,
330            )
331        };
332        // Copy diagnostics before closing a partial native ownership tree.
333        let result = bridge_check(&bridge, status);
334        if raw.is_null() {
335            return result.and_then(|()| Err(missing("XDNA program")));
336        }
337        let state = Mutex::new(State {
338            pending: VecDeque::with_capacity(128),
339            leases: Vec::with_capacity(buffers.len()),
340            ..State::default()
341        });
342        let program = XdnaProgram(Arc::new(Program {
343            bridge,
344            device: self.clone(),
345            raw,
346            buffers,
347            state,
348            retired: AtomicU64::new(0),
349            charges: Vec::new(),
350        }));
351        result?;
352        Ok(program)
353    }
354}
355
356fn compose_transaction(
357    original: &[u8],
358    prefix: &[u8],
359    prefix_operations: u32,
360    suffix: &[u8],
361    suffix_operations: u32,
362) -> Result<Vec<u8>> {
363    let invalid = || Error::Message("invalid or oversized AIE2P transaction 0.1".into());
364    if original.len() < 16
365        || original[..4] != [0, 1, 4, 6]
366        || original[5] != 1
367        || original[4] == 0
368        || original[4] > 8
369        || u32::from_le_bytes(original[12..16].try_into().unwrap()) as usize != original.len()
370    {
371        return Err(invalid());
372    }
373    let length = original
374        .len()
375        .checked_add(prefix.len())
376        .and_then(|n| n.checked_add(suffix.len()))
377        .filter(|n| *n <= u32::MAX as usize)
378        .ok_or_else(invalid)?;
379    let operations = u32::from_le_bytes(original[8..12].try_into().unwrap())
380        .checked_add(prefix_operations)
381        .and_then(|n| n.checked_add(suffix_operations))
382        .ok_or_else(invalid)?;
383    let mut bytes = Vec::with_capacity(length);
384    bytes.extend_from_slice(&original[..16]);
385    bytes.extend_from_slice(prefix);
386    bytes.extend_from_slice(&original[16..]);
387    bytes.extend_from_slice(suffix);
388    bytes[8..12].copy_from_slice(&operations.to_le_bytes());
389    bytes[12..16].copy_from_slice(&(length as u32).to_le_bytes());
390    Ok(bytes)
391}
392
393#[cfg(test)]
394mod transaction_tests {
395    use super::compose_transaction;
396
397    fn original() -> Vec<u8> {
398        let mut command = vec![0, 1, 4, 6, 1, 1, 0, 0];
399        command.extend_from_slice(&2u32.to_le_bytes());
400        command.extend_from_slice(&24u32.to_le_bytes());
401        command.extend_from_slice(&[11, 22, 33, 44, 55, 66, 77, 88]);
402        command
403    }
404
405    #[test]
406    fn wrapping_preserves_original_body_and_updates_only_extent_and_count() {
407        let original = original();
408        let result = compose_transaction(&original, &[9, 8, 7, 6], 1, &[5, 4, 3, 2], 3).unwrap();
409        assert_eq!(&result[..8], &original[..8]);
410        assert_eq!(u32::from_le_bytes(result[8..12].try_into().unwrap()), 6);
411        assert_eq!(u32::from_le_bytes(result[12..16].try_into().unwrap()), 32);
412        assert_eq!(&result[16..20], &[9, 8, 7, 6]);
413        assert_eq!(&result[20..28], &original[16..]);
414        assert_eq!(&result[28..], &[5, 4, 3, 2]);
415    }
416
417    #[test]
418    fn wrapping_rejects_malformed_headers_and_operation_overflow() {
419        let original = original();
420        for length in 0..original.len() {
421            assert!(compose_transaction(&original[..length], &[], 0, &[], 0).is_err());
422        }
423        for (offset, value) in [
424            (0, 1),
425            (1, 2),
426            (2, 5),
427            (3, 7),
428            (4, 0),
429            (4, 9),
430            (5, 2),
431            (12, 16),
432        ] {
433            let mut malformed = original.clone();
434            malformed[offset] = value;
435            assert!(compose_transaction(&malformed, &[], 0, &[], 0).is_err());
436        }
437        assert!(compose_transaction(&original, &[], u32::MAX, &[], 0).is_err());
438        assert!(compose_transaction(&original, &[], u32::MAX - 2, &[], 1).is_err());
439    }
440}