@doki-land/live2d-renderer 0.0.19 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -322,12 +322,112 @@ function fitClippingContexts(contexts, byIndex, margin = 0.05) {
322
322
  )
323
323
  }));
324
324
  }
325
- function maskLayoutVec4(layout) {
326
- return new Float32Array([layout.x, layout.y, layout.width, layout.height]);
325
+ function maskLayoutVec4(layout, into) {
326
+ const out = into && into.length >= 4 ? into : new Float32Array(4);
327
+ out[0] = layout.x;
328
+ out[1] = layout.y;
329
+ out[2] = layout.width;
330
+ out[3] = layout.height;
331
+ return out;
332
+ }
333
+ function maskChannelVec4(flag, into) {
334
+ const out = into && into.length >= 4 ? into : new Float32Array(4);
335
+ out[0] = flag[0];
336
+ out[1] = flag[1];
337
+ out[2] = flag[2];
338
+ out[3] = flag[3];
339
+ return out;
340
+ }
341
+
342
+ // src/render/clipping-plan.ts
343
+ function clippingTopologyKey(drawables) {
344
+ let key = `n:${drawables.length}`;
345
+ for (let i = 0; i < drawables.length; i++) {
346
+ const d = drawables[i];
347
+ key += `|${d.index}:${d.invertedMask ? 1 : 0}:`;
348
+ const masks = d.maskIndices;
349
+ for (let j = 0; j < masks.length; j++) {
350
+ key += `${masks[j]},`;
351
+ }
352
+ }
353
+ return key;
354
+ }
355
+ function atlasOptionsKey(options) {
356
+ return `${options.mode ?? "rgba"}:${options.inset ?? ""}:${options.renderTextureCount ?? ""}`;
327
357
  }
328
- function maskChannelVec4(flag) {
329
- return new Float32Array([flag[0], flag[1], flag[2], flag[3]]);
358
+ function fitClippingContextsInPlace(contexts, byIndex, margin = 0.05) {
359
+ for (let i = 0; i < contexts.length; i++) {
360
+ const ctx = contexts[i];
361
+ ctx.modelBounds = calcClippedDrawableBounds(
362
+ ctx.clippedIndices,
363
+ byIndex,
364
+ margin
365
+ );
366
+ }
330
367
  }
368
+ var ResidentClippingPlan = class {
369
+ #cacheKey = "";
370
+ #contexts = [];
371
+ #clipped = /* @__PURE__ */ new Set();
372
+ #maskOnly = /* @__PURE__ */ new Set();
373
+ #byIndex = /* @__PURE__ */ new Map();
374
+ /**
375
+ * Returns a partition whose `contexts` array identity is stable across
376
+ * frames with the same topology. `modelBounds` are refreshed in place.
377
+ */
378
+ resolve(drawables, options = {}) {
379
+ const nextKey = `${clippingTopologyKey(drawables)}|${atlasOptionsKey(options)}`;
380
+ if (nextKey !== this.#cacheKey) {
381
+ const partitioned = partitionForClipping(drawables, options);
382
+ this.#contexts = partitioned.contexts.map((ctx) => ({
383
+ key: ctx.key,
384
+ maskIndices: ctx.maskIndices,
385
+ clippedIndices: ctx.clippedIndices,
386
+ invertedMask: ctx.invertedMask,
387
+ layout: ctx.layout,
388
+ channelIndex: ctx.channelIndex,
389
+ channelFlag: ctx.channelFlag,
390
+ bufferIndex: ctx.bufferIndex,
391
+ modelBounds: { ...ctx.modelBounds }
392
+ }));
393
+ this.#clipped = new Set(partitioned.clipped);
394
+ this.#maskOnly = new Set(partitioned.maskOnly);
395
+ this.#cacheKey = nextKey;
396
+ }
397
+ this.#byIndex.clear();
398
+ for (let i = 0; i < drawables.length; i++) {
399
+ const d = drawables[i];
400
+ if ("vertexPositions" in d) {
401
+ this.#byIndex.set(d.index, d);
402
+ }
403
+ }
404
+ fitClippingContextsInPlace(this.#contexts, this.#byIndex);
405
+ return {
406
+ contexts: this.#contexts,
407
+ clipped: this.#clipped,
408
+ maskOnly: this.#maskOnly
409
+ };
410
+ }
411
+ /** Convenience for full drawable meshes (GPU backends). */
412
+ resolveMeshes(drawables, options = {}) {
413
+ return this.resolve(drawables, options);
414
+ }
415
+ /** True when the last {@link resolve} reused the cached atlas layout. */
416
+ get hasPlan() {
417
+ return this.#cacheKey.length > 0;
418
+ }
419
+ /** Exposed for tests — context array identity while topology holds. */
420
+ get residentContexts() {
421
+ return this.#contexts;
422
+ }
423
+ clear() {
424
+ this.#cacheKey = "";
425
+ this.#contexts = [];
426
+ this.#clipped = /* @__PURE__ */ new Set();
427
+ this.#maskOnly = /* @__PURE__ */ new Set();
428
+ this.#byIndex.clear();
429
+ }
430
+ };
331
431
 
332
432
  // src/render/coords.ts
333
433
  function modelYUpToCanvasPixelY(modelY, canvasHeight) {
@@ -443,6 +543,7 @@ var Canvas2DModelDrawPass = class {
443
543
  #ctx;
444
544
  #options;
445
545
  #textures = [];
546
+ #clipPlan = new ResidentClippingPlan();
446
547
  constructor(ctx, options) {
447
548
  this.#ctx = ctx;
448
549
  this.#options = options;
@@ -459,10 +560,10 @@ var Canvas2DModelDrawPass = class {
459
560
  const h = ctx.canvas.height;
460
561
  const byIndex = /* @__PURE__ */ new Map();
461
562
  for (const d of drawables) byIndex.set(d.index, d);
462
- const partitioned = partitionForClipping(drawables, {
563
+ const partitioned = this.#clipPlan.resolveMeshes(drawables, {
463
564
  mode: "uv-grid"
464
565
  });
465
- const contexts = fitClippingContexts(partitioned.contexts, byIndex);
566
+ const contexts = partitioned.contexts;
466
567
  const { maskOnly } = partitioned;
467
568
  if (contexts.length === 0) {
468
569
  for (const d of drawables) {
@@ -627,6 +728,7 @@ var Canvas2DModelDrawPass = class {
627
728
  }
628
729
  destroy() {
629
730
  this.#textures = [];
731
+ this.#clipPlan.clear();
630
732
  }
631
733
  };
632
734
  var Canvas2DRendererImpl = class {
@@ -683,6 +785,25 @@ function isCanvas2DAvailable() {
683
785
  return !!canvas.getContext("2d");
684
786
  }
685
787
 
788
+ // src/render/mesh-interleave.ts
789
+ function interleavePosUv(positions, uvs, into) {
790
+ const n = Math.floor(positions.length / 2);
791
+ const need = n * 4;
792
+ const out = into && into.length >= need ? into : new Float32Array(need);
793
+ for (let i = 0; i < n; i++) {
794
+ const o = i * 4;
795
+ const p = i * 2;
796
+ out[o] = positions[p];
797
+ out[o + 1] = positions[p + 1];
798
+ out[o + 2] = uvs[p] ?? 0;
799
+ out[o + 3] = uvs[p + 1] ?? 0;
800
+ }
801
+ return out;
802
+ }
803
+ function interleaveByteLength(positions) {
804
+ return Math.floor(positions.length / 2) * 16;
805
+ }
806
+
686
807
  // src/backends/webgl2.ts
687
808
  var VS = `#version 300 es
688
809
  layout(location = 0) in vec2 a_pos;
@@ -791,19 +912,6 @@ function linkProgram(gl, vsSource, fsSource) {
791
912
  }
792
913
  return program;
793
914
  }
794
- function interleavePosUv(positions, uvs) {
795
- const n = Math.floor(positions.length / 2);
796
- const out = new Float32Array(n * 4);
797
- for (let i = 0; i < n; i++) {
798
- const o = i * 4;
799
- const p = i * 2;
800
- out[o] = positions[p];
801
- out[o + 1] = positions[p + 1];
802
- out[o + 2] = uvs[p] ?? 0;
803
- out[o + 3] = uvs[p + 1] ?? 0;
804
- }
805
- return out;
806
- }
807
915
  function requireUniform(gl, program, name) {
808
916
  const loc = gl.getUniformLocation(program, name);
809
917
  if (!loc) {
@@ -841,6 +949,13 @@ var WebGl2ModelDrawPass = class {
841
949
  #maskTex = null;
842
950
  #maskW = 0;
843
951
  #maskH = 0;
952
+ #clipPlan = new ResidentClippingPlan();
953
+ #interleaved = new Float32Array(64);
954
+ #indicesRef = null;
955
+ #layoutScratch = new Float32Array(4);
956
+ #boundsScratch = new Float32Array(4);
957
+ #channelScratch = new Float32Array(4);
958
+ #defaultChannel = new Float32Array([0, 0, 0, 1]);
844
959
  constructor(gl) {
845
960
  this.#gl = gl;
846
961
  this.#program = linkProgram(gl, VS, FS);
@@ -1012,13 +1127,25 @@ var WebGl2ModelDrawPass = class {
1012
1127
  }
1013
1128
  gl.bindTexture(gl.TEXTURE_2D, null);
1014
1129
  }
1015
- #uploadMesh(d) {
1130
+ #uploadMesh(d, uploadIndices = true) {
1016
1131
  const gl = this.#gl;
1017
- const interleaved = interleavePosUv(d.vertexPositions, d.uvs);
1132
+ this.#interleaved = interleavePosUv(
1133
+ d.vertexPositions,
1134
+ d.uvs,
1135
+ this.#interleaved
1136
+ );
1137
+ const vBytes = interleaveByteLength(d.vertexPositions);
1018
1138
  gl.bindBuffer(gl.ARRAY_BUFFER, this.#vbo);
1019
- gl.bufferData(gl.ARRAY_BUFFER, interleaved, gl.DYNAMIC_DRAW);
1139
+ gl.bufferData(
1140
+ gl.ARRAY_BUFFER,
1141
+ this.#interleaved.subarray(0, vBytes / 4),
1142
+ gl.DYNAMIC_DRAW
1143
+ );
1020
1144
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.#ibo);
1021
- gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, d.indices, gl.DYNAMIC_DRAW);
1145
+ if (uploadIndices && this.#indicesRef !== d.indices) {
1146
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, d.indices, gl.STATIC_DRAW);
1147
+ this.#indicesRef = d.indices;
1148
+ }
1022
1149
  }
1023
1150
  #drawMeshColor(d, useMask, invertMask, layout, modelBounds, channelFlag) {
1024
1151
  const gl = this.#gl;
@@ -1041,19 +1168,23 @@ var WebGl2ModelDrawPass = class {
1041
1168
  );
1042
1169
  gl.uniform1f(this.#useMaskLoc, useMask ? 1 : 0);
1043
1170
  gl.uniform1f(this.#invertMaskLoc, invertMask ? 1 : 0);
1044
- const layoutVec = maskLayoutVec4(
1045
- layout ?? { x: 0, y: 0, width: 1, height: 1 }
1171
+ gl.uniform4fv(
1172
+ this.#maskLayoutLoc,
1173
+ maskLayoutVec4(
1174
+ layout ?? { x: 0, y: 0, width: 1, height: 1 },
1175
+ this.#layoutScratch
1176
+ )
1046
1177
  );
1047
- gl.uniform4fv(this.#maskLayoutLoc, layoutVec);
1048
1178
  gl.uniform4fv(
1049
1179
  this.#maskBoundsLoc,
1050
1180
  maskLayoutVec4(
1051
- modelBounds ?? { x: -1, y: -1, width: 2, height: 2 }
1181
+ modelBounds ?? { x: -1, y: -1, width: 2, height: 2 },
1182
+ this.#boundsScratch
1052
1183
  )
1053
1184
  );
1054
1185
  gl.uniform4fv(
1055
1186
  this.#channelFlagLoc,
1056
- channelFlag ?? new Float32Array([0, 0, 0, 1])
1187
+ channelFlag ?? this.#defaultChannel
1057
1188
  );
1058
1189
  if (useMask && this.#maskTex) {
1059
1190
  gl.activeTexture(gl.TEXTURE1);
@@ -1072,6 +1203,7 @@ var WebGl2ModelDrawPass = class {
1072
1203
  PREVIEW_STROKE.a * d.opacity
1073
1204
  );
1074
1205
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, lines, gl.DYNAMIC_DRAW);
1206
+ this.#indicesRef = null;
1075
1207
  gl.drawElements(gl.LINES, lines.length, gl.UNSIGNED_SHORT, 0);
1076
1208
  }
1077
1209
  }
@@ -1085,8 +1217,14 @@ var WebGl2ModelDrawPass = class {
1085
1217
  gl.uniform1i(this.#maskTexLoc, 0);
1086
1218
  gl.uniform1f(this.#maskUseTexLoc, gpuTex ? 1 : 0);
1087
1219
  gl.uniform1f(this.#maskOpacityLoc, Math.max(d.opacity, 1));
1088
- gl.uniform4fv(this.#maskWriteLayoutLoc, maskLayoutVec4(layout));
1089
- gl.uniform4fv(this.#maskWriteBoundsLoc, maskLayoutVec4(modelBounds));
1220
+ gl.uniform4fv(
1221
+ this.#maskWriteLayoutLoc,
1222
+ maskLayoutVec4(layout, this.#layoutScratch)
1223
+ );
1224
+ gl.uniform4fv(
1225
+ this.#maskWriteBoundsLoc,
1226
+ maskLayoutVec4(modelBounds, this.#boundsScratch)
1227
+ );
1090
1228
  gl.uniform4fv(this.#maskWriteChannelLoc, channelFlag);
1091
1229
  gl.drawElements(gl.TRIANGLES, d.indices.length, gl.UNSIGNED_SHORT, 0);
1092
1230
  }
@@ -1097,8 +1235,8 @@ var WebGl2ModelDrawPass = class {
1097
1235
  const height = canvas instanceof HTMLCanvasElement ? canvas.height : canvas.height;
1098
1236
  const byIndex = /* @__PURE__ */ new Map();
1099
1237
  for (const d of drawables) byIndex.set(d.index, d);
1100
- const partitioned = partitionForClipping(drawables);
1101
- const contexts = fitClippingContexts(partitioned.contexts, byIndex);
1238
+ const partitioned = this.#clipPlan.resolveMeshes(drawables);
1239
+ const contexts = partitioned.contexts;
1102
1240
  const { maskOnly } = partitioned;
1103
1241
  gl.bindVertexArray(this.#vao);
1104
1242
  if (contexts.length > 0) {
@@ -1113,7 +1251,10 @@ var WebGl2ModelDrawPass = class {
1113
1251
  gl.enable(gl.BLEND);
1114
1252
  gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
1115
1253
  for (const ctx of contexts) {
1116
- const flag = maskChannelVec4(ctx.channelFlag);
1254
+ const flag = maskChannelVec4(
1255
+ ctx.channelFlag,
1256
+ this.#channelScratch
1257
+ );
1117
1258
  for (const mi of ctx.maskIndices) {
1118
1259
  const maskMesh = byIndex.get(mi);
1119
1260
  if (maskMesh) {
@@ -1152,7 +1293,7 @@ var WebGl2ModelDrawPass = class {
1152
1293
  clip.invertedMask,
1153
1294
  clip.layout,
1154
1295
  clip.modelBounds,
1155
- maskChannelVec4(clip.channelFlag)
1296
+ maskChannelVec4(clip.channelFlag, this.#channelScratch)
1156
1297
  );
1157
1298
  } else {
1158
1299
  this.#drawMeshColor(d, false, false, null, null, null);
@@ -1167,6 +1308,8 @@ var WebGl2ModelDrawPass = class {
1167
1308
  destroy() {
1168
1309
  const gl = this.#gl;
1169
1310
  this.#clearGpuTextures();
1311
+ this.#clipPlan.clear();
1312
+ this.#indicesRef = null;
1170
1313
  if (this.#maskTex) gl.deleteTexture(this.#maskTex);
1171
1314
  if (this.#maskFbo) gl.deleteFramebuffer(this.#maskFbo);
1172
1315
  gl.deleteBuffer(this.#vbo);
@@ -1240,6 +1383,155 @@ function isWebGl2Available() {
1240
1383
  return !!gl;
1241
1384
  }
1242
1385
 
1386
+ // src/backends/webgpu-mesh-cache.ts
1387
+ function indexBufferByteSize(byteLength) {
1388
+ return byteLength + 3 & ~3;
1389
+ }
1390
+ function createOrGrowBuffer(device, prev, prevBytes, needBytes, usage) {
1391
+ const bytes = Math.max(4, needBytes);
1392
+ if (prev && prevBytes >= bytes) return { buffer: prev, bytes: prevBytes };
1393
+ prev?.destroy();
1394
+ return {
1395
+ buffer: device.createBuffer({
1396
+ size: bytes,
1397
+ usage
1398
+ }),
1399
+ bytes
1400
+ };
1401
+ }
1402
+ var WebGpuMeshCache = class {
1403
+ #slots = /* @__PURE__ */ new Map();
1404
+ #maskGeneration = 0;
1405
+ bumpMaskGeneration() {
1406
+ this.#maskGeneration += 1;
1407
+ }
1408
+ get maskGeneration() {
1409
+ return this.#maskGeneration;
1410
+ }
1411
+ clear() {
1412
+ for (const slot of this.#slots.values()) {
1413
+ slot.vbo.destroy();
1414
+ slot.ibo.destroy();
1415
+ slot.ubo.destroy();
1416
+ }
1417
+ this.#slots.clear();
1418
+ }
1419
+ /** Drop bind groups that reference model textures (after setTextures). */
1420
+ invalidateTextureBindGroups() {
1421
+ for (const slot of this.#slots.values()) {
1422
+ slot.bgMask = null;
1423
+ slot.bgTextured = null;
1424
+ slot.bgClippedTextured = null;
1425
+ slot.bgSolid = null;
1426
+ slot.bgClippedSolid = null;
1427
+ slot.texIndex = -1;
1428
+ }
1429
+ }
1430
+ ensureSlot(device, drawableIndex) {
1431
+ let slot = this.#slots.get(drawableIndex);
1432
+ if (slot) return slot;
1433
+ const vbo = device.createBuffer({
1434
+ size: 64,
1435
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
1436
+ });
1437
+ const ibo = device.createBuffer({
1438
+ size: 4,
1439
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST
1440
+ });
1441
+ const ubo = device.createBuffer({
1442
+ size: 80,
1443
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
1444
+ });
1445
+ slot = {
1446
+ index: drawableIndex,
1447
+ vbo,
1448
+ vboBytes: 64,
1449
+ interleaved: new Float32Array(16),
1450
+ ibo,
1451
+ iboBytes: 4,
1452
+ indicesRef: null,
1453
+ uvsRef: null,
1454
+ ubo,
1455
+ uboFloats: new Float32Array(20),
1456
+ bgMask: null,
1457
+ bgTextured: null,
1458
+ bgClippedTextured: null,
1459
+ bgSolid: null,
1460
+ bgClippedSolid: null,
1461
+ texIndex: -1,
1462
+ useWhite: false,
1463
+ maskGeneration: -1
1464
+ };
1465
+ this.#slots.set(drawableIndex, slot);
1466
+ return slot;
1467
+ }
1468
+ /**
1469
+ * Upload interleaved pos/uv when positions change every frame; refresh
1470
+ * index buffer only when the `indices` TypedArray identity changes.
1471
+ */
1472
+ uploadMesh(device, slot, positions, uvs, indices) {
1473
+ slot.interleaved = interleavePosUv(positions, uvs, slot.interleaved);
1474
+ const vBytes = interleaveByteLength(positions);
1475
+ const grownV = createOrGrowBuffer(
1476
+ device,
1477
+ slot.vbo,
1478
+ slot.vboBytes,
1479
+ vBytes,
1480
+ GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
1481
+ );
1482
+ slot.vbo = grownV.buffer;
1483
+ slot.vboBytes = grownV.bytes;
1484
+ device.queue.writeBuffer(
1485
+ slot.vbo,
1486
+ 0,
1487
+ slot.interleaved.buffer,
1488
+ slot.interleaved.byteOffset,
1489
+ vBytes
1490
+ );
1491
+ const iBytes = indexBufferByteSize(indices.byteLength);
1492
+ const grownI = createOrGrowBuffer(
1493
+ device,
1494
+ slot.ibo,
1495
+ slot.iboBytes,
1496
+ iBytes,
1497
+ GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST
1498
+ );
1499
+ if (grownI.buffer !== slot.ibo) {
1500
+ slot.indicesRef = null;
1501
+ }
1502
+ slot.ibo = grownI.buffer;
1503
+ slot.iboBytes = grownI.bytes;
1504
+ if (slot.indicesRef !== indices || slot.uvsRef !== uvs) {
1505
+ if (iBytes === indices.byteLength) {
1506
+ device.queue.writeBuffer(
1507
+ slot.ibo,
1508
+ 0,
1509
+ indices.buffer,
1510
+ indices.byteOffset,
1511
+ indices.byteLength
1512
+ );
1513
+ } else {
1514
+ const padded = new Uint16Array(iBytes / 2);
1515
+ padded.set(indices);
1516
+ device.queue.writeBuffer(slot.ibo, 0, padded);
1517
+ }
1518
+ slot.indicesRef = indices;
1519
+ slot.uvsRef = uvs;
1520
+ }
1521
+ }
1522
+ writeUbo(device, slot, floats) {
1523
+ const n = floats.length;
1524
+ for (let i = 0; i < n; i++) slot.uboFloats[i] = floats[i];
1525
+ device.queue.writeBuffer(
1526
+ slot.ubo,
1527
+ 0,
1528
+ slot.uboFloats.buffer,
1529
+ slot.uboFloats.byteOffset,
1530
+ Math.max(16, n * 4 + 15 & ~15)
1531
+ );
1532
+ }
1533
+ };
1534
+
1243
1535
  // src/backends/webgpu.ts
1244
1536
  var SOLID_WGSL = (
1245
1537
  /* wgsl */
@@ -1432,33 +1724,6 @@ var BLEND_MODES = [
1432
1724
  BlendMode.Additive,
1433
1725
  BlendMode.Multiplicative
1434
1726
  ];
1435
- function indexBufferSize(byteLength) {
1436
- return byteLength + 3 & ~3;
1437
- }
1438
- function writeIndexBuffer(device, indices) {
1439
- const size = indexBufferSize(indices.byteLength);
1440
- const ibo = device.createBuffer({
1441
- size: Math.max(4, size),
1442
- usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
1443
- mappedAtCreation: true
1444
- });
1445
- new Uint16Array(ibo.getMappedRange()).set(indices);
1446
- ibo.unmap();
1447
- return ibo;
1448
- }
1449
- function interleavePosUv2(positions, uvs) {
1450
- const n = Math.floor(positions.length / 2);
1451
- const out = new Float32Array(n * 4);
1452
- for (let i = 0; i < n; i++) {
1453
- const o = i * 4;
1454
- const p = i * 2;
1455
- out[o] = positions[p];
1456
- out[o + 1] = positions[p + 1];
1457
- out[o + 2] = uvs[p] ?? 0;
1458
- out[o + 3] = uvs[p + 1] ?? 0;
1459
- }
1460
- return out;
1461
- }
1462
1727
  function createWhiteTexture(device) {
1463
1728
  const tex = device.createTexture({
1464
1729
  size: [1, 1],
@@ -1514,8 +1779,17 @@ var WebGpuRendererImpl = class {
1514
1779
  #maskW = 0;
1515
1780
  #maskH = 0;
1516
1781
  #whiteTexture = null;
1517
- #transient = [];
1782
+ #whiteView = null;
1783
+ #maskView = null;
1518
1784
  #gpuTextures = [];
1785
+ #gpuTextureViews = [];
1786
+ #meshCache = new WebGpuMeshCache();
1787
+ #clipPlan = new ResidentClippingPlan();
1788
+ #layoutScratch = new Float32Array(4);
1789
+ #channelScratch = new Float32Array(4);
1790
+ #boundsScratch = new Float32Array(4);
1791
+ /** Preview-only line index buffers destroyed at endFrame. */
1792
+ #transient = [];
1519
1793
  #options;
1520
1794
  constructor(options = {}) {
1521
1795
  this.#options = options;
@@ -1885,6 +2159,7 @@ var WebGpuRendererImpl = class {
1885
2159
  addressModeV: "clamp-to-edge"
1886
2160
  });
1887
2161
  this.#whiteTexture = createWhiteTexture(device);
2162
+ this.#whiteView = this.#whiteTexture.createView();
1888
2163
  this.#ensureMsaa();
1889
2164
  }
1890
2165
  #ensureMsaa() {
@@ -1923,11 +2198,15 @@ var WebGpuRendererImpl = class {
1923
2198
  });
1924
2199
  this.#maskW = w;
1925
2200
  this.#maskH = h;
2201
+ this.#maskView = this.#maskTexture.createView();
2202
+ this.#meshCache.bumpMaskGeneration();
1926
2203
  return this.#maskTexture;
1927
2204
  }
1928
2205
  #clearGpuTextures() {
1929
2206
  for (const t of this.#gpuTextures) t?.destroy();
1930
2207
  this.#gpuTextures = [];
2208
+ this.#gpuTextureViews = [];
2209
+ this.#meshCache.invalidateTextureBindGroups();
1931
2210
  }
1932
2211
  setTextures(textures) {
1933
2212
  const device = this.#device;
@@ -1936,6 +2215,7 @@ var WebGpuRendererImpl = class {
1936
2215
  let maxIndex = -1;
1937
2216
  for (const t of textures) maxIndex = Math.max(maxIndex, t.index);
1938
2217
  this.#gpuTextures = new Array(Math.max(0, maxIndex + 1)).fill(null);
2218
+ this.#gpuTextureViews = new Array(Math.max(0, maxIndex + 1)).fill(null);
1939
2219
  for (const t of textures) {
1940
2220
  const gpuTex = device.createTexture({
1941
2221
  size: [t.width, t.height],
@@ -1948,6 +2228,7 @@ var WebGpuRendererImpl = class {
1948
2228
  [t.width, t.height]
1949
2229
  );
1950
2230
  this.#gpuTextures[t.index] = gpuTex;
2231
+ this.#gpuTextureViews[t.index] = gpuTex.createView();
1951
2232
  }
1952
2233
  }
1953
2234
  createModelDrawPass() {
@@ -2018,8 +2299,8 @@ var WebGpuRendererImpl = class {
2018
2299
  }
2019
2300
  const byIndex = /* @__PURE__ */ new Map();
2020
2301
  for (const d of drawables) byIndex.set(d.index, d);
2021
- const partitioned = partitionForClipping(drawables);
2022
- const contexts = fitClippingContexts(partitioned.contexts, byIndex);
2302
+ const partitioned = this.#clipPlan.resolveMeshes(drawables);
2303
+ const contexts = partitioned.contexts;
2023
2304
  const { maskOnly } = partitioned;
2024
2305
  let maskTex = null;
2025
2306
  if (contexts.length > 0) {
@@ -2090,27 +2371,23 @@ var WebGpuRendererImpl = class {
2090
2371
  }
2091
2372
  #drawMaskMesh(d, pass, layout, sampler, whiteTex, atlasLayout, modelBounds, channelFlag) {
2092
2373
  const device = this.#device;
2093
- if (!device || d.opacity <= 0) return;
2094
- const interleaved = interleavePosUv2(d.vertexPositions, d.uvs);
2095
- const vbo = device.createBuffer({
2096
- size: interleaved.byteLength,
2097
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
2098
- mappedAtCreation: true
2099
- });
2100
- new Float32Array(vbo.getMappedRange()).set(interleaved);
2101
- vbo.unmap();
2102
- const gpuTex = this.#gpuTextures[d.textureIndex] ?? whiteTex;
2103
- const ubo = device.createBuffer({
2104
- size: 64,
2105
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
2106
- mappedAtCreation: true
2107
- });
2108
- const layoutVec = maskLayoutVec4(atlasLayout);
2109
- const ch = maskChannelVec4(channelFlag);
2110
- const boundsVec = maskLayoutVec4(modelBounds);
2111
- new Float32Array(ubo.getMappedRange()).set([
2374
+ const whiteView = this.#whiteView;
2375
+ if (!device || !whiteView || d.opacity <= 0) return;
2376
+ const slot = this.#meshCache.ensureSlot(device, d.index);
2377
+ this.#meshCache.uploadMesh(
2378
+ device,
2379
+ slot,
2380
+ d.vertexPositions,
2381
+ d.uvs,
2382
+ d.indices
2383
+ );
2384
+ const layoutVec = maskLayoutVec4(atlasLayout, this.#layoutScratch);
2385
+ const ch = maskChannelVec4(channelFlag, this.#channelScratch);
2386
+ const boundsVec = maskLayoutVec4(modelBounds, this.#boundsScratch);
2387
+ const useWhite = !this.#gpuTextures[d.textureIndex];
2388
+ this.#meshCache.writeUbo(device, slot, [
2112
2389
  Math.max(d.opacity, 1),
2113
- this.#gpuTextures[d.textureIndex] ? 1 : 0,
2390
+ useWhite ? 0 : 1,
2114
2391
  0,
2115
2392
  0,
2116
2393
  layoutVec[0],
@@ -2126,23 +2403,24 @@ var WebGpuRendererImpl = class {
2126
2403
  boundsVec[2],
2127
2404
  boundsVec[3]
2128
2405
  ]);
2129
- ubo.unmap();
2130
- const ibo = writeIndexBuffer(device, d.indices);
2131
- pass.setBindGroup(
2132
- 0,
2133
- device.createBindGroup({
2406
+ const texView = this.#gpuTextureViews[d.textureIndex] ?? whiteView;
2407
+ if (!slot.bgMask || slot.texIndex !== d.textureIndex || slot.useWhite !== useWhite) {
2408
+ slot.bgMask = device.createBindGroup({
2134
2409
  layout,
2135
2410
  entries: [
2136
- { binding: 0, resource: { buffer: ubo } },
2411
+ { binding: 0, resource: { buffer: slot.ubo } },
2137
2412
  { binding: 1, resource: sampler },
2138
- { binding: 2, resource: gpuTex.createView() }
2413
+ { binding: 2, resource: texView }
2139
2414
  ]
2140
- })
2141
- );
2142
- pass.setVertexBuffer(0, vbo);
2143
- pass.setIndexBuffer(ibo, "uint16");
2415
+ });
2416
+ slot.texIndex = d.textureIndex;
2417
+ slot.useWhite = useWhite;
2418
+ }
2419
+ pass.setBindGroup(0, slot.bgMask);
2420
+ pass.setVertexBuffer(0, slot.vbo);
2421
+ pass.setIndexBuffer(slot.ibo, "uint16");
2144
2422
  pass.drawIndexed(d.indices.length);
2145
- this.#transient.push(vbo, ubo, ibo);
2423
+ void whiteTex;
2146
2424
  }
2147
2425
  #drawColorMesh(d, pass, useMask, invertMask, maskTex, atlasLayout, channelFlag, modelBounds) {
2148
2426
  const device = this.#device;
@@ -2151,6 +2429,7 @@ var WebGpuRendererImpl = class {
2151
2429
  const texturedLayout = this.#texturedBindGroupLayout;
2152
2430
  const clippedSolidLayout = this.#clippedSolidBindGroupLayout;
2153
2431
  const clippedTexturedLayout = this.#clippedTexturedBindGroupLayout;
2432
+ const maskView = this.#maskView;
2154
2433
  if (!device || !sampler || !solidLayout || !texturedLayout || !clippedSolidLayout || !clippedTexturedLayout) {
2155
2434
  return;
2156
2435
  }
@@ -2158,30 +2437,30 @@ var WebGpuRendererImpl = class {
2158
2437
  const set = this.#pipelines[d.blendMode] ?? this.#pipelines[BlendMode.Normal];
2159
2438
  if (!set) return;
2160
2439
  const layoutVec = maskLayoutVec4(
2161
- atlasLayout ?? { x: 0, y: 0, width: 1, height: 1 }
2440
+ atlasLayout ?? { x: 0, y: 0, width: 1, height: 1 },
2441
+ this.#layoutScratch
2442
+ );
2443
+ const ch = maskChannelVec4(
2444
+ channelFlag ?? [0, 0, 0, 1],
2445
+ this.#channelScratch
2162
2446
  );
2163
- const ch = maskChannelVec4(channelFlag ?? [0, 0, 0, 1]);
2164
2447
  const boundsVec = maskLayoutVec4(
2165
- modelBounds ?? { x: -1, y: -1, width: 2, height: 2 }
2448
+ modelBounds ?? { x: -1, y: -1, width: 2, height: 2 },
2449
+ this.#boundsScratch
2166
2450
  );
2451
+ const slot = this.#meshCache.ensureSlot(device, d.index);
2167
2452
  const gpuTex = this.#gpuTextures[d.textureIndex] ?? null;
2168
- if (gpuTex) {
2169
- const interleaved = interleavePosUv2(d.vertexPositions, d.uvs);
2170
- const vbo2 = device.createBuffer({
2171
- size: interleaved.byteLength,
2172
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
2173
- mappedAtCreation: true
2174
- });
2175
- new Float32Array(vbo2.getMappedRange()).set(interleaved);
2176
- vbo2.unmap();
2177
- const uboSize = useMask && maskTex ? 64 : 16;
2178
- const ubo = device.createBuffer({
2179
- size: uboSize,
2180
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
2181
- mappedAtCreation: true
2182
- });
2183
- if (useMask && maskTex) {
2184
- new Float32Array(ubo.getMappedRange()).set([
2453
+ const texView = this.#gpuTextureViews[d.textureIndex] ?? null;
2454
+ if (gpuTex && texView) {
2455
+ this.#meshCache.uploadMesh(
2456
+ device,
2457
+ slot,
2458
+ d.vertexPositions,
2459
+ d.uvs,
2460
+ d.indices
2461
+ );
2462
+ if (useMask && maskTex && maskView) {
2463
+ this.#meshCache.writeUbo(device, slot, [
2185
2464
  d.opacity,
2186
2465
  invertMask ? 1 : 0,
2187
2466
  0,
@@ -2199,64 +2478,51 @@ var WebGpuRendererImpl = class {
2199
2478
  boundsVec[2],
2200
2479
  boundsVec[3]
2201
2480
  ]);
2202
- } else {
2203
- new Float32Array(ubo.getMappedRange()).set([
2204
- d.opacity,
2205
- 0,
2206
- 0,
2207
- 0
2208
- ]);
2209
- }
2210
- ubo.unmap();
2211
- const ibo = writeIndexBuffer(device, d.indices);
2212
- if (useMask && maskTex) {
2213
- pass.setPipeline(set.clippedTextured);
2214
- pass.setBindGroup(
2215
- 0,
2216
- device.createBindGroup({
2481
+ if (!slot.bgClippedTextured || slot.texIndex !== d.textureIndex || slot.maskGeneration !== this.#meshCache.maskGeneration) {
2482
+ slot.bgClippedTextured = device.createBindGroup({
2217
2483
  layout: clippedTexturedLayout,
2218
2484
  entries: [
2219
- { binding: 0, resource: { buffer: ubo } },
2485
+ { binding: 0, resource: { buffer: slot.ubo } },
2220
2486
  { binding: 1, resource: sampler },
2221
- { binding: 2, resource: gpuTex.createView() },
2222
- { binding: 3, resource: maskTex.createView() }
2487
+ { binding: 2, resource: texView },
2488
+ { binding: 3, resource: maskView }
2223
2489
  ]
2224
- })
2225
- );
2490
+ });
2491
+ slot.texIndex = d.textureIndex;
2492
+ slot.maskGeneration = this.#meshCache.maskGeneration;
2493
+ }
2494
+ pass.setPipeline(set.clippedTextured);
2495
+ pass.setBindGroup(0, slot.bgClippedTextured);
2226
2496
  } else {
2227
- pass.setPipeline(set.textured);
2228
- pass.setBindGroup(
2229
- 0,
2230
- device.createBindGroup({
2497
+ this.#meshCache.writeUbo(device, slot, [d.opacity, 0, 0, 0]);
2498
+ if (!slot.bgTextured || slot.texIndex !== d.textureIndex) {
2499
+ slot.bgTextured = device.createBindGroup({
2231
2500
  layout: texturedLayout,
2232
2501
  entries: [
2233
- { binding: 0, resource: { buffer: ubo } },
2502
+ { binding: 0, resource: { buffer: slot.ubo } },
2234
2503
  { binding: 1, resource: sampler },
2235
- { binding: 2, resource: gpuTex.createView() }
2504
+ { binding: 2, resource: texView }
2236
2505
  ]
2237
- })
2238
- );
2506
+ });
2507
+ slot.texIndex = d.textureIndex;
2508
+ }
2509
+ pass.setPipeline(set.textured);
2510
+ pass.setBindGroup(0, slot.bgTextured);
2239
2511
  }
2240
- pass.setVertexBuffer(0, vbo2);
2241
- pass.setIndexBuffer(ibo, "uint16");
2512
+ pass.setVertexBuffer(0, slot.vbo);
2513
+ pass.setIndexBuffer(slot.ibo, "uint16");
2242
2514
  pass.drawIndexed(d.indices.length);
2243
- this.#transient.push(vbo2, ubo, ibo);
2244
2515
  return;
2245
2516
  }
2246
- const vbo = device.createBuffer({
2247
- size: d.vertexPositions.byteLength,
2248
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
2249
- mappedAtCreation: true
2250
- });
2251
- new Float32Array(vbo.getMappedRange()).set(d.vertexPositions);
2252
- vbo.unmap();
2253
- if (useMask && maskTex) {
2254
- const ubo = device.createBuffer({
2255
- size: 80,
2256
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
2257
- mappedAtCreation: true
2258
- });
2259
- new Float32Array(ubo.getMappedRange()).set([
2517
+ this.#meshCache.uploadMesh(
2518
+ device,
2519
+ slot,
2520
+ d.vertexPositions,
2521
+ d.uvs,
2522
+ d.indices
2523
+ );
2524
+ if (useMask && maskTex && maskView) {
2525
+ this.#meshCache.writeUbo(device, slot, [
2260
2526
  PREVIEW_FILL.r,
2261
2527
  PREVIEW_FILL.g,
2262
2528
  PREVIEW_FILL.b,
@@ -2278,54 +2544,61 @@ var WebGpuRendererImpl = class {
2278
2544
  boundsVec[2],
2279
2545
  boundsVec[3]
2280
2546
  ]);
2281
- ubo.unmap();
2282
- const ibo = writeIndexBuffer(device, d.indices);
2283
- pass.setPipeline(set.clippedFill);
2284
- pass.setBindGroup(
2285
- 0,
2286
- device.createBindGroup({
2547
+ if (!slot.bgClippedSolid || slot.maskGeneration !== this.#meshCache.maskGeneration) {
2548
+ slot.bgClippedSolid = device.createBindGroup({
2287
2549
  layout: clippedSolidLayout,
2288
2550
  entries: [
2289
- { binding: 0, resource: { buffer: ubo } },
2551
+ { binding: 0, resource: { buffer: slot.ubo } },
2290
2552
  { binding: 1, resource: sampler },
2291
- { binding: 2, resource: maskTex.createView() }
2553
+ { binding: 2, resource: maskView }
2292
2554
  ]
2293
- })
2294
- );
2295
- pass.setVertexBuffer(0, vbo);
2296
- pass.setIndexBuffer(ibo, "uint16");
2555
+ });
2556
+ slot.maskGeneration = this.#meshCache.maskGeneration;
2557
+ }
2558
+ pass.setPipeline(set.clippedFill);
2559
+ pass.setBindGroup(0, slot.bgClippedSolid);
2560
+ pass.setVertexBuffer(0, slot.vbo);
2561
+ pass.setIndexBuffer(slot.ibo, "uint16");
2297
2562
  pass.drawIndexed(d.indices.length);
2298
- this.#transient.push(vbo, ubo, ibo);
2299
2563
  return;
2300
2564
  }
2301
- const fillUbo = this.#makeColorUbo(
2302
- device,
2565
+ this.#meshCache.writeUbo(device, slot, [
2303
2566
  PREVIEW_FILL.r,
2304
2567
  PREVIEW_FILL.g,
2305
2568
  PREVIEW_FILL.b,
2306
2569
  PREVIEW_FILL.a * d.opacity
2307
- );
2308
- const fillIbo = writeIndexBuffer(device, d.indices);
2309
- pass.setPipeline(set.fill);
2310
- pass.setBindGroup(
2311
- 0,
2312
- device.createBindGroup({
2570
+ ]);
2571
+ if (!slot.bgSolid) {
2572
+ slot.bgSolid = device.createBindGroup({
2313
2573
  layout: solidLayout,
2314
- entries: [{ binding: 0, resource: { buffer: fillUbo } }]
2315
- })
2316
- );
2317
- pass.setVertexBuffer(0, vbo);
2318
- pass.setIndexBuffer(fillIbo, "uint16");
2574
+ entries: [{ binding: 0, resource: { buffer: slot.ubo } }]
2575
+ });
2576
+ }
2577
+ pass.setPipeline(set.fill);
2578
+ pass.setBindGroup(0, slot.bgSolid);
2579
+ pass.setVertexBuffer(0, slot.vbo);
2580
+ pass.setIndexBuffer(slot.ibo, "uint16");
2319
2581
  pass.drawIndexed(d.indices.length);
2320
2582
  const lines = triangleEdgesToLineList(d.indices);
2321
- const lineUbo = this.#makeColorUbo(
2322
- device,
2583
+ const lineUbo = device.createBuffer({
2584
+ size: 16,
2585
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
2586
+ mappedAtCreation: true
2587
+ });
2588
+ new Float32Array(lineUbo.getMappedRange()).set([
2323
2589
  PREVIEW_STROKE.r,
2324
2590
  PREVIEW_STROKE.g,
2325
2591
  PREVIEW_STROKE.b,
2326
2592
  PREVIEW_STROKE.a * d.opacity
2327
- );
2328
- const lineIbo = writeIndexBuffer(device, lines);
2593
+ ]);
2594
+ lineUbo.unmap();
2595
+ const lineIbo = device.createBuffer({
2596
+ size: Math.max(4, lines.byteLength + 3 & ~3),
2597
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
2598
+ mappedAtCreation: true
2599
+ });
2600
+ new Uint16Array(lineIbo.getMappedRange()).set(lines);
2601
+ lineIbo.unmap();
2329
2602
  pass.setPipeline(set.line);
2330
2603
  pass.setBindGroup(
2331
2604
  0,
@@ -2334,20 +2607,10 @@ var WebGpuRendererImpl = class {
2334
2607
  entries: [{ binding: 0, resource: { buffer: lineUbo } }]
2335
2608
  })
2336
2609
  );
2337
- pass.setVertexBuffer(0, vbo);
2610
+ pass.setVertexBuffer(0, slot.vbo);
2338
2611
  pass.setIndexBuffer(lineIbo, "uint16");
2339
2612
  pass.drawIndexed(lines.length);
2340
- this.#transient.push(vbo, fillUbo, fillIbo, lineUbo, lineIbo);
2341
- }
2342
- #makeColorUbo(device, r, g, b, a) {
2343
- const ubo = device.createBuffer({
2344
- size: 16,
2345
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
2346
- mappedAtCreation: true
2347
- });
2348
- new Float32Array(ubo.getMappedRange()).set([r, g, b, a]);
2349
- ubo.unmap();
2350
- return ubo;
2613
+ this.#transient.push(lineUbo, lineIbo);
2351
2614
  }
2352
2615
  endFrame() {
2353
2616
  const device = this.#device;
@@ -2376,13 +2639,17 @@ var WebGpuRendererImpl = class {
2376
2639
  destroy() {
2377
2640
  for (const b of this.#transient) b.destroy();
2378
2641
  this.#transient = [];
2642
+ this.#meshCache.clear();
2643
+ this.#clipPlan.clear();
2379
2644
  this.#clearGpuTextures();
2380
2645
  this.#msaaTexture?.destroy();
2381
2646
  this.#msaaTexture = null;
2382
2647
  this.#maskTexture?.destroy();
2383
2648
  this.#maskTexture = null;
2649
+ this.#maskView = null;
2384
2650
  this.#whiteTexture?.destroy();
2385
2651
  this.#whiteTexture = null;
2652
+ this.#whiteView = null;
2386
2653
  this.#device?.destroy();
2387
2654
  this.#device = null;
2388
2655
  this.#context = null;
@@ -5927,6 +6194,7 @@ export {
5927
6194
  Moc3DrawableFlag,
5928
6195
  PREVIEW_FILL,
5929
6196
  PREVIEW_STROKE,
6197
+ ResidentClippingPlan,
5930
6198
  WebGl2RendererImpl,
5931
6199
  WebGpuRendererImpl,
5932
6200
  applyMoc3Glues,
@@ -5935,6 +6203,7 @@ export {
5935
6203
  calcClippedDrawableBounds,
5936
6204
  calcVertexBounds,
5937
6205
  canvasCompositeForBlendMode,
6206
+ clippingTopologyKey,
5938
6207
  compileSharedModelCompile,
5939
6208
  createCanvas2DRenderer,
5940
6209
  createMoc2Backend,
@@ -5954,6 +6223,9 @@ export {
5954
6223
  expandBounds,
5955
6224
  fingerprintSnapshot,
5956
6225
  fitClippingContexts,
6226
+ fitClippingContextsInPlace,
6227
+ interleaveByteLength,
6228
+ interleavePosUv,
5957
6229
  isCanvas2DAvailable,
5958
6230
  isCpuProgramBytes,
5959
6231
  isWebGl2Available,