Время, память и конфигурация

Сборочные параметры, измерение времени и кадровой частоты, а также работа с памятью устройства.

Время и 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-настройки — не здесь, а в конструкторах соответствующих классов.