wgsl.runWGSL Öğren

Interaktif ders / Etüt

WGSL Etütleri

Bu seri pikselle düşünmeyi öğretir: önce düz renk ve UV, sonra zaman, SDF, smoothstep, procedural noise, dönüşümler, raymarching ve compute tarafı. Her aşama kendi WGSL koduyla Studio'da açılabilir.

01 / fragment · vec4

Renk

Studio'da aç

Bir fragment shader'ın tek görevi var: ekrandaki her piksel için bir vec4 renk döndürmek (r, g, b, a). Burada hangi piksel olduğu önemli değil — hepsi aynı rengi alıyor.

Dene: ilk üç değeri değiştir (0.01.0). vec4<f32>(1.0, 0.0, 0.5, 1.0) magenta yapar.
etut-01-renk.wgslfragment · vec4
// Aşama 01 — Düz renk
// Bir fragment shader, her piksel için tek bir vec4 döndürür.

@vertex
fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4<f32> {
  // Tek bir üçgenle ekranı kapla (fullscreen triangle trick)
  let p = vec2<f32>(f32((vi << 1u) & 2u), f32(vi & 2u));
  return vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
}

@fragment
fn fs() -> @location(0) vec4<f32> {
  // r, g, b, a — değerleri 0..1 arasında
  return vec4<f32>(0.13, 0.83, 0.93, 1.0);
}

02 / position · gradient

UV koordinatları

Studio'da aç

Fragment shader'a @builtin(position) gelirse her pikselin nerede olduğunu öğreniriz. Çözünürlüğe bölünce UV elde edilir: sol üst (0,0), sağ alt (1,1). UV'yi doğrudan renge yazınca yatay/dikey gradyan çıkar.

Dene: uv.x ve uv.y'yi kanallar arasında değiş, 0.5 sabitini animate etmek için u.time kullan: 0.5 + 0.5 * sin(u.time).
etut-02-uv-koordinatlari.wgslposition · gradient
// Aşama 02 — UV koordinatları
// frag.xy / u.resolution → 0..1 aralığında konum.

struct U {
  resolution: vec2<f32>,
  time: f32,
};
@group(0) @binding(0) var<uniform> u: U;

@vertex
fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4<f32> {
  let p = vec2<f32>(f32((vi << 1u) & 2u), f32(vi & 2u));
  return vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
}

@fragment
fn fs(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
  let uv = frag.xy / u.resolution;
  return vec4<f32>(uv.x, uv.y, 0.5, 1.0);
}

03 / uniform · animate

Zaman

Studio'da aç

u.time uniform'ı CPU'dan her frame yenileniyor. Bunu sin/cos ile birleştirince animasyon çıkar. Aynı shader saniyede 60 kez yeniden çalışıyor; değişen tek şey time değeri.

Dene: dalga frekansını değiştir (* 3.14* 8.0 yap), ya da u.time'a çarpan ekle: sin(u.time * 4.0 + ...).
etut-03-zaman.wgsluniform · animate
// Aşama 03 — Zaman ile animasyon
// u.time saniye cinsinden, her frame artar.

struct U {
  resolution: vec2<f32>,
  time: f32,
};
@group(0) @binding(0) var<uniform> u: U;

@vertex
fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4<f32> {
  let p = vec2<f32>(f32((vi << 1u) & 2u), f32(vi & 2u));
  return vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
}

@fragment
fn fs(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
  let uv = frag.xy / u.resolution;
  let r = 0.5 + 0.5 * sin(u.time + uv.x * 3.14);
  let g = 0.5 + 0.5 * sin(u.time * 1.3 + uv.y * 3.14);
  let b = 0.85;
  return vec4<f32>(r * 0.3, g, b, 1.0);
}

04 / length · step

Mesafe alanları (SDF)

Studio'da aç

Geometriyi çizmek yerine mesafeyle tarif ediyoruz. length(uv - center) bir noktanın merkeze uzaklığını verir; - r dersen şekil içeride negatif, kenarda 0, dışarıda pozitif olur. step() ile bunu sert bir iç/dış maskesine çevirebilirsin.

Dene: yarıçapı animate et — let r = 0.30 + 0.10 * sin(u.time * 2.0);. Karenin SDF'i: max(abs(uv.x), abs(uv.y)) - r.
etut-04-mesafe-alanlari-sdf.wgsllength · step
// Aşama 04 — Signed Distance Field
// Daireyi mesafe fonksiyonuyla çiz.

struct U {
  resolution: vec2<f32>,
  time: f32,
};
@group(0) @binding(0) var<uniform> u: U;

@vertex
fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4<f32> {
  let p = vec2<f32>(f32((vi << 1u) & 2u), f32(vi & 2u));
  return vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
}

@fragment
fn fs(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
  // Aspect-correct UV, merkez (0, 0)
  let aspect = u.resolution.x / u.resolution.y;
  let uv = (frag.xy / u.resolution - 0.5) * vec2<f32>(aspect, 1.0);

  let r = 0.30;
  let d = length(uv) - r;       // negatif: içeride · pozitif: dışarıda
  let inside = step(d, 0.0);    // 1.0 içerde, 0.0 dışarıda

  let bg = vec3<f32>(0.05, 0.07, 0.10);
  let fg = vec3<f32>(0.13, 0.83, 0.93);
  return vec4<f32>(mix(bg, fg, inside), 1.0);
}

05 / smoothstep · edge

Smoothstep / AA

Studio'da aç

step() sert bir 0/1 keser — kenarda piksel piksel zigzag olur. Smoothstep ise iki eşik arası yumuşak bir geçiş döndürür. Eşikleri piksel boyutuna bağlarsan (1.0 / resolution.y), her ekran çözünürlüğünde tek piksellik bir anti-alias çıkar.

Dene: aa değerini 0.05 yap — daire bulanıklaşır (yumuşak gölge etkisi). 0.0 yap, kenar titremeli geri döner.
etut-05-smoothstep-aa.wgslsmoothstep · edge
// Aşama 05 — Smoothstep ile anti-aliasing
// Eşik genişliğini piksel boyutuna bağla, her çözünürlükte temiz kenar.

struct U {
  resolution: vec2<f32>,
  time: f32,
};
@group(0) @binding(0) var<uniform> u: U;

@vertex
fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4<f32> {
  let p = vec2<f32>(f32((vi << 1u) & 2u), f32(vi & 2u));
  return vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
}

@fragment
fn fs(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
  let aspect = u.resolution.x / u.resolution.y;
  let uv = (frag.xy / u.resolution - 0.5) * vec2<f32>(aspect, 1.0);

  let r = 0.30 + 0.05 * sin(u.time * 1.5);
  let d = length(uv) - r;

  // 1.5 piksellik yumuşatma bandı
  let aa = 1.5 / u.resolution.y;
  let inside = 1.0 - smoothstep(-aa, aa, d);

  let bg = vec3<f32>(0.05, 0.07, 0.10);
  let fg = vec3<f32>(0.13, 0.83, 0.93);
  return vec4<f32>(mix(bg, fg, inside), 1.0);
}

06 / fract · hash

Procedural noise

Studio'da aç

WGSL'de hazır random() yok — kendin deterministik bir hash yazarsın. sin · dot kombinasyonu klasik trick: aynı girdiye her zaman aynı pseudo-random çıktıyı verir. Komşu noktalara mix'lersen value noise olur — doku, bulut, zemin için temel.

Dene: uv * 8.0'i * 24.0 yap — daha ince doku. Birkaç oktav topla (FBM): noise(uv*4.0)*0.5 + noise(uv*8.0)*0.25 + ....
etut-06-procedural-noise.wgslfract · hash
// Aşama 06 — Value noise
// hash → komşu interpolation → smooth pseudo-random doku

struct U {
  resolution: vec2<f32>,
  time: f32,
};
@group(0) @binding(0) var<uniform> u: U;

fn hash(p: vec2<f32>) -> f32 {
  return fract(sin(dot(p, vec2<f32>(12.9898, 78.233))) * 43758.5453);
}

fn noise(p: vec2<f32>) -> f32 {
  let i = floor(p);
  let f = fract(p);
  // Hermite easing
  let s = f * f * (3.0 - 2.0 * f);
  let a = hash(i + vec2<f32>(0.0, 0.0));
  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, s.x), mix(c, d, s.x), s.y);
}

@vertex
fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4<f32> {
  let p = vec2<f32>(f32((vi << 1u) & 2u), f32(vi & 2u));
  return vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
}

@fragment
fn fs(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
  let uv = frag.xy / u.resolution;
  let n = noise(uv * 8.0 + vec2<f32>(u.time * 0.3, u.time * 0.15));
  let col = mix(
    vec3<f32>(0.04, 0.10, 0.15),
    vec3<f32>(0.13, 0.83, 0.93),
    n
  );
  return vec4<f32>(col, 1.0);
}

07 / mat2x2 · rotate

Matris dönüşümleri

Studio'da aç

WGSL'de matris çarpımı yerleşik bir operatör: mat2x2 * vec2 = vec2. Burada her piksel kendi UV'sini 2D dönme matrisiyle çarpıyor — sonuç: tüm sahne dönmüş gibi görünüyor. Ama dönen sahne değil, koordinat sistemi. Aynı mantıkla mat3x3 3D rotate, mat4x4 projeksiyon.

Dene: matrise scale ekle — let S = mat2x2<f32>(1.5, 0.0, 0.0, 1.5); sonra uv = S * R * uv;. Sıralama önemli (önce dön, sonra ölç).
etut-07-matris-donusumleri.wgslmat2x2 · rotate
// Aşama 07 — Matris dönüşümleri
// 2x2 dönme matrisiyle UV'yi döndür.

struct U {
  resolution: vec2<f32>,
  time: f32,
};
@group(0) @binding(0) var<uniform> u: U;

@vertex
fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4<f32> {
  let p = vec2<f32>(f32((vi << 1u) & 2u), f32(vi & 2u));
  return vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
}

@fragment
fn fs(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
  let aspect = u.resolution.x / u.resolution.y;
  var uv = (frag.xy / u.resolution - 0.5) * vec2<f32>(aspect, 1.0);

  // 2D dönme matrisi (radyan cinsinden)
  let a = u.time * 0.5;
  let R = mat2x2<f32>(
    cos(a), -sin(a),
    sin(a),  cos(a)
  );
  uv = R * uv;

  // Kare SDF
  let r = 0.25;
  let d = max(abs(uv.x), abs(uv.y)) - r;

  let aa = 1.5 / u.resolution.y;
  let inside = 1.0 - smoothstep(-aa, aa, d);

  let bg = vec3<f32>(0.05, 0.07, 0.10);
  let fg = vec3<f32>(0.13, 0.83, 0.93);
  return vec4<f32>(mix(bg, fg, inside), 1.0);
}

08 / smin · union

SDF kompozisyonu

Studio'da aç

İki SDF'i birleştirmek operatörlerle yapılır: min(a, b) = union (sert kesişim), max(a, b) = intersection, max(a, -b) = subtraction. smin ("smooth min") ise yumuşak birleşim — tipik metaball efekti, lav lambası, organik şekiller.

Dene: smin'in k'sını 0.30 yap, daha akışkan birleşim. Üçüncü daire ekle. max'a çevir: kesişim.
etut-08-sdf-kompozisyonu.wgslsmin · union
// Aşama 08 — SDF kompozisyonu
// İki dairenin smooth-min ile yumuşak birleşmesi (metaball).

struct U {
  resolution: vec2<f32>,
  time: f32,
};
@group(0) @binding(0) var<uniform> u: U;

// Smooth minimum
fn smin(a: f32, b: f32, k: f32) -> f32 {
  let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
  return mix(b, a, h) - k * h * (1.0 - h);
}

@vertex
fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4<f32> {
  let p = vec2<f32>(f32((vi << 1u) & 2u), f32(vi & 2u));
  return vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
}

@fragment
fn fs(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
  let aspect = u.resolution.x / u.resolution.y;
  let uv = (frag.xy / u.resolution - 0.5) * vec2<f32>(aspect, 1.0);

  let p1 = vec2<f32>( 0.25 * cos(u.time), 0.0);
  let p2 = vec2<f32>(-0.25 * cos(u.time), 0.0);

  let d1 = length(uv - p1) - 0.18;
  let d2 = length(uv - p2) - 0.18;
  let d  = smin(d1, d2, 0.12);

  let aa = 1.5 / u.resolution.y;
  let inside = 1.0 - smoothstep(-aa, aa, d);

  let bg = vec3<f32>(0.05, 0.07, 0.10);
  let fg = vec3<f32>(0.13, 0.83, 0.93);
  return vec4<f32>(mix(bg, fg, inside), 1.0);
}

09 / atan2 · kaleidoscope

Polar koordinat

Studio'da aç

(x, y)(r, θ) dönüşümü (length ve atan2) radyal desenler için temel: halka, ışın, kaleidoscope. θ üzerinde modulo + abs simetri segmentleri çıkarır — tek bir formül 6 (veya 8, 12) kez yansıyarak ayna efekti verir.

Dene: segments4.0 ya da 12.0 yap. r * 24.0* 48.0 ile değiştir, halkalar sıklaşır. cos(theta * 6.0)'daki 6'yı değiştir.
etut-09-polar-koordinat.wgslatan2 · kaleidoscope
// Aşama 09 — Polar koordinat ve kaleidoskop
// (x, y) → (r, θ) → simetri uygula → renklendir

struct U {
  resolution: vec2<f32>,
  time: f32,
};
@group(0) @binding(0) var<uniform> u: U;

@vertex
fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4<f32> {
  let p = vec2<f32>(f32((vi << 1u) & 2u), f32(vi & 2u));
  return vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
}

@fragment
fn fs(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
  let aspect = u.resolution.x / u.resolution.y;
  let uv = (frag.xy / u.resolution - 0.5) * vec2<f32>(aspect, 1.0);

  let r = length(uv);
  var theta = atan2(uv.y, uv.x) + u.time * 0.3;

  // 6'lı simetri
  let segments = 6.0;
  let segAng = 6.2831 / segments;
  theta = abs(((theta + segAng * 0.5) % segAng) - segAng * 0.5);

  // Halka + ışın
  let ring  = 0.5 + 0.5 * sin(r * 24.0 - u.time * 2.0);
  let arm   = pow(cos(theta * 6.0) * 0.5 + 0.5, 4.0);
  let blend = ring * 0.5 + arm * 0.5;

  let col = mix(
    vec3<f32>(0.03, 0.07, 0.13),
    vec3<f32>(0.13, 0.83, 0.93),
    blend
  );
  return vec4<f32>(col, 1.0);
}

10 / sdf · lighting

Raymarching 3D

Studio'da aç

3D rendering tek bir fragment shader'da. Her piksel için kameradan bir ışın atılır. Işın SDF'in verdiği güvenli mesafe kadar ilerler — yüzeye varana kadar (d < 0.001). Çarpınca: normal SDF gradyanından, ışıklandırma dot(n, light)'tan, rim'i amber dokunuşla. Polygon yok, mesh yok — saf matematik.

Dene: üçüncü küre ekle. lightDir'i u.time ile animate et. Zemin: p.y + 1.0'ı sahne SDF'e min'le ekle.
etut-10-raymarching-3d.wgslsdf · lighting
// Aşama 10 — Raymarching 3D
// SDF + ışın yürüyüşü + ışıklandırma → mesh'siz 3D sahne.

struct U {
  resolution: vec2<f32>,
  time: f32,
};
@group(0) @binding(0) var<uniform> u: U;

fn sdSphere(p: vec3<f32>, r: f32) -> f32 {
  return length(p) - r;
}

fn smin(a: f32, b: f32, k: f32) -> f32 {
  let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
  return mix(b, a, h) - k * h * (1.0 - h);
}

// Sahne: iki küre, smooth union
fn scene(p: vec3<f32>) -> f32 {
  let t = u.time;
  let s1 = sdSphere(p - vec3<f32>(0.0, 0.0, 0.0), 0.6);
  let s2 = sdSphere(
    p - vec3<f32>(cos(t) * 0.9, sin(t * 0.7) * 0.4, sin(t) * 0.9),
    0.35
  );
  return smin(s1, s2, 0.20);
}

// Normal — SDF gradyanından
fn calcNormal(p: vec3<f32>) -> vec3<f32> {
  let e = 0.0015;
  return normalize(vec3<f32>(
    scene(p + vec3<f32>(e, 0.0, 0.0)) - scene(p - vec3<f32>(e, 0.0, 0.0)),
    scene(p + vec3<f32>(0.0, e, 0.0)) - scene(p - vec3<f32>(0.0, e, 0.0)),
    scene(p + vec3<f32>(0.0, 0.0, e)) - scene(p - vec3<f32>(0.0, 0.0, e))
  ));
}

@vertex
fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4<f32> {
  let p = vec2<f32>(f32((vi << 1u) & 2u), f32(vi & 2u));
  return vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
}

@fragment
fn fs(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
  let aspect = u.resolution.x / u.resolution.y;
  // Y'yi flip et (ekran yukarıdan aşağı, dünya aşağıdan yukarı)
  let uv = (frag.xy / u.resolution - 0.5) * vec2<f32>(aspect, -1.0);

  // Kamera
  let ro = vec3<f32>(0.0, 0.0, -2.4);
  let rd = normalize(vec3<f32>(uv, 1.5));

  // Ray march
  var t: f32 = 0.0;
  var hit: bool = false;
  for (var i = 0; i < 80; i = i + 1) {
    let p = ro + rd * t;
    let d = scene(p);
    if d < 0.001 {
      hit = true;
      break;
    }
    if t > 10.0 { break; }
    t = t + d;
  }

  var col: vec3<f32>;
  if hit {
    let p = ro + rd * t;
    let n = calcNormal(p);
    let lightDir = normalize(vec3<f32>(0.6, 0.8, -0.4));
    let diffuse  = max(dot(n, lightDir), 0.0);
    let ambient  = 0.18;
    let lit = ambient + diffuse * 0.82;

    let baseCol = vec3<f32>(0.13, 0.83, 0.93);
    col = baseCol * lit;

    // Rim ışık (amber)
    let rim = pow(1.0 - max(dot(n, -rd), 0.0), 2.0);
    col = col + vec3<f32>(0.96, 0.62, 0.04) * rim * 0.35;
  } else {
    let v = length(uv) * 0.6;
    col = mix(
      vec3<f32>(0.04, 0.08, 0.13),
      vec3<f32>(0.01, 0.02, 0.04),
      v
    );
  }

  return vec4<f32>(col, 1.0);
}

11 / workgroup · buffer

Compute & storage

Studio'da aç

Pipeline'ın diğer yarısı: compute shader bir storage buffer'ı doldurur, sonra fragment onu okur. Burada compute her frame 1024 noktalık bir sinüs dalgası üretir, fragment de aynı buffer'ı okuyup bar chart çiziyor. @workgroup_size(64) = her workgroup'ta 64 thread, runtime 16 workgroup dispatch ediyor → toplam 1024 invocation. Not: WGSL fragment shader'da read_write storage'a izin vermez, o yüzden aynı buffer iki binding'e bağlı: compute @binding(1)'e yazıyor, fragment @binding(2)'den okuyor.

Dene: cs'i değiştir — data_w[idx] = 0.5 + 0.4 * sin(x * 12.0 + u.time * 3.0) hızlandırır. Ya da fragment'te bar yerine alan dolduran çizim: let v = data_r[i]; return vec4(v, v*0.6, 1.0, 1.0);.
etut-11-compute-storage.wgslworkgroup · buffer
// Aşama 11 — Compute → storage → fragment
// Compute her frame buffer'ı doldurur, fragment aynı buffer'ı okur.
// Aynı bellek, iki binding: compute write, fragment read.

struct U {
  resolution: vec2<f32>,
  time: f32,
};
@group(0) @binding(0) var<uniform>             u:      U;
@group(0) @binding(1) var<storage, read_write> data_w: array<f32>;  // compute yazar
@group(0) @binding(2) var<storage, read>       data_r: array<f32>;  // fragment okur

const N: u32 = 1024u;

// 1) Compute: 1024 noktalık dalgayı üret
@compute @workgroup_size(64)
fn cs(@builtin(global_invocation_id) gid: vec3<u32>) {
  let idx = gid.x;
  if idx >= N { return; }
  let x = f32(idx) / f32(N);
  data_w[idx] = 0.5 + 0.4 * sin(x * 6.2831 * 3.0 + u.time * 2.0);
}

// 2) Render: aynı buffer'ı oku, bar chart çiz
@vertex
fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4<f32> {
  let p = vec2<f32>(f32((vi << 1u) & 2u), f32(vi & 2u));
  return vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
}

@fragment
fn fs(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
  let uv = frag.xy / u.resolution;
  let i = u32(uv.x * f32(N));
  let v = data_r[i];

  let on = step(1.0 - uv.y, v);
  let bg = vec3<f32>(0.04, 0.07, 0.10);
  let fg = vec3<f32>(0.13, 0.83, 0.93);
  return vec4<f32>(mix(bg, fg, on), 1.0);
}

12 / parallel · reduce

Workgroup memory

Studio'da aç

var<workgroup> sadece aynı workgroup'un thread'lerinin gördüğü paylaşımlı bellek. workgroupBarrier() ile thread'ler birbirini bekler. Burada her workgroup 64 değeri log₂(64) = 6 paralel adımda topluyor — sıralı CPU 64 ekleme yaparken GPU 6'ya iner. Üst yarı: 1024 ham veri (cyan). Alt yarı: 16 workgroup'un partial ortalamaları (amber).

Dene: reduce'u + yerine max yap (her workgroup'un max'ı). WG'yi 32 yap (5 adımda biter), ama runtime dispatch'i de buna göre değişmeli — değiştirmezsen yarı veri görünür.
etut-12-workgroup-memory.wgslparallel · reduce
// Aşama 12 — Workgroup memory ve parallel reduce
// Her workgroup 64 değeri 6 adımda log₂ reduce ile toplar.

struct U {
  resolution: vec2<f32>,
  time: f32,
};
@group(0) @binding(0) var<uniform>             u:      U;
@group(0) @binding(1) var<storage, read_write> data_w: array<f32>;
@group(0) @binding(2) var<storage, read>       data_r: array<f32>;

const N:  u32 = 1024u;
const WG: u32 = 64u;

// Workgroup-yerel paylaşımlı bellek (sadece bu workgroup'un thread'leri görür)
var<workgroup> wg_buf: array<f32, 64>;

@compute @workgroup_size(64)
fn cs(
  @builtin(local_invocation_id)  lid:  vec3<u32>,
  @builtin(workgroup_id)         wgid: vec3<u32>,
  @builtin(global_invocation_id) gid:  vec3<u32>,
) {
  let idx = gid.x;

  // 1) Her thread bir değer üretsin, ham sonucu yazsın
  let val = 0.5 + 0.5 * sin(f32(idx) * 0.04 + u.time * 1.2);
  data_w[idx] = val;

  // 2) Workgroup-yerel reduce: 64 değer → 1 toplam, 6 paralel adımda.
  //    Barrier'lar uniform control flow'da olmalı — bu yüzden loop yerine unroll.
  wg_buf[lid.x] = val;
  workgroupBarrier();

  if lid.x < 32u { wg_buf[lid.x] = wg_buf[lid.x] + wg_buf[lid.x + 32u]; }
  workgroupBarrier();
  if lid.x < 16u { wg_buf[lid.x] = wg_buf[lid.x] + wg_buf[lid.x + 16u]; }
  workgroupBarrier();
  if lid.x <  8u { wg_buf[lid.x] = wg_buf[lid.x] + wg_buf[lid.x +  8u]; }
  workgroupBarrier();
  if lid.x <  4u { wg_buf[lid.x] = wg_buf[lid.x] + wg_buf[lid.x +  4u]; }
  workgroupBarrier();
  if lid.x <  2u { wg_buf[lid.x] = wg_buf[lid.x] + wg_buf[lid.x +  2u]; }
  workgroupBarrier();
  if lid.x <  1u { wg_buf[lid.x] = wg_buf[lid.x] + wg_buf[lid.x +  1u]; }
  workgroupBarrier();

  // 3) Thread 0 partial ortalamayı yazsın (offset N'den)
  if lid.x == 0u {
    data_w[N + wgid.x] = wg_buf[0] / f32(WG);
  }
}

@vertex
fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4<f32> {
  let p = vec2<f32>(f32((vi << 1u) & 2u), f32(vi & 2u));
  return vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
}

@fragment
fn fs(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
  let uv = frag.xy / u.resolution;

  let bg   = vec3<f32>(0.04, 0.07, 0.10);
  let fg   = vec3<f32>(0.13, 0.83, 0.93);
  let warm = vec3<f32>(0.96, 0.62, 0.04);

  // Üst yarı: 1024 ham veri (ince cyan barlar)
  if uv.y < 0.5 {
    let i = u32(uv.x * f32(N));
    let v = data_r[i];
    let yLocal = uv.y / 0.5;
    let on = step(1.0 - yLocal, v);
    return vec4<f32>(mix(bg, fg, on), 1.0);
  }

  // Alt yarı: 16 partial sum (kalın amber barlar)
  let wgIdx = u32(uv.x * 16.0);
  let v = data_r[N + wgIdx];
  let yLocal = (uv.y - 0.5) / 0.5;
  let on = step(1.0 - yLocal, v);
  return vec4<f32>(mix(bg, warm * 0.8, on), 1.0);
}