From bf6281f8f47ac78eea9596b99c21cf63be39f23e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 7 Jul 2026 08:05:55 +0530 Subject: [PATCH] Map vertex outputs to fragment inputs --- kitty/shaders/slang.py | 84 +++++++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 22 deletions(-) diff --git a/kitty/shaders/slang.py b/kitty/shaders/slang.py index 9f91f5dfa..415b689da 100644 --- a/kitty/shaders/slang.py +++ b/kitty/shaders/slang.py @@ -510,12 +510,14 @@ class GLSLMetadata: uniform_structs: dict[str, dict[str, str]] input_locations: dict[str, int] uniform_struct_names: dict[str, str] + fragment_inputs: dict[int, str] def __init__(self) -> None: self.loose_uniforms = {} self.uniform_structs = {} self.input_locations = {} self.uniform_struct_names = {} + self.fragment_inputs = {} def merge(self, other: 'GLSLMetadata') -> None: self.loose_uniforms.update(other.loose_uniforms) @@ -528,7 +530,8 @@ class GLSLMetadata: 'loose_uniforms': self.loose_uniforms, 'uniform_structs': self.uniform_structs, 'input_locations': self.input_locations, - 'uniform_struct_names': self.uniform_struct_names + 'uniform_struct_names': self.uniform_struct_names, + 'fragment_inputs': self.fragment_inputs, } @classmethod @@ -538,11 +541,13 @@ class GLSLMetadata: ans.uniform_structs = d['uniform_structs'] ans.input_locations = d['input_locations'] ans.uniform_struct_names = d['uniform_struct_names'] + ans.fragment_inputs = d['fragment_inputs'] return ans -def fixup_opengl_code(glsl_code: str, path: str) -> tuple[str, GLSLMetadata]: - is_fragment_shader = 'frag' in os.path.basename(path).split('.') +def fixup_opengl_code(glsl_code: str, shader_name: str, existing_metadata: GLSLMetadata | None) -> tuple[str, GLSLMetadata]: + is_fragment_shader = existing_metadata is None + shader_name += '.frag.glsl' if is_fragment_shader else '.vert.glsl' lines: list[str] = [] in_uniform_block = False in_uniform_block_contents = False @@ -556,12 +561,32 @@ def fixup_opengl_code(glsl_code: str, path: str) -> tuple[str, GLSLMetadata]: uniform_struct_names = {} input_locations = {} named_interface_blocks = set() + pipeline_io_vars: dict[int, str] = {} + replacements = { + 'gl_VertexIndex': 'gl_VertexID', + 'gl_BaseVertex': '0', + 'gl_InstanceIndex': 'gl_InstanceID', + 'gl_BaseInstance': '0', + } + fragment_inputs = {} if existing_metadata is None else existing_metadata.fragment_inputs.copy() + + def register_pipeline_boundary_io(line: str, next_line: str) -> None: + m = re.search(r'location = (\d+)', line) + assert m is not None + name = next_line.split()[-1].rstrip(';') + location = int(m.group(1)) + if existing_metadata is None: + if not next_line.startswith('out '): + pipeline_io_vars[location] = name + else: + with suppress(KeyError): + replacements[name] = fragment_inputs.pop(location) def add_uniform_name(name: str, uniform_names: dict[str, str] = uniform_names) -> str: name = name.rstrip(';') uniform_name = name.rpartition('_')[0] if uniform_name in uniform_names: - raise KeyError(f'The uniform name {uniform_name} is used with multiple suffixes in {path}') + raise KeyError(f'The uniform name {uniform_name} is used with multiple suffixes in {shader_name}') if '[' in name: name = name.partition('[')[0] + '[0]' uniform_names[uniform_name] = name @@ -604,9 +629,11 @@ def fixup_opengl_code(glsl_code: str, path: str) -> tuple[str, GLSLMetadata]: line = '// ' + line elif line.startswith('layout(binding ='): line = '// ' + line - elif line.startswith('layout(location =') and (is_fragment_shader or next_line.startswith('out ')): + elif line.startswith('layout(location =') and (is_fragment_shader or next_line.startswith('out ')): # )) + register_pipeline_boundary_io(line, next_line) line = '// ' + line elif line.startswith('flat layout(location ='): + register_pipeline_boundary_io(line[len('flat '):], next_line) line = 'flat' elif line: # )))) words = line.split() @@ -628,46 +655,57 @@ def fixup_opengl_code(glsl_code: str, path: str) -> tuple[str, GLSLMetadata]: name = words[-1].rstrip(';') input_locations[name.rpartition('_')[0]] = int(lines[-1].split()[-1].rstrip(')')) lines.append(line) + if fragment_inputs: + raise ValueError( + f'Could not match vertex outputs to fragment inputs for shader: {shader_name}. Leftover fragment inputs: {", ".join(fragment_inputs.values())}') ans = '\n'.join(lines) for block_name, names in uniform_blocks.items(): for u in names: u = u.partition('[')[0] # ] - ans = ans.replace(f'{block_name}.{u}', u) - ans = ans.replace('gl_VertexIndex', 'gl_VertexID') - ans = ans.replace('gl_BaseVertex', '0') - ans = ans.replace('gl_InstanceIndex', 'gl_InstanceID') - ans = ans.replace('gl_BaseInstance', '0') + replacements[f'{block_name}.{u}'] = u + for x in named_interface_blocks: - ans = re.sub(rf'\b{x}\.', '', ans) + replacements[f'{x}.'] = '' + + def sub(m: re.Match[str]) -> str: + return replacements[m.group(1)] + ans = re.sub(r'\b(' + '|'.join(re.escape(word) for word in replacements) + r')\b', sub, ans) m = GLSLMetadata() m.loose_uniforms = uniform_names m.uniform_structs = uniform_structs m.input_locations = input_locations m.uniform_struct_names = uniform_struct_names + if is_fragment_shader: + m.fragment_inputs = pipeline_io_vars return ans, m +def shader_name_from_path(path: str) -> str: + parts = os.path.basename(path).split('.') + if parts[1] in ('vert', 'frag', 'glsl'): + return parts[0] + return '.'.join(parts[:2]) + + def fixup_opengl_files(paths: Iterable[str]) -> None: ' Convert the GLSL output of slangc to something that will work with OpenGL 3.1 ' metadata_map: dict[str, GLSLMetadata] = {} dest_dir = '' - for path in paths: + for path in sorted(paths): dest_dir = os.path.dirname(path) with open(path, 'r') as f: glsl_code = f.read() + shader_name = shader_name_from_path(path) try: - fixed, metadata = fixup_opengl_code(glsl_code, path) + fixed, metadata = fixup_opengl_code(glsl_code, shader_name, metadata_map.get(shader_name)) except Exception: os.unlink(path) raise - parts = os.path.basename(path).split('.') write_if_changed(path, fixed) - if len(parts) == 3: - name = parts[0] - if name in metadata_map: - metadata_map[name].merge(metadata) - else: - metadata_map[name] = metadata + if shader_name in metadata_map: + metadata_map[shader_name].merge(metadata) + else: + metadata_map[shader_name] = metadata for name, gm in metadata_map.items(): with open(os.path.join(dest_dir, f'{name}.glsl.json'), 'w') as f: f.write(json.dumps(gm.asdict())) @@ -685,14 +723,16 @@ def write_if_changed(dest: str, text: str) -> None: def write_glsl_header(dest_dir: str, dest: str = 'kitty/glsl-uniforms.h') -> None: metadata_map = {} for x in glob.glob(os.path.join(dest_dir, '*.glsl.json')): - name = os.path.basename(x).split('.')[0] + shader_name = shader_name_from_path(x) with open(x) as f: d = json.load(f) - metadata_map[name] = GLSLMetadata.fromdict(d) + metadata_map[shader_name] = GLSLMetadata.fromdict(d) lines = ['// generated by slang.py DO NOT EDIT', '#include "gl.h"', ''] a = lines.append for name in sorted(metadata_map): + if '.' in name: + continue m = metadata_map[name] struct_name = name.capitalize() + 'Uniforms' a('')