diff --git a/applications/PoP/docs/resident_budget.md b/applications/PoP/docs/resident_budget.md new file mode 100644 index 0000000..ccc43e3 --- /dev/null +++ b/applications/PoP/docs/resident_budget.md @@ -0,0 +1,111 @@ +# Бюджет резидента W1/W2: как мерить и как освобождать + +Резидент huge-режима — окно `0x4100..0xBB00` (стек с 0xBB00): код в W1, +данные в W2, между концом данных и стеком остаётся куча. Всё, что туда не +влезло, живёт в банках. + +## Как СМОТРЕТЬ, а не гадать + +**Карта линкера врёт.** File-static SDCC в неё не попадает, и «дырка» между +двумя именованными символами приписывается предыдущему целиком. По карте +выходило, что у `pop_bg` 1529 Б данных (на деле 289), а у `pop_t_win_clear` +1282 Б кода — при том, что это однострочник, а 1282 Б это два статических +помощника соседнего `pop_blit_b`. + +Точный источник — объектные файлы: строки `A size flags ` в +`.rel` дают ровный размер каждой области модуля, а `S Def/Ref` — кто +символ определяет и кто на него ссылается. + +**Частоту вызовов мерить в MAME счётчиком**, а не оценивать по смыслу: + +``` +bpset ,1,{printf "F %d ...",temp0,...; temp0=0;...; g} +bpset ,1,{temp0=temp0+1; g} +``` + +Обязательна **канарейка** — счётчик заведомо горячей функции в том же +прогоне. Дважды спасала: один раз показала, что перехода комнаты в окне +замера не было (все нули), другой — что зонды вообще не встали (в zsh +`set -- $pair` НЕ разбивает строку на слова, и адрес уезжал в мусор). +Полную перерисовку комнаты форсировать читом `+`/`-`, ходьбой ненадёжно. + +## Сделано + +### 1. malloc вон из резидента (−613 Б) + +`cbl_open` держал `malloc`/`free` в мёртвой ветке `CBL_UNDERRUN_SILENCE`, а +линкер тянет `.rel` целиком — и куча приезжала каждому приложению. Разведены +две публичные точки входа (`cbl_open` / `cbl_open_silence`) поверх общего +`_cbl_open_raw`; `cbl_close` больше не зовёт `free`. + +### 2. Разрез pop_tile: холодная половина в банк 5 (−1788 Б) + +`pop_tile.c` был крупнейшим жильцом резидента (5 972 Б кода). Целиком он не +уедет: его const-таблицы (`POP_TILE_DIV/MOD`, `pop_tile_table`, таблицы +кадров) читают банки 2, 3, 7 и 8, а таблица в чужом банке не видна. + +Отбирали ЗАМЕРОМ, на двух тайлсетах (подземелье ур. 1 и дворец ур. 4 — +`pop_mem_b` рисует композитный кусок и мог оказаться дворцовым). Порог — +пик не больше 3 вызовов на кадр. + +| уехало в банк 5 | пик/кадр | | осталось в резиденте | пик/кадр | +|---|---:|---|---|---:| +| `pop_mem_b` | 0 | | `pop_tile_code` | 296 | +| `pop_cd_hit` (+`hit_rect`) | 0 | | `pop_cd_touch` | 198 | +| `pop_t_win_set/clear` | 0..1 | | `pop_blit_b` (+2 статика) | 184 | +| `pop_heal_off` | 0..1 | | `pop_wall_modifier` | 101 | +| `pop_potion_flask` | 0..1 | | `pop_env_b` | 73 | +| `pop_room_set_above/below` | 1 | | `pop_tile_mod` | 70 | +| `pop_cd_init/clear` | 1 | | `pop_cd_batch_end` | 40 | +| `pop_bar_black` | 3 | | `pop_fore_set_clip` | 2 | +| `pop_cd_hit_slot` | 2..3 | | все const-таблицы | — | + +`pop_fore_set_clip` (88 Б) оставлен намеренно: не стоит отказа от прямого +вызова из банка 4, ради которого он и заводился. + +**Цена трамплина замерена**: 252 такта пролог + 84 эпилог + ~50 на стороне +вызывающего = **~410 тактов** на вызов. Итого ~1 000 тактов на кадр покоя +(0,2 % работы) и ~3 700 на кадр редрава (0,009 растра). + +**Ключ, почему это безопасно:** вызов банк → резидент ПРЯМОЙ, трамплин не +нужен (W1/W2 замаплены всегда). Поэтому `blit_b_clip` просто перестал быть +`static` и объявлен в `_pop_tile.h`, а не переехал следом за `pop_mem_b`. + +### Итог + +| | было | стало | +|---|---:|---:| +| `_CODE` резидента | 24 329 | **21 928** | +| свободно до стека | **129 Б** | **2 535 Б** | +| BANK5 | 2 080 (13 %) | 3 954 (24 %) | + +Проверено в MAME: уровень 1 (подземелье) и уровень 4 (дворец), переходы +комнат читом `+`, ходьба — фон, факелы, решётки, гобелены, колонны без +искажений. + +## ЛОВУШКА: данные банка в его страницу — НЕ ДЕЛАТЬ без разбора + +Отдельная попытка (`--bank-data=SRC`, коммиты 3545826/9025573) **откачена**: +перенос писучих данных банкового модуля в его 16-КБ страницу давал цветной +мусор блоками и ронял DSS. + +У `pop_trob` причина найдена: `pop_trob_modif()` ВОЗВРАЩАЕТ УКАЗАТЕЛЬ на +`room_modif[24][30]`, а зовут её из банков 2, 3, 7 и резидента — после +переноса они пишут по 0xC000+ в СВОЮ страницу, поверх чужого кода. +Def/Ref-анализ такого не видит: снаружи ссылки на символ нет, есть ссылка на +функцию, отдающую его адрес. Но и `pop_room`, у которого утечки указателя +найти не удалось, ломался так же — механизм понят не до конца. + +Нулевая инициализация при этом ни при чём: `mkexe -p 0` был проверен по +образу (прогон нулей 14 304 Б, самый длинный прогон 0xFF — 14). + +**Перенос КОДА в банк — штатный путь, на нём стоят все наши банки. Ломался +именно перенос ДАННЫХ.** + +## Что осталось + +- `roomtest.c` 2 508 Б и `pop_kid.c` 2 418 Б — следующие по величине, но оба + горячие (главный цикл и `play_seq`). +- `pop_level.c` 956 Б кода + 660 Б данных (из них `pop_dl1`/`pop_dl2` по + 256 Б — таблицы дверных связей). +- BANK7 на 77 %: если понадобится место в нём — выносить `pop_redraw.c`. diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index 184b93c..8944583 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -35,7 +35,7 @@ MEMORY ?= huge # small-раскладка + банки кода в W # ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том # же ALLOCS — иначе сравниваются не правки, а уровни оптимизации. ALLOCS ?= 3000 -EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 4=pop_kdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c --bank 7=pop_redraw.c --bank 8=roomtest_cold.c --bank 8=pop_level_cold.c --bank 8=pop_kboot.c --bank 8=pop_guard_cold.c --bank 8=pop_shadow.c --bank 8=pop_sfx_cold.c $(PROF_FLAGS) +EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 4=pop_kdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 5=pop_tile_cold.c --bank 6=pop_trob.c --bank 7=pop_redraw.c --bank 8=roomtest_cold.c --bank 8=pop_level_cold.c --bank 8=pop_kboot.c --bank 8=pop_guard_cold.c --bank 8=pop_shadow.c --bank 8=pop_sfx_cold.c $(PROF_FLAGS) # Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по # умолчанию, пока идёт работа с производительностью; make PROF=0 выключает. # Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте diff --git a/applications/PoP/roomtest/_pop_tile.h b/applications/PoP/roomtest/_pop_tile.h new file mode 100644 index 0000000..2069088 --- /dev/null +++ b/applications/PoP/roomtest/_pop_tile.h @@ -0,0 +1,18 @@ +/* + * _pop_tile.h — внутренний контракт между половинами общих листьев слоя + * фона (НЕ публичный, публичный — pop_tile.h). Горячая половина живёт в + * резиденте (pop_tile.c), холодная — в банке 5 (pop_tile_cold.c); + * критерий разреза и замеры — в шапке pop_tile_cold.c. + */ +#ifndef _POP_TILE_INTERNAL_H +#define _POP_TILE_INTERNAL_H + +#include + +/* Клипованный путь блита — статик горячей половины, ставший общим: его + * зовёт pop_mem_b из холодной. Переносить его следом НЕЛЬЗЯ (его основной + * вызывающий pop_blit_b делает 184 вызова на кадр редрава), а вызов + * банк -> резидент и так прямой: W1/W2 замаплены всегда. */ +void blit_b_clip(const uint8_t *img, int x, int top, uint8_t w, uint8_t h); + +#endif diff --git a/applications/PoP/roomtest/pop_bg.h b/applications/PoP/roomtest/pop_bg.h index 9d27417..a1c864b 100644 --- a/applications/PoP/roomtest/pop_bg.h +++ b/applications/PoP/roomtest/pop_bg.h @@ -64,12 +64,12 @@ void pop_room_draw(uint8_t room_num, * [10] — тайл (0,9) комнаты СНИЗУ-СЛЕВА (сосед угла (2,0); см. * load_rowbelow, seg008:368). 0 — нет комнаты снизу. */ /* НЕ __banked: тело в резиденте (pop_tile.c) — из банка зовётся прямым call. */ -void pop_room_set_below(const uint8_t *below_row0_fg); +void pop_room_set_below(const uint8_t *below_row0_fg) __banked; /* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы * кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 — нет * комнаты сверху (кромка уровня → пол). */ -void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod); +void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked; /* Перерисовать передний слой (fore) и оверлеи тайлов ПОВЕРХ персонажа — * ОДИН проход на всех Char (Кид, страж, скелет, дальше тень/визирь): в diff --git a/applications/PoP/roomtest/pop_cdraw.h b/applications/PoP/roomtest/pop_cdraw.h index f05fb15..3c9552d 100644 --- a/applications/PoP/roomtest/pop_cdraw.h +++ b/applications/PoP/roomtest/pop_cdraw.h @@ -128,12 +128,12 @@ extern uint8_t pop_cd_dirty; /* биты страниц: ме extern uint8_t pop_cd_ymin[2][10], pop_cd_ymax[2][10]; /* Задели ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 включительно) то, * что трогали на странице p. Резидент (pop_tile.c). */ -uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1); +uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1) __banked; /* То же для СЛОТА персонажа: спрайт и накладной, координаты изнутри pop_cd * (см. тело — там про цену пяти аргументов). */ -uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p); -void pop_cd_clear(uint8_t p); /* снять метку страницы целиком */ -void pop_cd_init(void); /* пустые диапазоны на обеих страницах */ +uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p) __banked; +void pop_cd_clear(uint8_t p) __banked; /* снять метку страницы целиком */ +void pop_cd_init(void) __banked; /* пустые диапазоны на обеих страницах */ void pop_cd_touch(int x, int y, int w, int h); #define POP_CD_TOUCH_ALL() pop_cd_touch(0, 0, 320, 256) diff --git a/applications/PoP/roomtest/pop_tile.c b/applications/PoP/roomtest/pop_tile.c index f1e745f..da9fdab 100644 --- a/applications/PoP/roomtest/pop_tile.c +++ b/applications/PoP/roomtest/pop_tile.c @@ -17,6 +17,7 @@ #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; @@ -307,89 +308,6 @@ void pop_cd_touch(int x, int y, int w, int h) /* Задел ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 ВКЛЮЧИТЕЛЬНО) то, что * трогали на странице p. Резидент: зовёт pop_cdraw.c из банка 4 прямым * вызовом, без трамплина. */ -/* Прямоугольник слота против метки — БЕЗ передачи пяти аргументов. - * - * pop_cd_hit принимает (p, x0, y0, x1, y1): три последних идут стеком, и - * функция целиком уезжает в IX-фрейм — 45 % её тактов уходит на `-n(ix)` - * (замер asm 2026-08-19). А зовут её из cd_quiet дважды на слот, то есть - * до восьми раз за кадр. Здесь координаты берутся прямо из pop_cd, и - * аргументов остаётся два. - * - * Спрайт и накладной проверяются РАЗДЕЛЬНО — объединённый bbox ловит - * касания углом, которых нет (разбор в cd_quiet, pop_cdraw.c). */ -static int hit_x, hit_y; /* аргументы hit_rect — file-scope, не стек */ -static uint16_t hit_w, hit_h; -static uint8_t hit_p; - -static uint8_t hit_rect(void) -{ - int8_t c0, c1, c; - uint8_t ya, yb; - int x1 = hit_x + (int)hit_w - 1; - int y1 = hit_y + (int)hit_h - 1; - if (!hit_w || !hit_h) return 0; - if (x1 < 0 || hit_x > 319) return 0; - if (y1 < 0 || hit_y > 255) return 0; - ya = (uint8_t)(hit_y < 0 ? 0 : hit_y); - yb = (uint8_t)(y1 > 255 ? 255 : y1); - c0 = (int8_t)((hit_x < 0 ? 0 : hit_x) >> 5); - c1 = (int8_t)((x1 > 319 ? 319 : x1) >> 5); - for (c = c0; c <= c1; c++) - if (ya <= pop_cd_ymax[hit_p][c] && yb >= pop_cd_ymin[hit_p][c]) return 1; - return 0; -} - -uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p) -{ - const pop_cdraw_t *s = &pop_cd[who]; - if (!(pop_cd_dirty & (1 << p))) return 0; - hit_p = p; - hit_x = s->x[p]; hit_y = s->y[p] + POP_YOFF; - hit_w = s->w[p]; hit_h = s->h[p]; - if (hit_rect()) return 1; - if (!s->ovalid[p]) return 0; - hit_x = s->ox[p]; hit_y = s->oy[p] + POP_YOFF; - hit_w = s->ow[p]; hit_h = s->oh[p]; - return hit_rect(); -} - -uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1) -{ - int8_t c0, c1, c; - uint8_t ya, yb; - if (!(pop_cd_dirty & (1 << p))) return 0; - if (x1 < 0 || x0 > 319) return 0; - if (x0 < 0) x0 = 0; - if (x1 > 319) x1 = 319; - if (y1 < 0 || y0 > 255) return 0; - ya = (uint8_t)(y0 < 0 ? 0 : y0); - yb = (uint8_t)(y1 > 255 ? 255 : y1); - c0 = (int8_t)(x0 >> 5); - c1 = (int8_t)(x1 >> 5); - /* Пересечение отрезков [ya,yb] и [ymin,ymax] хотя бы в одной колонке. - * Пустая колонка держит ymin = 255, ymax = 0 — условие ниже её отсеет - * само, отдельной проверки «пусто» не нужно. */ - for (c = c0; c <= c1; c++) - if (ya <= pop_cd_ymax[p][c] && yb >= pop_cd_ymin[p][c]) return 1; - return 0; -} - -/* Страница приведена в порядок — снять с неё метку целиком. */ -/* Начальное состояние обеих страниц — «ничего не трогали». ЯВНО, а не - * расчётом на обнуление _DATA: пустая колонка обозначается ymin = 255, - * ymax = 0, а нули от crt0 читались бы как «затронута строка 0». */ -void pop_cd_init(void) -{ - pop_cd_clear(0); - pop_cd_clear(1); -} - -void pop_cd_clear(uint8_t p) -{ - uint8_t c; - for (c = 0; c < 10; c++) { pop_cd_ymin[p][c] = 255; pop_cd_ymax[p][c] = 0; } - pop_cd_dirty = (uint8_t)(pop_cd_dirty & ~(1 << p)); -} /* ---- Блит куска атласа ---------------------------------------------- */ /* Клипованный путь — ОТДЕЛЬНОЙ функцией. У него девять 16-битных локалей, @@ -399,27 +317,6 @@ void pop_cd_clear(uint8_t p) * перерисовка тайлов — идёт с 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, НЕ локали. @@ -447,7 +344,7 @@ 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) +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; @@ -653,59 +550,12 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) 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) @@ -757,25 +607,6 @@ uint8_t pop_wall_modifier(int row, int col) 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. Резидент — чтобы вызов из банка шёл без трамплина. */ @@ -791,13 +622,3 @@ void pop_fore_set_clip(int x, int y, int w, int 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; } diff --git a/applications/PoP/roomtest/pop_tile.h b/applications/PoP/roomtest/pop_tile.h index 5bee865..8de2a70 100644 --- a/applications/PoP/roomtest/pop_tile.h +++ b/applications/PoP/roomtest/pop_tile.h @@ -93,8 +93,8 @@ extern uint8_t pop_t_fclip_on; * ВАЖНО: пока окно стоит, pop_blit_b НЕ ставит пометку «фон трогали» (признак * fore-прохода) — вызывающий обязан пометить прямоугольник САМ (pop_cd_touch * или pop_bar_black, который метит сам). */ -void pop_t_win_set(int x, int ytop, int w, int h); -void pop_t_win_clear(void); +void pop_t_win_set(int x, int ytop, int w, int h) __banked; +void pop_t_win_clear(void) __banked; /* ---- Таблица тайлов (piece, seg008.c:27) ---------------------------- */ typedef struct { @@ -154,7 +154,7 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom); /* Блит getimage-блоба из обычной памяти (W2) — без atlas_image и без * маппинга страницы; клип полосы у потолка и окно перерисовки те же, что у * pop_blit_b. Для композитных спрайтов, собранных в рантайме. */ -void pop_mem_b(const uint8_t *img, int x, int ybottom); +void pop_mem_b(const uint8_t *img, int x, int ybottom) __banked; void pop_env_b(uint8_t id, int x, int yb); void pop_wall_b(uint8_t id, int x, int yb); @@ -162,14 +162,14 @@ void pop_fore_b(uint8_t id, int x, int yb); void pop_pot_b(uint8_t id, int x, int yb); /* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose. */ -void pop_heal_off(int x, int y, int w, int h); +void pop_heal_off(int x, int y, int w, int h) __banked; /* Чёрная плита по ЭКРАННЫМ координатам, с учётом переворота (POP_FLIP_TOP). * Пишет банком 0x50 — и в видео-ОЗУ, и в ОЗУ-копию, иначе heal вернёт * стёртое. ВСЕ стирания слоя фона обязаны идти через неё, а не через голый * bar: иначе при перевороте плита ложится в неотражённое место (так * ломались решётка и плита-потолок). */ -void pop_bar_black(int x, int top, int w, int h); +void pop_bar_black(int x, int top, int w, int h) __banked; /* ---- Чтение тайла отрисованной комнаты ------------------------------ */ uint8_t pop_tile_code(int row, int col); @@ -183,6 +183,6 @@ uint8_t pop_wall_modifier(int row, int col); /* Склянка зелья (draw_tile_fore, chtab_1): нужна и полной отрисовке тайла, и * fore-проходу поверх персонажа. */ -void pop_potion_flask(int row, int col, uint8_t modif); +void pop_potion_flask(int row, int col, uint8_t modif) __banked; #endif diff --git a/applications/PoP/roomtest/pop_tile_cold.c b/applications/PoP/roomtest/pop_tile_cold.c new file mode 100644 index 0000000..f4daa3f --- /dev/null +++ b/applications/PoP/roomtest/pop_tile_cold.c @@ -0,0 +1,240 @@ +/* + * pop_tile_cold.c — ХОЛОДНАЯ половина общих листьев слоя фона (банк 5). + * + * ЗАЧЕМ РАЗРЕЗ. pop_tile.c был самым крупным жильцом резидента (5 972 Б + * кода), а резидент huge-режима — это всё окно 0x4100..0xBB00, и место в нём + * нужно под музыку, заставки и UI. Но целиком pop_tile не уедет: его + * const-таблицы (POP_TILE_DIV/MOD, pop_tile_table, таблицы кадров) читают + * банки 2, 3, 7 и 8, а таблица в чужом банке не видна. + * + * КРИТЕРИЙ ОТБОРА — ЗАМЕР, а не догадка. Каждая функция pop_tile была + * посчитана брейкпоинтом-счётчиком в MAME: сколько вызовов в кадре покоя и + * сколько в кадре ПОЛНОЙ перерисовки комнаты (её форсирует чит +/-). Сюда + * переехало только то, у чего пик <= 3 вызовов на кадр — и проверено это + * на ДВУХ тайлсетах: подземелье (уровень 1) и дворец (уровень 4), потому + * что pop_mem_b рисует композитный кусок и мог оказаться дворцовым. + * Замер: pop_mem_b 0, pop_cd_hit 0, pop_t_win_set 0..1, pop_heal_off 0..1, + * pop_room_set_above 1, pop_bar_black 3, pop_potion_flask 0..1, + * pop_cd_hit_slot 2..3 — при канарейке pop_blit_b 184..214 в том же кадре. + * + * ЦЕНА. Трамплин замерен: 252 такта пролог + 84 эпилог + ~50 на стороне + * вызывающего = ~410 тактов на вызов. На кадр покоя выходит ~1 000 тактов + * (0,2 % работы кадра), на кадр редрава ~3 700 (0,009 растра). + * + * ЧТО ОСТАЛОСЬ В РЕЗИДЕНТЕ и почему: pop_blit_b (184 вызова на редрав) со + * своими статиками blit_b_clip/blit_b_oversize, pop_cd_touch (198), + * pop_tile_code (296), pop_wall_modifier (101), pop_env_b (73), + * pop_tile_mod (70), pop_cd_batch_end (40) — и все const-таблицы. + * pop_fore_set_clip оставлен намеренно: 88 Б не стоят отказа от прямого + * вызова из банка 4, ради которого он и заводился. + * + * ВЫЗОВЫ ОТСЮДА В РЕЗИДЕНТ — прямые, без трамплина: W1/W2 замаплены всегда. + * Поэтому blit_b_clip (статик горячей половины) просто перестал быть static + * и объявлен в _pop_tile.h, а не переехал следом. + */ + +#include +#include +#include +#include "pop_bg.h" /* POP_YOFF, POP_PLAYFIELD_H */ +#include "pop_map.h" /* pop_upside — переворот (зелье инверсии) */ +#include "pop_cdraw.h" /* pop_cd, pop_cd_dirty, pop_cd_ymin/ymax */ +#include "pop_tile.h" +#include "_pop_draw.h" /* pop_heal_fast */ +#include "_pop_tile.h" /* blit_b_clip — из горячей половины */ + +#define FLIP_TOP(top, h) POP_FLIP_TOP(top, h) /* формула — в pop_bg.h */ + +/* Смещение склянки внутри тайла (seg008 draw_tile_fore): те же числа, что в + * tile_table горячей половины — дублируем, потому что там они локальные + * define'ы, а тащить их в публичный заголовок ради одной функции незачем. */ +#define POT_FORE_X 2 +#define POT_FORE_Y (-3) + +/* Прямоугольник слота против метки — БЕЗ передачи пяти аргументов. + * + * pop_cd_hit принимает (p, x0, y0, x1, y1): три последних идут стеком, и + * функция целиком уезжает в IX-фрейм — 45 % её тактов уходит на `-n(ix)` + * (замер asm 2026-08-19). А зовут её из cd_quiet дважды на слот, то есть + * до восьми раз за кадр. Здесь координаты берутся прямо из pop_cd, и + * аргументов остаётся два. + * + * Спрайт и накладной проверяются РАЗДЕЛЬНО — объединённый bbox ловит + * касания углом, которых нет (разбор в cd_quiet, pop_cdraw.c). */ +static int hit_x, hit_y; /* аргументы hit_rect — file-scope, не стек */ +static uint16_t hit_w, hit_h; +static uint8_t hit_p; + +static uint8_t hit_rect(void) +{ + int8_t c0, c1, c; + uint8_t ya, yb; + int x1 = hit_x + (int)hit_w - 1; + int y1 = hit_y + (int)hit_h - 1; + if (!hit_w || !hit_h) return 0; + if (x1 < 0 || hit_x > 319) return 0; + if (y1 < 0 || hit_y > 255) return 0; + ya = (uint8_t)(hit_y < 0 ? 0 : hit_y); + yb = (uint8_t)(y1 > 255 ? 255 : y1); + c0 = (int8_t)((hit_x < 0 ? 0 : hit_x) >> 5); + c1 = (int8_t)((x1 > 319 ? 319 : x1) >> 5); + for (c = c0; c <= c1; c++) + if (ya <= pop_cd_ymax[hit_p][c] && yb >= pop_cd_ymin[hit_p][c]) return 1; + return 0; +} + +uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p) __banked +{ + const pop_cdraw_t *s = &pop_cd[who]; + if (!(pop_cd_dirty & (1 << p))) return 0; + hit_p = p; + hit_x = s->x[p]; hit_y = s->y[p] + POP_YOFF; + hit_w = s->w[p]; hit_h = s->h[p]; + if (hit_rect()) return 1; + if (!s->ovalid[p]) return 0; + hit_x = s->ox[p]; hit_y = s->oy[p] + POP_YOFF; + hit_w = s->ow[p]; hit_h = s->oh[p]; + return hit_rect(); +} + +uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1) __banked +{ + int8_t c0, c1, c; + uint8_t ya, yb; + if (!(pop_cd_dirty & (1 << p))) return 0; + if (x1 < 0 || x0 > 319) return 0; + if (x0 < 0) x0 = 0; + if (x1 > 319) x1 = 319; + if (y1 < 0 || y0 > 255) return 0; + ya = (uint8_t)(y0 < 0 ? 0 : y0); + yb = (uint8_t)(y1 > 255 ? 255 : y1); + c0 = (int8_t)(x0 >> 5); + c1 = (int8_t)(x1 >> 5); + /* Пересечение отрезков [ya,yb] и [ymin,ymax] хотя бы в одной колонке. + * Пустая колонка держит ymin = 255, ymax = 0 — условие ниже её отсеет + * само, отдельной проверки «пусто» не нужно. */ + for (c = c0; c <= c1; c++) + if (ya <= pop_cd_ymax[p][c] && yb >= pop_cd_ymin[p][c]) return 1; + return 0; +} + +/* Страница приведена в порядок — снять с неё метку целиком. */ +/* Начальное состояние обеих страниц — «ничего не трогали». ЯВНО, а не + * расчётом на обнуление _DATA: пустая колонка обозначается ymin = 255, + * ymax = 0, а нули от crt0 читались бы как «затронута строка 0». */ +void pop_cd_init(void) __banked +{ + pop_cd_clear(0); + pop_cd_clear(1); +} + +void pop_cd_clear(uint8_t p) __banked +{ + uint8_t c; + for (c = 0; c < 10; c++) { pop_cd_ymin[p][c] = 255; pop_cd_ymax[p][c] = 0; } + pop_cd_dirty = (uint8_t)(pop_cd_dirty & ~(1 << p)); +} + +/* Окно под точечную перерисовку/запечку — контракт в шапке pop_tile.h. + * Логическая пара нужна для отсева тайлов (он считается из ряда комнаты), + * экранная — для клипа самого блита; при перевороте они разные. */ +void pop_t_win_set(int x, int ytop, int w, int h) __banked +{ + 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) __banked +{ pop_t_fclip_on = 0; } + +/* Блит 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) __banked +{ + 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); + } +} + +/* Heal по КОМНАТНОЙ координате (её и знают точечные перерисовки: чомпер, + * пики, loose, ворота, полоса потолка). При перевороте прямоугольник + * обязан отразиться ровно так же, как сам тайл, — иначе heal чистит одно + * место, а рисует в другое: именно так «ломалась анимация чомперов» + * (найдено пользователем на первом же прогоне зеркального фона). */ +void pop_bar_black(int x, int top, int w, int h) __banked +{ + 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) __banked +{ + int top = y + POP_YOFF; + if (pop_upside) top = FLIP_TOP(top, h); + pop_heal_fast(x, top, w, h); +} + +/* ---- Зелье: склянка (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) __banked +{ + 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); +} + +/* Ряд 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) __banked +{ 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) __banked +{ pop_t_above = above_row2_fg; pop_t_abovemod = above_row2_mod; }