diff --git a/kitty/borders.py b/kitty/borders.py index bdd85af55..3adbc0d35 100644 --- a/kitty/borders.py +++ b/kitty/borders.py @@ -7,7 +7,7 @@ from functools import partial from typing import NamedTuple from .fast_data_types import BORDERS_PROGRAM, current_focused_os_window_id, get_options, init_borders_program, set_borders_rects -from .shaders import program_for +from .shaders.legacy import program_for from .typing_compat import LayoutType from .utils import color_as_int from .window_list import WindowGroup, WindowList diff --git a/kitty/boss.py b/kitty/boss.py index 8ce021a4b..c2016abb5 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -148,7 +148,7 @@ from .session import ( most_recent_session, save_as_session, ) -from .shaders import load_shader_programs +from .shaders.legacy import load_shader_programs from .simple_cli_definitions import grab_keyboard_docs from .tabs import SpecialWindow, SpecialWindowInstance, Tab, TabDict, TabManager from .types import _T, AsyncResponse, LayerShellConfig, SingleInstanceData, WindowSystemMouseEvent, ac diff --git a/kitty/main.py b/kitty/main.py index e0e56a73d..98a092f8f 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -59,7 +59,7 @@ from .options.types import Options from .options.utils import DELETE_ENV_VAR from .os_window_size import edge_spacing, initial_window_size_func from .session import create_sessions, get_os_window_sizing_data -from .shaders import CompileError, load_shader_programs +from .shaders.legacy import CompileError, load_shader_programs from .types import LayerShellConfig from .utils import ( cleanup_ssh_control_masters, diff --git a/kitty/shaders/__init__.py b/kitty/shaders/__init__.py index 7fb10c3c9..e69de29bb 100644 --- a/kitty/shaders/__init__.py +++ b/kitty/shaders/__init__.py @@ -1,230 +0,0 @@ -#!/usr/bin/env python -# License: GPLv3 Copyright: 2023, Kovid Goyal - -import re -from collections.abc import Callable, Iterator -from functools import lru_cache, partial -from itertools import count -from typing import Any, Literal, NamedTuple, Optional - -from kitty.constants import read_kitty_resource -from kitty.fast_data_types import ( - BGIMAGE_PROGRAM, - BLINK, - BLIT_PROGRAM, - CELL_BG_PROGRAM, - CELL_FG_PROGRAM, - CELL_PROGRAM, - COLOR_IS_INDEX, - COLOR_IS_RGB, - COLOR_IS_SPECIAL, - COLOR_NOT_SET, - DECORATION, - DECORATION_MASK, - DIM, - GLSL_VERSION, - GRAPHICS_ALPHA_MASK_PROGRAM, - GRAPHICS_PREMULT_PROGRAM, - GRAPHICS_PROGRAM, - MARK, - MARK_MASK, - REVERSE, - ROUNDED_RECT_PROGRAM, - SCREENSHOT_PROGRAM, - STRIKETHROUGH, - TINT_PROGRAM, - TRAIL_PROGRAM, - compile_program, - get_options, - init_cell_program, -) - - -def identity(x: str) -> str: - return x - - -class CompileError(ValueError): - pass - - -class Program: - - include_pat: Optional['re.Pattern[str]'] = None - filename_number_base: int = 7893000 - - def __init__(self, name: str, vertex_name: str = '', fragment_name: str = '') -> None: - self.name = name - self.filename_number_counter = count(self.filename_number_base + 1) - self.filename_map: dict[str, int] = {} - if Program.include_pat is None: - Program.include_pat = re.compile(r'^#pragma\s+kitty_include_shader\s+<(.+?)>', re.MULTILINE) - self.vertex_name = vertex_name or f'{name}_vertex.glsl' - self.fragment_name = fragment_name or f'{name}_fragment.glsl' - self.original_vertex_sources = tuple(self._load_sources(self.vertex_name, set())) - self.original_fragment_sources = tuple(self._load_sources(self.fragment_name, set())) - self.vertex_sources = self.original_vertex_sources - self.fragment_sources = self.original_fragment_sources - - def _load_sources(self, name: str, seen: set[str], level: int = 0) -> Iterator[str]: - if level == 0: - yield f'#version {GLSL_VERSION}\n' - if name in seen: - return - seen.add(name) - self.filename_map[name] = fnum = next(self.filename_number_counter) - src = read_kitty_resource(name).decode('utf-8') - pos = 0 - lnum = 0 - assert Program.include_pat is not None - for m in Program.include_pat.finditer(src): - prefix = src[pos:m.start()] - if prefix: - yield f'\n#line {lnum} {fnum}\n{prefix}' - lnum += prefix.count('\n') - iname = m.group(1) - yield from self._load_sources(iname, seen, level+1) - pos = m.end() - if pos < len(src): - yield f'\n#line {lnum} {fnum}\n{src[pos:]}' - - def apply_to_sources(self, vertex: Callable[[str], str] = identity, frag: Callable[[str], str] = identity) -> None: - self.vertex_sources = self.original_vertex_sources if vertex is identity else tuple(map(vertex, self.original_vertex_sources)) - self.fragment_sources = self.original_fragment_sources if frag is identity else tuple(map(frag, self.original_fragment_sources)) - - def compile(self, program_id: int, allow_recompile: bool = False) -> None: - cerr: CompileError = CompileError() - try: - compile_program(program_id, self.vertex_sources, self.fragment_sources, allow_recompile) - return - except ValueError as err: - lines = str(err).splitlines() - msg = [] - pat = re.compile(r'\b(' + str(self.filename_number_base).replace('0', r'\d') + r')\b') - rmap = {str(v): k for k, v in self.filename_map.items()} - - def sub(m: 're.Match[str]') -> str: - return rmap.get(m.group(1), m.group(1)) - - for line in lines: - msg.append(pat.sub(sub, line)) - cerr = CompileError('\n'.join(msg)) - raise cerr - - -@lru_cache(maxsize=64) -def program_for(name: str) -> Program: - return Program(name) - - -class MultiReplacer: - - pat: Optional['re.Pattern[str]'] = None - - def __init__(self, **replacements: Any): - self.replacements = {k: str(v) for k, v in replacements.items()} - if MultiReplacer.pat is None: - MultiReplacer.pat = re.compile(r'\{([A-Z_]+)\}') - - def _sub(self, m: 're.Match[str]') -> str: - return self.replacements.get(m.group(1), m.group(1)) - - def __call__(self, src: str) -> str: - assert self.pat is not None - return self.pat.sub(self._sub, src) - -null_replacer = MultiReplacer() - - -class TextFgOverrideThreshold(NamedTuple): - value: float = 0 - unit: Literal['%', 'ratio'] = '%' - scaled_value: float = 0 - - -class LoadShaderPrograms: - text_fg_override_threshold: TextFgOverrideThreshold = TextFgOverrideThreshold() - text_old_gamma: bool = False - cell_program_replacer: MultiReplacer = null_replacer - - @property - def needs_recompile(self) -> bool: - opts = get_options() - return ( - opts.text_fg_override_threshold != (self.text_fg_override_threshold.value, self.text_fg_override_threshold.unit) - or (opts.text_composition_strategy == 'legacy') != self.text_old_gamma - ) - - def recompile_if_needed(self) -> None: - if self.needs_recompile: - self(allow_recompile=True) - - def __call__(self, allow_recompile: bool = False) -> None: - opts = get_options() - self.text_old_gamma = opts.text_composition_strategy == 'legacy' - - text_fg_override_threshold: float = opts.text_fg_override_threshold[0] - match opts.text_fg_override_threshold[1]: - case '%': - text_fg_override_threshold = max(0, min(text_fg_override_threshold, 100.0)) * 0.01 - case 'ratio': - text_fg_override_threshold = max(0, min(text_fg_override_threshold, 21.0)) - self.text_fg_override_threshold = TextFgOverrideThreshold( - opts.text_fg_override_threshold[0], opts.text_fg_override_threshold[1], text_fg_override_threshold) - - cell = program_for('cell') - if self.cell_program_replacer is null_replacer: - self.cell_program_replacer = MultiReplacer( - REVERSE_SHIFT=REVERSE, - STRIKE_SHIFT=STRIKETHROUGH, - DIM_SHIFT=DIM, - BLINK_SHIFT=BLINK, - DECORATION_SHIFT=DECORATION, - MARK_SHIFT=MARK, - MARK_MASK=MARK_MASK, - DECORATION_MASK=DECORATION_MASK, - COLOR_NOT_SET=COLOR_NOT_SET, - COLOR_IS_SPECIAL=COLOR_IS_SPECIAL, - COLOR_IS_INDEX=COLOR_IS_INDEX, - COLOR_IS_RGB=COLOR_IS_RGB, - ) - - def resolve_cell_defines(only_fg: int, only_bg: int, src: str) -> str: - r = self.cell_program_replacer.replacements - r['ONLY_FOREGROUND'] = str(only_fg) - r['ONLY_BACKGROUND'] = str(only_bg) - r['DO_FG_OVERRIDE'] = '1' if self.text_fg_override_threshold.scaled_value else '0' - r['FG_OVERRIDE_ALGO'] = '1' if self.text_fg_override_threshold.unit == '%' else '2' - r['FG_OVERRIDE_THRESHOLD'] = str(self.text_fg_override_threshold.scaled_value) - r['TEXT_NEW_GAMMA'] = '0' if self.text_old_gamma else '1' - return self.cell_program_replacer(src) - for prog, (only_fg, only_bg) in { - CELL_PROGRAM: (0, 0), CELL_FG_PROGRAM: (1, 0), CELL_BG_PROGRAM: (0, 1), - }.items(): - fn = partial(resolve_cell_defines, only_fg, only_bg) - cell.apply_to_sources(vertex=fn, frag=fn) - cell.compile(prog, allow_recompile) - graphics = program_for('graphics') - - def resolve_graphics_fragment_defines(which: str, is_premult: bool, f: str) -> str: - ans = f.replace('#define ALPHA_TYPE', f'#define {which}', 1) - return ans.replace('TEXTURE_IS_NOT_PREMULTIPLIED', '0' if is_premult else '1') - - for p, (which, is_premult) in { - GRAPHICS_PROGRAM: ('IMAGE', False), - GRAPHICS_ALPHA_MASK_PROGRAM: ('ALPHA_MASK', False), - GRAPHICS_PREMULT_PROGRAM: ('IMAGE', True), - }.items(): - graphics.apply_to_sources(frag=partial(resolve_graphics_fragment_defines, which, is_premult)) - graphics.compile(p, allow_recompile) - - program_for('bgimage').compile(BGIMAGE_PROGRAM, allow_recompile) - program_for('tint').compile(TINT_PROGRAM, allow_recompile) - program_for('trail').compile(TRAIL_PROGRAM, allow_recompile) - program_for('blit').compile(BLIT_PROGRAM, allow_recompile) - program_for('screenshot').compile(SCREENSHOT_PROGRAM, allow_recompile) - program_for('rounded_rect').compile(ROUNDED_RECT_PROGRAM, allow_recompile) - init_cell_program() - - -load_shader_programs = LoadShaderPrograms() diff --git a/kitty/shaders/legacy.py b/kitty/shaders/legacy.py new file mode 100644 index 000000000..7fb10c3c9 --- /dev/null +++ b/kitty/shaders/legacy.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2023, Kovid Goyal + +import re +from collections.abc import Callable, Iterator +from functools import lru_cache, partial +from itertools import count +from typing import Any, Literal, NamedTuple, Optional + +from kitty.constants import read_kitty_resource +from kitty.fast_data_types import ( + BGIMAGE_PROGRAM, + BLINK, + BLIT_PROGRAM, + CELL_BG_PROGRAM, + CELL_FG_PROGRAM, + CELL_PROGRAM, + COLOR_IS_INDEX, + COLOR_IS_RGB, + COLOR_IS_SPECIAL, + COLOR_NOT_SET, + DECORATION, + DECORATION_MASK, + DIM, + GLSL_VERSION, + GRAPHICS_ALPHA_MASK_PROGRAM, + GRAPHICS_PREMULT_PROGRAM, + GRAPHICS_PROGRAM, + MARK, + MARK_MASK, + REVERSE, + ROUNDED_RECT_PROGRAM, + SCREENSHOT_PROGRAM, + STRIKETHROUGH, + TINT_PROGRAM, + TRAIL_PROGRAM, + compile_program, + get_options, + init_cell_program, +) + + +def identity(x: str) -> str: + return x + + +class CompileError(ValueError): + pass + + +class Program: + + include_pat: Optional['re.Pattern[str]'] = None + filename_number_base: int = 7893000 + + def __init__(self, name: str, vertex_name: str = '', fragment_name: str = '') -> None: + self.name = name + self.filename_number_counter = count(self.filename_number_base + 1) + self.filename_map: dict[str, int] = {} + if Program.include_pat is None: + Program.include_pat = re.compile(r'^#pragma\s+kitty_include_shader\s+<(.+?)>', re.MULTILINE) + self.vertex_name = vertex_name or f'{name}_vertex.glsl' + self.fragment_name = fragment_name or f'{name}_fragment.glsl' + self.original_vertex_sources = tuple(self._load_sources(self.vertex_name, set())) + self.original_fragment_sources = tuple(self._load_sources(self.fragment_name, set())) + self.vertex_sources = self.original_vertex_sources + self.fragment_sources = self.original_fragment_sources + + def _load_sources(self, name: str, seen: set[str], level: int = 0) -> Iterator[str]: + if level == 0: + yield f'#version {GLSL_VERSION}\n' + if name in seen: + return + seen.add(name) + self.filename_map[name] = fnum = next(self.filename_number_counter) + src = read_kitty_resource(name).decode('utf-8') + pos = 0 + lnum = 0 + assert Program.include_pat is not None + for m in Program.include_pat.finditer(src): + prefix = src[pos:m.start()] + if prefix: + yield f'\n#line {lnum} {fnum}\n{prefix}' + lnum += prefix.count('\n') + iname = m.group(1) + yield from self._load_sources(iname, seen, level+1) + pos = m.end() + if pos < len(src): + yield f'\n#line {lnum} {fnum}\n{src[pos:]}' + + def apply_to_sources(self, vertex: Callable[[str], str] = identity, frag: Callable[[str], str] = identity) -> None: + self.vertex_sources = self.original_vertex_sources if vertex is identity else tuple(map(vertex, self.original_vertex_sources)) + self.fragment_sources = self.original_fragment_sources if frag is identity else tuple(map(frag, self.original_fragment_sources)) + + def compile(self, program_id: int, allow_recompile: bool = False) -> None: + cerr: CompileError = CompileError() + try: + compile_program(program_id, self.vertex_sources, self.fragment_sources, allow_recompile) + return + except ValueError as err: + lines = str(err).splitlines() + msg = [] + pat = re.compile(r'\b(' + str(self.filename_number_base).replace('0', r'\d') + r')\b') + rmap = {str(v): k for k, v in self.filename_map.items()} + + def sub(m: 're.Match[str]') -> str: + return rmap.get(m.group(1), m.group(1)) + + for line in lines: + msg.append(pat.sub(sub, line)) + cerr = CompileError('\n'.join(msg)) + raise cerr + + +@lru_cache(maxsize=64) +def program_for(name: str) -> Program: + return Program(name) + + +class MultiReplacer: + + pat: Optional['re.Pattern[str]'] = None + + def __init__(self, **replacements: Any): + self.replacements = {k: str(v) for k, v in replacements.items()} + if MultiReplacer.pat is None: + MultiReplacer.pat = re.compile(r'\{([A-Z_]+)\}') + + def _sub(self, m: 're.Match[str]') -> str: + return self.replacements.get(m.group(1), m.group(1)) + + def __call__(self, src: str) -> str: + assert self.pat is not None + return self.pat.sub(self._sub, src) + +null_replacer = MultiReplacer() + + +class TextFgOverrideThreshold(NamedTuple): + value: float = 0 + unit: Literal['%', 'ratio'] = '%' + scaled_value: float = 0 + + +class LoadShaderPrograms: + text_fg_override_threshold: TextFgOverrideThreshold = TextFgOverrideThreshold() + text_old_gamma: bool = False + cell_program_replacer: MultiReplacer = null_replacer + + @property + def needs_recompile(self) -> bool: + opts = get_options() + return ( + opts.text_fg_override_threshold != (self.text_fg_override_threshold.value, self.text_fg_override_threshold.unit) + or (opts.text_composition_strategy == 'legacy') != self.text_old_gamma + ) + + def recompile_if_needed(self) -> None: + if self.needs_recompile: + self(allow_recompile=True) + + def __call__(self, allow_recompile: bool = False) -> None: + opts = get_options() + self.text_old_gamma = opts.text_composition_strategy == 'legacy' + + text_fg_override_threshold: float = opts.text_fg_override_threshold[0] + match opts.text_fg_override_threshold[1]: + case '%': + text_fg_override_threshold = max(0, min(text_fg_override_threshold, 100.0)) * 0.01 + case 'ratio': + text_fg_override_threshold = max(0, min(text_fg_override_threshold, 21.0)) + self.text_fg_override_threshold = TextFgOverrideThreshold( + opts.text_fg_override_threshold[0], opts.text_fg_override_threshold[1], text_fg_override_threshold) + + cell = program_for('cell') + if self.cell_program_replacer is null_replacer: + self.cell_program_replacer = MultiReplacer( + REVERSE_SHIFT=REVERSE, + STRIKE_SHIFT=STRIKETHROUGH, + DIM_SHIFT=DIM, + BLINK_SHIFT=BLINK, + DECORATION_SHIFT=DECORATION, + MARK_SHIFT=MARK, + MARK_MASK=MARK_MASK, + DECORATION_MASK=DECORATION_MASK, + COLOR_NOT_SET=COLOR_NOT_SET, + COLOR_IS_SPECIAL=COLOR_IS_SPECIAL, + COLOR_IS_INDEX=COLOR_IS_INDEX, + COLOR_IS_RGB=COLOR_IS_RGB, + ) + + def resolve_cell_defines(only_fg: int, only_bg: int, src: str) -> str: + r = self.cell_program_replacer.replacements + r['ONLY_FOREGROUND'] = str(only_fg) + r['ONLY_BACKGROUND'] = str(only_bg) + r['DO_FG_OVERRIDE'] = '1' if self.text_fg_override_threshold.scaled_value else '0' + r['FG_OVERRIDE_ALGO'] = '1' if self.text_fg_override_threshold.unit == '%' else '2' + r['FG_OVERRIDE_THRESHOLD'] = str(self.text_fg_override_threshold.scaled_value) + r['TEXT_NEW_GAMMA'] = '0' if self.text_old_gamma else '1' + return self.cell_program_replacer(src) + for prog, (only_fg, only_bg) in { + CELL_PROGRAM: (0, 0), CELL_FG_PROGRAM: (1, 0), CELL_BG_PROGRAM: (0, 1), + }.items(): + fn = partial(resolve_cell_defines, only_fg, only_bg) + cell.apply_to_sources(vertex=fn, frag=fn) + cell.compile(prog, allow_recompile) + graphics = program_for('graphics') + + def resolve_graphics_fragment_defines(which: str, is_premult: bool, f: str) -> str: + ans = f.replace('#define ALPHA_TYPE', f'#define {which}', 1) + return ans.replace('TEXTURE_IS_NOT_PREMULTIPLIED', '0' if is_premult else '1') + + for p, (which, is_premult) in { + GRAPHICS_PROGRAM: ('IMAGE', False), + GRAPHICS_ALPHA_MASK_PROGRAM: ('ALPHA_MASK', False), + GRAPHICS_PREMULT_PROGRAM: ('IMAGE', True), + }.items(): + graphics.apply_to_sources(frag=partial(resolve_graphics_fragment_defines, which, is_premult)) + graphics.compile(p, allow_recompile) + + program_for('bgimage').compile(BGIMAGE_PROGRAM, allow_recompile) + program_for('tint').compile(TINT_PROGRAM, allow_recompile) + program_for('trail').compile(TRAIL_PROGRAM, allow_recompile) + program_for('blit').compile(BLIT_PROGRAM, allow_recompile) + program_for('screenshot').compile(SCREENSHOT_PROGRAM, allow_recompile) + program_for('rounded_rect').compile(ROUNDED_RECT_PROGRAM, allow_recompile) + init_cell_program() + + +load_shader_programs = LoadShaderPrograms()