use anyhow::Result;
use std::path::Path;
fn preprocess_glsl_for_shadertoy(glsl_source: &str) -> String {
let mut source = glsl_source.to_string();
if let Some(main_pos) = source.find("void mainImage") {
if let (Some(paren_start), Some(paren_end)) =
(source[main_pos..].find('('), source[main_pos..].find(')'))
{
let abs_start = main_pos + paren_start + 1;
let abs_end = main_pos + paren_end;
let params = &source[abs_start..abs_end];
if let Some(in_pos) = params.find("in vec2") {
let ident_start = abs_start
+ in_pos
+ "in vec2".len()
+ params[in_pos + "in vec2".len()..]
.chars()
.take_while(|c| c.is_whitespace())
.count();
let mut ident_end = ident_start;
for ch in source[ident_start..abs_end].chars() {
if ch.is_alphanumeric() || ch == '_' {
ident_end += ch.len_utf8();
} else {
break;
}
}
source.replace_range(ident_start..ident_end, "_fc_raw");
}
}
if let Some(rel_brace) = source[main_pos..].find('{') {
let inject_pos = main_pos + rel_brace + 1; let inject = "\n vec2 fragCoord = vec2(_fc_raw.x, iResolution.y - _fc_raw.y);\n gl_FragCoord_st = fragCoord;\n";
source.insert_str(inject_pos, inject);
}
}
source
}
fn glsl_wrapper_template(glsl_source: &str) -> String {
format!(
r#"#version 450
// Uniforms - must match Rust struct layout (std140)
// Total size: 256 bytes
layout(set = 0, binding = 0) uniform Uniforms {{
vec2 iResolution; // offset 0, size 8 - Viewport resolution
float iTime; // offset 8, size 4 - Time in seconds
float iTimeDelta; // offset 12, size 4 - Time since last frame
vec4 iMouse; // offset 16, size 16 - Mouse state (xy=current, zw=click)
vec4 iDate; // offset 32, size 16 - Date (year, month, day, seconds)
float iOpacity; // offset 48, size 4 - Window opacity
float iTextOpacity; // offset 52, size 4 - Text opacity
float iFullContent; // offset 56, size 4 - Full content mode (1.0 = enabled)
float iFrame; // offset 60, size 4 - Frame counter
float iFrameRate; // offset 64, size 4 - Current FPS
float iResolutionZ; // offset 68, size 4 - Pixel aspect ratio (usually 1.0)
float iBrightness; // offset 72, size 4 - Shader brightness multiplier (0.05-1.0)
float iTimeKeyPress; // offset 76, size 4 - Time when last key was pressed
// Cursor uniforms (Ghostty-compatible, v1.2.0+)
vec4 iCurrentCursor; // offset 80, size 16 - xy=position, zw=size (pixels)
vec4 iPreviousCursor; // offset 96, size 16 - xy=previous position, zw=size
vec4 iCurrentCursorColor; // offset 112, size 16 - RGBA (opacity baked into alpha)
vec4 iPreviousCursorColor; // offset 128, size 16 - RGBA previous color
float iTimeCursorChange; // offset 144, size 4 - Time when cursor last moved
// Cursor shader configuration uniforms
float iCursorTrailDuration;// offset 148, size 4 - Trail effect duration (seconds)
float iCursorGlowRadius; // offset 152, size 4 - Glow effect radius (pixels)
float iCursorGlowIntensity;// offset 156, size 4 - Glow effect intensity (0-1)
vec4 iCursorShaderColor; // offset 160, size 16 - User-configured cursor color (aligned to 16)
// Channel resolution uniforms (Shadertoy-compatible)
vec4 iChannelResolution0; // offset 176, size 16 - iChannel0 resolution [width, height, 1, 0]
vec4 iChannelResolution1; // offset 192, size 16 - iChannel1 resolution
vec4 iChannelResolution2; // offset 208, size 16 - iChannel2 resolution
vec4 iChannelResolution3; // offset 224, size 16 - iChannel3 resolution
vec4 iChannelResolution4; // offset 240, size 16 - iChannel4 resolution
vec4 iCubemapResolution; // offset 256, size 16 - Cubemap resolution [size, size, 1, 0]
// Background color uniform
vec4 iBackgroundColor; // offset 272, size 16 - Solid background color [R, G, B, A]
// When A > 0, use this as background instead of shader output
// Progress bar state
vec4 iProgress; // offset 288, size 16 - x=state(0-4), y=percent(0-1), z=isActive(0/1), w=activeCount
}}; // total: 304 bytes
// Shadertoy-compatible iChannelResolution array accessor
// Usage: iChannelResolution[0].xyz, iChannelResolution[1].xy, etc.
vec3 iChannelResolution[5] = vec3[5](
iChannelResolution0.xyz,
iChannelResolution1.xyz,
iChannelResolution2.xyz,
iChannelResolution3.xyz,
iChannelResolution4.xyz
);
// User-defined texture channels (iChannel0-3) - Shadertoy compatible
layout(set = 0, binding = 1) uniform texture2D _iChannel0Tex;
layout(set = 0, binding = 2) uniform sampler _iChannel0Sampler;
layout(set = 0, binding = 3) uniform texture2D _iChannel1Tex;
layout(set = 0, binding = 4) uniform sampler _iChannel1Sampler;
layout(set = 0, binding = 5) uniform texture2D _iChannel2Tex;
layout(set = 0, binding = 6) uniform sampler _iChannel2Sampler;
layout(set = 0, binding = 7) uniform texture2D _iChannel3Tex;
layout(set = 0, binding = 8) uniform sampler _iChannel3Sampler;
// Terminal content texture (iChannel4)
layout(set = 0, binding = 9) uniform texture2D _iChannel4Tex;
layout(set = 0, binding = 10) uniform sampler _iChannel4Sampler;
// Cubemap texture (iCubemap)
layout(set = 0, binding = 11) uniform textureCube _iCubemapTex;
layout(set = 0, binding = 12) uniform sampler _iCubemapSampler;
// Combined samplers for texture() calls
#define iChannel0 sampler2D(_iChannel0Tex, _iChannel0Sampler)
#define iChannel1 sampler2D(_iChannel1Tex, _iChannel1Sampler)
#define iChannel2 sampler2D(_iChannel2Tex, _iChannel2Sampler)
#define iChannel3 sampler2D(_iChannel3Tex, _iChannel3Sampler)
#define iChannel4 sampler2D(_iChannel4Tex, _iChannel4Sampler)
#define iCubemap samplerCube(_iCubemapTex, _iCubemapSampler)
// Input from vertex shader
layout(location = 0) in vec2 v_uv;
// Output color
layout(location = 0) out vec4 outColor;
// Global fragCoord for Shadertoy compatibility (avoids WGSL parameter passing issues)
vec2 gl_FragCoord_st;
// ============ User shader code begins ============
{glsl_source}
// ============ User shader code ends ============
void main() {{
// Populate iChannelResolution array at runtime (naga drops dynamic initializers)
iChannelResolution[0] = iChannelResolution0.xyz;
iChannelResolution[1] = iChannelResolution1.xyz;
iChannelResolution[2] = iChannelResolution2.xyz;
iChannelResolution[3] = iChannelResolution3.xyz;
iChannelResolution[4] = iChannelResolution4.xyz;
// Flip once here (wgpu y=0 top -> Shadertoy y=0 bottom).
vec2 st_fragCoord = vec2(gl_FragCoord_st.x, iResolution.y - gl_FragCoord_st.y);
gl_FragCoord_st = st_fragCoord;
vec4 shaderColor;
mainImage(shaderColor, st_fragCoord);
// Apply brightness multiplier to shader background (not text)
vec3 dimmedShaderRgb = shaderColor.rgb * iBrightness;
if (iFullContent > 0.5) {{
// Full content mode: shader has full control over terminal content
// The shader receives terminal content via iChannel4 and returns processed output.
// We use the shader's output directly - it has already done its own compositing.
//
// The shader output (shaderColor/dimmedShaderRgb) contains:
// - CRT effects, distortion, scanlines, color grading, etc.
// - The shader's own sampling and processing of iChannel4
vec4 terminalColor = texture(iChannel4, vec2(v_uv.x, 1.0 - v_uv.y));
float hasContent = step(0.01, terminalColor.a);
// When keep_text_opaque is enabled (iTextOpacity = 1.0):
// - Content areas (text or colored bg): opacity = 1.0
// - Empty areas (default bg): opacity = iOpacity
// When disabled (iTextOpacity = iOpacity):
// - Everything uses iOpacity
float pixelOpacity = mix(iOpacity, iTextOpacity, hasContent);
// Determine if we need to composite over a background color
// This is needed for cursor shaders when no background shader is active
float useSolidBg = step(0.01, iBackgroundColor.a);
vec3 bgColor = iBackgroundColor.rgb * iBrightness;
// Composite shader output over background color where there's no content
// For areas with content (terminal text), use shader output directly
// For empty areas, blend shader output over background
vec3 shaderOverBg = dimmedShaderRgb + bgColor * (1.0 - terminalColor.a);
vec3 finalRgb = mix(dimmedShaderRgb, shaderOverBg, useSolidBg);
// Detect chain mode (iOpacity ≈ 0 signals rendering to intermediate for another shader)
float isChainMode = step(iOpacity, 0.001);
// In chain mode: preserve full RGB, output hasContent as alpha for transparency detection
// In final mode: apply pixelOpacity as normal (premultiplied output)
vec3 chainRgb = finalRgb;
float chainAlpha = hasContent;
vec3 finalModeRgb = finalRgb * pixelOpacity;
float finalModeAlpha = pixelOpacity;
outColor = vec4(
mix(finalModeRgb, chainRgb, isChainMode),
mix(finalModeAlpha, chainAlpha, isChainMode)
);
}} else {{
// Background-only mode: text is composited cleanly on top of shader background
vec4 terminalColor = texture(iChannel4, vec2(v_uv.x, 1.0 - v_uv.y));
// Terminal texture is premultiplied alpha (rgb already multiplied by alpha)
// from GPU blending onto transparent background.
// Scale by iTextOpacity to allow fading terminal content.
vec3 srcPremul = terminalColor.rgb * iTextOpacity;
float srcA = terminalColor.a * iTextOpacity;
// Determine background color:
// - If iBackgroundColor.a > 0, use it as solid background (with brightness applied)
// - Otherwise, use shader output (dimmedShaderRgb) as background
float useSolidBg = step(0.01, iBackgroundColor.a);
vec3 bgColor = mix(dimmedShaderRgb, iBackgroundColor.rgb * iBrightness, useSolidBg);
// Detect chain mode (iOpacity ≈ 0 signals rendering to intermediate for another shader)
float isChainMode = step(iOpacity, 0.001);
// In chain mode: use full background color for RGB, terminal-only alpha
// In final mode: use premultiplied background with full alpha compositing
vec3 bgPremul = bgColor * iOpacity;
float bgA = iOpacity;
// RGB: in chain mode use full bgColor, in final mode use premultiplied
vec3 effectiveBgRgb = mix(bgPremul, bgColor, isChainMode);
// Standard "over" compositing with the effective background
vec3 finalRgb = srcPremul + effectiveBgRgb * (1.0 - srcA);
// Alpha: in chain mode preserve terminal alpha only (for transparency detection)
// In final mode, composite with background opacity
float finalA_chain = srcA;
float finalA_final = srcA + bgA * (1.0 - srcA);
float finalA = mix(finalA_final, finalA_chain, isChainMode);
outColor = vec4(finalRgb, finalA);
}}
}}
"#
)
}
#[derive(Debug, Clone, Copy)]
enum BuiltinPositionOrder {
After,
Before,
}
fn replace_required(source: &str, from: &str, to: &str, context: &str) -> Result<String> {
if !source.contains(from) {
return Err(anyhow::anyhow!(
"WGSL post-processing failed: {} — target pattern not found in naga output.\n\
Expected pattern: {:?}",
context,
from
));
}
Ok(source.replace(from, to))
}
fn transpile_impl(
glsl_source: &str,
name: &str,
debug_glsl_filename: &str,
builtin_order: BuiltinPositionOrder,
) -> Result<String> {
let glsl_source = preprocess_glsl_for_shadertoy(glsl_source);
let wrapped_glsl = glsl_wrapper_template(&glsl_source);
#[cfg(debug_assertions)]
{
let debug_path = std::env::temp_dir().join(debug_glsl_filename);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let _ = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(&debug_path)
.and_then(|mut file| std::io::Write::write_all(&mut file, wrapped_glsl.as_bytes()));
}
#[cfg(not(unix))]
{
let _ = std::fs::write(&debug_path, &wrapped_glsl);
}
}
#[cfg(not(debug_assertions))]
let _ = debug_glsl_filename;
let mut parser = naga::front::glsl::Frontend::default();
let options = naga::front::glsl::Options::from(naga::ShaderStage::Fragment);
let module = parser.parse(&options, &wrapped_glsl).map_err(|errors| {
let error_messages: Vec<String> = errors
.errors
.iter()
.map(|e| format!(" {:?}", e.kind))
.collect();
anyhow::anyhow!(
"GLSL parse error in '{}'. Errors:\n{}",
name,
error_messages.join("\n")
)
})?;
let info = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
)
.validate(&module)
.map_err(|e| anyhow::anyhow!("Shader validation failed for '{}': {:?}", name, e))?;
let mut fragment_wgsl = String::new();
let mut writer =
naga::back::wgsl::Writer::new(&mut fragment_wgsl, naga::back::wgsl::WriterFlags::empty());
writer
.write(&module, &info)
.map_err(|e| anyhow::anyhow!("WGSL generation failed for '{}': {:?}", name, e))?;
let fragment_wgsl = fragment_wgsl.replace("fn main(", "fn fs_main(");
let builtin_param = "@builtin(position) frag_pos: vec4<f32>";
let location_param = "@location(0) v_uv: vec2<f32>";
let (with_space_before, with_space_after, without_space_before, without_space_after) =
match builtin_order {
BuiltinPositionOrder::After => (
format!("@fragment \nfn fs_main({location_param}) -> FragmentOutput {{",),
format!(
"@fragment \nfn fs_main({location_param}, {builtin_param}) -> FragmentOutput {{",
),
format!("@fragment\nfn fs_main({location_param}) -> FragmentOutput {{",),
format!(
"@fragment\nfn fs_main({location_param}, {builtin_param}) -> FragmentOutput {{",
),
),
BuiltinPositionOrder::Before => (
format!("@fragment \nfn fs_main({location_param}) -> FragmentOutput {{",),
format!(
"@fragment \nfn fs_main({builtin_param}, {location_param}) -> FragmentOutput {{",
),
format!("@fragment\nfn fs_main({location_param}) -> FragmentOutput {{",),
format!(
"@fragment\nfn fs_main({builtin_param}, {location_param}) -> FragmentOutput {{",
),
),
};
let fragment_wgsl = if fragment_wgsl.contains(&with_space_before) {
replace_required(
&fragment_wgsl,
&with_space_before,
&with_space_after,
"@builtin(position) injection target not found in naga output",
)?
} else {
replace_required(
&fragment_wgsl,
&without_space_before,
&without_space_after,
"@builtin(position) injection target not found in naga output",
)?
};
let uv_assign_target = "v_uv_1 = v_uv;";
let uv_assign_replacement = "v_uv_1 = v_uv;\n // Seed gl_FragCoord_st with raw @builtin(position)\n gl_FragCoord_st = vec2<f32>(frag_pos.x, frag_pos.y);";
let fragment_wgsl = replace_required(
&fragment_wgsl,
uv_assign_target,
uv_assign_replacement,
"gl_FragCoord_st seeding target ('v_uv_1 = v_uv;') not found in naga output",
)?;
let full_wgsl = format!(
r#"// Auto-generated WGSL from GLSL shader: {name}
struct VertexOutput {{
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}}
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {{
var out: VertexOutput;
// Generate full-screen quad vertices (triangle strip)
let x = f32(vertex_index & 1u);
let y = f32((vertex_index >> 1u) & 1u);
// Full screen in NDC - standard orientation
// Y-flip is handled in fragment shader via gl_FragCoord_st
out.position = vec4<f32>(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0);
out.uv = vec2<f32>(x, y);
return out;
}}
// ============ Fragment shader (transpiled from GLSL) ============
{fragment_wgsl}
"#,
);
Ok(full_wgsl)
}
pub(crate) fn transpile_glsl_to_wgsl(glsl_source: &str, shader_path: &Path) -> Result<String> {
transpile_impl(
glsl_source,
&shader_path.display().to_string(),
"par_term_debug_wrapped.glsl",
BuiltinPositionOrder::After,
)
}
pub(crate) fn transpile_glsl_to_wgsl_source(glsl_source: &str, name: &str) -> Result<String> {
transpile_impl(
glsl_source,
name,
"par_term_debug_wrapped_source.glsl",
BuiltinPositionOrder::Before,
)
}