Files
Sprinter-SDCC/applications/PoP/docs/shadow_render.md
T
snark13 860468f3c5 libbgi: NOT_PUT тоже через акселератор + изыскания по виду Тени
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>
2026-08-11 15:41:06 +03:00

161 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Отрисовка Тени (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-цветной палитры.