// =====================================================================
// cc_gpu_eval_from_topo.js -- CC eval WGSL that reads from the topo
// blob.
//
// This is the workhorse for Phase E of the CC -> PASS_B consolidation.
// It mirrors CC_GPU_PATCH_WGSL's per-vertex eval pipeline but pulls
// every CC table out of the `topo` storage binding at the byte offsets
// stored in header slots 67..86 (per the Phase B/C packing).
//
// Consumed by PASS_B_WGSL via `${CC_GPU_EVAL_FROM_TOPO_WGSL}`. The
// caller must:
//   1. Concatenate ${CC_GPU_BSPLINE_WGSL} FIRST (this file uses
//      evalBicubicBSplineSharpness_setup et al.).
//   2. Bind topology.gpuBlob as `topo : array<u32>`.
//   3. Call `evalCCFromTopo(triId, w0, w1, w2) -> EvalResult` per
//      vertex to obtain the CC limit-surface (P, N) plus a status code.
//
// Dawn bug workaround: returning the 64-B IrregularInfo struct from a
// function corrupts the compiled shader (bisected during Phase D).
// We never declare an IrregularInfo struct in this file; the irregular
// fields are read out of topo as inline u32/i32/f32 primitives at the
// one call site that needs them (`evalIrregularFromTopo`).
// =====================================================================
'use strict';

const CC_GPU_EVAL_FROM_TOPO_WGSL = /* wgsl */ `
// ---- struct PolyInfo (32 u32, 128 B per poly) ----
struct PolyInfoCC {
  polyType         : u32,
  subdivLevel      : u32,
  regionMask       : u32,
  regionDataStart  : u32,
  regionDataNum    : u32,
  pointsStart      : u32,
  triCenterIndex   : u32,
  _pad0            : u32,
  edgeSharp0       : f32,
  edgeSharp1       : f32,
  edgeSharp2       : f32,
  edgeSharp3       : f32,
  _pad1a           : u32, _pad1b : u32, _pad1c : u32, _pad1d : u32,
  tri0_mask : u32, tri0_dataStart : u32, tri0_dataNum : u32, tri0_pointsStart : u32,
  tri1_mask : u32, tri1_dataStart : u32, tri1_dataNum : u32, tri1_pointsStart : u32,
  tri2_mask : u32, tri2_dataStart : u32, tri2_dataNum : u32, tri2_pointsStart : u32,
  _pad2a : u32, _pad2b : u32, _pad2c : u32, _pad2d : u32,
};
struct TriPatchMapEntryCC {
  patchIter : u32,
  u0 : f32, v0 : f32,
  u1 : f32, v1 : f32,
  u2 : f32, v2 : f32,
  _pad : u32,
};
struct RegionInfoCC {
  irregularIndex       : i32,
  irregularPolyIndex   : u32,
  irregularSubdivLevel : u32,
  rotation             : u32,
  irregularRegionScale : u32,
  _pad0 : u32,
  _pad1 : u32,
  _pad2 : u32,
};
struct EvalResultCC {
  P      : vec3<f32>,
  status : u32, // 0=regular ok, 1=irregular ok, 2=oob, 3=tri-skip, 4=err, 5=triReg ok, 6=triIrreg ok
  N      : vec3<f32>,
  _pad   : u32,
};

// ---- topo header reads (constant offsets into the topo u32 blob) ----
// All header slots are u32, byte-aligned at slot * 4. To get an
// element-index into the u32 view: \`topo[N] >> 2u\`.

fn ccTopoVec3At(vec3Idx: u32) -> vec3<f32> {
  let b = (topo[79] >> 2u) + vec3Idx * 3u;
  return vec3<f32>(bitcast<f32>(topo[b + 0u]),
                   bitcast<f32>(topo[b + 1u]),
                   bitcast<f32>(topo[b + 2u]));
}

// Per Phase D's bisect, returning PolyInfo / TriPatchMapEntry /
// RegionInfo structs from accessor functions WORKS. Only IrregularInfo
// (64 B with mixed i32/u32 fields) triggers the Dawn bug. Use struct
// returns for the working ones; inline irregular reads at the call site.

fn ccTopoPolyInfoAt(patchIter: u32) -> PolyInfoCC {
  let b = (topo[67] >> 2u) + patchIter * 32u;
  var p : PolyInfoCC;
  p.polyType         = topo[b + 0u];
  p.subdivLevel      = topo[b + 1u];
  p.regionMask       = topo[b + 2u];
  p.regionDataStart  = topo[b + 3u];
  p.regionDataNum    = topo[b + 4u];
  p.pointsStart      = topo[b + 5u];
  p.triCenterIndex   = topo[b + 6u];
  p.edgeSharp0       = bitcast<f32>(topo[b + 8u]);
  p.edgeSharp1       = bitcast<f32>(topo[b + 9u]);
  p.edgeSharp2       = bitcast<f32>(topo[b + 10u]);
  p.edgeSharp3       = bitcast<f32>(topo[b + 11u]);
  p.tri0_mask        = topo[b + 16u];
  p.tri0_dataStart   = topo[b + 17u];
  p.tri0_dataNum     = topo[b + 18u];
  p.tri0_pointsStart = topo[b + 19u];
  p.tri1_mask        = topo[b + 20u];
  p.tri1_dataStart   = topo[b + 21u];
  p.tri1_dataNum     = topo[b + 22u];
  p.tri1_pointsStart = topo[b + 23u];
  p.tri2_mask        = topo[b + 24u];
  p.tri2_dataStart   = topo[b + 25u];
  p.tri2_dataNum     = topo[b + 26u];
  p.tri2_pointsStart = topo[b + 27u];
  return p;
}

fn ccTopoTriPatchAt(triId: u32) -> TriPatchMapEntryCC {
  let b = (topo[69] >> 2u) + triId * 8u;
  var t : TriPatchMapEntryCC;
  t.patchIter = topo[b + 0u];
  t.u0 = bitcast<f32>(topo[b + 1u]);
  t.v0 = bitcast<f32>(topo[b + 2u]);
  t.u1 = bitcast<f32>(topo[b + 3u]);
  t.v1 = bitcast<f32>(topo[b + 4u]);
  t.u2 = bitcast<f32>(topo[b + 5u]);
  t.v2 = bitcast<f32>(topo[b + 6u]);
  return t;
}

fn ccTopoRegionAt(idx: i32) -> RegionInfoCC {
  let b = (topo[73] >> 2u) + u32(idx) * 8u;
  var r : RegionInfoCC;
  r.irregularIndex       = bitcast<i32>(topo[b + 0u]);
  r.irregularPolyIndex   = topo[b + 1u];
  r.irregularSubdivLevel = topo[b + 2u];
  r.rotation             = topo[b + 3u];
  r.irregularRegionScale = topo[b + 4u];
  return r;
}

fn ccTopoTriPatchCount() -> u32 {
  return topo[70];
}

fn ccTopoCpRedirect(i: i32) -> i32 {
  return bitcast<i32>(topo[(topo[71] >> 2u) + u32(i)]);
}

fn ccTopoIrregEdgeShAt(i: u32) -> f32 {
  return bitcast<f32>(topo[(topo[77] >> 2u) + i]);
}

// ---- popcount + region mask -> offset ----

fn ccTopoPopcount32(x: u32) -> u32 {
  var n = x;
  n = n - ((n >> 1u) & 0x55555555u);
  n = (n & 0x33333333u) + ((n >> 2u) & 0x33333333u);
  n = (n + (n >> 4u)) & 0x0f0f0f0fu;
  return (n * 0x01010101u) >> 24u;
}
fn ccTopoRegionGetOffset(mask: u32, regionBaseIndex: u32) -> i32 {
  let bit = 1u << regionBaseIndex;
  if ((mask & bit) == 0u) { return -1; }
  return i32(ccTopoPopcount32(mask & (bit - 1u)));
}

// ---- Regular bicubic sub-region eval ----

fn ccTopoEvalRegular(pointsStart: u32, edgeSharp: vec4<f32>,
                     regionC: i32, regionR: i32,
                     regionDimI: i32, regionUv: vec2<f32>) -> array<vec3<f32>, 2> {
  let isRegionL = (regionC == 0);
  let isRegionR = (regionC == regionDimI - 1);
  let isRegionD = (regionR == 0);
  let isRegionU = (regionR == regionDimI - 1);

  var rotation : i32 = 0;
  var numAdded : i32 = 0;
  var sharpnessSides : array<f32, 4> = array<f32, 4>(0.0, 0.0, 0.0, 0.0);
  if (isRegionL && edgeSharp.x > 0.0) { rotation = 0; numAdded = numAdded + 1; sharpnessSides[0] = edgeSharp.x; }
  if (isRegionU && edgeSharp.y > 0.0) { rotation = 1; numAdded = numAdded + 1; sharpnessSides[1] = edgeSharp.y; }
  if (isRegionR && edgeSharp.z > 0.0) { rotation = 2; numAdded = numAdded + 1; sharpnessSides[2] = edgeSharp.z; }
  if (isRegionD && edgeSharp.w > 0.0) { rotation = 3; numAdded = numAdded + 1; sharpnessSides[3] = edgeSharp.w; }

  var ptStart : vec2<i32>;
  var ptDirX  : vec2<i32>;
  var ptDirY  : vec2<i32>;
  var localUv : vec2<f32>;
  if (rotation == 0) {
    ptStart = vec2<i32>(0, 0); ptDirX = vec2<i32>( 1,  0); ptDirY = vec2<i32>( 0,  1);
    localUv = vec2<f32>(regionUv.x,         regionUv.y);
  } else if (rotation == 1) {
    ptStart = vec2<i32>(0, 3); ptDirX = vec2<i32>( 0, -1); ptDirY = vec2<i32>( 1,  0);
    localUv = vec2<f32>(1.0 - regionUv.y,   regionUv.x);
  } else if (rotation == 2) {
    ptStart = vec2<i32>(3, 3); ptDirX = vec2<i32>(-1,  0); ptDirY = vec2<i32>( 0, -1);
    localUv = vec2<f32>(1.0 - regionUv.x,   1.0 - regionUv.y);
  } else {
    ptStart = vec2<i32>(3, 0); ptDirX = vec2<i32>( 0,  1); ptDirY = vec2<i32>(-1,  0);
    localUv = vec2<f32>(regionUv.y,         1.0 - regionUv.x);
  }

  var sharpness : f32 = 0.0;
  var multiSharpMode : i32 = 0;
  var multiSharpY : f32 = 0.0;
  var multiSharpReverseY : bool = false;
  var multiSharpReverseX : bool = true;
  var oppositeAxis : i32 = 0;
  var oppositeSharpA : f32 = 0.0;
  var oppositeSharpB : f32 = 0.0;
  let sL = sharpnessSides[0];
  let sU = sharpnessSides[1];
  let sR = sharpnessSides[2];
  let sD = sharpnessSides[3];
  if (numAdded == 1) {
    sharpness = sharpnessSides[rotation];
  } else if (numAdded == 2) {
    rotation = 0;
    ptStart   = vec2<i32>(0, 0);
    ptDirX    = vec2<i32>(1, 0);
    ptDirY    = vec2<i32>(0, 1);
    localUv   = vec2<f32>(regionUv.x, regionUv.y);
    if (sL > 0.0 && sR > 0.0) {
      multiSharpMode = 2; oppositeAxis = 0;
      oppositeSharpA = sL; oppositeSharpB = sR;
    } else if (sU > 0.0 && sD > 0.0) {
      multiSharpMode = 2; oppositeAxis = 1;
      oppositeSharpA = sU; oppositeSharpB = sD;
    } else {
      multiSharpMode = 1;
      let sx = select(sR, sL, sL > 0.0);
      multiSharpReverseX = (sL > 0.0);
      let sy = select(sD, sU, sU > 0.0);
      multiSharpReverseY = (sD > 0.0);
      sharpness = sx;
      multiSharpY = sy;
    }
  } else {
    multiSharpMode = 3;
    rotation = 0;
    ptStart   = vec2<i32>(0, 0);
    ptDirX    = vec2<i32>(1, 0);
    ptDirY    = vec2<i32>(0, 1);
    localUv   = vec2<f32>(regionUv.x, regionUv.y);
  }

  let start = i32(pointsStart);
  let rowStride = regionDimI + 3;
  var cpVtx : array<vec3<f32>, 16>;
  let cpDataOffV3 = topo[81];
  for (var r: i32 = 0; r < 4; r = r + 1) {
    for (var c: i32 = 0; c < 4; c = c + 1) {
      let offX = ptStart.x + c * ptDirX.x + r * ptDirY.x;
      let offY = ptStart.y + c * ptDirX.y + r * ptDirY.y;
      let srcC = regionC + offX;
      let srcR = regionR + offY;
      let localOffset = srcR * rowStride + srcC;
      let cpIndex = u32(ccTopoCpRedirect(start + localOffset));
      cpVtx[r * 4 + c] = ccTopoVec3At(cpDataOffV3 + cpIndex);
    }
  }

  if (multiSharpMode == 2) {
    var basisA: array<vec4<f32>, 4>;
    var basisB: array<vec4<f32>, 4>;
    if (oppositeAxis == 0) {
      basisA = evalBicubicBSplineSharpness_setup_full(oppositeSharpA, true,  0.0, false, localUv.x, localUv.y);
      basisB = evalBicubicBSplineSharpness_setup_full(oppositeSharpB, false, 0.0, false, localUv.x, localUv.y);
    } else {
      basisA = evalBicubicBSplineSharpness_setup_full(0.0, true, oppositeSharpA, false, localUv.x, localUv.y);
      basisB = evalBicubicBSplineSharpness_setup_full(0.0, true, oppositeSharpB, true,  localUv.x, localUv.y);
    }
    var sumPa = vec3<f32>(0.0);
    var sumDxa = vec3<f32>(0.0);
    var sumDya = vec3<f32>(0.0);
    var sumPb = vec3<f32>(0.0);
    for (var r: u32 = 0u; r < 4u; r = r + 1u) {
      for (var c: u32 = 0u; c < 4u; c = c + 1u) {
        let P = cpVtx[r * 4u + c];
        sumPa  = sumPa  + P * (basisA[0][c] * basisA[2][r]);
        sumDxa = sumDxa + P * (basisA[1][c] * basisA[2][r]);
        sumDya = sumDya + P * (basisA[0][c] * basisA[3][r]);
        sumPb  = sumPb  + P * (basisB[0][c] * basisB[2][r]);
      }
    }
    let avgP = (sumPa + sumPb) * 0.5;
    let nRaw = cross(sumDxa, sumDya);
    let nLen = length(nRaw);
    let N = select(vec3<f32>(0.0), nRaw / nLen, nLen > 0.0);
    return array<vec3<f32>, 2>(avgP, N);
  }

  var basis : array<vec4<f32>, 4>;
  if (multiSharpMode == 1) {
    basis = evalBicubicBSplineSharpness_setup_full(
      sharpness, multiSharpReverseX,
      multiSharpY, multiSharpReverseY,
      localUv.x, localUv.y);
  } else {
    basis = evalBicubicBSplineSharpness_setup(sharpness, localUv.x, localUv.y);
  }
  let basisX  = basis[0];
  let basisDx = basis[1];
  let basisY  = basis[2];
  let basisDy = basis[3];

  var sumP  = vec3<f32>(0.0);
  var sumDx = vec3<f32>(0.0);
  var sumDy = vec3<f32>(0.0);
  for (var r: u32 = 0u; r < 4u; r = r + 1u) {
    let wY  = basisY[r];
    let wDy = basisDy[r];
    for (var c: u32 = 0u; c < 4u; c = c + 1u) {
      let wX  = basisX[c];
      let wDx = basisDx[c];
      let P   = cpVtx[r * 4u + c];
      sumP  = sumP  + P * (wX  * wY);
      sumDx = sumDx + P * (wDx * wY);
      sumDy = sumDy + P * (wX  * wDy);
    }
  }

  var nRaw = cross(sumDx, sumDy);
  if (multiSharpMode == 1 && (multiSharpReverseX != multiSharpReverseY)) {
    nRaw = -nRaw;
  }
  let nLen = length(nRaw);
  let N    = select(vec3<f32>(0.0), nRaw / nLen, nLen > 0.0);
  return array<vec3<f32>, 2>(sumP, N);
}

// ---- Irregular helpers ----

struct CcTopoIrregOffsets {
  indexPts : array<i32, 16>,
  foundSharpness : f32,
};

const CC_TOPO_SUBDIV_MAX_SHARPNESS : f32 = 10.0;

fn ccTopoGetIrregularRegionPts(targetRegion: u32, currCorner: u32,
                                valence: u32, isBorder: u32,
                                sliceStartOffset: i32,
                                edgeSharpnessStart: i32) -> CcTopoIrregOffsets {
  let numThreadsForWedge = select(valence, valence + 1u, isBorder != 0u);
  let prevCorner = (currCorner + (numThreadsForWedge - 1u)) % numThreadsForWedge;
  let nextCorner = (currCorner + 1u) % numThreadsForWedge;
  let nextNextCorner = (currCorner + 2u) % numThreadsForWedge;

  var foundSharpness : f32 = 0.0;
  let isPrevBorder = (isBorder != 0u && currCorner == 0u);
  let isNextBorder = (isBorder != 0u && currCorner == valence - 1u);

  var out : CcTopoIrregOffsets;

  if (targetRegion == 3u) {
    foundSharpness = 0.0;
    out.indexPts[ 0] = sliceStartOffset + 0;
    out.indexPts[ 1] = sliceStartOffset + (i32(1u + nextCorner * 12u + 0u));
    out.indexPts[ 2] = sliceStartOffset + select(i32(1u + nextCorner * 12u + 4u),
                                                  i32(1u + nextCorner * 12u + 1u),
                                                  isNextBorder);
    out.indexPts[ 3] = sliceStartOffset + select(i32(1u + nextCorner * 12u + 8u),
                                                  i32(1u + nextCorner * 12u + 2u),
                                                  isNextBorder);
    out.indexPts[ 4] = sliceStartOffset + i32(1u + currCorner * 12u + 0u);
    out.indexPts[ 5] = sliceStartOffset + i32(1u + currCorner * 12u + 1u);
    out.indexPts[ 6] = sliceStartOffset + i32(1u + currCorner * 12u + 2u);
    out.indexPts[ 7] = sliceStartOffset + i32(1u + currCorner * 12u + 3u);
    out.indexPts[ 8] = sliceStartOffset + i32(1u + currCorner * 12u + 4u);
    out.indexPts[ 9] = sliceStartOffset + i32(1u + currCorner * 12u + 5u);
    out.indexPts[10] = sliceStartOffset + i32(1u + currCorner * 12u + 6u);
    out.indexPts[11] = sliceStartOffset + i32(1u + currCorner * 12u + 7u);
    out.indexPts[12] = sliceStartOffset + i32(1u + currCorner * 12u + 8u);
    out.indexPts[13] = sliceStartOffset + i32(1u + currCorner * 12u + 9u);
    out.indexPts[14] = sliceStartOffset + i32(1u + currCorner * 12u + 10u);
    out.indexPts[15] = sliceStartOffset + i32(1u + currCorner * 12u + 11u);
  } else if (targetRegion == 2u) {
    if (edgeSharpnessStart >= 0) {
      foundSharpness = ccTopoIrregEdgeShAt(u32(edgeSharpnessStart) + currCorner);
    }
    if (isPrevBorder) { foundSharpness = CC_TOPO_SUBDIV_MAX_SHARPNESS; }

    out.indexPts[ 0] = sliceStartOffset + select(i32(1u + prevCorner * 12u + 0u), 0, isPrevBorder);
    out.indexPts[ 1] = sliceStartOffset + 0;
    out.indexPts[ 2] = sliceStartOffset + i32(1u + nextCorner * 12u + 0u);
    out.indexPts[ 3] = sliceStartOffset + select(i32(1u + nextCorner * 12u + 4u),
                                                  i32(1u + nextCorner * 12u + 1u),
                                                  isNextBorder);

    out.indexPts[ 4] = sliceStartOffset + select(i32(1u + prevCorner * 12u + 1u), 0, isPrevBorder);
    out.indexPts[ 5] = sliceStartOffset + i32(1u + currCorner * 12u + 0u);
    out.indexPts[ 6] = sliceStartOffset + i32(1u + currCorner * 12u + 1u);
    out.indexPts[ 7] = sliceStartOffset + i32(1u + currCorner * 12u + 2u);

    out.indexPts[ 8] = sliceStartOffset + select(i32(1u + prevCorner * 12u + 2u), 0, isPrevBorder);
    out.indexPts[ 9] = sliceStartOffset + i32(1u + currCorner * 12u + 4u);
    out.indexPts[10] = sliceStartOffset + i32(1u + currCorner * 12u + 5u);
    out.indexPts[11] = sliceStartOffset + i32(1u + currCorner * 12u + 6u);

    out.indexPts[12] = sliceStartOffset + select(i32(1u + prevCorner * 12u + 3u), 0, isPrevBorder);
    out.indexPts[13] = sliceStartOffset + i32(1u + currCorner * 12u + 8u);
    out.indexPts[14] = sliceStartOffset + i32(1u + currCorner * 12u + 9u);
    out.indexPts[15] = sliceStartOffset + i32(1u + currCorner * 12u + 10u);
  } else if (targetRegion == 1u) {
    if (edgeSharpnessStart >= 0) {
      foundSharpness = ccTopoIrregEdgeShAt(u32(edgeSharpnessStart) + nextCorner);
    }
    if (isNextBorder) { foundSharpness = CC_TOPO_SUBDIV_MAX_SHARPNESS; }

    out.indexPts[ 0] = sliceStartOffset + select(i32(1u + nextCorner * 12u + 9u), 0, isNextBorder);
    out.indexPts[ 1] = sliceStartOffset + select(i32(1u + nextCorner * 12u + 8u),
                                                  i32(1u + nextCorner * 12u + 2u),
                                                  isNextBorder);
    out.indexPts[ 2] = sliceStartOffset + i32(1u + currCorner * 12u + 3u);
    out.indexPts[ 3] = sliceStartOffset + i32(1u + currCorner * 12u + 7u);

    out.indexPts[ 4] = sliceStartOffset + select(i32(1u + nextCorner * 12u + 5u), 0, isNextBorder);
    out.indexPts[ 5] = sliceStartOffset + select(i32(1u + nextCorner * 12u + 4u),
                                                  i32(1u + nextCorner * 12u + 1u),
                                                  isNextBorder);
    out.indexPts[ 6] = sliceStartOffset + i32(1u + currCorner * 12u + 2u);
    out.indexPts[ 7] = sliceStartOffset + i32(1u + currCorner * 12u + 6u);

    out.indexPts[ 8] = sliceStartOffset + select(i32(1u + nextCorner * 12u + 1u), 0, isNextBorder);
    out.indexPts[ 9] = sliceStartOffset + select(i32(1u + nextCorner * 12u + 0u),
                                                  i32(1u + nextCorner * 12u + 0u),
                                                  isNextBorder);
    out.indexPts[10] = sliceStartOffset + i32(1u + currCorner * 12u + 1u);
    out.indexPts[11] = sliceStartOffset + i32(1u + currCorner * 12u + 5u);

    out.indexPts[12] = sliceStartOffset + select(i32(1u + nextNextCorner * 12u + 0u), 0, isNextBorder);
    out.indexPts[13] = sliceStartOffset + 0;
    out.indexPts[14] = sliceStartOffset + i32(1u + currCorner * 12u + 0u);
    out.indexPts[15] = sliceStartOffset + i32(1u + currCorner * 12u + 4u);
  } else {
    for (var k: i32 = 0; k < 16; k = k + 1) { out.indexPts[k] = 0; }
  }
  out.foundSharpness = foundSharpness;
  return out;
}

fn ccTopoCalcNumDstPointsPerSliceForWedge(valence: u32, hasBorder: u32) -> u32 {
  var n = 1u + valence * 12u;
  if (hasBorder != 0u) { n = n + 3u; }
  return n;
}

fn ccTopoEvaluateIrregularPointFull(limitP: vec3<f32>, limitN: vec3<f32>,
                                     wedgeStartV3: u32,
                                     edgeSharpnessStart: i32,
                                     valence: u32, isBorder: u32, patchSliceLevel: f32,
                                     currCorner: u32, uv: vec2<f32>) -> array<vec3<f32>, 2> {
  // Fractional-level threshold: slides minT continuously across the
  // last computed slice as patchSliceLevel sweeps from (numSlices-1)
  // up to numSlices. See the matching CPU comment in cc_patcheval.js.
  let numSlices = max(1.0, ceil(patchSliceLevel));
  let innerFrac = clamp(numSlices - patchSliceLevel, 0.0, 1.0);
  let minT = (1.0 + innerFrac) * pow(2.0, -numSlices);

  let isUvXLessThanY = uv.x < uv.y;
  let uvT   = max(uv.x, uv.y);
  let evalT = max(uvT, minT);

  let ratioT  = select(0.0, uvT / evalT, evalT != 0.0);
  let uvScale = select(0.0, 1.0 / ratioT, ratioT != 0.0);

  let uvEval = vec2<f32>(uvScale * uv.x, uvScale * uv.y);
  let levelBaseT = max(uvEval.x, uvEval.y);
  let levelT = -log2(max(1e-5, levelBaseT));

  let sliceLevelForGridCalc = min(i32(numSlices) - 1, i32(floor(levelT)));
  let halfGridSize = 1.0 / f32(1 << u32(sliceLevelForGridCalc + 1));

  var regionIndex : u32;
  if (isUvXLessThanY) {
    regionIndex = select(3u, 2u, uvEval.x < halfGridSize);
  } else {
    regionIndex = select(3u, 1u, uvEval.y < halfGridSize);
  }

  let regionStartUvX = f32(regionIndex % 2u);
  let regionStartUvY = f32(regionIndex / 2u);
  let regionUvX = (uvEval.x - regionStartUvX * halfGridSize) / halfGridSize;
  let regionUvY = (uvEval.y - regionStartUvY * halfGridSize) / halfGridSize;

  var regionUv : vec2<f32>;
  if (regionIndex == 1u) {
    regionUv = vec2<f32>(clamp(regionUvY, 0.0, 1.0), clamp(1.0 - regionUvX, 0.0, 1.0));
  } else {
    regionUv = vec2<f32>(clamp(regionUvX, 0.0, 1.0), clamp(regionUvY, 0.0, 1.0));
  }

  let numPtsPerSlice = ccTopoCalcNumDstPointsPerSliceForWedge(valence, isBorder);
  let sliceStartOffset = i32(u32(sliceLevelForGridCalc) * numPtsPerSlice);

  let ro = ccTopoGetIrregularRegionPts(regionIndex, currCorner,
                                        valence, isBorder,
                                        sliceStartOffset, edgeSharpnessStart);

  var foundSharpness = ro.foundSharpness;
  if (foundSharpness < CC_TOPO_SUBDIV_MAX_SHARPNESS) {
    foundSharpness = max(foundSharpness - f32(sliceLevelForGridCalc), 0.0);
  }
  foundSharpness = max(0.0, foundSharpness - 1.0);

  let basis = evalBicubicBSplineSharpness_setup(foundSharpness, regionUv.x, regionUv.y);
  let basisX  = basis[0];
  let basisDx = basis[1];
  let basisY  = basis[2];
  let basisDy = basis[3];

  var sumP  = vec3<f32>(0.0);
  var sumDx = vec3<f32>(0.0);
  var sumDy = vec3<f32>(0.0);
  for (var r: u32 = 0u; r < 4u; r = r + 1u) {
    let wY  = basisY[r];
    let wDy = basisDy[r];
    for (var c: u32 = 0u; c < 4u; c = c + 1u) {
      let wX  = basisX[c];
      let wDx = basisDx[c];
      let ptIdx = u32(ro.indexPts[r * 4u + c]);
      let P = ccTopoVec3At(wedgeStartV3 + ptIdx);
      sumP  = sumP  + P * (wX  * wY);
      sumDx = sumDx + P * (wDx * wY);
      sumDy = sumDy + P * (wX  * wDy);
    }
  }

  let nRaw = cross(sumDx, sumDy);
  let nLen = length(nRaw);
  let evalN_unit = select(vec3<f32>(0.0), nRaw / nLen, nLen > 0.0);

  let foundP = mix(limitP, sumP, ratioT);
  let foundN_raw = mix(limitN, evalN_unit, ratioT);
  let foundNLen = length(foundN_raw);
  let foundN = select(foundN_raw, foundN_raw / foundNLen, foundNLen > 0.0);

  return array<vec3<f32>, 2>(foundP, foundN);
}

// Irregular branch. RegionInfo returned as struct (32 B, safe).
// IrregularInfo fields read INLINE (Dawn bug workaround).
fn ccTopoEvalIrregular(regionDataStart: u32, subdivLevel: u32, redirect: i32,
                       regionC: i32, regionR: i32, regionDimI: i32,
                       regionUv: vec2<f32>) -> array<vec3<f32>, 3> {
  var halfGridShift : i32;
  if (subdivLevel == 1u) { halfGridShift = 0; }
  else if (subdivLevel == 2u) { halfGridShift = 1; }
  else {
    return array<vec3<f32>, 3>(vec3<f32>(0.0), vec3<f32>(0.0), vec3<f32>(1.0, 0.0, 0.0));
  }
  let halfGridIndex = (regionR >> u32(halfGridShift)) * 2 + (regionC >> u32(halfGridShift));

  var rotatedUv : vec2<f32>;
  if (halfGridIndex == 0)      { rotatedUv = vec2<f32>(regionUv.x,         regionUv.y); }
  else if (halfGridIndex == 1) { rotatedUv = vec2<f32>(regionUv.y,         1.0 - regionUv.x); }
  else if (halfGridIndex == 2) { rotatedUv = vec2<f32>(1.0 - regionUv.y,   regionUv.x); }
  else                         { rotatedUv = vec2<f32>(1.0 - regionUv.x, 1.0 - regionUv.y); }

  let actualRegion = ccTopoRegionAt(i32(regionDataStart) + redirect);
  let irregIdx = actualRegion.irregularIndex;
  if (irregIdx < 0) {
    return array<vec3<f32>, 3>(vec3<f32>(0.0), vec3<f32>(0.0), vec3<f32>(2.0, 0.0, 0.0));
  }

  // INLINE IrregularInfo read (Dawn struct-return bug workaround).
  let irregBase = (topo[75] >> 2u) + u32(irregIdx) * 16u;
  let info_centerVertex              = topo[irregBase + 0u];
  let info_centerTrianglePoly        = bitcast<i32>(topo[irregBase + 1u]);
  let info_centerTriangleLimitOffset = bitcast<i32>(topo[irregBase + 2u]);
  let info_valence                   = topo[irregBase + 3u];
  let info_isBorder                  = topo[irregBase + 4u];
  let info_edgeSharpnessStart        = bitcast<i32>(topo[irregBase + 5u]);
  let info_wedgeStartPoint           = topo[irregBase + 6u];
  let info_irregularLevel            = bitcast<f32>(topo[irregBase + 8u]);

  var srcLimitIndex : u32;
  if (info_centerTrianglePoly >= 0) {
    let baseOffset = topo[85] + topo[86];   // numRegularPoints + numIrregularVertexWedges
    srcLimitIndex = baseOffset + u32(info_centerTriangleLimitOffset);
  } else {
    srcLimitIndex = info_centerVertex;
  }

  let limitP = ccTopoVec3At(topo[83] + srcLimitIndex);
  let limitN = ccTopoVec3At(topo[84] + srcLimitIndex);

  var uvOffset : vec2<f32>;
  var uvScale  : vec2<f32>;
  let regionScale = actualRegion.irregularRegionScale;
  if (regionScale < 4u) {
    let x = f32(regionScale % 2u);
    let y = f32(regionScale / 2u);
    uvOffset = vec2<f32>(x / 2.0, y / 2.0);
    uvScale  = vec2<f32>(0.5, 0.5);
  } else {
    uvOffset = vec2<f32>(0.0, 0.0);
    uvScale  = vec2<f32>(1.0, 1.0);
  }

  let uv = vec2<f32>(uvOffset.x + uvScale.x * rotatedUv.x,
                     uvOffset.y + uvScale.y * rotatedUv.y);

  let regionCorner = actualRegion.irregularPolyIndex;
  let wedgeStartV3 = topo[82] + info_wedgeStartPoint; // fullWedgeOffsetV3 + ...

  let r = ccTopoEvaluateIrregularPointFull(limitP, limitN,
                                            wedgeStartV3,
                                            info_edgeSharpnessStart,
                                            info_valence, info_isBorder,
                                            info_irregularLevel,
                                            regionCorner, uv);
  return array<vec3<f32>, 3>(r[0], r[1], vec3<f32>(0.0));
}

// ---- Tri helpers (for triangle source polys) ----

fn ccTopoSubQuadBaryFromTriBary(triBary: vec2<f32>) -> vec2<f32> {
  let a = -1.0;
  let b = 3.0 - 2.0 * triBary.x + 2.0 * triBary.y;
  let c = -6.0 * triBary.y;
  let disc = b * b - 4.0 * a * c;
  let quadratic = sqrt(max(disc, 0.0));
  let y0 = (-b + quadratic) / (2.0 * a);
  let denom = 0.5 - (1.0 / 6.0) * y0;
  let x0 = select(0.0, triBary.x / denom, denom != 0.0);
  return vec2<f32>(x0, y0);
}

fn ccTopoRotateTriBaryForCorner(baseCorner: u32, triBary: vec2<f32>) -> vec2<f32> {
  if (baseCorner == 0u) { return triBary; }
  if (baseCorner == 1u) { return vec2<f32>(1.0 - (triBary.x + triBary.y), triBary.x); }
  return vec2<f32>(triBary.y, 1.0 - (triBary.x + triBary.y));
}

struct CcTopoTriSubQuadResult {
  foundCorner : u32,
  subQuadBary : vec2<f32>,
};

fn ccTopoSubQuadAndUvFromTriBary(triBary: vec2<f32>) -> CcTopoTriSubQuadResult {
  let underS0 = triBary.y < 1.0 - 2.0 * triBary.x;
  let underS1 = triBary.y < 0.5 - 0.5 * triBary.x;
  let underS2 = triBary.y < triBary.x;
  var baseCorner : u32 = 0u;
  if (!underS2 && !underS1) { baseCorner = 1u; }
  if (underS2 && !underS0)  { baseCorner = 2u; }
  let rotateBary = ccTopoRotateTriBaryForCorner(baseCorner, triBary);
  let subQuad = ccTopoSubQuadBaryFromTriBary(rotateBary);
  var out : CcTopoTriSubQuadResult;
  out.foundCorner = baseCorner;
  out.subQuadBary = subQuad;
  return out;
}

fn ccTopoPickTriGridMask(pi: PolyInfoCC, corner: u32) -> u32 {
  if (corner == 0u) { return pi.tri0_mask; }
  if (corner == 1u) { return pi.tri1_mask; }
  return pi.tri2_mask;
}
fn ccTopoPickTriGridDataStart(pi: PolyInfoCC, corner: u32) -> u32 {
  if (corner == 0u) { return pi.tri0_dataStart; }
  if (corner == 1u) { return pi.tri1_dataStart; }
  return pi.tri2_dataStart;
}
fn ccTopoPickTriGridPointsStart(pi: PolyInfoCC, corner: u32) -> u32 {
  if (corner == 0u) { return pi.tri0_pointsStart; }
  if (corner == 1u) { return pi.tri1_pointsStart; }
  return pi.tri2_pointsStart;
}
fn ccTopoPickTriEdgeSharpness(pi: PolyInfoCC, idx: u32) -> f32 {
  if (idx == 0u) { return pi.edgeSharp0; }
  if (idx == 1u) { return pi.edgeSharp1; }
  return pi.edgeSharp2;
}

// =====================================================================
// Top-level CC eval entry point. Caller passes (triId, w0, w1, w2)
// where the weights are tri-bary (sum to 1.0). Returns (P, N, status).
// =====================================================================
fn evalCCFromTopo(triId: u32, w0: f32, w1: f32, w2: f32) -> EvalResultCC {
  var out : EvalResultCC;
  out.P = vec3<f32>(0.0);
  out.N = vec3<f32>(0.0);
  out.status = 2u; // default oob

  if (triId >= ccTopoTriPatchCount()) { return out; }

  let tpm = ccTopoTriPatchAt(triId);
  let u = w0 * tpm.u0 + w1 * tpm.u1 + w2 * tpm.u2;
  let v = w0 * tpm.v0 + w1 * tpm.v1 + w2 * tpm.v2;

  let pi = ccTopoPolyInfoAt(tpm.patchIter);

  var regionMask     : u32;
  var regionDataStart: u32;
  var pointsStart    : u32;
  var edgeSharp      : vec4<f32>;
  var subdivLevel    : u32;
  var patchUv        : vec2<f32>;

  if (pi.polyType == 3u) {
    let triR = ccTopoSubQuadAndUvFromTriBary(vec2<f32>(u, v));
    regionMask      = ccTopoPickTriGridMask(pi, triR.foundCorner);
    regionDataStart = ccTopoPickTriGridDataStart(pi, triR.foundCorner);
    pointsStart     = ccTopoPickTriGridPointsStart(pi, triR.foundCorner);
    let s0 = ccTopoPickTriEdgeSharpness(pi, triR.foundCorner);
    let s3 = ccTopoPickTriEdgeSharpness(pi, (triR.foundCorner + 2u) % 3u);
    edgeSharp = vec4<f32>(s0, 0.0, 0.0, s3);
    subdivLevel = 1u;
    patchUv     = triR.subQuadBary;
  } else {
    regionMask      = pi.regionMask;
    regionDataStart = pi.regionDataStart;
    pointsStart     = pi.pointsStart;
    edgeSharp       = vec4<f32>(pi.edgeSharp0, pi.edgeSharp1, pi.edgeSharp2, pi.edgeSharp3);
    subdivLevel     = pi.subdivLevel;
    patchUv         = vec2<f32>(u, v);
  }

  let regionDim   = 1u << subdivLevel;
  let regionDimI  = i32(regionDim);

  let baseUvX = patchUv.x * f32(regionDim);
  let baseUvY = patchUv.y * f32(regionDim);
  var regionC = clamp(i32(floor(baseUvX)), 0, regionDimI - 1);
  var regionR = clamp(i32(floor(baseUvY)), 0, regionDimI - 1);
  let regionUv = vec2<f32>(baseUvX - f32(regionC), baseUvY - f32(regionR));

  let regionBaseIndex = u32(regionR * regionDimI + regionC);
  let redirect = ccTopoRegionGetOffset(regionMask, regionBaseIndex);

  if (redirect < 0) {
    let r = ccTopoEvalRegular(pointsStart, edgeSharp, regionC, regionR, regionDimI, regionUv);
    out.P = r[0];
    out.N = r[1];
    out.status = select(0u, 5u, pi.polyType == 3u);
  } else {
    let r = ccTopoEvalIrregular(regionDataStart, subdivLevel, redirect,
                                 regionC, regionR, regionDimI, regionUv);
    if (r[2].x > 0.5) {
      out.status = 4u; // err
    } else {
      out.P = r[0];
      out.N = r[1];
      out.status = select(1u, 6u, pi.polyType == 3u);
    }
  }
  return out;
}
`;

if (typeof module !== 'undefined' && module.exports) {
  module.exports = { CC_GPU_EVAL_FROM_TOPO_WGSL };
}
