// =====================================================================
// JS port of amolite's CC topology helpers. The port follows
// src/PipeLib/AmolitePipe/DcciSubdiv.{h,cpp} line by line so a reader
// can place the C++ and JS side by side and confirm the algorithm
// matches. No JS-native shortcuts (half-edge structures, idiomatic
// rewrites, etc.) -- if amolite walks a loop twice, the JS port walks
// it twice; if amolite stores data in a flat array, the JS port stores
// it in a flat (Typed) array.
//
// Identifier names match the C++ source exactly (including the m_
// prefix). Struct factories use plain objects with those same field
// names, NOT JS-idiomatic camelCase substitutes.
//
// See PHASE_1B_PLAN.md for the porting roadmap. Each function is
// gated by an extension to cc_dump emitting the equivalent amolite
// output for byte-equal comparison.
// =====================================================================

// ---- PolyCorner (DcciSubdiv.h:41) ---------------------------------
// A reference to a single corner of a polygon: {polyIndex, polyCorner}.
function makePolyCorner(polyIndex, polyCorner) {
  if (polyIndex === undefined)  polyIndex = -1;
  if (polyCorner === undefined) polyCorner = -1;
  return { m_polyIndex: polyIndex, m_polyCorner: polyCorner };
}
// Port of PolyCorner::IsValid (DcciSubdiv.h:57).
function polyCornerIsValid(pc) {
  return pc.m_polyIndex >= 0 && pc.m_polyCorner >= 0;
}
// Port of PolyCorner::IsSame (DcciSubdiv.h:62).
function polyCornerIsSame(lhs, rhs) {
  return lhs.m_polyIndex === rhs.m_polyIndex && lhs.m_polyCorner === rhs.m_polyCorner;
}

// ---- EdgePair (DcciSubdiv.h:8) ------------------------------------
// Canonical (min, max) vertex pair. Used as the hash-map key in
// EdgeLookup; we expose the canonical-order helper and a string
// key (since JS Map can't use struct keys cleanly).
function edgePairKey(lhs, rhs) {
  const lo = lhs < rhs ? lhs : rhs;
  const hi = lhs < rhs ? rhs : lhs;
  return lo + ',' + hi;
}
function edgePairLo(lhs, rhs) { return lhs < rhs ? lhs : rhs; }
function edgePairHi(lhs, rhs) { return lhs < rhs ? rhs : lhs; }

// ---- EdgeLookup (DcciSubdiv.h:78) ---------------------------------
function makeEdgeLookup() {
  return {
    m_numEdges:               0,
    m_hashMap:                new Map(),  // key string -> edgeId int
    m_vertLhs:                [],   // amolite::vector<int>
    m_vertRhs:                [],
    m_edgeNumFaces:           [],
    m_edgeStartFaces:         [],
    m_edgeCreaseSharpness:    [],
    m_edgeIsHardNormal:       [],  // stored as ints (0/1), matching amolite's m_edgeIsHardNormal int vector
    m_edgeIsHardTangent:      [],
    m_edgePolyData:           [],  // PolyCorner[]
  };
}
function edgeLookupReset(el) {
  el.m_numEdges = 0;
  el.m_hashMap.clear();
  el.m_vertLhs.length             = 0;
  el.m_vertRhs.length             = 0;
  el.m_edgeNumFaces.length        = 0;
  el.m_edgeStartFaces.length      = 0;
  el.m_edgeCreaseSharpness.length = 0;
  el.m_edgeIsHardNormal.length    = 0;
  el.m_edgeIsHardTangent.length   = 0;
  el.m_edgePolyData.length        = 0;
}
function edgeLookupFetchEdge(el, lhs, rhs) {
  const key = edgePairKey(lhs, rhs);
  const v = el.m_hashMap.get(key);
  return (v === undefined) ? -1 : v;
}
// Port of EdgeLookup::InsertEdge (DcciSubdiv.h:134).
function edgeLookupInsertEdge(el, lhs, rhs) {
  let val = edgeLookupFetchEdge(el, lhs, rhs);
  if (val < 0) {
    // insert
    const lo = edgePairLo(lhs, rhs);
    const hi = edgePairHi(lhs, rhs);
    val = el.m_numEdges;
    el.m_hashMap.set(edgePairKey(lhs, rhs), val);
    el.m_numEdges++;
    el.m_edgeNumFaces.push(1);
    // note: pair reorders so lhs < rhs
    el.m_vertLhs.push(lo);
    el.m_vertRhs.push(hi);
  } else {
    if (!(el.m_edgeNumFaces[val] >= 1)) {
      throw new Error('edgeLookup.InsertEdge: edge ' + val + ' has bad numFaces ' +
                      el.m_edgeNumFaces[val]);
    }
    el.m_edgeNumFaces[val]++;
  }
  return val;
}
function edgeLookupGetNumFacesForEdge(el, edgeIndex) {
  return el.m_edgeNumFaces[edgeIndex];
}
function edgeLookupGetFaceForEdge(el, edgeIndex, faceIndex) {
  const num   = el.m_edgeNumFaces[edgeIndex];
  const start = el.m_edgeStartFaces[edgeIndex];
  if (!(faceIndex >= 0 && faceIndex < num)) {
    throw new Error('GetFaceForEdge: faceIndex ' + faceIndex + ' out of [0,' + num + ')');
  }
  return el.m_edgePolyData[start + faceIndex];
}
function edgeLookupGetEdgeIsHardNormal(el, edgeIndex) {
  return el.m_edgeIsHardNormal[edgeIndex] !== 0;
}
function edgeLookupGetEdgeIsHardTangent(el, edgeIndex) {
  return el.m_edgeIsHardTangent[edgeIndex] !== 0;
}

// ---- VertexAdjData (DcciSubdiv.h:321) -----------------------------
function makeVertexAdjData() {
  return {
    m_vertAdjSize:  [],   // amolite::vector<int>
    m_vertAdjStart: [],
    m_vertAdjData:  [],
  };
}
function vertexAdjDataReset(va) {
  va.m_vertAdjSize.length  = 0;
  va.m_vertAdjStart.length = 0;
  va.m_vertAdjData.length  = 0;
}

// ---- HardEdgeSection (DcciSubdiv.h:351) ---------------------------
function makeHardEdgeSection() {
  return { m_localCornerNum: 0, m_localCornerStart: 0 };
}

// ---- VertexWedgeInfo (DcciSubdiv.h:357) ---------------------------
function makeVertexWedgeInfo() {
  return {
    m_wedgeEdgeStart:       -1,
    m_wedgeEdgeNum:         -1,
    m_baseVertex:           -1,
    m_isCircular:           false,
    m_hardEdgeSectionNum:   -1,
    m_hardEdgeSectionStart: -1,
    m_isWedgeIrregular:     false,
  };
}

// ---- WedgeCornerInfo (DcciSubdiv.h:398) ---------------------------
function makeWedgeCornerInfo() {
  return {
    m_polyIndex:            -1,
    m_polyCorner:           -1,
    m_edgeIndex:            -1,
    m_isHardEdgeNormal:     false,
    m_isHardEdgeTangent:    false,
    m_creaseSharpness:      0.0,
    m_numFaces:             0,
    m_isManifoldConnection: false,
  };
}

// ---- VertexWedgeData (DcciSubdiv.h:424) ---------------------------
function makeVertexWedgeData() {
  return {
    m_wedgeEdgeSize:      [],   // amolite::vector<int>
    m_wedgeEdgeStart:     [],
    m_wedgeEdgeInfo:      [],   // VertexWedgeInfo[]
    m_wedgeCornerData:    [],   // WedgeCornerInfo[]
    m_hardEdgeSectionData: [],  // HardEdgeSection[]
  };
}
function vertexWedgeDataReset(wd) {
  wd.m_wedgeEdgeSize.length      = 0;
  wd.m_wedgeEdgeStart.length     = 0;
  wd.m_wedgeEdgeInfo.length      = 0;
  wd.m_wedgeCornerData.length    = 0;
  wd.m_hardEdgeSectionData.length = 0;
}

// ---- CatmullClarkTopoHelper (DcciSubdiv.h:453) --------------------
function makeCatmullClarkTopoHelper() {
  return {
    m_numVerts:        0,
    m_numEdges:        -1,
    m_numFaces:        -1,

    m_polySizes:       [],
    m_polyStarts:      [],
    m_polyIndexList:   [],

    m_edgeLookup:      makeEdgeLookup(),
    m_adjVertData:     makeVertexAdjData(),
    m_adjFaceData:     makeVertexAdjData(),

    m_wedges:          makeVertexWedgeData(),

    m_vertIsBorder:    [],
    m_edgeIsBorder:    [],

    m_edgeSharpness:   [],
    m_vertSharpness:   [],

    m_enableSemiSharp: false,
  };
}
function catmullClarkTopoHelperReset(h) {
  h.m_enableSemiSharp     = false;
  h.m_polySizes.length    = 0;
  h.m_polyStarts.length   = 0;
  h.m_polyIndexList.length= 0;
  edgeLookupReset(h.m_edgeLookup);
  vertexAdjDataReset(h.m_adjVertData);
  vertexAdjDataReset(h.m_adjFaceData);
  vertexWedgeDataReset(h.m_wedges);
  h.m_numVerts = 0;
  h.m_numEdges = -1;
  h.m_numFaces = -1;
}

// =====================================================================
// CalcPrefixSumVec (general helper used throughout DcciSubdiv.cpp).
// Returns an array where out[i] = sum of in[0..i-1] (i.e., the
// start offset of segment i in a packed layout).
// =====================================================================
function calcPrefixSumVec(inArr) {
  const n = inArr.length;
  const out = new Array(n);
  let s = 0;
  for (let i = 0; i < n; i++) {
    out[i] = s;
    s += inArr[i];
  }
  return out;
}

// =====================================================================
// buildEdgeLookup: line-by-line port of BuildEdgeLookup
// (DcciSubdiv.cpp:40). Walks every polygon's edges to populate the
// EdgeLookup struct -- edge identity, per-edge face counts, edge
// crease sharpness, hard-edge flags, and the per-edge face list.
//
// Args mirror amolite's signature exactly (same names, same order):
//
//   edgeLookup        : output (mutated)
//   polySizes         : Int32Array | int[]
//   polyIndexList     : Int32Array | int[]   (flat per-corner vertex ids)
//   creaseLhs/Rhs     : int[]                (per-crease vertex pair)
//   creaseWeight      : float[]
//   hardEdgeNormalLhs/Rhs   : int[]
//   hardEdgeTangentLhs/Rhs  : int[]
//   numVerts          : int
//
// Note: amolite stores m_edgeIsHardNormal / m_edgeIsHardTangent as
// `amolite::vector<int>` (0/1). The JS port matches.
// =====================================================================
function buildEdgeLookup(edgeLookup, polySizes, polyIndexList,
                        creaseLhs, creaseRhs, creaseWeight,
                        hardEdgeNormalLhs, hardEdgeNormalRhs,
                        hardEdgeTangentLhs, hardEdgeTangentRhs,
                        numVerts) {
  edgeLookupReset(edgeLookup);

  let polyStart = 0;
  let totalPolyEdges = 0;

  for (let polyIter = 0; polyIter < polySizes.length; polyIter++) {
    const polySize = polySizes[polyIter];

    for (let i = 0; i < polySize; i++) {
      const j = (i + 1) % polySize;
      const v0 = polyIndexList[polyStart + i];
      const v1 = polyIndexList[polyStart + j];

      edgeLookupInsertEdge(edgeLookup, v0, v1);
    }

    polyStart += polySize;
    totalPolyEdges += polySize;
  }

  const numEdges = edgeLookup.m_edgeNumFaces.length;

  // resize-with-fill, matching amolite's .resize(N, fill).
  for (let i = 0; i < numEdges; i++) edgeLookup.m_edgeCreaseSharpness.push(0.0);
  for (let i = 0; i < numEdges; i++) edgeLookup.m_edgeIsHardNormal.push(0);
  for (let i = 0; i < numEdges; i++) edgeLookup.m_edgeIsHardTangent.push(0);

  const numCreases = creaseLhs.length;
  if (numCreases !== creaseRhs.length)   throw new Error('creaseLhs/creaseRhs length mismatch');
  if (numCreases !== creaseWeight.length) throw new Error('creaseLhs/creaseWeight length mismatch');

  {
    const numHardEdges = hardEdgeNormalLhs.length;
    if (numHardEdges !== hardEdgeNormalRhs.length) throw new Error('hardEdgeNormalLhs/Rhs length mismatch');
    for (let hardIter = 0; hardIter < numHardEdges; hardIter++) {
      const lhs = hardEdgeNormalLhs[hardIter];
      const rhs = hardEdgeNormalRhs[hardIter];

      const edgeIndex = edgeLookupFetchEdge(edgeLookup, lhs, rhs);
      edgeLookup.m_edgeIsHardNormal[edgeIndex] = 1;

      // by default turn hard normals into fully sharp edges
      edgeLookup.m_edgeCreaseSharpness[edgeIndex] = 10.0;
    }
  }

  for (let creaseIter = 0; creaseIter < numCreases; creaseIter++) {
    const lhs = creaseLhs[creaseIter];
    const rhs = creaseRhs[creaseIter];

    const weight = creaseWeight[creaseIter];

    const edgeIndex = edgeLookupFetchEdge(edgeLookup, lhs, rhs);
    edgeLookup.m_edgeCreaseSharpness[edgeIndex] = weight;
  }

  {
    const numHardEdges = hardEdgeTangentLhs.length;
    if (numHardEdges !== hardEdgeTangentRhs.length) throw new Error('hardEdgeTangentLhs/Rhs length mismatch');
    for (let hardIter = 0; hardIter < numHardEdges; hardIter++) {
      const lhs = hardEdgeTangentLhs[hardIter];
      const rhs = hardEdgeTangentRhs[hardIter];

      const edgeIndex = edgeLookupFetchEdge(edgeLookup, lhs, rhs);
      edgeLookup.m_edgeIsHardTangent[edgeIndex] = 1;
    }
  }

  edgeLookup.m_edgeStartFaces = calcPrefixSumVec(edgeLookup.m_edgeNumFaces);
  const edgeAdded = new Array(numEdges);
  for (let i = 0; i < numEdges; i++) edgeAdded[i] = 0;

  // amolite: m_edgePolyData.resize(totalPolyEdges) -- default-constructed
  // PolyCorner entries get (-1, -1). We initialize to that too.
  edgeLookup.m_edgePolyData.length = 0;
  for (let i = 0; i < totalPolyEdges; i++) edgeLookup.m_edgePolyData.push(makePolyCorner());

  polyStart = 0;
  for (let polyIter = 0; polyIter < polySizes.length; polyIter++) {
    const polySize = polySizes[polyIter];

    for (let i = 0; i < polySize; i++) {
      const j = (i + 1) % polySize;
      const v0 = polyIndexList[polyStart + i];
      const v1 = polyIndexList[polyStart + j];

      const edgeIndex = edgeLookupFetchEdge(edgeLookup, v0, v1);

      const startForEdge = edgeLookup.m_edgeStartFaces[edgeIndex];
      const addedForEdge = edgeAdded[edgeIndex];

      const edgePoly = makePolyCorner(polyIter, i);

      edgeLookup.m_edgePolyData[startForEdge + addedForEdge] = edgePoly;

      edgeAdded[edgeIndex]++;
    }

    polyStart += polySize;
  }

  for (let i = 0; i < edgeAdded.length; i++) {
    if (edgeAdded[i] !== edgeLookup.m_edgeNumFaces[i]) {
      throw new Error('buildEdgeLookup: edgeAdded[' + i + ']=' + edgeAdded[i] +
                      ' != m_edgeNumFaces=' + edgeLookup.m_edgeNumFaces[i]);
    }
  }
  for (let i = 0; i < edgeLookup.m_edgePolyData.length; i++) {
    const poly = edgeLookup.m_edgePolyData[i];
    if (!(poly.m_polyIndex >= 0 && poly.m_polyCorner >= 0)) {
      throw new Error('buildEdgeLookup: m_edgePolyData[' + i + '] invalid');
    }
  }
}

// =====================================================================
// buildFaceAdjData: line-by-line port of BuildFaceAdjData
// (DcciSubdiv.cpp:234). Produces per-vertex list of incident polygons.
//
//   m_vertAdjSize[v]  -- valence (number of incident polygons)
//   m_vertAdjStart[v] -- start offset into m_vertAdjData
//   m_vertAdjData[]   -- flat per-vertex polygon-ids
// =====================================================================
function buildFaceAdjData(adjData, polySizes, polyIndexList, numVerts) {
  vertexAdjDataReset(adjData);

  // find number of adjacencies (valence)
  {
    for (let i = 0; i < numVerts; i++) adjData.m_vertAdjSize.push(0);

    let polyStart = 0;
    for (let poly = 0; poly < polySizes.length; poly++) {
      const currSize = polySizes[poly];
      for (let vert = 0; vert < currSize; vert++) {
        const v = polyIndexList[polyStart + vert];

        adjData.m_vertAdjSize[v]++;
      }

      polyStart += currSize;
    }
    if (polyStart !== polyIndexList.length) {
      throw new Error('buildFaceAdjData: polyStart ' + polyStart +
                      ' != polyIndexList.length ' + polyIndexList.length);
    }
  }

  // allocate space
  {
    for (let i = 0; i < numVerts; i++) adjData.m_vertAdjStart.push(0);

    let vertStart = 0;
    for (let i = 0; i < numVerts; i++) {
      adjData.m_vertAdjStart[i] = vertStart;
      vertStart += adjData.m_vertAdjSize[i];
    }

    for (let i = 0; i < vertStart; i++) adjData.m_vertAdjData.push(-1);
  }

  {
    // how many adjacent verts we have already added to this vert
    const vertNumAdded = new Array(numVerts);
    for (let i = 0; i < numVerts; i++) vertNumAdded[i] = 0;

    let polyStart = 0;
    for (let poly = 0; poly < polySizes.length; poly++) {
      const currSize = polySizes[poly];
      for (let vert = 0; vert < currSize; vert++) {
        const v = polyIndexList[polyStart + vert];

        const added = vertNumAdded[v];
        const start = adjData.m_vertAdjStart[v];

        // sanity check: make sure the previous value is -1
        const prevVal = adjData.m_vertAdjData[start + added];
        if (prevVal !== -1) {
          throw new Error('buildFaceAdjData: prev value at ' + (start + added) +
                          ' was ' + prevVal + ' not -1');
        }

        adjData.m_vertAdjData[start + added] = poly;

        vertNumAdded[v]++;
      }

      polyStart += currSize;
    }
    if (polyStart !== polyIndexList.length) {
      throw new Error('buildFaceAdjData (pass 3): polyStart ' + polyStart +
                      ' != polyIndexList.length ' + polyIndexList.length);
    }

    // sanity check
    for (let i = 0; i < numVerts; i++) {
      const size  = adjData.m_vertAdjSize[i];
      const added = vertNumAdded[i];
      if (size !== added) {
        throw new Error('buildFaceAdjData: vert ' + i + ' size=' + size + ' added=' + added);
      }
    }
    for (let i = 0; i < adjData.m_vertAdjData.length; i++) {
      if (adjData.m_vertAdjData[i] < 0) {
        throw new Error('buildFaceAdjData: m_vertAdjData[' + i + ']=' + adjData.m_vertAdjData[i]);
      }
    }
  }
}

// =====================================================================
// buildVertAdjData: line-by-line port of BuildVertAdjData
// (DcciSubdiv.cpp:856). Produces per-vertex list of adjacent vertices,
// edge-deduplicated via the EdgeLookup so the same shared edge isn't
// counted from both adjacent polygons.
//
//   m_vertAdjSize[v]  -- # of distinct adjacent verts
//   m_vertAdjStart[v] -- start offset into m_vertAdjData
//   m_vertAdjData[]   -- flat per-vertex neighbor-vertex-ids
// =====================================================================
function buildVertAdjData(adjData, polySizes, polyIndexList, numVerts, edgeLookup) {
  vertexAdjDataReset(adjData);

  // find number of adjacencies (valence)
  {
    for (let i = 0; i < numVerts; i++) adjData.m_vertAdjSize.push(0);

    const isEdgeTaken = new Array(edgeLookup.m_numEdges);
    for (let i = 0; i < edgeLookup.m_numEdges; i++) isEdgeTaken[i] = false;

    let polyStart = 0;
    for (let poly = 0; poly < polySizes.length; poly++) {
      const currSize = polySizes[poly];
      for (let vert = 0; vert < currSize; vert++) {
        const v0 = polyIndexList[polyStart + vert];
        const v1 = polyIndexList[polyStart + ((vert + 1) % currSize)];

        const edgeIndex = edgeLookupFetchEdge(edgeLookup, v0, v1);
        const isTaken = isEdgeTaken[edgeIndex];
        if (isTaken) {
          // no op, we already added this edge
        } else {
          adjData.m_vertAdjSize[v0]++;
          adjData.m_vertAdjSize[v1]++;

          isEdgeTaken[edgeIndex] = true;
        }
      }

      polyStart += currSize;
    }
    if (polyStart !== polyIndexList.length) {
      throw new Error('buildVertAdjData: polyStart ' + polyStart +
                      ' != polyIndexList.length ' + polyIndexList.length);
    }
  }

  // allocate space
  {
    for (let i = 0; i < numVerts; i++) adjData.m_vertAdjStart.push(0);

    let vertStart = 0;
    for (let i = 0; i < numVerts; i++) {
      adjData.m_vertAdjStart[i] = vertStart;
      vertStart += adjData.m_vertAdjSize[i];
    }

    for (let i = 0; i < vertStart; i++) adjData.m_vertAdjData.push(-1);
  }

  // find adjacencies
  {
    const vertNumAdded = new Array(numVerts);
    for (let i = 0; i < numVerts; i++) vertNumAdded[i] = 0;

    const isEdgeTaken = new Array(edgeLookup.m_numEdges);
    for (let i = 0; i < edgeLookup.m_numEdges; i++) isEdgeTaken[i] = false;

    let polyStart = 0;
    for (let poly = 0; poly < polySizes.length; poly++) {
      const currSize = polySizes[poly];
      for (let vert = 0; vert < currSize; vert++) {
        const v0 = polyIndexList[polyStart + vert];
        const v1 = polyIndexList[polyStart + ((vert + 1) % currSize)];

        const edgeIndex = edgeLookupFetchEdge(edgeLookup, v0, v1);
        const isTaken = isEdgeTaken[edgeIndex];
        if (isTaken) {
          // no op, we already added this edge
        } else {
          {
            const currV = v0;

            const added = vertNumAdded[currV];
            const start = adjData.m_vertAdjStart[currV];

            const prevVal = adjData.m_vertAdjData[start + added];
            if (prevVal !== -1) {
              throw new Error('buildVertAdjData: prev at ' + (start + added) + ' = ' + prevVal);
            }

            adjData.m_vertAdjData[start + added] = v1;

            vertNumAdded[currV]++;
          }

          {
            const currV = v1;

            const added = vertNumAdded[currV];
            const start = adjData.m_vertAdjStart[currV];

            const prevVal = adjData.m_vertAdjData[start + added];
            if (prevVal !== -1) {
              throw new Error('buildVertAdjData: prev at ' + (start + added) + ' = ' + prevVal);
            }

            adjData.m_vertAdjData[start + added] = v0;

            vertNumAdded[currV]++;
          }
          isEdgeTaken[edgeIndex] = true;
        }
      }

      polyStart += currSize;
    }
    if (polyStart !== polyIndexList.length) {
      throw new Error('buildVertAdjData (pass 3): polyStart ' + polyStart +
                      ' != polyIndexList.length ' + polyIndexList.length);
    }

    // sanity check
    for (let i = 0; i < numVerts; i++) {
      const size  = adjData.m_vertAdjSize[i];
      const added = vertNumAdded[i];
      if (size !== added) {
        throw new Error('buildVertAdjData: vert ' + i + ' size=' + size + ' added=' + added);
      }
    }
    for (let i = 0; i < adjData.m_vertAdjData.length; i++) {
      if (adjData.m_vertAdjData[i] < 0) {
        throw new Error('buildVertAdjData: m_vertAdjData[' + i + ']=' + adjData.m_vertAdjData[i]);
      }
    }
  }
}

// =====================================================================
// Helpers used inside buildWedgeData. Each is a line-by-line port of
// the matching static function in DcciSubdiv.cpp.
// =====================================================================

// Port of GetFaceInCornerVec (DcciSubdiv.cpp:316).
function getFaceInCornerVec(wedgeCorners, faceIndex, expectedCorner) {
  let ret = -1;
  for (let i = 0; i < wedgeCorners.length; i++) {
    const corner = wedgeCorners[i];
    if (corner.m_polyIndex === faceIndex) {
      if (corner.m_polyCorner !== expectedCorner) {
        throw new Error('getFaceInCornerVec: m_polyCorner=' + corner.m_polyCorner +
                        ' expected=' + expectedCorner);
      }
      ret = i;
      break;
    }
  }
  return ret;
}

// Port of FindMatchingBackwardsEdgeInVec (DcciSubdiv.cpp:334).
function findMatchingBackwardsEdgeInVec(wedgeCorners, edgeData,
                                        polySizes, polyStarts, polyIndexData,
                                        cornerIndex) {
  const baseCorner = wedgeCorners[cornerIndex];

  const basePolySize  = polySizes[baseCorner.m_polyIndex];
  const basePolyStart = polyStarts[baseCorner.m_polyIndex];
  if (!(baseCorner.m_polyCorner >= 0))           throw new Error('m_polyCorner < 0');
  if (!(baseCorner.m_polyCorner < basePolySize)) throw new Error('m_polyCorner >= polySize');

  const baseVtx0 = polyIndexData[basePolyStart + (baseCorner.m_polyCorner + 0) % basePolySize];
  const baseVtx1 = polyIndexData[basePolyStart + (baseCorner.m_polyCorner + (basePolySize - 1)) % basePolySize];

  const edgeIndex = edgeLookupFetchEdge(edgeData, baseVtx0, baseVtx1);

  const isHardNormal    = edgeLookupGetEdgeIsHardNormal(edgeData, edgeIndex);
  const numFacesForEdge = edgeLookupGetNumFacesForEdge(edgeData, edgeIndex);

  // skip borders or non-manifold edges, and hard edges would be on different wedges

  let foundIndex = -1;

  if (!isHardNormal && numFacesForEdge === 2) {
    for (let cornerIter = 0; cornerIter < wedgeCorners.length; cornerIter++) {
      if (cornerIter !== cornerIndex) {
        const currCorner = wedgeCorners[cornerIter];

        const currPolySize  = polySizes[currCorner.m_polyIndex];
        const currPolyStart = polyStarts[currCorner.m_polyIndex];
        if (!(currCorner.m_polyCorner >= 0))            throw new Error('m_polyCorner < 0');
        if (!(currCorner.m_polyCorner < currPolySize))  throw new Error('m_polyCorner >= polySize');

        const currVtx0 = polyIndexData[currPolyStart + (currCorner.m_polyCorner + 0) % currPolySize];
        if (currVtx0 !== baseVtx0) {
          throw new Error('findMatchingBackwardsEdgeInVec: currVtx0=' + currVtx0 + ' != baseVtx0=' + baseVtx0);
        }

        const currVtx1 = polyIndexData[currPolyStart + (currCorner.m_polyCorner + 1) % currPolySize];

        if (currVtx0 === baseVtx0 && currVtx1 === baseVtx1) {
          if (foundIndex !== -1) {
            throw new Error('findMatchingBackwardsEdgeInVec: two matches found');
          }
          foundIndex = cornerIter;
        }
      }
    }
  }

  return foundIndex;
}

// Port of GetEdgeInCornerVec (DcciSubdiv.cpp:397). Unused here but
// kept for parity with the C++ source.
function getEdgeInCornerVec(wedgeCorners, edgeIndex) {
  for (let i = 0; i < wedgeCorners.length; i++) {
    if (wedgeCorners[i].m_edgeIndex === edgeIndex) return i;
  }
  return -1;
}

// Port of ChooseBestStartWedgeCorner (DcciSubdiv.cpp:413).
function chooseBestStartWedgeCorner(wedgeCorners, wedgePrev, wedgeNext, isAvailable) {
  let ret = -1;

  // -1: Nothing
  //  0: Regular manifold edge
  //  1: Manifold hard edge
  //  2: Non-manifold or border with wedgePrev invalid

  let foundRank = -1;

  const numCorners = wedgeCorners.length;
  if (wedgePrev.length    !== numCorners) throw new Error('wedgePrev len mismatch');
  if (wedgeNext.length    !== numCorners) throw new Error('wedgeNext len mismatch');
  if (isAvailable.length  !== numCorners) throw new Error('isAvailable len mismatch');

  for (let i = 0; i < numCorners; i++) {
    if (isAvailable[i]) {
      const corner = wedgeCorners[i];

      let currRank = 0;

      if (corner.m_isHardEdgeTangent) currRank = 1;
      if (corner.m_isHardEdgeNormal)  currRank = 2;
      if (wedgePrev[i] < 0)           currRank = 3;

      if (currRank >= foundRank) {
        foundRank = currRank;
        ret = i;
      }
    }
  }

  return ret;
}

// Port of GetOutgoingEdge (DcciSubdiv.cpp:467).
function getOutgoingEdge(cornerInfo, edgeData, polySizes, polyStarts, polyIndexData) {
  if (!(cornerInfo.m_polyIndex >= 0)) throw new Error('getOutgoingEdge: m_polyIndex < 0');
  const start = polyStarts[cornerInfo.m_polyIndex];
  const size  = polySizes [cornerInfo.m_polyIndex];
  if (!(0 <= cornerInfo.m_polyCorner && cornerInfo.m_polyCorner < size)) {
    throw new Error('getOutgoingEdge: m_polyCorner out of range');
  }

  const currVtx = polyIndexData[start + (cornerInfo.m_polyCorner + 0) % size];
  const prevVtx = polyIndexData[start + (cornerInfo.m_polyCorner + (size - 1)) % size];
  // nextVtx computed in C++ but unused -- omit.

  return edgeLookupFetchEdge(edgeData, prevVtx, currVtx);
}

// Port of GetIncomingEdge (DcciSubdiv.cpp:485).
function getIncomingEdge(cornerInfo, edgeData, polySizes, polyStarts, polyIndexData) {
  if (!(cornerInfo.m_polyIndex >= 0)) throw new Error('getIncomingEdge: m_polyIndex < 0');
  const start = polyStarts[cornerInfo.m_polyIndex];
  const size  = polySizes [cornerInfo.m_polyIndex];
  if (!(0 <= cornerInfo.m_polyCorner && cornerInfo.m_polyCorner < size)) {
    throw new Error('getIncomingEdge: m_polyCorner out of range');
  }

  const currVtx = polyIndexData[start + (cornerInfo.m_polyCorner + 0) % size];
  // prevVtx computed in C++ but unused -- omit.
  const nextVtx = polyIndexData[start + (cornerInfo.m_polyCorner + 1) % size];

  return edgeLookupFetchEdge(edgeData, currVtx, nextVtx);
}

// =====================================================================
// buildWedgeData: line-by-line port of BuildWedgeData
// (DcciSubdiv.cpp:505). Constructs per-vertex wedge structure -- the
// fan(s) of polygons around each vertex, partitioned by hard edges
// and borders into separate wedges. Each wedge holds a sequence of
// WedgeCornerInfo entries in walk order, plus VertexWedgeInfo
// metadata + hard-edge section data.
// =====================================================================
function buildWedgeData(dstWedgeData, adjData, edgeData, numVerts,
                       polySizes, polyStarts, polyIndexData,
                       vertCreaseIndex, vertCreaseWeight) {
  vertexWedgeDataReset(dstWedgeData);

  for (let i = 0; i < numVerts; i++) dstWedgeData.m_wedgeEdgeSize.push(-1);
  for (let i = 0; i < numVerts; i++) dstWedgeData.m_wedgeEdgeStart.push(-1);

  for (let vtxIter = 0; vtxIter < numVerts; vtxIter++) {
    // edges for this vertex
    const wedgeCorners = [];

    const wedgePrev = [];
    const wedgeNext = [];

    const edgeNum   = adjData.m_vertAdjSize[vtxIter];
    const edgeStart = adjData.m_vertAdjStart[vtxIter];

    let debugValid = true;

    {
      for (let edgeIter = 0; edgeIter < edgeNum; edgeIter++) {
        const otherVtx = adjData.m_vertAdjData[edgeStart + edgeIter];

        const edgeIndex = edgeLookupFetchEdge(edgeData, vtxIter, otherVtx);

        const numFacesOnEdge = edgeLookupGetNumFacesForEdge(edgeData, edgeIndex);

        if (numFacesOnEdge !== 2) {
          debugValid = false;
        }

        // amolite computes faceForward / faceBackward / isBorder /
        // isNonManiforld here but doesn't use them in this scope.
        // const isBorder        = (numFacesOnEdge === 1);
        // const isNonManifold   = (numFacesOnEdge >= 3);

        for (let faceIter = 0; faceIter < numFacesOnEdge; faceIter++) {
          const edgeSide = edgeLookupGetFaceForEdge(edgeData, edgeIndex, faceIter);

          const numSides = polySizes[edgeSide.m_polyIndex];
          const start    = polyStarts[edgeSide.m_polyIndex];

          const v0 = polyIndexData[start + (edgeSide.m_polyCorner + 0) % numSides];
          const v1 = polyIndexData[start + (edgeSide.m_polyCorner + 1) % numSides];

          if (!((v0 === vtxIter && v1 === otherVtx) || (v1 === vtxIter && v0 === otherVtx))) {
            throw new Error('buildWedgeData: edgeSide does not bound (vtxIter, otherVtx)');
          }

          let actualSide = -1;
          // isForward computed in C++ but only used implicitly in the
          // actualSide choice.
          if (v0 === vtxIter) {
            // if the first vertex is the wedge center, this is the
            // incoming edge
            if (v1 !== otherVtx) throw new Error('asym v0/v1');
            actualSide = edgeSide.m_polyCorner;
          } else {
            // if the second vertex is the wedge center, this is the
            // outgoing edge
            if (v1 !== vtxIter) throw new Error('asym v0/v1');
            if (v0 !== otherVtx) throw new Error('asym v0/v1');
            actualSide = (edgeSide.m_polyCorner + 1) % numSides;
          }

          let cornerInVec = getFaceInCornerVec(wedgeCorners, edgeSide.m_polyIndex, actualSide);

          if (cornerInVec < 0) {
            const toAdd = makeWedgeCornerInfo();

            toAdd.m_polyIndex  = edgeSide.m_polyIndex;
            toAdd.m_polyCorner = actualSide;

            cornerInVec = wedgeCorners.length;
            wedgeCorners.push(toAdd);
            wedgeNext.push(-1);
            wedgePrev.push(-1);
          }
        }
      }

      for (let i = 0; i < wedgeCorners.length; i++) {
        const foundIndex = findMatchingBackwardsEdgeInVec(
            wedgeCorners, edgeData,
            polySizes, polyStarts, polyIndexData, i);

        {
          // fetch the backwards index for this
          let edgeIndex = -1;
          {
            const baseCorner = wedgeCorners[i];

            const basePolySize  = polySizes[baseCorner.m_polyIndex];
            const basePolyStart = polyStarts[baseCorner.m_polyIndex];
            if (!(baseCorner.m_polyCorner >= 0))            throw new Error('m_polyCorner < 0');
            if (!(baseCorner.m_polyCorner < basePolySize))  throw new Error('m_polyCorner >= polySize');

            const baseVtx0 = polyIndexData[basePolyStart + (baseCorner.m_polyCorner + 0) % basePolySize];
            const baseVtx1 = polyIndexData[basePolyStart + (baseCorner.m_polyCorner + 1) % basePolySize];

            edgeIndex = edgeLookupFetchEdge(edgeData, baseVtx0, baseVtx1);
          }

          wedgeCorners[i].m_edgeIndex         = edgeIndex;
          wedgeCorners[i].m_isHardEdgeNormal  = edgeLookupGetEdgeIsHardNormal(edgeData, edgeIndex);
          wedgeCorners[i].m_isHardEdgeTangent = edgeLookupGetEdgeIsHardTangent(edgeData, edgeIndex);

          wedgeCorners[i].m_creaseSharpness = edgeData.m_edgeCreaseSharpness[edgeIndex];

          wedgeCorners[i].m_numFaces = edgeLookupGetNumFacesForEdge(edgeData, edgeIndex);
        }

        if (foundIndex >= 0) {
          wedgeNext[i] = foundIndex;
          wedgePrev[foundIndex] = i;

          wedgeCorners[i].m_isManifoldConnection = true;
        }
      }
    }

    const numCornersFound = wedgeCorners.length;

    const isAvailable = new Array(numCornersFound);
    for (let i = 0; i < numCornersFound; i++) isAvailable[i] = true;

    const originalWedgeStart = dstWedgeData.m_wedgeEdgeInfo.length;
    let wedgesAdded = 0;

    let bestCorner = chooseBestStartWedgeCorner(wedgeCorners, wedgePrev, wedgeNext, isAvailable);

    while (bestCorner >= 0) {
      const foundCornersForWedge = [];

      {
        let iterCorner = bestCorner;
        while (iterCorner >= 0) {
          foundCornersForWedge.push(iterCorner);
          isAvailable[iterCorner] = false;

          iterCorner = wedgeNext[iterCorner];
          if (iterCorner >= 0) {
            if (!isAvailable[iterCorner]) {
              iterCorner = -1;
            }
          }
        }
      }

      const cornersToAdd = [];
      for (let i = 0; i < foundCornersForWedge.length; i++) {
        const foundCornerIndex = foundCornersForWedge[i];
        const cornerInfo = wedgeCorners[foundCornerIndex];
        cornersToAdd.push(cornerInfo);
      }

      // amolite computes wedgeFacePts here as a debug-only scratchpad;
      // it's never written into dstWedgeData. Omit.

      for (let i = 0; i < cornersToAdd.length - 1; i++) {
        const lhs = cornersToAdd[i];
        const rhs = cornersToAdd[i + 1];

        const outgoing = getOutgoingEdge(lhs, edgeData, polySizes, polyStarts, polyIndexData);
        const incoming = getIncomingEdge(rhs, edgeData, polySizes, polyStarts, polyIndexData);

        if (outgoing !== incoming) {
          throw new Error('buildWedgeData: adjacent wedge corners disagree on edge (' +
                          outgoing + ' vs ' + incoming + ')');
        }
      }

      const hardEdgeSections = [];
      {
        let hardEdgeLastStart = 0;

        for (let i = 1; i < cornersToAdd.length; i++) {
          // skip first corner, because we ignore the first incoming edge if it is hard
          const corner = cornersToAdd[i];

          // refers to the incoming hard edge
          if (corner.m_isHardEdgeNormal) {
            const section = makeHardEdgeSection();
            section.m_localCornerStart = hardEdgeLastStart;
            section.m_localCornerNum   = i - hardEdgeLastStart;
            hardEdgeSections.push(section);

            hardEdgeLastStart = i;
          }
        }

        const lastSection = makeHardEdgeSection();
        lastSection.m_localCornerStart = hardEdgeLastStart;
        lastSection.m_localCornerNum   = cornersToAdd.length - hardEdgeLastStart;
        hardEdgeSections.push(lastSection);
      }

      let hasBorderEdge = false;
      {
        const lhs = cornersToAdd[cornersToAdd.length - 1];
        const rhs = cornersToAdd[0];

        const outgoing = getOutgoingEdge(lhs, edgeData, polySizes, polyStarts, polyIndexData);
        const incoming = getIncomingEdge(rhs, edgeData, polySizes, polyStarts, polyIndexData);

        if (incoming !== outgoing) {
          // border
          hasBorderEdge = true;

          const extraEdge = makeWedgeCornerInfo();
          extraEdge.m_edgeIndex         = outgoing;
          extraEdge.m_isHardEdgeNormal  = edgeLookupGetEdgeIsHardNormal(edgeData, extraEdge.m_edgeIndex);
          extraEdge.m_isHardEdgeTangent = edgeLookupGetEdgeIsHardTangent(edgeData, extraEdge.m_edgeIndex);
          extraEdge.m_creaseSharpness   = edgeData.m_edgeCreaseSharpness[extraEdge.m_edgeIndex];
          extraEdge.m_numFaces          = edgeLookupGetNumFacesForEdge(edgeData, extraEdge.m_edgeIndex);

          cornersToAdd.push(extraEdge);
        }
      }

      const wedgeInfo = makeVertexWedgeInfo();
      wedgeInfo.m_baseVertex            = vtxIter;
      wedgeInfo.m_isCircular            = !hasBorderEdge;
      wedgeInfo.m_wedgeEdgeNum          = cornersToAdd.length;
      wedgeInfo.m_wedgeEdgeStart        = dstWedgeData.m_wedgeCornerData.length;
      wedgeInfo.m_hardEdgeSectionNum    = hardEdgeSections.length;
      wedgeInfo.m_hardEdgeSectionStart  = dstWedgeData.m_hardEdgeSectionData.length;

      for (let i = 0; i < cornersToAdd.length; i++) {
        dstWedgeData.m_wedgeCornerData.push(cornersToAdd[i]);
      }
      for (let i = 0; i < hardEdgeSections.length; i++) {
        dstWedgeData.m_hardEdgeSectionData.push(hardEdgeSections[i]);
      }

      dstWedgeData.m_wedgeEdgeInfo.push(wedgeInfo);
      wedgesAdded++;

      bestCorner = chooseBestStartWedgeCorner(wedgeCorners, wedgePrev, wedgeNext, isAvailable);
    }

    if (dstWedgeData.m_wedgeEdgeInfo.length !== originalWedgeStart + wedgesAdded) {
      throw new Error('buildWedgeData: wedge bookkeeping mismatch');
    }
    dstWedgeData.m_wedgeEdgeStart[vtxIter] = originalWedgeStart;
    dstWedgeData.m_wedgeEdgeSize[vtxIter]  = wedgesAdded;
  }
}

// =====================================================================
// duplicateSameIdsOnDifferentPositions: line-by-line port of
// DuplicateSameIdsOnDifferentPositions template (DcciSubdiv.cpp:2161).
//
// For each (uvId, posId) pair seen in (srcIds, posIds), produces a
// new dstIds where each unique pair gets its own slot, and dstData
// is the matching src attribute value duplicated to fill those slots.
//
// stride: how many scalars per attribute entry (1 for Vec, 2 for
// Vec2, 4 for Vec4). srcData is a flat Float32-like array of
// numAttrs * stride elements.
// =====================================================================
function duplicateSameIdsOnDifferentPositions(dstIdsOut, dstDataOut,
                                              posIds, srcIds, srcData, stride) {
  const numAttrs = (srcData.length / stride) | 0;

  // slow approach, optimize later (matches amolite verbatim)
  const posIdsForAttr = new Array(numAttrs);
  for (let i = 0; i < numAttrs; i++) posIdsForAttr[i] = [];

  for (let i = 0; i < srcIds.length; i++) {
    const uvId  = srcIds[i];
    const posId = posIds[i];

    const currAttrVec = posIdsForAttr[uvId];
    let foundIndex = -1;
    for (let k = 0; k < currAttrVec.length; k++) {
      if (currAttrVec[k] === posId) { foundIndex = k; break; }
    }
    if (foundIndex < 0) {
      currAttrVec.push(posId);
    }
  }

  const numPosForAttr = new Array(numAttrs);
  for (let i = 0; i < numAttrs; i++) numPosForAttr[i] = posIdsForAttr[i].length;

  const startPosForAttr = calcPrefixSumVec(numPosForAttr);
  const totalNum = (numAttrs > 0)
    ? (startPosForAttr[numAttrs - 1] + numPosForAttr[numAttrs - 1])
    : 0;

  // dstAttrData[] of length totalNum (each slot = stride scalars).
  const dstAttrData = new Float32Array(totalNum * stride);

  for (let i = 0; i < srcIds.length; i++) {
    const attr = srcIds[i];

    const start = startPosForAttr[attr];
    const num   = numPosForAttr[attr];

    // attrVal = srcData[attr] (stride scalars).
    for (let j = 0; j < num; j++) {
      for (let s = 0; s < stride; s++) {
        dstAttrData[(start + j) * stride + s] = srcData[attr * stride + s];
      }
    }
  }

  const dstAttrIds = new Array(srcIds.length);
  for (let i = 0; i < srcIds.length; i++) {
    const attr  = srcIds[i];
    const posId = posIds[i];

    const currAttrVec = posIdsForAttr[attr];
    let foundIndex = -1;
    for (let k = 0; k < currAttrVec.length; k++) {
      if (currAttrVec[k] === posId) { foundIndex = k; break; }
    }
    if (foundIndex < 0) {
      throw new Error('duplicateSameIdsOnDifferentPositions: posId ' + posId +
                      ' not in posIdsForAttr[' + attr + ']');
    }

    const dstStart = startPosForAttr[attr];
    const dstId    = dstStart + foundIndex;

    dstAttrIds[i] = dstId;
  }

  // Copy out (amolite assigns directly; we mutate the passed arrays).
  dstDataOut.length = 0;
  for (let i = 0; i < dstAttrData.length; i++) dstDataOut.push(dstAttrData[i]);
  dstIdsOut.length = 0;
  for (let i = 0; i < dstAttrIds.length; i++) dstIdsOut.push(dstAttrIds[i]);
}

// =====================================================================
// prepMeshForCatmullClark: line-by-line port of
// DcciSubdiv::PrepMeshForCatmullClark (DcciSubdiv.cpp:2240). Mutates
// the AmoliteMesh-equivalent struct in place:
//   - For each UV set: duplicates UV ids so each (uv, pos) pair has
//     a unique id (amolite needs this for CC weight propagation).
//   - For each color set: same (no-op on our fixtures).
//   - From normal-ids: derives hard-edge-normal pairs (no-op on our
//     fixtures since they don't carry m_normalIds; the parseOBJ +
//     smoothNormalsInPlace path produces per-vertex normals only).
//
// Our JS mesh struct uses these field names (matching amolite):
//   m_uvSetNames     : string[]                (or absent)
//   m_uvData         : Float32[][]             (2 floats per entry)
//   m_uvIds          : int[][]                 (one id per polygon corner)
//   m_colorSetNames  : string[]                (or absent)
//   m_colorIds       : int[][]
//   m_normalIds      : int[]                   (or absent)
//   m_polyIndexList  : int[]
// =====================================================================
function prepMeshForCatmullClark(mesh) {
  // uv sets
  {
    const numUvSets = (mesh.m_uvSetNames && mesh.m_uvSetNames.length) || mesh.m_uvIds.length;
    for (let uvSetIter = 0; uvSetIter < numUvSets; uvSetIter++) {
      const srcIds = mesh.m_uvIds[uvSetIter];
      const srcUvs = mesh.m_uvData[uvSetIter];

      if (srcIds.length !== mesh.m_polyIndexList.length) {
        throw new Error('prepMeshForCatmullClark: uvIds[' + uvSetIter + '].length ' +
                        srcIds.length + ' != polyIndexList.length ' +
                        mesh.m_polyIndexList.length);
      }

      const posIds = mesh.m_polyIndexList;

      const dstUvData = [];
      const dstUvIds  = [];

      duplicateSameIdsOnDifferentPositions(
          dstUvIds, dstUvData,
          mesh.m_polyIndexList,
          srcIds, srcUvs, /*stride=*/ 2);

      mesh.m_uvData[uvSetIter] = dstUvData;
      mesh.m_uvIds [uvSetIter] = dstUvIds;
    }
  }

  // color sets -- no-op on our fixtures (no colors); port the loop
  // shell so the JS structure matches amolite.
  {
    const numColorSets = (mesh.m_colorSetNames && mesh.m_colorSetNames.length) || 0;
    for (let colorSetIter = 0; colorSetIter < numColorSets; colorSetIter++) {
      // amolite calls DcciMeshUtil::FetchAllColors -> ApplyAllColors here
      // (which expand the colorIds array into a Vec4 vector then
      // re-collapse it). We don't carry color data through our OBJ
      // path; throw if a fixture surprises us by including color.
      throw new Error('prepMeshForCatmullClark: color set ' + colorSetIter +
                      ' encountered -- not ported yet');
    }
  }

  // hard edges from normals -- only runs if mesh.m_normalIds is
  // populated. Our parseOBJ + smoothNormalsInPlace path produces
  // per-vertex smooth normals but doesn't populate m_normalIds, so
  // this branch is skipped. When a future fixture authors hard
  // shading edges we'll port the block (DcciSubdiv.cpp:2366-2500ish).
  if (mesh.m_normalIds && mesh.m_normalIds.length > 0) {
    throw new Error('prepMeshForCatmullClark: m_normalIds-driven hard-edge ' +
                    'detection not ported yet');
  }
}

// =====================================================================
// applySwapToVector: line-by-line port of ApplySwapToVector
// (DcciSubdiv.cpp:1881). Two-pass UV-seam detector helper. Records
// the first UV-index seen at an edge; if the second visit sees a
// different UV index, marks the entry -2 (= "seam"); -1 = unvisited.
// =====================================================================
function applySwapToVector(vecData, index, value) {
  const srcVal = vecData[index];
  let dstVal = -1;

  if (srcVal === -1) {
    dstVal = value;
  } else if (srcVal >= 0) {
    if (srcVal === value) {
      dstVal = value;
    } else {
      dstVal = -2;
    }
  } else if (srcVal === -2) {
    // if a seam was found, keep the seam (-2 -> -1 in amolite -- this
    // looks like an off-by-one but mirrors the C++ verbatim; the
    // final pass checks for -2 so it's already been recorded).
    dstVal = -1;
  } else {
    throw new Error('applySwapToVector: unexpected srcVal ' + srcVal);
  }

  vecData[index] = dstVal;
}

// =====================================================================
// buildHardTangentEdgesFromMesh: line-by-line port of
// BuildHardTangentEdgesFromMesh (DcciSubdiv.cpp:1917). Detects UV
// seams as hard-tangent edges -- an edge crossing a UV discontinuity
// (two adjacent faces disagree on the UV index for one of the edge's
// endpoints) marks the edge as a tangent-side hard edge.
// =====================================================================
function buildHardTangentEdgesFromMesh(hardEdgeTangentListLhs, hardEdgeTangentListRhs,
                                       srcMeshBase, tangentUvIndex) {
  hardEdgeTangentListLhs.length = 0;
  hardEdgeTangentListRhs.length = 0;

  const edgeLookup = makeEdgeLookup();

  const polySizes     = srcMeshBase.m_polySizes;
  const polyIndexList = srcMeshBase.m_polyIndexList;

  if (tangentUvIndex < srcMeshBase.m_uvIds.length) {
    const uvIds = srcMeshBase.m_uvIds[tangentUvIndex];

    {
      let polyStart = 0;
      for (let polyIter = 0; polyIter < polySizes.length; polyIter++) {
        const polySize = polySizes[polyIter];

        for (let i = 0; i < polySize; i++) {
          const j = (i + 1) % polySize;
          const v0 = polyIndexList[polyStart + i];
          const v1 = polyIndexList[polyStart + j];

          edgeLookupInsertEdge(edgeLookup, v0, v1);
        }

        polyStart += polySize;
      }
    }

    if (uvIds.length !== polyIndexList.length) {
      throw new Error('buildHardTangentEdgesFromMesh: uvIds.length ' + uvIds.length +
                      ' != polyIndexList.length ' + polyIndexList.length);
    }

    const numEdges = edgeLookup.m_edgeNumFaces.length;
    const lhsUvVec = new Array(numEdges);
    const rhsUvVec = new Array(numEdges);
    for (let i = 0; i < numEdges; i++) { lhsUvVec[i] = -1; rhsUvVec[i] = -1; }

    {
      let polyStart = 0;
      for (let polyIter = 0; polyIter < polySizes.length; polyIter++) {
        const polySize = polySizes[polyIter];

        for (let i = 0; i < polySize; i++) {
          const j = (i + 1) % polySize;
          let v0 = polyIndexList[polyStart + i];
          let v1 = polyIndexList[polyStart + j];

          const foundEdge = edgeLookupFetchEdge(edgeLookup, v0, v1);
          if (foundEdge < 0) throw new Error('buildHardTangentEdgesFromMesh: edge not found');

          const lhsVtx = edgeLookup.m_vertLhs[foundEdge];
          const rhsVtx = edgeLookup.m_vertRhs[foundEdge];

          let uv0 = uvIds[polyStart + i];
          let uv1 = uvIds[polyStart + j];

          if (!(uv0 >= 0)) throw new Error('buildHardTangentEdgesFromMesh: uv0 < 0');
          if (!(uv1 >= 0)) throw new Error('buildHardTangentEdgesFromMesh: uv1 < 0');

          if (v0 > v1) {
            const tv = v0; v0 = v1; v1 = tv;
            const tu = uv0; uv0 = uv1; uv1 = tu;
          }
          if (lhsVtx !== v0) throw new Error('buildHardTangentEdgesFromMesh: lhsVtx != v0');
          if (rhsVtx !== v1) throw new Error('buildHardTangentEdgesFromMesh: rhsVtx != v1');

          applySwapToVector(lhsUvVec, foundEdge, uv0);
          applySwapToVector(rhsUvVec, foundEdge, uv1);
        }

        polyStart += polySize;
      }
    }

    for (let i = 0; i < numEdges; i++) {
      const lhsUv = lhsUvVec[i];
      const rhsUv = rhsUvVec[i];

      if (lhsUv === -1) throw new Error('buildHardTangentEdgesFromMesh: lhsUv[' + i + '] == -1');
      if (rhsUv === -1) throw new Error('buildHardTangentEdgesFromMesh: rhsUv[' + i + '] == -1');

      if (lhsUv === -2 || rhsUv === -2) {
        const lhsVtx = edgeLookup.m_vertLhs[i];
        const rhsVtx = edgeLookup.m_vertRhs[i];

        hardEdgeTangentListLhs.push(lhsVtx);
        hardEdgeTangentListRhs.push(rhsVtx);
      }
    }
  }
}

// =====================================================================
// DcciMeshSubdivTopo (DcciSubdiv.h:1291) -- minimal factory matching
// the fields BuildEdgeLookup + downstream consume.
// =====================================================================
function makeDcciMeshSubdivTopo() {
  return {
    m_numVerts:                0,
    m_tanUvIndex:              0,
    m_polySizes:               [],
    m_polyStarts:              [],
    m_polyIndexList:           [],
    m_creaseEdgeListLhs:       [],
    m_creaseEdgeListRhs:       [],
    m_creaseEdgeWeight:        [],
    m_creaseVertexList:        [],
    m_creaseVertexWeight:      [],
    m_hardEdgeNormalListLhs:   [],
    m_hardEdgeNormalListRhs:   [],
    m_hardEdgeTangentListLhs:  [],
    m_hardEdgeTangentListRhs:  [],
  };
}

// =====================================================================
// makeSubdivTopoFromMeshVtx: line-by-line port of
// DcciSubdiv::MakeSubdivTopoFromMeshVtx (DcciSubdiv.cpp:2040). Copies
// the AmoliteMesh-shaped input into a DcciMeshSubdivTopo (which is
// what ApplyCatmullClarkToWeights -> BuildCatmullClarkTopoHelper
// consumes) AND synthesizes the hard-tangent edges from UV seams.
// =====================================================================
function makeSubdivTopoFromMeshVtx(dstTopo, srcMesh, tangentUvIndex) {
  // Reset
  dstTopo.m_numVerts                = 0;
  dstTopo.m_tanUvIndex              = 0;
  dstTopo.m_polySizes.length        = 0;
  dstTopo.m_polyStarts.length       = 0;
  dstTopo.m_polyIndexList.length    = 0;
  dstTopo.m_creaseEdgeListLhs.length= 0;
  dstTopo.m_creaseEdgeListRhs.length= 0;
  dstTopo.m_creaseEdgeWeight.length = 0;
  dstTopo.m_creaseVertexList.length = 0;
  dstTopo.m_creaseVertexWeight.length = 0;
  dstTopo.m_hardEdgeNormalListLhs.length = 0;
  dstTopo.m_hardEdgeNormalListRhs.length = 0;
  dstTopo.m_hardEdgeTangentListLhs.length = 0;
  dstTopo.m_hardEdgeTangentListRhs.length = 0;

  dstTopo.m_tanUvIndex = tangentUvIndex;

  // m_posData isn't carried into the topo struct -- it stays on the
  // AmoliteMesh. We pass posData around separately at the apply step.
  dstTopo.m_numVerts = srcMesh.m_posData.length / 3;   // posData is flat [x,y,z,...]

  for (let i = 0; i < srcMesh.m_polySizes.length; i++) dstTopo.m_polySizes.push(srcMesh.m_polySizes[i]);
  dstTopo.m_polyStarts = calcPrefixSumVec(srcMesh.m_polySizes);
  for (let i = 0; i < srcMesh.m_polyIndexList.length; i++) dstTopo.m_polyIndexList.push(srcMesh.m_polyIndexList[i]);

  for (let i = 0; i < srcMesh.m_creaseEdgeListLhs.length; i++) dstTopo.m_creaseEdgeListLhs.push(srcMesh.m_creaseEdgeListLhs[i]);
  for (let i = 0; i < srcMesh.m_creaseEdgeListRhs.length; i++) dstTopo.m_creaseEdgeListRhs.push(srcMesh.m_creaseEdgeListRhs[i]);
  for (let i = 0; i < srcMesh.m_creaseEdgeWeight.length; i++) dstTopo.m_creaseEdgeWeight.push(srcMesh.m_creaseEdgeWeight[i]);

  for (let i = 0; i < srcMesh.m_creaseVertexList.length; i++) dstTopo.m_creaseVertexList.push(srcMesh.m_creaseVertexList[i]);
  for (let i = 0; i < srcMesh.m_creaseVertexWeight.length; i++) dstTopo.m_creaseVertexWeight.push(srcMesh.m_creaseVertexWeight[i]);

  // amolite uses m_hardEdgeListLhs / m_hardEdgeListRhs from AmoliteMesh
  // for the normal-side hard edges. Our JS AmoliteMesh-like input
  // uses the same names.
  for (let i = 0; i < srcMesh.m_hardEdgeListLhs.length; i++) dstTopo.m_hardEdgeNormalListLhs.push(srcMesh.m_hardEdgeListLhs[i]);
  for (let i = 0; i < srcMesh.m_hardEdgeListRhs.length; i++) dstTopo.m_hardEdgeNormalListRhs.push(srcMesh.m_hardEdgeListRhs[i]);

  buildHardTangentEdgesFromMesh(dstTopo.m_hardEdgeTangentListLhs,
                                dstTopo.m_hardEdgeTangentListRhs,
                                srcMesh, tangentUvIndex);
}

// =====================================================================
// decompressCreaseSharpness: line-by-line port of
// DecompressCreaseSharpness (DcciSubdiv.cpp:1431). Resolves per-crease
// vertex pairs into a flat per-edge sharpness array, indexed by the
// edge IDs in the supplied EdgeLookup.
// =====================================================================
function decompressCreaseSharpness(edgeWeight, edgeLookup,
                                   creaseLhs, creaseRhs, creaseWeight) {
  const numCreases = creaseLhs.length;
  if (numCreases !== creaseRhs.length)    throw new Error('creaseLhs/creaseRhs len mismatch');
  if (numCreases !== creaseWeight.length) throw new Error('creaseLhs/creaseWeight len mismatch');

  edgeWeight.length = 0;
  for (let i = 0; i < edgeLookup.m_numEdges; i++) edgeWeight.push(0.0);

  for (let i = 0; i < numCreases; i++) {
    const lhs = creaseLhs[i];
    const rhs = creaseRhs[i];
    const weight = creaseWeight[i];

    const edgeId = edgeLookupFetchEdge(edgeLookup, lhs, rhs);
    if (edgeId < 0) {
      throw new Error('decompressCreaseSharpness: crease (' + lhs + ',' + rhs + ') has no edge');
    }
    edgeWeight[edgeId] = weight;
  }
}

// =====================================================================
// applyVertexAndBorderSharpness: line-by-line port of the
// `enableSemiSharp` block inside BuildCatmullClarkTopoHelper
// (DcciSubdiv.cpp:1533-1562). Populates helper.m_vertSharpness from
// vertCreaseIndex/Weight, then optionally boosts border edges to
// sharpness >= 1.
//
// Amolite's `enableSemiSharp` flag wraps both this block AND the
// preceding DecompressCreaseSharpness call. The two are split here
// for testability, but the caller is responsible for the conditional.
// =====================================================================
function applyVertexAndBorderSharpness(helper, vertCreaseIndex, vertCreaseWeight,
                                       bordersAsSharp) {
  // helper.m_vertSharpness already needs to be at length numVerts and
  // filled with zero before assignment. amolite's `.resize(numVerts)`
  // default-initializes float to 0.
  helper.m_vertSharpness.length = 0;
  for (let i = 0; i < helper.m_numVerts; i++) helper.m_vertSharpness.push(0.0);

  const numCreaseVerts = vertCreaseIndex.length;
  if (vertCreaseWeight.length !== numCreaseVerts) {
    throw new Error('vertCreaseIndex/Weight length mismatch');
  }

  for (let i = 0; i < numCreaseVerts; i++) {
    const v = vertCreaseIndex[i];
    const w = vertCreaseWeight[i];
    helper.m_vertSharpness[v] = w;
  }

  if (bordersAsSharp) {
    for (let i = 0; i < helper.m_numEdges; i++) {
      const w = helper.m_edgeSharpness[i];
      const isBorder = helper.m_edgeIsBorder[i];
      if (isBorder) {
        helper.m_edgeSharpness[i] = Math.max(w, 1.0);
      }
    }
  }
}

// ---- CatmullClarkPatches::VertexInfo (DcciSubdiv.h:630) -----------
function makeVertexInfo() {
  return {
    m_valence:                0,
    m_onlyQuads:              false,
    m_isNonManifold:          false,
    m_isBorder:               false,
    m_isIrregular:            false,
    m_numWedges:              0,
    m_numIrregularWedges:     0,
    m_irregularSubdivLevel:   0,
    m_dstIrregularIndex:      0,
    m_vertexSharpness:        0.0,
    m_limitStartIndex:        0,
  };
}

// ---- CatmullClarkPatches container (partial -- only the fields
// the ported functions need so far). -------------------------------
function makeCatmullClarkPatches() {
  return {
    m_vertexInfo:                [],
    m_polyInfo:                  [],
    m_regionData:                [],   // RegionInfo[]   (Phase 1b.12a)
    m_irregularWedgeInfo:        [],   // IrregularWedgeInfo[] (Phase 1b.12b)
    m_regularVertexIndices:      [],   // ControlPoint[] (Phase 1b.12d)
    m_irregularVertexIndices:    [],   // ControlPoint[] (Phase 1b.12e)
    m_weightInflData:            null, // RuntimeWeightInflData (Phase 1b.12f)
    m_controlPointRedirect:      null, // ControlPointRedirect  (Phase 1b.12f)
    m_vertexLimitInfo:           null, // VertexLimitInfo (Phase 1b.13e)
    m_numIrregularVertexWedges:  -1,
    m_numRegularPoints:          -1,
    m_numLimitPoints:            -1,
    m_numIrregularTriangles:     -1,
  };
}
// Port of CatmullClarkPatches::VertexLimitInfo (DcciSubdiv.h:1074).
function makeVertexLimitInfo() {
  return {
    m_limitDataStart:     [],
    m_limitDataNum:       [],
    m_limitInfluenceData: [],
    m_limitWeightDataP:   [],
    m_limitWeightDataT0:  [],
    m_limitWeightDataT1:  [],
  };
}
// Port of CatmullClarkPatches::GetPolyDimControlPoints (DcciSubdiv.cpp:3621).
function getPolyDimControlPoints(type) {
  if (type === kSubdivPolyType_QuadRegular)    return 4;
  if (type === kSubdivPolyType_QuadIrregular1) return 5;
  if (type === kSubdivPolyType_QuadIrregular2) return 7;
  if (type === kSubdivPolyType_Triangle)       return -1;  // amolite leaves uninit
  throw new Error('getPolyDimControlPoints: bad type ' + type);
}
// Port of CatmullClarkPatches::RuntimeWeightInflData (DcciSubdiv.h:1098).
function makeRuntimeWeightInflData() {
  return {
    m_numBaseCps:                -1,
    m_numCpItems:                -1,
    m_numSubdiv0:                -1,
    m_numSubdiv1:                -1,
    m_numSubdiv2:                -1,
    m_numIrregularPoints:        -1,
    m_numRegularPoints:          -1,
    m_cpItemStart:               [],
    m_cpItemCount:               [],
    m_inflData:                  [],
    m_weightData:                [],
    m_regularAndIrregularPoints: [],
    m_runtimeIrregularInfo:      [],
    m_irregularEdgeSharpness:    [],
    m_irregularGroupStart:       [],
    m_irregularGroupNum:         [],
  };
}
// Port of CatmullClarkPatches::ControlPointRedirect (DcciSubdiv.h:1054).
function makeControlPointRedirect() {
  return {
    m_regularCps:   [],
    m_irregularCps: [],
  };
}

// Port of RuntimeWeightInflData::FetchRegularIndex (DcciSubdiv.cpp:4833).
function runtimeWeightInflDataFetchRegularIndex(w, ptIndex) {
  return w.m_regularAndIrregularPoints[ptIndex];
}
// Port of RuntimeWeightInflData::FetchIrregularIndex (DcciSubdiv.cpp:4838).
function runtimeWeightInflDataFetchIrregularIndex(w, ptIndex) {
  return w.m_regularAndIrregularPoints[w.m_numRegularPoints + ptIndex];
}
// Port of RuntimeWeightInflData::FetchRegularFlatVertex (DcciSubdiv.cpp:4843).
// For base CPs returns an identity stencil; for non-base unique CPs
// unpacks the (start, count) slice into a fresh FlatWeightVertex.
function runtimeWeightInflDataFetchRegularFlatVertex(w, ptIndex) {
  if (ptIndex < w.m_numBaseCps) {
    return makeFlatWeightVertexValId(1.0, ptIndex);
  }
  const cpIndex = ptIndex - w.m_numBaseCps;
  const start   = w.m_cpItemStart[cpIndex];
  const count   = w.m_cpItemCount[cpIndex];
  const dstInfl = new Array(count);
  const dstW    = new Array(count);
  for (let i = 0; i < count; i++) {
    dstInfl[i] = w.m_inflData[start + i];
    dstW   [i] = w.m_weightData[start + i];
  }
  // amolite asserts strict sortedness; mirror.
  for (let i = 0; i < count - 1; i++) {
    if (!(dstInfl[i] < dstInfl[i + 1])) {
      throw new Error('runtimeWeightInflDataFetchRegularFlatVertex: influences not strictly sorted at ' + i);
    }
  }
  return { m_influences: dstInfl, m_weights: dstW };
}

// ---- CatmullClarkPatches::IrregularWedgeInfo (DcciSubdiv.h:720) ---
function makeIrregularWedgeInfo() {
  return {
    m_centerVertex:               -1,
    m_centerTrianglePoly:         -1,
    m_centerTriangleLimitOffset:  -1,
    m_localWedgeIndex:            -1,
    m_subdivLevel:                -1,
    m_valence:                    -1,
    m_isCircular:                 false,
    m_irregularVertexStart:       -1,
    m_irregularVertexNum:         -1,
  };
}

// ---- CatmullClarkPatches::RegionInfo (DcciSubdiv.h:827) -----------
const kRegionScale_00   = 0;
const kRegionScale_01   = 1;
const kRegionScale_10   = 2;
const kRegionScale_11   = 3;
const kRegionScale_Full = 4;
const kRegionScale_Num  = 5;

function makeRegionInfo() {
  return {
    m_irregularIndex:        -1,
    m_irregularPolyIndex:    -1,
    m_irregularSubdivLevel:  -1,
    m_rotation:              -1,
    m_irregularRegionScale:  -1,
  };
}
function regionInfoEquals(a, b) {
  return a.m_irregularIndex       === b.m_irregularIndex &&
         a.m_irregularPolyIndex   === b.m_irregularPolyIndex &&
         a.m_irregularSubdivLevel === b.m_irregularSubdivLevel &&
         a.m_rotation             === b.m_rotation &&
         a.m_irregularRegionScale === b.m_irregularRegionScale;
}

// ---- RegionGrid factory + methods (DcciSubdiv.h:661) --------------
function makeRegionGrid() {
  return {
    m_regionMask:      0,    // ~0u in C++; cleared below by buildCalcQuadRegionData
    m_regionDataStart: -1,
    m_regionDataNum:   -1,
    m_pointsStart:     -1,
    m_pointsNum:       -1,
  };
}
function regionGridIsRegionSet(rg, regionGridIndex) {
  return (rg.m_regionMask & (1 << regionGridIndex)) !== 0;
}
function regionGridGetRegionOffset(rg, regionGridIndex) {
  if (!regionGridIsRegionSet(rg, regionGridIndex)) return -1;
  const maskBelow = ((1 << regionGridIndex) - 1) >>> 0;
  const bitsBelow = (maskBelow & rg.m_regionMask) >>> 0;
  return countBits(bitsBelow);
}

// ---- IntVec2 (used for grid positions) ---------------------------
function intVec2(x, y) { return { x, y }; }

// ---- eSubdivPolyType (DcciSubdiv.h:647) ---------------------------
const kSubdivPolyType_QuadRegular    = 0;
const kSubdivPolyType_QuadIrregular1 = 1;
const kSubdivPolyType_QuadIrregular2 = 2;
const kSubdivPolyType_Triangle       = 3;

// Port of CatmullClarkPatches::GetPolySubdivLevel (DcciSubdiv.cpp:3550).
function getPolySubdivLevel(type) {
  switch (type) {
    case kSubdivPolyType_QuadRegular:    return 0;
    case kSubdivPolyType_QuadIrregular1: return 1;
    case kSubdivPolyType_QuadIrregular2: return 2;
    case kSubdivPolyType_Triangle:       return 2;
    default: throw new Error('getPolySubdivLevel: bad type ' + type);
  }
}
// Port of CatmullClarkPatches::GetPolyNumRegions (DcciSubdiv.cpp:3574).
function getPolyNumRegions(type) {
  switch (type) {
    case kSubdivPolyType_QuadRegular:    return 1;
    case kSubdivPolyType_QuadIrregular1: return 4;
    case kSubdivPolyType_QuadIrregular2: return 16;
    case kSubdivPolyType_Triangle:       return 12;
    default: throw new Error('getPolyNumRegions: bad type ' + type);
  }
}

// ---- CatmullClarkPatches::PolyInfo (DcciSubdiv.h:689) -------------
function makePolyInfo() {
  return {
    m_type:             -1,   // (eSubdivPolyType)-1
    m_subdivLevel:      -1,
    m_numRegions:       -1,
    m_triCenterIndex:   -1,
    // m_quadRegionGrid populated by BuildCalcQuadRegionData (Phase 1b.12a)
    m_quadRegionGrid:   null, // makeRegionGrid() -- set in PolyInfo if needed
    // m_triRegionGrids[3] populated by BuildTriRegionData (Phase 1b.12c)
    m_triRegionGrids:   null,
    m_edgeSharpness:    [-1.0, -1.0, -1.0, -1.0],
  };
}

const SUBDIV_MAX_SHARPNESS = 10;

// =====================================================================
// buildCalcPolyInfo: line-by-line port of
// CatmullClarkPatches::BuildCalcPolyInfo (DcciSubdiv.cpp:3952).
// Classifies every polygon (Triangle / QuadRegular / QuadIrregular1 /
// QuadIrregular2), reads helper.m_edgeSharpness per polygon edge,
// applies the "non-2-faces => max sharpness" rule and the per-poly
// subdivLevel deduction, and stores results in patches.m_polyInfo[].
// =====================================================================
function buildCalcPolyInfo(patches, helper0) {
  const numPoly = helper0.m_numFaces;
  patches.m_polyInfo.length = 0;
  for (let i = 0; i < numPoly; i++) patches.m_polyInfo.push(makePolyInfo());

  let numTris = 0;

  for (let polyIter = 0; polyIter < numPoly; polyIter++) {
    const polySize  = helper0.m_polySizes[polyIter];
    const polyStart = helper0.m_polyStarts[polyIter];

    let maxIrregular = 0;
    for (let cornerIter = 0; cornerIter < polySize; cornerIter++) {
      const vtx = helper0.m_polyIndexList[polyStart + cornerIter];
      const vtxInfo = patches.m_vertexInfo[vtx];

      if (vtxInfo.m_irregularSubdivLevel > maxIrregular) {
        maxIrregular = vtxInfo.m_irregularSubdivLevel;
      }
    }

    const polyInfo = makePolyInfo();
    polyInfo.m_type = -1;

    if (polySize === 3) {
      polyInfo.m_type = kSubdivPolyType_Triangle;

      polyInfo.m_triCenterIndex = numTris;

      numTris++;
    } else {
      if (maxIrregular === 0) {
        polyInfo.m_type = kSubdivPolyType_QuadRegular;
      } else if (maxIrregular === 1) {
        polyInfo.m_type = kSubdivPolyType_QuadIrregular1;
      } else if (maxIrregular === 2) {
        polyInfo.m_type = kSubdivPolyType_QuadIrregular2;
      } else {
        throw new Error('buildCalcPolyInfo: unexpected maxIrregular ' + maxIrregular);
      }
    }

    polyInfo.m_numRegions  = getPolyNumRegions(polyInfo.m_type);
    polyInfo.m_subdivLevel = getPolySubdivLevel(polyInfo.m_type);

    for (let cornerIter = 0; cornerIter < polySize; cornerIter++) {
      const vtx0 = helper0.m_polyIndexList[polyStart + cornerIter];
      const vtx1 = helper0.m_polyIndexList[polyStart + (cornerIter + 1) % polySize];
      const edgeIndex = edgeLookupFetchEdge(helper0.m_edgeLookup, vtx0, vtx1);

      let baseSharpness = helper0.m_edgeSharpness[edgeIndex];

      const numPolysForEdge = helper0.m_edgeLookup.m_edgeNumFaces[edgeIndex];
      if (numPolysForEdge !== 2) {
        baseSharpness = SUBDIV_MAX_SHARPNESS;
      }

      if (baseSharpness < SUBDIV_MAX_SHARPNESS) {
        baseSharpness = Math.max(0.0, baseSharpness - polyInfo.m_subdivLevel);
      }

      if (!(0 <= baseSharpness && baseSharpness <= SUBDIV_MAX_SHARPNESS)) {
        throw new Error('buildCalcPolyInfo: baseSharpness ' + baseSharpness + ' out of range');
      }

      polyInfo.m_edgeSharpness[cornerIter] = baseSharpness;
    }

    patches.m_polyInfo[polyIter] = polyInfo;
  }

  patches.m_numIrregularTriangles = numTris;
}

// =====================================================================
// buildCalcVertexInfo: line-by-line port of
// CatmullClarkPatches::BuildCalcVertexInfo (DcciSubdiv.cpp:3777).
//
// Reads helper.m_wedges + helper.m_polySizes + helper.m_vertSharpness;
// writes (a) m_isWedgeIrregular on every VertexWedgeInfo and (b) the
// per-vertex VertexInfo entries in patches.m_vertexInfo[].
//
// IMPORTANT: preserves the "two-opposite-sharp = still regular"
// exemption (lines 3870-3882). That exemption is itself a bug --
// it's Fix 3 in AMOLITE_DEFERRED_FIXES.md. We keep it here so the
// JS output matches amolite until amolite's fix lands.
// =====================================================================
function buildCalcVertexInfo(patches, helper0) {
  let numIrregularWedgesAdded = 0;
  let numLimitPoints = 0;

  let numRegularVtx = 0;

  const numVerts = helper0.m_numVerts;
  patches.m_vertexInfo.length = 0;
  for (let i = 0; i < numVerts; i++) patches.m_vertexInfo.push(makeVertexInfo());

  for (let vtxIter = 0; vtxIter < numVerts; vtxIter++) {
    if (helper0.m_wedges.m_wedgeEdgeSize.length !== helper0.m_numVerts) {
      throw new Error('m_wedgeEdgeSize length mismatch');
    }

    const wedgeNum   = helper0.m_wedges.m_wedgeEdgeSize[vtxIter];
    const wedgeStart = helper0.m_wedges.m_wedgeEdgeStart[vtxIter];

    let isAnyIrregular = false;
    let isAnyTri = false;

    if (wedgeNum !== 1) {
      isAnyIrregular = true;
    }

    let numIrregularWedge = 0;

    let valence = 0;
    let isBorder = false;
    for (let wedgeIter = 0; wedgeIter < wedgeNum; wedgeIter++) {
      let isWedgeIrregular = false;
      const wedgeInfo = helper0.m_wedges.m_wedgeEdgeInfo[wedgeStart + wedgeIter];

      const edgeNum   = wedgeInfo.m_wedgeEdgeNum;
      const edgeStart = wedgeInfo.m_wedgeEdgeStart;

      const foundSharpEdges = [];

      let numActualPolys = -1;
      if (wedgeInfo.m_isCircular) {
        numActualPolys = edgeNum;

        // for full circular, any valence other than 4 is irregular
        if (edgeNum !== 4) {
          isWedgeIrregular = true;
        }
      } else {
        // last "edge" is the second border of the wedge with an invalid poly
        numActualPolys = edgeNum - 1;
        isBorder = true;

        const lastCornerInfo = helper0.m_wedges.m_wedgeCornerData[edgeStart + edgeNum - 1];
        if (!(lastCornerInfo.m_polyIndex < 0)) {
          throw new Error('open-wedge last corner should have polyIndex < 0');
        }

        // anything other than 2 is irregular
        if (numActualPolys !== 2) {
          isWedgeIrregular = true;
        }
      }

      valence += numActualPolys;
      for (let edgeIter = 0; edgeIter < numActualPolys; edgeIter++) {
        const cornerInfo = helper0.m_wedges.m_wedgeCornerData[edgeStart + edgeIter];
        if (cornerInfo.m_creaseSharpness > 0.0) {
          foundSharpEdges.push(edgeIter);
        }

        if (!(cornerInfo.m_polyIndex >= 0)) {
          throw new Error('non-last wedge corner has polyIndex < 0');
        }
        const polySize = helper0.m_polySizes[cornerInfo.m_polyIndex];

        if (polySize !== 3 && polySize !== 4) {
          throw new Error('poly size ' + polySize + ' (only 3 or 4 supported)');
        }

        if (polySize === 3) {
          isAnyTri = true;
        }
      }

      // AMOLITE_DEFERRED_FIXES.md Fix 3 applied: any sharp incident
      // edge marks the wedge irregular. The previous exemption
      // ("2 opposite sharp edges at valence-4 = regular") was
      // topologically true but produced wrong limit positions at
      // patches near crease-border junctions in single_crease --
      // QuadRegular's single sub-region uses smooth bicubic stencils
      // instead of semi-sharp stencils, causing patch 20 to disagree
      // with patches 0/1/21 on v22's limit and tearing the surface.
      //
      // Promoting to QuadIrregular1 (subdivLevel >= 1) splits the
      // patch into 4 sub-regions so each touches at most one sharp
      // edge -- the semi-sharp basis applies correctly and all
      // adjacent patches agree at shared verts.
      if (foundSharpEdges.length > 0) {
        isWedgeIrregular = true;
      }

      if (isAnyTri) {
        isWedgeIrregular = true;
      }

      if (isWedgeIrregular) {
        isAnyIrregular = true;
        numIrregularWedge++;
      }

      wedgeInfo.m_isWedgeIrregular = isWedgeIrregular;
    }

    const dstVertexInfo = makeVertexInfo();

    dstVertexInfo.m_valence              = valence;
    dstVertexInfo.m_onlyQuads            = !isAnyTri;
    dstVertexInfo.m_numWedges            = wedgeNum;
    dstVertexInfo.m_isIrregular          = isAnyIrregular;
    dstVertexInfo.m_numIrregularWedges   = numIrregularWedge;
    dstVertexInfo.m_isNonManifold        = (wedgeNum !== 1);
    dstVertexInfo.m_isBorder             = isBorder;

    if (!dstVertexInfo.m_onlyQuads) {
      dstVertexInfo.m_irregularSubdivLevel = 2;
    } else if (isAnyIrregular) {
      dstVertexInfo.m_irregularSubdivLevel = 1;
    } else {
      dstVertexInfo.m_irregularSubdivLevel = 0;
    }

    dstVertexInfo.m_dstIrregularIndex = -1;
    if (dstVertexInfo.m_irregularSubdivLevel >= 1) {
      dstVertexInfo.m_dstIrregularIndex = numIrregularWedgesAdded;
      numIrregularWedgesAdded += numIrregularWedge;
    }

    dstVertexInfo.m_vertexSharpness = helper0.m_vertSharpness[vtxIter];

    dstVertexInfo.m_limitStartIndex = numLimitPoints;
    numLimitPoints += wedgeNum;

    if (dstVertexInfo.m_irregularSubdivLevel === 0) {
      numRegularVtx++;
    }

    patches.m_vertexInfo[vtxIter] = dstVertexInfo;
  }

  patches.m_numIrregularVertexWedges = numIrregularWedgesAdded;
  patches.m_numRegularPoints         = numRegularVtx;

  patches.m_numLimitPoints = -1;
}

// =====================================================================
// Border detection -- inline block in BuildCatmullClarkTopoHelper
// (DcciSubdiv.cpp:~1500). Sets per-vertex / per-edge isBorder flags
// from the edge face-count.
// =====================================================================
function detectBorders(helper) {
  helper.m_vertIsBorder.length = 0;
  helper.m_edgeIsBorder.length = 0;

  for (let i = 0; i < helper.m_numVerts; i++) helper.m_vertIsBorder.push(false);
  for (let i = 0; i < helper.m_numEdges; i++) helper.m_edgeIsBorder.push(false);

  // an edge is a border if it only has one face attached to it
  for (let i = 0; i < helper.m_numEdges; i++) {
    const numFaces = helper.m_edgeLookup.m_edgeNumFaces[i];
    if (numFaces === 1) {
      helper.m_edgeIsBorder[i] = true;

      const v0 = helper.m_edgeLookup.m_vertLhs[i];
      const v1 = helper.m_edgeLookup.m_vertRhs[i];

      helper.m_vertIsBorder[v0] = true;
      helper.m_vertIsBorder[v1] = true;
    }
  }
}

// =====================================================================
// FlatWeightVertex (DcciSubdiv.h:217) + AddDirect / MulDirect ops
// (DcciSubdiv.cpp:1807, 1853). Sparse representation of a subdivided
// point as a weighted sum of source positions. Influence list is
// kept sorted by influence id -- AddDirect's merge step depends on it.
// =====================================================================
function makeFlatWeightVertex() {
  return { m_weights: [], m_influences: [] };
}
// Port of FlatWeightVertex(float val, int id) ctor (DcciSubdiv.h:233).
function makeFlatWeightVertexValId(val, id) {
  return { m_weights: [val], m_influences: [id] };
}
function flatWeightVertexGetNum(fwv) {
  if (fwv.m_weights.length !== fwv.m_influences.length) {
    throw new Error('FWV: m_weights/m_influences length mismatch');
  }
  return fwv.m_weights.length;
}
// Port of DcciSubdiv::AddDirect (DcciSubdiv.cpp:1807). Merge-sort by
// influence id, summing weights for shared influences. Math.fround on
// the per-slot sum matches amolite's f32 accumulation (m_weights is
// amolite::vector<float>).
function flatWeightVertexAddDirect(lhs, rhs) {
  const fr = Math.fround;
  const numLhs = flatWeightVertexGetNum(lhs);
  const numRhs = flatWeightVertexGetNum(rhs);
  let dstNum = 0;
  {
    let cL = 0, cR = 0;
    while (cL < numLhs || cR < numRhs) {
      const nL = (cL < numLhs) ? lhs.m_influences[cL] : Number.MAX_SAFE_INTEGER;
      const nR = (cR < numRhs) ? rhs.m_influences[cR] : Number.MAX_SAFE_INTEGER;
      const nv = (nL < nR) ? nL : nR;
      if (nv === nL) cL++;
      if (nv === nR) cR++;
      dstNum++;
    }
  }

  const ret = { m_weights: new Array(dstNum), m_influences: new Array(dstNum) };
  for (let i = 0; i < dstNum; i++) { ret.m_weights[i] = -1.0; ret.m_influences[i] = -1; }

  let currLhs = 0, currRhs = 0, index = 0;
  while (currLhs < numLhs || currRhs < numRhs) {
    const nextLhs = (currLhs < numLhs) ? lhs.m_influences[currLhs] : Number.MAX_SAFE_INTEGER;
    const nextRhs = (currRhs < numRhs) ? rhs.m_influences[currRhs] : Number.MAX_SAFE_INTEGER;

    const nextVal = (nextLhs < nextRhs) ? nextLhs : nextRhs;

    const inflLhs = (nextLhs === nextVal) ? fr(lhs.m_weights[currLhs]) : 0.0;
    const inflRhs = (nextRhs === nextVal) ? fr(rhs.m_weights[currRhs]) : 0.0;

    ret.m_influences[index] = nextVal;
    ret.m_weights[index]    = fr(inflLhs + inflRhs);

    if (nextVal === nextLhs) currLhs++;
    if (nextVal === nextRhs) currRhs++;

    index++;
  }
  return ret;
}
// Port of DcciSubdiv::MulDirect (DcciSubdiv.cpp:1853). Math.fround on
// the per-slot multiply matches amolite's f32 arithmetic.
function flatWeightVertexMulDirect(lhs, rhs) {
  const fr = Math.fround;
  const rhsF = fr(rhs);
  const ret = { m_weights: new Array(lhs.m_weights.length), m_influences: lhs.m_influences.slice() };
  const n = ret.m_weights.length;
  for (let i = 0; i < n; i++) ret.m_weights[i] = fr(fr(lhs.m_weights[i]) * rhsF);
  return ret;
}

// Port of FlatWeightVertex::GetNewPairSizeFromIndices (DcciSubdiv.h:283).
// Merge-sort union count over two sorted int lists.
function flatWeightVertexGetNewPairSizeFromIndices(lhs, rhs) {
  let numFound = 0;
  let cL = 0, cR = 0;
  const numL = lhs.length, numR = rhs.length;
  while (cL < numL || cR < numR) {
    const nextLhs = (cL < numL) ? lhs[cL] : Number.MAX_SAFE_INTEGER;
    const nextRhs = (cR < numR) ? rhs[cR] : Number.MAX_SAFE_INTEGER;
    const nextVal = nextLhs < nextRhs ? nextLhs : nextRhs;
    if (nextVal === nextLhs) cL++;
    if (nextVal === nextRhs) cR++;
    numFound++;
  }
  return numFound;
}

// Port of FlatWeightVertex::NormalizeSignedRange (DcciSubdiv.cpp:1706).
// Scales m_weights[] by 1 / max(|w|). Despite the name it operates on
// the absolute max, not the signed range -- amolite's naming.
function flatWeightVertexNormalizeSignedRange(fwv) {
  let maxT = 0.0;
  for (let i = 0; i < fwv.m_weights.length; i++) {
    const w = Math.abs(fwv.m_weights[i]);
    if (w > maxT) maxT = w;
  }
  const scale = safeInv(maxT);
  for (let i = 0; i < fwv.m_weights.length; i++) {
    fwv.m_weights[i] *= scale;
  }
}

// Port of FlatWeightVertex::CompactInfluenceList (DcciSubdiv.cpp:1723).
// Removes (influence, weight) pairs where |weight| <= eps.
function flatWeightVertexCompactInfluenceList(fwv, eps) {
  if (eps === undefined) eps = 1e-5;
  let numNonZero = 0;
  for (let i = 0; i < fwv.m_weights.length; i++) {
    const w = fwv.m_weights[i];
    if (w < -eps || eps < w) numNonZero++;
  }
  if (fwv.m_influences.length !== numNonZero) {
    const retInfl = new Array(numNonZero);
    const retW    = new Array(numNonZero);
    let currNonZero = 0;
    for (let i = 0; i < fwv.m_weights.length; i++) {
      const w = fwv.m_weights[i];
      const idx = fwv.m_influences[i];
      if (w < -eps || eps < w) {
        retInfl[currNonZero] = idx;
        retW   [currNonZero] = w;
        currNonZero++;
      }
    }
    if (currNonZero !== numNonZero) {
      throw new Error('flatWeightVertexCompactInfluenceList: currNonZero != numNonZero');
    }
    fwv.m_influences = retInfl;
    fwv.m_weights    = retW;
  }
}

// Port of FlatWeightVertex::MergeIndexList (DcciSubdiv.cpp:1765). Merge
// srcList in-place with otherList; result is the sorted union (dedup
// of equal entries via "advance both" rule).
function flatWeightVertexMergeIndexList(srcList, otherList) {
  const dstNum = flatWeightVertexGetNewPairSizeFromIndices(srcList, otherList);
  const ret = new Array(dstNum).fill(-1);
  let cL = 0, cR = 0;
  const numL = srcList.length, numR = otherList.length;
  let index = 0;
  while (cL < numL || cR < numR) {
    const nextLhs = (cL < numL) ? srcList  [cL] : Number.MAX_SAFE_INTEGER;
    const nextRhs = (cR < numR) ? otherList[cR] : Number.MAX_SAFE_INTEGER;
    const nextVal = nextLhs < nextRhs ? nextLhs : nextRhs;
    ret[index] = nextVal;
    if (nextVal === nextLhs) cL++;
    if (nextVal === nextRhs) cR++;
    index++;
  }
  // Copy back into srcList (amolite assigns the new vector by value).
  srcList.length = 0;
  for (let i = 0; i < ret.length; i++) srcList.push(ret[i]);
}

// Port of FlatWeightVertex::Normalize (DcciSubdiv.h:254). Divides
// each weight by the sum.
function flatWeightVertexNormalize(fwv) {
  let sum = 0.0;
  const n = flatWeightVertexGetNum(fwv);
  for (let i = 0; i < n; i++) sum += fwv.m_weights[i];
  // amolite uses SafeInv(sum) -- returns 0 if sum is 0, else 1/sum.
  const scale = (sum !== 0.0) ? (1.0 / sum) : 0.0;
  for (let i = 0; i < n; i++) fwv.m_weights[i] *= scale;
}

// =====================================================================
// "Point ops" objects -- the JS port emulates amolite's C++ template
// pattern via a small ops table. Each ApplySubdivisionTo* takes an
// `ops` object so the same function works for both Vec3 (used by
// ApplyWeightsToPoints) and FlatWeightVertex (used by
// ApplyCatmullClarkToWeights).
// =====================================================================
const Vec3Ops = {
  zero:        () => [0.0, 0.0, 0.0],
  copy:        (a) => [a[0], a[1], a[2]],
  add:         (a, b) => [a[0]+b[0], a[1]+b[1], a[2]+b[2]],
  scale:       (a, s) => [a[0]*s, a[1]*s, a[2]*s],
  // a * s + b * 0 -- expression amolite spells (a * s)
  // amolite C++ supports (a + b), (a * s), (s * a) inline. JS uses
  // these helpers.
};
const FlatWeightVertexOps = {
  zero:        () => makeFlatWeightVertex(),
  copy:        (a) => ({ m_weights: a.m_weights.slice(), m_influences: a.m_influences.slice() }),
  add:         (a, b) => flatWeightVertexAddDirect(a, b),
  scale:       (a, s) => flatWeightVertexMulDirect(a, s),
};

// =====================================================================
// buildIndexList: line-by-line port of BuildIndexList
// (DcciSubdiv.cpp:180). Produces dstPolySizes (all 4 -- the
// subdivided mesh is quads) and dstPolyIndexList using the [vertices,
// edges, faces] ordering for the new vertex array.
// =====================================================================
function buildIndexList(dstPolySizes, dstPolyIndexList, helper) {
  dstPolySizes.length = 0;
  dstPolyIndexList.length = 0;

  const numFaces = helper.m_polySizes.length;
  const numEdges = helper.m_edgeLookup.m_numEdges;

  // the order of the vertex array is: [vertices, edges, faces]
  const startVert = 0;
  const startEdge = helper.m_numVerts;
  const startFace = helper.m_numVerts + numEdges;

  let polyStart = 0;
  for (let polyIter = 0; polyIter < helper.m_polySizes.length; polyIter++) {
    const polySize = helper.m_polySizes[polyIter];

    for (let iter = 0; iter < polySize; iter++) {
      const i = (iter + (polySize - 1)) % polySize;
      const j = (iter + (polySize + 0)) % polySize;
      const k = (iter + (polySize + 1)) % polySize;
      const v0 = helper.m_polyIndexList[polyStart + i];
      const v1 = helper.m_polyIndexList[polyStart + j];
      const v2 = helper.m_polyIndexList[polyStart + k];

      const edge0 = edgeLookupFetchEdge(helper.m_edgeLookup, v0, v1);
      const edge1 = edgeLookupFetchEdge(helper.m_edgeLookup, v1, v2);
      if (edge0 < 0) throw new Error('buildIndexList: edge0 not found');
      if (edge1 < 0) throw new Error('buildIndexList: edge1 not found');

      const dstPts = [
        startFace + polyIter,
        startEdge + edge0,
        startVert + v1,
        startEdge + edge1,
      ];

      dstPolySizes.push(4);
      dstPolyIndexList.push(dstPts[0]);
      dstPolyIndexList.push(dstPts[1]);
      dstPolyIndexList.push(dstPts[2]);
      dstPolyIndexList.push(dstPts[3]);
    }

    polyStart += polySize;
  }
}

// =====================================================================
// applySubdivisionToFacePts: port of ApplySubdivisionToFacePts
// (DcciSubdiv.cpp:992). Each face point = average of its corner
// positions.
// =====================================================================
function applySubdivisionToFacePts(facePts, polySizes, polyIndexList, posData, ops) {
  facePts.length = 0;
  const numFaces = polySizes.length;
  for (let i = 0; i < numFaces; i++) facePts.push(ops.zero());

  let polyStart = 0;
  for (let polyIter = 0; polyIter < polySizes.length; polyIter++) {
    const polySize = polySizes[polyIter];

    if (polySize > 0) {
      // start at first one to avoid issues with skinning accumulation
      let sumP = ops.copy(posData[polyIndexList[polyStart]]);

      for (let i = 1; i < polySize; i++) {
        const v = polyIndexList[polyStart + i];
        const P = posData[v];
        sumP = ops.add(sumP, P);
      }

      const avgP = ops.scale(sumP, 1.0 / polySize);
      facePts[polyIter] = avgP;
    }
    polyStart += polySize;
  }
}

// =====================================================================
// applySubdivisionToEdgePtsSemiSharp: port of
// ApplySubdivisionToEdgePtsSemiSharp (DcciSubdiv.cpp:1080).
// Per-edge subdivided point with semi-sharp blend:
//   - 2-face interior edge: lerp(smooth=(P0+P1+F0+F1)/4, sharp=(P0+P1)/2, sat(sharpness))
//   - boundary / non-manifold: sharp rule only
// =====================================================================
function applySubdivisionToEdgePtsSemiSharp(edgePts, edgeLookup, posData, facePts,
                                            edgeSharpness, tessellate, ops) {
  edgePts.length = 0;
  const numEdges = edgeLookup.m_numEdges;
  for (let i = 0; i < numEdges; i++) edgePts.push(ops.zero());

  for (let edgeIter = 0; edgeIter < numEdges; edgeIter++) {
    const numFaces = edgeLookup.m_edgeNumFaces[edgeIter];

    if (numFaces === 2 && !tessellate) {
      const sharpness = edgeSharpness[edgeIter];

      const v0 = edgeLookup.m_vertLhs[edgeIter];
      const v1 = edgeLookup.m_vertRhs[edgeIter];

      const P0 = posData[v0];
      const P1 = posData[v1];

      const poly0 = edgeLookupGetFaceForEdge(edgeLookup, edgeIter, 0);
      const poly1 = edgeLookupGetFaceForEdge(edgeLookup, edgeIter, 1);

      const F0 = facePts[poly0.m_polyIndex];
      const F1 = facePts[poly1.m_polyIndex];

      const sharpP  = ops.scale(ops.add(P0, P1), 0.5);
      const smoothP = ops.scale(ops.add(ops.add(P0, P1), ops.add(F0, F1)), 0.25);

      // Saturate(sharpness)
      let t = sharpness;
      if (t < 0.0) t = 0.0; else if (t > 1.0) t = 1.0;

      const dstP = ops.add(ops.scale(smoothP, 1.0 - t), ops.scale(sharpP, t));

      edgePts[edgeIter] = dstP;
    } else {
      // border or non-manifold -- sharp rule only
      const v0 = edgeLookup.m_vertLhs[edgeIter];
      const v1 = edgeLookup.m_vertRhs[edgeIter];

      const P0 = posData[v0];
      const P1 = posData[v1];

      const dstP = ops.scale(ops.add(P0, P1), 0.5);
      edgePts[edgeIter] = dstP;
    }
  }
}

// =====================================================================
// getCenterVertexEdgeSharpnessInfoHelper: port of
// GetCenterVertexEdgeSharpnessInfoHelper (DcciSubdiv.cpp:7442).
// Returns the three blend weights {smooth, edge, corner} for the
// vertex-point semi-sharp rule.
// =====================================================================
function getCenterVertexEdgeSharpnessInfoHelper(secondHighestEdgeSharpness,
                                                 thirdHighestEdgeSharpness,
                                                 centerVertexSharpness,
                                                 iteration) {
  function sat(x) { return x < 0.0 ? 0.0 : (x > 1.0 ? 1.0 : x); }

  const secondInfl = sat((secondHighestEdgeSharpness >= SUBDIV_MAX_SHARPNESS) ? 1.0 : (secondHighestEdgeSharpness - iteration));
  const thirdInfl  = sat((thirdHighestEdgeSharpness  >= SUBDIV_MAX_SHARPNESS) ? 1.0 : (thirdHighestEdgeSharpness  - iteration));

  const vertexInfl = sat((centerVertexSharpness >= SUBDIV_MAX_SHARPNESS) ? 1.0 : (centerVertexSharpness - iteration));

  const cornerInfl = (thirdInfl > vertexInfl) ? thirdInfl : vertexInfl;
  const edgeInfl   = secondInfl;

  return {
    dstWeightCorner: cornerInfl,
    dstWeightEdge:   (1.0 - cornerInfl) * edgeInfl,
    dstWeightSmooth: (1.0 - cornerInfl) * (1.0 - edgeInfl),
  };
}

// =====================================================================
// applySubdivisionToVertPtsSemiSharp: port of
// ApplySubdivisionToVertPtsSemiSharp (DcciSubdiv.cpp:1204). The CC
// vertex point rule with semi-sharp blending between smooth/edge/corner.
// =====================================================================
function applySubdivisionToVertPtsSemiSharp(vertPts, adjVertData, adjFaceData,
                                            posData, facePts, numVerts,
                                            edgeLookup, edgeSharpness, vertSharpness,
                                            lockBorderVerts, vertIsBorder, tessellate,
                                            ops) {
  vertPts.length = 0;
  for (let i = 0; i < numVerts; i++) vertPts.push(ops.zero());

  for (let vertIndex = 0; vertIndex < numVerts; vertIndex++) {
    const vertP = posData[vertIndex];

    const numAdjVerts = adjVertData.m_vertAdjSize[vertIndex];
    const numAdjPolys = adjFaceData.m_vertAdjSize[vertIndex];

    if (numAdjVerts === 0 || numAdjPolys === 0) {
      vertPts[vertIndex] = ops.copy(vertP);
    } else {
      const startV = adjVertData.m_vertAdjStart[vertIndex];
      let sumEdgeP = ops.copy(posData[adjVertData.m_vertAdjData[startV + 0]]);

      for (let i = 1; i < numAdjVerts; i++) {
        const adjV = adjVertData.m_vertAdjData[startV + i];
        const adjP = posData[adjV];
        sumEdgeP = ops.add(sumEdgeP, adjP);
      }

      const startF = adjFaceData.m_vertAdjStart[vertIndex];
      let sumFaceP = ops.copy(facePts[adjFaceData.m_vertAdjData[startF + 0]]);

      for (let i = 1; i < numAdjPolys; i++) {
        const adjF = adjFaceData.m_vertAdjData[startF + i];
        const adjP = facePts[adjF];
        sumFaceP = ops.add(sumFaceP, adjP);
      }

      // find top three crease sharpness values touching this vert
      const topThreeSharpnessValues = [0.0, 0.0, 0.0];
      const topThreeSharpnessVerts  = [0,   0,   0];

      for (let i = 0; i < numAdjVerts; i++) {
        const adjV = adjVertData.m_vertAdjData[startV + i];
        const edgeId = edgeLookupFetchEdge(edgeLookup, vertIndex, adjV);
        let sharpnessValue = edgeSharpness[edgeId];

        const numFaces = edgeLookupGetNumFacesForEdge(edgeLookup, edgeId);
        const isHard   = edgeLookupGetEdgeIsHardNormal(edgeLookup, edgeId);

        if (numFaces !== 2 || isHard) {
          sharpnessValue = 10.0;
        }

        let sharpnessVertex = adjV;

        if (sharpnessValue > 0) {
          for (let s = 0; s < 3; s++) {
            if (sharpnessValue > topThreeSharpnessValues[s]) {
              const tv = topThreeSharpnessValues[s]; topThreeSharpnessValues[s] = sharpnessValue; sharpnessValue = tv;
              const ti = topThreeSharpnessVerts [s]; topThreeSharpnessVerts [s] = sharpnessVertex; sharpnessVertex = ti;
            }
          }
        }
      }

      const centerVertexSharpness = vertSharpness[vertIndex];

      const w = getCenterVertexEdgeSharpnessInfoHelper(
          topThreeSharpnessValues[1], topThreeSharpnessValues[2],
          centerVertexSharpness, 0);
      const dstWeightSmooth = w.dstWeightSmooth;
      const dstWeightEdge   = w.dstWeightEdge;
      const dstWeightCorner = w.dstWeightCorner;

      // smooth rule: P*(v-2)/v + sumEdge/v^2 + sumFace/v^2
      let dst_smooth;
      {
        const scaleV = (numAdjVerts - 2) / numAdjVerts;
        const scaleE = 1.0 / (numAdjVerts * numAdjVerts);
        const scaleF = 1.0 / (numAdjVerts * numAdjVerts);

        dst_smooth = ops.add(ops.add(ops.scale(vertP, scaleV), ops.scale(sumEdgeP, scaleE)),
                             ops.scale(sumFaceP, scaleF));
      }

      let dst_edge;
      {
        // sharpness scalar is computed but unused in amolite's code at
        // this point -- the SubdivVertexSharpRule uses fixed 1/8 weights.
        const lhsV = topThreeSharpnessVerts[0];
        const rhsV = topThreeSharpnessVerts[1];
        const lhsP = posData[lhsV];
        const rhsP = posData[rhsV];

        // (lhsP + rhsP + 6 * vertP) * 0.125
        dst_edge = ops.scale(ops.add(ops.add(lhsP, rhsP), ops.scale(vertP, 6.0)), 0.125);
      }

      const dst_corner = ops.copy(vertP);

      const dst = ops.add(
        ops.add(ops.scale(dst_corner, dstWeightCorner), ops.scale(dst_edge, dstWeightEdge)),
        ops.scale(dst_smooth, dstWeightSmooth));

      vertPts[vertIndex] = dst;
    }
  }
}

// =====================================================================
// applySubdivisionToPts: port of ApplySubdivisionToPts
// (DcciSubdiv.cpp:1370). Orchestrates face / edge / vert point
// computation and concatenates into [verts, edges, faces].
// =====================================================================
function applySubdivisionToPts(dstPts, helper, posData, lockBorderVerts, tessellate, ops) {
  dstPts.length = 0;

  const facePts = [];
  applySubdivisionToFacePts(facePts, helper.m_polySizes, helper.m_polyIndexList, posData, ops);

  const edgePts = [];
  applySubdivisionToEdgePtsSemiSharp(edgePts, helper.m_edgeLookup,
                                     posData, facePts, helper.m_edgeSharpness,
                                     tessellate, ops);

  const vertPts = [];
  applySubdivisionToVertPtsSemiSharp(vertPts, helper.m_adjVertData, helper.m_adjFaceData,
                                     posData, facePts, helper.m_numVerts,
                                     helper.m_edgeLookup, helper.m_edgeSharpness,
                                     helper.m_vertSharpness, lockBorderVerts,
                                     helper.m_vertIsBorder, tessellate, ops);

  // order: [vertices, edges, faces]
  for (let i = 0; i < vertPts.length; i++) dstPts.push(vertPts[i]);
  for (let i = 0; i < edgePts.length; i++) dstPts.push(edgePts[i]);
  for (let i = 0; i < facePts.length; i++) dstPts.push(facePts[i]);
}

// =====================================================================
// subdivideEdgeSharpness: port of SubdivideEdgeSharpness
// (DcciSubdiv.cpp:1567). After one CC subdivision step, each crease
// edge gets propagated as up to two sub-edges with possibly reduced
// sharpness.
// =====================================================================
function subdivideEdgeSharpness(dstEdgeListLhs, dstEdgeListRhs, dstEdgeListSharpness,
                                helper,
                                srcEdgeListLhs, srcEdgeListRhs, srcEdgeListSharpness) {
  dstEdgeListLhs.length = 0;
  dstEdgeListRhs.length = 0;
  dstEdgeListSharpness.length = 0;

  for (let edgeIter = 0; edgeIter < srcEdgeListLhs.length; edgeIter++) {
    const lhs = srcEdgeListLhs[edgeIter];
    const rhs = srcEdgeListRhs[edgeIter];
    const weight = srcEdgeListSharpness[edgeIter];

    if (weight > 0) {
      let lhsFoundNum = 0;
      let lhsFoundSharpness = 0.0;
      {
        const lhsNum = helper.m_adjVertData.m_vertAdjSize[lhs];
        const lhsStart = helper.m_adjVertData.m_vertAdjStart[lhs];
        for (let i = 0; i < lhsNum; i++) {
          const lhsAdj = helper.m_adjVertData.m_vertAdjData[lhsStart + i];
          if (lhsAdj !== rhs) {
            const edgeId = edgeLookupFetchEdge(helper.m_edgeLookup, lhsAdj, lhs);
            if (edgeId < 0) throw new Error('subdivideEdgeSharpness: edge not found (lhs side)');
            const lhsSharpness = helper.m_edgeSharpness[edgeId];
            if (lhsSharpness > 0) {
              lhsFoundNum++;
              lhsFoundSharpness = Math.max(lhsSharpness, lhsFoundSharpness);
            }
          }
        }
      }

      let rhsFoundNum = 0;
      let rhsFoundSharpness = 0.0;
      {
        const rhsNum = helper.m_adjVertData.m_vertAdjSize[rhs];
        const rhsStart = helper.m_adjVertData.m_vertAdjStart[rhs];
        for (let i = 0; i < rhsNum; i++) {
          const rhsAdj = helper.m_adjVertData.m_vertAdjData[rhsStart + i];
          if (rhsAdj !== lhs) {
            const edgeId = edgeLookupFetchEdge(helper.m_edgeLookup, rhsAdj, rhs);
            if (edgeId < 0) throw new Error('subdivideEdgeSharpness: edge not found (rhs side)');
            const rhsSharpness = helper.m_edgeSharpness[edgeId];
            if (rhsSharpness > 0) {
              rhsFoundNum++;
              rhsFoundSharpness = Math.max(rhsSharpness, rhsFoundSharpness);
            }
          }
        }
      }

      let w0 = Math.max(0.0, weight - 1.0);
      let w1 = Math.max(0.0, weight - 1.0);

      if (lhsFoundNum === 1 && rhsFoundNum === 1) {
        w0 = Math.max(0.0, 0.25 * (3.0 * weight + lhsFoundSharpness) - 1.0);
        w1 = Math.max(0.0, 0.25 * (3.0 * weight + rhsFoundSharpness) - 1.0);
      }

      const midEdge = edgeLookupFetchEdge(helper.m_edgeLookup, lhs, rhs);
      if (midEdge < 0) throw new Error('subdivideEdgeSharpness: midEdge not found');

      if (w0 > 0.0) {
        dstEdgeListLhs.push(lhs);
        dstEdgeListRhs.push(helper.m_numVerts + midEdge);
        dstEdgeListSharpness.push(w0);
      }

      if (w1 > 0.0) {
        dstEdgeListLhs.push(helper.m_numVerts + midEdge);
        dstEdgeListRhs.push(rhs);
        dstEdgeListSharpness.push(w1);
      }
    }
  }
}

// =====================================================================
// applyCatmullClarkToWeights: port of
// DcciSubdiv::ApplyCatmullClarkToWeights (DcciSubdiv.cpp:2100). One
// CC subdivision step on the WEIGHT side -- produces a FlatWeightVertex
// array where each subdivided point's weights are computed by
// composing the CC stencils with the source weights.
// =====================================================================
function applyCatmullClarkToWeights(dstMesh, helper, dstWeights,
                                    srcMesh, semiSharp, tangentUvIndex) {
  const lockBorderVerts = false;
  const tessellate      = false;

  // helper.Reset() then full BuildCatmullClarkTopoHelper -- the chain
  // ports we've shipped already. Wire them in order.
  catmullClarkTopoHelperReset(helper);
  helper.m_polySizes     = srcMesh.m_polySizes;
  helper.m_polyIndexList = srcMesh.m_polyIndexList;
  helper.m_polyStarts    = calcPrefixSumVec(srcMesh.m_polySizes);
  helper.m_numVerts      = srcMesh.m_numVerts;
  helper.m_enableSemiSharp = semiSharp;

  buildEdgeLookup(helper.m_edgeLookup,
                  srcMesh.m_polySizes, srcMesh.m_polyIndexList,
                  srcMesh.m_creaseEdgeListLhs, srcMesh.m_creaseEdgeListRhs, srcMesh.m_creaseEdgeWeight,
                  srcMesh.m_hardEdgeNormalListLhs, srcMesh.m_hardEdgeNormalListRhs,
                  srcMesh.m_hardEdgeTangentListLhs, srcMesh.m_hardEdgeTangentListRhs,
                  srcMesh.m_numVerts);

  helper.m_numFaces = srcMesh.m_polySizes.length;
  helper.m_numEdges = helper.m_edgeLookup.m_numEdges;

  buildVertAdjData(helper.m_adjVertData,
                   srcMesh.m_polySizes, srcMesh.m_polyIndexList,
                   srcMesh.m_numVerts, helper.m_edgeLookup);

  buildFaceAdjData(helper.m_adjFaceData,
                   srcMesh.m_polySizes, srcMesh.m_polyIndexList, srcMesh.m_numVerts);

  detectBorders(helper);

  buildWedgeData(helper.m_wedges, helper.m_adjVertData, helper.m_edgeLookup,
                 srcMesh.m_numVerts,
                 srcMesh.m_polySizes, helper.m_polyStarts, srcMesh.m_polyIndexList,
                 srcMesh.m_creaseVertexList, srcMesh.m_creaseVertexWeight);

  if (semiSharp) {
    decompressCreaseSharpness(helper.m_edgeSharpness, helper.m_edgeLookup,
                              srcMesh.m_creaseEdgeListLhs, srcMesh.m_creaseEdgeListRhs,
                              srcMesh.m_creaseEdgeWeight);
    applyVertexAndBorderSharpness(helper,
                                  srcMesh.m_creaseVertexList, srcMesh.m_creaseVertexWeight,
                                  /*bordersAsSharp=*/ true);
  }

  // dstMesh.m_numVerts = numVerts + numFaces + numEdges
  dstMesh.m_numVerts = helper.m_numVerts + helper.m_numFaces + helper.m_numEdges;

  // dstMesh.m_polySizes / m_polyIndexList
  {
    const dstPolySizes     = [];
    const dstPolyIndexList = [];
    buildIndexList(dstPolySizes, dstPolyIndexList, helper);
    dstMesh.m_polySizes     = dstPolySizes;
    dstMesh.m_polyIndexList = dstPolyIndexList;
    dstMesh.m_polyStarts    = calcPrefixSumVec(dstPolySizes);
  }

  // dstMesh creases via subdivideEdgeSharpness
  {
    const dstLhs = [], dstRhs = [], dstW = [];
    subdivideEdgeSharpness(dstLhs, dstRhs, dstW,
                           helper,
                           srcMesh.m_creaseEdgeListLhs, srcMesh.m_creaseEdgeListRhs,
                           srcMesh.m_creaseEdgeWeight);
    dstMesh.m_creaseEdgeListLhs = dstLhs;
    dstMesh.m_creaseEdgeListRhs = dstRhs;
    dstMesh.m_creaseEdgeWeight  = dstW;
  }

  // srcWeights = identity stencil per source vertex (1.0 * vertId).
  // dstWeights = ApplySubdivisionToPts(srcWeights) using FWV ops.
  const numVerts = srcMesh.m_numVerts;
  const srcWeights = new Array(numVerts);
  for (let i = 0; i < numVerts; i++) {
    srcWeights[i] = makeFlatWeightVertexValId(1.0, i);
  }

  dstWeights.length = 0;
  applySubdivisionToPts(dstWeights, helper, srcWeights, lockBorderVerts, tessellate,
                        FlatWeightVertexOps);
}

// =====================================================================
// convolveWeights: line-by-line port of DcciSubdiv::ConvolveWeights
// (DcciSubdiv.cpp:2066). For each fineWeights entry, composes
// (coarseWeights[infl] * fineWeight) over all influences. Used to
// merge two CC subdivision levels into one weight set.
// =====================================================================
function convolveWeights(dstWeights, coarseWeights, fineWeights, normalize) {
  const numFine = fineWeights.length;
  dstWeights.length = 0;
  for (let i = 0; i < numFine; i++) dstWeights.push(makeFlatWeightVertex());

  for (let dstIter = 0; dstIter < numFine; dstIter++) {
    const currFine = fineWeights[dstIter];
    const numFineInfl = flatWeightVertexGetNum(currFine);

    let sum = makeFlatWeightVertex();
    for (let i = 0; i < numFineInfl; i++) {
      const coarseInfl   = currFine.m_influences[i];
      const coarseWeight = currFine.m_weights[i];

      const currCoarse = coarseWeights[coarseInfl];
      const temp = flatWeightVertexAddDirect(sum, flatWeightVertexMulDirect(currCoarse, coarseWeight));
      sum = temp;
    }

    if (normalize) flatWeightVertexNormalize(sum);

    dstWeights[dstIter] = sum;
  }
}

// =====================================================================
// applyWeightsToPoints: line-by-line port of
// DcciSubdiv::ApplyWeightsToPoints (DcciSubdiv.h:1349, templated).
// Applies a FlatWeightVertex stencil set to a source-point array.
// Used to derive level-1 / level-2 CC control point positions from
// the source mesh positions.
// =====================================================================
function applyWeightsToPoints(dstPoints, srcPts, srcWeights, normalize, ops) {
  const dstNum = srcWeights.length;
  dstPoints.length = 0;
  for (let i = 0; i < dstNum; i++) dstPoints.push(ops.zero());

  for (let dstIter = 0; dstIter < dstNum; dstIter++) {
    let sum = ops.zero();
    const currWeightVtx = srcWeights[dstIter];
    const numInfl = flatWeightVertexGetNum(currWeightVtx);

    let sumInfl = 0.0;

    for (let i = 0; i < numInfl; i++) {
      const srcIndex = currWeightVtx.m_influences[i];
      const srcInfl  = currWeightVtx.m_weights[i];

      sumInfl += srcInfl;
      const curr = srcPts[srcIndex];
      sum = ops.add(sum, ops.scale(curr, srcInfl));
    }

    const scale = normalize ? ((sumInfl !== 0.0) ? (1.0 / sumInfl) : 0.0) : 1.0;
    dstPoints[dstIter] = ops.scale(sum, scale);
  }
}

// =====================================================================
// Helpers for BuildCalcQuadRegionData (DcciSubdiv.cpp:4055-4191).
// =====================================================================

// Port of GetQuadCornerDirU (line 4055).
function getQuadCornerDirU(corner) {
  const dirU = [[1, 0], [0, -1], [-1, 0], [0, 1]];
  return intVec2(dirU[corner][0], dirU[corner][1]);
}
// Port of GetQuadCornerDirV (line 4069).
function getQuadCornerDirV(corner) {
  const dirV = [[0, 1], [1, 0], [0, -1], [-1, 0]];
  return intVec2(dirV[corner][0], dirV[corner][1]);
}
// Port of GetQuadCornerPosUv (line 4083).
function getQuadCornerPosUv(corner) {
  const posUv = [[0, 0], [0, 1], [1, 1], [1, 0]];
  return intVec2(posUv[corner][0], posUv[corner][1]);
}
// Port of GridIndexFromCorner (line 4116).
function gridIndexFromCorner(corner) {
  const lookup = [0, 2, 3, 1];
  return lookup[corner];
}
// Port of GetQuadCornerSudivRegionUv (line 4130).
function getQuadCornerSudivRegionUv(corner, subdivLevel) {
  const gridIndex = gridIndexFromCorner(corner);
  const row = (gridIndex / 2) | 0;
  const col = gridIndex % 2;
  switch (subdivLevel) {
    case 0: return intVec2(0, 0);
    case 1: return intVec2(1 * col, 1 * row);
    case 2: return intVec2(3 * col, 3 * row);
    default: throw new Error('getQuadCornerSudivRegionUv: bad subdivLevel ' + subdivLevel);
  }
}
// Port of CountBits (line 4158).
function countBits(packed) {
  let num = 0;
  for (let i = 0; i < 32; i++) {
    if ((packed & (1 << (31 - i))) !== 0) num++;
  }
  return num;
}

// =====================================================================
// findCornerForPoly: line-by-line port of
// CatmullClarkBuilder::FindCornerForPoly (DcciSubdiv.cpp:3515).
// Walks the per-vertex wedge corner data for vtxId and locates the
// entry pointing at polyId. Returns null if not found.
// =====================================================================
function findCornerForPoly(helper0, vtxId, polyId) {
  const wedgesForVtxNum   = helper0.m_wedges.m_wedgeEdgeSize[vtxId];
  const wedgesForVtxStart = helper0.m_wedges.m_wedgeEdgeStart[vtxId];

  let found = false;
  let localWedgeIndex  = -1;
  let localWedgeCorner = -1;
  let polyCorner       = -1;

  for (let wedgeIter = 0; wedgeIter < wedgesForVtxNum; wedgeIter++) {
    const wedgeInfo = helper0.m_wedges.m_wedgeEdgeInfo[wedgesForVtxStart + wedgeIter];
    // Match VertexWedgeInfo::GetNumPoly() (DcciSubdiv.h:370).
    const numPoly = wedgeInfo.m_isCircular ? wedgeInfo.m_wedgeEdgeNum : (wedgeInfo.m_wedgeEdgeNum - 1);

    for (let polyIter = 0; polyIter < numPoly; polyIter++) {
      const cornerInfo = helper0.m_wedges.m_wedgeCornerData[wedgeInfo.m_wedgeEdgeStart + polyIter];
      if (cornerInfo.m_polyIndex === polyId) {
        if (found) throw new Error('findCornerForPoly: multiple matches at vtx ' + vtxId + ' poly ' + polyId);
        found = true;
        localWedgeCorner = polyIter;
        polyCorner       = cornerInfo.m_polyCorner;
        localWedgeIndex  = wedgeIter;
      }
    }
  }

  return found ? { localWedgeIndex, localWedgeCorner, polyCorner } : null;
}

// =====================================================================
// buildCalcQuadRegionData: line-by-line port of
// CatmullClarkBuilder::BuildCalcQuadRegionData (DcciSubdiv.cpp:4193).
//
// For each quad poly, builds a per-region grid that says which
// sub-regions (within the polyInfo.m_numRegions sub-cells of the
// patch) are "irregular" -- i.e., need to route through the
// irregular-wedge eval rather than the regular bicubic path.
//
// Output: patches.m_regionData (flat RegionInfo[]) + per poly's
// m_quadRegionGrid.m_regionMask / m_regionDataStart / m_regionDataNum.
// =====================================================================
function buildCalcQuadRegionData(patches, helper0) {
  const numPolys = helper0.m_polySizes.length;

  // Allocate m_quadRegionGrid on each poly that's a quad. (TriRegion
  // is a different pass; for quads we set it now, tris stay null.)
  for (let i = 0; i < numPolys; i++) {
    if (!patches.m_polyInfo[i].m_quadRegionGrid) {
      patches.m_polyInfo[i].m_quadRegionGrid = makeRegionGrid();
    }
  }

  for (let polyIter = 0; polyIter < numPolys; polyIter++) {
    const polySize  = helper0.m_polySizes[polyIter];
    const polyStart = helper0.m_polyStarts[polyIter];

    const polyInfo  = patches.m_polyInfo[polyIter];

    const numRegions = getPolyNumRegions(polyInfo.m_type);
    const regionData = new Array(numRegions);
    for (let i = 0; i < numRegions; i++) regionData[i] = -1;

    const regions = [];
    const polySubdivLevel = getPolySubdivLevel(polyInfo.m_type);

    if (polySize === 4) {
      // for subdiv level 0, all regions stay invalid (regular bicubic
      // is the default; valid entries override to the irregular path)
      if (polySubdivLevel >= 1) {
        for (let i = 0; i < 4; i++) {
          const vtxId = helper0.m_polyIndexList[polyStart + i];
          const vtxInfo = patches.m_vertexInfo[vtxId];

          if (!(polySubdivLevel >= vtxInfo.m_irregularSubdivLevel)) {
            throw new Error('buildCalcQuadRegionData: polySubdivLevel ' + polySubdivLevel +
                            ' < vtx irregularSubdivLevel ' + vtxInfo.m_irregularSubdivLevel);
          }

          if (vtxInfo.m_irregularSubdivLevel >= 1) {
            if (!(vtxInfo.m_dstIrregularIndex >= 0)) {
              throw new Error('buildCalcQuadRegionData: vtx ' + vtxId + ' dstIrregularIndex < 0');
            }

            const found = findCornerForPoly(helper0, vtxId, polyIter);
            if (!found) throw new Error('buildCalcQuadRegionData: no corner for (vtx ' + vtxId + ', poly ' + polyIter + ')');

            if (!(found.localWedgeCorner < vtxInfo.m_valence)) {
              throw new Error('buildCalcQuadRegionData: localWedgeCorner >= valence');
            }

            const regionInfo = makeRegionInfo();
            regionInfo.m_irregularIndex       = vtxInfo.m_dstIrregularIndex;
            regionInfo.m_irregularPolyIndex   = found.localWedgeCorner;
            regionInfo.m_irregularSubdivLevel = polySubdivLevel - vtxInfo.m_irregularSubdivLevel;
            regionInfo.m_rotation             = i;

            const dirU   = getQuadCornerDirU(i);
            const dirV   = getQuadCornerDirV(i);
            const baseUv = getQuadCornerSudivRegionUv(i, polySubdivLevel);

            const rowLen = 1 << polySubdivLevel;

            if (polySubdivLevel === vtxInfo.m_irregularSubdivLevel) {
              regionInfo.m_irregularRegionScale = kRegionScale_Full;

              if (regionData[baseUv.y * rowLen + baseUv.x] >= 0) {
                throw new Error('buildCalcQuadRegionData: regionData slot already set');
              }
              regionData[baseUv.y * rowLen + baseUv.x] = regions.length;
              regions.push(regionInfo);
            } else {
              if (polySubdivLevel !== vtxInfo.m_irregularSubdivLevel + 1) {
                throw new Error('buildCalcQuadRegionData: subdivLevel mismatch');
              }

              for (let offsetV = 0; offsetV < 2; offsetV++) {
                for (let offsetU = 0; offsetU < 2; offsetU++) {
                  const localGridIndex = offsetV * 2 + offsetU;

                  const dstX = baseUv.x + offsetV * dirV.x + offsetU * dirU.x;
                  const dstY = baseUv.y + offsetV * dirV.y + offsetU * dirU.y;

                  const subInfo = makeRegionInfo();
                  subInfo.m_irregularIndex       = regionInfo.m_irregularIndex;
                  subInfo.m_irregularPolyIndex   = regionInfo.m_irregularPolyIndex;
                  subInfo.m_irregularSubdivLevel = regionInfo.m_irregularSubdivLevel;
                  subInfo.m_rotation             = regionInfo.m_rotation;
                  subInfo.m_irregularRegionScale = localGridIndex;

                  if (regionData[dstY * rowLen + dstX] >= 0) {
                    throw new Error('buildCalcQuadRegionData: sub-region slot already set');
                  }
                  regionData[dstY * rowLen + dstX] = regions.length;
                  regions.push(subInfo);
                }
              }
            }
          }
        }
      }
    }

    // Write the grid + RegionInfo array into the patches struct.
    polyInfo.m_quadRegionGrid.m_regionDataNum   = regions.length;
    polyInfo.m_quadRegionGrid.m_regionDataStart = patches.m_regionData.length;
    polyInfo.m_quadRegionGrid.m_regionMask      = 0;

    if (regions.length > 0) {
      // Reserve space.
      for (let i = 0; i < regions.length; i++) {
        patches.m_regionData.push(makeRegionInfo());
      }

      let numAdded = 0;
      let packed = 0;
      if (regionData.length >= 32) {
        throw new Error('buildCalcQuadRegionData: regionData.length >= 32 (mask overflow)');
      }
      for (let i = 0; i < regionData.length; i++) {
        const localIndex = regionData[i];
        if (localIndex >= 0) {
          const currInfo = regions[localIndex];
          patches.m_regionData[polyInfo.m_quadRegionGrid.m_regionDataStart + numAdded] = currInfo;
          packed = (packed | (1 << i)) >>> 0;
          numAdded++;
        }
      }
      polyInfo.m_quadRegionGrid.m_regionMask = packed;

      // Sanity (amolite asserts): countBits(mask) == regions.length
      const counted = countBits(polyInfo.m_quadRegionGrid.m_regionMask);
      if (counted !== regions.length) {
        throw new Error('buildCalcQuadRegionData: countBits mismatch (' +
                        counted + ' vs ' + regions.length + ')');
      }
      if (numAdded !== regions.length) {
        throw new Error('buildCalcQuadRegionData: numAdded mismatch');
      }
    }
  }
}

// =====================================================================
// buildVertexIrregularData: line-by-line port of
// CatmullClarkBuilder::BuildVertexIrregularData (DcciSubdiv.cpp:4370).
// Builds the IrregularWedgeInfo[] array -- one entry per
// vertex-irregular wedge, then triangle-irregular entries appended.
// =====================================================================
function buildVertexIrregularData(patches, helper0) {
  const vtxNum = helper0.m_numVerts;

  const numIrregularWedges    = patches.m_numIrregularVertexWedges;
  const numIrregularTriangles = patches.m_numIrregularTriangles;

  patches.m_irregularWedgeInfo.length = 0;
  for (let i = 0; i < numIrregularWedges + numIrregularTriangles; i++) {
    patches.m_irregularWedgeInfo.push(makeIrregularWedgeInfo());
  }

  for (let vtxIter = 0; vtxIter < vtxNum; vtxIter++) {
    const vtxInfo = patches.m_vertexInfo[vtxIter];

    if (vtxInfo.m_dstIrregularIndex >= 0) {
      if (!(vtxInfo.m_irregularSubdivLevel >= 1)) {
        throw new Error('buildVertexIrregularData: vtx ' + vtxIter + ' has dstIrregularIndex but irregularSubdivLevel < 1');
      }

      const numWedges            = vtxInfo.m_numWedges;
      const numIrregularWedgesV  = vtxInfo.m_numIrregularWedges;

      const actualWedgeStart = helper0.m_wedges.m_wedgeEdgeStart[vtxIter];
      const actualWedgeNum   = helper0.m_wedges.m_wedgeEdgeSize[vtxIter];
      if (actualWedgeNum !== numWedges) {
        throw new Error('buildVertexIrregularData: actualWedgeNum ' + actualWedgeNum + ' != vtxInfo.m_numWedges ' + numWedges);
      }

      let wedgeAdded = 0;
      for (let i = 0; i < numWedges; i++) {
        const wedgeInfo = helper0.m_wedges.m_wedgeEdgeInfo[actualWedgeStart + i];
        if (wedgeInfo.m_isWedgeIrregular) {
          const dstIndex = vtxInfo.m_dstIrregularIndex + wedgeAdded;
          const irregularInfo = patches.m_irregularWedgeInfo[dstIndex];

          irregularInfo.m_centerVertex       = vtxIter;
          irregularInfo.m_centerTrianglePoly = -1;
          irregularInfo.m_localWedgeIndex    = i;
          irregularInfo.m_subdivLevel        = vtxInfo.m_irregularSubdivLevel;

          wedgeAdded++;
        }
      }

      if (wedgeAdded !== numIrregularWedgesV) {
        throw new Error('buildVertexIrregularData: wedgeAdded ' + wedgeAdded +
                        ' != numIrregularWedges ' + numIrregularWedgesV);
      }
    }
  }

  // Triangle pass
  {
    let triAdded = 0;
    for (let polyIter = 0; polyIter < helper0.m_polySizes.length; polyIter++) {
      const currSize = helper0.m_polySizes[polyIter];
      if (currSize === 3) {
        const irregularInfo = patches.m_irregularWedgeInfo[numIrregularWedges + triAdded];

        const polyInfo = patches.m_polyInfo[polyIter];
        if (polyInfo.m_triCenterIndex !== triAdded) {
          throw new Error('buildVertexIrregularData: triCenterIndex mismatch');
        }
        if (!(irregularInfo.m_centerVertex < 0)) {
          throw new Error('buildVertexIrregularData: irregularInfo.m_centerVertex should be < 0');
        }

        irregularInfo.m_centerTrianglePoly       = polyIter;
        irregularInfo.m_isCircular               = true;
        irregularInfo.m_valence                  = 3;
        irregularInfo.m_subdivLevel              = 2;
        irregularInfo.m_centerTriangleLimitOffset = triAdded;

        triAdded++;
      }
    }
    if (triAdded !== numIrregularTriangles) {
      throw new Error('buildVertexIrregularData: triAdded ' + triAdded +
                      ' != numIrregularTriangles ' + numIrregularTriangles);
    }
  }
}

// =====================================================================
// buildTriRegionData: line-by-line port of
// CatmullClarkBuilder::BuildTriRegionData (DcciSubdiv.cpp:4464).
// For each triangle patch (one IrregularWedgeInfo entry per tri,
// appended after vertex-irregular wedges by buildVertexIrregularData),
// emits 3 corner RegionGrids each with up to 2 region entries
// (a corner region for the irregular vertex + the center region
// rotating around that corner).
// =====================================================================
function buildTriRegionData(patches, helper0) {
  const numTris             = patches.m_numIrregularTriangles;
  const numIrregularWedges  = patches.m_numIrregularVertexWedges;

  for (let triIter = 0; triIter < numTris; triIter++) {
    const irregularInfo = patches.m_irregularWedgeInfo[numIrregularWedges + triIter];
    const polyIter = irregularInfo.m_centerTrianglePoly;
    if (!(polyIter >= 0)) throw new Error('buildTriRegionData: centerTrianglePoly < 0');

    const polyInfo = patches.m_polyInfo[polyIter];
    if (polyInfo.m_triCenterIndex !== triIter) {
      throw new Error('buildTriRegionData: triCenterIndex mismatch');
    }
    if (polyInfo.m_type !== kSubdivPolyType_Triangle) {
      throw new Error('buildTriRegionData: poly ' + polyIter + ' is not a triangle');
    }
    if (polyInfo.m_quadRegionGrid && polyInfo.m_quadRegionGrid.m_regionDataNum !== 0) {
      throw new Error('buildTriRegionData: tri poly has quadRegionGrid');
    }

    const polySize  = helper0.m_polySizes[polyIter];
    if (polySize !== 3) throw new Error('buildTriRegionData: poly ' + polyIter + ' polySize != 3');
    const polyStart = helper0.m_polyStarts[polyIter];

    // Allocate m_triRegionGrids[3] on the polyInfo if not already.
    if (!polyInfo.m_triRegionGrids) {
      polyInfo.m_triRegionGrids = [makeRegionGrid(), makeRegionGrid(), makeRegionGrid()];
    }

    for (let corner = 0; corner < 3; corner++) {
      // always 2x2
      const grid = makeRegionGrid();
      const vtx = helper0.m_polyIndexList[polyStart + corner];

      const vtxInfo = patches.m_vertexInfo[vtx];

      grid.m_regionMask      = 0;
      grid.m_regionDataNum   = 0;
      grid.m_regionDataStart = patches.m_regionData.length;

      if (vtxInfo.m_irregularSubdivLevel >= 1) {
        if (vtxInfo.m_irregularSubdivLevel !== 2) {
          throw new Error('buildTriRegionData: tri-incident vtx ' + vtx + ' has irregularSubdivLevel != 2');
        }

        grid.m_regionMask    = (grid.m_regionMask | (1 << 0)) >>> 0;
        grid.m_regionDataNum++;

        const found = findCornerForPoly(helper0, vtx, polyIter);
        if (!found) throw new Error('buildTriRegionData: no corner for (vtx ' + vtx + ', poly ' + polyIter + ')');

        const regionInfo = makeRegionInfo();
        regionInfo.m_rotation             = 0;
        regionInfo.m_irregularSubdivLevel = 2;
        regionInfo.m_irregularIndex       = vtxInfo.m_dstIrregularIndex;
        regionInfo.m_irregularPolyIndex   = found.localWedgeCorner;
        regionInfo.m_irregularRegionScale = kRegionScale_Full;

        patches.m_regionData.push(regionInfo);
      }

      {
        grid.m_regionMask    = (grid.m_regionMask | (1 << 3)) >>> 0;
        grid.m_regionDataNum++;

        const regionInfo = makeRegionInfo();
        regionInfo.m_rotation             = 2;
        regionInfo.m_irregularSubdivLevel = 2;
        regionInfo.m_irregularIndex       = numIrregularWedges + triIter;
        regionInfo.m_irregularPolyIndex   = corner; // inner region rotates around corner
        regionInfo.m_irregularRegionScale = kRegionScale_Full;

        patches.m_regionData.push(regionInfo);
      }

      polyInfo.m_triRegionGrids[corner] = grid;
    }
  }
}

// =====================================================================
// Phase 1b.12d: ControlPoint + Fetch helpers + CollectPointsForPatches
// =====================================================================

// ---- CatmullClarkPatches::ControlPoint (DcciSubdiv.h:880) -----------
const kCpType_Invalid       = 0;
const kCpType_Direct        = 1;
const kCpType_Border        = 2;
const kCpType_ConcaveCorner = 3;
const kCpType_ConvexCorner  = 4;
const kCpType_Num           = 5;

function makeControlPoint() {
  return { m_v0: -1, m_v1: -1, m_v2: -1, m_v3: -1 };
}
// Port of ControlPoint::MakeInvalid (DcciSubdiv.h:900).
function controlPointMakeInvalid() {
  return makeControlPoint();
}
// Port of ControlPoint::MakeDirect (DcciSubdiv.h:906).
function controlPointMakeDirect(v0) {
  if (!(v0 >= 0)) throw new Error('controlPointMakeDirect: v0 < 0');
  const ret = makeControlPoint();
  ret.m_v0 = v0;
  return ret;
}
// Port of ControlPoint::MakeDirectOrInvalid (DcciSubdiv.h:915).
function controlPointMakeDirectOrInvalid(v0) {
  const ret = makeControlPoint();
  ret.m_v0 = v0;
  return ret;
}
// Port of ControlPoint::MakeBorder (DcciSubdiv.h:922).
function controlPointMakeBorder(v0, v1) {
  if (!(v0 >= 0)) throw new Error('controlPointMakeBorder: v0 < 0');
  if (!(v1 >= 0)) throw new Error('controlPointMakeBorder: v1 < 0');
  const ret = makeControlPoint();
  ret.m_v0 = v0;
  ret.m_v1 = v1;
  return ret;
}
// Port of ControlPoint::MakeConcaveCorner (DcciSubdiv.h:933). Note the
// swap to canonicalize (m_v1, m_v2) order.
function controlPointMakeConcaveCorner(v0, v1, v2) {
  if (!(v0 >= 0)) throw new Error('controlPointMakeConcaveCorner: v0 < 0');
  if (!(v1 >= 0)) throw new Error('controlPointMakeConcaveCorner: v1 < 0');
  if (!(v2 >= 0)) throw new Error('controlPointMakeConcaveCorner: v2 < 0');
  const ret = makeControlPoint();
  ret.m_v0 = v0;
  ret.m_v1 = v1;
  ret.m_v2 = v2;
  if (ret.m_v1 > ret.m_v2) {
    const t = ret.m_v1; ret.m_v1 = ret.m_v2; ret.m_v2 = t;
  }
  return ret;
}
// Port of ControlPoint::MakeConvexCorner (DcciSubdiv.h:953).
function controlPointMakeConvexCorner(v0, v1, v2) {
  if (!(v0 >= 0)) throw new Error('controlPointMakeConvexCorner: v0 < 0');
  if (!(v1 >= 0)) throw new Error('controlPointMakeConvexCorner: v1 < 0');
  if (!(v2 >= 0)) throw new Error('controlPointMakeConvexCorner: v2 < 0');
  const ret = makeControlPoint();
  ret.m_v0 = v0;
  ret.m_v1 = v1;
  ret.m_v2 = v2;
  ret.m_v3 = 0;
  return ret;
}
// Port of ControlPoint::GetType (DcciSubdiv.h:967).
function controlPointGetType(cp) {
  if (cp.m_v0 < 0 && cp.m_v1 < 0 && cp.m_v2 < 0 && cp.m_v3 < 0) return kCpType_Invalid;
  if (cp.m_v0 >= 0 && cp.m_v1 < 0 && cp.m_v2 < 0 && cp.m_v3 < 0) return kCpType_Direct;
  if (cp.m_v0 >= 0 && cp.m_v1 >= 0 && cp.m_v2 < 0 && cp.m_v3 < 0) return kCpType_Border;
  if (cp.m_v0 >= 0 && cp.m_v1 >= 0 && cp.m_v2 >= 0 && cp.m_v3 < 0) return kCpType_ConcaveCorner;
  if (cp.m_v0 >= 0 && cp.m_v1 >= 0 && cp.m_v2 >= 0 && cp.m_v3 >= 0) return kCpType_ConvexCorner;
  throw new Error('controlPointGetType: invalid combo (' +
                  cp.m_v0 + ',' + cp.m_v1 + ',' + cp.m_v2 + ',' + cp.m_v3 + ')');
}

// =====================================================================
// PolyCorner navigation. All operate on helper0 (which carries
// m_polySizes / m_polyStarts / m_polyIndexList / m_edgeLookup).
// =====================================================================

// Port of CatmullClarkBuilder::FetchEdgeFromPoly (DcciSubdiv.cpp:3197).
function fetchEdgeFromPoly(helper0, edgePoly) {
  let ret = -1;
  if (polyCornerIsValid(edgePoly)) {
    const polyStart = helper0.m_polyStarts[edgePoly.m_polyIndex];
    const polySize  = helper0.m_polySizes[edgePoly.m_polyIndex];
    const vtx0 = helper0.m_polyIndexList[polyStart + (edgePoly.m_polyCorner + 0) % polySize];
    const vtx1 = helper0.m_polyIndexList[polyStart + (edgePoly.m_polyCorner + 1) % polySize];
    const edgeIndex = edgeLookupFetchEdge(helper0.m_edgeLookup, vtx0, vtx1);
    if (!(edgeIndex >= 0)) throw new Error('fetchEdgeFromPoly: edge not found');
    ret = edgeIndex;
  }
  return ret;
}
// Port of CatmullClarkBuilder::FetchCornerOpp (DcciSubdiv.cpp:3219).
function fetchCornerOpp(helper0, srcCorner) {
  const ret = makePolyCorner();
  if (polyCornerIsValid(srcCorner)) {
    const edgeIndex = fetchEdgeFromPoly(helper0, srcCorner);
    if (!(edgeIndex >= 0)) throw new Error('fetchCornerOpp: edge not found');
    const numPolysForEdge = edgeLookupGetNumFacesForEdge(helper0.m_edgeLookup, edgeIndex);
    if (numPolysForEdge === 2) {
      const edgePoly0 = edgeLookupGetFaceForEdge(helper0.m_edgeLookup, edgeIndex, 0);
      const edgePoly1 = edgeLookupGetFaceForEdge(helper0.m_edgeLookup, edgeIndex, 1);
      const isSame0 = polyCornerIsSame(edgePoly0, srcCorner);
      const isSame1 = polyCornerIsSame(edgePoly1, srcCorner);
      if (!((isSame0 && !isSame1) || (!isSame0 && isSame1))) {
        throw new Error('fetchCornerOpp: srcCorner not exactly one of the two edge faces');
      }
      if (isSame0) { ret.m_polyIndex = edgePoly1.m_polyIndex; ret.m_polyCorner = edgePoly1.m_polyCorner; }
      else         { ret.m_polyIndex = edgePoly0.m_polyIndex; ret.m_polyCorner = edgePoly0.m_polyCorner; }
    }
  }
  return ret;
}
// Port of CatmullClarkBuilder::FetchCornerPrev (DcciSubdiv.cpp:3256).
function fetchCornerPrev(helper0, srcCorner) {
  const ret = makePolyCorner();
  if (polyCornerIsValid(srcCorner)) {
    const polySize = helper0.m_polySizes[srcCorner.m_polyIndex];
    ret.m_polyIndex  = srcCorner.m_polyIndex;
    ret.m_polyCorner = (srcCorner.m_polyCorner + (polySize - 1)) % polySize;
  }
  return ret;
}
// Port of CatmullClarkBuilder::FetchCornerNext (DcciSubdiv.cpp:3273).
function fetchCornerNext(helper0, srcCorner) {
  const ret = makePolyCorner();
  if (polyCornerIsValid(srcCorner)) {
    const polySize = helper0.m_polySizes[srcCorner.m_polyIndex];
    ret.m_polyIndex  = srcCorner.m_polyIndex;
    ret.m_polyCorner = (srcCorner.m_polyCorner + 1) % polySize;
  }
  return ret;
}
// Port of CatmullClarkBuilder::FetchCornerNeighborCW (DcciSubdiv.cpp:3291).
function fetchCornerNeighborCW(helper0, srcCorner) {
  let ret = makePolyCorner();
  if (polyCornerIsValid(srcCorner)) {
    const prev = fetchCornerPrev(helper0, srcCorner);
    const prevOpp = fetchCornerOpp(helper0, prev);
    ret = prevOpp;
  }
  return ret;
}
// Port of CatmullClarkBuilder::FetchCornerNeighborCCW (DcciSubdiv.cpp:3306).
function fetchCornerNeighborCCW(helper0, srcCorner) {
  let ret = makePolyCorner();
  if (polyCornerIsValid(srcCorner)) {
    const opp = fetchCornerOpp(helper0, srcCorner);
    const oppNext = fetchCornerNext(helper0, opp);
    ret = oppNext;
  }
  return ret;
}
// Port of CatmullClarkBuilder::FetchCornerNeighborOppositeIfRegular (DcciSubdiv.cpp:3321).
function fetchCornerNeighborOppositeIfRegular(helper0, srcCorner) {
  let ret = makePolyCorner();
  if (polyCornerIsValid(srcCorner)) {
    const cw0 = fetchCornerNeighborCW(helper0, srcCorner);
    const cw1 = fetchCornerNeighborCW(helper0, cw0);
    const ccw0 = fetchCornerNeighborCCW(helper0, srcCorner);
    const ccw1 = fetchCornerNeighborCCW(helper0, ccw0);
    const isSame  = polyCornerIsSame(cw1, ccw1);
    const isValid = polyCornerIsValid(ccw1);
    if (isSame && isValid) ret = ccw1;
  }
  return ret;
}

// Port of CatmullClarkBuilder::GetPolyCornerVtx (DcciSubdiv.cpp:3344).
function getPolyCornerVtx(helper0, srcCorner) {
  let ret = -1;
  if (polyCornerIsValid(srcCorner)) {
    const polyStart = helper0.m_polyStarts[srcCorner.m_polyIndex];
    ret = helper0.m_polyIndexList[polyStart + srcCorner.m_polyCorner];
  }
  return ret;
}
// Port of CatmullClarkBuilder::GetPolyCornerVtxControlPoint (DcciSubdiv.cpp:3356).
function getPolyCornerVtxControlPoint(helper0, srcCorner) {
  if (!polyCornerIsValid(srcCorner)) {
    throw new Error('getPolyCornerVtxControlPoint: srcCorner not valid');
  }
  const v0 = getPolyCornerVtx(helper0, srcCorner);
  return controlPointMakeDirect(v0);
}
// Port of CatmullClarkBuilder::GetPolyCornerBorderControlPoint (DcciSubdiv.cpp:3370).
function getPolyCornerBorderControlPoint(helper0, currCorner, cp0, cp1) {
  const currV0 = getPolyCornerVtx(helper0, currCorner);
  return getPolyCornerBorderControlPointVtx(currV0, cp0, cp1);
}
// Port of CatmullClarkBuilder::GetPolyCornerBorderControlPointVtx (DcciSubdiv.cpp:3382).
function getPolyCornerBorderControlPointVtx(currV0, cp0, cp1) {
  let ret;
  if (currV0 >= 0) {
    ret = controlPointMakeDirect(currV0);
  } else {
    const type0 = controlPointGetType(cp0);
    const type1 = controlPointGetType(cp1);
    if (type0 !== kCpType_Direct) throw new Error('getPolyCornerBorderControlPointVtx: cp0 not Direct');
    if (type1 !== kCpType_Direct) throw new Error('getPolyCornerBorderControlPointVtx: cp1 not Direct');
    ret = controlPointMakeBorder(cp0.m_v0, cp1.m_v0);
  }
  return ret;
}
// Port of CatmullClarkBuilder::GetPolyCornerCornerControlPoint (DcciSubdiv.cpp:3410).
function getPolyCornerCornerControlPoint(helper0, currCorner, cp0, cp1, cp1_interior, cp2, cp2_interior) {
  const currV0 = getPolyCornerVtx(helper0, currCorner);
  return getPolyCornerCornerControlPointVtx(currV0, cp0, cp1, cp1_interior, cp2, cp2_interior);
}
// Port of CatmullClarkBuilder::GetPolyCornerCornerControlPointVtx (DcciSubdiv.cpp:3426).
function getPolyCornerCornerControlPointVtx(currV0, cp0, cp1, cp1_interior, cp2, cp2_interior) {
  let ret;
  if (currV0 >= 0) {
    ret = controlPointMakeDirect(currV0);
  } else {
    const type0 = controlPointGetType(cp0);
    const type1 = controlPointGetType(cp1);
    const type2 = controlPointGetType(cp2);
    const type1_interior = controlPointGetType(cp1_interior);
    const type2_interior = controlPointGetType(cp2_interior);

    if (type0 !== kCpType_Direct) throw new Error('getPolyCornerCornerControlPointVtx: cp0 not Direct');

    if (type1 !== kCpType_Direct && type1 !== kCpType_Border) {
      throw new Error('getPolyCornerCornerControlPointVtx: cp1 type ' + type1);
    }
    if (type2 !== kCpType_Direct && type2 !== kCpType_Border) {
      throw new Error('getPolyCornerCornerControlPointVtx: cp2 type ' + type2);
    }

    if (type1 === kCpType_Border) {
      if (cp1.m_v0 !== cp0.m_v0) throw new Error('getPolyCornerCornerControlPointVtx: cp1.m_v0 != cp0.m_v0');
    } else if (type1 === kCpType_Direct) {
      if (type1_interior !== kCpType_Direct) throw new Error('getPolyCornerCornerControlPointVtx: cp1_interior not Direct');
    } else {
      throw new Error('getPolyCornerCornerControlPointVtx: cp1 type unreachable');
    }

    if (type2 === kCpType_Border) {
      if (cp2.m_v0 !== cp0.m_v0) throw new Error('getPolyCornerCornerControlPointVtx: cp2.m_v0 != cp0.m_v0');
    } else if (type2 === kCpType_Direct) {
      if (type2_interior !== kCpType_Direct) throw new Error('getPolyCornerCornerControlPointVtx: cp2_interior not Direct');
    } else {
      throw new Error('getPolyCornerCornerControlPointVtx: cp2 type unreachable');
    }

    if (type1 === kCpType_Direct && type2 === kCpType_Direct) {
      ret = controlPointMakeConcaveCorner(cp0.m_v0, cp1.m_v0, cp2.m_v0);
    } else if (type1 === kCpType_Border && type2 === kCpType_Border) {
      ret = controlPointMakeConvexCorner(cp0.m_v0, cp1.m_v1, cp2.m_v1);
    } else if (type1 === kCpType_Direct && type2 === kCpType_Border) {
      ret = controlPointMakeBorder(cp1.m_v0, cp1_interior.m_v0);
    } else if (type1 === kCpType_Border && type2 === kCpType_Direct) {
      ret = controlPointMakeBorder(cp2.m_v0, cp2_interior.m_v0);
    } else {
      throw new Error('getPolyCornerCornerControlPointVtx: unreachable type combo');
    }
  }
  return ret;
}

// =====================================================================
// FetchIndex_1_* (level-1 indexing into the once-subdivided mesh).
// Indices into the would-be-built m_topo1, computed from m_topo0 and
// helper0 directly. Layout: [verts | edge points | face points].
// =====================================================================

// Port of CatmullClarkBuilder::FetchIndex_1_V (DcciSubdiv.cpp:2798).
function fetchIndex_1_V(topo0, helper0, polyIndex, cornerIndex) {
  if (polyIndex < 0) return -1;
  const polyStart = topo0.m_polyStarts[polyIndex];
  const polySize  = topo0.m_polySizes[polyIndex];
  if (!(cornerIndex < polySize)) throw new Error('fetchIndex_1_V: cornerIndex out of range');
  const vtx = topo0.m_polyIndexList[polyStart + cornerIndex];
  return vtx;
}
// Port of CatmullClarkBuilder::FetchIndex_1_E (DcciSubdiv.cpp:2813).
function fetchIndex_1_E(topo0, helper0, polyIndex, cornerIndex) {
  if (polyIndex < 0) return -1;
  const polyStart = topo0.m_polyStarts[polyIndex];
  const polySize  = topo0.m_polySizes[polyIndex];
  if (!(cornerIndex < polySize)) throw new Error('fetchIndex_1_E: cornerIndex out of range');
  const vtx0 = topo0.m_polyIndexList[polyStart + (cornerIndex + 0) % polySize];
  const vtx1 = topo0.m_polyIndexList[polyStart + (cornerIndex + 1) % polySize];
  const edge = edgeLookupFetchEdge(helper0.m_edgeLookup, vtx0, vtx1);
  return helper0.m_numVerts + edge;
}
// Port of CatmullClarkBuilder::FetchIndex_1_F (DcciSubdiv.cpp:2834).
function fetchIndex_1_F(topo0, helper0, polyIndex) {
  if (polyIndex < 0) return -1;
  return helper0.m_numVerts + helper0.m_numEdges + polyIndex;
}

// =====================================================================
// FetchIndex_2_* (level-2 indexing into the twice-subdivided mesh).
// Uses topo0 + helper0 + helper1. helper1 must be built from the
// level-1 mesh (topo1 = output of one ApplyCatmullClarkToWeights).
// =====================================================================

// Port of CatmullClarkBuilder::FetchIndex_2_VV (DcciSubdiv.cpp:2846).
function fetchIndex_2_VV(topo0, helper0, helper1, polyIndex, cornerIndex) {
  if (polyIndex < 0) return -1;
  return fetchIndex_1_V(topo0, helper0, polyIndex, cornerIndex);
}
// Port of CatmullClarkBuilder::FetchIndex_2_EV (DcciSubdiv.cpp:2859).
function fetchIndex_2_EV(topo0, helper0, helper1, polyIndex, cornerIndex) {
  if (polyIndex < 0) return -1;
  return fetchIndex_1_E(topo0, helper0, polyIndex, cornerIndex);
}
// Port of CatmullClarkBuilder::FetchIndex_2_EE (DcciSubdiv.cpp:2872).
function fetchIndex_2_EE(topo0, helper0, helper1, polyIndex, cornerIndex, side) {
  if (polyIndex < 0) return -1;
  const ev = fetchIndex_1_E(topo0, helper0, polyIndex, cornerIndex);
  let vv = -1;
  {
    const polyStart = topo0.m_polyStarts[polyIndex];
    const polySize  = topo0.m_polySizes[polyIndex];
    if (!(cornerIndex < polySize)) throw new Error('fetchIndex_2_EE: cornerIndex out of range');
    const vtx0 = topo0.m_polyIndexList[polyStart + (cornerIndex + 0) % polySize];
    const vtx1 = topo0.m_polyIndexList[polyStart + (cornerIndex + 1) % polySize];
    if (!(side === 0 || side === 1)) throw new Error('fetchIndex_2_EE: side ' + side);
    vv = (side === 0) ? vtx0 : vtx1;
  }
  const ee_edgeIndex = edgeLookupFetchEdge(helper1.m_edgeLookup, vv, ev);
  return helper1.m_numVerts + ee_edgeIndex;
}
// Port of CatmullClarkBuilder::FetchIndex_2_EF (DcciSubdiv.cpp:2901).
function fetchIndex_2_EF(topo0, helper0, helper1, polyIndex, cornerIndex) {
  if (polyIndex < 0) return -1;
  const ev = fetchIndex_1_E(topo0, helper0, polyIndex, cornerIndex);
  const fv = fetchIndex_1_F(topo0, helper0, polyIndex);
  const ef_edgeIndex = edgeLookupFetchEdge(helper1.m_edgeLookup, fv, ev);
  return helper1.m_numVerts + ef_edgeIndex;
}
// Port of CatmullClarkBuilder::FetchIndex_2_FF (DcciSubdiv.cpp:2917).
function fetchIndex_2_FF(topo0, helper0, helper1, polyIndex, cornerIndex) {
  if (polyIndex < 0) return -1;
  const polyStart0 = topo0.m_polyStarts[polyIndex];
  const f1 = polyStart0 + cornerIndex;
  return helper1.m_numVerts + helper1.m_numEdges + f1;
}
// Port of CatmullClarkBuilder::FetchIndex_2_FV (DcciSubdiv.cpp:2934).
function fetchIndex_2_FV(topo0, helper0, helper1, polyIndex) {
  if (polyIndex < 0) return -1;
  return fetchIndex_1_F(topo0, helper0, polyIndex);
}

// =====================================================================
// collectPointsForPatches: line-by-line port of
// CatmullClarkBuilder::CollectPointsForPatches (DcciSubdiv.cpp:6137).
// Writes m_regularVertexIndices[] + per-poly m_quadRegionGrid.m_pointsStart/Num
// + per-tri-subgrid m_triRegionGrids[corner].m_pointsStart/Num.
// =====================================================================
function collectPointsForPatches(patches, topo0, helper0, topo1, helper1) {
  if (patches.m_regularVertexIndices.length !== 0) {
    throw new Error('collectPointsForPatches: m_regularVertexIndices not empty');
  }
  const numPolys = helper0.m_polySizes.length;
  if (numPolys !== patches.m_polyInfo.length) {
    throw new Error('collectPointsForPatches: numPolys mismatch');
  }

  for (let polyIter = 0; polyIter < numPolys; polyIter++) {
    const polyInfo = patches.m_polyInfo[polyIter];
    const polyNumSides = helper0.m_polySizes[polyIter];

    if (polyInfo.m_type === kSubdivPolyType_Triangle) {
      // each triangle is split into 3 smaller 2x2 grids
      for (let subGrid = 0; subGrid < 3; subGrid++) {
        const corner0 = (subGrid + 0) % 3;
        const corner1 = (subGrid + 1) % 3;
        const corner2 = (subGrid + 2) % 3;

        const c0 = makePolyCorner(polyIter, corner0);
        const c1 = makePolyCorner(polyIter, corner1);
        const c2 = makePolyCorner(polyIter, corner2);

        const raw_vtx0 = getPolyCornerVtx(helper0, c0);
        const raw_vtx1 = getPolyCornerVtx(helper0, c1);
        const raw_vtx2 = getPolyCornerVtx(helper0, c2);
        void raw_vtx0; void raw_vtx1; void raw_vtx2;

        const subdiv_vtx0 = patches.m_vertexInfo[raw_vtx0].m_irregularSubdivLevel;
        const subdiv_vtx1 = patches.m_vertexInfo[raw_vtx1].m_irregularSubdivLevel;
        const subdiv_vtx2 = patches.m_vertexInfo[raw_vtx2].m_irregularSubdivLevel;
        void subdiv_vtx0; void subdiv_vtx1; void subdiv_vtx2;

        const c0_l = fetchCornerNeighborCCW(helper0, c0);
        const c0_r = fetchCornerNeighborCW(helper0, c0);
        const c0_o = fetchCornerNeighborOppositeIfRegular(helper0, c0);

        const c1_l = fetchCornerNeighborCCW(helper0, c1);
        const c1_r = fetchCornerNeighborCW(helper0, c1);
        const c1_o = fetchCornerNeighborOppositeIfRegular(helper0, c1);
        void c1_l; void c1_o;

        const c2_l = fetchCornerNeighborCCW(helper0, c2);
        const c2_r = fetchCornerNeighborCW(helper0, c2);
        const c2_o = fetchCornerNeighborOppositeIfRegular(helper0, c2);
        void c2_r; void c2_o;

        const vtx11 = fetchIndex_2_VV(topo0, helper0, helper1, polyIter, corner0);
        const vtx31 = fetchIndex_2_EV(topo0, helper0, helper1, polyIter, corner0);
        const vtx13 = fetchIndex_2_EV(topo0, helper0, helper1, polyIter, corner2);
        const vtx33 = fetchIndex_2_FV(topo0, helper0, helper1, polyIter);

        const vtx21 = fetchIndex_2_EE(topo0, helper0, helper1, polyIter, corner0, 0);
        const vtx12 = fetchIndex_2_EE(topo0, helper0, helper1, polyIter, corner2, 1);
        const vtx32 = fetchIndex_2_EF(topo0, helper0, helper1, polyIter, corner0);
        const vtx23 = fetchIndex_2_EF(topo0, helper0, helper1, polyIter, corner2);

        const vtx22 = fetchIndex_2_FF(topo0, helper0, helper1, polyIter, corner0);

        const vtx41 = fetchIndex_2_EE(topo0, helper0, helper1, polyIter, corner0, 1);
        const vtx42 = fetchIndex_2_FF(topo0, helper0, helper1, polyIter, corner1);
        const vtx43 = fetchIndex_2_EF(topo0, helper0, helper1, polyIter, corner1);
        const vtx44 = -1;
        const vtx34 = fetchIndex_2_EF(topo0, helper0, helper1, polyIter, corner1);
        const vtx24 = fetchIndex_2_FF(topo0, helper0, helper1, polyIter, corner2);
        const vtx14 = fetchIndex_2_EE(topo0, helper0, helper1, polyIter, corner2, 0);

        const vtx01_p = fetchCornerPrev(helper0, c0_r);
        const vtx01 = fetchIndex_2_EE(topo0, helper0, helper1, vtx01_p.m_polyIndex, vtx01_p.m_polyCorner, 1);

        const vtx02 = fetchIndex_2_FF(topo0, helper0, helper1, c0_r.m_polyIndex, c0_r.m_polyCorner);

        const vtx03_side = fetchCornerOpp(helper0, c2);
        const vtx03 = fetchIndex_2_EF(topo0, helper0, helper1, vtx03_side.m_polyIndex, vtx03_side.m_polyCorner);

        const vtx04 = fetchIndex_2_FF(topo0, helper0, helper1, c2_l.m_polyIndex, c2_l.m_polyCorner);

        const vtx10 = fetchIndex_2_EE(topo0, helper0, helper1, c0_l.m_polyIndex, c0_l.m_polyCorner, 0);
        const vtx20 = fetchIndex_2_FF(topo0, helper0, helper1, c0_l.m_polyIndex, c0_l.m_polyCorner);
        const vtx30 = fetchIndex_2_EF(topo0, helper0, helper1, c1_r.m_polyIndex, c1_r.m_polyCorner);
        const vtx40 = fetchIndex_2_FF(topo0, helper0, helper1, c1_r.m_polyIndex, c1_r.m_polyCorner);

        const vtx00 = fetchIndex_2_FF(topo0, helper0, helper1, c0_o.m_polyIndex, c0_o.m_polyCorner);

        const v11 = controlPointMakeDirect(vtx11);
        const v12 = controlPointMakeDirect(vtx12);
        const v13 = controlPointMakeDirect(vtx13);
        const v14 = controlPointMakeDirect(vtx14);
        const v21 = controlPointMakeDirect(vtx21);
        const v22 = controlPointMakeDirect(vtx22);
        const v23 = controlPointMakeDirect(vtx23);
        const v24 = controlPointMakeDirect(vtx24);
        const v31 = controlPointMakeDirect(vtx31);
        const v32 = controlPointMakeDirect(vtx32);
        const v33 = controlPointMakeDirect(vtx33);
        const v34 = controlPointMakeDirect(vtx34);
        const v41 = controlPointMakeDirect(vtx41);
        const v42 = controlPointMakeDirect(vtx42);
        const v43 = controlPointMakeDirect(vtx43);
        const v44 = controlPointMakeInvalid();
        void vtx44;

        const v01 = getPolyCornerBorderControlPointVtx(vtx01, v11, v21);
        const v02 = getPolyCornerBorderControlPointVtx(vtx02, v12, v22);
        const v03 = getPolyCornerBorderControlPointVtx(vtx03, v13, v23);
        const v04 = getPolyCornerBorderControlPointVtx(vtx04, v14, v24);

        const v10 = getPolyCornerBorderControlPointVtx(vtx10, v11, v12);
        const v20 = getPolyCornerBorderControlPointVtx(vtx20, v21, v22);
        const v30 = getPolyCornerBorderControlPointVtx(vtx30, v31, v32);
        const v40 = getPolyCornerBorderControlPointVtx(vtx40, v41, v42);

        const v00 = getPolyCornerCornerControlPointVtx(vtx00, v11, v01, v02, v10, v20);

        const numToAdd = 25;
        const regularStart = patches.m_regularVertexIndices.length;

        polyInfo.m_triRegionGrids[subGrid].m_pointsStart = regularStart;
        polyInfo.m_triRegionGrids[subGrid].m_pointsNum   = numToAdd;

        const cps = [
          v00, v01, v02, v03, v04,
          v10, v11, v12, v13, v14,
          v20, v21, v22, v23, v24,
          v30, v31, v32, v33, v34,
          v40, v41, v42, v43, v44,
        ];
        for (let k = 0; k < numToAdd; k++) patches.m_regularVertexIndices.push(cps[k]);
      }
    } else {
      if (polyNumSides !== 4) throw new Error('collectPointsForPatches: poly ' + polyIter + ' not 4-sided');

      const c0 = makePolyCorner(polyIter, 0);
      const c1 = makePolyCorner(polyIter, 1);
      const c2 = makePolyCorner(polyIter, 2);
      const c3 = makePolyCorner(polyIter, 3);

      const c0_l = fetchCornerNeighborCCW(helper0, c0);
      const c0_r = fetchCornerNeighborCW(helper0, c0);
      const c0_o = fetchCornerNeighborOppositeIfRegular(helper0, c0);

      const c1_l = fetchCornerNeighborCCW(helper0, c1);
      const c1_r = fetchCornerNeighborCW(helper0, c1);
      const c1_o = fetchCornerNeighborOppositeIfRegular(helper0, c1);

      const c2_l = fetchCornerNeighborCCW(helper0, c2);
      const c2_r = fetchCornerNeighborCW(helper0, c2);
      const c2_o = fetchCornerNeighborOppositeIfRegular(helper0, c2);

      const c3_l = fetchCornerNeighborCCW(helper0, c3);
      const c3_r = fetchCornerNeighborCW(helper0, c3);
      const c3_o = fetchCornerNeighborOppositeIfRegular(helper0, c3);

      const raw_vtx0 = getPolyCornerVtx(helper0, c0);
      const raw_vtx1 = getPolyCornerVtx(helper0, c1);
      const raw_vtx2 = getPolyCornerVtx(helper0, c2);
      const raw_vtx3 = getPolyCornerVtx(helper0, c3);

      const subdiv_vtx0 = patches.m_vertexInfo[raw_vtx0].m_irregularSubdivLevel;
      const subdiv_vtx1 = patches.m_vertexInfo[raw_vtx1].m_irregularSubdivLevel;
      const subdiv_vtx2 = patches.m_vertexInfo[raw_vtx2].m_irregularSubdivLevel;
      const subdiv_vtx3 = patches.m_vertexInfo[raw_vtx3].m_irregularSubdivLevel;

      if (polyInfo.m_type === kSubdivPolyType_QuadRegular) {
        if (subdiv_vtx0 !== 0) throw new Error('collectPointsForPatches: regular poly subdiv_vtx0 != 0');
        if (subdiv_vtx1 !== 0) throw new Error('collectPointsForPatches: regular poly subdiv_vtx1 != 0');
        if (subdiv_vtx2 !== 0) throw new Error('collectPointsForPatches: regular poly subdiv_vtx2 != 0');
        if (subdiv_vtx3 !== 0) throw new Error('collectPointsForPatches: regular poly subdiv_vtx3 != 0');

        const v11 = getPolyCornerVtxControlPoint(helper0, c0);
        const v12 = getPolyCornerVtxControlPoint(helper0, c3);
        const v21 = getPolyCornerVtxControlPoint(helper0, c1);
        const v22 = getPolyCornerVtxControlPoint(helper0, c2);

        const vtx01 = fetchCornerPrev(helper0, c0_r);
        const vtx20 = fetchCornerPrev(helper0, c1_r);
        const vtx32 = fetchCornerPrev(helper0, c2_r);
        const vtx13 = fetchCornerPrev(helper0, c3_r);

        const vtx10 = fetchCornerNext(helper0, c0_l);
        const vtx31 = fetchCornerNext(helper0, c1_l);
        const vtx23 = fetchCornerNext(helper0, c2_l);
        const vtx02 = fetchCornerNext(helper0, c3_l);

        const vtx00 = fetchCornerNext(helper0, fetchCornerNext(helper0, c0_o));
        const vtx30 = fetchCornerNext(helper0, fetchCornerNext(helper0, c1_o));
        const vtx33 = fetchCornerNext(helper0, fetchCornerNext(helper0, c2_o));
        const vtx03 = fetchCornerNext(helper0, fetchCornerNext(helper0, c3_o));

        const v01 = getPolyCornerBorderControlPoint(helper0, vtx01, v11, v21);
        const v02 = getPolyCornerBorderControlPoint(helper0, vtx02, v12, v22);

        const v10 = getPolyCornerBorderControlPoint(helper0, vtx10, v11, v12);
        const v20 = getPolyCornerBorderControlPoint(helper0, vtx20, v21, v22);

        const v31 = getPolyCornerBorderControlPoint(helper0, vtx31, v21, v11);
        const v32 = getPolyCornerBorderControlPoint(helper0, vtx32, v22, v12);

        const v13 = getPolyCornerBorderControlPoint(helper0, vtx13, v12, v11);
        const v23 = getPolyCornerBorderControlPoint(helper0, vtx23, v22, v21);

        const v00 = getPolyCornerCornerControlPoint(helper0, vtx00, v11, v01, v02, v10, v20);
        const v30 = getPolyCornerCornerControlPoint(helper0, vtx30, v21, v20, v10, v31, v32);
        const v33 = getPolyCornerCornerControlPoint(helper0, vtx33, v22, v23, v13, v32, v31);
        const v03 = getPolyCornerCornerControlPoint(helper0, vtx03, v12, v13, v23, v02, v01);

        const numToAdd = 16;
        const regularStart = patches.m_regularVertexIndices.length;

        polyInfo.m_quadRegionGrid.m_pointsStart = regularStart;
        polyInfo.m_quadRegionGrid.m_pointsNum   = numToAdd;

        const cps = [
          v00, v01, v02, v03,
          v10, v11, v12, v13,
          v20, v21, v22, v23,
          v30, v31, v32, v33,
        ];
        for (let k = 0; k < numToAdd; k++) patches.m_regularVertexIndices.push(cps[k]);
      } else if (polyInfo.m_type === kSubdivPolyType_QuadIrregular1) {
        if (!(subdiv_vtx0 <= 1)) throw new Error('collectPointsForPatches: irregular1 subdiv_vtx0 > 1');
        if (!(subdiv_vtx1 <= 1)) throw new Error('collectPointsForPatches: irregular1 subdiv_vtx1 > 1');
        if (!(subdiv_vtx2 <= 1)) throw new Error('collectPointsForPatches: irregular1 subdiv_vtx2 > 1');
        if (!(subdiv_vtx3 <= 1)) throw new Error('collectPointsForPatches: irregular1 subdiv_vtx3 > 1');

        const vtx11 = fetchIndex_1_V(topo0, helper0, polyIter, 0);
        const vtx31 = fetchIndex_1_V(topo0, helper0, polyIter, 1);
        const vtx33 = fetchIndex_1_V(topo0, helper0, polyIter, 2);
        const vtx13 = fetchIndex_1_V(topo0, helper0, polyIter, 3);

        const vtx21 = fetchIndex_1_E(topo0, helper0, polyIter, 0);
        const vtx32 = fetchIndex_1_E(topo0, helper0, polyIter, 1);
        const vtx23 = fetchIndex_1_E(topo0, helper0, polyIter, 2);
        const vtx12 = fetchIndex_1_E(topo0, helper0, polyIter, 3);

        const vtx22 = fetchIndex_1_F(topo0, helper0, polyIter);

        const vtx01_p = fetchCornerPrev(helper0, c0_r);
        const vtx01   = fetchIndex_1_E(topo0, helper0, vtx01_p.m_polyIndex, vtx01_p.m_polyCorner);

        const vtx30_p = fetchCornerPrev(helper0, c1_r);
        const vtx30   = fetchIndex_1_E(topo0, helper0, vtx30_p.m_polyIndex, vtx30_p.m_polyCorner);

        const vtx43_p = fetchCornerPrev(helper0, c2_r);
        const vtx43   = fetchIndex_1_E(topo0, helper0, vtx43_p.m_polyIndex, vtx43_p.m_polyCorner);

        const vtx14_p = fetchCornerPrev(helper0, c3_r);
        const vtx14   = fetchIndex_1_E(topo0, helper0, vtx14_p.m_polyIndex, vtx14_p.m_polyCorner);

        const vtx10 = fetchIndex_1_E(topo0, helper0, c0_l.m_polyIndex, c0_l.m_polyCorner);
        const vtx41 = fetchIndex_1_E(topo0, helper0, c1_l.m_polyIndex, c1_l.m_polyCorner);
        const vtx34 = fetchIndex_1_E(topo0, helper0, c2_l.m_polyIndex, c2_l.m_polyCorner);
        const vtx03 = fetchIndex_1_E(topo0, helper0, c3_l.m_polyIndex, c3_l.m_polyCorner);

        const vtx00 = fetchIndex_1_F(topo0, helper0, c0_o.m_polyIndex);
        const vtx40 = fetchIndex_1_F(topo0, helper0, c1_o.m_polyIndex);
        const vtx44 = fetchIndex_1_F(topo0, helper0, c2_o.m_polyIndex);
        const vtx04 = fetchIndex_1_F(topo0, helper0, c3_o.m_polyIndex);

        const vtx20 = fetchIndex_1_F(topo0, helper0, c0_l.m_polyIndex);
        const vtx42 = fetchIndex_1_F(topo0, helper0, c1_l.m_polyIndex);
        const vtx24 = fetchIndex_1_F(topo0, helper0, c2_l.m_polyIndex);
        const vtx02 = fetchIndex_1_F(topo0, helper0, c3_l.m_polyIndex);

        const v11 = controlPointMakeDirect(vtx11);
        const v12 = controlPointMakeDirect(vtx12);
        const v13 = controlPointMakeDirect(vtx13);
        const v21 = controlPointMakeDirect(vtx21);
        const v22 = controlPointMakeDirect(vtx22);
        const v23 = controlPointMakeDirect(vtx23);
        const v31 = controlPointMakeDirect(vtx31);
        const v32 = controlPointMakeDirect(vtx32);
        const v33 = controlPointMakeDirect(vtx33);

        const v01 = getPolyCornerBorderControlPointVtx(vtx01, v11, v21);
        const v02 = getPolyCornerBorderControlPointVtx(vtx02, v12, v22);
        const v03 = getPolyCornerBorderControlPointVtx(vtx03, v13, v23);

        const v41 = getPolyCornerBorderControlPointVtx(vtx41, v31, v21);
        const v42 = getPolyCornerBorderControlPointVtx(vtx42, v32, v22);
        const v43 = getPolyCornerBorderControlPointVtx(vtx43, v33, v23);

        const v10 = getPolyCornerBorderControlPointVtx(vtx10, v11, v12);
        const v20 = getPolyCornerBorderControlPointVtx(vtx20, v21, v22);
        const v30 = getPolyCornerBorderControlPointVtx(vtx30, v31, v32);

        const v14 = getPolyCornerBorderControlPointVtx(vtx14, v13, v12);
        const v24 = getPolyCornerBorderControlPointVtx(vtx24, v23, v22);
        const v34 = getPolyCornerBorderControlPointVtx(vtx34, v33, v32);

        let v00 = getPolyCornerCornerControlPointVtx(vtx00, v11, v01, v02, v10, v20);
        let v40 = getPolyCornerCornerControlPointVtx(vtx40, v31, v30, v20, v41, v42);
        let v44 = getPolyCornerCornerControlPointVtx(vtx44, v33, v43, v42, v34, v24);
        let v04 = getPolyCornerCornerControlPointVtx(vtx04, v13, v14, v24, v03, v02);

        if (subdiv_vtx0 > 0) v00 = controlPointMakeInvalid();
        if (subdiv_vtx1 > 0) v40 = controlPointMakeInvalid();
        if (subdiv_vtx2 > 0) v44 = controlPointMakeInvalid();
        if (subdiv_vtx3 > 0) v04 = controlPointMakeInvalid();

        const numToAdd = 25;
        const regularStart = patches.m_regularVertexIndices.length;

        polyInfo.m_quadRegionGrid.m_pointsStart = regularStart;
        polyInfo.m_quadRegionGrid.m_pointsNum   = numToAdd;

        const cps = [
          v00, v01, v02, v03, v04,
          v10, v11, v12, v13, v14,
          v20, v21, v22, v23, v24,
          v30, v31, v32, v33, v34,
          v40, v41, v42, v43, v44,
        ];
        for (let k = 0; k < numToAdd; k++) patches.m_regularVertexIndices.push(cps[k]);
      } else if (polyInfo.m_type === kSubdivPolyType_QuadIrregular2) {
        const vtx11 = fetchIndex_2_VV(topo0, helper0, helper1, polyIter, 0);
        const vtx51 = fetchIndex_2_VV(topo0, helper0, helper1, polyIter, 1);
        const vtx55 = fetchIndex_2_VV(topo0, helper0, helper1, polyIter, 2);
        const vtx15 = fetchIndex_2_VV(topo0, helper0, helper1, polyIter, 3);

        const vtx31 = fetchIndex_2_EV(topo0, helper0, helper1, polyIter, 0);
        const vtx53 = fetchIndex_2_EV(topo0, helper0, helper1, polyIter, 1);
        const vtx35 = fetchIndex_2_EV(topo0, helper0, helper1, polyIter, 2);
        const vtx13 = fetchIndex_2_EV(topo0, helper0, helper1, polyIter, 3);

        const vtx21 = fetchIndex_2_EE(topo0, helper0, helper1, polyIter, 0, 0);
        const vtx52 = fetchIndex_2_EE(topo0, helper0, helper1, polyIter, 1, 0);
        const vtx45 = fetchIndex_2_EE(topo0, helper0, helper1, polyIter, 2, 0);
        const vtx14 = fetchIndex_2_EE(topo0, helper0, helper1, polyIter, 3, 0);

        const vtx41 = fetchIndex_2_EE(topo0, helper0, helper1, polyIter, 0, 1);
        const vtx54 = fetchIndex_2_EE(topo0, helper0, helper1, polyIter, 1, 1);
        const vtx25 = fetchIndex_2_EE(topo0, helper0, helper1, polyIter, 2, 1);
        const vtx12 = fetchIndex_2_EE(topo0, helper0, helper1, polyIter, 3, 1);

        const vtx32 = fetchIndex_2_EF(topo0, helper0, helper1, polyIter, 0);
        const vtx43 = fetchIndex_2_EF(topo0, helper0, helper1, polyIter, 1);
        const vtx34 = fetchIndex_2_EF(topo0, helper0, helper1, polyIter, 2);
        const vtx23 = fetchIndex_2_EF(topo0, helper0, helper1, polyIter, 3);

        const vtx22 = fetchIndex_2_FF(topo0, helper0, helper1, polyIter, 0);
        const vtx42 = fetchIndex_2_FF(topo0, helper0, helper1, polyIter, 1);
        const vtx44 = fetchIndex_2_FF(topo0, helper0, helper1, polyIter, 2);
        const vtx24 = fetchIndex_2_FF(topo0, helper0, helper1, polyIter, 3);

        const vtx33 = fetchIndex_2_FV(topo0, helper0, helper1, polyIter);

        const vtx01_p = fetchCornerPrev(helper0, c0_r);
        const vtx01   = fetchIndex_2_EE(topo0, helper0, helper1, vtx01_p.m_polyIndex, vtx01_p.m_polyCorner, 1);

        const vtx50_p = fetchCornerPrev(helper0, c1_r);
        const vtx50   = fetchIndex_2_EE(topo0, helper0, helper1, vtx50_p.m_polyIndex, vtx50_p.m_polyCorner, 1);

        const vtx65_p = fetchCornerPrev(helper0, c2_r);
        const vtx65   = fetchIndex_2_EE(topo0, helper0, helper1, vtx65_p.m_polyIndex, vtx65_p.m_polyCorner, 1);

        const vtx16_p = fetchCornerPrev(helper0, c3_r);
        const vtx16   = fetchIndex_2_EE(topo0, helper0, helper1, vtx16_p.m_polyIndex, vtx16_p.m_polyCorner, 1);

        const vtx10 = fetchIndex_2_EE(topo0, helper0, helper1, c0_l.m_polyIndex, c0_l.m_polyCorner, 0);
        const vtx61 = fetchIndex_2_EE(topo0, helper0, helper1, c1_l.m_polyIndex, c1_l.m_polyCorner, 0);
        const vtx56 = fetchIndex_2_EE(topo0, helper0, helper1, c2_l.m_polyIndex, c2_l.m_polyCorner, 0);
        const vtx05 = fetchIndex_2_EE(topo0, helper0, helper1, c3_l.m_polyIndex, c3_l.m_polyCorner, 0);

        const vtx02 = fetchIndex_2_FF(topo0, helper0, helper1, c0_r.m_polyIndex, c0_r.m_polyCorner);
        const vtx40 = fetchIndex_2_FF(topo0, helper0, helper1, c1_r.m_polyIndex, c1_r.m_polyCorner);
        const vtx64 = fetchIndex_2_FF(topo0, helper0, helper1, c2_r.m_polyIndex, c2_r.m_polyCorner);
        const vtx26 = fetchIndex_2_FF(topo0, helper0, helper1, c3_r.m_polyIndex, c3_r.m_polyCorner);

        const vtx20 = fetchIndex_2_FF(topo0, helper0, helper1, c0_l.m_polyIndex, c0_l.m_polyCorner);
        const vtx62 = fetchIndex_2_FF(topo0, helper0, helper1, c1_l.m_polyIndex, c1_l.m_polyCorner);
        const vtx46 = fetchIndex_2_FF(topo0, helper0, helper1, c2_l.m_polyIndex, c2_l.m_polyCorner);
        const vtx04 = fetchIndex_2_FF(topo0, helper0, helper1, c3_l.m_polyIndex, c3_l.m_polyCorner);

        const vtx30_side = fetchCornerOpp(helper0, c0);
        const vtx30 = fetchIndex_2_EF(topo0, helper0, helper1, vtx30_side.m_polyIndex, vtx30_side.m_polyCorner);

        const vtx63_side = fetchCornerOpp(helper0, c1);
        const vtx63 = fetchIndex_2_EF(topo0, helper0, helper1, vtx63_side.m_polyIndex, vtx63_side.m_polyCorner);

        const vtx36_side = fetchCornerOpp(helper0, c2);
        const vtx36 = fetchIndex_2_EF(topo0, helper0, helper1, vtx36_side.m_polyIndex, vtx36_side.m_polyCorner);

        const vtx03_side = fetchCornerOpp(helper0, c3);
        const vtx03 = fetchIndex_2_EF(topo0, helper0, helper1, vtx03_side.m_polyIndex, vtx03_side.m_polyCorner);

        const vtx00 = fetchIndex_2_FF(topo0, helper0, helper1, c0_o.m_polyIndex, c0_o.m_polyCorner);
        const vtx60 = fetchIndex_2_FF(topo0, helper0, helper1, c1_o.m_polyIndex, c1_o.m_polyCorner);
        const vtx66 = fetchIndex_2_FF(topo0, helper0, helper1, c2_o.m_polyIndex, c2_o.m_polyCorner);
        const vtx06 = fetchIndex_2_FF(topo0, helper0, helper1, c3_o.m_polyIndex, c3_o.m_polyCorner);

        const v11 = controlPointMakeDirect(vtx11);
        const v12 = controlPointMakeDirect(vtx12);
        const v13 = controlPointMakeDirect(vtx13);
        const v14 = controlPointMakeDirect(vtx14);
        const v15 = controlPointMakeDirect(vtx15);

        const v21 = controlPointMakeDirect(vtx21);
        const v22 = controlPointMakeDirect(vtx22);
        const v23 = controlPointMakeDirect(vtx23);
        const v24 = controlPointMakeDirect(vtx24);
        const v25 = controlPointMakeDirect(vtx25);

        const v31 = controlPointMakeDirect(vtx31);
        const v32 = controlPointMakeDirect(vtx32);
        const v33 = controlPointMakeDirect(vtx33);
        const v34 = controlPointMakeDirect(vtx34);
        const v35 = controlPointMakeDirect(vtx35);

        const v41 = controlPointMakeDirect(vtx41);
        const v42 = controlPointMakeDirect(vtx42);
        const v43 = controlPointMakeDirect(vtx43);
        const v44 = controlPointMakeDirect(vtx44);
        const v45 = controlPointMakeDirect(vtx45);

        const v51 = controlPointMakeDirect(vtx51);
        const v52 = controlPointMakeDirect(vtx52);
        const v53 = controlPointMakeDirect(vtx53);
        const v54 = controlPointMakeDirect(vtx54);
        const v55 = controlPointMakeDirect(vtx55);

        const v01 = getPolyCornerBorderControlPointVtx(vtx01, v11, v21);
        const v02 = getPolyCornerBorderControlPointVtx(vtx02, v12, v22);
        const v03 = getPolyCornerBorderControlPointVtx(vtx03, v13, v23);
        const v04 = getPolyCornerBorderControlPointVtx(vtx04, v14, v24);
        const v05 = getPolyCornerBorderControlPointVtx(vtx05, v15, v25);

        const v61 = getPolyCornerBorderControlPointVtx(vtx61, v51, v41);
        const v62 = getPolyCornerBorderControlPointVtx(vtx62, v52, v42);
        const v63 = getPolyCornerBorderControlPointVtx(vtx63, v53, v43);
        const v64 = getPolyCornerBorderControlPointVtx(vtx64, v54, v44);
        const v65 = getPolyCornerBorderControlPointVtx(vtx65, v55, v45);

        const v10 = getPolyCornerBorderControlPointVtx(vtx10, v11, v12);
        const v20 = getPolyCornerBorderControlPointVtx(vtx20, v21, v22);
        const v30 = getPolyCornerBorderControlPointVtx(vtx30, v31, v32);
        const v40 = getPolyCornerBorderControlPointVtx(vtx40, v41, v42);
        const v50 = getPolyCornerBorderControlPointVtx(vtx50, v51, v52);

        const v16 = getPolyCornerBorderControlPointVtx(vtx16, v15, v14);
        const v26 = getPolyCornerBorderControlPointVtx(vtx26, v25, v24);
        const v36 = getPolyCornerBorderControlPointVtx(vtx36, v35, v34);
        const v46 = getPolyCornerBorderControlPointVtx(vtx46, v45, v44);
        const v56 = getPolyCornerBorderControlPointVtx(vtx56, v55, v54);

        let v00 = getPolyCornerCornerControlPointVtx(vtx00, v11, v01, v02, v10, v20);
        let v60 = getPolyCornerCornerControlPointVtx(vtx60, v51, v61, v62, v50, v40);
        let v66 = getPolyCornerCornerControlPointVtx(vtx66, v55, v56, v46, v65, v64);
        let v06 = getPolyCornerCornerControlPointVtx(vtx06, v15, v16, v26, v05, v04);

        if (subdiv_vtx0 > 0) v00 = controlPointMakeInvalid();
        if (subdiv_vtx1 > 0) v60 = controlPointMakeInvalid();
        if (subdiv_vtx2 > 0) v66 = controlPointMakeInvalid();
        if (subdiv_vtx3 > 0) v06 = controlPointMakeInvalid();

        const numToAdd = 49;
        const regularStart = patches.m_regularVertexIndices.length;

        polyInfo.m_quadRegionGrid.m_pointsStart = regularStart;
        polyInfo.m_quadRegionGrid.m_pointsNum   = numToAdd;

        const cps = [
          v00, v01, v02, v03, v04, v05, v06,
          v10, v11, v12, v13, v14, v15, v16,
          v20, v21, v22, v23, v24, v25, v26,
          v30, v31, v32, v33, v34, v35, v36,
          v40, v41, v42, v43, v44, v45, v46,
          v50, v51, v52, v53, v54, v55, v56,
          v60, v61, v62, v63, v64, v65, v66,
        ];
        for (let k = 0; k < numToAdd; k++) patches.m_regularVertexIndices.push(cps[k]);
      } else {
        throw new Error('collectPointsForPatches: bad polyType ' + polyInfo.m_type);
      }
    }
  }
}

// =====================================================================
// collectPointsForIrregular: line-by-line port of
// CatmullClarkBuilder::CollectPointsForIrregular (DcciSubdiv.cpp:4552).
// For each entry in m_irregularWedgeInfo[], computes the control-point
// indices (1 + 6 * valence [+ 2 if not circular]) for a vertex wedge,
// or 1 + 18 (= 19) for a triangle center. Appends to
// m_patches.m_irregularVertexIndices and writes
// irregularInfo.m_irregularVertexStart / m_irregularVertexNum +
// (vertex path) m_isCircular / m_valence.
// =====================================================================
function collectPointsForIrregular(patches, topo0, helper0, topo1, helper1) {
  for (let irregularIter = 0; irregularIter < patches.m_irregularWedgeInfo.length; irregularIter++) {
    const irregularInfo = patches.m_irregularWedgeInfo[irregularIter];

    if (!(irregularInfo.m_irregularVertexStart < 0)) throw new Error('collectPointsForIrregular: m_irregularVertexStart not -1');
    if (!(irregularInfo.m_irregularVertexNum   < 0)) throw new Error('collectPointsForIrregular: m_irregularVertexNum not -1');

    const okV = irregularInfo.m_centerTrianglePoly >= 0 && irregularInfo.m_centerVertex <  0;
    const okT = irregularInfo.m_centerTrianglePoly <  0 && irregularInfo.m_centerVertex >= 0;
    if (!(okV || okT)) throw new Error('collectPointsForIrregular: ambiguous (tri/vtx) center');

    if (irregularInfo.m_centerVertex >= 0) {
      if (!(irregularInfo.m_centerTrianglePoly < 0)) throw new Error('collectPointsForIrregular: tri+vtx both set');

      const wedgeVtxStart = helper0.m_wedges.m_wedgeEdgeStart[irregularInfo.m_centerVertex];
      const wedgeIndex    = wedgeVtxStart + irregularInfo.m_localWedgeIndex;

      const wedgeEdgeInfo = helper0.m_wedges.m_wedgeEdgeInfo[wedgeIndex];

      irregularInfo.m_isCircular = wedgeEdgeInfo.m_isCircular;
      irregularInfo.m_valence    = irregularInfo.m_isCircular ? wedgeEdgeInfo.m_wedgeEdgeNum : wedgeEdgeInfo.m_wedgeEdgeNum - 1;

      let numPts = -1;
      if (irregularInfo.m_isCircular) {
        numPts = 1 + 6 * irregularInfo.m_valence;
      } else {
        numPts = 1 + 6 * irregularInfo.m_valence + 2;
      }

      const dstCps = new Array(numPts);
      for (let i = 0; i < numPts; i++) dstCps[i] = makeControlPoint();

      const vertInfo = patches.m_vertexInfo[irregularInfo.m_centerVertex];

      if (wedgeEdgeInfo.m_isCircular !== irregularInfo.m_isCircular) {
        throw new Error('collectPointsForIrregular: isCircular mismatch');
      }

      if (vertInfo.m_irregularSubdivLevel === 1) {
        const vtx_c = irregularInfo.m_centerVertex;
        dstCps[0] = controlPointMakeDirect(vtx_c);

        for (let valenceIter = 0; valenceIter < irregularInfo.m_valence; valenceIter++) {
          const cornerInfo = helper0.m_wedges.m_wedgeCornerData[wedgeEdgeInfo.m_wedgeEdgeStart + valenceIter];

          const polySize = helper0.m_polySizes[cornerInfo.m_polyIndex];
          if (polySize !== 4) throw new Error('collectPointsForIrregular: subdiv-1 polySize != 4');

          const corner0 = makePolyCorner(cornerInfo.m_polyIndex, cornerInfo.m_polyCorner);
          const corner1 = fetchCornerNext(helper0, corner0);
          const corner2 = fetchCornerNext(helper0, corner1);

          const vtx00 = fetchIndex_1_E(topo0, helper0, corner0.m_polyIndex, corner0.m_polyCorner);
          const vtx11 = fetchIndex_1_E(topo0, helper0, corner1.m_polyIndex, corner1.m_polyCorner);
          const vtx02 = fetchIndex_1_E(topo0, helper0, corner2.m_polyIndex, corner2.m_polyCorner);

          const vtx01 = fetchIndex_1_F(topo0, helper0, corner0.m_polyIndex);

          const vtx10 = fetchIndex_1_V(topo0, helper0, corner1.m_polyIndex, corner1.m_polyCorner);
          const vtx12 = fetchIndex_1_V(topo0, helper0, corner2.m_polyIndex, corner2.m_polyCorner);

          dstCps[1 + 6 * valenceIter + 0] = controlPointMakeDirect(vtx00);
          dstCps[1 + 6 * valenceIter + 1] = controlPointMakeDirect(vtx01);
          dstCps[1 + 6 * valenceIter + 2] = controlPointMakeDirect(vtx02);
          dstCps[1 + 6 * valenceIter + 3] = controlPointMakeDirect(vtx10);
          dstCps[1 + 6 * valenceIter + 4] = controlPointMakeDirect(vtx11);
          dstCps[1 + 6 * valenceIter + 5] = controlPointMakeDirect(vtx12);
        }

        if (!irregularInfo.m_isCircular) {
          const valenceIter = irregularInfo.m_valence - 1;

          const cornerInfo = helper0.m_wedges.m_wedgeCornerData[wedgeEdgeInfo.m_wedgeEdgeStart + valenceIter];

          const polySize = helper0.m_polySizes[cornerInfo.m_polyIndex];
          if (polySize !== 4) throw new Error('collectPointsForIrregular: subdiv-1 tail polySize != 4');

          const corner0 = makePolyCorner(cornerInfo.m_polyIndex, cornerInfo.m_polyCorner);
          const corner1 = fetchCornerNext(helper0, corner0);
          const corner2 = fetchCornerNext(helper0, corner1);
          const corner3 = fetchCornerNext(helper0, corner2);

          const vtx00 = fetchIndex_1_E(topo0, helper0, corner3.m_polyIndex, corner3.m_polyCorner);
          const vtx10 = fetchIndex_1_V(topo0, helper0, corner3.m_polyIndex, corner3.m_polyCorner);

          dstCps[1 + 6 * irregularInfo.m_valence + 0] = controlPointMakeDirect(vtx00);
          dstCps[1 + 6 * irregularInfo.m_valence + 1] = controlPointMakeDirect(vtx10);
        }
      } else if (vertInfo.m_irregularSubdivLevel === 2) {
        const vtx_c = irregularInfo.m_centerVertex;
        dstCps[0] = controlPointMakeDirect(vtx_c);

        for (let valenceIter = 0; valenceIter < irregularInfo.m_valence; valenceIter++) {
          const cornerInfo = helper0.m_wedges.m_wedgeCornerData[wedgeEdgeInfo.m_wedgeEdgeStart + valenceIter];

          const polySize = helper0.m_polySizes[cornerInfo.m_polyIndex];
          if (polySize !== 4 && polySize !== 3) {
            throw new Error('collectPointsForIrregular: subdiv-2 polySize ' + polySize);
          }

          const corner0    = makePolyCorner(cornerInfo.m_polyIndex, cornerInfo.m_polyCorner);
          const cornerLast = fetchCornerPrev(helper0, corner0);

          const vtx00 = fetchIndex_2_EE(topo0, helper0, helper1, corner0.m_polyIndex, corner0.m_polyCorner, 0);
          const vtx01 = fetchIndex_2_FF(topo0, helper0, helper1, corner0.m_polyIndex, corner0.m_polyCorner);
          const vtx02 = fetchIndex_2_EF(topo0, helper0, helper1, cornerLast.m_polyIndex, cornerLast.m_polyCorner);

          const vtx10 = fetchIndex_2_EV(topo0, helper0, helper1, corner0.m_polyIndex, corner0.m_polyCorner);
          const vtx11 = fetchIndex_2_EF(topo0, helper0, helper1, corner0.m_polyIndex, corner0.m_polyCorner);
          const vtx12 = fetchIndex_2_FV(topo0, helper0, helper1, corner0.m_polyIndex);

          dstCps[1 + 6 * valenceIter + 0] = controlPointMakeDirect(vtx00);
          dstCps[1 + 6 * valenceIter + 1] = controlPointMakeDirect(vtx01);
          dstCps[1 + 6 * valenceIter + 2] = controlPointMakeDirect(vtx02);
          dstCps[1 + 6 * valenceIter + 3] = controlPointMakeDirect(vtx10);
          dstCps[1 + 6 * valenceIter + 4] = controlPointMakeDirect(vtx11);
          dstCps[1 + 6 * valenceIter + 5] = controlPointMakeDirect(vtx12);
        }

        if (!irregularInfo.m_isCircular) {
          const valenceIter = irregularInfo.m_valence - 1;

          const cornerInfo = helper0.m_wedges.m_wedgeCornerData[wedgeEdgeInfo.m_wedgeEdgeStart + valenceIter];

          const polySize = helper0.m_polySizes[cornerInfo.m_polyIndex];
          if (polySize !== 4 && polySize !== 3) {
            throw new Error('collectPointsForIrregular: subdiv-2 tail polySize ' + polySize);
          }

          const corner0    = makePolyCorner(cornerInfo.m_polyIndex, cornerInfo.m_polyCorner);
          const cornerLast = fetchCornerPrev(helper0, corner0);

          const vtx00 = fetchIndex_2_EE(topo0, helper0, helper1, cornerLast.m_polyIndex, cornerLast.m_polyCorner, 1);
          const vtx10 = fetchIndex_2_EV(topo0, helper0, helper1, cornerLast.m_polyIndex, cornerLast.m_polyCorner);

          dstCps[1 + 6 * irregularInfo.m_valence + 0] = controlPointMakeDirect(vtx00);
          dstCps[1 + 6 * irregularInfo.m_valence + 1] = controlPointMakeDirect(vtx10);
        }
      } else {
        throw new Error('collectPointsForIrregular: irregularSubdivLevel ' + vertInfo.m_irregularSubdivLevel);
      }

      const dstStart = patches.m_irregularVertexIndices.length;
      irregularInfo.m_irregularVertexStart = dstStart;
      irregularInfo.m_irregularVertexNum   = dstCps.length;

      for (let i = 0; i < dstCps.length; i++) patches.m_irregularVertexIndices.push(dstCps[i]);
    } else {
      if (!(irregularInfo.m_centerTrianglePoly >= 0)) throw new Error('collectPointsForIrregular: no center');
      const numPts = 1 + 6 * 3;   // always 19 points going around a triangle center

      const polyIndex = irregularInfo.m_centerTrianglePoly;
      if (!irregularInfo.m_isCircular)        throw new Error('collectPointsForIrregular: tri must be circular');
      if (irregularInfo.m_valence !== 3)      throw new Error('collectPointsForIrregular: tri valence != 3');
      if (!(irregularInfo.m_localWedgeIndex < 0)) throw new Error('collectPointsForIrregular: tri localWedgeIndex >= 0');

      const vtxCenter = fetchIndex_2_FV(topo0, helper0, helper1, polyIndex);

      const vtx_0_00 = fetchIndex_2_EF(topo0, helper0, helper1, polyIndex, 2);
      const vtx_0_01 = fetchIndex_2_FF(topo0, helper0, helper1, polyIndex, 0);
      const vtx_0_02 = fetchIndex_2_EE(topo0, helper0, helper1, polyIndex, 0, 0);

      const vtx_0_10 = fetchIndex_2_EV(topo0, helper0, helper1, polyIndex, 2);
      const vtx_0_11 = fetchIndex_2_EE(topo0, helper0, helper1, polyIndex, 2, 1);
      const vtx_0_12 = fetchIndex_2_VV(topo0, helper0, helper1, polyIndex, 0);

      const vtx_1_00 = fetchIndex_2_EF(topo0, helper0, helper1, polyIndex, 0);
      const vtx_1_01 = fetchIndex_2_FF(topo0, helper0, helper1, polyIndex, 1);
      const vtx_1_02 = fetchIndex_2_EE(topo0, helper0, helper1, polyIndex, 1, 0);

      const vtx_1_10 = fetchIndex_2_EV(topo0, helper0, helper1, polyIndex, 0);
      const vtx_1_11 = fetchIndex_2_EE(topo0, helper0, helper1, polyIndex, 0, 1);
      const vtx_1_12 = fetchIndex_2_VV(topo0, helper0, helper1, polyIndex, 1);

      const vtx_2_00 = fetchIndex_2_EF(topo0, helper0, helper1, polyIndex, 1);
      const vtx_2_01 = fetchIndex_2_FF(topo0, helper0, helper1, polyIndex, 2);
      const vtx_2_02 = fetchIndex_2_EE(topo0, helper0, helper1, polyIndex, 2, 0);

      const vtx_2_10 = fetchIndex_2_EV(topo0, helper0, helper1, polyIndex, 1);
      const vtx_2_11 = fetchIndex_2_EE(topo0, helper0, helper1, polyIndex, 1, 1);
      const vtx_2_12 = fetchIndex_2_VV(topo0, helper0, helper1, polyIndex, 2);

      const vtxStart = patches.m_irregularVertexIndices.length;
      irregularInfo.m_irregularVertexNum   = numPts;
      irregularInfo.m_irregularVertexStart = vtxStart;

      const cps = [
        controlPointMakeDirect(vtxCenter),
        controlPointMakeDirect(vtx_0_00), controlPointMakeDirect(vtx_0_01), controlPointMakeDirect(vtx_0_02),
        controlPointMakeDirect(vtx_0_10), controlPointMakeDirect(vtx_0_11), controlPointMakeDirect(vtx_0_12),
        controlPointMakeDirect(vtx_1_00), controlPointMakeDirect(vtx_1_01), controlPointMakeDirect(vtx_1_02),
        controlPointMakeDirect(vtx_1_10), controlPointMakeDirect(vtx_1_11), controlPointMakeDirect(vtx_1_12),
        controlPointMakeDirect(vtx_2_00), controlPointMakeDirect(vtx_2_01), controlPointMakeDirect(vtx_2_02),
        controlPointMakeDirect(vtx_2_10), controlPointMakeDirect(vtx_2_11), controlPointMakeDirect(vtx_2_12),
      ];
      if (cps.length !== numPts) throw new Error('collectPointsForIrregular: tri cps length mismatch');
      for (let i = 0; i < numPts; i++) patches.m_irregularVertexIndices.push(cps[i]);
    }
  }
}

// =====================================================================
// Phase 1b.12f: BuildRuntimePoints support
// =====================================================================

// Port of CatmullClarkPatches::ControlPoint::InterpolateControlPointFlatWeight
// (DcciSubdiv.cpp:3716). Returns the FlatWeightVertex that interpolates
// srcPts according to the ControlPoint type:
//   Invalid       -> zero
//   Direct        -> srcPts[v0]
//   Border        -> 2 * v0 - v1
//   ConcaveCorner -> -v0 + v1 + v2
//   ConvexCorner  -> 3 * v0 - v1 - v2
function interpolateControlPointFlatWeight(cp, srcPts) {
  const type = controlPointGetType(cp);
  let ret;
  if (type === kCpType_Invalid) {
    ret = makeFlatWeightVertex();
  } else if (type === kCpType_Direct) {
    if (!(cp.m_v0 >= 0)) throw new Error('interpolateControlPointFlatWeight: Direct v0 < 0');
    // copy to avoid sharing m_weights/m_influences arrays
    const src = srcPts[cp.m_v0];
    ret = { m_weights: src.m_weights.slice(), m_influences: src.m_influences.slice() };
  } else if (type === kCpType_Border) {
    if (!(cp.m_v0 >= 0)) throw new Error('interpolateControlPointFlatWeight: Border v0 < 0');
    if (!(cp.m_v1 >= 0)) throw new Error('interpolateControlPointFlatWeight: Border v1 < 0');
    ret = flatWeightVertexAddDirect(
      flatWeightVertexMulDirect(srcPts[cp.m_v0],  2.0),
      flatWeightVertexMulDirect(srcPts[cp.m_v1], -1.0));
  } else if (type === kCpType_ConcaveCorner) {
    if (!(cp.m_v0 >= 0)) throw new Error('interpolateControlPointFlatWeight: ConcaveCorner v0 < 0');
    if (!(cp.m_v1 >= 0)) throw new Error('interpolateControlPointFlatWeight: ConcaveCorner v1 < 0');
    if (!(cp.m_v2 >= 0)) throw new Error('interpolateControlPointFlatWeight: ConcaveCorner v2 < 0');
    ret = flatWeightVertexAddDirect(
      flatWeightVertexMulDirect(srcPts[cp.m_v0], -1.0),
      flatWeightVertexAddDirect(
        flatWeightVertexMulDirect(srcPts[cp.m_v1], 1.0),
        flatWeightVertexMulDirect(srcPts[cp.m_v2], 1.0)));
  } else if (type === kCpType_ConvexCorner) {
    if (!(cp.m_v0 >= 0)) throw new Error('interpolateControlPointFlatWeight: ConvexCorner v0 < 0');
    if (!(cp.m_v1 >= 0)) throw new Error('interpolateControlPointFlatWeight: ConvexCorner v1 < 0');
    if (!(cp.m_v2 >= 0)) throw new Error('interpolateControlPointFlatWeight: ConvexCorner v2 < 0');
    ret = flatWeightVertexAddDirect(
      flatWeightVertexAddDirect(
        flatWeightVertexMulDirect(srcPts[cp.m_v0],  3.0),
        flatWeightVertexMulDirect(srcPts[cp.m_v1], -1.0)),
      flatWeightVertexMulDirect(srcPts[cp.m_v2], -1.0));
  } else {
    throw new Error('interpolateControlPointFlatWeight: bad type ' + type);
  }
  return ret;
}

// ---- FullCpItem (DcciSubdiv.cpp:4912) -----------------------------
function makeFullCpItem() {
  return {
    m_type:            -1,
    m_v0:              -1,
    m_v1:              -1,
    m_v2:              -1,
    m_v3:              -1,
    m_subdivLevel:     -1,
    m_originalIndex:   -1,
    m_originalSrcType: -1,
  };
}
function maxInt(a, b) { return a > b ? a : b; }
function minInt(a, b) { return a < b ? a : b; }
// Port of FullCpItem::MakeFromVertex (DcciSubdiv.cpp:4929).
function fullCpItemMakeFromVertex(vtx, subdivLevel, originalIndex, originalSrcType) {
  const ret = makeFullCpItem();
  ret.m_type            = kCpType_Direct;
  ret.m_v0              = maxInt(0, vtx);
  ret.m_subdivLevel     = subdivLevel;
  ret.m_originalIndex   = originalIndex;
  ret.m_originalSrcType = originalSrcType;
  return ret;
}
// Port of FullCpItem::MakeFromControlPoint (DcciSubdiv.cpp:4944).
function fullCpItemMakeFromControlPoint(cp, subdivLevel, originalIndex, originalSrcType) {
  const ret = makeFullCpItem();
  ret.m_type = controlPointGetType(cp);
  if (ret.m_type === kCpType_Invalid) {
    throw new Error('fullCpItemMakeFromControlPoint: Invalid type');
  }
  ret.m_v0 = cp.m_v0;
  if (cp.m_v1 >= 0 && cp.m_v2 < 0) {
    ret.m_v1 = cp.m_v1;
    ret.m_v2 = cp.m_v2;
  } else {
    ret.m_v1 = minInt(cp.m_v1, cp.m_v2);
    ret.m_v2 = maxInt(cp.m_v1, cp.m_v2);
  }
  ret.m_v3              = cp.m_v3;
  ret.m_subdivLevel     = subdivLevel;
  ret.m_originalIndex   = originalIndex;
  ret.m_originalSrcType = originalSrcType;
  return ret;
}
// Port of FullCpItem::IsSameCp (DcciSubdiv.cpp:4972).
function fullCpItemIsSameCp(lhs, rhs) {
  return lhs.m_type        === rhs.m_type &&
         lhs.m_v0          === rhs.m_v0   &&
         lhs.m_v1          === rhs.m_v1   &&
         lhs.m_v2          === rhs.m_v2   &&
         lhs.m_v3          === rhs.m_v3   &&
         lhs.m_subdivLevel === rhs.m_subdivLevel;
}
// Port of FullCpItem::operator< (DcciSubdiv.cpp:4987). Returns
// negative / zero / positive for ascending sort.
function fullCpItemCompare(lhs, rhs) {
  if (lhs.m_subdivLevel !== rhs.m_subdivLevel) return lhs.m_subdivLevel - rhs.m_subdivLevel;
  if (lhs.m_type        !== rhs.m_type)        return lhs.m_type        - rhs.m_type;
  if (lhs.m_v0          !== rhs.m_v0)          return lhs.m_v0          - rhs.m_v0;
  if (lhs.m_v1          !== rhs.m_v1)          return lhs.m_v1          - rhs.m_v1;
  if (lhs.m_v2          !== rhs.m_v2)          return lhs.m_v2          - rhs.m_v2;
  if (lhs.m_v3          !== rhs.m_v3)          return lhs.m_v3          - rhs.m_v3;
  if (lhs.m_originalIndex   !== rhs.m_originalIndex)   return lhs.m_originalIndex   - rhs.m_originalIndex;
  if (lhs.m_originalSrcType !== rhs.m_originalSrcType) return lhs.m_originalSrcType - rhs.m_originalSrcType;
  return 0;
}

// =====================================================================
// buildRuntimePoints: line-by-line port of
// CatmullClarkBuilder::BuildRuntimePoints (DcciSubdiv.cpp:5020).
// Builds patches.m_weightInflData (compact base-vertex stencils for all
// unique regular + irregular control points, deduped via FullCpItem
// sort) and patches.m_controlPointRedirect (per-CP remap into the
// deduped table).
//
// `mesh` is the post-prep mesh providing numBaseVerts via the
// base-stencil array sizes. The three posWeights are the identity +
// level-1 + level-2 stencils computed alongside applyCatmullClarkToWeights
// + convolveWeights.
// =====================================================================
function buildRuntimePoints(patches, helper0, posWeights0, posWeights1, posWeights2) {
  const numBaseVerts = helper0.m_numVerts;
  const numRegular   = patches.m_regularVertexIndices.length;
  const numIrregular = patches.m_irregularVertexIndices.length;

  const vecRegularSubdivLevel   = new Array(numRegular).fill(-1);
  const vecIrregularSubdivLevel = new Array(numIrregular).fill(-1);

  const allCpItems = new Array(numBaseVerts + numRegular + numIrregular);
  for (let i = 0; i < allCpItems.length; i++) allCpItems[i] = makeFullCpItem();

  // ---- per-poly subdivLevel ----
  for (let regularIter = 0; regularIter < patches.m_polyInfo.length; regularIter++) {
    const polyInfo = patches.m_polyInfo[regularIter];

    let isTri = false;
    let subdivLevel = -1;
    if      (polyInfo.m_type === kSubdivPolyType_QuadRegular)    subdivLevel = 0;
    else if (polyInfo.m_type === kSubdivPolyType_QuadIrregular1) subdivLevel = 1;
    else if (polyInfo.m_type === kSubdivPolyType_QuadIrregular2) subdivLevel = 2;
    else if (polyInfo.m_type === kSubdivPolyType_Triangle)       { subdivLevel = 2; isTri = true; }

    if (isTri) {
      for (let corner = 0; corner < 3; corner++) {
        const ptsNum   = polyInfo.m_triRegionGrids[corner].m_pointsNum;
        const ptsStart = polyInfo.m_triRegionGrids[corner].m_pointsStart;
        for (let i = 0; i < ptsNum; i++) vecRegularSubdivLevel[ptsStart + i] = subdivLevel;
      }
    } else {
      const ptsNum   = polyInfo.m_quadRegionGrid.m_pointsNum;
      const ptsStart = polyInfo.m_quadRegionGrid.m_pointsStart;
      for (let i = 0; i < ptsNum; i++) vecRegularSubdivLevel[ptsStart + i] = subdivLevel;
    }
  }

  // ---- per-irregular-wedge subdivLevel ----
  for (let irregularIter = 0; irregularIter < patches.m_irregularWedgeInfo.length; irregularIter++) {
    const irregularInfo = patches.m_irregularWedgeInfo[irregularIter];
    const ptsNum     = irregularInfo.m_irregularVertexNum;
    const ptsStart   = irregularInfo.m_irregularVertexStart;
    const subdivLevel = irregularInfo.m_subdivLevel;
    if (!(1 <= subdivLevel && subdivLevel <= 2)) {
      throw new Error('buildRuntimePoints: irregular subdivLevel ' + subdivLevel);
    }
    for (let i = 0; i < ptsNum; i++) vecIrregularSubdivLevel[ptsStart + i] = subdivLevel;
  }

  // ---- populate allCpItems ----
  for (let i = 0; i < numBaseVerts; i++) {
    allCpItems[i] = fullCpItemMakeFromVertex(i, 0, i, 0);
  }
  for (let i = 0; i < numRegular; i++) {
    const subdivLevel = vecRegularSubdivLevel[i];
    if (!(0 <= subdivLevel && subdivLevel <= 2)) {
      throw new Error('buildRuntimePoints: regular subdivLevel ' + subdivLevel + ' (i=' + i + ')');
    }
    let srcCp = patches.m_regularVertexIndices[i];
    const dstIndex = numBaseVerts + i;
    if (srcCp.m_v0 < 0 && srcCp.m_v1 < 0 && srcCp.m_v2 < 0 && srcCp.m_v3 < 0) {
      srcCp = controlPointMakeDirect(0);
    }
    allCpItems[dstIndex] = fullCpItemMakeFromControlPoint(srcCp, subdivLevel, i, 1);
  }
  for (let i = 0; i < numIrregular; i++) {
    const subdivLevel = vecIrregularSubdivLevel[i];
    if (!(1 <= subdivLevel && subdivLevel <= 2)) {
      throw new Error('buildRuntimePoints: irregular subdivLevel ' + subdivLevel);
    }
    const srcCp = patches.m_irregularVertexIndices[i];
    if (srcCp.m_v0 < 0 && srcCp.m_v1 < 0 && srcCp.m_v2 < 0 && srcCp.m_v3 < 0) {
      throw new Error('buildRuntimePoints: irregular CP all-invalid');
    }
    const dstIndex = numBaseVerts + numRegular + i;
    allCpItems[dstIndex] = fullCpItemMakeFromControlPoint(srcCp, subdivLevel, i, 2);
  }

  // ---- sort + dedup ----
  allCpItems.sort(fullCpItemCompare);

  const redirect = makeControlPointRedirect();
  redirect.m_regularCps   = new Array(numRegular).fill(-1);
  redirect.m_irregularCps = new Array(numIrregular).fill(-1);

  const vecCpDstIndex      = new Array(allCpItems.length).fill(-1);
  const vecUniqueCpsInOrder = [];

  let dstIndex = 0;
  let lastIndex = -1;
  while (dstIndex < allCpItems.length) {
    let isSame = false;
    if (lastIndex >= 0) {
      isSame = fullCpItemIsSameCp(allCpItems[lastIndex], allCpItems[dstIndex]);
    }
    if (isSame) {
      vecCpDstIndex[dstIndex] = vecUniqueCpsInOrder.length - 1;
    } else {
      lastIndex = dstIndex;
      vecUniqueCpsInOrder.push(dstIndex);
      vecCpDstIndex[dstIndex] = vecUniqueCpsInOrder.length - 1;
    }
    dstIndex++;
  }

  // ---- compute FlatWeightVertex per unique CP ----
  // amolite leaves the base-vertex slots default: writes only happen for
  // non-base unique CPs at index (i - numBaseVerts). This shifts the
  // non-base CP layout down so flatWeights[0..numNonBase) is the data;
  // flatWeights[numNonBase..numUnique) stays at default (empty FWV +
  // num=0). The 2nd loop reads the same way, leaving the inflData
  // tail untouched. The runtime indexes m_cpItemStart[ptIndex - numBaseCps]
  // so the layout matches.
  let endSubdiv0 = 0, endSubdiv1 = 0, endSubdiv2 = 0;
  const flatWeights    = new Array(vecUniqueCpsInOrder.length);
  const flatWeightsNum = new Array(vecUniqueCpsInOrder.length).fill(0);
  for (let i = 0; i < flatWeights.length; i++) flatWeights[i] = makeFlatWeightVertex();

  let totalNum = 0;
  for (let i = 0; i < vecUniqueCpsInOrder.length; i++) {
    const sortedCpIndex = vecUniqueCpsInOrder[i];
    const cpItem = allCpItems[sortedCpIndex];

    if (cpItem.m_subdivLevel === 0) endSubdiv0 = i + 1;
    if (cpItem.m_subdivLevel === 1) endSubdiv1 = i + 1;
    if (cpItem.m_subdivLevel === 2) endSubdiv2 = i + 1;

    if (i < numBaseVerts) {
      if (cpItem.m_type !== kCpType_Direct)  throw new Error('buildRuntimePoints: base vtx not Direct (i=' + i + ')');
      if (cpItem.m_subdivLevel !== 0)        throw new Error('buildRuntimePoints: base vtx subdivLevel != 0');
      if (cpItem.m_v0 !== i)                 throw new Error('buildRuntimePoints: base vtx v0 != i (' + cpItem.m_v0 + ' vs ' + i + ')');
      // Match amolite: do NOT write to flatWeights/flatWeightsNum.
    } else {
      let pSrcWeights = null;
      if      (cpItem.m_subdivLevel === 0) pSrcWeights = posWeights0;
      else if (cpItem.m_subdivLevel === 1) pSrcWeights = posWeights1;
      else if (cpItem.m_subdivLevel === 2) pSrcWeights = posWeights2;
      else throw new Error('buildRuntimePoints: bad subdivLevel ' + cpItem.m_subdivLevel);

      if (cpItem.m_originalSrcType === 0) throw new Error('buildRuntimePoints: non-base i had srcType 0');

      const cp = makeControlPoint();
      cp.m_v0 = cpItem.m_v0;
      cp.m_v1 = cpItem.m_v1;
      cp.m_v2 = cpItem.m_v2;
      cp.m_v3 = cpItem.m_v3;

      const currVtx = interpolateControlPointFlatWeight(cp, pSrcWeights);
      const numCurrWeights = flatWeightVertexGetNum(currVtx);

      if (numCurrWeights === 0) throw new Error('buildRuntimePoints: unique CP ' + i + ' has 0 weights');

      // amolite writes at (i - numBaseVerts) so non-base data lives at
      // the front of the array. The trailing numBaseVerts slots stay
      // default (which is what the runtime expects).
      flatWeights[i - numBaseVerts]    = currVtx;
      flatWeightsNum[i - numBaseVerts] = numCurrWeights;
      totalNum                        += numCurrWeights;
    }
  }

  // ---- runtimeInflData ----
  const prefixNumWeights = calcPrefixSumVec(flatWeightsNum);

  const runtimeInflData = makeRuntimeWeightInflData();

  runtimeInflData.m_numSubdiv0 = endSubdiv0 - numBaseVerts;
  runtimeInflData.m_numSubdiv1 = endSubdiv1 - endSubdiv0;
  runtimeInflData.m_numSubdiv2 = endSubdiv2 - endSubdiv1;

  runtimeInflData.m_numBaseCps = numBaseVerts;
  runtimeInflData.m_numCpItems = flatWeightsNum.length;

  runtimeInflData.m_cpItemStart = prefixNumWeights.slice();
  runtimeInflData.m_cpItemCount = flatWeightsNum.slice();

  runtimeInflData.m_inflData   = new Array(totalNum).fill(-1);
  runtimeInflData.m_weightData = new Array(totalNum).fill(-1.0);

  for (let i = 0; i < flatWeightsNum.length; i++) {
    const start = prefixNumWeights[i];
    const num   = flatWeightsNum[i];
    const currVtx = flatWeights[i];
    for (let localIter = 0; localIter < num; localIter++) {
      runtimeInflData.m_inflData[start + localIter]   = currVtx.m_influences[localIter];
      runtimeInflData.m_weightData[start + localIter] = currVtx.m_weights[localIter];
    }
  }

  patches.m_weightInflData = runtimeInflData;

  // ---- redirect: regularCps / irregularCps -> uniqueIndex ----
  patches.m_controlPointRedirect = makeControlPointRedirect();
  patches.m_controlPointRedirect.m_regularCps   = new Array(numRegular).fill(-1);
  patches.m_controlPointRedirect.m_irregularCps = new Array(numIrregular).fill(-1);

  if (vecCpDstIndex.length !== allCpItems.length) {
    throw new Error('buildRuntimePoints: vecCpDstIndex.length != allCpItems.length');
  }

  for (let sortedIter = 0; sortedIter < vecCpDstIndex.length; sortedIter++) {
    const cpItem = allCpItems[sortedIter];
    const uniqueIndex = vecCpDstIndex[sortedIter];
    if (cpItem.m_originalSrcType === 0) {
      if (!(uniqueIndex < numBaseVerts)) {
        throw new Error('buildRuntimePoints: base-vtx CP uniqueIndex ' + uniqueIndex + ' >= numBaseVerts');
      }
    } else if (cpItem.m_originalSrcType === 1) {
      const srcIndex = cpItem.m_originalIndex;
      patches.m_controlPointRedirect.m_regularCps[srcIndex] = uniqueIndex;
    } else if (cpItem.m_originalSrcType === 2) {
      const srcIndex = cpItem.m_originalIndex;
      patches.m_controlPointRedirect.m_irregularCps[srcIndex] = uniqueIndex;
    } else {
      throw new Error('buildRuntimePoints: bad originalSrcType ' + cpItem.m_originalSrcType);
    }
  }

  runtimeInflData.m_numRegularPoints   = patches.m_controlPointRedirect.m_regularCps.length;
  runtimeInflData.m_numIrregularPoints = patches.m_controlPointRedirect.m_irregularCps.length;

  runtimeInflData.m_regularAndIrregularPoints =
    new Array(runtimeInflData.m_numRegularPoints + runtimeInflData.m_numIrregularPoints).fill(-1);

  for (let i = 0; i < patches.m_controlPointRedirect.m_regularCps.length; i++) {
    if (!(patches.m_controlPointRedirect.m_regularCps[i] >= 0)) {
      throw new Error('buildRuntimePoints: regularCps[' + i + '] < 0');
    }
    runtimeInflData.m_regularAndIrregularPoints[i] = patches.m_controlPointRedirect.m_regularCps[i];
  }
  for (let i = 0; i < patches.m_controlPointRedirect.m_irregularCps.length; i++) {
    if (!(patches.m_controlPointRedirect.m_irregularCps[i] >= 0)) {
      throw new Error('buildRuntimePoints: irregularCps[' + i + '] < 0');
    }
    runtimeInflData.m_regularAndIrregularPoints[runtimeInflData.m_numRegularPoints + i] =
      patches.m_controlPointRedirect.m_irregularCps[i];
  }

  // amolite has a redundant `m_patches.m_weightInflData = runtimeInflData;`
  // at the end (after mutating runtimeInflData post-assignment); the JS
  // port already holds a reference so the mutations are visible.
  patches.m_weightInflData = runtimeInflData;
}

// =====================================================================
// Phase 1b.12g: BuildRuntimeIrregularInfo
// =====================================================================

const IRREGULAR_GROUP_SIZE = 64;

// Port of CatmullClarkPatches::RuntimeIregularInfo (DcciSubdiv.h:750).
function makeRuntimeIregularInfo() {
  return {
    m_valence:                       -1,
    m_isBorder:                      -1,
    m_pointStart:                    -1,
    m_pointCount:                    -1,
    m_subdivLevel:                   -1,
    m_centerVertexSharpness:         -1.0,
    m_edgeSharpnessStart:            -1,
    m_centerSemisharpEdge0:          -1,
    m_centerSemisharpEdge1:          -1,
    m_secondHighestEdgeSharpness:    -1.0,
    m_thirdHighestEdgeSharpness:     -1.0,
  };
}
// Port of RuntimeIregularInfo::GetNumThreadsForGroup (DcciSubdiv.h:798).
function runtimeIregularInfoGetNumThreadsForGroup(rii) {
  let ret = rii.m_valence;
  if (rii.m_isBorder) ret++;
  return ret;
}

// =====================================================================
// buildRuntimeIrregularInfo: line-by-line port of
// CatmullClarkBuilder::BuildRuntimeIrregularInfo (DcciSubdiv.cpp:5373).
// Builds patches.m_weightInflData.m_runtimeIrregularInfo[] (per-wedge
// runtime info: valence, isBorder, sharpness, edge sharpness offset,
// top-three semisharp edges), m_irregularEdgeSharpness[], and the
// IRREGULAR_GROUP_SIZE-bin packing into m_irregularGroupStart/Num.
// =====================================================================
function buildRuntimeIrregularInfo(patches, helper0) {
  const numIrregularInfo = patches.m_irregularWedgeInfo.length;

  // Resize the runtimeIrregularInfo array.
  patches.m_weightInflData.m_runtimeIrregularInfo.length = 0;
  for (let i = 0; i < numIrregularInfo; i++) {
    patches.m_weightInflData.m_runtimeIrregularInfo.push(makeRuntimeIregularInfo());
  }

  for (let irregularIter = 0; irregularIter < numIrregularInfo; irregularIter++) {
    const irregularInfo = patches.m_irregularWedgeInfo[irregularIter];

    const runtimeInfo = makeRuntimeIregularInfo();
    runtimeInfo.m_isBorder    = irregularInfo.m_isCircular ? 0 : 1;
    runtimeInfo.m_pointStart  = irregularInfo.m_irregularVertexStart;
    runtimeInfo.m_pointCount  = irregularInfo.m_irregularVertexNum;
    runtimeInfo.m_subdivLevel = irregularInfo.m_subdivLevel;
    runtimeInfo.m_valence     = irregularInfo.m_valence;

    let vertexSharpness = 0.0;

    const edgeSharpness = [];
    let maxSharpness = -1.0;

    const topThreeSharpnessValues = [0.0, 0.0, 0.0];
    const topThreeSharpnessEdges  = [0, 0, 0];

    // Triangle-center patches always have zero sharpness; vertex
    // patches walk the wedge corners.
    const numThreads = runtimeInfo.m_isBorder ? (runtimeInfo.m_valence + 1) : runtimeInfo.m_valence;
    for (let i = 0; i < numThreads; i++) edgeSharpness.push(-1.0);

    if (irregularInfo.m_centerVertex >= 0) {
      const vertInfo = patches.m_vertexInfo[irregularInfo.m_centerVertex];
      vertexSharpness = vertInfo.m_vertexSharpness;
      // (vertInfo.m_isNonManifold check in amolite has no effect -- the
      //  isNonManifold flag is written but never used by the rest of
      //  this function. Skip.)

      const startWedgeForVertex = helper0.m_wedges.m_wedgeEdgeStart[irregularInfo.m_centerVertex];
      const numWedgeForEdge     = helper0.m_wedges.m_wedgeEdgeSize[irregularInfo.m_centerVertex];
      const localWedge          = irregularInfo.m_localWedgeIndex;
      if (!(localWedge >= 0))               throw new Error('buildRuntimeIrregularInfo: localWedge < 0');
      if (!(localWedge < numWedgeForEdge))  throw new Error('buildRuntimeIrregularInfo: localWedge >= numWedgeForEdge');

      const wedgeInfo = helper0.m_wedges.m_wedgeEdgeInfo[startWedgeForVertex + localWedge];

      const cornerNum   = wedgeInfo.m_wedgeEdgeNum;
      const cornerStart = wedgeInfo.m_wedgeEdgeStart;
      if (numThreads !== cornerNum) throw new Error('buildRuntimeIrregularInfo: numThreads != cornerNum (' + numThreads + ' vs ' + cornerNum + ')');

      for (let threadIter = 0; threadIter < numThreads; threadIter++) {
        const cornerInfo = helper0.m_wedges.m_wedgeCornerData[cornerStart + threadIter];
        const currSharpness = cornerInfo.m_creaseSharpness;

        if (currSharpness > maxSharpness) maxSharpness = currSharpness;
        edgeSharpness[threadIter] = currSharpness;

        let sharpnessValue = currSharpness;
        let sharpnessEdge  = threadIter;

        if (runtimeInfo.m_isBorder) {
          // border: first + last threads are infinitely sharp.
          if (threadIter === 0 || threadIter === runtimeInfo.m_valence) {
            sharpnessValue = SUBDIV_MAX_SHARPNESS;
          }
        }
        // non-manifold edge -> infinite sharpness too.
        const numPolysForEdge = edgeLookupGetNumFacesForEdge(helper0.m_edgeLookup, cornerInfo.m_edgeIndex);
        if (numPolysForEdge >= 3) {
          sharpnessValue = SUBDIV_MAX_SHARPNESS;
        }

        // 3-slot insertion sort (amolite's exact pattern).
        for (let i = 0; i < 3; i++) {
          if (sharpnessValue > topThreeSharpnessValues[i]) {
            const tv = topThreeSharpnessValues[i]; topThreeSharpnessValues[i] = sharpnessValue; sharpnessValue = tv;
            const te = topThreeSharpnessEdges[i];  topThreeSharpnessEdges[i]  = sharpnessEdge;  sharpnessEdge  = te;
          }
        }
      }
    } else {
      maxSharpness = 0.0;
    }

    runtimeInfo.m_centerVertexSharpness      = vertexSharpness;
    runtimeInfo.m_secondHighestEdgeSharpness = topThreeSharpnessValues[1];
    runtimeInfo.m_thirdHighestEdgeSharpness  = topThreeSharpnessValues[2];

    runtimeInfo.m_centerSemisharpEdge0       = topThreeSharpnessEdges[0];
    runtimeInfo.m_centerSemisharpEdge1       = topThreeSharpnessEdges[1];

    if (!(maxSharpness >= 0.0 && maxSharpness <= SUBDIV_MAX_SHARPNESS)) {
      throw new Error('buildRuntimeIrregularInfo: maxSharpness out of range (' + maxSharpness + ')');
    }

    if (maxSharpness === 0.0) {
      runtimeInfo.m_edgeSharpnessStart = -1;
    } else {
      runtimeInfo.m_edgeSharpnessStart = patches.m_weightInflData.m_irregularEdgeSharpness.length;

      const subdivLevel = irregularInfo.m_subdivLevel;
      for (let i = 0; i < edgeSharpness.length; i++) {
        let sharpness = edgeSharpness[i];
        if (sharpness < SUBDIV_MAX_SHARPNESS) {
          sharpness = Math.max(0.0, sharpness - subdivLevel);
        }
        patches.m_weightInflData.m_irregularEdgeSharpness.push(sharpness);
      }
    }

    patches.m_weightInflData.m_runtimeIrregularInfo[irregularIter] = runtimeInfo;
  }

  // ----- IRREGULAR_GROUP_SIZE bin packing -----
  {
    let lastIrregularStart = 0;
    let irregularIter      = 0;
    let numThreadsInGroup  = 0;

    while (irregularIter < numIrregularInfo) {
      const runtimeIrregularInfo = patches.m_weightInflData.m_runtimeIrregularInfo[irregularIter];
      const numThreadsRequired = runtimeIregularInfoGetNumThreadsForGroup(runtimeIrregularInfo);

      if (numThreadsInGroup + numThreadsRequired <= IRREGULAR_GROUP_SIZE) {
        numThreadsInGroup += numThreadsRequired;
      } else {
        const numIrregularPatches = irregularIter - lastIrregularStart;
        patches.m_weightInflData.m_irregularGroupNum.push(numIrregularPatches);
        lastIrregularStart = irregularIter;
        numThreadsInGroup  = numThreadsRequired;
      }

      irregularIter++;
    }

    if (numThreadsInGroup > 0) {
      const numIrregularPatches = irregularIter - lastIrregularStart;
      patches.m_weightInflData.m_irregularGroupNum.push(numIrregularPatches);
    }

    patches.m_weightInflData.m_irregularGroupStart =
      calcPrefixSumVec(patches.m_weightInflData.m_irregularGroupNum);
  }
}

// =====================================================================
// Phase 1b.13a: BSplineUtil + Mat44 helpers
// =====================================================================

// ---- Mat44 (DcciSubdiv consumer needs InitIdentity/At/GetTranspose +
// MulMatVec + scaled-add ((1-t)*A + t*B)). Row-major: At(r,c) = m_data[r*4+c]).
function makeMat44() {
  const m = new Float32Array(16);
  return { m_data: m };
}
function mat44InitIdentity(m) {
  for (let i = 0; i < 16; i++) {
    m.m_data[i] = ((i >> 2) === (i & 3)) ? 1.0 : 0.0;
  }
}
function mat44At(m, r, c)         { return m.m_data[r * 4 + c]; }
function mat44Set(m, r, c, val)   { m.m_data[r * 4 + c] = val; }
// Port of Mat44::GetTranspose (Mat44.h:32).
function mat44GetTranspose(m) {
  const ret = makeMat44();
  for (let r = 0; r < 4; r++) {
    for (let c = 0; c < 4; c++) {
      ret.m_data[r * 4 + c] = m.m_data[c * 4 + r];
    }
  }
  return ret;
}
// Port of Mat44::MulMatVec (Mat44.cpp:67). Returns Vec4 ({x,y,z,w}).
// Math.fround() at each step matches amolite's f32 accumulation. The
// C++ uses LEFT-TO-RIGHT sequential accumulation (sum += At*x, sum +=
// At*y, ...) -- not a paired tree -- so we mirror that order exactly
// to match the bottom 1-2 bits of f32 representation.
function mat44MulMatVec(lhs, rhs) {
  const fr = Math.fround;
  const rx = fr(rhs.x), ry = fr(rhs.y), rz = fr(rhs.z), rw = fr(rhs.w);
  const out = { x: 0, y: 0, z: 0, w: 0 };
  for (let r = 0; r < 4; r++) {
    let sum = 0.0;
    sum = fr(sum + fr(fr(lhs.m_data[r*4 + 0]) * rx));
    sum = fr(sum + fr(fr(lhs.m_data[r*4 + 1]) * ry));
    sum = fr(sum + fr(fr(lhs.m_data[r*4 + 2]) * rz));
    sum = fr(sum + fr(fr(lhs.m_data[r*4 + 3]) * rw));
    if (r === 0) out.x = sum;
    if (r === 1) out.y = sum;
    if (r === 2) out.z = sum;
    if (r === 3) out.w = sum;
  }
  return out;
}
// (1-t)*A + t*B (used in CalcSharpnessMatricesAndEndpoints). Float32
// math to match amolite's f32 Mat44 operator+ / operator*.
function mat44ScaledSum(a, sa, b, sb) {
  const fr = Math.fround;
  const sa32 = fr(sa), sb32 = fr(sb);
  const ret = makeMat44();
  for (let i = 0; i < 16; i++) {
    ret.m_data[i] = fr(fr(fr(a.m_data[i]) * sa32) + fr(fr(b.m_data[i]) * sb32));
  }
  return ret;
}

// ---- scalar helpers ----
// Saturate / SafeInv from RuntimeLib/Core/CoreHelpers.
function saturateF(x) { return x < 0.0 ? 0.0 : (x > 1.0 ? 1.0 : x); }
function safeInv(x)   { return (x !== 0.0) ? (1.0 / x) : 0.0; }
function calcFrac(t)  { return t - Math.floor(t); }

// =====================================================================
// BSplineUtil basis evaluators.
// =====================================================================

// Port of BSplineUtil::GetMInfinity (DcciSubdiv.cpp:8798).
function bsplineUtilGetMInfinity() {
  const ret = makeMat44();
  mat44InitIdentity(ret);
  mat44Set(ret, 3, 1, -1.0);
  mat44Set(ret, 3, 2,  2.0);
  mat44Set(ret, 3, 3,  0.0);
  return ret;
}

// Port of BSplineUtil::EvalBSplineBasis (DcciSubdiv.cpp:8808). Float32
// math; amolite operates on Vec4 components which are f32. C++ source
// is `(-ttt + 3*tt - 3*t + 1) / 6.0f`, left-to-right in f32.
function bsplineUtilEvalBSplineBasis(srcT) {
  const fr = Math.fround;
  const t   = fr(saturateF(srcT));
  const tt  = fr(t * t);
  const ttt = fr(fr(t * t) * t);
  const six = fr(6.0);
  // x: (-ttt + 3*tt - 3*t + 1) / 6
  let x = fr(-ttt);
  x = fr(x + fr(fr(3) * tt));
  x = fr(x + fr(fr(-3) * t));
  x = fr(x + fr(1));
  x = fr(x / six);
  // y: (3*ttt - 6*tt + 4) / 6
  let y = fr(fr(3) * ttt);
  y = fr(y + fr(fr(-6) * tt));
  y = fr(y + fr(4));
  y = fr(y / six);
  // z: (-3*ttt + 3*tt + 3*t + 1) / 6
  let z = fr(fr(-3) * ttt);
  z = fr(z + fr(fr(3) * tt));
  z = fr(z + fr(fr(3) * t));
  z = fr(z + fr(1));
  z = fr(z / six);
  // w: ttt / 6
  const w = fr(ttt / six);
  return { x, y, z, w };
}

// Port of BSplineUtil::EvalBSplineBasisDeriv (DcciSubdiv.cpp:8830).
function bsplineUtilEvalBSplineBasisDeriv(srcT) {
  const fr = Math.fround;
  const t  = fr(saturateF(srcT));
  const tt = fr(t * t);
  const six = fr(6.0);
  // x: (-3*tt + 6*t - 3) / 6
  let x = fr(fr(-3) * tt);
  x = fr(x + fr(fr(6) * t));
  x = fr(x + fr(-3));
  x = fr(x / six);
  // y: (9*tt - 12*t) / 6
  let y = fr(fr(9) * tt);
  y = fr(y + fr(fr(-12) * t));
  y = fr(y / six);
  // z: (-9*tt + 6*t + 3) / 6
  let z = fr(fr(-9) * tt);
  z = fr(z + fr(fr(6) * t));
  z = fr(z + fr(3));
  z = fr(z / six);
  // w: (3*tt) / 6
  const w = fr(fr(fr(3) * tt) / six);
  return { x, y, z, w };
}

// Port of BSplineUtil::CalcMSharpness (DcciSubdiv.cpp:8848). Float32
// math throughout to match amolite's f32 accumulation.
function bsplineUtilCalcMSharpness(rho) {
  const fr = Math.fround;
  const rhoF = fr(rho);
  const oneMinusRho = fr(1.0 - rhoF);
  const invOneMinusRho = fr(safeInv(oneMinusRho));
  const r2 = fr(rhoF * rhoF);
  const r1 = rhoF;
  const scale = fr(fr(1.0 - rhoF) / fr(6.0 * rhoF));

  function mul(a, b)     { return fr(fr(a) * fr(b)); }
  function poly(c2, c1, c0) {
    return fr(fr(mul(c2, r2) + mul(c1, r1)) + fr(c0));
  }

  const ret = makeMat44();
  mat44Set(ret, 0, 0, mul(scale, mul(mul(6.0, r1), invOneMinusRho)));
  mat44Set(ret, 0, 1, mul(scale, poly( 6.0, -5.0,  1.0)));
  mat44Set(ret, 0, 2, mul(scale, poly(-12.0, 10.0, -2.0)));
  mat44Set(ret, 0, 3, mul(scale, poly( 6.0, -5.0,  1.0)));

  mat44Set(ret, 1, 0, 0.0);
  mat44Set(ret, 1, 1, mul(scale, mul(poly(2.0, 3.0, 1.0), invOneMinusRho)));
  mat44Set(ret, 1, 2, mul(scale, fr(mul(4.0, r1) + fr(-2.0))));
  mat44Set(ret, 1, 3, mul(scale, fr(mul(-2.0, r1) + fr(1.0))));

  mat44Set(ret, 2, 0, 0.0);
  mat44Set(ret, 2, 1, mul(scale, fr(r1 + fr(1.0))));
  mat44Set(ret, 2, 2, mul(scale, mul(poly(2.0, 6.0, -2.0), invOneMinusRho)));
  mat44Set(ret, 2, 3, mul(scale, fr(r1 + fr(1.0))));

  mat44Set(ret, 3, 0, 0.0);
  mat44Set(ret, 3, 1, mul(scale, fr(mul(-2.0, r1) + fr(1.0))));
  mat44Set(ret, 3, 2, mul(scale, fr(mul(4.0, r1) + fr(-2.0))));
  mat44Set(ret, 3, 3, mul(scale, mul(poly(2.0, 3.0, 1.0), invOneMinusRho)));

  return ret;
}

// Port of BSplineUtil::CalcSharpnessMatricesAndEndpoints (DcciSubdiv.cpp:8885).
function bsplineUtilCalcSharpnessMatricesAndEndpoints(sharpness) {
  const M_infinity = bsplineUtilGetMInfinity();
  const S_floor = Math.floor(sharpness);
  const S_ceil  = Math.ceil(sharpness);

  const rho_floor = Math.pow(2.0, S_floor);
  const rho_ceil  = Math.pow(2.0, S_ceil);

  let M_floor = bsplineUtilCalcMSharpness(rho_floor);
  const M_ceil = bsplineUtilCalcMSharpness(rho_ceil);

  if (sharpness < 1.0) {
    M_floor = makeMat44();
    mat44InitIdentity(M_floor);
  }

  const S_frac = calcFrac(sharpness);

  const M_infinity_hat = mat44ScaledSum(M_floor, 1.0 - S_frac, M_infinity, S_frac);
  const M_s_hat        = mat44ScaledSum(M_floor, 1.0 - S_frac, M_ceil,     S_frac);

  const seg0 = 1.0 - Math.pow(2.0, -S_floor);
  const seg1 = 1.0 - Math.pow(2.0, -S_ceil);

  return { M_infinity_hat, M_s_hat, seg0, seg1 };
}

// Port of BSplineUtil::CalcBasisAndDerivFromSharpnessMatrices (DcciSubdiv.cpp:8916).
function bsplineUtilCalcBasisAndDerivFromSharpnessMatrices(
    M_infinity_hat, M_s_hat, seg0, seg1, t, reverseSharpnessBasis) {
  const M_infinity = bsplineUtilGetMInfinity();

  let sharpnessT = t;
  if (reverseSharpnessBasis) sharpnessT = 1.0 - t;

  const basis      = bsplineUtilEvalBSplineBasis(sharpnessT);
  const basisDeriv = bsplineUtilEvalBSplineBasisDeriv(sharpnessT);

  let retBasis, retBasisDeriv;
  if (sharpnessT < seg0) {
    retBasis      = mat44MulMatVec(mat44GetTranspose(M_infinity), basis);
    retBasisDeriv = mat44MulMatVec(mat44GetTranspose(M_infinity), basisDeriv);
  } else if (sharpnessT < seg1) {
    retBasis      = mat44MulMatVec(mat44GetTranspose(M_infinity_hat), basis);
    retBasisDeriv = mat44MulMatVec(mat44GetTranspose(M_infinity_hat), basisDeriv);
  } else {
    retBasis      = mat44MulMatVec(mat44GetTranspose(M_s_hat), basis);
    retBasisDeriv = mat44MulMatVec(mat44GetTranspose(M_s_hat), basisDeriv);
  }

  if (reverseSharpnessBasis) {
    const t1 = retBasis;
    const t2 = retBasisDeriv;
    retBasis      = { x: t1.w, y: t1.z, z: t1.y, w: t1.x };
    retBasisDeriv = { x: t2.w, y: t2.z, z: t2.y, w: t2.x };
  }
  return { basis: retBasis, basisDeriv: retBasisDeriv };
}

// Port of BSplineUtil::CalcBasisAndDerivForSharpnessRef (DcciSubdiv.cpp:8965).
function bsplineUtilCalcBasisAndDerivForSharpnessRef(t, sharpness, reverseSharpnessBasis) {
  const sm = bsplineUtilCalcSharpnessMatricesAndEndpoints(sharpness);
  return bsplineUtilCalcBasisAndDerivFromSharpnessMatrices(
    sm.M_infinity_hat, sm.M_s_hat, sm.seg0, sm.seg1, t, reverseSharpnessBasis);
}

// Port of BSplineUtil::EvalBicubicBSplineSharpnessFlatWeights (DcciSubdiv.cpp:9052).
// Writes foundP/foundT0/foundT1 (FlatWeightVertex). Sharpness applies on the
// tx (x-axis) direction; ty uses the standard B-spline basis.
//
// Layout (amolite's comment):
//   12  13  14  15
//    8   9  10  11
//    4   5   6   7
//    0   1   2   3
// sharpness line is on 5->9 (along tx = 0).
function bsplineUtilEvalBicubicBSplineSharpnessFlatWeights(
    ptOffsets, ptData, sharpness, tx, ty) {
  const sx = bsplineUtilCalcBasisAndDerivForSharpnessRef(tx, sharpness, true);
  const basisX  = sx.basis;
  const basisDx = sx.basisDeriv;

  const basisY  = bsplineUtilEvalBSplineBasis(ty);
  const basisDy = bsplineUtilEvalBSplineBasisDeriv(ty);

  const dataX  = [basisX.x,  basisX.y,  basisX.z,  basisX.w];
  const dataY  = [basisY.x,  basisY.y,  basisY.z,  basisY.w];
  const dataDx = [basisDx.x, basisDx.y, basisDx.z, basisDx.w];
  const dataDy = [basisDy.x, basisDy.y, basisDy.z, basisDy.w];

  let sumP  = makeFlatWeightVertex();
  let sumDx = makeFlatWeightVertex();
  let sumDy = makeFlatWeightVertex();

  for (let r = 0; r < 4; r++) {
    for (let c = 0; c < 4; c++) {
      const w_x  = dataX[c];
      const w_dx = dataDx[c];
      const w_y  = dataY[r];
      const w_dy = dataDy[r];

      const offsetVtx = ptOffsets[r * 4 + c];
      const P = ptData[offsetVtx];

      sumP  = flatWeightVertexAddDirect(sumP,  flatWeightVertexMulDirect(P, w_x * w_y));
      sumDx = flatWeightVertexAddDirect(sumDx, flatWeightVertexMulDirect(P, w_dx * w_y));
      sumDy = flatWeightVertexAddDirect(sumDy, flatWeightVertexMulDirect(P, w_x * w_dy));
    }
  }
  return { foundP: sumP, foundT0: sumDx, foundT1: sumDy };
}

// =====================================================================
// Phase 1b.13d: IrregularWedgeLimitFinder + CalculateLimitWeights
// =====================================================================

// Port of SubdivVertexSharpRuleFloat (DcciSubdiv.cpp:7437).
function subdivVertexSharpRuleFloat(e0, e1, v) {
  return 0.125 * (e0 + 6.0 * v + e1);
}

// Port of GetCenterVertexEdgeSharpnessInfoHelper (DcciSubdiv.cpp:7442).
// Note: amolite has TWO versions -- the one at 7442 (used by the
// IrregularWedge limit finder) clamps SUBDIV_MAX_SHARPNESS edges to
// 1.0f. The earlier version in DcciSubdiv.cpp:2113 (used by
// ApplySubdivisionToVertPtsSemiSharp, Phase 1b.10) does not. They have
// the same name but different bodies; we ship a separate JS function
// to avoid collision with the 1b.10 port.
function getCenterVertexEdgeSharpnessInfoHelperIrregular(
    secondHighestEdgeSharpness, thirdHighestEdgeSharpness, centerVertexSharpness, iteration) {
  const secondInfl = saturateF(
    secondHighestEdgeSharpness >= SUBDIV_MAX_SHARPNESS ? 1.0 :
    (secondHighestEdgeSharpness - iteration));
  const thirdInfl = saturateF(
    thirdHighestEdgeSharpness >= SUBDIV_MAX_SHARPNESS ? 1.0 :
    (thirdHighestEdgeSharpness - iteration));
  const vertexInfl = saturateF(
    centerVertexSharpness >= SUBDIV_MAX_SHARPNESS ? 1.0 :
    (centerVertexSharpness - iteration));

  const cornerInfl = thirdInfl > vertexInfl ? thirdInfl : vertexInfl;
  const edgeInfl = secondInfl;

  const dstWeightCorner = cornerInfl;
  const dstWeightEdge   = (1.0 - cornerInfl) * edgeInfl;
  const dstWeightSmooth = (1.0 - cornerInfl) * (1.0 - edgeInfl);
  return { dstWeightSmooth, dstWeightEdge, dstWeightCorner };
}

// Port of IrregularWedgeLimitFinder (DcciSubdiv.h:571). Note: amolite's
// commented-out fields (m_dataOuter, m_irregularWedgeIndex, etc.) are
// not present -- the limit finder only uses what's live.
function makeIrregularWedgeLimitFinder() {
  return {
    m_dataInner:                  [[], []],    // 2x array of f32 (2 * numThreads)
    m_dataCenter:                 [0.0, 0.0],  // f32[2]
    m_irregularSharpness:         [],          // f32[numThreads]
    m_wedgeIsBorder:              false,
    m_wedgeValence:               0,
    m_wedgeCenterSharpness:       0.0,
    m_wedgeSecondHighestSharpness:0.0,
    m_wedgeThirdHighestSharpness: 0.0,
    m_wedgeCenterSemisharpEdge0:  0,
    m_wedgeCenterSemisharpEdge1:  0,
    m_lastDataStartIndex:         0,
    m_numThreads:                 0,
    m_maxNumSlices:               0,
  };
}

// Port of InitLimitIrregularWedgeDecoderPerThreadData (DcciSubdiv.cpp:7928).
function initLimitIrregularWedgeDecoderPerThreadData(wedgeDecoder, runtimeIrregularInfo, irregularEdgeSharpness) {
  const numThreadsForGroup = runtimeIrregularInfo.m_isBorder
    ? (runtimeIrregularInfo.m_valence + 1)
    : runtimeIrregularInfo.m_valence;

  wedgeDecoder.m_numThreads = numThreadsForGroup;

  wedgeDecoder.m_dataInner[0] = new Array(numThreadsForGroup * 2).fill(0.0);
  wedgeDecoder.m_dataInner[1] = new Array(numThreadsForGroup * 2).fill(0.0);

  wedgeDecoder.m_irregularSharpness = new Array(numThreadsForGroup).fill(0.0);

  for (let threadIter = 0; threadIter < numThreadsForGroup; threadIter++) {
    if (!(threadIter < IRREGULAR_GROUP_SIZE)) {
      throw new Error('initLimitIrregularWedgeDecoder: threadId >= IRREGULAR_GROUP_SIZE');
    }
    let edgeSharpness = 0.0;
    if (runtimeIrregularInfo.m_edgeSharpnessStart >= 0) {
      edgeSharpness = irregularEdgeSharpness[runtimeIrregularInfo.m_edgeSharpnessStart + threadIter];
    }
    wedgeDecoder.m_irregularSharpness[threadIter] = edgeSharpness;
  }

  wedgeDecoder.m_wedgeIsBorder              = !!runtimeIrregularInfo.m_isBorder;
  wedgeDecoder.m_wedgeValence               = runtimeIrregularInfo.m_valence;
  wedgeDecoder.m_wedgeCenterSharpness       = runtimeIrregularInfo.m_centerVertexSharpness;
  wedgeDecoder.m_wedgeSecondHighestSharpness= runtimeIrregularInfo.m_secondHighestEdgeSharpness;
  wedgeDecoder.m_wedgeThirdHighestSharpness = runtimeIrregularInfo.m_thirdHighestEdgeSharpness;
  wedgeDecoder.m_wedgeCenterSemisharpEdge0  = runtimeIrregularInfo.m_centerSemisharpEdge0;
  wedgeDecoder.m_wedgeCenterSemisharpEdge1  = runtimeIrregularInfo.m_centerSemisharpEdge1;
}

// Port of LimitIrregularWedgeInitDecoderSingleWeight (DcciSubdiv.cpp:7991).
// Initialize the decoder for weight-extraction mode: input vector is
// the standard basis vector at `weightIndex` (1 at one slot, 0 elsewhere).
function limitIrregularWedgeInitDecoderSingleWeight(wedgeDecoder, runtimeIrregularInfo, irregularEdgeSharpness, weightIndex) {
  // amolite: `wedgeDecoder = IrregularWedgeLimitFinder()` -- reset.
  // We reset the live fields in-place.
  wedgeDecoder.m_dataInner = [[], []];
  wedgeDecoder.m_dataCenter = [0.0, 0.0];
  wedgeDecoder.m_irregularSharpness = [];
  wedgeDecoder.m_wedgeIsBorder = false;
  wedgeDecoder.m_wedgeValence = 0;
  wedgeDecoder.m_wedgeCenterSharpness = 0.0;
  wedgeDecoder.m_wedgeSecondHighestSharpness = 0.0;
  wedgeDecoder.m_wedgeThirdHighestSharpness = 0.0;
  wedgeDecoder.m_wedgeCenterSemisharpEdge0 = 0;
  wedgeDecoder.m_wedgeCenterSemisharpEdge1 = 0;
  wedgeDecoder.m_lastDataStartIndex = 0;
  wedgeDecoder.m_numThreads = 0;
  wedgeDecoder.m_maxNumSlices = 0;

  initLimitIrregularWedgeDecoderPerThreadData(wedgeDecoder, runtimeIrregularInfo, irregularEdgeSharpness);

  wedgeDecoder.m_lastDataStartIndex = 0;

  let expectedNumPts = 1 + 2 * runtimeIrregularInfo.m_valence;
  if (runtimeIrregularInfo.m_isBorder) expectedNumPts += 1;

  if (!(weightIndex < expectedNumPts)) {
    throw new Error('limitIrregularWedgeInitDecoderSingleWeight: weightIndex >= expectedNumPts');
  }

  for (let i = 0; i < expectedNumPts; i++) {
    const val = (i === weightIndex) ? 1.0 : 0.0;
    if (i === 0) {
      wedgeDecoder.m_dataCenter[0] = val;
    } else {
      wedgeDecoder.m_dataInner[0][i - 1] = val;
    }
  }
}

// Port of LimitIrregularWedgeRunIteration (DcciSubdiv.cpp:8173). One
// CC subdiv step on the wedge data block.
function limitIrregularWedgeRunIteration(wedgeDecoder, iteration) {
  const prevIter = wedgeDecoder.m_lastDataStartIndex;
  const currIter = (wedgeDecoder.m_lastDataStartIndex + 1) % 2;

  // Reset target slots to debugging sentinels (matches amolite).
  for (let i = 0; i < wedgeDecoder.m_numThreads; i++) {
    wedgeDecoder.m_dataCenter[currIter]              = -1000.0;
    wedgeDecoder.m_dataInner[currIter][2 * i + 0]    = -2000.0;
    wedgeDecoder.m_dataInner[currIter][2 * i + 1]    = -2000.0;
  }

  // Pass 1: compute face points f11.
  for (let threadId = 0; threadId < wedgeDecoder.m_numThreads; threadId++) {
    const threadCorner = threadId;
    const valence  = wedgeDecoder.m_wedgeValence;
    const isBorder = wedgeDecoder.m_wedgeIsBorder;
    const numThreadsForWedge = isBorder ? (valence + 1) : valence;

    const currCorner = threadCorner;
    const nextCorner = (threadCorner + 1) % numThreadsForWedge;
    const firstThreadForWedge = threadId - threadCorner;

    const p00 = wedgeDecoder.m_dataCenter[prevIter];
    const p02 = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + nextCorner) + 0];
    const p20 = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + currCorner) + 0];
    const p22 = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + currCorner) + 1];

    const f11 = 0.25 * (p00 + p02 + p20 + p22);
    wedgeDecoder.m_dataInner[currIter][2 * (firstThreadForWedge + currCorner) + 1] = f11;
  }

  // Pass 2: compute edge points e10.
  for (let threadId = 0; threadId < wedgeDecoder.m_numThreads; threadId++) {
    const threadCorner = threadId;
    const valence  = wedgeDecoder.m_wedgeValence;
    const isBorder = wedgeDecoder.m_wedgeIsBorder;
    const numThreadsForWedge = isBorder ? (valence + 1) : valence;

    const currCorner = threadCorner;
    const nextCorner = (threadCorner + 1) % numThreadsForWedge;
    const prevCorner = (threadCorner + (numThreadsForWedge - 1)) % numThreadsForWedge;
    const firstThreadForWedge = threadId - threadCorner;

    const p00 = wedgeDecoder.m_dataCenter[prevIter];
    const p20 = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + currCorner) + 0];
    const p22 = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + currCorner) + 1];
    const f11 = wedgeDecoder.m_dataInner[currIter][2 * (firstThreadForWedge + currCorner) + 1];

    const edgeBaseSharpness = wedgeDecoder.m_irregularSharpness[firstThreadForWedge + currCorner];
    const edgeCurrSharpnessT = saturateF(
      (edgeBaseSharpness < 10.0) ? (edgeBaseSharpness - iteration) : 10.0);

    const isEdgeBorder = (isBorder && (threadCorner === 0 || threadCorner === valence));

    let e10 = 0.0;
    const e10_sharp = 0.5 * (p00 + p20);

    if (isEdgeBorder) {
      e10 = e10_sharp;
    } else {
      const f1_n1 = wedgeDecoder.m_dataInner[currIter][2 * (firstThreadForWedge + prevCorner) + 1];
      const e10_smooth = 0.25 * (p00 + p20 + f1_n1 + f11);
      e10 = e10_smooth * (1.0 - edgeCurrSharpnessT) + e10_sharp * edgeCurrSharpnessT;
    }

    wedgeDecoder.m_dataInner[currIter][2 * (firstThreadForWedge + currCorner) + 0] = e10;
  }

  // Pass 3 (single-thread): center vertex update.
  {
    const threadId = 0;
    const threadCorner = threadId;
    const valence  = wedgeDecoder.m_wedgeValence;
    const firstThreadForWedge = threadId - threadCorner;

    if (threadCorner === 0) {
      const centerVertexSharpness  = wedgeDecoder.m_wedgeCenterSharpness;
      const secondHighestSharpness = wedgeDecoder.m_wedgeSecondHighestSharpness;
      const thirdHighestSharpness  = wedgeDecoder.m_wedgeThirdHighestSharpness;
      const edge0 = wedgeDecoder.m_wedgeCenterSemisharpEdge0;
      const edge1 = wedgeDecoder.m_wedgeCenterSemisharpEdge1;

      const ws = getCenterVertexEdgeSharpnessInfoHelperIrregular(
        secondHighestSharpness, thirdHighestSharpness, centerVertexSharpness, iteration);

      const centerVal = wedgeDecoder.m_dataCenter[prevIter];

      let dst_smooth = 0.0;
      {
        let sumEdge = 0.0;
        let sumFace = 0.0;
        for (let cornerIter = 0; cornerIter < valence; cornerIter++) {
          const currEdge = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + cornerIter) + 0];
          const currFace = wedgeDecoder.m_dataInner[currIter][2 * (firstThreadForWedge + cornerIter) + 1];
          sumEdge += currEdge;
          sumFace += currFace;
        }
        const scaleCenter   = (valence - 2) / valence;
        const scaleEdgeFace = 1.0 / (valence * valence);
        dst_smooth = scaleCenter * centerVal + scaleEdgeFace * sumEdge + scaleEdgeFace * sumFace;
      }

      let dst_edge = 0.0;
      {
        const valEdge0 = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + edge0) + 0];
        const valEdge1 = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + edge1) + 0];
        dst_edge = subdivVertexSharpRuleFloat(valEdge0, valEdge1, centerVal);
      }

      const dst_corner = centerVal;
      const dst = dst_corner * ws.dstWeightCorner +
                  dst_edge   * ws.dstWeightEdge   +
                  dst_smooth * ws.dstWeightSmooth;

      wedgeDecoder.m_dataCenter[currIter] = dst;
    }
  }

  wedgeDecoder.m_lastDataStartIndex = (wedgeDecoder.m_lastDataStartIndex + 1) % 2;
}

// Port of LimitIrregularWedgeCalcLimitP (DcciSubdiv.cpp:8515).
function limitIrregularWedgeCalcLimitP(wedgeDecoder) {
  const prevIter = wedgeDecoder.m_lastDataStartIndex;
  let ret = 0.0;

  const threadId = 0;
  const threadCorner = threadId;
  const valence  = wedgeDecoder.m_wedgeValence;
  const isBorder = wedgeDecoder.m_wedgeIsBorder;
  const numThreadsForWedge = isBorder ? (valence + 1) : valence;
  const firstThreadForWedge = threadId - threadCorner;

  const centerVal = wedgeDecoder.m_dataCenter[prevIter];

  if (isBorder) {
    ret = centerVal;
  } else {
    let sumEdge = 0.0;
    let sumFace = 0.0;
    for (let cornerIter = 0; cornerIter < numThreadsForWedge; cornerIter++) {
      const currEdge = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + cornerIter) + 0];
      let currFace = 0.0;
      if (cornerIter < valence) {
        currFace = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + cornerIter) + 1];
      }
      sumEdge += currEdge;
      sumFace += currFace;
    }
    let numer = 0.0;
    numer += valence * valence * centerVal;
    numer += 4.0 * sumEdge;
    numer += 1.0 * sumFace;

    const denom = valence * (valence + 5.0);
    ret = numer * safeInv(denom);
  }
  return ret;
}

// Port of CalcAn (DcciSubdiv.cpp:8574).
function calcAn(n) {
  const PI = 3.14159265359;
  const invN = safeInv(n);

  let An = 0.0;
  An += 1.0;
  An += Math.cos(2.0 * PI * invN);
  An += Math.cos(PI * invN) * Math.sqrt(2.0 * (9.0 + Math.cos(2.0 * PI * invN)));
  return An;
}

// Port of LimitIrregularWedgeCalcLimitT (DcciSubdiv.cpp:8589).
function limitIrregularWedgeCalcLimitT(wedgeDecoder, tangentCorner) {
  const prevIter = wedgeDecoder.m_lastDataStartIndex;
  const PI = 3.14159265359;

  let ret = 0.0;
  const threadId = 0;
  const threadCorner = threadId;
  const valence  = wedgeDecoder.m_wedgeValence;
  const isBorder = wedgeDecoder.m_wedgeIsBorder;
  const firstThreadForWedge = threadId - threadCorner;

  if (valence === 1) {
    const centerVal = wedgeDecoder.m_dataCenter[prevIter];
    const e0 = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + 0) + 0];
    const e1 = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + 1) + 0];
    ret = (tangentCorner === 0) ? (e0 - centerVal) : (e1 - centerVal);
  } else if (valence === 2 && !isBorder) {
    const e0 = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + 0) + 0];
    const e1 = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + 1) + 0];
    const f0 = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + 0) + 1];
    const f1 = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + 1) + 1];
    ret = (tangentCorner === 0) ? (e0 - e1) : (f0 - f1);
  } else {
    const numThreadsForWedge = isBorder ? (valence + 1) : valence;
    const centerVal = wedgeDecoder.m_dataCenter[prevIter];

    if (isBorder) {
      const currEdge = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + tangentCorner) + 0];
      ret = currEdge - centerVal;
    } else {
      let sum_c2 = 0.0;
      const An = calcAn(valence);

      for (let cornerIter = 0; cornerIter < numThreadsForWedge; cornerIter++) {
        const currEdge = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + cornerIter) + 0];

        let currFace;
        if (isBorder && cornerIter === valence) {
          const edge0 = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + 0) + 0];
          const edge1 = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + valence) + 0];
          currFace = centerVal + (edge0 - centerVal) + (edge1 - centerVal);
        } else {
          currFace = wedgeDecoder.m_dataInner[prevIter][2 * (firstThreadForWedge + cornerIter) + 1];
        }

        const j = ((valence + cornerIter - tangentCorner) % valence);
        const invN = safeInv(numThreadsForWedge);
        const cosj0 = Math.cos(2.0 * PI * j * invN);
        const cosj1 = Math.cos(2.0 * PI * (j + 1.0) * invN);

        let curr_c2 = 0.0;
        curr_c2 += An * cosj0 * currEdge;
        curr_c2 += (cosj0 + cosj1) * currFace;
        sum_c2 += curr_c2;
      }
      ret = sum_c2;
    }
  }
  return ret;
}

// Port of CatmullClarkPatches::CalculateLimitWeights (DcciSubdiv.cpp:8026).
// For each ring point, drive 10 iterations of the wedge limit finder
// and read out (p, t0, t1). Returns the three weight arrays.
function calculateLimitWeights(runtimeIrregularInfo, irregularEdgeSharpness) {
  let expectedNumPts = 1 + 2 * runtimeIrregularInfo.m_valence;
  if (runtimeIrregularInfo.m_isBorder) expectedNumPts += 1;

  const weightP  = new Array(expectedNumPts).fill(0.0);
  const weightT0 = new Array(expectedNumPts).fill(0.0);
  const weightT1 = new Array(expectedNumPts).fill(0.0);

  for (let weightIter = 0; weightIter < expectedNumPts; weightIter++) {
    const limitFinder = makeIrregularWedgeLimitFinder();
    limitIrregularWedgeInitDecoderSingleWeight(limitFinder, runtimeIrregularInfo, irregularEdgeSharpness, weightIter);

    const s_numIter = 10;
    for (let iter = 0; iter < s_numIter; iter++) {
      limitIrregularWedgeRunIteration(limitFinder, iter);
    }

    weightP [weightIter] = limitIrregularWedgeCalcLimitP(limitFinder);
    weightT0[weightIter] = limitIrregularWedgeCalcLimitT(limitFinder, 0);
    weightT1[weightIter] = limitIrregularWedgeCalcLimitT(limitFinder, 1);
  }

  return { weightP, weightT0, weightT1 };
}

// =====================================================================
// Phase 1b.13e: BuildLimitPointsInfo (the goal post)
// =====================================================================

// Port of CatmullClarkBuilder::BuildLimitPointsInfo (DcciSubdiv.cpp:5571).
// Builds patches.m_vertexLimitInfo (the per-output-vertex stencils that
// Phase 2 / runtime CC consume to compute limit positions + tangents
// each frame).
function buildLimitPointsInfo(patches, helper0) {
  const numVtx = patches.m_vertexInfo.length;

  // ---- regular path: per-vertex bicubic limit weights ----
  const regularVertexP  = new Array(numVtx);
  const regularVertexT0 = new Array(numVtx);
  const regularVertexT1 = new Array(numVtx);
  for (let i = 0; i < numVtx; i++) {
    regularVertexP[i]  = makeFlatWeightVertex();
    regularVertexT0[i] = makeFlatWeightVertex();
    regularVertexT1[i] = makeFlatWeightVertex();
  }

  for (let polyIter = 0; polyIter < patches.m_polyInfo.length; polyIter++) {
    const polyInfo = patches.m_polyInfo[polyIter];
    const polyIndexStart = helper0.m_polyStarts[polyIter];
    const polyIndexCount = helper0.m_polySizes[polyIter];

    if (polyInfo.m_type !== kSubdivPolyType_QuadRegular &&
        polyInfo.m_type !== kSubdivPolyType_QuadIrregular1 &&
        polyInfo.m_type !== kSubdivPolyType_QuadIrregular2) {
      continue;
    }
    if (polyIndexCount !== 4) {
      throw new Error('buildLimitPointsInfo: non-quad poly ' + polyIter);
    }

    const cpDim = getPolyDimControlPoints(polyInfo.m_type);

    // Per-corner direction vectors into the (cpDim x cpDim) grid.
    const cornerDirX = [
      [ 1,  0],
      [ 0, -1],
      [-1,  0],
      [ 0,  1],
    ];
    const cornerDirY = [
      [0, 1],
      [1, 0],
      [0,-1],
      [-1,0],
    ];
    const cornerGrid = [
      [0, 0],
      [0, 1],
      [1, 1],
      [1, 0],
    ];

    for (let polyCorner = 0; polyCorner < 4; polyCorner++) {
      const currVtx = helper0.m_polyIndexList[polyIndexStart + polyCorner];
      const vtxInfo = patches.m_vertexInfo[currVtx];
      if (vtxInfo.m_dstIrregularIndex >= 0) continue;

      const sharpness0 = polyInfo.m_edgeSharpness[(polyCorner + 3) % 4];
      const sharpness1 = polyInfo.m_edgeSharpness[polyCorner];

      if (!(sharpness0 === 0.0 || sharpness1 === 0.0)) {
        throw new Error('buildLimitPointsInfo: both edges sharp at corner (' +
                        sharpness0 + ', ' + sharpness1 + ')');
      }

      const actualSharpness = sharpness0 > sharpness1 ? sharpness0 : sharpness1;
      const mainEdgeX = (sharpness0 > 0);
      const needsRotation = mainEdgeX;

      const dirX = { x: cornerDirX[polyCorner][0], y: cornerDirX[polyCorner][1] };
      const dirY = { x: cornerDirY[polyCorner][0], y: cornerDirY[polyCorner][1] };
      const gridStart = {
        x: cornerGrid[polyCorner][0] * (cpDim - 1),
        y: cornerGrid[polyCorner][1] * (cpDim - 1),
      };

      const pts = new Array(16);
      for (let r = 0; r < 4; r++) {
        for (let c = 0; c < 4; c++) {
          const gx = gridStart.x + c * dirX.x + r * dirY.x;
          const gy = gridStart.y + c * dirX.y + r * dirY.y;
          if (!(gx >= 0 && gx < cpDim && gy >= 0 && gy < cpDim)) {
            throw new Error('buildLimitPointsInfo: polyCpGrid OOB');
          }
          const srcOffset = gy * cpDim + gx;
          const ptIndex = patches.m_controlPointRedirect.m_regularCps[
            polyInfo.m_quadRegionGrid.m_pointsStart + srcOffset];
          pts[r * 4 + c] = ptIndex;
        }
      }

      let uv = { x: 0.0, y: 0.0 };
      if (needsRotation) {
        const tempPts = new Array(16);
        for (let r = 0; r < 4; r++) {
          for (let c = 0; c < 4; c++) {
            const dstIndex = r * 4 + c;
            const srcIndex = (3 - r) + c * 4;
            tempPts[dstIndex] = pts[srcIndex];
          }
        }
        // amolite REPLACES the local pts with tempPts here (the rotation
        // result), but only the rotated `uv` is actually used by the
        // EvalBicubic call below since the eval uses the pre-rotation
        // `pts` array (amolite never writes back tempPts -> pts in the
        // source). Match exactly.
        const dstUv = { x: uv.y, y: 1.0 - uv.x };
        uv = dstUv;
      }

      const ptFlatWeights = new Array(16);
      for (let i = 0; i < 16; i++) {
        const basePt = pts[i];
        ptFlatWeights[i] = runtimeWeightInflDataFetchRegularFlatVertex(patches.m_weightInflData, basePt);
      }

      const dummyOffsets = new Array(16);
      for (let i = 0; i < 16; i++) dummyOffsets[i] = i;

      const ev = bsplineUtilEvalBicubicBSplineSharpnessFlatWeights(
        dummyOffsets, ptFlatWeights, actualSharpness, uv.x, uv.y);

      flatWeightVertexCompactInfluenceList(ev.foundP);
      flatWeightVertexCompactInfluenceList(ev.foundT0);
      flatWeightVertexCompactInfluenceList(ev.foundT1);

      regularVertexP[currVtx]  = ev.foundP;
      regularVertexT0[currVtx] = ev.foundT0;
      regularVertexT1[currVtx] = ev.foundT1;
    }
  }

  // ---- irregular path: per-irregular-wedge limit weights ----
  patches.m_numLimitPoints = patches.m_numRegularPoints +
                             patches.m_numIrregularVertexWedges +
                             patches.m_numIrregularTriangles;

  const numLimitPoints = patches.m_numLimitPoints;
  const limitVertexP  = new Array(numLimitPoints);
  const limitVertexT0 = new Array(numLimitPoints);
  const limitVertexT1 = new Array(numLimitPoints);
  for (let i = 0; i < numLimitPoints; i++) {
    limitVertexP[i]  = makeFlatWeightVertex();
    limitVertexT0[i] = makeFlatWeightVertex();
    limitVertexT1[i] = makeFlatWeightVertex();
  }

  const numIrregularPatch = patches.m_irregularWedgeInfo.length;
  const irregularVertexP  = new Array(numIrregularPatch);
  const irregularVertexT0 = new Array(numIrregularPatch);
  const irregularVertexT1 = new Array(numIrregularPatch);

  if (patches.m_irregularWedgeInfo.length !== patches.m_weightInflData.m_runtimeIrregularInfo.length) {
    throw new Error('buildLimitPointsInfo: irregularWedgeInfo / runtimeIrregularInfo length mismatch');
  }

  for (let irregIter = 0; irregIter < numIrregularPatch; irregIter++) {
    const runtimeInfo = patches.m_weightInflData.m_runtimeIrregularInfo[irregIter];
    // const wedgeInfo = patches.m_irregularWedgeInfo[irregIter];

    const valence  = runtimeInfo.m_valence;
    const isBorder = !!runtimeInfo.m_isBorder;

    const lw = calculateLimitWeights(runtimeInfo, patches.m_weightInflData.m_irregularEdgeSharpness);
    const weightP  = lw.weightP;
    const weightT0 = lw.weightT0;
    const weightT1 = lw.weightT1;

    const numRingPts = 1 + valence * 2 + (isBorder ? 1 : 0);

    // Collect ring point FWVs via the runtime accessors.
    const collectedLimitRingPts = new Array(numRingPts);
    {
      // Center.
      const runtimeCpIndex = runtimeWeightInflDataFetchIrregularIndex(
        patches.m_weightInflData, runtimeInfo.m_pointStart + 0);
      collectedLimitRingPts[0] = runtimeWeightInflDataFetchRegularFlatVertex(
        patches.m_weightInflData, runtimeCpIndex);
    }
    for (let threadCorner = 0; threadCorner < valence; threadCorner++) {
      const idx0 = runtimeWeightInflDataFetchIrregularIndex(
        patches.m_weightInflData, runtimeInfo.m_pointStart + 1 + 6 * threadCorner + 0);
      const idx1 = runtimeWeightInflDataFetchIrregularIndex(
        patches.m_weightInflData, runtimeInfo.m_pointStart + 1 + 6 * threadCorner + 1);
      collectedLimitRingPts[1 + threadCorner * 2 + 0] = runtimeWeightInflDataFetchRegularFlatVertex(patches.m_weightInflData, idx0);
      collectedLimitRingPts[1 + threadCorner * 2 + 1] = runtimeWeightInflDataFetchRegularFlatVertex(patches.m_weightInflData, idx1);
    }
    if (isBorder) {
      const idx0 = runtimeWeightInflDataFetchIrregularIndex(
        patches.m_weightInflData, runtimeInfo.m_pointStart + 1 + 6 * valence + 0);
      collectedLimitRingPts[1 + valence * 2 + 0] = runtimeWeightInflDataFetchRegularFlatVertex(patches.m_weightInflData, idx0);
    }

    // Combine ring weights * ring FWVs.
    let foundP  = makeFlatWeightVertex();
    let foundT0 = makeFlatWeightVertex();
    let foundT1 = makeFlatWeightVertex();
    for (let ringPtIter = 0; ringPtIter < collectedLimitRingPts.length; ringPtIter++) {
      const currP = collectedLimitRingPts[ringPtIter];
      const wp  = weightP [ringPtIter];
      const wt0 = weightT0[ringPtIter];
      const wt1 = weightT1[ringPtIter];

      foundP  = flatWeightVertexAddDirect(foundP,  flatWeightVertexMulDirect(currP, wp));
      foundT0 = flatWeightVertexAddDirect(foundT0, flatWeightVertexMulDirect(currP, wt0));
      foundT1 = flatWeightVertexAddDirect(foundT1, flatWeightVertexMulDirect(currP, wt1));
    }

    flatWeightVertexNormalizeSignedRange(foundT0);
    flatWeightVertexNormalizeSignedRange(foundT1);

    flatWeightVertexCompactInfluenceList(foundP);
    flatWeightVertexCompactInfluenceList(foundT0);
    flatWeightVertexCompactInfluenceList(foundT1);

    irregularVertexP [irregIter] = foundP;
    irregularVertexT0[irregIter] = foundT0;
    irregularVertexT1[irregIter] = foundT1;
  }

  // ---- gather per-vertex (regular or irregular) into limitVertex* ----
  for (let vtxIter = 0; vtxIter < numVtx; vtxIter++) {
    const vtxInfo = patches.m_vertexInfo[vtxIter];

    const regularP  = regularVertexP[vtxIter];
    const regularT0 = regularVertexT0[vtxIter];
    const regularT1 = regularVertexT1[vtxIter];

    const numP = flatWeightVertexGetNum(regularP);
    const isValid = numP > 0;
    if (isValid) {
      if (flatWeightVertexGetNum(regularT0) === 0) throw new Error('buildLimitPointsInfo: regularT0 empty but P not');
      if (flatWeightVertexGetNum(regularT1) === 0) throw new Error('buildLimitPointsInfo: regularT1 empty but P not');
    } else {
      if (flatWeightVertexGetNum(regularT0) !== 0) throw new Error('buildLimitPointsInfo: regularT0 has weights but P empty');
      if (flatWeightVertexGetNum(regularT1) !== 0) throw new Error('buildLimitPointsInfo: regularT1 has weights but P empty');
    }

    const vtxStartIndex = vtxInfo.m_limitStartIndex;
    const vtxNumWedges  = vtxInfo.m_numWedges;

    if (isValid) {
      if (vtxNumWedges !== 1) throw new Error('buildLimitPointsInfo: regular vtx with numWedges != 1');
      limitVertexP [vtxStartIndex + 0] = regularP;
      limitVertexT0[vtxStartIndex + 0] = regularT0;
      limitVertexT1[vtxStartIndex + 0] = regularT1;
    } else {
      const irregularIndex = vtxInfo.m_dstIrregularIndex;
      if (!(irregularIndex >= 0)) throw new Error('buildLimitPointsInfo: irregular vtx has dstIrregularIndex < 0');

      for (let wedgeIter = 0; wedgeIter < vtxNumWedges; wedgeIter++) {
        limitVertexP [vtxStartIndex + wedgeIter] = irregularVertexP [irregularIndex + wedgeIter];
        limitVertexT0[vtxStartIndex + wedgeIter] = irregularVertexT0[irregularIndex + wedgeIter];
        limitVertexT1[vtxStartIndex + wedgeIter] = irregularVertexT1[irregularIndex + wedgeIter];
      }
    }
  }

  // ---- tri-center slots after the vertex slots ----
  for (let triIter = 0; triIter < patches.m_numIrregularTriangles; triIter++) {
    const srcIndex = patches.m_numIrregularVertexWedges + triIter;
    const dstIndex = patches.m_numRegularPoints + patches.m_numIrregularVertexWedges + triIter;
    limitVertexP [dstIndex] = irregularVertexP [srcIndex];
    limitVertexT0[dstIndex] = irregularVertexT0[srcIndex];
    limitVertexT1[dstIndex] = irregularVertexT1[srcIndex];
  }

  // ---- merge + normalize + pack into VertexLimitInfo ----
  const numLimitPointInfluences = new Array(numLimitPoints).fill(0);
  let totalInfluneces = 0;

  for (let i = 0; i < numLimitPoints; i++) {
    let foundP  = limitVertexP[i];
    let foundT0 = limitVertexT0[i];
    let foundT1 = limitVertexT1[i];

    const influences = foundP.m_influences.slice();
    flatWeightVertexMergeIndexList(influences, foundT0.m_influences);
    flatWeightVertexMergeIndexList(influences, foundT1.m_influences);

    const fullZero = makeFlatWeightVertex();
    fullZero.m_influences = influences.slice();
    fullZero.m_weights    = new Array(influences.length).fill(0.0);

    const dstP  = flatWeightVertexAddDirect(foundP,  fullZero);
    let   dstT0 = flatWeightVertexAddDirect(foundT0, fullZero);
    let   dstT1 = flatWeightVertexAddDirect(foundT1, fullZero);

    flatWeightVertexNormalizeSignedRange(dstT0);
    flatWeightVertexNormalizeSignedRange(dstT1);

    limitVertexP [i] = dstP;
    limitVertexT0[i] = dstT0;
    limitVertexT1[i] = dstT1;

    numLimitPointInfluences[i] = influences.length;
    totalInfluneces           += influences.length;
  }

  const limitPointInfluencesStart = calcPrefixSumVec(numLimitPointInfluences);

  const limitInfo = makeVertexLimitInfo();
  limitInfo.m_limitDataNum   = numLimitPointInfluences.slice();
  limitInfo.m_limitDataStart = limitPointInfluencesStart.slice();
  limitInfo.m_limitInfluenceData = new Array(totalInfluneces).fill(0);
  limitInfo.m_limitWeightDataP   = new Array(totalInfluneces).fill(0.0);
  limitInfo.m_limitWeightDataT0  = new Array(totalInfluneces).fill(0.0);
  limitInfo.m_limitWeightDataT1  = new Array(totalInfluneces).fill(0.0);

  for (let vtxIter = 0; vtxIter < numLimitPoints; vtxIter++) {
    const foundP  = limitVertexP[vtxIter];
    const foundT0 = limitVertexT0[vtxIter];
    const foundT1 = limitVertexT1[vtxIter];

    const numInfl = foundP.m_influences.length;
    if (flatWeightVertexGetNum(foundP)  !== numInfl) throw new Error('buildLimitPointsInfo: P length mismatch');
    if (flatWeightVertexGetNum(foundT0) !== numInfl) throw new Error('buildLimitPointsInfo: T0 length mismatch');
    if (flatWeightVertexGetNum(foundT1) !== numInfl) throw new Error('buildLimitPointsInfo: T1 length mismatch');

    const start = limitPointInfluencesStart[vtxIter];
    for (let inflIter = 0; inflIter < numInfl; inflIter++) {
      limitInfo.m_limitInfluenceData[start + inflIter] = foundP.m_influences[inflIter];
      limitInfo.m_limitWeightDataP  [start + inflIter] = foundP.m_weights   [inflIter];
      limitInfo.m_limitWeightDataT0 [start + inflIter] = foundT0.m_weights  [inflIter];
      limitInfo.m_limitWeightDataT1 [start + inflIter] = foundT1.m_weights  [inflIter];
    }
  }

  patches.m_vertexLimitInfo = limitInfo;
}

// =====================================================================
// Phase 3a: IrregularWedgeDecoder + per-frame iteration
// =====================================================================

// Port of CatmullClarkPatches::CalcNumDstPointsPerSliceForWedge
// (DcciSubdiv.cpp:7137).
function calcNumDstPointsPerSliceForWedge(valence, hasBorder) {
  let numPointsPerSlice = 1 + valence * 12;
  if (hasBorder) numPointsPerSlice += 3;
  return numPointsPerSlice;
}
// Port of CatmullClarkPatches::CalcNumSlicesForWedge (DcciSubdiv.cpp:7149).
function calcNumSlicesForWedge(targetLevel) {
  const c = Math.ceil(targetLevel);
  return c < 1 ? 1 : c;
}
// Port of CatmullClarkPatches::CalcNumDstPointsForWedge (DcciSubdiv.cpp:7155).
function calcNumDstPointsForWedge(valence, hasBorder, targetLevel) {
  return calcNumSlicesForWedge(targetLevel) * calcNumDstPointsPerSliceForWedge(valence, hasBorder);
}

// Port of struct IrregularWedgeDecoder (DcciSubdiv.h:504). Vec3 fields
// are stored as flat Float32Array (3 floats per Vec3) for cache-
// friendly access. m_dataInner[2] is two ping-pong buffers; m_dataOuter
// is single-buffered (discarded each iteration); m_dataCenter[2] holds
// per-wedge center positions.
function makeIrregularWedgeDecoder() {
  return {
    // Vec3[IRREGULAR_GROUP_SIZE * 6] each (stored as flat f32 N*3).
    m_dataOuter:    new Float32Array(IRREGULAR_GROUP_SIZE * 6 * 3),
    m_dataInner:    [new Float32Array(IRREGULAR_GROUP_SIZE * 6 * 3),
                     new Float32Array(IRREGULAR_GROUP_SIZE * 6 * 3)],
    // Vec3[IRREGULAR_GROUP_SIZE / 2] each.
    m_dataCenter:   [new Float32Array((IRREGULAR_GROUP_SIZE / 2) * 3),
                     new Float32Array((IRREGULAR_GROUP_SIZE / 2) * 3)],
    m_irregularWedgeIndex:        new Int32Array(IRREGULAR_GROUP_SIZE),
    m_irregularCornerIndex:       new Int32Array(IRREGULAR_GROUP_SIZE),
    m_irregularSharpness:         new Float32Array(IRREGULAR_GROUP_SIZE),
    m_wedgeIsBorder:              new Uint8Array(IRREGULAR_GROUP_SIZE),
    m_wedgeValence:               new Int32Array(IRREGULAR_GROUP_SIZE),
    m_wedgeCenterSharpness:       new Float32Array(IRREGULAR_GROUP_SIZE),
    m_wedgeSecondHighestSharpness:new Float32Array(IRREGULAR_GROUP_SIZE),
    m_wedgeThirdHighestSharpness: new Float32Array(IRREGULAR_GROUP_SIZE),
    m_wedgeCenterSemisharpEdge0:  new Float32Array(IRREGULAR_GROUP_SIZE),
    m_wedgeCenterSemisharpEdge1:  new Float32Array(IRREGULAR_GROUP_SIZE),
    m_wedgeNumSlices:             new Float32Array(IRREGULAR_GROUP_SIZE),
    m_lastDataStartIndex:         0,
    m_maxNumSlices:               0,
  };
}

// ---- Vec3 helpers for IrregularWedge math (operate on flat f32 trios) ----
function v3Get(arr, i)               { return [arr[3*i+0], arr[3*i+1], arr[3*i+2]]; }
function v3Set(arr, i, v)            { arr[3*i+0] = v[0]; arr[3*i+1] = v[1]; arr[3*i+2] = v[2]; }
function v3Add(a, b)                 { return [a[0]+b[0], a[1]+b[1], a[2]+b[2]]; }
function v3AddN(...vs) {
  let x = 0, y = 0, z = 0;
  for (let i = 0; i < vs.length; i++) { x += vs[i][0]; y += vs[i][1]; z += vs[i][2]; }
  return [x, y, z];
}
function v3Scale(a, s)               { return [a[0]*s, a[1]*s, a[2]*s]; }
function v3Lerp(a, b, t)             { return [a[0]+(b[0]-a[0])*t, a[1]+(b[1]-a[1])*t, a[2]+(b[2]-a[2])*t]; }

// SubdivVertexSharpRule for Vec3 (DcciSubdiv.cpp:7432). 0.125 * (e0 + 6*v + e1).
function subdivVertexSharpRuleVec3(e0, e1, v) {
  return [
    0.125 * (e0[0] + 6.0 * v[0] + e1[0]),
    0.125 * (e0[1] + 6.0 * v[1] + e1[1]),
    0.125 * (e0[2] + 6.0 * v[2] + e1[2]),
  ];
}

// Port of GetCenterVertexEdgeSharpnessInfoHelper (DcciSubdiv.cpp:7442).
// Same shape as the irregular version we already shipped in Phase 1b.13d
// -- duplicate it under the per-frame-specific name to avoid the
// "two functions of the same name with different bodies" confusion.
function getCenterVertexEdgeSharpnessInfoHelperPerFrame(
    secondHighestEdgeSharpness, thirdHighestEdgeSharpness, centerVertexSharpness, iteration) {
  const secondInfl = saturateF(
    secondHighestEdgeSharpness >= SUBDIV_MAX_SHARPNESS ? 1.0 :
    (secondHighestEdgeSharpness - iteration));
  const thirdInfl = saturateF(
    thirdHighestEdgeSharpness >= SUBDIV_MAX_SHARPNESS ? 1.0 :
    (thirdHighestEdgeSharpness - iteration));
  const vertexInfl = saturateF(
    centerVertexSharpness >= SUBDIV_MAX_SHARPNESS ? 1.0 :
    (centerVertexSharpness - iteration));

  const cornerInfl = thirdInfl > vertexInfl ? thirdInfl : vertexInfl;
  const edgeInfl   = secondInfl;

  return {
    dstWeightCorner: cornerInfl,
    dstWeightEdge:   (1.0 - cornerInfl) * edgeInfl,
    dstWeightSmooth: (1.0 - cornerInfl) * (1.0 - edgeInfl),
  };
}

// Port of static InitIrregularWedgeDecoderPerThreadData
// (DcciSubdiv.cpp:7163). Populates per-thread metadata for the group's
// wedges; dead slots get safe default values.
function initIrregularWedgeDecoderPerThreadData(wedgeDecoder, runtimeWeightInflData, wedgeLevels, irregularGroupIndex) {
  const irregularGroupPatchStart = runtimeWeightInflData.m_irregularGroupStart[irregularGroupIndex];
  const irregularGroupPatchNum   = runtimeWeightInflData.m_irregularGroupNum[irregularGroupIndex];

  let maxNumSlices = 0;
  let threadStart  = 0;
  for (let patchIter = 0; patchIter < irregularGroupPatchNum; patchIter++) {
    const currIrregularWedge = irregularGroupPatchStart + patchIter;
    const runtimeIrregularInfo = runtimeWeightInflData.m_runtimeIrregularInfo[currIrregularWedge];

    const numThreadsForGroup = runtimeIrregularInfo.m_isBorder
      ? (runtimeIrregularInfo.m_valence + 1)
      : runtimeIrregularInfo.m_valence;
    const wedgeLevel = wedgeLevels[currIrregularWedge];
    const numSlices  = calcNumSlicesForWedge(wedgeLevel);

    if (numSlices > maxNumSlices) maxNumSlices = numSlices;

    for (let threadIter = 0; threadIter < numThreadsForGroup; threadIter++) {
      const threadId = threadStart + threadIter;
      if (!(threadId < IRREGULAR_GROUP_SIZE)) {
        throw new Error('initIrregularWedgeDecoderPerThreadData: threadId >= IRREGULAR_GROUP_SIZE');
      }
      wedgeDecoder.m_irregularWedgeIndex[threadId]  = patchIter;
      wedgeDecoder.m_irregularCornerIndex[threadId] = threadIter;

      let edgeSharpness = 0.0;
      if (runtimeIrregularInfo.m_edgeSharpnessStart >= 0) {
        edgeSharpness = runtimeWeightInflData.m_irregularEdgeSharpness[
          runtimeIrregularInfo.m_edgeSharpnessStart + threadIter];
      }
      wedgeDecoder.m_irregularSharpness[threadId] = edgeSharpness;

      wedgeDecoder.m_wedgeIsBorder[threadId]               = runtimeIrregularInfo.m_isBorder ? 1 : 0;
      wedgeDecoder.m_wedgeValence[threadId]                = runtimeIrregularInfo.m_valence;
      wedgeDecoder.m_wedgeCenterSharpness[threadId]        = runtimeIrregularInfo.m_centerVertexSharpness;
      wedgeDecoder.m_wedgeSecondHighestSharpness[threadId] = runtimeIrregularInfo.m_secondHighestEdgeSharpness;
      wedgeDecoder.m_wedgeThirdHighestSharpness[threadId]  = runtimeIrregularInfo.m_thirdHighestEdgeSharpness;
      wedgeDecoder.m_wedgeCenterSemisharpEdge0[threadId]   = runtimeIrregularInfo.m_centerSemisharpEdge0;
      wedgeDecoder.m_wedgeCenterSemisharpEdge1[threadId]   = runtimeIrregularInfo.m_centerSemisharpEdge1;
      wedgeDecoder.m_wedgeNumSlices[threadId]              = wedgeLevel;
    }
    threadStart += numThreadsForGroup;
  }

  // Dead slots. amolite assigns dummy defaults so nothing is uninitialized.
  for (let threadId = threadStart; threadId < IRREGULAR_GROUP_SIZE; threadId++) {
    wedgeDecoder.m_irregularWedgeIndex[threadId]        = -1;
    wedgeDecoder.m_irregularCornerIndex[threadId]       = 0;
    wedgeDecoder.m_irregularSharpness[threadId]         = 0.0;
    wedgeDecoder.m_wedgeIsBorder[threadId]              = 0;
    wedgeDecoder.m_wedgeValence[threadId]               = 4;
    wedgeDecoder.m_wedgeCenterSharpness[threadId]       = 0.0;
    wedgeDecoder.m_wedgeSecondHighestSharpness[threadId]= 0.0;
    wedgeDecoder.m_wedgeThirdHighestSharpness[threadId] = 0.0;
    wedgeDecoder.m_wedgeCenterSemisharpEdge0[threadId]  = 0;
    wedgeDecoder.m_wedgeCenterSemisharpEdge1[threadId]  = 0;
    wedgeDecoder.m_wedgeNumSlices[threadId]             = 0;
  }
  wedgeDecoder.m_maxNumSlices = maxNumSlices;
}

// Port of FetchSrcIrregularVal (DcciSubdiv.cpp:7281).
function fetchSrcIrregularVal(runtimeWeightInflData, frameRuntimePts, pointStart, offset) {
  const srcIndex = runtimeWeightInflDataFetchIrregularIndex(runtimeWeightInflData, pointStart + offset);
  // frameRuntimePts is a Float32Array of 3*N (flat xyz). Return [x,y,z].
  return [
    frameRuntimePts[srcIndex * 3 + 0],
    frameRuntimePts[srcIndex * 3 + 1],
    frameRuntimePts[srcIndex * 3 + 2],
  ];
}

// Port of CatmullClarkPatches::IrregularWedgeInitDecoderVec3
// (DcciSubdiv.cpp:7290). Loads the initial inner ring + center positions
// for every active wedge in the group from frameRuntimePts.
// Applies the AMOLITE_DEFERRED_FIXES.md Fix 1 dead-slot guard (`if
// (localWedge < 0) continue;`).
function irregularWedgeInitDecoderVec3(wedgeDecoder, runtimeWeightInflData,
                                       frameRuntimePts, framePtsOffsetPerGroup,
                                       wedgeLevels, irregularGroupIndex) {
  // amolite: `wedgeDecoder = IrregularWedgeDecoder{};` -- reset in-place.
  wedgeDecoder.m_dataOuter.fill(0);
  wedgeDecoder.m_dataInner[0].fill(0);
  wedgeDecoder.m_dataInner[1].fill(0);
  wedgeDecoder.m_dataCenter[0].fill(0);
  wedgeDecoder.m_dataCenter[1].fill(0);
  wedgeDecoder.m_irregularWedgeIndex.fill(0);
  wedgeDecoder.m_irregularCornerIndex.fill(0);
  wedgeDecoder.m_irregularSharpness.fill(0);
  wedgeDecoder.m_wedgeIsBorder.fill(0);
  wedgeDecoder.m_wedgeValence.fill(0);
  wedgeDecoder.m_wedgeCenterSharpness.fill(0);
  wedgeDecoder.m_wedgeSecondHighestSharpness.fill(0);
  wedgeDecoder.m_wedgeThirdHighestSharpness.fill(0);
  wedgeDecoder.m_wedgeCenterSemisharpEdge0.fill(0);
  wedgeDecoder.m_wedgeCenterSemisharpEdge1.fill(0);
  wedgeDecoder.m_wedgeNumSlices.fill(0);
  wedgeDecoder.m_lastDataStartIndex = 0;
  wedgeDecoder.m_maxNumSlices = 0;

  initIrregularWedgeDecoderPerThreadData(wedgeDecoder, runtimeWeightInflData, wedgeLevels, irregularGroupIndex);

  for (let threadId = 0; threadId < IRREGULAR_GROUP_SIZE; threadId++) {
    const irregularGroupWedgeStart = runtimeWeightInflData.m_irregularGroupStart[irregularGroupIndex];
    const localWedge   = wedgeDecoder.m_irregularWedgeIndex[threadId];
    const threadCorner = wedgeDecoder.m_irregularCornerIndex[threadId];

    // Deferred fix (AMOLITE_DEFERRED_FIXES.md Fix 1): dead thread slot
    // -> skip cleanly. amolite-side reads m_runtimeIrregularInfo[-1]
    // and crashes on amolite::vector OOB; the JS port matches valid
    // behavior without the crash.
    if (localWedge < 0) continue;

    const fullWedge = irregularGroupWedgeStart + localWedge;
    const runtimeIrregularInfo = runtimeWeightInflData.m_runtimeIrregularInfo[fullWedge];

    let expectedNumPts = 1 + 6 * runtimeIrregularInfo.m_valence;
    if (runtimeIrregularInfo.m_isBorder) expectedNumPts += 2;

    const framePtsOffset = framePtsOffsetPerGroup[localWedge];
    if (runtimeIrregularInfo.m_pointCount !== expectedNumPts) {
      throw new Error('irregularWedgeInitDecoderVec3: m_pointCount ' +
                      runtimeIrregularInfo.m_pointCount + ' != ' + expectedNumPts);
    }

    const isThisThreadBorderCorner = (runtimeIrregularInfo.m_isBorder && (threadCorner === runtimeIrregularInfo.m_valence));

    if (threadCorner === 0) {
      const val = fetchSrcIrregularVal(runtimeWeightInflData, frameRuntimePts, framePtsOffset, 0);
      v3Set(wedgeDecoder.m_dataCenter[0], localWedge, val);
    }

    if (!isThisThreadBorderCorner) {
      // Standard corner: 6 points per thread.
      for (let k = 0; k < 6; k++) {
        const val = fetchSrcIrregularVal(runtimeWeightInflData, frameRuntimePts, framePtsOffset, 1 + 6 * threadCorner + k);
        v3Set(wedgeDecoder.m_dataInner[0], 6 * threadId + k, val);
      }
    } else {
      // Phantom border corner: fetch only 0/1 -> slots 0/3; sentinel
      // the rest (amolite stores -13/-14/-23/-24, we mirror).
      const val0 = fetchSrcIrregularVal(runtimeWeightInflData, frameRuntimePts, framePtsOffset, 1 + 6 * threadCorner + 0);
      const val1 = fetchSrcIrregularVal(runtimeWeightInflData, frameRuntimePts, framePtsOffset, 1 + 6 * threadCorner + 1);
      v3Set(wedgeDecoder.m_dataInner[0], 6 * threadId + 0, val0);
      v3Set(wedgeDecoder.m_dataInner[0], 6 * threadId + 1, [-13, -13, -13]);
      v3Set(wedgeDecoder.m_dataInner[0], 6 * threadId + 2, [-14, -14, -14]);
      v3Set(wedgeDecoder.m_dataInner[0], 6 * threadId + 3, val1);
      v3Set(wedgeDecoder.m_dataInner[0], 6 * threadId + 4, [-23, -23, -23]);
      v3Set(wedgeDecoder.m_dataInner[0], 6 * threadId + 5, [-24, -24, -24]);
    }
  }
}

// Port of CatmullClarkPatches::IrregularWedgeRunIteration (DcciSubdiv.cpp:7459).
// Four sequential passes (face / edge / vertex / center). No dead-slot
// guard needed: all accesses are on fixed-size arrays so dead slots
// hit UB-but-tolerated values that no live thread reads back.
function irregularWedgeRunIteration(wedgeDecoder, iteration) {
  const prevIter = wedgeDecoder.m_lastDataStartIndex;
  const currIter = (wedgeDecoder.m_lastDataStartIndex + 1) % 2;

  // Reset target slots to debugging sentinels (mirror amolite).
  for (let i = 0; i < IRREGULAR_GROUP_SIZE; i++) {
    if (i < IRREGULAR_GROUP_SIZE / 2) {
      v3Set(wedgeDecoder.m_dataCenter[currIter], i, [-1000, -1000, -1000]);
    }
    for (let k = 0; k < 6; k++) {
      v3Set(wedgeDecoder.m_dataInner[currIter], 6 * i + k, [-2000, -2000, -2000]);
      v3Set(wedgeDecoder.m_dataOuter,             6 * i + k, [-3000, -3000, -3000]);
    }
  }

  // Pass 1: face points (f11, f13, f31, f33).
  for (let threadId = 0; threadId < IRREGULAR_GROUP_SIZE; threadId++) {
    const localWedge = wedgeDecoder.m_irregularWedgeIndex[threadId];
    const threadCorner = wedgeDecoder.m_irregularCornerIndex[threadId];
    const valence  = wedgeDecoder.m_wedgeValence[threadId];
    const isBorder = !!wedgeDecoder.m_wedgeIsBorder[threadId];
    const numThreadsForWedge = isBorder ? (valence + 1) : valence;
    const currCorner = threadCorner;
    const nextCorner = (threadCorner + 1) % numThreadsForWedge;
    const firstThreadForWedge = threadId - threadCorner;

    const p00 = v3Get(wedgeDecoder.m_dataCenter[prevIter], localWedge);
    const p02 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + nextCorner) + 0);
    const p04 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + nextCorner) + 3);
    const p20 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + currCorner) + 0);
    const p22 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + currCorner) + 1);
    const p24 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + currCorner) + 2);
    const p40 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + currCorner) + 3);
    const p42 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + currCorner) + 4);
    const p44 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + currCorner) + 5);

    const f11 = v3Scale(v3AddN(p00, p02, p20, p22), 0.25);
    const f13 = v3Scale(v3AddN(p02, p04, p22, p24), 0.25);
    const f31 = v3Scale(v3AddN(p20, p22, p40, p42), 0.25);
    const f33 = v3Scale(v3AddN(p22, p24, p42, p44), 0.25);

    v3Set(wedgeDecoder.m_dataInner[currIter], 6 * (firstThreadForWedge + currCorner) + 1, f11);
    v3Set(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + currCorner) + 5, f13);
    v3Set(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + currCorner) + 1, f31);
    v3Set(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + currCorner) + 3, f33);
  }

  // Pass 2: edge points (e10, e12, e21, e23, e30, e32).
  for (let threadId = 0; threadId < IRREGULAR_GROUP_SIZE; threadId++) {
    const localWedge = wedgeDecoder.m_irregularWedgeIndex[threadId];
    const threadCorner = wedgeDecoder.m_irregularCornerIndex[threadId];
    const valence  = wedgeDecoder.m_wedgeValence[threadId];
    const isBorder = !!wedgeDecoder.m_wedgeIsBorder[threadId];
    const numThreadsForWedge = isBorder ? (valence + 1) : valence;
    const currCorner = threadCorner;
    const nextCorner = (threadCorner + 1) % numThreadsForWedge;
    const prevCorner = (threadCorner + (numThreadsForWedge - 1)) % numThreadsForWedge;
    const firstThreadForWedge = threadId - threadCorner;

    const p00 = v3Get(wedgeDecoder.m_dataCenter[prevIter], localWedge);
    const p02 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + nextCorner) + 0);
    const p04 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + nextCorner) + 3);
    const p20 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + currCorner) + 0);
    const p22 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + currCorner) + 1);
    const p24 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + currCorner) + 2);
    const p40 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + currCorner) + 3);
    const p42 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + currCorner) + 4);
    const p44 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + currCorner) + 5);

    const f11 = v3Get(wedgeDecoder.m_dataInner[currIter], 6 * (firstThreadForWedge + currCorner) + 1);
    const f13 = v3Get(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + currCorner) + 5);
    const f31 = v3Get(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + currCorner) + 1);
    const f33 = v3Get(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + currCorner) + 3);

    const edgeBaseSharpness   = wedgeDecoder.m_irregularSharpness[firstThreadForWedge + currCorner];
    const edgeCurrSharpnessT  = saturateF((edgeBaseSharpness < 10.0) ? (edgeBaseSharpness - iteration) : 10.0);
    const isEdgeBorder        = (isBorder && (threadCorner === 0 || threadCorner === valence));

    let e10, e30;
    const e10_sharp = v3Scale(v3Add(p00, p20), 0.5);
    const e30_sharp = v3Scale(v3Add(p20, p40), 0.5);

    if (isEdgeBorder) {
      e10 = e10_sharp;
      e30 = e30_sharp;
    } else {
      const f1_n1 = v3Get(wedgeDecoder.m_dataInner[currIter], 6 * (firstThreadForWedge + prevCorner) + 1);
      const f3_n1 = v3Get(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + prevCorner) + 5);

      const e10_smooth = v3Scale(v3AddN(p00, p20, f1_n1, f11), 0.25);
      const e30_smooth = v3Scale(v3AddN(p20, p40, f3_n1, f31), 0.25);

      e10 = v3Lerp(e10_smooth, e10_sharp, edgeCurrSharpnessT);
      e30 = v3Lerp(e30_smooth, e30_sharp, edgeCurrSharpnessT);
    }

    const e12 = v3Scale(v3AddN(f11, f13, p02, p22), 0.25);
    const e32 = v3Scale(v3AddN(f31, f33, p22, p42), 0.25);
    const e21 = v3Scale(v3AddN(p20, p22, f11, f31), 0.25);
    const e23 = v3Scale(v3AddN(p22, p24, f13, f33), 0.25);

    v3Set(wedgeDecoder.m_dataInner[currIter], 6 * (firstThreadForWedge + currCorner) + 0, e10);
    v3Set(wedgeDecoder.m_dataInner[currIter], 6 * (firstThreadForWedge + currCorner) + 2, e12);
    v3Set(wedgeDecoder.m_dataInner[currIter], 6 * (firstThreadForWedge + currCorner) + 4, e21);
    v3Set(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + currCorner) + 4, e23);
    v3Set(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + currCorner) + 0, e30);
    v3Set(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + currCorner) + 2, e32);
  }

  // Pass 3: vertex points (v20, v22).
  for (let threadId = 0; threadId < IRREGULAR_GROUP_SIZE; threadId++) {
    const localWedge = wedgeDecoder.m_irregularWedgeIndex[threadId];
    const threadCorner = wedgeDecoder.m_irregularCornerIndex[threadId];
    const valence  = wedgeDecoder.m_wedgeValence[threadId];
    const isBorder = !!wedgeDecoder.m_wedgeIsBorder[threadId];
    const numThreadsForWedge = isBorder ? (valence + 1) : valence;
    const currCorner = threadCorner;
    const nextCorner = (threadCorner + 1) % numThreadsForWedge;
    const prevCorner = (threadCorner + (numThreadsForWedge - 1)) % numThreadsForWedge;
    const firstThreadForWedge = threadId - threadCorner;

    const p00 = v3Get(wedgeDecoder.m_dataCenter[prevIter], localWedge);
    const p02 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + nextCorner) + 0);
    const p04 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + nextCorner) + 3);
    const p2_n2 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + prevCorner) + 1);

    const p20 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + currCorner) + 0);
    const p22 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + currCorner) + 1);
    const p24 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + currCorner) + 2);
    const p40 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + currCorner) + 3);
    const p42 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + currCorner) + 4);
    const p44 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + currCorner) + 5);

    const f11 = v3Get(wedgeDecoder.m_dataInner[currIter], 6 * (firstThreadForWedge + currCorner) + 1);
    const f13 = v3Get(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + currCorner) + 5);
    const f31 = v3Get(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + currCorner) + 1);
    const f33 = v3Get(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + currCorner) + 3);

    const edgeBaseSharpness  = wedgeDecoder.m_irregularSharpness[firstThreadForWedge + currCorner];
    const edgeCurrSharpnessT = saturateF((edgeBaseSharpness < 10.0) ? (edgeBaseSharpness - iteration) : 10.0);
    const isEdgeBorder = (isBorder && (threadCorner === 0 || threadCorner === valence));

    let v20;
    // amolite comment: "oops, need to use previous edge vertices"
    // -- uses p00/p40/p20 (previous-iteration) for v20_sharp, not e10/e30/p20.
    const v20_sharp = subdivVertexSharpRuleVec3(p00, p40, p20);
    const inv16 = 0.0625;

    if (isEdgeBorder) {
      v20 = v20_sharp;
    } else {
      const f1_n1 = v3Get(wedgeDecoder.m_dataInner[currIter], 6 * (firstThreadForWedge + prevCorner) + 1);
      const e2_n1 = v3Get(wedgeDecoder.m_dataInner[currIter], 6 * (firstThreadForWedge + prevCorner) + 2);
      const f3_n1 = v3Get(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + prevCorner) + 5);
      void e2_n1;

      // amolite: 0.5*p20 + inv16*(p00+p40+p2_n2+p22) + inv16*(f1_n1+f3_n1+f11+f31).
      const v20_smooth = v3AddN(
        v3Scale(p20, 0.5),
        v3Scale(v3AddN(p00, p40, p2_n2, p22), inv16),
        v3Scale(v3AddN(f1_n1, f3_n1, f11, f31), inv16)
      );
      v20 = v3Lerp(v20_smooth, v20_sharp, edgeCurrSharpnessT);
    }

    // amolite: v22 = 0.5*p22 + inv16*(p20+p24+p02+p42) + inv16*(f11+f31+f13+f33).
    const v22 = v3AddN(
      v3Scale(p22, 0.5),
      v3Scale(v3AddN(p20, p24, p02, p42), inv16),
      v3Scale(v3AddN(f11, f31, f13, f33), inv16)
    );

    v3Set(wedgeDecoder.m_dataInner[currIter], 6 * (firstThreadForWedge + currCorner) + 3, v20);
    v3Set(wedgeDecoder.m_dataInner[currIter], 6 * (firstThreadForWedge + currCorner) + 5, v22);
  }

  // Pass 4: center vertex (only threadCorner == 0 per wedge).
  for (let threadId = 0; threadId < IRREGULAR_GROUP_SIZE; threadId++) {
    const localWedge   = wedgeDecoder.m_irregularWedgeIndex[threadId];
    const threadCorner = wedgeDecoder.m_irregularCornerIndex[threadId];
    const valence      = wedgeDecoder.m_wedgeValence[threadId];
    const firstThreadForWedge = threadId - threadCorner;

    if (threadCorner !== 0) continue;
    if (localWedge < 0)     continue;

    const centerVertexSharpness  = wedgeDecoder.m_wedgeCenterSharpness[threadId];
    const secondHighestSharpness = wedgeDecoder.m_wedgeSecondHighestSharpness[threadId];
    const thirdHighestSharpness  = wedgeDecoder.m_wedgeThirdHighestSharpness[threadId];
    const edge0 = wedgeDecoder.m_wedgeCenterSemisharpEdge0[threadId];
    const edge1 = wedgeDecoder.m_wedgeCenterSemisharpEdge1[threadId];

    const ws = getCenterVertexEdgeSharpnessInfoHelperPerFrame(
      secondHighestSharpness, thirdHighestSharpness, centerVertexSharpness, iteration);

    const centerVal = v3Get(wedgeDecoder.m_dataCenter[prevIter], localWedge);

    let sumEdge = [0, 0, 0];
    let sumFace = [0, 0, 0];
    for (let cornerIter = 0; cornerIter < valence; cornerIter++) {
      const currEdge = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + cornerIter) + 0);
      const currFace = v3Get(wedgeDecoder.m_dataInner[currIter], 6 * (firstThreadForWedge + cornerIter) + 1);
      sumEdge = v3Add(sumEdge, currEdge);
      sumFace = v3Add(sumFace, currFace);
    }
    const scaleCenter   = (valence - 2) / valence;
    const scaleEdgeFace = 1.0 / (valence * valence);
    const dst_smooth = v3AddN(
      v3Scale(centerVal, scaleCenter),
      v3Scale(sumEdge,    scaleEdgeFace),
      v3Scale(sumFace,    scaleEdgeFace)
    );

    const valEdge0 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + edge0) + 0);
    const valEdge1 = v3Get(wedgeDecoder.m_dataInner[prevIter], 6 * (firstThreadForWedge + edge1) + 0);
    const dst_edge = subdivVertexSharpRuleVec3(valEdge0, valEdge1, centerVal);

    const dst_corner = centerVal;

    const dst = v3AddN(
      v3Scale(dst_corner, ws.dstWeightCorner),
      v3Scale(dst_edge,   ws.dstWeightEdge),
      v3Scale(dst_smooth, ws.dstWeightSmooth)
    );

    v3Set(wedgeDecoder.m_dataCenter[currIter], localWedge, dst);
  }

  wedgeDecoder.m_lastDataStartIndex = (wedgeDecoder.m_lastDataStartIndex + 1) % 2;
}

// Port of CatmullClarkPatches::IrregularWedgeSavePointsVec3
// (DcciSubdiv.cpp:7839). Saves the current iteration's per-wedge grid
// points into a destination buffer for the patch evaluator (Phase 4)
// to consume. Applies the AMOLITE_DEFERRED_FIXES.md Fix 2 dead-slot
// guard.
function irregularWedgeSavePointsVec3(wedgeDecoder, dstWedgePts, dstWedgeOffsets,
                                      irregularGroupWedgeStart, iteration) {
  const currIter = wedgeDecoder.m_lastDataStartIndex;

  for (let threadId = 0; threadId < IRREGULAR_GROUP_SIZE; threadId++) {
    const localWedge = wedgeDecoder.m_irregularWedgeIndex[threadId];

    // Deferred fix (AMOLITE_DEFERRED_FIXES.md Fix 2).
    if (localWedge < 0) continue;

    const fullWedge = irregularGroupWedgeStart + localWedge;
    const threadCorner = wedgeDecoder.m_irregularCornerIndex[threadId];
    const valence  = wedgeDecoder.m_wedgeValence[threadId];
    const isBorder = !!wedgeDecoder.m_wedgeIsBorder[threadId];
    const currCorner = threadCorner;
    const firstThreadForWedge = threadId - threadCorner;

    const numPointsPerSlice = calcNumDstPointsPerSliceForWedge(valence, isBorder);
    const startIndexForWedge = dstWedgeOffsets[fullWedge];
    const offsetForSlice = numPointsPerSlice * iteration;
    const dstFullOffset = startIndexForWedge + offsetForSlice;

    if (threadCorner === 0) {
      const center = v3Get(wedgeDecoder.m_dataCenter[currIter], localWedge);
      v3Set(dstWedgePts, dstFullOffset + 0, center);
    }

    const v00 = v3Get(wedgeDecoder.m_dataInner[currIter], 6 * (firstThreadForWedge + currCorner) + 0);
    const v01 = v3Get(wedgeDecoder.m_dataInner[currIter], 6 * (firstThreadForWedge + currCorner) + 1);
    const v02 = v3Get(wedgeDecoder.m_dataInner[currIter], 6 * (firstThreadForWedge + currCorner) + 2);
    const v03 = v3Get(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + currCorner) + 5);
    const v10 = v3Get(wedgeDecoder.m_dataInner[currIter], 6 * (firstThreadForWedge + currCorner) + 3);
    const v11 = v3Get(wedgeDecoder.m_dataInner[currIter], 6 * (firstThreadForWedge + currCorner) + 4);
    const v12 = v3Get(wedgeDecoder.m_dataInner[currIter], 6 * (firstThreadForWedge + currCorner) + 5);
    const v13 = v3Get(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + currCorner) + 4);
    const v20 = v3Get(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + currCorner) + 0);
    const v21 = v3Get(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + currCorner) + 1);
    const v22 = v3Get(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + currCorner) + 2);
    const v23 = v3Get(wedgeDecoder.m_dataOuter,           6 * (firstThreadForWedge + currCorner) + 3);

    if (isBorder && threadCorner === valence) {
      // Phantom border corner: only 3 points (v00, v10, v20).
      v3Set(dstWedgePts, dstFullOffset + 1 + 12 * currCorner + 0, v00);
      v3Set(dstWedgePts, dstFullOffset + 1 + 12 * currCorner + 1, v10);
      v3Set(dstWedgePts, dstFullOffset + 1 + 12 * currCorner + 2, v20);
    } else {
      v3Set(dstWedgePts, dstFullOffset + 1 + 12 * currCorner +  0, v00);
      v3Set(dstWedgePts, dstFullOffset + 1 + 12 * currCorner +  1, v01);
      v3Set(dstWedgePts, dstFullOffset + 1 + 12 * currCorner +  2, v02);
      v3Set(dstWedgePts, dstFullOffset + 1 + 12 * currCorner +  3, v03);
      v3Set(dstWedgePts, dstFullOffset + 1 + 12 * currCorner +  4, v10);
      v3Set(dstWedgePts, dstFullOffset + 1 + 12 * currCorner +  5, v11);
      v3Set(dstWedgePts, dstFullOffset + 1 + 12 * currCorner +  6, v12);
      v3Set(dstWedgePts, dstFullOffset + 1 + 12 * currCorner +  7, v13);
      v3Set(dstWedgePts, dstFullOffset + 1 + 12 * currCorner +  8, v20);
      v3Set(dstWedgePts, dstFullOffset + 1 + 12 * currCorner +  9, v21);
      v3Set(dstWedgePts, dstFullOffset + 1 + 12 * currCorner + 10, v22);
      v3Set(dstWedgePts, dstFullOffset + 1 + 12 * currCorner + 11, v23);
    }
  }
}

if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    makePolyCorner,
    makeEdgeLookup, edgeLookupReset, edgeLookupFetchEdge, edgeLookupInsertEdge,
    edgeLookupGetNumFacesForEdge, edgeLookupGetFaceForEdge,
    edgeLookupGetEdgeIsHardNormal, edgeLookupGetEdgeIsHardTangent,
    makeVertexAdjData, vertexAdjDataReset,
    makeHardEdgeSection,
    makeVertexWedgeInfo, makeWedgeCornerInfo,
    makeVertexWedgeData, vertexWedgeDataReset,
    makeCatmullClarkTopoHelper, catmullClarkTopoHelperReset,
    calcPrefixSumVec,
    buildEdgeLookup,
    buildFaceAdjData,
    buildVertAdjData,
    // Phase 1b.4
    getFaceInCornerVec, findMatchingBackwardsEdgeInVec, getEdgeInCornerVec,
    chooseBestStartWedgeCorner, getOutgoingEdge, getIncomingEdge,
    buildWedgeData,
    detectBorders,
    // Phase 1b.5
    decompressCreaseSharpness,
    applyVertexAndBorderSharpness,
    // Phase 1b.6
    makeVertexInfo, makeCatmullClarkPatches,
    buildCalcVertexInfo,
    // Phase 1b.7
    kSubdivPolyType_QuadRegular, kSubdivPolyType_QuadIrregular1,
    kSubdivPolyType_QuadIrregular2, kSubdivPolyType_Triangle,
    getPolySubdivLevel, getPolyNumRegions,
    makePolyInfo,
    buildCalcPolyInfo,
    SUBDIV_MAX_SHARPNESS,
    // Phase 1b.8
    applySwapToVector,
    buildHardTangentEdgesFromMesh,
    makeDcciMeshSubdivTopo,
    makeSubdivTopoFromMeshVtx,
    // Phase 1b.9
    duplicateSameIdsOnDifferentPositions,
    prepMeshForCatmullClark,
    // Phase 1b.10
    makeFlatWeightVertex, makeFlatWeightVertexValId, flatWeightVertexGetNum,
    flatWeightVertexAddDirect, flatWeightVertexMulDirect,
    flatWeightVertexNormalize,
    Vec3Ops, FlatWeightVertexOps,
    buildIndexList,
    applySubdivisionToFacePts,
    applySubdivisionToEdgePtsSemiSharp,
    getCenterVertexEdgeSharpnessInfoHelper,
    applySubdivisionToVertPtsSemiSharp,
    applySubdivisionToPts,
    subdivideEdgeSharpness,
    applyCatmullClarkToWeights,
    // Phase 1b.11
    convolveWeights,
    applyWeightsToPoints,
    // Phase 1b.12a
    makeRegionInfo, regionInfoEquals,
    kRegionScale_00, kRegionScale_01, kRegionScale_10, kRegionScale_11,
    kRegionScale_Full, kRegionScale_Num,
    makeRegionGrid, regionGridIsRegionSet, regionGridGetRegionOffset,
    intVec2, countBits,
    getQuadCornerDirU, getQuadCornerDirV, getQuadCornerPosUv,
    gridIndexFromCorner, getQuadCornerSudivRegionUv,
    findCornerForPoly,
    buildCalcQuadRegionData,
    // Phase 1b.12b / 12c
    makeIrregularWedgeInfo,
    buildVertexIrregularData,
    buildTriRegionData,
    // Phase 1b.12d
    polyCornerIsValid, polyCornerIsSame,
    kCpType_Invalid, kCpType_Direct, kCpType_Border,
    kCpType_ConcaveCorner, kCpType_ConvexCorner, kCpType_Num,
    makeControlPoint, controlPointMakeInvalid, controlPointMakeDirect,
    controlPointMakeDirectOrInvalid, controlPointMakeBorder,
    controlPointMakeConcaveCorner, controlPointMakeConvexCorner,
    controlPointGetType,
    fetchEdgeFromPoly, fetchCornerOpp, fetchCornerPrev, fetchCornerNext,
    fetchCornerNeighborCW, fetchCornerNeighborCCW, fetchCornerNeighborOppositeIfRegular,
    getPolyCornerVtx, getPolyCornerVtxControlPoint,
    getPolyCornerBorderControlPoint, getPolyCornerBorderControlPointVtx,
    getPolyCornerCornerControlPoint, getPolyCornerCornerControlPointVtx,
    fetchIndex_1_V, fetchIndex_1_E, fetchIndex_1_F,
    fetchIndex_2_VV, fetchIndex_2_EV, fetchIndex_2_EE,
    fetchIndex_2_EF, fetchIndex_2_FF, fetchIndex_2_FV,
    collectPointsForPatches,
    // Phase 1b.12e
    collectPointsForIrregular,
    // Phase 1b.12f
    makeRuntimeWeightInflData, makeControlPointRedirect,
    interpolateControlPointFlatWeight,
    makeFullCpItem, fullCpItemMakeFromVertex, fullCpItemMakeFromControlPoint,
    fullCpItemIsSameCp, fullCpItemCompare,
    buildRuntimePoints,
    // Phase 1b.12g
    IRREGULAR_GROUP_SIZE,
    makeRuntimeIregularInfo, runtimeIregularInfoGetNumThreadsForGroup,
    buildRuntimeIrregularInfo,
    // Phase 1b.13a
    makeMat44, mat44InitIdentity, mat44At, mat44Set,
    mat44GetTranspose, mat44MulMatVec, mat44ScaledSum,
    saturateF, safeInv, calcFrac,
    bsplineUtilGetMInfinity,
    bsplineUtilEvalBSplineBasis, bsplineUtilEvalBSplineBasisDeriv,
    bsplineUtilCalcMSharpness,
    bsplineUtilCalcSharpnessMatricesAndEndpoints,
    bsplineUtilCalcBasisAndDerivFromSharpnessMatrices,
    bsplineUtilCalcBasisAndDerivForSharpnessRef,
    bsplineUtilEvalBicubicBSplineSharpnessFlatWeights,
    // Phase 1b.13b
    flatWeightVertexGetNewPairSizeFromIndices,
    flatWeightVertexNormalizeSignedRange,
    flatWeightVertexCompactInfluenceList,
    flatWeightVertexMergeIndexList,
    // Phase 1b.13c
    runtimeWeightInflDataFetchRegularIndex,
    runtimeWeightInflDataFetchIrregularIndex,
    runtimeWeightInflDataFetchRegularFlatVertex,
    // Phase 1b.13d
    subdivVertexSharpRuleFloat,
    getCenterVertexEdgeSharpnessInfoHelperIrregular,
    makeIrregularWedgeLimitFinder,
    initLimitIrregularWedgeDecoderPerThreadData,
    limitIrregularWedgeInitDecoderSingleWeight,
    limitIrregularWedgeRunIteration,
    limitIrregularWedgeCalcLimitP,
    limitIrregularWedgeCalcLimitT,
    calcAn,
    calculateLimitWeights,
    // Phase 1b.13e
    makeVertexLimitInfo, getPolyDimControlPoints,
    buildLimitPointsInfo,
    // Phase 3a
    calcNumDstPointsPerSliceForWedge, calcNumSlicesForWedge,
    calcNumDstPointsForWedge,
    makeIrregularWedgeDecoder,
    subdivVertexSharpRuleVec3,
    getCenterVertexEdgeSharpnessInfoHelperPerFrame,
    initIrregularWedgeDecoderPerThreadData,
    fetchSrcIrregularVal,
    irregularWedgeInitDecoderVec3,
    irregularWedgeRunIteration,
    irregularWedgeSavePointsVec3,
  };
}
