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:
@@ -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->h = p[2];
|
||||
s->flags = 0;
|
||||
s->page = 0; /* img в обычной памяти (не W0-атлас) */
|
||||
s->drawn[0].on = 0;
|
||||
s->drawn[1].on = 0;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
* прерывания НЕ трогает (только мапит W3), поэтому длина прохода не
|
||||
* влияет на аудио-ISR. w,h кадра ≤ 255 (см. sprite.h).
|
||||
*/
|
||||
#include <sprinter.h>
|
||||
#include "../_bgi.h"
|
||||
#include <sprite.h>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user