Interface System
The interface system is one of AXSLC's key convenience features. It allows you to define reusable field sets at global scope and reference them by name in stages.
Motivation
In traditional GLSL, passing data between shader stages requires declaring matching interface blocks in both stages:
// vertex.glsl
out VertexOut {
vec2 v_uv;
vec3 v_normal;
} v;
// fragment.glsl
in VertexOut {
vec2 v_uv;
vec3 v_normal;
} v;If you add or change a field, you must update both files. AXSLC's interface system eliminates this duplication.
Global Interface Definitions
Declare an interface once at global scope:
interface Varyings {
vec2 v_uv;
vec3 v_normal;
};This declaration is not emitted directly to any GLSL output. It serves as a template.
Stage References
Reference the interface by name in stages:
@stage vertex {
out Varyings v;
// ...
}
@stage fragment {
in Varyings v;
// ...
}The emitter looks up the Varyings interface definition and expands it inline into a GLSL interface block:
// out.vert.glsl
out Varyings {
vec2 v_uv;
vec3 v_normal;
} v;
// out.frag.glsl
in Varyings {
vec2 v_uv;
vec3 v_normal;
} v;Instance Names
The identifier after the interface name (v in the examples above) becomes the block instance name in GLSL. This is optional:
out Varyings; // No instance nameEmits:
out Varyings {
vec2 v_uv;
vec3 v_normal;
};Without an instance name, fields are accessed directly: v_uv instead of v.v_uv.
Inline Interface Blocks
You can also declare interface blocks inline without referencing a global definition:
@stage vertex {
out VertexOut {
vec2 v_uv;
vec3 v_normal;
} v;
}This is emitted directly without lookup. It's useful for stage-specific interfaces that aren't shared.
Annotations
Interface fields can have annotations:
interface Varyings {
@location(0) vec2 v_uv;
@location(1) vec3 v_normal;
@location(2) vec4 v_color;
};These annotations are preserved when the interface is expanded:
out Varyings {
layout(location = 0) vec2 v_uv;
layout(location = 1) vec3 v_normal;
layout(location = 2) vec4 v_color;
} v;Complete Example
@version 450;
// Define shared interface once
interface Varyings {
vec2 v_uv;
vec3 v_normal;
vec3 v_world_pos;
};
@stage vertex {
in Attributes {
@location(0) vec3 a_pos;
@location(1) vec2 a_uv;
@location(2) vec3 a_normal;
} a;
// Reference the interface
out Varyings v;
@binding(0) uniform mat4 u_mvp;
@binding(1) uniform mat4 u_model;
void main() {
v.v_uv = a.a_uv;
v.v_normal = mat3(u_model) * a.a_normal;
vec4 world_pos = u_model * vec4(a.a_pos, 1.0);
v.v_world_pos = world_pos.xyz;
gl_Position = u_mvp * world_pos;
}
}
@stage fragment {
// Same interface, automatically matched
in Varyings v;
out vec4 o_color;
@binding(2) uniform sampler2D u_tex;
void main() {
vec3 normal = normalize(v.v_normal);
vec4 tex_color = texture(u_tex, v.v_uv);
o_color = tex_color;
}
}Both stages share the same Varyings definition. If you need to add a new varying (e.g., vec4 v_tangent), you only change it in one place.
Resolution Process
When the emitter encounters an InterfaceRef node (like out Varyings v), it:
- Calls
find_interface(program, "Varyings") - Looks up the corresponding
InterfaceDeclin the program globals - Emits a GLSL interface block with:
- The interface name as the block name
- The fields from the
InterfaceDecl - The
is_inflag from theInterfaceRef(invsout) - The instance name from the
InterfaceRef
If no matching InterfaceDecl is found, the emitter silently emits nothing (the downstream GLSL compiler will report the error).