noesis_wgpu 0.1.0

A wgpu render device for the Noesis GUI Native SDK: draws Noesis's batches on a wgpu device and queue you provide.
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
// `@blend_src` needs this enable directive, which must precede every other
// declaration. Gated so only the LCD variant requires DUAL_SOURCE_BLENDING.
#ifdef EFFECT_SDF_LCD
enable dual_source_blending;
#endif

// One source for every Noesis shader. shader_preproc.rs strips the #ifdef
// branches using the define set from shader_defines::defines_for_shader().
//
// Ported from Shader.140.{vert,frag} in $NOESIS_SDK_DIR/Src/Packages/Render/
// GLRenderDevice/Src/; "GL ref" comments quote the GL code a block ports.

// ─── Uniforms ───
// cbuffer0_vs[16] (projection, 64B) + cbuffer1_vs[2] (glyph atlas size, 8B
// padded to vec4) packed into one struct so a single dynamic-offset bind group
// covers both. The SDF vertex shader needs cbuffer1_vs.xy to scale uv1 into
// glyph-atlas texel coords (`st1` below); other shaders ignore it.
struct VsUniforms {
    projection: mat4x4<f32>,
    glyph_size: vec4<f32>,
}

@group(0) @binding(0) var<uniform> vs_uniforms: VsUniforms;

// cbuffer0_ps[8]: values[0] is the fill color for EFFECT_RGBA, and
// values[0].x the opacity for the pattern and linear paints. PAINT_RADIAL
// documents its own layout.
struct PsUniforms0 {
    values: array<vec4<f32>, 2>,
}

@group(1) @binding(0) var<uniform> ps_uniforms0: PsUniforms0;

// cbuffer1_ps: read only by EFFECT_SHADOW / EFFECT_BLUR. Noesis declares
// float[128], but those effects use at most the first 7 floats, so two vec4s
// (values[0] = cb[0..3], values[1] = cb[4..7]). Shares group(1) to stay within
// the downlevel 4-bind-group limit.
#ifdef HAS_CBUFFER1_PS
struct PsUniforms1 {
    values: array<vec4<f32>, 2>,
}

@group(1) @binding(1) var<uniform> ps_uniforms1: PsUniforms1;
#endif

// group(2): the paint texture. Holds `pattern`, `ramps`, or `glyphs`
// depending on the shader, and the DOWNSAMPLE/UPSAMPLE source. Shaders that
// don't read it get a dummy bind group.
#ifdef HAS_PAINT_TEXTURE
@group(2) @binding(0) var paint_texture: texture_2d<f32>;
@group(2) @binding(1) var paint_sampler: sampler;
#endif

// group(3) bindings 0/1: `image`, the offscreen render of the layer being
// composited. Read by EFFECT_OPACITY, UPSAMPLE, SHADOW, and BLUR.
#ifdef HAS_IMAGE_TEXTURE
@group(3) @binding(0) var image_texture: texture_2d<f32>;
@group(3) @binding(1) var image_sampler: sampler;
#endif

// group(3) bindings 2/3: the blurred `shadow` pass, read by EFFECT_SHADOW and
// EFFECT_BLUR. Shares `image`'s group to stay within the 4-bind-group limit.
#ifdef HAS_SHADOW_TEXTURE
@group(3) @binding(2) var shadow_texture: texture_2d<f32>;
@group(3) @binding(3) var shadow_sampler: sampler;
#endif

// ─── Vertex I/O ───
// VsIn locations are the noesis_runtime VertexAttr indices (see
// vertex_layout.rs); each attribute is gated by its own HAS_* define.

struct VsIn {
    @location(0) pos: vec2<f32>,
#ifdef HAS_COLOR
    @location(1) color: vec4<f32>,
#endif
#ifdef HAS_UV0
    @location(2) uv0: vec2<f32>,
#endif
#ifdef HAS_UV1
    @location(3) uv1: vec2<f32>,
#endif
#ifdef HAS_COVERAGE
    @location(4) coverage: f32,
#endif
#ifdef HAS_RECT
    @location(5) rect: vec4<f32>,
#endif
#ifdef HAS_TILE
    @location(6) tile: vec4<f32>,
#endif
#ifdef HAS_IMAGE_POS
    @location(7) image_pos: vec4<f32>,
#endif
}

struct VsOut {
    @builtin(position) clip_position: vec4<f32>,
#ifdef HAS_COLOR
    // GL ref: `flat in vec4 color;`
    @location(0) @interpolate(flat) color: vec4<f32>,
#endif
#ifdef HAS_UV0
    @location(1) uv0: vec2<f32>,
#endif
#ifdef HAS_UV1
    @location(2) uv1: vec2<f32>,
#endif
#ifdef HAS_COVERAGE
    @location(4) coverage: f32,
#endif
#ifdef HAS_RECT
    @location(5) @interpolate(flat) rect: vec4<f32>,
#endif
#ifdef HAS_TILE
    @location(6) @interpolate(flat) tile: vec4<f32>,
#endif
#ifdef HAS_IMAGE_POS
    @location(7) image_pos: vec4<f32>,
#endif
#ifdef HAS_ST1
    // SDF only. uv1 in glyph-atlas texel space (uv1 × glyph_size). The
    // fragment uses dFdx(st1) to size the AA window per fragment.
    @location(3) st1: vec2<f32>,
#endif
#ifdef DOWNSAMPLE
    // EFFECT_DOWNSAMPLE box-filters four taps; the vertex shader spreads
    // attr_uv0 ± attr_uv1 into uv0..uv3 (GL Shader.140.vert DOWNSAMPLE block).
    @location(8) uv2: vec2<f32>,
    @location(9) uv3: vec2<f32>,
#endif
}

// ─── Vertex shader ───
@vertex
fn vs_main(in: VsIn) -> VsOut {
    var out: VsOut;
    // Noesis stores Matrix4 row-major (GetData()[i*4 + j] = row i, col j) and
    // uploads it verbatim to cbuffer0_vs. WGSL's mat4x4<f32> loads the 16
    // floats as column-major, which transposes the stored matrix relative
    // to the logical one. Right-multiplying `v * M` recovers the logical
    // transform (matches the GL 140 reference `vec4(pos, 0, 1) * mat4(...)`).
    out.clip_position = vec4<f32>(in.pos, 0.0, 1.0) * vs_uniforms.projection;
#ifdef HAS_COLOR
    out.color = in.color;
#endif
#ifdef DOWNSAMPLE
    // 4-tap box filter offsets (GL Shader.140.vert DOWNSAMPLE): uv0 is the
    // tap centre, uv1 carries the ±offset. Overrides the plain HAS_UV0/UV1
    // pass-through below (guarded by #ifndef DOWNSAMPLE).
    out.uv0 = in.uv0 + vec2<f32>(in.uv1.x, in.uv1.y);
    out.uv1 = in.uv0 + vec2<f32>(in.uv1.x, -in.uv1.y);
    out.uv2 = in.uv0 + vec2<f32>(-in.uv1.x, in.uv1.y);
    out.uv3 = in.uv0 + vec2<f32>(-in.uv1.x, -in.uv1.y);
#endif
#ifndef DOWNSAMPLE
#ifdef HAS_UV0
    out.uv0 = in.uv0;
#endif
#ifdef HAS_UV1
    out.uv1 = in.uv1;
#endif
#endif
#ifdef HAS_COVERAGE
    out.coverage = in.coverage;
#endif
#ifdef HAS_RECT
    out.rect = in.rect;
#endif
#ifdef HAS_TILE
    out.tile = in.tile;
#endif
#ifdef HAS_IMAGE_POS
    out.image_pos = in.image_pos;
#endif
#ifdef HAS_ST1
    // GL ref: `st1 = vec2(attr_uv1 * vec2(cbuffer1_vs[0], cbuffer1_vs[1]))`
    out.st1 = in.uv1 * vs_uniforms.glyph_size.xy;
#endif
    return out;
}

// ─── Fragment shader ───
//
// Each variant keeps one PAINT_* block (defining `paint` and `opacity`) and
// one EFFECT_* block that returns. The trailing `return vec4<f32>(0.0)` is
// unreachable; it keeps the function well-formed for every define set.
// EFFECT_SDF_LCD has its own dual-output `fs_main` below.
#ifndef EFFECT_SDF_LCD
@fragment
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
#ifdef EFFECT_RGBA
    return ps_uniforms0.values[0];
#endif

#ifdef EFFECT_MASK
    return vec4<f32>(1.0);
#endif

#ifdef EFFECT_CLEAR
    return vec4<f32>(0.0);
#endif

#ifdef PAINT_SOLID
    let paint = in.color;
    let opacity = 1.0;
#endif

// ─── Pattern paints ───
// The wrap variants (all but PAINT_PATTERN_PLAIN) read a `rect` attribute, the
// UV bounds of the pattern; fragments outside it contribute zero. REPEAT and
// MIRROR* also read `tile`, the tile origin (xy) and size (zw) for the wrap.
// GL ref: Shader.140.frag PAINT_PATTERN and its *_PATTERN subblocks.

#ifdef PAINT_PATTERN_PLAIN
    // The sampler handles wrap and filtering.
    let paint = textureSample(paint_texture, paint_sampler, in.uv0);
    let opacity = ps_uniforms0.values[0].x;
#endif

#ifdef CLAMP_PATTERN
    // Masking to `rect` in-shader lets Noesis atlas several patterns in one
    // texture, which a sampler wrap mode can't do.
    let clamped_uv = clamp(in.uv0, in.rect.xy, in.rect.zw);
    let inside = select(0.0, 1.0, all(in.uv0 == clamped_uv));
    let paint = inside * textureSample(paint_texture, paint_sampler, in.uv0);
    let opacity = ps_uniforms0.values[0].x;
#endif

#ifdef REPEAT_PATTERN
    // `tile` = (origin.xy, size.zw). Normalise uv into tile-local space,
    // `fract` to wrap, then lift back into pattern UV space and clamp by
    // `rect`. `textureSampleGrad` preserves the original screen-space
    // derivatives so the sampler picks the right mip despite the UV wrap.
    let raw = (in.uv0 - in.tile.xy) / in.tile.zw;
    let wrap = fract(raw);
    let uv = wrap * in.tile.zw + in.tile.xy;
    let clamped_uv = clamp(uv, in.rect.xy, in.rect.zw);
    let inside = select(0.0, 1.0, all(uv == clamped_uv));
    let paint = inside
        * textureSampleGrad(paint_texture, paint_sampler, uv, dpdx(in.uv0), dpdy(in.uv0));
    let opacity = ps_uniforms0.values[0].x;
#endif

#ifdef MIRRORU_PATTERN
    // Triangle-wave on U, plain fract on V. `abs(x - 2*floor((x-1)/2) - 2)`
    // is the SDK's branchless triangle-wave of period 2 in [0,2]; clamped
    // back into tile space below.
    let raw = (in.uv0 - in.tile.xy) / in.tile.zw;
    let wrap = vec2<f32>(
        abs(raw.x - 2.0 * floor((raw.x - 1.0) * 0.5) - 2.0),
        fract(raw.y),
    );
    let uv = wrap * in.tile.zw + in.tile.xy;
    let clamped_uv = clamp(uv, in.rect.xy, in.rect.zw);
    let inside = select(0.0, 1.0, all(uv == clamped_uv));
    let paint = inside
        * textureSampleGrad(paint_texture, paint_sampler, uv, dpdx(in.uv0), dpdy(in.uv0));
    let opacity = ps_uniforms0.values[0].x;
#endif

#ifdef MIRRORV_PATTERN
    // Mirror V only.
    let raw = (in.uv0 - in.tile.xy) / in.tile.zw;
    let wrap = vec2<f32>(
        fract(raw.x),
        abs(raw.y - 2.0 * floor((raw.y - 1.0) * 0.5) - 2.0),
    );
    let uv = wrap * in.tile.zw + in.tile.xy;
    let clamped_uv = clamp(uv, in.rect.xy, in.rect.zw);
    let inside = select(0.0, 1.0, all(uv == clamped_uv));
    let paint = inside
        * textureSampleGrad(paint_texture, paint_sampler, uv, dpdx(in.uv0), dpdy(in.uv0));
    let opacity = ps_uniforms0.values[0].x;
#endif

#ifdef MIRROR_PATTERN
    // Mirror both axes.
    let raw = (in.uv0 - in.tile.xy) / in.tile.zw;
    let wrap = abs(raw - 2.0 * floor((raw - vec2<f32>(1.0)) * 0.5) - vec2<f32>(2.0));
    let uv = wrap * in.tile.zw + in.tile.xy;
    let clamped_uv = clamp(uv, in.rect.xy, in.rect.zw);
    let inside = select(0.0, 1.0, all(uv == clamped_uv));
    let paint = inside
        * textureSampleGrad(paint_texture, paint_sampler, uv, dpdx(in.uv0), dpdy(in.uv0));
    let opacity = ps_uniforms0.values[0].x;
#endif

#ifdef PAINT_LINEAR
    // Noesis bakes per-gradient ramps into a 2D texture atlas, one row
    // per gradient. `uv0.x` is the gradient parameter (0..1 along the
    // gradient axis); `uv0.y` picks the atlas row. Matches the GL 140
    // reference: `vec4 paint = texture(ramps, uv0); opacity_ = cbuffer0_ps[0];`
    let paint = textureSample(paint_texture, paint_sampler, in.uv0);
    let opacity = ps_uniforms0.values[0].x;
#endif

#ifdef PAINT_RADIAL
    // GL ref Shader.140.frag PAINT_RADIAL. cbuffer0_ps layout
    // (values[0] = cb[0..3], values[1] = cb[4..7]):
    //   cb[0..2]: coefficients for the gradient parameter `u`
    //   cb[3]:    opacity
    //   cb[4..5]: focal-offset `dd` coefficients
    //   cb[6]:    ramp atlas row
    // uv0 is in focal-relative gradient space.
    let cb0 = ps_uniforms0.values[0];
    let cb1 = ps_uniforms0.values[1];
    let dd = cb1.x * in.uv0.x - cb1.y * in.uv0.y;
    let r = sqrt(in.uv0.x * in.uv0.x + in.uv0.y * in.uv0.y - dd * dd);
    let u = cb0.x * in.uv0.x + cb0.y * in.uv0.y + cb0.z * r;
    let paint = textureSample(paint_texture, paint_sampler, vec2<f32>(u, cb1.z));
    let opacity = cb0.w;
#endif

#ifdef EFFECT_PATH
    return opacity * paint;
#endif

#ifdef EFFECT_PATH_AA
    return (opacity * in.coverage) * paint;
#endif

#ifdef EFFECT_OPACITY
    // GL ref Shader.140.frag EFFECT_OPACITY:
    //   fragColor = texture(image, uv1) * (opacity_ * paint.a)
    // Color comes from the layer; the paint only contributes its alpha
    // (an opacity mask or a solid opacity).
    return textureSample(image_texture, image_sampler, in.uv1)
        * (opacity * paint.a);
#endif

#ifdef EFFECT_SHADOW
    // GL ref Shader.140.frag EFFECT_SHADOW:
    //   shadowColor = cbuffer1_ps[0..3]
    //   offset      = (cbuffer1_ps[4], -cbuffer1_ps[5])
    //   uv          = clamp(uv1 - offset, rect.xy, rect.zw)
    //   alpha       = mix(image(uv).a, shadow(uv).a, cbuffer1_ps[6])
    //   img         = image(clamp(uv1, rect.xy, rect.zw))
    //   fragColor   = (img + (1 - img.a) * (shadowColor * alpha)) * (opacity * paint.a)
    // The shadow composites under the layer (premultiplied `1 - img.a`).
    let shadow_color = ps_uniforms1.values[0];
    let shadow_offset = vec2<f32>(ps_uniforms1.values[1].x, -ps_uniforms1.values[1].y);
    let shadow_uv = clamp(in.uv1 - shadow_offset, in.rect.xy, in.rect.zw);
    let shadow_alpha = mix(
        textureSample(image_texture, image_sampler, shadow_uv).a,
        textureSample(shadow_texture, shadow_sampler, shadow_uv).a,
        ps_uniforms1.values[1].z,
    );
    let shadow_img = textureSample(
        image_texture, image_sampler, clamp(in.uv1, in.rect.xy, in.rect.zw));
    return (shadow_img + (1.0 - shadow_img.a) * (shadow_color * shadow_alpha))
        * (opacity * paint.a);
#endif

#ifdef EFFECT_BLUR
    // GL ref Shader.140.frag EFFECT_BLUR:
    //   fragColor = mix(image(uv1), shadow(uv1), cbuffer1_ps[0]) * (opacity * paint.a)
    // cbuffer1_ps[0] crossfades from the unblurred layer (0) to the blur (1).
    return mix(
        textureSample(image_texture, image_sampler, in.uv1),
        textureSample(shadow_texture, shadow_sampler, in.uv1),
        ps_uniforms1.values[0].x,
    ) * (opacity * paint.a);
#endif

#ifdef EFFECT_DOWNSAMPLE
    // GL ref Shader.140.frag EFFECT_DOWNSAMPLE: box filter of four taps of
    // the group(2) source at the VS-computed UVs.
    return (textureSample(paint_texture, paint_sampler, in.uv0)
        + textureSample(paint_texture, paint_sampler, in.uv1)
        + textureSample(paint_texture, paint_sampler, in.uv2)
        + textureSample(paint_texture, paint_sampler, in.uv3))
        * 0.25;
#endif

#ifdef EFFECT_UPSAMPLE
    // GL ref Shader.140.frag EFFECT_UPSAMPLE:
    //   mix(texture(image, uv1), texture(pattern, uv0), color.a)
    // `image` (group 3) is the lower-resolution pass, group(2) the
    // same-resolution source.
    return mix(
        textureSample(image_texture, image_sampler, in.uv1),
        textureSample(paint_texture, paint_sampler, in.uv0),
        in.color.a,
    );
#endif

#ifdef EFFECT_SDF
    // GL ref Shader.140.frag EFFECT_SDF:
    //   distance = SDF_SCALE * (texture(glyphs, uv1).r - SDF_BIAS)
    //   gradLen  = length(dFdx(st1))
    //   scale    = 1 / gradLen
    //   base     = SDF_BASE_DEV * (1 - (clamp(scale, MIN, MAX) - MIN) / (MAX - MIN))
    //   range    = SDF_AA_FACTOR * gradLen
    //   alpha    = smoothstep(base - range, base + range, distance)
    //   fragColor = (alpha * opacity) * paint
    let SDF_SCALE: f32 = 7.96875;
    let SDF_BIAS: f32 = 0.50196078431;
    let SDF_AA_FACTOR: f32 = 0.65;
    let SDF_BASE_MIN: f32 = 0.125;
    let SDF_BASE_MAX: f32 = 0.25;
    let SDF_BASE_DEV: f32 = -0.65;

    let distance = SDF_SCALE * (textureSample(paint_texture, paint_sampler, in.uv1).r - SDF_BIAS);
    let gradLen = length(dpdx(in.st1));
    let scale = 1.0 / gradLen;
    let base = SDF_BASE_DEV
        * (1.0 - (clamp(scale, SDF_BASE_MIN, SDF_BASE_MAX) - SDF_BASE_MIN)
                  / (SDF_BASE_MAX - SDF_BASE_MIN));
    let range = SDF_AA_FACTOR * gradLen;
    let alpha = smoothstep(base - range, base + range, distance);
    return (alpha * opacity) * paint;
#endif

    return vec4<f32>(0.0);
}
#endif

// ─── EFFECT_SDF_LCD: subpixel text via dual-source blending ───
//
// The SDK has no GL/VK reference for this path (those devices report no
// subpixel rendering). This is the usual single-channel SDF LCD technique:
// sample the distance field at three points a third of a screen pixel apart
// horizontally, and use each one's SDF_SOLID-style coverage for one of R/G/B.
//
// blend_src(0) is paint premultiplied by per-channel coverage; blend_src(1) is
// the coverage. The SrcOver_Dual blend (`cs + cd*(1 - src1)`) then gives
// `paint*cov + dst*(1 - cov)` per channel.
#ifdef EFFECT_SDF_LCD
struct FsLcdOut {
    @location(0) @blend_src(0) color: vec4<f32>,
    @location(0) @blend_src(1) coverage: vec4<f32>,
}

@fragment
fn fs_main(in: VsOut) -> FsLcdOut {
    let paint = in.color;

    let SDF_SCALE: f32 = 7.96875;
    let SDF_BIAS: f32 = 0.50196078431;
    let SDF_AA_FACTOR: f32 = 0.65;
    let SDF_BASE_MIN: f32 = 0.125;
    let SDF_BASE_MAX: f32 = 0.25;
    let SDF_BASE_DEV: f32 = -0.65;

    // AA window sizing, as in EFFECT_SDF.
    let gradLen = length(dpdx(in.st1));
    let scale = 1.0 / gradLen;
    let base = SDF_BASE_DEV
        * (1.0 - (clamp(scale, SDF_BASE_MIN, SDF_BASE_MAX) - SDF_BASE_MIN)
                  / (SDF_BASE_MAX - SDF_BASE_MIN));
    let range = SDF_AA_FACTOR * gradLen;

    // One-third-of-a-pixel horizontal stagger in glyph-atlas UV space. dpdx(uv1)
    // is the UV change per screen pixel in x; the R/G/B stripes sit at -1/3, 0,
    // +1/3 pixel.
    let duv = dpdx(in.uv1) * (1.0 / 3.0);
    let dr = SDF_SCALE * (textureSample(paint_texture, paint_sampler, in.uv1 - duv).r - SDF_BIAS);
    let dg = SDF_SCALE * (textureSample(paint_texture, paint_sampler, in.uv1).r - SDF_BIAS);
    let db = SDF_SCALE * (textureSample(paint_texture, paint_sampler, in.uv1 + duv).r - SDF_BIAS);
    let cov = vec3<f32>(
        smoothstep(base - range, base + range, dr),
        smoothstep(base - range, base + range, dg),
        smoothstep(base - range, base + range, db),
    );
    let cov_a = (cov.r + cov.g + cov.b) * (1.0 / 3.0);

    var out: FsLcdOut;
    // Premultiplied so the dual blend's `One` src factor leaves it untouched.
    out.color = vec4<f32>(paint.rgb * cov, paint.a * cov_a);
    out.coverage = vec4<f32>(cov * paint.a, cov_a * paint.a);
    return out;
}
#endif