Files
Sprinter-SDCC/applications/PoP/roomtest/pop_kid.c
T
snark13 f2093e0d89 PoP roomtest: fore-слой — окно клипа, кэш кладки; кадр 2.5 -> 0.78 периода
Жалоба: стойка неподвижных Кида и стража занимала больше полутора
кадровых периодов.  Гипотеза «виноват __banked» ЗАМЕРОМ НЕ
ПОДТВЕРДИЛАСЬ: весь банковый вызов (трамплин + смена страницы + тело
pop_guard_tick + возврат) стоит 654 такта при бюджете кадра 430 000.

Как мерил (выборка PC бесполезна — мост MAME отвечает из фреймового
колбэка, все сэмплы падают в обработчик прерывания): breakpoint'ы MAME с
действием {printf totalcycles; g} на входах фаз главного цикла, разности
соседних меток = стоимость фазы.  Плюс профилирование полосами бордюра
(make PROF=1, макрос PROF() в roomtest.c) для быстрого взгляда.

Замер комнаты 3 (Kid + страж), такты, кадр = 430 000:
  fore поверх стража  431 964
  fore поверх Kid     402 816   -> 78 % всей работы кадра
  остальное           241 956
  ИТОГО             1 076 736   = 2.5 кадра

Две причины, обе устранены:

1. Fore-слой рисовал ЦЕЛЫЕ тайлы, хотя существует ровно для того, чтобы
   вернуть куски поверх спрайта — за его прямоугольником в видеопамяти и
   так правильный фон.  Введено ОКНО клипа (pop_fore_set_clip): спрайт
   сообщает свой итоговый габарит (у Kid — с клинком, брызгами и
   обрезкой clip_char), fore-проход режет по нему.  Отсев трёхступенчатый:
   тайл целиком (tile_in_fclip, до обращения к атласу), кусок по грубому
   габариту (до gfx_w0_map — w/h лежат в EMM-странице), и точный клип в
   blit_b.  Для последнего добавлен libbgi-примитив
   gfx_blit_part_noclip — пара к gfx_blit_noclip, но под-прямоугольник.

2. Оставшиеся 341 К после клипа оказались НЕ пикселями: 18 вызовов
   pop_prandom за проход, ~10 700 тактов каждый (32-битный LCG:
   __mullong ~8 000 + __moduint).  Раскладка кладки тайла — чистая
   функция (комната, ряд, колонка), то есть константа комнаты, а
   wall_pattern пересчитывал её каждый кадр.  Теперь кэшируются готовые
   РЕШЕНИЯ (что рисовать и с каким смещением), 3 байта на тайл, сброс в
   pop_room_draw.  Порядок вызовов prandom воспроизведён один в один,
   включая то, что значение метки берётся только при сработавшем условии.

Итог того же замера: fore поверх стража 37 464, поверх Kid 46 464,
кадр целиком 334 716 = 0.78 периода (было 2.5).  Ускорение 3.2x, сами
fore-проходы — 10x.

Проверка отсутствия регрессии: попиксельная разность скриншотов комнат
1/2/3 до и после — отличаются ТОЛЬКО языки пламени факелов (анимация),
кладка и метки совпадают байт в байт.

Побочно: sprinter-cc научился пробрасывать -DNAME в sdcc.

Память: куча W2 1245 -> 996 Б (кэш кладки 120 Б), резидент W3 12 819 ->
14 512 (свободно 1872 Б — становится тесно), банк 1 236/16384.

ВНИМАНИЕ: make size-check показывает рост 7 программ, но эталон
docs/size_baseline.tsv отстал (последний раз принят в 484b18d, libbgi
менялась в 95c22be/c127a4b/64ce633) — к этой правке рост отношения не
имеет: новый модуль библиотеки в чужие программы не линкуется.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 00:19:32 +03:00

512 lines
25 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* pop_kid.c — движок Kid (Фаза K1): порт seg006.c play_seq/load_frame +
* seqtbl-интерпретатор. Данные — kid_data.h.
*/
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_kid.h"
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
#include "pop_map.h" /* pop_clip_char_top (clip_char) */
#include "pop_geom.h"
#include "kid_data.h"
kid_t Kid;
/* Флаг сотрясения (seg006 knock): ставится seqtbl-командой KNOCK_UP/DOWN
* в play_seq, потребляется pop_check_knock (pop_map) — трясёт loose-полы. */
int8_t knock;
/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ */
#define KID_MAXPAGES 28
static atlas_t kidp[KID_MAXPAGES];
static uint8_t kid_npages;
/* chtab_0 «меч в руке» (add_sword_to_objtable, seg006:1798): отдельный
* спрайт клинка поверх Kid в кадрах «нашёл меч»/убирание в ножны (229..237).
* Из sword_tbl взяты только строки 35..42 — те, что адресуют эти кадры (бой
* не портирован); индекс в атласе = sword_frame − 35. Смещения таблицы —
* в ЭКРАННЫХ пикселях: оригинал применяет их ПОСЛЕ calc_screen_x_coord. */
#define SWORD_SF_LO 35
#define SWORD_SF_HI 42
static const int8_t SWORD_DX[8] = { 7, 14, 14, 14, 28, 28, 21, 14 };
static const int8_t SWORD_DY[8] = {-32,-32,-31,-29,-28,-28,-25,-22 };
static atlas_t swordp;
static uint8_t sword_ok;
int pop_sword_load(void)
{
if (atlas_load(&swordp, "KID\\sword.atl") != 0) return -1;
sword_ok = 1;
return 0;
}
int pop_kid_load(uint8_t npages)
{
static const char *const names[KID_MAXPAGES] = {
"KID\\kid0.atl","KID\\kid1.atl","KID\\kid2.atl","KID\\kid3.atl","KID\\kid4.atl","KID\\kid5.atl",
"KID\\kid6.atl","KID\\kid7.atl","KID\\kid8.atl","KID\\kid9.atl","KID\\kid10.atl","KID\\kid11.atl",
"KID\\kid12.atl","KID\\kid13.atl","KID\\kid14.atl","KID\\kid15.atl","KID\\kid16.atl","KID\\kid17.atl",
"KID\\kid18.atl","KID\\kid19.atl","KID\\kid20.atl","KID\\kid21.atl","KID\\kid22.atl","KID\\kid23.atl",
"KID\\kid24.atl","KID\\kid25.atl","KID\\kid26.atl","KID\\kid27.atl"
};
uint8_t i;
if (npages > KID_MAXPAGES) npages = KID_MAXPAGES;
for (i = 0; i < npages; i++)
if (atlas_load(&kidp[i], names[i]) != 0)
return -1;
kid_npages = npages;
return 0;
}
void pop_kid_free(void)
{
uint8_t i;
for (i = 0; i < kid_npages; i++)
atlas_free(&kidp[i]);
kid_npages = 0;
}
/* ---- Данные анимации в EMM-странице (kid_data.bin) ------------------ *
* Таблица кадров (241*5 Б) и seqtbl (2310 Б) занимали 3.5 КБ в _CODE окна
* W1/W2 — самый дефицитный ресурс. Теперь они лежат в отдельной EMM-
* странице, которая маппится в W0 РОВНО на время play_seq (раз за тик), а
* весь остальной код читает cur_frame — 5-байтовую копию текущего кадра.
* Это ровно устройство оригинала: load_frame (seg006) заполняет cur_frame,
* и коллизия/отрисовка работают с ним, а не с таблицей.
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
extern void _gfx_w0_isr(void);
#define KD_DATA_OFF 0x100u
/* Адрес кадра i в замапленной странице. Считаем В uint16_t и кастуем ОДИН
* раз: запись `(const uint8_t *)CONST + (uint16_t)i * 5u` SDCC 4.5 собирает
* НЕВЕРНО — умножение делает в 16 битах (add hl,hl / add hl,bc), а потом
* берёт только МЛАДШИЙ байт результата и подставляет старший байт константы
* (`ld c,l` / `inc b`). Для кадров i >= 52 (i*5 >= 256) адрес уезжает на
* 256*k, и cur_frame наполняется чужой строкой таблицы: у бега/шага/подъёма
* пропадал бит FRAME_NEEDS_FLOOR, и Kid проваливался сквозь пол.
* Артефакт: .sprinter-cc-roomtest/pop_kid.asm, функция load_frame. */
static const uint8_t *kd_frame_ptr(uint8_t i)
{
uint16_t off = (uint16_t)i;
off += (uint16_t)(off << 2); /* i*5 = i + i*4, без умножения */
off += (uint16_t)(KD_DATA_OFF + KID_BIN_FRAMES_OFF);
return (const uint8_t *)off;
}
#define KD_SEQ(cs) (*((const uint8_t *)(KD_DATA_OFF + KID_BIN_SEQTBL_OFF) + \
(uint16_t)((cs) - SEQTBL_BASE)))
/* Байт seqtbl по адресу оригинала (база SEQTBL_BASE); ТОЛЬКО под замапленной
* страницей данных — то есть внутри play_seq. */
#define SEQ(cs) KD_SEQ(cs)
static uint8_t kdat_blk, kdat_page, kdat_ok;
/* Текущий кадр (порт глобала cur_frame, seg006): 5 байт вместо таблицы. */
static kframe cur_frame;
int pop_kid_data_load(const char *path)
{
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
uint8_t saved_w3, blk;
uint16_t stub;
int fd, n;
fd = open(path, O_RDONLY);
if (fd < 0) return -1;
blk = mem_alloc_pages(1);
if (!blk) { close(fd); return -1; }
kdat_blk = blk;
kdat_page = mem_get_page(blk, 0);
saved_w3 = _io_page_w3;
sprinter_page_w3(kdat_page);
n = read(fd, pg + KD_DATA_OFF, 16384 - KD_DATA_OFF);
if (n >= (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
stub = (uint16_t)&_gfx_w0_isr; /* ISR-стаб W0-страницы (как pop_level) */
pg[0x38] = 0xC3;
pg[0x39] = (uint8_t)(stub & 0xFF);
pg[0x3A] = (uint8_t)(stub >> 8);
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN */
}
sprinter_page_w3(saved_w3);
close(fd);
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) { mem_free_block(blk); return -1; }
kdat_ok = 1;
return 0;
}
void pop_kid_data_free(void)
{
if (kdat_ok) { mem_free_block(kdat_blk); kdat_ok = 0; }
}
/* Прочитать кадр из ЛЮБОЙ таблицы страницы данных (у Kid и у стража они
* разные: KID_BIN_FRAMES_OFF / KID_BIN_GFRAMES_OFF). Сама маппит страницу,
* поэтому годится для вызова извне (pop_guard) — но НЕ в горячем цикле. */
void pop_kid_data_frame(uint16_t tbl_off, uint8_t idx, uint8_t *out5)
{
const uint8_t *f;
uint16_t off = (uint16_t)idx;
if (!kdat_ok) { out5[0] = 255; return; }
off += (uint16_t)(off << 2); /* idx*5, без умножения */
off += (uint16_t)(KD_DATA_OFF + tbl_off);
gfx_w0_map(kdat_page);
f = (const uint8_t *)off;
out5[0] = f[0]; out5[1] = f[1]; out5[2] = f[2];
out5[3] = f[3]; out5[4] = f[4];
gfx_w0_unmap();
}
/* load_frame (seg006:0293): скопировать кадр Kid.frame из таблицы в
* cur_frame. Звать ТОЛЬКО когда страница данных замаплена в W0. */
static void load_frame(void)
{
const uint8_t *f = kd_frame_ptr(Kid.frame);
cur_frame.image = f[0];
cur_frame.dx = (int8_t)f[1];
cur_frame.dy = (int8_t)f[2];
cur_frame.flags = f[3];
cur_frame.sword = f[4];
}
static uint8_t char_dx_forward(int8_t dx)
{
if (Kid.direction < 0) dx = (int8_t)-dx;
return (uint8_t)(Kid.x + dx);
}
/* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу —
* их дёргают несколько раз за кадр, маппинг страницы тут недопустим. */
int8_t kid_cur_dx(void) { return cur_frame.dx; }
uint8_t kid_cur_flags(void) { return cur_frame.flags; }
/* seqtbl_offset_char: сменить последовательность Kid (единая точка —
* pop_ctrl/pop_map зовут ЕЁ, а не дублируют kid_seq_off/kid_seqtbl). */
void kid_set_seq(uint8_t seq_id) { Kid.curr_seq = kid_seq_off[seq_id]; }
/* play_seq (seg006.c:570): крутит опкоды до КАДРА (байт < 0xF0).
* Страница данных маппится в W0 на ВЕСЬ разбор — один map/unmap за вызов
* (в тике, до отрисовки: конфликта с атласом в W0 нет). В конце —
* load_frame: пока страница ещё замаплена, копируем кадр в cur_frame. */
void play_seq(void)
{
if (!kdat_ok) return;
gfx_w0_map(kdat_page);
for (;;) {
uint8_t cmd = SEQ(Kid.curr_seq);
Kid.curr_seq++;
switch (cmd) {
case 0xFB: /* DX */
Kid.x = char_dx_forward((int8_t)SEQ(Kid.curr_seq));
Kid.curr_seq++;
break;
case 0xFA: /* DY */
Kid.y = (uint8_t)(Kid.y + (int8_t)SEQ(Kid.curr_seq));
Kid.curr_seq++;
break;
case 0xFE: /* FLIP */
Kid.direction = (int8_t)~Kid.direction;
break;
case 0xF7: /* JMP_IF_FEATHER: нет пера -> пропустить адрес */
Kid.curr_seq += 2;
break;
case 0xFF: { /* JMP */
uint16_t a = (uint16_t)(SEQ(Kid.curr_seq) |
((uint16_t)SEQ((uint16_t)(Kid.curr_seq + 1)) << 8));
Kid.curr_seq = a;
break;
}
case 0xFD: /* UP */
Kid.curr_row--;
break;
case 0xFC: /* DOWN */
Kid.curr_row++;
break;
case 0xF9: /* ACTION */
Kid.action = SEQ(Kid.curr_seq);
Kid.curr_seq++;
break;
case 0xF8: /* SET_FALL */
Kid.fall_x = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++;
Kid.fall_y = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++;
break;
case 0xF5: /* KNOCK_UP: сотрясение трясёт loose РЯДОМ ВЫШЕ (check_knock) */
knock = 1;
break;
case 0xF4: /* KNOCK_DOWN: приземление трясёт loose В СВОЁМ РЯДУ */
knock = -1;
break;
case 0xF2: /* SOUND — потребить 1 байт */
Kid.curr_seq++;
break;
case 0xF1: /* END_LEVEL */
break;
case 0xF3: /* GET_ITEM: аргумент 1 — применить эффект предмета
* (порт play_seq -> proc_get_object, seg006) */
if (SEQ(Kid.curr_seq) == 1)
pop_proc_get_object();
Kid.curr_seq++;
break;
case 0xF6: /* DIE nop */
break;
default: /* КАДР */
Kid.frame = cmd;
load_frame(); /* cur_frame := таблица[frame] (страница ещё в W0) */
gfx_w0_unmap();
return;
}
}
}
void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir)
{
Kid.curr_col = col;
Kid.curr_row = row;
Kid.direction = dir;
Kid.x = (uint8_t)(pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
Kid.y = (uint8_t)pop_y_land[row + 1];
Kid.fall_x = Kid.fall_y = 0;
Kid.repeat = 0;
Kid.curr_seq = kid_seq_off[seq_id];
play_seq(); /* до первого кадра */
}
void kid_tick(void)
{
play_seq();
}
/* Отрисовка текущего кадра: obj_x=(char_x+dx)*2-116, obj_y=char_y+dy
* (seg008.c:1728 load_frame_to_obj). НИЗ спрайта на obj_y, левый край
* на obj_x. Калибровка XOFF/YOFF — сверить с эталоном.
*
* Зеркалирование (K2a): спрайты Kid хранятся ЛИЦОМ ВЛЕВО (как в оригинале).
* seg008.c:864 — флип, когда персонаж смотрит ВПРАВО (direction==dir_0_right
* ==0), и тогда xpos -= image->w (draw_mid). Реализуем бесплатным
* горизонтальным флипом column-major спрайта (gfx_blit_cols flip=1):
* зеркало происходит ВНУТРИ [x,x+w), поэтому при флипе подаём x=obj_x-w,
* чтобы «hot-point» кадра остался на месте.
*
* Kid рисуется банком GFX_BANK_SPRITE (только видео-ОЗУ, НЕ теневая копия
* — 0xFF прозрачен), чтобы kid_heal() мог восстановить чистый фон из
* теневой копии (фон запёкся в неё при pop_room_draw). Прямоугольник
* последней отрисовки запоминается для heal. */
#define KID_XOFF 0
#define KID_YOFF 0
/* Прямоугольник последней отрисовки Kid — ПО СТРАНИЦЕ (0/1). У каждой
* графической страницы своя ОЗУ-копия/видео-ОЗУ, поэтому heal должен
* стирать пиксели Kid именно той страницы, в которую сейчас рисуем
* (при дабл-буфере страница чередуется — прошлый Kid на этой странице
* был 2 логических кадра назад). В однобуфере используется только [0]. */
static int kid_lx[2], kid_ly[2];
static uint16_t kid_lw[2], kid_lh[2];
static uint8_t kid_lvalid[2];
/* Метрики последнего kid_draw для футпринта fore-over-Kid (pop_bg):
* obj_x — ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y — низ, w/h — размер. */
static int kid_fp_x, kid_fp_y;
static uint16_t kid_fp_w, kid_fp_h;
/* Рендер-смещение по ЛОГИЧЕСКОЙ X (straddle у шва, порт xpos_in_drawn_room):
* ∓140 когда комната Kid ≠ отрисованной; 0 обычно. */
static int kid_render_dx;
void pop_kid_set_render_dx(int dx) { kid_render_dx = dx; }
int kid_fp_obj_x(void) { return kid_fp_x; }
int kid_fp_obj_y(void) { return kid_fp_y; }
uint16_t kid_fp_width(void) { return kid_fp_w; }
uint16_t kid_fp_height(void) { return kid_fp_h; }
/* draw_hurt_splash (seg006:2003): «брызги» урона поверх персонажа — спрайт
* chtab_2_kid id 218 (res619.png; в наших атласах это image 218, стр. 27).
* Позиция от кадра Kid: obj_y = 15, obj_x += 5 ВПЕРЁД (obj_dx_forward).
* Кадры смерти/падения (185, 106..110) и пик (177) в оригинале смещают
* иначе — тут только общий случай + «на пиках» (порт по мере надобности).
* Рисовать ПОСЛЕ kid_draw (splash кладётся в objtable позже Kid). */
#define KID_SPLASH_IMAGE 218
void kid_draw_splash(void)
{
int obj_x = kid_fp_x, obj_y = kid_fp_y - 15;
uint8_t page = KID_SPLASH_IMAGE >> 3, idx = KID_SPLASH_IMAGE & 7;
const uint8_t *img;
uint16_t w, h;
if (page >= kid_npages) return;
if (Kid.frame == 177) /* напоролся на пики */
obj_y = kid_fp_y, obj_x -= (Kid.direction < 0) ? -5 : 5;
else
obj_x += (Kid.direction < 0) ? -5 : 5;
obj_x = obj_x * 8 / 7; /* лог. X -> экран (calc_screen_x_coord) */
img = (const uint8_t *)atlas_image(&kidp[page], idx);
gfx_w0_map(kidp[page].page);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
if (w && h) {
uint8_t flip = (Kid.direction >= 0);
int bx = obj_x;
int top = obj_y - (int)h + 1;
if (flip) bx -= (int)w;
gfx_set_bank(GFX_BANK_SPRITE);
gfx_blit_cols(bx, top + POP_YOFF, img, flip);
gfx_set_bank(GFX_BANK_NORMAL);
pop_clip_sprite(bx, w, top + POP_YOFF, (int)h);
/* Прямоугольник splash расширяет область heal ЭТОЙ страницы: иначе
* брызги останутся на экране после кадра. */
{
uint8_t dp = gfx_get_draw_page();
if (kid_lvalid[dp]) {
int x0 = kid_lx[dp] < bx ? kid_lx[dp] : bx;
int y0 = kid_ly[dp] < top ? kid_ly[dp] : top;
int x1 = kid_lx[dp] + (int)kid_lw[dp];
int y1 = kid_ly[dp] + (int)kid_lh[dp];
if (bx + (int)w > x1) x1 = bx + (int)w;
if (top + (int)h > y1) y1 = top + (int)h;
kid_lx[dp] = x0; kid_ly[dp] = y0;
kid_lw[dp] = (uint16_t)(x1 - x0); kid_lh[dp] = (uint16_t)(y1 - y0);
/* Окно fore-слоя расширяем тем же объединением: брызги
* тоже должны уходить под передние грани тайлов. */
pop_fore_set_clip(x0, y0 + POP_YOFF, x1 - x0, y1 - y0);
}
}
}
gfx_w0_unmap();
}
/* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid. */
void kid_heal(void)
{
uint8_t p = gfx_get_draw_page();
if (kid_lvalid[p])
gfx_heal(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]); /* +YOFF: центрирование */
kid_lvalid[p] = 0;
}
void kid_draw(void)
{
kframe fr = cur_frame; /* load_frame уже положил кадр (play_seq) */
int fwd, obj_x, obj_y, top;
uint8_t page, idx;
const uint8_t *img;
uint16_t w, h;
/* Окно fore-слоя по умолчанию пустое: на любом раннем выходе (нет
* кадра / страница не загружена) перекрывать нечего, и fore-проход
* не должен трогать экран. */
pop_fore_set_clip(0, 0, 0, 0);
if (fr.image == 255)
return; /* пустой кадр */
/* +kid_render_dx: straddle-смещение (∓140) когда комната Kid ≠
* отрисованной — спрайт за кромкой. Коллизия (Kid.x в pop_map) НЕ
* смещается: она в кадре комнаты Kid; смещение только рендерное. */
fwd = (int)Kid.x + ((Kid.direction < 0) ? -fr.dx : fr.dx) + kid_render_dx;
obj_x = (fwd << 1) - 116;
obj_y = (int)Kid.y + fr.dy;
if ((int8_t)(fr.flags ^ Kid.direction) >= 0)
obj_x++; /* even/odd pixel (в ЛОГИЧЕСКОЙ координате) */
kid_fp_x = obj_x; /* ЛОГИЧЕСКАЯ X (до ×8/7) — для футпринта fore */
kid_fp_y = obj_y;
/* seg008 calc_screen_x_coord: логическая X (280 шир = 28px/тайл) ->
* экран (320 шир = 32px/тайл), ×320/280 = ×8/7. БЕЗ этого Kid уезжает
* влево от тайлов тем сильнее, чем правее (col9 ~36px). Коллизия — в
* логической системе (не масштабируется). */
obj_x = obj_x * 8 / 7;
page = (uint8_t)(fr.image >> 3);
idx = (uint8_t)(fr.image & 7);
if (page >= kid_npages)
return; /* страница не загружена */
img = (const uint8_t *)atlas_image(&kidp[page], idx);
gfx_w0_map(kidp[page].page);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
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); /* видео-ОЗУ, без теневой копии */
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 + 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 */
}
/* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх
* Kid. Прямоугольник heal расширяем объединением — иначе клинок, выйдя
* за габарит Kid, оставит след. */
if (sword_ok && Kid.frame >= 229 && Kid.frame < 238) {
uint8_t sf = (uint8_t)(fr.sword & 0x3F);
if (sf >= SWORD_SF_LO && sf <= SWORD_SF_HI) {
uint8_t si = (uint8_t)(sf - SWORD_SF_LO);
const uint8_t *simg = (const uint8_t *)atlas_image(&swordp, si);
uint16_t sw, sh;
gfx_w0_map(swordp.page);
sw = (uint16_t)(simg[0] | ((uint16_t)simg[1] << 8));
sh = (uint16_t)(simg[2] | ((uint16_t)simg[3] << 8));
if (sw && sh) {
uint8_t sflip = (Kid.direction >= 0);
int sbx = obj_x + ((Kid.direction < 0) ? -(int)SWORD_DX[si]
: (int)SWORD_DX[si]);
int stop = obj_y + (int)SWORD_DY[si] - (int)sh + 1;
uint8_t dp = gfx_get_draw_page() & 1;
if (sflip) sbx -= (int)sw;
gfx_set_bank(GFX_BANK_SPRITE);
gfx_blit_cols_part(sbx, stop + POP_YOFF, simg, sflip, 0, 0);
gfx_set_bank(GFX_BANK_NORMAL);
if (!kid_lvalid[dp]) {
kid_lx[dp] = sbx; kid_ly[dp] = stop;
kid_lw[dp] = sw; kid_lh[dp] = sh;
kid_lvalid[dp] = 1;
} else {
int x0 = kid_lx[dp], y0 = kid_ly[dp];
int x1 = x0 + (int)kid_lw[dp], y1 = y0 + (int)kid_lh[dp];
if (sbx < x0) x0 = sbx;
if (stop < y0) y0 = stop;
if (sbx + (int)sw > x1) x1 = sbx + (int)sw;
if (stop + (int)sh > y1) y1 = stop + (int)sh;
kid_lx[dp] = x0; kid_ly[dp] = y0;
kid_lw[dp] = (uint16_t)(x1 - x0);
kid_lh[dp] = (uint16_t)(y1 - y0);
}
}
}
}
gfx_w0_unmap();
/* Окно fore-слоя = итоговый прямоугольник спрайта (с клинком и
* обрезкой clip_char): fore-проход вернёт куски тайлов ТОЛЬКО поверх
* него, а не целыми тайлами — см. pop_bg.c, fclip_on. Ставим и когда
* рисовать нечего (kid_lvalid=0): нулевое окно = fore-проход не
* трогает экран, что и правильно — перекрывать нечего. */
{
uint8_t dp = gfx_get_draw_page() & 1;
if (kid_lvalid[dp])
pop_fore_set_clip(kid_lx[dp], kid_ly[dp] + POP_YOFF,
(int)kid_lw[dp], (int)kid_lh[dp]);
else
pop_fore_set_clip(0, 0, 0, 0);
}
}