Files
Sprinter-SDCC/applications/PoP/roomtest/pop_tile.c
T
snark13 a3c473d910 Оптимизация каскада плит: -27% работы за кадр (пять правок с замерами)
Сцена — ур.13 комната 23, шесть плит с потолка (docs/perf_l13_room23.md).
Все замеры сняты одним и тем же прогоном «ESC -> зонды -> roomtest».

                       было       стало
  работа за кадр    1 437 150   1 051 332   -27%
  зелёная (фон)       805 000     575 730   -28%
  циан (спрайты)      631 800     438 546   -31%
  синяя                142 830     142 830   в бюджете

Цена одной перерисовки:
  RDA_CEIL_GONE (запечь колодец)  251 335 -> 137 975   -45%
  RD_FLOOR (щебень на посадке)    198 805 -> 182 124    -8%
  RDA_CEIL (дрожащая плита)        48 785 ->  43 543   -11%

C1. Пометки «фон трогали» в mob_render ПОДАВЛЕНЫ (pop_cd_mute): коридор
куска уже помечен одним вызовом в pop_loose_mob_tick, а три блита метили
подмножества того же прямоугольника по 4 502 такта.  Плюс пометка в
mob_spawn_copy — кусок, рождённый внутри тика, свою мог не получить
(слот выдаётся с начала таблицы, то есть уже пройденный).  -55 тыс.

G2. Контекст тайла — file-scope, а не локали draw_tile (порт
load_curr_and_left_tile, seg008:0339).  В функции 57 вызовов, и каждое
живое через вызов значение спиливалось: 26 байт кадра и 513 обращений
`-N(ix)`.  Стало 33 обращения, кадра нет, банк 7 -703 Б.  -55 тыс.

G1. Окно клипа для точечной перерисовки (pop_t_win_set/clear).  В
pop_ceil_bake_empty куски ряда 0 высотой 63 px рисовались целиком, хотя
восстановить надо девять строк: блит стоил 21 447, стал 7 619.  -90 тыс.
В pop_floor_bake окно попробовано и ОТКАЧЕНО (там нет клипа, блиты шли
быстрым путём, а окно уводило их в blit_b_clip и не отсеивало ничего:
187 266 -> 198 279) — вернуться после G3, запись в коде.

G3. blit_b_clip: байтовый габарит + file-scope вместо локалей.  Одного
байтового габарита НЕ ХВАТИЛО (кадр остался, 211 -> 173 обращений) —
значений, живых через шесть вызовов ядер libbgi, больше, чем регистров.
С file-scope: 51 обращение, кадр 22 -> 12 Б.  Клипованный блит подешевел.

C4. Падающий кусок КЛИПУЕТСЯ сам, вместо чистки бортов после.  Раньше он
рисовался в борт целиком и взводил border_dirty, а pop_room_clip_borders
стирал две полосы 320x28 — 150 978 тактов в каждом кадре, пока хоть один
кусок торчит выше поля (гряда 13-го рождается ровно у потолка, y=2), то
есть почти весь каскад.  Стало 1 722.  -138 тыс.

Заодно blit_b_oversize больше не ходит через blit_b_clip (у того габарит
теперь байтовый) — рисует напрямую gfx_blit_part.  Это путь под
полноэкранные подложки интро/финала (320x200, docs/perf_l13_room23.md §5).

Проверено: 8 наборов tests-host зелёные; в MAME каскад рисуется корректно
(кадр с летящими плитами, чистые борта, итоговая картинка как до правок).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 13:39:57 +03:00

679 lines
39 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_tile.c — общие ЛИСТЬЯ слоя фона: блит куска атласа, чтение тайла
* комнаты, таблица тайлов. РЕЗИДЕНТ W1 (_CODE).
*
* Мотивация раскладки (почему резидент, а не банк) — в шапке pop_tile.h:
* коротко, эти функции и таблицы нужны ОБЕИМ половинам слоя фона (горячей —
* fore-проход каждый кадр, холодной — полная отрисовка комнаты), а код и
* rodata банка видны только пока банк замаплен. W1 замаплено всегда.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#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_level.h" /* pop_doorlink2 — подстановка нажатой кнопки */
#include "_pop_draw.h" /* pop_heal_fast */
/* ---- Состояние (писучее, _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);
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;
}
/* ---- «Фон трогали ВОТ ЗДЕСЬ» (DRAW-COST; контракт — pop_cdraw.h) ----- *
*
* Метка — БИТОВАЯ МАСКА ТАЙЛОВ, по слову на ряд и по набору на страницу.
* Раньше здесь лежал ОДИН union-прямоугольник на страницу, и это убивало
* пропуск перерисовки персонажа: три факела комнаты трогают по пятнышку
* 16x18 в колонках 1, 6 и 8, а их объединение — полоса x 40..280 на всю
* комнату. Кид, стоящий где угодно между крайними факелами, попадал в неё
* и перерисовывался каждый кадр со всем fore-проходом (замер: циан 231 829
* тактов против 20 071, период 4 растровых кадра против 3).
*
* Гранулярность тайла (32 x 63) вместо точного прямоугольника — осознанное
* огрубление: факел помечает всю свою колонку по высоте ряда. Персонаж и
* так занимает бОльшую часть высоты ряда, зато касания перестают
* склеиваться. См. docs/impl_diff.md. */
uint8_t pop_cd_dirty;
uint16_t pop_cd_dmask[2][3];
/* CD_LOW[n] = n младших единиц: маска пробега колонок c0..c1 считается как
* CD_LOW[c1+1] & ~CD_LOW[c0] — два чтения таблицы вместо цикла сдвигов. */
static const uint16_t CD_LOW[11] = {
0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F,
0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF
};
/* Экранный y -> ряд комнаты 0..2 с клампом (полоса кладки у потолка и низ
* стены ложатся на крайние ряды). Цепочкой сравнений, а не делением на 63:
* у SDCC z80 одно деление ~5 400 тактов (memory sdcc_z80_division_hoisting). */
static uint8_t cd_row_of(int y)
{
y -= POP_YOFF;
if (y < 63) return 0;
if (y < 126) return 1;
return 2;
}
/* Колонки прямоугольника [x..x1] (включительно) -> битовая маска. Тайл ровно
* 32 px и начинается с x=0, поэтому колонка — просто сдвиг. */
static uint16_t cd_cols_of(int x, int x1)
{
uint8_t c0, c1;
if (x < 0) x = 0;
if (x1 > 319) x1 = 319;
if (x1 < x) return 0; /* весь прямоугольник вне экрана */
c0 = (uint8_t)(x >> 5);
c1 = (uint8_t)(x1 >> 5);
return (uint16_t)(CD_LOW[c1 + 1] & ~CD_LOW[c0]);
}
/* Обе страницы помечаются сразу (персонаж чинится на каждой в свой кадр),
* поэтому цикл развёрнут: индекс-переменная заставляла 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)
{
uint16_t cols;
uint8_t r0, r1;
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;
}
cols = cd_cols_of(x, x + w - 1);
if (!cols) return;
r0 = cd_row_of(y);
r1 = cd_row_of(y + h - 1);
pop_cd_dmask[0][r0] |= cols;
pop_cd_dmask[1][r0] |= cols;
if (r1 != r0) {
pop_cd_dmask[0][r1] |= cols;
pop_cd_dmask[1][r1] |= cols;
if (r1 - r0 > 1) { /* прямоугольник накрыл все три ряда */
pop_cd_dmask[0][1] |= cols;
pop_cd_dmask[1][1] |= cols;
}
}
pop_cd_dirty = 3;
}
/* Задел ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 ВКЛЮЧИТЕЛЬНО) то, что
* трогали на странице p. Резидент: зовёт pop_cdraw.c из банка 4 прямым
* вызовом, без трамплина. */
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1)
{
uint16_t cols;
uint8_t r0, r1;
if (!(pop_cd_dirty & (1 << p))) return 0;
cols = cd_cols_of(x0, x1);
if (!cols) return 0;
r0 = cd_row_of(y0);
r1 = cd_row_of(y1);
if (pop_cd_dmask[p][r0] & cols) return 1;
if (r1 != r0) {
if (pop_cd_dmask[p][r1] & cols) return 1;
if (r1 - r0 > 1 && (pop_cd_dmask[p][1] & cols)) return 1;
}
return 0;
}
/* Страница приведена в порядок — снять с неё метку целиком. */
void pop_cd_clear(uint8_t p)
{
pop_cd_dmask[p][0] = pop_cd_dmask[p][1] = pop_cd_dmask[p][2] = 0;
pop_cd_dirty = (uint8_t)(pop_cd_dirty & ~(1 << p));
}
/* ---- Блит куска атласа ---------------------------------------------- */
/* Клипованный путь — ОТДЕЛЬНОЙ функцией. У него девять 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 */
/* Окно под точечную перерисовку/запечку — контракт в шапке pop_tile.h.
* Логическая пара нужна для отсева тайлов (он считается из ряда комнаты),
* экранная — для клипа самого блита; при перевороте они разные. */
void pop_t_win_set(int x, int ytop, int w, int h)
{
pop_t_fclip_x0 = x;
pop_t_fclip_x1 = x + w;
pop_t_fclip_ly0 = ytop;
pop_t_fclip_ly1 = ytop + h;
if (pop_upside) {
int t = FLIP_TOP(ytop, h);
pop_t_fclip_y0 = t;
pop_t_fclip_y1 = t + h;
} else {
pop_t_fclip_y0 = ytop;
pop_t_fclip_y1 = ytop + h;
}
pop_t_fclip_on = 1;
}
void pop_t_win_clear(void) { pop_t_fclip_on = 0; }
/* Клипованный путь блита. Габарит и смещения внутри куска — БАЙТЫ, а вся
* рабочая переменная — 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;
static 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);
}
void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
{
const uint8_t *img;
uint8_t w, h; /* БАЙТЫ, а не uint16_t — см. ниже */
int top;
if (idx >= a->count)
return;
/* Грубый отсев ДО маппинга страницы: реальные w/h лежат в заголовке
* ленты, то есть в EMM-странице, и чтобы их прочитать, надо сделать
* gfx_w0_map. Берём заведомо больший габарит (кадры фона не крупнее
* 64×64) и, если он не задевает окно, уходим не заплатив ни за
* atlas_image, ни за map/unmap. */
if (pop_t_fclip_on &&
(x >= pop_t_fclip_x1 || x + 64 <= pop_t_fclip_x0 ||
ybottom + POP_YOFF + 1 <= pop_t_fclip_ly0 ||
ybottom + POP_YOFF - 64 >= pop_t_fclip_ly1))
return;
pop_dbg_b1(); /* ВРЕМЕННО */
img = (const uint8_t *)atlas_image(a, idx);
gfx_w0_map(a->page);
pop_dbg_b2(); /* ВРЕМЕННО */
/* Габарит читаем БАЙТАМИ. С `uint16_t w = img[0] | (img[1] << 8)` SDCC
* разворачивал каждую такую строку в два десятка IX-относительных
* пересылок: 16-битные w/h не влезали в регистры, и функция целиком
* уезжала в стековый кадр (пролог `ld iy,#-14 / add iy,sp / ld sp,iy`,
* дальше всё через -N(ix) по 19 тактов). Замер 2026-08-13: до вызова
* ядра блита уходило ~6 700 тактов при том, что работы там — прочитать
* четыре байта заголовка и сравнить границы.
* Байтовый габарит корректен по построению: обе ветки ниже И ТАК требуют
* w < 256 && h < 256 (иначе ядро не примет), а кадры наших атласов не
* крупнее 32x63. Формат .atl допускает больше — такой кадр уходит на
* общий путь выше. */
if (img[1] | img[3]) { /* кадр больше 255 — редкий путь */
blit_b_oversize(img, x, ybottom);
gfx_w0_unmap();
return;
}
w = img[0];
h = img[2];
top = ybottom - (int)h + 1 + POP_YOFF; /* +YOFF: центрирование */
/* Переворот применяем ЗДЕСЬ, до клипа: дальше вся геометрия (окно
* fore-слоя, полоса у потолка, отсев по экрану) считается уже в
* экранных координатах результата. */
if (pop_upside) top = FLIP_TOP(top, h);
if (w && 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 && (top < POP_YOFF || top + (int)h > POP_CLIP_BOTTOM)) {
blit_b_clip(img, x, top, w, h);
gfx_w0_unmap();
return;
}
/* w<256 && h<256 больше не проверяем — гарантировано типом. */
if (x >= 0 && top >= 0 &&
x + (int)w <= 320 && top + (int)h <= 256) {
pop_dbg_b6(); /* ВРЕМЕННО: пометка «шли в noclip» */
if (pop_upside) gfx_blit_noclip_vflip(x, top, img);
else gfx_blit_noclip(x, top, img);
} else {
if (pop_upside) gfx_blit_part_vflip(x, top, img, 0, 0, (int)w, (int)h);
else gfx_blit_part(x, top, img, 0, 0, (int)w, (int)h);
}
pop_dbg_b3(); /* ВРЕМЕННО */
pop_cd_touch(x, top, (int)w, (int)h);
pop_dbg_b4(); /* ВРЕМЕННО */
} else {
blit_b_clip(img, x, top, w, h);
pop_dbg_b3(); pop_dbg_b4(); /* ВРЕМЕННО: медленный путь целиком в b3 */
}
}
gfx_w0_unmap();
pop_dbg_b5(); /* ВРЕМЕННО */
}
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); }
/* Heal по КОМНАТНОЙ координате (её и знают точечные перерисовки: чомпер,
* пики, loose, ворота, полоса потолка). При перевороте прямоугольник
* обязан отразиться ровно так же, как сам тайл, — иначе heal чистит одно
* место, а рисует в другое: именно так «ломалась анимация чомперов»
* (найдено пользователем на первом же прогоне зеркального фона). */
void pop_bar_black(int x, int top, int w, int h)
{
if (pop_upside) top = FLIP_TOP(top, h);
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK);
bar(x, top, x + w - 1, top + h - 1);
pop_cd_touch(x, top, w, h); /* bar идёт мимо pop_blit_b (DRAW-COST) */
}
void pop_heal_off(int x, int y, int w, int h)
{
int top = y + POP_YOFF;
if (pop_upside) top = FLIP_TOP(top, h);
pop_heal_fast(x, top, w, h);
}
/* ---- Чтение тайла отрисованной комнаты ------------------------------ */
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 */
}
/* ---- Зелье: склянка (tiles_10_potion, seg008 draw_tile_fore) --------- *
* Тип = (modif & 0xF8) >> 3: 0 = пусто (ничего не рисуем), 2..4 — «большая»
* склянка (id 13), остальные — маленькая (id 12). Спрайты из chtab_1 (атлас
* pop_pot), поэтому НЕ из env: раньше рисовали chtab_6 id 12 (res212),
* которого в VDUNGEON нет — оттуда и «мусорный объект». Позиция оригинала:
* xh = fore_x + draw_xh, xl = 6, ybottom = fore_y + draw_main_y.
*
* Здесь, а не в банке: склянку рисует и полная отрисовка тайла (холодная
* половина), и fore-проход поверх персонажа (горячая). */
void pop_potion_flask(int row, int col, uint8_t modif)
{
uint8_t type = (uint8_t)((modif & 0xF8) >> 3);
uint8_t id = 12;
if (type == 0) return;
if (type >= 2 && type < 5) id = 13; /* большая склянка */
if (pop_palace) id += 2; /* seg008:747: во дворце склянки другие */
pop_pot_b(id, (POT_FORE_X + POP_COL_XH[col]) * 8 + 6,
POT_FORE_Y + 63 * row + 62);
}
/* Окно 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;
}
}
/* Ряд 0 комнаты СНИЗУ (fg-коды) — нужен draw_tile_topright/floorright, когда
* тайл нижнего ряда стал видимым (empty после падения loose): «стена вниз» =
* топрайт тайла room_B(0, col-1). 0 — нет комнаты снизу (кромка = стена). */
void pop_room_set_below(const uint8_t *below_row0_fg) { pop_t_below = below_row0_fg; }
/* Ряд 2 (fg-коды + модификаторы) комнаты СВЕРХУ — для полосы кладки у
* потолка (seg008 draw_room: доп. верхний ряд из room_A, draw_tile_aboveroom
* при draw_main_y=-1). 0 — нет комнаты сверху (кромка уровня → пол). */
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod)
{ pop_t_above = above_row2_fg; pop_t_abovemod = above_row2_mod; }