diff --git a/applications/PoP/roomtest/TASKS.md b/applications/PoP/roomtest/TASKS.md
index ed1c79f..a6aa8c2 100644
--- a/applications/PoP/roomtest/TASKS.md
+++ b/applications/PoP/roomtest/TASKS.md
@@ -6,10 +6,8 @@
а то, **что берём в работу сейчас и в каком порядке**. Каждая запись: что
сделать, почему именно сейчас, чем подтверждать результат.
-**Открытые баги** ([`bug_list.md`](bug_list.md)):
-[BUG-DOOR-CLIP](bug_list.md#bug-door-clip) (Кид не обрезается правым косяком
-двери уровня) + незакрытые оптимизации [T-1](bug_list.md#t-1),
-[T-2](bug_list.md#t-2) и незаконченная
+**Открытых багов нет** ([`bug_list.md`](bug_list.md)): остались незакрытые
+оптимизации [T-1](bug_list.md#t-1), [T-2](bug_list.md#t-2) и незаконченная
[таблица обхода 24 комнат](bug_list.md#обход-всех-24-комнат-уровня-1).
Правило проекта в силе: механику сверять с `../SDLPoP/src/` ДО кодинга;
@@ -373,9 +371,9 @@
захлопывает решётку за спиной. Проверено: старт даёт room 1, col 0, падение
на row 1 — как по данным уровня.
-⏳ **Остаток:** `#define ROOMNAV` пока ВКЛЮЧЁН — он нужен, чтобы попадать в
-комнату 9 для работы над [BUG-DOOR-CLIP](bug_list.md#bug-door-clip).
-Выключить перед L1-PASS.
+`#define ROOMNAV` оставлен ВКЛЮЧЁННЫМ осознанно: это наш чит, которого в
+оригинале не было, — как и `S` (выдать меч), `K`, `I`. Все они со временем
+съедутся в общий блок читов, разрешаемый в настройках (решение 2026-08-01).
### L1-EXIT. Выход с уровня (дверь уровня) — **СДЕЛАНО 2026-08-01**
Портированы: ветка двери уровня из `up_pressed` + `go_up_leveldoor`
@@ -397,8 +395,10 @@
Вывод на будущее: геометрию персонажа в этом порте меняет `pop_map` (пишет в
`Kid`), а не диспетчер.
-⏳ Косметика: [BUG-DOOR-CLIP](bug_list.md#bug-door-clip) — нет обрезки силуэта
-правым косяком проёма.
+✅ Косметика тоже закрыта: BUG-DOOR-CLIP (обрезка силуэта правым косяком
+проёма) — недоставало второй половины `clip_char` (`obj_clip_right`) и
+обрезки СПРАВА у колоночного блита. В libbgi добавлен
+`gfx_blit_cols_part_w(..., maxw)`; разбор — в [`bug_closed.md`](bug_closed.md).
### L1-TRIAGE. Ревизия багов — **ЧАСТЬ 1 СДЕЛАНА 2026-08-01**
diff --git a/applications/PoP/roomtest/bug_closed.md b/applications/PoP/roomtest/bug_closed.md
index 9b531f1..90e3a33 100644
--- a/applications/PoP/roomtest/bug_closed.md
+++ b/applications/PoP/roomtest/bug_closed.md
@@ -150,6 +150,53 @@ blueline, грани пик/loose соседа, кадр loose) **всегда**
---
+## BUG-DOOR-CLIP. Подъём по лестнице двери уровня: нет обрезки по правому косяку — **ИСПРАВЛЕН 2026-08-01**
+
+**Был симптом (найден пользователем сразу после L1-EXIT):** при подъёме по
+лестнице за дверью уровня (кадры 224..228) силуэт Кида вылезал ПРАВЕЕ правого
+косяка проёма. По высоте обрезка была корректна.
+
+**Причина — недопортированная половина `clip_char`** (seg006:1231). Для
+кадров двери оригинал ставит ДВА клипа, у нас был только первый:
+
+```c
+if (frame >= frame_224_exit_stairs_8 && frame < 229) {
+ obj_clip_top = leveldoor_ybottom + 1;
+ obj_clip_right = leveldoor_right;
+}
+```
+
+Отдельная ловушка: комментарий в SDLPoP говорит «frames 217..228», а КОД
+проверяет `>= frame_224_exit_stairs_8`, то есть **224..228**. Портировано по
+коду.
+
+**Почему именно обрезка, а не fore-слой:** створка и косяк уходят в оригинале
+ЦЕЛИКОМ в backtable (`draw_leveldoor`, все `add_backtable`), то есть рисуются
+ПОД персонажем и перекрыть его не могут. Единственный способ спрятать
+поднимающегося — срезать сам спрайт.
+
+**Фикс:**
+- libbgi: `gfx_blit_cols_part_w(..., uint8_t maxw)` — обрезка СПРАВА у
+ колоночного блита. Для column-major это ровно уменьшение числа колонок,
+ то есть внутри ядра механизм уже был (так же клипается край экрана:
+ `w = _bgi_maxx + 1 - x`), наружу не было выведено. Тело блита переехало
+ туда, `gfx_blit_cols_part` стал тонкой обёрткой (maxw=0) — тем же приёмом,
+ каким `gfx_blit_cols` уже обёрнут вокруг `gfx_blit_cols_part`. Работает и
+ при flip: первые `maxw` нарисованных колонок всегда ложатся в ЛЕВУЮ часть
+ футпринта.
+- PoP: `pop_leveldoor_right` / `pop_leveldoor_ybottom` (порт одноимённых
+ глобалов) пишет `draw_leveldoor` в `pop_state` — их читает `clip_char` из
+ другого банка; `pop_clip_char_right()` отдаёт границу, `kid_draw`
+ превращает её в `maxw` и уводит эти кадры с noclip-пути на общий.
+ `kid_lw` (прямоугольник heal) тоже сужается — стираем ровно нарисованное.
+
+**Проверено в MAME:** `pop_leveldoor_right = 176` — ровно
+`(draw_xh<<3) + 48` для двери комнаты 9; `pop_leveldoor_ybottom` = 112 у
+закрытой створки и 69 у поднятой (сходится с формулой оригинала).
+Отрисовка подтверждена пользователем на живом подъёме.
+
+---
+
## BUG-SEAM-PINGPONG (#4). Пинг-понг drawn_room у шва с закрытыми воротами — **РЕШЁН 2026-07-22**
Настоящий корень найден потиковой трассой ЖИВОГО SDLPoP 1.23
diff --git a/applications/PoP/roomtest/bug_list.md b/applications/PoP/roomtest/bug_list.md
index b8fba1e..d3708f0 100644
--- a/applications/PoP/roomtest/bug_list.md
+++ b/applications/PoP/roomtest/bug_list.md
@@ -14,63 +14,17 @@ odd-pixel арифметика `char_x`, подстановка тайла на
починена кодом ещё в июле, список просто отстал — чем именно закрыта каждая,
расписано в [`bug_closed.md`](bug_closed.md)).
+**Открытых багов нет.** Осталось только незакрытое по оптимизации и
+незаконченная ревизия комнат:
+
| ID | что | тип |
|----|-----|-----|
-| [BUG-DOOR-CLIP](#bug-door-clip) | Кид не обрезается правым косяком двери уровня | Medium |
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация |
| [T-2](#t-2) | Кид перерисовывается в покое | оптимизация |
| [обход 24 комнат](#обход-всех-24-комнат-уровня-1) | таблица заполнена на 5 строк из 24 | ревизия |
---
-
-## BUG-DOOR-CLIP. Подъём по лестнице двери уровня: нет обрезки по правому косяку — **Medium**
-
-**Симптом (2026-08-01, найден пользователем сразу после L1-EXIT):** при
-подъёме по лестнице за дверью уровня (seq_70, кадры 224..228) силуэт Кида
-вылезает ПРАВЕЕ правого косяка проёма — должен уходить за него. По ВЫСОТЕ
-обрезка корректна.
-
-**Причина — недопортированная половина `clip_char`** (seg006:1231). Для
-кадров двери уровня оригинал ставит ДВА клипа, а мы портировали только
-первый:
-
-```c
-if (frame >= frame_224_exit_stairs_8 && frame < 229) {
- obj_clip_top = leveldoor_ybottom + 1; // ЕСТЬ (pop_clip_char_top)
- obj_clip_right = leveldoor_right; // НЕТ
-}
-```
-
-`leveldoor_right = (draw_xh << 3) + 48` считается в `draw_leveldoor`
-(seg008:1D29) от колонки ЛЕВОЙ половины двери; для palace-уровней (+8,
-`tbl_level_type`) — на уровне 1 не применяется.
-
-Обратить внимание: комментарий в SDLPoP говорит «frames 217..228», а КОД
-проверяет `>= frame_224_exit_stairs_8`, то есть **224..228**. Портировать по
-коду.
-
-**Чего не хватает в движке:** обрезки СПРАВА у колоночного блита.
-`gfx_blit_cols_part` (libbgi) умеет только обрезку сверху (`skip`/`rows`) плюс
-клип по краю ЭКРАНА (`w = _bgi_maxx + 1 - x`). Для column-major спрайта
-обрезка справа — это ровно уменьшение `w`, то есть внутри ядра она уже есть,
-не хватает способа задать её снаружи.
-
-**План (не начат):**
-1. libbgi: тело `gfx_blit_cols_part` вынести в вариант с ограничением
- ширины (напр. `gfx_blit_cols_part_w(..., uint8_t maxw)`), а нынешнюю
- функцию оставить тонкой обёрткой — тем же приёмом, каким `gfx_blit_cols`
- уже обёрнут вокруг `gfx_blit_cols_part`. Флип тут не мешает: в кадрах
- двери направление всегда dir_FF_left, то есть flip=0.
-2. PoP: `pop_clip_char_top` дополнить правым клипом (или завести
- `pop_clip_char_right`), считать `leveldoor_right` от колонки двери.
-3. `make size-check` (трогаем libbgi) + проверка стоп-кадром в MAME:
- поймать кадр 224..228 клавишей `1` и сверить кромку.
-
-**Пока не починено, дефект косметический** — вход на следующий уровень
-работает.
-
----
# Оптимизация отрисовки (записано 2026-07-29)
@@ -181,6 +135,7 @@ Kid ставится на первый пол, `pop_trob_reset()` возвращ
- Читы (`pop_cheat.h`): **K** — убить стража, **I** — бессмертие (toggle),
**S** — выдать меч.
- Респавн после смерти — по **↑** (или авто через `RESPAWN_DELAY`); ставит
- Kid в `START_ROOM`.
-- `START_ROOM=1 / COL 3 / ROW 0` — жёстко зашитый тестовый старт; перевести
- на `pop_level_start_*` (задача L1-START в [`TASKS.md`](TASKS.md)).
+ Kid в стартовую позицию УРОВНЯ (`pop_start_level`, порт do_startpos).
+- **ROOMNAV (`=`/`-`) — тоже наш чит**, которого в оригинале не было, как и
+ `S`. Все они со временем съедутся в общий блок читов, разрешаемый в
+ настройках; пока просто включены (`pop_cheats = 1` в `roomtest.c`).
diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c
index 55ed518..e313d2a 100644
--- a/applications/PoP/roomtest/pop_bg.c
+++ b/applications/PoP/roomtest/pop_bg.c
@@ -585,6 +585,11 @@ static void draw_leveldoor(uint8_t modif, int xh, int dmy)
int ybottom = dmy - 13;
int ylo = ybottom - (int)(modif & 3) - 48; /* leveldoor_ybottom */
int y = ybottom - (int)modif;
+ /* leveldoor_right (seg008:1D29): правая кромка проёма — от xh ТЕКУЩЕГО
+ * тайла, то есть ПРАВОЙ половины двери (сюда мы попадаем из
+ * draw_tile_anim_right, разбирая левого соседа). +8 у palace-уровней
+ * (tbl_level_type) — на уровне 1 не применяется. */
+ pop_leveldoor_right = xh * 8 + 48;
env_b(99, x, ybottom);
if (modif)
env_b(144, x, ybottom - 4); /* лестница (комната ≠ стартовой) */
@@ -593,6 +598,9 @@ static void draw_leveldoor(uint8_t modif, int xh, int dmy)
if (y > ylo) ylo += 4;
else break;
}
+ /* ИМЕННО после цикла: в оригинале clip_char читает уже доведённое
+ * значение (низ поднятой створки), а не стартовое. */
+ pop_leveldoor_ybottom = ylo;
env_b(34, x, dmy - 64);
}
diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c
index 5121ca9..8a0ece6 100644
--- a/applications/PoP/roomtest/pop_kid.c
+++ b/applications/PoP/roomtest/pop_kid.c
@@ -679,7 +679,8 @@ void kid_draw(void)
uint8_t flip = (Kid.direction >= 0); /* dir_0_right -> зеркало */
int bx = obj_x + KID_XOFF;
int skip = 0; /* строк спрайта срезано сверху */
- int ct;
+ int ct, cr;
+ uint16_t vis_w = w; /* видимая ширина (клип справа) */
if (flip)
bx -= (int)w; /* seg008: xpos -= image->w */
top = obj_y - (int)h + 1 + KID_YOFF;
@@ -692,14 +693,27 @@ void kid_draw(void)
skip = ct - top;
if (skip >= (int)h) skip = (int)h; /* обрезан целиком */
}
+ /* clip_char, вторая половина: правый косяк двери уровня. Дверь
+ * рисуется ПОД персонажем (вся в backtable), поэтому спрятать его
+ * можно только срезав спрайт — см. pop_clip_char_right. */
+ cr = pop_clip_char_right();
+ if (cr) {
+ int avail = cr - bx;
+ if (avail <= 0) vis_w = 0; /* весь спрайт за косяком */
+ else if (avail < (int)w) vis_w = (uint16_t)avail;
+ }
gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */
- if (skip < (int)h) { /* +YOFF: центрирование */
- if (pop_onscreen_cols(bx, top + skip + POP_YOFF, w,
- (uint16_t)((int)h - skip)))
+ if (skip < (int)h && vis_w) { /* +YOFF: центрирование */
+ /* noclip-ядро ширину не ограничивает, поэтому при обрезке справа
+ * идём общим путём: это редкие кадры (подъём в двери уровня раз
+ * за уровень), цена подготовки тут не видна. */
+ if (vis_w == w && pop_onscreen_cols(bx, top + skip + POP_YOFF, w,
+ (uint16_t)((int)h - skip)))
gfx_blit_cols_part_noclip(bx, top + skip + POP_YOFF, img, flip,
(uint8_t)skip, 0);
else
- gfx_blit_cols_part(bx, top + skip + POP_YOFF, img, flip, skip, 0);
+ gfx_blit_cols_part_w(bx, top + skip + POP_YOFF, img, flip,
+ skip, 0, (uint8_t)(vis_w == w ? 0 : vis_w));
}
gfx_set_bank(GFX_BANK_NORMAL);
/* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) —
@@ -708,8 +722,8 @@ void kid_draw(void)
/* 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_lw[dp] = vis_w; kid_lh[dp] = (uint16_t)((int)h - skip);
+ kid_lvalid[dp] = (skip < (int)h && vis_w != 0);
kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */
}
/* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх
diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c
index 75ca0e6..c46430a 100644
--- a/applications/PoP/roomtest/pop_map.c
+++ b/applications/PoP/roomtest/pop_map.c
@@ -1384,6 +1384,21 @@ void pop_loose_tick(void) __banked
* check_spike_below колонки берутся get_tile_div_mod БЕЗ −7.
*
* Возврат: КОМНАТНЫЙ y верхней границы (0 = клипа нет). */
+/* Вторая половина ветки двери уровня из clip_char (seg006:1231):
+ * obj_clip_right = leveldoor_right — ЭКРАННЫЙ X, правее которого спрайт не
+ * рисуется. 0 = обрезки нет.
+ *
+ * Почему обрезка, а не fore-слой: створка и косяк уходят в оригинале
+ * ЦЕЛИКОМ в backtable (draw_leveldoor, все add_backtable), то есть рисуются
+ * ПОД персонажем и перекрыть его не могут. Единственный способ спрятать
+ * поднимающегося по лестнице — срезать сам спрайт. */
+int pop_clip_char_right(void) __banked
+{
+ if (Kid.frame >= 224 && Kid.frame < 229)
+ return pop_leveldoor_right;
+ return 0;
+}
+
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked
{
static const int16_t y_clip[5] = { -60, 3, 66, 129, 192 };
@@ -1393,6 +1408,14 @@ int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked
int8_t cL, cR, trow, clip_row;
uint8_t t;
+ /* Кадры подъёма по лестнице двери уровня (clip_char, seg006:1231):
+ * персонаж режется по низу поднятой створки. ВНИМАНИЕ: комментарий в
+ * SDLPoP говорит «frames 217..228», а КОД проверяет
+ * `>= frame_224_exit_stairs_8`, то есть 224..228 — портируем по коду.
+ * Правую кромку отдаёт pop_clip_char_right (парная половина ветки). */
+ if (Kid.frame >= 224 && Kid.frame < 229)
+ return pop_leveldoor_ybottom + 1;
+
if (Kid.direction >= 0) xl -= wh; /* лицом вправо: левый край внутрь */
xr = xl + wh; /* char_x_right */
top_y = obj_y - (int)h + 1; /* char_top_y */
diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h
index 3d2eeba..0d705ff 100644
--- a/applications/PoP/roomtest/pop_map.h
+++ b/applications/PoP/roomtest/pop_map.h
@@ -122,6 +122,10 @@ void pop_phys_tick(void) __banked;
* Возврат: КОМНАТНЫЙ y границы, 0 = клипа нет. Зовёт kid_draw. */
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked;
+/* Парная половина clip_char: ЭКРАННЫЙ X, правее которого спрайт не рисуется
+ * (правый косяк двери уровня в кадрах подъёма 224..228). 0 = обрезки нет. */
+int pop_clip_char_right(void) __banked;
+
/* Тип препятствия/края ВПЕРЕДИ по направлению (pop_edge_type после
* pop_edge_distance): для control (бежать / осторожный шаг / стоп у стены). */
#define EDGE_CLOSER 0 /* край уступа над ямой (дистанция = до кромки) */
diff --git a/applications/PoP/roomtest/pop_state.c b/applications/PoP/roomtest/pop_state.c
index daf98b6..fae7ec3 100644
--- a/applications/PoP/roomtest/pop_state.c
+++ b/applications/PoP/roomtest/pop_state.c
@@ -17,3 +17,9 @@ uint8_t pop_loose_landed;
/* Чит «бессмертие» (клавиша I). Отладочный: Кид получает анимацию удара,
* но HP не теряет — иначе проверять боёвку приходится с трёх попаданий. */
uint8_t pop_immortal;
+
+/* Габариты последней нарисованной двери уровня (порт leveldoor_right /
+ * leveldoor_ybottom): пишет draw_leveldoor из банка pop_bg, читает
+ * clip_char из pop_map. См. pop_state.h. */
+int pop_leveldoor_right;
+int pop_leveldoor_ybottom;
diff --git a/applications/PoP/roomtest/pop_state.h b/applications/PoP/roomtest/pop_state.h
index 85c74c3..239cb46 100644
--- a/applications/PoP/roomtest/pop_state.h
+++ b/applications/PoP/roomtest/pop_state.h
@@ -18,4 +18,15 @@ extern uint8_t pop_loose_landed;
* (боёвка в банке, пики в pop_map), переключает главный цикл. */
extern uint8_t pop_immortal;
+/* Габариты ПОСЛЕДНЕЙ нарисованной двери уровня — порт глобалов
+ * leveldoor_right / leveldoor_ybottom (seg008 draw_leveldoor). Их ставит
+ * отрисовка (банк pop_bg), а читает clip_char (pop_map, W1/W2): кадры
+ * подъёма по лестнице (224..228) обрезаются по правому косяку проёма и по
+ * низу поднятой створки. Дверь рисуется ПОД персонажем, поэтому перекрыть
+ * его не может — оригинал именно режет спрайт.
+ * right — экранный X правой кромки; ybottom — room-space Y (без POP_YOFF).
+ * 0 = двери в отрисованной комнате нет. */
+extern int pop_leveldoor_right;
+extern int pop_leveldoor_ybottom;
+
#endif
diff --git a/libbgi/common/gfx_blit_cols_part.c b/libbgi/common/gfx_blit_cols_part.c
index a12d181..15f0826 100644
--- a/libbgi/common/gfx_blit_cols_part.c
+++ b/libbgi/common/gfx_blit_cols_part.c
@@ -9,9 +9,9 @@
* сверху для column-major бесплатна: колонка = непрерывный кусок ОЗУ, старт
* просто сдвигается на skip байт, длина accel-блока уменьшается.
*
- * Здесь же живёт полное тело блита колонками — gfx_blit_cols() тонкая
- * обёртка (skip=0, rows=0), чтобы не дублировать клип-логику.
- * Подробности зеркала/клипа — см. шапку gfx_blit_cols.c.
+ * Тело (вместе с клип-логикой) — в gfx_blit_cols_part_w.c: этот вызов =
+ * «без ограничения ширины». Подробности зеркала/клипа — см. шапку
+ * gfx_blit_cols.c.
*/
#include
#include "../_bgi.h"
@@ -19,44 +19,5 @@
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();
+ gfx_blit_cols_part_w(x, y, img, flip, skip, rows, 0);
}
diff --git a/libbgi/common/gfx_blit_cols_part_w.c b/libbgi/common/gfx_blit_cols_part_w.c
new file mode 100644
index 0000000..901fc9a
--- /dev/null
+++ b/libbgi/common/gfx_blit_cols_part_w.c
@@ -0,0 +1,70 @@
+/*
+ * gfx_blit_cols_part_w — блит спрайта column-major с обрезкой СВЕРХУ
+ * (skip/rows) И ОГРАНИЧЕНИЕМ ШИРИНЫ (maxw): рисуются только первые maxw
+ * колонок футпринта, считая от x. maxw = 0 — «вся ширина кадра».
+ *
+ * Здесь живёт ПОЛНОЕ тело блита колонками; gfx_blit_cols_part (maxw=0) и
+ * gfx_blit_cols (ещё и skip=rows=0) — тонкие обёртки, чтобы клип-логика не
+ * дублировалась. Подробности зеркала/клипа — см. шапку gfx_blit_cols.c.
+ *
+ * Зачем ограничение ширины. Обрезка СПРАВА для column-major — это ровно
+ * уменьшение числа колонок, то есть внутри уже есть (так же клипается край
+ * экрана: `w = _bgi_maxx + 1 - x`), наружу просто не было выведено.
+ * Понадобилось для порта clip_char в PoP: персонаж, поднимающийся по
+ * лестнице за дверью уровня, обязан уходить за правый косяк проёма
+ * (obj_clip_right), а дверь рисуется ПОД ним и перекрыть его не может.
+ *
+ * Важно: ограничение работает и при flip. При зеркале источник читается с
+ * последней колонки к первой (sstride < 0), но первые w нарисованных
+ * колонок в любом случае ложатся в ЛЕВУЮ часть футпринта [x, x+w) — то
+ * есть уменьшение w всегда срезает справа, как и требуется.
+ */
+#include
+#include "../_bgi.h"
+
+void gfx_blit_cols_part_w(int x, int y, const void *img, uint8_t flip,
+ int skip, int rows, uint8_t maxw)
+{
+ 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 (maxw && (int)maxw < w)
+ w = (int)maxw; /* обрезка справа (см. шапку) */
+ 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/include/gfx.h b/libbgi/include/gfx.h
index 8467707..d2a0e0e 100644
--- a/libbgi/include/gfx.h
+++ b/libbgi/include/gfx.h
@@ -128,6 +128,20 @@ void gfx_blit_cols(int x, int y, const void *img, uint8_t flip);
void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
int skip, int rows);
+/* То же + ОГРАНИЧЕНИЕ ШИРИНЫ: рисуются только первые maxw колонок футпринта,
+ * считая от x (maxw = 0 — вся ширина кадра). То есть обрезка СПРАВА по
+ * произвольной линии, а не только по краю экрана.
+ *
+ * Для column-major это столь же дёшево, как обрезка сверху: колонок просто
+ * меньше. Работает и при flip — зеркало происходит внутри футпринта, а
+ * первые maxw нарисованных колонок всегда ложатся в его ЛЕВУЮ часть.
+ *
+ * Мотивация — clip_char в PoP (obj_clip_right): персонаж, поднимающийся по
+ * лестнице за дверью уровня, обязан уходить за правый косяк проёма, а сама
+ * дверь рисуется ПОД ним и перекрыть его не может. */
+void gfx_blit_cols_part_w(int x, int y, const void *img, uint8_t flip,
+ int skip, int rows, uint8_t maxw);
+
/* То же БЕЗ клипа по экрану — пара к gfx_blit_cols_part так же, как
* gfx_blit_part_noclip к gfx_blit_part. Вызывающий гарантирует: спрайт
* целиком на экране, ширина/высота кадра и y <= 255, skip < высоты.