Commit Graph

29 Commits

Author SHA1 Message Date
snark13 0086ac80c2 Таблицы анимации Кида: kid_data.bin -> KID\kid.ani
Все ресурсы Кида теперь зовутся kid.*: kid.arc (атласы), kid.pal
(палитра), kid.ani (кадры + seqtbl). Расширение .ani говорит о
содержимом: это не «какие-то данные», а таблица кадров и байткод
последовательностей движения.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 22:02:14 +03:00
snark13 65797af44c Ресурсы — в архивы PBA1; тайминги сцены PV — по часам насоса CBL
ТАЙМИНГИ.  Пользователь заметил, что заклинание Джафара не совпадает с
музыкой. Замер в MAME (breakpoint на вспышке + курсор трека): реплика m53
успевала ДОИГРАТЬ до молнии, хотя по шкале оригинала между входом Джафара
и заклинанием ровно 822 тика = 13,7 с.

Причина: пейсинг считал НАШИ ожидания vsync, а не прошедшее время.
Полноэкранная копия страницы стоит больше кадра луча, и разница копилась.
Кадровые прерывания для счёта не годятся (теряются в di-окнах
акселератора), поэтому часами стал насос CBL: он идёт от расхода буфера
железом, 85,4 Гц, и ему безразлично, чем занят главный цикл. Один тик
оригинала = 57/40 тика насоса (0,07 % ошибки, без деления в кадре).
Когда звук выключен, работает прежний путь по кадрам луча.

После фикса замер даёт 229 блоков остатка m53 против расчётных 233 —
расхождение 47 мс.

АРХИВЫ.  Группы ресурсов сложены в PBA1: kid (58 атласов), pv (78),
shadow (32), bg (25), title (20), guard (10), vizier (5), skel (4). Было
232 файла на образе, стало 8 архивов плюс шесть палитр и таблицы уровней.
При цене `open` 51,4 мс против 32,6 мс за чтение 16 КБ это возвращает
секунды на каждой загрузке.

- pop_pack_arc.py печатает индексы элементов константами (ARC_<GRP>_<FILE>),
  порядок задаёт Makefile, а серии код проверяет статически;
- atlas_load разделён: atlas_attach принимает уже прочитанную страницу,
  поэтому чтение из архива не дублирует проверку магии и подготовку W0;
- имена архивов живут в pop_arc.c и адресуются номером. Строковый литерал
  лежит в rodata своего банка, и указатель на него из другого банка после
  переключения W3 показывает на чужие данные — ровно так падала загрузка
  фона (поймано брейкпоинтом на puts).

Проверено в MAME: заставка, интро и уровень 1 собираются из архивов,
Тень грузит все 32 страницы. Host-тесты: 15 наборов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 21:29:14 +03:00
snark13 47a4b084c4 Меню, статус-строка и оболочка игры: title/intro/cutscene/HoF, POP.CFG, палитры
Эта сессия (меню + текст в служебных полосах):

* Меню: рамка выделения считается от силуэта текста (SEL_PAD сверху и
  снизу), а не «на глаз»; все экраны центрируются в игровых 200 строках
  (UI_CENTER_TOP/UI_CENTER_FIELD) и в них помещаются; Enter и Space —
  равноправные клавиши действия (ui_action_down).
* Settings: убраны SHOW SPRINTER SCREEN и BACK, добавлены DEBUG BAR и
  ABOUT.  About показывает тот же текст, что стартовый Sprinter screen,
  минус строка про клавиши — общий about_text(), чтобы экраны не
  разъехались.  CONTROLS собирается таблицей и центрируется по
  фактическому числу строк.
* GAME PAUSED переехала в нижнюю статус-строку, как в оригинале
  (SDLPoP rect_bottom_text = {193,70,202,250}): это состояние программы,
  а не пункт меню.  POP_HP_Y вынесен в pop_cdraw.h — полосу делят два
  модуля.
* pop_status.c (банк 9) — текст в обеих служебных полосах.  Нижняя:
  порт display_text_bottom + таймера (QUICKSAVE/QUICKLOAD/SOUND ON/OFF,
  24 тика).  Верхняя отладочная переведена с палочек на текст
  «Level ##, Room ##, Speed: …, Sound: …, Immortal #» малым шрифтом, с
  своим форматированием чисел (без printf и без деления).
  Заявка сообщения — запись одного байта pop_status_msg: резидент W1/W2
  не растёт, весь рендер в банке.  Бюджет после правок не изменился
  (_CODE 23981, куча 267 Б).
* Цена вывода: блит глифа ~4,6 тыс. тактов независимо от размера, поэтому
  всё change-driven, отладочная строка перерисовывается ПО ПОЛЯМ, пробелы
  не блитятся вовсе, а вход в комнату заливает только игровое поле
  (pop_screen_fill_field) — борта от комнаты к комнате не меняются.
* Интро: в PV-сцене зазвучали пропавшие эффекты оригинала — закрытие
  ворот (4) и открытие двери покоев (51), из которой входит Джафар.
* docs/status_line_text.md — полная инвентаризация ВСЕХ текстов SDLPoP в
  статус-строке: геометрия, семантика text_time_total как идентификатора
  сообщения, мигание, рестарт по истечении 36/288.

Вместе с этим выкладывается накопленная работа по оболочке полной игры:
автомат состояний (pop_app), title, intro/PV и cutscene, attract-demo,
Hall of Fame, глобальный таймер, настройки и POP.CFG, модуль палитр и
fade, звуковой набор, host-тесты на новые швы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:58:36 +03:00
snark13 30bcc3459b Атлас Тени: запечённый набор вместо спрайтов стража
Оригинал кладёт спрайт дважды — прозрачным блитом в x и XOR-блиттером в
x+1; пакетный блит так не умеет, поэтому результат запечён упаковщиком.
Две половины, как и у оригинала: sk* — кадры вне боя (спрайты Кида),
sf* — кадры 150..189 (SHADOW.DAT, тоже графика Кида).  251 спрайт,
32 EMM-страницы, палитра 16 цветов в 0xA0..0xAF.

Закрывает BUG-SHADOW-SET: раньше тип 4 уходил в guard_names, и Тень в
бою дралась серым стражем.

Грабля: kid.pal заливает все 256 записей и затирает слоты Тени —
палитра вынесена в pop_shadow_pal_apply рядом с pop_bg_pal_apply.

Проверено в MAME на 6-м уровне: силуэт с контуром, как в оригинале.
2026-08-20 11:22:53 +03:00
snark13 a6e39070af Фиксированный логический кадр по лучу + режимы FASTEST/FAST/NORMAL
Период стал max(n, ceil(W)) вместо ceil(W)+2: три gfx_wait_vsync после
работы отсчитывались от её КОНЦА, поэтому бюджет кадра был один растр.
Теперь ждём от якоря начала кадра, и при n=3 бюджет 1 290 240 тактов.

Счёт кадров — программный, по биту 5 порта 0xFE (положение луча), а не по
кадровым прерываниям: те теряются в DI-окнах акселератора фазозависимо
(замер: 0..2,8 %, на полной перерисовке три подряд).  Условие точности
одно — зазор между выборками меньше 64 512 тактов; точки выборки
расставлены по замеру, а не на глаз.

Режимы (pop_pace.h), клавиша P по кругу, дефолт FASTEST.  Условие боя
взято у оригинала буквально (SDLPoP seg003.c:363): Kid.sword ==
SWORD_2_DRAWN, а не «идёт бой».

Проверено в MAME на 11/15: счётчик без недосчёта на 270 кадров, период
ровно 3 растра на 302 логических кадрах (ни длиннее 3,1, ни короче 2,9),
NORMAL даёт ровно 4, с вынутым мечом — ровно 5.
2026-08-19 23:06:27 +03:00
snark13 79bcabde94 Габариты спрайтов в байтах: uint16_t -> uint8_t в слоте отрисовки
Замечание пользователя: спрайты наших атласов не крупнее 64x64, а w/h
почти везде были uint16_t.  Это уже записано в памяти проекта
(pop_sprite_size_limits: весь игровой кадр PoP <= 56x63; больше 255 только
восемь полноэкранных подложек титров, а они через слот персонажа не
проходят).

Переведены в uint8_t: w/h, ow/oh, fpw/fph, cw/ch в pop_cdraw_t, параметры
cd_overlay_add и cd_clip_add, локали w/h/vis_w в pop_char_draw и
cd_splash, и чтение габарита из шапки ленты (было двухбайтным сложением
со сдвигом).

Эффект: лёгкая позиция 438 600 -> 438 324 (там персонажи не рисуются,
поэтому почти ничего), тяжёлая — циан 181 404 -> 180 270.  Плюс 24 байта
_DATA на двух слотах.

Скромно, но код от этого не запутаннее, а честнее: тип теперь отражает
реальный диапазон.  Проверено в MAME — бой идёт, хвостов и обрезков нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:00:19 +03:00
snark13 6f077b0d6d Разбор P14: он сводится к P4 (цене блита)
Fore-проход Кида в тяжёлой позиции (89 268) разложен зондами:

  вход + set_clip + char_footprint   10 872
  арифметика границ окна              3 786
  шов ворот + overlay-цикл            3 294
  ЦИКЛ fore_tile ПО ТАЙЛАМ           67 854   76 %
  gate_over_char + хвост              3 462

Счётчик показал, что цикл обходит ВСЕГО 4 тайла, и 3 из них реально
рисуют.  То есть 67 854 — не перебор лишних тайлов и не проверки, а цена
самих блитов переднего слоя: около четырёх блитов по ~16 000, где 6 765
на каждом — фиксированная накладная.

Значит отдельной оптимизации fore-прохода почти нет: срезать можно цену
блита (P4, ~27 000 из 67 854), char_footprint из физики (P10) и слияние
двух трамплинов в банк 2 (~4 000).

P4 поднят в очереди: он бьёт и по fore-проходу (4 блита), и по зелёной
фазе (8 блитов) — то есть работает и в динамике, и в статике.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:29:19 +03:00
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 379c513087 cd_quiet: сравнение снимка без построения структуры — минус 8 016
cd_sig_make СТРОИТ структуру из тринадцати полей в стековом кадре (то есть
через -n(ix)), и только потом шёл побайтовый цикл сравнения.  А зовётся
проверка четыре раза за кадр: pop_char_skip_mask дважды, и в ней по два
слота.

Новый cd_sig_same сравнивает поля прямо с источником, с ранним выходом на
первом расхождении — у двигающегося персонажа это обычно первое же поле.
cd_sig_make остался: он нужен pop_char_draw, чтобы снимок записать.

Замер 11/15, лёгкая позиция: участок «mob_draw + guard_over_kid +
skip_mask» 47 883 -> 43 875, циан 59 265 -> 55 257, синяя 223 902 ->
219 753, работа кадра 464 796 -> 456 780.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:33:31 +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 6faf81016a Замер цианной фазы: крупного лишнего в отрисовке персонажа нет
Циан 188 004 не двигался ни от P1, ни от P5, ни от P2b — разложил его
зондами.

Хорошая новость: Кид УЖЕ пропускается (204 такта на pop_char_draw), то
есть надежда P3 сбылась после P1 — метка от чомпера до него больше не
дотягивается.  Страж же перерисовывается каждый кадр честно: пламя
правого факела (0,7) рисуется в ячейке (0,8), где он стоит, и реально
накрывает ему голову (пламя занимает y 5..22, страж 12..62).

Отрисовка стража — 148 302:

  pop_char_fore (2 трамплина в банк 2 + обход тайлов)  62 778   42 %
  клинок (sword_draw + overlay_add + clip_add)         27 522   19 %
  блит спрайта + clip_char_right                       20 982   14 %
  загрузка кадра и геометрия                           12 696    9 %
  pop_clip_char_top (трамплин банк 4 -> банк 3)         8 658    6 %
  снимок прямоугольника + cd_clip_add                   7 890    5 %
  gfx_w0_unmap + cd_sig_make                            4 968    3 %
  вход + cd_heal                                        2 946    2 %

Единственная явно лишняя статья — трамплин clip_char_top, и снять его
непросто: функции нужны get_tile и таблицы деления из банка 3, перенос в
резидент вернёт тот же трамплин внутрь.  Остальное — работа, которую
персонаж действительно делает.

Зонды переставлены с уже закрытых замеров (loose_tick, физика) внутрь
pop_cdraw; оснастка снимается позицией P12, когда оптимизация закончится.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:53:49 +03:00
snark13 085a198c20 Мигание тени: кадр приходил из кэша, а снимок писался по Guard.frame
Тень на уровне 6 стояла на двух страницах дабл-буфера в разных позах.
Отрисовка брала image из кэша kid_frame/pop_gframe, который наполняет тик,
а снимок пропуска кадра писала по Char.frame — расхождение застревало
навсегда, потому что снимок совпадал и страница больше не перерисовывалась.

Кадр теперь грузит сама отрисовка, как в оригинале (add_*_to_objtable →
load_fram_det_col, seg008:22F0/2324).  Разбор — BUGS_CLOSED.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 11:48:45 +03:00
snark13 67d62e3e8b Уровни 12 и 13: тень, Джафар, падающие плиты + разбор слоёв
Уровень 12 (тень) — порт seg002/seg006:
- check_shadow: подъём тени в комнате 15 по условию «меч подобран»,
  init_shad_12, вход падением (seq 7);
- autocontrol_shadow_level12: ждать Кида, бой, сближение, СЛИЯНИЕ;
- общий урон (ранил тень — ранил себя) и check_killed_shadow
  (убил тень — убил себя);
- таймер вспышки слияния 42 -> -1, sword_disappears при уходе из
  комнаты 18, появление плит в комнатах 2/13 после слияния.

Переход 12 -> 13 бесшовный: двери у уровня нет, он кончается фактом
попадания в комнату 23 (tbl_seamless_exit); флаг pop_seamless не даёт
сбросить HP.  Чит навигации по комнатам этот триггер придерживает —
иначе комнату 23 двенадцатого уровня не посмотреть в принципе.

Уровень 13 (Джафар): guard_notice_timer (фора после встречи),
on_guard_killed + Jaffar_exit, check_fall_flo (гряда плит сверху) и три
исключения loose-механики.  Плюс спрайты визиря VIZIER.DAT — без них он
рисовался обычным стражем; заодно цвет из данных комнаты применяется
только к обычному стражу, как в оригинале.

Падающие плиты — семь дефектов, найденных прогонами:
- MOB_MAX 4 -> 14: check_fall_flo роняет шесть плит разом, лишние
  терялись без щебня;
- одиночные сигналы посадки/провала больше не затираются в кадре;
- честный loose_fall: сбитая плита рождается в том же кадре, от места
  удара, с половинной скоростью;
- при снятии плиты метится и сосед справа (висел передний торец);
- heal и отрисовка кусков разнесены на два прохода;
- куски рисуются ПОСЛЕ фона, порядок между собой — по убыванию y
  (compare_curr_objs для пары 0x80 сортирует наоборот);
- wall_pattern убран из ceil_over_kid_tile: у оригинала узор из
  draw_tile_bottom идёт в фон, а не в передний слой.

Хост-тесты: два новых набора, t_shadow (45 проверок) и t_jaffar (44).
TK_ROOMS 8 -> 24 — спецсобытия адресуют комнаты по реальным номерам.

Осознанные расхождения (docs/impl_diff.md): нет мигания Кида спрайтами
тени при слиянии, чит навигации не запускает бесшовный переход, чит K
убивает через штатный путь смерти.

Открытым остаётся разбор слоёв: MOB-CLIP-RIGHT, MID-OVERLAY-LAYER,
GUARD-FALLOUT-VICTORY, BG-ONCE (BUGS_OPEN.md / TASKS_OPEN.md).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 18:21:52 +03:00
snark13 948d8f08f2 Переворот: перерисовка вместо отражения, переключение на границе кадра, clip_char
Три правки по следам прогона пользователя на зелье инверсии.

1. ЛИШНИЕ ЯЗЫКИ ПЛАМЕНИ.  pop_flip_screen отражал уже нарисованное копией
   акселератора, а пламя факела ЗАПЕКАЕТСЯ в ОЗУ-копию (pop_torch_draw: heal'а
   у него нет, следующий кадр непрозрачно накрывает предыдущий).  Отражённое
   вместе с фоном, оно оказывалось в зеркальной позиции, где накрывать его
   некому — и оставалось навсегда (уходило только после входа в комнату,
   который строит фон с нуля).  Теперь переворот ПЕРЕРИСОВЫВАЕТ комнату тем же
   приёмом, что вход (ENTER-ROOM-FAST).  Это совпадает с оригиналом: SDLPoP на
   need_redraw_because_flipped зовёт redraw_screen(0), а не отражает картинку
   (seg000.c:928).

2. МОМЕНТ ПЕРЕКЛЮЧЕНИЯ.  Зелье выпивается из play_seq, в середине кадра, и
   pop_upside переключался прямо там — остаток кадра рисовался зеркально
   поверх ещё неперевёрнутого фона.  Разделены pop_upside_want (пишут зелье,
   смерть, чит U) и pop_upside (читают слои); переключение — одно место, начало
   кадра, вместе с перерисовкой.  Оригиналу этого не нужно: он всегда рисует в
   offscreen неперевёрнутым и зеркалит только на выводе — расхождение записано
   в docs/impl_diff.md.

3. CLIP_CHAR В ПЕРЕВОРОТЕ.  Граница clip_char приходит в ЛОГИЧЕСКИХ
   координатах (y_clip), сравнивать её с уже отражённым top нельзя: то, что
   логически выше линии, на экране ниже неё.  Теперь тот же срез применяется
   с другого конца — укорачивает кадр снизу (bcut), верх остаётся на месте;
   строки ленты не сдвигаются, потому что зеркальная лента отдаёт их в
   обратном порядке.  Раньше в перевороте клип был отключён совсем, и висящий
   Кид рисовался поверх плиты.

Проверено в MAME: переворот чистый, фон без остатков.  tests-host 6/6.
2026-08-12 23:33:01 +03:00
snark13 5b6664ffce Зеркальные кадры персонажей — готовыми файлами, а не разворотом в рантайме
Замеры в MAME (такты Z80, кадр = 430 000) решили вопрос однозначно:
  загрузка 28 атласов Кида с HDD  — 27 160 092 такта (1.26 с), 970 003 на страницу;
  разворот ОДНОЙ страницы на месте — 12 645 582 такта (0.59 с), т.е. 771 такт/байт.
Даже идеальный вариант (копия accel'ом ~0.25 кадра + asm-реверс ~2.2 кадра)
дал бы ~1.05 млн на страницу — то есть в лучшем случае сравнялся бы с чтением
готового файла.  На 34 страницы: 1.5 с загрузки против 20 с разворота.

* Упаковщики пишут второй набор: vflip_cols/vflip_page в pop_pack_kid.py ->
  kid0_v.atl…kid27_v.atl, sword_v.atl; pop_pack_guard.py -> g0_v.atl…g4_v.atl
  ТОЛЬКО для обычного стража (на уровне 9 tbl_guard_type = 0; тень рисуется
  атласами Кида, скелет/визирь с переворотом не встречаются).
* pop_vflip.c схлопнулся до таблицы «страница -> зеркальная пара»; весь
  побайтовый разворот и аллокация EMM удалены.
* pop_vflip_load_all (банк 8) грузит набор ОДНИМ вызовом и откуда угодно —
  задел под загрузку ресурсов во время интро.  Зовётся при старте и при
  смене уровня, если это POP_UPSIDE_LEVEL; чит U на других уровнях поднимает
  набор лениво (страховка в главном цикле).
* Стартовый уровень стал параметром сборки: make LEVEL=N (-DFIRST_LEVEL).

Проверено в MAME на уровне 9: переворот мгновенный, pop_flip_screen = 897 263
такта (2.1 кадра) — это сама копия экрана, загрузки в кадре больше нет.
tests-host 6/6.
2026-08-12 23:17:22 +03:00
snark13 755190674c L9-INVERT II.4: персонажи рисуются перевёрнутыми (зеркальные страницы атласов)
Спрайты персонажей хранятся column-major (ради бесплатного H-флипа), а
вертикальное зеркало у такой раскладки — разворот байтов ВНУТРИ колонки,
чего accel не умеет.  Поэтому pop_vflip.c готовит зеркальные КОПИИ страниц
атласов: копия побайтово повторяет раскладку .atl (заголовок, каталог,
смещения лент), развёрнуты только пиксели — значит вызывающему достаточно
подменить номер страницы, которую он мапит в W0.

* Кэш ленивый (страница зеркалится при первом обращении в перевёрнутом
  виде) и живёт до смены уровня — зелье переключает состояние туда-обратно,
  перезеркаливать по 16 КБ на каждый переворот незачем.  EMM хватает: 34
  страницы из ~215 свободных.
* pop_cdraw/pop_kdraw: страница + позиция (top = 191 - obj_y для персонажа,
  192 - top - h для клинка).  «Пропустить skip строк сверху» у зеркальной
  ленты = «срезать снизу» у оригинала, поэтому клипы считаются как обычно.
* Модуль резидентный: он маппит W3 (приёмник копии) — банку так нельзя.
* pop_vflip_reset на смене уровня: копии сделаны из старых страниц.

Проверено в MAME: чит U переворачивает всё разом, Кид и факелы вверх ногами,
полоса HP на месте, бег без следов на фоне (heal бьёт в те же координаты).

ОСТАЁТСЯ (L9-CLIPCHAR): clip_char в перевороте должен резать снизу, а не
сверху — пока при pop_upside не режем вовсе.  Тени/брызг это не касается.
2026-08-12 22:25:18 +03:00
snark13 147cb185b1 Резидент: pop_kid/pop_guard/main расколоты по частоте вызова (куча 6669 Б)
MEM-COLD3, шаги 3-5.  Кандидаты выбирались не по размеру, а по тому, КТО
зовёт: если единственный вызывающий уже в банке, код едет к нему и трамплин
не появляется вовсе.

* pop_kdraw.c -> БАНК 4 (к pop_cdraw.c): клинок в руке (pop_sword_draw) и
  блит спрайта chtab_2 по id (pop_kid_img_blit).  Оба звал ровно pop_cdraw,
  теперь вызовы прямые — контракт и условие корректности в _pop_kdraw.h.
* pop_kboot.c -> БАНК 8: загрузка атласов Кида и страницы kid_data.bin
  (раз за игру).  Дескриптор страницы — _pop_kid.h.
* pop_guard_cold.c -> БАНК 8: страж идёт за Кидом + вход стража в комнату.
  Оба зовёт roomtest_cold.c из того же банка — снова без трамплина.
* Из main в банк 8 уехали pop_boot (загрузка ресурсов, палитра, первый
  экран), pop_level_switch (смена уровня) и pop_shutdown.  FIRST_LEVEL
  переехал в pop_tune.h — константа нужна обеим половинам цикла.

_CODE 23005 -> 19861, куча 3525 -> 6669 Б.  Банк 8 — 6.5 из 16 КБ.
tests-host 6/6 (в наборы добавлены eng_pop_kboot и eng_pop_guard_cold).
2026-08-12 22:03:34 +03:00
snark13 361c9d9f78 Мышь уровня 8 + холодная половина главного цикла в банк 8
Спецсобытие уровня 8 (SDLPoP seg003:0545/0ABA, seg002:07EB): когда дверь
уровня открыта, а Кид остаётся в комнате 16, через 150 кадров приходит мышь,
пробегает справа налево по верхнему ряду, наступает на кнопку (0,7) —
решётка (0,3) открывается — и убегает.

Ассетов не потребовалось: кадры 186..188 идут по таблице Кида, их спрайты
(images 130..132) уже лежат в kid16.atl, отрисовка подхватывает мышь сама.

- guards.c: pop_check_mouse, autocontrol_mouse, ветки мыши в
  autocontrol_opponent / play_guard / check_can_guard_see_kid;
- pop_leveldoor_open стал word, как в оригинале (в него же считается
  задержка — байт переполнился бы через 255 кадров);
- leave_guard больше не записывает тень и мышь в данные уровня (seg002:02F5);
- полосы HP у мыши нет (seg000:1159);
- tests-host/t_mouse: 17 проверок, единственный набор с guards.c.

Разгрузка резидента (куча в huge упала до 879 Б): pop_start_level,
find_start_level_door, чит-навигация по комнатам и лейбл номера уехали в
roomtest_cold.c (--bank 8, n_banks = 8).  _CODE 25653 -> 24825, куча -> 1707 Б;
кадровый путь не тронут.

Проверено: tests-host 6/6, make size-check OK, живьём в MAME (мышь появилась,
нажала кнопку, решётка поехала вверх, мышь ушла).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:21:20 +03:00
snark13 9ecea17ba6 Уровень 6: переход на 7-й, портал, многокомнатное падение плиты
Всё проверено пользователем в MAME.

1. Переход 6 -> 7 падением.  leave_room (seg002:0504) для «вниз» на уровне 6
   из комнаты 1 даёт особый результат −2, главный цикл (seg000:0893) читает
   его как ++next_level; вход на 7-м (set_start_pos, seg003:0196) ставит Кида
   в комнату 17 и сразу переводит экран на комнату под ней.  Проверка ОБЯЗАНА
   идти раньше связи вниз: у комнаты 1 сосед снизу есть (шахта, комната 3).

2. BUG-BALCONY-RIGHT: правая половина портала не рисовалась.  id 12 стоял в
   FORE_ENV_IDS (в tile_table он fore_id ЗЕЛЬЯ), но зелье рисуется из
   chtab_1, а в chtab_6 под этим номером — правая часть арки балкона 32x62,
   идущая в backtable.  Спрайт уезжал в fore-атлас, env-страница получала
   дырку (idx 12: w=0 h=0).  Найдено трассой POP_TRACE_BT + чтением каталога
   .atl; дифф комнаты с оригиналом 1151 -> 375.

3. BUG-BELOWROW-WALL: жёлтые треугольники в шахте падения.  load_rowbelow
   (seg008:368) при отсутствующей комнате снизу подставляет tiles_0_empty
   колонкам 1..9 и tiles_20_wall только левому краю; у нас стеной забивались
   все 11 позиций.

4. BUG-MOB-MULTIROOM: честный mob_down_a_row (seg007:1387) — кусок переходит
   в комнату снизу и летит дальше.  Плита (1,7) комнаты 6 пролетает шахту
   комнаты 7 и жмёт кнопку (2,7) комнаты 11, открывая ворота комнаты 18.

5. BUG-MOB-STALE-PAGE: чистка следа куска стояла под гейтом «в отрисованной
   комнате» — улетевший вниз оставлял себя на одной из страниц навсегда.

6. BUG-CHAR-STALE-PAGE: тень застывала на двух страницах в РАЗНЫХ позах.
   Прямоугольник и valid пишутся внутри `if (w && h)`, а снимок состояния
   брался безусловно — при нулевом спрайте слот залипал «тихим».  Остаток
   (почему спрайт нулевой) заведён как SPRITE-ZERO-W0 в BUGS_OPEN.

NEXT_SESSION переписан: следующая задача — зацеп за край пола в свободном
падении (вход на уровень 7).  tests-host 5/5, size-check чист.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:01:58 +03:00
snark13 3913f1eb7d Отражение в зеркале: fore-проход поверх него (ноги/голова вылезали из арки)
Симптом (скриншоты пользователя, ур. 4): у отражения видны ноги ниже
нижней кромки зеркала, а на прыжке — голова и руки выше верхней.  В
оригинале и то и другое скрыто.

Причина: над отражением не шёл fore-проход.  В SDLPoP отражение уходит в
objtable (add_objtable(4), seg003:0798), и передний слой тайлов накрывает
его на общих основаниях — у зеркала это fore_id 77, ПЕРЕДНЯЯ ЧАСТЬ АРКИ.
Именно она прячет всё, что вылезло из проёма.  Клип obj_clip_top этого не
делает и делать не может: при curr_row = 0 он равен y_clip[1] = 3, то есть
режет только по верху поля.

У нас отражение рисуется мимо конвейера персонажей (сокращённый путь, как
и в оригинале), поэтому проход надо звать руками — ровно как это делает
pop_char_fore для слотов: pop_fore_set_clip по нарисованному
прямоугольнику + pop_fore_over_char(&Char, ...) с ЛОГИЧЕСКОЙ x кадра.

Порядок сходится сам: pop_check_mirror стоит до отрисовки персонажей, так
что получается отражение -> его fore -> Кид -> его fore, и каждый ставит
своё окно клипа.

Банк 4: 9590 -> 9770.  tests-host: все 5 наборов прошли.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 22:19:14 +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 844fa6d767 L4-MIRROR шаг 4: прыжок сквозь зеркало и рождение тени
Порт seg003:0798..08A9 + seg004:0239 + seg002:081D/1131 + seg006:1945.

- is_obstacle (pop_map.c): ветка зеркала — Кид, кадры бегового прыжка
  39..43, направление ВЛЕВО -> modif = 0x56, pop_jumped_mirror = -1,
  препятствия нет (пролетает насквозь).
- mirror_image / jump_through_mirror / pop_check_mirror (pop_map.c):
  отражённый Char уходит в слот Guard как CHARID_1_SHADOW, guardhp =
  hitp_max, у Кида hitp_curr = 1.  savekid НЕ делается — как в оригинале,
  отражается только копия.  Полосы HP перерисует pop_hp_draw сам.
- pop_check_mirror() зовётся из главного цикла ПЕРЕД отрисовкой персонажей
  (в оригинале — первая строка draw_people, seg008:228A).
- autocontrol_shadow + autocontrol_shadow_level4 + clear_char (guards.c):
  тень идёт СВОЕЙ веткой целиком, к стражьему ИИ не сводится — на уровне 4
  она не дерётся, а бежит влево и при x < 80 исчезает.

АТЛАС ТЕНИ — вскрылось при чтении seg006:0532.  Тень вне боевых кадров
150..189 ходит по таблице КИДА, и image оттуда индексирует спрайты Кида,
а не стража.  Выбор атласа в pop_cdraw шёл по СЛОТУ, то есть тень
рисовалась бы спрайтами стража.  Условие вынесено в
pop_frame_tbl_is_guard() (pop_kid.c) — его теперь читают и load_frame, и
отрисовка, разъехаться не могут.  Заодно из pop_load_fram_det_col выделен
pop_load_frame() без determine_col: jump_through_mirror берёт ось
отражения из curr_col, и пересчёт колонки по x там был бы вреден.

Константы MIRROR_* и DIR_56_NONE переехали в pop_guard.h — нужны и
постановке тайла (банк 6), и ИИ тени (банк 1).

Звука sound_45_jump_through_mirror в порте нет, пропущен.

Шаг 5 (клип тени слева от зеркала) НЕ сделан и оказался не однострочником:
в pop_cdraw есть клип сверху/снизу/справа, левого нет вовсе — нужен новый
примитив либо срез исходных колонок.  Расписано в TASKS_OPEN.

Цена: _CODE +18 Б, банк 1 2311 -> 2367, банк 3 10328 -> 10551,
банк 4 8243 -> 8267.  tests-host: все 5 наборов прошли.
НЕ ПРОВЕРЕНО В MAME — сценарий проверки записан в TASKS_OPEN.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 21:46:30 +03:00
snark13 fc0ede91e1 scr_x: байтовая таблица x/7 вместо словарной — 1 такт быстрее, -1152 Б
Гипотеза «двухбайтная индексация съест выигрыш от сложения» не
подтвердилась.  Собраны ОБА варианта, такты посчитаны по сгенерированному
asm (хвост после проверки границ):

  int16_t готовое: add hl,hl / add hl,de / ld e,(hl) / inc hl / ld d,(hl)
                   = 132 такта, 2 304 байта
  int8_t  x/7:     add hl,bc / ld a,(hl) / ld l,a / rlca / sbc a,a /
                   ld h,a / add hl,de  = 131 такт, 1 152 байта

Расширение знака плюс 16-битное сложение стоят ровно столько же, сколько
лишний add hl,hl при двухбайтном индексе, а обращений к памяти на одно
меньше — под wait-state'ами Sprinter (2,4x номинала именно на обращениях
к ОЗУ) байтовый вариант ещё чуть выгоднее номинала.

Банк 4: 9 394 -> 8 243 из 16 384 (свободно 8 141 вместо 6 990) — запас под
рост pop_cdraw, о котором и был вопрос.

Тест переименован в geom_mul8div7_table_rules и проверяет ОБА правила
генерации таблицы: тождество 8x/7 == x + x/7 и усечение к нулю.
tests-host: [geom] 1992 -> 3144, все 5 наборов прошли.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 20:43:27 +03:00
snark13 8175121d25 scr_x: таблица готовых значений на весь диапазон, включая отрицательные
Первая версия крыла только 0..255 байтовой таблицей x/7 по тождеству
8x/7 == x + x/7.  Это было мимо: obj_x = 2*fwd - 116 уходит в минус, как
только fwd < 58 (левее x_bump[5]) — то есть у ЛЕВОЙ КРОМКИ комнаты, и там
мы продолжали звать __divsint.

Границы взяты из данных, а не на глаз: kid_data.bin даёт dx кадров Кида
-5..+10, стража -2..+10; при Char.x типа uint8_t и render_dx из
{-140,0,+140} полный диапазон obj_x = -416..695.  SCRX[1152] кроет
-448..703 — деление стало недостижимым, оставлено страховкой.

Хранится ГОТОВОЕ значение (int16_t), а не x/7: байтовая таблица вдвое
меньше, но со знаковыми значениями требует расширения знака плюс
16-битного сложения — те же такты, что лишний add hl,hl при 2-байтном
индексе.  Кодоген проверен: индекс полный 16-битный (грабли
sdcc_z80_const_ptr_index_bug обойдены отдельной uint16_t-переменной),
~130 тактов номинала против ~1 000 у __divsint.

Банк 4: 7 336 -> 9 394 из 16 384 (свободно 6 990).  Таблица сверена
питоном обратно из .c (1 152 записи), правило генерации «усечение к нулю»
закреплено тестом geom_mul8div7_trunc_to_zero на целевом компиляторе.
tests-host: [geom] 1961 -> 1992, все 5 наборов прошли.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 20:35:01 +03:00
snark13 72797e1be8 Инвентаризация делений по .asm: убраны три, остальные разобраны
Обход всех сгенерированных .asm (awk по `call __div/__mod/__mul` с
привязкой к строке исходника) нашёл 31 вызов в 9 модулях.  Три из них
были в горячем пути:

- pop_cdraw.c calc_screen_x_coord: `x * 8 / 7` -> __divsint, 2 400 тактов
  на ПЕРСОНАЖА КАЖДЫЙ КАДР (два вызова при живом сопернике).  Заменено
  тождеством 8x/7 == x + x/7 плюс таблица DIV7[256] в банке 4 — резидент
  не тронут, обычный диапазон (obj_x 0..252 при x_bump 58..184) покрыт
  целиком, деление осталось только хвостом для шва (render_dx = ∓140).
- pop_guard.c guard_col_from_x: /14 и %14 звались БЕЗУСЛОВНО, мимо
  POP_TILE_DIV — единственное 16-битное деление без подключённой таблицы.
- pop_trob.c animate_chomper: `tp / 10` на чомпера каждый кадр, при том
  что TP_ROW/TP_COL лежали в этом же файле, но ниже по тексту.  Таблицы
  подняты выше чомперов.

Остальные 25 оставлены осознанно и расписаны в TASKS_OPEN.md: хвосты за
таблицей (x вне 0..255 = персонаж в соседней комнате), намеренный
медленный хвост pop_y_to_row, недостижимая ветка pop_rnd_fit и холодные
места (вход стража, старт уровня, читы, имя файла, отладочный HUD).

В банках 4, 6, 7 теперь ноль __div*.  Тождество 8x/7 закреплено тестом
geom_mul8div7_identity (перебор −420..700), таблица DIV7 сверена с x//7.
tests-host: [geom] 840 -> 1961, все 5 наборов прошли.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 20:23:43 +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 b0524b9ad0 Оптимизация: логический кадр уложился в бюджет, цикл 4 растровых кадра -> 3
Главный цикл спейсится тремя gfx_wait_vsync, поэтому работа сверх 430 000
тактов стоит сразу целый лишний растровый кадр.  Было 470 964, стало
~425 600 — игра быстрее на треть (16,7 логических кадров/с против 12,5).

- pop_y_to_row: цепочка сравнений вместо (y+60)/63%4-1.  ВАЖНО: медленный
  хвост вынесен в ОТДЕЛЬНУЮ функцию — SDCC видит одинаковое выражение в двух
  ветках и поднимает деление в вершину, быстрые возвраты не спасают.
- col_from_x (pop_bg) и get_tile_div_mod (pop_map) — общие резидентные
  таблицы POP_TILE_DIV/POP_TILE_MOD в pop_tile.c (const банка из чужого
  банка не читается).
- pop_fore_over_char: расширение окна считается арифметикой, а не перебором
  10 колонок и 3 рядов (условие монотонно -> границы).  Формулы сверены с
  прежним перебором перебором значений, расхождений нет.
- pop_cd_touch: цикл по страницам развёрнут, x+w/y+h считаются один раз.
  Зовётся с каждого блита фона, стоил 6 846 тактов.
- process_trobs: tp/10 и tp%10 у факелов — таблицей.
- Пустой слот соперника (стража на сцене нет, на странице ничего не
  нарисовано) считается «тихим»: ни heal, ни вход в pop_char_draw, ни
  fore-проход.

Приём для поиска делений: брейкпоинт на __divsint/__divuint/__divuchar с
печатью адреса возврата (printf "%04X", w@(sp)).

Профиль остатка — TASKS_OPEN.md#draw-cost.  tests-host: 5/5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 15:49:46 +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