31b82661eb
Порт PoP переехал в applications/SprPoP — приложение, которое собирается само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной папки. Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT, по умолчанию ../..). applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся архивом закрытых задач, багов и исполненных планов. Скопировано из applications/PoP/roomtest@4b74478. Перенос проверен побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита, все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host- тесты зелёные (15/15). Раскладка: src/ рукописный C (roomtest.c -> sprpop.c) gen/ генерируемые заголовки, в репозитории assets/orig/ оригинальные данные игры, вне репозитория (копирайт) assets/packed/ то, что ложится на диск, в раскладке диска tools/ конверторы; все пути — в одном tools/paths.py build/ выход: exe, каталоги ресурсов, hdd/, промежуточные atl/ Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что пересчитывается каждым make. Автоматика построена на ОТСУТСТВИИ файла, а не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по времени превращалось бы в лотерею. Недостающий ресурс или заголовок чинится сам, рекурсивным вызовом в ветку генерации. Музыка собирается из любого из четырёх наборов записей (make music-mp3, music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама делает музыку устаревшей. Длины реплик больше не захардкожены: упаковщик печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них — иначе mt32 (реплики на 6% длиннее) молча ломал катсцену. Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR), HDD_IMG стал ?=; команда сборки roomtest не изменилась. Корневой make host-tests переключён на SprPoP. Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики приходила раньше молнии. Это обход, а не лечение; разбор с замерами — docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
161 lines
11 KiB
Markdown
161 lines
11 KiB
Markdown
# Отрисовка Тени (charid_1_shadow) — изыскания, отложено
|
||
|
||
Статус на 2026-08-11: **отложено по решению пользователя.** Тень пока
|
||
рисуется как обычный персонаж — простой копией из атласов Кида
|
||
(`pop_cdraw.c`, банк 0x5C, аппаратная прозрачность `#FF`). Вернуться к
|
||
«правильному» виду, когда будут сделаны все уровни: тогда будет известно,
|
||
какими именно кадрами тень вообще пользуется.
|
||
|
||
Этот файл собирает всё, что уже выяснено, чтобы не переоткрывать.
|
||
|
||
---
|
||
|
||
## 1. Как тень выглядит в оригинале
|
||
|
||
Тень рисуется **двумя блитами ОДНОГО И ТОГО ЖЕ спрайта Кида**
|
||
(`seg008:1602`, `add_objtable`):
|
||
|
||
```c
|
||
case 1: // shadow
|
||
add_midtable(obj_chtab, obj_id + 1, obj_xh, obj_xl, obj_y, blitters_2_or, 1);
|
||
add_midtable(obj_chtab, obj_id + 1, obj_xh, obj_xl + 1, obj_y, blitters_3_xor, 1);
|
||
```
|
||
|
||
OR на месте, XOR со сдвигом на пиксель вправо. XOR гасит совпавшее,
|
||
остаются края — отсюда «контурный» вид. Это ЗАМЫСЕЛ оригинала, а не
|
||
артефакт SDLPoP: подтверждено печатью из живого SDLPoP (метка `DBGMIRROR`
|
||
в `add_objtable`) — и тень, и отражение идут из `chtab=2` (собственные
|
||
спрайты Кида), `swordbits=0`, обычными кадрами:
|
||
|
||
```
|
||
type=4 chtab=2 img=40 dir=0 clipL=137 clipT=3 charid=0 frame=41 <- отражение
|
||
type=1 chtab=2 img=41 dir=0 clipL=137 clipT=3 charid=1 frame=42 <- тень
|
||
```
|
||
|
||
Единственное различие между отражением и тенью — блиттер.
|
||
|
||
## 2. Чем мы располагаем
|
||
|
||
Блочные AND/OR/XOR/NOT акселератора подняты в libbgi 2026-08-11 (полный
|
||
набор строками и колонками, `gfx_blit_op` / `gfx_blit_part_op` /
|
||
`gfx_blit_cols_op` / `gfx_blit_cols_part_wx_op`; регресс — `tests/accop`,
|
||
10/10 PASS). Механика и ловушки — memory `accel_block_ops` и шапка
|
||
`libbgi/common/_gfx_blit_full_op.c`. То есть примитивов достаточно, дело
|
||
не в них.
|
||
|
||
## 3. Две причины, по которым «в лоб» не получается
|
||
|
||
### 3.1 XOR несовместим с нашей прозрачностью `#FF`
|
||
|
||
Аппаратная прозрачность (бит 3 видеобанка) подавляет запись байта `#FF`,
|
||
то есть смотрит на **результат** операции:
|
||
|
||
| операция | прозрачный пиксель источника | итог |
|
||
|---|---|---|
|
||
| AND | `#FF & bg = bg` | работает даром |
|
||
| OR | `#FF \| bg = #FF`, запись подавляется | работает даром |
|
||
| XOR | `#FF ^ bg = ~bg`, подавления нет | **инверсия фона по всему футпринту** |
|
||
|
||
Совпадение с «ничего не делать» у XOR получается только там, где фон равен
|
||
0 (`#FF ^ 0 = #FF` → подавляется). В DOS-оригинале прозрачный индекс = 0 —
|
||
нейтральный и для OR, и для XOR, поэтому там оба блиттера работают на одном
|
||
наборе спрайтов. У нас прозрачный `0xFF` (`pop_pack_kid.py`: `0 -> 0xFF`,
|
||
`i -> 0x70 + i`).
|
||
|
||
Замаскировать `#FF` внутри операции нельзя в принципе: побитовые AND/OR/XOR
|
||
не умеют «выбрать по условию», а `#FF` — нейтраль только для AND. Значит
|
||
источнику XOR-прохода нужен **прозрачный `0x00`**, то есть отдельный набор
|
||
спрайтов.
|
||
|
||
### 3.2 Операция читает ОЗУ-копию экрана, а не видео-ОЗУ
|
||
|
||
Чтение страниц `#50..#5F` всегда отдаёт ОЗУ-копию (memory
|
||
`sprinter_vram_transparency`), а персонажи рисуются банком `0x5C` («не
|
||
писать в копию» — на этом держится даровой heal). Поэтому второй проход
|
||
**не увидит результат первого**: два блита оригинала выродились бы в
|
||
«просто XOR», контурного эффекта не будет.
|
||
|
||
Лечится не банком `0x50` (он ломает heal — копия перестанет быть чистым
|
||
фоном), а **однопроходным композитом**: всё складывается в буфере
|
||
акселератора за один проход по колонке j футпринта
|
||
|
||
```
|
||
буфер := s[j] ; спрайт
|
||
буфер |= bg[j] ; вертикальное чтение экрана
|
||
буфер ^= s[j-1] ; тот же спрайт, предыдущая колонка = сдвиг на +1 px
|
||
запись ; вертикальная запись колонки
|
||
```
|
||
|
||
что **точно эквивалентно** двум блитам оригинала (крайние колонки: `x` —
|
||
только OR, `x+w` — только XOR) и вдобавок дешевле их: 4 burst'а на колонку
|
||
против 6. Такому композиту тоже нужен источник с прозрачным `0x00` — уже
|
||
на обоих шагах.
|
||
|
||
## 4. Сколько стоит подготовить источник с прозрачным `0x00`
|
||
|
||
Замер 2026-08-11 (`tests/convbench`, watchpoint по IO-записи в MAME, кадр =
|
||
430 080 тактов). Цикл безветвочный (`ADD A,A / SBC A,A / CPL / AND` —
|
||
маска из бита 7: прозрачный `#FF` отличается от цветов Кида `0x70..0x7F`
|
||
именно им), 59 номинальных T-states на байт, по факту **145.3 такта/байт**
|
||
(2.5× wait-state'ов ОЗУ):
|
||
|
||
| объём | кадров | секунд |
|
||
|---|---|---|
|
||
| 1 страница атласа, 16 КБ | 5.5 | 0.11 |
|
||
| весь атлас Кида, 28 страниц × 16 КБ = 448 КБ | 155 | 3.2 |
|
||
| он же **по реальному размеру данных (186 КБ)** | 64 | **1.3** |
|
||
|
||
Последняя строка — замечание пользователя: 28 атласов занимают 186 КБ, а не
|
||
448 КБ; обрабатывать по фактическому размеру ленты вместо целой страницы
|
||
даёт 2.4× (ценой проверки границы в цикле). Потолок разгона самого цикла —
|
||
ещё примерно вдвое (раскрутка убирает `djnz`, чтение через SP парами +
|
||
таблица 256 Б вместо арифметики), то есть **~0.7 с** на 186 КБ. Порядок
|
||
величины при этом не меняется.
|
||
|
||
**Окна:** источник и приёмник — разные EMM-страницы, а окно под атласы одно
|
||
(W0), поэтому конвертация гоняется «страница-источник в W0 →
|
||
страница-приёмник в W3» целыми страницами; побайтно переключать окно нельзя.
|
||
EMM-бюджет: +28 страниц (448 КБ) из ~3440 КБ свободных — не проблема
|
||
(memory `sprinter_emm_budget`), и он одинаков в любом из вариантов.
|
||
|
||
## 5. Варианты (когда вернёмся)
|
||
|
||
1. **Конвертация в рантайме при загрузке уровня с тенью** (4, 5, 6, 12):
|
||
диск и упаковщик не трогаем, цена — 1.3 с (или 0.7 с после разгона) на
|
||
загрузку такого уровня.
|
||
2. **Лениво, постранично** — 0.11 с (5.5 кадра) при первом обращении тени к
|
||
странице; рывок один раз на страницу, суммарно меньше, чем вариант 1.
|
||
3. **Второй набор `.atl` от упаковщика** (`pop_pack_kid.py`, прозрачный
|
||
`0x00`): 0 с рантайма, +186 КБ на образе и вторая ветка в загрузчике
|
||
атласов.
|
||
4. **Только OR-проход** (то, чем можно обойтись бесплатно): OR с нашим
|
||
`#FF`-атласом работает как есть, тень получается сплошным силуэтом в
|
||
палитре Кида, без контурного эффекта. Расхождение с оригиналом — тогда
|
||
записью в `docs/impl_diff.md`.
|
||
|
||
**Ключ к выбору — какие кадры тень вообще использует.** Предположение
|
||
пользователя: только бег, длинный прыжок (из зеркала), питьё зелья и
|
||
боёвка; прыжки с места и подтягивания — нет. Если так, конвертировать
|
||
(или паковать) нужно единицы страниц, а не 28, и разница между вариантами
|
||
почти исчезает. Список снимать по факту — когда уровни 5/6/12 будут
|
||
проходиться.
|
||
|
||
## 6. Что ещё придётся проверить глазами
|
||
|
||
Палитра. У нас индексы разложены группами по 16 (`pop_pack_bg.py`):
|
||
`0x30` VGA16, `0x40` chtab_1, `0x50` env, `0x60` wall, `0x70` kid, `0x80`
|
||
sword, `0x90` guard. Отсюда ожидания (аналитические, в MAME НЕ
|
||
проверялись):
|
||
|
||
- OR-проход ложится удачно: `0x7X | 0x5Y = 0x7Z` — результат остаётся в
|
||
палитре Кида, а младший ниббл получается ровно тот же, что дал бы DOS
|
||
(там OR шёл по 4-битным индексам внутри одной палитры);
|
||
- XOR-проход уводит результат в группы `0x0Z` (поверх OR-результата) и
|
||
`0x2Z` (по чистому фону) — **обе группы палитры у нас не заполнены**, то
|
||
есть контур рискует оказаться просто чёрным.
|
||
|
||
Значит к «посмотреть глазами» добавляется вопрос, чем заполнять `0x00..0x0F`
|
||
и `0x20..0x2F` — по сути это и будет выбор цветов тени. В DOS такого
|
||
вопроса не было: XOR двух 4-битных индексов всегда оставался внутри той же
|
||
16-цветной палитры.
|