Skip to content

Commit b419f37

Browse files
committed
feat(resolve): add --promoted and --predecessors extension selection flags
--promoted automatically includes any extension whose commands were promoted into the requested core version, in both the same-name case (e.g. ARB_copy_buffer → glCopyBufferSubData) and the renamed case (e.g. ARB_multitexture → glActiveTexture). Selection is scoped per-API to prevent cross-contamination in merged builds. --predecessors automatically includes any extension that is a predecessor of an already-selected extension — i.e. its commands are aliases of commands in the selected set. Applies to a fixed-point closure, so chains of predecessors (A superseded by B superseded by C) are followed fully. Runs after --promoted, so promoted extensions also seed the predecessor search. Both flags are independent of --alias, which remains a runtime concern (filling null function pointer slots at load time) rather than a selection concern. Signed-off-by: Steven Noonan <steven@uplinklabs.net>
1 parent 4f5a565 commit b419f37

2 files changed

Lines changed: 176 additions & 6 deletions

File tree

src/cli.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ use crate::ir::Version;
1616
about = "Vulkan/OpenGL/GLES/EGL/GLX/WGL loader generator"
1717
)]
1818
pub struct Cli {
19+
/// Automatically include any extension whose commands were promoted into the
20+
/// requested core version, even if not listed in --extensions.
21+
#[arg(long)]
22+
pub promoted: bool,
23+
24+
/// Automatically include any extension that is a predecessor of an
25+
/// explicitly selected extension (i.e. its commands are aliases of commands
26+
/// in the selected set). For example, if GL_KHR_parallel_shader_compile is
27+
/// selected, GL_ARB_parallel_shader_compile is included automatically.
28+
#[arg(long)]
29+
pub predecessors: bool,
30+
1931
/// API specifiers: comma-separated name\[:profile\]=version pairs.
2032
/// Profile is required for GL (core|compat). Version is optional (latest if omitted).
2133
/// Example: gl:core=3.3,gles2=3.0

src/resolve.rs

Lines changed: 164 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ pub struct AliasPair {
187187
pub fn build_feature_sets(cli: &Cli) -> Result<Vec<FeatureSet>> {
188188
let requests = cli.api_requests()?;
189189
let ext_filter = cli.extension_filter()?;
190+
let promoted = cli.promoted;
191+
let predecessors = cli.predecessors;
190192

191193
let alias = match &cli.generator {
192194
crate::cli::Generator::C(c) => c.alias,
@@ -209,16 +211,24 @@ pub fn build_feature_sets(cli: &Cli) -> Result<Vec<FeatureSet>> {
209211
for (spec_name, reqs) in &by_spec {
210212
let sources = fetch::load_spec(spec_name, cli.fetch)?;
211213
let raw = parse::parse(&sources, spec_name)?;
212-
let fs = resolve_feature_set(&raw, reqs, &ext_filter, true, alias)?;
214+
let fs =
215+
resolve_feature_set(&raw, reqs, &ext_filter, true, alias, promoted, predecessors)?;
213216
feature_sets.push(fs);
214217
}
215218
} else {
216219
for req in &requests {
217220
let spec_name = req.spec_name();
218221
let sources = fetch::load_spec(spec_name, cli.fetch)?;
219222
let raw = parse::parse(&sources, spec_name)?;
220-
let fs =
221-
resolve_feature_set(&raw, std::slice::from_ref(req), &ext_filter, false, alias)?;
223+
let fs = resolve_feature_set(
224+
&raw,
225+
std::slice::from_ref(req),
226+
&ext_filter,
227+
false,
228+
alias,
229+
promoted,
230+
predecessors,
231+
)?;
222232
feature_sets.push(fs);
223233
}
224234
}
@@ -236,6 +246,8 @@ fn resolve_feature_set(
236246
ext_filter: &Option<Vec<String>>,
237247
is_merged: bool,
238248
want_aliases: bool,
249+
want_promoted: bool,
250+
want_predecessors: bool,
239251
) -> Result<FeatureSet> {
240252
let spec_name = &raw.spec_name;
241253
let is_vulkan = spec_name == "vk";
@@ -265,10 +277,14 @@ fn resolve_feature_set(
265277
let mut req_commands: IndexMap<String, ()> = IndexMap::new(); // preserves order
266278
let mut removed_commands: HashSet<String> = HashSet::new();
267279
let mut removed_enums: HashSet<String> = HashSet::new();
280+
// Per-API core command sets used by --promoted to scope promotion checks.
281+
// Keyed by API name (e.g. "gl", "gles2"); values exclude profile-removed commands.
282+
let mut per_api_core_cmds: HashMap<String, HashSet<String>> = HashMap::new();
268283

269284
for feat in &selected_features {
270285
let req_for_api = requests.iter().find(|r| r.name == feat.api);
271286
let profile = req_for_api.and_then(|r| r.profile.as_deref());
287+
let api_cmds = per_api_core_cmds.entry(feat.api.clone()).or_default();
272288

273289
for require in &feat.raw.requires {
274290
if !api_profile_matches(
@@ -283,6 +299,7 @@ fn resolve_feature_set(
283299
req_enums.extend(require.enums.iter().cloned());
284300
for cmd in &require.commands {
285301
req_commands.entry(cmd.clone()).or_insert(());
302+
api_cmds.insert(cmd.clone());
286303
}
287304
}
288305
for remove in &feat.raw.removes {
@@ -291,6 +308,11 @@ fn resolve_feature_set(
291308
}
292309
removed_commands.extend(remove.commands.iter().cloned());
293310
removed_enums.extend(remove.enums.iter().cloned());
311+
// Apply removes inline — features are processed in version order so
312+
// each version's removes are applied immediately after its requires.
313+
for cmd in &remove.commands {
314+
api_cmds.remove(cmd.as_str());
315+
}
294316
}
295317
}
296318
for cmd in &removed_commands {
@@ -300,7 +322,15 @@ fn resolve_feature_set(
300322
// ------------------------------------------------------------------
301323
// Step 3: Determine which extensions are selected.
302324
// ------------------------------------------------------------------
303-
let selected_exts = select_extensions(raw, requests, ext_filter, spec_name);
325+
let selected_exts = select_extensions(
326+
raw,
327+
requests,
328+
ext_filter,
329+
spec_name,
330+
&per_api_core_cmds,
331+
want_promoted,
332+
want_predecessors,
333+
);
304334

305335
// ------------------------------------------------------------------
306336
// Step 4: Collect additional required names from extensions.
@@ -578,6 +608,9 @@ fn select_extensions<'a>(
578608
requests: &[ApiRequest],
579609
filter: &Option<Vec<String>>,
580610
spec_name: &str,
611+
per_api_core_cmds: &HashMap<String, HashSet<String>>,
612+
want_promoted: bool,
613+
want_predecessors: bool,
581614
) -> Vec<SelectedExt<'a>> {
582615
let api_set: HashSet<&str> = requests.iter().map(|r| r.name.as_str()).collect();
583616
// WGL mandatory extensions (spec gotcha #9).
@@ -590,7 +623,8 @@ fn select_extensions<'a>(
590623
HashSet::new()
591624
};
592625

593-
raw.extensions
626+
let mut selected: Vec<SelectedExt<'a>> = raw
627+
.extensions
594628
.iter()
595629
.filter(|e| {
596630
let supported = e.supported.iter().any(|s| api_set.contains(s.as_str()));
@@ -606,7 +640,131 @@ fn select_extensions<'a>(
606640
}
607641
})
608642
.map(|e| SelectedExt { raw: e })
609-
.collect()
643+
.collect();
644+
645+
// Build the bidirectional alias map once — it's used by both the
646+
// --promoted and --predecessors passes.
647+
let cmd_to_alias: HashMap<&str, &str> = if want_promoted || want_predecessors {
648+
let mut m = HashMap::new();
649+
for (name, cmd) in &raw.commands {
650+
if let Some(ref alias) = cmd.alias {
651+
m.insert(name.as_str(), alias.as_str());
652+
m.insert(alias.as_str(), name.as_str());
653+
}
654+
}
655+
m
656+
} else {
657+
HashMap::new()
658+
};
659+
660+
if want_promoted {
661+
// Snapshot names already selected so we don't duplicate them.
662+
let already: HashSet<&str> = selected.iter().map(|e| e.raw.name.as_str()).collect();
663+
664+
for ext in &raw.extensions {
665+
if already.contains(ext.name.as_str()) {
666+
continue;
667+
}
668+
669+
// An extension is considered promoted if, for at least one API A that:
670+
// (a) the extension claims to support, and
671+
// (b) we are generating,
672+
// any of the extension's commands for that API appear in A's core
673+
// command set — either directly (same-name promotion) or via the
674+
// alias graph (renamed promotion, e.g. glActiveTextureARB → glActiveTexture).
675+
//
676+
// Checking per-API (rather than against the unified req_commands) prevents
677+
// cross-contamination in merged builds: a GLES2-only extension whose
678+
// commands happen to match GLES2 core will not be auto-included for gl:core.
679+
let is_promoted = ext
680+
.supported
681+
.iter()
682+
.filter(|s| api_set.contains(s.as_str()))
683+
.any(|api| {
684+
let Some(core_cmds) = per_api_core_cmds.get(api.as_str()) else {
685+
return false;
686+
};
687+
ext.requires
688+
.iter()
689+
// Only consider require blocks that apply to this API.
690+
.filter(|req| api_profile_matches(req.api.as_deref(), None, api, None))
691+
.any(|req| {
692+
req.commands.iter().any(|c| {
693+
// Same-name promotion: the command landed in core
694+
// with the same name (e.g. ARB_copy_buffer →
695+
// glCopyBufferSubData is unchanged).
696+
core_cmds.contains(c.as_str())
697+
// Renamed promotion: the command has an alias
698+
// that is in core (e.g. glActiveTextureARB →
699+
// glActiveTexture).
700+
|| cmd_to_alias
701+
.get(c.as_str())
702+
.is_some_and(|a| core_cmds.contains(*a))
703+
})
704+
})
705+
});
706+
707+
if is_promoted {
708+
selected.push(SelectedExt { raw: ext });
709+
}
710+
}
711+
}
712+
713+
if want_predecessors {
714+
// Build the set of all commands contributed by the currently selected
715+
// extensions (after --promoted may have expanded the set).
716+
// An unselected extension is a "predecessor" of the selected set if any
717+
// of its commands are aliases of commands in this set — i.e. the
718+
// extension was superseded by one already selected.
719+
//
720+
// We iterate to a fixed point because adding a predecessor may itself
721+
// have predecessors not yet in the set.
722+
loop {
723+
// Collect commands from all currently selected extensions.
724+
let selected_ext_cmds: HashSet<&str> = selected
725+
.iter()
726+
.flat_map(|e| {
727+
e.raw
728+
.requires
729+
.iter()
730+
.flat_map(|req| req.commands.iter().map(String::as_str))
731+
})
732+
.collect();
733+
734+
let already: HashSet<&str> = selected.iter().map(|e| e.raw.name.as_str()).collect();
735+
736+
let mut added_any = false;
737+
for ext in &raw.extensions {
738+
if already.contains(ext.name.as_str()) {
739+
continue;
740+
}
741+
let supported = ext.supported.iter().any(|s| api_set.contains(s.as_str()));
742+
if !supported {
743+
continue;
744+
}
745+
let is_predecessor = ext.requires.iter().any(|req| {
746+
req.commands.iter().any(|c| {
747+
// This extension's command is in the selected set directly,
748+
// or its alias is — meaning a newer extension absorbed it.
749+
selected_ext_cmds.contains(c.as_str())
750+
|| cmd_to_alias
751+
.get(c.as_str())
752+
.is_some_and(|a| selected_ext_cmds.contains(*a))
753+
})
754+
});
755+
if is_predecessor {
756+
selected.push(SelectedExt { raw: ext });
757+
added_any = true;
758+
}
759+
}
760+
761+
if !added_any {
762+
break;
763+
}
764+
}
765+
}
766+
767+
selected
610768
}
611769

612770
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)