libbgi: спрайтовый движок v2 + accel-блит/heal leaf'ы + noclip-путь
Спрайтовая графика поверх accel block-copy (docs/sprite-api-design.md): - Ядро блиттинга: leaf'ы _bgi_blit_rows_raw (dst фикс, только src-страйд) / _bgi_copy_rows_raw (getimage) / _bgi_heal_rows_raw (src==dst). DI один на спрайт (санкция: малый спрайт под одним DI аудио не рвёт); src[0]-фикс снят (точная MAME подавляет CPU-байт триггера записи — на железе перепроверить; для heal был избыточен и снят безусловно). - Общие bracket-free ядра _gfx_blit_full/_gfx_heal_full (полная ширина: клип по экрану + split >256 для putimage) + лин _gfx_blit_sprite/ _gfx_heal_sprite (кадр ≤64, без split, 8-бит w/h) + noclip-варианты (клип-кода нет → полный codegen-win). Имя *_full (не *_clip) — «clip» двусмысленно (sprite-ядра тоже клипуют; различитель — ширина/split). - Движок retained-модели <sprite.h>: sprite_init/update/flip + inline move/frame/show/hide/touch; drawn[2] per-page внутри структуры; кадр — двухпроходно heal ВСЕ -> блит ВСЕ под одной W3-скобкой/банком на проход. - Флаг gfx_sprite_clip(on/off): приложение, само следящее за границами, отключает клип (~+19% на анимации; диспетч пока через if — funcptr далее). - putsprite/movesprite/gfx_blit/putimage(COPY)/getimage переведены на ядро. Тесты: examples/balls (движок, дабл-буфер, boundary-тест клипа), tests/sprites (RAM PASS, клип 4 края, атлас), tests/blitw (trig-leak), tests/spriteclip (hardware-probe: железо НЕ режет за краем -> клип нужен), tests/blitperf, tests/gfxbanks. size-baseline обновлён (53 программы). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+104
-13
@@ -84,27 +84,110 @@ Quick wins:
|
||||
- [ ] кэширование block-size между burst'ами (accel помнит размер)
|
||||
|
||||
Новые возможности:
|
||||
- [ ] **пакетное чтение/запись массива байт через акселератор** —
|
||||
отдельные ф-ии, которые в ОДНОМ вызове читают/пишут строку или
|
||||
столбец пикселей (гориз./верт.), а не по пикселю через
|
||||
_bgi_read_raw/_bgi_plot_raw. Аналогично тому, как сейчас рисуются
|
||||
линии (_bgi_hspan_raw/_bgi_vspan_raw задают Port_Y и адрес один раз,
|
||||
далее burst). Цель — ускорить блиттинг: getimage/putimage сейчас
|
||||
зовут _bgi_read_raw/_gfx_getpixel256_raw по-пиксельно, из-за чего на
|
||||
каждый пиксель повторяются out Port_Y + ld bc,(base) + bounds-check
|
||||
(см. разбор _bgi_read_raw). Дать `_bgi_hread_raw`/`_bgi_hwrite_raw`
|
||||
(+ vertical) с ABI как у fill-сегментов (HL=addr, B=len, E=y, буфер
|
||||
в DE) и перевести getimage/putimage на них.
|
||||
- [ ] `gfx_blit` / `gfx_blit_transparent` — block copy (LD L,L / LD A,A),
|
||||
прозрачность через bank 0x58 («FF is transparent»)
|
||||
- [x] ~~пакетное чтение/запись массива байт через акселератор~~ —
|
||||
сделано 2026-07-11 (Фаза A спрайтового дизайна,
|
||||
docs/sprite-api-design.md): leaf `_bgi_copy_rows_raw` (accel
|
||||
block-copy LD L,L, до 256 байт/burst, размер блока армируется
|
||||
один раз) + ядро `gfx_blit_part` (клиппинг, полосы ≤256);
|
||||
putimage(COPY_PUT) и getimage переведены (регресс tests/bgi_img
|
||||
1:1 с per-pixel эталоном, подрежимы банков на accel-пути —
|
||||
tests/gfxbanks).
|
||||
- [ ] **вертикальный copy-leaf `_bgi_copy_cols_raw`** (режим LD A,A —
|
||||
вертикальная копия, Port_Y двигается сам как у vfill). Анализ
|
||||
2026-07-12: для СПРАЙТОВ требует column-major хранения (читать
|
||||
линейный буфер вертикально нельзя — Port_Y не действует вне
|
||||
видеоокна; референс docs/samples/balls пишет колонками и потому
|
||||
выводит спрайт ТРАНСПОНИРОВАННЫМ — незаметно только на
|
||||
симметричном шаре) → формат несовместим с getimage, не делать.
|
||||
А вот **вертикальный HEAL формат-независим** (экран→экран) и
|
||||
выгоден для узких высоких областей: L-полоска 2×16 при
|
||||
горизонтальном движении спрайта = 2 burst'а вместо 16 (8×
|
||||
меньше оверхеда); выбор ориентации по форме — как в
|
||||
_gfx_rectfill256. Нюанс: между read и write колонки Port_Y
|
||||
надо вернуть на y0 (стоп → OUT → ре-арм, ~20Т/колонку).
|
||||
Делать вместе с L-strip оптимизацией heal.
|
||||
- [ ] **паттерны через акселератор + FF-прозрачность** (идея 2026-07-11):
|
||||
всё, что сейчас рисуется по-пиксельно из-за «дырок», можно гнать
|
||||
burst'ами через банк 0x58 — дырки паттерна кодируются 0xFF и
|
||||
отбрасываются железом на записи:
|
||||
- стилизованные линии (_bgi_styled_line: DOTTED/DASHED/CENTER/
|
||||
USERBIT сейчас per-pixel): построить 16-байтовый шаблон строки
|
||||
из 16-бит маски (бит=цвет, 0=0xFF) и повторять accel-copy;
|
||||
- fill-паттерны (_bgi_fill_span, LTSLASH_FILL и пр.): 8-байтовые
|
||||
строки-шаблоны 8×8 паттерна тем же способом;
|
||||
- ВНИМАНИЕ: на MAME 0.283 запись FF через 0x58 портит теневое
|
||||
ОЗУ (частичный скип, см. sprite-api-design «Результаты Фазы 0»)
|
||||
— включать после подтверждения полного подавления на железе,
|
||||
либо через 0x5C + пере-heal.
|
||||
- [x] ~~gfx_blit / putsprite / gfx_heal / movesprite~~ — Фаза B
|
||||
сделана 2026-07-11 (docs/sprite-api-design.md §3): GFX_BANK_*
|
||||
константы + gfx_blit/gfx_blit_part/gfx_heal в gfx.h,
|
||||
putsprite/movesprite в graphics.h; проверено tests/sprites
|
||||
(прозрачность, клиппинг 4 краёв, heal src==dst, чистый след
|
||||
movesprite, атлас). Осталась Фаза C — пример-курсор.
|
||||
- [ ] **managed-движок спрайтов v2** — sprite_t (drawn-координаты
|
||||
per-page ВНУТРИ структуры) + retained-модель:
|
||||
sprite_move/frame/show/hide меняют только state,
|
||||
sprite_update(arr,n) делает «heal все → блит все» на текущей
|
||||
draw-странице, sprite_flip — сахар с vsync+флипом; z-order =
|
||||
индекс массива. Интегрирует спрайты со сменой страниц —
|
||||
снимает с приложения учёт drawn[2][N] (источник багов,
|
||||
examples/balls 2026-07-11). Полный эскиз:
|
||||
docs/sprite-api-design.md §9.1.
|
||||
- [ ] `gfx_xor_rect` / `gfx_or_rect` / `gfx_and_rect` / `gfx_invert_rect`
|
||||
- [ ] шрифты ≠ 8×8: gfx_set_font_data(ptr,w,h,advance), proportional,
|
||||
8×16/16×16, отдельный font_id API; font-quad для 640×256
|
||||
(per-cell палитра через дескрипторы 0x0300..0x039F)
|
||||
|
||||
Оптимизации (не сейчас):
|
||||
- [x] ~~stride-арифметика в _bgi_copy_rows_raw — вон из горячего
|
||||
цикла~~ — сделано 2026-07-12 (профиль examples/balls vs
|
||||
docs/samples/balls): универсальный leaf — SMC-патч страйдов в
|
||||
8-битные add/adc-цепочки при входе (140Т → 73Т/строку, BC/push/
|
||||
pop/ex ушли); heal — отдельный `_bgi_heal_rows_raw` без адресной
|
||||
арифметики вообще (77Т/строку против 204Т; референсный уровень).
|
||||
Шар 16×16: ~9.6 → ~6.6 кТ (heal 16×173 + блит 16×236). Регресс:
|
||||
blitw/bgi_img/sprites/balls — 1:1. Остатки разрыва с asm-
|
||||
референсом — C-обвязка вызовов (batch-пасс, см. ниже) и спец-
|
||||
blit-leaf (dstride 0: −30Т/строку — делать по замеру).
|
||||
- [ ] **batch-пасс для спрайтов** (движок v2, §9.1) — В РАБОТЕ 2026-07-12.
|
||||
Разрыв ~2× с asm-референсом (docs/samples/balls) — это НЕ
|
||||
W3-скобка (_bgi_begin = 5 инструкций, ~35Т), а per-call
|
||||
C-обвязка, повторяемая 2×N раз: полный клип (528/362 Б),
|
||||
парс заголовка img, save/restore банка, SDCC-фрейм 7-арг вызова.
|
||||
Референс всё это платит ОДИН раз на проход: W3 замаплен на видео
|
||||
на всю программу (в цикле не трогает), банк-подрежим ставит один
|
||||
`out` на проход (0x5C рисовать всё → 0x50 лечить всё), DI держит
|
||||
на весь проход. Движок v2 повторяет это (per-pass W3/банк/клип-
|
||||
fast-path, адресация из кэша w,h в структуре), НО DI оставляем
|
||||
гранулярным (по строке в leaf) — референс держит длинный DI
|
||||
только потому, что у него нет аудио-ISR; под CBL/IM2 длинный DI
|
||||
сорвёт звук (docs/accel-fill-budget.md).
|
||||
- [ ] клип fast-path «прямоугольник целиком на экране» в gfx_blit_part
|
||||
(4 сравнения вместо полного пути) — делается в _gfx_blit_full/
|
||||
_gfx_heal_full вместе с batch-пассом.
|
||||
- [ ] размер: общий clip-хелпер для gfx_blit_part (528 Б) и gfx_heal
|
||||
(362 Б) — клиппинг сейчас продублирован; кандидат −300..400 Б.
|
||||
- [ ] gfx_line через accel для пологих диагоналей (runs ≥ 4-5 px)
|
||||
- [ ] композитные примитивы с одним W3-swap на операцию
|
||||
- [ ] ~~**спрайт-анимация: heal только открывшейся L-полоски**~~ —
|
||||
ОТВЕРГНУТО 2026-07-12 для ПРОЗРАЧНЫХ спрайтов (наш случай).
|
||||
L-полоска (bbox старой позиции минус новой) корректна ТОЛЬКО для
|
||||
непрозрачного full-box спрайта: тогда зону перекрытия целиком
|
||||
перекрашивает новый блит. У прозрачного спрайта (0xFF через
|
||||
0x5C) в перекрытии дырки нового кадра НЕ перекрывают старые
|
||||
непрозрачные пиксели → на хвосте остаётся «полумесяц» старого
|
||||
изображения ВНУТРИ bbox-перекрытия, куда L-полоска не достаёт.
|
||||
Точный «новооткрытый» набор = old_opaque AND NOT new_opaque —
|
||||
это heal-с-маской по форме, а не по bbox, на Z80 дороже самого
|
||||
heal. Подтверждено чтением референса docs/samples/balls:
|
||||
restore_bg лечит ПОЛНЫЙ 16×16 (ld b,16 + 16 байт/строку),
|
||||
L-полоску не использует. Итог: heal остаётся full-box; экономия
|
||||
только через batch-пасс (амортизация обвязки), не через L-полоску.
|
||||
L-полоска годна лишь для непрозрачных тайлов фона — не спрайтов.
|
||||
Дисциплина «heal ВСЕ → блит ВСЕ» по-прежнему обязательна (слияние
|
||||
heal+блит per-sprite выкусывает соседа на перекрытии — проверено
|
||||
examples/balls 2026-07-11).
|
||||
- [ ] **span-примитивы для узких прямоугольников**: при узкой стороне
|
||||
≤ 8 линий chunked-rectfill проигрывает простому циклу
|
||||
_bgi_hspan_raw/_bgi_vspan_raw (подготовка+precompute ~340Т уходят
|
||||
@@ -134,6 +217,14 @@ Quick wins:
|
||||
Y..Y+15): в MAME 0.283 НЕ работает (2026-06-01). На железе:
|
||||
dual-write тест → если работает, кэшировать Port_Y в putpixel
|
||||
(~8× меньше OUT для Брезенхэма); если нет — вычистить из доков.
|
||||
- [ ] **Подрежимы видеостраниц #50..#5F** (tests/gfxbanks +
|
||||
tests/sprites, дизайн docs/sprite-api-design.md): перепрогнать
|
||||
на железе и сверить со скриншотами MAME. Три открытых вопроса: (1) 0x58 — полное
|
||||
подавление FF-записи (док/master-MAME) или FF протекает в
|
||||
теневое ОЗУ (MAME 0.283)? (2) FF в VRAM — цвет 255 (MAME) или
|
||||
display-подстановка фона из ОЗУ (тогда дешёвое стирание
|
||||
FF-заливкой, tests/fferase)? (3) accel-путь = CPU-пути (в ПЛМ
|
||||
пути физически разные).
|
||||
- [ ] fdmax: лимит манипуляторов и зависание 9-го OPEN — MAME vs железо.
|
||||
- [ ] CBL: щелчок перед первым проигрыванием звука за сессию (tests/
|
||||
cbltest, tests/cblwav) — воспроизводится ТОЛЬКО на первом запуске
|
||||
|
||||
+22
-4
@@ -190,8 +190,13 @@ wrchar rdchar`; режимы `gettextmode/settextmode` (0x02=40×32,
|
||||
|
||||
Setup: `gfx_init(mode,page)→prev`, `gfx_done(prev)`.
|
||||
Страницы/банк: `gfx_set/get_visible_page`, `gfx_set/get_draw_page`
|
||||
(double buffering), `gfx_set/get_bank` (0x50..0x5F, 0x58 = FF-
|
||||
прозрачность).
|
||||
(double buffering), `gfx_set/get_bank` (GFX_BANK_NORMAL 0x50 /
|
||||
NOSHADOW 0x54 / TRANSPARENT 0x58 / SPRITE 0x5C — аппаратные подрежимы
|
||||
записи, действуют на ВСЕ примитивы; см. docs/sprite-api-design.md).
|
||||
Блиттинг (Фаза B, 2026-07-11): `gfx_blit(x,y,img)`,
|
||||
`gfx_blit_part(x,y,img,sx,sy,w,h)` (атлас), `gfx_heal(x,y,w,h)`
|
||||
(восстановить фон из ОЗУ-копии; стирание спрайтов без save-буфера);
|
||||
img — getimage-формат, буфер вне W3; клиппинг по экрану есть.
|
||||
|
||||
`gfx_wait_vsync()` (2026-07-07, редизайн): ждёт переход бита 5 порта
|
||||
0xFE из 1 в 0 — реальное аппаратное состояние луча (Y>256 → начало
|
||||
@@ -234,8 +239,21 @@ CP), `line(x1,y1,x2,y2)` (не двигает).
|
||||
`fillpoly(n,pts)`, `fillellipse(x,y,xr,yr)`.
|
||||
Заливка областей (Ф2c): `floodfill(x,y,border)` (медленно, но верно),
|
||||
`pieslice(x,y,st,end,r)`, `sector(x,y,st,end,xr,yr)`.
|
||||
Спрайты (Ф2d): `imagesize/getimage/putimage` (COPY/XOR/OR/AND/NOT_PUT;
|
||||
формат буфера: uint16 w,h + w*h байт).
|
||||
Образы (Ф2d): `imagesize/getimage/putimage` (COPY/XOR/OR/AND/NOT_PUT;
|
||||
формат буфера: uint16 w,h + w*h байт). COPY_PUT и getimage — через
|
||||
accel block-copy (2026-07-11, Фаза A sprite-api-design), COPY с
|
||||
клиппингом и текущим банком; XOR/OR/AND/NOT — per-pixel.
|
||||
Спрайты (2026-07-11, Фаза B sprite-api-design): `putsprite(x,y,img)` —
|
||||
блит getimage-буфера с аппаратной прозрачностью (0xFF =
|
||||
GFX_TRANSPARENT не пишется) банком GFX_BANK_SPRITE (0x5C, фон в
|
||||
ОЗУ-копии цел); `movesprite(ox,oy,x,y,img)` — heal старой позиции +
|
||||
putsprite новой (save-буфер не нужен). Низкий уровень в <gfx.h>:
|
||||
`gfx_blit(x,y,img)`, `gfx_blit_part(x,y,img,sx,sy,w,h)` (атлас
|
||||
кадров), `gfx_heal(x,y,w,h)` (восстановление фона из ОЗУ-копии),
|
||||
константы GFX_BANK_NORMAL/NOSHADOW/TRANSPARENT/SPRITE. Правило: фон
|
||||
рисовать банком 0x50, спрайты/оверлеи — putsprite/0x5C; буферы
|
||||
образов — вне W3 (< 0xC000). Тесты: tests/sprites, tests/gfxbanks,
|
||||
tests/bgi_img.
|
||||
Стиль линий (Ф2d): `setlinestyle(style,upattern,thick)`/`getlinesettings`
|
||||
(SOLID/DOTTED/CENTER/DASHED/USERBIT + NORM/THICK) — на line/rectangle/
|
||||
drawpoly.
|
||||
|
||||
@@ -3,13 +3,16 @@ accfill 3772
|
||||
argv 3431
|
||||
assrtest 3847
|
||||
attrprob 4087
|
||||
balls 6393
|
||||
banked 1056
|
||||
bankedbg 1067
|
||||
banklocl 4683
|
||||
banktest 3767
|
||||
bgi_img 6222
|
||||
bgi_img 7060
|
||||
bgitest 3737
|
||||
bios_text 4461
|
||||
blitperf 5860
|
||||
blitw 4939
|
||||
cat 913
|
||||
cblstream 6217
|
||||
cbltest 6366
|
||||
@@ -24,6 +27,7 @@ filetest 10547
|
||||
gets 509
|
||||
gfx_dbuf 5032
|
||||
gfx_demo 4103
|
||||
gfxbanks 6044
|
||||
hello 4167
|
||||
hello2 4244
|
||||
irqtest 5850
|
||||
@@ -39,6 +43,8 @@ rt_test 4892
|
||||
seek 4188
|
||||
simple 955
|
||||
solidt 11531
|
||||
spriteclip 4187
|
||||
sprites 9968
|
||||
stattest 7549
|
||||
stdlib 6643
|
||||
stest2 3644
|
||||
|
||||
|
@@ -0,0 +1,594 @@
|
||||
# Спрайтовое расширение BGI — дизайн
|
||||
|
||||
Статус: дизайн на ревью (2026-07-11). Реализация не начата.
|
||||
|
||||
Расширение libbgi для спрайтовой графики на аппаратных подрежимах
|
||||
видеостраниц `#50..#5F` + переписывание putimage/getimage через
|
||||
акселератор (одно ядро на всё).
|
||||
|
||||
Зафиксированные решения (обсуждены 2026-07-11):
|
||||
1. **Два слоя**: быстрое ядро в `<gfx.h>` (`gfx_*`), тонкие BGI-обёртки
|
||||
в `<graphics.h>` (`putsprite`/`movesprite`, ускоренный `putimage`).
|
||||
2. **Примитивы + move-хелпер**, без managed-движка (таблица спрайтов,
|
||||
z-order — «следующая версия», см. §9).
|
||||
3. **Формат данных — getimage** (uint16 w, uint16 h, пиксели построчно).
|
||||
Единственный in-memory формат; атласы — через блит
|
||||
под-прямоугольника, а не через новый формат.
|
||||
4. **Курсор мыши** — пример + MAME-тест на новых примитивах, НЕ в
|
||||
библиотеке.
|
||||
|
||||
## 1. Аппаратная база
|
||||
|
||||
### 1.1 Подрежимы вывода (Иван Мак §4.3, Architecture)
|
||||
|
||||
Страницы `#50..#5F` — графическая видео-область. Биты 3..0 номера
|
||||
страницы на АДРЕС не влияют (адрес задают PORT_Y `0x89` + 10 младших
|
||||
бит CPU-адреса); биты 2 и 3 задают независимые подрежимы ЗАПИСИ.
|
||||
Биты 0 и 1 обязаны быть 0 (зарезервированы под будущие конфигурации).
|
||||
|
||||
| Банк | bit3 | bit2 | Семантика записи |
|
||||
|-------|------|------|------------------------------------------------------|
|
||||
| 0x50 | 0 | 0 | обычная: в видео-ОЗУ **и** в ОЗУ-копию |
|
||||
| 0x58 | 1 | 0 | байт `0xFF` **не записывается вообще** (прозрачность)|
|
||||
| 0x54 | 0 | 1 | **только** в видео-ОЗУ; ОЗУ-копия не трогается |
|
||||
| 0x5C | 1 | 1 | оба эффекта — режим подвижного спрайта |
|
||||
|
||||
Ключевые факты:
|
||||
|
||||
- **Прозрачность (bit3) — на пути ЗАПИСИ.** «В процессе записи
|
||||
проверяется, не равен ли записываемый байт значению #FF. Если
|
||||
равен, то запись не производится» (официальный док). Проверку
|
||||
делает ПЛМ — блиттеру не нужно смотреть ни одного байта.
|
||||
ОТКРЫТЫЙ вопрос (док молчит): как видеокарта ОТОБРАЖАЕТ байт FF,
|
||||
реально попавший в VRAM (записанный через банк без bit3) — как
|
||||
обычный цвет 255 или подставляет фон из ОЗУ-копии? Проверяется
|
||||
шагом 3 теста Фазы 0; от ответа зависит возможность дешёвого
|
||||
стирания FF-заливкой (tests/fferase).
|
||||
- **Чтение из `#50..#5F` всегда возвращает ОЗУ-копию** — видео-ОЗУ
|
||||
write-only для CPU. Отсюда «heal»: в банке 0x50 прочитать
|
||||
прямоугольник (придёт фон из ОЗУ) и записать обратно (уйдёт в
|
||||
видео-ОЗУ) — фон под спрайтом восстановлен **без save-буфера**.
|
||||
- Подрежимы комбинируются с акселератором (то же оборудование ПЛМ);
|
||||
это их заявленное назначение — «ускорение работы со спрайтовой
|
||||
графикой». Подтвердить в MAME — Фаза 0.
|
||||
|
||||
### 1.2 Accel block-copy (accelerator_doc.txt)
|
||||
|
||||
Режим `LD L,L` (горизонтальная копия): после задания размера блока
|
||||
(`LD D,D` + immediate у `LD A,n` — SMC, см. memory/sprinter_accelerator)
|
||||
триггер `LD A,(HL)` burst-читает блок в память акселератора, триггер
|
||||
`LD (DE),A` burst-пишет его по DE. До 256 байт за выстрел, источник и
|
||||
приёмник — любое ОЗУ `0x0000..0xBFFF` + видеоокно (не ПЗУ/FastRAM).
|
||||
Размер блока переживает `LD B,B` (подтверждено tests/accfill) — задаём
|
||||
один раз на блит, дальше по паре триггеров на строку.
|
||||
|
||||
Канонический референс — `Draw_Restangle_Data` из accelerator_doc.txt:
|
||||
источник (строки подряд, шаг = ширина), приёмник — один и тот же
|
||||
CPU-адрес, строку выбирает PORT_Y (инкремент на строку). **Это ровно
|
||||
layout формата getimage** — совпадение формата и железа буквальное.
|
||||
|
||||
## 2. Одно ядро на четыре операции
|
||||
|
||||
Вся разница между операциями — какой банк в W3 и куда смотрят src/dst:
|
||||
|
||||
| Операция | src → dst | Банк W3 |
|
||||
|-----------------------|-----------------------------|----------------|
|
||||
| `putimage(COPY)` | буфер → экран | текущий (0x50) |
|
||||
| `putsprite` | буфер → экран | 0x5C |
|
||||
| `getimage` | экран (ОЗУ-копия) → буфер | любой |
|
||||
| `gfx_heal` | экран → экран (src == dst) | 0x50 (форс) |
|
||||
|
||||
Отдельного «спрайтового блиттера» нет: прозрачный блит — это обычный
|
||||
блит с банком 0x58/0x5C. heal — это блит, у которого src-указатель
|
||||
лежит в самом видеоокне (чтение вернёт ОЗУ-копию) и совпадает с dst;
|
||||
шаг строк источника = 0, потому что строку и там и там выбирает один
|
||||
PORT_Y.
|
||||
|
||||
## 3. Публичный API
|
||||
|
||||
### 3.1 `<gfx.h>` — ядро
|
||||
|
||||
```c
|
||||
/* Семантические имена банков для gfx_set_bank (значения — номер
|
||||
* страницы W3; биты 0,1 всегда 0 — резерв конфигураций): */
|
||||
#define GFX_BANK_NORMAL 0x50 /* запись в видео-ОЗУ + ОЗУ-копию */
|
||||
#define GFX_BANK_NOSHADOW 0x54 /* только видео-ОЗУ (врем. вывод) */
|
||||
#define GFX_BANK_TRANSPARENT 0x58 /* байт 0xFF не записывается */
|
||||
#define GFX_BANK_SPRITE 0x5C /* NOSHADOW + TRANSPARENT */
|
||||
|
||||
#define GFX_TRANSPARENT 0xFF /* прозрачный цвет (256-режим: пиксель;
|
||||
16-режим: ПАРА пикселей цвета 15) */
|
||||
|
||||
/* Блит картинки getimage-формата в (x,y) ТЕКУЩИМ банком (gfx_set_bank).
|
||||
* Клиппинг по экрану есть (в отличие от putimage Turbo C).
|
||||
* Требование: img вне W3 (адрес < 0xC000) — W3 занят видеобанком. */
|
||||
void gfx_blit(int x, int y, const void *img);
|
||||
|
||||
/* Блит под-прямоугольника картинки (атлас кадров): прямоугольник
|
||||
* (sx,sy,w,h) внутри img выводится в (x,y). Под-прямоугольник обязан
|
||||
* лежать внутри img (по экрану — клиппится). */
|
||||
void gfx_blit_part(int x, int y, const void *img,
|
||||
int sx, int sy, int w, int h);
|
||||
|
||||
/* Восстановить прямоугольник экрана из ОЗУ-копии (стирание спрайта/
|
||||
* оверлея). Всегда работает банком 0x50 независимо от gfx_set_bank.
|
||||
* Клиппится по экрану. */
|
||||
void gfx_heal(int x, int y, int w, int h);
|
||||
```
|
||||
|
||||
Заметки по семантике:
|
||||
|
||||
- `gfx_blit*` уважают текущий `gfx_set_bank` — так же, как уже сегодня
|
||||
его уважают ВСЕ примитивы через `_bgi_begin` (т.е. `gfx_set_bank
|
||||
(GFX_BANK_NOSHADOW); line(...)` — легальный способ рисовать временную
|
||||
линию-перекрестье). Спрайтовые обёртки (§3.2) ставят банк сами на
|
||||
время вызова и восстанавливают.
|
||||
- Клиппинг в ядре всегда, в т.ч. в fast-версии: это не валидация
|
||||
параметров, а функциональность (спрайты штатно уходят за края).
|
||||
Цена — один расчёт на блит, не на пиксель.
|
||||
- Цвет 255 через 0x58/0x5C нарисовать нельзя — он и есть прозрачный.
|
||||
В initgraph палитра 0..15 занята EGA, 255 рекомендуется не занимать.
|
||||
|
||||
### 3.2 `<graphics.h>` — BGI-обёртки
|
||||
|
||||
```c
|
||||
/* Вывести спрайт (getimage-формат, 0xFF = прозрачно) в (x,y).
|
||||
* Банк на время вызова — GFX_BANK_SPRITE (0x5C): прозрачные точки не
|
||||
* пишутся, фон в ОЗУ-копии не портится → стирается gfx_heal'ом /
|
||||
* movesprite'ом. Клиппится по экрану. */
|
||||
void putsprite(int x, int y, const void *img);
|
||||
|
||||
/* Переместить спрайт: heal прямоугольника (oldx,oldy,w,h) по размерам
|
||||
* img + putsprite в (x,y). Порядок heal→draw; перекрытие старой и
|
||||
* новой позиций безопасно (heal берёт фон из ОЗУ-копии). */
|
||||
void movesprite(int oldx, int oldy, int x, int y, const void *img);
|
||||
```
|
||||
|
||||
`putimage`/`getimage`/`imagesize` — сигнатуры и формат без изменений,
|
||||
`putimage(COPY_PUT)` и `getimage` переезжают на accel-ядро (§5).
|
||||
|
||||
Хотспот и кадры атласа НЕ кодируются в данных — это аргументы вызова:
|
||||
`putsprite(x - HOT_X, y - HOT_Y, img)`;
|
||||
кадр N — `gfx_blit_part(x, y, sheet, n*FRAME_W, 0, FRAME_W, FRAME_H)`
|
||||
(банк выставить `GFX_BANK_SPRITE` вокруг — или см. §9 про
|
||||
`putsprite_part`).
|
||||
|
||||
## 4. Внутренности
|
||||
|
||||
### 4.1 Leaf-примитив (per-driver, asm)
|
||||
|
||||
Один новый leaf в `bgi256/` (и позже `bgi16/`), объявление в `_bgi.h`:
|
||||
|
||||
```c
|
||||
/* Копирование h строк по w пикселей через accel block-copy.
|
||||
* dst/src — CPU-адреса ПЕРВОЙ строки; dstride/sstride — шаг адреса
|
||||
* между строками (0 = адрес не двигается, строку выбирает PORT_Y —
|
||||
* сторона, живущая в видеоокне); y0 — стартовый PORT_Y (инкремент
|
||||
* на строку). Raw: без клиппинга, W3 уже замаплен (_bgi_begin),
|
||||
* вызывающий гарантирует 1 <= w <= 256 (полоса).
|
||||
* DI/EI — вокруг каждой пары триггеров (как в fill-сегментах). */
|
||||
void _bgi_copy_rows_raw(uint8_t *dst, const uint8_t *src,
|
||||
uint8_t w /*0=256*/, uint8_t h /*0=256*/,
|
||||
int dstride, int sstride, uint8_t y0);
|
||||
```
|
||||
|
||||
Схема тела (референс — `Draw_Restangle_Data` + существующий SMC-паттерн
|
||||
`_gfx_hfill256_segment`):
|
||||
|
||||
```
|
||||
SMC: размер блока <- w (LD D,D ; LD A,#imm ; LD B,B — один раз)
|
||||
loop h раз:
|
||||
out (0x89), y ; y++
|
||||
di
|
||||
LD L,L ; режим копии
|
||||
LD A,(HL) ; burst-чтение src
|
||||
LD (DE),A ; burst-запись dst
|
||||
LD B,B ; стоп
|
||||
ei
|
||||
HL += sstride ; DE += dstride
|
||||
```
|
||||
|
||||
Один leaf покрывает все четыре операции §2:
|
||||
|
||||
| Вызов | dst, dstride | src, sstride |
|
||||
|--------------|-------------------------|----------------------------|
|
||||
| блит | экран (base+x), 0 | буфер (row0), img_w |
|
||||
| getimage | буфер, w | экран (base+x), 0 |
|
||||
| heal | экран (base+x), 0 | тот же адрес экрана, 0 |
|
||||
|
||||
Регистровая раскладка — на этапе реализации; ориентир: HL=src, DE=dst
|
||||
внутри цикла (триггеры именно такие), w через SMC, остальное со стека
|
||||
в локальные регистры/IX (вызывается один-два раза на блит — не горячий
|
||||
ABI, в отличие от fill-сегментов).
|
||||
|
||||
### 4.2 Ядро (common/)
|
||||
|
||||
`gfx_blit_part` — единственная «умная» функция (реальная логика):
|
||||
|
||||
1. прочитать w,h из заголовка img (для gfx_blit: весь rect);
|
||||
2. клиппинг: x<0 → сдвиг sx и сужение w; правый/нижний край → сужение;
|
||||
пустой результат → выход;
|
||||
3. `src0 = img + 4 + sy*img_w + sx`;
|
||||
4. полосы: если w > 256 — разрезать на вертикальные полосы ≤ 256 байт
|
||||
(при экране 320 полос максимум две; каждая полоса = один SMC размера
|
||||
блока);
|
||||
5. `_bgi_begin()` → `_bgi_copy_rows_raw(...)` на полосу → `_bgi_end()`.
|
||||
|
||||
Остальное — тонкие модули (1 функция = 1 .rel):
|
||||
|
||||
- `gfx_blit.c` — читает w,h, зовёт gfx_blit_part(x,y,img,0,0,w,h);
|
||||
- `gfx_heal.c` — клип; банк: сохранить `_gfx_bank`, форс 0x50,
|
||||
`_bgi_begin`; leaf с dst=src=base+x, strides 0; восстановить банк;
|
||||
- `putsprite.c` — сохранить `_gfx_bank`, `|` → 0x5C, `gfx_blit`,
|
||||
восстановить (именно `save/restore`, а не тупо 0x50 — уважаем
|
||||
вложенность и пользовательский temp-режим);
|
||||
- `movesprite.c` — `gfx_heal(oldx,oldy,w,h)` (w,h из заголовка img) +
|
||||
`putsprite(x,y,img)`.
|
||||
|
||||
### 4.3 Совместимость с существующим кодом
|
||||
|
||||
- `_bgi_begin/_bgi_end` не меняются (банк уже берут из `_gfx_bank`).
|
||||
- `gfx_set_bank/gfx_get_bank` не меняются; в `gfx.h` добавляются только
|
||||
`GFX_BANK_*`/`GFX_TRANSPARENT` и три прототипа.
|
||||
- Довесок к контракту (задокументировать у putimage/getimage тоже):
|
||||
буферы картинок обязаны лежать вне W3 (`< 0xC000`) — на время
|
||||
операции W3 замаплен на видеобанк. Это верно и сегодня (per-pixel
|
||||
путь), просто не было записано.
|
||||
- Второй draw-page: ядро использует `_gfx_addr_base` — двойная
|
||||
буферизация работает автоматически.
|
||||
|
||||
## 5. Переезд putimage/getimage на ядро
|
||||
|
||||
- `putimage(COPY_PUT)` → `gfx_blit` (текущим банком — поведение
|
||||
обратно-совместимо: дефолтный банк 0x50).
|
||||
- `getimage` → `_bgi_copy_rows_raw` (dst=буфер). Клиппинга нет, как в
|
||||
BGI (контракт: rect валиден); safe-версия сохраняет текущие проверки.
|
||||
- `XOR/OR/AND/NOT_PUT` — остаются на per-pixel пути (v1). У
|
||||
акселератора есть блочные AND/OR/XOR (пример «encode» в
|
||||
accelerator_doc.txt) — ускорение этих op — «следующая версия» (§9).
|
||||
- `imagesize` — без изменений (256-режим); для 16-режима станет
|
||||
mode-specific (§8).
|
||||
|
||||
Ожидание по скорости: per-pixel путь платит на КАЖДЫЙ пиксель
|
||||
`out Port_Y` + пересчёт адреса + bounds-check (~50–80Т); ядро платит на
|
||||
СТРОКУ ~40–60Т CPU + burst акселератора (~байт/7МГц). На спрайте 16×16
|
||||
это порядка 10–20× (замерить в Фазе A, добавить в size-baseline).
|
||||
|
||||
## 6. Паттерны использования (войдут в libc-reference)
|
||||
|
||||
Подвижный спрайт (канон):
|
||||
|
||||
```c
|
||||
initgraph();
|
||||
/* фон рисуем обычным банком — он попадает и в ОЗУ-копию (бэкап) */
|
||||
draw_background();
|
||||
putsprite(x, y, hero); /* 0x5C: фон в ОЗУ цел */
|
||||
while (game) {
|
||||
int nx = x + dx, ny = y + dy;
|
||||
gfx_wait_vsync();
|
||||
movesprite(x, y, nx, ny, hero); /* heal старого + блит нового */
|
||||
x = nx; y = ny;
|
||||
}
|
||||
```
|
||||
|
||||
Курсор мыши (пример examples/, не API): то же самое с
|
||||
`GFX_BANK_SPRITE`; фон под курсором живёт в ОЗУ-копии, никакой
|
||||
getimage/буфер не нужен. Перерисовка — из главного цикла по
|
||||
`mouse_getxy()`.
|
||||
|
||||
Впечатать спрайт в фон навсегда (декорация):
|
||||
`gfx_set_bank(GFX_BANK_TRANSPARENT); gfx_blit(...); gfx_set_bank(GFX_BANK_NORMAL);`
|
||||
— 0x58 без bit2: спрайт уходит и в ОЗУ-копию, heal его уже «не сотрёт».
|
||||
ВНИМАНИЕ: на MAME 0.283 этот паттерн ломается — FF-байты спрайта
|
||||
попадают в ОЗУ-копию (частичный скип, см. результаты Фазы 0);
|
||||
до подтверждения на железе печатать декорации без FF в данных.
|
||||
|
||||
ВАЖНО про cleardevice/bar поверх спрайтов: пока действует банк с bit2
|
||||
(0x54/0x5C), «фоновые» операции не обновляют ОЗУ-копию. Правило:
|
||||
сцена/фон — только обычным банком (или 0x58), спрайты/оверлеи — 0x5C.
|
||||
|
||||
## 7. План работ
|
||||
|
||||
- **Фаза 0 — верификация подрежимов** (`tests/gfxbanks`).
|
||||
ВНИМАНИЕ: поведение подрежимов в MAME может отличаться от реального
|
||||
устройства (прецедент — Port_Y banking, memory/gfx_port_y_banking).
|
||||
Результат в MAME НЕ финален: тест гоняется и в MAME, и на железе;
|
||||
до прогона на железе вердикты считаются предварительными.
|
||||
Последовательный сценарий на одном экране, скриншот после каждого
|
||||
шага (палитра[255] = синий; фон — узнаваемый паттерн, не заливка):
|
||||
1. **Фон** через 0x50 (уходит и в VRAM, и в ОЗУ-копию).
|
||||
2. **Спрайт через 0x5C** (bit2+bit3): квадрат, внутри зелёный круг,
|
||||
вокруг круга — байты 0xFF. Ожидание по доку: виден круг поверх
|
||||
фона, FF-точки скипнуты (фон вокруг круга цел). Проверяет
|
||||
bit3-скип на CPU-пути; если вместо фона вокруг круга синяя рамка —
|
||||
bit3 не эмулируется/не работает.
|
||||
3. **Ключевой шаг — FF реально в VRAM**: через 0x54 (bit2, БЕЗ bit3 —
|
||||
скипа нет, FF пишется) залить квадрат байтом 0xFF поверх круга.
|
||||
Что отображается в квадрате:
|
||||
- **синий** → FF в VRAM — обычный цвет 255, подстановки при
|
||||
отображении нет (семантика дока полная) → стирание только heal;
|
||||
- **фон** → видеокарта подставляет байт из ОЗУ-копии, когда в
|
||||
VRAM лежит FF → «слой спрайтов» существует на уровне
|
||||
отображения, дешёвое стирание FF-заливкой работает
|
||||
(tests/fferase становится штатным путём);
|
||||
- **круг остался** → запись через 0x54 не произошла — bit2/банк
|
||||
не эмулируется.
|
||||
4. **Heal** через 0x50 (чтение+запись того же rect): фон должен
|
||||
восстановиться полностью. Заодно различает bit2: если bit2 не
|
||||
работал, шаги 2–3 испортили ОЗУ-копию и heal вернёт не фон.
|
||||
5. **Accel-путь**: повторить шаги 2–3 записью через акселератор
|
||||
(copy/fill) — работает ли bit3-скип и поведение FF на burst-пути.
|
||||
Если MAME не эмулирует биты 2/3 — API остаётся как есть (семантика
|
||||
задана железом/доком), но функциональные тесты уезжают в раздел
|
||||
«проверить на железе» TODO, а пример курсора делаем без bit2
|
||||
(fallback: save/restore через getimage).
|
||||
|
||||
**РЕЗУЛЬТАТЫ (MAME 0.283 = сборка v306, 2026-07-11; железо — TODO):**
|
||||
| Вопрос | Вердикт в MAME |
|
||||
|---|---|
|
||||
| bit2 (0x54/0x5C): ОЗУ-копия не трогается | РАБОТАЕТ (A: PASS, B: PASS) |
|
||||
| bit3 через 0x5C: скип FF | РАБОТАЕТ, полный no-op (CPU и accel) |
|
||||
| bit3 через 0x58: скип FF | **ЧАСТИЧНО**: VRAM скипается, но теневое ОЗУ ПОЛУЧАЕТ FF (C: FAIL) — код драйвера 0.283 гасит только vram_w; в master-драйвере исправлено на полное подавление (по доку) |
|
||||
| Отображение FF в VRAM (шаг 3) | обычный **цвет 255** (синий), подстановки ОЗУ нет → стирание = heal; FF-заливка НЕ работает |
|
||||
| heal (чтение ОЗУ + запись 0x50) | РАБОТАЕТ (полосы восстановлены; FF-порчу ОЗУ честно переносит в VRAM) |
|
||||
| accel-путь vs CPU-путь | идентичны во всех подрежимах (в MAME оба через ram_w) |
|
||||
|
||||
Следствия: (1) putsprite через 0x5C работает одинаково во всех
|
||||
версиях — расхождение 0.283 не задевает; (2) паттерн «впечатать
|
||||
декорацию через 0x58» на 0.283 портит ОЗУ-копию FF-байтами спрайта —
|
||||
использовать только после проверки на железе; (3) tests/fferase в
|
||||
MAME заведомо FAIL — остаётся для прогона на железе.
|
||||
Квирк инфраструктуры: delayms/sleep в --memory small не работают
|
||||
(irq_install → EINVAL, калибровка молча не происходит) — паузы в
|
||||
тесте сделаны по RTC (getdatetime).
|
||||
- **tests/fferase — альтернативное стирание FF-заливкой через 0x54**
|
||||
(существует ДО подтверждения итога шага 3 и на MAME, И на железе;
|
||||
после — либо удаляется, либо становится основой оптимизации).
|
||||
Сценарий: фон → спрайт через 0x5C → залить прямоугольник спрайта
|
||||
байтом 0xFF через 0x54 (bit3=0 — FF реально пишется в VRAM) →
|
||||
скриншот: восстановился ли фон. Если да (на железе!) — стирание
|
||||
вдвое дешевле heal (fill-burst без фазы чтения, готовый
|
||||
_gfx_rectfill256), реализацию gfx_heal можно переключить, сигнатура
|
||||
и movesprite не меняются. Основной механизм move в любом случае —
|
||||
heal (чтение/запись через 0x50): он работает при ОБЕИХ семантиках.
|
||||
- **Фаза A — ядро + переезд** (без нового публичного API, чистое
|
||||
ускорение): `_bgi_copy_rows_raw` (bgi256), `gfx_blit_part` + полосы +
|
||||
клиппинг, порт putimage(COPY)/getimage. Регресс — tests/bgitest +
|
||||
`make size-check`.
|
||||
|
||||
**ГОТОВО 2026-07-11**: leaf `bgi256/_bgi_copy_rows_raw.c` (регистры
|
||||
BC = счётчик строк/y, stride-сложения через push bc; SMC размера
|
||||
блока один раз на вызов), ядро `common/gfx_blit_part.c` (клиппинг
|
||||
всегда, sy без __mulint — циклом сложений, полосы ≤256), putimage
|
||||
(COPY_PUT → ядро; XOR/OR/AND/NOT — прежний per-pixel путь) и
|
||||
getimage (grab-leaf, dstride = w) переведены. Прототип
|
||||
gfx_blit_part пока в _bgi.h (Фаза B перенесёт в gfx.h как есть).
|
||||
Проверено в MAME: tests/bgi_img финальный кадр 1:1 с per-pixel
|
||||
эталоном (src == COPY, XOR×2 чист); tests/gfxbanks — прозрачность
|
||||
FF и bit2 работают на accel-COPY пути идентично CPU-пути (круг с
|
||||
прозрачными полями через putimage/0x5C, FF→VRAM через putimage/0x54).
|
||||
Размер: bgi_img +592 Б (_CODE; leaf + клиппинг-ядро + ветка COPY),
|
||||
baseline принят. Попутно: pgrep-фильтр в mame_interactive.py сужен
|
||||
до эмулятора (ложно срабатывал на параллельную сборку MAME из
|
||||
исходников).
|
||||
- **Фаза B — спрайтовый API**: GFX_BANK_*, gfx_blit, gfx_heal,
|
||||
putsprite, movesprite; тест tests/sprites (анимация по синусоиде
|
||||
поверх пёстрого фона, скриншоты «фон не разрушен»).
|
||||
|
||||
**ГОТОВО 2026-07-11**: константы GFX_BANK_*/GFX_TRANSPARENT и
|
||||
gfx_blit/gfx_blit_part/gfx_heal — в gfx.h; putsprite/movesprite — в
|
||||
graphics.h; модули common/gfx_blit.c, gfx_heal.c, putsprite.c,
|
||||
movesprite.c (тонкие, поверх ядра Фазы A). putsprite/gfx_heal
|
||||
сохраняют и восстанавливают пользовательский банк (heal — форс
|
||||
0x50, putsprite — форс 0x5C на время вызова). Проверено в MAME
|
||||
(tests/sprites): прозрачные углы, клиппинг во все 4 края,
|
||||
heal-стирание (accel src==dst — риск §10.4 закрыт), movesprite
|
||||
12 шагов с чистым следом, кадр атласа через gfx_blit_part; ОЗУ-копия
|
||||
цела (проверки A/B PASS). size-check: роста существующих программ
|
||||
нет (новые модули тянутся только пользователями API).
|
||||
|
||||
Демо **examples/balls** (2026-07-11): 8 разноцветных шаров 16×16
|
||||
(по спрайту на цвет, прозрачные углы) над чёрно-белой шахматкой;
|
||||
скорости 1..4 привязаны к кадрам (gfx_wait_vsync, speed = кадров на
|
||||
шаг: 50/25/~17/12.5 px/с — подтверждено покадровыми скриншотами),
|
||||
отражение от краёв, Esc — выход. **Двойная буферизация** по образцу
|
||||
docs/samples/balls (реф. asm-демо Sprinter): рисование только на
|
||||
скрытой странице (heal ВСЕХ старых позиций этой страницы → блит
|
||||
ВСЕХ шаров), флип по vsync — одиночная страница мерцала (heal+блит
|
||||
на видимой ловились лучом; пользователь заметил, реф не мерцает).
|
||||
Требует зеркалирования палитры в палитру 1 (initgraph грузит EGA
|
||||
только в 0) и координат drawn[2][N] per-page. Побочно ушли и
|
||||
одно-кадровые артефакты перекрытий/снапшотов.
|
||||
- **Фаза C — пример курсора** (examples/): мышь + putsprite/gfx_heal,
|
||||
он же живой тест temp-режима.
|
||||
- Документация: libc-reference (раздел «Спрайты»), обновить TODO.
|
||||
|
||||
## 8. Режим 16 цветов (Фаза 2 libbgi — заметки на будущее)
|
||||
|
||||
- Прозрачная единица — БАЙТ = пара пикселей цвета 15 (0xFF). Фигурные
|
||||
края спрайта квантуются парами; прозрачного «одиночного» пикселя нет.
|
||||
- Leaf `bgi16/_bgi_copy_rows_raw`: те же триггеры, адресация x/2;
|
||||
клиппинг и координаты x — по чётным границам (нечётные края потребуют
|
||||
RMW-кромок per-row, v1 16-режима: требовать чётные x/w).
|
||||
- `imagesize`/формат: данные packed (2 пикселя/байт) — imagesize станет
|
||||
mode-specific leaf'ом.
|
||||
|
||||
## 9. Не в этой версии (кандидаты в следующую)
|
||||
|
||||
- **Managed-движок**: см. эскиз §9.1 (sprite_t + retained-модель).
|
||||
- **putsprite_part** (кадр атласа одним вызовом, без ручного банка) —
|
||||
добавить, как только появится первый пользователь-игра.
|
||||
- **.spr-ресурс**: файловый заголовок-обёртка (магия/версия/hotspot)
|
||||
вокруг getimage-payload + загрузчик + PC-конвертер (PNG→spr, iconv-
|
||||
стиль пайплайн). In-memory формат не меняется.
|
||||
- **XOR/OR/AND_PUT через акселератор** (блочные режимы есть в железе).
|
||||
- **RLE-сжатие** (распаковка при загрузке — фича загрузчика).
|
||||
- **Вертикальный блит** (`LD A,A`-режим) — для повёрнутых спрайтов не
|
||||
хватает и его; не тянем.
|
||||
- **ISR-курсор** (перерисовка из IM2-тика) — после Audio/IM2 v2.
|
||||
|
||||
## 9.1 Эскиз managed-движка: sprite_t + retained-модель (v2, НЕ реализовано)
|
||||
|
||||
Мотивация — опыт examples/balls (2026-07-11): при двойной буферизации
|
||||
приложение обязано помнить, где каждый спрайт РЕАЛЬНО нарисован на
|
||||
КАЖДОЙ из двух страниц (drawn[2][N]), и соблюдать двухпроходную
|
||||
дисциплину «heal все → блит все». Оба правила легко нарушить (heal по
|
||||
позиции прошлого кадра вместо позапрошлого → фон зарастает кромками;
|
||||
слияние heal+блит per-sprite → укусы на перекрытиях — проверено
|
||||
экспериментально). Этот учёт — работа библиотеки, не приложения.
|
||||
|
||||
Разбор asm-референса (docs/samples/balls, 2026-07-12) подтвердил
|
||||
направление: наш ~2× разрыв — не W3-скобка (_bgi_begin = 5 инструкций),
|
||||
а per-call C-обвязка (клип 528/362 Б, парс заголовка, save/restore
|
||||
банка, SDCC-фрейм 7-арг), повторяемая 2×N раз. Референс платит это
|
||||
ОДИН раз на проход: W3 замаплен на видео на всю программу, банк-подрежим
|
||||
ставит один `out` на проход (0x5C рисовать всё → флип → 0x50 лечить
|
||||
всё), клипа нет вообще (адрес спрайта = сдвиги), heal — полный 16×16
|
||||
(restore_bg). Движок v2 повторяет ту же per-pass структуру (одна
|
||||
W3-скобка + один банк + клип-fast-path на проход; w,h из кэша структуры).
|
||||
ЕДИНСТВЕННОЕ отличие, которое НЕ копируем: референс держит DI на весь
|
||||
проход (32 шара) — у него нет аудио-ISR; у нас DI остаётся гранулярным
|
||||
(по строке в leaf), иначе длинный DI сорвёт CBL/IM2-звук.
|
||||
|
||||
### Структура (черновик)
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
const void *img; /* getimage-формат / атлас-лента */
|
||||
int x, y; /* ЛОГИЧЕСКАЯ позиция (куда хочет) */
|
||||
int sx, sy; /* кадр атласа (под-прямоугольник img) */
|
||||
uint8_t w, h; /* размер кадра, 0 = 256 (кэш заголовка) */
|
||||
uint8_t flags; /* bit0 SPR_VISIBLE; bit1/2 dirty[page] */
|
||||
/* --- внутреннее (владеет движок) --- */
|
||||
struct {
|
||||
int x, y; /* где нарисован на странице p */
|
||||
uint8_t on; /* нарисован ли вообще */
|
||||
} drawn[2];
|
||||
} sprite_t; /* ~24 байта на спрайт */
|
||||
```
|
||||
|
||||
Инварианты: `img` живёт, пока спрайт активен; размер кадра постоянен
|
||||
(смена кадра = смена sx/sy в той же ленте); прозрачность — 0xFF в
|
||||
данных, как везде.
|
||||
|
||||
### API (черновик; префикс sprite_, заголовок <sprite.h> в libbgi)
|
||||
|
||||
```c
|
||||
void sprite_init (sprite_t *s, const void *img); /* w,h из заголовка,
|
||||
невидим, не рисует */
|
||||
void sprite_move (sprite_t *s, int x, int y); /* O(1): только state */
|
||||
void sprite_frame(sprite_t *s, int sx, int sy); /* кадр атласа, O(1) */
|
||||
void sprite_show (sprite_t *s); /* O(1) */
|
||||
void sprite_hide (sprite_t *s); /* O(1) */
|
||||
void sprite_touch(sprite_t *s); /* O(1): принудительная
|
||||
перерисовка на обеих
|
||||
страницах (dirty) */
|
||||
/* flags: SPR_ALWAYS — перерисовывать каждый кадр (эквивалент touch
|
||||
* на каждом кадре; для статики, над которой постоянно ходят). */
|
||||
|
||||
/* Вся реальная работа — один вызов на кадр. Рисует на ТЕКУЩЕЙ
|
||||
* draw-странице p (для дабл-буфера вызывать после
|
||||
* gfx_set_draw_page(hidden); для одиночной страницы/курсора — прямо
|
||||
* на видимой):
|
||||
* проход 1: heal всех, у кого drawn[p].on и (скрыт ИЛИ сместился
|
||||
* относительно drawn[p] ИЛИ dirty[p]);
|
||||
* проход 2: блит всех видимых из них же (gfx_blit_part через 0x5C)
|
||||
* в порядке массива (индекс = z-order, последний сверху);
|
||||
* обновить drawn[p]/dirty[p].
|
||||
* Спрайты, не менявшиеся с прошлого визита этой страницы (и без
|
||||
* touch/SPR_ALWAYS), не трогаются. */
|
||||
void sprite_update(sprite_t *arr, uint8_t count);
|
||||
|
||||
/* Сахар поверх update для канонического дабл-буфер-кадра:
|
||||
* set_draw_page(hidden) + sprite_update + gfx_wait_vsync +
|
||||
* set_visible_page(hidden). Приложение, рисующее свой HUD/фон,
|
||||
* зовёт составные части само (HUD банком 0x50 НА СКРЫТОЙ странице —
|
||||
* тогда heal учитывает его автоматически через ОЗУ-копию). */
|
||||
void sprite_flip(sprite_t *arr, uint8_t count);
|
||||
```
|
||||
|
||||
Пример (balls сжимается до):
|
||||
|
||||
```c
|
||||
sprite_t sp[N];
|
||||
... sprite_init/sprite_move/sprite_show ...
|
||||
for (;;) {
|
||||
for (i...) sprite_move(&sp[i], new_x, new_y); /* физика */
|
||||
sprite_flip(sp, N); /* весь кадр */
|
||||
}
|
||||
```
|
||||
|
||||
### Решения и границы
|
||||
|
||||
- **Retained-модель**: move/frame/show/hide меняют только state (O(1),
|
||||
сколько угодно раз за кадр) — рисование один раз, в update. Это
|
||||
железно закрепляет дисциплину двух проходов внутри библиотеки.
|
||||
- **Пересечения со статикой — ответственность приложения** (touch /
|
||||
SPR_ALWAYS), автодетекта НЕТ — осознанно: heal активного спрайта
|
||||
выкусывает статичный под ним, но автоматическое обнаружение — это
|
||||
не только O(N²) rect-проверок; для корректного z-порядка пришлось бы
|
||||
перерисовывать и пересекающих НОВЫЕ позиции, а перерисовка статика
|
||||
полным блитом затирает спрайты выше него уже ВНЕ грязной зоны →
|
||||
транзитивный каскад либо клиппинг блита по произвольной области.
|
||||
На Z80 это дороже самого рисования; приложение же знает сцену и
|
||||
помечает дёшево (обсуждено с пользователем 2026-07-11: ручное
|
||||
назначение «активных» выгоднее координатных проверок).
|
||||
- **Массив, а не handle-таблица**: владеет приложение, без malloc и
|
||||
лимитов; z-order = индекс. Пересортировка z = перестановка массива
|
||||
(drawn-состояние едет вместе со структурой — безопасно).
|
||||
- **Интеграция со страницами**: движок НЕ владеет флипом (sprite_flip
|
||||
лишь сахар) — приложение может рисовать динамический фон/HUD на
|
||||
скрытой странице до sprite_update. Правило прежнее: всё «фоновое» —
|
||||
банком 0x50, спрайты — движком.
|
||||
- **Оптимизация внутри update, невидимая снаружи**: объединение
|
||||
heal-прямоугольников соседей и т.п. — меняется реализация update,
|
||||
не API. ВНИМАНИЕ: «heal только открывшейся L-полоски» для
|
||||
ПРОЗРАЧНЫХ спрайтов НЕ годится (отвергнуто 2026-07-12): в зоне
|
||||
перекрытия старой и новой позиций дырки нового кадра не перекрывают
|
||||
старые непрозрачные пиксели → трейлинг-«полумесяц» остаётся внутри
|
||||
bbox-перекрытия, куда L-полоска не достаёт. Точный набор для heal =
|
||||
old_opaque AND NOT new_opaque (маска по форме, дороже full-box heal).
|
||||
Референс docs/samples/balls (restore_bg) лечит полный 16×16 — full-box
|
||||
heal обязателен. Экономия — только амортизация обвязки (batch-пасс).
|
||||
- Вне скоупа v2: скроллящийся фон (инвалидирует ОЗУ-копию целиком —
|
||||
другой движок), ISR-перерисовка (курсор из IM2 — после Audio/IM2),
|
||||
коллизии (у приложения есть x/y — пусть считает само).
|
||||
|
||||
## 9а. Квирк CPU-байта write-триггера accel-копии (найден 2026-07-11)
|
||||
|
||||
Триггер записи `LD (DE),A` — реальная CPU-инструкция: её собственный
|
||||
цикл записи кладёт байт A в dst[0] ДО burst'а FSM. После burst-чтения
|
||||
`LD A,(HL)` в A остаётся ПОСЛЕДНИЙ байт строки. При банке 0x50 утечка
|
||||
невидима (burst перезаписывает dst[0] правильным src[0]), но при
|
||||
0x58/0x5C, если src[0] == 0xFF, перезапись скипается — на экране
|
||||
вертикальная полоса цвета последних байтов строк (симптом: клипнутые
|
||||
спрайты в tests/sprites). Нейтрализовано в `_bgi_copy_rows_raw`:
|
||||
src[0] предчитывается до армирования и подставляется в A через
|
||||
EX AF,AF' перед триггером — CPU-байт становится src[0], корректным при
|
||||
любом банке и любой семантике первого цикла. Регресс: tests/blitw
|
||||
(визуальная секция «trig leak»). Подтверждено в MAME 0.283; на железе
|
||||
проверить вместе с остальным (полоса цвета — маркер, что железо делает
|
||||
так же; чистый col0 — что CPU-цикл триггера подавляется ПЛМ).
|
||||
|
||||
## 10. Риски / что проверить артефактом (Фаза 0)
|
||||
|
||||
Статус 2026-07-11: пп. 1–3 закрыты для MAME 0.283 (tests/gfxbanks,
|
||||
результаты в §7); на железе — всё ещё TODO.
|
||||
|
||||
1. ~~Эмулирует ли MAME биты 2/3~~ — эмулирует (с квирком 0x58: FF
|
||||
протекает в теневое ОЗУ; в master-драйвере исправлено). Железо —
|
||||
проверить.
|
||||
2. ~~Отображение байта FF в VRAM~~ — в MAME обычный цвет 255,
|
||||
подстановки нет → стирание = heal. Железо может отличаться —
|
||||
tests/fferase остаётся до прогона на реальном Sprinter.
|
||||
3. ~~Подавление 0xFF на accel-пути~~ — в MAME accel-путь идентичен
|
||||
CPU-пути (оба через ram_w). Железо — проверить (в ПЛМ пути
|
||||
физически разные).
|
||||
4. ~~Копия accel'ом при src в видеоокне и при src == dst~~ — оба
|
||||
подтверждены: getimage через accel (образ 1:1, tests/bgi_img) и
|
||||
gfx_heal (стирание чисто, tests/sprites S2/S3). Железо — как всё
|
||||
остальное, перепроверить.
|
||||
5. `LD A,(nn)`/`LD A,(HL)` как источник размера блока (quick-win из
|
||||
TODO) — если работает, SMC в leaf'е не нужен.
|
||||
6. Долгие burst'ы и IM2/CBL: пара триггеров на строку под DI — бюджет
|
||||
как у fill (docs/accel-fill-budget.md), пересчитать для copy.
|
||||
@@ -0,0 +1,4 @@
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||
EXAMPLE := balls
|
||||
EXTRA_FLAGS ?= --gfx 256
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* balls — демо спрайтового движка v2 (<sprite.h>): N шаров 16×16 летают
|
||||
* над чёрно-белой шахматкой, отражаясь от краёв. Esc — выход.
|
||||
*
|
||||
* РАНЬШЕ (до движка) приложение вело drawn[2][N] координаты вручную и
|
||||
* само соблюдало дисциплину «heal ВСЕ → блит ВСЕ» на скрытой странице.
|
||||
* Теперь это работа движка: sprite_move меняет только состояние,
|
||||
* sprite_flip(sp, N) за кадр делает draw-page=скрытая → heal все старые →
|
||||
* блит все видимые (одна W3-скобка + один банк на проход) → vsync → флип.
|
||||
* Учёт drawn[2] спрятан в sprite_t. Профиль/дизайн:
|
||||
* docs/sprite-api-design.md §9.1.
|
||||
*
|
||||
* BOUNDARY-ТЕСТ клипа: диапазон движения выходит ЗА края экрана на
|
||||
* половину спрайта — шары штатно наполовину уходят за все 4 края,
|
||||
* включая ОТРИЦАТЕЛЬНЫЕ x/y слева/сверху. Клип _gfx_blit_clip (общий
|
||||
* для putsprite и движка) обязан авто-обрезать: правый/нижний — урезать
|
||||
* w/h, левый/верхний — сдвинуть начало источника (sx/sy) и урезать.
|
||||
*
|
||||
* Двойная буферизация: рисуем на скрытой странице, флип по vsync (иначе
|
||||
* heal+блит на видимой ловятся лучом — мерцание). Палитру страницы 1
|
||||
* зеркалим из 0 (initgraph грузит EGA только в палитру 0).
|
||||
*
|
||||
* Скорость привязана к КАДРАМ (50 Гц): один проход цикла = один шаг.
|
||||
*/
|
||||
|
||||
#include <graphics.h>
|
||||
#include <gfx.h>
|
||||
#include <sprite.h>
|
||||
#include <conio.h>
|
||||
#include <time.h>
|
||||
|
||||
#define FPS_METER 1 /* 1 = счётчик кадров/с слева-сверху (диагностика) */
|
||||
|
||||
#define NBALLS 16 /* количество шаров */
|
||||
#define BW 16 /* сторона спрайта */
|
||||
#define CELL 16 /* клетка шахматки */
|
||||
|
||||
/* Диапазон отражения — на пол-спрайта ЗА каждым краем (boundary-тест):
|
||||
* x ∈ [-8 .. 312], y ∈ [-8 .. 248]. */
|
||||
#define MINY (0) /* -8: половина за левым/верхним краем */
|
||||
#define MINX (0) /* -8: половина за левым/верхним краем */
|
||||
#define MAXX (320 - BW) /* 312: половина за правым краем */
|
||||
#define MAXY (256 - BW) /* 248: половина за нижним краем */
|
||||
|
||||
/* Цвета шаров (не совпадают с фоном BLACK/WHITE) */
|
||||
static const uint8_t ball_colors[] = {
|
||||
YELLOW, LIGHTRED, LIGHTGREEN, LIGHTCYAN,
|
||||
LIGHTMAGENTA, LIGHTBLUE, RED, GREEN
|
||||
};
|
||||
#define NCOLORS ((uint8_t)sizeof(ball_colors))
|
||||
|
||||
static sprite_t sp[NBALLS]; /* спрайты — движок ведёт drawn[2] */
|
||||
static int8_t vx[NBALLS], vy[NBALLS]; /* направление ±1 (физика — наша) */
|
||||
static uint8_t spr[NCOLORS][4 + BW * BW]; /* по спрайту на цвет */
|
||||
static uint8_t egapal[16 * 4];
|
||||
|
||||
static void build_sprites(void)
|
||||
{
|
||||
uint8_t c;
|
||||
int x, y;
|
||||
|
||||
for (c = 0; c < NCOLORS; c++) {
|
||||
uint8_t *p = spr[c];
|
||||
|
||||
*p++ = BW; *p++ = 0; *p++ = BW; *p++ = 0;
|
||||
for (y = 0; y < BW; y++)
|
||||
for (x = 0; x < BW; x++) {
|
||||
int dx = 2 * x - (BW - 1), dy = 2 * y - (BW - 1); /* ×2: центр между пикселями */
|
||||
*p++ = (dx * dx + dy * dy <= 15 * 15)
|
||||
? ball_colors[c] : GFX_TRANSPARENT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_board(void)
|
||||
{
|
||||
int cx, cy;
|
||||
|
||||
for (cy = 0; cy < 256; cy += CELL)
|
||||
for (cx = 0; cx < 320; cx += CELL) {
|
||||
setfillstyle(SOLID_FILL,
|
||||
(((cx ^ cy) >> 4) & 1) ? WHITE : BLACK);
|
||||
bar(cx, cy, cx + CELL - 1, cy + CELL - 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void init_balls(void)
|
||||
{
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0; i < NBALLS; i++) {
|
||||
sprite_init(&sp[i], spr[i % NCOLORS]);
|
||||
sp[i].x = (20 + i * 37) % (MAXX - 1) + 1;
|
||||
sp[i].y = (20 + i * 53) % (MAXY - 1) + 1;
|
||||
sprite_show(&sp[i]);
|
||||
vx[i] = (i & 1) ? 1 : -1;
|
||||
vy[i] = (i & 2) ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
#if FPS_METER
|
||||
/* Кадров за последнюю RTC-секунду — белым в чёрной плашке слева-сверху
|
||||
* на ТЕКУЩЕЙ draw-странице (рисуем ДО sprite_update, чтобы шары легли
|
||||
* поверх и heal учитывал плашку через ОЗУ-копию). */
|
||||
static void show_fps(uint8_t v)
|
||||
{
|
||||
char buf[4];
|
||||
|
||||
buf[0] = (char)('0' + (v / 100) % 10);
|
||||
buf[1] = (char)('0' + (v / 10) % 10);
|
||||
buf[2] = (char)('0' + v % 10);
|
||||
buf[3] = 0;
|
||||
setcolor(WHITE);
|
||||
setfillstyle(SOLID_FILL, BLACK);
|
||||
bar(0, 0, 27, 9);
|
||||
outtextxy(2, 1, buf);
|
||||
}
|
||||
#endif
|
||||
|
||||
int main(void)
|
||||
{
|
||||
uint8_t i, page, hidden;
|
||||
#if FPS_METER
|
||||
datetime_t dt;
|
||||
uint8_t frames = 0, last_sec;
|
||||
#endif
|
||||
|
||||
build_sprites();
|
||||
initgraph();
|
||||
gfx_sprite_clip(0);
|
||||
|
||||
/* Палитра страницы 1 = палитре страницы 0. */
|
||||
gfx_pal_get(0, 0, 16, egapal);
|
||||
gfx_pal_load(1, 0, 16, egapal);
|
||||
|
||||
init_balls();
|
||||
|
||||
/* Обе страницы: шахматка (в ОЗУ-копию — бэкап для heal) + шары в
|
||||
* стартовых позициях (sprite_update рисует на текущей draw-странице
|
||||
* и снимает её dirty-бит). */
|
||||
for (page = 0; page < 2; page++) {
|
||||
gfx_set_draw_page(page);
|
||||
draw_board();
|
||||
sprite_update(sp, NBALLS);
|
||||
}
|
||||
gfx_set_visible_page(0);
|
||||
#if FPS_METER
|
||||
getdatetime(&dt);
|
||||
last_sec = dt.second;
|
||||
#endif
|
||||
|
||||
for (;;) {
|
||||
if (kbhit() && getch() == 27)
|
||||
break;
|
||||
|
||||
/* Физика: обновить позиции (только состояние спрайтов). */
|
||||
for (i = 0; i < NBALLS; i++) {
|
||||
int nx = sp[i].x + vx[i];
|
||||
int ny = sp[i].y + vy[i];
|
||||
if (nx <= MINX || nx >= MAXX) vx[i] = -vx[i];
|
||||
if (ny <= MINY || ny >= MAXY) vy[i] = -vy[i];
|
||||
sprite_move(&sp[i], nx, ny);
|
||||
}
|
||||
|
||||
/* Кадр: рисуем на скрытой странице. sprite_flip развёрнут, чтобы
|
||||
* плашку FPS нарисовать на скрытой странице ДО sprite_update. */
|
||||
hidden = gfx_get_visible_page() ^ 1;
|
||||
gfx_set_draw_page(hidden);
|
||||
#if FPS_METER
|
||||
frames++;
|
||||
getdatetime(&dt);
|
||||
if (dt.second != last_sec) {
|
||||
last_sec = dt.second;
|
||||
show_fps(frames);
|
||||
frames = 0;
|
||||
}
|
||||
#endif
|
||||
sprite_update(sp, NBALLS);
|
||||
gfx_wait_vsync();
|
||||
gfx_set_visible_page(hidden); /* флип в vblank — без мерцания */
|
||||
}
|
||||
|
||||
closegraph();
|
||||
return 0;
|
||||
}
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
make clean
|
||||
make run
|
||||
@@ -68,6 +68,30 @@ void _bgi_hspan_raw(int x, int y, int len, uint8_t color) __preserves_regs(d
|
||||
void _bgi_vspan_raw(int x, int y, int len, uint8_t color) __preserves_regs(d,e); /* V-отрезок, без скобки */
|
||||
void _bgi_clear_raw(uint8_t color); /* заливка экрана, без скобки (клоббер всех) */
|
||||
|
||||
/* Копирование h строк по w байт через accel block-copy (LD L,L) —
|
||||
* ядро блиттинга (putimage, getimage, gfx_blit, gfx_heal). Без скобки и
|
||||
* клиппинга; w,h: 0 = 256; stride 0 = адрес на месте (сторона экрана,
|
||||
* строку выбирает Port_Y); y0 — стартовый Port_Y. Направление задают
|
||||
* указатели (буфер обязан быть вне W3). Контракт/схема — в
|
||||
* bgi256/_bgi_copy_rows_raw.c. */
|
||||
void _bgi_copy_rows_raw(const uint8_t *src, uint8_t *dst,
|
||||
uint8_t w, uint8_t h,
|
||||
int dstride, int sstride, uint8_t y0);
|
||||
|
||||
/* Специализация для БЛИТА: dst фиксирован (dstride 0), src двигается на
|
||||
* sstride (SMC-константа); dst-цепочка выкинута (~24Т/строку). Ядро
|
||||
* gfx_blit_part/putsprite/спрайтового движка. Без скобки/клиппинга; банк
|
||||
* ставит вызывающий (0x5C прозрачный). bgi256/_bgi_blit_rows_raw.c. */
|
||||
void _bgi_blit_rows_raw(const uint8_t *src, uint8_t *dst,
|
||||
uint8_t w, uint8_t h,
|
||||
int sstride, uint8_t y0);
|
||||
|
||||
/* Специализация для heal: src == dst == адрес экрана, оба страйда 0
|
||||
* (строку выбирает Port_Y) — адресной арифметики в цикле нет, строка
|
||||
* 77Т + burst'ы. y0 — стартовый Port_Y; w,h: 0 = 256. Без скобки и
|
||||
* клиппинга; банк ставит вызывающий (gfx_heal форсит 0x50). */
|
||||
void _bgi_heal_rows_raw(uint8_t *scr, int y0, uint8_t w, uint8_t h);
|
||||
|
||||
/* Self-bracket версии (begin + raw + end) — для разовых вызовов из
|
||||
* математики, которой не нужен пакетный режим (floodfill, cleardevice).
|
||||
* Разовое чтение пикселя — публичный getpixel (та же композиция;
|
||||
@@ -75,6 +99,48 @@ void _bgi_clear_raw(uint8_t color); /* заливка экр
|
||||
void _bgi_hspan (int x, int y, int len, uint8_t color);
|
||||
void _bgi_clearall(uint8_t color);
|
||||
|
||||
/* Ядро блиттинга gfx_blit_part/gfx_blit/gfx_heal (Фаза B) — публичные,
|
||||
* прототипы в <gfx.h>; спрайтовые обёртки putsprite/movesprite — в
|
||||
* <graphics.h>. Реализации: common/gfx_blit_part.c и соседи. */
|
||||
|
||||
/* Bracket-free общие ядра (полная ширина) (common/_gfx_blit_full.c, _gfx_heal_full.c):
|
||||
* клип по экрану + нарезка полос ≤256 + accel-leaf, БЕЗ _bgi_begin/
|
||||
* _bgi_end и БЕЗ смены банка. Вызывающий обязан замапить W3 (_bgi_begin)
|
||||
* и выставить нужный банк ДО вызова. Общее ядро для разовых обёрток
|
||||
* (gfx_blit_part/gfx_heal) и batch-пасса спрайтового движка v2, который
|
||||
* держит одну скобку + один банк на весь кадр (docs/sprite-api-design.md
|
||||
* §9.1). blit: банк 0x5C для прозрачности; heal: банк 0x50. */
|
||||
void _gfx_blit_full(int x, int y, const void *img,
|
||||
int sx, int sy, int w, int h);
|
||||
void _gfx_heal_full(int x, int y, int w, int h);
|
||||
|
||||
/* Лин-ядра для СПРАЙТОВ (кадр ≤ 64×64, sprite.h): как *_full, но без
|
||||
* split'а >256 и с 8-битными w/h (движок/putsprite — горячий путь;
|
||||
* программа только со спрайтами не тянет общие *_full со split'ом).
|
||||
* common/_gfx_blit_sprite.c, _gfx_heal_sprite.c. */
|
||||
void _gfx_blit_sprite(int x, int y, const void *img,
|
||||
int sx, int sy, uint8_t w, uint8_t h);
|
||||
void _gfx_heal_sprite(int x, int y, uint8_t w, uint8_t h);
|
||||
|
||||
/* NOCLIP-ядра: клип-кода нет вообще (полный выигрыш от codegen, не только
|
||||
* скип проверок). Зовутся при gfx_sprite_clip(0) — приложение
|
||||
* гарантирует пределы. common/_gfx_blit_sprite_noclip.c, _gfx_heal_… */
|
||||
void _gfx_blit_sprite_noclip(int x, int y, const void *img,
|
||||
int sx, int sy, uint8_t w, uint8_t h);
|
||||
void _gfx_heal_sprite_noclip(int x, int y, uint8_t w, uint8_t h);
|
||||
|
||||
/* Диспетчеризация clip/noclip через УКАЗАТЕЛИ (не ветка в горячем цикле):
|
||||
* gfx_sprite_clip() перенаправляет их на clip- или noclip-ядро ОДИН раз,
|
||||
* а sprite_update/putsprite/movesprite зовут через указатель — один
|
||||
* call-site, без if в цикле → тесный codegen (полный выигрыш noclip,
|
||||
* см. docs/sprite-api-design.md). Установка по умолчанию (clip on) — в
|
||||
* initgraph. */
|
||||
typedef void (*_gfx_blit_fp)(int, int, const void *, int, int,
|
||||
uint8_t, uint8_t);
|
||||
typedef void (*_gfx_heal_fp)(int, int, uint8_t, uint8_t);
|
||||
extern _gfx_blit_fp _gfx_blit_fn;
|
||||
extern _gfx_heal_fp _gfx_heal_fn;
|
||||
|
||||
/* ---- Fill-burst'ы акселератора 256 (register-ABI, asm-only) ------ *
|
||||
* Вызываются ТОЛЬКО из asm raw-примитивов; аргументы в регистрах, без
|
||||
* глобального скретча в _DATA:
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* _bgi_blit_rows_raw — специализация accel block-copy для БЛИТА
|
||||
* (буфер → экран): dst ФИКСИРОВАН (адрес экрана base+x; строку выбирает
|
||||
* Port_Y, dstride == 0 по определению), src двигается на sstride (ширину
|
||||
* картинки) на строку. Выделен из _bgi_copy_rows_raw (2026-07-12,
|
||||
* профиль examples/balls): у блита dst-цепочка (ld a,e; add; ld e,a;
|
||||
* ld a,d; adc; ld d,a ≈ 24Т/строку) — мёртвый груз, т.к. dst не меняется;
|
||||
* здесь её нет вообще. src-страйд SMC-патчится в 8-битную adc-цепочку
|
||||
* (константа на вызов). Универсальный _bgi_copy_rows_raw остаётся для
|
||||
* getimage (grab: dst двигается, src фикс — другая специализация).
|
||||
*
|
||||
* ВХОД (__sdcccall(1)): src→HL, dst→DE (фикс); стек: w 4(ix), h 5(ix),
|
||||
* sstride 6/7(ix), y0 8(ix). w — байт/строку за burst (1..256, 0=256);
|
||||
* h — строк (1..256, 0=256); sstride — шаг адреса src между строками;
|
||||
* y0 — стартовый Port_Y. Callee-pop 5 байт.
|
||||
*
|
||||
* Raw: клиппинга НЕТ (вызывающий), W3 замаплен (_bgi_begin/_bgi_end
|
||||
* снаружи), банк выставлен вызывающим (0x5C прозрачный / 0x50 обычный).
|
||||
* Буфер src обязан быть ВНЕ W3 (< 0xC000). Полосы > 256 режет вызывающий.
|
||||
*
|
||||
* ОДИН DI на весь блит (arm + все строки): убирает per-row EI-окно, где
|
||||
* CBL-ISR мог пере-армировать акселератор своим размером блока, и тесней
|
||||
* loop (санкция пользователя 2026-07-12: малый спрайт под одним DI аудио
|
||||
* не рвёт). ВНИМАНИЕ: h ограничен малыми спрайтами — большие блиты
|
||||
* (putimage 256 строк) вызывающий обязан резать на чанки ≤16 строк.
|
||||
*
|
||||
* Квирк CPU-байта write-триггера (см. _bgi_copy_rows_raw): src[0]
|
||||
* предчитывается и подставляется в A через EX AF,AF' — CPU-цикл
|
||||
* LD (DE),A кладёт корректный src[0] при любом банке.
|
||||
*
|
||||
* Клоббер: AF/AF'/BC/DE/HL; IX сохраняется.
|
||||
*/
|
||||
|
||||
#include "../_bgi.h"
|
||||
|
||||
void _bgi_blit_rows_raw(const uint8_t *src, uint8_t *dst,
|
||||
uint8_t w, uint8_t h,
|
||||
int sstride, uint8_t y0) __naked
|
||||
{
|
||||
(void)src; (void)dst; (void)w; (void)h; (void)sstride; (void)y0;
|
||||
__asm
|
||||
push ix
|
||||
ld ix, #0
|
||||
add ix, sp
|
||||
;; SMC: размер блока <- w (immediate у ld a,#n — требование FSM)
|
||||
ld a, 4 (ix)
|
||||
ld (br_len_imm), a
|
||||
;; SMC: src-страйд — константа на вызов (8-битная adc-цепочка)
|
||||
ld a, 6 (ix)
|
||||
ld (br_slo_imm), a
|
||||
ld a, 7 (ix)
|
||||
ld (br_shi_imm), a
|
||||
ld b, 5 (ix) ; B = счётчик строк (0 => 256: djnz)
|
||||
ld c, 8 (ix) ; C = y
|
||||
di ; один DI на весь блит
|
||||
ld d, d ; 0x52 — режим размера блока
|
||||
ld a, #0
|
||||
br_len_imm = . - 1
|
||||
ld b, b ; 0x40 — стоп (B=счётчик не тронут)
|
||||
br_row:
|
||||
ld a, c
|
||||
out (#0x89), a ; Port_Y = y
|
||||
inc c
|
||||
;; ld a, (hl) ; A = src[0] (accel выключен)
|
||||
;; ex af, af' ; спрятать src[0]
|
||||
ld l, l ; 0x6D — армировать копию строки
|
||||
ld a, (hl) ; burst: src -> память акселератора
|
||||
;; ex af, af' ; A = src[0] — CPU-байт триггера записи
|
||||
ld (de), a ; burst: память акселератора -> dst (фикс)
|
||||
ld b, b ; 0x40 — стоп
|
||||
;; src += sstride (dst НЕ двигается — dstride 0)
|
||||
ld a, l
|
||||
add a, #0
|
||||
br_slo_imm = . - 1
|
||||
ld l, a
|
||||
ld a, h
|
||||
adc a, #0
|
||||
br_shi_imm = . - 1
|
||||
ld h, a
|
||||
djnz br_row
|
||||
ei ; закрыть DI-окно блита
|
||||
pop ix
|
||||
;; callee-pop 5 байт (w,h,sstride:2,y0)
|
||||
pop hl ; ret-адрес
|
||||
pop af
|
||||
pop af
|
||||
inc sp
|
||||
jp (hl)
|
||||
__endasm;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* _bgi_copy_rows_raw — копирование h строк по w байт через block-copy
|
||||
* акселератора (LD L,L), mode 0x81. Ядро блиттинга: putimage(COPY),
|
||||
* getimage, gfx_blit*, gfx_heal — все операции блиттинга это один этот
|
||||
* leaf с разными src/dst (docs/sprite-api-design.md §2, §4.1):
|
||||
*
|
||||
* блит src=буфер, sstride=w картинки; dst=экран, dstride=0
|
||||
* grab src=экран, sstride=0; dst=буфер, dstride=w
|
||||
* Для heal (src == dst == экран, оба страйда 0) — специализированный
|
||||
* _bgi_heal_rows_raw (без адресной арифметики в цикле вообще).
|
||||
* Страйды здесь — константы на весь вызов и патчатся SMC при входе
|
||||
* (8-битные цепочки add/adc, 73Т/строку вместо 140Т у прежнего
|
||||
* варианта с перечитыванием через IX; профиль examples/balls).
|
||||
*
|
||||
* ВХОД (__sdcccall(1)): src→HL, dst→DE; стек: w 4(ix), h 5(ix),
|
||||
* dstride 6/7(ix), sstride 8/9(ix), y0 10(ix).
|
||||
* w — байт в строке за burst, 1..256 (0 = 256 — конвенция акселератора);
|
||||
* h — строк, 1..256 (0 = 256);
|
||||
* dstride/sstride — шаг адреса между строками (0 = адрес на месте,
|
||||
* строку выбирает Port_Y — сторона, живущая в видеоокне W3);
|
||||
* y0 — стартовый Port_Y, инкремент на строку.
|
||||
*
|
||||
* Raw: клиппинга НЕТ (весь клип делает вызывающий), W3 уже замаплен
|
||||
* (_bgi_begin/_bgi_end снаружи). Буферная сторона обязана быть ВНЕ W3
|
||||
* (< 0xC000) — на время операции W3 занят видеобанком. Полосы > 256
|
||||
* байт режет вызывающий (см. gfx_blit_part).
|
||||
*
|
||||
* Схема (референс Draw_Restangle_Data, accelerator_doc.txt): размер
|
||||
* блока армируется ОДИН раз (SMC у LD A,#n — операнд ОБЯЗАН быть
|
||||
* immediate, memory/sprinter_accelerator; размер переживает LD B,B —
|
||||
* tests/accfill), далее на строку: OUT Port_Y, DI, LD L,L (армировать
|
||||
* копию), LD A,(HL) (burst-чтение src в память акселератора),
|
||||
* LD (DE),A (burst-запись из памяти акселератора в dst), LD B,B, EI.
|
||||
* Между армированием и триггерами — только регистровые M1-опкоды.
|
||||
*
|
||||
* КВИРК ТРИГГЕРА ЗАПИСИ (найден tests/sprites — клипнутые спрайты):
|
||||
* LD (DE),A — реальная CPU-инструкция: её собственный цикл записи
|
||||
* кладёт байт A в dst[0], и лишь затем burst FSM пишет src[0..w-1]
|
||||
* поверх. После burst-чтения в A остаётся ПОСЛЕДНИЙ байт строки —
|
||||
* при банке 0x50 утечка невидима (src[0] тут же перезапишет), но при
|
||||
* 0x58/0x5C, если src[0] == 0xFF, перезапись скипается и в dst[0]
|
||||
* остаётся вертикальная полоса цвета последних байтов строк.
|
||||
* Нейтрализация: src[0] предчитывается ДО армирования и подставляется
|
||||
* в A через EX AF,AF' (регистровый M1-опкод — под armed разрешён)
|
||||
* прямо перед триггером — CPU-байт становится src[0], что корректно
|
||||
* при ЛЮБОМ банке и любой семантике первого цикла записи (в MAME
|
||||
* 0.283 подтверждено tests/blitw+sprites; на железе перепроверить).
|
||||
*
|
||||
* Клоббер: AF/AF'/BC/DE/HL; IX сохраняется (callee-saved).
|
||||
*/
|
||||
|
||||
#include "../_bgi.h"
|
||||
|
||||
void _bgi_copy_rows_raw(const uint8_t *src, uint8_t *dst,
|
||||
uint8_t w, uint8_t h,
|
||||
int dstride, int sstride, uint8_t y0) __naked
|
||||
{
|
||||
__asm
|
||||
push ix
|
||||
ld ix, #0
|
||||
add ix, sp
|
||||
;; SMC: размер блока <- w (immediate у ld a,#n — требование FSM)
|
||||
ld a, 4 (ix)
|
||||
ld (cr_len_imm), a
|
||||
;; SMC: страйды — константы на весь вызов; патчим immediate
|
||||
;; 8-битных цепочек сложения вместо перечитывания из стека
|
||||
;; через IX на каждой строке (профиль examples/balls
|
||||
;; 2026-07-12: было 140Т/строку, стало 73Т — и BC не нужен,
|
||||
;; push/pop и ex de,hl ушли).
|
||||
ld a, 8 (ix)
|
||||
ld (cr_slo_imm), a
|
||||
ld a, 9 (ix)
|
||||
ld (cr_shi_imm), a
|
||||
ld a, 6 (ix)
|
||||
ld (cr_dlo_imm), a
|
||||
ld a, 7 (ix)
|
||||
ld (cr_dhi_imm), a
|
||||
ld b, 5 (ix) ; B = счётчик строк (0 => 256: djnz)
|
||||
ld c, 10 (ix) ; C = y
|
||||
;; ОДИН DI на весь блит (arm + все строки): убирает per-row
|
||||
;; EI-окно (CBL-ISR не пере-армирует акселератор посреди спрайта)
|
||||
;; и тесней loop. Санкция пользователя 2026-07-12: малый спрайт
|
||||
;; под одним DI аудио не рвёт. ВНИМАНИЕ: для больших h (putimage
|
||||
;; 256 строк) вызывающий обязан резать на чанки ≤16 строк.
|
||||
di
|
||||
ld d, d ; 0x52 — режим размера блока
|
||||
ld a, #0
|
||||
cr_len_imm = . - 1
|
||||
ld b, b ; 0x40 — стоп (B=счётчик не тронут)
|
||||
cr_row:
|
||||
ld a, c
|
||||
out (#0x89), a ; Port_Y = y
|
||||
inc c
|
||||
;; ld a, (hl) ; A = src[0] (accel выключен)
|
||||
;; ex af, af' ; спрятать src[0] в A'
|
||||
ld l, l ; 0x6D — армировать копию строки
|
||||
ld a, (hl) ; burst: src -> память акселератора
|
||||
;; ex af, af' ; A = src[0] — CPU-байт триггера записи
|
||||
ld (de), a ; burst: память акселератора -> dst
|
||||
ld b, b ; 0x40 — стоп (no-op по регистрам)
|
||||
;; src += sstride (SMC-immediates, 16-бит через adc)
|
||||
ld a, l
|
||||
add a, #0
|
||||
cr_slo_imm = . - 1
|
||||
ld l, a
|
||||
ld a, h
|
||||
adc a, #0
|
||||
cr_shi_imm = . - 1
|
||||
ld h, a
|
||||
;; dst += dstride
|
||||
ld a, e
|
||||
add a, #0
|
||||
cr_dlo_imm = . - 1
|
||||
ld e, a
|
||||
ld a, d
|
||||
adc a, #0
|
||||
cr_dhi_imm = . - 1
|
||||
ld d, a
|
||||
djnz cr_row
|
||||
ei ; закрыть DI-окно блита
|
||||
pop ix
|
||||
;; callee-pop 7 байт стековых аргументов
|
||||
pop hl ; ret-адрес
|
||||
pop af
|
||||
pop af
|
||||
pop af
|
||||
inc sp
|
||||
jp (hl)
|
||||
__endasm;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* _bgi_heal_rows_raw — специализация accel-копии для heal (src == dst ==
|
||||
* адрес экрана): h строк по w байт «прочитать ОЗУ-копию → записать в
|
||||
* VRAM+ОЗУ» на одном месте. Выделен из _bgi_copy_rows_raw по профилю
|
||||
* examples/balls (2026-07-12): у heal ОБА страйда нулевые по
|
||||
* определению (строку выбирает Port_Y, CPU-адрес не меняется), поэтому
|
||||
* адресной арифметики в цикле нет вообще — строка 77Т + burst'ы против
|
||||
* 204Т у универсального leaf'а до оптимизации.
|
||||
*
|
||||
* ВХОД (__sdcccall(1)): scr→HL (base + x), y0→DE (E — стартовый
|
||||
* Port_Y); стек: w 4(ix), h 5(ix) — 1..256, 0 = 256 (конвенция
|
||||
* акселератора). Callee-pops 2 байта.
|
||||
*
|
||||
* Raw: клиппинга нет, W3 замаплен вызывающим; банк выбирает
|
||||
* вызывающий (gfx_heal форсит 0x50 — иначе heal бессмыслен).
|
||||
* Полосы > 256 байт режет вызывающий.
|
||||
*
|
||||
* Квирк CPU-байта write-триггера (см. _bgi_copy_rows_raw): для heal
|
||||
* предчитанный src[0] — ровно тот байт, что burst запишет в dst[0],
|
||||
* так что фикс EX AF,AF' здесь одновременно и нейтрализация утечки,
|
||||
* и просто корректное значение.
|
||||
*
|
||||
* Клоббер: AF/AF'/BC/DE/HL; IX сохраняется.
|
||||
*/
|
||||
|
||||
#include "../_bgi.h"
|
||||
|
||||
void _bgi_heal_rows_raw(uint8_t *scr, int y0, uint8_t w, uint8_t h) __naked
|
||||
{
|
||||
__asm
|
||||
push ix
|
||||
ld ix, #0
|
||||
add ix, sp
|
||||
;; SMC: размер блока <- w (immediate — требование FSM)
|
||||
ld a, 4 (ix)
|
||||
ld (hr_len_imm), a
|
||||
ld c, e ; C = y (взять ДО затирания DE)
|
||||
ld b, 5 (ix) ; B = счётчик строк (0 => 256)
|
||||
ld d, h
|
||||
ld e, l ; DE = HL = scr (src == dst)
|
||||
;; ОДИН DI на весь спрайт (arm + все строки): убирает per-row
|
||||
;; EI-окно, где CBL-ISR мог пере-армировать акселератор своим
|
||||
;; размером блока (латентная реентерабельность), и тесней loop.
|
||||
;; Санкция пользователя 2026-07-12: 16×16 = 256 байт под одним DI
|
||||
;; (как 1×256 fill) на аудио не влияет. ВНИМАНИЕ: h ограничен
|
||||
;; малыми спрайтами — для больших блитов (putimage 256 строк)
|
||||
;; вызывающий обязан резать на чанки ≤16 строк (иначе долгий DI).
|
||||
di
|
||||
ld d, d ; 0x52 — режим размера блока
|
||||
ld a, #0
|
||||
hr_len_imm = . - 1
|
||||
ld b, b ; 0x40 — стоп (B=счётчик не тронут)
|
||||
hr_row:
|
||||
ld a, c
|
||||
out (#0x89), a ; Port_Y = y
|
||||
inc c
|
||||
;; ld a, (hl) ; предчитать ОЗУ[0] (accel выключен)
|
||||
;; ex af, af'
|
||||
ld l, l ; 0x6D — армировать копию строки
|
||||
ld a, (hl) ; burst: ОЗУ-копия -> память акселератора
|
||||
;; ex af, af' ; A = ОЗУ[0] — CPU-байт триггера корректен
|
||||
ld (de), a ; burst: память акселератора -> VRAM+ОЗУ
|
||||
ld b, b ; 0x40 — стоп
|
||||
djnz hr_row ; адреса не меняются — только Port_Y
|
||||
ei ; закрыть DI-окно спрайта
|
||||
pop ix
|
||||
pop hl ; ret-адрес; callee-pop 2 байта (w,h)
|
||||
inc sp
|
||||
inc sp
|
||||
jp (hl)
|
||||
__endasm;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* _gfx_blit_full — bracket-free ядро блита под-прямоугольника картинки
|
||||
* getimage-формата (см. gfx_blit_part). Клип по экрану + нарезка на
|
||||
* полосы ≤256 + вызов accel-leaf'а _bgi_copy_rows_raw. БЕЗ _bgi_begin/
|
||||
* _bgi_end и БЕЗ смены банка — вызывающий обязан замапить W3 (_bgi_begin)
|
||||
* и выставить нужный _gfx_bank ДО вызова.
|
||||
*
|
||||
* Вынесено из gfx_blit_part (2026-07-12) ради batch-пасса спрайтового
|
||||
* движка v2: sprite_update держит одну W3-скобку и один банк на ВЕСЬ
|
||||
* проход, вызывая это ядро в цикле по спрайтам (амортизация обвязки —
|
||||
* см. docs/sprite-api-design.md §9.1). gfx_blit_part теперь = _bgi_begin
|
||||
* + это ядро + _bgi_end.
|
||||
*
|
||||
* Clip fast-path: если прямоугольник целиком на экране — 4 сравнения
|
||||
* вместо полного пути коррекции краёв (TODO «клип fast-path»).
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
|
||||
void _gfx_blit_full(int x, int y, const void *img,
|
||||
int sx, int sy, int w, int h)
|
||||
{
|
||||
const uint8_t *p = (const uint8_t *)img;
|
||||
const uint8_t *src;
|
||||
uint16_t dst;
|
||||
int img_w;
|
||||
|
||||
/* img_w — ПОЛНАЯ ширина картинки/ленты из заголовка getimage
|
||||
* (p[0]=lo, p[1]=hi); это шаг между строками источника (sstride).
|
||||
* sx/sy — смещение кадра внутри картинки (атлас). Подробный разбор
|
||||
* формата и sx/sy — в common/_gfx_blit_sprite.c. */
|
||||
img_w = p[0] | (p[1] << 8);
|
||||
|
||||
/* Клип по экрану — только если хоть один край вылезает. */
|
||||
if (x < 0 || y < 0 || x + w > _bgi_maxx + 1 || y + h > _bgi_maxy + 1) {
|
||||
if (x < 0) { sx -= x; w += x; x = 0; }
|
||||
if (y < 0) { sy -= y; h += y; y = 0; }
|
||||
if (x + w > _bgi_maxx + 1) w = _bgi_maxx + 1 - x;
|
||||
if (y + h > _bgi_maxy + 1) h = _bgi_maxy + 1 - y;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
}
|
||||
|
||||
/* src = img + 4 + sy*img_w + sx — без __mulint (обычно sy == 0). */
|
||||
src = p + 4 + sx;
|
||||
while (sy--)
|
||||
src += img_w;
|
||||
|
||||
dst = _gfx_addr_base + x;
|
||||
while (w > 256) {
|
||||
_bgi_blit_rows_raw(src, (uint8_t *)dst, 0 /* =256 */, (uint8_t)h,
|
||||
img_w, (uint8_t)y);
|
||||
src += 256;
|
||||
dst += 256;
|
||||
w -= 256;
|
||||
}
|
||||
_bgi_blit_rows_raw(src, (uint8_t *)dst, (uint8_t)w, (uint8_t)h,
|
||||
img_w, (uint8_t)y);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* _gfx_blit_sprite — лин-ядро блита СПРАЙТА (кадр ≤ 64×64, см. sprite.h):
|
||||
* то же, что _gfx_blit_full, но БЕЗ split'а на полосы >256 (спрайт
|
||||
* заведомо ≤256 байт в строке) и с 8-битными w/h (дешевле передача
|
||||
* аргументов). Один вызов _bgi_blit_rows_raw. БЕЗ _bgi_begin/_bgi_end
|
||||
* и смены банка — вызывающий (sprite_update/putsprite) держит скобку и
|
||||
* банк 0x5C. Проверок размера НЕТ (контракт ≤64 — в доке, не в коде).
|
||||
*
|
||||
* Клип по ЭКРАНУ остаётся (это функциональность — спрайты штатно уходят
|
||||
* за края): fast-path «целиком на экране» = 4 сравнения.
|
||||
*
|
||||
* ФОРМАТ getimage и параметры атласа
|
||||
* ----------------------------------
|
||||
* img указывает на getimage-буфер: 4-байтовый заголовок + пиксели.
|
||||
* Заголовок = uint16 width, uint16 height (little-endian):
|
||||
* p[0]=width lo, p[1]=width hi, p[2]=height lo, p[3]=height hi.
|
||||
* Дальше (p+4) — пиксели ПОСТРОЧНО, по width байт на строку.
|
||||
*
|
||||
* int img_w = p[0] | (p[1] << 8);
|
||||
* — это ПОЛНАЯ ширина картинки/ленты-атласа (склейка двух байтов
|
||||
* заголовка в 16-бит). Используется как ШАГ между строками источника
|
||||
* (sstride): чтобы спуститься на строку ниже в буфере, надо прибавить к
|
||||
* адресу целую ширину картинки. Для одиночного спрайта img_w == ширине
|
||||
* кадра; для ЛЕНТЫ-АТЛАСА (много кадров в ряд) img_w — ширина всей ленты
|
||||
* (> ширины кадра w), поэтому img_w остаётся int (лента может быть шире
|
||||
* 64, хотя КАДР ≤ 64).
|
||||
*
|
||||
* (sx, sy) — смещение КАДРА (под-прямоугольника) ВНУТРИ картинки:
|
||||
* левый-верхний угол вырезаемого кадра в пикселях. Для целого спрайта
|
||||
* sx = sy = 0. Для кадра N горизонтальной ленты: sx = N*FRAME_W, sy = 0
|
||||
* (вертикальные атласы — sy). Отсюда адрес первого пикселя кадра:
|
||||
* src = p + 4 (пропустить заголовок)
|
||||
* + sx (сместиться на sx пикселей вправо в первой строке)
|
||||
* + sy*img_w (спуститься на sy строк — цикл сложений ниже, без
|
||||
* умножения: обычно sy == 0).
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
#include <stdio.h>
|
||||
|
||||
void _gfx_blit_sprite(int x, int y, const void *img,
|
||||
int sx, int sy, uint8_t w, uint8_t h)
|
||||
{
|
||||
const uint8_t *p = (const uint8_t *)img;
|
||||
const uint8_t *src;
|
||||
uint16_t dst;
|
||||
int img_w = p[0] | (p[1] << 8); /* ширина картинки/ленты = sstride */
|
||||
int cw = w, ch = h; /* рабочие w,h кадра (для клипа) */
|
||||
|
||||
if (x < 0 || y < 0 || x + cw > _bgi_maxx + 1 || y + ch > _bgi_maxy + 1) {
|
||||
if (x < 0) { sx -= x; cw += x; x = 0; }
|
||||
if (y < 0) { sy -= y; ch += y; y = 0; }
|
||||
if (x + cw > _bgi_maxx + 1) cw = _bgi_maxx + 1 - x;
|
||||
if (y + ch > _bgi_maxy + 1) ch = _bgi_maxy + 1 - y;
|
||||
if (cw <= 0 || ch <= 0) return;
|
||||
}
|
||||
|
||||
|
||||
/* Адрес первого пикселя кадра: заголовок(4) + sx вправо, затем sy
|
||||
* строк вниз (по img_w на строку) — циклом сложений, без __mulint
|
||||
* (обычно sy == 0: целый спрайт или горизонтальная лента). */
|
||||
src = p + 4 + sx;
|
||||
while (sy--)
|
||||
src += img_w;
|
||||
|
||||
dst = _gfx_addr_base + x;
|
||||
|
||||
_bgi_blit_rows_raw(src, (uint8_t *)dst, (uint8_t)cw, (uint8_t)ch,
|
||||
img_w, (uint8_t)y);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* _gfx_blit_sprite_noclip — блит спрайта БЕЗ клипа по экрану. Как
|
||||
* _gfx_blit_sprite, но клип-математики нет ВООБЩЕ (не скип проверки —
|
||||
* кода нет физически): приложение ГАРАНТИРУЕТ, что спрайт целиком на
|
||||
* экране (флаг gfx_sprite_clip(0)). Отдельная функция, а не ветка в
|
||||
* _gfx_blit_sprite: без клип-кода SDCC тесней раскладывает регистры
|
||||
* (меньше спиллов) — отсюда полный выигрыш ~+19% на анимации (замер
|
||||
* A/B на examples/balls 2026-07-12), а не только скип сравнений.
|
||||
*
|
||||
* ВНИМАНИЕ: запись за край экрана портит соседнюю память (Port_Y за 256
|
||||
* заворачивается наверх, x за 320 — в соседнюю строку/страницу; см.
|
||||
* tests/spriteclip). Вызывать только когда спрайт заведомо в пределах.
|
||||
*
|
||||
* Формат img/img_w/sx/sy — как в _gfx_blit_sprite (getimage-заголовок +
|
||||
* лента-атлас). БЕЗ _bgi_begin/_bgi_end и смены банка (держит вызывающий).
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
|
||||
void _gfx_blit_sprite_noclip(int x, int y, const void *img,
|
||||
int sx, int sy, uint8_t w, uint8_t h)
|
||||
{
|
||||
const uint8_t *p = (const uint8_t *)img;
|
||||
const uint8_t *src;
|
||||
int img_w = p[0] | (p[1] << 8); /* ширина картинки/ленты = sstride */
|
||||
|
||||
src = p + 4 + sx;
|
||||
while (sy--)
|
||||
src += img_w;
|
||||
|
||||
_bgi_blit_rows_raw(src, (uint8_t *)(_gfx_addr_base + x),
|
||||
w, h, img_w, (uint8_t)y);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* _gfx_heal_full — bracket-free ядро heal прямоугольника экрана из
|
||||
* ОЗУ-копии (см. gfx_heal). Клип по экрану + нарезка на полосы ≤256 +
|
||||
* вызов accel-leaf'а _bgi_heal_rows_raw (src == dst, strides 0). БЕЗ
|
||||
* _bgi_begin/_bgi_end и БЕЗ смены банка — вызывающий обязан замапить W3
|
||||
* и выставить банк GFX_BANK_NORMAL (0x50) ДО вызова.
|
||||
*
|
||||
* Вынесено из gfx_heal (2026-07-12) ради batch-пасса движка v2:
|
||||
* sprite_update лечит ВСЕ старые позиции под одной W3-скобкой и одним
|
||||
* банком 0x50 (docs/sprite-api-design.md §9.1). gfx_heal теперь =
|
||||
* save-банк + set 0x50 + _bgi_begin + это ядро + _bgi_end + restore.
|
||||
*
|
||||
* Clip fast-path: целиком на экране → 4 сравнения вместо полного пути.
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
|
||||
void _gfx_heal_full(int x, int y, int w, int h)
|
||||
{
|
||||
uint8_t *scr;
|
||||
|
||||
if (x < 0 || y < 0 || x + w > _bgi_maxx + 1 || y + h > _bgi_maxy + 1) {
|
||||
if (x < 0) { w += x; x = 0; }
|
||||
if (y < 0) { h += y; y = 0; }
|
||||
if (x + w > _bgi_maxx + 1) w = _bgi_maxx + 1 - x;
|
||||
if (y + h > _bgi_maxy + 1) h = _bgi_maxy + 1 - y;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
}
|
||||
|
||||
scr = (uint8_t *)(_gfx_addr_base + x);
|
||||
while (w > 256) {
|
||||
_bgi_heal_rows_raw(scr, y, 0 /* =256 */, (uint8_t)h);
|
||||
scr += 256;
|
||||
w -= 256;
|
||||
}
|
||||
_bgi_heal_rows_raw(scr, y, (uint8_t)w, (uint8_t)h);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* _gfx_heal_sprite — лин-ядро heal СПРАЙТА (≤ 64×64, см. sprite.h): как
|
||||
* _gfx_heal_full, но БЕЗ split'а на полосы >256 и с 8-битными w/h. Один
|
||||
* вызов _bgi_heal_rows_raw. БЕЗ _bgi_begin/_bgi_end и смены банка —
|
||||
* вызывающий (sprite_update) держит скобку и банк 0x50. Проверок
|
||||
* размера НЕТ (контракт ≤64 — в доке). Клип по экрану остаётся.
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
|
||||
void _gfx_heal_sprite(int x, int y, uint8_t w, uint8_t h)
|
||||
{
|
||||
uint8_t *scr;
|
||||
int cw = w, ch = h;
|
||||
|
||||
if (x < 0 || y < 0 || x + cw > _bgi_maxx + 1 || y + ch > _bgi_maxy + 1) {
|
||||
if (x < 0) { cw += x; x = 0; }
|
||||
if (y < 0) { ch += y; y = 0; }
|
||||
if (x + cw > _bgi_maxx + 1) cw = _bgi_maxx + 1 - x;
|
||||
if (y + ch > _bgi_maxy + 1) ch = _bgi_maxy + 1 - y;
|
||||
if (cw <= 0 || ch <= 0) return;
|
||||
}
|
||||
|
||||
scr = (uint8_t *)(_gfx_addr_base + x);
|
||||
_bgi_heal_rows_raw(scr, y, (uint8_t)cw, (uint8_t)ch);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* _gfx_heal_sprite_noclip — heal спрайта БЕЗ клипа по экрану. Как
|
||||
* _gfx_heal_sprite, но клип-математики нет (приложение гарантирует
|
||||
* пределы, флаг gfx_sprite_clip(0)). Отдельная функция ради полного
|
||||
* выигрыша (тесней codegen без клип-кода). БЕЗ скобки/смены банка.
|
||||
* ВНИМАНИЕ про запись за край — см. _gfx_blit_sprite_noclip.
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
|
||||
void _gfx_heal_sprite_noclip(int x, int y, uint8_t w, uint8_t h)
|
||||
{
|
||||
_bgi_heal_rows_raw((uint8_t *)(_gfx_addr_base + x), y, w, h);
|
||||
}
|
||||
@@ -18,3 +18,10 @@ uint8_t _gfx_bank = 0x50;
|
||||
* (CPU 0xC000+), page 1 — 320..639 (CPU 0xC140+), остаток — дескрипторы
|
||||
* режима/палитра, их не трогаем. */
|
||||
uint16_t _gfx_addr_base = 0xC000;
|
||||
|
||||
/* Клип спрайтов по экрану: 1 = включён (безопасно, дефолт), 0 = выключен
|
||||
* (приложение само гарантирует, что спрайты не выходят за края — даёт
|
||||
* ~+19% на анимации, см. docs/sprite-api-design.md). Читает движок/
|
||||
* putsprite/movesprite: при 1 зовут clip-ядра, при 0 — noclip-ядра.
|
||||
* Ненулевой дефолт → в _INITIALIZED (как _gfx_bank). */
|
||||
uint8_t _gfx_sprite_clip = 1;
|
||||
|
||||
@@ -1,27 +1,51 @@
|
||||
/*
|
||||
* getimage — сохранить прямоугольник экрана в буфер bitmap.
|
||||
* Формат: uint16 width, uint16 height, затем w*h байт (по строкам).
|
||||
* Чтение raw в одной W3-скобке.
|
||||
*
|
||||
* Через accel-ядро: _bgi_copy_rows_raw строками экран→буфер (Фаза A
|
||||
* спрайтового дизайна). Чтение экрана возвращает ОЗУ-копию (видео-ОЗУ
|
||||
* write-only) — после рисования в temp-банке (0x54/0x5C) getimage
|
||||
* вернёт ФОН, а не то, что на экране (это и есть механизм heal).
|
||||
*
|
||||
* Клиппинга нет (контракт BGI: rect валиден); safe-версия отсекает
|
||||
* выход за экран целиком. Буфер bitmap обязан лежать вне W3
|
||||
* (< 0xC000) — на время операции W3 замаплен на видеобанк.
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
|
||||
void getimage(int left, int top, int right, int bottom, void *bitmap)
|
||||
{
|
||||
uint8_t *p = (uint8_t *)bitmap;
|
||||
const uint8_t *src;
|
||||
int w = right - left + 1;
|
||||
int h = bottom - top + 1;
|
||||
int x, y;
|
||||
int rem;
|
||||
|
||||
if (w <= 0 || h <= 0) return;
|
||||
#ifndef GFX_NOCHECK
|
||||
if (left < 0 || top < 0 || right > _bgi_maxx || bottom > _bgi_maxy)
|
||||
return;
|
||||
#endif
|
||||
|
||||
*p++ = (uint8_t)w;
|
||||
*p++ = (uint8_t)(w >> 8);
|
||||
*p++ = (uint8_t)h;
|
||||
*p++ = (uint8_t)(h >> 8);
|
||||
|
||||
/* Полосы <= 256 байт (лимит блока акселератора); шаг строки
|
||||
* буфера — полная ширина w у ОБЕИХ полос (полосы одной строки
|
||||
* лежат в буфере подряд). */
|
||||
src = (const uint8_t *)(_gfx_addr_base + left);
|
||||
rem = w;
|
||||
_bgi_begin();
|
||||
for (y = 0; y < h; y++)
|
||||
for (x = 0; x < w; x++)
|
||||
*p++ = (uint8_t)_bgi_read_raw(left + x, top + y);
|
||||
while (rem > 256) {
|
||||
_bgi_copy_rows_raw(src, p, 0 /* =256 */, (uint8_t)h,
|
||||
w, 0, (uint8_t)top);
|
||||
src += 256;
|
||||
p += 256;
|
||||
rem -= 256;
|
||||
}
|
||||
_bgi_copy_rows_raw(src, p, (uint8_t)rem, (uint8_t)h,
|
||||
w, 0, (uint8_t)top);
|
||||
_bgi_end();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* gfx_blit — блит ЦЕЛОЙ картинки getimage-формата в (x,y) текущим
|
||||
* банком (gfx_set_bank). Читает w,h из заголовка и делегирует ядру
|
||||
* gfx_blit_part (клиппинг/полосы/accel — там).
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
|
||||
void gfx_blit(int x, int y, const void *img)
|
||||
{
|
||||
const uint8_t *p = (const uint8_t *)img;
|
||||
|
||||
gfx_blit_part(x, y, img, 0, 0,
|
||||
p[0] | (p[1] << 8), p[2] | (p[3] << 8));
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* gfx_blit_part — блит под-прямоугольника картинки getimage-формата
|
||||
* (uint16 w, uint16 h, пиксели построчно) в (x,y) ТЕКУЩИМ банком
|
||||
* (gfx_set_bank): 0x50 обычный, 0x58 прозрачный (0xFF не пишется),
|
||||
* 0x54/0x5C временный (ОЗУ-копия не трогается). Ядро блиттинга поверх
|
||||
* accel-leaf'а _bgi_copy_rows_raw (docs/sprite-api-design.md §4.2):
|
||||
* putimage(COPY_PUT) — тонкая обёртка над этой функцией.
|
||||
*
|
||||
* Под-прямоугольник (sx,sy,w,h) ОБЯЗАН лежать внутри картинки (для
|
||||
* атласов кадров); по экрану клиппится здесь — всегда, в т.ч. в fast
|
||||
* (спрайты штатно уходят за края; цена — один расчёт на блит).
|
||||
*
|
||||
* Требование: img вне W3 (< 0xC000) — на время блита W3 замаплен на
|
||||
* видеобанк.
|
||||
*
|
||||
* Фаза A: объявление в _bgi.h (internal); Фаза B переносит прототип в
|
||||
* публичный gfx.h — символ уже итоговый. 256-режим: пиксель = байт;
|
||||
* при добавлении 16-цветного режима (Фаза 2 libbgi) адресную
|
||||
* арифметику по x/сдвигам придётся спустить в mode-specific leaf.
|
||||
*/
|
||||
|
||||
#include "../_bgi.h"
|
||||
|
||||
void gfx_blit_part(int x, int y, const void *img,
|
||||
int sx, int sy, int w, int h)
|
||||
{
|
||||
/* Разовый вызов: своя W3-скобка вокруг bracket-free ядра. Клип,
|
||||
* нарезку на полосы и accel-копию делает _gfx_blit_full; batch-путь
|
||||
* спрайтового движка зовёт то же ядро под ОДНОЙ скобкой на кадр. */
|
||||
_bgi_begin();
|
||||
_gfx_blit_full(x, y, img, sx, sy, w, h);
|
||||
_bgi_end();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* gfx_heal — восстановить прямоугольник экрана из ОЗУ-копии (стирание
|
||||
* спрайта/оверлея, нарисованного банком с битом 2: 0x54/0x5C).
|
||||
*
|
||||
* Механизм (docs/sprite-api-design.md §2): чтение из #50..#5F всегда
|
||||
* возвращает ОЗУ-копию (видео-ОЗУ write-only), поэтому accel-копия
|
||||
* src == dst == адрес экрана в банке 0x50 читает ФОН из ОЗУ и пишет
|
||||
* его в видео-ОЗУ (+ в ОЗУ то же самое — безвредно). Оба stride = 0:
|
||||
* строку и чтения и записи выбирает один Port_Y. Save-буфер не нужен.
|
||||
*
|
||||
* Банк на время операции — ВСЕГДА GFX_BANK_NORMAL (независимо от
|
||||
* gfx_set_bank: heal через 0x54/0x5C не имел бы смысла — запись не
|
||||
* дошла бы до ОЗУ, а через 0x58 дырявила бы FF-байты фона);
|
||||
* пользовательский банк сохраняется и восстанавливается. Клип, полосы
|
||||
* и accel-heal — в bracket-free ядре _gfx_heal_full (общем с batch-путём
|
||||
* спрайтового движка); здесь — только банк-дисциплина и W3-скобка.
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
|
||||
void gfx_heal(int x, int y, int w, int h)
|
||||
{
|
||||
uint8_t saved = gfx_get_bank();
|
||||
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
_bgi_begin();
|
||||
_gfx_heal_full(x, y, w, h);
|
||||
_bgi_end();
|
||||
gfx_set_bank(saved);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* movesprite — переместить спрайт (кадр ≤ 64×64): восстановить фон под
|
||||
* старой позицией (heal по размерам img из заголовка) и вывести спрайт в
|
||||
* новой (putsprite). Порядок heal→draw делает перекрытие старой и новой
|
||||
* позиций безопасным: heal читает фон из ОЗУ-копии, которую спрайт
|
||||
* (банк 0x5C) никогда не трогал.
|
||||
*
|
||||
* Идёт через лин-ядро _gfx_heal_sprite (без split'а, 8-бит w/h) — банк
|
||||
* на время heal форсится 0x50 (иначе heal бессмыслен), затем putsprite.
|
||||
* Спрайтовая программа не тянет общие *_full.
|
||||
*
|
||||
* Канонический цикл анимации — docs/sprite-api-design.md §6:
|
||||
* putsprite(x, y, hero); ...
|
||||
* gfx_wait_vsync(); movesprite(x, y, nx, ny, hero);
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
|
||||
void movesprite(int oldx, int oldy, int x, int y, const void *img)
|
||||
{
|
||||
const uint8_t *p = (const uint8_t *)img;
|
||||
uint8_t saved = gfx_get_bank();
|
||||
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
_bgi_begin();
|
||||
if (_gfx_sprite_clip) _gfx_heal_sprite(oldx, oldy, p[0], p[2]);
|
||||
else _gfx_heal_sprite_noclip(oldx, oldy, p[0], p[2]);
|
||||
_bgi_end();
|
||||
gfx_set_bank(saved);
|
||||
|
||||
putsprite(x, y, img);
|
||||
}
|
||||
@@ -2,8 +2,20 @@
|
||||
* putimage — вывести образ bitmap левым-верхним углом в (left,top)
|
||||
* операцией op. Заголовок буфера: uint16 width, uint16 height.
|
||||
*
|
||||
* COPY — пишем src; XOR/OR/AND — комбинируем с экраном (нужно raw-
|
||||
* чтение); NOT — пишем ~src. Всё в одной W3-скобке (raw-плот/чтение).
|
||||
* COPY_PUT — через accel-ядро gfx_blit_part (block-copy строками, до
|
||||
* 256 байт за burst; Фаза A спрайтового дизайна) — С КЛИППИНГОМ по
|
||||
* экрану (superset поведения Turbo C, где выход за экран — UB) и
|
||||
* с учётом текущего банка (gfx_set_bank: 0x58 даёт аппаратную
|
||||
* прозрачность 0xFF, 0x54/0x5C — временный вывод).
|
||||
*
|
||||
* XOR/OR/AND — комбинируем с экраном по-пиксельно (raw-чтение +
|
||||
* raw-плот в одной W3-скобке); NOT — пишем ~src. Ускорение этих op
|
||||
* блочными режимами акселератора — «следующая версия» (см.
|
||||
* docs/sprite-api-design.md §9). Клиппинга на этом пути НЕТ
|
||||
* (per-pixel bounds-check остаётся только в safe-версии).
|
||||
*
|
||||
* Буфер bitmap обязан лежать вне W3 (< 0xC000) — на время операции W3
|
||||
* замаплен на видеобанк.
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
|
||||
@@ -15,9 +27,14 @@ void putimage(int left, int top, const void *bitmap, int op)
|
||||
|
||||
w = p[0] | (p[1] << 8);
|
||||
h = p[2] | (p[3] << 8);
|
||||
p += 4;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
if (op == COPY_PUT) {
|
||||
gfx_blit_part(left, top, bitmap, 0, 0, w, h);
|
||||
return;
|
||||
}
|
||||
|
||||
p += 4;
|
||||
_bgi_begin();
|
||||
for (y = 0; y < h; y++) {
|
||||
for (x = 0; x < w; x++) {
|
||||
@@ -26,9 +43,8 @@ void putimage(int left, int top, const void *bitmap, int op)
|
||||
case XOR_PUT: dst = (uint8_t)_bgi_read_raw(left + x, top + y) ^ src; break;
|
||||
case OR_PUT: dst = (uint8_t)_bgi_read_raw(left + x, top + y) | src; break;
|
||||
case AND_PUT: dst = (uint8_t)_bgi_read_raw(left + x, top + y) & src; break;
|
||||
case NOT_PUT: dst = (uint8_t)~src; break;
|
||||
case COPY_PUT:
|
||||
default: dst = src; break;
|
||||
case NOT_PUT:
|
||||
default: dst = (uint8_t)~src; break;
|
||||
}
|
||||
_bgi_plot_raw(left + x, top + y, dst);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* putsprite — вывести спрайт getimage-формата (0xFF = прозрачно, кадр
|
||||
* ≤ 64×64) в (x,y). Банк на время вызова — GFX_BANK_SPRITE (0x5C):
|
||||
* прозрачные байты (bit3) не пишутся, ОЗУ-копия фона (bit2) цела →
|
||||
* спрайт стирается gfx_heal/movesprite без save-буфера. Пользовательский
|
||||
* банк сохраняется/восстанавливается (уважаем вложенный temp-режим).
|
||||
*
|
||||
* Идёт через лин-ядро _gfx_blit_sprite (без split'а >256, 8-бит w/h) —
|
||||
* спрайтовая программа не тянет общий _gfx_blit_full. Клип по экрану —
|
||||
* в ядре. w,h — младшие байты заголовка (кадр ≤ 64).
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
|
||||
void putsprite(int x, int y, const void *img)
|
||||
{
|
||||
const uint8_t *p = (const uint8_t *)img;
|
||||
uint8_t saved = gfx_get_bank();
|
||||
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
_bgi_begin();
|
||||
if (_gfx_sprite_clip) _gfx_blit_sprite(x, y, img, 0, 0, p[0], p[2]);
|
||||
else _gfx_blit_sprite_noclip(x, y, img, 0, 0, p[0], p[2]);
|
||||
_bgi_end();
|
||||
gfx_set_bank(saved);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* sprite_flip — сахар канонического дабл-буфер-кадра поверх
|
||||
* sprite_update (docs/sprite-api-design.md §9.1):
|
||||
* draw-страница = скрытая → sprite_update → ждать кадр → показать её.
|
||||
* Флип по vsync делает переход tear-free. Приложение, рисующее свой
|
||||
* динамический фон/HUD, делает это банком 0x50 на СКРЫТОЙ странице ДО
|
||||
* sprite_flip (тогда heal учтёт его через ОЗУ-копию) — либо зовёт
|
||||
* составные части (gfx_set_draw_page/sprite_update/gfx_wait_vsync/
|
||||
* gfx_set_visible_page) сам.
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
#include <sprite.h>
|
||||
|
||||
void sprite_flip(sprite_t *arr, uint8_t count)
|
||||
{
|
||||
uint8_t hidden = gfx_get_visible_page() ^ 1;
|
||||
|
||||
gfx_set_draw_page(hidden);
|
||||
sprite_update(arr, count);
|
||||
gfx_wait_vsync();
|
||||
gfx_set_visible_page(hidden);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* sprite_init — инициализировать спрайт: img + размер кадра из заголовка
|
||||
* getimage (uint16 w, uint16 h). Спрайт невидим (flags = 0) и нигде не
|
||||
* нарисован (drawn[*].on = 0) — первый sprite_show + sprite_update его
|
||||
* выведет. Для атласа после init перезадать s->w/s->h (размер кадра) и
|
||||
* s->sx/s->sy. См. docs/sprite-api-design.md §9.1.
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
#include <sprite.h>
|
||||
|
||||
void sprite_init(sprite_t *s, const void *img)
|
||||
{
|
||||
const uint8_t *p = (const uint8_t *)img;
|
||||
|
||||
s->img = img;
|
||||
s->x = 0;
|
||||
s->y = 0;
|
||||
s->sx = 0;
|
||||
s->sy = 0;
|
||||
/* Размер кадра ≤ 255 (см. sprite.h) → младший байт uint16-заголовка;
|
||||
* старший байт для спрайта-кадра всегда 0. */
|
||||
s->w = p[0];
|
||||
s->h = p[2];
|
||||
s->flags = 0;
|
||||
s->drawn[0].on = 0;
|
||||
s->drawn[1].on = 0;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* sprite_update — отрисовать кадр сцены на ТЕКУЩЕЙ draw-странице p
|
||||
* (gfx_get_draw_page). Ядро batch-пасса движка v2 (см.
|
||||
* docs/sprite-api-design.md §9.1): вся работа кадра — здесь, двухпроходно
|
||||
* «heal ВСЕ → блит ВСЕ», под ОДНОЙ W3-скобкой и ОДНИМ банком на проход.
|
||||
*
|
||||
* Дисциплина двух проходов ОБЯЗАТЕЛЬНА: слияние heal+блит per-sprite не
|
||||
* работает на перекрытиях (heal соседа выкусывает только что
|
||||
* нарисованный спрайт — проверено на examples/balls 2026-07-11).
|
||||
*
|
||||
* Что перерисовывается: спрайт, у которого страница p ГРЯЗНАЯ (dbit —
|
||||
* взведён sprite_move/frame/show/hide/touch) ИЛИ флаг SPR_ALWAYS.
|
||||
* Неизменившиеся с прошлого визита этой страницы (и без SPR_ALWAYS) не
|
||||
* трогаются — суть retained-модели.
|
||||
*
|
||||
* Проход 1 (банк 0x50): heal старой позиции drawn[p] у тех, кто нарисован
|
||||
* (drawn[p].on) и требует обновления → снять drawn[p].on.
|
||||
* Проход 2 (банк 0x5C): блит видимых, требующих обновления, в (x,y) кадром
|
||||
* (sx,sy) → записать drawn[p] = (x,y), on = 1; снять dbit (страница p
|
||||
* актуальна для этого спрайта). z-order = порядок массива.
|
||||
*
|
||||
* DI живёт внутри leaf'ов (по строке) — скобка _bgi_begin/_bgi_end
|
||||
* прерывания НЕ трогает (только мапит W3), поэтому длина прохода не
|
||||
* влияет на аудио-ISR. w,h кадра ≤ 255 (см. sprite.h).
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
#include <sprite.h>
|
||||
|
||||
void sprite_update(sprite_t *arr, uint8_t count)
|
||||
{
|
||||
uint8_t p = gfx_get_draw_page(); /* 0 / 1 */
|
||||
uint8_t dbit = p ? _SPR_DIRTY1 : _SPR_DIRTY0;
|
||||
uint8_t saved = gfx_get_bank();
|
||||
uint8_t clip = _gfx_sprite_clip; /* раз на кадр: clip- или noclip-ядро */
|
||||
uint8_t i;
|
||||
sprite_t *s;
|
||||
|
||||
/* Проход 1: heal ВСЕХ старых позиций, требующих обновления. */
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
_bgi_begin();
|
||||
for (i = 0, s = arr; i < count; i++, s++) {
|
||||
if (s->drawn[p].on && (s->flags & (dbit | SPR_ALWAYS))) {
|
||||
if (clip) _gfx_heal_sprite(s->drawn[p].x, s->drawn[p].y, s->w, s->h);
|
||||
else _gfx_heal_sprite_noclip(s->drawn[p].x, s->drawn[p].y, s->w, s->h);
|
||||
s->drawn[p].on = 0;
|
||||
}
|
||||
}
|
||||
_bgi_end();
|
||||
|
||||
/* Проход 2: блит ВСЕХ видимых, требующих обновления. */
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
_bgi_begin();
|
||||
for (i = 0, s = arr; i < count; i++, s++) {
|
||||
if (s->flags & (dbit | SPR_ALWAYS)) {
|
||||
if (s->flags & SPR_VISIBLE) {
|
||||
if (clip) _gfx_blit_sprite(s->x, s->y, s->img, s->sx, s->sy, s->w, s->h);
|
||||
else _gfx_blit_sprite_noclip(s->x, s->y, s->img, s->sx, s->sy, s->w, s->h);
|
||||
s->drawn[p].x = s->x;
|
||||
s->drawn[p].y = s->y;
|
||||
s->drawn[p].on = 1;
|
||||
}
|
||||
s->flags &= ~dbit; /* страница p обновлена для спрайта */
|
||||
}
|
||||
}
|
||||
_bgi_end();
|
||||
|
||||
gfx_set_bank(saved);
|
||||
}
|
||||
+51
-1
@@ -46,9 +46,59 @@ void gfx_set_visible_page(uint8_t page); /* 0 or 1 */
|
||||
inline uint8_t gfx_get_visible_page(void) { return _gfx_visible_page; }
|
||||
void gfx_set_draw_page(uint8_t page); /* 0 or 1 */
|
||||
inline uint8_t gfx_get_draw_page(void) { return _gfx_draw_page; }
|
||||
inline void gfx_set_bank(uint8_t bank) { _gfx_bank = bank; } /* 0x50..0x5F */
|
||||
|
||||
/* Видеобанк W3 (0x50..0x5F): биты 2/3 номера страницы — аппаратные
|
||||
* подрежимы ЗАПИСИ (Иван Мак §4.3; docs/sprite-api-design.md §1),
|
||||
* действуют на ВСЕ примитивы рисования до следующего gfx_set_bank.
|
||||
* Биты 0/1 всегда 0 (резерв конфигураций). */
|
||||
#define GFX_BANK_NORMAL 0x50 /* запись в видео-ОЗУ + ОЗУ-копию */
|
||||
#define GFX_BANK_NOSHADOW 0x54 /* только видео-ОЗУ (врем. вывод) */
|
||||
#define GFX_BANK_TRANSPARENT 0x58 /* байт 0xFF не записывается */
|
||||
#define GFX_BANK_SPRITE 0x5C /* NOSHADOW + TRANSPARENT */
|
||||
|
||||
/* Прозрачный цвет аппаратной прозрачности (256-режим: пиксель;
|
||||
* 16-режим: ПАРА пикселей цвета 15). Цвет 255 нельзя нарисовать
|
||||
* через банк с битом 3 — он и есть прозрачный. */
|
||||
#define GFX_TRANSPARENT 0xFF
|
||||
|
||||
inline void gfx_set_bank(uint8_t bank) { _gfx_bank = bank; } /* GFX_BANK_* */
|
||||
inline uint8_t gfx_get_bank(void) { return _gfx_bank; }
|
||||
|
||||
/* Клип спрайтов по экрану (putsprite/movesprite/спрайтовый движок).
|
||||
* on=1 (дефолт) — клипить (спрайты могут уходить за края); on=0 —
|
||||
* ОТКЛЮЧИТЬ (приложение ГАРАНТИРУЕТ, что спрайты целиком на экране —
|
||||
* иначе запись за край портит соседнюю память, см. tests/spriteclip).
|
||||
* Выигрыш ~+19% на анимации: движок зовёт noclip-ядра без клип-кода
|
||||
* (не просто скип проверки — вся клип-математика физически отсутствует,
|
||||
* SDCC тесней раскладывает регистры). Флаг читается раз на спрайт.
|
||||
* НЕ влияет на putimage/gfx_blit (у них клип всегда). */
|
||||
extern uint8_t _gfx_sprite_clip;
|
||||
inline void gfx_sprite_clip(uint8_t on) { _gfx_sprite_clip = on; }
|
||||
inline uint8_t gfx_get_sprite_clip(void) { return _gfx_sprite_clip; }
|
||||
|
||||
/* ---- Блиттинг (ядро спрайтов; Фаза B sprite-api-design) ----------- *
|
||||
* Формат картинки — getimage: uint16 w, uint16 h, пиксели построчно
|
||||
* (1 байт/пиксель в 256-режиме). Буфер обязан лежать ВНЕ W3
|
||||
* (< 0xC000) — на время операции W3 замаплен на видеобанк.
|
||||
* Блит через accel block-copy (до 256 байт/burst), клиппинг по экрану
|
||||
* есть всегда; рисует ТЕКУЩИМ банком (gfx_set_bank). */
|
||||
|
||||
/* Блит целой картинки левым-верхним углом в (x,y). */
|
||||
void gfx_blit(int x, int y, const void *img);
|
||||
|
||||
/* Блит под-прямоугольника (sx,sy,w,h) картинки (атлас кадров:
|
||||
* кадр N ленты — sx = N*FRAME_W). Под-прямоугольник обязан лежать
|
||||
* внутри img; по экрану — клиппится. */
|
||||
void gfx_blit_part(int x, int y, const void *img,
|
||||
int sx, int sy, int w, int h);
|
||||
|
||||
/* Восстановить прямоугольник экрана из ОЗУ-копии (стирание спрайта/
|
||||
* оверлея, нарисованного банком с битом 2: 0x54/0x5C). Всегда
|
||||
* работает банком GFX_BANK_NORMAL независимо от gfx_set_bank;
|
||||
* клиппится по экрану. Буфер сохранения не нужен: ОЗУ-копия и есть
|
||||
* бэкап фона (чтение из #50..#5F всегда возвращает её). */
|
||||
void gfx_heal(int x, int y, int w, int h);
|
||||
|
||||
/* Block until the next frame (50 Hz). Типичный double-buffer:
|
||||
* gfx_set_draw_page(hidden); draw_frame(); gfx_wait_vsync();
|
||||
* gfx_set_visible_page(hidden); // tear-free flip */
|
||||
|
||||
@@ -194,9 +194,30 @@ unsigned imagesize(int left, int top, int right, int bottom);
|
||||
void getimage(int left, int top, int right, int bottom, void *bitmap);
|
||||
|
||||
/* Вывести образ левым-верхним углом в (left,top) операцией op
|
||||
* (COPY/XOR/OR/AND/NOT_PUT). */
|
||||
* (COPY/XOR/OR/AND/NOT_PUT). COPY_PUT — через акселератор, с
|
||||
* клиппингом по экрану и текущим банком gfx_set_bank (см. <gfx.h>);
|
||||
* XOR/OR/AND/NOT — по-пиксельно, без клиппинга. */
|
||||
void putimage(int left, int top, const void *bitmap, int op);
|
||||
|
||||
/* ---- Спрайты (аппаратная прозрачность; docs/sprite-api-design.md) - *
|
||||
* Спрайт — тот же буфер getimage-формата; прозрачные точки в данных =
|
||||
* байты 0xFF (GFX_TRANSPARENT). Прозрачность и сохранность фона
|
||||
* обеспечивает железо (банк GFX_BANK_SPRITE = 0x5C): прозрачные байты
|
||||
* не записываются, ОЗУ-копия фона не трогается — стирание без
|
||||
* save-буфера через gfx_heal (см. <gfx.h>).
|
||||
* ВАЖНО: фон/сцену рисовать ОБЫЧНЫМ банком (0x50) — то, что нарисовано
|
||||
* при активном бите 2 (0x54/0x5C), в ОЗУ-копию не попадает и heal
|
||||
* вернёт то, что лежало под ним. */
|
||||
|
||||
/* Вывести спрайт в (x,y): банк на время вызова — GFX_BANK_SPRITE,
|
||||
* затем прежний. Клиппится по экрану. */
|
||||
void putsprite(int x, int y, const void *img);
|
||||
|
||||
/* Переместить спрайт: восстановить фон под (oldx,oldy) (gfx_heal по
|
||||
* размерам img) и вывести спрайт в (x,y). Перекрытие старой и новой
|
||||
* позиций безопасно (heal читает фон из ОЗУ-копии). */
|
||||
void movesprite(int oldx, int oldy, int x, int y, const void *img);
|
||||
|
||||
/* ---- Текст ------------------------------------------------------- *
|
||||
* Шрифт 8×8 (системный). Рисуется текущим цветом на фоновом. */
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* sprite.h — managed-движок спрайтов (retained-модель) поверх ядра
|
||||
* gfx_blit/gfx_heal. Дизайн: docs/sprite-api-design.md §9.1.
|
||||
*
|
||||
* Мотивация — profile examples/balls vs asm-референса docs/samples/balls
|
||||
* (2026-07-12): ~2× разрыв не в W3-скобке, а в per-call C-обвязке
|
||||
* (клип/парс заголовка/save-restore банка/SDCC-фрейм), повторяемой 2×N
|
||||
* раз за кадр. Движок амортизирует её: sprite_update держит ОДНУ
|
||||
* W3-скобку и ОДИН банк на весь ПРОХОД, вызывая bracket-free ядра
|
||||
* (_gfx_heal_full/_gfx_blit_full) в цикле по спрайтам. DI остаётся
|
||||
* гранулярным (по строке в leaf) — под CBL/IM2-аудио длинный DI
|
||||
* недопустим.
|
||||
*
|
||||
* Retained-модель: sprite_move/frame/show/hide/touch меняют ТОЛЬКО
|
||||
* состояние (O(1), сколько угодно раз за кадр); всё рисование —
|
||||
* один раз в sprite_update, двухпроходно «heal ВСЕ → блит ВСЕ» (слияние
|
||||
* per-sprite выкусывает соседей на перекрытии — проверено). Учёт
|
||||
* «где спрайт реально нарисован на КАЖДОЙ из двух страниц» (drawn[2])
|
||||
* ведёт движок, не приложение.
|
||||
*
|
||||
* Формат картинки — getimage (uint16 w, uint16 h, пиксели построчно,
|
||||
* 1 байт/пиксель в 256-режиме); 0xFF = прозрачно (банк 0x5C). Буфер
|
||||
* обязан лежать ВНЕ W3 (< 0xC000). Размер КАДРА спрайта ≤ 64×64
|
||||
* (требование движка): кэш w,h — uint8_t, и блит идёт лин-ядром без
|
||||
* split'а на полосы >256 (спрайт заведомо ≤256 в строке). Для больших
|
||||
* картинок — gfx_blit/putimage напрямую (там общий путь со split'ом).
|
||||
* ЛЕНТА-АТЛАС может быть шире 64 (img_w — шаг), но КАДР ≤ 64.
|
||||
*
|
||||
* z-order = индекс в массиве (последний рисуется сверху). Массив
|
||||
* владеет приложением: без malloc, без лимитов; пересортировка z =
|
||||
* перестановка элементов (drawn-состояние едет со структурой).
|
||||
*/
|
||||
#ifndef SPRITE_H
|
||||
#define SPRITE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Публичные флаги sprite_t.flags. */
|
||||
#define SPR_VISIBLE 0x01u /* спрайт рисуется в sprite_update */
|
||||
#define SPR_ALWAYS 0x02u /* перерисовывать КАЖДЫЙ кадр (статик, над */
|
||||
/* которым постоянно ходят активные спрайты) */
|
||||
|
||||
/* Внутренние dirty-биты (владеет движок): «страница p требует обновления
|
||||
* этого спрайта». Приложению не трогать — только через sprite_*. */
|
||||
#define _SPR_DIRTY0 0x04u
|
||||
#define _SPR_DIRTY1 0x08u
|
||||
#define _SPR_DIRTY (_SPR_DIRTY0 | _SPR_DIRTY1)
|
||||
|
||||
typedef struct {
|
||||
const void *img; /* getimage-формат / атлас-лента (вне W3) */
|
||||
int x, y; /* ЛОГИЧЕСКАЯ позиция левого-верхнего угла кадра */
|
||||
int sx, sy; /* смещение кадра внутри img (атлас; 0 = целая) */
|
||||
uint8_t w, h; /* размер кадра (кэш заголовка), 1..255 */
|
||||
uint8_t flags; /* SPR_* | внутренние dirty */
|
||||
/* --- внутреннее (владеет движок) --- */
|
||||
struct {
|
||||
int x, y; /* где нарисован на странице p */
|
||||
uint8_t on; /* нарисован ли на странице p */
|
||||
} drawn[2];
|
||||
} sprite_t;
|
||||
|
||||
/* Инициализация: w,h из заголовка img, невидим, ничего не нарисовано.
|
||||
* Для АТЛАСА (кадр — под-прямоугольник) после init выставить s->w/s->h
|
||||
* в размер кадра и s->sx/s->sy — на нужный кадр (или sprite_frame). */
|
||||
void sprite_init(sprite_t *s, const void *img);
|
||||
|
||||
/* Кадр всей сцены: на ТЕКУЩЕЙ draw-странице (gfx_get_draw_page)
|
||||
* heal ВСЕХ старых позиций → блит ВСЕХ видимых (двухпроходно, одна
|
||||
* скобка/банк на проход). Спрайты без изменений с прошлого визита этой
|
||||
* страницы (и без SPR_ALWAYS) не трогаются. Для дабл-буфера звать после
|
||||
* gfx_set_draw_page(hidden); для одиночной страницы/курсора — прямо на
|
||||
* видимой. */
|
||||
void sprite_update(sprite_t *arr, uint8_t count);
|
||||
|
||||
/* Сахар канонического дабл-буфер-кадра: draw-страница = скрытая →
|
||||
* sprite_update → gfx_wait_vsync → сделать скрытую видимой (tear-free).
|
||||
* Динамический фон/HUD рисовать банком 0x50 на скрытой странице ДО
|
||||
* sprite_flip — тогда heal учтёт его через ОЗУ-копию. */
|
||||
void sprite_flip(sprite_t *arr, uint8_t count);
|
||||
|
||||
/* --- O(1) изменения состояния (inline: без call-оверхеда в физике) --- */
|
||||
|
||||
/* Переместить: меняет только логическую позицию + грязнит обе страницы. */
|
||||
inline void sprite_move(sprite_t *s, int x, int y) {
|
||||
if (s->x != x || s->y != y) { s->x = x; s->y = y; s->flags |= _SPR_DIRTY; }
|
||||
}
|
||||
|
||||
/* Сменить кадр атласа (под-прямоугольник img). */
|
||||
inline void sprite_frame(sprite_t *s, int sx, int sy) {
|
||||
if (s->sx != sx || s->sy != sy) { s->sx = sx; s->sy = sy; s->flags |= _SPR_DIRTY; }
|
||||
}
|
||||
|
||||
/* Показать (если был скрыт — грязнит обе страницы). */
|
||||
inline void sprite_show(sprite_t *s) {
|
||||
if (!(s->flags & SPR_VISIBLE)) s->flags |= (SPR_VISIBLE | _SPR_DIRTY);
|
||||
}
|
||||
|
||||
/* Скрыть (если был виден — грязнит: sprite_update сотрёт heal'ом). */
|
||||
inline void sprite_hide(sprite_t *s) {
|
||||
if (s->flags & SPR_VISIBLE) { s->flags &= ~SPR_VISIBLE; s->flags |= _SPR_DIRTY; }
|
||||
}
|
||||
|
||||
/* Принудительная перерисовка на ОБЕИХ страницах (напр. после того как
|
||||
* активный спрайт прошёл над статиком — приложение помечает статик). */
|
||||
inline void sprite_touch(sprite_t *s) { s->flags |= _SPR_DIRTY; }
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,4 @@
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||
EXAMPLE := blitperf
|
||||
EXTRA_FLAGS ?= --gfx 256 --memory small
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* blitperf — микробенчмарк спрайтового пути: операций в секунду (RTC)
|
||||
* для каждого слоя цепочки putsprite/gfx_heal. Диагностика разрыва
|
||||
* с asm-референсом (examples/balls ~1.5 мс/шар при теории ~0.3 мс):
|
||||
* отделяет стоимость leaf'ов от C-обвязки (клип/банк/заголовок) и
|
||||
* даёт опорные точки (bar — давно оптимизированный accel-путь).
|
||||
*
|
||||
* Методика: измеряется число вызовов за одну RTC-секунду (старт по
|
||||
* смене секунды; время проверяется раз в 8 операций). Строка «empty»
|
||||
* — цена самого цикла (вызов пустышки + getdatetime/8) — вычитать из
|
||||
* остальных: t_op = 1/n_op - 1/n_empty.
|
||||
*
|
||||
* ВНИМАНИЕ: использует ВНУТРЕННИЕ символы libbgi (прототипы
|
||||
* продублированы ниже — сверять с libbgi/_bgi.h при изменениях ABI).
|
||||
*
|
||||
* Запуск:
|
||||
* python3 toolchain/mame_interactive.py tests/blitperf/blitperf.exe \
|
||||
* --snap 22 --timeout 24
|
||||
*/
|
||||
|
||||
#include <graphics.h>
|
||||
#include <gfx.h>
|
||||
#include <time.h>
|
||||
|
||||
/* --- внутренности libbgi (копия из _bgi.h, НЕ публичный API) ------- */
|
||||
extern uint16_t _gfx_addr_base;
|
||||
void _bgi_begin(void);
|
||||
void _bgi_end(void);
|
||||
void _bgi_heal_rows_raw(uint8_t *scr, int y0, uint8_t w, uint8_t h);
|
||||
void _bgi_copy_rows_raw(const uint8_t *src, uint8_t *dst,
|
||||
uint8_t w, uint8_t h,
|
||||
int dstride, int sstride, uint8_t y0);
|
||||
/* ------------------------------------------------------------------- */
|
||||
|
||||
#define BW 16
|
||||
static uint8_t spr[4 + BW * BW];
|
||||
|
||||
static void build_sprite(void)
|
||||
{
|
||||
uint8_t *p = spr;
|
||||
int i;
|
||||
|
||||
*p++ = BW; *p++ = 0; *p++ = BW; *p++ = 0;
|
||||
for (i = 0; i < BW * BW; i++)
|
||||
*p++ = (uint8_t)(LIGHTRED + ((i & 7) == 0)); /* без 0xFF */
|
||||
}
|
||||
|
||||
/* --- операции ------------------------------------------------------ */
|
||||
|
||||
static void op_empty(void) { }
|
||||
|
||||
static void op_leaf_heal(void) /* leaf + W3-скобка, без C-обвязки */
|
||||
{
|
||||
_bgi_begin();
|
||||
_bgi_heal_rows_raw((uint8_t *)(_gfx_addr_base + 100), 100, BW, BW);
|
||||
_bgi_end();
|
||||
}
|
||||
|
||||
static void op_leaf_blit(void) /* copy-leaf + скобка, без обвязки */
|
||||
{
|
||||
_bgi_begin();
|
||||
_bgi_copy_rows_raw(spr + 4, (uint8_t *)(_gfx_addr_base + 100),
|
||||
BW, BW, 0, BW, 100);
|
||||
_bgi_end();
|
||||
}
|
||||
|
||||
static void op_gfx_heal(void) { gfx_heal(100, 100, BW, BW); }
|
||||
static void op_blit_part(void) { gfx_blit_part(100, 100, spr, 0, 0, BW, BW); }
|
||||
static void op_putsprite(void) { putsprite(100, 100, spr); }
|
||||
static void op_bar(void) /* опорная точка: accel-fill 16×16 */
|
||||
{
|
||||
bar(100, 100, 100 + BW - 1, 100 + BW - 1);
|
||||
}
|
||||
|
||||
/* --- измеритель ---------------------------------------------------- */
|
||||
|
||||
static uint16_t bench(void (*op)(void))
|
||||
{
|
||||
datetime_t dt;
|
||||
uint8_t s0, k;
|
||||
uint16_t n = 0;
|
||||
|
||||
getdatetime(&dt);
|
||||
s0 = dt.second;
|
||||
do { getdatetime(&dt); } while (dt.second == s0); /* синхр. к границе */
|
||||
s0 = dt.second;
|
||||
for (;;) {
|
||||
for (k = 0; k < 8; k++)
|
||||
op();
|
||||
n += 8;
|
||||
getdatetime(&dt);
|
||||
if (dt.second != s0)
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
static void say_num(int y, const char *tag, uint16_t v)
|
||||
{
|
||||
char buf[16];
|
||||
uint8_t i;
|
||||
uint16_t d = 10000;
|
||||
|
||||
for (i = 0; tag[i] && i < 9; i++)
|
||||
buf[i] = tag[i];
|
||||
while (i < 10) buf[i++] = ' ';
|
||||
for (; d; d /= 10)
|
||||
buf[i++] = (char)('0' + (v / d) % 10);
|
||||
buf[i] = 0;
|
||||
setcolor(WHITE);
|
||||
outtextxy(4, y, buf);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
build_sprite();
|
||||
initgraph();
|
||||
cleardevice();
|
||||
setcolor(WHITE);
|
||||
outtextxy(4, 4, "BLITPERF ops/sec (16x16)");
|
||||
|
||||
say_num(20, "empty", bench(op_empty));
|
||||
say_num(30, "leafheal", bench(op_leaf_heal));
|
||||
say_num(40, "leafblit", bench(op_leaf_blit));
|
||||
say_num(50, "gfx_heal", bench(op_gfx_heal));
|
||||
say_num(60, "blitpart", bench(op_blit_part));
|
||||
say_num(70, "putsprite", bench(op_putsprite));
|
||||
say_num(80, "bar", bench(op_bar));
|
||||
outtextxy(4, 96, "done");
|
||||
|
||||
for (;;) { }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||
EXAMPLE := blitw
|
||||
EXTRA_FLAGS ?= --gfx 256 --memory small
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* blitw — программный регресс ядра блиттинга gfx_blit_part по ширинам.
|
||||
*
|
||||
* Репро артефакта tests/sprites (клипнутые шары w=12 рисуются со
|
||||
* сдвигом): блитим паттерн-образ 24×24 разными ширинами банком 0x50
|
||||
* (запись идёт и в ОЗУ-копию → getpixel читает ровно то, что записано)
|
||||
* и побайтно сверяем прямоугольник + рамку 1px вокруг (должна остаться
|
||||
* фоном). Вердикт по каждой ширине печатается на экране:
|
||||
* w=NN PASS | w=NN FAIL @x,y eXX gYY (exp/got первого расхождения)
|
||||
*
|
||||
* Запуск:
|
||||
* python3 toolchain/mame_interactive.py tests/blitw/blitw.exe \
|
||||
* --snap 14 --timeout 16
|
||||
*/
|
||||
|
||||
#include <graphics.h>
|
||||
#include <gfx.h>
|
||||
|
||||
#define IW 24
|
||||
#define IH 24
|
||||
#define TX 200 /* позиция тестового прямоугольника (правее текста) */
|
||||
#define TY 30
|
||||
|
||||
static uint8_t img[4 + IW * IH];
|
||||
static char msg[40];
|
||||
|
||||
static uint8_t pat(int x, int y)
|
||||
{
|
||||
return (uint8_t)(((x + 1) * (y + 3)) & 0x7F); /* != 0xFF, != 0 кроме редких */
|
||||
}
|
||||
|
||||
static void build_img(void)
|
||||
{
|
||||
uint8_t *p = img;
|
||||
int x, y;
|
||||
|
||||
*p++ = IW; *p++ = 0; *p++ = IH; *p++ = 0;
|
||||
for (y = 0; y < IH; y++)
|
||||
for (x = 0; x < IW; x++)
|
||||
*p++ = pat(x, y);
|
||||
}
|
||||
|
||||
static char hexd(uint8_t v)
|
||||
{
|
||||
v &= 15;
|
||||
return v < 10 ? '0' + v : 'A' + v - 10;
|
||||
}
|
||||
|
||||
/* Проверить блит шириной w в (tx,TY): весь rect = pat, рамка = 0. */
|
||||
static void check_w(int w, int tx, int ty, int row)
|
||||
{
|
||||
int x, y;
|
||||
uint8_t exp, got;
|
||||
uint8_t fail = 0;
|
||||
|
||||
/* фон под областью + рамкой */
|
||||
setfillstyle(SOLID_FILL, 0);
|
||||
bar(tx - 2, ty - 2, tx + w + 1, ty + IH + 1);
|
||||
|
||||
gfx_blit_part(tx, ty, img, 0, 0, w, IH);
|
||||
|
||||
for (y = -1; y <= IH && !fail; y++) {
|
||||
for (x = -1; x <= w && !fail; x++) {
|
||||
exp = (y >= 0 && y < IH && x >= 0 && x < w) ? pat(x, y) : 0;
|
||||
got = (uint8_t)getpixel(tx + x, ty + y);
|
||||
if (got != exp) {
|
||||
fail = 1;
|
||||
msg[0] = 'w'; msg[1] = '0' + w / 10; msg[2] = '0' + w % 10;
|
||||
msg[3] = ' '; msg[4] = 'F'; msg[5] = 'A'; msg[6] = 'I';
|
||||
msg[7] = 'L'; msg[8] = ' ';
|
||||
msg[9] = 'x'; msg[10] = (char)('0' + (x + 1) / 10);
|
||||
msg[11] = (char)('0' + (x + 1) % 10);
|
||||
msg[12] = 'y'; msg[13] = (char)('0' + (y + 1) / 10);
|
||||
msg[14] = (char)('0' + (y + 1) % 10);
|
||||
msg[15] = ' '; msg[16] = 'e'; msg[17] = hexd(exp >> 4);
|
||||
msg[18] = hexd(exp);
|
||||
msg[19] = ' '; msg[20] = 'g'; msg[21] = hexd(got >> 4);
|
||||
msg[22] = hexd(got);
|
||||
msg[23] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!fail) {
|
||||
msg[0] = 'w'; msg[1] = '0' + w / 10; msg[2] = '0' + w % 10;
|
||||
msg[3] = ' '; msg[4] = 'P'; msg[5] = 'A'; msg[6] = 'S';
|
||||
msg[7] = 'S'; msg[8] = 0;
|
||||
}
|
||||
outtextxy(4, 40 + row * 10, msg);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
static const uint8_t widths[] = { 24, 16, 12, 8, 4, 2, 1 };
|
||||
uint8_t i;
|
||||
|
||||
build_img();
|
||||
initgraph();
|
||||
cleardevice();
|
||||
setcolor(WHITE);
|
||||
outtextxy(4, 4, "BLITW: gfx_blit_part width sweep");
|
||||
|
||||
for (i = 0; i < sizeof(widths); i++)
|
||||
check_w(widths[i], TX, TY, i);
|
||||
|
||||
/* и правый край, как в sprites: dst у конца строки page 0 */
|
||||
check_w(12, 308 - 12, TY, 7); /* dst близко к краю, но без клипа */
|
||||
|
||||
/* Регресс утечки CPU-байта write-триггера (см. шапку
|
||||
* _bgi_copy_rows_raw). Программно (getpixel = ОЗУ) утечка на MAME
|
||||
* 0.283 НЕ ловится: ОЗУ-ветка 0x58 пишет все байты, и burst
|
||||
* перезаписывает утечку в ОЗУ; жертвой остаётся только VRAM
|
||||
* (FF-скип). Поэтому проверка ВИЗУАЛЬНАЯ: блит образа с col0=0xFF
|
||||
* и последним столбцом YELLOW через 0x5C поверх ЗЕЛЁНОЙ плашки.
|
||||
* col0 (x=TX..) обязан остаться ЗЕЛЁНЫМ; до фикса он был YELLOW
|
||||
* (CPU-байт = последний байт burst-чтения). */
|
||||
{
|
||||
static uint8_t timg[4 + 16 * 8];
|
||||
uint8_t *p = timg;
|
||||
int x, y;
|
||||
|
||||
*p++ = 16; *p++ = 0; *p++ = 8; *p++ = 0;
|
||||
for (y = 0; y < 8; y++)
|
||||
for (x = 0; x < 16; x++)
|
||||
*p++ = (x == 0) ? 0xFF : (x == 15 ? YELLOW : LIGHTBLUE);
|
||||
|
||||
setfillstyle(SOLID_FILL, GREEN);
|
||||
bar(TX - 4, 150 - 4, TX + 19, 150 + 11);
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
gfx_blit(TX, 150, timg);
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
outtextxy(4, 40 + 8 * 10 + 4, "trig leak: col0 @200,150 green?");
|
||||
}
|
||||
|
||||
outtextxy(4, 40 + 9 * 10 + 6, "done");
|
||||
for (;;) { }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||
EXAMPLE := gfxbanks
|
||||
EXTRA_FLAGS ?= --gfx 256 --memory small
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* gfxbanks — Фаза 0 спрайтового дизайна: верификация подрежимов
|
||||
* видеостраниц #50..#5F (docs/sprite-api-design.md, «Фаза 0»).
|
||||
*
|
||||
* Семантика по доку (Иван Мак §4.3):
|
||||
* bit3 (0x58/0x5C) — запись байта 0xFF НЕ производится (прозрачность);
|
||||
* bit2 (0x54/0x5C) — запись только в видео-ОЗУ, ОЗУ-копия не трогается;
|
||||
* чтение из #50..#5F всегда возвращает ОЗУ-копию (getpixel = ОЗУ).
|
||||
*
|
||||
* ВНИМАНИЕ: вердикт MAME НЕ финален — тест прогоняется и в MAME, и на
|
||||
* железе. Известное расхождение: в MAME 0.283 (наш v306) FF-скип bit3
|
||||
* гасит ТОЛЬКО запись в VRAM, а теневое ОЗУ при bit2=0 всё равно
|
||||
* получает FF (ram_w: `if (BIT(~page,2)) ram[...]=data;
|
||||
* if (!(BIT(page,3) && data==0xff)) vram_w(...)`); в master-драйвере
|
||||
* уже полное подавление (`return`) — по доку. Через 0x5C расхождение
|
||||
* не проявляется (bit2 гасит ОЗУ-ветку) — поэтому putsprite планово
|
||||
* работает через 0x5C. На банке 0x58 зона SKIP это расхождение
|
||||
* ловит: chkC = FAIL на 0.283, PASS по доку/master.
|
||||
*
|
||||
* Сценарий (4 стадии, скриншот после каждой; палитра[255] = синий;
|
||||
* паузы — по RTC getdatetime, т.к. delayms в --memory small неработосп.
|
||||
* (irq_install → EINVAL), а скорость CPU в паузах не участвует):
|
||||
* S1 фон: вертикальные полосы RED/LIGHTGRAY через 0x50 (VRAM+ОЗУ).
|
||||
* S2 «спрайты» (после Фазы A putimage(COPY) идёт через акселератор,
|
||||
* поэтому CPU-путь в ZL — честный цикл putpixel):
|
||||
* ZL (CPU-путь): банк 0x5C, putpixel-ями круг GREEN с полями
|
||||
* 0xFF — ожидание: круг поверх полос, поля прозрачны;
|
||||
* ZR (accel-путь): банк 0x5C, bar 0xFF 64×64 (accel-fill:
|
||||
* ожидание no-op) + putimage того же круга (accel-COPY:
|
||||
* прозрачность полей на burst-пути);
|
||||
* ZB (bit3-skip): банк 0x58, putimage 0xFF (accel-copy, верх) +
|
||||
* bar 0xFF (accel-fill, низ) — ожидание по доку: no-op
|
||||
* экрана и ОЗУ.
|
||||
* Проверки по ОЗУ (getpixel): A = ОЗУ под ZL/ZR не тронуто (bit2);
|
||||
* C = ОЗУ под ZB не тронуто (полное подавление FF-записи).
|
||||
* Hex-пробы ОЗУ печатаются рядом (ожидание — цвета полос 04/07).
|
||||
* S3 КЛЮЧЕВОЙ ШАГ — 0xFF реально в VRAM: банк 0x54 (скипа нет),
|
||||
* квадрат 0xFF 32×32 поверх круга (CPU putpixel-ями в ZL,
|
||||
* accel-copy putimage'ем в ZR).
|
||||
* Экран: СИНИЙ квадрат → FF отображается как цвет 255
|
||||
* (семантика дока полная, стирание = heal);
|
||||
* ПОЛОСЫ → видеокарта подставляет ОЗУ-копию
|
||||
* (дешёвое стирание FF-заливкой работает);
|
||||
* круг/зелёный → 0x54 не эмулируется.
|
||||
* Проверка B = ОЗУ всё ещё полосы (bit2 у 0x54).
|
||||
* S4 heal: чтение+запись через 0x50 ЛЕВЫХ половин всех зон —
|
||||
* левые половины возвращаются к фону ИЗ ОЗУ-КОПИИ, правые
|
||||
* сохраняют артефакты S2/S3. NB: если ОЗУ под зоной испорчено
|
||||
* (chkC FAIL на 0.283), heal честно перенесёт порчу в VRAM —
|
||||
* зона SKIP посинеет на этой стадии.
|
||||
*
|
||||
* Запуск:
|
||||
* python3 toolchain/mame_interactive.py tests/gfxbanks/gfxbanks.exe \
|
||||
* --snap 13,17,21,25 --timeout 27
|
||||
*/
|
||||
|
||||
#include <graphics.h>
|
||||
#include <gfx.h>
|
||||
#include <time.h>
|
||||
|
||||
#define ZY 56 /* верх зон */
|
||||
#define ZH 64 /* зоны 64×64 */
|
||||
#define ZLX 16 /* 0x5C, CPU-путь */
|
||||
#define ZRX 120 /* 0x5C, accel-путь */
|
||||
#define ZBX 224 /* 0x58, bit3-skip */
|
||||
|
||||
#define STRIPE_Y0 48
|
||||
#define STRIPE_Y1 135
|
||||
|
||||
/* Точки-пробы внутри области, куда S2 писал НЕ-FF байты (круг r=28 и
|
||||
* квадрат +16..+47 содержат все три), — общие для проверок и hex-дампа. */
|
||||
static const uint8_t probe[3][2] = { {32, 32}, {20, 28}, {40, 36} };
|
||||
|
||||
/* Полосы шириной 8: цвет по абсолютному x */
|
||||
static uint8_t stripe_color(int x)
|
||||
{
|
||||
return ((x >> 3) & 1) ? LIGHTGRAY : RED;
|
||||
}
|
||||
|
||||
/* Спрайт 64×64: круг GREEN r=28, вокруг — прозрачный 0xFF */
|
||||
static uint8_t spr[4 + 64 * 64];
|
||||
/* Квадрат 32×32 сплошного 0xFF */
|
||||
static uint8_t ffsq[4 + 32 * 32];
|
||||
|
||||
static void build_images(void)
|
||||
{
|
||||
uint8_t *p = spr;
|
||||
int x, y;
|
||||
|
||||
*p++ = 64; *p++ = 0; *p++ = 64; *p++ = 0;
|
||||
for (y = 0; y < 64; y++)
|
||||
for (x = 0; x < 64; x++) {
|
||||
int dx = x - 32, dy = y - 32;
|
||||
*p++ = (dx * dx + dy * dy <= 28 * 28) ? GREEN : 0xFF;
|
||||
}
|
||||
|
||||
p = ffsq;
|
||||
*p++ = 32; *p++ = 0; *p++ = 32; *p++ = 0;
|
||||
for (x = 0; x < 32 * 32; x++)
|
||||
*p++ = 0xFF;
|
||||
}
|
||||
|
||||
/* ОЗУ-копия зоны — нетронутые полосы? */
|
||||
static uint8_t ram_is_stripes(int zx)
|
||||
{
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0; i < 3; i++) {
|
||||
int x = zx + probe[i][0];
|
||||
if (getpixel(x, ZY + probe[i][1]) != stripe_color(x))
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static char hexd(uint8_t v)
|
||||
{
|
||||
v &= 15;
|
||||
return v < 10 ? '0' + v : 'A' + v - 10;
|
||||
}
|
||||
|
||||
/* Hex-дамп трёх ОЗУ-проб зоны: "ZX 04 04 07" в колонке x=216 */
|
||||
static void show_probes(const char *tag, int zx, int ty)
|
||||
{
|
||||
char buf[12];
|
||||
uint8_t i, v;
|
||||
|
||||
buf[0] = tag[0];
|
||||
buf[1] = tag[1];
|
||||
for (i = 0; i < 3; i++) {
|
||||
v = (uint8_t)getpixel(zx + probe[i][0], ZY + probe[i][1]);
|
||||
buf[2 + i * 3] = ' ';
|
||||
buf[3 + i * 3] = hexd(v >> 4);
|
||||
buf[4 + i * 3] = hexd(v);
|
||||
}
|
||||
buf[11] = 0;
|
||||
outtextxy(216, ty, buf);
|
||||
}
|
||||
|
||||
/* Пауза n секунд по RTC (гранулярность 1 с, не зависит от CPU/режима) */
|
||||
static void pause_sec(uint8_t n)
|
||||
{
|
||||
datetime_t dt;
|
||||
uint8_t last;
|
||||
|
||||
getdatetime(&dt);
|
||||
last = dt.second;
|
||||
while (n) {
|
||||
getdatetime(&dt);
|
||||
if (dt.second != last) {
|
||||
last = dt.second;
|
||||
n--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* CPU-путь: вывести rect w×h из img (getimage-формат) в (x0,y0)
|
||||
* по-пиксельно через putpixel (LD (HL),A — НЕ акселератор). */
|
||||
static void blit_cpu(int x0, int y0, const void *img, int w, int h)
|
||||
{
|
||||
const uint8_t *p = (const uint8_t *)img + 4;
|
||||
int x, y;
|
||||
|
||||
for (y = 0; y < h; y++)
|
||||
for (x = 0; x < w; x++)
|
||||
putpixel(x0 + x, y0 + y, *p++);
|
||||
}
|
||||
|
||||
/* CPU-путь: залить rect цветом по-пиксельно. */
|
||||
static void fill_cpu(int x0, int y0, int w, int h, uint8_t color)
|
||||
{
|
||||
int x, y;
|
||||
|
||||
for (y = 0; y < h; y++)
|
||||
for (x = 0; x < w; x++)
|
||||
putpixel(x0 + x, y0 + y, color);
|
||||
}
|
||||
|
||||
/* heal: прочитать (вернёт ОЗУ-копию) и записать (уйдёт в VRAM+ОЗУ)
|
||||
* каждый пиксель прямоугольника банком 0x50. Прообраз gfx_heal. */
|
||||
static void heal(int x0, int y0, int w, int h)
|
||||
{
|
||||
int x, y;
|
||||
|
||||
for (y = y0; y < y0 + h; y++)
|
||||
for (x = x0; x < x0 + w; x++)
|
||||
putpixel(x, y, getpixel(x, y));
|
||||
}
|
||||
|
||||
static void say(int y, const char *s)
|
||||
{
|
||||
outtextxy(4, y, s);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
uint8_t chkA, chkB, chkC;
|
||||
int x;
|
||||
|
||||
build_images();
|
||||
initgraph();
|
||||
gfx_pal_set(0, 255, 0, 0, 255); /* цвет 255 = ярко-синий */
|
||||
cleardevice();
|
||||
setcolor(WHITE);
|
||||
|
||||
/* ---- S1: фон ------------------------------------------------ */
|
||||
say(4, "GFXBANKS: page submodes 50/54/58/5C");
|
||||
for (x = 0; x < 320; x += 8) {
|
||||
setfillstyle(SOLID_FILL, stripe_color(x));
|
||||
bar(x, STRIPE_Y0, x + 7, STRIPE_Y1);
|
||||
}
|
||||
outtextxy(ZLX, 44, "5C CPU");
|
||||
outtextxy(ZRX, 44, "5C ACC");
|
||||
outtextxy(ZBX, 44, "58 SKIP");
|
||||
say(148, "S1 stripes via 50");
|
||||
pause_sec(4);
|
||||
|
||||
/* ---- S2: спрайты через 0x5C / no-op через 0x58 --------------- */
|
||||
gfx_set_bank(0x5C);
|
||||
blit_cpu(ZLX, ZY, spr, 64, 64); /* CPU: круг + FF-поля */
|
||||
setfillstyle(SOLID_FILL, 255);
|
||||
bar(ZRX, ZY, ZRX + 63, ZY + 63); /* accel-fill FF — ждём no-op */
|
||||
putimage(ZRX, ZY, spr, COPY_PUT); /* accel-COPY: круг + FF-поля */
|
||||
gfx_set_bank(0x58);
|
||||
putimage(ZBX, ZY, ffsq, COPY_PUT); /* accel-copy FF, верх зоны */
|
||||
setfillstyle(SOLID_FILL, 255);
|
||||
bar(ZBX, ZY + 32, ZBX + 31, ZY + 63); /* accel-fill FF, низ зоны */
|
||||
gfx_set_bank(0x50);
|
||||
|
||||
chkA = ram_is_stripes(ZLX) && ram_is_stripes(ZRX);
|
||||
chkC = ram_is_stripes(ZBX);
|
||||
say(158, "S2 sprites 5C, ff-skip 58");
|
||||
say(168, chkA ? "A bit2 RAM intact: PASS" : "A bit2 RAM intact: FAIL");
|
||||
show_probes("ZL", ZLX, 168);
|
||||
say(178, chkC ? "C bit3 skip (RAM): PASS" : "C bit3 skip (RAM): FAIL");
|
||||
show_probes("ZB", ZBX, 178);
|
||||
pause_sec(4);
|
||||
|
||||
/* ---- S3: 0xFF реально в VRAM (банк 0x54, скипа нет) ---------- */
|
||||
gfx_set_bank(0x54);
|
||||
fill_cpu(ZLX + 16, ZY + 16, 32, 32, 0xFF); /* CPU-путь */
|
||||
putimage(ZRX + 16, ZY + 16, ffsq, COPY_PUT); /* accel-copy путь */
|
||||
gfx_set_bank(0x50);
|
||||
|
||||
chkB = ram_is_stripes(ZLX) && ram_is_stripes(ZRX);
|
||||
say(188, "S3 KEY: ff into vram via 54");
|
||||
say(198, chkB ? "B 54 RAM intact: PASS" : "B 54 RAM intact: FAIL");
|
||||
show_probes("ZR", ZRX, 198);
|
||||
say(208, "S3 look: BLUE=doc STRIPES=subst");
|
||||
pause_sec(4);
|
||||
|
||||
/* ---- S4: heal левых половин зон ------------------------------ */
|
||||
heal(ZLX, ZY, 32, ZH);
|
||||
heal(ZRX, ZY, 32, ZH);
|
||||
heal(ZBX, ZY, 32, ZH);
|
||||
say(218, "S4 healed left halves, done");
|
||||
|
||||
for (;;) { }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||
EXAMPLE := spriteclip
|
||||
EXTRA_FLAGS ?= --gfx 256 --memory small
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* spriteclip — ЭКСПЕРИМЕНТ (гипотеза пользователя 2026-07-12): режет ли
|
||||
* ЖЕЛЕЗО спрайт, выведенный за ПРАВЫЙ/НИЖНИЙ край экрана, БЕЗ
|
||||
* программного клипа? Или запись за x>=320 / y>=256 портит соседнюю
|
||||
* память (следующую строку / страницу 1 / верх экрана)?
|
||||
*
|
||||
* Метод: блит 16×16 НЕПРОЗРАЧНОГО белого спрайта мимо клипа — прямым
|
||||
* вызовом leaf'а _bgi_copy_rows_raw (в обход _gfx_blit_clip). Фон —
|
||||
* синий; зелёные линии-маркеры на x=319 и y=255 (граница экрана);
|
||||
* левый край (0..7) и верх (0..7) залиты красным-маркером, чтобы
|
||||
* поймать «заворот» переполнения именно туда.
|
||||
*
|
||||
* Читать скриншот:
|
||||
* - ПРАВЫЙ (спрайт в x=312): если видно только 8 левых колонок белого
|
||||
* (x=312..319), а правых 8 нет И красная левая полоса цела →
|
||||
* железо режет по правому краю. Если правые 8 колонок «завернулись»
|
||||
* белым в x=0..7 (поверх красного) или появился мусор — НЕ режет,
|
||||
* клип обязателен.
|
||||
* - НИЖНИЙ (спрайт в y=250): если видно 6 строк белого (y=250..255),
|
||||
* а строк за 256 нет И красная верхняя полоса цела → железо режет по
|
||||
* нижнему. Если белое всплыло сверху (y=0..) — Port_Y завернулся.
|
||||
* - УГОЛ (x=312,y=250): комбинация.
|
||||
*
|
||||
* НЕ для CI-регресса — разовый hardware-probe (и в MAME, и на железе).
|
||||
*/
|
||||
|
||||
#include <graphics.h>
|
||||
#include <gfx.h>
|
||||
#include <conio.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* Внутренние символы libbgi (глобальны в bgi256.lib) — тянем напрямую
|
||||
* ради no-clip блита. Обычному коду так делать НЕ надо (есть gfx_blit). */
|
||||
extern void _bgi_begin(void);
|
||||
extern void _bgi_end(void);
|
||||
extern void _bgi_copy_rows_raw(const uint8_t *src, uint8_t *dst,
|
||||
uint8_t w, uint8_t h,
|
||||
int dstride, int sstride, uint8_t y0);
|
||||
extern uint16_t _gfx_addr_base;
|
||||
|
||||
#define BW 16
|
||||
static uint8_t sprite[4 + BW * BW];
|
||||
|
||||
static void build_sprite(void)
|
||||
{
|
||||
uint8_t *p = sprite;
|
||||
int i;
|
||||
|
||||
*p++ = BW; *p++ = 0; *p++ = BW; *p++ = 0;
|
||||
for (i = 0; i < BW * BW; i++)
|
||||
*p++ = WHITE; /* непрозрачный: каждый байт пишется */
|
||||
}
|
||||
|
||||
/* Блит БЕЗ клипа: адрес = base + x, Port_Y стартует с y, строки шагают
|
||||
* Port_Y'ем (dstride 0). x/y могут выходить за экран — в этом и суть. */
|
||||
static void blit_noclip(int x, int y)
|
||||
{
|
||||
uint8_t *dst = (uint8_t *)(_gfx_addr_base + x);
|
||||
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
_bgi_begin();
|
||||
_bgi_copy_rows_raw(sprite + 4, dst, BW, BW, 0, BW, (uint8_t)y);
|
||||
_bgi_end();
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
build_sprite();
|
||||
initgraph();
|
||||
|
||||
/* Фон синий. */
|
||||
setfillstyle(SOLID_FILL, BLUE);
|
||||
bar(0, 0, 319, 255);
|
||||
|
||||
/* Красные маркер-полосы: левые 8 колонок и верхние 8 строк —
|
||||
* сюда «завернётся» переполнение вправо/вниз, если железо не режет. */
|
||||
setfillstyle(SOLID_FILL, RED);
|
||||
bar(0, 0, 7, 255);
|
||||
bar(0, 0, 319, 7);
|
||||
|
||||
/* Зелёные маркеры границы экрана. */
|
||||
setcolor(GREEN);
|
||||
line(319, 0, 319, 255);
|
||||
line(0, 255, 319, 255);
|
||||
|
||||
/* Sanity: клипнутый putsprite тем же спрайтом в безопасном месте. */
|
||||
putsprite(140, 120, sprite);
|
||||
|
||||
/* Эксперимент — три no-clip блита за края: */
|
||||
blit_noclip(312, 100); /* правый: x+16=328, 8 колонок за 320 */
|
||||
blit_noclip(100, 250); /* нижний: y+16=266, 10 строк за 256 */
|
||||
blit_noclip(312, 250); /* угол: и вправо, и вниз */
|
||||
|
||||
getch();
|
||||
closegraph();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||
EXAMPLE := sprites
|
||||
EXTRA_FLAGS ?= --gfx 256 --memory small
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* sprites — тест Фазы B спрайтового API (docs/sprite-api-design.md §3):
|
||||
* putsprite / movesprite / gfx_heal / gfx_blit_part поверх accel-ядра.
|
||||
*
|
||||
* Сцена: полосатый фон (вертикальные полосы 8px RED/LIGHTGRAY, банк
|
||||
* 0x50 → фон и в VRAM, и в ОЗУ-копии). Спрайт — шар 24×24 (YELLOW с
|
||||
* RED-ободом), углы = 0xFF (GFX_TRANSPARENT).
|
||||
*
|
||||
* Стадии (паузы по RTC, скриншот после каждой):
|
||||
* S1 фон + разметка.
|
||||
* S2 putsprite:
|
||||
* - в центре зоны (C) — прозрачные углы: полосы сквозь них;
|
||||
* - 4 клип-позиции: слева x=-12, справа x=308, сверху y=-10,
|
||||
* снизу y=244 — рисуется только видимая часть, соседняя
|
||||
* память не портится;
|
||||
* - heal-проба: putsprite в H=(60,130) и тут же gfx_heal того же
|
||||
* rect — шара БЫТЬ НЕ ДОЛЖНО (первая проверка heal src==dst
|
||||
* через акселератор — риск §10.4 дизайна);
|
||||
* - атлас: gfx_blit_part кадра 1 (MAGENTA-шар) из ленты 2×24×24
|
||||
* в (270,130) банком GFX_BANK_SPRITE.
|
||||
* Проверка A (по ОЗУ, getpixel = ОЗУ-копия): полосы под C целы.
|
||||
* S3 movesprite: шар из (16,60) 12 шагов по (+16,+8) до (208,156) —
|
||||
* след обязан быть чистым (heal старой позиции на каждом шаге).
|
||||
* Проверка B: ОЗУ вдоль пути — полосы.
|
||||
*
|
||||
* VRAM программно не читается (write-only) — корректность экрана
|
||||
* подтверждают скриншоты; программные проверки ловят порчу ОЗУ-копии.
|
||||
*
|
||||
* Запуск:
|
||||
* python3 toolchain/mame_interactive.py tests/sprites/sprites.exe \
|
||||
* --snap 13,17,21 --timeout 23
|
||||
*/
|
||||
|
||||
#include <graphics.h>
|
||||
#include <gfx.h>
|
||||
#include <time.h>
|
||||
|
||||
#define BW 24 /* сторона спрайта */
|
||||
#define BG_Y0 40
|
||||
#define BG_Y1 199
|
||||
|
||||
#define CX 150 /* центральный putsprite */
|
||||
#define CY 100
|
||||
|
||||
static uint8_t ball[4 + BW * BW]; /* YELLOW шар */
|
||||
static uint8_t strip[4 + 2 * BW * BW]; /* лента: кадр0 YELLOW, кадр1 MAGENTA */
|
||||
|
||||
static uint8_t stripe_color(int x)
|
||||
{
|
||||
return ((x >> 3) & 1) ? LIGHTGRAY : RED;
|
||||
}
|
||||
|
||||
/* Шар: r<=9 — fill, r 9..11 — ring, дальше прозрачно (0xFF). */
|
||||
static uint8_t ball_pixel(int x, int y, uint8_t fill, uint8_t ring)
|
||||
{
|
||||
int dx = x - BW / 2, dy = y - BW / 2;
|
||||
int d2 = dx * dx + dy * dy;
|
||||
|
||||
if (((x == 0 || x == BW - 1) && ((y >=0 && y <= 3) || (y >=BW - 4 && y <= BW - 1))) ||
|
||||
((y == 0 || y == BW - 1) && ((x >=0 && x <= 3) || (x >=BW - 4 && x <= BW - 1)))) return GREEN;
|
||||
if (x==0) return GFX_TRANSPARENT;
|
||||
if (d2 <= 81) return fill;
|
||||
if (d2 <= 121) return ring;
|
||||
return GFX_TRANSPARENT;
|
||||
}
|
||||
|
||||
static void build_images(void)
|
||||
{
|
||||
uint8_t *p;
|
||||
int x, y, f;
|
||||
|
||||
p = ball;
|
||||
*p++ = BW; *p++ = 0; *p++ = BW; *p++ = 0;
|
||||
for (y = 0; y < BW; y++)
|
||||
for (x = 0; x < BW; x++)
|
||||
*p++ = ball_pixel(x, y, YELLOW, RED);
|
||||
|
||||
/* Лента 48×24: кадры лежат рядом по x (кадр N: sx = N*BW). */
|
||||
p = strip;
|
||||
*p++ = 2 * BW; *p++ = 0; *p++ = BW; *p++ = 0;
|
||||
for (y = 0; y < BW; y++)
|
||||
for (f = 0; f < 2; f++)
|
||||
for (x = 0; x < BW; x++)
|
||||
*p++ = ball_pixel(x, y, f ? LIGHTMAGENTA : YELLOW,
|
||||
f ? BLUE : RED);
|
||||
}
|
||||
|
||||
/* ОЗУ-копия (getpixel) в трёх точках зоны 24×24 — нетронутые полосы? */
|
||||
static uint8_t ram_is_stripes(int zx, int zy)
|
||||
{
|
||||
static const uint8_t off[3][2] = { {12, 12}, {8, 16}, {16, 6} };
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0; i < 3; i++) {
|
||||
int x = zx + off[i][0];
|
||||
if (getpixel(x, zy + off[i][1]) != stripe_color(x))
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void pause_sec(uint8_t n)
|
||||
{
|
||||
datetime_t dt;
|
||||
uint8_t last;
|
||||
|
||||
getdatetime(&dt);
|
||||
last = dt.second;
|
||||
while (n) {
|
||||
getdatetime(&dt);
|
||||
if (dt.second != last) {
|
||||
last = dt.second;
|
||||
n--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void say(int y, const char *s)
|
||||
{
|
||||
outtextxy(4, y, s);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
uint8_t chkA, chkB;
|
||||
int x, i;
|
||||
|
||||
build_images();
|
||||
initgraph();
|
||||
cleardevice();
|
||||
gfx_sprite_clip(1);
|
||||
setcolor(WHITE);
|
||||
|
||||
/* ---- S1: фон -------------------------------------------------- */
|
||||
say(4, "SPRITES: putsprite/movesprite/heal");
|
||||
for (x = 0; x < 320; x += 8) {
|
||||
setfillstyle(SOLID_FILL, stripe_color(x));
|
||||
bar(x, BG_Y0, x + 7, BG_Y1);
|
||||
}
|
||||
say(208, "S1 bg");
|
||||
(void)getchar();
|
||||
|
||||
/* ---- S2: putsprite + клипы + heal-проба + атлас ---------------- */
|
||||
putsprite(CX, CY, ball); /* центр: прозрачные углы */
|
||||
(void)getchar();
|
||||
putsprite(-12, 52, ball); /* клип слева */
|
||||
(void)getchar();
|
||||
putsprite(308, 82, ball); /* клип справа */
|
||||
(void)getchar();
|
||||
putsprite(297, 162, ball); /* клип справа */
|
||||
(void)getchar();
|
||||
putsprite(60, -10, ball); /* клип сверху */
|
||||
(void)getchar();
|
||||
putsprite(290, 244, ball); /* клип снизу */
|
||||
(void)getchar();
|
||||
putsprite(60, 130, ball); /* heal-проба: нарисовать... */
|
||||
(void)getchar();
|
||||
gfx_heal(60, 130, BW, BW); /* ...и стереть (шара НЕТ) */
|
||||
(void)getchar();
|
||||
gfx_set_bank(GFX_BANK_SPRITE); /* атлас: кадр 1 (MAGENTA) */
|
||||
(void)getchar();
|
||||
gfx_blit_part(270, 130, strip, BW, 0, BW, BW);
|
||||
(void)getchar();
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
(void)getchar();
|
||||
|
||||
chkA = ram_is_stripes(CX, CY);
|
||||
say(218, "S2 putsprite+clip, heal@60,130");
|
||||
say(228, chkA ? "A RAM under sprite: PASS"
|
||||
: "A RAM under sprite: FAIL");
|
||||
pause_sec(4);
|
||||
|
||||
/* ---- S3: movesprite — след обязан быть чистым ------------------ */
|
||||
x = 16;
|
||||
putsprite(x, 60, ball);
|
||||
(void)getchar();
|
||||
for (i = 0; i < 12; i++) {
|
||||
movesprite(x, 60 + (x - 16) / 2, x + 16, 60 + (x - 16 + 16) / 2,
|
||||
ball);
|
||||
x += 16;
|
||||
(void)getchar();
|
||||
}
|
||||
chkB = ram_is_stripes(16, 60) && ram_is_stripes(112, 108);
|
||||
say(238, chkB ? "B RAM along path: PASS"
|
||||
: "B RAM along path: FAIL");
|
||||
say(248, "S3 moved 16,60 -> 208,156 done");
|
||||
|
||||
(void)getchar();
|
||||
}
|
||||
@@ -212,7 +212,12 @@ end)
|
||||
|
||||
|
||||
def check_no_stray_mame():
|
||||
out = subprocess.run(["pgrep", "-fl", "mame"], capture_output=True, text=True).stdout
|
||||
# Ловим только запущенный ЭМУЛЯТОР (mame/mame.arm/mame.exe c машиной
|
||||
# sprinter), а не всё, где в командной строке встречается «mame»:
|
||||
# голый паттерн ложно срабатывал на параллельную сборку MAME из
|
||||
# исходников (clang/make с путями .../src/mame/...).
|
||||
out = subprocess.run(["pgrep", "-fl", r"mame[^ ]* sprinter( |$)"],
|
||||
capture_output=True, text=True).stdout
|
||||
if out.strip():
|
||||
print("ОШИБКА: есть запущенные копии MAME — закройте их:")
|
||||
print(out)
|
||||
|
||||
Reference in New Issue
Block a user