/* * pop_tile.c — общие ЛИСТЬЯ слоя фона: блит куска атласа, чтение тайла * комнаты, таблица тайлов. РЕЗИДЕНТ W1 (_CODE). * * Мотивация раскладки (почему резидент, а не банк) — в шапке pop_tile.h: * коротко, эти функции и таблицы нужны ОБЕИМ половинам слоя фона (горячей — * fore-проход каждый кадр, холодной — полная отрисовка комнаты), а код и * rodata банка видны только пока банк замаплен. W1 замаплено всегда. */ #include #include #include #include "pop_bg.h" /* POP_YOFF, POP_PLAYFIELD_H */ #include "pop_map.h" /* pop_upside — переворот (зелье инверсии) */ #include "pop_cdraw.h" /* POP_CD_TOUCH — фон тронут, персонажей перерисовать */ #include "pop_tile.h" #include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */ #include "pop_level.h" /* pop_doorlink2 — подстановка нажатой кнопки */ #include "_pop_draw.h" /* pop_heal_fast */ #include "_pop_tile.h" /* контракт с холодной половиной (pop_tile_cold.c) */ /* ---- Состояние (писучее, _DATA/W2 — видно из любого банка) ---------- */ atlas_t pop_env[POP_ENV_PAGES], pop_wall_at, pop_fore_at, pop_pot_at; /* Загруженный тайлсет: 0 подземелье, 1 дворец (порт tbl_level_type в той * части, что касается ОТРИСОВКИ). Ставит pop_bg_load; резидент — потому * что по нему расходятся ветки и в банке 7 (полная отрисовка тайла), и в * банке 2 (fore-проход), и в банке 3 (модификатор пустой клетки от упавшей * плиты, remove_loose seg007:0EB8). Читать напрямую, без трамплина. */ uint8_t pop_palace; const uint8_t *pop_t_fg, *pop_t_bg, *pop_t_lfg, *pop_t_lbg; const uint8_t *pop_t_below, *pop_t_above, *pop_t_abovemod; uint8_t pop_t_room; pop_rnd_t pop_t_seed; int pop_t_clip_top; int pop_t_fclip_x0, pop_t_fclip_y0, pop_t_fclip_x1, pop_t_fclip_y1; /* ТО ЖЕ ОКНО В ЛОГИЧЕСКИХ (неперевёрнутых) координатах. Экранная пара нужна * для клипа самого блита — он режет уже перевёрнутый прямоугольник; а вот * ОТСЕВ ТАЙЛОВ идёт по позиции тайла в комнате (63*row + …), и она от * переворота не зависит. Пока пара была одна, при pop_upside отсев сравнивал * логический y тайла с зеркальным окном и выбрасывал именно те тайлы, что * должны лечь ПОВЕРХ персонажа: передняя грань колонны переставала рисоваться * и Кид оказывался перед ней (найдено пользователем на уровне 9). */ int pop_t_fclip_ly0, pop_t_fclip_ly1; uint8_t pop_t_fclip_on; uint8_t pop_t_bake_rest; /* Позиция склянки внутри тайла зелья — поля fore_x/fore_y записи 0x0A той же * таблицы. Вынесены МАКРОСАМИ (одно место правки, дублирования данных нет): * пока таблица была `static const`, SDCC сворачивала `tile_table[10].fore_x` * в константу, но глобальную (её читают оба банка) уже не сворачивает — * чтение полей в рантайме стоило +150 Б и IX-кадр на 13 локалов. */ #define POT_FORE_X 2 #define POT_FORE_Y (-3) /* ---- Таблица тайлов (piece, seg008.c:27) --------------------------- */ const piece pop_tile_table[31] = { { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, /* 00 empty */ { 41, 1, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0}, /* 01 floor */ {127, 1, 0, 133, 1, 2, 145, 0, 43, 0, 0, 0}, /* 02 spike */ { 92, 1, 0, 93, 1, 2, 0, 94, 43, 95, 1, 0}, /* 03 pillar */ { 46, 1, 0, 47, 1, 2, 0, 48, 43, 49, 3, 0}, /* 04 gate */ { 41, 1, 1, 35, 1, 3, 145, 0, 36, 0, 0, 0}, /* 05 stuck floor */ { 41, 1, 0, 42, 1, 2, 145, 0, 96, 0, 0, 0}, /* 06 close button */ { 46, 1, 0, 0, 0, 2, 0, 0, 43, 49, 3, 0}, /* 07 door top w/floor */ { 86, 1, 0, 87, 1, 2, 0, 0, 43, 88, 1, 0}, /* 08 big pillar bottom */ { 0, 0, 0, 89, 0, 3, 0, 90, 0, 91, 1, 3}, /* 09 big pillar top */ { 41, 1, 0, 42, 1, 2, 145, 0, 43, 12, POT_FORE_X, POT_FORE_Y}, /* 0A potion */ { 0, 1, 0, 0, 0, 0, 145, 0, 0, 0, 0, 0}, /* 0B loose floor */ { 0, 0, 0, 0, 0, 2, 0, 0, 85, 49, 3, 0}, /* 0C door top */ { 75, 1, 0, 42, 1, 2, 0, 0, 43, 77, 0, 0}, /* 0D mirror */ { 97, 1, 0, 98, 1, 2, 145, 0, 43,100, 0, 0}, /* 0E debris */ {147, 1, 0, 42, 1, 1, 145, 0,149, 0, 0, 0}, /* 0F open button */ { 41, 1, 0, 37, 0, 0, 0, 38, 43, 0, 0, 0}, /* 10 leveldoor left */ { 0, 0, 0, 39, 1, 2, 0, 40, 43, 0, 0, 0}, /* 11 leveldoor right */ { 0, 0, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0}, /* 12 chomper */ { 41, 1, 0, 42, 1, 2, 0, 0, 43, 0, 0, 0}, /* 13 torch */ { 0, 0, 0, 1, 1, 2, 0, 2, 0, 0, 0, 0}, /* 14 wall */ { 30, 1, 0, 31, 1, 2, 0, 0, 43, 0, 0, 0}, /* 15 skeleton */ { 41, 1, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0}, /* 16 sword */ { 41, 1, 0, 10, 0, 0, 0, 11, 43, 0, 0, 0}, /* 17 balcony left */ { 0, 0, 0, 12, 1, 2, 0, 13, 43, 0, 0, 0}, /* 18 balcony right */ { 92, 1, 0, 42, 1, 2, 145, 0, 43, 95, 1, 0}, /* 19 lattice pillar */ { 1, 0, 0, 0, 0, 0, 0, 0, 2, 9, 0,-53}, /* 1A lattice down */ { 3, 0,-10, 0, 0, 0, 0, 0, 0, 9, 0,-53}, /* 1B lattice small */ { 4, 0,-10, 0, 0, 0, 0, 0, 0, 9, 0,-53}, /* 1C lattice left */ { 5, 0,-10, 0, 0, 0, 0, 0, 0, 9, 0,-53}, /* 1D lattice right */ { 97, 1, 0, 98, 1, 2, 0, 0, 43,100, 0, 0}, /* 1E debris w/torch */ }; /* Деление координаты на ширину тайла ТАБЛИЦЕЙ (tile_div_tbl/tile_mod_tbl, * seg006:702/23A6) — как в оригинале. Живут в резиденте, потому что нужны * и карте (банк 3, колонка персонажа), и слою фона (банк 2, футпринт), а * const-таблицу чужого банка не прочитать. * * Почему не деление: SDCC разворачивает / и % в __divsint + __modsint, * причём __modsint внутри снова зовёт __divsint — замер в MAME дал 5 400 * тактов на один вызов при 13+ вызовах за кадр. Индекс — координата как * есть (0..255), смещение -58 уже внутри значений: POP_TILE_DIV[x] = * floor((x - 58) / 14), POP_TILE_MOD[x] — остаток. Сверено с SDLPoP * побайтно. */ const int8_t POP_TILE_DIV[256] = { -5, -5, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, }; const uint8_t POP_TILE_MOD[256] = { 12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11, 12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11, 12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, }; const uint8_t POP_COL_XH[10] = {0, 4, 8, 12, 16, 20, 24, 28, 32, 36}; const uint8_t POP_WALL_FRAM_BOTTOM[4] = {7, 9, 5, 3}; const uint8_t POP_WALL_FRAM_MAIN[4] = {8, 10, 6, 4}; const uint8_t POP_SPIKES_FRAM_LEFT[10] = {0, 128, 129, 130, 131, 132, 131, 129, 128, 0}; const uint8_t POP_LOOSE_FRAM_LEFT[12] = {41,69,41,70,70,41,41,41,70,70,70,0}; const uint8_t POP_LOOSE_FRAM_BOTTOM[12] = {43,73,43,74,74,43,43,43,74,74,74,0}; /* ---- Чомпер: номер позы и передние куски (seg008 chomper_fram1/for) --- * Резидент, а не банк: позу считает и полная отрисовка тайла (холодная * половина, pop_room.c), и fore-проход поверх персонажа (горячая, * pop_bg.c) — а const-таблица банка из чужого банка не читается вовсе. */ static const uint8_t CHOMP_FRAM1[7] = {3, 2, 0, 1, 4, 3, 3}; const uint8_t POP_CHOMP_FRAM_FOR[5] = {106, 107, 108, 109, 110}; uint8_t pop_chomp_pose(uint8_t modif) { uint8_t m = (uint8_t)(modif & 0x7F); /* В ЗАПЕЧКЕ — поза ПОКОЯ, по той же причине, что у плиты и пик: фаза * челюстей транзиентна. Покой и есть фаза 0 (она же 6, на которой * animate_chomper снимает trob): CHOMP_FRAM1[0] == CHOMP_FRAM1[6]. */ if (pop_t_bake_rest) m = 0; else if (m > 6) m = 6; return CHOMP_FRAM1[m]; } /* get_loose_frame (seg008:761): индекс дрожащего кадра по модификатору. * В режиме ЗАПЕЧКИ фона отдаёт кадр ПОКОЯ — транзиентный кадр нельзя * консервировать в ОЗУ-копию (см. pop_t_bake_rest в pop_bg.c). */ uint8_t pop_loose_frame(uint8_t m) { if (pop_t_bake_rest) return 0; if (m & 0x80) { m &= 0x7F; if (m > 10) return 1; } return m; } /* Кадр пик по их модификатору: 1..4 — выдвижение, 0x80|n — «вышли целиком» * (один кадр 5), 6..8 — уход. Как и у плиты, в режиме ЗАПЕЧКИ отдаёт кадр * ПОКОЯ (0 = пик не видно): выдвинутая пика — транзиент, её место в ОЗУ-копии * фона делает heal вечным (BUG-SPIKE-BAKED, см. BUGS_CLOSED.md). * * Отдельная функция, а не `(m & 0x80) ? 5 : m` по месту: считают кадр ЧЕТЫРЕ * слоя (draw_tile_anim, draw_tile_anim_right соседа, fore-проход и * mid-оверлей), и одного забытого хватило, чтобы пики законсервировались. */ uint8_t pop_spike_frame(uint8_t m) { if (pop_t_bake_rest) return 0; return (uint8_t)((m & 0x80) ? 5 : m); } /* ---- «Фон трогали ВОТ ЗДЕСЬ» (DRAW-COST; контракт — pop_cdraw.h) ----- * * * Метка — БИТОВАЯ МАСКА ТАЙЛОВ, по слову на ряд и по набору на страницу. * Раньше здесь лежал ОДИН union-прямоугольник на страницу, и это убивало * пропуск перерисовки персонажа: три факела комнаты трогают по пятнышку * 16x18 в колонках 1, 6 и 8, а их объединение — полоса x 40..280 на всю * комнату. Кид, стоящий где угодно между крайними факелами, попадал в неё * и перерисовывался каждый кадр со всем fore-проходом (замер: циан 231 829 * тактов против 20 071, период 4 растровых кадра против 3). * * ГРАНУЛЯРНОСТЬ: колонка 32 px по горизонтали, ТОЧНЫЙ диапазон y по * вертикали. Раньше по вертикали стоял номер ряда (три полосы по 63 px), и * это склеивало касания, разнесённые внутри ряда. Найдено пользователем на * сцене 11/15 (2026-08-19): пламя факела занимает y 5..22, клинок стоящего * стража — y 31..37, между ними девять пикселей чистого зазора, а метка * считала слот задетым, потому что оба попадают в ряд 0 и колонку 7. * Стоило это 148 302 такта в кадр — 23 % работы — на перерисовку персонажа, * которого никто не трогал. * * Почему диапазон, а не более мелкие полосы: полосы по 16 px эту пару всё * равно склеивают (пламя кончается в полосе 1, клинок в ней же начинается), * а 8-пиксельные потребовали бы 24 маски на страницу. Пара ymin/ymax на * колонку — 40 байт на обе страницы, точнее любых полос и без битовой возни. * * У ОРИГИНАЛА такой метки нет вовсе: и Apple II (FRAMEADV.S RedBlockFast с * шестью буферами wipebuf/redbuf/movebuf/floorbuf/halfbuf/objbuf), и SDLPoP * (set_redraw_fore в redraw_at_char) метят ЦЕЛЫМИ тайлами, но им это не * мешает — персонаж у них рисуется каждый кадр безусловно, а пометки нужны * только фону. Пропуск неизменившегося персонажа — наша добавка, поэтому и * точность метки нужна выше оригинальной. См. docs/impl_diff.md. */ uint8_t pop_cd_dirty; /* [страница][колонка] — диапазон затронутых экранных y. Пусто = ymin > ymax * (заполняется как ymin = 255, ymax = 0). */ uint8_t pop_cd_ymin[2][10], pop_cd_ymax[2][10]; /* CD_LOW, cd_row_of и cd_cols_of СНЯТЫ вместе с переходом на диапазон y: * колонка теперь считается прямым сдвигом (x >> 5), а вертикаль сравнением * отрезков — битовые маски больше не нужны. */ /* Обе страницы помечаются сразу (персонаж чинится на каждой в свой кадр), * поэтому цикл развёрнут: индекс-переменная заставляла SDCC считать адрес * каждой ячейки сложением. Зовётся с КАЖДОГО блита фона. */ /* Пакетная пометка (pop_cd_batch_begin/end). Один тайл рисуется пятью-шестью * спрайтами, и каждый звал pop_cd_touch со своими четырьмя 16-битными * аргументами — 2 866 тактов на спрайт, 15% всей цены блита (замер * 2026-08-13). В пакете спрайты копят ОБЩИЙ прямоугольник, а настоящая * пометка ставится одна на тайл. * * Объединение ЗАГРУБЛЯЕТ прямоугольник (накрывает и то, чего между кусками * не касались), и это безопасно ровно в одну сторону: лишняя пометка стоит * лишней перерисовки персонажа, пропущенная — стёртого и не восстановленного * персонажа. Практически загрубление почти нулевое: все куски одного тайла * и так лежат в тех же 1-2 колонках сетки 32px и том же ряду. */ uint8_t pop_cd_batch; static int cdb_x0, cdb_y0, cdb_x1, cdb_y1; void pop_cd_batch_begin(void) { pop_cd_batch = 1; cdb_x0 = cdb_y0 = 32767; cdb_x1 = cdb_y1 = -32768; } void pop_cd_batch_end(void) { pop_cd_batch = 0; /* СНАЧАЛА снять: ниже настоящий вызов */ if (cdb_x1 >= cdb_x0 && cdb_y1 >= cdb_y0) pop_cd_touch(cdb_x0, cdb_y0, cdb_x1 - cdb_x0 + 1, cdb_y1 - cdb_y0 + 1); } /* Подавление на серию блитов — см. шапку в pop_cdraw.h. Отдельным значением * того же флага, а не вторым флагом: у pop_cd_touch тогда остаётся ОДНА * проверка на общем пути. */ #define CD_BATCH_MUTE 2 void pop_cd_mute(void) { pop_cd_batch = CD_BATCH_MUTE; } void pop_cd_unmute(void) { pop_cd_batch = 0; } void pop_cd_touch(int x, int y, int w, int h) { if (pop_cd_batch) { /* копим, не разбирая на колонки/ряды */ if (pop_cd_batch == CD_BATCH_MUTE) return; /* область помечена вызывающим */ int x1 = x + w - 1, y1 = y + h - 1; if (x < cdb_x0) cdb_x0 = x; if (y < cdb_y0) cdb_y0 = y; if (x1 > cdb_x1) cdb_x1 = x1; if (y1 > cdb_y1) cdb_y1 = y1; return; } { int8_t c0, c1, c; uint8_t y0, y1; int xr = x + w - 1; if (xr < 0 || x > 319) return; /* весь прямоугольник вне поля */ if (x < 0) x = 0; if (xr > 319) xr = 319; c0 = (int8_t)(x >> 5); c1 = (int8_t)(xr >> 5); /* Клип по вертикали: экранные y не выходят за байт, а всё, что выше * поля или ниже его, персонажам всё равно не принадлежит. */ if (y < 0) y = 0; if (y > 255) return; y0 = (uint8_t)y; y1 = (y + (int)h - 1 > 255) ? 255 : (uint8_t)(y + h - 1); /* Страницы обновляются НЕЗАВИСИМО. Общее условие по странице 0 * («если ей стало теснее — записать в обе») ломается сразу после * pop_cd_clear(0): страница 1 хранит свои старые границы, условие по * нулевой уже не выполняется, и её метка перестаёт расти. */ for (c = c0; c <= c1; c++) { if (y0 < pop_cd_ymin[0][c]) pop_cd_ymin[0][c] = y0; if (y1 > pop_cd_ymax[0][c]) pop_cd_ymax[0][c] = y1; if (y0 < pop_cd_ymin[1][c]) pop_cd_ymin[1][c] = y0; if (y1 > pop_cd_ymax[1][c]) pop_cd_ymax[1][c] = y1; } } pop_cd_dirty = 3; } /* Задел ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 ВКЛЮЧИТЕЛЬНО) то, что * трогали на странице p. Резидент: зовёт pop_cdraw.c из банка 4 прямым * вызовом, без трамплина. */ /* ---- Блит куска атласа ---------------------------------------------- */ /* Клипованный путь — ОТДЕЛЬНОЙ функцией. У него девять 16-битных локалей, * и SDCC заводит под них кадр IX; пока он жил в общем теле, за этот кадр * платили и блиты БЕЗ клипа (`ld -n(ix)` = 19 тактов номинала, у нас ~46). * А блитов без клипа большинство: весь фон вне fore-прохода — факелы, зелья, * перерисовка тайлов — идёт с pop_t_fclip_on == 0. */ #define FLIP_TOP(top, h) POP_FLIP_TOP(top, h) /* формула — в pop_bg.h */ /* Клипованный путь блита. Габарит и смещения внутри куска — БАЙТЫ, а вся * рабочая переменная — file-scope, НЕ локали. * * Корректность байтового габарита по построению: кадры наших атласов не * крупнее 56x63 (просканированы все 109 .atl, docs/perf_l13_room23.md §5), а * кадр больше 255 в pop_blit_b уходит в blit_b_oversize и сюда не попадает. * * Зачем file-scope: с локалями SDCC заводил 22-байтовый стековый кадр и 211 * обращений `-N(ix)` — при ~46 замеренных тактах на обращение это ~9 700 * такта из 14 088 цены клипованного блита (замер 2026-08-17, * docs/perf_green_phase.md §1.6). Одного байтового габарита НЕ ХВАТИЛО: * кадр остался, обращений стало 173 — значений, живых через шесть вызовов * ядер libbgi, всё равно больше, чем регистров у Z80. С file-scope * спиливать нечего, а адресация абсолютная (13 тактов против 19). * Тот же приём и по той же причине применён к draw_tile (pop_room.c). * * Реентерабельности не было и не требуется: единственный вызывающий — * pop_blit_b, и он себя не зовёт. * * Арифметика клипа считается в int16 (bc_d), в байт уходит только результат — * иначе вычитание ушло бы в перенос по модулю 256. */ static const uint8_t *bc_img; static int bc_dx, bc_dy; static int16_t bc_d; static uint8_t bc_w, bc_h, bc_dw, bc_dh, bc_sx, bc_sy; void blit_b_clip(const uint8_t *img, int x, int top, uint8_t w, uint8_t h) { bc_img = img; bc_w = w; bc_h = h; bc_dx = x; bc_dy = top; bc_dw = w; bc_dh = h; bc_sx = 0; bc_sy = 0; if (!pop_upside) { if (pop_t_clip_top && bc_dy < pop_t_clip_top) { /* полоса у потолка */ bc_d = (int16_t)(pop_t_clip_top - bc_dy); if (bc_d >= (int16_t)bc_dh) return; bc_sy = (uint8_t)bc_d; bc_dy = pop_t_clip_top; bc_dh = (uint8_t)(bc_dh - bc_sy); } } else { /* ПЕРЕВОРОТ: то, что в обычном виде торчало ВЫШЕ поля (полоса кладки * у потолка), теперь торчит НИЖЕ него — и без обрезки лезет в борт, * где живёт полоса HP (баг «мусор ниже 192», найден пользователем). * Режем по границам поля ВСЕГДА, а не по pop_t_clip_top: зеркальный * спрайт может вылезти и там, где в обычном виде клип не ставили. */ if (bc_dy < POP_YOFF) { bc_d = (int16_t)(POP_YOFF - bc_dy); if (bc_d >= (int16_t)bc_dh) return; bc_sy = (uint8_t)bc_d; bc_dy = POP_YOFF; bc_dh = (uint8_t)(bc_dh - bc_sy); } if (bc_dy + bc_dh > POP_CLIP_BOTTOM) { bc_d = (int16_t)(POP_CLIP_BOTTOM - bc_dy); if (bc_d <= 0) return; bc_dh = (uint8_t)bc_d; } } if (pop_t_fclip_on) { /* окно fore-слоя / перерисовки */ if (bc_dx < pop_t_fclip_x0) { bc_d = (int16_t)(pop_t_fclip_x0 - bc_dx); if (bc_d >= (int16_t)bc_dw) return; bc_sx = (uint8_t)(bc_sx + (uint8_t)bc_d); bc_dx += bc_d; bc_dw = (uint8_t)(bc_dw - (uint8_t)bc_d); } if (bc_dy < pop_t_fclip_y0) { bc_d = (int16_t)(pop_t_fclip_y0 - bc_dy); if (bc_d >= (int16_t)bc_dh) return; bc_sy = (uint8_t)(bc_sy + (uint8_t)bc_d); bc_dy += bc_d; bc_dh = (uint8_t)(bc_dh - (uint8_t)bc_d); } if (bc_dx + bc_dw > pop_t_fclip_x1) { bc_d = (int16_t)(pop_t_fclip_x1 - bc_dx); if (bc_d <= 0) return; bc_dw = (uint8_t)bc_d; } if (bc_dy + bc_dh > pop_t_fclip_y1) { bc_d = (int16_t)(pop_t_fclip_y1 - bc_dy); if (bc_d <= 0) return; bc_dh = (uint8_t)bc_d; } } if (!bc_dw || !bc_dh) return; /* ПЕРЕВОРОТ: экранная строка (dy+i) берёт источник (h-1-i), поэтому * обрезка СВЕРХУ экрана съедает НИЖНИЕ строки источника. В терминах * куска источника это sy' = h - sy - dh (при обрезке только сверху даёт * 0, при обрезке только снизу — верхний край). */ /* Проверки `dw < 256 && dh < 256 && w < 256` убраны как тождественные: * это уже байты. */ if (pop_upside) { bc_d = (int16_t)bc_h - (int16_t)bc_sy - (int16_t)bc_dh; if (bc_d < 0) bc_d = 0; if (bc_dx >= 0 && bc_dy >= 0 && bc_dx + bc_dw <= 320 && bc_dy + bc_dh <= 256) gfx_blit_part_noclip_vflip(bc_dx, bc_dy, bc_img, bc_sx, (uint8_t)bc_d, bc_dw, bc_dh); else gfx_blit_part_vflip(bc_dx, bc_dy, bc_img, bc_sx, (uint8_t)bc_d, bc_dw, bc_dh); } else if (bc_dx >= 0 && bc_dy >= 0 && bc_dx + bc_dw <= 320 && bc_dy + bc_dh <= 256) { if (bc_sx == 0 && bc_sy == 0 && bc_dw == bc_w && bc_dh == bc_h) gfx_blit_noclip(bc_dx, bc_dy, bc_img); else gfx_blit_part_noclip(bc_dx, bc_dy, bc_img, bc_sx, bc_sy, bc_dw, bc_dh); } else { gfx_blit_part(bc_dx, bc_dy, bc_img, bc_sx, bc_sy, bc_dw, bc_dh); } /* Кусок ФОНА лёг туда, где может лежать уже нарисованный персонаж — * пометить область (DRAW-COST, pop_cdraw.h). Признак «это не fore-проход * поверх персонажа» — окно клипа: его ставит только он. */ if (!pop_t_fclip_on) pop_cd_touch(bc_dx, bc_dy, bc_dw, bc_dh); } /* Кадр шире или выше 255 — общий (медленный) путь. Вынесен отдельно, чтобы * 16-битная арифметика габарита не жила ни в горячем pop_blit_b, ни в * blit_b_clip (у того габарит теперь БАЙТОВЫЙ, см. его шапку). У нас таких * кадров в игровом кадре нет вовсе (максимум 56x63), но контракт формата * .atl их допускает, а интро/финал такие подложки принесут (320x200) — * см. docs/perf_l13_room23.md §5. * * Клипа полосы у потолка и окна перерисовки здесь нет намеренно: такой кадр * — полноэкранная подложка, ей ни то, ни другое не адресовано; за клип по * экрану отвечает само ядро gfx_blit_part. */ static void blit_b_oversize(const uint8_t *img, int x, int ybottom) { uint16_t w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); uint16_t h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); int top = ybottom - (int)h + 1 + POP_YOFF; if (pop_upside) top = FLIP_TOP(top, h); if (!w || !h) return; gfx_blit_part(x, top, img, 0, 0, (int)w, (int)h); if (!pop_t_fclip_on) pop_cd_touch(x, top, (int)w, (int)h); } /* Рабочие переменные pop_blit_b — file-scope, не локали и не стековые * аргументы (см. пролог функции). Реентерабельности нет и не требуется: * pop_blit_b себя не зовёт, а blit_b_clip держит СВОЙ набор (bc_*). */ static atlas_t *pb_a; static uint8_t pb_idx, pb_w, pb_h; static int pb_x, pb_yb, pb_top; static const uint8_t *pb_img; void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) { pop_beam_sample(); /* ТЕМП КАДРА: выборка луча, см. pop_pace.h */ /* Аргументы сразу в file-scope: третий и дальше SDCC передаёт СТЕКОМ, и * дальше каждое их чтение — `-N(ix)` (замер 2026-08-17: 76 таких * обращений в теле, ~3 500 такта из 14 500 цены блита). То же лечение, * что у draw_tile и blit_b_clip. */ pb_a = a; pb_idx = idx; pb_x = x; pb_yb = ybottom; if (pb_idx >= pb_a->count) return; /* Грубый отсев ДО маппинга страницы: реальные pb_w/pb_h лежат в заголовке * ленты, то есть в EMM-странице, и чтобы их прочитать, надо сделать * gfx_w0_map. Берём заведомо больший габарит (кадры фона не крупнее * 64×64) и, если он не задевает окно, уходим не заплатив ни за * atlas_image, ни за map/unmap. */ if (pop_t_fclip_on && (pb_x >= pop_t_fclip_x1 || pb_x + 64 <= pop_t_fclip_x0 || pb_yb + POP_YOFF + 1 <= pop_t_fclip_ly0 || pb_yb + POP_YOFF - 64 >= pop_t_fclip_ly1)) return; pb_img = (const uint8_t *)atlas_image(pb_a, pb_idx); gfx_w0_map(pb_a->page); /* Габарит читаем БАЙТАМИ. С `uint16_t pb_w = pb_img[0] | (pb_img[1] << 8)` SDCC * разворачивал каждую такую строку в два десятка IX-относительных * пересылок: 16-битные pb_w/pb_h не влезали в регистры, и функция целиком * уезжала в стековый кадр (пролог `ld iy,#-14 / add iy,sp / ld sp,iy`, * дальше всё через -N(ix) по 19 тактов). Замер 2026-08-13: до вызова * ядра блита уходило ~6 700 тактов при том, что работы там — прочитать * четыре байта заголовка и сравнить границы. * Байтовый габарит корректен по построению: обе ветки ниже И ТАК требуют * pb_w < 256 && pb_h < 256 (иначе ядро не примет), а кадры наших атласов не * крупнее 32x63. Формат .atl допускает больше — такой кадр уходит на * общий путь выше. */ if (pb_img[1] | pb_img[3]) { /* кадр больше 255 — редкий путь */ blit_b_oversize(pb_img, pb_x, pb_yb); pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ gfx_w0_unmap(); return; } pb_w = pb_img[0]; pb_h = pb_img[2]; pb_top = pb_yb - (int)pb_h + 1 + POP_YOFF; /* +YOFF: центрирование */ /* Переворот применяем ЗДЕСЬ, до клипа: дальше вся геометрия (окно * fore-слоя, полоса у потолка, отсев по экрану) считается уже в * экранных координатах результата. */ if (pop_upside) pb_top = FLIP_TOP(pb_top, pb_h); if (pb_w && pb_h) { if (!pop_t_fclip_on && !pop_t_clip_top) { /* БЫСТРЫЙ путь: клипа нет вовсе. Так идут все блиты фона вне * fore-прохода. Линейное ядро без клипа — ~2.9× быстрее общего * (замер: 4617 против 13288 тактов на спрайт 32×3; gfx_blit * платит за клип/16-бит/split, а не за пиксели). W3-скобку * ставит сама libbgi: из кода с --w3 её вызывать нельзя. */ /* В перевёрнутом виде быстрый путь годится, только если спрайт * целиком внутри поля: иначе он залезет в борт (см. blit_b_clip). */ if (pop_upside && (pb_top < POP_YOFF || pb_top + (int)pb_h > POP_CLIP_BOTTOM)) { blit_b_clip(pb_img, pb_x, pb_top, pb_w, pb_h); pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ gfx_w0_unmap(); return; } /* pb_w<256 && pb_h<256 больше не проверяем — гарантировано типом. */ /* «Целиком на экране?» — ДВУМЯ беззнаковыми сравнениями вместо * четырёх знаковых. Отрицательная координата в беззнаковом виде * становится очень большой и проваливает то же условие, что * проверка `>= 0`, а верхняя граница переносится в правую часть, * так что сложение уходит вместе с ней. Знаковое сравнение у * SDCC z80 стоит дорого: пара `sbc` плюс `jp PO / xor 0x80 / jp P` * на каждое (видно в листинге). * * Границы правой части неотрицательны по построению: pb_w и pb_h * не больше 255, значит 320-pb_w >= 65 и 256-pb_h >= 1. */ if ((unsigned int)pb_x <= (unsigned int)(320 - (int)pb_w) && (unsigned int)pb_top <= (unsigned int)(256 - (int)pb_h)) { if (pop_upside) gfx_blit_noclip_vflip(pb_x, pb_top, pb_img); else gfx_blit_noclip(pb_x, pb_top, pb_img); } else { if (pop_upside) gfx_blit_part_vflip(pb_x, pb_top, pb_img, 0, 0, (int)pb_w, (int)pb_h); else gfx_blit_part(pb_x, pb_top, pb_img, 0, 0, (int)pb_w, (int)pb_h); } pop_cd_touch(pb_x, pb_top, (int)pb_w, (int)pb_h); } else { blit_b_clip(pb_img, pb_x, pb_top, pb_w, pb_h); } } pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ gfx_w0_unmap(); } void pop_env_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_env[id >> POP_ENV_SHIFT], (uint8_t)(id & POP_ENV_MASK), x, yb); } void pop_wall_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_wall_at, id, x, yb); } void pop_fore_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_fore_at, id, x, yb); } void pop_pot_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_pot_at, id, x, yb); } /* ---- Чтение тайла отрисованной комнаты ------------------------------ */ uint8_t pop_tile_code(int row, int col) { if (col < 0) { if (pop_t_lfg && row >= 0 && row < 3) return pop_t_lfg[row]; return 20; /* уровневая кромка = стена */ } if (row < 0) { /* верхняя полоса: ряд 2 комнаты сверху */ if (row == -1 && pop_t_above && col < 10) return pop_t_above[col]; return 1; /* нет комнаты сверху → пол (кромка) */ } if (row > 2 || col > 9) return 20; return pop_t_fg[row * 10 + col]; } uint8_t pop_tile_mod(int row, int col) { if (col < 0) { if (pop_t_lbg && row >= 0 && row < 3) return pop_t_lbg[row]; return 0; } if (row == -1 && pop_t_abovemod && col < 10) return pop_t_abovemod[col]; if (row < 0 || row > 2 || col > 9) return 0; return pop_t_bg[row * 10 + col]; } /* Порт подстановки нажатой кнопки из get_tile_to_draw (seg008:240): opener → * floor, closer → stuck, когда таймер связи > 1. Любой слой, который * выбирает ветку отрисовки по типу тайла, обязан смотреть на ПОДСТАВЛЕННЫЙ * тип (иначе кнопка, на которой стоит Kid, ведёт себя как «не пол» — напр. * climb-оверлей уходил в draw_other_overlay и закрашивал Kid целым тайлом * вместо узкой кромки floor_left_overlay). */ uint8_t pop_tile_code_drawn(int row, int col) { uint8_t code = pop_tile_code(row, col); if (code == 0x0F) { if ((pop_doorlink2(pop_tile_mod(row, col)) & 0x1F) > 1) code = 1; } else if (code == 0x06) { if ((pop_doorlink2(pop_tile_mod(row, col)) & 0x1F) > 1) code = 5; } return code; } uint8_t pop_wall_modifier(int row, int col) { uint8_t lw = (col > 0) ? (pop_tile_code(row, col - 1) == 20) : 1; uint8_t rw = (col < 9) ? (pop_tile_code(row, col + 1) == 20) : 1; if (lw && rw) return 3; /* WWW */ if (lw) return 2; /* WWS */ if (rw) return 1; /* SWW */ return 0; /* SWS */ } /* Окно fore-клипа: ставит тот, кто нарисовал персонажа (pop_cdraw, банк 4), * читает blit_b. Резидент — чтобы вызов из банка шёл без трамплина. */ void pop_fore_set_clip(int x, int y, int w, int h) { pop_t_fclip_x0 = x; pop_t_fclip_y0 = y; pop_t_fclip_x1 = x + w; pop_t_fclip_y1 = y + h; if (pop_upside) { /* вернуть окно в неперевёрнутый вид */ pop_t_fclip_ly0 = 2 * POP_YOFF + POP_PLAYFIELD_H - (y + h); pop_t_fclip_ly1 = 2 * POP_YOFF + POP_PLAYFIELD_H - y; } else { pop_t_fclip_ly0 = y; pop_t_fclip_ly1 = y + h; } }