/* * 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 /* Публичные флаги 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 /* перемещение достигло цели */ /* Внутренние биты an_flags (владеет движок) — три РАЗНЫЕ категории: * режим анимации (зеркало параметра mode), свойство ЛЕНТЫ (ориентация * данных в атласе) и runtime-состояние (меняется само в процессе). */ #define _SPR_PP_BACK 0x10u /* runtime: маятник сейчас идёт назад */ #define _SPR_STRIP_HORZ 0x20u /* лента: кадры вдоль X (двигается 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) */ /* ВНИМАНИЕ: порядок полей ЗАШИТ в asm-ядро тикера (офсеты полей в * common/_sprite_tick.c, tick_move) — при изменении порядка/типов * пересчитать офсеты там (offsetof-пробником, рецепт в его шапке). */ typedef struct { const void *img; /* getimage-формат / атлас-лента (вне W3) */ /* --- кэш адреса кадра (владеет движок; менять ТОЛЬКО через * sprite_frame) --- * src — ГОТОВЫЙ адрес первого пикселя ТЕКУЩЕГО кадра (img + 4 + * sx + sy*stride). Блит берёт его как есть: ни чтения заголовка, * ни умножения в кадровом цикле. Тикер анимации двигает src * инкрементально (±an_step байт); sx/sy при этом НЕ обновляются — * они «последняя позиция, установленная sprite_frame», и снова * становятся истиной при следующем sprite_frame/sprite_anim * (те ставят ось абсолютно, а не относительно). */ const uint8_t *src; /* адрес пикселей текущего кадра */ uint16_t stride; /* полная ширина img в байтах (шаг строки) */ int x, y; /* ЛОГИЧЕСКАЯ позиция левого-верхнего угла кадра */ int sx, sy; /* смещение кадра, заданное sprite_frame (см. src)*/ 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; /* кадров между сменами / счётчик */ uint16_t an_step; /* шаг кадра в БАЙТАХ src: fw (гориз. лента) * * или fh*stride (верт.) — считает sprite_anim */ const uint8_t *an_src0; /* src кадра first (для wrap цикла) */ uint8_t mv_speed, mv_timer; /* кадров между шагами / счётчик */ uint8_t mv_step; /* макс. пикселей вдоль большей оси за шаг */ int mv_tx, mv_ty; /* цель (для sprite_move_stop) */ /* DDA по главной оси (беззнаковый — на Z80 16-бит беззнаковые * сравнения в разы дешевле знаковых Брезенхэма, 2026-07-14): * главная ось шагает каждую итерацию, минорная подтягивается * аккумулятором; «приехали» = mv_rem == 0 (сравнений координат нет). */ uint16_t mv_rem; /* осталось шагов главной оси до цели */ uint16_t mv_acc; /* аккумулятор минорной оси (старт dmaj/2) */ uint16_t mv_dmaj, mv_dmin; /* |дельты| главной / минорной осей */ uint8_t mv_ymaj; /* 1 = главная ось Y (иначе X) */ int8_t mv_sgnmaj, mv_sgnmin;/* знаки шага главной/минорной осей */ /* --- внутреннее (владеет движок) --- */ struct { int x, y; /* где нарисован на странице p */ uint8_t on; /* нарисован ли на странице p */ } drawn[2]; /* Слой для Y-сортировки (см. gfx_sprite_ysort): при включённой * сортировке спрайты упорядочиваются по {layer, y} — слой старше: * слой 1 ВСЕГДА поверх слоя 0 (ходячие/летающие/курсор...), внутри * слоя — painter по y. 0 после sprite_init; без сортировки не * используется (порядок = индекс массива). Поле В КОНЦЕ структуры: * офсеты полей выше зашиты в asm тикера/сортировщика. */ uint8_t layer; } sprite_t; /* Инициализация: w,h из заголовка img, кадр (0,0), невидим, ничего не * нарисовано. Для АТЛАСА (кадр — под-прямоугольник) после init * выставить s->w/s->h в размер кадра и перейти на нужный кадр ТОЛЬКО * через sprite_frame (он ведёт кэш src; голые s->sx/s->sy кэш не * обновят). */ void sprite_init(sprite_t *s, const void *img); /* Кадр всей сцены: на ТЕКУЩЕЙ draw-странице (gfx_get_draw_page) * heal ВСЕХ старых позиций → блит ВСЕХ видимых (двухпроходно, одна * скобка/банк на проход). Спрайты без изменений с прошлого визита этой * страницы (и без SPR_ALWAYS) не трогаются. Для дабл-буфера звать после * gfx_set_draw_page(hidden); для одиночной страницы/курсора — прямо на * видимой. * * ВСЯ СЦЕНА СТРАНИЦЫ — ОДИН ВЫЗОВ! Два sprite_update на одну страницу * (например «сначала ходячие, потом летающие») ЛОМАЮТ перекрытия МЕЖДУ * группами: heal второй группы берёт из ОЗУ-копии чистый ФОН (спрайты * в копию не пишутся — банк 0x5C) и стирает нарисованных первой * группой; двухпроходная дисциплина «heal все → блит все» покрывает * перекрытия только ВНУТРИ одного вызова. Логические группы держать * диапазонами индексов одного массива (z-порядок групп = порядок * диапазонов; слоение при Y-сортировке — полем layer). */ 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); /* Режимы gfx_sprite_ysort (битовая маска компонентов ключа). */ #define YSORT_OFF 0u /* сортировки нет: z-order = индекс массива */ #define YSORT_Y 0x01u /* painter по y (нижние поверх верхних) */ #define YSORT_LAYER 0x02u /* слои: layer 1 поверх layer 0 */ /* Y-сортировка блит-прохода (painter's algorithm): mode — какие * компоненты ключа участвуют в порядке отрисовки: * YSORT_Y | YSORT_LAYER — полный порядок {layer, y}: сначала слои * (спрайт слоя 1 ВСЕГДА поверх слоя 0 — ходячие/летающие/ * курсор...), внутри слоя — по y (нижние по экрану поверх * верхних, «ближе к зрителю»); * YSORT_Y — только painter по y, слои игнорируются; * YSORT_LAYER — только слои; внутри слоя порядок стабилен * (наследуется от прошлого кадра / порядка массива); * YSORT_OFF (0, дефолт) — z-order = индекс массива. * Выключенный компонент маскируется в ключе (2 AND на спрайт, ~1К на * 15 спрайтов) — сортировщик не замедляется ветвлениями. * * ПЕРСИСТЕНТНОСТЬ: таблица порядка живёт МЕЖДУ кадрами — каждый кадр * ключи обновляются и порядок пересортировывается вставками; спрайты * двигаются на пиксели за кадр, порядок почти актуален → почти * линейно. Цена (замер rpgwalk-15, 2026-07-14): ~28К тактов на 15 * спрайтов (~2К/спрайт, ~6% бюджета кадра; построение с нуля каждый * кадр стоило 44К). Массив приложения при этом НЕ трогается (sp[2] * всегда персонаж 2) — сортируются внутренние записи-указатели. При * смене arr/count (другая сцена) таблица перестраивается сама; первый * кадр после включения/смены — полная сортировка (разово дороже). * * Сортировка СТАБИЛЬНА относительно ПРОШЛОГО КАДРА: спрайты с равным * ключом сохраняют взаимный порядок прошлого кадра (не мерцают), а не * порядок массива. Лимит: до 32 спрайтов на вызов sprite_update; * больше — кадр рисуется без сортировки (безопасный фолбэк). * Выключено = 0 тактов И 0 байт: код сортировщика и таблица не * линкуются (funcptr-DCE). heal-проход порядка не требует и остаётся * линейным. * * Замечания: (1) перекрывающиеся спрайты обязаны быть dirty ОБА, чтобы * смена их взаимного порядка отрисовалась — движущиеся грязнятся сами, * статики помечать sprite_touch (правило то же, что и без сортировки); * (2) сортировка перемешивает порядок W0-атласов — смен страницы W0 * может стать больше (один дешёвый OUT на смену). */ void gfx_sprite_ysort(uint8_t on); /* --- 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: пересчитывает * кэш src (одно 16-бит умножение sy*stride) — при АКТИВНОЙ кадровой * анимации не звать (тикер ведёт src сам). */ void sprite_frame(sprite_t *s, int sx, int sy); /* Показать (если был скрыт — грязнит обе страницы). */ 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); typedef struct { int16_t x, y; uint16_t w, h; } gfx_rect_t; /* Скролл региона area из НЕактивной страницы в активную со сдвигом на * dx/dy (0 = чистая копия/heal, >0 = в сторону увеличения координат, * <0 = уменьшения). Открывшуюся полосу |d| НЕ заполняют — возвращают * в *dirty (может быть NULL). Банк 0x50: копия = скролл + heal цели. */ void gfx_scroll_h(const gfx_rect_t *area, int16_t dx, gfx_rect_t *dirty); void gfx_scroll_v(const gfx_rect_t *area, int16_t dy, gfx_rect_t *dirty); #endif