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:
2026-07-12 21:47:37 +03:00
parent 78161561e7
commit 72ce66275e
42 changed files with 2835 additions and 32 deletions
+104 -13
View File
@@ -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
View File
@@ -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.
+7 -1
View File
@@ -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
1 # Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
3 argv
4 assrtest
5 attrprob
6 balls
7 banked
8 bankedbg
9 banklocl
10 banktest
11 bgi_img
12 bgitest
13 bios_text
14 blitperf
15 blitw
16 cat
17 cblstream
18 cbltest
27 gets
28 gfx_dbuf
29 gfx_demo
30 gfxbanks
31 hello
32 hello2
33 irqtest
43 seek
44 simple
45 solidt
46 spriteclip
47 sprites
48 stattest
49 stdlib
50 stest2
+594
View File
@@ -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 (~5080Т); ядро платит на
СТРОКУ ~4060Т 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: пп. 13 закрыты для 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.
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := balls
EXTRA_FLAGS ?= --gfx 256
include $(PROJ_ROOT)/app.mk
+185
View File
@@ -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;
}
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
make clean
make run
+66
View File
@@ -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:
+90
View File
@@ -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;
}
+130
View File
@@ -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;
}
+72
View File
@@ -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;
}
+57
View File
@@ -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);
}
+69
View File
@@ -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);
}
+32
View File
@@ -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);
}
+36
View File
@@ -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);
}
+25
View File
@@ -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);
}
+13
View File
@@ -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);
}
+7
View File
@@ -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;
+29 -5
View File
@@ -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();
}
+14
View File
@@ -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));
}
+33
View File
@@ -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();
}
+29
View File
@@ -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);
}
+31
View File
@@ -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);
}
+22 -6
View File
@@ -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);
}
+25
View File
@@ -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);
}
+22
View File
@@ -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);
}
+27
View File
@@ -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;
}
+68
View File
@@ -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
View File
@@ -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 */
+22 -1
View File
@@ -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 (системный). Рисуется текущим цветом на фоновом. */
+107
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := blitperf
EXTRA_FLAGS ?= --gfx 256 --memory small
include $(PROJ_ROOT)/app.mk
+131
View File
@@ -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 (;;) { }
}
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := blitw
EXTRA_FLAGS ?= --gfx 256 --memory small
include $(PROJ_ROOT)/app.mk
+136
View File
@@ -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 (;;) { }
}
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := gfxbanks
EXTRA_FLAGS ?= --gfx 256 --memory small
include $(PROJ_ROOT)/app.mk
+259
View File
@@ -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 (;;) { }
}
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := spriteclip
EXTRA_FLAGS ?= --gfx 256 --memory small
include $(PROJ_ROOT)/app.mk
+97
View File
@@ -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;
}
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := sprites
EXTRA_FLAGS ?= --gfx 256 --memory small
include $(PROJ_ROOT)/app.mk
+189
View File
@@ -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();
}
+6 -1
View File
@@ -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)