From 65821543815e0fd566afbd0edd1e8b9e82d758a4 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Tue, 28 Jul 2026 09:50:47 +0300 Subject: [PATCH] =?UTF-8?q?PoP=20roomtest:=20clip=5Fchar=20(=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=85=D0=BD=D1=8F=D1=8F=20=D0=BE=D0=B1=D1=80=D0=B5=D0=B7?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=81=D0=BF=D1=80=D0=B0=D0=B9=D1=82=D0=B0)=20+?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=B4=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BD=D0=B0=D0=B6=D0=B0=D1=82=D0=BE=D0=B9=20=D0=BA?= =?UTF-8?q?=D0=BD=D0=BE=D0=BF=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Спуск Кида с кнопки (room8, кромка (0,6)) рисовался неверно: Кид просвечивал в щель между кнопкой и ближним столбом, а ближняя рука была срезана до одного оторванного пикселя. Две независимые причины. 1. Не был портирован clip_char() (seg006:1749) — оригинал перед add_objtable обрезает спрайт персонажа сверху по y_clip[curr_row+1], когда тайл над головой стена или пол. Порт: pop_clip_char_top() (pop_map.c, метрики по set_char_collision) + новый примитив gfx_blit_cols_part() в libbgi (блит column-major с пропуском skip верхних строк; обрезка сверху бесплатна — колонка непрерывна в ОЗУ, сдвигается только старт). gfx_blit_cols стал тонкой обёрткой над ним. kid_heal чистит уже ОБРЕЗАННЫЙ прямоугольник, иначе стирается кромка пола над срезом. 2. climb_overlay_tile (порт draw_floor_overlay, seg008:1E3A) выбирал ветку по СЫРОМУ коду тайла, а get_tile_to_draw (seg008:240) подменяет нажатую кнопку на floor/stuck. Тайл-кнопка не проходил тест floor → уходил в draw_other_overlay, который кладёт поверх Кида ВЕСЬ тайл вместо узкой кромки floor_left_overlay[frame-137]. Фикс — tile_code_drawn(): одна подстановка на все слои (fore_only_tile/overlay_mid_tile перестали её дублировать, W3 −293 Б). Проверено покадрово в MAME (шаг gv + снимок): кадры спуска 148..138 чистые, после приземления на кнопке мусора нет. Заодно: gfx_heal_noclip() (пара к gfx_blit_noclip; heal 11658 -> 8982 тактов) и rest_pending в pop_trob — холостой проход по 30 тайлам только когда есть отложенные редрои пик/кнопок (pop_process_trobs 89-111К -> 70-92К тактов). START_ROOM временно = 8 (отладка спуска с кнопки), вернуть на 6/старт уровня. Co-Authored-By: Claude Opus 5 --- applications/PoP/docs/clip_char_plan.md | 112 ++++++++++++++++++++++++ applications/PoP/roomtest/bug_list.md | 13 +++ applications/PoP/roomtest/pop_bg.c | 58 +++++++++--- applications/PoP/roomtest/pop_kid.c | 23 ++++- applications/PoP/roomtest/pop_map.c | 48 ++++++++++ applications/PoP/roomtest/pop_map.h | 7 ++ applications/PoP/roomtest/pop_trob.c | 21 +++-- applications/PoP/roomtest/roomtest.c | 4 +- libbgi/common/gfx_blit_cols.c | 40 +-------- libbgi/common/gfx_blit_cols_part.c | 62 +++++++++++++ libbgi/common/gfx_heal_noclip.c | 30 +++++++ libbgi/include/gfx.h | 13 +++ 12 files changed, 369 insertions(+), 62 deletions(-) create mode 100644 applications/PoP/docs/clip_char_plan.md create mode 100644 libbgi/common/gfx_blit_cols_part.c create mode 100644 libbgi/common/gfx_heal_noclip.c diff --git a/applications/PoP/docs/clip_char_plan.md b/applications/PoP/docs/clip_char_plan.md new file mode 100644 index 0000000..61e2682 --- /dev/null +++ b/applications/PoP/docs/clip_char_plan.md @@ -0,0 +1,112 @@ +# План: порт `clip_char()` — обрезка спрайта персонажа + +Статус: в работе с 2026-07-28. Контекст: баг «спуск Кида с кнопки в комнате 8» +(Kid просвечивает в щель между кнопкой и ближним столбом, мусор на кромке). + +## Симптом + +room 8, Kid спускается (climbdown) с тайла-кнопки у ближней колонны. Спрайт +Кида нарисован ЦЕЛИКОМ, включая часть, которая в оригинале обрезана по линии +пола: видно «просвет» Кида в щели между кнопкой и колонной и мусор на кромке. + +## Корень + +Не портирован `clip_char()` (SDLPoP `seg006.c:1749`, вызывается из +`add_kid_to_objtable`/`add_guard_to_objtable`, `seg008.c:1671/1690`, ПОСЛЕ +`set_char_collision`/`set_objtile_at_char`/`redraw_at_char*` и ПЕРЕД +`add_objtable`). Оригинал кладёт в objtable не только позицию спрайта, но и +прямоугольник клипа `obj_clip_{top,bottom,left,right}`; блиттер рисует только +внутри него. Мы рисуем без клипа вообще. + +## Что делает оригинал (дословно) + +`reset_obj_clip()` → `left=0, top=0, right=320, bottom=192`. + +Дальше (упрощая C-трюк с глобалью `curr_tile2`, которую ставит каждый +`get_tile`: `X == wall || tile_is_floor(curr_tile2)` — это «тайл X = стена +ИЛИ пол»): + +``` +T_L = get_tile(room, char_col_left, char_top_row) +T_R = get_tile(room, char_col_right, char_top_row) +if (T_L — стена или пол) && + ( (action == stand && (frame == 79 || frame == 81)) /* прыжок вверх / зацеп */ + || (T_R — стена или пол) ) +{ + clip_row = Char.curr_row + 1; + clip_y = y_clip[clip_row]; /* y_clip[] = {-60, 3, 66, 129, 192} */ + if (clip_row == 1 || (clip_y < obj_y && clip_y - 15 < char_top_y)) + obj_clip_top = char_top_y = clip_y; +} +``` + +Смысл: `y_clip[row+1]` — верхняя граница СВОЕЙ полосы ряда. Если над головой +пол/стена — всё, что выше этой линии, не рисуется (персонаж «уходит под пол»). +Для ряда 0 (`clip_row == 1`) клип применяется БЕЗУСЛОВНО. + +Метрики из `set_char_collision` (seg006:0723): +- `char_x_left = obj_x/2 + 58` (и `-= char_width_half`, если смотрит вправо) +- `char_x_right = char_x_left + char_width_half`, `char_width_half = (w+1)/2` +- `char_top_y = obj_y - h + 1`; если `>= 192` → `0` +- `char_top_row = y_to_row_mod4(char_top_y)` +- `char_col_left = MAX(get_tile_div_mod(char_x_left), 0)`, + `char_col_right = MIN(get_tile_div_mod(char_x_right), 9)` + +Второй блок `clip_char` — `obj_clip_right` (doortop / стена / зеркало для +виса-полёта-подъёма при взгляде влево, кадры 137..139) и ветка кадров 224..228 +(выход в дверь уровня). **Фаза 2**, не в этом заходе. + +## Наши точки касания + +- `roomtest/pop_kid.c` `kid_draw()` — сам считает `obj_x/obj_y/w/h` и блитит + через `gfx_blit_cols(bx, top + POP_YOFF, img, flip)`; запоминает + прямоугольник в `kid_l{x,y,w,h}[page]` для `kid_heal()`. +- `roomtest/pop_map.c` — там `get_tile()`, `tile_is_floor()`, `y_to_row()`, + `get_tile_div_mod_m7()`, Kid-структура. Аналогичный расчёт габарита уже + есть в `check_spike_below()` (строка ~1198) — брать за образец. +- `libbgi/common/gfx_blit_cols.c` — клип по экрану есть (при `y < 0` + пропускает `sy` верхних строк колонки), но произвольной верхней границы нет. + +## Шаги + +1. **libbgi**: `gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip, + int sy, int h)` — «пропустить `sy` верхних строк спрайта, нарисовать `h`». + Реализация = тело `gfx_blit_cols` с предустановленными `sy`/`h` (источник + `+= sy`, экранный `y += sy`). Прототип в `libbgi/include/gfx.h`. + Стоимость: 0 в общем пути (обычный `gfx_blit_cols` вызывает то же ядро). +2. **pop_map.c**: `int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, + uint16_t h)` — порт первого блока `clip_char`; возвращает КОМНАТНЫЙ y + (0 = клипа нет). Экспорт в `pop_map.h`. +3. **pop_kid.c**: в `kid_draw()` после расчёта `top` — + `ct = pop_clip_char_top(...)`; если `ct > top` → `sy = ct - top`, + рисовать `gfx_blit_cols_part(bx, ct + POP_YOFF, img, flip, sy, h - sy)`; + в `kid_l*[page]` класть ОБРЕЗАННЫЙ прямоугольник (иначе heal чистит лишнее + и стирает кромку пола). То же для `kid_draw_splash` — там оригинал делает + `reset_obj_clip()`, т.е. splash НЕ клипится (ничего не менять). +4. **Сборка**: `make -C libbgi`, `make -C applications/PoP/roomtest`, + `make size-check`; вывести свободное место в W2/W3 (порог 512 Б). +5. **Проверка в MAME** (`mame_hdd_test_disk`, полный рестарт после + пересборки образа): комната 6 → переход в 8 → влезть на кнопку → спуск. + Сверять с эталоном: живой SDLPoP той же позой (см. `pop_check_sdlpop_first`). + +## Риски / что проверить отдельно + +- `char_top_row` считается `y_to_row_mod4` — у нас `y_to_row()` даёт −1 для + полосы у потолка; `get_tile(row −1)` уже умеет ряд 2 верхнего соседа. +- Kid ниже комнаты (`char_top_y >= 192` → `0`) — обязателен ресет, иначе клип + прыгнет. +- `clip_row == 1` (ряд 0) — клип БЕЗУСЛОВНЫЙ: проверить, что не режет Кида в + обычной стойке на ряду 0 (условие внешнего `if` про пол/стену над головой + должно отсекать). +- Клип меняет прямоугольник heal → возможен «хвост» на второй странице + дабл-буфера: проверять оба кадра (SPACE — выключить дабл-буфер). + +## Дальше (Фаза 2, отдельно) + +`obj_clip_right` (doortop/стена/зеркало) — нужен для виса и подъёма при +взгляде влево; и `obj_clip_left` для зеркала (уровень 4). Требует клипа по +колонкам в `gfx_blit_cols` (обрезка справа = уменьшить `w`) — дёшево, но +без тестовой сцены проверять нечем. + +См. memory: `pop_clip_char_todo`, `pop_backtable_vs_midtable`, +`pop_check_sdlpop_first`, `gfx_blit_noclip_fast`. diff --git a/applications/PoP/roomtest/bug_list.md b/applications/PoP/roomtest/bug_list.md index 953318d..4b8dd18 100644 --- a/applications/PoP/roomtest/bug_list.md +++ b/applications/PoP/roomtest/bug_list.md @@ -94,6 +94,19 @@ openness рисует `bar(BLACK)` по всему col0 + **полный `draw_t ## Исправлено +- **Спуск с кнопки (room8, кромка (0,6)): Кид просвечивал в щель, ближняя рука + срезана до одного пикселя** — ИСПРАВЛЕНО 2026-07-28. Две причины: + (а) не был портирован `clip_char()` (seg006:1749) — верхняя обрезка спрайта по + `y_clip[curr_row+1]`, когда тайл над головой стена/пол; сделано + (`pop_clip_char_top` в pop_map.c + `gfx_blit_cols_part` в libbgi, heal чистит + уже обрезанный прямоугольник); + (б) `climb_overlay_tile` выбирал ветку `draw_floor_overlay` (seg008:1E3A) по + СЫРОМУ коду тайла — а нажатая кнопка в `get_tile_to_draw` (seg008:240) + подменяется на floor/stuck. Тайл-кнопка не проходил тест floor, уходил в + `draw_other_overlay` и закрашивал Kid ЦЕЛЫМ тайлом вместо узкой кромки + `floor_left_overlay[frame-137]`. Фикс — `tile_code_drawn()` (одна подстановка + на все слои). Проверено покадрово в MAME (кадры 148..138). + - **Шов ворот жёг 50% кадра в покое (закрытая решётка)** — ИСПРАВЛЕНО 2026-07-22. Причина — **баг кодогенератора SDCC z80** (memory `sdcc_z80_cmp_store_a_bug`): `if (m[9] != seam_sig) seam_sig = m[9];` diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c index 85a0ec4..59426f5 100644 --- a/applications/PoP/roomtest/pop_bg.c +++ b/applications/PoP/roomtest/pop_bg.c @@ -91,7 +91,18 @@ static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) } /* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose. */ -static void heal_off(int x, int y, int w, int h) { gfx_heal(x, y + POP_YOFF, w, h); } +static void heal_off(int x, int y, int w, int h) +{ + int sy = y + POP_YOFF; + /* Без клипа — линейным ядром (~2.9×, как gfx_blit_noclip): общий + * gfx_heal платит за клип/16-бит/split, а не за пиксели (замер: + * 11 658 тактов на heal 22×22). */ + if (x >= 0 && sy >= 0 && w > 0 && h > 0 && w < 256 && h < 256 && + x + w <= 320 && sy + h <= 256) + gfx_heal_noclip(x, sy, (uint8_t)w, (uint8_t)h); + else + gfx_heal(x, sy, w, h); +} /* Область, куда в этом кадре рисовался ОВЕРЛЕЙ (не футпринт fore, а * draw_other_overlay/floor_overlay/полоса потолка). Оверлей включается @@ -271,6 +282,20 @@ static uint8_t tile_mod(int row, int col) return g_bg[row * 10 + col]; } +/* Тип тайла «как он РИСУЕТСЯ» — порт подстановки нажатой кнопки из + * get_tile_to_draw (seg008:240): opener → floor, closer → stuck, когда таймер + * связи > 1. Любой слой, который выбирает ветку отрисовки по типу тайла, + * обязан смотреть на ПОДСТАВЛЕННЫЙ тип (иначе кнопка, на которой стоит Kid, + * ведёт себя как «не пол» — напр. climb-оверлей уходил в draw_other_overlay + * и закрашивал Kid целым тайлом вместо узкой кромки floor_left_overlay). */ +static uint8_t tile_code_drawn(int row, int col) +{ + uint8_t code = tile_code(row, col); + if (code == 0x0F) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 1; } + else if (code == 0x06) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 5; } + return code; +} + /* seg008.c:1255 (без FAKE_TILES): сосед — стена, если его тип == 20. * На кромке col0 сосед по умолчанию = стена (как render_room). */ static uint8_t wall_modifier(int row, int col) @@ -682,7 +707,7 @@ void pop_torch_draw(int row, int col, uint8_t modif) int x = (col + 1 < 10 ? COL_XH[col + 1] : COL_XH[col] + 4) * 8 + 8; int yb = 63 * row + 62 - 40; if (modif >= 9) return; - heal_off(x - 2, yb - 19, 22, 22); + heal_off(x - 1, yb - 18, 18, 19); gfx_set_bank(GFX_BANK_SPRITE); pot_b((uint8_t)(modif + 1), x, yb); gfx_set_bank(GFX_BANK_NORMAL); @@ -988,12 +1013,10 @@ static int8_t y_to_row(int y) /* y_to_row_mod4 (seg006, TILE_SIZEY= static void fore_only_tile(int row, int col) { - uint8_t code = tile_code(row, col); - const piece *t; /* Нажатая кнопка → пол/stuck (get_tile_to_draw), иначе поверх Kid лезет * передняя грань КНОПКИ. */ - if (code == 0x0F) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 1; } - else if (code == 0x06) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 5; } + uint8_t code = tile_code_drawn(row, col); + const piece *t; t = &tile_table[code]; int x = COL_XH[col] * 8; int dmy = 63 * row + 62; @@ -1055,13 +1078,24 @@ static void fore_tile(int row, int col) /* передний слой одно * 137..144 индексирует floor_left_overlay. */ static const uint8_t FLOOR_LEFT_OVERLAY[8] = {32, 151, 151, 150, 150, 151, 32, 32}; +static void other_overlay_tile(int row, int col); /* fwd (draw_floor_overlay -> else) */ + static void climb_overlay_tile(int row, int col, uint8_t fidx) { - uint8_t code = tile_code(row, col); + /* тип — ПОДСТАВЛЕННЫЙ (нажатая кнопка = пол): иначе кромка, за которую + * Kid спускается/подтягивается, закрашивается целым тайлом */ + uint8_t code = tile_code_drawn(row, col); int x, dmy, dby; const piece *t; - if (code != 1 && code != 3 && code != 5 && code != 19) return; /* не floor-тайл */ if (tile_code(row, col - 1) != 0) return; /* слева не пусто → не кромка */ + if (code != 1 && code != 3 && code != 5 && code != 19) { + /* seg008:1E3A draw_floor_overlay: floor-оверлей рисуется только для + * floor/pillar/stuck/torch, а для ОСТАЛЬНЫХ тайлов вызывается + * draw_other_overlay() — иначе кнопка (opener/closer), за кромку + * которой Kid спускается, не перекрывает его. */ + other_overlay_tile(row, col); + return; + } x = COL_XH[col] * 8; dmy = 63 * row + 62; dby = dmy + 3; @@ -1082,16 +1116,12 @@ static void climb_overlay_tile(int row, int col, uint8_t fidx) * draw_tile_base, draw_tile_anim (свои пики) и draw_tile_bottom. */ static void overlay_mid_tile(int row, int col) { - uint8_t code = tile_code(row, col); - uint8_t lcode = tile_code(row, col - 1); + uint8_t code = tile_code_drawn(row, col); + uint8_t lcode = tile_code_drawn(row, col - 1); const piece *t; int x = COL_XH[col] * 8; int dby = 63 * row + 65; int dmy = dby - 3; - if (code == 0x0F) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 1; } - else if (code == 0x06) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 5; } - if (lcode == 0x0F) { if ((pop_doorlink2(tile_mod(row, col - 1)) & 0x1F) > 1) lcode = 1; } - else if (lcode == 0x06) { if ((pop_doorlink2(tile_mod(row, col - 1)) & 0x1F) > 1) lcode = 5; } t = &tile_table[code]; ov_mark(x, 63 * row + 2, 64, 64); /* тайл + правый сосед (грани) */ if (code != 20 && lcode == 1) /* draw_tile_right: только 42 */ diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index 098bd87..c725e94 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -7,6 +7,7 @@ #include #include "pop_kid.h" #include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */ +#include "pop_map.h" /* pop_clip_char_top (clip_char) */ #include "kid_data.h" kid_t Kid; @@ -292,18 +293,32 @@ void kid_draw(void) if (w && h) { uint8_t flip = (Kid.direction >= 0); /* dir_0_right -> зеркало */ int bx = obj_x + KID_XOFF; + int skip = 0; /* строк спрайта срезано сверху */ + int ct; if (flip) bx -= (int)w; /* seg008: xpos -= image->w */ top = obj_y - (int)h + 1 + KID_YOFF; uint8_t dp = gfx_get_draw_page(); /* в какую страницу рисуем */ + /* clip_char (seg006:1749): тайл над головой = стена/пол → всё выше + * линии полосы своего ряда НЕ рисуется. Иначе Kid просвечивает + * сквозь щели кладки при спуске/висе. */ + ct = pop_clip_char_top(kid_fp_x, obj_y, w, h); + if (ct > top) { + skip = ct - top; + if (skip >= (int)h) skip = (int)h; /* обрезан целиком */ + } gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */ - gfx_blit_cols(bx, top + POP_YOFF, img, flip); /* +YOFF: центрирование */ + if (skip < (int)h) /* +YOFF: центрирование */ + gfx_blit_cols_part(bx, top + skip + POP_YOFF, img, flip, skip, 0); gfx_set_bank(GFX_BANK_NORMAL); /* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) — * узко, только по прямоугольнику Кида и только при выходе. */ - pop_clip_sprite(bx, w, top + POP_YOFF, (int)h); - kid_lx[dp] = bx; kid_ly[dp] = top; - kid_lw[dp] = w; kid_lh[dp] = h; kid_lvalid[dp] = 1; + pop_clip_sprite(bx, w, top + skip + POP_YOFF, (int)h - skip); + /* heal чистит ТОЛЬКО нарисованное: иначе стирается кромка пола над + * срезом (мусор/дыра в кладке на второй странице дабл-буфера). */ + kid_lx[dp] = bx; kid_ly[dp] = top + skip; + kid_lw[dp] = w; kid_lh[dp] = (uint16_t)((int)h - skip); + kid_lvalid[dp] = (skip < (int)h); kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */ } gfx_w0_unmap(); diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index b58629d..57750aa 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -1050,6 +1050,54 @@ void pop_loose_tick(void) /* y_to_row_mod4 (seg006): ряд по y с оборотом mod4 (TILE_SIZEY=63). */ static int8_t y_to_row(int16_t y) { return (int8_t)(((y + 60) / 63) % 4 - 1); } +/* ---- clip_char (seg006:1749), первый блок: обрезка спрайта СВЕРХУ ----- * + * Оригинал перед add_objtable кладёт в objtable прямоугольник клипа; блиттер + * рисует персонажа только внутри него. Верхняя граница: если тайл НАД + * головой (ряд char_top_row) — стена или пол, всё выше линии своей полосы + * (y_clip[curr_row+1]) не рисуется. Без этого Kid, ушедший головой под пол + * (спуск с кромки/кнопки, вис), просвечивает сквозь щели кладки. + * + * Хитрость исходника: `X == wall || tile_is_floor(curr_tile2)` — curr_tile2 + * ставит сам get_tile, т.е. это просто «тайл X = стена ИЛИ пол». + * + * Метрики габарита — порт set_char_collision (seg006:0723); в отличие от + * check_spike_below колонки берутся get_tile_div_mod БЕЗ −7. + * + * Возврат: КОМНАТНЫЙ y верхней границы (0 = клипа нет). */ +int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) +{ + static const int16_t y_clip[5] = { -60, 3, 66, 129, 192 }; + int wh = ((int)w + 1) >> 1; /* char_width_half */ + int xl = obj_x / 2 + 58; /* char_x_left */ + int xr, top_y, clip_y; + int8_t cL, cR, trow, clip_row; + uint8_t t; + + if (Kid.direction >= 0) xl -= wh; /* лицом вправо: левый край внутрь */ + xr = xl + wh; /* char_x_right */ + top_y = obj_y - (int)h + 1; /* char_top_y */ + if (top_y >= 192) top_y = 0; /* весь спрайт ниже комнаты */ + trow = y_to_row((int16_t)top_y); /* char_top_row */ + cL = get_tile_div_mod(xl); if (cL < 0) cL = 0; + cR = get_tile_div_mod(xr); if (cR > 9) cR = 9; + + t = get_tile(cL, trow); + if (t != TILE_WALL && !tile_is_floor(t)) return 0; + /* кадр 79 (прыжок вверх) / 81 (зацеп) в стойке клипятся и по одной грани */ + if (!(Kid.action == 0 && (Kid.frame == 79 || Kid.frame == 81))) { + t = get_tile(cR, trow); + if (t != TILE_WALL && !tile_is_floor(t)) return 0; + } + + clip_row = (int8_t)(Kid.curr_row + 1); + if (clip_row < 0 || clip_row > 4) return 0; + clip_y = y_clip[clip_row]; + /* ряд 0 — клип безусловный; иначе только когда линия реально режет спрайт */ + if (clip_row == 1 || (clip_y < obj_y && clip_y - 15 < top_y)) + return clip_y; + return 0; +} + /* check_leave_below (порт leave_room, seg002: leave_dir=3 при chary>=211): * Kid пересёк нижнюю границу комнаты (провал в дыру ИЛИ спуск-зацеп/climbdown * — единый триггер по y). Репроекция в кадр комнаты снизу (goto_other_room diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h index 036c5bf..1b93f9c 100644 --- a/applications/PoP/roomtest/pop_map.h +++ b/applications/PoP/roomtest/pop_map.h @@ -93,6 +93,13 @@ extern uint8_t pop_fell_out; * Порядок как в оригинале (seg000 play_kid_frame). */ void pop_phys_tick(void); +/* clip_char (seg006:1749), верхняя граница клипа спрайта персонажа: если тайл + * над головой — стена/пол, всё выше линии своей полосы не рисуется (Kid, + * ушедший головой под пол при спуске/висе, не просвечивает сквозь щели). + * obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ спрайта, w/h — размер. + * Возврат: КОМНАТНЫЙ y границы, 0 = клипа нет. Зовёт kid_draw. */ +int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h); + /* Тип препятствия/края ВПЕРЕДИ по направлению (pop_edge_type после * pop_edge_distance): для control (бежать / осторожный шаг / стоп у стены). */ #define EDGE_CLOSER 0 /* край уступа над ямой (дистанция = до кромки) */ diff --git a/applications/PoP/roomtest/pop_trob.c b/applications/PoP/roomtest/pop_trob.c index 720526c..685f1c9 100644 --- a/applications/PoP/roomtest/pop_trob.c +++ b/applications/PoP/roomtest/pop_trob.c @@ -41,6 +41,12 @@ static uint8_t button_rest[POP_ROOMTILES]; * расхождение было именно в безусловном редрое). */ static uint8_t trob_drawn[POP_ROOMTILES]; +/* Сколько тайлов ждут отложенного редроя (spike_rest/button_rest). Без + * него цикл по 30 тайлам крутился КАЖДЫЙ кадр впустую: замер в MAME + * (2026-07-27) — служебная часть pop_process_trobs ~50 000 тактов при + * кадре 430 080, то есть ~12 % кадра на пустой проход. */ +static uint8_t rest_pending; + /* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и * выбора следующего кадра пламени. */ static unsigned long trob_seed; @@ -57,6 +63,7 @@ void pop_trob_reset(void) uint8_t i; for (i = 0; i < POP_NROOMS; i++) room_seen[i] = 0; for (i = 0; i < POP_ROOMTILES; i++) { spike_rest[i] = 0; button_rest[i] = 0; trob_drawn[i] = 0; } + rest_pending = 0; trobs_count = 0; } @@ -101,6 +108,7 @@ void pop_trob_room_changed(void) { uint8_t i; for (i = 0; i < POP_ROOMTILES; i++) { spike_rest[i] = 0; button_rest[i] = 0; trob_drawn[i] = 0; } + rest_pending = 0; } /* anim_tile_modif (seg000:0B12) — при входе в комнату завести анимации её @@ -304,19 +312,20 @@ void pop_process_trobs(uint8_t cur_room) * выносит оба деления (__divuchar/__moduchar) в вершину цикла как общее * подвыражение аргументов redraw — 30*2=60 делений/кадр даже когда нет * активных пик/кнопок (тот же случай, что был в pop_loose_tick). */ - { - uint8_t row = 0, col = 0; + if (rest_pending) { + uint8_t row = 0, col = 0, left = 0; for (i = 0; i < POP_ROOMTILES; i++) { if (spike_rest[i]) { pop_spike_redraw(row, col); - spike_rest[i]--; + if (--spike_rest[i]) left++; } if (button_rest[i]) { pop_button_redraw(row, col); - button_rest[i]--; + if (--button_rest[i]) left++; } if (++col == 10) { col = 0; row++; } } + rest_pending = left; } if (trobs_count == 0) return; @@ -398,8 +407,8 @@ void pop_process_trobs(uint8_t cur_room) : (uint8_t)(get_doorlink_timer(mod[tp]) > 1); if (sig != trob_drawn[tp]) { uint8_t trow = tp / 10, tcol = tp % 10; - if (code == TILE_SPIKE) { pop_spike_redraw(trow, tcol); spike_rest[tp] = 1; } - else { pop_button_redraw(trow, tcol); button_rest[tp] = 1; } + if (code == TILE_SPIKE) { pop_spike_redraw(trow, tcol); spike_rest[tp] = 1; rest_pending++; } + else { pop_button_redraw(trow, tcol); button_rest[tp] = 1; rest_pending++; } trob_drawn[tp] = sig; } } diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 1691c2c..14a4233 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -26,8 +26,8 @@ __sfr __at (0xFE) io_border; /* Тест-старт для отладки пик (Фаза S): комната 6 = тестовая (пики в col3/col4 * row2, шахта col3). Kid на полу row0 col1, идёт вправо → падает в шахту → * пики. TODO: вернуть на pop_level_start_room (комната 1) после отладки. */ -#define START_ROOM 6 -#define START_COL 4 +#define START_ROOM 8 /* ОТЛАДКА clip_char: спуск с кнопки (0,6) */ +#define START_COL 8 #define START_ROW 0 #define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */ #define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */ diff --git a/libbgi/common/gfx_blit_cols.c b/libbgi/common/gfx_blit_cols.c index 8f512a8..57adc7c 100644 --- a/libbgi/common/gfx_blit_cols.c +++ b/libbgi/common/gfx_blit_cols.c @@ -16,46 +16,14 @@ * ПОЛНОЙ высоте (колонки в ОЗУ лежат full_h друг от друга), длина accel- * блока — по обрезанной. Спрайты Kid < 256 по обеим осям → нарезка на * полосы >256 не нужна (raw принимает uint8_t w,h; 0 => 256). img вне W3. + * + * Тело (вместе с клипом) — в gfx_blit_cols_part.c: этот вызов = «весь кадр, + * без обрезки сверху». */ #include #include "../_bgi.h" void gfx_blit_cols(int x, int y, const void *img, uint8_t flip) { - const uint8_t *p = (const uint8_t *)img; - int full_w = p[0] | (p[1] << 8); - int full_h = p[2] | (p[3] << 8); - const uint8_t *px = p + 4; - int w = full_w, h = full_h; - int sstride = flip ? -full_h : full_h; /* шаг между колонками в ОЗУ */ - int sy = 0; /* пропуск строк сверху колонки */ - int cx = 0; /* пропуск колонок слева экрана */ - const uint8_t *src; - uint8_t *dst; - - if (full_w <= 0 || full_h <= 0) - return; - - /* Клип по экрану — только если хоть один край вылезает. */ - if (x < 0 || y < 0 || x + w > _bgi_maxx + 1 || y + h > _bgi_maxy + 1) { - if (x < 0) { cx = -x; w += x; x = 0; } - if (y < 0) { sy = -y; h += y; y = 0; } - if (x + w > _bgi_maxx + 1) w = _bgi_maxx + 1 - x; - if (y + h > _bgi_maxy + 1) h = _bgi_maxy + 1 - y; - if (w <= 0 || h <= 0) return; - } - - /* Первая ВИДИМАЯ колонка (с учётом флипа) + первая видимая строка. - * cx экранных колонок слева пропускаем шагом sstride (в флипе он - * отрицателен — уходим к более ранним колонкам источника); sy строк - * сверху — простым +sy внутри колонки (порядок строк флип не меняет). */ - src = flip ? (px + (uint16_t)(full_w - 1) * (uint16_t)full_h) : px; - if (cx) - src += cx * sstride; - src += sy; - - _bgi_begin(); - dst = (uint8_t *)(_gfx_addr_base + (uint16_t)x); - _bgi_blit_cols_raw(src, dst, (uint8_t)w, (uint8_t)h, sstride, (uint8_t)y); - _bgi_end(); + gfx_blit_cols_part(x, y, img, flip, 0, 0); } diff --git a/libbgi/common/gfx_blit_cols_part.c b/libbgi/common/gfx_blit_cols_part.c new file mode 100644 index 0000000..a12d181 --- /dev/null +++ b/libbgi/common/gfx_blit_cols_part.c @@ -0,0 +1,62 @@ +/* + * gfx_blit_cols_part — блит спрайта column-major с ОБРЕЗКОЙ СВЕРХУ: рисуются + * только строки [skip, skip+rows) кадра, и (x,y) — экранная позиция ПЕРВОЙ + * нарисованной строки (как у gfx_blit_part: координата назначения под- + * прямоугольника). rows <= 0 — «до низа кадра». + * + * Нужно для порта clip_char (PoP): персонаж, чья голова ушла под пол/стену, + * рисуется обрезанным по линии пола (obj_clip_top), а не целиком. Обрезка + * сверху для column-major бесплатна: колонка = непрерывный кусок ОЗУ, старт + * просто сдвигается на skip байт, длина accel-блока уменьшается. + * + * Здесь же живёт полное тело блита колонками — gfx_blit_cols() тонкая + * обёртка (skip=0, rows=0), чтобы не дублировать клип-логику. + * Подробности зеркала/клипа — см. шапку gfx_blit_cols.c. + */ +#include +#include "../_bgi.h" + +void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip, + int skip, int rows) +{ + const uint8_t *p = (const uint8_t *)img; + int full_w = p[0] | (p[1] << 8); + int full_h = p[2] | (p[3] << 8); + const uint8_t *px = p + 4; + int w = full_w, h; + int sstride = flip ? -full_h : full_h; /* шаг между колонками в ОЗУ */ + int sy; /* пропуск строк сверху колонки */ + int cx = 0; /* пропуск колонок слева экрана */ + const uint8_t *src; + uint8_t *dst; + + if (full_w <= 0 || full_h <= 0) + return; + if (skip < 0) skip = 0; + if (skip >= full_h) + return; /* обрезано целиком */ + sy = skip; + h = full_h - skip; + if (rows > 0 && rows < h) + h = rows; + + /* Клип по экрану — только если хоть один край вылезает. */ + if (x < 0 || y < 0 || x + w > _bgi_maxx + 1 || y + h > _bgi_maxy + 1) { + if (x < 0) { cx = -x; w += x; x = 0; } + if (y < 0) { sy -= y; h += y; y = 0; } + if (x + w > _bgi_maxx + 1) w = _bgi_maxx + 1 - x; + if (y + h > _bgi_maxy + 1) h = _bgi_maxy + 1 - y; + if (w <= 0 || h <= 0) return; + } + + /* Первая ВИДИМАЯ колонка (с учётом флипа) + первая видимая строка. */ + src = flip ? (px + (uint16_t)(full_w - 1) * (uint16_t)full_h) : px; + if (cx) + src += cx * sstride; + src += sy; + + _bgi_begin(); + dst = (uint8_t *)(_gfx_addr_base + (uint16_t)x); + _bgi_blit_cols_raw(src, dst, (uint8_t)w, (uint8_t)h, sstride, (uint8_t)y); + _bgi_end(); +} diff --git a/libbgi/common/gfx_heal_noclip.c b/libbgi/common/gfx_heal_noclip.c new file mode 100644 index 0000000..5cb5ec3 --- /dev/null +++ b/libbgi/common/gfx_heal_noclip.c @@ -0,0 +1,30 @@ +/* + * gfx_heal_noclip — восстановить прямоугольник экрана из ОЗУ-копии БЕЗ + * клипа: вызывающий гарантирует, что прямоугольник целиком на экране, + * w,h ≤ 255. Быстрый вариант gfx_heal для тайловых движков (пара к + * gfx_blit_noclip). + * + * Зачем: gfx_heal идёт через общее ядро _gfx_heal_full (клип, нарезка + * полос >256, 16-битная арифметика) — замер в MAME (PoP roomtest, + * 2026-07-27): heal области 22×22 стоил 11 658 тактов при кадре 430 080, + * т.е. цена почти вся в обвязке, а не в пикселях. Линейное спрайтовое + * ядро _gfx_heal_sprite_noclip делает ту же работу без этого груза. + * + * Банк — как у gfx_heal: на время операции ВСЕГДА GFX_BANK_NORMAL + * (чтение отдаёт ОЗУ-копию, запись должна дойти и до ОЗУ), затем + * пользовательский банк восстанавливается. W3-скобка ставится ЗДЕСЬ: + * из модуля, собранного с --w3, её вызывать нельзя (после _bgi_begin + * окно W3 занято видеобанком и код вызывающего исчезает). + */ +#include "../_bgi.h" + +void gfx_heal_noclip(int x, int y, uint8_t w, uint8_t h) +{ + uint8_t saved = gfx_get_bank(); + + gfx_set_bank(GFX_BANK_NORMAL); + _bgi_begin(); + _gfx_heal_sprite_noclip(x, y, w, h); + _bgi_end(); + gfx_set_bank(saved); +} diff --git a/libbgi/include/gfx.h b/libbgi/include/gfx.h index f5af11e..6ca16c8 100644 --- a/libbgi/include/gfx.h +++ b/libbgi/include/gfx.h @@ -94,6 +94,11 @@ void gfx_blit(int x, int y, const void *img); * пиксели). Для тайловых движков; где нужен клип — gfx_blit/gfx_blit_part. */ void gfx_blit_noclip(int x, int y, const void *img); +/* Быстрый heal БЕЗ клипа: прямоугольник обязан быть целиком на экране, + * w,h ≤ 255. Пара к gfx_blit_noclip (общее ядро тратит время на клип и + * 16-бит, а не на пиксели: замер — 11 658 тактов на heal 22×22). */ +void gfx_heal_noclip(int x, int y, uint8_t w, uint8_t h); + /* Блит под-прямоугольника (sx,sy,w,h) картинки (атлас кадров: * кадр N ленты — sx = N*FRAME_W). Под-прямоугольник обязан лежать * внутри img; по экрану — клиппится. */ @@ -107,6 +112,14 @@ void gfx_blit_part(int x, int y, const void *img, * Для персонажей (флип); фон — обычный gfx_blit (row-major). */ void gfx_blit_cols(int x, int y, const void *img, uint8_t flip); +/* То же с ОБРЕЗКОЙ СВЕРХУ: рисуются только строки [skip, skip+rows) кадра, + * (x,y) — экранная позиция ПЕРВОЙ нарисованной строки (как gfx_blit_part). + * rows <= 0 = «до низа кадра». Для персонажа, ушедшего головой под пол + * (порт clip_char/obj_clip_top в PoP). Обрезка сверху бесплатна: колонка — + * непрерывный кусок ОЗУ, сдвигается только старт. */ +void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip, + int skip, int rows); + /* Восстановить прямоугольник экрана из ОЗУ-копии (стирание спрайта/ * оверлея, нарисованного банком с битом 2: 0x54/0x5C). Всегда * работает банком GFX_BANK_NORMAL независимо от gfx_set_bank;