diff --git a/bin/sprinter-cc b/bin/sprinter-cc index 21c1eac..45e1e22 100755 --- a/bin/sprinter-cc +++ b/bin/sprinter-cc @@ -15,8 +15,8 @@ # 0xA2 to auto-detect whether DSS already mapped W2 # (program > 16 KB) or whether we need to allocate # it ourselves (program < 16 KB). Covers 0..30 KB. -# big: tiny + banked code in W1 [TODO] -# huge: small + banked code in W3 [TODO] +# big: tiny + banked code in W1 +# huge: small + banked code in W3 # manual: explicit via --memory-manual / --code-loc / --data-loc # --memory-manual SPEC placement spec, only with --memory manual. # SPEC = comma-separated KEY=VAL list: @@ -33,6 +33,18 @@ # -Wl FLAG extra linker flag (repeatable) # --bank N=FILE.c compile FILE.c as bank N; repeatable; pulls crt0_banked # automatically and adds -Wl-b_BANKN=0x{N}C000 +# --w3 FILE.c place FILE.c resident in window 3 (0xC000), called +# DIRECTLY (no trampoline). Repeatable. Defaults to +# --memory small; works in tiny|small|big|huge. DSS maps +# all image pages incl. W3. Compiled with --codeseg/ +# --constseg W3CODE (writable statics stay in W2). In huge +# the resident page SHARES W3 with trampoline banks: crt0 +# captures/restores it (W3_RESIDENT), mkexe gets -W. +# Rules: W3 code must NOT switch the W3 page; W1/W2 code that +# swaps W3 to another page must restore it; no writable data +# belongs in W3 (code + rodata only). From a __banked +# context the resident W3 code is unreachable (but resident +# -> __banked via the W1 trampoline is fine). # --mkexe FLAG extra mkexe flag (repeatable; e.g. --mkexe -p --mkexe 0) # --max-allocs N SDCC --max-allocs-per-node (default: 100000 — smaller/ # faster code at the cost of compile time; pass a lower @@ -88,6 +100,11 @@ SOURCES=() LD_EXTRA=() MKEXE_EXTRA=() BANK_SPECS=() # entries like "1=engine.c" +W3_SPECS=() # entries like "mod.c" — резидентные модули окна W3 (--w3) +W3_RELS=() # заполняется при компиляции W3-модулей +W3_LD_FLAGS=() # -Wl-b_W3CODE=0xC000, если есть --w3 +USER_SET_MEMORY="" # непусто, если --memory задан явно (для --w3 авто-small) +W3_RESIDENT_HUGE=0 # 1, если --w3 в режиме huge (резидент делит W3 с банками) MAX_ALLOCS="100000" # sdcc --max-allocs-per-node; дефолт 100000 — агрессивная # регистровая аллокация (медленнее компиляция, меньше/ # быстрее код; как fast-сборки библиотек). --max-allocs N @@ -112,8 +129,9 @@ while [[ $# -gt 0 ]]; do -S) STACK_ADDR="$2"; shift 2;; --code-loc) USER_CODE_LOC="$2"; shift 2;; --data-loc) USER_DATA_LOC="$2"; shift 2;; - --memory) MEMORY_MODE="$2"; shift 2;; + --memory) MEMORY_MODE="$2"; USER_SET_MEMORY=1; shift 2;; --memory-manual) MEMORY_MANUAL="$2"; shift 2;; + --w3) W3_SPECS+=("$2"); shift 2;; --stack-size) STACK_SIZE="$2"; shift 2;; -Wl) LD_EXTRA+=("$2"); shift 2;; --bank) BANK_SPECS+=("$2"); shift 2;; @@ -132,6 +150,24 @@ done [[ -z "$OUT" ]] && { echo "sprinter-cc: -o NAME is required" >&2; exit 1; } [[ ${#SOURCES[@]} -eq 0 ]] && { echo "sprinter-cc: no input files" >&2; exit 1; } +# ------- --w3: резидентный код окна W3 (подход A) ---------------------------- +# W3-модули едут в область W3CODE @0xC000 и вызываются напрямую (без +# трамплинов). Работает поверх small-лейаута: main-код с 0x4100 (W1/W2), +# W3 свободно, DSS маппит все страницы образа сам (проверено tests/w3probe). +# --w3 подразумевает small; с huge несовместим (там W3 занят банками). +if [[ ${#W3_SPECS[@]} -gt 0 ]]; then + [[ -z "$USER_SET_MEMORY" ]] && MEMORY_MODE="small" # --w3 → small по умолчанию + case "$MEMORY_MODE" in + tiny|small|big|huge) : ;; # поддержаны (huge — резидент делит W3 с банками) + *) echo "sprinter-cc: --w3 поддержан для tiny|small|big|huge (дано: $MEMORY_MODE)" >&2; exit 1;; + esac + # huge: резидентный W3-код (0xC000, HOME) сосуществует с трамплин-банками + # W3 — crt0_banked захватывает резидентную страницу и возвращает её + # дефолтом после загрузки банков (см. crt0_banked.s, W3_RESIDENT). + W3_RESIDENT_HUGE=0 + [[ "$MEMORY_MODE" == "huge" ]] && W3_RESIDENT_HUGE=1 +fi + # ------- BGI graphics driver selection (--gfx) ------------------------------- # graphics.h — mode-agnostic слой; конкретный видеорежим задаёт driver- # архив. Одновременно только один. drv16 пока не реализован. @@ -186,7 +222,8 @@ case "$MEMORY_MODE" in huge) # small layout + banked code in W3. crt0_banked auto-detects W2 # the same way crt0_small does, then loads banks from the .EXE. - MODE_CODE_LOC="0x4100"; MODE_DATA_LOC="0x8000" + # MODE_CODE_LOC="0x4100"; MODE_DATA_LOC="0x8000" + MODE_CODE_LOC="0x4100"; MODE_DATA_LOC="0" ;; manual) # Defaults if SPEC omits a key. @@ -280,6 +317,7 @@ asm_runtime() { local prefix="" [[ $DEBUG_RT -eq 1 ]] && prefix+="DEBUG_RT = 1"$'\n' [[ $BANK_W1 -eq 1 ]] && prefix+="BANK_W1 = 1"$'\n' + [[ $W3_RESIDENT_HUGE -eq 1 ]] && prefix+="W3_RESIDENT = 1"$'\n' if [[ -n "$prefix" ]]; then local patched="$WORK/$(basename "$src" .s)_patched.s" { printf "%s" "$prefix"; cat "$src"; } > "$patched" @@ -305,9 +343,14 @@ if [[ -n "$STACK_SIZE" ]]; then printf " .module sprinter_heap_top\n ___sdcc_heap_end = 0x%04X\n .globl ___sdcc_heap_end\n" \ "$_heap_top" > "$WORK/heap_top.s" HEAP_TOP_SRC="$WORK/heap_top.s" - [[ $VERBOSE -eq 1 ]] && echo " heap_top: custom HEAP_TOP=$(printf '0x%04X' $_heap_top) (stack reserve $_stack_sz bytes)" + HEAP_TOP_VAL=$(printf '0x%04X' "$_heap_top") + [[ $VERBOSE -eq 1 ]] && echo " heap_top: custom HEAP_TOP=$HEAP_TOP_VAL (stack reserve $_stack_sz bytes)" else HEAP_TOP_SRC="$RUNTIME/heap_top.s" + # Дефолт из runtime/heap_top.s (для отчёта раскладки) — обычно 0xBB00. + HEAP_TOP_VAL=$(grep -oiE '___sdcc_heap_end[[:space:]]*=[[:space:]]*0x[0-9A-Fa-f]+' "$RUNTIME/heap_top.s" \ + | grep -oiE '0x[0-9A-Fa-f]+' | head -1) + HEAP_TOP_VAL="${HEAP_TOP_VAL:-0xBB00}" fi run "$SDASZ80" -o "$HEAP_TOP_REL" "$HEAP_TOP_SRC" @@ -322,6 +365,20 @@ for src in "${SOURCES[@]}"; do USER_RELS+=("$rel") done +# 2b. --w3 resident modules → .rel. --codeseg/--constseg W3CODE кладёт код +# и rodata в W3 (0xC000); --dataseg НЕ трогаем — писучие статики +# остаются в обычном _DATA (W2). Прямые вызовы, без трамплинов. +if [[ ${#W3_SPECS[@]} -gt 0 ]]; then + for src in "${W3_SPECS[@]}"; do + rel="$WORK/w3_$(basename "$src" .c).rel" + run "$SDCC" "${CC_FLAGS[@]}" --codeseg W3CODE --constseg W3CODE \ + -c -o "$rel" "$src" + W3_RELS+=("$rel") + done + W3_LD_FLAGS+=("-Wl-b_W3CODE=0xC000") + [[ $VERBOSE -eq 1 ]] && echo " w3: ${#W3_SPECS[@]} module(s) resident @0xC000 (direct call)" +fi + # 3. bank infrastructure — required whenever crt0_banked is in play. # Always assemble bank.s for _bank_pages + the bcall/bjump trampolines. # If the user did not pass any --bank, generate a tiny stub providing @@ -369,6 +426,7 @@ IHX="$WORK/$(basename "$OUT" .exe).ihx" LINK_FLAGS=(-mz80 --no-std-crt0 --std-c99 --opt-code-size --code-loc "$CODE_LOC" --data-loc "$DATA_LOC") LINK_FLAGS+=("${BANK_LD_FLAGS[@]}") +LINK_FLAGS+=("${W3_LD_FLAGS[@]}") for f in "${LD_EXTRA[@]}"; do LINK_FLAGS+=("$f"); done # libsprinter.lib via -l/-L (sdcc passes -lsprinter through to sdldz80). # @@ -379,12 +437,12 @@ for f in "${LD_EXTRA[@]}"; do LINK_FLAGS+=("$f"); done # the warning is just noise. In verbose mode show everything. if [[ $VERBOSE -eq 1 ]]; then run "$SDCC" "${LINK_FLAGS[@]}" -o "$IHX" \ - "$CRT0_REL" "$HEAP_TOP_REL" "${USER_RELS[@]}" "${BANK_RELS[@]}" \ + "$CRT0_REL" "$HEAP_TOP_REL" "${USER_RELS[@]}" "${BANK_RELS[@]}" "${W3_RELS[@]}" \ "-L$LIB_DIR" "${GFX_LD[@]}" "$LIBC_LD" else # Drop the warning line + its two follow-up "Library:" lines. run "$SDCC" "${LINK_FLAGS[@]}" -o "$IHX" \ - "$CRT0_REL" "$HEAP_TOP_REL" "${USER_RELS[@]}" "${BANK_RELS[@]}" \ + "$CRT0_REL" "$HEAP_TOP_REL" "${USER_RELS[@]}" "${BANK_RELS[@]}" "${W3_RELS[@]}" \ "-L$LIB_DIR" "${GFX_LD[@]}" "$LIBC_LD" 2>&1 \ | awk ' /^\?ASlink-Warning-Definition of public symbol/ { skip = 3 } @@ -393,9 +451,11 @@ else ' fi -# Quick bank-size check (only meaningful if there are banks). -if [[ ${#BANK_SPECS[@]} -gt 0 ]] && [[ -f "${IHX%.ihx}.map" ]]; then - python3 "$CHECK_BANKS" "${IHX%.ihx}.map" || true +# Отчёт по раскладке памяти + проверка лимитов — для ЛЮБОЙ модели памяти +# (сколько свободно в W1/W2, W3 при --w3, и в каждом банке при --bank). +if [[ -f "${IHX%.ihx}.map" ]]; then + python3 "$CHECK_BANKS" "${IHX%.ihx}.map" \ + --mode "$MEMORY_MODE" --heap-top "$HEAP_TOP_VAL" --stack "$STACK_ADDR" || true fi # 5. mkexe → .exe. In BIG mode tell mkexe banks live at 0x4000 (W1). @@ -404,6 +464,7 @@ fi MK_PREFIX=() [[ $VERBOSE -eq 1 ]] && MK_PREFIX+=(-v) [[ $BANK_W1 -eq 1 ]] && MK_PREFIX+=(-B 0x4000) +[[ $W3_RESIDENT_HUGE -eq 1 ]] && MK_PREFIX+=(-W) MK_PREFIX+=("${MKEXE_EXTRA[@]}") MK_PREFIX+=(-L "$LOAD_ADDR" -E "$ENTRY_ADDR" -S "$STACK_ADDR" -o "$OUT") run "$MKEXE" "${MK_PREFIX[@]}" "$IHX" diff --git a/docs/libc-reference.md b/docs/libc-reference.md index 261203a..18bd895 100644 --- a/docs/libc-reference.md +++ b/docs/libc-reference.md @@ -259,6 +259,14 @@ putsprite новой (save-буфер не нужен). Низкий урове рисовать банком 0x50, спрайты/оверлеи — putsprite/0x5C; буферы образов — вне W3 (< 0xC000). Тесты: tests/sprites, tests/gfxbanks, tests/bgi_img. +Скролл региона из НЕактивной страницы в активную (): +`gfx_scroll_h(area, dx, dirty)` — горизонтальный (dx>0 = вправо; быстрый +построчный accel-скролл без страйдов, DI-банды по 16 строк), +`gfx_scroll_v(area, dy, dirty)` — вертикальный (dy>0 = вниз; через +строку-буфер на стеке, ПОКА не оптимален — Port_Y один на burst, разный +read/write Y невозможен без ломающего accel OUT). Копия = скролл + heal +цели (банк 0x50). Открывшуюся полосу |d| не заполняют — возвращают в +*dirty (NULL = не нужно). Пример: examples/scroll. Стиль линий (Ф2d): `setlinestyle(style,upattern,thick)`/`getlinesettings` (SOLID/DOTTED/CENTER/DASHED/USERBIT + NORM/THICK) — на line/rectangle/ drawpoly. diff --git a/docs/memory management b/docs/memory management deleted file mode 100644 index 5379221..0000000 --- a/docs/memory management +++ /dev/null @@ -1,67 +0,0 @@ - -1) если я и DATA и CODE размещаю в одном окне (W1 - #4000 или W2 - #8000, неважно), -то при вызове set_videomode глобальные переменные (errno, g_text_attr) не меняют -своих значений. -если же DATA и CODE находятся в разных окнах (не важно где DATA - в W1 или W2, главное -что не в том где CODE) - то при вызове se_videomode значения глобальных переменных меняются - -То есть похоже что для DATA не назначается отдельный блок памяти а назначается только для CODE -Это полностью соответствует документации - если приложение менее 16К (как у нас) то ему выделяется -только одна страница. И получается что работа со второй страницей идет несанкционированно (ей память -не выделена). - -Потому предлагается -1) сейчас размещать ВСЕ в одной странице (и DATA и CODE и стек) - в W2. - -2) дальше - добавить в нашу обертку sprinter-cc режимы памяти - --tiny - все приложение помещается в одну страницу - в W2 (и DATA и CODE и стек) --small - приложение помещается в две страницы - CODE в W1, DATA и стек - в W2. -в этом режиме над отдельно выделять и маппить страницу в W2 для DATA и стека --big - DATA, CODE и стек помещаются в одну страницу W2 как в -tiny, добавляется поддержка banked в W1, -страница W3 остается служебной и для работы с граффикой из banked code --huge - приложение помещается в двух страницах как и -small но так же добавляется поддержка banked но -уже в страницу W3 - -Из документации - - - > Теперь адресса #4000..#7FFF,#8000..#BFFF,#C000..#FFFF, когда ДСС - передаёт управление эти прогораммам, какие банки там нахадятся по - умолчанию? - - В зависимости от адреса загрузки и размера приложения DSS выделяет - необходимое число страниц памяти. Так при размере меньше 16К будет - выделена - одна страница, при размере больше 16К - две, и т.д. В окна с - "неиспользуемым" адресном пространством будет подключатся - специальная страница #FF.Если приложению требуется памяти больше чем - зарезервировано в exe-файле, оно должно выделить себе дополнительный - блок памяти самостоятельно. - - - > В конфигурации спринтер - > по #0000..#3FFF, при работе ДСС находится сама ДСС с её Резетами, - > чтоб использывать когда сюда подставленна страница пользователся - > резеты не доступны!. - - Это так в нижних 16K находится DSS / BIOS в остальных 48К - приложение, но с - определенными особенностями. Стек не должен быть выше #BFFF при - вызове DSS и ниже #8000 при вызове некоторых функций BIOS. - - -Так же посмотри вот сюда - возможно нам придется для режимов -small и -huge делать свой первичным загрузчиком - -Из документации - - - Выполнение EXE-файла осуществляется по следующим пунктам: - 1) Открывает exe-файл на чтение; - 2) Считывает в рабочую область префикс exe-файла; - 3) Выделяет блок памяти, требуемый для загрузки всего файла или первичного - загрузчика, если его размер не равен нулю; - 4) Сохраняет стек; - 5) Подключает страницы из выделенного блока; - 6) Строит префикс запуска программы и устанавливает на него регистр IX; - 7) Считывает файл по адресу указанному в смещении 16 (Адрес расположения кода в - памяти); - 8) Закрывает exe-файл, если это не первичный загрузчик; - 9) Устанавливает стек равным значению из смещения 20 (Адрес расположения стека); - 10) Передает управление по адресу указанному в смещении 18 (Адрес запуска); diff --git a/docs/memory-management.md b/docs/memory-management.md new file mode 100644 index 0000000..07ace4f --- /dev/null +++ b/docs/memory-management.md @@ -0,0 +1,308 @@ +# Управление памятью в sprinter-cc + +Документ описывает модель памяти Sprinter (Sp2000), режимы памяти обёртки +`sprinter-cc` и способы размещения кода/данных: single-page, split, банки +(трамплины) и резидентный код окна W3 (`--w3`). + +Всё в этом документе подтверждено сборкой и прогоном в MAME v3.06 / DSS 1.71.57 +(см. `tests/w3probe`, `tests/banktest`, `tests/banklocl`). + +--- + +## 1. Аппаратная модель памяти + +Z80 видит 64 КБ, разбитые на **четыре окна по 16 КБ**. Каждое окно независимо +маппится на физическую страницу через порт-регистр страницы: + +| Окно | Адреса | Порт страницы | Назначение по умолчанию | +|------|---------------|---------------|--------------------------| +| W0 | `0x0000-0x3FFF` | `0x82` | **DSS / BIOS** (RST-ы, системные вызовы) | +| W1 | `0x4000-0x7FFF` | `0xA2` | приложение | +| W2 | `0x8000-0xBFFF` | `0xC2` | приложение | +| W3 | `0xC000-0xFFFF` | `0xE2` | приложение / графика / банки | + +- Запись в порт `0x{8/A/C/E}2` меняет физ-страницу окна; чтение возвращает + текущую страницу. +- **W0 занят DSS/BIOS** — там живут RST-обработчики (`RST #08` BIOS, `RST #10` + ESTEX). Пока в W0 стоит системная страница, вызовы доступны; подменять W0 + нельзя без потери RST-ов. +- «Неиспользуемое» окно маппится на **специальную страницу `#FF`**: чтение даёт + `0xFF`, запись игнорируется. Это ключевая причина «молчаливой» порчи данных — + см. §7. + +### Порты в C + +`` даёт SFR-обёртки: `_io_page_w0..w3` (чтение/запись порта), +`sprinter_page_w0..w3(page)`. + +--- + +## 2. Как DSS загружает EXE + +Из документации DSS, последовательность `EXEC`: + +1. Открыть exe-файл на чтение. +2. Считать префикс exe в рабочую область. +3. **Выделить блок памяти** размером под весь файл (если `loader==0`) или под + первичный загрузчик (`loader>0`). +4. Сохранить стек. +5. **Подключить страницы** выделенного блока в окна (последовательно от окна + адреса загрузки: W1→W2→W3…). +6. Построить префикс запуска → регистр `IX`. +7. Считать файл по адресу загрузки (смещение 16 в заголовке). +8. Закрыть exe, **если это не первичный загрузчик** (`loader==0`). +9. Установить `SP` = значение из смещения 20 (адрес стека). +10. Передать управление по адресу из смещения 18 (entry). + +**Следствия, на которых стоит вся схема памяти:** + +- DSS выделяет **число страниц по размеру образа**: `<16 КБ` → 1 страница, + `16..32 КБ` → 2, `32..48 КБ` → 3. Лишние окна = страница `#FF`. +- Страницы маппятся **подряд** начиная с окна адреса загрузки. Образ, тянущийся + `0x4100..0xFFFF` (3 страницы), даёт W1+W2+W3 замапленными автоматически — + **это и есть база для резидентного кода W3** (§6, подход A). +- `loader>0` (multi-bank .exe) → DSS грузит только HOME-часть и **оставляет + файл открытым** (handle в `IX-3`), а crt0 дочитывает банки сам (§5). + +Упаковкой в этот формат занимается `toolchain/mkexe`. + +--- + +## 3. Правила стека (критично) + +- **`SP ≤ 0xBFFF`** при вызовах DSS (ESTEX, `RST #10`). +- **`SP ≥ 0x8000`** при вызовах некоторых функций BIOS (`RST #08`). +- Пересечение этих требований → **стек обязан жить в W2** (`0x8000-0xBFFF`). + По умолчанию `SP` инициализируется в `0xBFFE`. + +Проверено (`tests/w3probe`): во всех режимах на входе `main` `SP ≈ 0xBFFC`, т.е. +в W2. + +**Chicken-and-egg для split-режимов** (small/huge): DSS ставит `SP=0xBFFE` из +заголовка, но для программ `<16 КБ` окно W2 ещё не выделено (там `#FF`). Пуши +туда теряются, первый `call` возвращается в мусор. Поэтому `crt0_small`/ +`crt0_banked` сначала работают на **загрузочном стеке в W1** (реальное ОЗУ), +маппят W2 и только потом переставляют `SP=0xBFFE`. Маппинг W2 делается через +**ESTEX `$3A SETWIN2`** (не BIOS `$C4`+OUT — тому нужен стек уже в W2). + +--- + +## 4. Режимы памяти + +Выбираются флагом `sprinter-cc --memory MODE` (по умолчанию `tiny`). Режим задаёт +адрес кода, размещение данных, crt0 и наличие банков. + +| Режим | CODE | DATA/BSS | Стек | Банки | crt0 | Первичный загрузчик | +|--------|------|----------|------|-------|------|----------------------| +| `tiny` | W2 `0x8100` | за кодом (W2) | W2 | — | `crt0.s` | нет (1 страница) | +| `small` | W1 `0x4100` | за кодом (W1→W2) | W2 | — | `crt0_small.s` | да (сам маппит W2) | +| `big` | W2 `0x8100` | за кодом (W2) | W2 | W1 (трамплины) | `crt0_banked.s` (BANK_W1) | да | +| `huge` | W1 `0x4100` | за кодом (W1→W2) | W2 | W3 (трамплины) | `crt0_banked.s` | да | +| `manual` | явно | явно | W2 | — | `crt0.s` | зависит | + +Во всех режимах **DATA цепляется линкером сразу за кодом** (`--data-loc 0`), а не +кладётся по фиксированному адресу. Раньше `huge` использовал фиксированный +`DATA=0x8000`, что ломалось при коде `>16 КБ` (код перетекал в W2 и накрывал +DATA); сейчас DATA динамически идёт за концом кода. + +### 4.1 `tiny` — всё в одной странице + +``` +0x8000..0x80FF зарезервировано (startup-prefix) +0x8100 _start / _CODE … _DATA … _BSS … _HEAP +0xBB00 heap top (по умолчанию) +0xBFFE стек ↓ +``` + +- Один блок 16 КБ, DSS маппит его в W2. W1 и W3 = `#FF`. +- Ничего выделять/маппить не надо; `crt0.s` предполагает, что W2 уже дан DSS. +- Практический потолок кода+данных+кучи+стека ≈ 14 КБ. +- `--code-loc 0x8100 --data-loc 0`; mkexe `-L 0x8100 -E 0x8100 -S 0xBFFE`. + +### 4.2 `small` — CODE в W1, данные перетекают в W2 + +``` +0x4100 _CODE … (W1) + … за кодом → _DATA _BSS _HEAP (W1, перетекает в W2) +0xBB00 heap top +0xBFFE стек ↓ (W2) +``` + +- Покрывает ~0..30 КБ (код+данные вместе). +- `crt0_small.s` авто-определяет W2: читает порт `0xC2`. Если `≠0xFF` — DSS уже + дал W2 (образ `>16 КБ`), маппить не надо. Если `=0xFF` — сам выделяет страницу + (`ESTEX $3D GETMEM`) и маппит (`ESTEX $3A SETWIN2`). +- `--code-loc 0x4100 --data-loc 0`. + +### 4.3 `big` — tiny + банки в W1 + +- База как `tiny` (CODE+DATA+стек в W2, `0x8100`). +- **Свапаемые банки в W1** (`0x4000-0x7FFF`, порт `0xA2`), вызываются через + трамплины (§5). `crt0_banked.s` собирается с `BANK_W1=1`. +- mkexe получает `-B 0x4000` (банки живут в W1). Виртуальный адрес банка N = + `0x{N}4000`. +- W3 свободно — доступно под графику или резидентный код (`--w3`). + +### 4.4 `huge` — small + банки в W3 + +- База как `small` (CODE `0x4100`, DATA за кодом, авто-детект W2). +- **Свапаемые банки в W3** (`0xC000-0xFFFF`, порт `0xE2`), через трамплины. + Виртуальный адрес банка N = `0x{N}C000`. +- `crt0_banked.s` (без `BANK_W1`) грузит банки из .exe после старта. + +### 4.5 `manual` — явное размещение + +`--memory manual --memory-manual SPEC`, где SPEC = список `KEY=VAL`: +`CODE=W1|W2`, `DATA=W1|W2|SAME`, `BANKED=W1|W3`. Плюс прямые `--code-loc` / +`--data-loc` / `-L` / `-E` / `-S` перекрывают дефолты любого режима. + +--- + +## 5. Банки и трамплины (`--bank`) + +Для `big`/`huge`. Модуль-банк собирается в отдельную область и линкуется по +**виртуальному 24-битному адресу** (`bank_id` в старшем байте): + +``` +sprinter-cc --memory huge --bank 1=engine.c --bank 2=audio.c -o app.exe main.c +``` + +- Банк N компилируется `--codeseg/--constseg/--dataseg BANKN`, линкуется + `-Wl-b_BANKN=0x{N}C000` (huge) или `0x{N}4000` (big). +- `main.c` обязан объявить `const uint8_t n_banks = N;` — `crt0_banked` читает + это **до gsinit**, поэтому только `const` (инициализатор ещё не скопирован). +- Функции банка помечаются `__banked` — SDCC генерирует вызов через трамплин + `___sdcc_bcall_ehl` (в `_CODE`/W1, всегда замаплен): он сохраняет текущую + страницу окна, маппит нужный банк (`_bank_pages[id]`), `jp` в функцию, по + возврату восстанавливает страницу. +- Загрузка: mkexe пакует `header + HOME + bank1(16К) + bank2(16К)…`, + `loader=размер HOME`; `crt0_banked` выделяет страницы (`GETMEM`), маппит и + дочитывает каждый банк `ESTEX READ` из открытого файла. +- Проверка размеров банков — `toolchain/check_banks.py` (часть отчёта + раскладки, см. §10). + +Writable bank-local данные возможны, но с оговорками — см. +`memory/bank_local_data_pattern`. + +--- + +## 6. Резидентный код окна W3 (`--w3`) + +**Альтернатива банкам без трамплинов.** Модуль размещается резидентно в W3 +(`0xC000`) и вызывается **прямым `call`** — как обычная функция. Работает во +**всех режимах** (`tiny|small|big|huge`); по умолчанию подразумевает `small`. + +``` +sprinter-cc --w3 render.c -o app.exe main.c # → small +sprinter-cc --memory huge --w3 render.c --bank 1=lvl.c -o app.exe main.c +``` + +**Как работает (подход A):** образ с областью `W3CODE`@`0xC000` тянется до `0xC0xx` +(≥3 страницы), и DSS сам маппит W3 при загрузке (§2) — **загрузчик в crt0 не +нужен** (кроме huge). Прямые вызовы резолвятся линкером в реальные `0xC0xx`. + +**Механизм сборки:** +- W3-модуль компилируется `--codeseg W3CODE --constseg W3CODE` — код и rodata в + W3. **`--dataseg` НЕ переопределяется**: писучие статики уходят в обычный + `_DATA` (W2). Отсюда правило «в W3 только код + rodata». +- Линк `-Wl-b_W3CODE=0xC000`. + +**Раскладка страниц по режимам (проверено MAME):** + +| Режим | W1 | W2 | W3 | +|-------|----|----|----| +| tiny | `#FF` (не исп.) | код+данные | **резидент** | +| small | код | данные | **резидент** | +| big | банк (трамплин) | код+данные | **резидент** | +| huge | код | данные | **резидент делит окно с трамплин-банками** | + +**huge — особый случай (резидент + банки в одном окне W3):** +- `crt0_banked` под `.ifdef W3_RESIDENT` захватывает физ-страницу резидента + (`in a,(0xE2)`) сразу после загрузки DSS и **возвращает её дефолтом** после + цикла загрузки банков (иначе в W3 остался бы последний банк). +- Трамплин на каждый `__banked`-вызов сам сохраняет/восстанавливает страницу + W3 — поэтому дефолтная страница обязана быть резидентной. +- mkexe получает флаг `-W` (разрешить HOME тянуться в W3 при наличии W3-банков — + намеренное совмещение). + +**Правила разработчика:** +- Код в W3 **не переключает** страницу W3. +- Код в W1/W2, свапающий W3 на другую страницу (скретч, графика), обязан + **вернуть исходную** (`in a,(0xE2)` → работа → `out (0xE2),a`); оборачивать в + `DI/EI`, если есть ISR, дёргающий W3. +- В W3 — **только код + rodata**, писучих переменных там быть не должно. +- Из `__banked`-контекста (пока в W3 замаплен банк) резидентный W3-код + **недостижим** транзитивно. Обратное — резидент → `__banked` через трамплин + W1 — **работает** (трамплин вернёт резидентную страницу перед `ret`). + +Подробности и артефакты — `memory/w3_resident_code`, тест `tests/w3probe`. + +--- + +## 7. Куча и стек + +- **Стек**: init `SP=0xBFFE`, растёт вниз. Меняется через `-S 0xADDR`. +- **Куча**: от конца `_BSS` вверх до `___sdcc_heap_end` (по умолчанию `0xBB00` → + ~1278 байт под стек). `malloc` берёт `&___sdcc_heap_end` как потолок. +- `runtime/heap.s` — динамическая куча (авто-размер = зазор BSS…heap_top), НЕ + фиксированный `.ds`. +- `--stack-size N` регенерирует `heap_top` как `HEAP_TOP = 0xBFFF - N`, зажимая + рост кучи ради стека. + +--- + +## 8. Семейство crt0 + +Выбирается режимом; `--crt0=TYPE` перекрывает. + +| crt0 | Файл | Для чего | +|------|------|----------| +| `default` | `runtime/crt0.s` | tiny/manual; парсит argv, argv[0] через APPINFO | +| `minimal` | `runtime/crt0_minimal.s` | tiny без argv (меньше размер) | +| `small` | `runtime/crt0_small.s` | small; авто-детект/выделение W2 | +| `banked` | `runtime/crt0_banked.s` | big/huge; авто-детект W2 + загрузка банков | + +`crt0`/`bank.s` собираются **пер-сборка** внутри `sprinter-cc` (с префиксами +`BANK_W1` / `W3_RESIDENT` / `DEBUG_RT`), а не бандлятся в библиотеку. + +--- + +## 9. Типовые грабли + +- **`static`-переменные читаются как `0xFF` / не меняются** — DATA попала в + невыделенное окно (`#FF`). Причина: код и данные в разных окнах при образе + `<16 КБ`, где DSS дал только одну страницу. Лечится правильным режимом + (`small`/`huge`) или единым окном (`tiny`). См. + `memory/sprinter_memory_modes`. +- **Крэш после первого `call` в split-режиме** — стек ещё в невыделенном W2. + Решает загрузочный стек в W1 (уже в crt0). +- **Банк «прыгает в мусор»** — таблица `_bank_pages[]` в `_DATA` занулилась + gsinit’ом; она обязана жить в `_CODE`. Уже исправлено в `bank.s`. +- **`--w3`: резидент недоступен из банка** — ожидаемо (см. §6); держи вход в + резидент только из W1/W2 или из самого W3-кода. + +--- + +## 10. Отчёт по раскладке памяти (после линковки) + +`sprinter-cc` печатает отчёт `toolchain/check_banks.py` для **любой** модели +памяти (по `.map`). Показывает, где легли код/данные и **сколько свободно** +в каждом окне и банке: + +``` +memory: huge — CODE в W1 (0x4100), DATA→W2, банки в W3 + _CODE @ 0x4100 size 3767 (W1) → 0x4FB7 + данные @ 0x4FB7 size 336 (W1) → 0x5107 [_DATA/_BSS/_INITIALIZED/…] + статика до 0x5107 — куча 0x5107..0xBB00 (27129 Б), стек 0xBB00..0xBFFE (1279 Б) + _W3CODE @ 0xC000 size 249 (резидент W3) → 0xC0F9, 16135 Б свободно до 0x10000 OK + _BANK1 @ 0x0001C000 size 219 / 16384 ( 1.3%) → 16165 Б свободно OK +``` + +- `_CODE` / `данные` — окно (W1/W2), размер, конец; строка `статика … куча … + стек …` = свободное место в W1/W2 (под кучу malloc до `___sdcc_heap_end` и + под стек). +- `_W3CODE` — только при `--w3`: остаток окна W3. +- `_BANKn` — только при `--bank`: занятость/остаток каждого 16 КБ-банка. +- Ненулевой выход (проглатывается `|| true`), если банк > 16 КБ или статика + заехала за `heap_top`/`0xC000`. +``` diff --git a/docs/samples/balls/balls.ACT b/docs/samples/balls/balls.ACT new file mode 100644 index 0000000..c969a60 Binary files /dev/null and b/docs/samples/balls/balls.ACT differ diff --git a/docs/samples/balls/balls.asm b/docs/samples/balls/balls.asm new file mode 100644 index 0000000..c179166 --- /dev/null +++ b/docs/samples/balls/balls.asm @@ -0,0 +1,721 @@ +;------------------------------------------------------------------------------- +; Balls mini-demo +; (c) Sayman 2015 +;------------------------------------------------------------------------------- +; , C. +;------------------------------------------------------------------------------- + +tbuf_addr: equ 4000h +screen_addr: equ 0xc000 + + +balls_pgn equ 1 +bgspr_pgn equ 6 +savescr_pgn equ 6 +tbuf_pgn equ 1 + +MAX_BALLS equ 32 ; 96 balls_cfg 576 + +balls_pal_size equ 128 +bgspr_pal_size equ 128 + +balls_struct: +.x equ 0 ; u16_t +.y equ 2 ; u8_t +.dx equ 3 ; i8_t +.dy equ 4 ; i8_t +.radius equ 5 ; u8_t + +balls_struct_bkp: +.x equ 0 ; u16_t +.y equ 2 ; u8_t + + + + include "dss.inc" + include "head.inc" + + +entry: push ix + call init + call read_bgspr + ld a,(pg_tbl.tbuf) + out (cpu_w1),a + ld a,(tmp_hndl) + call close + call read_bgpal + call read_balls_spr + call read_balls_pal + call vinit + call balls_reindex + call balls_init + call draw_bgspr + call CopySCR_1to2 + ld a,0xc0 + out (port_y),a + ld a,(pg_w3) + out (cpu_w3),a + call balls_main + sub a + jp quit0 + + + + include "misc.asm" + include "math.inc" + + +rand16 ld de,.seed ; Seed is usually 0 + ld a,d + ld h,e + ld l,253 + or a + sbc hl,de + sbc a,0 + sbc hl,de + ld d,0 + sbc a,d + ld e,a + sbc hl,de + jr nc,.rand + inc hl +.rand ld (rand16+1),hl + ret + +.seed: dw 0 + + + +inc_pg1: ld a,(ix) + out (cpu_w1),a + inc ix + ret + +inc_pg3: ld a,(ix) + out (cpu_w3),a + inc ix + ret + + +init: ld c,balls_pgn + ld hl,pg_tbl.balls +.loop0: push bc + push hl + call gmem + pop hl + pop bc + jp c,gmem_err + ld (hl),a + inc hl + dec c + jr nz,.loop0 + ld c,bgspr_pgn +.loop1: push bc + push hl + call gmem + pop hl + pop bc + jp c,gmem_err + ld (hl),a + inc hl + dec c + jr nz,.loop1 + ld c,savescr_pgn +.loop2: push bc + push hl + call gmem + pop hl + pop bc + jp c,gmem_err + ld (hl),a + inc hl + dec c + jr nz,.loop2 + ld c,tbuf_pgn +.loop3: push bc + push hl + call gmem + pop hl + pop bc + jp c,gmem_err + ld (hl),a + inc hl + dec c + jr nz,.loop3 + ex af,af + in a,(cpu_w1) + ld (pg_w1),a + in a,(cpu_w3) + ld (pg_w3),a + in a,(port_y) + ld (old_y),a + ex af,af + out (cpu_w1),a + ret + + + +set_scr_w3: out (cpu_w3),a + ret + +set_scr_w1: out (cpu_w1),a + ret + + + +vinit: ld a,81h + ld bc,50h + rst 10h + call cls + call set_global_pal + call CopySCR_1to2 + ld a,norm_scr + call set_scr_w3 + ret + + +CopySCR_1to2: sub a + out (port_y), a + ld hl, 0xc000 + ld de, 0xc140 + ld bc, 320 + ld d, d + ld a, 0 ; 256 bytes + ld a, a +; ldir +.loop: REPT 64 + ldi + ENDM + jp pe,.loop + + ld b, b + ld hl, 0xc3E0 + ld de, 0xc3E4 +; ld bc, 0004h + ld a, a + ldi + ldi + ldi + ldi +; ldir + ld b, b + ret + + +cls: ld hl,0xc3E0 + ld de,0 + ld d,d + ld a,0 + ld b,b + ld b,4 +.loop1: ld a,d + out (port_y),a + ld e,e + ld (hl),e + ld b,b + inc hl + djnz .loop1 + + ld hl,0xc000 + ld de,0 + ld bc,320 + ld d,d + ld a,0 + ld b,b +.loop2: ld a,d + out (port_y),a + ld e,e + ld (hl),e + ld b,b + inc hl + dec bc + ld a,b + or c + jr nz,.loop2 + ret + +set_global_pal: ld bc,0ffa4h + ld hl,pal + ld de,0 + sub a + rst 8 + ret + + + +read_bgspr: ld hl,data_files.bgspr + push hl + call open + pop hl + ld (open_err.err_file+1),hl + jp c,open_err + ld (rd_err.err_file+1),hl + ld (tmp_hndl),a + ld ix,pg_tbl.bgspr +.loop: call inc_pg1 + ld a,(tmp_hndl) + ld hl,tbuf_addr + ld de,16320 + push ix + call read + pop ix + jp c,rd_err + or a + ret nz + jr .loop + +read_bgpal: ld hl,data_files.bgpal + push hl + call open + pop hl + ld (open_err.err_file+1),hl + jp c,open_err + ld (rd_err.err_file+1),hl + ld (tmp_hndl),a + ld hl,pal + ld de,bgspr_pal_size*4 + call read + jp c,rd_err + ld a,(tmp_hndl) + call close + ret + +read_balls_spr: ld hl,data_files.balls + push hl + call open + pop hl + ld (open_err.err_file+1),hl + jp c,open_err + ld (rd_err.err_file+1),hl + ld (tmp_hndl),a + ld hl,balls_spr + ld de,1024 + call read + jp c,rd_err + ld a,(tmp_hndl) + call close + ret + +read_balls_pal: ld hl,data_files.balls_pal + push hl + call open + pop hl + ld (open_err.err_file+1),hl + jp c,open_err + ld (rd_err.err_file+1),hl + ld (tmp_hndl),a + ld hl,pal+bgspr_pal_size*4 + ld de,balls_pal_size*4 + call read + jp c,rd_err + ld a,(tmp_hndl) + call close + ret + + +balls_reindex: ld hl,balls_spr + ld c,128 + ld de,1024 +.loop0: ld a,(hl) + inc a + jr z,.loop1 + dec a + add a,c + ld (hl),a +.loop1: inc hl + dec de + ld a,e + or d + jr nz,.loop0 + ret + + +balls_init: ld hl,balls_cfg + ld de,balls_cfg+1 + ld bc,MAX_BALLS*6 + xor a + ld (hl),a + ldir + ld hl,balls_bkp + ld de,balls_bkp+1 + ld bc,MAX_BALLS*3 + xor a + ld (hl),a + ldir + ld iy,MAX_BALLS + ld ix,balls_cfg +.loop0: ld de,320-16 ; 1616 + push de + call rand16 ; X + pop de + inc hl + push de + call lmod + pop de + ex de,hl +.next0: ld (ix+balls_struct.x),e + ld (ix+balls_struct.x+1),d + ld de,256-16 + push de + call rand16 + pop de + inc hl + push de + call lmod + pop de + ex de,hl +.next1: ld (ix+balls_struct.y),e + call rand16 + ld a,l + and 1 + jr nz,.next2 + ld a,-1 +.next2: ld (ix+balls_struct.dx),a + call rand16 + ld a,l + and 1 + jr nz,.next3 + ld a,-1 +.next3: ld (ix+balls_struct.dy),a + ld de,6 + add ix,de + dec iy + ld a,iyl + or iyh + jr nz,.loop0 + ret + + +draw_bgspr: ld ix,pg_tbl.bgspr + call inc_pg1 + ld hl,tbuf_addr + ld c,51 + ld a,0 +.loop: ld de,screen_addr + out (port_y),a + ex af,af + di + ld d,d + ld a,0 + ld l,l + ld a,(hl) + ld (de),a + ld b,b + inc h + inc d + ld d,d + ld a,64 + ld l,l + ld a,(hl) + ld (de),a + ld b,b + ld a,64 + add a,l + ld l,a + adc a,h + sub l + ld h,a + dec c + call z,nextpage + ex af,af + inc a + jr nz,.loop + ei + halt + di + ret + +nextpage: call inc_pg1 + ld hl,tbuf_addr + ld c,51 + ret + + +balls_main: + +.loop1: ld iy,MAX_BALLS + ld ix,balls_cfg + call save_coords + push ix + push iy +.loop0: ld e,(ix+balls_struct.x) + ld d,(ix+balls_struct.x+1) + ld hl,320-16 + call check_x + ld c,(ix+balls_struct.y) + ld hl,256-16 + call check_y + ld a,(ix+balls_struct.dx) + ld h,0 + ld l,a + rlca + jr nc,.a1 + dec h +.a1: rrca + add hl,de + ex de,hl + ld (ix+balls_struct.x),e + ld (ix+balls_struct.x+1),d + ld a,(ix+balls_struct.dy) + add a,c + ld (ix+balls_struct.y),a + ld de,6 + add ix,de + dec iy + ld a,iyl + or iyh + jr nz,.loop0 + pop iy + pop ix + + ld a,norm_scr|trans_scr|tmp_scr + call set_scr_w3 + +.loop2: ld hl,balls_spr + ld a,iyl + and 3 + ld e,(ix+balls_struct.x) + ld d,(ix+balls_struct.x+1) + ld c,(ix+balls_struct.y) + call draw_balls + ld de,6 + add ix,de + dec iy + ld a,iyl + or iyh + jr nz,.loop2 + + ei + halt + di + in a,(rgmod) + and 1 + xor 1 + out (rgmod),a + + ld a,norm_scr + call set_scr_w3 + ld iy,MAX_BALLS + ld ix,balls_bkp +.loop3: ld e,(ix+balls_struct.x) + ld d,(ix+balls_struct.x+1) + ld c,(ix+balls_struct.y) + call restore_bg + ld de,3 + add ix,de + dec iy + ld a,iyl + or iyh + jr nz,.loop3 + +.blah: call check_key + jr c,._ret_ + jp .loop1 +._ret_: ret + +save_coords: push ix + push iy + ld hl,balls_bkp +.loop: ld e,(ix+balls_struct.x) + ld d,(ix+balls_struct.x+1) + ld c,(ix+balls_struct.y) + ld (hl),e + inc hl + ld (hl),d + inc hl + ld (hl),c + inc hl + ld de,6 + add ix,de + dec iy + ld a,iyh + or iyl + jr nz,.loop + pop iy + pop ix + ret + + +restore_bg: + in a,(rgmod) + or a + ld a,0xc0 + jr nz,.restore_to_0 + ld a,0xc1 + jr .restore_to_1 +.restore_to_0: add a,d + ld d,a + ld h,d + ld l,e + jr .restore +.restore_to_1: add a,d + ld d,a + ld a,0x40 + add a,e + ld e,a + adc a,d + sub e + ld d,a + ld h,d + ld l,e +.restore: ld a,c + out (port_y),a + ld b,16 + di +.loop: ld d,d + ld a,16 + ld l,l + ld a,(hl) + ld (de),a + ld b,b + in a,(port_y) + inc a + out (port_y),a + djnz .loop + ret + + + +draw_balls: ld b,a + in a,(rgmod) + or a + ld a,0xc1 + jr z,.draw_to_1 ; 0 , 1 + ld a,0xc0 ; (1) 0 +.draw_to_0: add a,d + ld d,a + jr .draw +.draw_to_1: add a,d + ld d,a + ld a,0x40 + add a,e + ld e,a + adc a,d + sub e + ld d,a + +.draw: ld a,b + add a,a + add a,a + add a,a + add a,a + add a,l + ld l,a + adc a,h + sub l + ld h,a ; *256 = + ld a,c + out (port_y),a + ld b,16 + di +.loop: + ld d,d + ld a,16 + ld l,l + ld a,(hl) + ld a,a + ld (de),a + ld b,b + ld a,c + out (port_y),a + ld a,l + add a,64 + ld l,a + ld a,0 + adc a,h + ld h,a + inc de + djnz .loop + ret + +check_x: ld a,e + or d + jr z,.next0 + sbc hl,de + jr z,.next1 + jr c,.next1 + ret +.next0: ld a,1 + ld (ix+balls_struct.dx),a + ret +.next1: ld a,-1 + ld (ix+balls_struct.dx),a + ret + + +check_y: ld a,c + or a + jr z,.next0 + ld a,l + sub c + jr z,.next1 + jr c,.next1 + ret +.next0: ld a,1 + ld (ix+balls_struct.dy),a + ret +.next1: ld a,-1 + ld (ix+balls_struct.dy),a + ret + + +sync: ei + halt + di + ret + + +check_key: push hl + push de + push bc + push ix + push iy + ld c,scankey + rst 10h + pop iy + pop ix + pop bc + pop de + pop hl + xor 1bh ; Esc... + ret nz +.esc: scf + ret + + + + +pg_tbl: +.balls: db 0 +.bgspr: ds 6 +.savescr: ds 6 +.tbuf: db 0 + +pg_w0: db 0 +pg_w1: db 0 +pg_w2: db 0 +pg_w3: db 0 +old_y: db 0 + + +data_files: +.balls: db "balls.spr",0 +.balls_pal: db "balls.act",0 +.bgspr: db "bgspr.spr",0 +.bgpal: db "bgspr.act",0 + +tmp_hndl: db 0 + + +open_err_str: db "Can't open file ",0 +read_err_str: db "Failed to read file ",0 +gmem_err_str: db "PANIC: Can not allocate memory!",cr,lf,0 +vm_err_str: db "PANIC: Unable to set videomode!",cr,lf,0 + +crlf0: db " ",cr,lf,0 + + +pal: equ ($/80h)*80h+80h +balls_spr: equ ((pal+1024)/80h)*80h+80h +balls_cfg: equ ((balls_spr+1024)/80h)*80h+80h +balls_bkp: equ (((balls_cfg)+6*MAX_BALLS)/80h)*80h+80h \ No newline at end of file diff --git a/docs/samples/balls/balls.bmp b/docs/samples/balls/balls.bmp new file mode 100644 index 0000000..f3e5e6a Binary files /dev/null and b/docs/samples/balls/balls.bmp differ diff --git a/docs/samples/balls/balls.exe b/docs/samples/balls/balls.exe new file mode 100644 index 0000000..751a58c Binary files /dev/null and b/docs/samples/balls/balls.exe differ diff --git a/docs/samples/balls/balls.spr b/docs/samples/balls/balls.spr new file mode 100644 index 0000000..91bcc2b --- /dev/null +++ b/docs/samples/balls/balls.spr @@ -0,0 +1,58 @@ +  + + + + +  + + + + + + + + +  + + + + + + +  + + + + +  + + +  + + +  + + + +  +  + +   +  + +   +  + +   +  + +   +  + +   + + +  + + +  \ No newline at end of file diff --git a/docs/samples/balls/balls.txt b/docs/samples/balls/balls.txt new file mode 100644 index 0000000..a565d0b --- /dev/null +++ b/docs/samples/balls/balls.txt @@ -0,0 +1,38 @@ + evo-sdk ( C) zx-evo, + . , + . : + +balls.asm - +dss.inc - +head.inc - exe +misc.inc - "" . +math.inc - , ( HTC) +make.bat - . make src_name. +sjasm.exe - . . +balls.exe - exe. 32 . +balls64.exe - 64 . +balls128.exe - 128 . +balls225.exe - 225 . +balls.ect - . 32. + bmp . +balls.spr - (). bmp. +bgspr.act - . 32 bmp. +bgspr.spr - , bmp. + + 64*16 (.. 4 , +16*16), 32 , .. 128 . + 320*256, 128 . + , , +"" 8 , 128 . + ( "", , + . + winhex bmp , bmp + , . + . + . + . 0 7fh. + . 256 , 128 + , . + != 0xff, +=128. + + !!! \ No newline at end of file diff --git a/docs/samples/balls/balls128.exe b/docs/samples/balls/balls128.exe new file mode 100644 index 0000000..f56f417 Binary files /dev/null and b/docs/samples/balls/balls128.exe differ diff --git a/docs/samples/balls/balls225.exe b/docs/samples/balls/balls225.exe new file mode 100644 index 0000000..71857d0 Binary files /dev/null and b/docs/samples/balls/balls225.exe differ diff --git a/docs/samples/balls/balls64.exe b/docs/samples/balls/balls64.exe new file mode 100644 index 0000000..674cfdd Binary files /dev/null and b/docs/samples/balls/balls64.exe differ diff --git a/docs/samples/balls/bgspr.act b/docs/samples/balls/bgspr.act new file mode 100644 index 0000000..49f7359 Binary files /dev/null and b/docs/samples/balls/bgspr.act differ diff --git a/docs/samples/balls/bgspr.bmp b/docs/samples/balls/bgspr.bmp new file mode 100644 index 0000000..c892d47 Binary files /dev/null and b/docs/samples/balls/bgspr.bmp differ diff --git a/docs/samples/balls/bgspr.spr b/docs/samples/balls/bgspr.spr new file mode 100644 index 0000000..841bc3b --- /dev/null +++ b/docs/samples/balls/bgspr.spr @@ -0,0 +1,1501 @@ +                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  + +  ''          + + + + +   '            + + + + + + +    ''        + + + + + + +    ''        + + + + + + +   ''        + + + + + + +   '''         + + + + + + + + + +   ''%'        + + + + + + + + + + + +  + ''%%          + + + + + + + + + + + + + + ''%%%''''          + + + + + + + + + + + + + + + + + ''''%%%'''''         + + + + + + + + + + + + + + + + + + + + + ''''%%%'''''         + + + + + + + + + + + + + + + + + + + + + '''''%%%%'''''''        + + + + + + + + + + + + + + + + + + + + + + + + + + ''%%%%%'''''''''        + + + + + + + + + + + + + + + + + + + + + + + %%&&&''%%''''''''           + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &&&&%%''''''''''          + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &&'''''''''''        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &&'''''''''''        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''''''''''         + + + + + + + + + + + + + + + + + + + + + + + + + + + +'''''''''''        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +'''%%%''''''''       + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +,,''%%%'''''''''''      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +,,'''%%%%'''''''''''  + +    + + + + + + +  + +  + + + + + + + + + + + +  + + + + + +,,,,%''%%%&'''''''''''  + + + +    + + + + + +  +  + + + + + + + + + + + + + + +,,,,%''%%%&'''''''''''  + + + +    + + + + + +  +  + + + + + + + + + + + + + + +,,,,,,%%%%&&''''%'''%''  + + + +   + +  + + + + + + + + + + + +  + + + + + + + + + +  + + + + + + + + +,,,,,%''%&&%,,,''%%%%%%'  + + + + + +  + + + +  + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +,,,,,%''&&&%,,,'''%%%%%%% + + + + + +  + + + + +   + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +  + + + + + + + + +,,,,,,''''&&%,,,,,'''%%%% + + + + + + + +  + + + + +   + + + + + + + + + + + + + + + + + + + + + + + + + + +  + + + + + + + + + + + +,,,,,,,,'''%&&%',,,,''%&&& + + + + + + + +  + + + + + +   + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +,,,,,,,,'''%&&%',,,,''%&&& + + + + + + + +  + + + + + +   + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +,,,,,,,'''%%&%%',,,,''%&&& + + + + + + + + + + + + + + + + + + +   + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +,,,,,,'''%%%&&,,,,,,'%%&& + + + + + + + + + + + + + + + + + + + + + + + +  + + + + + + + + + + + + + + + + + + + + + + + + + + + + +,,,,,,,''''%&&&,,,,,''%%& + + + + + + + + + + + + + + + + + + + + + + + + + + +   + + + + + + + + + + + + + + + + + + + + +,,,,,,,,%'''%&&%&,,,,''%&% + + + + + + + + + +  + + + + + + + + + + + + +   + + + + + + + + + + + + + + + +,,,,,,,,'''%%%%&%,,,,''%& + + + + + + + + + +  + + + + + + + + + +   + + + + + + + + + + +,,,,,,,,''%%%%&&&&,,,,,''%&& + + + + + + + + + + +  + + + + + + + +  + + + + + + + + + + + + +,,,,,,,,''%%%%&&&&,,,,,''%&& + + + + + + + + + + +  + + + + + + + +  + + + + + + + + + + + + +,,,,,,,,,''%%%''%&%&,,,,,'''%&& + + + + + + + + + + + + + + + + + + + + +  + + + + + + + + + + + + + + +,,,,,,,,,,,,,''''%%%%''%%&&,,,,,''%%&& + + + + + + + + + + + + + + + + + + + +  +  + + + + + + + + + + + + + + + +,,,,,,,,,,,''''''%%%'%%'%&%,,,,''%%%&% + + + + + + + + + + + + + + +  + + + + + + + + + + + + + + + + + +,,,,,'''%%''%%%%%%'&&%&&,,,'''''%&& + + + + + + + + + + + + + +  + + + + + + + + + + + + + +%,,,'''''%%'''%'%%%'&&&,,''''''%%&'' + + + + + + + + + + + """" + + + + + + + + + + + + + %,,,'''''%%'''%'%%%'&&&,,''''''%%&'' + + + + + + + + + + + """" + + + + + + + + + + + + + '''%%'%%&''%%%%'%%%%%,,,''''''%%&&%%%'''' + + + + + + + + + + #""""" + + + + + + + + + + + + + + + + ''%%%'&&&'%%'%'%%&,''''''%%%&%'''''' + + + + + + + + + + +  + +###""""" + + + + + + + + + + + + + + + + + + + + +  + + + + + + + + + %%''&%&'''''&&''''''%%%''''' + +  + + + + + +#####""# + + + + + + + + + + + + + + + + + + + + +  + + + + +  '%%'''%&%''' +  + + + + + + +#######"$$$ + + + + + + + + + + + + + + + + + + +  + +  ''%&'''  + + + +#$$####""$$$$ +" + + + + + + + + + + + + + + + + + + + + + ''%&'''  + + + +#$$####""$$$$ +" + + + + + + + + + + + + + + + + + + + + + ''%''' +##"$$$""$$"$$$$!"$ + + + + + + + + + + + + + + +  +   '''''' + +#!$""$$"$$$"$$$$! + + + + + + + + + + +  '''!!"""""#!#!$$"$$"$$$"$$"$"!#$$"$$$" + + + + +    ''!"!##"""#!###!#$"$$"$$$"""$!#!#$$"$$$"$$" + +  '##""!####"#!#####!#$"$$"$""$$$###!#$$"$$$"$$"   '!##"!#!!######!#######!#$""""$$"######!#$$"$$$"$$"    '!##"!#!!######!#######!#$""""$$"######!#$$"$$$"$$"    ''''' !#!"##!######!#########!#$$$$##""######!#$"$$"$$"     ''    !!#"" !!!!!!!!!####!!!!!!!##"###""#!!!!!!           !!"!! !!!!!!!#!!!!!!!!!!!###""###"#!!!!!        !!# ! !!!#!!!!! !!!####""###!              !!! !!##                !!! !!##                                                                                                                                                                                              ++           +++/                  +++++((              ++++///((              ++++///((                   /+++//((((            ///++/((((((                   ///++/((((((                    ///++((((                    ///++(                    ///++(                 )....))+/          //))..)...       ///))--.....-       /--))-))....---      ////---)-)))..-----      ////------))).----)..)     ////------))).----)..)  /---   ..))///------)))-----).).--// ///---  --)-...//-------))---/)))..---///////----  ----)...-//----.../////)))------//////////------/    ---)))).-////--....///---////---////////////////--/      /////----))))-////---....-/---/////---///////////--/      /////----))))-////---....-/---/////---/////-/    ///////----))--///----))).)/--/////---    //////////----/)--///-----))))--/////--      /////////---/////////----)))/////        /////////////////--))//         ///////////--//    ///////    ///////    /                      ++  ++++++  ++++++/-------- ++++++++///------------..... ++++++++++/////-------...------.........++++++++++++///////-----......//----.......))..+++++++++///+++++///////-))))).......///--.....)))))))..+++++++//////++++++////////)))))).......////.....))))))----++++/////////++++++////////)))))).......////.....))))))----++++//////////++++++//////////))))))......./////..)))))------.+++/////******///++++///////////.))))))....--/////..)))------...++////**//****////++++///////.....))..-----////..)-----....))++///***///*((////+++++/////////..---------///.)-///....)))))++//***((///(((////+++++////////----------//////////..))))))+++/////**(((((///((////+++++////////----------//////////..))))))+++/////**(((((///((*///++++++///////--------)........///.))))))++++/////**(((((((//((*//+++++++++/////////------).........///.)))---++++//////*(((((((**(((*//+++++++//////++////////---)))))...)))..////)----++++/////*//(((((((****(((//+++++///////////++//))))))))))))))..////-----+++///////*///((((*******((//++++///////////////+)))))))).--///-----++++///****//*///(((********(//+++/////////////////////+//---///------+++++///*******/*///(**********(//+++/////////////////////+//---///------+++++///*******/*///(**********///////*******/////////////+///-----++++++++//**********/*//((*********///////************/////////++///+++++++++++/************//((((*********///////************//*****////++++/+++++++++++++++++////*************//((((********/////(((*********/((/*******/////+++/+++++++++++++++///////(//*************//((((((*****////(((((******//((((**********////++////+++++++++++++++//////*/((/***********(((//(((((*****////(((((******//((((**********////++////+++++++++++++++//////*/((/***********(((//(((((*****////((((((*****//((((***********////+++++++++++++++++++++++++/////****/((/**********((((/((((((****////*(((((((***//((((*************////////++++++++++++++++++++++++////*******/((/*********((((//(((((((((////***((((((**//((((((**************///////////+++++++++++///////////////******(((((/(//*********((((//((((((((//*(****(((((**//((((((((**********************////++++++++////////////////****((((((((((/***(*****(((((//(((((((((*((*****((**//(((((((((*************************//+++++++++/////++++++//////////////////**(((((((((///((((((**(((((((/(((((((((*((*****((**//(((((((((*************************//+++++++++/////++++++//////////////////**(((((((((///((((((**(((((((/((((((((*((((******///(((((((((((************************///+++++++++++++++++++///////+++++++++///////////////***///*((((((((((//(((((((((((((((/((((((((*(((((**((*/(((((((((((((((*((********************/////++++++++++++++++++++++++++/////////+/++++++++++////////////////******/*(((((((((((/*((((((((((((/(//(((((((*(((((((((//((((((((((((((((((*********************/////+++++++++++++++++++++++++++////////////++++++++++++++++++++++////////////////********/(((((((((((((((((((((((((////((((((*((((((((((/((((((((((((((((((((((**************((********////++++++++++++++++++++++++++++++////////////////+++++++++++++++++++++++++++++++++++++//(((((((((((((*********((/(((((((((((((((((((((((((*///(((((*(((((((((//(((((((((((((((((((((((************(((((*******///////+++++++++++++++++++++++++++++++/////////////////++++++++//++++++++++++++++++++++++++///++++++++++++++++++++++/+++++++++++++++///((((((((((((((((******(((((((((((((((((((((((((((((**//(((((((((((((((/(((((((((((((((((((((((*((((*******(((((((*******///////////++++++++/++++++++++++++++++++++++++/////////////////+++++++++++++/////++++++++++++++++++++//++++++++++++++///////+++++++++++++++++++++//////++++++++++++++++++///////(((((((((((((((((**(((((((((((((((((((//((((((((******//((((((((((((((/(((((((((((((((((((((((*((((*******(((((((*******///////////++++++++/++++++++++++++++++++++++++/////////////////+++++++++++++/////++++++++++++++++++++//++++++++++++++///////+++++++++++++++++++++//////++++++++++++++++++///////(((((((((((((((((**(((((((((((((((((((//((((((((******//((((((((((((((/(((((((((((((((((((((**((((((((((*((((((((**************///////////////+++++++++++++++++++++++++++++++++++//////////////////++++++++++++//////////+++++++++++++++++///////++++++++/////////////+++++++++++++++////////////+++++++++++++++++++//////////((((((((((((((((((((((((((((((((((((((////((//((((*******//(((((((((((((/((((((((((((((((((((**((((((((((((((((((((((((**********************/////////++++++++++++++++++++++++++++///////////////////+++++++++++++///////////++++++++//////++//////++++//////////////////++++//////////////////////++++++++++++++++++///////((((((((((((((((((((((((((((((((((((((((/////////////*********//(( \ No newline at end of file diff --git a/docs/samples/balls/dss.inc b/docs/samples/balls/dss.inc new file mode 100644 index 0000000..dbd5cc2 --- /dev/null +++ b/docs/samples/balls/dss.inc @@ -0,0 +1,68 @@ +;------------------------- +;dss functions defines +;file functions +fopen equ 11h +fclose equ 12h +fread equ 13h +fwrite equ 14h +move_fp equ 15h +fgetattr equ 16h +fgetdt equ 17h +fsetdt equ 18h +fcreate equ 0ah +fcreaten equ 0bh +chdir equ 1dh +curdir equ 1eh +;memory functions +setwin1 equ 39h +getmem equ 3dh +setmem equ 3fh +freemem equ 3eh +;keyb functions +waitkey equ 30h +scankey equ 31h +quit equ 41h +getarg equ 43h +;vmode functions +setvmode equ 50h +getvmode equ 51h +selvpage equ 54h +;screen and text functions +pchar equ 5bh +pchars equ 5ch +;other +getver equ 0 +;end dss defines +;------------------------- +;hardware defines +port_y equ 89h ;port for Y coord +_320p equ 81h ;320 pixels mode +rgmod equ 0c9h +border equ 0feh +rgscr equ 0e9h +rgacc equ 0a9h + +norm_scr equ 50h +;trans_scr equ 54h +trans_scr equ 00001000b +tmp_scr equ 00000100b + + +e_cache equ 0fbh +d_cache equ 7bh +sys_port3c equ 3ch +sys_port7c equ 7ch + +cpu_w0 equ 82h ;cpu window 0 = addr 0000h +cpu_w1 equ 0a2h ;... 1 = 4000h +cpu_w2 equ 0c2h ;... 2 = 8000h +cpu_w3 equ 0e2h ;... 3 = 0c000h +;end hardware defines +;------------------------- +;characters +cr equ 0dh +lf equ 0ah +tab equ 9 +space equ 20h +;------------------------ + diff --git a/docs/samples/balls/head.inc b/docs/samples/balls/head.inc new file mode 100644 index 0000000..da66767 --- /dev/null +++ b/docs/samples/balls/head.inc @@ -0,0 +1,20 @@ +; .Z80 +; ASEG +; org 100h + org 8100h-512 + + db "EXE" + db 0 + dw 200h + dw 0 + dw 0 + dw 0 + dw 0 + dw 0 + dw entry + dw entry + dw 0bfffh + ds 490 + +; .PHASE 8100h + diff --git a/docs/samples/balls/make.bat b/docs/samples/balls/make.bat new file mode 100644 index 0000000..de301d8 --- /dev/null +++ b/docs/samples/balls/make.bat @@ -0,0 +1,20 @@ +@echo off +if "%1" == "" goto error + +if EXIST %1.exe ( + del %1.exe +) +sjasm.exe -L %1.asm %1.exe %1.lst +if errorlevel 1 goto ERR +echo Ok! +goto END + +:ERR +del %1.exe +pause +echo 訡 樨... +goto END + +:error +echo usage: make sourcefile +:END \ No newline at end of file diff --git a/docs/samples/balls/math.inc b/docs/samples/balls/math.inc new file mode 100644 index 0000000..2c058f8 --- /dev/null +++ b/docs/samples/balls/math.inc @@ -0,0 +1,89 @@ +;Input: H = Multiplier, E = Multiplicand, L = 0, D = 0 +;Output: HL = Product +mul8_8: ld b,7 + sla h ; optimised 1st iteration + jr nc,$+3 + ld l,e +.loop: add hl,hl ; unroll 7 times + jr nc,$+3 ; ... + add hl,de ; ... + djnz .loop + ret + + +lmod: call ldiv + ex de,hl + ret + +ldiv: xor a + ex af,af' + ex de,hl + jr dv1 + +adiv: ld a,h + xor d ;set sign flag for quotient + ld a,h ;get sign of dividend + ex af,af' + call negif + ex de,hl + call negif +dv1: ld b,1 + ld a,h + or l + ret z +dv8: push hl + add hl,hl + jr c,dv2 + ld a,d + cp h + jr c,dv2 + jp nz,dv6 + ld a,e + cp l + jr c,dv2 +dv6: pop af + inc b + jp dv8 + +dv2: pop hl + ex de,hl + push hl + ld hl,0 + ex (sp),hl + +dv4: ld a,h + cp d + jr c,dv3 + jp nz,dv5 + ld a,l + cp e + jr c,dv3 + +dv5: sbc hl,de +dv3: ex (sp),hl + ccf + adc hl,hl + srl d + rr e + ex (sp),hl + djnz dv4 + pop de + ex de,hl + ex af,af' + call m,negat + ex de,hl + or a ;test remainder sign bit + call m,negat + ex de,hl + ret + +negif: bit 7,h + ret z +negat: ld b,h + ld c,l + ld hl,0 + or a + sbc hl,bc + ret + + diff --git a/docs/samples/balls/misc.asm b/docs/samples/balls/misc.asm new file mode 100644 index 0000000..d047ec6 --- /dev/null +++ b/docs/samples/balls/misc.asm @@ -0,0 +1,113 @@ +; . +; A . +; , -1 (255) +; , 0. +quit0: pop ix + ld b,a + ld c,quit + rst 10h + jp $ ; + +;-[]------------------------------------------------------------ +; misc procedures and functions... +;--------------------------------------------------------------- +open: ld a,1 + ld c,fopen + rst 10h + ret + +close: ld c,fclose + rst 10h + ret + +read: ld c,fread + rst 10h + ret + +;dir_ret: ld hl,cur_dir +; ld c,chdir +; rst 10h +; ret + +gmem: ld c,getmem + ld b,1 + rst 10h + ret + +; . +; C = () +; HL = . +save_pg: in a,(c) + ld (hl),a + ret + +; . +; C = () +; HL = . +restore_pg: ld a,(hl) + out (c),a + ret + + +; +open_err: + ld hl,open_err_str + ld c,pchars + rst 10h +.err_file: ld hl,0 + ld c,pchars + rst 10h + ld hl,crlf0 + ld c,pchars + rst 10h + ld a,-1 + jp quit0 + + +; +rd_err: + ld hl,read_err_str + ld c,pchars + rst 10h +.err_file: ld hl,0 + ld c,pchars + rst 10h + ld hl,crlf0 + ld c,pchars + rst 10h + ld a,-1 + jp quit0 + + +; +gmem_err: ld hl,gmem_err_str + ld c,pchars + rst 10h + ld a,-1 + jp quit0 + +; +vm_err: ld hl,vm_err_str + ld c,pchars + rst 10h + jp quit0 + + IFDEF debug +debug_print: ld c,pchars + rst 10h + ret + ENDIF + +; +;tok_err: +; ld hl,token_err_str +; ld c,pchars +; rst 10h +;.token_ptr: ld hl,0 +; ld c,pchars +; rst 10h +; ld hl,crlf0 +; ld c,pchars +; rst 10h +; ld a,-1 +; jp quit0 diff --git a/docs/samples/balls/sjasm.exe b/docs/samples/balls/sjasm.exe new file mode 100644 index 0000000..2aab2e5 Binary files /dev/null and b/docs/samples/balls/sjasm.exe differ diff --git a/examples/scroll/Makefile b/examples/scroll/Makefile new file mode 100644 index 0000000..0aa7e3d --- /dev/null +++ b/examples/scroll/Makefile @@ -0,0 +1,4 @@ +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := scroll +EXTRA_FLAGS ?= --gfx 256 +include $(PROJ_ROOT)/app.mk diff --git a/examples/scroll/run.sh b/examples/scroll/run.sh new file mode 100755 index 0000000..e9e8c78 --- /dev/null +++ b/examples/scroll/run.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +make clean +make run diff --git a/examples/scroll/scroll-impl-guide.md b/examples/scroll/scroll-impl-guide.md new file mode 100644 index 0000000..beae6d4 --- /dev/null +++ b/examples/scroll/scroll-impl-guide.md @@ -0,0 +1,223 @@ +# Руководство: скроллинг-примитивы для libbgi + +Самодостаточное ТЗ на реализацию двух функций скролла в libbgi. Написано так, +чтобы реализовать с нуля без остального контекста. Итог изысканий по железу +Sprinter (см. §1) и разбора существующего кода libbgi (см. §3). + +**Что делаем:** `gfx_scroll_h` (горизонтальный — нужен Loom для комнат шире +320 px) и `gfx_scroll_v` (вертикальный — для симметрии API; Loom пока не +требует, высота комнат 144 < 256). + +**Где живёт:** расширение `C-Compiler/libbgi` — accel-leaf в `bgi256/`, +обёртка в `common/`, объявление в `include/gfx.h`. + +**СНАЧАЛА прочти §7 (блокеры).** Два пункта надо проверить в dev-MAME до +написания рабочего кода — если они не пройдут, схема «чистого фона» едет. + +--- + +## 1. Аппаратная модель (всё, что нужно знать) + +**Режим `0x81`: 320×256×256, chunky — 1 байт/пиксель.** Пиксель (x,y): CPU-адрес +`0xC000 + x`, строку выбирает **Port_Y** (порт `0x89`) `= y`. Горизонталь — в +адресе, вертикаль — в Port_Y. (Ссылка: `libbgi/include/gfx.h` шапка.) + +**Две видеостраницы различаются ТОЛЬКО базой строки:** page0 → `0xC000`, +page1 → `0xC140` (шаг `0x140` = 320). Обе адресуемы в W3 **одновременно**; +Port_Y и номера банков — общие для обеих. Значит: +> `dst_addr = src_addr + 0x140` копирует page0→page1 без ремаппинга W3. +> Со сдвигом: `dst = src ± 0x140 + dx`. + +**Банки (значение W3-страницы, порт `0xE2` = `0x50..0x5F`)** — режим ЗАПИСИ, +действует на все примитивы до смены (`libbgi/include/gfx.h`): +| Банк | Имя | Запись | Чтение | +|---|---|---|---| +| `0x50` | `GFX_BANK_NORMAL` | VRAM **+ ОЗУ-копия** | ОЗУ-копия | +| `0x54` | `GFX_BANK_NOSHADOW` | только VRAM | ОЗУ-копия | +| `0x58` | `GFX_BANK_TRANSPARENT` | VRAM, байт `0xFF` не пишется | ОЗУ-копия | +| `0x5C` | `GFX_BANK_SPRITE` | VRAM, `0xFF` не пишется | ОЗУ-копия | + +Чтение `#50..#5F` **всегда** отдаёт ОЗУ-копию (видео-ОЗУ write-only). `0xFF` — +аппаратно-прозрачный цвет (`GFX_TRANSPARENT`). + +**Акселератор** (`C-Compiler/docs/converted/accel_r.txt`): внутр. ОЗУ-буфер +1..256 байт. Управление опкодами-«NOP»: +- `LD B,B` — стоп; `LD D,D` затем `LD A,n` — размер блока (`n=0` → 256); +- `LD L,L` — **горизонтальная** копия (буфер ← `LD A,(HL)`; буфер → `LD (DE),A`); +- `LD A,A` (`0x7F`) — **вертикальная** копия (линии экрана, Port_Y шагает внутри + burst'а); +- `LD C,C` / `LD E,E` — fill (гориз./верт.). + +Скорость ≈ **байт / 7 мкс**. Полный экран 320×256 ≈ **26 мс** (это **>1 кадра** +@ 50 Гц — держать в уме, §9). На время работы акселератора — **DI** (меняется +система команд, ISR сломается). + +--- + +## 2. Ключевой инсайт: чистый фон достаётся бесплатно + +- Спрайты рисуются банком с битом 2 (`0x54`/`0x5C`) → **только VRAM**, + ОЗУ-копию не трогают. +- ⇒ ОЗУ-копия (`0x50`) любой страницы — это фон **без спрайтов**, всегда. +- ⇒ скролл-копия из `0x50` в `0x50` читает чистый фон и пишет VRAM+тень — это + **одновременно скролл И heal** целевой страницы (её старые спрайты затираются + сдвинутым фоном). + +Ровно так работает `gfx_heal` (частный случай `src==dst`, §3). Скролл — тот же +механизм, но `src != dst`. + +--- + +## 3. Что уже есть в libbgi — образцы для копирования + +| Файл | Что | Зачем образец | +|---|---|---| +| `bgi256/_bgi_copy_rows_raw.c` | гориз. leaf (`LD L,L`), src/dst + страйды, Port_Y на строку | ядро всех блитов; страйды, SMC, DI | +| `bgi256/_bgi_heal_rows_raw.c` | `src==dst==экран`, страйды 0 | спец-случай heal | +| `bgi256/_bgi_blit_cols_raw.c` | **верт. leaf** (`LD A,A`), колонка=burst, Port_Y сброс на колонку, `dst+=1` | **прямой образец для `scroll_h`** | +| `common/_gfx_heal_full.c` | клип по экрану + нарезка полос >256 + вызов leaf | образец обёртки-ядра | +| `common/gfx_heal.c` | save-банк → `0x50` → `_bgi_begin` → ядро → `_bgi_end` → restore | образец верхней обёртки | +| `docs/converted/accel_r.txt` | screen→screen верт. копия LDIR-идиомом, `dst=src+0x140` | канонический скролл-паттерн | + +Правило из `_bgi_copy_rows_raw`: **одна сторона копии — видео (в W3, строку даёт +Port_Y), другая может быть линейным буфером ВНЕ W3** (`< 0xC000`). Для скролла +обе стороны — видео (см. §4). + +--- + +## 4. `gfx_scroll_h` — горизонтальный (наш, приоритет) + +**Идиом: вертикальный режим `LD A,A`, проход по колонкам.** Высота playfield +(144) ≤ 256 → одна колонка = один burst, **резать 320-строку на 256+64 НЕ надо** +(главная причина брать верт. режим). Это доковый screen→screen паттерн +(`accel_r.txt`) + `_bgi_blit_cols_raw`. + +Параметры на колонку: +- `src = base(src_page) + x` — банк `0x50`, чтение = ОЗУ-копия = чистый фон; +- `dst = base(dst_page) + x + dx` — банк `0x50`, запись = VRAM + тень dst; +- `base`: page0 `0xC000`, page1 `0xC140`; +- размер блока = высота playfield (`h`, для Loom 144); +- Port_Y start = верх playfield (`y0`, для Loom 0); +- копируем `(w − |dx|)` колонок; вакантные `|dx|` колонок **не трогаем**. + +**Знак:** зафиксировать `dx > 0` = сдвиг вправо (открывается слева), `dx < 0` = +влево; вернуть открывшийся край в `*dirty`. + +**Порядок (по образцу докового LDIR-примера + `_bgi_blit_cols_raw`):** банда +колонок под DI → `LD D,D`/`LD A,h` (block) → `LD A,A` (верт. режим) → на колонку +`{ read col в accel; write col из accel; src++; dst++ }` → `EI`. При straight-copy +Port_Y авто-шагает (доковый LDIR его не трогает) — но `_bgi_blit_cols_raw` +сбрасывает Port_Y=`y0` на каждую колонку; **проверить, нужен ли сброс** (§7.1 +покажет). + +**Возврат:** `*dirty` = прямоугольник `|dx|` колонок с открывшегося края; +полосу не чистим — её дозаполняет вызывающий (в Loom — декодер EGA-страйпов). + +```c +/* Горизонтальный скролл playfield. Копирует чистый фон (ОЗУ-копия src_page) + со сдвигом dx в dst_page (обе стороны банк 0x50: запись бьёт VRAM+тень). + dx>0 — вправо. Вакантную полосу НЕ заполняет — возвращает в *dirty. */ +void gfx_scroll_h(uint8_t src_page, uint8_t dst_page, + const gfx_rect_t *area, int dx, gfx_rect_t *dirty); +``` + +--- + +## 5. `gfx_scroll_v` — вертикальный (симметрия; Loom не нужен) + +**Асимметрия железа:** горизонтальный сдвиг — это смещение CPU-адреса (src и dst +независимы) → легко. **Вертикальный сдвиг — это смещение Port_Y, а Port_Y один +на burst** → в простом LDIR-идиоме read и write идут с одного Port_Y, сдвига по +Y нет. Поэтому `scroll_v` сложнее `scroll_h`. Два пути: + +- **(A) менять Port_Y между fill и flush одного burst'а**: `OUT Port_Y=y_src`; + read col в accel; `OUT Port_Y=y_src+dy`; write col из accel. Требует, чтобы + accel-буфер пережил `OUT` между чтением и записью — **не доказано** доком/ + тестами (§7.3). Если пройдёт — симметрично `scroll_h`, верт. режим. +- **(B) безопасный fallback**: grab (video→RAM-буфер вне W3, гориз. режим), + затем blit (буфер→video со сдвигом Y — буферная сторона CPU-адресуема, сдвиг + любой). Два прохода + буфер, но заведомо работает. + +**Рекомендация:** реализовать (A), если §7.3 пройдёт; иначе (B). Делать +**последним** — для Loom не критично. API симметричный (`dy` вместо `dx`): + +```c +void gfx_scroll_v(uint8_t src_page, uint8_t dst_page, + const gfx_rect_t *area, int dy, gfx_rect_t *dirty); +``` + +--- + +## 6. Общие требования реализации + +- **DI по банде, НЕ один DI на весь проход.** Полный playfield ~14 мс под DI + порвёт будущий CBL-звук. Одна колонка (144 Б ≈ 20 мкс) — безопасное DI-окно; + резать проход на банды колонок/строк с `EI` между (как `_bgi_copy_rows_raw` + режет ≤16 строк). Пока звука нет — не критично, но заложить сразу. +- **Клип по `area`** (образец — `_gfx_heal_full`). +- **Буфер (если путь (B))** — вне W3 (`< 0xC000`). +- **Верхняя обёртка**: save банк → `gfx_set_bank(GFX_BANK_NORMAL)` → + `_bgi_begin()` → ядро → `_bgi_end()` → restore (образец — `gfx_heal.c`). +- **Размещение**: leaf `bgi256/_bgi_scroll_cols_raw.c` (+ `_rows_raw` для (A)), + обёртки `common/gfx_scroll_h.c` / `gfx_scroll_v.c`, объявления в + `include/gfx.h`. `gfx_rect_t` — если ещё нет, определить там же + (`int x,y,w,h`). +- **Для Loom (не требование к примитиву, но контекст)**: вызывающий держит + `dx` кратным 8 (ширина EGA-страйпа) → `dirty` = целые страйпы. Обёртка этому + не мешает, но и не навязывает. + +--- + +## 7. ПРОВЕРИТЬ ПЕРВЫМ — блокеры (dev-MAME) + +1. **Верт. режим (`LD A,A`) в банке `0x50` читает ОЗУ-копию (чистый фон) и + пишет VRAM+тень.** Гориз. heal это доказывает построчно; для верт. прохода — + прогон: нарисовать фон, поверх спрайт банком `0x5C`, сделать `scroll_h` на + dx>0, убедиться что спрайт **не размазался** (копировался фон, не VRAM со + спрайтом). Заодно выяснить, нужен ли сброс Port_Y на колонку (§4). +2. **ОЗУ-копия per-page или общая.** `C-Compiler/applications/PoP/docs/double_buffer_plan.md` + пишет «теневая копия одна — общая». Если тень физически одна (а не адресуется + по базе `0xC000/0xC140` как VRAM), page0 и page1 не удержат фон **разных** + положений камеры во время скролла → пинг-понг (§8) сломается. Проверить: + записать разный фон в page0 и page1 банком `0x50`, сверить чтение обеих. +3. **(только для `scroll_v`, путь A)** accel-буфер переживает `OUT Port_Y` между + fill и flush одного burst'а. Доковый straight-copy этого не проверяет. + +--- + +## 8. Порядок кадра в Loom (контекст использования `scroll_h`) + +Пинг-понг двух страниц; спрайты в VRAM-only банк, поэтому тень каждой страницы +остаётся чистым фоном: + +1. `gfx_set_draw_page(B)` — B = скрытая; +2. `gfx_scroll_h(A, B, area, dx, &dirty)` — B получает сдвинутый чистый фон в + VRAM+тень; старые спрайты B затёрты (copy = heal); +3. декодировать EGA-страйпы фона в `dirty`, **банк `0x50`** (иначе тень B + останется с дырой — следующий скролл из B прочитает мусор); +4. вывести актора/спрайты в B, банк `0x5C`; +5. `gfx_wait_vsync()` → `gfx_set_visible_page(B)`. + +Следующий кадр: роли A/B меняются, читаем уже чистую тень B. Смаза нет by design +(§2). При `dx==0` (камера стоит) — обычный per-sprite `gfx_heal`, а не полная +копия. + +--- + +## 9. Бюджет и проверка корректности + +- 320×256 ≈ 26 мс (**>кадр**); playfield 320×144 ≈ **14–15 мс** (<кадр); скролл + копирует `(320−dx)` колонок → чуть меньше. Итого копия ~кадр + докод полосы + + спрайты; целевой бюджет ~2 кадра/шаг (plan.md §6). **Замерить на PoC-1** + маркерами `OUT (0xFE)` (до появления звука порт свободен). +- Корректность — визуально/дифф-тестом в dev-MAME. Фон-декодер уже проверен + диффом против ScummVM; скролл проверять на реальной комнате шире 320. + +--- + +## 10. Связанные документы + +`plan.md §6` (графика, скроллинг), `toolchain-requirements.md §1` (этот +примитив), `decisions.md`. Железо: `C-Compiler/docs/converted/accel_r.txt`, +`libbgi/include/gfx.h`, `C-Compiler/docs/sprite-api-design.md`, +`C-Compiler/docs/memory-management.md`. diff --git a/examples/scroll/scroll.c b/examples/scroll/scroll.c new file mode 100644 index 0000000..7be012f --- /dev/null +++ b/examples/scroll/scroll.c @@ -0,0 +1,87 @@ +/* + * scroll — визуальная проверка скролл-примитивов libbgi (gfx_scroll_h / + * gfx_scroll_v). Копирует регион из НЕактивной страницы в активную со + * сдвигом и переключает страницы. + * + * ДИАГНОСТИКА. Страница 0 — диагональные полосы color=((x+y)>>3)&15 + * (любой сдвиг ломает непрерывность диагоналей; фаза по Y ловит + * вертикальный «съезд» Port_Y-квирка). Страница 1 — сплошной серый + * фон-маркер: после скролла на нём проступают ровно скопированные + * прямоугольники, по их границам видно корректность (позицию/высоту). + * + * Два региона в одном кадре: слева ГОРИЗОНТАЛЬНЫЙ скролл (dx>0 — картинка + * вправо, открывается серая полоса слева), справа ВЕРТИКАЛЬНЫЙ (dy>0 — + * вниз, открывается серая полоса сверху). Если верт. Port_Y-квирк не + * вылечен — правый прямоугольник съедет вниз/опустеет сверху. Esc — + * выход. + */ + +#include +#include +#include +#include + +static uint8_t egapal[16 * 4]; + +__sfr __at (0xFE) io_border; + +/* Диагональные полосы 16-цветной палитры (8-пиксельные ступени). */ +static void draw_diagonals(void) +{ + int bx, by; + for (by = 0; by < 256; by += 8) + for (bx = 0; bx < 320; bx += 8) { + setfillstyle(SOLID_FILL, (((bx + by) >> 3) & 15)); + bar(bx, by, bx + 7, by + 7); + } +} + +/* Регион горизонтального скролла (слева) и вертикального (справа). */ +static gfx_rect_t area_h = { 0, 56, 320, 144 }; +// static gfx_rect_t area_h = { 16, 48, 80, 80 }; +// static gfx_rect_t area_v = { 176, 48, 80, 80 }; + +int main(void) +{ + uint8_t hidden; + + initgraph(); + + /* Палитра страницы 1 = палитре страницы 0 (initgraph грузит EGA + * только в палитру 0). */ + gfx_pal_get(0, 0, 16, egapal); + gfx_pal_load(1, 0, 16, egapal); + + /* Страница 0 — диагонали (источник фона). */ + gfx_set_draw_page(0); + draw_diagonals(); + + gfx_set_draw_page(1); + draw_diagonals(); + + gfx_set_visible_page(0); + + /* Рисуем на скрытой странице (1), потом флип. */ + hidden = gfx_get_visible_page() ^ 1; /* = 1 */ + gfx_set_draw_page(hidden); + + + + for (;;) { + if (kbhit() && getch() == 27) + break; + io_border = 2; + gfx_scroll_v(&area_h, 2, 0); /* вправо на 48 px */ + // gfx_scroll_v(&area_v, 2, 0); /* вниз на 48 px */ + io_border = 0; + gfx_wait_vsync(); + gfx_wait_vsync(); + + hidden = gfx_get_visible_page() ^ 1; + gfx_set_draw_page(hidden ^ 1); + gfx_set_visible_page(hidden); + } + + closegraph(); + return 0; +} diff --git a/libbgi/_bgi.h b/libbgi/_bgi.h index dddcdb4..10f6ba7 100644 --- a/libbgi/_bgi.h +++ b/libbgi/_bgi.h @@ -37,6 +37,7 @@ extern uint8_t _bgi_prevmode; /* видеорежим до initgraph */ /* CPU-адрес колонки 0 текущей draw-страницы: 0xC000 (page 0) или * 0xC140 (page 1); каждый примитив использует его вместо константы. */ extern uint16_t _gfx_addr_base; +extern uint16_t _gfx_addr_shadow_base; /* ---- Низкоуровневый видеорежим (libc/video) ---------------------- */ uint8_t _videomode_raw_get(void); @@ -108,6 +109,30 @@ void _bgi_heal_rows_raw(uint8_t *scr, int y0, uint8_t w, uint8_t h); void _bgi_hspan (int x, int y, int len, uint8_t color); void _bgi_clearall(uint8_t color); + +/* Скролл-ядра video->video (банк 0x50: копия = скролл + heal цели; без + * скобки/клиппинга; DI/EI бандами по 16 ВНУТРИ; перекрытия нет — стороны + * в разных страницах). Контракты — в шапках bgi256/_bgi_scroll_*_raw.c. + * + * _bgi_scroll_rows_raw — ядро gfx_scroll_h: сдвиг ТОЛЬКО по X (разница + * адресов src/dst), построчно, БЕЗ страйдов — один OUT Port_Y на строку + * (~54Т/строку, на 5-7% быстрее cols). w — байт/строка (0=256), h — + * строк 1..256 (0=256, конвенция accel), y0 — стартовый Port_Y (++ на строку). */ +void _bgi_scroll_rows_raw(uint16_t src, uint16_t dst, + uint8_t w, uint8_t h, uint8_t y0); + +/* _bgi_scroll_cols_raw — ядро gfx_scroll_v: сдвиг по X И/ИЛИ Y за один + * проход БЕЗ буфера. Колонка = вертикальный accel-burst (LD A,A): читаем + * с Port_Y=y0, пишем с Port_Y=y1 (сдвиг dy = y1−y0; dx — в адресах). STOP + * между read и write делает промежуточный OUT Port_Y безопасным. w — + * число КОЛОНОК 1..255 (0=выход), h — высота колонки = размер accel-блока + * (0=256), y0/y1 — Port_Y чтения/записи. */ +void _bgi_scroll_cols_raw(uint16_t src, uint16_t dst, + uint8_t w, uint8_t h, uint8_t y0, uint8_t y1); + + + + /* Ядро блиттинга gfx_blit_part/gfx_blit/gfx_heal (Фаза B) — публичные, * прототипы в ; спрайтовые обёртки putsprite/movesprite — в * . Реализации: common/gfx_blit_part.c и соседи. */ diff --git a/libbgi/bgi256/_bgi_scroll_cols_raw.c b/libbgi/bgi256/_bgi_scroll_cols_raw.c new file mode 100644 index 0000000..6d84556 --- /dev/null +++ b/libbgi/bgi256/_bgi_scroll_cols_raw.c @@ -0,0 +1,102 @@ +/* + * _bgi_scroll_cols_raw — accel-скролл КОЛОНКАМИ video->video (mode 0x81). + * УНИВЕРСАЛЕН: делает и горизонтальный, и ВЕРТИКАЛЬНЫЙ сдвиг за один + * проход, БЕЗ буфера-посредника. Ядро gfx_scroll_v (вертикаль); может + * служить и горизонтали (gfx_scroll_h держит свой rows-вариант — он + * на ~5-7% быстрее, т.к. на строку нужен один OUT Port_Y, а не два). + * + * КАК РАБОТАЕТ. Колонка = один вертикальный accel-burst (LD A,A, 0x7F — + * «копия блока для вертикальных линий»): read набирает h байт колонки src + * с Port_Y=y0 (Port_Y авто-инкрементится внутри burst'а: y0→y0+h), write + * выгружает их в колонку dst с Port_Y=y1 (y1→y1+h). Вертикальный сдвиг = + * разница y0/y1; горизонтальный = разница адресов src/dst (dx). Оба сразу + * = произвольный (dx,dy). dst/src двигаются на +1 (следующий x) за колонку. + * + * ПОЧЕМУ РАБОТАЕТ смена Port_Y между read и write (в отличие от наивного + * rows-варианта, где это ломало accel). Ключ — STOP (LD B,B) ПЕРЕД + * OUT Port_Y=y1: он разоружает accel (m_acc_dir=0), поэтому fetch операнда + * OUT (immediate байт 0x89 из программной памяти) НЕ перезапускает + * read-burst. Затем LD A,A снова взводит вертикальный режим для записи. + * Оба OUT'а (y0 и y1) выполняются только пока accel РАЗОРУЖЁН. Порядок на + * колонку: {OUT y0 · arm · read · STOP · OUT y1 · arm · write · STOP}. + * (Подробно про квирк перезапуска — memory/accel_operand_fetch_retrigger.) + * + * DI/EI — БАНДАМИ по 16 колонок ВНУТРИ (как _gfx_recthfill256): держать DI + * на весь скролл нельзя (порвёт CBL-звук). Размер блока (h) армируется в + * КАЖДОЙ скобке (в окне EI CBL-ISR мог бы переармировать accel). Единая + * тело-петля для полных банд и хвоста (счётчик колонок decrement'ится в + * стек-слоте); y0 держим в C (LD A,C 4Т вместо LD A,#imm 7Т на колонку), + * y1 — SMC-immediate (грузится только пока разоружены — безопасно). + * + * ВХОД (__sdcccall(1)): src->HL, dst->DE; стек: w 4(ix) — число КОЛОНОК + * 1..255 (0 = тихий выход; ровно 256 режет вызывающий), h 5(ix) — высота + * колонки = размер accel-блока 1..256 (0 = 256, конвенция accel), + * y0 6(ix) — Port_Y чтения, y1 7(ix) — Port_Y записи. Callee-pop 4 байта. + * + * Raw: клиппинга нет (вызывающий), W3 замаплен (_bgi_begin/end снаружи), + * банк 0x50 (запись бьёт VRAM+тень — скролл И heal цели). Перекрытия + * src/dst нет by design — стороны в РАЗНЫХ видеостраницах. Колонок >255 и + * колонки >256 строк режет вызывающий. Клоббер: AF/BC; HL/DE/IX + * сохраняются (LD A,A/LD D,D — accel-опкоды-no-op'ы, регистры не трогают). + */ + +#include "../_bgi.h" + +void _bgi_scroll_cols_raw(uint16_t src, uint16_t dst, + uint8_t w, uint8_t h, uint8_t y0, uint8_t y1) __naked { + (void)src; (void)dst; (void)w; (void)h; (void)y0; (void)y1; + __asm + push ix + ld ix, #0 + add ix, sp + ;; HL = src, DE = dst (адреса-колонки; строку задаёт Port_Y) + ld a, 5 (ix) + ld (sc_col_len), a ; SMC: размер accel-блока <- h (0 = 256) + ld a, 7 (ix) + ld (sc_col_y1), a ; SMC: Port_Y записи <- y1 + ld c, 6 (ix) ; C = Port_Y чтения (y0) — LD A,C дешевле imm + sc_col_band: + ld a, 4 (ix) ; колонок осталось + or a, a + jr Z, sc_col_done ; 0 => выход + cp a, #16 + jr NC, sc_col_full ; >=16 — полная банда + ld b, a ; <16 — хвост: B = остаток + jr sc_col_go + sc_col_full: + ld b, #16 ; 16 колонок в банде + sc_col_go: + ld a, 4 (ix) + sub a, b + ld 4 (ix), a ; колонок -= B + di + ld d, d ; 0x52 — режим размера блока (no-op для D) + ld a, #0 + sc_col_len = . - 1 + ld b, b ; выключить — размер сохранён (B=счётчик цел) + sc_col_next: + ld a, c ; Port_Y чтения (y0) + out (#0x89), a ; accel РАЗОРУЖЁН — операнд OUT безопасен + ld a, a ; 0x7F — арм вертикальной копии + ld a, (hl) ; read-burst: колонка src -> буфер (Port_Y y0..) + ld b, b ; 0x40 — STOP (разоружить перед OUT y1) + ld a, #0 + sc_col_y1 = . - 1 + out (#0x89), a ; Port_Y записи (y1) — accel разоружён + ld a, a ; 0x7F — арм вертикальной копии + ld (de), a ; write-burst: буфер -> колонка dst (Port_Y y1..) + ld b, b ; 0x40 — STOP + inc hl ; src += 1 (следующий x) + inc de ; dst += 1 + djnz sc_col_next + ei ; окно прерываний между бандами + jr sc_col_band + sc_col_done: + pop ix + ;; callee-pop 4 байта (w, h, y0, y1) + pop hl ; ret-адрес + pop af + pop af + jp (hl) + __endasm; +} diff --git a/libbgi/bgi256/_bgi_scroll_rows_raw.c b/libbgi/bgi256/_bgi_scroll_rows_raw.c new file mode 100644 index 0000000..2774253 --- /dev/null +++ b/libbgi/bgi256/_bgi_scroll_rows_raw.c @@ -0,0 +1,129 @@ +/* + * _bgi_scroll_rows_raw — БЫСТРАЯ построчная копия video->video со сдвигом + * ТОЛЬКО по X для gfx_scroll_h (mode 0x81). Специализация _bgi_copy_rows_raw + * для скролла: обе стороны — видео, ОБА страйда НУЛЕВЫЕ (CPU-адрес строки + * фиксирован, строку выбирает Port_Y), поэтому адресной арифметики в цикле + * НЕТ ВООБЩЕ — ни страйдовых add (как у copy_rows), ни даже inc адресов + * (как у heal). От _bgi_heal_rows_raw отличается тем, что src != dst + * (HL — колонка-старт src-страницы, DE — dst-страницы со сдвигом dx): + * горизонтальный сдвиг целиком в разнице адресов, задаётся вызывающим. + * + * Вертикального сдвига НЕТ (read и write одной строки на одном Port_Y) — + * для сдвига по Y есть _bgi_scroll_cols_raw (колонки, вертикальный режим). + * Зато rows на ~5-7% быстрее cols: на строку один OUT Port_Y (54Т/строку), + * а cols нужен OUT на чтение И на запись колонки. + * + * ПОРЯДОК на строку: {OUT Port_Y · arm(LD L,L) · read(LD A,(HL)) · + * write(LD (DE),A) · STOP(LD B,B)}. Между read- и write-триггером — + * НИЧЕГО: на Sprinter accel перехватывает любое чтение программной памяти, + * а fetch операнда любой инструкции там (immediate у OUT/ADD) спурьезно + * перезапустил бы read-burst и набил буфер кодом. Port_Y ставится ДО + * arm'а (accel разоружён STOP'ом прошлой строки — операнд OUT безопасен). + * (memory/accel_operand_fetch_retrigger.) + * + * DI/EI — БАНДАМИ по 16 строк ВНУТРИ (как _gfx_recthfill256): накрывать DI + * весь сдвиг нельзя (порвёт CBL-звук). Армирование размера блока — в + * КАЖДОЙ скобке (в окне EI CBL-ISR армирует accel своим размером). Число + * полных 16-банд и хвост считаем ОДИН раз на входе (precompute) — в петле + * банд остаётся дешёвый dec (без per-band `sub 16` через IX-слот). + * Регистры упёрты (HL src, DE dst, B djnz, C бегущий Port_Y) — быстрее уже + * некуда: out(c) без стопов потребовал бы Port_Y-регистра, которого нет. + * + * ВХОД (__sdcccall(1)): src->HL, dst->DE; стек: w 4(ix) — байт в строке + * (размер accel-блока, 1..256, 0=256), h 5(ix) — строк 1..256 (0 = 256, + * КОНВЕНЦИЯ accel — как у w), y0 6(ix) — стартовый Port_Y (инкремент на + * строку). Callee-pop 3 байта. + * + * Raw: клиппинга нет (вызывающий), W3 замаплен (_bgi_begin/end снаружи), + * банк 0x50 (запись бьёт VRAM+тень — скролл И heal цели). Перекрытия + * src/dst нет by design — стороны в РАЗНЫХ видеостраницах. Строку >256 + * байт режет вызывающий. Клоббер: AF/BC; HL/DE/IX сохраняются + * (адреса-константы не трогаются — LD L,L/LD D,D это accel-опкоды-no-op'ы). + */ + +#include "../_bgi.h" + +void _bgi_scroll_rows_raw(uint16_t src, uint16_t dst, + uint8_t w, uint8_t h, uint8_t y0) __naked { + (void)src; (void)dst; (void)w; (void)h; (void)y0; + __asm + push ix + ld ix, #0 + add ix, sp + ;; HL = src, DE = dst (адреса-константы; строку задаёт Port_Y) + ld a, 4 (ix) + ld (sc_row_len), a ; SMC: размер блока <- w (0 = 256) + ld (sc_row_len2), a ; ...и в хвостовую скобку + ld c, 6 (ix) ; C = бегущий Port_Y (y0) + + ;; --- precompute: полных 16-банд (ix+5) + хвост (SMC); h=0 => 256 --- + ld a, 5 (ix) + and #0x0F + ld (sc_row_tail), a ; хвост = h & 15 (0 и при h=0, и при h=256) + ld a, 5 (ix) + or a, a + jr NZ, sc_row_shr + ld a, #16 ; h=0 => 256 => ровно 16 полных банд + jr sc_row_setfull + sc_row_shr: + rrca + rrca + rrca + rrca + and #0x0F ; full = h >> 4 + sc_row_setfull: + ld 5 (ix), a ; ix+5 = число полных 16-строчных банд + + sc_row_band: + ld a, 5 (ix) + or a, a + jr Z, sc_row_tailband ; полные банды кончились + dec 5 (ix) ; full -= 1 + ld b, #16 ; 16 строк в банде + di + ld d, d ; 0x52 — режим размера блока (no-op для D) + ld a, #0 + sc_row_len = . - 1 + ld b, b ; выключить — размер сохранён (B=16 цел) + sc_row_next: + ld a, c + out (#0x89), a ; Port_Y = y (accel ВЫКЛЮЧЕН — операнд безопасен) + inc c ; y++ + ld l, l ; 0x6D — арм гориз. копии (no-op для L) + ld a, (hl) ; read-burst: src-строка -> буфер + ld (de), a ; write-burst: буфер -> dst-строка (вплотную!) + ld b, b ; 0x40 — стоп + djnz sc_row_next + ei ; окно прерываний между бандами + jr sc_row_band + ;; --- хвост h&15 строк: отдельная скобка со СВОИМ армированием --- + sc_row_tailband: + ld a, #0 + sc_row_tail = . - 1 + or a, a + jr Z, sc_row_done ; хвоста нет + ld b, a ; B = остаток строк (1..15) + di + ld d, d + ld a, #0 + sc_row_len2 = . - 1 + ld b, b + sc_row_trow: + ld a, c + out (#0x89), a + inc c + ld l, l + ld a, (hl) + ld (de), a + ld b, b + djnz sc_row_trow + ei + sc_row_done: + pop ix + ;; callee-pop 3 байта (w, h, y0) + pop hl ; ret-адрес + pop af + inc sp + jp (hl) + __endasm; +} diff --git a/libbgi/common/_gfx_state.c b/libbgi/common/_gfx_state.c index b093212..32abfcf 100644 --- a/libbgi/common/_gfx_state.c +++ b/libbgi/common/_gfx_state.c @@ -18,6 +18,7 @@ uint8_t _gfx_bank = 0x50; * (CPU 0xC000+), page 1 — 320..639 (CPU 0xC140+), остаток — дескрипторы * режима/палитра, их не трогаем. */ uint16_t _gfx_addr_base = 0xC000; +uint16_t _gfx_addr_shadow_base = 0xC140; /* Клип спрайтов по экрану: 1 = включён (безопасно, дефолт), 0 = выключен * (приложение само гарантирует, что спрайты не выходят за края — даёт diff --git a/libbgi/common/gfx_scroll_h.c b/libbgi/common/gfx_scroll_h.c new file mode 100644 index 0000000..706100e --- /dev/null +++ b/libbgi/common/gfx_scroll_h.c @@ -0,0 +1,64 @@ +/* + * gfx_scroll_h — ГОРИЗОНТАЛЬНЫЙ скролл региона (mode-agnostic обёртка). + * + * Берёт чистый фон из НЕактивной (теневой) страницы и копирует его со + * сдвигом dx по X в активную (draw) страницу. Обе стороны — банк 0x50 + * (GFX_BANK_NORMAL): запись бьёт VRAM + ОЗУ-копию, т.е. копия + * ОДНОВРЕМЕННО скроллит И «лечит» целевую страницу (старые спрайты + * затираются сдвинутым фоном; scroll-impl-guide.md §2). + * + * dx > 0 — сдвиг в сторону УВЕЛИЧЕНИЯ X (изображение едет вправо, + * открывается полоса слева); dx < 0 — влево (открывается справа); + * dx == 0 — чистая копия фона (полный heal целевой страницы). В обеих + * страницах остаёмся в пределах area. Открывшуюся |dx|-полосу НЕ + * заполняем — возвращаем её в *dirty (её дозаполняет вызывающий). + * + * РЕАЛИЗАЦИЯ — построчно, через _bgi_scroll_rows_raw (без страйдов, один + * OUT Port_Y на строку — на ~5-7% быстрее колоночного _bgi_scroll_cols_raw, + * которому на колонку нужен OUT и на чтение, и на запись). Горизонтальный + * сдвиг — чистая разница CPU-адресов src/dst (src_x vs dst_x), Port_Y для + * read и write один (одна строка). DI/EI бандами по 16 строк — ВНУТРИ + * leaf'а; он же режет высоту на банды и принимает h 1..256 (0=256), так что + * здесь делим только ширину >256 (лимит accel-блока). Перекрытий src/dst + * нет: стороны в разных страницах. + */ + +#include "../include/sprite.h" +#include "../_bgi.h" + +void gfx_scroll_h(const gfx_rect_t *area, int16_t dx, gfx_rect_t *dirty) { + + uint8_t saved = gfx_get_bank(); + uint16_t adx = (dx < 0) ? (uint16_t)(-dx) : (uint16_t)dx; + /* src-строка стартует правее на |dx| при сдвиге влево, dst — правее + * на dx при сдвиге вправо; так оба края остаются внутри area. */ + uint16_t src_x = area->x + ((dx < 0) ? adx : 0); + uint16_t dst_x = area->x + ((dx > 0) ? adx : 0); + uint16_t w = area->w - adx; /* байт (пикселей) в строке */ + uint8_t h = (uint8_t)area->h; /* строк (256 => 0 = 256 у leaf) */ + uint16_t src = _gfx_addr_shadow_base + src_x; + uint16_t dst = _gfx_addr_base + dst_x; + + gfx_set_bank(GFX_BANK_NORMAL); + _bgi_begin(); + + /* Ширина >256 байт — банды ≤256 (размер accel-блока, 0=256). Высоту и + * DI/EI-банды по 16 строк leaf делает сам (h: 0=256). Между бандами по + * ширине CPU-адрес двигаем на 256 (Port_Y выбирает строку). */ + while (w) { + uint8_t cw = (w > 256) ? 256 : (uint8_t)w; /* 0 => 256 */ + _bgi_scroll_rows_raw(src, dst, cw, h, (uint8_t)area->y); + src += 256; dst += 256; + w -= (w > 256) ? 256 : w; + } + + _bgi_end(); + gfx_set_bank(saved); + + if (dirty) { + dirty->x = (dx >= 0) ? area->x : (int16_t)(area->x + area->w - adx); + dirty->y = area->y; + dirty->w = adx; + dirty->h = area->h; + } +} diff --git a/libbgi/common/gfx_scroll_v.c b/libbgi/common/gfx_scroll_v.c new file mode 100644 index 0000000..b1518b4 --- /dev/null +++ b/libbgi/common/gfx_scroll_v.c @@ -0,0 +1,63 @@ +/* + * gfx_scroll_v — ВЕРТИКАЛЬНЫЙ скролл региона (mode-agnostic обёртка). + * + * Симметрична gfx_scroll_h, но сдвиг по Y. Берёт чистый фон из НЕактивной + * (теневой) страницы и копирует его со сдвигом dy по Y в активную (draw) + * страницу. Банк 0x50: копия = скролл + heal целевой страницы (старые + * спрайты затираются сдвинутым фоном; см. gfx_scroll_h / guide §2). + * + * dy > 0 — сдвиг в сторону УВЕЛИЧЕНИЯ Y (изображение едет вниз, + * открывается полоса сверху); dy < 0 — вверх (открывается снизу); + * dy == 0 — чистая копия (полный heal). Открывшуюся |dy|-полосу НЕ + * заполняем — возвращаем в *dirty. + * + * РЕАЛИЗАЦИЯ — колонками, через _bgi_scroll_cols_raw (вертикальный режим + * accel): колонку читаем с Port_Y=ys, пишем с Port_Y=yd (= ys+dy) — сдвиг + * по Y без буфера-посредника (STOP между read и write делает промежуточный + * OUT Port_Y безопасным; см. шапку leaf'а). Один проход, вдвое быстрее + * прежнего grab→blit-через-буфер. Ширину >255 режем на банды колонок + * (leaf сам делит их на DI-скобки по 16). Высота колонки h = area->h−|dy| + * (≤256, 0=256 у accel). Перекрытия src/dst нет: стороны в разных страницах. + */ + +#include "../include/sprite.h" +#include "../_bgi.h" + +void gfx_scroll_v(const gfx_rect_t *area, int16_t dy, gfx_rect_t *dirty) { + + uint8_t saved = gfx_get_bank(); + uint16_t ady = (dy < 0) ? (uint16_t)(-dy) : (uint16_t)dy; + /* Строку чтения (ys) сдвигаем вниз на |dy| при сдвиге вверх, строку + * записи (yd) — вниз на dy при сдвиге вниз; края в пределах area. */ + uint8_t ys = (uint8_t)(area->y + ((dy < 0) ? ady : 0)); + uint8_t yd = (uint8_t)(area->y + ((dy > 0) ? ady : 0)); + uint16_t hb = area->h - ady; /* строк к переносу (≤256) */ + + if (hb) { /* 0 = переносить нечего */ + uint8_t hblk = (uint8_t)hb; /* размер accel-блока: 256 => 0 */ + uint16_t w = area->w; + uint16_t src = _gfx_addr_shadow_base + area->x; + uint16_t dst = _gfx_addr_base + area->x; + + gfx_set_bank(GFX_BANK_NORMAL); + _bgi_begin(); + + /* >255 колонок — банды (leaf 8-битный по числу колонок; 0=выход, + * поэтому ≤255, а не 256). Высота колонки одна на весь проход. */ + while (w) { + uint8_t cw = (w > 255) ? 255 : (uint8_t)w; + _bgi_scroll_cols_raw(src, dst, cw, hblk, ys, yd); + src += cw; dst += cw; w -= cw; + } + + _bgi_end(); + gfx_set_bank(saved); + } + + if (dirty) { + dirty->x = area->x; + dirty->y = (dy >= 0) ? area->y : (int16_t)(area->y + area->h - ady); + dirty->w = area->w; + dirty->h = ady; + } +} diff --git a/libbgi/common/gfx_set_draw_page.c b/libbgi/common/gfx_set_draw_page.c index 04d9b30..b0641fa 100644 --- a/libbgi/common/gfx_set_draw_page.c +++ b/libbgi/common/gfx_set_draw_page.c @@ -10,5 +10,11 @@ void gfx_set_draw_page(uint8_t page) _gfx_draw_page = page & 1; /* Прямые константы короче (0xC000 + (cond ? 0x140 : 0)) на 3 * инструкции Z80 — SDCC не сворачивает сложение констант. */ - _gfx_addr_base = _gfx_draw_page ? 0xC140 : 0xC000; + if(_gfx_draw_page) { + _gfx_addr_base = 0xC140; + _gfx_addr_shadow_base = 0xC000; + } else { + _gfx_addr_base = 0xC000; + _gfx_addr_shadow_base = 0xC140; + } } diff --git a/libbgi/include/sprite.h b/libbgi/include/sprite.h index a3df7e1..bb8133a 100644 --- a/libbgi/include/sprite.h +++ b/libbgi/include/sprite.h @@ -308,4 +308,16 @@ void atlas_sprite_init(sprite_t *s, const atlas_t *a, uint8_t idx); void gfx_w0_map(uint8_t page); void gfx_w0_unmap(void); +typedef struct { + int16_t x, y; + uint16_t w, h; +} gfx_rect_t; + +/* Скролл региона area из НЕактивной страницы в активную со сдвигом на + * dx/dy (0 = чистая копия/heal, >0 = в сторону увеличения координат, + * <0 = уменьшения). Открывшуюся полосу |d| НЕ заполняют — возвращают + * в *dirty (может быть NULL). Банк 0x50: копия = скролл + heal цели. */ +void gfx_scroll_h(const gfx_rect_t *area, int16_t dx, gfx_rect_t *dirty); +void gfx_scroll_v(const gfx_rect_t *area, int16_t dy, gfx_rect_t *dirty); + #endif diff --git a/runtime/crt0_banked.s b/runtime/crt0_banked.s index aa4df32..6d19522 100644 --- a/runtime/crt0_banked.s +++ b/runtime/crt0_banked.s @@ -121,6 +121,17 @@ w2_join: ld (_estex_block_id), a ld (_estex_startup_ix), ix +.ifdef W3_RESIDENT + ;; --w3 в HUGE: резидентный код окна W3 (0xC000) — часть HOME-образа, + ;; замаплен DSS. Загрузка банков ниже подменяет W3 своими страницами; + ;; запомним физ-страницу резидента, чтобы вернуть её дефолтом после + ;; цикла — тогда прямые вызовы в резидентный W3-код снова корректны. + ;; (Трамплин bank.s сам сохраняет/восстанавливает W3 на каждый __banked + ;; вызов, поэтому дефолтная страница обязана быть резидентной.) + in a, (#0xE2) + ld (_w3_resident_page), a +.endif + ;; ---- Allocate _n_banks pages via ESTEX GETMEM ---- ld a, (_n_banks) or a, a @@ -182,6 +193,14 @@ load_bank_done: rst #0x10 ;; Ignore CF from close. +.ifdef W3_RESIDENT + ;; Вернуть резидентную страницу W3 дефолтом (цикл выше оставил в W3 + ;; последний банк). После этого прямые call'ы в резидентный код 0xC0xx + ;; корректны, а трамплины будут сохранять/возвращать именно её. + ld a, (_w3_resident_page) + out (#0xE2), a +.endif + skip_bank_load: ;; ---- Standard SDCC init path ---- call gsinit @@ -411,6 +430,10 @@ _estex_block_id:: .ds 1 _bank_block_id:: .ds 1 +.ifdef W3_RESIDENT +_w3_resident_page: + .ds 1 +.endif .area _DATA _argc:: diff --git a/tests/w3probe/Makefile b/tests/w3probe/Makefile new file mode 100644 index 0000000..ebebd01 --- /dev/null +++ b/tests/w3probe/Makefile @@ -0,0 +1,42 @@ +# w3probe — проверка --w3 (резидентный код окна W3, прямой вызов без +# трамплинов) во ВСЕХ режимах памяти. Все пробы переиспользуют w3res.c +# (резидентный W3-модуль: w3_show/w3_probe/w3_sig). +# +# small : код 0x4100 (W1/W2), W3 резидент. w3probe.exe +# tiny : код 0x8100 (W2), W1 не исп., W3 резидент. w3tiny.exe +# big : код 0x8100 (W2), банки в W1, W3 резидент. w3big.exe +# huge : код 0x4100, W3 ДЕЛЯТ резидент + трамплин-банки. w3huge.exe +# +# Проверено в MAME (2026-07-21): все режимы работают, включая huge — +# резидент↔W3-банк через трамплин в W1, crt0_banked возвращает резидентную +# страницу дефолтом. Раскладка страниц: tiny W1=FF (не исп.), big/huge W1=F1. + +PROJ_ROOT := $(abspath $(CURDIR)/../..) +CC := $(PROJ_ROOT)/bin/sprinter-cc +MAKE_HDD := $(PROJ_ROOT)/toolchain/make_hdd.sh +HDD_IMG := $(PROJ_ROOT)/mame/v306/IMG/test_hdd.chd + +EXES := w3probe.exe w3tiny.exe w3big.exe w3huge.exe + +all: $(EXES) + +w3probe.exe: w3probe.c w3res.c + $(CC) --memory small --w3 w3res.c -o $@ w3probe.c + +w3tiny.exe: w3tiny.c w3res.c + $(CC) --memory tiny --w3 w3res.c -o $@ w3tiny.c + +w3big.exe: w3big.c w3res.c bigbank.c + $(CC) --memory big --w3 w3res.c --bank 1=bigbank.c -o $@ w3big.c + +w3huge.exe: w3huge.c w3res.c w3huge_res.c hugebank.c + $(CC) --memory huge --w3 w3res.c --w3 w3huge_res.c --bank 1=hugebank.c -o $@ w3huge.c + +# Упаковать все четыре на HDD (D:) для прогона в MAME. После — рестарт MAME. +hdd: $(EXES) + $(MAKE_HDD) $(HDD_IMG) $(EXES) + +clean: + rm -rf .sprinter-cc-* $(EXES) + +.PHONY: all hdd clean diff --git a/tests/w3probe/bigbank.c b/tests/w3probe/bigbank.c new file mode 100644 index 0000000..93299b1 --- /dev/null +++ b/tests/w3probe/bigbank.c @@ -0,0 +1,12 @@ +/* bigbank.c — свапаемый банк в W1 (big mode, трамплин через порт 0xA2). + * Отдельно от резидентного W3-кода (w3res.c) — проверяем сосуществование. */ + +#include +#include +#include + +void bigbank_func(void) __banked +{ + printf("W1-BANK: bigbank_func @ ~0x4xxx (trampoline), W1 phys=0x%02X\n", + _io_page_w1); +} diff --git a/tests/w3probe/hugebank.c b/tests/w3probe/hugebank.c new file mode 100644 index 0000000..c785fe7 --- /dev/null +++ b/tests/w3probe/hugebank.c @@ -0,0 +1,13 @@ +/* hugebank.c — СВАПАЕМЫЙ банк в окне W3 (huge mode, трамплин через 0xE2). + * Отдельная страница, подменяет резидентный код на время __banked-вызова; + * трамплин обязан вернуть резидентную страницу по выходу. */ + +#include +#include +#include + +void hugebank_func(void) __banked +{ + printf("W3-BANK: hugebank_func (trampoline), W3 phys now=0x%02X\n", + _io_page_w3); +} diff --git a/tests/w3probe/w3big.c b/tests/w3probe/w3big.c new file mode 100644 index 0000000..587358e --- /dev/null +++ b/tests/w3probe/w3big.c @@ -0,0 +1,26 @@ +/* w3big.c — проба --w3 в режиме big. + * big: код 0x8100 (W2), свапаемые банки в W1 (трамплины), W3 свободно под + * резидент. Проверяем сосуществование: W1-банк (трамплин) + W3-резидент + * (прямой вызов) в одной программе. */ + +#include +#include +#include + +/* crt0_banked читает это ДО gsinit — только `const`. */ +const uint8_t n_banks = 1; + +void bigbank_func(void) __banked; /* W1-банк, трамплин */ +extern uint8_t w3_probe(uint8_t x); /* W3-резидент, прямо */ +extern void w3_show(void); + +int main(void) +{ + printf("BIG : main @ 0x%04X | W1=%02X W2=%02X W3=%02X\n", + (unsigned)&main, _io_page_w1, _io_page_w2, _io_page_w3); + bigbank_func(); /* W1 через трамплин */ + w3_show(); /* W3 напрямую */ + printf("BIG : w3_probe(0x42)=0x%02X (expect 0xBD), exit to DSS.\n", + (unsigned)w3_probe(0x42)); + return 0; +} diff --git a/tests/w3probe/w3huge.c b/tests/w3probe/w3huge.c new file mode 100644 index 0000000..86ae070 --- /dev/null +++ b/tests/w3probe/w3huge.c @@ -0,0 +1,33 @@ +/* w3huge.c — проба --w3 в режиме huge (самый сложный случай). + * huge: код 0x4100 (W1/W2), окно W3 ДЕЛЯТ резидентный код (0xC000, прямой + * вызов) и свапаемые трамплин-банки. crt0_banked захватывает резидентную + * страницу и возвращает её дефолтом после загрузки банков; трамплин + * сохраняет/восстанавливает W3 на каждый __banked-вызов. + * + * Ключевая проверка: w3_show() (резидент) вызывается ДО и ПОСЛЕ + * hugebank_func() (банк). Второй вызов проходит только если трамплин + * вернул резидентную страницу в W3. */ + +#include +#include +#include + +const uint8_t n_banks = 1; /* один свапаемый W3-банк */ + +void hugebank_func(void) __banked; /* W3-банк, трамплин */ +extern uint8_t w3_probe(uint8_t x); /* W3-резидент, прямой вызов */ +extern void w3_show(void); /* W3-резидент, прямой вызов */ +extern void w3_resident_calls_bank(void); /* W3-резидент, зовёт банк */ + +int main(void) +{ + printf("HUGE: main @ 0x%04X | W1=%02X W2=%02X W3(resident)=%02X\n", + (unsigned)&main, _io_page_w1, _io_page_w2, _io_page_w3); + w3_show(); /* резидент напрямую (до банка) */ + hugebank_func(); /* HOME → W3-банк (свап+возврат) */ + w3_resident_calls_bank(); /* резидент → W3-банк (трамплин W1) */ + w3_show(); /* резидент СНОВА — возврат стр. */ + printf("HUGE: w3_probe(0x42)=0x%02X (expect 0xBD), exit to DSS.\n", + (unsigned)w3_probe(0x42)); + return 0; +} diff --git a/tests/w3probe/w3huge_res.c b/tests/w3probe/w3huge_res.c new file mode 100644 index 0000000..c6870c6 --- /dev/null +++ b/tests/w3probe/w3huge_res.c @@ -0,0 +1,26 @@ +/* w3huge_res.c — второй резидентный W3-модуль для huge-пробы. + * + * Содержит РЕЗИДЕНТНУЮ функцию (W3CODE @0xC000, прямой вызов), которая + * сама зовёт W3 __banked-функцию (свапаемый банк, трамплин). Этот случай + * ДОЛЖЕН работать: трамплин ___sdcc_bcall_ehl живёт в _CODE (W1, всегда + * замаплен) — он сохраняет резидентную страницу W3, маппит банк, а по + * возврату восстанавливает резидента. Адрес возврата (0xC0xx) снова + * валиден, и управление корректно возвращается в резидентную функцию. + * + * (Контраст с запретом «из __banked-контекста нельзя дёрнуть резидент»: + * здесь наоборот — резидент зовёт банк, и это ок.) */ + +#include +#include +#include + +void hugebank_func(void) __banked; /* W3-банк, трамплин */ + +void w3_resident_calls_bank(void) +{ + printf("W3-RES: w3_resident_calls_bank @ 0x%04X -> calling W3 __banked...\n", + (unsigned)&w3_resident_calls_bank); + hugebank_func(); /* резидент → банк через трамплин W1 */ + printf("W3-RES: returned into resident OK (W3 phys now=0x%02X)\n", + _io_page_w3); +} diff --git a/tests/w3probe/w3probe.c b/tests/w3probe/w3probe.c new file mode 100644 index 0000000..2a3725a --- /dev/null +++ b/tests/w3probe/w3probe.c @@ -0,0 +1,27 @@ +/* w3probe.c — явная проба подхода A для --w3. + * + * main (W1/W2) зовёт функцию w3_show() в W3, которая печатает свой + * собственный адрес (должен быть 0xC0xx) через printf. Затем main + * проверяет прямой вызов с возвратом и ЧИСТО выходит в DSS (return 0), + * чтобы вернулось приглашение шелла. */ + +#include +#include + +extern const uint8_t w3_sig; /* rodata в W3 */ +extern uint8_t w3_probe(uint8_t x); /* код в W3, прямой вызов */ +extern void w3_show(void); /* код в W3, зовёт printf */ + +int main(void) +{ + printf("HOME: main @ 0x%04X | w3_probe @ 0x%04X | w3_show @ 0x%04X\n", + (unsigned)&main, (unsigned)&w3_probe, (unsigned)&w3_show); + + w3_show(); /* W3-код печатает свой адрес */ + + printf("HOME: w3_probe(0x42) = 0x%02X (expect 0xBD)\n", + (unsigned)w3_probe(0x42)); + + printf("HOME: done, exiting to DSS.\n"); + return 0; /* чистый выход → приглашение DSS */ +} diff --git a/tests/w3probe/w3res.c b/tests/w3probe/w3res.c new file mode 100644 index 0000000..9615d7d --- /dev/null +++ b/tests/w3probe/w3res.c @@ -0,0 +1,25 @@ +/* w3res.c — резидентный модуль окна W3 (проба подхода A, явная версия). + * + * Компилируется с --codeseg W3CODE --constseg W3CODE (БЕЗ --dataseg), + * линкуется по -Wl-b_W3CODE=0xC000. Здесь только код + rodata. */ + +#include +#include + +/* rodata в W3 (для проверки, что область реально по 0xC000). */ +const uint8_t w3_sig = 0x5A; + +/* Функция, живущая в W3. Печатает СВОЙ адрес через printf — а printf + * лежит в libc (_CODE = W1/W2). Тем самым проверяем оба направления: + * W3-код исполняется по 0xC0xx И умеет звать код в W1/W2 обычным call'ом. */ +void w3_show(void) +{ + printf("W3 : w3_show @ 0x%04X | w3_sig @ 0x%04X = 0x%02X\n", + (unsigned)&w3_show, (unsigned)&w3_sig, (unsigned)w3_sig); +} + +/* Прямой вызов с возвратом значения: x^0xFF. */ +uint8_t w3_probe(uint8_t x) +{ + return (uint8_t)(x ^ 0xFF); +} diff --git a/tests/w3probe/w3tiny.c b/tests/w3probe/w3tiny.c new file mode 100644 index 0000000..b4c1990 --- /dev/null +++ b/tests/w3probe/w3tiny.c @@ -0,0 +1,30 @@ +/* w3tiny.c — проба --w3 в режиме tiny. + * tiny: код с 0x8100 (W2), W1 не используется, W3 свободно под резидент. + * Образ тянется 0x8100..0xC0xx (2 страницы) → DSS маппит W2 и W3. */ + +#include +#include + +extern uint8_t w3_probe(uint8_t x); +extern void w3_show(void); + +uint8_t w1_p, w2_p, w3_p; /* физ-страницы окон (заполняются asm) */ + +int main(void) +{ + __asm + in a,(#0xA2) + ld (_w1_p), a + in a,(#0xC2) + ld (_w2_p), a + in a,(#0xE2) + ld (_w3_p), a + __endasm; + + printf("TINY: main @ 0x%04X | pages W1=%02X W2=%02X W3=%02X\n", + (unsigned)&main, w1_p, w2_p, w3_p); + w3_show(); + printf("TINY: w3_probe(0x42)=0x%02X (expect 0xBD), exit to DSS.\n", + (unsigned)w3_probe(0x42)); + return 0; +} diff --git a/toolchain/check_banks.py b/toolchain/check_banks.py index 3295e1c..fdd9f1a 100755 --- a/toolchain/check_banks.py +++ b/toolchain/check_banks.py @@ -1,65 +1,140 @@ #!/usr/bin/env python3 """ -Parse an SDCC .map file produced by sdldz80 and verify that every named -bank fits inside its 16 KB window. +Отчёт по раскладке памяти образа + проверка лимитов, по .map-файлу +sdldz80. Печатается для ЛЮБОЙ модели памяти (не только при --bank): -The Sprinter toolchain expects each `_BANKn` area (with n >= 1) to occupy -at most 16384 bytes — that is the size of CPU window 3 where banked code -runs at execution time. The SDCC linker itself does not enforce this -limit, so we catch it post-link. + - _CODE — основной код (W1 0x4100 или W2 0x8100), размер, конец; + - данные — _DATA/_BSS/_INITIALIZED/… цепочкой за кодом, окно, конец; + - статика/куча/стек — где кончается статика и сколько свободно под кучу + (до ___sdcc_heap_end) и стек; + - _W3CODE — резидентный код окна W3 (--w3), если есть; + - _BANKn — банки (big/huge), каждый ≤ 16 КБ (размер окна W3/W1). -We also surface the HOME budget: anything left between the end of _CODE -and the start of window 2 (0x8000) is leftover space for adding code/data -without banking. +Раскладка режимов — docs/memory-management.md §4. Линкер сам лимиты не +проверяет — ловим здесь пост-линк. -Usage: check_banks.py +Usage: check_banks.py [--mode MODE] [--heap-top 0xNNNN] + [--stack 0xNNNN] -Exits non-zero with a clear message if any bank is over its limit. +Выходит с ненулевым кодом, если банк не влез в окно или статика заехала +за потолок кучи/окна. """ +import argparse import re import sys -BANK_LIMIT = 16 * 1024 -# HOME upper bound depends on layout: -# HUGE: CODE at 0x4100 (W1), HOME may spill into W2 → ceiling 0xC000 -# BIG: CODE at 0x8100 (W2), HOME stays in W2 → ceiling 0xC000 -# tiny/small: same ceiling — anything in W3 is bank territory. -# We pick the ceiling per-image based on where _CODE lives, so we don't -# falsely flag W2-resident code as "spilled into stack/heap". -HOME_CEILING = 0xC000 +BANK_LIMIT = 16 * 1024 # окно W3/W1 под банк +W_CEILING = 0xC000 # верх W2 (дальше — только W3/банки) + +# Области .map, составляющие HOME (код + данные), в порядке цепочки линкера. +# Всё, что не банк и не резидент W3 и лежит < 0xC000. +_BANK_RE = re.compile(r"_BANK\d+$") + + +def win_label(addr): + """Окно по адресу (0-based физического образа игнорируем — берём Z80-view).""" + a = addr & 0xFFFF + if 0x4000 <= a < 0x8000: + return "W1" + if 0x8000 <= a < 0xC000: + return "W2" + if 0xC000 <= a <= 0xFFFF: + return "W3" + return "??" def parse_map(path): - """ - Returns a dict {area_name: (addr, size)} for area lines like: - _CODE 00004100 00000313 = ... - """ + """{area_name: (addr, size)} для строк вида `_CODE 00004100 00000313 = ...`.""" line_re = re.compile(r"^\s*(_\w+)\s+([0-9A-Fa-f]{8})\s+([0-9A-Fa-f]{8})\s*=") areas = {} with open(path) as f: for ln in f: m = line_re.match(ln) - if not m: - continue - name = m.group(1) - addr = int(m.group(2), 16) - size = int(m.group(3), 16) - areas[name] = (addr, size) + if m: + areas[m.group(1)] = (int(m.group(2), 16), int(m.group(3), 16)) return areas -def main(): - if len(sys.argv) != 2: - sys.exit("usage: check_banks.py ") +MODE_DESC = { + "tiny": "CODE+DATA в W2 (0x8100)", + "small": "CODE в W1 (0x4100), DATA→W2", + "big": "CODE+DATA в W2 (0x8100), банки в W1", + "huge": "CODE в W1 (0x4100), DATA→W2, банки в W3", + "manual": "ручная раскладка", +} - areas = parse_map(sys.argv[1]) + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("mapfile") + ap.add_argument("--mode", default="") + ap.add_argument("--heap-top", default="0xBB00") + ap.add_argument("--stack", default="0xBFFE") + args = ap.parse_args() + + areas = parse_map(args.mapfile) + heap_top = int(args.heap_top, 0) + stack_top = int(args.stack, 0) fails = [] - bank_names = sorted( - n for n in areas if re.fullmatch(r"_BANK\d+", n) - ) + desc = MODE_DESC.get(args.mode, "") + hdr = f"memory: {args.mode}" if args.mode else "memory:" + if desc: + hdr += f" — {desc}" + print(hdr) + + # --- HOME: код + данные (всё < 0xC000, кроме банков) -------------- + home = {n: v for n, v in areas.items() + if not _BANK_RE.match(n) and n != "_W3CODE" and (v[0] & 0xFFFF) < W_CEILING} + + if "_CODE" in areas: + caddr, csize = areas["_CODE"] + cend = caddr + csize + print(f" _CODE @ 0x{caddr & 0xFFFF:04X} size {csize:>6} " + f"({win_label(caddr)}) → 0x{cend & 0xFFFF:04X}") + + # данные = все HOME-области, кроме _CODE (цепляются за кодом) + data = {n: v for n, v in home.items() if n != "_CODE"} + if data: + dstart = min(v[0] for v in data.values()) + dend = max(v[0] + v[1] for v in data.values()) + print(f" данные @ 0x{dstart & 0xFFFF:04X} size {dend - dstart:>6} " + f"({win_label(dstart)}) → 0x{dend & 0xFFFF:04X}" + f" [_DATA/_BSS/_INITIALIZED/…]") + else: + dend = cend + + # статика/куча/стек + static_end = max(v[0] + v[1] for v in home.values()) + heap_free = heap_top - static_end + stack_room = stack_top + 1 - heap_top + print(f" статика до 0x{static_end & 0xFFFF:04X} — куча " + f"0x{static_end & 0xFFFF:04X}..0x{heap_top & 0xFFFF:04X} ({heap_free} Б), " + f"стек 0x{heap_top & 0xFFFF:04X}..0x{stack_top & 0xFFFF:04X} ({stack_room} Б)") + if static_end > heap_top: + print(f" ОШИБКА: статика заехала за heap_top 0x{heap_top & 0xFFFF:04X} " + f"(нет места под кучу/стек)") + fails.append(("_HOME", static_end)) + elif static_end > W_CEILING: + print(f" ОШИБКА: статика вышла за 0x{W_CEILING:04X}") + fails.append(("_HOME", static_end)) + + # --- Резидентный код окна W3 (--w3) ------------------------------- + if "_W3CODE" in areas: + waddr, wsize = areas["_W3CODE"] + wend = waddr + wsize + wfree = 0x10000 - wend + marker = "OK" + if wend > 0x10000: + marker = "OVERFLOW" + fails.append(("_W3CODE", wsize)) + print(f" _W3CODE @ 0x{waddr & 0xFFFF:04X} size {wsize:>6} " + f"(резидент W3) → 0x{wend & 0xFFFF:04X}, {wfree} Б свободно до 0x10000 {marker}") + + # --- Банки (big/huge) --------------------------------------------- + bank_names = sorted(n for n in areas if _BANK_RE.match(n)) for name in bank_names: addr, size = areas[name] pct = size * 100.0 / BANK_LIMIT @@ -67,24 +142,18 @@ def main(): if size > BANK_LIMIT: marker = "OVERFLOW" fails.append((name, size)) - print(f" {name:<12} @ 0x{addr:08X} size {size:>5} / {BANK_LIMIT} ({pct:5.1f}%) {marker}") - - # HOME budget — _CODE can land in W1 (huge/small) or W2 (big/tiny). - if "_CODE" in areas: - addr, size = areas["_CODE"] - end = addr + size - budget_remaining = HOME_CEILING - end - which_window = "W2 (0x8000-0xBFFF)" if addr >= 0x8000 else "HOME (0x4100-0xBFFF)" - print(f" _CODE @ 0x{addr:08X} size {size:>5} → ends at 0x{end:04X}, " - f"{which_window} has {budget_remaining} bytes free before 0x{HOME_CEILING:04X}") - if budget_remaining < 0: - print(f" ERROR: _CODE extends past 0x{HOME_CEILING:04X} (stack/heap territory)") - fails.append(("_CODE", size)) + # виртуальный адрес: старший байт = номер банка, младшие 16 — окно + free = BANK_LIMIT - size + print(f" {name:<12} @ 0x{addr:08X} size {size:>6} / {BANK_LIMIT} " + f"({pct:4.1f}%) → {free} Б свободно {marker}") if fails: print() for name, size in fails: - print(f" {name} too big: {size} bytes (limit {BANK_LIMIT})") + if name in ("_HOME", "_W3CODE"): + print(f" {name}: раскладка не влезла (конец 0x{size & 0xFFFF:04X})") + else: + print(f" {name} слишком большой: {size} Б (лимит {BANK_LIMIT})") sys.exit(1) diff --git a/toolchain/mkexe/mkexe.c b/toolchain/mkexe/mkexe.c index 888bf9c..b126c24 100644 --- a/toolchain/mkexe/mkexe.c +++ b/toolchain/mkexe/mkexe.c @@ -277,11 +277,14 @@ int main(int argc, char **argv) { uint32_t pad_byte = 0xFF; uint32_t bank_base = DEFAULT_BANK_BASE; int verbose = 0; + int allow_w3_home = 0; /* -W: HOME может тянуться в W3 при W3-банках + (резидентный код --w3 в HUGE делит окно) */ for (int i = 1; i < argc; i++) { const char *a = argv[i]; if (!strcmp(a, "-h") || !strcmp(a, "--help")) { usage(stdout); return 0; } if (!strcmp(a, "-v")) { verbose = 1; continue; } + if (!strcmp(a, "-W")) { allow_w3_home = 1; continue; } if (!strcmp(a, "-o") && i + 1 < argc) { out_path = argv[++i]; continue; } if (!strcmp(a, "-L") && i + 1 < argc) { if (parse_addr(argv[++i], &load_addr) < 0) { fprintf(stderr, "mkexe: bad -L\n"); return 1; } @@ -386,12 +389,17 @@ int main(int argc, char **argv) { } } - /* HOME must not extend past 0xBFFF (W3 = bank territory in HUGE, - free in BIG but still off-limits to HOME). */ - if (home->hi >= 0xC000u) { + /* HOME может тянуться в W3 (0xC000..0xFFFF) — это резидентный код + окна W3 (--w3, подход A): DSS грузит непрерывный образ и маппит + W1/W2/W3. Запрещаем ТОЛЬКО когда W3 реально занят трамплин-банками + (HUGE: bank_base=0xC000, max_bank>0) — тогда HOME и банки + столкнулись бы в одном окне. */ + if (home->hi >= 0xC000u && max_bank > 0 && bank_base == DEFAULT_BANK_BASE + && !allow_w3_home) { fprintf(stderr, - "mkexe: HOME image extends to 0x%04X, past window 2 end (0xBFFF).\n" - " Code grew too big for HOME — move some .c files into a bank.\n", + "mkexe: HOME image extends to 0x%04X, into W3 (0xC000+) which\n" + " holds trampoline banks in HUGE mode — collision.\n" + " Move some .c files into a bank, or pass -W for --w3 residents.\n", home->hi); return 2; }