338 lines
22 KiB
JavaScript
338 lines
22 KiB
JavaScript
// 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(); } };
|
||
};
|