Массив популяций занимает nx*ny*9*4 байта и показывался шейдеру одной привязкой. dzn объявляет max_storage_buffer_binding_size = 128 МиБ, поэтому потолок выходил 3.73 млн узлов: 14 прогонов кампании из 115 падали на создании bind group, а на них приходится 70.7% её стоимости. Ограничена при этом ровно привязка: max_buffer_size у dzn 2047 МиБ. Поэтому тот же буфер теперь показывается девятью привязками, по одному направлению в каждой, и потолок поднимается до 33.5 млн узлов — самая крупная сетка кампании (4096x4096, 16.8 млн) проходит с запасом. Как устроено: * шаг между направлениями выровнен на 256 байт (dir_stride), потому что смещение привязки обязано быть кратно min_storage_buffer_offset_alignment; * обращения к популяциям в WGSL идут через fget/fset/pget/pset, а их тело генерируется под вариант (build_shader); * вариант выбирается по max_storage_buffer_binding_size адаптера — где предела нет, собирается прежний общий, без switch в аксессорах; * KBC2D_SPLIT_POPULATIONS=1 включает раздельные принудительно: иначе сверить два варианта на одной карте нечем. Проверено: * на одном драйвере оба варианта дают одно и то же — Cd 2.39486, energy_end 5.74813e-05; раздельный стоит 5.7% пропускной способности; * на сетке 2048x2048, где раздельные привязки и нужны, нативный прогон против контейнерного: energy_end расходится на 2.4e-07, enstrophy_end на 1.3e-07; * preflight в контейнере: 115 сценариев из 115, ни одного отказа (было 14); * 29 собственных тестов решателя зелёные. Образ опубликован как notbigghost/kbc2d:1.2.0 (он же latest). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2039 lines
88 KiB
Rust
2039 lines
88 KiB
Rust
//! БЭКЕНД ПОД ВИДЕОКАРТУ на 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<storage, read_write> gam : array<f32>;
|
||
@group(0) @binding(3) var<storage, read> solid: array<u32>;
|
||
@group(0) @binding(4) var<storage, read> links: array<GLink>;
|
||
@group(0) @binding(5) var<storage, read> beta : array<f32>;
|
||
@group(0) @binding(6) var<storage, read_write> parts: array<Partial>;
|
||
@group(0) @binding(7) var<uniform> P : LevelParams;
|
||
//__POPULATIONS__
|
||
|
||
@group(1) @binding(0) var<uniform> D : Dyn;
|
||
@group(1) @binding(1) var<uniform> S : Substep;
|
||
// results[0..11] — итоги шага, results[16 + s*4 + c] — сила подшага s.
|
||
// Сила и итоги держатся в ОДНОМ буфере намеренно: связка уровня использует 7 storage-биндингов,
|
||
// и отдельный буфер сил вывел бы конвейер за предел 8, гарантируемый лимитами по умолчанию.
|
||
@group(1) @binding(2) var<storage, read_write> results : array<f32>;
|
||
|
||
@group(2) @binding(0) var<storage, read> amr_src: array<f32>;
|
||
@group(2) @binding(1) var<storage, read_write> amr_dst: array<f32>;
|
||
@group(2) @binding(2) var<storage, read> ghosts : array<GGhost>;
|
||
@group(2) @binding(3) var<storage, read> gh_a : array<f32>;
|
||
@group(2) @binding(4) var<storage, read> gh_b : array<f32>;
|
||
@group(2) @binding(5) var<storage, read> rest : array<vec2<u32>>;
|
||
@group(2) @binding(6) var<uniform> A : AmrParams;
|
||
|
||
// (узел, смещение первого линка, число линков, —). Линки одного узла лежат подряд.
|
||
@group(3) @binding(0) var<storage, read> wnodes : array<vec4<u32>>;
|
||
|
||
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<f32> {
|
||
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<f32>(t, cc);
|
||
}
|
||
|
||
// ───── физика: построчный перенос math.rs ─────
|
||
|
||
// Энтропийное равновесие в product-form (точный максимизатор энтропии при заданных rho, rho*u)
|
||
fn feq9(rho: f32, ux0: f32, uy0: f32) -> array<f32,9> {
|
||
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<f32,9>(
|
||
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<f32,9>) -> vec3<f32> {
|
||
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<f32>(rho, mx/rho, my/rho);
|
||
}
|
||
|
||
// Проекция неравновесия на сдвиговую часть. model=0: {N, Pi_xy} (N1); model=1: плюс след T (N2)
|
||
fn shift9(d: array<f32,9>, model: u32) -> array<f32,9> {
|
||
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<f32,9>(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<function, array<f32,9>>, off: u32) {
|
||
for (var i = 0u; i < 9u; i = i + 1u) { (*base)[i] = fget(i, off); }
|
||
}
|
||
|
||
// Возвращает (пост-столкновительные популяции, gamma, признак вырождения)
|
||
struct CollOut { fv: array<f32,9>, gamma: f32, degen: f32 };
|
||
|
||
fn collide9(fin: array<f32,9>, 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<f32,9>;
|
||
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<f32,9>;
|
||
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<f32,9>;
|
||
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<f32,9>, ux: f32, uy: f32) -> array<f32,9> {
|
||
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<f32,9>, rho_out: f32, uy_out: f32) -> array<f32,9> {
|
||
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<u32>, nwg: vec3<u32>) -> u32 { return gid.y * nwg.x * 64u + gid.x; }
|
||
fn wlin(wid: vec3<u32>, nwg: vec3<u32>) -> u32 { return wid.y * nwg.x + wid.x; }
|
||
|
||
@compute @workgroup_size(64)
|
||
fn k_collide(@builtin(global_invocation_id) gid: vec3<u32>,
|
||
@builtin(num_workgroups) nwg: vec3<u32>) {
|
||
let nd = lin(gid, nwg);
|
||
if (nd >= P.n) { return; }
|
||
var fv: array<f32,9>;
|
||
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<u32>,
|
||
@builtin(num_workgroups) nwg: vec3<u32>) {
|
||
let nd = lin(gid, nwg);
|
||
if (nd >= P.n) { return; }
|
||
var cx = array<i32,9>(0, 1, 0, -1, 0, 1, -1, -1, 1);
|
||
var cy = array<i32,9>(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<u32>,
|
||
@builtin(num_workgroups) nwg: vec3<u32>) {
|
||
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<u32>,
|
||
@builtin(num_workgroups) nwg: vec3<u32>) {
|
||
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<u32>,
|
||
@builtin(num_workgroups) nwg: vec3<u32>) {
|
||
let y = lin(gid, nwg);
|
||
if (y >= P.ny) { return; }
|
||
// вход: скоростной Zou-He по ВСЕМУ столбцу, включая угловые узлы
|
||
let a = y*P.nx;
|
||
var fv: array<f32,9>;
|
||
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<f32,9>;
|
||
load9(&gv, b);
|
||
var uyo = 0.0;
|
||
if (D.outlet_extrap != 0u) {
|
||
let c = y*P.nx + P.nx - 2u;
|
||
var cv: array<f32,9>;
|
||
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<workgroup> wfx: array<f32, 256>;
|
||
var<workgroup> wfy: array<f32, 256>;
|
||
var<workgroup> wtz: array<f32, 256>;
|
||
|
||
@compute @workgroup_size(256)
|
||
fn k_force(@builtin(local_invocation_id) lid: vec3<u32>) {
|
||
let t = lid.x;
|
||
var cx = array<f32,9>(0.0, 1.0, 0.0, -1.0, 0.0, 1.0, -1.0, -1.0, 1.0);
|
||
var cy = array<f32,9>(0.0, 0.0, 1.0, 0.0, -1.0, 1.0, 1.0, -1.0, -1.0);
|
||
// накопители на каждое тело держатся в регистрах, редукция потом идёт по телам подряд
|
||
var pfx = array<f32,4>(0.0, 0.0, 0.0, 0.0);
|
||
var pfy = array<f32,4>(0.0, 0.0, 0.0, 0.0);
|
||
var ptz = array<f32,4>(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<workgroup> wp: array<Partial, 64>;
|
||
|
||
@compute @workgroup_size(64)
|
||
fn k_stats1(@builtin(global_invocation_id) gid: vec3<u32>,
|
||
@builtin(local_invocation_id) lid: vec3<u32>,
|
||
@builtin(workgroup_id) wid: vec3<u32>,
|
||
@builtin(num_workgroups) nwg: vec3<u32>) {
|
||
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<f32,9>;
|
||
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<f32,9>;
|
||
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<u32>) {
|
||
let t = lid.x;
|
||
// слагаемых до 10^5 на поток — здесь и живёт основная потеря f32, поэтому все
|
||
// аддитивные накопители компенсированные
|
||
var rho = vec2<f32>(0.0, 0.0);
|
||
var gsum = vec2<f32>(0.0, 0.0);
|
||
var cnt = vec2<f32>(0.0, 0.0);
|
||
var degen = vec2<f32>(0.0, 0.0);
|
||
var xineg = vec2<f32>(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<f32>(0.0, 0.0);
|
||
var qgsum = vec2<f32>(0.0, 0.0);
|
||
var qcnt = vec2<f32>(0.0, 0.0);
|
||
var qdeg = vec2<f32>(0.0, 0.0);
|
||
var qxin = vec2<f32>(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<f32> {
|
||
var ff: array<f32,9>;
|
||
for (var i = 0u; i < 9u; i = i + 1u) { ff[i] = pget(i, nd); }
|
||
let m = macros9(ff);
|
||
return vec2<f32>(m.y, m.z);
|
||
}
|
||
|
||
// (du/dx, du/dy, dv/dx, dv/dy): центральная разность там, где оба соседа жидкие,
|
||
// односторонняя — где один твёрдый, ноль — если твёрдые оба.
|
||
fn grad_u_at_t(nd: u32) -> vec4<f32> {
|
||
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<f32>(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<f32>(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<f32>(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<f32,9> {
|
||
let axx = pxx - rho*CS2;
|
||
let ayy = pyy - rho*CS2;
|
||
var w = array<f32,9>(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<f32,9>(0.0, 1.0, 0.0, -1.0, 0.0, 1.0, -1.0, -1.0, 1.0);
|
||
var cy = array<f32,9>(0.0, 0.0, 1.0, 0.0, -1.0, 1.0, 1.0, -1.0, -1.0);
|
||
var out: array<f32,9>;
|
||
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<f32,9> {
|
||
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<f32,9>(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<f32,9>(0.0, 1.0, 0.0, -1.0, 0.0, 1.0, -1.0, -1.0, 1.0);
|
||
var cy = array<f32,9>(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<u32>,
|
||
@builtin(num_workgroups) nwg: vec3<u32>) {
|
||
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<u32,9>(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<u32,9>(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<u32>,
|
||
@builtin(num_workgroups) nwg: vec3<u32>) {
|
||
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<f32,4>(w00, w10, w01, w11);
|
||
var cs = array<u32,4>(g.c00, g.c10, g.c01, g.c11);
|
||
var rho = 0.0; var ux = 0.0; var uy = 0.0;
|
||
var neq = array<f32,9>(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<f32,9>;
|
||
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<u32>,
|
||
@builtin(num_workgroups) nwg: vec3<u32>) {
|
||
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<u32>,
|
||
@builtin(num_workgroups) nwg: vec3<u32>) {
|
||
let k = lin(gid, nwg);
|
||
if (k >= A.nrestrict) { return; }
|
||
let pr = rest[k];
|
||
var ff: array<f32,9>;
|
||
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<f32> {
|
||
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<u32> = geom.solid.iter().map(|s| *s as u32).collect();
|
||
let solid_buf = storage_init(device, "solid", bytemuck::cast_slice(&solid));
|
||
let links: Vec<GLink> = 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<GpuLevel>,
|
||
pre: wgpu::Buffer,
|
||
|
||
dyn_buf: wgpu::Buffer,
|
||
results_buf: wgpu::Buffer,
|
||
staging: wgpu::Buffer,
|
||
/// по одной группе на подшаг — различаются только индексом подшага в `Substep`
|
||
dyn_binds: Vec<wgpu::BindGroup>,
|
||
|
||
amr: Option<AmrRes>,
|
||
|
||
pipes: Pipes,
|
||
readback: wgpu::Buffer,
|
||
|
||
fluid_count: R,
|
||
step_index: u64,
|
||
d_ref: R,
|
||
/// Номера шагов, посчитанных на устройстве, но ещё не прочитанных на хост.
|
||
/// Длина = занятые слоты истории.
|
||
pending: Vec<u64>,
|
||
}
|
||
|
||
struct AmrRes {
|
||
nghost: u32,
|
||
nrestrict: u32,
|
||
bg_ghost_old: wgpu::BindGroup,
|
||
bg_ghost_new: wgpu::BindGroup,
|
||
bg_fill: Vec<wgpu::BindGroup>,
|
||
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<Sim, String> {
|
||
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<f32> =
|
||
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::<Dyn>() as u64,
|
||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||
mapped_at_creation: false,
|
||
});
|
||
// история на HIST шагов + рабочие слоты силы на 8 подшагов
|
||
let hist_bytes = (HIST * std::mem::size_of::<Results>()) 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<wgpu::BindGroup> = (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<GGhost> = 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<wgpu::BindGroup> = (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<StepRec>) {
|
||
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<StepRec>) {
|
||
let n = self.pending.len();
|
||
if n == 0 {
|
||
return;
|
||
}
|
||
let bytes = (n * std::mem::size_of::<Results>()) 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<Results> = match rx.recv() {
|
||
Ok(Ok(())) => {
|
||
let data = slice.get_mapped_range();
|
||
bytemuck::cast_slice::<u8, Results>(&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<f32> {
|
||
self.download(&self.l0.f, (math::Q * self.l0.stride * 4) as u64)
|
||
}
|
||
|
||
/// Скачать произвольный буфер уровня. Буфер приёма выделен один раз при сборке под самый
|
||
/// большой запрос (популяции L0); для меньших читается только его начало.
|
||
fn download(&self, src: &wgpu::Buffer, bytes: u64) -> Vec<f32> {
|
||
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::<u8, f32>(&m[..bytes as usize]).to_vec()
|
||
}
|
||
_ => vec![f32::NAN; (bytes / 4) as usize],
|
||
};
|
||
rb.unmap();
|
||
out
|
||
}
|
||
|
||
/// Полное поле скорости уровня L0 — для метрик эталонных течений и радиуса влияния.
|
||
pub fn sample_velocity(&self) -> (Vec<R>, Vec<R>) {
|
||
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<R>, Vec<R>) {
|
||
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<R>, Vec<bool>) {
|
||
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<storage, read_write> f{i} : array<f32>;
|
||
",
|
||
POP_F0 as usize + i
|
||
);
|
||
d += &format!(
|
||
"@group(0) @binding({}) var<storage, read_write> q{i} : array<f32>;
|
||
",
|
||
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<storage, read_write> f : array<f32>;
|
||
";
|
||
d += "@group(0) @binding(1) var<storage, read_write> post : array<f32>;
|
||
";
|
||
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)],
|
||
})
|
||
}
|