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
+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