libbgi: переворот экрана акселератором (gfx_copy_page + _bgi_flip_rows_raw)

Первый шаг L9-INVERT.  Разведка железа закрыта экспериментом: Port_Y можно
менять между read- и write-триггером, если перед вторым OUT стоит STOP —
приём уже был в проде в _bgi_scroll_cols_raw (указал пользователь), так что
обходной путь через ОЗУ-буфер не понадобился.

- _bgi_flip_rows_raw: построчная копия video->video с реверсом Port_Y
  приёмника (DI/EI бандами по 16 строк, размер блока армируется в каждой
  скобке, убывающий y1 держится SMC-ячейкой);
- gfx_copy_page(area, GFX_COPY_DIRECT|GFX_COPY_VFLIP): копия области из
  неактивной страницы в активную; банк 0x50, то есть приёмник обновляется и
  в VRAM, и в ОЗУ-копии — иначе heal восстанавливал бы старый фон;
- tests/pageflip: 5/5 PASS в MAME (прямая, vflip, рамка, широкая 320 двумя
  проходами, источник цел).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 16:38:10 +03:00
parent 52b43b3d81
commit 7fe4c25ad1
8 changed files with 454 additions and 1 deletions
+9
View File
@@ -163,6 +163,15 @@ void _bgi_scroll_rows_raw(uint16_t src, uint16_t dst,
void _bgi_scroll_cols_raw(uint16_t src, uint16_t dst,
uint8_t w, uint8_t h, uint8_t y0, uint8_t y1);
/* _bgi_flip_rows_raw — ядро gfx_copy_page(GFX_COPY_VFLIP): построчная копия
* с ПЕРЕВОРОТОМ ПО ВЕРТИКАЛИ. Как scroll_rows, но Port_Y чтения растёт, а
* Port_Y записи УБЫВАЕТ, поэтому на строку нужны два OUT — безопасные
* благодаря STOP'у между read- и write-триггером (приём из scroll_cols).
* w — байт/строка (0=256), h — строк 1..256 (0=256), y0 — Port_Y первой
* строки src (++), y1 — Port_Y первой строки dst (--). */
void _bgi_flip_rows_raw(uint16_t src, uint16_t dst,
uint8_t w, uint8_t h, uint8_t y0, uint8_t y1);
+151
View File
@@ -0,0 +1,151 @@
/*
* _bgi_flip_rows_raw — построчная копия video->video с ПЕРЕВОРОТОМ ПО
* ВЕРТИКАЛИ (mode 0x81). Строка src с Port_Y = y0 уходит в строку dst с
* Port_Y = y1, дальше y0 растёт, а y1 УБЫВАЕТ: за h строк область
* отражается сверху вниз. Ядро gfx_copy_page(GFX_COPY_VFLIP) — переворот
* экрана для зелья инверсии (applications/PoP/docs/l9_invert_plan.md).
*
* Отличие от _bgi_scroll_rows_raw: там read и write идут на ОДНОМ Port_Y
* (одна строка), поэтому OUT на строку один. Здесь строки разные, значит
* Port_Y надо сменить МЕЖДУ read- и write-триггером — а это ровно тот
* случай, где наивная схема ломается: accel перехватывает любое чтение
* программной памяти, и fetch immediate-операнда OUT спурьезно
* перезапустил бы read-burst, набив буфер кодом
* (memory/accel_operand_fetch_retrigger).
*
* РЕШЕНИЕ — то же, что в _bgi_scroll_cols_raw (там оно уже в проде,
* gfx_scroll_v): STOP (LD B,B) ПЕРЕД вторым OUT. Он разоружает accel
* (m_acc_dir = 0), после чего операнд OUT безопасен; затем LD L,L снова
* взводит горизонтальный режим — уже для записи. Порядок на строку:
* {OUT y0 · arm · read · STOP · OUT y1 · arm · write · STOP}
* Оба OUT выполняются только пока accel РАЗОРУЖЁН — это инвариант, на
* котором всё держится; менять порядок нельзя.
*
* Убывающий y1 держим SMC-ячейкой, а не регистром: HL/DE заняты адресами,
* B — счётчиком банды, C — растущим y0, и свободного 8-битного не
* остаётся. Обновление ячейки (`dec a` + `ld (…),a`) стоит на строке
* 17Т и идёт ПОСЛЕ STOP'а, то есть при разоружённом accel — безопасно.
*
* DI/EI — БАНДАМИ по 16 строк ВНУТРИ (как у scroll-ядер): держать DI на
* весь проход нельзя (порвёт CBL-звук). Размер блока армируется в КАЖДОЙ
* скобке — в окне EI между бандами CBL-ISR армирует accel своим размером.
*
* ВХОД (__sdcccall(1)): src->HL, dst->DE; стек: w 4(ix) — байт в строке
* (размер accel-блока, 1..256, 0 = 256), h 5(ix) — строк 1..256 (0 = 256,
* конвенция accel), y0 6(ix) — Port_Y ПЕРВОЙ строки src (инкремент),
* y1 7(ix) — Port_Y ПЕРВОЙ строки dst (ДЕКРЕМЕНТ). Callee-pop 4 байта.
*
* Raw: клиппинга нет (вызывающий), W3 замаплен (_bgi_begin/_bgi_end
* снаружи), банк 0x50 (запись бьёт VRAM + ОЗУ-копию — иначе перевёрнутый
* фон не попадёт в копию и heal начнёт восстанавливать старую картинку).
* Перекрытия src/dst нет by design — стороны в РАЗНЫХ видеостраницах.
* Строку > 256 байт режет вызывающий. Клоббер: AF/BC; HL/DE/IX
* сохраняются (LD L,L / LD D,D / LD B,B — accel-опкоды-no-op'ы).
*/
#include "../_bgi.h"
void _bgi_flip_rows_raw(uint16_t src, uint16_t dst,
uint8_t w, uint8_t h, uint8_t y0, uint8_t y1) __naked {
(void)src; (void)dst; (void)w; (void)h; (void)y0; (void)y1;
__asm
push ix
ld ix, #0
add ix, sp
;; HL = src, DE = dst (адреса-константы; строку задаёт Port_Y)
ld a, 4 (ix)
ld (fl_row_len), a ; SMC: размер блока <- w (0 = 256)
ld (fl_row_len2), a ; ...и в хвостовую скобку
ld a, 7 (ix)
ld (fl_row_ydst), a ; SMC: Port_Y записи <- y1 (убывает)
ld c, 6 (ix) ; C = бегущий Port_Y чтения (y0)
;; --- precompute: полных 16-банд (ix+5) + хвост (SMC); h=0 => 256 ---
ld a, 5 (ix)
and #0x0F
ld (fl_row_tail), a ; хвост = h & 15 (0 и при h=0, и при h=256)
ld a, 5 (ix)
or a, a
jr NZ, fl_row_shr
ld a, #16 ; h=0 => 256 => ровно 16 полных банд
jr fl_row_setfull
fl_row_shr:
rrca
rrca
rrca
rrca
and #0x0F ; full = h >> 4
fl_row_setfull:
ld 5 (ix), a ; ix+5 = число полных 16-строчных банд
fl_row_band:
ld a, 5 (ix)
or a, a
jr Z, fl_row_tailband ; полные банды кончились
dec 5 (ix) ; full -= 1
ld b, #16 ; 16 строк в банде
di
ld d, d ; 0x52 режим размера блока (no-op для D)
ld a, #0
fl_row_len = . - 1
ld b, b ; выключить размер сохранён (B=16 цел)
fl_row_next:
ld a, c
out (#0x89), a ; Port_Y = y0 (accel РАЗОРУЖЁН операнд безопасен)
inc c ; y0++
ld l, l ; 0x6D арм гориз. копии
ld a, (hl) ; read-burst: src-строка -> буфер
ld b, b ; 0x40 STOP (разоружить перед OUT y1)
ld a, #0
fl_row_ydst = . - 1
out (#0x89), a ; Port_Y = y1 accel разоружён
dec a
ld (fl_row_ydst), a ; y1-- (запись при разоружённом accel)
ld l, l ; 0x6D арм гориз. копии для записи
ld (de), a ; write-burst: буфер -> dst-строка
ld b, b ; 0x40 стоп
djnz fl_row_next
ei ; окно прерываний между бандами
jr fl_row_band
;; --- хвост h&15 строк: отдельная скобка со СВОИМ армированием ---
fl_row_tailband:
;; y1 у хвоста ПРОДОЛЖЕНИЕ, а не исходное значение: переносим
;; текущее из основной скобки (у неё своя immediate-ячейка).
ld a, (fl_row_ydst)
ld (fl_row_ydst2), a
ld a, #0
fl_row_tail = . - 1
or a, a
jr Z, fl_row_done ; хвоста нет
ld b, a ; B = остаток строк (1..15)
di
ld d, d
ld a, #0
fl_row_len2 = . - 1
ld b, b
fl_row_trow:
ld a, c
out (#0x89), a
inc c
ld l, l
ld a, (hl)
ld b, b
ld a, #0
fl_row_ydst2 = . - 1
out (#0x89), a
dec a
ld (fl_row_ydst2), a
ld l, l
ld (de), a
ld b, b
djnz fl_row_trow
ei
fl_row_done:
pop ix
;; callee-pop 4 байта (w, h, y0, y1)
pop hl ; ret-адрес
pop af
pop af
jp (hl)
__endasm;
}
+65
View File
@@ -0,0 +1,65 @@
/*
* gfx_copy_page — копия области из НЕактивной страницы в активную:
* прямая или с ПЕРЕВОРОТОМ ПО ВЕРТИКАЛИ (mode-agnostic обёртка).
*
* Зачем. Два сценария, оба про «взять готовую картинку целиком»:
* - GFX_COPY_DIRECT — второй странице дабл-буфера отдать то, что уже
* нарисовано в первой, вместо повторной отрисовки сцены (у тайловых
* движков это в разы дешевле и, главное, невидимо для игрока: он
* видит только готовый кадр);
* - GFX_COPY_VFLIP — перевернуть уже нарисованное вверх ногами (зелье
* инверсии в порте Prince of Persia).
*
* Направление то же, что у gfx_scroll_*: источник — НЕактивная (теневая)
* страница, приёмник — активная (draw). Банк на время копии —
* GFX_BANK_NORMAL (0x50): запись бьёт VRAM И ОЗУ-копию. Это не деталь, а
* инвариант: accel ЧИТАЕТ ОЗУ-копию, поэтому копируется чистый фон без
* спрайтов, а если писать банком без тени, копия страницы-приёмника
* останется старой и heal начнёт восстанавливать прошлую картинку.
*
* VFLIP отражает область ВНУТРИ СЕБЯ: строка area->y + i уходит в строку
* area->y + area->h - 1 - i. Горизонтального зеркала здесь нет и не
* будет — accel копирует блок только вперёд, побайтовый реверс стоил бы
* 320 burst'ов на строку вместо двух (для персонажей зеркало делается
* раскладкой спрайта, см. gfx_blit_cols).
*
* Ширина режется на burst'ы ≤ 255 байт (лимит accel-блока; 256 у нынешних
* leaf'ов даёт мусор на втором проходе — тот же TODO, что в gfx_scroll_h).
* Высоту и DI/EI-банды по 16 строк leaf делает сам.
*/
#include "../include/gfx.h"
#include "../include/sprite.h"
#include "../_bgi.h"
void gfx_copy_page(const gfx_rect_t *area, uint8_t mode) {
uint8_t saved = gfx_get_bank();
uint16_t w = area->w;
uint8_t h = (uint8_t)area->h; /* 256 => 0 — конвенция accel */
uint16_t src = _gfx_addr_shadow_base + (uint16_t)area->x;
uint16_t dst = _gfx_addr_base + (uint16_t)area->x;
uint8_t y0 = (uint8_t)area->y;
/* Port_Y приёмника: при перевороте начинаем с НИЖНЕЙ строки области и
* идём вверх; при прямой копии — та же строка, что у источника. */
uint8_t y1 = (mode == GFX_COPY_VFLIP)
? (uint8_t)(area->y + area->h - 1)
: y0;
if (!w || !area->h) return;
gfx_set_bank(GFX_BANK_NORMAL);
_bgi_begin();
while (w) {
uint8_t cw = (w > 255) ? 255 : (uint8_t)w;
if (mode == GFX_COPY_VFLIP)
_bgi_flip_rows_raw(src, dst, cw, h, y0, y1);
else
_bgi_scroll_rows_raw(src, dst, cw, h, y0);
src += cw; dst += cw; w -= cw;
}
_bgi_end();
gfx_set_bank(saved);
}
+11
View File
@@ -320,4 +320,15 @@ typedef struct {
void gfx_scroll_h(const gfx_rect_t *area, int16_t dx, gfx_rect_t *dirty);
void gfx_scroll_v(const gfx_rect_t *area, int16_t dy, gfx_rect_t *dirty);
/* Копия области из НЕактивной страницы в активную: прямая или с
* ПЕРЕВОРОТОМ ПО ВЕРТИКАЛИ (строка area->y + i -> area->y + h - 1 - i).
* Банк на время копии — 0x50, то есть приёмник обновляется И в VRAM, и в
* ОЗУ-копии (иначе heal целевой страницы восстанавливал бы старый фон).
* Источником служит ОЗУ-копия, поэтому переносится ЧИСТЫЙ фон — спрайты,
* нарисованные банком без тени, не копируются.
* Горизонтального зеркала нет: accel копирует блок только вперёд. */
#define GFX_COPY_DIRECT 0
#define GFX_COPY_VFLIP 1
void gfx_copy_page(const gfx_rect_t *area, uint8_t mode);
#endif