skia-safe 0.99.0

Safe Skia Bindings for Rust
Documentation
use std::cell::RefCell;
use std::ffi::{self, CString};
use std::fmt;
use std::mem;
use std::os::raw::{self, c_char};
use std::ptr;

use skia_bindings as sb;

use super::{Device, GetProc, GetProcOf, Instance, PhysicalDevice, Queue, Version};
use crate::gpu;

pub use super::vulkan_backend_context_builder::BackendContextBuilder as Builder;

// The `GrBackendContext` memory layout generated by bindgen does not match in size, so we do need
// to use a pointer here for now.
pub struct BackendContext<'a> {
    pub(crate) native: ptr::NonNull<ffi::c_void>,
    get_proc: &'a dyn GetProc,
}

impl Drop for BackendContext<'_> {
    fn drop(&mut self) {
        unsafe { sb::C_VulkanBackendContext_delete(self.native.as_ptr()) }
    }
}

impl fmt::Debug for BackendContext<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("BackendContext")
            .field("native", &self.native)
            .finish()
    }
}

// TODO: add some accessor functions to the public fields.
// TODO: may support Clone (note the original structure holds a smartpointer!)
// TODO: think about making this safe in respect to the lifetime of the handles
//       it refers to.
impl BackendContext<'_> {
    pub fn new_builder<'a>(
        instance: Instance,
        physical_device: PhysicalDevice,
        device: Device,
        (queue, queue_index): (Queue, usize),
        get_proc: &'a impl GetProc,
        max_api_version: Option<Version>,
    ) -> Builder<'a> {
        Builder::new(
            instance,
            physical_device,
            device,
            (queue, queue_index),
            get_proc,
            max_api_version,
        )
    }

    #[deprecated(
        since = "0.98.0",
        note = "use BackendContext::new_builder(...).build()"
    )]
    #[allow(deprecated)]
    /// # Safety
    /// `instance`, `physical_device`, `device`, and `queue` must outlive the `BackendContext`
    /// returned.
    pub unsafe fn new(
        instance: Instance,
        physical_device: PhysicalDevice,
        device: Device,
        (queue, queue_index): (Queue, usize),
        get_proc: &dyn GetProc,
    ) -> BackendContext {
        unsafe {
            Self::new_internal(
                instance,
                physical_device,
                device,
                (queue, queue_index),
                get_proc,
                None,
                gpu::Protected::No,
                &[],
                &[],
            )
        }
    }

    #[deprecated(
        since = "0.98.0",
        note = "use BackendContext::new_builder(...).with_extensions(...).build()"
    )]
    /// # Safety
    /// `instance`, `physical_device`, `device`, and `queue` must outlive the `BackendContext`
    /// returned.
    pub unsafe fn new_with_extensions<'a>(
        instance: Instance,
        physical_device: PhysicalDevice,
        device: Device,
        (queue, queue_index): (Queue, usize),
        get_proc: &'a dyn GetProc,
        instance_extensions: &[&str],
        device_extensions: &[&str],
    ) -> BackendContext<'a> {
        unsafe {
            Self::new_internal(
                instance,
                physical_device,
                device,
                (queue, queue_index),
                get_proc,
                None,
                gpu::Protected::No,
                instance_extensions,
                device_extensions,
            )
        }
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) unsafe fn new_internal<'a>(
        instance: Instance,
        physical_device: PhysicalDevice,
        device: Device,
        (queue, queue_index): (Queue, usize),
        get_proc: &'a dyn GetProc,
        max_api_version: Option<Version>,
        protected_context: gpu::Protected,
        instance_extensions: &[&str],
        device_extensions: &[&str],
    ) -> BackendContext<'a> {
        // Pin the extensions string in memory and provide pointers to the NewWithExtension function,
        // but there is no need to retain them, because because the implementations copies these strings, too.
        let instance_extensions: Vec<CString> = instance_extensions
            .iter()
            .map(|str| CString::new(*str).unwrap())
            .collect();
        let instance_extensions: Vec<*const c_char> =
            instance_extensions.iter().map(|cs| cs.as_ptr()).collect();
        let device_extensions: Vec<CString> = device_extensions
            .iter()
            .map(|str| CString::new(*str).unwrap())
            .collect();
        let device_extensions: Vec<*const c_char> =
            device_extensions.iter().map(|cs| cs.as_ptr()).collect();

        let resolver = unsafe { Self::begin_resolving_proc(get_proc) };
        let native = unsafe {
            sb::C_VulkanBackendContext_new(
                instance as _,
                physical_device as _,
                device as _,
                queue as _,
                queue_index.try_into().unwrap(),
                protected_context,
                max_api_version.map(|version| *version).unwrap_or(0),
                Some(global_get_proc),
                instance_extensions.as_ptr(),
                instance_extensions.len(),
                device_extensions.as_ptr(),
                device_extensions.len(),
            )
        };
        drop(resolver);
        BackendContext {
            native: ptr::NonNull::new(native).unwrap(),
            get_proc,
        }
    }

    pub fn set_protected_context(&mut self, protected_context: gpu::Protected) -> &mut Self {
        unsafe {
            sb::C_VulkanBackendContext_setProtectedContext(
                self.native.as_ptr() as _,
                protected_context,
            )
        }
        self
    }

    /// Sets the maximum Vulkan API version Skia should use.
    ///
    /// Passing `None` restores Skia's default `0` sentinel. Skia then queries
    /// `vkEnumerateInstanceVersion()` and uses that loader-reported version as the upper limit when
    /// validating Vulkan entry points.
    pub fn set_max_api_version(&mut self, version: impl Into<Option<Version>>) -> &mut Self {
        unsafe {
            sb::C_VulkanBackendContext_setMaxAPIVersion(
                self.native.as_ptr() as _,
                version.into().map(|version| *version).unwrap_or(0),
            )
        }
        self
    }

    pub(crate) unsafe fn begin_resolving(&self) -> impl Drop {
        unsafe { Self::begin_resolving_proc(self.get_proc) }
    }

    // The idea here is to set up a thread local variable with the GetProc function trait
    // and reroute queries to global_get_proc as long the caller does not invoke the Drop
    // impl trait that is returned.
    // This is an attempt to support Rust Closures / Functions that resolve function pointers instead
    // of relying on a global extern "C" function.
    // TODO: This is a mess, highly unsafe, and needs to be simplified / rewritten
    //       by someone who understands Rust better.
    unsafe fn begin_resolving_proc(get_proc_trait_object: &dyn GetProc) -> impl Drop {
        THREAD_LOCAL_GET_PROC.with(|get_proc| {
            *get_proc.borrow_mut() =
                Some(unsafe { mem::transmute::<&dyn GetProc, TraitObject>(get_proc_trait_object) })
        });

        EndResolving {}
    }
}

struct EndResolving {}

impl Drop for EndResolving {
    fn drop(&mut self) {
        THREAD_LOCAL_GET_PROC.with(|get_proc| *get_proc.borrow_mut() = None)
    }
}

thread_local! {
    static THREAD_LOCAL_GET_PROC: RefCell<Option<TraitObject>> = const { RefCell::new(None) };
}

// https://doc.rust-lang.org/1.19.0/std/raw/struct.TraitObject.html
#[repr(C)]
// Copy & Clone are required for the *get_proc.borrow() below. And std::raw::TraitObject
// can not be used, because it's unstable (last checked 1.36).
#[derive(Copy, Clone)]
struct TraitObject {
    pub data: *mut (),
    pub vtable: *mut (),
}

// The global resolvement function passed to Skia.
unsafe extern "C" fn global_get_proc(
    name: *const raw::c_char,
    instance: Instance,
    device: Device,
) -> *const raw::c_void {
    THREAD_LOCAL_GET_PROC.with(|get_proc| {
        match *get_proc.borrow() {
            Some(get_proc) => {
                let get_proc_trait_object: &dyn GetProc = unsafe { mem::transmute(get_proc) };
                if !device.is_null() {
                    get_proc_trait_object(GetProcOf::Device(device, name))
                } else {
                    // note: instance may be null here!
                    get_proc_trait_object(GetProcOf::Instance(instance, name))
                }
            }
            None => panic!("Vulkan GetProc called outside of a thread local resolvement context."),
        }
    })
}