Proper fix for custom shader scaling on OS window grow + shrink
Some checks are pending
CI / Linux (python=3.14 cc=clang sanitize=1) (push) Waiting to run
CI / Linux (python=3.12 cc=gcc sanitize=0) (push) Waiting to run
CI / Linux (python=3.13 cc=gcc sanitize=1) (push) Waiting to run
CI / Linux package (push) Waiting to run
CI / Bundle test (macos-latest) (push) Waiting to run
CI / Bundle test (ubuntu-latest) (push) Waiting to run
CI / macOS Brew (push) Waiting to run
CI / Test ./dev.sh and benchmark (push) Waiting to run
CodeQL / CodeQL-Build (actions, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (c, macos-latest) (push) Waiting to run
CodeQL / CodeQL-Build (c, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (go, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (python, ubuntu-latest) (push) Waiting to run
Depscan / Scan dependencies for vulnerabilities (push) Waiting to run

This commit is contained in:
Kovid Goyal 2026-09-06 14:58:18 +05:30
parent e55739bd10
commit d124d4f097
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
5 changed files with 62 additions and 23 deletions

View file

@ -284,11 +284,15 @@ the pipeline via the ``KittyTextures`` struct (``t.a``, ``t.b``, ``t.persist``).
frame *N+1*. This allows effects that accumulate state over time (e.g.
simulation steps, trails, fluid simulations).
All three named textures have the same pixel dimensions as the OS window
viewport. They must be declared in the top-level ``textures`` directive before
they can be referenced. A named texture that is the output target for the
current group cannot simultaneously be sampled as input; kitty automatically
substitutes the backbuffer in that case.
All three named textures cover the same area as the OS Window viewport and are
sampled in the same UV co-ordinate space as ``t.pos``, via the ``Sample()``
method on ``KittyTexture``. Note that the underlying GPU textures can be larger
than the viewport, since kitty grows them to fit the largest OS Window and
never shrinks them again, so always sample via ``Sample()`` rather than using
the raw ``tex`` sampler. They must be declared in the top-level ``textures``
directive before they can be referenced. A named texture that is the output
target for the current group cannot simultaneously be sampled as input; kitty
automatically substitutes the backbuffer in that case.
**How groups chain together**

View file

@ -31,14 +31,13 @@ fragment_main(float4 color, KittyTextures t, KittyCustomShaderData d) {
uv.y += 0.5f;
// Original Y pixel coordinate within the viewport (for scanline phase)
float fragCoordY = (t.pos.y - t.viewport.y) * float(d.viewport_size_pixels.y);
float fragCoordY = t.pos.y * float(d.viewport_size_pixels.y) * t.viewport.w;
// Scanline darkness at this pixel row
float apply = abs(sin(fragCoordY) * 0.25f * SCAN);
// Sample the backbuffer at the warped viewport UV
float2 sample_uv = t.viewport.xy + uv * t.viewport.zw;
float3 col = t.backbuffer.Sample(sample_uv).rgb;
float3 col = t.backbuffer.Sample(uv).rgb;
// apply the tint
col = lerp(col, col * TINT.rgb, TINT.a);

View file

@ -459,10 +459,6 @@ fragment_main(float4 color, KittyTextures t, KittyCustomShaderData d) {
float timestamp = fmod(d.timestamp, 86400.0f) * TIME_SCALE;
float2 uv = t.pos;
// d.src_rect.zy = (sx, sy) = (vw/W, vh/H): fraction of the shared texture used by this viewport.
// Named textures (a, persist) and the backbuffer span the full W×H texture, so we must scale
// t.pos back to raw texture UV when sampling them, otherwise we read outside the valid region.
float2 tex_uv = uv * d.src_rect.zy;
float aspect = pixelDims.x / max(pixelDims.y, 1.0f);
float2 mouseUV = (d.mouse_pos.xy - t.viewport.xy) / max(t.viewport.zw, float2(1e-5f, 1e-5f));
@ -472,7 +468,7 @@ fragment_main(float4 color, KittyTextures t, KittyCustomShaderData d) {
// First group: Accumulate radial aurora rays cast from sky center into scratch texture 'a'
if (STAGE == 0) {
float prev = t.persist.Sample(tex_uv).r;
float prev = t.persist.Sample(uv).r;
float decayed = prev * saturate(1.0f - ACCUMULATION_DECAY);
float deposit = 0.0f;
@ -513,7 +509,7 @@ fragment_main(float4 color, KittyTextures t, KittyCustomShaderData d) {
}
// Second group: Copy scratch texture 'a' to 'persist'
if (STAGE == 1) { return t.a.Sample(tex_uv); }
if (STAGE == 1) { return t.a.Sample(uv); }
// Third group: Main composite pass
AuroraRandParams rp = getAuroraParams(d.timestamp, INITIAL_RANDOM_SEED);
@ -522,7 +518,7 @@ fragment_main(float4 color, KittyTextures t, KittyCustomShaderData d) {
float2 fragCoord = uv * pixelDims;
float persistentTrail = t.persist.Sample(tex_uv).r * MOUSE_PERSIST_INTENSITY;
float persistentTrail = t.persist.Sample(uv).r * MOUSE_PERSIST_INTENSITY;
float3 ro = float3(0.0f, 0.0f, -CAMERA_DISTANCE);
float2 centeredUV = uv - 0.5f;
@ -562,7 +558,7 @@ fragment_main(float4 color, KittyTextures t, KittyCustomShaderData d) {
float gate = smoothCutoff(effectLum, EFFECT_FLOOR, EFFECT_CEILING);
col *= gate;
float4 terminalColor = t.backbuffer.Sample(tex_uv);
float4 terminalColor = t.backbuffer.Sample(uv);
float termLum = dot(terminalColor.rgb, float3(0.299f, 0.587f, 0.114f));
float isBlack = step(termLum, 0.001f);
float textPreserve = 1.0f - smoothstep(BLACK_BLEND_THRESHOLD, BLACK_BLEND_THRESHOLD + 0.2f, termLum);

View file

@ -66,11 +66,25 @@ pipeline_fragment_main(float2 backbuffer_pos, int group, float4 viewport, float
float4 color = backbuffer.Sample(backbuffer_pos); // pre multiplied linear RGB
color = float4(color.rgb / color.a, color.a); // un pre multiplied color passed through pipeline
// src_rect encodes (0, sy, sx, 0) — the fraction of the shared texture occupied by the current
// viewport. Normalize backbuffer_pos so that t.pos is always in [0,1] viewport UV regardless of
// whether the shared texture is larger than the current viewport (e.g. after a window shrink).
// viewport. The shared texture is grown to fit the largest OS Window viewport and never
// shrunk, so sx/sy are less than one whenever this window is smaller than that. Normalize
// backbuffer_pos so that t.pos is always in [0,1] viewport UV and wrap the samplers so that
// shaders can address them in that same space, regardless of the size of the shared texture.
float2 src_scale = float2(max(1e-6, csd.src_rect[right]), max(1e-6, csd.src_rect[top]));
float2 viewport_pos = backbuffer_pos / src_scale;
KittyTextures t = { backbuffer, a, b, persist, viewport_pos, viewport, animation_progress, group };
// Half a texel inset, in viewport UV, so that clamped samples do not bilinearly blend in the
// stale texels that lie just outside the viewport region of the shared texture.
float2 max_pos = 1.f - 0.5f / max(float2(csd.viewport_size_pixels), float2(1.f));
KittyTextures t = {
{ backbuffer, src_scale, max_pos },
{ a, src_scale, max_pos },
{ b, src_scale, max_pos },
{ persist, src_scale, max_pos },
viewport_pos,
viewport,
animation_progress,
group
};
// PIPELINE
return vec4_premul(color.rgb, color.a);
}

View file

@ -72,17 +72,43 @@ public struct KittyCustomShaderData {
public float cursor_trail_change_time;
}
// A texture kitty renders into, addressed in the same UV co-ordinate space as
// KittyTextures.pos. kitty renders into textures that are at least as large as
// the largest OS Window viewport, so the pixels for the current OS Window
// occupy only the lower-left corner of the texture after the window is made
// smaller. Sampling via the Sample() method below hides that detail, so
// shaders never need to know the size of the underlying texture.
public struct KittyTexture {
// The underlying texture. Use this only if you need raw texture UV,
// remember to multiply your co-ordinates by scale below when you do.
public Sampler2D tex;
// The fraction of tex occupied by the OS Window viewport. Always in (0, 1].
public float2 scale;
// The largest usable co-ordinate for Sample() below. Half a texel less than
// one, so that sampling the top/right edge does not blend in texels from
// outside the viewport.
public float2 max_pos;
// Sample this texture at pos, which is in the same UV co-ordinate space as
// KittyTextures.pos, that is, [0, 1] with the origin at the bottom-left.
// Co-ordinates outside that range are clamped to the edge of the viewport.
public float4
Sample(float2 pos) {
return tex.Sample(clamp(pos, float2(0.f), max_pos) * scale);
}
}
public struct KittyTextures {
// the buffer containing the original rendered pixels from kitty's own rendering. This will
// be updated with the output color only after all custom shaders in this group have run
public Sampler2D backbuffer;
public KittyTexture backbuffer;
// The following are named textures that can be sampled from in custom
// shaders. The persist texture will persist across frames, anything
// written to it in render number N can be read from it in subsequent
// renders.
public Sampler2D a;
public Sampler2D b;
public Sampler2D persist;
public KittyTexture a;
public KittyTexture b;
public KittyTexture persist;
public float2 pos; // the position of this pixel in the viewport (UV coordinates, [0,1] range, origin at bottom-left)