(int x, int y), мировые координаты, знак не ограничен (поле безгранично в обе стороны по обеим осям).chunkSize × chunkSize клеток (по умолчанию 64). ChunkCoord — координата чанка, x = floorDiv(worldX, chunkSize)./ и % округляют к нулю, что даёт неверный чанк для отрицательных координат (пример: x=-1, chunkSize=64 → -1/64==0, а нужно -1). См. ChunkGrid::floorDiv/floorMod.worldPx = worldCell * tileSize. Перевод обратно — тоже через std::floor, не truncate (см. ChunkedTileMap::worldToTile), по той же причине.ChunkGrid::width()/height() — не граница мира. Это номинальный размер стартовой области, используется только для центрирования камеры при старте и в randomize(). Реальных ограничений на координаты нет.InteractiveApp (demo/life) LifeMap : LifeLikeAutomaton : ChunkedTileMap (engine)
└─ DefaultApplication : Application ├─ ChunkGrid геометрия
├─ Input ├─ ChunkStore общее состояние, все 3 потока
├─ Camera2D ├─ SimulationCoordinator фазы/generations/commit
└─ CameraController ├─ ChunkMapRenderer рисует видимые чанки
└─ ISimulationBackend ⇒ CpuLifeBackend / CudaLifeBackend
ChunkStore — unordered_map<ChunkCoord, shared_ptr<Chunk>> за shared_mutex, плюс unordered_set<ChunkCoord> активных чанков. setTile/paintBrush пишут туда сразу; simulateActiveChunks() обрабатывает остальное асинхронно.
| Поток | Делает | Владеет / не трогает |
|---|---|---|
| Update (main) | onUpdate: ввод, камера, paintBrush/stampPattern (пишут в ChunkStore напрямую, могут создать новый Chunk), simulateActiveChunks() — запускает шаг и сразу возвращает управление | пишет камеру под m_cameraMutex; GL-контекста здесь нет |
| Render | commitReadyChunks(camera) → render(camera) → ImGui → glfwSwapBuffers | единственный поток с активным GL-контекстом (glfwMakeContextCurrent вызывается только тут) |
| Worker pool (TaskScheduler) | simulateChunk() батчами по min(256, active/threads), зовёт ISimulationBackend::simulate() | только simBuffer/renderBuffer под chunkMutex; не трогает GL и камеру |
Локи точечные, не один общий мьютекс: ChunkStore::mutex() (структура карты), Chunk::chunkMutex (данные одного чанка), Application::m_cameraMutex (снимок камеры), плюс фазовый барьер ниже.
glfwMakeContextCurrent(nullptr)) перед стартом render-потока. Любой код, способный создать Chunk — а с ним GL-ресурсы ChunkRenderer (VAO/VBO) — либо должен выполняться на render-потоке, либо откладывать создание этих ресурсов.
До недавнего времени это правило нарушалось: ChunkRenderer создавал VAO/VBO прямо в конструкторе, а рисование кистью/загрузка паттерна на ещё не существующем участке карты создают Chunk из update-потока. Итог — чанк с невалидными GL-объектами, который никогда не рисуется, хотя честно продолжает симулироваться. Исправлено переносом создания GL-ресурсов в ChunkRenderer::ensureGLReady(), вызываемую лениво из render()/updateIndices() — они и раньше звались только с render-потока.
| # | Update-поток | # | Render-поток |
|---|---|---|---|
| 1 | ввод, камера | 1 | снимок камеры под мьютексом |
| 2 | не пауза и прошёл simSpeedMs → simulateActiveChunks() | 2 | commitReadyChunks(camera), если фаза ReadyToCommit |
| 3 | предыдущее поколение ещё не Idle → no-op | 3 | render(camera): видимые чанки, upload+draw |
| 4 | ImGui, glfwSwapBuffers |
Idle → Computing → ReadyToCommit → Committing → Idle
| Переход | Кто | Что гарантирует |
|---|---|---|
| Idle → Computing | update-поток | снимок активных чанков, батчи на TaskScheduler; повторный вызов во время расчёта — no-op |
| → ReadyToCommit | последний воркер | tasksRemaining.fetch_sub(1)==1 — всё поколение посчитано |
| → Committing | render-поток | swap sim/render буферов; recalcLiveCells() для всех закоммиченных чанков, не только видимых — иначе невидимый чанк стирается по устаревшему счётчику живых клеток (баг был, исправлен) |
| → Idle | render-поток | пустые чанки без входящей жизни — erase; соседи, куда перетекает жизнь — create/activate |
Смысл барьера: Computing и Committing не пересекаются во времени, иначе сосед мог бы прочитать чанк, наполовину переключённый на новое поколение — рваная, неравномерная симуляция на границах.
getOrCreateChunk(): клик/кисть, загрузка паттерна, активация соседа. Заранее ничего не создаётся.collectBorderNeighbors() сканирует край renderBuffer после шага; живая клетка на краю (включая углы, для диагональных соседей) добавляет координату соседнего чанка в активацию на следующий шаг. Без этого глайдер обрезается на границе чанка.liveCells == 0 после коммита → чанк в emptyCoords → erase, если он не входит в набор соседей, куда как раз перетекает жизнь (иначе можно стереть чанк за миг до того, как в него что-то родится).LifeLikeAutomaton зависит от ISimulationBackend, не от конкретной реализации; какой бэкенд создать — решает MakeSimulationBackend() (CUDA, если собрано с FE_USE_CUDA и GPU доступен на машине, иначе CPU — никогда не возвращает nullptr).
| Метод | Контракт |
|---|---|
simulate(ext, extW, out, S, rule) | ext — окрестность чанка (S+2)×(S+2), border=1, row-major; out — новое состояние S×S. Обязан быть потокобезопасным: вызывается параллельно из разных воркеров для разных чанков. |
simulateDirect(..., glVBO) | CUDA-GL interop: D2D-копия сразу в VBO чанка, минуя PCI-E round-trip через CPU. На Windows WDDM недоступен (GL-контекст в другом потоке) — тихий откат на обычный путь. |
Правило — таблица подстановки LifeRule::table[centerAlive][aliveNeighbors] (данные, не код — это единственный способ отдать правило в CUDA-ядро без виртуальных вызовов). Пример, MakeConwayRule() в engine/simulation/LifeRule.h:
r.table[0][3] = 255; // мёртвая клетка рождается при 3 соседях r.table[1][2] = 255; // живая клетка выживает при 2 или 3
Новое правило — новая функция MakeXxxRule() той же формы, передать в конструктор LifeLikeAutomaton. Backend'ов трогать не нужно — оба читают одну и ту же таблицу.
Реализовать ISimulationBackend::simulate() (обязательно) и опционально simulateDirect()/supportsGLInterop() для GPU-interop. Подключить в MakeSimulationBackend(). Обязательное условие — потокобезопасность simulate(), он зовётся параллельно из разных воркеров.
cmake --preset x64-release cmake --build out/build/x64-release
Test_correctness | still-life, blinker, glider, RLE-парсер, побайтовое сравнение CPU/CUDA после 100 шагов |
Test_propagation | глайдер у границы чанка — должен выйти с другой стороны целым и в правильной позиции; требует OpenGL-контекст, пропускается на CI без GPU |
Test_capture | детерминированный GIF-дамп, используется как регрессионный отпечаток |
Test_benchmark | пропускная способность CPU vs CUDA, аргументы — размер чанка и число итераций |
| Главный цикл, GL-контекст | engine/core/Application.{h,cpp}, DefaultApplication.h |
| Хранилище чанков | engine/chunk/ChunkStore.{h,cpp}, Chunk.{h,cpp} |
| Геометрия мира | engine/chunk/ChunkGrid.h, ChunkCoord.h, AABB.h |
| Оркестрация шага | engine/simulation/SimulationCoordinator.{h,cpp} |
| Бэкенды | engine/simulation/{ISimulationBackend,CpuLifeBackend,CudaLifeBackend,SimulationBackendFactory}.* |
| Правило автомата | engine/simulation/LifeRule.h, engine/automaton/LifeLikeAutomaton.{h,cpp} |
| Рендер чанков | engine/chunk/ChunkRenderer.{h,cpp}, ChunkMapRenderer.{h,cpp} |
| Пул потоков | engine/core/TaskScheduler.{h,cpp} |
| RLE-паттерны | engine/utils/RleLoader.h, demo/life/patterns/*.rle |
| Демки | demo/life/{full,minimal,capture_ui}/main.cpp |
| Тесты | tests/{correctness,propagation,capture,benchmark}/main.cpp |