1/**
2 * The sandbox page: editor on the left, output on the right. The tic/toc
3 * numbers the script prints ARE the timing report — compare them with a
4 * MATLAB run of the same script.
5 */
6import { CodeEditor } from './editor/codeEditor.ts';
7import { requestSandboxDevice, runScript, type SandboxGpu } from './mgpu/session.ts';
8import { formatFailure } from './mgpu/errors.ts';
9import fusedLoop from '../examples/fused_loop.m?raw';
10import matmul from '../examples/matmul.m?raw';
11import monteCarloPi from '../examples/monte_carlo_pi.m?raw';
12import logisticEnsemble from '../examples/logistic_ensemble.m?raw';
13import reductions from '../examples/reductions.m?raw';
15const EXAMPLES: { key: string; title: string; source: string }[] = [
16 { key: 'fused_loop', title: 'fused elementwise loop', source: fusedLoop },
17 { key: 'matmul', title: 'matrix multiply (GEMM)', source: matmul },
18 { key: 'monte_carlo_pi', title: 'Monte Carlo pi (masks)', source: monteCarloPi },
19 { key: 'logistic_ensemble', title: 'logistic map ensemble', source: logisticEnsemble },
20 { key: 'reductions', title: 'fused reductions', source: reductions },
21];
23const $ = <T extends HTMLElement>(id: string): T => {
24 const el = document.getElementById(id);
25 if (!el) throw new Error(`missing #${id}`);
26 return el as T;
27};
29const consoleEl = $<HTMLPreElement>('console');
30const runBtn = $<HTMLButtonElement>('run');
31const copyBtn = $<HTMLButtonElement>('copy');
32const exampleSel = $<HTMLSelectElement>('example');
33const planDetails = $<HTMLElement>('plandetails');
34const planEl = $<HTMLPreElement>('plan');
35const kernelsEl = $<HTMLElement>('kernels');
36const runState = $<HTMLElement>('runstate');
37const deviceEl = $<HTMLElement>('device');
38const gpuWarn = $<HTMLElement>('gpuwarn');
40const editor = new CodeEditor({
41 textarea: $<HTMLTextAreaElement>('source'),
42 overlay: $('highlight'),
43 onInput: () => {
44 $('editstate').textContent = '';
45 },
46});
48for (const ex of EXAMPLES) {
49 const opt = document.createElement('option');
50 opt.value = ex.key;
51 opt.textContent = ex.title;
52 exampleSel.appendChild(opt);
53}
54exampleSel.addEventListener('change', () => {
55 const ex = EXAMPLES.find((e) => e.key === exampleSel.value);
56 if (ex) editor.value = ex.source;
57});
58editor.value = EXAMPLES[0].source;
60const append = (text: string, cls?: string): void => {
61 if (cls) {
62 const span = document.createElement('span');
63 span.className = cls;
64 span.textContent = text;
65 consoleEl.appendChild(span);
66 } else {
67 consoleEl.appendChild(document.createTextNode(text));
68 }
69 consoleEl.scrollTop = consoleEl.scrollHeight;
70};
72let gpu: SandboxGpu | null = null;
73(async () => {
74 try {
75 gpu = await requestSandboxDevice();
76 deviceEl.textContent = `GPU: ${gpu.description}`;
77 gpu.device.lost.then((info) => {
78 gpu = null;
79 gpuWarn.hidden = false;
80 gpuWarn.textContent = `The GPU device was lost (${info.message}); reload the page.`;
81 runBtn.disabled = true;
82 });
83 } catch (e) {
84 gpuWarn.hidden = false;
85 gpuWarn.innerHTML =
86 `<b>No WebGPU here.</b> ${e instanceof Error ? e.message : String(e)} — ` +
87 `Chrome/Edge have it on by default; Firefox and Safari are rolling it out.`;
88 runBtn.disabled = true;
89 }
90})();
92runBtn.addEventListener('click', () => {
93 void runGpu();
94});
95async function runGpu(): Promise<void> {
96 if (!gpu) return;
97 runBtn.disabled = true;
98 runState.textContent = 'compiling…';
99 consoleEl.textContent = '';
100 planDetails.hidden = true;
101 const source = editor.value;
102 try {
103 const run = await runScript(gpu.device, source, (text) => {
104 runState.textContent = 'running…';
105 append(text);
106 });
107 if (run.result.error) {
108 append(`\n${run.result.error}\n`, 'err');
109 }
110 append(
111 `\n[compile ${(run.compileSeconds * 1000).toFixed(0)} ms · ` +
112 `run ${run.result.totalSeconds.toFixed(3)} s · f32 · ${gpu.description}]\n`,
113 'meta',
114 );
115 planEl.textContent = run.planDescription.join('\n') || '(no GPU ops)';
116 kernelsEl.textContent = '';
117 for (const k of run.kernels) {
118 const d = document.createElement('details');
119 const summary = document.createElement('summary');
120 const b = document.createElement('b');
121 b.textContent = k.label;
122 summary.append('wgsl · ', b);
123 const pre = document.createElement('pre');
124 pre.textContent = k.code.trim();
125 d.append(summary, pre);
126 kernelsEl.appendChild(d);
127 }
128 planDetails.hidden = false;
129 runState.textContent = '';
130 } catch (e) {
131 append(formatFailure(e, source) + '\n', 'err');
132 runState.textContent = '';
133 } finally {
134 runBtn.disabled = !gpu;
135 }
136}
138copyBtn.addEventListener('click', () => {
139 void navigator.clipboard.writeText(editor.value).then(() => {
140 copyBtn.textContent = 'Copied ✓';
141 setTimeout(() => {
142 copyBtn.textContent = 'Copy script';
143 }, 1200);
144 });
145});