L9-INVERT II.4: персонажи рисуются перевёрнутыми (зеркальные страницы атласов)

Спрайты персонажей хранятся column-major (ради бесплатного H-флипа), а
вертикальное зеркало у такой раскладки — разворот байтов ВНУТРИ колонки,
чего accel не умеет.  Поэтому pop_vflip.c готовит зеркальные КОПИИ страниц
атласов: копия побайтово повторяет раскладку .atl (заголовок, каталог,
смещения лент), развёрнуты только пиксели — значит вызывающему достаточно
подменить номер страницы, которую он мапит в W0.

* Кэш ленивый (страница зеркалится при первом обращении в перевёрнутом
  виде) и живёт до смены уровня — зелье переключает состояние туда-обратно,
  перезеркаливать по 16 КБ на каждый переворот незачем.  EMM хватает: 34
  страницы из ~215 свободных.
* pop_cdraw/pop_kdraw: страница + позиция (top = 191 - obj_y для персонажа,
  192 - top - h для клинка).  «Пропустить skip строк сверху» у зеркальной
  ленты = «срезать снизу» у оригинала, поэтому клипы считаются как обычно.
* Модуль резидентный: он маппит W3 (приёмник копии) — банку так нельзя.
* pop_vflip_reset на смене уровня: копии сделаны из старых страниц.

Проверено в MAME: чит U переворачивает всё разом, Кид и факелы вверх ногами,
полоса HP на месте, бег без следов на фоне (heal бьёт в те же координаты).

ОСТАЁТСЯ (L9-CLIPCHAR): clip_char в перевороте должен резать снизу, а не
сверху — пока при pop_upside не режем вовсе.  Тени/брызг это не касается.
This commit is contained in:
2026-08-12 22:25:18 +03:00
parent 055d6d7c89
commit 755190674c
8 changed files with 216 additions and 6 deletions
+111
View File
@@ -0,0 +1,111 @@
/*
* pop_vflip.c — ЗЕРКАЛЬНЫЕ ПО ВЕРТИКАЛИ копии страниц атласов персонажей
* (зелье инверсии, уровень 9). См. pop_vflip.h.
*
* Зачем отдельный набор страниц. Спрайты персонажей хранятся COLUMN-MAJOR
* (out[col*h + row], transpose_cols в pop_pack_kid.py) — ради БЕСПЛАТНОГО
* горизонтального флипа: accel копирует колонки, и порядок колонок задаётся
* знаком страйда. А вертикальное зеркало у такой раскладки — это разворот
* байтов ВНУТРИ колонки, чего accel не умеет: блок он копирует только
* вперёд. Значит перевёрнутые кадры надо готовить заранее.
*
* Устройство. Копия — ПОСТРАНИЧНАЯ и побайтово совпадает с оригиналом по
* раскладке: тот же заголовок .atl, тот же каталог, ленты на тех же
* смещениях; развёрнуты только сами пиксели. Поэтому вызывающему не нужно
* менять НИЧЕГО, кроме номера страницы, которую он мапит в W0 — все
* atlas_image-смещения и размеры кадров остаются прежними.
*
* Кэш ЛЕНИВЫЙ: страница зеркалится при первом обращении к ней в
* перевёрнутом виде (~16 КБ копирования = заметно, но однократно на кадр
* анимации), дальше отдаётся из таблицы. Держится до конца уровня;
* pop_vflip_reset освобождает всё (смена уровня / зелье выпито обратно —
* см. pop_vflip.h про то, почему НЕ освобождаем при каждом перевороте).
*
* РЕЗИДЕНТ, а не банк: функция маппит окно W3 (приёмник копии), а банковый
* код в нём же и исполняется — правило из _pop_level.h.
*/
#include <stdint.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include <sprite.h>
#include "pop_vflip.h"
/* Раскладка .atl (sprite.h §Атласы): 0x100-байтовый заголовок, каталог с
* 0x68 — по 8 байт на ленту: [0..1] смещение ленты, дальше размеры. */
#define ATL_DIR_OFF 0x68u
#define ATL_COUNT_OFF 0x04u /* число лент в заголовке */
/* Таблица кэша: страница-источник -> страница-копия. Персонажных атласов
* у нас 28 (Кид) + 5 (страж) + 1 (меч) = 34; берём с запасом. */
#define VF_MAX 40
static uint8_t vf_src[VF_MAX], vf_dst[VF_MAX], vf_blk[VF_MAX], vf_n;
/* Развернуть ленту на месте В СТРАНИЦЕ-ПРИЁМНИКЕ (она замаплена в W3).
* off — смещение ленты от начала страницы; формат ленты: w, _, h, _, пиксели
* колонками по h байт. */
static void flip_strip(uint16_t off)
{
uint8_t *p = (uint8_t *)(0xC000u + off);
uint8_t w = p[0], h = p[2];
uint8_t *col = p + 4;
uint8_t c;
if (!w || !h) return;
for (c = 0; c < w; c++) {
uint8_t *a = col, *b = col + h - 1;
while (a < b) { uint8_t t = *a; *a++ = *b; *b-- = t; }
col += h;
}
}
/* Сделать зеркальную копию страницы src. Возврат номера копии или 0. */
static uint8_t make_flipped(uint8_t src)
{
uint8_t blk, dst, saved, count, i;
uint16_t k;
const uint8_t *s;
uint8_t *d;
blk = mem_alloc_pages(1);
if (!blk) return 0;
dst = mem_get_page(blk, 0);
/* Побайтовая копия страницы: источник в W0 (0x0000-0x3FFF), приёмник в
* W3 (0xC000). Оба окна свободны для резидентного кода W1/W2. */
saved = _io_page_w3;
gfx_w0_map(src);
sprinter_page_w3(dst);
s = (const uint8_t *)0x0000;
d = (uint8_t *)0xC000;
for (k = 0; k < 16384u; k++) d[k] = s[k];
gfx_w0_unmap();
/* Развернуть пиксели каждой ленты — уже в приёмнике (он в W3). */
count = ((const uint8_t *)0xC000)[ATL_COUNT_OFF];
for (i = 0; i < count; i++) {
const uint8_t *e = (const uint8_t *)(0xC000u + ATL_DIR_OFF) + (uint16_t)i * 8;
uint16_t off = (uint16_t)(e[0] | ((uint16_t)e[1] << 8));
if (off >= 0x100u && off < 0x4000u) flip_strip(off);
}
sprinter_page_w3(saved);
if (vf_n < VF_MAX) {
vf_src[vf_n] = src; vf_dst[vf_n] = dst; vf_blk[vf_n] = blk; vf_n++;
}
return dst;
}
uint8_t pop_vflip_page(uint8_t src)
{
uint8_t i;
for (i = 0; i < vf_n; i++)
if (vf_src[i] == src) return vf_dst[i];
return make_flipped(src);
}
void pop_vflip_reset(void)
{
uint8_t i;
for (i = 0; i < vf_n; i++) mem_free_block(vf_blk[i]);
vf_n = 0;
}