cvkg-render-gpu 0.1.22

Cyber Viking Kvasir Graph (CVKG) - High-fidelity agentic UI framework
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
// =============================================================================
// SVG Filter Shaders — WGPU WGSL
// =============================================================================
// Implements all 16 SVG filter primitives as fullscreen quad fragment shaders.
// Each primitive is selected via a mode uniform. All shaders share the same
// vertex stage (fullscreen triangle) and bind group layout.
//
// Bind group layout (all shaders):
//   @group(0) @binding(0) var<uniform> params: FilterParams;
//   @group(0) @binding(1) var t_src: texture_2d<f32>;
//   @group(0) @binding(2) var s_src: sampler;
//   @group(0) @binding(3) var t_src2: texture_2d<f32>;  // second input (blend/composite/displacement)
//   @group(0) @binding(4) var s_src2: sampler;
//
// =============================================================================

// ── Shared Uniforms ──────────────────────────────────────────────────────────

struct FilterParams {
    // Region: x, y, width, height in pixels
    region:       vec4<f32>,
    // Source texture size (for texel step computation)
    src_size:     vec4<f32>,
    // Second source texture size
    src2_size:    vec4<f32>,
    // Mode selector (see constants below)
    mode:         u32,
    // Sub-mode / variant (e.g. blend type, composite operator)
    sub_mode:     u32,
    // Generic parameters (usage varies by shader)
    param0:       f32,
    param1:       f32,
    param2:       f32,
    param3:       f32,
    // Color matrix (4x5 stored as 5 vec4 rows, last component of each is the offset)
    cm_row0:      vec4<f32>,
    cm_row1:      vec4<f32>,
    cm_row2:      vec4<f32>,
    cm_row3:      vec4<f32>,
    // Flood color + opacity
    flood_color:  vec4<f32>,
    // Offset
    offset:       vec2<f32>,
    // Convolve matrix (up to 9 coefficients for 3x3)
    kernel:       vec4<f32>,  // k0 k1 k2 k3
    kernel2:      vec4<f32>,  // k4 k5 k6 k7
    kernel3:      f32,        // k8
    kernel_divisor: f32,
    kernel_bias:    f32,
    _kpad:          f32,
    // Displacement map scale
    disp_scale:   f32,
    _dpad0:       f32,
    _dpad1:       f32,
    _dpad2:       f32,
    // Turbulence parameters
    turb_base_freq: vec2<f32>,
    turb_seed:      f32,
    turb_num_octaves: f32,
    _tpad:            f32,
};

// ── Mode Constants ───────────────────────────────────────────────────────────

const MODE_GAUSSIAN_BLUR_H: u32 = 0u;
const MODE_GAUSSIAN_BLUR_V: u32 = 1u;
const MODE_COLOR_MATRIX:    u32 = 2u;
const MODE_BLEND:           u32 = 3u;
const MODE_COMPOSITE:       u32 = 4u;
const MODE_FLOOD:           u32 = 5u;
const MODE_OFFSET:          u32 = 6u;
const MODE_MERGE:           u32 = 7u;
const MODE_COMPONENT_XFER:  u32 = 8u;
const MODE_CONVOLVE:        u32 = 9u;
const MODE_DISPLACEMENT:    u32 = 10u;
const MODE_MORPHOLOGY:      u32 = 11u;
const MODE_TILE:            u32 = 12u;
const MODE_TURBULENCE:      u32 = 13u;

// Blend sub-modes
const BLEND_NORMAL:   u32 = 0u;
const BLEND_MULTIPLY: u32 = 1u;
const BLEND_SCREEN:   u32 = 2u;
const BLEND_DARKEN:   u32 = 3u;
const BLEND_LIGHTEN:  u32 = 4u;

// Composite sub-modes
const COMPOSITE_OVER:    u32 = 0u;
const COMPOSITE_IN:      u32 = 1u;
const COMPOSITE_OUT:     u32 = 2u;
const COMPOSITE_ATOP:    u32 = 3u;
const COMPOSITE_XOR:     u32 = 4u;
const COMPOSITE_LIGHTER: u32 = 5u;

// Morphology sub-modes
const MORPH_ERODE:  u32 = 0u;
const MORPH_DILATE: u32 = 1u;

// Component transfer sub-modes
const XFER_IDENTITY:     u32 = 0u;
const XFER_TABLE:        u32 = 1u;
const XFER_DISCRETE:     u32 = 2u;
const XFER_LINEAR:       u32 = 3u;
const XFER_GAMMA:        u32 = 4u;

// ── Bindings ────────────────────────────────────────────────────────────────

@group(0) @binding(0) var<uniform> params: FilterParams;
@group(0) @binding(1) var t_src: texture_2d<f32>;
@group(0) @binding(2) var s_src: sampler;
@group(0) @binding(3) var t_src2: texture_2d<f32>;
@group(0) @binding(4) var s_src2: sampler;

// ── Fullscreen Triangle Vertex ──────────────────────────────────────────────

struct VertexOutput {
    @builtin(position) position: vec4<f32>,
    @location(0)             texcoord: vec2<f32>,
};

@vertex
fn vs_filter(@builtin(vertex_index) idx: u32) -> VertexOutput {
    var out: VertexOutput;
    // Fullscreen triangle: (-1,-1), (3,-1), (-1,3)
    let x = f32((idx & 1u) << 2u) - 1.0;
    let y = f32((idx >> 1u) << 2u) - 1.0;
    out.position = vec4<f32>(x, y, 0.0, 1.0);
    out.texcoord = vec2<f32>((x + 1.0) * 0.5, (1.0 - y) * 0.5);
    return out;
}

// ── Fragment: Mode Dispatch ─────────────────────────────────────────────────

@fragment
fn fs_filter(in: VertexOutput) -> @location(0) vec4<f32> {
    switch params.mode {
        case MODE_GAUSSIAN_BLUR_H: { return gaussian_blur_h(in); }
        case MODE_GAUSSIAN_BLUR_V: { return gaussian_blur_v(in); }
        case MODE_COLOR_MATRIX:    { return color_matrix(in); }
        case MODE_BLEND:           { return blend(in); }
        case MODE_COMPOSITE:       { return composite(in); }
        case MODE_FLOOD:           { return flood(in); }
        case MODE_OFFSET:          { return offset(in); }
        case MODE_MERGE:           { return merge(in); }
        case MODE_COMPONENT_XFER:  { return component_transfer(in); }
        case MODE_CONVOLVE:        { return convolve(in); }
        case MODE_DISPLACEMENT:    { return displacement(in); }
        case MODE_MORPHOLOGY:      { return morphology(in); }
        case MODE_TILE:            { return tile(in); }
        case MODE_TURBULENCE:      { return turbulence(in); }
        default: { return textureSample(t_src, s_src, in.texcoord); }
    }
}

// ── Gaussian Blur (Separable) ───────────────────────────────────────────────
// param0 = kernel radius in pixels
// param1 = sigma

fn gaussian_weight(offset: f32, sigma: f32) -> f32 {
    let s = max(sigma, 0.3); // avoid div by zero
    return exp(-0.5 * (offset * offset) / (s * s));
}

fn gaussian_blur_h(in: VertexOutput) -> @location(0) vec4<f32> {
    let radius = i32(params.param0);
    let sigma = params.param1;
    let texel = 1.0 / params.src_size.xy;
    var color = vec4<f32>(0.0);
    var weight_sum = 0.0;

    for (var i: i32 = -radius; i <= radius; i++) {
        let w = gaussian_weight(f32(i), sigma);
        let uv = in.texcoord + vec2<f32>(f32(i) * texel.x, 0.0);
        color += textureSample(t_src, s_src, uv) * w;
        weight_sum += w;
    }

    return color / max(weight_sum, 0.001);
}

fn gaussian_blur_v(in: VertexOutput) -> @location(0) vec4<f32> {
    let radius = i32(params.param0);
    let sigma = params.param1;
    let texel = 1.0 / params.src_size.xy;
    var color = vec4<f32>(0.0);
    var weight_sum = 0.0;

    for (var i: i32 = -radius; i <= radius; i++) {
        let w = gaussian_weight(f32(i), sigma);
        let uv = in.texcoord + vec2<f32>(0.0, f32(i) * texel.y);
        color += textureSample(t_src, s_src, uv) * w;
        weight_sum += w;
    }

    return color / max(weight_sum, 0.001);
}

// ── Color Matrix ────────────────────────────────────────────────────────────

fn color_matrix(in: VertexOutput) -> @location(0) vec4<f32> {
    let src = textureSample(t_src, s_src, in.texcoord);
    let r = dot(params.cm_row0, vec4<f32>(src.rgb, 1.0));
    let g = dot(params.cm_row1, vec4<f32>(src.rgb, 1.0));
    let b = dot(params.cm_row2, vec4<f32>(src.rgb, 1.0));
    let a = dot(params.cm_row3, vec4<f32>(src.rgb, 1.0));
    return vec4<f32>(clamp(r, 0.0, 1.0), clamp(g, 0.0, 1.0), clamp(b, 0.0, 1.0), clamp(a, 0.0, 1.0));
}

// ── Blend ───────────────────────────────────────────────────────────────────

fn blend_normal(a: vec4<f32>, b: vec4<f32>) -> vec4<f32> { return a; }
fn blend_multiply(a: vec4<f32>, b: vec4<f32>) -> vec4<f32> { return a * b; }
fn blend_screen(a: vec4<f32>, b: vec4<f32>) -> vec4<f32> { return a + b - a * b; }
fn blend_darken(a: vec4<f32>, b: vec4<f32>) -> vec4<f32> { return min(a, b); }
fn blend_lighten(a: vec4<f32>, b: vec4<f32>) -> vec4<f32> { return max(a, b); }

fn blend(in: VertexOutput) -> @location(0) vec4<f32> {
    let a = textureSample(t_src, s_src, in.texcoord);
    let b = textureSample(t_src2, s_src2, in.texcoord);
    switch params.sub_mode {
        case BLEND_MULTIPLY: { return blend_multiply(a, b); }
        case BLEND_SCREEN:   { return blend_screen(a, b); }
        case BLEND_DARKEN:   { return blend_darken(a, b); }
        case BLEND_LIGHTEN:  { return blend_lighten(a, b); }
        default:             { return blend_normal(a, b); }
    }
}

// ── Composite ────────────────────────────────────────────────────────────────

fn composite(in: VertexOutput) -> @location(0) vec4<f32> {
    let a = textureSample(t_src, s_src, in.texcoord);
    let b = textureSample(t_src2, s_src2, in.texcoord);
    let fa = a.a;
    let fb = b.a;
    switch params.sub_mode {
        case COMPOSITE_IN: {
            return a * fb;
        }
        case COMPOSITE_OUT: {
            return a * (1.0 - fb);
        }
        case COMPOSITE_ATOP: {
            return a * fb + b * (1.0 - fa);
        }
        case COMPOSITE_XOR: {
            return a * (1.0 - fb) + b * (1.0 - fa);
        }
        case COMPOSITE_LIGHTER: {
            return a + b;
        }
        default { // OVER
            return a + b * (1.0 - fa);
        }
    }
}

// ── Flood ───────────────────────────────────────────────────────────────────

fn flood(in: VertexOutput) -> @location(0) vec4<f32> {
    return params.flood_color;
}

// ── Offset ──────────────────────────────────────────────────────────────────

fn offset(in: VertexOutput) -> @location(0) vec4<f32> {
    let uv = in.texcoord - params.offset / params.src_size.xy;
    return textureSample(t_src, s_src, uv);
}

// ── Merge ───────────────────────────────────────────────────────────────────
// Merge layers multiple inputs by simple alpha compositing (over).
// For 2 inputs: src over src2. For more, we'd need multiple passes.

fn merge(in: VertexOutput) -> @location(0) vec4<f32> {
    let a = textureSample(t_src, s_src, in.texcoord);
    let b = textureSample(t_src2, s_src2, in.texcoord);
    // Alpha compositing: a over b
    let out_a = a.a + b.a * (1.0 - a.a);
    if out_a < 0.001 {
        return vec4<f32>(0.0);
    }
    return vec4<f32>((a.rgb * a.a + b.rgb * b.a * (1.0 - a.a)) / out_a, out_a);
}

// ── Component Transfer ──────────────────────────────────────────────────────
// param0-3: linear slope/intercept per channel
// sub_mode: 0=identity, 1=table, 2=discrete, 3=linear, 4=gamma
// param0 = gamma (for gamma mode), param1 = linear slope, param2 = linear intercept

fn component_transfer(in: VertexOutput) -> @location(0) vec4<f32> {
    let src = textureSample(t_src, s_src, in.texcoord);
    switch params.sub_mode {
        case XFER_LINEAR: {
            let slope = params.param1;
            let intercept = params.param2;
            return vec4<f32>(
                clamp(src.r * slope + intercept, 0.0, 1.0),
                clamp(src.g * slope + intercept, 0.0, 1.0),
                clamp(src.b * slope + intercept, 0.0, 1.0),
                clamp(src.a * slope + intercept, 0.0, 1.0),
            );
        }
        case XFER_GAMMA: {
            let gamma = max(params.param0, 0.01);
            return vec4<f32>(
                pow(src.r, gamma),
                pow(src.g, gamma),
                pow(src.b, gamma),
                src.a,
            );
        }
        default { // IDENTITY, TABLE, DISCRETE: pass through
            return src;
        }
    }
}

// ── Convolve Matrix (3x3) ───────────────────────────────────────────────────
// kernel: k0-k8 stored in params.kernel (vec4), params.kernel2 (vec4), params.kernel3.x
// kernel_divisor, kernel_bias

fn convolve(in: VertexOutput) -> @location(0) vec4<f32> {
    let texel = 1.0 / params.src_size.xy;
    let k = array<vec4<f32>, 3>(
        params.kernel,
        params.kernel2,
        vec4<f32>(params.kernel3, 0.0, 0.0, 0.0)
    );
    let divisor = select(params.kernel_divisor, 1.0, params.kernel_divisor == 0.0);
    let bias = params.kernel_bias;

    var color = vec4<f32>(0.0);
    for (var y: i32 = -1; y <= 1; y++) {
        for (var x: i32 = -1; x <= 1; x++) {
            let uv = in.texcoord + vec2<f32>(f32(x) * texel.x, f32(y) * texel.y);
            let sample = textureSample(t_src, s_src, uv);
            let ki = k[u32(y + 1)][u32(x + 1)];
            color += sample * ki;
        }
    }

    color = color / divisor + vec4<f32>(bias);
    return vec4<f32>(clamp(color.rgb, vec3<f32>(0.0), vec3<f32>(1.0)), max(color.a, 0.0));
}

// ── Displacement Map ────────────────────────────────────────────────────────
// param0 = scale
// Channel selectors encoded in sub_mode bits:
//   bits 0-1: x channel (0=R, 1=G, 2=B, 3=A)
//   bits 2-3: y channel

fn channel_from_select(sample: vec4<f32>, sel: u32) -> f32 {
    switch sel {
        case 0u: { return sample.r; }
        case 1u: { return sample.g; }
        case 2u: { return sample.b; }
        default: { return sample.a; }
    }
}

fn displacement(in: VertexOutput) -> @location(0) vec4<f32> {
    let scale = params.disp_scale;
    let x_sel = params.sub_mode & 3u;
    let y_sel = (params.sub_mode >> 2u) & 3u;
    let disp = textureSample(t_src2, s_src2, in.texcoord);
    let dx = (channel_from_select(disp, x_sel) - 0.5) * scale;
    let dy = (channel_from_select(disp, y_sel) - 0.5) * scale;
    let uv = in.texcoord + vec2<f32>(dx, dy);
    return textureSample(t_src, s_src, uv);
}

// ── Morphology ──────────────────────────────────────────────────────────────
// sub_mode: 0=erode, 1=dilate
// param0 = radius_x, param1 = radius_y

fn morphology(in: VertexOutput) -> @location(0) vec4<f32> {
    let rx = i32(params.param0);
    let ry = i32(params.param1);
    let texel = 1.0 / params.src_size.xy;
    var result = textureSample(t_src, s_src, in.texcoord);

    if params.sub_mode == u32(MORPH_DILATE) {
        // Dilate: take max
        for (var y: i32 = -ry; y <= ry; y++) {
            for (var x: i32 = -rx; x <= rx; x++) {
                let uv = in.texcoord + vec2<f32>(f32(x) * texel.x, f32(y) * texel.y);
                let s = textureSample(t_src, s_src, uv);
                result = max(result, s);
            }
        }
    } else {
        // Erode: take min
        for (var y: i32 = -ry; y <= ry; y++) {
            for (var x: i32 = -rx; x <= rx; x++) {
                let uv = in.texcoord + vec2<f32>(f32(x) * texel.x, f32(y) * texel.y);
                let s = textureSample(t_src, s_src, uv);
                result = min(result, s);
            }
        }
    }

    return result;
}

// ── Tile ────────────────────────────────────────────────────────────────────
// Tile the source (alpha-only) into the filter region.

fn tile(in: VertexOutput) -> @location(0) vec4<f32> {
    let src_w = params.src_size.x;
    let src_h = params.src_size.y;
    let region = params.region;
    // Map UV to source texture space, wrapping
    let u = in.texcoord.x * region.z;
    let v = in.texcoord.y * region.w;
    let src_uv = vec2<f32>(u / src_w, v / src_h) % 1.0;
    return textureSample(t_src, s_src, fract(src_uv));
}

// ── Turbulence (Perlin-like) ────────────────────────────────────────────────
// Uses a simple value-noise approach with octave summation.
// turb_base_freq = base frequency (x, y)
// turb_seed = random seed
// turb_num_octaves = number of octaves

fn hash(p: vec2<f32>) -> f32 {
    let h = dot(p, vec2<f32>(127.1, 311.7));
    return fract(sin(h + params.turb_seed) * 43758.5453);
}

fn noise(p: vec2<f32>) -> f32 {
    let i = floor(p);
    let f = fract(p);
    let u = f * f * (3.0 - 2.0 * f); // smoothstep
    let a = hash(i);
    let b = hash(i + vec2<f32>(1.0, 0.0));
    let c = hash(i + vec2<f32>(0.0, 1.0));
    let d = hash(i + vec2<f32>(1.0, 1.0));
    return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

fn turbulence(in: VertexOutput) -> @location(0) vec4<f32> {
    let region = params.region;
    let pos = in.texcoord * region.zw;
    let base_freq = params.turb_base_freq;
    let octaves = i32(params.turb_num_octaves);

    var value = 0.0;
    var amplitude = 0.5;
    var frequency = 1.0;
    for (var i: i32 = 0; i < 8; i++) {
        if i >= octaves { break; }
        value += amplitude * noise(pos * frequency * base_freq);
        frequency *= 2.0;
        amplitude *= 0.5;
    }

    // Return as RGBA (same value in all channels for turbulence)
    let v = clamp(value, 0.0, 1.0);
    return vec4<f32>(v, v, v, 1.0);
}