Skip to main content

vyre_driver_wgpu/engine/
graph.rs

1//! GPU-resident dispatch graph execution (Innovation I.14).
2//!
3//! A graph records many dependent pipeline dispatches into one command buffer
4//! and submits it once. This gives callers one CPU-to-GPU launch while the GPU
5//! executes the ordered kernel sequence already resident in the command queue.
6
7use crate::buffer::GpuBufferHandle;
8use crate::pipeline::compound::CompoundResource;
9use crate::pipeline::WgpuPipeline;
10use smallvec::SmallVec;
11use vyre_driver::{BackendError, DispatchConfig};
12
13/// A GPU-resident or host-side resource.
14#[derive(Clone)]
15pub enum GpuResource {
16    /// Host-side byte slice.
17    Borrowed(Vec<u8>),
18    /// GPU-resident buffer handle.
19    Resident(GpuBufferHandle),
20}
21
22impl From<Vec<u8>> for GpuResource {
23    fn from(bytes: Vec<u8>) -> Self {
24        Self::Borrowed(bytes)
25    }
26}
27
28impl From<GpuBufferHandle> for GpuResource {
29    fn from(handle: GpuBufferHandle) -> Self {
30        Self::Resident(handle)
31    }
32}
33
34/// Ordered graph of compiled wgpu pipeline dispatches.
35#[derive(Default)]
36pub struct GpuDispatchGraph {
37    ops: SmallVec<[GraphOp; 8]>,
38}
39
40#[derive(Clone)]
41struct GraphOp {
42    pipeline: WgpuPipeline,
43    input: GpuResource,
44}
45
46impl GpuDispatchGraph {
47    /// Create an empty graph.
48    #[must_use]
49    pub fn new() -> Self {
50        Self {
51            ops: SmallVec::new(),
52        }
53    }
54
55    /// Append one pipeline dispatch to the graph.
56    pub fn push(&mut self, pipeline: WgpuPipeline, input: impl Into<GpuResource>) {
57        self.ops.push(GraphOp {
58            pipeline,
59            input: input.into(),
60        });
61    }
62
63    /// Number of dispatch nodes in the graph.
64    #[must_use]
65    pub fn len(&self) -> usize {
66        self.ops.len()
67    }
68
69    /// Return true when the graph has no dispatch nodes.
70    #[must_use]
71    pub fn is_empty(&self) -> bool {
72        self.ops.is_empty()
73    }
74
75    /// Execute all graph nodes with one queue submission.
76    ///
77    /// # Errors
78    ///
79    /// Returns a backend error if any pipeline binding, dispatch, or readback
80    /// fails. Empty graphs return an empty output vector.
81    pub fn dispatch(&self, config: &DispatchConfig) -> Result<Vec<Vec<Vec<u8>>>, BackendError> {
82        if self.ops.is_empty() {
83            return Ok(Vec::new());
84        }
85
86        // V7-PERF-021: Zero-copy graph execution (I.14).
87        // Convert GpuResources to substrate-neutral Resources for the pipeline engine.
88        // Resident handles carry their owning namespace, not a bare id.
89        let mut internal_requests: SmallVec<[(&WgpuPipeline, CompoundResource<'_>); 8]> =
90            SmallVec::with_capacity(self.ops.len());
91        for op in &self.ops {
92            let res = match &op.input {
93                GpuResource::Borrowed(bytes) => CompoundResource::Borrowed(bytes),
94                GpuResource::Resident(handle) => {
95                    CompoundResource::Resident(handle.resident_handle()?)
96                }
97            };
98            internal_requests.push((&op.pipeline, res));
99        }
100
101        WgpuPipeline::dispatch_compound_borrowed(&internal_requests, config)
102    }
103}
104
105/// CPU-side launch accounting for graph dispatch.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub struct LaunchAccounting {
108    /// Number of queue submissions required by sequential per-op dispatch.
109    pub sequential_submissions: usize,
110    /// Number of queue submissions required by graph dispatch.
111    pub graph_submissions: usize,
112}
113
114impl LaunchAccounting {
115    /// Return the integer launch-count reduction from graph recording.
116    #[must_use]
117    pub fn reduction_factor(self) -> usize {
118        self.sequential_submissions / self.graph_submissions.max(1)
119    }
120}
121
122/// Compute launch-count accounting for an ordered graph with `op_count` nodes.
123#[must_use]
124pub fn launch_accounting(op_count: usize) -> LaunchAccounting {
125    LaunchAccounting {
126        sequential_submissions: op_count,
127        graph_submissions: usize::from(op_count > 0),
128    }
129}