# Идеи и вопросы «на подумать» (PoP) Не план работ, а список того, что осознанно отложено: каждая запись — гипотеза с причиной, по которой её стоит проверить, и с тем, что мешает сделать это прямо сейчас. ## Зелье «переворот экрана» (upside-down) **Вопрос пользователя (2026-08-01).** Тайлы фона у нас лежат строками, а кадры Кида/стражей — КОЛОНКАМИ (`transpose_cols` в `pop_pack_kid.py`, ради бесплатного горизонтального зеркала). Значит вертикальный переворот для персонажей заметно сложнее, чем для фона. Верно; но прежде чем это чинить, надо знать три факта. **Факт 1 — когда оно вообще нужно.** Зелье переворота — тип 4 (`proc_get_object`, `seg006.c:1885` → `toggle_upside()`). Скан всех уровней по данным (`res200N.bin`, тайл 10 = зелье, тип в backtable): тип 4 встречается **впервые на уровне 9** (две склянки), и больше нигде. Тип 3 (перо, медленное падение) — уровень 7. То есть **до уровня 9 механика не нужна вообще**, и «на первом этапе просто не реализовывать» — не компромисс, а точное соответствие данным уровней 1..8. **Факт 2 — что именно делает оригинал.** НЕ переворачивает спрайты. `flip_screen` (`seg009.c:1042`) → `flip_not_ega` (`seg009.c:1023`) меняет местами СТРОКИ готового offscreen-буфера (top↔bottom, порядок пикселей внутри строки не трогает — это вертикальное зеркало, не поворот на 180°). Вызывается вокруг отрисовки кадра целиком (`seg003.c:296..301`): перевернул буфер → дорисовал → перевернул обратно. Так что в оригинале это post-process всего экрана, и вопрос «как перевернуть колоночный спрайт» там просто не возникает. **Факт 3 — почему нам этот приём не подходит как есть.** У нас нет шага «готовый offscreen → экран»: рисуем прямо в видеостраницу, а heal берёт фон из ОЗУ-копии этой же страницы. Переворот всей страницы построчно — это 320×192 Б копирования КАЖДЫЙ кадр, что мимо бюджета на порядок. **Варианты, которые надо будет взвесить (не сейчас):** 1. **Предпечённые перевёрнутые атласы.** Второй набор кадров Кида/стража, перевёрнутый по вертикали ещё в `pop_pack_kid.py` (там уже есть транспонирование — добавляется одной строкой). Рантайм: выбор набора + зеркальная арифметика Y. Память: ещё ~28 страниц EMM при бюджете ~3.3 МБ — не проблема. Похоже, самый дешёвый по тактам путь. 2. **Фон рисовать с обратным Y** — для row-major тайлов строка остаётся непрерывным accel-прогоном, меняется только адрес назначения; цена — вызов на строку вместо вызова на тайл. Померить, прежде чем закладывать. 3. **Аппаратная помощь** — до проектирования проверить, есть ли у акселератора направление копирования «вниз» (обратный инкремент адреса); если есть, вариант 1 может и не понадобиться. Смотреть `docs/new/06-accel.md` и `docs/reference/accel_r.txt`. **Почему не сейчас.** Уровни 1..8 этого не требуют, а к уровню 9 у нас уже будет ответ на вопрос «сколько стоит кадр» (задачи CLIP-1/T-2) — без него выбирать между вариантами выше бессмысленно. ## Заменить генератор псевдослучайных чисел Сейчас стоит LCG оригинала, шаг на ассемблере (~1 020 тактов), бит-в-бит совместимый с SDLPoP. Есть более дешёвые Z80-генераторы (86–148 тактов), но потолок выигрыша — 2 814 тактов за кадр, 0.65 %, и он растворяется в обёртках вызова. Тексты процедур, разбор качества и порядок действий — `prng_alternatives.md`. Первый шаг там не про генератор: слить приведение к диапазону в ту же asm-процедуру, чтобы на вызов был один `call`, а не три. ## Отключать мышь на время игры **Гипотеза.** Мышь на Sprinter — источник прерываний (обёртки RST 30h, см. memory `mouse_api`). Игре она не нужна вообще: управление — raw-клавиатура (``), которую мы и так забираем у DSS целиком. Значит каждое мышиное прерывание за кадр — украденные такты в бюджете, который у нас и без того занят на 86 %. **Откуда взялось (2026-07-30).** При замере бюджета по 100 кадрам три кадра выбились до 552–647 К тактов (1.28–1.51 кадра) при типичных 371 К. Причиной оказалось движение мыши на ХОСТЕ: при неподвижной мыши 225 кадров подряд прошли без единого превышения. То есть эффект реальный и измеримый, просто в тесте он был наведён извне. **Что проверить.** 1. Есть ли у драйвера мыши (RST 30h) функция «выключить/включить» — разобрать список из 14 обёрток; если нет явной, посмотреть, что делает «hide cursor» и снимает ли она обработчик. 2. Сколько тактов реально стоит одно мышиное прерывание на нашем железе (замер: breakpoint на входе ISR + totalcycles, при движении мыши). 3. Не ломает ли отключение выход в DSS: состояние обязано восстанавливаться при `exit`, включая аварийный (atexit). **Почему не сейчас.** Выигрыш проявляется только когда игрок реально двигает мышью, то есть в норме его нет; а риск оставить систему без мыши после выхода — заметный. Делать после того, как закроем стражей и займёмся бюджетом всерьёз (там же, где батчинг кроссбанковых вызовов и возможный возврат `pop_bg` в резидент `--w3`). ## Готовить следующий уровень, пока играет мелодия конца текущего **САМАЯ ДАЛЬНЯЯ ВЕРСИЯ.** Не полишинг и не порт: это улучшение ПРОТИВ оригинала. Планируем, но не раньше, чем закроем уровни и полишинг. **Идея (пользователь, 2026-08-28).** Пока звучит мелодия конца уровня (и трек заставки между уровнями), экран не меняется — значит в это время можно успеть прочитать с диска следующий уровень, чтобы после музыки он появлялся сразу, а не через секунду-другую загрузки. **Что УЖЕ сделано и мерено (2026-08-28).** Половина этого уже работает: трек заставки играет ПОВЕРХ загрузки уровня — так же, как в оригинале (`seg003:68-108`: `load_intro` возвращает управление, `load_level()` идёт под музыку, и только потом `while (check_sound_playing())` на чёрном экране). Насос это переживает: 500 подряд вызовов `pop_sfx_fill` через всю загрузку — максимальный зазор 245 832 такта при дедлайне 251 000, ни одной пропущенной порции. Осталась вторая половина: мелодия конца уровня досиживается на ЖИВОМ экране пройденного уровня (как в оригинале, `play_level_2`), и только после неё начинается загрузка. **Оговорки, найденные при разборе.** 1. **Загрузку придётся разрезать надвое.** Сейчас `pop_level_switch` мешает дисковую работу (файл уровня в EMM-страницу, атласы стража и тайлсета) с экранно-палитровой (`pop_bg_load` переписывает записи палитры и атласы, `pop_guard_load` физически применяет цветовые слоты, обе страницы заливаются, `pop_pal_black`). Вторую половину НЕЛЬЗЯ выполнять, пока на экране ещё живёт пройденный уровень — иначе палитра поедет прямо на картинке. То есть в «музыкальное окно» можно вынести только диск, а всё, что трогает палитру и VRAM, остаётся после. 2. **Бюджет кадра.** Ожидание мелодии идёт в игровом цикле (гейт `pop_endmus_left` + `pop_music_busy()`), кадр там обычный, 80-100 мс. Шаг подкачки должен быть такого же размера, как музыкальный — полстраницы (~16 мс), а не страница целиком; иначе кадры на экране пройденного уровня начнут дёргаться. 3. **Память.** Страницы следующего уровня придётся держать одновременно со страницами текущего — проверить EMM-бюджет (memory `sprinter_emm_budget`), тяжёлые тут не файл уровня, а атласы тайлсета и стража. 4. **Риск невелик, но он в самом хрупком месте.** Палитро-атласный обмен — источник уже пойманных багов (`PAL-DUNGEON-STALE`, `PAL-L1-AFTER-INTRO`), и трогать его ради экономии секунды стоит только на спокойную голову. **Выигрыш.** Одна-две секунды один раз на уровень. ## Музыка одним постоянно открытым архивом (замер 2026-08-31, НЕ сейчас) **Идея.** Сейчас каждый трек — отдельный файл `MUS/mNN.bin`, и `pop_music_load_begin` открывает свой на каждый запуск. Свести треки в один файл и держать его `fd` открытым: вместо `chdir`+`open`+`chdir` останется `lseek` к смещению трека. **Почему это стоит внимания — разложение окна старта музыки** (полный замер и метод — `sound_plan.md` §9): | шаг | цена | уйдёт? | |---|---:|---| | ожидание `pop_music_service` в кадре | 22,6 мс | нет | | `chdir` #1 | 46,1 мс | **да** | | `open` файла трека | 34,5 мс | **да** | | `chdir` #2 | 59,0 мс | **да** | | чтение первых 8 КБ | 34,0 мс | нет | | **итого** | **196,7 мс** | | Уходит **139,6 мс** — но это ВЕРХНЯЯ граница: появляется `lseek` к смещению трека, которого сейчас нет вовсе (файл читается последовательно с нуля), и его цена НЕ ИЗМЕРЕНА. Реальный выигрыш = 139,6 минус `lseek`; померить можно тем же способом, `lseek` уже используется в `pop_arc.c:89`. Остаётся ≈ 57 мс, то есть старт трека ускоряется примерно вчетверо. **Где заметно.** Реплики PV-сцены идут встык (три трека подряд), плюс каждый игровой джингл — смерть, зелье, подобранный меч. **Чем осложнено.** 1. **`PBA1` под это не годится.** Размер элемента там `uint16` (≤ 64 КБ), а трек — до 1,2 МБ (m56, 78 страниц); резать по страницам нельзя, элементов вышло бы ~240 при `POP_ARC_MAX = 126`. Нужен свой индекс со смещениями — то есть эта задача СМЫКАЕТСЯ с `MUS/MUSIC.IDX` из `music_runtime_index_plan.md`: одно изменение раскладки, а не два. 2. **Постоянно занятый файловый манипулятор** — один из восьми (memory `dss_fd_limit`, девятый `open` вешает DSS). Рядом свои открывают `POP.CFG`, quicksave и загрузка уровня. 3. **`chdir` убрать нельзя** — он и есть половина выигрыша, но нужен старым DSS (`POP_PATH_CALL`); экономия берётся не его удалением, а тем, что открытие вообще перестаёт выполняться на каждый трек. **Оговорка.** Задержка старта музыки сама по себе НЕ является дефектом: трек начинается на границе события, на слух это не сбой. У эффектов такой задержки нет вовсе — они целиком в EMM (см. `sound_plan.md` §9.1). Так что это оптимизация, а не исправление.