Реализовать двухпанельный Commander от платформенного PoC до этапов P6-P20: EMM-каталог, сортировку и выбор, операции с файлами и деревьями, транзакционное копирование, метаданные, политику конфликтов и предварительную проверку свободного места.
Добавить проектную документацию, HDD/MAME-сценарии и проверенные артефакты. Расширить libc операцией bank_write_page, исправлением режима O_RDONLY и связанными регрессионными проверками.
roomtest:
- QuickSave/QuickLoad (F6/F9): снапшот 'POPQ' v3 в POP.SAV/POP.BAK на HDD,
транзакционная запись (POP.NEW -> rename, откат при ошибке), XOR-контрольная
сумма payload'а; сериализация всех игровых переменных через W0-примитивы
pop_qs_*; pop_qsave_process() на границе кадра вне Char-окон
- pop_qsave_restore_room(): полная перезагрузка комнаты после загрузки
(карта/края/швы, сброс bake-кэша, перерисовка обеих страниц, инвалидация
кэшей спрайтов и HP)
- сериализаторы в pop_map/pop_loose_mob/pop_trob/pop_guard_ai
(+ восстановление инвариантов: mobs_live, trob_drawn, redraw)
- immortal-чит 2 уровня: уровень 2 поглощает только малый урон Kid'а
libc/libbgi:
- bank_load_file()/bank_save_file() — резидентное файловое I/O в банк,
без правила W3 (путь читается до переключения страницы)
- gfx_w0_page_prepare(page) — подготовка W0-окна (IRQ/NMI-стабы) одной
функцией; atlas_load.c и roomtest переведены на новые примитивы;
ручные ISR-стабы удалены
sprinter-cc / сборка:
- --bank N=FILE.c: автогенерация n_banks (_n_banks_auto.c), ручные
const n_banks удалены из тестов
- roomtest/app.mk: ресурсы через stamp-файлы (.resource-stamps/) — один
запуск упаковщика на группу вместо N под -B; HDD_PACK_ARGS
Вертикальный скролл перестал ходить через строку-буфер на стеке: колонку
читаем с Port_Y=ys, пишем с Port_Y=yd, а STOP между read- и write-триггером
делает промежуточный OUT Port_Y безопасным. Один проход вместо
grab→blit-через-буфер.
Справочник libc приведён в соответствие (там же строка про новую точку
входа cbl_open_silence).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tests/cbltest не попал в правку API (мой греп обрезался на артефактах
сборки, полный make его и поймал).
Эталон размеров: cblstream −614, cbltest −617, cblwav −592 — это ушедший
malloc. bgi_img +229 к моей правке отношения не имеет (CBL он не линкует
вовсе): рост пришёл с b3e754a, реверта «каталог атласа читается из W0».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Линкер тянет .rel целиком, поэтому malloc/free, стоявшие в мёртвой ветке
CBL_UNDERRUN_SILENCE внутри cbl_open, приезжали в резидент КАЖДОМУ
приложению — включая те, что льют тишину сами и кучей не пользуются.
Разведено:
cbl_open(freq, fmt, pump, fill) — ничего не аллоцирует;
cbl_open_silence(freq, fmt, pump, fill) — аллоцирует буфер тишины;
_cbl_open_raw(...) — общее тело.
Параметр underrun_mode из публичного API убран: режим задаёт выбор функции.
cbl_close больше не зовёт free — иначе malloc возвращался бы тем же путём.
Буфер тишины живёт до выхода из программы и переиспользуется; его размер
запоминается, иначе открытие 16-бит после 8-бит писало бы memset'ом мимо
выделенного куска. _cbl_open_raw указатель на буфер не трогает вовсе —
иначе cbl_open после cbl_open_silence терял бы уже выделенную память.
В дереве режим SILENCE не использовал никто: все три вызова (cblwav,
cblstream, PoP) передавали CBL_UNDERRUN_APP.
Итог для PoP: _CODE 24329 -> 23716 (−613 Б), свободно в резиденте 129 ->
747 Б. make size-check чистый (67 программ), звук в MAME проверен —
насос отработал 696 запросов, pop_snd_ok/want = 1/1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gfx_blit_noclip_vflip / gfx_blit_part_noclip_vflip / gfx_blit_part_vflip.
Ассемблера не потребовалось: у row-major вертикальное зеркало — это порядок
строк, а он задаётся ЗНАКОМ src-страйда, который _bgi_blit_rows_raw и так
принимает знаковым и патчит SMC. Даём адрес последней строки и -stride.
Клип у part_vflip свой: обрезка сверху экрана съедает НИЖНИЕ строки
источника, и первая строка обязана считаться от высоты ДО клипа (поймано
тестом — сначала формула брала уже укороченную).
tests/pageflip расширен до 8 проверок, все PASS в MAME.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Первый шаг 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>
NOT. У акселератора нет режима «инвертировать буфер»: буфер меняется
только на ЧТЕНИИ и только опкодами AND/OR/XOR (HL) (драйвер MAME,
update_accel_buffer), а `CPL` автомат вообще не распознаёт — инвертируется
регистр CPU, не буфер. Зато ~src = src XOR #FF, поэтому NOT собирается из
уже имеющегося: буфер := src, XOR-burst по константному блоку единиц
(common/_bgi_ones256.c), запись. Ядра _bgi_blit_{rows,cols}_not_raw.c;
между burst'ами меняется HL, поэтому каждая смена — под СТОПом (иначе fetch
операнда перезапустит burst). В колоночном варианте второй OUT Port_Y не
нужен: op-burst идёт по блоку единиц горизонтально, а Port_Y шагает только
вертикальный.
Наружу — тем же op-параметром (GFX_OP_NOT), диспетчер один раз на вызов, в
цикл по полосам/колонкам не заходит. putimage лишился попиксельного пути
ЦЕЛИКОМ: все пять операций BGI идут через акселератор и клиппируются
одинаково. tests/accop дополнен T9/T10 (NOT строками и колонками) —
10/10 PASS в MAME; tests/bgi_img P1/P2 по-прежнему PASS.
tests/convbench (новый) — замер побайтной конвертации атласа
«прозрачный #FF -> 0x00» (источник для XOR-блита): 145.3 такта/байт, то
есть 2.5× от 59 номинальных T-states цикла. 0.11 с на страницу 16 КБ,
3.2 с на все 28 страниц, 1.3 с по фактическому объёму данных (186 КБ).
applications/PoP/docs/shadow_render.md — вид Тени (два блиттера OR+XOR)
ОТЛОЖЕН по решению пользователя: пока рисуем обычной копией атласами Кида.
В документе собрано, почему в лоб не выходит (XOR несовместим с
прозрачностью #FF; операция читает ОЗУ-копию, поэтому два прохода
оригинала вырождаются в один XOR — нужен однопроходный композит
s | bg ^ s(сдвиг)), замер стоимости источника и четыре варианта. Ключ к
выбору — список кадров, которыми тень реально пользуется; снимать по факту,
когда пойдут уровни 5/6/12.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Акселератор умеет не только копировать блок, но и совмещать его с
приёмником: опкод `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>
Скелет (L3-SKEL, ассеты + механика):
- pop_pack_guard.py получил параметр набора (GUARD/SKEL): атлас скелета
poc/res/skel/g0..g3.atl (28 кадров), палитра — из его res750.pal (на ур. 3
curr_guard_color = 0, оригинал палитру не подменяет);
- pop_guard_load выбирает набор по tbl_guard_type и перезагружается ПРИ СМЕНЕ
УРОВНЯ (load_lev_spr, seg000:1092) — без этого скелет рисовался атласом
стража и был невидим;
- load_frame: charid_4_skeleton идёт по таблице стража (seg006:529), тень —
только в кадрах 150..189. Пока ветка была одна (charid_2_guard), скелет
получал image из таблицы Кида (180 при 28 спрайтах) и не рисовался;
- check_skel (seg002:1042), ветка charid_4 в enter_guard, возрождение в
комнате 3 при падении (seg002:252), autocontrol_skeleton;
- leveldoor_open (seg007:456) — новый флаг, сбрасывается стартом уровня.
Цвета стражей (BUG-GUARD-COLOR-1, закрыт):
- все 7 палитр res10.bin -> pop_guard_pal.h, заливка 16 слотов по
guards_color комнаты перед отрисовкой (set_chtab_palette, seg003:257).
Проверено в MAME: ур. 2 комн. 11 = цвет 1, комн. 7 = цвет 3, полоса HP
меняется вместе со стражем. Грабля: gfx_pal_load отдаёт указатель в BIOS,
а тот читает только #4000-#BFFF — таблицу из банка копируем в стек.
Кэш соседних комнат (BUG-SWORD-GHOST-1, закрыт):
- pop_map кэширует fg соседей слева/справа ЦЕЛИКОМ и резолвит col -10..19.
Было -2..11, дальше мнимая стена: луч видимости упирался в неё (страж
после follow_guard в col 12), Кид прятал меч посреди боя и не мог достать
обратно. +48 байт W2.
Окклюзия соперника:
- pop_fore_over_char получил проход other_overlay_tile (порядок midtable,
seg008:1B06) и расширение перебора объединённым прямоугольником
«персонаж + клинок + брызги» — падающий скелет больше не рисуется поверх
кладки и верхней грани пола;
- клип полем 192 строк (reset_obj_clip, seg006:0507) для спрайта, клинка
(общий pop_sword_draw) и брызг — спрайт не залезает на полосу HP;
- ROOMNAV после смерти Кида делает честный pop_start_level: телепорт
«оживлял» мёртвого мимо старта уровня, оставляя живого скелета рядом с
вернувшейся кучей костей.
Ассеты чомпера (под L3-CHOMP): весь набор кадров в атласе явным списком
(101-105 низ, 111-113 верх, 106-110 фронт, 114-123 кровь mono-силуэтом) —
render_room анимированные тайлы пропускает, и в атласе не было ни одного.
Число EMM-страниц не изменилось.
Тесты: tests-host все 5 наборов зелёные, t_char вырос до 65 проверок
(резолв колонок за краем комнаты, возрождение скелета); в testkit добавлен
гард «код наехал на данные» (DATA_LOC).
Доски: TASKS.md разнесён на TASKS_OPEN/TASKS_CLOSED, закрытые баги с
разбором корней — в bug_closed.md; заведены DRAW-CHAR (отрисовка одна на всех
Char, как физика после GUARD-PHYS) и L3-COLOR (зелёная кладка уровня 3:
level_var_palettes = ресурс 20, есть в MSDOS/PRINCE.DAT).
В roomtest.c временно оставлен автостоп на падении соперника (отладка
падений скелета) — помечен ВРЕМЕННО.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Симптом: при удерживаемом Shift часть нажатий стрелок не отрабатывает
(~15 % по наблюдению пользователя), без Shift потерь нет.
Переведено в числа: нажимается Home — тоже расширенная клавиша (тот же
E0-префикс и тот же «fake shift»), но игрой игнорируется, поэтому рельеф
комнаты на результат не влияет. Счётчики — брейкпоинты MAME с действием
{ b@ADDR = b@ADDR+1 ; g } на входе клавиатурной ветки трамплина, на чтении
порта 0x18 и на установке make-бита.
Что измерено (10 нажатий Shift+Home, дошло make):
игра идёт, опрос ВКЛ 9/10 игра идёт, опрос ВЫКЛ 9/10
игра ЗАМОРОЖЕНА (блитов нет вообще, длинных DI нет) 8/10
Обе исходные гипотезы отпали:
- длина наших DI-окон ни при чём (в замороженном кадре потерь больше);
- снятие di в accel-ядрах libbgi УРОНИЛО машину — режим «акселератор при
EI» из docs/new/06-accel.md §6.6 в этой прошивке недоступен.
Байт теряется НИЖЕ нашего кода: на 49 прочитанных байт пришлось только 28
входов в клавиатурную ветку, то есть ~44 % импульсов запроса прерывания не
обслуживается и трёхбайтовый FIFO SIO переполняется.
Потолок приёма измерен отдельной программой tests/kbdpoll (ничего, кроме
kbd_raw_poll в цикле): 25 нажатий -> 25 make, 250 байт из 250, ноль потерь.
Значит опрос лечит полностью, вопрос только в плотности: нужно раз в
~0.5 мс, а шесть вызовов за 60-мс кадр давали раз в 10 мс.
Поэтому вызовы из roomtest.c УБРАНЫ (они стояли там, где прерывания и так
разрешены, и дублировали трамплин — 9/10 с ними и без). Сама функция
kbd_raw_poll оставлена в libc: она корректна и нужна как основа плотного
опроса. В заголовке и в libc-reference — честная оговорка, чтобы её не
ставили в игровой цикл «на всякий случай» без замера.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверяет то, на чём стоит план раскладки PoP (layout_plan_v2.md §2):
R3 резидент/HOME -> __banked через трамплин работает;
R4 примитивы libbgi можно звать ИЗ БАНКА: _bgi_begin запоминает текущую
страницу W3 (порт 0xE2), _bgi_end её возвращает — банк переживает
рисование и продолжает исполняться;
то же верно для функции W1/W2, вызванной из банка: она рисует, а в W3
остаётся страница БАНКА, не резидента.
Результат в MAME: все пять полос на месте, вердикт ЗЕЛЁНЫЙ. Замеры:
страница банка 0xF0 до рисования, после прямого блита и после возврата из
W1/W2-функции — та же 0xF0; резидент 0xF3; банк дожил до конца и вернул
корректное значение.
Два побочных вывода, важных для стражей:
1. Писучие статики __banked-модуля линкуются В СТРАНИЦУ БАНКА (адрес
0x1C000+), снаружи их не прочитать — состояние банка держать в W1/W2.
2. Инлайновый `in a,(0xE2)` посреди тела функции затирает A, куда SDCC уже
положил параметр (в первой версии пробника цвет заливки становился
номером страницы, и «резидент не рисовал»). Читать порт отдельной
__naked-функцией.
Имя exe — 8.3 (w3bgfx.exe): DSS длинных имён не понимает.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Скролл региона video->video (неактивная страница -> активная, банк 0x50:
копия = скролл + heal цели):
- gfx_scroll_h / _bgi_scroll_rows_raw — горизонтальный, построчно без
страйдов (~54Т/строку), DI/EI бандами по 16 строк, h=0=>256;
- gfx_scroll_v / _bgi_scroll_cols_raw — верт. И/ИЛИ гориз. за один проход
без буфера (колонка = accel-burst LD A,A, STOP между read/write делает
промежуточный OUT Port_Y безопасным), банды по 16 колонок;
- _gfx_addr_shadow_base (адрес неактивной страницы) + gfx_rect_t.
Пример examples/scroll.
check_banks.py + sprinter-cc: отчёт раскладки памяти для ЛЮБОЙ модели
(W1/W2 код/данные, остаток кучи/стека, W3 при --w3, банки при --bank),
не только при --bank. Док docs/memory-management.md §10.
--w3 (резидентный код окна W3) + сопутствующее: crt0_banked W3_RESIDENT,
mkexe -W, tests/w3probe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Логический кадр = ровно n кадровых интервалов (1=50/2=25/3=~16.7 fps);
при переполнении слота — выравнивание на ближайший фронт (без дрейфа
фазы, в отличие от наивного «жди n фронтов»).
Механика: фоновый счётчик _gfx_frame_tick инкрементит _gfx_frame_isr,
поставленный в СВОЙ слот цепи (irq_chain_add); gfx_set_fps_div(1) снимает
только этот слот (irq_chain_remove), не трогая хендлер приложения.
gfx_wait_vsync: ветка n>=2 (счётчик + halt) перед лучевым поллингом;
поллинг вынесен в static gfx_wait_vsync_beam (функция с хвостовым __asm
не должна иметь переходов через asm — SDCC не эмитит эпилог-метку;
ранний return делителя в чистом-C gfx_wait_vsync).
Файлы: common/_gfx_fps_state.c (данные), _gfx_frame_isr.c (ISR),
gfx_set_fps_div.c (сеттер, единственная ссылка на irq-механику → DCE).
Работает tiny/big/huge (цепочка all-modes); small для мелких программ
= EINVAL.
Проверено MAME (tests/fpsdiv): n=1/2/3 → 20/40/60 кадров на 20 wait'ов
(drift=0); n=2 с рендер-заглушкой ~1 кадр → период держится 2
(поглощение перерасхода, наивный путь дал бы ~60); huge идентично;
small = EINVAL graceful.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_irq_user → _irq_chain[4]+_irq_chain_n (W2-BSS); трамплин tr_frame
проходит слоты (один тяжёлый сейв на всю цепь, пустой слот пропуск).
API irq_chain_add (0/-1+ENOMEM) / irq_chain_remove(h); irq_install →
обёртка chain_add (EBUSY исчез), irq_remove() рвёт всю цепь; refcount
таблицы на первом/последнем слоте, мутации под IRQ_DISABLE. Лимит
4 кадровых + 1 CTC.
All-modes: трамплин copy-safe (только jr/djnz + литерал jp 0x0038),
_irq_table_ref копирует его в _irq_tramp_w2buf (W2) когда оригинал в W1
(small/huge), вектор → на копию; вокруг вызова хендлеров восстанавливает
базовую W1-страницу (_irq_app_w1_page = IN 0xA2). Хендлер может лежать
где угодно в плоском 0x4000-0xBFFF.
Проверено в MAME (tests/irqtest, 2 хендлера): tiny/big/huge — chain
h1=h2 → remove h2 → h1 жив/h2=0; huge = код в W1, remap работает.
Follow-up: CTC в small/huge = EINVAL (нужна W2-копия _irq_ctc_tramp);
small для мелких программ (BSS в W1) = irq_install EINVAL, safe.
Доки: im2_isr_design (цепочка), sprite-api §9е (делитель поверх цепи),
fast_ram §8. rpgprof: gfx_sprite_ysort в профиль (+230 Б baseline).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Оптимизации A+B (профиль rpgwalk-15: активная часть кадра 410К → 307К
тактов из 430080; лимит спрайтов 16×16 на стабильные 48.8 fps: 14 → ~21):
- (B) sprite_t.src/stride — готовый адрес кадра: считают только
sprite_frame (теперь функция, одно умножение на СМЕНУ кадра) и тикер
(±an_step БАЙТ инкрементально); блит-ядра принимают src+stride,
img/sx/sy из сигнатуры ушли. Блит 177К → 146К на кадр.
- (A) тик 100К → 37.7К: tween переформулирован Брезенхэм → беззнаковый
DDA (mv_rem/mv_acc, «приехали» = rem==0 — без знаковых 16-бит
сравнений), tick_move и tick_anim — ручной asm (SDCC спиллит такие
функции в IX-фрейм ~100 обращений; C-реструктуризации не помогали —
проверено кодогеном). Биты an_flags переименованы по категориям
(_SPR_STRIP_HORZ, _SPR_PP_BACK).
Y-сортировка (gfx_sprite_ysort, идеи пользователя — 8-бит ключ,
персистентность):
- painter's algorithm по ключу {layer:8, clamp_y:8}; поле
sprite_t.layer (в КОНЦЕ структуры — asm-офсеты не сдвигает): слои
сцены в одном массиве/одном sprite_update;
- ПЕРСИСТЕНТНАЯ asm-таблица {key16, ptr16}: resort порядка прошлого
кадра (почти линейно), rebuild при смене arr/count; массив
приложения не трогается; ~28К/15 спрайтов (с нуля было 44К);
- компоненты YSORT_Y/YSORT_LAYER отключаемы независимо масками ключа
(без ветвлений в сортировщике); ВНИМАНИЕ: mode=1 значит Y-only,
полный порядок = YSORT_Y|YSORT_LAYER;
- funcptr-DCE: выключено = код и таблица не линкуются (rpgwalk −190 Б);
- ПРАВИЛО в sprite.h: два sprite_update на страницу запрещены (heal
второй группы стирает спрайты первой — ОЗУ-копия чистая).
Попутные фиксы:
- libbgi/Makefile: .rel зависят от заголовков (HDRS) — stale .rel со
старой раскладкой sprite_t молча ломал рантайм;
- rpgprof: --memory small (перерос tiny: BSS вылезал за W2 → мгновенный
«Unexpected application termination»; mkexe это пока не ловит);
- tests/spranim: проверки переведены на кэш src, добавлены T6 (reframe
после тикера) и T7 (Y-сортировка: порядок, слои, персистентный
resort, LAYER-only) — 7/7 PASS в MAME.
Доки: §9д — новый бюджет (19.5К/спрайт), §9е — ПЛАН FPS-делителя
(frame pacing, gfx_set_fps_div); TODO — дизайн цепочки irq-обработчиков.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- gfx_pal_sync(): палитра страницы 1 := палитре 0 (все 256 записей,
чанками по 64 через общий буфер _gfx_pal_buf в W2) — обязательный
шаг дабл-буфера (грабли examples/space: без него флип на страницу 1
чёрный). examples/space переведён на хелпер.
- gfx_pal_fsave(pal, path): 256 записей × 4 Б (B,G,R,0 — родной формат
BIOS $A4) = 1024 Б.
- gfx_pal_fload(pal, path): принимает и усечённый файл (64 Б = палитра
16 цветов) — грузит сколько есть, остальные слоты не трогает;
возвращает число записей.
tests/palfile (MAME dev, все PASS): fsave; fload восстанавливает
испорченные слоты (n=256); усечённый файл на 2 записи чинит только их
(n=2, слот 2 не тронут); sync чинит испорченный слот палитры 1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Реализация §9г по требованиям пользователя:
- sprite_anim(first,last,speed,mode): ANIM_LOOP / ANIM_PINGPONG /
ANIM_ONCE (one-shot замирает на last), | ANIM_HORIZ для
горизонтальных лент. Смена кадра в тике = ±an_step к оси ленты —
без умножений (осевое смещение first считается в setup циклом).
- sprite_moveto(tx,ty,max_step,interval): Брезенхэм порциями
≤max_step вдоль большей оси (меньшая — err-аккумулятором,
нелинейные шаги Y сами собой), деления нет.
- Одновременность кадровой и tween — независимые поля/биты.
- Статус: sprite_anim_status() — битовое поле SPR_ANIM_ON/DONE +
SPR_MOVE_ON/DONE; sprite_anim_frame() — текущий индекс кадра;
sprite_moving().
- Стопы: sprite_anim_stop(frame | -1 = текущий);
sprite_move_stop(0 = замереть / 1 = прыжок в цель + DONE).
- Тикер _sprite_tick — проход 0 sprite_update через funcptr
_spr_tick_fn (DCE: без sprite_anim/moveto код не линкуется,
цена — один if на кадр; +40 Б программе с движком, sprite_t +18 Б).
tests/spranim (MAME dev, все PASS): LOOP/PINGPONG (разворот на границе
без удвоения краёв)/ONCE+DONE; пример (0,0)→(100,50), шаг 5,
интервал 4 → ровно 80 кадров, Y идёт 3/2/3/2; стопы обоих видов.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Спрайтовая графика поверх accel block-copy (docs/sprite-api-design.md):
- Ядро блиттинга: leaf'ы _bgi_blit_rows_raw (dst фикс, только src-страйд) /
_bgi_copy_rows_raw (getimage) / _bgi_heal_rows_raw (src==dst). DI один на
спрайт (санкция: малый спрайт под одним DI аудио не рвёт); src[0]-фикс
снят (точная MAME подавляет CPU-байт триггера записи — на железе
перепроверить; для heal был избыточен и снят безусловно).
- Общие bracket-free ядра _gfx_blit_full/_gfx_heal_full (полная ширина:
клип по экрану + split >256 для putimage) + лин _gfx_blit_sprite/
_gfx_heal_sprite (кадр ≤64, без split, 8-бит w/h) + noclip-варианты
(клип-кода нет → полный codegen-win). Имя *_full (не *_clip) — «clip»
двусмысленно (sprite-ядра тоже клипуют; различитель — ширина/split).
- Движок retained-модели <sprite.h>: sprite_init/update/flip + inline
move/frame/show/hide/touch; drawn[2] per-page внутри структуры; кадр —
двухпроходно heal ВСЕ -> блит ВСЕ под одной W3-скобкой/банком на проход.
- Флаг gfx_sprite_clip(on/off): приложение, само следящее за границами,
отключает клип (~+19% на анимации; диспетч пока через if — funcptr далее).
- putsprite/movesprite/gfx_blit/putimage(COPY)/getimage переведены на ядро.
Тесты: examples/balls (движок, дабл-буфер, boundary-тест клипа),
tests/sprites (RAM PASS, клип 4 края, атлас), tests/blitw (trig-leak),
tests/spriteclip (hardware-probe: железо НЕ режет за краем -> клип нужен),
tests/blitperf, tests/gfxbanks. size-baseline обновлён (53 программы).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Семантика FSM акселератора вскрыта по драйверу MAME (sprinter.cpp) и
подтверждена экспериментами в MAME (tests/accfill): армирование живёт до
следующего accel-опкода (мульти-триггер работает); под армированием
триггерит ЛЮБОЙ non-M1 доступ к памяти (fetch операнда djnz/out!);
вертикальный Fill двигает Port_Y и не возвращает; размер блока переживает
LD B,B. Детали: memory/accel_multitrigger_fill.
- _bgi_clear_raw: 20 DI-скобок по 16 колонок вместо полного брекета на
каждую из 320 колонок; армирование размера один раз на скобку.
- _gfx_rectfill256 разделён: диспетчер (проверка ориентации ~160-245Т,
break-even |w-h| >= ~4) + leaf'ы _gfx_recthfill256/_gfx_rectvfill256
для прямого вызова, когда форма известна заранее.
- Leaf'ы: чанки <=16 линий одной скобкой (~54-56Т/линию против ~250Т у
per-line варианта); счётчик чанков precompute'ится в байт-регистр
(dec e/jr nz ~46Т/чанк вместо 16-бит арифметики в IX-слотах
~173Т/чанк); хвост — отдельная скобка со своим армированием (CBL-ISR
в окне EI армирует акселератор своим размером — не выносить).
- getpixel/putpixel/_bgi_read: уборка мёртвого закомментированного кода.
- tests/accfill: регресс chunked-заливок (B0 clear, B1 vert 1+3 чанка,
B2 horz чанк+хвост, полноширинный bar 320x8 — путь w>=256).
Прежние реализации сохранены под #if 0 для отката/сравнения.
Проверено в MAME (все PASS); семантика эмуляции — перепроверить на
реальном Sprinter. size-baseline: accfill добавлен, gfx_demo +54 Б
(обвязка диспетчера), остальные без роста.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- удалён глобальный скретч акселератора (_gfx_acc256.c); fill-сегменты
(_gfx_hfill256/_gfx_vfill256) принимают аргументы в регистрах HL/C/B/E,
вызываются только из asm raw-примитивов
- новый _gfx_rectfill256: заливка прямоугольника через Horizontal_Size
- raw-примитивы (plot/read/hspan/vspan/clear) переписаны под новый ABI
- graphics.h: typedef color_t (uint8_t) для всех public color-функций
- sprinter-cc: флаг --safe (линковка *_safe.lib при наличии)
- CLAUDE.md/mame_interactive: авто-прогон тестов в MAME
- gfx_demo: демонстрация rectfill
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Графика вынесена из libc/ в новую библиотеку libbgi/:
- common/ — mode-agnostic математика и состояние (один исходник,
.rel попадает в оба driver-архива);
- bgi256/ + bgi16/ — mode-specific leaf'ы (raw-плот/чтение/спаны);
- include/ — graphics.h + gfx.h; _bgi.h — внутренний заголовок.
Собираются lib/bgi256.lib (и bgi16.lib в Фазе 2); выбор режима
линковкой через sprinter-cc --gfx 256|16. libc/ теперь без графики.
tests/bgi_img — тест спрайтов getimage/putimage/imagesize (5 операций
COPY/XOR/OR/AND/NOT + XOR-round-trip + self-check imagesize).
Проверен автотестом в MAME.
Примечание: make size-check пока красный (gfx_dbuf/gfx_demo выросли
после реорга) — закрыть по завершении миграции libbgi.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
settextstyle/gettextsettings/textwidth/textheight: DEFAULT_FONT 8×8,
целочисленный масштаб 1..10, HORIZ/VERT (поворот 90° CCW), прозрачный
фон. Свой scaled-рендер (_bgi_draw_scaled) читает глиф через leaf
_bgi_font_rows (interleaved системный шрифт) и рисует блоки size×size
raw в одной W3-скобке. outtext/outtextxy переведены на него. Проверено
в MAME (tests/bgitest): размеры 1..4 + вертикальный текст.
Доки обновлены (Ф2d-1/2/3 готовы; осталось viewport/клиппинг).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
setlinestyle/getlinesettings: SOLID/DOTTED/CENTER/DASHED/USERBIT +
NORM/THICK. _bgi_styled_line — Брезенхэм с 16-битной маской (пропуск
пикселя по биту) и дублированием ±1 перпендикулярно оси для THICK;
SOLID+NORM идёт быстрым путём (accel _bgi_lineseg). line/lineto/linerel/
rectangle/drawpoly переведены на него. Проверено в MAME (tests/bgitest).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Растровые образы: imagesize (4 байта заголовка w,h + w*h пикселей),
getimage (захват прямоугольника), putimage с COPY/XOR/OR/AND/NOT_PUT.
Блит идёт raw в одной W3-скобке — добавлен _gfx_getpixel256_raw в gfx +
leaf _bgi_read_raw в drv256. Проверено в MAME (tests/bgitest): захват
спрайта, 3 COPY-копии, XOR/OR/COPY поверх фона.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
floodfill — скан-строчная заливка области до границы (self-bracket
чтение: корректно, но медленно; raw-оптимизация в TODO).
pieslice/sector — залитые сектора круга/эллипса: границу (центр→дуга→
центр) прогоняем через _bgi_poly_edge и заливаем min/max по строкам,
как fillpoly (для >180° возможен перелив — упрощение). Проверено в
MAME (tests/bgitest): floodfill круга, круговая диаграмма, штрих-сектор.
Доки/справочник/память обновлены (Ф2a-c готовы; Ф2d = images/viewport/
text-style/line-style — осталось).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
setfillstyle/getfillsettings + 10 стандартных 8×8 паттернов Borland.
bar теперь честно учитывает стиль заливки; bar3d (3D-брусок), fillpoly
(scanline min/max по строкам через брезенхэмовский проход рёбер),
fillellipse (полуширина строки через целочисленный _bgi_isqrt — без
32-бит). Общий _bgi_fill_span (SOLID/EMPTY/паттерн) с клипом, поверх
raw-hline в одной W3-скобке. Проверено в MAME (tests/bgitest): solid/
hatch бары, bar3d со slash, синий fillellipse, xhatch-треугольник.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Дуги/эллипсы через целочисленную тригонометрию Q7 (_bgi_trig.c, ×128) +
общий рисователь полилинией (_bgi_arc_draw.c). arc(x,y,st,end,r),
ellipse(x,y,st,end,xr,yr), drawpoly(n,pts). Проверено в MAME (tests/
bgitest): окружность/эллипс/дуга/полигон рисуются корректно.
ВАЖНО: тригонометрию считаем в int (Q7), НЕ через (long)…>>8 — первая
версия на 32-бит арифметике рисовала эллипс прямоугольником (SDCC/z80
криво собирает 32-бит; см. memory/avoid_32bit_arith_z80). Q7 даёт
радиус×значение ≤ 255×128 < 32767 — всё влезает в 16 бит.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Функционально-совместимый с Turbo-C <graphics.h> поверх gfx.h.
Архитектура: mode-agnostic математика (libc/bgi/*.c → sprinter.lib) +
driver-leaf'ы per-режим (libc/bgi/drv256/*.c → sprinter_gfx256.lib).
Режим выбирается линковкой: sprinter-cc --gfx 256 (16 — позже, тем же
leaf-split'ом). Один код работает в любом режиме без правок.
API: initgraph/closegraph/graphresult/cleardevice, set/get color+bkcolor,
getmaxx/y/color, put/getpixel, moveto/moverel/getx/gety, line/lineto/
linerel, rectangle, bar, circle, outtext/outtextxy. initgraph грузит
EGA-палитру 0..15. Пакетные примитивы (circle) — одна W3-скобка на
примитив (иначе на порядок медленнее). Проверено в MAME (tests/bgitest).
Попутно: gfx_getpixel256 в libc/gfx. size-check без регресса, baseline
обновлён. Детали: memory/bgi_two_lib_design, docs/TODO.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gfx_wait_vsync() и sleep() раньше предполагали, что КАЖДОЕ прерывание
на векторе 0xFF — кадровый тик; с CBL/клавиатурой на том же векторе
это уже не так.
gfx_wait_vsync(): вместо halt — polling бита 5 порта 0xFE (реальная
позиция луча, см. MAME kbd_fe_r), доступного пока включён CBL bit7
порта 0x004E. Разделяемое владение портом с CBL через
_cbl_port_ref/unref (тот же ref-counting паттерн, что у IM2-таблицы) —
cbl_close() возвращает "немой" режим вместо полного выключения, если
gfx ещё держит ссылку. Fallback на halt при таймауте.
sleep()/delayms(): калиброванный busy-wait по духу delayms.asm вместо
подсчёта halt-пробуждений. Калибровка одна на кадр (не на секунду —
не переполняет uint16_t и не требует умножения/32-бит арифметики),
общий движок libc/time/_sleep_calib.c для обеих функций. Fallback на
старое поведение при EBUSY (фрейм-хук занят другим irq_install()).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CBL уже имеет аппаратный буфер 256 Б (2×128, двойная буферизация на
стороне железа) — держать поверх него ещё одно кольцо в libc было бы
лишней копией. cbl_open(freq, fmt, pump_mode, underrun_mode, fill)
регистрирует callback, вызываемый из ISR за очередным блоком; он сам
пропихивает данные приложения (откуда угодно) через cbl_push_otir()/
cbl_push_accel() — без промежуточного буфера.
- два насоса: OTIR (порт 0x4F) и ACCEL (акселератор, спец-страница
EMM 0xFD@0xC000); OTIR+16-бит запрещён (EINVAL) — по исходнику MAME
порт данных физически не может собрать 16-бит сэмпл из пары байт;
- форматы CBL_FMT_MONO8/16/STEREO8/16, частоты 7.8..109к;
- CBL_UNDERRUN_APP (по умолчанию, недолив не наша забота) /
CBL_UNDERRUN_SILENCE (буфер тишины malloc'ится только в этом режиме);
- tests/cbltest: матрица 64 комбинации (2 насоса × 8 форматов × 4
частоты); tests/cblwav: banked-стрим речи с дискеты (физстраницы
кэшированы заранее — mem_get_page нельзя звать из fill()/ISR);
tests/cblstream: единственный случай с собственным кольцом уровня
приложения (диск нельзя читать из fill()).
Verified в MAME 2026-07-07 — все три теста работают.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- irq_ctc_install(handler, div2, div3) / irq_ctc_remove: канал 2 CTC
делит видеотакт 875 кГц (1 тик = 1 знакоместо), канал 3 считает от
него и прерывает; f = 875000/(div2*div3), div 0 = 256. Пресет
IRQ_CTC_VSYNC_DIV2/3 = 112*160 — точное начало кадра ~48.8 Гц БЕЗ
примеси клавиатуры (вектор 0x06 отделён от общего 0xFF)
- CTC-трамплин: полный сейв -> handler -> EI/RETI; RETI обязателен
(daisy chain Z84C015 снимает IUS только по опкоду RETI); к DSS не
чейнится — личное прерывание
- общая IM2-таблица под счётчиком ссылок (_irq_table.c): кадровый и
CTC-хендлеры независимы, последний unref возвращает I/IM 1;
atexit-уборка глушит CTC обязательно (иначе кГц-прерывания душат
шелл после выхода)
- порты/слова по docs/samples: CH0=0x10/CH2=0x12/CH3=0x13,
0x57/0xD7/вектор в CH0, стоп 0x03
- irqtest: CTC-vsync параллельно с кадровым + произвольная частота;
MAME: frame 48 Гц, ctc(vsync) 49 Гц (parallel frame жив),
ctc(50x50) 350 Гц точно по формуле, remove/выход чистые
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Первая версия висла на первом прерывании. Две причины (verified по
docs/samples/sprinterIntLib.asm, SIO_CTC_KEY.asm и исходникам MAME):
- DSS работает в IM 1 (обработчик 0x0038); I=0x3F — наследие Spectrum
ROM, НЕ таблица: чтение [I<<8|0xFF] давало мусор (0x00BF) и прыжок в
никуда. Чейн из трамплина теперь ВСЕГДА jp 0x0038 (interrupted-PC на
стеке = имитация RST 38); irq_remove безусловно восстанавливает IM 1
- CBL-фильтр по биту 7 порта 0xFE убран: при выключенном CBL бит
подтянут к 1 (MAME kbd_fe_r: data |= 0xE0) — каждый кадр ложно
уходил в чейн, user-handler не вызывался бы. Вернуть в Phase 2
вместе с поддержкой CBL
Попутно подтверждено: порт 0x19 = SIO-A RR0 (Z84C015), бит 0 = Rx
Available; вектора встроенной периферии SIO 0x10..0x1E / CTC 0x06
(заливка 257×H ловит любые); внешний вектор 0xFF.
irqtest: диагностика I до установки + фаза без ESTEX; прогон в MAME:
~49 Гц, клавиатура жива, remove останавливает тики, чистый выход.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Проще исходного плана (docs/im2_isr_design.md обновлён): отдельный
--memory im2 не понадобился.
- <irq.h>: irq_install(handler) — вызов ~50 Гц только на КАДРОВЫХ
прерываниях (клавиатура bit0:0x19 и CBL bit7:0xFE отфильтровываются);
штатный обработчик DSS чейнится ВСЕГДА (SMC-jp, адрес из старой
IM2-таблицы по регистру I) — клавиатура/SYSTIME/мышь живут
- вектор-таблица: 513 Б BSS + runtime-выравнивание; Sprinter шлёт
только вектор 0xFF, поэтому jp-заглушка лежит внутри самой таблицы
по смещению H — без linker-областей и правок crt0
- трамплин: полный сейв обоих наборов+IX/IY вокруг user-handler'а
(ex af,af' как .db 0x08 — апостроф ломает препроцессор SDCC)
- tiny/big: работает (код в W2); small/huge: EINVAL по проверке
адресов; irq_remove идемпотентен и висит на atexit (выход без
снятия = I в памяти умершего процесса = крах шелла); old_I==0 → IM1
- tests/irqtest: тики за 3 с против time() (~50 Гц), живая клавиатура
под handler'ом, остановка после remove, чистый выход
- docs: im2_isr_design (статус+дельты), libc-reference (<irq.h>), TODO
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>