diff --git a/applications/PoP/docs/l9_invert_plan.md b/applications/PoP/docs/l9_invert_plan.md index 70d8b89..5f45b91 100644 --- a/applications/PoP/docs/l9_invert_plan.md +++ b/applications/PoP/docs/l9_invert_plan.md @@ -33,7 +33,28 @@ # Часть I — libbgi -## I.0 РАЗВЕДКА ЖЕЛЕЗА — делать ПЕРВОЙ, до любого кода +## I.0 РАЗВЕДКА ЖЕЛЕЗА — ЗАКРЫТА 2026-08-12 + +**Оба факта подтверждены, ядро и обёртка написаны и проверены в MAME** +(`tests/pageflip`, 5/5 PASS — прямая копия, vflip, целость рамки, широкая +копия 320 = два прохода, неприкосновенность источника). + +- **Р1 подтверждён**: Port_Y можно менять между read- и write-триггером, + если ПЕРЕД вторым OUT стоит STOP (`LD B,B`) — он разоружает accel, и fetch + immediate-операнда OUT уже безопасен. Приём не новый: ровно так работает + `_bgi_scroll_cols_raw` (ядро `gfx_scroll_v`), то есть он был в проде + задолго до этой задачи — указал пользователь. Значит и обходной путь + через ОЗУ-буфер не нужен; +- **Р2 подтверждён**: обе страницы адресуемы одновременно, копия между ними + идёт по разнице баз (`_gfx_addr_shadow_base` / `_gfx_addr_base`), строку + выбирает Port_Y. + +Сделано: `libbgi/bgi256/_bgi_flip_rows_raw.c` (ядро) + +`libbgi/common/gfx_copy_page.c` (обёртка, режимы `GFX_COPY_DIRECT` / +`GFX_COPY_VFLIP`) + `tests/pageflip`. Пункты I.1 и I.3 ниже — ЗАКРЫТЫ этим +же коммитом; описание оставлено как контракт. + +### Исходный текст разведки (для истории) Два факта, на которых стоит вся схема, сейчас НЕ подтверждены артефактом. Пока их нет, остальные пункты не начинать (memory `defer_unexplained_quirks`). diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv index ce45987..4352a40 100644 --- a/docs/size_baseline.tsv +++ b/docs/size_baseline.tsv @@ -41,6 +41,7 @@ malloc 4461 mem_test 4555 mouse 4382 openenv 6124 +pageflip 4960 palfile 5406 ptime 5744 rt_test 5187 diff --git a/libbgi/_bgi.h b/libbgi/_bgi.h index ecec621..81d0c26 100644 --- a/libbgi/_bgi.h +++ b/libbgi/_bgi.h @@ -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); + diff --git a/libbgi/bgi256/_bgi_flip_rows_raw.c b/libbgi/bgi256/_bgi_flip_rows_raw.c new file mode 100644 index 0000000..8f53f49 --- /dev/null +++ b/libbgi/bgi256/_bgi_flip_rows_raw.c @@ -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; +} diff --git a/libbgi/common/gfx_copy_page.c b/libbgi/common/gfx_copy_page.c new file mode 100644 index 0000000..5b73251 --- /dev/null +++ b/libbgi/common/gfx_copy_page.c @@ -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); +} diff --git a/libbgi/include/sprite.h b/libbgi/include/sprite.h index bb8133a..c5a2dbc 100644 --- a/libbgi/include/sprite.h +++ b/libbgi/include/sprite.h @@ -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 diff --git a/tests/pageflip/Makefile b/tests/pageflip/Makefile new file mode 100644 index 0000000..d475dd5 --- /dev/null +++ b/tests/pageflip/Makefile @@ -0,0 +1,4 @@ +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := pageflip +EXTRA_FLAGS ?= --gfx 256 +include $(PROJ_ROOT)/app.mk diff --git a/tests/pageflip/pageflip.c b/tests/pageflip/pageflip.c new file mode 100644 index 0000000..3f9c088 --- /dev/null +++ b/tests/pageflip/pageflip.c @@ -0,0 +1,191 @@ +/* + * pageflip — регресс gfx_copy_page: копия области из НЕактивной страницы в + * активную, прямая (GFX_COPY_DIRECT) и с переворотом по вертикали + * (GFX_COPY_VFLIP, ядро _bgi_flip_rows_raw). + * + * ЗАЧЕМ ИМЕННО ТАКОЙ ТЕСТ. Переворот строит на квирке акселератора: между + * read- и write-триггером надо СМЕНИТЬ Port_Y, а fetch операнда любой + * инструкции в этом окне перезапускает read-burst и набивает буфер кодом + * (memory/accel_operand_fetch_retrigger). Ядро обходит это STOP'ом перед + * вторым OUT — приём взят из _bgi_scroll_cols_raw, где он уже в проде. + * Ошибка здесь даёт не «немного не так», а сдвинутые/мусорные строки, + * поэтому проверка ПОБАЙТНАЯ, а не на глаз. + * + * Что проверяется: + * T1 DIRECT: строка i источника легла в строку i приёмника; + * T2 VFLIP: строка i источника легла в строку (h-1-i) приёмника; + * T3 рамка вокруг области цела — burst не вылез за пределы; + * T4 ШИРОКАЯ копия (320 байт = два прохода по 255+65): вылезший или + * потерянный второй проход виден по краям строки; + * T5 ИСТОЧНИК не тронут — копия односторонняя. + * + * Паттерн намеренно разный и по строкам, и по колонкам: цвет строки несёт + * её номер (сдвиг по Y виден сразу), а маркеры на первом/последнем пикселе + * ловят сдвиг по X и потерю хвоста широкой копии. + * + * ВИД НА ЭКРАНЕ: сами области выглядят чёрными — палитра по умолчанию + * задаёт лишь 16 первых цветов, а тест намеренно пишет индексы 0x21..0x60, + * чтобы значения были различимы побайтно. Это ожидаемо: проверка идёт + * getpixel'ом по ИНДЕКСАМ, а не глазами. + * + * getpixel читает ОЗУ-КОПИЮ экрана — ту самую, которую обязана обновить + * копия (банк 0x50). Значит T1/T2 заодно подтверждают инвариант «после + * gfx_copy_page heal целевой страницы восстановит НОВЫЙ фон, а не старый»: + * без записи в тень тест бы их провалил. + */ + +#include +#include +#include + +#define AX 8 /* область-образец: левый верхний угол */ +#define AY 40 +#define AW 64 +#define AH 32 + +#define WX 0 /* широкая область (два прохода по ширине) */ +#define WY 120 +#define WW 320 +#define WH 16 + +#define ROW_C0 0x41 /* цвет строки i = ROW_C0 + i */ +#define MARK_L 0x21 /* маркер левого края строки */ +#define MARK_R 0x22 /* маркер правого края */ +#define FRAME 0x2F /* рамка вокруг области */ +#define DIRT 0x35 /* чем забиваем приёмник до копии */ + +/* Нарисовать образец в ТЕКУЩЕЙ draw-странице: каждая строка своим цветом, + * по краям маркеры. */ +static void draw_pattern(int x0, int y0, int w, int h) +{ + int y; + for (y = 0; y < h; y++) { + setfillstyle(SOLID_FILL, (uint8_t)(ROW_C0 + y)); + bar(x0, y0 + y, x0 + w - 1, y0 + y); + putpixel(x0, y0 + y, MARK_L); + putpixel(x0 + w - 1, y0 + y, MARK_R); + } +} + +/* Забить приёмник мусором + рамка вокруг (её никто не должен трогать). */ +static void draw_dirt(int x0, int y0, int w, int h) +{ + setfillstyle(SOLID_FILL, FRAME); + bar(x0 - 1, y0 - 1, x0 + w, y0 + h); + setfillstyle(SOLID_FILL, DIRT); + bar(x0, y0, x0 + w - 1, y0 + h - 1); +} + +/* Строка src_y образца обязана лежать в строке dst_y приёмника. */ +static uint8_t row_matches(int x0, int y0, int w, int src_y, int dst_y) +{ + int x; + uint8_t want = (uint8_t)(ROW_C0 + src_y); + if ((uint8_t)getpixel(x0, y0 + dst_y) != MARK_L) return 0; + if ((uint8_t)getpixel(x0 + w - 1, y0 + dst_y) != MARK_R) return 0; + for (x = 1; x < w - 1; x++) + if ((uint8_t)getpixel(x0 + x, y0 + dst_y) != want) return 0; + return 1; +} + +static uint8_t check_direct(int x0, int y0, int w, int h) +{ + int y; + for (y = 0; y < h; y++) + if (!row_matches(x0, y0, w, y, y)) return 0; + return 1; +} + +static uint8_t check_vflip(int x0, int y0, int w, int h) +{ + int y; + for (y = 0; y < h; y++) + if (!row_matches(x0, y0, w, y, h - 1 - y)) return 0; + return 1; +} + +static uint8_t check_frame(int x0, int y0, int w, int h) +{ + int x, y; + for (x = -1; x <= w; x++) { + if ((uint8_t)getpixel(x0 + x, y0 - 1) != FRAME) return 0; + if ((uint8_t)getpixel(x0 + x, y0 + h) != FRAME) return 0; + } + for (y = 0; y < h; y++) { + if ((uint8_t)getpixel(x0 - 1, y0 + y) != FRAME) return 0; + if ((uint8_t)getpixel(x0 + w, y0 + y) != FRAME) return 0; + } + return 1; +} + +static void say(int y, const char *s, uint8_t ok) +{ + setcolor(WHITE); + outtextxy(8, y, (char *)s); + setcolor(ok ? GREEN : RED); + outtextxy(200, y, ok ? (char *)"PASS" : (char *)"FAIL"); +} + +void main(void) +{ + gfx_rect_t area; + uint8_t t[5]; + uint8_t i; + + initgraph(); + gfx_set_visible_page(0); + gfx_pal_sync(); /* у страницы 1 своя палитра */ + gfx_set_bank(GFX_BANK_NORMAL); + + /* Образец — в страницу 1 (она будет НЕактивной, то есть источником). */ + gfx_set_draw_page(1); + setfillstyle(SOLID_FILL, BLACK); + bar(0, 0, 319, 255); + draw_pattern(AX, AY, AW, AH); + draw_pattern(WX, WY, WW, WH); + + /* Приёмник — страница 0. */ + gfx_set_draw_page(0); + setfillstyle(SOLID_FILL, BLACK); + bar(0, 0, 319, 255); + draw_dirt(AX, AY, AW, AH); + + /* T1: прямая копия. */ + area.x = AX; area.y = AY; area.w = AW; area.h = AH; + gfx_copy_page(&area, GFX_COPY_DIRECT); + t[0] = check_direct(AX, AY, AW, AH); + + /* T2/T3: та же область с переворотом (поверх результата T1 — значит + * непереписанная строка сразу видна как «не отразилось»). */ + gfx_copy_page(&area, GFX_COPY_VFLIP); + t[1] = check_vflip(AX, AY, AW, AH); + t[2] = check_frame(AX, AY, AW, AH); + + /* T4: широкая копия — 320 байт не влезают в один accel-блок. */ + setfillstyle(SOLID_FILL, DIRT); + bar(WX, WY, WX + WW - 1, WY + WH - 1); + area.x = WX; area.y = WY; area.w = WW; area.h = WH; + gfx_copy_page(&area, GFX_COPY_VFLIP); + t[3] = check_vflip(WX, WY, WW, WH); + + /* T5: источник обязан остаться нетронутым. */ + gfx_set_draw_page(1); + t[4] = check_direct(AX, AY, AW, AH); + gfx_set_draw_page(0); + + setcolor(WHITE); + outtextxy(8, 8, "gfx_copy_page test v1"); + say(150, "T1 direct copy", t[0]); + say(162, "T2 vflip copy", t[1]); + say(174, "T3 frame intact", t[2]); + say(186, "T4 vflip 320 wide", t[3]); + say(198, "T5 source intact", t[4]); + + for (i = 0; i < 5; i++) + if (!t[i]) break; + setcolor(WHITE); + outtextxy(8, 216, i == 5 ? (char *)"ALL PASS" : (char *)"SOME FAILED"); + outtextxy(8, 228, "done"); + + for (;;) { } +}