Compare commits

...

86 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
b0dc2c9096 Add tests for specialize_cell_shader() to kitty_tests/shaders.py 2026-07-02 12:00:32 +00:00
Kovid Goyal
3982dc04d6 Function to specialize cell shader at runtime based on options 2026-07-02 17:24:48 +05:30
Kovid Goyal
a98f7448da Dump slang file metadata when building 2026-07-02 15:34:14 +05:30
Kovid Goyal
1252974765 Port the cell fragment shader 2026-07-02 14:47:36 +05:30
Kovid Goyal
f322d6d9c4 Remove glsl file is fixing it fails 2026-07-02 14:35:54 +05:30
Kovid Goyal
0d5a7511b1 Lower glsl version to 140 to match existing shaders 2026-07-02 13:47:37 +05:30
Kovid Goyal
fe84bc9225 Preserve layout location attribs for VAOs 2026-07-02 13:34:35 +05:30
Kovid Goyal
d114cb1554 Finish porting cell vertex shader 2026-07-02 12:57:38 +05:30
Kovid Goyal
5e525dba6b More work on cell shader porting 2026-07-02 12:55:03 +05:30
Kovid Goyal
9ad95fcb1f More cell shader porting work 2026-07-02 12:09:40 +05:30
Kovid Goyal
4f9dc41e06 More porting work on cell shader 2026-07-02 11:38:14 +05:30
Kovid Goyal
ee17d47e76 Port the HSL UV shader 2026-07-02 10:40:15 +05:30
Kovid Goyal
9d3d3a7d85 More work on porting cell shader 2026-07-02 10:12:57 +05:30
Kovid Goyal
db7261440d Start work on porting cell shader 2026-07-01 23:29:55 +05:30
Kovid Goyal
bd81b461f1 Rebuild shaders if slangc version does not match 2026-07-01 23:12:52 +05:30
Kovid Goyal
74701a701a Bump slang version, allows removing one glsl workaround 2026-07-01 22:59:59 +05:30
Kovid Goyal
81a6fc3832 Rebuild when build script is newer than cached IR 2026-07-01 16:01:26 +05:30
Kovid Goyal
16e4459fae Handle fixup of flat location specifiers 2026-07-01 15:56:54 +05:30
Kovid Goyal
28bd195499 Make mypy happy 2026-07-01 15:52:11 +05:30
Kovid Goyal
85d9de7aca Handle uniform structs 2026-07-01 15:38:02 +05:30
Kovid Goyal
024296f36e Nicer validation integration 2026-07-01 15:10:14 +05:30
Kovid Goyal
18d1614924 Handle array uniforms when fixing GLSL 2026-07-01 15:04:55 +05:30
Kovid Goyal
d2cc286412 Do not generate empty default specialization files 2026-07-01 14:54:49 +05:30
Kovid Goyal
8e597c1de0 Reduce the number of specialisation vars for the cell shader
Use macros for the from C constants
2026-07-01 14:13:35 +05:30
Kovid Goyal
e0b208cc39 Infrastructure for porting cell shader 2026-07-01 13:52:58 +05:30
Kovid Goyal
ff6a9faf2c Install gslangValidator in CI 2026-07-01 10:45:59 +05:30
Kovid Goyal
8a1c322e09 ... 2026-07-01 10:43:50 +05:30
Kovid Goyal
75aa909fbe Only validate newly built files 2026-07-01 10:32:43 +05:30
Kovid Goyal
4f144a2147 Automatically validate GLSL shaders after build 2026-07-01 10:23:27 +05:30
Kovid Goyal
c2924944f0 Port rounded_rect shader 2026-07-01 10:03:12 +05:30
Kovid Goyal
149915c15c Use gather for texture sampling 2026-07-01 08:56:29 +05:30
Kovid Goyal
5dc9b3a5ff Port the screenshot shader 2026-07-01 08:15:17 +05:30
Kovid Goyal
c268e3a2e2 Port the blit shader 2026-07-01 08:02:59 +05:30
Kovid Goyal
740d2275dc Rename blit common shader module 2026-07-01 07:49:59 +05:30
Kovid Goyal
5e71d46a38 Port the trail shader 2026-07-01 07:45:44 +05:30
Kovid Goyal
9bb2c168ac Also extract sampler uniforms 2026-07-01 07:26:14 +05:30
Kovid Goyal
296c9abdc3 Port the tint shader 2026-06-30 23:13:30 +05:30
Kovid Goyal
749b3c48b6 Remove unused code 2026-06-30 23:04:22 +05:30
Kovid Goyal
81f467626c Use same version of slang as bundled version in CI 2026-06-30 23:01:43 +05:30
Kovid Goyal
837e33395e Output uniform variable names mapping for GLSL shaders 2026-06-30 22:33:29 +05:30
Kovid Goyal
cbebf86fb7 Port the bgimage shader 2026-06-30 22:25:01 +05:30
Kovid Goyal
f0402437ac Get slangc working in macOS frozen build 2026-06-30 13:05:08 +05:30
Kovid Goyal
54c22e6c6c Only copy needed slang dlls 2026-06-30 12:11:18 +05:30
Kovid Goyal
c16b78a6f4 Get freezing of slang on linux working 2026-06-30 11:41:30 +05:30
Kovid Goyal
1fa3f450b5 Ensure slangc is found when freezing 2026-06-30 09:43:39 +05:30
Kovid Goyal
64c6af3ad3 Nicer error message when slangc not found 2026-06-30 09:34:05 +05:30
Kovid Goyal
af61fd1b10 Move glfw build code into setup.py
This was on my TODO list for a long time and doing this allows the
setup.py module code to be re-used in slang.py
2026-06-30 09:27:27 +05:30
Kovid Goyal
cfbae29da9 Fix building of shaders under ASAN
We cant import fast_data_types.so into the system python in that case,
so build shaders by running the compiler under the kitty launcher.
2026-06-30 09:04:13 +05:30
Kovid Goyal
6f0fac372a Cleanup build output 2026-06-30 08:48:55 +05:30
Kovid Goyal
236b9a8978 ... 2026-06-30 08:43:08 +05:30
Kovid Goyal
db16c1b771 Ensure shaders are built after fast_data_types.so 2026-06-30 08:34:44 +05:30
Kovid Goyal
a12883abf3 Move specialisations into compiler code
Allows for dynamic specialisations
2026-06-30 08:11:18 +05:30
Kovid Goyal
bbec9d5bbd Fix failing tests 2026-06-29 23:24:40 +05:30
Kovid Goyal
5bc8cfaaf5 Implement specialisation for slang shaders 2026-06-29 23:11:53 +05:30
Kovid Goyal
c49dcf9fca ... 2026-06-29 08:37:16 +05:30
Kovid Goyal
3f89d0a94b More work on slangc compilation 2026-06-29 08:30:25 +05:30
Kovid Goyal
71fd4d3c57 More work on slang compiler 2026-06-28 13:06:22 +05:30
Kovid Goyal
3968a86693 Note that shader-slang is now a dependency 2026-06-28 09:08:37 +05:30
Kovid Goyal
8d20d25288 Have dev.sh set SLANGC as well 2026-06-28 09:03:50 +05:30
Kovid Goyal
e008088b9f Ensure slangc is available in CI 2026-06-28 08:59:49 +05:30
Kovid Goyal
1326ac914f More work on slang compilation 2026-06-28 08:54:37 +05:30
Kovid Goyal
980de0c3f6 More work on integrating slangc into the build pipeline 2026-06-28 08:54:37 +05:30
Kovid Goyal
ab59a7f72c Setup location for compiled shaders 2026-06-28 08:54:37 +05:30
Kovid Goyal
c801efdd03 Function to build slang code to IR 2026-06-28 08:54:37 +05:30
copilot-swe-agent[bot]
c899873784 Fix parser to strip semicolons from module/import names; update tests 2026-06-28 08:54:36 +05:30
copilot-swe-agent[bot]
060c235224 Add slang parser corner case tests and implement test_slang_ordering 2026-06-28 08:54:36 +05:30
Kovid Goyal
135ba45c7e Work on generating build tree for slang files 2026-06-28 08:54:36 +05:30
Kovid Goyal
5b4e3a12a1 Use the slangc binary instead trying to get the C++ extension working everywhere is too fragile 2026-06-28 08:54:36 +05:30
Kovid Goyal
d52013db7e Add build test for loading slangc 2026-06-28 08:54:36 +05:30
Kovid Goyal
fff087bd49 Get slangc building with clang 2026-06-28 08:54:36 +05:30
Kovid Goyal
b5339915e6 ... 2026-06-28 08:54:36 +05:30
Kovid Goyal
0ad4f694d3 More work on slangc 2026-06-28 08:54:36 +05:30
Kovid Goyal
68d4688a72 ... 2026-06-28 08:54:36 +05:30
Kovid Goyal
8acfd7bbf2 Move glsl loading code to legacy 2026-06-28 08:54:36 +05:30
Kovid Goyal
951fea567b Move shader pythons code into shaders package 2026-06-28 08:54:36 +05:30
Kovid Goyal
ccfb06d6fc Move shaders into kitty package 2026-06-28 08:54:36 +05:30
Kovid Goyal
0d5e61e7e6 Rationalize build/test/package cycle 2026-06-28 08:54:36 +05:30
Kovid Goyal
50a95b5513 Remove unneeded build step 2026-06-28 08:54:35 +05:30
Kovid Goyal
9f85dfd6cc Work on slang module compilation 2026-06-28 08:54:35 +05:30
Kovid Goyal
50eb2c13f2 Stub slang module 2026-06-28 08:54:35 +05:30
Kovid Goyal
f9bc1c9fdf DRYer 2026-06-28 08:54:35 +05:30
copilot-swe-agent[bot]
8d196d86ba Use GITHUB_TOKEN for slang release API request to avoid rate limits 2026-06-28 08:54:35 +05:30
copilot-swe-agent[bot]
3b15d86b64 Address code review: use url=None, add TypeError comment 2026-06-28 08:54:35 +05:30
copilot-swe-agent[bot]
d8972f2f95 Implement install_slang_compiler() in ci.py 2026-06-28 08:54:35 +05:30
Kovid Goyal
e64663aa74 Stub for installation of slang on CI 2026-06-28 08:54:35 +05:30
Kovid Goyal
44b3ecf06f Add slang to sources 2026-06-28 08:54:35 +05:30
35 changed files with 2687 additions and 271 deletions

View File

@@ -4,8 +4,10 @@
import glob import glob
import io import io
import json
import lzma import lzma
import os import os
import platform
import shlex import shlex
import shutil import shutil
import subprocess import subprocess
@@ -22,6 +24,7 @@ is_codeql = os.environ.get('KITTY_CODEQL') == '1'
is_macos = 'darwin' in sys.platform.lower() is_macos = 'darwin' in sys.platform.lower()
running_under_sanitizer = os.environ.get('KITTY_SANITIZE') == '1' running_under_sanitizer = os.environ.get('KITTY_SANITIZE') == '1'
SW = '' SW = ''
SLANG_INSTALL_DIR = '/tmp/slang'
def do_print_crash_reports() -> None: def do_print_crash_reports() -> None:
@@ -101,6 +104,33 @@ def install_fonts() -> None:
tf.extractall(fonts_dir) tf.extractall(fonts_dir)
def install_slang_compiler() -> None:
os_name = 'macos' if is_macos else 'linux'
machine = platform.machine().lower()
arch = 'aarch64' if machine in ('aarch64', 'arm64') else 'x86_64'
with open('bypy/sources.json') as f:
data = json.loads(f.read())
for dep in data:
if dep['name'].startswith('slang '):
version = dep['name'].split()[-1]
break
url = f'https://github.com/shader-slang/slang/releases/download/v{version}/slang-{version}-{os_name}-{arch}.tar.gz'
install_dir = SLANG_INSTALL_DIR
os.makedirs(install_dir, exist_ok=True)
data = download_with_retry(url)
with tarfile.open(fileobj=io.BytesIO(data), mode='r:gz') as tf:
try:
tf.extractall(install_dir, filter='fully_trusted')
except TypeError:
# filter parameter not supported on older Python versions
tf.extractall(install_dir)
pc_dir = os.path.join(install_dir, 'lib', 'pkgconfig')
pp = os.environ.get('PKG_CONFIG_PATH', '')
os.environ['PKG_CONFIG_PATH'] = f'{pc_dir}:{pp}' if pp else pc_dir
def install_deps() -> None: def install_deps() -> None:
print('Installing kitty dependencies...') print('Installing kitty dependencies...')
sys.stdout.flush() sys.stdout.flush()
@@ -118,7 +148,8 @@ def install_deps() -> None:
run('sudo apt-get install -y --fix-missing libgl1-mesa-dev libxi-dev libxrandr-dev libxinerama-dev ca-certificates' run('sudo apt-get install -y --fix-missing libgl1-mesa-dev libxi-dev libxrandr-dev libxinerama-dev ca-certificates'
' libxcursor-dev libxcb-xkb-dev libdbus-1-dev libxkbcommon-dev libharfbuzz-dev libx11-xcb-dev zsh' ' libxcursor-dev libxcb-xkb-dev libdbus-1-dev libxkbcommon-dev libharfbuzz-dev libx11-xcb-dev zsh'
' libpng-dev liblcms2-dev libfontconfig-dev libxkbcommon-x11-dev libcanberra-dev libxxhash-dev uuid-dev' ' libpng-dev liblcms2-dev libfontconfig-dev libxkbcommon-x11-dev libcanberra-dev libxxhash-dev uuid-dev'
' libsimde-dev libsystemd-dev libcairo2-dev zsh bash dash systemd-coredump gdb') ' libsimde-dev libsystemd-dev libcairo2-dev zsh bash dash systemd-coredump gdb'
' libwayland-dev wayland-protocols glslang-tools')
# for some reason these directories are world writable which causes zsh # for some reason these directories are world writable which causes zsh
# compinit to break # compinit to break
run('sudo chmod -R og-w /usr/share/zsh') run('sudo chmod -R og-w /usr/share/zsh')
@@ -129,14 +160,21 @@ def install_deps() -> None:
if sys.version_info[:2] < (3, 7): if sys.version_info[:2] < (3, 7):
cmd += ' importlib-resources dataclasses' cmd += ' importlib-resources dataclasses'
run(cmd) run(cmd)
install_slang_compiler()
install_fonts() install_fonts()
def set_slangc() -> None:
slangc = os.path.join(SW if is_bundle else SLANG_INSTALL_DIR, 'bin', 'slangc')
os.environ['SLANGC'] = slangc
def build_kitty() -> None: def build_kitty() -> None:
python = shutil.which('python3') if is_bundle else sys.executable python = shutil.which('python3') if is_bundle else sys.executable
cmd = f'{python} setup.py build --verbose' cmd = f'{python} setup.py build --verbose'
if running_under_sanitizer: if running_under_sanitizer:
cmd += ' --debug --sanitize' cmd += ' --debug --sanitize'
set_slangc()
run(cmd) run(cmd)
@@ -146,12 +184,15 @@ def test_kitty() -> None:
run('sudo chmod -R 777 /cores') run('sudo chmod -R 777 /cores')
if running_under_sanitizer: if running_under_sanitizer:
os.environ['MallocNanoZone'] = '0' os.environ['MallocNanoZone'] = '0'
set_slangc()
run('./test.py', print_crash_reports=True) run('./test.py', print_crash_reports=True)
def package_kitty() -> None: def package_kitty() -> None:
set_slangc()
python = 'python3' if is_macos else 'python' python = 'python3' if is_macos else 'python'
run(f'{python} setup.py linux-package --update-check-interval=0 --verbose') run(f'{python} setup.py linux-package --update-check-interval=0 --verbose')
run('make FAIL_WARN=1 docs')
if is_macos: if is_macos:
run('python3 setup.py kitty.app --update-check-interval=0 --verbose') run('python3 setup.py kitty.app --update-check-interval=0 --verbose')
run('kitty.app/Contents/MacOS/kitty +runpy "from kitty.constants import *; print(kitty_exe())"') run('kitty.app/Contents/MacOS/kitty +runpy "from kitty.constants import *; print(kitty_exe())"')
@@ -166,7 +207,8 @@ def replace_in_file(path: str, src: str, dest: str) -> None:
def setup_bundle_env() -> None: def setup_bundle_env() -> None:
global SW global SW
os.environ['SW'] = SW = '/Users/Shared/kitty-build/sw/sw' if is_macos else os.path.join(os.environ['GITHUB_WORKSPACE'], 'sw') os.environ['SW'] = SW = '/Users/Shared/kitty-build/sw/sw' if is_macos else os.path.join(
os.environ['GITHUB_WORKSPACE'], 'sw')
os.environ['PKG_CONFIG_PATH'] = os.path.join(SW, 'lib', 'pkgconfig') os.environ['PKG_CONFIG_PATH'] = os.path.join(SW, 'lib', 'pkgconfig')
if is_macos: if is_macos:
os.environ['PATH'] = '{}:{}'.format('/usr/local/opt/sphinx-doc/bin', os.environ['PATH']) os.environ['PATH'] = '{}:{}'.format('/usr/local/opt/sphinx-doc/bin', os.environ['PATH'])
@@ -282,9 +324,9 @@ def main() -> None:
if action == 'build': if action == 'build':
build_kitty() build_kitty()
elif action == 'package': elif action == 'package':
package_kitty() build_kitty()
elif action == 'test':
test_kitty() test_kitty()
package_kitty()
elif action == 'test': elif action == 'test':
test_kitty() test_kitty()
elif action == 'govulncheck': elif action == 'govulncheck':

View File

@@ -117,18 +117,12 @@ jobs:
- name: Build kitty package - name: Build kitty package
run: python .github/workflows/ci.py package run: python .github/workflows/ci.py package
- name: Build kitty
run: python setup.py build --debug
- name: Run mypy - name: Run mypy
run: which python && python -m mypy --version && ./test.py mypy run: which python && python -m mypy --version && ./test.py mypy
- name: Run go vet - name: Run go vet
run: go version && go vet -tags testing ./... run: go version && go vet -tags testing ./...
- name: Build docs
run: make FAIL_WARN=1 docs
- name: Build static kittens - name: Build static kittens
run: python setup.py build-static-binaries run: python setup.py build-static-binaries
@@ -179,18 +173,9 @@ jobs:
with: with:
go-version-file: go.mod go-version-file: go.mod
- name: Build kitty
run: python3 .github/workflows/ci.py build
- name: Test kitty
run: python3 .github/workflows/ci.py test
- name: Install deps for docs - name: Install deps for docs
run: python3 -m pip install -r docs/requirements.txt run: python3 -m pip install -r docs/requirements.txt
- name: Builds docs
run: make FAIL_WARN=1 docs
- name: Build kitty package - name: Build kitty package
run: python3 .github/workflows/ci.py package run: python3 .github/workflows/ci.py package

View File

@@ -393,6 +393,7 @@ func build(args []string) {
} }
root := root_dir() root := root_dir()
os.Setenv("DEVELOP_ROOT", root) os.Setenv("DEVELOP_ROOT", root)
os.Setenv("SLANGC", filepath.Join(root, "bin", "slangc"))
prepend("PKG_CONFIG_PATH", filepath.Join(root, "lib", "pkgconfig")) prepend("PKG_CONFIG_PATH", filepath.Join(root, "lib", "pkgconfig"))
if runtime.GOOS == "darwin" { if runtime.GOOS == "darwin" {
os.Setenv("PKGCONFIG_EXE", filepath.Join(root, "bin", "pkg-config")) os.Setenv("PKGCONFIG_EXE", filepath.Join(root, "bin", "pkg-config"))

View File

@@ -11,7 +11,7 @@ import sys
import tempfile import tempfile
from contextlib import suppress from contextlib import suppress
from bypy.constants import LIBDIR, PREFIX, PYTHON, ismacos, worker_env from bypy.constants import BIN, LIBDIR, PREFIX, PYTHON, ismacos, worker_env
from bypy.constants import SRC as KITTY_DIR from bypy.constants import SRC as KITTY_DIR
from bypy.utils import run_shell, walk from bypy.utils import run_shell, walk
@@ -39,6 +39,7 @@ def run(*args, **extra_env):
env.update(extra_env) env.update(extra_env)
env['SW'] = PREFIX env['SW'] = PREFIX
env['LD_LIBRARY_PATH'] = LIBDIR env['LD_LIBRARY_PATH'] = LIBDIR
env['SLANGC'] = os.path.join(BIN, 'slangc')
if ismacos: if ismacos:
env['PKGCONFIG_EXE'] = os.path.join(PREFIX, 'bin', 'pkg-config') env['PKGCONFIG_EXE'] = os.path.join(PREFIX, 'bin', 'pkg-config')
cwd = env.pop('cwd', KITTY_DIR) cwd = env.pop('cwd', KITTY_DIR)
@@ -89,7 +90,7 @@ def build_frozen_tools(kitty_exe):
def sanitize_source_folder(path: str) -> None: def sanitize_source_folder(path: str) -> None:
for q in walk(path): for q in walk(path):
if os.path.splitext(q)[1] not in ('.py', '.glsl', '.ttf', '.otf', '.json'): if os.path.splitext(q)[1] not in ('.py', '.glsl', '.slang', '.ttf', '.otf', '.json'):
os.unlink(q) os.unlink(q)
@@ -99,6 +100,7 @@ def build_c_extensions(ext_dir, args):
shutil.copytree( shutil.copytree(
KITTY_DIR, writeable_src_dir, symlinks=True, KITTY_DIR, writeable_src_dir, symlinks=True,
ignore=shutil.ignore_patterns('b', 'build', 'dist', '*_commands.json', '*.o', '*.so', '*.dylib', '*.pyd')) ignore=shutil.ignore_patterns('b', 'build', 'dist', '*_commands.json', '*.o', '*.so', '*.dylib', '*.pyd'))
shutil.rmtree(os.path.join(writeable_src_dir, 'shaders'))
with suppress(FileNotFoundError): with suppress(FileNotFoundError):
os.unlink(os.path.join(writeable_src_dir, 'kitty', 'launcher', 'kitty')) os.unlink(os.path.join(writeable_src_dir, 'kitty', 'launcher', 'kitty'))

View File

@@ -10,7 +10,7 @@ import subprocess
import tarfile import tarfile
import time import time
from bypy.constants import OUTPUT_DIR, PREFIX, python_major_minor_version from bypy.constants import BIN, LIBDIR, OUTPUT_DIR, PREFIX, python_major_minor_version
from bypy.freeze import extract_extension_modules, freeze_python, path_to_freeze_dir from bypy.freeze import extract_extension_modules, freeze_python, path_to_freeze_dir
from bypy.utils import get_dll_path, mkdtemp, py_compile, walk from bypy.utils import get_dll_path, mkdtemp, py_compile, walk
@@ -90,6 +90,14 @@ def copy_libs(env):
shutil.copy2(x, dest) shutil.copy2(x, dest)
dest = os.path.join(dest, os.path.basename(x)) dest = os.path.join(dest, os.path.basename(x))
subprocess.check_call(['chrpath', '-d', dest]) subprocess.check_call(['chrpath', '-d', dest])
# Copy slang
x = 'libslang-compiler.so'
shutil.copy2(os.path.join(LIBDIR, f'{x}.0.0.0.0'), env.lib_dir)
os.symlink(f'{x}.0.0.0.0', os.path.join(env.lib_dir, x))
for x in ('glsl-module', 'glslang'):
x = f'libslang-{x}-0.0.0.so'
shutil.copy2(os.path.join(LIBDIR, x), env.lib_dir)
shutil.copy2(os.path.join(BIN, 'slangc'), env.bin_dir)
def add_ca_certs(env): def add_ca_certs(env):
@@ -138,10 +146,6 @@ def copy_python(env):
shutil.rmtree(env.py_dir) shutil.rmtree(env.py_dir)
def build_launcher(env):
iv['build_frozen_launcher']([path_to_freeze_dir(), env.obj_dir])
def is_elf(path): def is_elf(path):
with open(path, 'rb') as f: with open(path, 'rb') as f:
return f.read(4) == b'\x7fELF' return f.read(4) == b'\x7fELF'
@@ -228,7 +232,7 @@ def main():
env = Env(os.path.join(ext_dir, kitty_constants['appname'])) env = Env(os.path.join(ext_dir, kitty_constants['appname']))
copy_libs(env) copy_libs(env)
copy_python(env) copy_python(env)
build_launcher(env) iv['build_frozen_launcher']([path_to_freeze_dir(), env.obj_dir])
files = find_binaries(env) files = find_binaries(env)
fix_permissions(files) fix_permissions(files)
add_ca_certs(env) add_ca_certs(env)

View File

@@ -12,7 +12,7 @@ import sys
import tempfile import tempfile
import zipfile import zipfile
from bypy.constants import PREFIX, PYTHON, SW, python_major_minor_version from bypy.constants import BIN, LIBDIR, PREFIX, PYTHON, SW, python_major_minor_version
from bypy.freeze import extract_extension_modules, freeze_python, path_to_freeze_dir from bypy.freeze import extract_extension_modules, freeze_python, path_to_freeze_dir
from bypy.macos_sign import codesign, create_entitlements_file, make_certificate_useable, notarize_app, verify_signature from bypy.macos_sign import codesign, create_entitlements_file, make_certificate_useable, notarize_app, verify_signature
from bypy.utils import current_dir, mkdtemp, py_compile, run_shell, timeit, walk from bypy.utils import current_dir, mkdtemp, py_compile, run_shell, timeit, walk
@@ -301,7 +301,7 @@ class Freeze(object):
self.fix_dependencies_in_lib(join(self.frameworks_dir, basename(path))) self.fix_dependencies_in_lib(join(self.frameworks_dir, basename(path)))
@flush @flush
def add_misc_libraries(self): def add_misc_libraries(self) -> None:
for x in ( for x in (
'sqlite3.0', 'sqlite3.0',
'z.1', 'z.1',
@@ -319,6 +319,22 @@ class Freeze(object):
dest = join(self.frameworks_dir, x) dest = join(self.frameworks_dir, x)
self.set_id(dest, f'{self.FID}/{x}') self.set_id(dest, f'{self.FID}/{x}')
self.fix_dependencies_in_lib(dest) self.fix_dependencies_in_lib(dest)
# Copy slang
x = 'libslang-compiler.0.0.0.0.dylib'
shutil.copy2(os.path.join(LIBDIR, x), self.frameworks_dir)
os.symlink(x, os.path.join(self.frameworks_dir, 'libslang-compiler.dylib'))
dest = join(self.frameworks_dir, x)
self.set_id(dest, f'{self.FID}/{x}')
self.fix_dependencies_in_lib(dest)
for x in ('glsl-module', 'glslang'):
x = f'libslang-{x}-0.0.0.dylib'
shutil.copy2(os.path.join(LIBDIR, x), self.frameworks_dir)
dest = join(self.frameworks_dir, x)
self.set_id(dest, f'{self.FID}/{x}')
dest = os.path.join(self.contents_dir, 'MacOS')
shutil.copy2(os.path.join(BIN, 'slangc'), dest)
dest = os.path.join(dest, 'slangc')
self.fix_dependencies_in_lib(dest)
@flush @flush
def add_package_dir(self, x, dest=None): def add_package_dir(self, x, dest=None):

View File

@@ -279,6 +279,15 @@
} }
}, },
{
"name": "slang 2026.12.2",
"unix": {
"file_extension": "tar.gz",
"hash": "sha256:1ed5ebf7886849ea621d6dabee20248e8551c80134aa8c4433746879cb20dea3",
"urls": ["git-submodules://github.com/shader-slang/slang.git"]
}
},
{ {
"name": "wayland 1.24.0", "name": "wayland 1.24.0",
"os": "linux", "os": "linux",

View File

@@ -89,6 +89,7 @@ Run-time dependencies:
* ``liblcms2`` * ``liblcms2``
* ``libxxhash`` * ``libxxhash``
* ``openssl`` * ``openssl``
* ``shader-slang`` (for the slangc compiler)
* ``pixman`` (not needed on macOS) * ``pixman`` (not needed on macOS)
* ``cairo`` (not needed on macOS) * ``cairo`` (not needed on macOS)
* ``freetype`` (not needed on macOS) * ``freetype`` (not needed on macOS)

View File

@@ -2,234 +2,13 @@
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import json
import os import os
import re import re
import subprocess
import sys import sys
from enum import Enum
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Sequence, Tuple
_plat = sys.platform.lower() _plat = sys.platform.lower()
is_linux = 'linux' in _plat is_linux = 'linux' in _plat
is_openbsd = 'openbsd' in _plat is_openbsd = 'openbsd' in _plat
base = os.path.dirname(os.path.abspath(__file__))
def null_func() -> None:
return None
class CompileKey(NamedTuple):
src: str
dest: str
class Command(NamedTuple):
desc: str
cmd: Sequence[str]
is_newer_func: Callable[[], bool]
on_success: Callable[[], None] = null_func
key: Optional[CompileKey] = None
keyfile: Optional[str] = None
class ISA(Enum):
X86 = 0x03
AMD64 = 0x3e
ARM64 = 0xb7
Other = 0x0
class BinaryArch(NamedTuple):
bits: int = 64
isa: ISA = ISA.AMD64
class CompilerType(Enum):
gcc = 'gcc'
clang = 'clang'
unknown = 'unknown'
class Env:
cc: List[str] = []
cppflags: List[str] = []
cflags: List[str] = []
ldflags: List[str] = []
library_paths: Dict[str, List[str]] = {}
ldpaths: List[str] = []
ccver: Tuple[int, int]
vcs_rev: str = ''
binary_arch: BinaryArch = BinaryArch()
native_optimizations: bool = False
primary_version: int = 0
secondary_version: int = 0
xt_version: str = ''
has_copy_file_range: bool = False
# glfw stuff
all_headers: List[str] = []
sources: List[str] = []
wayland_packagedir: str = ''
wayland_scanner: str = ''
wayland_scanner_code: str = ''
wayland_protocols: Tuple[str, ...] = ()
def __init__(
self, cc: List[str] = [], cppflags: List[str] = [], cflags: List[str] = [], ldflags: List[str] = [],
library_paths: Dict[str, List[str]] = {}, ldpaths: Optional[List[str]] = None, ccver: Tuple[int, int] = (0, 0),
vcs_rev: str = '', binary_arch: BinaryArch = BinaryArch(),
native_optimizations: bool = False,
):
self.cc, self.cppflags, self.cflags, self.ldflags, self.library_paths = cc, cppflags, cflags, ldflags, library_paths
self.ldpaths = ldpaths or []
self.ccver = ccver
self.vcs_rev = vcs_rev
self.binary_arch = binary_arch
self.native_optimizations = native_optimizations
self._cc_version_string = ''
self._compiler_type: Optional[CompilerType] = None
@property
def cc_version_string(self) -> str:
if not self._cc_version_string:
self._cc_version_string = subprocess.check_output(self.cc + ['--version']).decode()
return self._cc_version_string
@property
def compiler_type(self) -> CompilerType:
if self._compiler_type is None:
raw = self.cc_version_string
if 'Free Software Foundation' in raw:
self._compiler_type = CompilerType.gcc
elif 'clang' in raw.lower().split():
self._compiler_type = CompilerType.clang
else:
self._compiler_type = CompilerType.unknown
return self._compiler_type
def copy(self) -> 'Env':
ans = Env(self.cc, list(self.cppflags), list(self.cflags), list(self.ldflags), dict(self.library_paths), list(self.ldpaths), self.ccver)
ans.all_headers = list(self.all_headers)
ans._cc_version_string = self._cc_version_string
ans.sources = list(self.sources)
ans.wayland_packagedir = self.wayland_packagedir
ans.wayland_scanner = self.wayland_scanner
ans.wayland_scanner_code = self.wayland_scanner_code
ans.wayland_protocols = self.wayland_protocols
ans.vcs_rev = self.vcs_rev
ans.binary_arch = self.binary_arch
ans.native_optimizations = self.native_optimizations
ans.primary_version = self.primary_version
ans.secondary_version = self.secondary_version
ans.xt_version = self.xt_version
ans.has_copy_file_range = self.has_copy_file_range
return ans
def wayland_protocol_file_name(base: str, ext: str = 'c') -> str:
base = os.path.basename(base).rpartition('.')[0]
return f'wayland-{base}-client-protocol.{ext}'
def init_env(
env: Env,
pkg_config: Callable[..., List[str]],
pkg_version: Callable[[str], Tuple[int, int]],
at_least_version: Callable[..., None],
test_compile: Callable[..., Any],
module: str = 'x11'
) -> Env:
ans = env.copy()
ans.cflags.append('-fPIC')
ans.cppflags.append(f'-D_GLFW_{module.upper()}')
ans.cppflags.append('-D_GLFW_BUILD_DLL')
with open(os.path.join(base, 'source-info.json')) as f:
sinfo = json.load(f)
module_sources = list(sinfo[module]['sources'])
if module in ('x11', 'wayland'):
remove = 'null_joystick.c' if is_linux else 'linux_joystick.c'
module_sources.remove(remove)
ans.sources = sinfo['common']['sources'] + module_sources
ans.all_headers = [x for x in os.listdir(base) if x.endswith('.h')]
if module in ('x11', 'wayland'):
ans.cflags.append('-pthread')
ans.ldpaths.extend('-pthread -lm'.split())
if not is_openbsd:
ans.ldpaths.extend('-lrt -ldl'.split())
major, minor = pkg_version('xkbcommon')
if (major, minor) < (0, 5):
raise SystemExit('libxkbcommon >= 0.5 required')
if major < 1:
ans.cflags.append('-DXKB_HAS_NO_UTF32')
if module == 'x11':
for dep in 'x11 xrandr xinerama xcursor xkbcommon xkbcommon-x11 x11-xcb dbus-1'.split():
ans.cflags.extend(pkg_config(dep, '--cflags-only-I'))
ans.ldpaths.extend(pkg_config(dep, '--libs'))
elif module == 'cocoa':
ans.cppflags.append('-DGL_SILENCE_DEPRECATION')
for f_ in 'Cocoa IOKit CoreFoundation CoreVideo QuartzCore UniformTypeIdentifiers'.split():
ans.ldpaths.extend(('-framework', f_))
elif module == 'wayland':
at_least_version('wayland-protocols', *sinfo['wayland_protocols'])
ans.wayland_packagedir = os.path.abspath(pkg_config('wayland-protocols', '--variable=pkgdatadir')[0])
ans.wayland_scanner = os.path.abspath(pkg_config('wayland-scanner', '--variable=wayland_scanner')[0])
scanner_version = tuple(map(int, pkg_config('wayland-scanner', '--modversion')[0].strip().split('.')))
ans.wayland_scanner_code = 'private-code' if scanner_version >= (1, 14, 91) else 'code'
ans.wayland_protocols = tuple(sinfo[module]['protocols'])
for p in ans.wayland_protocols:
ans.sources.append(wayland_protocol_file_name(p))
ans.all_headers.append(wayland_protocol_file_name(p, 'h'))
for dep in 'wayland-client wayland-cursor xkbcommon dbus-1'.split():
ans.cflags.extend(pkg_config(dep, '--cflags-only-I'))
ans.ldpaths.extend(pkg_config(dep, '--libs'))
has_memfd_create = test_compile(env.cc, '-Werror', src='''#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
int main(void) {
return syscall(__NR_memfd_create, "test", 0);
}''')
if has_memfd_create:
ans.cppflags.append('-DHAS_MEMFD_CREATE')
return ans
def build_wayland_protocols(
env: Env,
parallel_run: Callable[[List[Command]], None],
emphasis: Callable[[str], str],
newer: Callable[..., bool],
dest_dir: str
) -> None:
items = []
for protocol in env.wayland_protocols:
if '/' in protocol:
src = os.path.join(env.wayland_packagedir, protocol)
if not os.path.exists(src):
raise SystemExit(f'The wayland-protocols package on your system is missing the {protocol} protocol definition file')
else:
src = os.path.join(os.path.dirname(os.path.abspath(__file__)), protocol)
if not os.path.exists(src):
raise SystemExit(f'The local Wayland protocol {protocol} is missing from kitty sources')
for ext in 'hc':
dest = wayland_protocol_file_name(src, ext)
dest = os.path.join(dest_dir, dest)
if newer(dest, src):
q = 'client-header' if ext == 'h' else env.wayland_scanner_code
items.append(Command(
f'Generating {emphasis(os.path.basename(dest))} ...',
[env.wayland_scanner, q, src, dest], lambda: True))
if items:
parallel_run(items)
class Arg: class Arg:

View File

@@ -7,7 +7,7 @@ from functools import partial
from typing import NamedTuple 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 .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 .typing_compat import LayoutType
from .utils import color_as_int from .utils import color_as_int
from .window_list import WindowGroup, WindowList from .window_list import WindowGroup, WindowList

View File

@@ -148,7 +148,7 @@ from .session import (
most_recent_session, most_recent_session,
save_as_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 .simple_cli_definitions import grab_keyboard_docs
from .tabs import SpecialWindow, SpecialWindowInstance, Tab, TabDict, TabManager from .tabs import SpecialWindow, SpecialWindowInstance, Tab, TabDict, TabManager
from .types import _T, AsyncResponse, LayerShellConfig, SingleInstanceData, WindowSystemMouseEvent, ac from .types import _T, AsyncResponse, LayerShellConfig, SingleInstanceData, WindowSystemMouseEvent, ac

View File

@@ -35,6 +35,8 @@ kitty_run_data: dict[str, Any] = getattr(sys, 'kitty_run_data', {})
launched_by_launch_services = kitty_run_data.get('launched_by_launch_services', False) launched_by_launch_services = kitty_run_data.get('launched_by_launch_services', False)
is_quick_access_terminal_app = kitty_run_data.get('is_quick_access_terminal_app', False) is_quick_access_terminal_app = kitty_run_data.get('is_quick_access_terminal_app', False)
unserialize_launch_flag = 'kitty-unserialize-data=' unserialize_launch_flag = 'kitty-unserialize-data='
slangc = os.environ.get('SLANGC', 'slangc').split()
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
extensions_dir: str = kitty_run_data['extensions_dir'] extensions_dir: str = kitty_run_data['extensions_dir']
@@ -61,7 +63,9 @@ if getattr(sys, 'frozen', False):
ans = os.path.join(ans, 'kitty') ans = os.path.join(ans, 'kitty')
return ans return ans
kitty_base_dir = get_frozen_base() kitty_base_dir = get_frozen_base()
del get_frozen_base if rpath := kitty_run_data.get('bundle_exe_dir'):
slangc = [os.path.join(rpath, 'slangc')]
del get_frozen_base, rpath
else: else:
kitty_base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) kitty_base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
extensions_dir = os.path.join(kitty_base_dir, 'kitty') extensions_dir = os.path.join(kitty_base_dir, 'kitty')
@@ -154,6 +158,7 @@ logo_png_file = os.path.join(kitty_base_dir, 'logo', 'kitty.png')
beam_cursor_data_file = os.path.join(kitty_base_dir, 'logo', 'beam-cursor.png') beam_cursor_data_file = os.path.join(kitty_base_dir, 'logo', 'beam-cursor.png')
shell_integration_dir = os.path.join(kitty_base_dir, 'shell-integration') shell_integration_dir = os.path.join(kitty_base_dir, 'shell-integration')
fonts_dir = os.path.join(kitty_base_dir, 'fonts') fonts_dir = os.path.join(kitty_base_dir, 'fonts')
shaders_dir = os.path.join(kitty_base_dir, 'shaders')
try: try:
shell_path = os.environ.get('SHELL') or pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh' shell_path = os.environ.get('SHELL') or pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh'
except KeyError: except KeyError:

View File

@@ -59,7 +59,7 @@ from .options.types import Options
from .options.utils import DELETE_ENV_VAR from .options.utils import DELETE_ENV_VAR
from .os_window_size import edge_spacing, initial_window_size_func from .os_window_size import edge_spacing, initial_window_size_func
from .session import create_sessions, get_os_window_sizing_data 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 .types import LayerShellConfig
from .utils import ( from .utils import (
cleanup_ssh_control_masters, cleanup_ssh_control_masters,

View File

@@ -0,0 +1,28 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
module alpha_blend;
// Alpha blend two colors returning the resulting color pre-multiplied by its alpha
// and its alpha. See https://en.wikipedia.org/wiki/Alpha_compositing
public float4 alpha_blend(float4 over, float4 under) {
float alpha = lerp(under.a, 1.0f, over.a);
float3 combined_color = lerp(under.rgb * under.a, over.rgb, over.a);
return float4(combined_color, alpha);
}
// Same as alpha_blend() except that it assumes over and under are both premultiplied.
public float4 alpha_blend_premul(float4 over, float4 under) {
float inv_over_alpha = 1.0f - over.a;
float alpha = over.a + under.a * inv_over_alpha;
return float4(over.rgb + under.rgb * inv_over_alpha, alpha);
}
// Same as alpha_blend_premul with under_alpha = 1 outputs a blended color
// with alpha 1 which is effectively pre-multiplied since alpha is 1
public float4 alpha_blend_premul(float4 over, float3 under) {
float inv_over_alpha = 1.0f - over.a;
return float4(over.rgb + under.rgb * inv_over_alpha, 1.0f);
}

View File

@@ -0,0 +1,90 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
import alpha_blend;
// Constants and Macros
#define left 0
#define top 1
#define right 2
#define bottom 3
#define tex_left 0.0
#define tex_top 0.0
#define tex_right 1.0
#define tex_bottom 1.0
#define x_axis 0
#define y_axis 1
static const float2 tex_map[4] = {
float2(tex_left, tex_top),
float2(tex_left, tex_bottom),
float2(tex_right, tex_bottom),
float2(tex_right, tex_top)
};
struct VertexOutput {
float2 texcoord : TEXCOORD;
float4 position : SV_Position;
};
// Helper Functions
float scale_factor(float window_size, float image_size) {
return window_size / image_size;
}
float tiling_factor(int i, float4 sizes, float tiled) {
int window = i;
int image = i + 2;
return tiled * scale_factor(sizes[window], sizes[image]) + (1.0 - tiled);
}
// Main Vertex Shader Entry Point
[shader("vertex")]
VertexOutput vertex_main(
uint vertex_id : SV_VertexID,
uniform float tiled,
uniform float4 sizes,
uniform float4 positions,
) {
const float2 pos_map[4] = {
float2(positions[left], positions[top]),
float2(positions[left], positions[bottom]),
float2(positions[right], positions[bottom]),
float2(positions[right], positions[top])
};
VertexOutput output;
// Calculate outputs
float2 tex_coords = tex_map[vertex_id];
output.texcoord = float2(
tex_coords[x_axis] * tiling_factor(x_axis, sizes, tiled),
tex_coords[y_axis] * tiling_factor(y_axis, sizes, tiled)
);
output.position = float4(pos_map[vertex_id], 0.0, 1.0);
return output;
}
// Global Texture Bindings
uniform Sampler2D image;
// Helper Functions
float4 alpha_blend(float4 color, float4 bg) {
// Performs standard linear alpha blending: src * src.a + dst * (1 - src.a)
float3 blended_rgb = color.rgb * color.a + bg.rgb * (1.0 - color.a);
float blended_alpha = color.a + bg.a * (1.0 - color.a);
return float4(blended_rgb, blended_alpha);
}
// Main Fragment Shader Entry Point
[shader("fragment")]
float4 fragment_main(float2 texcoord: TEXCOORD, uniform float4 background) : SV_Target {
// Sample the texture using Slang's intrinsic texture object syntax
float4 color = image.Sample(texcoord);
// Compute final color with alpha blending
float4 premult_color = alpha_blend(color, background);
return premult_color;
}

View File

@@ -0,0 +1,33 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
// Maps a vertex id to src and destination co-ordinates
module blit_common;
public struct BlitOutput {
public float2 texcoord;
public float2 position;
};
#define left 0
#define top 1
#define right 2
#define bottom 3
// Static constant array mapping vertex IDs
static const int2 vertex_pos_map[4] = {
{right, top},
{right, bottom},
{left, bottom},
{left, top}
};
public BlitOutput get_coords_for_blit(uint vertex_id, float4 src_rect, float4 dest_rect) {
int2 pos = vertex_pos_map[vertex_id];
BlitOutput output;
output.texcoord = float2(src_rect[pos.x], src_rect[pos.y]);
output.position = float2(dest_rect[pos.x], dest_rect[pos.y]);
return output;
}

32
kitty/shaders/blit.slang Normal file
View File

@@ -0,0 +1,32 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
import blit_common;
import alpha_blend;
import utils;
import linear2srgb;
struct VSOutput {
float2 texcoord : TEXCOORD;
float4 position : SV_Position;
};
[shader("vertex")]
VSOutput vertex_main(
uint vertex_id : SV_VertexID,
uniform float4 src_rect,
uniform float4 dest_rect,
) {
BlitOutput ans = get_coords_for_blit(vertex_id, src_rect, dest_rect);
return {ans.texcoord, float4(ans.position[0], ans.position[1], 0.0, 1.0)};
}
uniform Sampler2D image;
[shader("fragment")]
float4 fragment_main(float2 texcoord : TEXCOORD) : SV_Target {
float4 color_premul = image.Sample(texcoord);
return vec4_premul(linear2srgb(color_premul.rgb / color_premul.a), color_premul.a);
}

569
kitty/shaders/cell.slang Normal file
View File

@@ -0,0 +1,569 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
// https://github.com/shader-slang/slang/issues/11874
// warnings-disable: 41012
import utils;
import hsluv;
import alpha_blend;
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
{
float use_cell_bg_for_selection_fg, use_cell_fg_for_selection_fg, use_cell_for_selection_bg;
uint default_fg, highlight_fg, highlight_bg, main_cursor_fg, main_cursor_bg, url_color, url_style, inverted, extra_cursor_fg, extra_cursor_bg;
uint columns, lines, sprites_xnum, sprites_ynum, cursor_shape, cell_width, cell_height;
uint cursor_x1, cursor_x2, cursor_y1, cursor_y2;
float cursor_opacity, inactive_text_alpha, dim_opacity, blink_opacity;
// must have unique entries with 0 being default_bg and unset being UINT32_MAX
uint bg_colors0, bg_colors1, bg_colors2, bg_colors3, bg_colors4, bg_colors5, bg_colors6, bg_colors7;
float bg_opacities0, bg_opacities1, bg_opacities2, bg_opacities3, bg_opacities4, bg_opacities5, bg_opacities6, bg_opacities7;
};
// Uniform Blocks (adjust binding slots as needed for your pipeline layout)
ConstantBuffer<CellRenderDataStruct> crd;
struct ColorTableStruct
{
uint color_table[NUM_COLORS + MARK_MASK + MARK_MASK + 2];
};
ConstantBuffer<ColorTableStruct> ColorTable;
#define color_table ColorTable.color_table
uniform float gamma_lut[256];
uniform Sampler2D<uint4> sprite_decorations_map;
//
static const int fg_index_map[3] = {0, 1, 0};
static const uint2 cell_pos_map[4] = {
uint2(1u, 0u), // right, top
uint2(1u, 1u), // right, bottom
uint2(0u, 1u), // left, bottom
uint2(0u, 0u) // left, top
};
static const uint cursor_shape_map[] = { // maps cursor shape to foreground sprite index
0u, // NO_CURSOR
0u, // BLOCK (this is rendered as background)
2u, // BEAM
3u, // UNDERLINE
4u // UNFOCUSED
};
// Vertex Input Attributes with explicit fixed locations. We use fixed
// locations as all variants of the cell program share the same VAOs
struct VertexInput
{
[[vk::location(0)]] uint3 colors;
[[vk::location(1)]] uint2 sprite_idx;
[[vk::location(2)]] uint is_selected;
};
// }}}
// Utility functions {{{
static const uint BYTE_MASK = 0xFF;
static const uint SPRITE_INDEX_MASK = 0x7fffffff;
static const uint SPRITE_COLORED_MASK = 0x80000000;
static const uint SPRITE_COLORED_SHIFT = 31u;
static const uint BIT_MASK = 1u;
// Linear space luminance values
static const float3 Y = float3(0.2126, 0.7152, 0.0722);
float3 color_to_vec(uint c) {
uint r, g, b;
r = (c >> 16) & BYTE_MASK;
g = (c >> 8) & BYTE_MASK;
b = c & BYTE_MASK;
return float3(gamma_lut[r], gamma_lut[g], gamma_lut[b]);
}
float one_if_equal_zero_otherwise(float a, float b) {
return (1.0f - zero_or_one(abs(float(a) - float(b))));
}
// We need an integer variant to accommodate GPU driver bugs, see
// https://github.com/kovidgoyal/kitty/issues/9072
uint one_if_equal_zero_otherwise(int a, int b) {
return (1u - uint(zero_or_one(abs(float(a) - float(b)))));
}
uint one_if_equal_zero_otherwise(uint a, uint b) {
return (1u - uint(zero_or_one(abs(float(a) - float(b)))));
}
uint resolve_color(uint c, uint defval) {
int t = int(c & BYTE_MASK);
uint is_one = one_if_equal_zero_otherwise(t, 1);
uint is_two = one_if_equal_zero_otherwise(t, 2);
uint is_neither_one_nor_two = 1u - is_one - is_two;
return is_one * color_table[(c >> 8) & BYTE_MASK] + is_two * (c >> 8) + is_neither_one_nor_two * defval;
}
float3 to_color(uint c, uint defval) {
return color_to_vec(resolve_color(c, defval));
}
[ForceInline]
float3 q_func(float type_val, uint which, float3 val) {
return one_if_equal_zero_otherwise(type_val, float(which)) * val;
}
float3 resolve_dynamic_color(uint c, float3 special_val, float3 defval) {
float type_val = float((c >> 24) & BYTE_MASK);
return (
q_func(type_val, COLOR_IS_RGB, color_to_vec(c)) +
q_func(type_val, COLOR_IS_INDEX, color_to_vec(color_table[c & BYTE_MASK])) +
q_func(type_val, COLOR_IS_SPECIAL, special_val) +
q_func(type_val, COLOR_NOT_SET, defval)
);
}
float contrast_ratio(float under_luminance, float over_luminance) {
return clamp((max(under_luminance, over_luminance) + 0.05f) / (min(under_luminance, over_luminance) + 0.05f), 1.f, 21.f);
}
float contrast_ratio(float3 a, float3 b) {
return contrast_ratio(dot(a, Y), dot(b, Y));
}
struct ColorPair {
float3 bg, fg;
};
float contrast_ratio(ColorPair a) {
return contrast_ratio(a.bg, a.fg);
}
ColorPair if_less_than_pair(float a, float b, ColorPair thenval, ColorPair elseval) {
return ColorPair(
if_less_than(a, b, thenval.bg, elseval.bg),
if_less_than(a, b, thenval.fg, elseval.fg)
);
}
ColorPair if_one_then_pair(float condition, ColorPair thenval, ColorPair elseval) {
return ColorPair(
if_one_then(condition, thenval.bg, elseval.bg),
if_one_then(condition, thenval.fg, elseval.fg)
);
}
ColorPair resolve_extra_cursor_colors_for_special_cursor(float3 cell_bg, float3 cell_fg) {
ColorPair cell = ColorPair(cell_fg, cell_bg);
ColorPair base = ColorPair(color_to_vec(crd.default_fg), color_to_vec(crd.bg_colors0));
float cr = contrast_ratio(cell);
float br = contrast_ratio(base);
ColorPair higher_contrast_pair = if_less_than_pair(cr, br, base, cell);
return if_less_than_pair(cr, 2.5f, higher_contrast_pair, cell);
}
ColorPair resolve_extra_cursor_colors(float3 cell_bg, float3 cell_fg, ColorPair main_cursor) {
ColorPair ans = ColorPair(
resolve_dynamic_color(crd.extra_cursor_bg, main_cursor.bg, main_cursor.bg),
resolve_dynamic_color(crd.extra_cursor_fg, cell_bg, main_cursor.fg)
);
ColorPair special = resolve_extra_cursor_colors_for_special_cursor(cell_bg, cell_fg);
return if_one_then_pair(zero_or_one(abs(float(crd.extra_cursor_bg & BYTE_MASK) - float(COLOR_IS_SPECIAL))), ans, special);
}
uint3 to_sprite_coords(uint idx) {
uint sprites_per_page = crd.sprites_xnum * crd.sprites_ynum;
uint z = idx / sprites_per_page;
uint num_on_last_page = idx - sprites_per_page * z;
uint y = num_on_last_page / crd.sprites_xnum;
uint x = num_on_last_page - crd.sprites_xnum * y;
return uint3(x, y, z);
}
float3 to_sprite_pos(uint2 pos, uint idx) {
uint3 c = to_sprite_coords(idx);
float2 s_xpos = float2(float(c.x), float(c.x) + 1.0f) * (1.0f / float(crd.sprites_xnum));
float2 s_ypos = float2(float(c.y), float(c.y) + 1.0f) * (1.0f / float(crd.sprites_ynum));
uint texture_height_px = (crd.cell_height + 1u) * crd.sprites_ynum;
float row_height = 1.0f / float(texture_height_px);
s_ypos[1] -= row_height; // skip the decorations_exclude row
return float3(s_xpos[pos.x], s_ypos[pos.y], float(c.z));
}
uint to_underline_exclusion_pos(uint2 sprite_idx) {
uint3 c = to_sprite_coords(sprite_idx[0]);
uint cell_top_px = c.y * (crd.cell_height + 1u);
return cell_top_px + crd.cell_height;
}
uint read_sprite_decorations_idx(uint2 sprite_idx) {
int idx = int(sprite_idx[0] & SPRITE_INDEX_MASK);
int width, height;
sprite_decorations_map.GetDimensions(width, height);
int2 sz = int2(width, height);
int y = idx / sz[0];
int x = idx - y * sz[0];
return sprite_decorations_map[int2(x, y)].r;
}
uint2 get_decorations_indices(uint2 sprite_idx, uint in_url /* [0, 1] */, uint text_attrs) {
uint decorations_idx = read_sprite_decorations_idx(sprite_idx);
uint has_decorations = uint(zero_or_one(float(decorations_idx)));
uint strike_style = ((text_attrs >> STRIKE_SHIFT) & BIT_MASK); // 0 or 1
uint strike_idx = decorations_idx * strike_style;
uint underline_style = ((text_attrs >> DECORATION_SHIFT) & DECORATION_MASK);
underline_style = in_url * crd.url_style + (1u - in_url) * underline_style; // [0, 5]
uint has_underline = uint(step(0.5f, float(underline_style))); // [0, 1]
return has_decorations * uint2(strike_idx, has_underline * (decorations_idx + underline_style));
}
uint is_cursor(uint x, uint y) {
uint clamped_x = clamp(x, crd.cursor_x1, crd.cursor_x2);
uint clamped_y = clamp(y, crd.cursor_y1, crd.cursor_y2);
return one_if_equal_zero_otherwise(x, clamped_x) * one_if_equal_zero_otherwise(y, clamped_y);
}
// }}}
struct CellData {
float has_cursor;
float has_block_cursor;
uint2 pos;
uint cursor_fg_sprite_idx;
ColorPair cursor;
};
struct VertexOutput {
float4 position : SV_Position;
float3 background;
float4 effective_background_premul;
float effective_text_alpha;
float3 sprite_pos;
float3 underline_pos;
float3 cursor_pos;
float3 strike_pos;
nointerpolation uint underline_exclusion_pos;
float3 cell_foreground;
float4 cursor_color_premult;
float3 decoration_fg;
float colored_sprite;
};
CellData set_vertex_position(
float3 cell_fg,
float3 cell_bg,
uint instance_id,
uint vertex_id,
float row_offset,
uint2 sprite_idx,
uint is_selected,
inout VertexOutput vo,
) {
CellData cell_out;
float dx = 2.0 / float(crd.columns);
float dy = 2.0 / float(crd.lines);
// The current cell being rendered
uint row = instance_id / crd.columns;
uint column = instance_id - row * crd.columns;
// The position of this vertex, at a corner of the cell
float left = -1.0 + float(column) * dx;
float top = 1.0 - (float(row) + row_offset) * dy;
uint2 pos = cell_pos_map[vertex_id];
float2 select_x = float2(left, left + dx);
float2 select_y = float2(top, top - dy);
float posX = select_x[pos.x];
float posY = select_y[pos.y];
vo.position = float4(posX, posY, 0.0, 1.0);
// The character sprite being rendered
if (!ONLY_BACKGROUND) {
vo.sprite_pos = to_sprite_pos(pos, sprite_idx[0] & SPRITE_INDEX_MASK);
vo.colored_sprite = float((sprite_idx[0] & SPRITE_COLORED_MASK) >> SPRITE_COLORED_SHIFT);
}
// Cursor shape and colors
float has_main_cursor = float(is_cursor(column, row));
float multicursor_shape = float((is_selected >> 2) & 3u);
float multicursor_uses_main_cursor_shape = float((is_selected >> 4) & BIT_MASK);
multicursor_shape = if_one_then(multicursor_uses_main_cursor_shape, crd.cursor_shape, multicursor_shape);
float final_cursor_shape = if_one_then(has_main_cursor, crd.cursor_shape, multicursor_shape);
float has_cursor = zero_or_one(final_cursor_shape);
float is_block_cursor = has_cursor * one_if_equal_zero_otherwise(final_cursor_shape, 1.0);
ColorPair main_cursor;
main_cursor.bg = color_to_vec(crd.main_cursor_bg);
main_cursor.fg = color_to_vec(crd.main_cursor_fg);
ColorPair extra_cursor = resolve_extra_cursor_colors(cell_bg, cell_fg, main_cursor);
ColorPair cursor = if_one_then_pair(has_main_cursor, main_cursor, extra_cursor);
cell_out.has_cursor = has_cursor;
cell_out.has_block_cursor = is_block_cursor;
cell_out.pos = pos;
cell_out.cursor_fg_sprite_idx = cursor_shape_map[int(final_cursor_shape)];
cell_out.cursor = cursor;
return cell_out;
}
float background_opacity_for(uint bg, uint colorval, float opacity_if_matched) {
float not_matched = step(1.0, abs(float(colorval) - float(bg)));
return not_matched + opacity_if_matched * (1.0 - not_matched);
}
float calc_background_opacity(uint bg) {
return (
background_opacity_for(bg, crd.bg_colors0, crd.bg_opacities0) *
background_opacity_for(bg, crd.bg_colors1, crd.bg_opacities1) *
background_opacity_for(bg, crd.bg_colors2, crd.bg_opacities2) *
background_opacity_for(bg, crd.bg_colors3, crd.bg_opacities3) *
background_opacity_for(bg, crd.bg_colors4, crd.bg_opacities4) *
background_opacity_for(bg, crd.bg_colors5, crd.bg_opacities5) *
background_opacity_for(bg, crd.bg_colors6, crd.bg_opacities6) *
background_opacity_for(bg, crd.bg_colors7, crd.bg_opacities7)
);
}
// Override foreground colors {{{
float3 fg_override_luminance(float colored_sprite, float under_luminance, float over_lumininace, float3 under, float3 over) {
// 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 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) {
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;
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);
float3 result_b = clamp(hsluvToRgb(float3(over_hsluv.x, over_hsluv.y, target_lum_b * 100.0)), 0.0, 1.0);
float result_a_ratio = contrast_ratio(under_luminance, dot(result_a, Y));
float result_b_ratio = contrast_ratio(under_luminance, dot(result_b, Y));
float3 result = lerp(result_a, result_b, step(result_a_ratio, result_b_ratio));
float fallback_condition = max(step(diff.x + diff.y + diff.z, 0.001), step(min_contrast_ratio, ratio));
return lerp(result, over, fallback_condition);
}
float3 override_foreground_color(float3 over, float3 under, float colored_sprite) {
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);
}
// }}}
[shader("vertex")]
VertexOutput vertex_main(
VertexInput vi,
uint vertex_id : SV_VertexID,
uint instance_id : SV_InstanceID,
uniform uint draw_bg_bitfield,
uniform float row_offset,
) {
VertexOutput vo;
// set cell color indices {{{
uint2 default_colors = uint2(crd.default_fg, crd.bg_colors0);
uint text_attrs = vi.sprite_idx[1];
uint is_reversed = ((text_attrs >> REVERSE_SHIFT) & BIT_MASK);
uint is_inverted = is_reversed + crd.inverted;
int fg_index = fg_index_map[is_inverted];
int bg_index = 1 - fg_index;
int mark = int(text_attrs >> MARK_SHIFT) & MARK_MASK;
uint has_mark = uint(step(1, float(mark)));
uint bg_as_uint = resolve_color(vi.colors[bg_index], default_colors[bg_index]);
bg_as_uint = has_mark * color_table[NUM_COLORS + mark - 1] + (BIT_MASK - has_mark) * bg_as_uint;
float cell_has_default_bg = 1.f - step(1.f, abs(float(bg_as_uint - crd.bg_colors0))); // 1 if has default bg else 0
float3 bg = color_to_vec(bg_as_uint);
uint fg_as_uint = resolve_color(vi.colors[fg_index], default_colors[fg_index]);
fg_as_uint = has_mark * color_table[NUM_COLORS + MARK_MASK + mark] + (1u - has_mark) * fg_as_uint;
float3 foreground = color_to_vec(fg_as_uint);
CellData cell_data = set_vertex_position(foreground, bg, instance_id, vertex_id, row_offset, vi.sprite_idx, vi.is_selected, vo);
// }}}
// Foreground {{{
if (!ONLY_BACKGROUND) { // background does not depend on foreground
float has_dim = float((text_attrs >> DIM_SHIFT) & BIT_MASK), has_blink = float((text_attrs >> BLINK_SHIFT) & BIT_MASK);
vo.effective_text_alpha = crd.inactive_text_alpha * if_one_then(has_dim, crd.dim_opacity, 1.0) * if_one_then(
has_blink, crd.blink_opacity, 1.0);
float in_url = float((vi.is_selected >> 1) & BIT_MASK);
vo.decoration_fg = if_one_then(in_url, color_to_vec(crd.url_color), to_color(vi.colors[2], fg_as_uint));
// Selection
float3 selection_color = if_one_then(crd.use_cell_bg_for_selection_fg, bg, color_to_vec(crd.highlight_fg));
selection_color = if_one_then(crd.use_cell_fg_for_selection_fg, foreground, selection_color);
foreground = if_one_then(float(vi.is_selected & BIT_MASK), selection_color, foreground);
vo.decoration_fg = if_one_then(float(vi.is_selected & BIT_MASK), selection_color, vo.decoration_fg);
// Underline and strike through (rendered via sprites)
uint2 decs = get_decorations_indices(vi.sprite_idx, uint(in_url), text_attrs);
vo.strike_pos = to_sprite_pos(cell_data.pos, decs[0]);
vo.underline_pos = to_sprite_pos(cell_data.pos, decs[1]);
vo.underline_exclusion_pos = to_underline_exclusion_pos(vi.sprite_idx);
// Cursor
vo.cursor_color_premult = float4(cell_data.cursor.bg * crd.cursor_opacity, crd.cursor_opacity);
float3 final_cursor_text_color = lerp(foreground, cell_data.cursor.fg, crd.cursor_opacity);
foreground = if_one_then(cell_data.has_block_cursor, final_cursor_text_color, foreground);
vo.decoration_fg = if_one_then(cell_data.has_block_cursor, final_cursor_text_color, vo.decoration_fg);
vo.cursor_pos = to_sprite_pos(cell_data.pos, cell_data.cursor_fg_sprite_idx * uint(cell_data.has_cursor));
}
// }}}
// Background {{{
float bg_alpha = calc_background_opacity(bg_as_uint);
// we use max so that opacity of the block cursor cell background goes from bg_alpha to 1
float effective_cursor_opacity = max(crd.cursor_opacity, bg_alpha);
// is_special_cell is either 0 or 1
float is_special_cell = cell_data.has_block_cursor + float(vi.is_selected & BIT_MASK);
is_special_cell += float(is_reversed); // reverse video cells should be opaque as well
is_special_cell = zero_or_one(is_special_cell);
cell_has_default_bg = if_one_then(is_special_cell, 0., cell_has_default_bg);
// special cells must always be fully opaque, otherwise leave bg_alpha untouched
bg_alpha = if_one_then(is_special_cell, 1.f, bg_alpha);
// Selection and cursor
bg_alpha = if_one_then(cell_data.has_block_cursor, effective_cursor_opacity, bg_alpha);
bg = if_one_then(float(vi.is_selected & BIT_MASK), if_one_then(crd.use_cell_for_selection_bg, color_to_vec(fg_as_uint), color_to_vec(crd.highlight_bg)), bg);
float3 background_rgb = if_one_then(cell_data.has_block_cursor, lerp(bg, cell_data.cursor.bg, crd.cursor_opacity), bg);
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_FOREGROUND) {
float4 bgpremul = vec4_premul(background_rgb, bg_alpha);
// draw_bg_bitfield has bit 0 set to draw default bg cells and bit 1 set to draw non-default bg cells
float cell_has_non_default_bg = 1.f - cell_has_default_bg;
uint draw_bg_mask = uint(2.f * cell_has_non_default_bg + cell_has_default_bg); // 1 if has default bg else 2
float draw_bg = step(0.5, float(draw_bg_bitfield & draw_bg_mask));
bgpremul *= draw_bg;
vo.effective_background_premul = bgpremul;
}
if (!ONLY_BACKGROUND) vo.cell_foreground = foreground;
return vo;
}
uniform Sampler2DArray sprites;
// Scaling factor for the extra text-alpha adjustment for luminance-difference.
static const float text_gamma_scaling = 0.5;
float clamp_to_unit_float(float x) {
// Clamp value to suitable output range
return clamp(x, 0.0f, 1.0f);
}
float4 foreground_contrast_new(float4 over, float3 under, float text_contrast, float text_gamma_adjustment) {
float under_luminance = dot(under, Y);
float over_lumininace = dot(over.rgb, Y);
// Apply additional gamma-adjustment scaled by the luminance difference, the darker the foreground the more adjustment we apply.
// A multiplicative contrast is also available to increase saturation.
over.a = clamp_to_unit_float(lerp(over.a, pow(over.a, text_gamma_adjustment), (1 - over_lumininace + under_luminance) * text_gamma_scaling) * text_contrast);
return over;
}
float4 foreground_contrast_old(float4 over, float3 under) {
// Simulation of gamma-incorrect blending
float under_luminance = dot(under, Y);
float over_lumininace = dot(over.rgb, Y);
// This is the original gamma-incorrect rendering, it is the solution of the following equation:
//
// linear2srgb(over * overA2 + under * (1 - overA2)) = linear2srgb(over) * over.a + linear2srgb(under) * (1 - over.a)
// ^ gamma correct blending with new alpha ^ gamma incorrect blending with old alpha
over.a = clamp_to_unit_float((srgb2linear(linear2srgb(over_lumininace) * over.a + linear2srgb(under_luminance) * (1.0f - over.a)) - under_luminance) / (over_lumininace - under_luminance));
return over;
}
float4 foreground_contrast(float4 over, float3 under, float text_contrast, float text_gamma_adjustment) {
if (TEXT_NEW_GAMMA) return foreground_contrast_new(over, under, text_contrast, text_gamma_adjustment);
return foreground_contrast_old(over, under);
}
float4 load_text_foreground_color(float3 sprite_pos, float colored_sprite, float3 cell_foreground) {
// For colored sprites use the color from the sprite rather than the text foreground
// Return non-premultiplied foreground color
float4 text_fg = sprites.Sample(sprite_pos);
return float4(lerp(cell_foreground, text_fg.xyz, colored_sprite), text_fg.w);
}
float4 calculate_premul_foreground_from_sprites(
float3 sprite_pos, float3 underline_pos, float3 cursor_pos, float3 strike_pos, uint underline_exclusion_pos,
float4 text_fg, float3 decoration_fg, float4 cursor_color_premult,
float effective_text_alpha,
) {
// Return premul foreground color from decorations (cursor, underline, strikethrough)
int width, height, layer;
sprites.GetDimensions(width, height, layer);
float3 sz = float3(float(width), float(height), float(layer));
float underline_alpha = sprites.Sample(underline_pos).w;
int3 fetch_coord = int3(int(sprite_pos.x * sz.x), int(underline_exclusion_pos), int(sprite_pos.z));
float underline_exclusion = sprites[fetch_coord].w;
underline_alpha *= 1.0 - underline_exclusion;
float strike_alpha = sprites.Sample(strike_pos).w;
float cursor_alpha = sprites.Sample(cursor_pos).w;
float combined_alpha = min(text_fg.w + strike_alpha, 1.0);
float4 ans = alpha_blend(
float4(text_fg.rgb, combined_alpha * effective_text_alpha),
float4(decoration_fg, underline_alpha * effective_text_alpha)
);
return lerp(ans, cursor_color_premult, cursor_alpha * cursor_color_premult.w);
}
float4 adjust_foreground_contrast_with_background(float4 text_fg, float3 bg, float text_contrast, float text_gamma_adjustment) {
return foreground_contrast(text_fg, bg, text_contrast, text_gamma_adjustment);
}
[shader("fragment")]
float4 fragment_main(
VertexOutput vo,
uniform float text_contrast,
uniform float text_gamma_adjustment,
) : SV_Target {
float4 ans_premul = 0;
if (!ONLY_FOREGROUND) ans_premul = vo.effective_background_premul;
if (!ONLY_BACKGROUND) {
// blend in the foreground color
float4 text_fg = load_text_foreground_color(vo.sprite_pos, vo.colored_sprite, vo.cell_foreground);
text_fg = adjust_foreground_contrast_with_background(text_fg, vo.background, text_contrast, text_gamma_adjustment);
float4 text_fg_premul = calculate_premul_foreground_from_sprites(
vo.sprite_pos, vo.underline_pos, vo.cursor_pos, vo.strike_pos, vo.underline_exclusion_pos,
text_fg, vo.decoration_fg, vo.cursor_color_premult, vo.effective_text_alpha);
if (ONLY_FOREGROUND) ans_premul = text_fg_premul;
else ans_premul = alpha_blend_premul(text_fg_premul, ans_premul);
}
return ans_premul;
}

View File

@@ -0,0 +1,49 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
import blit_common;
import alpha_blend;
import utils;
extern static const bool is_alpha_mask = false;
extern static const bool texture_is_not_premultiplied = false;
struct VSOutput
{
float2 texcoord : TEXCOORD;
float4 position : SV_Position;
};
[shader("vertex")]
VSOutput vertex_main(
uint vertex_id : SV_VertexID,
uniform float4 src_rect,
uniform float4 dest_rect,
) {
BlitOutput ans = get_coords_for_blit(vertex_id, src_rect, dest_rect);
return {ans.texcoord, float4(ans.position[0], ans.position[1], 0.0, 1.0)};
}
uniform Sampler2D image;
[shader("fragment")]
float4 fragment_main(
float2 texcoord : TEXCOORD,
uniform float3 amask_fg,
uniform float4 amask_bg_premult,
uniform float extra_alpha
) : SV_Target {
float4 color = image.Sample(texcoord);
if (is_alpha_mask) {
color = float4(amask_fg, color.r);
color = vec4_premul(color);
color = alpha_blend_premul(color, amask_bg_premult);
} else color.a *= extra_alpha;
if (texture_is_not_premultiplied) color = vec4_premul(color);
return color;
}

216
kitty/shaders/hsluv.slang Normal file
View File

@@ -0,0 +1,216 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
module hsluv;
// Helper Functions
float divide(float num, float denom) {
return num / (abs(denom) + 1e-15) * sign(denom);
}
float3 divide(float3 num, float3 denom) {
return num / (abs(denom) + 1e-15) * sign(denom);
}
float3 hsluv_intersectLineLine(float3 line1x, float3 line1y, float3 line2x, float3 line2y) {
return (line1y - line2y) / (line2x - line1x);
}
float3 hsluv_distanceFromPole(float3 pointx, float3 pointy) {
return sqrt(pointx * pointx + pointy * pointy);
}
float3 hsluv_lengthOfRayUntilIntersect(float theta, float3 x, float3 y) {
float3 len = divide(y, sin(theta) - x * cos(theta));
len = lerp(len, (float3)1000.0, step(len, (float3)0.0));
return len;
}
float hsluv_maxSafeChromaForL(float L) {
// Transposed from GLSL column-major constructor to Slang row-major layout
float3x3 m2 = float3x3(
3.2409699419045214, -1.5373831775700935, -0.49861076029300328,
-0.96924363628087983, 1.8759675015077207, 0.041555057407175613,
0.055630079696993609,-0.20397695888897657, 1.0569715142428786
);
float sub0 = L + 16.0;
float sub1 = sub0 * sub0 * sub0 * 0.000000641;
float sub2 = lerp(L / 903.2962962962963, sub1, step(0.0088564516790356308, sub1));
float3 top1 = (284517.0 * m2[0] - 94839.0 * m2[2]) * sub2;
float3 bottom = (632260.0 * m2[2] - 126452.0 * m2[1]) * sub2;
float3 top2 = (838422.0 * m2[2] + 769860.0 * m2[1] + 731718.0 * m2[0]) * L * sub2;
float3 bounds0x = top1 / bottom;
float3 bounds0y = top2 / bottom;
float3 bounds1x = top1 / (bottom + 126452.0);
float3 bounds1y = (top2 - 769860.0 * L) / (bottom + 126452.0);
float3 xs0 = hsluv_intersectLineLine(bounds0x, bounds0y, -1.0 / bounds0x, (float3)0.0);
float3 xs1 = hsluv_intersectLineLine(bounds1x, bounds1y, -1.0 / bounds1x, (float3)0.0);
float3 lengths0 = hsluv_distanceFromPole(xs0, bounds0y + xs0 * bounds0x);
float3 lengths1 = hsluv_distanceFromPole(xs1, bounds1y + xs1 * bounds1x);
return min(lengths0.x,
min(lengths1.x,
min(lengths0.y,
min(lengths1.y,
min(lengths0.z,
lengths1.z)))));
}
float hsluv_maxChromaForLH(float L, float H) {
float hrad = radians(H);
// Transposed from GLSL column-major constructor to Slang row-major layout
float3x3 m2 = float3x3(
3.2409699419045214, -1.5373831775700935, -0.49861076029300328,
-0.96924363628087983, 1.8759675015077207, 0.041555057407175613,
0.055630079696993609,-0.20397695888897657, 1.0569715142428786
);
float sub1 = pow(L + 16.0, 3.0) / 1560896.0;
float sub2 = lerp(L / 903.2962962962963, sub1, step(0.0088564516790356308, sub1));
float3 top1 = (284517.0 * m2[0] - 94839.0 * m2[2]) * sub2;
float3 bottom = (632260.0 * m2[2] - 126452.0 * m2[1]) * sub2;
float3 top2 = (838422.0 * m2[2] + 769860.0 * m2[1] + 731718.0 * m2[0]) * L * sub2;
float3 bound0x = top1 / bottom;
float3 bound0y = top2 / bottom;
float3 bound1x = top1 / (bottom + 126452.0);
float3 bound1y = (top2 - 769860.0 * L) / (bottom + 126452.0);
float3 lengths0 = hsluv_lengthOfRayUntilIntersect(hrad, bound0x, bound0y);
float3 lengths1 = hsluv_lengthOfRayUntilIntersect(hrad, bound1x, bound1y);
return min(lengths0.x,
min(lengths1.x,
min(lengths0.y,
min(lengths1.y,
min(lengths0.z,
lengths1.z)))));
}
float3 hsluv_fromLinear(float3 c) {
return lerp(c * 12.92, 1.055 * pow(max(c, (float3)0), (float3)(1.0 / 2.4)) - 0.055, step(0.0031308, c));
}
float3 hsluv_toLinear(float3 c) {
return lerp(c / 12.92, pow(max((c + 0.055) / (1.0 + 0.055), (float3)0), (float3)2.4), step(0.04045, c));
}
float hsluv_yToL(float Y) {
return lerp(Y * 903.2962962962963, 116.0 * pow(max(Y, 0), 1.0 / 3.0) - 16.0, step(0.0088564516790356308, Y));
}
float hsluv_lToY(float L) {
return lerp(L / 903.2962962962963, pow((max(L, 0) + 16.0) / 116.0, 3.0), step(8.0, L));
}
float3 xyzToRgb(float3 tuple) {
// Transposed layout from GLSL column-major matrix construction
const float3x3 m = float3x3(
3.2409699419045214, -0.96924363628087983, 0.055630079696993609,
-1.5373831775700935, 1.8759675015077207, -0.20397695888897657,
-0.49861076029300328, 0.041555057407175613, 1.0569715142428786
);
// GLSL `tuple * m` is transformed to Slang/HLSL `mul(m, tuple)`
return hsluv_fromLinear(mul(m, tuple));
}
float3 rgbToXyz(float3 tuple) {
// Transposed layout from GLSL column-major matrix construction
const float3x3 m = float3x3(
0.41239079926595948, 0.21263900587151036, 0.019330818715591851,
0.35758433938387796, 0.71516867876775593, 0.11919477979462599,
0.18048078840183429, 0.072192315360733715, 0.95053215224966058
);
// GLSL `tuple * m` is transformed to Slang/HLSL `mul(m, tuple)`
return mul(m, hsluv_toLinear(tuple));
}
float3 xyzToLuv(float3 tuple) {
float X = tuple.x;
float Y = tuple.y;
float Z = tuple.z;
float L = hsluv_yToL(Y);
float div = 1.0 / max(dot(tuple, float3(1, 15, 3)), 1e-15);
return float3(
1.0,
(52.0 * (X * div) - 2.57179),
(117.0 * (Y * div) - 6.08816)
) * L;
}
float3 luvToXyz(float3 tuple) {
float L = tuple.x;
float U = divide(tuple.y, 13.0 * L) + 0.19783000664283681;
float V = divide(tuple.z, 13.0 * L) + 0.468319994938791;
float Y = hsluv_lToY(L);
float X = 2.25 * U * Y / V;
float Z = (3.0 / V - 5.0) * Y - (X / 3.0);
return float3(X, Y, Z);
}
float3 luvToLch(float3 tuple) {
float L = tuple.x;
float U = tuple.y;
float V = tuple.z;
float C = length(tuple.yz);
float H = degrees(atan2(V, U)); // Slang standard library uses atan2(y, x)
H += 360.0 * step(H, 0.0);
return float3(L, C, H);
}
float3 lchToLuv(float3 tuple) {
float hrad = radians(tuple.z);
return float3(
tuple.x,
cos(hrad) * tuple.y,
sin(hrad) * tuple.y
);
}
float3 hsluvToLch(float3 tuple) {
tuple.y *= hsluv_maxChromaForLH(tuple.z, tuple.x) * 0.01;
return tuple.zyx;
}
float3 lchToHsluv(float3 tuple) {
tuple.y = divide(tuple.y, hsluv_maxChromaForLH(tuple.x, tuple.z) * 0.01);
return tuple.zyx;
}
float3 lchToRgb(float3 tuple) {
return xyzToRgb(luvToXyz(lchToLuv(tuple)));
}
float3 rgbToLch(float3 tuple) {
return luvToLch(xyzToLuv(rgbToXyz(tuple)));
}
public float3 hsluvToRgb(float3 tuple) {
return lchToRgb(hsluvToLch(tuple));
}
public float3 rgbToHsluv(float3 tuple) {
return lchToHsluv(rgbToLch(tuple));
}
float3 luvToRgb(float3 tuple) {
return xyzToRgb(luvToXyz(tuple));
}

View File

@@ -7,8 +7,8 @@ from functools import lru_cache, partial
from itertools import count from itertools import count
from typing import Any, Literal, NamedTuple, Optional from typing import Any, Literal, NamedTuple, Optional
from .constants import read_kitty_resource from kitty.constants import read_kitty_resource
from .fast_data_types import ( from kitty.fast_data_types import (
BGIMAGE_PROGRAM, BGIMAGE_PROGRAM,
BLINK, BLINK,
BLIT_PROGRAM, BLIT_PROGRAM,

View File

@@ -0,0 +1,38 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
module linear2srgb;
// Scalar sRGB to Linear conversion
public float srgb2linear(float x) {
float lower = x / 12.92;
float upper = pow((x + 0.055f) / 1.055f, 2.4f);
// GLSL mix() is replaced by Slang's lerp()
return lerp(lower, upper, step(0.04045f, x));
}
// Scalar Linear to sRGB conversion
public float linear2srgb(float x) {
float lower = 12.92 * x;
float upper = 1.055 * pow(x, 1.0f / 2.4f) - 0.055f;
return lerp(lower, upper, step(0.0031308f, x));
}
// Vector Linear to sRGB conversion
public float3 linear2srgb(float3 x) {
float3 lower = 12.92 * x;
float3 upper = 1.055 * pow(x, float3(1.0f / 2.4f)) - 0.055f;
return lerp(lower, upper, step(float3(0.0031308f), x));
}
// Vector sRGB to Linear conversion
public float3 srgb2linear(float3 c) {
// You can call the scalar version per-component,
// or pass the whole vector if you overload it for float3.
return float3(srgb2linear(c.r), srgb2linear(c.g), srgb2linear(c.b));
}

View File

@@ -0,0 +1,68 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
import alpha_blend;
#define left 0
#define top 1
#define right 2
#define bottom 3
static const int2 vertex_pos_map[4] = {
int2(right, top),
int2(right, bottom),
int2(left, bottom),
int2(left, top)
};
static const float4 dest_rect = float4(-1, 1, 1, -1);
[shader("vertex")]
float4 vertex_main(uint vertex_id : SV_VertexID) : SV_Position {
int2 pos = vertex_pos_map[vertex_id];
return float4(dest_rect[pos.x], dest_rect[pos.y], 0, 1);
}
// Signed distance function for a rounded rectangle
float rounded_rectangle_sdf(float2 p, float2 b, float r) {
// signed distance field
// first term is used for points outside the rectangle
float2 q = abs(p) - b;
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
}
[shader("fragment")]
float4 fragment_main(
uniform float4 rect,
uniform float2 params,
uniform float4 color,
uniform float4 background_color,
float4 frag_coord : SV_Position // SV_Position provides gl_FragCoord equivalent in Slang
) : SV_Target {
float2 size = rect.zw; // GLSL .ba maps to .zw or .ba in Slang (using .zw is typical)
float2 origin = rect.xy;
float thickness = params[0];
float corner_radius = params[1];
// Position must be relative to the center of the rectangle of (size) located at (origin)
// frag_coord.xy maps directly to gl_FragCoord.xy
float2 position = frag_coord.xy - size / 2.0 - origin;
// Calculate distance to rounded rectangle
float dist = rounded_rectangle_sdf(position, size * 0.5 - corner_radius, corner_radius);
// The border is outer - inner rects
float outer_edge = -dist;
float inner_edge = outer_edge - thickness;
// Smooth borders (anti-alias)
static const float step_size = 1.0;
float alpha = smoothstep(-step_size, step_size, outer_edge) - smoothstep(-step_size, step_size, inner_edge);
float4 ans = color;
ans.a *= alpha;
// pre-multiplied output
return alpha_blend(ans, background_color);
}

View File

@@ -0,0 +1,88 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
import blit_common;
import linear2srgb;
struct VSOutput {
float2 texcoord : TEXCOORD;
float4 position : SV_Position;
};
[shader("vertex")]
VSOutput vertex_main(
uint vertex_id : SV_VertexID,
uniform float4 src_rect,
uniform float4 dest_rect,
) {
BlitOutput ans = get_coords_for_blit(vertex_id, src_rect, dest_rect);
return {ans.texcoord, float4(ans.position[0], ans.position[1], 0.0, 1.0)};
}
uniform Sampler2D image;
float3 safe_unpremult_to_linear(float4 s) {
// Avoid division by zero by replacing 0.0 alpha with 1.0.
// If alpha is 0.0, the division is safe, and lerp masks the result to 0.0 anyway.
float safe_alpha = lerp(1.0f, s.a, step(0.00001f, s.a));
float3 unpremult = s.rgb / safe_alpha;
// Select between the computed linear color and float3(0.0) based on alpha presence
return lerp(float3(0.0f), srgb2linear(unpremult), step(0.00001f, s.a));
}
[shader("fragment")]
float4 fragment_main(float2 texcoord : TEXCOORD, uniform float2 src_size) : SV_Target {
float4 s00, s10, s01, s11;
float2 texel_size = 1.0 / src_size;
// Use Slang target switches to dynamically compile for specific backend features
__target_switch
{
// Modern backends with full texture gathering capabilities
case spirv: case hlsl: case metal: {
float2 gather_coord = texcoord - (0.5 * texel_size);
float4 r_gather = image.GatherRed(gather_coord);
float4 g_gather = image.GatherGreen(gather_coord);
float4 b_gather = image.GatherBlue(gather_coord);
float4 a_gather = image.GatherAlpha(gather_coord);
s00 = float4(r_gather.w, g_gather.w, b_gather.w, a_gather.w); // Bottom-Left
s10 = float4(r_gather.z, g_gather.z, b_gather.z, a_gather.z); // Bottom-Right
s01 = float4(r_gather.x, g_gather.x, b_gather.x, a_gather.x); // Top-Left
s11 = float4(r_gather.y, g_gather.y, b_gather.y, a_gather.y); // Top-Right
}
// Fallback for older targets or legacy GLSL versions
default: {
s00 = image.Sample(texcoord + float2(-0.25, -0.25) * texel_size);
s10 = image.Sample(texcoord + float2( 0.25, -0.25) * texel_size);
s01 = image.Sample(texcoord + float2(-0.25, 0.25) * texel_size);
s11 = image.Sample(texcoord + float2( 0.25, 0.25) * texel_size);
}
}
// Unpremultiply and convert to linear for each sample
float3 linear00 = safe_unpremult_to_linear(s00);
float3 linear10 = safe_unpremult_to_linear(s10);
float3 linear01 = safe_unpremult_to_linear(s01);
float3 linear11 = safe_unpremult_to_linear(s11);
// Average the alpha values
float avg_alpha = (s00.a + s10.a + s01.a + s11.a) * 0.25;
// For proper downsampling with transparency, weight colors by their alpha
float3 weighted_sum = linear00 * s00.a + linear10 * s10.a + linear01 * s01.a + linear11 * s11.a;
float total_weight = s00.a + s10.a + s01.a + s11.a;
// Calculate the weighted average color in linear space
float safe_total_weight = lerp(1.0f, total_weight, step(0.00001f, total_weight));
float3 avg_linear = lerp(float3(0.0f), weighted_sum / safe_total_weight, step(0.00001f, total_weight));
// Convert back to sRGB
float3 srgb_color = linear2srgb(avg_linear);
// Output unpremultiplied sRGB color
return float4(srgb_color, avg_alpha);
}

716
kitty/shaders/slang.py Normal file
View File

@@ -0,0 +1,716 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2026, Kovid Goyal <kovid at kovidgoyal.net>
import fcntl
import json
import os
import re
import runpy
import shutil
import sys
import time
from collections import OrderedDict
from contextlib import contextmanager, suppress
from enum import StrEnum
from functools import lru_cache
from itertools import chain
from pathlib import Path
from types import MappingProxyType
from typing import Any, Callable, Iterable, Iterator, NamedTuple
from kitty.constants import read_kitty_resource, shaders_dir, slangc
from kitty.fast_data_types import (
BLINK,
COLOR_IS_INDEX,
COLOR_IS_RGB,
COLOR_IS_SPECIAL,
COLOR_NOT_SET,
DECORATION,
DECORATION_MASK,
DIM,
GLSL_VERSION,
MARK,
MARK_MASK,
REVERSE,
STRIKETHROUGH,
get_boss,
get_options,
)
from kitty.options.types import Options, defaults
@lru_cache(maxsize=64)
def get_shader_src(name: str) -> str:
return read_kitty_resource(f'{name}.slang', 'kitty.shaders').decode()
@lru_cache(maxsize=2)
def self_mtime() -> float:
with suppress(Exception):
return os.path.getmtime(__file__)
return 0
@lru_cache(maxsize=2)
def slangc_version() -> str:
import subprocess
return subprocess.check_output(slangc + ['-version'], stderr=subprocess.STDOUT).decode().strip()
def is_dir_slangc_version_ok(path: str) -> bool:
with suppress(OSError), open(os.path.join(path, 'slangc.version')) as f:
return f.read().strip() == slangc_version()
return False
def ensure_cache_dir(path: str) -> None:
os.makedirs(path, exist_ok=True)
# slang IR is version dependent and the compiler often crashes when loading .slang-module from another version
if not is_dir_slangc_version_ok(path):
shutil.rmtree(path)
os.makedirs(path)
with open(os.path.join(path, 'slangc.version'), 'w') as f:
f.write(slangc_version())
class Stage(StrEnum):
vertex = 'vertex'
fragment = 'fragment'
class EntryPoint(NamedTuple):
stage: Stage
name: str
def asdict(self) -> dict[str, str]:
return {'stage': str(self.stage), 'name': self.name}
@classmethod
def fromdict(self, s: dict[str, str]) -> 'EntryPoint':
return EntryPoint(Stage(s['stage']), s['name'])
class Specialization(NamedTuple):
name: str
variables: MappingProxyType[str, str]
@property
def filename_insert(self) -> str:
return f'.{self.name}' if self.name else '.default-specialization'
class SlangFile(NamedTuple):
path: str = ''
text: str = ''
imports: frozenset[str] = frozenset()
entry_points: frozenset[EntryPoint] = frozenset()
module: str = ''
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()
ans['imports'] = tuple(ans['imports'])
ans['entry_points'] = tuple(ep.asdict() for ep in ans['entry_points'])
ans['specializable_variables'] = dict(ans['specializable_variables'])
ans['disable_warnings'] = tuple(ans['disable_warnings'])
if skip_source:
ans['text'] = ''
ans['path'] = os.path.basename(ans['path'])
del ans['opts']
return ans
@classmethod
def fromdict(cls, s: dict[str, Any]) -> 'SlangFile':
return SlangFile(
s['path'], s['text'], frozenset(s['imports']),
frozenset(EntryPoint.fromdict(x) for x in s['entry_points']),
s['module'], MappingProxyType(s['specializable_variables']), frozenset(s['disable_warnings']))
@property
def should_compile_to_ir(self) -> bool:
return bool(self.module or self.entry_points)
@property
def defines(self) -> MappingProxyType[str, str]:
ans = {}
match os.path.basename(self.path):
case 'cell.slang':
ans['MARK_MASK'] = str(MARK_MASK)
ans['REVERSE_SHIFT'] = str(REVERSE)
ans['STRIKE_SHIFT'] = str(STRIKETHROUGH)
ans['DIM_SHIFT'] = str(DIM)
ans['BLINK_SHIFT'] = str(BLINK)
ans['DECORATION_SHIFT'] = str(DECORATION)
ans['MARK_SHIFT'] = str(MARK)
ans['DECORATION_MASK'] = str(DECORATION_MASK)
ans['COLOR_NOT_SET'] = str(COLOR_NOT_SET)
ans['COLOR_IS_SPECIAL'] = str(COLOR_IS_SPECIAL)
ans['COLOR_IS_INDEX'] = str(COLOR_IS_INDEX)
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:
return Specialization(name, MappingProxyType(kwargs))
match os.path.basename(self.path):
case 'graphics.slang':
yield s()
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)
case _:
yield s()
def parse_slang_text(src_code: str, path: str = '') -> SlangFile:
text = re.sub(r'/\*[\s\S]*?\*/', '', src_code)
entry_points, imports = [], set()
module = ''
found_entry_point = ''
specializable_variables = {}
disable_warnings = []
for line in text.splitlines():
line = line.strip()
if not line:
continue
if line.startswith('//'):
if line.startswith('// warnings-disable: '):
words = line.split()
for word in words[2:]:
for w in word.split(','):
disable_warnings.append(w)
continue
words = line.split()
if found_entry_point:
if words[0].startswith('['): # ]
continue
for q in words:
if '(' in q:
name = q.partition('(')[0] # ))
match found_entry_point:
case 'vertex':
entry_points.append(EntryPoint(Stage.vertex, name))
case 'fragment' | 'pixel':
entry_points.append(EntryPoint(Stage.fragment, name))
break
found_entry_point = ''
else:
match words[0]:
case 'module':
module = words[1].removesuffix(';')
case 'import':
imports.add(words[1].removesuffix(';'))
case 'extern':
if len(words) > 3 and words[1:3] == ['static', 'const']:
specializable_variables[line.partition('=')[0].split()[-1].rstrip(';')] = line
case _:
if words[0].startswith('[shader('): # ])
text = words[0].partition('(')[2].partition(')')[0].strip()
found_entry_point = text[1:-1]
return SlangFile(
path, src_code, frozenset(imports), frozenset(entry_points), module,
MappingProxyType(specializable_variables), frozenset(disable_warnings))
@lru_cache(4096)
def parse_slang_file(path: str) -> SlangFile:
with open(path) as f:
text = f.read()
return parse_slang_text(text, path)
def build_import_graph(dirpath: str) -> dict[str, SlangFile]:
graph: dict[str, SlangFile] = {}
for root, _, files in os.walk(os.path.abspath(dirpath)):
for file in files:
if file.endswith('.slang'):
full_path = os.path.abspath(os.path.join(root, file))
relpath = os.path.relpath(full_path, root)
modname = os.path.splitext(relpath.replace(os.sep, '.'))[0]
graph[modname] = parse_slang_file(full_path)
return graph
def topological_sort(graph: dict[str, SlangFile]) -> list[str]:
visited = set()
order = []
def visit(node: str) -> None:
if node in visited or node not in graph:
return
for dep in graph[node].imports:
visit(dep)
visited.add(node)
order.append(node)
for node in graph:
visit(node)
return order
def get_ordered_sources_in_tree(dirpath: str) -> OrderedDict[str, SlangFile]:
g = build_import_graph(dirpath)
return OrderedDict({k: g[k] for k in topological_sort(g)})
def future() -> float:
return time.time() + 1000000
def safe_mtime(path: str, defval: float = 0) -> float:
with suppress(OSError):
return os.path.getmtime(path)
return defval if defval >= 0 else future()
def read_deps_file(path: str) -> Iterator[str]:
with open(path) as f:
for line in f:
line = line.partition(':')[2].strip()
yield from line.split()
def get_newest_dep_time(path: str) -> float:
with suppress(OSError):
ans = 0.
for deppath in read_deps_file(path):
mtime = os.path.getmtime(deppath)
ans = max(mtime, ans)
return max(ans, self_mtime())
return future()
class Command(NamedTuple):
needs_build: bool
description: str
cmd: list[str]
def commands_to_compile_dir_to_ir(sources: dict[str, SlangFile], src_dir: str, output_dirpath: str) -> Iterator[Command]:
cmdbase = list(slangc) + ['-warnings-as-errors', 'all']
for name, sfile in sources.items():
if sfile.should_compile_to_ir:
parts = name.split('.')
base_dest = os.path.join(output_dirpath, *parts)
slang_module = f'{base_dest}.slang-module'
deps_file = f'{base_dest}.deps'
module_mtime = safe_mtime(slang_module)
needs_build = module_mtime < get_newest_dep_time(deps_file)
defines = [f'-D{k}={v}' for k, v in sfile.defines.items()]
yield Command(needs_build, f'Compiling |{name}.slang| ...', cmdbase + defines + [
'-I', output_dirpath, '-I', src_dir, '-depfile', deps_file,
'-target', 'none', '-o', slang_module, '--', sfile.path,
])
def iter_entry_point_shaders(sources: dict[str, SlangFile], dest_dir: str) -> Iterator[tuple[str, str, list[str], SlangFile]]:
cmdbase = list(slangc) + ['-warnings-as-errors', 'all']
for name, sfile in sources.items():
if not sfile.entry_points:
continue
parts = name.split('.')
base_dest = os.path.join(dest_dir, *parts)
slang_module = f'{base_dest}.slang-module'
cmd = list(cmdbase)
if sfile.disable_warnings:
cmd += ['-warnings-disable', ','.join(sfile.disable_warnings)]
cmd += ['-I', dest_dir, slang_module]
yield base_dest, slang_module, cmd, sfile
def serialize_source_metadata(sources: dict[str, SlangFile], dest_dir: str) -> None:
for base_dest, slang_module, scmd, sfile in iter_entry_point_shaders(sources, dest_dir):
dest = f'{base_dest}.json'
with open(dest, 'w') as f:
f.write(json.dumps(sfile.asdict(skip_source=True), indent=2, sort_keys=True))
def commands_to_compile_to_spirv(sources: dict[str, SlangFile], dest_dir: str, built_files: list[str]) -> Iterator[Command]:
# glsl 450 is vulkan 1.1 and spirv 1.3 released 2008
base_cmd = ['-target', 'spirv', '-profile', 'glsl_450', '-capability', 'vk_mem_model', '-fvk-use-entrypoint-name']
for base_dest, slang_module, scmd, sfile in iter_entry_point_shaders(sources, dest_dir):
for x in sfile.specializations:
cmd = list(scmd)
dest = f'{base_dest}.{x.name}.spv' if x.name else f'{base_dest}.spv'
if x.variables:
cmd.insert(-1, f'{base_dest}{x.filename_insert}.slang-module')
cmd += base_cmd + ['-o', dest, '-reflection-json', dest + '.json']
output_mtime = safe_mtime(dest)
module_mtime = os.path.getmtime(slang_module)
needs_build = output_mtime < module_mtime
if needs_build:
built_files.append(dest)
yield Command(needs_build, f'Linking |{os.path.basename(dest)}| ...', cmd)
# GLSL {{{
def commands_to_compile_to_glsl(sources: dict[str, SlangFile], dest_dir: str, built_glsl_files: list[str]) -> Iterator[Command]:
glsl_version = max(150, GLSL_VERSION) # slangc fails with glsl_140 https://github.com/shader-slang/slang/issues/11898
for base_dest, slang_module, cmd, sfile in iter_entry_point_shaders(sources, dest_dir):
module_mtime = os.path.getmtime(slang_module)
extra_cmd = ['-line-directive-mode', 'none', '-target', 'glsl', '-profile', f'glsl_{glsl_version}']
for ep in sfile.entry_points:
for sp in sfile.specializations:
v = {Stage.vertex: 'vert', Stage.fragment: 'frag'}[ep.stage]
c = list(cmd)
dest = f'{base_dest}{sp.filename_insert}.{v}.glsl' if sp.name else f'{base_dest}.{v}.glsl'
if sp.variables:
c.insert(-1, f'{base_dest}{sp.filename_insert}.slang-module')
c += extra_cmd + ['-entry', ep.name, '-stage', ep.stage.name, '-o', dest]
output_mtime = safe_mtime(dest)
needs_build = output_mtime < module_mtime
if needs_build:
built_glsl_files.append(dest)
yield Command(needs_build, f'Linking |{os.path.basename(slang_module)}| to GLSL {ep.stage.value} shader ...', c)
def fixup_opengl_code(glsl_code: str, path: str) -> tuple[str, dict[str, Any]]:
is_fragment_shader = 'frag' in os.path.basename(path).split()
lines = []
in_uniform_block = False
in_uniform_block_contents = False
uniform_block_is_struct = False
current_uniform_struct_members: dict[str, str] = {}
uniform_blocks = {}
current_uniform_names: list[str] = []
uniform_names: dict[str, str] = {}
uniform_structs = {}
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}')
uniform_names[uniform_name] = name
return name
src_lines = glsl_code.splitlines()
for i, line in enumerate(src_lines):
if in_uniform_block:
if in_uniform_block_contents:
if line.startswith('}'):
in_uniform_block = in_uniform_block_contents = False
block_name = line.lstrip('}').rstrip(';').strip()
if uniform_block_is_struct:
uniform_structs[block_name.rpartition('_')[0]] = {
'name': block_name, 'members': current_uniform_struct_members}
else:
uniform_blocks[block_name] = current_uniform_names
line = '// ' + line
current_uniform_names = []
else:
if uniform_block_is_struct:
current_uniform_names.append(add_uniform_name(line.split()[-1], current_uniform_struct_members))
else:
line = line.strip()
current_uniform_names.append(add_uniform_name(line.split()[-1]))
line = 'uniform ' + line
elif line.startswith('{'): # }}
if not uniform_block_is_struct:
line = '// ' + line
in_uniform_block_contents = True
current_uniform_names = []
else:
if line.startswith('#version '):
line = f'#version {GLSL_VERSION}'
if not is_fragment_shader:
line += '\n#extension GL_ARB_explicit_attrib_location : require'
elif line.startswith('#extension ') or line in ('layout(row_major) buffer;', 'layout(push_constant)'):
line = '// ' + line
elif line.startswith('layout(binding ='):
line = '// ' + line
elif line.startswith('layout(location =') and not is_fragment_shader:
line = '// ' + line
elif line.startswith('flat layout(location ='):
line = 'flat'
elif line: # ))))
words = line.split()
if 'uniform' in words and line.startswith('layout('): # )
in_uniform_block = True
in_uniform_block_contents = False
uniform_block_is_struct = line.startswith('layout(std140') # )
if uniform_block_is_struct:
current_uniform_struct_members = {}
else:
line = '// ' + line
elif words[0] == 'uniform' and len(words) > 2 and words[1].startswith('sampler'):
add_uniform_name(words[2])
lines.append(line)
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')
return ans, {'loose_uniforms': uniform_names, 'uniform_structs': uniform_structs}
def fixup_opengl_files(*paths: str) -> None:
' Convert the GLSL output of slangc to something that will work with OpenGL 3.1 '
for path in paths:
with open(path, 'r+') as f:
glsl_code = f.read()
try:
fixed, metadata = fixup_opengl_code(glsl_code, path)
except Exception:
os.unlink(path)
raise
f.seek(0)
f.truncate()
f.write(fixed)
with open(path + '.json', 'w') as f:
f.write(json.dumps(metadata))
# }}}
ParallelRun = Callable[[Iterable[tuple[bool, str, list[str]]]], None]
def copy_files_preserving_structure(source_dir: str, dest_dir: str, extension: str) -> None:
'''
Copies all files with a specific extension from a source directory
to a destination directory while preserving the subdirectory structure.
'''
source = Path(source_dir)
destination = Path(dest_dir)
if not extension.startswith('.'):
extension = f".{extension}"
# Recursively find all matching files
for file_path in source.rglob(f"*{extension}"):
if file_path.is_file():
# Calculate relative path to maintain folder hierarchy
relative_path = file_path.relative_to(source)
target_path = destination / relative_path
target_path.parent.mkdir(parents=True, exist_ok=True)
# Copy file while preserving original metadata
shutil.copy2(file_path, target_path)
def create_specialisations(sources: dict[str, SlangFile], dest_dir: str) -> Iterator[Command]:
for base_dest, slang_module, cmd, sfile in iter_entry_point_shaders(sources, dest_dir):
if sfile.entry_points and sfile.specializations:
for sp in sfile.specializations:
dest = f'{base_dest}{sp.filename_insert}.slang'
payload = existing = ''
if sp.variables:
lines = []
for key, val in sp.variables.items():
declaration = sfile.specializable_variables[key].rpartition('=')[0]
if not declaration:
declaration = sfile.specializable_variables[key].rstrip(';')
declaration = declaration.replace('extern ', 'export ', 1)
lines.append(f'{declaration} = {val};')
payload = '\n'.join(lines)
with suppress(FileNotFoundError), open(dest) as f:
existing = f.read()
if needs_build := payload != existing:
if payload:
with open(dest, 'w') as fw:
fw.write(payload)
else:
os.remove(dest)
yield Command(needs_build, f'Compiling specialisation |{os.path.basename(dest)}| ...',
list(slangc) + [dest, '-o', dest + '-module'])
def compile_builtin_shaders(build_dir: str, dest_dir: str, parallel_run: ParallelRun) -> None:
ensure_cache_dir(build_dir)
ensure_cache_dir(dest_dir)
src_dir = os.path.abspath('kitty/shaders')
source_tree = get_ordered_sources_in_tree(src_dir)
serialize_source_metadata(source_tree, dest_dir)
# First ensure all IR is generated
parallel_run(commands_to_compile_dir_to_ir(source_tree, src_dir, build_dir))
# Copy IR to dest_dir
copy_files_preserving_structure(build_dir, dest_dir, '.slang-module')
# Create the specializations
parallel_run(create_specialisations(source_tree, dest_dir))
# Now Vulkan shaders
built_spirv_files: list[str] = []
spirv_commands = commands_to_compile_to_spirv(source_tree, dest_dir, built_spirv_files)
# Now glsl files
built_glsl_files: list[str] = []
glsl_commands = commands_to_compile_to_glsl(source_tree, dest_dir, built_glsl_files)
# Now run all commands
parallel_run(chain(spirv_commands, glsl_commands))
fixup_opengl_files(*built_glsl_files)
if shutil.which('glslangValidator'):
from kitty.shaders.validate_shaders import validation_command_for_file
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")}')
setup = runpy.run_path('setup.py')
Command = setup['Command']
parallel_run = setup['parallel_run']
emphasis = setup['emphasis']
def prun(cmds: Iterable[tuple[bool, str, list[str]]]) -> None:
needed = []
for (needs_build, desc, cmd) in cmds:
if needs_build:
desc = re.sub(r'\|(.+?)\|', lambda m: emphasis(m.group(1)), desc)
needed.append(Command(desc, cmd, lambda: True))
parallel_run(needed)
compile_builtin_shaders(sys.argv[-2], sys.argv[-1], prun)
def test_slang_build() -> None:
import subprocess
if shutil.which(slangc[0]) is None:
raise AssertionError(f'The shader slang compiler ({slangc[0]}) not in PATH: {os.environ.get("PATH")}')
q = os.path.join(shaders_dir, 'graphics.spv')
if not os.path.isfile(q):
raise AssertionError(f'The compiled graphics shader {q} does not exist')
if not get_shader_src('graphics'):
raise AssertionError('Could not load graphics.slang shader source')
src = b'''
#language slang 2026
[shader("vertex")]
float4 main(uint vertex_id : SV_VertexID) : SV_Position { return float4(vertex_id, 1, 0, 1); }
'''
cp = subprocess.run(slangc + '-lang slang -entry main -stage vertex -target glsl -o /dev/stdout -- -'.split(),
input=src, capture_output=True)
if cp.returncode != 0:
raise AssertionError(f'Test compile of shader to GLSL failed with returncode: {cp.returncode} and stderr: {cp.stderr.decode()}')
if __name__ == '__main__':
main()

32
kitty/shaders/tint.slang Normal file
View File

@@ -0,0 +1,32 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
// Main Vertex Shader Entry Point
[shader("vertex")]
float4 vertex_main(
uint vertex_id : SV_VertexID,
uniform float4 edges, // [ left, top, right, bottom ]
) : SV_Position {
// Extract boundaries from the edges vector
float left = edges[0];
float top = edges[1];
float right = edges[2];
float bottom = edges[3];
// Static mapping table for vertex positions
const float2 pos_map[4] = {
float2(left, top),
float2(left, bottom),
float2(right, bottom),
float2(right, top)
};
// Calculate final position
return float4(pos_map[vertex_id], 0.0, 1.0);
}
[shader("fragment")]
float4 fragment_main(uniform float4 tint_color) : SV_Target {
return tint_color;
}

41
kitty/shaders/trail.slang Normal file
View File

@@ -0,0 +1,41 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
struct VertexOutput
{
float2 frag_pos : TEXCOORD;
float4 position : SV_Position;
};
// Main Vertex Shader Entry Point
[shader("vertex")]
VertexOutput vertex_main(uint vertex_id : SV_VertexID, uniform float4 x_coords, uniform float4 y_coords) {
VertexOutput output;
float2 pos = float2(x_coords[vertex_id], y_coords[vertex_id]);
output.position = float4(pos, 1.0, 1.0);
output.frag_pos = pos;
return output;
}
// Main Fragment Shader Entry Point
[shader("fragment")]
float4 fragment_main(
float2 frag_pos : TEXCOORD,
uniform float2 cursor_edge_x,
uniform float2 cursor_edge_y,
uniform float3 trail_color,
uniform float trail_opacity
) : SV_Target {
float opacity = trail_opacity;
// Evaluate if the fragment falls inside the bounding box of the cursor
float in_x = step(cursor_edge_x[0], frag_pos.x) * step(frag_pos.x, cursor_edge_x[1]);
float in_y = step(cursor_edge_y[1], frag_pos.y) * step(frag_pos.y, cursor_edge_y[0]);
// Mask out opacity inside the active cursor area
opacity *= 1.0f - (in_x * in_y);
// Output color with premultiplied alpha formatting
return float4(trail_color * opacity, opacity);
}

34
kitty/shaders/utils.slang Normal file
View File

@@ -0,0 +1,34 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
module utils;
// Return 0 if x < 1 otherwise 1
public __generic<T : __BuiltinFloatingPointType, int N = 1>
vector<T, N> zero_or_one(vector<T, N> x) {
return step((vector<T, N>)1.0f, x);
}
// condition must be zero or one. When 1 thenval is returned otherwise elseval
public __generic<T : __BuiltinFloatingPointType, int N = 1>
vector<T, N> if_one_then(vector<T, N> condition, vector<T, N> thenval, vector<T, N> elseval) {
return lerp(elseval, thenval, condition);
}
// a < b ? thenval : elseval
public __generic<T : __BuiltinFloatingPointType, int N = 1>
vector<T, N> if_less_than(vector<T, N> a, vector<T, N> b, vector<T, N> thenval, vector<T, N> elseval) {
return lerp(thenval, elseval, step(b, a));
}
// Replaces vec4(rgb * a, a)
public float4 vec4_premul(float3 rgb, float a) {
return float4(rgb * a, a);
}
// Overloaded variation replacing vec4(rgba.rgb * rgba.a, rgba.a)
public float4 vec4_premul(float4 rgba) {
return float4(rgba.rgb * rgba.a, rgba.a);
}

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python3
import subprocess
import sys
from pathlib import Path
from typing import Iterable
# Map the custom extensions to the required glslangValidator stage strings
stage_mapping = {
'.vert.glsl': 'vert',
'.frag.glsl': 'frag',
'.vertex.glsl': 'vert',
'.fragment.glsl': 'frag',
}
def validation_command_for_file(path: str | Path) -> list[str]:
file_path = Path(path)
matched_ext = next(ext for ext in stage_mapping if file_path.name.endswith(ext))
stage = stage_mapping[matched_ext]
return ['glslangValidator', '-S', stage, str(file_path)]
def validate_glsl_files(shader_files: Iterable[str | Path], verbose: bool = False) -> None:
error_count = 0
# Process each shader file
for file_path in sorted(Path(f) for f in shader_files):
# Identify extension matching suffix
if verbose:
print(f'Validating: {file_path.name}')
result = subprocess.run(validation_command_for_file(file_path))
# Check exit code
if result.returncode != 0:
error_count += 1
print(f'❌ Failed: {file_path.name}', file=sys.stderr)
else:
if verbose:
print(f'✅ Passed: {file_path.name}')
if verbose:
print('-' * 50)
# Print execution summary
if error_count == 0:
if verbose:
print("Success: All shaders validated successfully!")
else:
raise SystemExit(f"Failure: {error_count} shader(s) failed validation.")
def validate_glsl_dir(directory_path: str, verbose: bool = False) -> None:
'''
Validates all GLSL shaders in the specified directory with names matching
name.vert.glsl or name.frag.glsl using glslangValidator.
'''
target_dir = Path(directory_path)
if not target_dir.is_dir():
raise SystemExit(f"Error: Directory '{directory_path}' does not exist.")
# Find all files matching the patterns
shader_files: list[Path] = []
for ext in stage_mapping.keys():
shader_files.extend(target_dir.glob(f'*{ext}'))
if not shader_files:
if verbose:
print(f"No matching shaders (*.vert.glsl or *.frag.glsl) found in '{target_dir}'.")
return
validate_glsl_files(shader_files)
if __name__ == "__main__":
dir_to_scan = sys.argv[1] if len(sys.argv) > 1 else 'shaders'
validate_glsl_dir(dir_to_scan, verbose=True)

View File

@@ -19,7 +19,7 @@ from . import BaseTest
class TestBuild(BaseTest): class TestBuild(BaseTest):
def test_exe(self) -> None: def test_exe(self) -> None:
from kitty.constants import kitten_exe, kitty_exe, str_version from kitty.constants import kitten_exe, kitty_exe, slangc, str_version
exe = kitty_exe() exe = kitty_exe()
self.assertTrue(os.access(exe, os.X_OK)) self.assertTrue(os.access(exe, os.X_OK))
self.assertTrue(os.path.isfile(exe)) self.assertTrue(os.path.isfile(exe))
@@ -28,14 +28,19 @@ class TestBuild(BaseTest):
self.assertTrue(os.access(exe, os.X_OK)) self.assertTrue(os.access(exe, os.X_OK))
self.assertTrue(os.path.isfile(exe)) self.assertTrue(os.path.isfile(exe))
self.assertIn(str_version, subprocess.check_output([exe, '--version']).decode()) self.assertIn(str_version, subprocess.check_output([exe, '--version']).decode())
self.assertTrue(shutil.which(slangc[0]), f'slang compiler: {slangc[0]} not found on PATH: {os.environ["PATH"]}')
def test_loading_extensions(self) -> None: def test_loading_extensions(self) -> None:
import kitty.fast_data_types as fdt import kitty.fast_data_types as fdt
from kittens.transfer import rsync from kittens.transfer import rsync
del fdt, rsync del fdt, rsync
def test_slang_build(self) -> None:
from kitty.shaders.slang import test_slang_build
test_slang_build()
def test_loading_shaders(self) -> None: def test_loading_shaders(self) -> None:
from kitty.shaders import Program from kitty.shaders.legacy import Program
for name in 'cell border bgimage tint graphics'.split(): for name in 'cell border bgimage tint graphics'.split():
Program(name) Program(name)

55
kitty_tests/shaders.py Normal file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2026, Kovid Goyal <kovid at kovidgoyal.net>
import os
import tempfile
from unittest.mock import patch
from kitty.options.types import defaults
from kitty.shaders.slang import specialize_cache, specialize_cell_shader
from . import BaseTest
class TestShaders(BaseTest):
def test_specialize_cell_shader(self):
specialize_cache.clear()
# No action when opts are the same as defaults
self.assertEqual(specialize_cell_shader(opts=defaults), {})
# Mock out compilation to avoid needing slangc
call_count = [0]
def fake_specialize_shaders_to(sources, dest_dir):
call_count[0] += 1
with open(os.path.join(dest_dir, 'cell.glsl'), 'wb') as f:
f.write(b'fake shader ' + str(call_count[0]).encode())
def fake_ensure_cache_dir(path):
os.makedirs(path, exist_ok=True)
with patch('kitty.shaders.slang.specialize_shaders_to', fake_specialize_shaders_to), \
patch('kitty.shaders.slang.ensure_cache_dir', fake_ensure_cache_dir):
# Changing options produces non-empty shaders
opts1 = defaults._replace(text_composition_strategy='legacy')
with tempfile.TemporaryDirectory() as dir1:
result1 = specialize_cell_shader(create_cache_dir=lambda: dir1, opts=opts1)
self.assertNotEqual(result1, {})
# Changing to different options produces different shaders
opts2 = defaults._replace(text_fg_override_threshold=(10.0, '%'))
with tempfile.TemporaryDirectory() as dir2:
result2 = specialize_cell_shader(create_cache_dir=lambda: dir2, opts=opts2)
self.assertNotEqual(result2, {})
self.assertNotEqual(result1, result2)
# Calling twice with the same options returns the same object
opts3 = defaults._replace(text_composition_strategy='legacy')
with tempfile.TemporaryDirectory() as dir3:
get_dir3 = lambda: dir3
result3a = specialize_cell_shader(create_cache_dir=get_dir3, opts=opts3)
result3b = specialize_cell_shader(create_cache_dir=get_dir3, opts=opts3)
self.assertIs(result3a, result3b)

161
kitty_tests/slang.py Normal file
View File

@@ -0,0 +1,161 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2026, Kovid Goyal <kovid at kovidgoyal.net>
import os
import tempfile
from kitty.shaders.slang import EntryPoint, SlangFile, Stage, build_import_graph, parse_slang_text, topological_sort
from . import BaseTest
class TestSlang(BaseTest):
def test_slang_parser(self):
def check(src: str, expected: SlangFile) -> None:
actual = parse_slang_text(src)
actual = actual._replace(text='')
self.assertEqual(expected, actual)
# Basic vertex + fragment entry points
check('''
[shader("vertex")]
void drawTriangle(float4 pos : POSITION) {
// vertex code
}
[shader("fragment")]
[numthreads(1, 1, 1)] // Handles intermediate attributes seamlessly
float4 psMain() : SV_Target {
return float4(1, 0, 0, 1);
}
''', SlangFile('', '', frozenset(), frozenset({EntryPoint(Stage.vertex, 'drawTriangle'), EntryPoint(Stage.fragment, 'psMain')})))
# Empty source
check('', SlangFile())
# Only line comments and block comments, no code
check('// just a comment\n/* block comment */', SlangFile('', '', frozenset(), frozenset()))
# Module and import declarations
check('''
module mymodule;
import utils;
import helpers;
''', SlangFile('', '', frozenset({'utils', 'helpers'}), frozenset(), 'mymodule'))
# pixel stage maps to Stage.fragment
check('''
[shader("pixel")]
float4 pixelMain() : SV_Target { return float4(0); }
''', SlangFile('', '', frozenset(), frozenset({EntryPoint(Stage.fragment, 'pixelMain')})))
# Block comment stripping removes multi-line comments before parsing
check('''
/* This is a block comment
spanning multiple lines */
[shader("vertex")]
void vertMain() {}
''', SlangFile('', '', frozenset(), frozenset({EntryPoint(Stage.vertex, 'vertMain')})))
# Block comment containing a shader attribute must not create a false entry point
check('''
/* [shader("vertex")]
void shouldNotBeDetected() {} */
[shader("fragment")]
void fragMain() {}
''', SlangFile('', '', frozenset(), frozenset({EntryPoint(Stage.fragment, 'fragMain')})))
# Multiple [attr] lines between [shader(...)] and the function declaration are skipped
check('''
[shader("fragment")]
[numthreads(4, 4, 1)]
[SomeOtherAttribute]
float4 fragMain() : SV_Target { return float4(0); }
''', SlangFile('', '', frozenset(), frozenset({EntryPoint(Stage.fragment, 'fragMain')})))
# Multiple entry points: vertex, pixel, and fragment stages
check('''
[shader("vertex")]
void vsMain(float4 pos : POSITION) {}
[shader("pixel")]
float4 psMain() : SV_Target { return float4(0); }
[shader("fragment")]
float4 fsMain() : SV_Target { return float4(0); }
''', SlangFile('', '', frozenset(), frozenset({
EntryPoint(Stage.vertex, 'vsMain'),
EntryPoint(Stage.fragment, 'psMain'),
EntryPoint(Stage.fragment, 'fsMain'),
})))
# module, imports and entry points together
check('''
module myshader;
import common;
[shader("vertex")]
void vsMain() {}
''', SlangFile('', '', frozenset({'common'}), frozenset({EntryPoint(Stage.vertex, 'vsMain')}), 'myshader'))
def test_slang_ordering(self):
# Test topological_sort with a manually constructed linear chain: a <- b <- c
graph: dict[str, SlangFile] = {
'a': SlangFile('', '', frozenset(), frozenset(), 'a'),
'b': SlangFile('', '', frozenset({'a'}), frozenset(), 'b'),
'c': SlangFile('', '', frozenset({'b'}), frozenset(), 'c'),
}
order = topological_sort(graph)
self.assertLess(order.index('a'), order.index('b'))
self.assertLess(order.index('b'), order.index('c'))
# Diamond dependency: base <- left, base <- right, left + right <- top
diamond: dict[str, SlangFile] = {
'base': SlangFile('', '', frozenset(), frozenset(), 'base'),
'left': SlangFile('', '', frozenset({'base'}), frozenset(), 'left'),
'right': SlangFile('', '', frozenset({'base'}), frozenset(), 'right'),
'top': SlangFile('', '', frozenset({'left', 'right'}), frozenset(), 'top'),
}
order2 = topological_sort(diamond)
self.assertLess(order2.index('base'), order2.index('left'))
self.assertLess(order2.index('base'), order2.index('right'))
self.assertLess(order2.index('left'), order2.index('top'))
self.assertLess(order2.index('right'), order2.index('top'))
# Node with an import not present in the graph is silently skipped
partial: dict[str, SlangFile] = {
'x': SlangFile('', '', frozenset({'missing'}), frozenset(), 'x'),
}
self.assertEqual(topological_sort(partial), ['x'])
# Empty graph
self.assertEqual(topological_sort({}), [])
# build_import_graph reads .slang files from a directory tree and parses them
with tempfile.TemporaryDirectory() as tmpdir:
files = {
'a': 'module a;\n',
'b': 'module b;\nimport a;\n',
'c': 'module c;\nimport b;\n',
}
for name, content in files.items():
with open(os.path.join(tmpdir, name + '.slang'), 'w') as f:
f.write(content)
graph2 = build_import_graph(tmpdir)
self.assertEqual(set(graph2.keys()), {'a', 'b', 'c'})
self.assertEqual(graph2['a'].imports, frozenset())
self.assertEqual(graph2['b'].imports, frozenset({'a'}))
self.assertEqual(graph2['c'].imports, frozenset({'b'}))
self.assertEqual(graph2['a'].module, 'a')
# Topological sort of file-based graph respects import dependencies
order3 = topological_sort(graph2)
self.assertLess(order3.index('a'), order3.index('b'))
self.assertLess(order3.index('b'), order3.index('c'))
# Non-.slang files are ignored
with open(os.path.join(tmpdir, 'ignored.txt'), 'w') as f:
f.write('not a slang file\n')
graph3 = build_import_graph(tmpdir)
self.assertNotIn('ignored', graph3)

266
setup.py
View File

@@ -18,14 +18,13 @@ import tempfile
import textwrap import textwrap
import time import time
from contextlib import suppress from contextlib import suppress
from enum import Enum
from functools import lru_cache, partial from functools import lru_cache, partial
from pathlib import Path from pathlib import Path
from typing import Callable, Dict, FrozenSet, Iterable, Iterator, List, Optional, Sequence, Set, Tuple, Union, cast from typing import Any, Callable, Dict, FrozenSet, Iterable, Iterator, List, NamedTuple, Optional, Sequence, Set, Tuple, Union, cast
from glfw import glfw
from glfw.glfw import ISA, BinaryArch, Command, CompileKey, CompilerType
src_base = os.path.dirname(os.path.abspath(__file__)) src_base = os.path.dirname(os.path.abspath(__file__))
glfw_base = os.path.join(src_base, 'glfw')
setattr(sys, 'running_from_setup', True) setattr(sys, 'running_from_setup', True)
def check_version_info() -> None: def check_version_info() -> None:
@@ -45,7 +44,6 @@ def check_version_info() -> None:
exit(f'kitty requires Python {minver}. Current Python version: {".".join(map(str, sys.version_info[:3]))}') exit(f'kitty requires Python {minver}. Current Python version: {".".join(map(str, sys.version_info[:3]))}')
check_version_info()
verbose = False verbose = False
build_dir = 'build' build_dir = 'build'
constants = os.path.join('kitty', 'constants.py') constants = os.path.join('kitty', 'constants.py')
@@ -65,18 +63,133 @@ is_macos = 'darwin' in _plat
is_openbsd = 'openbsd' in _plat is_openbsd = 'openbsd' in _plat
is_freebsd = 'freebsd' in _plat is_freebsd = 'freebsd' in _plat
is_netbsd = 'netbsd' in _plat is_netbsd = 'netbsd' in _plat
is_linux = 'linux' in _plat
is_dragonflybsd = 'dragonfly' in _plat is_dragonflybsd = 'dragonfly' in _plat
is_bsd = is_freebsd or is_netbsd or is_dragonflybsd or is_openbsd is_bsd = is_freebsd or is_netbsd or is_dragonflybsd or is_openbsd
is_windows = sys.platform == 'win32' is_windows = sys.platform == 'win32'
is_arm = platform.processor() == 'arm' or platform.machine() in ('arm64', 'aarch64') is_arm = platform.processor() == 'arm' or platform.machine() in ('arm64', 'aarch64')
c_std = '' if is_openbsd else '-std=c11' c_std = '' if is_openbsd else '-std=c11'
Env = glfw.Env
env = Env()
PKGCONFIG = os.environ.get('PKGCONFIG_EXE', 'pkg-config') PKGCONFIG = os.environ.get('PKGCONFIG_EXE', 'pkg-config')
link_targets: List[str] = [] link_targets: List[str] = []
macos_universal_arches = ('arm64', 'x86_64') if is_arm else ('x86_64', 'arm64') macos_universal_arches = ('arm64', 'x86_64') if is_arm else ('x86_64', 'arm64')
def null_func() -> None:
return None
class CompileKey(NamedTuple):
src: str
dest: str
class Command(NamedTuple):
desc: str
cmd: Sequence[str]
is_newer_func: Callable[[], bool]
on_success: Callable[[], None] = null_func
key: Optional[CompileKey] = None
keyfile: Optional[str] = None
class ISA(Enum):
X86 = 0x03
AMD64 = 0x3e
ARM64 = 0xb7
Other = 0x0
class BinaryArch(NamedTuple):
bits: int = 64
isa: ISA = ISA.AMD64
class CompilerType(Enum):
gcc = 'gcc'
clang = 'clang'
unknown = 'unknown'
class Env:
cc: List[str] = []
cppflags: List[str] = []
cflags: List[str] = []
ldflags: List[str] = []
library_paths: Dict[str, List[str]] = {}
ldpaths: List[str] = []
ccver: Tuple[int, int]
vcs_rev: str = ''
binary_arch: BinaryArch = BinaryArch()
native_optimizations: bool = False
primary_version: int = 0
secondary_version: int = 0
xt_version: str = ''
has_copy_file_range: bool = False
# glfw stuff
all_headers: List[str] = []
sources: List[str] = []
wayland_packagedir: str = ''
wayland_scanner: str = ''
wayland_scanner_code: str = ''
wayland_protocols: Tuple[str, ...] = ()
def __init__(
self, cc: List[str] = [], cppflags: List[str] = [], cflags: List[str] = [], ldflags: List[str] = [],
library_paths: Dict[str, List[str]] = {}, ldpaths: Optional[List[str]] = None, ccver: Tuple[int, int] = (0, 0),
vcs_rev: str = '', binary_arch: BinaryArch = BinaryArch(),
native_optimizations: bool = False,
):
self.cc, self.cppflags, self.cflags, self.ldflags, self.library_paths = cc, cppflags, cflags, ldflags, library_paths
self.ldpaths = ldpaths or []
self.ccver = ccver
self.vcs_rev = vcs_rev
self.binary_arch = binary_arch
self.native_optimizations = native_optimizations
self._cc_version_string = ''
self._compiler_type: Optional[CompilerType] = None
@property
def cc_version_string(self) -> str:
if not self._cc_version_string:
self._cc_version_string = subprocess.check_output(self.cc + ['--version']).decode()
return self._cc_version_string
@property
def compiler_type(self) -> CompilerType:
if self._compiler_type is None:
raw = self.cc_version_string
if 'Free Software Foundation' in raw:
self._compiler_type = CompilerType.gcc
elif 'clang' in raw.lower().split():
self._compiler_type = CompilerType.clang
else:
self._compiler_type = CompilerType.unknown
return self._compiler_type
def copy(self) -> 'Env':
ans = Env(self.cc, list(self.cppflags), list(self.cflags), list(self.ldflags), dict(self.library_paths), list(self.ldpaths), self.ccver)
ans.all_headers = list(self.all_headers)
ans._cc_version_string = self._cc_version_string
ans.sources = list(self.sources)
ans.wayland_packagedir = self.wayland_packagedir
ans.wayland_scanner = self.wayland_scanner
ans.wayland_scanner_code = self.wayland_scanner_code
ans.wayland_protocols = self.wayland_protocols
ans.vcs_rev = self.vcs_rev
ans.binary_arch = self.binary_arch
ans.native_optimizations = self.native_optimizations
ans.primary_version = self.primary_version
ans.secondary_version = self.secondary_version
ans.xt_version = self.xt_version
ans.has_copy_file_range = self.has_copy_file_range
return ans
env = Env()
def LinkKey(output: str) -> CompileKey: def LinkKey(output: str) -> CompileKey:
return CompileKey('', output) return CompileKey('', output)
@@ -1038,11 +1151,114 @@ def find_c_files() -> Tuple[List[str], List[str]]:
return ans, headers return ans, headers
def wayland_protocol_file_name(base: str, ext: str = 'c') -> str:
base = os.path.basename(base).rpartition('.')[0]
return f'wayland-{base}-client-protocol.{ext}'
def glfw_init_env(
env: Env,
pkg_config: Callable[..., List[str]],
pkg_version: Callable[[str], Tuple[int, int]],
at_least_version: Callable[..., None],
test_compile: Callable[..., Any],
module: str = 'x11'
) -> Env:
ans = env.copy()
ans.cflags.append('-fPIC')
ans.cppflags.append(f'-D_GLFW_{module.upper()}')
ans.cppflags.append('-D_GLFW_BUILD_DLL')
with open(os.path.join(glfw_base, 'source-info.json')) as f:
sinfo = json.load(f)
module_sources = list(sinfo[module]['sources'])
if module in ('x11', 'wayland'):
remove = 'null_joystick.c' if is_linux else 'linux_joystick.c'
module_sources.remove(remove)
ans.sources = sinfo['common']['sources'] + module_sources
ans.all_headers = [x for x in os.listdir(glfw_base) if x.endswith('.h')]
if module in ('x11', 'wayland'):
ans.cflags.append('-pthread')
ans.ldpaths.extend('-pthread -lm'.split())
if not is_openbsd:
ans.ldpaths.extend('-lrt -ldl'.split())
major, minor = pkg_version('xkbcommon')
if (major, minor) < (0, 5):
raise SystemExit('libxkbcommon >= 0.5 required')
if major < 1:
ans.cflags.append('-DXKB_HAS_NO_UTF32')
if module == 'x11':
for dep in 'x11 xrandr xinerama xcursor xkbcommon xkbcommon-x11 x11-xcb dbus-1'.split():
ans.cflags.extend(pkg_config(dep, '--cflags-only-I'))
ans.ldpaths.extend(pkg_config(dep, '--libs'))
elif module == 'cocoa':
ans.cppflags.append('-DGL_SILENCE_DEPRECATION')
for f_ in 'Cocoa IOKit CoreFoundation CoreVideo QuartzCore UniformTypeIdentifiers'.split():
ans.ldpaths.extend(('-framework', f_))
elif module == 'wayland':
at_least_version('wayland-protocols', *sinfo['wayland_protocols'])
ans.wayland_packagedir = os.path.abspath(pkg_config('wayland-protocols', '--variable=pkgdatadir')[0])
ans.wayland_scanner = os.path.abspath(pkg_config('wayland-scanner', '--variable=wayland_scanner')[0])
scanner_version = tuple(map(int, pkg_config('wayland-scanner', '--modversion')[0].strip().split('.')))
ans.wayland_scanner_code = 'private-code' if scanner_version >= (1, 14, 91) else 'code'
ans.wayland_protocols = tuple(sinfo[module]['protocols'])
for p in ans.wayland_protocols:
ans.sources.append(wayland_protocol_file_name(p))
ans.all_headers.append(wayland_protocol_file_name(p, 'h'))
for dep in 'wayland-client wayland-cursor xkbcommon dbus-1'.split():
ans.cflags.extend(pkg_config(dep, '--cflags-only-I'))
ans.ldpaths.extend(pkg_config(dep, '--libs'))
has_memfd_create = test_compile(env.cc, '-Werror', src='''#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
int main(void) {
return syscall(__NR_memfd_create, "test", 0);
}''')
if has_memfd_create:
ans.cppflags.append('-DHAS_MEMFD_CREATE')
return ans
def build_wayland_protocols(
env: Env,
parallel_run: Callable[[List[Command]], None],
emphasis: Callable[[str], str],
newer: Callable[..., bool],
dest_dir: str
) -> None:
items = []
for protocol in env.wayland_protocols:
if '/' in protocol:
src = os.path.join(env.wayland_packagedir, protocol)
if not os.path.exists(src):
raise SystemExit(f'The wayland-protocols package on your system is missing the {protocol} protocol definition file')
else:
src = os.path.join(glfw_base, protocol)
if not os.path.exists(src):
raise SystemExit(f'The local Wayland protocol {protocol} is missing from kitty sources')
for ext in 'hc':
dest = wayland_protocol_file_name(src, ext)
dest = os.path.join(dest_dir, dest)
if newer(dest, src):
q = 'client-header' if ext == 'h' else env.wayland_scanner_code
items.append(Command(
f'Generating {emphasis(os.path.basename(dest))} ...',
[env.wayland_scanner, q, src, dest], lambda: True))
if items:
parallel_run(items)
def compile_glfw(compilation_database: CompilationDatabase, build_dsym: bool = False) -> None: def compile_glfw(compilation_database: CompilationDatabase, build_dsym: bool = False) -> None:
modules = 'cocoa' if is_macos else 'x11 wayland' modules = 'cocoa' if is_macos else 'x11 wayland'
for module in modules.split(): for module in modules.split():
try: try:
genv = glfw.init_env(env, pkg_config, pkg_version, at_least_version, test_compile, module) genv = glfw_init_env(env, pkg_config, pkg_version, at_least_version, test_compile, module)
except SystemExit as err: except SystemExit as err:
if module != 'wayland': if module != 'wayland':
raise raise
@@ -1053,7 +1269,7 @@ def compile_glfw(compilation_database: CompilationDatabase, build_dsym: bool = F
all_headers = [os.path.join('glfw', x) for x in genv.all_headers] all_headers = [os.path.join('glfw', x) for x in genv.all_headers]
if module == 'wayland': if module == 'wayland':
try: try:
glfw.build_wayland_protocols(genv, parallel_run, emphasis, newer, 'glfw') build_wayland_protocols(genv, parallel_run, emphasis, newer, 'glfw')
except SystemExit as err: except SystemExit as err:
print(err, file=sys.stderr) print(err, file=sys.stderr)
print(error('Disabling building of wayland backend'), file=sys.stderr) print(error('Disabling building of wayland backend'), file=sys.stderr)
@@ -1182,6 +1398,7 @@ def build_uniforms_header(skip_generation: bool = False) -> str:
a(f' ans->{n} = get_uniform_location(program, "{n}");') a(f' ans->{n} = get_uniform_location(program, "{n}");')
a('}') a('}')
a('') a('')
# }]]]))
src = '\n'.join(lines) src = '\n'.join(lines)
try: try:
with open(dest) as f: with open(dest) as f:
@@ -1204,6 +1421,24 @@ def wrapped_kittens() -> str:
raise Exception('Failed to read wrapped kittens from kitty wrapper script') raise Exception('Failed to read wrapped kittens from kitty wrapper script')
def build_shaders(args: Options, kitty_exe: str, for_freeze: bool) -> None:
if args.skip_code_generation:
print('Skipping building of shaders due to command line option', flush=True)
return
env = os.environ.copy()
env['ASAN_OPTIONS'] = 'detect_leaks=0'
cp = subprocess.run([
kitty_exe, '+launch', os.path.join(src_base, 'kitty/shaders/slang.py'), 'build/shaders', 'shaders',
], env=env)
if cp.returncode != 0:
if os.environ.get('CI') == 'true' and cp.returncode < 0 and shutil.which('coredumpctl'):
subprocess.run(['sh', '-c', 'echo bt | coredumpctl debug'])
raise SystemExit(f'Generating shaders failed with exit code: {cp.returncode}')
if for_freeze:
libdir = os.path.join(os.path.dirname(kitty_exe), '..', 'Resources' if is_macos else 'lib', 'kitty')
shutil.copytree('shaders', os.path.join(libdir, 'shaders'), dirs_exist_ok=True)
def build(args: Options, native_optimizations: bool = True, call_init: bool = True) -> None: def build(args: Options, native_optimizations: bool = True, call_init: bool = True) -> None:
if call_init: if call_init:
init_env_from_args(args, native_optimizations) init_env_from_args(args, native_optimizations)
@@ -1280,6 +1515,8 @@ def build_static_kittens(
raise SystemExit(f'The version of go on this system ({current_go_version}) is too old. go >= {required_go_version} is needed') raise SystemExit(f'The version of go on this system ({current_go_version}) is too old. go >= {required_go_version} is needed')
if not for_platform: if not for_platform:
update_go_generated_files(args, os.path.join(launcher_dir, appname)) update_go_generated_files(args, os.path.join(launcher_dir, appname))
build_shaders(args, os.path.join(launcher_dir, appname), for_freeze)
if args.skip_building_kitten: if args.skip_building_kitten:
print('Skipping building of the kitten binary because of a command line option. Build is incomplete', file=sys.stderr) print('Skipping building of the kitten binary because of a command line option. Build is incomplete', file=sys.stderr)
return '' return ''
@@ -1877,7 +2114,7 @@ def package(args: Options, bundle_type: str, do_build_all: bool = True) -> None:
shutil.copy2('logo/beam-cursor@2x.png', os.path.join(libdir, 'logo')) shutil.copy2('logo/beam-cursor@2x.png', os.path.join(libdir, 'logo'))
shutil.copytree('shell-integration', os.path.join(libdir, 'shell-integration'), dirs_exist_ok=True) shutil.copytree('shell-integration', os.path.join(libdir, 'shell-integration'), dirs_exist_ok=True)
shutil.copytree('fonts', os.path.join(libdir, 'fonts'), dirs_exist_ok=True) shutil.copytree('fonts', os.path.join(libdir, 'fonts'), dirs_exist_ok=True)
allowed_extensions = frozenset('py glsl so'.split()) allowed_extensions = frozenset('py slang glsl so'.split())
def src_ignore(parent: str, entries: Iterable[str]) -> List[str]: def src_ignore(parent: str, entries: Iterable[str]) -> List[str]:
return [ return [
@@ -1924,8 +2161,10 @@ def package(args: Options, bundle_type: str, do_build_all: bool = True) -> None:
for f_ in files: for f_ in files:
path = os.path.join(root, f_) path = os.path.join(root, f_)
os.chmod(path, 0o755 if should_be_executable(path) else 0o644) os.chmod(path, 0o755 if should_be_executable(path) else 0o644)
if not for_freeze and not bundle_type.startswith('macos-'): if not for_freeze:
build_static_kittens(args, launcher_dir=launcher_dir) if not bundle_type.startswith('macos-'):
build_static_kittens(args, launcher_dir=launcher_dir)
shutil.copytree('shaders', os.path.join(libdir, 'shaders'), dirs_exist_ok=True)
if not is_macos and not is_windows: if not is_macos and not is_windows:
create_linux_bundle_gunk(ddir, args) create_linux_bundle_gunk(ddir, args)
@@ -1957,7 +2196,7 @@ def clean(for_cross_compile: bool = False) -> None:
'linux-package', 'kitty.app', 'asan-launcher', 'linux-package', 'kitty.app', 'asan-launcher',
'kitty-profile') # no fonts as that is not generated by build 'kitty-profile') # no fonts as that is not generated by build
if not for_cross_compile: if not for_cross_compile:
safe_remove('docs/generated') safe_remove('docs/generated', 'shaders')
clean_launcher_dir('kitty/launcher') clean_launcher_dir('kitty/launcher')
def excluded(root: str, d: str) -> bool: def excluded(root: str, d: str) -> bool:
@@ -2330,6 +2569,7 @@ def do_build(args: Options) -> None:
def main() -> None: def main() -> None:
check_version_info()
global verbose, build_dir global verbose, build_dir
if len(sys.argv) > 1 and sys.argv[1] == 'build-dep': if len(sys.argv) > 1 and sys.argv[1] == 'build-dep':
return build_dep() return build_dep()

View File

@@ -15,6 +15,7 @@ in
simde simde
go go
matplotlib matplotlib
shader-slang
] ]
++ optionals stdenv.isDarwin [ ++ optionals stdenv.isDarwin [
libpng libpng