agg-gui-wgpu 0.5.0

Hardware-accelerated wgpu renderer for agg-gui — DrawCtx via Vulkan/DX12/Metal/WebGL2, with SSAA, screenshot capture, and a custom render-pass hook
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
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
//! WGSL shader sources for all rendering pipelines.
//!
//! A single WGSL source compiles for both native wgpu (Vulkan/DX12/Metal) and
//! wgpu's WebGL2 backend — no `#[cfg(target_arch = "wasm32")]` splits required.
//!
//! All shaders share the same NDC vertex math as the GL backend:
//!   `ndc = (pos / resolution) * 2.0 - 1.0`
//!
//! Uniform struct byte layouts must exactly match the corresponding Rust
//! `#[repr(C)]` structs defined in `pipelines.rs`.

// ---------------------------------------------------------------------------
// Solid color pipeline (flat fill / stroke)
// ---------------------------------------------------------------------------
// group(0) binding(0): SolidUniforms { resolution, pad, color }

pub(crate) const SOLID_WGSL: &str = "
struct SolidUniforms {
    resolution: vec2<f32>,
    pad: vec2<f32>,
    color: vec4<f32>,
}
@group(0) @binding(0) var<uniform> u: SolidUniforms;

struct VIn { @location(0) pos: vec2<f32> }
struct VOut { @builtin(position) clip_pos: vec4<f32> }

@vertex fn vs_main(in: VIn) -> VOut {
    let ndc = (in.pos / u.resolution) * 2.0 - 1.0;
    return VOut(vec4<f32>(ndc, 0.0, 1.0));
}
@fragment fn fs_main() -> @location(0) vec4<f32> {
    return u.color;
}
";

// ---------------------------------------------------------------------------
// AA solid color pipeline (tess2 edge-flag halo strips)
// ---------------------------------------------------------------------------
// Same uniforms as solid; extra per-vertex alpha attribute for analytic AA.
// group(0) binding(0): SolidUniforms { resolution, pad, color }

pub(crate) const AA_SOLID_WGSL: &str = "
struct SolidUniforms {
    resolution: vec2<f32>,
    pad: vec2<f32>,
    color: vec4<f32>,
}
@group(0) @binding(0) var<uniform> u: SolidUniforms;

struct VIn {
    @location(0) pos: vec2<f32>,
    @location(1) alpha: f32,
}
struct VOut {
    @builtin(position) clip_pos: vec4<f32>,
    @location(0) v_alpha: f32,
}

@vertex fn vs_main(in: VIn) -> VOut {
    let ndc = (in.pos / u.resolution) * 2.0 - 1.0;
    return VOut(vec4<f32>(ndc, 0.0, 1.0), in.alpha);
}
@fragment fn fs_main(in: VOut) -> @location(0) vec4<f32> {
    return vec4<f32>(u.color.rgb, u.color.a * in.v_alpha);
}
";

// ---------------------------------------------------------------------------
// AA-texture pipeline — direct port of agg-sharp `Graphics2DGpu` AA.
// ---------------------------------------------------------------------------
//
// Vertex format: pos:vec2 + uv:vec2 (16 bytes). The U coordinate
// indexes into a 1024-wide alpha-step texture (col 0 α=0, cols 1+
// α=opaque) sampled LINEAR — the agg-sharp scheme described in
// `aa_texture_mesh.rs`. The fragment outputs PREMULTIPLIED colour
// (`color * α, α`) and the pipeline blends with One / OMSA so
// per-pixel coverage is correctly composited without bleed into
// neighbouring shapes.
//
// group(0) binding(0): AaTexUniforms { resolution, pad, color }
// group(1) binding(0): alpha-step texture
// group(1) binding(1): linear sampler

pub(crate) const AA_TEXTURE_WGSL: &str = "
struct AaTexUniforms {
    resolution: vec2<f32>,
    pad: vec2<f32>,
    color: vec4<f32>,
}
@group(0) @binding(0) var<uniform> u: AaTexUniforms;
@group(1) @binding(0) var step_tex: texture_2d<f32>;
@group(1) @binding(1) var step_sampler: sampler;

struct VIn {
    @location(0) pos: vec2<f32>,
    @location(1) uv: vec2<f32>,
}
struct VOut {
    @builtin(position) clip_pos: vec4<f32>,
    @location(0) v_uv: vec2<f32>,
}

@vertex fn vs_main(in: VIn) -> VOut {
    let ndc = (in.pos / u.resolution) * 2.0 - 1.0;
    return VOut(vec4<f32>(ndc, 0.0, 1.0), in.uv);
}

@fragment fn fs_main(in: VOut) -> @location(0) vec4<f32> {
    let tex_a = textureSample(step_tex, step_sampler, in.v_uv).a;
    let a = u.color.a * tex_a;
    return vec4<f32>(u.color.rgb * a, a);
}
";

// ---------------------------------------------------------------------------
// Gradient pipeline (linear + radial, SVG spread modes)
// ---------------------------------------------------------------------------
// group(0) binding(0): GradientUniforms (see pipelines.rs)
// group(1) binding(0): ramp texture, binding(1): sampler

pub(crate) const GRADIENT_WGSL: &str = "
struct GradientUniforms {
    resolution: vec2<f32>,
    pad0: vec2<f32>,
    line: vec4<f32>,
    radial: vec4<f32>,
    focal: vec2<f32>,
    pad1: vec2<f32>,
    screen_inv_a: vec4<f32>,
    screen_inv_b: vec2<f32>,
    pad2: vec2<f32>,
    gradient_inv_a: vec4<f32>,
    gradient_inv_b: vec2<f32>,
    kind: u32,
    spread_mode: u32,
    global_alpha: f32,
    _pad3a: f32,
    _pad3b: f32,
    _pad3c: f32,
}
@group(0) @binding(0) var<uniform> u: GradientUniforms;
@group(1) @binding(0) var u_ramp: texture_2d<f32>;
@group(1) @binding(1) var u_sampler: sampler;

struct VIn {
    @location(0) pos: vec2<f32>,
    @location(1) alpha: f32,
}
struct VOut {
    @builtin(position) clip_pos: vec4<f32>,
    @location(0) v_pos: vec2<f32>,
    @location(1) v_alpha: f32,
}

@vertex fn vs_main(in: VIn) -> VOut {
    let ndc = (in.pos / u.resolution) * 2.0 - 1.0;
    return VOut(vec4<f32>(ndc, 0.0, 1.0), in.pos, in.alpha);
}

fn aff(a: vec4<f32>, b: vec2<f32>, p: vec2<f32>) -> vec2<f32> {
    return vec2<f32>(p.x*a.x + p.y*a.z + b.x, p.x*a.y + p.y*a.w + b.y);
}

fn apply_spread(t: f32) -> f32 {
    if u.spread_mode == 1u {
        return 1.0 - abs((t - 2.0 * floor(t * 0.5)) - 1.0);
    } else if u.spread_mode == 2u {
        return t - floor(t);
    }
    return clamp(t, 0.0, 1.0);
}

fn linear_t(p: vec2<f32>) -> f32 {
    let a = u.line.xy;
    let b = u.line.zw;
    let d = b - a;
    let len2 = max(dot(d, d), 0.000001);
    return dot(p - a, d) / len2;
}

fn radial_t(p: vec2<f32>) -> f32 {
    let c = u.radial.xy;
    let r = max(u.radial.z, 0.000001);
    let f = u.focal;
    let dv = p - f;
    let fc = f - c;
    let A = dot(dv, dv);
    if A <= 0.000001 { return 0.0; }
    let B = 2.0 * dot(fc, dv);
    let C = dot(fc, fc) - r * r;
    let disc = max(B*B - 4.0*A*C, 0.0);
    let k = (-B + sqrt(disc)) / (2.0 * A);
    if k > 0.000001 { return 1.0 / k; }
    return 0.0;
}

@fragment fn fs_main(in: VOut) -> @location(0) vec4<f32> {
    var p = aff(u.screen_inv_a, u.screen_inv_b, in.v_pos);
    p = aff(u.gradient_inv_a, u.gradient_inv_b, p);
    var t: f32;
    if u.kind == 1u { t = radial_t(p); } else { t = linear_t(p); }
    let tc = apply_spread(t);
    let c = textureSample(u_ramp, u_sampler, vec2<f32>(tc, 0.5));
    return vec4<f32>(c.rgb, c.a * in.v_alpha * u.global_alpha);
}
";

// ---------------------------------------------------------------------------
// Textured quad pipeline (image blits, Label backbuffers)
// ---------------------------------------------------------------------------
// group(0) binding(0): TexUniforms { resolution, pad }
// group(1) binding(0): texture, binding(1): sampler

pub(crate) const TEX_WGSL: &str = "
struct TexUniforms {
    resolution: vec2<f32>,
    pad: vec2<f32>,
    tint: vec4<f32>,
}
@group(0) @binding(0) var<uniform> u: TexUniforms;
@group(1) @binding(0) var u_tex: texture_2d<f32>;
@group(1) @binding(1) var u_sampler: sampler;

struct VIn {
    @location(0) pos: vec2<f32>,
    @location(1) uv: vec2<f32>,
}
struct VOut {
    @builtin(position) clip_pos: vec4<f32>,
    @location(0) v_uv: vec2<f32>,
}

@vertex fn vs_main(in: VIn) -> VOut {
    let ndc = (in.pos / u.resolution) * 2.0 - 1.0;
    return VOut(vec4<f32>(ndc, 0.0, 1.0), in.uv);
}
@fragment fn fs_main(in: VOut) -> @location(0) vec4<f32> {
    return textureSample(u_tex, u_sampler, in.v_uv) * u.tint;
}
";

// ---------------------------------------------------------------------------
// 4×4-box downsample pipeline — used by SSAA at 4× linear minification.
//
// The destination quad is 1/4 the linear size of the source texture; a single
// bilinear tap would only cover 4 of the 16 source texels under each output
// pixel.  We instead place 4 bilinear taps at the centres of the four 2×2
// quadrants of the 4×4 source region under each dst pixel.  Each tap exactly
// averages its 2×2 quadrant (bilinear at the texel-grid junction has uniform
// 0.25/0.25/0.25/0.25 weights), so averaging the 4 taps gives the correct
// 4×4 box average — every source texel contributes 1/16, no aliasing.
//
// Reuses the textured-quad bind groups (TexUniforms + texture + sampler) and
// vertex shader from `TEX_WGSL`; only the fragment shader changes.
// ---------------------------------------------------------------------------

pub(crate) const TEX_DOWNSAMPLE_4X_WGSL: &str = "
struct TexUniforms {
    resolution: vec2<f32>,
    pad: vec2<f32>,
    tint: vec4<f32>,
}
@group(0) @binding(0) var<uniform> u: TexUniforms;
@group(1) @binding(0) var u_tex: texture_2d<f32>;
@group(1) @binding(1) var u_sampler: sampler;

struct VIn {
    @location(0) pos: vec2<f32>,
    @location(1) uv: vec2<f32>,
}
struct VOut {
    @builtin(position) clip_pos: vec4<f32>,
    @location(0) v_uv: vec2<f32>,
}

@vertex fn vs_main(in: VIn) -> VOut {
    let ndc = (in.pos / u.resolution) * 2.0 - 1.0;
    return VOut(vec4<f32>(ndc, 0.0, 1.0), in.uv);
}

@fragment fn fs_main(in: VOut) -> @location(0) vec4<f32> {
    let dims = vec2<f32>(textureDimensions(u_tex, 0));
    let texel = vec2<f32>(1.0) / dims;
    let s0 = textureSample(u_tex, u_sampler, in.v_uv + vec2<f32>(-1.0, -1.0) * texel);
    let s1 = textureSample(u_tex, u_sampler, in.v_uv + vec2<f32>( 1.0, -1.0) * texel);
    let s2 = textureSample(u_tex, u_sampler, in.v_uv + vec2<f32>(-1.0,  1.0) * texel);
    let s3 = textureSample(u_tex, u_sampler, in.v_uv + vec2<f32>( 1.0,  1.0) * texel);
    return (s0 + s1 + s2 + s3) * 0.25;
}
";

// ---------------------------------------------------------------------------
// 3×3-box downsample pipeline — used by SSAA at 3× linear minification.
//
// The destination quad is 1/3 the linear size of the source texture.  A single
// bilinear tap reads 4 texels of the 3×3 source region under each dst pixel —
// the corner 2×2 block — and entirely skips the opposite row + column (5 of 9
// texels never contribute), producing visible aliasing that looks close to no
// AA at all.
//
// Instead we do 9 point-aligned taps at integer texel offsets {-1, 0, +1}² from
// the centre.  With the bilinear sampler and centre-aligned UVs (centre of the
// 3×3 cell falls on a source texel centre, so v_uv * dims is an integer + 0.5),
// each tap returns exactly one source texel — equivalent to point sampling.
// Averaging the 9 gives the correct 3×3 box: every source texel contributes
// 1/9, no aliasing.
//
// Reuses the textured-quad bind groups + vertex shader from `TEX_WGSL`.
// ---------------------------------------------------------------------------

pub(crate) const TEX_DOWNSAMPLE_3X_WGSL: &str = "
struct TexUniforms {
    resolution: vec2<f32>,
    pad: vec2<f32>,
    tint: vec4<f32>,
}
@group(0) @binding(0) var<uniform> u: TexUniforms;
@group(1) @binding(0) var u_tex: texture_2d<f32>;
@group(1) @binding(1) var u_sampler: sampler;

struct VIn {
    @location(0) pos: vec2<f32>,
    @location(1) uv: vec2<f32>,
}
struct VOut {
    @builtin(position) clip_pos: vec4<f32>,
    @location(0) v_uv: vec2<f32>,
}

@vertex fn vs_main(in: VIn) -> VOut {
    let ndc = (in.pos / u.resolution) * 2.0 - 1.0;
    return VOut(vec4<f32>(ndc, 0.0, 1.0), in.uv);
}

@fragment fn fs_main(in: VOut) -> @location(0) vec4<f32> {
    let dims = vec2<f32>(textureDimensions(u_tex, 0));
    let texel = vec2<f32>(1.0) / dims;
    var sum = vec4<f32>(0.0);
    for (var dy: i32 = -1; dy <= 1; dy = dy + 1) {
        for (var dx: i32 = -1; dx <= 1; dx = dx + 1) {
            let off = vec2<f32>(f32(dx), f32(dy)) * texel;
            sum = sum + textureSample(u_tex, u_sampler, in.v_uv + off);
        }
    }
    return sum * (1.0 / 9.0);
}
";

// ---------------------------------------------------------------------------
// Layer composite pipeline
// ---------------------------------------------------------------------------
// Reuses the TEX_WGSL vertex shader.
// group(0) binding(0): LayerUniforms
// group(1) binding(0): layer texture, binding(1): sampler

pub(crate) const LAYER_WGSL: &str = "
struct LayerUniforms {
    resolution: vec2<f32>,
    alpha: f32,
    mask_enabled: u32,
    layer_size: vec2<f32>,
    mask_radius: f32,
    pad0: f32,
    mask_rect: vec4<f32>,
}
@group(0) @binding(0) var<uniform> u: LayerUniforms;
@group(1) @binding(0) var u_tex: texture_2d<f32>;
@group(1) @binding(1) var u_sampler: sampler;

struct VIn {
    @location(0) pos: vec2<f32>,
    @location(1) uv: vec2<f32>,
}
struct VOut {
    @builtin(position) clip_pos: vec4<f32>,
    @location(0) v_uv: vec2<f32>,
}

@vertex fn vs_main(in: VIn) -> VOut {
    let ndc = (in.pos / u.resolution) * 2.0 - 1.0;
    return VOut(vec4<f32>(ndc, 0.0, 1.0), in.uv);
}

fn rounded_mask(p: vec2<f32>) -> f32 {
    let half_size = u.mask_rect.zw * 0.5;
    let r = min(u.mask_radius, min(half_size.x, half_size.y));
    let center = u.mask_rect.xy + half_size;
    let q = abs(p - center) - max(half_size - vec2<f32>(r), vec2<f32>(0.0));
    let dist = length(max(q, vec2<f32>(0.0))) + min(max(q.x, q.y), 0.0) - r;
    return clamp(0.5 - dist, 0.0, 1.0);
}

@fragment fn fs_main(in: VOut) -> @location(0) vec4<f32> {
    let c = textureSample(u_tex, u_sampler, in.v_uv);
    var mask = 1.0;
    if u.mask_enabled != 0u {
        mask = rounded_mask(in.v_uv * u.layer_size);
    }
    let a = u.alpha * mask;
    return vec4<f32>(c.rgb * a, c.a * a);
}
";

// ---------------------------------------------------------------------------
// LCD subpixel text pipeline (3-pass write-mask fallback)
// ---------------------------------------------------------------------------
// Three render pipelines created from this shader, differing only in the
// ColorTargetState.write_mask (RED / GREEN / BLUE).  Each pass sets u.channel
// to select which subpixel channel's coverage drives the output alpha.
// group(0) binding(0): LcdUniforms { resolution, channel, pad, color }
// group(1) binding(0): mask texture (RGB8 packed as RGBA8), binding(1): sampler

pub(crate) const LCD_WGSL: &str = "
struct LcdUniforms {
    resolution: vec2<f32>,
    channel: u32,
    pad: u32,
    color: vec4<f32>,
}
@group(0) @binding(0) var<uniform> u: LcdUniforms;
@group(1) @binding(0) var u_mask: texture_2d<f32>;
@group(1) @binding(1) var u_sampler: sampler;

struct VIn {
    @location(0) pos: vec2<f32>,
    @location(1) uv: vec2<f32>,
}
struct VOut {
    @builtin(position) clip_pos: vec4<f32>,
    @location(0) v_uv: vec2<f32>,
}

@vertex fn vs_main(in: VIn) -> VOut {
    let ndc = (in.pos / u.resolution) * 2.0 - 1.0;
    return VOut(vec4<f32>(ndc, 0.0, 1.0), in.uv);
}
@fragment fn fs_main(in: VOut) -> @location(0) vec4<f32> {
    let c = textureSample(u_mask, u_sampler, in.v_uv).rgb;
    var ch: f32;
    if u.channel == 0u { ch = c.r; }
    else if u.channel == 1u { ch = c.g; }
    else { ch = c.b; }
    return vec4<f32>(u.color.rgb, ch * u.color.a);
}
";

// ---------------------------------------------------------------------------
// LCD backbuffer pipeline (3-pass write-mask, two-plane input)
// ---------------------------------------------------------------------------
// Composites an LcdCoverage-mode cached backbuffer (premultiplied colour plane
// + per-channel alpha plane) with premultiplied src-over blend.
// group(0) binding(0): LcbUniforms { resolution, channel, pad }
// group(1) binding(0): color plane, binding(1): alpha plane, binding(2): sampler

pub(crate) const LCB_WGSL: &str = "
struct LcbUniforms {
    resolution: vec2<f32>,
    channel: u32,
    global_alpha: f32,
}
@group(0) @binding(0) var<uniform> u: LcbUniforms;
@group(1) @binding(0) var u_color: texture_2d<f32>;
@group(1) @binding(1) var u_alpha: texture_2d<f32>;
@group(1) @binding(2) var u_sampler: sampler;

struct VIn {
    @location(0) pos: vec2<f32>,
    @location(1) uv: vec2<f32>,
}
struct VOut {
    @builtin(position) clip_pos: vec4<f32>,
    @location(0) v_uv: vec2<f32>,
}

@vertex fn vs_main(in: VIn) -> VOut {
    let ndc = (in.pos / u.resolution) * 2.0 - 1.0;
    return VOut(vec4<f32>(ndc, 0.0, 1.0), in.uv);
}
@fragment fn fs_main(in: VOut) -> @location(0) vec4<f32> {
    let c = textureSample(u_color, u_sampler, in.v_uv).rgb;
    let a = textureSample(u_alpha, u_sampler, in.v_uv).rgb;
    var cc: f32;
    var aa: f32;
    var col: vec3<f32>;
    if u.channel == 0u {
        cc = c.r; aa = a.r; col = vec3<f32>(cc, 0.0, 0.0);
    } else if u.channel == 1u {
        cc = c.g; aa = a.g; col = vec3<f32>(0.0, cc, 0.0);
    } else {
        cc = c.b; aa = a.b; col = vec3<f32>(0.0, 0.0, cc);
    }
    // Premultiplied output: scale colour AND alpha by the same global
    // alpha so a set_global_alpha fade stays premult-correct.
    return vec4<f32>(col * u.global_alpha, aa * u.global_alpha);
}
";

// ---------------------------------------------------------------------------
// Grayscale text pipeline (single-pass, alpha-writing) — used INSIDE layers
// ---------------------------------------------------------------------------
// The 3-pass LCD write-mask pipelines above never write the alpha channel
// (each pass masks to one colour channel).  Against the opaque surface that
// is fine (dst alpha is pinned at 1), but inside a transparent compositing
// layer the text region keeps alpha=0, so the pop_layer composite blends it
// additively and the glyphs wash out to white.  Inside a layer we therefore
// render text as a plain grayscale coverage quad: one pass, ColorWrites::ALL,
// standard src-over.  The coverage is the average of the mask's three
// channels (equal for a grayscale mask), so there is no subpixel chroma —
// which is correct, since LCD subpixel geometry only makes sense against the
// final opaque backbuffer, not an intermediate layer.
// group(0) binding(0): LcdUniforms { resolution, channel(unused), pad, color }
// group(1) binding(0): mask texture, binding(1): sampler

pub(crate) const TEXT_GRAY_WGSL: &str = "
struct LcdUniforms {
    resolution: vec2<f32>,
    channel: u32,
    pad: u32,
    color: vec4<f32>,
}
@group(0) @binding(0) var<uniform> u: LcdUniforms;
@group(1) @binding(0) var u_mask: texture_2d<f32>;
@group(1) @binding(1) var u_sampler: sampler;

struct VIn {
    @location(0) pos: vec2<f32>,
    @location(1) uv: vec2<f32>,
}
struct VOut {
    @builtin(position) clip_pos: vec4<f32>,
    @location(0) v_uv: vec2<f32>,
}

@vertex fn vs_main(in: VIn) -> VOut {
    let ndc = (in.pos / u.resolution) * 2.0 - 1.0;
    return VOut(vec4<f32>(ndc, 0.0, 1.0), in.uv);
}
@fragment fn fs_main(in: VOut) -> @location(0) vec4<f32> {
    let c = textureSample(u_mask, u_sampler, in.v_uv).rgb;
    let cov = (c.r + c.g + c.b) * (1.0 / 3.0);
    // Straight-alpha colour + coverage — the standard src-over blend folds
    // the coverage into both colour and the layer's accumulating alpha.
    return vec4<f32>(u.color.rgb, cov * u.color.a);
}
";

// ---------------------------------------------------------------------------
// LCD backbuffer FLATTEN pipeline (single-pass, alpha-writing) — INSIDE layers
// ---------------------------------------------------------------------------
// Two-plane cached backbuffer analogue of TEXT_GRAY: collapses the per-channel
// (premultiplied colour, per-channel alpha) planes into a single premultiplied
// RGBA sample so the layer texture receives correct alpha.  Alpha is the
// Rec.709 LUMINANCE-WEIGHTED mean of the three channel alphas; colour is the
// premultiplied colour plane as-is.  One pass, ColorWrites::ALL, premultiplied
// src-over.
//
// Software counterpart: `agg_gui::lcd_coverage::collapse_lcd_pixel`, which
// backs both `LcdBuffer::to_rgba8_top_down_collapsed` and the default
// `DrawCtx::draw_lcd_backbuffer_arc`.  Same weighted alpha, with ONE deliberate
// difference: that path additionally lifts the alpha to `max(weighted,
// max_i colour_i)`.  Do NOT port the lift here.  It exists only because the CPU
// path hands back STRAIGHT alpha and so has to divide colour by alpha, which
// would clamp for light text; this shader stays PREMULTIPLIED end to end and
// never divides, so it has no clamp to avoid — and the lift would cost it a
// `dst * (aa - weighted)` luminance error it currently does not have.
//
// Why luminance-weighted and not `max`: the per-channel composite over a uniform
// destination `d` is `out_i = c_i + d*(1 - a_i)`, while a single-alpha flatten
// yields `out_i = c_i + d*(1 - aa)`.  The Rec.709-weighted sum of the encoded
// channels is preserved exactly when `aa` is the Rec.709-weighted mean of the
// `a_i`, so perceived luminance over any neutral background is right to first
// order.  `max` over-weights coverage on the two channels below the max and so
// biases EVERY unequal-alpha pixel dark — every LCD glyph edge, and (for a Label,
// which paints no opaque background) essentially every text pixel.  That was the
// "text inside windows renders ~20% bolder when LCD is on" bug, windows being
// retained compositing layers that always take this path.  Chroma is still
// collapsed, which is intentional inside a layer.
// group(0) binding(0): LcbUniforms { resolution, channel(unused), global_alpha }
// group(1) binding(0): colour plane, binding(1): alpha plane, binding(2): sampler

pub(crate) const LCB_FLATTEN_WGSL: &str = "
struct LcbUniforms {
    resolution: vec2<f32>,
    channel: u32,
    global_alpha: f32,
}
@group(0) @binding(0) var<uniform> u: LcbUniforms;
@group(1) @binding(0) var u_color: texture_2d<f32>;
@group(1) @binding(1) var u_alpha: texture_2d<f32>;
@group(1) @binding(2) var u_sampler: sampler;

struct VIn {
    @location(0) pos: vec2<f32>,
    @location(1) uv: vec2<f32>,
}
struct VOut {
    @builtin(position) clip_pos: vec4<f32>,
    @location(0) v_uv: vec2<f32>,
}

@vertex fn vs_main(in: VIn) -> VOut {
    let ndc = (in.pos / u.resolution) * 2.0 - 1.0;
    return VOut(vec4<f32>(ndc, 0.0, 1.0), in.uv);
}
@fragment fn fs_main(in: VOut) -> @location(0) vec4<f32> {
    let c = textureSample(u_color, u_sampler, in.v_uv).rgb;
    let a = textureSample(u_alpha, u_sampler, in.v_uv).rgb;
    let aa = dot(a, vec3<f32>(0.2126, 0.7152, 0.0722));
    // Premultiplied: colour plane is already colour*coverage, so scaling both
    // it and the alpha by global_alpha keeps the sample premult-correct.
    return vec4<f32>(c * u.global_alpha, aa * u.global_alpha);
}
";

// ---------------------------------------------------------------------------
// Scaled / region capture blit — used by `screenshot_scaled.rs`.
//
// Draws a single fullscreen triangle over a small offscreen RGBA8 texture,
// sampling a sub-rect of the capture texture through a LINEAR sampler so the
// downscale comes for free.  No vertex buffer: positions are generated from
// `vertex_index`, and the UV window comes from the uniform block.
//
// Channel order needs no shader work: `textureSample` returns components in
// RGBA order whatever the source texture's memory layout is (BGRA surfaces
// included), and the blit target is RGBA8, so the R↔B swap that the CPU-side
// `poll_capture_readback` does per pixel happens implicitly here.
//
// sRGB does need explicit handling.  When the source is an sRGB-format
// texture the sampler already decoded to linear, so `flags.x` asks the
// fragment shader to re-encode to sRGB before writing to the (non-sRGB)
// target.  That makes the stored bytes equal the raw surface bytes — what
// `read_captured_screenshot` / `poll_capture_readback` hand back today —
// while the filtering itself still happens in linear space, which is the
// correct space to average in for a downscale.
// ---------------------------------------------------------------------------

pub(crate) const SCALED_BLIT_WGSL: &str = "
struct ScaledBlitUniforms {
    uv_offset: vec2<f32>,
    uv_scale: vec2<f32>,
    flags: vec4<f32>,
}
@group(0) @binding(0) var<uniform> u: ScaledBlitUniforms;
@group(1) @binding(0) var u_tex: texture_2d<f32>;
@group(1) @binding(1) var u_sampler: sampler;

struct VOut {
    @builtin(position) clip_pos: vec4<f32>,
    @location(0) v_uv: vec2<f32>,
}

@vertex fn vs_main(@builtin(vertex_index) vi: u32) -> VOut {
    var corners = array<vec2<f32>, 3>(
        vec2<f32>(-1.0, -1.0),
        vec2<f32>( 3.0, -1.0),
        vec2<f32>(-1.0,  3.0)
    );
    let p = corners[vi];
    // NDC -> [0,1] with the Y flip: v=0 is the TOP row of the source texture,
    // matching the Y-down framebuffer convention of `src_region`.
    let uv01 = vec2<f32>(p.x * 0.5 + 0.5, 0.5 - p.y * 0.5);
    return VOut(vec4<f32>(p, 0.0, 1.0), u.uv_offset + uv01 * u.uv_scale);
}

fn linear_to_srgb(c: vec3<f32>) -> vec3<f32> {
    let lo = c * 12.92;
    let hi = 1.055 * pow(max(c, vec3<f32>(0.0)), vec3<f32>(1.0 / 2.4)) - 0.055;
    return select(hi, lo, c <= vec3<f32>(0.0031308));
}

@fragment fn fs_main(in: VOut) -> @location(0) vec4<f32> {
    var c = textureSample(u_tex, u_sampler, in.v_uv);
    if (u.flags.x > 0.5) {
        c = vec4<f32>(linear_to_srgb(c.rgb), c.a);
    }
    return c;
}
";