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
+4
View File
@@ -52,6 +52,10 @@ Kid — `kid_data.h` (генерится `pop_extract_kid_data.py`).
Эталон комнаты — `../toolchain/1.1-2.png`. Пересобрал HDD-образ → MAME
ОБЯЗАН полный рестарт (`mame_hdd_rebuild_restart`).
**Тайминги моста** (не ждать дольше, см. `docs/mame-autotest.md` §10):
старт `run_bridge.sh` → 6 с`go` → 8 с`keyseq d:{ENTER}` +
`keyseq roomtest{ENTER}` → 5 с → программа работает.
Отладочные тумблеры в живой сессии (`roomtest.c`): **SPACE** — вкл/выкл
дабл-буфер (в однобуфере видно баги рисования без мерцания-через-кадр);
**1** — заморозить кадр, **2** — продолжить (разбор позы/окклюзии);
+2 -2
View File
@@ -42,7 +42,7 @@ EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --ban
# MAME = время фазы в растровых строках.
PROF ?= 1
PROF_FLAGS := -DPROF_BORDER=$(PROF)
EXTRA_SRCS := pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c
EXTRA_SRCS := pop_vflip.c pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c
BG_DIR := $(CURDIR)/../poc/res/bg
KID_DIR := $(CURDIR)/../poc/res/kid
@@ -112,4 +112,4 @@ $(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py
cd $(TC) && python3 pop_extract_kid_data.py
$(EXAMPLE).exe: roomtest_cold.c roomtest_cold.h guards.c pop_guard.c pop_guard_cold.c pop_guard.h pop_cdraw.c pop_cdraw.h pop_draw.c _pop_draw.h pop_tile.c pop_tile.h pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_room.c _pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_kid.c pop_kdraw.c pop_kboot.c _pop_kid.h _pop_kdraw.h pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level_cold.c _pop_level.h pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA)
$(EXAMPLE).exe: roomtest_cold.c roomtest_cold.h guards.c pop_guard.c pop_guard_cold.c pop_guard.h pop_cdraw.c pop_cdraw.h pop_draw.c _pop_draw.h pop_tile.c pop_tile.h pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_room.c _pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_vflip.c pop_vflip.h pop_kid.c pop_kdraw.c pop_kboot.c _pop_kid.h _pop_kdraw.h pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level_cold.c _pop_level.h pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA)
+26 -3
View File
@@ -20,7 +20,8 @@
#include <sprite.h>
#include "pop_guard.h"
#include "pop_kid.h"
#include "_pop_kdraw.h" /* меч + блит по id — сосед по банку 4 */ /* атласы/кадр Кида, pop_sword_draw, окна Char */
#include "_pop_kdraw.h"
#include "pop_vflip.h" /* зеркальные страницы атласов (зелье инверсии) */ /* меч + блит по id — сосед по банку 4 */ /* атласы/кадр Кида, pop_sword_draw, окна Char */
#include "pop_cdraw.h"
#include "pop_bg.h" /* POP_YOFF, pop_fore_over_char, pop_fore_set_clip */
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
@@ -521,7 +522,18 @@ void pop_char_draw(uint8_t who) __banked
idx = (uint8_t)(fr->image & 7);
if (page >= npages) return; /* страница набора не загружена */
img = (const uint8_t *)atlas_image(&pages[page], idx);
gfx_w0_map(pages[page].page);
/* ПЕРЕВОРОТ (зелье инверсии): тот же атлас, но страница с вертикально
* зеркальными пикселями — смещения лент и размеры кадров у копии те же,
* поэтому меняется ровно номер страницы (pop_vflip.h). Если EMM не
* дала страницу — рисуем обычной: неперевёрнутый кадр лучше дыры. */
{
uint8_t vpg = pages[page].page;
if (pop_upside) {
uint8_t f = pop_vflip_page(vpg);
if (f) vpg = f;
}
gfx_w0_map(vpg);
}
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
dp = gfx_get_draw_page() & 1;
@@ -534,13 +546,24 @@ void pop_char_draw(uint8_t who) __banked
bx = obj_x;
if (flip) bx -= (int)w;
top = obj_y - (int)h + 1;
/* Поле переворачивается вокруг своей середины: строка t уходит в
* (POP_PLAYFIELD_H - 1 - t), значит верх кадра [top, top+h) —
* в 192 - top - h, что при top = obj_y-h+1 даёт ровно 191 - obj_y.
* Пиксели уже зеркальны (страница выше), поэтому дальше всё
* считается как обычно: «пропустить skip строк сверху» у зеркальной
* ленты означает «срезать снизу» у оригинала — ровно то, что нужно. */
if (pop_upside) top = (POP_PLAYFIELD_H - 1) - obj_y;
vis_w = w;
lskip = 0;
skip = 0;
/* clip_char (seg006:1231/1749): тайл над головой — стена/пол → всё
* выше линии полосы своего ряда НЕ рисуется, иначе персонаж
* просвечивает сквозь щели кладки при спуске/висе. */
ct = pop_clip_char_top(fp_x, obj_y, w, h);
/* clip_char в перевёрнутом виде должен резать СНИЗУ, а не сверху
* (тайл над головой стал тайлом под ногами) — отдельная задача
* L9-CLIPCHAR; пока в перевороте не режем вовсе: артефакт мельче,
* чем срезанный не с той стороны персонаж. */
ct = pop_upside ? -1000 : pop_clip_char_top(fp_x, obj_y, w, h);
if (ct > top) {
skip = ct - top;
if (skip > (int)h) skip = (int)h; /* обрезан целиком */
+13 -1
View File
@@ -20,6 +20,8 @@
#include "pop_guard.h" /* CHARID_* — клинок есть и у стража */
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
#include "_pop_draw.h" /* pop_onscreen_cols */
#include "pop_map.h" /* pop_upside — зелье инверсии */
#include "pop_vflip.h" /* зеркальные страницы атласов */
#include "kid_data.h"
/* chtab_0 «меч в руке» (add_sword_to_objtable, seg006:1798): клинок —
@@ -66,7 +68,14 @@ uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr,
if (id == 0xFF) return 0;
simg = (const uint8_t *)atlas_image(&swordp, id);
gfx_w0_map(swordp.page);
{ /* ПЕРЕВОРОТ: зеркальная копия страницы меча (см. pop_vflip.h) */
uint8_t vpg = swordp.page;
if (pop_upside) {
uint8_t f = pop_vflip_page(vpg);
if (f) vpg = f;
}
gfx_w0_map(vpg);
}
w = (uint16_t)(simg[0] | ((uint16_t)simg[1] << 8));
h = (uint16_t)(simg[2] | ((uint16_t)simg[3] << 8));
if (!w || !h) return 0;
@@ -74,6 +83,9 @@ uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr,
flip = (uint8_t)(ch->direction >= 0);
bx = obj_x + ((ch->direction < 0) ? -(int)SWORD_DX[sf] : (int)SWORD_DX[sf]);
top = obj_y + (int)SWORD_DY[sf] - (int)h + 1;
/* Та же формула, что у самого персонажа (pop_cdraw.c): верх кадра
* [top, top+h) уезжает в 192 - top - h. */
if (pop_upside) top = POP_PLAYFIELD_H - top - (int)h;
if (flip) bx -= (int)w;
/* Поле комнаты — 192 строки (reset_obj_clip, seg006:0507): клинок
* обрезается по нему так же, как сам персонаж. Без этого у падающего
+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;
}
+37
View File
@@ -0,0 +1,37 @@
/*
* pop_vflip.h — зеркальные по вертикали копии страниц атласов персонажей
* (зелье инверсии, уровень 9). Реализация и «почему постранично» —
* в pop_vflip.c.
*
* Кому нужно. Фон и все row-major спрайты переворачиваются даром (порядок
* строк = знак страйда, gfx_blit_*_vflip), а персонажи хранятся колонками —
* им нужен заранее развёрнутый набор пикселей. Отсюда правило вызывающего:
*
* страница = pop_upside ? pop_vflip_page(a->page) : a->page;
*
* и больше НИЧЕГО не меняется: смещения лент, размеры кадров, флип по
* горизонтали (знак страйда) остаются как были.
*
* Почему кэш не сбрасывается на каждом перевороте: зелье инверсии в
* оригинале переключает состояние туда-обратно (второе зелье, смерть Кида —
* seg000:1224), и держать 34 страницы EMM дешевле, чем перезеркаливать по
* 16 КБ при каждом переключении. Бюджет EMM это позволяет: свободно ~215
* страниц из 256 (memory sprinter_emm_budget). Освобождаем на смене
* уровня — вместе с самими атласами.
*/
#ifndef POP_VFLIP_H
#define POP_VFLIP_H
#include <stdint.h>
/* Номер страницы с ВЕРТИКАЛЬНО ЗЕРКАЛЬНОЙ копией атласа src. Первый вызов
* для страницы делает копию (~16 КБ побайтно — ощутимо, но однократно),
* дальше отдаёт из кэша. 0 — не хватило EMM-страниц (вызывающий должен
* нарисовать обычным атласом: лучше неперевёрнутый кадр, чем пустота). */
uint8_t pop_vflip_page(uint8_t src);
/* Освободить все копии (смена уровня). Атласы к этому моменту тоже
* перезагружаются, и старые копии стали бы мусором. */
void pop_vflip_reset(void);
#endif /* POP_VFLIP_H */
@@ -22,6 +22,8 @@
#include "pop_cdraw.h" /* pop_cd — инвалидация слотов после переворота */
#include <sprite.h> /* gfx_copy_page */
#include "pop_tune.h"
#include "pop_ctrl.h" /* pop_ctrl_init/close — ввод поднимает и закрывает boot */
#include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */
#include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */
#include <stdio.h> /* puts/printf — сообщения об ошибках загрузки */
#include <kbd_raw.h>
@@ -622,6 +624,9 @@ int pop_level_switch(void) __banked
* пределы, и персонаж просто не виден. */
pop_guard_free();
pop_guard_load();
/* Зеркальные копии атласов сделаны из СТАРЫХ страниц —
* после перезагрузки спрайтов они мусор (pop_vflip.h). */
pop_vflip_reset();
/* Тайлсет: подземелье <-> дворец (порт tbl_envir_ki,
* seg000:1108). Внутри — no-op, если тип не менялся;
* при смене выгружает старые атласы и правит 32 записи
+18
View File
@@ -240,3 +240,21 @@ end
- **Второй видеорежим/варианты BIOS** — при необходимости менять
`COMMON_ARGS`.
```
## 10. Тайминги MCP-моста (run_bridge.sh) — НЕ ждать дольше
Запуск через `mame/v306/run_bridge.sh` + `bridge_cmd.sh` / MCP `mame-z80`.
Паузы ниже — измеренные на этой машине; ждать дольше бессмысленно, а
привычка ставить `sleep 30..60` съедает минуты на каждый прогон:
| шаг | пауза ПОСЛЕ шага |
|-----|------------------|
| запустили `run_bridge.sh` | **6 с** → машина поднялась, можно слать `go` |
| `go` (снять с дебаггерного стопа) | **8 с** → DSS догрузился, принимает ввод |
| `keyseq d:{ENTER}` + `keyseq roomtest{ENTER}` | **5 с** → программа уже стартовала |
То есть весь цикл «перезапуск + старт теста» укладывается в ~20 секунд.
Пересобрал HDD-образ (`make hdd`) → MAME **обязан** полный рестарт
(`chdman -f` даёт новый inode; см. memory `mame_hdd_rebuild_restart`):
остановка через `exit` в дебаггере, затем `run_bridge.sh` заново.