Commit Graph

8 Commits

Author SHA1 Message Date
snark13 e501982457 Проверка «задет ли слот» без пяти аргументов: минус 3 684
pop_cd_hit принимает (p, x0, y0, x1, y1) — три последних идут стеком, и
функция целиком уезжает в IX-фрейм: 45 % её тактов на `-n(ix)` (asm).
А зовут её из cd_quiet до восьми раз за кадр.

Новый pop_cd_hit_slot(who, p) берёт координаты прямо из pop_cd, а само
сравнение вынесено в hit_rect с file-scope аргументами.  Первая попытка —
обёртка, которая внутри всё равно звала pop_cd_hit — не дала ничего
(1799 Z80 вместо 1318, то есть стало хуже), и это записано здесь, чтобы
не повторять: снимать аргументы со стека нужно у ТОГО, кто их читает.

asm на путь «спрайт + накладной»: было 1799 + 2x1318 = 4435 тактов Z80,
стало 1221 + 2x1009 = 3239 (-27 %).

Замер 11/15, лёгкая позиция: синяя 219 894 -> 218 052, циан 41 280 ->
39 438, работа кадра 442 662 -> 438 978.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:56:50 +03:00
snark13 767d6f78a4 P15: метка «фон трогали» стала точной — минус 167 880 тактов на кадре
Две правки, обе про ложные срабатывания пропуска отрисовки персонажа.

1. МЕТКА: вместо «маска колонок по 32 px на ТРИ ряда по 63 px» теперь на
   каждую колонку хранится диапазон затронутых y (ymin/ymax, 40 байт на обе
   страницы).  Прежняя гранулярность склеивала касания внутри ряда: пламя
   факела занимает y 33..50, клинок стоящего стража — y 59..65, между ними
   девять пикселей зазора, а метка считала слот задетым.

2. ПРОВЕРКА: cd_quiet сверяет с меткой спрайт и накладной (клинок, брызги)
   ДВУМЯ ОТДЕЛЬНЫМИ прямоугольниками, а не объединённым bbox.  Объединение
   включает пустой угол между ними, и он ловил касания, которых нет: спрайт
   стража лежит в колонке 8, клинок уходит в колонку 7 на y 59..65, пламя
   метит колонку 7 на y 33..50 — прямоугольник «спрайт + клинок»
   (x 241..284, y 46..84) цеплял метку углом.

Без второй правки первая почти ничего не дала (632 676 против 628 542 до
неё): объединённый bbox продолжал ловить ложное пересечение.

Замер 11/15:

  фаза      до P15    после
  синяя    259 050   223 902   (heal тоже перестал платить)
  зелёная  181 494   181 494
  циан     194 262    59 406
  работа   632 676   464 796

Проверено в MAME: в статике картинка чистая, в динамике (пробежка, бой,
переход в соседнюю комнату) хвостов и просвечивания нет.  Хост-тесты
зелёные.

Заодно найден и исправлен собственный баг первой редакции: обе страницы
обновлялись по условию, проверяющему только страницу 0, и после
pop_cd_clear(0) метка страницы 1 переставала расти.  Плюс pop_cd_init:
пустая колонка обозначается ymin = 255, а нули от crt0 читались бы как
«затронута строка 0».

У ОРИГИНАЛА такой метки нет вовсе: и Apple II (FRAMEADV.S RedBlockFast,
шесть буферов по блокам), и SDLPoP (set_redraw_fore) метят целыми тайлами,
но им это не мешает — персонаж у них рисуется каждый кадр безусловно.
Пропуск неизменившегося персонажа — наша добавка, поэтому и точность метки
нужна выше оригинальной.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:04:46 +03:00
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
snark13 b27b31318a Разбор draw_tile: модель цены блита + пакетная пометка pop_cd_touch
Замер (MAME, комната 23 ур.13) разложил цену одного gfx_blit_noclip
регрессией по 339 блитам, высоты 3..63:

    такты = 8791 + 198.2 * h + 5.96 * (w * h)

Модель ложится на весь диапазон (32x3 -> 9 891, 32x13 -> 13 887,
32x60 -> 32 073), разброс внутри размерной группы — ТРИ такта.

Выводы:
- 5.96 на байт — предел железа: байт идёт через акселератор дважды
  (burst src->память акселератора, burst ->экран), по 3 такта на проход
  при системном клоке 21 МГц.  Ускорять передачу нечем.
- 198 на строку — цикл _bgi_blit_rows_raw; при w=32 это половина
  построчной цены.
- 8791 на ВЫЗОВ — крупнейшая статья, НЕ объяснена.  Проверено, что это не
  W3-скобка (в обеих половинах по 5 инструкций) и не прерывания (разброс
  3 такта).  Один тайл = 5-6 спрайтов ~ 107 000 тактов, из них ~50 000 —
  постоянные накладные вызовов.  Это и есть главный резерв.

Заодно: pop_cd_touch собирается в ПАКЕТ на тайл (pop_cd_batch_begin/end,
скобка в draw_tile) вместо вызова на каждый кусок — раньше 2 866 тактов
на спрайт, 15% цены блита.  ВНИМАНИЕ: выигрыш замером НЕ подтверждён —
в захваченных кадрах скобка не сработала (блиты шли из холодной отрисовки
комнаты, мимо draw_tile).  Правка безопасна по построению: объединение
прямоугольников может пометку только расширить, не сузить.

Снятые сегодня неверные утверждения (чтобы не всплыли):
- «клипованный блит дороже полного» — артефакт сравнения разных выборок
  спрайтов; 32x60 с клипом до полосы стоит 7 236, то есть клип работает;
- «блиты идут программным циклом со скоростью ldir» — нет, ядро на
  акселераторе, см. модель выше;
- «отложить запекание на кадр» — НЕЛЬЗЯ: запекание пишет ОЗУ-копию фона,
  из которой heal восстанавливает; отложенное даёт призрак плиты.

Все 8 наборов tests-host проходят.  Оснастка замера временная.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 23:36:48 +03:00
snark13 8f0362f3d4 L4-MIRROR шаги 3 и 5 + левый клип колонок в libbgi
libbgi: новая gfx_blit_cols_part_wx — обрезка СЛЕВА (skipw) в дополнение к
верхней (skip/rows) и правой (maxw).  По подсказке пользователя сделано
примитивом, а не обходным путём «нарисовать и вернуть фон поверх лишнего»:
для column-major левая обрезка стоит ровно столько же, сколько правая —
колонка это непрерывный кусок ОЗУ, меняется стартовая колонка источника и
экранная X.  Внутри это уже было (так клипается левый край экрана), наружу
не было выведено.  Полное тело блита колонками переехало туда,
gfx_blit_cols_part_w стала обёрткой (skipw=0).  size-check: OK, роста нет
(62 программы, -22..-33 Б на пользователей блита).

Шаг 5 — клип ТЕНИ (seg008:1699): на уровне зеркала она показывается только
СПРАВА от него, obj_clip_left = 137 + (mirror_column-4)*32.

Шаг 3 — ОТРАЖЕНИЕ (check_mirror, seg003:0798): пока Кид стоит на тайле
зеркала, каждый кадр рисуется его зеркальная копия с клипом
left = (curr_col<<5)+9 и top = y_clip[curr_row+1].  Отдельной функцией
pop_mirror_draw, а НЕ третьим слотом Char — это структура самого оригинала:
отражение идёт сокращённым путём load_frame_to_obj + add_objtable(4), без
клинка, брызг, fore-прохода и пропуска кадра; гейтить всё это в общем теле
значило бы добавить ветки в самый горячий путь.  Свой heal (pop_mirror_heal)
рядом с pop_char_heal, в skip-маске не участвует.

tests-host: заглушки pop_mirror_draw/pop_mirror_heal (pop_map теперь на них
ссылается), все 5 наборов прошли.

Цена: _CODE 24501 -> 24629, банк 3 10551, банк 4 8267 -> 9590.
НЕ ПРОВЕРЕНО В MAME.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 22:09:45 +03:00
snark13 4718bff767 Уровень 4: skip по маске тайлов + порт loose_land, ворота 0xFF, пламя в фон
ОПТИМИЗАЦИЯ.  Метка «фон трогали» была одним union-прямоугольником на
страницу, и три факела комнаты склеивались в полосу x 40..280 на всю
комнату — Кид, стоящий между крайними факелами, терял пропуск перерисовки
и каждый кадр платил полным fore-проходом.  Теперь это маска тайлов
(uint16_t pop_cd_dmask[2][3]: бит = колонка, слово = ряд, набор = страница),
проверка — три AND через резидентный pop_cd_hit.  Гранулярность тайла — это
гранулярность оригинала (redraw_frames_anim[tilepos], set_wipe;
подтайловое уточнение там только по высоте, wipe_heights).  Замер на (1,7):
циан 233 515 -> 24 781, работа за кадр 517 609 -> 306 553, период 4 -> 3
растровых кадра.

BUG-LOOSE-BUTTON-1.  Упавшая плита не нажимала кнопку.  Три слоя: порт
loose_land не звал trigger_button вовсе; pop_room_col_landing считала
площадкой только чистый пол, а у оригинала их семь (пол, пика, обе кнопки,
зелье, оба факела); сигнал приходил в момент ОТРЫВА плиты, из-за чего
ворота начинали открываться, пока она ещё в воздухе.  Нажатие в оригинале
ОДНО, но с button_type = tiles_14_debris — это «открыть НАСОВСЕМ»
(modifier 0xFF), и кнопка съедается.  Посадка в комнате снизу переехала на
новый сигнал pop_loose_exit (взводит mob_tick_one, когда кусок ушёл ниже
поля).

BUG-GATE-FF-1.  0xFF был перегружен: сторожевое «тайла нет» в gate_modif и
живое «открыто навсегда» из trigger_gate.  gate_passable заворачивал Кида в
воротах, нарисованных открытыми.  Мёртвая ветка убрана.

BUG-TORCH-CHOMP-1.  Чомпер (0,7) комнаты 23 healит x 224..255 / y 30..93 и
стирал пламя факела (0,6), которое рисуется в ячейке правого соседа.  Фикс —
запекать пламя (GFX_BANK_NORMAL): у факела все девять кадров на общем
канвасе 16x18 и непрозрачны, протухнуть в ОЗУ-копии нечему, а heal чомпера
сам возвращает огонь и кладёт челюсти поверх — z-порядок как в оригинале.
Пузырёк зелья так нельзя (ползёт вверх, нужен heal) — остаётся в SPRITE.

Заведено открытым: died_on_button (seg007:776) не портирован — нужен тайл
tiles_5_stuck в атласе.

Проверено пользователем в MAME (уровень 4: плита 16(1,1) -> кнопка 17(0,1)
-> ворота 23(0,9); комната 23 с чомпером и факелом); make -C tests-host —
все 5 наборов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:52:04 +03:00
snark13 a25ce58869 DRAW-COST шаг 1: пропуск неизменившегося персонажа — 210% -> 116% кадра
Персонаж, у которого с прошлой отрисовки ЭТОЙ страницы дабл-буфера не
изменился ни один вход отрисовки, а фон в его прямоугольнике не трогали,
уже нарисован правильно: heal, блит и fore-проход пропускаются целиком.
Не спецкейс «мёртвый страж», а общее правило — покрывает и труп, и
стоящего Кида, и ждущего стража.

Механизм: снимок входов по страницам (pop_cdraw.c, cd_sig/cd_quiet) +
позиционная метка «фон трогали вот здесь» (pop_cd_touch в резидентном
pop_tile.c, зовёт сам pop_blit_b).  Решение перепроверяется перед
отрисовкой, а pop_char_draw страхуется собственным heal — если тик всё-таки
сдвинул персонажа, прошлый кадр стирается там.  Слоты рядом (32 px) —
перерисовываем оба, иначе heal соседа выест кусок из «тихого».

Метка обязана быть ПОЗИЦИОННОЙ: с флагом «фон трогали хоть где-то» выигрыш
был ровно нулевым — факелы анимируются каждый кадр и гасили пропуск для
всех сразу (597 684 такта, как без оптимизации).

Замеры (MAME, брейкпоинты по totalcycles, бюджет кадра 430 000):
  комн. 1.3, труп стража, Кид стоит: 210 % -> 116 % (500 772 такта),
  ноль вызовов pop_heal_fast за кадр, весь фон — 2 блита (44 136);
  комн. 1.1, Кид стоит вдали от факелов: 404 112 (94 %), цикл 4 -> 3 кадра.

Узкое место сместилось на ЛОГИКУ: 60 % кадра уходит на тик персонажей,
которые СТОЯТ, ещё 28 % — на loose_tick + process_trobs в комнате без
единой ловушки.  Разбивка и план — TASKS_OPEN.md#draw-cost.
2026-08-08 18:39:00 +03:00
snark13 bbf91d10ee DRAW-CHAR: отрисовка одна на всех Char; разгрузка банка 2 (90.4% -> 72.9%)
DRAW-CHAR.  Отрисовка персонажа сведена к одному набору функций над Char —
как физика после GUARD-PHYS.  В оригинале add_kid_to_objtable (seg008:22F0) и
add_guard_to_objtable (seg008:2324) имеют идентичное тело и различаются
окном (loadkid/loadshad), набором спрайтов и типом объекта, а
redraw_at_char/redraw_at_char2 гейтов по charid не имеют вовсе.

  pop_gdraw.c -> pop_cdraw.c: pop_char_draw/heal/fore(who), слот
  POP_CH_KID / POP_CH_OPP; состояние слотов pop_cd[] в _DATA — читается из
  любого банка без трамплина.  Проход окклюзии тоже один
  (pop_fore_over_char), pop_fore_over_kid больше нет.

Починилось само (расхождения, которые и были ценой дублирования): у
соперника не было clip_char; у Кида не было клипа полем 192 и ветки брызг
«мёртв/падение»; char_width_half СТРАЖА считался по спрайту КИДА.

Замер: _CODE 24 881 -> 20 524 (куча 2023 -> 6333), BANK2 -265, итого -3.2 КБ.
Проверено пользователем в MAME; циан-полоса профиля подросла — оптимизация
заведена отдельной задачей DRAW-COST.

MEM-BANK2, шаг 1: общие «листья» слоя фона в РЕЗИДЕНТ (pop_tile.c/.h).
Ограничение платформы: писучие данные банка лежат в _DATA и видны всем, а
const-таблицы — в странице банка, из другого банка их не прочитать; трамплин
же выбирается объявлением, то есть __banked на листе бьёт и по горячим
вызывающим (654 такта).  W1 замаплено всегда — оттуда обе половины зовут
листья прямым call и читают таблицы напрямую.

MEM-BANK2, шаг 2: дедуп внутри банка.  wall_pattern 808 -> 394 и wall_rnd
786 -> 654: четыре ветки по виду стены отличались только набором кусков и
числами в одной серии prandom — сведены к таблицам WP_PARTS и WR_RULE,
порядок вызовов prandom сохранён дословно.

Заодно: kid_seq_off больше не static const в kid_data.h (230 Б мёртвой копии
в каждом из 9 модулей) — генератор pop_extract_kid_data.py отдаёт
макро-инициализатор, массив определяет один pop_kid.c.

Итог: BANK2 14 815 -> 11 942 (72.9 %, свободно 4442 Б), _CODE 22 556,
куча 4301 Б.  tests-host зелёные (65/39/53/1723/1); в MAME комната 1
совпала с дорефакторным снимком попиксельно (0 из 227 520), комната 3 —
та же раскладка кладки.

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