/ proof-of-concept / fmm3dbie-webgpu-poc
Sign in
proof-of-concept / fmm3dbie-webgpu-poc
fmm3dbie-webgpu-poc / index.html
180 lines · 12.5 KBBlameHistoryRaw
1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="utf-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><rect width=%22100%22 height=%22100%22 rx=%2224%22 fill=%22%230b0e12%22/><rect x=%2218%22 y=%2218%22 width=%2264%22 height=%2264%22 rx=%2218%22 fill=%22none%22 stroke=%22%239aa4af%22 stroke-width=%225%22/><circle cx=%2242%22 cy=%2256%22 r=%228%22 fill=%22%23b2182b%22/><circle cx=%2242%22 cy=%2256%22 r=%2219%22 fill=%22none%22 stroke=%22%232166ac%22 stroke-width=%225%22/></svg>" />
7 <title>fmm3dbie-webgpu-poc: a room with impedance walls, solved on your GPU</title>
8 <style>
9 :root {
10 --bg: #ffffff;
11 --ink: #1f2328;
12 --ink-2: #57606a;
13 --line: #d0d7de;
14 --accent: #0969da;
15 --panel-bg: #f6f8fa;
16 --warn: #9a5b00;
17 --bad: #cf222e;
18 color-scheme: light dark;
19 }
20 @media (prefers-color-scheme: dark) {
21 :root {
22 --bg: #14171a;
23 --ink: #e6e9ec;
24 --ink-2: #9aa4af;
25 --line: #333b44;
26 --accent: #58a6ff;
27 --panel-bg: #1a1e23;
28 --warn: #e3a341;
29 --bad: #ff7b72;
30 }
31 }
32 * { box-sizing: border-box; }
33 body { margin: 0; background: var(--bg); color: var(--ink); font: 15px/1.5 system-ui, -apple-system, sans-serif; }
34 main { max-width: 1180px; margin: 0 auto; padding: 20px 16px 56px; }
35 h1 { font-size: 20px; margin: 0 0 2px; }
36 h2 { font-size: 15px; margin: 22px 0 6px; }
37 .sub { color: var(--ink-2); margin: 0 0 12px; font-size: 13.5px; max-width: 78ch; }
38 a { color: var(--accent); }
39 .panel { background: var(--panel-bg); border: 1px solid var(--line); border-radius: 8px; padding: 10px 14px; }
40 .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 6px 16px; }
41 .field { display: flex; flex-direction: column; gap: 2px; font-size: 12.5px; color: var(--ink-2); }
42 .field > span { white-space: nowrap; }
43 input[type='number'], select, button {
44 font: inherit; font-size: 13.5px; color: var(--ink); background: var(--bg);
45 border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; min-width: 0;
46 }
47 input[type='number'] { width: 100%; font-variant-numeric: tabular-nums; }
48 .xyz { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 4px; }
49 button { cursor: pointer; }
50 button:hover:not(:disabled) { border-color: var(--accent); }
51 button:disabled { opacity: 0.5; cursor: default; }
52 button.primary { border-color: var(--accent); color: var(--accent); font-weight: 600; min-width: 6em; }
53 .row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; margin-top: 10px; }
54 .row label { font-size: 13px; color: var(--ink-2); white-space: nowrap; }
55 .derived { font-size: 13px; color: var(--ink-2); font-variant-numeric: tabular-nums; }
56 .derived b { color: var(--ink); font-weight: 600; }
57 .warn { color: var(--warn); }
58 .bad { color: var(--bad); }
59 progress { width: 220px; height: 10px; vertical-align: middle; accent-color: var(--accent); }
60 #status { font-size: 13px; color: var(--ink-2); font-variant-numeric: tabular-nums; }
61 #err { color: var(--bad); white-space: pre-wrap; font-size: 13px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; margin: 8px 0 0; }
62 #err:empty { display: none; }
63 .view-bar { display: flex; flex-wrap: wrap; gap: 6px 16px; align-items: center; margin: 14px 0 6px; font-size: 13px; color: var(--ink-2); }
64 .stage { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 14px; align-items: start; }
65 @media (max-width: 900px) { .stage { grid-template-columns: minmax(0, 1fr); } }
66 .room { position: relative; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: #0b0e12; aspect-ratio: 4 / 3; }
67 #room { width: 100%; height: 100%; display: block; cursor: grab; touch-action: none; }
68 #room:active { cursor: grabbing; }
69 .room .hint { position: absolute; left: 10px; bottom: 8px; font-size: 11.5px; color: #9aa4af; pointer-events: none; }
70 .room .empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #9aa4af; font-size: 14px; text-align: center; padding: 20px; }
71 .cbar { position: absolute; right: 10px; top: 10px; bottom: 10px; display: flex; flex-direction: column; align-items: center; gap: 4px; color: #c9d1d9; font-size: 11px; font-variant-numeric: tabular-nums; pointer-events: none; }
72 .cbar canvas { flex: 1; width: 14px; min-height: 60px; border: 1px solid #333b44; border-radius: 2px; }
73 .side { display: flex; flex-direction: column; gap: 12px; }
74 table { border-collapse: collapse; width: 100%; font-size: 13px; font-variant-numeric: tabular-nums; }
75 td { padding: 3px 4px; border-bottom: 1px solid var(--line); vertical-align: top; }
76 td:first-child { color: var(--ink-2); }
77 td:last-child { text-align: right; }
78 #conv { width: 100%; height: 150px; display: block; }
79 .slices { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-top: 14px; }
80 @media (max-width: 700px) { .slices { grid-template-columns: minmax(0, 1fr); } }
81 figure.slice { margin: 0; }
82 figure.slice canvas { width: 100%; aspect-ratio: 1; display: block; border: 1px solid var(--line); border-radius: 6px; background: repeating-conic-gradient(var(--panel-bg) 0 25%, var(--bg) 0 50%) 0 0 / 16px 16px; cursor: crosshair; }
83 figure.slice figcaption { font-size: 12px; color: var(--ink-2); margin-top: 3px; }
84 #hover { min-height: 1.5em; font-size: 12.5px; color: var(--ink-2); font-variant-numeric: tabular-nums; margin-top: 4px; }
85 .notes { font-size: 13.5px; color: var(--ink-2); max-width: 82ch; }
86 .notes p { margin: 6px 0; }
87 .notes code { font-size: 12.5px; }
88 #device { font-size: 12px; color: var(--ink-2); margin-top: 18px; }
89 </style>
90 </head>
91 <body>
92 <main>
93 <h1>fmm3dbie-webgpu-poc</h1>
94 <p class="sub">
95 A point source at a fixed frequency in a closed room with partially absorbing walls, solved with a high-order boundary integral equation on your GPU through WebGPU, in single precision. This is a browser port of a <a href="https://vault1.magland.org/proof-of-concept/fmm3dbie-gpu-poc">CUDA proof of concept</a> for
96 <a href="https://github.com/fastalgorithms/fmm3dbie">fmm3dbie</a>'s interior Helmholtz impedance solver. Choose the problem, press Solve, then turn the room with the mouse and click a slice to move the source.
97 </p>
99 <div class="panel">
100 <div class="grid">
101 <label class="field" title="Named cases from the CUDA proof of concept, plus a quick one for slow GPUs"><span>case</span>
102 <select autocomplete="off" id="preset">
103 <option value="quick">quick (1,176 points)</option>
104 <option value="small" selected>small (4,704 points)</option>
105 <option value="medium">medium (17,496 points)</option>
106 <option value="large">large (48,600 points)</option>
107 <option value="custom">custom</option>
108 </select>
109 </label>
110 <label class="field" title="Wavenumber k = 2π / wavelength, for a room of half-width 1"><span>wavenumber k</span><input autocomplete="off" id="k" type="number" min="0.5" max="60" step="0.5" /></label>
111 <label class="field" title="Normalized wall admittance β > 0; smaller β means more reflective walls"><span>wall admittance β</span><input autocomplete="off" id="beta" type="number" min="0.001" max="5" step="0.01" /></label>
112 <label class="field" title="Patches per edge of each cube face (6 m² patches in all)"><span>patches per face edge m</span><input autocomplete="off" id="m" type="number" min="1" max="16" step="1" /></label>
113 <label class="field" title="Polynomial order per patch; (order + 1)² nodes each"><span>order per patch</span>
114 <select autocomplete="off" id="norder"><option>4</option><option>5</option><option>6</option><option>7</option><option>8</option><option>9</option><option>10</option></select>
115 </label>
116 <label class="field" title="The walls are |x|^p + |y|^p + |z|^p = 1: p = 2 is a sphere, larger p a sharper cube, which needs more patches or a higher order for the same accuracy"><span>room shape exponent p</span>
117 <select autocomplete="off" id="p"><option>2</option><option>4</option><option>6</option><option selected>8</option><option>10</option><option>12</option><option>16</option><option>20</option><option>30</option></select>
118 </label>
119 <label class="field" title="Source position; must be inside the room. You can also click a slice."><span>source (x, y, z)</span>
120 <span class="xyz"><input autocomplete="off" id="x0" type="number" step="0.05" /><input autocomplete="off" id="y0" type="number" step="0.05" /><input autocomplete="off" id="z0" type="number" step="0.05" /></span>
121 </label>
122 <label class="field" title="Relative residual at which GMRES stops"><span>GMRES tolerance</span>
123 <select autocomplete="off" id="eps"><option value="1e-4">1e-4</option><option value="1e-5" selected>1e-5</option><option value="1e-6">1e-6</option></select>
124 </label>
125 <label class="field" title="Grid points per side of the three slices"><span>slice resolution</span>
126 <select autocomplete="off" id="ngrid"><option>61</option><option selected>101</option><option>151</option></select>
127 </label>
128 </div>
129 <div class="row">
130 <button id="solve" class="primary">Solve</button>
131 <button id="cancel" disabled>Cancel</button>
132 <label title="Also solve a problem with a known solution (the field of a source outside the room) and report the error"><input autocomplete="off" id="selftest" type="checkbox" /> self-test</label>
133 <span class="derived" id="derived"></span>
134 </div>
135 <div class="row" style="margin-top: 6px"><progress id="progress" max="1" value="0"></progress><span id="status">loading…</span></div>
136 <pre id="err"></pre>
137 </div>
139 <div class="view-bar">
140 <label>show
141 <select autocomplete="off" id="mode">
142 <option value="pressure">pressure Re(u e^−iωt)</option>
143 <option value="level">level (dB re median)</option>
144 </select>
145 </label>
146 <label><input autocomplete="off" id="animate" type="checkbox" checked /> animate over one period</label>
147 <label><input autocomplete="off" id="showWalls" type="checkbox" checked /> walls</label>
148 <label><input autocomplete="off" id="showXY" type="checkbox" checked /> xy</label>
149 <label><input autocomplete="off" id="showXZ" type="checkbox" checked /> xz</label>
150 <label><input autocomplete="off" id="showYZ" type="checkbox" /> yz</label>
151 </div>
153 <div class="stage">
154 <div class="room">
155 <canvas id="room"></canvas>
156 <div class="empty" id="empty">Press Solve to compute the field.</div>
157 <div class="cbar"><span id="cbarHi"></span><canvas id="cbar" width="14" height="256"></canvas><span id="cbarLo"></span></div>
158 <div class="hint">drag to turn · scroll to zoom · the nearer walls are cut away</div>
159 </div>
160 <div class="side">
161 <div class="panel">
162 <table id="results"><tbody><tr><td>no solve yet</td><td></td></tr></tbody></table>
163 </div>
164 <div class="panel"><canvas id="conv"></canvas></div>
165 </div>
166 </div>
168 <div class="slices" id="slices"></div>
169 <div id="hover"></div>
171 <h2>What is computed</h2>
172 <div class="notes">
173 <p>The room is the superellipsoid |x|<sup>p</sup> + |y|<sup>p</sup> + |z|<sup>p</sup> = 1, with a point source inside and impedance walls (∂u/∂n − ikβu = 0). We solve fmm3dbie's combined field integral equation on 6m² curved patches: near interactions use precomputed quadrature weights, far interactions a direct sum, and GMRES runs on the CPU. The GPU work is in single precision.</p>
174 <p>The self-test solves a problem with a known solution and reports the error. The energy mismatch compares the power from the source with the power absorbed by the walls. Large cases may need more GPU memory than the browser allows.</p>
175 </div>
176 <div id="device"></div>
177 </main>
178 <script type="module" src="/src/main.ts"></script>
179 </body>
180</html>
↑↓ move↵openescclose