Files
Sprinter-SDCC/applications/PoP/roomtest/pop_tile.c
T
snark13 980d48c168 Пики оставались навсегда: запечка фона консервировала выдвинутый кадр
Уровень 7, комната 19.  Кнопка в (0,5) при нажатии зовёт pop_floor_bake,
а тот перерисовывает и правого соседа — пики (0,6) — в банке, который
пишет в ОЗУ-копию.  Кадр брался живой, и выдвинутая пика оставалась в
фоне: дальше heal возвращал её каждый кадр.  В (0,7) чисто, потому что
окно клипа запечки кончается на x=219, а колонка 7 начинается с 224.

Флаг pop_t_bake_rest («в фон кладём покой») для этого и был, но читал его
только pop_loose_frame.  Кадр пик считался по месту в ЧЕТЫРЁХ слоях.
Добавлен pop_spike_frame — порт get_spike_frame (SDLPoP seg008:08A0),
которого у нас не было, — и все четыре слоя переведены на него; заодно
позу покоя в запечке стал отдавать pop_chomp_pose.

Одна точка лечит все пять путей запечки, включая вход в комнату.
Резидент +28 Б, скорость не затронута.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:53:44 +03:00

728 lines
42 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);
/* В ЗАПЕЧКЕ — поза ПОКОЯ, по той же причине, что у плиты и пик: фаза
* челюстей транзиентна. Покой и есть фаза 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 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);
}
/* Рабочие переменные 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)
{
/* Аргументы сразу в 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);
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);
gfx_w0_unmap();
return;
}
/* pb_w<256 && pb_h<256 больше не проверяем — гарантировано типом. */
if (pb_x >= 0 && pb_top >= 0 &&
pb_x + (int)pb_w <= 320 && pb_top + (int)pb_h <= 256) {
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);
}
}
gfx_w0_unmap();
}
/* Блит getimage-блоба из ОБЫЧНОЙ памяти (W2), а не из EMM-страницы атласа:
* те же клип полосы у потолка и окно перерисовки, что у pop_blit_b, но без
* atlas_image и без gfx_w0_map/unmap (это ещё ~1 350 такта). Заведено под
* композитный спрайт падающего куска (pop_room.c: три части сложены в один
* блоб, один вызов блита вместо трёх). */
void pop_mem_b(const uint8_t *img, int x, int ybottom)
{
uint8_t w = img[0], h = img[2];
int top;
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;
if (!w || !h) return;
top = ybottom - (int)h + 1 + POP_YOFF;
if (pop_upside) top = FLIP_TOP(top, h);
if (!pop_t_fclip_on && !pop_t_clip_top &&
!(pop_upside && (top < POP_YOFF || top + (int)h > POP_CLIP_BOTTOM)) &&
x >= 0 && top >= 0 && x + (int)w <= 320 && top + (int)h <= 256) {
if (pop_upside) gfx_blit_noclip_vflip(x, top, img);
else gfx_blit_noclip(x, top, img);
pop_cd_touch(x, top, (int)w, (int)h);
} else {
blit_b_clip(img, x, top, w, h);
}
}
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; }