diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv index 65c4e7b..cf1d3fe 100644 --- a/docs/size_baseline.tsv +++ b/docs/size_baseline.tsv @@ -1,5 +1,6 @@ # Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update accfill 3786 +accop 6501 argv 3431 assrtest 3847 atlas 9193 @@ -8,7 +9,7 @@ banked 1056 bankedbg 1067 banklocl 4683 banktest 3767 -bgi_img 7200 +bgi_img 8451 bgitest 3751 bios_text 4461 blitperf 5860 @@ -32,7 +33,7 @@ gfxbanks 6044 hello 4167 hello2 4244 irqtest 6680 -kbdpoll 1219 +kbdpoll 1282 kbdraw 4911 ls 4835 malloc 4461 diff --git a/docs/sprite-api-design.md b/docs/sprite-api-design.md index 361a4af..ee1b5c0 100644 --- a/docs/sprite-api-design.md +++ b/docs/sprite-api-design.md @@ -485,7 +485,19 @@ getimage/буфер не нужен. Перерисовка — из главн - **.spr-ресурс**: файловый заголовок-обёртка (магия/версия/hotspot) вокруг getimage-payload + загрузчик + PC-конвертер (PNG→spr, iconv- стиль пайплайн). In-memory формат не меняется. -- **XOR/OR/AND_PUT через акселератор** (блочные режимы есть в железе). +- ~~**XOR/OR/AND_PUT через акселератор** (блочные режимы есть в железе).~~ + **СДЕЛАНО 2026-08-11** (tests/accop, tests/bgi_img P1/P2): блочные + AND/OR/XOR акселератора подняты в libbgi как полный набор — + `_bgi_blit_rows_op_raw`/`_bgi_blit_cols_op_raw` (ядра) → + `gfx_blit_op`/`gfx_blit_part_op` (строками) и + `gfx_blit_cols_op`/`gfx_blit_cols_part_wx_op` (колонками, с флипом и + клип-окном); `putimage(XOR/OR/AND_PUT)` переведён на них, попиксельным + остался только NOT_PUT (инверсия источника — у железа такого режима нет). + Одна функция на три операции: опкод `and/or/xor (hl)` патчится SMC, как + размер блока. Две оговорки — в шапке `common/_gfx_blit_full_op.c`: + операция читает ОЗУ-копию экрана (при банках 0x54/0x5C это чистый фон, + а не нарисованное поверх), и аппаратная прозрачность #FF совместима с + AND/OR, но НЕ с XOR (источнику для XOR нужен прозрачный 0x00). - **RLE-сжатие** (распаковка при загрузке — фича загрузчика). - **Вертикальный блит** (`LD A,A`-режим) — для повёрнутых спрайтов не хватает и его; не тянем. diff --git a/libbgi/_bgi.h b/libbgi/_bgi.h index 1067b64..fe3a261 100644 --- a/libbgi/_bgi.h +++ b/libbgi/_bgi.h @@ -87,6 +87,15 @@ void _bgi_blit_rows_raw(const uint8_t *src, uint8_t *dst, uint8_t w, uint8_t h, int sstride, uint8_t y0); +/* То же с ЛОГИЧЕСКОЙ ОПЕРАЦИЕЙ (приёмник = приёмник источник): между + * чтением источника и записью вставлен третий burst — чтение приёмника в + * режиме операции. Всё горизонтально, поэтому три burst'а идут одной + * цепочкой без сброса Port_Y. op — ОПКОД `and/or/xor (hl)` (GFX_OP_* из + * ), патчится SMC. bgi256/_bgi_blit_rows_op_raw.c. */ +void _bgi_blit_rows_op_raw(const uint8_t *src, uint8_t *dst, + uint8_t w, uint8_t h, + int sstride, uint8_t y0, uint8_t op); + /* КОЛОНОЧНАЯ специализация (column-major src -> экран): каждая колонка — * вертикальный accel-burst (LD A,A, Port_Y авто-шаг); dst +1/колонку, src * +sstride/колонку. Даёт бесплатный горизонтальный флип (sstride<0 + src @@ -96,6 +105,15 @@ void _bgi_blit_cols_raw(const uint8_t *src, uint8_t *dst, uint8_t w, uint8_t h, int sstride, uint8_t y0); +/* То же с ЛОГИЧЕСКОЙ ОПЕРАЦИЕЙ (приёмник = приёмник источник): между + * чтением источника и записью вставлен третий burst — чтение приёмника в + * режиме операции. op — ОПКОД `and/or/xor (hl)` (GFX_OP_* из ), + * патчится SMC. bgi256/_bgi_blit_cols_op_raw.c (там же — прозрачность и + * почему op-burst читает ОЗУ-копию). */ +void _bgi_blit_cols_op_raw(const uint8_t *src, uint8_t *dst, + uint8_t w, uint8_t h, + int sstride, uint8_t y0, uint8_t op); + /* Специализация для heal: src == dst == адрес экрана, оба страйда 0 * (строку выбирает Port_Y) — адресной арифметики в цикле нет, строка * 77Т + burst'ы. y0 — стартовый Port_Y; w,h: 0 = 256. Без скобки и @@ -148,6 +166,13 @@ 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); +/* То же с ЛОГИЧЕСКОЙ ОПЕРАЦИЕЙ (приёмник = приёмник источник) — + * близнец _gfx_blit_full поверх _bgi_blit_rows_op_raw. op — опкод + * GFX_OP_* из . common/_gfx_blit_full_op.c (там же — прозрачность + * и почему op-burst читает ОЗУ-копию). */ +void _gfx_blit_full_op(int x, int y, const void *img, + int sx, int sy, int w, int h, uint8_t op); + /* Лин-ядра для СПРАЙТОВ (кадр ≤ 64×64, sprite.h): как *_full, но без * split'а >256 и с 8-битными w/h (движок/putsprite — горячий путь; * программа только со спрайтами не тянет общие *_full со split'ом). diff --git a/libbgi/bgi256/_bgi_blit_cols_op_raw.c b/libbgi/bgi256/_bgi_blit_cols_op_raw.c new file mode 100644 index 0000000..97923c9 --- /dev/null +++ b/libbgi/bgi256/_bgi_blit_cols_op_raw.c @@ -0,0 +1,135 @@ +/* + * _bgi_blit_cols_op_raw — accel-блит КОЛОНКАМИ с ЛОГИЧЕСКОЙ ОПЕРАЦИЕЙ: + * приёмник = приёмник источник, где = AND / OR / XOR. Клон + * _bgi_blit_cols_raw (тот же column-major источник, тот же бесплатный + * горизонтальный флип), но между чтением источника и записью вставлен + * ТРЕТИЙ burst — чтение приёмника в режиме операции. + * + * КАК ЭТО РАБОТАЕТ (docs/new/06-accel.md §6.2, подтверждено драйвером MAME + * src/mame/sinclair/sprinter.cpp — accel_control_r/update_accel_buffer). + * Опкоды `AND (HL)` #A6, `OR (HL)` #B6, `XOR (HL)` #AE НЕ являются + * отдельным режимом акселератора: их M1-выборка переводит внутренний + * автомат из «буфер := память» в «буфер = память», а сам burst + * запускается операндным чтением (HL) — в ТЕКУЩЕМ режиме направления + * (гориз. LD L,L / верт. LD A,A). То есть операция ОРТОГОНАЛЬНА + * направлению: для column-major спрайтов работает так же, как копия. + * Следующее армирование (LD L,L / LD A,A / LD B,B) возвращает автомат в + * «буфер := память». + * + * ОДНА функция на три операции: опкод операции патчится SMC при входе + * (байт op), ровно как размер блока и страйд. op — это и есть опкод + * (GFX_OP_AND/OR/XOR в ); проверку значения делает вызывающий. + * + * ПОСЛЕДОВАТЕЛЬНОСТЬ НА КОЛОНКУ (три burst'а вместо двух): + * OUT Port_Y = y0 ; accel разоружён — операнд OUT безопасен + * LD L,L / LD A,(DE) ; гориз. чтение: h смежных байт src -> буфер + * LD A,A / (HL) ; верт. чтение: буфер = видео-колонка + * LD B,B ; СТОП — разоружить перед вторым OUT + * OUT Port_Y = y0 ; вернуть Y: верт. burst сдвинул его на +h + * LD A,A / LD (HL),A ; верт. запись: буфер -> видео-колонка + * LD B,B + * Второй OUT обязателен: вертикальный burst ШАГАЕТ Port_Y (§6.1), и после + * op-чтения он стоит на y0+h. СТОП перед OUT обязателен по квирку + * memory/accel_operand_fetch_retrigger (fetch операнда под армированным + * accel перезапускает burst). Буфер и размер блока переживают LD B,B + * (§6.3) — это и позволяет разнести чтение и запись. + * + * ЧТО ЧИТАЕТ op-burst: страницы #50..#5F на ЧТЕНИЕ всегда отдают ОЗУ-копию + * экрана, а не видео-ОЗУ (memory/sprinter_vram_transparency). Значит при + * банке 0x54/0x5C (бит 2 — «не писать в ОЗУ-копию») операция считает + * ЧИСТЫЙ ФОН, а не то, что уже нарисовано поверх него. Нужна операция с + * реально видимым содержимым — вызывающий обязан рисовать банком 0x50 + * (GFX_BANK_NORMAL), тогда копия и видео-ОЗУ совпадают. + * + * ПРОЗРАЧНОСТЬ. Аппаратная прозрачность (банки 0x58/0x5C — не писать байт + * #FF) действует на РЕЗУЛЬТАТ, а не на источник, поэтому даром достаётся + * только двум операциям: + * AND — #FF нейтрален сам по себе (#FF & bg = bg); + * OR — #FF | bg = #FF, и запись такого байта подавляется банком 0x58/0x5C; + * XOR — НЕ работает: #FF ^ bg = ~bg, инверсия фона на прозрачных пикселях. + * Для XOR источник обязан иметь прозрачный индекс 0x00 (нейтральный к XOR + * и к OR) и рисоваться банком без подавления. Подробно — шапка + * common/gfx_blit_cols_part_wx_op.c. + * + * ВХОД (__sdcccall(1)): src→HL, dst→DE; стек: w 4(ix), h 5(ix), + * sstride 6/7(ix), y0 8(ix), op 9(ix). Смысл w/h/sstride/y0 — как у + * _bgi_blit_cols_raw. Callee-pop 6 Б. + * + * Raw: без клиппинга (вызывающий); W3 замаплен (_bgi_begin/end снаружи); + * банк выставлен вызывающим. src ВНЕ W3. ОДИН DI на весь блит (втрое + * больше трафика, чем у копии — для больших спрайтов резать вызывающему). + * Клоббер: AF/BC/DE/HL; IX сохраняется. + */ + +#include "../_bgi.h" + +void _bgi_blit_cols_op_raw(const uint8_t *src, uint8_t *dst, + uint8_t w, uint8_t h, + int sstride, uint8_t y0, uint8_t op) __naked +{ + (void)src; (void)dst; (void)w; (void)h; (void)sstride; (void)y0; (void)op; + __asm + push ix + ld ix, #0 + add ix, sp + ;; SMC: размер accel-блока <- h (высота колонки) + ld a, 5 (ix) + ld (bo_len_imm), a + ;; SMC: src-страйд между колонками (8-бит adc-цепочка, ± на вызов) + ld a, 6 (ix) + ld (bo_slo_imm), a + ld a, 7 (ix) + ld (bo_shi_imm), a + ;; SMC: опкод операции — and/or/xor (hl) (#A6/#B6/#AE) + ld a, 9 (ix) + ld (bo_op_imm), a + ld b, 4 (ix) ; B = число колонок (0 => 256: djnz) + ld c, 8 (ix) ; C = y0 (Port_Y, константа) + ;; Операция существует только в форме (HL) — приёмник обязан + ;; жить в HL; источник переезжает в DE (читается LD A,(DE)). + ex de, hl ; HL = dst (видео), DE = src (ОЗУ) + di ; один DI на весь блит + ld d, d ; 0x52 — режим размера блока (no-op для D) + ld a, #0 + bo_len_imm = . - 1 + ld b, b ; 0x40 — стоп (B=счётчик не тронут) + bo_col: + ld a, c + out (#0x89), a ; Port_Y = y0 (верх колонки) + ;; 1) ЧТЕНИЕ ИСТОЧНИКА — ГОРИЗОНТАЛЬНОЙ копией (LD L,L): колонка + ;; спрайта лежит в ОЗУ СМЕЖНО, шагает адрес, не Port_Y. + ld l, l ; 0x6D — гориз. режим + ld a, (de) ; read-burst: h байт src -> буфер accel + ;; 2) ОПЕРАЦИЯ с приёмником — ВЕРТИКАЛЬНЫМ чтением (LD A,A): + ;; буфер = видео-колонка; Port_Y уезжает на y0+h. + ld a, a ; 0x7F — верт. режим + xor a, (hl) ; SMC: and/or/xor (hl) — op-burst + bo_op_imm = . - 1 + ld b, b ; 0x40 — стоп (разоружить перед OUT) + ;; 3) ЗАПИСЬ — снова с верха колонки. + ld a, c + out (#0x89), a ; Port_Y = y0 (accel разоружён) + ld a, a ; 0x7F — верт. режим + ld (hl), a ; write-burst: буфер -> видео-колонка + ld b, b ; 0x40 — стоп + ;; src += sstride (следующая колонка источника) + ld a, e + add a, #0 + bo_slo_imm = . - 1 + ld e, a + ld a, d + adc a, #0 + bo_shi_imm = . - 1 + ld d, a + inc hl ; dst += 1 (следующий x) + djnz bo_col + ei ; закрыть DI-окно + pop ix + ;; callee-pop 6 байт (w,h,sstride:2,y0,op) + pop hl ; ret-адрес + pop af + pop af + pop af + jp (hl) + __endasm; +} diff --git a/libbgi/bgi256/_bgi_blit_rows_op_raw.c b/libbgi/bgi256/_bgi_blit_rows_op_raw.c new file mode 100644 index 0000000..841cd1d --- /dev/null +++ b/libbgi/bgi256/_bgi_blit_rows_op_raw.c @@ -0,0 +1,118 @@ +/* + * _bgi_blit_rows_op_raw — accel-блит СТРОКАМИ с ЛОГИЧЕСКОЙ ОПЕРАЦИЕЙ: + * приёмник = приёмник источник, где = AND / OR / XOR. Клон + * _bgi_blit_rows_raw (dst фиксирован, строку выбирает Port_Y; src шагает + * на sstride), но между чтением источника и записью вставлен ТРЕТИЙ + * burst — чтение приёмника в режиме операции. + * + * КАК ЭТО РАБОТАЕТ (docs/new/06-accel.md §6.2 + пример «XOR блока данных» + * §6.3; сверено с драйвером MAME src/mame/sinclair/sprinter.cpp, + * accel_control_r/update_accel_buffer). Опкоды `AND (HL)` #A6, + * `OR (HL)` #B6, `XOR (HL)` #AE — не режим направления, а переключатель + * внутреннего автомата с «буфер := память» на «буфер = память»; сам + * burst запускает операндное чтение (HL) в ТЕКУЩЕМ режиме направления. + * Следующее армирование (LD L,L / LD A,A / LD B,B) возвращает автомат к + * присваиванию. + * + * ЗДЕСЬ ВСЁ ГОРИЗОНТАЛЬНО — и источник (строка спрайта в ОЗУ), и приёмник + * (строка экрана), поэтому три burst'а идут ОДНОЙ цепочкой, ровно как в + * примере документации, без промежуточных STOP/OUT: + * LD L,L ; гориз. режим + * LD A,(DE) ; read-burst : w байт src -> буфер accel + * (HL) ; op-burst : буфер = строка экрана + * LD (HL),A ; write-burst: буфер -> строка экрана + * LD B,B ; стоп + * Port_Y в горизонтальном режиме не шагает (шагает внутренний адрес, + * который каждый burst берёт заново из регистра) — сбрасывать между + * burst'ами нечего. Это дешевле колоночного близнеца + * (_bgi_blit_cols_op_raw), где приёмник вертикальный и Port_Y после + * op-burst'а приходится возвращать. + * + * ОДНА функция на три операции: опкод патчится SMC при входе (байт op), + * ровно как размер блока и страйд. op — это и есть опкод (GFX_OP_AND/ + * OR/XOR в ); проверку значения делает вызывающий. + * + * ЧТО ЧИТАЕТ op-burst: страницы #50..#5F на ЧТЕНИЕ всегда отдают ОЗУ-копию + * экрана, а не видео-ОЗУ (memory/sprinter_vram_transparency). При банке + * 0x54/0x5C (бит 2 — «не писать в ОЗУ-копию») операция считает ЧИСТЫЙ ФОН, + * а не то, что нарисовано поверх; нужна операция с видимой картинкой — + * рисовать банком 0x50 (GFX_BANK_NORMAL). + * + * ПРОЗРАЧНОСТЬ: аппаратная (банк 0x58/0x5C — не писать #FF) смотрит на + * РЕЗУЛЬТАТ, поэтому даром достаётся AND (#FF нейтрален) и OR (#FF | bg = + * #FF, запись подавляется), но НЕ XOR (#FF ^ bg = ~bg). Источник для XOR + * обязан хранить прозрачный пиксель как 0x00. Подробно — шапка + * common/_gfx_blit_full_op.c. + * + * ВХОД (__sdcccall(1)): src→HL, dst→DE (фикс); стек: w 4(ix), h 5(ix), + * sstride 6/7(ix), y0 8(ix), op 9(ix). Смысл w/h/sstride/y0 — как у + * _bgi_blit_rows_raw. Callee-pop 6 Б. + * + * Raw: без клиппинга (вызывающий); W3 замаплен (_bgi_begin/end снаружи); + * банк выставлен вызывающим. src ВНЕ W3. ОДИН DI на весь блит (втрое + * больше трафика, чем у копии — большие блиты режет вызывающий). + * Клоббер: AF/BC/DE/HL; IX сохраняется. + */ + +#include "../_bgi.h" + +void _bgi_blit_rows_op_raw(const uint8_t *src, uint8_t *dst, + uint8_t w, uint8_t h, + int sstride, uint8_t y0, uint8_t op) __naked +{ + (void)src; (void)dst; (void)w; (void)h; (void)sstride; (void)y0; (void)op; + __asm + push ix + ld ix, #0 + add ix, sp + ;; SMC: размер блока <- w (immediate у ld a,#n — требование FSM) + ld a, 4 (ix) + ld (bro_len_imm), a + ;; SMC: src-страйд — константа на вызов (8-битная adc-цепочка) + ld a, 6 (ix) + ld (bro_slo_imm), a + ld a, 7 (ix) + ld (bro_shi_imm), a + ;; SMC: опкод операции — and/or/xor (hl) (#A6/#B6/#AE) + ld a, 9 (ix) + ld (bro_op_imm), a + ld b, 5 (ix) ; B = счётчик строк (0 => 256: djnz) + ld c, 8 (ix) ; C = y + ;; Операция существует только в форме (HL) — приёмник обязан + ;; жить в HL; источник переезжает в DE (читается LD A,(DE)). + ex de, hl ; HL = dst (экран, фикс), DE = src (ОЗУ) + di ; один DI на весь блит + ld d, d ; 0x52 — режим размера блока (no-op для D) + ld a, #0 + bro_len_imm = . - 1 + ld b, b ; 0x40 — стоп (B=счётчик не тронут) + bro_row: + ld a, c + out (#0x89), a ; Port_Y = y + inc c + ld l, l ; 0x6D — армировать гориз. копию строки + ld a, (de) ; read-burst : src -> буфер акселератора + xor a, (hl) ; SMC: op-burst — буфер = строка экрана + bro_op_imm = . - 1 + ld (hl), a ; write-burst: буфер -> строка экрана + ld b, b ; 0x40 — стоп + ;; src += sstride (dst НЕ двигается — строку выбирает Port_Y) + ld a, e + add a, #0 + bro_slo_imm = . - 1 + ld e, a + ld a, d + adc a, #0 + bro_shi_imm = . - 1 + ld d, a + djnz bro_row + ei ; закрыть DI-окно блита + pop ix + ;; callee-pop 6 байт (w,h,sstride:2,y0,op) + pop hl ; ret-адрес + pop af + pop af + pop af + jp (hl) + __endasm; +} diff --git a/libbgi/common/_gfx_blit_full_op.c b/libbgi/common/_gfx_blit_full_op.c new file mode 100644 index 0000000..119accb --- /dev/null +++ b/libbgi/common/_gfx_blit_full_op.c @@ -0,0 +1,79 @@ +/* + * _gfx_blit_full_op — bracket-free ядро блита под-прямоугольника картинки + * getimage-формата ЛОГИЧЕСКОЙ ОПЕРАЦИЕЙ (приёмник = приёмник + * источник). Полный близнец _gfx_blit_full: тот же клип по экрану, та же + * нарезка на полосы ≤256 байт, БЕЗ _bgi_begin/_bgi_end и БЕЗ смены банка + * (вызывающий обязан замапить W3 и выставить банк). Разница одна — вызов + * op-ядра _bgi_blit_rows_op_raw вместо копирующего. + * + * ПОЧЕМУ ОТДЕЛЬНОЕ ТЕЛО, а не флаг в _gfx_blit_full. Копирующий путь — + * горячий (спрайтовый движок гоняет его в batch-цикле); ветка «а не + * операция ли?» тянула бы op-ядро в любую программу с блитом (~+170 Б + * _CODE при нулевой пользе). Клип-математика дублируется сознательно — + * правки вносить в ОБА файла (близнец: common/_gfx_blit_full.c). + * + * ---- ПРОЗРАЧНОСТЬ (главное отличие от копии) ---------------------- + * Аппаратная прозрачность (банк 0x58/0x5C: байт #FF не пишется) смотрит на + * РЕЗУЛЬТАТ операции, а не на источник, поэтому достаётся даром не всем: + * GFX_OP_AND — прозрачный #FF нейтрален сам по себе (#FF & bg = bg), + * банк не важен; + * GFX_OP_OR — #FF | bg = #FF, и запись такого байта подавляется банком + * 0x58/0x5C: обычный атлас годится КАК ЕСТЬ. Оговорка: + * подавится и «настоящий» результат, случайно равный #FF; + * GFX_OP_XOR — с прозрачностью #FF НЕ РАБОТАЕТ: #FF ^ bg = ~bg, то есть + * инверсия фона по всему прямоугольнику картинки. Источник + * для XOR обязан хранить прозрачный пиксель как 0x00 — + * нейтральный элемент и для XOR, и для OR (так же, как в + * DOS-оригиналах, где прозрачный индекс = 0). + * Замаскировать #FF внутри операции нельзя в принципе: побитовые AND/OR/XOR + * не умеют «выбрать по условию», а #FF — нейтраль только для AND. + * + * ---- ЧТО ЧИТАЕТ ОПЕРАЦИЯ ------------------------------------------ + * Чтение страниц #50..#5F всегда отдаёт ОЗУ-копию экрана + * (memory/sprinter_vram_transparency), поэтому при банке 0x54/0x5C («не + * писать в копию») операция совмещается с ЧИСТЫМ ФОНОМ, а не с тем, что + * уже нарисовано поверх. Нужен результат поверх нарисованного — рисовать + * банком GFX_BANK_NORMAL (0x50), где копия и видео-ОЗУ совпадают; платой + * будет потеря дарового heal из копии. + * + * Цена: три accel-burst'а на строку вместо двух (+50% времени accel); + * подготовка/клип — как у копии. + */ +#include "../_bgi.h" + +void _gfx_blit_full_op(int x, int y, const void *img, + int sx, int sy, int w, int h, uint8_t op) +{ + 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). */ + 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 = p + 4 + sx; + if (sy) + src += (uint16_t)sy * (uint16_t)img_w; + + dst = _gfx_addr_base + x; + while (w > 256) { + _bgi_blit_rows_op_raw(src, (uint8_t *)dst, 0 /* =256 */, (uint8_t)h, + img_w, (uint8_t)y, op); + src += 256; + dst += 256; + w -= 256; + } + _bgi_blit_rows_op_raw(src, (uint8_t *)dst, (uint8_t)w, (uint8_t)h, + img_w, (uint8_t)y, op); +} diff --git a/libbgi/common/gfx_blit_cols_op.c b/libbgi/common/gfx_blit_cols_op.c new file mode 100644 index 0000000..3d0b45e --- /dev/null +++ b/libbgi/common/gfx_blit_cols_op.c @@ -0,0 +1,17 @@ +/* + * gfx_blit_cols_op — блит спрайта column-major ЛОГИЧЕСКОЙ ОПЕРАЦИЕЙ, без + * клип-параметров: приёмник = приёмник источник (op = GFX_OP_AND / + * GFX_OP_OR / GFX_OP_XOR). Относится к gfx_blit_cols_part_wx_op так же, + * как gfx_blit_cols к gfx_blit_cols_part_wx — тонкая обёртка. + * + * Прозрачность, выбор банка и цена операции — в шапке + * gfx_blit_cols_part_wx_op.c (коротко: XOR требует источника с прозрачным + * 0x00, OR работает с обычным #FF-атласом через банк 0x58/0x5C). + */ +#include +#include "../_bgi.h" + +void gfx_blit_cols_op(int x, int y, const void *img, uint8_t flip, uint8_t op) +{ + gfx_blit_cols_part_wx_op(x, y, img, flip, 0, 0, 0, 0, op); +} diff --git a/libbgi/common/gfx_blit_cols_part_wx_op.c b/libbgi/common/gfx_blit_cols_part_wx_op.c new file mode 100644 index 0000000..fa11ddd --- /dev/null +++ b/libbgi/common/gfx_blit_cols_part_wx_op.c @@ -0,0 +1,95 @@ +/* + * gfx_blit_cols_part_wx_op — блит спрайта column-major ЛОГИЧЕСКОЙ ОПЕРАЦИЕЙ: + * приёмник = приёмник источник (op = GFX_OP_AND / GFX_OP_OR / + * GFX_OP_XOR). Полный близнец gfx_blit_cols_part_wx: те же клип-параметры + * (skip/rows сверху, skipw/maxw по колонкам), тот же бесплатный флип; вся + * разница — вызов op-ядра вместо копирующего. + * + * ПОЧЕМУ ОТДЕЛЬНОЕ ТЕЛО, а не флаг в gfx_blit_cols_part_wx. Копирующий + * путь — самый горячий в движке персонажей (PoP зовёт его на каждого Char + * каждый кадр); ветка «а не операция ли?» в его хвосте тянула бы op-ядро в + * ЛЮБУЮ программу, использующую блит колонками (~+180 Б _CODE при нулевой + * пользе). Клип-математика здесь дублируется сознательно — правки клипа + * вносить в ОБА файла (близнец: common/gfx_blit_cols_part_wx.c). + * + * ПРОЗРАЧНОСТЬ — ГЛАВНОЕ ОТЛИЧИЕ ОТ КОПИИ. Аппаратная прозрачность (банк + * 0x58/0x5C: байт #FF не пишется) смотрит на РЕЗУЛЬТАТ операции, а не на + * источник. Отсюда: + * GFX_OP_AND — прозрачный #FF нейтрален сам по себе (#FF & bg = bg); + * банк любой. + * GFX_OP_OR — #FF | bg = #FF, запись подавляется банком 0x58/0x5C; + * работает с нашими обычными атласами КАК ЕСТЬ. Оговорка: + * подавится и «настоящий» результат, случайно равный #FF. + * GFX_OP_XOR — с прозрачностью #FF НЕ РАБОТАЕТ: #FF ^ bg = ~bg, то есть + * инверсия фона по всему прямоугольнику спрайта. Источник + * для XOR обязан хранить прозрачный пиксель как 0x00 — + * нейтральный элемент и для XOR, и для OR (так же, как в + * оригинале DOS-игр, где прозрачный индекс = 0). + * + * ЧТО ЧИТАЕТ ОПЕРАЦИЯ. Чтение страниц #50..#5F всегда отдаёт ОЗУ-копию + * экрана (memory/sprinter_vram_transparency), поэтому при банке 0x54/0x5C + * (не писать в копию) операция считает ЧИСТЫЙ ФОН, а не то, что уже + * нарисовано поверх. Нужен результат поверх нарисованного — рисовать + * банком GFX_BANK_NORMAL (0x50), у которого копия и видео-ОЗУ совпадают; + * платой будет потеря дарового heal из копии. + * + * Цена: три accel-burst'а на колонку вместо двух (+50% времени accel); + * подготовка/клип — как у копии. + */ +#include +#include "../_bgi.h" + +void gfx_blit_cols_part_wx_op(int x, int y, const void *img, uint8_t flip, + int skip, int rows, uint8_t skipw, uint8_t maxw, + uint8_t op) +{ + const uint8_t *p = (const uint8_t *)img; + int full_w = p[0] | (p[1] << 8); + int full_h = p[2] | (p[3] << 8); + const uint8_t *px = p + 4; + int w = full_w, h; + int sstride = flip ? -full_h : full_h; /* шаг между колонками в ОЗУ */ + int sy; /* пропуск строк сверху колонки */ + int cx = 0; /* пропуск колонок слева */ + const uint8_t *src; + uint8_t *dst; + + if (full_w <= 0 || full_h <= 0) + return; + if (skipw) { /* обрезка СЛЕВА */ + if ((int)skipw >= w) return; /* срезано целиком */ + cx = (int)skipw; + x += (int)skipw; + w -= (int)skipw; + } + if (maxw && (int)maxw < w) + w = (int)maxw; /* обрезка справа */ + if (skip < 0) skip = 0; + if (skip >= full_h) + return; /* обрезано целиком */ + sy = skip; + h = full_h - skip; + if (rows > 0 && rows < h) + h = rows; + + /* Клип по экрану — только если хоть один край вылезает. */ + if (x < 0 || y < 0 || x + w > _bgi_maxx + 1 || y + h > _bgi_maxy + 1) { + if (x < 0) { cx -= 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 = flip ? (px + (uint16_t)(full_w - 1) * (uint16_t)full_h) : px; + if (cx) + src += cx * sstride; + src += sy; + + _bgi_begin(); + dst = (uint8_t *)(_gfx_addr_base + (uint16_t)x); + _bgi_blit_cols_op_raw(src, dst, (uint8_t)w, (uint8_t)h, sstride, + (uint8_t)y, op); + _bgi_end(); +} diff --git a/libbgi/common/gfx_blit_op.c b/libbgi/common/gfx_blit_op.c new file mode 100644 index 0000000..2425caa --- /dev/null +++ b/libbgi/common/gfx_blit_op.c @@ -0,0 +1,14 @@ +/* + * gfx_blit_op — блит ЦЕЛОЙ картинки getimage-формата ЛОГИЧЕСКОЙ ОПЕРАЦИЕЙ + * (приёмник = приёмник источник). Близнец gfx_blit: читает w,h из + * заголовка и делегирует ядру gfx_blit_part_op (клип/полосы/accel — там). + */ +#include "../_bgi.h" + +void gfx_blit_op(int x, int y, const void *img, uint8_t op) +{ + const uint8_t *p = (const uint8_t *)img; + + gfx_blit_part_op(x, y, img, 0, 0, + p[0] | (p[1] << 8), p[2] | (p[3] << 8), op); +} diff --git a/libbgi/common/gfx_blit_part_op.c b/libbgi/common/gfx_blit_part_op.c new file mode 100644 index 0000000..4f557cf --- /dev/null +++ b/libbgi/common/gfx_blit_part_op.c @@ -0,0 +1,21 @@ +/* + * gfx_blit_part_op — блит под-прямоугольника картинки getimage-формата + * ЛОГИЧЕСКОЙ ОПЕРАЦИЕЙ (приёмник = приёмник источник; op — + * GFX_OP_AND / GFX_OP_OR / GFX_OP_XOR из ). Близнец gfx_blit_part: + * своя W3-скобка вокруг bracket-free ядра _gfx_blit_full_op, текущий банк + * (gfx_set_bank), клип по экрану. + * + * Прозрачность (XOR требует источника с прозрачным 0x00, AND/OR работают + * с обычным #FF-атласом) и то, что операция читает ОЗУ-копию экрана, — + * в шапке common/_gfx_blit_full_op.c. + */ + +#include "../_bgi.h" + +void gfx_blit_part_op(int x, int y, const void *img, + int sx, int sy, int w, int h, uint8_t op) +{ + _bgi_begin(); + _gfx_blit_full_op(x, y, img, sx, sy, w, h, op); + _bgi_end(); +} diff --git a/libbgi/common/putimage.c b/libbgi/common/putimage.c index 2f1bdb0..dd7d3ad 100644 --- a/libbgi/common/putimage.c +++ b/libbgi/common/putimage.c @@ -8,11 +8,17 @@ * с учётом текущего банка (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-версии). + * XOR/OR/AND_PUT — тоже через акселератор (gfx_blit_part_op, 2026-08-11; + * закрыт пункт 2d-1 docs/TODO.md): акселератор умеет блочные AND/OR/XOR + * между своим буфером и памятью приёмника, попиксельного цикла CPU больше + * нет. Клиппинг у них теперь ТОТ ЖЕ, что у COPY_PUT (раньше на этом пути + * клипа не было вовсе). Оговорки — в шапке common/_gfx_blit_full_op.c: + * операция читает ОЗУ-копию экрана (при банках 0x54/0x5C это чистый фон), + * а прозрачность #FF совместима с AND/OR, но не с XOR. + * + * NOT_PUT — по-прежнему по-пиксельно (пишем ~src): это не операция с + * приёмником, а инверсия источника, у акселератора такого режима нет. + * Клиппинга на этом пути НЕТ (per-pixel bounds-check только в safe). * * Буфер bitmap обязан лежать вне W3 (< 0xC000) — на время операции W3 * замаплен на видеобанк. @@ -23,31 +29,30 @@ void putimage(int left, int top, const void *bitmap, int op) { const uint8_t *p = (const uint8_t *)bitmap; int w, h, x, y; - uint8_t src, dst; + uint8_t gop; w = p[0] | (p[1] << 8); h = p[2] | (p[3] << 8); if (w <= 0 || h <= 0) return; - if (op == COPY_PUT) { + switch (op) { + case COPY_PUT: gfx_blit_part(left, top, bitmap, 0, 0, w, h); return; + case XOR_PUT: gop = GFX_OP_XOR; break; + case OR_PUT: gop = GFX_OP_OR; break; + case AND_PUT: gop = GFX_OP_AND; break; + default: gop = 0; break; /* NOT_PUT — попиксельно ниже */ + } + if (gop) { + gfx_blit_part_op(left, top, bitmap, 0, 0, w, h, gop); + return; } p += 4; _bgi_begin(); - for (y = 0; y < h; y++) { - for (x = 0; x < w; x++) { - src = *p++; - switch (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: - default: dst = (uint8_t)~src; break; - } - _bgi_plot_raw(left + x, top + y, dst); - } - } + for (y = 0; y < h; y++) + for (x = 0; x < w; x++) + _bgi_plot_raw(left + x, top + y, (uint8_t)~(*p++)); _bgi_end(); } diff --git a/libbgi/include/gfx.h b/libbgi/include/gfx.h index 8485f6b..bfefbd6 100644 --- a/libbgi/include/gfx.h +++ b/libbgi/include/gfx.h @@ -156,6 +156,41 @@ void gfx_blit_cols_part_w(int x, int y, const void *img, uint8_t flip, void gfx_blit_cols_part_wx(int x, int y, const void *img, uint8_t flip, int skip, int rows, uint8_t skipw, uint8_t maxw); +/* ---- Блит колонками с ЛОГИЧЕСКОЙ ОПЕРАЦИЕЙ ------------------------ * + * Акселератор умеет не только копировать блок, но и совмещать его с тем, + * что уже лежит в приёмнике: приёмник = приёмник источник. Значения + * GFX_OP_* — это ОПКОДЫ Z80 `and/or/xor (hl)`: именно ими переключается + * автомат акселератора, и именно они патчатся SMC в ядре (одна функция на + * все три операции, ветвления в цикле нет). Иных значений не бывает. + * + * ПРОЗРАЧНОСТЬ. Аппаратная прозрачность (банк с битом 3 — не писать #FF) + * смотрит на РЕЗУЛЬТАТ, а не на источник: + * GFX_OP_AND — #FF нейтрален сам по себе, обычный атлас годится; + * GFX_OP_OR — #FF | bg = #FF, подавляется банком 0x58/0x5C — обычный + * атлас годится; + * GFX_OP_XOR — с прозрачным #FF даёт инверсию фона по всему футпринту; + * источник ОБЯЗАН хранить прозрачный пиксель как 0x00. + * Операция читает приёмник из ОЗУ-копии экрана: при банках 0x54/0x5C это + * ЧИСТЫЙ ФОН (нарисованного поверх не видно), при 0x50 — реальная картинка. + * Подробности — шапка common/gfx_blit_cols_part_wx_op.c. */ +#define GFX_OP_AND 0xA6 /* опкод `and (hl)` */ +#define GFX_OP_XOR 0xAE /* опкод `xor (hl)` */ +#define GFX_OP_OR 0xB6 /* опкод `or (hl)` */ + +/* --- строками (row-major, как gfx_blit/gfx_blit_part) --- */ +void gfx_blit_op(int x, int y, const void *img, uint8_t op); +void gfx_blit_part_op(int x, int y, const void *img, + int sx, int sy, int w, int h, uint8_t op); + +/* --- колонками (column-major, как gfx_blit_cols; flip бесплатный) --- */ +void gfx_blit_cols_op(int x, int y, const void *img, uint8_t flip, uint8_t op); + +/* Полный вариант: те же клип-параметры, что у gfx_blit_cols_part_wx + * (skip/rows — обрезка сверху, skipw/maxw — окно по колонкам). */ +void gfx_blit_cols_part_wx_op(int x, int y, const void *img, uint8_t flip, + int skip, int rows, uint8_t skipw, uint8_t maxw, + uint8_t op); + /* То же БЕЗ клипа по экрану — пара к gfx_blit_cols_part так же, как * gfx_blit_part_noclip к gfx_blit_part. Вызывающий гарантирует: спрайт * целиком на экране, ширина/высота кадра и y <= 255, skip < высоты. diff --git a/tests/accop/Makefile b/tests/accop/Makefile new file mode 100644 index 0000000..99d5e8f --- /dev/null +++ b/tests/accop/Makefile @@ -0,0 +1,4 @@ +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := accop +EXTRA_FLAGS ?= --gfx 256 +include $(PROJ_ROOT)/app.mk diff --git a/tests/accop/accop.c b/tests/accop/accop.c new file mode 100644 index 0000000..825d2ef --- /dev/null +++ b/tests/accop/accop.c @@ -0,0 +1,205 @@ +/* + * accop — регресс БЛОЧНЫХ ЛОГИЧЕСКИХ ОПЕРАЦИЙ акселератора (AND/OR/XOR) + * в libbgi: приёмник = приёмник источник, строками и колонками. + * + * ЧТО ПРОВЕРЯЕТСЯ (по байтам, а не глазами — getpixel читает ту же + * ОЗУ-копию экрана, что и сам op-burst): + * T1-T3 строками (gfx_blit_op → _bgi_blit_rows_op_raw): OR / XOR / AND; + * T4-T6 колонками (gfx_blit_cols_op → _bgi_blit_cols_op_raw): то же. + * Колоночный путь — самый рискованный: приёмник читается и + * пишется ВЕРТИКАЛЬНО, а вертикальный burst ШАГАЕТ Port_Y, так + * что между операцией и записью Port_Y надо вернуть на верх + * колонки (иначе колонка уедет вниз на h пикселей); + * T7 колонками + flip (зеркало и операция должны быть ортогональны); + * T8 прозрачность #FF при OR и банке 0x58: #FF | bg = #FF, запись + * подавляется железом => обычный спрайтовый атлас годится для OR + * как есть (для XOR — нет, см. шапку _gfx_blit_full_op.c). + * + * Источник НЕ константный: пиксель = (col*IH + row) & 0x0F, то есть у + * соседей по строке И по колонке разные значения — сдвиг на пиксель, + * перепутанный порядок байт в буфере акселератора или уехавший Port_Y + * дают FAIL, а не «случайно совпало». Вокруг каждого прямоугольника + * проверяется рамка в 1px: лишний burst/переполнение блока её порвёт. + * + * Семантика FSM (драйвер MAME src/mame/sinclair/sprinter.cpp, + * accel_control_r/update_accel_buffer + docs/new/06-accel.md §6.2): + * опкоды AND/OR/XOR (HL) не режим направления, а переключатель автомата + * «буфер := память» -> «буфер = память»; burst запускает операндное + * чтение (HL) в текущем режиме направления. Операция ОРТОГОНАЛЬНА + * горизонтальному/вертикальному режиму — это и подтверждают T4-T7. + */ + +#include +#include + +#define IW 12 /* ширина картинки, пикс */ +#define IH 8 /* высота картинки, пикс */ + +#define BG_OR 0x30 /* фон под OR: 0x30 | v = 0x30+v — все разные */ +#define BG_XOR 0x3C /* фон под XOR: 0x3C ^ v — все разные */ +#define BG_AND 0x3F /* фон под AND: 0x3F & v = v — все разные */ +#define FRAME 0x21 /* рамка вокруг: не должна быть тронута */ + +/* getimage-формат: uint16 w, uint16 h, дальше пиксели. */ +static uint8_t img_rows[4 + IW * IH]; /* построчно (row-major) */ +static uint8_t img_cols[4 + IW * IH]; /* поколоночно (col-major) */ +static uint8_t img_tr[4 + IW * IH]; /* row-major с дырами 0xFF */ + +/* Значение пикселя картинки в (col,row) — одинаково для всех трёх + * буферов, меняется только раскладка в памяти. */ +static uint8_t pix(int col, int row) +{ + return (uint8_t)((col * IH + row) & 0x0F); +} + +static void make_imgs(void) +{ + int col, row; + + img_rows[0] = IW; img_rows[1] = 0; img_rows[2] = IH; img_rows[3] = 0; + img_cols[0] = IW; img_cols[1] = 0; img_cols[2] = IH; img_cols[3] = 0; + img_tr[0] = IW; img_tr[1] = 0; img_tr[2] = IH; img_tr[3] = 0; + + for (col = 0; col < IW; col++) { + for (row = 0; row < IH; row++) { + uint8_t v = pix(col, row); + img_rows[4 + row * IW + col] = v; + img_cols[4 + col * IH + row] = v; + /* дыры прозрачности: первая строка и первая колонка */ + img_tr[4 + row * IW + col] = (row == 0 || col == 0) ? 0xFF : v; + } + } +} + +/* Фон: рамка FRAME по периметру + прямоугольник bg внутри. */ +static void prep(int x0, int y0, uint8_t bg) +{ + setfillstyle(SOLID_FILL, FRAME); + bar(x0 - 1, y0 - 1, x0 + IW, y0 + IH); + setfillstyle(SOLID_FILL, bg); + bar(x0, y0, x0 + IW - 1, y0 + IH - 1); +} + +/* Ожидаемый результат операции op над bg и пикселем картинки. */ +static uint8_t want_op(uint8_t bg, uint8_t v, uint8_t op) +{ + if (op == GFX_OP_OR) return (uint8_t)(bg | v); + if (op == GFX_OP_XOR) return (uint8_t)(bg ^ v); + return (uint8_t)(bg & v); +} + +/* Прямоугольник совпал попиксельно И рамка вокруг цела? + * flip != 0 — экранная колонка col взята из исходной (IW-1-col). */ +static uint8_t check(int x0, int y0, uint8_t bg, uint8_t op, uint8_t flip) +{ + int col, row; + + for (col = 0; col < IW; col++) + for (row = 0; row < IH; row++) { + uint8_t v = pix(flip ? (IW - 1 - col) : col, row); + if ((uint8_t)getpixel(x0 + col, y0 + row) != want_op(bg, v, op)) + return 0; + } + for (col = -1; col <= IW; col++) { + if ((uint8_t)getpixel(x0 + col, y0 - 1) != FRAME) return 0; + if ((uint8_t)getpixel(x0 + col, y0 + IH) != FRAME) return 0; + } + for (row = 0; row < IH; row++) { + if ((uint8_t)getpixel(x0 - 1, y0 + row) != FRAME) return 0; + if ((uint8_t)getpixel(x0 + IW, y0 + row) != FRAME) return 0; + } + return 1; +} + +/* T8: OR с прозрачным 0xFF под банком 0x58 — дыры обязаны остаться фоном. */ +static uint8_t check_transp(int x0, int y0, uint8_t bg) +{ + int col, row; + + for (col = 0; col < IW; col++) + for (row = 0; row < IH; row++) { + uint8_t want = (row == 0 || col == 0) + ? bg /* #FF|bg = #FF, подавлено */ + : (uint8_t)(bg | pix(col, row)); + if ((uint8_t)getpixel(x0 + col, y0 + row) != want) + return 0; + } + return 1; +} + +static void say(int y, const char *name, uint8_t ok) +{ + char line[40]; + int i = 0; + + while (name[i] && i < 30) { line[i] = name[i]; i++; } + line[i++] = ':'; + line[i++] = ' '; + if (ok) { line[i++] = 'P'; line[i++] = 'A'; line[i++] = 'S'; line[i++] = 'S'; } + else { line[i++] = 'F'; line[i++] = 'A'; line[i++] = 'I'; line[i++] = 'L'; } + line[i] = 0; + setcolor(ok ? LIGHTGREEN : LIGHTRED); + outtextxy(8, y, line); +} + +/* Прогон одной операции: подготовить фон, блитнуть, проверить. + * kind: 0 — строками, 1 — колонками, 2 — колонками с флипом. */ +static uint8_t run(int x0, int y0, uint8_t bg, uint8_t op, uint8_t kind) +{ + prep(x0, y0, bg); + if (kind == 0) + gfx_blit_op(x0, y0, img_rows, op); + else + gfx_blit_cols_op(x0, y0, img_cols, (uint8_t)(kind == 2), op); + return check(x0, y0, bg, op, (uint8_t)(kind == 2)); +} + +int main(void) +{ + uint8_t t[8]; + int i; + + initgraph(); + cleardevice(); + make_imgs(); + + /* Банк 0x50: запись идёт и в видео-ОЗУ, и в ОЗУ-копию, поэтому + * getpixel (читает копию) видит ровно то, что записано, а op-burst + * (читает её же) — реальный фон. Это дефолт, ставим явно. */ + gfx_set_bank(GFX_BANK_NORMAL); + + t[0] = run( 8, 180, BG_OR, GFX_OP_OR, 0); + t[1] = run( 40, 180, BG_XOR, GFX_OP_XOR, 0); + t[2] = run( 72, 180, BG_AND, GFX_OP_AND, 0); + t[3] = run(104, 180, BG_OR, GFX_OP_OR, 1); + t[4] = run(136, 180, BG_XOR, GFX_OP_XOR, 1); + t[5] = run(168, 180, BG_AND, GFX_OP_AND, 1); + t[6] = run(200, 180, BG_XOR, GFX_OP_XOR, 2); /* колонки + зеркало */ + + /* T8: прозрачность #FF при OR — банк с подавлением записи #FF. */ + prep(232, 180, BG_OR); + gfx_set_bank(GFX_BANK_TRANSPARENT); + gfx_blit_op(232, 180, img_tr, GFX_OP_OR); + gfx_set_bank(GFX_BANK_NORMAL); + t[7] = check_transp(232, 180, BG_OR); + + setcolor(WHITE); + outtextxy(8, 8, "ACCEL block AND/OR/XOR test v1"); + + say( 28, "T1 rows OR", t[0]); + say( 40, "T2 rows XOR", t[1]); + say( 52, "T3 rows AND", t[2]); + say( 64, "T4 cols OR", t[3]); + say( 76, "T5 cols XOR", t[4]); + say( 88, "T6 cols AND", t[5]); + say(100, "T7 cols XOR flip", t[6]); + say(112, "T8 rows OR transp", t[7]); + + for (i = 0; i < 8; i++) + if (!t[i]) break; + setcolor(WHITE); + outtextxy(8, 136, i == 8 ? "ALL PASS" : "SOME FAILED"); + + outtextxy(8, 160, "done"); + for (;;) { } +} diff --git a/tests/bgi_img/bgi_img.c b/tests/bgi_img/bgi_img.c index 07ff492..1e8e212 100644 --- a/tests/bgi_img/bgi_img.c +++ b/tests/bgi_img/bgi_img.c @@ -15,6 +15,15 @@ * Прогон в MAME визуальный: см. что источник и его COPY-копия * идентичны (в т.ч. красный маркер в левом-верхнем углу — ловит * зеркалирование), а XOR-дважды не оставляет следа на фоне. + * + * 2026-08-11: XOR/OR/AND_PUT переведены с попиксельного цикла на блочные + * операции акселератора (gfx_blit_part_op), поэтому к визуальной проверке + * добавлены ДВЕ БАЙТОВЫЕ (PASS/FAIL на экране) — глазами тут промах на + * пиксель или уехавший Port_Y не поймать: + * P1 COPY == источник побайтно (клип/адресация не сдвинули образ); + * P2 XOR дважды == чистый фон побайтно (op-burst читает ровно тот байт + * приёмника, поверх которого потом пишет). + * Механика самих блочных операций проверяется отдельно — tests/accop. */ #include @@ -53,10 +62,30 @@ static void draw_source(void) rectangle(l - 1, t - 1, r + 1, b + 1); /* рамка вокруг источника */ } +/* Прямоугольник (x0,y0,w,h) совпадает с прямоугольником (x1,y1,w,h)? */ +static uint8_t rect_eq(int x0, int y0, int x1, int y1, int w, int h) +{ + int cx, cy; + + for (cy = 0; cy < h; cy++) + for (cx = 0; cx < w; cx++) + if (getpixel(x0 + cx, y0 + cy) != getpixel(x1 + cx, y1 + cy)) + return 0; + return 1; +} + +static void say(int x, int y, const char *s, uint8_t ok) +{ + setcolor(ok ? LIGHTGREEN : LIGHTRED); + outtextxy(x, y, s); + outtextxy(x + textwidth((char *)s) + 4, y, ok ? "PASS" : "FAIL"); +} + int main(void) { static const int ops[5] = { COPY_PUT, XOR_PUT, OR_PUT, AND_PUT, NOT_PUT }; static const char *names[5] = { "COPY", "XOR", "OR", "AND", "NOT" }; + uint8_t p_copy, p_xor2; int i, x; int sl = SPR_L, st = SPR_T; int sr = SPR_L + SPR_W - 1, sb = SPR_T + SPR_H - 1; @@ -95,6 +124,10 @@ int main(void) putimage(x, 120, sprite, ops[i]); } + /* P1: COPY-копия (x = 16, первая в ряду) обязана совпасть с источником + * побайтно — ловит сдвиг на пиксель и зеркалирование. */ + p_copy = rect_eq(16, 120, sl, st, SPR_W, SPR_H); + /* --- XOR дважды = восстановление фона --- */ setcolor(WHITE); outtextxy(16, 176, "XOR x2 -> restores bg (patch stays clean):"); @@ -110,5 +143,12 @@ int main(void) setcolor(WHITE); outtextxy(130, 202, "<- these two must match ->"); + /* P2: наложенный и снятый XOR не оставил следа — сравниваем с + * нетронутым зелёным эталоном справа. */ + p_xor2 = rect_eq(16, 190, 80, 190, SPR_W, SPR_H); + + say(16, 228, "P1 COPY==src:", p_copy); + say(16, 240, "P2 XORx2==bg:", p_xor2); + for (;;) { } }