diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40d104aa6..c637069c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,9 @@ jobs: - name: Install Vulkan loader run: sudo apt-get install libvulkan-dev - uses: actions/checkout@v4 + - name: Checkout submodule + # Manually update submodules with --checkout because they are configured with update=none and will be skipped otherwise + run: git submodule update --recursive --init --force --checkout - name: Test all targets run: cargo test --workspace --all-targets - name: Test docs diff --git a/analysis/Cargo.toml b/analysis/Cargo.toml index 64d058be9..08f4e910c 100644 --- a/analysis/Cargo.toml +++ b/analysis/Cargo.toml @@ -4,3 +4,5 @@ version = "2.0.0" edition = "2021" [dependencies] +roxmltree = "0.19" +log = "0.4" diff --git a/analysis/src/lib.rs b/analysis/src/lib.rs index 88cb85962..75975a551 100644 --- a/analysis/src/lib.rs +++ b/analysis/src/lib.rs @@ -1,9 +1,36 @@ -use std::path::Path; +mod xml; -pub struct Analysis {} +use log::debug; +use std::{fs, path::Path}; + +#[derive(Debug)] +pub struct Analysis { + pub vk: Library, + pub video: Library, +} impl Analysis { - pub fn new(_vulkan_headers_path: impl AsRef) -> Analysis { - Analysis {} + pub fn new(vulkan_headers_path: impl AsRef) -> Analysis { + let vulkan_headers_path = vulkan_headers_path.as_ref(); + Analysis { + vk: Library::new(vulkan_headers_path.join("registry/vk.xml")), + video: Library::new(vulkan_headers_path.join("registry/video.xml")), + } + } +} + +#[derive(Debug)] +pub struct Library { + _xml: xml::Registry, +} + +impl Library { + fn new(xml_path: impl AsRef) -> Library { + debug!("parsing {:?}", xml_path.as_ref()); + // We leak the input string here for convenience, to avoid explicit lifetimes. + let xml_input = Box::leak(fs::read_to_string(xml_path).unwrap().into_boxed_str()); + Library { + _xml: xml::Registry::parse(xml_input, "vulkan"), + } } } diff --git a/analysis/src/xml.rs b/analysis/src/xml.rs new file mode 100644 index 000000000..1456c3940 --- /dev/null +++ b/analysis/src/xml.rs @@ -0,0 +1,721 @@ +use log::debug; +use roxmltree::StringStorage; +use std::borrow::Cow; + +/// A node with its `'input` lifetime set to `'static`. +type Node<'a> = roxmltree::Node<'a, 'static>; +/// String type used for XML attribute and text values. +/// +/// A `&'static str` is not used directly because sometimes string allocation is +/// needed, for example when replacing `"` with normal quotes. +type XmlStr = Cow<'static, str>; + +/// Retrieves the value of the `node`'s attribute named `name`. +fn attribute(node: Node, name: &str) -> Option { + node.attribute_node(name) + .map(|attr| match attr.value_storage().clone() { + StringStorage::Borrowed(s) => Cow::Borrowed(s), + StringStorage::Owned(s) => Cow::Owned((*s).into()), + }) +} + +/// Retrieves the text inside the next child element of `node` named `name`. +fn child_text(node: Node, name: &str) -> Option { + let child = node.children().find(|node| node.has_tag_name(name)); + child.map(|node| match node.text_storage().cloned().unwrap() { + StringStorage::Borrowed(s) => Cow::Borrowed(s), + StringStorage::Owned(s) => Cow::Owned((*s).into()), + }) +} + +/// Retrieves the text of all of `node`'s descendants, concatenated. +/// Anything within a `` element will be ignored. +fn descendant_text(node: Node) -> String { + node.descendants() + .filter(Node::is_text) + // Ignore any text within a element. + .filter(|node| !node.ancestors().any(|node| node.has_tag_name("comment"))) + .map(|node| node.text().unwrap()) + .collect::() +} + +/// Returns [`true`] when the `node`'s "api" attribute matches the `expected` API. +fn api_matches(node: &Node, expected: &str) -> bool { + node.attribute("api") + .map(|values| values.split(',').any(|value| value == expected)) + .unwrap_or(true) +} + +/// Raw representation of Vulkan XML files (`vk.xml`, `video.xml`). +#[derive(Debug, Default)] +pub struct Registry { + pub externals: Vec, + pub basetypes: Vec, + pub bitmask_types: Vec, + pub bitmask_aliases: Vec, + pub handles: Vec, + pub handle_aliases: Vec, + pub enum_types: Vec, + pub enum_aliases: Vec, + pub funcpointers: Vec, + pub structs: Vec, + pub struct_aliases: Vec, + pub unions: Vec, + pub constants: Vec, + pub constant_aliases: Vec, + pub enums: Vec, + pub bitmasks: Vec, + pub commands: Vec, + pub command_aliases: Vec, + pub features: Vec, + pub extensions: Vec, +} + +impl Registry { + pub fn parse(input: &'static str, api: &str) -> Registry { + let doc = roxmltree::Document::parse(input).unwrap(); + Registry::from_node(doc.root_element(), api) + } + + fn from_node(registry_node: Node, api: &str) -> Registry { + let mut registry = Registry::default(); + for registry_child in registry_node + .children() + .filter(|node| api_matches(node, api)) + { + match registry_child.tag_name().name() { + "types" => { + for type_node in registry_child + .children() + .filter(|node| node.has_tag_name("type")) + .filter(|node| api_matches(node, api)) + { + if type_node.has_attribute("alias") { + match type_node.attribute("category") { + Some("bitmask") => { + registry.bitmask_aliases.push(Alias::from_node(type_node)); + } + Some("handle") => { + registry.handle_aliases.push(Alias::from_node(type_node)); + } + Some("enum") => { + registry.enum_aliases.push(Alias::from_node(type_node)); + } + Some("struct") => { + registry.struct_aliases.push(Alias::from_node(type_node)); + } + ignored => { + debug!("ignored type alias from category {ignored:?}"); + } + } + } else { + match type_node.attribute("category") { + Some("basetype") => { + registry.basetypes.push(BaseType::from_node(type_node)) + } + Some("bitmask") => registry + .bitmask_types + .push(BitMaskType::from_node(type_node)), + Some("handle") => { + registry.handles.push(Handle::from_node(type_node)) + } + Some("enum") => { + registry.enum_types.push(EnumType::from_node(type_node)) + } + Some("funcpointer") => registry + .funcpointers + .push(FuncPointer::from_node(type_node)), + Some("struct") => { + registry.structs.push(Structure::from_node(type_node, api)) + } + Some("union") => { + registry.unions.push(Structure::from_node(type_node, api)); + } + Some(ignored) => { + debug!("ignored type from category {ignored:?}"); + } + None => { + registry.externals.push(External::from_node(type_node)); + } + } + } + } + } + "enums" => match registry_child.attribute("type") { + Some("enum") => registry.enums.push(Enum::from_node(registry_child, api)), + Some("bitmask") => registry + .bitmasks + .push(BitMask::from_node(registry_child, api)), + None if registry_child.attribute("name") == Some("API Constants") => { + for enum_node in registry_child + .children() + .filter(|node| node.has_tag_name("enum")) + .filter(|node| api_matches(node, api)) + { + if enum_node.has_attribute("alias") { + registry.constant_aliases.push(Alias::from_node(enum_node)); + } else { + registry.constants.push(Constant::from_node(enum_node)); + } + } + } + ignored => { + debug!("ignored enums item with type {ignored:?}"); + } + }, + "commands" => { + for command_node in registry_child + .children() + .filter(|node| node.has_tag_name("command")) + .filter(|node| api_matches(node, api)) + { + if command_node.has_attribute("alias") { + registry + .command_aliases + .push(Alias::from_node(command_node)); + } else { + registry + .commands + .push(Command::from_node(command_node, api)); + } + } + } + "feature" => { + registry + .features + .push(Feature::from_node(registry_child, api)); + } + "extensions" => { + for extension_node in registry_child + .children() + .filter(|node| node.has_tag_name("extension")) + .filter(|node| { + node.attribute("supported") + .map(|values| values.split(',').any(|support| support == api)) + .unwrap_or(true) + }) + { + registry + .extensions + .push(Extension::from_node(extension_node, api)); + } + } + _ => (), + } + } + + registry + } +} + +#[derive(Debug)] +pub struct Alias { + pub name: XmlStr, + pub alias: XmlStr, +} + +impl Alias { + fn from_node(node: Node) -> Alias { + Alias { + name: attribute(node, "name").unwrap(), + alias: attribute(node, "alias").unwrap(), + } + } +} + +#[derive(Debug)] +pub struct External { + pub name: XmlStr, + pub requires: Option, +} + +impl External { + fn from_node(node: Node) -> External { + External { + name: attribute(node, "name").unwrap(), + requires: attribute(node, "requires"), + } + } +} + +#[derive(Debug)] +pub struct BaseType { + pub name: XmlStr, + /// [`None`] indicates this being a platform-specific type. + pub ty: Option, +} + +impl BaseType { + fn from_node(node: Node) -> BaseType { + BaseType { + name: child_text(node, "name").unwrap(), + ty: child_text(node, "type").map(Into::into), + } + } +} + +#[derive(Debug)] +pub struct BitMaskType { + pub requires: Option, + pub bitvalues: Option, + pub ty: XmlStr, + pub name: XmlStr, +} + +impl BitMaskType { + fn from_node(node: Node) -> BitMaskType { + BitMaskType { + requires: attribute(node, "requires"), + bitvalues: attribute(node, "bitvalues"), + ty: child_text(node, "type").unwrap(), + name: child_text(node, "name").unwrap(), + } + } +} + +#[derive(Debug)] +pub struct Handle { + pub parent: Option, + pub objtypeenum: XmlStr, + pub ty: XmlStr, + pub name: XmlStr, +} + +impl Handle { + fn from_node(node: Node) -> Handle { + Handle { + parent: attribute(node, "parent"), + objtypeenum: attribute(node, "objtypeenum").unwrap(), + ty: child_text(node, "type").unwrap(), + name: child_text(node, "name").unwrap(), + } + } +} + +#[derive(Debug)] +pub struct EnumType { + pub name: XmlStr, +} + +impl EnumType { + fn from_node(node: Node) -> EnumType { + EnumType { + name: attribute(node, "name").unwrap(), + } + } +} + +#[derive(Debug)] +pub struct FuncPointer { + pub name: XmlStr, + pub c_declaration: String, + pub requires: Option, +} + +impl FuncPointer { + fn from_node(node: Node) -> FuncPointer { + FuncPointer { + name: child_text(node, "name").unwrap(), + c_declaration: descendant_text(node), + requires: attribute(node, "requires"), + } + } +} + +#[derive(Debug)] +pub struct StructureMember { + pub name: XmlStr, + pub c_declaration: String, + pub values: Option, + pub len: Option, + pub altlen: Option, + pub optional: Option, +} + +impl StructureMember { + fn from_node(node: Node) -> StructureMember { + StructureMember { + name: child_text(node, "name").unwrap(), + c_declaration: descendant_text(node), + values: attribute(node, "values"), + len: attribute(node, "len"), + altlen: attribute(node, "altlen"), + optional: attribute(node, "optional"), + } + } +} + +#[derive(Debug)] +pub struct Structure { + pub name: XmlStr, + pub structextends: Option, + pub members: Vec, +} + +impl Structure { + fn from_node(node: Node, api: &str) -> Structure { + Structure { + name: attribute(node, "name").unwrap(), + structextends: attribute(node, "structextends"), + members: node + .children() + .filter(|node| node.has_tag_name("member")) + .filter(|node| api_matches(node, api)) + .map(StructureMember::from_node) + .collect(), + } + } +} + +#[derive(Debug)] +pub struct Constant { + pub ty: XmlStr, + pub value: XmlStr, + pub name: XmlStr, +} + +impl Constant { + fn from_node(node: Node) -> Constant { + Constant { + ty: attribute(node, "type").unwrap(), + value: attribute(node, "value").unwrap(), + name: attribute(node, "name").unwrap(), + } + } +} + +#[derive(Debug)] +pub struct EnumValue { + pub value: XmlStr, + pub name: XmlStr, +} + +impl EnumValue { + fn from_node(node: Node) -> EnumValue { + EnumValue { + value: attribute(node, "value").unwrap(), + name: attribute(node, "name").unwrap(), + } + } +} + +#[derive(Debug)] +pub struct Enum { + pub name: XmlStr, + pub values: Vec, + pub aliases: Vec, +} + +impl Enum { + fn from_node(node: Node, api: &str) -> Enum { + let mut value = Enum { + name: attribute(node, "name").unwrap(), + values: Vec::new(), + aliases: Vec::new(), + }; + + for variant in node + .children() + .filter(|node| node.has_tag_name("enum")) + .filter(|node| api_matches(node, api)) + { + if variant.has_attribute("alias") { + value.aliases.push(Alias::from_node(variant)); + } else { + value.values.push(EnumValue::from_node(variant)); + } + } + + value + } +} + +#[derive(Debug)] +pub struct BitMaskBit { + pub bitpos: XmlStr, + pub name: XmlStr, +} + +impl BitMaskBit { + fn from_node(node: Node) -> BitMaskBit { + BitMaskBit { + bitpos: attribute(node, "bitpos").unwrap(), + name: attribute(node, "name").unwrap(), + } + } +} + +#[derive(Debug)] +pub struct BitMask { + pub name: XmlStr, + pub bits: Vec, + /// Some bitmask variants represent literal values instead of specific + /// individual bits, e.g. a combination of bits, or no bits at all. A good + /// example for this is `VkCullModeFlagBits::FRONT_AND_BACK`. + pub values: Vec, + pub aliases: Vec, +} + +impl BitMask { + fn from_node(node: Node, api: &str) -> BitMask { + let mut value = BitMask { + name: attribute(node, "name").unwrap(), + bits: Vec::new(), + values: Vec::new(), + aliases: Vec::new(), + }; + + for variant in node + .children() + .filter(|node| node.has_tag_name("enum")) + .filter(|node| api_matches(node, api)) + { + if variant.has_attribute("alias") { + value.aliases.push(Alias::from_node(variant)); + } else if variant.has_attribute("value") { + value.values.push(EnumValue::from_node(variant)); + } else { + value.bits.push(BitMaskBit::from_node(variant)); + } + } + + value + } +} + +#[derive(Debug)] +pub struct CommandParam { + pub name: XmlStr, + pub c_declaration: String, + pub len: Option, + pub altlen: Option, + pub optional: Option, +} + +impl CommandParam { + fn from_node(node: Node) -> CommandParam { + CommandParam { + name: child_text(node, "name").unwrap(), + c_declaration: descendant_text(node), + len: attribute(node, "len"), + altlen: attribute(node, "altlen"), + optional: attribute(node, "optional"), + } + } +} + +#[derive(Debug)] +pub struct Command { + pub return_type: XmlStr, + pub name: XmlStr, + pub params: Vec, +} + +impl Command { + fn from_node(node: Node, api: &str) -> Command { + let proto = node + .children() + .find(|child| child.has_tag_name("proto")) + .filter(|node| api_matches(node, api)) + .unwrap(); + Command { + return_type: child_text(proto, "type").unwrap(), + name: child_text(proto, "name").unwrap(), + params: node + .children() + .filter(|child| child.has_tag_name("param")) + .filter(|node| api_matches(node, api)) + .map(CommandParam::from_node) + .collect(), + } + } +} + +#[derive(Debug)] +pub struct RequireConstant { + pub name: XmlStr, + /// `Some` indicates a new constant being defined here. + pub value: Option, +} + +impl RequireConstant { + fn from_node(node: Node) -> RequireConstant { + RequireConstant { + name: attribute(node, "name").unwrap(), + value: attribute(node, "value"), + } + } +} + +#[derive(Debug)] +pub struct RequireEnumVariant { + pub name: XmlStr, + pub offset: XmlStr, + pub extends: XmlStr, +} + +impl RequireEnumVariant { + fn from_node(node: Node) -> RequireEnumVariant { + RequireEnumVariant { + name: attribute(node, "name").unwrap(), + offset: attribute(node, "offset").unwrap(), + extends: attribute(node, "extends").unwrap(), + } + } +} + +#[derive(Debug)] +pub struct RequireBitPos { + pub name: XmlStr, + pub bitpos: XmlStr, + pub extends: XmlStr, +} + +impl RequireBitPos { + fn from_node(node: Node) -> RequireBitPos { + RequireBitPos { + name: attribute(node, "name").unwrap(), + bitpos: attribute(node, "bitpos").unwrap(), + extends: attribute(node, "extends").unwrap(), + } + } +} + +#[derive(Debug)] +pub struct RequireType { + pub name: XmlStr, +} + +impl RequireType { + fn from_node(node: Node) -> RequireType { + RequireType { + name: attribute(node, "name").unwrap(), + } + } +} + +#[derive(Debug)] +pub struct RequireCommand { + pub name: XmlStr, +} + +impl RequireCommand { + fn from_node(node: Node) -> RequireCommand { + RequireCommand { + name: attribute(node, "name").unwrap(), + } + } +} + +#[derive(Debug, Default)] +pub struct Require { + pub depends: Option, + pub enum_variants: Vec, + pub bitpositions: Vec, + pub constants: Vec, + pub types: Vec, + pub commands: Vec, +} + +impl Require { + fn from_node(node: Node, api: &str) -> Require { + let mut value = Require { + depends: attribute(node, "depends"), + ..Default::default() + }; + + for child in node.children().filter(|node| api_matches(node, api)) { + match child.tag_name().name() { + "enum" => { + if child.has_attribute("offset") { + value + .enum_variants + .push(RequireEnumVariant::from_node(child)); + } else if child.has_attribute("bitpos") { + value.bitpositions.push(RequireBitPos::from_node(child)); + } else { + value.constants.push(RequireConstant::from_node(child)); + } + } + "type" => value.types.push(RequireType::from_node(child)), + "command" => value.commands.push(RequireCommand::from_node(child)), + _ => (), + } + } + + value + } +} + +#[derive(Debug)] +pub struct Feature { + pub name: XmlStr, + pub number: XmlStr, + pub requires: Vec, +} + +impl Feature { + fn from_node(node: Node, api: &str) -> Feature { + Feature { + name: attribute(node, "name").unwrap(), + number: attribute(node, "number").unwrap(), + requires: node + .children() + .filter(|child| child.has_tag_name("require")) + .filter(|node| api_matches(node, api)) + .map(|child| Require::from_node(child, api)) + .collect(), + } + } +} + +#[derive(Debug)] +pub struct Extension { + pub name: XmlStr, + pub number: Option, + pub ty: Option, + pub requires: Vec, +} + +impl Extension { + fn from_node(node: Node, api: &str) -> Extension { + Extension { + name: attribute(node, "name").unwrap(), + number: attribute(node, "number"), + ty: attribute(node, "type"), + requires: node + .children() + .filter(|child| child.has_tag_name("require")) + .filter(|node| api_matches(node, api)) + .map(|child| Require::from_node(child, api)) + .collect(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn vk_xml() { + let xml_input = Box::leak( + std::fs::read_to_string("../generator/Vulkan-Headers/registry/vk.xml") + .unwrap() + .into_boxed_str(), + ); + + Registry::parse(xml_input, "vulkan"); + } + + #[test] + fn video_xml() { + let xml_input = Box::leak( + std::fs::read_to_string("../generator/Vulkan-Headers/registry/video.xml") + .unwrap() + .into_boxed_str(), + ); + + Registry::parse(xml_input, "vulkan"); + } +} diff --git a/ash/src/vk/native.rs b/ash/src/vk/native.rs index 223fb738f..f28431a80 100644 --- a/ash/src/vk/native.rs +++ b/ash/src/vk/native.rs @@ -1,4 +1,4 @@ -/* automatically generated by rust-bindgen 0.69.1 */ +/* automatically generated by rust-bindgen 0.69.2 */ #[repr(C)] #[derive(Copy, Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] diff --git a/generator-rewrite/Cargo.toml b/generator-rewrite/Cargo.toml index 08e458ac0..7178e4d9b 100644 --- a/generator-rewrite/Cargo.toml +++ b/generator-rewrite/Cargo.toml @@ -6,3 +6,4 @@ publish = false [dependencies] analysis = { path = "../analysis" } +env_logger = "0.11" diff --git a/generator-rewrite/src/main.rs b/generator-rewrite/src/main.rs index 0d35dbe2f..05e478997 100644 --- a/generator-rewrite/src/main.rs +++ b/generator-rewrite/src/main.rs @@ -1,5 +1,6 @@ use analysis::Analysis; fn main() { + env_logger::init(); let _analysis = Analysis::new("generator/Vulkan-Headers"); }