1e6c377edc
Резидента --w3 больше нет: отрисовка (pop_bg + pop_gdraw) уехала в БАНК 2, физика/коллизия (pop_map) — в БАНК 3. Куча W1/W2 1294 -> 6750 Б. Что это разблокировало. Резидент был тупиком: из банка он недостижим ни прямо, ни транзитивно, поэтому pop_map (самый крупный модуль, 5.8 КБ) в банк было не увести — он зовёт mob-отрисовку. Проверено, что банк->банк РАБОТАЕТ: ___sdcc_bcall_ehl читает страницу окна портом 0xE2 и кладёт её на СТЕК своего кадра (runtime/bank.s), поэтому вложенность корректна по построению. Подтверждено в MAME цепочкой W1 -> банк1 -> банк2 -> банк1: nested=124 after=8, ровно ожидаемое. Значит развязка mob'а (самое рисковое место, loose-полы) НЕ понадобилась — pop_map зовёт pop_bg трамплином. Правила вызовов проверены на сгенерированном asm и записаны в memory sdcc_banked_call_rules: трамплин выбирает ОБЪЯВЛЕНИЕ (__banked), а не раскладка — даже внутри одного .c между __banked функциями он есть. Внутрибанковые функции оставлены непомеченными и зовутся напрямую, в т.ч. через границу файла (pop_gdraw -> pop_fore_over_char). sprinter-cc: --dataseg BANKn БОЛЬШЕ НЕ ставится по умолчанию. Раньше вся писучая память банкового модуля уезжала в страницу банка и снаружи не читалась (проверено на .map: глобал лёг по 0x0001C000) — грабли на каждом переносе. Теперь данные банков по умолчанию в общем _DATA (W1/W2, замаплен всегда), а прежнее поведение — по явному --bank-data. Замеры (комната 3, Kid + страж; кадр Sprinter в турбо = 430 000 тактов): до переноса 338 508 (0.79 кадра) + pop_bg в банк 2 347 100 (+2.5 %) + pop_map в банк 3 371 100 (+9.6 % к исходному, 0.86 кадра) Плата — трамплины (~654 такта на вызов, ~30 вызовов за кадр). При пейсинге в 3 кадра это 29 % логического кадра, но запас до ОДНОГО кадра всего ~59 000 тактов — под звук его надо возвращать (следующий шаг: батчить кроссбанковые вызовы, начиная с pop_redraw_needed). Профилирование бордюром включено по умолчанию (make PROF=0 выключает) и переведено на реально работающие биты: бит 0 (красный) у бордюра Sprinter ИГНОРИРУЕТСЯ, поэтому различимых состояний четыре и значения обязаны быть чётными — 0 чёрный (ждём vsync), 2 синий (логика), 4 зелёный (фон), 6 циан (спрайты). Раньше нечётные номера сливались и полосы не читались. Проверено в MAME: комнаты 1/3 рисуются как прежде, бег и коллизия работают, в комнате 12 плиты проваливаются со щебнем — то есть цепочка loose банк3 -> банк2 живая. Полосы бордюра в комнате 3: логика 73 строки, фон 64, спрайты 128, свободно 23. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
502 lines
25 KiB
Bash
Executable File
502 lines
25 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
#
|
||
# sprinter-cc — wrapper around SDCC for building Sprinter .EXE files.
|
||
#
|
||
# Usage:
|
||
# sprinter-cc -o foo.exe foo.c [more.c ...] [options]
|
||
#
|
||
# Options:
|
||
# -o NAME output executable name (required)
|
||
# --crt0=TYPE crt0 variant: default | minimal | banked (default: default)
|
||
# --memory MODE memory layout: tiny | small | big | huge | manual
|
||
# tiny (default): CODE+DATA+stack in W2 (single page, ≤14 KB)
|
||
# small: CODE in W1, DATA+BSS continue into W2, heap spans
|
||
# remainder, stack in W2. crt0_small reads port
|
||
# 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
|
||
# 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:
|
||
# CODE=W1|W2 DATA=W1|W2|SAME BANKED=W1|W3
|
||
# Example: --memory-manual CODE=W2,DATA=W2,BANKED=W3
|
||
# -I PATH additional include path (repeatable)
|
||
# -DNAME[=VAL] preprocessor define, passed to sdcc as is (repeatable)
|
||
# -L 0xADDR code load address (default: derived from --memory mode)
|
||
# -E 0xADDR entry address (default: same as -L)
|
||
# -S 0xADDR stack address (default: 0xBFFE)
|
||
# --stack-size N bytes reserved for the stack (default: ~1278, sets
|
||
# HEAP_TOP = stack_top + 1 - N to constrain heap growth)
|
||
# --code-loc 0xN override SDCC --code-loc (default: derived from --memory)
|
||
# --data-loc 0xN override SDCC --data-loc (default: derived from --memory)
|
||
# -Wl FLAG extra linker flag (repeatable)
|
||
# --bank N=FILE.c compile FILE.c as bank N; repeatable; pulls crt0_banked
|
||
# --bank-data put bank modules' writable data INTO the bank page
|
||
# (default: it goes to the shared _DATA in W1/W2, which
|
||
# stays mapped and is visible from everywhere)
|
||
# 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 — this includes
|
||
# opening a graphics batch bracket itself (_bgi_begin/_bgi_end
|
||
# map the video bank via the SAME port 0xE2; doing that from
|
||
# W3 code executes from video RAM -> white screen). Calling
|
||
# HOME graphics primitives is fine: they open and close the
|
||
# bracket internally and restore the page before returning.
|
||
# 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
|
||
# value, e.g. SDCC's own default 3000, to compile faster).
|
||
# --debug enable runtime diagnostics — defines DEBUG_RT for both
|
||
# sdcc (-DDEBUG_RT) and the crt0 assembly (prepended
|
||
# `DEBUG_RT = 1`). Exposes runtime introspection symbols
|
||
# such as `_w2_self_allocated` (see <sprinter.h>).
|
||
# --safe link safe versions of libraries (e.g. lib/bgi256_safe.lib)
|
||
# if they exist; otherwise fall back to standard library.
|
||
# -v verbose (echo every command)
|
||
# -h, --help show this help
|
||
#
|
||
# Example:
|
||
# sprinter-cc -o hello.exe hello.c
|
||
# sprinter-cc -o app.exe main.c --bank 1=engine.c
|
||
# sprinter-cc -o tiny.exe tiny.c --crt0=minimal
|
||
|
||
set -eo pipefail
|
||
|
||
# ------- Locate the toolchain ------------------------------------------------
|
||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||
PROJ_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )"
|
||
|
||
SDCC_BIN="$PROJ_ROOT/third_party/sdcc/bin"
|
||
SDCC="$SDCC_BIN/sdcc"
|
||
SDASZ80="$SDCC_BIN/sdasz80"
|
||
MKEXE="$PROJ_ROOT/toolchain/mkexe/mkexe"
|
||
CHECK_BANKS="$PROJ_ROOT/toolchain/check_banks.py"
|
||
|
||
RUNTIME="$PROJ_ROOT/runtime"
|
||
LIB_DIR="$PROJ_ROOT/lib"
|
||
INC_DIR="$PROJ_ROOT/libc/include"
|
||
BGI_INC_DIR="$PROJ_ROOT/libbgi/include" # gfx.h, graphics.h (BGI)
|
||
|
||
# ------- Defaults ------------------------------------------------------------
|
||
OUT=""
|
||
CRT0_TYPE="default"
|
||
MEMORY_MODE="tiny"
|
||
MEMORY_MANUAL=""
|
||
# CODE_LOC / DATA_LOC are derived from MEMORY_MODE unless overridden via
|
||
# explicit --code-loc / --data-loc; capture user intent in USER_*.
|
||
USER_CODE_LOC=""
|
||
USER_DATA_LOC=""
|
||
LOAD_ADDR=""
|
||
ENTRY_ADDR=""
|
||
STACK_ADDR="0xBFFE"
|
||
STACK_SIZE="" # if set, used to compute HEAP_TOP = STACK_ADDR + 1 - STACK_SIZE
|
||
DEBUG_RT=0 # if 1, prepend `DEBUG_RT = 1` to crt0 + pass -DDEBUG_RT to sdcc
|
||
VERBOSE=0
|
||
USER_INCS=()
|
||
SOURCES=()
|
||
LD_EXTRA=()
|
||
MKEXE_EXTRA=()
|
||
BANK_SPECS=() # entries like "1=engine.c"
|
||
W3_SPECS=() # entries like "mod.c" — резидентные модули окна W3 (--w3)
|
||
USER_DEFS=() # -DFOO / -DFOO=bar — пробрасываются в sdcc как есть
|
||
BANK_LOCAL_DATA=0 # 1 (--bank-data): писучие данные банков — В СТРАНИЦУ банка
|
||
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
|
||
# переопределяет (например 3000 = дефолт SDCC для скорости
|
||
# компиляции).
|
||
GFX_MODE="" # "256" → link BGI graphics.h driver sprinter_gfx256.lib
|
||
SAFE_LIBS=0 # if 1, prefer *_safe.lib variants (fallback to standard)
|
||
|
||
# ------- Parse args ----------------------------------------------------------
|
||
usage() {
|
||
sed -n '3,28p' "$0" | sed 's/^# \{0,1\}//'
|
||
exit "${1:-0}"
|
||
}
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
-o) OUT="$2"; shift 2;;
|
||
--crt0=*) CRT0_TYPE="${1#*=}"; shift;;
|
||
-I) USER_INCS+=("-I" "$2"); shift 2;;
|
||
-L) LOAD_ADDR="$2"; shift 2;;
|
||
-E) ENTRY_ADDR="$2"; shift 2;;
|
||
-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"; 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;;
|
||
-D*) USER_DEFS+=("$1"); shift;;
|
||
--bank) BANK_SPECS+=("$2"); shift 2;;
|
||
--bank-data) BANK_LOCAL_DATA=1; shift;;
|
||
--mkexe) MKEXE_EXTRA+=("$2"); shift 2;;
|
||
--max-allocs) MAX_ALLOCS="$2"; shift 2;;
|
||
--gfx) GFX_MODE="$2"; shift 2;;
|
||
--debug) DEBUG_RT=1; shift;;
|
||
--safe) SAFE_LIBS=1; shift;;
|
||
-v) VERBOSE=1; shift;;
|
||
-h|--help) usage 0;;
|
||
-*) echo "sprinter-cc: unknown option: $1" >&2; usage 1;;
|
||
*) SOURCES+=("$1"); shift;;
|
||
esac
|
||
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 ПО УМОЛЧАНИЮ, но поддержан во всех режимах
|
||
# tiny|small|big|huge. В huge резидент ДЕЛИТ окно W3 с трамплин-банками
|
||
# (порт 0xE2): crt0_banked запоминает резидентную страницу и возвращает её
|
||
# дефолтом после загрузки банков, а трамплин bank.s сохраняет/восстанавливает
|
||
# W3 на каждый __banked вызов (см. W3_RESIDENT ниже). Следствие: резидент ->
|
||
# __banked работает, обратное — НЕТ (из банка резидентной страницы в 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 пока не реализован.
|
||
GFX_LD=()
|
||
case "$GFX_MODE" in
|
||
"") ;; # graphics.h не используется
|
||
256)
|
||
if [[ "$SAFE_LIBS" -eq 1 ]] && [[ -f "$LIB_DIR/bgi256_safe.lib" ]]; then
|
||
GFX_LD=("-lbgi256_safe")
|
||
[[ $VERBOSE -eq 1 ]] && echo " gfx: safe variant (lib/bgi256_safe.lib)"
|
||
else
|
||
GFX_LD=("-lbgi256")
|
||
fi
|
||
;;
|
||
16) echo "sprinter-cc: --gfx 16 ещё не реализован (пока только 256)" >&2; exit 1;;
|
||
*) echo "sprinter-cc: --gfx: ожидается 256 (или 16), дано: $GFX_MODE" >&2; exit 1;;
|
||
esac
|
||
|
||
# ------- libc variant selection (--safe) --------------------------------------
|
||
# sprinter.lib — fast (LIBC_NOCHECK: параметр-валидации вырезаны);
|
||
# sprinter_safe.lib — с валидациями. Критичные гарды (_fd_guard) — в обеих.
|
||
LIBC_LD="-lsprinter"
|
||
if [[ "$SAFE_LIBS" -eq 1 ]] && [[ -f "$LIB_DIR/sprinter_safe.lib" ]]; then
|
||
LIBC_LD="-lsprinter_safe"
|
||
[[ $VERBOSE -eq 1 ]] && echo " libc: safe variant (lib/sprinter_safe.lib)"
|
||
fi
|
||
|
||
# ------- Resolve memory mode → CODE_LOC / DATA_LOC ---------------------------
|
||
# tiny : CODE in W2 (0x8100), DATA auto after code (= W2)
|
||
# small : CODE in W1 (0x4100), DATA in W2 (0x8000) — crt0 must alloc W2
|
||
# big : tiny + banked code in W1 (--bank stays in HOME-style trampolines)
|
||
# huge : small + banked code in W3 — same as today's crt0_banked layout
|
||
# manual: parsed from --memory-manual SPEC
|
||
case "$MEMORY_MODE" in
|
||
tiny)
|
||
MODE_CODE_LOC="0x8100"; MODE_DATA_LOC="0"
|
||
;;
|
||
small)
|
||
# CODE at 0x4100; DATA/BSS/HEAP chained auto by linker. Supports
|
||
# any size up to ~30 KB code+data combined. crt0_small reads port
|
||
# 0xA2 at startup to detect if DSS already mapped W2 (program > 16
|
||
# KB) and skips SETWIN2 in that case; otherwise it allocates W2
|
||
# itself via ESTEX $3D + $3A.
|
||
MODE_CODE_LOC="0x4100"; MODE_DATA_LOC="0"
|
||
;;
|
||
big)
|
||
# tiny layout (CODE+DATA in W2) + banked code in W1. crt0_banked
|
||
# gets BANK_W1=1 prepended so its trampoline + bank loader use
|
||
# port 0xA2 (W1) and load address 0x4000. mkexe gets -B 0x4000.
|
||
MODE_CODE_LOC="0x8100"; MODE_DATA_LOC="0"
|
||
;;
|
||
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="0"
|
||
;;
|
||
manual)
|
||
# Defaults if SPEC omits a key.
|
||
MODE_CODE_LOC="0x4100"
|
||
MODE_DATA_LOC="0"
|
||
IFS=',' read -ra _parts <<< "$MEMORY_MANUAL"
|
||
for _p in "${_parts[@]}"; do
|
||
[[ -z "$_p" ]] && continue
|
||
_k="${_p%%=*}"; _v="${_p#*=}"
|
||
case "$_k=$_v" in
|
||
CODE=W1) MODE_CODE_LOC="0x4100";;
|
||
CODE=W2) MODE_CODE_LOC="0x8100";;
|
||
DATA=W1) MODE_DATA_LOC="0x4000";;
|
||
DATA=W2) MODE_DATA_LOC="0x8000";;
|
||
DATA=SAME) MODE_DATA_LOC="0";;
|
||
BANKED=W1|BANKED=W3) : ;; # recorded; banked window only matters for --bank trampolines
|
||
*)
|
||
echo "sprinter-cc: bad --memory-manual entry: $_p" >&2
|
||
echo " valid: CODE=W1|W2, DATA=W1|W2|SAME, BANKED=W1|W3" >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
done
|
||
;;
|
||
*)
|
||
echo "sprinter-cc: unknown --memory mode: $MEMORY_MODE" >&2
|
||
echo " valid: tiny | small | big | huge | manual" >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
# Explicit --code-loc / --data-loc win over the mode defaults.
|
||
CODE_LOC="${USER_CODE_LOC:-$MODE_CODE_LOC}"
|
||
DATA_LOC="${USER_DATA_LOC:-$MODE_DATA_LOC}"
|
||
|
||
LOAD_ADDR="${LOAD_ADDR:-$CODE_LOC}"
|
||
ENTRY_ADDR="${ENTRY_ADDR:-$LOAD_ADDR}"
|
||
|
||
[[ $VERBOSE -eq 1 ]] && echo " memory: mode=$MEMORY_MODE code=$CODE_LOC data=$DATA_LOC load=$LOAD_ADDR"
|
||
|
||
# Pick crt0 source. Memory mode drives default crt0 selection; explicit
|
||
# --crt0=TYPE still wins (e.g. --crt0=minimal for tiny w/o argv).
|
||
#
|
||
# big/huge ALWAYS use crt0_banked, even without explicit --bank flags:
|
||
# big — banks in W1 (BANK_W1=1 prepended) — code stays in W2 = tiny layout
|
||
# huge — banks in W3 — code in W1 = small layout, crt0_banked auto-detects W2
|
||
# crt0_banked also exports startup-info symbols (estex_file_handle etc.).
|
||
case "$MEMORY_MODE" in
|
||
small) DEFAULT_CRT0="small";;
|
||
big|huge) DEFAULT_CRT0="banked";;
|
||
*) DEFAULT_CRT0="default";; # tiny, manual
|
||
esac
|
||
if [[ "$CRT0_TYPE" == "default" && "$DEFAULT_CRT0" != "default" ]]; then
|
||
CRT0_TYPE="$DEFAULT_CRT0"
|
||
fi
|
||
|
||
case "$CRT0_TYPE" in
|
||
default) CRT0_SRC="$RUNTIME/crt0.s"; ;;
|
||
minimal) CRT0_SRC="$RUNTIME/crt0_minimal.s";;
|
||
small) CRT0_SRC="$RUNTIME/crt0_small.s"; ;;
|
||
banked) CRT0_SRC="$RUNTIME/crt0_banked.s"; ;;
|
||
*) echo "sprinter-cc: bad --crt0 type: $CRT0_TYPE" >&2; exit 1;;
|
||
esac
|
||
|
||
# Banked builds always need crt0_banked, regardless of what the user asked.
|
||
if [[ ${#BANK_SPECS[@]} -gt 0 ]] && [[ "$CRT0_TYPE" != "banked" ]]; then
|
||
[[ $VERBOSE -eq 1 ]] && echo " --bank present → switching to crt0_banked"
|
||
CRT0_SRC="$RUNTIME/crt0_banked.s"
|
||
fi
|
||
|
||
# ------- Build in a per-output workdir ---------------------------------------
|
||
WORK="$(dirname "$OUT")/.sprinter-cc-$(basename "$OUT" .exe)"
|
||
mkdir -p "$WORK"
|
||
|
||
run() {
|
||
[[ $VERBOSE -eq 1 ]] && echo " $*"
|
||
"$@"
|
||
}
|
||
|
||
# Decide whether the build needs the W1 banking window (BIG mode). This
|
||
# affects crt0_banked.s and bank.s — both honour `BANK_W1 = 1` prepended
|
||
# by us. HUGE / default (no BANK_W1) → banks live in W3 at port 0xE2.
|
||
BANK_W1=0
|
||
[[ "$MEMORY_MODE" == "big" ]] && BANK_W1=1
|
||
|
||
# Helper: assemble a runtime .s with optional `DEBUG_RT = 1` and/or
|
||
# `BANK_W1 = 1` prepended. No-op prefix path keeps the original source
|
||
# (zero overhead for hot non-debug, non-banked builds).
|
||
asm_runtime() {
|
||
local src="$1" out="$2"
|
||
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"
|
||
run "$SDASZ80" -o "$out" "$patched"
|
||
else
|
||
run "$SDASZ80" -o "$out" "$src"
|
||
fi
|
||
}
|
||
|
||
# 1. crt0 — patched with DEBUG_RT / BANK_W1 if requested.
|
||
CRT0_REL="$WORK/$(basename "$CRT0_SRC" .s).rel"
|
||
asm_runtime "$CRT0_SRC" "$CRT0_REL"
|
||
|
||
# 1b. heap_top — either default from runtime/, or generate custom if user
|
||
# specified --stack-size. HEAP_TOP = stack_top + 1 - stack_size, e.g.
|
||
# --stack-size 2048 with default stack=0xBFFE → HEAP_TOP = 0xB7FF.
|
||
HEAP_TOP_REL="$WORK/heap_top.rel"
|
||
if [[ -n "$STACK_SIZE" ]]; then
|
||
# Resolve numeric values (accept hex 0xNNN or decimal).
|
||
_stack_top=$(( STACK_ADDR )) # bash auto-detects 0x prefix
|
||
_stack_sz=$(( STACK_SIZE ))
|
||
_heap_top=$(( _stack_top + 1 - _stack_sz ))
|
||
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"
|
||
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"
|
||
|
||
# 2. user sources → .rel (HOME)
|
||
USER_RELS=()
|
||
CC_FLAGS=(-mz80 --no-std-crt0 --std-c99 --opt-code-size -I "$INC_DIR" -I "$BGI_INC_DIR" "${USER_INCS[@]}" "${USER_DEFS[@]}")
|
||
[[ $DEBUG_RT -eq 1 ]] && CC_FLAGS+=(-DDEBUG_RT)
|
||
[[ -n "$MAX_ALLOCS" ]] && CC_FLAGS+=(--max-allocs-per-node "$MAX_ALLOCS")
|
||
for src in "${SOURCES[@]}"; do
|
||
rel="$WORK/$(basename "$src" .c).rel"
|
||
run "$SDCC" "${CC_FLAGS[@]}" -c -o "$rel" "$src"
|
||
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
|
||
# n_banks = 0 (crt0_banked.s imports this symbol unconditionally).
|
||
# In BIG mode banks live in W1 at low16 = 0x4000; in HUGE / default
|
||
# mode they live in W3 at low16 = 0xC000.
|
||
BANK_RELS=()
|
||
BANK_LD_FLAGS=()
|
||
if [[ "$CRT0_TYPE" == "banked" ]]; then
|
||
if [[ $BANK_W1 -eq 1 ]]; then
|
||
BANK_LOW16=0x4000
|
||
else
|
||
BANK_LOW16=0xC000
|
||
fi
|
||
# Trampoline + _bank_pages table — depend on BANK_W1, so assemble per build.
|
||
BANK_TRAMP_REL="$WORK/bank_trampoline.rel"
|
||
asm_runtime "$RUNTIME/bank.s" "$BANK_TRAMP_REL"
|
||
BANK_RELS+=("$BANK_TRAMP_REL")
|
||
|
||
if [[ ${#BANK_SPECS[@]} -eq 0 ]]; then
|
||
# User has no banks — supply n_banks=0 so crt0_banked links.
|
||
STUB_C="$WORK/_n_banks_stub.c"
|
||
STUB_REL="$WORK/_n_banks_stub.rel"
|
||
echo "const unsigned char n_banks = 0;" > "$STUB_C"
|
||
run "$SDCC" "${CC_FLAGS[@]}" -c -o "$STUB_REL" "$STUB_C"
|
||
BANK_RELS+=("$STUB_REL")
|
||
else
|
||
for spec in "${BANK_SPECS[@]}"; do
|
||
bank_n="${spec%%=*}"
|
||
bank_src="${spec#*=}"
|
||
rel="$WORK/bank${bank_n}_$(basename "$bank_src" .c).rel"
|
||
# По умолчанию --dataseg НЕ задаём: писучие данные банкового
|
||
# модуля (глобалы, file-static, статики функций) остаются в
|
||
# общем _DATA, то есть в W1/W2 — они замаплены всегда и видны
|
||
# и банку, и остальному коду. С --dataseg BANKn они уезжают в
|
||
# СТРАНИЦУ БАНКА и снаружи не читаются (проверено на .map:
|
||
# глобал банкового модуля лёг по 0x0001C000): это экономит
|
||
# W1/W2, но требует ручного контроля видимости, поэтому
|
||
# включается явно через --bank-data.
|
||
bank_data_flags=()
|
||
[[ $BANK_LOCAL_DATA -eq 1 ]] && bank_data_flags=(--dataseg "BANK${bank_n}")
|
||
run "$SDCC" "${CC_FLAGS[@]}" \
|
||
--codeseg "BANK${bank_n}" --constseg "BANK${bank_n}" \
|
||
"${bank_data_flags[@]}" \
|
||
-c -o "$rel" "$bank_src"
|
||
BANK_RELS+=("$rel")
|
||
# Virtual address: bank_n in upper byte, BANK_LOW16 in low half.
|
||
addr=$(printf "0x%X" $(( (bank_n << 16) | BANK_LOW16 )))
|
||
BANK_LD_FLAGS+=("-Wl-b_BANK${bank_n}=${addr}")
|
||
done
|
||
fi
|
||
fi
|
||
|
||
# 4. link → .ihx
|
||
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).
|
||
#
|
||
# Filter benign "?ASlink-Warning-Definition ... found more than once"
|
||
# warnings. These come from intentional sprinter.lib overrides of the
|
||
# SDCC z80.lib defaults (puts, ___sdcc_heap, asctime/localtime, etc.).
|
||
# Linker picks the first found = our version, so behaviour is correct;
|
||
# 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[@]}" "${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[@]}" "${W3_RELS[@]}" \
|
||
"-L$LIB_DIR" "${GFX_LD[@]}" "$LIBC_LD" 2>&1 \
|
||
| awk '
|
||
/^\?ASlink-Warning-Definition of public symbol/ { skip = 3 }
|
||
skip > 0 { skip--; next }
|
||
{ print }
|
||
'
|
||
fi
|
||
|
||
# Отчёт по раскладке памяти + проверка лимитов — для ЛЮБОЙ модели памяти
|
||
# (сколько свободно в 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).
|
||
# IHX is the positional input — it must come last. Build the prefix
|
||
# (options + extras) first, then append the positional.
|
||
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"
|
||
|
||
echo "sprinter-cc: wrote $OUT"
|