Skip to content

Emitter

The emitter generates final GLSL code for each shader stage.

Entry Point

cpp
std::string OpenGLGLSLEmitter::emit(
    const Program &program,
    StageKind stage,
    const std::unordered_set<std::string> &used_uniforms
);

Called once per stage. used_uniforms is the set of uniform names that the linker determined are referenced in this stage.

Output Order

For each stage, the emitter writes sections in this order:

  1. Version header: #version 450 core + blank line
  2. Global declarations:
    • StructDeclstruct Name { ... };
    • FuncDecl → full function with body
    • VarDecl (with is_const=true) → const type name = expr;
  3. Global uniforms/buffers that satisfy both:
    • The uniform name appears in used_uniforms
    • The uniform is not shadowed by a stage-local declaration
  4. Stage items (in order of appearance):
    • InterfaceBlockin/out BlockName { fields } instance;
    • InterfaceRef → resolved from global InterfaceDecl
    • UniformDecllayout(...) uniform type name;
    • BufferDecllayout(...) buffer BlockName { fields } instance;
    • StructDecl, FuncDecl → same as globals

Dead Code Elimination

Layout Qualifier Generation

layout_quals(const Annotations &) accumulates qualifier fragments from the annotation list in order:

AnnotationFragment
@location(N)location = N
@binding(N)binding = N
@std430std430
@std140std140

Fragments are joined with , and wrapped in layout(...).

Example:

axsl
@std430 @binding(2)
buffer Data { float values[]; } data;

Emits:

glsl
layout(std430, binding = 2) buffer Data {
    float values[];
} data;

If no qualifying annotations are present, no layout prefix is emitted.

Uniform Shadowing

A global uniform (or buffer block) is shadowed when the target stage contains its own declaration with the same name.

When a stage shadow is detected:

  • The global version is suppressed from that stage's output
  • The stage-local version is emitted instead

This allows per-stage overrides of globally declared uniforms.

Example

axsl
uniform float global_value = 1.0;

@stage vertex {
    uniform float global_value = 2.0;  // shadows global
    void main() {
        // uses 2.0 in vertex stage
    }
}

@stage fragment {
    void main() {
        // uses 1.0 in fragment stage
    }
}

Vertex output:

glsl
uniform float global_value = 2.0;  // stage-local

Fragment output:

glsl
uniform float global_value = 1.0;  // global

Shadowing is checked by is_shadowed(name, stage, program), which scans the stage's item list for a UniformDecl or BufferDecl with the matching name.

Interface Resolution

When a stage contains in Name inst or out Name inst (an InterfaceRef node), the emitter:

  1. Calls find_interface(program, name) to look up the corresponding InterfaceDecl
  2. Emits the block with:
    • The InterfaceDecl's field list
    • The original is_in flag (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).

Example

axsl
interface Varyings {
    vec2 v_uv;
    vec3 v_normal;
};

@stage vertex {
    out Varyings v;
}

Emits:

glsl
out Varyings {
    vec2 v_uv;
    vec3 v_normal;
} v;

Parenthesization

emit_expr_paren(id) adds parentheses only when the sub-expression is a BinaryExpr or TernaryExpr.

Other expression types (identifiers, calls, field access, subscripts) are emitted without wrapping parentheses.

This produces clean output while ensuring binary operators nested inside other binary expressions are fully parenthesized.

Example

axsl
a + b * c

Emits:

glsl
a + (b * c)
axsl
func(a + b)

Emits:

glsl
func((a + b))

Float Literal Format

Float values are emitted via snprintf with the %g format specifier.

%g removes trailing zeros and the trailing decimal point when no fractional part is present, producing compact output:

  • 1.01
  • 3.141593.14159
  • 0.50.5

TIP

While GLSL technically requires 1.0 for float literals, most drivers accept 1. or 1 when the context is clear. AXSLC uses %g for readability.

Dead Code Elimination

The emitter only includes global uniforms in a stage's output if:

  1. The uniform name appears in used_uniforms (computed by the linker)
  2. The uniform is not shadowed by a stage-local uniform

This prevents:

  • Unused uniform warnings from the GLSL compiler
  • Unnecessary uniform slots in the compiled shader
  • Reduced shader binary size

Example

axsl
uniform float u_vertex_only;  // only used in vertex
uniform float u_fragment_only;  // only used in fragment

@stage vertex {
    void main() {
        float x = u_vertex_only;
    }
}

@stage fragment {
    void main() {
        float y = u_fragment_only;
    }
}

Vertex output:

glsl
uniform float u_vertex_only;  // present

Fragment output:

glsl
uniform float u_fragment_only;  // present

Each stage only includes the uniforms it actually uses.

Complete Output Example

Input (AXSL):

axsl
@version 450;

struct Material {
    vec3 albedo;
};

const float PI = 3.14159;

uniform mat4 u_mvp;  // used in vertex only

@stage vertex {
    in Attributes {
        @location(0) vec3 a_pos;
    } a;

    void main() {
        gl_Position = u_mvp * vec4(a.a_pos, 1.0);
    }
}

Output (out.vert.glsl):

glsl
#version 450 core

struct Material {
    vec3 albedo;
};
const float PI = 3.14159;

uniform mat4 u_mvp;

in Attributes {
    layout(location = 0) vec3 a_pos;
} a;

void main() {
    gl_Position = (u_mvp * vec4(a.a_pos, 1));
}

Note:

  • Version header is first
  • Global struct and constant are included
  • Global uniform u_mvp is included (used in main)
  • Stage-local in block is emitted with location qualifiers
  • Float literal 1.0 is emitted as 1 (using %g)