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:
2026-07-13 15:52:00 +03:00
parent c4a512200f
commit 0e935343d9
22 changed files with 648 additions and 29 deletions
+4 -3
View File
@@ -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
1 # Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
2 accfill
3 argv
4 assrtest
5 atlas
6 attrprob
7 balls
8 banked
9 bankedbg
10 banklocl
11 banktest
12 bgi_img
13 bgitest
14 bios_text
15 blitperf
44 seek
45 simple
46 solidt
47 spriteclip
48 sprites
49 stattest
50 stdlib
+63
View File
@@ -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: пп. 13 закрыты для MAME 0.283 (tests/gfxbanks, Статус 2026-07-11: пп. 13 закрыты для MAME 0.283 (tests/gfxbanks,
+15 -1
View File
@@ -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:
+13
View File
@@ -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;
+9
View File
@@ -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;
-16
View File
@@ -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;
+43
View File
@@ -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;
}
+12
View File
@@ -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;
+16
View File
@@ -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;
}
+22
View File
@@ -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;
}
+68
View File
@@ -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;
}
+30
View File
@@ -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;
}
+17
View File
@@ -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;
}
+13
View File
@@ -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;
}
+1
View File
@@ -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;
} }
+18 -1
View File
@@ -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);
+43
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
atlas.atl
frames*.raw
+13
View File
@@ -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
+94
View File
@@ -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;
}
+29
View File
@@ -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)")
+115
View File
@@ -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()