libbgi: блочные AND/OR/XOR акселератора (блит строками и колонками)

Акселератор умеет не только копировать блок, но и совмещать его с
приёмником: опкод `and/or/xor (hl)` между триггерами чтения и записи
переводит внутренний автомат из «буфер := память» в «буфер <op>= память».
Операция ортогональна направлению (гориз. LD L,L / верт. LD A,A), значит
одинаково работает и для row-major картинок, и для column-major спрайтов
персонажей с бесплатным флипом.

Полный набор, по образцу существующего копирующего:
- ядра bgi256/_bgi_blit_rows_op_raw.c и _bgi_blit_cols_op_raw.c;
- строками: gfx_blit_op / gfx_blit_part_op (через _gfx_blit_full_op);
- колонками: gfx_blit_cols_op / gfx_blit_cols_part_wx_op (skip/rows/
  skipw/maxw и flip — как у копирующего близнеца);
- putimage(XOR/OR/AND_PUT) переведён на accel, попиксельным остался
  только NOT_PUT — закрыт пункт 2d-1 docs/TODO.md.

Одна функция на три операции: опкод патчится SMC, как размер блока и
страйд, — ветвления в цикле нет.  Колоночный путь дороже строкового на
один OUT Port_Y: вертикальный op-burst шагает Port_Y, и перед записью
его надо вернуть на верх колонки (STOP обязателен — иначе fetch операнда
OUT перезапустит burst, memory/accel_operand_fetch_retrigger).

Две оговорки (в шапках модулей):
- операция ЧИТАЕТ ОЗУ-копию экрана, а не видео-ОЗУ, поэтому при банках
  0x54/0x5C совмещается с чистым фоном, а не с нарисованным поверх;
- аппаратная прозрачность #FF совместима с AND (нейтраль) и OR (#FF|bg =
  #FF, запись подавляется), но НЕ с XOR: там источник обязан хранить
  прозрачный пиксель как 0x00.

Проверка: tests/accop (новый) — 8/8 PASS в MAME, побайтно, источник с
разными значениями по строкам И колонкам + рамка вокруг; tests/bgi_img
получил две байтовые самопроверки (COPY==источник, XOR дважды == фон),
обе PASS.  make size-check: роста нет (эталон принят заново — kbdpoll
+63 Б приехал из прошлой сессии, воспроизводится и без этих правок).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 15:03:47 +03:00
parent d154cb452c
commit a823e7ee9c
15 changed files with 829 additions and 23 deletions
+135
View File
@@ -0,0 +1,135 @@
/*
* _bgi_blit_cols_op_raw — accel-блит КОЛОНКАМИ с ЛОГИЧЕСКОЙ ОПЕРАЦИЕЙ:
* приёмник = приёмник <op> источник, где <op> = 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-выборка переводит внутренний
* автомат из «буфер := память» в «буфер <op>= память», а сам 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 в <gfx.h>); проверку значения делает вызывающий.
*
* ПОСЛЕДОВАТЕЛЬНОСТЬ НА КОЛОНКУ (три burst'а вместо двух):
* OUT Port_Y = y0 ; accel разоружён — операнд OUT безопасен
* LD L,L / LD A,(DE) ; гориз. чтение: h смежных байт src -> буфер
* LD A,A / <op> (HL) ; верт. чтение: буфер <op>= видео-колонка
* 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, константа)
;; Операция существует только в форме <op> (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):
;; буфер <op>= видео-колонка; 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;
}