PoP: скелет уровня 3, цвета стражей, окклюзия соперника; кэш соседних комнат
Скелет (L3-SKEL, ассеты + механика): - pop_pack_guard.py получил параметр набора (GUARD/SKEL): атлас скелета poc/res/skel/g0..g3.atl (28 кадров), палитра — из его res750.pal (на ур. 3 curr_guard_color = 0, оригинал палитру не подменяет); - pop_guard_load выбирает набор по tbl_guard_type и перезагружается ПРИ СМЕНЕ УРОВНЯ (load_lev_spr, seg000:1092) — без этого скелет рисовался атласом стража и был невидим; - load_frame: charid_4_skeleton идёт по таблице стража (seg006:529), тень — только в кадрах 150..189. Пока ветка была одна (charid_2_guard), скелет получал image из таблицы Кида (180 при 28 спрайтах) и не рисовался; - check_skel (seg002:1042), ветка charid_4 в enter_guard, возрождение в комнате 3 при падении (seg002:252), autocontrol_skeleton; - leveldoor_open (seg007:456) — новый флаг, сбрасывается стартом уровня. Цвета стражей (BUG-GUARD-COLOR-1, закрыт): - все 7 палитр res10.bin -> pop_guard_pal.h, заливка 16 слотов по guards_color комнаты перед отрисовкой (set_chtab_palette, seg003:257). Проверено в MAME: ур. 2 комн. 11 = цвет 1, комн. 7 = цвет 3, полоса HP меняется вместе со стражем. Грабля: gfx_pal_load отдаёт указатель в BIOS, а тот читает только #4000-#BFFF — таблицу из банка копируем в стек. Кэш соседних комнат (BUG-SWORD-GHOST-1, закрыт): - pop_map кэширует fg соседей слева/справа ЦЕЛИКОМ и резолвит col -10..19. Было -2..11, дальше мнимая стена: луч видимости упирался в неё (страж после follow_guard в col 12), Кид прятал меч посреди боя и не мог достать обратно. +48 байт W2. Окклюзия соперника: - pop_fore_over_char получил проход other_overlay_tile (порядок midtable, seg008:1B06) и расширение перебора объединённым прямоугольником «персонаж + клинок + брызги» — падающий скелет больше не рисуется поверх кладки и верхней грани пола; - клип полем 192 строк (reset_obj_clip, seg006:0507) для спрайта, клинка (общий pop_sword_draw) и брызг — спрайт не залезает на полосу HP; - ROOMNAV после смерти Кида делает честный pop_start_level: телепорт «оживлял» мёртвого мимо старта уровня, оставляя живого скелета рядом с вернувшейся кучей костей. Ассеты чомпера (под L3-CHOMP): весь набор кадров в атласе явным списком (101-105 низ, 111-113 верх, 106-110 фронт, 114-123 кровь mono-силуэтом) — render_room анимированные тайлы пропускает, и в атласе не было ни одного. Число EMM-страниц не изменилось. Тесты: tests-host все 5 наборов зелёные, t_char вырос до 65 проверок (резолв колонок за краем комнаты, возрождение скелета); в testkit добавлен гард «код наехал на данные» (DATA_LOC). Доски: TASKS.md разнесён на TASKS_OPEN/TASKS_CLOSED, закрытые баги с разбором корней — в bug_closed.md; заведены DRAW-CHAR (отрисовка одна на всех Char, как физика после GUARD-PHYS) и L3-COLOR (зелёная кладка уровня 3: level_var_palettes = ресурс 20, есть в MSDOS/PRINCE.DAT). В roomtest.c временно оставлен автостоп на падении соперника (отладка падений скелета) — помечен ВРЕМЕННО. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,621 @@
|
||||
# roomtest — ЗАКРЫТЫЕ задачи (архив досок, обновлено 2026-08-07)
|
||||
|
||||
Сделанное — с протоколами замеров, граблями и причинами решений. Файл
|
||||
существует не ради истории: половина записей ниже — это ЧИСЛА (сколько тактов
|
||||
стоил heal, сколько байт теряет клавиатура, почему `static inline` дорог) и
|
||||
перечень того, что делать НЕЛЬЗЯ, потому что уже пробовали.
|
||||
|
||||
Открытые задачи — [`TASKS_OPEN.md`](TASKS_OPEN.md); открытые баги —
|
||||
[`bug_list.md`](bug_list.md), закрытые с разбором корней —
|
||||
[`bug_closed.md`](bug_closed.md).
|
||||
|
||||
---
|
||||
|
||||
## Приёмка уровней
|
||||
|
||||
<a id="l1-pass"></a>
|
||||
### L1-PASS. Сквозное прохождение уровня 1 — **ЗАКРЫТ 2026-08-07**
|
||||
|
||||
> **Smoke 2026-08-05 (пользователь): успешный.** От старта до выхода с
|
||||
> уровня одним заходом — меч подобран, **оба стража побеждены в честном бою**
|
||||
> (без читов), выход отработал корректно. Ценность прогона в том, что он снял
|
||||
> главные риски этапа 1 разом — боёвка, предметы, переход с уровня — и стал
|
||||
> регресс-базой для уровня 2.
|
||||
>
|
||||
> **Полный обход комнат 2026-08-07 (пользователь): крупных багов нет.** Этим
|
||||
> же прогоном закрыта [таблица обхода 24 комнат](bug_closed.md#обход-всех-24-комнат-уровня-1)
|
||||
> и [чек-листы ручной перепроверки фиксов](bug_closed.md#ручная-перепроверка-2026-08-03)
|
||||
> — обе уехали в `bug_closed.md`.
|
||||
|
||||
Приёмка этапа 1 и одновременно регресс-база для уровня 2: от старта до двери
|
||||
уровня одним заходом — подбор меча, страж, кнопки/ворота, пики, loose-полы,
|
||||
зелье, падения. Точки, где смотрели внимательно, — закрытая косметика
|
||||
окклюзии (потолок при прыжке вверх, шов при анимации решётки, грани дальней
|
||||
колонны) и подъём на тайл-кнопку (оговорка к BUG-3 в `bug_closed.md`).
|
||||
|
||||
<a id="l2-pass"></a>
|
||||
### L2-PASS. Приёмка уровня 2 — **ЗАКРЫТ 2026-08-07**
|
||||
|
||||
> **Smoke 2026-08-05 (пользователь): уровень 2 пройден.** Из smoke пришли
|
||||
> BUG-LOOSE-3 и BUG-GUARD-DEAF-1 — оба закрыты (`bug_closed.md`). Следом
|
||||
> прогнан smoke уровня 3.
|
||||
>
|
||||
> **Полный обход комнат 2026-08-07 (пользователь): крупных багов нет.**
|
||||
> Открытыми с этого прогона остались три записи в
|
||||
> [`bug_list.md`](bug_list.md): [BUG-GUARD-COLOR-1](bug_list.md#bug-guard-color-1)
|
||||
> (страж всегда одного цвета), BUG-GUARD-SPLASH-1 (нет брызг при попадании по
|
||||
> стражу — **закрыт 2026-08-07**, разбор в
|
||||
> [`bug_closed.md`](bug_closed.md#bug-guard-splash-1)) и
|
||||
> [BUG-CHEAT-FIGHT-1](bug_list.md#bug-cheat-fight-1) (наш чит `+`/`−` в бою
|
||||
> отнимает управление). Ни одна играть не мешает.
|
||||
|
||||
Ниже — **карта содержимого уровня, снятая прямо с `res2002.bin`**. Она
|
||||
осталась в архиве не как отчёт, а как справочник для повторных прогонов и
|
||||
для сравнения с SDLPoP: если механика в таблице есть, а в игре не сработала —
|
||||
это баг, а не «так задумано».
|
||||
|
||||
**Стражи — 5, в комнатах 4, 7, 11, 15, 24** (skill 1/2/1/1/3, цвета
|
||||
1/3/1/1/6 — цвет мы пока игнорируем, атлас один, см.
|
||||
[BUG-GUARD-COLOR-1](bug_list.md#bug-guard-color-1)). Заметить: страж
|
||||
комнаты 24 со skill 3 — первый по-настоящему опасный.
|
||||
|
||||
**Кнопки и что они открывают** (декодировано из LINKLOC/LINKMAP):
|
||||
|
||||
| Кнопка | Тип | Цель |
|
||||
|--------|-----|------|
|
||||
| к.9 @ряд1,кол1 | RAISE | **дверь уровня** к.23 @1,3 — то есть выход |
|
||||
| к.11 @1,1 | RAISE | ворота к.18 @0,9 |
|
||||
| к.18 @0,7 | RAISE | ворота к.7 @0,9 **и** к.18 @0,9 (две сразу) |
|
||||
| к.18 @0,2 | DROP | закрывает ворота к.7 @0,9 |
|
||||
| к.13 @1,4 | DROP | закрывает ворота к.13 @1,5 |
|
||||
|
||||
**Ловушки и предметы по комнатам:**
|
||||
|
||||
```
|
||||
к. 3 loose@2,2 зелье@2,5 (здоровье)
|
||||
к. 4 loose@1,7 loose@1,8 + СТРАЖ
|
||||
к. 5 дверь уровня @1,2-3 — ВХОД (захлопывается за спиной)
|
||||
к. 6 пики@1,3 loose@1,5 зелье@2,7 (здоровье)
|
||||
к. 7 ворота@0,9 пики@2,7 + СТРАЖ
|
||||
к. 8 зелье@2,2 (здоровье)
|
||||
к. 9 кнопка RAISE@1,1 loose@2,6
|
||||
к.10 пики@2,2
|
||||
к.11 кнопка RAISE@1,1 + СТРАЖ
|
||||
к.12 зелье@2,6 (здоровье) loose@2,8
|
||||
к.13 зелье@1,3 (ВРЕДНОЕ, −1 HP) кнопка DROP@1,4 ворота@1,5 зелье@1,8
|
||||
к.15 + СТРАЖ
|
||||
к.18 кнопка DROP@0,2 loose@0,4 кнопка RAISE@0,7 ворота@0,9 зелье@2,6
|
||||
к.19 пики@1,4
|
||||
к.20 ЗЕЛЬЕ@1,2 — БОЛЬШАЯ СКЛЯНКА (+1 к потолку HP) пики@2,6 пики@2,7
|
||||
к.23 дверь уровня @1,3-4 — ВЫХОД
|
||||
к.24 + СТРАЖ (skill 3)
|
||||
```
|
||||
|
||||
Отдельно проверялось то, что появилось именно на этом уровне:
|
||||
- **большая склянка** (к.20) — потолок HP становится 4, индикатор рисует
|
||||
четыре деления, и это HP **переносится на уровень 3**;
|
||||
- **меч уже в руках** с самого старта (`have_sword = level >= 2`);
|
||||
- **выход через дверь к.23** вживую (кнопка в к.9);
|
||||
- **смерть/респавн** возвращают на уровень 2, а не на 1.
|
||||
|
||||
---
|
||||
|
||||
## Уровни и механика
|
||||
|
||||
<a id="l3-chkp"></a>
|
||||
### L3-CHKP. Чекпойнт уровня 3 — **СДЕЛАНО 2026-08-06** (коммит 0cd6b2d)
|
||||
|
||||
`level3_set_chkp` (seg002:0665): флаг взводится, когда Кид уходит **ВЛЕВО ИЗ
|
||||
комнаты 7**; `do_startpos` (seg003:141) по нему подменяет старт на комнату 2,
|
||||
тайлпос 6, направление влево и снимает loose-плиту (комната 7, колонка 4,
|
||||
ряд 0). Константы — в `pop_tune.h` (`POP_CHKP_*`), механика `hitp_beg_lev`
|
||||
была сделана раньше в L2.
|
||||
|
||||
**Тонкость, на которой сначала ошиблись:** `level3_set_chkp` вызван из
|
||||
`leave_room` ДО `goto_other_room`, поэтому `Char.room == 7` — это комната, ИЗ
|
||||
которой уходят, а не в которую входят. Поймал пользователь прогоном в
|
||||
SDLPoP: смерть В комнате 7 вернула его в стартовую 9, а плита осталась цела.
|
||||
|
||||
**Проверено в MAME:** вход в 7 флаг не ставит, уход влево — ставит; респавн в
|
||||
комнате 2; после обычной смерти (без чекпойнта) плиты восстанавливаются как
|
||||
раньше. Вживую в игре (не читом) — потрогать на приёмке уровня 3
|
||||
([L3-PASS](TASKS_OPEN.md#l3-pass)).
|
||||
|
||||
### L2. Переход на уровень 2 и его игра — **МАШИНЕРИЯ СДЕЛАНА 2026-08-04**
|
||||
|
||||
Порт `levels_plan.md` §2 (шаг 1). Что появилось:
|
||||
|
||||
- **Номер уровня стал состоянием.** `pop_current_level` (порт
|
||||
`current_level`) в `pop_level.c`; `pop_next_level` больше не флаг, а
|
||||
НОМЕР — `END_LEVEL` его инкрементит (как `++next_level`, seg006:662), а
|
||||
главный цикл срабатывает по расхождению `pop_next_level !=
|
||||
pop_current_level` (порт `play_level_2`, seg003:0386).
|
||||
- **Загрузка по номеру** — `pop_level_load_num(n)`: `LEVELS\res20NN.bin`
|
||||
(fallback `a:\`), старая EMM-страница отпускается ТОЛЬКО после успешной
|
||||
загрузки новой (нет файла — играем дальше на текущем). На диск кладутся
|
||||
все 15 уровней (34 КБ).
|
||||
- **Потабличные различия** (`data.h:840..848`) — таблицы по 16 в
|
||||
`pop_level.c`: `tbl_entry_pose` (поза входа), `tbl_guard_hp` (HP стража,
|
||||
ушло из хардкода `3`), `tbl_guard_type` (**−1 = стражей нет**, иначе на
|
||||
14/15 они полезли бы из данных комнат), `tbl_level_type` (тайлсет —
|
||||
пока только читается).
|
||||
- **`find_start_level_door`** (seg003:02E6) — на уровне 2 это НЕ косметика:
|
||||
стартовый тайл (комната 5, ряд 1, колонка 3) — правая половина двери
|
||||
уровня, и без `modif = 43` + `add_trob(...,3)` Кид материализуется внутри
|
||||
глухой створки. Тип 3 = «быстро закрыть»: дверь захлопывается за спиной
|
||||
за три кадра, как в оригинале.
|
||||
- **HP через уровень** — `hitp_beg_lev` (seg003): рестарт уровня
|
||||
откатывает HP к нему, пройденный уровень подтягивает его к `hitp_max`.
|
||||
Заодно реализована **большая склянка** (`add_life`, тип зелья 2: +1 к
|
||||
ПОТОЛКУ HP до 10) — на уровне 2 она есть, комната 20.
|
||||
- **Меч** — `have_sword = level >= 2` (play_level, seg003:106), а не
|
||||
жёсткий ноль.
|
||||
- **Чит Shift+L** (seg000:698) — `pop_next_level = pop_current_level + 1`.
|
||||
NB: `L` без Shift занят отладочным «осторожным шагом вправо»
|
||||
(`pop_ctrl` `KBD_DBG_STEPR`); с Shift шаг тоже пройдёт, но уровень тут же
|
||||
сменится — на практике не мешает.
|
||||
|
||||
**Проверено в MAME (2026-08-04):** старт уровня 1 → Shift+L → уровень 2
|
||||
рисуется правильно (комната 5, большие колонны — новые для нас тайлы 8/9 —
|
||||
на месте, дверь захлопнута, HP 3) → влево в комнату 4, страж на месте →
|
||||
Shift+L → уровень 3 (комната 9). То есть цепочка загрузок работает
|
||||
повторно, а не только один раз.
|
||||
|
||||
Контент уровня 2 (сквозное прохождение, выход через дверь) закрыт отдельно —
|
||||
[L2-PASS](#l2-pass). Цвет стража из данных остался открытым багом —
|
||||
[BUG-GUARD-COLOR-1](bug_list.md#bug-guard-color-1).
|
||||
|
||||
### L1-START. Старт по данным уровня — **СДЕЛАНО 2026-08-01**
|
||||
|
||||
Старт и оба рестарта (смерть, выпадение из уровня) сведены в один
|
||||
`pop_start_level()` — порт `start_level` + `do_startpos` + `set_start_pos`
|
||||
(seg003): комната/тайл/направление берутся из `pop_level_start_*`, направление
|
||||
инвертируется (`~start_dir`), поза входа — из `tbl_entry_pose`. У уровня 1
|
||||
это «падение внутрь» плюс нажатие кнопки room5(0,2) — то самое, что
|
||||
захлопывает решётку за спиной. Проверено: старт даёт room 1, col 0, падение
|
||||
на row 1 — как по данным уровня.
|
||||
|
||||
`#define ROOMNAV` оставлен ВКЛЮЧЁННЫМ осознанно: это наш чит, которого в
|
||||
оригинале не было, — как и `S` (выдать меч), `K`, `I`. Все они со временем
|
||||
съедутся в общий блок читов, разрешаемый в настройках (решение 2026-08-01).
|
||||
|
||||
### L1-EXIT. Выход с уровня (дверь уровня) — **СДЕЛАНО 2026-08-01**
|
||||
|
||||
Портированы: ветка двери уровня из `up_pressed` + `go_up_leveldoor`
|
||||
(seg005:0482/0574) — в `pop_leveldoor_enter()` (`pop_map.c`, тайлы и
|
||||
геометрия) и `up_pressed()` (`pop_ctrl.c`, только последовательность);
|
||||
опкод `0xF1 END_LEVEL` в `play_seq` теперь инкрементит `pop_next_level`
|
||||
(порт `next_level`), а главный цикл по нему перезапускает уровень — ровно
|
||||
та точка, куда `levels_plan.md` §2.2 подключила загрузку уровня 2.
|
||||
|
||||
Открытость двери проверяем по `modifier >= 42` (ветка `fix_exit_door`), а не
|
||||
по ванильному `leveldoor_open`: с ванильным условием можно войти в ещё
|
||||
ползущую створку.
|
||||
|
||||
**Грабли, которые стоили отдельного разбора:** `go_up_leveldoor` сначала
|
||||
писал `Char.x`/`Char.direction`, и оба присваивания молча терялись — окно
|
||||
`Char` вокруг диспетчера возвращает назад ТОЛЬКО `curr_seq` и `sword`
|
||||
(`pop_savekid_state`). Направление оставалось «вправо», а все `DX`
|
||||
последовательности seq_70 отрицательные, поэтому Кид уходил ИЗ проёма влево.
|
||||
Вывод на будущее: геометрию персонажа в этом порте меняет `pop_map` (пишет в
|
||||
`Kid`), а не диспетчер.
|
||||
|
||||
✅ Косметика тоже закрыта: BUG-DOOR-CLIP (обрезка силуэта правым косяком
|
||||
проёма) — недоставало второй половины `clip_char` (`obj_clip_right`) и
|
||||
обрезки СПРАВА у колоночного блита. В libbgi добавлен
|
||||
`gfx_blit_cols_part_w(..., maxw)`; разбор — в [`bug_closed.md`](bug_closed.md).
|
||||
|
||||
### L1-TRIAGE. Ревизия багов — **ЗАКРЫТА** (часть 1 — 2026-08-01, хвост — 2026-08-07)
|
||||
|
||||
✅ **Три Critical'а прогнаны в MAME и закрыты** (протокол с числами — в
|
||||
[`bug_closed.md`](bug_closed.md), раздел «Проверено в MAME 2026-08-01»):
|
||||
BUG-1 (провал на row 1 при переходе через открытые ворота) и BUG-2
|
||||
(ping-pong при возврате) **не воспроизводятся**, BUG-3 (окклюзия climb-up на
|
||||
кнопке) закрыт фиксом `tile_code_drawn` от 2026-07-28. Заодно снят неверный
|
||||
диагноз BUG-1: репроекция Y при БОКОВОМ переходе — не наш пробел, а точное
|
||||
поведение `goto_other_room` (`SDLPoP/src/seg002.c:390` меняет только `x`).
|
||||
Список разделён на [`bug_list.md`](bug_list.md) (открытое) и
|
||||
[`bug_closed.md`](bug_closed.md) (закрытое + разбор корней).
|
||||
|
||||
✅ **Косметика окклюзии тоже закрыта** — BUG-CEIL-1/2/3 и BUG-OCCL-1 были
|
||||
починены кодом ещё в июле, а записи никто не снял: `ceil_over_kid_tile`
|
||||
(`pop_bg.c:1275`), `pop_ceil_modif` + `pop_ceil_shake_draw`/`_bake_empty`
|
||||
(`:547,815,827`), `bar` с `POP_YOFF+3` в `pop_room_redraw_seam_left` (`:793`),
|
||||
разделение слоёв по `add_backtable` vs `ptr_add_table` в `overlay_mid_tile`
|
||||
(`:1367`). Разбор — в [`bug_closed.md`](bug_closed.md).
|
||||
|
||||
✅ **Хвост закрыт 2026-08-07:** таблица обхода 24 комнат уровня 1 — прогоном
|
||||
всех комнат уровней 1 и 2 (крупных багов нет), см.
|
||||
[`bug_closed.md`](bug_closed.md#обход-всех-24-комнат-уровня-1).
|
||||
|
||||
---
|
||||
|
||||
## Отрисовка и память
|
||||
|
||||
<a id="clip-1"></a>
|
||||
### CLIP-1. Аудит блитов: где клип не нужен — **СДЕЛАНО 2026-08-01**
|
||||
|
||||
> **Итог.** Heal Кида и стража переведены на выбор ядра по тому же тесту,
|
||||
> что давно стоит у блитов. Замер в MAME (счётчики `totalcycles` на входах
|
||||
> `kid_heal` и `kid_tick`, то есть вся группа heal за кадр; комната 1, Кид
|
||||
> стоит, стража нет):
|
||||
>
|
||||
> | путь | тактов на кадр | кадров в выборке |
|
||||
> |------|----------------|------------------|
|
||||
> | клипающее ядро (как было) | **26 200** | 149 |
|
||||
> | noclip (стало) | **15 848** | 239 |
|
||||
>
|
||||
> **−10 352 такта на кадр, то есть −39.5 % с группы heal** (≈0.49 мс при
|
||||
> ~21 МГц). A/B честный: оба замера сняты в ОДНОМ прогоне, вторая половина —
|
||||
> с пропатченным в памяти условием (`jr nz` → `jr` в `pop_heal_fast`), то
|
||||
> есть на той же геометрии и в той же сцене.
|
||||
>
|
||||
> **Размер: −362 Б суммарно** (не плюс!): `_CODE` 25 289 → 25 306 (+17),
|
||||
> BANK2 13 792 → **13 676** (−116, свободно стало 2708 Б — это тот самый
|
||||
> тесный банк из рисков `levels_plan.md` §5), BANK3 6512 → 6249 (−263),
|
||||
> BANK4 без изменений.
|
||||
>
|
||||
> **Грабли, стоившие двух пересборок** (вынесено в память
|
||||
> `sdcc-static-inline-double-cost`): первым заходом хелпер был `static
|
||||
> inline` в `pop_bg.h` — и SDCC 4.5 И встроил его тело (181 Б) в каждое
|
||||
> место вызова, И оставил отдельную копию в КАЖДОМ TU, который видит
|
||||
> заголовок. `pop_guard_heal` раздулся с ~60 до 663 Б, итого +1091 Б в
|
||||
> `_CODE` и +636 Б в банке стража. Лечится обычной функцией в одном
|
||||
> резидентном модуле (`pop_draw.c`, W1 — из банков это прямой `call` без
|
||||
> трамплина, как у `pop_sword_draw`).
|
||||
>
|
||||
> **Проверено визуально:** обычная ходьба, прыжок, спуск и позиция «за
|
||||
> решёткой шва» (straddle — там как раз работает клипающий фолбэк) —
|
||||
> артефактов и следов нет.
|
||||
|
||||
**Ниже — исходная постановка задачи (что и почему смотрели).**
|
||||
|
||||
**Зачем было сейчас.** Кадр занят на ~86 %; подготовка клипающего варианта
|
||||
стоит ~5.6 К тактов на вызов, а общее ядро против линейного — 13 288 против
|
||||
4 617 тактов на спрайт 32×3 (`libbgi/include/gfx.h`). Это самая дешёвая
|
||||
оставшаяся оптимизация: не переписывание логики, а выбор ядра.
|
||||
|
||||
**Что чинили:**
|
||||
|
||||
- ✅ `kid_heal()` и `pop_guard_heal()` звали `gfx_heal` — **всегда с
|
||||
клипом**, хотя `gfx_heal_noclip` существует и `heal_off` в `pop_bg.c` им
|
||||
уже пользовался. Это heal 2–4 прямоугольников КАЖДЫЙ кадр; замер общего
|
||||
ядра — 11 658 тактов на heal 22×22. Теперь все три идут через общий
|
||||
`pop_heal_fast` (`pop_draw.c` + `_pop_draw.h`).
|
||||
- ⛔ `pop_room_clip_borders()` (`pop_bg.c`) — `gfx_heal(0,0,320,…)`:
|
||||
**оставлен клипающим осознанно**. Полоса шириной 320 не лезет в 8-битный
|
||||
параметр noclip-ядра, а бить её на два куска по 160 нет смысла: гейт
|
||||
`border_dirty` пускает туда только в кадрах падения, и выигрыш подготовки
|
||||
тонет в цене самих 320×28 пикселей. Причина записана прямо в коде, чтобы
|
||||
не «оптимизировать» повторно.
|
||||
|
||||
**Что обязано остаться с клипом** (зафиксировано комментариями в коде):
|
||||
- кромочные тайлы фона (`blit_b` в `pop_bg.c`) — тайл у края экрана режется
|
||||
по построению;
|
||||
- спрайты при straddle (`kid_render_dx = ∓140`, комната Кида ≠ отрисованной)
|
||||
и при падении ниже поля — фолбэки в `kid_draw`/`pop_guard_draw`;
|
||||
- борта поля (`pop_room_clip_borders`) — см. выше.
|
||||
|
||||
**Что уже было правильно** (шаблон, который и распространили): блиты Кида,
|
||||
стража, клинка и брызг спрашивают `pop_onscreen_cols()` и уходят в
|
||||
`gfx_blit_cols_part_noclip`. Отдельный случай — `pop_kid_img_blit`: noclip
|
||||
БЕЗ проверки, потому что единственный вызывающий (полоса HP) рисует по
|
||||
фиксированным координатам; это тоже помечено в коде.
|
||||
|
||||
<a id="mem-bank5"></a>
|
||||
### MEM-BANK5. Разгрузка W1/W2 новым банком кода — **СДЕЛАНО 2026-08-05**
|
||||
|
||||
Итог: `pop_ctrl.c` уехал в банк 5, куча 180 Б → **2298 Б** (после снижения
|
||||
`--max-allocs`, см. [BUILD-FAST](#build-fast), — 2751 Б).
|
||||
|
||||
Вопрос 2026-08-04: «надо делать новый банк?». **Да, и он лечит именно то,
|
||||
что жмёт.** В нашей раскладке (`MEMORY=huge`, small-вариант) CODE и DATA
|
||||
живут в ОДНОМ 32-КБ пространстве W1+W2 — карта сборки на тот момент:
|
||||
|
||||
```
|
||||
_CODE 0x4100..0xAA00 26880 Б
|
||||
_DATA 0xAAD0..0xB9B0 3808 Б
|
||||
_BSS 0xB9B8..0xBADA 290 Б
|
||||
куча 0xBADA..0xBB00 38 Б ← упёрлись сюда, добавляя отладку
|
||||
стек 0xBB00..0xC000 1280 Б
|
||||
```
|
||||
|
||||
Поэтому **каждый килобайт кода, уехавший в банк, становится килобайтом,
|
||||
доступным данным**. Отдельного «дефицита W2» у нас нет — дефицит один.
|
||||
(38 байт кучи не опасны сами по себе: malloc'ом мы не пользуемся, страницы
|
||||
берутся через `mem_alloc_block`. Опасно то, что следующая структура
|
||||
данных упрётся в стек молча.)
|
||||
|
||||
Резидентный код по модулям (из `.sprinter-cc-roomtest/*.rel`):
|
||||
|
||||
| модуль | _CODE | как часто зовётся | в банк? |
|
||||
|--------|------:|-------------------|---------|
|
||||
| `pop_kid.c` | 6287 | `load_frame`/`play_seq` — 2×/кадр (Кид + страж) | частично: холодная половина (загрузка страниц спрайтов, `pop_kid_load`) — да; движок кадров — нет |
|
||||
| `roomtest.c` | 3893 | main-loop | нет (точка входа, зовёт всех) |
|
||||
| `pop_trob.c` | 2526 | `do_trobs` 1×/кадр, но `pop_trob_modif` — горячий аксессор | кандидат, если вынести аксессор в резидент |
|
||||
| `pop_level.c`| 2292 | `pop_level_tile` — из `pop_bg` (банк 2) на каждый тайл | **нет**: банк→банк на каждый тайл убьёт отрисовку |
|
||||
| `pop_ctrl.c` | 2189 | `user_control` 1×/кадр | **взят** — дёшево и безопасно |
|
||||
| `pop_guard.c`| 923 | 1×/кадр | нет смысла |
|
||||
|
||||
Критерий кандидата — **не размер, а частота вызова и отсутствие горячих
|
||||
банк→банк переходов**; `pop_level` показывает, что большой холодный на вид
|
||||
модуль может быть горячим аксессором. Следующий шаг (расщепление
|
||||
`pop_kid.c`) — в [`TASKS_OPEN.md`](TASKS_OPEN.md#mem-next), брать по факту
|
||||
нехватки места.
|
||||
|
||||
<a id="build-fast"></a>
|
||||
### BUILD-FAST. Сборка 10 минут → 1:48 — **СДЕЛАНО 2026-08-06** (коммит 0cd6b2d)
|
||||
|
||||
`--max-allocs-per-node` — во сколько вариантов размещения регистров SDCC
|
||||
упирается на узел. У `sprinter-cc` дефолт 100000 (агрессивно, как fast-сборки
|
||||
библиотек), и на крупных модулях roomtest это МИНУТЫ на банк. Дефолт SDCC —
|
||||
3000, для разработки его достаточно: разница в размере — единицы процента.
|
||||
|
||||
Итог: сборка с нуля **1:48 вместо >10 минут**, куча 2751 Б вместо 2298
|
||||
(на 3000 резидент иначе не влезает — замер: конец `_HOME` 0xBC69 при стеке с
|
||||
0xBB00). В Makefile это `ALLOCS ?= 3000`:
|
||||
|
||||
```
|
||||
make — быстрая сборка (ALLOCS=3000)
|
||||
make ALLOCS=100000 — как раньше: минимальный код, для замеров размера
|
||||
и для «релизного» образа
|
||||
```
|
||||
|
||||
**ВАЖНО:** любое сравнение занятости банков имеет смысл только при ОДНОМ и
|
||||
том же `ALLOCS` — иначе сравниваются не правки, а уровни оптимизации.
|
||||
|
||||
<a id="dbg-cheats"></a>
|
||||
### DBG-CHEATS. Отладочные читы SDLPoP — **`[`/`]` СДЕЛАНЫ 2026-08-05**, остальное — оценка
|
||||
|
||||
Мотив прямой: мост MAME **теряет нажатия при быстрой отправке**, поэтому
|
||||
подогнать Кида в нужную позу для отладки автоматикой нельзя — именно на это
|
||||
упёрлись BUG-LOOSE-2 и BUG-GATE-PASS-1.
|
||||
|
||||
**Сделано:**
|
||||
|
||||
- **`[` / `]` — сдвинуть Кида на пиксель влево/вправо.** Оригинал —
|
||||
`../SDLPoP/src/seg000.c:1828`:
|
||||
```c
|
||||
if (key_states[SDL_SCANCODE_RIGHTBRACKET] & key_state) ++Char.x;
|
||||
else if (key_states[SDL_SCANCODE_LEFTBRACKET] & key_state) --Char.x;
|
||||
```
|
||||
У нас: коды PS/2 set 2 `[` = **0x54**, `]` = **0x5B** в `pop_cheat.h`
|
||||
рядом с `KBD_CHEAT_KILL/IMMO/SWORD`; обработка — в том же блоке читов
|
||||
`roomtest.c`, **по фронту** (`*_prev`, как у остальных), иначе одно
|
||||
нажатие уедет на десяток пикселей. Работает по `Kid.x` напрямую:
|
||||
геометрию персонажа в этом порте меняет не диспетчер (см. грабли L1-EXIT).
|
||||
- **Shift+L — следующий уровень** (вошло в L2-машинерию).
|
||||
|
||||
**Остальные — оценка, а не обязательство** (актуальный статус —
|
||||
[`TASKS_OPEN.md`](TASKS_OPEN.md), «Отложено осознанно»):
|
||||
|
||||
| Чит | Вердикт |
|
||||
|-----|---------|
|
||||
| **T — таймер** | **не сейчас**: таймера уровня у нас нет вообще (Фаза 6), чит пришлось бы делать вместе с механикой |
|
||||
| **F — остаток feather-fall** | **вместе с Shift+W**: ветка `JMP_IF_FEATHER` (опкод `0xF7`) в `play_seq` есть, но не проверена ничем; индикатор без самого зелья бесполезен, а пара «включить + видеть остаток» закрывает ветку целиком. Перо — уровень 7, так что не срочно |
|
||||
| **Shift+F9 — quickload с тем же уровнем** | **самое ценное и самое дорогое**: это сериализация `Char` + `room_modif` всех комнат + trob'ов + стражей (`levels_plan.md` §4). Даёт воспроизводимый регресс «вот кадр, где баг» вместо ручной подгонки |
|
||||
|
||||
---
|
||||
|
||||
<a id="kbd-1"></a>
|
||||
## KBD-1. Shift + стрелки: нажатия теряются — **ПРИЧИНА НАЙДЕНА, ФИКС ПРОВЕРЕН** (остаток отложен)
|
||||
|
||||
> **ПОПРАВКА К ПОСЫЛКЕ (2026-08-05).** Ниже «fake shift» подан как
|
||||
> установленный факт («при зажатом Shift PS/2 удваивает трафик»). Прямой
|
||||
> замер потока байт это не подтвердил: клавиатура MAME-Sprinter
|
||||
> (`pc_kbd ms_naturl`) обёртку `E0 F0 12` / `E0 12` не шлёт вовсе — при
|
||||
> зажатом Shift поток на стрелку ровно `E0 75 E0 75 …`. Значит удвоения
|
||||
> трафика в связке Shift+стрелка нет, и мотивировка «поэтому FIFO
|
||||
> переполняется» отпадает; сам ФИКС (плотный опрос `kbd_raw_poll`) остаётся
|
||||
> верным и нужным — переполнение вызывает не Shift, а короткая жизнь
|
||||
> импульса IRQ (пункт 3 гипотезы) плюс DI-окна графики. Разбор и следствия
|
||||
> — [BUG-KBD-5](bug_closed.md).
|
||||
>
|
||||
> **Итог (2026-08-01).** Причина — не наш код и не DI-окна графики: импульс
|
||||
> запроса прерывания здесь теряется примерно в 44 % случаев, и трёхбайтовый
|
||||
> FIFO SIO переполняется. Лечится ПЛОТНЫМ опросом: `kbd_raw_poll` повешен
|
||||
> idle-хуком на ожидание кадра (`gfx_set_idle_hook`, новый API libbgi) —
|
||||
> процессор всё равно проводит там ~42 мс из 60, крутя опрос луча.
|
||||
>
|
||||
> **Проверка в roomtest тем же счётным методом: 35 нажатий Shift+Home →
|
||||
> 35 make, ноль потерь** (до фикса — 9 из 10). Боевой сценарий тоже:
|
||||
> четыре Shift+→ подряд дали четыре осторожных шага, `Kid.x` 114 → 147.
|
||||
> Цена: `_CODE` +170 Б, кадровый бюджет не затронут (опрос стоит в
|
||||
> простое).
|
||||
>
|
||||
> **ОСТАТОК (ручная проверка пользователем, 2026-08-01): «стало значительно
|
||||
> лучше, но иногда при зажатом Shift стрелка всё-таки пропускается».**
|
||||
> Ощущение, не замер — счётчики на 35 нажатиях подряд потерь не показали,
|
||||
> значит остаточная частота заметно ниже прежних ~15 %. **Задача осознанно
|
||||
> ОТЛОЖЕНА до финальной полировки всей программы** (решение пользователя);
|
||||
> сейчас клавиатура пригодна для работы.
|
||||
>
|
||||
> **Где именно осталась дыра — чтобы на полировке не начинать с нуля.**
|
||||
> Idle-хук покрывает простой, то есть ~2/3 кадра. Оставшаяся треть — это
|
||||
> занятая фаза, и там DI-окно одного accel-прохода доходит до ~650 мкс при
|
||||
> допуске FIFO ~300 мкс. Пачка байт, целиком попавшая в такое окно, всё
|
||||
> ещё может потерять байт — ровно «иногда». Порядок действий, если
|
||||
> вернёмся:
|
||||
> 1. Вернуть вызовы `kbd_raw_poll()` между блитами занятой фазы (они
|
||||
> бесплатны; сами по себе не помогали, но вместе с хуком закрывают
|
||||
> именно этот зазор) и при необходимости внутрь тайловых циклов
|
||||
> `pop_bg` — тогда слепым остаётся только тело одного блита.
|
||||
> 2. Мерить тем же счётным методом (см. ниже), а не на ощупь: скриптовые
|
||||
> нажатия ровнее человеческих, поэтому набирать выборку от 50 нажатий.
|
||||
> 3. Если и это не добьёт — остаются два рычага вне нашего кода: Scan Code
|
||||
> Set 3 через BIOS `$EA` (убирает «fake shift» в корне, но в MAME
|
||||
> непроверяемо — обратный путь к клавиатуре не разведён) и общий
|
||||
> `m_irq_off_timer` в драйвере MAME.
|
||||
|
||||
**Симптом (пользователь, 2026-08-01).** Залипаний почти нет, но при
|
||||
УДЕРЖИВАЕМОМ Shift первые один-два нажатия ← дают осторожные шаги, дальше
|
||||
нажатия ← не отрабатываются, пока Shift не отпустишь.
|
||||
|
||||
**Рабочая гипотеза, с которой начинали.** Три факта складывались в одну
|
||||
картину:
|
||||
|
||||
1. **PS/2 Set 2, «fake shift».** При зажатом Shift нажатие РАСШИРЕННОЙ
|
||||
клавиши (стрелки — `E0`-коды) обрамляется фиктивным отпусканием/нажатием
|
||||
шифта: нажатие ← шлёт `E0 F0 12` + `E0 6B` = **5 байт** (без шифта было
|
||||
бы 2), отпускание — `E0 F0 6B` + `E0 12` = **5 байт** (было 3).
|
||||
(**Опровергнуто замером 2026-08-05, см. поправку выше.**)
|
||||
2. **Приёмный FIFO SIO — 3 байта.** Пачка в 5 байт переживает только то,
|
||||
что мы успеваем вычерпывать её по ходу. Потерянный make стрелки =
|
||||
«нажатие не отработало»; потерянный break = залипание (его лечит
|
||||
`kbd_raw_sync`, но ценой сброса всех немодификаторных клавиш).
|
||||
3. **Импульс IRQ клавиатуры в MAME живёт 32 такта CPU.**
|
||||
`mame/sources/MAME/src/mame/sinclair/sprinter.cpp`: `on_kbd_data()`
|
||||
выставляет `m_irqs->in_set<1>()` НА КАЖДЫЙ принятый байт (то есть старая
|
||||
запись в `docs/TODO.md` «MAME не даёт per-byte INT» — неверна), но тут же
|
||||
заводит `m_irq_off_timer` на 32 такта, а `irq_off()` снимает линию.
|
||||
**Если в эти 32 такта мы под `DI` — прерывание пропало насовсем**, байт
|
||||
остаётся в FIFO до следующего IRQ (следующий байт или кадровый 50 Гц).
|
||||
4. **Наши DI-окна длинные.** Ядра акселератора держат `di` на ВЕСЬ блит
|
||||
(`libbgi/bgi256/_bgi_blit_cols_raw.c:47` — «один DI на весь блит»);
|
||||
порядок цены прохода — 13.6 К тактов (`libbgi/include/gfx.h`), это
|
||||
сотни микросекунд, на порядки больше 32-тактового импульса.
|
||||
|
||||
Логика ввода (`pop_ctrl.c`, порт `read_user_control`/`safe_step`) сверена с
|
||||
SDLPoP и корректна: `safe_step()` ставит `control_forward = CONTROL_IGNORE`,
|
||||
и это снимается в `read_user_control()` при ОТПУСКАНИИ стрелки — то есть
|
||||
повторные тапы ← при зажатом Shift обязаны работать. Не работали они
|
||||
потому, что до нас не доезжал либо make, либо break стрелки.
|
||||
|
||||
### ЧТО ИЗМЕРЕНО (сессия 2026-08-01) — гипотеза про DI НЕ подтвердилась
|
||||
|
||||
**Методика.** Симптом «нажатие не отработало» переведён в счётчики, чтобы не
|
||||
спорить с глазами. Нажимается **Home** — тоже расширенная клавиша (тот же
|
||||
`E0`-префикс), но игрой игнорируется, поэтому Кид стоит на месте и рельеф
|
||||
комнаты на результат не влияет. Брейкпоинты с действием
|
||||
`{ b@ADDR = b@ADDR + 1 ; g }` (счёт без остановки машины) в трёх точках: вход
|
||||
клавиатурной ветки трамплина, чтение порта 0x18 внутри drain-цикла, запись
|
||||
make-бита для кода `0x6C`. Скратч-байты — хвост `ovr_tile[]`.
|
||||
|
||||
**Симптом воспроизведён скриптом:** при зажатом Shift 10 нажатий → до
|
||||
декодера дошло 9 make-байт. Без Shift потерь нет — ровно как сообщил
|
||||
пользователь.
|
||||
|
||||
| Прогон | make дошло / нажато | overrun |
|
||||
|--------|---------------------|---------|
|
||||
| игра идёт, `kbd_raw_poll` ВКЛ | 9 / 10 | 3 |
|
||||
| игра идёт, `kbd_raw_poll` ВЫКЛ (патч `ret` в точке входа) | 9 / 10 | 4 |
|
||||
| игра ЗАМОРОЖЕНА клавишей «1» (блитов нет вообще, значит и длинных DI нет) | **8 / 10** | 6 |
|
||||
|
||||
**Вывод 1: наши DI-окна ни при чём.** В замороженном кадре, где блитов нет
|
||||
и прерывания разрешены практически всё время, потерь НЕ меньше, а больше.
|
||||
|
||||
**Вывод 2: `kbd_raw_poll()` в той расстановке бесполезен** — 9/10 и с ним, и
|
||||
без. Причина понятна задним числом: шесть вызовов стояли В ТЕХ ЖЕ точках,
|
||||
где прерывания и так разрешены. Вызовы из `roomtest.c` убраны; сама функция
|
||||
в libc оставлена — она корректна и нужна как заготовка под «плотный опрос».
|
||||
|
||||
**Вывод 3 (главный): байт теряется НИЖЕ нашего кода.** Счётчик чтений порта
|
||||
0x18: 5 нажатий Shift+Home должны дать ровно 50 байт. Насчитано **49** — и
|
||||
ровно один make потерян. То есть до процессора байт не доехал вообще,
|
||||
декодер тут ни при чём.
|
||||
|
||||
**Вывод 4: прерывание на байт теряется примерно в 44 % случаев.** На тех же
|
||||
49 прочитанных байтах — только **28 входов** в клавиатурную ветку трамплина
|
||||
(1.75 байта за вход). Байты копятся в трёхбайтовом FIFO вплотную к потолку;
|
||||
одна неудачная пауза — и байт потерян.
|
||||
|
||||
### ПОТОЛОК ПЛОТНОГО ОПРОСА ИЗМЕРЕН — приём лечит полностью
|
||||
|
||||
`tests/kbdpoll` — программа, которая не делает НИЧЕГО, кроме
|
||||
`kbd_raw_poll()` в бесконечном цикле (ни графики, ни vsync, ни вывода:
|
||||
любая работа разредила бы опрос и испортила замер). Это физический
|
||||
максимум плотности. Тот же счётный метод, те же брейкпоинты-счётчики.
|
||||
|
||||
| Прогон | нажатий | make дошло | байт прочитано / ожидалось |
|
||||
|--------|---------|-----------|-----------------------------|
|
||||
| контроль: Shift зажат 4 с, нажатий нет | 0 | 0 | 0 (Shift сам ничего не шлёт — автоповтора у модификатора нет) |
|
||||
| Shift + Home | **25** | **25** | **250 / 250** |
|
||||
|
||||
**Ни одного потерянного байта.** Для сравнения: в игре при шести вызовах
|
||||
за кадр терялся 1 байт из 50. При такой частоте потерь вероятность
|
||||
случайно получить ноль потерь на 250 байтах ≈ 0.6 %, так что результат не
|
||||
совпадение.
|
||||
|
||||
**Вывод: опрос — рабочее решение, вопрос только в ПЛОТНОСТИ.** Нужно
|
||||
опрашивать примерно раз в 0.5 мс (≈10 000 тактов), а шесть вызовов за
|
||||
60-мс кадр давали один раз в 10 мс — в двадцать раз реже необходимого.
|
||||
|
||||
**Где взять частоту:** логический тик = 60 мс, из них ~18 мс занято
|
||||
работой и **~42 мс процессор простаивает внутри `gfx_wait_vsync`**, опрашивая
|
||||
луч. Опрос там стоит ноль и покрывает две трети периода с запасом.
|
||||
Остаётся слепым только тело одного accel-блита под DI (до ~650 мкс) —
|
||||
разорвать его нельзя (см. «что НЕ делать»).
|
||||
|
||||
**Почему нужна именно такая частота** (вопрос «PS/2 же не даёт больше 30
|
||||
нажатий в секунду»). Частота опроса определяется НЕ темпом нажатий, а
|
||||
темпом байт ВНУТРИ одного нажатия и глубиной FIFO. Клавиатура выдаёт байты
|
||||
со скоростью провода: 11 бит на байт при ~10–16 кГц = ~0.7–1.1 мс на байт.
|
||||
Воронка — 3 байта. Значит между двумя вычерпываниями имеют право прийти
|
||||
максимум два байта, то есть вычерпывать надо не реже чем раз в ~1.5–2 мс
|
||||
(0.5 мс взято с запасом). **Даже ОДНО нажатие в секунду переполнит FIFO**,
|
||||
если в эти несколько миллисекунд его никто не разгребает. Замер это
|
||||
подтверждает: 1.75 байта за одно вычерпывание — уже 58 % ёмкости. В норме
|
||||
разгребает прерывание; опрос понадобился только потому, что ~44 % импульсов
|
||||
здесь теряется.
|
||||
|
||||
**Альтернатива, которая убирает опрос совсем — уменьшить трафик, а не
|
||||
ускорять разгребание.** BIOS `$EA` (`FN_KBD_OUT`, `docs/new/09-input.md`
|
||||
§9.2) шлёт байт НА клавиатуру, то есть ей можно скомандовать:
|
||||
- **Scan Code Set 3** — нет ни «fake shift», ни `E0`-префиксов: make = 1 байт,
|
||||
break = 2;
|
||||
- либо хотя бы отключить typematic (`0xF5`/`0xF7`).
|
||||
|
||||
**Но проверить это в MAME НЕЛЬЗЯ:** в `sprinter.cpp` подключено только
|
||||
направление клавиатура→SIO (`m_kbd->out_data_cb() → rxa_w`); обратный путь
|
||||
(SIO→клавиатура) не разведён вовсе, так что команда просто уйдёт в никуда.
|
||||
Плюс пришлось бы переписать все наши константы кодов под Set 3. Значит это
|
||||
кандидат на «когда дойдём до реального железа».
|
||||
|
||||
**Что сделано по этому плану (2026-08-01):**
|
||||
1. ✅ Idle-хук в libbgi: `gfx_set_idle_hook(fn)`, вызывается в цикле ожидания
|
||||
луча внутри `gfx_wait_vsync`. Приложение ставит туда `kbd_raw_poll`.
|
||||
Полезен не только нам — любой программе даёт «качать» что-то в ожидании
|
||||
кадра. Осторожно с регистрами: цикл ждёт на BC-таймауте, вокруг вызова
|
||||
нужен push/pop, а сам таймаут в итерациях станет длиннее по времени.
|
||||
**Важно про цену: это НЕ новая нагрузка.** Опрос ставится ровно туда,
|
||||
где процессор и так впустую крутит `in a,(#0xFE)` — 42 мс из 60.
|
||||
**Ограничитель области, если «постоянный опрос» не нравится:** потери
|
||||
случаются ТОЛЬКО при зажатом модификаторе (замерено), значит хук можно
|
||||
взводить лишь пока нажат Shift/Ctrl/Alt.
|
||||
2. ✅ Перемерено в roomtest тем же счётным методом: **35/35**, потерь нет.
|
||||
3. ⏳ Вызовы в занятую треть кадра НЕ возвращены: одного idle-хука хватило.
|
||||
Держать в уме, если на реальном железе или на более тяжёлых сценах
|
||||
(несколько стражей) потери появятся снова — накрыть блиты дешевле, чем
|
||||
изобретать что-то новое.
|
||||
4. ⏳ Ручная проверка пользователем — «стало значительно лучше, иногда всё
|
||||
ещё пропускает»; остаток отложен (см. врезку в начале записи).
|
||||
|
||||
**Куда смотреть дальше, если плотного опроса не хватит.**
|
||||
1. **Драйвер MAME — НЕ ТРОГАЕМ** (решение пользователя: пересборка MAME на
|
||||
его машине занимает часы). Для протокола, подозрение осталось:
|
||||
`sinclair/sprinter.cpp` держит запрос от клавиатуры ровно **32 такта
|
||||
CPU**, и `m_irq_off_timer` — **один на два источника** (`irq_on()` экрана
|
||||
заводит его же, `irq_off()` гасит разом обе линии). То есть кадровое
|
||||
прерывание способно обрезать клавиатурный импульс — правдоподобное
|
||||
объяснение «44 % пропущенных импульсов».
|
||||
2. **Реальное железо.** Если п.1 — чисто эмуляционный артефакт, на железе
|
||||
проблемы может не быть вовсе. Проверять при первом прогоне на живом
|
||||
Sprinter.
|
||||
|
||||
**Про совпадение кадрового и клавиатурного прерываний** (вопрос
|
||||
пользователя, 2026-08-01). Документация Sprinter: оба приходят с вектором
|
||||
`0FFh`, различать по биту приёма байта в порту клавиатуры — «не пришёл,
|
||||
значит экран»; совпадение возможно, но «исключительно редкий случай»
|
||||
(в новой версии обещают развести жёстче через ПЛМ). То есть наш трамплин
|
||||
делает ровно предписанное. Известный побочный эффект: при совпадении мы
|
||||
обслуживаем клавиатуру и `reti`, пропуская кадровую цепочку и DSS — на
|
||||
потерю байт это не влияет (линия кадрового остаётся взведённой и вызывает
|
||||
повторный вход), но кадровый тик может пропасть. Отдельная мелкая правка.
|
||||
|
||||
**Что НЕ делать (проверено, стоило времени):**
|
||||
- **Снимать `di` в accel-ядрах libbgi нельзя.** Патч `di`→`nop` в
|
||||
`_bgi_blit_cols_raw`/`_bgi_heal_rows_raw`/`_bgi_blit_rows_raw` прямо в
|
||||
памяти **уронил машину в перезагрузку**. То есть режим «акселератор
|
||||
работает при EI» из `docs/new/06-accel.md §6.6` в этой прошивке/эмуляции
|
||||
недоступен — вопрос закрыт артефактом, а не рассуждением.
|
||||
- Дробить DI-окна по колонкам смысла тоже нет: см. вывод 1.
|
||||
Reference in New Issue
Block a user