Shader packs: geometry stages whose varyings travel in interface blocks

A shadow program with a geometry stage is folded into one vertex stage
(translate/geometry.rs), which reads both stages' declarations one variable
at a time. An `out` interface block in the vertex stage was read as a single
declaration: each member after the first had its type taken for a variable
name, and that type (`float`...) was then renamed throughout the vertex
stage, so the program failed with "`far` is `float` in one stage and
`hz_vs_float` in the other" and the shadow map stayed clear.

- Both stages' blocks become plain varyings first (interface_blocks), and
  `flatten` now also lowers a block with an instance array, the form of a
  geometry stage's input (`vIn[i].m` reads `hz_<block>_m[i]`).
- A geometry stage's input and output blocks may share a name (GLSL 4.60,
  4.3.9): its inputs read as `hz_in_<block>_<member>`, so they no longer
  collide with its outputs.
- Statement reading takes every qualifier the translator knows (`sample`,
  `precise`): one it missed made a type a renamed name the same way.

Part of #6812

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Arslan ArmanulyandClaude Opus 5.5 committed 2026-11-02 16:37:40 +05:00
1 parent 898042ced8
commit d5418e0137
2 files changed
+154 -53

No files matched your search

@@ -12,7 +12,7 @@
use std::collections::BTreeSet;
use std::fmt::Write;
use super::scan::significant;
use super::scan::{QUALIFIERS, STORAGE, significant};
use super::{Error, GbuffersInputs, Program, Target};
use crate::Preprocessed;
use crate::macros::lex;
@@ -158,22 +158,9 @@ fn classify(tokens: &[&str], range: std::ops::Range<usize>) -> Statement {
}
k += 1;
}
let quals = [
"in",
"out",
"attribute",
"varying",
"flat",
"smooth",
"noperspective",
"centroid",
"invariant",
"highp",
"mediump",
"lowp",
"const",
];
while k < words.len() && quals.contains(&words[k]) {
// Every qualifier the translator knows (`sample`, `precise`...), in any order (GLSL
// 4.60, 4.12): one it missed would be taken for the type, and the type for a name.
while k < words.len() && (STORAGE.contains(&words[k]) || QUALIFIERS.contains(&words[k])) {
k += 1;
}
match words.get(k) {
@@ -246,8 +233,14 @@ fn combine(
message,
}]
};
let gs_tokens = lex(&geometry.code);
let vs_tokens = lex(&vertex.code);
// Interface blocks first become plain varyings (`interface_blocks`), the vertex stage's
// `out` block and the geometry stage's `in` array alike: what follows reads declarations
// of one type each. The geometry stage's input blocks read as `hz_in_<block>_<member>`,
// apart from its output blocks, which may have the same names.
let gs_code = super::interface_blocks::flatten_as(&geometry.code, "in_");
let vs_code = super::interface_blocks::flatten(&vertex.code);
let gs_tokens = lex(gs_code.as_deref().unwrap_or(&geometry.code));
let vs_tokens = lex(vs_code.as_deref().unwrap_or(&vertex.code));
let gs = statements(&gs_tokens);
let vs = statements(&vs_tokens);
@@ -429,8 +422,12 @@ fn combine(
}
main.push_str(" hz_vs_main();\n");
for (name, _) in &vs_outs {
if gs_ins.iter().any(|(n, _)| n == name) {
writeln!(main, " {name}[hz_i] = hz_vs_{name};").unwrap();
let block = name.strip_prefix("hz_").map(|m| format!("hz_in_{m}"));
if let Some((input, _)) = gs_ins
.iter()
.find(|(n, _)| n == name || Some(n) == block.as_ref())
{
writeln!(main, " {input}[hz_i] = hz_vs_{name};").unwrap();
}
}
main.push_str(" hz_gl_in[hz_i].p = hz_vs_Position;\n }\n hz_gs_main();\n");
@@ -583,17 +580,8 @@ mod tests {
use crate::pack::tests::zip;
use crate::{Macros, Pack, preprocess};
#[test]
fn a_geometry_stage_folds_into_the_vertex_stage() {
let vsh = "#version 430\nout vec3 vPos;\nout vec4 vColor;\nvoid main() {\n \
vPos = gl_Vertex.xyz;\n vColor = gl_Color * float(gl_InstanceID + 1);\n \
gl_Position = ftransform();\n}\n";
let gsh = "#version 430\nlayout(triangles) in;\nlayout(triangle_strip, max_vertices = 4) out;\n\
in vec3 vPos[];\nin vec4 vColor[];\nout vec4 gColor;\nvoid main() {\n \
for (int i = 0; i < 3; i++) {\n gColor = vColor[i];\n \
gl_Position = gl_in[i].gl_Position;\n EmitVertex();\n }\n \
gl_Position = vec4(vPos[0], 1.0);\n EmitVertex();\n EndPrimitive();\n}\n";
let fsh = "#version 430\nin vec4 gColor;\nvoid main() {\n gl_FragData[0] = gColor;\n}\n";
/// `vsh`, `gsh` and `fsh` (a pack's shadow program) folded and translated.
fn fold(vsh: &str, gsh: &str, fsh: &str) -> Result<(Program, u32), Vec<Error>> {
let pack = Pack::from_zip_bytes(
"P.zip",
zip(&[
@@ -617,19 +605,73 @@ mod tests {
.into(),
index: "uint(@)".into(),
};
let (program, triangles) = translate_geometry(
translate_geometry(
&pp("shadow.vsh"),
&pp("shadow.gsh"),
&pp("shadow.fsh"),
&inputs,
)
.unwrap_or_else(|e| panic!("{e:?}"));
}
#[test]
fn a_geometry_stage_folds_into_the_vertex_stage() {
let vsh = "#version 430\nout vec3 vPos;\nout vec4 vColor;\nvoid main() {\n \
vPos = gl_Vertex.xyz;\n vColor = gl_Color * float(gl_InstanceID + 1);\n \
gl_Position = ftransform();\n}\n";
let gsh = "#version 430\nlayout(triangles) in;\nlayout(triangle_strip, max_vertices = 4) out;\n\
in vec3 vPos[];\nin vec4 vColor[];\nout vec4 gColor;\nvoid main() {\n \
for (int i = 0; i < 3; i++) {\n gColor = vColor[i];\n \
gl_Position = gl_in[i].gl_Position;\n EmitVertex();\n }\n \
gl_Position = vec4(vPos[0], 1.0);\n EmitVertex();\n EndPrimitive();\n}\n";
let fsh = "#version 430\nin vec4 gColor;\nvoid main() {\n gl_FragData[0] = gColor;\n}\n";
let (program, triangles) = fold(vsh, gsh, fsh).unwrap_or_else(|e| panic!("{e:?}"));
// A strip of four corners is two triangles.
assert_eq!(triangles, 2);
assert_eq!(program.vertex.entry_points.len(), 1);
assert_eq!(program.fragment.entry_points.len(), 1);
}
/// Stages that pass their varyings in interface blocks (GLSL 4.60, 4.3.9), members with
/// their own qualifiers: the vertex stage's `out` block, the geometry stage's `in` block
/// array and `out` block, the fragment stage's `in` block. The geometry stage's two blocks
/// share a name, as an input and an output block may (4.3.9).
#[test]
fn interface_blocks_pass_between_the_stages() {
let vsh = "#version 430\nuniform float far;\n\
out VertexData {\n vec4 color;\n flat float id;\n vec2 uv;\n} vOut;\n\
void main() {\n vOut.color = gl_Color;\n vOut.id = far;\n \
vOut.uv = gl_Vertex.xy;\n gl_Position = ftransform();\n}\n";
let gsh = "#version 430\nlayout(triangles) in;\nlayout(triangle_strip, max_vertices = 3) out;\n\
in VertexData {\n vec4 color;\n flat float id;\n vec2 uv;\n} vIn[];\n\
out VertexData {\n vec4 color;\n flat float id;\n} gOut;\n\
void main() {\n for (int i = 0; i < 3; i++) {\n \
gOut.color = vIn[i].color * vIn[i].uv.x;\n gOut.id = vIn[0].id;\n \
gl_Position = gl_in[i].gl_Position;\n EmitVertex();\n }\n \
EndPrimitive();\n}\n";
let fsh = "#version 430\nuniform float far;\n\
in VertexData {\n vec4 color;\n flat float id;\n} gIn;\n\
void main() {\n gl_FragData[0] = gIn.color * gIn.id / far;\n}\n";
let (program, triangles) = fold(vsh, gsh, fsh).unwrap_or_else(|e| panic!("{e:?}"));
assert_eq!(triangles, 1);
assert_eq!(program.fragment.entry_points.len(), 1);
}
/// An output qualified `sample` declares no variable named after its type (GLSL 4.60, 4.3).
#[test]
fn sample_outputs_rename_no_type() {
let vsh = "#version 430\nuniform float far;\n\
out sample float vDepth;\nvoid main() {\n vDepth = far;\n \
gl_Position = ftransform();\n}\n";
let gsh = "#version 430\nlayout(triangles) in;\nlayout(triangle_strip, max_vertices = 3) out;\n\
in float vDepth[];\nout float gDepth;\nvoid main() {\n \
for (int i = 0; i < 3; i++) {\n gDepth = vDepth[i];\n \
gl_Position = gl_in[i].gl_Position;\n EmitVertex();\n }\n \
EndPrimitive();\n}\n";
let fsh = "#version 430\nuniform float far;\nin float gDepth;\n\
void main() {\n gl_FragData[0] = vec4(gDepth / far);\n}\n";
fold(vsh, gsh, fsh).unwrap_or_else(|e| panic!("{e:?}"));
}
#[test]
fn writes_run_once_and_keep_their_else() {
let code =
@@ -4,8 +4,10 @@
//! type, found Interpolation(Flat)"). Each member of such a block becomes a varying of its own,
//! `hz_<block>_<member>` (the name the next stage's block gives it too: stages match blocks by
//! block name and members by name), and `instance.member` reads it. A block without an instance
//! name keeps its members' names. Blocks with an instance array (a geometry stage's input) and
//! blocks whose instance is used whole are left as written. Lines stay where they were.
//! name keeps its members' names. Of a block with an instance array (a geometry stage's input,
//! `in VertexData { vec2 uv; } vIn[];`), each member becomes an array, `vIn[i].uv` reading
//! `hz_VertexData_uv[i]`. Blocks whose instance is used whole are left as written. Lines stay
//! where they were.
use super::scan::significant;
use crate::macros::lex;
@@ -36,10 +38,19 @@ struct Block {
name: String,
members: Vec<(usize, Vec<(usize, String)>)>,
instance: Option<(usize, String)>,
/// The instance's array suffix (`[]`, `[3]`), empty when it is not an array.
array: String,
}
/// `code` with its plain `in`/`out` interface blocks as varyings, or `None` when it has none.
pub(super) fn flatten(code: &str) -> Option<String> {
flatten_as(code, "")
}
/// `flatten`, the members of an `in` block with an instance named `hz_<inputs><block>_<member>`:
/// a geometry stage's input and output blocks may share their name (GLSL 4.60, 4.3.9), and the
/// geometry stage reads its inputs under names of its own.
pub(super) fn flatten_as(code: &str, inputs: &str) -> Option<String> {
let mut tokens: Vec<String> = lex(code).into_iter().map(str::to_owned).collect();
let blocks = find_blocks(&tokens);
if blocks.is_empty() {
@@ -48,17 +59,25 @@ pub(super) fn flatten(code: &str) -> Option<String> {
let mut changed = false;
for b in blocks {
let inside = |i: usize| (b.start..=b.end).contains(&i);
let prefix = if b.storage == "in" { inputs } else { "" };
let renamed = |member: &str| match &b.instance {
Some(_) => format!("hz_{}_{member}", b.name),
Some(_) => format!("hz_{prefix}{}_{member}", b.name),
None => member.to_owned(),
};
// Every use of the instance outside the block must be `instance.member`.
// Every use of the instance outside the block must be `instance.member`, or
// `instance[i].member` for an instance array.
let mut reads = Vec::new();
if let Some((_, instance)) = &b.instance {
let mut whole = false;
for i in (0..tokens.len()).filter(|&i| !inside(i) && tokens[i] == *instance) {
let next = next_significant(&tokens, i + 1);
let member = next
let index = match b.array.is_empty() {
true => Some(i),
false => next_significant(&tokens, i + 1)
.filter(|&o| tokens[o] == "[")
.and_then(|o| closing(&tokens, o)),
};
let member = index
.and_then(|x| next_significant(&tokens, x + 1))
.filter(|&d| tokens[d] == ".")
.and_then(|d| next_significant(&tokens, d + 1));
let known = member.filter(|&m| {
@@ -66,8 +85,8 @@ pub(super) fn flatten(code: &str) -> Option<String> {
.iter()
.any(|(_, names)| names.iter().any(|(_, n)| *n == tokens[m]))
});
match (next, known) {
(Some(_), Some(m)) => reads.push((i, m)),
match (index, known) {
(Some(x), Some(m)) => reads.push((i, x, m)),
_ => whole = true,
}
}
@@ -75,10 +94,11 @@ pub(super) fn flatten(code: &str) -> Option<String> {
continue;
}
}
for (i, member) in reads {
for (i, index, member) in reads {
tokens[i] = renamed(&tokens[member]);
// `.member` goes, with the spaces around it but not its line breaks.
for t in &mut tokens[i + 1..=member] {
// `.member` goes, with the spaces around it but not its line breaks; an index stays
// (`v[i].m` is `hz_B_m[i]`).
for t in &mut tokens[index + 1..=member] {
t.retain(|c| c == '\n');
}
}
@@ -91,7 +111,8 @@ pub(super) fn flatten(code: &str) -> Option<String> {
for (first, names) in &b.members {
tokens[*first] = format!("{} {}", b.storage, tokens[*first]);
for (at, name) in names {
tokens[*at] = renamed(name);
// Of an instance array, each member is an array of the instance's size, outermost.
tokens[*at] = renamed(name) + &b.array;
}
}
changed = true;
@@ -111,6 +132,24 @@ fn next_significant(tokens: &[String], from: usize) -> Option<usize> {
(from..tokens.len()).find(|&i| significant(&tokens[i]))
}
/// The `]` that closes the `[` at `open`.
fn closing(tokens: &[String], open: usize) -> Option<usize> {
let mut depth = 0usize;
for (j, t) in tokens.iter().enumerate().skip(open) {
match t.as_str() {
"[" => depth += 1,
"]" => {
depth -= 1;
if depth == 0 {
return Some(j);
}
}
_ => {}
}
}
None
}
/// The `in`/`out` interface blocks at global scope that can be flattened.
fn find_blocks(tokens: &[String]) -> Vec<Block> {
let mut out = Vec::new();
@@ -235,10 +274,17 @@ fn block(tokens: &[String], start: usize, open: usize) -> Option<Block> {
.filter(|&j| significant(&tokens[j]))
.collect();
let end = (close + 1..tokens.len()).find(|&j| tokens[j] == ";")?;
let instance = match after.as_slice() {
[] => None,
[n] if super::scan::is_ident(&tokens[*n]) => Some((*n, tokens[*n].clone())),
_ => return None, // an instance array, or something else
let (instance, array) = match after.as_slice() {
[] => (None, String::new()),
// `vIn[]`, `vIn[3]`: a geometry stage's input (GLSL 4.60, 4.3.9).
[n, rest @ ..]
if super::scan::is_ident(&tokens[*n])
&& (rest.is_empty() || tokens[rest[0]] == "[" && tokens[*rest.last()?] == "]") =>
{
let array = rest.iter().map(|&j| tokens[j].as_str()).collect();
(Some((*n, tokens[*n].clone())), array)
}
_ => return None,
};
if members.is_empty() {
return None;
@@ -252,6 +298,7 @@ fn block(tokens: &[String], start: usize, open: usize) -> Option<Block> {
name,
members,
instance,
array,
})
}
@@ -286,14 +333,26 @@ mod tests {
);
}
#[test]
fn an_instance_array_makes_each_member_an_array() {
let code = "in VertexData {\n flat int id;\n vec2 st[2];\n} vIn[];\n\
void main() { int i = vIn[0].id; vec2 a = vIn[i + 1] . st[1]; }\n";
let out = flatten(code).unwrap();
assert_eq!(out.lines().count(), code.lines().count());
assert_eq!(
out.split_whitespace().collect::<Vec<_>>().join(" "),
"in flat int hz_VertexData_id[]; in vec2 hz_VertexData_st[][2]; void main() { \
int i = hz_VertexData_id[0]; vec2 a = hz_VertexData_st[i + 1][1]; }"
);
}
#[test]
fn other_blocks_stay_as_written() {
for code in [
// Uniform and storage blocks, a geometry stage's instance array, an instance
// used whole.
// Uniform and storage blocks, an instance used whole.
"uniform Frame { float t; } frame;\nvoid main() {}\n",
"buffer Data { float v[]; } data;\nvoid main() {}\n",
"in VertexData { flat int id; } vIn[];\nvoid main() {}\n",
"in VertexData { flat int id; } vIn[];\nvoid f(VertexData d) {}\nvoid main() { f(vIn[0]); }\n",
"out VertexData { flat int id; } vOut;\nvoid f(int x) {}\nvoid main() { vOut = vOut; }\n",
] {
assert_eq!(flatten(code), None, "{code}");