libbgi: скролл-примитивы + --w3 + отчёт раскладки памяти

Скролл региона video->video (неактивная страница -> активная, банк 0x50:
копия = скролл + heal цели):
- gfx_scroll_h / _bgi_scroll_rows_raw — горизонтальный, построчно без
  страйдов (~54Т/строку), DI/EI бандами по 16 строк, h=0=>256;
- gfx_scroll_v / _bgi_scroll_cols_raw — верт. И/ИЛИ гориз. за один проход
  без буфера (колонка = accel-burst LD A,A, STOP между read/write делает
  промежуточный OUT Port_Y безопасным), банды по 16 колонок;
- _gfx_addr_shadow_base (адрес неактивной страницы) + gfx_rect_t.
Пример examples/scroll.

check_banks.py + sprinter-cc: отчёт раскладки памяти для ЛЮБОЙ модели
(W1/W2 код/данные, остаток кучи/стека, W3 при --w3, банки при --bank),
не только при --bank.  Док docs/memory-management.md §10.

--w3 (резидентный код окна W3) + сопутствующее: crt0_banked W3_RESIDENT,
mkexe -W, tests/w3probe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 19:38:00 +03:00
parent d1bc97c589
commit 95c22be9bd
46 changed files with 4125 additions and 133 deletions
+71 -10
View File
@@ -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"
+8
View File
@@ -259,6 +259,14 @@ putsprite новой (save-буфер не нужен). Низкий урове
рисовать банком 0x50, спрайты/оверлеи — putsprite/0x5C; буферы
образов — вне W3 (< 0xC000). Тесты: tests/sprites, tests/gfxbanks,
tests/bgi_img.
Скролл региона из НЕактивной страницы в активную (<sprite.h>):
`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.
-67
View File
@@ -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 (Адрес запуска);
+308
View File
@@ -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
`<sprinter.h>` даёт 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`.
```
Binary file not shown.
+721
View File
@@ -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 ; ðàçìåðû êàæäîãî øàðèêà 16íà16
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
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.
+58
View File
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
+38
View File
@@ -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.
за тормоза в математике сильно не пинать!!!
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

File diff suppressed because one or more lines are too long
+68
View File
@@ -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
;------------------------
+20
View File
@@ -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
+20
View File
@@ -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
+89
View File
@@ -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
+113
View File
@@ -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
Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := scroll
EXTRA_FLAGS ?= --gfx 256
include $(PROJ_ROOT)/app.mk
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
make clean
make run
+223
View File
@@ -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 ≈ **1415 мс** (<кадр); скролл
копирует `(320dx)` колонок → чуть меньше. Итого копия ~кадр + докод полосы +
спрайты; целевой бюджет ~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`.
+87
View File
@@ -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 <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <conio.h>
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;
}
+25
View File
@@ -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 = y1y0; 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) — публичные,
* прототипы в <gfx.h>; спрайтовые обёртки putsprite/movesprite — в
* <graphics.h>. Реализации: common/gfx_blit_part.c и соседи. */
+102
View File
@@ -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;
}
+129
View File
@@ -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;
}
+1
View File
@@ -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 = выключен
* (приложение само гарантирует, что спрайты не выходят за края — даёт
+64
View File
@@ -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;
}
}
+63
View File
@@ -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;
}
}
+7 -1
View File
@@ -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;
}
}
+12
View File
@@ -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
+23
View File
@@ -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::
+42
View File
@@ -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
+12
View File
@@ -0,0 +1,12 @@
/* bigbank.c — свапаемый банк в W1 (big mode, трамплин через порт 0xA2).
* Отдельно от резидентного W3-кода (w3res.c) — проверяем сосуществование. */
#include <stdio.h>
#include <stdint.h>
#include <sprinter.h>
void bigbank_func(void) __banked
{
printf("W1-BANK: bigbank_func @ ~0x4xxx (trampoline), W1 phys=0x%02X\n",
_io_page_w1);
}
+13
View File
@@ -0,0 +1,13 @@
/* hugebank.c — СВАПАЕМЫЙ банк в окне W3 (huge mode, трамплин через 0xE2).
* Отдельная страница, подменяет резидентный код на время __banked-вызова;
* трамплин обязан вернуть резидентную страницу по выходу. */
#include <stdio.h>
#include <stdint.h>
#include <sprinter.h>
void hugebank_func(void) __banked
{
printf("W3-BANK: hugebank_func (trampoline), W3 phys now=0x%02X\n",
_io_page_w3);
}
+26
View File
@@ -0,0 +1,26 @@
/* w3big.c — проба --w3 в режиме big.
* big: код 0x8100 (W2), свапаемые банки в W1 (трамплины), W3 свободно под
* резидент. Проверяем сосуществование: W1-банк (трамплин) + W3-резидент
* (прямой вызов) в одной программе. */
#include <stdio.h>
#include <stdint.h>
#include <sprinter.h>
/* 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;
}
+33
View File
@@ -0,0 +1,33 @@
/* w3huge.c — проба --w3 в режиме huge (самый сложный случай).
* huge: код 0x4100 (W1/W2), окно W3 ДЕЛЯТ резидентный код (0xC000, прямой
* вызов) и свапаемые трамплин-банки. crt0_banked захватывает резидентную
* страницу и возвращает её дефолтом после загрузки банков; трамплин
* сохраняет/восстанавливает W3 на каждый __banked-вызов.
*
* Ключевая проверка: w3_show() (резидент) вызывается ДО и ПОСЛЕ
* hugebank_func() (банк). Второй вызов проходит только если трамплин
* вернул резидентную страницу в W3. */
#include <stdio.h>
#include <stdint.h>
#include <sprinter.h>
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;
}
+26
View File
@@ -0,0 +1,26 @@
/* w3huge_res.c — второй резидентный W3-модуль для huge-пробы.
*
* Содержит РЕЗИДЕНТНУЮ функцию (W3CODE @0xC000, прямой вызов), которая
* сама зовёт W3 __banked-функцию (свапаемый банк, трамплин). Этот случай
* ДОЛЖЕН работать: трамплин ___sdcc_bcall_ehl живёт в _CODE (W1, всегда
* замаплен) — он сохраняет резидентную страницу W3, маппит банк, а по
* возврату восстанавливает резидента. Адрес возврата (0xC0xx) снова
* валиден, и управление корректно возвращается в резидентную функцию.
*
* (Контраст с запретом «из __banked-контекста нельзя дёрнуть резидент»:
* здесь наоборот — резидент зовёт банк, и это ок.) */
#include <stdio.h>
#include <stdint.h>
#include <sprinter.h>
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);
}
+27
View File
@@ -0,0 +1,27 @@
/* w3probe.c — явная проба подхода A для --w3.
*
* main (W1/W2) зовёт функцию w3_show() в W3, которая печатает свой
* собственный адрес (должен быть 0xC0xx) через printf. Затем main
* проверяет прямой вызов с возвратом и ЧИСТО выходит в DSS (return 0),
* чтобы вернулось приглашение шелла. */
#include <stdio.h>
#include <stdint.h>
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 */
}
+25
View File
@@ -0,0 +1,25 @@
/* w3res.c — резидентный модуль окна W3 (проба подхода A, явная версия).
*
* Компилируется с --codeseg W3CODE --constseg W3CODE (БЕЗ --dataseg),
* линкуется по -Wl-b_W3CODE=0xC000. Здесь только код + rodata. */
#include <stdio.h>
#include <stdint.h>
/* 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);
}
+30
View File
@@ -0,0 +1,30 @@
/* w3tiny.c — проба --w3 в режиме tiny.
* tiny: код с 0x8100 (W2), W1 не используется, W3 свободно под резидент.
* Образ тянется 0x8100..0xC0xx (2 страницы) → DSS маппит W2 и W3. */
#include <stdio.h>
#include <stdint.h>
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;
}
+119 -50
View File
@@ -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 <path-to.map>
Usage: check_banks.py <path.map> [--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 <path-to.map>")
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)
+13 -5
View File
@@ -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;
}