Время и FPS
Три маленьких инструмента и один сырой источник времени. Ничего не выделяют, все функции встроены в место вызова; на ESP32 время берётся прямым чтением системного таймера, без прослоек. FPS и длительность кадра движок считает сам — игре обычно достаточно читать готовое с рендерера (r.getFPS(), r.getFrameTime()).
Сырое время — getSystemMicros()
uint64_t us = getSystemMicros(); // микросекунды от включения
Монотонные микросекунды, никогда не идут назад. Фундамент для всего остального: замеры, кулдауны, метки времени в логе.
Секундомер — Stopwatch
Замерить, сколько длится что-то: участок кода, загрузка уровня, время до респауна.
| Метод | Описание |
|---|---|
void reset() |
Отметить старт прямо сейчас (конструктор делает это сам) |
uint64_t micros() |
Микросекунды с отметки |
float seconds() |
То же в секундах float |
Stopwatch load;
// ... грузим уровень ...
printf("Уровень загрузился за %.2f с\n", load.seconds());
dt игрового цикла — FrameClock
Самая частая рукопись в игровых циклах — «сколько прошло с прошлого кадра» — уже встроена: один tick() в начале кадра, и dt готов.
| Метод | Описание |
|---|---|
float tick() |
Вызвать раз в кадр: вернёт секунды с прошлого tick(). Пауза/лаг обрезаются до 0.1 с — физика и анимация не дёргаются гигантским шагом |
float dt() |
Значение последнего tick(), когда в кадре оно нужно ещё раз |
void reset() |
Забыть прошлое: следующий tick() вернёт 0 |
FrameClock clock;
while (true)
{
const float dt = clock.tick(); // вот и всё — вместо 8 строк вручную
player.update(dt);
cam.updateAnim(dt);
r.render();
}
Первый tick() возвращает 0, потому что до него ещё не прошло ни одного кадра.
Счётчик кадров — PerfCounter
FPS и длительность кадра. Движок тикает им сам (у рендерера он внутри), поэтому обычно вам сюда не нужно — читайте готовое: r.getFPS() (мгновенный), r.getAverageFPS() (сглаженный), r.getFrameTime() (длительность последнего кадра, микросекунды). Нужен замер своего куска — заведите свой счётчик: beginFrame() в начале, endFrame() в конце, дальше те же три чтения.
| Метод | Описание |
|---|---|
void beginFrame() / void endFrame() |
Открыть и закрыть замер кадра |
float getFPS() |
Мгновенный FPS: 1 / длительность последнего кадра |
float getAverageFPS() |
Сглаженный FPS: точное среднее за каждую секунду, сглаженное между секундами. Реагирует за пару секунд, не прыгает от покадрового дрожания |
uint32_t getFrameTime() |
Длительность последнего замера в микросекундах |
Память
Вся память движка проходит через Mem: два вызова на выделение/освобождение и четыре чтения состояния кучи. Ничего лишнего — на ESP32-S3 вызов падает прямо в аллокатор IDF, без прослоек и виртуальных вызовов, так что цена вызова равна цене сырого heap_caps-вызова.
Когда что использовать: игровые объекты и всё, что само владеет собой, создавайте через обычный new/delete (или контейнеры) — они уходят в стандартную кучу. Mem::alloc — под «сырые» блоки данных рендера: вершины, лица мешей, буферы кадра, свои таблицы. Он даёт выравнивание 16 байт по умолчанию (дружелюбно к SIMD-проходам и кэшу) и кладёт блок в быструю внутреннюю SRAM, только если там есть место.
Выделение и освобождение
| Метод | Описание |
|---|---|
void *Mem::alloc(bytes, align = 16) |
Блок в bytes байт с выравниванием align (степень двойки, минимум 4). nullptr — только если память кончилась |
void Mem::free(ptr) |
Освободить блок из alloc. nullptr разрешён и игнорируется — проверять перед вызовом не нужно |
Выравнивание по умолчанию — 16 байт: вершины, матрицы и SIMD-проходы движка рассчитаны именно на него. Меняйте только осознанно (например, 4 для маленьких таблиц индексов).
Диагностика кучи
| Метод | Описание |
|---|---|
size_t Mem::freeHeap() |
Всего свободной RAM, байт |
size_t Mem::freeInternal() |
Свободная быстрая внутренняя SRAM — та, на которой работает рендер. Главные числа для решений «влезет ли ещё один меш» |
size_t Mem::largestBlock() |
Самый большой непрерывный блок, который ещё можно выделить разом. Если он много меньше freeHeap() — куча фрагментирована |
size_t Mem::minFreeHeap() |
Минимум freeHeap() за всё время работы — реальный запас до out-of-memory |
Пара-другая таких чтений в кадре ничего не стоит; HUD-строки вида «heap XK / block XK» собираются ровно из них (см. Examples/benchmark.cpp).
Пример
// Свой меш: вершины с выравниванием по умолчанию, индексы - без запаса
Vertex *verts = static_cast<Vertex *>(Mem::alloc(vertCount * sizeof(Vertex)));
Face16 *faces = static_cast<Face16 *>(Mem::alloc(faceCount * sizeof(Face16), 4));
if (verts && faces)
{
// ... заполнить и отдать мешу ...
}
Mem::free(verts);
Mem::free(faces);
// перед загрузкой уровня: хватит ли места
if (Mem::largestBlock() < neededBytes)
std::printf("Не влезет: нужно %u, крупнейший блок %u\n",
(unsigned)neededBytes, (unsigned)Mem::largestBlock());
Конфигурация сборки
Параметры компиляции движка: размеры экрана, распиновка дисплея не здесь (она в вызове begin3D), а вот геометрия кадра и опциональные подсистемы — тут. Каждый ключ можно задать через idf.py menuconfig/sdkconfig (префикс CONFIG_) или -D при сборке; значения по умолчанию — для стандартной платы 480×320.
| Ключ | По умолчанию | Что задаёт |
|---|---|---|
PIP3D_SCREEN_WIDTH / PIP3D_SCREEN_HEIGHT |
480 / 320 | Размер кадра в пикселях |
PIP3D_SCREEN_BAND_COUNT |
8 | Число горизонтальных полос рендера (кадр считается полосами) |
PIP3D_DISPLAY_ROTATION / PIP3D_DISPLAY_ORDER / PIP3D_DISPLAY_INVERT / PIP3D_DISPLAY_SWAP |
1 / 1 / false / false | Ориентация и quirks конкретной матрицы |
PIP3D_SIMD |
0 (1 из sdkconfig) | SIMD-путь заливки/смешивания |
PIP3D_ENABLE_LOGGING / PIP3D_LOG_DEFAULT_LEVEL |
0 / 1 | Serial-лог движка и уровень по умолчанию |
PIP3D_ENABLE_GIZMOS |
0 | Отладочные гизмо (Debug::Gizmos) |
PIP3D_ENABLE_DRAW_TELEMETRY |
0 | Счётчики прохода отрисовки (Telemetry) |
PIP3D_2D_BACKFACE |
1 | Отсечение задних граней в 2D-хелперах |
PIP3D_DEBUG_BILLBOARD / PIP3D_DEBUG_MIPMAP |
0 | Отладочные режимы билбордов и мипмапов |
Уровень детализации примитивов, полосы и прочие runtime-настройки — не здесь, а в конструкторах соответствующих классов.