#[allow(unused_imports)]
use crate::error::ErrorCode;

use std::{
    ffi::CString,
    fmt::{self, Display, Formatter},
    marker::PhantomData,
    ptr,
};

use num_enum::{IntoPrimitive, TryFromPrimitive};
use singe_core::impl_enum_conversion;
use singe_cuda_sys::{driver, runtime};

use crate::{
    dim::Dim3,
    error::{Error, Result},
    event::Event,
    memory::{MemAccessDescriptor, MemoryCopyKind, MemoryPoolProps},
    stream::Stream,
    try_cuda,
    types::{DeviceFunction, DevicePtr, HostFunction},
};

/// Identifiers for [`GraphKernelNodeAttribute`] values used by CUDA graph kernel nodes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, IntoPrimitive)]
#[repr(u32)]
pub enum GraphKernelNodeAttributeId {
    /// Identifies [`GraphKernelNodeAttribute::Cooperative`].
    Cooperative = runtime::cudaLaunchAttributeID::cudaLaunchAttributeCooperative as _,
    /// Identifies [`GraphKernelNodeAttribute::ClusterDimension`].
    ClusterDimension = runtime::cudaLaunchAttributeID::cudaLaunchAttributeClusterDimension as _,
    /// Identifies [`GraphKernelNodeAttribute::Priority`].
    Priority = runtime::cudaLaunchAttributeID::cudaLaunchAttributePriority as _,
    /// Identifies [`GraphKernelNodeAttribute::PreferredSharedMemoryCarveout`].
    /// The value is a percentage in the range `0..=100` describing the preferred
    /// shared-memory carveout for the launch. This is only a hint, and the driver
    /// may choose a different configuration if required.
    PreferredSharedMemoryCarveout =
        runtime::cudaLaunchAttributeID::cudaLaunchAttributePreferredSharedMemoryCarveout as _,
}

impl_enum_conversion!(
    u32,
    runtime::cudaLaunchAttributeID,
    GraphKernelNodeAttributeId
);

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GraphKernelNodeAttribute {
    Cooperative(bool),
    ClusterDimension(Dim3),
    Priority(i32),
    PreferredSharedMemoryCarveout(u32),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MemoryAllocationNodeInfo {
    ptr: DevicePtr,
    pub byte_size: usize,
}

bitflags::bitflags! {
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    pub struct GraphInstantiateFlags: u64 {
        const AUTO_FREE_ON_LAUNCH = driver::CUgraphInstantiate_flags::CUDA_GRAPH_INSTANTIATE_FLAG_AUTO_FREE_ON_LAUNCH as _;
        const UPLOAD = driver::CUgraphInstantiate_flags::CUDA_GRAPH_INSTANTIATE_FLAG_UPLOAD as _;
        const DEVICE_LAUNCH = driver::CUgraphInstantiate_flags::CUDA_GRAPH_INSTANTIATE_FLAG_DEVICE_LAUNCH as _;
        const USE_NODE_PRIORITY = driver::CUgraphInstantiate_flags::CUDA_GRAPH_INSTANTIATE_FLAG_USE_NODE_PRIORITY as _;
    }
}

bitflags::bitflags! {
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    pub struct GraphDebugDotFlags: u32 {
        const VERBOSE = driver::CUgraphDebugDot_flags::CU_GRAPH_DEBUG_DOT_FLAGS_VERBOSE as _;
        const RUNTIME_TYPES = driver::CUgraphDebugDot_flags::CU_GRAPH_DEBUG_DOT_FLAGS_RUNTIME_TYPES as _;
        const KERNEL_NODE_PARAMS = driver::CUgraphDebugDot_flags::CU_GRAPH_DEBUG_DOT_FLAGS_KERNEL_NODE_PARAMS as _;
        const MEMCPY_NODE_PARAMS = driver::CUgraphDebugDot_flags::CU_GRAPH_DEBUG_DOT_FLAGS_MEMCPY_NODE_PARAMS as _;
        const MEMSET_NODE_PARAMS = driver::CUgraphDebugDot_flags::CU_GRAPH_DEBUG_DOT_FLAGS_MEMSET_NODE_PARAMS as _;
        const HOST_NODE_PARAMS = driver::CUgraphDebugDot_flags::CU_GRAPH_DEBUG_DOT_FLAGS_HOST_NODE_PARAMS as _;
        const EVENT_NODE_PARAMS = driver::CUgraphDebugDot_flags::CU_GRAPH_DEBUG_DOT_FLAGS_EVENT_NODE_PARAMS as _;
        const EXTERNAL_SEMAPHORE_SIGNAL_NODE_PARAMS = driver::CUgraphDebugDot_flags::CU_GRAPH_DEBUG_DOT_FLAGS_EXT_SEMAS_SIGNAL_NODE_PARAMS as _;
        const EXTERNAL_SEMAPHORE_WAIT_NODE_PARAMS = driver::CUgraphDebugDot_flags::CU_GRAPH_DEBUG_DOT_FLAGS_EXT_SEMAS_WAIT_NODE_PARAMS as _;
        const KERNEL_NODE_ATTRIBUTES = driver::CUgraphDebugDot_flags::CU_GRAPH_DEBUG_DOT_FLAGS_KERNEL_NODE_ATTRIBUTES as _;
        const HANDLES = driver::CUgraphDebugDot_flags::CU_GRAPH_DEBUG_DOT_FLAGS_HANDLES as _;
        const MEMORY_ALLOC_NODE_PARAMS = driver::CUgraphDebugDot_flags::CU_GRAPH_DEBUG_DOT_FLAGS_MEM_ALLOC_NODE_PARAMS as _;
        const MEMORY_FREE_NODE_PARAMS = driver::CUgraphDebugDot_flags::CU_GRAPH_DEBUG_DOT_FLAGS_MEM_FREE_NODE_PARAMS as _;
        const BATCH_MEM_OP_NODE_PARAMS = driver::CUgraphDebugDot_flags::CU_GRAPH_DEBUG_DOT_FLAGS_BATCH_MEM_OP_NODE_PARAMS as _;
        const EXTRA_TOPOLOGY_INFO = driver::CUgraphDebugDot_flags::CU_GRAPH_DEBUG_DOT_FLAGS_EXTRA_TOPO_INFO as _;
        const CONDITIONAL_NODE_PARAMS = driver::CUgraphDebugDot_flags::CU_GRAPH_DEBUG_DOT_FLAGS_CONDITIONAL_NODE_PARAMS as _;
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, IntoPrimitive)]
#[repr(u32)]
pub enum GraphNodeType {
    Kernel = driver::CUgraphNodeType::CU_GRAPH_NODE_TYPE_KERNEL as _,
    Memcpy = driver::CUgraphNodeType::CU_GRAPH_NODE_TYPE_MEMCPY as _,
    Memset = driver::CUgraphNodeType::CU_GRAPH_NODE_TYPE_MEMSET as _,
    Host = driver::CUgraphNodeType::CU_GRAPH_NODE_TYPE_HOST as _,
    Graph = driver::CUgraphNodeType::CU_GRAPH_NODE_TYPE_GRAPH as _,
    Empty = driver::CUgraphNodeType::CU_GRAPH_NODE_TYPE_EMPTY as _,
    WaitEvent = driver::CUgraphNodeType::CU_GRAPH_NODE_TYPE_WAIT_EVENT as _,
    EventRecord = driver::CUgraphNodeType::CU_GRAPH_NODE_TYPE_EVENT_RECORD as _,
    ExternalSemaphoresSignal = driver::CUgraphNodeType::CU_GRAPH_NODE_TYPE_EXT_SEMAS_SIGNAL as _,
    ExternalSemaphoresWait = driver::CUgraphNodeType::CU_GRAPH_NODE_TYPE_EXT_SEMAS_WAIT as _,
    MemoryAlloc = driver::CUgraphNodeType::CU_GRAPH_NODE_TYPE_MEM_ALLOC as _,
    MemoryFree = driver::CUgraphNodeType::CU_GRAPH_NODE_TYPE_MEM_FREE as _,
    BatchMemOp = driver::CUgraphNodeType::CU_GRAPH_NODE_TYPE_BATCH_MEM_OP as _,
    Conditional = driver::CUgraphNodeType::CU_GRAPH_NODE_TYPE_CONDITIONAL as _,
}

impl_enum_conversion!(u32, runtime::cudaGraphNodeType, GraphNodeType);

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, IntoPrimitive)]
#[repr(u8)]
pub enum GraphDependencyType {
    Default = driver::CUgraphDependencyType::CU_GRAPH_DEPENDENCY_TYPE_DEFAULT as _,
    Programmatic = driver::CUgraphDependencyType::CU_GRAPH_DEPENDENCY_TYPE_PROGRAMMATIC as _,
}

impl From<driver::CUgraphDependencyType> for GraphDependencyType {
    fn from(value: driver::CUgraphDependencyType) -> Self {
        match value {
            driver::CUgraphDependencyType::CU_GRAPH_DEPENDENCY_TYPE_DEFAULT => Self::Default,
            driver::CUgraphDependencyType::CU_GRAPH_DEPENDENCY_TYPE_PROGRAMMATIC => {
                Self::Programmatic
            }
        }
    }
}

impl From<GraphDependencyType> for driver::CUgraphDependencyType {
    fn from(value: GraphDependencyType) -> Self {
        match value {
            GraphDependencyType::Default => Self::CU_GRAPH_DEPENDENCY_TYPE_DEFAULT,
            GraphDependencyType::Programmatic => Self::CU_GRAPH_DEPENDENCY_TYPE_PROGRAMMATIC,
        }
    }
}

impl Display for GraphNodeType {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            Self::Kernel => write!(f, "cudaGraphNodeTypeKernel"),
            Self::Memcpy => write!(f, "cudaGraphNodeTypeMemcpy"),
            Self::Memset => write!(f, "cudaGraphNodeTypeMemset"),
            Self::Host => write!(f, "cudaGraphNodeTypeHost"),
            Self::Graph => write!(f, "cudaGraphNodeTypeGraph"),
            Self::Empty => write!(f, "cudaGraphNodeTypeEmpty"),
            Self::WaitEvent => write!(f, "cudaGraphNodeTypeWaitEvent"),
            Self::EventRecord => write!(f, "cudaGraphNodeTypeEventRecord"),
            Self::ExternalSemaphoresSignal => {
                write!(f, "cudaGraphNodeTypeExternalSemaphoresSignal")
            }
            Self::ExternalSemaphoresWait => {
                write!(f, "cudaGraphNodeTypeExternalSemaphoresWait")
            }
            Self::MemoryAlloc => write!(f, "cudaGraphNodeTypeMemAlloc"),
            Self::MemoryFree => write!(f, "cudaGraphNodeTypeMemFree"),
            Self::BatchMemOp => write!(f, "cudaGraphNodeTypeBatchMemOp"),
            Self::Conditional => write!(f, "cudaGraphNodeTypeConditional"),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, IntoPrimitive)]
#[repr(u32)]
pub enum GraphExecUpdateResult {
    Success = driver::CUgraphExecUpdateResult::CU_GRAPH_EXEC_UPDATE_SUCCESS as _,
    Error = driver::CUgraphExecUpdateResult::CU_GRAPH_EXEC_UPDATE_ERROR as _,
    ErrorTopologyChanged =
        driver::CUgraphExecUpdateResult::CU_GRAPH_EXEC_UPDATE_ERROR_TOPOLOGY_CHANGED as _,
    ErrorNodeTypeChanged =
        driver::CUgraphExecUpdateResult::CU_GRAPH_EXEC_UPDATE_ERROR_NODE_TYPE_CHANGED as _,
    ErrorFunctionChanged =
        driver::CUgraphExecUpdateResult::CU_GRAPH_EXEC_UPDATE_ERROR_FUNCTION_CHANGED as _,
    ErrorParametersChanged =
        driver::CUgraphExecUpdateResult::CU_GRAPH_EXEC_UPDATE_ERROR_PARAMETERS_CHANGED as _,
    ErrorNotSupported =
        driver::CUgraphExecUpdateResult::CU_GRAPH_EXEC_UPDATE_ERROR_NOT_SUPPORTED as _,
    ErrorUnsupportedFunctionChange =
        driver::CUgraphExecUpdateResult::CU_GRAPH_EXEC_UPDATE_ERROR_UNSUPPORTED_FUNCTION_CHANGE
            as _,
    ErrorAttributesChanged =
        driver::CUgraphExecUpdateResult::CU_GRAPH_EXEC_UPDATE_ERROR_ATTRIBUTES_CHANGED as _,
}

impl_enum_conversion!(driver::CUgraphExecUpdateResult, GraphExecUpdateResult);

impl Display for GraphExecUpdateResult {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            Self::Success => write!(f, "CU_GRAPH_EXEC_UPDATE_SUCCESS"),
            Self::Error => write!(f, "CU_GRAPH_EXEC_UPDATE_ERROR"),
            Self::ErrorTopologyChanged => {
                write!(f, "CU_GRAPH_EXEC_UPDATE_ERROR_TOPOLOGY_CHANGED")
            }
            Self::ErrorNodeTypeChanged => {
                write!(f, "CU_GRAPH_EXEC_UPDATE_ERROR_NODE_TYPE_CHANGED")
            }
            Self::ErrorFunctionChanged => {
                write!(f, "CU_GRAPH_EXEC_UPDATE_ERROR_FUNCTION_CHANGED")
            }
            Self::ErrorParametersChanged => {
                write!(f, "CU_GRAPH_EXEC_UPDATE_ERROR_PARAMETERS_CHANGED")
            }
            Self::ErrorNotSupported => {
                write!(f, "CU_GRAPH_EXEC_UPDATE_ERROR_NOT_SUPPORTED")
            }
            Self::ErrorUnsupportedFunctionChange => {
                write!(f, "CU_GRAPH_EXEC_UPDATE_ERROR_UNSUPPORTED_FUNCTION_CHANGE")
            }
            Self::ErrorAttributesChanged => {
                write!(f, "CU_GRAPH_EXEC_UPDATE_ERROR_ATTRIBUTES_CHANGED")
            }
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GraphNode {
    handle: runtime::cudaGraphNode_t,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GraphEdgeData {
    pub from_port: u8,
    pub to_port: u8,
    pub dependency_type: GraphDependencyType,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GraphDependency {
    pub node: GraphNode,
    pub data: GraphEdgeData,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GraphEdge {
    pub from: GraphNode,
    pub to: GraphNode,
    pub data: GraphEdgeData,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Position {
    pub x: usize,
    pub y: usize,
    pub z: usize,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Extent {
    pub width: usize,
    pub height: usize,
    pub depth: usize,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PitchedPtr {
    ptr: *mut (),
    pub pitch: usize,
    pub x_size: usize,
    pub y_size: usize,
}

impl PitchedPtr {
    pub const fn new(ptr: *mut (), pitch: usize, x_size: usize, y_size: usize) -> Self {
        Self {
            ptr,
            pitch,
            x_size,
            y_size,
        }
    }

    pub const fn ptr(self) -> *mut () {
        self.ptr
    }
}

#[derive(Debug, Clone, Copy)]
pub struct Memcpy3DNodeParams {
    pub src_array: Option<ArrayHandle>,
    pub src_pos: Position,
    pub src_ptr: PitchedPtr,
    pub dst_array: Option<ArrayHandle>,
    pub dst_pos: Position,
    pub dst_ptr: PitchedPtr,
    pub extent: Extent,
    pub kind: MemoryCopyKind,
}

#[derive(Debug, Clone, Copy)]
pub struct MemcpyToSymbolNodeParams {
    pub symbol: *const (),
    pub src: *const (),
    pub count: usize,
    pub offset: usize,
    pub kind: MemoryCopyKind,
}

#[derive(Debug, Clone, Copy)]
pub struct MemcpyFromSymbolNodeParams {
    pub dst: *mut (),
    pub symbol: *const (),
    pub count: usize,
    pub offset: usize,
    pub kind: MemoryCopyKind,
}

#[derive(Debug, Clone)]
pub struct MemAllocNodeParams<'a> {
    pub pool_props: MemoryPoolProps,
    pub access_descs: &'a [MemAccessDescriptor],
    pub byte_size: usize,
}

impl Default for GraphEdgeData {
    fn default() -> Self {
        Self {
            from_port: 0,
            to_port: 0,
            dependency_type: GraphDependencyType::Default,
        }
    }
}

impl From<runtime::cudaGraphEdgeData> for GraphEdgeData {
    fn from(value: runtime::cudaGraphEdgeData) -> Self {
        Self {
            from_port: value.from_port,
            to_port: value.to_port,
            dependency_type: GraphDependencyType::try_from(value.type_)
                .unwrap_or(GraphDependencyType::Default),
        }
    }
}

impl From<GraphEdgeData> for runtime::cudaGraphEdgeData {
    fn from(value: GraphEdgeData) -> Self {
        Self {
            from_port: value.from_port,
            to_port: value.to_port,
            type_: value.dependency_type.into(),
            reserved: [0; 5],
        }
    }
}

impl From<Position> for runtime::cudaPos {
    fn from(value: Position) -> Self {
        Self {
            x: value.x as _,
            y: value.y as _,
            z: value.z as _,
        }
    }
}

impl From<Extent> for runtime::cudaExtent {
    fn from(value: Extent) -> Self {
        Self {
            width: value.width as _,
            height: value.height as _,
            depth: value.depth as _,
        }
    }
}

impl From<PitchedPtr> for runtime::cudaPitchedPtr {
    fn from(value: PitchedPtr) -> Self {
        Self {
            ptr: value.ptr.cast(),
            pitch: value.pitch as _,
            xsize: value.x_size as _,
            ysize: value.y_size as _,
        }
    }
}

impl From<&Memcpy3DNodeParams> for runtime::cudaMemcpy3DParms {
    fn from(value: &Memcpy3DNodeParams) -> Self {
        Self {
            srcArray: value
                .src_array
                .map_or(ptr::null_mut(), |handle| unsafe { handle.as_raw() }),
            srcPos: value.src_pos.into(),
            srcPtr: value.src_ptr.into(),
            dstArray: value
                .dst_array
                .map_or(ptr::null_mut(), |handle| unsafe { handle.as_raw() }),
            dstPos: value.dst_pos.into(),
            dstPtr: value.dst_ptr.into(),
            extent: value.extent.into(),
            kind: value.kind.into(),
        }
    }
}

impl GraphNode {
    const unsafe fn from_raw(handle: runtime::cudaGraphNode_t) -> Self {
        Self { handle }
    }

    /// Returns the node type.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn node_type(self) -> Result<GraphNodeType> {
        let mut kind = runtime::cudaGraphNodeType::CU_GRAPH_NODE_TYPE_KERNEL;
        unsafe {
            try_cuda!(runtime::cudaGraphNodeGetType(self.handle, &raw mut kind))?;
        }
        Ok(kind.into())
    }

    /// Returns this node's dependencies.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn dependencies(self) -> Result<Vec<GraphDependency>> {
        unsafe {
            let mut count = 0;
            try_cuda!(runtime::cudaGraphNodeGetDependencies(
                self.handle,
                ptr::null_mut(),
                ptr::null_mut(),
                &raw mut count,
            ))?;

            if count == 0 {
                return Ok(Vec::new());
            }

            let mut handles = Vec::with_capacity(count as usize);
            let mut edge_data = Vec::with_capacity(count as usize);
            try_cuda!(runtime::cudaGraphNodeGetDependencies(
                self.handle,
                handles.as_mut_ptr(),
                edge_data.as_mut_ptr(),
                &raw mut count,
            ))?;
            handles.set_len(count as usize);
            edge_data.set_len(count as usize);

            Ok(handles
                .into_iter()
                .zip(edge_data)
                .map(|(handle, data)| GraphDependency {
                    node: Self { handle },
                    data: data.into(),
                })
                .collect())
        }
    }

    /// Returns this node's dependent nodes.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn dependent_nodes(self) -> Result<Vec<GraphDependency>> {
        unsafe {
            let mut count = 0;
            try_cuda!(runtime::cudaGraphNodeGetDependentNodes(
                self.handle,
                ptr::null_mut(),
                ptr::null_mut(),
                &raw mut count,
            ))?;

            if count == 0 {
                return Ok(Vec::new());
            }

            let mut handles = Vec::with_capacity(count as usize);
            let mut edge_data = Vec::with_capacity(count as usize);
            try_cuda!(runtime::cudaGraphNodeGetDependentNodes(
                self.handle,
                handles.as_mut_ptr(),
                edge_data.as_mut_ptr(),
                &raw mut count,
            ))?;
            handles.set_len(count as usize);
            edge_data.set_len(count as usize);

            Ok(handles
                .into_iter()
                .zip(edge_data)
                .map(|(handle, data)| GraphDependency {
                    node: Self { handle },
                    data: data.into(),
                })
                .collect())
        }
    }

    /// Returns the event of this event record node.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn event_record_node_event(self) -> Result<runtime::cudaEvent_t> {
        let mut event = ptr::null_mut();
        unsafe {
            try_cuda!(runtime::cudaGraphEventRecordNodeGetEvent(
                self.handle,
                &raw mut event,
            ))?;
        }
        if event.is_null() {
            return Err(Error::NullHandle);
        }
        Ok(event)
    }

    /// Returns the event of this event wait node.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn event_wait_node_event(self) -> Result<runtime::cudaEvent_t> {
        let mut event = ptr::null_mut();
        unsafe {
            try_cuda!(runtime::cudaGraphEventWaitNodeGetEvent(
                self.handle,
                &raw mut event,
            ))?;
        }
        if event.is_null() {
            return Err(Error::NullHandle);
        }
        Ok(event)
    }

    /// Gets a handle to the embedded graph in a child graph node.
    /// This call does not clone the graph.
    /// Changes to the graph will be reflected in the node, and the node retains ownership of the graph.
    ///
    /// Allocation and free nodes cannot be added to the returned graph.
    /// Attempting to do so will return an error.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn child_graph(self) -> Result<Graph> {
        let mut graph = ptr::null_mut();
        unsafe {
            try_cuda!(runtime::cudaGraphChildGraphNodeGetGraph(
                self.handle,
                &raw mut graph,
            ))?;
        }
        if graph.is_null() {
            return Err(Error::NullHandle);
        }
        Ok(unsafe { Graph::from_raw_borrowed(graph) })
    }

    /// Returns the parameters of this memcpy node.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn memcpy_node_params(self) -> Result<runtime::cudaMemcpy3DParms> {
        let mut params = runtime::cudaMemcpy3DParms::default();
        unsafe {
            try_cuda!(runtime::cudaGraphMemcpyNodeGetParams(
                self.handle,
                &raw mut params,
            ))?;
        }
        Ok(params)
    }

    /// Returns the parameters of this memset node.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn memset_node_params(self) -> Result<driver::CUDA_MEMSET_NODE_PARAMS> {
        let mut params = driver::CUDA_MEMSET_NODE_PARAMS::default();
        unsafe {
            try_cuda!(runtime::cudaGraphMemsetNodeGetParams(
                self.handle,
                &raw mut params,
            ))?;
        }
        Ok(params)
    }

    /// Returns the parameters of this host node.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn host_node_params(self) -> Result<driver::CUDA_HOST_NODE_PARAMS> {
        let mut params = driver::CUDA_HOST_NODE_PARAMS::default();
        unsafe {
            try_cuda!(runtime::cudaGraphHostNodeGetParams(
                self.handle,
                &raw mut params,
            ))?;
        }
        Ok(params)
    }

    /// Returns the parameters of a memory allocation node.
    /// The `poolProps` and `accessDescs` values in the returned parameters are owned by the node.
    /// This memory remains valid until the node is destroyed.
    /// The returned parameters must not be modified.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn mem_alloc_node_info(self) -> Result<MemoryAllocationNodeInfo> {
        let mut params = runtime::cudaMemAllocNodeParams::default();
        unsafe {
            try_cuda!(runtime::cudaGraphMemAllocNodeGetParams(
                self.handle,
                &raw mut params,
            ))?;
        }
        Ok(MemoryAllocationNodeInfo {
            ptr: DevicePtr::new(params.dptr as _),
            byte_size: params.bytesize as usize,
        })
    }

    /// Returns the address of this memory free node.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub unsafe fn mem_free_node_ptr(self) -> Result<DevicePtr> {
        let ptr = ptr::null_mut();
        unsafe {
            try_cuda!(runtime::cudaGraphMemFreeNodeGetParams(self.handle, ptr))?;
        }
        Ok(DevicePtr::new(ptr as _))
    }

    /// Returns the requested kernel node attribute.
    ///
    /// Note:
    ///
    /// Note that this function may also return error codes from previous, asynchronous launches.
    pub fn kernel_node_attribute(
        self,
        id: GraphKernelNodeAttributeId,
    ) -> Result<GraphKernelNodeAttribute> {
        let mut value = runtime::cudaLaunchAttributeValue::default();
        unsafe {
            try_cuda!(runtime::cudaGraphKernelNodeGetAttribute(
                self.handle,
                id.into(),
                &raw mut value,
            ))?;

            Ok(match id {
                GraphKernelNodeAttributeId::Cooperative => {
                    GraphKernelNodeAttribute::Cooperative(*value.cooperative.as_ref() != 0)
                }
                GraphKernelNodeAttributeId::ClusterDimension => {
                    let dim = value.clusterDim.as_ref();
                    GraphKernelNodeAttribute::ClusterDimension(Dim3::new(dim.x, dim.y, dim.z))
                }
                GraphKernelNodeAttributeId::Priority => {
                    GraphKernelNodeAttribute::Priority(*value.priority.as_ref())
                }
                GraphKernelNodeAttributeId::PreferredSharedMemoryCarveout => {
                    GraphKernelNodeAttribute::PreferredSharedMemoryCarveout(
                        *value.sharedMemCarveout.as_ref(),
                    )
                }
            })
        }
    }

    /// Sets a kernel node attribute.
    ///
    /// Note:
    ///
    /// Note that this function may also return error codes from previous, asynchronous launches.
    pub fn set_kernel_node_attribute(&mut self, attribute: GraphKernelNodeAttribute) -> Result<()> {
        let (id, value) = match attribute {
            GraphKernelNodeAttribute::Cooperative(value) => {
                let mut attr = runtime::cudaLaunchAttributeValue {
                    cooperative: runtime::__BindgenUnionField::new(),
                    ..runtime::cudaLaunchAttributeValue::default()
                };
                unsafe { *attr.cooperative.as_mut() = i32::from(value) };
                (GraphKernelNodeAttributeId::Cooperative, attr)
            }
            GraphKernelNodeAttribute::ClusterDimension(value) => {
                let mut attr = runtime::cudaLaunchAttributeValue {
                    clusterDim: runtime::__BindgenUnionField::new(),
                    ..runtime::cudaLaunchAttributeValue::default()
                };
                unsafe {
                    *attr.clusterDim.as_mut() = runtime::cudaLaunchAttributeValue__bindgen_ty_1 {
                        x: value.x,
                        y: value.y,
                        z: value.z,
                    };
                }
                (GraphKernelNodeAttributeId::ClusterDimension, attr)
            }
            GraphKernelNodeAttribute::Priority(value) => {
                let mut attr = runtime::cudaLaunchAttributeValue {
                    priority: runtime::__BindgenUnionField::new(),
                    ..runtime::cudaLaunchAttributeValue::default()
                };
                unsafe { *attr.priority.as_mut() = value as _ };
                (GraphKernelNodeAttributeId::Priority, attr)
            }
            GraphKernelNodeAttribute::PreferredSharedMemoryCarveout(value) => {
                let mut attr = runtime::cudaLaunchAttributeValue {
                    sharedMemCarveout: runtime::__BindgenUnionField::new(),
                    ..runtime::cudaLaunchAttributeValue::default()
                };
                unsafe { *attr.sharedMemCarveout.as_mut() = value };
                (
                    GraphKernelNodeAttributeId::PreferredSharedMemoryCarveout,
                    attr,
                )
            }
        };

        unsafe {
            try_cuda!(runtime::cudaGraphKernelNodeSetAttribute(
                self.handle,
                id.into(),
                &raw const value,
            ))?;
        }
        Ok(())
    }

    /// Copies attributes from `src` to this node.
    /// Both nodes must have the same context.
    ///
    /// Note:
    ///
    /// Note that this function may also return error codes from previous, asynchronous launches.
    pub fn copy_kernel_node_attributes(self, other: Self) -> Result<()> {
        unsafe {
            try_cuda!(runtime::cudaGraphKernelNodeCopyAttributes(
                self.handle,
                other.handle
            ))?;
        }
        Ok(())
    }

    pub const unsafe fn as_raw(self) -> runtime::cudaGraphNode_t {
        self.handle
    }
}

impl MemoryAllocationNodeInfo {
    pub const fn ptr(&self) -> DevicePtr {
        self.ptr
    }
}

#[derive(Debug)]
pub struct Graph {
    handle: runtime::cudaGraph_t,
    owns_handle: bool,
}

impl Graph {
    pub(crate) const unsafe fn from_raw(handle: runtime::cudaGraph_t) -> Self {
        Self {
            handle,
            owns_handle: true,
        }
    }

    pub(crate) const unsafe fn from_raw_borrowed(handle: runtime::cudaGraph_t) -> Self {
        Self {
            handle,
            owns_handle: false,
        }
    }

    /// Creates an empty graph.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn create() -> Result<Self> {
        let mut handle = ptr::null_mut();
        unsafe {
            try_cuda!(runtime::cudaGraphCreate(&raw mut handle, 0))?;
        }
        Ok(Self {
            handle,
            owns_handle: true,
        })
    }

    pub fn instantiate(&self) -> Result<ExecutableGraph> {
        self.instantiate_with_flags(GraphInstantiateFlags::empty())
    }

    /// Instantiates graph as an executable graph.
    /// The graph is validated for any structural constraints or intra-node constraints which were not previously validated.
    /// If instantiation is successful, returns an instantiated executable graph.
    ///
    /// The flags parameter controls the behavior of instantiation and subsequent graph launches.
    /// Valid flags are:
    ///
    /// * [`GraphInstantiateFlags::AUTO_FREE_ON_LAUNCH`], which configures a graph containing memory allocation nodes to automatically free any unfreed memory allocations before
    ///    the graph is relaunched.
    ///
    /// * [`GraphInstantiateFlags::DEVICE_LAUNCH`], which configures the graph for launch from the device.
    ///    If this flag is passed, the executable graph handle returned can
    ///    be used to launch the graph from both the host and device.
    ///    This flag can only be used on platforms which support unified addressing.
    ///    This flag cannot be used in conjunction with [`GraphInstantiateFlags::AUTO_FREE_ON_LAUNCH`].
    ///
    /// * [`GraphInstantiateFlags::USE_NODE_PRIORITY`], which causes the graph to use the priorities from the per-node attributes rather than the priority of the launch stream
    ///    during execution.
    ///    Note that priorities are only available on kernel nodes, and are copied from stream priority during stream
    ///    capture.
    ///
    /// If graph contains any allocation or free nodes, there can be at most one executable graph in existence for that graph at a time.
    /// An attempt to instantiate a second executable graph before dropping the first will result in an error.
    /// The same also applies if graph contains any device-updatable kernel nodes.
    ///
    /// If graph contains kernels which call device-side [`ExecutableGraph::launch`] from multiple devices, this will result in an error.
    ///
    /// Graphs instantiated for launch on the device have additional restrictions which do not apply to host graphs:
    ///
    /// * The graph's nodes must reside on a single device.
    /// * The graph can only contain kernel nodes, memcpy nodes, memset nodes, and child graph nodes.
    /// * The graph cannot be empty and must contain at least one kernel, memcpy, or memset node.
    ///    Operation-specific restrictions are
    ///    outlined below.
    /// * Kernel nodes:
    ///   + Use of CUDA Dynamic Parallelism is not permitted.
    ///   + Cooperative launches are permitted as long as MPS is not in use.
    /// * Memcpy nodes:
    ///   + Only copies involving device memory and/or pinned device-mapped host memory are permitted.
    ///   + Copies involving CUDA arrays are not permitted.
    ///   + Both operands must be accessible from the current device, and the current device must match the device of other nodes in the
    ///      graph.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn instantiate_with_flags(&self, flags: GraphInstantiateFlags) -> Result<ExecutableGraph> {
        let mut handle = ptr::null_mut();
        unsafe {
            try_cuda!(runtime::cudaGraphInstantiateWithFlags(
                &raw mut handle,
                self.handle,
                flags.bits(),
            ))?;
        }
        Ok(ExecutableGraph { handle })
    }

    /// Creates a copy of `original_graph`.
    /// All parameters are copied into the cloned graph.
    /// The original graph may be modified after this call without affecting the clone.
    ///
    /// Child graph nodes in the original graph are recursively copied into the clone.
    ///
    /// Note:
    ///
    /// : Cloning is not supported for graphs which contain memory allocation nodes, memory free nodes, or conditional nodes.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn try_clone(&self) -> Result<Self> {
        let mut handle = ptr::null_mut();
        unsafe {
            try_cuda!(runtime::cudaGraphClone(&raw mut handle, self.handle))?;
        }
        Ok(Self {
            handle,
            owns_handle: true,
        })
    }

    pub fn add_dependency(&mut self, from: GraphNode, to: GraphNode) -> Result<()> {
        self.add_dependencies(&[from], &[to])
    }

    pub fn add_dependencies(&mut self, from: &[GraphNode], to: &[GraphNode]) -> Result<()> {
        self.add_dependencies_with_data(from, to, &[])
    }

    /// Elements in `from` and `to` at corresponding indices define each dependency to add.
    /// Each node in `from` and `to` must belong to this graph.
    ///
    /// If `from` and `to` are empty, the call returns without modifying the graph.
    /// Specifying an existing dependency will return an error.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn add_dependencies_with_data(
        &mut self,
        from: &[GraphNode],
        to: &[GraphNode],
        edge_data: &[GraphEdgeData],
    ) -> Result<()> {
        if from.len() != to.len() {
            return Err(Error::GraphDependencyMismatch);
        }
        if !edge_data.is_empty() && edge_data.len() != from.len() {
            return Err(Error::GraphDependencyMismatch);
        }
        if from.is_empty() {
            return Ok(());
        }

        let from_raw: Vec<_> = from.iter().map(|node| node.handle).collect();
        let to_raw: Vec<_> = to.iter().map(|node| node.handle).collect();
        let edge_data_raw: Vec<_> = edge_data.iter().copied().map(Into::into).collect();
        unsafe {
            try_cuda!(runtime::cudaGraphAddDependencies(
                self.handle,
                from_raw.as_ptr(),
                to_raw.as_ptr(),
                if edge_data_raw.is_empty() {
                    ptr::null()
                } else {
                    edge_data_raw.as_ptr()
                },
                from_raw.len() as runtime::size_t,
            ))?;
        }
        Ok(())
    }

    pub fn remove_dependency(&mut self, from: GraphNode, to: GraphNode) -> Result<()> {
        self.remove_dependencies(&[from], &[to])
    }

    pub fn remove_dependencies(&mut self, from: &[GraphNode], to: &[GraphNode]) -> Result<()> {
        self.remove_dependencies_with_data(from, to, &[])
    }

    /// Elements in `from` and `to` at corresponding indices define each dependency to remove.
    /// Each node in `from` and `to` must belong to this graph.
    ///
    /// If `from` and `to` are empty, the call returns without modifying the graph.
    /// Specifying an edge that does not exist in the graph, with data matching `edge_data`, results in an error.
    /// Passing an empty `edge_data` slice is equivalent to passing default edge data for each edge.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn remove_dependencies_with_data(
        &mut self,
        from: &[GraphNode],
        to: &[GraphNode],
        edge_data: &[GraphEdgeData],
    ) -> Result<()> {
        if from.len() != to.len() {
            return Err(Error::GraphDependencyMismatch);
        }
        if !edge_data.is_empty() && edge_data.len() != from.len() {
            return Err(Error::GraphDependencyMismatch);
        }
        if from.is_empty() {
            return Ok(());
        }

        let from_raw: Vec<_> = from.iter().map(|node| node.handle).collect();
        let to_raw: Vec<_> = to.iter().map(|node| node.handle).collect();
        let edge_data_raw: Vec<_> = edge_data.iter().copied().map(Into::into).collect();
        unsafe {
            try_cuda!(runtime::cudaGraphRemoveDependencies(
                self.handle,
                from_raw.as_ptr(),
                to_raw.as_ptr(),
                if edge_data_raw.is_empty() {
                    ptr::null()
                } else {
                    edge_data_raw.as_ptr()
                },
                from_raw.len() as runtime::size_t,
            ))?;
        }
        Ok(())
    }

    pub fn add_edges(&mut self, edges: &[GraphEdge]) -> Result<()> {
        if edges.is_empty() {
            return Ok(());
        }

        let from: Vec<_> = edges.iter().map(|edge| edge.from).collect();
        let to: Vec<_> = edges.iter().map(|edge| edge.to).collect();
        let data: Vec<_> = edges.iter().map(|edge| edge.data).collect();
        self.add_dependencies_with_data(&from, &to, &data)
    }

    pub fn remove_edges(&mut self, edges: &[GraphEdge]) -> Result<()> {
        if edges.is_empty() {
            return Ok(());
        }

        let from: Vec<_> = edges.iter().map(|edge| edge.from).collect();
        let to: Vec<_> = edges.iter().map(|edge| edge.to).collect();
        let data: Vec<_> = edges.iter().map(|edge| edge.data).collect();
        self.remove_dependencies_with_data(&from, &to, &data)
    }

    /// Creates a new node which performs no operation, and adds it to the graph with the given dependencies.
    /// The dependency list may be empty, in which case the node will be placed at the root of the graph, and it may not contain duplicate entries.
    ///
    /// An empty node performs no operation during execution, but can be used for transitive ordering.
    /// For example, a phased execution graph with 2 groups of n nodes with a barrier between them can be represented using an empty node and 2\*n dependency edges, rather than no empty node and n^2 dependency edges.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn add_empty_node(&mut self, dependencies: &[GraphNode]) -> Result<GraphNode> {
        let mut handle = ptr::null_mut();
        let dependencies_raw: Vec<_> = dependencies.iter().map(|node| node.handle).collect();
        unsafe {
            try_cuda!(runtime::cudaGraphAddEmptyNode(
                &raw mut handle,
                self.handle,
                dependencies_raw.as_ptr(),
                dependencies_raw.len() as runtime::size_t,
            ))?;
            Ok(GraphNode::from_raw(handle))
        }
    }

    /// Creates a new event record node and adds it to the graph with the given dependencies and event.
    /// The dependency list may be empty, in which case the node will be placed at the root of the graph, and it may not contain duplicate entries.
    ///
    /// Each launch of the graph will record event to capture execution of the node's dependencies.
    ///
    /// These nodes may not be used in loops or conditionals.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn add_event_record_node(
        &mut self,
        dependencies: &[GraphNode],
        event: &Event,
    ) -> Result<GraphNode> {
        let mut handle = ptr::null_mut();
        let dependencies_raw: Vec<_> = dependencies.iter().map(|node| node.handle).collect();
        unsafe {
            try_cuda!(runtime::cudaGraphAddEventRecordNode(
                &raw mut handle,
                self.handle,
                dependencies_raw.as_ptr(),
                dependencies_raw.len() as runtime::size_t,
                event.as_raw(),
            ))?;
            Ok(GraphNode::from_raw(handle))
        }
    }

    /// Creates a new event wait node and adds it to the graph with the given dependencies and event.
    /// The dependency list may be empty, in which case the node will be placed at the root of the graph, and it may not contain duplicate entries.
    ///
    /// The graph node will wait for all work captured in event.
    /// See [`sys::cuEventRecord`](singe_cuda_sys::driver::cuEventRecord) for details on what is captured by an event.
    /// The synchronization will be performed efficiently on the device when applicable. event may be from a different context or device than the launch stream.
    ///
    /// These nodes may not be used in loops or conditionals.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn add_event_wait_node(
        &mut self,
        dependencies: &[GraphNode],
        event: &Event,
    ) -> Result<GraphNode> {
        let mut handle = ptr::null_mut();
        let dependencies_raw: Vec<_> = dependencies.iter().map(|node| node.handle).collect();
        unsafe {
            try_cuda!(runtime::cudaGraphAddEventWaitNode(
                &raw mut handle,
                self.handle,
                dependencies_raw.as_ptr(),
                dependencies_raw.len() as runtime::size_t,
                event.as_raw(),
            ))?;
            Ok(GraphNode::from_raw(handle))
        }
    }

    /// Creates a new CPU execution node and adds it to the graph with the given dependencies and host-node parameters.
    /// The dependency list may be empty, in which case the node will be placed at the root of the graph, and it may not contain duplicate entries.
    ///
    /// When the graph is launched, the node will invoke the specified CPU function.
    /// Host nodes are not supported under MPS with pre-Volta GPUs.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn add_host_node(
        &mut self,
        dependencies: &[GraphNode],
        params: &HostNodeParams,
    ) -> Result<GraphNode> {
        let mut handle = ptr::null_mut();
        let dependencies_raw: Vec<_> = dependencies.iter().map(|node| node.handle).collect();
        let params = params.into();
        unsafe {
            try_cuda!(runtime::cudaGraphAddHostNode(
                &raw mut handle,
                self.handle,
                dependencies_raw.as_ptr(),
                dependencies_raw.len() as runtime::size_t,
                &raw const params,
            ))?;
            Ok(GraphNode::from_raw(handle))
        }
    }

    /// Creates a new kernel execution node and adds it to the graph with the given dependencies, launch configuration, and kernel parameters.
    /// The dependency list may be empty, in which case the node will be placed at the root of the graph, and it may not contain duplicate entries.
    ///
    /// When the graph is launched, the node will invoke the kernel on the grid and blocks specified by [`LaunchConfig`](crate::module::LaunchConfig).
    /// [`LaunchConfig::shared_memory_bytes`](crate::module::LaunchConfig::shared_memory_bytes) sets the amount of dynamic shared memory that will be available to each thread block.
    /// Kernel parameters are passed with [`KernelParameters`](crate::module::KernelParameters).
    ///
    /// Note:
    ///
    /// Kernels launched using graphs must not use texture and surface references.
    /// Reading or writing through any texture or surface reference is undefined behavior.
    /// This restriction does not apply to texture and surface objects.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    /// * The underlying CUDA API can also be used with a runtime kernel handle queried via [`sys::cudaLibraryGetKernel`](singe_cuda_sys::runtime::cudaLibraryGetKernel) or [`sys::cudaGetKernel`](singe_cuda_sys::runtime::cudaGetKernel).
    ///    The symbol passed to [`sys::cudaGetKernel`](singe_cuda_sys::runtime::cudaGetKernel) should be registered with the same CUDA Runtime instance.
    /// * Passing a symbol that belongs to a different runtime instance will result in undefined behavior.
    pub fn add_kernel_node(
        &mut self,
        dependencies: &[GraphNode],
        params: &KernelNodeParams<'_>,
    ) -> Result<GraphNode> {
        let mut handle = ptr::null_mut();
        let dependencies_raw: Vec<_> = dependencies.iter().map(|node| node.handle).collect();
        let params = params.into();
        unsafe {
            try_cuda!(runtime::cudaGraphAddKernelNode(
                &raw mut handle,
                self.handle,
                dependencies_raw.as_ptr(),
                dependencies_raw.len() as runtime::size_t,
                &raw const params,
            ))?;
            Ok(GraphNode::from_raw(handle))
        }
    }

    /// Creates a new 1D memcpy node and adds it to the graph with the given dependencies.
    /// The dependency list may be empty, in which case the node will be placed at the root of the graph, and it may not contain duplicate entries.
    ///
    /// When the graph is launched, the node copies `count` bytes from `src` to `dst`.
    /// The transfer direction is described by [`MemoryCopyKind`].
    /// [`MemoryCopyKind::Default`] is recommended when unified virtual addressing is available, in which case the transfer direction is inferred from the pointer values.
    /// Launching a memcpy node with dst and src pointers that do not match the direction of the copy results in an undefined behavior.
    ///
    /// Memcpy nodes have additional restrictions for managed memory if any device in the system does not support concurrent managed access.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn add_memcpy_node_1d(
        &mut self,
        dependencies: &[GraphNode],
        params: &Memcpy1DNodeParams,
    ) -> Result<GraphNode> {
        let mut handle = ptr::null_mut();
        let dependencies_raw: Vec<_> = dependencies.iter().map(|node| node.handle).collect();
        unsafe {
            try_cuda!(runtime::cudaGraphAddMemcpyNode1D(
                &raw mut handle,
                self.handle,
                dependencies_raw.as_ptr(),
                dependencies_raw.len() as runtime::size_t,
                params.dst.cast(),
                params.src.cast(),
                params.count as _,
                params.kind.into(),
            ))?;
            Ok(GraphNode::from_raw(handle))
        }
    }

    /// Creates a new memcpy node and adds it to the graph with the given dependencies.
    /// The dependency list may be empty, in which case the node will be placed at the root of the graph, and it may not contain duplicate entries.
    ///
    /// When the graph is launched, the node will perform the memcpy described by `params`.
    /// See [`sys::cudaMemcpy3D`](singe_cuda_sys::runtime::cudaMemcpy3D) for a description of the structure and its restrictions.
    ///
    /// Memcpy nodes have additional restrictions for managed memory if any device in the system does not support concurrent managed access.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn add_memcpy_node(
        &mut self,
        dependencies: &[GraphNode],
        params: &Memcpy3DNodeParams,
    ) -> Result<GraphNode> {
        let mut handle = ptr::null_mut();
        let dependencies_raw: Vec<_> = dependencies.iter().map(|node| node.handle).collect();
        let params = params.into();
        unsafe {
            try_cuda!(runtime::cudaGraphAddMemcpyNode(
                &raw mut handle,
                self.handle,
                dependencies_raw.as_ptr(),
                dependencies_raw.len() as runtime::size_t,
                &raw const params,
            ))?;
            Ok(GraphNode::from_raw(handle))
        }
    }

    pub fn add_memcpy_node_to_symbol(
        &mut self,
        dependencies: &[GraphNode],
        params: &MemcpyToSymbolNodeParams,
    ) -> Result<GraphNode> {
        let mut handle = ptr::null_mut();
        let dependencies_raw: Vec<_> = dependencies.iter().map(|node| node.handle).collect();
        unsafe {
            try_cuda!(runtime::cudaGraphAddMemcpyNodeToSymbol(
                &raw mut handle,
                self.handle,
                dependencies_raw.as_ptr(),
                dependencies_raw.len() as runtime::size_t,
                params.symbol.cast(),
                params.src.cast(),
                params.count as _,
                params.offset as _,
                params.kind.into(),
            ))?;
            Ok(GraphNode::from_raw(handle))
        }
    }

    pub fn add_memcpy_node_from_symbol(
        &mut self,
        dependencies: &[GraphNode],
        params: &MemcpyFromSymbolNodeParams,
    ) -> Result<GraphNode> {
        let mut handle = ptr::null_mut();
        let dependencies_raw: Vec<_> = dependencies.iter().map(|node| node.handle).collect();
        unsafe {
            try_cuda!(runtime::cudaGraphAddMemcpyNodeFromSymbol(
                &raw mut handle,
                self.handle,
                dependencies_raw.as_ptr(),
                dependencies_raw.len() as runtime::size_t,
                params.dst.cast(),
                params.symbol.cast(),
                params.count as _,
                params.offset as _,
                params.kind.into(),
            ))?;
            Ok(GraphNode::from_raw(handle))
        }
    }

    /// Creates a new memset node and adds it to the graph with the given dependencies.
    /// The dependency list may be empty, in which case the node will be placed at the root of the graph, and it may not contain duplicate entries.
    ///
    /// The element size must be 1, 2, or 4 bytes.
    /// When the graph is launched, the node will perform the memset described by `params`.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn add_memset_node(
        &mut self,
        dependencies: &[GraphNode],
        params: &MemsetNodeParams,
    ) -> Result<GraphNode> {
        let mut handle = ptr::null_mut();
        let dependencies_raw: Vec<_> = dependencies.iter().map(|node| node.handle).collect();
        let params = params.into();
        unsafe {
            try_cuda!(runtime::cudaGraphAddMemsetNode(
                &raw mut handle,
                self.handle,
                dependencies_raw.as_ptr(),
                dependencies_raw.len() as runtime::size_t,
                &raw const params,
            ))?;
            Ok(GraphNode::from_raw(handle))
        }
    }

    /// Creates a new node which executes an embedded graph, and adds it to the graph with the given dependencies.
    /// The dependency list may be empty, in which case the node will be placed at the root of the graph, and it may not contain duplicate entries.
    ///
    /// If `child_graph` contains allocation nodes, free nodes, or conditional nodes, this call will return an error.
    ///
    /// The node executes an embedded child graph.
    /// The child graph is cloned in this call.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn add_child_graph_node(
        &mut self,
        dependencies: &[GraphNode],
        child_graph: &Self,
    ) -> Result<GraphNode> {
        let mut handle = ptr::null_mut();
        let dependencies_raw: Vec<_> = dependencies.iter().map(|node| node.handle).collect();
        unsafe {
            try_cuda!(runtime::cudaGraphAddChildGraphNode(
                &raw mut handle,
                self.handle,
                dependencies_raw.as_ptr(),
                dependencies_raw.len() as runtime::size_t,
                child_graph.handle,
            ))?;
            Ok(GraphNode::from_raw(handle))
        }
    }

    /// Creates a new memory free node and adds it to the graph with the given dependencies and address.
    /// The dependency list may be empty, in which case the node will be placed at the root of the graph, and it may not contain duplicate entries.
    ///
    /// [`Graph::add_mem_free_node`] will return [`ErrorCode::InvalidValue`] if the user attempts to free:
    ///
    /// * an allocation twice in the same graph.
    /// * an address that was not returned by an allocation node.
    /// * an invalid address.
    ///
    /// The following restrictions apply to graphs which contain allocation and/or memory free nodes:
    ///
    /// * Nodes and edges of the graph cannot be deleted.
    /// * The graph can only be used in a child node if the ownership is moved to the parent.
    /// * Only one instantiation of the graph may exist at any point in time.
    /// * The graph cannot be cloned.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    pub fn add_mem_free_node(
        &mut self,
        dependencies: &[GraphNode],
        ptr: DevicePtr,
    ) -> Result<GraphNode> {
        let mut handle = ptr::null_mut();
        let dependencies_raw: Vec<_> = dependencies.iter().map(|node| node.handle).collect();
        unsafe {
            try_cuda!(runtime::cudaGraphAddMemFreeNode(
                &raw mut handle,
                self.handle,
                dependencies_raw.as_ptr(),
                dependencies_raw.len() as runtime::size_t,
                ptr.as_ptr() as _,
            ))?;
            Ok(GraphNode::from_raw(handle))
        }
    }

    /// Creates a new allocation node and adds it to the graph with the given dependencies and allocation parameters.
    /// The dependency list may be empty, in which case the node will be placed at the root of the graph, and it may not contain duplicate entries.
    ///
    /// When [`Graph::add_mem_alloc_node`] creates an allocation node, it returns the address of the allocation in [`MemoryAllocationNodeInfo::ptr`].
    /// The allocation's address remains fixed across instantiations and launches.
    ///
    /// If the allocation is freed in the same graph, by creating a free node using [`Graph::add_mem_free_node`], the allocation can be accessed by nodes ordered after the allocation node but before the free node.
    /// These allocations cannot be freed outside the owning graph, and they can only be freed once in the owning graph.
    ///
    /// If the allocation is not freed in the same graph, then it can be accessed not only by nodes in the graph which are ordered after the allocation node, but also by stream operations ordered after the graph's execution but before the allocation is freed.
    ///
    /// Allocations which are not freed in the same graph can be freed by:
    ///
    /// * passing the allocation to [`DeviceMemory::free_async`](crate::memory::DeviceMemory::free_async) or [`DeviceMemory::free`](crate::memory::DeviceMemory::free);
    /// * launching a graph with a free node for that allocation; or
    /// * specifying [`GraphInstantiateFlags::AUTO_FREE_ON_LAUNCH`] during instantiation, which makes each launch behave as though it called [`DeviceMemory::free_async`](crate::memory::DeviceMemory::free_async) for every unfreed allocation.
    ///
    /// It is not possible to free an allocation in both the owning graph and another graph.
    /// If the allocation is freed in the same graph, a free node cannot be added to another graph.
    /// If the allocation is freed in another graph, a free node can no longer be added to the owning graph.
    ///
    /// The following restrictions apply to graphs which contain allocation and/or memory free nodes:
    ///
    /// * Nodes and edges of the graph cannot be deleted.
    /// * The graph can only be used in a child node if the ownership is moved to the parent.
    /// * Only one instantiation of the graph may exist at any point in time.
    /// * The graph cannot be cloned.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    pub fn add_mem_alloc_node(
        &mut self,
        dependencies: &[GraphNode],
        params: &MemAllocNodeParams<'_>,
    ) -> Result<(GraphNode, DevicePtr)> {
        let mut handle = ptr::null_mut();
        let dependencies_raw: Vec<_> = dependencies.iter().map(|node| node.handle).collect();
        let access_descs: Vec<_> = params
            .access_descs
            .iter()
            .copied()
            .map(Into::into)
            .collect();
        let mut params_raw = runtime::cudaMemAllocNodeParams {
            poolProps: params.pool_props.into(),
            accessDescs: access_descs.as_ptr(),
            accessDescCount: access_descs.len() as runtime::size_t,
            bytesize: params.byte_size as _,
            dptr: 0,
        };
        unsafe {
            try_cuda!(runtime::cudaGraphAddMemAllocNode(
                &raw mut handle,
                self.handle,
                dependencies_raw.as_ptr(),
                dependencies_raw.len() as runtime::size_t,
                &raw mut params_raw,
            ))?;
            // TODO: verify dptr?
            Ok((
                GraphNode::from_raw(handle),
                DevicePtr::new(params_raw.dptr as *mut ()),
            ))
        }
    }

    /// Returns this graph's nodes.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn nodes(&self) -> Result<Vec<GraphNode>> {
        unsafe {
            let mut count = 0;
            try_cuda!(runtime::cudaGraphGetNodes(
                self.handle,
                ptr::null_mut(),
                &raw mut count,
            ))?;

            if count == 0 {
                return Ok(Vec::new());
            }

            let mut handles = Vec::with_capacity(count as usize);
            try_cuda!(runtime::cudaGraphGetNodes(
                self.handle,
                handles.as_mut_ptr(),
                &raw mut count,
            ))?;
            handles.set_len(count as usize);

            Ok(handles
                .into_iter()
                .map(|handle| GraphNode { handle })
                .collect())
        }
    }

    /// Returns this graph's root nodes.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn root_nodes(&self) -> Result<Vec<GraphNode>> {
        unsafe {
            let mut count = 0;
            try_cuda!(runtime::cudaGraphGetRootNodes(
                self.handle,
                ptr::null_mut(),
                &raw mut count,
            ))?;

            if count == 0 {
                return Ok(Vec::new());
            }

            let mut handles = Vec::with_capacity(count as usize);
            try_cuda!(runtime::cudaGraphGetRootNodes(
                self.handle,
                handles.as_mut_ptr(),
                &raw mut count,
            ))?;
            handles.set_len(count as usize);

            Ok(handles
                .into_iter()
                .map(|handle| GraphNode { handle })
                .collect())
        }
    }

    /// Returns this graph's dependency edges.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn edges(&self) -> Result<Vec<GraphEdge>> {
        unsafe {
            let mut count = 0;
            try_cuda!(runtime::cudaGraphGetEdges(
                self.handle,
                ptr::null_mut(),
                ptr::null_mut(),
                ptr::null_mut(),
                &raw mut count,
            ))?;

            if count == 0 {
                return Ok(Vec::new());
            }

            let len = count as usize;
            let mut from = Vec::with_capacity(len);
            let mut to = Vec::with_capacity(len);
            let mut edge_data = Vec::with_capacity(len);
            try_cuda!(runtime::cudaGraphGetEdges(
                self.handle,
                from.as_mut_ptr(),
                to.as_mut_ptr(),
                edge_data.as_mut_ptr(),
                &raw mut count,
            ))?;
            let len = count as usize;
            from.set_len(len);
            to.set_len(len);
            edge_data.set_len(len);

            Ok(from
                .into_iter()
                .zip(to)
                .zip(edge_data)
                .map(|((from, to), data)| GraphEdge {
                    from: GraphNode { handle: from },
                    to: GraphNode { handle: to },
                    data: data.into(),
                })
                .collect())
        }
    }

    /// Using the provided graph, write to path a DOT formatted description of the graph.
    /// By default this includes the graph topology, node types, node id, kernel names and memcpy direction. flags can be specified to write more detailed information about each node type such as parameter values, kernel attributes, node and function handles.
    pub fn write_dot(&self, path: &str, flags: GraphDebugDotFlags) -> Result<()> {
        let path = CString::new(path)?;
        unsafe {
            try_cuda!(runtime::cudaGraphDebugDotPrint(
                self.handle,
                path.as_ptr(),
                flags.bits(),
            ))?;
        }
        Ok(())
    }

    pub const unsafe fn as_raw(&self) -> runtime::cudaGraph_t {
        self.handle
    }
}

impl Drop for Graph {
    fn drop(&mut self) {
        if !self.owns_handle {
            return;
        }
        unsafe {
            if let Err(err) = try_cuda!(runtime::cudaGraphDestroy(self.handle)) {
                #[cfg(debug_assertions)]
                eprintln!("failed to destroy cuda graph: {err}");
            }
        }
    }
}

#[derive(Debug)]
pub struct ExecutableGraph {
    handle: runtime::cudaGraphExec_t,
}

impl ExecutableGraph {
    /// Returns the flags that were passed to instantiation for the given executable graph.
    /// [`GraphInstantiateFlags::UPLOAD`] will not be returned by this API as it does not affect the resulting executable graph.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn flags(&self) -> Result<GraphInstantiateFlags> {
        let mut flags = 0;
        unsafe {
            try_cuda!(runtime::cudaGraphExecGetFlags(self.handle, &raw mut flags))?;
        }
        Ok(GraphInstantiateFlags::from_bits_retain(flags))
    }

    /// Executes this executable graph in `stream`.
    /// Only one instance of this executable graph may be executing at a time.
    /// Each launch is ordered behind both any previous work in `stream` and any previous launches of this executable graph.
    /// To execute a graph concurrently, it must be instantiated multiple times into multiple executable graphs.
    ///
    /// If any allocations created by this executable graph remain unfreed from a previous launch and the graph was not instantiated with [`GraphInstantiateFlags::AUTO_FREE_ON_LAUNCH`], the launch will fail with [`ErrorCode::InvalidValue`].
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn launch(&self, stream: &Stream) -> Result<()> {
        unsafe {
            try_cuda!(runtime::cudaGraphLaunch(self.handle, stream.as_raw()))?;
        }
        Ok(())
    }

    /// Uploads this executable graph to the device in `stream` without executing it.
    /// Uploads of the same executable graph will be serialized.
    /// Each upload is ordered behind both any previous work in `stream` and any previous launches of this executable graph.
    /// Uses memory cached by `stream` to back the allocations owned by this executable graph.
    ///
    /// Note:
    ///
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    pub fn upload(&self, stream: &Stream) -> Result<()> {
        unsafe {
            try_cuda!(runtime::cudaGraphUpload(self.handle, stream.as_raw()))?;
        }
        Ok(())
    }

    /// Updates this executable graph with the node parameters in a topologically identical `graph`.
    ///
    /// Limitations:
    ///
    /// * Kernel nodes:
    ///   + The owning context of the function cannot change.
    ///   + A node whose function originally did not use CUDA dynamic parallelism cannot be updated to a function which uses CDP.
    ///   + A node whose function originally did not make device-side update calls cannot be updated to a function which makes device-side
    ///      update calls.
    ///   + A cooperative node cannot be updated to a non-cooperative node, and vice-versa.
    ///   + If the graph was instantiated with [`GraphInstantiateFlags::USE_NODE_PRIORITY`], the priority attribute cannot change.
    ///      Equality
    ///      is checked on the originally requested priority values, before they are clamped to the device's supported range.
    ///   + If this executable graph was not instantiated for device launch, a node whose function originally did not use device-side [`ExecutableGraph::launch`] cannot be updated to a function which uses device-side [`ExecutableGraph::launch`] unless the node resides on the same device as nodes which contained such calls at instantiate-time.
    ///      If no such calls were
    ///      present at instantiation, these updates cannot be performed at all.
    ///   + Neither the source graph nor this executable graph may contain device-updatable kernel nodes.
    /// * Memset and memcpy nodes:
    ///   + The CUDA device(s) to which the operand(s) was allocated/mapped cannot change.
    ///   + The source/destination memory must be allocated from the same contexts as the original source/destination memory.
    ///   + For 2d memsets, only address and assigned value may be updated.
    ///   + For 1d memsets, updating dimensions is also allowed, but may fail if the resulting operation doesn't map onto the work resources
    ///      already allocated for the node.
    /// * Additional memcpy node restrictions:
    ///   + Changing either the source or destination memory type(i.e. [`MemoryType::Device`](crate::types::MemoryType::Device), [`MemoryType::Array`](crate::types::MemoryType::Array), etc.) is not supported.
    /// * Conditional nodes:
    ///   + Changing node parameters is not supported.
    ///   + Changing parameters of nodes within the conditional body graph is subject to the rules above.
    ///   + Conditional handle flags and default values are updated as part of the graph update.
    ///
    /// Note: The API may add further restrictions in future releases.
    /// [`ExecutableGraph::update`] sets the update result to [`GraphExecUpdateResult::ErrorTopologyChanged`] under the following conditions:
    ///
    /// * The count of nodes directly in the executable graph and the source graph differ.
    /// * The source graph has more exit nodes.
    /// * A node in the source graph has a different number of dependencies than the paired node from the executable graph.
    /// * A node in the source graph has a dependency that does not match the corresponding dependency of the paired node from the executable graph.
    ///    The dependencies are paired based on edge order and
    ///    a dependency does not match when the nodes are already paired based on other edges examined in the graph.
    ///
    /// [`ExecutableGraph::update`] sets the update result to:
    ///
    /// * [`GraphExecUpdateResult::Error`] if passed an invalid value.
    /// * [`GraphExecUpdateResult::ErrorTopologyChanged`] if the graph topology changed.
    /// * [`GraphExecUpdateResult::ErrorNodeTypeChanged`] if the type of a node changed.
    /// * [`GraphExecUpdateResult::ErrorFunctionChanged`] if the function of a kernel node changed (CUDA driver before 11.2).
    /// * [`GraphExecUpdateResult::ErrorUnsupportedFunctionChange`] if the function of a kernel changed in an unsupported way.
    /// * [`GraphExecUpdateResult::ErrorParametersChanged`] if any parameters to a node changed in a way that is not supported.
    /// * [`GraphExecUpdateResult::ErrorAttributesChanged`] if any attributes of a node changed in a way that is not supported.
    /// * [`GraphExecUpdateResult::ErrorNotSupported`] if something about a node is unsupported, like the node's type or configuration.
    ///
    /// If the update fails for a reason not listed above, the result will be [`GraphExecUpdateResult::Error`].
    /// If the update succeeds, the result will be [`GraphExecUpdateResult::Success`].
    ///
    /// [`ExecutableGraph::update`] succeeds when the update was performed successfully.
    /// It returns [`ErrorCode::GraphExecUpdateFailure`] if the graph update was not performed because it included changes which violated constraints specific to instantiated graph update.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn update(&mut self, graph: &Graph) -> Result<ExecutableGraphUpdate> {
        let mut result_info = runtime::cudaGraphExecUpdateResultInfo::default();
        unsafe {
            try_cuda!(runtime::cudaGraphExecUpdate(
                self.handle,
                graph.handle,
                &raw mut result_info,
            ))?;
        }
        Ok(result_info.into())
    }

    /// Sets the parameters of a kernel node in this executable graph.
    /// The node is identified by the corresponding `node` in the non-executable graph from which this executable graph was instantiated.
    ///
    /// `node` must not have been removed from the original graph.
    /// All node parameters may change, but the following restrictions apply to function updates:
    ///
    /// * The owning device of the function cannot change.
    /// * A node whose function originally did not use CUDA dynamic parallelism cannot be updated to a function which uses CDP
    /// * A node whose function originally did not make device-side update calls cannot be updated to a function which makes device-side
    ///    update calls.
    /// * If this executable graph was not instantiated for device launch, a node whose function originally did not use device-side [`ExecutableGraph::launch`] cannot be updated to a function which uses device-side [`ExecutableGraph::launch`] unless the node resides on the same device as nodes which contained such calls at instantiate-time.
    ///    If no such calls were
    ///    present at instantiation, these updates cannot be performed at all.
    ///
    /// The modifications only affect future launches of this executable graph.
    /// Already enqueued or running launches of this executable graph are not affected by this call.
    /// The original `node` is also not modified by this call.
    ///
    /// If `node` is a device-updatable kernel node, the next upload or launch of this executable graph will overwrite any previous device-side updates.
    /// Additionally, applying host updates to a device-updatable kernel node while it is being updated from the device will result in undefined behavior.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    /// * The API can also be used with a runtime kernel handle queried through [`sys::cudaLibraryGetKernel`](singe_cuda_sys::runtime::cudaLibraryGetKernel) or [`sys::cudaGetKernel`](singe_cuda_sys::runtime::cudaGetKernel) and then passed to the API as a raw pointer.
    ///    The symbol passed to [`sys::cudaGetKernel`](singe_cuda_sys::runtime::cudaGetKernel) should be registered with the same CUDA Runtime instance.
    /// * Passing a symbol that belongs that belongs to a different runtime instance will result in undefined behavior.
    ///    The only type
    ///    that can be reliably passed to a different runtime instance is the runtime kernel handle type itself.
    pub fn set_kernel_node_params(
        &mut self,
        node: GraphNode,
        params: &KernelNodeParams<'_>,
    ) -> Result<()> {
        let params = params.into();
        unsafe {
            try_cuda!(runtime::cudaGraphExecKernelNodeSetParams(
                self.handle,
                node.handle,
                &raw const params,
            ))?;
        }
        Ok(())
    }

    /// Updates the work represented by `node` in this executable graph as though `node` had contained the given `params` at instantiation.
    /// `node` must remain in the graph which was used to instantiate this executable graph.
    /// Changed edges to and from `node` are ignored.
    ///
    /// src and dst must be allocated from the same contexts as the original source and destination memory.
    /// The instantiation-time memory operands must be 1-dimensional.
    /// Zero-length operations are not supported.
    ///
    /// The modifications only affect future launches of this executable graph.
    /// Already enqueued or running launches of this executable graph are not affected by this call.
    /// The original `node` is also not modified by this call.
    ///
    /// Returns [`ErrorCode::InvalidValue`] if the memory operands' mappings changed or the original memory operands are multidimensional.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn set_memcpy_node_1d_params(
        &mut self,
        node: GraphNode,
        params: &Memcpy1DNodeParams,
    ) -> Result<()> {
        unsafe {
            try_cuda!(runtime::cudaGraphExecMemcpyNodeSetParams1D(
                self.handle,
                node.handle,
                params.dst.cast(),
                params.src.cast(),
                params.count as _,
                params.kind.into(),
            ))?;
        }
        Ok(())
    }

    /// Updates the work represented by `node` in this executable graph as though `node` had contained the given `params` at instantiation.
    /// `node` must remain in the graph which was used to instantiate this executable graph.
    /// Changed edges to and from `node` are ignored.
    ///
    /// The source and destination memory in `params` must be allocated from the same contexts as the original source and destination memory.
    /// Both the instantiation-time memory operands and the memory operands in `params` must be 1-dimensional.
    /// Zero-length operations are not supported.
    ///
    /// The modifications only affect future launches of this executable graph.
    /// Already enqueued or running launches of this executable graph are not affected by this call.
    /// The original `node` is also not modified by this call.
    ///
    /// Returns [`ErrorCode::InvalidValue`] if the memory operands' mappings changed or either the original or new memory operands are multidimensional.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn set_memcpy_node_params(
        &mut self,
        node: GraphNode,
        params: &Memcpy3DNodeParams,
    ) -> Result<()> {
        let params = params.into();
        unsafe {
            try_cuda!(runtime::cudaGraphExecMemcpyNodeSetParams(
                self.handle,
                node.handle,
                &raw const params,
            ))?;
        }
        Ok(())
    }

    pub fn set_memcpy_node_to_symbol_params(
        &mut self,
        node: GraphNode,
        params: &MemcpyToSymbolNodeParams,
    ) -> Result<()> {
        unsafe {
            try_cuda!(runtime::cudaGraphExecMemcpyNodeSetParamsToSymbol(
                self.handle,
                node.handle,
                params.symbol.cast(),
                params.src.cast(),
                params.count as _,
                params.offset as _,
                params.kind.into(),
            ))?;
        }
        Ok(())
    }

    pub fn set_memcpy_node_from_symbol_params(
        &mut self,
        node: GraphNode,
        params: &MemcpyFromSymbolNodeParams,
    ) -> Result<()> {
        unsafe {
            try_cuda!(runtime::cudaGraphExecMemcpyNodeSetParamsFromSymbol(
                self.handle,
                node.handle,
                params.dst.cast(),
                params.symbol.cast(),
                params.count as _,
                params.offset as _,
                params.kind.into(),
            ))?;
        }
        Ok(())
    }

    /// Updates the work represented by `node` in this executable graph as though `node` had contained the given `params` at instantiation.
    /// `node` must remain in the graph which was used to instantiate this executable graph.
    /// Changed edges to and from `node` are ignored.
    ///
    /// Zero sized operations are not supported.
    ///
    /// The new destination pointer in `params` must be to the same kind of allocation as the original destination pointer and have the same context association and device mapping as the original destination pointer.
    ///
    /// Both the value and pointer address may be updated.
    /// Changing other aspects of the memset (width, height, element size or pitch) may cause the update to be rejected.
    /// Specifically, for 2d memsets, all dimension changes are rejected.
    /// For 1d memsets, changes in height are explicitly rejected and other changes are opportunistically allowed if the resulting work maps onto the work resources already allocated for the node.
    ///
    /// The modifications only affect future launches of this executable graph.
    /// Already enqueued or running launches of this executable graph are not affected by this call.
    /// The original `node` is also not modified by this call.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn set_memset_node_params(
        &mut self,
        node: GraphNode,
        params: &MemsetNodeParams,
    ) -> Result<()> {
        let params = params.into();
        unsafe {
            try_cuda!(runtime::cudaGraphExecMemsetNodeSetParams(
                self.handle,
                node.handle,
                &raw const params,
            ))?;
        }
        Ok(())
    }

    /// Updates the work represented by `node` in this executable graph as though `node` had contained the given `params` at instantiation.
    /// `node` must remain in the graph which was used to instantiate this executable graph.
    /// Changed edges to and from `node` are ignored.
    ///
    /// The modifications only affect future launches of this executable graph.
    /// Already enqueued or running launches of this executable graph are not affected by this call.
    /// The original `node` is also not modified by this call.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn set_host_node_params(&mut self, node: GraphNode, params: &HostNodeParams) -> Result<()> {
        let params = params.into();
        unsafe {
            try_cuda!(runtime::cudaGraphExecHostNodeSetParams(
                self.handle,
                node.handle,
                &raw const params,
            ))?;
        }
        Ok(())
    }

    /// Sets the event of an event record node in this executable graph.
    /// The node is identified by the corresponding `node` in the non-executable graph from which this executable graph was instantiated.
    ///
    /// The modifications only affect future launches of this executable graph.
    /// Already enqueued or running launches of this executable graph are not affected by this call.
    /// The original `node` is also not modified by this call.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn set_event_record_node_event(&mut self, node: GraphNode, event: &Event) -> Result<()> {
        unsafe {
            try_cuda!(runtime::cudaGraphExecEventRecordNodeSetEvent(
                self.handle,
                node.handle,
                event.as_raw(),
            ))?;
        }
        Ok(())
    }

    /// Updates the work represented by `node` in this executable graph as though the nodes contained in `node`'s graph had the parameters contained in `child_graph`'s nodes at instantiation.
    /// `node` must remain in the graph which was used to instantiate this executable graph.
    /// Changed edges to and from `node` are ignored.
    ///
    /// The modifications only affect future launches of this executable graph.
    /// Already enqueued or running launches of this executable graph are not affected by this call.
    /// The original `node` is also not modified by this call.
    ///
    /// The topology of `child_graph`, as well as the node insertion order, must match that of the graph contained in `node`.
    /// See [`ExecutableGraph::update`] for a list of restrictions on what can be updated in an instantiated graph.
    /// The update is recursive, so child graph nodes contained within the top level child graph will also be updated.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn set_child_graph_node(&mut self, node: GraphNode, child_graph: &Graph) -> Result<()> {
        unsafe {
            try_cuda!(runtime::cudaGraphExecChildGraphNodeSetParams(
                self.handle,
                node.handle,
                child_graph.handle,
            ))?;
        }
        Ok(())
    }

    /// Sets the event of an event wait node in this executable graph.
    /// The node is identified by the corresponding `node` in the non-executable graph from which this executable graph was instantiated.
    ///
    /// The modifications only affect future launches of this executable graph.
    /// Already enqueued or running launches of this executable graph are not affected by this call.
    /// The original `node` is also not modified by this call.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn set_event_wait_node_event(&mut self, node: GraphNode, event: &Event) -> Result<()> {
        unsafe {
            try_cuda!(runtime::cudaGraphExecEventWaitNodeSetEvent(
                self.handle,
                node.handle,
                event.as_raw(),
            ))?;
        }
        Ok(())
    }

    /// Sets `node` to be either enabled or disabled.
    /// Disabled nodes are functionally equivalent to empty nodes until they are reenabled.
    /// Existing node parameters are not affected by disabling/enabling the node.
    ///
    /// The node is identified by the corresponding `node` in the non-executable graph from which this executable graph was instantiated.
    ///
    /// `node` must not have been removed from the original graph.
    ///
    /// The modifications only affect future launches of this executable graph.
    /// Already enqueued or running launches of this executable graph are not affected by this call.
    /// The original `node` is also not modified by this call.
    ///
    /// Note:
    ///
    /// Currently only kernel, memset and memcpy nodes are supported.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn set_node_enabled(&mut self, node: GraphNode, enabled: bool) -> Result<()> {
        unsafe {
            try_cuda!(runtime::cudaGraphNodeSetEnabled(
                self.handle,
                node.handle,
                u32::from(enabled),
            ))?;
        }
        Ok(())
    }

    /// Returns whether `node` is enabled.
    ///
    /// The node is identified by the corresponding `node` in the non-executable graph from which this executable graph was instantiated.
    ///
    /// `node` must not have been removed from the original graph.
    ///
    /// Note:
    ///
    /// Currently only kernel, memset and memcpy nodes are supported.
    ///
    /// Note:
    ///
    /// * Graph objects are not threadsafe.
    /// * Note that this function may also return error codes from previous, asynchronous launches.
    /// * Note that this function may also return [`ErrorCode::NotInitialized`], [`ErrorCode::CallRequiresNewerDriver`] or [`ErrorCode::NoDevice`] if this call tries to initialize internal CUDA RT state.
    /// * Note that as specified by [`Stream::add_callback`] no CUDA function may be called from callback.
    ///    [`ErrorCode::NotPermitted`] may, but is not guaranteed to, be returned as a diagnostic in such case.
    pub fn is_node_enabled(&self, node: GraphNode) -> Result<bool> {
        let mut enabled = 0;
        unsafe {
            try_cuda!(runtime::cudaGraphNodeGetEnabled(
                self.handle,
                node.handle,
                &raw mut enabled,
            ))?;
        }
        Ok(enabled != 0)
    }

    pub const unsafe fn as_raw(&self) -> runtime::cudaGraphExec_t {
        self.handle
    }
}

impl Drop for ExecutableGraph {
    fn drop(&mut self) {
        unsafe {
            if let Err(err) = try_cuda!(runtime::cudaGraphExecDestroy(self.handle)) {
                #[cfg(debug_assertions)]
                eprintln!("failed to destroy cuda graph exec: {err}");
            }
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExecutableGraphUpdate {
    pub result: GraphExecUpdateResult,
    pub error_node: Option<GraphNode>,
    pub error_from_node: Option<GraphNode>,
}

impl From<runtime::cudaGraphExecUpdateResultInfo> for ExecutableGraphUpdate {
    fn from(value: runtime::cudaGraphExecUpdateResultInfo) -> Self {
        Self {
            result: value.result.into(),
            error_node: if value.errorNode.is_null() {
                None
            } else {
                Some(GraphNode {
                    handle: value.errorNode,
                })
            },
            error_from_node: if value.errorFromNode.is_null() {
                None
            } else {
                Some(GraphNode {
                    handle: value.errorFromNode,
                })
            },
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct MemsetNodeParams {
    pub dst: DevicePtr,
    pub pitch: usize,
    pub value: u32,
    pub element_size: u32,
    pub width: usize,
    pub height: usize,
}

impl MemsetNodeParams {
    pub const fn new(dst: DevicePtr, element_size: u32, width: usize) -> Self {
        Self {
            dst,
            pitch: 0,
            value: 0,
            element_size,
            width,
            height: 1,
        }
    }
}

impl From<&MemsetNodeParams> for driver::CUDA_MEMSET_NODE_PARAMS {
    fn from(value: &MemsetNodeParams) -> Self {
        Self {
            dst: value.dst.as_ptr() as _,
            pitch: value.pitch as _,
            value: value.value,
            elementSize: value.element_size,
            width: value.width as _,
            height: value.height as _,
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct HostNodeParams {
    pub func: HostFunction,
    pub user_data: *mut (),
}

impl HostNodeParams {
    pub const fn new(func: HostFunction, user_data: *mut ()) -> Self {
        Self { func, user_data }
    }
}

impl From<&HostNodeParams> for driver::CUDA_HOST_NODE_PARAMS {
    fn from(value: &HostNodeParams) -> Self {
        Self {
            fn_: value.func.as_raw(),
            userData: value.user_data.cast(),
        }
    }
}

#[derive(Debug, Clone)]
pub struct KernelNodeParams<'a> {
    pub func: DeviceFunction,
    pub grid_dim: Dim3,
    pub block_dim: Dim3,
    pub shared_mem_bytes: usize,
    pub kernel_params: *mut *mut (),
    pub extra: *mut *mut (),
    _phantom: PhantomData<&'a ()>,
}

impl KernelNodeParams<'_> {
    pub const fn new(func: DeviceFunction, grid_dim: Dim3, block_dim: Dim3) -> Self {
        Self {
            func,
            grid_dim,
            block_dim,
            shared_mem_bytes: 0,
            kernel_params: ptr::null_mut(),
            extra: ptr::null_mut(),
            _phantom: PhantomData,
        }
    }

    pub const fn with_shared_mem_bytes(mut self, shared_mem_bytes: usize) -> Self {
        self.shared_mem_bytes = shared_mem_bytes;
        self
    }

    pub const fn with_kernel_params(mut self, kernel_params: *mut *mut ()) -> Self {
        self.kernel_params = kernel_params;
        self
    }

    pub const fn with_extra(mut self, extra: *mut *mut ()) -> Self {
        self.extra = extra;
        self
    }
}

impl From<&KernelNodeParams<'_>> for runtime::cudaKernelNodeParams {
    fn from(value: &KernelNodeParams<'_>) -> Self {
        Self {
            func: value.func.as_raw().cast(),
            gridDim: value.grid_dim.into(),
            blockDim: value.block_dim.into(),
            sharedMemBytes: value.shared_mem_bytes as _,
            kernelParams: value.kernel_params.cast(),
            extra: value.extra.cast(),
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct Memcpy1DNodeParams {
    pub dst: *mut (),
    pub src: *const (),
    pub count: usize,
    pub kind: MemoryCopyKind,
}

impl Memcpy1DNodeParams {
    pub const fn new(dst: *mut (), src: *const (), count: usize, kind: MemoryCopyKind) -> Self {
        Self {
            dst,
            src,
            count,
            kind,
        }
    }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct ArrayHandle(runtime::cudaArray_t);

impl ArrayHandle {
    pub const unsafe fn from_raw(handle: runtime::cudaArray_t) -> Self {
        Self(handle)
    }

    pub const unsafe fn as_raw(self) -> runtime::cudaArray_t {
        self.0
    }
}
