diff --git a/docs/custom-shaders.rst b/docs/custom-shaders.rst index d646cd3c9..7670a66f6 100644 --- a/docs/custom-shaders.rst +++ b/docs/custom-shaders.rst @@ -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** diff --git a/kitty/shaders/custom/crt.slang b/kitty/shaders/custom/crt.slang index 835df6c30..138f2ff2c 100644 --- a/kitty/shaders/custom/crt.slang +++ b/kitty/shaders/custom/crt.slang @@ -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); diff --git a/kitty/shaders/custom/northern-lights.slang b/kitty/shaders/custom/northern-lights.slang index 9b319f956..8255ba5ca 100644 --- a/kitty/shaders/custom/northern-lights.slang +++ b/kitty/shaders/custom/northern-lights.slang @@ -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); diff --git a/kitty/shaders/custom/pipeline.slang b/kitty/shaders/custom/pipeline.slang index 4f278212e..e50e7cb0a 100644 --- a/kitty/shaders/custom/pipeline.slang +++ b/kitty/shaders/custom/pipeline.slang @@ -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); } diff --git a/kitty/shaders/custom/types.slang b/kitty/shaders/custom/types.slang index f82b974d3..4b2a96ab1 100644 --- a/kitty/shaders/custom/types.slang +++ b/kitty/shaders/custom/types.slang @@ -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)