framefields 2.2.0-beta.1 → 2.2.0-beta.2

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.
Files changed (31) hide show
  1. package/dist/effects/index.d.mts +1 -1
  2. package/dist/effects/index.mjs +1 -1
  3. package/dist/index--1jnkoBj.d.mts.map +1 -1
  4. package/dist/{index-DLbtJPrc.d.mts → index-HyzpJrHe.d.mts} +31 -10
  5. package/dist/index-HyzpJrHe.d.mts.map +1 -0
  6. package/dist/index.d.mts +1 -1
  7. package/dist/index.mjs +3 -3
  8. package/dist/preview/index.mjs +1 -1
  9. package/dist/{preview-BQ3BhcVX.mjs → preview-BXu7FyvN.mjs} +2 -2
  10. package/dist/{preview-BQ3BhcVX.mjs.map → preview-BXu7FyvN.mjs.map} +1 -1
  11. package/dist/preview-engine/{chunk-Z457JJCO.mjs → chunk-3NXJVSZY.mjs} +1 -1
  12. package/dist/preview-engine/{chunk-HFUA47QH.mjs → chunk-6RSP766E.mjs} +1 -1
  13. package/dist/preview-engine/chunk-HCZCEOV5.mjs +174 -0
  14. package/dist/preview-engine/chunk-X6HUH6K4.mjs +1 -0
  15. package/dist/preview-engine/effects.mjs +1 -1
  16. package/dist/preview-engine/framefields.mjs +1 -1
  17. package/dist/preview-engine/player.mjs +1 -1
  18. package/dist/preview-engine/{renderers-HVTQ2B3H.mjs → renderers-NYK2NDZJ.mjs} +1 -1
  19. package/dist/renderer/index.mjs +1 -1
  20. package/dist/renderer-BkV_xpMb.mjs +3 -0
  21. package/dist/{renderer-dc39dBVA.mjs → renderer-CqT2vxa4.mjs} +989 -101
  22. package/dist/renderer-CqT2vxa4.mjs.map +1 -0
  23. package/dist/{renderer-DzDZlez7.mjs → renderer-Dqdega0-.mjs} +6 -3
  24. package/dist/renderer-Dqdega0-.mjs.map +1 -0
  25. package/package.json +55 -55
  26. package/dist/index-DLbtJPrc.d.mts.map +0 -1
  27. package/dist/preview-engine/chunk-7OMK352W.mjs +0 -1
  28. package/dist/preview-engine/chunk-K7M5EIFC.mjs +0 -98
  29. package/dist/renderer-CZKFEf_6.mjs +0 -3
  30. package/dist/renderer-DzDZlez7.mjs.map +0 -1
  31. package/dist/renderer-dc39dBVA.mjs.map +0 -1
@@ -190,7 +190,7 @@ const COCO_CLASSES = [
190
190
  ];
191
191
 
192
192
  //#endregion
193
- //#region ../vision/dist/src-BjTNynFj.mjs
193
+ //#region ../vision/dist/src-CQS94ONs.mjs
194
194
  function computeIoU(boxA, boxB) {
195
195
  const xA = Math.max(boxA.originX, boxB.originX);
196
196
  const yA = Math.max(boxA.originY, boxB.originY);
@@ -263,11 +263,11 @@ var TemporalObjectTracker = class {
263
263
  for (let d = 0; d < detections.length; d++) {
264
264
  const det = detections[d];
265
265
  if (trk.category !== det.category) continue;
266
- const iou = computeIoU(trk.box, det.boundingBox);
267
- if (iou >= this._iouThreshold) candidateMatches.push({
266
+ const iou$1 = computeIoU(trk.box, det.boundingBox);
267
+ if (iou$1 >= this._iouThreshold) candidateMatches.push({
268
268
  trackIdx: t,
269
269
  detIdx: d,
270
- iou
270
+ iou: iou$1
271
271
  });
272
272
  }
273
273
  }
@@ -858,11 +858,13 @@ function decodeRtmdetIns(out, opts) {
858
858
  kept.sort((a, b) => b.detection.score - a.detection.score);
859
859
  const detections = kept.map((k) => k.detection);
860
860
  const masks = [];
861
- if (!out.masks) return {
861
+ if (!out.masks && !out.maskPlane) return {
862
862
  detections,
863
863
  masks
864
864
  };
865
- const alphaOf = probabilityToAlpha(opts.maskThreshold ?? .5, opts.featherRadius ?? .05);
865
+ const threshold = opts.maskThreshold ?? .5;
866
+ const feather = opts.featherRadius ?? .05;
867
+ const alphaOf = out.maskLogits ? logitToAlpha(threshold, feather) : probabilityToAlpha(threshold, feather);
866
868
  const { maskWidth: mw, maskHeight: mh } = out;
867
869
  const plane = mw * mh;
868
870
  const gx = mw / transform.inputWidth;
@@ -870,7 +872,8 @@ function decodeRtmdetIns(out, opts) {
870
872
  for (let d = 0; d < kept.length; d++) {
871
873
  const { index, detection } = kept[d];
872
874
  const box = detection.boundingBox;
873
- const base = index * plane;
875
+ const data = out.maskPlane ? out.maskPlane(index) : out.masks;
876
+ const base = out.maskPlane ? 0 : index * plane;
874
877
  const bx0 = Math.max(0, Math.floor(box.originX) - MASK_BOX_PAD_PX);
875
878
  const by0 = Math.max(0, Math.floor(box.originY) - MASK_BOX_PAD_PX);
876
879
  const bx1 = Math.min(sw - 1, Math.ceil(box.originX + box.width) + MASK_BOX_PAD_PX);
@@ -888,7 +891,7 @@ function decodeRtmdetIns(out, opts) {
888
891
  const mx0 = clamp$1(Math.floor(mx), 0, mw - 1);
889
892
  const mx1 = Math.min(mw - 1, mx0 + 1);
890
893
  const fx = clamp$1(mx - mx0, 0, 1);
891
- const alpha = alphaOf(out.masks[base + my0 * mw + mx0] * (1 - fx) * (1 - fy) + out.masks[base + my0 * mw + mx1] * fx * (1 - fy) + out.masks[base + my1 * mw + mx0] * (1 - fx) * fy + out.masks[base + my1 * mw + mx1] * fx * fy);
894
+ const alpha = alphaOf(data[base + my0 * mw + mx0] * (1 - fx) * (1 - fy) + data[base + my0 * mw + mx1] * fx * (1 - fy) + data[base + my1 * mw + mx0] * (1 - fx) * fy + data[base + my1 * mw + mx1] * fx * fy);
892
895
  if (alpha > 0) {
893
896
  mask[row + sx] = alpha;
894
897
  area += alpha / 255;
@@ -922,6 +925,21 @@ function probabilityToAlpha(threshold, feather) {
922
925
  const inv = 255 / (2 * feather);
923
926
  return (p) => p <= lo ? 0 : p >= threshold + feather ? 255 : (p - lo) * inv;
924
927
  }
928
+ /**
929
+ * Logit → 0..255 alpha: `probabilityToAlpha` after a sigmoid, which is only
930
+ * evaluated inside the feather band.
931
+ */
932
+ function logitToAlpha(threshold, feather) {
933
+ const toAlpha = probabilityToAlpha(threshold, feather);
934
+ const logit = (p) => p <= 0 ? -Infinity : p >= 1 ? Infinity : Math.log(p / (1 - p));
935
+ if (feather <= 0) {
936
+ const edge = logit(threshold);
937
+ return (l) => l > edge ? 255 : 0;
938
+ }
939
+ const lo = logit(threshold - feather);
940
+ const hi = logit(threshold + feather);
941
+ return (l) => l <= lo ? 0 : l >= hi ? 255 : toAlpha(1 / (1 + Math.exp(-l)));
942
+ }
925
943
  function clamp$1(v, lo, hi) {
926
944
  return v < lo ? lo : v > hi ? hi : v;
927
945
  }
@@ -972,9 +990,9 @@ function decodeRtmo(out, opts) {
972
990
  });
973
991
  }
974
992
  people.sort((a, b) => b.score - a.score);
975
- const iou = opts.iouThreshold ?? .6;
993
+ const iou$1 = opts.iouThreshold ?? .6;
976
994
  const kept = [];
977
- for (const p of people) if (!kept.some((k) => isDuplicate(k, p, iou))) kept.push(p);
995
+ for (const p of people) if (!kept.some((k) => isDuplicate(k, p, iou$1))) kept.push(p);
978
996
  return { people: kept };
979
997
  }
980
998
  function clamp(v, lo, hi) {
@@ -985,8 +1003,8 @@ function clamp(v, lo, hi) {
985
1003
  * within 10% of the stronger detection's box size of each other (boxes may differ when one
986
1004
  * of them also covers a flowing garment or a shadow).
987
1005
  */
988
- function isDuplicate(a, b, iou) {
989
- if (computeIoU(a.boundingBox, b.boundingBox) > iou) return true;
1006
+ function isDuplicate(a, b, iou$1) {
1007
+ if (computeIoU(a.boundingBox, b.boundingBox) > iou$1) return true;
990
1008
  const scale = Math.sqrt(a.boundingBox.width * a.boundingBox.height);
991
1009
  let sum = 0;
992
1010
  let n = 0;
@@ -1028,6 +1046,286 @@ function decodeSelfie(alphas, matteWidth, matteHeight, opts) {
1028
1046
  coverage: sw * sh > 0 ? sum / (255 * sw * sh) : 0
1029
1047
  };
1030
1048
  }
1049
+ /**
1050
+ * Frame preprocessing on the GPU: a texture becomes a model's NCHW float input
1051
+ * in a storage buffer, so a frame drawn by the renderer reaches the model
1052
+ * without a round trip through the CPU. The math is `imageToTensor`'s
1053
+ * (pixel-centre bilinear sampling, letterbox padding, channel order,
1054
+ * normalisation); see decode/preprocess.ts for the per-model contracts.
1055
+ *
1056
+ * Also: a content hash of a texture (two order-independent 32-bit sums, read
1057
+ * back as 8 bytes), so layers showing the same pixels can share a pass, and a
1058
+ * plain RGBA readback for paths that need pixels on the CPU.
1059
+ */
1060
+ const TENSOR_WGSL = `
1061
+ struct Params {
1062
+ src: vec2u,
1063
+ dst: vec2u,
1064
+ scale: vec2f,
1065
+ offset: vec2f,
1066
+ lo: vec2i,
1067
+ hi: vec2i,
1068
+ mean: vec4f,
1069
+ invStd: vec4f,
1070
+ pad: vec4f,
1071
+ order: vec4u,
1072
+ };
1073
+
1074
+ @group(0) @binding(0) var<uniform> p: Params;
1075
+ @group(0) @binding(1) var src: texture_2d<f32>;
1076
+ @group(0) @binding(2) var<storage, read_write> dst: array<f32>;
1077
+
1078
+ @compute @workgroup_size(16, 16)
1079
+ fn main(@builtin(global_invocation_id) id: vec3u) {
1080
+ if (id.x >= p.dst.x || id.y >= p.dst.y) { return; }
1081
+ let x = i32(id.x);
1082
+ let y = i32(id.y);
1083
+ let plane = p.dst.x * p.dst.y;
1084
+ let o = id.y * p.dst.x + id.x;
1085
+ var v = p.pad.xyz;
1086
+ if (x >= p.lo.x && x <= p.hi.x && y >= p.lo.y && y <= p.hi.y) {
1087
+ let maxX = i32(p.src.x) - 1;
1088
+ let maxY = i32(p.src.y) - 1;
1089
+ let sy = max(0.0, (f32(y) + 0.5 - p.offset.y) / p.scale.y - 0.5);
1090
+ let iy0 = min(maxY, i32(floor(sy)));
1091
+ let iy1 = min(maxY, iy0 + 1);
1092
+ let fy = sy - f32(iy0);
1093
+ let sx = max(0.0, (f32(x) + 0.5 - p.offset.x) / p.scale.x - 0.5);
1094
+ let ix0 = min(maxX, i32(floor(sx)));
1095
+ let ix1 = min(maxX, ix0 + 1);
1096
+ let fx = sx - f32(ix0);
1097
+ var rgba =
1098
+ textureLoad(src, vec2i(ix0, iy0), 0) * ((1.0 - fx) * (1.0 - fy)) +
1099
+ textureLoad(src, vec2i(ix1, iy0), 0) * (fx * (1.0 - fy)) +
1100
+ textureLoad(src, vec2i(ix0, iy1), 0) * ((1.0 - fx) * fy) +
1101
+ textureLoad(src, vec2i(ix1, iy1), 0) * (fx * fy);
1102
+ rgba = rgba * 255.0;
1103
+ let s = vec3f(rgba[p.order.x], rgba[p.order.y], rgba[p.order.z]);
1104
+ v = (s - p.mean.xyz) * p.invStd.xyz;
1105
+ }
1106
+ dst[o] = v.x;
1107
+ dst[plane + o] = v.y;
1108
+ dst[2u * plane + o] = v.z;
1109
+ }
1110
+ `;
1111
+ const HASH_WGSL = `
1112
+ @group(0) @binding(0) var src: texture_2d<f32>;
1113
+ @group(0) @binding(1) var<storage, read_write> acc: array<atomic<u32>, 2>;
1114
+
1115
+ var<workgroup> local: array<atomic<u32>, 2>;
1116
+
1117
+ @compute @workgroup_size(16, 16)
1118
+ fn main(
1119
+ @builtin(global_invocation_id) id: vec3u,
1120
+ @builtin(local_invocation_index) li: u32,
1121
+ ) {
1122
+ let dim = textureDimensions(src);
1123
+ if (id.x < dim.x && id.y < dim.y) {
1124
+ let c = pack4x8unorm(textureLoad(src, vec2i(id.xy), 0));
1125
+ let i = id.y * dim.x + id.x;
1126
+ let a = (c ^ (i * 0x9e3779b9u)) * 0x85ebca6bu;
1127
+ let b = (c + i * 0xc2b2ae35u) * 0x27d4eb2fu;
1128
+ atomicAdd(&local[0], a ^ (a >> 15u));
1129
+ atomicAdd(&local[1], b ^ (b >> 13u));
1130
+ }
1131
+ workgroupBarrier();
1132
+ if (li == 0u) {
1133
+ atomicAdd(&acc[0], atomicLoad(&local[0]));
1134
+ atomicAdd(&acc[1], atomicLoad(&local[1]));
1135
+ }
1136
+ }
1137
+ `;
1138
+ const PARAMS_BYTES = 112;
1139
+ const pipelines = /* @__PURE__ */ new WeakMap();
1140
+ var FrameTensorPipeline = class FrameTensorPipeline$1 {
1141
+ tensorPipeline;
1142
+ hashPipeline;
1143
+ params;
1144
+ /** One per device. */
1145
+ static for(device) {
1146
+ let pipeline = pipelines.get(device);
1147
+ if (!pipeline) {
1148
+ pipeline = new FrameTensorPipeline$1(device);
1149
+ pipelines.set(device, pipeline);
1150
+ }
1151
+ return pipeline;
1152
+ }
1153
+ constructor(device) {
1154
+ this.device = device;
1155
+ this.tensorPipeline = device.createComputePipeline({
1156
+ label: "vision_frame_tensor",
1157
+ layout: "auto",
1158
+ compute: {
1159
+ module: device.createShaderModule({ code: TENSOR_WGSL }),
1160
+ entryPoint: "main"
1161
+ }
1162
+ });
1163
+ this.hashPipeline = device.createComputePipeline({
1164
+ label: "vision_frame_hash",
1165
+ layout: "auto",
1166
+ compute: {
1167
+ module: device.createShaderModule({ code: HASH_WGSL }),
1168
+ entryPoint: "main"
1169
+ }
1170
+ });
1171
+ this.params = device.createBuffer({
1172
+ label: "vision_frame_tensor_params",
1173
+ size: PARAMS_BYTES,
1174
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
1175
+ });
1176
+ }
1177
+ /** Bytes of the NCHW float tensor for `spec`. */
1178
+ static tensorBytes(spec) {
1179
+ return 3 * spec.width * spec.height * 4;
1180
+ }
1181
+ /**
1182
+ * Writes `image` as the model input described by `spec` into `out` (at least
1183
+ * `tensorBytes(spec)`, STORAGE usage). Submits its own command buffer, so it
1184
+ * runs before anything submitted later (e.g. the inference reading `out`).
1185
+ */
1186
+ toTensor(image, spec, out) {
1187
+ const transform = computeInputTransform(image.width, image.height, spec);
1188
+ const { scaleX, scaleY, offsetX, offsetY } = transform;
1189
+ const params = new ArrayBuffer(PARAMS_BYTES);
1190
+ const u32 = new Uint32Array(params);
1191
+ const f32 = new Float32Array(params);
1192
+ const i32 = new Int32Array(params);
1193
+ u32.set([
1194
+ image.width,
1195
+ image.height,
1196
+ spec.width,
1197
+ spec.height
1198
+ ], 0);
1199
+ f32.set([
1200
+ scaleX,
1201
+ scaleY,
1202
+ offsetX,
1203
+ offsetY
1204
+ ], 4);
1205
+ i32.set([
1206
+ Math.ceil(offsetX),
1207
+ Math.ceil(offsetY),
1208
+ Math.min(spec.width, Math.floor(offsetX + image.width * scaleX)) - 1,
1209
+ Math.min(spec.height, Math.floor(offsetY + image.height * scaleY)) - 1
1210
+ ], 8);
1211
+ const inv = spec.std.map((s) => 1 / s);
1212
+ f32.set([...spec.mean, 0], 12);
1213
+ f32.set([...inv, 0], 16);
1214
+ f32.set([
1215
+ 0,
1216
+ 1,
1217
+ 2
1218
+ ].map((c) => (spec.padValue - spec.mean[c]) * inv[c]), 20);
1219
+ u32.set(spec.channels === "bgr" ? [
1220
+ 2,
1221
+ 1,
1222
+ 0,
1223
+ 3
1224
+ ] : [
1225
+ 0,
1226
+ 1,
1227
+ 2,
1228
+ 3
1229
+ ], 24);
1230
+ this.device.queue.writeBuffer(this.params, 0, params);
1231
+ const encoder$1 = this.device.createCommandEncoder({ label: "vision_frame_tensor" });
1232
+ const pass = encoder$1.beginComputePass();
1233
+ pass.setPipeline(this.tensorPipeline);
1234
+ pass.setBindGroup(0, this.device.createBindGroup({
1235
+ layout: this.tensorPipeline.getBindGroupLayout(0),
1236
+ entries: [
1237
+ {
1238
+ binding: 0,
1239
+ resource: { buffer: this.params }
1240
+ },
1241
+ {
1242
+ binding: 1,
1243
+ resource: image.texture.createView()
1244
+ },
1245
+ {
1246
+ binding: 2,
1247
+ resource: { buffer: out }
1248
+ }
1249
+ ]
1250
+ }));
1251
+ pass.dispatchWorkgroups(Math.ceil(spec.width / 16), Math.ceil(spec.height / 16));
1252
+ pass.end();
1253
+ this.device.queue.submit([encoder$1.finish()]);
1254
+ return transform;
1255
+ }
1256
+ /** A key that is equal for textures with equal size and pixels. */
1257
+ async contentKey(image) {
1258
+ const acc = this.device.createBuffer({
1259
+ size: 8,
1260
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
1261
+ });
1262
+ const staging = this.device.createBuffer({
1263
+ size: 8,
1264
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
1265
+ });
1266
+ const encoder$1 = this.device.createCommandEncoder({ label: "vision_frame_hash" });
1267
+ const pass = encoder$1.beginComputePass();
1268
+ pass.setPipeline(this.hashPipeline);
1269
+ pass.setBindGroup(0, this.device.createBindGroup({
1270
+ layout: this.hashPipeline.getBindGroupLayout(0),
1271
+ entries: [{
1272
+ binding: 0,
1273
+ resource: image.texture.createView()
1274
+ }, {
1275
+ binding: 1,
1276
+ resource: { buffer: acc }
1277
+ }]
1278
+ }));
1279
+ pass.dispatchWorkgroups(Math.ceil(image.width / 16), Math.ceil(image.height / 16));
1280
+ pass.end();
1281
+ encoder$1.copyBufferToBuffer(acc, 0, staging, 0, 8);
1282
+ this.device.queue.submit([encoder$1.finish()]);
1283
+ await staging.mapAsync(GPUMapMode.READ);
1284
+ const [a, b] = new Uint32Array(staging.getMappedRange().slice(0));
1285
+ staging.unmap();
1286
+ staging.destroy();
1287
+ acc.destroy();
1288
+ return `${image.width}x${image.height}:gpu:${a.toString(36)}:${b.toString(36)}`;
1289
+ }
1290
+ };
1291
+ /** Reads an 8-bit RGBA/BGRA texture back as RGBA pixels. */
1292
+ async function readTexturePixels(image) {
1293
+ const { device, texture, width, height } = image;
1294
+ const unaligned = width * 4;
1295
+ const bytesPerRow = Math.ceil(unaligned / 256) * 256;
1296
+ const staging = device.createBuffer({
1297
+ size: bytesPerRow * height,
1298
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
1299
+ label: "vision_frame_readback"
1300
+ });
1301
+ const encoder$1 = device.createCommandEncoder({ label: "vision_frame_readback" });
1302
+ encoder$1.copyTextureToBuffer({ texture }, {
1303
+ buffer: staging,
1304
+ bytesPerRow,
1305
+ rowsPerImage: height
1306
+ }, [
1307
+ width,
1308
+ height,
1309
+ 1
1310
+ ]);
1311
+ device.queue.submit([encoder$1.finish()]);
1312
+ await staging.mapAsync(GPUMapMode.READ);
1313
+ const mapped = new Uint8Array(staging.getMappedRange());
1314
+ const data = new Uint8ClampedArray(width * height * 4);
1315
+ for (let y = 0; y < height; y++) data.set(mapped.subarray(y * bytesPerRow, y * bytesPerRow + unaligned), y * unaligned);
1316
+ staging.unmap();
1317
+ staging.destroy();
1318
+ if (texture.format.startsWith("bgra")) for (let i = 0; i < data.length; i += 4) {
1319
+ const b = data[i];
1320
+ data[i] = data[i + 2];
1321
+ data[i + 2] = b;
1322
+ }
1323
+ return {
1324
+ data,
1325
+ width,
1326
+ height
1327
+ };
1328
+ }
1031
1329
  /** COCO-17 keypoint indices (RTMO output order). */
1032
1330
  const COCO17_KEYPOINTS = {
1033
1331
  NOSE: 0,
@@ -1474,6 +1772,431 @@ function createNeutralLandmarks() {
1474
1772
  });
1475
1773
  return landmarks;
1476
1774
  }
1775
+ /**
1776
+ * RTMDet-Ins post-processing, done here instead of inside the ONNX graph.
1777
+ *
1778
+ * The mmdeploy end2end export runs top-k, per-class NMS and mask assembly in
1779
+ * the graph and returns a full input-sized mask for each of up to 100
1780
+ * detections (40 on a typical frame: ~65 MB of floats). The runner instead
1781
+ * cuts the graph before its first data-dependent op (see `RTMDET_INS_HEAD`)
1782
+ * and reproduces the tail exactly:
1783
+ *
1784
+ * 1. top 5000 anchors by their best class score;
1785
+ * 2. ONNX NonMaxSuppression per class (IoU > 0.6 suppresses, scores > 0.05,
1786
+ * at most 200 per class);
1787
+ * 3. the 100 best (class, box) pairs;
1788
+ * 4. masks on demand: a 3-layer dynamic 1×1 conv over the 8 mask features
1789
+ * plus each pixel's offset from the anchor, at 1/8 of the input size.
1790
+ * The logits are upsampled (bilinear) and squashed by the decoder.
1791
+ *
1792
+ * Constants and layer layout match the export's graph; `models.e2e.test.ts`
1793
+ * checks the result against the end2end outputs.
1794
+ */
1795
+ /** Head tensors of the mmdeploy RTMDet-Ins exports (all variants). */
1796
+ const RTMDET_INS_HEAD = {
1797
+ scores: "/Sigmoid_output_0",
1798
+ boxes: "/Concat_7_output_0",
1799
+ kernels: "/Concat_5_output_0",
1800
+ maskFeatures: "/bbox_head/mask_head/projection/Conv_output_0"
1801
+ };
1802
+ const PRE_NMS = 5e3;
1803
+ const IOU_THRESHOLD = Math.fround(.6);
1804
+ const SCORE_FLOOR = Math.fround(.05);
1805
+ const MAX_PER_CLASS = 200;
1806
+ const MAX_DETECTIONS = 100;
1807
+ const STRIDES = [
1808
+ 8,
1809
+ 16,
1810
+ 32
1811
+ ];
1812
+ /** Mask features are at stride 8; relative offsets are divided by stride × 8. */
1813
+ const MASK_STRIDE = 8;
1814
+ const MASK_CHANNELS = 8;
1815
+ const KERNEL_SIZE = 169;
1816
+ /** Steps 1–3: the detections the end2end graph would return, best first. */
1817
+ function selectInstances(head) {
1818
+ const { scores, boxes, anchors, classes } = head;
1819
+ const best = new Float32Array(anchors);
1820
+ for (let a = 0; a < anchors; a++) {
1821
+ let max = 0;
1822
+ const row = a * classes;
1823
+ for (let c = 0; c < classes; c++) {
1824
+ const s = scores[row + c];
1825
+ if (s > max) max = s;
1826
+ }
1827
+ best[a] = max;
1828
+ }
1829
+ const order = [];
1830
+ for (let a = 0; a < anchors; a++) if (best[a] > SCORE_FLOOR) order.push(a);
1831
+ order.sort((x, y) => best[y] - best[x] || x - y);
1832
+ if (order.length > PRE_NMS) order.length = PRE_NMS;
1833
+ const picked = [];
1834
+ const candidates = [];
1835
+ const kept = [];
1836
+ for (let c = 0; c < classes; c++) {
1837
+ candidates.length = 0;
1838
+ for (const a of order) if (scores[a * classes + c] > SCORE_FLOOR) candidates.push(a);
1839
+ if (candidates.length === 0) continue;
1840
+ candidates.sort((x, y) => scores[y * classes + c] - scores[x * classes + c] || x - y);
1841
+ kept.length = 0;
1842
+ for (const a of candidates) {
1843
+ if (kept.length >= MAX_PER_CLASS) break;
1844
+ let suppressed = false;
1845
+ for (const k of kept) if (iou(boxes, a, k) > IOU_THRESHOLD) {
1846
+ suppressed = true;
1847
+ break;
1848
+ }
1849
+ if (!suppressed) {
1850
+ kept.push(a);
1851
+ picked.push({
1852
+ anchor: a,
1853
+ classIndex: c,
1854
+ score: scores[a * classes + c]
1855
+ });
1856
+ }
1857
+ }
1858
+ }
1859
+ picked.sort((x, y) => y.score - x.score);
1860
+ if (picked.length > MAX_DETECTIONS) picked.length = MAX_DETECTIONS;
1861
+ return picked;
1862
+ }
1863
+ function iou(boxes, a, b) {
1864
+ const a0 = boxes[a * 4];
1865
+ const a1 = boxes[a * 4 + 1];
1866
+ const a2 = boxes[a * 4 + 2];
1867
+ const a3 = boxes[a * 4 + 3];
1868
+ const b0 = boxes[b * 4];
1869
+ const b1 = boxes[b * 4 + 1];
1870
+ const b2 = boxes[b * 4 + 2];
1871
+ const b3 = boxes[b * 4 + 3];
1872
+ const w = Math.min(a2, b2) - Math.max(a0, b0);
1873
+ const h = Math.min(a3, b3) - Math.max(a1, b1);
1874
+ if (w <= 0 || h <= 0) return 0;
1875
+ const inter = w * h;
1876
+ const union = (a2 - a0) * (a3 - a1) + (b2 - b0) * (b3 - b1) - inter;
1877
+ return union > 0 ? inter / union : 0;
1878
+ }
1879
+ /** Anchor position and stride, in input pixels (mmdet point priors, offset 0). */
1880
+ function anchorPrior(anchor, inputWidth, inputHeight) {
1881
+ let base = 0;
1882
+ for (const stride of STRIDES) {
1883
+ const cols = Math.ceil(inputWidth / stride);
1884
+ const count = cols * Math.ceil(inputHeight / stride);
1885
+ if (anchor < base + count) {
1886
+ const i = anchor - base;
1887
+ return {
1888
+ x: i % cols * stride,
1889
+ y: Math.floor(i / cols) * stride,
1890
+ stride
1891
+ };
1892
+ }
1893
+ base += count;
1894
+ }
1895
+ throw new RangeError(`anchor ${anchor} is out of range`);
1896
+ }
1897
+ /**
1898
+ * Step 4: mask logits for one anchor, `maskHeight × maskWidth`, row-major.
1899
+ * Writes into `out` when it is large enough.
1900
+ */
1901
+ function maskLogits(head, anchor, out) {
1902
+ const { kernels, maskFeatures, maskWidth: mw, maskHeight: mh } = head;
1903
+ const plane = mw * mh;
1904
+ const logits = out && out.length >= plane ? out : new Float32Array(plane);
1905
+ const prior = anchorPrior(anchor, head.inputWidth, head.inputHeight);
1906
+ const k = kernels.subarray(anchor * KERNEL_SIZE, (anchor + 1) * KERNEL_SIZE);
1907
+ const w1 = 0;
1908
+ const w2 = 80;
1909
+ const w3 = 144;
1910
+ const b1 = 152;
1911
+ const b2 = 160;
1912
+ const b3 = 168;
1913
+ const scale = 1 / (prior.stride * MASK_STRIDE);
1914
+ const input = new Float32Array(2 + MASK_CHANNELS);
1915
+ const hidden1 = new Float32Array(8);
1916
+ const hidden2 = new Float32Array(8);
1917
+ for (let y = 0; y < mh; y++) {
1918
+ const relY = (prior.y - y * MASK_STRIDE) * scale;
1919
+ for (let x = 0; x < mw; x++) {
1920
+ const p = y * mw + x;
1921
+ input[0] = (prior.x - x * MASK_STRIDE) * scale;
1922
+ input[1] = relY;
1923
+ for (let c = 0; c < MASK_CHANNELS; c++) input[2 + c] = maskFeatures[c * plane + p];
1924
+ for (let o = 0; o < 8; o++) {
1925
+ let sum$1 = k[b1 + o];
1926
+ const row = w1 + o * 10;
1927
+ for (let i = 0; i < 10; i++) sum$1 += k[row + i] * input[i];
1928
+ hidden1[o] = sum$1 > 0 ? sum$1 : 0;
1929
+ }
1930
+ for (let o = 0; o < 8; o++) {
1931
+ let sum$1 = k[b2 + o];
1932
+ const row = w2 + o * 8;
1933
+ for (let i = 0; i < 8; i++) sum$1 += k[row + i] * hidden1[i];
1934
+ hidden2[o] = sum$1 > 0 ? sum$1 : 0;
1935
+ }
1936
+ let sum = k[b3];
1937
+ for (let i = 0; i < 8; i++) sum += k[w3 + i] * hidden2[i];
1938
+ logits[p] = sum;
1939
+ }
1940
+ }
1941
+ return logits;
1942
+ }
1943
+ /**
1944
+ * Minimal ONNX protobuf wire-format helpers: split a message into fields,
1945
+ * read strings, write length-delimited fields. Only what the graph editors
1946
+ * (onnx-subgraph.ts, onnx-float16.ts) need; unknown fields are copied as raw
1947
+ * bytes, so nothing else in a model is touched.
1948
+ */
1949
+ const MODEL_GRAPH = 7;
1950
+ const GRAPH_NODE = 1;
1951
+ const GRAPH_INITIALIZER = 5;
1952
+ const GRAPH_INPUT = 11;
1953
+ const GRAPH_OUTPUT = 12;
1954
+ const GRAPH_VALUE_INFO = 13;
1955
+ const NODE_INPUT = 1;
1956
+ const NODE_OUTPUT$1 = 2;
1957
+ const VALUE_INFO_NAME = 1;
1958
+ const TENSOR_NAME = 8;
1959
+ const WIRE_VARINT = 0;
1960
+ const WIRE_I64 = 1;
1961
+ const WIRE_LEN = 2;
1962
+ const WIRE_I32 = 5;
1963
+ function readVarint(buf, pos) {
1964
+ let value = 0;
1965
+ let scale = 1;
1966
+ for (;;) {
1967
+ if (pos >= buf.length) throw new Error("ONNX model: truncated varint");
1968
+ const byte = buf[pos++];
1969
+ value += (byte & 127) * scale;
1970
+ if (byte < 128) return [value, pos];
1971
+ scale *= 128;
1972
+ }
1973
+ }
1974
+ function fields(buf) {
1975
+ const out = [];
1976
+ let pos = 0;
1977
+ while (pos < buf.length) {
1978
+ const start = pos;
1979
+ const [tag, afterTag] = readVarint(buf, pos);
1980
+ pos = afterTag;
1981
+ const number = Math.floor(tag / 8);
1982
+ const wire = tag & 7;
1983
+ let body = buf.subarray(pos, pos);
1984
+ if (wire === WIRE_VARINT) pos = readVarint(buf, pos)[1];
1985
+ else if (wire === WIRE_I64) pos += 8;
1986
+ else if (wire === WIRE_I32) pos += 4;
1987
+ else if (wire === WIRE_LEN) {
1988
+ const [length, afterLength] = readVarint(buf, pos);
1989
+ body = buf.subarray(afterLength, afterLength + length);
1990
+ pos = afterLength + length;
1991
+ } else throw new Error(`ONNX model: unsupported wire type ${wire}`);
1992
+ if (pos > buf.length) throw new Error("ONNX model: truncated field");
1993
+ out.push({
1994
+ number,
1995
+ wire,
1996
+ raw: buf.subarray(start, pos),
1997
+ body
1998
+ });
1999
+ }
2000
+ return out;
2001
+ }
2002
+ const decoder = new TextDecoder();
2003
+ const encoder = new TextEncoder();
2004
+ function strings(buf, number) {
2005
+ return fields(buf).filter((f) => f.number === number && f.wire === WIRE_LEN).map((f) => decoder.decode(f.body));
2006
+ }
2007
+ function firstString(buf, number) {
2008
+ return strings(buf, number)[0];
2009
+ }
2010
+ function varint(value) {
2011
+ const bytes = [];
2012
+ while (value >= 128) {
2013
+ bytes.push(value % 128 | 128);
2014
+ value = Math.floor(value / 128);
2015
+ }
2016
+ bytes.push(value);
2017
+ return bytes;
2018
+ }
2019
+ function lengthDelimited(number, payload) {
2020
+ const head = [...varint(number * 8 + WIRE_LEN), ...varint(payload.length)];
2021
+ const out = new Uint8Array(head.length + payload.length);
2022
+ out.set(head, 0);
2023
+ out.set(payload, head.length);
2024
+ return out;
2025
+ }
2026
+ function concat(parts) {
2027
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
2028
+ let offset = 0;
2029
+ for (const part of parts) {
2030
+ out.set(part, offset);
2031
+ offset += part.length;
2032
+ }
2033
+ return out;
2034
+ }
2035
+ /**
2036
+ * Cuts an ONNX model down to the nodes that compute a chosen set of tensors,
2037
+ * which become the graph's outputs. Works on the protobuf wire format directly
2038
+ * (only the fields it needs are decoded; everything else is copied byte for
2039
+ * byte), so it needs no protobuf library and keeps the weights untouched.
2040
+ *
2041
+ * Used to run a detector's fixed-shape body on the GPU and do its
2042
+ * data-dependent tail (top-k, NMS, mask assembly) ourselves.
2043
+ */
2044
+ /** ValueInfoProto for a float tensor of unknown shape. */
2045
+ function floatOutput(name) {
2046
+ const typeProto = lengthDelimited(1, new Uint8Array([8, 1]));
2047
+ return concat([lengthDelimited(VALUE_INFO_NAME, encoder.encode(name)), lengthDelimited(2, typeProto)]);
2048
+ }
2049
+ /**
2050
+ * Returns a model whose outputs are `outputs` (tensors of the original graph),
2051
+ * keeping only the nodes, initializers and inputs needed to compute them.
2052
+ * Output types come from the graph's value_info when present; otherwise they
2053
+ * are declared as float tensors.
2054
+ */
2055
+ function extractSubgraph(model, outputs) {
2056
+ const modelFields = fields(model);
2057
+ const graphField = modelFields.find((f) => f.number === MODEL_GRAPH && f.wire === WIRE_LEN);
2058
+ if (!graphField) throw new Error("ONNX model has no graph");
2059
+ const graphFields = fields(graphField.body);
2060
+ const nodes = graphFields.filter((f) => f.number === GRAPH_NODE).map((f) => ({
2061
+ field: f,
2062
+ inputs: strings(f.body, NODE_INPUT),
2063
+ outputs: strings(f.body, NODE_OUTPUT$1)
2064
+ }));
2065
+ const producer = /* @__PURE__ */ new Map();
2066
+ nodes.forEach((node, index) => {
2067
+ for (const name of node.outputs) producer.set(name, index);
2068
+ });
2069
+ const keepNode = /* @__PURE__ */ new Set();
2070
+ const needed = /* @__PURE__ */ new Set();
2071
+ const stack = [...outputs];
2072
+ for (const name of outputs) if (!producer.has(name)) throw new Error(`ONNX model has no tensor '${name}'`);
2073
+ while (stack.length > 0) {
2074
+ const name = stack.pop();
2075
+ if (needed.has(name)) continue;
2076
+ needed.add(name);
2077
+ const index = producer.get(name);
2078
+ if (index === void 0 || keepNode.has(index)) continue;
2079
+ keepNode.add(index);
2080
+ for (const input of nodes[index].inputs) if (input) stack.push(input);
2081
+ }
2082
+ const valueInfo = /* @__PURE__ */ new Map();
2083
+ for (const f of graphFields) {
2084
+ if (f.number !== GRAPH_VALUE_INFO) continue;
2085
+ const name = firstString(f.body, VALUE_INFO_NAME);
2086
+ if (name !== void 0) valueInfo.set(name, f.body);
2087
+ }
2088
+ const parts = [];
2089
+ let nodeIndex = 0;
2090
+ for (const f of graphFields) switch (f.number) {
2091
+ case GRAPH_NODE:
2092
+ if (keepNode.has(nodeIndex)) parts.push(f.raw);
2093
+ nodeIndex++;
2094
+ break;
2095
+ case GRAPH_INITIALIZER: {
2096
+ const name = firstString(f.body, TENSOR_NAME);
2097
+ if (name !== void 0 && needed.has(name)) parts.push(f.raw);
2098
+ break;
2099
+ }
2100
+ case GRAPH_INPUT:
2101
+ case GRAPH_VALUE_INFO: {
2102
+ const name = firstString(f.body, VALUE_INFO_NAME);
2103
+ if (name !== void 0 && needed.has(name)) parts.push(f.raw);
2104
+ break;
2105
+ }
2106
+ case GRAPH_OUTPUT: break;
2107
+ default: parts.push(f.raw);
2108
+ }
2109
+ for (const name of outputs) parts.push(lengthDelimited(GRAPH_OUTPUT, valueInfo.get(name) ?? floatOutput(name)));
2110
+ const graph = concat(parts);
2111
+ return concat(modelFields.map((f) => f === graphField ? lengthDelimited(MODEL_GRAPH, graph) : f.raw));
2112
+ }
2113
+ /**
2114
+ * onnxruntime-web 1.30's WebGPU EP computes grouped convolutions wrong in its
2115
+ * default NHWC layout when a group has more than one channel and the count
2116
+ * isn't a multiple of 4 (RTMO's head: 18 per group → duplicate skeletons).
2117
+ * NCHW is correct but ~30% slower, so only models with such a convolution get
2118
+ * it. Depthwise convolutions (one channel per group: RTMDet-Ins, Selfie) are
2119
+ * fine in NHWC.
2120
+ */
2121
+ const NODE_OP_TYPE = 4;
2122
+ const NODE_ATTRIBUTE = 5;
2123
+ const NODE_OUTPUT = 2;
2124
+ const ATTR_NAME = 1;
2125
+ const ATTR_I = 3;
2126
+ const ATTR_T = 5;
2127
+ const TENSOR_DIMS = 1;
2128
+ function varintValues(field) {
2129
+ const values = [];
2130
+ if (field.wire === WIRE_LEN) {
2131
+ let pos = 0;
2132
+ while (pos < field.body.length) {
2133
+ const [value, next] = readVarint(field.body, pos);
2134
+ values.push(value);
2135
+ pos = next;
2136
+ }
2137
+ } else if (field.wire === WIRE_VARINT) {
2138
+ let pos = 0;
2139
+ pos = readVarint(field.raw, pos)[1];
2140
+ values.push(readVarint(field.raw, pos)[0]);
2141
+ }
2142
+ return values;
2143
+ }
2144
+ function tensorDims(tensor) {
2145
+ return fields(tensor).filter((f) => f.number === TENSOR_DIMS).flatMap(varintValues);
2146
+ }
2147
+ /**
2148
+ * True when the model has a grouped convolution the NHWC kernels get wrong,
2149
+ * and when it can't be read (NCHW is always correct).
2150
+ */
2151
+ function needsNchwLayout(model) {
2152
+ let graph;
2153
+ try {
2154
+ graph = fields(model).find((f) => f.number === MODEL_GRAPH && f.wire === WIRE_LEN);
2155
+ } catch {
2156
+ return true;
2157
+ }
2158
+ if (!graph) return true;
2159
+ const dims = /* @__PURE__ */ new Map();
2160
+ const convs = [];
2161
+ for (const f of fields(graph.body)) if (f.number === GRAPH_INITIALIZER) {
2162
+ const name = firstString(f.body, TENSOR_NAME);
2163
+ if (name !== void 0) dims.set(name, tensorDims(f.body));
2164
+ } else if (f.number === GRAPH_NODE) {
2165
+ const op = firstString(f.body, NODE_OP_TYPE);
2166
+ if (op !== "Conv" && op !== "Constant") continue;
2167
+ const attributes = fields(f.body).filter((a) => a.number === NODE_ATTRIBUTE).map((a) => a.body);
2168
+ if (op === "Constant") {
2169
+ const value = attributes.find((a) => firstString(a, ATTR_NAME) === "value");
2170
+ const tensor = value && fields(value).find((a) => a.number === ATTR_T);
2171
+ const output$1 = strings(f.body, NODE_OUTPUT)[0];
2172
+ if (tensor && output$1) dims.set(output$1, tensorDims(tensor.body));
2173
+ continue;
2174
+ }
2175
+ const groupAttr = attributes.find((a) => firstString(a, ATTR_NAME) === "group");
2176
+ const groupField = groupAttr && fields(groupAttr).find((a) => a.number === ATTR_I);
2177
+ const group = groupField ? varintValues(groupField)[0] ?? 1 : 1;
2178
+ const weight = strings(f.body, NODE_INPUT)[1];
2179
+ if (group > 1 && weight) convs.push({
2180
+ weight,
2181
+ group
2182
+ });
2183
+ }
2184
+ return convs.some(({ weight }) => {
2185
+ const perGroup = dims.get(weight)?.[1];
2186
+ return perGroup !== void 0 && perGroup > 1 && perGroup % 4 !== 0;
2187
+ });
2188
+ }
2189
+ /**
2190
+ * Browser / WebGPU session provider .
2191
+ *
2192
+ * Mirrors `NodeSessionProvider` but uses `onnxruntime-web` with the WebGPU execution provider
2193
+ * and a WASM fallback. The ORT module is imported lazily behind an injectable loader, so:
2194
+ * - the Node core entry never pulls browser code, and
2195
+ * - unit tests can run fully offline with a fake module.
2196
+ *
2197
+ * `onnxruntime-web` is an OPTIONAL peer dependency — consumers of `@framefields/vision/web`
2198
+ * provide it; the main `@framefields/vision` entry never touches it.
2199
+ */
1477
2200
  const ORT_WEB_SPECIFIER = "onnxruntime-web";
1478
2201
  const defaultLoader = () => import(
1479
2202
  /* @vite-ignore */
@@ -1485,10 +2208,12 @@ var WebGPUProvider = class {
1485
2208
  _providers;
1486
2209
  _wasmFallback;
1487
2210
  _ortPromise;
2211
+ device;
1488
2212
  constructor(options = {}) {
1489
2213
  this._loader = options.loader ?? defaultLoader;
1490
2214
  this._providers = options.executionProviders ?? ["webgpu", "wasm"];
1491
2215
  this._wasmFallback = options.wasmFallback !== false;
2216
+ this.device = options.device;
1492
2217
  }
1493
2218
  ort() {
1494
2219
  if (!this._ortPromise) this._ortPromise = this._loader().catch((err) => {
@@ -1501,8 +2226,10 @@ var WebGPUProvider = class {
1501
2226
  const ort = await this.ort();
1502
2227
  let session;
1503
2228
  try {
2229
+ const device = this.device;
2230
+ const layout = needsNchwLayout(modelBytes) ? "NCHW" : "NHWC";
1504
2231
  session = await ort.InferenceSession.create(modelBytes, {
1505
- executionProviders: [...this._providers],
2232
+ executionProviders: this._providers.map((name) => withProviderOptions(name, device, layout)),
1506
2233
  graphOptimizationLevel: "all",
1507
2234
  logSeverityLevel: 3
1508
2235
  });
@@ -1517,11 +2244,15 @@ var WebGPUProvider = class {
1517
2244
  return {
1518
2245
  run: async (input) => {
1519
2246
  const inputName = session.inputNames.includes(input.name) ? input.name : session.inputNames[0] ?? input.name;
1520
- const results = await session.run({ [inputName]: new ort.Tensor("float32", input.data, [...input.dims]) });
2247
+ const tensor = input.gpuBuffer && ort.Tensor.fromGpuBuffer ? ort.Tensor.fromGpuBuffer(input.gpuBuffer, {
2248
+ dataType: "float32",
2249
+ dims: [...input.dims]
2250
+ }) : new ort.Tensor("float32", input.data, [...input.dims]);
2251
+ const results = await session.run({ [inputName]: tensor });
1521
2252
  const outputs = {};
1522
- for (const [name, tensor] of Object.entries(results)) outputs[name] = {
1523
- data: tensor.data,
1524
- dims: [...tensor.dims]
2253
+ for (const [name, tensor$1] of Object.entries(results)) outputs[name] = {
2254
+ data: tensor$1.data,
2255
+ dims: [...tensor$1.dims]
1525
2256
  };
1526
2257
  return outputs;
1527
2258
  },
@@ -1531,6 +2262,23 @@ var WebGPUProvider = class {
1531
2262
  };
1532
2263
  }
1533
2264
  };
2265
+ /**
2266
+ * The WebGPU EP runs in NCHW. Its default NHWC layout computes grouped convolutions wrong
2267
+ * when the channels per group aren't a multiple of 4 (onnxruntime-web 1.30) — RTMO's head
2268
+ * has group=8 convs over 104 channels (13 per group), which turned one dancer into several
2269
+ * skeletons. NCHW matches the CPU EP on every pinned model.
2270
+ */
2271
+ function withProviderOptions(name, device, preferredLayout) {
2272
+ if (name !== "webgpu") return name;
2273
+ return device ? {
2274
+ name,
2275
+ preferredLayout,
2276
+ device
2277
+ } : {
2278
+ name,
2279
+ preferredLayout
2280
+ };
2281
+ }
1534
2282
  const ORT_WEBGPU_SPECIFIER = "onnxruntime-web/webgpu";
1535
2283
  const WEBGPU_SPECIFIER = "webgpu";
1536
2284
  let nodeWebGPUReady;
@@ -1609,6 +2357,7 @@ function patchSupportedFeatures(features) {
1609
2357
  }
1610
2358
  var NodeWebGPUProvider = class {
1611
2359
  kind = "webgpu";
2360
+ device;
1612
2361
  _inner;
1613
2362
  constructor(options = {}) {
1614
2363
  const loader = options.loader ?? (async () => {
@@ -1621,6 +2370,7 @@ var NodeWebGPUProvider = class {
1621
2370
  if (mod.env?.webgpu) mod.env.webgpu.powerPreference = "high-performance";
1622
2371
  return mod;
1623
2372
  });
2373
+ this.device = options.device;
1624
2374
  this._inner = new WebGPUProvider({
1625
2375
  wasmFallback: false,
1626
2376
  ...options,
@@ -1632,6 +2382,27 @@ var NodeWebGPUProvider = class {
1632
2382
  }
1633
2383
  };
1634
2384
  let defaultProvider;
2385
+ let visionDevice;
2386
+ /**
2387
+ * The GPU device the default provider runs WebGPU sessions on — the renderer's,
2388
+ * so frames can go from its textures into the models without a CPU round trip.
2389
+ * Applies to sessions created afterwards; without it, onnxruntime-web creates
2390
+ * its own device.
2391
+ */
2392
+ function setVisionDevice(device) {
2393
+ visionDevice = device;
2394
+ }
2395
+ /**
2396
+ * Hosts (the renderer) register a listener under this symbol to log the
2397
+ * backend; a global keeps it working however the packages are bundled.
2398
+ */
2399
+ const VISION_BACKEND_LISTENER = Symbol.for("framefields.vision.backend");
2400
+ function reportBackend(info) {
2401
+ const listener = globalThis[VISION_BACKEND_LISTENER];
2402
+ if (typeof listener === "function") try {
2403
+ listener(info);
2404
+ } catch {}
2405
+ }
1635
2406
  function requestedVisionBackend() {
1636
2407
  const value = typeof process !== "undefined" ? process.env.FRAMEFIELDS_VISION_PROVIDER?.trim().toLowerCase() : void 0;
1637
2408
  return value === "webgpu" || value === "cuda" || value === "cpu" ? value : "auto";
@@ -1653,6 +2424,9 @@ var AutoSessionProvider = class {
1653
2424
  get kind() {
1654
2425
  return this._resolved?.kind ?? "webgpu";
1655
2426
  }
2427
+ get device() {
2428
+ return this._resolved?.device;
2429
+ }
1656
2430
  async createSession(modelBytes) {
1657
2431
  if (this._resolved) return this._resolved.createSession(modelBytes);
1658
2432
  if (!this._resolving) {
@@ -1670,16 +2444,42 @@ var AutoSessionProvider = class {
1670
2444
  }
1671
2445
  async resolve(modelBytes) {
1672
2446
  const backend = requestedVisionBackend();
1673
- if (backend === "cpu") return { provider: new NodeSessionProvider() };
1674
- if (backend === "cuda") return { provider: new NodeSessionProvider({ executionProviders: ["cuda", "cpu"] }) };
1675
- const webgpu = new NodeWebGPUProvider({ wasmFallback: false });
2447
+ if (backend === "cpu") {
2448
+ reportBackend({
2449
+ backend: "cpu",
2450
+ sharedDevice: false,
2451
+ reason: "FRAMEFIELDS_VISION_PROVIDER=cpu"
2452
+ });
2453
+ return { provider: new NodeSessionProvider() };
2454
+ }
2455
+ if (backend === "cuda") {
2456
+ reportBackend({
2457
+ backend: "cuda",
2458
+ sharedDevice: false
2459
+ });
2460
+ return { provider: new NodeSessionProvider({ executionProviders: ["cuda", "cpu"] }) };
2461
+ }
2462
+ const webgpu = new NodeWebGPUProvider({
2463
+ wasmFallback: false,
2464
+ device: visionDevice
2465
+ });
1676
2466
  try {
2467
+ const first = await webgpu.createSession(modelBytes);
2468
+ reportBackend({
2469
+ backend: "webgpu",
2470
+ sharedDevice: !!visionDevice
2471
+ });
1677
2472
  return {
1678
2473
  provider: webgpu,
1679
- first: await webgpu.createSession(modelBytes)
2474
+ first
1680
2475
  };
1681
2476
  } catch (err) {
1682
2477
  if (backend === "webgpu") throw err;
2478
+ reportBackend({
2479
+ backend: "cpu",
2480
+ sharedDevice: false,
2481
+ reason: err instanceof Error ? err.message : String(err)
2482
+ });
1683
2483
  return { provider: new NodeSessionProvider() };
1684
2484
  }
1685
2485
  }
@@ -1775,6 +2575,9 @@ var NodeSessionProvider = class {
1775
2575
  };
1776
2576
  }
1777
2577
  };
2578
+ function isGpuImage(image) {
2579
+ return image.texture !== void 0;
2580
+ }
1778
2581
  const MAX_INSTANCE_PASSES = 4;
1779
2582
  /**
1780
2583
  * Lazy vision runner.
@@ -1811,6 +2614,7 @@ var VisionRunner = class VisionRunner$1 {
1811
2614
  sessions: /* @__PURE__ */ new Map(),
1812
2615
  tensorScratch: /* @__PURE__ */ new Map(),
1813
2616
  runQueue: /* @__PURE__ */ new Map(),
2617
+ gpuInputs: /* @__PURE__ */ new Map(),
1814
2618
  instancePasses: []
1815
2619
  };
1816
2620
  }
@@ -1865,7 +2669,8 @@ var VisionRunner = class VisionRunner$1 {
1865
2669
  const pass = await this.instancePass(image);
1866
2670
  return decodeRtmdetIns({
1867
2671
  ...pass.outputs,
1868
- masks: void 0
2672
+ masks: void 0,
2673
+ maskPlane: void 0
1869
2674
  }, this.instanceDecodeOptions(image, pass.transform)).detections;
1870
2675
  }
1871
2676
  /** COCO-80 boxes plus a frame-aligned soft mask per instance. */
@@ -1909,30 +2714,21 @@ var VisionRunner = class VisionRunner$1 {
1909
2714
  for (const promise of core.sessions.values()) promise.then((session) => session.release()).catch(() => void 0);
1910
2715
  core.sessions.clear();
1911
2716
  core.runQueue.clear();
2717
+ for (const { buffer } of core.gpuInputs.values()) buffer.destroy();
2718
+ core.gpuInputs.clear();
1912
2719
  core.instancePasses.length = 0;
1913
2720
  core.store.clearMemoryCache();
1914
2721
  }
1915
- instancePass(image) {
2722
+ async instancePass(image) {
1916
2723
  const passes = this._core.instancePasses;
1917
- const content = contentKey(image);
2724
+ const content = isGpuImage(image) ? await FrameTensorPipeline.for(image.device).contentKey(image) : contentKey(image);
1918
2725
  const byContent = passes.find((pass$1) => pass$1.content === content);
1919
2726
  if (byContent) return byContent.result;
1920
2727
  const key = this.modelKeyFor("segment");
1921
- const result = this.infer(key, image).then(({ outputs, transform }) => {
1922
- const dets = output(outputs, "dets", key);
1923
- const masks = output(outputs, "masks", key);
1924
- return {
1925
- transform,
1926
- outputs: {
1927
- dets: dets.data,
1928
- labels: output(outputs, "labels", key).data,
1929
- masks: masks.data,
1930
- count: dets.dims[1] ?? 0,
1931
- maskHeight: masks.dims[masks.dims.length - 2] ?? 0,
1932
- maskWidth: masks.dims[masks.dims.length - 1] ?? 0
1933
- }
1934
- };
1935
- });
2728
+ const result = this.infer(key, image).then(({ outputs, transform }) => ({
2729
+ transform,
2730
+ outputs: RTMDET_INS_HEAD.scores in outputs ? headPassOutputs(outputs, key, transform) : end2endPassOutputs(outputs, key)
2731
+ }));
1936
2732
  const pass = {
1937
2733
  content,
1938
2734
  result
@@ -1971,10 +2767,33 @@ var VisionRunner = class VisionRunner$1 {
1971
2767
  });
1972
2768
  return run;
1973
2769
  }
1974
- async inferNow(key, image) {
2770
+ async inferNow(key, frame) {
1975
2771
  const session = await this.session(key);
1976
2772
  const desc = VISION_MODELS[key];
1977
2773
  const spec = preprocessSpec(desc.family, desc.input);
2774
+ const dims = [
2775
+ 1,
2776
+ 3,
2777
+ spec.height,
2778
+ spec.width
2779
+ ];
2780
+ let image;
2781
+ if (isGpuImage(frame)) {
2782
+ if (this._core.provider.device === frame.device) {
2783
+ const buffer = this.gpuInput(key, frame.device, spec);
2784
+ const transform$1 = FrameTensorPipeline.for(frame.device).toTensor(frame, spec, buffer);
2785
+ return {
2786
+ outputs: await session.run({
2787
+ name: "input",
2788
+ data: EMPTY_TENSOR,
2789
+ gpuBuffer: buffer,
2790
+ dims
2791
+ }),
2792
+ transform: transform$1
2793
+ };
2794
+ }
2795
+ image = await readTexturePixels(frame);
2796
+ } else image = frame;
1978
2797
  const size = 3 * spec.width * spec.height;
1979
2798
  const scratchByKey = this._core.tensorScratch;
1980
2799
  let scratch = scratchByKey.get(key);
@@ -1987,21 +2806,33 @@ var VisionRunner = class VisionRunner$1 {
1987
2806
  outputs: await session.run({
1988
2807
  name: "input",
1989
2808
  data: tensor,
1990
- dims: [
1991
- 1,
1992
- 3,
1993
- spec.height,
1994
- spec.width
1995
- ]
2809
+ dims
1996
2810
  }),
1997
2811
  transform
1998
2812
  };
1999
2813
  }
2814
+ gpuInput(key, device, spec) {
2815
+ const inputs = this._core.gpuInputs;
2816
+ const size = FrameTensorPipeline.tensorBytes(spec);
2817
+ const current = inputs.get(key);
2818
+ if (current?.device === device && current.buffer.size >= size) return current.buffer;
2819
+ current?.buffer.destroy();
2820
+ const buffer = device.createBuffer({
2821
+ label: `vision_input_${key}`,
2822
+ size,
2823
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
2824
+ });
2825
+ inputs.set(key, {
2826
+ device,
2827
+ buffer
2828
+ });
2829
+ return buffer;
2830
+ }
2000
2831
  session(key) {
2001
2832
  const { sessions, store, provider } = this._core;
2002
2833
  let promise = sessions.get(key);
2003
2834
  if (!promise) {
2004
- promise = store.ensure(key).then((bytes) => provider.createSession(bytes)).catch((err) => {
2835
+ promise = store.ensure(key).then((bytes) => provider.createSession(VISION_MODELS[key].family === "rtmdet-ins" ? rtmdetInsBody(bytes) : bytes)).catch((err) => {
2005
2836
  sessions.delete(key);
2006
2837
  store.evict(key);
2007
2838
  throw err;
@@ -2011,6 +2842,74 @@ var VisionRunner = class VisionRunner$1 {
2011
2842
  return promise;
2012
2843
  }
2013
2844
  };
2845
+ const EMPTY_TENSOR = new Float32Array(0);
2846
+ /**
2847
+ * The RTMDet-Ins graph up to its head (see `RTMDET_INS_HEAD`): no NMS or
2848
+ * full-size masks in the graph, so the GPU part has fixed shapes and only
2849
+ * ~9 MB comes back. Models without those tensors run end to end.
2850
+ */
2851
+ function rtmdetInsBody(bytes) {
2852
+ try {
2853
+ return extractSubgraph(bytes, Object.values(RTMDET_INS_HEAD));
2854
+ } catch {
2855
+ return bytes;
2856
+ }
2857
+ }
2858
+ /** Outputs of the end2end graph: NMS done, one input-sized mask per detection. */
2859
+ function end2endPassOutputs(outputs, key) {
2860
+ const dets = output(outputs, "dets", key);
2861
+ const masks = output(outputs, "masks", key);
2862
+ return {
2863
+ dets: dets.data,
2864
+ labels: output(outputs, "labels", key).data,
2865
+ masks: masks.data,
2866
+ count: dets.dims[1] ?? 0,
2867
+ maskHeight: masks.dims[masks.dims.length - 2] ?? 0,
2868
+ maskWidth: masks.dims[masks.dims.length - 1] ?? 0
2869
+ };
2870
+ }
2871
+ /** Outputs of the head: selection here, mask logits only for detections asked for. */
2872
+ function headPassOutputs(outputs, key, transform) {
2873
+ const scores = output(outputs, RTMDET_INS_HEAD.scores, key);
2874
+ const features = output(outputs, RTMDET_INS_HEAD.maskFeatures, key);
2875
+ const head = {
2876
+ scores: scores.data,
2877
+ boxes: output(outputs, RTMDET_INS_HEAD.boxes, key).data,
2878
+ kernels: output(outputs, RTMDET_INS_HEAD.kernels, key).data,
2879
+ maskFeatures: features.data,
2880
+ anchors: scores.dims[1] ?? 0,
2881
+ classes: scores.dims[2] ?? 0,
2882
+ maskHeight: features.dims[2] ?? 0,
2883
+ maskWidth: features.dims[3] ?? 0,
2884
+ inputWidth: transform.inputWidth,
2885
+ inputHeight: transform.inputHeight
2886
+ };
2887
+ const picked = selectInstances(head);
2888
+ const dets = new Float32Array(picked.length * 5);
2889
+ const labels = new Int32Array(picked.length);
2890
+ picked.forEach(({ anchor, classIndex, score }, i) => {
2891
+ dets.set(head.boxes.subarray(anchor * 4, anchor * 4 + 4), i * 5);
2892
+ dets[i * 5 + 4] = score;
2893
+ labels[i] = classIndex;
2894
+ });
2895
+ const planes = /* @__PURE__ */ new Map();
2896
+ return {
2897
+ dets,
2898
+ labels,
2899
+ count: picked.length,
2900
+ maskWidth: head.maskWidth,
2901
+ maskHeight: head.maskHeight,
2902
+ maskLogits: true,
2903
+ maskPlane: (index) => {
2904
+ let plane = planes.get(index);
2905
+ if (!plane) {
2906
+ plane = maskLogits(head, picked[index].anchor);
2907
+ planes.set(index, plane);
2908
+ }
2909
+ return plane;
2910
+ }
2911
+ };
2912
+ }
2014
2913
  /** Size plus a 64-bit hash of the pixels: equal keys mean the same picture. */
2015
2914
  function contentKey(image) {
2016
2915
  const { data } = image;
@@ -2217,16 +3116,16 @@ function matchPoseToTracks(people, trackedObjects, minIoU = .15) {
2217
3116
  const pBox = getPersonBoundingBox(person);
2218
3117
  const isNormalized = pBox.width <= 1.05 && pBox.height <= 1.05;
2219
3118
  for (const trk of personTracks) {
2220
- const iou = computeIoU(pBox, isNormalized && trk.boundingBox.normalizedWidth > 0 ? {
3119
+ const iou$1 = computeIoU(pBox, isNormalized && trk.boundingBox.normalizedWidth > 0 ? {
2221
3120
  originX: trk.boundingBox.normalizedX,
2222
3121
  originY: trk.boundingBox.normalizedY,
2223
3122
  width: trk.boundingBox.normalizedWidth,
2224
3123
  height: trk.boundingBox.normalizedHeight
2225
3124
  } : trk.boundingBox);
2226
- if (iou >= minIoU) candidateMatches.push({
3125
+ if (iou$1 >= minIoU) candidateMatches.push({
2227
3126
  personIdx: p,
2228
3127
  trackId: trk.trackId,
2229
- iou
3128
+ iou: iou$1
2230
3129
  });
2231
3130
  }
2232
3131
  }
@@ -2427,14 +3326,14 @@ var VisionBundle = class {
2427
3326
  let bestIoU = .1;
2428
3327
  for (const p of poseRes.people) {
2429
3328
  const pBox = getPersonBoundingBox(p);
2430
- const iou = computeIoU(pBox, pBox.width <= 1.05 && pBox.height <= 1.05 && trk.boundingBox.normalizedWidth > 0 ? {
3329
+ const iou$1 = computeIoU(pBox, pBox.width <= 1.05 && pBox.height <= 1.05 && trk.boundingBox.normalizedWidth > 0 ? {
2431
3330
  originX: trk.boundingBox.normalizedX,
2432
3331
  originY: trk.boundingBox.normalizedY,
2433
3332
  width: trk.boundingBox.normalizedWidth,
2434
3333
  height: trk.boundingBox.normalizedHeight
2435
3334
  } : trk.boundingBox);
2436
- if (iou > bestIoU) {
2437
- bestIoU = iou;
3335
+ if (iou$1 > bestIoU) {
3336
+ bestIoU = iou$1;
2438
3337
  bestPerson = p;
2439
3338
  }
2440
3339
  }
@@ -3197,7 +4096,7 @@ var VisionNode = class VisionNode$1 {
3197
4096
  };
3198
4097
 
3199
4098
  //#endregion
3200
- //#region ../../nodes/node-vision/dist/renderers-BcBWZ-Rc.mjs
4099
+ //#region ../../nodes/node-vision/dist/renderers-CPLmY2dA.mjs
3201
4100
  let scratchValues = new Uint8Array(0);
3202
4101
  let scratchLengths = new Uint32Array(0);
3203
4102
  function encodeMask(mask, width, height) {
@@ -3404,7 +4303,7 @@ var MatteCompositor = class {
3404
4303
  return tex;
3405
4304
  }
3406
4305
  /** Clears `target` and draws `picture` through `mask` into it. */
3407
- draw(encoder, target, picture, mask, mode, bounds) {
4306
+ draw(encoder$1, target, picture, mask, mode, bounds) {
3408
4307
  const w = mask.width;
3409
4308
  const h = mask.height;
3410
4309
  const data = new Float32Array(8);
@@ -3450,7 +4349,7 @@ var MatteCompositor = class {
3450
4349
  }
3451
4350
  ]
3452
4351
  });
3453
- const pass = encoder.beginRenderPass({
4352
+ const pass = encoder$1.beginRenderPass({
3454
4353
  label: "vision_matte_composite",
3455
4354
  colorAttachments: [{
3456
4355
  view: target,
@@ -3484,8 +4383,9 @@ let sharedCompositorFormat = null;
3484
4383
  const frameCache = new VisionFrameCache();
3485
4384
  /** The frame each node's child texture holds (drawn on its last call). */
3486
4385
  const heldFrameByNode = /* @__PURE__ */ new Map();
3487
- /** Per node: a copy of the held frame, and the composited matte. */
4386
+ /** Per node: a copy of the held frame, the frame being analysed, and the composited matte. */
3488
4387
  const heldTextures = /* @__PURE__ */ new Map();
4388
+ const analysisTextures = /* @__PURE__ */ new Map();
3489
4389
  const matteTextures = /* @__PURE__ */ new Map();
3490
4390
  /** One tracker per vision node — track ids must not mix across nodes/sources. */
3491
4391
  const objectTrackers = /* @__PURE__ */ new Map();
@@ -3524,6 +4424,7 @@ function nodeTexture(textures, device, key, like, usage, label) {
3524
4424
  return tex;
3525
4425
  }
3526
4426
  const heldTexture = (device, key, like) => nodeTexture(heldTextures, device, key, like, GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, "vision_held_frame");
4427
+ const analysisTexture = (device, key, like) => nodeTexture(analysisTextures, device, key, like, GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC, "vision_analysis_frame");
3527
4428
  const matteTexture = (device, key, like) => nodeTexture(matteTextures, device, key, like, GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC, "vision_matte");
3528
4429
  /** Everything that decides a frame's results besides the frame itself. */
3529
4430
  function cacheSource(nodeKey, child, op, width, height) {
@@ -3825,8 +4726,9 @@ function maskCentroid(mask) {
3825
4726
  };
3826
4727
  }
3827
4728
  const VisionWebGPURenderer = async (args) => {
3828
- const { ctx, encoder, pass, targetView, targetWidth, targetHeight, props, drawChild } = args;
4729
+ const { ctx, encoder: encoder$1, pass, targetView, targetWidth, targetHeight, props, drawChild } = args;
3829
4730
  const { virtualMedia } = props;
4731
+ setVisionDevice(ctx.device);
3830
4732
  const rawOp = virtualMedia?.operation;
3831
4733
  if (rawOp?.op !== "Vision") return;
3832
4734
  const op = rawOp;
@@ -3874,11 +4776,11 @@ const VisionWebGPURenderer = async (args) => {
3874
4776
  const wantsPose = op.enablePose === true || mode === "skeleton";
3875
4777
  const cacheable = !wantsSelfie && !wantsPose && (wantsDetection || wantsSegmentation);
3876
4778
  const source = cacheable ? cacheSource(nodeKeyStr, childMedia, op, targetWidth, targetHeight) : "";
3877
- const keyThresholdAt = (frame) => op.keyBackground === true ? backgroundKeyThreshold(op, compositionFrame - (frameIdx - frame), fps) : void 0;
3878
- const lookup = (frame) => {
3879
- if (!cacheable || frame === void 0) return void 0;
3880
- const hit = frameCache.get(source, frame);
3881
- if (!hit || hit.keyThreshold !== keyThresholdAt(frame)) return void 0;
4779
+ const keyThresholdAt = (frame$1) => op.keyBackground === true ? backgroundKeyThreshold(op, compositionFrame - (frameIdx - frame$1), fps) : void 0;
4780
+ const lookup = (frame$1) => {
4781
+ if (!cacheable || frame$1 === void 0) return void 0;
4782
+ const hit = frameCache.get(source, frame$1);
4783
+ if (!hit || hit.keyThreshold !== keyThresholdAt(frame$1)) return void 0;
3882
4784
  if (wantsSegmentation && visionBundle && !hit.masks) return void 0;
3883
4785
  return hit;
3884
4786
  };
@@ -3889,43 +4791,33 @@ const VisionWebGPURenderer = async (args) => {
3889
4791
  let heldTex;
3890
4792
  if (isMatteMode && hasPreviousFrame && !exact) {
3891
4793
  heldTex = heldTexture(ctx.device, nodeKeyStr, childTex);
3892
- encoder.copyTextureToTexture({ texture: childTex }, { texture: heldTex }, [
4794
+ encoder$1.copyTextureToTexture({ texture: childTex }, { texture: heldTex }, [
3893
4795
  targetWidth,
3894
4796
  targetHeight,
3895
4797
  1
3896
4798
  ]);
3897
4799
  }
4800
+ let frame;
3898
4801
  let framePixels = null;
3899
4802
  if (hasPreviousFrame && !cached) {
3900
- const unalignedBytesPerRow = targetWidth * 4;
3901
- const bytesPerRow = Math.ceil(unalignedBytesPerRow / 256) * 256;
3902
- const bufferSize = bytesPerRow * targetHeight;
3903
- const stagingBuffer = ctx.device.createBuffer({
3904
- size: bufferSize,
3905
- usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
3906
- label: "vision_frame_staging"
3907
- });
3908
- const readbackEncoder = ctx.device.createCommandEncoder({ label: "vision_readback_encoder" });
3909
- readbackEncoder.copyTextureToBuffer({ texture: childTex }, {
3910
- buffer: stagingBuffer,
3911
- bytesPerRow,
3912
- rowsPerImage: targetHeight
3913
- }, [
4803
+ const analysis = analysisTexture(ctx.device, nodeKeyStr, childTex);
4804
+ const copy = ctx.device.createCommandEncoder({ label: "vision_analysis_copy" });
4805
+ copy.copyTextureToTexture({ texture: childTex }, { texture: analysis }, [
3914
4806
  targetWidth,
3915
4807
  targetHeight,
3916
4808
  1
3917
4809
  ]);
3918
- ctx.device.queue.submit([readbackEncoder.finish()]);
3919
- await stagingBuffer.mapAsync(GPUMapMode.READ);
3920
- const mappedBytes = new Uint8Array(stagingBuffer.getMappedRange());
3921
- framePixels = new Uint8ClampedArray(targetWidth * targetHeight * 4);
3922
- if (bytesPerRow === unalignedBytesPerRow) framePixels.set(mappedBytes.subarray(0, framePixels.length));
3923
- else for (let y = 0; y < targetHeight; y++) framePixels.set(mappedBytes.subarray(y * bytesPerRow, y * bytesPerRow + unalignedBytesPerRow), y * unalignedBytesPerRow);
3924
- stagingBuffer.unmap();
3925
- stagingBuffer.destroy();
4810
+ ctx.device.queue.submit([copy.finish()]);
4811
+ frame = {
4812
+ texture: analysis,
4813
+ device: ctx.device,
4814
+ width: targetWidth,
4815
+ height: targetHeight
4816
+ };
4817
+ if (isMatteMode && op.keyBackground === true) framePixels = (await readTexturePixels(frame)).data;
3926
4818
  }
3927
4819
  const childView = childTex.createView();
3928
- ctx.renderer.beginFrame(encoder, childView, {
4820
+ ctx.renderer.beginFrame(encoder$1, childView, {
3929
4821
  r: 0,
3930
4822
  g: 0,
3931
4823
  b: 0,
@@ -3955,13 +4847,9 @@ const VisionWebGPURenderer = async (args) => {
3955
4847
  frame: frameIdx
3956
4848
  });
3957
4849
  }
3958
- } else if (framePixels) {
4850
+ } else if (frame) {
3959
4851
  const runner = getSharedRunner(op);
3960
- const image = {
3961
- data: framePixels,
3962
- width: targetWidth,
3963
- height: targetHeight
3964
- };
4852
+ const image = frame;
3965
4853
  let detections = [];
3966
4854
  if (wantsDetection) {
3967
4855
  detections = await runner.detect(image);
@@ -3988,7 +4876,7 @@ const VisionWebGPURenderer = async (args) => {
3988
4876
  if (isMatteMode) {
3989
4877
  subject = selectSubject(op.matteSource === "selfie" ? personMatteAsMask(personMatte) : mergeSubjectMask(frameMasks.length > 0 ? frameMasks : visionBundle?.getMaskResult(frameIdx) ?? []), nodeKeyStr, frameIdx);
3990
4878
  const threshold = keyThresholdAt(heldFrame ?? frameIdx);
3991
- if (subject && threshold !== void 0) subject = {
4879
+ if (subject && threshold !== void 0 && framePixels) subject = {
3992
4880
  ...subject,
3993
4881
  mask: growMaskIntoForeground(subject.mask, subject.width, subject.height, framePixels, threshold, op.featherRadius)
3994
4882
  };
@@ -4004,7 +4892,7 @@ const VisionWebGPURenderer = async (args) => {
4004
4892
  if (isMatteMode) {
4005
4893
  const picture = exact ? childTex : heldTex;
4006
4894
  if (!subject || !picture) {
4007
- ctx.renderer.beginFrame(encoder, targetView, {
4895
+ ctx.renderer.beginFrame(encoder$1, targetView, {
4008
4896
  r: 0,
4009
4897
  g: 0,
4010
4898
  b: 0,
@@ -4018,9 +4906,9 @@ const VisionWebGPURenderer = async (args) => {
4018
4906
  }
4019
4907
  const maskTex = sharedCompositor.uploadMask(nodeKeyStr, subject.mask, subject.width, subject.height);
4020
4908
  const matteTex = matteTexture(ctx.device, nodeKeyStr, childTex);
4021
- sharedCompositor.draw(encoder, matteTex.createView(), picture, maskTex, mode, mode === "crop" ? maskBounds(subject) ?? void 0 : void 0);
4909
+ sharedCompositor.draw(encoder$1, matteTex.createView(), picture, maskTex, mode, mode === "crop" ? maskBounds(subject) ?? void 0 : void 0);
4022
4910
  visionBundle?.setStencilTexture(matteTex);
4023
- const outPass$1 = ctx.renderer.beginFrame(encoder, targetView, {
4911
+ const outPass$1 = ctx.renderer.beginFrame(encoder$1, targetView, {
4024
4912
  r: 0,
4025
4913
  g: 0,
4026
4914
  b: 0,
@@ -4043,7 +4931,7 @@ const VisionWebGPURenderer = async (args) => {
4043
4931
  width: targetWidth,
4044
4932
  height: targetHeight
4045
4933
  });
4046
- const skelPass = ctx.renderer.beginFrame(encoder, targetView, {
4934
+ const skelPass = ctx.renderer.beginFrame(encoder$1, targetView, {
4047
4935
  r: 0,
4048
4936
  g: 0,
4049
4937
  b: 0,
@@ -4060,7 +4948,7 @@ const VisionWebGPURenderer = async (args) => {
4060
4948
  }
4061
4949
  }
4062
4950
  if (mode === "boxes" || mode === "tracking") {
4063
- const outPass$1 = ctx.renderer.beginFrame(encoder, targetView, {
4951
+ const outPass$1 = ctx.renderer.beginFrame(encoder$1, targetView, {
4064
4952
  r: 0,
4065
4953
  g: 0,
4066
4954
  b: 0,
@@ -4127,7 +5015,7 @@ const VisionWebGPURenderer = async (args) => {
4127
5015
  outPass$1.end();
4128
5016
  return;
4129
5017
  }
4130
- const outPass = ctx.renderer.beginFrame(encoder, targetView, {
5018
+ const outPass = ctx.renderer.beginFrame(encoder$1, targetView, {
4131
5019
  r: 0,
4132
5020
  g: 0,
4133
5021
  b: 0,
@@ -4159,4 +5047,4 @@ var renderers_default = defineRenderer({ WebGPURenderer: VisionWebGPURenderer })
4159
5047
 
4160
5048
  //#endregion
4161
5049
  export { PoseSkeletonRenderer as a, TemporalObjectTracker as c, VisionRunner as d, analyzeSequence as f, COCO_CLASSES as h, COCO17_KEYPOINT_NAMES as i, VisionBundle as l, pinNodeToObject as m, COCO17_BONES as n, SegmentationTexturePool as o, pinNodeToLandmark as p, COCO17_KEYPOINTS as r, SpatialLandmarkTransformer as s, renderers_default as t, VisionNode as u };
4162
- //# sourceMappingURL=renderer-dc39dBVA.mjs.map
5050
+ //# sourceMappingURL=renderer-CqT2vxa4.mjs.map