diff --git a/applications/PoP/roomtest/CLAUDE.md b/applications/PoP/roomtest/CLAUDE.md index 716f150..395da68 100644 --- a/applications/PoP/roomtest/CLAUDE.md +++ b/applications/PoP/roomtest/CLAUDE.md @@ -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** — продолжить (разбор позы/окклюзии); diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index 1661b5a..6934e28 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -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) diff --git a/applications/PoP/roomtest/pop_cdraw.c b/applications/PoP/roomtest/pop_cdraw.c index 96f1022..7945bdc 100644 --- a/applications/PoP/roomtest/pop_cdraw.c +++ b/applications/PoP/roomtest/pop_cdraw.c @@ -20,7 +20,8 @@ #include #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; /* обрезан целиком */ diff --git a/applications/PoP/roomtest/pop_kdraw.c b/applications/PoP/roomtest/pop_kdraw.c index bab4f21..f070810 100644 --- a/applications/PoP/roomtest/pop_kdraw.c +++ b/applications/PoP/roomtest/pop_kdraw.c @@ -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): клинок * обрезается по нему так же, как сам персонаж. Без этого у падающего diff --git a/applications/PoP/roomtest/pop_vflip.c b/applications/PoP/roomtest/pop_vflip.c new file mode 100644 index 0000000..4f97bf4 --- /dev/null +++ b/applications/PoP/roomtest/pop_vflip.c @@ -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 +#include +#include +#include +#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; +} diff --git a/applications/PoP/roomtest/pop_vflip.h b/applications/PoP/roomtest/pop_vflip.h new file mode 100644 index 0000000..a0f6687 --- /dev/null +++ b/applications/PoP/roomtest/pop_vflip.h @@ -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 + +/* Номер страницы с ВЕРТИКАЛЬНО ЗЕРКАЛЬНОЙ копией атласа src. Первый вызов + * для страницы делает копию (~16 КБ побайтно — ощутимо, но однократно), + * дальше отдаёт из кэша. 0 — не хватило EMM-страниц (вызывающий должен + * нарисовать обычным атласом: лучше неперевёрнутый кадр, чем пустота). */ +uint8_t pop_vflip_page(uint8_t src); + +/* Освободить все копии (смена уровня). Атласы к этому моменту тоже + * перезагружаются, и старые копии стали бы мусором. */ +void pop_vflip_reset(void); + +#endif /* POP_VFLIP_H */ diff --git a/applications/PoP/roomtest/roomtest_cold.c b/applications/PoP/roomtest/roomtest_cold.c index 882c812..f534dcc 100644 --- a/applications/PoP/roomtest/roomtest_cold.c +++ b/applications/PoP/roomtest/roomtest_cold.c @@ -22,6 +22,8 @@ #include "pop_cdraw.h" /* pop_cd — инвалидация слотов после переворота */ #include /* 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 /* puts/printf — сообщения об ошибках загрузки */ #include @@ -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 записи diff --git a/docs/mame-autotest.md b/docs/mame-autotest.md index ff30364..7791caf 100644 --- a/docs/mame-autotest.md +++ b/docs/mame-autotest.md @@ -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` заново.