diff --git a/kitty/shaders/cell.slang b/kitty/shaders/cell.slang index be379b85b..2b5632ebd 100644 --- a/kitty/shaders/cell.slang +++ b/kitty/shaders/cell.slang @@ -12,13 +12,10 @@ import linear2srgb; #define NUM_COLORS 256 -extern static const bool DO_FG_OVERRIDE; extern static const uint FG_OVERRIDE_ALGO; -extern static const float FG_OVERRIDE_THRESHOLD; extern static const bool TEXT_NEW_GAMMA; extern static const bool ONLY_FOREGROUND; extern static const bool ONLY_BACKGROUND; -static const bool OVERRIDE_FG_COLORS = DO_FG_OVERRIDE == 1 && !ONLY_BACKGROUND; // Inputs {{{ struct CellRenderDataStruct @@ -343,20 +340,20 @@ float calc_background_opacity(uint bg) { } // Override foreground colors {{{ -float3 fg_override_luminance(float colored_sprite, float under_luminance, float over_lumininace, float3 under, float3 over) { +float3 fg_override_luminance(float colored_sprite, float under_luminance, float over_lumininace, float3 under, float3 over, float fg_override_thresold) { // If the difference in luminance is too small, // force the foreground color to be black or white. float diff_luminance = abs(under_luminance - over_lumininace); - float override_level = (1.f - colored_sprite) * step(diff_luminance, FG_OVERRIDE_THRESHOLD); + float override_level = (1.f - colored_sprite) * step(diff_luminance, fg_override_thresold); float original_level = 1.f - override_level; return original_level * over + override_level * float3(step(under_luminance, 0.5f)); } -float3 fg_override_contrast(float under_luminance, float over_luminance, float3 under, float3 over) { +float3 fg_override_contrast(float under_luminance, float over_luminance, float3 under, float3 over, float fg_override_thresold) { float ratio = contrast_ratio(under_luminance, over_luminance); float3 diff = abs(under - over); float3 over_hsluv = rgbToHsluv(over); - const float min_contrast_ratio = FG_OVERRIDE_THRESHOLD; + const float min_contrast_ratio = fg_override_thresold; float target_lum_a = clamp((under_luminance + 0.05) * min_contrast_ratio - 0.05, 0.0, 1.0); float target_lum_b = clamp((under_luminance + 0.05) / min_contrast_ratio - 0.05, 0.0, 1.0); float3 result_a = clamp(hsluvToRgb(float3(over_hsluv.x, over_hsluv.y, target_lum_a * 100.0)), 0.0, 1.0); @@ -368,11 +365,11 @@ float3 fg_override_contrast(float under_luminance, float over_luminance, float3 return lerp(result, over, fallback_condition); } -float3 override_foreground_color(float3 over, float3 under, float colored_sprite) { +float3 override_foreground_color(float3 over, float3 under, float colored_sprite, float fg_override_thresold) { float under_luminance = dot(under, Y); float over_lumininace = dot(over.rgb, Y); - if (FG_OVERRIDE_ALGO == 1) return fg_override_luminance(colored_sprite, under_luminance, over_lumininace, under, over); - return fg_override_contrast(under_luminance, over_lumininace, under, over); + if (FG_OVERRIDE_ALGO == 1) return fg_override_luminance(colored_sprite, under_luminance, over_lumininace, under, over, fg_override_thresold); + return fg_override_contrast(under_luminance, over_lumininace, under, over, fg_override_thresold); } // }}} @@ -383,6 +380,7 @@ VertexOutput vertex_main( uint instance_id : SV_InstanceID, uniform uint draw_bg_bitfield, uniform float row_offset, + uniform float fg_override_thresold, ) { VertexOutput vo; @@ -452,9 +450,9 @@ VertexOutput vertex_main( vo.background = background_rgb; // }}} - if (!ONLY_BACKGROUND && OVERRIDE_FG_COLORS) { - vo.decoration_fg = override_foreground_color(vo.decoration_fg, background_rgb, vo.colored_sprite); - foreground = override_foreground_color(foreground, background_rgb, vo.colored_sprite); + if (!ONLY_BACKGROUND && FG_OVERRIDE_ALGO > 0) { + vo.decoration_fg = override_foreground_color(vo.decoration_fg, background_rgb, vo.colored_sprite, fg_override_thresold); + foreground = override_foreground_color(foreground, background_rgb, vo.colored_sprite, fg_override_thresold); } if (!ONLY_FOREGROUND) { diff --git a/kitty/shaders/slang.py b/kitty/shaders/slang.py index 0c601d1e0..a0edc76cb 100644 --- a/kitty/shaders/slang.py +++ b/kitty/shaders/slang.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # License: GPLv3 Copyright: 2026, Kovid Goyal -import fcntl +import hashlib import json import os import re @@ -10,10 +10,10 @@ import shutil import sys import time from collections import OrderedDict -from contextlib import contextmanager, suppress +from contextlib import suppress from enum import StrEnum from functools import lru_cache -from itertools import chain +from itertools import chain, product from pathlib import Path from types import MappingProxyType from typing import Any, Callable, Iterable, Iterator, NamedTuple @@ -33,8 +33,6 @@ from kitty.fast_data_types import ( MARK_MASK, REVERSE, STRIKETHROUGH, - get_boss, - get_options, ) from kitty.options.types import Options, defaults @@ -99,6 +97,40 @@ class Specialization(NamedTuple): return f'.{self.name}' if self.name else '.default-specialization' +def cell_variant(opts: Options = defaults, only_fg: bool = False, only_bg: bool = False) -> dict[str, str]: + text_fg_override_threshold: float = opts.text_fg_override_threshold[0] + algo = '0' + match opts.text_fg_override_threshold[1]: + case '%': + text_fg_override_threshold = max(0, min(text_fg_override_threshold, 100.0)) * 0.01 + algo = '1' + case 'ratio': + text_fg_override_threshold = max(0, min(text_fg_override_threshold, 21.0)) + algo = '2' + return { + 'FG_OVERRIDE_ALGO': algo, + 'TEXT_NEW_GAMMA': 'false' if opts.text_composition_strategy == 'legacy' else 'true', + 'ONLY_FOREGROUND': 'true' if only_fg else 'false', + 'ONLY_BACKGROUND': 'true' if only_bg else 'false', + } + + +@lru_cache(maxsize=2) +def cell_variations() -> tuple[MappingProxyType[str, str], ...]: + variations = {'FG_OVERRIDE_ALGO': ('0', '1', '2')} + bool_variations = 'false', 'true' + variants_dict = {k: variations.get(k, bool_variations) for k in cell_variant()} + return tuple(MappingProxyType(dict(zip(variants_dict.keys(), comb))) for comb in product(*variants_dict.values())) + + +def variant_name(variant: dict[str, str], default: dict[str, str]) -> str: + if variant == default: + return '' + data = ' '.join(f'{k}={variant[k]}' for k in sorted(default)).encode() + key = hashlib.md5(data, usedforsecurity=False) + return key.hexdigest()[:5] + + class SlangFile(NamedTuple): path: str = '' text: str = '' @@ -108,8 +140,6 @@ class SlangFile(NamedTuple): specializable_variables: MappingProxyType[str, str] = MappingProxyType({}) disable_warnings: frozenset[str] = frozenset() - opts: Options | None = None - def asdict(self, skip_source: bool = False) -> dict[str, Any]: ' Return a dict useable for serialization to JSON ' ans = self._asdict() @@ -120,7 +150,6 @@ class SlangFile(NamedTuple): if skip_source: ans['text'] = '' ans['path'] = os.path.basename(ans['path']) - del ans['opts'] return ans @classmethod @@ -153,9 +182,6 @@ class SlangFile(NamedTuple): ans['COLOR_IS_RGB'] = str(COLOR_IS_RGB) return MappingProxyType(ans) - def get_options(self) -> Options: - return self.opts or defaults - @property def specializations(self) -> Iterator[Specialization]: def s(name: str = '', **kwargs: str) -> Specialization: @@ -167,29 +193,14 @@ class SlangFile(NamedTuple): yield s('alpha_mask', is_alpha_mask='true') yield s('premult', texture_is_not_premultiplied='true') case 'cell.slang': - opts = self.get_options() - 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 - algo = '1' - case 'ratio': - text_fg_override_threshold = max(0, min(text_fg_override_threshold, 21.0)) - algo = '2' - base = {k:str(v) for k, v in dict( - DO_FG_OVERRIDE='true' if text_fg_override_threshold else 'false', - FG_OVERRIDE_ALGO=algo, - FG_OVERRIDE_THRESHOLD=text_fg_override_threshold, - TEXT_NEW_GAMMA='false' if opts.text_composition_strategy == 'legacy' else 'true', - ONLY_FOREGROUND='false', - ONLY_BACKGROUND='false', - ).items()} - yield s('', **base) - base['ONLY_FOREGROUND'] = 'true' - yield s('fg', **base) - base['ONLY_FOREGROUND'] = 'false' - base['ONLY_BACKGROUND'] = 'true' - yield s('bg', **base) + d = cell_variant() + seen = set() + for variant in cell_variations(): + name = variant_name(dict(variant), d) + if name in seen: + raise Exception('Variant names for cell shader not unique') + seen.add(name) + yield s(name, **variant) case _: yield s() @@ -575,106 +586,6 @@ def compile_builtin_shaders(build_dir: str, dest_dir: str, parallel_run: Paralle parallel_run((True, f'Validating |{os.path.basename(x)}| ...', validation_command_for_file(x)) for x in built_glsl_files) -@contextmanager -def lock_directory(target_dir: str) -> Iterator[None]: - ''' - Context manager to exclusively lock a directory using a hidden lock file. - Works across all Unix-like operating systems. - ''' - os.makedirs(target_dir, exist_ok=True) - lock_file_path = os.path.join(target_dir, '.shaders.lock') - lock_fd = os.open(lock_file_path, os.O_CREAT | os.O_WRONLY) - try: - fcntl.flock(lock_fd, fcntl.LOCK_EX) - yield - finally: - fcntl.flock(lock_fd, fcntl.LOCK_UN) - os.close(lock_fd) - - -def run_commands(cmds: Iterable[Command], cwd: str | None = None) -> None: - import subprocess - workers = [] - for c in cmds: - if c.needs_build: - try: - p = subprocess.Popen(c.cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, cwd=cwd) - except FileNotFoundError: - raise Exception(f'Could not find slangc compiler ({slangc}) in PATH: {os.environ.get("PATH")}') - workers.append((c, p)) - errors = [] - for (c, p) in workers: - if p.wait() != 0: - assert p.stderr is not None - stderr = p.stderr.read().decode('utf-8', 'replace') - errors.append((c, stderr)) - if errors: - raise Exception(f'Compiling shader failed. Command that was run: {errors[0][0]}\n{errors[0][1]}') - - -def specialize_shaders_to(sources: dict[str, SlangFile], dest_dir: str) -> None: - for name in sources: - shutil.copy2(os.path.join(shaders_dir, f'{name}.slang-module'), dest_dir) - specialisation_cmds = create_specialisations(sources, dest_dir) - run_commands(specialisation_cmds, dest_dir) - _ = [] - spirv = commands_to_compile_to_spirv(sources, dest_dir, _) - glsl_built_files: list[str] = [] - glsl = commands_to_compile_to_glsl(sources, dest_dir, glsl_built_files) - run_commands(chain(spirv, glsl), dest_dir) - fixup_opengl_files(*glsl_built_files) - - -@lru_cache(maxsize=2) -def per_process_cache_dir() -> str: - ' A dir that has the lifetime of this process ' - import atexit - from tempfile import mkdtemp - ans = mkdtemp() - boss = get_boss() - try: - boss.atexit.rmtree(ans) - except Exception: # happens if no boss exists - atexit.register(shutil.rmtree, ans) - return ans - - -specialize_cache: dict[str, tuple[tuple[Specialization, ...], dict[str, bytes]]] = {} - - -def specialize_cell_shader( - create_cache_dir: Callable[[], str] = per_process_cache_dir, - opts: Options | None = None -) -> dict[str, bytes]: - ' Specialize the cell shader based on the specified options ' - with open(os.path.join(shaders_dir, 'cell.json')) as f: - builtin_sfile = SlangFile.fromdict(json.load(f)) - d = builtin_sfile._asdict() - if opts is None: - with suppress(RuntimeError): - opts = get_options() - d['opts'] = opts - sfile = SlangFile(**d) - builtin, current = tuple(builtin_sfile.specializations), tuple(sfile.specializations) - if builtin == current: # options not changed from defaults - return {} - dest_dir = create_cache_dir() - cache_key = f'cell-{dest_dir}' - if (cx := specialize_cache.get(cache_key)) and cx[0] == current: - return cx[1] - - with lock_directory(dest_dir): - ensure_cache_dir(dest_dir) - specialize_shaders_to({'cell': sfile}, dest_dir) - ans = {} - for x in os.listdir(dest_dir): - if x.rpartition('.')[2] in ('spv', 'glsl', 'msl'): - with open(os.path.join(dest_dir, x), 'rb') as fb: - ans[x] = fb.read() - specialize_cache[cache_key] = (current, ans) - return ans - - def main() -> None: if not shutil.which(slangc[0]): raise SystemExit(f'The shader slang compiler ({slangc[0]}) not in PATH: {os.environ.get("PATH")}')