Files
Sprinter-SDCC/libbgi/include/sprite.h
T
snark13 eb9ca0179d libbgi: авто-анимация спрайтов — кадровая + tween (tests/spranim все PASS)
Реализация §9г по требованиям пользователя:
- sprite_anim(first,last,speed,mode): ANIM_LOOP / ANIM_PINGPONG /
  ANIM_ONCE (one-shot замирает на last), | ANIM_HORIZ для
  горизонтальных лент.  Смена кадра в тике = ±an_step к оси ленты —
  без умножений (осевое смещение first считается в setup циклом).
- sprite_moveto(tx,ty,max_step,interval): Брезенхэм порциями
  ≤max_step вдоль большей оси (меньшая — err-аккумулятором,
  нелинейные шаги Y сами собой), деления нет.
- Одновременность кадровой и tween — независимые поля/биты.
- Статус: sprite_anim_status() — битовое поле SPR_ANIM_ON/DONE +
  SPR_MOVE_ON/DONE; sprite_anim_frame() — текущий индекс кадра;
  sprite_moving().
- Стопы: sprite_anim_stop(frame | -1 = текущий);
  sprite_move_stop(0 = замереть / 1 = прыжок в цель + DONE).
- Тикер _sprite_tick — проход 0 sprite_update через funcptr
  _spr_tick_fn (DCE: без sprite_anim/moveto код не линкуется,
  цена — один if на кадр; +40 Б программе с движком, sprite_t +18 Б).

tests/spranim (MAME dev, все PASS): LOOP/PINGPONG (разворот на границе
без удвоения краёв)/ONCE+DONE; пример (0,0)→(100,50), шаг 5,
интервал 4 → ровно 80 кадров, Y идёт 3/2/3/2; стопы обоих видов.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 16:14:31 +03:00

225 lines
15 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* sprite.h — managed-движок спрайтов (retained-модель) поверх ядра
* gfx_blit/gfx_heal. Дизайн: docs/sprite-api-design.md §9.1.
*
* Мотивация — profile examples/balls vs asm-референса docs/samples/balls
* (2026-07-12): ~2× разрыв не в W3-скобке, а в per-call C-обвязке
* (клип/парс заголовка/save-restore банка/SDCC-фрейм), повторяемой 2×N
* раз за кадр. Движок амортизирует её: sprite_update держит ОДНУ
* W3-скобку и ОДИН банк на весь ПРОХОД, вызывая bracket-free ядра
* (_gfx_heal_full/_gfx_blit_full) в цикле по спрайтам. DI остаётся
* гранулярным (по строке в leaf) — под CBL/IM2-аудио длинный DI
* недопустим.
*
* Retained-модель: sprite_move/frame/show/hide/touch меняют ТОЛЬКО
* состояние (O(1), сколько угодно раз за кадр); всё рисование —
* один раз в sprite_update, двухпроходно «heal ВСЕ → блит ВСЕ» (слияние
* per-sprite выкусывает соседей на перекрытии — проверено). Учёт
* «где спрайт реально нарисован на КАЖДОЙ из двух страниц» (drawn[2])
* ведёт движок, не приложение.
*
* Формат картинки — getimage (uint16 w, uint16 h, пиксели построчно,
* 1 байт/пиксель в 256-режиме); 0xFF = прозрачно (банк 0x5C). Буфер
* обязан лежать ВНЕ W3 (< 0xC000). Размер КАДРА спрайта ≤ 64×64
* (требование движка): кэш w,h — uint8_t, и блит идёт лин-ядром без
* split'а на полосы >256 (спрайт заведомо ≤256 в строке). Для больших
* картинок — gfx_blit/putimage напрямую (там общий путь со split'ом).
* ЛЕНТА-АТЛАС может быть шире 64 (img_w — шаг), но КАДР ≤ 64.
*
* z-order = индекс в массиве (последний рисуется сверху). Массив
* владеет приложением: без malloc, без лимитов; пересортировка z =
* перестановка элементов (drawn-состояние едет со структурой).
*/
#ifndef SPRITE_H
#define SPRITE_H
#include <stdint.h>
/* Публичные флаги sprite_t.flags. */
#define SPR_VISIBLE 0x01u /* спрайт рисуется в sprite_update */
#define SPR_ALWAYS 0x02u /* перерисовывать КАЖДЫЙ кадр (статик, над */
/* которым постоянно ходят активные спрайты) */
/* Внутренние dirty-биты (владеет движок): «страница p требует обновления
* этого спрайта». Приложению не трогать — только через sprite_*. */
#define _SPR_DIRTY0 0x04u
#define _SPR_DIRTY1 0x08u
#define _SPR_DIRTY (_SPR_DIRTY0 | _SPR_DIRTY1)
/* Статус-биты sprite_t.an_flags (чтение — sprite_anim_status()):
* *_ON — анимация активна (тикается в sprite_update); *_DONE — дошла
* до конца сама (one-shot до last / перемещение до цели) или была
* добита sprite_move_stop(at_target=1). DONE сбрасывается новым
* запуском соответствующей анимации. */
#define SPR_ANIM_ON 0x01u /* кадровая анимация идёт */
#define SPR_ANIM_DONE 0x02u /* one-shot дошёл до last */
#define SPR_MOVE_ON 0x04u /* tween-перемещение идёт */
#define SPR_MOVE_DONE 0x08u /* перемещение достигло цели */
#define _SPR_ANIM_BACK 0x10u /* внутр.: маятник идёт назад */
#define _SPR_ANIM_HORZ 0x20u /* внутр.: лента горизонтальная (ось sx) */
#define _SPR_ANIM_PP 0x40u /* внутр.: режим маятника */
#define _SPR_ANIM_ONCE 0x80u /* внутр.: режим one-shot */
/* Режимы sprite_anim(). */
#define ANIM_LOOP 0u /* 0,1,..,last,first,.. (цикл) */
#define ANIM_PINGPONG 1u /* 0,1,..,last,last-1,..,first,1,.. */
#define ANIM_ONCE 2u /* first..last, замереть на last + DONE */
#define ANIM_HORIZ 0x80u /* ИЛИ-флаг: кадры вдоль X (сетка nx>1) */
typedef struct {
const void *img; /* getimage-формат / атлас-лента (вне W3) */
int x, y; /* ЛОГИЧЕСКАЯ позиция левого-верхнего угла кадра */
int sx, sy; /* смещение кадра внутри img (атлас; 0 = целая) */
uint8_t w, h; /* размер кадра (кэш заголовка), 1..255 */
uint8_t flags; /* SPR_* | внутренние dirty */
uint8_t page; /* 0 = img в обычной памяти; иначе физ. страница *
* W0-атласа (img — адрес 0x0100-0x3FFF, движок *
* сам мапит страницу в W0 на время блита) */
/* --- авто-анимация (владеет тикер; настраивать ТОЛЬКО через
* sprite_anim../sprite_moveto.., читать — inline-аксессорами) --- */
uint8_t an_flags; /* статус: SPR_ANIM_.. | SPR_MOVE_.. */
uint8_t an_frame; /* ТЕКУЩИЙ индекс кадра (0-based по ленте) */
uint8_t an_first, an_last; /* границы анимации (индексы) */
uint8_t an_speed, an_timer; /* кадров между сменами / счётчик */
uint8_t an_step; /* шаг оси = fh (верт. лента) или fw (гориз.) */
int an_lo; /* осевое смещение кадра first (для wrap) */
uint8_t mv_speed, mv_timer; /* кадров между шагами / счётчик */
uint8_t mv_step; /* макс. пикселей вдоль большей оси за шаг */
int mv_tx, mv_ty; /* цель */
int mv_dx, mv_dy, mv_err;/* Брезенхэм: dx, -dy, аккумулятор */
int8_t mv_sgnx, mv_sgny; /* знаки шага осей */
/* --- внутреннее (владеет движок) --- */
struct {
int x, y; /* где нарисован на странице p */
uint8_t on; /* нарисован ли на странице p */
} drawn[2];
} sprite_t;
/* Инициализация: w,h из заголовка img, невидим, ничего не нарисовано.
* Для АТЛАСА (кадр — под-прямоугольник) после init выставить s->w/s->h
* в размер кадра и s->sx/s->sy — на нужный кадр (или sprite_frame). */
void sprite_init(sprite_t *s, const void *img);
/* Кадр всей сцены: на ТЕКУЩЕЙ draw-странице (gfx_get_draw_page)
* heal ВСЕХ старых позиций → блит ВСЕХ видимых (двухпроходно, одна
* скобка/банк на проход). Спрайты без изменений с прошлого визита этой
* страницы (и без SPR_ALWAYS) не трогаются. Для дабл-буфера звать после
* gfx_set_draw_page(hidden); для одиночной страницы/курсора — прямо на
* видимой. */
void sprite_update(sprite_t *arr, uint8_t count);
/* Сахар канонического дабл-буфер-кадра: draw-страница = скрытая →
* sprite_update → gfx_wait_vsync → сделать скрытую видимой (tear-free).
* Динамический фон/HUD рисовать банком 0x50 на скрытой странице ДО
* sprite_flip — тогда heal учтёт его через ОЗУ-копию. */
void sprite_flip(sprite_t *arr, uint8_t count);
/* --- O(1) изменения состояния (inline: без call-оверхеда в физике) --- */
/* Переместить: меняет только логическую позицию + грязнит обе страницы. */
inline void sprite_move(sprite_t *s, int x, int y) {
if (s->x != x || s->y != y) { s->x = x; s->y = y; s->flags |= _SPR_DIRTY; }
}
/* Сменить кадр атласа (под-прямоугольник img). */
inline void sprite_frame(sprite_t *s, int sx, int sy) {
if (s->sx != sx || s->sy != sy) { s->sx = sx; s->sy = sy; s->flags |= _SPR_DIRTY; }
}
/* Показать (если был скрыт — грязнит обе страницы). */
inline void sprite_show(sprite_t *s) {
if (!(s->flags & SPR_VISIBLE)) s->flags |= (SPR_VISIBLE | _SPR_DIRTY);
}
/* Скрыть (если был виден — грязнит: sprite_update сотрёт heal'ом). */
inline void sprite_hide(sprite_t *s) {
if (s->flags & SPR_VISIBLE) { s->flags &= ~SPR_VISIBLE; s->flags |= _SPR_DIRTY; }
}
/* Принудительная перерисовка на ОБЕИХ страницах (напр. после того как
* активный спрайт прошёл над статиком — приложение помечает статик). */
inline void sprite_touch(sprite_t *s) { s->flags |= _SPR_DIRTY; }
/* ==== Авто-анимация (§9г дизайна) ====================================
* Тикается САМА в начале каждого sprite_update (раз на кадр), код
* тикера подключается через funcptr — программа без анимаций его не
* линкует. Кадровая анимация и tween-перемещение НЕЗАВИСИМЫ и могут
* идти одновременно. Пока кадровая активна — не трогать sprite_frame
* вручную (тикер ведёт ось сам); границы кадров должны лежать вдоль
* ОДНОЙ оси ленты (вертикальная по умолчанию, ANIM_HORIZ — вдоль X). */
/* Запустить кадровую анимацию: кадры first..last (индексы вдоль оси
* ленты), смена раз в speed кадров (1 = каждый кадр), mode = ANIM_LOOP/
* ANIM_PINGPONG/ANIM_ONCE (| ANIM_HORIZ для горизонтальной ленты).
* Старт с first; сбрасывает SPR_ANIM_DONE. */
void sprite_anim(sprite_t *s, uint8_t first, uint8_t last,
uint8_t speed, uint8_t mode);
/* Остановить кадровую анимацию: frame = индекс кадра, который останется
* на экране, или -1 — замереть на текущем. */
void sprite_anim_stop(sprite_t *s, int8_t frame);
/* Запустить перемещение к (tx,ty): раз в interval кадров — шаг до
* max_step пикселей вдоль БОЛЬШЕЙ оси (меньшая тянется Брезенхэмом,
* без деления). Сбрасывает SPR_MOVE_DONE. Пример: (0,0)→(100,50),
* max_step 5, interval 4 → 20 шагов × 4 кадра = 80 кадров (1.6 с). */
void sprite_moveto(sprite_t *s, int tx, int ty,
uint8_t max_step, uint8_t interval);
/* Остановить перемещение: at_target = 0 — замереть где есть;
* 1 — прыгнуть в цель (и взвести SPR_MOVE_DONE). */
void sprite_move_stop(sprite_t *s, uint8_t at_target);
/* Статус анимаций: битовое поле SPR_ANIM_ON/DONE | SPR_MOVE_ON/DONE. */
inline uint8_t sprite_anim_status(const sprite_t *s) { return s->an_flags; }
/* Текущий индекс кадра ленты (ведёт тикер; валиден и после stop). */
inline uint8_t sprite_anim_frame(const sprite_t *s) { return s->an_frame; }
/* Перемещение ещё идёт? */
inline uint8_t sprite_moving(const sprite_t *s) {
return s->an_flags & SPR_MOVE_ON;
}
/* ==== Атласы в EMM-страницах (W0) ====================================
* Файл .atl (docs/sprite-api-design.md §3.1, вариант II): 0x100-байтовый
* заголовок (магия 'SPA1', count; каталог с 0x68) + getimage-ленты;
* файл-офсет == офсет в странице == адрес в W0. Загрузчик читает файл
* ЦЕЛИКОМ в EMM-страницу через W3 и патчит ISR-стаб (0x38/0x66) —
* страница безопасна в W0 при включённых прерываниях (§9в).
*
* ВАЖНО: указатели лент — адреса 0x0100-0x3FFF, разыменовывать их можно
* ТОЛЬКО при подключенной странице (sprite_update мапит сам; для ручного
* putsprite/gfx_blit_part — обернуть в gfx_w0_map/gfx_w0_unmap).
* Пока страница в W0 — НИКАКИХ ESTEX/BIOS-вызовов. */
typedef struct {
uint8_t page; /* физ. страница (маппить в W0/W3) */
uint8_t blk; /* EMM-блок — для atlas_free */
uint8_t count; /* число лент в атласе */
} atlas_t;
/* Загрузить .atl в свежую EMM-страницу (read в W3 + патч стаба).
* 0 — OK; -1 + errno (файл/память/формат). */
int atlas_load(atlas_t *a, const char *path);
/* Освободить EMM-страницу атласа (все sprite_t с его лентами должны
* быть скрыты/переинициализированы ДО вызова). */
void atlas_free(atlas_t *a);
/* Указатель-лента idx (W0-адрес, см. ВАЖНО выше) — для gfx_blit_part
* и ручных вызовов; кадр (i,j) ленты: sx = i*fw, sy = j*fh. */
const void *atlas_image(const atlas_t *a, uint8_t idx);
/* Инициализировать спрайт лентой idx атласа: img/w/h (размер КАДРА из
* каталога) /page; невидим, кадр (0,0). Аналог sprite_init для W0. */
void atlas_sprite_init(sprite_t *s, const atlas_t *a, uint8_t idx);
/* Ручное подключение страницы атласа в W0 (для putsprite/gfx_blit_part
* вне движка). map ставит и _gfx_w0_cur (ISR-стаб восстановит страницу
* после прерывания), unmap возвращает страницу ядра DSS. */
void gfx_w0_map(uint8_t page);
void gfx_w0_unmap(void);
#endif