Theme

alex/volcanic

Automated vulkan bindings

BRANCH main
main
SNAPSHOT Improved loading api and made ready for publish
DOWNLOAD
Download file

root / codegen / src / emit / loader.rs

use super::*;

pub(super) fn emit_loader_module(out: &mut String, ctx: &Context, vk: &Registry) {
    let selected_commands = selected_command_names(ctx);
    let commands: IndexMap<_, _> = vk
        .commands
        .iter()
        .map(|command| (command.name.as_str(), command))
        .collect();
    let handle_parents = handle_parent_map(vk);
    let dispatchable_handles = dispatchable_handle_names(vk);
    let mut global = Vec::new();
    let mut instance = Vec::new();
    let mut device = Vec::new();

    for command in &vk.commands {
        if !selected_commands.contains(command.name.as_str())
            || !select::api_matches(command.api.as_deref())
        {
            continue;
        }
        match command_scope(command, &commands, &handle_parents, &dispatchable_handles) {
            CommandScope::Global => global.push(command),
            CommandScope::Instance => instance.push(command),
            CommandScope::Device => device.push(command),
        }
    }

    out.push_str("#![allow(clippy::too_many_arguments)]\n\n");
    out.push_str("use super::*;\n");
    out.push_str("use core::ffi::CStr;\n\n");
    out.push_str("#[cfg(feature = \"core_1_0\")]\nmod library;\n");
    out.push_str("#[cfg(feature = \"core_1_0\")]\npub use library::{Loader, InstanceCommands, DeviceCommands};\n\n");
    emit_load_error(out);
    emit_loader_helpers(out);
    emit_loader_struct(
        out,
        ctx,
        &commands,
        "CmdsGlobal",
        CommandScope::Global,
        &global,
    );
    emit_loader_struct(
        out,
        ctx,
        &commands,
        "CmdsInstance",
        CommandScope::Instance,
        &instance,
    );
    emit_loader_struct(
        out,
        ctx,
        &commands,
        "CmdsDevice",
        CommandScope::Device,
        &device,
    );
}

fn emit_load_error(out: &mut String) {
    out.push_str(
        r#"#[derive(Debug)]
pub enum LoadError {
    Library {
        path: std::path::PathBuf,
        source: libloading::Error,
    },
    Symbol {
        name: &'static CStr,
        source: libloading::Error,
    },
    MissingBootstrap {
        name: &'static CStr,
    },
}

impl core::fmt::Display for LoadError {
    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::Library { path, source } => write!(formatter, "failed to open Vulkan loader `{}`: {source}", path.display()),
            Self::Symbol { name, source } => write!(
                formatter,
                "failed to load Vulkan loader symbol `{}`: {source}",
                name.to_str().unwrap_or("<invalid symbol>")
            ),
            Self::MissingBootstrap { name } => write!(
                formatter,
                "Vulkan loader did not return required bootstrap symbol `{}`",
                name.to_str().unwrap_or("<invalid symbol>")
            ),
        }
    }
}

impl std::error::Error for LoadError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Library { source, .. } | Self::Symbol { source, .. } => Some(source),
            Self::MissingBootstrap { .. } => None,
        }
    }
}

"#,
    );
}

fn emit_loader_helpers(out: &mut String) {
    out.push_str(
        r#"#[cfg(feature = "core_1_0")]
unsafe fn load_get_instance_proc_addr(
    library: &libloading::Library,
) -> Result<PFN_vkGetInstanceProcAddr, LoadError> {
    unsafe { library.get::<PFN_vkGetInstanceProcAddr>(c"vkGetInstanceProcAddr") }
        .map(|symbol| *symbol)
        .map_err(|source| LoadError::Symbol {
            name: c"vkGetInstanceProcAddr",
            source,
        })
}

#[cfg(feature = "core_1_0")]
unsafe fn load_global_command<T: Copy>(
    get_instance_proc_addr: PFN_vkGetInstanceProcAddr,
    name: &'static CStr,
) -> Option<T> {
    unsafe { cast_vk_function(get_instance_proc_addr(core::ptr::null_mut(), name.as_ptr())) }
}

#[cfg(feature = "core_1_0")]
unsafe fn load_instance_command<T: Copy>(
    get_instance_proc_addr: PFN_vkGetInstanceProcAddr,
    instance: VkInstance,
    name: &'static CStr,
) -> Option<T> {
    unsafe { cast_vk_function(get_instance_proc_addr(instance, name.as_ptr())) }
}

#[cfg(feature = "core_1_0")]
unsafe fn load_device_command<T: Copy>(
    get_device_proc_addr: PFN_vkGetDeviceProcAddr,
    device: VkDevice,
    name: &'static CStr,
) -> Option<T> {
    unsafe { cast_vk_function(get_device_proc_addr(device, name.as_ptr())) }
}

#[cfg(feature = "core_1_0")]
unsafe fn cast_vk_function<T: Copy>(function: PFN_vkVoidFunction) -> Option<T> {
    function.map(|function| {
        assert_eq!(core::mem::size_of::<T>(), core::mem::size_of_val(&function));
        unsafe { core::mem::transmute_copy(&function) }
    })
}

"#,
    );
}

pub(super) fn selected_command_names(ctx: &Context) -> IndexSet<&str> {
    ctx.ownership
        .types
        .keys()
        .filter_map(|name| name.strip_prefix("PFN_"))
        .collect()
}

pub(super) fn emit_loader_struct(
    out: &mut String,
    ctx: &Context,
    commands: &IndexMap<&str, &Command>,
    struct_name: &str,
    scope: CommandScope,
    struct_commands: &[&Command],
) {
    out.push_str("    #[derive(Clone, Copy, Default)]\n");
    out.push_str(&format!("    pub struct {struct_name} {{\n"));
    for command in struct_commands {
        let pfn = format!("PFN_{}", command.name);
        emit_cfg(out, ctx.type_condition(&pfn), "        ");
        out.push_str(&format!("        pub {}: Option<{pfn}>,\n", command.name));
    }
    out.push_str("    }\n\n");

    out.push_str(&format!("    impl {struct_name} {{\n"));
    emit_loader_load_method(out, ctx, scope, struct_commands);
    for command in struct_commands {
        emit_loader_method(out, ctx, commands, command);
    }
    out.push_str("    }\n\n");
}

pub(super) fn emit_loader_load_method(
    out: &mut String,
    ctx: &Context,
    scope: CommandScope,
    struct_commands: &[&Command],
) {
    out.push_str("        #[cfg(feature = \"core_1_0\")]\n");
    match scope {
        CommandScope::Global => {
            out.push_str(
                "        pub unsafe fn load(library: &libloading::Library) -> Result<Self, LoadError> {\n",
            );
            out.push_str(
                "            let get_instance_proc_addr = unsafe { load_get_instance_proc_addr(library)? };\n",
            );
            out.push_str("            Ok(Self {\n");
            for command in struct_commands {
                emit_loader_load_field(
                    out,
                    ctx,
                    command,
                    "load_global_command(get_instance_proc_addr, ",
                    ")",
                    Some("get_instance_proc_addr"),
                );
            }
        }
        CommandScope::Instance => {
            out.push_str("        pub unsafe fn load(\n");
            out.push_str("            library: &libloading::Library,\n");
            out.push_str("            instance: VkInstance,\n");
            out.push_str("        ) -> Result<Self, LoadError> {\n");
            out.push_str(
                "            let get_instance_proc_addr = unsafe { load_get_instance_proc_addr(library)? };\n",
            );
            out.push_str("            Ok(Self {\n");
            for command in struct_commands {
                emit_loader_load_field(
                    out,
                    ctx,
                    command,
                    "load_instance_command(get_instance_proc_addr, instance, ",
                    ")",
                    None,
                );
            }
        }
        CommandScope::Device => {
            out.push_str("        pub unsafe fn load(\n");
            out.push_str("            library: &libloading::Library,\n");
            out.push_str("            instance: VkInstance,\n");
            out.push_str("            device: VkDevice,\n");
            out.push_str("        ) -> Result<Self, LoadError> {\n");
            out.push_str(
                "            let get_instance_proc_addr = unsafe { load_get_instance_proc_addr(library)? };\n",
            );
            out.push_str(
                "            let get_device_proc_addr: PFN_vkGetDeviceProcAddr = unsafe {\n",
            );
            out.push_str(
                "                load_instance_command(get_instance_proc_addr, instance, c\"vkGetDeviceProcAddr\")\n",
            );
            out.push_str("            }\n");
            out.push_str("            .ok_or(LoadError::MissingBootstrap {\n");
            out.push_str("                name: c\"vkGetDeviceProcAddr\",\n");
            out.push_str("            })?;\n");
            out.push_str(
                "            Ok(unsafe { Self::from_proc_addr(get_device_proc_addr, device) })\n",
            );
            out.push_str("        }\n");
            out.push_str("        #[cfg(feature = \"core_1_0\")]\n");
            out.push_str("        pub unsafe fn from_proc_addr(get_device_proc_addr: PFN_vkGetDeviceProcAddr, device: VkDevice) -> Self {\n");
            out.push_str("            Self {\n");
            for command in struct_commands {
                emit_loader_load_field(
                    out,
                    ctx,
                    command,
                    "load_device_command(get_device_proc_addr, device, ",
                    ")",
                    None,
                );
            }
        }
    }
    if scope == CommandScope::Device {
        out.push_str("            }\n");
    } else {
        out.push_str("            })\n");
    }
    out.push_str("        }\n");
}

pub(super) fn emit_loader_load_field(
    out: &mut String,
    ctx: &Context,
    command: &Command,
    load_prefix: &str,
    load_suffix: &str,
    direct_value: Option<&str>,
) {
    let pfn = format!("PFN_{}", command.name);
    emit_cfg(out, ctx.type_condition(&pfn), "            ");
    if let Some(value) = direct_value.filter(|_| command.name == "vkGetInstanceProcAddr") {
        out.push_str(&format!("            {}: Some({value}),\n", command.name));
    } else {
        out.push_str(&format!(
            "            {}: unsafe {{ {load_prefix}c\"{}\"{load_suffix} }},\n",
            command.name, command.name
        ));
    }
}

pub(super) fn emit_loader_method(
    out: &mut String,
    ctx: &Context,
    commands: &IndexMap<&str, &Command>,
    command: &Command,
) {
    let pfn = format!("PFN_{}", command.name);
    let signature = resolve_command_signature(command, commands);
    let ret = rust_decl_type(signature.proto.as_ref().unwrap(), true);
    let ret = if ret == "()" {
        String::new()
    } else {
        format!(" -> {ret}")
    };
    let params = params(&signature.params);
    let args = call_args(&signature.params);

    emit_cfg(out, ctx.type_condition(&pfn), "        ");
    if params.is_empty() {
        out.push_str(&format!(
            "        pub unsafe fn {}(&self){ret} {{\n",
            command.name
        ));
    } else {
        out.push_str(&format!(
            "        pub unsafe fn {}(&self, {params}){ret} {{\n",
            command.name
        ));
    }
    out.push_str(&format!(
        "            unsafe {{ (self.{0}.expect(\"Vulkan command {0} is not loaded\"))({args}) }}\n",
        command.name
    ));
    out.push_str("        }\n");
}

pub(super) fn command_scope(
    command: &Command,
    commands: &IndexMap<&str, &Command>,
    handle_parents: &IndexMap<String, String>,
    dispatchable_handles: &IndexSet<String>,
) -> CommandScope {
    match command.name.as_str() {
        "vkGetInstanceProcAddr" => return CommandScope::Global,
        "vkGetDeviceProcAddr" => return CommandScope::Instance,
        _ => {}
    }

    let signature = resolve_command_signature(command, commands);
    let Some(first_param) = signature
        .params
        .iter()
        .find(|param| select::api_matches(param.api.as_deref()))
    else {
        return CommandScope::Global;
    };
    let handle = first_param.base.as_str();
    if !dispatchable_handles.contains(handle) {
        return CommandScope::Global;
    }
    command_scope_for_handle(handle, handle_parents)
}

pub(super) fn command_scope_for_handle(
    handle: &str,
    handle_parents: &IndexMap<String, String>,
) -> CommandScope {
    match handle {
        "VkInstance" | "VkPhysicalDevice" => return CommandScope::Instance,
        "VkDevice" => return CommandScope::Device,
        _ => {}
    }

    let mut current = handle;
    let mut seen = IndexSet::new();
    while let Some(parent) = handle_parents.get(current) {
        if !seen.insert(current.to_string()) {
            break;
        }
        match parent.as_str() {
            "VkDevice" => return CommandScope::Device,
            "VkInstance" | "VkPhysicalDevice" => return CommandScope::Instance,
            _ => current = parent,
        }
    }
    CommandScope::Global
}

pub(super) fn handle_parent_map(vk: &Registry) -> IndexMap<String, String> {
    vk.types
        .iter()
        .filter(|ty| ty.category == "handle")
        .filter(|ty| select::api_matches(ty.api.as_deref()))
        .filter_map(|ty| {
            ty.parent
                .as_ref()
                .map(|parent| (ty.name.clone(), parent.clone()))
        })
        .collect()
}

pub(super) fn dispatchable_handle_names(vk: &Registry) -> IndexSet<String> {
    let handle_aliases: IndexMap<_, _> = vk
        .types
        .iter()
        .filter(|ty| ty.category == "handle")
        .filter_map(|ty| {
            ty.alias
                .as_ref()
                .map(|alias| (ty.name.clone(), alias.clone()))
        })
        .collect();
    let mut handles: IndexSet<_> = vk
        .types
        .iter()
        .filter(|ty| ty.category == "handle")
        .filter(|ty| select::api_matches(ty.api.as_deref()))
        .filter(|ty| {
            ty.handle_macro
                .as_deref()
                .is_some_and(|name| name != "VK_DEFINE_NON_DISPATCHABLE_HANDLE")
        })
        .map(|ty| ty.name.clone())
        .collect();

    loop {
        let mut changed = false;
        for (name, alias) in &handle_aliases {
            if !handles.contains(name) && handles.contains(alias) {
                handles.insert(name.clone());
                changed = true;
            }
        }
        if !changed {
            break;
        }
    }

    handles
}