Банк 2 (pop_bg + pop_gdraw) подошёл к границе страницы вплотную: 16 021 из
16 384, свободно 363 байта. А расти ему ещё есть куда — тайлы поздних
уровней, чомперы, зеркало, анимации смерти стража.
pop_gdraw.c уехал в банк 4 (n_banks 3 -> 4):
банк 2 16 021 -> 13 792 (84.2 %, свободно 2 592)
банк 4 2 236 (13.6 %, свободно 14 148)
Цена: pop_fore_over_char стал кроссбанковым, поэтому помечен __banked —
один трамплин (~654 такта) за кадр, других вызывающих у него нет.
pop_fore_set_clip уже был __banked, так что там ничего не изменилось.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Каркас предметов уже был (check_get_item/do_pickup/proc_get_object), пустой
оставалась только ветка зелий. Порт seg005 get_item + seg006
proc_get_object:
- pop_get_item_action теперь отдаёт 3 = «пить» и делает do_pickup с ТИПОМ
зелья, который лежит в старших битах модификатора тайла (modif >> 3);
- pop_ctrl на код 3 запускает seq_78_drink;
- эффекты: тип 1 (здоровье) — +1 HP через hitp_delta и красная вспышка,
причём как в оригинале только если HP не полные; тип 5 («злое») — −1 HP.
Типы 2/3/4/6 (жизнь, перо, переворот, открыть ворота) — свойства поздних
уровней, портируем вместе с ними.
Вспышка фона получила цвет: меч даёт ярко-жёлтую (было), зелье — красную
(flash_color оригинала; двух значений достаточно, других в игре нет).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Прошлая правка убрала только управляющий терминал (os.setsid), и mmd
переключился на stdin: lsof показал fd 0 = /dev/ttys002, процесс снова спал,
теперь уже после отметки «копирование файлов».
Каналов, откуда mtools может ждать ответ, два — /dev/tty и stdin — и
закрывать надо оба. Обёртка mt() теперь и создаёт новую сессию, и подаёт
stdin из /dev/null.
Проверено: с stdin=/dev/null mmd на свежем образе отрабатывает с кодом 0, а
на уже существующем каталоге честно возвращает 1 и ничего не спрашивает.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Симптом: сборка образа молча вставала навсегда сразу после эхо-строки
команды. Появилось не «само» — ровно тогда, когда образ разложили по
подкаталогам (BG/KID/GUARD/LEVELS) и в скрипте появился mmd.
Диагноз по артефакту, а не по догадке: зависший процесс — `mmd z:/BG`,
и lsof показал fd 0 = /dev/null, fd 4 = /dev/tty. То есть mtools (собран
с enable-raw-term) для интерактивного вопроса открывает УПРАВЛЯЮЩИЙ
ТЕРМИНАЛ напрямую, в обход stdin — поэтому ни `< /dev/null`, ни
перенаправления stdio не помогают. А `2>/dev/null` на mmd прятал сам
вопрос, из-за чего это выглядело как зависание на пустом месте.
Из НЕинтерактивного запуска (CI, фоновая задача) терминала нет, вопрос не
задаётся, и баг не воспроизводится — потому и жил незамеченным.
Лечение: все вызовы mtools идут через обёртку mt(), которая запускает их в
НОВОЙ СЕССИИ (os.setsid + exec питоном; setsid(1) в macOS нет). Без
управляющего терминала открывать /dev/tty нечего, и mtools выбирает
неинтерактивный путь.
Заодно добавлены отметки этапов («разметка и формат», «копирование
файлов», «конвертация RAW -> CHD»): если что-то встанет снова, будет сразу
видно где, а не после последнего аргумента команды.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Смерть стража теперь персистентна между входами в комнату — по механизму
оригинала, а не отдельной таблицей «убит/не убит». В SDLPoP массивы
level.guards_* лежат в ОЗУ и движок их ПЕРЕПИСЫВАЕТ: leave_guard (seg002:02F5)
кладёт туда позицию/направление/мастерство, а у МЁРТВОГО ещё и curr_seq;
enter_guard, увидев непустой seq_hi, поднимает стража прямо в этой
последовательности и по кадру смерти (185/177/178) ставит alive = 1.
У нас уровень лежит в EMM-странице только на чтение, поэтому в W2 добавлена
живая копия — 6 байт на комнату (tile/dir/x/skill/seq_lo/seq_hi):
- pop_guard_leave() в начале enter_room запоминает уходящего стража;
- pop_guard_enter поднимает труп сохранённой последовательностью И
сохранённой X (pos_guards пересчитывает её из колонки только при загрузке
уровня, дальше ею владеет leave_guard — иначе тело прыгает в центр тайла).
ГРАБЛИ: guards_seq_lo/hi в ФАЙЛЕ уровня не используются, там 0xFF во всех
комнатах (оригинал чистит их в reset_level_unused_fields). Прочитав их как
есть, я скормил интерпретатору curr_seq = 0xFFFF, и приложение зависало —
бордюр оставался синим, цикл не доходил до vsync. Живая копия стартует
нулями: 0 = «поднимать стандартной стойкой».
Меч Киду больше не выдаётся автоматически: DEBUG_SWORD_ROOM убран, вместо
него чит S (выдать меч). Штатный путь — подобрать с пола.
Проверено в MAME: чит K убивает стража, уход из комнаты 3 и возврат —
тело на месте, страж не воскресает.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Регрессия от окна Char вокруг control() (cf06896): диспетчер работает с
копией Char, а часть его действий у нас исполняет pop_map (pop_down_action,
pop_jump_up_seq, safe_step, зацеп) — и пишет ПРЯМО в Kid, потому что на Char
он ещё не переведён. Завершающее `Kid = Char` затирало эти правки:
выравнивание x и ряд терялись, и спуск с уступа через вис не срабатывал —
Кид просто приседал.
pop_savekid_state теперь копирует только то, что диспетчер реально меняет
у персонажа: curr_seq и sword. Когда pop_map переведём на Char, вернётся
полное копирование — в комментарии это зафиксировано.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Боёвка (порт seg002/seg006):
- check_sword_hurting / check_hurting / check_sword_hurt / hurt_by_sword /
take_hp через дельты; do_delta_hp сводит их раз в кадр;
- парирование (justblocked), refractimer после ранения стража;
- смерть по seq_71_dying — через неё же теперь работает чит K: страж
действительно погибает, а не замирает на месте;
- парные окна Char/Opp: loadkid_and_opp / savekid_and_opp /
saveshad_and_opp.
Индикаторы HP (порт draw_kid_hp / draw_guard_hp): Кид слева, страж справа.
Перерисовка ТОЛЬКО при изменении числа и тогда на ОБЕИХ страницах
дабл-буфера (счётчик hp_todo, иначе на второй странице осталось бы старое
значение и мерцало через кадр); pop_hp_invalidate при входе в комнату, где
фон перерисован целиком.
Чит I — бессмертие Кида (нашего изобретения, в оригинале его нет).
Перекрывает и путь «безоружного закалывают насмерть»: тот идёт мимо HP, и
без этого чит бесполезен ровно там, где нужен.
ДВА НАЙДЕННЫХ БАГА:
1. Полосу HP блитил row-major примитивом, а атласы Кида и стража хранятся
COLUMN-major (ради бесплатного флипа) — марки выходили транспонированными.
Теперь колоночный блит, стрелки как в оригинале.
2. Кэш кадра для ОТРИСОВКИ заполняли pop_savekid/pop_saveshad. Любое окно
Char БЕЗ play_seq — а это оба окна боёвки — записывало Киду кадр, который
принадлежал СТРАЖУ, и kid_draw искал этот image в атласе Кида, рисуя
произвольную позу. Владельцем кэша стал load_frame: он один знает, чей
кадр загружен (по Char.charid).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пункт 2 плана закрыт: страж не только замечает Кида, но и идёт к нему и
дерётся. Порт по SDLPoP, диспетчер общий — ИИ выставляет те же control_*,
что и клавиатура игрока.
guards.c (банк 1), порт seg002:
- autocontrol_guard_active (737) + kid_in_sight (0A93) + kid_armed (0AC1)
+ kid_far (09CB);
- guard_advance / guard_block / guard_strike с таблицами вероятностей по
12 градациям мастерства (seg002:26..38), бросок prob > prandom(255);
- move_2_backward / move_3_up / move_6_shift / move_down_back;
- таймеры justblocked / kid_sword_strike / guard_refrac убывают раз в кадр
в autocontrol_opponent, как в оригинале.
pop_ctrl.c, порт seg005: control_with_sword (964), swordfight (0CDB),
sword_strike, parry, forward_with_sword, back_with_sword. Ветвление у
Кида и у соперника разное — соперник блокирует только на кадре 152, Кид
ещё и по 153 (и тогда последовательность прокручивается сразу).
pop_guard.c: guard_skill из данных уровня (12 градаций, вне диапазона -> 3),
HP по get_guard_hp (extrastrength[skill] + tbl_guard_hp[уровень]),
собственный сид бросков pop_fight_seed — иначе перерисовка стены сбивала бы
решения стража.
char_opp_dist переехал из банка в pop_kid.c: он нужен по ОБЕ стороны
банковой границы — и ИИ, и диспетчеру боёвки.
Проверено в MAME (комната 3): страж проходит комнату, встаёт в дистанцию и
машет мечом, позы меняются. Урона пока нет — HP-учёт и check_hurt
следующим шагом, без них бой не заканчивается.
Бюджет В БОЮ (175 кадров): 412 224 – 421 068 = 0.958–0.979 кадра.
В покое было 400 800. Запас в худшем кадре ~9 000 — тесно, но в один
кадр укладываемся; оптимизация отложена сознательно.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Тексты обеих Z80-процедур, разбор их устройства и качества, почему НЕ берём
8-битный RND Apple II (вырожденные младшие биты — раскладка кладки читает
prandom(1), вышла бы шахматка), и главное — сколько это реально даст.
Потолок выигрыша 2 814 тактов за кадр (0.65 %): тело генератора уже не
основной расход, остаются обёртка pop_prandom, pop_rnd_fit и ABI вызова.
Поэтому первый шаг, если упрёмся, — слить приведение к диапазону в ту же
asm-процедуру (один call вместо трёх), и только потом менять генератор.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Возврат к БИТ-В-БИТ генератору оригинала по умолчанию (POP_PRANDOM_EXACT=1):
по нему проще отлаживать и сверять картинку с эталоном. Чтобы это не
стоило процента бюджета, сам шаг LCG переписан на Z80-ассемблере —
единственное место в порте, где это сделано, с явного разрешения.
Приём: 214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3 — схема Горнера по
РАЗРЕЖЕННОЙ записи константы. Вместо 12 сложений (по числу единиц в
0x343FD) — 17 удвоений, три сложения и одно вычитание; величина 3*s,
нужная в конце, попадается по дороге на втором шаге.
Проверка в ДВА этапа:
- схема на хосте: horner(s) == s*214013+2531011 на 3 000 000 сидов;
- сама asm-транскрипция на живой машине: breakpoint на pop_prandom,
11 последовательных состояний сида из MAME — каждый переход совпал с
s*214013+2531011 бит-в-бит.
Замер, комната 3, 175 кадров (медиана кадра / prandom->torch_draw):
C, бит-в-бит (16-бит половины) 403 632 / 10 933
C, xorshift16 + шаг Вейля 397 986 / 7 927
asm, бит-в-бит 400 800 / 9 331
То есть asm вернул половину разрыва (2 832 такта за кадр), сохранив
совместимость с эталоном. Ветка xorshift оставлена под
-DPOP_PRANDOM_EXACT=0 как запасной ход — брать её имеет смысл, только
если не хватит последних 2 800 тактов.
Итог оптимизационного круга: 416 154 -> 400 800 (0.968 -> 0.932 кадра).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ревью на 32-бит сделан ПО ASM, а не по коду (искали и безымянные
временные): во всём приложении был ровно ОДИН 32-битный вызов —
__mullong в pop_prandom. Замер в MAME: 8 430 тактов на вызов, два
вызова за кадр. Прочие библиотечные вызовы 16-битные (__divsint 16,
__modsint 12, __moduchar 8, __divuchar 5).
1. pop_prandom. Состояние 32-бит -> две 16-битные половины. Два
генератора, выбор через POP_PRANDOM_EXACT:
- 0 (по умолчанию) — xorshift16 + шаг Вейля, без единого умножения;
- 1 — LCG оригинала бит-в-бит, посчитанный половинами (для сверки
картинки с эталоном).
8-битный RND Apple II (5*x+23 mod 256) НЕ взят: у LCG по модулю 256
вырождены младшие биты (бит 0 просто чередуется), а раскладка кладки
берёт как раз prandom(1) и prandom(4) — вместо шума вышла бы
правильная шахматка. Шаг Вейля ещё и убирает ноль как неподвижную
точку xorshift (сид кладки вполне может быть нулём).
Бит-в-бит эквивалентность half-word версии проверена на хосте:
70 000 сидов x 8 шагов + 7 крайних сидов x 2000 шагов.
Остаток 0..maxv: делитель степень двойки — маска вместо __moduint.
2. libbgi: gfx_blit_cols_part_noclip — column-major блит без клипа
(пара к gfx_blit_cols_part, как gfx_blit_part_noclip к
gfx_blit_part). Клипающий вариант платит ~5 622 такта подготовки на
КАЖДЫЙ вызов независимо от того, вылезает край (замер: подготовка
5 622 против 13 596 на сам accel-проход). Kid, страж и клинок
выбирают путь по pop_onscreen_cols. size-check: роста нет.
Бюджет (175 кадров, комната 3, медиана):
было (после клинка) 416 154 0.968 кадра
стало 397 986 0.926 кадра
Разница между генераторами, замер на одинаковой сборке:
xorshift16 + Вейль 397 986 prandom->torch_draw 7 927
бит-в-бит LCG 403 632 prandom->torch_draw 10 933
то есть точность обходится в 5 646 тактов за кадр (1.3 %).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Меч в оригинале ОДИН на всех: и Кид, и страж рисуют клинок из chtab_0
(add_sword_to_objtable, seg006:1798). Раньше sword.atl содержал только
кадры подъёма/ножен (sword_tbl 35..42), поэтому у стража меча не было
видно вовсе.
- pop_pack_kid.py пакует chtab_0 целиком (id 0..33, 5168 Б), индекс в
атласе = id;
- pop_extract_kid_data.py вытаскивает sword_tbl (53 строки) в kid_data.h
макро-инициализаторами — таблица ложится в один TU, а не в каждый;
- pop_sword_draw (pop_kid.c) — общая точка отрисовки клинка с полным
условием оригинала (кадры 229..237 ИЛИ меч обнажён ИЛИ живой страж);
зовут и kid_draw, и pop_guard_draw.
Раздельный heal накладных спрайтов. Клинок и брызги урона раньше
объединялись в один прямоугольник с персонажем, а объединение почти вдвое
больше суммы двух (клинок уходит вперёд-вверх) — heal же стоит ровно по
площади. Теперь у накладных свой прямоугольник и свой gfx_heal, а
объединение осталось ТОЛЬКО для окна fore-клипа: там это 4 сравнения без
рисования, но покрыть клинок обязано, иначе он полезет поверх столба.
Отладка: DEBUG_SWORD_ROOM — Киду выдаётся меч при входе в комнату 3
(там страж), чтобы не бегать за ним в комнату 15.
Бюджет (225 кадров, комната 3): 415 284 – 416 370 = 0.966–0.968 кадра.
Против 384 168 – 384 636 до этого шага, то есть +31 700. Разложение по
фазам (медианы): process_trobs 83 190, pop_guard_draw 92 483 (спрайт
28 763 + клинок 23 820 + fore_over_char 39 906), kid_draw 59 089, fore
поверх Кида + борта 47 483, heal 40 068, логика 76 338, ввод 13 050.
Видно, что клинок 21x8 стоит почти как спрайт стража — это фиксированные
накладные расходы клипающего блита, а не пиксели; лечится noclip-путём
для column-major (см. gfx_blit_noclip_fast). Отдельным шагом.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Первый самостоятельный кусок ИИ стража (план, пункт 2). В оригинале у
соперника нет своего диспетчера: autocontrol_* (seg002) выставляет те же
глобалы control_*, что и ввод игрока, а дальше исполняется общий control().
Инфраструктура под это встала прошлым коммитом, здесь — сама логика.
Порт:
- check_can_guard_see_kid (seg003:688) — луч видимости по ряду: стены и
верхи дверей рвут его совсем, loose/чомпер/дыра/неподнятые ворота дают
«вижу, но не пойду». В guards.c (банк 1);
- Opp + loadshad_and_opp (seg006:841) и char_opp_dist (seg006:2135);
- autocontrol_guard_inactive (seg002:710) + move_* (seg002:0706..);
- ветки control(): control_guard_inactive (seg006:2123) и draw_sword
(seg005:945) — соперник уходит сразу в seq_90 en garde;
- pop_guard_tick перестроен по play_guard_frame (seg000:1246): окно
Char/Opp вокруг ИИ, диспетчера и play_seq.
По дороге:
- Kid.alive не выставлялся (= 0 = «мёртв» в семантике оригинала), из-за
чего луч видимости не мог сработать в принципе — ставим -1 в kid_init;
- Kid.room не выставлялась вовсе; условие Kid.room == Guard.room всегда
было ложным. Ставим в enter_room (полная модель Kid.room != drawn_room
у шва по-прежнему впереди);
- pop_tile_at — тайл текущей комнаты наружу из pop_map (луч видимости);
- control_x/y/shift открыты в шину: ИИ заполняет оси как есть, без
flip_control_x (его «вперёд» уже в системе персонажа).
Проверено в MAME (комната 3, страж на tile 17): страж переходит из
стойки 166 в 171 stand_with_sword, sword=2, can_guard_see_kid=2; ввод
игрока не пострадал. Клинок отдельным спрайтом пока не рисуется —
sword.atl содержит только кадры 229..237 (подъём меча Кидом), остальные
строки sword_tbl приедут с боёвкой.
Бюджет (225 кадров, комната 3): 384 168 – 384 636 тактов, 0.893–0.895
кадра, запас 45 364. Прошлый замер 382 584 – 383 064 — шаг стоил ~1 570.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Инфраструктура пункта 2. В оригинале у соперника НЕТ своего диспетчера:
autocontrol_* (seg002) выставляет те же глобалы control_*, что и ввод
игрока, а дальше исполняется тот же control() (seg005:252). Значит перед
портом ИИ надо было привести к этому обе половины:
- control_forward/backward/up/down/shift2 перестали быть static в
pop_ctrl.c — это общая шина синтетического ввода, объявлена в pop_ctrl.h
вместе с POP_CONTROL_*;
- control() переименован в pop_control() и работает с Char, а не с Kid;
- ввод игрока обёрнут в окно Char (loadkid/user_control/savekid), как в
play_frame оригинала.
Ловушка по дороге (ввод отвалился целиком, Kid не двигался): макрос
seqtbl_offset_char вёл на kid_set_seq, который пишет прямо в Kid, а
следом savekid затирал Kid копией Char со старой curr_seq. В оригинале
seqtbl_offset_char работает именно с Char — макрос переведён на
pop_char_set_seq. kid_set_seq остался для вызовов ВНЕ окна (pop_map).
Добавлен pop_savekid_state (Kid = Char без кадра): control() кадр не
трогает, а cur_frame в этот момент принадлежит тому, кто последним крутил
play_seq.
Проверено в MAME: бег и упор в стену работают как прежде.
Бюджет (комната 3, 125 кадров): 382 584..383 064 против 380 292..381 282,
то есть +2 300 тактов на копии окна. 0.891 кадра, запас 46 936.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пункт 1 плана стражей. play_seq был прибит к Киду, поэтому страж стоял на
захардкоженном кадре 166. Теперь как в оригинале: интерпретатор работает
с АКТИВНЫМ персонажем Char, а вокруг стоят loadkid/savekid и
loadshad/saveshad (порт seg006:809..825).
Почему копия, а не указатель: так в оригинале, и на Z80 это быстрее —
горячий цикл обращается к глобалу абсолютной адресацией, а 16-байтовое
копирование платится один раз на переключение персонажа, тогда как
указатель дал бы индексную адресацию в каждом обращении.
Сопутствующее:
- kid_t и pop_char_t слиты в один pop_char_t (pop_char.h): в оригинале
char_type один на всех, и без этого общий интерпретатор невозможен.
Kid получил поля room/charid/sword/alive — они и так нужны боёвке;
- load_frame выбирает таблицу кадров по Char.charid (у стража своя,
frame_tbl_guard с индексом frame + add_frame − 149, seg006:0293);
- cur_frame разведён на два кэша: страж тикает ПОСЛЕ Кида, и без этого
kid_draw брал бы кадр стража. savekid/saveshad раскладывают кадр по
своему персонажу;
- kid_set_seq пишет ИМЕННО Kid (его зовут pop_ctrl/pop_map вне окна Char,
иначе loadkid затёр бы), для окна Char добавлен pop_char_set_seq —
порт seqtbl_offset_char;
- в pop_map 9 голых play_seq() заменены на pop_kid_play() (load+play+save);
- страж входит в комнату через seq_77_guard_stand_inactive (seg002:0208),
а не через прибитый кадр.
Проверено в MAME: Kid бегает и упирается в стену как прежде, в комнате 12
плиты проваливаются со щебнем (правка задела 9 вызовов play_seq в
физике), страж в комнате 3 рисуется в той же позе, но теперь
curr_seq=0x19A9 и charid=2 — кадр получен прокруткой последовательности,
а не константой.
Бюджет (комната 3, 150 кадров): 380 292..381 282 тактов против
370 140..371 160 до правки, то есть +10 150 (+2.7 %). Основное — не
копии Char, а то, что страж теперь реально крутит интерпретатор каждый
кадр, а раньше стоял замороженным. 0.887 кадра, запас 48 718.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Мышь игре не нужна (управление — raw-клавиатура, которую мы и так
забираем у DSS), а её прерывания воруют такты из бюджета, занятого на
86 %. Эффект измерен побочно: при движении мыши на хосте кадры выбивались
до 1.5 кадрового периода, при неподвижной — 225 кадров без превышений.
Записано с тем, что проверить (есть ли в RST 30h выключение, сколько
стоит одно прерывание, восстановление состояния на выходе) и почему не
сейчас: выигрыш только когда игрок двигает мышью, риск оставить систему
без мыши после выхода — заметный.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Делитель (torch_tick & 1) занижал скорость пламени вдвое: один логический
кадр = 3 vsync и соответствует игровому тику оригинала, а animate_torch
(seg007:03C1) меняет кадр КАЖДЫЙ тик. Теперь темп задаётся явной
константой TORCH_ANIM_DIV (1 = как в оригинале, 2 = прежнее поведение),
счётчик компилируется только когда он реально нужен.
Побочный эффект важнее визуального: раньше половина кадров делала работу
факелов, половина нет, и бюджет кадра «прыгал». Замер по 100 кадрам
до правки: 349 008..371 262, разброс 22 254 такта (6.2 %). После: по
225 кадрам 370 140..371 160, разброс 1020 тактов (0.27 %) — каждый кадр
стал худшим случаем, и цифре можно верить.
Бюджет сейчас (комната 3, Kid + страж, статика): 0.861..0.863 кадра,
запас 58 840 тактов до 430 000. Кроссбанковых вызовов 19 за кадр
(~654 такта каждый = 12 400, 3.3 % кадра) — столько максимум вернёт
батчинг; профиль вызовов снят breakpoint'ом на ___sdcc_bcall_ehl с
печатью HL/E и раскладкой адресов по .map.
Замечание по методике: мерить надо ПО МНОЖЕСТВУ кадров. Единичные
всплески до 1.5 кадра, которые я сперва принял за проблему движка,
оказались наводкой от прерываний мыши на хосте — при неподвижной мыши
225 кадров подряд без единого превышения.
Проверено, что --w3 (он остался в sprinter-cc) кладёт в W3 только код и
rodata: --dataseg ему не передаётся, глобал --w3 модуля лёг в общий
_DATA — сюрпризов при возврате к резиденту не будет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Резидента --w3 больше нет: отрисовка (pop_bg + pop_gdraw) уехала в БАНК 2,
физика/коллизия (pop_map) — в БАНК 3. Куча W1/W2 1294 -> 6750 Б.
Что это разблокировало. Резидент был тупиком: из банка он недостижим ни
прямо, ни транзитивно, поэтому pop_map (самый крупный модуль, 5.8 КБ) в
банк было не увести — он зовёт mob-отрисовку. Проверено, что банк->банк
РАБОТАЕТ: ___sdcc_bcall_ehl читает страницу окна портом 0xE2 и кладёт её
на СТЕК своего кадра (runtime/bank.s), поэтому вложенность корректна по
построению. Подтверждено в MAME цепочкой W1 -> банк1 -> банк2 -> банк1:
nested=124 after=8, ровно ожидаемое. Значит развязка mob'а (самое
рисковое место, loose-полы) НЕ понадобилась — pop_map зовёт pop_bg
трамплином.
Правила вызовов проверены на сгенерированном asm и записаны в memory
sdcc_banked_call_rules: трамплин выбирает ОБЪЯВЛЕНИЕ (__banked), а не
раскладка — даже внутри одного .c между __banked функциями он есть.
Внутрибанковые функции оставлены непомеченными и зовутся напрямую, в т.ч.
через границу файла (pop_gdraw -> pop_fore_over_char).
sprinter-cc: --dataseg BANKn БОЛЬШЕ НЕ ставится по умолчанию. Раньше вся
писучая память банкового модуля уезжала в страницу банка и снаружи не
читалась (проверено на .map: глобал лёг по 0x0001C000) — грабли на
каждом переносе. Теперь данные банков по умолчанию в общем _DATA (W1/W2,
замаплен всегда), а прежнее поведение — по явному --bank-data.
Замеры (комната 3, Kid + страж; кадр Sprinter в турбо = 430 000 тактов):
до переноса 338 508 (0.79 кадра)
+ pop_bg в банк 2 347 100 (+2.5 %)
+ pop_map в банк 3 371 100 (+9.6 % к исходному, 0.86 кадра)
Плата — трамплины (~654 такта на вызов, ~30 вызовов за кадр). При
пейсинге в 3 кадра это 29 % логического кадра, но запас до ОДНОГО кадра
всего ~59 000 тактов — под звук его надо возвращать (следующий шаг:
батчить кроссбанковые вызовы, начиная с pop_redraw_needed).
Профилирование бордюром включено по умолчанию (make PROF=0 выключает) и
переведено на реально работающие биты: бит 0 (красный) у бордюра Sprinter
ИГНОРИРУЕТСЯ, поэтому различимых состояний четыре и значения обязаны быть
чётными — 0 чёрный (ждём vsync), 2 синий (логика), 4 зелёный (фон),
6 циан (спрайты). Раньше нечётные номера сливались и полосы не читались.
Проверено в MAME: комнаты 1/3 рисуются как прежде, бег и коллизия
работают, в комнате 12 плиты проваливаются со щебнем — то есть цепочка
loose банк3 -> банк2 живая. Полосы бордюра в комнате 3: логика 73
строки, фон 64, спрайты 128, свободно 23.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Разгрузка W1/W2 под будущий ИИ стражей: _kbdraw_down был БАЙТОМ на
скан-код (512 Б в _DATA при 32-килобайтной раскладке). Теперь бит на
код: код>>3 = байт, код&7 = бит, расширенные (префикс 0xE0) — смещение
+32 байта вместо +256.
Трамплин прерывания строит маску СДВИГОМ, а не таблицей: таблица
потребовала бы `ld hl,#метка` внутри трамплина, а он копируется в W2
побайтно и обязан быть без абсолютных само-ссылок (см. его шапку).
Маска строится в BC, поэтому в клавиатурной ветке добавлен push/pop bc.
Трамплин вырос 244 -> 267 Б, буфер копии поднят 320 -> 336 (запас 69 Б).
Проверено в MAME на roomtest, все три класса клавиш:
- обычные: '=' (обход комнат) и 'K' (чит-убийство стража — читал
guardhp_curr/delta: 3/0 -> 0/-3);
- расширенные (E0): стрелка вправо — Kid добежал до края комнаты;
- модификаторы: удержание Shift ставит бит 2 байта 2 карты
(скан-код 0x12), отпускание снимает.
Скорость: кадр 334 716 -> 338 508 тактов (+1.1 %) на битовой арифметике
в kbd_raw_down (~15 вызовов за кадр); при бюджете 430 000 это 0.79
периода вместо 0.78 — регрессии нет.
Итог по roomtest: данные 4422 -> 4022 Б, куча W2 996 -> 1294 Б.
Эталон размеров принят заново (make size-baseline): _CODE десяти
программ вырос на 14-23 Б — это код битовой арифметики в трамплине и
kbd_raw_down, обмен на -448 Б данных, которые size_check не считает.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Жалоба: стойка неподвижных Кида и стража занимала больше полутора
кадровых периодов. Гипотеза «виноват __banked» ЗАМЕРОМ НЕ
ПОДТВЕРДИЛАСЬ: весь банковый вызов (трамплин + смена страницы + тело
pop_guard_tick + возврат) стоит 654 такта при бюджете кадра 430 000.
Как мерил (выборка PC бесполезна — мост MAME отвечает из фреймового
колбэка, все сэмплы падают в обработчик прерывания): breakpoint'ы MAME с
действием {printf totalcycles; g} на входах фаз главного цикла, разности
соседних меток = стоимость фазы. Плюс профилирование полосами бордюра
(make PROF=1, макрос PROF() в roomtest.c) для быстрого взгляда.
Замер комнаты 3 (Kid + страж), такты, кадр = 430 000:
fore поверх стража 431 964
fore поверх Kid 402 816 -> 78 % всей работы кадра
остальное 241 956
ИТОГО 1 076 736 = 2.5 кадра
Две причины, обе устранены:
1. Fore-слой рисовал ЦЕЛЫЕ тайлы, хотя существует ровно для того, чтобы
вернуть куски поверх спрайта — за его прямоугольником в видеопамяти и
так правильный фон. Введено ОКНО клипа (pop_fore_set_clip): спрайт
сообщает свой итоговый габарит (у Kid — с клинком, брызгами и
обрезкой clip_char), fore-проход режет по нему. Отсев трёхступенчатый:
тайл целиком (tile_in_fclip, до обращения к атласу), кусок по грубому
габариту (до gfx_w0_map — w/h лежат в EMM-странице), и точный клип в
blit_b. Для последнего добавлен libbgi-примитив
gfx_blit_part_noclip — пара к gfx_blit_noclip, но под-прямоугольник.
2. Оставшиеся 341 К после клипа оказались НЕ пикселями: 18 вызовов
pop_prandom за проход, ~10 700 тактов каждый (32-битный LCG:
__mullong ~8 000 + __moduint). Раскладка кладки тайла — чистая
функция (комната, ряд, колонка), то есть константа комнаты, а
wall_pattern пересчитывал её каждый кадр. Теперь кэшируются готовые
РЕШЕНИЯ (что рисовать и с каким смещением), 3 байта на тайл, сброс в
pop_room_draw. Порядок вызовов prandom воспроизведён один в один,
включая то, что значение метки берётся только при сработавшем условии.
Итог того же замера: fore поверх стража 37 464, поверх Kid 46 464,
кадр целиком 334 716 = 0.78 периода (было 2.5). Ускорение 3.2x, сами
fore-проходы — 10x.
Проверка отсутствия регрессии: попиксельная разность скриншотов комнат
1/2/3 до и после — отличаются ТОЛЬКО языки пламени факелов (анимация),
кладка и метки совпадают байт в байт.
Побочно: sprinter-cc научился пробрасывать -DNAME в sdcc.
Память: куча W2 1245 -> 996 Б (кэш кладки 120 Б), резидент W3 12 819 ->
14 512 (свободно 1872 Б — становится тесно), банк 1 236/16384.
ВНИМАНИЕ: make size-check показывает рост 7 программ, но эталон
docs/size_baseline.tsv отстал (последний раз принят в 484b18d, libbgi
менялась в 95c22be/c127a4b/64ce633) — к этой правке рост отношения не
имеет: новый модуль библиотеки в чужие программы не линкуется.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Разгрузка W1/W2 перед ИИ стражей (вариант 2 из двух обсуждённых).
1. pop_guard.c разделён по окнам: состояние/логика (Guard, HP, enter,
kill, load_frame) остаются в W1/W2 — их обязан видеть банк guards.c;
ОТРИСОВКА уехала в новый pop_gdraw.c, собираемый как --w3 (резидент).
Правило границы: резидент = только то, что рисует и зовётся
исключительно из главного цикла. Кадр стража стал глобальным
(pop_gframe): заполняет логика, читает резидент.
2. Страж не окклюдировался передними гранями тайлов — рисовался поверх
столба. В оригинале любой Char это запись midtable, а foretable
рисуется после всех midtable (draw_tile_fore, seg008:690), т.е. столб
перекрывает всех одинаково. Футпринт персонажа выделен из
pop_fore_over_kid в char_footprint(), поверх него добавлен
pop_fore_over_char() — слой fore + полоса потолка, без оверлеев поз
виса/полёта/подъёма (у стража их нет; появятся — портируем
redraw_at_char2 общим кодом, а не догадками).
3. Упаковщик стража: тот же off-by-one, что уже ловили у Kid.
load_chtab_from_file(id_chtab_5_guard, 750) даёт images[0] = res751,
а рисование индексирует images[frame.image] — значит image=N это
res(751+N), а не res(750+N). Из-за сдвига frame_166_stand_inactive
рисовался как res767 (выпад) вместо res768 (стойка).
Проверено в MAME: страж в комнатах 3 и 21 стоит в правильной позе;
окклюзия подтверждена патчем Guard.x в живой сессии — при заходе за
столб спрайт корректно срезается его передней гранью.
Память: _CODE 26 149 -> 25 703, куча W2 805 -> 1245 Б, резидент W3
11 656 -> 12 819 (свободно 3565 Б), банк 1 236/16384 Б.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Шаги 1-2 из плана стражей:
1. Спрайты (chtab_5_guard, база 750). Новый упаковщик pop_pack_guard.py:
data/GUARD/res751..784 -> GUARD\g0..g4.atl (5 EMM-страниц, адресация
id>>3 / id&7, как у Kid). Палитра берётся НЕ из PNG, а из res10.bin
(guard_palettes: 7 палитр по 16 цветов, 6-бит) по level.guards_color —
на уровне 1 у обоих стражей color = 2; группа слотов 0x90..0x9F
добавлена в общий kid.pal.
2. Таблица кадров стража у оригинала СВОЯ (frame_tbl_guard, seg006:372,
41 запись) и индексируется как frame + add_frame - 149, где add_frame
= 70 для кадров 102..106. Она дописана в kid_data.bin (3515 -> 3720 Б,
смещение в KID_BIN_GFRAMES_OFF); pop_kid получил pop_kid_data_frame()
— чтение кадра из ЛЮБОЙ таблицы страницы данных.
3. Появление: pop_level_guard() читает guards_tile/dir/color/skill из
уровня, pop_guard_enter() ставит стража по enter_guard (seg002:0112) +
pos_guards (seg003): row из тайла, y = y_land[row+1], x из колонки,
charid = guard, sword сложен, alive = -1, HP = 3. Отрисовка
pop_guard_draw() — та же математика, что kid_draw (load_frame_to_obj +
calc_screen_x_coord), но атлас стража и своя таблица кадров; heal по
странице дабл-буфера, как у Kid.
Интерпретатора последовательностей у стража ПОКА НЕТ: кадр ставится
напрямую (166 = frame_166_stand_inactive, что и даёт seq_77 при входе в
комнату). play_seq для произвольного персонажа + ИИ — следующая фаза.
Проверено в MAME: в комнате 3 страж появляется на своём месте (ряд 1,
кол 7) и рисуется; цвета совпадают с эталонным рендером спрайта в
палитре color=2.
ВНИМАНИЕ по памяти: куча W2 просела до 805 Б (было 2349). Перед ИИ
стражей нужен шаг 5 плана (данные: room_modif 720 Б, dl1/dl2 512 Б,
_kbdraw_down 512 Б) либо вынос кода отрисовки стража в резидент W3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Все .atl лежали в корне диска рядом с exe — с атласами стражей корень
зарос бы окончательно. Теперь ресурсы разложены по каталогам (8.3, как
принято в DSS):
BG\ фон (env0..4, wall, fore, pot)
KID\ персонаж (kid0..27, kid.pal, sword, kid_data.bin)
GUARD\ стражи (появятся здесь)
LEVELS\ уровни (res2001.bin)
make_hdd.sh принимает аргумент вида КАТАЛОГ:файл — создаёт каталог на
образе и кладёт файл туда; без префикса файл идёт в корень. В Makefile
roomtest появилась цель `make hdd`, которая собирает образ с этой
раскладкой (раньше команда набиралась руками на 15 строк).
Проверено в MAME: DSS открывает пути вида KID\kid0.atl — комната
рисуется, Kid и факелы на месте, то есть все атласы, палитра, таблицы
анимации и уровень грузятся из подкаталогов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Раскладка (по подтверждённой пробником модели, tests/w3bankgfx):
- roomtest переведён на MEMORY=huge: та же small-раскладка резидента
(CODE в W1, DATA за ним) плюс банки кода в W3;
- guards.c собирается как --bank 1=guards.c — там будет ИИ и боёвка;
- СОСТОЯНИЕ стража живёт в W1/W2 (pop_guard.c): писучие статики
__banked-модуля линкуются в страницу банка и снаружи не читаются, так
что банк — только код;
- поля pop_char_t повторяют char_type оригинала (types.h:302), чтобы порт
seg005/seg006 ложился один в один.
Режим читов (порт cheats_enabled, seg000:111): глобальный флаг pop_cheats,
на время разработки включается в main. Реализован один чит — K (kill
guard, seg000:786): скелета не берёт, живому стражу ставит
guardhp_delta = -guardhp_curr и alive = 0. Обработка по фронту нажатия.
Остальные читы оригинала не портированы.
Проверено в MAME: приложение в huge-раскладке стартует, комната рисуется,
Kid бегает — то есть банкованный pop_guard_tick() зовётся каждый кадр
через трамплин и корректно возвращается; K не роняет приложение (стража
в комнате пока нет). Банк занят на 6 Б из 16384.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверяет то, на чём стоит план раскладки PoP (layout_plan_v2.md §2):
R3 резидент/HOME -> __banked через трамплин работает;
R4 примитивы libbgi можно звать ИЗ БАНКА: _bgi_begin запоминает текущую
страницу W3 (порт 0xE2), _bgi_end её возвращает — банк переживает
рисование и продолжает исполняться;
то же верно для функции W1/W2, вызванной из банка: она рисует, а в W3
остаётся страница БАНКА, не резидента.
Результат в MAME: все пять полос на месте, вердикт ЗЕЛЁНЫЙ. Замеры:
страница банка 0xF0 до рисования, после прямого блита и после возврата из
W1/W2-функции — та же 0xF0; резидент 0xF3; банк дожил до конца и вернул
корректное значение.
Два побочных вывода, важных для стражей:
1. Писучие статики __banked-модуля линкуются В СТРАНИЦУ БАНКА (адрес
0x1C000+), снаружи их не прочитать — состояние банка держать в W1/W2.
2. Инлайновый `in a,(0xE2)` посреди тела функции затирает A, куда SDCC уже
положил параметр (в первой версии пробника цвет заливки становился
номером страницы, и «резидент не рисовал»). Читать порт отдельной
__naked-функцией.
Имя exe — 8.3 (w3bgfx.exe): DSS длинных имён не понимает.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Сведены дубли, разъехавшиеся по модулям:
- x_bump[20] был в pop_kid (uint8_t!) и в pop_map (int16_t) — теперь одна
таблица int16_t;
- y_land[5] — две копии;
- y_to_row_mod4 — в pop_bg и pop_map;
- 32-битный LCG оригинала (prandom) — в pop_bg и pop_trob; функция теперь
одна, а СИДЫ остались раздельными (у раскладки кладки и у фаз факелов
свои последовательности, смешивать нельзя — иначе поедет рисунок стен).
Экономия по коду скромная (_CODE 24462 -> 24421, W3 11643 -> 11632: часть
выигрыша съели межмодульные вызовы). Главное здесь другое: pop_geom лежит
в W1/W2 и не трогает графику, то есть это тот самый «чистый» слой, который
сможет звать __banked-код стражей (docs/layout_plan_v2.md §4, §5.2).
Проверено в MAME: комната 1 после пересборки отрисована ПОБАЙТОВО так же,
как до правки (0 различающихся пикселей в области комнаты) — значит
последовательности PRNG и геометрия не поехали; Kid бегает.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Loose-полы, плита-потолок и щебень от приземления больше не зовут pop_bg —
ставят пометки (pop_set_redraw / pop_set_redraw_above), которые разбирает
pop_redraw_needed из главного цикла. Удалены самодельные счётчики
loose_bake/loose_rest/ceil_rest/ceil_bake/land_bake: их роль (вторая
страница дабл-буфера) теперь у счётчика страниц в пометке.
Осталось ОДНО исключение: падающий кусок (mob) — spawn/tick/pos. Это
движущийся ОБЪЕКТ, а не перерисовка тайла, и в оригинале он живёт отдельно
(mobs + draw_moving), поэтому разделение его на логику и отрисовку —
следующая фаза. Из-за него pop_loose_tick остаётся единственной функцией
pop_map, которую нельзя звать из __banked-кода; вся коллизия, физика,
кромки и предметы — то, что понадобится стражам — чисты от графики.
Замер: _CODE 24718 -> 24462, _DATA 4301 -> 4219 (ушли rest-массивы).
Проверено в MAME: комната 12, осторожный шаг на плиту — тряска, падение
плиты, Kid проваливается на ряд 1, дыра и щебень отрисованы; на
замороженном кадре обе страницы дабл-буфера побайтово совпадают в области
изменений (7 строк).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт архитектуры оригинала: логика анимации тайлов НИЧЕГО не рисует, она
ставит флаг (set_redraw_full/set_wipe/redraw_20h/redraw_21h, seg007), а
отрисовка идёт отдельным проходом redraw_needed (seg008:0178). У нас
появился pop_redraw.c/.h: pop_set_redraw(tilepos, вид, страницы) +
pop_set_redraw_above(col, ...) + pop_redraw_needed(), который зовёт
главный цикл в слое фона (до kid_draw).
Отличие от оригинала (наша платформа): счётчик пометки — это ЧИСЛО СТРАНИЦ
дабл-буфера (обычно 2), а вид перерисовки хранится явно (heal+поверх или
запечь фон), потому что у нас у каждой страницы своя ОЗУ-копия фона. В
оригинале вид кодируется тем, в какой из таблиц redraw_frames_* стоит флаг.
pop_trob переведён на пометки: пики, кнопки, дверь уровня. Его самодельные
массивы spike_rest/button_rest/ldoor_rest/rest_pending удалены — их роль
теперь у счётчика страниц в pop_redraw. Прямыми вызовами pop_bg осталось
только пламя факела и пузырёк зелья: это не тайловая перерисовка, а
покадровый оверлей; из-за них pop_process_trobs остаётся единственной
функцией модуля, которую нельзя звать из банка.
Зачем: из __banked-кода резидентная страница W3 недостижима транзитивно
(docs/layout_plan_v2.md §2 R2), поэтому логика, которую будут звать стражи,
не должна вызывать pop_bg.
Проверено в MAME: комната 6 — Kid на кнопке-открывалке, решётка в шве
поднимается; сошёл с кнопки — закрывается; упал в шахту на пики — пики
выдвинулись и отрисованы (кадр 177).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Кадры Kid читались из EMM-страницы по НЕВЕРНОМУ адресу для индексов >= 52.
Запись
(const uint8_t *)(0x100) + (uint16_t)i * 5u
SDCC 4.5 собрал так: умножение честно в 16 битах (add hl,hl / add hl,bc),
а затем `ld c,l` + `inc b` — то есть взял только МЛАДШИЙ байт результата и
подставил старший байт константы. При i*5 >= 256 адрес уезжал на -256*k,
и cur_frame наполнялся чужой строкой таблицы: у кадров бега/шага/подъёма
пропадал бит FRAME_NEEDS_FLOOR — Kid «вкручивался» в пол и проваливался
вниз, последовательности кадров не соответствовали seqtbl.
Фикс: адрес считается в uint16_t (i*5 = i + i<<2, без умножения) и
кастуется один раз — сгенерированный код теперь сохраняет старший байт
(ex de,hl / inc d).
Коварство бага: тот же паттерн в pop_level.c (room_fg_ptr/room_bg_ptr,
links) компилируется ПРАВИЛЬНО — проверил все три места по .asm. Записано
в memory sdcc_z80_const_ptr_index_bug.
Проверено в MAME на замороженных кадрах: 10 выборок (кадры 4,10,15,49,50,
54,123) — cur_frame совпадает с таблицей во всех, включая те, что раньше
были испорчены.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
--w3 берёт ОДИН файл на флаг, поэтому запись "--w3 pop_trob.c pop_map.c"
означала "W3 = pop_trob", а pop_map всё это время ехал в W1/W2 (в
build-каталоге лежал осиротевший w3_pop_map.rel). Теперь список явный.
pop_trob переведён в W1/W2: в W3 должно оставаться только то, что банк
никогда не позовёт (из __banked резидентная страница W3 не видна ни
напрямую, ни транзитивно — docs/layout_plan_v2.md §2 R2). pop_trob же
стражам понадобится: в оригинале они тоже давят кнопки.
Замер: W3 14376 -> 11643 (свободно 2008 -> 4741 Б), W1/W2 _CODE
21634 -> 24367 (куча 5315 -> 2582 Б). Освободившееся место в W3 —
задел под шаг 3 (loose/потолок из pop_map, чтобы pop_map стал
bank-safe).
Проверено в MAME скриптом: комната рисуется, Kid бежит и тормозит
(кадры 15 -> 10 -> 15), факелы анимируются (152 различающихся пикселя
между соседними кадрами) — то есть pop_trob работает из нового окна.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kid_frames (241*5) и kid_seqtbl (2310) занимали 3.5 КБ в _CODE окна W1/W2
— самого дефицитного ресурса. Теперь они лежат в kid_data.bin (отдельная
EMM-страница), которая маппится в W0 ровно на время play_seq — один
map/unmap за тик, в фазе тика, без конфликта с атласом в W0.
Ключ к переносу — порт load_frame/cur_frame (seg006): оригинал раз за тик
копирует кадр в структуру, и вся коллизия/отрисовка читает ЕЁ, а не
таблицу. У нас так же: kid_cur_dx/kid_cur_flags (их дёргают несколько раз
за кадр из pop_map) и kid_draw читают cur_frame — 5 байт в _DATA.
kid_seq_off (230 Б) оставлен резидентным: его читает kid_set_seq из
pop_ctrl/pop_map, вне страницы.
pop_extract_kid_data.py теперь пишет и kid_data.bin, и урезанный
kid_data.h (тип kframe, размеры, смещения в бинаре, kid_seq_off).
Замер: _CODE 24881 -> 21634 (-3247 Б), куча W2 2076 -> 5315 Б, W3 без
изменений. Проверено в MAME скриптом: стойка -> бег (кадр 8) -> стоп,
перемещение и коллизия у кромки работают, спрайт рисуется.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Новый документ applications/PoP/docs/layout_plan_v2.md по свежему замеру
(коммит 1214785): точные размеры окон/модулей/функций/данных, уточнённая
модель банкинга и пошаговый план.
Главное уточнение против v1: из __banked-кода резидент W3 недостижим — и
транзитивно тоже (bank -> pop_map -> pop_bg сломается). Отсюда целевая
раскладка: W3-резидент = графика, которую зовёт только главный цикл;
W1/W2 = ядро, достижимое отовсюду (включая банки); банки = новая холодная
логика (стражи/боёвка). Проверено по libbgi: скобка _bgi_begin/_bgi_end
сохраняет и возвращает ТЕКУЩУЮ страницу W3, поэтому примитивы libbgi
можно звать и из банка; нельзя лишь открывать скобку из кода, лежащего
в W3.
Крупнейшие цели: kid_data.h (3745 Б таблиц в _CODE) -> EMM-страница с
портом load_frame/cur_frame; вынос loose/потолка из pop_map в W3 (делает
pop_map bank-safe); дедуп геометрии в pop_geom.c; разгрузка _DATA.
Попутная находка: --w3 принимает ОДИН файл на флаг, поэтому в Makefile
"--w3 pop_trob.c pop_map.c" кладёт в W3 только pop_trob, а pop_map едет
в W1/W2 (в build-каталоге остался устаревший w3_pop_map.rel).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs/extra — ~570 МБ архивов чужих исходников (525 МБ из них — четыре
почти одинаковых zip'а bad_apple); docs/sources — клоны чужих
репозиториев со своими .git внутри, которые при обычном add стали бы
битыми gitlink-ссылками (без .gitmodules клон их не подтянет).
Материалы остаются на диске, но в историю не попадают: раздувание репо
необратимо без перезаписи истории.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- docs/new/ — сводные справочники (архитектура, BIOS, DSS, память,
графика, акселератор, IRQ, порты, ввод, звук, известные баги);
- docs/Original/ — первоисточники, из которых они собраны (BIOS, Estex
DSS, мануалы, описание акселератора), + Форум.doc/.docx в reference;
- libbgi/common/gfx_scroll_h.c — обход бага скролла при ширине >256
(правка автора: шаг банды 255 и продвижение указателей на cw; старый
вариант с 256 оставлен закомментированным с TODO);
- удалён applications/PoP/roomtest/hang_variants.png — рабочая раскладка
из разбора позы виса, в репозитории ей не место.
Большие архивы (docs/extra ~568 МБ, docs/sources с вложенными git-репо
~68 МБ) в коммит НЕ включены — см. обсуждение.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Два дефекта открытой двери:
1. Слева от лестницы оставалась поднявшаяся решётка. Оригинал рисует ВСЕ
куски двери blitters_0_no_transp, а наш упаковщик по умолчанию гонит
пиксель 0 в 0xFF (ключ прозрачности) — марш лестницы 144 переставал
закрашивать створку под собой. Добавил LEVELDOOR_ENV_IDS в
NO_TRANSP_ENV_IDS (тот же приём, что для 43/73/74/96/149).
2. Дверь дрожала через кадр: створка анимируется в back-страницу, и
ПОСЛЕДНИЙ кадр анимации ложился только на одну из двух страниц, вторая
застревала на шаг раньше. Добавлен отложенный редрой (ldoor_rest) —
повтор финального кадра на второй странице, как spike_rest/button_rest.
Проверено в MAME с заморозкой кадра: обе страницы в области двери
побайтово одинаковы, слева от лестницы чистый чёрный фон.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Комната 9: портал на уровень 2 рисовался чёрным проёмом — не был
портирован draw_leveldoor (seg008:1D29). Дверь рисуется при обработке
ПРАВОЙ половины (tile_left = 0x10), все куски со сдвигом +8 px:
99 низ лестницы (всегда),
144 марш лестницы за створкой (когда створка тронулась),
33 слайс створки — повторяется вниз с шагом 4 px до y = ybottom-modif
(modif 0 = закрыто на всю высоту, 43 = открыто, остаётся кромка),
34 верх коробки.
Анимация: animate_leveldoor (seg007:05F1) — type 0..2 открытие (modif++
до 43), type>=3 быстрое закрытие со скоростями {0,5,17,99}. Кнопка
заводит trob через trigger_1 (seg007:0999): дверь открывается ОДИН раз,
при modif != 0 кнопка уже ничего не делает.
Перерисовка створки — pop_leveldoor_redraw: draw_tile правой половины С
ЗАПЕЧКОЙ в ОЗУ-копию (банк TRANSPARENT), иначе kid_heal возвращал бы из
фона закрытую створку. Куски двери непрозрачные, wipe не нужен.
Спрайты 33/34/99/144 не попадали в атлас: сбор идёт прогоном
render_room.py, а он draw_leveldoor не реализует — добавлены явным
набором LEVELDOOR_ENV_IDS (как STUCK_ENV_IDS для нажатой кнопки).
Проверено в MAME (комната 9): закрытая дверь = решётка как в оригинале;
после нажатия кнопки (0,0) створка едет вверх, открывая лестницу.
Вход в дверь (кадры 217..228 + переход на уровень 2) НЕ делался.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. Жёлтая вспышка подъёма меча была ОДНОЙ длинной заливкой. В оригинале
(seg003:0AFC flash_if_hurt / remove_flash_if_hurt) цвет ставится и
СНИМАЕТСЯ в том же кадре — пока идёт flash_time, видно быстрый строб
«жёлтый/чёрный». У нас теперь так же: цвет ставится в кадре и
снимается после ПЕРВОГО gfx_wait_vsync из трёх (≈20 мс жёлтого,
40 мс чёрного — близко к оригинальным 2 тикам таймера).
2. Красной вспышки при потере HP не было вовсе. Порт второй ветки
flash_if_hurt: если flash_time не активен, но в этом кадре hitp_delta<0
— do_flash(color_12_brightred) ровно на кадр. У нас триггер —
pop_kid_hurt (он же рисует «брызги»).
3. Чтобы вспышке было от чего срабатывать, портирован урон падения из
land() (seg005), которого у нас не было: <22 — мягко, <33 — −1 HP и
seq_20 (2 этажа), иначе take_hp(100) + seq_22_crushed (насмерть).
ВНИМАНИЕ: это меняет геймплей — падение с 2 этажей теперь отнимает HP,
с 3+ убивает (как в оригинале).
Проверено в MAME: pop_flash_time тикает 5→3→1→0 (строб), падение с ряда 0
на ряд 2 даёт hitp_curr 3→2 и кадр 109 (medium land).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три расхождения с оригиналом на сцене подъёма меча (комната 15):
1. Лежащий меч не блестел. Порт animate_sword (seg007:0425) +
start_anim_sword (seg007:087C): при входе в комнату тайлу даётся
случайная фаза (prandom & 0x1F), каждый кадр счётчик вниз, на 0 —
новый период 0x28..0x67. Кадр блеска рисуется РОВНО на modif==1
((modif==1)+10 в draw_tile), т.е. одиночная вспышка раз в 40..103
тика. Перерисовка тайла — по смене видимого кадра, схемой кнопки
(текущая страница сразу, вторая через rest-цикл).
2. В кадрах «нашёл меч» клинка не было видно. Порт
add_sword_to_objtable (seg006:1798): клинок — ОТДЕЛЬНЫЙ спрайт
chtab_0 поверх Kid со смещением из sword_tbl. Смещения в ЭКРАННЫХ
пикселях (оригинал применяет их после calc_screen_x_coord), поэтому
берём уже масштабированный obj_x. Из таблицы взяты только строки
35..42 (кадры 229..236) — бой не портирован. Новый атлас sword.atl
(8 спрайтов, 1.6 КБ) + палитра chtab_0 в слотах 0x80..0x8F.
Прямоугольник heal расширяется объединением с клинком, иначе он
оставлял след за габаритом Kid.
3. Не было вспышки фона. do_flash = set_bg_attr(0, color) — оригинал
подменяет НУЛЕВУЮ запись палитры, вспыхивает всё чёрное поле экрана;
proc_get_object ставит flash_color=14, flash_time=8. У нас gfx_pal_set
на обе страницы. ВАЖНО: вызов gfx_pal_set(0,0,0,0,0) пятью литералами
ломает SDCC 4.5 (эмитит невалидный `ld hl, a`) — обёрнуто в функцию с
параметрами.
Попутно: TROBS_MAX 24 -> 30 (как в оригинале) + при входе в комнату из
списка выбрасываются «декоративные» trob ДРУГИХ комнат (факелы/зелья/
меч анимируются только в отрисованной). Без этого отладочный обход всех
24 комнат забивал список, и новые анимации молча не заводились.
Проверено в MAME: блеск (брейк на редрое тайла срабатывает), клинок в
руке виден, фон вспыхивает жёлтым.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Комната 15, тайл (2,2): меч не рисовался вовсе — в draw_tile не было
ветки tiles_22_sword (seg008 draw_tile_anim):
add_midtable(chtab_1, (modifier == 1) + 10, draw_xh, 0, draw_main_y - 3, ...)
Спрайты уже лежали в нашем pot-атласе (chtab_1 пакуется целиком, id 1..23),
так что понадобился только вызов. У нас предмет рисуется статикой в фоне,
а не в midtable: пока меч лежит, он не анимируется, а Kid и так поверх фона.
Подъём — порт цепочки оригинала:
- check_get_item/get_item (seg005:061F/073E) -> pop_get_item_action() в
pop_map (тайлы): стоя НА предмете отступить на тайл назад, подровняться
по кромке и присесть; из приседа над мечом — поднять;
- do_pickup (seg006:1671): тайл -> пол, pop_item_taken = tilepos+1;
roomtest запекает пол на ОБЕИХ страницах (pop_floor_bake) и пишет
per-room override, чтобы меч не воскресал при возврате в комнату;
- триггер — Shift (control_shift2) в стойке и в приседе, как в
control_standing/control_crouched;
- seq_91 pickupsword: опкод SEQ_GET_ITEM с аргументом 1 в play_seq теперь
зовёт pop_proc_get_object (seg006:16CB) -> pop_have_sword = 1.
Статус: pop_have_sword. Боевой режим (стойка с мечом, бой) НЕ делаем и
он тут не нужен: seq_91 сам заканчивается убиранием меча в ножны
(кадры 230..240) и возвратом в обычную стойку — как в оригинале до
встречи со стражем. Питьё зелий не портировано: над зельем Kid только
приседает (get_item возвращает «не обработано»).
Проверено в MAME (комната 15): меч виден на полу, Shift над ним даёт
присед -> кадр 229 «нашёл меч» -> ножны -> стойка; меч с пола исчезает,
pop_have_sword = 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Осторожный шаг к проваливающемуся полу проваливал Kid с первого же шага:
в pop_edge_distance не было ветки оригинала (seg004:067C)
if (tiletype == tiles_11_loose) goto loc_59FB; // CLOSER + до кромки
— плита читалась как обычный пол (EDGE_FLOOR, 11), и весь механизм
«проверки ногой» пролетал мимо. С веткой safe_step (seg005) отрабатывает
три фазы, как в оригинале: (1) укороченный шаг РОВНО до кромки плиты
(seq 29..42 по distance), (2) seq_44 testfoot — щуп ногой, плита трясётся
по SEQ_KNOCK_DOWN, Kid остаётся на месте (Char.repeat), (3) только третье
нажатие — шаг на плиту и падение вместе с ней.
Заодно портированы соседние ветки той же функции: верх двери лицом вправо
(проверяется ДО wall_type) и closer/меч/зелье (кромка, пока есть зазор).
Проверено вручную в MAME: все три фазы.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Комната 12, вис и подтягивание на кромке loose-плиты над дырой от
соседней упавшей: плита рисовалась ПОД Кидом — он лез на неё «с
переднего края» вместо проёма. Не хватало двух кусков draw_tile:
1. draw_loose (seg008:0A38) кладёт нижнюю грань плиты (loose_fram_bottom)
В ОБЕ таблицы — backtable И foretable, безусловно. Это единственный
кусок тайла с таким поведением: у обычного пола bottom идёт только в
backtable, а fore_id = 0. Добавлено в fore_tile (наш проход foretable
по тайлам футпринта Кида); ceiling-случай это уже делал отдельно.
2. draw_tile_base (seg008:0A8E) подставляет id: у loose верх плиты берётся
из loose_fram_left, у opener'а без пола слева — 148. В нашем
midtable-оверлее (overlay_mid_tile) стоял голый tile_table.base_id, а у
loose он 0 — верх плиты в оверлей не попадал, и поверх Кида ложилась
только передняя грань. Перенесён draw_tile_base целиком.
Проверено в MAME: кадр виса на кромке целой плиты (frame 89, x=95,
col 2) — плита закрывает Кида, наружу торчат только пальцы, как в SDLPoP.
Голова стоящего Кида поверх падающей НА НЕГО плиты — артефакт САМОГО
оригинала (сверено с SDLPoP v1.24), не чинить: записано в bug_list.md
(раздел «НЕ БАГИ») и в memory.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Комната 5, прыжок в решётку: Kid оставался стоять на 6 пикселей выше
пола и без приземления-приседания. Шесть пикселей — это dy(-6) кадра
frame_25_standing_jump_10: удар обрывал standjump ровно между кадрами
25 и 26, парный dy(+6) не выполнялся.
От оригинального bumped() у нас был портирован только хвост (выровнять
X к грани + seq_47_bump). Добавлены недостающие ветки:
- bumped() — исход удара выбирается по тайлу, НА КОТОРОМ персонаж
оказался после отжатия (сквозь стену/верх двери это
соседняя клетка, для ворот/зеркала — сама клетка);
- bumped_floor — прижимает Y к y_land, ветка fall_y>=22 (только отжать
на 5) и выбор сиквенса по кадру: 24/25/40..42/102..106
-> seq_46_hardbump (отскок с приседанием), иначе 47;
- bumped_fall — удар выше пола на 15+ px (беззнаковое сравнение
оригинала) или не над полом: seq_45, в свободном
падении — только гашение fall_x.
Гард «уже в отскоке — не рестартить» (наш, от edge-триггера коллизий)
перенесён внутрь bumped_floor: выравнивание X/Y идёт всегда, рестарта
анимации нет. Проверено в MAME.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Комната 1, плита (2,6): её правая грань (env 42, лежит целиком в ячейке 7)
оставалась ПОВЕРХ пола (2,7) и перекрывала его переднюю грань.
Причина: mob_render перерисовывал соседний тайл по m->row — а это
ЛОГИЧЕСКИЙ счётчик «сквозь какой ряд летим», который mob_down_a_row уводит
на ряд вперёд. Для плиты НИЖНЕГО ряда он сразу становится 3, и окклюзия
звала draw_tile(3, col+1) — ряда 3 нет, тайл рисовался за нижним краем
экрана, то есть пол соседа не перерисовывался никогда.
Оригинал берёт ряд ИЗ КООРДИНАТЫ куска, draw_mob (seg007:13E5):
tile_row = y_to_row_mod4(ypos); set_redraw2(tilepos справа)
top_row = y_to_row_mod4(ypos - 18); если отличается — ТОЖЕ пометить
То есть помечаются ДВА тайла справа: под низом куска и под его верхом, пока
кусок висит на границе рядов. Второго у нас не было вовсе — и именно он тут
решающий: при y=194 нижний ряд даёт -1 (полоса у потолка), а верхний — 2,
то есть настоящий пол (2,7).
Проверено в MAME покадрово (bp на pop_loose_mob_spawn + cmd gv; NB: главный
цикл ждёт ТРИ vsync на итерацию, один gv = треть игрового кадра): следов
плиты на полу (2,7) нет, передняя грань цела, потолочная полоса не тронута.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обход графа связей res2001.bin (links @1952) от стартовой комнаты 1: 13, 18
и 24 недостижимы — ссылки наружу у них есть, на них не ссылается никто
(24: L->9, но у 9 R=0). Несимметричные ссылки ровно у этих трёх, у прочих
21 симметрия полная — признак выкинутых из компоновки комнат. В таблицу
обхода добавлена пометка, чтобы не гоняться за призраками: рендер кромки
читает колонку соседа ПО ССЫЛКЕ, а сосед этих комнат соседом себя не
считает, так что странный шов там — свойство данных.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Комментарий у блока разбора --w3 утверждал, что режим подразумевает small и
несовместим с huge, хотя код тремя строками ниже huge как раз поддерживает:
резидент делит окно W3 с трамплин-банками (порт 0xE2), crt0_banked запоминает
резидентную страницу и возвращает её дефолтом после загрузки банков, а
трамплин bank.s сохраняет/восстанавливает W3 на каждый __banked вызов.
Следствие, которое теперь тоже записано: резидент -> __banked работает,
обратное невозможно (из банка резидентной страницы в W3 просто нет).
В справке по --w3 дополнено правило «W3-код не переключает страницу W3»:
сюда же относится собственная графическая скобка _bgi_begin/_bgi_end — она
маппит видеобанк ЧЕРЕЗ ТОТ ЖЕ порт 0xE2, и открытая из W3-кода означает
исполнение из видео-ОЗУ (белый экран). Звать графические примитивы HOME
можно: скобку они открывают и закрывают внутри себя.
Только комментарии, поведение не менялось.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ROOMNAV (одна строка #define в roomtest.c): '+'/'-' — следующая/предыдущая
комната уровня ПО НОМЕРУ (1..24, с обёрткой), Kid ставится на первый пол,
pop_trob_reset() возвращает пики/ворота в исходное. Нужен для обхода всех
24 комнат в поиске багов отрисовки, включая недостижимые обычным путём.
Цена — 505 Б кода W1 (22816 -> 23321), они же вычитаются из кучи (4178 ->
3673 Б); W3 не тронут. Переводить приложение в huge ради этого не стали:
смена модели памяти (банкованный W1 + трамплины) ради полукилобайта —
плохой размен прямо перед прогоном комнат.
Номер комнаты — ПОЛОСКАМИ в верхнем борте (слева десятки, справа единицы),
а не outtextxy: текст тянет системный знакогенератор (_gfx_font_buf, 2 КБ
статики) и сажает кучу до 576 Б. Цвет полосок 0x57, а не WHITE: палитра
игровая, запись 15 в ней ЧЁРНАЯ (kid.pal 0x0F = 0,0,0) — из-за этого
закомментированный ранее лейбл был бы невидим в любом случае.
bug_list.md: раздел TODO (T-1 редрой пик по причине, T-2 idle-skip) +
пустая таблица на 24 комнаты под результаты обхода.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Выдвинутая пика живёт ТОЛЬКО в видео-ОЗУ (банк SPRITE, в запечённый фон не
входит), а kid_heal каждый кадр возвращает под спрайтом Кида печёный фон —
то есть стирает попавшие под него части остриёв. Редрой «только при смене
видимой сигнатуры» их не возвращал: hold держится 15 кадров (modif
0x8F..0x81) с неизменным кадром 5. Итог по дампу VRAM (комната 14, Kid на
(2,7), modif 0x8E): у спрайта 132 стёрто третье остриё (x 245..247), у 138 —
прямое целиком (x 258..260) и низ наклонного, страницы дабл-буфера
расходились на 8 пикселей. SDLPoP: animate_spike (seg007:0353) зовёт
redraw_21h БЕЗУСЛОВНО, вне всяких if — редрой каждый кадр, пока trob жив.
pop_spike_redraw переписан на честный redraw_tile_height (seg007:0218):
heal + ПОЛНЫЙ draw_tile своего тайла и правого соседа. Рисовать только два
спрайта остриёв нельзя — теряется порядок слоёв внутри тайла
(draw_tile_anim_right идёт ПЕРВЫМ, база и fore соседа ложатся поверх), и
остриё лезло на колонну (2,9). draw_tile заодно сам восстанавливает вклад
соседних пик (ветка lcode==2), так что две пики подряд больше не гасят
друг друга.
Пламя факела: heal убран. Оригинал рисует его blitters_0_no_transp (seg008
draw_tile_anim_right), все 9 кадров лежат на общем канвасе 16x18 и бокс
центрирован в ячейке — непрозрачный блит сам полностью накрывает предыдущий
кадр. Кадры пакуются с opaque=True (POT_FLAME_IDS).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Кнопка-closer (tiles_6) в нажатом состоянии рисуется как tiles_5_stuck
(get_tile_to_draw seg008:2FE), но её грани — env id 35 (правая) и 36
(нижняя) — не попадали в атлас: pop_pack_bg собирает набор спрайтов
прогоном render_room по статике уровней, а тайла 5 статически в уровнях
нет. Итог — чёрный провал на месте нижней грани. Добавлены явным
набором STUCK_ENV_IDS (как loose/пики/ворота). Заодно OPENER_NOFLOOR_ENV_IDS
= {148}: левая половина кнопки-opener, когда слева пусто (draw_tile_base
seg008:0A8E) — ветка была не реализована ни в pop_bg, ни в render_room.
bake_rest: при ЗАПЕЧКЕ статического фона (запись идёт в ОЗУ-копию
акселератора, из неё восстанавливает heal) get_loose_frame возвращает
кадр ПОКОЯ. Иначе транзиентный кадр тряски соседней плиты консервируется
в копии, а так как запечка идёт двумя кадрами (по одному на страницу
дабл-буфера) — на страницах застывают РАЗНЫЕ кадры, вечное мерцание.
mobs[MOB_MAX] вместо одиночных статиков: две плиты, упавшие подряд,
теряли первый кусок (застывал в воздухе) и оставляли щебень только от
одной. Плюс отложенная допечка прошлого приземления ПЕРЕД разбором
нового и ограничение чёрного бара в pop_loose_bake_empty низом тайла.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kid не мог залезть на тайл поднятой решётки: срывался обратно. can_climb_up
(seg005:09DF) выбирает seq_73 («влез под решётку и съехал») только для
ЗАКРЫТЫХ ворот — условие включает curr_room_modif[curr_tilepos] >> 2 < 6, а у
нас его не было, и seq_73 играл при любой решётке над головой при взгляде
влево.
Та же дыра нашлась в спуске: down_pressed (seg005:482) запрещает climb-down с
тайла ворот лицом влево тоже ТОЛЬКО пока они закрыты
(|| curr_room_modif[curr_tilepos] >> 2 >= 6). Без этого Kid приседал вместо
спуска даже под поднятой решёткой.
Openness обеих проверок берётся общим хелпером gate_modif(col,row) (вынесен из
gate_passable — умеет свою комнату и соседнюю через шов).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три фикса, найденные разбором в MAME.
1. Просвечивание Кида сквозь торец кнопки (подъём/спуск у кромки).
draw_tile_bottom (seg008:570) рисует нижнюю грань блиттером
blitters_0_no_transp: пиксель индекса 0 заливается ЧЁРНЫМ, а не
пропускается. У граней кнопок (env 96/149) и нижних кадров loose (73/74)
последняя строка целиком нулевая — наш упаковщик переводил 0 в 0xFF для
ВСЕХ спрайтов, и она становилась дырой, сквозь которую был виден Kid за
тайлом (на запечённом фоне не видно — там и так чёрное).
pop_pack_bg.py: NO_TRANSP_ENV_IDS пакуется с сохранением индекса 0 как
ЦВЕТА (0x50 = чёрный в палитре env). Ноль тактов в рантайме.
2. Разворот в проёме опущенной решётки выкидывал Кида на другую её сторону.
check_bumped проверял столкновение УРОВНЕМ («край зашёл за грань»), поэтому
разворот на месте внутри тайла-стены менял, какую грань мы меряем, и это
читалось как новое столкновение → Kid.x = char_dx_forward(d) швырял его
сквозь решётку. Оригинал (check_collisions + get_row_collision_data,
seg004:0004) считает флаги перекрытия от ГАБАРИТА персонажа, БЕЗ
направления, и зовёт bumped() только на переходе 0 -> 1 (вправо смотрит на
0x0F, влево на 0xF0). Порт этого edge-триггера: флаги от габарита + память
прошлого кадра по колонке, сброс при смене комнаты (в оригинале это
несовпадение prev_coll_room/curr_row_coll_room). Проверено: теперь
разворот даёт уход в комнату 8 (leave_room, взгляд влево, char_x_left<=54)
— то есть туда, где Kid физически и стоит.
3. Снимок шва расширен до ДВУХ колонок с каждой стороны (lcol_fg[6]/
rcol_fg[6]: col9+col8 и col0+col1). Kid, стоящий В шве (curr_col=-1),
смотрит вперёд на колонку -2, где раньше была мнимая стена (get_tile ->
TILE_WALL). Оригинал резолвит любую колонку через find_room_of_tile
(seg006:005D).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Спуск Кида с кнопки (room8, кромка (0,6)) рисовался неверно: Кид просвечивал
в щель между кнопкой и ближним столбом, а ближняя рука была срезана до одного
оторванного пикселя. Две независимые причины.
1. Не был портирован clip_char() (seg006:1749) — оригинал перед add_objtable
обрезает спрайт персонажа сверху по y_clip[curr_row+1], когда тайл над
головой стена или пол. Порт: pop_clip_char_top() (pop_map.c, метрики по
set_char_collision) + новый примитив gfx_blit_cols_part() в libbgi (блит
column-major с пропуском skip верхних строк; обрезка сверху бесплатна —
колонка непрерывна в ОЗУ, сдвигается только старт). gfx_blit_cols стал
тонкой обёрткой над ним. kid_heal чистит уже ОБРЕЗАННЫЙ прямоугольник,
иначе стирается кромка пола над срезом.
2. climb_overlay_tile (порт draw_floor_overlay, seg008:1E3A) выбирал ветку по
СЫРОМУ коду тайла, а get_tile_to_draw (seg008:240) подменяет нажатую кнопку
на floor/stuck. Тайл-кнопка не проходил тест floor → уходил в
draw_other_overlay, который кладёт поверх Кида ВЕСЬ тайл вместо узкой
кромки floor_left_overlay[frame-137]. Фикс — tile_code_drawn(): одна
подстановка на все слои (fore_only_tile/overlay_mid_tile перестали её
дублировать, W3 −293 Б).
Проверено покадрово в MAME (шаг gv + снимок): кадры спуска 148..138 чистые,
после приземления на кнопке мусора нет.
Заодно: gfx_heal_noclip() (пара к gfx_blit_noclip; heal 11658 -> 8982 тактов)
и rest_pending в pop_trob — холостой проход по 30 тайлам только когда есть
отложенные редрои пик/кнопок (pop_process_trobs 89-111К -> 70-92К тактов).
START_ROOM временно = 8 (отладка спуска с кнопки), вернуть на 6/старт уровня.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Графика chtab_1 (flame/sword/potion) — новый атлас pop_pot.atl:
- склянка зелья берётся из chtab_1 (seg008 draw_tile_fore: id 12 малая /
13 большая при типах 2..4), а НЕ chtab_6 id 12 — res212 в VDUNGEON нет,
каскад падал на VPALACE и рисовал кусок дворцовой декорации («мусорный
объект» в комнате 5);
- пакуем chtab_1 целиком (id 1..23): пламя, склянки, кадры пузырька;
- MONO-блит (method_3_blit_mono, seg009:3040) красит спрайт цветом из
ОБЩЕЙ 16-цветной палитры экрана — она добавлена в 0x30..0x3F, палитра
chtab_1 в 0x40..0x4F; пузырёк пакуется силуэтом цвета 12 (красный),
его маска — цвета 0;
- в env-атлас добавлено основание факела (env 146, seg008:489 — рисует
правый сосед); в статический render_room оно не попадало.
Анимация (seg007 animate_torch/animate_potion + seg000:0B12
anim_tile_modif): при входе в комнату факелам и зельям задаётся случайная
стартовая фаза и заводится trob; факел — get_torch_frame, пламя в ячейке
ПРАВОГО соседа (xh = draw_xh+1, y = draw_main_y−40); зелье —
bubble_next_frame по младшим 3 битам модификатора (старшие 5 = тип, при
загрузке уровня modif <<= 3, seg009). Пламя и пузырёк не запекаются в
фон: heal своей области + кадр поверх. Скорость пламени /2 — наш
логический кадр короче игрового тика оригинала.
Скорость отрисовки (профиль в MAME, кадр = 430 080 тактов):
- замер показал, что цена блита почти НЕ зависит от размера — 13 288
тактов на спрайт 32×3 против 4 617 у линейного спрайтового ядра без
клипа; платим за проход gfx_blit → gfx_blit_part → _gfx_blit_full;
- в libbgi добавлен gfx_blit_noclip() — блит без клипа в ТЕКУЩЕМ банке
(putsprite не годится: навязывает GFX_BANK_SPRITE, фону нужен
TRANSPARENT ради ОЗУ-копии); pop_bg.blit_b уходит на него, когда
спрайт целиком на экране и не нужен g_clip_top → ~2.9× на блит;
- W3-скобку ставит САМА libbgi: из модуля с --w3 её вызывать нельзя —
после _bgi_begin окно W3 занято видеобанком и код вызывающего исчезает
(проявлялось белым экраном);
- редрой шва больше не перерисовывает полосу потолка (bar начинается с
POP_YOFF+3) — это удваивало стоимость блока при анимации решётки;
- docs/size_optimization_plan.md §7–§8: замеры, сделанное и запас
(батчинг W3-скобки, решётка одним спрайтом, лишние блиты).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Фича — плита-ПОТОЛОК (loose ряда 2 комнаты сверху, комн.5 (2,5) над комн.6):
- check_press (seg006:1683) портирован целиком: кадры виса 87..99 и подъёма
135..140 «нажимают» тайл, за который Kid держится; кадр 79 — удар снизу
(с проверкой action, как в оригинале); стояние — тайл под ним;
- ряд −1 в pop_map.get_tile = ряд 2 комнаты сверху (порт find_room_of_tile),
состояние тряски/падения — pop_ceil_modif[10]; do_knock(-1) трясёт потолок;
- падение: mob переведён на координаты оригинала (y_loose_land/y_something),
спавнится в ряду −1, по move_loose находит пол → loose_land: debris +
do_knock; heal коридора — по прошлой позиции ДЛЯ КАЖДОЙ страницы;
- урон: check_loose_fall_on_kid + fell_on_your_head (seq_52/seq_22, −1 HP)
и «брызги» draw_hurt_splash (kid image 218);
- колодец наверх: leave_room «вверх» проверяется ПЕРВЫМ (иначе y≈248 при
подтягивании ловил check_leave_below и ронял Kid из уровня);
- персистентность: общий tile-override (щебень внизу, пустота в комнате
сверху) вместо debris-only.
Окклюзия — приведена к оригиналу (найдено трассировкой собранного SDLPoP):
- draw_tile_right/anim_right/draw_loose кладут спрайты в add_backtable
НАПРЯМУЮ, минуя ptr_add_table → правая грань соседа («шахматка» столба 93),
blueline и грани loose/пик соседа ВСЕГДА под персонажем. draw_other_overlay
поверх Kid даёт только: 42 при левом соседе-поле, base, свои пики, bottom
(overlay_mid_tile);
- порядок объектов: тайл Kid (set_objtile_at_char) и порядок обхода
(ряды 2→0, колонки 0→9), внутри тайла — сортировка по y (sort_curr_objs);
то же правило для падающей плиты (pop_loose_mob_draw_over);
- fore-слой рисуется ПОСЛЕ оверлеев (foretable после midtable), иначе тёмный
скос floor_left_overlay ложился поверх колонны;
- fore_tile больше не рисует bottom_id (в оригинале он в backtable) — минус
2..4 блита за кадр;
- полоса потолка поверх Kid = draw_tile_bottom(1)+draw_loose(1)+draw_tile_fore
(arg_0=1 добавляет в foretable), остальное — под ним;
- следы оверлея восстанавливаются на ОБЕИХ страницах (pop_fore_heal).
Прочее:
- control(): при action bumped/in_freefall управление игнорируется целиком
(seg005) — иначе прерванный присед терял dy(1)+dy(1) из medland и Kid
навсегда оставался на 2px выше пола;
- check_bumped: стена ищется по колонке переднего края (а не curr_col) +
гард разворота — Kid больше не проходит сквозь опущенную решётку шва;
- pop_floor_bake для смены floor→debris (bake_empty стирал верх стены ряда
ниже — чёрный прямоугольник);
- в pop_room_redraw_seam_left восстанавливается полоса потолка над решёткой.
Проверено в MAME (bridge); эталон — собранный SDLPoP из applications/PoP/SDLPoP.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт Prince of Persia (applications/PoP/roomtest): развитие уровня,
объекты (loose-полы/обломки), переходы между комнатами, фон-упаковка;
планы (room_model/size_optimization), bug_list, pop_trob.
Арт third_party/16x16-RPG-characters.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- kbd_raw_sync: цикл вычерпывания SIO FIFO (не 1 байт/прерывание) —
фикс залипания клавиш; overrun-wipe сбрасывает только пострадавшие
клавиши, не модификаторы (typematic их не перечитывает).
- Гайд docs/kbd-games.md; заметка о Rx-overrun в docs/TODO.md; справочник
скан-кодов в docs/libc-reference.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Скролл региона video->video (неактивная страница -> активная, банк 0x50:
копия = скролл + heal цели):
- gfx_scroll_h / _bgi_scroll_rows_raw — горизонтальный, построчно без
страйдов (~54Т/строку), DI/EI бандами по 16 строк, h=0=>256;
- gfx_scroll_v / _bgi_scroll_cols_raw — верт. И/ИЛИ гориз. за один проход
без буфера (колонка = accel-burst LD A,A, STOP между read/write делает
промежуточный OUT Port_Y безопасным), банды по 16 колонок;
- _gfx_addr_shadow_base (адрес неактивной страницы) + gfx_rect_t.
Пример examples/scroll.
check_banks.py + sprinter-cc: отчёт раскладки памяти для ЛЮБОЙ модели
(W1/W2 код/данные, остаток кучи/стека, W3 при --w3, банки при --bank),
не только при --bank. Док docs/memory-management.md §10.
--w3 (резидентный код окна W3) + сопутствующее: crt0_banked W3_RESIDENT,
mkexe -W, tests/w3probe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Loose-пол room1(2,6) при падении даёт кусок, который в оригинале улетает в
комнату снизу (links.down) и приземляется на первый floor, превращая его в
debris (порт move_loose/loose_land seg007). Для room1(2,6): room2(0,6)=empty
пролёт → room2(1,6)=floor приземление → debris.
Минимальная реализация (без анимации полёта куска, только персистентный
результат — мини-версия P0 из docs/gates_spikes_plan.md):
- pop_map: сигнал pop_loose_fell (tilepos+1 упавшего loose).
- pop_level: pop_room_col_landing(room,col) — ряд первого floor сверху вниз
(пустые ряды пролетаются), иначе -1.
- roomtest: при pop_loose_fell вычисляет debris-приземление в комнате снизу →
персистентный override (dbr_room/dbr_pos, живёт между переходами) →
enter_room применяет поверх загруженных тайлов (floor→debris 0x0E).
Проверено в MAME: провал loose room1(2,6) → debris в room2(1,6), сохраняется
при повторных входах.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Переходы в соседние комнаты по горизонтали (порт SDLPoP leave_room/
goto_other_room/find_room_of_tile), проверено в MAME (room2↔room3↔room9,
room1↔room5 и т.д.):
- pop_level: pop_room_load теперь извлекает и rightcol (col0 правого соседа),
как leftcol — для коллизии/рендера правого шва.
- pop_map: pop_map_set_edges(l,r,u,d, lcol_fg, rcol_fg) — связи + кромки швов.
get_tile(col=-1)=lcol (col9 левого), get_tile(col=10)=rcol (col0 правого),
с гейтом по связи (нет соседа → стена) — порт find_room_of_tile. check_leave
(порт leave_room): передний край char_x_left<=54 / char_x_right>=201 (и
обратные <=57 / >=198) → x∓140, сигнал pop_leave_dir (1=left,2=right).
- roomtest: rcol-массивы; enter_room задаёт edges; обработчик pop_leave_dir
переключает комнату по pop_room_link.
- Фикс двойного перехода: без коллизии ЛЕВОГО шва Kid, войдя справа в соседа
(curr_col=-1), стоял «в стене» → in_wall выбрасывал его на второй переход.
Левый шов (get_tile(-1)=lcol) это чинит.
docs/gates_spikes_plan.md — ПОДРОБНЫЙ самодостаточный план на след. сессию:
интерактивные объекты (RAISE/DROP-кнопки, гейты, пики) + HP/смерть. Контекст
текущего roomtest, форматы уровня (LINKLOC/LINKMAP @1440/1696), данные room6,
декод связи кнопка(0,2)→гейт room8(0,9), триггер пик (check_spike_below),
точные ссылки SDLPoP, фазы P0(персистентное per-room состояние+trob)/
S(пики+HP/смерть)/B(кнопки+гейты).
NB: L3-вверх (climb-up в комнату сверху) ещё не сделан.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
При пересечении нижней границы комнаты Kid переходит в комнату links.down,
продолжая падать/спускаться (порт SDLPoP goto_other_room/leave_room).
- pop_map check_leave_below (порт leave_room, seg002): триггер по Kid.y>=211
(не curr_row) — единый для провала И спуска-зацепа/climbdown; репроекция
координат в кадр комнаты снизу (y-=189, curr_row=y_to_row_mod4) + сигнал
pop_fell_out. Убран прежний преждевременный триггер y>=189 из do_fall
(из-за него climbdown с curr_row=3 ждал y_land[4]=244 → пролёт через комнату).
- roomtest: enter_room(room) — загрузка комнаты в рабочие массивы + отрисовка
фона в обе страницы; обработчик pop_fell_out переключает комнату по
pop_room_link(down) или респавнит (нет комнаты снизу). cur_room-трекинг.
- pop_loose_reset (pop_map) + pop_loose_mob_reset (pop_bg): сброс loose-состояния
(индексировано позицией тайла) при смене комнаты — иначе течёт в новую.
Фиксы окклюзии при висе/спуске (сверено с SDLPoP):
- other_overlay_tile: пустой тайл НЕ окклюдирует — draw_tile для него рисует
лишь фоновую сетку (BLUELINE «силуэт кладки»), которая в оригинале уходит в
y-сортируемый midtable ПОЗАДИ Kid; у нас клалась поверх. Пропускаем.
- pop_fore_over_kid: порт set_char_collision (seg006:0723) — char_bottom_row =
y_to_row_mod4(obj_y), обёртка -1 → ряд 3 (Kid НИЖЕ комнаты при спуске), а не
ряд 0. Прежний кламп в 0 давал rT>rB → цикл окклюзии не выполнялся, нога
рисовалась поверх пола. single-room: ряд 3 клампится в 2 (передняя грань пола).
Проверено в MAME: провал вниз, спуск-зацеп/climbdown с кромки, окклюзия ноги.
Известно (позже): осколки упавшего loose в комнате снизу (перенос mob),
per-room modified-tile tracking (re-entry восстанавливает исходные тайлы), HP.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Отказ от room1_data.h: уровень читается из сырого blueprnt DAT 1.0
(applications/PoP/SDLPoP/data/LEVELS/res2001.bin, формат — Table 6 POP-DAT).
- pop_level.c/.h: уровень грузится ОДИН раз в отдельную EMM-страницу (данные
с offset 0x100, ISR-стаб в первых байтах — страница безопасна для W0-маппинга,
как атласы). pop_room_load(room) извлекает комнату в массивы приложения (W2):
fg[30] (тайл-код = байт & 0x1F, верхние биты-модификаторы пока отброшены,
как делал render_room.py), bg[30] (raw) + срезы соседей для кромок — правый
столбец left-комнаты (leftcol), верхний ряд down-комнаты (belowrow).
pop_room_link() — связи для будущих переходов (L2/L3).
- Рабочая копия ТЕКУЩЕЙ комнаты — в обычной памяти (W2, мутабельная: loose→
empty); страница уровня маппится в W0 только на время извлечения.
- Проверено в MAME: комната 1 рисуется идентично. Данные сверены байт-в-байт
(fg&0x1F, bg raw, leftcol из room5 совпали); belowrow теперь из реального
room2 (links.down=2), а не из прежнего частично-выдуманного хардкода.
- Память (small): DATA до 0xA260, стек от 0xBFFF — запас ~7.5КБ; EMM-страница
уровня вне W1/W2.
NB: res2001.bin берётся из клона SDLPoP (data/ в .gitignore) — как и генерация
атласов через pop_pack_bg.py; в наш репозиторий сырой уровень не тащим.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Проваливающиеся полы в roomtest (порт SDLPoP seg007/seg008), проверено в MAME:
- Падающий кусок (mob): правый край env-42 (w=26, доходит до mob_x+57)
теперь полностью покрыт heal-коридором (MOB_W 56->64) — убран тёмный
хвост-тень на полу 2,7 ПОСЛЕ падения.
- Окклюзия правого куска во ВРЕМЯ падения: после отрисовки плиты
перерисовывается сосед-пол draw_tile(row,col+1) в том же кадре — край
прячется за полом 2,7 (порт redraw_at_cur_mob: set_redraw_full+1).
- Тряска от сотрясения (knock): seqtbl-команды KNOCK_DOWN/UP в play_seq ->
флаг knock -> do_knock(ряд) трясёт loose ряда из покоя (modif=0x80).
KNOCK_DOWN в land-seq (приземление) и runcyc (footstep).
- Фикс «бесконечной тряски» (дабл-буфер): при завершении тряски (0x84->0)
перерисовать покойный кадр плиты на ОБЕИХ страницах (loose_rest, 2 кадра) —
иначе на одной странице застревает дрожащий кадр правой грани (живёт в
тайлах col и col+1) -> мерцание через флип.
Инфраструктура/документация:
- app.mk: цель `make hdd` (упаковка в D: для MCP-моста MAME).
- docs: POP-DAT-FormatSpecifications.pdf/.txt как каноническая спецификация
форматов ресурсов; ссылки в README/MSDOS_RESOURCE_FORMAT.
- README.md/CLAUDE.md для applications/PoP и roomtest (правило «SDLPoP —
источник истины», порядок слоёв, режим отладки freeze 1/2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Дополнен точными данными из SDLPoP (сверено): кадровые таблицы
loose_fram_left/right/bottom, get_loose_frame, y_loose_land, delay=11;
триггеры с call-sites (check_press->make_loose_fall при стоянии на 11,
frame79-сверху; do_knock на жёстком приземлении; animate каждый кадр);
tile_is_floor(11)=1. Плюс подход интеграции в наш движок: общая
мутабельная копия комнаты pop_map<->pop_bg + per-page запекание пустоты
после падения; тонкое место — перерисовка динамического тайла в дабл-буфере.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Тап-бег с кромки [1,3] вправо -> Kid проваливался в стену col2/3 ->
респавн, вместо посадки на [2,4]. Сверено с SDLPoP seg006:
- start_fall: frame 9 -> seq_7_fall (был ошибочно seq_19 как у 13);
frame 13 -> seq_19 (лишний dx(1)). + хвост seg006:1099: тайл ПЕРЕД
персонажем — стена -> Char.x = char_dx_forward(-1);
- in_wall переписан аутентично (seg006:1292): выталкивает ВПЕРЁД из стены
(delta+4) на соседний тайл, а не назад вглубь (наш старый через
dist_from_wall_forward давал отрицательный сдвиг -> Kid уходил в стену
col2 -> проваливался). distance_to_edge_weight + 6-delta/+4.
Проверено в MAME (пользователь): бег с кромки [1,3] -> посадка на [2,4].
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kid — высокий спрайт: голова/руки торчат в ряд выше опорного, floor/wall
там должны перекрывать его. Сверено с SDLPoP (seg003 redraw_at_char2 +
seg008 draw_tile_fore/draw_other_overlay/draw_floor_overlay):
- fore_tile (draw_tile_fore): стена рисует и WALL_FRAM_BOTTOM (нижняя
грань-решётка), не только MAIN — руки при прыжке в потолок [2,7]->[1,7]
уходят за стену;
- диапазон рядов форсит включение ряда над опорным (y_to_row верха спрайта
мог схлопнуться из-за +60-сдвига);
- other_overlay_tile (draw_other_overlay): на КРОМКЕ пола (сосед слева пуст)
перерисовать весь тайл поверх Kid — но ТОЛЬКО в позах захвата (78-79),
виса (action 2/6), полёта (3/4), старта падения (bumped 102-106) и начала
подъёма (135/136, до SEQ_UP на 141 Kid ещё в ряду ПОД полом). Гейтинг
как в redraw_at_char2 — при стоянии/беге ложной окклюзии нет;
- отладочный стоп-кадр: '1' заморозить / '2' продолжить (для разбора поз).
Проверено в MAME (пользователь): прыжок-в-потолок, короткий прыжок,
спрыгивание, подъём на [0,3] — окклюзия корректна.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Убирает мерцание/тиринг при перерисовке слоёв (Kid/fore/пол-оверлей):
рендер всего кадра в скрытую страницу, tear-free флип на vblank.
Инфра libbgi (gfx_set_draw_page/visible_page/wait_vsync) уже была.
- фон комнаты рисуется в ОБЕ графические страницы (у каждой своя
ОЗУ-копия — источник heal); палитра 0->1 уже синкалась gfx_pal_sync;
- kid_heal/kid_draw: прямоугольник Kid запоминается ПО СТРАНИЦЕ
(kid_l*[2]) — при чередовании страниц heal стирает пиксели своей
страницы (прошлый Kid там был 2 логических кадра назад);
- цикл: draw в back, 3x wait_vsync (пейсинг), gfx_set_visible_page(back);
- SPACE (edge) — тумблер: off = однобуфер (draw==visible==0) для отладки.
Мерцание при подъёме подтверждено устранённым в MAME (пользователь).
План: applications/PoP/docs/double_buffer_plan.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Разбор SDLPoP (seg007 loose/trob/mob, seg008 draw_loose): хранение
состояния (tile 11 + curr_room_modif: 0 покой / 0x80.. тряска / 1..11
отсчёт падения), триггеры тряски (do_knock на приземлении → shake ряда)
и падения (make_loose_fall при стойке/зацепе на loose), падающий кусок
(mob → debris снизу, empty сверху), отрисовка по статусу (loose_fram_*
через get_loose_frame). Порядок реализации L1..L5 + что нужно в нашем
движке (динамический тайловый слой: room_modif[], trob-очередь, mob).
ПЛАН — не реализация.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Проблема из динамики: при подъёме [1,2]→[0,3] нижняя часть Kid, которая
физически за полом назначения, просвечивала.
Порт SDLPoP draw_floor_overlay (seg008:1457): на кадрах подъёма 137..144,
если тайл-назначения floor-подобный (floor/pillar/stuck/torch) И тайл СЛЕВА
пуст (кромка), передний край пола (floor_left_overlay[frame-137] =
{32,151,151,150,150,151,32,32}) + низ пола рисуются ПОВЕРХ нижней части Kid.
- pop_bg: climb_overlay_tile + доп-проход в pop_fore_over_kid (новый параметр
frame) на кадрах 137..144.
- pop_pack_bg.py: CLIMB_OVERLAY_ENV_IDS={32,150,151} явно добавлены в env-фон
(не попадают в used render_room — рантайм-анимация); env4 count 16→24.
- roomtest: передача Kid.frame в pop_fore_over_kid.
Проверено в MAME (кадр 137): при подтягивании видна только голова/плечи Kid
над кромкой [0,3], нижняя часть скрыта за полом.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Баг (из динамики): стоя на кромке [1,3]/[1,4] лицом вправо + Up, Kid
телепортировался/застревал в стене col8.
КОРЕНЬ (трасса MCP): jumpup(seq_14) на кромке → приземление над ямой →
падение; на кадрах падения action=3 (midair) check_bumped был НЕ заглушён
(guard покрывал только freefall/hang/climb-кадры). Kid дрейфовал к стене,
get_tile вне рядов давал WALL, dist_from_wall_forward/x_bump[] — мусор с
большим отрицательным сдвигом → Kid.x=char_dx_forward((int8_t)d) → underflow
uint8 X (0→255) → долёт до col8 и застревание в стене.
Фикс check_bumped: заглушить и в action MIDAIR (кадры падения), и при
curr_row вне [0..2] (падение мимо пола — тайлы вне комнаты = WALL, bump-мусор;
fell_out ловит do_fall). Проверено в MAME: на кромке Kid больше НЕ уходит
в стену — чисто падает (в тесте сброс на старт).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Проблема из динамики: за стенами Kid не прятался (стена [2,9] должна
перекрывать). В PoP основная грань стены (wall_fram_main) добавляется в
FORETABLE (seg008:712) — перекрывает персонажа. У нас fore-проход
рисовал только fore_id, а у стены (0x14) fore_id=0.
fore_tile: для стены (code==20) рисуем WALL_FRAM_MAIN + wall_pattern(,,1)
поверх Kid (как draw_tile_fore), а не только fore_id. Проверено в MAME:
Kid прячется за правой стеной col9.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Порт SDLPoP seg003 redraw_at_char + seg008 set_char_collision: после
kid_draw передний слой (fore_id = foretable-кусок) тайлов ФУТПРИНТА
спрайта Kid перерисовывается ПОВЕРХ него — то, что по изометрии перед
персонажем (передние грани колонн/ворот/большой колонны/щебня). Стены и
факелы имеют fore_id=0 → остаются сзади (в статическом фоне).
- pop_bg: pop_fore_over_kid(obj_x,obj_y,w,h,dir) — считает футпринт
(char_x_left/right, col_from_x, y_to_row_mod4) рядов top..bottom ×
колонок left..right (≤2×2=4 тайла) и рисует fore_id каждого в
GFX_BANK_SPRITE (видео-ОЗУ; kid_heal восстановит из теневого фона,
fore в нём запечён pop_room_draw).
- pop_kid: kid_fp_obj_x/y/width/height — метрики последнего кадра
(obj_x ЛОГИЧЕСКАЯ, до ×8/7) для футпринта.
- roomtest: вызов pop_fore_over_kid после kid_draw.
Проверено в MAME: Kid, идя влево мимо колонны под навесом (row1 col3),
корректно уходит ЗА её переднюю грань (скрывается) и выходит с другой
стороны; задние колонны/факелы остаются сзади.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Порт SDLPoP seg004/005/006 «hang state»:
- pop_map: check_grab (зацеп за уступ в падении по Shift → seq_15 → вис),
can_grab/can_grab_front_above + tile-запросы над/за персонажем;
pop_jump_up_seq (check_jump_up: ↑ в стойке = чистый прыжок seq_28/14
ЛИБО прыжок-с-зацепом seq_8/24/16 за уступ выше → запрыгнуть на этаж);
pop_hang_* (climb_up seq_10/73, hang_fall seq_23/11, hang-у-стены);
pop_down_action (спуск seq_68 у края лицом от края / отступ / присед).
- pop_ctrl: control_hanging/can_climb_up/hang_fall, control_jumpup,
jump_up через pop_jump_up_seq, down_pressed через pop_down_action;
pop_ctrl_shift_held() для check_grab.
Ключевой фикс check_bumped (seg004 guard'ы): не бампить при action
hang_climb/hang_straight, на кадрах подъёма/спуска 135..148 И на кадрах
виса 87..99. Без последнего спуск (seq_68) на кадре frame_91 (action
ещё midair, act(hang_climb) идёт следующим опкодом) ловил отскок у стены
и рвал цепочку hang→hang_fall→seq_11, приземляя не туда.
Проверено в MAME (HDD-тест, покадровая трасса через мост): прыжок-с-
зацепом [row2 col4]→вис→подтягивание→[row1 col3]; спуск [row1 col3]→
[row2 col4] с корректной позицией у стены (x=122, совпадает с SDLPoP).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Клавиши 1/2/3 зовут gfx_set_fps_div(n) на лету (дефолт n=1 не меняет
поведение). Проверено в MAME: при n=2 FPS-метр стоит РОВНО на 024
(48.83/2) все 8 секунд без плавания — логический кадр = ровно 2 vsync'а,
скорость персонажей (пиксель/сек) постоянна. +808 Б (демо тянет код
делителя).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Логический кадр = ровно n кадровых интервалов (1=50/2=25/3=~16.7 fps);
при переполнении слота — выравнивание на ближайший фронт (без дрейфа
фазы, в отличие от наивного «жди n фронтов»).
Механика: фоновый счётчик _gfx_frame_tick инкрементит _gfx_frame_isr,
поставленный в СВОЙ слот цепи (irq_chain_add); gfx_set_fps_div(1) снимает
только этот слот (irq_chain_remove), не трогая хендлер приложения.
gfx_wait_vsync: ветка n>=2 (счётчик + halt) перед лучевым поллингом;
поллинг вынесен в static gfx_wait_vsync_beam (функция с хвостовым __asm
не должна иметь переходов через asm — SDCC не эмитит эпилог-метку;
ранний return делителя в чистом-C gfx_wait_vsync).
Файлы: common/_gfx_fps_state.c (данные), _gfx_frame_isr.c (ISR),
gfx_set_fps_div.c (сеттер, единственная ссылка на irq-механику → DCE).
Работает tiny/big/huge (цепочка all-modes); small для мелких программ
= EINVAL.
Проверено MAME (tests/fpsdiv): n=1/2/3 → 20/40/60 кадров на 20 wait'ов
(drift=0); n=2 с рендер-заглушкой ~1 кадр → период держится 2
(поглощение перерасхода, наивный путь дал бы ~60); huge идентично;
small = EINVAL graceful.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_irq_user → _irq_chain[4]+_irq_chain_n (W2-BSS); трамплин tr_frame
проходит слоты (один тяжёлый сейв на всю цепь, пустой слот пропуск).
API irq_chain_add (0/-1+ENOMEM) / irq_chain_remove(h); irq_install →
обёртка chain_add (EBUSY исчез), irq_remove() рвёт всю цепь; refcount
таблицы на первом/последнем слоте, мутации под IRQ_DISABLE. Лимит
4 кадровых + 1 CTC.
All-modes: трамплин copy-safe (только jr/djnz + литерал jp 0x0038),
_irq_table_ref копирует его в _irq_tramp_w2buf (W2) когда оригинал в W1
(small/huge), вектор → на копию; вокруг вызова хендлеров восстанавливает
базовую W1-страницу (_irq_app_w1_page = IN 0xA2). Хендлер может лежать
где угодно в плоском 0x4000-0xBFFF.
Проверено в MAME (tests/irqtest, 2 хендлера): tiny/big/huge — chain
h1=h2 → remove h2 → h1 жив/h2=0; huge = код в W1, remap работает.
Follow-up: CTC в small/huge = EINVAL (нужна W2-копия _irq_ctc_tramp);
small для мелких программ (BSS в W1) = irq_install EINVAL, safe.
Доки: im2_isr_design (цепочка), sprite-api §9е (делитель поверх цепи),
fast_ram §8. rpgprof: gfx_sprite_ysort в профиль (+230 Б baseline).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
wpiset на порт W1 (0xA2) + iff1 в dev-MAME: DSS перемапливает W1 при
ВКЛЮЧЁННЫХ прерываниях во время системных вызовов (файловые, загрузка
exe, видео; страницы 0xFE/FF/F3/0x50, PC ядра 0x15xx-0x2Exx) —
ограничение irq_install «только tiny/big» обосновано, handler в
W1-коде небезопасен принципиально. §9е дополнен фактом; в TODO —
дизайн цепочки irq-обработчиков (массив слотов в W2, chain_add/remove,
irq_install как обёртка; реализация по потребности).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Оптимизации A+B (профиль rpgwalk-15: активная часть кадра 410К → 307К
тактов из 430080; лимит спрайтов 16×16 на стабильные 48.8 fps: 14 → ~21):
- (B) sprite_t.src/stride — готовый адрес кадра: считают только
sprite_frame (теперь функция, одно умножение на СМЕНУ кадра) и тикер
(±an_step БАЙТ инкрементально); блит-ядра принимают src+stride,
img/sx/sy из сигнатуры ушли. Блит 177К → 146К на кадр.
- (A) тик 100К → 37.7К: tween переформулирован Брезенхэм → беззнаковый
DDA (mv_rem/mv_acc, «приехали» = rem==0 — без знаковых 16-бит
сравнений), tick_move и tick_anim — ручной asm (SDCC спиллит такие
функции в IX-фрейм ~100 обращений; C-реструктуризации не помогали —
проверено кодогеном). Биты an_flags переименованы по категориям
(_SPR_STRIP_HORZ, _SPR_PP_BACK).
Y-сортировка (gfx_sprite_ysort, идеи пользователя — 8-бит ключ,
персистентность):
- painter's algorithm по ключу {layer:8, clamp_y:8}; поле
sprite_t.layer (в КОНЦЕ структуры — asm-офсеты не сдвигает): слои
сцены в одном массиве/одном sprite_update;
- ПЕРСИСТЕНТНАЯ asm-таблица {key16, ptr16}: resort порядка прошлого
кадра (почти линейно), rebuild при смене arr/count; массив
приложения не трогается; ~28К/15 спрайтов (с нуля было 44К);
- компоненты YSORT_Y/YSORT_LAYER отключаемы независимо масками ключа
(без ветвлений в сортировщике); ВНИМАНИЕ: mode=1 значит Y-only,
полный порядок = YSORT_Y|YSORT_LAYER;
- funcptr-DCE: выключено = код и таблица не линкуются (rpgwalk −190 Б);
- ПРАВИЛО в sprite.h: два sprite_update на страницу запрещены (heal
второй группы стирает спрайты первой — ОЗУ-копия чистая).
Попутные фиксы:
- libbgi/Makefile: .rel зависят от заголовков (HDRS) — stale .rel со
старой раскладкой sprite_t молча ломал рантайм;
- rpgprof: --memory small (перерос tiny: BSS вылезал за W2 → мгновенный
«Unexpected application termination»; mkexe это пока не ловит);
- tests/spranim: проверки переведены на кэш src, добавлены T6 (reframe
после тикера) и T7 (Y-сортировка: порядок, слои, персистентный
resort, LAYER-only) — 7/7 PASS в MAME.
Доки: §9д — новый бюджет (19.5К/спрайт), §9е — ПЛАН FPS-делителя
(frame pacing, gfx_set_fps_div); TODO — дизайн цепочки irq-обработчиков.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Плашка слева-сверху (значение раз в секунду, рисуется на обеих
страницах — fps_draw=2, как в examples/space). ~39 fps на 8 ходящих
персонажах (кадр на грани 20 мс — частично двухvsync'овые кадры).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Демо на реальном арте (third_party/16x16-RPG-characters, bard):
conv_sprites.py режет PNG 192×128 (8 персонажей = блоки 3×4 кадров:
ряды вниз/влево/вправо/вверх × кадры маятника 0/1/2) в 8 вертикальных
лент по 12 кадров и строит bard.pal (слоты 0-15 EGA + цвета PNG с 16,
прозрачность → 0xFF). 8×12 кадров не лезут в одну EMM-страницу —
ДВА атласа по 4 персонажа (движок сам переключает страницы W0).
Палитра из файла: gfx_pal_fload + gfx_pal_sync. Смена направления =
sprite_anim(dir*3, dir*3+2, PINGPONG). Правила хождения (двухфазная
машина на sprite_moveto): до края → разворот 180° → случайная точка
(не меньше четверти экрана) → поворот ±90° → снова до края.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- gfx_pal_sync(): палитра страницы 1 := палитре 0 (все 256 записей,
чанками по 64 через общий буфер _gfx_pal_buf в W2) — обязательный
шаг дабл-буфера (грабли examples/space: без него флип на страницу 1
чёрный). examples/space переведён на хелпер.
- gfx_pal_fsave(pal, path): 256 записей × 4 Б (B,G,R,0 — родной формат
BIOS $A4) = 1024 Б.
- gfx_pal_fload(pal, path): принимает и усечённый файл (64 Б = палитра
16 цветов) — грузит сколько есть, остальные слоты не трогает;
возвращает число записей.
tests/palfile (MAME dev, все PASS): fsave; fload восстанавливает
испорченные слоты (n=256); усечённый файл на 2 записи чинит только их
(n=2, слот 2 не тронут); sync чинит испорченный слот палитры 1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Всё сразу: .atl с диска (mkatlas.py из генерённых лент) → atlas_load в
EMM-страницу (W0, ISR-стаб) → движок sprite_t.page → авто-анимации:
5 астероидов (ANIM_LOOP вращение + sprite_moveto к случайным целям
разных скоростей; по прибытии — взрыв ANIM_ONCE в точке + новая цель,
по SPR_ANIM_DONE взрыв прячется), маяк ANIM_PINGPONG; дабл-буфер,
FPS-метр, ESC. ~48 fps (vsync-кап).
Грабли по дороге (оба — прикладные, не библиотека):
- фон рисовался rand()'ом с разными последовательностями на страницах
→ мерцание звёзд; фикс — фиксированный seed на draw_space;
- НЕ была скопирована палитра страницы 0 → 1 (у каждой страницы своя,
см. gfx.h) — страница 1 показывалась чёрной, флип мигал
«сцена/чёрный»; fps-плашка теперь рисуется на ОБЕИХ страницах
(fps_draw=2 кадра при смене секунды).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Реализация §9г по требованиям пользователя:
- sprite_anim(first,last,speed,mode): ANIM_LOOP / ANIM_PINGPONG /
ANIM_ONCE (one-shot замирает на last), | ANIM_HORIZ для
горизонтальных лент. Смена кадра в тике = ±an_step к оси ленты —
без умножений (осевое смещение first считается в setup циклом).
- sprite_moveto(tx,ty,max_step,interval): Брезенхэм порциями
≤max_step вдоль большей оси (меньшая — err-аккумулятором,
нелинейные шаги Y сами собой), деления нет.
- Одновременность кадровой и tween — независимые поля/биты.
- Статус: sprite_anim_status() — битовое поле SPR_ANIM_ON/DONE +
SPR_MOVE_ON/DONE; sprite_anim_frame() — текущий индекс кадра;
sprite_moving().
- Стопы: sprite_anim_stop(frame | -1 = текущий);
sprite_move_stop(0 = замереть / 1 = прыжок в цель + DONE).
- Тикер _sprite_tick — проход 0 sprite_update через funcptr
_spr_tick_fn (DCE: без sprite_anim/moveto код не линкуется,
цена — один if на кадр; +40 Б программе с движком, sprite_t +18 Б).
tests/spranim (MAME dev, все PASS): LOOP/PINGPONG (разворот на границе
без удвоения краёв)/ONCE+DONE; пример (0,0)→(100,50), шаг 5,
интервал 4 → ровно 80 кадров, Y идёт 3/2/3/2; стопы обоих видов.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Решение: in-memory формат остаётся (getimage + sx/sy, ленты/сетки).
Рекомендация: кадры одного спрайта — одного размера с полной сеткой;
для спрайтов разных размеров — контейнерный формат .atl (каталог +
независимые getimage-ленты, паддинг невозможен по построению).
Формат — предложение, реализация по потребности первого приложения.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Диспетчеризация clip/noclip через указатели _gfx_blit_fn/_gfx_heal_fn
(common/_gfx_sprite_fns.c, дефолт clip): gfx_sprite_clip() — теперь
модуль, переключает указатели один раз; sprite_update/putsprite/
movesprite зовут через указатель — ветка if(clip) из горячего цикла
убрана (съедала половину выигрыша noclip). Программа без вызова
gfx_sprite_clip() noclip-ядра не линкует.
Замер dev-MAME (16 шаров, uncapped): clip 50 → noclip 60-61 fps
(+20-22%). Регресс tests/sprites (A/B PASS), size-check OK
(balls −237 Б, sprites −3177 Б — отвязались лишние ядра).
Фикс CPU-байта write-триггера (preread + EX AF,AF') снят: точная
dev-MAME эмулирует ПЛМ, подавляющую CPU-байт при активном burst'е —
подтверждено по байтам VRAM (tests/blitw col0 = GREEN через
read_vram MCP-моста). Для heal фикс был избыточен всегда (банк 0x50
перезаписывает dst[0]). Строки фикса оставлены закомментированными
в трёх leaf'ах на случай отличий реального железа; шапки и §9а/§9б
дизайна обновлены. НА ЖЕЛЕЗЕ ПЕРЕПРОВЕРИТЬ.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Спрайтовая графика поверх accel block-copy (docs/sprite-api-design.md):
- Ядро блиттинга: leaf'ы _bgi_blit_rows_raw (dst фикс, только src-страйд) /
_bgi_copy_rows_raw (getimage) / _bgi_heal_rows_raw (src==dst). DI один на
спрайт (санкция: малый спрайт под одним DI аудио не рвёт); src[0]-фикс
снят (точная MAME подавляет CPU-байт триггера записи — на железе
перепроверить; для heal был избыточен и снят безусловно).
- Общие bracket-free ядра _gfx_blit_full/_gfx_heal_full (полная ширина:
клип по экрану + split >256 для putimage) + лин _gfx_blit_sprite/
_gfx_heal_sprite (кадр ≤64, без split, 8-бит w/h) + noclip-варианты
(клип-кода нет → полный codegen-win). Имя *_full (не *_clip) — «clip»
двусмысленно (sprite-ядра тоже клипуют; различитель — ширина/split).
- Движок retained-модели <sprite.h>: sprite_init/update/flip + inline
move/frame/show/hide/touch; drawn[2] per-page внутри структуры; кадр —
двухпроходно heal ВСЕ -> блит ВСЕ под одной W3-скобкой/банком на проход.
- Флаг gfx_sprite_clip(on/off): приложение, само следящее за границами,
отключает клип (~+19% на анимации; диспетч пока через if — funcptr далее).
- putsprite/movesprite/gfx_blit/putimage(COPY)/getimage переведены на ядро.
Тесты: examples/balls (движок, дабл-буфер, boundary-тест клипа),
tests/sprites (RAM PASS, клип 4 края, атлас), tests/blitw (trig-leak),
tests/spriteclip (hardware-probe: железо НЕ режет за краем -> клип нужен),
tests/blitperf, tests/gfxbanks. size-baseline обновлён (53 программы).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Симметрично libbgi (bgi256/bgi256_safe): fast = -DLIBC_NOCHECK,
дефолт sprinter-cc; safe линкуется по --safe (флаг уже существовал).
Под LIBC_NOCHECK вырезаны ТОЛЬКО параметр-валидации:
- fgetc/fputc: NULL-check в горячей asm-обёртке (~11Т на каждый байт);
- fgets/fputs/fread/fwrite: NULL ptr/fp и EBADF на неверное направление
потока; ftell/fseek/ungetc/fclose: NULL fp; cputs: NULL s.
НЕ тронуты: критичный _fd_guard (9-й OPEN вешает DSS — в обеих
версиях), функциональная маршрутизация (консоль/направление/hold),
cold-path валидации (fopen/cbl_open/irq/settextmode — экономии ноль).
libc/Makefile — dual-build по образцу libbgi (build/fast + build/safe,
общий стейл-контроль). Корневой Makefile: в TESTS добавлены bgitest,
bgi_img, accfill, cblstream — раньше не собирались корневым make и
выпадали из size-check при чистой пересборке.
Дельты fast vs старая (safe-семантика): filetest -481, fbench -384,
solidt -180, errno -71, остальные -3..-9; роста нет. Проверено в MAME:
filetest fast и safe (--safe, sprinter_safe.lib) — прогоны идентичны.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Inline (чистый C99 `inline` без static, паттерн из libbgi/5de2f06):
textattr, get_text_attr, get/set_putch_raw_mode (conio.h; g_text_attr
и pc_raw_mode объявлены публично), isatty (unistd.h — сворачивается в
константу при константном fd). Модули удалены (5 шт).
«Толстые» кандидаты НЕ инлайнены — критерий проверен замером на 47
программах: feof/ferror/clearerr (тело ~12-15 байт с NULL-проверкой,
filetest +56 Б при инлайне) и textcolor/textbackground/set_text_attr
(RMW-маски, conio2 +13 Б) остаются модулями — при 2+ сайтах вызова
инлайн крупнее call+общее тело. Правило: инлайнить только тела
<= ~6 байт на сайте или сворачиваемые константами.
Дельты: solidt -94, hello -24, mouse -7, bios_text -7; conio2 +11
(textattr×5 — паритет, принято за скорость). z80.lib SDCC не содержит
feof/isatty — маскировки удалённых модулей нет, провал инлайна = ошибка
линковки. Проверено в MAME: conio2 (атрибутная матрица), filetest
(полный прогон). В size_baseline также вошли gfx_dbuf +608/gfx_demo
+25 — это первый чистый релинк run-рендера текста (ba09c0b), не inline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
setcolor/getcolor/setbkcolor/getbkcolor/getmaxx/getmaxy/getmaxcolor/
getx/gety/moveto/moverel/setfillstyle/graphresult (graphics.h) и
gfx_get_bank/gfx_set_bank/gfx_get_draw_page/gfx_get_visible_page
(gfx.h) определены inline в публичных заголовках; state-переменные
объявлены там же (хранилище прежнее — _bgi_state.c/_gfx_state.c).
Именно `inline` БЕЗ static: проверено артефактами (.asm) — SDCC 4.5
инлайнит вызов при всех наших флагах (--opt-code-size/--opt-code-speed/
--max-allocs) и не эмитит standalone-тело; `static inline` эмитил бы
мёртвую копию каждого аксессора в КАЖДЫЙ включивший модуль. Отказ
инлайнить = громкая ошибка линковки (все 47 программ слинковались).
Экономия ~30-40Т на вызов, минус 17 .rel; по _CODE размер-нейтрально
(сайт вызова ≈ телу). size_baseline: bgitest +267/accfill +528 — это
НЕ inline, а run-рендер текста из ba09c0b (draw_scaled потянул
vspan_raw+vfill256 и сам вырос) — цена за ~2-6× скорость текста.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Строка глифа сканируется на прогоны единичных битов; прогон n битов =
прямоугольник n*size×size → hspan/vspan-примитивы (одиночный пиксель —
plot_raw). Координаты инкрементальные (+= size за бит) — умножений на
пиксель нет. Прогон клипится до span'а — клип теперь работает и в
fast-сборке (span-raw диапазон не клипят). ~2× на size=1, ~6× на
size=4. Проверено в MAME (bgitest: масштабы 1..4 + VERT — пиксель в
пиксель).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
plot/read/hspan/vspan_raw читают D/E (y в DE), но не пишут — аннотация
задокументирована в _bgi.h. Честное измерение (полная пересборка с/без,
diff всех .asm в common/ и bgi256/): кодогенерация SDCC 4.5 НЕ меняется —
вызывающие держат локали в IX-фрейме и перезагружают DE перед каждым
вызовом, спасений DE вокруг вызовов не было. Оставлено как контракт на
будущее (изменение вызывающих/компилятора); при правке asm сверять
клоббер-лист.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bar(): rectfill-ветки выходили ранним return без _bgi_end() — W3
оставался замаплен на видеобанк после каждого bar() со сплошной
заливкой.
_bgi_read дублировал getpixel (та же композиция begin+read_raw+end);
единственный потребитель floodfill переведён на getpixel, модуль
удалён. getpixel.c: убраны мёртвые статики _gp_* (остались от
до-register версии).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
При узкой стороне <= 8 линий chunked-rectfill проигрывает циклу
_bgi_hspan_raw/_bgi_vspan_raw (подготовка ~340Т впустую, break-even
n~9): либо fast-path в диспетчере, либо рецепт для пользователя —
решать по профилю.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs/accel-fill-budget.md: полный разбор v3 (подготовка ~340Т на
прямоугольник, 46Т на чанк 16 линий, 53Т/51Т на линию, ~107 байт/leaf)
против per-line di/ei вариантов (djnz 72Т/линию ~63 байта; 16-бит IX
206Т/линию); break-even n≈9 линий, тотализатор, решение остаться на v3.
Краткие выжимки — в шапки _gfx_rect*fill256.
Идеи на будущее (там же): ограничить контракт стороной <=256 (широкие
прямоугольники пользователь выводит в два приёма); реализовать оба
варианта (поблочный/построчный) с переключением опцией сборки в духе
GFX_NOCHECK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Семантика FSM акселератора вскрыта по драйверу MAME (sprinter.cpp) и
подтверждена экспериментами в MAME (tests/accfill): армирование живёт до
следующего accel-опкода (мульти-триггер работает); под армированием
триггерит ЛЮБОЙ non-M1 доступ к памяти (fetch операнда djnz/out!);
вертикальный Fill двигает Port_Y и не возвращает; размер блока переживает
LD B,B. Детали: memory/accel_multitrigger_fill.
- _bgi_clear_raw: 20 DI-скобок по 16 колонок вместо полного брекета на
каждую из 320 колонок; армирование размера один раз на скобку.
- _gfx_rectfill256 разделён: диспетчер (проверка ориентации ~160-245Т,
break-even |w-h| >= ~4) + leaf'ы _gfx_recthfill256/_gfx_rectvfill256
для прямого вызова, когда форма известна заранее.
- Leaf'ы: чанки <=16 линий одной скобкой (~54-56Т/линию против ~250Т у
per-line варианта); счётчик чанков precompute'ится в байт-регистр
(dec e/jr nz ~46Т/чанк вместо 16-бит арифметики в IX-слотах
~173Т/чанк); хвост — отдельная скобка со своим армированием (CBL-ISR
в окне EI армирует акселератор своим размером — не выносить).
- getpixel/putpixel/_bgi_read: уборка мёртвого закомментированного кода.
- tests/accfill: регресс chunked-заливок (B0 clear, B1 vert 1+3 чанка,
B2 horz чанк+хвост, полноширинный bar 320x8 — путь w>=256).
Прежние реализации сохранены под #if 0 для отката/сравнения.
Проверено в MAME (все PASS); семантика эмуляции — перепроверить на
реальном Sprinter. size-baseline: accfill добавлен, gfx_demo +54 Б
(обвязка диспетчера), остальные без роста.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Эталон отставал от коммита 0296079. Дельты объяснены: bgitest/bgi_img
уменьшились (убран скретч _gfx_acc256), gfx_demo вырос (демо rectfill в
исходнике), gfx_dbuf вырос (vsync-wait тянет CBL через общий порт
0x004E — см. _cbl_port_ref/unref).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Задел для будущих leaf'ов, читающих/пишущих строку или столбец пикселей
одним burst'ом (как _bgi_hspan_raw/_bgi_vspan_raw), чтобы ускорить блит
getimage/putimage (сейчас per-pixel _bgi_read_raw повторяет Port_Y +
addr + bounds-check).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_bgi_vspan_raw: safe-версия (без GFX_NOCHECK) больше НЕ клиппит диапазон
y+len и не обрабатывает y<0 — проверяется только валидность x/y (как в
_bgi_hspan_raw). Сознательный компромисс: safe ловит грубый выход за
экран по координате, но не частичный отрезок; y+len<=256 — обязанность
вызывающего.
Оба span'а: в safe добавлена проверка len==0 → return (иначе B=0 по
конвенции акселератора рисует «256»). В fast (GFX_NOCHECK) проверка
вырезается — поведение прежнее (256 точек), задокументировано в шапках.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- удалён глобальный скретч акселератора (_gfx_acc256.c); fill-сегменты
(_gfx_hfill256/_gfx_vfill256) принимают аргументы в регистрах HL/C/B/E,
вызываются только из asm raw-примитивов
- новый _gfx_rectfill256: заливка прямоугольника через Horizontal_Size
- raw-примитивы (plot/read/hspan/vspan/clear) переписаны под новый ABI
- graphics.h: typedef color_t (uint8_t) для всех public color-функций
- sprinter-cc: флаг --safe (линковка *_safe.lib при наличии)
- CLAUDE.md/mame_interactive: авто-прогон тестов в MAME
- gfx_demo: демонстрация rectfill
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 14:32:21 +03:00
582 changed files with 56717 additions and 907 deletions
хранят по контейнерному формату (§1) множество мелких чанков (десятки—сотни
байт каждый).
Что подтверждено:
- Наборы `shadow.dat`/`kid.dat` и `fat.dat`/`vizier.dat` содержат **побайтово
идентичные фрагменты** данных в начале файла — это ожидаемо: "Тень" (Shadow)
визуально копирует анимацию Кида, а "Толстый страж" (Fat guard, пасхалка)
переиспользует модель Визиря. Подтверждает, что персонажи одного "типа
тела" используют общий набор геометрии/анимации.
- Отдельные чанки *не* имеют очевидного унифицированного заголовка
(высота/ширина/палитра) фиксированного размера — попытка интерпретировать
первые байты чанка как `{height:u16, width:u16, flags:u16}` не подтвердилась
на реальных данных (получаются нереалистичные размеры для маленьких чанков).
Вероятно, как и в Apple II версии (см. `FRAMEDEF.S`/`SEQTABLE.S`), геометрия
кадра (ширина, высота, точка привязки) хранится **отдельно от самих
пиксельных данных** — в таблицах внутри `PRINCE.EXE`, а не в `.DAT`-чанке.
Сам чанк, вероятно, содержит только упакованные пиксельные данные
(RLE/дельта-упаковка, по аналогии с `UNPACK.S` в Apple II исходниках).
- Точный алгоритм упаковки пикселей **не восстановлен** в рамках этого
анализа по сырым байтам — байт-в-байт разбор распаковщика без
дизассемблирования `PRINCE.EXE` надёжно не сделать. **Но для практических
целей это не требуется**: см. §7 — в SDLPoP уже есть тот же самый набор
изображений в готовом, распакованном виде (PNG), которым можно пользоваться
напрямую как источником ассетов, не реализуя свой декодер `.DAT`-пикселей.
Писать собственный декодер имеет смысл только если понадобится читать
оригинальные `.DAT` "на лету" (например, для точной сверки контента именно
нашей копии игры) — тогда ориентир — исходник SDLPoP (`src/seg009.c`).
---
## 4. Звук — уверенность: высокая (по структуре), низкая (по деталям кодека)
Обнаружено 4 параллельных набора звуковых ресурсов под разные звуковые
устройства DOS-эпохи — типично для игр начала 1990-х с "звуковым меню":
| Файл | Устройство | Формат чанка |
|---|---|---|
| `midisnd1.dat`, `midisnd2.dat` | General MIDI / MPU-401 | каждый чанк = 2-байтовый LE-префикс длины + встроенный Standard MIDI File (`MThd`...`MTrk`...) |
| `mt32snd1.dat`, `mt32snd2.dat` | Roland MT-32/CM-32L | тот же формат: префикс длины + `MThd`/`MTrk`, с MT-32-специфичными SysEx (видны строки `MT-32.mff`, текстовые мета-события вроде `"The Princess awaits"`) |
| `prince.dat` | (аналогично MIDI) | отдельный крупный музыкальный ресурс, тот же MIDI-контейнер — вероятно, финальная тема |
| `digisnd1/2/3.dat` | Covox / Disney Sound Source / Sound Blaster (оцифрованный звук) | чанк начинается с нескольких служебных байт, среди которых слово `0x2AF8` = 11000 — похоже на частоту дискретизации 11 кГц; далее — сырые 8-битные PCM-сэмплы (значения кластеризуются вокруг ~0x7A–0x90, типично для беззнакового 8-бит аудио, смещённого к середине шкалы) |
| `ibm_snd1.dat`, `ibm_snd2.dat` | PC Speaker | чанк — последовательность троек байт похожих на (длительность, делитель_частоты) — простой формат "бипера", отличный от MIDI |
Подтверждено разбором первых чанков в каждом файле (см. байтовые дампы,
проверялись скриптом). Точная семантика полей внутри `digisnd`/`ibm_snd`
(разрядность, порядок байт служебного заголовка) не выведена до конца — при
реализации порта достаточно распознавания по типу файла и (для MIDI-семейства)
можно напрямую воспроизводить встроенный Standard MIDI File, пропустив
2-байтовый префикс длины.
---
## 5. Готовые распакованные ассеты в SDLPoP (`data/`) — практический источник для порта
Репозиторий github.com/NagyD/SDLPoP содержит папку `data/`, где, помимо
самих `.DAT`-контейнеров, каждый ресурс **продублирован в виде отдельно
распакованного файла**, названного по его `id` из таблицы оглавления (§1.2).
Проверено через GitHub API (`api.github.com/repos/NagyD/SDLPoP/contents/...`):
| `DIGISND1.DAT`, `MIDISND2.DAT` и др. | сырые `.DAT` | размер **не совпадает** с нашими локальными файлами (48545 vs 50101, 18408 vs 18958) — другой релиз/сборка игры |
| `GUARD/res751.png` … `res784.png` | готовые PNG, по одному на кадр анимации, имя = `res<id>.png` | id-диапазон (751-784) точно совпадает с нашим разбором `guard.dat` |
| `LEVELS/res2000.bin` … `res2015.bin` | сырые дампы уровней по 2304-2305 байт | id совпадает с `levels.dat`; содержимое `res2001.bin`**сверено побайтово** с нашим id=2001 — тайловые данные совпадают |
| `KID/`, `PRINCE/`, `SHADOW/`, `SKEL/`, `VIZIER/`, `FAT/`, `TITLE/`, `VDUNGEON/`, `PV/`, `IBM_SND1/`, `IBM_SND2/`, `font/`, `music/` | аналогичные наборы для остальных ресурсов | не проверялись по отдельности, но структура (папка на каждый `.dat`, файлы `res<id>.ext`) наблюдается одинаково |
**Вывод:** это данные из немного **другого релиза DOS-версии**, чем те, что
лежат у нас в `MSDOS/` (см. расхождение в размере `digisnd`/`midisnd`), но
формат контейнера и нумерация `id` — те же самые. Практически это значит:
1. Для получения играбельных PNG-спрайтов и VGA-фонов **не нужно
реализовывать декодер сжатия пикселей** — можно взять готовые файлы
`data/<ИМЯ>/res<id>.png` напрямую как исходный материал для конвертации
под видеорежим ZX Sprinter (в т.ч. `VPALACE`/`VDUNGEON` — уже
256-цветный VGA-арт, что прямо отвечает на вопрос про полноцветность).
2. Если в проекте важно использовать именно ту версию контента, что в наших
`MSDOS/*.dat` (а не версию из SDLPoP) — распаковку своих файлов всё же
придётся делать (кодек пикселей по-прежнему не восстановлен для сырых
`.DAT`, см. §3), либо принять решение работать с версией SDLPoP как
мастер-источником ассетов вместо своей.
---
## 6. Служебные не-ресурсные файлы
-`config.dat`, `setup.dat` — 28 байт, не являются ресурсными контейнерами
(не проходят проверку §1.1 — "размер" получается больше самого файла).
Скорее всего простые бинарные структуры настроек (звук/видеорежим,
выбранный на этапе `SETUP.EXE`/`INSTALL.EXE`), не связаны с игровым
-`PRINCE.EXE` / `PRINCE.REM` — почти идентичны (отличие в единичных байтах
в районе смещения ~0x4ED0), похоже на кряк/патч одного байта проверки —
не относится к формату ресурсов.
-`old-games.nfo` — ASCII-арт NFO релиз-группы (old-games.ru), не игровые
данные.
---
## 7. Итоговая таблица уверенности
| Раздел | Уверенность | Как подтверждено |
|---|---|---|
| Контейнер `.DAT` (заголовок + таблица) | Высокая | Проверено скриптом на всех 28 файлах, инвариант offset+size выполняется без исключений; независимо подтверждено именованием `res<id>.*` в SDLPoP `data/` |
| ID-пространства ресурсов | Средняя-высокая | Наблюдение по диапазонам + сверка с `res<id>` именами файлов SDLPoP и побайтовым содержимым `res2001.bin` |
| Формат уровня = формату Apple II | Высокая (по размеру и содержимому), служебный блок id=2000 — открытый вопрос | Совпадение размера (2304 vs 2305), тайловые байты сходятся с `res2001.bin` из SDLPoP |
| Формат изображений/спрайтов (сырой `.DAT`) | Низкая-средняя | Контейнер подтверждён, кодек пикселей — нет; но практически закрыто наличием готовых PNG в SDLPoP `data/` (§5) |
| Формат звука (тип контейнера) | Высокая для MIDI-семейств, средняя для digisnd/ibm_snd | Явные MIDI-сигнатуры `MThd`/`MTrk` видны в байтах |
**Рекомендация для дальнейшей работы:** для получения арт-ассетов (спрайты,
фоны, палитры) — использовать готовые распакованные файлы из
`github.com/NagyD/SDLPoP/tree/master/data` (§5), это быстрее и надёжнее
самостоятельной реализации декодера. Декодер сырого `.DAT`-формата
изображений и точную семантику служебных полей `digisnd`/`ibm_snd`
(§3, §4) стоит восстанавливать только если понадобится читать именно нашу
локальную копию `MSDOS/*.dat` "как есть" — тогда ориентир прежний: исходник
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.