Skip to main content

vyre_driver/program_walks/
outputs.rs

1//! Output-buffer readback layout and budget checks.
2
3use std::ops::Range;
4use std::sync::Arc;
5
6use vyre_foundation::ir::{BufferDecl, DataType, Program};
7
8use crate::backend::{BackendError, DispatchConfig};
9
10/// Enforces [`DispatchConfig::max_output_bytes`] against materialized readback buffers.
11///
12/// # Errors
13///
14/// Returns when the summed output length exceeds the configured cap.
15pub fn enforce_actual_output_budget(
16    config: &DispatchConfig,
17    outputs: &[Vec<u8>],
18) -> Result<(), BackendError> {
19    let Some(limit) = config.max_output_bytes else {
20        return Ok(());
21    };
22    let actual = outputs.iter().try_fold(0usize, |sum, output| {
23        sum.checked_add(output.len()).ok_or_else(|| {
24            BackendError::new(
25                "actual readback size overflows usize. Fix: split the Program output before dispatch.",
26            )
27        })
28    })?;
29    if actual > limit {
30        return Err(BackendError::new(format!(
31            "actual readback size {actual} exceeds DispatchConfig.max_output_bytes {limit}. Fix: narrow BufferDecl::output_byte_range or raise max_output_bytes."
32        )));
33    }
34    Ok(())
35}
36
37/// Output readback layout derived from a program's declared output range.
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub struct OutputLayout {
40    /// Full output buffer byte size allocated on the GPU.
41    pub full_size: usize,
42    /// Consumer-visible byte count returned from dispatch.
43    pub read_size: usize,
44    /// Aligned source offset copied from the GPU output buffer.
45    pub copy_offset: usize,
46    /// Aligned staging-buffer byte size.
47    pub copy_size: usize,
48    /// Offset within the staging buffer where the requested range starts.
49    pub trim_start: usize,
50}
51
52/// Readback and allocation metadata for one writable buffer.
53#[derive(Clone, Debug)]
54pub struct OutputBindingLayout {
55    /// Buffer binding slot.
56    pub binding: u32,
57    /// Buffer name for diagnostics.
58    pub name: Arc<str>,
59    /// Full readback/copy layout for this binding.
60    pub layout: OutputLayout,
61    /// Rounded-up 32-bit word count used for allocation and clears.
62    pub word_count: usize,
63}
64
65/// Derive output readback layout for a program.
66///
67/// # Errors
68///
69/// Returns a backend error when the program has no output buffer or declares
70/// an out-of-bounds output byte range.
71pub fn output_layout_from_program(program: &Program) -> Result<OutputLayout, BackendError> {
72    let Some(&index) = program.output_buffer_indices().first() else {
73        return Err(BackendError::new(
74            "program has no output buffer. Fix: declare exactly one output buffer in the vyre Program.",
75        ));
76    };
77    let output = program.buffers().get(index as usize).ok_or_else(|| {
78        BackendError::new(format!(
79            "output buffer index {index} is out of bounds. Fix: rebuild the Program so writable buffer metadata stays consistent."
80        ))
81    })?;
82    output_binding_layout(output).map(|output| output.layout)
83}
84
85/// All output-buffer binding layouts for `program`, in declaration order.
86///
87/// # Errors
88///
89/// Returns when there is no output buffer, an index is invalid, or layout
90/// math fails.
91pub fn output_binding_layouts(program: &Program) -> Result<Vec<OutputBindingLayout>, BackendError> {
92    let mut outputs = reserved_output_layout_slots(program.output_buffer_indices().len())?;
93    output_binding_layouts_into(program, &mut outputs)?;
94    Ok(outputs)
95}
96
97/// Write output-buffer binding layouts into caller-owned storage.
98///
99/// # Errors
100///
101/// Returns when there is no output buffer, an index is invalid, or layout
102/// math fails.
103pub fn output_binding_layouts_into(
104    program: &Program,
105    outputs: &mut Vec<OutputBindingLayout>,
106) -> Result<(), BackendError> {
107    outputs.clear();
108    reserve_output_layout_slots(outputs, program.output_buffer_indices().len())?;
109    for &index in program.output_buffer_indices() {
110        let output = program.buffers().get(index as usize).ok_or_else(|| {
111            BackendError::new(
112                format!(
113                    "output buffer index {index} is out of bounds. Fix: rebuild the Program so writable buffer metadata stays consistent."
114                ),
115            )
116        })?;
117        outputs.push(output_binding_layout(output)?);
118    }
119    if outputs.is_empty() {
120        return Err(BackendError::new(
121            "program has no output buffer. Fix: declare at least one writable buffer in the vyre Program.",
122        ));
123    }
124    Ok(())
125}
126
127/// Per-output binding layout for a single declared output buffer.
128///
129/// # Errors
130///
131/// Returns when counts, element size, or declared byte range are inconsistent.
132pub fn output_binding_layout(output: &BufferDecl) -> Result<OutputBindingLayout, BackendError> {
133    output_binding_layout_parts(
134        output.binding(),
135        &output.name,
136        &output.element,
137        output.count(),
138        output.output_byte_range(),
139    )
140}
141
142/// Readback layout for one writable binding, with every input supplied directly.
143///
144/// [`output_binding_layout`] reads these five values off a [`BufferDecl`]. A
145/// backend that resolves a runtime-sized writable buffer (one declared without
146/// `.with_count(n)`, so `count == 0`) from the caller's input bytes calls this
147/// instead and passes the resolved element count, so allocation size, copy size,
148/// and trim offsets all come from this one routine rather than from a second,
149/// drifting copy of the same arithmetic.
150///
151/// # Errors
152///
153/// Returns when counts, element size, or the declared byte range are inconsistent.
154pub fn output_binding_layout_parts(
155    binding: u32,
156    name: &Arc<str>,
157    element: &DataType,
158    count: u32,
159    output_byte_range: Option<Range<usize>>,
160) -> Result<OutputBindingLayout, BackendError> {
161    let count = usize::try_from(count).map_err(|_| {
162        BackendError::new(
163            "program output element count exceeds usize. Fix: split the dispatch into smaller output buffers.",
164        )
165    })?;
166    element.validate_layout().map_err(|error| {
167        BackendError::new(format!(
168            "program output `{name}` has malformed data-type layout metadata: {error}"
169        ))
170    })?;
171    let full_size = element.packed_size_bytes(count).map_err(|error| {
172        BackendError::new(format!(
173            "program output `{name}` byte size could not be computed: {error}"
174        ))
175    })?.ok_or_else(|| {
176        BackendError::new(
177            "program output element type has no fixed packed byte size. Fix: validate the Program and flatten variable-size outputs before backend pipeline compilation.",
178        )
179    })?;
180    let layout = output_layout(output_byte_range, full_size)?;
181    let word_count = full_size
182        .checked_add(3)
183        .and_then(|n| n.checked_div(4))
184        .ok_or_else(|| {
185            BackendError::new(
186                "program output word count overflows usize. Fix: split the dispatch into smaller output buffers.",
187            )
188        })?
189        .max(1);
190    Ok(OutputBindingLayout {
191        binding,
192        name: Arc::clone(name),
193        layout,
194        word_count,
195    })
196}
197
198fn output_layout(
199    output_byte_range: Option<Range<usize>>,
200    full_size: usize,
201) -> Result<OutputLayout, BackendError> {
202    let range = output_byte_range.unwrap_or(0..full_size);
203    if range.start > range.end || range.end > full_size {
204        return Err(BackendError::new(format!(
205            "output byte range {:?} is outside output buffer size {full_size}. Fix: declare a range within the output buffer.",
206            range
207        )));
208    }
209    let copy_offset = range.start & !3;
210    let copy_end = align_up_to_u32_word(range.end)?.min(full_size.max(4));
211    let copy_size = copy_end.checked_sub(copy_offset).ok_or_else(|| {
212        BackendError::new(format!(
213            "aligned output copy range underflowed: copy_end={copy_end}, copy_offset={copy_offset}. Fix: declare output_byte_range inside the output buffer."
214        ))
215    })?.max(4);
216    Ok(OutputLayout {
217        full_size,
218        read_size: range.end - range.start,
219        copy_offset,
220        copy_size,
221        trim_start: range.start - copy_offset,
222    })
223}
224
225fn reserve_output_layout_slots(
226    outputs: &mut Vec<OutputBindingLayout>,
227    capacity: usize,
228) -> Result<(), BackendError> {
229    crate::allocation::try_reserve_vec_to_capacity(outputs, capacity).map_err(|error| {
230        BackendError::new(format!(
231            "output binding layout planning could not reserve {capacity} output slot(s): {error}. Fix: split the Program output set or reuse caller-owned output layout scratch."
232        ))
233    })
234}
235
236fn reserved_output_layout_slots(capacity: usize) -> Result<Vec<OutputBindingLayout>, BackendError> {
237    let mut outputs = Vec::new();
238    reserve_output_layout_slots(&mut outputs, capacity)?;
239    Ok(outputs)
240}
241
242fn align_up_to_u32_word(value: usize) -> Result<usize, BackendError> {
243    value.checked_add(3).map(|end| end & !3).ok_or_else(|| {
244        BackendError::new(format!(
245            "aligned output copy end overflows usize for byte offset {value}. Fix: declare a smaller output_byte_range before backend readback planning."
246        ))
247    })
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use vyre_foundation::ir::{BufferDecl, DataType};
254
255    #[test]
256    fn output_layout_planning_uses_fallible_modular_reservation_and_alignment() {
257        let source = include_str!("outputs.rs");
258        let production = source
259            .split("#[cfg(test)]")
260            .next()
261            .expect("Fix: output-layout source must contain production section before tests");
262
263        assert!(
264            production.contains("fn reserve_output_layout_slots")
265                && production.contains("fn align_up_to_u32_word")
266                && production.contains("try_reserve_vec_to_capacity"),
267            "Fix: output layout planning must keep reservation and alignment as modular fallible helpers."
268        );
269        assert!(
270            !production.contains("Vec::with_capacity")
271                && !production.contains(".reserve(program.output_buffer_indices().len())")
272                && !production.contains(".next_multiple_of(4)")
273                && !production.contains(".unwrap_or(full_size)"),
274            "Fix: output layout planning must not allocate infallibly or hide overflow in release paths."
275        );
276    }
277
278    #[test]
279    fn output_layout_alignment_rejects_usize_overflow() {
280        let error =
281            align_up_to_u32_word(usize::MAX).expect_err("max byte offset cannot align upward");
282        assert!(
283            error.to_string().contains("Fix:"),
284            "alignment overflow must be actionable: {error}"
285        );
286    }
287
288    #[test]
289    fn output_layout_uses_packed_size_for_subbyte_elements() {
290        let output = BufferDecl::output("packed_i4", 0, DataType::I4).with_count(3);
291        let layout = output_binding_layout(&output)
292            .expect("Fix: packed I4 output layout should use packed byte sizing");
293
294        assert_eq!(layout.layout.full_size, 2);
295        assert_eq!(layout.layout.read_size, 2);
296        assert_eq!(layout.word_count, 1);
297    }
298
299    #[test]
300    fn output_layout_rejects_malformed_data_type_layouts() {
301        let output = BufferDecl::output(
302            "bad_bsr",
303            0,
304            DataType::SparseBsr {
305                element: Box::new(DataType::F32),
306                block_rows: 0,
307                block_cols: 4,
308            },
309        )
310        .with_count(1);
311
312        let error = output_binding_layout(&output)
313            .expect_err("zero-height BSR blocks must not enter output planning");
314        assert!(
315            error
316                .to_string()
317                .contains("SparseBsr block_rows must be > 0"),
318            "Fix: malformed output data-type layout diagnostics must remain actionable: {error}"
319        );
320    }
321}
322
323/// Fixed scalar element size in bytes for [`DataType`].
324///
325/// # Errors
326///
327/// Returns when the type has no fixed size (e.g. unsized or dynamic).
328pub fn element_size_bytes(data_type: &DataType) -> Result<usize, BackendError> {
329    data_type.size_bytes().ok_or_else(|| {
330        BackendError::new(
331            "output buffer element type has no fixed scalar element size. Fix: validate the Program and flatten variable-size outputs before backend pipeline compilation.",
332        )
333    })
334}