1// One explicit time step (symplectic Euler) of the von Karman plate with a
2// thickness field.
3//
4// State layout (structure of arrays, n = nx * ny nodes):
5// pos: [u | v | w | wLo] wLo carries the rounding error of the w update
6// vel: [du | dv | dw]
7// A workgroup of 16x16 threads advances a 12x12 tile of nodes in three
8// phases separated by barriers. (1) Every thread loads one entry of the tile
9// plus its 2-node halo (16x16 values of w, u, v, w0, the thickness, and the
10// node weights) into shared memory. (2) The threads compute, once each
11// and again into shared memory, the membrane forces of the 13x13 cells
12// around the tile and the weighted bending moments and mixed differences
13// that the bending stencil gathers. (3) Each of the 144 tile nodes evaluates
14// its update from shared memory alone. On grids of a few thousand nodes the
15// kernel is bound by the latency of one workgroup rather than by throughput,
16// so the shared phases, which replace a tenfold per-node recomputation of
17// the stencil ingredients, are what make a step cheap.
18//
19// The thickness enters as the local bending stiffness D(h) at the nodes and
20// the cells, the local membrane stiffnesses at the cells (mean thickness of
21// the four corners), and the node mass. Nodes outside the plate (weight 0)
22// are skipped: a second difference exists only where the node and both
23// neighbors belong to the plate, and a cell contributes only when its four
24// corners do. That reproduces the free edge of the rectangle and gives a
25// staircase free edge for any other outline, including a center hole.
27@group(0) @binding(0) var<uniform> P: Params;
28@group(0) @binding(1) var<uniform> SP: StepParams;
29@group(0) @binding(2) var<storage, read> posIn: array<f32>;
30@group(0) @binding(3) var<storage, read> velIn: array<f32>;
31@group(0) @binding(4) var<storage, read_write> posOut: array<f32>;
32@group(0) @binding(5) var<storage, read_write> velOut: array<f32>;
33@group(0) @binding(6) var<storage, read_write> hist: array<f32>; // [slot * n + node]: transverse velocity at the audio sample instants
34// Two 8-float slots of drumstick state: position, velocity, force, max force,
35// time since the contact released, withdrawn flag, and two spare.
36const STICK_RELEASE: f32 = 5e-3; // [s] a release longer than this ends the hit
37@group(0) @binding(7) var<storage, read_write> stick: array<f32>;
39const TILE = 12; // nodes per workgroup along each axis (must match simulation.ts)
40const HALO = 2;
41const TW = 16; // TILE + 2 * HALO: the loaded tile, one thread per entry
42const CT = 13; // TILE + 1: cells adjacent to the tile's nodes
43const MT = 14; // TILE + 2: nodes at which the bending moments are needed
44// Tile with halo, local index (j - oy) * TW + (i - ox).
45var<workgroup> tw: array<f32, 256>; // w
46var<workgroup> tu: array<f32, 256>; // u
47var<workgroup> tv: array<f32, 256>; // v
48var<workgroup> tw0: array<f32, 256>; // stress-free shape
49var<workgroup> th: array<f32, 256>; // thickness
50var<workgroup> tn: array<u32, 64>; // node weights in quarters (0..4), four to a word
51// Membrane force contributions of the CT x CT cells, 4 nodes x 3 components each.
52var<workgroup> cellF: array<f32, 2028>;
53// Weighted bending moments at the MT x MT inner nodes and mixed differences at the CT x CT cells.
54var<workgroup> ma: array<f32, 196>;
55var<workgroup> mb: array<f32, 196>;
56var<workgroup> cc: array<f32, 169>;
58// Tile origin in global node coordinates (set once per invocation).
59var<private> ox: i32;
60var<private> oy: i32;
61var<private> ihx2: f32;
62var<private> ihy2: f32;
63var<private> ihxy: f32;
65fn id(i: i32, j: i32) -> u32 { return u32(j) * P.nx + u32(i); }
66fn lidx(i: i32, j: i32) -> u32 { return u32(j - oy) * u32(TW) + u32(i - ox); }
67fn midx(i: i32, j: i32) -> u32 { return u32(j - oy - 1) * u32(MT) + u32(i - ox - 1); }
68fn cidx(ci: i32, cj: i32) -> u32 { return u32(cj - oy - 1) * u32(CT) + u32(ci - ox - 1); }
69// Tile lookups with global indices; the tile covers [ox, ox + TW) x [oy, oy + TW).
70fn W(i: i32, j: i32) -> f32 { return tw[lidx(i, j)]; }
71fn U(i: i32, j: i32) -> f32 { return tu[lidx(i, j)]; }
72fn V(i: i32, j: i32) -> f32 { return tv[lidx(i, j)]; }
73fn W0(i: i32, j: i32) -> f32 { return tw0[lidx(i, j)]; }
74// Transverse velocity, read from global memory (only the loss Laplacian needs it,
75// and keeping it out of shared memory keeps the workgroup within 16 KB).
76fn DW(i: i32, j: i32) -> f32 { return velIn[2u * P.n + id(i, j)]; }
77fn TH(i: i32, j: i32) -> f32 { return th[lidx(i, j)]; }
78fn NWq(i: i32, j: i32) -> u32 { let l = lidx(i, j); return (tn[l >> 2u] >> ((l & 3u) * 8u)) & 0xFFu; }
79fn NW(i: i32, j: i32) -> f32 { return 0.25 * f32(NWq(i, j)); }
80fn INS(i: i32, j: i32) -> bool { return NWq(i, j) != 0u; }
81fn insCell(ci: i32, cj: i32) -> bool { return INS(ci, cj) && INS(ci + 1, cj) && INS(ci + 1, cj + 1) && INS(ci, cj + 1); }
82// Bending stiffness at a node and at a cell (mean thickness of the four corners).
83fn Dn(i: i32, j: i32) -> f32 { let h = TH(i, j); return P.Db * h * h * h; }
84fn cellH(ci: i32, cj: i32) -> f32 { return 0.25 * (TH(ci, cj) + TH(ci + 1, cj) + TH(ci + 1, cj + 1) + TH(ci, cj + 1)); }
86// Graph Laplacian of the transverse velocity at node (i, j), over neighbors in the plate.
87fn lapDW(i: i32, j: i32) -> f32 {
88 let c = DW(i, j);
89 var lap = 0.0;
90 if (INS(i - 1, j)) { lap += (DW(i - 1, j) - c) * ihx2; }
91 if (INS(i + 1, j)) { lap += (DW(i + 1, j) - c) * ihx2; }
92 if (INS(i, j - 1)) { lap += (DW(i, j - 1) - c) * ihy2; }
93 if (INS(i, j + 1)) { lap += (DW(i, j + 1) - c) * ihy2; }
94 return lap;
95}
97// Weighted bending moments at nodes; zero where the second difference is undefined.
98fn Ma(i: i32, j: i32) -> f32 {
99 if (!(INS(i - 1, j) && INS(i, j) && INS(i + 1, j))) { return 0.0; }
100 let a = (W(i + 1, j) - 2.0 * W(i, j) + W(i - 1, j)) * ihx2;
101 var b = 0.0;
102 if (INS(i, j - 1) && INS(i, j + 1)) { b = (W(i, j + 1) - 2.0 * W(i, j) + W(i, j - 1)) * ihy2; }
103 return NW(i, j) * Dn(i, j) * (a + P.nu * b);
104}
105fn Mb(i: i32, j: i32) -> f32 {
106 if (!(INS(i, j - 1) && INS(i, j) && INS(i, j + 1))) { return 0.0; }
107 let b = (W(i, j + 1) - 2.0 * W(i, j) + W(i, j - 1)) * ihy2;
108 var a = 0.0;
109 if (INS(i - 1, j) && INS(i + 1, j)) { a = (W(i + 1, j) - 2.0 * W(i, j) + W(i - 1, j)) * ihx2; }
110 return NW(i, j) * Dn(i, j) * (b + P.nu * a);
111}
112// Weighted mixed second difference at a cell center; zero outside the plate.
113fn Cc(ci: i32, cj: i32) -> f32 {
114 if (!insCell(ci, cj)) { return 0.0; }
115 let h = cellH(ci, cj);
116 return P.Db * h * h * h * (W(ci + 1, cj + 1) - W(ci + 1, cj) - W(ci, cj + 1) + W(ci, cj)) * ihxy;
117}
119// Membrane forces of the bilinear cell (ci, cj) on its four nodes (2x2 Gauss),
120// written to cellF at local cell index lc.
121fn cellForces(ci: i32, cj: i32, lc: u32) {
122 let base = lc * 12u;
123 if (!insCell(ci, cj)) {
124 for (var q = 0u; q < 12u; q++) { cellF[base + q] = 0.0; }
125 return;
126 }
127 let hc = cellH(ci, cj);
128 let C = P.Cm * hc;
129 let G = P.Gm * hc;
130 let Un = vec4f(U(ci, cj), U(ci + 1, cj), U(ci + 1, cj + 1), U(ci, cj + 1));
131 let Vn = vec4f(V(ci, cj), V(ci + 1, cj), V(ci + 1, cj + 1), V(ci, cj + 1));
132 let W0n = vec4f(W0(ci, cj), W0(ci + 1, cj), W0(ci + 1, cj + 1), W0(ci, cj + 1));
133 let Wn = vec4f(W(ci, cj), W(ci + 1, cj), W(ci + 1, cj + 1), W(ci, cj + 1)) + W0n; // total shape
134 let q = 0.25 * P.hx * P.hy;
135 let ihx = 1.0 / P.hx; let ihy = 1.0 / P.hy;
136 let g3 = 0.57735026918962576;
137 var Fu = vec4f(0.0); var Fv = vec4f(0.0); var Fw = vec4f(0.0);
138 for (var gp = 0; gp < 4; gp++) {
139 var xi = -g3; if ((gp & 1) != 0) { xi = g3; }
140 var eta = -g3; if ((gp & 2) != 0) { eta = g3; }
141 let dNdx = vec4f(-(1.0 - eta), 1.0 - eta, 1.0 + eta, -(1.0 + eta)) * (0.5 * ihx);
142 let dNdy = vec4f(-(1.0 - xi), -(1.0 + xi), 1.0 + xi, 1.0 - xi) * (0.5 * ihy);
143 let ux = dot(dNdx, Un); let uy = dot(dNdy, Un);
144 let vx = dot(dNdx, Vn); let vy = dot(dNdy, Vn);
145 let wx = dot(dNdx, Wn); let wy = dot(dNdy, Wn);
146 let w0x = dot(dNdx, W0n); let w0y = dot(dNdy, W0n);
147 // Marguerre strains: total slope minus the stress-free slope.
148 let exx = ux + 0.5 * (wx * wx - w0x * w0x);
149 let eyy = vy + 0.5 * (wy * wy - w0y * w0y);
150 let gxy = uy + vx + wx * wy - w0x * w0y;
151 let Nxx = C * (exx + P.nu * eyy);
152 let Nyy = C * (eyy + P.nu * exx);
153 let Nxy = G * gxy;
154 Fu -= q * (Nxx * dNdx + Nxy * dNdy);
155 Fv -= q * (Nyy * dNdy + Nxy * dNdx);
156 Fw -= q * ((Nxx * wx + Nxy * wy) * dNdx + (Nxy * wx + Nyy * wy) * dNdy);
157 }
158 for (var a = 0u; a < 4u; a++) {
159 cellF[base + a * 3u] = Fu[a];
160 cellF[base + a * 3u + 1u] = Fv[a];
161 cellF[base + a * 3u + 2u] = Fw[a];
162 }
163}
165// True for the plate nodes of the drumstick contact patch (same rule as patchAround in params.ts).
166fn inContactPatch(i: i32, j: i32) -> bool {
167 if (nodeWeight(i, j) == 0.0) { return false; }
168 let dxs = nodeX(i) - P.strikeX; let dys = nodeY(j) - P.strikeY;
169 return dxs * dxs + dys * dys <= P.strikeR2 || id(i, j) == P.strikeNode;
170}
171// True for the plate nodes of the mount patch.
172fn inMount(i: i32, j: i32) -> bool {
173 if (P.mountOn == 0u || nodeWeight(i, j) == 0.0) { return false; }
174 let dxm = nodeX(i) - P.mountX; let dym = nodeY(j) - P.mountY;
175 return dxm * dxm + dym * dym <= P.mountR2 || id(i, j) == P.mountNode;
176}
178// Stored membrane force on local node `a` of cell (ci, cj).
179fn cellForce(ci: i32, cj: i32, a: u32) -> vec3f {
180 let base = cidx(ci, cj) * 12u + a * 3u;
181 return vec3f(cellF[base], cellF[base + 1u], cellF[base + 2u]);
182}
184@compute @workgroup_size(16, 16)
185fn main(@builtin(local_invocation_id) lid: vec3u, @builtin(workgroup_id) wid: vec3u) {
186 let n = P.n;
187 ox = i32(wid.x) * TILE - HALO;
188 oy = i32(wid.y) * TILE - HALO;
189 ihx2 = 1.0 / (P.hx * P.hx); ihy2 = 1.0 / (P.hy * P.hy); ihxy = 1.0 / (P.hx * P.hy);
190 let lt = lid.y * 16u + lid.x;
192 // Phase 1: one entry of the tile with halo per thread.
193 {
194 let gi = ox + i32(lid.x); let gj = oy + i32(lid.y);
195 var ww = 0.0; var uu = 0.0; var vv = 0.0; var z0 = 0.0; var hh = 0.0;
196 if (gi >= 0 && gi < i32(P.nx) && gj >= 0 && gj < i32(P.ny)) {
197 let k = id(gi, gj);
198 let st = nd[k];
199 ww = posIn[2u * n + k]; uu = posIn[k]; vv = posIn[n + k]; z0 = st.x; hh = st.z;
200 }
201 tw[lt] = ww; tu[lt] = uu; tv[lt] = vv; tw0[lt] = z0; th[lt] = hh;
202 if (lt < 64u) {
203 var word = 0u;
204 for (var q = 0u; q < 4u; q++) {
205 let e = lt * 4u + q;
206 let ei = ox + i32(e % u32(TW)); let ej = oy + i32(e / u32(TW));
207 var wq = 0u;
208 if (ei >= 0 && ei < i32(P.nx) && ej >= 0 && ej < i32(P.ny)) { wq = u32(round(4.0 * nd[id(ei, ej)].y)); }
209 word |= wq << (q * 8u);
210 }
211 tn[lt] = word;
212 }
213 }
214 workgroupBarrier();
216 // Phase 2: membrane forces of the CT x CT cells, bending moments at the MT x MT
217 // inner nodes, mixed differences at the cells; one item per thread each.
218 if (lt < 169u) {
219 let ci = ox + 1 + i32(lt % u32(CT)); let cj = oy + 1 + i32(lt / u32(CT));
220 cellForces(ci, cj, lt);
221 cc[lt] = Cc(ci, cj);
222 }
223 if (lt < 196u) {
224 let mi = ox + 1 + i32(lt % u32(MT)); let mj = oy + 1 + i32(lt / u32(MT));
225 ma[lt] = Ma(mi, mj);
226 mb[lt] = Mb(mi, mj);
227 }
228 workgroupBarrier();
230 // Phase 3: the update of the TILE x TILE nodes (no barriers past this point).
231 if (lt >= 144u) { return; }
232 let i = ox + HALO + i32(lt % u32(TILE)); let j = oy + HALO + i32(lt / u32(TILE));
233 if (i >= i32(P.nx) || j >= i32(P.ny)) { return; }
234 let k = id(i, j);
235 let weight = NW(i, j);
236 if (weight == 0.0) {
237 // Outside the plate: the node stays at rest.
238 posOut[k] = 0.0; posOut[n + k] = 0.0; posOut[2u * n + k] = 0.0; posOut[3u * n + k] = 0.0;
239 velOut[k] = 0.0; velOut[n + k] = 0.0; velOut[2u * n + k] = 0.0;
240 return;
241 }
243 // Elastic forces: bending from the discrete Kirchhoff energy, membrane from the cells.
244 let bend = (ma[midx(i - 1, j)] - 2.0 * ma[midx(i, j)] + ma[midx(i + 1, j)]) * ihx2
245 + (mb[midx(i, j - 1)] - 2.0 * mb[midx(i, j)] + mb[midx(i, j + 1)]) * ihy2
246 + 2.0 * (1.0 - P.nu) * (cc[cidx(i, j)] - cc[cidx(i - 1, j)] - cc[cidx(i, j - 1)] + cc[cidx(i - 1, j - 1)]) * ihxy;
247 var F = vec3f(0.0, 0.0, -P.hx * P.hy * bend);
248 F += cellForce(i, j, 0u);
249 F += cellForce(i - 1, j, 1u);
250 F += cellForce(i - 1, j - 1, 2u);
251 F += cellForce(i, j - 1, 3u);
252 let pOld = vec3f(posIn[k], posIn[n + k], posIn[2u * n + k]);
253 let vel = vec3f(velIn[k], velIn[n + k], velIn[2u * n + k]);
254 // The mount: a spring and a dashpot to ground over the mount patch, on all three components.
255 let mounted = inMount(i, j);
256 if (mounted) { F -= P.mountKw * weight * pOld + P.mountCw * weight * vel; }
257 let mass = P.rho * TH(i, j) * P.hx * P.hy * weight;
258 var acc = F * (1.0 / mass);
259 // Optional scaling of the in-plane inertia (lengthens the stable time step).
260 acc.x *= P.invInPlaneMass; acc.y *= P.invInPlaneMass;
262 // Losses: frequency-independent on all components, the graph Laplacian of the
263 // transverse velocity (~k^2), and an extra frequency-independent in-plane loss.
264 acc -= 2.0 * SP.sigma0 * vel;
265 acc.z += 2.0 * SP.sigma1 * lapDW(i, j);
266 acc.x -= 2.0 * SP.sigmaIn * vel.x;
267 acc.y -= 2.0 * SP.sigmaIn * vel.y;
269 // Drumstick: Hertz contact between a free mass and the contact patch (the
270 // plate nodes within the tip radius, plus the nearest one). The contact
271 // coordinate is the mass-weighted mean displacement of the patch, and the
272 // force is spread over the patch in proportion to the node masses, so the
273 // pair is conjugate and the contact has a potential energy. The hit may
274 // chatter (release and re-engage within a fraction of a millisecond) and
275 // the plate may rebound onto the stick within a few milliseconds; both
276 // count as the hit. Once the contact has been released for STICK_RELEASE
277 // the stick is withdrawn, so a plate that swings back later as a whole
278 // (a light plate rocking on its mount) cannot be struck a second time.
279 if ((SP.flags & 1u) != 0u) {
280 let sIn = 8u * SP.parity;
281 if (P.mountOn == 0u) {
282 // Free plate: inertia relief. The force of the previous step is applied
283 // to the whole plate as the rigid acceleration field that balances the
284 // patch load and its moments about the center, so the plate gains no net
285 // momentum (the thread would take it up in reality). The one-step lag
286 // integrates to zero. A mounted plate takes the reaction through the mount.
287 let Fprev = stick[sIn + 2u];
288 acc.z += Fprev * (1.0 / P.mass + nodeY(j) * P.patchCY / P.Ix + nodeX(i) * P.patchCX / P.Iy);
289 }
290 if (inContactPatch(i, j)) {
291 let sx = stick[sIn]; let sv = stick[sIn + 1u]; let sMax = stick[sIn + 3u];
292 var released = stick[sIn + 4u]; // time since the contact last released [s]
293 var withdrawn = stick[sIn + 5u];
294 var Fc = 0.0;
295 if (withdrawn == 0.0) {
296 var wc = 0.0;
297 for (var pj = i32(P.patchJ0); pj <= i32(P.patchJ1); pj++) {
298 for (var pi = i32(P.patchI0); pi <= i32(P.patchI1); pi++) {
299 if (inContactPatch(pi, pj)) { wc += nodeMass(pi, pj) * posIn[2u * n + id(pi, pj)]; }
300 }
301 }
302 wc /= P.patchMass;
303 let pen = wc - sx; // the stick sits on the +z side and moves toward -z
304 if (pen > 0.0) { Fc = P.K * pow(pen, P.alpha); released = 0.0; }
305 else if (sMax > 0.0) {
306 released += P.dt;
307 if (released > STICK_RELEASE) { withdrawn = 1.0; }
308 }
309 }
310 acc.z -= Fc / P.patchMass;
311 if (k == P.strikeNode) {
312 let sOut = 8u * (1u - SP.parity);
313 let svn = sv + P.dt * Fc / P.stickMass;
314 stick[sOut] = sx + P.dt * svn;
315 stick[sOut + 1u] = svn;
316 stick[sOut + 2u] = Fc;
317 stick[sOut + 3u] = max(sMax, Fc);
318 stick[sOut + 4u] = released;
319 stick[sOut + 5u] = withdrawn;
320 }
321 }
322 }
324 let vNew = vel + P.dt * acc;
325 if (SP.audioSlot >= 0) { hist[u32(SP.audioSlot) * n + k] = vNew.z; }
326 var pNew = pOld + P.dt * vNew;
327 // Compensated update of w: the increment dt * v can be far below the
328 // float32 resolution of w, so the rounding error is carried separately.
329 let y = P.dt * vNew.z + posIn[3u * n + k];
330 let t = pOld.z + y;
331 let lo = (pOld.z - t) + y;
332 pNew.z = t;
333 posOut[k] = pNew.x; posOut[n + k] = pNew.y; posOut[2u * n + k] = pNew.z; posOut[3u * n + k] = lo;
334 velOut[k] = vNew.x; velOut[n + k] = vNew.y; velOut[2u * n + k] = vNew.z;
335}