Files
juzi/taobao/utils/native-face-webgl.js
T
xsl 774376fa16 feat: 完善淘宝小程序拍照、报告与登录流程
接入本地实时识别和自动三连拍,调整报告裁切、商品跳转、分享标题及居中页眉;完善游客会话、后台渠道隔离、部署脚本与回归测试。
2026-11-04 15:07:51 +08:00

338 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Explicit WebGL1 only. Native capability flags are never forged. Compatibility
// profiles may disable packing/float downloads, but must pass real shaders and
// the identical numerical selftest. There is no CPU fallback.
var nativeProbe = require('./native-face-webgl-probe');
function createAttempt(face, platform, profile) {
var tf = face.tf, canvas = null, gl = null, gpgpu = null, backend = null, removed = false;
var disposed = false, lost = false, originals = [], resources = [], drawCalls = 0, pixelReads = 0;
var copyBuffers = false, translateFramebufferY = false, bufferWrapped = false, geometryWrapped = false, boundFramebuffer = null;
var stripQuad = false, stripGuardReady = false, elementBuffer = null, quadBuffers = [];
var status = { requested: true, selftest: 'not_run', stage: 'uninitialized', code: 'NONE', contextLost: false,
profile: profile, matmulDraws: 0, convDraws: 0, pixelReads: 0, floatReads: 0, byteReads: 0, selftestMs: 0, warmupMs: 0,
matmul: null, conv: null, nativeProbe: null, glError: 0, framebufferStatus: 0,
nativeStrategy: 'standard', nativeAttempts: 0, nativeInitialStage: 'not_run', nativeInitialCode: 'NONE', drawStrategy: 'indexed', quadStripDraws: 0 };
function summary() {
var value = {};
Object.keys(status).forEach(function (key) {
if (key === 'matmul' || key === 'conv' || key === 'nativeProbe') {
var metrics = status[key]; value[key] = metrics ? {} : null;
if (metrics) Object.keys(metrics).forEach(function (name) { value[key][name] = metrics[name]; });
} else value[key] = status[key];
});
return value;
}
// These values only describe fixed synthetic selftest arrays. Never call
// this function with model output, a camera frame or face coordinates.
function numericalMetrics(actual, expected) {
var length = actual && typeof actual.length === 'number' ? actual.length : 0;
var compared = 0;
var result = { actualLength: length, nonFiniteCount: 0, mismatchCount: 0, maxAbsError: 0,
firstExpected: expected.length ? expected[0] : null, firstActual: null, expectedChecksum: 0, actualChecksum: 0 };
for (var index = 0; index < expected.length; index++) result.expectedChecksum += expected[index];
for (var index = 0; index < length; index++) {
var number = actual[index];
if (typeof number !== 'number' || !isFinite(number)) { result.nonFiniteCount++; result.mismatchCount++; continue; }
if (!index) result.firstActual = number;
result.actualChecksum += number;
var difference = index < expected.length ? Math.abs(number - expected[index]) : null;
if (difference !== null) compared++;
if (difference === null || difference > 0.0001) result.mismatchCount++;
if (difference !== null && difference > result.maxAbsError) result.maxAbsError = difference;
}
if (length < expected.length) result.mismatchCount += expected.length - length;
if (result.nonFiniteCount || !isFinite(result.actualChecksum)) result.actualChecksum = null;
if (!compared || !isFinite(result.maxAbsError)) result.maxAbsError = null;
return result;
}
function fail(code, stage) {
status.code = code; status.stage = stage;
if (status.selftest !== 'passed') status.selftest = 'failed';
var error = new Error('本地 WebGL 人脸引擎不可用(' + code + ')');
error.code = 'NATIVE_FACE_WEBGL_' + code; error.gpu = summary(); return error;
}
function check() {
if (disposed) throw fail('DISPOSED', 'disposed');
try { if (gl && gl.isContextLost()) lost = true; }
catch (_) { lost = true; }
if (lost) { status.contextLost = true; throw fail('CONTEXT_LOST', 'context_lost'); }
}
function lostEvent() { lost = true; status.contextLost = true; status.code = 'CONTEXT_LOST'; status.stage = 'context_lost'; }
function intercept(name, replacement) {
var original = gl[name];
if (typeof original !== 'function') throw fail('INSTRUMENTATION_FAILED', 'get_context');
try { gl[name] = replacement(original); } catch (_) { throw fail('INSTRUMENTATION_FAILED', 'get_context'); }
var actual = gl[name];
if (actual === original || typeof actual !== 'function') throw fail('INSTRUMENTATION_FAILED', 'get_context');
originals.push({ name: name, original: original });
}
function track() {
intercept('drawElements', function (original) { return function (mode, count, type, offset) {
if (stripQuad) {
var entry = quadBuffer(elementBuffer);
if (mode !== gl.TRIANGLES || count !== 6 || type !== gl.UNSIGNED_SHORT || offset !== 0 || !entry || !entry.valid) throw fail('DRAW_UNSUPPORTED', status.stage);
return gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
drawCalls++; return original.apply(gl, arguments);
}; });
intercept('drawArrays', function (original) { return function () {
drawCalls++; if (status.drawStrategy === 'quad_strip') status.quadStripDraws++;
return original.apply(gl, arguments);
}; });
intercept('readPixels', function (original) { return function () {
pixelReads++; status.pixelReads = pixelReads;
if (arguments[5] === gl.FLOAT) status.floatReads++;
if (arguments[5] === gl.UNSIGNED_BYTE) status.byteReads++;
return original.apply(gl, arguments);
}; });
['Buffer', 'Framebuffer', 'Texture', 'Program', 'Shader', 'Renderbuffer'].forEach(function (kind) {
var deleted = gl['delete' + kind];
if (typeof deleted !== 'function') throw fail('INSTRUMENTATION_FAILED', 'get_context');
intercept('create' + kind, function (original) { return function () {
var value = original.apply(gl, arguments);
if (value) resources.push({ kind: kind, value: value, deleted: false, remove: deleted });
return value;
}; });
intercept('delete' + kind, function (original) { return function (value) {
for (var index = resources.length - 1; index >= 0; index--) if (resources[index].kind === kind && resources[index].value === value) { resources[index].deleted = true; break; }
return original.apply(gl, arguments);
}; });
});
}
function glError(stage) {
check();
status.glError = gl.getError(); status.framebufferStatus = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
if (status.glError !== gl.NO_ERROR || status.framebufferStatus !== gl.FRAMEBUFFER_COMPLETE) throw fail('READBACK_FAILED', stage);
}
function quadBuffer(buffer) {
for (var index = 0; index < quadBuffers.length; index++) if (quadBuffers[index].buffer === buffer) return quadBuffers[index];
return null;
}
function fixedQuadIndices(data) {
try {
var bytes = ArrayBuffer.isView(data) ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
: data instanceof ArrayBuffer ? new Uint8Array(data) : null;
if (!bytes || bytes.byteLength !== 12) return false;
var copy = new Uint8Array(12); copy.set(bytes);
var values = new Uint16Array(copy.buffer), expected = [0, 1, 2, 2, 1, 3];
for (var index = 0; index < 6; index++) if (values[index] !== expected[index]) return false;
return true;
} catch (_) { return false; }
}
function guardStripQuad() {
if (stripGuardReady) { stripQuad = true; return; }
// Only our pinned TFJS quad is equivalent to a four-vertex strip. Record
// its actual uploaded index bytes; other geometry must never be converted.
intercept('bindBuffer', function (original) { return function (target, buffer) {
var result = original.apply(gl, arguments); if (target === gl.ELEMENT_ARRAY_BUFFER) elementBuffer = buffer; return result;
}; });
intercept('bufferData', function (original) { return function (target, data) {
var result = original.apply(gl, arguments);
if (target === gl.ELEMENT_ARRAY_BUFFER && elementBuffer != null) {
var entry = quadBuffer(elementBuffer);
if (!entry) { entry = { buffer: elementBuffer, valid: false }; quadBuffers.push(entry); }
entry.valid = fixedQuadIndices(data);
}
return result;
}; });
if (typeof gl.bufferSubData === 'function') intercept('bufferSubData', function (original) { return function (target) {
var result = original.apply(gl, arguments), entry = target === gl.ELEMENT_ARRAY_BUFFER ? quadBuffer(elementBuffer) : null;
if (entry) entry.valid = false; return result;
}; });
intercept('deleteBuffer', function (original) { return function (buffer) {
var entry = quadBuffer(buffer); if (entry) entry.valid = false;
if (elementBuffer === buffer) elementBuffer = null;
return original.apply(gl, arguments);
}; });
stripGuardReady = true; stripQuad = true;
}
function nativeCompatibility() {
var strategies = ['standard', 'buffer_copy', 'framebuffer_y', 'buffer_copy_framebuffer_y'];
for (var index = 0; index < strategies.length; index++) {
status.nativeStrategy = strategies[index]; status.nativeAttempts++; status.drawStrategy = 'indexed'; stripQuad = false;
copyBuffers = index === 1 || index === 3; translateFramebufferY = index >= 2;
if (copyBuffers && !bufferWrapped) {
// ArrayBuffer is a standard bufferData input. Copy only the visible
// bytes of a typed view, never the rest of its underlying allocation.
intercept('bufferData', function (original) { return function (target, data, usage) {
if (copyBuffers && ArrayBuffer.isView(data)) {
var copy = new Uint8Array(data.byteLength); copy.set(new Uint8Array(data.buffer, data.byteOffset, data.byteLength)); data = copy.buffer;
}
return original.call(gl, target, data, usage);
}; }); bufferWrapped = true;
}
if (translateFramebufferY && !geometryWrapped) {
// This is a measured fallback, never a device-name assumption. It is
// selected only if all asymmetric native probes actually pass with it.
intercept('bindFramebuffer', function (original) { return function (target, value) {
var result = original.apply(gl, arguments); if (target === gl.FRAMEBUFFER) boundFramebuffer = value; return result;
}; });
intercept('viewport', function (original) { return function (x, y, width, height) {
return original.call(gl, x, translateFramebufferY && boundFramebuffer != null ? canvas.height - y - height : y, width, height);
}; });
intercept('scissor', function (original) { return function (x, y, width, height) {
return original.call(gl, x, translateFramebufferY && boundFramebuffer != null ? canvas.height - y - height : y, width, height);
}; }); geometryWrapped = true;
}
status.nativeProbe = nativeProbe(gl); check();
if (status.nativeProbe.stage === 'passed' && status.nativeProbe.code === 'NONE') return;
if (status.nativeAttempts === 1) { status.nativeInitialStage = status.nativeProbe.stage; status.nativeInitialCode = status.nativeProbe.code; }
if (status.nativeProbe.stage === 'constant_indexed' && status.nativeProbe.code === 'VALUE_MISMATCH' && status.nativeProbe.constantArrays === 'passed') {
// The device has already rendered a real four-vertex strip correctly.
// Revalidate asymmetric pixels/textures with that path, then guard all
// TFJS indexed entry points before loading any model.
status.drawStrategy = 'quad_strip'; status.nativeAttempts++;
status.nativeProbe = nativeProbe(gl, { drawStrategy: 'quad_strip' }); check();
if (status.nativeProbe.stage === 'passed' && status.nativeProbe.code === 'NONE') { guardStripQuad(); return; }
}
// Buffer/coordinate variants cannot fix missing APIs, bad shader source,
// failed linking, GL errors or an unsupported synchronous read contract.
if (status.nativeProbe.code !== 'VALUE_MISMATCH') break;
}
throw fail('PREFLIGHT_FAILED', 'native_preflight');
}
function numericalSelftest() {
var tensors = [], started = Date.now();
status.selftest = 'running'; status.stage = 'matmul';
function keep(tensor) { tensors.push(tensor); return tensor; }
function cleanup() { tensors.forEach(function (tensor) { try { tensor.dispose(); } catch (_) {} }); }
var work = Promise.resolve().then(function () {
check();
var identity = [], expected = [];
for (var index = 0; index < 256; index++) { identity.push(Math.floor(index / 16) === index % 16 ? 1 : 0); expected.push(index % 23); }
var a = keep(tf.tensor2d(identity, [16, 16])), b = keep(tf.tensor2d(expected, [16, 16]));
var draws = drawCalls, reads = pixelReads, product;
try { product = keep(tf.matMul(a, b)); } catch (error) { status.matmulDraws = drawCalls - draws; check();
if (error && error.code === 'NATIVE_FACE_WEBGL_DRAW_UNSUPPORTED') throw error; throw fail('MATMUL_FAILED', 'matmul'); }
status.stage = 'readback';
return product.data().then(function (actual) {
check(); status.matmulDraws = drawCalls - draws;
status.matmul = numericalMetrics(actual, expected);
glError('readback');
if (status.matmul.nonFiniteCount || status.matmul.mismatchCount || status.matmul.actualLength !== 256 || !status.matmulDraws) throw fail('MATMUL_FAILED', 'matmul');
if (pixelReads <= reads) throw fail('READBACK_FAILED', 'readback');
}, function () { status.matmulDraws = drawCalls - draws; check(); throw fail('READBACK_FAILED', 'readback'); });
}).then(function () {
status.stage = 'conv2d'; check();
var values = []; for (var index = 0; index < 256; index++) values.push(index % 4 + 1);
var input = keep(tf.tensor4d(values, [1, 8, 8, 4])), filter = keep(tf.tensor4d([1, 2, 1, 2, 1, 2, 1, 2], [1, 1, 4, 2]));
var draws = drawCalls, reads = pixelReads, result;
try { result = keep(tf.conv2d(input, filter, 1, 'valid')); } catch (error) { status.convDraws = drawCalls - draws; check();
if (error && error.code === 'NATIVE_FACE_WEBGL_DRAW_UNSUPPORTED') throw error; throw fail('CONV_FAILED', 'conv2d'); }
status.stage = 'readback';
return result.data().then(function (actual) {
check(); status.convDraws = drawCalls - draws;
var expected = []; for (var index = 0; index < 128; index++) expected.push(index % 2 ? 20 : 10);
status.conv = numericalMetrics(actual, expected);
glError('readback');
if (status.conv.nonFiniteCount || status.conv.mismatchCount || status.conv.actualLength !== 128 || !status.convDraws) throw fail('CONV_FAILED', 'conv2d');
if (pixelReads <= reads) throw fail('READBACK_FAILED', 'readback');
status.pixelReads = pixelReads; status.selftestMs = Date.now() - started;
status.selftest = 'passed'; status.code = 'NONE';
}, function () { status.convDraws = drawCalls - draws; check(); throw fail('READBACK_FAILED', 'readback'); });
});
return work.then(function () { cleanup(); }, function (error) { status.selftestMs = Date.now() - started; cleanup(); throw error; });
}
function initialize() {
return Promise.resolve().then(function () {
status.stage = 'create_canvas'; check();
if (!platform || typeof platform.createOffscreenCanvas !== 'function') throw fail('API_MISSING', 'create_canvas');
try { canvas = platform.createOffscreenCanvas(32, 32); } catch (_) { throw fail('CANVAS_FAILED', 'create_canvas'); }
if (!canvas || typeof canvas.getContext !== 'function') throw fail('CANVAS_FAILED', 'create_canvas');
status.stage = 'get_context';
// Taobao's native readPixels flips Y by default. Tensor textures need
// standard framebuffer coordinates and row order (official doc121761).
try { gl = canvas.getContext('webgl', { enable_flip_y_after_read_pixels: false }); } catch (_) { throw fail('CONTEXT_FAILED', 'get_context'); }
if (!gl || typeof gl.isContextLost !== 'function') throw fail('CONTEXT_FAILED', 'get_context');
check(); track();
if (typeof canvas.addEventListener === 'function') canvas.addEventListener('webglcontextlost', lostEvent);
status.stage = 'native_preflight'; nativeCompatibility();
status.stage = 'register_backend';
try {
tf.webgl.setWebGLContext(1, gl);
// Reset cached feature probes so they inspect this actual context. Do
// not set HAS_WEBGL, float capability or any other ENV feature flag.
tf.env().reset();
// Strategy switches only: never override version, HAS_WEBGL, float
// renderability or any feature/capability probe. Every profile must
// still compute and read the original fixed numerical workload.
if (profile === 'unpacked_byte') tf.env().set('WEBGL_PACK', false);
if (profile === 'packed_byte' || profile === 'unpacked_byte') tf.env().set('WEBGL_DOWNLOAD_FLOAT_ENABLED', false);
gpgpu = new tf.webgl.GPGPUContext(gl);
backend = new tf.webgl.MathBackendWebGL(gpgpu);
if (!tf.registerBackend('webgl', function () { return backend; }, 2)) throw new Error('registration failed');
return Promise.resolve(tf.setBackend('webgl'));
} catch (_) { check(); throw fail('BACKEND_FAILED', 'register_backend'); }
}).then(function () {
check(); if (tf.getBackend() !== 'webgl') throw fail('BACKEND_FAILED', 'register_backend');
// Tensor programs draw full-screen quads. Two-sided drawing avoids
// inherited native face-winding state; numerical tests still decide
// whether the resulting computation is usable.
gl.disable(gl.CULL_FACE); gl.disable(gl.BLEND); gl.colorMask(true, true, true, true);
return numericalSelftest();
}).catch(function (error) {
if (!error || !/^NATIVE_FACE_WEBGL_/.test(error.code || '')) error = fail('BACKEND_FAILED', status.stage);
throw error;
});
}
function dispose() {
if (disposed) return;
// Owner must call only after queued initialization/inference has settled
// and model tensors have been released.
disposed = true;
if (canvas && typeof canvas.removeEventListener === 'function') { try { canvas.removeEventListener('webglcontextlost', lostEvent); } catch (_) {} }
if (backend) { try { tf.removeBackend('webgl'); removed = true; } catch (_) {} }
if (backend && !removed) { try { backend.dispose(); } catch (_) {} }
if (gpgpu) { try { gpgpu.program = null; gpgpu.outputTexture = null; gpgpu.dispose(); } catch (_) {} }
// TFJS 1.7's external GPGPU dispose leaves vertexBuffer and shader objects
// to host GC. Explicitly delete every remaining object created on our GL.
if (gl) {
try { gl.useProgram(null); gl.bindFramebuffer(gl.FRAMEBUFFER, null); } catch (_) {}
resources.forEach(function (resource) { if (!resource.deleted) { try { resource.remove.call(gl, resource.value); resource.deleted = true; } catch (_) {} } });
for (var index = originals.length - 1; index >= 0; index--) { try { gl[originals[index].name] = originals[index].original; } catch (_) {} }
try { tf.webgl.setWebGLContext(1, null); tf.env().reset(); } catch (_) {}
}
if (status.code === 'NONE') { status.stage = 'disposed'; status.code = 'DISPOSED'; }
canvas = null; gl = null; gpgpu = null; backend = null; resources = []; originals = []; quadBuffers = []; elementBuffer = null;
}
return { initialize: initialize, check: check, dispose: dispose, summary: summary,
warmupStart: function () { check(); status.stage = 'warmup'; },
warmupDone: function (milliseconds) { check(); status.warmupMs = milliseconds; status.stage = 'ready'; status.code = 'NONE'; },
warmupFailed: function () { return fail('WARMUP_FAILED', 'warmup'); },
runtimeFailed: function () { check(); return fail('READBACK_FAILED', 'readback'); } };
}
module.exports = function createWebGL(face, platform) {
var profiles = ['default', 'packed_byte', 'unpacked_byte'], active = null, attemptCount = 0, disposed = false;
function summary() {
var value = active ? active.summary() : { requested: true, selftest: 'not_run', stage: 'uninitialized', code: 'NONE', contextLost: false,
profile: 'default', matmulDraws: 0, convDraws: 0, pixelReads: 0, floatReads: 0, byteReads: 0, selftestMs: 0, warmupMs: 0, matmul: null, conv: null, nativeProbe: null, glError: 0, framebufferStatus: 0,
nativeStrategy: 'standard', nativeAttempts: 0, nativeInitialStage: 'not_run', nativeInitialCode: 'NONE', drawStrategy: 'indexed', quadStripDraws: 0 };
value.attemptCount = attemptCount; return value;
}
function initialize() {
function attempt(index) {
if (disposed) {
var error = new Error('本地 WebGL 人脸引擎已关闭'); error.code = 'NATIVE_FACE_WEBGL_DISPOSED'; error.gpu = summary(); return Promise.reject(error);
}
active = createAttempt(face, platform, profiles[index]); attemptCount++;
return active.initialize().catch(function (error) {
// Only actual numerical/readback failures warrant compatibility
// retries. Context loss, missing APIs and resource errors fail closed.
var retry = /^NATIVE_FACE_WEBGL_(MATMUL_FAILED|CONV_FAILED|READBACK_FAILED)$/.test(error && error.code || '');
active.dispose();
if (!disposed && retry && index + 1 < profiles.length) return attempt(index + 1);
if (error && /^NATIVE_FACE_WEBGL_/.test(error.code || '')) error.gpu = summary();
throw error;
});
}
return attempt(0);
}
return { initialize: initialize, summary: summary,
check: function () { if (active) return active.check(); },
dispose: function () { disposed = true; if (active) active.dispose(); },
warmupStart: function () { return active.warmupStart(); },
warmupDone: function (milliseconds) { return active.warmupDone(milliseconds); },
warmupFailed: function () { return active.warmupFailed(); },
runtimeFailed: function () { return active.runtimeFailed(); } };
};