Проект:         WebCAE (https://github.com/-)
Библиотека:     Netgen mesh generator (NGSolve)
Лицензия:       LGPL-2.1-only
Апстрим:        https://github.com/NGSolve/netgen.git
Версия:         v6.2.2404
Коммит:         c488fa936ab5817d455a9bfe27d0c17c99933cc5
Автор изменений: Alexey Smerdov <alexeysmerdov@gmail.com>
Даты изменений: 2026-07-18 (ng_mpi.cpp, python_mesh.cpp — порт под Emscripten)
                2026-07-19 (basegeom.hpp, basegeom.cpp — список непромешенных граней)
                2026-07-28 (global.hpp, global.cpp, improve2.hpp, improve3.cpp,
                            smoothing3.cpp — прогресс и кооперативное прерывание)
                2026-08-01 (basegeom.cpp — ранний честный отказ вместо перехода в
                            MeshVolume при непустом ng_last_failed_faces: апстрим не
                            проверял успех MeshSurface перед MeshVolume, из-за чего
                            дырявая поверхностная сетка иногда портила память вместо
                            штатного исключения — docs/2026-08-01-mesh-aborted-findings.md)
                2026-08-11 (improve3.cpp — SwapImproveEdge, ветка nsuround >= 5:
                            выход, если обход кольца тетов вокруг ребра не замкнулся
                            и suroundpts[] остался PointIndex::INVALID. Без этого
                            CalcBad читает mesh.Points()[0] = data[-1] — 40 байт до
                            начала массива точек. Доказано ASan; в соседней ветке
                            nsuround == 3 апстрим ту же ситуацию уже ловит, а здесь
                            проверки нет ни в v6.2.2404, ни в master 81120fc
                            — docs/2026-08-11-netgen-swapimprove-asan-findings.md)

Это ПОЛНЫЙ набор наших изменений библиотеки Netgen в том виде, в котором она
скомпилирована в поставляемый модуль app/public/wasm/netgen_spike_web.wasm.
Изменения распространяются на условиях LGPL-2.1 (раздел 2 LGPL-2.1).

Применение к чистому апстриму:
    git clone https://github.com/NGSolve/netgen.git
    cd netgen && git checkout c488fa936ab5817d455a9bfe27d0c17c99933cc5
    git apply /путь/к/netgen-6.2.2404-webcae-all.patch

Проверка пригодности патча (offline, без сети):
    node scripts/verify-third-party-patches.mjs

Файл netgen-progress-and-cooperative-abort.patch — ПОДМНОЖЕСТВО этого патча
(только изменения от 2026-07-28), оставлен для удобства пересборки; юридически
значимым документом по LGPL-2.1 является данный файл.

diff --git a/libsrc/core/ng_mpi.cpp b/libsrc/core/ng_mpi.cpp
index b6adbe4..14fc63f 100644
--- a/libsrc/core/ng_mpi.cpp
+++ b/libsrc/core/ng_mpi.cpp
@@ -7,9 +7,8 @@
 
 #include "array.hpp"
 #include "ngcore_api.hpp"
-#include "pybind11/pytypes.h"
-
 #ifdef NG_PYTHON
+#include "pybind11/pytypes.h"
 #include "python_ngcore.hpp"
 #endif
 
diff --git a/libsrc/meshing/basegeom.cpp b/libsrc/meshing/basegeom.cpp
index 379185f..2710309 100644
--- a/libsrc/meshing/basegeom.cpp
+++ b/libsrc/meshing/basegeom.cpp
@@ -6,6 +6,9 @@
 
 namespace netgen
 {
+  // ПАТЧ WebCAE: см. basegeom.hpp.
+  DLL_HEADER std::vector<int> ng_last_failed_faces;
+
   struct PointTree
   {
       BoxTree<3> tree;
@@ -841,6 +844,11 @@ namespace netgen
     mesh.ClearFaceDescriptors();
 
     size_t n_failed_faces = 0;
+    // ПАТЧ WebCAE: запоминаем НОМЕРА не сшившихся граней, а не только их число.
+    // Апстрим печатает "SURFACE MESHING ERROR OCCURRED IN N FACES:" и не говорит,
+    // в каких именно — а пользователю нужно знать, что чинить/огрублять.
+    // Список читается снаружи через netgen::GetLastFailedFaces().
+    ng_last_failed_faces.clear();
     Array<int, PointIndex> glob2loc(mesh.GetNP());
     for(auto k : Range(faces))
     {
@@ -963,7 +971,10 @@ namespace netgen
             }
             else
                 if(MeshFace(mesh, mparam, k, glob2loc))
+                {
                     n_failed_faces++;
+                    ng_last_failed_faces.push_back(static_cast<int>(k) + 1); // 1-based, как FaceDescriptor
+                }
         }
     }
 
@@ -971,6 +982,9 @@ namespace netgen
     {
         cout << "WARNING! NOT ALL FACES HAVE BEEN MESHED" << endl;
         cout << "SURFACE MESHING ERROR OCCURRED IN " << n_failed_faces << " FACES:" << endl;
+        cout << "FAILED FACES:";
+        for(auto fnr : ng_last_failed_faces) cout << " " << fnr;
+        cout << endl;
         return;
     }
 
@@ -1330,6 +1344,28 @@ namespace netgen
       {
         multithread.task = "Volume meshing";
 
+        // ПАТЧ WebCAE: MeshSurface() (см. выше) на n_failed_faces>0 печатает
+        // "SURFACE MESHING ERROR OCCURRED IN N FACES" и возвращается с ДЫРЯВОЙ
+        // поверхностной сеткой — часть граней не триангулирована вовсе, а не
+        // просто грубо. Без этой проверки выполнение шло прямо в MeshVolume,
+        // который считает недостающие грани "open elements" и пытается
+        // достроить объём через advancing front с эскалацией giveuptol
+        // (meshfunc.cpp, цикл do{...}while с cntsteps). На реальных телах
+        // корпуса (docs/2026-08-01-mesh-aborted-findings.md) этот цикл ни разу
+        // не восстановился — он либо кидает штатное исключение (MESHING3_*
+        // != OK ниже), либо ПОРТИТ ПАМЯТЬ и падает EXC_BAD_ACCESS/WASM trap
+        // "memory access out of bounds" в никак не связанном месте (замерено
+        // lldb: два прогона одного и того же тела упали в РАЗНЫХ местах —
+        // netgen::operator<<(MeshPoint) meshtype.hpp:391 и
+        // netgen::Element::IsDeleted meshtype.hpp:1046 — классический почерк
+        // порчи кучи, не единичный баг в одной точке). Ранний честный отказ
+        // здесь превращает недетерминированный крэш в тот же "Meshing failed
+        // [failed faces: ...]", который пользователь и так уже видит при
+        // полностью упавшей поверхности — данные не теряются, поведение
+        // становится предсказуемым.
+        if (!ng_last_failed_faces.empty())
+          return 1;
+
         MESHING3_RESULT res = MeshVolume (mparam, *mesh);
 
         if (res != MESHING3_OK) return 1;
diff --git a/libsrc/meshing/basegeom.hpp b/libsrc/meshing/basegeom.hpp
index 8d5a0ee..876cf2b 100644
--- a/libsrc/meshing/basegeom.hpp
+++ b/libsrc/meshing/basegeom.hpp
@@ -17,6 +17,11 @@ struct Tcl_Interp;
 
 namespace netgen
 {
+  // ПАТЧ WebCAE: номера граней (1-based), которые не удалось замешить в последнем
+  // вызове GenerateMesh. Апстрим сообщает только их количество, а продукту нужно
+  // сказать пользователю, ЧТО чинить. Читается сразу после неудачного вызова.
+  DLL_HEADER extern std::vector<int> ng_last_failed_faces;
+
   class Refinement;
 
   struct ShapeProperties
diff --git a/libsrc/meshing/global.cpp b/libsrc/meshing/global.cpp
index bdde304..303d5ce 100644
--- a/libsrc/meshing/global.cpp
+++ b/libsrc/meshing/global.cpp
@@ -52,6 +52,10 @@ namespace netgen
   int silentflag = 0;
   int testmode = 0;
 
+  // ПАТЧ WebCAE: хук на чтение multithread.terminate (см. global.hpp).
+  // По умолчанию нулевой — поведение апстрима не меняется.
+  ng_terminate_hook_t ng_terminate_hook = nullptr;
+
   volatile multithreadt multithread;
 
   string ngdir = ".";
diff --git a/libsrc/meshing/global.hpp b/libsrc/meshing/global.hpp
index 1ab2ad3..9843639 100644
--- a/libsrc/meshing/global.hpp
+++ b/libsrc/meshing/global.hpp
@@ -31,6 +31,42 @@ namespace netgen
 
   DLL_HEADER extern mutex tcl_todo_mutex;
 
+  // ── ПАТЧ WebCAE: прогресс и кооперативный дедлайн мешинга ──────────────────
+  // Netgen проверяет multithread.terminate примерно в 100 точках внутренних
+  // циклов (delaunay, meshing2/3, improve2/3, smoothing2/3, meshfunc, basegeom,
+  // occgenmesh) — это единственные места, которые гарантированно вызываются
+  // часто на ЛЮБОЙ стадии. Наружу же Netgen не отдаёт ни прогресса, ни способа
+  // прерваться: multithread.percent/task просто лежат в памяти, а опросить их
+  // некому — в браузере мешинг идёт ОДНИМ синхронным вызовом внутри воркера,
+  // событийный цикл воркера всё это время не крутится.
+  //
+  // Поэтому поле terminate стало прокси: КАЖДОЕ его чтение дополнительно
+  // дёргает ng_terminate_hook. Хост (spike/netgen/netgen_tetmesh.cpp) вешает
+  // туда троттлящую функцию, которая отдаёт percent/task в JS и, если исчерпан
+  // лимит времени, ставит multithread.terminate = 1 — то есть пользуется тем же
+  // штатным кооперативным остановом Netgen, только по таймеру, а не по кнопке.
+  //
+  // Все существующие места использования (`if (multithread.terminate)`,
+  // `!multithread.terminate`, `multithread.terminate != 1`, `= 0`, `= 1`,
+  // `return multithread.terminate;`) компилируются без правок за счёт
+  // operator int() / operator=.
+  typedef void (*ng_terminate_hook_t)();
+  DLL_HEADER extern ng_terminate_hook_t ng_terminate_hook;
+
+  class NgTerminateFlag
+  {
+    int value;
+  public:
+    NgTerminateFlag() : value(0) { }
+    operator int() const volatile
+    {
+      if (ng_terminate_hook) ng_terminate_hook();
+      return value;
+    }
+    void operator= (int v) volatile { value = v; }
+    void operator= (int v) { value = v; }
+  };
+
   class DLL_HEADER multithreadt
   {
   public:
@@ -38,7 +74,7 @@ namespace netgen
     int testmode;
     int redraw;
     int drawing;
-    int terminate;
+    NgTerminateFlag terminate;   // ПАТЧ WebCAE: было `int terminate;`
     int running;
     double percent;
     const char * task;
diff --git a/libsrc/meshing/improve2.hpp b/libsrc/meshing/improve2.hpp
index 539b005..e913316 100644
--- a/libsrc/meshing/improve2.hpp
+++ b/libsrc/meshing/improve2.hpp
@@ -51,6 +51,10 @@ void BuildEdgeList( const Mesh & mesh, const Table<TINDEX, PointIndex> & element
       ArrayMem<std::tuple<PointIndex,PointIndex>, 100> local_edges;
       for (auto pi : myrange)
       {
+        // ПАТЧ WebCAE: чтение terminate дёргает ng_terminate_hook (global.hpp).
+        // Само значение здесь не используется — прерывать сбор нельзя, нужен
+        // только регулярный признак жизни на длинной молчаливой стадии.
+        (void) (int) multithread.terminate;
         local_edges.SetSize(0);
 
         for(auto ei : elementsonnode[pi])
diff --git a/libsrc/meshing/improve3.cpp b/libsrc/meshing/improve3.cpp
index 1fcb11a..a0c4d1b 100644
--- a/libsrc/meshing/improve3.cpp
+++ b/libsrc/meshing/improve3.cpp
@@ -151,6 +151,10 @@ tuple<double, double, int> MeshOptimize3d :: UpdateBadness()
     int bad_elements_local = 0;
     for (ElementIndex ei : myrange)
     {
+        // ПАТЧ WebCAE: чтение terminate дёргает ng_terminate_hook (global.hpp).
+        // Само значение здесь не используется — прерывать сбор нельзя, нужен
+        // только регулярный признак жизни на длинной молчаливой стадии.
+        (void) (int) multithread.terminate;
       auto & el = mesh[ei];
       if(mp.only3D_domain_nr && mp.only3D_domain_nr != el.GetIndex()) continue;
       if(!el.BadnessValid())
@@ -373,6 +377,11 @@ void MeshOptimize3d :: CombineImprove ()
   {
     for(auto i : myrange)
     {
+      // ПАТЧ WebCAE: чтение terminate дёргает ng_terminate_hook (global.hpp) —
+      // без него эта стадия идёт молча секундами: ни прогресса, ни дедлайна.
+      if (multithread.terminate)
+        break;
+
       auto [p0,p1] = edges[i];
       double d_badness = CombineImproveEdge (elementsonnode, p0, p1, is_point_removed, true);
       if(d_badness<0.0)
@@ -395,6 +404,11 @@ void MeshOptimize3d :: CombineImprove ()
   int cnt = 0;
   for(auto [d_badness, ei] : edges_with_improvement)
   {
+      // ПАТЧ WebCAE: чтение terminate дёргает ng_terminate_hook (global.hpp) —
+      // без него эта стадия идёт молча секундами: ни прогресса, ни дедлайна.
+      if (multithread.terminate)
+        break;
+
       auto [p0,p1] = edges[ei];
       if (CombineImproveEdge (elementsonnode, p0, p1, is_point_removed, false) < 0.0)
         cnt++;
@@ -626,6 +640,11 @@ void MeshOptimize3d :: SplitImprove ()
 
     for(auto i : myrange)
     {
+      // ПАТЧ WebCAE: чтение terminate дёргает ng_terminate_hook (global.hpp) —
+      // без него эта стадия идёт молча секундами: ни прогресса, ни дедлайна.
+      if (multithread.terminate)
+        break;
+
       auto [p0,p1] = edges[i];
       double d_badness = SplitImproveEdge (elementsonnode, locfaces, badmax, p0, p1, ptmp, true);
       if(d_badness<0.0)
@@ -649,6 +668,11 @@ void MeshOptimize3d :: SplitImprove ()
   NgArray<INDEX_3> locfaces;
   for(auto [d_badness, ei] : edges_with_improvement)
   {
+      // ПАТЧ WebCAE: чтение terminate дёргает ng_terminate_hook (global.hpp) —
+      // без него эта стадия идёт молча секундами: ни прогресса, ни дедлайна.
+      if (multithread.terminate)
+        break;
+
       auto [p0,p1] = edges[ei];
       if (SplitImproveEdge (elementsonnode, locfaces, badmax, p0, p1, ptmp, false) < 0.0)
         cnt++;
@@ -1154,6 +1178,26 @@ double MeshOptimize3d :: SwapImproveEdge (
                 }
         }
 
+      // ПАТЧ WebCAE (2026-08-11): обход кольца выше может НЕ ЗАМКНУТЬСЯ — тогда
+      // suroundpts[l] остаётся PointIndex::INVALID (== BASE-1 == 0), и первый же
+      // CalcBad ниже читает mesh.Points()[0] = data[-1], то есть 40 байт (ровно
+      // один MeshPoint) ДО начала массива точек. Доказано ASan на теле 16
+      // сборки Wikispeed: heap-buffer-overflow, READ of size 8, "located 40 bytes
+      // before 40960-byte region", стек SwapImproveEdge → CalcBad (improve3.hpp:62).
+      //
+      // В соседней ветке (nsuround == 3) автор Netgen эту же ситуацию уже ловит —
+      // `if (!pi5.IsValid()) throw NgException("Illegal state observed in SwapImprove")`.
+      // Здесь проверки не было ни в v6.2.2404, ни в апстрим-master (81120fc,
+      // сверено 2026-08-11): дефект апстрима, не наш.
+      //
+      // Выходим, а не бросаем: swap — необязательная оптимизация, ронять из-за неё
+      // мешинг незачем, а бросок поменял бы и поведение натива (сейчас он отдаёт
+      // мягкий отказ). Возврат 0.0 = «улучшения нет», ровно как на строке 724
+      // (`if (mesh.BoundaryEdge (pi1, pi2)) return 0.0;`). Сетка не тронута:
+      // между началом ветки и этим местом mesh не мутируется, d_badness ещё 0.0.
+      for (int k = 0; k < nsuround; k++)
+        if (!suroundpts[k].IsValid())
+          return 0.0;
 
       bad1 = 0;
       for (int k = 0; k < nsuround; k++)
@@ -1396,6 +1440,11 @@ void MeshOptimize3d :: SwapImprove (const NgBitArray * working_elements)
 
   for(auto [d_badness, ei] : edges_with_improvement)
   {
+      // ПАТЧ WebCAE: чтение terminate дёргает ng_terminate_hook (global.hpp) —
+      // без него эта стадия идёт молча секундами: ни прогресса, ни дедлайна.
+      if (multithread.terminate)
+        break;
+
       auto [pi0,pi1] = edges[ei];
       if(SwapImproveEdge (working_elements, elementsonnode, faces, pi0, pi1, false) < 0.0)
           cnt++;
diff --git a/libsrc/meshing/python_mesh.cpp b/libsrc/meshing/python_mesh.cpp
index 3a652e0..1561e5c 100644
--- a/libsrc/meshing/python_mesh.cpp
+++ b/libsrc/meshing/python_mesh.cpp
@@ -1,5 +1,5 @@
-#include "pybind11/pytypes.h"
 #ifdef NG_PYTHON
+#include "pybind11/pytypes.h"
 
 #include <regex>
 
diff --git a/libsrc/meshing/smoothing3.cpp b/libsrc/meshing/smoothing3.cpp
index 3cc10f7..91dae01 100644
--- a/libsrc/meshing/smoothing3.cpp
+++ b/libsrc/meshing/smoothing3.cpp
@@ -1420,6 +1420,16 @@ void Mesh :: ImproveMesh (const MeshingParameters & mp, OPTIMIZEGOAL goal)
 
         for (auto i : myrange)
         {
+          // ПАТЧ WebCAE: единственная частая точка опроса на этой стадии.
+          // Апстрим проверяет terminate раз на цвет (выше по циклу), а сама
+          // BFGS-оптимизация точек идёт секундами без единого признака жизни:
+          // прогресс наружу не шёл и дедлайн не срабатывал. Чтение terminate
+          // дёргает ng_terminate_hook (см. global.hpp); percent по counter —
+          // честная доля обработанных точек, она монотонна по всем цветам.
+          if (multithread.terminate)
+            break;
+          multithread.percent = 100.0 * counter / points.Size();
+
           PointIndex pi = color_table[icolor][i];
           if ( (*this)[pi].Type() == INNERPOINT )
           {
