use std::collections::{HashMap, HashSet};
use anyhow::Result;
use indexmap::IndexMap;
use serde::Serialize;
use crate::cli::{ApiRequest, Cli};
use crate::fetch;
use crate::ir::{RawCommand, RawSpec};
use crate::parse;
use crate::parse::commands::infer_vulkan_scope;
use crate::parse::types::ident_words;
#[derive(Debug, Serialize)]
pub struct FeatureSet {
pub spec_name: String,
pub display_name: String,
pub apis: Vec<String>,
pub is_merged: bool,
pub is_vulkan: bool,
pub is_gl_family: bool,
pub context_name: String,
pub features: Vec<Feature>,
pub extensions: Vec<Extension>,
pub commands: Vec<Command>,
pub types: Vec<TypeDef>,
pub flat_enums: Vec<FlatEnum>,
pub enum_groups: Vec<EnumGroup>,
pub feature_pfn_ranges: Vec<PfnRange>,
pub ext_pfn_ranges: IndexMap<String, Vec<PfnRange>>,
pub ext_subset_indices: IndexMap<String, Vec<u16>>,
pub alias_pairs: Vec<AliasPair>,
pub required_headers: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct Feature {
pub index: u16,
pub short_name: String,
pub full_name: String,
pub version: SerVersion,
pub packed: u16,
pub api: String,
}
#[derive(Debug, Serialize)]
pub struct SerVersion {
pub major: u32,
pub minor: u32,
}
#[derive(Debug, Serialize)]
pub struct Extension {
pub index: u16,
pub name: String,
pub short_name: String,
pub hash: String,
pub protect: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct Command {
pub index: u16,
pub name: String,
pub short_name: String,
pub pfn_type: String,
pub return_type: String,
pub params_str: String,
pub params: Vec<Param>,
pub scope: String,
pub protect: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct Param {
pub type_raw: String,
pub name: String,
}
#[derive(Debug, Serialize)]
pub struct TypeDef {
pub name: String,
pub raw_c: String,
pub category: String,
pub protect: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct FlatEnum {
pub name: String,
pub value: String,
pub comment: String,
}
#[derive(Debug, Serialize)]
pub struct EnumGroup {
pub name: String,
pub is_bitmask: bool,
pub bitwidth: u32,
pub values: Vec<FlatEnum>,
}
#[derive(Debug, Serialize, Clone, Copy)]
pub struct PfnRange {
pub extension: u16,
pub start: u16,
pub count: u16,
}
#[derive(Debug, Serialize)]
pub struct AliasPair {
pub canonical: u16,
pub secondary: u16,
}
pub fn build_feature_sets(cli: &Cli) -> Result<Vec<FeatureSet>> {
let requests = cli.api_requests()?;
let ext_filter = cli.extension_filter()?;
let promoted = cli.promoted;
let predecessors = cli.predecessors;
let alias = match &cli.generator {
crate::cli::Generator::C(c) => c.alias,
crate::cli::Generator::Rust(r) => r.alias,
};
let mut feature_sets = Vec::new();
if cli.merge {
let mut by_spec: IndexMap<String, Vec<ApiRequest>> = IndexMap::new();
for req in requests {
by_spec
.entry(req.spec_name().to_string())
.or_default()
.push(req);
}
for (spec_name, reqs) in &by_spec {
let sources = fetch::load_spec(spec_name, cli.fetch)?;
let raw = parse::parse(&sources, spec_name)?;
let fs =
resolve_feature_set(&raw, reqs, &ext_filter, true, alias, promoted, predecessors)?;
feature_sets.push(fs);
}
} else {
for req in &requests {
let spec_name = req.spec_name();
let sources = fetch::load_spec(spec_name, cli.fetch)?;
let raw = parse::parse(&sources, spec_name)?;
let fs = resolve_feature_set(
&raw,
std::slice::from_ref(req),
&ext_filter,
false,
alias,
promoted,
predecessors,
)?;
feature_sets.push(fs);
}
}
Ok(feature_sets)
}
fn resolve_feature_set(
raw: &RawSpec,
requests: &[ApiRequest],
ext_filter: &Option<Vec<String>>,
is_merged: bool,
want_aliases: bool,
want_promoted: bool,
want_predecessors: bool,
) -> Result<FeatureSet> {
let spec_name = &raw.spec_name;
let is_vulkan = spec_name == "vk";
let is_gl_family = matches!(spec_name.as_str(), "gl" | "egl" | "glx" | "wgl");
let display_name = match spec_name.as_str() {
"gl" => "GL",
"egl" => "EGL",
"glx" => "GLX",
"wgl" => "WGL",
"vk" => "Vulkan",
_ => spec_name.as_str(),
};
let api_names: Vec<String> = requests.iter().map(|r| r.name.clone()).collect();
let selected_features = select_features(raw, requests);
let mut req_types: HashSet<String> = HashSet::new();
let mut req_enums: HashSet<String> = HashSet::new();
let mut req_commands: IndexMap<String, ()> = IndexMap::new(); let mut removed_commands: HashSet<String> = HashSet::new();
let mut removed_enums: HashSet<String> = HashSet::new();
let mut per_api_core_cmds: HashMap<String, HashSet<String>> = HashMap::new();
for feat in &selected_features {
let req_for_api = requests.iter().find(|r| r.name == feat.api);
let profile = req_for_api.and_then(|r| r.profile.as_deref());
let api_cmds = per_api_core_cmds.entry(feat.api.clone()).or_default();
for require in &feat.raw.requires {
if !api_profile_matches(
require.api.as_deref(),
require.profile.as_deref(),
&feat.api,
profile,
) {
continue;
}
req_types.extend(require.types.iter().cloned());
req_enums.extend(require.enums.iter().cloned());
for cmd in &require.commands {
req_commands.entry(cmd.clone()).or_insert(());
api_cmds.insert(cmd.clone());
}
}
for remove in &feat.raw.removes {
if !profile_matches(remove.profile.as_deref(), profile) {
continue;
}
removed_commands.extend(remove.commands.iter().cloned());
removed_enums.extend(remove.enums.iter().cloned());
for cmd in &remove.commands {
api_cmds.remove(cmd.as_str());
}
}
}
for cmd in &removed_commands {
req_commands.shift_remove(cmd.as_str());
}
let selected_exts = select_extensions(
raw,
requests,
ext_filter,
spec_name,
&per_api_core_cmds,
want_promoted,
want_predecessors,
);
let mut ext_commands: IndexMap<String, usize> = IndexMap::new();
for (ext_idx, ext) in selected_exts.iter().enumerate() {
for require in &ext.raw.requires {
for api in &api_names {
if !api_profile_matches(require.api.as_deref(), None, api, None) {
continue;
}
req_types.extend(require.types.iter().cloned());
req_enums.extend(require.enums.iter().cloned());
for e in &require.enums {
removed_enums.remove(e.as_str());
}
for cmd in &require.commands {
if !req_commands.contains_key(cmd.as_str()) {
ext_commands.entry(cmd.clone()).or_insert(ext_idx);
}
}
}
}
}
for e in &removed_enums {
req_enums.remove(e.as_str());
}
let core_cmd_names: Vec<String> = req_commands.keys().cloned().collect();
let ext_cmd_names: Vec<String> = ext_commands.keys().cloned().collect();
let all_cmd_names: Vec<&str> = core_cmd_names
.iter()
.chain(ext_cmd_names.iter())
.map(String::as_str)
.collect();
let pfn_prefix = api_pfn_prefix(spec_name);
let name_prefix = api_name_prefix(spec_name);
let mut commands: Vec<Command> = Vec::with_capacity(all_cmd_names.len());
for (idx, &cmd_name) in all_cmd_names.iter().enumerate() {
let raw_cmd = match raw.commands.get(cmd_name) {
Some(c) => c,
None => {
eprintln!("warning: command '{}' required but not in spec", cmd_name);
continue;
}
};
let scope = if is_vulkan {
infer_vulkan_scope(raw_cmd).c_name().to_string()
} else {
String::new()
};
let protect = find_command_protect(raw, cmd_name, &selected_exts);
commands.push(build_command(
idx as u16,
raw_cmd,
&scope,
protect,
pfn_prefix,
name_prefix,
));
}
let features: Vec<Feature> = selected_features
.iter()
.enumerate()
.map(|(i, sf)| {
let ver = &sf.raw.version;
let short = version_short_name(&sf.raw.name, &sf.api);
Feature {
index: i as u16,
full_name: sf.raw.name.clone(),
short_name: short,
version: SerVersion {
major: ver.major,
minor: ver.minor,
},
packed: ver.packed(),
api: sf.api.clone(),
}
})
.collect();
let mut sorted_exts: Vec<_> = selected_exts.iter().collect();
sorted_exts.sort_by_key(|e| e.raw.name.as_str());
let extensions: Vec<Extension> = sorted_exts
.iter()
.enumerate()
.map(|(i, e)| build_extension(i as u16, e))
.collect();
let ext_index_map: HashMap<&str, u16> = extensions
.iter()
.map(|e| (e.name.as_str(), e.index))
.collect();
let feature_pfn_ranges = build_feature_pfn_ranges(&selected_features, &features, &commands);
let mut ext_pfn_ranges: IndexMap<String, Vec<PfnRange>> = IndexMap::new();
let mut ext_subset_indices: IndexMap<String, Vec<u16>> = IndexMap::new();
for api in &api_names {
let (ranges, indices) = build_ext_pfn_ranges(
api,
&selected_exts,
&ext_commands,
&ext_index_map,
&commands,
);
ext_pfn_ranges.insert(api.clone(), ranges);
ext_subset_indices.insert(api.clone(), indices);
}
if is_vulkan {
let type_names: HashSet<&str> = raw.types.iter().map(|t| t.name.as_str()).collect();
for &cmd_name in &all_cmd_names {
if let Some(raw_cmd) = raw.commands.get(cmd_name) {
for param in &raw_cmd.params {
if !param.type_name.is_empty() {
req_types.insert(param.type_name.clone());
}
}
}
}
loop {
let mut added = false;
for t in &raw.types {
if t.raw_c.is_empty() {
continue;
}
let auto = matches!(
t.category.as_str(),
"define" | "basetype" | "bitmask" | "funcpointer" | "enum" | "handle"
);
if !auto && !req_types.contains(&t.name) {
continue;
}
for word in crate::parse::types::ident_words(&t.raw_c) {
if type_names.contains(word) && req_types.insert(word.to_string()) {
added = true;
}
}
}
if !added {
break;
}
}
}
let types = build_type_list(raw, &req_types, spec_name, is_vulkan);
let flat_enums = build_flat_enums(raw, &req_enums, is_vulkan);
let enum_groups = build_enum_groups(raw);
let alias_pairs = if want_aliases {
build_alias_pairs(raw, &commands)
} else {
Vec::new()
};
let required_headers = collect_required_headers(raw, &req_types, spec_name);
let context_name = build_context_name(spec_name);
Ok(FeatureSet {
spec_name: spec_name.clone(),
display_name: display_name.to_string(),
apis: api_names,
is_merged,
is_vulkan,
is_gl_family,
context_name,
features,
extensions,
commands,
types,
flat_enums,
enum_groups,
feature_pfn_ranges,
ext_pfn_ranges,
ext_subset_indices,
alias_pairs,
required_headers,
})
}
struct SelectedFeature<'a> {
api: String,
raw: &'a crate::ir::RawFeature,
}
fn select_features<'a>(raw: &'a RawSpec, requests: &[ApiRequest]) -> Vec<SelectedFeature<'a>> {
let mut selected = Vec::new();
for req in requests {
let max_ver = req.version.clone();
for feat in &raw.features {
if feat.api != req.name {
continue;
}
if let Some(ref mv) = max_ver
&& feat.version > *mv
{
continue;
}
selected.push(SelectedFeature {
api: req.name.clone(),
raw: feat,
});
}
}
selected.sort_by(|a, b| {
api_order(&a.api)
.cmp(&api_order(&b.api))
.then_with(|| a.raw.version.cmp(&b.raw.version))
});
selected
}
fn api_order(api: &str) -> u8 {
match api {
"gl" | "glcore" => 0,
"gles1" => 1,
"gles2" => 2,
"egl" => 3,
"glx" => 4,
"wgl" => 5,
"vk" | "vulkan" => 6,
_ => 7,
}
}
struct SelectedExt<'a> {
raw: &'a crate::ir::RawExtension,
}
fn select_extensions<'a>(
raw: &'a RawSpec,
requests: &[ApiRequest],
filter: &Option<Vec<String>>,
spec_name: &str,
per_api_core_cmds: &HashMap<String, HashSet<String>>,
want_promoted: bool,
want_predecessors: bool,
) -> Vec<SelectedExt<'a>> {
let api_set: HashSet<&str> = requests.iter().map(|r| r.name.as_str()).collect();
let wgl_mandatory: HashSet<&str> = if spec_name == "wgl" {
["WGL_ARB_extensions_string", "WGL_EXT_extensions_string"]
.iter()
.copied()
.collect()
} else {
HashSet::new()
};
let mut selected: Vec<SelectedExt<'a>> = raw
.extensions
.iter()
.filter(|e| {
let supported = e.supported.iter().any(|s| api_set.contains(s.as_str()));
if !supported {
return false;
}
if wgl_mandatory.contains(e.name.as_str()) {
return true;
}
match filter {
None => true,
Some(list) => list.contains(&e.name),
}
})
.map(|e| SelectedExt { raw: e })
.collect();
let cmd_to_alias: HashMap<&str, &str> = if want_promoted || want_predecessors {
let mut m = HashMap::new();
for (name, cmd) in &raw.commands {
if let Some(ref alias) = cmd.alias {
m.insert(name.as_str(), alias.as_str());
m.insert(alias.as_str(), name.as_str());
}
}
m
} else {
HashMap::new()
};
let enum_to_alias: HashMap<&str, &str> = if want_predecessors {
let mut m = HashMap::new();
for (name, e) in &raw.flat_enums {
if let Some(ref alias) = e.alias {
m.insert(name.as_str(), alias.as_str());
m.insert(alias.as_str(), name.as_str());
}
}
m
} else {
HashMap::new()
};
if want_promoted {
let already: HashSet<&str> = selected.iter().map(|e| e.raw.name.as_str()).collect();
for ext in &raw.extensions {
if already.contains(ext.name.as_str()) {
continue;
}
let is_promoted = ext
.supported
.iter()
.filter(|s| api_set.contains(s.as_str()))
.any(|api| {
let Some(core_cmds) = per_api_core_cmds.get(api.as_str()) else {
return false;
};
ext.requires
.iter()
.filter(|req| api_profile_matches(req.api.as_deref(), None, api, None))
.any(|req| {
req.commands.iter().any(|c| {
core_cmds.contains(c.as_str())
|| cmd_to_alias
.get(c.as_str())
.is_some_and(|a| core_cmds.contains(*a))
})
})
});
if is_promoted {
selected.push(SelectedExt { raw: ext });
}
}
}
if want_predecessors {
loop {
let selected_ext_cmds: HashSet<&str> = selected
.iter()
.flat_map(|e| {
e.raw
.requires
.iter()
.flat_map(|req| req.commands.iter().map(String::as_str))
})
.collect();
let selected_ext_enums: HashSet<&str> = selected
.iter()
.flat_map(|e| {
e.raw
.requires
.iter()
.flat_map(|req| req.enums.iter().map(String::as_str))
})
.collect();
let already: HashSet<&str> = selected.iter().map(|e| e.raw.name.as_str()).collect();
let mut added_any = false;
for ext in &raw.extensions {
if already.contains(ext.name.as_str()) {
continue;
}
let supported = ext.supported.iter().any(|s| api_set.contains(s.as_str()));
if !supported {
continue;
}
let is_predecessor = ext.requires.iter().any(|req| {
req.commands.iter().any(|c| {
selected_ext_cmds.contains(c.as_str())
|| cmd_to_alias
.get(c.as_str())
.is_some_and(|a| selected_ext_cmds.contains(*a))
}) || req.enums.iter().any(|e| {
selected_ext_enums.contains(e.as_str())
|| enum_to_alias
.get(e.as_str())
.is_some_and(|a| selected_ext_enums.contains(*a))
})
});
if is_predecessor {
selected.push(SelectedExt { raw: ext });
added_any = true;
}
}
if !added_any {
break;
}
}
}
selected
}
fn build_command(
index: u16,
raw: &RawCommand,
scope: &str,
protect: Option<String>,
pfn_prefix: &str,
name_prefix: &str,
) -> Command {
let short_name = raw
.name
.strip_prefix(name_prefix)
.unwrap_or(&raw.name)
.to_string();
let pfn_type = if pfn_prefix == "PFN_" {
format!("PFN_{}", raw.name)
} else {
let stem = raw.name.strip_prefix(name_prefix).unwrap_or(&raw.name);
format!("{}{}PROC", pfn_prefix, stem.to_uppercase())
};
let params: Vec<Param> = raw
.params
.iter()
.map(|p| Param {
type_raw: p.type_raw.clone(),
name: p.name.clone(),
})
.collect();
let params_str = if params.is_empty() {
"void".to_string()
} else {
params
.iter()
.map(|p| {
if p.name.is_empty() {
p.type_raw.clone()
} else if p.type_raw.trim_end().ends_with(']') {
p.type_raw.trim().to_string()
} else {
format!("{} {}", p.type_raw, p.name)
}
})
.collect::<Vec<_>>()
.join(", ")
};
Command {
index,
name: raw.name.clone(),
short_name,
pfn_type,
return_type: raw.return_type.clone(),
params_str,
params,
scope: scope.to_string(),
protect,
}
}
fn build_extension(index: u16, e: &SelectedExt<'_>) -> Extension {
use xxhash_rust::xxh3::xxh3_64;
let hash_val = xxh3_64(e.raw.name.as_bytes());
let hash = format!("0x{:016x}", hash_val);
let short = ext_short_name(&e.raw.name);
Extension {
index,
name: e.raw.name.clone(),
short_name: short,
hash,
protect: e.raw.protect.clone(),
}
}
fn build_context_name(spec: &str) -> String {
let display = match spec {
"gl" => "GL",
"egl" => "EGL",
"glx" => "GLX",
"wgl" => "WGL",
"vk" => "Vulkan",
other => other,
};
format!("Gloam{}Context", display)
}
fn ext_short_name(name: &str) -> String {
for prefix in &["GL_", "EGL_", "GLX_", "WGL_", "VK_"] {
if let Some(s) = name.strip_prefix(prefix) {
return s.to_string();
}
}
name.to_string()
}
fn version_short_name(name: &str, api: &str) -> String {
let prefix = match api {
"gl" | "glcore" => "GL_",
"gles1" | "gles2" => "GL_",
"egl" => "EGL_",
"glx" => "GLX_",
"wgl" => "WGL_",
"vk" | "vulkan" => "VK_",
_ => "",
};
name.strip_prefix(prefix).unwrap_or(name).to_string()
}
fn build_feature_pfn_ranges(
features: &[SelectedFeature<'_>],
feat_entries: &[Feature],
commands: &[Command],
) -> Vec<PfnRange> {
let cmd_index: HashMap<&str, u16> = commands
.iter()
.map(|c| (c.name.as_str(), c.index))
.collect();
let mut ranges: Vec<PfnRange> = Vec::new();
for feat in feat_entries {
let sf = match features.iter().find(|f| f.raw.name == feat.full_name) {
Some(f) => f,
None => continue,
};
let mut cmd_indices: Vec<u16> = Vec::new();
for require in &sf.raw.requires {
for cmd_name in &require.commands {
if let Some(&idx) = cmd_index.get(cmd_name.as_str()) {
cmd_indices.push(idx);
}
}
}
cmd_indices.sort_unstable();
cmd_indices.dedup();
ranges.extend(indices_to_ranges(feat.index, &cmd_indices));
}
ranges
}
fn build_ext_pfn_ranges(
api: &str,
exts: &[SelectedExt<'_>],
_ext_commands: &IndexMap<String, usize>, ext_index_map: &HashMap<&str, u16>, commands: &[Command],
) -> (Vec<PfnRange>, Vec<u16>) {
let cmd_index: HashMap<&str, u16> = commands
.iter()
.map(|c| (c.name.as_str(), c.index))
.collect();
let mut ranges: Vec<PfnRange> = Vec::new();
let mut subset_indices: Vec<u16> = Vec::new();
let relevant_exts: Vec<(usize, &SelectedExt)> = exts
.iter()
.enumerate()
.filter(|(_, e)| e.raw.supported.iter().any(|s| s == api))
.collect();
for (_orig_idx, ext) in &relevant_exts {
let sorted_ext_idx = match ext_index_map.get(ext.raw.name.as_str()) {
Some(&i) => i,
None => continue,
};
subset_indices.push(sorted_ext_idx);
let mut cmd_indices: Vec<u16> = Vec::new();
for require in &ext.raw.requires {
if !api_profile_matches(require.api.as_deref(), None, api, None) {
continue;
}
for cmd_name in &require.commands {
if let Some(&pfn_idx) = cmd_index.get(cmd_name.as_str()) {
cmd_indices.push(pfn_idx);
}
}
}
cmd_indices.sort_unstable();
cmd_indices.dedup();
ranges.extend(indices_to_ranges(sorted_ext_idx, &cmd_indices));
}
subset_indices.sort_unstable();
(ranges, subset_indices)
}
fn indices_to_ranges(ext_idx: u16, sorted: &[u16]) -> Vec<PfnRange> {
if sorted.is_empty() {
return Vec::new();
}
let mut ranges = Vec::new();
let mut start = sorted[0];
let mut count = 1u16;
for &idx in &sorted[1..] {
if idx == start + count {
count += 1;
} else {
ranges.push(PfnRange {
extension: ext_idx,
start,
count,
});
start = idx;
count = 1;
}
}
ranges.push(PfnRange {
extension: ext_idx,
start,
count,
});
ranges
}
fn build_type_list(
raw: &RawSpec,
req_types: &HashSet<String>,
spec_name: &str,
is_vulkan: bool,
) -> Vec<TypeDef> {
let gl_auto_exclude: HashSet<&str> = ["stddef", "khrplatform", "inttypes"]
.iter()
.copied()
.collect();
let include_protections = infer_include_protections(raw);
let ext_type_protect = build_ext_type_protections(raw);
let type_list: Vec<TypeDef> = raw
.types
.iter()
.filter(|t| {
if t.raw_c.is_empty() {
return false;
}
if t.category == "enum" && t.raw_c.is_empty() {
return false;
}
if t.category == "include" {
return include_protections.contains_key(&t.name);
}
if is_vulkan {
let auto = matches!(
t.category.as_str(),
"define" | "basetype" | "bitmask" | "funcpointer" | "enum" | "handle"
);
if auto {
return t
.api
.as_deref()
.is_none_or(|a| a.split(',').any(|s| s.trim() == "vulkan"));
}
return req_types.contains(&t.name);
}
if gl_auto_exclude.contains(t.name.as_str()) {
return false;
}
req_types.contains(&t.name) || t.api.as_deref().is_none_or(|a| a == spec_name)
})
.map(|t| {
let protect = if t.category == "include" {
include_protections
.get(&t.name)
.cloned()
.unwrap_or_default()
} else {
if let Some(p) = ext_type_protect.get(t.name.as_str()) {
p.clone()
} else {
t.protect.iter().cloned().collect()
}
};
TypeDef {
name: t.name.clone(),
raw_c: normalize_raw_c(&t.raw_c),
category: t.category.clone(),
protect,
}
})
.collect::<Vec<TypeDef>>();
topo_sort_typedefs(type_list)
}
fn topo_sort_typedefs(types: Vec<TypeDef>) -> Vec<TypeDef> {
let n = types.len();
if n < 2 {
return types;
}
let mut name_to_idx: HashMap<&str, usize> = HashMap::new();
for (i, t) in types.iter().enumerate() {
name_to_idx.insert(t.name.as_str(), i);
}
let scan_cats: &[&str] = &["struct", "union", "funcpointer"];
let deps: Vec<Vec<usize>> = types
.iter()
.enumerate()
.map(|(i, t)| {
let mut d: Vec<usize> = Vec::new();
if scan_cats.contains(&t.category.as_str()) {
for word in crate::parse::types::ident_words(&t.raw_c) {
if word == t.name.as_str() {
continue;
}
if let Some(&dep_idx) = name_to_idx.get(word)
&& dep_idx != i
{
d.push(dep_idx);
}
}
d.sort_unstable();
d.dedup();
}
d
})
.collect();
let mut in_degree: Vec<usize> = deps.iter().map(|d| d.len()).collect();
let mut rev: Vec<Vec<usize>> = vec![Vec::new(); n];
for (i, dep_list) in deps.iter().enumerate() {
for &dep in dep_list {
rev[dep].push(i);
}
}
let mut queue: std::collections::VecDeque<usize> =
(0..n).filter(|&i| in_degree[i] == 0).collect();
let mut order: Vec<usize> = Vec::with_capacity(n);
while let Some(node) = queue.pop_front() {
order.push(node);
for &dependent in &rev[node] {
in_degree[dependent] -= 1;
if in_degree[dependent] == 0 {
queue.push_back(dependent);
}
}
}
if order.len() < n {
let mut stranded: Vec<usize> = (0..n).filter(|&i| in_degree[i] != 0).collect();
let stranded_names: std::collections::HashSet<usize> = stranded.iter().copied().collect();
let mut s_in: HashMap<usize, usize> = HashMap::new();
let mut s_rev: HashMap<usize, Vec<usize>> = HashMap::new();
for &i in &stranded {
s_in.insert(i, 0);
s_rev.insert(i, Vec::new());
}
for &i in &stranded {
for word in crate::parse::types::ident_words(&types[i].raw_c) {
if let Some(&j) = name_to_idx.get(word)
&& j != i
&& stranded_names.contains(&j)
{
*s_in.get_mut(&i).unwrap() += 1;
s_rev.get_mut(&j).unwrap().push(i);
}
}
}
let mut s_deps: HashMap<usize, std::collections::HashSet<usize>> = HashMap::new();
for &i in &stranded {
let mut deps_i = std::collections::HashSet::new();
for word in crate::parse::types::ident_words(&types[i].raw_c) {
if let Some(&j) = name_to_idx.get(word)
&& j != i
&& stranded_names.contains(&j)
{
deps_i.insert(j);
}
}
s_deps.insert(i, deps_i);
}
let mut s_in2: HashMap<usize, usize> =
stranded.iter().map(|&i| (i, s_deps[&i].len())).collect();
let mut s_rev2: HashMap<usize, Vec<usize>> =
stranded.iter().map(|&i| (i, Vec::new())).collect();
for &i in &stranded {
for &j in &s_deps[&i] {
s_rev2.get_mut(&j).unwrap().push(i);
}
}
let mut s_queue: std::collections::VecDeque<usize> = stranded
.iter()
.filter(|&&i| s_in2[&i] == 0)
.copied()
.collect();
let mut s_order: Vec<usize> = Vec::new();
while let Some(node) = s_queue.pop_front() {
s_order.push(node);
for &dep in &s_rev2[&node] {
let e = s_in2.get_mut(&dep).unwrap();
*e -= 1;
if *e == 0 {
s_queue.push_back(dep);
}
}
}
let processed: std::collections::HashSet<usize> = s_order.iter().copied().collect();
stranded.retain(|i| !processed.contains(i));
s_order.extend(stranded);
order.extend(s_order);
}
let mut out: Vec<Option<TypeDef>> = types.into_iter().map(Some).collect();
order.into_iter().map(|i| out[i].take().unwrap()).collect()
}
fn build_ext_type_protections(raw: &RawSpec) -> HashMap<String, Vec<String>> {
let mut tmp: HashMap<&str, Option<Vec<String>>> = HashMap::new();
for ext in &raw.extensions {
for require in &ext.requires {
for type_name in &require.types {
let entry = tmp
.entry(type_name.as_str())
.or_insert_with(|| Some(Vec::new()));
match entry {
None => {} Some(guards) => {
if ext.protect.is_empty() {
*entry = None;
} else {
for p in &ext.protect {
if !guards.contains(p) {
guards.push(p.clone());
}
}
}
}
}
}
}
}
tmp.into_iter()
.map(|(name, state)| (name.to_string(), state.unwrap_or_default()))
.collect()
}
fn record_protect<'a>(
name: &'a str,
ext_protect: &[String],
all_dep_names: &std::collections::HashSet<&str>,
map: &mut HashMap<&'a str, Option<Vec<String>>>,
) {
if !all_dep_names.contains(name) {
return;
}
let entry = map.entry(name).or_insert_with(|| Some(Vec::new()));
if let Some(guards) = entry {
if ext_protect.is_empty() {
*entry = None;
} else {
for p in ext_protect {
if !guards.contains(p) {
guards.push(p.clone());
}
}
}
}
}
fn infer_include_protections(raw: &RawSpec) -> HashMap<String, Vec<String>> {
let include_names: std::collections::HashSet<&str> = raw
.types
.iter()
.filter(|t| t.category == "include")
.map(|t| t.name.as_str())
.collect();
let mut include_to_deps: HashMap<&str, std::collections::HashSet<&str>> = HashMap::new();
for t in &raw.types {
if t.category == "include" {
continue;
}
if let Some(ref req) = t.requires
&& include_names.contains(req.as_str())
{
include_to_deps
.entry(req.as_str())
.or_default()
.insert(t.name.as_str());
}
}
let mut type_protect: HashMap<&str, Option<Vec<String>>> = HashMap::new();
let all_dep_names: std::collections::HashSet<&str> = include_to_deps
.values()
.flat_map(|s| s.iter().copied())
.collect();
for ext in &raw.extensions {
for require in &ext.requires {
for type_name in &require.types {
record_protect(
type_name.as_str(),
&ext.protect,
&all_dep_names,
&mut type_protect,
);
}
for cmd_name in &require.commands {
if let Some(cmd) = raw.commands.get(cmd_name.as_str()) {
for param in &cmd.params {
record_protect(
param.type_name.as_str(),
&ext.protect,
&all_dep_names,
&mut type_protect,
);
}
}
}
}
}
let mut type_own_protect: HashMap<&str, Option<Vec<String>>> = HashMap::new();
for t in &raw.types {
if t.category == "include" || t.raw_c.is_empty() {
continue;
}
if let Some(ref p) = t.protect {
type_own_protect.insert(t.name.as_str(), Some(vec![p.clone()]));
}
}
for ext in &raw.extensions {
for require in &ext.requires {
for type_name in &require.types {
type_own_protect
.entry(type_name.as_str())
.or_insert_with(|| {
if ext.protect.is_empty() {
None
} else {
Some(ext.protect.clone())
}
});
}
}
}
for t in &raw.types {
if t.raw_c.is_empty() || t.category == "include" {
continue;
}
let struct_protect = match type_own_protect.get(t.name.as_str()) {
None => continue, Some(None) => None, Some(Some(v)) => Some(v.as_slice()),
};
for word in ident_words(&t.raw_c) {
if !all_dep_names.contains(word) {
continue;
}
let entry = type_protect.entry(word).or_insert_with(|| Some(Vec::new()));
match (entry.as_mut(), struct_protect) {
(None, _) => {} (Some(_), None) => {
*entry = None;
} (Some(guards), Some(ps)) => {
for p in ps {
if !guards.contains(p) {
guards.push(p.to_string());
}
}
}
}
}
}
let mut result: HashMap<String, Vec<String>> = HashMap::new();
for (include_name, dep_names) in &include_to_deps {
let mut all_guards: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut unconditional = false;
let mut any_found = false;
for &dep_name in dep_names {
if let Some(state) = type_protect.get(dep_name) {
any_found = true;
match state {
None => {
unconditional = true;
break;
}
Some(guards) => {
all_guards.extend(guards.iter().cloned());
}
}
}
}
if !any_found {
continue;
}
if unconditional || all_guards.is_empty() {
result.insert(include_name.to_string(), Vec::new());
} else {
let mut v: Vec<String> = all_guards.into_iter().collect();
v.sort();
result.insert(include_name.to_string(), v);
}
}
result
}
fn collect_required_headers(
raw: &RawSpec,
req_types: &HashSet<String>,
spec_name: &str,
) -> Vec<String> {
let gl_auto_exclude: HashSet<&str> = ["stddef", "khrplatform", "inttypes"]
.iter()
.copied()
.collect();
let mut headers: IndexMap<String, ()> = IndexMap::new();
for t in &raw.types {
let selected = if spec_name == "vk" {
req_types.contains(&t.name)
} else {
!gl_auto_exclude.contains(t.name.as_str())
&& (req_types.contains(&t.name) || t.api.as_deref().is_none_or(|a| a == spec_name))
};
if !selected {
continue;
}
if let Some(ref req) = t.requires
&& let Some(hdr) = requires_to_bundled_header(req)
{
headers.insert(hdr.to_string(), ());
}
}
if spec_name == "vk" {
headers.insert("vk_platform.h".to_string(), ());
let vk_video_includes: std::collections::HashSet<&str> = raw
.types
.iter()
.filter(|t| t.category == "include" && t.name.starts_with("vk_video/"))
.map(|t| t.name.as_str())
.collect();
for t in &raw.types {
if t.category == "include" {
continue;
}
if let Some(ref req) = t.requires
&& vk_video_includes.contains(req.as_str())
{
let auto = matches!(
t.category.as_str(),
"define" | "basetype" | "bitmask" | "funcpointer" | "enum" | "handle"
);
if auto || req_types.contains(&t.name) {
headers.insert(req.clone(), ());
}
}
}
}
headers.into_keys().collect()
}
fn requires_to_bundled_header(requires: &str) -> Option<&'static str> {
match requires {
"khrplatform" => Some("KHR/khrplatform.h"),
"eglplatform" => Some("EGL/eglplatform.h"),
"vk_platform" => Some("vk_platform.h"),
_ => None,
}
}
fn normalize_raw_c(raw: &str) -> String {
raw.trim().to_string()
}
fn build_flat_enums(raw: &RawSpec, req_enums: &HashSet<String>, is_vulkan: bool) -> Vec<FlatEnum> {
raw.flat_enums
.iter()
.filter(|(name, _)| is_vulkan || req_enums.contains(*name))
.filter_map(|(_, e)| {
let value = e.value.as_deref().or(e.alias.as_deref())?;
Some(FlatEnum {
name: e.name.clone(),
value: value.to_string(),
comment: e.comment.clone(),
})
})
.collect()
}
fn build_enum_groups(raw: &RawSpec) -> Vec<EnumGroup> {
raw.enum_groups
.iter()
.map(|g| {
let raw_values: Vec<FlatEnum> = g
.values
.iter()
.filter_map(|v| {
let val = v.value.as_deref().or(v.alias.as_deref())?;
Some(FlatEnum {
name: v.name.clone(),
value: val.to_string(),
comment: v.comment.clone(),
})
})
.collect();
EnumGroup {
name: g.name.clone(),
is_bitmask: false,
bitwidth: g.bitwidth.unwrap_or(32),
values: sort_enum_values(raw_values),
}
})
.collect()
}
fn sort_enum_values(values: Vec<FlatEnum>) -> Vec<FlatEnum> {
let n = values.len();
if n == 0 {
return values;
}
let name_to_idx: HashMap<&str, usize> = values
.iter()
.enumerate()
.map(|(i, v)| (v.name.as_str(), i))
.collect();
let deps: Vec<Option<usize>> = values
.iter()
.map(|v| {
let val = v.value.trim();
let is_numeric = val.starts_with(|c: char| c.is_ascii_digit())
|| val.starts_with("0x")
|| val.starts_with("0X")
|| (val.starts_with('-') && val.len() > 1);
if is_numeric {
return None;
}
name_to_idx.get(val).copied()
})
.collect();
let mut in_degree: Vec<usize> = deps.iter().map(|d| d.is_some() as usize).collect();
let mut rev: Vec<Vec<usize>> = vec![Vec::new(); n];
for (i, dep) in deps.iter().enumerate() {
if let Some(d) = dep {
rev[*d].push(i);
}
}
let mut queue: std::collections::VecDeque<usize> =
(0..n).filter(|&i| in_degree[i] == 0).collect();
let mut order: Vec<usize> = Vec::with_capacity(n);
while let Some(node) = queue.pop_front() {
order.push(node);
for &dep in &rev[node] {
in_degree[dep] -= 1;
if in_degree[dep] == 0 {
queue.push_back(dep);
}
}
}
for (i, item) in in_degree.iter().enumerate().take(n) {
if *item != 0 {
order.push(i);
}
}
let mut out: Vec<Option<FlatEnum>> = values.into_iter().map(Some).collect();
order.into_iter().map(|i| out[i].take().unwrap()).collect()
}
fn build_alias_pairs(raw: &RawSpec, commands: &[Command]) -> Vec<AliasPair> {
let idx: HashMap<&str, u16> = commands
.iter()
.map(|c| (c.name.as_str(), c.index))
.collect();
let mut groups: HashMap<String, Vec<String>> = HashMap::new();
for (name, cmd) in &raw.commands {
if let Some(ref alias) = cmd.alias {
if !idx.contains_key(name.as_str()) || !idx.contains_key(alias.as_str()) {
continue;
}
let (canonical, secondary) = if alias.len() < name.len()
|| (alias.len() == name.len() && alias.as_str() < name.as_str())
{
(alias.clone(), name.clone())
} else {
(name.clone(), alias.clone())
};
groups.entry(canonical).or_default().push(secondary);
}
}
let mut pairs: Vec<AliasPair> = Vec::new();
for (canonical, secondaries) in groups {
let Some(&ci) = idx.get(canonical.as_str()) else {
continue;
};
for secondary in secondaries {
let Some(&si) = idx.get(secondary.as_str()) else {
continue;
};
pairs.push(AliasPair {
canonical: ci,
secondary: si,
});
}
}
pairs.sort_by_key(|p| (p.canonical, p.secondary));
pairs
}
fn find_command_protect(
_raw: &RawSpec,
cmd_name: &str,
exts: &[SelectedExt<'_>],
) -> Option<String> {
for ext in exts {
for require in &ext.raw.requires {
if require.commands.iter().any(|c| c == cmd_name)
&& let Some(p) = ext.raw.protect.first()
{
return Some(p.clone());
}
}
}
None
}
fn api_pfn_prefix(spec: &str) -> &'static str {
match spec {
"vk" => "PFN_",
"gl" | "gles1" | "gles2" | "glcore" => "PFNGL",
"egl" => "PFNEGL",
"glx" => "PFNGLX",
"wgl" => "PFNWGL",
_ => "PFN",
}
}
fn api_name_prefix(spec: &str) -> &'static str {
match spec {
"gl" | "gles1" | "gles2" | "glcore" => "gl",
"egl" => "egl",
"glx" => "glX",
"wgl" => "wgl",
"vk" | "vulkan" => "vk",
_ => "",
}
}
fn api_profile_matches(
elem_api: Option<&str>,
elem_profile: Option<&str>,
target_api: &str,
target_prof: Option<&str>,
) -> bool {
if let Some(a) = elem_api
&& !a.split(',').any(|x| x.trim() == target_api)
{
return false;
}
profile_matches(elem_profile, target_prof)
}
fn profile_matches(elem_profile: Option<&str>, target_profile: Option<&str>) -> bool {
match (elem_profile, target_profile) {
(None, _) => true,
(Some(_), None) => true,
(Some(ep), Some(tp)) => ep == tp,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_enum(name: &str, value: &str) -> FlatEnum {
FlatEnum {
name: name.to_string(),
value: value.to_string(),
comment: String::new(),
}
}
#[test]
fn sort_enum_values_numeric_only_preserves_order() {
let input = vec![
make_enum("VK_FOO", "0"),
make_enum("VK_BAR", "1"),
make_enum("VK_BAZ", "2"),
];
let out = sort_enum_values(input);
assert_eq!(out[0].name, "VK_FOO");
assert_eq!(out[1].name, "VK_BAR");
assert_eq!(out[2].name, "VK_BAZ");
}
#[test]
fn sort_enum_values_alias_placed_after_target() {
let input = vec![
make_enum("VK_ALIAS", "VK_ORIGINAL"),
make_enum("VK_ORIGINAL", "42"),
];
let out = sort_enum_values(input);
let original_pos = out.iter().position(|e| e.name == "VK_ORIGINAL").unwrap();
let alias_pos = out.iter().position(|e| e.name == "VK_ALIAS").unwrap();
assert!(original_pos < alias_pos, "alias must come after its target");
}
#[test]
fn sort_enum_values_empty_input() {
assert!(sort_enum_values(vec![]).is_empty());
}
#[test]
fn sort_enum_values_negative_numeric_not_treated_as_alias() {
let input = vec![make_enum("VK_MAX", "-1"), make_enum("VK_ZERO", "0")];
let out = sort_enum_values(input);
assert_eq!(out[0].name, "VK_MAX");
assert_eq!(out[1].name, "VK_ZERO");
}
#[test]
fn sort_enum_values_hex_literal_not_treated_as_alias() {
let input = vec![make_enum("VK_HEX", "0xFF"), make_enum("VK_OTHER", "0x00")];
let out = sort_enum_values(input);
assert_eq!(out[0].name, "VK_HEX");
assert_eq!(out[1].name, "VK_OTHER");
}
#[test]
fn profile_matches_both_none() {
assert!(profile_matches(None, None));
}
#[test]
fn profile_matches_element_none_always_matches() {
assert!(profile_matches(None, Some("core")));
assert!(profile_matches(None, Some("compat")));
}
#[test]
fn profile_matches_target_none_always_matches() {
assert!(profile_matches(Some("core"), None));
}
#[test]
fn profile_matches_same() {
assert!(profile_matches(Some("core"), Some("core")));
}
#[test]
fn profile_matches_different() {
assert!(!profile_matches(Some("core"), Some("compat")));
}
#[test]
fn pfn_prefix_gl_family() {
for api in &["gl", "gles1", "gles2", "glcore"] {
assert_eq!(api_pfn_prefix(api), "PFNGL", "failed for '{api}'");
}
}
#[test]
fn pfn_prefix_vulkan() {
assert_eq!(api_pfn_prefix("vk"), "PFN_");
}
#[test]
fn name_prefix_gl_family() {
assert_eq!(api_name_prefix("gl"), "gl");
assert_eq!(api_name_prefix("gles1"), "gl");
assert_eq!(api_name_prefix("gles2"), "gl");
}
#[test]
fn name_prefix_glx_is_case_sensitive() {
assert_eq!(api_name_prefix("glx"), "glX");
}
}