# Отрисовка Тени (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-цветной палитры.