mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-15 17:16:16 -08:00
test(danmuku-mask): [PKG-MASK-02] reproduce historical lifecycle and failure paths
This commit is contained in:
1 parent
d81b881085
commit
ccf3d6ed14
10 files changed
+952
-18
No files matched your search
@@ -0,0 +1,259 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Historical runner includes fatal isolated Node rejection probes.
|
||||
import test from 'node:test'
|
||||
import { danmukuMaskEnvironment, danmukuMaskHistorical, flushMask } from './helpers/danmuku-mask.js'
|
||||
|
||||
// These assertions describe frozen historical defects, never candidate acceptance.
|
||||
const implementations = await danmukuMaskHistorical()
|
||||
const detailed = implementations.filter(item => ['frozen-source', 'published-1.1.0-main'].includes(item.name))
|
||||
|
||||
test('Historical SDK probes replace exactly five call sites and retain verified input identities', () => {
|
||||
const expected = JSON.parse(fs.readFileSync('refactor/baselines/danmuku-mask-failures-validation.json', 'utf8'))
|
||||
const actual = implementations.map(({ code: _code, ...identity }) => identity)
|
||||
assert.deepEqual(actual, expected.inputs, 'SDK seam transformations must remain exactly reproducible')
|
||||
assert.equal(implementations.length, 7)
|
||||
for (const item of implementations) {
|
||||
assert.match(item.inputSha256, /^[a-f0-9]{64}$/u)
|
||||
assert.match(item.transformedSha256, /^[a-f0-9]{64}$/u)
|
||||
if (!item.source) {
|
||||
assert.match(item.archiveSha256, /^[a-f0-9]{64}$/u)
|
||||
assert.match(item.probeSha256, /^[a-f0-9]{64}$/u)
|
||||
assert.deepEqual(item.counts, { setBackend: 2, createSegmenter: 1, toBinaryMask: 1, drawMask: 1 })
|
||||
assert.equal(item.replacements.length, 5)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for (const implementation of implementations) {
|
||||
test(`${implementation.name}: historical stop during initialization is undone by late initialization`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation, { pendingInit: true })
|
||||
const pending = env.result.start()
|
||||
await flushMask()
|
||||
assert.equal(env.initGates.length, 1)
|
||||
env.result.stop()
|
||||
assert.equal(env.style.maskImage, 'none')
|
||||
env.initGates[0].resolve()
|
||||
await pending
|
||||
await flushMask()
|
||||
assert.equal(env.draws.length, 1)
|
||||
assert.equal(env.style.maskImage, 'url(data:image/png;base64,historical-mask)')
|
||||
assert.equal(env.frames.size, 1)
|
||||
env.result.stop()
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical inference completion after destroy writes a mask and revives RAF`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation, { pendingSegment: true })
|
||||
await env.result.start()
|
||||
await flushMask()
|
||||
assert.equal(env.segmentGates.length, 1)
|
||||
env.destroy()
|
||||
env.segmentGates[0].resolve([{}])
|
||||
await flushMask()
|
||||
assert.equal(env.draws.length, 1)
|
||||
assert.equal(env.style.maskImage, 'url(data:image/png;base64,historical-mask)')
|
||||
assert.equal(env.frames.size, 1)
|
||||
assert.equal(env.created[0].disposed, 0)
|
||||
assert.deepEqual([...env.listeners].map(([name, callbacks]) => [name, callbacks.size]), [['ready', 1], ['destroy', 1]])
|
||||
env.result.stop()
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical repeated start leaves a second RAF chain after stop`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation)
|
||||
await env.result.start()
|
||||
await flushMask()
|
||||
await env.result.start()
|
||||
await flushMask()
|
||||
assert.equal(env.created.length, 1)
|
||||
assert.equal(env.frames.size, 2)
|
||||
env.result.stop()
|
||||
assert.equal(env.frames.size, 1)
|
||||
await env.frame()
|
||||
assert.equal(env.frames.size, 1)
|
||||
assert.equal(env.style.maskImage, 'url(data:image/png;base64,historical-mask)')
|
||||
env.result.stop()
|
||||
})
|
||||
}
|
||||
|
||||
for (const implementation of detailed) {
|
||||
for (const action of ['stop', 'destroy']) {
|
||||
test(`${implementation.name}: historical ${action} during initialization does not dispose the late model`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation, { pendingInit: true })
|
||||
const pending = env.result.start()
|
||||
await flushMask()
|
||||
if (action === 'stop')
|
||||
env.result.stop()
|
||||
else env.destroy()
|
||||
env.initGates[0].resolve()
|
||||
await pending
|
||||
await flushMask()
|
||||
assert.equal(env.created[0].disposed, 0)
|
||||
assert.equal(env.frames.size, 1)
|
||||
assert.equal(env.style.maskMode, 'alpha')
|
||||
env.result.stop()
|
||||
})
|
||||
}
|
||||
|
||||
test(`${implementation.name}: historical stop during inference cannot prevent its pending write`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation, { pendingSegment: true })
|
||||
await env.result.start()
|
||||
env.result.stop()
|
||||
assert.deepEqual(env.writes, ['none'])
|
||||
env.segmentGates[0].resolve([{}])
|
||||
await flushMask()
|
||||
assert.deepEqual(env.writes, ['none', 'url(data:image/png;base64,historical-mask)'])
|
||||
assert.equal(env.frames.size, 1)
|
||||
env.result.stop()
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical concurrent starts initialize two models and lose disposal ownership`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation, { pendingInit: true })
|
||||
const first = env.result.start()
|
||||
const second = env.result.start()
|
||||
await flushMask()
|
||||
assert.equal(env.created.length, 2)
|
||||
for (const gate of env.initGates) gate.resolve()
|
||||
await Promise.all([first, second])
|
||||
await flushMask()
|
||||
assert.equal(env.frames.size, 2)
|
||||
env.destroy()
|
||||
assert.equal(env.frames.size, 1)
|
||||
assert.deepEqual(env.created.map(instance => instance.disposed), [0, 0])
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical inference rejection logs once per frame and keeps retrying`, async () => {
|
||||
const error = new Error('controlled inference failure')
|
||||
const env = danmukuMaskEnvironment(implementation, { segmentError: error })
|
||||
await env.result.start()
|
||||
await flushMask()
|
||||
assert.equal(env.frames.size, 1)
|
||||
assert.equal(env.logs.length, 1)
|
||||
assert.deepEqual(env.logs[0], ['error', 'Error in segmentBody:', error])
|
||||
assert.equal(env.draws.length, 0)
|
||||
await env.frame()
|
||||
assert.equal(env.logs.length, 2)
|
||||
assert.equal(env.frames.size, 1)
|
||||
env.settings.segmentError = null
|
||||
await env.frame()
|
||||
assert.equal(env.draws.length, 1)
|
||||
env.result.stop()
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical fulfilled false WebGL selection does not trigger CPU fallback`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation, { backendResult: { webgl: false } })
|
||||
await env.result.start()
|
||||
await flushMask()
|
||||
assert.deepEqual(env.backends, ['webgl'])
|
||||
assert.equal(env.created.length, 1)
|
||||
assert.equal(env.logs.length, 0)
|
||||
env.result.stop()
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical rejected WebGL selection tries CPU and ignores its fulfilled boolean`, async () => {
|
||||
for (const cpu of [true, false]) {
|
||||
const env = danmukuMaskEnvironment(implementation, { backendError: { webgl: new Error('no WebGL') }, backendResult: { cpu } })
|
||||
await env.result.start()
|
||||
await flushMask()
|
||||
assert.deepEqual(env.backends, ['webgl', 'cpu'])
|
||||
assert.deepEqual(env.logs, [['warn', 'WebGL backend not available, falling back to CPU', 'no WebGL']])
|
||||
assert.equal(env.created.length, 1)
|
||||
env.result.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical CPU rejection rejects explicit start before creating a model`, async () => {
|
||||
const error = new Error('controlled CPU failure')
|
||||
const env = danmukuMaskEnvironment(implementation, { backendError: { webgl: new Error('no WebGL'), cpu: error } })
|
||||
await assert.rejects(env.result.start(), failure => failure === error)
|
||||
assert.deepEqual(env.backends, ['webgl', 'cpu'])
|
||||
assert.equal(env.created.length, 0)
|
||||
assert.equal(env.frames.size, 0)
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical model initialization rejection resolves start and polls without a model`, async () => {
|
||||
const error = new Error('controlled model init failure')
|
||||
const env = danmukuMaskEnvironment(implementation, { initError: error })
|
||||
assert.equal(await env.result.start(), undefined)
|
||||
assert.deepEqual(env.logs, [['error', 'Error initializing segmenter:', error]])
|
||||
assert.equal(env.frames.size, 1)
|
||||
await env.frame()
|
||||
assert.equal(env.created.length, 1)
|
||||
assert.equal(env.inferences.length, 0)
|
||||
env.result.stop()
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical missing danmuku rejects start after allocating the model and makes stop throw`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation, { noDanmuku: true })
|
||||
await assert.rejects(env.result.start(), error => error.name === 'TypeError' && error.message.includes('style'))
|
||||
assert.throws(() => env.result.stop(), error => error.name === 'TypeError' && error.message.includes('style'))
|
||||
assert.equal(env.created.length, 1)
|
||||
assert.equal(env.created[0].disposed, 0)
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical unavailable 2D context logs read failures and keeps the frame loop`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation, { noContext: true })
|
||||
await env.result.start()
|
||||
await flushMask()
|
||||
assert.equal(env.logs.length, 1)
|
||||
assert.equal(env.logs[0][2].name, 'TypeError')
|
||||
assert.match(env.logs[0][2].message, /getImageData/u)
|
||||
assert.equal(env.frames.size, 1)
|
||||
assert.equal(env.writes.length, 0)
|
||||
env.result.stop()
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical canvas allocation failure rejects start but leaves the initialized model owned`, async () => {
|
||||
const error = new Error('controlled canvas allocation failure')
|
||||
const env = danmukuMaskEnvironment(implementation, { canvasError: error })
|
||||
await assert.rejects(env.result.start(), failure => failure === error)
|
||||
assert.equal(env.created.length, 1)
|
||||
assert.equal(env.created[0].disposed, 0)
|
||||
assert.equal(env.frames.size, 0)
|
||||
env.destroy()
|
||||
assert.equal(env.created[0].disposed, 0)
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical canvas read security failure is logged and retried without clearing the previous mask`, async () => {
|
||||
const error = new Error('controlled canvas taint')
|
||||
error.name = 'SecurityError'
|
||||
const env = danmukuMaskEnvironment(implementation, { readError: error })
|
||||
await env.result.start()
|
||||
await flushMask()
|
||||
assert.equal(env.logs[0][2], error)
|
||||
assert.equal(env.style.maskImage, 'url(prior-mask)')
|
||||
await env.frame()
|
||||
assert.equal(env.logs.length, 2)
|
||||
env.result.stop()
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical zero RAF id survives stop and retained ready listeners restart after destroy`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation, { firstFrame: 0, paused: true })
|
||||
await env.result.start()
|
||||
assert.deepEqual([...env.frames.keys()], [0])
|
||||
env.destroy()
|
||||
assert.deepEqual([...env.frames.keys()], [0])
|
||||
assert.deepEqual(env.cancelled, [])
|
||||
env.emit('ready')
|
||||
await flushMask()
|
||||
assert.equal(env.frames.size, 2)
|
||||
assert.equal(env.created[0].disposed, 0)
|
||||
})
|
||||
|
||||
for (const scenario of ['ready-backend', 'missing-video']) {
|
||||
test(`${implementation.name}: historical ${scenario} unhandled rejection is observed only in a fatal isolated child`, () => {
|
||||
const script = `import { danmukuMaskHistorical, danmukuMaskEnvironment, flushMask } from './test/helpers/danmuku-mask.js';
|
||||
const implementation = (await danmukuMaskHistorical()).find(item => item.name === ${JSON.stringify(implementation.name)});
|
||||
const settings = ${scenario === 'ready-backend' ? '{backendError: {webgl: new Error("no WebGL"), cpu: new Error("MASK02_READY_BACKEND_REJECTION")}}' : '{noVideo: true}'};
|
||||
const env = danmukuMaskEnvironment(implementation, settings);
|
||||
${scenario === 'ready-backend' ? 'env.emit("ready");' : 'await env.result.start();'}
|
||||
await flushMask();`
|
||||
const child = spawnSync(process.execPath, ['--unhandled-rejections=strict', '--input-type=module', '-e', script], { encoding: 'utf8', timeout: 15000 })
|
||||
assert.equal(child.error, undefined)
|
||||
assert.equal(child.status, 1)
|
||||
assert.match(child.stderr, scenario === 'ready-backend' ? /MASK02_READY_BACKEND_REJECTION/u : /Cannot read properties of undefined \(reading 'paused'\)/u)
|
||||
assert.doesNotMatch(child.stderr, /AssertionError|Model network access is forbidden/u)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import vm from 'node:vm'
|
||||
import { transform } from 'esbuild'
|
||||
import ts from 'typescript'
|
||||
import { verifyDanmukuMaskContract } from '../../refactor/scripts/danmuku-mask-contract.mjs'
|
||||
import { hash, readMember } from '../../refactor/scripts/releases.mjs'
|
||||
|
||||
const sourceFile = 'packages/artplayer-plugin-danmuku-mask/src/index.js'
|
||||
let historical
|
||||
export function danmukuMaskHistorical() {
|
||||
historical ||= loadHistorical()
|
||||
return historical
|
||||
}
|
||||
|
||||
function evaluate(code, additions = {}) {
|
||||
const module = { exports: {} }
|
||||
const context = {
|
||||
module,
|
||||
exports: module.exports,
|
||||
console: { log() {}, warn() {}, error() {} },
|
||||
fetch() { throw new Error('Model network access is forbidden in historical probes') },
|
||||
...additions,
|
||||
}
|
||||
context.global = context
|
||||
context.self = context
|
||||
vm.runInNewContext(code, context, { timeout: 5000 })
|
||||
return module.exports.default || module.exports
|
||||
}
|
||||
|
||||
function instrument(code, factory, label) {
|
||||
const text = factory.toString()
|
||||
const start = code.indexOf(text)
|
||||
assert(start >= 0 && !code.includes(text, start + 1), 'Exported factory must map uniquely to the evaluated bytes')
|
||||
const prefix = 'const factory = ('
|
||||
const parsed = ts.createSourceFile('factory.js', `${prefix}${text});`, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS)
|
||||
assert.equal(parsed.parseDiagnostics.length, 0)
|
||||
const replacements = []
|
||||
const counts = { setBackend: 0, createSegmenter: 0, toBinaryMask: 0, drawMask: 0 }
|
||||
const configs = new Set()
|
||||
function findConfigs(node) {
|
||||
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isObjectLiteralExpression(node.initializer)) {
|
||||
const properties = node.initializer.properties.map(property => property.name?.getText(parsed))
|
||||
if (properties.includes('runtime') && properties.includes('modelType'))
|
||||
configs.add(node.name.text)
|
||||
}
|
||||
ts.forEachChild(node, findConfigs)
|
||||
}
|
||||
findConfigs(parsed)
|
||||
function visit(node) {
|
||||
if (ts.isCallExpression(node)) {
|
||||
const args = node.arguments
|
||||
const callee = node.expression.getText(parsed)
|
||||
let target
|
||||
if (args.length === 1 && ts.isStringLiteral(args[0]) && ['webgl', 'cpu'].includes(args[0].text))
|
||||
target = 'setBackend'
|
||||
else if (callee === 'createSegmenter' || callee.endsWith('.createSegmenter') || callee.includes('is not a supported model name.') || (args.length === 2 && ts.isIdentifier(args[1]) && configs.has(args[1].text)))
|
||||
target = 'createSegmenter'
|
||||
else if (callee === 'toBinaryMask' || callee.endsWith('.toBinaryMask') || callee.includes('mask.toImageData') || (args.length === 5 && args[3].getText(parsed).endsWith('.drawContour') && args[4].getText(parsed).endsWith('.foregroundThreshold')))
|
||||
target = 'toBinaryMask'
|
||||
else if (args.length === 5 && args[3].getText(parsed).endsWith('.opacity') && args[4].getText(parsed).endsWith('.maskBlurAmount'))
|
||||
target = 'drawMask'
|
||||
if (target) {
|
||||
counts[target]++
|
||||
replacements.push({ start: start + node.expression.getStart(parsed) - prefix.length, end: start + node.expression.end - prefix.length, target, original: callee })
|
||||
// Inline SDK bodies are replaced as one expression, never traversed as owned lifecycle code.
|
||||
return
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(parsed)
|
||||
assert.deepEqual(counts, { setBackend: 2, createSegmenter: 1, toBinaryMask: 1, drawMask: 1 }, label)
|
||||
let result = code
|
||||
for (const replacement of [...replacements].sort((a, b) => b.start - a.start))
|
||||
result = `${result.slice(0, replacement.start)}globalThis.__maskProbe.${replacement.target}${result.slice(replacement.end)}`
|
||||
return { code: result, counts, replacements: replacements.map(({ start, end, target, original }) => ({ start, end, target, originalSha256: hash(original) })) }
|
||||
}
|
||||
|
||||
async function loadHistorical() {
|
||||
const contract = await verifyDanmukuMaskContract()
|
||||
const implementations = []
|
||||
const source = contract.sources.get(sourceFile)
|
||||
const sourceCode = (await transform(source, { format: 'cjs', target: 'es2020' })).code
|
||||
implementations.push({ name: 'frozen-source', code: sourceCode, inputSha256: hash(source), transformedSha256: hash(sourceCode), source: true, replacements: [] })
|
||||
for (const release of [contract.baseline.release, ...contract.baseline.previous]) {
|
||||
const archive = contract.archives.get(release.version)
|
||||
for (const entry of ['main', 'legacy', 'module']) {
|
||||
const member = `package/${release.manifest[entry].replace(/^\.\//u, '')}`
|
||||
const input = readMember(archive, member).toString()
|
||||
const code = entry === 'module' ? (await transform(input, { format: 'cjs', target: 'es2020' })).code : input
|
||||
const modified = instrument(code, evaluate(code), `${release.version}/${entry}`)
|
||||
implementations.push({ name: `published-${release.version}-${entry}`, archiveSha256: release.sha256, member, inputSha256: hash(input), transformedSha256: hash(code), probeSha256: hash(modified.code), ...modified })
|
||||
}
|
||||
}
|
||||
return implementations
|
||||
}
|
||||
|
||||
export const flushMask = () => new Promise(resolve => setImmediate(resolve))
|
||||
function deferred() {
|
||||
let resolve
|
||||
let reject
|
||||
const promise = new Promise((accept, fail) => {
|
||||
resolve = accept
|
||||
reject = fail
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
export function danmukuMaskEnvironment(implementation, settings = {}) {
|
||||
const env = {
|
||||
frames: new Map(),
|
||||
listeners: new Map(),
|
||||
logs: [],
|
||||
backends: [],
|
||||
created: [],
|
||||
inferences: [],
|
||||
masks: [],
|
||||
draws: [],
|
||||
initGates: [],
|
||||
segmentGates: [],
|
||||
writes: [],
|
||||
cancelled: [],
|
||||
settings,
|
||||
}
|
||||
let nextFrame = settings.firstFrame ?? 1
|
||||
const style = { maskMode: 'luminance', maskImage: 'url(prior-mask)' }
|
||||
Object.defineProperty(style, 'maskImage', {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => env.writes.at(-1) ?? 'url(prior-mask)',
|
||||
set: value => env.writes.push(value),
|
||||
})
|
||||
const context = {
|
||||
getImageData() {
|
||||
if (settings.readError)
|
||||
throw settings.readError
|
||||
return { data: new Uint8ClampedArray([255, 255, 255, 255]) }
|
||||
},
|
||||
putImageData() {},
|
||||
}
|
||||
env.canvas = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
getContext: () => settings.noContext ? null : context,
|
||||
toDataURL: () => 'data:image/png;base64,historical-mask',
|
||||
}
|
||||
env.video = settings.noVideo ? undefined : { paused: settings.paused ?? false, ended: false, videoWidth: 2, videoHeight: 1 }
|
||||
env.style = style
|
||||
env.art = {
|
||||
isDestroy: false,
|
||||
template: { $video: env.video, $danmuku: settings.noDanmuku ? undefined : { style } },
|
||||
on(name, callback) {
|
||||
if (!env.listeners.has(name))
|
||||
env.listeners.set(name, new Set())
|
||||
env.listeners.get(name).add(callback)
|
||||
},
|
||||
off(name, callback) { env.listeners.get(name)?.delete(callback) },
|
||||
}
|
||||
const probe = {
|
||||
SupportedModels: { MediaPipeSelfieSegmentation: 'selfie' },
|
||||
async setBackend(name) {
|
||||
env.backends.push(name)
|
||||
if (settings.backendError?.[name])
|
||||
throw settings.backendError[name]
|
||||
return settings.backendResult?.[name] ?? true
|
||||
},
|
||||
async createSegmenter(model, config) {
|
||||
const instance = {
|
||||
model,
|
||||
config,
|
||||
disposed: 0,
|
||||
dispose() { instance.disposed++ },
|
||||
async segmentPeople(video) {
|
||||
env.inferences.push({ instance, video })
|
||||
if (settings.segmentError)
|
||||
throw settings.segmentError
|
||||
if (settings.pendingSegment) {
|
||||
const gate = deferred()
|
||||
env.segmentGates.push(gate)
|
||||
return gate.promise
|
||||
}
|
||||
return settings.empty ? [] : [{}]
|
||||
},
|
||||
}
|
||||
env.created.push(instance)
|
||||
if (settings.initError)
|
||||
throw settings.initError
|
||||
if (settings.pendingInit) {
|
||||
const gate = deferred()
|
||||
env.initGates.push(gate)
|
||||
await gate.promise
|
||||
}
|
||||
return instance
|
||||
},
|
||||
async toBinaryMask(...args) {
|
||||
env.masks.push(args)
|
||||
return {}
|
||||
},
|
||||
async drawMask(...args) { env.draws.push(args) },
|
||||
}
|
||||
const factory = evaluate(implementation.code, {
|
||||
__maskProbe: probe,
|
||||
require(name) {
|
||||
assert(['@tensorflow-models/body-segmentation', '@tensorflow/tfjs-core', '@tensorflow/tfjs-backend-webgl', '@tensorflow/tfjs-backend-cpu'].includes(name))
|
||||
return probe
|
||||
},
|
||||
console: { log() {}, warn: (...args) => env.logs.push(['warn', ...args]), error: (...args) => env.logs.push(['error', ...args]) },
|
||||
document: { createElement(tag) {
|
||||
assert.equal(tag, 'canvas')
|
||||
if (settings.canvasError)
|
||||
throw settings.canvasError
|
||||
return env.canvas
|
||||
} },
|
||||
requestAnimationFrame(callback) {
|
||||
const id = nextFrame++
|
||||
env.frames.set(id, callback)
|
||||
return id
|
||||
},
|
||||
cancelAnimationFrame(id) {
|
||||
env.cancelled.push(id)
|
||||
env.frames.delete(id)
|
||||
},
|
||||
})
|
||||
env.result = factory(settings.option)(env.art)
|
||||
env.emit = (name) => {
|
||||
for (const callback of [...(env.listeners.get(name) || [])]) callback()
|
||||
}
|
||||
env.destroy = () => {
|
||||
env.art.isDestroy = true
|
||||
env.emit('destroy')
|
||||
}
|
||||
env.frame = async () => {
|
||||
const frames = [...env.frames.values()]
|
||||
env.frames.clear()
|
||||
for (const callback of frames) await callback()
|
||||
await flushMask()
|
||||
}
|
||||
return env
|
||||
}
|
||||
Reference in new issue
Block a user