60 Commits

Author SHA1 Message Date
snark13 5ef084eca4 docs: loose_floors_plan — конкретика SDLPoP + подход интеграции
Дополнен точными данными из SDLPoP (сверено): кадровые таблицы
loose_fram_left/right/bottom, get_loose_frame, y_loose_land, delay=11;
триггеры с call-sites (check_press->make_loose_fall при стоянии на 11,
frame79-сверху; do_knock на жёстком приземлении; animate каждый кадр);
tile_is_floor(11)=1.  Плюс подход интеграции в наш движок: общая
мутабельная копия комнаты pop_map<->pop_bg + per-page запекание пустоты
после падения; тонкое место — перерисовка динамического тайла в дабл-буфере.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 21:43:43 +03:00
snark13 36a5a5e194 roomtest: фикс провала сквозь пол при беге с кромки (порт start_fall/in_wall)
Тап-бег с кромки [1,3] вправо -> Kid проваливался в стену col2/3 ->
респавн, вместо посадки на [2,4].  Сверено с SDLPoP seg006:

- start_fall: frame 9 -> seq_7_fall (был ошибочно seq_19 как у 13);
  frame 13 -> seq_19 (лишний dx(1)).  + хвост seg006:1099: тайл ПЕРЕД
  персонажем — стена -> Char.x = char_dx_forward(-1);
- in_wall переписан аутентично (seg006:1292): выталкивает ВПЕРЁД из стены
  (delta+4) на соседний тайл, а не назад вглубь (наш старый через
  dist_from_wall_forward давал отрицательный сдвиг -> Kid уходил в стену
  col2 -> проваливался).  distance_to_edge_weight + 6-delta/+4.

Проверено в MAME (пользователь): бег с кромки [1,3] -> посадка на [2,4].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 21:23:09 +03:00
snark13 237f780e0e roomtest: fore-окклюзия пола/стены над Kid (порт redraw_at_char/char2)
Kid — высокий спрайт: голова/руки торчат в ряд выше опорного, floor/wall
там должны перекрывать его.  Сверено с SDLPoP (seg003 redraw_at_char2 +
seg008 draw_tile_fore/draw_other_overlay/draw_floor_overlay):

- fore_tile (draw_tile_fore): стена рисует и WALL_FRAM_BOTTOM (нижняя
  грань-решётка), не только MAIN — руки при прыжке в потолок [2,7]->[1,7]
  уходят за стену;
- диапазон рядов форсит включение ряда над опорным (y_to_row верха спрайта
  мог схлопнуться из-за +60-сдвига);
- other_overlay_tile (draw_other_overlay): на КРОМКЕ пола (сосед слева пуст)
  перерисовать весь тайл поверх Kid — но ТОЛЬКО в позах захвата (78-79),
  виса (action 2/6), полёта (3/4), старта падения (bumped 102-106) и начала
  подъёма (135/136, до SEQ_UP на 141 Kid ещё в ряду ПОД полом).  Гейтинг
  как в redraw_at_char2 — при стоянии/беге ложной окклюзии нет;
- отладочный стоп-кадр: '1' заморозить / '2' продолжить (для разбора поз).

Проверено в MAME (пользователь): прыжок-в-потолок, короткий прыжок,
спрыгивание, подъём на [0,3] — окклюзия корректна.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 20:41:55 +03:00
snark13 023b45eb85 roomtest: двойная буферизация (два экрана + флип), тумблер SPACE
Убирает мерцание/тиринг при перерисовке слоёв (Kid/fore/пол-оверлей):
рендер всего кадра в скрытую страницу, tear-free флип на vblank.
Инфра libbgi (gfx_set_draw_page/visible_page/wait_vsync) уже была.

- фон комнаты рисуется в ОБЕ графические страницы (у каждой своя
  ОЗУ-копия — источник heal); палитра 0->1 уже синкалась gfx_pal_sync;
- kid_heal/kid_draw: прямоугольник Kid запоминается ПО СТРАНИЦЕ
  (kid_l*[2]) — при чередовании страниц heal стирает пиксели своей
  страницы (прошлый Kid там был 2 логических кадра назад);
- цикл: draw в back, 3x wait_vsync (пейсинг), gfx_set_visible_page(back);
- SPACE (edge) — тумблер: off = однобуфер (draw==visible==0) для отладки.

Мерцание при подъёме подтверждено устранённым в MAME (пользователь).
План: applications/PoP/docs/double_buffer_plan.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 16:07:03 +03:00
snark13 fce3e58830 applications/PoP/docs: план порта loose floors (проваливающиеся полы)
Разбор SDLPoP (seg007 loose/trob/mob, seg008 draw_loose): хранение
состояния (tile 11 + curr_room_modif: 0 покой / 0x80.. тряска / 1..11
отсчёт падения), триггеры тряски (do_knock на приземлении → shake ряда)
и падения (make_loose_fall при стойке/зацепе на loose), падающий кусок
(mob → debris снизу, empty сверху), отрисовка по статусу (loose_fram_*
через get_loose_frame).  Порядок реализации L1..L5 + что нужно в нашем
движке (динамический тайловый слой: room_modif[], trob-очередь, mob).

ПЛАН — не реализация.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 12:31:23 +03:00
snark13 3c0baacbf6 libc/kbd: recovery по Rx-overrun SIO (залипание клавиш) + kbd_raw_sync
Симптом (интермиттентный): при отпускании shift+стрелка иногда стрелка
залипает.  Диагностика: на чистом одновременном release break-коды
обрабатываются верно (проверено MCP) → drain-логика ISR корректна.
Остаточное залипание = переполнение 3-байтного аппаратного FIFO SIO при
пачке скан-кодов (F0 12 E0 F0 74 = 5 байт) во время длинных DI-окон →
потерян break → залипание.

Фикс: трамплин после drain читает RR1 SIO (бит5 = Rx Overrun), при
overrun делает Error Reset (WR0=0x30) и взводит _kbdraw_overrun.
Новый kbd_raw_sync() (звать раз в кадр) по флагу сбрасывает всё
held-состояние _kbdraw_down (какой break потерян — неизвестно; реально
зажатые перечитаются).  pop_ctrl_tick зовёт kbd_raw_sync().  Буфер W2-
трамплина 288→320 (трамплин 244 Б).

ВНИМАНИЕ: путь overrun НЕ проверен детерминированно (баг интермиттентный,
зависит от тайминга DI) — ТРЕБУЕТ ПРОВЕРКИ на железе/в длинной сессии.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 12:26:42 +03:00
snark13 cbae48dbc4 applications/PoP/roomtest: пол-оверлей при подъёме (draw_floor_overlay)
Проблема из динамики: при подъёме [1,2]→[0,3] нижняя часть Kid, которая
физически за полом назначения, просвечивала.

Порт SDLPoP draw_floor_overlay (seg008:1457): на кадрах подъёма 137..144,
если тайл-назначения floor-подобный (floor/pillar/stuck/torch) И тайл СЛЕВА
пуст (кромка), передний край пола (floor_left_overlay[frame-137] =
{32,151,151,150,150,151,32,32}) + низ пола рисуются ПОВЕРХ нижней части Kid.

- pop_bg: climb_overlay_tile + доп-проход в pop_fore_over_kid (новый параметр
  frame) на кадрах 137..144.
- pop_pack_bg.py: CLIMB_OVERLAY_ENV_IDS={32,150,151} явно добавлены в env-фон
  (не попадают в used render_room — рантайм-анимация); env4 count 16→24.
- roomtest: передача Kid.frame в pop_fore_over_kid.

Проверено в MAME (кадр 137): при подтягивании видна только голова/плечи Kid
над кромкой [0,3], нижняя часть скрыта за полом.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 11:37:56 +03:00
snark13 419d5e4eff applications/PoP/roomtest: фикс ухода Kid в стену при прыжке с кромки
Баг (из динамики): стоя на кромке [1,3]/[1,4] лицом вправо + Up, Kid
телепортировался/застревал в стене col8.

КОРЕНЬ (трасса MCP): jumpup(seq_14) на кромке → приземление над ямой →
падение; на кадрах падения action=3 (midair) check_bumped был НЕ заглушён
(guard покрывал только freefall/hang/climb-кадры).  Kid дрейфовал к стене,
get_tile вне рядов давал WALL, dist_from_wall_forward/x_bump[] — мусор с
большим отрицательным сдвигом → Kid.x=char_dx_forward((int8_t)d) → underflow
uint8 X (0→255) → долёт до col8 и застревание в стене.

Фикс check_bumped: заглушить и в action MIDAIR (кадры падения), и при
curr_row вне [0..2] (падение мимо пола — тайлы вне комнаты = WALL, bump-мусор;
fell_out ловит do_fall).  Проверено в MAME: на кромке Kid больше НЕ уходит
в стену — чисто падает (в тесте сброс на старт).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 11:20:48 +03:00
snark13 5935724897 applications/PoP/roomtest: стены в fore-проходе поверх Kid
Проблема из динамики: за стенами Kid не прятался (стена [2,9] должна
перекрывать).  В PoP основная грань стены (wall_fram_main) добавляется в
FORETABLE (seg008:712) — перекрывает персонажа.  У нас fore-проход
рисовал только fore_id, а у стены (0x14) fore_id=0.

fore_tile: для стены (code==20) рисуем WALL_FRAM_MAIN + wall_pattern(,,1)
поверх Kid (как draw_tile_fore), а не только fore_id.  Проверено в MAME:
Kid прячется за правой стеной col9.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 21:47:10 +03:00
snark13 78f2aaec1a applications/PoP/roomtest: fore-слой поверх Kid (передние тайлы)
Порт SDLPoP seg003 redraw_at_char + seg008 set_char_collision: после
kid_draw передний слой (fore_id = foretable-кусок) тайлов ФУТПРИНТА
спрайта Kid перерисовывается ПОВЕРХ него — то, что по изометрии перед
персонажем (передние грани колонн/ворот/большой колонны/щебня).  Стены и
факелы имеют fore_id=0 → остаются сзади (в статическом фоне).

- pop_bg: pop_fore_over_kid(obj_x,obj_y,w,h,dir) — считает футпринт
  (char_x_left/right, col_from_x, y_to_row_mod4) рядов top..bottom ×
  колонок left..right (≤2×2=4 тайла) и рисует fore_id каждого в
  GFX_BANK_SPRITE (видео-ОЗУ; kid_heal восстановит из теневого фона,
  fore в нём запечён pop_room_draw).
- pop_kid: kid_fp_obj_x/y/width/height — метрики последнего кадра
  (obj_x ЛОГИЧЕСКАЯ, до ×8/7) для футпринта.
- roomtest: вызов pop_fore_over_kid после kid_draw.

Проверено в MAME: Kid, идя влево мимо колонны под навесом (row1 col3),
корректно уходит ЗА её переднюю грань (скрывается) и выходит с другой
стороны; задние колонны/факелы остаются сзади.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 21:34:32 +03:00
snark13 ea57a03543 applications/PoP/roomtest: K4 — зацеп/вис/подтягивание/спуск
Порт SDLPoP seg004/005/006 «hang state»:

- pop_map: check_grab (зацеп за уступ в падении по Shift → seq_15 → вис),
  can_grab/can_grab_front_above + tile-запросы над/за персонажем;
  pop_jump_up_seq (check_jump_up: ↑ в стойке = чистый прыжок seq_28/14
  ЛИБО прыжок-с-зацепом seq_8/24/16 за уступ выше → запрыгнуть на этаж);
  pop_hang_* (climb_up seq_10/73, hang_fall seq_23/11, hang-у-стены);
  pop_down_action (спуск seq_68 у края лицом от края / отступ / присед).
- pop_ctrl: control_hanging/can_climb_up/hang_fall, control_jumpup,
  jump_up через pop_jump_up_seq, down_pressed через pop_down_action;
  pop_ctrl_shift_held() для check_grab.

Ключевой фикс check_bumped (seg004 guard'ы): не бампить при action
hang_climb/hang_straight, на кадрах подъёма/спуска 135..148 И на кадрах
виса 87..99.  Без последнего спуск (seq_68) на кадре frame_91 (action
ещё midair, act(hang_climb) идёт следующим опкодом) ловил отскок у стены
и рвал цепочку hang→hang_fall→seq_11, приземляя не туда.

Проверено в MAME (HDD-тест, покадровая трасса через мост): прыжок-с-
зацепом [row2 col4]→вис→подтягивание→[row1 col3]; спуск [row1 col3]→
[row2 col4] с корректной позицией у стены (x=122, совпадает с SDLPoP).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 21:07:20 +03:00
snark13 cd8d566d82 applications/PoP: порт Prince of Persia — PoC (roomtest) + пайплайн
Порт PoP на Sprinter.  Текущий PoC — applications/PoP/roomtest/:
комната 1 (фон-композиция тайлов) + Kid с управлением на raw-клавиатуре
и коллизией с картой.

- roomtest — pop_bg (фон), pop_kid (спрайты Kid, column-major флип,
  seqtbl-анимация), pop_ctrl (порт control() PoP на held-state
  kbd_raw), pop_map (коллизия seg004/005: бег/стоп у стены,
  падение/приземление, отскок seq_47, вертикальный прыжок K4.1).
  MEMORY=small (DATA сразу за CODE, ~23КБ кода не лезет в huge).
- toolchain (PoP) — pop_pack_kid/pop_pack_bg/render_room/extract —
  распаковка res-графики MSDOS в атласы + композиция комнат.
- toolchain/ (корень) — make_hdd.sh (быстрый HDD-тест вместо FDD),
  png_strip.py / room_compose.py (ассет-пайплайн).
- docs — PORT_PLAN, KID_PLAN, форматы ресурсов (Apple II / MSDOS / DAT).
- bgtest/coltest/poc — ранние PoC (фон, коллизия, первый прототип).

.gitignore: build-артефакты applications/*/*/*; исключены внешние
референс-репозитории (SDLPoP/mininim/PR/Apple-II — свои git-клоны) и
оригинальные game-данные MSDOS/ (копирайт, только для реверса форматов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:51:08 +03:00
snark13 484b18d10c libc+libbgi: raw-клавиатура (held-state) + column-major блит спрайтов
libc/kbd: kbd_raw_open/close/down — сырой PS/2-канал клавиатуры с
held-state (битовая карта _kbdraw_down[512], EXT-клавиши +256).  Пока
raw открыт, кадровый IRQ-трамплин перехватывает байт SIO у DSS и
декодирует make/break (0xF0/0xE0-префиксы) сам.  FIFO вычерпывается В
ЦИКЛЕ (приёмный буфер SIO 3 байта; пачка break-кодов при одновременном
отпускании иначе теряется → залипание клавиши).  Буфер W2-трамплина
поднят 224→288 Б под выросший обработчик.

libc/conio: kbd_mod_state() — live-состояние модификаторов (ESTEX
CTRLKEY $33h), Shift/Ctrl/Alt/Lock прямо сейчас, KBD_MOD_* маска.

libbgi: gfx_blit_cols(x,y,img,flip) + _bgi_blit_cols_raw — блит
column-major спрайта вертикальным accel-проходом, бесплатный
горизонтальный флип (sstride<0), клип по экрану.  Для персонажей.

libbgi/atlas_load: восстанавливать W3 ДО записи a->count (atlas_t в
--bank памяти резолвится через W3; count оставался мусором).

tests/kbdraw — тест raw-клавиатуры; size-baseline +kbdraw.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:48:32 +03:00
snark13 e14b6745f7 examples/rpgwalk: переключатель FPS-делителя (1/2/3) — проверка пейсинга
Клавиши 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>
2026-07-15 10:42:28 +03:00
snark13 1b4fbeaa6b libbgi: FPS-делитель gfx_set_fps_div(n) поверх цепочки кадровых IRQ
Логический кадр = ровно 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>
2026-07-15 10:36:37 +03:00
snark13 2c6f4e33c3 libc/irq: цепочка кадровых обработчиков + all-modes W1-remap
_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>
2026-07-15 10:23:39 +03:00
snark13 6b6986d9b6 docs: W1-перемапы DSS при EI подтверждены артефактом; дизайн цепочки irq
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>
2026-07-14 17:34:21 +03:00
snark13 b9ddce8d34 libbgi: спрайты — кэш адреса кадра, DDA+asm тик, Y-сортировка со слоями
Оптимизации 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>
2026-07-14 17:19:24 +03:00
snark13 0eec977630 libbgi: sy*img_w умножением вместо O(sy)-цикла — rpgwalk 24 → стабильные 48 fps
Профилирование rpgwalk в dev-MAME (watchpoint на OUT-маркеры +
totalcycles) показало: blit 8 спрайтов ел 17 мс из 20.5 — 85% в цикле
`while (sy--) src += img_w;` blit-обёрток (писался под «обычно sy==0»,
а вертикальные ленты атласов дают sy до 176 → до 64К тактов на кадр).

Фикс: src += sy*img_w через __mulint (O(1); __mul16 адаптивен — при
sy < 256 крутит 8 итераций, ~500Т) + if (sy): горизонтальные ленты и
одиночные спрайты не платят и за умножение.  Blit: 45К → 11.8К
тактов/спрайт.

Замерен бюджет кадра (docs/sprite-api-design.md §9д): кадр 48.83 Гц =
430080 тактов @21МГц; спрайт 16×16 ≈ 26К (тик 7.3К + heal 6.4К +
blit 11.8К) → лимит стабильных 48 fps = 14 спрайтов (15 — 94% кадров,
16 — на грани).  FPS-плашка bar+outtextxy стоит ~210К (полкадра!) —
HUD рисовать putimage-заготовкой.

examples/rpgwalk/rpgprof.c — профилировочная копия демо: визуальный
профайлер (цвет бордера по секциям кадра) + маркеры для тактового
профайла через wpiset дебаггера (рецепт в шапке и §9д).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 21:54:37 +03:00
snark13 02f7afe765 examples/rpgwalk: FPS-метр
Плашка слева-сверху (значение раз в секунду, рисуется на обеих
страницах — fps_draw=2, как в examples/space).  ~39 fps на 8 ходящих
персонажах (кадр на грани 20 мс — частично двухvsync'овые кадры).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 17:38:04 +03:00
snark13 b010d24792 examples/rpgwalk: 8 RPG-персонажей ходят по травяному полю
Демо на реальном арте (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>
2026-07-13 17:27:00 +03:00
snark13 6dea6955c2 libbgi: gfx_pal_sync + палитра ↔ файл (gfx_pal_fsave/fload); все PASS
- 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>
2026-07-13 16:51:49 +03:00
snark13 8792594c5a examples/space: демо полного спрайтового стека (атлас W0 + авто-анимации)
Всё сразу: .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>
2026-07-13 16:40:48 +03:00
snark13 eb9ca0179d libbgi: авто-анимация спрайтов — кадровая + tween (tests/spranim все PASS)
Реализация §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>
2026-07-13 16:14:31 +03:00
snark13 0e935343d9 libbgi: W0-атласы спрайтов — загрузчик, движок, упаковщик (все проверки PASS)
Реализация §3.1/§9в: атлас = один .atl-файл = одна EMM-страница,
подключаемая в W0 на время блита.

- atlas_load: read() файла целиком в страницу через W3 + патч ISR-стаба
  (0x38: JP _gfx_w0_isr; 0x66: RETN); atlas_free/atlas_image/
  atlas_sprite_init; gfx_w0_map/unmap для ручных вызовов.
- _gfx_w0_isr (стаб из tests/w0page): свап на ядро DSS → честный 0x38 →
  restore спрайт-страницы; покрывает IM1 и IM2-чейн.
- sprite_t.page (0 = обычная память); sprite_update в блит-проходе
  мапит страницу по смене (один OUT на атлас), эпилог возвращает DSS
  (+52 Б на движок — цена фичи).
- toolchain/mkatlas.py: PNG (indexed) / raw → .atl; заголовок 0x100,
  каталог 0x68 (19 лент), файл-офсет == офсет страницы == W0-адрес.
- Сплит _gfx_sprite_fns → _gfx_blit_fn.c + _gfx_heal_fn.c (1 указатель
  = 1 модуль): putsprite-only программа не тянет heal-ядро (spriteclip
  ловил +292 Б; теперь −68 Б от эталона).

tests/atlas (MAME dev, PASS): загрузка (count/страница), каталог и
данные по W0-адресам (заголовок ленты через gfx_w0_map), 3 спрайта из
двух лент через движок — пиксели проверены read_vram побайтно
(0x10/0x12/0x21, прозрачный угол = фон).

docs: §9г — предложение авто-анимации (кадровая ±step без умножений,
tween-Брезенхэм порциями, тикер через funcptr) — ОБСУЖДАЕТСЯ.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 15:52:00 +03:00
snark13 c4a512200f tests/w0page + docs §9в: атласы в страницах W0 — probe-тест (все PASS)
Схема: атлас в EMM-странице, на время sprite_update страница
подключается в W0.  Все прерывания исполняют 0x38 текущей страницы W0
(IM1 напрямую, IM2-трамплин чейнит jp 0x0038) — защита: стаб в первых
0x100 байтах страницы (0x38: JP на W2-хелпер: свап на страницу DSS →
честный 0x38 → restore спрайт-страницы; 0x66: RETN).

Проверено в dev-MAME (tests/w0page, все PASS):
  P1 ESTEX READ в W3-замапленную страницу;
  P3 IM1: ~5 c busy-цикла с EI при странице в W0 — жив, сентинел цел;
  P4 IM2: 100 кадровых прерываний посчитаны при подключенной странице
     (тракт трамплин→чейн→стаб→DSS→restore);
  P5 accel-блит src=0x0100 (W0) — пиксели верны по ОЗУ-копии.

Формат .atl в §3.1 дополнен: атлас ≤ 16К−0x100 (одна страница, один
файл), вариант II — 0x100-заголовок, файл-офсет == офсет страницы ==
W0-адрес, загрузка = read() целиком + патч стаба.  На железе
перепроверить.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 12:30:24 +03:00
snark13 88a00bb4ec docs: атласы — рекомендация по компоновке + предложение файл-формата .atl
Решение: in-memory формат остаётся (getimage + sx/sy, ленты/сетки).
Рекомендация: кадры одного спрайта — одного размера с полной сеткой;
для спрайтов разных размеров — контейнерный формат .atl (каталог +
независимые getimage-ленты, паддинг невозможен по построению).
Формат — предложение, реализация по потребности первого приложения.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 11:44:49 +03:00
snark13 ee1ca00c6f libbgi: funcptr-диспетч clip/noclip спрайтовых ядер + снятие src[0]-фикса
Диспетчеризация 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>
2026-07-13 11:37:54 +03:00
snark13 72ce66275e libbgi: спрайтовый движок v2 + accel-блит/heal leaf'ы + noclip-путь
Спрайтовая графика поверх 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>
2026-07-12 21:51:52 +03:00
snark13 78161561e7 sprinter-cc: --max-allocs 100000 по умолчанию для пользовательского кода
Тот же приём, что во fast-библиотеках (786836e): агрессивная
регистровая аллокация SDCC.  --max-allocs N переопределяет (меньшее
значение = быстрее компиляция).

Замер (47 программ): mdview -235, banklocl -149, fbench -110,
filetest -90, ls -81, solidt -69 и т.д.; 4 микро-роста (+1..+12 —
другие развязки аллокатора, шум).  Полная сборка ~2 мин -> ~4:15.
MAME: filetest/bgitest/seek (с big.txt, скриншот пользователя) зелёные.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 22:35:44 +03:00
snark13 786836e2e7 libc+libbgi: fast-версии собираются с --max-allocs-per-node 100000
Идея из mdview2 (memory/mdview2_size_budget): большее время компиляции
покупает более агрессивную регистровую аллокацию SDCC.  Применено к
fast-вариантам обеих библиотек (sprinter.lib, bgi256.lib); safe-версии
остаются на дефолте — быстрая пересборка для отладки.

Замер (47 программ, роста нет): solidt -507, filetest -506, fbench
-461, bgi_img -339, bgitest -316, gfx_dbuf -316, fdmax -278, errno
-179, gfx_demo -173, accfill -143, ptime -128, stattest -117, cbl* -53,
остальные до -28.  Время сборки: libc fast 17с -> 68с, libbgi 43с
(обе) — терпимо.  MAME: filetest + bgitest зелёные.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 22:24:50 +03:00
snark13 9f8aa6fc28 libc: две версии библиотеки — sprinter.lib (fast) / sprinter_safe.lib
Симметрично 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>
2026-07-10 22:11:01 +03:00
snark13 c9ac0999fd libc: тонкие аксессоры → inline в заголовках (по размерному критерию)
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>
2026-07-10 21:53:37 +03:00
snark13 5de2f06cfb libbgi: тривиальные аксессоры → inline в заголовках (17 модулей удалено)
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>
2026-07-10 21:21:26 +03:00
snark13 ba09c0bd05 libbgi: _bgi_draw_scaled — рендер текста run'ами вместо поштучных плотов
Строка глифа сканируется на прогоны единичных битов; прогон n битов =
прямоугольник n*size×size → hspan/vspan-примитивы (одиночный пиксель —
plot_raw).  Координаты инкрементальные (+= size за бит) — умножений на
пиксель нет.  Прогон клипится до span'а — клип теперь работает и в
fast-сборке (span-raw диапазон не клипят).  ~2× на size=1, ~6× на
size=4.  Проверено в MAME (bgitest: масштабы 1..4 + VERT — пиксель в
пиксель).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 19:36:53 +03:00
snark13 38bfabb2ca libbgi: __preserves_regs(d,e) на raw-примитивы (контракт, без эффекта сейчас)
plot/read/hspan/vspan_raw читают D/E (y в DE), но не пишут — аннотация
задокументирована в _bgi.h.  Честное измерение (полная пересборка с/без,
diff всех .asm в common/ и bgi256/): кодогенерация SDCC 4.5 НЕ меняется —
вызывающие держат локали в IX-фрейме и перезагружают DE перед каждым
вызовом, спасений DE вокруг вызовов не было.  Оставлено как контракт на
будущее (изменение вызывающих/компилятора); при правке asm сверять
клоббер-лист.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 19:35:32 +03:00
snark13 40e896a73d libbgi: фикс утечки W3-скобки в bar() + удалить дубликат _bgi_read
bar(): rectfill-ветки выходили ранним return без _bgi_end() — W3
оставался замаплен на видеобанк после каждого bar() со сплошной
заливкой.

_bgi_read дублировал getpixel (та же композиция begin+read_raw+end);
единственный потребитель floodfill переведён на getpixel, модуль
удалён.  getpixel.c: убраны мёртвые статики _gp_* (остались от
до-register версии).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 19:35:32 +03:00
snark13 5f0c46f0ea docs: идея span-примитивов для узких прямоугольников (TODO + fill-budget)
При узкой стороне <= 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>
2026-07-10 18:02:16 +03:00
snark13 580837d2ee docs: бюджет тактов/размеров chunked-заливок + идеи дальнейших упрощений
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>
2026-07-10 17:58:17 +03:00
snark13 2c414da465 libbgi: chunked-заливки на мульти-триггере акселератора + сплит rectfill
Семантика 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>
2026-07-10 17:41:49 +03:00
snark13 e553e5e6c9 docs: обновить size_baseline после bgi256 register-ABI рефактора
Эталон отставал от коммита 0296079.  Дельты объяснены: bgitest/bgi_img
уменьшились (убран скретч _gfx_acc256), gfx_demo вырос (демо rectfill в
исходнике), gfx_dbuf вырос (vsync-wait тянет CBL через общий порт
0x004E — см. _cbl_port_ref/unref).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:17:04 +03:00
snark13 f0f0ab9276 docs: TODO — пакетное чтение/запись массива байт через акселератор
Задел для будущих leaf'ов, читающих/пишущих строку или столбец пикселей
одним burst'ом (как _bgi_hspan_raw/_bgi_vspan_raw), чтобы ускорить блит
getimage/putimage (сейчас per-pixel _bgi_read_raw повторяет Port_Y +
addr + bounds-check).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:17:04 +03:00
snark13 9b1ce71130 libbgi: упростить safe-контроль span'ов + отсекать len==0
_bgi_vspan_raw: safe-версия (без GFX_NOCHECK) больше НЕ клиппит диапазон
y+len и не обрабатывает y<0 — проверяется только валидность x/y (как в
_bgi_hspan_raw).  Сознательный компромисс: safe ловит грубый выход за
экран по координате, но не частичный отрезок; y+len<=256 — обязанность
вызывающего.

Оба span'а: в safe добавлена проверка len==0 → return (иначе B=0 по
конвенции акселератора рисует «256»).  В fast (GFX_NOCHECK) проверка
вырезается — поведение прежнее (256 точек), задокументировано в шапках.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:16:53 +03:00
snark13 029607971f libbgi: bgi256 fill-примитивы на register-ABI + typedef color_t
- удалён глобальный скретч акселератора (_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>
2026-07-10 14:32:21 +03:00
snark13 52636a5d6e libbgi: выделить графику BGI в отдельную библиотеку + тест спрайтов
Графика вынесена из 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>
2026-07-09 14:18:33 +03:00
snark13 3bf50f7ff7 docs: единый справочник по автотестам в MAME; убрать метод AUTORUN.BAT
- docs/mame-autotest.md — исчерпывающий документ: запуск, ввод команд,
  скриншоты, завершение сессий, анализ, раскладка клавиатуры, все квирки.
  Одного этого документа достаточно, чтобы работать с MAME в режиме
  автотестирования.
- mame_interactive.py теперь единственный инструмент: авто-запускает exe
  вводом пути (a:\<exe>+Enter), --step опционален (доп. ввод в программу),
  умные дефолты снимков/таймаута.
- удалён mame_auto_test.py (старый метод через AUTORUN.BAT chainload) и
  все упоминания AUTORUN.BAT в доках; интерактивный ввод его заменил.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:41:45 +03:00
snark13 657c6d2955 libc: BGI Фаза 2d-3 — settextstyle (масштаб и направление текста)
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>
2026-07-08 16:18:48 +03:00
snark13 b26560c409 libc: BGI Фаза 2d-2 — setlinestyle (стили и толщина линий)
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>
2026-07-08 16:14:03 +03:00
snark13 25cb7ba554 libc: BGI Фаза 2d-1 — getimage/putimage/imagesize (спрайты)
Растровые образы: 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>
2026-07-08 16:09:51 +03:00
snark13 dc37a14010 libc: BGI Фаза 2c — floodfill/pieslice/sector
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>
2026-07-08 16:04:04 +03:00
snark13 ecb419efda libc: BGI Фаза 2b — заливки (setfillstyle/bar/bar3d/fillpoly/fillellipse)
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>
2026-07-08 15:58:06 +03:00
snark13 5a48f7fafb libc: BGI Фаза 2a — arc/ellipse/drawpoly
Дуги/эллипсы через целочисленную тригонометрию 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>
2026-07-08 15:50:00 +03:00
snark13 e761d21505 libc: graphics.h — Turbo-C BGI-слой, Фаза 1 (режим 256)
Функционально-совместимый с 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>
2026-07-08 15:40:49 +03:00
snark13 72466f7cad toolchain: скриптовый интерактивный ввод в DSS через MAME
mame_interactive.py печатает произвольный текст в командную строку DSS,
дёргая поля AT/PS-2-клавиатуры :kbd:ms_naturl через Lua set_value
(at_keyboard сам генерит scancode'ы → SIO Z84C015 → DSS). Раньше
инъекция шла в ZX-матрицу :IO_LINE*, которую DSS не читает — отсюда
«нет эффекта». Полная раскладка char→(port,mask,shift) с авто-Shift.

Квирки: attotime.seconds целое (субсекунды через attoseconds/1e18),
клавишу держать коротко (~0.06с, иначе автоповтор), дискета без
AUTORUN.BAT → приглашение C:\>. Проверено end-to-end: dir<Enter> и
запуск теста набором a:\rt_test.exe<Enter> (Shift для ':' и '\').

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 12:18:37 +03:00
snark13 d652f89240 toolchain: автотест .exe в MAME без участия человека
AUTORUN.BAT chainload из system.bat (правится пользователем один раз)
+ mame_auto_test.py: кладёт .exe и сгенерированный AUTORUN.BAT на
дискету, гоняет MAME с Lua-таймингом (register_periodic +
manager.machine.time) для скриншотов и выхода по таймауту.

Natural keyboard (Lua natkeyboard:post/post_coded, -autoboot_command)
и прямая инъекция через ioport.fields[...]:set_value() не работают на
этом драйвере — перепробовано разными способами; AUTORUN.BAT chainload
оказался единственным надёжным путём запуска без участия человека.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 11:56:54 +03:00
snark13 46bd9ad0f1 gfx/time: vsync через polling бита кадра, sleep/delayms без halt-подсчёта
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>
2026-07-08 10:47:57 +03:00
snark13 5086c47f0f libc: IM2 Phase 2b — звук CBL/COVOX через callback fill(), без кольца libc
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>
2026-07-07 21:21:54 +03:00
snark13 8a952b99eb irq: Phase 2a — CTC-таймер на отдельном векторе 0x06 (Z84C015)
- 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>
2026-07-07 09:15:04 +03:00
snark13 5184415fc4 irq: фикс Phase 1 после отладки — DSS работает в IM 1, чейн всегда на 0x0038
Первая версия висла на первом прерывании. Две причины (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>
2026-07-07 09:05:28 +03:00
snark13 a6fe50e247 libc: IM2 user-ISR, Phase 1 — libc/irq (irq_install/irq_remove) + irqtest
Проще исходного плана (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>
2026-07-06 21:21:16 +03:00
504 changed files with 22067 additions and 1451 deletions
+13
View File
@@ -0,0 +1,13 @@
CompileFlags:
Add:
- -Ilibbgi/include
- -Ilibc/include
- -I.
- --target=z80-unknown-unknown
- -mz80
- -std=c99
- -D__SDCC # если нужно гасить SDCC-специфику через #ifdef
Remove:
- --target=z80-unknown-unknown
Diagnostics:
UnusedIncludes: None # опционально, чтобы не ругался на "лишние" инклюды под другой таргет
+26 -2
View File
@@ -3,7 +3,7 @@
# =========================================================================== # ===========================================================================
# `build/` directories anywhere in the tree # `build/` directories anywhere in the tree
# (top-level build/, lib/build/, toolchain/*/build/, ...) # (top-level build/, libc/build/, libbgi/build/, toolchain/*/build/, ...)
build/ build/
# sprinter-cc per-example intermediate directory # sprinter-cc per-example intermediate directory
@@ -40,7 +40,31 @@ tests/*/*.cdb
tests/*/*.mem tests/*/*.mem
tests/*/*.rst tests/*/*.rst
# libc archive (built from libc/, see lib/Makefile) # applications/<app>/<prog>/ — реальные приложения (та же схема, что
# examples/ и tests/, но на уровень глубже: applications/PoP/roomtest/...)
applications/*/*/*.exe
applications/*/*/*.asm
applications/*/*/*.lst
applications/*/*/*.lk
applications/*/*/*.ihx
applications/*/*/*.noi
applications/*/*/*.sym
applications/*/*/*.map
applications/*/*/*.rel
applications/*/*/*.cdb
applications/*/*/*.mem
applications/*/*/*.rst
# PoP-порт: внешние референс-репозитории (собственные git-клоны — НЕ
# часть этого репозитория) + оригинальные game-данные (копирайт, только
# для реверса форматов на этой машине).
applications/PoP/SDLPoP/
applications/PoP/mininim/
applications/PoP/PR/
applications/PoP/Prince-of-Persia-Apple-II/
applications/PoP/MSDOS/
# libc + libbgi archives (built by libc/Makefile + libbgi/Makefile)
lib/*.lib lib/*.lib
# Host-built mkexe binary + test outputs (input fixtures *.bin/*.ihx kept) # Host-built mkexe binary + test outputs (input fixtures *.bin/*.ihx kept)
+26 -11
View File
@@ -6,20 +6,34 @@ Target-слой SDCC 4.5 (z80) для компьютера Sprinter Sp2000: crt0
## Сборка и проверка ## Сборка и проверка
``` ```
make # tools + lib + все тесты (43) + examples make # tools + lib + libbgi + все тесты (45) + examples
make -C lib # только libc → lib/sprinter.lib make -C libc # только libc → lib/sprinter.lib (fast) + sprinter_safe.lib
make -C libbgi # только BGI → lib/bgi256.lib (fast) + bgi256_safe.lib
make floppy # упаковать все .exe в mame/v306/IMG/mc.img make floppy # упаковать все .exe в mame/v306/IMG/mc.img
make size-check # размерный регресс: _CODE vs docs/size_baseline.tsv make size-check # размерный регресс: _CODE vs docs/size_baseline.tsv
make size-baseline # принять текущие размеры эталоном make size-baseline # принять текущие размеры эталоном
``` ```
Одиночный тест: `cd tests/<имя> && make run` (пакует ТОЛЬКО этот exe Обе библиотеки собираются в двух вариантах: fast (дефолт; `-D*_NOCHECK`
+ EXTRA_DATA на дискету и запускает MAME). Тесты в MAME гоняет параметр-валидации вырезаны) и safe (линкуется по `sprinter-cc --safe`).
пользователь — готовь дискету и проси прогнать. Критичные гарды (напр. _fd_guard — 9-й OPEN вешает DSS) — в ОБОИХ.
После правок libc: пересборка от чистого листа (`make -C lib clean`) Графика (BGI) — отдельная библиотека libbgi/ (см. ниже). Программа,
не обязательна — stale .rel чистятся автоматически; `make size-check` использующая graphics.h, собирается с `--gfx 256` (или `--gfx 16` в
обязателен (рост _CODE без причины — регрессия). Фазе 2): sprinter-cc подлинкует lib/bgi256.lib и добавит -I libbgi/include.
Одиночный тест: `cd tests/<имя> && make run` (пакует exe + EXTRA_DATA на
дискету и запускает MAME автоматически через `toolchain/mame_interactive.py`,
снимает скриншоты, выводит пути). Для сложных сценариев (диалог, несколько
шагов ввода) — прямой вызов:
`python3 toolchain/mame_interactive.py tests/<имя>/<имя>.exe --snap T1,T2 --timeout T`.
Скриншоты лежат в `mame/v306/snap_auto/sprinter/` (читаются инструментом Read).
Подробности: `docs/mame-autotest.md`.
После правок libc/libbgi: пересборка от чистого листа (`make -C libc clean`
/ `make -C libbgi clean`) не обязательна — stale .rel чистятся
автоматически; `make size-check` обязателен (рост _CODE без причины —
регрессия).
## Правила libc ## Правила libc
@@ -29,7 +43,7 @@ make size-baseline # принять текущие размеры эталон
(`_`-префикс); общие статики — в отдельные data-модули (`_`-префикс); общие статики — в отдельные data-модули
(`_xxx_state.c`); internal-заголовки (`_file.h`, `_gfx.h`, …) — (`_xxx_state.c`); internal-заголовки (`_file.h`, `_gfx.h`, …) —
рядом с исходниками, НЕ в libc/include. рядом с исходниками, НЕ в libc/include.
- Имя файла = имя функции. lib/Makefile собирает wildcard'ом — - Имя файла = имя функции. libc/Makefile собирает wildcard'ом —
ничего регистрировать не надо. ничего регистрировать не надо.
- Комментарии — на русском; шапка модуля объясняет что/зачем + ABI. - Комментарии — на русском; шапка модуля объясняет что/зачем + ABI.
- File-scope переменные НЕ инициализировать `= 0` (crt0 зануляет - File-scope переменные НЕ инициализировать `= 0` (crt0 зануляет
@@ -54,12 +68,13 @@ make size-baseline # принять текущие размеры эталон
лимит 8 файловых манипуляторов, 9-й OPEN ВЕШАЕТ DSS (_fd_guard); лимит 8 файловых манипуляторов, 9-й OPEN ВЕШАЕТ DSS (_fd_guard);
ENV $46: A=0 = NOT FOUND. ENV $46: A=0 = NOT FOUND.
- Перед обвинением компилятора/железа — подтвердить артефактом - Перед обвинением компилятора/железа — подтвердить артефактом
(сгенерированный .asm в lib/build/, дамп, репро) — см. (сгенерированный .asm в libc/build/ или libbgi/build/, дамп, репро) — см.
memory/defer_unexplained_quirks. memory/defer_unexplained_quirks.
## Структура ## Структура
- `libc/<area>/*.c` — модули libc; `libc/include/` — публичные заголовки - `libc/<area>/*.c` — модули libc (ядро, БЕЗ графики); `libc/include/` — публичные заголовки libc
- `libbgi/` — графика BGI (отдельная библиотека): `common/` — mode-agnostic (один исходник, .rel в обеих driver-библиотеках), `bgi256/` + `bgi16/` — mode-specific leaf'ы (реальные реализации, без обёрток); `include/` — graphics.h + gfx.h; `_bgi.h` — внутренний заголовок. Собирает `lib/bgi256.lib``bgi16.lib` в Фазе 2). Выбор режима линковкой: `--gfx 256` / `--gfx 16`.
- `runtime/` — crt0-семейство, heap, bank (bank.s собирается per-build) - `runtime/` — crt0-семейство, heap, bank (bank.s собирается per-build)
- `bin/sprinter-cc` — обёртка компилятора; `toolchain/mkexe` — упаковщик - `bin/sprinter-cc` — обёртка компилятора; `toolchain/mkexe` — упаковщик
- `tests/` — по одному API/фиче; `examples/` — реальные приложения - `tests/` — по одному API/фиче; `examples/` — реальные приложения
+10 -6
View File
@@ -2,7 +2,7 @@
# #
# make build host tools, libc archive, all tests, all apps # make build host tools, libc archive, all tests, all apps
# make tools build only host tools (mkexe) # make tools build only host tools (mkexe)
# make lib build lib/sprinter.lib (libc archive used by sprinter-cc) # make lib build lib/sprinter.lib (libc) + lib/bgi256.lib (libbgi)
# make tests build all libc feature tests under tests/ # make tests build all libc feature tests under tests/
# make examples build all real applications under examples/ # make examples build all real applications under examples/
# make floppy package every .exe + test fixtures into mame/v306/IMG/mc.img # make floppy package every .exe + test fixtures into mame/v306/IMG/mc.img
@@ -16,10 +16,11 @@
TESTS := hello hello2 simple banked bankedbg banktest strtest cat seek \ TESTS := hello hello2 simple banked bankedbg banktest strtest cat seek \
malloc mem_test argv errno rt_test openenv ls conio conio2 \ malloc mem_test argv errno rt_test openenv ls conio conio2 \
attrprob timedir mouse banklocl stdlib assrtest ptime stattest \ attrprob timedir mouse banklocl stdlib assrtest ptime stattest \
filetest fdmax fbench solidt dec_test gets stest2 winrest \ filetest fdmax fbench solidt irqtest cbltest cblwav cblstream dec_test gets stest2 winrest \
bios_text text_palette \ bios_text text_palette \
gfx_demo gfx_d16 gfx_text gfx_mous gfx_dbuf gfx_demo gfx_dbuf bgitest bgi_img accfill
# gfx_d16 / gfx_text / gfx_mous — 16-цветные; убраны до Фазы 2 (bgi16.lib
# ещё не собирается). Вернуть мигрированными на BGI --gfx 16.
# Larger end-user applications under examples/. # Larger end-user applications under examples/.
APPS := mdview mdview2 APPS := mdview mdview2
@@ -34,6 +35,7 @@ ALL_EXES := $(TEST_EXES) $(APP_EXES)
DATA_FILES := \ DATA_FILES := \
tests/cat/test.txt \ tests/cat/test.txt \
tests/seek/big.txt \ tests/seek/big.txt \
tests/cblwav/speech.pcm \
examples/mdview/SAMPLE.MD examples/mdview/SAMPLE.MD
.PHONY: all tools lib tests examples check clean sdcc floppy \ .PHONY: all tools lib tests examples check clean sdcc floppy \
@@ -45,7 +47,8 @@ tools:
$(MAKE) -C toolchain/mkexe $(MAKE) -C toolchain/mkexe
lib: lib:
$(MAKE) -C lib $(MAKE) -C libc
$(MAKE) -C libbgi
check: tools check: tools
$(MAKE) -C toolchain/mkexe check $(MAKE) -C toolchain/mkexe check
@@ -80,7 +83,8 @@ size-baseline:
clean: clean:
$(MAKE) -C toolchain/mkexe clean $(MAKE) -C toolchain/mkexe clean
$(MAKE) -C lib clean $(MAKE) -C libc clean
$(MAKE) -C libbgi clean
@for t in $(TESTS); do $(MAKE) -C tests/$$t clean; done @for t in $(TESTS); do $(MAKE) -C tests/$$t clean; done
@for a in $(APPS); do $(MAKE) -C examples/$$a clean; done @for a in $(APPS); do $(MAKE) -C examples/$$a clean; done
+6 -1
View File
@@ -62,8 +62,13 @@ $(EXAMPLE).exe: $(SOURCES) $(MKEXE) $(LIB) $(RUNTIME_DEPS)
$(MKEXE): $(MKEXE):
$(MAKE) -C $(PROJ_ROOT)/toolchain/mkexe $(MAKE) -C $(PROJ_ROOT)/toolchain/mkexe
# $(LIB) = lib/sprinter.lib (libc). Графика (bgi256.lib) собирает
# libbgi/Makefile; гоним и его — иначе standalone `make` в тесте с
# --gfx 256 не найдёт bgi256.lib при линковке. Оба инкрементальные,
# на повторном запуске ничего не пересобирают.
$(LIB): $(LIB):
$(MAKE) -C $(PROJ_ROOT)/lib $(MAKE) -C $(PROJ_ROOT)/libc
$(MAKE) -C $(PROJ_ROOT)/libbgi
clean: clean:
rm -rf .sprinter-cc-* $(EXAMPLE).exe rm -rf .sprinter-cc-* $(EXAMPLE).exe
+27
View File
@@ -0,0 +1,27 @@
# bgtest — мини-тест атласов статического фона PoP (Шаг 2, до порта room.c).
# Проверяет загрузку .atl + палитру + прямую адресацию + прозрачность.
#
# --memory huge (как poc): atlas_load/gfx_blit трогают W3; huge кладёт
# CODE в W1, DATA/BSS в W2 — без банков. --gfx 256 подлинкует bgi256.
#
# Данные фона генерит toolchain/pop_pack_bg.py в ../poc/res/bg/.
PROJ_ROOT := $(abspath $(CURDIR)/../../..)
EXAMPLE := bgtest
MEMORY ?= huge
EXTRA_FLAGS ?= --gfx 256
BG_DIR := $(CURDIR)/../poc/res/bg
BG_DATA := $(BG_DIR)/pop_env0.atl $(BG_DIR)/pop_env1.atl $(BG_DIR)/pop_env2.atl \
$(BG_DIR)/pop_env3.atl $(BG_DIR)/pop_env4.atl \
$(BG_DIR)/pop_wall.atl $(BG_DIR)/pop_fore.atl $(BG_DIR)/pop_bg.pal
EXTRA_DATA := $(BG_DATA)
include $(PROJ_ROOT)/app.mk
# Ассеты фона: пересобрать пакером, если исходники поменялись.
$(BG_DATA): $(PROJ_ROOT)/applications/PoP/toolchain/pop_pack_bg.py \
$(PROJ_ROOT)/applications/PoP/toolchain/render_room.py
cd $(PROJ_ROOT)/applications/PoP/toolchain && python3 pop_pack_bg.py
$(EXAMPLE).exe: $(BG_DATA)
+112
View File
@@ -0,0 +1,112 @@
/*
* bgtest.c — мини-тест атласов статического фона PoP (Шаг 2 порта, ДО
* порта room.c). Проверяет на MAME: загрузку 7 .atl, палитру pop_bg.pal,
* ПРЯМУЮ адресацию (id -> страница/idx без remap-таблиц) и прозрачность
* (индекс 0xFF). Рисует сетку репрезентативных спрайтов на цветной
* заливке — прозрачные области должны показать фон.
*
* Раскладка из toolchain/pop_pack_bg.py (см. pop_bg_atlas.h):
* ENV фон id N -> env[N>>5], idx N&31 ; WALL/FORE idx = id.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <conio.h>
#include <stdio.h>
/* Имена файлов — плоская ФС диска (make_disk кладёт по basename). */
static const char *const ENV_ATL[5] = {
"pop_env0.atl", "pop_env1.atl", "pop_env2.atl",
"pop_env3.atl", "pop_env4.atl"
};
static atlas_t env[5];
static atlas_t wall_a;
static atlas_t fore_a;
/* Блит одного спрайта ленты idx атласа a в (x,y): страница атласа в W0,
* gfx_blit читает w/h из getimage-заголовка ленты (в W0). */
static void put(atlas_t *a, unsigned char idx, int x, int y)
{
const void *img = atlas_image(a, idx);
gfx_w0_map(a->page);
gfx_blit(x, y, img);
gfx_w0_unmap();
}
static void put_env(unsigned char id, int x, int y)
{
put(&env[id >> 5], (unsigned char)(id & 31), x, y);
}
int main(void)
{
int i;
/* Загрузка всех 7 атласов (env0..4 + wall + fore). */
for (i = 0; i < 5; i++) {
if (atlas_load(&env[i], ENV_ATL[i]) != 0) {
printf("atlas_load %s failed\n", ENV_ATL[i]);
return 1;
}
}
if (atlas_load(&wall_a, "pop_wall.atl") != 0) { puts("wall atl fail"); return 1; }
if (atlas_load(&fore_a, "pop_fore.atl") != 0) { puts("fore atl fail"); return 1; }
initgraph();
gfx_set_draw_page(0);
gfx_set_visible_page(0);
if (gfx_pal_fload(0, "pop_bg.pal") < 0)
gfx_pal_fload(0, "a:\\pop_bg.pal");
/* Свой яркий фон-индекс (вне 0x50..0x6F, занятых графикой) — чтобы
* прозрачные (0xFF) области спрайтов были ЯВНО видны. */
#define BG_IDX 0x20
gfx_pal_set(0, BG_IDX, 120, 0, 90); /* r,g,b — тёмно-пурпурный */
gfx_pal_sync();
setfillstyle(SOLID_FILL, BG_IDX);
bar(0, 0, 319, 255);
setcolor(WHITE);
outtextxy(2, 2, "PoP bg atlas test");
/* Прозрачный блит: банк не пишет 0xFF (фон проступает). */
gfx_set_bank(GFX_BANK_TRANSPARENT);
/* --- Стены (chtab_7): нижние грани 7/9/5/3, основные 8/10/6/4 --- */
put(&wall_a, 3, 4, 20); put(&wall_a, 5, 40, 20);
put(&wall_a, 7, 76, 20); put(&wall_a, 9, 112, 20);
put(&wall_a, 4, 4, 90); put(&wall_a, 6, 40, 90);
put(&wall_a, 8, 76, 90); put(&wall_a, 10, 112, 90);
/* декали-марки стен 14..17 */
put(&wall_a, 14, 150, 20); put(&wall_a, 15, 168, 20);
put(&wall_a, 16, 186, 20); put(&wall_a, 17, 204, 20);
/* --- Столб: база 92, боковая грань 93, фронт 95 (fore) --- */
put_env(92, 4, 160);
put_env(93, 40, 160);
put(&fore_a, 95, 76, 160);
/* --- Пол: база 41, правый треуг. 42, низ 43; силуэт 44/45 --- */
put_env(41, 150, 120);
put_env(42, 190, 120);
put_env(44, 230, 120);
put_env(45, 260, 120);
/* --- Решётка-окно 126, дебрис 97/98, слайсы ворот 52/53 --- */
put_env(126, 150, 160);
put_env(97, 190, 160);
put_env(52, 230, 160);
put_env(53, 250, 160);
gfx_set_bank(GFX_BANK_NORMAL);
/* Держим картинку на экране до нажатия (не полагаемся на блокирующий
* getch — в автотесте клавиш нет, крутимся до таймаута MAME). */
while (!kbhit())
gfx_wait_vsync();
closegraph();
for (i = 0; i < 5; i++) atlas_free(&env[i]);
atlas_free(&wall_a);
atlas_free(&fore_a);
return 0;
}
+6
View File
@@ -0,0 +1,6 @@
# coltest — прототип вертикального accel-copy (gfx_blit_cols) + флип.
PROJ_ROOT := $(abspath $(CURDIR)/../../..)
EXAMPLE := coltest
MEMORY ?= huge
EXTRA_FLAGS ?= --gfx 256
include $(PROJ_ROOT)/app.mk
+64
View File
@@ -0,0 +1,64 @@
/*
* coltest.c — прототип K2a: проверка вертикального accel-COPY
* (gfx_blit_cols) + прозрачности 0xFF + горизонтального флипа.
*
* Спрайт 16x24 column-major, АСИММЕТРИЧНЫЙ:
* левые 8 колонок: верх (row<12) = ПРОЗРАЧНО (0xFF), низ = БЕЛЫЙ (1)
* правые 8 колонок: КРАСНЫЙ (2)
* На синем фоне (3). Ожидаем на MAME:
* normal @ (50,100): слева бело-снизу+прозрачно-сверху, справа красный;
* flip @(120,100): ЗЕРКАЛО — слева красный, справа бело+прозрачно-сверху;
* в прозрачных местах виден СИНИЙ фон.
*/
#include <graphics.h>
#include <gfx.h>
#include <conio.h>
#define W 16
#define H 24
static unsigned char spr[4 + W * H];
int main(void)
{
int col, row;
spr[0] = W; spr[1] = 0;
spr[2] = H; spr[3] = 0;
for (col = 0; col < W; col++)
for (row = 0; row < H; row++) {
/* ФИНАЛ: АСИММЕТРИЧНЫЙ — лево верх прозрачно / низ белый,
* право красное. normal: лево бело+прозрач-верх, право красн;
* flip: ЗЕРКАЛО (лево красн, право бело+прозрач-верх). */
unsigned char v;
if (col < 8)
v = (row < 12) ? 0xFF : 1;
else
v = 2;
spr[4 + col * H + row] = v;
}
initgraph();
gfx_set_draw_page(0);
gfx_set_visible_page(0);
gfx_pal_set(0, 1, 255, 255, 255); /* белый */
gfx_pal_set(0, 2, 224, 32, 32); /* красный */
gfx_pal_set(0, 3, 32, 48, 200); /* синий фон */
gfx_pal_set(0, 255, 0, 224, 0); /* ДИАГ: 0xFF как ЗЕЛЁНЫЙ цвет (bank NORMAL) */
gfx_pal_sync();
setfillstyle(SOLID_FILL, 3);
bar(0, 0, 319, 255);
setcolor(1);
outtextxy(40, 80, "normal");
outtextxy(110, 80, "flip");
gfx_set_bank(GFX_BANK_TRANSPARENT); /* ДИАГ: 0x58 подавление 0xFF (с тенью) */
gfx_blit_cols(50, 100, spr, 0); /* обычный */
gfx_blit_cols(120, 100, spr, 1); /* зеркало */
gfx_set_bank(GFX_BANK_NORMAL);
while (!kbhit())
gfx_wait_vsync();
closegraph();
return 0;
}
@@ -0,0 +1,283 @@
# Формат ресурсов Prince of Persia (Apple II, оригинальные исходники 1989)
Источник — официально опубликованные Джорданом Мехнером исходники
(`Prince-of-Persia-Apple-II/`, 6502-ассемблер). В отличие от DOS-версии, здесь
формат восстановлен **напрямую по коду**, а не по догадкам о байтах —
уверенность высокая везде, где указана ссылка на конкретный файл/строки.
---
## 1. Формат уровня (`01 POP Source/Levels/LEVEL0`…`LEVEL14`, 2304 байта)
Файлы уровня — это побайтовый дамп структуры `blueprnt`, которая грузится по
фиксированному адресу `$b700` (`EQ.S:28`) и объявлена как `dum blueprnt` в
`EQ.S:258-266`. Никакого отдельного заголовка файла нет — это чистый образ
структуры в памяти:
| Поле | Размер | Смещение в файле | Описание |
|---|---|---|---|
| `BLUETYPE` | 720 Б | 0719 | 24 экрана × 30 тайлов: тип объекта/тайла |
| `BLUESPEC` | 720 Б | 7201439 | 24 экрана × 30 тайлов: доп. байт состояния объекта |
| `LINKLOC` | 256 Б | 14401695 | Таблица связей нажимных плит/дверей, часть 1 |
| `LINKMAP` | 256 Б | 16961951 | Таблица связей, часть 2 |
| `MAP` | 96 Б | 19522047 | 24 экрана × 4 байта: граф соседних экранов |
| `INFO` | 256 Б | 20482303 | Метаданные уровня: старт Кида, стражников и т.д. |
Сумма: 720+720+256+256+96+256 = **2304** — точно совпадает с размером файла,
что подтверждает: это чистый дамп структуры, без обёртки.
### 1.1 Сетка тайлов (`BLUETYPE` / `BLUESPEC`)
Каждый экран — ровно **30 тайлов** (10 столбцов × 3 ряда): подтверждено
таблицами `BlockTable`/`BlockEdge` (`TABLES.S:74-154`) и логикой перехода
между экранами в `CTRLSUBS.S:218-234` (при переходе через край экрана
`tempblockx` меняется на ±10, `tempblocky` — на ±3).
Функция `CALCBLUE` (`GRAFIX.S:1757-1784`) вычисляет для экрана 1–24:
`BlueType = blueprnt + (screen-1)*30`, `BlueSpec = BlueType + 24*30`,
используя таблицу `Mult30` (`TABLES.S:131-140`).
Байт `BLUETYPE` упакован битовыми полями (`EQ.S:484-486`):
```
бит 7-6: secmask (%11000000) — назначение не установлено по доступному коду
(возможно, служебное поле редактора)
бит 5: reqmask (%00100000) — флаг "необходимая опорная плитка"
(проверяется в BREAKLOOSE, MOVER.S:395-397)
бит 4-0: idmask (%00011111) — тип тайла/объекта, 0-29
```
Перечень 30 типов объектов (`MOVEDATA.S:8-37`):
```
0 space 8 pillarbottom 16 exit 24 window2
1 floor 9 pillartop 17 exit2 25 archbot
2 spikes 10 flask 18 slicer 26 archtop1
3 posts 11 loose 19 torch 27 archtop2
4 gate 12 panelwof 20 block 28 archtop3
5 dpressplate 13 mirror 21 bones 29 archtop4
6 pressplate 14 rubble 22 sword
7 panelwif 15 upressplate 23 window
```
Проверено вручную на дампе начала `LEVEL1` (`00 00 00 21 01 21 21 21 34 34
33 33 21 23 00 34 14 14 14 34 14 34 34 2e 23 0b 01 21 34`) — например,
`0x33 → id=0x13=19 (torch)`+reqmask, `0x34 → id=20 (block)`+reqmask —
декодирование по таблице сходится чисто.
`BLUESPEC` — доп. байт, чья семантика зависит от типа тайла (единой схемы
нет, разбирается объект-специфичным кодом):
- **gate** (дверь, `FRAMEADV.S:2222-2234`): на диске — маленький enum (1 =
начинает открытой сверху, 2 = снизу, …), который через `initsettings`
(`FRAMEADV.S:22-23`, диапазон `gminval=0`..`gmaxval=188`, из
`MOVEDATA.S:56-57`) при инициализации уровня превращается в живой счётчик
"высоты двери" 0–188.
- **loose** (шаткая плитка, `FRAMEADV.S:2224-2237`): при инициализации всегда
принудительно обнуляется, независимо от значения на диске.
- **flask** (зелье, `FRAMEADV.S:2226,2239-2246`): значение×32 выбирает
цвет/тип зелья.
- **spikes** (шипы, `MOVER.S:365-382`, константы `spikeExt=5, spikeRet=9` в
`MOVEDATA.S:45-46`): 0 = безопасно/убраны, 1–8 — кадр анимации
выдвижения/втягивания, `$FF` = навсегда заклинило (тело наколото).
- **pressplate/upressplate** (нажимные плиты, `MOVER.S:425-464`,
`FRAMEADV.S:2059-2098`): значение — это **индекс в цепочке связей**
`LINKLOC`/`LINKMAP` (см. ниже); младшие 5 бит `LINKMAP` по этому индексу
одновременно служат счётчиком таймера плиты (0–31), определяющим
состояние "поднято/опущено".
### 1.2 `LINKLOC` / `LINKMAP` — граф триггеров (нажимные плиты → двери и т.п.)
Два параллельных массива по 256 байт кодируют цепочки "нажатие плиты X →
сработать объект на экране S, блок B". Восстановлено из `MOVER.S:506-537`
(цикл `trigger`) и `MOVER.S:1549-1581` (`gettimer/chgtimer/getloc/
getlastflag/getscrn`):
```
LINKLOC[i]: бит 7 = флаг "последнее звено цепочки"
биты 6-5 = младшие 2 бита номера целевого экрана
биты 4-0 = номер целевого блока (0-29); $FF = "никуда не привязано"
LINKMAP[i]: биты 7-5 = старшие 3 бита номера целевого экрана
(вместе с LINKLOC биты 6-5 → полный номер экрана 0-31)
биты 4-0 = таймер обратного отсчёта плиты (0-31, значим только
по индексу самой плиты)
```
`BLUESPEC` плиты хранит индекс `i` её *первого* звена; `getlastflag` идёт
вперёд (`inc linkindex`), пока не встретит бит 7 в `LINKLOC`. Сверено на
`LEVEL1`: байт по смещению 1440 (`0x89 = 10001001` → флаг конца цепочки,
целевой блок 9) и параллельно байт по смещению 1696 (`0x60 = 01100000`
старшие биты номера экрана) — согласуется с этой раскладкой. Заполнены
реально используемые уровнем звенья, остальное — "мусорные" повторяющиеся
байты-заполнители.
### 1.3 `MAP` — граф соседних экранов
24 записи × 4 байта = 96 байт: для каждого экрана (1–24)
`MAP[(scrn-1)*4 + 0..3] = левый, правый, верхний, нижний соседние экраны`,
читается через `GETLEFT/GETRIGHT/GETUP/GETDOWN` (`CTRLSUBS.S:244-274`,
индексация `MAP-4..MAP-1,x` при `x = scrn*4`). Экран `0` зарезервирован как
"нет экрана" (проверка `beq ]rts` в этих же процедурах).
### 1.4 `INFO` — метаданные уровня (256 байт, база = смещение файла 2048)
Объявлено как `dum INFO` в `EQ.S:272-288`:
| Смещение (от начала INFO) | Поле | Размер |
|---|---|---|
| 0 | "число экранов + 1" (используется в `SETINITIALS`, `SUBS.S:1441-1445`) | 1 |
| 1–63 | резерв/не используется | 63 |
| 64 | `KidStartScrn` | 1 |
| 65 | `KidStartBlock` | 1 |
| 66 | `KidStartFace` (направление; при загрузке инвертируется XOR `$ff`, `SUBS.S:1516-1518`) | 1 |
| 67 | заполнитель | 1 |
| 68 | `SwStartScrn` (стартовый экран меча) | 1 |
| 69 | `SwStartBlock` | 1 |
| 70 | заполнитель | 1 |
| 7194 | `GdStartBlock[1..24]` — стартовый блок стражника на экране; **≥30 = "стражника нет"** (`AUTO.S:1832-1834`, `SUBS.S:1677-1679`) | 24 |
| 95118 | `GdStartFace[1..24]` (86 = "стражника нет", см. `ShadFace cmp #86` по всему `AUTO.S`) | 24 |
| 119142 | `GdStartX[1..24]` — пересчитывается заново из блока при старте уровня, значение на диске почти не используется (`SUBS.S:1674-1690`) | 24 |
| 143166 | `GdStartSeqL[1..24]` | 24 |
| 167190 | `GdStartProg[1..24]` — "программа"/поведение ИИ стражника | 24 |
| 191214 | `GdStartSeqH[1..24]` — обнуляется при старте (`SUBS.S:1685-1686`) | 24 |
| 215–255 | резерв/не используется | 41 |
Проверено на `LEVEL1`: байт по смещению файла 0x800 = `0x18`=24 (число
активных экранов = 23+1); по смещению 0x840 — `01 00 ff 00 00 00 ff 1e 1e
11 1e 1e ...``KidStartScrn=1, KidStartBlock=0, KidStartFace=$FF,
SwStartScrn=0, SwStartBlock=0`, далее 24 байта `GdStartBlock`, в основном
`0x1e`(30, "нет стражника"), с реальной расстановкой только на экране 3
(`0x11`=17) и экране 23 (`0x06`) — согласуется с уровнем, где всего два
стражника.
### 1.5 Как уровень попадает с диска (важно: имя файла — не игровой механизм)
В рантайме нет чтения "по имени файла LEVELn" — это чисто утилита для
экспорта в этом репозитории. Реально `LOADLEVELX` (`MISC.S:795-809`)
использует фиксированные таблицы по номеру уровня `bluepTRKlst`/
`bluepREGlst` (`MISC.S:776-787`), дающие физическую **дорожку (1-33)** и
**регион (0/1)**, затем `rdbluep` (`MASTER.S:598-616`) вызывает
низкоуровневое чтение `rw18` (`RdGrpErr`) 9 физических групп по 256 байт
(`$b7-$bf`) — 9×256=2304 байта — прямо в буфер blueprint; регион 0/1 выбирает
половину 18-секторной дорожки (два уровня делят одну дорожку). Файлы
`LEVELn` в этом репозитории — реконструкция этого сырого блока для удобства
работы с инструментами.
---
## 2. Формат изображений/спрайтов (`IMG.CHTAB1-7`, `IMG.BGTAB1/2.DUN/.PAL`)
Каждый такой файл грузится целиком по **фиксированному адресу**, заданному
константами `chtableN`/`bgtableN` (`GAMEEQ.S:9-18`):
```
chtable1=$6000 chtable2=$8400 chtable3=$0800 chtable4=$9600
chtable5=$a800 chtable6=$6000 chtable7=$9f00
bgtable1=$6000 bgtable2=$8400
```
— то есть смещения внутри файла один-в-один совпадают с адресами в памяти
после загрузки.
### 2.1 Раскладка контейнера
Восстановлено из заголовка-комментария "Image table format" в `HIRES.S:181-186`,
процедуры разрешения указателя `setimage` (`HIRES.S:263-277`) и
`GETWIDTH`/`PREPREP` (`HIRES.S:283-339`):
```
Смещение 0 : 1 байт — число изображений в таблице (максимум 127,
в образцах встречается 0x7f)
Смещение 1..254 : 127 × 2-байтных little-endian указателей
(указатель на изображение N — по смещению 1+(N-1)*2,
N=1..127) — АБСОЛЮТНЫЕ адреса в адресном пространстве
фиксированной загрузки этой таблицы, указывающие на
запись данных этого изображения
Смещение 255 : заполнитель (таблица указателей занимает ровно 256 байт)
Смещение 256 (база+0x100) и далее:
последовательно идущие записи данных изображений:
байт 0: ширина (в байтах на строку)
байт 1: высота (число строк)
байты 2..(2+ширина*высота-1): сырые байты пикселей,
слева направо, сверху вниз, БЕЗ сжатия
```
Проверено вручную на `IMG.BGTAB1.DUN`: с точной арифметикой индексов из
`setimage` (`Y = image*2 - 1`, `HIRES.S:264-267`) первые ~30 записей дают
строго возрастающую последовательность указателей `0x6101, 0x6133, 0x6159,
0x618b, 0x61c9, 0x61fb, 0x6221, 0x6313, 0x63c9, ...` — указатель
изображения #1 приходится ровно на `bgtable1 ($6000) + 0x100`, то есть точно
на конец 256-байтной таблицы указателей. Это независимо подтверждает и
размер таблицы, и семантику указателей.
**Важно: сжатия в этом формате нет.** RLE/дельта-упаковка (`SngExpand`/
`DblExpand`/`DeltaExpPop`/`DeltaExpWipe` в `01 POP Source/Source/UNPACK.S`)
применяется только к полноэкранным изображениям (титры/пролог/катсцены), но
не к CHTAB/BGTAB — спрайты и фоновые тайлы хранятся как чистые упакованные
байты hi-res/double-hi-res экрана Apple II, без какого-либо RLE или дельты.
### 2.2 Параметры отрисовки (не часть файла ресурса)
При выводе спрайта (`LAY`/`FASTLAY`/`PEEL` и т.д., `HIRES.S:658-1740`)
используются zero-page параметры `PAGE/XCO/YCO/OFFSET/IMAGE/OPACITY/TABLE/
BANK` (описаны в `HIRES.S:155-178`): `OFFSET` (0–6) — горизontальный сдвиг на
под-байтовый пиксель, `OPACITY` выбирает режим совмещения (AND/OR/STA/XOR/
маска-OR) плюс отдельный бит горизонтального зеркалирования (бит 7). Это
чисто рантайм-параметры отрисовки, не хранящиеся в файле ресурса. Точный
механизм барабанного сдвига для `OFFSET` (таблицы `HRTABLES.S`/`YLO`/`YHI`)
не прослежен до конца — при необходимости требует отдельного анализа.
### 2.3 Инструмент DRAZ (авторская утилита создания спрайтов)
В `04 Support/DRAZ` нет исходников самой утилиты DRAZ — только файлы данных
(`PAC.*` — позы персонажей, и уже скомпилированные `IMG.*`), поэтому
внутренний пайплайн DRAZ (как позы превращаются в CHTAB) напрямую не виден.
Формат контейнера выше выведен полностью из кода движка-потребителя, что
является надёжным, но косвенным источником.
Отдельно: в игровой логике списков объектов (`ADDBACK`, `GRAFIX.S:191-214`)
встречается **рантайм-упаковка ссылки на фоновую картинку** в один байт: бит
7 выбирает `bgtable1` или `bgtable2`, биты 0-6 — номер картинки в таблице
(0-63). Это соглашение для внутриигровых списков объектов (`bgIMG` и т.п.), а
не свойство самих файлов CHTAB/BGTAB на диске.
---
## 3. "Главного индекса ресурсов" не существует
В отличие от DOS-версии (см. `docs/MSDOS_RESOURCE_FORMAT.md`), в рантайм-коде
Apple II **нет обобщённого справочника "имя ресурса → расположение на
диске"**. Расположение каждого ресурса зашито напрямую как таблицы
дорожка/группа-секторов прямо в коде загрузчика:
- Уровни: `bluepTRKlst`/`bluepREGlst` (`MISC.S:776-787`), используются
`LOADLEVELX`/`LOADLEVEL` (`MISC.S:795-809`, `MASTER.S:467-481`).
- Альтернативные наборы фонов/персонажей: `bg1trk`/`bg2trk`/`ch4trk`/`ch4off`
(`MASTER.S:522-528`).
- Массовая загрузка при старте (chtable1-7, bgtable1-2, seqtable и т.д.):
прямые вызовы `rw18`/`RdGrp`/`RdSeq` с литеральными hex-списками
групп-секторов в `MASTER.S:1250-1360` и `BOOT.S:100-118`.
Весь дисковый ввод-вывод идёт через нестандартный низкоуровневый драйвер
`rw18` (`rw18 = $d000`, `EQ.S:11-12`; папка `02 POP Disk Routines/RW1835`),
реализующий нестандартный формат **18 секторов/дорожку** (вместо 16 у
стандартного DOS 3.3) — этим объясняется, почему регионы уровня (9×256Б)
идут парами на одной физической дорожке. Символические имена
`chtableN`/`bgtableN` в `GAMEEQ.S` — ближайший аналог "индекса ресурсов", но
они связывают ресурс с **фиксированным адресом в ОЗУ**, а не с положением на
диске; связь с диском — отдельная, вручную сопровождаемая таблица,
сопоставленная с ресурсом лишь порядком вызовов загрузчика.
---
## 4. Что ещё не восстановлено (открытые вопросы)
- Точное назначение бит `secmask` (%11000000) в `BLUETYPE` — не встречено
использование в доступном игровом коде (возможно, поле только для
редактора уровней, не читается движком).
- Механизм барабанного сдвига `OFFSET` для суб-байтового позиционирования
спрайта по X (`HRTABLES.S`) — не прослежен в деталях.
- Внутренний формат авторских файлов `PAC.*` инструмента DRAZ (как позы
скелетной анимации превращаются в растровые кадры CHTAB) — исходники DRAZ
отсутствуют в репозитории, можно только косвенно восстановить по
результату (уже скомпилированным `IMG.*`).
+199
View File
@@ -0,0 +1,199 @@
# Prince of Persia — Kid (персонаж): анализ и план
Статус: план (2026-07-16). Опирается на разбор `SDLPoP/src/seg006.c`
(ядро физики/управления Kid), `seqtbl.c` (таблицы последовательностей),
`types.h` (char_type, seq_*, SEQ_*, actions_*), `SDLPoP/data/KID` (спрайты).
Фон уже готов и проверен на MAME (`applications/PoP/roomtest`, см.
`memory/pop_background_strategy`) — Kid развиваем в том же `roomtest` как
новый PoC (решение пользователя: старый `poc/` не трогаем).
**Копирайт:** спрайты Kid (`SDLPoP/data/KID`) — Broderbund/Ubisoft.
Использование настоящей графики Kid — сознательное решение пользователя
(в отличие от плейсхолдера в старом `poc/`, см. PORT_PLAN §5.1).
---
## 1. Как устроен персонаж в оригинале (что портируем)
### 1.1 Состояние — `char_type` (14 полей, types.h)
```
frame текущий номер кадра (индекс во frame_table_kid)
x, y позиция (byte; x — с учётом direction)
direction -1 влево / 0 вправо
curr_col, логическая клетка (тайл), где персонаж
curr_row
action КАТЕГОРИЯ действия (actions_*, см. 1.2)
fall_x, скорость падения (fall_y<22 = 1 ряд, <33 = 2 ряда)
fall_y
room комната
repeat счётчик для удержания-ввода (напр. повторный прыжок)
sword есть ли меч (бой — вне Фазы 1)
alive жив/мёртв
curr_seq УКАЗАТЕЛЬ в seqtbl (байткод текущей последовательности)
```
Состояние крошечное — легко живёт в W2.
### 1.2 Категории действия — `actions_*` (9 шт)
`0 stand`, `1 run_jump`, `2 hang_climb`, `3 in_midair`, `4 in_freefall`,
`5 bumped`, `6 hang_straight`, `7 turn`, `99 hurt`. `action` определяет,
как `check_action()`/`play_kid()` реагируют на ввод и физику каждый тик.
### 1.3 Движок анимации/движения — ГЛАВНОЕ
**Движение НЕ физика, а байткод + per-frame смещения** (подтверждает
PORT_PLAN §6). Три уровня:
1. **`seqtbl`** — байткод-программа на действие. Опкоды (types.h):
`SEQ_DX`(0xFB) сдвиг x на amount×direction, `SEQ_DY`(0xFA) сдвиг y,
`SEQ_FLIP`(0xFE) разворот, `SEQ_JMP`(0xFF)/`SEQ_JMP_IF_FEATHER`(0xF7),
`SEQ_UP`/`SEQ_DOWN`(0xFD/0xFC) смена ряда, `SEQ_ACTION`(0xF9) задать
`Char.action`, `SEQ_SET_FALL`(0xF8), `SEQ_KNOCK_UP/DOWN`, `SEQ_SOUND`,
`SEQ_DIE`/`SEQ_END_LEVEL`/`SEQ_GET_ITEM`. **Байт < 0xF0 = НОМЕР КАДРА**
→ ставит `Char.frame` и play_seq возвращается (один кадр за тик).
2. **`play_seq()`** (seg006.c:570) — интерпретатор: крутит опкоды из
`seqtbl + Char.curr_seq`, пока не встретит кадр. ~15 case — портируется
1-в-1. **Квирк:** seqtbl использует АБСОЛЮТНЫЕ DOS-адреса в JMP;
`SEQTBL_0 = seqtbl - SEQTBL_BASE(0x196E)` — при порте пересчитать
базу (JMP-адреса в наших данных).
3. **`frame_table_kid[]`** (seg006.c:127, ~180 кадров) — на КАЖДЫЙ кадр:
`{image, sword_flags, dx, dy, flags}`. `image` — индекс спрайта Kid;
`dx/dy` — смещение позиции ЭТОГО кадра; `flags`: 0x1F weight_x, 0x20
thin, 0x40 needs_floor, 0x80 even/odd-pixel (влияет на x-рендер).
**Тик персонажа:** `play_kid()` (диспетчер по action+вводу) → `play_seq()`
(двигает curr_seq, ставит кадр, применяет seq-dx/dy) → `frame_table[frame]`
даёт image+собственные dx/dy → позиция и спрайт. У нас это ложится на
`sprite_frame`+`sprite_move` (НЕ `sprite_anim`/`sprite_moveto` — см.
PORT_PLAN §6: авторские таблицы, не автопрогрессия).
### 1.4 Управление — `control_kid()`/`read_user_control()` (seg006.c)
Читает ввод (у нас — held-state `kbd_raw`, уже готово, §2 PORT_PLAN) и по
`Char.action` выбирает последовательность (`seqtbl_offset_char(seq_id)`).
Логика «что можно из какого состояния» — ядро ощущения PoP.
### 1.5 Взаимодействие с картой — collision (seg006.c)
`check_on_floor()`/`start_fall()` — пол под ногами / падение в яму;
`in_wall()` — упор в стену (сдвиг наружу); `check_grab()`/
`can_grab_front_above()` — зацеп за уступ; `fell_out()` — вывалиться из
комнаты; `check_spiked()`/loose — ловушки; `fall_accel()`/`fall_speed()`
ускорение падения. Всё читает ТИП тайла (`get_tile`) — у нас это уже
разобранные `fg[]/bg[]` (level.h/room1_data.h).
---
## 2. Спрайты Kid (219 шт, 16 цветов, 177 КБ)
- 219 PNG (`data/KID`), 16-цветные (палитра `res400.pal`, 16×RGB как env/
wall), макс кадр **53×35** — влезает в лимит движка 64×64. 177 КБ в
8bpp.
- `frame_table_kid` отображает кадр→`image` (индекс спрайта). Число
РАЗЛИЧНЫХ image — уточнить (≤219); паковать те, что реально используются
платформинг-последовательностями Фазы 1 (не все 219 — бой/катсцены
отдельно).
- **Палитра:** Kid 16 цветов → слоты Sprinter `0x70-0x7F` (env 0x50, wall
0x60 уже заняты; Kid не пересекается). Пиксель i: 0→0xFF, i→0x70+i.
Тот же пайплайн, что `pop_pack_bg.py`.
- **Атлас:** прямая адресация по номеру image (как фон): `kid[img>>5]`,
idx `img&31`; ~7 EMM-страниц (или SHIFT=4). Свой пакер `pop_pack_kid.py`
(переиспользовать код `pop_pack_bg.py`).
### 2.1 РЕШЕНИЕ ДО СТАРТА: per-frame offset vs padding
Кадры Kid — РАЗНОГО размера, а `sprite_t` рисует от угла фикс. w/h. Два
пути (см. PORT_PLAN §6.1, `memory/png_strip_padding_tradeoff`):
- **Padding** (bottom-center) — просто, но 219×53×35 ≈ 406 КБ (раздув ×2.3).
- **Per-frame offset** — хранить XCO/YCO кадра (у оригинала он и есть,
`APPLEII_RESOURCE_FORMAT §2.2`), рисовать `blit(x+xco, y+yco)`; паддинг не
нужен, память по факту (177 КБ). Требует лёгкого расширения хранения
(offset рядом с кадром) ИЛИ ручного смещения в коде рендера Kid.
**Рекомендация:** per-frame offset — оригинал так и делает (frame_table dx/dy
+ image XCO/YCO), даёт точное позиционирование И экономию. Хранить xco/yco
в нашей копии frame_table (добавить 2 байта/кадр — ~360 Б). Не тянуть
расширение `sprite.h` — рисовать Kid прямым `gfx_blit(x+xco, y+yco, img)`
(как фон), НЕ через retained `sprite_t`, раз позиция и кадр всё равно
задаются вручную каждый тик.
---
## 3. Данные для порта (объём)
- `frame_table_kid` → C-массив ~180×(5+2 offset) ≈ 1.3 КБ (const, ROM).
- `seqtbl` (нужные последовательности) → C-массив байт. Весь seqtbl ~1-2 КБ;
для Фазы 1 можно взять только платформинг-последовательности (вырезать
бой/гардов 55-92) — оценить после разметки. JMP-адреса пересчитать под
свою базу.
- Спрайты — атласы (EMM, не W2).
---
## 4. Фазы работы (по твоему списку, порядок по зависимостям)
**Фаза K0 — конвейер спрайтов + отрисовка одного кадра**
- `pop_pack_kid.py`: 219 (или подмножество) → `kid*.atl` + `kid.pal`
(слоты 0x70), таблица кадр→image + xco/yco.
- Отрисовать Kid ОДНИМ кадром (stand) в roomtest поверх фона на верном
тайле — проверить палитру/позицию/прозрачность на MAME.
- Артефакт-цель: Kid стоит на уступе комнаты 1 как в `1.1-2.png`.
**Фаза K1 — движок анимации (play_seq + frame_table)**
- Портировать `play_seq()` (интерпретатор) + `frame_table_kid` + минимальный
`seqtbl` (stand/run/turn).
- Прогнать несколько последовательностей вручную (stand→run→stop) —
проверить, что кадры и смещения совпадают с оригиналом (сверять с
SDLPoP/скриншотами, тайминг 50 Гц).
**Фаза K2 — управление на месте + ходьба (твои а, б)**
- `control_kid` подмножество: stand (2), run (1/84/13), turn (5/6),
standing_jump (3), crouch (50/49), safe_step (29-44 — аккуратный шаг).
- Held-state через `kbd_raw` (готово).
**Фаза K3 — коллизия с картой (твой п.3)**
- `check_on_floor`/`start_fall` — падение в ямы (тип тайла под ногами из
`fg[]`); `in_wall`/стоп у стены; `fell_out` (край экрана — пока без
перехода комнат).
- Падения/приземления (seq 7/17/19/20) + `fall_accel/fall_speed`.
**Фаза K4 — прыжки и повисание (твои а-прыжок, в)**
- run_jump (4), jump_up (28/14), grab (8/16/24), climb_up (10)/down (68),
hang (25/6), release (11/23). Это самый «PoP-овый» кусок — сверять
дистанции/тайминг с оригиналом (не на глаз).
**Фаза K5 — прочее (твой г)**
- drink (78), level_door (70), crouch_hop (79), spiked/loose/chomped
(ловушки, если тайлы есть в комнате), death (71).
Бой (меч, seq 55-92, стражники — seg005) — ВНЕ этого плана (отдельная фаза
полного приложения, PORT_PLAN §7 Фаза 3).
---
## 5. Риски/решения ДО кода (правило defer_unexplained_quirks)
1. **Per-frame offset** (§2.1) — решить до K0 (влияет на формат данных).
Рекомендация: xco/yco в frame_table, прямой blit.
2. **seqtbl rebasing** — JMP-адреса абсолютные (SEQTBL_BASE 0x196E); при
порте пересчитать в оффсеты своего массива. Проверить на 1-2 seq.
3. **Тайминг** — оригинал (DOS) фиксированный тик; наш 50 Гц. Если
логическая частота кадров иная — пересчёт dx/dy (PORT_PLAN §8.4).
Сверять дистанцию бега/прыжка с эталоном.
4. **Число реально нужных кадров/последовательностей** для Фазы 1 —
разметить (вырезать бой/катсцены/гардов), чтобы не тянуть все 219
спрайта и весь seqtbl.
5. **Копирайт графики Kid** — подтверждено решение пользователя (§вводная).
---
## 6. Что переиспользуем (готово)
- Фон комнаты (`pop_bg.c`) — Kid рисуется ПОВЕРХ (сейчас — прямым blit;
heal против фона — когда/если понадобится через RAM-копию, фон её уже
заполняет, `GFX_BANK_TRANSPARENT`).
- `kbd_raw` held-state (§2 PORT_PLAN) — готов и проверен.
- Пакер спрайтов/палитра (`pop_pack_bg.py`) — шаблон для `pop_pack_kid.py`.
- Разобранная карта комнаты (`fg[]/bg[]`, level.h) — для коллизий.
- `gfx_blit`/`gfx_w0_map` из W0-атласа — проверенный путь (bgtest/roomtest).
@@ -0,0 +1,291 @@
# Формат ресурсов Prince of Persia (MS-DOS, каталог `MSDOS/`)
Документ описывает бинарный формат `*.DAT`-файлов ресурсов DOS-версии PoP.
Исходников для этой версии нет, поэтому всё, что ниже — результат
структурного (эмпирического) анализа реальных файлов из `MSDOS/`, а не чтения
кода. Уровень уверенности указан для каждого раздела. Все находки проверены
скриптами (Python), которые разбирают файл и валидируют согласованность
(например: смещение+размер последней записи таблицы точно совпадает с
началом самой таблицы — то есть данные и каталог стыкуются без дыр).
Для справки при последующей реализации (порт на ZX Sprinter) стоит держать в
уме два внешних проекта:
- **SDLPoP** (github.com/NagyD/SDLPoP, GPLv3) — open-source реализация
DOS-версии на основе дизассемблирования оригинального `PRINCE.EXE`. Содержит
рабочий код чтения `.DAT`-файлов и полный кодек изображений/уровней. Точные
структуры (`dat_table_type` и т.п.), процитированные ниже, получены через
автоматический пересказ содержимого файла третьей стороной, а не через
прямое чтение исходника — поэтому такие детали помечены как "требует сверки
при реализации", в отличие от эмпирически подтверждённых байтовых оффсетов.
- **Princed Resources / PR** (github.com/NagyD/PR, princed.org, GPLv2) — это
профильный инструмент именно для распаковки/запаковки `.DAT`-ресурсов PoP
(версии DAT 1 и 2), сделанный тем же автором. В его документации
(`doc/Dataformats.md`) официально описаны экспортные форматы ресурсов —
это подтверждает и уточняет часть находок ниже (см. §3–4), и является более
надёжным источником, чем самостоятельная догадка по байтам.
**Важная находка:** репозиторий SDLPoP в папке `data/` содержит не только
код движка, но и **реальные ресурсы игры** — как сырые `.DAT`-контейнеры, так
и уже распакованные поштучно файлы (PNG-кадры спрайтов, `.pal`-палитры,
`.bin`-дампы уровней), см. §7. Это готовый источник ассетов и одновременно
независимая проверка формата, описанного в этом документе.
---
## 1. Общий контейнер `.DAT` (уверенность: высокая, подтверждено на 28 файлах)
Каждый `*.DAT`-файл (кроме служебных `config.dat`/`setup.dat`, см. §5) — это
простой архив-контейнер: блок данных + оглавление (каталог ресурсов) в конце
файла.
### 1.1 Заголовок файла (6 байт, смещение 0x00)
| Смещение | Размер | Поле | Значение |
|----------|--------|--------------|----------|
| 0x00 | 4 | `tableOffset`| LE u32. Абсолютное смещение в файле, с которого начинается таблица оглавления. Совпадает с "концом данных". |
| 0x04 | 2 | `tableSize` | LE u16. Размер таблицы оглавления в байтах. |
Инвариант, подтверждённый на всех 28 `.dat`-файлах в каталоге:
```
tableOffset + tableSize == размер файла (без исключений)
```
Данные ресурсов идут сразу после заголовка, начиная с байта 0x06, и
заканчиваются на `tableOffset`.
### 1.2 Таблица оглавления (по смещению `tableOffset`, длиной `tableSize`)
Таблица — плоский массив записей по 8 байт. Количество записей:
`tableSize / 8` (округление вниз; в файле почти всегда остаётся 2 "лишних"
байта в хвосте таблицы — назначение не установлено, вероятно, служебное поле
инструмента-упаковщика или паддинг; на итоговый разбор не влияет).
Запись (8 байт):
| Смещение в записи | Размер | Поле | Описание |
|---|---|---|---|
| 0 | 2 | `size` | LE u16 — размер данных ресурса в байтах |
| 2 | 2 | `id` | LE u16 — идентификатор ресурса |
| 4 | 2 | `offset` | LE u16 — **абсолютное** смещение данных ресурса в файле (не относительное!) |
| 6 | 2 | `reserved` | во всех проверенных записях (сотни штук) всегда `0x0000` |
Проверено на `levels.dat`: 16 записей, `id`=2000..2015, и `offset[i] + size[i]
== offset[i+1]` для всех соседних записей, а последняя запись заканчивается
ровно на `tableOffset` — то есть данные абсолютно плотно упакованы, без
пробелов, для этого файла. В других файлах (например `guard.dat`) между
записями изредка есть небольшие зазоры в несколько байт (вероятно, выравнивание
или "мёртвые" байты от инструмента-компоновщика) — не является нарушением
формата.
### 1.3 Диапазоны `id` по типам файлов (собрано эмпирически)
Похоже, что числовые ID образуют условные "пространства имён" по типу
контента — вероятно, глобальные константы в оригинальном коде:
| Файл(ы) | Диапазон `id` | Кол-во записей | Предполагаемое содержимое |
|---|---|---|---|
| `levels.dat` | 20002015 | 16 | id=2000 — служебный блок (16 байт, см. §3); id=2001..2015 — 15 уровней |
| `guard.dat`, `fat.dat`, `skel.dat`, `shadow.dat` | 750–784 (варьируется) | ~3035 | id=751(750) — служебный блок; остальные — кадры анимации спрайта |
| `vizier.dat` | аналогично guard | — | кадры анимации визиря |
| `kid.dat` | ~400+ | 220 | кадры анимации игрока (намного больше — герой умеет гораздо больше действий) |
| `guard1.dat`, `guard2.dat` | 750 (1 запись) | 1 | вероятно, дополнительные/альтернативные кадры/варианты |
| `title.dat` | 40–55 | 12 | картинки титульного экрана/логотипов |
| `cpalace.dat`,`epalace.dat`,`vpalace.dat`,`cdungeon.dat`,`edungeon.dat`,`vdungeon.dat` | 2001343 | 205238 | фоновые тайлы дворца/подземелья, отдельно для CGA(`c*`)/EGA(`e*`)/VGA(`v*`) |
| `pv.dat` | 800981 | 103 | доп. графика (возможно, "Prince/Vizier" катсцены) |
| `digisnd1/2/3.dat` | 10000+ | 20–44 | оцифрованный звук (Covox/Disney Sound Source) |
| `midisnd1/2.dat` | 10024+ / аналог | 16 | General MIDI музыка |
| `mt32snd1/2.dat` | 10000+ | 24/7 | музыка для Roland MT-32 |
| `ibm_snd1/2.dat` | 10000+ | 44 | музыка/эффекты через PC-спикер |
| `prince.dat` | — (1 крупный ресурс) | — | MIDI-тема (вероятно, финальная тема "Принц"/титры — см. текстовые события "The Princess awaits") |
Во всех файлах первая (наименьшая по `id`) запись — маленький "служебный"
ресурс (6–44 байта), стоящий перед основным контентом. Скорее всего это
локальная мини-таблица/палитра/список ссылок для данного набора ресурсов —
по аналогии с тем, что у уровней id=2000 отдельно от самих уровней (см. §3).
---
## 2. Формат уровня (`levels.dat`, id=2001..2015) — уверенность: высокая
Каждая запись уровня имеет размер **2305 байт** и по данным полностью
совпадает по объёму с уровнями из Apple II версии (`01 POP Source/Levels/LEVELn`
— ровно **2304 байта** каждый, см. `docs/APPLEII_RESOURCE_FORMAT.md`).
Вывод: формат карты уровня в DOS-версии, судя по всему, **унаследован
практически без изменений от оригинального Apple II формата** (Джордан
Мехнер писал игру на 6502 и данные уровней переносились как есть), с добавлением
одного лишнего байта в DOS-упаковке (2304+1=2305 — вероятно, контрольный байт/
маркер конца, добавленный DOS-упаковщиком ресурсов, а не часть игровых данных).
**Практическое следствие:** байтовая структура самого уровня (тайлы 3×10 на
экран, 24 экрана, таблицы стражников, дверей и т.д.) должна документироваться
один раз — по исходникам Apple II (см. соответствующий раздел), и напрямую
применяться к DOS `levels.dat`, отбросив 1 лишний байт в конце каждой записи.
Байтовые значения тайлов в дампе (в основном 0x00–0x39) визуально согласуются
с диапазоном небольших целых кодов тайлов, что для формата карты и ожидается.
Первая запись, id=2000, размер 16 байт — не уровень, а отдельный маленький
блок (возможно: количество уровней, начальный уровень, версия формата,
стартовые координаты игрока/охраны по умолчанию). Точное назначение не
установлено — требует сопоставления с диз­ассемблированным кодом загрузчика
уровней (в SDLPoP это, по всем признакам, отдельная процедура чтения
`level` ресурса).
**Сверка с независимой распаковкой SDLPoP (`data/LEVELS/`):** там лежат файлы
`res2000.bin``res2015.bin` (16 штук — количество совпадает). Байты
`res2001.bin` содержательно совпадают с тайловыми данными нашей записи
id=2001 (та же последовательность значений тайлов) — это подтверждает, что
нумерация id верна. Но есть нестыковка по размеру: у SDLPoP `res2000.bin`
**2305 байт** (как и все остальные), тогда как в нашем локальном
`levels.dat` запись id=2000 — всего **16 байт**. Скорее всего, это разные
релизы/сборки игры (см. §7 — размеры некоторых `.dat` у SDLPoP и у нас уже
отличались), и в версии SDLPoP маленький служебный блок либо отсутствует,
либо пронумерован иначе. Это не меняет сам формат контейнера, но означает,
что **точную семантику 16-байтного блока id=2000 в нашей копии игры пока
нельзя проверить через данные SDLPoP** — открытый вопрос.
---
### 2.1 Кросс-подтверждение по исходникам Apple II
Фоновый анализ исходников Apple II (см. `docs/APPLEII_RESOURCE_FORMAT.md`)
подтверждает и объясняет структуру уровня напрямую по коду. Уровень на Apple
II — дамп структуры `blueprnt` (`EQ.S`): `BLUETYPE`(720Б, 24 экрана×30 тайлов)
+ `BLUESPEC`(720Б) + `LINKLOC`(256Б) + `LINKMAP`(256Б) + `MAP`(96Б, граф
соседних экранов) + `INFO`(256Б, метаданные/старт Кида/стражников) = ровно
2304 байта. Учитывая, что DOS-запись уровня — это ровно 2304+1 байт с
байтовыми значениями тайлов, укладывающимися в диапазон 0–29 (id тайла) плюс
служебные биты (аналогично `idmask=%00011111`, `reqmask=%00100000` из
`EQ.S:484-486`), можно с высокой уверенностью считать, что **DOS-версия
использует ту же самую раскладку `blueprnt`**, лишь с добавлением одного
байта (вероятно, контрольной суммы) в конце DOS-упаковки. Это снимает
необходимость отдельно реверсить формат уровня для DOS — таблица тайлов,
enum id (0=space...29=archtop4), формат `LINKLOC`/`LINKMAP` и `INFO` из
Apple II документа применимы напрямую.
## 3. Графика (спрайты и фоновые тайлы) — уверенность: средняя/низкая
Файлы `kid.dat`, `guard.dat`, `fat.dat`, `shadow.dat`, `skel.dat`,
`vizier.dat`, `title.dat`, `c/e/v-palace.dat`, `c/e/v-dungeon.dat`, `pv.dat`
хранят по контейнерному формату (§1) множество мелких чанков (десятки—сотни
байт каждый).
Что подтверждено:
- Наборы `shadow.dat`/`kid.dat` и `fat.dat`/`vizier.dat` содержат **побайтово
идентичные фрагменты** данных в начале файла — это ожидаемо: "Тень" (Shadow)
визуально копирует анимацию Кида, а "Толстый страж" (Fat guard, пасхалка)
переиспользует модель Визиря. Подтверждает, что персонажи одного "типа
тела" используют общий набор геометрии/анимации.
- Отдельные чанки *не* имеют очевидного унифицированного заголовка
(высота/ширина/палитра) фиксированного размера — попытка интерпретировать
первые байты чанка как `{height:u16, width:u16, flags:u16}` не подтвердилась
на реальных данных (получаются нереалистичные размеры для маленьких чанков).
Вероятно, как и в Apple II версии (см. `FRAMEDEF.S`/`SEQTABLE.S`), геометрия
кадра (ширина, высота, точка привязки) хранится **отдельно от самих
пиксельных данных** — в таблицах внутри `PRINCE.EXE`, а не в `.DAT`-чанке.
Сам чанк, вероятно, содержит только упакованные пиксельные данные
(RLE/дельта-упаковка, по аналогии с `UNPACK.S` в Apple II исходниках).
- Точный алгоритм упаковки пикселей **не восстановлен** в рамках этого
анализа по сырым байтам — байт-в-байт разбор распаковщика без
дизассемблирования `PRINCE.EXE` надёжно не сделать. **Но для практических
целей это не требуется**: см. §7 — в SDLPoP уже есть тот же самый набор
изображений в готовом, распакованном виде (PNG), которым можно пользоваться
напрямую как источником ассетов, не реализуя свой декодер `.DAT`-пикселей.
Писать собственный декодер имеет смысл только если понадобится читать
оригинальные `.DAT` "на лету" (например, для точной сверки контента именно
нашей копии игры) — тогда ориентир — исходник SDLPoP (`src/seg009.c`).
---
## 4. Звук — уверенность: высокая (по структуре), низкая (по деталям кодека)
Обнаружено 4 параллельных набора звуковых ресурсов под разные звуковые
устройства DOS-эпохи — типично для игр начала 1990-х с "звуковым меню":
| Файл | Устройство | Формат чанка |
|---|---|---|
| `midisnd1.dat`, `midisnd2.dat` | General MIDI / MPU-401 | каждый чанк = 2-байтовый LE-префикс длины + встроенный Standard MIDI File (`MThd`...`MTrk`...) |
| `mt32snd1.dat`, `mt32snd2.dat` | Roland MT-32/CM-32L | тот же формат: префикс длины + `MThd`/`MTrk`, с MT-32-специфичными SysEx (видны строки `MT-32.mff`, текстовые мета-события вроде `"The Princess awaits"`) |
| `prince.dat` | (аналогично MIDI) | отдельный крупный музыкальный ресурс, тот же MIDI-контейнер — вероятно, финальная тема |
| `digisnd1/2/3.dat` | Covox / Disney Sound Source / Sound Blaster (оцифрованный звук) | чанк начинается с нескольких служебных байт, среди которых слово `0x2AF8` = 11000 — похоже на частоту дискретизации 11 кГц; далее — сырые 8-битные PCM-сэмплы (значения кластеризуются вокруг ~0x7A–0x90, типично для беззнакового 8-бит аудио, смещённого к середине шкалы) |
| `ibm_snd1.dat`, `ibm_snd2.dat` | PC Speaker | чанк — последовательность троек байт похожих на (длительность, делитель_частоты) — простой формат "бипера", отличный от MIDI |
Подтверждено разбором первых чанков в каждом файле (см. байтовые дампы,
проверялись скриптом). Точная семантика полей внутри `digisnd`/`ibm_snd`
(разрядность, порядок байт служебного заголовка) не выведена до конца — при
реализации порта достаточно распознавания по типу файла и (для MIDI-семейства)
можно напрямую воспроизводить встроенный Standard MIDI File, пропустив
2-байтовый префикс длины.
---
## 5. Готовые распакованные ассеты в SDLPoP (`data/`) — практический источник для порта
Репозиторий github.com/NagyD/SDLPoP содержит папку `data/`, где, помимо
самих `.DAT`-контейнеров, каждый ресурс **продублирован в виде отдельно
распакованного файла**, названного по его `id` из таблицы оглавления (§1.2).
Проверено через GitHub API (`api.github.com/repos/NagyD/SDLPoP/contents/...`):
| Подпапка/файл в `data/` | Содержимое | Соответствие нашему разбору |
|---|---|---|
| `GUARD.DAT`, `GUARD1.DAT`, `GUARD2.DAT` | сырые `.DAT` | размер **побайтово совпадает** с нашими локальными `guard.dat`/`guard1.dat`/`guard2.dat` (6950 / 117 / 117 байт) |
| `DIGISND1.DAT`, `MIDISND2.DAT` и др. | сырые `.DAT` | размер **не совпадает** с нашими локальными файлами (48545 vs 50101, 18408 vs 18958) — другой релиз/сборка игры |
| `GUARD/res751.png``res784.png` | готовые PNG, по одному на кадр анимации, имя = `res<id>.png` | id-диапазон (751-784) точно совпадает с нашим разбором `guard.dat` |
| `VPALACE/res200.pal`, `res201.png`, `res202.png`, … | палитра (JASC `.pal`) + PNG-кадры фонов дворца, **VGA-вариант (256 цветов)** | id-диапазон (200+) совпадает с `vpalace.dat` |
| `LEVELS/res2000.bin``res2015.bin` | сырые дампы уровней по 2304-2305 байт | id совпадает с `levels.dat`; содержимое `res2001.bin` **сверено побайтово** с нашим id=2001 — тайловые данные совпадают |
| `KID/`, `PRINCE/`, `SHADOW/`, `SKEL/`, `VIZIER/`, `FAT/`, `TITLE/`, `VDUNGEON/`, `PV/`, `IBM_SND1/`, `IBM_SND2/`, `font/`, `music/` | аналогичные наборы для остальных ресурсов | не проверялись по отдельности, но структура (папка на каждый `.dat`, файлы `res<id>.ext`) наблюдается одинаково |
**Вывод:** это данные из немного **другого релиза DOS-версии**, чем те, что
лежат у нас в `MSDOS/` (см. расхождение в размере `digisnd`/`midisnd`), но
формат контейнера и нумерация `id` — те же самые. Практически это значит:
1. Для получения играбельных PNG-спрайтов и VGA-фонов **не нужно
реализовывать декодер сжатия пикселей** — можно взять готовые файлы
`data/<ИМЯ>/res<id>.png` напрямую как исходный материал для конвертации
под видеорежим ZX Sprinter (в т.ч. `VPALACE`/`VDUNGEON` — уже
256-цветный VGA-арт, что прямо отвечает на вопрос про полноцветность).
2. Если в проекте важно использовать именно ту версию контента, что в наших
`MSDOS/*.dat` (а не версию из SDLPoP) — распаковку своих файлов всё же
придётся делать (кодек пикселей по-прежнему не восстановлен для сырых
`.DAT`, см. §3), либо принять решение работать с версией SDLPoP как
мастер-источником ассетов вместо своей.
---
## 6. Служебные не-ресурсные файлы
- `config.dat`, `setup.dat` — 28 байт, не являются ресурсными контейнерами
(не проходят проверку §1.1 — "размер" получается больше самого файла).
Скорее всего простые бинарные структуры настроек (звук/видеорежим,
выбранный на этапе `SETUP.EXE`/`INSTALL.EXE`), не связаны с игровым
контентом.
- `desktopd.cfg`, `setup.cfg` — текстовые/бинарные конфиги DOS-инсталлятора,
вне скоупа игровых ресурсов.
- `PRINCE.EXE` / `PRINCE.REM` — почти идентичны (отличие в единичных байтах
в районе смещения ~0x4ED0), похоже на кряк/патч одного байта проверки —
не относится к формату ресурсов.
- `old-games.nfo` — ASCII-арт NFO релиз-группы (old-games.ru), не игровые
данные.
---
## 7. Итоговая таблица уверенности
| Раздел | Уверенность | Как подтверждено |
|---|---|---|
| Контейнер `.DAT` (заголовок + таблица) | Высокая | Проверено скриптом на всех 28 файлах, инвариант offset+size выполняется без исключений; независимо подтверждено именованием `res<id>.*` в SDLPoP `data/` |
| ID-пространства ресурсов | Средняя-высокая | Наблюдение по диапазонам + сверка с `res<id>` именами файлов SDLPoP и побайтовым содержимым `res2001.bin` |
| Формат уровня = формату Apple II | Высокая (по размеру и содержимому), служебный блок id=2000 — открытый вопрос | Совпадение размера (2304 vs 2305), тайловые байты сходятся с `res2001.bin` из SDLPoP |
| Формат изображений/спрайтов (сырой `.DAT`) | Низкая-средняя | Контейнер подтверждён, кодек пикселей — нет; но практически закрыто наличием готовых PNG в SDLPoP `data/` (§5) |
| Формат звука (тип контейнера) | Высокая для MIDI-семейств, средняя для digisnd/ibm_snd | Явные MIDI-сигнатуры `MThd`/`MTrk` видны в байтах |
**Рекомендация для дальнейшей работы:** для получения арт-ассетов (спрайты,
фоны, палитры) — использовать готовые распакованные файлы из
`github.com/NagyD/SDLPoP/tree/master/data` (§5), это быстрее и надёжнее
самостоятельной реализации декодера. Декодер сырого `.DAT`-формата
изображений и точную семантику служебных полей `digisnd`/`ibm_snd`
(§3, §4) стоит восстанавливать только если понадобится читать именно нашу
локальную копию `MSDOS/*.dat` "как есть" — тогда ориентир прежний: исходник
SDLPoP (`src/seg009.c`, `src/data.c`/`data.h`).
+509
View File
@@ -0,0 +1,509 @@
# Prince of Persia на ZX Sprinter — план порта
Статус: план (2026-07-15). §2 (A: kbd_mod_state / B: kbd_raw) —
РЕАЛИЗОВАНО и частично проверено в MAME (tests/kbdraw, 2026-07-15,
подробности в §2.2); PoC (§5) и остальные фазы — не начаты. Опирается на
`APPLEII_RESOURCE_FORMAT.md` / `MSDOS_RESOURCE_FORMAT.md` / `README.md` в
этой папке, на текущий sprinter-cc/libc/libbgi (см. §1) и на локальные копии
`applications/PoP/SDLPoP` (github.com/NagyD/SDLPoP, GPLv3) и
`applications/PoP/PR` (github.com/NagyD/PR, GPLv2) — используются только как
справочник по структурам/константам оригинального движка и как источник
готовых распакованных ассетов (`SDLPoP/data/`), не как код для копирования.
---
## 1. Что уже есть в sprinter-cc и библиотеках (используем как есть)
Собрано из `docs/TODO.md`, `docs/libc-reference.md`, `docs/sprite-api-design.md`,
`libbgi/include/{gfx.h,sprite.h,graphics.h}`, `examples/rpgwalk`.
- **Графика 320×256×256** (`GFX_MODE_320x256x256`, режим 0x81) — разрешение и
глубина цвета совпадают почти впрямую с VGA-ассетами оригинала
(`SDLPoP/data/VPALACE`, `VDUNGEON` — уже 256-цветные PNG). Не нужно ужимать
в EGA/CGA палитру.
- **BGI-слой** (`graphics.h`) — примитивы, палитра, текст, `getimage/putimage`
— Фазы 1-2d готовы и проверены в MAME.
- **Спрайтовый движок v2** (`sprite.h`, ветка `sprite-engine-v2`) — ровно то,
что нужно персонажам PoP:
- retained-модель (`sprite_update`/`sprite_flip`, double-buffer, dirty-биты,
heal+blit за один проход);
- кадровая анимация по ленте (`sprite_anim`, LOOP/PINGPONG/ONCE,
горизонтальная/вертикальная лента) и tween-перемещение
(`sprite_moveto`, DDA без knowledge-heavy арифметики);
- Y-сортировка слоями (`gfx_sprite_ysort`, `layer`) — то, что нужно для
«Кид перед/за стражником» без ручной пересортировки;
- атласы в EMM-страницах (`atlas_t`/`atlas_load`) — на восьмерых
персонажей в `rpgwalk` уже работает: прямой прецедент для Кида/стражника;
- ограничение кадра ≤ 64×64 — с запасом (см. §3: кадры Кида в оригинале
~12-30 × 39-42 px).
- **Frame pacing** (`gfx_set_fps_div`) + цепочка кадровых IRQ — стабильный
логический тик независимо от рендер-нагрузки экрана (проверено MAME).
- **EMM-бюджет**: ~3.3 МБ свободно на старте (`memory/sprinter_emm_budget`) —
с большим запасом на все спрайт-атласы и предрендеренные фоны комнат (см.
§4) даже без выгрузки неиспользуемых уровней.
- **Файловый ввод-вывод** (FILE* v2, `fopen/fread/...`) — для загрузки
уровней/атласов/палитр с дискеты, по образцу `rpgwalk` (`atlas_load`,
`gfx_pal_fload`).
- **Клавиатура (событийная)** — `kbhit/getch/getkey` (ASCII + `KEY_*` скан-код
для стрелок), см. §2 — это НЕ то, что нужно для управления Кидом один в
один (см. ниже).
- **Звук** — `cbl.h` (потоковый CBL/COVOX, callback-модель, verified MAME) —
подходит для оцифрованных эффектов (`digisnd*.dat` — PC-звук
~11 кГц 8-бит, см. `MSDOS_RESOURCE_FORMAT.md` §4).
Вывод: **движок отрисовки и анимации почти не требует нового кода**
самый близкий по духу пример (`rpgwalk`: атласы, анимация, tween, дабл-буфер,
FPS-делитель) переносится на PoP почти без изменений архитектуры.
---
## 2. Единственный принципиальный пробел: удержание клавиш
**Спайк проведён (2026-07-15), вопрос закрыт артефактами — не догадкой.**
`getch`/`getkey` — это события ESTEX WAITKEY/SCANKEY (по нажатию), без чёткой
информации о СОСТОЯНИИ (что зажато прямо сейчас, несколько клавиш
одновременно). Prince of Persia на управлении требует именно состояния:
держать направление (бег) + одновременно нажать вверх (прыжок вперёд), держать
Shift (модификатор) + направление и т.д.
### 2.1 Находки
1. **`docs/converted/ProgrammerManual.txt` документирует функцию, которую мы
раньше пропустили: `CTRLKEY` (ESTEX $33h)** — «Получить состояние
клавиатуры». Дословно: «данные берутся не из буфера клавиатуры (как в
остальных функциях), а непосредственно из результатов ПОСЛЕДНЕГО
сканирования» — то есть это НАСТОЯЩЕЕ live-state, не событие. Но
покрывает только модификаторы: Left/Right Shift, Ctrl, Alt,
Rus/Lat, Num/Scroll/Caps Lock, Insert (не обычные клавиши вроде стрелок).
Готовое решение для «держать Shift = бежать» — тривиальная обёртка,
без архитектурных рисков.
2. Для ОБЫЧНЫХ клавиш (стрелки, буквы) такого live-state нет нигде в ESTEX —
`WAITKEY`/`SCANKEY`/`TESTKEY` ($30/$31/$37h) — все три отдают ОДИНАКОВЫЙ
формат «очередное нажатие», без release. `TESTKEY` не удаляет событие из
буфера (полезно для «подсмотреть, не потребляя»), но это тоже разовое
нажатие, не состояние.
3. Автоповтор клавиатуры (typematic) не годится как замена held-state:
`MAME_MCP_GUIDE.md` фиксирует задержку до первого повтора ~1 секунда
(типично для PS/2) — на порядок медленнее кадра (20 мс), не подходит для
платформера.
4. **Решающий артефакт — `libc/irq/_irq_tramp.c` (сам трамплин прерывания,
не гипотеза):** вектор 0xFF общий для кадра/клавиатуры/CBL. Ветка
клавиатуры (бит 0 порта 0x19 = SIO-A RR0 «байт принят») делает буквально
`jp 0x0038` (прямиком в DSS) **до какого-либо чтения порта данных 0x18 И
до нашей кадровой цепочки (`_irq_chain`)** — наш `irq_chain_add`
вообще не видит клавиатурные прерывания, они физически не доходят до
цепочки (см. `tr_notkbd`/`tr_frame` разбор в файле). Значит текущая
инфраструктура (тот же механизм, что несёт FPS-делитель) НЕ дает
зацепки для клавиатуры без правки самого трамплина.
5. Регистр данных SIO (порт 0x18) — аппаратный приёмный буфer, чтение
деструктивно (дёргает байт из очереди); кто прочитал первым, тот и
владеет байтом. Значит «подглядеть, не мешая DSS» технически
невозможно — необходимо либо совсем не трогать этот путь (статус-кво),
либо взять его СЕБЕ полностью на время геймплея.
### 2.2 Рекомендация (конкретная, не три равнозначных варианта)
**A. Тривиально, почти без риска — обернуть `CTRLKEY` ($33h)** отдельной
функцией (например `kbd_mod_state()` в `<conio.h>`) — даёт настоящий
held-state для Shift/Ctrl/Alt. Можно делать хоть сейчас, не архитектурное
решение.
**B. Для обычных клавиш (стрелки и т.д.) — по прецеденту CBL.** В
`_irq_tramp.c` уже есть пример «приватного» пути на том же векторе 0xFF,
который сознательно НЕ чейнится к DSS (CBL: бит 7 порта 0xFE, свой
хук `_irq_cbl_hook`, полный сейв, свой `reti`). Предлагаемый новый
компонент `<kbd_raw.h>` — симметричный: ветка по биту 0 порта 0x19 читает
порт 0x18 САМА (декодирует PS/2 make/break, `0xF0`-префикс — протокол
уже задокументирован в `docs/samples/sprinterKeybLib.asm`), ведёт битовую
карту «клавиша N зажата», и НЕ прыгает в DSS, пока путь активен —
жизненный цикл `kbd_raw_open()`/`kbd_raw_close()` один в один как у
`cbl_open`/`cbl_close`.
**Важное следствие (сообщить пользователю явно, не прятать):** пока
`kbd_raw_open()` активен, DSS вообще не получает клавиатурных байт —
`kbhit/getch/getkey/CTRLKEY` заведомо не будут работать, ESC для выхода
в DSS-смысле тоже (нужно проверять raw-битовую карту самим). Это
нормально для активной фазы геймплея (у самой игры и так свой цикл
ввода), но означает: экраны/паузы, которым нужен ESTEX-ввод (например,
диалог сохранения через `fopen`, если тот когда-либо потребует ввода
с консоли), должны на это время `kbd_raw_close()`.
**Не рекомендую вариант «таймаут-эвристика поверх SCANKEY»** — after
находки о typematic-задержке ~1с он не даёт нужной задержки для игры;
рекомендация A+B закрывает потребность без компромиссов.
**Статус: A+B РЕАЛИЗОВАНЫ (2026-07-15, по согласованию с пользователем).**
- A: `kbd_mod_state()``libc/conio/kbd_mod_state.c` + `<conio.h>`
(`KBD_MOD_*`).
- B: `<kbd_raw.h>` (`libc/kbd/`) + правка `libc/irq/_irq_tramp.c`
(новая ветка на бите 0 порта 0x19: raw активен → сама читает порт
0x18, декодирует make/break, НЕ чейнится к DSS; raw выключен —
поведение как раньше, без изменений). Трамплин вырос со 150 до
220 байт — `_IRQ_TRAMP_BUF_SIZE` поднят с 224 до 288 (было 4 байта
запаса, стало ≥60). `make -C libc` (fast+safe) — чисто.
- **Верификация в MAME** (`tests/kbdraw`, полный цикл open→держать→
отпустить→ESC-выход→close): `KBD_LEFT` (0x16B, расширенный код
E0 6B) — down на нажатие, up на отпускание, ТОЧНО совпало с
константой из `<kbd_raw.h>`; `KBD_ESC` (0x76, обычный код) —
корректно закрыл raw-канал и вернул DSS (`IM` вернулся в 1).
Побочно найдено и задокументировано в `docs/libc-reference.md`
(`<kbd_raw.h>`): MAME-мостовой `press_key` дёргает ОБЕ клавиатуры
(PC+ZX) одновременно и через ZX-путь давал паразitный незатухающий
бит — не относится к реальному сценарию (пользователь подтвердил:
матрица на Sprinter давно не используется), но означает, что
будущие MAME-тесты этой функции надо гонять через `:kbd:ms_naturl:*`
напрямую, не через удобный `press_key`. UP/DOWN/RIGHT/SPACE/SHIFT
константы — НЕ перепроверены поштучно (тот же общеизвестный
стандарт PS/2 Set 2, что и подтверждённые LEFT/ESC — проверить перед
использованием в PoC, если управление будет ощущаться неверно).
- На реальном железе — не проверено (только MAME).
---
## 3. Формат данных — что напрямую переносим из docs/*RESOURCE_FORMAT.md
- **Уровень** (`BLUETYPE`/`BLUESPEC`/`LINKLOC`/`LINKMAP`/`MAP`/`INFO`,
2304 байта, 24 экрана × 30 тайлов) — читаем один раз при загрузке уровня
в свою C-структуру (прямой memcpy дампа файла, поля читаем по офсетам
из `APPLEII_RESOURCE_FORMAT.md` §1). DOS `levels.dat` даёт то же самое
+1 байт в конце записи — отбросить.
- **Графика фона/спрайтов** — кодек сжатия DOS `.DAT` не восстановлен и
восстанавливать не будем: используем уже распакованные PNG из
`SDLPoP/data/{KID,GUARD,VPALACE,VDUNGEON,...}` (см.
`MSDOS_RESOURCE_FORMAT.md` §5, §7 — тот же контейнерный формат/нумерация,
просто другой релиз сборки данных). Измерено локально: кадры Кида —
~12×39 .. 30×42 px (P-режим, 4-бит палитра), фоновые тайлы подземелья —
32 px по ширине (10 колонок × 32 = 320 — сходится с шириной экрана), высота
тайла 20/60/62 px (неоднородные ряды пола/потолка/арок) — укладывается в
лимит спрайтового движка (кадр ≤ 64×64) без всяких изменений движка.
- **Звук** — `digisnd*.dat` (PC-звук 8-бит ~11 кГц) — конвертация в сырой
PCM и проигрывание через `cbl_open`/`cbl_push_*`; `ibm_snd*.dat` (PC-спикер
тройки «частота×2Б + длительность») — тривиальный бипер, не требует CBL.
MIDI-семейство (`midisnd`, `mt32snd`, `prince.dat`) — вне скоупа (нет
синтеза MIDI на платформе; не блокирует геймплей).
---
## 4. Стратегия фона — ПЕРЕСМОТРЕНО 2026-07-15: тайловый рендерер В РАНТАЙМЕ
**Было** (первая версия плана): офлайн-склейка каждой комнаты в готовую
растровую картинку 320×~193, `gfx_blit` целиком при входе — обоснование
было «ноль нового кода в libbgi». Пересчёт по факту наличия структурных
данных комнаты (§3.4 формата, `level.h`) показал: 16 уровней × 24 комнаты ×
~60-80 КБ/картинка — это **30+ МБ**, при том что одна и та же картинка
тайла (пол/стена/колонна) переиспользуется в десятках комнат — офлайн-
склейка печёт её заново в каждую копию.
**Стало**: тайлы — переиспользуемый набор картинок ОДИН на визуальный
стиль (не на комнату), структурные данные комнаты — компактные (60 байт:
30×foretable+30×backtable, все 16 уровней ≈ 37 КБ, см. `level.h`).
`room_draw()` (applications/PoP/poc/room.c) проходит 30 тайлов комнаты и
зовёт `gfx_blit` для каждого, читая картинку из таблицы по типу тайла
(`tile_images[TILE_TYPE]`). Итог: десятки-сотни КБ переиспользуемых
тайл-картинок на весь визуальный стиль + ~37 КБ структуры уровней —
вместо 30+ МБ.
**Почему это НЕ бьёт по бюджету кадра**: `room_draw()` зовётся ОДИН РАЗ
при входе в комнату (смена комнаты — не every-frame событие), не в
игровом цикле — это не `sprite_update`, тактовый бюджет кадра не
затронут.
Анимированные тайлы (факел, шипы, дверь-плита) по-прежнему рисуются как
отдельные `sprite_t` поверх фона — движок это уже умеет (Y-order/layers,
dirty-биты, heal против фона через ОЗУ-копию); `room_draw()` кладёт в
ОЗУ-копию именно статичную геометрию (пол/стены/колонны Фазы 1 — §5.2),
поверх неё heal спрайтов работает как обычно.
`toolchain/room_compose.py` (генерик-компоновщик тайлов в одну картинку,
§6.1) остаётся полезным ИНСТРУМЕНТОМ конвертации отдельных тайл-картинок
(PNG → getimage raw), просто теперь его выход — 32 маленьких файла
`tileNN.raw` (по одному на тип тайла), а не один большой файл на комнату;
сама раскладка/повторное использование по комнатам — в C-коде
(`room_draw`), не в офлайн-склейке.
---
## 5. Proof-of-Concept — цель: доказать, что порт вообще ощущается как PoP
**Объём**: одна комната (например Level 1, экран старта Кида), без
переходов между экранами, без стражников (стретч-цель, не обязательна).
**Что показываем**:
1. Кид на экране, с закреплённым офлайн-конвертированным набором кадров
(подмножество: idle, walk L/R, jump-начало/дуга/приземление, стоп-на-краю,
возможно повисание на краю) — атлас в W0-странице, по образцу `rpgwalk`.
2. Управление: держать влево/вправо — идёт; отпустил — тормозит/стоит;
нажатие вверх во время бега — прыжок вперёд (дуга по авторским таблицам
смещений, не по gravity-физике «с нуля» — см. §6). Здесь же проверяется
решение по §2 (реальный held-state).
3. Столкновения: пол/край экрана/провал — по факту чтения тайла из
`BLUETYPE` под ногами (без LINKLOC-триггеров пока).
4. Стабильный кадр 50 Гц через уже готовый `gfx_wait_vsync`/дабл-буфер
(без FPS-делителя — Кид анимируется каждый видеокадр, как в оригинале).
**Критерий успеха**: субъективно «прыжок ощущается как в PoP» (дистанция и
тайминг прыжка сверены с оригинальными таблицами, не подобраны на глаз —
см. §6), управление отзывчивое (не событийное с задержкой), сцена не мерцает
на стыке спрайт/фон.
**Не входит в PoC**: стражники/бой, звук, HUD/таймер, переходы между
комнатами, ловушки/триггеры, титры/меню, сохранения.
**Расположение**: `applications/PoP/poc/` (свой sprinter-cc проект + Python
конвертер ассетов, по структуре `examples/rpgwalk`).
### 5.1 Статус (2026-07-15) — первая итерация: управление + коллизия края
Сделано и проверено в MAME (`applications/PoP/poc/`, `make run`):
держать LEFT/RIGHT (`kbd_raw_down`, raw-канал из §2) — идёт непрерывно,
отпустил — стоит на месте (не событийно, реальный held-state);
столкновение с краями экрана (клип по `MINX`/`MAXX`); анимация
ходьбы/разворота лицом по направлению (`sprite_anim` пинг-понг);
дабл-буфер + `gfx_wait_vsync` — без видимого мерцания. Сборка —
`--memory huge` без `--bank` (§10, подтверждено рабочим).
**Важное отступление от плана (осознанно, не молча):** персонаж —
ВРЕМЕННАЯ заглушка (лицензированный спрайт-пак
`third_party/16x16-RPG-characters` через `tools/gen_kid_placeholder.py`,
тот же источник, что уже использует `examples/rpgwalk`), а НЕ
конвертированная графика оригинальной Prince of Persia. Причина:
исходный набор кадров Кида (`SDLPoP/data/KID`) — копирайт
Broderbund/Ubisoft; автоматический конвейер, который систематически
извлекает и переупаковывает его в новый формат, — это на практике
внутрипроектное решение, которое стоит принимать пользователю явно
для каждого шага, а не проводить асинхронно агентом без лишнего
подтверждения. Сама графика — не то, что проверяет PoC (§5 явно:
цель — ощущение управления/коллизий, не визуальная точность). Замена
на настоящую графику Кида — отдельный шаг, на усмотрение пользователя.
**Ещё не сделано** (следующие итерации §5): авторские таблицы
смещений кадров (§6 — движение при ходьбе линейное, px/кадр),
реальный уровень/фон по `BLUETYPE`/`LEVEL1` (сейчас — плейсхолдер:
плоский пол на весь экран, без ямы/выступа), `kbd_mod_state`/
Shift-бег не подключены к игровому циклу (обёртка готова с Фазы A).
**Прыжок/присед добавлены и ПРОВЕРЕНЫ (2026-07-15)**: состояние
`jumping`/`jump_t`/`crouching`, своя приблизительная дуга прыжка
(`jump_height[]`, 40 кадров) — не авторская таблица, см. §6.1.
HUD-текст статуса (нет отдельной позы).
Живое тестирование пользователем нашло реальный баг: держа UP чуть
дольше 0.8 с (длительность дуги), получали ДВА прыжка подряд — код
проверял `kbd_raw_down(KBD_UP)` как уровень (держится, пока клавиша
физически зажата), а не как фронт нажатия, поэтому в момент
приземления «UP всё ещё зажат» тут же триггерил новый прыжок.
Исправлено edge-detect'ом (`up_prev` — предыдущее состояние UP,
триггер только на переход 0→1). Проверено брейкпоинтом в отладчике
MAME на адресе входа в код прыжка: за одно длинное удержание UP
брейкпоинт срабатывает РОВНО ОДИН РАЗ — фикс подтверждён на уровне
кода, не только «на глаз».
Побочный урок (см. `docs/libc-reference.md` `<kbd_raw.h>`): моя
более ранняя попытка проверить UP/DOWN/RIGHT по скриншотам после
`press_key` ошибочно решила, что скрипт их не нажимает вообще —
на самом деле нажимает исправно, просто скриншот ловил случайный
момент дуги. Брейкпоинт/watchpoint на конкретный адрес кода —
надёжнее скриншота для таких проверок.
---
## 6. Модель движения: авторские таблицы кадров, не физика с нуля
Оригинальный движок PoP не считает прыжок как непрерывную физику
(gravity/velocity каждый тик) — движение персонажа задано таблицами кадров
анимации, где у части кадров зашито фиксированное смещение (dx, dy) для
ЭТОГО конкретного кадра последовательности (структура видна и в
исходниках Apple II — `SEQTABLE.S`/`MOVER.S`, и в SDLPoP `seg003.c`/`seq*`
таблицах). Практическое следствие для нашего движка:
- **Не использовать** `sprite_anim`/`sprite_moveto` для основного
персонажа как есть (они лианейно тянут по таймеру/тянут к линейной
цели) — вместо этого приложение само на каждый логический тик:
переключает кадр (`sprite_frame`, атлас как лента поз, не «прогрессия
первый..последний» автоматом) и одновременно применяет dx,dy ЭТОГО
кадра к позиции (`sprite_move`).
- Готовая автоматика движка (`sprite_anim`/`sprite_moveto`/tween,
Y-сортировка) остаётся полезной для декоративных/фоновых элементов
(факелы, патрулирующий стражник вне боя — почти один в один паттерн
`rpgwalk`).
- Источник таблиц смещений: переснять из `Prince-of-Persia-Apple-II/01 POP
Source/Source/{MOVER.S,SEQTABLE.S,FRAMEADV.S}` и/или
`SDLPoP/src/seq*.c` — задача Фазы 1 полной реализации (§7), не PoC
(для PoC можно взять урезанный набор смещений вручную по количеству
пикселей на кадр, посчитанному по видео/скриншотам оригинала, и уточнить
позже).
### 6.1 Инструмент конвертации кадров разного размера (`toolchain/png_strip.py`)
Кадры персонажа в оригинале — РАЗНОГО размера каждый (bbox зависит от
позы; `sprite_t` нашего движка (`libbgi/include/sprite.h`) хранит ОДИН
фиксированный w/h на весь спрайт и рисует от угла, без per-frame
смещения — в отличие от оригинала, где на каждый кадр было своё XCO/YCO
(`APPLEII_RESOURCE_FORMAT.md` §2.2). `toolchain/png_strip.py` (генерик,
не завязан на PoP — принимает произвольный список PNG) закрывает это
ПАДДИНГОМ: канвас = макс. w/h среди кадров ленты, якорь по умолчанию
bottom-center («ноги на месте»), остальное — прозрачность.
**Компромисс, не полноценное решение**: один сильно выбивающийся по
размеру кадр в ленте раздувает канвас (и память) ВСЕХ кадров этой же
ленты. Смягчается группировкой по похожим размерам в отдельные атласы
(не одна лента на все позы актора — так уже сделано для ходьбы отдельно
от прыжка).
**Полноценное решение (кандидат в будущее расширение библиотеки, НЕ
делать без предложения и подтверждения пользователя)**: per-frame
смещение в `sprite_t` (аналог XCO/YCO оригинала) — тогда паддинг
не нужен вообще, экономия памяти по полной. Делать только если память
станет РЕАЛЬНОЙ проблемой (не гипотетической) — тогда предложить как
отдельную правку `sprite.h`/движка. Подробности компромисса —
memory/png_strip_padding_tradeoff.
---
## 7. Полноценное приложение — фазы (после PoC)
Порядок — по риску и зависимостям, не по геймплейной важности.
**Фаза 0 — инфраструктура порта** (расширяет PoC, не переписывает):
- Хелд-стейт клавиатуры — финальное решение и реализация по §2 (после
подтверждения пользователем).
- Полный конвертер уровней (все 15 файлов `levels.dat`/`LEVELn`) → бинарный
формат приложения (можно 1-в-1 raw dump, читать по офсетам в рантайме —
не обязательно разворачивать в C-struct с указателями).
- Полный конвертер фона (24 экрана × N уровней) в растры + конвертер
спрайт-лент Кид/стражник/скелет/тень/Джаффар в атласы `.atl` (расширение
`conv_sprites.py`/формата `.atl`, если частот кадров/атласов на актора не
хватит текущего лимита — см. риск в §8).
**Фаза 1 — Кид, полный набор действий**: стоять/идти/бежать/тормозить/
разворот/прыжок (на месте, вперёд, «прыжок с разбега»)/повисание на
краю/подтягивание/спуск по свисанию/приседание/питьё зелья/смерть от
провала. Переходы между экранами (`MAP`-граф, `INFO.KidStartScrn`).
**Фаза 2 — мир и ловушки**: нажимные плиты/двери через граф
`LINKLOC`/`LINKMAP` (см. `APPLEII_RESOURCE_FORMAT.md` §1.2), шипы
(выдвижение/втягивание/заклинивание), шаткие плиты (loose, обрушение),
зелья (эффект по `BLUESPEC×32`), стартовые позиции по `INFO`.
**Фаза 3 — бой**: подбор/выхватывание меча, состояние стойки, парирование/
удар, коллизия клинков — по логике `AUTO.S`/`seg003-006.c` (референс, не
копия). Стражник: базовое AI-поведение по `GdStartProg` (несколько
шаблонов программ), Y-сортировка слоями уже есть в движке для «кто
спереди/сзади».
**Фаза 4 — разнообразие противников**: скелет, тень (копия анимации Кида —
подтверждено побайтовым совпадением данных, см. `MSDOS_RESOURCE_FORMAT.md`
§3), толстый стражник/визирь (общая база анимации с визирем).
**Фаза 5 — звук**: CBL-эффекты (шаги, удары, двери, падение) из
`digisnd*.dat`→PCM; PC-спикер тройки (`ibm_snd*.dat`) как опциональный
дешёвый бипер без CBL, если формат подтвердится простым парсингом.
**Фаза 6 — оболочка**: титры, меню/выбор уровня, HUD (таймер/жизни),
сохранение прогресса (FILE*), финальные катсцены — по минимуму,
геймплейно не критично.
**Фаза 7 — стабилизация**: полный прогон всех 14 уровней в MAME
(`mame_interactive.py`), затем на реальном железе; профилирование бюджета
кадра по методике `sprite_engine_perf`/`sprite-api-design.md` §9д на самых
насыщенных экранах (несколько стражников + ловушки одновременно —
проверить лимит ~21 спрайт/кадр и Y-sort лимит 32); при необходимости —
банкинг (`--memory big/huge`) для кода/уровня, если размер вылезет за
tiny/small.
---
## 8. Риски, требующие спайка/артефакта до架构 решений
(по правилу `defer_unexplained_quirks` — не гадать, проверять)
1. **Held-state клавиатуры** (§2) — блокирует даже PoC, если решать
«правильно»; иначе PoC на компромиссном варианте 2 (таймаут-эвристика).
2. **Бюджет спрайтов на насыщенный экран** — сцена с 2+ стражниками +
несколько анимированных ловушек может приблизиться к лимиту
~21 спрайт/кадр (`sprite_engine_perf`) — нужна прикидка по реальным
уровням (сколько объектов одновременно активно в худшем экране).
3. **Ёмкость одного атласа/страницы EMM на актора** — у Кида ~220 кадров
(все действия) против 4×12 у `rpgwalk` — потребуется либо несколько
атласов на актора с переключением по фазе действия (стоять/идти отдельно
от боя), либо расширение формата `.atl`/загрузчика на мульти-страничные
атласы — оценить фактический байтовый вес конвертированных кадров Кида
прежде чем проектировать.
4. **Тайминг оригинала** — сверить логическую частоту кадров анимации
оригинала (Apple II ~60 Гц NTSC / DOS — фиксированный таймер) с 50 Гц
Sprinter; если оригинал считался на другой частоте — потребуется
коэффициент пересчёта смещений кадров (§6), иначе прыжки/бег будут
визуально быстрее/медленнее эталона.
---
## 10. Режим памяти сборки
Пользователь предложил `huge` (горячий код в W1, данные в W2, редко
вызываемая логика — банками в W3) как целевой режим. Согласен, с уточнением
по срокам принятия решения.
**`huge` — правильная цель для ПОЛНОГО приложения**, но не то, с чего надо
стартовать:
- Layout `huge` (см. `memory_modes_implemented`): CODE_LOC=0x4100 (W1),
DATA_LOC=0x8000 (W2), банки — W3 (порт 0xE2), `crt0_banked` +
автодетект W2 (как `small`). Состояние приложения (структуры Кида,
уровня, массив `sprite_t`) остаётся в обычном W2-heap ДАЖЕ если код,
который его трогает, забанкован — `malloc` из банка возвращает
W2-указатель (`bank_local_data_pattern`), так что данные не привязаны к
конкретному банку.
- Оверхед `__banked`-вызова (trampoline: +3 байта на стеке между ret и
аргументами, виртуальный 24-битный адрес, см. `sdcc_banking`) — фиксированная
небольшая цена ЗА ВЫЗОВ, не за такт. Это не страшно для функций, которые
вызываются РЕДКО за кадр (AI одного стражника, диалог, переход между
комнатами) — страшно было бы забанковать что-то, что дёргается ВНУТРИ
горячего цикла отрисовки (там уже и так основной бюджет уходит на
`sprite_update`/блиты — см. `sprite_engine_perf`, ~19.5К тактов/спрайт).
Правило простое: **не банковать код на пути "раз в кадр на объект",
банковать код на пути "раз в кадр на комнату/раз в переход/раз в
редкое событие"**: логика ИИ стражника целиком, диалоги/катсцены, меню/
титры/выбор уровня, парсинг уровня при входе в комнату, сериализация
сохранений — хорошие кандидаты в банки; тик Кида, чтение столкновений,
вызов `sprite_update`/`gfx_wait_vsync`, обработка ввода — должны остаться
небанкованными (W1/W2).
- Гранулярность банкования — целый файл (`--bank N=FILE.c`), это уже
системный паттерн проекта (тот же принцип, что и «1 файл = 1 юнит DCE» в
libc) — значит выгодно с САМОГО начала Фазы 1 (не задним числом) резать
исходники приложения по границе «горячее/холодное» файл-в-файл: например
`kid_tick.c`/`collision.c`/`room.c`/`input.c` — неизменно вне банков;
`guard_ai_*.c`/`dialogue.c`/`menu.c`/`levelload.c`/`combat.c` — кандидаты
под `--bank`. Тогда переход на `huge` позже — это правка Makefile/
sprinter-cc-вызова (`--memory huge --bank N=file.c ...`), а не рефакторинг
логики.
**Уточнение (проверено в `bin/sprinter-cc`, строки ~342-350): можно сразу
собирать PoC на `--memory huge` без единого `--bank`.** Скрипт сам
подставляет стаб `const unsigned char n_banks = 0;`, когда `--bank` не
передан ни один раз — `crt0_banked` линкуется и корректно пропускает цикл
загрузки банков при старте. Layout при этом byte-в-byte совпадает с тем,
что делает `crt0_small` для режима `small` (CODE 0x4100/W1, DATA 0x8000/W2,
автодетект W2) — разница только в том, что попутно линкуется сам
`bank.s` (таблица `_bank_pages` + trampoline-инфраструктура), это
незначительный довесок к размеру, не к рантайм-цене. Значит **PoC можно
сразу собирать вызовом `sprinter-cc --memory huge` без `--bank`-флагов** —
и когда в полном приложении появятся первые «холодные» файлы, переход на
банкование — это просто добавление `--bank N=file.c`, без смены
`--memory`/адресов/crt0. Сборочная конфигурация не потребует миграции
между PoC и полным приложением.
Единственное, что стоит сделать уже в Фазе 1 полного приложения (не в
PoC) — планировать структуру исходников с расчётом на будущий файл-в-файл
сплит под банки (см. выше), раз гранулярность банкования — целый файл.
---
## 9. Что нужно от пользователя, прежде чем двигаться дальше
- Подтверждение направления по §2 (какой из трёх вариантов held-state
клавиатуры пробовать первым, или сначала спайк-эксперимент в MAME).
- Подтверждение объёма PoC (§5) — устраивает ли «одна комната без
стражников», или сразу закладывать хотя бы одного патрулирующего
стражника (это не архитектурно сложнее — Y-order и tween уже есть,
просто больше конвертации ассетов).
+81
View File
@@ -0,0 +1,81 @@
# Форматы ресурсов Prince of Persia — сводка
Цель этих документов — подготовить почву для будущего порта Prince of Persia
на ZX Sprinter, разобрав, как устроены ресурсы игры в двух доступных нам
версиях:
- [`APPLEII_RESOURCE_FORMAT.md`](./APPLEII_RESOURCE_FORMAT.md) — формат
уровней и графики по официально опубликованным исходникам 1989 года
(6502-ассемблер). Уверенность высокая везде — восстановлено прямым чтением
кода движка, а не догадками.
- [`MSDOS_RESOURCE_FORMAT.md`](./MSDOS_RESOURCE_FORMAT.md) — формат `.DAT`
ресурсов DOS-версии (исходников нет). Восстановлено эмпирически (разбор
байтов + перепроверка скриптами) и сверено с документацией открытых
сторонних инструментов (SDLPoP, Princed Resources).
## Главный вывод
**Формат уровня практически идентичен в обеих версиях**: Apple II `LEVELn`
занимает ровно 2304 байта (структура `blueprnt` — тайлы, связи
плит/дверей, граф экранов, метаданные старта Кида/стражников), а запись
уровня в DOS `levels.dat` занимает 2305 байт с байтовыми значениями тайлов
того же диапазона. То есть Джордан Мехнер перенёс формат карты уровня в
DOS-порт практически без изменений (+1 байт, вероятно контрольная сумма от
DOS-упаковщика). Это значит: раскладку `BLUETYPE`/`BLUESPEC`/`LINKLOC`/
`LINKMAP`/`MAP`/`INFO`, задокументированную по Apple II исходникам, можно
применять напрямую и к DOS `levels.dat`.
Формат же **графики отличается принципиально**: на Apple II это простой
несжатый rowbyte-формат hi-res экрана с плоской таблицей указателей; в DOS —
контейнер с оглавлением ресурсов (id/size/offset), с отдельными вариантами
под CGA/EGA/VGA — точный кодек пикселей внутри сырого `.DAT`-чанка не
восстановлен ни для той, ни для другой версии до конца. **Но для DOS-графики
это не блокирует работу**: в репозитории github.com/NagyD/SDLPoP (папка
`data/`) уже лежат готовые распакованные PNG для каждого спрайта/фона
(включая VGA-256-цветный вариант `VPALACE`/`VDUNGEON` — то, что нужно под
320×256×256 Sprinter), см. §5 `MSDOS_RESOURCE_FORMAT.md`. Это другой
релиз/сборка данных, чем наш локальный `MSDOS/` (некоторые звуковые `.dat`
отличаются по размеру), но нумерация ресурсов и формат контейнера — те же,
что подтверждено побайтовой сверкой уровня `res2001.bin`.
## Общий контейнерный формат DOS `.DAT` (кратко)
```
[0x00] u32 LE tableOffset — смещение начала таблицы оглавления
[0x04] u16 LE tableSize — размер таблицы оглавления
[0x06..tableOffset) — данные ресурсов (конкатенация чанков)
[tableOffset..tableOffset+tableSize)
— массив записей по 8 байт:
u16 size, u16 id, u16 offset(абсолютный), u16 reserved(=0)
```
Инвариант `tableOffset + tableSize == размер файла` подтверждён на всех 28
`.dat`-файлах в `MSDOS/`, и независимо — именованием файлов `res<id>.*` в
`data/` репозитория SDLPoP.
## Готовые ассеты для порта (важно для практической работы)
`github.com/NagyD/SDLPoP/tree/master/data` содержит не только код движка, но
и сами ресурсы игры — как сырые `.DAT`, так и распакованные поштучно файлы
(`res<id>.png` для спрайтов/фонов, `res<id>.pal` для палитр, `res<id>.bin`
для уровней). Для арт-ассетов (в т.ч. нужного полноцветного VGA-варианта
дворца/подземелий) практичнее взять их оттуда напрямую, чем писать свой
декодер сжатия пикселей DOS `.DAT`.
## Что дальше (не сделано в этом заходе)
1. Точный кодек сжатия пикселей спрайтов в сыром DOS `.DAT` (нужен только
если понадобится читать именно нашу локальную копию `MSDOS/*.dat`
"как есть", а не ассеты из SDLPoP `data/`) — сверка с исходником SDLPoP,
`src/seg009.c`.
2. Семантика служебных полей `digisnd*.dat`/`ibm_snd*.dat` перед сырыми
сэмплами/нотами (частично прояснено документацией Princed Resources —
PC speaker: 1 байт заголовка + повторяющиеся тройки байт "2 байта частоты
+ 1 байт длительности"; WAV: 8 бит, моно, unsigned, 11025 Гц).
3. Назначение бит `secmask` в `BLUETYPE` (Apple II) и служебного блока
`id=2000` в начале DOS `levels.dat` (16 байт в нашей копии, но 2305 байт
в версии SDLPoP — расхождение между релизами, не разобрано).
4. Оценка, какие видеорежимы/цветовые палитры ZX Sprinter реалистично
покрывают исходную графику (CGA/EGA/VGA варианты в DOS-ресурсах против
hi-res Apple II) — отдельная архитектурная задача порта, не формат
ресурсов как таковой.
@@ -0,0 +1,69 @@
# Double buffer (два экрана + флип) — план
Цель: убрать мерцание/тиринг при перерисовке слоёв (Kid ↔ fore/пол-оверлей)
и гарантировать, что на экране ВСЕГДА готовый кадр с правильным порядком
слоёв. Нужно для отладки fore-слоя (видно момент композиции, а не
промежуточные состояния heal-рендера). **Тумблер обязателен**
однобуферный режим удобнее для отладки багов рисования.
## Что уже готово (libbgi — трогать НЕ нужно)
- Две графических страницы 0/1: `gfx_set_draw_page(p)` (двигает
`_gfx_addr_base` 0xC000/0xC140 — рисуют все примитивы),
`gfx_set_visible_page(p)` (ESTEX $54 SELPAGE — display-учёт DSS).
- `gfx_wait_vsync()` — луч (bit5 порта 0xFE), момент без разрывов.
- Паттерн из gfx.h: `set_draw_page(hidden); draw(); wait_vsync();
set_visible_page(hidden);`
- Замечание gfx.h: у каждой страницы СВОЯ плоскость палитры
(page0→pal0, page1→pal1) — для seamless грузить одну палитру в ОБЕ.
## Текущая модель рендера roomtest (однобуфер)
`roomtest.c`: `draw_page=0`, `visible_page=0` фиксированы. Фон комнаты
рисуется ОДИН раз в видео-ОЗУ + теневую копию (GFX_BANK_TRANSPARENT).
Цикл: 3× `gfx_wait_vsync` (пейсинг) → `pop_ctrl_tick` → `kid_heal()`
(восстановить прямоугольник Кида из тени) → `kid_tick`/`pop_phys_tick`
→ `kid_draw` → `pop_fore_over_kid`. Мерцание = heal+draw+fore длиннее
бланка, луч ловит промежуток.
## Работа на стороне PoP
1. **Инициализация обеих страниц**: `pop_room_draw` в page 0 И page 1
(теневая копия одна — общая, из неё heal'ит любая страница).
2. **Палитра в обе плоскости**: сейчас `gfx_pal_fload(0,...)` + sync.
Продублировать в plane 1 (проверить сигнатуру gfx_pal_fload/sync —
plane-параметр).
3. **Per-page heal-история** (ядро): вынести `kid_lx/ly/lw/lh` в
массивы `[2]`, индекс = страница, в которую рисуем. `kid_heal(page)`
восстанавливает СВОЙ прошлый прямоугольник (кадр -2, т.к. рисуем
через страницу). То же для fore/пол-оверлея, если они рисуют вне
футпринта Кида.
4. **Ping-pong в цикле**:
```
uint8_t back = dbuf ? (front ^ 1) : 0;
gfx_set_draw_page(back);
kid_heal(back); kid_tick; phys; kid_draw; fore;
gfx_wait_vsync();
if (dbuf) { gfx_set_visible_page(back); front = back; }
```
5. **Тумблер** `dbuf`: off → draw==visible==0, без флипа, heal[0] —
бит-в-бит текущее поведение. Управление — клавишей (напр. F2) или
compile-флагом.
6. **Пейсинг**: сейчас 3× vsync/лог.кадр. При флипе — один vsync перед
свопом; недостающий пейсинг добрать `gfx_set_fps_div(3)` или ручным
счётом кадров, чтобы скорость игры не изменилась.
## Порядок
- D1: обе страницы + палитра в обе плоскости; ping-pong без per-page
heal (проверить, что флип работает, фон корректен на обеих).
- D2: per-page heal-история (kid_l*[2]) — убрать «хвост» Кида.
- D3: тумблер dbuf + сверка однобуферного пути с текущим (регресс-нет).
- D4: пейсинг (fps_div) — вернуть исходную скорость.
## Связанные
Рендер-модель — [[pop_fore_layer]], [[pop_background_strategy]];
heal — kid_heal/gfx_heal. Будущие динамические слои (loose-floors
[[двойной буфер требует их per-page перерисовки]]) должны рисоваться в
обе страницы по той же дисциплине.
+143
View File
@@ -0,0 +1,143 @@
# Loose floors (проваливающиеся полы) — план порта
Разбор SDLPoP (seg007 loose/trob/mob, seg008 draw_loose). ПЛАН, ещё не
реализовано. Тайл в комнате 1: `[2,6] = 0x0B = tiles_11_loose`.
## 1. Хранение состояния
- **Тип тайла**: `curr_room_tiles[tilepos] & 0x1F == 11` (tiles_11_loose).
Бит `0x20` = «solid» loose (авто-падающий вариант, ур.13 — от шага НЕ
падает). После падения тайл → `0` (tiles_0_empty).
- **Модификатор** `curr_room_modif[tilepos]` = состояние анимации:
- `0` — покой (обычный loose-пол);
- `0x80..0x83`**трясётся** (бит7); за ~4 кадра затухает обратно в 0;
- `1..11`**обратный отсчёт до падения** (на нём что-то стоит);
достигает `loose_floor_delay = 11` → падает.
## 2. Анимация тряски (shake) — когда включается
- **Триггер = do_knock** (seg007:0FE0): на ЖЁСТКОМ приземлении в кадрах
посадки играет `SEQ_KNOCK_DOWN` → взводит `knock``check_knock()`
`do_knock(room, curr_row (knock>0))`.
- `do_knock(room, row)`: по всем колонкам ряда — если тайл loose →
`loose_make_shake()`.
- `loose_make_shake()` (seg007:0FB4): если `modif==0` (и не ур.13) →
`modif = 0x80`, `add_trob(type 1)`.
- **Отсюда кейс пользователя**: Kid падает/приземляется на `[2,4]`
do_knock трясёт ВСЕ loose-тайлы ряда 2 → `[2,6]` трясётся. (Через
knock-смещение ряда может задеть и соседний ряд.)
- `animate_loose` (кадрово): `++modif`; при бите7 трясёт до `>=0x84`
сброс в 0, `trob.type=-1`. `loose_shake()` играет звук
(sound 20/21/22) по таблице `loose_sound[]`.
## 3. Анимация падения (fall) — когда включается
- **Триггер = make_loose_fall(1)** (seg007:0EF6), вызывается когда:
- Kid СТОИТ на loose-тайле — `check_press()` (seg006): кадр с
FRAME_NEEDS_FLOOR над loose → make_loose_fall(1);
- зацеп/подтягивание на loose (`check_grab`, `check_jump_up`);
- пробой сверху: кадр 79 (jumphang) над loose → make_loose_fall(1);
- авто-падающие (ур.13) — `make_loose_fall(-(prandom&0x0F))`.
- `make_loose_fall(modifier)`: если НЕ solid (`tiles & 0x20 == 0`) и
`(sbyte)modif <= 0``modif = modifier`, `add_trob(type 0)`.
- `animate_loose`: `++modif`; когда `modif >= 11` (loose_floor_delay) →
`remove_loose()` (тайл → empty) + `add_mob()` (спавн падающего куска).
## 4. Падающий кусок (mob)
- `add_mob()` кладёт `curmob` в `mobs[]` (до 14). `do_mobs()` каждый
кадр: `move_mob()` (гравитация, y растёт) + `check_loose_fall_on_kid()`
(урон Киду/страже, если попал).
- Приземление куска → тайл под ним `curr_room_tiles[...] = tiles_14_debris`
(seg007 move_mob:1053). Т.е. **loose(11) упал → сверху empty(0), снизу
debris(14)**.
## 5. Отрисовка по статусу
- Куски тайла: `loose_fram_left[]={41,69,41,70,70,41,41,41,70,70,70,0}`,
`loose_fram_right[]={42,71,...}`, `loose_fram_bottom[]={43,73,...}`
(env-спрайты, seg008:518/596/608).
- Индекс кадра = `get_loose_frame(modifier)` (seg008): `0` = ровный
(41/42/43); `1..10` = дрожащие варианты (69–74); при бите7/большой
задержке — низкие индексы.
- **До падения**: рисуем loose с `get_loose_frame(modif)` (0 = ровно,
иначе колеблется). **После**: сверху empty, снизу debris(14) — обычная
статическая отрисовка (у нас уже есть tile 0x0E/14 debris в tile_table).
## 6. Что нужно в нашем движке (сейчас НЕТ)
Наш `pop_bg` рисует комнату СТАТИЧЕСКИ один раз. Loose-полы требуют
**динамического тайлового слоя**:
1. **Массив модификаторов** `room_modif[30]` (у нас есть `bg[30]` — можно
переиспользовать/рядом) — состояние каждого тайла.
2. **Очередь trob** (список анимируемых тайлов) + `animate_loose` пер-кадр
→ перерисовка ТОЛЬКО изменившихся тайлов (как heal-прямоугольник Kid).
3. **make_loose_fall / do_knock / loose_make_shake** — триггеры (из
физики Kid: приземление→knock, стойка на loose→fall).
4. **mob-система** (падающий кусок): минимум 1–2 mob'а, гравитация,
приземление → debris. Урон Киду (`check_loose_fall_on_kid`) — можно
Фазой 2.
5. **Перерисовка тайла**: `draw_tile(row,col)` у нас уже умеет loose
(`code==11`, `loose_fram_*` в env) — нужно вызывать его выборочно с
текущим модификатором (сейчас draw_tile берёт статический bg).
**Порядок реализации (предложение):**
- L1: room_modif[] + выборочная перерисовка тайла по модификатору
(draw_loose с get_loose_frame) — статика→динамика одного тайла.
- L2: trob-очередь + animate_loose (тряска по do_knock на приземлении).
- L3: make_loose_fall (стойка на loose) + отсчёт + remove → empty.
- L4: mob (падающий кусок → debris снизу).
- L5: урон Киду от падающего куска.
## Конкретика из SDLPoP (сверено 2026-07-18, готово к реализации)
Таблицы (seg008.c), индекс = `get_loose_frame(modif)`:
- `loose_fram_left[] = {41,69,41,70,70,41,41,41,70,70,70,0}`
- `loose_fram_right[] = {42,71,42,72,72,42,42,42,72,72,72,0}`
- `loose_fram_bottom[]= {43,73,43,74,74,43,43,43,74,74,74,0}`
- `get_loose_frame(m)`: если `(m&0x80)` (или delay>11) → `m&=0x7F; if(m>10) return 1;``return m;`
- `y_loose_land[] = {2,65,128,191,254}` (mob), `loose_floor_delay = 11`.
Триггеры (call-sites):
- **make_loose_fall(modifier=1)** — из `check_press()` (seg006): когда Kid
СТОИТ на тайле (FRAME_NEEDS_FLOOR, action < hang_climb / turn / bumped) и
`get_tile_at_char()==11`; ИЛИ `frame==79` (прыжок вверх) и
`get_tile_above_char()==11` (пробой сверху). `tile_is_floor(11)==1`
Kid стоит на loose (start_fall НЕ зовётся). Тело:
`if(!(tile&0x20) && (sbyte)modif<=0){ modif=modifier; add_trob(type0); }`
- **do_knock(row)** — на ЖЁСТКОМ приземлении (SEQ_KNOCK_DOWN→check_knock,
seg003): по всем колонкам ряда `if(tile==11) loose_make_shake()`
(`if(modif==0){ modif=0x80; add_trob(type1); }`).
- **animate_loose** (каждый кадр, seg007:816): `++modif`; если `&0x80`
(тряска): `if(modif>=0x84){modif=0; trob=-1;}`; иначе (отсчёт):
`if(modif>=11){ remove_loose(tile→0); trob=-1; add_mob(); } else shake`.
## Интеграция в наш движок (roomtest) — подход
Наш движок ПЕКЁТ комнату один раз (двойной буфер: своя ОЗУ-копия на
страницу). Loose требует динамики + общего состояния pop_map↔pop_bg:
1. **Общая МУТАБЕЛЬНАЯ копия комнаты**: roomtest.c держит `uint8_t
room_fg[30]` (копия room1_fg) и передаёт ОДИН указатель и в
`pop_room_draw`, и в `pop_map_set` → мутации loose видны обоим.
(Сейчас g_fg — `const`; сделать неконстантным.)
2. **Состояние**: `uint8_t pop_loose_modif[30]` (0 / 0x80.. / 1..11).
3. **Модель** (pop_map): `check_press()` в `pop_phys_tick` (make_loose_fall
при стоянии на 11), `animate` каждый кадр.
4. **Перерисовка** (pop_bg, на back-странице каждый кадр):
- тряска/отсчёт (код всё ещё 11): `gfx_heal(tile rect)` (вернуть
печёный фон) + нарисовать loose-кадр (left/right/bottom по
get_loose_frame) банком SPRITE;
- падение (11→0): mutate `g_fg[pos]=0` + «запечь пустоту» на ОБЕИХ
страницах (bake-счётчик 2: чёрный bar + draw_tile empty банком NORMAL
на текущей странице 2 кадра подряд) → дальше heal показывает пусто.
5. **L4 mob**: падающий кусок → debris(14) снизу (y_loose_land); пока
отложено — упавший loose = пусто. **L5** урон — позже.
Риск: перерисовка динамического тайла в двойном буфере (per-page heal +
bake) — единственное тонкое место; остальное — прямой порт логики выше.
## Связанные
Триггеры завязаны на физику Kid ([[pop_hang_state]] check_press/check_grab,
приземление land/SEQ_KNOCK_DOWN). Отрисовка — [[pop_fore_layer]] /
[[pop_background_strategy]] (draw_tile уже знает loose_fram_*).
+53
View File
@@ -0,0 +1,53 @@
# PoC порта Prince of Persia (applications/PoP/docs/PORT_PLAN.md §5).
# --memory huge, БЕЗ --bank.
#
# ПОЧЕМУ huge, а НЕ small (исправлено 2026-07-16): poc использует
# raw-клавиатуру (kbd_raw_open → IM2-таблица). Буферы IM2 (_irq_vec_buf,
# BSS) ОБЯЗАНЫ жить в W2 (0x8000-0xBFFF) — во время прерывания W1/W3
# могут быть перемаплены DSS (см. libc/irq/_irq_table.c, memory/
# fps_divider: «verified tiny/big/huge, small=EINVAL»). --memory small
# пулит W1+W2 в плоские ~32КБ и чейнит DATA за CODE — при небольшом CODE
# BSS уезжает в W1 (<0x8000), и _irq_table_ref отдаёт EINVAL →
# kbd_raw_open молча возвращал -1, poc печатал ошибку УЖЕ в графическом
# режиме (невидимо) и выходил в prompt. huge кладёт CODE в W1, а
# DATA/BSS/STACK/HEAP жёстко в W2 → IM2 работает. Цена: раздел 16КБ
# CODE / 16КБ DATA вместо общего 32КБ-пула small — сейчас влезает с
# запасом; при росте настоящего PoP CODE>16КБ понадобится банк под код.
#
# --bank НЕ нужен: gfx_blit_part()/atlas_load() сами временно трогают W3
# (видеобанк / чтение атласа) — банк room.c в W3 давал вероятностный
# «снег»; банк в W1 несовместим с CODE=W1 (трамплин переключения W1 сам
# бы уехал). sprintf() заменён на ручное hex-форматирование пути в
# tile_atlas_load() (единственный потребитель printf, ~2.9КБ).
PROJ_ROOT := $(abspath $(CURDIR)/../../..)
EXAMPLE := poc
MEMORY ?= huge
EXTRA_FLAGS ?= --gfx 256
EXTRA_SRCS := room.c
TILE_ATLASES := res/tiles/tile01.atl res/tiles/tile14.atl res/tiles/tile03.atl \
res/tiles/tile13.atl res/tiles/tile0e.atl res/tiles/tile0b.atl
EXTRA_DATA := tools/kid.atl tools/room.pal res/room1.dat $(TILE_ATLASES)
include $(PROJ_ROOT)/app.mk
LEVEL1_BIN := $(PROJ_ROOT)/applications/PoP/SDLPoP/data/LEVELS/res2001.bin
tools/kid.raw tools/kid.pal: tools/gen_kid_placeholder.py
cd tools && python3 gen_kid_placeholder.py
tools/kid.atl: tools/kid.raw
python3 $(PROJ_ROOT)/toolchain/mkatlas.py $@ tools/kid.raw:16x16:1x12
res/room1.dat: tools/extract_room.py $(LEVEL1_BIN)
python3 tools/extract_room.py $(LEVEL1_BIN) 1 res/room1.dat
res/tiles/1F-0.png: tools/gen_tile_placeholders.py
cd tools && python3 gen_tile_placeholders.py
tools/room.pal $(TILE_ATLASES): tools/kid.pal res/tiles/1F-0.png tools/build_room_palette.py
cd tools && python3 build_room_palette.py
# make_disk.py упаковывает EXTRA_DATA на диск ПОД БАЗОВЫМ ИМЕНЕМ
# (плоская ФС) — tileNN.atl/room1.dat оказываются в корне рядом с
# kid.atl/room.pal, room.c/poc.c грузят их без пути.
$(EXAMPLE).exe: room.c tools/kid.atl tools/room.pal res/room1.dat $(TILE_ATLASES)
+153
View File
@@ -0,0 +1,153 @@
/*
* level.h — структуры уровня PoP под наш движок.
*
* Формат — POP-DAT-FormatSpecifications.pdf §3.4 (DAT 1.0): комната =
* 30 тайлов (10 колонок x 3 ряда), foretable даёт тип тайла,
* backtable — модификатор/состояние (семантика зависит от типа).
* Адресация тайла: tile = (room-1)*30 + tileOffset, tileOffset 0-9 =
* верхний ряд, 10-19 = средний, 20-29 = нижний, слева направо.
*
* Фаза 1 (applications/PoP/docs/PORT_PLAN.md §5, сузили объём):
* только геометрия (пол/стены/провалы) для коллизий — двери, факелы,
* ловушки, гарды НЕ используются (структуры под них здесь тоже
* упрощены/оставлены как заготовка на потом, см. §7 плана Фаза 2).
*/
#ifndef LEVEL_H
#define LEVEL_H
#include <stdint.h>
/* 1 (не 24) — PoC грузит и рисует только комнату 1; расширить, когда
* появится настоящий level_load() на несколько комнат. */
#define LEVEL_ROOMS 1 /* комнаты нумеруются 1..24 в файле,
* здесь индекс 0..23 = комната N+1 */
#define ROOM_COLS 10
#define ROOM_ROWS 3
#define ROOM_TILES (ROOM_COLS * ROOM_ROWS) /* 30 */
/* --- Типы тайлов (Table 7 спецификации) --- */
#define TILE_TYPE(byte) ((uint8_t)((byte) & 0x1F))
#define TILE_MODIFIER(byte) ((uint8_t)(((byte) >> 5) & 1))
enum {
TILE_EMPTY = 0x00,
TILE_FLOOR = 0x01,
TILE_SPIKES = 0x02,
TILE_PILLAR = 0x03,
TILE_GATE = 0x04,
TILE_STUCK_BUTTON = 0x05,
TILE_DROP_BUTTON = 0x06,
TILE_TAPESTRY = 0x07,
TILE_PILLAR_BOTTOM = 0x08,
TILE_PILLAR_TOP = 0x09,
TILE_POTION = 0x0A,
TILE_LOOSE = 0x0B,
TILE_TAPESTRY_TOP = 0x0C,
TILE_MIRROR = 0x0D,
TILE_DEBRIS = 0x0E,
TILE_RAISE_BUTTON = 0x0F,
TILE_EXIT_LEFT = 0x10,
TILE_EXIT_RIGHT = 0x11,
TILE_CHOPPER = 0x12,
TILE_TORCH = 0x13,
TILE_WALL = 0x14,
TILE_SKELETON = 0x15,
TILE_SWORD = 0x16,
TILE_BALCONY_LEFT = 0x17,
TILE_BALCONY_RIGHT = 0x18,
TILE_LATTICE_PILLAR = 0x19,
TILE_LATTICE_SUPPORT= 0x1A,
TILE_LATTICE_SMALL = 0x1B,
TILE_LATTICE_LEFT = 0x1C,
TILE_LATTICE_RIGHT = 0x1D,
TILE_TORCH_DEBRIS = 0x1E,
TILE_NULL = 0x1F
};
/* Твёрдые тайлы — Фаза 1 (только геометрия); классификация наша, для
* коллизий движка, не часть исходного формата. Двери/шипы/дробилки и
* т.п. сознательно исключены из объёма Фазы 1 (см. §5 плана) — при
* встрече в реальных данных трактовать как проходимые до Фазы 2.
*
* tile_is_solid() — "есть опора сверху" (вертикальный смысл: можно
* стоять НА этом тайле) — Floor ТОЖЕ solid в этом смысле! Для
* горизонтальной коллизии (можно ли ВОЙТИ в эту клетку сбоку) нужен
* ОТДЕЛЬНЫЙ предикат — см. tile_blocks_side ниже. Баг 2026-07-16:
* col_blocked() в poc.c ошибочно звал tile_is_solid() для бокового
* упора — Floor блокировал сам себя, Кид не мог сдвинуться с места
* стоя на полу. */
static inline uint8_t tile_is_solid(uint8_t byte)
{
switch (TILE_TYPE(byte)) {
case TILE_FLOOR:
case TILE_PILLAR:
case TILE_PILLAR_BOTTOM:
case TILE_PILLAR_TOP:
case TILE_WALL:
case TILE_BALCONY_LEFT:
case TILE_BALCONY_RIGHT:
return 1;
default:
return 0;
}
}
/* tile_blocks_side() — настоящая преграда СБОКУ (нельзя войти в
* клетку по горизонтали): Wall/Pillar-семейство. Floor/Balcony НЕ
* блокируют — по ним идёшь (тайл под ногами, не впереди). Lattice-
* колонны (0x19-0x1D) — узкие, Кид физически проходит мимо (см.
* gen_tile_placeholders.py draw_lattice_like) — тоже НЕ блокируют. */
static inline uint8_t tile_blocks_side(uint8_t byte)
{
switch (TILE_TYPE(byte)) {
case TILE_PILLAR:
case TILE_PILLAR_BOTTOM:
case TILE_PILLAR_TOP:
case TILE_WALL:
return 1;
default:
return 0;
}
}
/* --- Тайл и комната --- */
typedef struct {
uint8_t type; /* foretable byte (rrmccccc — см. TILE_TYPE/MODIFIER) */
uint8_t state; /* backtable byte — модификатор/состояние */
} tile_t;
typedef struct {
tile_t tiles[ROOM_TILES]; /* индекс = tileOffset 0..29 */
uint8_t link_left, link_right; /* links-блок: 0 = нет соседа */
uint8_t link_up, link_down;
uint8_t guard_location; /* 0..29; 30 (и выше) = нет гарда */
int8_t guard_direction; /* 0 = вправо, -1 = влево */
uint8_t guard_skill; /* 0..9 */
uint8_t guard_colour; /* индекс палитры, Table 11 */
/* door I/II (событийные цепочки) — Фаза 2, не здесь */
} room_t;
typedef struct {
room_t rooms[LEVEL_ROOMS]; /* индекс 0 = комната 1 (файл 1-based) */
uint8_t start_room; /* 1..24 */
uint8_t start_location; /* 0..29 */
int8_t start_direction; /* 0 = вправо, -1 = влево */
} level_t;
/* Тайл по (room 1-based, col 0-9, row 0-2). */
static inline tile_t *level_tile(level_t *lv, uint8_t room, uint8_t col, uint8_t row)
{
return &lv->rooms[room - 1].tiles[row * ROOM_COLS + col];
}
/* Загружает ОДНУ комнату + стартовую позицию уровня из файла в формате
* tools/extract_room.py (63 Б: foretable[30]+backtable[30] той комнаты
* + start_room+start_pos+start_dir, вырезанные из res20NN.bin — layout
* подтверждён декодом байт 2026-07-15/16: файл начинается СРАЗУ с
* foretable[720], потом backtable[720], без заголовка; start_position
* — смещение 2112, сверено со структурой level_type в SDLPoP/src/
* types.h). Заполняет lv->start_room/start_location/start_direction.
* 0 — OK, -1 — файл не найден/короче 63 Б. */
int level_load_room(level_t *lv, uint8_t room, const char *path);
#endif
+264
View File
@@ -0,0 +1,264 @@
/*
* poc.c — PoC порта Prince of Persia (applications/PoP/docs/PORT_PLAN.md
* §5): проверяем управление (raw-клавиатура, held-state) + коллизию
* по краям экрана + анимацию ходьбы + прыжок/присед поверх готового
* спрайтового движка (sprite.h).
*
* ВАЖНО: персонаж — ВРЕМЕННАЯ ЗАГЛУШКА (лицензированный спрайт-пак
* third_party/16x16-RPG-characters через tools/gen_kid_placeholder.py,
* тот же источник, что уже использует examples/rpgwalk), НЕ графика
* оригинальной Prince of Persia — см. §5 и §8.5 плана. У заглушки нет
* отдельных поз прыжка/приседа — механика (тайминг дуги, состояние,
* коллизия с полом) проверяется на том же спрайте без смены позы;
* визуально это упрощение, не финальный вид.
*
* Дуга прыжка (jump_height[]) — СВОЯ, приблизительная (не таблица
* смещений оригинала — см. §6 плана: авторские таблицы кадров решено
* не переносить, только код/структуры).
*
* Нет ещё (следующие итерации): реальный уровень/фон по BLUETYPE,
* рывок вбок при прыжке с разбега, зацепление за уступ.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <kbd_raw.h>
#include <conio.h>
#include <stdio.h>
#include "level.h"
#include "room.h"
/* Комната 1 уровня 1 — РЕАЛЬНАЯ геометрия (foretable/backtable),
* вырезана tools/extract_room.py из applications/PoP/SDLPoP/data/
* LEVELS/res2001.bin (level_load_room(), см. level.h/room.c) — не
* плейсхолдер. Верхний ряд (row0) — floor-уступ на cols 3-7 (там
* реально стоит персонаж в оригинале), стены по cols 8-9; ряды 1-2 —
* ниже уступа (торч/колонны/пол — Фаза 1 просто их отрисовывает по
* тем же типам, без многоуровневой физики падения). */
static level_t test_level;
#define CHAR_ROW 0 /* ряд, где стоит персонаж (floor-уступ room1) */
#define ROW_Y(r) ((r) * 64) /* room_row_h все по 64 */
#define GROUND_Y ROW_Y(CHAR_ROW + 1) /* низ ряда CHAR_ROW = верх пола */
#define KIDY (GROUND_Y - 16) /* y спрайта (16 px высотой) */
#define SPEED 2 /* px/кадр — заглушка, не авторский темп */
/* Настоящая преграда (Wall/Pillar) слева/справа от кандидата x в
* CHAR_ROW — блокирует движение (грубая проверка по краям хитбокса
* 16px, без под-тайловой подгонки — для PoC достаточно, см.
* PORT_PLAN.md §6). tile_blocks_side(), НЕ tile_is_solid(): Floor —
* тайл, на котором Кид СТОИТ (тот же CHAR_ROW), tile_is_solid() его
* тоже считает "твёрдым" (можно стоять сверху) — если проверять им же
* боковую преграду, Кид не мог сдвинуться с собственного пола (баг,
* найден 2026-07-16). */
static uint8_t col_blocked(int x)
{
uint8_t c0, c1;
if (x < 0 || x + 15 >= ROOM_COLS * ROOM_TILE_W)
return 1;
c0 = (uint8_t)(x / ROOM_TILE_W);
c1 = (uint8_t)((x + 15) / ROOM_TILE_W);
if (tile_blocks_side(level_tile(&test_level, 1, c0, CHAR_ROW)->type))
return 1;
if (tile_blocks_side(level_tile(&test_level, 1, c1, CHAR_ROW)->type))
return 1;
return 0;
}
/* Ленты атласа: dir*3+frame, dir 0=вниз/1=влево/2=вправо/3=вверх,
* 3 кадра маятника на направление (см. tools/gen_kid_placeholder.py). */
#define DIR_DOWN 0
#define DIR_LEFT 1
#define DIR_RIGHT 2
/* Дуга прыжка: своя, приблизительная (не авторская таблица, см. шапку
* файла) — высота над полом (px) по кадрам 0..19, УЖЕ ПОЛНЫЙ горб
* (подъём 2→22 к элементу 10, спуск обратно к 2 к элементу 19) — БЕЗ
* зеркалирования в коде, массив читается один раз целиком. БАГ,
* найденный пользователем 2026-07-15: раньше код ЕЩЁ РАЗ зеркалил
* этот уже полный горб на 40 кадров — получалось два полных прыжка
* подряд от одного триггера (не проблема клавиатуры/декодера, чистая
* рассинхронизация данных и комментария). 20 кадров @ 50 Гц ~= 0.4 с. */
static const uint8_t jump_height[20] = {
2, 4, 7, 10, 13, 16, 18, 20, 21, 22,
22, 21, 20, 18, 16, 13, 10, 7, 4, 2
};
#define JUMP_FRAMES 20
static atlas_t at;
static sprite_t kid;
static uint8_t facing = DIR_DOWN; /* текущее направление анимации */
static uint8_t jumping = 0; /* 0 = на земле */
static uint8_t jump_t = 0; /* кадр дуги, 0..JUMP_FRAMES-1 */
static uint8_t crouching = 0;
/* Прыжок — level-triggered НАМЕРЕННО (не edge-detect): если UP всё ещё
* зажат к моменту приземления — следующий прыжок стартует СРАЗУ (цепочка
* прыжков, пока держишь); отпустил раньше — второй прыжок не начнётся
* сам, только по следующему нажатию. Раньше здесь были up_prev/
* jump_cooldown — попытка "починить" ровно ЭТО поведение, приняв его
* за баг; убрано 2026-07-15 после уточнения желаемого поведения. */
static void draw_room(void)
{
setfillstyle(SOLID_FILL, BLACK);
bar(0, 0, 319, 255);
room_draw(&test_level, 1);
setcolor(LIGHTGRAY);
outtextxy(60, 4, "PoP PoC: hold LEFT/RIGHT to walk, ESC to quit");
outtextxy(4, 14, "(placeholder tiles -- not original PoP art)");
}
/* HUD-плашка состояния (нет отдельной позы прыжка/приседа — статус
* текстом, рисуется банком 0x50, heal спрайтового движка её не
* трогает — как fps-плашка в examples/rpgwalk). */
static void show_state(uint8_t jump, uint8_t crouch)
{
setfillstyle(SOLID_FILL, BLACK);
bar(0, 24, 60, 32);
setcolor(YELLOW);
if (jump)
outtextxy(0, 24, "JUMP");
else if (crouch)
outtextxy(0, 24, "CROUCH");
}
static void set_facing(uint8_t dir)
{
if (facing == dir)
return;
facing = dir;
sprite_anim(&kid, (uint8_t)(dir * 3), (uint8_t)(dir * 3 + 2),
6, ANIM_PINGPONG);
}
int main(void)
{
uint8_t page, hidden;
int x;
if (level_load_room(&test_level, 1, "room1.dat") != 0 &&
level_load_room(&test_level, 1, "a:\\room1.dat") != 0) {
puts("room1.dat not found");
return 1;
}
/* Стартовая позиция. В данных room1 start_location = tileOffset 0
* (row0/col0) — а там EMPTY (провал, без пола); в Фазе 1 нет физики
* падения, поэтому для PoC ставим Кида на floor-уступ (col 3, где он
* реально стоит в оригинале). Когда появится многоуровневая физика —
* брать col из start_location. start_direction: -1 = влево, 0 =
* вправо (level.h). */
x = 3 * ROOM_TILE_W;
facing = (test_level.start_direction < 0) ? DIR_LEFT : DIR_RIGHT;
if (atlas_load(&at, "kid.atl") != 0 &&
atlas_load(&at, "a:\\kid.atl") != 0) {
puts("kid.atl not found");
return 1;
}
if (tile_atlas_load("") == 0 && tile_atlas_load("a:\\") == 0) {
puts("tile atlases not found");
atlas_free(&at);
return 1;
}
initgraph();
/* room.pal = EGA16 + Kid + Floor + Wall — ОДНА палитра на всё,
* собрана tools/build_room_palette.py (см. --seed-pal в
* toolchain/png_strip.py) — Kid и тайлы на экране одновременно,
* их "свои" цвета обязаны жить в одной таблице. */
if (gfx_pal_fload(0, "room.pal") < 0)
gfx_pal_fload(0, "a:\\room.pal");
gfx_pal_sync();
gfx_sprite_clip(0); /* коллизия по краям гарантирует границы */
/* kid.atl — ОДНА лента (12 кадров вертикально: dir*3+кадр, см. шапку
* файла и examples/rpgwalk); индекс atlas_sprite_init — это НОМЕР
* ЛЕНТЫ (персонажа), а не кадра. Лента одна → всегда 0. Стартовый
* кадр направления выставляем sprite_frame (вертикальная лента, fh=16:
* кадр N на sy=N*16). Баг Соннета (найден 2026-07-16): здесь стоял
* facing*3 как индекс ЛЕНТЫ — при старте лицом влево (idx 3) читался
* мусор за каталогом атласа → мусорные w/h/src → блит спрайта заливал
* пол-экрана «снегом». */
atlas_sprite_init(&kid, &at, 0);
sprite_frame(&kid, 0, (int)(facing * 3) * 16);
kid.x = x;
kid.y = KIDY;
sprite_show(&kid);
for (page = 0; page < 2; page++) { /* фон + спрайт на обе страницы */
gfx_set_draw_page(page);
draw_room();
sprite_update(&kid, 1);
}
gfx_set_visible_page(0);
/* kbd_raw требует BSS в W2 (IM2-таблица) — недоступно в --memory small
* (там BSS может уехать в W1 → EINVAL); poc собирается --memory huge
* (CODE в W1, DATA/BSS в W2). closegraph ДО puts — иначе сообщение
* ушло бы в графический режим (невидимо), а программа молча вышла бы
* в prompt (баг Соннета, найден 2026-07-16). */
if (kbd_raw_open() != 0) {
closegraph();
puts("kbd_raw_open failed (need memory mode with BSS in W2)");
atlas_free(&at);
return 1;
}
for (;;) {
uint8_t moving = 0;
int y = KIDY;
if (jumping) {
/* дуга идёт сама; направлением можно скользить вбок,
* поза не меняется (нет отдельного кадра прыжка) */
if (kbd_raw_down(KBD_LEFT)) {
if (!col_blocked(x - SPEED)) x -= SPEED;
} else if (kbd_raw_down(KBD_RIGHT)) {
if (!col_blocked(x + SPEED)) x += SPEED;
}
y = KIDY - jump_height[jump_t];
jump_t++;
if (jump_t >= JUMP_FRAMES) {
jumping = 0;
y = KIDY;
}
} else if (crouching) {
if (!kbd_raw_down(KBD_DOWN))
crouching = 0;
} else if (kbd_raw_down(KBD_UP)) {
jumping = 1;
jump_t = 0;
} else if (kbd_raw_down(KBD_DOWN)) {
crouching = 1;
} else if (kbd_raw_down(KBD_LEFT)) {
if (!col_blocked(x - SPEED)) x -= SPEED;
set_facing(DIR_LEFT);
moving = 1;
} else if (kbd_raw_down(KBD_RIGHT)) {
if (!col_blocked(x + SPEED)) x += SPEED;
set_facing(DIR_RIGHT);
moving = 1;
}
if (!moving && !jumping && (sprite_anim_status(&kid) & SPR_ANIM_ON))
sprite_anim_stop(&kid, (int8_t)(facing * 3));
sprite_move(&kid, x, y);
if (kbd_raw_down(KBD_ESC))
break;
hidden = gfx_get_visible_page() ^ 1;
gfx_set_draw_page(hidden);
show_state(jumping, crouching);
sprite_update(&kid, 1);
gfx_wait_vsync();
gfx_set_visible_page(hidden);
}
kbd_raw_close();
closegraph();
tile_atlas_free();
atlas_free(&at);
return 0;
}
Binary file not shown.
@@ -0,0 +1,32 @@
/* pop_bg_atlas.h — раскладка атласов статического фона PoP.
* Сгенерировано toolchain/pop_pack_bg.py — НЕ править вручную.
*
* Прямая адресация (ноль remap-таблиц в W2):
* ENV фон id N -> atlas env_bg[N>>5], idx N&31
* WALL id N -> atlas wall, idx N
* FORE id N -> atlas fore, idx N
*/
#ifndef POP_BG_ATLAS_H
#define POP_BG_ATLAS_H
#define POP_ENV_SHIFT 5
#define POP_ENV_MASK 31
#define POP_ENV_PAGES 5
/* Палитра: env-слоты, wall-слоты (сприйт-пиксель i -> база+i). */
#define POP_PAL_ENV 0x50
#define POP_PAL_WALL 0x60
/* Имена файлов атласов (грузятся atlas_load). */
static const char *const pop_env_atl[POP_ENV_PAGES] = {
"pop_env0.atl",
"pop_env1.atl",
"pop_env2.atl",
"pop_env3.atl",
"pop_env4.atl",
};
#define POP_WALL_ATL "pop_wall.atl"
#define POP_FORE_ATL "pop_fore.atl"
#define POP_BG_PAL "pop_bg.pal"
#endif
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+39
View File
@@ -0,0 +1,39 @@
/* kid_atlas.h — раскладка атласов Kid. Сгенерировано pop_pack_kid.py. */
#ifndef KID_ATLAS_H
#define KID_ATLAS_H
#define KID_SHIFT 3
#define KID_MASK 7
#define KID_PAGES 28
#define KID_PAL 0x70
static const char *const kid_atl[KID_PAGES] = {
"kid0.atl",
"kid1.atl",
"kid2.atl",
"kid3.atl",
"kid4.atl",
"kid5.atl",
"kid6.atl",
"kid7.atl",
"kid8.atl",
"kid9.atl",
"kid10.atl",
"kid11.atl",
"kid12.atl",
"kid13.atl",
"kid14.atl",
"kid15.atl",
"kid16.atl",
"kid17.atl",
"kid18.atl",
"kid19.atl",
"kid20.atl",
"kid21.atl",
"kid22.atl",
"kid23.atl",
"kid24.atl",
"kid25.atl",
"kid26.atl",
"kid27.atl",
};
#define KID_PAL_FILE "kid.pal"
#endif
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 867 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@

Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@


Some files were not shown because too many files have changed in this diff Show More