Skip to content

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:

glsl
// 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:

axsl
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:

axsl
@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:

glsl
// 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:

axsl
out Varyings;  // No instance name

Emits:

glsl
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:

axsl
@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:

axsl
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:

glsl
out Varyings {
    layout(location = 0) vec2 v_uv;
    layout(location = 1) vec3 v_normal;
    layout(location = 2) vec4 v_color;
} v;

Complete Example

axsl
@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:

  1. Calls find_interface(program, "Varyings")
  2. Looks up the corresponding InterfaceDecl in the program globals
  3. Emits a GLSL interface block with:
    • The interface name as the block name
    • The fields from the InterfaceDecl
    • The is_in flag from the InterfaceRef (in vs out)
    • 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).

Interface Template Expansion