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
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := accop
EXTRA_FLAGS ?= --gfx 256
include $(PROJ_ROOT)/app.mk
+205
View File
@@ -0,0 +1,205 @@
/*
* accop — регресс БЛОЧНЫХ ЛОГИЧЕСКИХ ОПЕРАЦИЙ акселератора (AND/OR/XOR)
* в libbgi: приёмник = приёмник <op> источник, строками и колонками.
*
* ЧТО ПРОВЕРЯЕТСЯ (по байтам, а не глазами — 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) не режим направления, а переключатель автомата
* «буфер := память» -> «буфер <op>= память»; burst запускает операндное
* чтение (HL) в текущем режиме направления. Операция ОРТОГОНАЛЬНА
* горизонтальному/вертикальному режиму — это и подтверждают T4-T7.
*/
#include <graphics.h>
#include <gfx.h>
#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 (;;) { }
}