Tessera — architecture notes

Внутренние заметки для тех, кто правит движок. Не для README — там "что" и "зачем"; здесь "как устроено" и "где грабли".

Координаты и единицы

Владение

InteractiveApp (demo/life)                      LifeMap : LifeLikeAutomaton : ChunkedTileMap (engine)
└─ DefaultApplication : Application              ├─ ChunkGrid            геометрия
   ├─ Input                                       ├─ ChunkStore           общее состояние, все 3 потока
   ├─ Camera2D                                     ├─ SimulationCoordinator  фазы/generations/commit
   └─ CameraController                             ├─ ChunkMapRenderer     рисует видимые чанки
                                                    └─ ISimulationBackend  ⇒ CpuLifeBackend / CudaLifeBackend

ChunkStoreunordered_map<ChunkCoord, shared_ptr<Chunk>> за shared_mutex, плюс unordered_set<ChunkCoord> активных чанков. setTile/paintBrush пишут туда сразу; simulateActiveChunks() обрабатывает остальное асинхронно.

Потоки

ПотокДелаетВладеет / не трогает
Update (main)onUpdate: ввод, камера, paintBrush/stampPattern (пишут в ChunkStore напрямую, могут создать новый Chunk), simulateActiveChunks() — запускает шаг и сразу возвращает управлениепишет камеру под m_cameraMutex; GL-контекста здесь нет
RendercommitReadyChunks(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 (снимок камеры), плюс фазовый барьер ниже.

Правило: GL только из render-потока. Контекст создаётся в main-потоке на инициализации и явно отвязывается (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не пауза и прошёл simSpeedMssimulateActiveChunks()2commitReadyChunks(camera), если фаза ReadyToCommit
3предыдущее поколение ещё не Idle → no-op3render(camera): видимые чанки, upload+draw
4ImGui, glfwSwapBuffers

Фазовый барьер (SimulationCoordinator)

Idle → Computing → ReadyToCommit → Committing → Idle
ПереходКтоЧто гарантирует
Idle → Computingupdate-потокснимок активных чанков, батчи на TaskScheduler; повторный вызов во время расчёта — no-op
→ ReadyToCommitпоследний воркерtasksRemaining.fetch_sub(1)==1 — всё поколение посчитано
→ Committingrender-потокswap sim/render буферов; recalcLiveCells() для всех закоммиченных чанков, не только видимых — иначе невидимый чанк стирается по устаревшему счётчику живых клеток (баг был, исправлен)
→ Idlerender-потокпустые чанки без входящей жизни — erase; соседи, куда перетекает жизнь — create/activate

Смысл барьера: Computing и Committing не пересекаются во времени, иначе сосед мог бы прочитать чанк, наполовину переключённый на новое поколение — рваная, неравномерная симуляция на границах.

Жизненный цикл чанка

Бэкенд симуляции

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_correctnessstill-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