// 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(); } }; };