/* * sprite_update — отрисовать кадр сцены на ТЕКУЩЕЙ draw-странице p * (gfx_get_draw_page). Ядро batch-пасса движка v2 (см. * docs/sprite-api-design.md §9.1): вся работа кадра — здесь, двухпроходно * «heal ВСЕ → блит ВСЕ», под ОДНОЙ W3-скобкой и ОДНИМ банком на проход. * * Дисциплина двух проходов ОБЯЗАТЕЛЬНА: слияние heal+блит per-sprite не * работает на перекрытиях (heal соседа выкусывает только что * нарисованный спрайт — проверено на examples/balls 2026-07-11). * * Что перерисовывается: спрайт, у которого страница p ГРЯЗНАЯ (dbit — * взведён sprite_move/frame/show/hide/touch) ИЛИ флаг SPR_ALWAYS. * Неизменившиеся с прошлого визита этой страницы (и без SPR_ALWAYS) не * трогаются — суть retained-модели. * * Проход 1 (банк 0x50): heal старой позиции drawn[p] у тех, кто нарисован * (drawn[p].on) и требует обновления → снять drawn[p].on. * Проход 2 (банк 0x5C): блит видимых, требующих обновления, в (x,y) кадром * (sx,sy) → записать drawn[p] = (x,y), on = 1; снять dbit (страница p * актуальна для этого спрайта). z-order = порядок массива. * * DI живёт внутри leaf'ов (по строке) — скобка _bgi_begin/_bgi_end * прерывания НЕ трогает (только мапит W3), поэтому длина прохода не * влияет на аудио-ISR. w,h кадра ≤ 255 (см. sprite.h). */ #include #include "../_bgi.h" #include void sprite_update(sprite_t *arr, uint8_t count) { uint8_t p = gfx_get_draw_page(); /* 0 / 1 */ uint8_t dbit = p ? _SPR_DIRTY1 : _SPR_DIRTY0; uint8_t saved = gfx_get_bank(); uint8_t i; const uint8_t *rec; /* курсор сорт-таблицы (NULL = без) */ sprite_t *s; /* Проход 0: тик авто-анимаций (§9г) — ДО heal/блита (двигает x/y и * ось кадра, взводя dirty). Указатель ставят sprite_anim/ * sprite_moveto; без них — один if на кадр, код тикера не линкуется. */ if (_spr_tick_fn) (*_spr_tick_fn)(arr, count); /* Проход 1: heal ВСЕХ старых позиций, требующих обновления. */ gfx_set_bank(GFX_BANK_NORMAL); _bgi_begin(); for (i = 0, s = arr; i < count; i++, s++) { if (s->drawn[p].on && (s->flags & (dbit | SPR_ALWAYS))) { (*_gfx_heal_fn)(s->drawn[p].x, s->drawn[p].y, s->w, s->h); s->drawn[p].on = 0; } } _bgi_end(); /* Y-сортировка блит-прохода (gfx_sprite_ysort): asm-сортировка * строит таблицу записей {key,ptr} по ВОЗРАСТАНИЮ y (нижние * рисуются последними = поверх) и возвращает её адрес; rec — курсор * по полю ptr записей (+1, шаг 3), NULL = сортировки нет. * Подключена указателем (funcptr-DCE). Сортируем ПОСЛЕ тика (y уже * сдвинуты). count больше таблицы — фолбэк на порядок массива. */ rec = 0; if (_spr_ysort_fn && count <= _GFX_YSORT_MAX) rec = (*_spr_ysort_fn)(arr, count) + 2; /* поле ptr записи */ /* Проход 2: блит ВСЕХ видимых, требующих обновления. Спрайты из * W0-атласов (s->page): подключить страницу перед блитом — один OUT * на СМЕНУ страницы (спрайты одного атласа подряд = один OUT; при * Y-сортировке смен может быть больше); порядок «_gfx_w0_cur, * затем OUT» закрывает гонку с ISR-стабом (он восстановит уже * новую). heal-проходу страница и порядок не нужны. */ gfx_set_bank(GFX_BANK_SPRITE); _bgi_begin(); { uint8_t mapped = 0; /* 0 = W0 не тронута (там ядро DSS) */ for (i = 0; i < count; i++) { if (rec) { s = *(sprite_t * const *)rec; /* Y-порядок */ rec += 4; } else s = arr + i; /* z = индекс массива */ if (s->flags & (dbit | SPR_ALWAYS)) { if (s->flags & SPR_VISIBLE) { if (s->page != mapped && s->page) { _gfx_w0_cur = s->page; _io_page_w0 = s->page; mapped = s->page; } /* src/stride — готовый кэш (sprite_frame/тикер), * ядро не считает адрес кадра вообще. */ (*_gfx_blit_fn)(s->x, s->y, s->src, s->stride, s->w, s->h); s->drawn[p].x = s->x; s->drawn[p].y = s->y; s->drawn[p].on = 1; } s->flags &= ~dbit; /* страница p обновлена для спрайта */ } } if (mapped) _io_page_w0 = _gfx_w0_dss; /* вернуть ядро DSS */ } _bgi_end(); gfx_set_bank(saved); }