Files
Sprinter-SDCC/bin/sprinter-cc
T
snark13 95c22be9bd 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>
2026-07-23 19:38:00 +03:00

473 lines
22 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)
# -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
# 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
# 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)
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;;
--bank) BANK_SPECS+=("$2"); shift 2;;
--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; с 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 пока не реализован.
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[@]}")
[[ $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"
run "$SDCC" "${CC_FLAGS[@]}" \
--codeseg "BANK${bank_n}" --constseg "BANK${bank_n}" --dataseg "BANK${bank_n}" \
-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"