par-term-render 0.6.7

GPU-accelerated rendering engine for par-term terminal emulator
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
use anyhow::Result;
use std::path::Path;

/// Pre-process GLSL to make Shadertoy `fragCoord` use the flipped Y convention **inside**
/// `mainImage`, avoiding cross-function `var<private>` writes that Metal is dropping.
///
/// Steps:
/// 1) Rename the `in vec2 <name>` parameter to `_fc_raw` (raw @builtin(position) coords).
/// 2) Inject at the start of `mainImage` a flipped local `vec2 fragCoord` and set
///    `gl_FragCoord_st` to that flipped value so shaders that read the global also see it.
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") {
        // Locate parameter list boundaries.
        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];

            // Find the first `in vec2` parameter and rename its identifier to `_fc_raw`.
            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");
            }
        }

        // Find the first '{' after the mainImage declaration to inject our prologue.
        if let Some(rel_brace) = source[main_pos..].find('{') {
            let inject_pos = main_pos + rel_brace + 1; // after '{'
            // Flip once here for Shadertoy convention (y=0 at bottom).
            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
}

/// The shared GLSL wrapper template injected around the user shader code.
///
/// The `{glsl_source}` placeholder is replaced with the user-provided (preprocessed) GLSL.
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);
    }}
}}
"#
    )
}

/// Controls how the `@builtin(position)` parameter is injected into the generated WGSL
/// `fs_main` function signature.
///
/// The two public transpile functions differ only in where they place the builtin relative
/// to the `@location(0) v_uv` parameter, which is determined by which naga output pattern
/// they were originally tuned against.
#[derive(Debug, Clone, Copy)]
enum BuiltinPositionOrder {
    /// `@location(0) v_uv, @builtin(position) frag_pos` — append after location param
    After,
    /// `@builtin(position) frag_pos, @location(0) v_uv` — prepend before location param
    Before,
}

/// Perform the string replacement and return an error if the target was not found.
///
/// This validates that the naga-generated WGSL contains the exact pattern we expect so
/// that a naga version change does not silently produce a broken shader.
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))
}

/// Core transpilation logic shared by both public entry points.
///
/// # Arguments
/// * `glsl_source` – The raw (not yet preprocessed) user GLSL shader source.
/// * `name` – A human-readable name used in error messages (file path or synthetic name).
/// * `debug_glsl_filename` – Filename (not full path) for the optional debug GLSL dump.
/// * `builtin_order` – Controls `@builtin(position)` placement in the WGSL signature.
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);

    // DEBUG: Write wrapped GLSL to file for inspection (debug builds only)
    #[cfg(debug_assertions)]
    {
        let debug_path = std::env::temp_dir().join(debug_glsl_filename);
        // Use restricted permissions (0o600) to prevent world-readable access on multi-user systems
        #[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);
        }
    }
    // Suppress unused-variable warning in release builds
    #[cfg(not(debug_assertions))]
    let _ = debug_glsl_filename;

    // Parse GLSL using naga
    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")
        )
    })?;

    // Validate the module
    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))?;

    // Generate WGSL output for fragment shader
    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))?;

    // Rename main() → fs_main() (naga always emits "main" for the entry point)
    let fragment_wgsl = fragment_wgsl.replace("fn main(", "fn fs_main(");

    // Inject @builtin(position) into fs_main's parameter list.
    //
    // Naga may emit the @fragment attribute and fn on a single line or with a newline
    // between them. We handle both variants. Each replacement is validated — if the
    // expected pattern is absent, naga's output format changed and we return an error
    // rather than silently producing a broken shader (M-11 fix).
    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 => (
                // with newline between @fragment and fn
                format!("@fragment \nfn fs_main({location_param}) -> FragmentOutput {{",),
                format!(
                    "@fragment \nfn fs_main({location_param}, {builtin_param}) -> FragmentOutput {{",
                ),
                // without newline
                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 {{",
                ),
            ),
        };

    // Try the "with space" variant first; if not found, try the "without space" variant.
    // At least one must succeed — both failing means naga changed its output format.
    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",
        )?
    };

    // Seed gl_FragCoord_st with the raw @builtin(position) coordinates immediately after
    // the v_uv assignment. The actual Y-flip is applied inside mainImage.
    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",
    )?;

    // Build the complete shader with vertex shader
    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)
}

/// Transpile a Ghostty/Shadertoy-style GLSL shader to WGSL
///
/// Supports standard Shadertoy uniforms:
/// - `iTime`: Current time in seconds
/// - `iResolution`: Viewport resolution (width, height, 1.0)
/// - `iMouse`: Mouse state (xy=current, zw=click)
/// - `iChannel0-3`: User-defined texture channels (Shadertoy compatible)
/// - `iChannel4`: Terminal content texture
/// - `iChannelResolution[0-4]`: Channel texture resolutions
///
/// Key press uniform (par-term specific):
/// - `iTimeKeyPress`: Time when last key was pressed (same timebase as iTime)
///
/// Ghostty-compatible cursor uniforms (v1.2.0+):
/// - `iCurrentCursor`: xy=position, zw=size (pixels)
/// - `iPreviousCursor`: xy=previous position, zw=size
/// - `iCurrentCursorColor`: RGBA (opacity baked into alpha)
/// - `iPreviousCursorColor`: RGBA previous color
/// - `iTimeCursorChange`: Time when cursor last moved
///
/// Cursor shader configuration uniforms (par-term specific):
/// - `iCursorShaderColor`: User-configured cursor color for effects (RGBA)
/// - `iCursorTrailDuration`: Trail effect duration in seconds
/// - `iCursorGlowRadius`: Glow effect radius in pixels
/// - `iCursorGlowIntensity`: Glow effect intensity (0.0-1.0)
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,
    )
}

/// Transpile a Ghostty/Shadertoy-style GLSL shader to WGSL from source string
///
/// Same as `transpile_glsl_to_wgsl` but takes a source string and name instead of a file path.
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,
    )
}