libbgi: W0-атласы спрайтов — загрузчик, движок, упаковщик (все проверки PASS)
Реализация §3.1/§9в: атлас = один .atl-файл = одна EMM-страница, подключаемая в W0 на время блита. - atlas_load: read() файла целиком в страницу через W3 + патч ISR-стаба (0x38: JP _gfx_w0_isr; 0x66: RETN); atlas_free/atlas_image/ atlas_sprite_init; gfx_w0_map/unmap для ручных вызовов. - _gfx_w0_isr (стаб из tests/w0page): свап на ядро DSS → честный 0x38 → restore спрайт-страницы; покрывает IM1 и IM2-чейн. - sprite_t.page (0 = обычная память); sprite_update в блит-проходе мапит страницу по смене (один OUT на атлас), эпилог возвращает DSS (+52 Б на движок — цена фичи). - toolchain/mkatlas.py: PNG (indexed) / raw → .atl; заголовок 0x100, каталог 0x68 (19 лент), файл-офсет == офсет страницы == W0-адрес. - Сплит _gfx_sprite_fns → _gfx_blit_fn.c + _gfx_heal_fn.c (1 указатель = 1 модуль): putsprite-only программа не тянет heal-ядро (spriteclip ловил +292 Б; теперь −68 Б от эталона). tests/atlas (MAME dev, PASS): загрузка (count/страница), каталог и данные по W0-адресам (заголовок ленты через gfx_w0_map), 3 спрайта из двух лент через движок — пиксели проверены read_vram побайтно (0x10/0x12/0x21, прозрачный угол = фон). docs: §9г — предложение авто-анимации (кадровая ±step без умножений, tween-Брезенхэм порциями, тикер через funcptr) — ОБСУЖДАЕТСЯ. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,13 +2,14 @@
|
|||||||
accfill 3772
|
accfill 3772
|
||||||
argv 3431
|
argv 3431
|
||||||
assrtest 3847
|
assrtest 3847
|
||||||
|
atlas 9154
|
||||||
attrprob 4087
|
attrprob 4087
|
||||||
balls 6156
|
balls 6208
|
||||||
banked 1056
|
banked 1056
|
||||||
bankedbg 1067
|
bankedbg 1067
|
||||||
banklocl 4683
|
banklocl 4683
|
||||||
banktest 3767
|
banktest 3767
|
||||||
bgi_img 7060
|
bgi_img 7057
|
||||||
bgitest 3737
|
bgitest 3737
|
||||||
bios_text 4461
|
bios_text 4461
|
||||||
blitperf 5860
|
blitperf 5860
|
||||||
@@ -43,7 +44,7 @@ rt_test 4892
|
|||||||
seek 4188
|
seek 4188
|
||||||
simple 955
|
simple 955
|
||||||
solidt 11531
|
solidt 11531
|
||||||
spriteclip 4187
|
spriteclip 4119
|
||||||
sprites 6791
|
sprites 6791
|
||||||
stattest 7549
|
stattest 7549
|
||||||
stdlib 6643
|
stdlib 6643
|
||||||
|
|||||||
|
@@ -700,6 +700,69 @@ Probe-тест tests/w0page (MAME dev, 2026-07-13, ВСЕ PASS):
|
|||||||
W1, но только tiny (в small/big/huge в W1 код) — отвергнута в пользу
|
W1, но только tiny (в small/big/huge в W1 код) — отвергнута в пользу
|
||||||
универсального W0.
|
универсального 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)
|
## 10. Риски / что проверить артефактом (Фаза 0)
|
||||||
|
|
||||||
Статус 2026-07-11: пп. 1–3 закрыты для MAME 0.283 (tests/gfxbanks,
|
Статус 2026-07-11: пп. 1–3 закрыты для MAME 0.283 (tests/gfxbanks,
|
||||||
|
|||||||
+15
-1
@@ -134,13 +134,27 @@ void _gfx_heal_sprite_noclip(int x, int y, uint8_t w, uint8_t h);
|
|||||||
* а sprite_update/putsprite/movesprite зовут через указатель — один
|
* а sprite_update/putsprite/movesprite зовут через указатель — один
|
||||||
* call-site, без if в цикле → тесный codegen (полный выигрыш noclip,
|
* call-site, без if в цикле → тесный codegen (полный выигрыш noclip,
|
||||||
* см. docs/sprite-api-design.md). Дефолт (clip-ядра) — статическая
|
* см. 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,
|
typedef void (*_gfx_blit_fp)(int, int, const void *, int, int,
|
||||||
uint8_t, uint8_t);
|
uint8_t, uint8_t);
|
||||||
typedef void (*_gfx_heal_fp)(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_blit_fp _gfx_blit_fn;
|
||||||
extern _gfx_heal_fp _gfx_heal_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) ------ *
|
/* ---- Fill-burst'ы акселератора 256 (register-ABI, asm-only) ------ *
|
||||||
* Вызываются ТОЛЬКО из asm raw-примитивов; аргументы в регистрах, без
|
* Вызываются ТОЛЬКО из asm raw-примитивов; аргументы в регистрах, без
|
||||||
* глобального скретча в _DATA:
|
* глобального скретча в _DATA:
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* atlas_free — освободить EMM-страницу атласа. Все спрайты с лентами
|
||||||
|
* этого атласа должны быть скрыты (и sprite_update прогнан) ДО вызова —
|
||||||
|
* иначе heal/блит полезут в чужую страницу.
|
||||||
|
*/
|
||||||
|
#include <sprinter_mem.h>
|
||||||
|
#include <sprite.h>
|
||||||
|
#include "../_bgi.h"
|
||||||
|
|
||||||
|
void atlas_free(atlas_t *a)
|
||||||
|
{
|
||||||
|
if (!a->page) return;
|
||||||
|
mem_free_block(a->blk);
|
||||||
|
a->page = 0;
|
||||||
|
a->count = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* atlas_image — W0-адрес ленты idx (из каталога атласа). Каталог
|
||||||
|
* читается через временный мап страницы в W3 (setup-путь, не горячий).
|
||||||
|
* Возвращённый указатель разыменовывать ТОЛЬКО при подключенной
|
||||||
|
* странице (sprite_update мапит сам; вручную — gfx_w0_map/unmap).
|
||||||
|
*/
|
||||||
|
#include <sprinter.h>
|
||||||
|
#include <sprite.h>
|
||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -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 <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sprinter.h>
|
||||||
|
#include <sprinter_mem.h>
|
||||||
|
#include <sprite.h>
|
||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -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 <sprinter.h>
|
||||||
|
#include <sprite.h>
|
||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -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 <sprinter.h>
|
||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* gfx_w0_unmap — вернуть в W0 страницу ядра DSS после gfx_w0_map.
|
||||||
|
* После этого ESTEX/BIOS снова доступны. Ничего не делает, если
|
||||||
|
* map ни разу не звался (_gfx_w0_dss ещё не снята).
|
||||||
|
*/
|
||||||
|
#include <sprinter.h>
|
||||||
|
#include "../_bgi.h"
|
||||||
|
|
||||||
|
void gfx_w0_unmap(void)
|
||||||
|
{
|
||||||
|
if (_gfx_w0_dss)
|
||||||
|
_io_page_w0 = _gfx_w0_dss;
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ void sprite_init(sprite_t *s, const void *img)
|
|||||||
s->w = p[0];
|
s->w = p[0];
|
||||||
s->h = p[2];
|
s->h = p[2];
|
||||||
s->flags = 0;
|
s->flags = 0;
|
||||||
|
s->page = 0; /* img в обычной памяти (не W0-атлас) */
|
||||||
s->drawn[0].on = 0;
|
s->drawn[0].on = 0;
|
||||||
s->drawn[1].on = 0;
|
s->drawn[1].on = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
* прерывания НЕ трогает (только мапит W3), поэтому длина прохода не
|
* прерывания НЕ трогает (только мапит W3), поэтому длина прохода не
|
||||||
* влияет на аудио-ISR. w,h кадра ≤ 255 (см. sprite.h).
|
* влияет на аудио-ISR. w,h кадра ≤ 255 (см. sprite.h).
|
||||||
*/
|
*/
|
||||||
|
#include <sprinter.h>
|
||||||
#include "../_bgi.h"
|
#include "../_bgi.h"
|
||||||
#include <sprite.h>
|
#include <sprite.h>
|
||||||
|
|
||||||
@@ -45,12 +46,25 @@ void sprite_update(sprite_t *arr, uint8_t count)
|
|||||||
}
|
}
|
||||||
_bgi_end();
|
_bgi_end();
|
||||||
|
|
||||||
/* Проход 2: блит ВСЕХ видимых, требующих обновления. */
|
/* Проход 2: блит ВСЕХ видимых, требующих обновления. Спрайты из
|
||||||
|
* W0-атласов (s->page): подключить страницу перед блитом — один OUT
|
||||||
|
* на СМЕНУ страницы (спрайты одного атласа подряд = один OUT);
|
||||||
|
* порядок «_gfx_w0_cur, затем OUT» закрывает гонку с ISR-стабом
|
||||||
|
* (он восстановит уже новую). heal-проходу страница не нужна
|
||||||
|
* (img не читается). */
|
||||||
gfx_set_bank(GFX_BANK_SPRITE);
|
gfx_set_bank(GFX_BANK_SPRITE);
|
||||||
_bgi_begin();
|
_bgi_begin();
|
||||||
|
{
|
||||||
|
uint8_t mapped = 0; /* 0 = W0 не тронута (там ядро DSS) */
|
||||||
|
|
||||||
for (i = 0, s = arr; i < count; i++, s++) {
|
for (i = 0, s = arr; i < count; i++, s++) {
|
||||||
if (s->flags & (dbit | SPR_ALWAYS)) {
|
if (s->flags & (dbit | SPR_ALWAYS)) {
|
||||||
if (s->flags & SPR_VISIBLE) {
|
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);
|
(*_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].x = s->x;
|
||||||
s->drawn[p].y = s->y;
|
s->drawn[p].y = s->y;
|
||||||
@@ -59,6 +73,9 @@ void sprite_update(sprite_t *arr, uint8_t count)
|
|||||||
s->flags &= ~dbit; /* страница p обновлена для спрайта */
|
s->flags &= ~dbit; /* страница p обновлена для спрайта */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (mapped)
|
||||||
|
_io_page_w0 = _gfx_w0_dss; /* вернуть ядро DSS */
|
||||||
|
}
|
||||||
_bgi_end();
|
_bgi_end();
|
||||||
|
|
||||||
gfx_set_bank(saved);
|
gfx_set_bank(saved);
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ typedef struct {
|
|||||||
int sx, sy; /* смещение кадра внутри img (атлас; 0 = целая) */
|
int sx, sy; /* смещение кадра внутри img (атлас; 0 = целая) */
|
||||||
uint8_t w, h; /* размер кадра (кэш заголовка), 1..255 */
|
uint8_t w, h; /* размер кадра (кэш заголовка), 1..255 */
|
||||||
uint8_t flags; /* SPR_* | внутренние dirty */
|
uint8_t flags; /* SPR_* | внутренние dirty */
|
||||||
|
uint8_t page; /* 0 = img в обычной памяти; иначе физ. страница *
|
||||||
|
* W0-атласа (img — адрес 0x0100-0x3FFF, движок *
|
||||||
|
* сам мапит страницу в W0 на время блита) */
|
||||||
/* --- внутреннее (владеет движок) --- */
|
/* --- внутреннее (владеет движок) --- */
|
||||||
struct {
|
struct {
|
||||||
int x, y; /* где нарисован на странице p */
|
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; }
|
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
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
atlas.atl
|
||||||
|
frames*.raw
|
||||||
@@ -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
|
||||||
@@ -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 <stdio.h>
|
||||||
|
#include <graphics.h>
|
||||||
|
#include <gfx.h>
|
||||||
|
#include <sprite.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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)")
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user