Реализовать двухпанельный Commander от платформенного PoC до этапов P6-P20: EMM-каталог, сортировку и выбор, операции с файлами и деревьями, транзакционное копирование, метаданные, политику конфликтов и предварительную проверку свободного места.
Добавить проектную документацию, HDD/MAME-сценарии и проверенные артефакты. Расширить libc операцией bank_write_page, исправлением режима O_RDONLY и связанными регрессионными проверками.
atlas значился 9193 Б против 9627 фактических, и расхождение держалось
через все правки этой сессии: замерял со СПРЯТАННЫМИ правками — те же
9627, то есть к ним оно отношения не имеет.
Похоже на механику самого инструмента: size_check.py читает .map С ДИСКА,
а не пересобирает. Программа, которую в тот раз не пересобрали, попадает
в эталон со СТАРЫМ числом — и дальше висит расхождением, пока её однажды
не соберут заново. Сегодня после make clean впервые за долгое время
пересобралось всё, и накопленный рост libc/libbgi стал видимым разом.
Записываю честное число. Полезный вывод на будущее: `make size-baseline`
имеет смысл только после полной пересборки тестов, иначе он фиксирует
смесь свежих и устаревших размеров.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
Два бага одного пути завершения, оба видны только на железе.
1. ЗВУК ПРОДОЛЖАЛСЯ ПОСЛЕ ВЫХОДА. cbl_close() закрывает СЕССИЮ, но не
гасит железо: bit7 порта 0x004E держит gfx_wait_vsync ради бита луча,
поэтому порт оставался включённым ("немой" режим), и CBL крутил свои 256
слотов уже под шеллом — тихо ровно до первой чужой записи в порт данных,
а дальше она зацикливалась. Новый cbl_shutdown() гасит bit7 независимо
от держателей и центрует ЦАП обычного COVOX; pop_shutdown зовёт его
последним действием, а _cbl_open_raw регистрирует в atexit его, а не
cbl_close.
2. ЦЕПОЧКА atexit НЕ ВЫПОЛНЯЛАСЬ ПРИ ВОЗВРАТЕ ИЗ main. crt0 уходил прямо
в ESTEX EXIT, то есть нарушал контракт C (возврат из main = exit(status)).
Молча терялись не только гашение звука и снятие vsync-ссылки, но и
_fclosall: буферизованная запись в файлы пропадала, если программа не
звала exit() явно. Теперь crt0 после main дёргает _atexit_hook.
Косвенность обязательна: прямая ссылка crt0 на разматыватель притащила бы
его и стек хендлеров в КАЖДУЮ программу. Указатель живёт в отдельном
data-модуле (два байта _DATA, ни байта кода), ставит его сам atexit() при
первой регистрации — нет регистраций, нет и кода. Тот же приём, что у
_irq_cbl_hook.
Цена замерена: +14 Б всем программам (блок в crt0) и +64 Б тем
одиннадцати, что реально регистрируют хендлеры (CBL, файловые через
_fclosall, irqtest, gfx_dbuf, solidt) — у них раньше эти хендлеры были
мёртвым кодом. Эталоны обновлены (кроме atlas: его +434 Б не отсюда,
замерен тот же и без этих правок).
Проверено в MAME: старт и звук как были, выход по F10 возвращает в шелл
чисто (текстовый режим восстановлен, зависания нет), повторный запуск
работает. Пункт 1 проверяется только на железе.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
cbl_close() не пишет в порт управления ноль: пока gfx_wait_vsync держит
ссылку ради бита луча (в PoP её берёт pop_pace_arm на всю программу),
bit7 остаётся включённым. А буфер CBL (256 слотов) при включённом bit7
крутится по кругу и не чистится ничем — железо бесконечно поёт хвост
последнего сэмпла, тоном и громкостью по последней мелодии. Пойман
пользователем на железе: Ctrl+S во время музыки и пропуск заставки
давали ноту до следующего cbl_open.
В MAME не воспроизводится: "немым" кодом частоты был reserved-код 2, у
которого divs[2]==0 и таймер не заводится вовсе. На железе reserved-коды
не определены, ЦАП тактируется — тишина держалась на свойстве эмулятора,
а не железа.
Теперь тишину даёт СОДЕРЖИМОЕ БУФЕРА:
- _cbl_port_sync() после каждой записи в порт управления зовёт
_cbl_prime(0x80). Это закрывает и паузу звука, и включение bit7 ради
луча на холодном старте (в буфере лежал мусор от прошлой программы), и
полное выключение — при bit7=0 те же 256 записей уходят прямо в ЦАП
обычного COVOX и центруют его, снимая щелчок;
- _CBL_VSYNC_FREQ переведён с reserved-кода 2 на документированный 8
(7,8125 кГц): поведение определено и на железе, и в MAME, прерывания
по-прежнему выключены (bit4=0), а бит 7 порта 0xFE трамплин смотрит
только при живом хуке насоса;
- cbl_close() зовёт sync внутри той же DI-скобки, где снимает хук, иначе
насос долил бы буфер уже после заливки.
Цена: +2 Б программам со звуком, +11 Б графическим (тянется _cbl_prime
следом за gfx_wait_vsync), 256 OUT'ов (~0,3 мс) на редкое событие —
эталоны cblstream/cbltest/cblwav/gfx_dbuf обновлены. Рост atlas в
size-check к этой правке отношения не имеет (замерен тот же и без неё).
Проверено: кодоген _cbl_port.asm; SprPoP пересобран и прогнан в MAME
(титры → пропуск заставки → уровень 1, пейсинг по лучу жив). Сам баг
проверяется только на железе.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
Корень двух багов SprPoP (KBD-STUCK-WAIT, частично GRAB-KBD-TIMING) нашёлся
в исходниках DSS (docs/sources/Estex-DSS): обработчик прерывания DSS живёт в
IM1 по 0x0038 и ПЕРВЫМ ДЕЛОМ делает `CALL KEYSCAN`, а тот вычерпывает FIFO
SIO досуха. Наш трамплин проверял «есть ли клавиатурный байт» один раз, на
входе в прерывание, а хвост кадрового пути уходил в DSS — значит скан-код,
прилетевший позже, доставался DSS и уезжал в его буфер. Rx-overrun при этом
НЕ взводится (байт не потерян железом, а прочитан не тем владельцем) — отсюда
и загадка исходного диагноза: бит залип при `_kbdraw_overrun == 0`.
Измерено в MAME (брейки + totalcycles): окно 738 тактов (~34 мкс) на каждом
кадровом прерывании, из них 481 такт — пролог самого DSS. Поэтому проверка
FIFO перед chain'ом снимает лишь треть и не годится (пробовали, кражи
продолжались); кадровый путь при открытом raw теперь заканчивается приватным
RETI, окно = 0. Цена: на это время у DSS замирает опрос мыши и мигание
текстового курсора — зафиксировано в <kbd_raw.h>.
Пойманный случай (старая сборка): DSS прочитал 0x74 (make стрелки «вправо»)
при _kbdraw_pending = EXT, то есть посылку E0 74 разорвало пополам между
двумя владельцами канала.
Проверка: брейк на входе KEYSCAN с условием «страница точно DSS + raw открыт»
до правки срабатывал мгновенно (50/с), после — молчит; положительный контроль
на нашем RETI срабатывает сразу.
Трамплин 300 -> 310 Б (буфер W2-копии поднят 336 -> 384, запас 74 Б);
размерный эталон обновлён: +10 Б у программ, линкующих IRQ.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Эта сессия (меню + текст в служебных полосах):
* Меню: рамка выделения считается от силуэта текста (SEL_PAD сверху и
снизу), а не «на глаз»; все экраны центрируются в игровых 200 строках
(UI_CENTER_TOP/UI_CENTER_FIELD) и в них помещаются; Enter и Space —
равноправные клавиши действия (ui_action_down).
* Settings: убраны SHOW SPRINTER SCREEN и BACK, добавлены DEBUG BAR и
ABOUT. About показывает тот же текст, что стартовый Sprinter screen,
минус строка про клавиши — общий about_text(), чтобы экраны не
разъехались. CONTROLS собирается таблицей и центрируется по
фактическому числу строк.
* GAME PAUSED переехала в нижнюю статус-строку, как в оригинале
(SDLPoP rect_bottom_text = {193,70,202,250}): это состояние программы,
а не пункт меню. POP_HP_Y вынесен в pop_cdraw.h — полосу делят два
модуля.
* pop_status.c (банк 9) — текст в обеих служебных полосах. Нижняя:
порт display_text_bottom + таймера (QUICKSAVE/QUICKLOAD/SOUND ON/OFF,
24 тика). Верхняя отладочная переведена с палочек на текст
«Level ##, Room ##, Speed: …, Sound: …, Immortal #» малым шрифтом, с
своим форматированием чисел (без printf и без деления).
Заявка сообщения — запись одного байта pop_status_msg: резидент W1/W2
не растёт, весь рендер в банке. Бюджет после правок не изменился
(_CODE 23981, куча 267 Б).
* Цена вывода: блит глифа ~4,6 тыс. тактов независимо от размера, поэтому
всё change-driven, отладочная строка перерисовывается ПО ПОЛЯМ, пробелы
не блитятся вовсе, а вход в комнату заливает только игровое поле
(pop_screen_fill_field) — борта от комнаты к комнате не меняются.
* Интро: в PV-сцене зазвучали пропавшие эффекты оригинала — закрытие
ворот (4) и открытие двери покоев (51), из которой входит Джафар.
* docs/status_line_text.md — полная инвентаризация ВСЕХ текстов SDLPoP в
статус-строке: геометрия, семантика text_time_total как идентификатора
сообщения, мигание, рестарт по истечении 36/288.
Вместе с этим выкладывается накопленная работа по оболочке полной игры:
автомат состояний (pop_app), title, intro/PV и cutscene, attract-demo,
Hall of Fame, глобальный таймер, настройки и POP.CFG, модуль палитр и
fade, звуковой набор, host-тесты на новые швы.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Спрайты персонажей хранятся column-major (ради бесплатного H-флипа), а
вертикальное зеркало у такой раскладки — разворот байтов ВНУТРИ колонки,
чего accel не умеет. Поэтому pop_vflip.c готовит зеркальные КОПИИ страниц
атласов: копия побайтово повторяет раскладку .atl (заголовок, каталог,
смещения лент), развёрнуты только пиксели — значит вызывающему достаточно
подменить номер страницы, которую он мапит в W0.
* Кэш ленивый (страница зеркалится при первом обращении в перевёрнутом
виде) и живёт до смены уровня — зелье переключает состояние туда-обратно,
перезеркаливать по 16 КБ на каждый переворот незачем. EMM хватает: 34
страницы из ~215 свободных.
* pop_cdraw/pop_kdraw: страница + позиция (top = 191 - obj_y для персонажа,
192 - top - h для клинка). «Пропустить skip строк сверху» у зеркальной
ленты = «срезать снизу» у оригинала, поэтому клипы считаются как обычно.
* Модуль резидентный: он маппит W3 (приёмник копии) — банку так нельзя.
* pop_vflip_reset на смене уровня: копии сделаны из старых страниц.
Проверено в MAME: чит U переворачивает всё разом, Кид и факелы вверх ногами,
полоса HP на месте, бег без следов на фоне (heal бьёт в те же координаты).
ОСТАЁТСЯ (L9-CLIPCHAR): clip_char в перевороте должен резать снизу, а не
сверху — пока при pop_upside не режем вовсе. Тени/брызг это не касается.
MEM-COLD3, шаг 1-2 из плана разгрузки резидента:
* pop_redraw.c целиком -> банк 7 (к pop_room.c: он и есть единственный
потребитель разбора пометок). Два модуля в одном банке линкуются в один
сегмент — проверено на .map (BANK7 7928 -> 8556).
* pop_level.c расколот по частоте вызова: горячая половина (чтение тайлов,
связи, дверные таблицы, живые стражи — зовут все банки, местами на каждый
тайл) осталась в резиденте, холодная (чтение файла уровня, разбор комнаты,
рестарт, потабличные различия) уехала в pop_level_cold.c -> банк 8.
Общее состояние объявлено в _pop_level.h: данные банковых модулей всё
равно линкуются в общий _DATA, в банк уехал только КОД.
Побочно найден баг кодогена SDCC: `return 1;` из ветки не кладёт 1 в A —
до __banked он маскировался случайным ненулевым остатком в A. Репро и
разбор — docs/bugs/sdcc-z80-ret-const-lost/, стаб t_char починен одним
выходом через переменную. Аудит всех .asm roomtest: других мест нет.
_CODE 25628 -> 23005, куча 902 -> 3525 Б. tests-host 6/6.
Первый шаг 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>
Три правки едут вместе намеренно: n_banks и --bank обязаны меняться
атомарно, иначе промежуточный коммит — зависание (см. ниже).
MEM-BANK5. CODE и DATA делят одно 32-КБ пространство W1+W2, поэтому
килобайт кода, уехавший в банк, — это килобайт, доступный данным.
Кандидат выбран не по размеру, а по частоте вызова: диспетчер управления
дёргается раз в кадр на персонажа и горячих банк→банк переходов не
создаёт (в отличие от pop_level, чей pop_level_tile зовётся из банка 2 на
КАЖДЫЙ тайл).
_CODE 26 780 -> 24 662 Б (−2 118)
куча 180 -> 2 298 Б
банк 5 2 211 / 16 384 (13.5 %)
Шина control_* (8 глобалов) переехала в pop_state.c. Сегодня она уцелела
бы и в pop_ctrl.c — банки собираются без --bank-data, их писучие данные
остаются в общем _DATA, — но это флаг сборки, а не свойство кода, а шину
трогают уже три банка: 5 пишет с клавиатуры, 1 подаёт синтетический ввод
ИИ (autocontrol_*, seg002), 3 читает через pop_ctrl_shift_held.
Заодно pop_ctrl.c наконец включает собственный заголовок — раньше
объявления жили прямо в нём.
ГРАБЛИ, на которые наступили (стоили дольше самой задачи): n_banks в
roomtest.c захардкожен, и его надо править вместе с числом --bank. С
n_banks=4 и пятым банком crt0 выделил четыре страницы, _bank_pages[5]
остался нулём, и первый же вызов pop_ctrl_init() через трамплин
отобразил в W3 страницу 0 и прыгнул на 0xC874 в мусор — исполнение
забрело в дисковый код DSS и осталось крутить чтение секторов. Симптом:
загрузка ресурсов проходит целиком (open=45 — все атласы), комната и Кид
успевают нарисоваться из enter_room, а HP и номер комнаты уже нет, и
kid_tick не вызывается ни разу. Ровно предупреждение из шапки
runtime/bank.s. Сверку n_banks с реальным максимальным индексом банка
записал в docs/TODO.md (Auto-banking) — это должно быть ошибкой сборки.
DBG-CHEATS: [ (0x54) и ] (0x5B) двигают Кида на пиксель (seg000:1828),
по фронту нажатия, под pop_cheats. Нужны потому, что мост MAME теряет
нажатия при быстрой отправке и подогнать Кида в позу скриптом нельзя —
на это упёрлись BUG-LOOSE-2 и BUG-GATE-PASS-1.
ROOMNAV больше не зовёт pop_trob_reset: reset обнуляет room_seen, то есть
чит ОТМАТЫВАЛ МИР (открытые/закрытые ворота, нажатые кнопки). Навигация
обязана только телепортировать.
Проверено в MAME: старт уровня 1 рисуется полностью (комната, Кид, HP,
номер), бег вправо и падение на второй ряд отрабатывают, ] даёт x+1 и
[ даёт x−1 по одному нажатию, Shift+→ — осторожный шаг (x 131 -> 142,
колонка 4 -> 5) и при удержании 90 кадров не срывается в бег (x 142 ->
151), то есть pop_ctrl_shift_held работает через границу банк 3 -> банк 5.
Наборы под ucsim: geom 39, grab 53, phys 1723 — все зелёные.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`make all` собирал и examples/, из-за чего цикл «правка libc -> проверка»
упирался в mdview (компилируется минутами) и ничего нового про libc не
показывал. Регресс ловит разжирение библиотеки, а для этого хватает
мелких tests/ — каждый тянет свой кусок libc и пересобирается за секунды.
- `make all` = tools lib tests; examples собираются явно (`make examples`,
и как зависимость `make floppy`);
- size_check.py смотрит только tests/*.
Эталон принят заново. Разбор расхождения, чтобы оно не выглядело
необъяснённым: эталон стоял с 30 июля (0280b05), а libc менялась 1 и 3
августа (b56f2b4 kbd_raw_poll, 1f16e8f fake shift) — _irq_tramp вырос
267 -> 336 Б и не был перебазирован, отсюда +33 Б у всех, кто линкует
трамплин (cbl*, irqtest, rt_test), и +22 у gfx_dbuf (gfx_set_idle_hook в
libbgi из того же KBD-1). Текущий фикс BUG-KBD-5 вернул 36 Б из этих 69.
Заодно выкинуты мёртвые строки эталона (rpgprof/rpgwalk/scroll/space —
их давно нет в APPS, mdview/mdview2 — теперь вне регресса).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обвязка для быстрых тестов plain-C логики: секунды вместо прогона в MAME,
без образа диска. ucsim_z80 идёт в комплекте нашего SDCC — новых
зависимостей нет.
ПОЧЕМУ ПОД Z80, А НЕ ХОСТОВЫМ GCC. У SDCC z80 int 16 бит, у хоста 32, и
расходится это НЕ в объявлениях, а в выражениях: integer promotion
повышает операнды до int независимо от того, объявлены они как uint8_t
или uint16_t. Перевод кода на фиксированные типы разницу не убирает —
убирает только исполнение с z80-семантикой. Побочно проверяется
кодогенерация SDCC и модули с inline-asm, которых хостовая сборка не
видит в принципе.
Устройство: crt0_ucsim.s (SP, зануление, main, halt), tcheck.* (итог в
структуру в ОЗУ), run_ucsim.py (гоняет ucsim, дампит tc_result, печатает
отчёт), host-tests.mk (общие правила). Вывода через printf нет: тестовый
бинарь линкуется без Sprinter-libc. Через ucsim-simif не идём — номера
его команд плавают между версиями, halt + dump работают везде.
Наборы лежат РЯДОМ с проверяемым кодом, обвязка общая:
testkit/t_selftest.c — самопроверка (sizeof(int)==2)
applications/PoP/roomtest/tests-host/ — движок PoP
Первый содержательный набор — t_geom: сверяет рукописный asm-LCG из
pop_geom.c с наивной 32-битной формулой на 128 шагах. Заявка «бит-в-бит
как в SDLPoP» до сих пор держалась на комментарии. Тест проверен
мутацией: порча эталонной константы даёт красный.
Планы дальнейшего покрытия:
docs/host-tests-plan.md — libc и libbgi (не начато)
applications/PoP/docs/host_tests_plan.md — движок PoP
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ручная проверка пользователем: стало значительно лучше, но редкие пропуски
стрелок при зажатом Shift всё же ощущаются. Счётчики на 35 нажатиях подряд
потерь не показали, то есть остаточная частота заметно ниже прежних ~15 %.
Задача отложена до финальной полировки программы (решение пользователя) —
для работы клавиатура пригодна.
Записано, где именно осталась дыра, чтобы не начинать с нуля: idle-хук
покрывает простой (~2/3 кадра), а в занятой трети DI-окно одного
accel-прохода доходит до ~650 мкс при допуске FIFO ~300 мкс — пачка байт,
целиком попавшая в такое окно, ещё может потерять байт. Порядок действий
на возврат: вызовы между блитами занятой фазы, замер тем же счётным методом
от 50 нажатий, и только потом рычаги вне нашего кода (Scan Code Set 3 через
BIOS $EA — в MAME непроверяемо; общий m_irq_off_timer в драйвере).
Заодно сняты оговорки «плотный опрос ещё не подтверждён замером» в
kbd_raw.h и libc-reference.md — теперь там штатный рецепт через
gfx_set_idle_hook.
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>
Разгрузка W1/W2 под будущий ИИ стражей: _kbdraw_down был БАЙТОМ на
скан-код (512 Б в _DATA при 32-килобайтной раскладке). Теперь бит на
код: код>>3 = байт, код&7 = бит, расширенные (префикс 0xE0) — смещение
+32 байта вместо +256.
Трамплин прерывания строит маску СДВИГОМ, а не таблицей: таблица
потребовала бы `ld hl,#метка` внутри трамплина, а он копируется в W2
побайтно и обязан быть без абсолютных само-ссылок (см. его шапку).
Маска строится в BC, поэтому в клавиатурной ветке добавлен push/pop bc.
Трамплин вырос 244 -> 267 Б, буфер копии поднят 320 -> 336 (запас 69 Б).
Проверено в MAME на roomtest, все три класса клавиш:
- обычные: '=' (обход комнат) и 'K' (чит-убийство стража — читал
guardhp_curr/delta: 3/0 -> 0/-3);
- расширенные (E0): стрелка вправо — Kid добежал до края комнаты;
- модификаторы: удержание Shift ставит бит 2 байта 2 карты
(скан-код 0x12), отпускание снимает.
Скорость: кадр 334 716 -> 338 508 тактов (+1.1 %) на битовой арифметике
в kbd_raw_down (~15 вызовов за кадр); при бюджете 430 000 это 0.79
периода вместо 0.78 — регрессии нет.
Итог по roomtest: данные 4422 -> 4022 Б, куча W2 996 -> 1294 Б.
Эталон размеров принят заново (make size-baseline): _CODE десяти
программ вырос на 14-23 Б — это код битовой арифметики в трамплине и
kbd_raw_down, обмен на -448 Б данных, которые size_check не считает.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- docs/new/ — сводные справочники (архитектура, BIOS, DSS, память,
графика, акселератор, IRQ, порты, ввод, звук, известные баги);
- docs/Original/ — первоисточники, из которых они собраны (BIOS, Estex
DSS, мануалы, описание акселератора), + Форум.doc/.docx в reference;
- libbgi/common/gfx_scroll_h.c — обход бага скролла при ширине >256
(правка автора: шаг банды 255 и продвижение указателей на cw; старый
вариант с 256 оставлен закомментированным с TODO);
- удалён applications/PoP/roomtest/hang_variants.png — рабочая раскладка
из разбора позы виса, в репозитории ей не место.
Большие архивы (docs/extra ~568 МБ, docs/sources с вложенными git-репо
~68 МБ) в коммит НЕ включены — см. обсуждение.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- kbd_raw_sync: цикл вычерпывания SIO FIFO (не 1 байт/прерывание) —
фикс залипания клавиш; overrun-wipe сбрасывает только пострадавшие
клавиши, не модификаторы (typematic их не перечитывает).
- Гайд docs/kbd-games.md; заметка о Rx-overrun в docs/TODO.md; справочник
скан-кодов в docs/libc-reference.md.
Co-Authored-By: Claude Opus 4.8 <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>
Клавиши 1/2/3 зовут gfx_set_fps_div(n) на лету (дефолт n=1 не меняет
поведение). Проверено в MAME: при n=2 FPS-метр стоит РОВНО на 024
(48.83/2) все 8 секунд без плавания — логический кадр = ровно 2 vsync'а,
скорость персонажей (пиксель/сек) постоянна. +808 Б (демо тянет код
делителя).
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>
wpiset на порт W1 (0xA2) + iff1 в dev-MAME: DSS перемапливает W1 при
ВКЛЮЧЁННЫХ прерываниях во время системных вызовов (файловые, загрузка
exe, видео; страницы 0xFE/FF/F3/0x50, PC ядра 0x15xx-0x2Exx) —
ограничение irq_install «только tiny/big» обосновано, handler в
W1-коде небезопасен принципиально. §9е дополнен фактом; в TODO —
дизайн цепочки irq-обработчиков (массив слотов в W2, chain_add/remove,
irq_install как обёртка; реализация по потребности).
Co-Authored-By: Claude Fable 5 <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>
Плашка слева-сверху (значение раз в секунду, рисуется на обеих
страницах — fps_draw=2, как в examples/space). ~39 fps на 8 ходящих
персонажах (кадр на грани 20 мс — частично двухvsync'овые кадры).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Демо на реальном арте (third_party/16x16-RPG-characters, bard):
conv_sprites.py режет PNG 192×128 (8 персонажей = блоки 3×4 кадров:
ряды вниз/влево/вправо/вверх × кадры маятника 0/1/2) в 8 вертикальных
лент по 12 кадров и строит bard.pal (слоты 0-15 EGA + цвета PNG с 16,
прозрачность → 0xFF). 8×12 кадров не лезут в одну EMM-страницу —
ДВА атласа по 4 персонажа (движок сам переключает страницы W0).
Палитра из файла: gfx_pal_fload + gfx_pal_sync. Смена направления =
sprite_anim(dir*3, dir*3+2, PINGPONG). Правила хождения (двухфазная
машина на sprite_moveto): до края → разворот 180° → случайная точка
(не меньше четверти экрана) → поворот ±90° → снова до края.
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>
Всё сразу: .atl с диска (mkatlas.py из генерённых лент) → atlas_load в
EMM-страницу (W0, ISR-стаб) → движок sprite_t.page → авто-анимации:
5 астероидов (ANIM_LOOP вращение + sprite_moveto к случайным целям
разных скоростей; по прибытии — взрыв ANIM_ONCE в точке + новая цель,
по SPR_ANIM_DONE взрыв прячется), маяк ANIM_PINGPONG; дабл-буфер,
FPS-метр, ESC. ~48 fps (vsync-кап).
Грабли по дороге (оба — прикладные, не библиотека):
- фон рисовался rand()'ом с разными последовательностями на страницах
→ мерцание звёзд; фикс — фиксированный seed на draw_space;
- НЕ была скопирована палитра страницы 0 → 1 (у каждой страницы своя,
см. gfx.h) — страница 1 показывалась чёрной, флип мигал
«сцена/чёрный»; fps-плашка теперь рисуется на ОБЕИХ страницах
(fps_draw=2 кадра при смене секунды).
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>
Решение: in-memory формат остаётся (getimage + sx/sy, ленты/сетки).
Рекомендация: кадры одного спрайта — одного размера с полной сеткой;
для спрайтов разных размеров — контейнерный формат .atl (каталог +
независимые getimage-ленты, паддинг невозможен по построению).
Формат — предложение, реализация по потребности первого приложения.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Диспетчеризация clip/noclip через указатели _gfx_blit_fn/_gfx_heal_fn
(common/_gfx_sprite_fns.c, дефолт clip): gfx_sprite_clip() — теперь
модуль, переключает указатели один раз; sprite_update/putsprite/
movesprite зовут через указатель — ветка if(clip) из горячего цикла
убрана (съедала половину выигрыша noclip). Программа без вызова
gfx_sprite_clip() noclip-ядра не линкует.
Замер dev-MAME (16 шаров, uncapped): clip 50 → noclip 60-61 fps
(+20-22%). Регресс tests/sprites (A/B PASS), size-check OK
(balls −237 Б, sprites −3177 Б — отвязались лишние ядра).
Фикс CPU-байта write-триггера (preread + EX AF,AF') снят: точная
dev-MAME эмулирует ПЛМ, подавляющую CPU-байт при активном burst'е —
подтверждено по байтам VRAM (tests/blitw col0 = GREEN через
read_vram MCP-моста). Для heal фикс был избыточен всегда (банк 0x50
перезаписывает dst[0]). Строки фикса оставлены закомментированными
в трёх leaf'ах на случай отличий реального железа; шапки и §9а/§9б
дизайна обновлены. НА ЖЕЛЕЗЕ ПЕРЕПРОВЕРИТЬ.
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>
Симметрично libbgi (bgi256/bgi256_safe): fast = -DLIBC_NOCHECK,
дефолт sprinter-cc; safe линкуется по --safe (флаг уже существовал).
Под LIBC_NOCHECK вырезаны ТОЛЬКО параметр-валидации:
- fgetc/fputc: NULL-check в горячей asm-обёртке (~11Т на каждый байт);
- fgets/fputs/fread/fwrite: NULL ptr/fp и EBADF на неверное направление
потока; ftell/fseek/ungetc/fclose: NULL fp; cputs: NULL s.
НЕ тронуты: критичный _fd_guard (9-й OPEN вешает DSS — в обеих
версиях), функциональная маршрутизация (консоль/направление/hold),
cold-path валидации (fopen/cbl_open/irq/settextmode — экономии ноль).
libc/Makefile — dual-build по образцу libbgi (build/fast + build/safe,
общий стейл-контроль). Корневой Makefile: в TESTS добавлены bgitest,
bgi_img, accfill, cblstream — раньше не собирались корневым make и
выпадали из size-check при чистой пересборке.
Дельты fast vs старая (safe-семантика): filetest -481, fbench -384,
solidt -180, errno -71, остальные -3..-9; роста нет. Проверено в MAME:
filetest fast и safe (--safe, sprinter_safe.lib) — прогоны идентичны.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Inline (чистый C99 `inline` без static, паттерн из libbgi/5de2f06):
textattr, get_text_attr, get/set_putch_raw_mode (conio.h; g_text_attr
и pc_raw_mode объявлены публично), isatty (unistd.h — сворачивается в
константу при константном fd). Модули удалены (5 шт).
«Толстые» кандидаты НЕ инлайнены — критерий проверен замером на 47
программах: feof/ferror/clearerr (тело ~12-15 байт с NULL-проверкой,
filetest +56 Б при инлайне) и textcolor/textbackground/set_text_attr
(RMW-маски, conio2 +13 Б) остаются модулями — при 2+ сайтах вызова
инлайн крупнее call+общее тело. Правило: инлайнить только тела
<= ~6 байт на сайте или сворачиваемые константами.
Дельты: solidt -94, hello -24, mouse -7, bios_text -7; conio2 +11
(textattr×5 — паритет, принято за скорость). z80.lib SDCC не содержит
feof/isatty — маскировки удалённых модулей нет, провал инлайна = ошибка
линковки. Проверено в MAME: conio2 (атрибутная матрица), filetest
(полный прогон). В size_baseline также вошли gfx_dbuf +608/gfx_demo
+25 — это первый чистый релинк run-рендера текста (ba09c0b), не inline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
setcolor/getcolor/setbkcolor/getbkcolor/getmaxx/getmaxy/getmaxcolor/
getx/gety/moveto/moverel/setfillstyle/graphresult (graphics.h) и
gfx_get_bank/gfx_set_bank/gfx_get_draw_page/gfx_get_visible_page
(gfx.h) определены inline в публичных заголовках; state-переменные
объявлены там же (хранилище прежнее — _bgi_state.c/_gfx_state.c).
Именно `inline` БЕЗ static: проверено артефактами (.asm) — SDCC 4.5
инлайнит вызов при всех наших флагах (--opt-code-size/--opt-code-speed/
--max-allocs) и не эмитит standalone-тело; `static inline` эмитил бы
мёртвую копию каждого аксессора в КАЖДЫЙ включивший модуль. Отказ
инлайнить = громкая ошибка линковки (все 47 программ слинковались).
Экономия ~30-40Т на вызов, минус 17 .rel; по _CODE размер-нейтрально
(сайт вызова ≈ телу). size_baseline: bgitest +267/accfill +528 — это
НЕ inline, а run-рендер текста из ba09c0b (draw_scaled потянул
vspan_raw+vfill256 и сам вырос) — цена за ~2-6× скорость текста.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
При узкой стороне <= 8 линий chunked-rectfill проигрывает циклу
_bgi_hspan_raw/_bgi_vspan_raw (подготовка ~340Т впустую, break-even
n~9): либо fast-path в диспетчере, либо рецепт для пользователя —
решать по профилю.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs/accel-fill-budget.md: полный разбор v3 (подготовка ~340Т на
прямоугольник, 46Т на чанк 16 линий, 53Т/51Т на линию, ~107 байт/leaf)
против per-line di/ei вариантов (djnz 72Т/линию ~63 байта; 16-бит IX
206Т/линию); break-even n≈9 линий, тотализатор, решение остаться на v3.
Краткие выжимки — в шапки _gfx_rect*fill256.
Идеи на будущее (там же): ограничить контракт стороной <=256 (широкие
прямоугольники пользователь выводит в два приёма); реализовать оба
варианта (поблочный/построчный) с переключением опцией сборки в духе
GFX_NOCHECK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>