@framefields/node-vision 2.0.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.
- package/.turbo/turbo-build.log +20 -0
- package/CHANGELOG.md +23 -0
- package/LICENCE +202 -0
- package/dist/index.d.mts +123 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +73 -0
- package/dist/index.mjs.map +1 -0
- package/dist/renderer.d.mts +7 -0
- package/dist/renderer.d.mts.map +1 -0
- package/dist/renderer.mjs +3 -0
- package/dist/renderers-B24hckhO.mjs +953 -0
- package/dist/renderers-B24hckhO.mjs.map +1 -0
- package/package.json +48 -0
- package/src/index.ts +2 -0
- package/src/renderers/frame-cache.ts +149 -0
- package/src/renderers/index.ts +6 -0
- package/src/renderers/matte-compositor.ts +189 -0
- package/src/renderers/webgpu-renderer.ts +997 -0
- package/src/shared/config.ts +88 -0
- package/src/shared/index.ts +1 -0
- package/tsconfig.json +11 -0
- package/tsdown.config.ts +13 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderers-B24hckhO.mjs","names":["outPass"],"sources":["../src/renderers/frame-cache.ts","../src/renderers/matte-compositor.ts","../src/renderers/webgpu-renderer.ts","../src/renderers/index.ts"],"sourcesContent":["/**\n * Per-frame vision results, so a frame is analysed once. A source frame gives\n * the same detections and masks for the same model and settings, so replays,\n * scrubbing back and later renders in the same process reuse them instead of\n * running inference again.\n *\n * Masks are run-length encoded: they are long flat runs with soft edges, so a\n * frame-sized mask shrinks from megabytes to tens of kilobytes.\n */\nimport type {\n\tInstanceMask,\n\tSegmentationResult,\n\tTrackedObject,\n} from \"@framefields/vision\";\n\ntype DetectedObject = SegmentationResult[\"detections\"][number];\n\n/** A frame-sized 0..255 mask as (value, run length) pairs. */\nexport interface RleMask {\n\treadonly width: number;\n\treadonly height: number;\n\treadonly values: Uint8Array;\n\treadonly lengths: Uint32Array;\n}\n\nlet scratchValues = new Uint8Array(0);\nlet scratchLengths = new Uint32Array(0);\n\nexport function encodeMask(\n\tmask: Uint8Array,\n\twidth: number,\n\theight: number,\n): RleMask {\n\t// One pass into reused worst-case buffers; masks are flat almost\n\t// everywhere, so the copies at the end are small.\n\tif (scratchValues.length < mask.length) {\n\t\tscratchValues = new Uint8Array(mask.length);\n\t\tscratchLengths = new Uint32Array(mask.length);\n\t}\n\tconst values = scratchValues;\n\tconst lengths = scratchLengths;\n\tlet runs = 0;\n\tlet start = 0;\n\tfor (let i = 1; i <= mask.length; i++) {\n\t\tif (i === mask.length || mask[i] !== mask[start]) {\n\t\t\tvalues[runs] = mask[start];\n\t\t\tlengths[runs] = i - start;\n\t\t\truns++;\n\t\t\tstart = i;\n\t\t}\n\t}\n\treturn {\n\t\twidth,\n\t\theight,\n\t\tvalues: values.slice(0, runs),\n\t\tlengths: lengths.slice(0, runs),\n\t};\n}\n\nexport function decodeMask(rle: RleMask): Uint8Array {\n\tconst out = new Uint8Array(rle.width * rle.height);\n\tlet at = 0;\n\tfor (let r = 0; r < rle.values.length; r++) {\n\t\tconst end = at + rle.lengths[r];\n\t\tif (rle.values[r] !== 0) out.fill(rle.values[r], at, end);\n\t\tat = end;\n\t}\n\treturn out;\n}\n\nfunction rleBytes(rle: RleMask): number {\n\treturn rle.values.byteLength + rle.lengths.byteLength;\n}\n\nexport interface CachedInstanceMask extends Omit<InstanceMask, \"mask\"> {\n\treadonly rle: RleMask;\n}\n\nexport interface VisionFrameResult {\n\treadonly detections: readonly DetectedObject[];\n\treadonly objects: readonly TrackedObject[];\n\t/** Instance masks, kept only when a vision bundle reads them back. */\n\treadonly masks: readonly CachedInstanceMask[] | null;\n\t/** The composited subject (after hold and background keying); null = none. */\n\treadonly subject: { readonly mask: CachedInstanceMask } | null;\n\t/** Background-key threshold the subject was grown with. */\n\treadonly keyThreshold?: number;\n}\n\nexport function toCachedMask(mask: InstanceMask): CachedInstanceMask {\n\tconst { mask: data, ...rest } = mask;\n\treturn { ...rest, rle: encodeMask(data, mask.width, mask.height) };\n}\n\nexport function fromCachedMask(mask: CachedInstanceMask): InstanceMask {\n\tconst { rle, ...rest } = mask;\n\treturn { ...rest, mask: decodeMask(rle) };\n}\n\nfunction resultBytes(result: VisionFrameResult): number {\n\tlet bytes =\n\t\t512 + result.detections.length * 128 + result.objects.length * 256;\n\tfor (const m of result.masks ?? []) bytes += rleBytes(m.rle);\n\tif (result.subject) bytes += rleBytes(result.subject.mask.rle);\n\treturn bytes;\n}\n\n/** Results by source (node + inference settings) and frame, least recently used out first. */\nexport class VisionFrameCache {\n\tprivate readonly entries = new Map<string, VisionFrameResult>();\n\tprivate bytes = 0;\n\n\tconstructor(private readonly maxBytes = 256 * 1024 * 1024) {}\n\n\tget(source: string, frame: number): VisionFrameResult | undefined {\n\t\tconst key = `${source}#${frame}`;\n\t\tconst hit = this.entries.get(key);\n\t\tif (hit) {\n\t\t\tthis.entries.delete(key);\n\t\t\tthis.entries.set(key, hit);\n\t\t}\n\t\treturn hit;\n\t}\n\n\tset(source: string, frame: number, result: VisionFrameResult): void {\n\t\tconst key = `${source}#${frame}`;\n\t\tconst old = this.entries.get(key);\n\t\tif (old) {\n\t\t\tthis.bytes -= resultBytes(old);\n\t\t\tthis.entries.delete(key);\n\t\t}\n\t\tthis.entries.set(key, result);\n\t\tthis.bytes += resultBytes(result);\n\t\tfor (const [k, v] of this.entries) {\n\t\t\tif (this.bytes <= this.maxBytes) break;\n\t\t\tthis.entries.delete(k);\n\t\t\tthis.bytes -= resultBytes(v);\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.entries.clear();\n\t\tthis.bytes = 0;\n\t}\n\n\tget size(): number {\n\t\treturn this.entries.size;\n\t}\n}\n","/// <reference types=\"webgpu\" />\n/**\n * GPU composite for the mask-family modes: the picture times the subject mask,\n * in one full-screen pass. Replaces building the RGBA result on the CPU and\n * uploading it; only the 8-bit mask goes to the GPU.\n *\n * - matte: picture × mask (premultiplied, so the background is transparent)\n * - mask: white silhouette (rgb = mask, a = 1)\n * - crop: matte, with the subject's bounding box stretched to the frame\n */\n\nconst WGSL = /* wgsl */ `\nstruct Params {\n\tmode: u32, // 0 matte, 1 mask, 2 crop\n\t_pad: u32,\n\tcropMin: vec2<f32>, // uv of the subject bounds (crop)\n\tcropMax: vec2<f32>,\n\t_pad2: vec2<f32>,\n};\n\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var picture: texture_2d<f32>;\n@group(0) @binding(2) var mask: texture_2d<f32>;\n@group(0) @binding(3) var samp: sampler;\n\nstruct VSOut {\n\t@builtin(position) pos: vec4<f32>,\n\t@location(0) uv: vec2<f32>,\n};\n\n@vertex fn vs(@builtin(vertex_index) i: u32) -> VSOut {\n\tvar p = array<vec2<f32>, 3>(vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));\n\tvar out: VSOut;\n\tout.pos = vec4(p[i], 0.0, 1.0);\n\tout.uv = vec2(p[i].x * 0.5 + 0.5, 0.5 - p[i].y * 0.5);\n\treturn out;\n}\n\n@fragment fn fs(in: VSOut) -> @location(0) vec4<f32> {\n\tvar uv = in.uv;\n\tif (params.mode == 2u) {\n\t\tuv = mix(params.cropMin, params.cropMax, uv);\n\t}\n\tlet m = textureSampleLevel(mask, samp, uv, 0.0).r;\n\tif (params.mode == 1u) {\n\t\treturn vec4(m, m, m, 1.0);\n\t}\n\treturn textureSampleLevel(picture, samp, uv, 0.0) * m;\n}\n`;\n\nexport type MatteMode = \"matte\" | \"mask\" | \"crop\";\n\nexport interface MatteBounds {\n\tx0: number;\n\ty0: number;\n\tx1: number;\n\ty1: number;\n}\n\nexport class MatteCompositor {\n\tprivate readonly pipeline: GPURenderPipeline;\n\tprivate readonly sampler: GPUSampler;\n\tprivate readonly uniforms: GPUBuffer[] = [];\n\tprivate uniformIndex = 0;\n\t/** One 8-bit mask texture per node, rewritten each frame. */\n\tprivate readonly masks = new Map<string, GPUTexture>();\n\n\tconstructor(\n\t\tprivate readonly device: GPUDevice,\n\t\tformat: GPUTextureFormat,\n\t) {\n\t\tconst module = device.createShaderModule({\n\t\t\tlabel: \"vision_matte_composite\",\n\t\t\tcode: WGSL,\n\t\t});\n\t\tthis.pipeline = device.createRenderPipeline({\n\t\t\tlabel: \"vision_matte_composite\",\n\t\t\tlayout: \"auto\",\n\t\t\tvertex: { module, entryPoint: \"vs\" },\n\t\t\tfragment: { module, entryPoint: \"fs\", targets: [{ format }] },\n\t\t\tprimitive: { topology: \"triangle-list\" },\n\t\t});\n\t\tthis.sampler = device.createSampler({\n\t\t\tmagFilter: \"linear\",\n\t\t\tminFilter: \"linear\",\n\t\t});\n\t}\n\n\t/** Uploads a frame-sized 0..255 mask for `key` and returns its texture. */\n\tuploadMask(\n\t\tkey: string,\n\t\tmask: Uint8Array,\n\t\twidth: number,\n\t\theight: number,\n\t): GPUTexture {\n\t\tlet tex = this.masks.get(key);\n\t\tif (!tex || tex.width !== width || tex.height !== height) {\n\t\t\ttex?.destroy();\n\t\t\ttex = this.device.createTexture({\n\t\t\t\tlabel: `vision_mask_${key}`,\n\t\t\t\tsize: [width, height],\n\t\t\t\tformat: \"r8unorm\",\n\t\t\t\tusage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,\n\t\t\t});\n\t\t\tthis.masks.set(key, tex);\n\t\t}\n\t\tconst aligned = Math.ceil(width / 256) * 256;\n\t\tlet bytes = mask;\n\t\tif (aligned !== width) {\n\t\t\tbytes = new Uint8Array(aligned * height);\n\t\t\tfor (let y = 0; y < height; y++) {\n\t\t\t\tbytes.set(mask.subarray(y * width, (y + 1) * width), y * aligned);\n\t\t\t}\n\t\t}\n\t\tthis.device.queue.writeTexture(\n\t\t\t{ texture: tex },\n\t\t\tbytes.buffer,\n\t\t\t{ offset: bytes.byteOffset, bytesPerRow: aligned, rowsPerImage: height },\n\t\t\t{ width, height },\n\t\t);\n\t\treturn tex;\n\t}\n\n\t/** Clears `target` and draws `picture` through `mask` into it. */\n\tdraw(\n\t\tencoder: GPUCommandEncoder,\n\t\ttarget: GPUTextureView,\n\t\tpicture: GPUTexture,\n\t\tmask: GPUTexture,\n\t\tmode: MatteMode,\n\t\tbounds?: MatteBounds,\n\t): void {\n\t\tconst w = mask.width;\n\t\tconst h = mask.height;\n\t\tconst data = new Float32Array(8);\n\t\tconst view = new Uint32Array(data.buffer);\n\t\tview[0] = mode === \"matte\" ? 0 : mode === \"mask\" ? 1 : 2;\n\t\tif (mode === \"crop\" && bounds) {\n\t\t\tdata[2] = bounds.x0 / w;\n\t\t\tdata[3] = bounds.y0 / h;\n\t\t\tdata[4] = (bounds.x1 + 1) / w;\n\t\t\tdata[5] = (bounds.y1 + 1) / h;\n\t\t} else {\n\t\t\tdata[2] = 0;\n\t\t\tdata[3] = 0;\n\t\t\tdata[4] = 1;\n\t\t\tdata[5] = 1;\n\t\t\tif (mode === \"crop\") view[0] = 0;\n\t\t}\n\t\t// A small ring of uniform buffers: several vision nodes may draw in one frame.\n\t\tif (this.uniformIndex >= this.uniforms.length) {\n\t\t\tthis.uniforms.push(\n\t\t\t\tthis.device.createBuffer({\n\t\t\t\t\tsize: data.byteLength,\n\t\t\t\t\tusage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tconst uniform = this.uniforms[this.uniformIndex];\n\t\tthis.uniformIndex = (this.uniformIndex + 1) % 16;\n\t\tthis.device.queue.writeBuffer(uniform, 0, data);\n\n\t\tconst bindGroup = this.device.createBindGroup({\n\t\t\tlayout: this.pipeline.getBindGroupLayout(0),\n\t\t\tentries: [\n\t\t\t\t{ binding: 0, resource: { buffer: uniform } },\n\t\t\t\t{ binding: 1, resource: picture.createView() },\n\t\t\t\t{ binding: 2, resource: mask.createView() },\n\t\t\t\t{ binding: 3, resource: this.sampler },\n\t\t\t],\n\t\t});\n\t\tconst pass = encoder.beginRenderPass({\n\t\t\tlabel: \"vision_matte_composite\",\n\t\t\tcolorAttachments: [\n\t\t\t\t{\n\t\t\t\t\tview: target,\n\t\t\t\t\tloadOp: \"clear\",\n\t\t\t\t\tstoreOp: \"store\",\n\t\t\t\t\tclearValue: { r: 0, g: 0, b: 0, a: 0 },\n\t\t\t\t},\n\t\t\t],\n\t\t});\n\t\tpass.setPipeline(this.pipeline);\n\t\tpass.setBindGroup(0, bindGroup);\n\t\tpass.draw(3);\n\t\tpass.end();\n\t}\n}\n","/// <reference types=\"webgpu\" />\nimport type { WebGPUNodeRenderer } from \"@framefields/node-sdk\";\nimport {\n\ttype InstanceMask,\n\tmaskBounds,\n\tmatchPoseToTracks,\n\tmergeSubjectMask,\n\ttype PersonMatte,\n\ttype PoseResult,\n\tPoseSkeletonRenderer,\n\tTemporalObjectTracker,\n\ttype TrackedObject,\n\ttype VisionBundle,\n\tVisionRunner,\n} from \"@framefields/vision\";\nimport type { VisionOperation } from \"../shared/config.js\";\nimport {\n\tfromCachedMask,\n\ttoCachedMask,\n\tVisionFrameCache,\n} from \"./frame-cache.js\";\nimport { MatteCompositor } from \"./matte-compositor.js\";\n\nlet sharedRunner: { key: string; runner: VisionRunner } | null = null;\nlet sharedSkeletonRenderer: PoseSkeletonRenderer | null = null;\nlet sharedCompositor: MatteCompositor | null = null;\nlet sharedCompositorFormat: GPUTextureFormat | null = null;\n/** Analysed frames, shared by every vision node in the process. */\nconst frameCache = new VisionFrameCache();\n/** The frame each node's child texture holds (drawn on its last call). */\nconst heldFrameByNode = new Map<string, number>();\n/** Per node: a copy of the held frame, and the composited matte. */\nconst heldTextures = new Map<string, GPUTexture>();\nconst matteTextures = new Map<string, GPUTexture>();\n/** One tracker per vision node — track ids must not mix across nodes/sources. */\nconst objectTrackers = new Map<string, TemporalObjectTracker>();\n\n/**\n * Persistent per-node child textures. The shared frame encoder is submitted at\n * the END of the frame, so a node renderer cannot read back the child it just\n * drew. We keep the texture around and read back the PREVIOUS frame's pixels at\n * the start of the next call — a stable one-frame delay instead of the\n * unpredictable multi-frame lag you get from pooled textures.\n *\n * Keyed on the stable Vision `Effect` instance (render ids change every frame),\n * with a size-keyed fallback for raw operation nodes.\n */\nconst childTexturesByEffect = new WeakMap<object, GPUTexture>();\nconst childTexturesByKey = new Map<string, GPUTexture>();\n\n/**\n * Last successful subject mask, reused for a few frames when the model misses —\n * a transient miss then holds the silhouette instead of flashing the raw plate.\n * Keyed per node/effect so multiple vision nodes do not collide.\n */\nconst lastSubjectByNode = new Map<\n\tstring,\n\t{ mask: InstanceMask; frame: number }\n>();\nconst SUBJECT_HOLD_FRAMES = 3;\n\n/** A per-node texture shaped like `like`, recreated when the size changes. */\nfunction nodeTexture(\n\ttextures: Map<string, GPUTexture>,\n\tdevice: GPUDevice,\n\tkey: string,\n\tlike: GPUTexture,\n\tusage: number,\n\tlabel: string,\n): GPUTexture {\n\tlet tex = textures.get(key);\n\tif (\n\t\t!tex ||\n\t\ttex.width !== like.width ||\n\t\ttex.height !== like.height ||\n\t\ttex.format !== like.format\n\t) {\n\t\ttex?.destroy();\n\t\ttex = device.createTexture({\n\t\t\tsize: [like.width, like.height],\n\t\t\tformat: like.format,\n\t\t\tusage,\n\t\t\tlabel,\n\t\t});\n\t\ttextures.set(key, tex);\n\t}\n\treturn tex;\n}\n\nconst heldTexture = (device: GPUDevice, key: string, like: GPUTexture) =>\n\tnodeTexture(\n\t\theldTextures,\n\t\tdevice,\n\t\tkey,\n\t\tlike,\n\t\tGPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,\n\t\t\"vision_held_frame\",\n\t);\n\nconst matteTexture = (device: GPUDevice, key: string, like: GPUTexture) =>\n\tnodeTexture(\n\t\tmatteTextures,\n\t\tdevice,\n\t\tkey,\n\t\tlike,\n\t\tGPUTextureUsage.RENDER_ATTACHMENT |\n\t\t\tGPUTextureUsage.TEXTURE_BINDING |\n\t\t\tGPUTextureUsage.COPY_SRC,\n\t\t\"vision_matte\",\n\t);\n\n/** Everything that decides a frame's results besides the frame itself. */\nfunction cacheSource(\n\tnodeKey: string,\n\tchild: unknown,\n\top: VisionOperation,\n\twidth: number,\n\theight: number,\n): string {\n\tconst c = child as { id?: unknown; operation?: { id?: unknown } } | undefined;\n\treturn JSON.stringify([\n\t\tnodeKey,\n\t\tc?.operation?.id ?? c?.id ?? null,\n\t\twidth,\n\t\theight,\n\t\top.mode ?? \"passthrough\",\n\t\top.variant,\n\t\top.confidence,\n\t\top.classes,\n\t\top.maskThreshold,\n\t\top.featherRadius,\n\t\top.matteSource,\n\t\top.keyBackground === true,\n\t\top.maxMissedFrames,\n\t]);\n}\n\n/** The background-key threshold at `frame` (it may be animated). */\nfunction backgroundKeyThreshold(\n\top: VisionOperation,\n\tframe: number,\n\tfps: number,\n): number {\n\tconst raw = (op as Record<string, unknown>).backgroundKeyThreshold;\n\tif (typeof raw === \"number\") return raw;\n\tif (typeof (raw as { get?: unknown })?.get === \"function\") {\n\t\treturn Number(\n\t\t\t(raw as { get: (ctx?: unknown) => unknown }).get({ frame, fps }),\n\t\t);\n\t}\n\tif (typeof (raw as { _value?: unknown })?._value === \"number\") {\n\t\treturn Number((raw as { _value: number })._value);\n\t}\n\treturn 70;\n}\n\n/**\n * The model's subject, or the last one through brief misses: a transient miss\n * then holds the silhouette instead of flashing the raw plate.\n */\nfunction selectSubject(\n\tselected: InstanceMask | undefined,\n\tnodeKey: string,\n\tframe: number,\n): InstanceMask | undefined {\n\tif (selected) {\n\t\tlastSubjectByNode.set(nodeKey, { mask: selected, frame });\n\t\treturn selected;\n\t}\n\tconst last = lastSubjectByNode.get(nodeKey);\n\tif (last && frame - last.frame <= SUBJECT_HOLD_FRAMES) return last.mask;\n\treturn undefined;\n}\n\n/**\n * Lazy shared runner — `create()` is a pure constructor (zero I/O); the first frame that\n * requests a task triggers that task's model download at inference time (never at init).\n * Recreated only when an option that changes inference changes.\n */\nfunction getSharedRunner(op: VisionOperation): VisionRunner {\n\tconst options = {\n\t\tvariant: op.variant,\n\t\tconfidence: op.confidence,\n\t\tclasses: op.classes,\n\t\tmaskThreshold: op.maskThreshold,\n\t\tfeatherRadius: op.featherRadius,\n\t\tmodelsDir: op.modelsDir,\n\t\tbaseUrl: op.baseUrl,\n\t};\n\tconst key = JSON.stringify(options);\n\tif (sharedRunner?.key !== key) {\n\t\tsharedRunner?.runner.close();\n\t\tsharedRunner = { key, runner: VisionRunner.create(options) };\n\t}\n\treturn sharedRunner.runner;\n}\n\nfunction getObjectTracker(\n\tnodeKey: string,\n\top: VisionOperation,\n): TemporalObjectTracker {\n\tlet tracker = objectTrackers.get(nodeKey);\n\tif (!tracker) {\n\t\ttracker = new TemporalObjectTracker({\n\t\t\tmaxMissedFrames: op.maxMissedFrames ?? 15,\n\t\t});\n\t\tobjectTrackers.set(nodeKey, tracker);\n\t}\n\treturn tracker;\n}\n\n/**\n * Normalizes the runner's pose keypoints (plate pixel space) to [0, 1] landmarks, which is\n * the contract the signal bundle and the skeleton pipeline expect.\n */\nfunction normalizePoseKeypoints(\n\tresult: PoseResult,\n\twidth: number,\n\theight: number,\n): PoseResult {\n\tif (width <= 0 || height <= 0) return result;\n\treturn {\n\t\tpeople: result.people.map((person) => ({\n\t\t\t...person,\n\t\t\tkeypoints: person.keypoints.map((k) => ({\n\t\t\t\tx: k.x / width,\n\t\t\t\ty: k.y / height,\n\t\t\t\tvisibility: k.visibility,\n\t\t\t})),\n\t\t})),\n\t};\n}\n\n/** Assigns each decoded instance mask to the temporal track whose box center is nearest the mask's centroid. */\nfunction assignMasksToTracks(\n\tmasks: readonly InstanceMask[],\n\ttracked: readonly { trackId: number; centerX: number; centerY: number }[],\n): InstanceMask[] {\n\treturn masks.map((mask) => {\n\t\tconst { cx, cy } = maskCentroid(mask);\n\t\tlet best: { trackId: number; distance: number } | null = null;\n\t\tfor (const obj of tracked) {\n\t\t\tconst dx = obj.centerX - cx;\n\t\t\tconst dy = obj.centerY - cy;\n\t\t\tconst distance = dx * dx + dy * dy;\n\t\t\tif (!best || distance < best.distance) {\n\t\t\t\tbest = { trackId: obj.trackId, distance };\n\t\t\t}\n\t\t}\n\t\treturn best ? { ...mask, trackId: best.trackId } : mask;\n\t});\n}\n\n/**\n * Grows a subject mask into connected foreground pixels — seeding a flood fill\n * from the subject through pixels that differ from the sampled backdrop recovers\n * thin or fast-moving edges (hair, fabric) without adding a near-uniform studio\n * wall or its soft grey shadow.\n */\nfunction fillInternalHoles(\n\tmask: Uint8Array,\n\twidth: number,\n\theight: number,\n): void {\n\t// Flood the background in from the border; whatever it cannot reach is a hole.\n\tconst n = width * height;\n\tconst exterior = new Uint8Array(n);\n\tconst queue = new Int32Array(n);\n\tlet tail = 0;\n\tconst seed = (i: number) => {\n\t\tif (exterior[i] || mask[i] > 0) return;\n\t\texterior[i] = 1;\n\t\tqueue[tail++] = i;\n\t};\n\tfor (let x = 0; x < width; x++) {\n\t\tseed(x);\n\t\tseed((height - 1) * width + x);\n\t}\n\tfor (let y = 0; y < height; y++) {\n\t\tseed(y * width);\n\t\tseed(y * width + width - 1);\n\t}\n\n\tlet head = 0;\n\twhile (head < tail) {\n\t\tconst i = queue[head++];\n\t\tconst x = i % width;\n\t\tlet j = i + 1;\n\t\tif (x + 1 < width && !exterior[j] && mask[j] === 0) {\n\t\t\texterior[j] = 1;\n\t\t\tqueue[tail++] = j;\n\t\t}\n\t\tj = i - 1;\n\t\tif (x > 0 && !exterior[j] && mask[j] === 0) {\n\t\t\texterior[j] = 1;\n\t\t\tqueue[tail++] = j;\n\t\t}\n\t\tj = i + width;\n\t\tif (j < n && !exterior[j] && mask[j] === 0) {\n\t\t\texterior[j] = 1;\n\t\t\tqueue[tail++] = j;\n\t\t}\n\t\tj = i - width;\n\t\tif (j >= 0 && !exterior[j] && mask[j] === 0) {\n\t\t\texterior[j] = 1;\n\t\t\tqueue[tail++] = j;\n\t\t}\n\t}\n\n\tfor (let i = 0; i < n; i++) {\n\t\tif (mask[i] === 0 && exterior[i] === 0) {\n\t\t\tmask[i] = 255;\n\t\t}\n\t}\n}\n\nfunction smoothMaskBoundary(\n\tmask: Uint8Array,\n\twidth: number,\n\theight: number,\n\tradius = 2,\n): Uint8Array {\n\tif (radius <= 0) return mask;\n\tconst temp = new Uint8Array(width * height);\n\tconst out = new Uint8Array(width * height);\n\tconst div = 2 * radius + 1;\n\n\t// Horizontal box filter\n\tfor (let y = 0; y < height; y++) {\n\t\tconst row = y * width;\n\t\tlet sum = 0;\n\t\tfor (let k = -radius; k <= radius; k++) {\n\t\t\tconst x = Math.max(0, Math.min(width - 1, k));\n\t\t\tsum += mask[row + x];\n\t\t}\n\t\tfor (let x = 0; x < width; x++) {\n\t\t\ttemp[row + x] = Math.round(sum / div);\n\t\t\tconst xOut = Math.max(0, x - radius);\n\t\t\tconst xIn = Math.min(width - 1, x + radius + 1);\n\t\t\tsum += mask[row + xIn] - mask[row + xOut];\n\t\t}\n\t}\n\n\t// Vertical box filter, a running sum per column walked row by row\n\t// (in memory order).\n\tconst sums = new Float64Array(width);\n\tfor (let k = -radius; k <= radius; k++) {\n\t\tconst row = Math.max(0, Math.min(height - 1, k)) * width;\n\t\tfor (let x = 0; x < width; x++) sums[x] += temp[row + x];\n\t}\n\tfor (let y = 0; y < height; y++) {\n\t\tconst row = y * width;\n\t\tconst rowOut = Math.max(0, y - radius) * width;\n\t\tconst rowIn = Math.min(height - 1, y + radius + 1) * width;\n\t\tfor (let x = 0; x < width; x++) {\n\t\t\tout[row + x] = Math.round(sums[x] / div);\n\t\t\tsums[x] += temp[rowIn + x] - temp[rowOut + x];\n\t\t}\n\t}\n\n\treturn out;\n}\n\nfunction growMaskIntoForeground(\n\tmask: Uint8Array,\n\twidth: number,\n\theight: number,\n\tpixels: Uint8ClampedArray,\n\tthreshold: number,\n\tfeatherRadius?: number,\n): Uint8Array {\n\t// Backdrop estimate from the frame border, ignoring already-masked pixels.\n\tlet br = 0;\n\tlet bg = 0;\n\tlet bb = 0;\n\tlet count = 0;\n\tconst sample = (x: number, y: number) => {\n\t\tconst i = y * width + x;\n\t\tif (mask[i]) return;\n\t\tconst p = i * 4;\n\t\tbr += pixels[p];\n\t\tbg += pixels[p + 1];\n\t\tbb += pixels[p + 2];\n\t\tcount++;\n\t};\n\tfor (let x = 0; x < width; x += 4) {\n\t\tsample(x, 0);\n\t\tsample(x, height - 1);\n\t}\n\tfor (let y = 0; y < height; y += 4) {\n\t\tsample(0, y);\n\t\tsample(width - 1, y);\n\t}\n\tif (count === 0) return mask;\n\tbr /= count;\n\tbg /= count;\n\tbb /= count;\n\n\t// Pixels that stand out from the backdrop, decided once per pixel.\n\tconst n = width * height;\n\tconst fg = new Uint8Array(n);\n\tfor (let i = 0, p = 0; i < n; i++, p += 4) {\n\t\tfg[i] =\n\t\t\tMath.max(\n\t\t\t\tMath.abs(pixels[p] - br),\n\t\t\t\tMath.abs(pixels[p + 1] - bg),\n\t\t\t\tMath.abs(pixels[p + 2] - bb),\n\t\t\t) > threshold\n\t\t\t\t? 1\n\t\t\t\t: 0;\n\t}\n\n\t// Each pixel enters the queue at most once: seeds are the masked pixels,\n\t// later pushes are pixels just raised to 255.\n\tconst queue = new Int32Array(n);\n\tlet tail = 0;\n\tfor (let i = 0; i < n; i++) {\n\t\tif (mask[i] > 0) {\n\t\t\tif (fg[i]) mask[i] = 255;\n\t\t\tqueue[tail++] = i;\n\t\t}\n\t}\n\n\tlet head = 0;\n\twhile (head < tail) {\n\t\tconst i = queue[head++];\n\t\tconst x = i % width;\n\t\tlet j = i + 1;\n\t\tif (x + 1 < width && mask[j] !== 255 && fg[j]) {\n\t\t\tmask[j] = 255;\n\t\t\tqueue[tail++] = j;\n\t\t}\n\t\tj = i - 1;\n\t\tif (x > 0 && mask[j] !== 255 && fg[j]) {\n\t\t\tmask[j] = 255;\n\t\t\tqueue[tail++] = j;\n\t\t}\n\t\tj = i + width;\n\t\tif (j < n && mask[j] !== 255 && fg[j]) {\n\t\t\tmask[j] = 255;\n\t\t\tqueue[tail++] = j;\n\t\t}\n\t\tj = i - width;\n\t\tif (j >= 0 && mask[j] !== 255 && fg[j]) {\n\t\t\tmask[j] = 255;\n\t\t\tqueue[tail++] = j;\n\t\t}\n\t}\n\n\tfillInternalHoles(mask, width, height);\n\n\tconst blurRadius =\n\t\tfeatherRadius !== undefined && featherRadius > 0\n\t\t\t? Math.max(1, Math.min(8, Math.round(featherRadius * 50)))\n\t\t\t: 2;\n\n\treturn smoothMaskBoundary(mask, width, height, blurRadius);\n}\n\nfunction maskCentroid(mask: InstanceMask): { cx: number; cy: number } {\n\t// First moment of the frame-aligned mask — cheap, deterministic association.\n\tconst { mask: data, width, height } = mask;\n\tlet sumX = 0;\n\tlet sumY = 0;\n\tlet count = 0;\n\tfor (let y = 0; y < height; y++) {\n\t\tconst row = y * width;\n\t\tfor (let x = 0; x < width; x++) {\n\t\t\tif (data[row + x] > 0) {\n\t\t\t\tsumX += x;\n\t\t\t\tsumY += y;\n\t\t\t\tcount++;\n\t\t\t}\n\t\t}\n\t}\n\tif (count === 0) return { cx: 0, cy: 0 };\n\treturn { cx: sumX / count, cy: sumY / count };\n}\n\nexport const VisionWebGPURenderer: WebGPUNodeRenderer = async (args) => {\n\tconst {\n\t\tctx,\n\t\tencoder,\n\t\tpass,\n\t\ttargetView,\n\t\ttargetWidth,\n\t\ttargetHeight,\n\t\tprops,\n\t\tdrawChild,\n\t} = args;\n\n\tconst { virtualMedia } = props;\n\tconst rawOp = virtualMedia?.operation as\n\t\t| VisionOperation\n\t\t| Record<string, unknown>\n\t\t| undefined;\n\tif (rawOp?.op !== \"Vision\") return;\n\tconst op = rawOp as VisionOperation;\n\n\t// Close the incoming render pass to allow custom render passes\n\tpass.end();\n\n\tconst childMedia = virtualMedia?.children?.[0];\n\tif (!childMedia) return;\n\n\t// 1. Persistent per-node child texture (see childTextures*). Reading the\n\t// texture we just drew would race the end-of-frame submit; reading last\n\t// frame's already-submitted pixels at the start of this call is correct.\n\tconst effectKey = (op as { effect?: object }).effect;\n\tconst hasEffectKey = effectKey !== undefined && effectKey !== null;\n\tconst nodeKeyStr =\n\t\t(virtualMedia as any)?.id ??\n\t\t(rawOp as any)?.id ??\n\t\t`${targetWidth}x${targetHeight}_${op.mode}_${op.variant}_${op.keyBackground}_${op.backgroundKeyThreshold}`;\n\tconst fallbackKey = `vision-${nodeKeyStr}`;\n\tlet childTex = hasEffectKey\n\t\t? childTexturesByEffect.get(effectKey as object)\n\t\t: childTexturesByKey.get(fallbackKey);\n\tif (\n\t\tchildTex &&\n\t\t(childTex.width !== targetWidth || childTex.height !== targetHeight)\n\t) {\n\t\tchildTex.destroy();\n\t\tchildTex = undefined;\n\t\tif (hasEffectKey) childTexturesByEffect.delete(effectKey as object);\n\t\telse childTexturesByKey.delete(fallbackKey);\n\t}\n\tconst hasPreviousFrame = childTex !== undefined;\n\tif (!childTex) {\n\t\tchildTex = ctx.device.createTexture({\n\t\t\tsize: [targetWidth, targetHeight],\n\t\t\tformat: ctx.renderer.format,\n\t\t\tusage:\n\t\t\t\tGPUTextureUsage.RENDER_ATTACHMENT |\n\t\t\t\tGPUTextureUsage.TEXTURE_BINDING |\n\t\t\t\tGPUTextureUsage.COPY_SRC,\n\t\t\tlabel: \"vision_child_persistent\",\n\t\t});\n\t\tif (hasEffectKey) childTexturesByEffect.set(effectKey as object, childTex);\n\t\telse childTexturesByKey.set(fallbackKey, childTex);\n\t\t// keep the fallback cache bounded across many renders in one process\n\t\tif (childTexturesByKey.size > 4) {\n\t\t\tconst oldest = childTexturesByKey.keys().next().value;\n\t\t\tif (oldest && oldest !== fallbackKey) {\n\t\t\t\tchildTexturesByKey.get(oldest)?.destroy();\n\t\t\t\tchildTexturesByKey.delete(oldest);\n\t\t\t}\n\t\t}\n\t}\n\n\tconst frameIdx = props.frame ?? 0;\n\tconst fps = props.fps ?? 24;\n\tconst mode = op.mode ?? \"passthrough\";\n\tconst isMatteMode = mode === \"mask\" || mode === \"matte\" || mode === \"crop\";\n\tconst visionBundle =\n\t\t(op.visionBundle as VisionBundle | undefined) ??\n\t\t((virtualMedia as Record<string, unknown>).visionBundle as\n\t\t\t| VisionBundle\n\t\t\t| undefined);\n\tconst wantsDetection =\n\t\top.enableDetection !== false || mode === \"boxes\" || mode === \"tracking\";\n\tconst wantsSegmentation =\n\t\top.enableSegmentation === true ||\n\t\t(isMatteMode && op.matteSource !== \"selfie\");\n\tconst wantsSelfie =\n\t\top.enableMatte === true || (isMatteMode && op.matteSource === \"selfie\");\n\tconst wantsPose = op.enablePose === true || mode === \"skeleton\";\n\n\t// 2. Results are cached per source frame (selfie mattes and poses are not,\n\t// so nodes that need them always run inference). A frame analysed before\n\t// is composited with its own pixels; otherwise inference runs on the\n\t// frame the child texture still holds, one frame behind.\n\tconst cacheable =\n\t\t!wantsSelfie && !wantsPose && (wantsDetection || wantsSegmentation);\n\tconst source = cacheable\n\t\t? cacheSource(nodeKeyStr, childMedia, op, targetWidth, targetHeight)\n\t\t: \"\";\n\tconst keyThresholdAt = (frame: number) =>\n\t\top.keyBackground === true\n\t\t\t? backgroundKeyThreshold(op, frame, fps)\n\t\t\t: undefined;\n\tconst lookup = (frame: number | undefined) => {\n\t\tif (!cacheable || frame === undefined) return undefined;\n\t\tconst hit = frameCache.get(source, frame);\n\t\tif (!hit || hit.keyThreshold !== keyThresholdAt(frame)) return undefined;\n\t\t// Results cached without instance masks can't feed a bundle that wants them.\n\t\tif (wantsSegmentation && visionBundle && !hit.masks) return undefined;\n\t\treturn hit;\n\t};\n\tconst heldFrame = hasPreviousFrame\n\t\t? heldFrameByNode.get(nodeKeyStr)\n\t\t: undefined;\n\tlet cached = lookup(frameIdx);\n\tconst exact = cached !== undefined;\n\tif (!cached) cached = lookup(heldFrame);\n\n\t// Keep the held frame's picture on the GPU: the matte of that frame is\n\t// composited from it after the child texture is redrawn below.\n\tlet heldTex: GPUTexture | undefined;\n\tif (isMatteMode && hasPreviousFrame && !exact) {\n\t\theldTex = heldTexture(ctx.device, nodeKeyStr, childTex);\n\t\tencoder.copyTextureToTexture({ texture: childTex }, { texture: heldTex }, [\n\t\t\ttargetWidth,\n\t\t\ttargetHeight,\n\t\t\t1,\n\t\t]);\n\t}\n\n\t// 3. Read back the held frame's pixels, only when it still needs inference.\n\tlet framePixels: Uint8ClampedArray | null = null;\n\tif (hasPreviousFrame && !cached) {\n\t\tconst bytesPerPixel = 4;\n\t\tconst unalignedBytesPerRow = targetWidth * bytesPerPixel;\n\t\tconst bytesPerRow = Math.ceil(unalignedBytesPerRow / 256) * 256;\n\t\tconst bufferSize = bytesPerRow * targetHeight;\n\n\t\tconst stagingBuffer = ctx.device.createBuffer({\n\t\t\tsize: bufferSize,\n\t\t\tusage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,\n\t\t\tlabel: \"vision_frame_staging\",\n\t\t});\n\n\t\tconst readbackEncoder = ctx.device.createCommandEncoder({\n\t\t\tlabel: \"vision_readback_encoder\",\n\t\t});\n\t\treadbackEncoder.copyTextureToBuffer(\n\t\t\t{ texture: childTex },\n\t\t\t{ buffer: stagingBuffer, bytesPerRow, rowsPerImage: targetHeight },\n\t\t\t[targetWidth, targetHeight, 1],\n\t\t);\n\t\tctx.device.queue.submit([readbackEncoder.finish()]);\n\n\t\tawait stagingBuffer.mapAsync(GPUMapMode.READ);\n\t\tconst mappedBytes = new Uint8Array(stagingBuffer.getMappedRange());\n\t\tframePixels = new Uint8ClampedArray(targetWidth * targetHeight * 4);\n\t\tif (bytesPerRow === unalignedBytesPerRow) {\n\t\t\tframePixels.set(mappedBytes.subarray(0, framePixels.length));\n\t\t} else {\n\t\t\tfor (let y = 0; y < targetHeight; y++) {\n\t\t\t\tframePixels.set(\n\t\t\t\t\tmappedBytes.subarray(\n\t\t\t\t\t\ty * bytesPerRow,\n\t\t\t\t\t\ty * bytesPerRow + unalignedBytesPerRow,\n\t\t\t\t\t),\n\t\t\t\t\ty * unalignedBytesPerRow,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tstagingBuffer.unmap();\n\t\tstagingBuffer.destroy();\n\t}\n\n\t// 4. Draw THIS frame's child into the persistent texture (recorded into the\n\t// shared encoder, submitted at end of frame, read back next call).\n\tconst childView = childTex.createView();\n\tconst clearPass = ctx.renderer.beginFrame(\n\t\tencoder,\n\t\tchildView,\n\t\t{ r: 0, g: 0, b: 0, a: 0 },\n\t\ttargetWidth,\n\t\ttargetHeight,\n\t\t\"clear\",\n\t);\n\tclearPass.end();\n\n\tawait drawChild(\n\t\tchildMedia,\n\t\t{ ...props },\n\t\tchildView,\n\t\tchildTex,\n\t\ttargetWidth,\n\t\ttargetHeight,\n\t);\n\theldFrameByNode.set(nodeKeyStr, frameIdx);\n\n\t// 5. Results: from the cache, or inference on the held frame's pixels\n\t// (models download lazily on first use of each task).\n\tlet frameObjects: readonly TrackedObject[] = [];\n\tlet frameMasks: readonly InstanceMask[] = [];\n\tlet personMatte: PersonMatte | undefined;\n\tlet currentPoseRes: PoseResult | undefined;\n\tlet subject: InstanceMask | undefined;\n\n\tif (cached) {\n\t\tframeObjects = cached.objects;\n\t\tif (wantsDetection) {\n\t\t\tvisionBundle?.setObjectResult(frameIdx, {\n\t\t\t\tobjects: cached.objects,\n\t\t\t\trawDetections: cached.detections,\n\t\t\t});\n\t\t}\n\t\tif (wantsSegmentation && visionBundle && cached.masks) {\n\t\t\tframeMasks = cached.masks.map(fromCachedMask);\n\t\t\tvisionBundle.setMaskResult(frameIdx, frameMasks);\n\t\t}\n\t\tif (cached.subject) {\n\t\t\tsubject = fromCachedMask(cached.subject.mask);\n\t\t\tlastSubjectByNode.set(nodeKeyStr, { mask: subject, frame: frameIdx });\n\t\t}\n\t} else if (framePixels) {\n\t\tconst runner = getSharedRunner(op);\n\t\tconst image = {\n\t\t\tdata: framePixels,\n\t\t\twidth: targetWidth,\n\t\t\theight: targetHeight,\n\t\t};\n\n\t\t// detect + segment share one RTMDet-Ins pass per frame inside the runner.\n\t\t// Tracked objects are kept locally (like masks below) so `boxes` /\n\t\t// `tracking` draw even without an attached visionBundle.\n\t\tlet detections: Awaited<ReturnType<VisionRunner[\"detect\"]>> = [];\n\t\tif (wantsDetection) {\n\t\t\tdetections = await runner.detect(image);\n\t\t\tconst tracker = getObjectTracker(nodeKeyStr, op);\n\t\t\tif (frameIdx === 0) {\n\t\t\t\ttracker.reset();\n\t\t\t}\n\t\t\tframeObjects = tracker.update(detections, frameIdx, fps);\n\t\t\tvisionBundle?.setObjectResult(frameIdx, {\n\t\t\t\tobjects: frameObjects,\n\t\t\t\trawDetections: detections,\n\t\t\t});\n\t\t}\n\n\t\t// Decoded masks are kept locally so mask-family modes render even without\n\t\t// an attached visionBundle (the round-trip through the bundle is for signal DX).\n\t\tif (wantsSegmentation) {\n\t\t\tconst segRes = await runner.segment(image);\n\t\t\tframeMasks = assignMasksToTracks(segRes.masks, frameObjects);\n\t\t\tvisionBundle?.setMaskResult(frameIdx, frameMasks);\n\t\t}\n\n\t\tif (wantsSelfie) {\n\t\t\tpersonMatte = await runner.matte(image);\n\t\t\tvisionBundle?.setMatteResult(frameIdx, personMatte);\n\t\t}\n\n\t\tif (wantsPose) {\n\t\t\tconst poseRes = await runner.pose(image);\n\t\t\tconst normalized = normalizePoseKeypoints(\n\t\t\t\tposeRes,\n\t\t\t\timage.width,\n\t\t\t\timage.height,\n\t\t\t);\n\t\t\tcurrentPoseRes = {\n\t\t\t\tpeople: matchPoseToTracks(normalized.people, frameObjects),\n\t\t\t};\n\t\t\tvisionBundle?.setPoseResult(frameIdx, currentPoseRes);\n\t\t}\n\n\t\tif (isMatteMode) {\n\t\t\tsubject = selectSubject(\n\t\t\t\top.matteSource === \"selfie\"\n\t\t\t\t\t? personMatteAsMask(personMatte)\n\t\t\t\t\t: mergeSubjectMask(\n\t\t\t\t\t\t\tframeMasks.length > 0\n\t\t\t\t\t\t\t\t? frameMasks\n\t\t\t\t\t\t\t\t: (visionBundle?.getMaskResult(frameIdx) ?? []),\n\t\t\t\t\t\t),\n\t\t\t\tnodeKeyStr,\n\t\t\t\tframeIdx,\n\t\t\t);\n\t\t\t// Opt-in: grow the subject into connected pixels that stand out from\n\t\t\t// the backdrop (catches edges the model's soft mask leaves behind).\n\t\t\tconst threshold = keyThresholdAt(heldFrame ?? frameIdx);\n\t\t\tif (subject && threshold !== undefined) {\n\t\t\t\tsubject = {\n\t\t\t\t\t...subject,\n\t\t\t\t\tmask: growMaskIntoForeground(\n\t\t\t\t\t\tsubject.mask,\n\t\t\t\t\t\tsubject.width,\n\t\t\t\t\t\tsubject.height,\n\t\t\t\t\t\tframePixels,\n\t\t\t\t\t\tthreshold,\n\t\t\t\t\t\top.featherRadius,\n\t\t\t\t\t),\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tif (cacheable && heldFrame !== undefined) {\n\t\t\tframeCache.set(source, heldFrame, {\n\t\t\t\tdetections,\n\t\t\t\tobjects: frameObjects,\n\t\t\t\tmasks: visionBundle ? frameMasks.map(toCachedMask) : null,\n\t\t\t\tsubject: subject ? { mask: toCachedMask(subject) } : null,\n\t\t\t\tkeyThreshold: keyThresholdAt(heldFrame),\n\t\t\t});\n\t\t}\n\t}\n\n\t// 6. Output to destination texture based on mode\n\n\t// Mask-family modes: the picture the results belong to, cut out on the GPU.\n\tif (isMatteMode) {\n\t\tconst picture = exact ? childTex : heldTex;\n\t\tif (!subject || !picture) {\n\t\t\tconst outPass = ctx.renderer.beginFrame(\n\t\t\t\tencoder,\n\t\t\t\ttargetView,\n\t\t\t\t{ r: 0, g: 0, b: 0, a: 0 },\n\t\t\t\ttargetWidth,\n\t\t\t\ttargetHeight,\n\t\t\t\t\"clear\",\n\t\t\t);\n\t\t\toutPass.end();\n\t\t\treturn;\n\t\t}\n\n\t\tif (!sharedCompositor || sharedCompositorFormat !== ctx.renderer.format) {\n\t\t\tsharedCompositor = new MatteCompositor(ctx.device, ctx.renderer.format);\n\t\t\tsharedCompositorFormat = ctx.renderer.format;\n\t\t}\n\t\tconst maskTex = sharedCompositor.uploadMask(\n\t\t\tnodeKeyStr,\n\t\t\tsubject.mask,\n\t\t\tsubject.width,\n\t\t\tsubject.height,\n\t\t);\n\t\tconst matteTex = matteTexture(ctx.device, nodeKeyStr, childTex);\n\t\tsharedCompositor.draw(\n\t\t\tencoder,\n\t\t\tmatteTex.createView(),\n\t\t\tpicture,\n\t\t\tmaskTex,\n\t\t\tmode,\n\t\t\tmode === \"crop\" ? (maskBounds(subject) ?? undefined) : undefined,\n\t\t);\n\t\tvisionBundle?.setStencilTexture(matteTex);\n\n\t\tconst outPass = ctx.renderer.beginFrame(\n\t\t\tencoder,\n\t\t\ttargetView,\n\t\t\t{ r: 0, g: 0, b: 0, a: 0 },\n\t\t\ttargetWidth,\n\t\t\ttargetHeight,\n\t\t\t\"clear\",\n\t\t);\n\t\tctx.renderer.drawTexture(outPass, matteTex, {\n\t\t\tx: 0,\n\t\t\ty: 0,\n\t\t\twidth: targetWidth,\n\t\t\theight: targetHeight,\n\t\t});\n\t\toutPass.end();\n\t\treturn;\n\t}\n\n\tif (mode === \"skeleton\") {\n\t\tif (!sharedSkeletonRenderer) {\n\t\t\tsharedSkeletonRenderer = new PoseSkeletonRenderer(ctx.device);\n\t\t}\n\t\tconst poseRes = visionBundle?.getPoseResult(frameIdx) ?? currentPoseRes;\n\t\tconst person = poseRes?.people[0];\n\t\tif (person && person.keypoints.length > 0) {\n\t\t\tconst skelTex = sharedSkeletonRenderer.renderToTexture(person.keypoints, {\n\t\t\t\twidth: targetWidth,\n\t\t\t\theight: targetHeight,\n\t\t\t});\n\t\t\tconst skelPass = ctx.renderer.beginFrame(\n\t\t\t\tencoder,\n\t\t\t\ttargetView,\n\t\t\t\t{ r: 0, g: 0, b: 0, a: 0 },\n\t\t\t\ttargetWidth,\n\t\t\t\ttargetHeight,\n\t\t\t\t\"clear\",\n\t\t\t);\n\t\t\tctx.renderer.drawTexture(skelPass, skelTex, {\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t\twidth: targetWidth,\n\t\t\t\theight: targetHeight,\n\t\t\t});\n\t\t\tskelPass.end();\n\t\t\treturn;\n\t\t}\n\t}\n\n\tif (mode === \"boxes\" || mode === \"tracking\") {\n\t\tconst outPass = ctx.renderer.beginFrame(\n\t\t\tencoder,\n\t\t\ttargetView,\n\t\t\t{ r: 0, g: 0, b: 0, a: 0 },\n\t\t\ttargetWidth,\n\t\t\ttargetHeight,\n\t\t\t\"clear\",\n\t\t);\n\t\tctx.renderer.drawTexture(outPass, childTex, {\n\t\t\tx: 0,\n\t\t\ty: 0,\n\t\t\twidth: targetWidth,\n\t\t\theight: targetHeight,\n\t\t});\n\n\t\tconst objects =\n\t\t\tframeObjects.length > 0\n\t\t\t\t? frameObjects\n\t\t\t\t: (visionBundle?.getObjectResult(frameIdx).objects ?? []);\n\t\tif (objects.length > 0) {\n\t\t\tconst boxColor = \"#38bdf8\";\n\t\t\tconst stroke = 2;\n\n\t\t\tfor (const obj of objects) {\n\t\t\t\tif (!obj.active) continue;\n\t\t\t\tconst b = obj.boundingBox;\n\n\t\t\t\tctx.renderer.drawRect(\n\t\t\t\t\toutPass,\n\t\t\t\t\t{ x: b.originX, y: b.originY, width: b.width, height: stroke },\n\t\t\t\t\tboxColor,\n\t\t\t\t);\n\t\t\t\tctx.renderer.drawRect(\n\t\t\t\t\toutPass,\n\t\t\t\t\t{\n\t\t\t\t\t\tx: b.originX,\n\t\t\t\t\t\ty: b.originY + b.height - stroke,\n\t\t\t\t\t\twidth: b.width,\n\t\t\t\t\t\theight: stroke,\n\t\t\t\t\t},\n\t\t\t\t\tboxColor,\n\t\t\t\t);\n\t\t\t\tctx.renderer.drawRect(\n\t\t\t\t\toutPass,\n\t\t\t\t\t{ x: b.originX, y: b.originY, width: stroke, height: b.height },\n\t\t\t\t\tboxColor,\n\t\t\t\t);\n\t\t\t\tctx.renderer.drawRect(\n\t\t\t\t\toutPass,\n\t\t\t\t\t{\n\t\t\t\t\t\tx: b.originX + b.width - stroke,\n\t\t\t\t\t\ty: b.originY,\n\t\t\t\t\t\twidth: stroke,\n\t\t\t\t\t\theight: b.height,\n\t\t\t\t\t},\n\t\t\t\t\tboxColor,\n\t\t\t\t);\n\n\t\t\t\tconst cornerLen = Math.min(16, b.width / 4, b.height / 4);\n\t\t\t\tctx.renderer.drawRect(\n\t\t\t\t\toutPass,\n\t\t\t\t\t{ x: b.originX, y: b.originY, width: cornerLen, height: stroke * 2 },\n\t\t\t\t\t\"#ffffff\",\n\t\t\t\t);\n\t\t\t\tctx.renderer.drawRect(\n\t\t\t\t\toutPass,\n\t\t\t\t\t{ x: b.originX, y: b.originY, width: stroke * 2, height: cornerLen },\n\t\t\t\t\t\"#ffffff\",\n\t\t\t\t);\n\n\t\t\t\tif (mode === \"tracking\") {\n\t\t\t\t\tctx.renderer.drawRect(\n\t\t\t\t\t\toutPass,\n\t\t\t\t\t\t{ x: obj.centerX - 3, y: obj.centerY - 3, width: 6, height: 6 },\n\t\t\t\t\t\t\"#ef4444\",\n\t\t\t\t\t\t3,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\toutPass.end();\n\t\treturn;\n\t}\n\n\t// Default passthrough: draw child texture to target\n\tconst outPass = ctx.renderer.beginFrame(\n\t\tencoder,\n\t\ttargetView,\n\t\t{ r: 0, g: 0, b: 0, a: 0 },\n\t\ttargetWidth,\n\t\ttargetHeight,\n\t\t\"clear\",\n\t);\n\tctx.renderer.drawTexture(outPass, childTex, {\n\t\tx: 0,\n\t\ty: 0,\n\t\twidth: targetWidth,\n\t\theight: targetHeight,\n\t});\n\toutPass.end();\n};\n\n/** Adapts a Selfie Segmenter matte to the instance-mask shape the composite path takes. */\nfunction personMatteAsMask(\n\tmatte: PersonMatte | undefined,\n): InstanceMask | undefined {\n\tif (!matte || matte.coverage <= 0) return undefined;\n\tconst area = Math.round(matte.coverage * matte.width * matte.height);\n\treturn {\n\t\tcategory: \"person\",\n\t\tmask: matte.mask,\n\t\twidth: matte.width,\n\t\theight: matte.height,\n\t\tarea,\n\t\tcoverage: matte.coverage,\n\t\tdetectionIndex: -1,\n\t};\n}\n","import { defineRenderer } from \"@framefields/node-sdk/renderer\";\nimport { VisionWebGPURenderer } from \"./webgpu-renderer.js\";\n\nexport default defineRenderer({\n\tWebGPURenderer: VisionWebGPURenderer,\n});\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@framefields/node-vision",
|
|
3
|
+
"version": "2.0.2",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"private": false,
|
|
6
|
+
"framefields": {
|
|
7
|
+
"type": "Vision",
|
|
8
|
+
"kind": "effect"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"module": "./dist/index.mjs",
|
|
12
|
+
"types": "./dist/index.d.mts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.mts",
|
|
16
|
+
"development": "./src/index.ts",
|
|
17
|
+
"import": "./dist/index.mjs",
|
|
18
|
+
"default": "./dist/index.mjs"
|
|
19
|
+
},
|
|
20
|
+
"./renderer": {
|
|
21
|
+
"types": "./dist/renderer.d.mts",
|
|
22
|
+
"development": "./src/renderers/index.ts",
|
|
23
|
+
"import": "./dist/renderer.mjs",
|
|
24
|
+
"default": "./dist/renderer.mjs"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"zod": "^4.3.6",
|
|
29
|
+
"@framefields/node-sdk": "2.0.2",
|
|
30
|
+
"@framefields/vision": "2.0.2",
|
|
31
|
+
"@framefields/core": "2.0.2"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^24.1.0",
|
|
35
|
+
"tsdown": "^0.19.0",
|
|
36
|
+
"typescript": "^5.8.3",
|
|
37
|
+
"webgpu": "^0.4.0",
|
|
38
|
+
"@framefields/tsconfig": "2.0.2"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsdown",
|
|
45
|
+
"dev": "tsdown --watch",
|
|
46
|
+
"clean": "rm -rf dist"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-frame vision results, so a frame is analysed once. A source frame gives
|
|
3
|
+
* the same detections and masks for the same model and settings, so replays,
|
|
4
|
+
* scrubbing back and later renders in the same process reuse them instead of
|
|
5
|
+
* running inference again.
|
|
6
|
+
*
|
|
7
|
+
* Masks are run-length encoded: they are long flat runs with soft edges, so a
|
|
8
|
+
* frame-sized mask shrinks from megabytes to tens of kilobytes.
|
|
9
|
+
*/
|
|
10
|
+
import type {
|
|
11
|
+
InstanceMask,
|
|
12
|
+
SegmentationResult,
|
|
13
|
+
TrackedObject,
|
|
14
|
+
} from "@framefields/vision";
|
|
15
|
+
|
|
16
|
+
type DetectedObject = SegmentationResult["detections"][number];
|
|
17
|
+
|
|
18
|
+
/** A frame-sized 0..255 mask as (value, run length) pairs. */
|
|
19
|
+
export interface RleMask {
|
|
20
|
+
readonly width: number;
|
|
21
|
+
readonly height: number;
|
|
22
|
+
readonly values: Uint8Array;
|
|
23
|
+
readonly lengths: Uint32Array;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let scratchValues = new Uint8Array(0);
|
|
27
|
+
let scratchLengths = new Uint32Array(0);
|
|
28
|
+
|
|
29
|
+
export function encodeMask(
|
|
30
|
+
mask: Uint8Array,
|
|
31
|
+
width: number,
|
|
32
|
+
height: number,
|
|
33
|
+
): RleMask {
|
|
34
|
+
// One pass into reused worst-case buffers; masks are flat almost
|
|
35
|
+
// everywhere, so the copies at the end are small.
|
|
36
|
+
if (scratchValues.length < mask.length) {
|
|
37
|
+
scratchValues = new Uint8Array(mask.length);
|
|
38
|
+
scratchLengths = new Uint32Array(mask.length);
|
|
39
|
+
}
|
|
40
|
+
const values = scratchValues;
|
|
41
|
+
const lengths = scratchLengths;
|
|
42
|
+
let runs = 0;
|
|
43
|
+
let start = 0;
|
|
44
|
+
for (let i = 1; i <= mask.length; i++) {
|
|
45
|
+
if (i === mask.length || mask[i] !== mask[start]) {
|
|
46
|
+
values[runs] = mask[start];
|
|
47
|
+
lengths[runs] = i - start;
|
|
48
|
+
runs++;
|
|
49
|
+
start = i;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
width,
|
|
54
|
+
height,
|
|
55
|
+
values: values.slice(0, runs),
|
|
56
|
+
lengths: lengths.slice(0, runs),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function decodeMask(rle: RleMask): Uint8Array {
|
|
61
|
+
const out = new Uint8Array(rle.width * rle.height);
|
|
62
|
+
let at = 0;
|
|
63
|
+
for (let r = 0; r < rle.values.length; r++) {
|
|
64
|
+
const end = at + rle.lengths[r];
|
|
65
|
+
if (rle.values[r] !== 0) out.fill(rle.values[r], at, end);
|
|
66
|
+
at = end;
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function rleBytes(rle: RleMask): number {
|
|
72
|
+
return rle.values.byteLength + rle.lengths.byteLength;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface CachedInstanceMask extends Omit<InstanceMask, "mask"> {
|
|
76
|
+
readonly rle: RleMask;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface VisionFrameResult {
|
|
80
|
+
readonly detections: readonly DetectedObject[];
|
|
81
|
+
readonly objects: readonly TrackedObject[];
|
|
82
|
+
/** Instance masks, kept only when a vision bundle reads them back. */
|
|
83
|
+
readonly masks: readonly CachedInstanceMask[] | null;
|
|
84
|
+
/** The composited subject (after hold and background keying); null = none. */
|
|
85
|
+
readonly subject: { readonly mask: CachedInstanceMask } | null;
|
|
86
|
+
/** Background-key threshold the subject was grown with. */
|
|
87
|
+
readonly keyThreshold?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function toCachedMask(mask: InstanceMask): CachedInstanceMask {
|
|
91
|
+
const { mask: data, ...rest } = mask;
|
|
92
|
+
return { ...rest, rle: encodeMask(data, mask.width, mask.height) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function fromCachedMask(mask: CachedInstanceMask): InstanceMask {
|
|
96
|
+
const { rle, ...rest } = mask;
|
|
97
|
+
return { ...rest, mask: decodeMask(rle) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resultBytes(result: VisionFrameResult): number {
|
|
101
|
+
let bytes =
|
|
102
|
+
512 + result.detections.length * 128 + result.objects.length * 256;
|
|
103
|
+
for (const m of result.masks ?? []) bytes += rleBytes(m.rle);
|
|
104
|
+
if (result.subject) bytes += rleBytes(result.subject.mask.rle);
|
|
105
|
+
return bytes;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Results by source (node + inference settings) and frame, least recently used out first. */
|
|
109
|
+
export class VisionFrameCache {
|
|
110
|
+
private readonly entries = new Map<string, VisionFrameResult>();
|
|
111
|
+
private bytes = 0;
|
|
112
|
+
|
|
113
|
+
constructor(private readonly maxBytes = 256 * 1024 * 1024) {}
|
|
114
|
+
|
|
115
|
+
get(source: string, frame: number): VisionFrameResult | undefined {
|
|
116
|
+
const key = `${source}#${frame}`;
|
|
117
|
+
const hit = this.entries.get(key);
|
|
118
|
+
if (hit) {
|
|
119
|
+
this.entries.delete(key);
|
|
120
|
+
this.entries.set(key, hit);
|
|
121
|
+
}
|
|
122
|
+
return hit;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
set(source: string, frame: number, result: VisionFrameResult): void {
|
|
126
|
+
const key = `${source}#${frame}`;
|
|
127
|
+
const old = this.entries.get(key);
|
|
128
|
+
if (old) {
|
|
129
|
+
this.bytes -= resultBytes(old);
|
|
130
|
+
this.entries.delete(key);
|
|
131
|
+
}
|
|
132
|
+
this.entries.set(key, result);
|
|
133
|
+
this.bytes += resultBytes(result);
|
|
134
|
+
for (const [k, v] of this.entries) {
|
|
135
|
+
if (this.bytes <= this.maxBytes) break;
|
|
136
|
+
this.entries.delete(k);
|
|
137
|
+
this.bytes -= resultBytes(v);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
clear(): void {
|
|
142
|
+
this.entries.clear();
|
|
143
|
+
this.bytes = 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get size(): number {
|
|
147
|
+
return this.entries.size;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/// <reference types="webgpu" />
|
|
2
|
+
/**
|
|
3
|
+
* GPU composite for the mask-family modes: the picture times the subject mask,
|
|
4
|
+
* in one full-screen pass. Replaces building the RGBA result on the CPU and
|
|
5
|
+
* uploading it; only the 8-bit mask goes to the GPU.
|
|
6
|
+
*
|
|
7
|
+
* - matte: picture × mask (premultiplied, so the background is transparent)
|
|
8
|
+
* - mask: white silhouette (rgb = mask, a = 1)
|
|
9
|
+
* - crop: matte, with the subject's bounding box stretched to the frame
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const WGSL = /* wgsl */ `
|
|
13
|
+
struct Params {
|
|
14
|
+
mode: u32, // 0 matte, 1 mask, 2 crop
|
|
15
|
+
_pad: u32,
|
|
16
|
+
cropMin: vec2<f32>, // uv of the subject bounds (crop)
|
|
17
|
+
cropMax: vec2<f32>,
|
|
18
|
+
_pad2: vec2<f32>,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
@group(0) @binding(0) var<uniform> params: Params;
|
|
22
|
+
@group(0) @binding(1) var picture: texture_2d<f32>;
|
|
23
|
+
@group(0) @binding(2) var mask: texture_2d<f32>;
|
|
24
|
+
@group(0) @binding(3) var samp: sampler;
|
|
25
|
+
|
|
26
|
+
struct VSOut {
|
|
27
|
+
@builtin(position) pos: vec4<f32>,
|
|
28
|
+
@location(0) uv: vec2<f32>,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
@vertex fn vs(@builtin(vertex_index) i: u32) -> VSOut {
|
|
32
|
+
var p = array<vec2<f32>, 3>(vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
|
33
|
+
var out: VSOut;
|
|
34
|
+
out.pos = vec4(p[i], 0.0, 1.0);
|
|
35
|
+
out.uv = vec2(p[i].x * 0.5 + 0.5, 0.5 - p[i].y * 0.5);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@fragment fn fs(in: VSOut) -> @location(0) vec4<f32> {
|
|
40
|
+
var uv = in.uv;
|
|
41
|
+
if (params.mode == 2u) {
|
|
42
|
+
uv = mix(params.cropMin, params.cropMax, uv);
|
|
43
|
+
}
|
|
44
|
+
let m = textureSampleLevel(mask, samp, uv, 0.0).r;
|
|
45
|
+
if (params.mode == 1u) {
|
|
46
|
+
return vec4(m, m, m, 1.0);
|
|
47
|
+
}
|
|
48
|
+
return textureSampleLevel(picture, samp, uv, 0.0) * m;
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
export type MatteMode = "matte" | "mask" | "crop";
|
|
53
|
+
|
|
54
|
+
export interface MatteBounds {
|
|
55
|
+
x0: number;
|
|
56
|
+
y0: number;
|
|
57
|
+
x1: number;
|
|
58
|
+
y1: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class MatteCompositor {
|
|
62
|
+
private readonly pipeline: GPURenderPipeline;
|
|
63
|
+
private readonly sampler: GPUSampler;
|
|
64
|
+
private readonly uniforms: GPUBuffer[] = [];
|
|
65
|
+
private uniformIndex = 0;
|
|
66
|
+
/** One 8-bit mask texture per node, rewritten each frame. */
|
|
67
|
+
private readonly masks = new Map<string, GPUTexture>();
|
|
68
|
+
|
|
69
|
+
constructor(
|
|
70
|
+
private readonly device: GPUDevice,
|
|
71
|
+
format: GPUTextureFormat,
|
|
72
|
+
) {
|
|
73
|
+
const module = device.createShaderModule({
|
|
74
|
+
label: "vision_matte_composite",
|
|
75
|
+
code: WGSL,
|
|
76
|
+
});
|
|
77
|
+
this.pipeline = device.createRenderPipeline({
|
|
78
|
+
label: "vision_matte_composite",
|
|
79
|
+
layout: "auto",
|
|
80
|
+
vertex: { module, entryPoint: "vs" },
|
|
81
|
+
fragment: { module, entryPoint: "fs", targets: [{ format }] },
|
|
82
|
+
primitive: { topology: "triangle-list" },
|
|
83
|
+
});
|
|
84
|
+
this.sampler = device.createSampler({
|
|
85
|
+
magFilter: "linear",
|
|
86
|
+
minFilter: "linear",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Uploads a frame-sized 0..255 mask for `key` and returns its texture. */
|
|
91
|
+
uploadMask(
|
|
92
|
+
key: string,
|
|
93
|
+
mask: Uint8Array,
|
|
94
|
+
width: number,
|
|
95
|
+
height: number,
|
|
96
|
+
): GPUTexture {
|
|
97
|
+
let tex = this.masks.get(key);
|
|
98
|
+
if (!tex || tex.width !== width || tex.height !== height) {
|
|
99
|
+
tex?.destroy();
|
|
100
|
+
tex = this.device.createTexture({
|
|
101
|
+
label: `vision_mask_${key}`,
|
|
102
|
+
size: [width, height],
|
|
103
|
+
format: "r8unorm",
|
|
104
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
105
|
+
});
|
|
106
|
+
this.masks.set(key, tex);
|
|
107
|
+
}
|
|
108
|
+
const aligned = Math.ceil(width / 256) * 256;
|
|
109
|
+
let bytes = mask;
|
|
110
|
+
if (aligned !== width) {
|
|
111
|
+
bytes = new Uint8Array(aligned * height);
|
|
112
|
+
for (let y = 0; y < height; y++) {
|
|
113
|
+
bytes.set(mask.subarray(y * width, (y + 1) * width), y * aligned);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
this.device.queue.writeTexture(
|
|
117
|
+
{ texture: tex },
|
|
118
|
+
bytes.buffer,
|
|
119
|
+
{ offset: bytes.byteOffset, bytesPerRow: aligned, rowsPerImage: height },
|
|
120
|
+
{ width, height },
|
|
121
|
+
);
|
|
122
|
+
return tex;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Clears `target` and draws `picture` through `mask` into it. */
|
|
126
|
+
draw(
|
|
127
|
+
encoder: GPUCommandEncoder,
|
|
128
|
+
target: GPUTextureView,
|
|
129
|
+
picture: GPUTexture,
|
|
130
|
+
mask: GPUTexture,
|
|
131
|
+
mode: MatteMode,
|
|
132
|
+
bounds?: MatteBounds,
|
|
133
|
+
): void {
|
|
134
|
+
const w = mask.width;
|
|
135
|
+
const h = mask.height;
|
|
136
|
+
const data = new Float32Array(8);
|
|
137
|
+
const view = new Uint32Array(data.buffer);
|
|
138
|
+
view[0] = mode === "matte" ? 0 : mode === "mask" ? 1 : 2;
|
|
139
|
+
if (mode === "crop" && bounds) {
|
|
140
|
+
data[2] = bounds.x0 / w;
|
|
141
|
+
data[3] = bounds.y0 / h;
|
|
142
|
+
data[4] = (bounds.x1 + 1) / w;
|
|
143
|
+
data[5] = (bounds.y1 + 1) / h;
|
|
144
|
+
} else {
|
|
145
|
+
data[2] = 0;
|
|
146
|
+
data[3] = 0;
|
|
147
|
+
data[4] = 1;
|
|
148
|
+
data[5] = 1;
|
|
149
|
+
if (mode === "crop") view[0] = 0;
|
|
150
|
+
}
|
|
151
|
+
// A small ring of uniform buffers: several vision nodes may draw in one frame.
|
|
152
|
+
if (this.uniformIndex >= this.uniforms.length) {
|
|
153
|
+
this.uniforms.push(
|
|
154
|
+
this.device.createBuffer({
|
|
155
|
+
size: data.byteLength,
|
|
156
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const uniform = this.uniforms[this.uniformIndex];
|
|
161
|
+
this.uniformIndex = (this.uniformIndex + 1) % 16;
|
|
162
|
+
this.device.queue.writeBuffer(uniform, 0, data);
|
|
163
|
+
|
|
164
|
+
const bindGroup = this.device.createBindGroup({
|
|
165
|
+
layout: this.pipeline.getBindGroupLayout(0),
|
|
166
|
+
entries: [
|
|
167
|
+
{ binding: 0, resource: { buffer: uniform } },
|
|
168
|
+
{ binding: 1, resource: picture.createView() },
|
|
169
|
+
{ binding: 2, resource: mask.createView() },
|
|
170
|
+
{ binding: 3, resource: this.sampler },
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
const pass = encoder.beginRenderPass({
|
|
174
|
+
label: "vision_matte_composite",
|
|
175
|
+
colorAttachments: [
|
|
176
|
+
{
|
|
177
|
+
view: target,
|
|
178
|
+
loadOp: "clear",
|
|
179
|
+
storeOp: "store",
|
|
180
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
});
|
|
184
|
+
pass.setPipeline(this.pipeline);
|
|
185
|
+
pass.setBindGroup(0, bindGroup);
|
|
186
|
+
pass.draw(3);
|
|
187
|
+
pass.end();
|
|
188
|
+
}
|
|
189
|
+
}
|