diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv index 152e1bf..12f5046 100644 --- a/docs/size_baseline.tsv +++ b/docs/size_baseline.tsv @@ -2,13 +2,14 @@ accfill 3772 argv 3431 assrtest 3847 +atlas 9154 attrprob 4087 -balls 6156 +balls 6208 banked 1056 bankedbg 1067 banklocl 4683 banktest 3767 -bgi_img 7060 +bgi_img 7057 bgitest 3737 bios_text 4461 blitperf 5860 @@ -43,7 +44,7 @@ rt_test 4892 seek 4188 simple 955 solidt 11531 -spriteclip 4187 +spriteclip 4119 sprites 6791 stattest 7549 stdlib 6643 diff --git a/docs/sprite-api-design.md b/docs/sprite-api-design.md index 64509fe..87c9ec4 100644 --- a/docs/sprite-api-design.md +++ b/docs/sprite-api-design.md @@ -700,6 +700,69 @@ Probe-тест tests/w0page (MAME dev, 2026-07-13, ВСЕ PASS): W1, но только tiny (в small/big/huge в W1 код) — отвергнута в пользу универсального W0. +## 9г. Авто-анимация спрайтов (ПРЕДЛОЖЕНИЕ 2026-07-13, обсуждается) + +Идея пользователя: sprite_update (вызывается раз на кадр) сам тикает +анимации — приложение только описывает их декларативно. + +### Кадровая анимация (смена изображений) + +«Крути кадры от M до P с интервалом N кадров»; два режима: линейный +(0/1/2/3/0/1/2/3) и маятник (0/1/2/3/2/1/0/...). + +Ключ к дешёвому тику на Z80: индекс кадра НЕ хранить — переход между +соседними кадрами ленты это ±step к sx или sy (step = fw или fh), +БЕЗ умножения/деления. Состояние: axis-смещение текущего кадра уже +живёт в s->sx/s->sy; добавляются границы и шаг: + + uint8_t an_speed; /* кадров между сменами; 0 = анимации нет */ + uint8_t an_timer; /* счётчик до следующей смены */ + uint8_t an_step; /* fw или fh (ось ленты) */ + int an_lo,an_hi; /* smin..smax по оси (M и P умножены заранее* + * в sprite_anim() — один раз, не в тике) */ + флаги: ANIM_PINGPONG, ANIM_DIR (текущее направление), ANIM_ONESHOT? + +API: sprite_anim(s, first, last, speed, mode) / sprite_anim_stop(s). +Тик (в sprite_update, до проходов): --an_timer; при 0: sy ± an_step, +на границе — wrap (линейный: sy=an_lo) или разворот (маятник: +инверсия DIR), s->flags |= _SPR_DIRTY. Стоимость: декремент + редкая +ветка — копейки. + +### Анимированное перемещение (tween) + +«Плыви к (tx,ty), максимум max_step пикселей за срабатывание, раз в +interval кадров». Пример: (0,0)→(100,50), шаг 5, интервал 4 → 20 +срабатываний × 4 кадра = 80 кадров (1.6 с); Y идёт нелинейно 2/3/2/3. + +Реализация — инкрементальный Брезенхэм (как _bgi_lineseg), но +«порциями» по ≤max_step вдоль БОЛЬШЕЙ оси за срабатывание: ошибка- +аккумулятор тянет меньшую ось, деления нет. Состояние ~10 байт: +tx,ty, err, adx,ady (абс. дельты), знаки, mv_speed/mv_timer. +Завершение: x==tx && y==ty → снять SPR_MOVING (опрос sprite_moving(s); +callback НЕ делаем — опрос проще и дешевле). + +### Общее / критика / расширения + +- Тик — отдельный проход в НАЧАЛЕ sprite_update (до heal: dirty должен + взводиться раньше проходов). ВАЖНО: тик должен идти раз на КАДР — + sprite_update и так зовётся на кадр (дабл-буфер: попеременно на + страницу); при просадке fps анимации замедляются вместе с кадром + (стандартное поведение, принимаем). +- DCE: тикер — в отдельном модуле, sprite_update зовёт его через + указатель _spr_tick_fn (дефолт NULL → один if на кадр); указатель + ставит первый вызов sprite_anim()/sprite_moveto(). Программа без + анимации не тянет код тикера (паттерн funcptr как у clip/noclip). +- Цена памяти: sprite_t вырастает на ~16-18 байт (кадровая ~10 + + tween ~10 с перекрытием полей?). Вариант «отдельная anim-структура + по указателю (NULL = нет)» экономит память статиков, но добавляет + indirection в тик и второй массив приложению. ПРЕДЛОЖЕНИЕ: поля в + sprite_t (спрайтов десятки — до 64×~34 Б ≈ 2 КБ, W2 переживёт), + но решение за пользователем. +- Расширения (потом): one-shot кадровая анимация (проиграть раз и + замереть/скрыться); дробная скорость 8.8 fixed-point (равномерное + медленное движение «1 пиксель в 1.5 кадра»); цепочки целей + (waypoints) — приложение ставит новую цель по sprite_moving()==0. + ## 10. Риски / что проверить артефактом (Фаза 0) Статус 2026-07-11: пп. 1–3 закрыты для MAME 0.283 (tests/gfxbanks, diff --git a/libbgi/_bgi.h b/libbgi/_bgi.h index 2dec31a..430a550 100644 --- a/libbgi/_bgi.h +++ b/libbgi/_bgi.h @@ -134,13 +134,27 @@ void _gfx_heal_sprite_noclip(int x, int y, uint8_t w, uint8_t h); * а sprite_update/putsprite/movesprite зовут через указатель — один * call-site, без if в цикле → тесный codegen (полный выигрыш noclip, * см. docs/sprite-api-design.md). Дефолт (clip-ядра) — статическая - * инициализация в common/_gfx_sprite_fns.c. */ + * инициализация; указатели в РАЗДЕЛЬНЫХ data-модулях (_gfx_blit_fn.c, + * _gfx_heal_fn.c): putsprite-only программа не тянет heal-ядро. */ typedef void (*_gfx_blit_fp)(int, int, const void *, int, int, uint8_t, uint8_t); typedef void (*_gfx_heal_fp)(int, int, uint8_t, uint8_t); extern _gfx_blit_fp _gfx_blit_fn; extern _gfx_heal_fp _gfx_heal_fn; +/* ---- Атласы в страницах W0 (sprite.h, §9в дизайна) ---------------- * + * _gfx_w0_dss — страница ядра DSS (снимается при первом map/load); + * _gfx_w0_cur — какая спрайт-страница должна стоять в W0 (читает + * ISR-стаб _gfx_w0_isr для restore после прерывания). + * common/_gfx_w0_state.c, _gfx_w0_isr.c. */ +extern uint8_t _gfx_w0_dss; +extern uint8_t _gfx_w0_cur; +void _gfx_w0_isr(void); + +/* Раскладка .atl-файла/страницы (вариант II, §3.1). */ +#define _ATL_DIR_OFF 0x68u /* каталог: count × 8 Б (макс 19) */ +#define _ATL_DATA_OFF 0x100u /* ленты; = резерв ISR-стаба */ + /* ---- Fill-burst'ы акселератора 256 (register-ABI, asm-only) ------ * * Вызываются ТОЛЬКО из asm raw-примитивов; аргументы в регистрах, без * глобального скретча в _DATA: diff --git a/libbgi/common/_gfx_blit_fn.c b/libbgi/common/_gfx_blit_fn.c new file mode 100644 index 0000000..eb839c4 --- /dev/null +++ b/libbgi/common/_gfx_blit_fn.c @@ -0,0 +1,13 @@ +/* + * _gfx_blit_fn — указатель диспетчеризации clip/noclip БЛИТ-ядра + * (data-модуль). Дефолт — clip-ядро; gfx_sprite_clip() перенаправляет. + * Горячие call-site'ы (sprite_update/putsprite) зовут через указатель — + * без if в цикле (ветка съедала половину выигрыша noclip: 52 vs 57 fps). + * + * РАЗДЕЛЬНО от _gfx_heal_fn (1 указатель = 1 модуль): putsprite-only + * программа не тянет heal-ядро (+292 Б ловили на tests/spriteclip). + * Ненулевой дефолт → в _INITIALIZED. + */ +#include "../_bgi.h" + +_gfx_blit_fp _gfx_blit_fn = _gfx_blit_sprite; diff --git a/libbgi/common/_gfx_heal_fn.c b/libbgi/common/_gfx_heal_fn.c new file mode 100644 index 0000000..0e412c9 --- /dev/null +++ b/libbgi/common/_gfx_heal_fn.c @@ -0,0 +1,9 @@ +/* + * _gfx_heal_fn — указатель диспетчеризации clip/noclip HEAL-ядра + * (data-модуль). Дефолт — clip-ядро; gfx_sprite_clip() перенаправляет. + * Отдельно от _gfx_blit_fn (1 указатель = 1 модуль — DCE): его тянут + * только movesprite/sprite_update, у putsprite heal'а нет. + */ +#include "../_bgi.h" + +_gfx_heal_fp _gfx_heal_fn = _gfx_heal_sprite; diff --git a/libbgi/common/_gfx_sprite_fns.c b/libbgi/common/_gfx_sprite_fns.c deleted file mode 100644 index 219b93a..0000000 --- a/libbgi/common/_gfx_sprite_fns.c +++ /dev/null @@ -1,16 +0,0 @@ -/* - * _gfx_sprite_fns — указатели диспетчеризации clip/noclip спрайтовых ядер - * (data-модуль). Дефолт — clip-ядра (безопасно); gfx_sprite_clip() - * перенаправляет на noclip и обратно. Горячие call-site'ы - * (sprite_update/putsprite/movesprite) зовут через указатель — один - * call-site без if в цикле → тесный codegen (ветка if(clip) в - * sprite_update съедала половину выигрыша noclip: 52 вместо 57 fps). - * - * Программа, не зовущая gfx_sprite_clip(), тянет только clip-ядра - * (noclip-ядра referenced лишь из модуля gfx_sprite_clip.c — DCE). - * Ненулевой дефолт → в _INITIALIZED (как _gfx_bank). - */ -#include "../_bgi.h" - -_gfx_blit_fp _gfx_blit_fn = _gfx_blit_sprite; -_gfx_heal_fp _gfx_heal_fn = _gfx_heal_sprite; diff --git a/libbgi/common/_gfx_w0_isr.c b/libbgi/common/_gfx_w0_isr.c new file mode 100644 index 0000000..877eced --- /dev/null +++ b/libbgi/common/_gfx_w0_isr.c @@ -0,0 +1,43 @@ +/* + * _gfx_w0_isr — ISR-стаб для спрайт-страниц в W0 (§9в дизайна). + * + * Сюда прыгает 3-байтный JP, который atlas_load патчит в 0x38 каждой + * атлас-страницы: пока страница подключена в W0, ЛЮБОЕ прерывание + * (IM1 напрямую; IM2-трамплин libc/irq чейнит jp 0x0038) попадает в + * этот стаб вместо кода ядра DSS. + * + * Схема (проверена tests/w0page, MAME dev 2026-07-13): + * 1) W0 := страница ядра DSS (_gfx_w0_dss); + * 2) подложить адрес возврата и прыгнуть в честный 0x0038 — DSS + * делает свой save/restore и завершает EI/RETI → возврат в хвост; + * 3) хвост: W0 := _gfx_w0_cur (спрайт-страница) — restore ОБЯЗАТЕЛЕН, + * он закрывает EI-щель между out(0x82) движка и DI leaf'а. + * Вложенное прерывание в хвосте (EI уже сделан DSS) безопасно: W0 в + * этот момент = DSS, штатный путь, стаб не реентерится. + * + * Живёт в _CODE — обязан быть в W2 (режимы tiny/big; в small/huge код + * в W1, который прерывание может перемапить — не использовать W0-атласы + * с irq_install там; сам стаб зовётся только при странице в W0, т.е. + * из окна sprite_update, где W1 стоит программный — работает). + */ +#include "../_bgi.h" + +void _gfx_w0_isr(void) __naked +{ + __asm + push af + ld a, (__gfx_w0_dss) + out (#0x82), a ; W0 = ядро DSS + pop af + push hl + ld hl, #w0_isr_ret + ex (sp), hl ; вершина стека = возврат, HL цел + jp 0x0038 ; честный обработчик (EI/RETI) + w0_isr_ret: + push af + ld a, (__gfx_w0_cur) + out (#0x82), a ; вернуть спрайт-страницу + pop af + ret + __endasm; +} diff --git a/libbgi/common/_gfx_w0_state.c b/libbgi/common/_gfx_w0_state.c new file mode 100644 index 0000000..829c0e8 --- /dev/null +++ b/libbgi/common/_gfx_w0_state.c @@ -0,0 +1,12 @@ +/* + * _gfx_w0_state — состояние W0-маппинга атласов (data-модуль). + * + * _gfx_w0_dss — страница ядра DSS (IN A,(0x82) на первом gfx_w0_map/ + * atlas_load); _gfx_w0_cur — спрайт-страница, которую ISR-стаб + * (_gfx_w0_isr) обязан вернуть в W0 после прерывания. Оба читаются из + * прерывания → живут в W2 (_DATA). Не инициализировать (crt0 зануляет). + */ +#include "../_bgi.h" + +uint8_t _gfx_w0_dss; +uint8_t _gfx_w0_cur; diff --git a/libbgi/common/atlas_free.c b/libbgi/common/atlas_free.c new file mode 100644 index 0000000..f5890ec --- /dev/null +++ b/libbgi/common/atlas_free.c @@ -0,0 +1,16 @@ +/* + * atlas_free — освободить EMM-страницу атласа. Все спрайты с лентами + * этого атласа должны быть скрыты (и sprite_update прогнан) ДО вызова — + * иначе heal/блит полезут в чужую страницу. + */ +#include +#include +#include "../_bgi.h" + +void atlas_free(atlas_t *a) +{ + if (!a->page) return; + mem_free_block(a->blk); + a->page = 0; + a->count = 0; +} diff --git a/libbgi/common/atlas_image.c b/libbgi/common/atlas_image.c new file mode 100644 index 0000000..4f13771 --- /dev/null +++ b/libbgi/common/atlas_image.c @@ -0,0 +1,22 @@ +/* + * atlas_image — W0-адрес ленты idx (из каталога атласа). Каталог + * читается через временный мап страницы в W3 (setup-путь, не горячий). + * Возвращённый указатель разыменовывать ТОЛЬКО при подключенной + * странице (sprite_update мапит сам; вручную — gfx_w0_map/unmap). + */ +#include +#include +#include "../_bgi.h" + +const void *atlas_image(const atlas_t *a, uint8_t idx) +{ + uint8_t saved = _io_page_w3; + const uint8_t *e; + uint16_t off; + + sprinter_page_w3(a->page); + e = (const uint8_t *)(0xC000 + _ATL_DIR_OFF) + (uint16_t)idx * 8; + off = e[0] | (uint16_t)(e[1] << 8); + sprinter_page_w3(saved); + return (const void *)off; +} diff --git a/libbgi/common/atlas_load.c b/libbgi/common/atlas_load.c new file mode 100644 index 0000000..a2dd543 --- /dev/null +++ b/libbgi/common/atlas_load.c @@ -0,0 +1,68 @@ +/* + * atlas_load — загрузить .atl-файл в свежую EMM-страницу (§3.1/§9в + * дизайна, вариант II: файл-офсет == офсет страницы == W0-адрес). + * + * Порядок: выделить страницу (mem_alloc_pages) → замапить в W3 → + * read() файла ЦЕЛИКОМ в 0xC000 (ESTEX READ пишет в W3 — проверено + * tests/w0page P1) → проверить магию 'SPA1' → пропатчить ISR-стаб + * (0x38: JP _gfx_w0_isr; 0x66: RETN — место зарезервировано в + * заголовке файла) → вернуть W3. Никакого пересчёта офсетов: каталог + * и ленты уже лежат по своим W0-адресам. + * + * Вызывать ВНЕ рендера (W3 на время загрузки занята страницей атласа). + * Ошибки: -1 + errno (open/read/alloc; EINVAL — не .atl). + */ +#include +#include +#include +#include +#include +#include +#include "../_bgi.h" + +int atlas_load(atlas_t *a, const char *path) +{ + uint8_t *pg = (uint8_t *)0xC000; + uint8_t saved_w3, blk, ok; + uint16_t stub; + int fd, n; + + fd = open(path, O_RDONLY); + if (fd < 0) return -1; + + blk = mem_alloc_pages(1); + if (!blk) { close(fd); return -1; } + + if (!_gfx_w0_dss) + _gfx_w0_dss = _io_page_w0; /* снять страницу ядра DSS */ + + a->blk = blk; + a->page = mem_get_page(blk, 0); + + saved_w3 = _io_page_w3; + sprinter_page_w3(a->page); + + n = read(fd, pg, 16384); + ok = (n >= (int)_ATL_DATA_OFF && + pg[0] == 'S' && pg[1] == 'P' && pg[2] == 'A' && pg[3] == '1'); + if (ok) { + a->count = pg[4]; + stub = (uint16_t)&_gfx_w0_isr; + pg[0x38] = 0xC3; /* JP _gfx_w0_isr */ + pg[0x39] = (uint8_t)(stub & 0xFF); + pg[0x3A] = (uint8_t)(stub >> 8); + pg[0x66] = 0xED; /* RETN (NMI) */ + pg[0x67] = 0x45; + } + + sprinter_page_w3(saved_w3); + close(fd); + + if (!ok) { + mem_free_block(blk); + a->page = 0; + errno = EINVAL; + return -1; + } + return 0; +} diff --git a/libbgi/common/atlas_sprite_init.c b/libbgi/common/atlas_sprite_init.c new file mode 100644 index 0000000..9d750ba --- /dev/null +++ b/libbgi/common/atlas_sprite_init.c @@ -0,0 +1,30 @@ +/* + * atlas_sprite_init — инициализировать спрайт лентой idx W0-атласа. + * Аналог sprite_init, но img/размер КАДРА берутся из каталога атласа + * (заголовок ленты трогать нельзя — он в невидимой сейчас странице), + * а page = страница атласа (sprite_update подключит её сам). + * Кадр (0,0); кадр (i,j) ленты — sprite_frame(s, i*fw, j*fh). + */ +#include +#include +#include "../_bgi.h" + +void atlas_sprite_init(sprite_t *s, const atlas_t *a, uint8_t idx) +{ + uint8_t saved = _io_page_w3; + const uint8_t *e; + + sprinter_page_w3(a->page); + e = (const uint8_t *)(0xC000 + _ATL_DIR_OFF) + (uint16_t)idx * 8; + s->img = (const void *)(e[0] | (uint16_t)(e[1] << 8)); + s->w = e[2]; /* fw — размер КАДРА */ + s->h = e[3]; /* fh */ + sprinter_page_w3(saved); + + s->x = 0; s->y = 0; + s->sx = 0; s->sy = 0; + s->flags = 0; + s->page = a->page; + s->drawn[0].on = 0; + s->drawn[1].on = 0; +} diff --git a/libbgi/common/gfx_w0_map.c b/libbgi/common/gfx_w0_map.c new file mode 100644 index 0000000..03e8d33 --- /dev/null +++ b/libbgi/common/gfx_w0_map.c @@ -0,0 +1,17 @@ +/* + * gfx_w0_map — подключить страницу атласа в W0 вручную (для putsprite/ + * gfx_blit_part вне движка; sprite_update мапит сам). Порядок важен: + * сначала _gfx_w0_cur (прерывание в щели до OUT пройдёт по стабу + * ПРЕДЫДУЩЕЙ страницы и восстановит уже новую — безвредно), потом OUT. + * Пока страница подключена — НИКАКИХ ESTEX/BIOS. Снятие — gfx_w0_unmap. + */ +#include +#include "../_bgi.h" + +void gfx_w0_map(uint8_t page) +{ + if (!_gfx_w0_dss) + _gfx_w0_dss = _io_page_w0; /* первая встреча: снять ядро DSS */ + _gfx_w0_cur = page; + _io_page_w0 = page; +} diff --git a/libbgi/common/gfx_w0_unmap.c b/libbgi/common/gfx_w0_unmap.c new file mode 100644 index 0000000..43306f4 --- /dev/null +++ b/libbgi/common/gfx_w0_unmap.c @@ -0,0 +1,13 @@ +/* + * gfx_w0_unmap — вернуть в W0 страницу ядра DSS после gfx_w0_map. + * После этого ESTEX/BIOS снова доступны. Ничего не делает, если + * map ни разу не звался (_gfx_w0_dss ещё не снята). + */ +#include +#include "../_bgi.h" + +void gfx_w0_unmap(void) +{ + if (_gfx_w0_dss) + _io_page_w0 = _gfx_w0_dss; +} diff --git a/libbgi/common/sprite_init.c b/libbgi/common/sprite_init.c index f0e5c09..620a183 100644 --- a/libbgi/common/sprite_init.c +++ b/libbgi/common/sprite_init.c @@ -22,6 +22,7 @@ void sprite_init(sprite_t *s, const void *img) s->w = p[0]; s->h = p[2]; s->flags = 0; + s->page = 0; /* img в обычной памяти (не W0-атлас) */ s->drawn[0].on = 0; s->drawn[1].on = 0; } diff --git a/libbgi/common/sprite_update.c b/libbgi/common/sprite_update.c index 4f9e24b..2a8fd98 100644 --- a/libbgi/common/sprite_update.c +++ b/libbgi/common/sprite_update.c @@ -23,6 +23,7 @@ * прерывания НЕ трогает (только мапит W3), поэтому длина прохода не * влияет на аудио-ISR. w,h кадра ≤ 255 (см. sprite.h). */ +#include #include "../_bgi.h" #include @@ -45,19 +46,35 @@ void sprite_update(sprite_t *arr, uint8_t count) } _bgi_end(); - /* Проход 2: блит ВСЕХ видимых, требующих обновления. */ + /* Проход 2: блит ВСЕХ видимых, требующих обновления. Спрайты из + * W0-атласов (s->page): подключить страницу перед блитом — один OUT + * на СМЕНУ страницы (спрайты одного атласа подряд = один OUT); + * порядок «_gfx_w0_cur, затем OUT» закрывает гонку с ISR-стабом + * (он восстановит уже новую). heal-проходу страница не нужна + * (img не читается). */ gfx_set_bank(GFX_BANK_SPRITE); _bgi_begin(); - for (i = 0, s = arr; i < count; i++, s++) { - if (s->flags & (dbit | SPR_ALWAYS)) { - if (s->flags & SPR_VISIBLE) { - (*_gfx_blit_fn)(s->x, s->y, s->img, s->sx, s->sy, s->w, s->h); - s->drawn[p].x = s->x; - s->drawn[p].y = s->y; - s->drawn[p].on = 1; + { + uint8_t mapped = 0; /* 0 = W0 не тронута (там ядро DSS) */ + + for (i = 0, s = arr; i < count; i++, s++) { + 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; + } + (*_gfx_blit_fn)(s->x, s->y, s->img, s->sx, s->sy, 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 обновлена для спрайта */ } - s->flags &= ~dbit; /* страница p обновлена для спрайта */ } + if (mapped) + _io_page_w0 = _gfx_w0_dss; /* вернуть ядро DSS */ } _bgi_end(); diff --git a/libbgi/include/sprite.h b/libbgi/include/sprite.h index 38d3ac4..2f391e0 100644 --- a/libbgi/include/sprite.h +++ b/libbgi/include/sprite.h @@ -52,6 +52,9 @@ typedef struct { 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 на время блита) */ /* --- внутреннее (владеет движок) --- */ struct { int x, y; /* где нарисован на странице p */ @@ -104,4 +107,44 @@ inline void sprite_hide(sprite_t *s) { * активный спрайт прошёл над статиком — приложение помечает статик). */ inline void sprite_touch(sprite_t *s) { s->flags |= _SPR_DIRTY; } +/* ==== Атласы в 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 diff --git a/tests/atlas/.gitignore b/tests/atlas/.gitignore new file mode 100644 index 0000000..2511926 --- /dev/null +++ b/tests/atlas/.gitignore @@ -0,0 +1,2 @@ +atlas.atl +frames*.raw diff --git a/tests/atlas/Makefile b/tests/atlas/Makefile new file mode 100644 index 0000000..d0b138c --- /dev/null +++ b/tests/atlas/Makefile @@ -0,0 +1,13 @@ +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := atlas +EXTRA_FLAGS ?= --gfx 256 --memory tiny +EXTRA_DATA := atlas.atl +include $(PROJ_ROOT)/app.mk + +# .atl собирается из сгенерированных лент упаковщиком mkatlas.py. +atlas.atl: gen_frames.py $(PROJ_ROOT)/toolchain/mkatlas.py + python3 gen_frames.py + python3 $(PROJ_ROOT)/toolchain/mkatlas.py $@ \ + frames1.raw:16x16:1x4 frames2.raw:24x24:1x2 + +$(EXAMPLE).exe: atlas.atl diff --git a/tests/atlas/atlas.c b/tests/atlas/atlas.c new file mode 100644 index 0000000..8caaad1 --- /dev/null +++ b/tests/atlas/atlas.c @@ -0,0 +1,94 @@ +/* + * atlas — тест W0-атласов (sprite.h: atlas_load/atlas_image/ + * atlas_sprite_init + спрайтовый движок с полем page). + * + * atlas.atl (генерят gen_frames.py + mkatlas.py): + * лента 0: 16×16, 4 кадра вертикально — кадр k: заливка 0x10+k, + * рамка 0x04, углы 0xFF (прозрачные); + * лента 1: 24×24, 2 кадра — заливка 0x20+k, рамка 0x0E. + * + * Стадии: + * A atlas_load: count == 2. + * B каталог/W0: getimage-заголовок ленты 0 (w=16, h=64) читается + * через указатель atlas_image при gfx_w0_map — проверка, что + * офсеты каталога и данные легли по своим W0-адресам. + * C движок: 3 спрайта из двух лент (кадры 0/2 ленты 0, кадр 1 + * ленты 1) на зелёном фоне — sprite_update сам мапит страницу. + * Экран write-only → корректность пикселей проверяется скриншотом/ + * read_vram: (108,68)=0x10, (148,68)=0x12, (188,72)=0x21, + * угол (100,60)=0x02 (фон сквозь прозрачный угол). + * + * Запуск через MAME-мост; Enter между стадиями. + */ +#include +#include +#include +#include + +static atlas_t at; +static sprite_t sp[3]; + +int main(void) +{ + const uint8_t *im; + uint8_t okA, okB, okC; + + puts("ATLAS: .atl load + W0 sprite engine"); + + /* ---- A: загрузка ------------------------------------------------ */ + if (atlas_load(&at, "atlas.atl") != 0 && + atlas_load(&at, "a:\\atlas.atl") != 0) { + puts("A atlas_load: FAIL (open/format)"); + return 1; + } + okA = (at.count == 2); + printf("A load: %s (page=%u count=%u)\n", + okA ? "PASS" : "FAIL", at.page, at.count); + + /* ---- B: каталог + данные по W0-адресам -------------------------- */ + im = (const uint8_t *)atlas_image(&at, 0); + gfx_w0_map(at.page); + okB = (im[0] == 16 && im[1] == 0 && im[2] == 64 && im[3] == 0) && + (im[4] == 0xFF) && /* угол кадра 0 — прозрачный */ + (im[4 + 17] == 0x10); /* (1,1) кадра 0 — заливка */ + gfx_w0_unmap(); + printf("B W0 dir/data: %s (img=%04x)\n", + okB ? "PASS" : "FAIL", (unsigned)im); + puts("Enter -> gfx"); + (void)getchar(); + + /* ---- C: движок со спрайтами из атласа --------------------------- */ + initgraph(); + cleardevice(); + setfillstyle(SOLID_FILL, GREEN); + bar(90, 50, 220, 100); + + atlas_sprite_init(&sp[0], &at, 0); /* лента 0, кадр 0 */ + sp[0].x = 100; sp[0].y = 60; + sprite_show(&sp[0]); + + atlas_sprite_init(&sp[1], &at, 0); /* лента 0, кадр 2 */ + sp[1].x = 140; sp[1].y = 60; + sprite_frame(&sp[1], 0, 2 * 16); + sprite_show(&sp[1]); + + atlas_sprite_init(&sp[2], &at, 1); /* лента 1, кадр 1 */ + sp[2].x = 180; sp[2].y = 60; + sprite_frame(&sp[2], 0, 24); + sprite_show(&sp[2]); + + sprite_update(sp, 3); + + /* ESTEX снова доступен: sprite_update вернул страницу ядра DSS. */ + okC = 1; /* пиксели — скриншотом/read_vram */ + setcolor(WHITE); + outtextxy(4, 4, "ATLAS: 3 sprites from .atl page (W0)"); + outtextxy(4, 200, okA && okB ? "A/B PASS; C: check pixels" + : "A/B FAIL"); + outtextxy(4, 210, "Enter to exit"); + (void)getchar(); + + closegraph(); + printf("done (A=%u B=%u C=%u)\n", okA, okB, okC); + return 0; +} diff --git a/tests/atlas/gen_frames.py b/tests/atlas/gen_frames.py new file mode 100644 index 0000000..00f47c2 --- /dev/null +++ b/tests/atlas/gen_frames.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Генератор тестовых лент для tests/atlas (сырые байты для mkatlas.py). + +frames1.raw — 16×16, 4 кадра вертикально (16×64): кадр k = заливка +0x10+k, рамка 0x04, углы 0xFF (прозрачные). +frames2.raw — 24×24, 2 кадра вертикально (24×48): кадр k = заливка +0x20+k, рамка 0x0E, углы 0xFF. +""" + + +def strip(fw, fh, n, fill0, border): + out = bytearray() + for k in range(n): + for y in range(fh): + for x in range(fw): + edge_x = x in (0, fw - 1) + edge_y = y in (0, fh - 1) + if edge_x and edge_y: + out.append(0xFF) # прозрачный угол + elif edge_x or edge_y: + out.append(border) + else: + out.append(fill0 + k) + return out + + +open("frames1.raw", "wb").write(strip(16, 16, 4, 0x10, 0x04)) +open("frames2.raw", "wb").write(strip(24, 24, 2, 0x20, 0x0E)) +print("gen_frames: frames1.raw (16x16x4), frames2.raw (24x24x2)") diff --git a/toolchain/mkatlas.py b/toolchain/mkatlas.py new file mode 100644 index 0000000..ec79400 --- /dev/null +++ b/toolchain/mkatlas.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +""" +mkatlas.py — упаковщик атласов спрайтов Sprinter (.atl, вариант II — +docs/sprite-api-design.md §3.1). + +Формат файла (== образ EMM-страницы, файл-офсет == W0-адрес): + 0x000 'S','P','A','1', count u8, резерв ×3 + 0x008 резерв (0x38-0x3A и 0x66-0x67 — под патч ISR-стаба загрузчиком) + 0x068 каталог: count × 8 Б {offset u16 LE, fw u8, fh u8, nx u8, ny u8, + резерв u16} — offset указывает на getimage-ЗАГОЛОВОК ленты + 0x100 ленты подряд: u16 w=fw*nx, u16 h=fh*ny (LE) + пиксели построчно + +Вход — список лент: + img.png:FWxFH индексированный PNG (mode P/L; байты как есть, + прозрачность = индекс 0xFF); сетка nx,ny из + размеров картинки + img.raw:FWxFH:NXxNY сырые байты (fw*nx)×(fh*ny), построчно + +Пример: + mkatlas.py sprites.atl hero.png:16x16 coin.png:16x16 boss.raw:24x24:2x2 + +Ограничения: count ≤ 19, файл ≤ 16384 Б, кадр ≤ 64×64. +""" +import os +import sys + +HDR_SIZE = 0x100 +DIR_OFF = 0x68 +MAX_ENTRY = (HDR_SIZE - DIR_OFF) // 8 # 19 +PAGE_SIZE = 16384 + + +def die(msg): + sys.exit(f"mkatlas: {msg}") + + +def load_png(path): + from PIL import Image + im = Image.open(path) + if im.mode not in ("P", "L"): + die(f"{path}: нужен индексированный PNG (mode P/L), не {im.mode}") + return im.width, im.height, bytes(im.tobytes()) + + +def parse_spec(spec): + parts = spec.split(":") + if len(parts) < 2: + die(f"'{spec}': нужен вид file:FWxFH[:NXxNY]") + path = parts[0] + fw, fh = (int(v) for v in parts[1].lower().split("x")) + if not (1 <= fw <= 64 and 1 <= fh <= 64): + die(f"{path}: кадр {fw}x{fh} вне 1..64 (контракт спрайтов)") + + if path.lower().endswith(".png"): + if len(parts) != 2: + die(f"{path}: для PNG сетка берётся из размеров картинки") + w, h, pix = load_png(path) + else: + if len(parts) != 3: + die(f"{path}: для raw обязательна сетка :NXxNY") + nx, ny = (int(v) for v in parts[2].lower().split("x")) + w, h = fw * nx, fh * ny + pix = open(path, "rb").read() + if len(pix) != w * h: + die(f"{path}: {len(pix)} байт, ожидалось {w}x{h}={w * h}") + + if w % fw or h % fh: + die(f"{path}: {w}x{h} не делится на кадр {fw}x{fh}") + nx, ny = w // fw, h // fh + if not (1 <= nx <= 255 and 1 <= ny <= 255): + die(f"{path}: сетка {nx}x{ny} вне 1..255") + return dict(path=path, fw=fw, fh=fh, nx=nx, ny=ny, w=w, h=h, pix=pix) + + +def main(): + if len(sys.argv) < 3: + sys.exit(__doc__.strip()) + out, specs = sys.argv[1], sys.argv[2:] + if len(specs) > MAX_ENTRY: + die(f"лент {len(specs)} > {MAX_ENTRY} (каталог 0x68-0xFF)") + + entries = [parse_spec(s) for s in specs] + + blob = bytearray(HDR_SIZE) + blob[0:4] = b"SPA1" + blob[4] = len(entries) + + off = HDR_SIZE + for i, e in enumerate(entries): + d = DIR_OFF + i * 8 + blob[d:d + 2] = off.to_bytes(2, "little") + blob[d + 2] = e["fw"] + blob[d + 3] = e["fh"] + blob[d + 4] = e["nx"] + blob[d + 5] = e["ny"] + blob += e["w"].to_bytes(2, "little") # getimage-заголовок ленты + blob += e["h"].to_bytes(2, "little") + blob += e["pix"] + off = len(blob) + + if len(blob) > PAGE_SIZE: + die(f"итог {len(blob)} Б > {PAGE_SIZE} (одна EMM-страница); " + f"разбейте на несколько .atl") + + with open(out, "wb") as f: + f.write(blob) + print(f"mkatlas: {out}: лент {len(entries)}, " + f"{len(blob)} Б (свободно {PAGE_SIZE - len(blob)})") + for i, e in enumerate(entries): + print(f" [{i}] {os.path.basename(e['path'])}: кадр " + f"{e['fw']}x{e['fh']}, сетка {e['nx']}x{e['ny']}") + + +if __name__ == "__main__": + main()