#include "fiko/dokum/sph_gpu.hpp" #include #include #include #include #include #include "fiko/core/gunluk.hpp" #include "fiko/dokum/dokum_kurulum.hpp" #include "fiko/dokum/malzeme.hpp" #include "fiko/dokum/voxel_kafes.hpp" #include "fiko/render/compute_baglam.hpp" #include "fiko/render/vulkan_baglam.hpp" // ============================================================================ // GPU SPH (PBF) — Vulkan compute. Shader SPIR-V yolu "shader/sph_*.comp.spv" // (app CMakeLists foreach'ine eklenir; kutuk SDF motoruyla aynı kalıp). // Sahne/ImGui/olay YOK — saf motor; worker'dan çağrılabilir (kilitsiz, tekil GPU // kaynağı kullanan ComputeBaglam kendi komut havuzunu tutar). // ============================================================================ namespace fiko::dokum { namespace { constexpr const char* kYogunlukSpv = "shader/sph_yogunluk.comp.spv"; constexpr const char* kKuvvetSpv = "shader/sph_kuvvet.comp.spv"; constexpr const char* kEntegreSpv = "shader/sph_entegre.comp.spv"; constexpr const char* kHashBucketSpv = "shader/sph_hash_bucket.comp.spv"; constexpr u32 kBucketK = 64; // hücre başına maks partikül (kova kapasitesi) constexpr u32 kBatchAdim = 8; // tek submit'te substep (emit/bin/readback batch sınırında) // Shader Itme (push constant) ile BİREBİR (std430 skaler sıralama). struct Itme { u32 n; f32 h, rho0, kutle, dt, epsCFM, sCorrK, sCorrN, dq, xsphC; f32 sigmaC; // kohesyon (PBF yüzey gerilimi karşılığı) — sph_ortak.glsl ile aynı sırada f32 gridOx, gridOy, gridOz; u32 gnx, gny, gnz; u32 bucketK; // hücre başına maks partikül (kova kapasitesi) f32 kafesOx, kafesOy, kafesOz, kafesH; u32 knx, kny, knz; f32 gx, gy, gz; u32 faz; }; inline u32 grup64(u32 n) { return (n + 63u) / 64u; } } // namespace // --------------------------------------------------------------------------- // SphGpuCozucu::Ic — GPU kaynakları + PBF durum tamponları // --------------------------------------------------------------------------- struct SphGpuCozucu::Ic { ComputeBaglam* comp = nullptr; std::unique_ptr compSahip; std::unique_ptr yogunluk, kuvvet, entegre, hashBucket; // Partikül SSBO'ları (kapasite = maksEmit). std::unique_ptr konum, hiz, eskiKonum, lambda; // Bucket grid SSBO'ları: hucreSay = hücre sayacı, parcaHucre = kova (gridHucre×bucketK). std::unique_ptr hucreSay, parcaHucre; // Voxel kafes tip SSBO (sınır projeksiyonu). std::unique_ptr kafesTip; u32 kapasite = 0; u32 hucreSayisi = 0; }; SphGpuCozucu::SphGpuCozucu(VulkanBaglam& vb) : vb_(&vb) { if (vb.aygitKayipMi()) { sonHata_ = "GPU aygıtı kayıp — CPU SPH'e düşülüyor."; return; } ic_ = std::make_unique(); ic_->compSahip = std::make_unique(vb); ic_->comp = ic_->compSahip.get(); const u32 itmeBayt = static_cast(sizeof(Itme)); // Binding sayıları shader layout'larıyla eşleşir (en yüksek binding+1). ic_->yogunluk = std::make_unique(vb, kYogunlukSpv, 6, itmeBayt); ic_->kuvvet = std::make_unique(vb, kKuvvetSpv, 7, itmeBayt); ic_->entegre = std::make_unique(vb, kEntegreSpv, 6, itmeBayt); ic_->hashBucket = std::make_unique(vb, kHashBucketSpv, 3, itmeBayt); if (!ic_->yogunluk->gecerli() || !ic_->kuvvet->gecerli() || !ic_->entegre->gecerli() || !ic_->hashBucket->gecerli()) { sonHata_ = "SPH compute pipeline kurulamadı (SPIR-V yok?) — CPU'ya düşülüyor."; ic_.reset(); return; } gpuHazir_ = true; } SphGpuCozucu::~SphGpuCozucu() = default; // --------------------------------------------------------------------------- // coz — PBF çözüm zinciri (CPU sphDolumCoz ile AYNI çıktı sözleşmesi) // --------------------------------------------------------------------------- SphDolumSonuc SphGpuCozucu::coz(const VoxelKafes& kafes, const DokumKurulum& kurulum, const Alasim& alasim, const SphDolumAyar& ayar, IlerlemeCB ilerleme) { SphDolumSonuc s; const u32 N = kafes.hucreSayi(); auto& d = s.dolum; d.varisZamani.assign(N, -1.0); d.varisT.assign(N, 0.0); d.coldShut.assign(N, 0); d.havaKapani.assign(N, 0); if (!gpuHazir_ || !ic_) return s; // çağıran CPU'ya düşer if (!kafes.gecerliMi() || !kurulum.aktifGirisVar()) return s; auto metalMi = [&](u32 idx) { return kafes.tip[idx] == HucreTip::Ic || kafes.tip[idx] == HucreTip::Sinir; }; auto icMi = [&](u32 idx) { return kafes.tip[idx] == HucreTip::Ic; }; u32 toplamMetal = 0, icSayi = 0; for (u32 i = 0; i < N; ++i) { if (metalMi(i)) ++toplamMetal; if (icMi(i)) ++icSayi; } d.toplamMetal = toplamMetal; if (icSayi == 0) return s; // Geometri ölçekleri (CPU yolla birebir). const f64 hFiz = kafes.h(); // metre const Vec3d m0 = kafes.merkez(0, 0, 0); f64 hMesh = hFiz; if (kafes.nx() > 1) hMesh = kafes.merkez(1, 0, 0).x - m0.x; else if (kafes.ny() > 1) hMesh = kafes.merkez(0, 1, 0).y - m0.y; else if (kafes.nz() > 1) hMesh = kafes.merkez(0, 0, 1).z - m0.z; const f64 birimOlcek = hFiz / hMesh; // mesh → metre const Vec3d kok{m0.x - 0.5 * hMesh, m0.y - 0.5 * hMesh, m0.z - 0.5 * hMesh}; // Partikül yarıçapı [m] + kütle/hacim — CPU ile TEK KAYNAK (formül iki yerde // kopyalanmıştı; sürüklenirse iki yol sessizce ayrışırdı). const f64 r = sphEtkinYaricapM(kafes, ayar); if (!(r > 0)) return s; const f64 cap = 2.0 * r; const f64 partikulHacim = 0.8 * cap * cap * cap; // FluidModel m_V kalıbı const f64 hSupport = 4.0 * r; // smoothing yarıçapı (SPlisHSPlasH h=4r) const f64 kutle = partikulHacim * alasim.yogunluk; // Emitter planı + havuz boyutu (CPU ile TEK KAYNAK — sphEmitPlani). const SphEmitPlan plan = sphEmitPlani(kafes, kurulum, alasim, ayar); if (plan.toplamHacimM3 <= 0) return s; const u32 maksEmit = plan.havuz; const f64 maksSure = (ayar.maksSure > 0) ? ayar.maksSure : 1.5 * plan.sonPencereS + 0.5; s.maksSureS = maksSure; // ---- GPU tamponlarını (yeniden) ayır ------------------------------------ const Aabbd& kafesSinir = kafes.sinir(); // Hash grid: kafes fiziksel AABB + pay (partiküller kaviteden hafif taşabilir). const Vec3d gMin{kok.x * birimOlcek - hSupport, kok.y * birimOlcek - hSupport, kok.z * birimOlcek - hSupport}; const f64 gLenX = kafes.nx() * hMesh * birimOlcek + 2 * hSupport; const f64 gLenY = kafes.ny() * hMesh * birimOlcek + 2 * hSupport; const f64 gLenZ = kafes.nz() * hMesh * birimOlcek + 2 * hSupport; const u32 gnx = std::max(1u, static_cast(std::ceil(gLenX / hSupport))); const u32 gny = std::max(1u, static_cast(std::ceil(gLenY / hSupport))); const u32 gnz = std::max(1u, static_cast(std::ceil(gLenZ / hSupport))); const u32 gridHucre = gnx * gny * gnz; (void)kafesSinir; ic_->kapasite = maksEmit; ic_->hucreSayisi = gridHucre; const u64 pv4 = static_cast(maksEmit) * 4 * sizeof(f32); ic_->konum = std::make_unique(*vb_, pv4); ic_->hiz = std::make_unique(*vb_, pv4); ic_->eskiKonum = std::make_unique(*vb_, pv4); ic_->lambda = std::make_unique(*vb_, static_cast(maksEmit) * sizeof(f32)); // Kova dizisi: gridHucre × bucketK (prefix-sum yok → CPU senkronu yok → batch). ic_->parcaHucre = std::make_unique( *vb_, static_cast(gridHucre) * kBucketK * sizeof(u32)); ic_->hucreSay = std::make_unique(*vb_, static_cast(gridHucre) * sizeof(u32)); ic_->kafesTip = std::make_unique(*vb_, static_cast(N) * sizeof(u32)); if (!ic_->konum->gecerli() || !ic_->hiz->gecerli() || !ic_->eskiKonum->gecerli() || !ic_->lambda->gecerli() || !ic_->parcaHucre->gecerli() || !ic_->hucreSay->gecerli() || !ic_->kafesTip->gecerli()) { gpuHazir_ = false; sonHata_ = "SPH SSBO ayrılamadı — CPU'ya düşülüyor."; return s; } // Kafes tip SSBO yükle (0=Dış,1=İç,2=Sınır). { std::vector tip(N); for (u32 i = 0; i < N; ++i) tip[i] = kafes.tip[i] == HucreTip::Ic ? 1u : kafes.tip[i] == HucreTip::Sinir ? 2u : 0u; ic_->kafesTip->yaz(tip.data(), static_cast(N) * sizeof(u32)); } // Host-ayna partikül dizileri (emit CPU'da; readback binning için). std::vector hKonum(static_cast(maksEmit) * 4, 0.f); std::vector hHiz(static_cast(maksEmit) * 4, 0.f); u32 aktif = 0; // yayınlanmış partikül sayısı // ---- Emitter planı: her giriş için doğuş konumu/yönü/hız/pencere --------- struct EmitPlan { Vec3d taban; // ağız merkezi [m] Vec3d yon; // birim Vec3d u, v; // ağız düzlemi ekseni f64 hiz; // çıkış hızı [m/s] u32 genislik; // ızgara genişliği f64 t0, t1; // pencere [s] f64 sonEmit; // son yayın anı (debiden aralık) f64 aralik; // yayın aralığı [s] = cap/hiz f64 T; // döküm sıcaklığı }; std::vector planlar; auto ortoTaban = [](Vec3d yon, Vec3d& u, Vec3d& v) { Vec3d x = yon; const f64 nn = std::sqrt(x.uzunlukKare()); if (nn < 1e-9) x = {0, 0, -1}; else x = x / nn; Vec3d yard = (std::abs(x.z) < 0.9) ? Vec3d{0, 0, 1} : Vec3d{1, 0, 0}; u = yard.capraz(x); u = u / std::sqrt(u.uzunlukKare()); v = x.capraz(u); }; for (const auto& g : kurulum.girisler) { if (!g.aktif || g.kutleKg <= 0 || g.sureS <= 0) continue; EmitPlan p{}; p.yon = Vec3d{g.yon.x, g.yon.y, g.yon.z}; ortoTaban(p.yon, p.u, p.v); p.taban = Vec3d{g.konum.x * birimOlcek, g.konum.y * birimOlcek, g.konum.z * birimOlcek} + p.yon * hSupport; // kaviteye it p.genislik = std::max(1u, static_cast(std::round(g.capMm * 1e-3 / cap))); // Dilim başına partikül sayısı (dairesel maske). u32 nIn = 0; const f64 yariCap2 = 0.25 * p.genislik * p.genislik * cap * cap; const f64 bas = -0.5 * (p.genislik - 1) * cap; for (u32 a = 0; a < p.genislik; ++a) for (u32 b = 0; b < p.genislik; ++b) { const f64 x = bas + a * cap, y = bas + b * cap; if (x * x + y * y <= yariCap2) ++nIn; } nIn = std::max(nIn, 1u); const f64 debiM3s = (g.kutleKg / alasim.yogunluk) / g.sureS; p.hiz = std::max(debiM3s * cap / (nIn * partikulHacim), 1e-3); p.aralik = cap / p.hiz; // bir partikül-katmanı yayma periyodu p.t0 = g.gecikmeS; p.t1 = g.gecikmeS + g.sureS; p.sonEmit = p.t0 - p.aralik; // ilk adımda hemen yay p.T = ayar.dokumSicaklik; planlar.push_back(p); } if (planlar.empty()) return s; // ---- PBF çözüm sabitleri ------------------------------------------------- Itme it{}; it.h = static_cast(hSupport); it.rho0 = static_cast(alasim.yogunluk); it.kutle = static_cast(kutle); it.epsCFM = static_cast(alasim.yogunluk * alasim.yogunluk * 1e-6); // CFM ε it.sCorrK = 1e-4f; it.sCorrN = 4.f; it.dq = 0.2f; // xSPH çalkantı katsayısı. ESKİ: sabit `0.01 + 2·viskozite` (viskozite // KATLANARAK çalkantıyı da değiştiriyordu — kullanıcı çalkantıyı ayrı // kontrol edemiyordu). YENİ: ayar > 0 ise o değer, değilse eski formül // (geri dönüş birebir). GPU PBF yolu için çalkantı "akıyor" izleniminin // en ucuz anahtarıdır (shader'da 3 satır: v += xsphC·dv). it.xsphC = static_cast( ayar.xsphKatsayi > 0.0 ? std::clamp(ayar.xsphKatsayi, 0.0, 0.5) : std::clamp(0.01 + 2.0 * ayar.viskozite, 0.0, 0.5)); // Kohesyon ivmesi ölçeği. σ [N/m] doğrudan ivme DEĞİL: kuvvet/m = σ·(1/h)·C // ölçeğinde. h [m] cinsinden 1/h büyüdükçe aynı σ daha güçlü çekim yapar // (ince h → ince parçacık → yüzey kuvveti baskın olur, doğru fizik). // O yüzden σ'yı h ile ölçekleyip 0.5·σ/h ile sınırlıyoruz: kararlılık tavanı // (CFL) korunur, çok ince kafeste patlama olmaz. it.sigmaC = 0.0f; if (ayar.yuzeyGerilim > 0 && hSupport > 1e-9) it.sigmaC = static_cast(std::clamp(0.5 * ayar.yuzeyGerilim / hSupport, 0.0, 50.0)); it.gridOx = static_cast(gMin.x); it.gridOy = static_cast(gMin.y); it.gridOz = static_cast(gMin.z); it.gnx = gnx; it.gny = gny; it.gnz = gnz; it.bucketK = kBucketK; it.kafesOx = static_cast(kok.x * birimOlcek); it.kafesOy = static_cast(kok.y * birimOlcek); it.kafesOz = static_cast(kok.z * birimOlcek); it.kafesH = static_cast(hMesh * birimOlcek); it.knx = kafes.nx(); it.kny = kafes.ny(); it.knz = kafes.nz(); Vec3f gy = kurulum.graviteYon.birim(); if (gy.uzunlukKare() < 1e-12f) gy = {0, 0, -1}; it.gx = static_cast(gy.x * ayar.gravite); it.gy = static_cast(gy.y * ayar.gravite); it.gz = static_cast(gy.z * ayar.gravite); // Zaman: CFL-benzeri substep. PBF büyük adımda kararlı → h/(vmaks) tabanlı dt. const f64 dt = std::clamp(ayar.maksAdimDt, 1e-4, 0.01); // PBF Jacobi iterasyonu: Macklin & Müller 2013 fill/akış için 3-4 yeterli // (>4 marjinal kazanç, lineer maliyet). 4'te kilitli üst sınır → GPU compute-bound // yükü ~%33 azalır, görsel dolum kalitesi korunur. Kullanıcı sıkı sıkıştırılamazlık // isterse basincMaksIter ile 2-4 arası ayarlanır. const u32 pbfIter = std::clamp(ayar.basincMaksIter, 2u, 4u); // Çıktı istatistik tamponları (CPU binning). const f64 hucreHacim = kafes.hucreHacim(); const u32 kareSayi = std::max(1u, ayar.kareSayi); const f64 kareAralik = maksSure / kareSayi; f64 sonrakiKare = kareAralik; std::vector sayac(N, 0); std::vector dolu(N, 0); std::vector hizToplam(N, 0.0); std::vector hizAdet(N, 0); s.hizMaks.assign(N, 0.f); s.hizOrt.assign(N, 0.f); s.oksit.assign(N, 0.f); const f64 doluPay = partikulHacim / hucreHacim; u32 dolan = 0; f64 t = 0, sonDegisimT = 0; bool iptal = false; const f64 kritikHiz = (ayar.kritikHiz > 0) ? ayar.kritikHiz : kritikGirisHiz(alasim); std::vector pOksit(maksEmit, 0); const auto baslangic = std::chrono::steady_clock::now(); bool gpuHata = false; // Pipeline descriptor set'leri TÜM SİMÜLASYON boyunca sabit (aynı buffer'lar) → // BİR KEZ bağla; döngü içinde yalnız dispatch kaydedilir (bucket grid batch). ic_->entegre->bagla({ic_->konum->tampon(), ic_->hiz->tampon(), ic_->eskiKonum->tampon(), ic_->lambda->tampon(), ic_->hucreSay->tampon(), ic_->parcaHucre->tampon()}); ic_->hashBucket->bagla({ic_->konum->tampon(), ic_->hucreSay->tampon(), ic_->parcaHucre->tampon()}); ic_->yogunluk->bagla({ic_->konum->tampon(), ic_->hiz->tampon(), ic_->eskiKonum->tampon(), ic_->lambda->tampon(), ic_->hucreSay->tampon(), ic_->parcaHucre->tampon()}); ic_->kuvvet->bagla({ic_->konum->tampon(), ic_->hiz->tampon(), ic_->eskiKonum->tampon(), ic_->lambda->tampon(), ic_->hucreSay->tampon(), ic_->parcaHucre->tampon(), ic_->kafesTip->tampon()}); const u64 saySifirBayt = static_cast(gridHucre) * sizeof(u32); // ---- Zaman döngüsü (BATCH: kBatchAdim substep tek submit) ---------------- while (t < maksSure && !gpuHata) { // (1) EMİT (CPU): pencere içi girişlerden yeni partikül doğ. for (auto& p : planlar) { if (t < p.t0 || t > p.t1) continue; while (p.sonEmit + p.aralik <= t && aktif < maksEmit) { p.sonEmit += p.aralik; // Advektif ofset: batch t'yi kBatchAdim·dt atlar → aynı pencereye düşen // emit olayları AYNI taban noktasına üst üste doğar (r=0 çifti → gradyan // patlaması). Doğum, olay zamanından bu yana akış kadar ilerletilir. const f64 iler = std::max(0.0, t - p.sonEmit) * p.hiz; const f64 bas = -0.5 * (p.genislik - 1) * cap; const f64 yariCap2 = 0.25 * p.genislik * p.genislik * cap * cap; for (u32 a = 0; a < p.genislik && aktif < maksEmit; ++a) for (u32 b = 0; b < p.genislik && aktif < maksEmit; ++b) { const f64 ox = bas + a * cap, oy = bas + b * cap; if (ox * ox + oy * oy > yariCap2) continue; const Vec3d pos = p.taban + p.u * ox + p.v * oy + p.yon * iler; const Vec3d vel = p.yon * p.hiz; const size_t k4 = static_cast(aktif) * 4; hKonum[k4 + 0] = (f32)pos.x; hKonum[k4 + 1] = (f32)pos.y; hKonum[k4 + 2] = (f32)pos.z; hKonum[k4 + 3] = 1.f; // aktif hHiz[k4 + 0] = (f32)vel.x; hHiz[k4 + 1] = (f32)vel.y; hHiz[k4 + 2] = (f32)vel.z; hHiz[k4 + 3] = (f32)p.T; ++aktif; } } } if (aktif == 0) { t += dt * kBatchAdim; continue; } // Host-ayna → GPU (batch boyunca sabit aktif set). const u64 aktifBayt4 = static_cast(aktif) * 4 * sizeof(f32); ic_->konum->yaz(hKonum.data(), aktifBayt4); ic_->hiz->yaz(hHiz.data(), aktifBayt4); it.n = aktif; it.dt = static_cast(dt); const u32 gDagit = grup64(aktif); // (2) TEK SUBMIT: kBatchAdim tam PBF adımı zincirlenir (predict → hücreSay // sıfırla → bucket-scatter → PBF iter → finalize). Bucket grid CPU senkronu // gerektirmez → tüm batch tek komut tamponunda (katilasma kalıbı → submit/ // fence-wait stall'ı kBatchAdim kat azalır). gpuHata = !ic_->comp->calis([&](VkCommandBuffer kb) { for (u32 sub = 0; sub < kBatchAdim; ++sub) { // (a) TAHMİN: x0=x, v+=g·dt, x+=v·dt. it.faz = 0u; ic_->entegre->dispatchKaydet(kb, &it, sizeof(it), gDagit, 1, 1); ComputeBoruHatti::ssboBariyeri(kb); // (b) hücreSay sıfırla (GPU fill; CPU senkronu YOK → batch korunur). vkCmdFillBuffer(kb, ic_->hucreSay->tampon(), 0, saySifirBayt, 0u); VkMemoryBarrier fb{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; fb.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; fb.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; vkCmdPipelineBarrier(kb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &fb, 0, nullptr, 0, nullptr); // (c) BUCKET SCATTER: partikül → hücre kovası (atomik yuva). ic_->hashBucket->dispatchKaydet(kb, &it, sizeof(it), gDagit, 1, 1); ComputeBoruHatti::ssboBariyeri(kb); // (d) PBF Jacobi: yoğunluk(λ) → düzeltme(Δp) + sınır. for (u32 iter = 0; iter < pbfIter; ++iter) { ic_->yogunluk->dispatchKaydet(kb, &it, sizeof(it), gDagit, 1, 1); ComputeBoruHatti::ssboBariyeri(kb); ic_->kuvvet->dispatchKaydet(kb, &it, sizeof(it), gDagit, 1, 1); ComputeBoruHatti::ssboBariyeri(kb); } // (e) FİNALİZE: v=(x-x0)/dt + XSPH. it.faz = 1u; ic_->entegre->dispatchKaydet(kb, &it, sizeof(it), gDagit, 1, 1); ComputeBoruHatti::ssboBariyeri(kb); } }); if (gpuHata) break; // Readback (batch sonu konum + hız) → host-ayna (emit + binning güncel kalır). ic_->konum->oku(hKonum.data(), aktifBayt4); ic_->hiz->oku(hHiz.data(), aktifBayt4); t += dt * kBatchAdim; // (6) BINNING (CPU): partikül → voxel hücre doluluk + hız + oksit. std::fill(sayac.begin(), sayac.end(), u16{0}); // Hız toplamı/sayacı da ADIMLIK (CPU yoluyla aynı): aksi halde sim boyunca // birikir → hız alanı yalnız son adımda dolmuş hücreleri yansıtır, akış // zaman içinde yanlış/donuk görünür. std::fill(hizToplam.begin(), hizToplam.end(), 0.0); std::fill(hizAdet.begin(), hizAdet.end(), 0u); for (u32 p = 0; p < aktif; ++p) { const size_t k4 = static_cast(p) * 4; const f64 wx = hKonum[k4 + 0], wy = hKonum[k4 + 1], wz = hKonum[k4 + 2]; const f64 vx = hHiz[k4 + 0], vy = hHiz[k4 + 1], vz = hHiz[k4 + 2]; const f64 vmag = std::sqrt(vx * vx + vy * vy + vz * vz); const f64 mi = (wx / birimOlcek - kok.x) / hMesh; const f64 mj = (wy / birimOlcek - kok.y) / hMesh; const f64 mk = (wz / birimOlcek - kok.z) / hMesh; if (mi < 0 || mj < 0 || mk < 0) continue; const u32 i = static_cast(mi), j = static_cast(mj), kk = static_cast(mk); if (i >= kafes.nx() || j >= kafes.ny() || kk >= kafes.nz()) continue; const u32 idx = kafes.indeks(i, j, kk); if (sayac[idx] < 65535) ++sayac[idx]; hizToplam[idx] += vmag; ++hizAdet[idx]; if (vmag > s.hizMaks[idx]) s.hizMaks[idx] = static_cast(vmag); if (vmag > s.maksHiz) s.maksHiz = vmag; // Oksit/bifilm (Campbell): serbest yüzey + hız > kritik → kalıcı işaret. // Serbest yüzey vekili: kaviteye yeni girmiş yüksek hızlı jet. if (!pOksit[p] && vmag > kritikHiz) pOksit[p] = 1; } // (7) Varış zamanı (ilk eşik aşımı; histerezis). for (u32 c = 0; c < N; ++c) { if (!icMi(c)) continue; const f64 oran = sayac[c] * doluPay; if (!dolu[c] && oran >= ayar.doluEsik) { dolu[c] = 1; if (d.varisZamani[c] < 0.0) d.varisZamani[c] = t; ++dolan; sonDegisimT = t; } else if (dolu[c] && oran < 0.6 * ayar.doluEsik) { dolu[c] = 0; --dolan; sonDegisimT = t; } } // (8) Animasyon karesi. // CPU yoluyla BIREBIR ayni zamanlama kurali: kare anini t'ye YAKLASTIR ve // kacirilan kareleri ATLAMA (hepsini ayni dolulukla doldur). `t >= sonrakiKare` // karsi karsida coklu kareyi anlik gonderip sonrakileri atlardi → animasyon // "donuk/zıplayan" gorunuyordu. Determinizm: kare sayisi ADIM sayisina degil // SIM sure yaslar. while (sonrakiKare <= t && s.kareler.size() < kareSayi) { SphDolumKare kare; kare.sure = sonrakiKare; kare.doluluk.resize(N); for (u32 c = 0; c < N; ++c) kare.doluluk[c] = static_cast(std::min(255.0, sayac[c] * doluPay * 255.0)); // Hucre-bası ort. hız alanı (CPU yolunun ikizi — "akıyor" gorseli icin). kare.hizOrt.resize(N); for (u32 c = 0; c < N; ++c) kare.hizOrt[c] = hizAdet[c] ? static_cast(hizToplam[c] / hizAdet[c]) : 0.0f; kare.ortT = static_cast(ayar.dokumSicaklik); // GPU ısı kuplajı: sonraki dalga // GÖRSEL partikül kaydı (CPU yolunun ikizi): konum/hız zaten batch sonu // readback ile host aynasında — ek indirme yok. Kapalıysa vektör boş. if (ayar.partikulKaydet && aktif > 0) { const u32 adim = std::max(1u, ayar.partikulAltOrnek); kare.partikul.reserve((aktif + adim - 1) / adim); for (u32 p = 0; p < aktif; p += adim) { const size_t k4 = static_cast(p) * 4; SphPartikul sp; sp.konum = {static_cast(hKonum[k4 + 0] / birimOlcek), static_cast(hKonum[k4 + 1] / birimOlcek), static_cast(hKonum[k4 + 2] / birimOlcek)}; const f64 vx = hHiz[k4 + 0], vy = hHiz[k4 + 1], vz = hHiz[k4 + 2]; sp.hiz = static_cast(std::sqrt(vx * vx + vy * vy + vz * vz)); sp.sicaklik = static_cast(ayar.dokumSicaklik); // GPU ısı kuplajı yok kare.partikul.push_back(sp); } } s.kareler.push_back(std::move(kare)); sonrakiKare += kareAralik; } if (ilerleme) { const f64 oran = std::min(1.0, std::max(t / maksSure, static_cast(dolan) / icSayi)); if (!ilerleme(oran, "SPH dolum (GPU)")) { iptal = true; break; } } s.adimSayi += kBatchAdim; if (t > plan.sonPencereS && (dolan == icSayi || t - sonDegisimT > 0.2)) break; } if (gpuHata) { gpuHazir_ = false; sonHata_ = "GPU SPH dispatch başarısız — CPU'ya düşülüyor."; gunluk::uyari("GPU SPH: dispatch başarısız, CPU sphDolumCoz'a devrediliyor"); SphDolumSonuc bos; // hazir=false → çağıran CPU'ya düşer bos.dolum.varisZamani.assign(N, -1.0); return bos; } // ---- Sonuçları haritala (CPU yolla birebir) ----------------------------- for (u32 c = 0; c < N; ++c) if (icMi(c) && !dolu[c] && d.varisZamani[c] >= 0.0) d.varisZamani[c] = -1.0; dolan = 0; for (u32 c = 0; c < N; ++c) if (icMi(c) && d.varisZamani[c] >= 0.0) ++dolan; // Sınır (kabuk) hücrelere varışı komşu iç hücreden yay (zincir sözleşmesi). u32 dolanKabuk = 0; for (u32 k = 0; k < kafes.nz(); ++k) for (u32 j = 0; j < kafes.ny(); ++j) for (u32 i = 0; i < kafes.nx(); ++i) { const u32 idx = kafes.indeks(i, j, k); if (kafes.tip[idx] != HucreTip::Sinir || d.varisZamani[idx] >= 0.0) continue; f64 enErken = -1.0; const i64 nx = kafes.nx(), ny = kafes.ny(), nz = kafes.nz(); const i64 komsu[6][3] = {{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}}; for (const auto& dk : komsu) { const i64 ni = i + dk[0], nj = j + dk[1], nk = k + dk[2]; if (ni < 0 || nj < 0 || nk < 0 || ni >= nx || nj >= ny || nk >= nz) continue; const u32 nidx = kafes.indeks((u32)ni, (u32)nj, (u32)nk); if (icMi(nidx) && d.varisZamani[nidx] >= 0.0 && (enErken < 0 || d.varisZamani[nidx] < enErken)) enErken = d.varisZamani[nidx]; } if (enErken >= 0) { d.varisZamani[idx] = enErken; ++dolanKabuk; } } d.dolanHucre = dolan + dolanKabuk; d.dolanHacim = d.dolanHucre * hucreHacim; f64 sonVaris = 0; d.sira.clear(); d.sira.reserve(dolan); for (u32 c = 0; c < N; ++c) if (d.varisZamani[c] >= 0.0) { d.sira.push_back(c); sonVaris = std::max(sonVaris, d.varisZamani[c]); d.varisT[c] = ayar.dokumSicaklik; // GPU ısı kuplajı sonraki dalga } std::sort(d.sira.begin(), d.sira.end(), [&](u32 a, u32 b) { return d.varisZamani[a] < d.varisZamani[b]; }); d.toplamSure = sonVaris; d.misrun = !iptal && (d.dolanHucre < toplamMetal); d.hazir = !iptal; for (u32 c = 0; c < N; ++c) if (hizAdet[c] > 0) s.hizOrt[c] = static_cast(hizToplam[c] / hizAdet[c]); // Oksit yoğunluğu alanı (oksitli partikülün son konumu → hücre). for (u32 p = 0; p < aktif; ++p) { if (!pOksit[p]) continue; ++s.oksitliPartikul; const size_t k4 = static_cast(p) * 4; const f64 mi = (hKonum[k4 + 0] / birimOlcek - kok.x) / hMesh; const f64 mj = (hKonum[k4 + 1] / birimOlcek - kok.y) / hMesh; const f64 mk = (hKonum[k4 + 2] / birimOlcek - kok.z) / hMesh; if (mi < 0 || mj < 0 || mk < 0) continue; const u32 i = static_cast(mi), j = static_cast(mj), kk = static_cast(mk); if (i >= kafes.nx() || j >= kafes.ny() || kk >= kafes.nz()) continue; s.oksit[kafes.indeks(i, j, kk)] += static_cast(doluPay); } s.partikulSayi = aktif; s.partikulKutleKg = static_cast(aktif) * kutle; s.cepheTMin = ayar.dokumSicaklik; s.cozumSuresiSn = std::chrono::duration(std::chrono::steady_clock::now() - baslangic).count(); s.iptal = iptal; s.hazir = !iptal; return s; } // --------------------------------------------------------------------------- // Serbest fonksiyon (combo bağlama sözleşmesi) // --------------------------------------------------------------------------- SphDolumSonuc sphGpuCoz(VulkanBaglam& vb, const VoxelKafes& kafes, const DokumKurulum& kurulum, const Alasim& alasim, const SphDolumAyar& ayar, IlerlemeCB ilerleme) { SphGpuCozucu cozucu(vb); if (!cozucu.gpuHazir()) { gunluk::uyari("GPU SPH hazır değil ({}) — çağıran CPU sphDolumCoz'a düşmeli", cozucu.sonHata()); SphDolumSonuc s; // hazir=false s.dolum.varisZamani.assign(kafes.hucreSayi(), -1.0); return s; } return cozucu.coz(kafes, kurulum, alasim, ayar, ilerleme); } bool sphGpuVarMi(VulkanBaglam& vb) { if (vb.aygitKayipMi()) return false; SphGpuCozucu test(vb); return test.gpuHazir(); } } // namespace fiko::dokum