diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv index decfe38..152e1bf 100644 --- a/docs/size_baseline.tsv +++ b/docs/size_baseline.tsv @@ -51,4 +51,5 @@ stest2 3644 strtest 1340 text_palette 5024 timedir 5454 +w0page 8831 winrest 4459 diff --git a/docs/sprite-api-design.md b/docs/sprite-api-design.md index cebded4..64509fe 100644 --- a/docs/sprite-api-design.md +++ b/docs/sprite-api-design.md @@ -195,10 +195,27 @@ sy=j*fh. (w/h ленты при этом задублированы: вывод построению: ленты разных размеров просто конкатенируются. Пример (два спрайта 16×16 на 8 и 12 вариаций + один 24×24 на 4): шапка 8 + каталог 24 + (4+2048) + (4+3072) + (4+2304) = 7468 байт, паддинг 0. -Ограничение u16-offset: файл ≤ 64 КБ (для больших — версия 'SPA2' с -u24/u32, пока не нужна). Загрузчик — тонкий хелпер (fread целиком + -арифметика каталога) + Python-упаковщик (PNG → .atl) в toolchain; -делать по потребности первого реального приложения. +Привязка к W0-страницам (решение 2026-07-13, см. §9в): один атлас = +одна EMM-страница = один файл. Размер данных атласа ограничен +16К − 0x100 (страница минус резерв ISR-стаба); если данных больше — +следующий атлас в ОТДЕЛЬНОМ файле на своей странице (sprite_t.page их +различает). Два варианта офсетов в каталоге: + + I. offset = адрес в W0 (0x0100-based): каталог сразу содержит + готовые указатели, данные в файле идут после каталога впритык. + II. файл несёт 0x100-байтовый заголовок, so that файл-офсет == + офсет в странице == W0-адрес: загрузчик читает файл ЦЕЛИКОМ в + offset 0 страницы и лишь патчит стаб (3 байта JP в 0x38, RETN в + 0x66). Раскладка заголовка: 0x00-0x07 магия+count, + 0x08-0x37 резерв, 0x38-0x3A и 0x66-0x67 — место под патч стаба, + 0x68-0xFF каталог (19 записей × 8 Б). Файл ≤ 16384 Б ровно. + +Рекомендуется II: загрузка = один read() страницы, замапленной в W3 +(приём проверен в mdview2) + патч 5 байт; офсеты совпадают в файле, +странице и W0 — нечего пересчитывать. Вариант I оставляет атласам +возможность жить в обычной памяти (malloc-буфер) без страницы. +Python-упаковщик (PNG → .atl) в toolchain и загрузчик — делать по +потребности первого реального приложения. ## 4. Внутренности @@ -648,6 +665,41 @@ noclip доехал до приложения. С vsync-капом оба ва ВНИМАНИЕ: noclip требует гарантии приложения, что спрайты целиком на экране — выход за край портит соседнюю память (tests/spriteclip). +## 9в. Атласы в страницах W0 (дизайн 2026-07-13; probe-тест PASS) + +Атласы грузятся в EMM-страницы (mem_alloc_pages) и на время +sprite_update подключаются в W0 (OUT 0x82) — W2-heap свободен от +спрайтов, объём = EMM (~3.4 МБ). Загрузка — read() в страницу, +замапленную в W3 (приём mdview2; вне рендера W3 свободна). + +ОПАСНОСТЬ: все прерывания исполняют 0x38 текущей страницы W0 — IM1 +напрямую, наш IM2-трамплин чейнит jp 0x0038 (libc/irq/_irq_tramp.c); +не ходит в W0 только личный CBL-путь. ЗАЩИТА — стаб на каждой +странице (первые 0x100 байт зарезервированы): + 0x38: JP ; 0x66: RETN (NMI). +Стаб: свап W0 на страницу ядра DSS (снята со старта IN A,(0x82)) → +подложить возврат на стек → jp 0x0038 (честный обработчик, EI/RETI) → +хвост: ВОССТАНОВИТЬ спрайт-страницу (_w0_cur) → ret. Restore +обязателен (закрывает EI-щель между out(0x82) движка и DI leaf'а); +один стаб покрывает IM1 и IM2-чейн; вложенное прерывание в хвосте +безопасно (W0 в этот момент = DSS, стаб не реентерится). + +ПРАВИЛО: пока спрайт-страница в W0 — НИКАКИХ ESTEX/BIOS (RST-диспетчер +живёт на странице DSS). Движок: пролог/эпилог sprite_update +сохраняет/возвращает страницу DSS; sprite_t получит поле page. + +Probe-тест tests/w0page (MAME dev, 2026-07-13, ВСЕ PASS): + P1 ESTEX READ в W3-замапленную страницу; + P3 IM1: busy-цикл ~5 c с EI при странице в W0 — жив, сентинел цел; + P4 IM2 (irq_install): 100 кадровых прерываний посчитаны при + подключенной странице (полный тракт трамплин→чейн→стаб→DSS→ + restore), сентинел цел; + P5 accel-блит src = 0x0100 (W0) банком 0x50 — пиксели верны по + ОЗУ-копии (getpixel). +На железе перепроверить вместе с остальным. Альтернатива без стабов — +W1, но только tiny (в small/big/huge в W1 код) — отвергнута в пользу +универсального W0. + ## 10. Риски / что проверить артефактом (Фаза 0) Статус 2026-07-11: пп. 1–3 закрыты для MAME 0.283 (tests/gfxbanks, diff --git a/tests/w0page/Makefile b/tests/w0page/Makefile new file mode 100644 index 0000000..dd68064 --- /dev/null +++ b/tests/w0page/Makefile @@ -0,0 +1,4 @@ +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := w0page +EXTRA_FLAGS ?= --gfx 256 --memory tiny +include $(PROJ_ROOT)/app.mk diff --git a/tests/w0page/w0page.c b/tests/w0page/w0page.c new file mode 100644 index 0000000..02f6701 --- /dev/null +++ b/tests/w0page/w0page.c @@ -0,0 +1,243 @@ +/* + * w0page — проверки схемы «спрайт-данные в странице W0» + * (docs/sprite-api-design.md §3.1/§9в, обсуждение 2026-07-13). + * + * Идея: атлас грузится в EMM-страницу; на время sprite_update страница + * подключается в W0 (0x0000-0x3FFF). Опасность: ВСЕ прерывания приходят + * на 0x38 текущей страницы W0 (IM1 напрямую; наш IM2-трамплин чейнит + * jp 0x0038). Защита: в первых 0x100 байтах страницы — стаб: + * 0x38: JP w0_isr_stub (W2), 0x66: RETN. Стаб свапит W0 на страницу + * ядра DSS, отдаёт прерывание настоящему 0x38 (подложив возврат) и + * ВОССТАНАВЛИВАЕТ спрайт-страницу. + * + * Стадии (Enter между; PASS/FAIL печатается ПОСЛЕ восстановления W0): + * P1 ESTEX READ в страницу, замапленную в W3 (приём mdview2). + * P2 подготовка страницы: стаб + картинка 16×16 в 0x100 + сентинел. + * P3 IM1: страница в W0, busy-цикл ~2-3 c с EI — выжить, сентинел + * через W0 верен на каждой итерации (restore работает). + * P4 IM2 (irq_install): страница в W0, дождаться 100 кадровых + * прерываний — счётчик растёт => полный тракт tramp→chain→стаб→ + * DSS→restore живёт; сентинел верен. + * P5 графика: gfx_blit картинки ИЗ АДРЕСА 0x0100 (accel-burst + * src в W0) банком 0x50; self-check getpixel по ОЗУ-копии. + * + * Сборка: tiny (irq_install требует код/данные в W2) + --gfx 256. + * Запуск через MAME-мост: press_key посимвольно + Enter'ы. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define PAT_LEN 1024 /* P1: объём проверки чтения */ +#define SENT_OFF 0x3F00u /* сентинел в конце страницы */ +#define SENT_VAL 0xA5 +#define IMG_OFF 0x0100u /* данные атласа — после резерва стаба */ +#define IMG_W 16 +#define IMG_H 16 + +static uint8_t w2buf[PAT_LEN]; /* эталон в W2 */ +static uint8_t blk, phys; /* EMM-блок и физ. страница */ +static uint8_t dss_page; /* страница ядра DSS (снята со старта) */ + +/* Для стаба (читается из прерывания — только W2-данные). */ +static uint8_t w0_cur; + +/* ISR-стаб (живёт в W2 = _CODE tiny): вызывается JP'ом с 0x38 + * спрайт-страницы. Свапит W0 на ядро DSS, отдаёт прерывание честному + * 0x38 (адрес возврата подложен на стек — DSS завершает EI/RETI и + * возвращается в хвост), затем возвращает спрайт-страницу. Вложенное + * прерывание в хвосте (EI уже сделан DSS) безопасно: W0 в этот момент + * = DSS, штатный путь, стаб не реентерится. */ +static void w0_isr_stub(void) __naked +{ + __asm + push af + ld a, (_dss_page) + out (#0x82), a + pop af + push hl + ld hl, #w0_isr_ret + ex (sp), hl ; вершина стека = возврат, HL цел + jp 0x0038 ; честный обработчик DSS (EI/RETI) + w0_isr_ret: + push af + ld a, (_w0_cur) + out (#0x82), a + pop af + ret + __endasm; +} + +/* W0 := A под DI (щель между out и прерыванием закрывается стабом, + * но сам свап делаем атомарно). */ +static void w0_set(uint8_t page) __naked +{ + (void)page; + __asm + di + out (#0x82), a + ei + ret + __endasm; +} + +static void w0_map(void) { w0_cur = phys; w0_set(phys); } +static void w0_unmap(void) { w0_set(dss_page); } + +/* ---- P4: счётчик кадровых прерываний -------------------------------- */ +static volatile uint8_t irq_ticks; +static void tick_isr(void) { irq_ticks++; } + +/* ---- P5: тестовая картинка ------------------------------------------ * + * Рамка RED, тело LIGHTCYAN, центр 2×2 YELLOW. */ +static void build_image_via_w3(void) +{ + uint8_t *p = (uint8_t *)0xC000 + IMG_OFF; + int x, y; + + p[0] = IMG_W; p[1] = 0; p[2] = IMG_H; p[3] = 0; + p += 4; + for (y = 0; y < IMG_H; y++) + for (x = 0; x < IMG_W; x++) { + uint8_t c = LIGHTCYAN; + if (x == 0 || y == 0 || x == IMG_W - 1 || y == IMG_H - 1) + c = RED; + if ((x == 7 || x == 8) && (y == 7 || y == 8)) + c = YELLOW; + *p++ = c; + } +} + +int main(void) +{ + uint8_t saved_w3; + int fd, i, n; + uint8_t ok; + + puts("W0PAGE: sprite data in W0 window probes"); + dss_page = _io_page_w0; /* IN A,(0x82): страница ядра */ + printf("dss page = %u\n", dss_page); + + blk = mem_alloc_pages(1); + if (!blk) { puts("alloc FAIL"); return 1; } + phys = mem_get_page(blk, 0); + printf("emm page = %u\n", phys); + + /* ---- P1: ESTEX READ в W3-замапленную страницу ------------------- */ + for (i = 0; i < PAT_LEN; i++) w2buf[i] = (uint8_t)(i * 7 + 3); + fd = creat("w0page.dat", 0); + if (fd < 0) { puts("creat FAIL"); return 1; } + write(fd, w2buf, PAT_LEN); + close(fd); + + fd = open("w0page.dat", O_RDONLY); + saved_w3 = _io_page_w3; + sprinter_page_w3(phys); + n = read(fd, (void *)0xC000, PAT_LEN); /* буфер — в W3! */ + close(fd); + + ok = (n == PAT_LEN); + if (ok) + for (i = 0; i < PAT_LEN; i++) + if (*((uint8_t *)0xC000 + i) != w2buf[i]) { ok = 0; break; } + + /* ---- P2: стаб + картинка + сентинел (страница ещё в W3) --------- */ + { + uint8_t *pg = (uint8_t *)0xC000; + uint16_t stub = (uint16_t)&w0_isr_stub; + + pg[0x38] = 0xC3; /* JP w0_isr_stub */ + pg[0x39] = (uint8_t)(stub & 0xFF); + pg[0x3A] = (uint8_t)(stub >> 8); + pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN (NMI) */ + pg[SENT_OFF] = SENT_VAL; + build_image_via_w3(); + } + sprinter_page_w3(saved_w3); + printf("P1 read->W3: %s\n", ok ? "PASS" : "FAIL"); + puts("P2 page prepared (stub+img)"); + (void)getchar(); + + /* ---- P3: IM1, страница в W0, busy-цикл с EI --------------------- * + * Внутри окна НИКАКИХ ESTEX/BIOS. ~50-100 прерываний IM1 пройдут + * через стаб; сентинел через W0 обязан оставаться верным. */ + ok = 1; + w0_map(); + { + uint8_t outer; + uint16_t inner; + for (outer = 0; outer < 60; outer++) + for (inner = 0; inner < 40000u; inner++) + if (*(volatile uint8_t *)SENT_OFF != SENT_VAL) ok = 0; + } + w0_unmap(); + printf("P3 IM1 in W0: %s\n", ok ? "PASS" : "FAIL"); + (void)getchar(); + + /* ---- P4: IM2, страница в W0, счёт кадровых прерываний ----------- * + * Бюджет ожидания: 100 кадров = 2 с; guard-циклов с запасом ~10 с + * (65000×150Т ≈ 0.5 с — прошлый одиночный цикл истекал раньше + * срока и давал ложный FAIL). */ + ok = 0; + { + uint8_t got = 0, sent_ok = 1, outer; + + if (irq_install(tick_isr) == 0) { + uint8_t start; + uint16_t guard; + + w0_map(); + start = irq_ticks; + for (outer = 0; outer < 20 && !ok && sent_ok; outer++) + for (guard = 0; guard < 60000u; guard++) { + got = (uint8_t)(irq_ticks - start); + if (got >= 100) { ok = 1; break; } + if (*(volatile uint8_t *)SENT_OFF != SENT_VAL) { + sent_ok = 0; + break; + } + } + w0_unmap(); + irq_remove(); + } else + puts("irq_install failed"); + printf("P4 IM2 in W0: %s (ticks=%u sent=%s)\n", + ok ? "PASS" : "FAIL", got, sent_ok ? "ok" : "BAD"); + } + puts("Enter -> gfx"); + (void)getchar(); + + /* ---- P5: accel-блит из W0-адреса -------------------------------- */ + initgraph(); + cleardevice(); + setfillstyle(SOLID_FILL, GREEN); + bar(140, 90, 175, 125); /* фон вокруг картинки */ + + w0_map(); + gfx_blit(150, 100, (const void *)IMG_OFF); /* src = 0x0100 (W0)! */ + w0_unmap(); + + ok = (getpixel(150, 100) == RED) /* рамка */ + && (getpixel(157, 107) == YELLOW) /* центр */ + && (getpixel(152, 103) == LIGHTCYAN) /* тело */ + && (getpixel(148, 100) == GREEN); /* фон цел */ + + setcolor(WHITE); + outtextxy(4, 4, "W0PAGE gfx: blit from 0x0100 (W0)"); + outtextxy(4, 200, ok ? "P5 blit from W0: PASS" + : "P5 blit from W0: FAIL"); + outtextxy(4, 210, "Enter to exit"); + (void)getchar(); + + closegraph(); + printf("P5 gfx blit: %s\n", ok ? "PASS" : "FAIL"); + puts("done"); + return 0; +}