mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-03 05:03:39 +02:00
Compare commits
1 Commits
copilot/ad
...
slang
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9664f35596 |
@@ -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) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2026, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
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")}')
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# License: GPLv3 Copyright: 2026, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from kitty.shaders.slang import EntryPoint, SlangFile, Stage, build_import_graph, parse_slang_text, topological_sort
|
||||
@@ -160,85 +159,3 @@ void vsMain() {}
|
||||
f.write('not a slang file\n')
|
||||
graph3 = build_import_graph(tmpdir)
|
||||
self.assertNotIn('ignored', graph3)
|
||||
|
||||
def test_specialize_cell_shader(self):
|
||||
from kitty.constants import slangc
|
||||
from kitty.options.types import Options, defaults
|
||||
from kitty.shaders.slang import specialize_cell_shader
|
||||
|
||||
def make_opts(**kwargs):
|
||||
d = defaults._asdict()
|
||||
d.update(kwargs)
|
||||
return Options(d)
|
||||
|
||||
# No action when opts are the same as defaults
|
||||
self.assertEqual(specialize_cell_shader(opts=defaults), {})
|
||||
self.assertEqual(specialize_cell_shader(opts=None), {})
|
||||
# Explicitly constructed opts equal to defaults must also be a no-op
|
||||
self.assertEqual(specialize_cell_shader(opts=make_opts()), {})
|
||||
|
||||
if not shutil.which(slangc[0]):
|
||||
self.skipTest(f'slangc ({slangc[0]}) not found in PATH')
|
||||
|
||||
# The skip is placed here intentionally: the default-opts assertions above
|
||||
# do not require slangc and should always run. Everything below invokes
|
||||
# the compiler to produce real GLSL output.
|
||||
|
||||
# Helper to run specialize_cell_shader with an isolated cache directory.
|
||||
# Returns (result_dict, create_cache_dir_callable).
|
||||
def compile_with_new_cache(opts):
|
||||
cache_dir = tempfile.mkdtemp()
|
||||
self.addCleanup(shutil.rmtree, cache_dir, True)
|
||||
# Each call with the same callable returns the same directory so
|
||||
# that the caching logic inside specialize_cell_shader can kick in.
|
||||
# Using a fresh mkdtemp() per invocation guarantees that no cached
|
||||
# result from a previous call can interfere with this one.
|
||||
def create_cache_dir():
|
||||
return cache_dir
|
||||
result = specialize_cell_shader(create_cache_dir=create_cache_dir, opts=opts)
|
||||
return result, create_cache_dir
|
||||
|
||||
# Changing the options must produce non-empty output with compiled shaders.
|
||||
opts_legacy = make_opts(text_composition_strategy='legacy')
|
||||
result_legacy, ccdir1 = compile_with_new_cache(opts_legacy)
|
||||
self.assertNotEqual(result_legacy, {}, 'Expected non-empty result for non-default opts')
|
||||
|
||||
# Output must include GLSL files whose content is valid GLSL text.
|
||||
glsl_items = {k: v.decode() for k, v in result_legacy.items() if k.endswith('.glsl')}
|
||||
self.assertTrue(glsl_items, 'Expected at least one .glsl file in the result')
|
||||
for name, glsl_text in glsl_items.items():
|
||||
self.assertIn('#version', glsl_text, f'{name} should contain a #version directive')
|
||||
|
||||
# With TEXT_NEW_GAMMA=false the compiler eliminates the new-gamma code
|
||||
# path, so foreground_contrast_new must NOT appear in the fragment shader.
|
||||
frag_glsl = {k: v for k, v in glsl_items.items() if k.endswith('.frag.glsl')}
|
||||
self.assertTrue(frag_glsl, 'Expected at least one fragment GLSL file')
|
||||
for name, glsl_text in frag_glsl.items():
|
||||
self.assertNotIn('foreground_contrast_new', glsl_text,
|
||||
f'{name}: legacy strategy should not contain foreground_contrast_new')
|
||||
|
||||
# Calling with the same opts and the same cache dir must return the
|
||||
# identical dict object (cached, no recompilation).
|
||||
result_legacy_again = specialize_cell_shader(create_cache_dir=ccdir1, opts=opts_legacy)
|
||||
self.assertIs(result_legacy, result_legacy_again,
|
||||
'Second call with unchanged opts must return the cached result')
|
||||
|
||||
# Changing options a second time must produce a different result.
|
||||
opts_fg_override = make_opts(text_fg_override_threshold=(0.5, '%'))
|
||||
result_fg, ccdir2 = compile_with_new_cache(opts_fg_override)
|
||||
self.assertNotEqual(result_fg, {}, 'Expected non-empty result for fg_override opts')
|
||||
self.assertNotEqual(result_legacy, result_fg,
|
||||
'Different opts must produce different compiled output')
|
||||
|
||||
# Verify that the GLSL content actually differs between the two option sets.
|
||||
frag_glsl2 = {k: v.decode() for k, v in result_fg.items() if k.endswith('.frag.glsl')}
|
||||
for name in frag_glsl:
|
||||
if name in frag_glsl2:
|
||||
self.assertNotEqual(frag_glsl[name], frag_glsl2[name],
|
||||
f'{name}: GLSL content must differ between option sets')
|
||||
|
||||
# Calling again with the second option set must also return the cached dict.
|
||||
result_fg_again = specialize_cell_shader(create_cache_dir=ccdir2, opts=opts_fg_override)
|
||||
self.assertIs(result_fg, result_fg_again,
|
||||
'Second call with unchanged fg_override opts must return the cached result')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user