diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01152c667..87fe8a160 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..7836c796c 100644 --- a/analysis/Cargo.toml +++ b/analysis/Cargo.toml @@ -4,3 +4,5 @@ version = "2.0.0" edition = "2021" [dependencies] +roxmltree = "0.19" +tracing = "0.1" diff --git a/analysis/src/lib.rs b/analysis/src/lib.rs index 88cb85962..81a7707de 100644 --- a/analysis/src/lib.rs +++ b/analysis/src/lib.rs @@ -1,9 +1,38 @@ -use std::path::Path; +mod xml; -pub struct Analysis {} +use std::{fs, path::Path}; +use tracing::{debug, error_span}; + +#[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 { + let xml = error_span!("xml", path = %xml_path.as_ref().display()).in_scope(|| { + // 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()); + debug!("parsing xml"); + xml::Registry::parse(xml_input, "vulkan") + }); + + Library { _xml: xml } } } diff --git a/analysis/src/xml.rs b/analysis/src/xml.rs new file mode 100644 index 000000000..e5dc246ca --- /dev/null +++ b/analysis/src/xml.rs @@ -0,0 +1,806 @@ +use roxmltree::StringStorage; +use std::{borrow::Cow, fmt::Write}; +use tracing::{debug, info_span, trace}; + +/// 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. +pub type XmlStr = Cow<'static, str>; + +pub trait UnwrapBorrowed<'a, B> +where + B: ToOwned + ?Sized, +{ + fn unwrap_borrowed(&self) -> &'a B; +} + +impl<'a, B> UnwrapBorrowed<'a, B> for Cow<'a, B> +where + B: ToOwned + ?Sized, +{ + fn unwrap_borrowed(&self) -> &'a B { + match self { + Cow::Borrowed(b) => b, + Cow::Owned(_) => panic!("Called `unwrap_borrowed` on `Owned` value"), + } + } +} + +/// Converts `roxmltree`'s `StringStorage` to a `XmlStr` +fn make_xml_str(string_storage: StringStorage<'static>) -> XmlStr { + match string_storage { + StringStorage::Borrowed(s) => Cow::Borrowed(s), + StringStorage::Owned(s) => Cow::Owned((*s).into()), + } +} + +/// Retrieves the value of the `node`'s attribute named `name`. +fn attribute(node: Node, name: &str) -> Option { + node.attribute_node(name) + .map(|attr| make_xml_str(attr.value_storage().clone())) +} + +/// Retrieves the ','-separated values of the `node`'s attribute named `name`. +fn attribute_comma_separated(node: Node, name: &str) -> Vec<&'static str> { + attribute(node, name) + .map(|value| value.unwrap_borrowed().split(',').collect()) + .unwrap_or_default() +} + +/// 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().unwrap().clone() { + 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) +} + +/// Returns a "pseudo-XML" representation of the node, for use in tracing spans. +fn node_span_field(node: &Node) -> String { + let mut output = format!("<{:?}", node.tag_name()); + for attr in node.attributes() { + write!(output, " {}='{}'", attr.name(), attr.value()).unwrap(); + } + + output + ">" +} + +/// 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)) + { + let _s = info_span!("type", node = node_span_field(&type_node)).entered(); + trace!("encountered node"); + 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)); + } + _ => debug!("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(_) => debug!("ignored"), + None => { + registry.externals.push(External::from_node(type_node)); + } + } + } + } + } + "enums" => { + let _s = info_span!("enum", node = node_span_field(®istry_child)).entered(); + trace!("encountered node"); + 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)); + } + } + } + _ => debug!("ignored"), + } + } + "commands" => { + for command_node in registry_child + .children() + .filter(|node| node.has_tag_name("command")) + .filter(|node| api_matches(node, api)) + { + let _s = + info_span!("command", node = node_span_field(&command_node)).entered(); + trace!("encountered node"); + 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" => { + let _s = + info_span!("feature", node = node_span_field(®istry_child)).entered(); + trace!("encountered node"); + 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) + }) + { + let _s = info_span!("extension", node = node_span_field(&extension_node)) + .entered(); + trace!("encountered node"); + 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: Vec<&'static str>, + pub altlen: Option, + pub optional: Vec<&'static str>, +} + +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_comma_separated(node, "len"), + altlen: attribute(node, "altlen"), + optional: attribute_comma_separated(node, "optional"), + } + } +} + +#[derive(Debug)] +pub struct Structure { + pub name: XmlStr, + pub structextends: Vec<&'static str>, + pub members: Vec, +} + +impl Structure { + fn from_node(node: Node, api: &str) -> Structure { + Structure { + name: attribute(node, "name").unwrap(), + structextends: attribute_comma_separated(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: Vec<&'static str>, +} + +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_comma_separated(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: u8, + pub extends: XmlStr, +} + +impl RequireEnumVariant { + fn from_node(node: Node) -> RequireEnumVariant { + RequireEnumVariant { + name: attribute(node, "name").unwrap(), + offset: attribute(node, "offset").unwrap().parse().unwrap(), + extends: attribute(node, "extends").unwrap(), + } + } +} + +#[derive(Debug)] +pub struct RequireBitPos { + pub name: XmlStr, + pub bitpos: u8, + pub extends: XmlStr, +} + +impl RequireBitPos { + fn from_node(node: Node) -> RequireBitPos { + RequireBitPos { + name: attribute(node, "name").unwrap(), + bitpos: attribute(node, "bitpos").unwrap().parse().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)] +pub struct Version { + pub major: u32, + pub minor: u32, +} + +impl Version { + fn from_str(s: &str) -> Option { + let major_minor = s.strip_prefix("VK_VERSION_")?; + + let mut iter = major_minor.split('_').flat_map(str::parse); + let (Some(major), Some(minor), None) = (iter.next(), iter.next(), iter.next()) else { + return None; + }; + + Some(Version { major, minor }) + } +} + +#[derive(Debug)] +pub enum Depends { + Version(Version), + Extension(&'static str), +} + +impl Depends { + fn from_str(s: &'static str) -> Depends { + Version::from_str(s).map_or_else(|| Depends::Extension(s), Depends::Version) + } +} + +#[derive(Debug, Default)] +pub struct Require { + pub depends: Vec, + 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") + .map(|value| (value.unwrap_borrowed().split(',').map(Depends::from_str)).collect()) + .unwrap_or_default(), + ..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 version: Version, + pub requires: Vec, +} + +impl Feature { + fn from_node(node: Node, api: &str) -> Feature { + let name = attribute(node, "name").unwrap(); + + Feature { + version: Version::from_str(&name).unwrap(), + name, + 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").map(|value| value.parse().unwrap()), + 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/generator-rewrite/Cargo.toml b/generator-rewrite/Cargo.toml index 08e458ac0..875badb00 100644 --- a/generator-rewrite/Cargo.toml +++ b/generator-rewrite/Cargo.toml @@ -6,3 +6,5 @@ publish = false [dependencies] analysis = { path = "../analysis" } +tracing = "0.1" +tracing-subscriber = "0.3" diff --git a/generator-rewrite/src/main.rs b/generator-rewrite/src/main.rs index 0d35dbe2f..fb5ff861d 100644 --- a/generator-rewrite/src/main.rs +++ b/generator-rewrite/src/main.rs @@ -1,5 +1,7 @@ use analysis::Analysis; fn main() { + tracing_subscriber::fmt::init(); let _analysis = Analysis::new("generator/Vulkan-Headers"); + // dbg!(_analysis); }