NOT. У акселератора нет режима «инвертировать буфер»: буфер меняется
только на ЧТЕНИИ и только опкодами AND/OR/XOR (HL) (драйвер MAME,
update_accel_buffer), а `CPL` автомат вообще не распознаёт — инвертируется
регистр CPU, не буфер. Зато ~src = src XOR #FF, поэтому NOT собирается из
уже имеющегося: буфер := src, XOR-burst по константному блоку единиц
(common/_bgi_ones256.c), запись. Ядра _bgi_blit_{rows,cols}_not_raw.c;
между burst'ами меняется HL, поэтому каждая смена — под СТОПом (иначе fetch
операнда перезапустит burst). В колоночном варианте второй OUT Port_Y не
нужен: op-burst идёт по блоку единиц горизонтально, а Port_Y шагает только
вертикальный.
Наружу — тем же op-параметром (GFX_OP_NOT), диспетчер один раз на вызов, в
цикл по полосам/колонкам не заходит. putimage лишился попиксельного пути
ЦЕЛИКОМ: все пять операций BGI идут через акселератор и клиппируются
одинаково. tests/accop дополнен T9/T10 (NOT строками и колонками) —
10/10 PASS в MAME; tests/bgi_img P1/P2 по-прежнему PASS.
tests/convbench (новый) — замер побайтной конвертации атласа
«прозрачный #FF -> 0x00» (источник для XOR-блита): 145.3 такта/байт, то
есть 2.5× от 59 номинальных T-states цикла. 0.11 с на страницу 16 КБ,
3.2 с на все 28 страниц, 1.3 с по фактическому объёму данных (186 КБ).
applications/PoP/docs/shadow_render.md — вид Тени (два блиттера OR+XOR)
ОТЛОЖЕН по решению пользователя: пока рисуем обычной копией атласами Кида.
В документе собрано, почему в лоб не выходит (XOR несовместим с
прозрачностью #FF; операция читает ОЗУ-копию, поэтому два прохода
оригинала вырождаются в один XOR — нужен однопроходный композит
s | bg ^ s(сдвиг)), замер стоимости источника и четыре варианта. Ключ к
выбору — список кадров, которыми тень реально пользуется; снимать по факту,
когда пойдут уровни 5/6/12.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
11 KiB
Отрисовка Тени (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. Варианты (когда вернёмся)
- Конвертация в рантайме при загрузке уровня с тенью (4, 5, 6, 12): диск и упаковщик не трогаем, цена — 1.3 с (или 0.7 с после разгона) на загрузку такого уровня.
- Лениво, постранично — 0.11 с (5.5 кадра) при первом обращении тени к странице; рывок один раз на страницу, суммарно меньше, чем вариант 1.
- Второй набор
.atlот упаковщика (pop_pack_kid.py, прозрачный0x00): 0 с рантайма, +186 КБ на образе и вторая ветка в загрузчике атласов. - Только 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-цветной палитры.