Files
Sprinter-SDCC/applications/SprPoP/docs/shadow_render.md
T
snark13 31b82661eb SprPoP: автономное приложение, выделенное из roomtest
Порт 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>
2026-08-27 12:12:28 +03:00

11 KiB
Raw Blame History

Отрисовка Тени (charid_1_shadow) — изыскания, отложено

Статус на 2026-08-11: отложено по решению пользователя. Тень пока рисуется как обычный персонаж — простой копией из атласов Кида (pop_cdraw.c, банк 0x5C, аппаратная прозрачность #FF). Вернуться к «правильному» виду, когда будут сделаны все уровни: тогда будет известно, какими именно кадрами тень вообще пользуется.

Этот файл собирает всё, что уже выяснено, чтобы не переоткрывать.


1. Как тень выглядит в оригинале

Тень рисуется двумя блитами ОДНОГО И ТОГО ЖЕ спрайта Кида (seg008:1602, add_objtable):

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-цветной палитры.