|
| 1 | +// Failure reporting for the demo pages. A classic script, deliberately ES5, deliberately not a |
| 2 | +// module: it is loaded from <head> before anything else, so it is already listening when the |
| 3 | +// module scripts are parsed and run. |
| 4 | +// |
| 5 | +// Why it exists: a WebGPU demo that cannot start has nowhere to say so. An uncaught throw in a |
| 6 | +// `<script type="module">` — a missing navigator.gpu, a null adapter, a module that failed to |
| 7 | +// parse — goes to the console and nothing else, and a phone has no console. The page just sits |
| 8 | +// there looking loaded, which reads as "the JavaScript never ran". This turns every one of those |
| 9 | +// into a banner on the page, with the environment details needed to tell the cases apart. |
| 10 | +// |
| 11 | +// The global is TWG_DIAG: |
| 12 | +// TWG_DIAG.fail(title, detail) show the banner (first call wins; later ones are ignored) |
| 13 | +// TWG_DIAG.report() Promise<string> of the environment report |
| 14 | +// TWG_DIAG.shown whether a banner is up |
| 15 | + |
| 16 | +(function () { |
| 17 | + 'use strict'; |
| 18 | + |
| 19 | + var W = window; |
| 20 | + if (W.TWG_DIAG) return; |
| 21 | + |
| 22 | + var CHECK = 'webgpu-check.html'; // resolved against this script's own URL below |
| 23 | + var checkHref = (function () { |
| 24 | + var s = document.currentScript; |
| 25 | + try { return new URL('docs/' + CHECK, s ? s.src : location.href).href; } catch (e) { return ''; } |
| 26 | + })(); |
| 27 | + |
| 28 | + var api = { shown: false }; |
| 29 | + W.TWG_DIAG = api; |
| 30 | + |
| 31 | + // ---- the environment report --------------------------------------------------------------- |
| 32 | + // Everything that decides whether a WebGPU page can run, in the order you would check it by |
| 33 | + // hand. The adapter probe is async and best-effort: a browser without navigator.gpu never |
| 34 | + // reaches it, and one that hangs on requestAdapter is cut off rather than left pending. |
| 35 | + var lines = function (o) { |
| 36 | + var out = [], k; |
| 37 | + for (k = 0; k < o.length; k++) out.push(o[k][0] + ': ' + o[k][1]); |
| 38 | + return out.join('\n'); |
| 39 | + }; |
| 40 | + |
| 41 | + api.report = function () { |
| 42 | + var base = [ |
| 43 | + ['page', location.href], |
| 44 | + ['userAgent', navigator.userAgent], |
| 45 | + ['secure context', String(W.isSecureContext)], |
| 46 | + ['navigator.gpu', navigator.gpu ? 'present' : 'MISSING'], |
| 47 | + ['viewport', W.innerWidth + '×' + W.innerHeight + ' @ dpr ' + (W.devicePixelRatio || 1)], |
| 48 | + ]; |
| 49 | + if (!navigator.gpu) return Promise.resolve(lines(base)); |
| 50 | + |
| 51 | + var timeout = new Promise(function (res) { |
| 52 | + setTimeout(function () { res(null); }, 4000); |
| 53 | + }); |
| 54 | + var probe = Promise.resolve() |
| 55 | + .then(function () { return navigator.gpu.requestAdapter(); }) |
| 56 | + .then(function (a) { |
| 57 | + if (!a) { base.push(['adapter', 'NULL — the browser has WebGPU but this device/driver gave no adapter']); return; } |
| 58 | + var info = a.info || {}; |
| 59 | + base.push(['adapter', [info.vendor, info.architecture, info.device, info.description] |
| 60 | + .filter(Boolean).join(' / ') || '(no info exposed)']); |
| 61 | + base.push(['features', a.features ? a.features.size + ' available' : '?']); |
| 62 | + var L = a.limits || {}; |
| 63 | + base.push(['maxBufferSize', String(L.maxBufferSize)]); |
| 64 | + base.push(['maxStorageBufferBindingSize', String(L.maxStorageBufferBindingSize)]); |
| 65 | + base.push(['maxComputeInvocationsPerWorkgroup', String(L.maxComputeInvocationsPerWorkgroup)]); |
| 66 | + base.push(['maxComputeWorkgroupStorageSize', String(L.maxComputeWorkgroupStorageSize)]); |
| 67 | + base.push(['maxStorageBuffersPerShaderStage', String(L.maxStorageBuffersPerShaderStage)]); |
| 68 | + }) |
| 69 | + .catch(function (e) { base.push(['adapter', 'requestAdapter threw: ' + (e && e.message || e)]); }); |
| 70 | + |
| 71 | + return Promise.race([probe, timeout]).then(function (r) { |
| 72 | + if (r === null) base.push(['adapter', 'requestAdapter did not answer within 4s']); |
| 73 | + return lines(base); |
| 74 | + }); |
| 75 | + }; |
| 76 | + |
| 77 | + // ---- the banner --------------------------------------------------------------------------- |
| 78 | + var CSS = |
| 79 | + '#twg-diag{position:fixed;left:0;right:0;top:0;z-index:2147483647;box-sizing:border-box;' + |
| 80 | + 'max-height:80vh;overflow:auto;padding:.8rem 2.5rem .8rem 1rem;background:#2c2413;color:#f0d79a;' + |
| 81 | + 'border-bottom:1px solid rgba(240,215,154,.4);' + |
| 82 | + 'font:13px/1.55 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;' + |
| 83 | + '-webkit-text-size-adjust:100%;text-align:left}' + |
| 84 | + '#twg-diag b{color:#ffe9b8}' + |
| 85 | + '#twg-diag a{color:#9fc0ff}' + |
| 86 | + '#twg-diag pre{white-space:pre-wrap;overflow-wrap:anywhere;margin:.5rem 0 0;' + |
| 87 | + 'font:11px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:#d8c79c}' + |
| 88 | + '#twg-diag summary{cursor:pointer;margin-top:.5rem;color:#e8c983}' + |
| 89 | + '#twg-diag .x{position:absolute;top:.35rem;right:.5rem;background:none;border:0;' + |
| 90 | + 'color:#f0d79a;font-size:20px;line-height:1;padding:.2rem .45rem;cursor:pointer}'; |
| 91 | + |
| 92 | + var mount = function (node) { |
| 93 | + var to = document.body || document.documentElement; |
| 94 | + to.appendChild(node); |
| 95 | + }; |
| 96 | + |
| 97 | + api.fail = function (title, detail) { |
| 98 | + if (api.shown) return; // the first failure is the interesting one |
| 99 | + api.shown = true; |
| 100 | + |
| 101 | + var run = function () { |
| 102 | + var style = document.createElement('style'); |
| 103 | + style.textContent = CSS; |
| 104 | + (document.head || document.documentElement).appendChild(style); |
| 105 | + |
| 106 | + var box = document.createElement('div'); |
| 107 | + box.id = 'twg-diag'; |
| 108 | + box.setAttribute('role', 'alert'); |
| 109 | + |
| 110 | + var close = document.createElement('button'); |
| 111 | + close.className = 'x'; |
| 112 | + close.setAttribute('aria-label', 'dismiss'); |
| 113 | + close.textContent = '×'; |
| 114 | + close.onclick = function () { box.parentNode.removeChild(box); }; |
| 115 | + |
| 116 | + var head = document.createElement('div'); |
| 117 | + var b = document.createElement('b'); |
| 118 | + b.textContent = title; |
| 119 | + head.appendChild(b); |
| 120 | + if (detail) { |
| 121 | + head.appendChild(document.createTextNode(' ')); |
| 122 | + head.appendChild(document.createTextNode(detail)); |
| 123 | + } |
| 124 | + if (checkHref) { |
| 125 | + head.appendChild(document.createTextNode(' ')); |
| 126 | + var a = document.createElement('a'); |
| 127 | + a.href = checkHref; |
| 128 | + a.textContent = 'Run the WebGPU check →'; |
| 129 | + head.appendChild(a); |
| 130 | + } |
| 131 | + |
| 132 | + var det = document.createElement('details'); |
| 133 | + var sum = document.createElement('summary'); |
| 134 | + sum.textContent = 'Environment details'; |
| 135 | + var pre = document.createElement('pre'); |
| 136 | + pre.textContent = 'collecting…'; |
| 137 | + det.appendChild(sum); |
| 138 | + det.appendChild(pre); |
| 139 | + api.report().then(function (t) { pre.textContent = t; }); |
| 140 | + |
| 141 | + box.appendChild(close); |
| 142 | + box.appendChild(head); |
| 143 | + box.appendChild(det); |
| 144 | + mount(box); |
| 145 | + }; |
| 146 | + |
| 147 | + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run); |
| 148 | + else run(); |
| 149 | + }; |
| 150 | + |
| 151 | + // ---- the traps ---------------------------------------------------------------------------- |
| 152 | + // A module that fails to parse, a module that throws while evaluating, and a rejected promise |
| 153 | + // nobody caught all end up here. Without these three, each of them is a silent blank page. |
| 154 | + var describe = function (e) { |
| 155 | + if (!e) return 'Unknown error.'; |
| 156 | + if (typeof e === 'string') return e; |
| 157 | + return (e.name ? e.name + ': ' : '') + (e.message || String(e)); |
| 158 | + }; |
| 159 | + |
| 160 | + W.addEventListener('error', function (ev) { |
| 161 | + // Resource errors (a 404 on <img>/<script src>) do not bubble as ErrorEvent with .error, but |
| 162 | + // they do arrive here in the capture phase; only the script-level ones matter for the banner. |
| 163 | + if (ev.target && ev.target !== W && ev.target.tagName) { |
| 164 | + var tag = ev.target.tagName; |
| 165 | + if (tag === 'SCRIPT' || tag === 'LINK') { |
| 166 | + var url = ev.target.src || ev.target.href; |
| 167 | + api.fail('This page could not load one of its scripts.', url |
| 168 | + ? 'The browser failed to fetch ' + url + '.' |
| 169 | + : 'A module script failed to load or parse — check that the library files under ' + |
| 170 | + 'src/ and dist/ are being served, and as JavaScript.'); |
| 171 | + } |
| 172 | + return; |
| 173 | + } |
| 174 | + api.fail('This page stopped with an error.', describe(ev.error || ev.message)); |
| 175 | + }, true); |
| 176 | + |
| 177 | + W.addEventListener('unhandledrejection', function (ev) { |
| 178 | + api.fail('This page stopped with an error.', describe(ev.reason)); |
| 179 | + }); |
| 180 | +})(); |
0 commit comments