"""CrossGL-to-GLSL code generator."""
from ..ast import (
AssignmentNode,
ArrayNode,
ArrayAccessNode,
BinaryOpNode,
BreakNode,
ContinueNode,
ForInNode,
ForNode,
FunctionCallNode,
IfNode,
LiteralPatternNode,
LoopNode,
MatchNode,
MemberAccessNode,
MeshOpNode,
PreprocessorNode,
RayQueryOpNode,
RayTracingOpNode,
RangeNode,
ReturnNode,
StructNode,
SwitchNode,
TernaryOpNode,
UnaryOpNode,
VariableNode,
WaveOpNode,
WhileNode,
WildcardPatternNode,
)
from .array_utils import (
parse_array_type,
format_array_type,
format_c_style_array_declaration,
split_array_type_suffix,
get_array_size_from_node,
evaluate_literal_int_expression,
collect_literal_int_constants,
collect_struct_member_types,
)
from ..validation import (
collect_cbuffer_declaration_name_conflicts,
collect_cbuffer_member_global_conflicts,
collect_duplicate_cbuffer_member_names,
collect_duplicate_cbuffer_names,
collect_non_resource_global_resource_shadows,
expression_debug_name,
floating_coordinate_dimension,
integer_coordinate_dimension,
is_floating_scalar_type,
is_integer_scalar_type,
is_numeric_scalar_type,
IMAGE_RESOURCE_INTRINSIC_NAMES,
INTEGER_COORDINATE_INTRINSIC_NAMES,
OFFSET_DIMENSION_INTRINSIC_NAMES,
texture_bias_argument_index,
texture_compare_argument_index,
texture_gather_component_argument_index,
texture_gradient_argument_indices,
texture_intrinsic_allowed_argument_counts,
texture_intrinsic_max_argument_count,
texture_intrinsic_min_argument_count,
texture_lod_argument_index,
texture_mip_level_argument_index,
texture_offset_argument_indices,
texture_query_lod_coordinate_argument_index,
texture_sample_index_argument_index,
)
from .stage_utils import (
compute_local_size,
normalize_stage_name,
should_emit_qualified_function,
stage_matches,
)
from .resource_arrays import collect_resource_array_size_hints
[docs]
class GLSLCodeGen:
"""Emit GLSL source from the shared CrossGL translator AST."""
def __init__(self):
"""Initialize GLSL type maps and per-generation stage/resource state."""
self.sampler_variables = set()
self.current_sampler_parameters = set()
self.texture_variable_types = {}
self.current_texture_parameters = {}
self.image_variable_formats = {}
self.current_image_format_parameters = {}
self.function_sampler_parameter_indices = {}
self.resource_array_size_hints = {}
self.function_resource_array_size_hints = {}
self.literal_int_constants = {}
self.current_stage_output = None
self.current_stage_inputs = {}
self.current_stage_outputs = {}
self.flattened_stage_variables = set()
self.structs_by_name = {}
self.vertex_input_struct_names = set()
self.vertex_output_struct_names = set()
self.fragment_input_struct_names = set()
self.vertex_input_member_names = set()
self.current_function_return_type = None
self.current_expression_expected_type = None
self.local_variable_types = {}
self.struct_member_types = {}
self.semantic_map = {
"gl_VertexID": "gl_VertexID",
"gl_InstanceID": "gl_InstanceID",
"gl_IsFrontFace": "gl_FrontFacing",
"gl_PrimitiveID": "gl_PrimitiveID",
"POSITION": "layout(location = 0)",
"NORMAL": "layout(location = 1)",
"TANGENT": "layout(location = 2)",
"BINORMAL": "layout(location = 3)",
"TEXCOORD": "layout(location = 4)",
"TEXCOORD0": "layout(location = 5)",
"TEXCOORD1": "layout(location = 6)",
"TEXCOORD2": "layout(location = 7)",
"TEXCOORD3": "layout(location = 8)",
"TEXCOORD4": "layout(location = 9)",
"TEXCOORD5": "layout(location = 10)",
"TEXCOORD6": "layout(location = 11)",
"TEXCOORD7": "layout(location = 12)",
# Vertex outputs
"gl_Position": "gl_Position",
"gl_PointSize": "gl_PointSize",
"gl_ClipDistance": "gl_ClipDistance",
# Fragment outputs
"gl_FragColor": "layout(location = 0)",
"gl_FragColor1": "layout(location = 1)",
"gl_FragColor2": "layout(location = 2)",
"gl_FragColor3": "layout(location = 3)",
"gl_FragColor4": "layout(location = 4)",
"gl_FragColor5": "layout(location = 5)",
"gl_FragColor6": "layout(location = 6)",
"gl_FragColor7": "layout(location = 7)",
"gl_FragDepth": "gl_FragDepth",
# Additional fragment inputs
"gl_FragCoord": "gl_FragCoord",
"gl_FrontFacing": "gl_FrontFacing",
"gl_PointCoord": "gl_PointCoord",
# Compute shader specific
"gl_GlobalInvocationID": "gl_GlobalInvocationID",
"gl_LocalInvocationID": "gl_LocalInvocationID",
"gl_WorkGroupID": "gl_WorkGroupID",
"gl_LocalInvocationIndex": "gl_LocalInvocationIndex",
"gl_WorkGroupSize": "gl_WorkGroupSize",
"gl_NumWorkGroups": "gl_NumWorkGroups",
}
self.type_mapping = {
# Most types are the same in CrossGL and GLSL
"vec2": "vec2",
"vec3": "vec3",
"vec4": "vec4",
"ivec2": "ivec2",
"ivec3": "ivec3",
"ivec4": "ivec4",
"mat2": "mat2",
"mat3": "mat3",
"mat4": "mat4",
"float": "float",
"int": "int",
"uint": "uint",
"bool": "bool",
"double": "double",
"void": "void",
"sampler": "sampler",
"sampler1D": "sampler1D",
"sampler2D": "sampler2D",
"sampler3D": "sampler3D",
"samplerCube": "samplerCube",
"sampler2DArray": "sampler2DArray",
"samplerCubeArray": "samplerCubeArray",
"sampler2DMS": "sampler2DMS",
"sampler2DMSArray": "sampler2DMSArray",
"sampler2DShadow": "sampler2DShadow",
"sampler2DArrayShadow": "sampler2DArrayShadow",
"samplerCubeShadow": "samplerCubeShadow",
"samplerCubeArrayShadow": "samplerCubeArrayShadow",
"iimage2D": "iimage2D",
"iimage3D": "iimage3D",
"iimage2DArray": "iimage2DArray",
"uimage2D": "uimage2D",
"uimage3D": "uimage3D",
"uimage2DArray": "uimage2DArray",
"image2D": "image2D",
"image3D": "image3D",
"imageCube": "imageCube",
"image2DArray": "image2DArray",
}
self.function_map = {
"atan2": "atan",
"lerp": "mix",
"frac": "fract",
"saturate": "clamp",
"tex2D": "texture",
"tex2Dproj": "textureProj",
"tex2Dlod": "textureLod",
"tex2Dbias": "texture",
"tex2Dgrad": "textureGrad",
"tex2Doffset": "textureOffset",
"texCUBE": "texture",
"texCUBElod": "textureLod",
"texCUBEbias": "texture",
"texCUBEgrad": "textureGrad",
"textureOffset": "textureOffset",
"textureProj": "textureProj",
"textureGatherOffset": "textureGatherOffset",
"textureGatherOffsets": "textureGatherOffsets",
"textureQueryLevels": "textureQueryLevels",
"textureQueryLod": "textureQueryLod",
"texelFetch": "texelFetch",
"imageAtomicAdd": "imageAtomicAdd",
"imageAtomicMin": "imageAtomicMin",
"imageAtomicMax": "imageAtomicMax",
"imageAtomicAnd": "imageAtomicAnd",
"imageAtomicOr": "imageAtomicOr",
"imageAtomicXor": "imageAtomicXor",
"imageAtomicExchange": "imageAtomicExchange",
"imageAtomicCompSwap": "imageAtomicCompSwap",
"atomicCounterIncrement": "atomicCounterIncrement",
"atomicCounterDecrement": "atomicCounterDecrement",
"atomicCounter": "atomicCounter",
"atomicCounterAdd": "atomicCounterAdd",
"mul": "*", # Matrix multiplication
"ddx": "dFdx",
"ddy": "dFdy",
"rsqrt": "inversesqrt",
"sincos": "sin_cos", # Custom function needed
"clip": "discard", # HLSL clip becomes GLSL discard
"log2": "log2",
"exp2": "exp2",
"pow": "pow",
"sqrt": "sqrt",
"abs": "abs",
"sign": "sign",
"floor": "floor",
"ceil": "ceil",
"round": "round",
"fmod": "mod",
"trunc": "trunc",
"min": "min",
"max": "max",
"clamp": "clamp",
"step": "step",
"smoothstep": "smoothstep",
"length": "length",
"distance": "distance",
"dot": "dot",
"cross": "cross",
"normalize": "normalize",
"reflect": "reflect",
"refract": "refract",
"all": "all",
"any": "any",
"sin": "sin",
"cos": "cos",
"tan": "tan",
"asin": "asin",
"acos": "acos",
"atan": "atan",
"sinh": "sinh",
"cosh": "cosh",
"tanh": "tanh",
}
[docs]
def generate(self, ast):
"""Generate complete GLSL source for a CrossGL AST."""
return self.generate_program(ast)
def generate_stage(self, ast, shader_type):
"""Generate GLSL source for a single requested shader stage."""
return self.generate_program(ast, target_stage=shader_type)
def generate_program(self, ast, target_stage=None):
"""Render an AST to GLSL, optionally filtering stage entry points."""
target_stage = normalize_stage_name(target_stage)
self.sampler_variables = set()
self.current_sampler_parameters = set()
self.texture_variable_types = {}
self.current_texture_parameters = {}
self.image_variable_formats = {}
self.current_image_format_parameters = {}
self.function_sampler_parameter_indices = (
self.collect_function_sampler_parameter_indices(ast)
)
self.literal_int_constants = collect_literal_int_constants(
getattr(ast, "constants", [])
)
self.current_stage_output = None
self.current_stage_inputs = {}
self.current_stage_outputs = {}
self.flattened_stage_variables = set()
self.current_function_return_type = None
self.current_expression_expected_type = None
self.local_variable_types = {}
self.struct_member_types = collect_struct_member_types(
getattr(ast, "structs", []), self.type_name_string
)
(
self.resource_array_size_hints,
self.function_resource_array_size_hints,
) = self.collect_resource_array_size_hints(ast)
self.validate_global_resource_shadows(ast)
code = "\n"
preprocessors = getattr(ast, "preprocessors", []) or []
version_line = None
extra_lines = []
for directive in preprocessors:
if isinstance(directive, PreprocessorNode):
if directive.directive == "precision":
line = (
f"precision {directive.content};"
if directive.content
else "precision;"
)
else:
line = f"#{directive.directive} {directive.content}".strip()
else:
line = str(directive).strip()
if line.startswith("#version") and version_line is None:
version_line = line
elif line:
extra_lines.append(line)
if version_line is None:
version_line = "#version 450 core"
code += f"{version_line}\n"
if extra_lines:
code += "\n".join(extra_lines) + "\n"
code += self.generate_constants(ast)
structs = getattr(ast, "structs", [])
self.structs_by_name = {
node.name: node for node in structs if isinstance(node, StructNode)
}
self.vertex_input_struct_names = self.stage_parameter_struct_names(
ast, "vertex"
)
self.vertex_output_struct_names = self.stage_return_struct_names(ast, "vertex")
self.fragment_input_struct_names = self.stage_parameter_struct_names(
ast, "fragment"
)
self.vertex_input_member_names = self.struct_member_names(
self.vertex_input_struct_names
)
emit_vertex_io = target_stage in {None, "vertex"}
emit_fragment_io = target_stage in {None, "fragment"}
emit_graphics_io = target_stage in {None, "vertex", "fragment"}
for node in structs:
if isinstance(node, StructNode):
if (
node.name == "VSInput"
or node.name in self.vertex_input_struct_names
):
if emit_vertex_io:
code += self.generate_stage_input_declarations(node)
elif not emit_graphics_io:
code += self.generate_struct(node)
elif node.name == "VSOutput":
emitted_io = False
if node.name in self.vertex_output_struct_names and emit_vertex_io:
code += self.generate_vertex_output_declarations(node)
emitted_io = True
if (
node.name in self.fragment_input_struct_names
and emit_fragment_io
):
code += self.generate_fragment_input_declarations(node)
emitted_io = True
if not emitted_io:
if emit_graphics_io:
code += self.generate_legacy_output_declarations(node)
else:
code += self.generate_struct(node)
elif node.name in self.vertex_output_struct_names:
if emit_vertex_io:
code += self.generate_vertex_output_declarations(node)
if (
node.name in self.fragment_input_struct_names
and emit_fragment_io
):
code += self.generate_fragment_input_declarations(node)
code += self.generate_struct(node)
elif node.name == "PSInput":
if emit_fragment_io:
code += self.generate_fragment_input_declarations(node)
elif not emit_graphics_io:
code += self.generate_struct(node)
elif node.name in self.fragment_input_struct_names:
if emit_fragment_io:
code += self.generate_fragment_input_declarations(node)
if not emit_graphics_io:
code += self.generate_struct(node)
elif node.name == "PSOutput":
if emit_graphics_io:
members = getattr(node, "members", [])
for member in members:
if hasattr(member, "member_type"):
member_type = self.map_type(member.member_type)
else:
member_type = self.map_type(
getattr(member, "vtype", "float")
)
# Handle semantic
semantic = None
if hasattr(member, "semantic"):
semantic = member.semantic
elif hasattr(member, "attributes"):
for attr in member.attributes:
if hasattr(attr, "name"):
semantic = attr.name
break
code += f"{self.map_semantic(semantic)} out {member_type} {member.name};\n"
else:
code += self.generate_struct(node)
else:
code += self.generate_struct(node)
global_vars = getattr(ast, "global_variables", [])
binding = 0
for index, node in enumerate(global_vars):
# Handle both old and new AST variable structures
resource_count = 1
if hasattr(node, "var_type"):
if hasattr(node.var_type, "name") or hasattr(
node.var_type, "element_type"
):
# Check if it's an ArrayType and handle specially for global variables
if (
hasattr(node.var_type, "element_type")
and str(type(node.var_type)).find("ArrayType") != -1
): # ArrayType
base_type = self.convert_type_node_to_string(
node.var_type.element_type
)
array_size = (
self.generate_expression(node.var_type.size)
if node.var_type.size
else (
self.resource_array_size_hints.get(node.name, "")
if self.is_inferable_resource_array_type(base_type)
else ""
)
)
vtype = base_type
array_suffix = f"[{array_size}]" if array_size else "[]"
resource_count = self.resource_array_count(
node.var_type.size if node.var_type.size else array_size
)
else:
# Use the proper type conversion for TypeNode objects
vtype = self.convert_type_node_to_string(node.var_type)
array_suffix = ""
else:
vtype = str(node.var_type)
array_suffix = ""
elif hasattr(node, "vtype"):
vtype = node.vtype
array_suffix = ""
else:
vtype = "float"
array_suffix = ""
if hasattr(node, "name"):
var_name = node.name
elif hasattr(node, "variable_name"):
var_name = node.variable_name
else:
var_name = f"var{index}"
mapped_type = self.map_resource_type_with_format(vtype, node)
if mapped_type == "sampler":
self.sampler_variables.add(var_name)
continue
if self.is_opaque_resource_type(mapped_type):
self.texture_variable_types[var_name] = mapped_type
explicit_format = self.explicit_image_format_qualifier(node)
if explicit_format:
self.image_variable_formats[var_name] = explicit_format
declaration = format_c_style_array_declaration(
f"{mapped_type}{array_suffix}", var_name
)
if self.is_opaque_resource_type(mapped_type):
layout = self.opaque_resource_layout(mapped_type, binding, node)
code += f"{layout} uniform {declaration};\n"
else:
code += f"layout(std140, binding = {binding}) {declaration};\n"
binding += (
resource_count if self.is_opaque_resource_type(mapped_type) else 1
)
cbuffers = getattr(ast, "cbuffers", [])
if cbuffers:
code += "// Constant Buffers\n"
code += self.generate_cbuffers(ast)
functions = getattr(ast, "functions", [])
for func in functions:
# Handle both old and new AST function structures
if hasattr(func, "qualifiers") and func.qualifiers:
qualifier = func.qualifiers[0] if func.qualifiers else None
else:
qualifier = getattr(func, "qualifier", None)
qualifier_name = normalize_stage_name(qualifier)
if not should_emit_qualified_function(target_stage, qualifier_name):
continue
if qualifier_name == "vertex":
code += "// Vertex Shader\n"
code += self.generate_function(func, shader_type="vertex")
elif qualifier_name == "fragment":
code += "// Fragment Shader\n"
code += self.generate_function(func, shader_type="fragment")
elif qualifier_name == "compute":
code += "// Compute Shader\n"
code += self.generate_function(func, shader_type="compute")
else:
code += self.generate_function(func)
# Handle shader stages (new AST structure)
if hasattr(ast, "stages") and ast.stages:
for stage_type, stage in ast.stages.items():
if hasattr(stage, "entry_point"):
stage_name = normalize_stage_name(stage_type)
if not stage_matches(target_stage, stage_name):
continue
code += f"// {stage_name.title()} Shader\n"
code += self.generate_function(
stage.entry_point,
shader_type=stage_name,
execution_config=getattr(stage, "execution_config", None),
)
if hasattr(stage, "local_functions"):
stage_name = normalize_stage_name(stage_type)
if not stage_matches(target_stage, stage_name):
continue
for func in stage.local_functions:
code += self.generate_function(func)
return code
def generate_constants(self, ast):
code = ""
for node in getattr(ast, "constants", []) or []:
name = getattr(node, "name", None)
if not name:
continue
const_type = getattr(node, "const_type", getattr(node, "vtype", "float"))
value = getattr(node, "value", None)
value_code = self.generate_constant_expression(value)
code += f"const {self.map_type(const_type)} {name} = {value_code};\n"
return f"{code}\n" if code else ""
def generate_constant_expression(self, expr):
value_code = self.generate_expression(expr)
if value_code == "True":
return "true"
if value_code == "False":
return "false"
return value_code
def generate_cbuffers(self, ast):
code = ""
cbuffers = getattr(ast, "cbuffers", [])
duplicate_names = collect_duplicate_cbuffer_names(cbuffers)
if duplicate_names:
names = ", ".join(sorted(duplicate_names))
raise ValueError(f"Duplicate cbuffer name(s) in OpenGL output: {names}")
declaration_conflicts = collect_cbuffer_declaration_name_conflicts(ast)
if declaration_conflicts:
names = ", ".join(sorted(declaration_conflicts))
raise ValueError(
"Cbuffer name(s) conflict with existing OpenGL declaration(s): "
f"{names}"
)
duplicate_members = collect_duplicate_cbuffer_member_names(cbuffers)
if duplicate_members:
names = ", ".join(sorted(duplicate_members))
raise ValueError(
f"Ambiguous cbuffer member name(s) in OpenGL output: {names}"
)
global_member_conflicts = collect_cbuffer_member_global_conflicts(ast)
if global_member_conflicts:
names = ", ".join(sorted(global_member_conflicts))
raise ValueError(
"Cbuffer member name(s) conflict with OpenGL global declaration(s): "
f"{names}"
)
for i, node in enumerate(cbuffers):
if isinstance(node, StructNode):
code += f"layout(std140, binding = {i}) uniform {node.name} {{\n"
members = getattr(node, "members", [])
for member in members:
if isinstance(member, ArrayNode):
element_type = getattr(
member, "element_type", getattr(member, "vtype", "float")
)
if member.size:
code += f" {self.map_type(element_type)} {member.name}[{member.size}];\n"
else:
# Dynamic arrays in uniform blocks need special handling in GLSL
code += (
f" {self.map_type(element_type)} {member.name}[];\n"
)
else:
if hasattr(member, "member_type"):
member_type = self.map_type(member.member_type)
else:
member_type = self.map_type(
getattr(member, "vtype", "float")
)
declaration = format_c_style_array_declaration(
member_type, member.name
)
code += f" {declaration};\n"
code += "};\n"
elif hasattr(node, "name") and hasattr(
node, "members"
): # CbufferNode handling
code += f"layout(std140, binding = {i}) uniform {node.name} {{\n"
for member in node.members:
if isinstance(member, ArrayNode):
element_type = getattr(
member, "element_type", getattr(member, "vtype", "float")
)
if member.size:
code += f" {self.map_type(element_type)} {member.name}[{member.size}];\n"
else:
# Dynamic arrays in uniform blocks need special handling in GLSL
code += (
f" {self.map_type(element_type)} {member.name}[];\n"
)
else:
if hasattr(member, "member_type"):
member_type = self.map_type(member.member_type)
else:
member_type = self.map_type(
getattr(member, "vtype", "float")
)
declaration = format_c_style_array_declaration(
member_type, member.name
)
code += f" {declaration};\n"
code += "};\n"
return code
def generate_compute_layout(self, execution_config=None):
x, y, z = compute_local_size(execution_config)
return (
f"layout(local_size_x = {x}, "
f"local_size_y = {y}, "
f"local_size_z = {z}) in;\n"
)
def generate_function(
self, func, indent=0, shader_type=None, execution_config=None
):
"""Render a function or GLSL ``main`` stage entry point."""
code = ""
code += " " * indent
param_list = getattr(func, "parameters", getattr(func, "params", []))
params = []
sampler_parameters = set()
texture_parameters = {}
image_format_parameters = {}
previous_function_return_type = self.current_function_return_type
previous_local_variable_types = self.local_variable_types
self.local_variable_types = {}
for p in param_list:
if hasattr(p, "param_type"):
if hasattr(p.param_type, "name"):
raw_param_type = p.param_type.name
else:
raw_param_type = p.param_type
elif hasattr(p, "vtype"):
raw_param_type = p.vtype
else:
raw_param_type = "float"
self.local_variable_types[p.name] = self.type_name_string(raw_param_type)
if self.is_sampler_type(raw_param_type):
sampler_parameters.add(p.name)
continue
param_type = self.map_resource_parameter_type_with_hint(
raw_param_type, p, getattr(func, "name", None)
)
if self.is_opaque_resource_type(
self.map_resource_type_with_format(
self.resource_base_type(raw_param_type), p
)
):
texture_parameters[p.name] = self.map_resource_type_with_format(
self.resource_base_type(raw_param_type), p
)
explicit_format = self.explicit_image_format_qualifier(p)
if explicit_format:
image_format_parameters[p.name] = explicit_format
semantic = self.semantic_from_node(p)
declaration = format_c_style_array_declaration(param_type, p.name)
semantic_attr = self.map_semantic(semantic)
params.append(
f"{declaration} {semantic_attr}" if semantic_attr else declaration
)
params_str = ", ".join(params)
stage_entry_types = {
"vertex",
"fragment",
"compute",
"geometry",
"tessellation_control",
"tessellation_evaluation",
"mesh",
"task",
"amplification",
"object",
"ray_generation",
"ray_intersection",
"ray_closest_hit",
"ray_any_hit",
"ray_miss",
"ray_callable",
}
stage_output = self.fragment_stage_output(func, shader_type)
if stage_output and stage_output["declaration"]:
code += f"{stage_output['declaration']}\n"
if shader_type == "compute":
code += self.generate_compute_layout(execution_config)
if shader_type in stage_entry_types:
code += "void main() {\n"
self.current_function_return_type = "void"
else:
raw_return_type = self.type_name_string(getattr(func, "return_type", None))
self.current_function_return_type = raw_return_type or "void"
return_type = self.map_type(self.current_function_return_type)
code += f"{return_type} {func.name}({params_str}) {{\n"
previous_sampler_parameters = self.current_sampler_parameters
previous_texture_parameters = self.current_texture_parameters
previous_image_format_parameters = self.current_image_format_parameters
previous_stage_output = self.current_stage_output
previous_stage_inputs = self.current_stage_inputs
previous_stage_outputs = self.current_stage_outputs
previous_flattened_stage_variables = self.flattened_stage_variables
self.current_sampler_parameters = sampler_parameters
self.current_texture_parameters = texture_parameters
self.current_image_format_parameters = image_format_parameters
self.current_stage_output = stage_output
self.current_stage_inputs = self.stage_input_member_maps(func, shader_type)
self.current_stage_outputs = self.stage_output_member_maps(func, shader_type)
self.flattened_stage_variables = set(self.current_stage_outputs)
body = getattr(func, "body", [])
if hasattr(body, "statements"):
for stmt in body.statements:
code += self.generate_statement(stmt, 1)
elif isinstance(body, list):
for stmt in body:
code += self.generate_statement(stmt, 1)
self.current_sampler_parameters = previous_sampler_parameters
self.current_texture_parameters = previous_texture_parameters
self.current_image_format_parameters = previous_image_format_parameters
self.current_stage_output = previous_stage_output
self.current_stage_inputs = previous_stage_inputs
self.current_stage_outputs = previous_stage_outputs
self.flattened_stage_variables = previous_flattened_stage_variables
self.current_function_return_type = previous_function_return_type
self.local_variable_types = previous_local_variable_types
code += "}\n\n"
return code
def stage_functions(self, ast, stage_name):
functions = []
for func in getattr(ast, "functions", []) or []:
qualifiers = getattr(func, "qualifiers", []) or []
qualifier = (
qualifiers[0] if qualifiers else getattr(func, "qualifier", None)
)
if normalize_stage_name(qualifier) == stage_name:
functions.append(func)
for stage_type, stage in getattr(ast, "stages", {}).items():
current_stage = normalize_stage_name(stage_type)
if current_stage == stage_name and hasattr(stage, "entry_point"):
functions.append(stage.entry_point)
return functions
def stage_parameter_struct_names(self, ast, stage_name):
struct_names = set()
for func in self.stage_functions(ast, stage_name):
parameters = getattr(func, "parameters", getattr(func, "params", [])) or []
for param in parameters:
type_name = self.type_node_name(getattr(param, "param_type", None))
if type_name in self.structs_by_name:
struct_names.add(type_name)
return struct_names
def stage_return_struct_names(self, ast, stage_name):
struct_names = set()
for func in self.stage_functions(ast, stage_name):
type_name = self.type_node_name(getattr(func, "return_type", None))
if type_name in self.structs_by_name:
struct_names.add(type_name)
return struct_names
def struct_member_names(self, struct_names):
names = set()
for struct_name in struct_names:
struct = self.structs_by_name.get(struct_name)
for member in getattr(struct, "members", []) or []:
names.add(member.name)
return names
def type_node_name(self, type_node):
if type_node is None:
return None
if hasattr(type_node, "name"):
return type_node.name
return str(type_node)
def generate_stage_input_declarations(self, node):
code = ""
for member in getattr(node, "members", []) or []:
member_type = self.member_type_name(member)
semantic = self.semantic_from_node(member)
layout = self.map_semantic(semantic)
prefix = f"{layout} " if layout.startswith("layout(") else ""
code += f"{prefix}in {member_type} {member.name};\n"
return code
def generate_legacy_output_declarations(self, node):
code = ""
for member in getattr(node, "members", []) or []:
code += f"out {self.member_type_name(member)} {member.name};\n"
return code
def generate_vertex_output_declarations(self, node):
code = ""
for member in getattr(node, "members", []) or []:
output_name = self.vertex_output_member_name(member)
if self.is_vertex_builtin_output(output_name):
continue
semantic = self.semantic_from_node(member)
layout = self.map_semantic(semantic)
prefix = f"{layout} " if layout.startswith("layout(") else ""
code += f"{prefix}out {self.member_type_name(member)} {output_name};\n"
return code
def generate_fragment_input_declarations(self, node):
code = ""
for member in getattr(node, "members", []) or []:
input_name = self.fragment_input_member_name(member, node.name)
if input_name is None:
continue
semantic = self.semantic_from_node(member)
layout = self.map_semantic(semantic)
prefix = f"{layout} " if layout.startswith("layout(") else ""
code += f"{prefix}in {self.member_type_name(member)} {input_name};\n"
return code
def member_type_name(self, member):
if hasattr(member, "member_type"):
return self.map_type(member.member_type)
return self.map_type(getattr(member, "vtype", "float"))
def vertex_output_member_name(self, member):
semantic = self.semantic_from_node(member)
mapped_semantic = self.map_semantic(semantic)
if self.is_vertex_builtin_output(mapped_semantic):
return mapped_semantic
if member.name in self.vertex_input_member_names:
return f"out_{member.name}"
return member.name
def fragment_input_member_name(self, member, struct_name):
if struct_name in self.vertex_output_struct_names:
output_name = self.vertex_output_member_name(member)
if self.is_vertex_builtin_output(output_name):
return None
return output_name
return member.name
def is_vertex_builtin_output(self, name):
return name in {"gl_Position", "gl_PointSize", "gl_ClipDistance"}
def stage_input_member_maps(self, func, shader_type):
if shader_type not in {"vertex", "fragment"}:
return {}
maps = {}
for param in getattr(func, "parameters", getattr(func, "params", [])) or []:
type_name = self.type_node_name(getattr(param, "param_type", None))
struct = self.structs_by_name.get(type_name)
if struct is None:
continue
if shader_type == "fragment":
member_map = {}
for member in getattr(struct, "members", []) or []:
input_name = self.fragment_input_member_name(member, type_name)
if input_name is not None:
member_map[member.name] = input_name
maps[param.name] = member_map
else:
maps[param.name] = {
member.name: member.name
for member in getattr(struct, "members", [])
}
return maps
def stage_output_member_maps(self, func, shader_type):
if shader_type != "vertex":
return {}
type_name = self.type_node_name(getattr(func, "return_type", None))
struct = self.structs_by_name.get(type_name)
if struct is None:
return {}
member_map = {
member.name: self.vertex_output_member_name(member)
for member in getattr(struct, "members", []) or []
}
maps = {}
body = getattr(func, "body", [])
statements = getattr(body, "statements", body if isinstance(body, list) else [])
for stmt in statements:
if not isinstance(stmt, VariableNode):
continue
if self.type_node_name(getattr(stmt, "var_type", None)) == type_name:
maps[stmt.name] = member_map
return maps
def function_return_type(self, func):
return_type = getattr(func, "return_type", None)
if return_type is None:
return "void"
return self.map_type(return_type)
def fragment_stage_output(self, func, shader_type):
if shader_type != "fragment":
return None
output_type = self.function_return_type(func)
if output_type == "void":
return None
semantic = self.semantic_from_node(func) or "gl_FragColor"
if semantic == "gl_FragDepth":
return {
"name": "gl_FragDepth",
"declaration": "",
}
layout = self.map_semantic(semantic)
if not layout.startswith("layout("):
layout = "layout(location = 0)"
output_name = self.fragment_output_name(semantic)
return {
"name": output_name,
"declaration": f"{layout} out {output_type} {output_name};",
}
def fragment_output_name(self, semantic):
if semantic and semantic.startswith("gl_FragColor"):
suffix = semantic[len("gl_FragColor") :]
return f"fragColor{suffix}"
return "fragColor"
def generate_statement(self, stmt, indent=0):
"""Render a single CrossGL AST statement as GLSL source."""
indent_str = " " * indent
if isinstance(stmt, VariableNode):
if stmt.name in self.flattened_stage_variables:
return ""
if hasattr(stmt, "var_type"):
var_type = self.convert_type_node_to_string(stmt.var_type)
elif hasattr(stmt, "vtype"):
var_type = stmt.vtype
else:
var_type = "float"
self.local_variable_types[stmt.name] = var_type
declaration = format_c_style_array_declaration(
self.map_type(var_type), stmt.name
)
declaration = f"{self.local_variable_qualifier(stmt)}{declaration}"
if hasattr(stmt, "initial_value") and stmt.initial_value is not None:
init_expr = self.generate_expression_with_expected(
stmt.initial_value, var_type
)
return f"{indent_str}{declaration} = {init_expr};\n"
else:
return f"{indent_str}{declaration};\n"
elif isinstance(stmt, ArrayNode):
return self.generate_array_declaration(stmt, indent)
elif isinstance(stmt, AssignmentNode):
return f"{indent_str}{self.generate_assignment(stmt)};\n"
elif isinstance(stmt, BreakNode):
return f"{indent_str}break;\n"
elif isinstance(stmt, ContinueNode):
return f"{indent_str}continue;\n"
elif isinstance(stmt, IfNode):
return self.generate_if(stmt, indent)
elif isinstance(stmt, ForNode):
return self.generate_for(stmt, indent)
elif isinstance(stmt, ForInNode):
return self.generate_for_in(stmt, indent)
elif isinstance(stmt, WhileNode):
return self.generate_while(stmt, indent)
elif isinstance(stmt, LoopNode):
return self.generate_loop(stmt, indent)
elif isinstance(stmt, SwitchNode):
return self.generate_switch(stmt, indent)
elif isinstance(stmt, MatchNode):
return self.generate_match(stmt, indent)
elif isinstance(stmt, ReturnNode):
if getattr(stmt, "value", None) is None:
return f"{indent_str}return;\n"
return_value_name = self.expression_name(stmt.value)
if return_value_name in self.flattened_stage_variables:
return f"{indent_str}return;\n"
if self.current_stage_output is not None:
if isinstance(stmt.value, list):
values = ", ".join(
self.generate_expression(val) for val in stmt.value
)
value = values
else:
value = self.generate_expression_with_expected(
stmt.value, self.current_function_return_type
)
return (
f"{indent_str}{self.current_stage_output['name']} = {value};\n"
f"{indent_str}return;\n"
)
if isinstance(stmt.value, list):
# Multiple return values
values = ", ".join(self.generate_expression(val) for val in stmt.value)
return f"{indent_str}return {values};\n"
else:
return (
f"{indent_str}return "
f"{self.generate_expression_with_expected(stmt.value, self.current_function_return_type)};\n"
)
elif hasattr(stmt, "__class__") and "ExpressionStatementNode" in str(
type(stmt)
):
# Handle ExpressionStatementNode
expr_code = self.generate_expression_statement(stmt)
return f"{indent_str}{expr_code};\n"
else:
# Handle expressions that may be used as statements
expr_result = self.generate_expression(stmt)
if expr_result.strip():
return f"{indent_str}{expr_result};\n"
else:
return f"{indent_str}// Unhandled statement: {type(stmt).__name__}\n"
def local_variable_qualifier(self, node):
return "const " if "const" in getattr(node, "qualifiers", []) else ""
def type_name_string(self, vtype):
if vtype is None:
return None
if hasattr(vtype, "name") or hasattr(vtype, "element_type"):
return self.convert_type_node_to_string(vtype)
return str(vtype)
def generate_expression_with_expected(self, expr, expected_type):
previous_expected_type = self.current_expression_expected_type
self.current_expression_expected_type = self.type_name_string(expected_type)
try:
return self.generate_expression(expr)
finally:
self.current_expression_expected_type = previous_expected_type
def is_scalar_value_type(self, vtype):
vtype = self.type_name_string(vtype)
if not vtype:
return False
return self.map_type(vtype) in {
"float",
"double",
"int",
"uint",
"bool",
}
def is_vector_value_type(self, vtype):
vtype = self.type_name_string(vtype)
if not vtype:
return False
return self.map_type(vtype) in {
"vec2",
"vec3",
"vec4",
"dvec2",
"dvec3",
"dvec4",
"ivec2",
"ivec3",
"ivec4",
"uvec2",
"uvec3",
"uvec4",
"bvec2",
"bvec3",
"bvec4",
}
def vector_component_type(self, vtype):
mapped_type = self.map_type(vtype)
if mapped_type.startswith("dvec"):
return "double"
if mapped_type.startswith("uvec"):
return "uint"
if mapped_type.startswith("ivec"):
return "int"
if mapped_type.startswith("bvec"):
return "bool"
if mapped_type.startswith("vec"):
return "float"
return None
def expression_result_type(self, expr):
if expr is None:
return None
if isinstance(expr, VariableNode):
return self.local_variable_types.get(getattr(expr, "name", None))
if isinstance(expr, (int, float)):
return "float" if isinstance(expr, float) else "int"
if isinstance(expr, BinaryOpNode):
left_type = self.expression_result_type(expr.left)
right_type = self.expression_result_type(expr.right)
if self.is_vector_value_type(left_type):
return left_type
if self.is_vector_value_type(right_type):
return right_type
if left_type == "float" or right_type == "float":
return "float"
return left_type or right_type
if isinstance(expr, UnaryOpNode):
return self.expression_result_type(expr.operand)
if isinstance(expr, AssignmentNode):
target = getattr(expr, "target", getattr(expr, "left", None))
return self.expression_result_type(target)
if isinstance(expr, ArrayAccessNode):
array_type = self.type_name_string(self.expression_result_type(expr.array))
if array_type and "[" in array_type and "]" in array_type:
base_type, _ = split_array_type_suffix(array_type)
return base_type
return array_type
if isinstance(expr, MemberAccessNode):
object_type = self.expression_result_type(expr.object)
member = str(expr.member)
if object_type and all(ch in "xyzwrgba" for ch in member):
component_type = self.vector_component_type(object_type)
if component_type and len(member) == 1:
return component_type
if component_type:
return f"{component_type}{len(member)}"
if object_type:
member_type = self.struct_member_types.get(
self.type_name_string(object_type), {}
).get(member)
if member_type:
return member_type
member_types = {
self.type_name_string(members[member])
for members in self.struct_member_types.values()
if member in members
}
if len(member_types) == 1:
return next(iter(member_types))
return None
if isinstance(expr, FunctionCallNode):
func_expr = getattr(expr, "function", None) or getattr(expr, "name", None)
func_name = getattr(func_expr, "name", func_expr)
if func_name in {
"float",
"double",
"int",
"uint",
"bool",
"vec2",
"vec3",
"vec4",
"ivec2",
"ivec3",
"ivec4",
"uvec2",
"uvec3",
"uvec4",
"bvec2",
"bvec3",
"bvec4",
}:
return str(func_name)
if hasattr(expr, "__class__") and "Literal" in str(expr.__class__):
literal_type = getattr(getattr(expr, "literal_type", None), "name", None)
if literal_type:
return literal_type
if hasattr(expr, "__class__") and "Identifier" in str(expr.__class__):
return self.local_variable_types.get(getattr(expr, "name", None))
return None
def generate_assignment(self, node, is_main=False):
left_node = getattr(node, "target", getattr(node, "left", None))
right_node = getattr(node, "value", getattr(node, "right", None))
left = self.generate_expression(left_node)
right = self.generate_expression_with_expected(
right_node, self.expression_result_type(left_node)
)
op = self.map_operator(getattr(node, "operator", getattr(node, "op", "=")))
return f"{left} {op} {right}"
def generate_if(self, node, indent, is_main=False):
indent_str = " " * indent
condition = self.generate_expression(
node.condition if hasattr(node, "condition") else node.if_condition
)
code = f"{indent_str}if ({condition}) {{\n"
if_body = node.if_body
if hasattr(if_body, "statements"):
for stmt in if_body.statements:
code += self.generate_statement(stmt, indent + 1)
elif isinstance(if_body, list):
for stmt in if_body:
code += self.generate_statement(stmt, indent + 1)
code += f"{indent_str}}}"
if hasattr(node, "else_if_conditions") and node.else_if_conditions:
for else_if_condition, else_if_body in zip(
node.else_if_conditions, node.else_if_bodies
):
condition = self.generate_expression(else_if_condition)
code += f" else if ({condition}) {{\n"
if hasattr(else_if_body, "statements"):
for stmt in else_if_body.statements:
code += self.generate_statement(stmt, indent + 1)
elif isinstance(else_if_body, list):
for stmt in else_if_body:
code += self.generate_statement(stmt, indent + 1)
code += f"{indent_str}}}"
if hasattr(node, "else_body") and node.else_body:
code += f" else {{\n"
else_body = node.else_body
if hasattr(else_body, "statements"):
for stmt in else_body.statements:
code += self.generate_statement(stmt, indent + 1)
elif isinstance(else_body, list):
for stmt in else_body:
code += self.generate_statement(stmt, indent + 1)
code += f"{indent_str}}}"
code += "\n"
return code
def generate_for(self, node, indent, is_main=False):
indent_str = " " * indent
init = self.generate_for_initializer(getattr(node, "init", None))
condition = (
self.generate_expression(node.condition)
if getattr(node, "condition", None)
else ""
)
update = (
self.generate_expression(node.update)
if getattr(node, "update", None)
else ""
)
code = f"{indent_str}for ({init}; {condition}; {update}) {{\n"
body = node.body
if hasattr(body, "statements"):
for stmt in body.statements:
code += self.generate_statement(stmt, indent + 1)
elif isinstance(body, list):
for stmt in body:
code += self.generate_statement(stmt, indent + 1)
code += f"{indent_str}}}\n"
return code
def generate_for_in(self, node, indent):
indent_str = " " * indent
pattern = getattr(node, "pattern", "item")
iterable_node = getattr(node, "iterable", "")
if isinstance(iterable_node, RangeNode):
start = self.generate_expression(iterable_node.start)
end = self.generate_expression(iterable_node.end)
comparator = "<=" if iterable_node.inclusive else "<"
code = (
f"{indent_str}for (int {pattern} = {start}; "
f"{pattern} {comparator} {end}; ++{pattern}) {{\n"
)
else:
iterable = self.generate_expression(iterable_node)
code = (
f"{indent_str}for (int {pattern} = 0; {pattern} < {iterable}; "
f"++{pattern}) {{\n"
)
code += self.generate_statement_body(getattr(node, "body", []), indent + 1)
code += f"{indent_str}}}\n"
return code
def generate_while(self, node, indent):
indent_str = " " * indent
condition = self.generate_expression(getattr(node, "condition", ""))
code = f"{indent_str}while ({condition}) {{\n"
code += self.generate_statement_body(getattr(node, "body", []), indent + 1)
code += f"{indent_str}}}\n"
return code
def generate_loop(self, node, indent):
indent_str = " " * indent
code = f"{indent_str}while (true) {{\n"
code += self.generate_statement_body(getattr(node, "body", []), indent + 1)
code += f"{indent_str}}}\n"
return code
def generate_switch(self, node, indent):
indent_str = " " * indent
expression = self.generate_expression(getattr(node, "expression", ""))
code = f"{indent_str}switch ({expression}) {{\n"
for case in getattr(node, "cases", []) or []:
value = getattr(case, "value", None)
if value is None:
code += f"{indent_str} default:\n"
else:
code += f"{indent_str} case {self.generate_expression(value)}:\n"
code += self.generate_statement_body(
getattr(case, "statements", []), indent + 2
)
default_case = getattr(node, "default_case", None)
if default_case is not None:
code += f"{indent_str} default:\n"
code += self.generate_statement_body(default_case, indent + 2)
code += f"{indent_str}}}\n"
return code
def generate_match(self, node, indent):
indent_str = " " * indent
expression = self.generate_expression(getattr(node, "expression", ""))
code = f"{indent_str}switch ({expression}) {{\n"
for arm in getattr(node, "arms", []) or []:
pattern = getattr(arm, "pattern", None)
if not self.is_supported_switch_match_arm(arm):
raise ValueError(
"Unsupported match arm for GLSL codegen; only unguarded "
"literal and wildcard patterns can be lowered to switch"
)
if isinstance(pattern, WildcardPatternNode):
code += f"{indent_str} default:\n"
else:
code += (
f"{indent_str} case "
f"{self.generate_expression(pattern.literal)}:\n"
)
body = getattr(arm, "body", [])
code += self.generate_statement_body(body, indent + 2)
if not self.statement_body_terminates(body):
code += f"{indent_str} break;\n"
code += f"{indent_str}}}\n"
return code
def is_supported_switch_match_arm(self, arm):
if getattr(arm, "guard", None) is not None:
return False
pattern = getattr(arm, "pattern", None)
return isinstance(pattern, (LiteralPatternNode, WildcardPatternNode))
def statement_body_terminates(self, body):
if hasattr(body, "statements"):
statements = body.statements
elif isinstance(body, list):
statements = body
elif body is None:
statements = []
else:
statements = [body]
return bool(statements) and isinstance(
statements[-1], (BreakNode, ContinueNode, ReturnNode)
)
def generate_statement_body(self, body, indent):
code = ""
if hasattr(body, "statements"):
for stmt in body.statements:
code += self.generate_statement(stmt, indent)
elif isinstance(body, list):
for stmt in body:
code += self.generate_statement(stmt, indent)
elif body is not None:
code += self.generate_statement(body, indent)
return code
def generate_for_initializer(self, init):
if init is None:
return ""
if isinstance(init, str):
return init
if isinstance(init, VariableNode) or (
hasattr(init, "__class__") and "ExpressionStatement" in str(init.__class__)
):
return self.generate_statement(init, 0).strip().rstrip(";")
return self.generate_expression(init).strip().rstrip(";")
def generate_expression(self, expr, is_main=False):
"""Render a CrossGL AST expression into GLSL expression syntax."""
if expr is None:
return ""
if isinstance(expr, str):
return expr
elif isinstance(expr, (int, float, bool)):
if isinstance(expr, bool):
return "true" if expr else "false"
return str(expr)
elif hasattr(expr, "__class__") and "VariableNode" in str(type(expr)):
if hasattr(expr, "name"):
return expr.name
else:
return str(expr)
elif hasattr(expr, "__class__") and "IdentifierNode" in str(type(expr)):
return expr.name
elif hasattr(expr, "__class__") and "LiteralNode" in str(type(expr)):
literal_type = getattr(getattr(expr, "literal_type", None), "name", None)
if (
literal_type == "uint"
and isinstance(expr.value, int)
and not isinstance(expr.value, bool)
):
return f"{expr.value}u"
return str(expr.value)
elif hasattr(expr, "__class__") and "BinaryOpNode" in str(type(expr)):
left = self.generate_expression(expr.left)
right = self.generate_expression(expr.right)
op = self.map_operator(expr.op)
return f"({left} {op} {right})"
elif hasattr(expr, "__class__") and "AssignmentNode" in str(type(expr)):
left = self.generate_expression(
expr.target if hasattr(expr, "target") else expr.left
)
right = self.generate_expression(
expr.value if hasattr(expr, "value") else expr.right
)
op = expr.operator if hasattr(expr, "operator") else expr.op
op = self.map_operator(op)
return f"{left} {op} {right}"
elif hasattr(expr, "__class__") and "UnaryOpNode" in str(type(expr)):
operand = self.generate_expression(expr.operand)
op = self.map_operator(expr.op)
return f"({op}{operand})"
elif isinstance(expr, WaveOpNode):
args = ", ".join(self.generate_expression(arg) for arg in expr.arguments)
return f"{expr.operation}({args})"
elif isinstance(expr, RayTracingOpNode):
args = ", ".join(self.generate_expression(arg) for arg in expr.arguments)
return f"{expr.operation}({args})"
elif isinstance(expr, MeshOpNode):
args = ", ".join(self.generate_expression(arg) for arg in expr.arguments)
return f"{expr.operation}({args})"
elif isinstance(expr, RayQueryOpNode):
query = self.generate_expression(expr.query_expr)
args = ", ".join(self.generate_expression(arg) for arg in expr.arguments)
return f"{query}.{expr.operation}({args})"
elif hasattr(expr, "__class__") and "ArrayAccessNode" in str(type(expr)):
# Handle array access properly
if hasattr(expr, "array") and hasattr(expr, "index"):
array = self.generate_expression(expr.array)
index = self.generate_expression(expr.index)
return f"{array}[{index}]"
else:
return str(expr)
elif hasattr(expr, "__class__") and "FunctionCallNode" in str(type(expr)):
# Map function names to GLSL equivalents
func_expr = getattr(expr, "function", getattr(expr, "name", expr))
func_name = None
if hasattr(func_expr, "name"):
func_name = func_expr.name
callee = func_name
elif isinstance(func_expr, str):
func_name = func_expr
callee = func_expr
else:
callee = self.generate_expression(func_expr)
original_func_name = func_name
func_name = self.function_map.get(func_name, func_name)
texture_call = self.generate_texture_call(func_name, expr.args)
if texture_call is not None:
return texture_call
if func_name in [
"vec2",
"vec3",
"vec4",
"ivec2",
"ivec3",
"ivec4",
"uvec2",
"uvec3",
"uvec4",
"bvec2",
"bvec3",
"bvec4",
]:
args = ", ".join(self.generate_expression(arg) for arg in expr.args)
return f"{func_name}({args})"
if func_name in ["mat2", "mat3", "mat4"]:
args = ", ".join(self.generate_expression(arg) for arg in expr.args)
return f"{func_name}({args})"
call_args = self.filter_sampler_arguments(original_func_name, expr.args)
args = ", ".join(self.generate_expression(arg) for arg in call_args)
return f"{func_name or callee}({args})"
elif hasattr(expr, "__class__") and "MemberAccessNode" in str(type(expr)):
flattened_member = self.flattened_stage_member_name(expr)
if flattened_member is not None:
return flattened_member
obj = self.generate_expression(expr.object)
return f"{obj}.{expr.member}"
elif hasattr(expr, "__class__") and "TernaryOpNode" in str(type(expr)):
condition = self.generate_expression(expr.condition)
true_expr = self.generate_expression(expr.true_expr)
false_expr = self.generate_expression(expr.false_expr)
return f"({condition} ? {true_expr} : {false_expr})"
else:
return str(expr)
def flattened_stage_member_name(self, expr):
object_name = self.expression_name(getattr(expr, "object", None))
if object_name in self.current_stage_inputs:
return self.current_stage_inputs[object_name].get(expr.member)
if object_name in self.current_stage_outputs:
return self.current_stage_outputs[object_name].get(expr.member)
return None
def expression_name(self, expr):
if isinstance(expr, str):
return expr
if hasattr(expr, "name") and isinstance(expr.name, str):
return expr.name
if isinstance(expr, ArrayAccessNode) or (
hasattr(expr, "__class__") and "ArrayAccess" in str(expr.__class__)
):
array_expr = getattr(expr, "array", getattr(expr, "array_expr", None))
return self.expression_name(array_expr)
return None
def is_explicit_sampler_argument(self, args):
if len(args) < 3:
return False
return self.texture_call_uses_explicit_sampler(args)
def texture_call_uses_explicit_sampler(self, args):
if len(args) < 2:
return False
sampler_name = self.expression_name(args[1]) or self.generate_expression(
args[1]
)
if (
sampler_name in self.sampler_variables
or sampler_name in self.current_sampler_parameters
):
return True
arg_type = self.expression_result_type(args[1])
return arg_type is not None and self.is_sampler_type(arg_type)
def texture_call_parts(self, args):
explicit_sampler = self.is_explicit_sampler_argument(args)
coord_index = 2 if explicit_sampler else 1
if len(args) <= coord_index:
return None
texture_name = self.generate_expression(args[0])
coord = self.generate_expression(args[coord_index])
extra_args = args[coord_index + 1 :]
return texture_name, coord, extra_args
def texture_resource_type(self, texture_arg):
texture_name = self.expression_name(texture_arg)
if not texture_name:
return None
return self.current_texture_parameters.get(
texture_name, self.texture_variable_types.get(texture_name)
)
def texture_argument_resource_type(self, texture_arg):
texture_type = self.texture_resource_type(texture_arg)
if texture_type is not None:
return texture_type
return self.expression_result_type(texture_arg)
def validate_texture_resource_argument(self, func_name, args):
if not args or func_name not in self.texture_resource_operation_names():
return
if self.texture_resource_type(args[0]) is not None:
return
arg_type = self.expression_result_type(args[0])
if arg_type is not None and self.is_inferable_resource_array_type(arg_type):
return
texture_name = self.expression_name(args[0]) or str(args[0])
raise ValueError(
f"OpenGL texture operation '{func_name}' requires a declared "
f"texture or image resource argument: {texture_name}"
)
def validate_image_resource_argument(self, func_name, args):
if not args or func_name not in IMAGE_RESOURCE_INTRINSIC_NAMES:
return
texture_type = self.texture_argument_resource_type(args[0])
if self.is_storage_image_type(texture_type):
return
texture_name = self.expression_name(args[0]) or str(args[0])
raise ValueError(
f"OpenGL image operation '{func_name}' requires a storage "
f"image resource argument: {texture_name}"
)
def is_integer_coordinate_type(self, vtype):
type_name = self.type_name_string(vtype)
base_type = self.resource_base_type(type_name)
mapped_type = self.map_type(base_type)
return base_type in {
"int",
"uint",
"ivec2",
"ivec3",
"ivec4",
"uvec2",
"uvec3",
"uvec4",
"int2",
"int3",
"int4",
"uint2",
"uint3",
"uint4",
} or mapped_type in {
"int",
"uint",
"ivec2",
"ivec3",
"ivec4",
"uvec2",
"uvec3",
"uvec4",
}
def resource_coordinate_dimension(self, texture_type):
texture_type = self.resource_base_type(texture_type)
if not texture_type or "Cube" in texture_type:
return None
for prefix in ("iimage", "uimage", "image"):
if texture_type.startswith(f"{prefix}2DArray"):
return 3
if texture_type.startswith(f"{prefix}3D"):
return 3
if texture_type.startswith(f"{prefix}2D"):
return 2
if texture_type.startswith(f"{prefix}1DArray"):
return 2
if texture_type.startswith(f"{prefix}1D"):
return 1
return {
"sampler1D": 1,
"sampler1DArray": 2,
"sampler2D": 2,
"sampler2DArray": 3,
"sampler2DMS": 2,
"sampler2DMSArray": 3,
"sampler3D": 3,
"isampler1D": 1,
"isampler1DArray": 2,
"isampler2D": 2,
"isampler2DArray": 3,
"isampler2DMS": 2,
"isampler2DMSArray": 3,
"isampler3D": 3,
"usampler1D": 1,
"usampler1DArray": 2,
"usampler2D": 2,
"usampler2DArray": 3,
"usampler2DMS": 2,
"usampler2DMSArray": 3,
"usampler3D": 3,
}.get(texture_type)
def resource_offset_dimension(self, func_name, texture_type):
texture_type = self.resource_base_type(texture_type)
if not texture_type or "Cube" in texture_type:
return None
if (
func_name == "texelFetchOffset"
and self.is_multisample_texture_resource_type(texture_type)
):
return None
if func_name in {"textureGatherOffset", "textureGatherOffsets"}:
return 2 if self.texture_gather_offset_supported(texture_type) else None
if func_name == "textureGatherCompareOffset":
return (
2
if self.texture_gather_compare_offset_supported(texture_type)
else None
)
if func_name in {"textureCompareOffset", "textureCompareProjOffset"}:
return 2 if self.texture_compare_offset_supported(texture_type) else None
if func_name in {"textureCompareLodOffset", "textureCompareProjLodOffset"}:
return (
2 if self.texture_compare_lod_offset_supported(texture_type) else None
)
if func_name in {"textureCompareGradOffset", "textureCompareProjGradOffset"}:
return (
2 if self.texture_compare_grad_offset_supported(texture_type) else None
)
if (
func_name != "texelFetchOffset"
and func_name in OFFSET_DIMENSION_INTRINSIC_NAMES
and not self.texture_sample_offset_supported(texture_type)
):
return None
return {
"sampler1D": 1,
"sampler1DArray": 1,
"sampler2D": 2,
"sampler2DArray": 2,
"sampler2DShadow": 2,
"sampler2DArrayShadow": 2,
"sampler3D": 3,
"isampler1D": 1,
"isampler1DArray": 1,
"isampler2D": 2,
"isampler2DArray": 2,
"isampler3D": 3,
"usampler1D": 1,
"usampler1DArray": 1,
"usampler2D": 2,
"usampler2DArray": 2,
"usampler3D": 3,
}.get(texture_type)
def resource_gradient_dimension(self, func_name, texture_type):
texture_type = self.resource_base_type(texture_type)
if not texture_type or self.is_multisample_texture_resource_type(texture_type):
return None
for prefix in ("iimage", "uimage", "image"):
if texture_type.startswith(prefix):
return None
if "Cube" in texture_type:
return 3
return {
"sampler1D": 1,
"sampler1DArray": 1,
"sampler2D": 2,
"sampler2DArray": 2,
"sampler2DShadow": 2,
"sampler2DArrayShadow": 2,
"sampler3D": 3,
"isampler1D": 1,
"isampler1DArray": 1,
"isampler2D": 2,
"isampler2DArray": 2,
"isampler3D": 3,
"usampler1D": 1,
"usampler1DArray": 1,
"usampler2D": 2,
"usampler2DArray": 2,
"usampler3D": 3,
}.get(texture_type)
def resource_query_lod_coordinate_dimension(self, texture_type):
texture_type = self.resource_base_type(texture_type)
if not texture_type or self.is_multisample_texture_resource_type(texture_type):
return None
for prefix in ("iimage", "uimage", "image"):
if texture_type.startswith(prefix):
return None
return {
"sampler1D": 1,
"sampler1DArray": 2,
"sampler2D": 2,
"sampler2DArray": 3,
"sampler2DShadow": 2,
"sampler2DArrayShadow": 3,
"sampler3D": 3,
"samplerCube": 3,
"samplerCubeArray": 4,
"samplerCubeShadow": 3,
"samplerCubeArrayShadow": 4,
"isampler1D": 1,
"isampler1DArray": 2,
"isampler2D": 2,
"isampler2DArray": 3,
"isampler3D": 3,
"isamplerCube": 3,
"isamplerCubeArray": 4,
"usampler1D": 1,
"usampler1DArray": 2,
"usampler2D": 2,
"usampler2DArray": 3,
"usampler3D": 3,
"usamplerCube": 3,
"usamplerCubeArray": 4,
}.get(texture_type)
def validate_integer_coordinate_argument(self, func_name, args):
if func_name not in INTEGER_COORDINATE_INTRINSIC_NAMES or len(args) < 2:
return
coord_type = self.expression_result_type(args[1])
if coord_type is None or self.is_integer_coordinate_type(coord_type):
return
raise ValueError(
f"OpenGL resource operation '{func_name}' requires an integer "
f"coordinate argument: {expression_debug_name(args[1])} has type "
f"{self.type_name_string(coord_type)}"
)
def validate_coordinate_dimension_argument(self, func_name, args):
if func_name not in INTEGER_COORDINATE_INTRINSIC_NAMES or len(args) < 2:
return
texture_type = self.texture_argument_resource_type(args[0])
expected_dimension = self.resource_coordinate_dimension(texture_type)
if expected_dimension is None:
return
coord_type = self.expression_result_type(args[1])
coord_dimension = integer_coordinate_dimension(
self.type_name_string(coord_type)
)
if coord_dimension is None or coord_dimension == expected_dimension:
return
raise ValueError(
f"OpenGL resource operation '{func_name}' requires a "
f"{expected_dimension}D integer coordinate for "
f"{self.resource_base_type(texture_type)}: "
f"{expression_debug_name(args[1])} has type "
f"{self.type_name_string(coord_type)}"
)
def validate_offset_dimension_argument(self, func_name, args):
offset_indices = texture_offset_argument_indices(
func_name,
self.texture_call_uses_explicit_sampler(args),
len(args),
)
if not offset_indices:
return
texture_type = self.texture_argument_resource_type(args[0])
expected_dimension = self.resource_offset_dimension(func_name, texture_type)
if expected_dimension is None:
return
for offset_index in offset_indices:
offset_type = self.expression_result_type(args[offset_index])
if offset_type is None:
continue
if not self.is_integer_coordinate_type(offset_type):
raise ValueError(
f"OpenGL resource operation '{func_name}' requires an integer "
f"offset argument: {expression_debug_name(args[offset_index])} "
f"has type {self.type_name_string(offset_type)}"
)
offset_dimension = integer_coordinate_dimension(
self.type_name_string(offset_type)
)
if offset_dimension is None or offset_dimension == expected_dimension:
continue
raise ValueError(
f"OpenGL resource operation '{func_name}' requires a "
f"{expected_dimension}D integer offset for "
f"{self.resource_base_type(texture_type)}: "
f"{expression_debug_name(args[offset_index])} has type "
f"{self.type_name_string(offset_type)}"
)
def gradient_argument_dimension(self, vtype):
type_name = self.resource_base_type(self.type_name_string(vtype))
mapped_type = self.map_type(type_name)
return floating_coordinate_dimension(
mapped_type
) or floating_coordinate_dimension(type_name)
def query_lod_coordinate_dimension(self, vtype):
type_name = self.resource_base_type(self.type_name_string(vtype))
mapped_type = self.map_type(type_name)
return floating_coordinate_dimension(
mapped_type
) or floating_coordinate_dimension(type_name)
def validate_query_lod_coordinate_argument(self, func_name, args):
coord_index = texture_query_lod_coordinate_argument_index(
func_name,
self.texture_call_uses_explicit_sampler(args),
len(args),
)
if coord_index is None:
return
texture_type = self.texture_argument_resource_type(args[0])
expected_dimension = self.resource_query_lod_coordinate_dimension(texture_type)
if expected_dimension is None:
return
coord_type = self.expression_result_type(args[coord_index])
if coord_type is None:
return
coord_dimension = self.query_lod_coordinate_dimension(coord_type)
if coord_dimension is None:
raise ValueError(
f"OpenGL texture query operation '{func_name}' requires a floating "
f"coordinate argument: {expression_debug_name(args[coord_index])} "
f"has type {self.type_name_string(coord_type)}"
)
if coord_dimension == expected_dimension:
return
raise ValueError(
f"OpenGL texture query operation '{func_name}' requires a "
f"{expected_dimension}D floating coordinate for "
f"{self.resource_base_type(texture_type)}: "
f"{expression_debug_name(args[coord_index])} has type "
f"{self.type_name_string(coord_type)}"
)
def validate_gradient_dimension_arguments(self, func_name, args):
gradient_indices = texture_gradient_argument_indices(
func_name,
self.texture_call_uses_explicit_sampler(args),
len(args),
)
if not gradient_indices:
return
texture_type = self.texture_argument_resource_type(args[0])
expected_dimension = self.resource_gradient_dimension(func_name, texture_type)
if expected_dimension is None:
return
for gradient_index in gradient_indices:
gradient_type = self.expression_result_type(args[gradient_index])
if gradient_type is None:
continue
gradient_dimension = self.gradient_argument_dimension(gradient_type)
if gradient_dimension is None:
raise ValueError(
f"OpenGL resource operation '{func_name}' requires a floating "
f"gradient argument: {expression_debug_name(args[gradient_index])} "
f"has type {self.type_name_string(gradient_type)}"
)
if gradient_dimension == expected_dimension:
continue
raise ValueError(
f"OpenGL resource operation '{func_name}' requires a "
f"{expected_dimension}D floating gradient for "
f"{self.resource_base_type(texture_type)}: "
f"{expression_debug_name(args[gradient_index])} has type "
f"{self.type_name_string(gradient_type)}"
)
def is_scalar_floating_type(self, vtype):
type_name = self.type_name_string(vtype)
if not type_name or "[" in str(type_name):
return False
mapped_type = self.map_type(type_name)
return is_floating_scalar_type(mapped_type) or is_floating_scalar_type(
type_name
)
def is_scalar_numeric_type(self, vtype):
type_name = self.type_name_string(vtype)
if not type_name or "[" in str(type_name):
return False
mapped_type = self.map_type(type_name)
return is_numeric_scalar_type(mapped_type) or is_numeric_scalar_type(type_name)
def is_scalar_integer_type(self, vtype):
type_name = self.type_name_string(vtype)
if not type_name or "[" in str(type_name):
return False
mapped_type = self.map_type(type_name)
return is_integer_scalar_type(mapped_type) or is_integer_scalar_type(type_name)
def texture_argument_diagnostic_type(self, arg):
texture_type = self.texture_resource_type(arg)
if texture_type is not None:
return texture_type
arg_name = self.expression_name(arg)
if (
arg_name in self.sampler_variables
or arg_name in self.current_sampler_parameters
):
return "sampler"
return self.expression_result_type(arg)
def validate_compare_argument(self, func_name, args):
compare_index = texture_compare_argument_index(
func_name,
self.texture_call_uses_explicit_sampler(args),
len(args),
)
if compare_index is None:
return
compare_type = self.expression_result_type(args[compare_index])
if compare_type is None or self.is_scalar_floating_type(compare_type):
return
raise ValueError(
f"OpenGL texture compare operation '{func_name}' requires a scalar "
f"floating compare argument: {expression_debug_name(args[compare_index])} "
f"has type {self.type_name_string(compare_type)}"
)
def validate_lod_argument(self, func_name, args):
lod_index = texture_lod_argument_index(
func_name,
self.texture_call_uses_explicit_sampler(args),
len(args),
)
if lod_index is None:
return
lod_type = self.texture_argument_diagnostic_type(args[lod_index])
if lod_type is None or self.is_scalar_numeric_type(lod_type):
return
raise ValueError(
f"OpenGL texture LOD operation '{func_name}' requires a scalar "
f"numeric lod argument: {expression_debug_name(args[lod_index])} "
f"has type {self.type_name_string(lod_type)}"
)
def validate_bias_argument(self, func_name, args):
bias_index = texture_bias_argument_index(
func_name,
self.texture_call_uses_explicit_sampler(args),
len(args),
)
if bias_index is None:
return
bias_type = self.texture_argument_diagnostic_type(args[bias_index])
if bias_type is None or self.is_scalar_numeric_type(bias_type):
return
raise ValueError(
f"OpenGL texture bias operation '{func_name}' requires a scalar "
f"numeric bias argument: {expression_debug_name(args[bias_index])} "
f"has type {self.type_name_string(bias_type)}"
)
def validate_mip_level_argument(self, func_name, args):
level_index = texture_mip_level_argument_index(func_name, len(args))
if level_index is None:
return
level_type = self.texture_argument_diagnostic_type(args[level_index])
if level_type is None or self.is_scalar_integer_type(level_type):
return
raise ValueError(
f"OpenGL resource operation '{func_name}' requires a scalar integer "
f"mip/sample level argument: {expression_debug_name(args[level_index])} "
f"has type {self.type_name_string(level_type)}"
)
def validate_sample_index_argument(self, func_name, args):
sample_index = texture_sample_index_argument_index(func_name, len(args))
if sample_index is None:
return
texture_type = self.texture_argument_resource_type(args[0])
if not self.is_multisample_texture_resource_type(texture_type):
return
sample_type = self.texture_argument_diagnostic_type(args[sample_index])
if sample_type is None or self.is_scalar_integer_type(sample_type):
return
raise ValueError(
f"OpenGL multisample texel fetch operation '{func_name}' requires a "
f"scalar integer sample index argument: "
f"{expression_debug_name(args[sample_index])} has type "
f"{self.type_name_string(sample_type)}"
)
def validate_gather_component_argument(self, func_name, args):
component_index = texture_gather_component_argument_index(
func_name,
self.texture_call_uses_explicit_sampler(args),
len(args),
)
if component_index is None:
return
component_type = self.texture_argument_diagnostic_type(args[component_index])
if component_type is None or self.is_scalar_integer_type(component_type):
return
raise ValueError(
f"OpenGL texture gather operation '{func_name}' requires a scalar "
f"integer component argument: "
f"{expression_debug_name(args[component_index])} has type "
f"{self.type_name_string(component_type)}"
)
def validate_texture_call_arity(self, func_name, args):
if func_name not in self.texture_resource_operation_names():
return
has_explicit_sampler = self.texture_call_uses_explicit_sampler(args)
min_count = texture_intrinsic_min_argument_count(
func_name,
has_explicit_sampler,
)
if min_count is not None and len(args) < min_count:
raise ValueError(
f"OpenGL texture operation '{func_name}' requires at least "
f"{min_count} argument(s), got {len(args)}"
)
allowed_counts = texture_intrinsic_allowed_argument_counts(
func_name,
has_explicit_sampler,
)
if allowed_counts is not None and len(args) not in allowed_counts:
counts = ", ".join(str(count) for count in allowed_counts)
raise ValueError(
f"OpenGL texture operation '{func_name}' accepts "
f"{counts} argument(s), got {len(args)}"
)
max_count = texture_intrinsic_max_argument_count(
func_name,
has_explicit_sampler,
)
if max_count is None or len(args) <= max_count:
return
raise ValueError(
f"OpenGL texture operation '{func_name}' accepts at most "
f"{max_count} argument(s), got {len(args)}"
)
def texture_resource_operation_names(self):
return {
"texture",
"textureLod",
"textureGrad",
"textureOffset",
"textureLodOffset",
"textureGradOffset",
"textureProj",
"textureProjOffset",
"textureProjLod",
"textureProjLodOffset",
"textureProjGrad",
"textureProjGradOffset",
"textureCompare",
"textureCompareOffset",
"textureCompareLod",
"textureCompareLodOffset",
"textureCompareGrad",
"textureCompareGradOffset",
"textureCompareProj",
"textureCompareProjOffset",
"textureCompareProjLod",
"textureCompareProjLodOffset",
"textureCompareProjGrad",
"textureCompareProjGradOffset",
"textureGather",
"textureGatherOffset",
"textureGatherOffsets",
"textureGatherCompare",
"textureGatherCompareOffset",
"textureQueryLod",
"textureQueryLevels",
"textureSize",
"textureSamples",
"texelFetch",
"texelFetchOffset",
"imageLoad",
"imageStore",
"imageSize",
"imageSamples",
"imageAtomicAdd",
"imageAtomicMin",
"imageAtomicMax",
"imageAtomicAnd",
"imageAtomicOr",
"imageAtomicXor",
"imageAtomicExchange",
"imageAtomicCompSwap",
}
def vector_component(self, expression, component):
if all(char.isalnum() or char in "_.[]" for char in expression):
return f"{expression}.{component}"
return f"({expression}).{component}"
def texture_query_lod_coordinate(self, texture_type, coord):
texture_type = self.resource_base_type(texture_type)
if texture_type in {"sampler2DArray", "sampler2DArrayShadow"}:
return self.vector_component(coord, "xy")
if texture_type in {"samplerCubeArray", "samplerCubeArrayShadow"}:
return self.vector_component(coord, "xyz")
return coord
def is_array_expression(self, node):
type_name = self.type_name_string(self.expression_result_type(node))
return isinstance(type_name, str) and "[" in type_name and "]" in type_name
def texture_gather_offsets_args(self, extra_args):
if len(extra_args) in {1, 2} and self.is_array_expression(extra_args[0]):
offsets_name = self.generate_expression(extra_args[0])
offset_args = [f"{offsets_name}[{index}]" for index in range(4)]
component_arg = extra_args[1] if len(extra_args) == 2 else None
return offset_args, component_arg
if len(extra_args) in {4, 5}:
component_arg = extra_args[4] if len(extra_args) == 5 else None
return extra_args[:4], component_arg
return None, None
def texture_gather_component_value(self, component_arg):
if component_arg is None:
return None
return self.literal_int_value(component_arg, self.literal_int_constants)
def texture_gather_call_expression(
self, function_name, texture_name, coord, offset_arg=None, component=None
):
args = [texture_name, coord]
if offset_arg is not None:
args.append(offset_arg)
if component is not None:
args.append(str(component))
return f"{function_name}({', '.join(args)})"
def texture_gather_offsets_expression(
self, texture_name, coord, offset_args, component
):
component_suffixes = ("x", "y", "z", "w")
component_values = []
for index, offset_arg in enumerate(offset_args):
gather = self.texture_gather_call_expression(
"textureGatherOffset",
texture_name,
coord,
self.generate_expression(offset_arg),
component,
)
component_values.append(f"{gather}.{component_suffixes[index]}")
return f"vec4({', '.join(component_values)})"
def texture_gather_dynamic_component_expression(self, build_expression, component):
component_calls = [build_expression(index) for index in range(4)]
return (
f"({component} == 0 ? {component_calls[0]} : "
f"{component} == 1 ? {component_calls[1]} : "
f"{component} == 2 ? {component_calls[2]} : {component_calls[3]})"
)
def unsupported_texture_gather_call(self, func_name, reason):
return (
f"/* unsupported GLSL texture gather: " f"{func_name} {reason} */ vec4(0.0)"
)
def texture_gather_supported(self, texture_type):
return self.resource_base_type(texture_type) in {
"sampler2D",
"sampler2DArray",
"samplerCube",
"samplerCubeArray",
}
def texture_gather_offset_supported(self, texture_type):
return self.resource_base_type(texture_type) in {
"sampler2D",
"sampler2DArray",
}
def unsupported_texture_projected_call(self, func_name, reason):
return (
f"/* unsupported GLSL projected texture: {func_name} {reason} */ vec4(0.0)"
)
def texture_sample_offset_supported(self, texture_type):
return self.resource_base_type(texture_type) in {
"sampler1D",
"sampler2D",
"sampler3D",
"sampler2DArray",
"sampler2DShadow",
"sampler2DArrayShadow",
}
def unsupported_texture_sample_offset_call(self, func_name, reason):
return f"/* unsupported GLSL texture offset: {func_name} {reason} */ vec4(0.0)"
def is_multisample_texture_resource_type(self, texture_type):
return self.resource_base_type(texture_type) in {
"sampler2DMS",
"sampler2DMSArray",
}
def unsupported_multisample_texture_call(self, func_name, texture_type):
texture_type = self.resource_base_type(texture_type)
return (
f"/* unsupported GLSL multisample texture call: "
f"{func_name} on {texture_type} */ vec4(0.0)"
)
def unsupported_multisample_texture_query_lod_call(self, texture_type):
texture_type = self.resource_base_type(texture_type)
return (
"/* unsupported GLSL multisample texture query: "
f"textureQueryLod on {texture_type} */ vec2(0.0)"
)
def unsupported_texture_query_levels_call(self, texture_type):
texture_type = self.resource_base_type(texture_type)
return (
"/* unsupported GLSL texture query: "
f"textureQueryLevels on {texture_type} */ 0"
)
def unsupported_texture_query_lod_call(self, texture_type):
texture_type = self.resource_base_type(texture_type)
return (
"/* unsupported GLSL texture query: "
f"textureQueryLod on {texture_type} */ vec2(0.0)"
)
def storage_image_texture_operation_expression(self, func_name, texture_type):
if not self.is_storage_image_type(texture_type):
return None
texture_type = self.resource_base_type(texture_type)
if func_name in {
"textureCompare",
"textureCompareOffset",
"textureCompareLod",
"textureCompareLodOffset",
"textureCompareGrad",
"textureCompareGradOffset",
"textureCompareProj",
"textureCompareProjOffset",
"textureCompareProjLod",
"textureCompareProjLodOffset",
"textureCompareProjGrad",
"textureCompareProjGradOffset",
}:
return (
"/* unsupported GLSL storage image texture comparison: "
f"{func_name} on {texture_type} */ 0.0"
)
if func_name in {
"texture",
"textureLod",
"textureGrad",
"textureOffset",
"textureLodOffset",
"textureGradOffset",
"textureProj",
"textureProjOffset",
"textureProjLod",
"textureProjLodOffset",
"textureProjGrad",
"textureProjGradOffset",
"textureGather",
"textureGatherOffset",
"textureGatherOffsets",
"textureGatherCompare",
"textureGatherCompareOffset",
"texelFetch",
"texelFetchOffset",
}:
return (
"/* unsupported GLSL storage image texture operation: "
f"{func_name} on {texture_type} */ vec4(0.0)"
)
return None
def unsupported_texture_samples_query_call(self):
return "/* unsupported GLSL texture samples query: requires multisample sampler */ 0"
def image_size_expression(self, image_arg):
image_name = self.generate_expression(image_arg)
return f"imageSize({image_name})"
def unsupported_multisample_texel_fetch_offset_call(self, texture_type):
texture_type = self.resource_base_type(texture_type)
return (
"/* unsupported GLSL texel fetch offset: "
f"multisample texture {texture_type} does not support offsets */ vec4(0.0)"
)
def is_cube_texture_resource_type(self, texture_type):
return self.resource_base_type(texture_type) in {
"samplerCube",
"samplerCubeArray",
"samplerCubeShadow",
"samplerCubeArrayShadow",
}
def unsupported_cube_texel_fetch_call(self, func_name, texture_type):
texture_type = self.resource_base_type(texture_type)
return (
f"/* unsupported GLSL texel fetch: {func_name} on "
f"{texture_type} */ vec4(0.0)"
)
def generate_texture_gather_call(self, func_name, args):
parts = self.texture_call_parts(args)
if parts is None:
return self.unsupported_texture_gather_call(
func_name, "requires texture and coordinate arguments"
)
texture_name, coord, extra_args = parts
texture_type = self.texture_resource_type(args[0])
if self.is_multisample_texture_resource_type(texture_type):
return self.unsupported_multisample_texture_call(func_name, texture_type)
if func_name == "textureGather" and not self.texture_gather_supported(
texture_type
):
return self.unsupported_texture_gather_call(
func_name, "requires 2D, 2D-array, cube, or cube-array textures"
)
if func_name in {
"textureGatherOffset",
"textureGatherOffsets",
} and not self.texture_gather_offset_supported(texture_type):
return self.unsupported_texture_gather_call(
func_name, "offsets require 2D or 2D-array textures"
)
offset_args = []
component_arg = None
if func_name == "textureGather":
if len(extra_args) > 1:
return self.unsupported_texture_gather_call(
func_name, "accepts at most one component argument"
)
if extra_args:
component_arg = extra_args[0]
elif func_name == "textureGatherOffset":
if len(extra_args) not in {1, 2}:
return self.unsupported_texture_gather_call(
func_name, "requires offset and optional component arguments"
)
offset_args = [extra_args[0]]
if len(extra_args) == 2:
component_arg = extra_args[1]
else:
offset_args, component_arg = self.texture_gather_offsets_args(extra_args)
if offset_args is None:
return self.unsupported_texture_gather_call(
func_name,
"requires a typed offsets array or four offset arguments",
)
component = self.texture_gather_component_value(component_arg)
if component is not None:
if component not in {0, 1, 2, 3}:
return self.unsupported_texture_gather_call(
func_name, "component literal must be 0, 1, 2, or 3"
)
if func_name == "textureGatherOffsets":
return self.texture_gather_offsets_expression(
texture_name, coord, offset_args, component
)
offset_arg = (
self.generate_expression(offset_args[0]) if offset_args else None
)
function_name = (
"textureGatherOffset"
if func_name == "textureGatherOffset"
else "textureGather"
)
return self.texture_gather_call_expression(
function_name, texture_name, coord, offset_arg, component
)
if component_arg is None:
if func_name == "textureGatherOffsets":
return self.texture_gather_offsets_expression(
texture_name, coord, offset_args, None
)
offset_arg = (
self.generate_expression(offset_args[0]) if offset_args else None
)
function_name = (
"textureGatherOffset"
if func_name == "textureGatherOffset"
else "textureGather"
)
return self.texture_gather_call_expression(
function_name, texture_name, coord, offset_arg
)
component_expr = self.generate_expression(component_arg)
if func_name == "textureGatherOffsets":
return self.texture_gather_dynamic_component_expression(
lambda option: self.texture_gather_offsets_expression(
texture_name, coord, offset_args, option
),
component_expr,
)
offset_arg = self.generate_expression(offset_args[0]) if offset_args else None
function_name = (
"textureGatherOffset"
if func_name == "textureGatherOffset"
else "textureGather"
)
return self.texture_gather_dynamic_component_expression(
lambda option: self.texture_gather_call_expression(
function_name, texture_name, coord, offset_arg, option
),
component_expr,
)
def texture_compare_coordinate(self, texture_type, coord, compare):
texture_type = self.resource_base_type(texture_type)
if texture_type == "samplerCubeArrayShadow":
return None
constructor = (
"vec4"
if texture_type
in {
"sampler2DArrayShadow",
"samplerCubeShadow",
}
else "vec3"
)
return f"{constructor}({coord}, {compare})"
def texture_compare_offset_supported(self, texture_type):
return self.resource_base_type(texture_type) in {
"sampler2DShadow",
"sampler2DArrayShadow",
}
def texture_compare_lod_supported(self, texture_type):
return self.resource_base_type(texture_type) == "sampler2DShadow"
def texture_compare_grad_supported(self, texture_type):
return self.resource_base_type(texture_type) in {
"sampler2DShadow",
"sampler2DArrayShadow",
"samplerCubeShadow",
}
def texture_compare_lod_offset_supported(self, texture_type):
return self.resource_base_type(texture_type) == "sampler2DShadow"
def texture_compare_grad_offset_supported(self, texture_type):
return self.resource_base_type(texture_type) in {
"sampler2DShadow",
"sampler2DArrayShadow",
}
def texture_compare_projected_coordinate(
self, texture_type, coord_arg, coord, compare
):
texture_type = self.resource_base_type(texture_type)
coord_type = self.resource_base_type(self.expression_result_type(coord_arg))
if texture_type == "sampler2DShadow":
if coord_type in {"vec3", "float3"}:
divisor = self.vector_component(coord, "z")
elif coord_type in {"vec4", "float4"}:
divisor = self.vector_component(coord, "w")
else:
return None
projected_coord = f"{self.vector_component(coord, 'xy')} / {divisor}"
return f"vec3({projected_coord}, {compare})"
if texture_type != "sampler2DArrayShadow" or coord_type not in {
"vec4",
"float4",
}:
return None
projected_coord = (
f"{self.vector_component(coord, 'xy')} / "
f"{self.vector_component(coord, 'w')}"
)
layer = self.vector_component(coord, "z")
return f"vec4({projected_coord}, {layer}, {compare})"
def unsupported_texture_compare_call(self, func_name, reason):
return f"/* unsupported GLSL texture compare: {func_name} {reason} */ 0.0"
def generate_texture_compare_call(self, func_name, args):
parts = self.texture_call_parts(args)
if parts is None:
return self.unsupported_texture_compare_call(
func_name, "requires texture and coordinate arguments"
)
texture_name, coord, extra_args = parts
if not extra_args:
return self.unsupported_texture_compare_call(
func_name, "requires a compare argument"
)
compare = self.generate_expression(extra_args[0])
texture_type = self.texture_resource_type(args[0])
if func_name in {
"textureCompareProj",
"textureCompareProjOffset",
"textureCompareProjLod",
"textureCompareProjLodOffset",
"textureCompareProjGrad",
"textureCompareProjGradOffset",
}:
coord_index = 2 if self.is_explicit_sampler_argument(args) else 1
compare_coord = self.texture_compare_projected_coordinate(
texture_type, args[coord_index], coord, compare
)
if compare_coord is None:
return self.unsupported_texture_compare_call(
func_name,
"requires sampler2DShadow vec3/vec4 or sampler2DArrayShadow vec4 projection coordinates",
)
if func_name == "textureCompareProj":
if len(extra_args) != 1:
return self.unsupported_texture_compare_call(
func_name, "accepts no extra arguments"
)
return f"texture({texture_name}, {compare_coord})"
if func_name == "textureCompareProjOffset":
if len(extra_args) != 2:
return self.unsupported_texture_compare_call(
func_name, "requires compare and offset arguments"
)
offset = self.generate_expression(extra_args[1])
return f"textureOffset({texture_name}, {compare_coord}, {offset})"
if func_name == "textureCompareProjLod":
if len(extra_args) != 2:
return self.unsupported_texture_compare_call(
func_name, "requires compare and lod arguments"
)
if self.resource_base_type(texture_type) == "sampler2DArrayShadow":
return self.unsupported_texture_compare_call(
func_name,
"projected explicit LOD is not supported for sampler2DArrayShadow",
)
lod = self.generate_expression(extra_args[1])
return f"textureLod({texture_name}, {compare_coord}, {lod})"
if func_name == "textureCompareProjLodOffset":
if len(extra_args) != 3:
return self.unsupported_texture_compare_call(
func_name, "requires compare, lod, and offset arguments"
)
if self.resource_base_type(texture_type) == "sampler2DArrayShadow":
return self.unsupported_texture_compare_call(
func_name,
"projected explicit LOD is not supported for sampler2DArrayShadow",
)
lod = self.generate_expression(extra_args[1])
offset = self.generate_expression(extra_args[2])
return (
f"textureLodOffset({texture_name}, {compare_coord}, "
f"{lod}, {offset})"
)
if func_name == "textureCompareProjGrad":
if len(extra_args) != 3:
return self.unsupported_texture_compare_call(
func_name,
"requires compare, gradient x, and gradient y arguments",
)
ddx = self.generate_expression(extra_args[1])
ddy = self.generate_expression(extra_args[2])
return f"textureGrad({texture_name}, {compare_coord}, {ddx}, {ddy})"
if len(extra_args) != 4:
return self.unsupported_texture_compare_call(
func_name,
"requires compare, gradient x, gradient y, and offset arguments",
)
ddx = self.generate_expression(extra_args[1])
ddy = self.generate_expression(extra_args[2])
offset = self.generate_expression(extra_args[3])
return (
f"textureGradOffset({texture_name}, {compare_coord}, "
f"{ddx}, {ddy}, {offset})"
)
if func_name == "textureCompare":
if texture_type == "samplerCubeArrayShadow":
return f"texture({texture_name}, {coord}, {compare})"
compare_coord = self.texture_compare_coordinate(
texture_type, coord, compare
)
if compare_coord is None:
return self.unsupported_texture_compare_call(
func_name, "requires supported shadow texture coordinates"
)
return f"texture({texture_name}, {compare_coord})"
if func_name == "textureCompareOffset":
if len(extra_args) != 2:
return self.unsupported_texture_compare_call(
func_name, "requires compare and offset arguments"
)
if not self.texture_compare_offset_supported(texture_type):
return self.unsupported_texture_compare_call(
func_name, "offsets require 2D or 2D-array shadow samplers"
)
compare_coord = self.texture_compare_coordinate(
texture_type, coord, compare
)
if compare_coord is None:
return self.unsupported_texture_compare_call(
func_name, "requires supported shadow texture coordinates"
)
offset = self.generate_expression(extra_args[1])
return f"textureOffset({texture_name}, {compare_coord}, {offset})"
if func_name == "textureCompareLod":
if len(extra_args) != 2:
return self.unsupported_texture_compare_call(
func_name, "requires compare and lod arguments"
)
if not self.texture_compare_lod_supported(texture_type):
return self.unsupported_texture_compare_call(
func_name, "explicit LOD requires 2D shadow samplers"
)
compare_coord = self.texture_compare_coordinate(
texture_type, coord, compare
)
if compare_coord is None:
return self.unsupported_texture_compare_call(
func_name, "requires supported shadow texture coordinates"
)
lod = self.generate_expression(extra_args[1])
return f"textureLod({texture_name}, {compare_coord}, {lod})"
if func_name == "textureCompareLodOffset":
if len(extra_args) != 3:
return self.unsupported_texture_compare_call(
func_name, "requires compare, lod, and offset arguments"
)
if not self.texture_compare_lod_offset_supported(texture_type):
return self.unsupported_texture_compare_call(
func_name, "explicit LOD offsets require 2D shadow samplers"
)
compare_coord = self.texture_compare_coordinate(
texture_type, coord, compare
)
if compare_coord is None:
return self.unsupported_texture_compare_call(
func_name, "requires supported shadow texture coordinates"
)
lod = self.generate_expression(extra_args[1])
offset = self.generate_expression(extra_args[2])
return f"textureLodOffset({texture_name}, {compare_coord}, {lod}, {offset})"
if func_name == "textureCompareGrad":
if len(extra_args) != 3:
return self.unsupported_texture_compare_call(
func_name, "requires compare, gradient x, and gradient y arguments"
)
if not self.texture_compare_grad_supported(texture_type):
return self.unsupported_texture_compare_call(
func_name,
"explicit gradients require 2D, 2D-array, or cube shadow samplers",
)
compare_coord = self.texture_compare_coordinate(
texture_type, coord, compare
)
if compare_coord is None:
return self.unsupported_texture_compare_call(
func_name, "requires supported shadow texture coordinates"
)
ddx = self.generate_expression(extra_args[1])
ddy = self.generate_expression(extra_args[2])
return f"textureGrad({texture_name}, {compare_coord}, {ddx}, {ddy})"
if func_name == "textureCompareGradOffset":
if len(extra_args) != 4:
return self.unsupported_texture_compare_call(
func_name,
"requires compare, gradient x, gradient y, and offset arguments",
)
if not self.texture_compare_grad_offset_supported(texture_type):
return self.unsupported_texture_compare_call(
func_name,
"explicit gradient offsets require 2D or 2D-array shadow samplers",
)
compare_coord = self.texture_compare_coordinate(
texture_type, coord, compare
)
if compare_coord is None:
return self.unsupported_texture_compare_call(
func_name, "requires supported shadow texture coordinates"
)
ddx = self.generate_expression(extra_args[1])
ddy = self.generate_expression(extra_args[2])
offset = self.generate_expression(extra_args[3])
return (
f"textureGradOffset({texture_name}, {compare_coord}, "
f"{ddx}, {ddy}, {offset})"
)
return None
def unsupported_texture_gather_compare_call(self, func_name, reason):
return (
f"/* unsupported GLSL texture gather compare: "
f"{func_name} {reason} */ vec4(0.0)"
)
def texture_gather_compare_offset_supported(self, texture_type):
return self.resource_base_type(texture_type) in {
"sampler2DShadow",
"sampler2DArrayShadow",
}
def generate_texture_gather_compare_call(self, func_name, args):
parts = self.texture_call_parts(args)
if parts is None:
return self.unsupported_texture_gather_compare_call(
func_name, "requires texture and coordinate arguments"
)
texture_name, coord, extra_args = parts
if not extra_args:
return self.unsupported_texture_gather_compare_call(
func_name, "requires a compare argument"
)
compare = self.generate_expression(extra_args[0])
if func_name == "textureGatherCompare":
if len(extra_args) != 1:
return self.unsupported_texture_gather_compare_call(
func_name, "accepts no extra arguments"
)
return f"textureGather({texture_name}, {coord}, {compare})"
if len(extra_args) != 2:
return self.unsupported_texture_gather_compare_call(
func_name, "requires compare and offset arguments"
)
if not self.texture_gather_compare_offset_supported(
self.texture_resource_type(args[0])
):
return self.unsupported_texture_gather_compare_call(
func_name, "offsets require 2D or 2D-array shadow samplers"
)
offset = self.generate_expression(extra_args[1])
return f"textureGatherOffset({texture_name}, {coord}, {compare}, {offset})"
def image_resource_format(self, texture_arg):
texture_name = self.expression_name(texture_arg)
if not texture_name:
return None
return self.current_image_format_parameters.get(
texture_name, self.image_variable_formats.get(texture_name)
)
def is_integer_image_type(self, texture_type):
return texture_type in {
"iimage2D",
"iimage3D",
"iimage2DArray",
"uimage2D",
"uimage3D",
"uimage2DArray",
}
def is_scalar_image_format(self, image_format):
return image_format in {
"r8",
"r8_snorm",
"r16",
"r16_snorm",
"r16f",
"r32f",
"r8i",
"r16i",
"r32i",
"r8ui",
"r16ui",
"r32ui",
}
def is_two_component_image_format(self, image_format):
return image_format in {
"rg8",
"rg8_snorm",
"rg16",
"rg16_snorm",
"rg16f",
"rg8i",
"rg16i",
"rg8ui",
"rg16ui",
"rg32f",
"rg32i",
"rg32ui",
}
def is_scalar_integer_image_resource(self, texture_type, image_format):
if image_format is not None:
return self.is_scalar_image_format(image_format)
return self.is_integer_image_type(texture_type)
def is_float_image_resource(self, texture_type):
return texture_type in {"image2D", "image3D", "image2DArray"}
def image_load_component_suffix(self, texture_type, image_format):
if self.is_scalar_integer_image_resource(texture_type, image_format):
return ".x"
if self.is_float_image_resource(texture_type) and self.is_scalar_value_type(
self.current_expression_expected_type
):
return ".x"
if self.is_two_component_image_format(image_format):
if self.is_scalar_value_type(self.current_expression_expected_type):
return ".x"
return ".xy"
return ""
def image_format_store_constructor(self, image_format):
return {
"r8": "vec4",
"r8_snorm": "vec4",
"r16": "vec4",
"r16_snorm": "vec4",
"r16f": "vec4",
"r32f": "vec4",
"r8i": "ivec4",
"r16i": "ivec4",
"r32i": "ivec4",
"r8ui": "uvec4",
"r16ui": "uvec4",
"r32ui": "uvec4",
}.get(image_format)
def integer_image_store_constructor(self, texture_type):
if texture_type in {"iimage2D", "iimage3D", "iimage2DArray"}:
return "ivec4"
if texture_type in {"uimage2D", "uimage3D", "uimage2DArray"}:
return "uvec4"
return None
def two_component_image_store_expression(
self, image_format, value, value_type=None
):
constructors = {
"rg8": ("vec4", "0.0"),
"rg8_snorm": ("vec4", "0.0"),
"rg16": ("vec4", "0.0"),
"rg16_snorm": ("vec4", "0.0"),
"rg16f": ("vec4", "0.0"),
"rg8i": ("ivec4", "0"),
"rg16i": ("ivec4", "0"),
"rg8ui": ("uvec4", "0u"),
"rg16ui": ("uvec4", "0u"),
"rg32f": ("vec4", "0.0"),
"rg32i": ("ivec4", "0"),
"rg32ui": ("uvec4", "0u"),
}
constructor = constructors.get(image_format)
if constructor is None:
return None
type_name, zero_value = constructor
if self.is_scalar_value_type(value_type):
return f"{type_name}({value}, {zero_value}, {zero_value}, {zero_value})"
return f"{type_name}({value}, {zero_value}, {zero_value})"
def image_store_value_expression(
self, texture_type, image_format, value, value_type=None
):
two_component_value = self.two_component_image_store_expression(
image_format, value, value_type
)
if two_component_value is not None:
return two_component_value
constructor = None
if self.is_scalar_integer_image_resource(texture_type, image_format):
constructor = self.integer_image_store_constructor(texture_type)
if constructor is None:
constructor = self.image_format_store_constructor(image_format)
elif self.is_float_image_resource(texture_type) and self.is_scalar_value_type(
value_type
):
constructor = "vec4"
if constructor:
return f"{constructor}({value})"
return value
def generate_texture_call(self, func_name, args):
if not func_name:
return None
self.validate_texture_call_arity(func_name, args)
self.validate_image_resource_argument(func_name, args)
self.validate_texture_resource_argument(func_name, args)
self.validate_integer_coordinate_argument(func_name, args)
self.validate_coordinate_dimension_argument(func_name, args)
self.validate_query_lod_coordinate_argument(func_name, args)
self.validate_compare_argument(func_name, args)
self.validate_lod_argument(func_name, args)
self.validate_bias_argument(func_name, args)
self.validate_sample_index_argument(func_name, args)
self.validate_mip_level_argument(func_name, args)
self.validate_gradient_dimension_arguments(func_name, args)
self.validate_offset_dimension_argument(func_name, args)
self.validate_gather_component_argument(func_name, args)
if func_name == "textureQueryLevels" and args:
texture_type = self.texture_resource_type(args[0])
if self.is_storage_image_type(texture_type):
return self.unsupported_texture_query_levels_call(texture_type)
if self.is_multisample_texture_resource_type(texture_type):
return "1"
return None
if func_name in {"textureSamples", "imageSamples"} and args:
texture_type = self.texture_resource_type(args[0])
if texture_type and not self.is_multisample_texture_resource_type(
texture_type
):
return self.unsupported_texture_samples_query_call()
if (
func_name == "imageSamples"
and self.is_multisample_texture_resource_type(texture_type)
):
texture_name = self.generate_expression(args[0])
return f"textureSamples({texture_name})"
return None
if func_name in {"textureSize", "imageSize"} and args:
texture_type = self.texture_resource_type(args[0])
if self.is_storage_image_type(texture_type):
return self.image_size_expression(args[0])
return None
if len(args) < 2:
return None
if func_name == "imageLoad" and len(args) >= 2:
image_name = self.generate_expression(args[0])
coord = self.generate_expression(args[1])
texture_type = self.texture_resource_type(args[0])
load_expr = f"imageLoad({image_name}, {coord})"
image_format = self.image_resource_format(args[0])
return f"{load_expr}{self.image_load_component_suffix(texture_type, image_format)}"
if func_name == "imageStore" and len(args) >= 3:
image_name = self.generate_expression(args[0])
coord = self.generate_expression(args[1])
value = self.generate_expression(args[2])
texture_type = self.texture_resource_type(args[0])
image_format = self.image_resource_format(args[0])
value = self.image_store_value_expression(
texture_type, image_format, value, self.expression_result_type(args[2])
)
return f"imageStore({image_name}, {coord}, {value})"
texture_type = self.texture_resource_type(args[0])
storage_image_operation = self.storage_image_texture_operation_expression(
func_name, texture_type
)
if storage_image_operation is not None:
return storage_image_operation
if func_name in {
"textureCompare",
"textureCompareOffset",
"textureCompareLod",
"textureCompareLodOffset",
"textureCompareGrad",
"textureCompareGradOffset",
"textureCompareProj",
"textureCompareProjOffset",
"textureCompareProjLod",
"textureCompareProjLodOffset",
"textureCompareProjGrad",
"textureCompareProjGradOffset",
}:
return self.generate_texture_compare_call(func_name, args)
if func_name in {"textureGatherCompare", "textureGatherCompareOffset"}:
return self.generate_texture_gather_compare_call(func_name, args)
if func_name in {
"textureGather",
"textureGatherOffset",
"textureGatherOffsets",
}:
return self.generate_texture_gather_call(func_name, args)
if func_name == "textureQueryLod":
parts = self.texture_call_parts(args)
if parts is None:
return None
texture_name, coord, _ = parts
texture_type = self.texture_resource_type(args[0])
if self.is_multisample_texture_resource_type(texture_type):
return self.unsupported_multisample_texture_query_lod_call(texture_type)
if self.is_storage_image_type(texture_type):
return self.unsupported_texture_query_lod_call(texture_type)
coord = self.texture_query_lod_coordinate(texture_type, coord)
return f"textureQueryLod({texture_name}, {coord})"
if func_name == "texelFetchOffset" and len(args) >= 4:
texture_type = self.texture_resource_type(args[0])
if self.is_cube_texture_resource_type(texture_type):
return self.unsupported_cube_texel_fetch_call(func_name, texture_type)
if self.is_multisample_texture_resource_type(texture_type):
return self.unsupported_multisample_texel_fetch_offset_call(
texture_type
)
return None
if func_name == "texelFetch" and len(args) >= 3:
texture_type = self.texture_resource_type(args[0])
if self.is_cube_texture_resource_type(texture_type):
return self.unsupported_cube_texel_fetch_call(func_name, texture_type)
return None
projected_texture_funcs = {
"textureProj",
"textureProjOffset",
"textureProjLod",
"textureProjLodOffset",
"textureProjGrad",
"textureProjGradOffset",
}
if func_name in projected_texture_funcs and args:
texture_type = self.resource_base_type(self.texture_resource_type(args[0]))
if texture_type in {"samplerCube", "samplerCubeArray"}:
return self.unsupported_texture_projected_call(
func_name,
"requires 1D, 2D, 2D-array, or 3D projection coordinates",
)
texture_funcs = {
"texture",
"textureLod",
"textureGrad",
"textureOffset",
"textureLodOffset",
"textureGradOffset",
*projected_texture_funcs,
}
if func_name in texture_funcs:
texture_type = self.texture_resource_type(args[0])
if self.is_multisample_texture_resource_type(texture_type):
return self.unsupported_multisample_texture_call(
func_name, texture_type
)
if func_name in {
"textureOffset",
"textureLodOffset",
"textureGradOffset",
} and not self.texture_sample_offset_supported(texture_type):
return self.unsupported_texture_sample_offset_call(
func_name,
"offsets require 1D, 2D, 2D-array, 3D, or planar shadow samplers",
)
if func_name not in texture_funcs or not self.is_explicit_sampler_argument(
args
):
return None
parts = self.texture_call_parts(args)
if parts is None:
return None
texture_name, coord, extra_args = parts
mapped_args = [texture_name, coord] + [
self.generate_expression(arg) for arg in extra_args
]
return f"{func_name}({', '.join(mapped_args)})"
def collect_function_sampler_parameter_indices(self, root):
sampler_indices = {}
visited = set()
def visit(value):
if value is None or isinstance(value, (str, int, float, bool)):
return
if isinstance(value, dict):
for item in value.values():
visit(item)
return
if isinstance(value, (list, tuple, set)):
for item in value:
visit(item)
return
value_id = id(value)
if value_id in visited:
return
visited.add(value_id)
name = getattr(value, "name", None)
params = getattr(value, "parameters", getattr(value, "params", []))
if name and params:
indices = []
for index, param in enumerate(params):
param_type = getattr(
param, "param_type", getattr(param, "vtype", None)
)
if self.is_sampler_type(param_type):
indices.append(index)
if indices:
sampler_indices[name] = set(indices)
if hasattr(value, "__dict__"):
for child in vars(value).values():
visit(child)
visit(root)
return sampler_indices
def collect_global_resource_names(self, root):
resource_names = set()
for node in getattr(root, "global_variables", []) or []:
var_type = getattr(node, "var_type", getattr(node, "vtype", "float"))
var_name = getattr(node, "name", getattr(node, "variable_name", None))
base_type = self.resource_base_type(var_type)
mapped_type = self.map_resource_type_with_format(base_type, node)
if var_name and (
self.is_sampler_type(base_type)
or self.is_opaque_resource_type(mapped_type)
):
resource_names.add(var_name)
return resource_names
def validate_global_resource_shadows(self, ast):
conflicts = collect_non_resource_global_resource_shadows(
ast,
self.collect_global_resource_names(ast),
self.is_inferable_resource_type,
)
if conflicts:
names = ", ".join(sorted(conflicts))
raise ValueError(
"Non-resource local declaration(s) shadow OpenGL global resource(s): "
f"{names}"
)
def filter_sampler_arguments(self, func_name, args):
sampler_indices = self.function_sampler_parameter_indices.get(func_name, set())
if not sampler_indices:
return args
return [arg for index, arg in enumerate(args) if index not in sampler_indices]
def collect_resource_array_size_hints(self, ast):
return collect_resource_array_size_hints(
global_arrays=self.collect_unsized_sampled_texture_globals(ast),
function_arrays=self.collect_unsized_sampled_texture_parameters(ast),
fixed_global_array_sizes=self.collect_fixed_resource_global_sizes(ast),
fixed_function_array_sizes=self.collect_fixed_resource_parameter_sizes(ast),
functions=self.collect_functions(ast),
walk_nodes=self.walk_ast,
expression_name=self.expression_name,
literal_int_value=self.literal_int_value,
visible_literal_int_constants=self.visible_literal_int_constants,
function_call_name=self.function_call_name,
initial_size=0,
format_size=lambda size: str(size) if size > 1 else "",
)
def collect_unsized_sampled_texture_globals(self, ast):
globals_by_name = {}
for node in getattr(ast, "global_variables", []) or []:
name = getattr(node, "name", getattr(node, "variable_name", None))
vtype = getattr(node, "var_type", getattr(node, "vtype", None))
if name and self.is_unsized_sampled_texture_array_type(vtype):
globals_by_name[name] = vtype
return globals_by_name
def collect_fixed_resource_global_sizes(self, ast):
global_arrays = {}
for node in getattr(ast, "global_variables", []) or []:
name = getattr(node, "name", getattr(node, "variable_name", None))
vtype = getattr(node, "var_type", getattr(node, "vtype", None))
size = self.fixed_resource_array_size(vtype)
if name and size is not None:
global_arrays[name] = size
return global_arrays
def collect_unsized_sampled_texture_parameters(self, ast):
function_arrays = {}
for func in self.collect_functions(ast):
func_name = getattr(func, "name", None)
if not func_name:
continue
for param in getattr(func, "parameters", getattr(func, "params", [])):
vtype = getattr(param, "param_type", getattr(param, "vtype", None))
if self.is_unsized_sampled_texture_array_type(vtype):
function_arrays.setdefault(func_name, {})[param.name] = vtype
return function_arrays
def collect_fixed_resource_parameter_sizes(self, ast):
function_arrays = {}
for func in self.collect_functions(ast):
func_name = getattr(func, "name", None)
if not func_name:
continue
for param in getattr(func, "parameters", getattr(func, "params", [])):
size = self.fixed_resource_array_size(
getattr(param, "param_type", getattr(param, "vtype", None))
)
if size is not None:
function_arrays.setdefault(func_name, {})[param.name] = size
return function_arrays
def fixed_resource_array_size(self, vtype):
if hasattr(vtype, "element_type") and str(type(vtype)).find("ArrayType") != -1:
if vtype.size is None:
return None
base_type = self.convert_type_node_to_string(vtype.element_type)
if not self.is_inferable_resource_array_type(base_type):
return None
size = self.literal_int_value(vtype.size, self.literal_int_constants)
return size if size is not None and size > 0 else None
if hasattr(vtype, "name") or hasattr(vtype, "element_type"):
return None
type_string = str(vtype)
if "[" not in type_string or "]" not in type_string:
return None
base_type, size = parse_array_type(type_string)
if size is None or not self.is_inferable_resource_array_type(base_type):
return None
return max(size, 1)
def is_unsized_sampled_texture_array_type(self, vtype):
if hasattr(vtype, "element_type") and str(type(vtype)).find("ArrayType") != -1:
if vtype.size is not None:
return False
base_type = self.convert_type_node_to_string(vtype.element_type)
return self.is_inferable_resource_array_type(base_type)
if hasattr(vtype, "name") or hasattr(vtype, "element_type"):
return False
type_string = str(vtype)
if "[" not in type_string or "]" not in type_string:
return False
base_type, size = parse_array_type(type_string)
return size is None and self.is_inferable_resource_array_type(base_type)
def collect_functions(self, root):
functions = []
for node in self.walk_ast(root):
if hasattr(node, "body") and hasattr(node, "parameters"):
functions.append(node)
return functions
def walk_ast(self, root):
visited = set()
def walk(value):
if value is None or isinstance(value, (str, int, float, bool)):
return
if isinstance(value, dict):
for item in value.values():
yield from walk(item)
return
if isinstance(value, (list, tuple, set)):
for item in value:
yield from walk(item)
return
value_id = id(value)
if value_id in visited:
return
visited.add(value_id)
yield value
if hasattr(value, "__dict__"):
for child in vars(value).values():
yield from walk(child)
yield from walk(root)
def is_sampler_type(self, vtype):
return self.resource_base_type(vtype) == "sampler"
def is_sampled_texture_type(self, vtype):
mapped_type = self.map_type(self.resource_base_type(vtype))
return (
mapped_type != "sampler"
and self.is_opaque_resource_type(mapped_type)
and not mapped_type.startswith(("image", "iimage", "uimage"))
)
def is_storage_image_type(self, vtype):
mapped_type = self.map_type(self.resource_base_type(vtype))
return self.is_opaque_resource_type(mapped_type) and mapped_type.startswith(
("image", "iimage", "uimage")
)
def is_inferable_resource_array_type(self, vtype):
return self.is_sampled_texture_type(vtype) or self.is_storage_image_type(vtype)
def is_inferable_resource_type(self, vtype):
return self.is_sampler_type(vtype) or self.is_inferable_resource_array_type(
vtype
)
def resource_base_type(self, vtype):
if vtype is None:
return ""
if hasattr(vtype, "element_type") and str(type(vtype)).find("ArrayType") != -1:
return self.resource_base_type(vtype.element_type)
if hasattr(vtype, "name") or hasattr(vtype, "element_type"):
vtype = self.convert_type_node_to_string(vtype)
vtype = str(vtype)
if "[" in vtype and "]" in vtype:
base_type, _ = parse_array_type(vtype)
return base_type
return vtype
def resource_array_count(self, size):
if size is None:
return 1
resolved_size = self.literal_int_value(size, self.literal_int_constants)
if resolved_size is not None:
return max(resolved_size, 1)
size_str = str(size)
return max(int(size_str), 1) if size_str.isdigit() else 1
def literal_int_value(self, expr, constants=None):
return evaluate_literal_int_expression(expr, constants)
def visible_literal_int_constants(self, func):
visible_constants = dict(self.literal_int_constants)
for param in getattr(func, "parameters", []) or []:
visible_constants.pop(getattr(param, "name", None), None)
for node in self.walk_ast(getattr(func, "body", [])):
if isinstance(node, VariableNode):
name = getattr(node, "name", None)
if not name:
continue
visible_constants.pop(name, None)
if "const" not in getattr(node, "qualifiers", []):
continue
value = self.literal_int_value(
getattr(node, "initial_value", None), visible_constants
)
if value is not None:
visible_constants[name] = value
return visible_constants
def function_call_name(self, call):
func_expr = getattr(call, "function", None)
if func_expr is None:
func_expr = getattr(call, "name", None)
if isinstance(func_expr, str):
return func_expr
if hasattr(func_expr, "name") and isinstance(func_expr.name, str):
return func_expr.name
return None
def map_type(self, vtype):
"""Map types to GLSL equivalents, handling both strings and TypeNode objects."""
if vtype is None:
return "float"
if hasattr(vtype, "name") or hasattr(vtype, "element_type"):
vtype_str = self.convert_type_node_to_string(vtype)
else:
vtype_str = str(vtype)
if "[" in vtype_str and "]" in vtype_str:
base_type, array_suffix = split_array_type_suffix(vtype_str)
base_mapped = self.type_mapping.get(base_type, base_type)
return f"{base_mapped}{array_suffix}"
return self.type_mapping.get(vtype_str, vtype_str)
def map_resource_parameter_type_with_hint(
self, vtype, node=None, function_name=None
):
if vtype is None:
return self.map_type(vtype)
function_hints = self.function_resource_array_size_hints.get(function_name, {})
param_name = getattr(node, "name", None)
if hasattr(vtype, "element_type") and str(type(vtype)).find("ArrayType") != -1:
base_type = self.convert_type_node_to_string(vtype.element_type)
if self.is_inferable_resource_array_type(base_type):
array_size = (
self.expression_to_string(vtype.size)
if vtype.size is not None
else function_hints.get(param_name, "")
)
mapped_type = self.map_image_base_type_with_format(base_type, node)
return (
f"{mapped_type}[{array_size}]" if array_size else f"{mapped_type}[]"
)
if not (hasattr(vtype, "name") or hasattr(vtype, "element_type")):
type_string = str(vtype)
if "[" in type_string and "]" in type_string:
base_type, array_suffix = split_array_type_suffix(type_string)
if self.is_inferable_resource_array_type(base_type):
mapped_type = self.map_image_base_type_with_format(base_type, node)
if array_suffix == "[]":
array_size = function_hints.get(param_name, "")
return (
f"{mapped_type}[{array_size}]"
if array_size
else f"{mapped_type}[]"
)
return f"{mapped_type}{array_suffix}"
return self.map_resource_type_with_format(vtype, node)
def map_resource_type_with_format(self, vtype, node=None):
if vtype is None:
return self.map_type(vtype)
if hasattr(vtype, "name") or hasattr(vtype, "element_type"):
vtype_str = self.convert_type_node_to_string(vtype)
else:
vtype_str = str(vtype)
if "[" in vtype_str and "]" in vtype_str:
base_type, array_suffix = split_array_type_suffix(vtype_str)
base_mapped = self.map_image_base_type_with_format(base_type, node)
return f"{base_mapped}{array_suffix}"
return self.map_image_base_type_with_format(vtype_str, node)
def map_image_base_type_with_format(self, vtype, node=None):
base_type = self.resource_base_type(vtype)
explicit_format = (
self.explicit_image_format_qualifier(node) if node is not None else None
)
if explicit_format in {
"r8",
"r8_snorm",
"r16",
"r16_snorm",
"r16f",
"r32f",
"r8i",
"r16i",
"r32i",
"r8ui",
"r16ui",
"r32ui",
"rg8",
"rg8_snorm",
"rg16",
"rg16_snorm",
"rg16f",
"rg8i",
"rg16i",
"rg8ui",
"rg16ui",
"rg32f",
"rg32i",
"rg32ui",
"rgba8",
"rgba8_snorm",
"rgba16",
"rgba16_snorm",
"rgba16f",
"rgba32f",
"rgba8i",
"rgba16i",
"rgba32i",
"rgba8ui",
"rgba16ui",
"rgba32ui",
}:
if explicit_format in {
"r8",
"r8_snorm",
"r16",
"r16_snorm",
"r16f",
"rg8",
"rg8_snorm",
"rg16",
"rg16_snorm",
"rg16f",
"rg32f",
"rgba8",
"rgba8_snorm",
"rgba16",
"rgba16_snorm",
"rgba16f",
"rgba32f",
}:
format_class = "r32f"
elif explicit_format.endswith("ui"):
format_class = "r32ui"
elif explicit_format.endswith("i"):
format_class = "r32i"
else:
format_class = explicit_format
format_type_map = {
"image2D": {
"r32f": "image2D",
"r32i": "iimage2D",
"r32ui": "uimage2D",
},
"iimage2D": {
"r32f": "image2D",
"r32i": "iimage2D",
"r32ui": "uimage2D",
},
"uimage2D": {
"r32f": "image2D",
"r32i": "iimage2D",
"r32ui": "uimage2D",
},
"image3D": {
"r32f": "image3D",
"r32i": "iimage3D",
"r32ui": "uimage3D",
},
"iimage3D": {
"r32f": "image3D",
"r32i": "iimage3D",
"r32ui": "uimage3D",
},
"uimage3D": {
"r32f": "image3D",
"r32i": "iimage3D",
"r32ui": "uimage3D",
},
"image2DArray": {
"r32f": "image2DArray",
"r32i": "iimage2DArray",
"r32ui": "uimage2DArray",
},
"iimage2DArray": {
"r32f": "image2DArray",
"r32i": "iimage2DArray",
"r32ui": "uimage2DArray",
},
"uimage2DArray": {
"r32f": "image2DArray",
"r32i": "iimage2DArray",
"r32ui": "uimage2DArray",
},
"imageCube": {"r32f": "imageCube"},
}
mapped_type = format_type_map.get(base_type, {}).get(format_class)
if mapped_type:
return mapped_type
return self.map_type(vtype)
def is_opaque_resource_type(self, vtype):
return vtype in {
"sampler1D",
"sampler2D",
"sampler3D",
"samplerCube",
"sampler2DArray",
"samplerCubeArray",
"sampler2DShadow",
"sampler2DArrayShadow",
"samplerCubeShadow",
"samplerCubeArrayShadow",
"sampler2DRect",
"samplerBuffer",
"sampler2DMS",
"sampler2DMSArray",
"isampler2D",
"usampler2D",
"image2D",
"image3D",
"imageCube",
"image2DArray",
"imageBuffer",
"iimage2D",
"iimage3D",
"iimage2DArray",
"uimage2D",
"uimage3D",
"uimage2DArray",
"atomic_uint",
}
def supported_image_formats(self):
return {
"r8",
"r8_snorm",
"r8i",
"r8ui",
"r16",
"r16_snorm",
"r16f",
"r16i",
"r16ui",
"r32f",
"r32i",
"r32ui",
"rg8",
"rg8_snorm",
"rg8i",
"rg8ui",
"rg16",
"rg16_snorm",
"rg16f",
"rg16i",
"rg16ui",
"rg32f",
"rg32i",
"rg32ui",
"rgba8",
"rgba8_snorm",
"rgba8i",
"rgba8ui",
"rgba16",
"rgba16_snorm",
"rgba16f",
"rgba16i",
"rgba16ui",
"rgba32f",
"rgba32i",
"rgba32ui",
}
def attribute_value_to_string(self, value):
if value is None:
return None
if isinstance(value, str):
return value
if hasattr(value, "name"):
return str(value.name)
if hasattr(value, "value"):
return str(value.value).strip('"')
return str(value)
def explicit_image_format_qualifier(self, node):
if not hasattr(node, "attributes"):
return None
supported_formats = self.supported_image_formats()
for attr in node.attributes:
attr_name = getattr(attr, "name", None)
if not attr_name:
continue
attr_name = str(attr_name).lower()
if attr_name in supported_formats:
return attr_name
if attr_name == "format":
arguments = getattr(attr, "arguments", []) or []
if not arguments:
continue
format_name = self.attribute_value_to_string(arguments[0])
if format_name is None:
continue
format_name = str(format_name).lower()
if format_name in supported_formats:
return format_name
return None
def is_image_format_attribute(self, attr):
attr_name = getattr(attr, "name", None)
if not attr_name:
return False
attr_name = str(attr_name).lower()
return attr_name == "format" or attr_name in self.supported_image_formats()
def semantic_from_node(self, node):
if hasattr(node, "semantic"):
return node.semantic
if not hasattr(node, "attributes"):
return None
for attr in node.attributes:
if self.is_image_format_attribute(attr):
continue
if hasattr(attr, "name"):
return attr.name
return None
def image_format_qualifier(self, vtype, node=None):
explicit_format = self.explicit_image_format_qualifier(node)
if explicit_format:
return explicit_format
if vtype in {
"image2D",
"image3D",
"imageCube",
"image2DArray",
}:
return "rgba32f"
if vtype in {
"iimage2D",
"iimage3D",
"iimage2DArray",
}:
return "r32i"
if vtype in {
"uimage2D",
"uimage3D",
"uimage2DArray",
}:
return "r32ui"
return None
def opaque_resource_layout(self, vtype, binding, node=None):
image_format = self.image_format_qualifier(vtype, node)
if image_format:
return f"layout({image_format}, binding = {binding})"
return f"layout(binding = {binding})"
def map_operator(self, op):
op_map = {
"PLUS": "+",
"MINUS": "-",
"MULTIPLY": "*",
"DIVIDE": "/",
"BITWISE_XOR": "^",
"LESS_THAN": "<",
"GREATER_THAN": ">",
"ASSIGN_ADD": "+=",
"ASSIGN_SUB": "-=",
"ASSIGN_OR": "|=",
"ASSIGN_MUL": "*=",
"ASSIGN_DIV": "/=",
"ASSIGN_MOD": "%=",
"ASSIGN_XOR": "^=",
"LESS_EQUAL": "<=",
"GREATER_EQUAL": ">=",
"EQUAL": "==",
"NOT_EQUAL": "!=",
"AND": "&&",
"OR": "||",
"EQUALS": "=",
"MOD": "%",
"BITWISE_OR": "|",
"BITWISE_AND": "&",
"BITWISE_XOR": "^",
"ASSIGN_SHIFT_LEFT": "<<=",
"ASSIGN_SHIFT_RIGHT": ">>=",
"ASSIGN_AND": "&=",
"LOGICAL_AND": "&&",
"ASSIGN_XOR": "^=",
"BITWISE_SHIFT_RIGHT": ">>",
"BITWISE_SHIFT_LEFT": "<<",
}
return op_map.get(op, op)
def map_semantic(self, semantic):
"""Map a CrossGL semantic to the corresponding GLSL builtin or layout."""
if semantic is not None:
return f"{self.semantic_map.get(semantic, semantic)}"
else:
return ""
def convert_type_node_to_string(self, type_node) -> str:
"""Convert new AST TypeNode to string representation."""
if hasattr(type_node, "name"):
return type_node.name
elif hasattr(type_node, "element_type") and hasattr(type_node, "size"):
if hasattr(type_node, "rows"):
element_type = self.convert_type_node_to_string(type_node.element_type)
if type_node.rows == type_node.cols:
return f"mat{type_node.rows}"
else:
return f"mat{type_node.cols}x{type_node.rows}"
elif str(type(type_node)).find("ArrayType") != -1:
element_type = self.convert_type_node_to_string(type_node.element_type)
if type_node.size is not None:
if isinstance(type_node.size, int):
return f"{element_type}[{type_node.size}]"
else:
size_str = self.expression_to_string(type_node.size)
return f"{element_type}[{size_str}]"
else:
return f"{element_type}[]"
else:
element_type = self.convert_type_node_to_string(type_node.element_type)
size = type_node.size
if element_type == "float":
return f"vec{size}"
elif element_type == "int":
return f"ivec{size}"
elif element_type == "uint":
return f"uvec{size}"
elif element_type == "bool":
return f"bvec{size}"
else:
return f"{element_type}{size}"
else:
return str(type_node)
def expression_to_string(self, expr):
"""Convert an expression node to a string representation."""
if hasattr(expr, "value"):
return str(expr.value)
elif getattr(expr, "name", None) is not None:
return str(expr.name)
else:
return self.generate_expression(expr)
def extract_semantic_from_attributes(self, attributes):
"""Extract semantic information from new AST attributes."""
semantic_attrs = [
"position",
"color",
"texcoord",
"normal",
"tangent",
"binormal",
"POSITION",
"COLOR",
"TEXCOORD",
"NORMAL",
"TANGENT",
"BINORMAL",
"TEXCOORD0",
"TEXCOORD1",
"TEXCOORD2",
"TEXCOORD3",
]
for attr in attributes:
if hasattr(attr, "name") and attr.name in semantic_attrs:
return attr.name
return None
def generate_array_declaration(self, stmt, indent):
indent_str = " " * indent
element_type = self.map_type(stmt.element_type)
size = get_array_size_from_node(stmt)
if size is None:
# In GLSL, dynamic sized arrays need special handling
# For instance in shader storage blocks, but for simple cases:
return f"{indent_str}{element_type} {stmt.name}[];\n"
else:
return f"{indent_str}{element_type} {stmt.name}[{size}];\n"
def generate_struct(self, node):
code = f"struct {node.name} {{\n"
members = getattr(node, "members", [])
for member in members:
if isinstance(member, ArrayNode):
element_type = getattr(
member, "element_type", getattr(member, "vtype", "float")
)
if member.size:
code += f" {self.map_type(element_type)} {member.name}[{member.size}];\n"
else:
code += f" {self.map_type(element_type)} {member.name}[];\n"
else:
if hasattr(member, "member_type"):
if str(type(member.member_type)).find("ArrayType") != -1:
# Handle array types with C-style syntax for struct members
element_type = self.convert_type_node_to_string(
member.member_type.element_type
)
element_type = self.map_type(element_type)
if member.member_type.size is not None:
size_str = self.expression_to_string(
member.member_type.size
)
code += f" {element_type} {member.name}[{size_str}];\n"
else:
code += f" {element_type} {member.name}[];\n"
else:
member_type_str = self.convert_type_node_to_string(
member.member_type
)
member_type = self.map_type(member_type_str)
code += f" {member_type} {member.name};\n"
elif hasattr(member, "vtype"):
member_type = self.map_type(member.vtype)
code += f" {member_type} {member.name};\n"
else:
code += f" float {member.name};\n"
code += "};\n"
return code
def generate_expression_statement(self, stmt):
"""Generate code for expression statements."""
if hasattr(stmt, "expression"):
expr = self.generate_expression(stmt.expression)
return expr
else:
# Fallback for direct expression
return self.generate_expression(stmt)