//! БЭКЕНД ПОД ВИДЕОКАРТУ на wgpu (Vulkan / DX12 / Metal). //! //! Физика построчно повторяет `math.rs`, но на f32: в WGSL нет двойной точности. Это законно //! именно потому, что порог вырожденности γ в схеме ОТНОСИТЕЛЬНЫЙ (`GREL`, доля от ⟨Δ|Δ⟩), а не //! абсолютный — иначе в f32 он срабатывал бы на подавляющем большинстве узлов и схема молча //! выродилась бы в LBGK. Топология задачи (маски, Bouzidi-линки, рамка и рестрикция патча) не //! дублируется: она строится теми же процедурами из `cpu`, а сюда загружается готовыми списками. //! //! Раскладка полей — SoA: `f[i*n + node]`. В отличие от AoS на процессоре, здесь важен //! коалесцированный доступ: соседние потоки читают соседние узлы одной и той же популяции. //! //! Ограничение реализации: за каждый шаг делается одно чтение 48-байтового буфера итогов //! (сила, зонд, статистика). Это синхронизация с GPU на каждом шаге; она заметно ограничивает //! частоту шагов на мелких сетках, зато `step()` возвращает те же величины, что и процессорный //! бэкенд, шаг в шаг. На крупных сетках стоимость самой синхронизации теряется на фоне счёта. use std::borrow::Cow; use bytemuck::{Pod, Zeroable}; use wgpu::util::DeviceExt; use crate::cpu; use crate::math::{self, R}; use crate::{Case, Collision, FieldKind, Spec, StepRec}; const WG: u32 = 64; /// Сколько шагов копится в буфере итогов до одной синхронизации с устройством. /// /// Раньше после каждого шага делалось `map_async` + `poll(Wait)` ради 48 байт: на сетке /// 240×120 счёт упирался в 863 шаг/с при том, что сам счёт занимал 0.27 мс из 1.16 — три /// четверти времени машина стояла. Теперь `k_stats2` пишет итоги в слот `step % HIST`, а /// хост читает всю пачку разом. Обязано совпадать с `HIST` в шейдере. const HIST: usize = 128; // ───────────────────────────────────────────────────────────────────────────── // Структуры, разделяемые с шейдером // ───────────────────────────────────────────────────────────────────────────── #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] struct LevelParams { n: u32, nx: u32, ny: u32, nlinks: u32, bcx: f32, bcy: f32, probe_node: u32, /// бит 0 — писать частичные суммы статистики; бит 1 — на этом уровне лежит зонд flags: u32, /// число граничных узлов (для моментных моделей стенки) nwall: u32, /// шаг между направлениями в буфере популяций, во флоатах (см. dir_stride) stride: u32, _pad: [u32; 2], } #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] struct Dyn { ux_in: f32, uy_in: f32, rho_out: f32, /// 0 — сдвиговая часть только девиатор (N1), 1 — девиатор со следом (N2) kbc_model: u32, outlet_extrap: u32, nparts: u32, refine: u32, collision: u32, /// Слот истории, в который `k_stats2` кладёт итоги этого шага. slot: u32, /// Режим стенки: 0 — Bouzidi, 1 — Град, 2 — HRR, 3 — простой отскок. wall_mode: u32, _pad: [u32; 2], } #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] struct Substep { idx: u32, _p: [u32; 3], } #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] struct AmrParams { nghost: u32, nrestrict: u32, /// шаги между направлениями в буферах популяций грубого и мелкого уровней cstride: u32, fstride: u32, r01: f32, rfc: f32, w: f32, _pad: f32, } #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] struct GLink { node: u32, far: u32, i: u32, ib: u32, kind: u32, q: f32, body: u32, _p: u32, } #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] struct GGhost { fine: u32, c00: u32, c10: u32, c01: u32, c11: u32, tx: f32, ty: f32, _p: u32, } /// Итоги шага, читаемые на хост. Порядок обязан совпадать с `results` в шейдере. #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable, Default, Debug)] struct Results { fx: f32, fy: f32, tz: f32, uy_probe: f32, rho_sum: f32, max_u: f32, g_sum: f32, g_min: f32, g_max: f32, cnt: f32, degen: f32, xineg: f32, /// Сила и момент по телам: [b*3 + c]. body: [f32; 12], } // ───────────────────────────────────────────────────────────────────────────── // Шейдер // ───────────────────────────────────────────────────────────────────────────── const SHADER: &str = r#" // ───── структуры (обязаны совпадать с gpu.rs) ───── struct LevelParams { n:u32, nx:u32, ny:u32, nlinks:u32, bcx:f32, bcy:f32, probe_node:u32, flags:u32, nwall:u32, stride:u32, lp2:u32, lp3:u32 }; // wall_mode: 0 — Bouzidi, 1 — Град, 2 — HRR, 3 — простой отскок struct Dyn { ux_in:f32, uy_in:f32, rho_out:f32, kbc_model:u32, outlet_extrap:u32, nparts:u32, refine:u32, collision:u32, slot:u32, wall_mode:u32, dp2:u32, dp3:u32 }; struct Substep { idx:u32, p0:u32, p1:u32, p2:u32 }; struct AmrParams { nghost:u32, nrestrict:u32, cstride:u32, fstride:u32, r01:f32, rfc:f32, w:f32, pad:f32 }; struct GLink { node:u32, far:u32, i:u32, ib:u32, kind:u32, q:f32, body:u32, p1:u32 }; struct GGhost { fine:u32, c00:u32, c10:u32, c01:u32, c11:u32, tx:f32, ty:f32, p0:u32 }; struct Partial { rho:f32, maxu:f32, gsum:f32, gmin:f32, gmax:f32, cnt:f32, degen:f32, xineg:f32 }; @group(0) @binding(2) var gam : array; @group(0) @binding(3) var solid: array; @group(0) @binding(4) var links: array; @group(0) @binding(5) var beta : array; @group(0) @binding(6) var parts: array; @group(0) @binding(7) var P : LevelParams; //__POPULATIONS__ @group(1) @binding(0) var D : Dyn; @group(1) @binding(1) var S : Substep; // results[0..11] — итоги шага, results[16 + s*4 + c] — сила подшага s. // Сила и итоги держатся в ОДНОМ буфере намеренно: связка уровня использует 7 storage-биндингов, // и отдельный буфер сил вывел бы конвейер за предел 8, гарантируемый лимитами по умолчанию. @group(1) @binding(2) var results : array; @group(2) @binding(0) var amr_src: array; @group(2) @binding(1) var amr_dst: array; @group(2) @binding(2) var ghosts : array; @group(2) @binding(3) var gh_a : array; @group(2) @binding(4) var gh_b : array; @group(2) @binding(5) var rest : array>; @group(2) @binding(6) var A : AmrParams; // (узел, смещение первого линка, число линков, —). Линки одного узла лежат подряд. @group(3) @binding(0) var wnodes : array>; const GREL: f32 = 1e-8; const CS2 : f32 = 0.3333333333; // Раскладка буфера итогов. results[slot*12 .. +12) — история одного шага; за историей идут // рабочие слоты силы по подшагам. История нужна, чтобы не синхронизироваться с устройством // на каждом шаге: результаты копятся и читаются пачкой (см. HIST в gpu.rs). const HIST: u32 = 128u; const SLOT: u32 = 24u; // чисел на шаг: 12 общих + 4 тела по 3 const MAXB: u32 = 4u; // вёдер силы по телам const FORCE_BASE: u32 = 3072u; // = HIST*SLOT // Компенсированное сложение Кэхена–Ноймайера: возвращает (сумма, накопленная поправка). // Наивная сумма по 10^5…10^7 значений в f32 съедает ~log2(N) бит; здесь потеря не копится, // а итог берётся как s + c. Стоит несколько операций на элемент. fn kadd(s: f32, c: f32, x: f32) -> vec2 { let t = s + x; var cc: f32; if (abs(s) >= abs(x)) { cc = c + ((s - t) + x); } else { cc = c + ((x - t) + s); } return vec2(t, cc); } // ───── физика: построчный перенос math.rs ───── // Энтропийное равновесие в product-form (точный максимизатор энтропии при заданных rho, rho*u) fn feq9(rho: f32, ux0: f32, uy0: f32) -> array { let ux = clamp(ux0, -0.95, 0.95); let uy = clamp(uy0, -0.95, 0.95); let sx = sqrt(1.0 + 3.0*ux*ux); let sy = sqrt(1.0 + 3.0*uy*uy); let base = rho * (2.0 - sx) * (2.0 - sy); let qx = (2.0*ux + sx) / (1.0 - ux); let qy = (2.0*uy + sy) / (1.0 - uy); let ix = 1.0 / qx; let iy = 1.0 / qy; return array( base*(4.0/9.0), base*(1.0/9.0)*qx, base*(1.0/9.0)*qy, base*(1.0/9.0)*ix, base*(1.0/9.0)*iy, base*(1.0/36.0)*qx*qy, base*(1.0/36.0)*ix*qy, base*(1.0/36.0)*ix*iy, base*(1.0/36.0)*qx*iy); } fn macros9(fv: array) -> vec3 { let rho = fv[0]+fv[1]+fv[2]+fv[3]+fv[4]+fv[5]+fv[6]+fv[7]+fv[8]; let mx = fv[1]+fv[5]+fv[8]-fv[3]-fv[6]-fv[7]; let my = fv[2]+fv[5]+fv[6]-fv[4]-fv[7]-fv[8]; return vec3(rho, mx/rho, my/rho); } // Проекция неравновесия на сдвиговую часть. model=0: {N, Pi_xy} (N1); model=1: плюс след T (N2) fn shift9(d: array, model: u32) -> array { let a = 0.25*(d[1] + d[3] - d[2] - d[4]); let b = 0.25*(d[5] + d[7] - d[6] - d[8]); var s = array(0.0, a, -a, a, -a, b, -b, b, -b); if (model == 1u) { let dt = d[1] + d[2] + d[3] + d[4] + 2.0*(d[5] + d[6] + d[7] + d[8]); let q = 0.25*dt; s[0] = s[0] - dt; s[1] = s[1] + q; s[2] = s[2] + q; s[3] = s[3] + q; s[4] = s[4] + q; } return s; } fn load9(base: ptr>, off: u32) { for (var i = 0u; i < 9u; i = i + 1u) { (*base)[i] = fget(i, off); } } // Возвращает (пост-столкновительные популяции, gamma, признак вырождения) struct CollOut { fv: array, gamma: f32, degen: f32 }; fn collide9(fin: array, b: f32, op: u32, model: u32) -> CollOut { var out: CollOut; // WGSL разрешает переменный индекс только по памяти (var), а не по значению (let/параметр), // поэтому всё, что индексируется в цикле, кладётся в var var fv = fin; let m = macros9(fin); var fe = feq9(m.x, m.y, m.z); if (op == 1u) { // LBGK: то же самое при gamma = 2 var g: array; for (var i = 0u; i < 9u; i = i + 1u) { g[i] = fv[i] + 2.0*b*(fe[i] - fv[i]); } out.fv = g; out.gamma = 2.0; out.degen = 0.0; return out; } var d: array; for (var i = 0u; i < 9u; i = i + 1u) { d[i] = fv[i] - fe[i]; } var ds = shift9(d, model); var num = 0.0; var den = 0.0; var nrm = 0.0; for (var i = 0u; i < 9u; i = i + 1u) { let inv = 1.0 / fe[i]; let dh = d[i] - ds[i]; num = num + ds[i]*dh*inv; den = den + dh*dh*inv; nrm = nrm + d[i]*d[i]*inv; } // относительный порог: den квадратична по неравновесию и физически мала let ok = den > GREL*nrm; let binv = 1.0 / b; var gm = 2.0; if (ok) { gm = binv - (2.0 - binv)*num/den; } var g: array; for (var i = 0u; i < 9u; i = i + 1u) { let dh = d[i] - ds[i]; g[i] = fv[i] - b*(2.0*ds[i] + gm*dh); } out.fv = g; out.gamma = gm; out.degen = select(1.0, 0.0, ok); return out; } fn zou_he_inlet(fin: array, ux: f32, uy: f32) -> array { var g = fin; let rho = (g[0] + g[2] + g[4] + 2.0*(g[3] + g[6] + g[7])) / (1.0 - ux); let dd = 0.5*(g[2] - g[4]); g[1] = g[3] + (2.0/3.0)*rho*ux; g[5] = g[7] - dd + (1.0/6.0)*rho*ux + 0.5*rho*uy; g[8] = g[6] + dd + (1.0/6.0)*rho*ux - 0.5*rho*uy; return g; } fn zou_he_outlet(fin: array, rho_out: f32, uy_out: f32) -> array { var g = fin; let ux = (g[0] + g[2] + g[4] + 2.0*(g[1] + g[5] + g[8])) / rho_out - 1.0; let dd = 0.5*(g[2] - g[4]); g[3] = g[1] - (2.0/3.0)*rho_out*ux; g[6] = g[8] - dd - (1.0/6.0)*rho_out*ux + 0.5*rho_out*uy_out; g[7] = g[5] + dd - (1.0/6.0)*rho_out*ux - 0.5*rho_out*uy_out; return g; } // ───── ядра ───── // У GPU жёсткий предел на число рабочих групп в ОДНОМ измерении (65535). Сетка 4096×2048 — // это 131072 группы по 64 узла, то есть вдвое больше предела, и запуск падает с ошибкой // валидации, а не считает медленно. Поэтому диспетчеризация двумерная, а линейный индекс // собирается обратно вручную. Сборка точная: gid.x = wid.x*64 + lid.x, поэтому // lin() = (wid.y*nwg.x + wid.x)*64 + lid.x — то есть номер группы тоже остаётся линейным. fn lin(gid: vec3, nwg: vec3) -> u32 { return gid.y * nwg.x * 64u + gid.x; } fn wlin(wid: vec3, nwg: vec3) -> u32 { return wid.y * nwg.x + wid.x; } @compute @workgroup_size(64) fn k_collide(@builtin(global_invocation_id) gid: vec3, @builtin(num_workgroups) nwg: vec3) { let nd = lin(gid, nwg); if (nd >= P.n) { return; } var fv: array; load9(&fv, nd); if (solid[nd] != 0u) { // внутри тела не считаем: популяции там фиктивны, Bouzidi всё равно их перекрывает for (var i = 0u; i < 9u; i = i + 1u) { pset(i, nd, fv[i]); } gam[nd] = 2.0; return; } var r = collide9(fv, beta[nd % P.nx], D.collision, D.kbc_model); for (var i = 0u; i < 9u; i = i + 1u) { pset(i, nd, r.fv[i]); } gam[nd] = r.gamma; } @compute @workgroup_size(64) fn k_stream(@builtin(global_invocation_id) gid: vec3, @builtin(num_workgroups) nwg: vec3) { let nd = lin(gid, nwg); if (nd >= P.n) { return; } var cx = array(0, 1, 0, -1, 0, 1, -1, -1, 1); var cy = array(0, 0, 1, 0, -1, 1, 1, -1, -1); let x = i32(nd % P.nx); let y = i32(nd / P.nx); let nxi = i32(P.nx); let nyi = i32(P.ny); for (var i = 0u; i < 9u; i = i + 1u) { var xs = (x - cx[i]) % nxi; if (xs < 0) { xs = xs + nxi; } var ys = (y - cy[i]) % nyi; if (ys < 0) { ys = ys + nyi; } fset(i, nd, pget(i, u32(ys*nxi + xs))); } } @compute @workgroup_size(64) fn k_bouzidi(@builtin(global_invocation_id) gid: vec3, @builtin(num_workgroups) nwg: vec3) { let k = lin(gid, nwg); if (k >= P.nlinks) { return; } let L = links[k]; let fi = pget(L.i, L.node); var v = fi; // ступенчатая модель игнорирует долю пересечения: стенка ровно посередине между узлами if (D.wall_mode == 3u) { fset(L.ib, L.node, fi); return; } if (L.kind == 0u) { // q < 1/2, есть дальний жидкий сосед v = 2.0*L.q*fi + (1.0 - 2.0*L.q)*pget(L.i, L.far); } else if (L.kind == 1u) { // q >= 1/2 let h = 1.0/(2.0*L.q); v = h*fi + (1.0 - h)*pget(L.ib, L.node); } fset(L.ib, L.node, v); } @compute @workgroup_size(64) fn k_walls(@builtin(global_invocation_id) gid: vec3, @builtin(num_workgroups) nwg: vec3) { let x = lin(gid, nwg); if (x >= P.nx) { return; } // зеркальное отражение: касательный импульс сохраняется, нормальный заворачивается fset(2u, x, fget(4u, x)); fset(5u, x, fget(8u, x)); fset(6u, x, fget(7u, x)); let t = (P.ny - 1u)*P.nx + x; fset(4u, t, fget(2u, t)); fset(7u, t, fget(6u, t)); fset(8u, t, fget(5u, t)); } @compute @workgroup_size(64) fn k_channel(@builtin(global_invocation_id) gid: vec3, @builtin(num_workgroups) nwg: vec3) { let y = lin(gid, nwg); if (y >= P.ny) { return; } // вход: скоростной Zou-He по ВСЕМУ столбцу, включая угловые узлы let a = y*P.nx; var fv: array; load9(&fv, a); var gi = zou_he_inlet(fv, D.ux_in, D.uy_in); for (var i = 0u; i < 9u; i = i + 1u) { fset(i, a, gi[i]); } // выход: давление-Zou-He let b = y*P.nx + P.nx - 1u; var gv: array; load9(&gv, b); var uyo = 0.0; if (D.outlet_extrap != 0u) { let c = y*P.nx + P.nx - 2u; var cv: array; load9(&cv, c); let s = cv[0]+cv[1]+cv[2]+cv[3]+cv[4]+cv[5]+cv[6]+cv[7]+cv[8]; uyo = ((cv[2]+cv[5]+cv[6]) - (cv[4]+cv[7]+cv[8])) / s; } var go = zou_he_outlet(gv, D.rho_out, uyo); for (var i = 0u; i < 9u; i = i + 1u) { fset(i, b, go[i]); } } // сила и момент по GMEM, редукция одной рабочей группой var wfx: array; var wfy: array; var wtz: array; @compute @workgroup_size(256) fn k_force(@builtin(local_invocation_id) lid: vec3) { let t = lid.x; var cx = array(0.0, 1.0, 0.0, -1.0, 0.0, 1.0, -1.0, -1.0, 1.0); var cy = array(0.0, 0.0, 1.0, 0.0, -1.0, 1.0, 1.0, -1.0, -1.0); // накопители на каждое тело держатся в регистрах, редукция потом идёт по телам подряд var pfx = array(0.0, 0.0, 0.0, 0.0); var pfy = array(0.0, 0.0, 0.0, 0.0); var ptz = array(0.0, 0.0, 0.0, 0.0); var k = t; loop { if (k >= P.nlinks) { break; } let L = links[k]; let fp = pget(L.i, L.node); let fb = fget(L.ib, L.node); let dfx = cx[L.i]*fp + cx[L.i]*fb; let dfy = cy[L.i]*fp + cy[L.i]*fb; // плечо до точки пересечения линка со стенкой, а не до узла let rx = f32(L.node % P.nx) - P.bcx + L.q*cx[L.i]; let ry = f32(L.node / P.nx) - P.bcy + L.q*cy[L.i]; let b = min(L.body, MAXB - 1u); pfx[b] = pfx[b] + dfx; pfy[b] = pfy[b] + dfy; ptz[b] = ptz[b] + rx*dfy - ry*dfx; k = k + 256u; } for (var b = 0u; b < MAXB; b = b + 1u) { wfx[t] = pfx[b]; wfy[t] = pfy[b]; wtz[t] = ptz[b]; workgroupBarrier(); var s = 128u; loop { if (s == 0u) { break; } if (t < s) { wfx[t] = wfx[t] + wfx[t + s]; wfy[t] = wfy[t] + wfy[t + s]; wtz[t] = wtz[t] + wtz[t + s]; } workgroupBarrier(); s = s >> 1u; } if (t == 0u) { let o = FORCE_BASE + (S.idx*MAXB + b)*4u; results[o + 0u] = wfx[0]; results[o + 1u] = wfy[0]; results[o + 2u] = wtz[0]; } workgroupBarrier(); } } // частичные суммы диагностики: одна Partial на рабочую группу var wp: array; @compute @workgroup_size(64) fn k_stats1(@builtin(global_invocation_id) gid: vec3, @builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3, @builtin(num_workgroups) nwg: vec3) { let nd = lin(gid, nwg); let t = lid.x; var p: Partial; p.rho = 0.0; p.maxu = 0.0; p.gsum = 0.0; p.gmin = 1e30; p.gmax = -1e30; p.cnt = 0.0; p.degen = 0.0; p.xineg = 0.0; if (nd < P.n && solid[nd] == 0u) { var fv: array; load9(&fv, nd); let m = macros9(fv); p.rho = m.x; p.maxu = sqrt(m.y*m.y + m.z*m.z); let g = gam[nd]; p.gsum = g; p.gmin = g; p.gmax = g; p.cnt = 1.0; // вырожденный узел помечен ровно gamma = 2 (см. k_collide) p.degen = select(0.0, 1.0, g == 2.0); // объёмная вязкость модели D: xi = cs^2 (1/(gamma*beta) - 1/2) let b = beta[nd % P.nx]; // при следе в сдвиговой части (N2) объёмная вязкость равна сдвиговой и всегда > 0 var xi = CS2*(1.0/(b + b) - 0.5); if (D.kbc_model == 0u) { xi = CS2*(1.0/(g*b) - 0.5); } p.xineg = select(0.0, 1.0, xi < 0.0); } // зонд следа снимается с того уровня, на котором он лежит if ((P.flags & 2u) != 0u && nd == P.probe_node) { var fv: array; load9(&fv, nd); let m = macros9(fv); results[D.slot*SLOT + 3u] = m.z; } wp[t] = p; workgroupBarrier(); var s = 32u; loop { if (s == 0u) { break; } if (t < s) { let o = wp[t + s]; wp[t].rho = wp[t].rho + o.rho; wp[t].maxu = max(wp[t].maxu, o.maxu); wp[t].gsum = wp[t].gsum + o.gsum; wp[t].gmin = min(wp[t].gmin, o.gmin); wp[t].gmax = max(wp[t].gmax, o.gmax); wp[t].cnt = wp[t].cnt + o.cnt; wp[t].degen = wp[t].degen + o.degen; wp[t].xineg = wp[t].xineg + o.xineg; } workgroupBarrier(); s = s >> 1u; } // хвостовые группы двумерной сетки лежат за пределами поля и в parts не пишут: // отображение группа→узлы осталось линейным, поэтому wl >= nparts ⇒ узлы >= n let wl = wlin(wid, nwg); if (t == 0u && wl < D.nparts && (P.flags & 1u) != 0u) { parts[wl] = wp[0]; } } // Размер группы обязан совпадать с длиной wp: при 256 потоках на 64 ячейки четыре потока // писали бы в одну и ту же ячейку и три четверти частичных сумм терялись бы молча. @compute @workgroup_size(64) fn k_stats2(@builtin(local_invocation_id) lid: vec3) { let t = lid.x; // слагаемых до 10^5 на поток — здесь и живёт основная потеря f32, поэтому все // аддитивные накопители компенсированные var rho = vec2(0.0, 0.0); var gsum = vec2(0.0, 0.0); var cnt = vec2(0.0, 0.0); var degen = vec2(0.0, 0.0); var xineg = vec2(0.0, 0.0); var maxu = 0.0; var gmin = 1e30; var gmax = -1e30; var k = t; loop { if (k >= D.nparts) { break; } let o = parts[k]; rho = kadd(rho.x, rho.y, o.rho); gsum = kadd(gsum.x, gsum.y, o.gsum); cnt = kadd(cnt.x, cnt.y, o.cnt); degen = kadd(degen.x, degen.y, o.degen); xineg = kadd(xineg.x, xineg.y, o.xineg); maxu = max(maxu, o.maxu); gmin = min(gmin, o.gmin); gmax = max(gmax, o.gmax); k = k + 64u; } var p: Partial; p.rho = rho.x + rho.y; p.gsum = gsum.x + gsum.y; p.cnt = cnt.x + cnt.y; p.degen = degen.x + degen.y; p.xineg = xineg.x + xineg.y; p.maxu = maxu; p.gmin = gmin; p.gmax = gmax; wp[t] = p; workgroupBarrier(); // сведение 256 потоков через 64 ячейки делаем последовательно нулевым потоком: // объём данных крошечный, а корректность важнее пары микросекунд if (t == 0u) { var qrho = vec2(0.0, 0.0); var qgsum = vec2(0.0, 0.0); var qcnt = vec2(0.0, 0.0); var qdeg = vec2(0.0, 0.0); var qxin = vec2(0.0, 0.0); var qmaxu = 0.0; var qgmin = 1e30; var qgmax = -1e30; for (var j = 0u; j < 64u; j = j + 1u) { let o = wp[j]; qrho = kadd(qrho.x, qrho.y, o.rho); qgsum = kadd(qgsum.x, qgsum.y, o.gsum); qcnt = kadd(qcnt.x, qcnt.y, o.cnt); qdeg = kadd(qdeg.x, qdeg.y, o.degen); qxin = kadd(qxin.x, qxin.y, o.xineg); qmaxu = max(qmaxu, o.maxu); qgmin = min(qgmin, o.gmin); qgmax = max(qgmax, o.gmax); } // сила усредняется по подшагам L1 и раскладывается по телам; общая — их сумма let inv = 1.0 / f32(D.refine); let o = D.slot*SLOT; var tfx = 0.0; var tfy = 0.0; var ttz = 0.0; for (var b = 0u; b < MAXB; b = b + 1u) { var fx = 0.0; var fy = 0.0; var tz = 0.0; for (var sb = 0u; sb < D.refine; sb = sb + 1u) { let g = FORCE_BASE + (sb*MAXB + b)*4u; fx = fx + results[g + 0u]; fy = fy + results[g + 1u]; tz = tz + results[g + 2u]; } results[o + 12u + b*3u + 0u] = fx*inv; results[o + 12u + b*3u + 1u] = fy*inv; results[o + 12u + b*3u + 2u] = tz*inv; tfx = tfx + fx*inv; tfy = tfy + fy*inv; ttz = ttz + tz*inv; } results[o + 0u] = tfx; results[o + 1u] = tfy; results[o + 2u] = ttz; results[o + 4u] = qrho.x + qrho.y; results[o + 5u] = qmaxu; results[o + 6u] = qgsum.x + qgsum.y; results[o + 7u] = qgmin; results[o + 8u] = qgmax; results[o + 9u] = qcnt.x + qcnt.y; results[o + 10u] = qdeg.x + qdeg.y; results[o + 11u] = qxin.x + qxin.y; } } // ───── граничное условие на моментах (Град / HRR) ───── // Скорость узла на момент t: берётся из ПОСТ-СТОЛКНОВИТЕЛЬНОГО поля. Столкновение сохраняет // rho и rho*u точно, поэтому это ровно u(x, t) — то, что требует прил. B, без отдельного // хранилища прошлого шага. fn u_at_t(nd: u32) -> vec2 { var ff: array; for (var i = 0u; i < 9u; i = i + 1u) { ff[i] = pget(i, nd); } let m = macros9(ff); return vec2(m.y, m.z); } // (du/dx, du/dy, dv/dx, dv/dy): центральная разность там, где оба соседа жидкие, // односторонняя — где один твёрдый, ноль — если твёрдые оба. fn grad_u_at_t(nd: u32) -> vec4 { let x = nd % P.nx; let y = nd / P.nx; let xm = y*P.nx + (x + P.nx - 1u) % P.nx; let xp = y*P.nx + (x + 1u) % P.nx; let ym = ((y + P.ny - 1u) % P.ny)*P.nx + x; let yp = ((y + 1u) % P.ny)*P.nx + x; let uc = u_at_t(nd); var dx = vec2(0.0, 0.0); let am = solid[xm] == 0u; let ap = solid[xp] == 0u; if (am && ap) { dx = 0.5*(u_at_t(xp) - u_at_t(xm)); } else if (ap) { dx = u_at_t(xp) - uc; } else if (am) { dx = uc - u_at_t(xm); } var dy = vec2(0.0, 0.0); let bm = solid[ym] == 0u; let bp = solid[yp] == 0u; if (bm && bp) { dy = 0.5*(u_at_t(yp) - u_at_t(ym)); } else if (bp) { dy = u_at_t(yp) - uc; } else if (bm) { dy = uc - u_at_t(ym); } return vec4(dx.x, dy.x, dx.y, dy.y); } // Приближение Града, ур. (2.13): популяции по rho, u и полному тензору давлений. fn grad_init9(rho: f32, ux: f32, uy: f32, pxx: f32, pxy: f32, pyy: f32) -> array { let axx = pxx - rho*CS2; let ayy = pyy - rho*CS2; var w = array(4.0/9.0, 1.0/9.0, 1.0/9.0, 1.0/9.0, 1.0/9.0, 1.0/36.0, 1.0/36.0, 1.0/36.0, 1.0/36.0); var cx = array(0.0, 1.0, 0.0, -1.0, 0.0, 1.0, -1.0, -1.0, 1.0); var cy = array(0.0, 0.0, 1.0, 0.0, -1.0, 1.0, 1.0, -1.0, -1.0); var out: array; for (var i = 0u; i < 9u; i = i + 1u) { let quad = axx*(cx[i]*cx[i] - CS2) + 2.0*pxy*cx[i]*cy[i] + ayy*(cy[i]*cy[i] - CS2); out[i] = w[i]*(rho + rho*(ux*cx[i] + uy*cy[i])/CS2 + quad/(2.0*CS2*CS2)); } return out; } // third=1 — HRR: ряд Эрмита продолжается на 3-й порядок рекурсивно из неравновесных // коэффициентов 2-го. Построчный перенос math::moment_wall. fn moment_wall9(rho: f32, ux: f32, uy: f32, dudx: f32, dudy: f32, dvdx: f32, dvdy: f32, b: f32, third: u32) -> array { let pref = rho*CS2/(2.0*b); let nxx = -pref*2.0*dudx; let nyy = -pref*2.0*dvdy; let nxy = -pref*(dudy + dvdx); var out = grad_init9(rho, ux, uy, rho*CS2 + rho*ux*ux + nxx, rho*ux*uy + nxy, rho*CS2 + rho*uy*uy + nyy); if (third == 1u) { let a3xxy = 2.0*ux*nxy + uy*nxx; let a3xyy = 2.0*uy*nxy + ux*nyy; let k = 1.0/(2.0*CS2*CS2*CS2); var w = array(4.0/9.0, 1.0/9.0, 1.0/9.0, 1.0/9.0, 1.0/9.0, 1.0/36.0, 1.0/36.0, 1.0/36.0, 1.0/36.0); var cx = array(0.0, 1.0, 0.0, -1.0, 0.0, 1.0, -1.0, -1.0, 1.0); var cy = array(0.0, 0.0, 1.0, 0.0, -1.0, 1.0, 1.0, -1.0, -1.0); for (var i = 0u; i < 9u; i = i + 1u) { out[i] = out[i] + w[i]*k*((cx[i]*cx[i] - CS2)*cy[i]*a3xxy + cx[i]*(cy[i]*cy[i] - CS2)*a3xyy); } } return out; } @compute @workgroup_size(64) fn k_moment_wall(@builtin(global_invocation_id) gid: vec3, @builtin(num_workgroups) nwg: vec3) { let w = lin(gid, nwg); if (w >= P.nwall) { return; } let wn = wnodes[w]; let node = wn.x; let first = wn.y; let cnt = wn.z; // целевая скорость (B 1): интерполяция между стенкой и дальним жидким соседом var ux = 0.0; var uy = 0.0; for (var k = 0u; k < cnt; k = k + 1u) { let L = links[first + k]; var fx = 0.0; var fy = 0.0; // стенка неподвижна; дальнего соседа может не быть if (solid[L.far] == 0u) { let uf = u_at_t(L.far); fx = uf.x; fy = uf.y; } ux = ux + (L.q*fx) / (1.0 + L.q); uy = uy + (L.q*fy) / (1.0 + L.q); } let inv = 1.0 / f32(cnt); ux = ux * inv; uy = uy * inv; // целевая плотность (B 3): известные популяции плюс отскок недостающих var missing = array(0u,0u,0u,0u,0u,0u,0u,0u,0u); for (var k = 0u; k < cnt; k = k + 1u) { missing[links[first + k].ib] = 1u; } var opp = array(0u, 3u, 4u, 1u, 2u, 7u, 8u, 5u, 6u); var rho = 0.0; for (var i = 0u; i < 9u; i = i + 1u) { if (missing[i] == 1u) { rho = rho + pget(opp[i], node); } else { rho = rho + fget(i, node); } } let g4 = grad_u_at_t(node); var g = moment_wall9(rho, ux, uy, g4.x, g4.y, g4.z, g4.w, beta[node % P.nx], select(0u, 1u, D.wall_mode == 2u)); for (var i = 0u; i < 9u; i = i + 1u) { if (missing[i] == 1u) { fset(i, node, g[i]); } } } // ───── AMR ───── @compute @workgroup_size(64) fn k_ghost(@builtin(global_invocation_id) gid: vec3, @builtin(num_workgroups) nwg: vec3) { let k = lin(gid, nwg); if (k >= A.nghost) { return; } let g = ghosts[k]; let w00 = (1.0 - g.tx)*(1.0 - g.ty); let w10 = g.tx*(1.0 - g.ty); let w01 = (1.0 - g.tx)*g.ty; let w11 = g.tx*g.ty; // отдельно интерполируются макропеременные, отдельно неравновесная часть (как в cpu.rs) var ws = array(w00, w10, w01, w11); var cs = array(g.c00, g.c10, g.c01, g.c11); var rho = 0.0; var ux = 0.0; var uy = 0.0; var neq = array(0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0); for (var j = 0u; j < 4u; j = j + 1u) { var cf: array; for (var i = 0u; i < 9u; i = i + 1u) { cf[i] = amr_src[i*A.cstride + cs[j]]; } let m = macros9(cf); rho = rho + ws[j]*m.x; ux = ux + ws[j]*m.y; uy = uy + ws[j]*m.z; var fe0 = feq9(m.x, m.y, m.z); for (var i = 0u; i < 9u; i = i + 1u) { neq[i] = neq[i] + ws[j]*(cf[i] - fe0[i]); } } var fe = feq9(rho, ux, uy); // неравновесная часть масштабируется: f^neq ~ tau*dt for (var i = 0u; i < 9u; i = i + 1u) { amr_dst[k*9u + i] = fe[i] + A.r01*neq[i]; } } @compute @workgroup_size(64) fn k_fill(@builtin(global_invocation_id) gid: vec3, @builtin(num_workgroups) nwg: vec3) { let k = lin(gid, nwg); if (k >= A.nghost) { return; } let g = ghosts[k]; // временная интерполяция рамки между состояниями L0 «до» и «после» шага for (var i = 0u; i < 9u; i = i + 1u) { amr_dst[i*A.fstride + g.fine] = (1.0 - A.w)*gh_a[k*9u + i] + A.w*gh_b[k*9u + i]; } } @compute @workgroup_size(64) fn k_restrict(@builtin(global_invocation_id) gid: vec3, @builtin(num_workgroups) nwg: vec3) { let k = lin(gid, nwg); if (k >= A.nrestrict) { return; } let pr = rest[k]; var ff: array; for (var i = 0u; i < 9u; i = i + 1u) { ff[i] = amr_src[i*A.fstride + pr.y]; } let m = macros9(ff); var fe = feq9(m.x, m.y, m.z); for (var i = 0u; i < 9u; i = i + 1u) { amr_dst[i*A.cstride + pr.x] = fe[i] + A.rfc*(ff[i] - fe[i]); } } "#; // ───────────────────────────────────────────────────────────────────────────── // Уровень на устройстве // ───────────────────────────────────────────────────────────────────────────── struct GpuLevel { nx: usize, ny: usize, n: usize, /// шаг между направлениями в `f`; см. dir_stride stride: usize, f: wgpu::Buffer, /// Стабилизатор γ поузлово — нужен для картинки; забирается с устройства как есть. gam: wgpu::Buffer, bind: wgpu::BindGroup, /// Группа 3: индекс граничных узлов для моментной стенки. wall_bind: wgpu::BindGroup, nwall: u32, /// число рабочих групп редукции статистики (оно же длина буфера частичных сумм) parts_count: u32, nlinks: u32, } impl GpuLevel { fn link_groups(&self) -> u32 { ceil_div(self.nlinks.max(1), WG) } } /// Переложить готовое стартовое поле (построенное общим кодом в `cpu::initial_field`) /// из AoS в раскладку SoA, которой пользуется GPU. /// Выравнивание шага между направлениями, во ФЛОАТАХ. /// /// Смещение каждой привязки обязано быть кратно `min_storage_buffer_offset_alignment`, /// а он равен 256 байтам у всех известных реализаций — это 64 значения f32. Шаг /// выравнивается всегда, даже когда привязка одна: так раскладка одна на оба варианта /// шейдера, и не надо помнить, какой из них сейчас собран. Перерасход памяти — меньше /// сотой доли процента. const DIR_ALIGN: usize = 64; /// Расстояние между началами соседних направлений в буфере популяций, во флоатах. fn dir_stride(n: usize) -> usize { n.div_ceil(DIR_ALIGN) * DIR_ALIGN } fn to_soa(f: &[[R; math::Q]], stride: usize) -> Vec { let n = f.len(); debug_assert!(stride >= n); let mut v = vec![0.0f32; 9 * stride]; for (k, c) in f.iter().enumerate() { for i in 0..math::Q { v[i * stride + k] = c[i] as f32; } } v } /// Залить уровень на устройство: поля, маски, линки, β и групповая привязка. fn make_level( device: &wgpu::Device, layout: &wgpu::BindGroupLayout, nx: usize, ny: usize, geom: &cpu::Geom, beta: &[f32], probe_node: u32, flags: u32, init_field: &[[R; math::Q]], wall_layout: &wgpu::BindGroupLayout, split: bool, ) -> GpuLevel { let n = nx * ny; let stride = dir_stride(n); let init = to_soa(init_field, stride); let mkf = |label: &str| { device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(label), contents: bytemuck::cast_slice(&init), usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST, }) }; let f = mkf("f"); let post = mkf("post"); let gam = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("gamma"), contents: bytemuck::cast_slice(&vec![2.0f32; n]), usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC, }); let solid: Vec = geom.solid.iter().map(|s| *s as u32).collect(); let solid_buf = storage_init(device, "solid", bytemuck::cast_slice(&solid)); let links: Vec = geom .links .iter() .map(|l| GLink { node: l.node, far: l.far, i: l.i as u32, ib: l.ib as u32, kind: match l.kind { math::LinkKind::Near => 0, math::LinkKind::Far => 1, math::LinkKind::Simple => 2, }, q: l.q as f32, body: l.body as u32, _p: 0, }) .collect(); // Пустая сцена (эталонные течения) даёт нулевой список линков, а шейдер всё равно // объявляет массив структур: буфер обязан вмещать хотя бы один элемент, иначе валидация // ругается на несоответствие размера. Читать его при этом некому — nlinks = 0. let links_pad = if links.is_empty() { vec![GLink::zeroed()] } else { links.clone() }; let links_buf = storage_init(device, "links", bytemuck::cast_slice(&links_pad)); let beta_buf = storage_init(device, "beta", bytemuck::cast_slice(beta)); // индекс граничных узлов: (узел, смещение первого линка, число линков, —) let wnodes: Vec<[u32; 4]> = geom .wall_nodes .iter() .map(|w| [w.node, w.first, w.count as u32, 0]) .collect(); let nwall = wnodes.len() as u32; let wnodes_pad = if wnodes.is_empty() { vec![[0u32; 4]] } else { wnodes.clone() }; let wall_buf = storage_init(device, "wall nodes", bytemuck::cast_slice(&wnodes_pad)); let wall_bind = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("wall"), layout: wall_layout, entries: &[bind(0, &wall_buf)], }); let parts_count = ceil_div(n as u32, WG); let parts = storage(device, "parts", (parts_count as u64) * 32); let params = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("level params"), contents: bytemuck::bytes_of(&LevelParams { n: n as u32, nx: nx as u32, ny: ny as u32, nlinks: links.len() as u32, bcx: geom.body_cx as f32, bcy: geom.body_cy as f32, probe_node, flags, nwall, stride: stride as u32, _pad: [0; 2], }), usage: wgpu::BufferUsages::UNIFORM, }); // Общая часть связки одинакова в обоих вариантах; различаются только популяции. let mut entries = vec![ bind(2, &gam), bind(3, &solid_buf), bind(4, &links_buf), bind(5, &beta_buf), bind(6, &parts), bind(7, ¶ms), ]; if split { // Тот же буфер, но показанный шейдеру по одному направлению: каждая привязка // укладывается в n*4 байта вместо n*9*4, и предел драйвера на размер ОДНОЙ // привязки перестаёт быть потолком для размера сетки. for i in 0..math::Q { let off = (i * stride * 4) as u64; let len = (n * 4) as u64; entries.push(bind_range(POP_F0 + i as u32, &f, off, len)); entries.push(bind_range(POP_P0 + i as u32, &post, off, len)); } } else { entries.push(bind(0, &f)); entries.push(bind(1, &post)); } let bind = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("level"), layout, entries: &entries, }); GpuLevel { nx, ny, n, stride, f, gam, bind, wall_bind, nwall, parts_count, nlinks: links.len() as u32 } } // ───────────────────────────────────────────────────────────────────────────── // Симуляция // ───────────────────────────────────────────────────────────────────────────── pub struct Sim { spec: Spec, device: wgpu::Device, queue: wgpu::Queue, adapter_name: String, l0: GpuLevel, l1: Option, pre: wgpu::Buffer, dyn_buf: wgpu::Buffer, results_buf: wgpu::Buffer, staging: wgpu::Buffer, /// по одной группе на подшаг — различаются только индексом подшага в `Substep` dyn_binds: Vec, amr: Option, pipes: Pipes, readback: wgpu::Buffer, fluid_count: R, step_index: u64, d_ref: R, /// Номера шагов, посчитанных на устройстве, но ещё не прочитанных на хост. /// Длина = занятые слоты истории. pending: Vec, } struct AmrRes { nghost: u32, nrestrict: u32, bg_ghost_old: wgpu::BindGroup, bg_ghost_new: wgpu::BindGroup, bg_fill: Vec, bg_restrict: wgpu::BindGroup, } struct Pipes { collide: wgpu::ComputePipeline, stream: wgpu::ComputePipeline, bouzidi: wgpu::ComputePipeline, moment_wall: wgpu::ComputePipeline, walls: wgpu::ComputePipeline, channel: wgpu::ComputePipeline, force: wgpu::ComputePipeline, stats1: wgpu::ComputePipeline, stats2: wgpu::ComputePipeline, ghost: wgpu::ComputePipeline, fill: wgpu::ComputePipeline, restrict: wgpu::ComputePipeline, empty_bg: wgpu::BindGroup, } impl Sim { pub fn new(spec: Spec) -> Result { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends: wgpu::Backends::PRIMARY, // Флаги читаются из окружения, и нужно это ровно ради одного — // WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER. // // По умолчанию wgpu ПРЯЧЕТ адаптеры, не прошедшие набор тестов соответствия // Vulkan («Adapter is not Vulkan compliant, hiding adapter»), и делает это // молча: request_adapter просто возвращает None. Под это правило попадает // dzn (Dozen) — драйвер Mesa, транслирующий Vulkan в D3D12. В WSL2 он // единственный способ добраться до карты: Linux-драйвера Vulkan у NVIDIA // там нет, устройство отдаётся через /dev/dxg по протоколу WDDM. // // Поведение по умолчанию НЕ меняется: без переменной окружения // несоответствующие адаптеры по-прежнему скрыты. Переменная — осознанное // согласие считать на непроверенном драйвере, и прежде чем на нём считать, // надо прогнать bench/parity.py: он сверяет GPU-путь с CPU в двойной // точности на течениях с точным решением. flags: wgpu::InstanceFlags::from_build_config().with_env(), ..Default::default() }); let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, compatible_surface: None, force_fallback_adapter: false, })) .ok_or("подходящий GPU-адаптер не найден (нужен Vulkan, DX12 или Metal)")?; let info = adapter.get_info(); let adapter_name = format!("{} ({:?})", info.name, info.backend); let (device, queue) = pollster::block_on(adapter.request_device( &wgpu::DeviceDescriptor { label: Some("kbc2d"), required_features: wgpu::Features::empty(), // берём то, что реально умеет адаптер: связке уровня нужно 8 storage-биндингов, // downlevel-профиль даёт всего 4 required_limits: adapter.limits(), memory_hints: wgpu::MemoryHints::Performance, }, None, )) .map_err(|e| format!("не удалось получить устройство: {e}"))?; // Моментная стенка добавляет к связке уровня ещё один storage-биндинг (индекс // граничных узлов), итого 9 против 8, гарантируемых лимитами по умолчанию. На // десктопных адаптерах их обычно >= 16, но проверить надо явно. if spec.wall.is_moment_based() { let lim = adapter.limits().max_storage_buffers_per_shader_stage; if lim < 9 { return Err(format!( "адаптер даёт только {lim} storage-биндингов на стадию, моментной стенке \ нужно 9. Запусти с --wall bouzidi либо --backend cpu" )); } } // Влезает ли массив популяций в ОДНУ привязку. Считаем по самому крупному // уровню: у патча измельчения своих узлов может оказаться больше, чем у L0. let max_nodes = { let n0 = spec.nx * spec.ny; let n1 = match (spec.refine > 1, spec.patch) { (true, Some((ax, bx, ay, by))) => { (spec.refine * (bx - ax) + 1) * (spec.refine * (by - ay) + 1) } _ => 0, }; n0.max(n1) }; let need = (math::Q * dir_stride(max_nodes) * 4) as u64; // Переменная окружения нужна не для работы, а для проверки: без неё раздельный // вариант включается только на драйверах с малым пределом, и сверить два // варианта на одной карте было бы нечем. let split = need > adapter.limits().max_storage_buffer_binding_size as u64 || std::env::var("KBC2D_SPLIT_POPULATIONS").is_ok_and(|v| v != "0"); if split { // Шаг между направлениями выровнен на DIR_ALIGN значений f32. Если адаптер // требует более крупного выравнивания смещений, раздельные привязки собрать // нельзя — лучше сказать это прямо, чем ловить невнятную ошибку валидации. let align = adapter.limits().min_storage_buffer_offset_alignment as usize; if (DIR_ALIGN * 4) % align != 0 { return Err(format!( "адаптер требует выравнивания смещений на {align} байт, а раскладка популяций рассчитана на {}. Считай на CPU либо на другом драйвере", DIR_ALIGN * 4 )); } let per = (dir_stride(max_nodes) * 4) as u64; if per > adapter.limits().max_storage_buffer_binding_size as u64 { return Err(format!( "адаптер ограничивает привязку {} МиБ, а одному направлению нужно {} МиБ. Сетка слишком крупная для этого драйвера — считай на CPU либо на драйвере без такого предела", adapter.limits().max_storage_buffer_binding_size / 1048576, per / 1048576 )); } } // ── топология берётся из процессорного бэкенда, а не строится заново ── let geom0 = cpu::Geom::build(spec.nx, spec.ny, &spec.scene); let fluid_count = geom0.solid.iter().filter(|s| !**s).count() as R; let beta0: Vec = math::beta_profile(spec.nx, spec.beta0, spec.sponge_in, spec.sponge_len, spec.sponge_mult) .into_iter() .map(|v| v as f32) .collect(); let module = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("kbc2d.wgsl"), source: wgpu::ShaderSource::Wgsl(Cow::Owned(build_shader(split))), }); let bgl_level = level_layout(&device, split); let bgl_dyn = dyn_layout(&device); let bgl_amr = amr_layout(&device); let bgl_wall = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("wall"), entries: &[ro(0)], }); let bgl_empty = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("empty"), entries: &[], }); let pl_level = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("level"), bind_group_layouts: &[&bgl_level, &bgl_dyn], push_constant_ranges: &[], }); let pl_wall = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("moment wall"), bind_group_layouts: &[&bgl_level, &bgl_dyn, &bgl_empty, &bgl_wall], push_constant_ranges: &[], }); let pl_amr = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("amr"), bind_group_layouts: &[&bgl_empty, &bgl_empty, &bgl_amr], push_constant_ranges: &[], }); let mk = |layout: &wgpu::PipelineLayout, entry: &str| { device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { label: Some(entry), layout: Some(layout), module: &module, entry_point: entry, compilation_options: Default::default(), cache: None, }) }; // ── общие буферы ── let dyn_buf = device.create_buffer(&wgpu::BufferDescriptor { label: Some("dyn"), size: std::mem::size_of::() as u64, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); // история на HIST шагов + рабочие слоты силы на 8 подшагов let hist_bytes = (HIST * std::mem::size_of::()) as u64; let results_buf = device.create_buffer(&wgpu::BufferDescriptor { label: Some("results"), size: hist_bytes + 8 * 4 * 4, usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC, mapped_at_creation: false, }); let staging = device.create_buffer(&wgpu::BufferDescriptor { label: Some("staging"), size: hist_bytes, usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); let nsub = spec.refine.max(1); let dyn_binds: Vec = (0..nsub) .map(|s| { let sb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("substep"), contents: bytemuck::bytes_of(&Substep { idx: s as u32, _p: [0; 3] }), usage: wgpu::BufferUsages::UNIFORM, }); device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("dyn"), layout: &bgl_dyn, entries: &[bind(0, &dyn_buf), bind(1, &sb), bind(2, &results_buf)], }) }) .collect(); // стартовое поле: либо сразу набегающий поток, либо покой (см. cpu.rs) let u0 = if spec.init_uniform { let (sn, cs) = spec.flow_angle.sin_cos(); (spec.units.u_lat * cs, spec.units.u_lat * sn) } else { (0.0, 0.0) }; // ── зонд ── let (px, py) = spec.probe; let probe_on_fine = matches!(spec.patch, Some((ax, bx, ay, by)) if px >= ax && px <= bx && py >= ay && py <= by) && spec.refine > 1; // ── уровень 0 ── let probe0 = if probe_on_fine { 0 } else { py * spec.nx + px }; let flags0 = 1 | if probe_on_fine { 0 } else { 2 }; let l0 = make_level( &device, &bgl_level, spec.nx, spec.ny, &geom0, &beta0, probe0 as u32, flags0, &cpu::initial_field(&cpu::Init { case: spec.case, nx: spec.nx, ny: spec.ny, u0, beta: spec.beta0, scene: Some(&spec.scene), taper: if spec.init_uniform { spec.init_taper } else { 0.0 }, }), &bgl_wall, split, ); let pre = device.create_buffer(&wgpu::BufferDescriptor { label: Some("pre"), size: (math::Q * l0.stride * 4) as u64, usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); // ── уровень 1 и AMR ── let mut l1 = None; let mut amr = None; if spec.refine > 1 { let (ax, bx, ay, by) = spec.patch.ok_or("refine > 1 требует патч")?; let r = spec.refine; let scene1 = spec.scene.refined(r as R, ax as R, ay as R); let nfx = r * (bx - ax) + 1; let nfy = r * (by - ay) + 1; let geom1 = cpu::Geom::build(nfx, nfy, &scene1); let tau0 = 1.0 / (2.0 * spec.beta0); let tau1 = r as R * (tau0 - 0.5) + 0.5; let beta1 = vec![(1.0 / (2.0 * tau1)) as f32; nfx]; let r01 = tau1 / (r as R * tau0); let patch = cpu::Patch::new(&spec, &geom0, &geom1.solid, r01); let probe1 = if probe_on_fine { ((py - ay) * r) * nfx + (px - ax) * r } else { 0 }; let flags1 = if probe_on_fine { 2 } else { 0 }; let lvl1 = make_level( &device, &bgl_level, nfx, nfy, &geom1, &beta1, probe1 as u32, flags1, &cpu::initial_field(&cpu::Init { case: spec.case, nx: nfx, ny: nfy, u0, beta: 1.0 / (2.0 * tau1), scene: Some(&scene1), taper: if spec.init_uniform { spec.init_taper * r as R } else { 0.0 }, }), &bgl_wall, split, ); let ghosts: Vec = patch .ghosts() .iter() .map(|g| GGhost { fine: g.fine, c00: g.c00, c10: g.c10, c01: g.c01, c11: g.c11, tx: g.tx as f32, ty: g.ty as f32, _p: 0, }) .collect(); let rest: Vec<[u32; 2]> = patch.restrict_pairs().iter().map(|&(c, fi)| [c, fi]).collect(); let ghost_bytes = (ghosts.len() * 9 * 4) as u64; let gh_old = storage(&device, "gh_old", ghost_bytes); let gh_new = storage(&device, "gh_new", ghost_bytes); let ghosts_buf = storage_init(&device, "ghosts", bytemuck::cast_slice(&ghosts)); let rest_buf = storage_init(&device, "rest", bytemuck::cast_slice(&rest)); let dummy = storage(&device, "dummy", 16); let base = AmrParams { nghost: ghosts.len() as u32, nrestrict: rest.len() as u32, cstride: l0.stride as u32, fstride: lvl1.stride as u32, r01: r01 as f32, rfc: (1.0 / r01) as f32, w: 0.0, _pad: 0.0, }; let mk_amr_bg = |src: &wgpu::Buffer, dst: &wgpu::Buffer, a: &wgpu::Buffer, b: &wgpu::Buffer, params: AmrParams| { let ub = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("amr params"), contents: bytemuck::bytes_of(¶ms), usage: wgpu::BufferUsages::UNIFORM, }); device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("amr"), layout: &bgl_amr, entries: &[ bind(0, src), bind(1, dst), bind(2, &ghosts_buf), bind(3, a), bind(4, b), bind(5, &rest_buf), bind(6, &ub), ], }) }; let bg_ghost_old = mk_amr_bg(&pre, &gh_old, &dummy, &dummy, base); let bg_ghost_new = mk_amr_bg(&l0.f, &gh_new, &dummy, &dummy, base); let bg_fill: Vec = (0..r) .map(|s| { let mut p = base; p.w = ((s + 1) as R / r as R) as f32; mk_amr_bg(&dummy, &lvl1.f, &gh_old, &gh_new, p) }) .collect(); let bg_restrict = mk_amr_bg(&lvl1.f, &l0.f, &dummy, &dummy, base); amr = Some(AmrRes { nghost: ghosts.len() as u32, nrestrict: rest.len() as u32, bg_ghost_old, bg_ghost_new, bg_fill, bg_restrict, }); l1 = Some(lvl1); } let pipes = Pipes { collide: mk(&pl_level, "k_collide"), stream: mk(&pl_level, "k_stream"), bouzidi: mk(&pl_level, "k_bouzidi"), moment_wall: mk(&pl_wall, "k_moment_wall"), walls: mk(&pl_level, "k_walls"), channel: mk(&pl_level, "k_channel"), force: mk(&pl_level, "k_force"), stats1: mk(&pl_level, "k_stats1"), stats2: mk(&pl_level, "k_stats2"), ghost: mk(&pl_amr, "k_ghost"), fill: mk(&pl_amr, "k_fill"), restrict: mk(&pl_amr, "k_restrict"), empty_bg: device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("empty"), layout: &bgl_empty, entries: &[], }), }; let readback = device.create_buffer(&wgpu::BufferDescriptor { label: Some("readback"), size: (math::Q * l0.stride * 4) as u64, usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); let d_ref = match spec.patch { Some(_) if spec.refine > 1 => spec.scene.ref_size() * spec.refine as R, _ => spec.scene.ref_size(), }; Ok(Sim { spec, device, queue, adapter_name, l0, l1, pre, dyn_buf, results_buf, staging, dyn_binds, amr, pipes, readback, fluid_count, step_index: 0, pending: Vec::with_capacity(HIST), d_ref, }) } pub fn name(&self) -> &'static str { // сигнатура бэкенда фиксирована; конкретный адаптер печатается отдельно Box::leak(format!("GPU: {} , f32", self.adapter_name).into_boxed_str()) } pub fn force_ref_size(&self) -> R { self.d_ref } fn inlet(&self, t: u64) -> (R, R) { let sp = &self.spec; let ramp = if sp.init_uniform { 1.0 } else { math::smoothstep(t as R / sp.ramp.max(1) as R) }; let u = sp.units.u_lat * ramp; let (s, c) = sp.flow_angle.sin_cos(); let mut uy = u * s; if sp.pert_dur > 0 && t >= sp.ramp && t < sp.ramp + sp.pert_dur { let ph = (t - sp.ramp) as R / sp.pert_dur as R; uy += sp.pert_amp * sp.units.u_lat * (std::f64::consts::PI * ph).sin(); } (u * c, uy) } /// Посчитать один шаг. Результаты не читаются сразу: они копятся в истории на устройстве /// и попадают в `out` пачкой — либо когда история заполнится, либо по явному `flush`. pub fn advance(&mut self, out: &mut Vec) { let t = self.step_index; let slot = self.pending.len() as u32; let (ux_in, uy_in) = self.inlet(t); let refine = self.spec.refine.max(1); let moment_wall = self.spec.wall.is_moment_based(); let channel = self.spec.case == Case::Channel; self.queue.write_buffer( &self.dyn_buf, 0, bytemuck::bytes_of(&Dyn { ux_in: ux_in as f32, uy_in: uy_in as f32, rho_out: 1.0, kbc_model: (self.spec.kbc_model == math::KbcModel::N2) as u32, outlet_extrap: self.spec.outlet_extrapolate as u32, nparts: self.l0.parts_count, refine: refine as u32, collision: (self.spec.collision == Collision::Bgk) as u32, slot, wall_mode: match self.spec.wall { math::WallModel::Bouzidi => 0, math::WallModel::Grad => 1, math::WallModel::Hrr => 2, math::WallModel::Staircase => 3, }, _pad: [0; 2], }), ); let mut enc = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("step") }); // «до» — состояние L0 перед столкновением, нужно как старый край рамки патча if self.amr.is_some() { enc.copy_buffer_to_buffer(&self.l0.f, 0, &self.pre, 0, (math::Q * self.l0.stride * 4) as u64); } { let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("L0"), timestamp_writes: None, }); p.set_bind_group(0, &self.l0.bind, &[]); p.set_bind_group(1, &self.dyn_binds[0], &[]); let ncell = ceil_div(self.l0.n as u32, WG); p.set_pipeline(&self.pipes.collide); dispatch(&mut p, ncell); p.set_pipeline(&self.pipes.stream); dispatch(&mut p, ncell); // Эталонные течения статей периодичны по обеим осям: ГУ не накладываются вовсе. if channel { if moment_wall { p.set_bind_group(2, &self.pipes.empty_bg, &[]); p.set_bind_group(3, &self.l0.wall_bind, &[]); p.set_pipeline(&self.pipes.moment_wall); dispatch(&mut p, ceil_div(self.l0.nwall.max(1), WG)); } else { p.set_pipeline(&self.pipes.bouzidi); dispatch(&mut p, self.l0.link_groups()); } // порядок обязателен: стенки снимают заворот по y, затем Zou-He — по x p.set_pipeline(&self.pipes.walls); dispatch(&mut p, ceil_div(self.l0.nx as u32, WG)); p.set_pipeline(&self.pipes.channel); dispatch(&mut p, ceil_div(self.l0.ny as u32, WG)); } } if let (Some(l1), Some(a)) = (self.l1.as_ref(), self.amr.as_ref()) { { let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("ghost"), timestamp_writes: None, }); p.set_bind_group(0, &self.pipes.empty_bg, &[]); p.set_bind_group(1, &self.pipes.empty_bg, &[]); p.set_pipeline(&self.pipes.ghost); p.set_bind_group(2, &a.bg_ghost_old, &[]); dispatch(&mut p, ceil_div(a.nghost, WG)); p.set_bind_group(2, &a.bg_ghost_new, &[]); dispatch(&mut p, ceil_div(a.nghost, WG)); } let ncell1 = ceil_div(l1.n as u32, WG); let nlink1 = l1.link_groups(); for s in 0..refine { { let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("L1"), timestamp_writes: None, }); p.set_bind_group(0, &l1.bind, &[]); p.set_bind_group(1, &self.dyn_binds[s], &[]); p.set_pipeline(&self.pipes.collide); dispatch(&mut p, ncell1); p.set_pipeline(&self.pipes.stream); dispatch(&mut p, ncell1); if channel { if moment_wall { p.set_bind_group(2, &self.pipes.empty_bg, &[]); p.set_bind_group(3, &l1.wall_bind, &[]); p.set_pipeline(&self.pipes.moment_wall); dispatch(&mut p, ceil_div(l1.nwall.max(1), WG)); } else { p.set_pipeline(&self.pipes.bouzidi); dispatch(&mut p, nlink1); } } // силу снимаем на каждом подшаге, усредняется она в k_stats2 p.set_pipeline(&self.pipes.force); p.dispatch_workgroups(1, 1, 1); } { let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("fill"), timestamp_writes: None, }); p.set_bind_group(0, &self.pipes.empty_bg, &[]); p.set_bind_group(1, &self.pipes.empty_bg, &[]); p.set_bind_group(2, &a.bg_fill[s], &[]); p.set_pipeline(&self.pipes.fill); dispatch(&mut p, ceil_div(a.nghost, WG)); } } { let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("restrict"), timestamp_writes: None, }); p.set_bind_group(0, &self.pipes.empty_bg, &[]); p.set_bind_group(1, &self.pipes.empty_bg, &[]); p.set_bind_group(2, &a.bg_restrict, &[]); p.set_pipeline(&self.pipes.restrict); dispatch(&mut p, ceil_div(a.nrestrict, WG)); } } else { let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("force L0"), timestamp_writes: None, }); p.set_bind_group(0, &self.l0.bind, &[]); p.set_bind_group(1, &self.dyn_binds[0], &[]); p.set_pipeline(&self.pipes.force); p.dispatch_workgroups(1, 1, 1); } { let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("stats"), timestamp_writes: None, }); p.set_bind_group(1, &self.dyn_binds[0], &[]); p.set_bind_group(0, &self.l0.bind, &[]); p.set_pipeline(&self.pipes.stats1); dispatch(&mut p, self.l0.parts_count); if let Some(l1) = self.l1.as_ref() { // на тонком уровне stats1 нужен только чтобы снять зонд (флаг записи сумм снят) p.set_bind_group(0, &l1.bind, &[]); dispatch(&mut p, ceil_div(l1.n as u32, WG)); p.set_bind_group(0, &self.l0.bind, &[]); } p.set_pipeline(&self.pipes.stats2); p.dispatch_workgroups(1, 1, 1); } self.queue.submit(Some(enc.finish())); self.pending.push(t); self.step_index += 1; if self.pending.len() == HIST { self.flush(out); } } /// Дочитать всё, что уже посчитано устройством. Обязательно вызывать перед тем, как /// смотреть на поля (кадр гифки, проверка на NaN) — иначе записи отстанут от состояния. pub fn flush(&mut self, out: &mut Vec) { let n = self.pending.len(); if n == 0 { return; } let bytes = (n * std::mem::size_of::()) as u64; let mut enc = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("flush") }); enc.copy_buffer_to_buffer(&self.results_buf, 0, &self.staging, 0, bytes); self.queue.submit(Some(enc.finish())); let slice = self.staging.slice(..bytes); let (tx, rx) = std::sync::mpsc::channel(); slice.map_async(wgpu::MapMode::Read, move |r| { let _ = tx.send(r); }); self.device.poll(wgpu::Maintain::Wait); let got: Vec = match rx.recv() { Ok(Ok(())) => { let data = slice.get_mapped_range(); bytemuck::cast_slice::(&data[..bytes as usize]).to_vec() } _ => vec![Results::default(); n], }; self.staging.unmap(); for (i, &t) in self.pending.iter().enumerate() { out.push(self.make_rec(t, &got[i])); } self.pending.clear(); } fn make_rec(&self, t: u64, res: &Results) -> StepRec { let cnt = if res.cnt > 0.0 { res.cnt as R } else { self.fluid_count }; let mut body = [[0.0 as R; 3]; crate::MAX_BODY_BUCKETS]; for b in 0..crate::MAX_BODY_BUCKETS.min(4) { for c in 0..3 { body[b][c] = res.body[b * 3 + c] as R; } } StepRec { step: t, fx: res.fx as R, fy: res.fy as R, tz: res.tz as R, body, uy_probe: res.uy_probe as R, rho_mean: res.rho_sum as R / self.fluid_count, max_u: res.max_u as R, gamma_mean: res.g_sum as R / cnt, gamma_min: res.g_min as R, gamma_max: res.g_max as R, degenerate_frac: res.degen as R / cnt, xi_negative_frac: res.xineg as R / cnt, } } /// Скачать популяции L0 на хост (нужно для кадров и проверки на NaN). fn download_l0(&self) -> Vec { self.download(&self.l0.f, (math::Q * self.l0.stride * 4) as u64) } /// Скачать произвольный буфер уровня. Буфер приёма выделен один раз при сборке под самый /// большой запрос (популяции L0); для меньших читается только его начало. fn download(&self, src: &wgpu::Buffer, bytes: u64) -> Vec { let rb = &self.readback; let mut enc = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("dl") }); enc.copy_buffer_to_buffer(src, 0, rb, 0, bytes); self.queue.submit(Some(enc.finish())); let slice = rb.slice(..); let (tx, rx) = std::sync::mpsc::channel(); slice.map_async(wgpu::MapMode::Read, move |r| { let _ = tx.send(r); }); self.device.poll(wgpu::Maintain::Wait); let out = match rx.recv() { Ok(Ok(())) => { let m = slice.get_mapped_range(); bytemuck::cast_slice::(&m[..bytes as usize]).to_vec() } _ => vec![f32::NAN; (bytes / 4) as usize], }; rb.unmap(); out } /// Полное поле скорости уровня L0 — для метрик эталонных течений и радиуса влияния. pub fn sample_velocity(&self) -> (Vec, Vec) { let n = self.l0.n; let stride = self.l0.stride; let raw = self.download_l0(); let mut ux = Vec::with_capacity(n); let mut uy = Vec::with_capacity(n); for k in 0..n { let c: [R; 9] = std::array::from_fn(|i| raw[i * stride + k] as R); let (_, a, b) = math::macros(&c); ux.push(a); uy.push(b); } (ux, uy) } /// Срез вдоль осевой линии: (ρ, u_x) по столбцам. На GPU это полное скачивание поля, /// поэтому x–t диагностика включается редким шагом и только там, где нужна. pub fn sample_centerline(&self) -> (Vec, Vec) { let (nx, ny) = (self.l0.nx, self.l0.ny); let stride = self.l0.stride; let raw = self.download_l0(); let y = ny / 2; let mut rho = Vec::with_capacity(nx); let mut ux = Vec::with_capacity(nx); for x in 0..nx { let k = y * nx + x; let c: [R; 9] = std::array::from_fn(|i| raw[i * stride + k] as R); let (r, a, _) = math::macros(&c); rho.push(r); ux.push(a); } (rho, ux) } pub fn is_finite(&self) -> bool { self.download_l0().iter().all(|v| v.is_finite()) } pub fn sample_field(&self, kind: FieldKind) -> (Vec, Vec) { let (nx, ny, n) = (self.l0.nx, self.l0.ny, self.l0.n); let stride = self.l0.stride; let raw = self.download_l0(); let node = |k: usize| -> [R; 9] { std::array::from_fn(|i| raw[i * stride + k] as R) }; let mut out = vec![0.0; n]; match kind { FieldKind::Speed => { for k in 0..n { let (_, ux, uy) = math::macros(&node(k)); out[k] = (ux * ux + uy * uy).sqrt(); } } FieldKind::Density => { for k in 0..n { out[k] = math::macros(&node(k)).0; } } FieldKind::Vorticity => { let mut ux = vec![0.0; n]; let mut uy = vec![0.0; n]; for k in 0..n { let (_, a, b) = math::macros(&node(k)); ux[k] = a; uy[k] = b; } for y in 0..ny { for x in 0..nx { let xp = (x + 1).min(nx - 1); let xm = x.saturating_sub(1); let yp = (y + 1).min(ny - 1); let ym = y.saturating_sub(1); let dvdx = (uy[y * nx + xp] - uy[y * nx + xm]) / (xp - xm).max(1) as R; let dudy = (ux[yp * nx + x] - ux[ym * nx + x]) / (yp - ym).max(1) as R; out[y * nx + x] = dvdx - dudy; } } } FieldKind::Gamma => { // берём ровно то, что посчитало устройство: пересчёт на хосте с базовым β // соврал бы всюду, где β — поле (включённая губка) let g = self.download(&self.l0.gam, (n * 4) as u64); for k in 0..n { out[k] = g[k] as R; } } } let solid = cpu::Geom::build(nx, ny, &self.spec.scene).solid; (out, solid) } } // ───────────────────────────────────────────────────────────────────────────── // Мелкие помощники // ───────────────────────────────────────────────────────────────────────────── /// Предел числа рабочих групп на измерение — 65535 (`maxComputeWorkgroupsPerDimension`). /// Всё, что не влезло, раскладывается по второму измерению; шейдеры собирают линейный /// индекс через lin()/wlin(). Хвост за пределами поля отсекается обычной проверкой границ. fn wg_grid(groups: u32) -> (u32, u32) { const MAX: u32 = 65535; if groups <= MAX { (groups.max(1), 1) } else { (MAX, ceil_div(groups, MAX)) } } /// Запустить ядро на `groups` рабочих групп, разложив их по двум измерениям. fn dispatch(p: &mut wgpu::ComputePass<'_>, groups: u32) { let (gx, gy) = wg_grid(groups); p.dispatch_workgroups(gx, gy, 1); } fn ceil_div(a: u32, b: u32) -> u32 { (a + b - 1) / b } fn bind(binding: u32, buf: &wgpu::Buffer) -> wgpu::BindGroupEntry<'_> { wgpu::BindGroupEntry { binding, resource: buf.as_entire_binding() } } /// Привязать кусок буфера. Смещение обязано быть кратно /// `min_storage_buffer_offset_alignment` — этим и занят `dir_stride`. fn bind_range(binding: u32, buf: &wgpu::Buffer, offset: u64, size: u64) -> wgpu::BindGroupEntry<'_> { wgpu::BindGroupEntry { binding, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: buf, offset, size: std::num::NonZeroU64::new(size), }), } } fn storage(device: &wgpu::Device, label: &str, size: u64) -> wgpu::Buffer { device.create_buffer(&wgpu::BufferDescriptor { label: Some(label), size: size.max(16), usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }) } fn storage_init(device: &wgpu::Device, label: &str, data: &[u8]) -> wgpu::Buffer { let pad; let data = if data.is_empty() { pad = [0u8; 16]; &pad[..] } else { data }; device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(label), contents: data, usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, }) } fn ro(binding: u32) -> wgpu::BindGroupLayoutEntry { entry(binding, wgpu::BufferBindingType::Storage { read_only: true }) } fn rw(binding: u32) -> wgpu::BindGroupLayoutEntry { entry(binding, wgpu::BufferBindingType::Storage { read_only: false }) } fn un(binding: u32) -> wgpu::BindGroupLayoutEntry { entry(binding, wgpu::BufferBindingType::Uniform) } fn entry(binding: u32, ty: wgpu::BufferBindingType) -> wgpu::BindGroupLayoutEntry { wgpu::BindGroupLayoutEntry { binding, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Buffer { ty, has_dynamic_offset: false, min_binding_size: None }, count: None, } } /// Номера привязок раздельных популяций. Взяты выше занятых (2..7), чтобы остальная /// часть связки не меняла нумерацию между вариантами шейдера. const POP_F0: u32 = 8; const POP_P0: u32 = 17; /// Собрать WGSL под конкретный адаптер. /// /// `split` — раскладывать ли популяции по девяти привязкам вместо одной общей. /// /// Зачем это вообще. Драйверы ограничивают размер ОДНОЙ привязки storage-буфера, и у /// разных он разный: нативные дают гигабайты, а dzn (трансляция Vulkan в D3D12 — /// единственный способ добраться до карты из WSL2, где Linux-драйвера Vulkan у NVIDIA /// нет) даёт 128 МиБ. Массив популяций занимает nx*ny*9*4 байта, так что на dzn потолок /// выходит 3.73 млн узлов — сетка 2048x2048 уже не проходит. При этом сам БУФЕР держать /// разрешено: max_buffer_size там 2 ГиБ. Ограничена только привязка. /// /// Отсюда второй вариант: тот же буфер показывается девятью привязками, по одному /// направлению в каждой, и потолок поднимается в девять раз — до 33.5 млн узлов, чего /// хватает всей кампании. Платой служит switch в аксессорах, поэтому там, где предела /// нет, собирается прежний общий вариант без всякого switch. fn build_shader(split: bool) -> String { let mut d = String::new(); if split { for i in 0..math::Q { d += &format!( "@group(0) @binding({}) var f{i} : array; ", POP_F0 as usize + i ); d += &format!( "@group(0) @binding({}) var q{i} : array; ", POP_P0 as usize + i ); } let arm = |pfx: &str, body: &dyn Fn(usize) -> String| { let mut t = String::new(); for i in 0..math::Q - 1 { t += &format!(" case {i}u: {{ {} }} ", body(i)); } t += &format!(" default: {{ {} }} ", body(math::Q - 1)); let _ = pfx; t }; d += "fn fget(i: u32, nd: u32) -> f32 { switch i { "; d += &arm("f", &|i| format!("return f{i}[nd];")); d += " } } "; d += "fn fset(i: u32, nd: u32, v: f32) { switch i { "; d += &arm("f", &|i| format!("f{i}[nd] = v;")); d += " } } "; d += "fn pget(i: u32, nd: u32) -> f32 { switch i { "; d += &arm("q", &|i| format!("return q{i}[nd];")); d += " } } "; d += "fn pset(i: u32, nd: u32, v: f32) { switch i { "; d += &arm("q", &|i| format!("q{i}[nd] = v;")); d += " } } "; } else { d += "@group(0) @binding(0) var f : array; "; d += "@group(0) @binding(1) var post : array; "; d += "fn fget(i: u32, nd: u32) -> f32 { return f[i*P.stride + nd]; } "; d += "fn fset(i: u32, nd: u32, v: f32) { f[i*P.stride + nd] = v; } "; d += "fn pget(i: u32, nd: u32) -> f32 { return post[i*P.stride + nd]; } "; d += "fn pset(i: u32, nd: u32, v: f32) { post[i*P.stride + nd] = v; } "; } SHADER.replace("//__POPULATIONS__", &d) } fn level_layout(device: &wgpu::Device, split: bool) -> wgpu::BindGroupLayout { let mut entries = vec![rw(2), ro(3), ro(4), ro(5), rw(6), un(7)]; if split { for i in 0..math::Q as u32 { entries.push(rw(POP_F0 + i)); entries.push(rw(POP_P0 + i)); } } else { entries.push(rw(0)); entries.push(rw(1)); } device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("level"), entries: &entries, }) } fn dyn_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("dyn"), entries: &[un(0), un(1), rw(2)], }) } fn amr_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("amr"), entries: &[ro(0), rw(1), ro(2), ro(3), ro(4), ro(5), un(6)], }) }