33 Commits

Author SHA1 Message Date
snark13 a14f19b657 mdview2: поддержка кодировок CP866/CP1251/KOI8-R/UTF-8 (F8)
- Детекция при открытии (BOM + эвристика по первым 4 КБ).
- 8-битные (CP866/CP1251/KOI8-R) — общий индекс/кэш, переключение
  мгновенным ремапом глифов [128-255] на отрисовке по attr (структурные
  глифы — рамка/HR/маркеры — не ремапятся).
- UTF-8 — отдельный набор: декодирование в CP866 (кириллица + ходовые
  символы: стрелки/галка/буллет/тире/кавычки/box), свой индекс/кэш.
- Два набора (docset_t g_doc[2]) со свапом «живых» глобалов; второй
  строится ЛЕНИВО при первом F8-переходе в него (build-on-demand).
- F8: цикл CP866→CP1251→KOI8R→UTF8; метка в меню видна только когда
  переключение возможно; во время сборки 8-бит первичного F8 крутит 8-бит.
- F1-справка: секция Encoding; меню разбито на блоки (F1/F8/F10).
- Фикс: g_doc обязан быть инициализирован (SDCC z80 не обнуляет статики
  надёжно) — иначе мусорный built вёл к показу неинициализированного набора.
- Убрана отладка времени обработки из статус-бара.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:00:49 +03:00
snark13 a6e0aacc80 mdview2: план поддержки кодировок CP1251/KOI8-R/UTF-8 (ревизия 2)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 22:39:49 +03:00
snark13 13ef8d2fa5 mdview2: выровненная отрисовка таблиц с рамкой
Таблица обрабатывается как блок в два прохода:
- проход 1: границы блока + ОТРЕНДЕРЕННЫЕ ширины колонок (мерим тем же
  inline_scan, что и при отрисовке — невидимые маркеры стиля **/код не
  раздувают столбцы);
- проход 2: верхняя рамка ┌┬┐ → строки данных │ ячейка<pad> │ → разделитель
  заголовка ├┼┤ (из строки |---|) → нижняя рамка └┴┘.

Колонки выровнены по содержимому, рамка CP866 box-drawing. Широкая таблица
остаётся nowrap+hscroll. Ячейки разбиваются cell-итератором (без массивов
на стеке); пустой g_cells под измерение освобождается ручным флашем
pending-сегмента (флаг g_skip_flush в emit_seg, чтобы не флашить повторно).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 22:23:57 +03:00
snark13 68201dca44 mdview2: 4 улучшения рендеринга markdown
1. Экранирование пунктуации: \* \_ \` \[ … → литерал, маркером НЕ считается
   (CommonMark ASCII-punctuation). Напр. "**...FILE\***" → болд "...FILE*".
   Не действует внутри инлайн-кода.
2. [x] / [ ] — ровно один символ в квадратных скобках → болд (нестандартно,
   для читабельного отображения task-list checkbox-ов). Не внутри кода.
3. Соседние пункты списка: если следующий имеет МЕНЬШИЙ отступ (dedent),
   пустую строку между ними больше не подавляем — пункты визуально разделены.
4. Абзац с ведущим отступом: все его перенесённые строки получают такой же
   отступ (continuation-префикс из col пробелов, как у списка/цитаты).

Реализация — в обоих inline-сканерах (inline_scan/scan_join_stream) + ветках
index_lines; новый helper is_escapable() и leading_spaces().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 21:38:54 +03:00
snark13 50596e6a2a mdview2: прогрессивная загрузка (первый экран сразу, скролл во время индексации)
Препроцессинг 52КБ занимает ~10с; теперь UI не ждёт его завершения.
index_lines() кооперативно вызывает progress_tick() (без прерываний —
IM2 отложен):
- первый экран рисуется, как только готово ≥ VIEW_H строк (~0.3с);
- статус-бар показывает растущее число обработанных строк с многоточием
  ("L 1-30 / 247...");
- ↑↓ PgUp PgDn Home End (=последняя готовая страница) и F1 работают по
  УЖЕ готовым строкам (drawable_lines = [0..n_lines-2], т.к. последний
  сегмент ещё в g_cells до flush);
- Esc/F10 во время загрузки — корректный выход (прерывание индексации
  флагом g_abort + штатный unload_file/pal_reset/clrscr).

После каждого тика форсируем ре-маппинг W3 (cur_page=0xFF), т.к.
отрисовка/WINREST/BIOS могли сбить страницу, на которую опирается fb().

Временно: debug-индикатор времени обработки "t=Ns" в зоне имени файла
(по просьбе — следить за временем при дальнейших оптимизациях).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 16:46:57 +03:00
snark13 de11882d16 mdview2: снять рудиментные merge-гварды (11→10с)
После полной миграции на merge флаги g_cur_merged/g_next_merged всегда
равны 1 (все типы блоков строит форвард-сканер). Убраны сами флаги и ~30
гвардов `if (g_cur_merged)` в горячем посимвольном цикле — это и небольшое
ускорение (ветка на символ), и чистка.

Проверено замером: инлайн gc_put (макрос/inline) в этот регистро-нагруженный
цикл, наоборот, ЗАМЕДЛЯЕТ (~11→13с) из-за роста спиллов — оставлен функцией.
Вывод: per-char микрооптимизации здесь исчерпаны (asm-fb не помог, инлайн
навредил, снятие гвардов дало ~1с).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 16:06:48 +03:00
snark13 92c0a9a4a9 mdview2: merge препроцессинга в один проход (22→11с)
Раньше препроцессинг сканировал файл ДВАЖДЫ: index_lines считал точки
переноса (forward-сканеры scan_join_stream/inline_scan), а отдельный
render_line_to_cache заново сканировал каждый сегмент для сборки
(char,attr)-ячеек. Профилирование показало, что всё время — в этих двух
посимвольных проходах (bank I/O и cont-walk ≈ 0).

Теперь forward-сканер собирает ячейки в g_cells ПО ХОДУ единственного
прохода; emit_seg флашит ячейки предыдущего сегмента в кэш (lag-1),
последний — после цикла. Перенос строки усекает буфер до снимка на
последнем пробеле (g_ncells_at_space), continuation-сегменты получают
префикс (отступ списка / маркер цитаты 0xB3 / title только в 1-й строке
заголовка). Прямые типы (код verbatim, HR, таблица через nowrap-inline,
blank/fence) строят ячейки на месте.

Миграция шла по типам блоков с dual-verify (старый render строил эталон,
merge сверял ячейки) — найдены и согласованы расхождения forward-сканера
со старым рендером: backtick внутри эмфазиса = литерал; одиночный маркер
закрывает ЛЮБОЙ активный эмфазис; soft-join пропускает ведущие пробелы
строки-продолжения; хвостовые пробелы нерелевантны. Все типы дали 0
расхождений, после чего render_line_to_cache / cc_put / cc_fill /
handle_inline_marker и verify-каркас удалены (−185 строк).

Итог на 52КБ README: 22→11с (2×). Совокупно с прошлым коммитом 24→11с.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 15:41:29 +03:00
snark13 2f4eaf5b25 mdview2: ускорение препроцессинга — asm fb() + инлайн рендер-цикла
Профилирование (52КБ README, раздельный замер): препроцессинг 24с, из них
bank I/O ~0с, cont-walk ~1с — всё время в посимвольных скан-циклах
(индексация 8с + рендер 13с, два прохода по файлу).

- fb() переписана на ассемблере (была закомментированная заготовка): убран
  вызов функции и 32-битная арифметика на каждый байт. Раскладка __sdcccall(1)
  для uint32 аргумента (p=HLDE) и возврат char в A сверены через sdcc -S;
  координация маппинга с bank_read держится на том, что _io_page_w3 — порт
  (__sfr 0xE2), пишем OUT — bank_read читает IN.
- render_line_to_cache: горячий путь обычного текста больше не вызывает
  handle_inline_marker (6 аргументов) на каждый символ — только на маркерах
  ` * _ ~; cc_put заинлайнен. Поведение идентично.

Итого 24→18с. Индекс-скан (8с) лёгкого инлайна не имеет (нет per-char
вызовов). Следующий шаг — merge двух проходов в один.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 11:23:21 +03:00
snark13 138e783e72 mdview2: горизонтальный скролл по типу блока + фиксы fence/HR/SDCC
Горизонтальный скролл (Фаза 5, финал):
- Скроллим по ТИПУ, а не по длине: новый флаг IF_HSCROLL ставится только
  на код и таблицы; HR и границы fence (IF_NOWRAP без IF_HSCROLL) не
  двигаются. Блок едет целиком, включая строки короче 80.
- Обход бага кодогенерации SDCC z80: `if (n!=g) g=n;` пишет (n-g) вместо n
  (SUB сравнения затирает A, store переиспользует испорченный A). Лечится
  записью viewport_x ДО сравнения. Минимальный репродьюсер и оба описания
  для трекера — в docs/bugs/sdcc-z80-cmp-store-a/ (воспроизводится на чистом
  sdcc 4.5, в т.ч. с --no-peep → это кодогенератор, не peephole).

Рендеринг:
- Отступленный fence (```c внутри списка) теперь распознаётся: is_fence_raw
  пропускает ведущие пробелы/табы; то же в рендере прячет строку-границу.
- Строки-разделители (HR, ровно 80) больше не участвуют в скролле.

Чистка: удалён мёртвый код (is_fence_delim, get_init_style[_raw], is_cont,
seg_flags). Makefile (mdview/mdview2): iconv UTF-8→CP866 завершается ненулевым
кодом при отбрасывании символов — игнорируем (|| true).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 10:11:27 +03:00
snark13 eab5a2d6ac mdview2: add render-cache markdown viewer (Phases 0-4)
New examples/mdview2 — render-cache version of mdview: each logical
line is rendered into an EMM (char,attr) buffer once during file load
(interleaved with index_lines()), then scrolling draws straight from
the cache via ESTEX WINREST, with no re-parsing or fb() access in the
steady state. Horizontal scroll still uses the old live-render path
(Phase 5, not yet migrated).

Format and budget were derisked empirically first (tests/winrest):
confirmed ESTEX WINCOPY/WINREST buffer layout and measured EMM free
space, both folded into the implementation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 22:41:32 +03:00
snark13 1b39a60ff0 tests: add hello2, simple, banktest smoke tests
hello2 — stdio vs conio output API comparison (fast/no-attr vs
slower/attributed), also exercises textcolor/textbackground.
simple — packed 2-bit style array bit-twiddling smoke test.
banktest — __banked function calls across two explicit banks in huge
memory mode.

hello2.c updated to call the renamed gettextmode/settextmode (see the
conio rename in 5e5e70d).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 17:12:19 +03:00
snark13 8cfdcf307a libc: add mem_bios/mem_estex EMM page-allocation wrappers
mem_alloc_pages/mem_free_block/mem_get_page/mem_info, implemented twice:
mem_estex.c via ESTEX $3C-$3E (INFOMEM/GETMEM/FREEMEM), mem_bios.c via
the BIOS $C0/$C2-$C4/$C6 EMM_FN* calls. lib/Makefile already listed both
under LIBC_C.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 17:09:14 +03:00
snark13 49a87b4d02 libc: add Sprinter palette API (graphics + text planes)
palette.h/video/palette.c wrap BIOS $A4 PIC_SET_PAL/PIC_GET_PAL and $A6
SET_PAL_INIT for the 8 palette pages (0-3 graphics, 4-7 text planes).
gfx_palette.c and conio/text_palette.c are thin per-domain wrappers;
text_palette.c maps the text "plane" 0..3 (paper/ink/blink-paper/
blink-ink) onto BIOS pages 4..7. Covered by tests/text_palette.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 17:07:55 +03:00
snark13 5e5e70d0c8 conio: switch cursor get/set to BIOS calls, rename text-mode accessors
_get_cursor/_set_cursor now use BIOS GetCursor/SetCursor (RST 8, 08Eh/084h)
instead of ESTEX CURSOR/LOCATE; cursor position packed into a two_bytes
union. get_videotextmode/set_videotextmode renamed to gettextmode/settextmode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 17:07:16 +03:00
snark13 016fedd94c mdview: speed up rendering via bios/text.h, fix wrap-boundary style bug
Replace per-character wrchar() loops in render_line/fill_row/help screens
with batched BIOS calls (bios_writeattr/bios_fillcharattr), and cache the
per-line index record (idx_get) instead of refetching it ~13 times per
rendered line — each refetch cost two W3 bank switches via bank_read().

Also fixes a pre-existing indexer bug: when word-wrap pushes a token that
starts with an emphasis/code marker (e.g. `_text_`) onto a new line, the
marker got re-scanned a second time during the wrap continuation, flipping
line_style back off and corrupting the rendered attribute of the next
word. Fixed by snapshotting line_style at the last seen space and rolling
back to that snapshot (both the saved continuation style and the live
scan state) when a wrap is taken.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 17:06:35 +03:00
snark13 c9d5ea35a4 libc: add bios/text.h BIOS text-output API
Direct __naked z80 wrappers over BIOS LP_PRINT_*/LP_SET_PLACE/LP_GET_PLACE
(RST 8, opcodes 081h-08Eh) for fast text rendering — a single BIOS call
replaces a wrchar() loop for constant-attribute/constant-character runs.
Covered by tests/bios_text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 17:06:13 +03:00
snark13 bd116d7361 ChangeLog:
- update.
2026-06-10 10:41:03 +03:00
snark13 858e5755ad ChangeLog:
- big commit.
2026-06-10 10:35:48 +03:00
snark13 f87b52bb7f ChangeLog:
- memory optimized version.
2026-06-08 15:34:36 +03:00
snark13 07c4004bd9 ChangeLog:
- commit current version MDView.c
2026-06-08 11:22:15 +03:00
snark13 6992c1436e docs(mdview): добавить описание работы и тестовые паттерны
Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 22:39:14 +03:00
snark13 07c398a560 ChangeLog:
- Russian commentaries.
2026-06-07 22:01:13 +03:00
snark13 4bed9d3f3f fix(mdview): корректный multiline quote join в render_line
- исправлен rewind до первого non-cont сегмента для continuation
- для quote-потока newline обрабатывается как soft join с пропуском сырого ' > ' маркера
- восстановлен quote-префикс на continuation строках

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 20:26:19 +03:00
snark13 463a058f56 mdview: multiline quote paragraphs with quote-aware joining
- Quote blocks now join consecutive quote lines into one paragraph with
  soft breaks and wrap continuation under quote prefix.
- Empty quote lines ('>' / '>   ') act as quote-paragraph separators.
- Nested quote starts ('> > ...') are not merged into the parent text and
  stay separate rows.
- Keeps existing inline emphasis handling inside quote content.

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 19:24:59 +03:00
snark13 8abc1d6c16 mdview: drop printf from status rendering
Replace status-line printf formatting with compact manual decimal rendering
(u16/u8 right-aligned helpers + wrchar/put_str_attr). This removes runtime
printf usage from mdview.c and keeps fixed status columns without stale digits.

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 19:19:13 +03:00
snark13 982af12710 mdview: multiline list items with lazy continuation and blank-line grouping
- List items (UL/OL) can now span multiple source lines: non-marker
  lines are joined into the current item as lazy continuation.
- Continuation-line leading indentation is trimmed before joining so
  wrapped item text is separated by a single space.
- A single blank line between adjacent list markers is suppressed
  (same visual list), while 2+ blank lines still produce a separator.

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 19:13:02 +03:00
snark13 394ee3a2cd mdview: blank-row block separation; tables nowrap; fix continuation marker
- Paragraph scanning no longer swallows the blank line separating it from
  the next block; runs of blank lines collapse to one row. Restores blank
  separation between paragraphs, headers and horizontal rules.
- Detect table rows (first non-space char '|') as nowrap segments; they are
  no longer merged into surrounding text or each other.
- Continuation (wrapped) rows render content as plain text and are no longer
  re-classified, so a wrapped word starting with '-'/'#'/'>' is not mis-drawn
  as a list/heading/quote marker.

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 12:50:30 +03:00
snark13 47c9cd326a mdview: wide-break '\\', strikethrough, horizontal scroll bounds + '<'
- A trailing backslash before a newline now forces an in-paragraph line
  break (like two trailing spaces); render consumes the marker (non-code).
- Add ~~strikethrough~~ inline style (INIT_STYLE_STRIKE / EM_STRIKE),
  parsed in inline_scan, the paragraph merger and render, mirroring **.
- Horizontal pan is bounded by the widest nowrap segment on screen, and a
  '<' indicator marks hidden content off the left edge.

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 11:55:19 +03:00
snark13 ca5f30b332 mdview: move segment index to EMM bank; inline hot byte reads
- Index is now an 8-byte record per visible segment in a dedicated EMM
  block (idx_get/idx_put), freeing ~11 KB of near RAM and lifting the
  old 2048-line cap (dynamic max_lines = index_pages * 2048).
- The per-byte scan keeps the previous segment offset in a near var
  (cur_seg_off) and mirrors the last record (cur_rec), so it never reads
  the index back from the bank.
- fb()/map_page() are inlined now that there is code headroom, removing
  per-byte call + 32-bit argument marshalling overhead.

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 11:49:36 +03:00
snark13 0ad0559fc8 mdview: faster file-byte access (fb byte-decode + page cache)
Decode page/offset from the 32-bit offset's bytes to avoid SDCC z80
32-bit shift/mask helpers on the hot path; map_page() uses a cached
file_phys[] table to skip mem_get_page() on every W3 swap.
Behaviour-preserving.

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 01:00:49 +03:00
snark13 035d93ab51 mdview: fix fenced code block emphasis leak and carry emphasis across wrap lines
- Add fenced code block tracking to the wrap-pass in index_lines().
  line_style is reset to PLAIN at every ``` delimiter, and all segs
  inside a fenced block get init_style=PLAIN. This prevents emphasis
  markers (e.g. _ in __var) inside code blocks from leaking into later
  normal text.
- Also carry init_style across wrap continuation segs so that a long
  bold/italic line that is wrapped continues with the correct style on
  the next segment.
- The fence bitmap pass now only updates in_code[], since init_style is
  already set correctly by the wrap pass.
2026-06-06 12:01:15 +03:00
snark13 0dedc4dac8 ChangeLog:
- prebuild.
2026-06-05 23:33:23 +03:00
snark13 737c974400 Add mdview markdown viewer, reorganize tests/examples and libc layout
- Split tests/ (libc feature tests) and examples/ (real apps); shared
  app.mk in repo root, was examples/example.mk
- libc/io/* split into libc/{conio,env,errno,file,mouse,string,sys,
  time,video}/ — clearer module boundaries
- New examples/mdview/: markdown viewer (Phases 1-5 + light nested
  lists). Headers (H1-H4), HR, ulist/olist/quote with nesting via
  leading spaces, fenced code blocks, inline emphasis (bold/italic/
  underscore/code), wrap/unwrap mode with soft wrap (F2), horizontal
  pan (← →) with '>' truncation indicator
- libc additions: scroll() in conio (ESTEX SCROLL), strlwr/strupr,
  gets() test
- Makefile updates across tests/ for the new shared app.mk path

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 22:23:36 +03:00
145 changed files with 9361 additions and 457 deletions
+18 -1
View File
@@ -9,7 +9,8 @@ build/
# sprinter-cc per-example intermediate directory
.sprinter-cc-*/
# Per-example final/intermediate outputs landing alongside the source
# Per-program final/intermediate outputs landing alongside the source
# (real apps under examples/ and libc feature tests under tests/).
examples/*/*.exe
examples/*/*.asm
examples/*/*.lst
@@ -23,6 +24,22 @@ examples/*/*.cdb
examples/*/*.mem
examples/*/*.rst
# Temporary build directory for floppy disk image preparation
examples/*/.disk_tmp/
tests/*/*.exe
tests/*/*.asm
tests/*/*.lst
tests/*/*.lk
tests/*/*.ihx
tests/*/*.noi
tests/*/*.sym
tests/*/*.map
tests/*/*.rel
tests/*/*.cdb
tests/*/*.mem
tests/*/*.rst
# libc archive (built from libc/, see lib/Makefile)
lib/*.lib
+32 -44
View File
@@ -1,58 +1,41 @@
# Sprinter C Compiler — top-level Makefile
#
# make build host tools, libc archive, and all examples
# make build host tools, libc archive, all tests, all apps
# make tools build only host tools (mkexe)
# make lib build lib/sprinter.lib (libc archive used by sprinter-cc)
# make examples build all examples
# make floppy package every example + test files into mame/v306/IMG/mc.img
# make tests build all libc feature tests under tests/
# make examples build all real applications under examples/
# make floppy package every .exe + test fixtures into mame/v306/IMG/mc.img
# make check run mkexe unit tests
# make clean remove all build artefacts
# make sdcc download/extract vendored SDCC
#
# Most heavy lifting is delegated to sub-Makefiles.
EXAMPLES := hello banked bankedbg strtest cat seek malloc mem_test argv errno rt_test openenv ls conio attrprob timedir mouse banklocl stdlib assrtest ptime stattest filetest gfx_demo gfx_d16 gfx_text gfx_mous
# Small libc-feature tests (one program per .c-language feature or libc API).
TESTS := hello banked bankedbg strtest cat seek malloc mem_test argv errno \
rt_test openenv ls conio attrprob timedir mouse banklocl stdlib \
assrtest ptime stattest filetest gfx_demo gfx_d16 gfx_text gfx_mous
# Larger end-user applications under examples/.
APPS := mdview
MAME_DIR := mame/v306
FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
MAKE_DISK := $(MAME_DIR)/make_disk.py
EXE_FILES := \
examples/hello/hello.exe \
examples/banked/banked.exe \
examples/bankedbg/bankedbg.exe \
examples/strtest/strtest.exe \
examples/cat/cat.exe \
examples/seek/seek.exe \
examples/malloc/malloc.exe \
examples/mem_test/mem_test.exe \
examples/argv/argv.exe \
examples/errno/errno.exe \
examples/rt_test/rt_test.exe \
examples/openenv/openenv.exe \
examples/ls/ls.exe \
examples/conio/conio.exe \
examples/attrprob/attrprob.exe \
examples/timedir/timedir.exe \
examples/mouse/mouse.exe \
examples/banklocl/banklocl.exe \
examples/stdlib/stdlib.exe \
examples/assrtest/assrtest.exe \
examples/ptime/ptime.exe \
examples/stattest/stattest.exe \
examples/filetest/filetest.exe \
examples/gfx_demo/gfx_demo.exe \
examples/gfx_d16/gfx_d16.exe \
examples/gfx_text/gfx_text.exe \
examples/gfx_mous/gfx_mous.exe
TEST_EXES := $(foreach t,$(TESTS),tests/$(t)/$(t).exe)
APP_EXES := $(foreach a,$(APPS),examples/$(a)/$(a).exe)
ALL_EXES := $(TEST_EXES) $(APP_EXES)
DATA_FILES := \
examples/cat/test.txt \
examples/seek/big.txt
tests/cat/test.txt \
tests/seek/big.txt \
examples/mdview/SAMPLE.MD
.PHONY: all tools lib examples check clean sdcc floppy $(EXAMPLES)
.PHONY: all tools lib tests examples check clean sdcc floppy $(TESTS) $(APPS)
all: tools lib examples
all: tools lib tests examples
tools:
$(MAKE) -C toolchain/mkexe
@@ -63,18 +46,22 @@ lib:
check: tools
$(MAKE) -C toolchain/mkexe check
examples: $(EXAMPLES)
tests: $(TESTS)
examples: $(APPS)
$(EXAMPLES): tools lib
$(TESTS): tools lib
$(MAKE) -C tests/$@
$(APPS): tools lib
$(MAKE) -C examples/$@
# Generate big.txt if missing (gen_bigfile.py creates 100 KB marker file).
examples/seek/big.txt:
cd examples/seek && python3 gen_bigfile.py big.txt 102400
tests/seek/big.txt:
cd tests/seek && python3 gen_bigfile.py big.txt 102400
# Re-pack the MAME floppy image with every built example + needed data files.
floppy: examples examples/seek/big.txt
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXE_FILES) $(DATA_FILES)
# Re-pack the MAME floppy image with every built exe + needed data files.
floppy: tests examples tests/seek/big.txt
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(ALL_EXES) $(DATA_FILES)
@echo
@echo "Floppy ready: $(FLOPPY_IMG)"
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
@@ -82,7 +69,8 @@ floppy: examples examples/seek/big.txt
clean:
$(MAKE) -C toolchain/mkexe clean
$(MAKE) -C lib clean
@for e in $(EXAMPLES); do $(MAKE) -C examples/$$e clean; done
@for t in $(TESTS); do $(MAKE) -C tests/$$t clean; done
@for a in $(APPS); do $(MAKE) -C examples/$$a clean; done
sdcc:
bash third_party/setup-sdcc.sh
+1 -2
View File
@@ -52,7 +52,7 @@ pages by program size — small programs get only one page. Pick a memory mode
on what your program needs:
| Mode | Code lives in | Banking | Use when | Note |
|---|---|---|---|
|---|---|---|---|---|
| `tiny` (default) | W2 (0x8100+) | no | code+data < 14 KB | |
| `small` | W1-W2 (0x4100+) | no | code+data < 30 KB | |
| `big` | W2 + W1 banking | yes (W1) | tiny + extra code modules | |
@@ -217,7 +217,6 @@ This repository contains:
* **Sprinter / Peters Plus** — Иван Мак, Дмитрий Паринов and the original team
* **SDCC** — for the underlying Z80 compiler
* **z88dk +pps** — Дмитрий M. for paving the way with the first Sprinter target
* **MAME** — for the Sprinter Sp2000 emulation
---
+8 -6
View File
@@ -1,9 +1,11 @@
# example.mk — shared Makefile fragment for simple Sprinter ESTEX examples.
# app.mk — shared Makefile fragment for any standalone Sprinter ESTEX
# program — used both by libc feature tests under tests/ and by real
# applications under examples/.
#
# Usage in an example's Makefile:
# Usage in a per-program Makefile:
#
# PROJ_ROOT := $(abspath $(CURDIR)/../..)
# EXAMPLE := my_example # base name (matches my_example.c)
# EXAMPLE := my_program # base name (matches my_program.c)
#
# # Optional overrides (any combination):
# # MEMORY := small # tiny | small | big | huge | manual
@@ -12,7 +14,7 @@
# # EXTRA_FLAGS := --crt0=minimal # passed through to sprinter-cc
# # EXTRA_DATA := test.txt # extra files to add to `make floppy`
#
# include $(PROJ_ROOT)/examples/example.mk
# include $(PROJ_ROOT)/app.mk
#
# Pipeline (all driven by sprinter-cc):
# crt0 + EXAMPLE.c + EXTRA_SRCS --sdcc--> .ihx
@@ -61,9 +63,9 @@ $(LIB):
clean:
rm -rf .sprinter-cc-* $(EXAMPLE).exe
# `make floppy` packs ONLY this example (+ optional EXTRA_DATA files) into
# `make floppy` packs ONLY this program (+ optional EXTRA_DATA files) into
# the MAME floppy image, replacing whatever was there. Handy for trying a
# single program without rebuilding all 27 examples.
# single program without rebuilding everything.
floppy: $(EXAMPLE).exe
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(EXTRA_DATA)
@echo
+127
View File
@@ -0,0 +1,127 @@
# [z80] `global = local` after `if (local != global)` stores `local - global` (A clobbered by comparison)
## Summary
On the z80 backend, the sequence
```c
if (n != g) { g = n; }
```
where `n` is in register `A` and `g` is a global, miscompiles. The compiler
evaluates the `!=` comparison with `SUB A,(HL)`, which destroys `A`, and then
emits the assignment `g = n` by storing `A` — which now holds `n - g`, not `n`.
As a result the global ends up holding `(unsigned char)(n - old_g)` instead of `n`.
It should either use `CP (HL)` (which leaves `A` intact) for the comparison, or
reload `n` before the store.
## Version
SDCC 4.5.0 #15242 (Mac OS X x86_64). Default options; also reproduces with
`--opt-code-speed` and with `--no-peep` (so this is a code-generator bug, not a
peephole-optimizer bug).
## Minimal reproducer
```c
unsigned char vx;
void update(unsigned char n)
{
if (n != vx) {
vx = n;
}
}
```
Build:
```
sdcc -mz80 -S repro.c
```
## Generated assembly (wrong)
```asm
_update::
;repro.c: if (n != vx) {
ld hl, #_vx
sub a, (hl) ; A (= n) is destroyed: A = n - vx
ret Z
;repro.c: vx = n;
ld (_vx+0), a ; stores (n - vx) instead of n
;repro.c: }
ret
```
With `--no-peep` the same defect is present (only the branch shape differs):
```asm
_update::
ld iy, #_vx
sub a, 0 (iy) ; A (= n) destroyed
jp NZ, 00112$
jp 00103$
00112$:
ld (_vx+0), a ; stores (n - vx)
00103$:
ret
```
## Why it happens
`n` arrives in `A` (sdcccall). The code generator picks `SUB A,(HL)` to evaluate
the relational `n != vx`. `SUB` overwrites `A` with the difference. The generator
then treats the still-live value `n` as if it were still in `A` and emits a bare
store `LD (_vx),A` for the assignment, without reloading `n` first. Because the
defect survives `--no-peep`, it is in code generation (register/lifetime tracking
across the comparison), not in the peephole optimizer.
The correct lowering for the comparison is `CP (HL)`, which sets the flags exactly
like `SUB` but preserves `A`, so the subsequent store would be correct with no
extra instructions.
## Variants that also reproduce
- `if (n == vx) return; vx = n;` (early-return form)
- `n` coming from a function call result instead of a parameter
- both `-mz80` default and `--opt-code-speed`
## Workaround
Store into the global *before* the comparison, so the destructive `SUB` is no
longer on the store path; compare a saved copy instead:
```c
void update(unsigned char n)
{
unsigned char old = vx;
vx = n; /* store first, A still holds n */
if (n != old) {
/* side effect */
}
}
```
generates the correct:
```asm
_update::
ld (_vx+0), a
ret
```
## Files in this directory
- `repro.c` — minimal reproducer
- `repro.asm` — generated output, default options (defect visible)
- `repro.nopeep.asm` — generated output with `--no-peep` (defect still present)
- `workaround.c` / `workaround.asm` — store-before-compare workaround (correct)
## Tracker search
A search of the SDCC bug tracker did not turn up an exact duplicate. The closest
version-matching report, #3834 "[Z80][SDCC 4.5] Compiler bug", is a *different*
defect (`genPointerSet`, swapped push/pop order), not this comparison-clobbers-A
case.
+127
View File
@@ -0,0 +1,127 @@
# [z80] `global = local` после `if (local != global)` пишет `local - global` (A затирается сравнением)
## Кратко
На бэкенде z80 последовательность
```c
if (n != g) { g = n; }
```
где `n` лежит в регистре `A`, а `g` — глобальная переменная, компилируется
неверно. Сравнение `!=` вычисляется через `SUB A,(HL)`, который **разрушает `A`**,
после чего присваивание `g = n` выполняется записью `A`а там уже `n - g`, а не
`n`. В итоге в глобальную переменную попадает `(unsigned char)(n - старое_g)`
вместо `n`.
Корректно было бы использовать для сравнения `CP (HL)` (он не меняет `A`) либо
перезагрузить `n` перед записью.
## Версия
SDCC 4.5.0 #15242 (Mac OS X x86_64). Опции по умолчанию; также воспроизводится с
`--opt-code-speed` и с `--no-peep` (то есть это баг кодогенератора, а не
peephole-оптимизатора).
## Минимальный пример
```c
unsigned char vx;
void update(unsigned char n)
{
if (n != vx) {
vx = n;
}
}
```
Сборка:
```
sdcc -mz80 -S repro.c
```
## Сгенерированный ассемблер (неверный)
```asm
_update::
;repro.c: if (n != vx) {
ld hl, #_vx
sub a, (hl) ; A (= n) разрушается: A = n - vx
ret Z
;repro.c: vx = n;
ld (_vx+0), a ; пишет (n - vx) вместо n
;repro.c: }
ret
```
С `--no-peep` дефект тот же (отличается лишь форма ветвления):
```asm
_update::
ld iy, #_vx
sub a, 0 (iy) ; A (= n) разрушен
jp NZ, 00112$
jp 00103$
00112$:
ld (_vx+0), a ; пишет (n - vx)
00103$:
ret
```
## Почему так происходит
`n` приходит в `A` (sdcccall). Кодогенератор выбирает `SUB A,(HL)` для вычисления
отношения `n != vx`. `SUB` перезаписывает `A` разностью. Далее генератор считает,
что ещё «живое» значение `n` по-прежнему в `A`, и для присваивания выдаёт голую
запись `LD (_vx),A`, не перезагрузив `n`. Поскольку дефект сохраняется при
`--no-peep`, он находится в кодогенерации (учёт регистров/времён жизни через
сравнение), а не в peephole-оптимизаторе.
Правильное преобразование сравнения — `CP (HL)`: он выставляет флаги ровно как
`SUB`, но сохраняет `A`, поэтому последующая запись была бы корректной без единой
лишней инструкции.
## Варианты, которые тоже воспроизводят
- `if (n == vx) return; vx = n;` (форма с ранним выходом)
- `n` как результат вызова функции, а не как параметр
- и `-mz80` по умолчанию, и `--opt-code-speed`
## Обходной путь (workaround)
Записывать в глобальную переменную **до** сравнения, чтобы разрушающий `SUB` не
оказался на пути записи; сравнивать сохранённую копию:
```c
void update(unsigned char n)
{
unsigned char old = vx;
vx = n; /* запись первой, A ещё держит n */
if (n != old) {
/* побочный эффект */
}
}
```
даёт корректное:
```asm
_update::
ld (_vx+0), a
ret
```
## Файлы в этом каталоге
- `repro.c` — минимальный пример
- `repro.asm` — вывод с опциями по умолчанию (дефект виден)
- `repro.nopeep.asm` — вывод с `--no-peep` (дефект сохраняется)
- `workaround.c` / `workaround.asm` — обход «запись до сравнения» (корректно)
## Поиск по трекеру
Поиск по баг-трекеру SDCC точного дубликата не нашёл. Ближайший по версии отчёт
#3834 «[Z80][SDCC 4.5] Compiler bug» — это **другой** дефект (`genPointerSet`,
переставленный порядок push/pop), а не данный случай «сравнение затирает A».
+61
View File
@@ -0,0 +1,61 @@
;--------------------------------------------------------
; File Created by SDCC : free open source ISO C Compiler
; Version 4.5.0 #15242 (Mac OS X x86_64)
;--------------------------------------------------------
.module repro
.optsdcc -mz80 sdcccall(1)
;--------------------------------------------------------
; Public variables in this module
;--------------------------------------------------------
.globl _update
.globl _vx
;--------------------------------------------------------
; special function registers
;--------------------------------------------------------
;--------------------------------------------------------
; ram data
;--------------------------------------------------------
.area _DATA
_vx::
.ds 1
;--------------------------------------------------------
; ram data
;--------------------------------------------------------
.area _INITIALIZED
;--------------------------------------------------------
; absolute external ram data
;--------------------------------------------------------
.area _DABS (ABS)
;--------------------------------------------------------
; global & static initialisations
;--------------------------------------------------------
.area _HOME
.area _GSINIT
.area _GSFINAL
.area _GSINIT
;--------------------------------------------------------
; Home
;--------------------------------------------------------
.area _HOME
.area _HOME
;--------------------------------------------------------
; code
;--------------------------------------------------------
.area _CODE
;repro.c:20: void update(unsigned char n)
; ---------------------------------
; Function update
; ---------------------------------
_update::
;repro.c:22: if (n != vx) {
ld hl, #_vx
sub a, (hl)
ret Z
;repro.c:23: vx = n;
ld (_vx+0), a
;repro.c:25: }
ret
.area _CODE
.area _INITIALIZER
.area _CABS (ABS)
+25
View File
@@ -0,0 +1,25 @@
/*
* Minimal reproducer — SDCC z80 codegen bug.
*
* "global = local" right after "if (local != global)" stores a WRONG value:
* the SUB used to evaluate the comparison clobbers register A (which still
* holds `n`), and the store reuses that clobbered A.
*
* Build (stock SDCC, no extra flags, no custom toolchain):
* sdcc -mz80 -S repro.c
*
* Tested with: SDCC 4.5.0 #15242 (Mac OS X x86_64), default options.
* Also reproduces with --opt-code-speed and with --no-peep
* (so it is a code-generator bug, not a peephole-optimizer bug).
*
* Expected semantics: after update(n), vx == n whenever n != vx.
* Actual result: vx == (unsigned char)(n - old_vx).
*/
unsigned char vx;
void update(unsigned char n)
{
if (n != vx) {
vx = n;
}
}
@@ -0,0 +1,64 @@
;--------------------------------------------------------
; File Created by SDCC : free open source ISO C Compiler
; Version 4.5.0 #15242 (Mac OS X x86_64)
;--------------------------------------------------------
.module repro
.optsdcc -mz80 sdcccall(1)
;--------------------------------------------------------
; Public variables in this module
;--------------------------------------------------------
.globl _update
.globl _vx
;--------------------------------------------------------
; special function registers
;--------------------------------------------------------
;--------------------------------------------------------
; ram data
;--------------------------------------------------------
.area _DATA
_vx::
.ds 1
;--------------------------------------------------------
; ram data
;--------------------------------------------------------
.area _INITIALIZED
;--------------------------------------------------------
; absolute external ram data
;--------------------------------------------------------
.area _DABS (ABS)
;--------------------------------------------------------
; global & static initialisations
;--------------------------------------------------------
.area _HOME
.area _GSINIT
.area _GSFINAL
.area _GSINIT
;--------------------------------------------------------
; Home
;--------------------------------------------------------
.area _HOME
.area _HOME
;--------------------------------------------------------
; code
;--------------------------------------------------------
.area _CODE
;repro.c:20: void update(unsigned char n)
; ---------------------------------
; Function update
; ---------------------------------
_update::
;repro.c:22: if (n != vx) {
ld iy, #_vx
sub a, 0 (iy)
jp NZ, 00112$
jp 00103$
00112$:
;repro.c:23: vx = n;
ld (_vx+0), a
00103$:
;repro.c:25: }
ret
.area _CODE
.area _INITIALIZER
.area _CABS (ABS)
@@ -0,0 +1,57 @@
;--------------------------------------------------------
; File Created by SDCC : free open source ISO C Compiler
; Version 4.5.0 #15242 (Mac OS X x86_64)
;--------------------------------------------------------
.module workaround
.optsdcc -mz80 sdcccall(1)
;--------------------------------------------------------
; Public variables in this module
;--------------------------------------------------------
.globl _update
.globl _vx
;--------------------------------------------------------
; special function registers
;--------------------------------------------------------
;--------------------------------------------------------
; ram data
;--------------------------------------------------------
.area _DATA
_vx::
.ds 1
;--------------------------------------------------------
; ram data
;--------------------------------------------------------
.area _INITIALIZED
;--------------------------------------------------------
; absolute external ram data
;--------------------------------------------------------
.area _DABS (ABS)
;--------------------------------------------------------
; global & static initialisations
;--------------------------------------------------------
.area _HOME
.area _GSINIT
.area _GSFINAL
.area _GSINIT
;--------------------------------------------------------
; Home
;--------------------------------------------------------
.area _HOME
.area _HOME
;--------------------------------------------------------
; code
;--------------------------------------------------------
.area _CODE
;workaround.c:4: void update(unsigned char n)
; ---------------------------------
; Function update
; ---------------------------------
_update::
ld (_vx+0), a
;workaround.c:8: if (n != old) {
;workaround.c:11: }
ret
.area _CODE
.area _INITIALIZER
.area _CABS (ABS)
@@ -0,0 +1,11 @@
/* Workaround: store BEFORE the comparison so the SUB that evaluates the
* comparison no longer sits on the store path. Generates correct code. */
unsigned char vx;
void update(unsigned char n)
{
unsigned char old = vx;
vx = n; /* store first, with A still holding n */
if (n != old) {
/* redraw / side effect */
}
}
+188
View File
@@ -0,0 +1,188 @@
# MDView: описание работы и тестовые паттерны
## Назначение
`mdview` — консольный просмотрщик Markdown для текстового режима 80x32.
Программа загружает `.MD`-файл, индексирует его на экранные сегменты и рендерит содержимое с учётом переносов, стилей и специальных блоков.
## Как работает программа
### 1) Загрузка файла
- На старте вызывается `load_file(path)`.
- Если путь не передан, используется `SAMPLE.MD`.
- При ошибках загрузки программа показывает причину и завершает работу после нажатия клавиши.
### 2) Индексация (`index_lines`)
- Файл разбирается в массив сегментов для быстрого рендера.
- Поддерживаются ключевые типы строк:
- заголовки `#..####`,
- маркированные/нумерованные списки,
- цитаты `>`,
- горизонтальные разделители,
- fenced code-блоки,
- таблицы (nowrap-режим),
- обычные параграфы.
- Для list/quote/plain используется общая логика сканирования с режимами склейки строк.
### 3) Рендер (`render_line`, `render_viewport`)
- Для каждой видимой строки рисуется префикс (маркер списка, цитаты и т.д.) и текст.
- Поддерживаются inline-стили:
- `**жирный**`,
- `*курсив*`,
- `_подчёркнутый_`,
- `~~зачёркнутый~~`,
- `` `code` ``.
- Табы расширяются до фиксированного шага.
- Для длинных nowrap-строк применяется горизонтальный сдвиг и индикаторы `<`/`>`.
### 4) Навигация
- `Up/Down` — прокрутка на 1 строку.
- `PgUp/PgDn` — прокрутка на экран.
- `Home/End` — начало/конец документа.
- `Left/Right` — горизонтальная прокрутка nowrap-контента.
- `Esc` или `F10` — выход.
## Сборка и запуск
```bash
make -C examples/mdview
```
Запуск на целевой системе:
```bash
mdview.exe <путь_к_файлу.md>
```
Если путь не указан, открывается `SAMPLE.MD`.
## Тестовые паттерны
Ниже набор паттернов для ручной регрессии.
### P01 — Базовый smoke-тест открытия
Вход:
- валидный markdown-файл среднего размера.
Ожидается:
- файл открывается без ошибок;
- статус-бар и меню отображаются корректно;
- прокрутка работает.
### P02 — Ошибка открытия файла
Вход:
- несуществующий путь к файлу.
Ожидается:
- сообщение `mdview: cannot load file`;
- корректный код причины (`open failed` и т.п.);
- ожидание клавиши перед выходом.
### P03 — Пустой файл
Вход:
- пустой `.md`.
Ожидается:
- сообщение `mdview: empty file`;
- корректное завершение после нажатия клавиши.
### P04 — Обычный параграф и переносы
Шаблон:
```md
Это длинный абзац для проверки переноса строк в обычном тексте.
Вторая строка должна мягко склеиться с первой.
```
Ожидается:
- строки склеиваются как единый параграф;
- переносы происходят по ширине экрана без потери символов.
### P05 — Hard break в параграфе
Шаблон:
```md
Первая строка с двумя пробелами в конце.
Вторая строка после hard break.
```
Ожидается:
- между строками сохраняется принудительный разрыв;
- inline-стиль не ломается.
### P06 — Многострочный список с lazy continuation
Шаблон:
```md
- Пункт списка, который продолжается
на следующей строке с отступом.
```
Ожидается:
- continuation-строка склеивается с пунктом через один пробел;
- лишние ведущие отступы continuation не попадают в итоговый текст;
- переносы не ломают маркер списка.
### P07 — Многострочная цитата
Шаблон:
```md
> Первая строка цитаты
> Вторая строка цитаты
> Третья строка цитаты
```
Ожидается:
- для продолжений корректно повторяется quote-префикс;
- сырой символ `>` из исходника не «просачивается» в середину текста.
### P08 — Пустая quoted-строка как разделитель
Шаблон:
```md
> Первый параграф цитаты
>
> Второй параграф цитаты
```
Ожидается:
- пустая quoted-строка разделяет два параграфа;
- рендер не объединяет их в один поток.
### P09 — Таблица в nowrap-режиме
Шаблон:
```md
| col1 | col2 | col3 |
|------|------|------|
| очень длинное значение | очень длинное значение | очень длинное значение |
```
Ожидается:
- строка таблицы не переносится по словам;
- работает горизонтальный сдвиг `Left/Right`;
- индикаторы `<`/`>` показывают скрытый контент.
### P10 — Fenced code-block
Шаблон:
~~~md
```text
**это не жирный**
_это не подчёркнутый_
```
~~~
Ожидается:
- внутри code-блока inline-разметка не применяется;
- текст отображается как литерал.
### P11 — Inline-стили на обычном тексте
Шаблон:
```md
Текст с **жирным**, *курсивом*, _подчёркнутым_, ~~зачёркнутым~~ и `code`.
```
Ожидается:
- все перечисленные стили рендерятся корректно;
- литеральные случаи вроде `2 * 3` не превращаются в стиль.
### P12 — Статус-бар и границы прокрутки
Вход:
- документ на несколько экранов.
Ожидается:
- корректные `L x-y / total` и `%`;
- `Home/End` приводят к ожидаемым позициям;
- при выходе за границы документа top-line корректно clamp-ится.
## Рекомендация по регрессии
После любых изменений в логике индексации/рендера прогонять минимум:
- `P04`, `P05`, `P06`, `P07`, `P09`, `P10`, `P11`, `P12`.
+44
View File
@@ -0,0 +1,44 @@
# Build mdview.exe — Markdown viewer for Sprinter.
#
# small memory mode: code in W1, data/stack/heap in W2 (32 KB total).
# W3 stays free for the file buffer (EMM-mapped).
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := mdview
MEMORY := small
include $(PROJ_ROOT)/app.mk
# ------------------------------------------------------------------
# Образ дискеты: только mdview.exe + README.MD (перекодированный
# из UTF-8 в CP866 — рабочую кодировку Sprinter).
#
# README.MD хранится в репозитории в UTF-8; iconv -c конвертирует
# его в CP866, отбрасывая символы без аналога в целевой кодировке.
# Результат кладётся в .disk_tmp/README.MD, чтобы make_disk.py
# использовал правильное имя файла на диске.
#
# iconv -c возвращает ненулевой код, если хоть один символ отброшен
# (даже с -c) — это ОЖИДАЕМО при потере символов без аналога в CP866,
# не ошибка конвертации; вывод при этом всё равно корректно записан.
# Поэтому код возврата iconv игнорируется (|| true).
# ------------------------------------------------------------------
DISK_TMP := .disk_tmp
README_DISK := $(DISK_TMP)/README.MD
$(DISK_TMP):
mkdir -p $@
$(README_DISK): README.MD | $(DISK_TMP)
iconv -c -f UTF-8 -t CP866 README.MD > $@ || true
floppy: $(EXAMPLE).exe $(README_DISK)
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(README_DISK)
@echo
@echo "Floppy ready: $(FLOPPY_IMG)"
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
clean:
rm -rf .sprinter-cc-* $(EXAMPLE).exe $(DISK_TMP)
.PHONY: all clean floppy run
+133
View File
@@ -0,0 +1,133 @@
# MDView — Просмотрщик Markdown для Sprinter
**MDView** — программа для просмотра документов в формате *Markdown* на компьютере
Sprinter (процессор Z80). Документ хранится в отдельном W3 окне и не занимает
основную RAM программы.
## Возможности
- Документы до **128 КБ** (8 страниц EMM по 16 КБ каждая)
- До **16 384** экранных строк в индексе
- Автоматический перенос слов по ширине экрана (80 столбцов)
- Горизонтальный сдвиг для широких строк (блоки кода, таблицы)
- Статус-бар: имя файла, диапазон строк, процент прокрутки
- Спиннер в строке состояния во время загрузки и индексации
- Поддержка «мягкого» склеивания строк в абзацах и цитатах
## Запуск
```
mdview [имя_файла.md]
```
Если имя файла не задано, загружается `README.MD`.
## Управление
```
Клавиша Действие
───────────── ────────────────────────────────────────
Up Down Прокрутка на одну строку вверх / вниз
PgUp PgDn Прокрутка на страницу (30 строк)
Home Начало документа
End Конец документа
Left Right Горизонтальный сдвиг (только nowrap-строки)
F1 Окно справки
F10 / Esc Выход из программы
```
## Синтаксис Markdown
### Заголовки
Поддерживаются уровни H1–H4. Уровни H5 и H6 отображаются как H4.
# Заголовок первого уровня
## Заголовок второго уровня
### Заголовок третьего уровня
#### Заголовок четвёртого уровня
### Текстовое форматирование
**Жирный текст** выделяется двойными звёздочками: `**текст**`
*Курсив* выделяется одиночными звёздочками `*текст*` или знаком подчёркивания `_текст_`
`Встроенный код` обозначается обратными кавычками
~~Зачёркнутый текст~~ — двойные тильды: `~~текст~~`
### Ненумерованный список
Маркеры `-`, `*` или `+`:
- Первый пункт списка
- Второй пункт списка
- Третий пункт с достаточно длинным текстом, который при необходимости
будет перенесён на следующую строку с сохранением отступа
### Нумерованный список
1. Первый элемент
2. Второй элемент
3. Третий элемент
### Цитата
> Блок цитаты начинается с символа `>`. Несколько последовательных
> строк одной цитаты склеиваются в единый абзац с автоматическим
> переносом слов.
### Блок кода (verbatim)
Блок кода заключается в тройные обратные кавычки. Внутри блока
текст отображается «как есть» без разбора Markdown:
```
#include <stdio.h>
#include <sprinter.h>
int main(void) {
puts("Hello, Sprinter!");
return 0;
}
```
### Горизонтальная линия
Три или более символов `---`, `***` или `___` на отдельной строке:
---
## Технические характеристики
- **Платформа:** Sprinter, процессор Z80 @ 21 МГц
- **Кодировка:** CP866 (DOS Cyrillic)
- **Максимальный размер файла:** 128 КБ
- **Максимальное число строк в индексе:** 16 384
- **Режим памяти:** small
- Код программы, cтек, данные, куча — окнa W1-W2 (32 КБ, адреса 0x40000xBFFF).
- Буфер файла — страницы EMM, отображаемые в W3 (0xC0000xFFFF)
## TODO
1. **Увеличение размера документов.** Снять лимит 128 КБ: Достаточно
разрешить работать с большим кол-вом страниц памяти, пока оттестированно
на работе с 8-мю страницами по 16Кб.
2. **Форматированные таблицы.** Разбирать строки вида `| ячейка | ячейка |`
с автоматическим выравниванием столбцов и отрисовкой разделительных
линий (строки `|---|---|`). На текущий момент таблицы отображаются
как обычные nowrap-строки без выравнивания.
3. **Поддержка кодировок CP1251 и UTF-8.** Автоопределение кодировки
по BOM, явное указание через аргумент командной строки (`--encoding cp1251`),
возможность переключения кодировки во время просмотра (`F8`).
Нужно прежде всего для документов на русском языке; CP866 кодировака уже поддерживается.
4. **Ускорение рендеринга.** Кэш строк экрана. Оптимизация цикла вывода
символов через BIOS WRCHAR (пакетный вывод, DMA).
---
*MDView v0.2 · (c) 2026 Петров А.Г.*
@@ -0,0 +1,103 @@
# mdview — модель документа и рендеринг (единый смешанный режим)
Дизайн-документ переработки `mdview.c` под новое ТЗ форматирования (`examples/mdview/todo2`). Описывает решение по хранению данных, разбор документа при загрузке, упрощение рендера и снятие лимита на число строк.
## 1. Контекст и главный вопрос
Мы отказываемся от двух режимов показа (Wrap/UnWrap-переключатель) и переходим к единому смешанному режиму: тип переноса задаётся типом блока (обычный текст/заголовки/списки/цитаты — Wrap; код/таблицы — UnWrap).
Главный вопрос: нужно ли хранить оригинальный байтовый контент файла, или при загрузке сразу преобразовать его в «готовый к показу» текст (склеить строки параграфов, убрать маркеры, развернуть отступы и т.п.)?
Ответ: **оригинал храним; отдельный «готовый» текстовый/ячеечный буфер не строим.** «Подготовка при загрузке» реализуется как построение компактного *индекса метаданных*, а не как преобразование *содержимого*.
## 2. Решение по архитектуре
### 2.1 Текущее состояние (база)
* Файл целиком лежит в EMM-страницах (до 8×16 КБ). В окно W3 (`0xC000`) в каждый момент замаплена ровно одна страница; доступ к байту — через `fb()`/`map_page()` (`mdview.c:198-213`).
* Индекс — это параллельные массивы по экранным сегментам: `line_offset[]` (смещение в файле) + битфлаги `cont/nowrap/blank/in_code`, 2-битный `line_kind[]`, `init_style[]` (`mdview.c:122-135`).
* `index_lines()` за один проход уже делает склейку параграфов обычного текста, перенос по словам на 80 колонок и перенос emphasis через soft/hard break (`mdview.c:692-911`).
* `render_line()` при каждой отрисовке заново читает байты из оригинала и заново парсит inline-разметку (`mdview.c:918-1156`).
Вывод: «готовим при загрузке» мы уже делаем — но готовим **индекс**, а не текст.
### 2.2 Почему не материализуем содержимое
* **Одно свободное окно W3.** Режим памяти `small`: код в W1, данные/стек/куча в W2, для банкуемых данных свободен только W3. Преобразование «оригинал → готовый буфер» требует одновременно держать замапленными исходную страницу (чтение) и страницу-приёмник (запись). При единственном окне это поток swap-ов на каждую границу. In-place преобразование тоже невозможно: склейка меняет длины, смещения «съезжают», параграф пересекает границу 16 КБ.
* **Удвоение памяти и срыв гарантии 128 КБ.** Оригинал может занимать все 8 страниц; готовой копии нужны свои страницы — гарантировать, что влезут обе, нельзя.
* **Готовая форма не обязательно меньше.** Снятие маркеров экономит байты, но добавляются отступы-продолжения у переносов списков/цитат. В лучшем случае ≈ размер оригинала, в худшем — больше. Ячеечная модель (символ+атрибут) — это ×2 (до 256 КБ), невозможно.
* **Покадровая стоимость и так мала.** За кадр рисуются только 30 видимых строк (~30×80 чтений). Единственный дорогой проход — `index_lines()` — неизбежен в любой архитектуре (нужен полный скан для `n_lines` и процента прокрутки).
### 2.3 Что реально оптимизировать
Не текст, а повторную работу `render_line()`:
* вызов `classify_line()` на каждый кадр (`mdview.c:984`);
* обратный проход к первому не-cont сегменту ради отступа продолжения (`mdview.c:944-970`).
Это снимается переносом результата классификации (`kind`, ширина отступа/контент-колонка) в сам индекс на этапе `index_lines()`.
## 3. Модель данных индекса
### 3.1 Запись сегмента
Единая запись на экранный сегмент полностью заменяет нынешние параллельные массивы (`line_offset`, `cont_flag`, `in_code`, `nowrap_flag`, `blank_flag`, `line_kind`, `init_style`); `index_lines()` переписывается с нуля под эту модель. Цель размера записи — 5–6 байт:
* `offset` — 3 байта (24-битное смещение в файле, покрывает 128 КБ).
* `flags` — 1 байт: биты `cont`, `nowrap`, `blank`, `in_code` + 2-битный `ckind` (тип продолжения: PLAIN/QUOTE/LIST/OTHER).
* `style` — 1 байт: стартовый стиль сегмента (`init_style`, теперь включая STRIKE) + при необходимости глубина вложенности.
* `indent` — 1 байт: предвычисленная контент-колонка/ширина префикса, чтобы рендер не вызывал `classify_line()` и не делал обратный проход.
### 3.2 Размещение и снятие лимита `MAX_LINES`
Проблема: `line_offset` сейчас `uint32_t[2048]` = 8 КБ, `init_style` = 2 КБ; суммарно статический индекс ~11.6 КБ near-памяти (W2). Рост лимита в near невозможен — W2 переполнится.
Решение:
* **Индекс храним в отдельном EMM-блоке** (свои страницы, помимо файловых), доступ — через тот же W3.
* **Near-кэш viewport**: перед отрисовкой кадра разово вычитываем записи для `VIEW_H+1` видимых сегментов в маленький near-массив (≈ `(VIEW_H+1)×6` ≈ 186 байт). Рендер работает по near-кэшу + читает только контент-страницы. Это устраняет per-byte thrashing между страницей индекса и страницей контента: переключений на кадр — единицы, а не тысячи.
* **Динамический размер**: число страниц под индекс выделяем пропорционально размеру файла (число сегментов ∈ размеру). `MAX_LINES` становится функцией от выделенных страниц индекса. Это прямо ложится на заметку v2 («чем больше банков под файл, тем больше буферы»).
* **Без регресса скорости при индексе в EMM**: чтобы вынос индекса в банки не замедлил сборку (W3 делится между чтением контента и записью индекса), записи копим в near-буфере батчами и сбрасываем в EMM-блок через `bank_write()` (он сам сохраняет/восстанавливает маппинг W3). Переключений окна на всю сборку — единицы, а не на каждый сегмент.
* Если индекс-страницы выделить не удалось — деградируем до текущего near-лимита и показываем явную диагностику обрезки (а не молчаливый обрыв в `emit_seg()``mdview.c:574`).
## 4. `index_lines()` — разбор по новому ТЗ
Единый проход по файлу строит сегменты. Деление на параграфы — по пустым строкам; несколько пустых строк подряд схлопываются в одну.
### 4.0 Скорость подготовки — главный приоритет
Требование: подготовка максимально быстрая (сейчас ~25 КБ готовятся 6–10 с). Две структурные причины медленности и их устранение:
* **Per-byte `fb(uint32_t)`.** Каждый доступ к байту пересчитывает страницу 32-битными `p >> 14` и `p & 0x3FFF` (`mdview.c:209-213`). На Z80 32-битная арифметика — это программные подпрограммы на каждый символ. Замена: **потоковый разбор** — мапим страницу один раз, идём по окну `char *`/16-битным индексом, страницу переключаем только на границе 16 КБ. 32-битным остаётся лишь сохраняемый в индекс `offset`.
* **Многократные пере-сканы.** На каждом `\n` внутри параграфа вызываются `is_fence_raw()`, `is_hr_raw()`, `classify_line()`, `is_line_blank()` (`mdview.c:800-809`) — каждая заново сканирует следующую строку, а `classify_line()` ещё и повторяет цикл детекции HR. Для параграфа из N строк — O(N×длина) лишней работы. Замена: **один проход** — каждую строку классифицируем ровно один раз в момент её начала, lookahead — минимальный (несколько первых байт).
* **32-битные сравнения.** Курсор скана — (страница:8 бит, смещение:16 бит); сравнение с концом — сначала по странице, потом 16-битно.
Ожидаемый эффект: подготовка — по сути один линейный проход с 16-битными операциями, кратное ускорение относительно текущего multi-pass + 32-bit. (`render_line()` может остаться на `fb()` — там только ~30×80 байт за кадр.)
### 4.1 Обычный текст (Wrap)
* **Soft break** (одиночный `\n`): склейка, следующая строка продолжается через пробел.
* **Wide break**: 2+ пробелов перед `\n` **или** символ `\` перед `\n` → принудительный перенос внутри параграфа, стиль сохраняется. (Текущий код ловит только 2 пробела — `mdview.c:813-818`; добавить ветку для `\`.)
* **Hard break** (пустая строка): новый параграф, отделяется ОДНОЙ пустой строкой независимо от числа пустых строк в оригинале.
* **Модификаторы** bold/italic/strike/code действуют через soft/wide break внутри параграфа и сбрасываются на границе параграфа. (Добавить STRIKE `~~…~~` — сейчас его нет в `INIT_STYLE_*`/`ATTR_*`.)
### 4.2 Заголовки (Wrap)
Один оригинальный абзац-строка; стартовый стиль по уровню. Во входе распознаём H1–H6, но H4/H5/H6 далее обрабатываются одинаково как H4 (сливаются в один стиль; `classify_line()` уже сворачивает `lvl>4``LK_H4`, `mdview.c:488`). Внутри допустимы bold/italic/code/strike. После заголовка всегда пустая строка.
### 4.3 Горизонтальный разделитель HR
Всегда одна строка, после неё всегда пустая строка (новый абзац).
### 4.4 Списки (Wrap) — НОВОЕ: многострочная склейка
* Пункт может занимать несколько оригинальных строк; soft break внутри пункта склеивается через пробел (как обычный текст). Сейчас списки эмитятся построчно (`mdview.c:762-772`) — переписать на paragraph-модель.
* Новая строка с префиксом пункта → новый пункт.
* Пустая строка завершает пункт. Следующая непустая НЕ-пункт строка не является продолжением.
* **Группировка**: если после ОДНОЙ пустой строки идёт снова пункт — это тот же список, пустая строка в показе подавляется (пункты идут вплотную). Только ДВЕ+ пустые строки между пунктами разрывают на разные списки (в показе — одна пустая строка между ними).
* Перенос продолжения пункта печатается с отступом до контент-колонки (для уровня 1 — 2 пробела).
* Незакрытые модификаторы НЕ переносятся на следующий пункт (каждый пункт — свой параграф).
* Вложенные списки поддерживаются (отступ растёт с ведущими пробелами).
* Отдельные стили: префикс маркера и текст списка.
Пример соответствует разделу «Списки» в `todo2` (строки 16).
### 4.5 Цитаты (Wrap)
* Отдельный параграф; перед текстом — префикс цитаты, перенесённые строки тоже предваряются префиксом.
* Многострочная склейка как у текста; пустая строка-цитата (`>`) показывается как пустая строка с префиксом.
* Вложенность (`> >` → двойной префикс). Отдельные стили: префикс и текст цитаты.
### 4.6 Блок кода ``` ``` ``` (UnWrap)
Весь блок одним стилем кода, без inline-модификаторов. После блока обязательна пустая строка. Строки не переносятся (truncate + горизонтальный скролл).
### 4.7 Таблицы (UnWrap)
Пока as-is, без переноса. Выравнивание столбцов — v2.
## 5. `render_line()` — упрощение
* Убрать ветку truncate-режима и `wrap_mode` (уже частично снято; `toggle_wrap()` — мёртвая заглушка `mdview.c:1286-1291`, удалить вместе с упоминаниями F2).
* Не вызывать `classify_line()` и не делать обратный проход: использовать `kind`/`indent`/`style` из индекса.
* Для cont-сегментов списков/цитат — печать отступа/префикса по `kind`+`indent` из записи сегмента.
* Inline-парсинг emphasis выполняется только в пределах видимого сегмента (дёшево); для кода/таблиц — отключён.
## 6. Горизонтальный скроллинг (UnWrap)
* Скроллится только UnWrap-текст (код/таблицы). Грануляция 8 символов (`HPAN_STEP`).
* Правый край: индикатор `>` своим стилем, если есть скрытый контент справа (есть — `mdview.c:1135-1155`).
* **Добавить** левый индикатор `<` в первой колонке, когда `viewport_x > 0`.
* **Границы скролла по факту**: текущий кламп жёстко до 240 (`mdview.c:1275`). Заменить на вычисление максимального переполнения среди UnWrap-строк в текущем viewport, чтобы вправо нельзя было уйти за самую длинную строку, а влево — до колонки 0.
## 7. Чеклист расхождений с текущим кодом
* [индекс] Перейти на запись-на-сегмент в EMM-банке + near-кэш viewport; снять `MAX_LINES=2048`.
* [скорость] Потоковый разбор: один проход, 16-битный курсор в окне (без per-byte `fb()` с 32-битной арифметикой), классификация строки один раз, минимальный lookahead; батч-флеш индекса.
* [текст] Wide break по символу `\`.
* [текст] Модификатор strikethrough `~~…~~` (+ стиль).
* [списки] Многострочная склейка пунктов и правило группировки по одной/двум пустым строкам.
* [цитаты] Многострочная склейка и повтор префикса (в т.ч. вложенные) на переносах.
* [заголовки] Читать H1–H6; H4/H5/H6 трактовать как H4 (частично уже есть — `mdview.c:488`).
* [скролл] Индикатор `<` и корректные границы по фактическому переполнению.
* [рендер] Снять per-кадровый `classify_line()` и обратный проход (данные — из индекса).
* [очистка] Удалить `toggle_wrap()` и упоминания F2 (`mdview.c:1286-1291`, `1311`).
## 8. Память: бюджет
* near (W2): текущий статический индекс ~11.6 КБ — у предела окна. После переноса `line_offset`/`init_style` в EMM в near остаётся near-кэш viewport (~0.2 КБ) + мелкие флаги → запас под стек/кучу растёт.
* EMM: файл до 8 страниц + индекс ~1–2 страницы (при 5–6 байт/сегмент и нескольких тысячах сегментов). Перед выделением проверять `mem_info()` на доступность страниц.
## 9. Заметки для v2
* **Прогрессивный показ**: отрисовать первую страницу (первые ~30 сегментов) ДО завершения полной подготовки; остальное доиндексировать дальше или по мере прокрутки. Процент и `End` показывать как «вычисляется», пока не готов полный `n_lines`. (Синергия с потоковым разбором §4.0: первый экран готов после разбора лишь нескольких КБ.)
* Разделители переноса Wrap: точка/запятая/`!`/`?`/дефис; правило «новая строка не начинается с разделителя», серия разделителей остаётся на первой строке.
* Таблицы: вычисление ширины столбцов и выравнивание.
* Подсветка синтаксиса внутри блоков кода.
* URL/Images и прочие типы строк.
@@ -0,0 +1,90 @@
# mdview — унифицированный рендеринг (Unified Wrap + Paragraph Model)
## Цель
Отказаться от двух режимов Wrap/Unwrap. Ввести единую модель отображения Markdown-документов, соответствующую стандарту CommonMark:
* Обычный текст: параграфы склеиваются по soft breaks (`\n` → пробел), перенос по словам на 80 колонок.
* Hard break (` \n`): принудительный перенос внутри параграфа.
* Paragraph break (`\n\n`): новый абзац (пустая строка между блоками).
* Fenced code block и таблицы (будущее): не переносятся, работают как truncate + горизонтальный скроллинг + индикатор `>`.
* Горизонтальный скроллинг активен только когда в текущем viewport есть nowrap-строки.
## Текущее состояние
* `mdview.c` использует `wrap_mode` (0 = truncate, 1 = wrap) и `toggle_wrap()` (F2).
* `index_lines()` делает два разных прохода: truncate (1 строка файла = 1 строка экрана) и wrap (сегментация по SCREEN_W).
* `render_line()` имеет ветвление по `wrap_mode` и `cont`.
* `line_offset[]`, `cont_flag[]`, `in_code[]`, `init_style[]` — существующие структуры.
## Предлагаемые изменения
### 1. Новые структуры данных
* Добавить `nowrap_flag[]` (битмап, аналогично `cont_flag[]`): строка не должна переноситься (code block, HR, таблица).
* Добавить `line_kind[]` (2 бита на строку, 512 байт для 2048 строк): хранит классификацию (PLAIN, H1-H4, HR, ULIST, OLIST, QUOTE, CODE, TABLE). Нужен для continuation-сегментов, чтобы render_line знал, какой префикс/отступ повторять.
* Убрать `wrap_mode` и `toggle_wrap()`.
* Убрать truncate-ветку из `index_lines()` и `render_line()`.
* `viewport_x` остаётся глобальным, но применяется только к `nowrap` строкам.
### 2. Новый `index_lines()` — paragraph scanner
Единый проход по файлу, без двух режимов.
#### 2.1 Сканирование «сущностей»
Walk по файлу от `p = 0` до `file_size`:
1. Если `fb(p) == '`' × 3 → Fenced code block. Все строки до закрывающего ` ``` ``nowrap_flag = 1`, `in_code = 1` (для body), `line_kind = CODE`. Длинные строки не wrap'аются.
2. Если строка начинается с `|...|` (будущее) → Table. `nowrap_flag = 1`, `line_kind = TABLE`.
3. Если строка — HR (`---`/`***`/`___`) → `nowrap_flag = 1`, `line_kind = HR`. 1 строка экрана.
4. Если строка — Header (`#...`), List (`- ` / `* ` / `+ ` / `N. ` / `N) `), Quote (`> `) → начало **Normal paragraph / block**. Обрабатывается как единый параграф до `\n\n` или EOF или code block.
5. Иначе — Normal paragraph (plain text).
#### 2.2 Normal paragraph — обработка
Внутри параграфа символы читаются как единый поток (не прерываясь на `\n`):
* `\n\n` → конец параграфа. Emit текущую сегментную строку (если есть). Добавить пустую строку (1 строка экрана с нулевой длиной, или offset указывающий на второй `\n`).
* ` \n` (два пробела или таб + пробел перед `\n`) → **Hard break**. Emit текущую сегментную строку. Начать новую сегментную строку с того же абзаца, **сохранив `line_style`**. `prev_ch = ' '`.
* Обычный `\n`**Soft break**. Считаем как 1 пробел: `visible col++`, `prev_ch = ' '`. Продолжаем текущую сегментную строку.
* Wrap at `SCREEN_W` — так же как сейчас, по `last_space`. При создании continuation-сегмента: `set_cont()`, `set_init_style_raw()` (carry emphasis), `set_line_kind()` (carry list/quote kind для отступа/префикса).
* Emphasis tracking (`line_style`) работает через весь параграф, через soft/hard breaks.
#### 2.3 Fenced code block — обработка
* Delimiter строка (` `): `line_kind = CODE`, `nowrap_flag = 1`, рендерится как пустая строка (или строка с ` ` — но сейчас пустая).
* Body строки: каждая физическая строка = 1 строка экрана. `nowrap_flag = 1`, `in_code = 1`, `line_kind = CODE`. Нет inline parsing, нет wrap.
* Если строка длиннее `SCREEN_W` — truncation (не wrap). `render_line()` рисует `>` в последней колонке, если есть content за пределами `SCREEN_W + viewport_x`.
#### 2.4 List continuation
* Первая строка list item: `line_kind = ULIST/OLIST`, `marker_visible_col` определяет отступ. Текст начинается после маркера.
* Wrap внутри list item: `set_line_kind()` = тот же `ULIST`/`OLIST`. `render_line()` для continuation-сегмента не рисует маркер, но добавляет отступ = `marker_visible_col` (т.е. количество пробелов до текста первой строки).
* Для вложенных списков отступ растёт, потому что `marker_visible_col` учитывает ведущие пробелы.
#### 2.5 Quote continuation
* Первая строка quote: `line_kind = QUOTE`, `marker_visible_col` = отступ + 2 (для `│ `).
* Wrap внутри quote: `set_line_kind()` = `QUOTE`. `render_line()` для continuation рисует префикс `│ ` (или пробелы, если символ не нужен на всех кроме первой строки).
### 3. Изменения в `render_line()`
* Убрать ветку `if (!wrap_mode)` (truncate mode больше не существует).
* Добавить `if (is_nowrap(line_idx))`: применяется `viewport_x`, отображается `>` в колонке 79 если контент выходит за пределы.
* Для `cont` сегментов (wrap continuation):
* Если `line_kind` == `QUOTE`: повторить префикс отступа + `│ ` (или только отступ, если решено показывать `│` только на первой строке).
* Если `line_kind` == `ULIST`/`OLIST`: повторить отступ = `marker_visible_col` (без маркера/цифр). Текст начинается с той же колонки, что и на первой строке item.
* Для plain text: просто продолжить текст без префиксов.
* Для `nowrap` строк: применять `viewport_x` как смещение. Текст сдвигается влево, скрытые символы не рисуются. Если за пределами видимой зоны есть ещё контент — `>` в колонке 79.
### 4. Горизонтальный скроллинг
* `scroll_h()` проверяет: есть ли в текущем viewport (top_line .. top_line+VIEW_H-1) хотя бы одна строка с `nowrap_flag == 1`. Если нет — return (no-op).
* Для `nowrap` строк `render_line()` использует `viewport_x` при отображении: символы с `cc < viewport_x` пропускаются, `cc >= viewport_x` рисуются.
* Для wrap-строк `viewport_x` игнорируется (эффективно = 0).
* При `Home`/`End`/`PgUp`/`PgDn`/`Up`/`Down``viewport_x` не сбрасывается (пользователь может скроллить по вертикали, оставаясь на горизонтальном смещении для code block).
### 5. Очистка и навигация
* Убрать `wrap_mode`, `toggle_wrap()`.
* Убрать F2 из меню и help.
* `scroll_up`/`scroll_down`/`scroll_h`/`clamp_top` — обновить без ссылок на `wrap_mode`.
* `calc_pct` и статусная строка — обновить, убрать ссылку на wrap/unwrap.
### 6. Порядок реализации (по файлу mdview.c)
1. **Структуры данных**: добавить `nowrap_flag[]`, `line_kind[]`, убрать `wrap_mode`.
2. **`index_lines()`**: полностью переписать в paragraph scanner. Это самый сложный и объёмный блок.
3. **`render_line()`**: убрать truncate, добавить nowrap/quote/list continuation, добавить `viewport_x` для nowrap.
4. **`scroll_h()`**: добавить проверку наличия nowrap в viewport.
5. **Очистка**: убрать toggle_wrap, обновить меню/help, обновить main() (убрать F2).
6. **Тестирование**: soft breaks, hard breaks, paragraph breaks, emphasis через soft breaks, fenced code blocks, list wrap с отступом, quote wrap с префиксом, горизонтальный скролл только для code.
## Orchestration
План реализуется одним агентом (последовательно в одном файле), параллелизм не требуется. Дочерние агенты не используются.
File diff suppressed because it is too large Load Diff
+180
View File
@@ -0,0 +1,180 @@
Новое требование к первоначальной подготовке документа.
Правила форматирования и показа -
1) Деление на параграфы. Параграфы разделяются пустыми строками.
2) Заголовки H1-H6 - отдельные параграфы (после них всегда пустые строки)
3) Разделители (---) - тоже - после них всегда пустые строки.
4) Таблица/код (через ```)/списки - это единый параграф.
Типы текста (строк) -
Обычный текст
Может в оригинале находиться на нескольких строках.
Строки могут разделяться
- обычный break - это когда на строке после текста идет перевод строки -
такой текст просто объединяется (в нашем случае если возможно то следующая строка продолжает
выводиться на той же строке что и предыдущая, только отделяется от нее уже не переводом строки
а через пробел.
- широкий break - это когда перед переводом строки есть два или более пробелов или перед переводом
строки находится символ обратный слэш '\' - в этом случае и когда мы выводим текст на экран следующая
строка начинается с новой строки.
- жесткий break - это когда две строки обычного текста разделяются двумя или более переводами
стрки (то есть между ними как минимум есть одна пустая строка) - в этом случае вторая строка
будет считаться новым параграфом и отделяться от предыдущей строки ОДНОЙ пустой строкой (вне
зависимости от того сколько пустых строк в оригинале.
Должно сохраняться действие модификатора bold/italic/strike начатое на одной строке на следующие
если они так же находятся в этом параграфе. Новый параграф (после пустой строки) теряет воздействие
незакрытого модификатора из предыдущего параграфа.
Тип переноса строк - Wrap.
Заголовки -
абзацы из одной оригинальной строки. Начальный стиль зависит от типа заголовка (в нем могут встречаться
модификаторы bold/italic/code/strike.
Тип переноса строк - Wrap.
Разделитель (horizontal rules) -
Всегда одна строка. После нее выводим пустую строку всегда (следующий текст - новый абзац)
Списки
как и в обычном тексте - один пункт списка может находиться в оригинале на нескольких строках.
если появляется новая строка с префиксом пункта списка - это означает что с этой строки начинается
новый пункт списка. если появляется пустая строка - это означает что следующая за пустой строкой
непустая строка уже не является продолжением пункта списка.
Если следующая за пустой строкой строка так же является пунктом списка то такая строка считается
продолжением текущего списка. Только если две строки списков разделены ДВУМЯ и более пустыми
строками то вторая строка с пунктом списка будет считаться началом нового списка -
Пример -
- строка 1
- строка 2
строка 3
- строка 4
- строка 5
- строка 6
Должно отображаться так -
- строка 1
- строка 2 строка 3
- строка 4
- строка 5
- строка 6
Тип переноса строк - Wrap.
Замечание - перенесенная строка продолжение пункта списка должна начинаться с отступа в
несколько пробелов (для списка первого уровня - два пробела) - то есть с той же позиции
что и начальный текст строки -
Пример -
- длинная строка которая не может поместиться и будет перенесена по слову 'будет'
Должно отображаться так -
- длинная строка которая не может поместиться и
будет перенесена по слову 'будет'
Незакрытые модификаторы типа (bold/etc.) не переносят свою модификацию не последующие пункты списка.
Действуют только в пределах одного пункта.
(то есть фактически - пункт списка - это отдельный параграф но следующий пункт списка (тоже отдельный
параграф) не отделяется от него пустой строкой.
Вложенные списки - поддерживаются.
Для отображения префиксов пунктов списка используется свой стиль. Так же для текста списков используется
отдельный (от обычного текста) стиль.
Блок Кода -
Весь блок кода отображается только одним стилем - стилем Кода. В нем не действуют модификаторы
bold/italic/etc (возможно в дальнейшей использование парсера языка для кода что бы отобразить
этот код с подсветкой синтаксиса этого языка, но сейчас весь блок рисуется только одним стилем)
После блока кода - обязательна пустая строка.
Тип переноса строк - UnWrap. - То есть строки НЕ ПЕРЕНОСЯТСЯ.
Quoted -
Отображается как отдельный параграф. Перед отображением текста отображается символ префикса Цитирования
Способы переноса текста аналогичны обычному тексту, за исключение того что перенесенные строки так же
предваряются префиксом Цитирования
Для отображения префиксов Цитирования используется свой стиль. Так же для текста цитирования используется
отдельный (от обычного текста) стиль.
Тип переноса строк - Wrap.
Пример форматирования -
> Первый параграф
>
> Второй параграф
> > Вложенный параграф
>
> Продолжение основной цитаты - длинная строка (переносится по слову 'строка')
Будет отображаться так -
| Первый параграф
|
| Второй параграф
| | Вложенный параграф
|
| Продолжение основной цитаты - длинная
| строка (переносится по слову 'строка')
Таблицы -
Пока отображаются as is.
Дальше возможно предусмотрим вариант вычисления ширины столбцов и форматирование
вывода что бы все ячейки столбца имели одинаковую ширину.
Тип переноса строк - UnWrap.
Типы переноса строк -
Wrap -
происходит перенос текста с одной строки на другую по разделителям пробелам (предусмотреть
во второй версии возможность использовать разделителем знаков точка, запятая, восклицательный
знак, вопросительный знак, дефис. Замечание - новая строка не может начинаться со знаков
разделителей - то есть если у нас идет многоточие (три точки) то нельзя что бы одна точка
была в конце первой строки а остальные две на другой - если идут несколько разделителей подряд
то они считаются как один и должны оставаться на первой строке).
UnWrap -
Текст НЕ ПЕРЕНОСИТСЯ.
Если строка не помещается на экране - то в конец строки на экране выводим символ-знак наличия
продолжения строки справа за краем экрана (свой стиль для этого символа).
Символ '>'.
Если есть хотя бы одна строка которая не помещается на экране - то разрешаем горизонтальный
скроллинг.
ЗАМЕЧАНИЕ - скроллируется только текст выводящийся в режиме UnWrap.
Если произведен скроллинг влево (строки UnWrap начинаются показываться не с первой позиции)
то на первой позиции отображем другой символ '<' сообщающий пользователю что есть текст
за левым краем экрана.
Скроллирование не бесконечно - если на экране нет строк UnWrap для которых есть скрытый текст
за правым краем экрана то скроллирование влево больше не возможно, и наоборот - если при
скроллировании вправо дошли до показа строк UnWrap с первой позиции то дальше скроллинг в этом
направлении так же невозможен. Скроллинг идет с грануляцией по 8 символов.
(сейчас UnWrap текст это только блоки кода и таблицы).
Проанализируй данную постановку задачи.
Что требуется -
Мы более не поддерживаем два режима показа - Wrap/UnWrap - только один (смешанный).
Потому вопрос - надо ли нам сохранять оригинальный контент считанного файла с диска ?
Или при подготовке данных считываемых с диска можно сразу преобразовывать его в формат
готовый для показа (объединять строки в одном параграфе и так далее) ?
Ограничения - не забывать о том что это все работает на компьютере с 8-ми битным процессором.
Потому очень экономно относимся к памяти и лишней работе процессора - все должно быть весьма
быстрым и компактным.
Требования для работы с файлами 128 Кб сохраняется.
Для версии 2 -
Подумай о возможности использования банков памяти не только для содержимого файла а например
для буферов типа line_offset/init_style и прочих (тогда их размер может так же быть динамическим
и чем больше банков памяти будет использовано для чтения файла тем больше станут размеры этих
буферов.
+49
View File
@@ -0,0 +1,49 @@
# Build mdview2.exe — Markdown viewer for Sprinter (render-cache version).
#
# Фаза 0: скаффолдинг — копия mdview.c как baseline, без изменений
# логики. План — docs/mdview2-plan.md.
#
# small memory mode: code in W1, data/stack/heap in W2 (32 KB total).
# W3 stays free for the file buffer (EMM-mapped).
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := mdview2
MEMORY := small
include $(PROJ_ROOT)/app.mk
# ------------------------------------------------------------------
# Образ дискеты: только mdview2.exe + README.MD (перекодированный
# из UTF-8 в CP866 — рабочую кодировку Sprinter).
#
# README.MD хранится в репозитории в UTF-8; iconv -c конвертирует
# его в CP866, отбрасывая символы без аналога в целевой кодировке.
# Результат кладётся в .disk_tmp/README.MD, чтобы make_disk.py
# использовал правильное имя файла на диске.
#
# iconv -c возвращает ненулевой код, если хоть один символ отброшен
# (даже с -c) — это ОЖИДАЕМО при потере символов без аналога в CP866,
# не ошибка конвертации; вывод при этом всё равно корректно записан.
# Поэтому код возврата iconv игнорируется (|| true).
# ------------------------------------------------------------------
DISK_TMP := .disk_tmp
README_DISK := $(DISK_TMP)/README.MD
$(DISK_TMP):
mkdir -p $@
$(README_DISK): README.MD | $(DISK_TMP)
iconv -c -f UTF-8 -t CP866 README.MD > $@ || true
# UTF8TEST.MD кладётся на диск КАК ЕСТЬ (в UTF-8, без перекодировки) —
# это тестовый вход для проверки UTF-8 рендеринга (Фаза 2 кодировок).
floppy: $(EXAMPLE).exe $(README_DISK)
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(README_DISK) UTF8TEST.MD
@echo
@echo "Floppy ready: $(FLOPPY_IMG)"
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
clean:
rm -rf .sprinter-cc-* $(EXAMPLE).exe $(DISK_TMP)
.PHONY: all clean floppy run
+799
View File
@@ -0,0 +1,799 @@
# MDView — Просмотрщик Markdown для Sprinter
**MDView** — программа для просмотра документов в формате *Markdown* на компьютере
Sprinter (процессор Z80). Документ хранится в отдельном W3 окне и не занимает
основную RAM программы.
## Возможности
- Документы до **128 КБ** (8 страниц EMM по 16 КБ каждая)
- До **16 384** экранных строк в индексе
- Автоматический перенос слов по ширине экрана (80 столбцов)
- Горизонтальный сдвиг для широких строк (блоки кода, таблицы)
- Статус-бар: имя файла, диапазон строк, процент прокрутки
- Спиннер в строке состояния во время загрузки и индексации
- Поддержка «мягкого» склеивания строк в абзацах и цитатах
## Запуск
```
mdview [имя_файла.md]
```
Если имя файла не задано, загружается `README.MD`.
## Управление
```
Клавиша Действие
───────────── ────────────────────────────────────────
Up Down Прокрутка на одну строку вверх / вниз
PgUp PgDn Прокрутка на страницу (30 строк)
Home Начало документа
End Конец документа
Left Right Горизонтальный сдвиг (только nowrap-строки)
F1 Окно справки
F8 Кодировка: CP866 → CP1251 → KOI8-R → UTF-8 → ...
F10 / Esc Выход из программы
```
Кодировка определяется автоматически при открытии (BOM + эвристика по
первым 4 КБ); `F8` переключает её вручную, если детекция ошиблась.
8-битные кодировки (CP866/CP1251/KOI8-R) переключаются мгновенно (ремап на
отрисовке). Второй набор индекс/кэша (например UTF-8) строится лениво — при
первом переключении в него (короткая пауза со спиннером), дальше мгновенно.
## Синтаксис Markdown
### Заголовки
Поддерживаются уровни H1–H4. Уровни H5 и H6 отображаются как H4.
# Заголовок первого уровня
## Заголовок второго уровня
### Заголовок третьего уровня
#### Заголовок четвёртого уровня
### Текстовое форматирование
**Жирный текст** выделяется двойными звёздочками: `**текст**`
*Курсив* выделяется одиночными звёздочками `*текст*` или знаком подчёркивания `_текст_`
`Встроенный код` обозначается обратными кавычками
~~Зачёркнутый текст~~ — двойные тильды: `~~текст~~`
### Ненумерованный список
Маркеры `-`, `*` или `+`:
- Первый пункт списка
- Второй пункт списка
- Третий пункт с достаточно длинным текстом, который при необходимости
будет перенесён на следующую строку с сохранением отступа
### Нумерованный список
1. Первый элемент
2. Второй элемент
3. Третий элемент
### Цитата
> Блок цитаты начинается с символа `>`. Несколько последовательных
> строк одной цитаты склеиваются в единый абзац с автоматическим
> переносом слов.
### Блок кода (verbatim)
Блок кода заключается в тройные обратные кавычки. Внутри блока
текст отображается «как есть» без разбора Markdown:
```
#include <stdio.h>
#include <sprinter.h>
int main(void) {
puts("Hello, Sprinter!");
return 0;
}
```
### Горизонтальная линия
Три или более символов `---`, `***` или `___` на отдельной строке:
---
## Технические характеристики
- **Платформа:** Sprinter, процессор Z80 @ 21 МГц
- **Кодировки:** CP866 / CP1251 / KOI8-R / UTF-8 (автоопределение, `F8`)
- **Максимальный размер файла:** 128 КБ
- **Максимальное число строк в индексе:** 16 384
- **Режим памяти:** small
- Код программы, cтек, данные, куча — окнa W1-W2 (32 КБ, адреса 0x40000xBFFF).
- Буфер файла — страницы EMM, отображаемые в W3 (0xC0000xFFFF)
## TODO
1. **Увеличение размера документов.** Снять лимит 128 КБ: Достаточно
разрешить работать с большим кол-вом страниц памяти, пока оттестированно
на работе с 8-мю страницами по 16Кб.
Сделано: форматированные таблицы с рамкой; поддержка кодировок
CP866 / CP1251 / KOI8-R / UTF-8 с автоопределением и переключением по `F8`
(второй набор строится лениво, по первому переключению).
4. **Ускорение рендеринга.** Кэш строк экрана. Оптимизация цикла вывода
символов через BIOS WRCHAR (пакетный вывод, DMA).
---
*MDView v0.2 · (c) 2026 Петров А.Г.*
---
# Sprinter C Compiler — v1.0
C toolchain for **Sprinter** — the Z80-based home computer by Peters Plus, running
ESTEX DSS. Host: macOS / Linux. Target: `.EXE` files in SprintEXE format.
Built on top of **SDCC 4.5** (vendored in `third_party/sdcc/`). This repository adds
everything Sprinter-specific: crt0, linker integration, libc wrappers over ESTEX,
banked-call trampolines, graphics & accelerator API, mouse driver wrappers, and the
`mkexe` utility for producing SprintEXE images.
## What you get
* **`bin/sprinter-cc`** — one-line driver: `sprinter-cc -o foo.exe foo.c`
* **Memory modes**: `tiny`, `small`, `big`, `huge`, `manual` — see below.
* **stdio + conio**: printf, puts, putchar, getchar, fopen/fread/..., cprintf, cputs, putch, textcolor/textbackground/textattr, gotoxy, kbhit/getch.
* **Graphics**: 320×256×256 and 640×256×16 modes, accelerator-backed primitives (hline / vline / rect / fill_rect / line via Bresenham, plus clear), bitmap-font text in both modes via BIOS character generator.
* **File I/O**: POSIX (`open`/`read`/`write`/`close`/`lseek`/`unlink`/`creat`), FILE\* streams (`fopen`/`fgets`/`fwrite`/...), directory listing (`ffirst`/`fnext`), `chdir`/`getcwd`/`mkdir`/`rmdir`, `stat`/`fstat`.
* **Memory**: 32 KB heap (W2-resident), banking-aware page allocator (`mem_alloc_pages`/`bank_read`/`bank_write`), explicit memory modes for sub-16 KB programs.
* **Mouse**: full Sprinter driver wrapper (14 functions including custom cursor bitmaps).
* **Environment**: `getenv`/`putenv`/`sysenv` over ESTEX `$46`.
* **Time**: `getdatetime`/`setdatetime` + POSIX `time`/`localtime`/`mktime`/`asctime`/`ctime`.
* **Misc**: `errno`/`strerror`/`perror`, `atexit`, `setjmp`/`longjmp`, `sleep`, full argv parsing in crt0.
## Quick start
```sh
git clone <this repo> sprinter-c
cd sprinter-c
make sdcc # one-time: fetch SDCC 4.5 binary (~25 MB)
make all # build mkexe + libsprinter.lib + 27 examples
make floppy # pack everything into mame/v306/IMG/mc.img
cd mame/v306 && ./run_mame.sh # boot Sprinter in MAME
```
Compile a single program:
```sh
cat > hello.c <<EOF
#include <stdio.h>
int main(void) { puts("Hello, Sprinter!"); return 0; }
EOF
bin/sprinter-cc -o hello.exe hello.c
```
That's it — `hello.exe` is now a valid SprintEXE you can `RUN HELLO` from the ESTEX shell.
## Memory modes
Sprinter's address space is four 16 KB windows (W0 / W1 / W2 / W3). DSS allocates
pages by program size — small programs get only one page. Pick a memory mode based
on what your program needs:
| Mode | Code lives in | Banking | Use when | Note |
|---|---|---|---|---|
| `tiny` (default) | W2 (0x8100+) | no | code+data < 14 KB | |
| `small` | W1-W2 (0x4100+) | no | code+data < 30 KB | |
| `big` | W2 + W1 banking | yes (W1) | tiny + extra code modules | |
| `huge` | W1-W2 + W3 banking | yes (W3) | small + extra code modules | |
| `manual` | user-specified | optional | special layouts | Not implemented |
```sh
sprinter-cc --memory small -o big.exe bigprog.c
sprinter-cc --memory huge -o app.exe main.c --bank 1=engine.c --bank 2=ai.c
```
Banked functions are declared with `__banked`:
```c
void engine_tick(int dt) __banked; // lives in BANK1, automatically swapped
```
## Examples (27 total)
| Example | What it demonstrates |
|---|---|
| `hello` | Hello world with stdio + conio Turbo-C-style colors |
| `argv` | argv parsing in crt0 |
| `cat` | File I/O — read & print TEST.TXT |
| `seek` | 32-bit lseek over a 100 KB file |
| `ls` | Directory listing via ffirst/fnext |
| `filetest` | FILE\* streams (fopen/fread/...) |
| `stattest` | `stat`/`fstat` on files and directories |
| `errno` | errno / strerror / perror |
| `mem_test` | Page allocator + bank\_read/bank\_write |
| `malloc` | Heap stress test (200+ allocations) |
| `banked` | Banked code in W3 (huge mode) |
| `bankedbg` | Banked code in W1 (big mode) |
| `banklocl` | Bank-local static data and BSS |
| `mouse` | Mouse driver in text mode |
| `gfx_mous` | Mouse with custom bitmap cursor in graphics mode |
| `gfx_demo` | 320×256×256 lines / rects / accelerator |
| `gfx_d16` | 640×256×16 same primitives |
| `gfx_text` | Bitmap-font text on graphics screen |
| `timedir` | Date/time + directory listing |
| `ptime` | POSIX time API |
| `openenv` | open() flags + environment vars |
| `conio` | conio API smoke test |
| `attrprob` | Probe Sprinter text attribute byte layout |
| `strtest` | string.h test (from SDCC's z80.lib) |
| `stdlib` | stdlib.h test (qsort / rand / strtol / etc.) |
| `assrtest` | assert() |
| `rt_test` | Runtime helpers (sleep, setjmp, atexit) |
## Headers
Standard:
* `<stdio.h>` — puts / printf / FILE\* + Sprinter-specific dec/hex helpers
* `<stdlib.h>` — atoi / atof / malloc / qsort / ... (from SDCC z80.lib)
* `<string.h>` / `<ctype.h>` / `<math.h>` — from SDCC z80.lib
* `<unistd.h>` — read / write / close / lseek / unlink
* `<fcntl.h>` — open / creat + O\_RDONLY / O\_CREAT / ...
* `<errno.h>` — errno + error names + strerror
* `<sys/stat.h>` — stat / fstat
* `<setjmp.h>` / `<assert.h>` — from SDCC
Sprinter-specific:
* `<conio.h>` — putch / cputs / cprintf, textcolor / textbackground / textattr, kbhit / getch, clrscr, gotoxy, wherex/y
* `<gfx.h>` — gfx_init/done, palette, putpixel, hline/vline/rect/fill_rect/line, text — for both 320 and 640 modes (gfx_\*16 variants)
* `<mouse.h>` — full 14-function driver wrapper + mouse_cursor_t with bitmap support
* `<dir.h>` — chdir / getcwd / mkdir / rmdir / ffirst / fnext / ffblk
* `<time.h>` — getdatetime / setdatetime + POSIX time / localtime / etc.
* `<sprinter.h>` — raw ports, ESTEX/BIOS function numbers, env API
* `<sprinter_exit.h>` — exit / \_exit / atexit
* `<sprinter_mem.h>` — mem\_alloc\_pages / mem\_free\_block / bank\_read / bank\_write
* `<sprinter_compat.h>` — Solid-C compatibility layer (aliases + BOOL/WORD/uint types)
## Toolchain commands
```sh
make all # build mkexe + lib + every example
make floppy # repack mame/v306/IMG/mc.img with all .exe files
make check # 17 mkexe unit-tests
make clean # remove all build artefacts
make sdcc # one-time: fetch SDCC 4.5 binary
```
### sprinter-cc options
```
sprinter-cc -o foo.exe foo.c [more.c ...] [options]
--memory MODE tiny | small | big | huge | manual (default: tiny)
--memory-manual SPEC explicit placement (CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3)
--stack-size N bytes reserved for the stack (default ~1278)
--crt0=TYPE default | minimal | banked | small
--bank N=FILE.c compile FILE.c into bank N (repeatable, max 15)
--debug enable runtime diagnostics (defines DEBUG_RT)
-I PATH extra include path
-L 0xADDR / -E / -S override load / entry / stack addresses
-Wl FLAG pass FLAG to sdldz80
--mkexe FLAG pass FLAG to mkexe (e.g. --mkexe -p --mkexe 0 for bank padding)
-v verbose
```
## Status
What works in v1.0:
* Compile / link / pack to SprintEXE — verified on all 27 examples
* Four memory modes (tiny / small / big / huge)
* Graphics (both modes) with accelerator
* Mouse (text + graphics cursor)
* File I/O, directories, environment, time
* All headers listed above
Deferred to v2.0 (see `docs/TODO.md`):
* **Turbo-C-style BGI graphics API** — `initgraph` / `setcolor` / `circle` /
`getimage` / `putimage` / etc. on top of our `gfx_*` primitives
* Remaining Solid-C compatibility gaps (Phase 2/3) — see `docs/solid_c_compatibility.md`
* Manual memory mode
* Rewrite FILE\* stream API (current implementation is very primitive and doesn't use buffers)
Deferred to v3.0:
* **IM2 interrupt handlers** — research complete (`docs/im2_isr_design.md`),
implementation scheduled for v3
* **Audio API** (AY-3-8910 + COVOX) — requires IM2
* **ISA-8 slot drivers** — requires IM2 (???)
## Documentation
* `docs/TODO.md` — roadmap and open work items
* `docs/solid_c_compatibility.md` — gap analysis vs Solid-C 2004
* `docs/im2_isr_design.md` — interrupt handler design (v2)
* `docs/converted/` — source documentation (ESTEX, BIOS, architecture)
converted to plain text for `grep`
* `docs/reference/`, `docs/samples/`, `docs/memory management/` — original
Russian docs and code samples from Peters Plus
## Repository layout
```
bin/sprinter-cc one-line compiler driver (bash)
toolchain/mkexe/ host-side tool: .ihx -> .exe SprintEXE
toolchain/check_banks.py post-link bank size enforcer
runtime/ crt0 variants (default, minimal, small, banked)
bank trampolines, heap, heap_top
libc/include/ headers
libc/io|stdio|mem|gfx/ C and asm sources for libsprinter.lib
lib/ Makefile that archives libsprinter.lib via sdar
examples/ 27 example programs
mame/v306/ MAME binary + Sprinter ROM/HDD images + floppy script
third_party/sdcc/ vendored SDCC 4.5 (fetched via `make sdcc`)
third_party/solid-c/ reference: original Sprinter native C (for compat target)
docs/ documentation
```
## License
This repository contains:
* Original code in `bin/`, `toolchain/`, `runtime/`, `libc/`, `lib/`, `examples/`
MIT-licensed.
* `third_party/sdcc/` — SDCC 4.5 under GPLv2 with linking exception
(see `third_party/sdcc/COPYING.txt`)
* `third_party/solid-c/` — original Sprinter Solid C, used only as a reference
## Credits
* **Sprinter / Peters Plus** — Иван Мак, Дмитрий Паринов and the original team
* **SDCC** — for the underlying Z80 compiler
* **MAME** — for the Sprinter Sp2000 emulation
---
For questions / patches: see CONTRIBUTING.md (TBD) or open an issue.
---
# TODO / Roadmap
Открытые задачи в порядке убывания приоритета. По мере появления реальных программ — приоритеты будут смещаться.
## Этап 5 — malloc / free + banking-aware page allocator ✅ ГОТОВО
- [x] SDCC's `malloc`/`free` + наш `runtime/heap.s` (полностью заменяет library heap.rel, 14000-байтный heap в окне 2)
- [x] `libc/mem/mem_alloc.c` — page allocator: `mem_alloc_pages`/`mem_free_block`/`mem_get_page`/`mem_info` через ESTEX `$3C/$3D/$3E` + BIOS `$C4`
- [x] `libc/mem/bank_io.c` — HOME-резидентные `bank_read`/`bank_write`/`bank_load_byte`/`bank_store_byte` со свопом W3 внутри
- [x] `examples/malloc_test/` — проверка SDCC's malloc (~210 64-байтных allocations через всю heap)
- [x] `examples/mem_test/` — проверка page allocator: 3 страницы, разные паттерны через bank_write, верификация через bank_read
## Этап 6 — argv parsing + sprinter-cc wrapper ✅ ГОТОВО
- [x] crt0 парсит ESTEX command-line из IX-prefix (inline asm в `runtime/crt0.s`)
- [x] Strip leading CP/M-style space (DSS quirk)
- [x] Передача `argc`/`argv` в main() через HL/DE (SDCC __sdcccall(1) ABI)
- [x] argv[0] = basename .EXE через ESTEX APPINFO ($47 subfn 2)
- [x] `runtime/crt0_minimal.s` — opt-out для очень маленьких программ
- [x] `runtime/crt0_banked.s` — теперь тоже парсит argv (parse_argv + get_progname скопированы из crt0.s; будет factored в argv.s когда возьмёмся за libsprinter.lib)
- [x] Bash-обёртка `bin/sprinter-cc`: `sprinter-cc -o foo.exe foo.c` одной строкой
- [x] Поддержка опций: `--memory`, `--memory-manual`, `--stack-size`, `--crt0=`, `--bank N=FILE.c`, `--debug`, `-I`, `-L`/`-E`/`-S`, `-Wl`, `--mkexe`
## Этап 8 — графика (320×256×256 + 640×256×16 + accel + bitmap font) ✅ ГОТОВО
- [x] **8a** Graphics core: `gfx_init`/`gfx_done`/`gfx_clear`/`gfx_putpixel`/`gfx_pal_load`/`gfx_pal_set` (libc/gfx/gfx_core.c). Палитра через BIOS PIC_SET_PAL ($A4). Verified 320×256×256.
- [x] **8b** Линии/прямоугольники/fill через accelerator (libc/gfx/gfx_lines.c): `gfx_hline`/`gfx_vline` через accel Fill (LD C,C / LD E,E + SMC block-size), `gfx_rect`/`gfx_fill_rect` с heuristic выбором ориентации (h/v bursts count), `gfx_line` с Bresenham для диагоналей. `gfx_clear` тоже переписан на column-major accel (~4× быстрее).
- [x] **8c** 640×256×16 mode (libc/gfx/gfx_16.c): `gfx_*16` API, HIGH nibble = LEFT pixel (документация misleading), per-row RMW для vline (один байт = 2 горизонтальных пикселя).
- [x] **8d** Bitmap font + gfx_text (libc/gfx/gfx_text.c): шрифт через BIOS WIN_GET_ZG ($B8), interleaved layout `font[row*256+char]`, `gfx_text`/`gfx_putchar` для 320 mode, `gfx_text16`/`gfx_putchar16` для 640 mode с pair-table lookup.
См. memory/sprinter_graphics.md, sprinter_accelerator.md, sprinter_graphics_16.md, sprinter_font_format.md.
Открытые мелочи (не блокируют):
- [ ] Шрифт-quad для 640: per-cell палитра (mode 0x82 разрешает 1 из 4 палитр per 16×8 cell) — через прямой доступ к area-описания экрана 0x0300..0x039F
## Auto-banking (см. `memory/banking_roadmap.md` для деталей)
Phase 1 — file-level bin-packing — реализовывать когда проект перерастёт ~30 KB кода.
- [ ] `toolchain/auto_bank.py`:
- Парсит размеры из `.rel`-файлов (или из .map после dry-run link'а)
- First-fit-decreasing bin-packing
- Уважает `#pragma codeseg BANKn` как manual override
- Перелинковывает с новыми `-Wl-b_BANKn=...` параметрами
- Печатает план распределения
Phase 2-5: incremental rebalance, declarative `banks.toml`, function-level, call-graph-aware. Только если/когда понадобится.
## Bank-local static data (mutable data в том же банке что и код) — ✅ ГОТОВО
- [x] Пример `examples/bank_local_data/` — функция в BANK1 со своим writable BSS array + const table + malloc-тест
- [x] `mkexe -p 0` для нулевого padding банков (BSS-storage обнуляется при загрузке)
- [x] Канонический рецепт: `--codeseg BANK1 --constseg BANK1 --dataseg BANK1` для bank1.c + `-Wl-b_BANK1=0x1C000` для линковки. **`--dataseg BANK1` РАБОТАЕТ** — раньше казалось обратное из-за trampoline bug который маскировал результат.
- [x] **Критичный фикс trampoline'a в runtime/bank.s** — старый `pop af; out (n), a` клобберил A → все banked-функции возвращающие uint8_t тихо возвращали мусор. Новый `pop bc; out (c), b` сохраняет A.
- [x] **malloc из banked-функции работает прозрачно** — heap живёт в W2 (HOME), W2 никогда не свапается trampoline'ом, pointer валиден из любого контекста. См. memory/bank_local_data_pattern.md.
- [x] Документация в memory: `memory/bank_local_data_pattern.md` (полный рецепт + malloc + nuances), `memory/sdcc_banking.md` (trampoline fix)
- [ ] Опционально — расширить `check_banks.py` чтобы показывать разбивку size = code + const + bss per bank (cosmetic)
Зачем: для модулей с большим private state (level loader, audio engine, scene data). Экономит W2 heap для динамики, а статика остаётся в бэке.
## Подсказки из solid-c (нативный Sprinter C — `third_party/solid-c/`)
После анализа solid-c'овской libc (см. `memory/solid_c_findings.md`) выявлены готовые паттерны для следующих недостающих функций. Приоритет от **высокого** к низкому:
### High-priority gaps (легко портировать, большая польза)
- [x] **`errno` + `strerror`/`perror`** — табличка 32 ошибок (libc/io/errno.c)
- [x] **Расширенный `open()`** для O_CREAT/O_TRUNC/O_APPEND/O_EXCL state machine
- [x] **`atexit`** — 8-callback LIFO + `exit()` + `_exit()` (libc/io/atexit.c)
- [x] **`setjmp`/`longjmp`** — 6-байт jmp_buf={sp,ix,pc} (libc/io/setjmp.c)
- [x] **`sleep(seconds)`** — 50Hz halt-loop (libc/io/sleep.c)
- [x] **ESTEX ENV API** ($46, getenv/putenv) — libc/io/env.c. Учли doc-bug: реально A=0 это NOT FOUND
### Medium-priority (нужно для shell-like утилит)
- [ ] **Mouse driver**`rst $30h`, 17 функций. **Сначала тест что работает в MAME**.
- [x] **`ffirst`/`fnext` + ffblk_t struct** для directory listing — реализовано, demo: ls.exe
- [x] **`getdatetime`/`setdatetime`** через ESTEX $21/$22 — libc/io/time.c, demo: time_dir_test
- [x] **`chdir`/`getcwd`/`mkdir`/`rmdir`** — wrappers для ESTEX $1B-$1E — libc/io/fsdir.c
- [x] **conio: `kbhit`/`getch`/`getche`/`cputs`/`clrscr`/`gotoxy`** — реализовано
- [x] **conio extras**: `wherex`/`wherey` ($53), `wrchar`/`rdchar` ($58/$57), `textmode_get/set` ($50/$51), `clrscr_attr` ($56) + COLOR macros
### Low-priority — ✅ FILE* stack ГОТОВО
- [x] **Минимальный unbuffered FILE\*** — `libc/stdio/file.c` + `libc/include/stdio.h`. fopen/fclose/fputs/fgets/fread/fwrite/fseek/ftell/rewind/feof/ferror/clearerr/fflush + stdin/stdout/stderr как pseudo-streams. См. `memory/file_star_design.md` и `examples/filetest`.
- [ ] fprintf / fscanf — нужна printf-через-callback machinery. Пока пользователь может `sprintf(buf, ...) + fputs(buf, fp)`.
- [ ] Опциональный buffered mode (setvbuf, line/block buffering) — если когда-то понадобится.
### POSIX time API — ✅ ГОТОВО
- [x] `libc/io/posix_time.c` — time/localtime/gmtime/mktime/asctime/ctime поверх getdatetime. SDCC's time.rel избегаем (нельзя override _RtcRead). См. `examples/ptime`.
### sys/stat — ✅ ГОТОВО
- [x] `libc/io/stat.c` — POSIX stat/fstat. Гибрид open+fstat для файлов, ffirst+iter для папок (включая "."/".."). См. `examples/stattest` и `memory/estex_ffirst_dotdot.md`.
### assert — ✅ ГОТОВО (используем SDCC's __assert через fallback include path)
## libc/stdlib — ✅ не нужно делать (см. memory/sdcc_stdlib_works.md)
Проверено через `examples/stdlib_test/`: SDCC z80.lib содержит работающие реализации:
- `atoi/atol/atof, strtol/strtoul, rand/srand, qsort/bsearch, abs/labs, div/ldiv`
- Полный `<string.h>` (memchr/cmp/set/cpy, strcat/cmp/cpy/len/chr/spn/etc.)
- `<ctype.h>` (toupper/tolower)
- `<math.h>` (sinf/cosf/sqrtf/etc.)
Линкер автоматически тянет из z80.lib когда нужно. **НЕ переписывать**.
Наши Sprinter-specific обязательные модули остаются: atexit, env, errno, setjmp, putchar/puts/getchar, conio, fsdir, time, mouse, open/read/lseek/close.
## Build-system: libsprinter.lib + sprinter-cc — ✅ ГОТОВО
- [x] `lib/Makefile` — собирает каждый libc/*.c в `.rel`, архивирует через sdar в `lib/sprinter.lib`
- [x] Включает runtime/bank.s и runtime/heap.s (auto-pulled при __banked/malloc)
- [x] `bin/sprinter-cc` — bash-wrapper: `sprinter-cc -o foo.exe foo.c` одной строкой
- [x] Поддержка опций `--crt0=default|minimal|banked`, `--bank N=FILE.c`, `-I`, `-L`/`-E`/`-S`, `-Wl`, `--mkexe`
- [x] `examples/hello_sccc/` — демо: `hello.c` собирается за один shell-вызов, размер совпадает с ручным Makefile (925 байт)
- [x] Split `putchar.c``putchar.c` + `puts.c` для per-function granularity (puts override SDCC's z80.lib version)
- [x] Включён в `make all` (зависимость `lib` перед `examples`)
Возможные улучшения (опционально):
- [ ] Мигрировать остальные examples на sprinter-cc вместо ручных Makefile (косметика)
- [ ] Дальнейшая декомпозиция libc/*.c per-function (но текущая granularity уже даёт нужный размер — линкер пакетует .rel целиком, и для большинства файлов это одна функция)
## Этап 9 — memory modes для sprinter-cc
DSS выделяет страницы памяти по размеру приложения: < 16 KB → одна страница, в остальные окна подключается «страница #FF» (read=0xFF, write игнорится). Из-за этого CODE-в-W1 + DATA-в-W2 для маленькой программы молча ломается. См. [memory/sprinter_memory_modes.md](../../.claude/projects/-Volumes-SAM8-Projects-DIY-Z80-Sprinter-C-Compiler/memory/sprinter_memory_modes.md).
- [x] **`tiny`**: всё (CODE+DATA+стек) в W2. Default. Verified hello/argv/conio/malloc/file/etc.
- [x] **`--memory MODE` флаг в sprinter-cc**: parser + per-mode дефолты CODE_LOC/DATA_LOC, override через явные `--code-loc`/`--data-loc`. tiny работает; small/big/huge компилируются с warning'ом (runtime не готов). Реализовано 2026-05-30.
- [x] **`--memory-manual SPEC`**: парсит `CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3`. Реализовано 2026-05-30.
- [x] **`small` runtime**: `runtime/crt0_small.s` использует ESTEX `$3D GETMEM` + `$3A SETWIN2` чтобы выделить и замапить W2-страницу ДО gsinit. **НЕ** BIOS `$C4` — стек на этом этапе в W1 (boot_stack в HOME), а BIOS требует стек в W2. После маппинга SP переключается на 0xBFFE, дальше стандартный flow. Реализовано 2026-05-30, verified hello.exe.
- [x] **`small` auto-detect для >16 KB программ**: `crt0_small.s` читает порт `0xC2` (текущая страница в W2 — не `0xA2`! это W1). Если 0xFF — выделяет page; иначе DSS уже сделала это (программа сама вылезла в W2). Один crt0 покрывает 0..30 KB. mkexe также разрешает HOME span W1+W2 (0x4000..0xBFFF). Verified hello: small (5 KB файл, SETWIN2 path) + 32 KB файл (auto-skip). Реализовано 2026-05-30.
- [x] **`big` runtime** (tiny + banked code в W1): параметризовали `crt0_banked.s` + `bank.s` через `.ifdef BANK_W1` — другой banking port (0xA2 vs 0xE2), другой load-addr (0x4000 vs 0xC000). sprinter-cc prepend'ит `BANK_W1 = 1` при `--memory big`, передаёт `mkexe -B 0x4000`. Пример `examples/banked_big/`. Реализовано 2026-05-30.
- [x] **`huge` runtime** (small + banked code в W3): merge W2-detect логики из `crt0_small.s` в `crt0_banked.s`. Существующий пример `examples/banked/` теперь использует MEMORY=huge. Реализовано 2026-05-30.
- [x] **`--debug` флаг**: prepend `DEBUG_RT = 1` в crt0 + `-DDEBUG_RT` в sdcc. Открывает symbol `_w2_self_allocated` (uint8_t) — runtime diagnostic кто аллоцировал W2. Реализовано 2026-05-30.
### Дизайн-решения по libc и crt0
**Одна `sprinter.lib`** работает для всех memory mode — `.rel`-члены relocatable, SDLD делает dead-code elimination per-member (без графики не подтягивает `gfx_core.rel` и т.д.). Verified hello vs malloc_test через map-файлы.
**`gfx.lib` отдельно — НЕ нужен**: dead-code elimination уже работает.
**`libc_banked` (libc в bank вместо HOME)** — идея на потом, когда HOME (16 KB) забит user-кодом + libc в `huge` mode. Реализуется через `--codeseg BANK0` при компиляции libc; trade-off: trampoline ~30 циклов на каждый libc-вызов. Триггер: реальная программа упрётся в HOME budget.
**HW-зависимые модули — `sprinter_home.lib` отдельно.** Часть libc физически не может быть забанкована в W3, потому что она РАБОТАЕТ с W3:
- `gfx_*` — пишет в видеопамять `0xC000+` после swap W3 на video page
- `bank_io` (mem_alloc_pages/bank_read/bank_write) — swap'ит W3 через `OUT (0xE2)`
- Будущие ISR — прерывание может прийти когда W3 на чём угодно
В huge mode эти модули ДОЛЖНЫ остаться в HOME (W1). Когда будем делать `libc_banked`, **одновременно** выделяем `sprinter_home.lib` (HOME-only) из `sprinter.lib` (bankable). Финальная схема:
```
sprinter_home.lib HOME-only: gfx, bank_io, ISR shims
sprinter.lib bankable: printf, malloc, string, conio, stdio, env, ...
sprinter_banked.lib тот же sprinter.lib но --codeseg BANK0 (для huge)
```
Триггер: реализация `--memory huge` runtime.
**crt0 — по одному на mode:**
- `crt0.s` — текущий, для **tiny/big**: SP=0xBFFE, парсит argv (W2-ресурс уже выделен DSS).
- `crt0_minimal.s` — текущий, для tiny без argv.
- `crt0_small.s`**новый, step 3**: для **small/huge**, аллоцирует W2 через `mem_alloc_pages` ДО gsinit, маппит в порт `0xA2`, потом стандартный flow.
- `crt0_banked.s` — текущий, для **big**: trampoline-таблица для W3 банков, CODE в W2.
- `crt0_banked_small.s`**новый**: huge = small (W2-alloc) + banked (W3 trampolines).
sprinter-cc подбирает crt0 по `--memory` mode (сейчас `--crt0=` это override).
- [x] **Настраиваемый размер стека**: флаг `sprinter-cc --stack-size BYTES`. Wrapper генерирует `heap_top.s` с `___sdcc_heap_end = stack_top + 1 - stack_size`, отдельный .rel линкуется per-program. Default ≈1278 байт (heap_top=0xBB00) из `runtime/heap_top.s`. Реализовано 2026-05-30.
Интерфейс: `sprinter-cc --memory [tiny|small|big|huge|manual] [--memory-manual SPEC] [--stack-size N] foo.c`. `--memory-manual` имеет смысл только с `--memory manual`.
## Known issues / quirks
- **ESTEX $46 ENV API**: ✅ работает. Док-ция в `DiskSyscalls.txt v1.6` ошибочно описывает return-status — A=0 это NOT FOUND, не FOUND. Зафиксировано в `memory/sprinter_platform.md`.
## ОБЯЗАТЕЛЬНЫЕ ЗАДАЧИ ДЛЯ V2 (после релиза v1)
### Turbo-C-style graphics API (BGI-like) — **MUST для v2**
Расширить наш `gfx_*` API до уровня **Turbo-C `<graphics.h>`** (BGI) для MS-DOS.
Программисты привыкшие к Turbo-C должны переносить графический код 1-в-1.
**Что должно быть** (на основе Borland BGI):
Setup/teardown:
- `initgraph()` / `closegraph()`у нас сейчас `gfx_init`/`gfx_done`, добавить alias
- `getmaxx()` / `getmaxy()` — макрос на GFX_WIDTH-1 / GFX_HEIGHT-1
- `cleardevice()` — alias to gfx_clear
- `getgraphmode()` / `setgraphmode()`у нас get_videomode/set_videomode
Color/palette:
- `setcolor(c)`, `getcolor()` — current draw color
- `setbkcolor(c)`, `getbkcolor()` — background color
- `setpalette(idx, c)` — палитра entry
- `getpalette(&info)` — read all palette
Primitives (мы уже имеем эквиваленты — добавить BGI-имена как aliases):
- `putpixel(x, y, c)` — есть как gfx_putpixel
- `getpixel(x, y)` — нужно реализовать (RMW обратное — IN)
- `moveto(x, y)`, `lineto(x, y)`, `linerel(dx, dy)` — current point + line drawing
- `line(x1, y1, x2, y2)` — есть как gfx_line
- `rectangle(x1, y1, x2, y2)` — есть как gfx_rect (но другой API: x1,y1,x2,y2 vs x,y,w,h!)
- `bar(x1, y1, x2, y2)` — есть как gfx_fill_rect
- `bar3d(x1, y1, x2, y2, depth, topflag)` — новое: rect + 3d edges
- `circle(x, y, r)`, `arc(...)`, `ellipse(...)`, `pieslice(...)` — новые primitives
- `fillpoly()`, `drawpoly()` — полигоны
- `floodfill(x, y, border_color)` — заливка
Text on graphics screen:
- `outtext(s)` / `outtextxy(x, y, s)` — есть как gfx_text (alias)
- `settextstyle(font, dir, size)` — multiple bitmap fonts
- `gettextsettings(&info)`
- `textwidth(s)` / `textheight(s)` — measure
Image manipulation:
- `imagesize(x1, y1, x2, y2)` — bytes needed for getimage
- `getimage(x1, y1, x2, y2, buf)` — save rect to buffer
- `putimage(x, y, buf, op)` — paste back with COPY_PUT/XOR_PUT/AND_PUT/OR_PUT/NOT_PUT
Clipping/viewport:
- `setviewport(x1, y1, x2, y2, clip)` — drawing clip rect
- `getviewsettings(&info)`
- `clearviewport()`
- `setactivepage(p)` / `setvisualpage(p)` — двойная буферизация (Sprinter имеет 2 screen)
Line style:
- `setlinestyle(style, pattern, thickness)` — SOLID_LINE / DOTTED_LINE / etc.
- `getlinesettings(&info)`
**Acceptance:** перенос типичной Turbo-C BGI программы (рисующей с использованием
moveto/lineto/circle/bar/setcolor) должен работать без существенных правок.
**Notes:**
- BGI fonts (TRIPLEX/SANS_SERIF/GOTHIC) — у нас один BIOS font, остальные нужно
добавить (как bitmap data в lib)
- imagesize/getimage/putimage — самые востребованные для game/animation
- Active/visual page (двойная буферизация) — Sprinter поддерживает 2 graphics pages,
нужен API switching
См. также `examples/` Turbo C 2.x BGIDEMO как reference что нужно.
### IM2 Interrupt Handlers — **MUST для v2**
User-задаваемые ISR через Z80 IM 2 mode. Нужны для:
- Timer ticks (50 Hz frame counter, плавная анимация)
- Music playback (AY, COVOX)
- Real-time games (input + game logic + render в interrupt-driven)
- Async keyboard / mouse handling
**Status:** ОТЛОЖЕНО до v2. Полный research + design в `docs/im2_isr_design.md`.
**Решение по архитектуре:** реализовать как отдельный memory mode `--memory im2`
(вместо того чтобы лезть во все существующие crt0). Detail'и в design-doc.
**Резюме research'а** (полный текст в `docs/im2_isr_design.md`):
- Vector 0xFF — frame + keyboard + CBL. Disambiguation по портам 0x19 / 0xFE
- Mouse hardware-IRQ не приходит (на текущей плате)
- Vector table / ISR / stack ОБЯЗАНЫ быть в W2 (0x8000..0xBFFF)
- DSS имеет свой IM 2 handler — нужно chain'иться (иначе клавиатура / SYSTIME ломаются)
### Прочие крупные пункты для v2
- [ ] **FILE API rewrite — buffered streams** — текущая реализация в
`libc/stdio/file.c` это provisional unbuffered shim (каждый fputc/fgetc
= один read/write syscall). Нужна полноценная buffered семантика
как в Solid-C:
```c
typedef struct {
uint flags; // +0..1 file status flags
int level; // +2..3 empty/fill level of buffer
char *curp; // +4..5 current active pointer
int fd; // +6..7 underlying low-level fd
char *buffer; // +8..9 data transfer buffer
char hold; // +10 ungetc byte if no buffer
short token; // +11..12 reserved
char dummy; // +13 reserved
} FILE;
```
stdin/stdout/stderr — fd-маркеры `0 / -1 / -2`. Отрицательные для
stdout/stderr выбраны намеренно: ESTEX OPEN может вернуть positive
small fd (1, 2, …) для обычного файла → если бы stdout=1, реальный
fd=1 сталкивался бы с идентификатором. fd=0 для stdin безопасно
(ESTEX 0 не возвращает). Сами fd не передаются в syscall'ы —
диспетчеризация по флагам `_F_CONIN/_F_CONOUT`.
Принтер-потоки (stdaux/stdprn) НЕ реализуем — Sprinter принтерной
API не имеет.
Альтернатива — взять реализацию из third_party/solid-c (sources в
`SRC/CLIB/`); там есть готовый buffered FILE + fopen/fread/fwrite/
fseek/setvbuf и т.д. Адаптировать к нашим open/read/write/lseek.
При rewrite заодно решить deferred issues stdio-review:
- `fwrite` short-write должен ставить `_F_ERROR`
- `fgets(buf, 1, fp)` — стандарт говорит "empty string", мы вернули NULL
- `mode_to_flags` — break-out на '+' (cosmetic)
- [ ] **Audio API** — AY-3-8910 + COVOX через прерывания (требует IM2)
- [ ] **ISA-8 slot support** — ZX-Bus карты (sound, network, etc.) — требует IM2 + чтения portов
## Прочие задачи (v1 backlog, не блокирующие)
- [x] **#9: text I/O split (Turbo-C style)** — stdio (puts/printf/putchar) теперь fast no-attr через PCHARS/PUTCHAR. conio (cputs/cprintf/putch) применяет attr через textcolor/textbackground/textattr. KEEP_EXIST_ATTR → conio fallback на fast path. Verified в hello.exe. См. `memory/text_output_api_split.md`. Реализовано 2026-05-31.
- [x] **Mouse API полный** (резидентный driver, RST 30h) — все 14 функций обёрнуты (init/show/hide/refresh/read/goto/bounds/text_cursor/load_cursor/get_cursor/get/set_sensitivity/video_mode_changed). См. `memory/mouse_api.md`. Verified в MAME 2026-05-31. Sensitivity = divider (меньше = быстрее).
- [ ] Interrupt handlers — IM 2 vector table в HOME для user ISR'ов
- [ ] Поддержка `restore SP on EXIT` (паттерн из z88dk +pps) — проверить нужно ли
- [ ] CI: автоматически запускать MAME с `-aviwrite` для screenshot-сравнения, чтобы тесты примеров проходили без человека
## Идеи на потом
- Поддержка `<setjmp.h>` (есть в SDCC stdlib — нужно протестировать что наш crt0 совместим)
- `<time.h>` через ESTEX SYSTIME (`$21`) и CMOS BIOS-функции
- ZX Spectrum-совместимый режим как отдельный target (для портирования спектрумовских программ)
- Поддержка ZX-Bus карт (sound, network, etc.) — нужны драйверы
- Profile-guided optimization tools (hot/cold detection) для крупных программ
## Linker duplicate-symbol warnings (благоприятные, отфильтрованы)
Когда мы сознательно overrides'им SDCC z80.lib функции собственной версией в `sprinter.lib`, `sdldz80` пишет `?ASlink-Warning-Definition of public symbol '...' found more than once`. Линкер берёт первое найденное определение (наше), поэтому поведение корректное — warning только noise.
Текущие overrides:
- `_puts` — наша версия через PCHARS+\r\n vs SDCC posix puts
- `___sdcc_heap` — наш heap в W2 vs SDCC's стандартный
- `_asctime`, `_localtime` (и возможно другие из time) — наш `posix_time.c` через ESTEX SYSTIME vs SDCC's `time.rel` который зависит от `_RtcRead`
**Текущее решение:** `bin/sprinter-cc` отфильтровывает warning-блок (warning + 2 follow-up `Library:` строки) из вывода `sdcc`. Через `-v` (verbose) всё показывается. Реализовано через awk-pipe.
**Возможные улучшения:**
- Перейти на explicit `--nostdlib` + ручной список нужных модулей из z80.lib (string, math, stdlib без override'нутых) — убрать ИСТОЧНИК warning'ов, не маскировать
- Или: переименовать наши `_puts` → `_puts_sprinter` + alias через linker flag (не уверен что SDCC поддерживает)
- Или: оставить как сейчас (рабочее и benign) — приоритет низкий
## TODO: проверить на реальном железе
- [ ] **Port_Y banking trick** (`docs/part2/SprinterGraphics programming.txt`):
доку утверждает что после `OUT (0x89), Y` адреса 0xC000+0x400*N в окне W3
маппятся на строки Y..Y+15 (одно программирование → 16 строк).
Empirical 2026-06-01 в MAME 0.283 этот trick **не работает** — пиксели
по адресам выше 0xC000+row_width уходят в невидимую область. Канонический
`docs/samples/plasma2.asm` тоже не использует banking, переустанавливает
Port_Y per row.
План:
1. Получить доступ к реальному Sprinter
2. Запустить тест dual-write (`_gfx_putpixel_raw` + второй write в `0xD000+x`)
3. Если на железе видны двойные линии → бага MAME, открыть issue с
минимальным репро
4. Если на железе тоже одна линия → документ неверный, удалить упоминание
из доки и просто оставить текущую реализацию (Port_Y per pixel)
5. Если banking работает на железе → внедрить кэширование Port_Y в
`_gfx_putpixel_raw` (sentinel out-of-range, см. memory/gfx_port_y_banking.md)
Связанный выигрыш для Bresenham (60-pixel диагональ) — около 8× меньше
OUT (0x89) операций, для `gfx_fill_rect 320x256` — 16× меньше. Не блокирует
release v1.
## GFX: расширения по `docs/part2/accelerator_doc.txt`
После прочтения детального accelerator doc выявлены незакрытые направления.
Сейчас в коде используется только горизонтальный/вертикальный Fill mode.
### Quick wins для текущих primitives
- [ ] **Заменить SMC на `LD A, (var)` для block-size**. Документ явно
разрешает `LD A, (HL)`, `LD A, (BC)`, `LD A, (DE)` (но не `LD A, r`).
Это уберёт SMC complexity в `gfx_lines.c:hfill_chunk/vfill_chunk` и
`gfx_16.c:g16_hfill_chunk`. Запрещено только register-to-register.
- [ ] **Кэширование block-size**. Документ показывает что accel запоминает
block size между bursts (см. `Horizontal_Line_Fill`: устанавливают
size + `LD B,B` отключение, потом включают Fill mode и используют
сохранённый size). Для `gfx_fill_rect` с 100 одинаковыми
строками — установить size 1 раз, а не 100.
### Bank-prefix modes (port 0xE2 bits)
Документ показывает три варианта банка видеостраницы помимо стандартного 0x50:
| Bank byte | Effect |
|---|---|
| 0x50 | Normal write — пишется в shadow + видимый |
| 0x54 | "no copy in main shadow RAM" |
| 0x58 | **"FF is transparent"** — байт 0xFF при write оставляет background |
| 0x5C | both |
Bank 0x58 объясняет почему mouse cursor рисуется с 0xFF-прозрачностью.
Это путь к **sprite-blending через accel block copy**:
- [ ] **`gfx_set_bank_transparent(on)`** или флаг в `gfx_set_bank` для
выбора 0x50/0x58 при отрисовке sprite'ов
- [ ] Использовать в новом `gfx_blit()` чтобы по факту получать
transparent sprites через accel-копию
### Block copy mode (sprite blit'ы)
`LD L,L` (horizontal) и `LD A,A` (vertical) — режим копирования блока через
256-байтную accel memory. Это базис для blit'ов.
- [ ] **`gfx_blit(src_data, x, y, w, h)`** — копирование sprite'а
(произвольный размер, через accel)
- [ ] **`gfx_blit_transparent(src, x, y, w, h)`** — с использованием bank 0x58
См. `Draw_Restangle_Data` в accelerator_doc.txt как референс.
### AND / OR / XOR operations через accel
Документ показывает что accel поддерживает логические операции с блоками
данных. Применения:
- XOR — инверсия области (выделение selection в UI)
- OR / AND — masking, alpha-style blending
- См. пример в accelerator_doc.txt: "256 bytes block coding via XOR"
- [ ] **`gfx_xor_rect`** / **`gfx_or_rect`** / **`gfx_and_rect`** —
примитивы логических операций над прямоугольником
- [ ] **`gfx_invert_rect(x, y, w, h)`** — alias на xor с 0xFF
### Bitmap fonts разных размеров
Сейчас `gfx_text` / `gfx_putchar` хардкоженно работают с 8×8 шрифтом
(BIOS WIN_GET_ZG возвращает 256×8 байт). Для будущих UI / титульников
нужны:
- [ ] **`gfx_set_font_size(w, h)`** — переключить ширину/высоту glyph'а
- [ ] **`gfx_set_font_data(ptr, w, h, advance)`** — заменить указатель
на пользовательский шрифт + размеры
- [ ] Поддержка **proportional** (advance != w) шрифтов — добавить
array advance[256] на ширину каждого glyph'а
- [ ] **Big-font режимы**: 8×16, 16×16, 16×8 (для титульников)
- [ ] Возможно отдельный API `gfx_text_ex(x, y, str, font_id)` где
font_id выбирает один из загруженных шрифтов
- [ ] **Anti-alias 2-bit шрифты** (бит фон / бит граница / 2-бит alpha?)
— far future, для smooth UI
## Финальный этап оптимизаций (не сейчас)
- **`gfx_line` через accel для пологих диагоналей** — Bresenham для линии с |dy| << |dx| (или наоборот) выдаёт длинные runs одинакового Y (или X): пиксель, пиксель, пиксель, шаг Y, пиксель, пиксель... Каждый такой run — это готовый аргумент для `gfx_hline` (или `vline`).
План исследования: посчитать длину runs как функцию от наклона; решить минимальный run length, при котором выгоднее accel hline чем N×putpixel (overhead accel ~20µs, putpixel ~5µs — accel выгоднее при run ≥ 4-5 px); для крутых диагоналей (dx ≈ dy) оставить Bresenham, для пологих — run-length-based fill.
Сейчас `gfx_line` orthogonal cases уже через accel — оптимизировать только косые.
- **`gfx_fill_rect` с одним W3-swap на всю операцию** — сейчас каждый внутренний `gfx_hline`/`gfx_vline` делает свой DI/save-W3/restore-W3/EI. Можно сделать internal `_fill_rect_inner` который держит W3 замапленным и DI весь цикл; ~20µs × количество строк/столбцов экономии. Применимо ко всем композитным примитивам.
+28
View File
@@ -0,0 +1,28 @@
# Тест UTF-8
Это **проверка** кодировки _UTF-8_ в `mdview2`.
Кириллица должна читаться: съешь же ещё этих мягких булок.
## Символы и пунктуация
Тире — длинное, и – короткое. Кавычки: «ёлочки» и “лапки”.
Многоточие… стрелки → ← ↑ ↓, галочка ✓ и крестик ✗.
Градус 25°, пункт списка • буллет, номер № 7.
## Список
- Первый пункт
- Второй пункт с длинным текстом, чтобы проверить перенос строки по словам на границе экрана восемьдесят символов
- Ёжик, ёлка, объём
> Цитата: «Краткость — сестра таланта».
## Таблица
| Язык | Привет | Число |
|----------|---------------|-------|
| Русский | Здравствуйте | 42 |
| English | Hello | 7 |
| Deutsch | Grüße | 100 |
Конец файла.
+144
View File
@@ -0,0 +1,144 @@
# mdview2 — поддержка кодировок (CP866 / CP1251 / KOI8-R / UTF-8)
Статус: РЕАЛИЗОВАНО (2026-06-25). Все фазы сделаны; Фаза 4 — eager-кооперативным
вариантом (см. ниже), а не idle-build. Осталась только проверка на железе и
возможная оптимизация «не строить заведомо бесполезный вторичный набор».
## Цель
- CP1251, KOI8-R — простой 1:1 маппинг байтов [128-255] в CP866 (кириллица
+ основная пунктуация). KOI8-R равнозначна, входит в общий цикл.
- UTF-8 — декодирование с маппингом кириллицы в CP866 + подстановка части
некириллических символов (стрелки, галочки, тире, буллеты, box-drawing) в
CP866/ASCII-глифы.
- Автоопределение кодировки при открытии (BOM + дешёвая эвристика).
- F8 (Codepage) — переключение по циклу CP866 → CP1251 → KOI8-R → UTF8.
## Ключевые наблюдения (определяют архитектуру)
1. **8-битные кодировки имеют ОДНУ структуру.** markdown-разметка вся в ASCII
(`# * | -` …); 8-битные различаются только глифами [128-255]. Значит индекс
и кэш (смещения, переносы, ширины таблиц) для CP866/CP1251/KOI8-R —
**общие**; переключение между ними = ремап глифов [128-255], БЕЗ
переиндексации.
2. **UTF-8 — другая структура** (кириллица 2 байта). Нужен отдельный
конвертированный CP866-буфер со своим индексом/кэшем.
3. **Проблема рамок таблиц / маркеров.** В кэше намешаны контентные глифы
(байты-из-источника [128-255] — НАДО ремапить) и вставленные нами CP866-
глифы (box `│─┼…`, маркер списка 0x07, цитата 0xB3, HR 0xC4 — ремапить
НЕЛЬЗЯ). По значению байта не различить → различаем по **атрибуту**.
## Архитектура
### Буферы
- `src_phys[]` — оригинальные байты файла (грузим как сейчас; храним всегда —
под ремап и raw-view, см. memory/mdview2_file_phys_preserve).
- 8-битный режим: `fb()` читает `src_phys` напрямую. Кэш контентных ячеек
хранит **исходный байт** (не пред-конвертированный).
- UTF-8 режим: отдельные EMM-страницы `utf_phys[]` = UTF8→CP866 конвертация
src; свой индекс/кэш; `fb()` читает `utf_phys`.
### Различение контент/структура по attr
Каждый структурный глиф получает НЕ-контентный attr:
- box-рамка таблиц → новый `ATTR_BOX` (сейчас TBL_ATTR=ATTR_TEXT — поменять);
- HR (0xC4) → ATTR_HR; маркер списка (0x07) → ATTR_LIST_MARKER; цитата
(0xB3) → ATTR_QUOTE_MARKER — уже различимы.
Правило ремапа: ремапить `char>=128` только если attr ∈ контентных
(TEXT/BOLD/ITALIC/UNDER/CODE/STRIKE/TITLE1-4). Структурные — как есть.
### Ремап на этапе отрисовки
`draw_line_from_cache`:
- encoding == CP866 или UTF-8: прямой `win_rest` из кэш-страницы (ремап не
нужен — CP866 identity; UTF-8-кэш уже CP866).
- encoding == CP1251/KOI8-R: читаем кэш-строку в near-буфер, ремапим
контентные байты [128-255] через активную таблицу (структурные пропускаем
по attr), пишем в scratch-страницу, `win_rest` из неё. Только видимые ~30
строк, на скролле — дёшево.
Итого: переключение между 8-битными — мгновенно (меняем активную таблицу +
redraw, draw ремапит). Переиндексация только при переходе в/из UTF-8.
## Детекция кодировки (дешёвый скан байтов, ДО построения)
1. **BOM**: первые 3 байта EF BB BF → UTF8 (и пропустить BOM).
2. **UTF-8 валидность** (если нет BOM): проход, проверка структуры
(лид-байты 0xC2-0xDF/0xE0-0xEF/0xF0-0xF4 + континюэйшны 0x80-0xBF;
одиночный 0x80-0xBF, 0xC0/0xC1, 0xF5+ → нарушение). 0 нарушений И есть
≥1 multibyte → UTF8. Любое нарушение → 8-бит.
3. **8-бит дизамбигуация** (CP866/CP1251/KOI8-R): счёт попаданий в байты
самых ходовых строчных русских букв (о е а и н т с р в л) каждой кодировки;
максимум выигрывает. (Предрасчёт байт-наборов по таблицам.)
4. **Фолбэк**: нет байт ≥0x80 или неоднозначно → CP866 (родная).
## Таблицы (static const, CODE/const-сегмент)
- `cp1251_to_866[256]`, `koi8r_to_866[256]` — байт→байт (ASCII identity;
кириллица по раскладкам; en/em-dash, «ёлочки», … → CP866-аналоги или '?').
- UTF-8: `utf_cyr_to_866[]` для U+0400..U+045F + компактная таблица символов
`utf_sym[]` (codepoint→CP866): U+2192→'>'/стрелка, U+2190→'<', U+2713/14 ✓
→'v'/box, U+2022 •→0x07/0xF9, U+2014/2013 —→'-', U+2026 …→"...",
U+00A0→' ', U+2500.. box→CP866 box; прочее → '?'.
## Поток загрузки
1. Грузим в `src_phys`. Детект-скан → кодировка E.
2. Если E ∈ {UTF8}: строим UTF-8-набор (utf_phys + конвертация + индекс),
показываем UTF-8. Иначе: показываем 8-битный (общий индекс на src,
активная таблица = E).
3. **Ленивый build второго набора в простое.** Главный цикл — НЕ блокирующий
getkey, а kbhit-поллинг: пока нет клавиш и второй набор (UTF-8 при
стартовом 8-бит, либо 8-бит при стартовом UTF-8) не построен — докручиваем
его инкрементально. После — F8 в любую сторону мгновенно.
## F8 — переключение
- Цикл g_encoding: CP866 → CP1251 → KOI8-R → UTF8 → CP866.
- 8-бит↔8-бит: сменить активную таблицу + redraw (без переиндексации).
- в/из UTF-8: переключить активный индекс/кэш на соответствующий набор
(если построен; иначе достроить — но при ленивом build обычно уже готов).
- Статус-бар: имя кодировки; меню (строка 31): «F8 Codepage».
## Фазы реализации (ИТОГ)
1.**Ядро 8-бит**: ATTR_BOX; кэш хранит исходный байт; таблицы CP1251/KOI8R;
ремап в draw (`win_rest_remap`).
2.**UTF-8 набор**: `alloc_and_convert_utf8` + `utf8_convert` (декодер 1/2/3-
байт, 4-байтные/битые → `?`); `utf_cyr_to_866[96]` + символьные подстановки
в `conv_emit_cp` (стрелки 0x18-0x1B, галка 0xFB, буллет 0xF9, тире/кавычки/
box/° и т.п.; «» → `<`/`>`, т.к. в CP866 гильеметов нет).
3.**Детекция** (BOM + эвристика), сэмпл — первые **4 КБ** (быстро); хвост-
обрезка multibyte на границе сэмпла не штрафуется.
4.**Сосуществование 2 наборов** (`docset_t g_doc[2]` + `doc_save/load/switch`,
свап «живых» глобалов). Сборка — **ленивая (build-on-demand)**: при старте
строится только первичный (показываемый) набор; UTF-8 конвертация тоже
ленивая (в `build_doc`, не до первого экрана → старт быстрый). Второй набор
достраивается `switch_encoding()` при первом F8-переходе в него (спиннер,
потом кэш). Eager-вариант отвергнут: удваивал старт и блокировал F8 на время
фоновой сборки.
5.**F8** полный цикл CP866→CP1251→KOI8R→UTF8→CP866 (внутри 8-бит — ремап,
на границе — `doc_switch`/ленивая сборка). Статус: `enc_name` кол.37. Меню
«F8 Codepage» показывается только когда переключение возможно (`g_f8_enabled`):
во время сборки 8-битного первичного F8 разрешён в `load_key` (только цикл
8-бит); во время сборки UTF-8 первичного метка F8 скрыта. F1-справка: секция
Encoding.
NB: меню-строка разбита на 10 блоков по 8 колонок, метки Fn кладутся в блок
(n-1)*8 (F1→0, F8→7, F10→9).
## Память
- 8-бит: src + scratch-страница для ремапа (1 стр.). Доп. индекса нет.
- UTF-8 набор: utf_phys (≤8 стр.) + свой индекс/кэш-контент. Строится лениво.
- Бюджет 215 свободных страниц (sprinter_emm_budget) — с запасом.
## Открытые вопросы (к реализации)
1. Имя кодировки в статусе — где (зона имени файла / отдельный слот).
2. Набор UTF-8-подстановок символов — приоритет (→ ← ✓ ✗ • — … « » box).
3. Глубина таблиц пунктуации CP1251/KOI8 (минимум кириллица+dash или полнее).
4. Объём детект-сэмпла (весь файл или первые N КБ).
## Риски
- Корректность различения контент/структура по attr — нужно, чтобы ВСЕ
вставленные глифы имели не-контентный attr (проверить box/markers).
- Два набора индекс/кэш (8-бит + UTF-8) + переключение активного — учёт
страниц, чтобы не течь и не путать.
- Точность таблиц (особенно UTF-8 символы) — итеративно по факту.
+164
View File
@@ -0,0 +1,164 @@
# mdview2 — план: рендер-кэш вместо живого парсинга на каждый скролл
Контекст и обоснование — см. обсуждение 2026-06-23 (после оптимизации mdview через
`<bios/text.h>`, см. `mdview-модель-документа-и-рендеринг.md`). Идея: вместо того
чтобы при каждом скролле повторно идти в файл (`fb()`, W3-банкинг) и гонять
markdown inline-парсер (`handle_inline_marker`/emphasis state machine), один раз
отрендерить каждую логическую строку в готовый байтовый буфер и дальше выводить
его на экран напрямую — без парсинга, без обращения к исходному файлу.
Ключевые подтверждённые факты (эмпирически в MAME, не из документации):
- **Формат буфера ESTEX `WINCOPY`(59h)/`WINREST`(5Ah)** — пара байт `(char, attr)`
на ячейку, по строкам, шаг строки = `width*2`, без паддинга. Можно генерировать
самим, не вызывая `WINCOPY`. `WINREST` копирует сразу `H` строк одним вызовом.
Детали ABI и регистров — см. `tests/winrest/winrest.c`.
- **Бюджет EMM**: 256 страниц (4 МБ) total, 215 (3440 КБ) free на старте программы.
Файл+индекс в худшем случае (128 КБ файл) съедают 16 страниц (256 КБ) — остаётся
≈199 страниц (3184 КБ). Кэш всего документа целиком (даже худший случай:
16384 строк × 160 байт = 2.6 МБ) укладывается без LRU/частичного кэша.
## Единый формат строки (пересмотрено 2026-06-23)
Исходно планировалось два типа строк (тип 1 — многоцветные, помещающиеся;
тип 2 — nowrap/code, один стиль, чистый текст для экономии памяти). От этого
деления отказались: строки таблиц (`IF_NOWRAP`, но не `IF_CODE`) всё равно
проходят inline-парсер и могут содержать несколько атрибутов (bold/italic в
ячейках) — предположение "один стиль" для них неверно. Бюджет EMM
([[sprinter_emm_budget]]) с большим запасом покрывает (char,attr)-формат для
ВСЕХ строк без исключения, поэтому усложнение не оправдано.
**Единый формат**: кэш-запись = `len` пар `(char,attr)` — реальная длина
контента в ячейках, без паддинга до 80, капается на `MAX_CACHE_LINE_LEN`=255.
Вывод: `bios_fillcharattr(' ', base_attr, SCREEN_W)` (очистить строку) →
`win_rest(row, left_margin, 1, len, page)` (контент). Для широких nowrap-строк
горизонтальный скролл (Фаза 5) — это просто смещение НАЧАЛА среза внутри ТОГО
ЖЕ (char,attr)-буфера на `hscroll*2` байт, тот же `win_rest`, без отдельного
плain-текстового формата и без необходимости на лету "разворачивать" текст+
атрибут в пары.
## Фазы реализации
### Фаза 0 — скаффолдинг `examples/mdview2/` [СДЕЛАНО 2026-06-23]
Новая директория со своим `Makefile`/`app.mk` (по аналогии с `mdview/`, не
модифицируем `mdview.c`). `mdview2.c` — копия `mdview.c` без изменений логики
(только usage-строка/заголовок комментария). Собирается чисто, дискета собрана.
### Фаза 1 — формат кэша и директория строк [СДЕЛАНО и ЗАКРЫТО 2026-06-23]
Реализовано в `mdview2.c`:
- `cache_rec_t` (РОВНО 8 байт: `page`, `off` (uint16_t), `len`, `flags`,
`reserved`, `pad[2]`; размер задаётся `CACHE_DIR_REC_SIZE = sizeof(cache_rec_t)`,
НЕ хардкодом — см. разобранный инцидент ниже) — **отдельная** директория
(`cache_dir_phys[]`/`cache_dir_get`/`cache_dir_put`), своя ёмкость =
`file_pages+1` страниц (как у индекса). **Важное уточнение к исходному тексту
плана ниже**: директория НЕ переиспользует слоты `idx_rec_t` in-place —
рендер-воркер (Фаза 2) при обработке строки N может заглядывать в idx-записи
СОСЕДНИХ строк (откат cont-сегментов, line_idx+1 для границы сегмента); если
бы строка N-1 была перезатёрта сразу после своего рендера, воркер строки N
прочитал бы уже не исходные off/flags, а указатель в кэш — поломав откат.
Бюджет EMM ([[sprinter_emm_budget]]) позволяет отдельный массив — он безопаснее.
- Пул контента — отдельные EMM-страницы (`cache_content_phys[]`), ленивый рост
по 1 странице в `cache_reserve()` (bump-allocator, запись никогда не
разбивается через границу страницы; длина капается на `MAX_CACHE_LINE_LEN`=255
ячеек).
- `cache_commit()` = один `bank_write()` на строку (буфер строки собирается
локально в W1/W2 заранее, не в W3 — иначе конфликт с `fb()` при чтении
исходника во время рендера).
- `win_rest()` (ESTEX WINREST 5Ah) — вывод готового буфера на экран.
- `phase1_selftest()` — самопроверка (резервирует/коммитит 4 тестовые ячейки,
кладёт запись в директорию по индексу 0, рисует через `win_rest` дважды
подряд с координатами через параметры функции) — убрать в Фазе 2.
**Подтверждено визуально в MAME (2026-06-23)**: оба квадрата 2×2 на месте, без
единой задержки, с координатами через переменные — Фаза 1 полностью закрыта.
**Разобранный инцидент (НЕ платформенный квирк)**: по пути долго казалось, что
`win_rest()` после "всплеска" обычных BIOS print-вызовов рисует буфер в
неправильном месте, причём воспроизводилось только когда `row`/`col` приходили
через переменные, а не как константы — это и было ключом. Настоящая причина:
`cache_rec_t` фактически занимал 7 байт (SDCC z80 не паддит структуры), а
`CACHE_DIR_REC_SIZE` был захардкожен как 8 — `bank_read`/`bank_write` копировали
8 байт в 7-байтный буфер на стеке, затирая соседнюю переменную (параметр `col`)
вызывающей функции. Фикс — `cache_rec_t` явно до 8 байт + размер через
`sizeof()`. Полная история и общий урок — [[sprinter_winrest_format]] и
[[defer_unexplained_quirks]].
### Фаза 2 — рендер-воркер (бывший `render_line`) [СДЕЛАНО 2026-06-23]
`render_line_to_cache(line_idx)` — адаптация `render_line()`: та же классификация
строк/префиксов/inline-парсинг (`handle_inline_marker`, emphasis state machine,
soft-wrap join) БЕЗ ИЗМЕНЕНИЙ, но вместо `bios_writeattr`/`flush_run`/`wrchar`/
`bios_fillcharattr` на экран — пишем `(char,attr)` пары через `cc_put`/`cc_fill`
в локальный `cellbuf` (без батчинга через `runbuf`/`flush_run` — тот паттерн был
нужен только чтобы минимизировать число BIOS-вызовов, при записи в локальный
буфер смысла нет, пишем посимвольно сразу по месту), затем ОДНИМ
`cache_reserve()`+`cache_commit()`+`cache_dir_put()` коммитим всю строку.
Кэшируется ПОЛНЫЙ контент строки до `MAX_CACHE_LINE_LEN`, без обрезки по
`SCREEN_W` и без среза по `viewport_x` (view/scroll-time понятия, Фаза 4-5).
Также найден и исправлен по ходу баг в `win_rest()`: `IX` всегда указывал на
начало страницы (`#0xC000`), полностью игнорируя `off` — работало только в
Фазе 1, где в кэше была ровно одна запись со смещением 0; как только Фаза 2
начала пакетировать много строк на одной странице с разными `off`, все строки
стали читаться с начала страницы. Фикс: `win_rest()` принимает `off`
(uint16_t), `IX = 0xC000 + off` (новая раскладка ABI сверена через `sdcc -S`
с непустым телом — у `__naked` с пустым телом SDCC не генерирует код доступа
к параметрам, нужен пробный non-naked враппер). Подтверждено визуально в MAME.
### Фаза 3 — фоновый пре-рендер с прогрессом [СДЕЛАНО 2026-06-23]
`emit_seg()` вызывает `render_line_to_cache()` **interleaved** с построением
индекса — но с отставанием на одну строку: рендер строки N требует уже
существующей idx-записи N+1 (источник `seg_end`), которой ещё нет в момент,
когда строка N только создана. Поэтому `emit_seg()` для новой строки рендерит
ПРЕДЫДУЩУЮ (`n_lines-2` после инкремента) — её флаги (`IF_NOWRAP`/`IF_CODE`/
`IF_BLANK`) к этому моменту уже дописаны вызовом `set_*_cur()` на предыдущей
итерации `index_lines()`. Последнюю строку файла (у которой "следующей" не
будет) дорендеривает сам `index_lines()` после выхода из цикла, как и
`render_line()` делал для последней строки при живом рендере (`seg_end =
file_size`). Требует, чтобы `cache_dir_phys[]` был выделен ДО `index_lines()`
это уже так (`load_file()` выделяет директорию кэша, затем вызывается
`index_lines()`).
**Важное сужение скоупа относительно исходного текста плана ниже**: пункты
"спиннер крутится, пока `rendered_up_to < n_lines`" и "скролл ограничен
диапазоном `[0, rendered_up_to]`" **не реализованы и не нужны** в этой
архитектуре — рендеринг происходит СИНХРОННО внутри той же однопроходной
`index_lines()`, без событийного цикла во время загрузки; пользователь
физически не может начать скроллить, пока `index_lines()` не вернёт
управление, а к этому моменту весь документ уже полностью в кэше. Спиннер
из `index_lines()` (каждые 16 строк) сохранён как есть — он покрывает
индексацию+рендер вместе, отдельный прогресс-индикатор не нужен.
### Фаза 4 — cache-only draw path при скролле
Цикл перерисовки видимой области (после пре-рендера) идёт **только** по
директории строк → `win_rest` (тип 1) или срез текста+`bios_writeattr` (тип 2).
Никаких обращений к `fb()`/исходному файлу в steady-state скролле.
### Фаза 5 — горизонтальный скролл для широких nowrap-строк
Срез ТОГО ЖЕ (char,attr)-кэш-буфера по текущему `hscroll`-офсету (смещение
начала на `hscroll*2` байт внутри буфера строки), `win_rest(row, col, 1,
visible_len, page)` с новым `off`. Никакого отдельного плоско-текстового
формата не нужно (см. пересмотр "Единый формат строки" выше).
### Фаза 6 — тестирование в MAME
Тот же набор паттернов, что использовался для mdview1 (заголовки/списки/цитаты/
code-block/bold-italic/nowrap-обрезка), ПЛЮС: реальный замер EMM на большом
документе (не синтетика — проверить, что бюджет из [[sprinter_emm_budget]]
действительно держится на чём-то близком к 128 КБ); UX пре-рендера (спиннер +
ограничение скролла, отсутствие "дыр" в недорендеренной области); проверка
`win_rest` на РЕАЛЬНОМ отрендеренном контенте (не только синтетический A/B/C/D
тест из `tests/winrest`).
## Открытые вопросы (решить по ходу, не блокируют старт Фазы 0)
- ~~Хранение длины строки в директории~~ — решено в Фазе 1: поле `len` (uint8_t)
прямо в `cache_rec_t`.
- ~~Деление строк на тип 1/тип 2~~ — отказались (см. "Единый формат строки"
выше): единый (char,attr)-формат для всех строк, капается на
`MAX_CACHE_LINE_LEN`=255 ячеек (с тем же индикатором обрезки на этапе вывода,
что уже есть в рендере для nowrap-строк).
- Освобождать ли страницы исходного файла после того, как все его строки
отрендерены (вернуть EMM в пул) — даёт больше места про запас, но усложняет
(нужна гарантия, что назад к файлу обращаться больше не придётся — а это не
так, если позже добавится поиск по тексту). Не делать в v1.
- BIOS-вариант `WIN_COPY_WIN`/`WIN_RESTORE_WIN` (0B2h/0B3h, RST 8) не проверен
(см. [[sprinter_winrest_format]]) — ESTEX-варианта достаточно для v1, проверять
BIOS-вариант только если понадобится экономия на RST-диспетчеризации.
File diff suppressed because it is too large Load Diff
+40 -16
View File
@@ -22,25 +22,49 @@ BUILD := $(PROJ_ROOT)/lib/build
# All libc C modules.
LIBC_C := \
libc/io/atexit.c libc/io/conio.c libc/io/cprintf.c libc/io/dir.c \
libc/io/videomode_raw.c \
libc/io/_errno_set.c \
libc/io/env.c libc/io/errno.c libc/io/fsdir.c \
libc/io/lseek.c libc/io/mouse.c libc/io/open.c \
libc/io/read.c libc/io/sleep.c \
libc/io/time.c libc/io/posix_time.c libc/io/unlink.c \
libc/sys/atexit.c \
libc/conio/conio.c \
libc/conio/cprintf.c \
libc/conio/text_palette.c \
libc/bios/text.c \
libc/io/dir.c \
libc/video/videomode_raw.c \
libc/video/palette.c \
libc/errno/_errno_set.c \
libc/env/env.c \
libc/errno/errno.c \
libc/io/fsdir.c \
libc/io/lseek.c \
libc/mouse/mouse.c \
libc/io/open.c \
libc/io/read.c \
libc/time/sleep.c \
libc/time/time.c \
libc/time/posix_time.c \
libc/io/unlink.c \
libc/io/stat.c \
libc/mem/bank_io_w3.c libc/mem/bank_io_w1.c libc/mem/mem_alloc.c \
libc/gfx/gfx_core.c libc/gfx/gfx_raw_common.c \
libc/gfx/gfx_raw_256.c libc/gfx/gfx_raw_16.c \
libc/gfx/gfx_256.c libc/gfx/gfx_16.c \
libc/gfx/gfx_font.c libc/gfx/gfx_text_256.c \
libc/mem/bank_io_w3.c \
libc/mem/bank_io_w1.c \
libc/mem/mem_estex.c \
libc/mem/mem_bios.c \
libc/gfx/gfx_core.c \
libc/gfx/gfx_palette.c \
libc/gfx/gfx_raw_common.c \
libc/gfx/gfx_raw_256.c \
libc/gfx/gfx_raw_16.c \
libc/gfx/gfx_256.c \
libc/gfx/gfx_16.c \
libc/gfx/gfx_font.c \
libc/gfx/gfx_text_256.c \
libc/gfx/gfx_text_16.c \
libc/stdio/getchar.c \
libc/stdio/putchar.c libc/stdio/puts.c libc/stdio/file.c \
libc/stdio/hex_print.c libc/stdio/dec_print.c \
libc/stdio/solid_helpers.c \
libc/io/solid_compat.c
libc/stdio/putchar.c \
libc/stdio/puts.c \
libc/file/file.c \
libc/stdio/hex_print.c \
libc/stdio/dec_print.c \
libc/string/strlwr.c \
libc/string/strupr.c
# Runtime modules to bundle (pulled by symbol references from libc-using code).
# NOTE: runtime/bank.s is NOT bundled — its trampoline depends on the banking
+302
View File
@@ -0,0 +1,302 @@
/*
* text.c — wrappers for the Sprinter BIOS text-output calls (RST 8,
* 081h..08Eh "LP_*"). See <bios/text.h> for the full opcode -> name
* mapping and semantics.
*
* ABI recap (docs/converted/bios.txt):
* - 081h/082h/083h explicitly preserve HL and IX -> no push/pop ix
* needed, and the result is the smallest/fastest of the set.
* - 084h/08Eh (place get/set) are used without push/pop ix elsewhere
* in this libc (libc/conio/conio.c gotoxy/wherex/wherey via the same
* opcodes) — empirically safe, followed here too.
* - Everything else clobbers IX like any other BIOS/ESTEX call, so
* every RST 8 below is bracketed with push ix / pop ix.
*
* Stack-argument layout: SDCC __sdcccall(1) places the first 8-bit arg
* in A, the second (only if the first was also 8-bit) in L; every arg
* beyond that lands on the stack in left-to-right declaration order,
* starting at [SP+2] (right after the 2-byte return address) — verified
* empirically via `sdcc -S` for 2..6 uint8 args and for pointer+N uint8
* args. IY is used as a scratch index register to read those without
* disturbing IX (the caller's frame pointer).
*/
#include <bios/text.h>
void bios_fillcharattr(char ch, uint8_t attr, uint8_t count) __naked
{
(void)ch; (void)attr; (void)count;
__asm
;; ch->A, attr->L, count on stack at [SP+2].
;; LP_PRINT_ALL (081h): A=ch, E=attr, B=count. Preserves HL,IX.
ld iy, #2
add iy, sp
ld b, 0 (iy) ; B = count
ld e, l ; E = attr
ld c, #0x81
rst #0x08
pop hl ; return address
inc sp ; consume count byte
jp (hl)
__endasm;
}
void bios_fillchar(char ch, uint8_t count) __naked
{
(void)ch; (void)count;
__asm
;; ch->A, count->L. LP_PRINT_SYM (082h): A=ch, B=count.
;; Preserves HL,IX.
ld b, l
ld c, #0x82
rst #0x08
ret
__endasm;
}
void bios_fillattr(uint8_t attr, uint8_t count) __naked
{
(void)attr; (void)count;
__asm
;; attr->A, count->L. LP_PRINT_ATR (083h): E=attr, B=count.
;; Preserves HL,IX.
ld e, a
ld b, l
ld c, #0x83
rst #0x08
ret
__endasm;
}
void bios_set_place(uint8_t row, uint8_t col) __naked
{
(void)row; (void)col;
__asm
;; row->A, col->L. LP_SET_PLACE (084h): D=row, E=col.
ld d, a
ld e, l
ld c, #0x84
rst #0x08
ret
__endasm;
}
uint16_t bios_get_place(void) __naked
{
__asm
;; LP_GET_PLACE (08Eh): returns D=row, E=col already the
;; uint16_t return convention (DE); nothing left to rearrange.
ld c, #0x8E
rst #0x08
ret
__endasm;
}
void bios_writeattr(const char *s, uint8_t len, uint8_t attr) __naked
{
(void)s; (void)len; (void)attr;
__asm
;; s->HL, len/attr on stack at [SP+2]/[SP+3].
;; LP_PRINT_LN (085h): HL=s, B=len, E=attr.
ld iy, #2
add iy, sp
ld b, 0 (iy) ; B = len
ld e, 1 (iy) ; E = attr
push ix
ld c, #0x85
rst #0x08
pop ix
pop hl ; return address
inc sp
inc sp ; consume len + attr
jp (hl)
__endasm;
}
void bios_write(const char *s, uint8_t len) __naked
{
(void)s; (void)len;
__asm
;; s->HL, len on stack at [SP+2].
;; LP_PRINT_LN2 (086h): HL=s, B=len.
ld iy, #2
add iy, sp
ld b, 0 (iy) ; B = len
push ix
ld c, #0x86
rst #0x08
pop ix
pop hl
inc sp
jp (hl)
__endasm;
}
void bios_writeattr_until(const char *s, uint8_t len, uint8_t attr, char sep) __naked
{
(void)s; (void)len; (void)attr; (void)sep;
__asm
;; s->HL, len/attr/sep on stack at [SP+2..4].
;; LP_PRINT_LN3 (087h): HL=s, B=len, E=attr, D=sep.
;; Pads with spaces after sep up to len.
ld iy, #2
add iy, sp
ld b, 0 (iy) ; B = len
ld e, 1 (iy) ; E = attr
ld d, 2 (iy) ; D = sep
push ix
ld c, #0x87
rst #0x08
pop ix
pop hl
inc sp
inc sp
inc sp ; consume len + attr + sep
jp (hl)
__endasm;
}
void bios_write_until(const char *s, uint8_t len, char sep) __naked
{
(void)s; (void)len; (void)sep;
__asm
;; s->HL, len/sep on stack at [SP+2..3].
;; LP_PRINT_LN4 (088h): HL=s, B=len, D=sep. Pads with spaces.
ld iy, #2
add iy, sp
ld b, 0 (iy) ; B = len
ld d, 1 (iy) ; D = sep
push ix
ld c, #0x88
rst #0x08
pop ix
pop hl
inc sp
inc sp
jp (hl)
__endasm;
}
void bios_clearwin(uint8_t row, uint8_t col, uint8_t height, uint8_t width,
uint8_t attr) __naked
{
(void)row; (void)col; (void)height; (void)width; (void)attr;
__asm
;; row->A, col->L, height/width/attr on stack at [SP+2..4].
;; LP_CLS_WIN (089h): D=row, E=col, H=height, L=width, B=attr.
ld iy, #2
add iy, sp
ld h, 0 (iy) ; H = height
ld c, 1 (iy) ; stash width (L is still col for now)
ld e, l ; E = col
ld d, a ; D = row
ld l, c ; L = width
ld b, 2 (iy) ; B = attr
push ix
ld c, #0x89
rst #0x08
pop ix
pop hl
inc sp
inc sp
inc sp ; consume height + width + attr
jp (hl)
__endasm;
}
void bios_scrollwin(uint8_t dir, uint8_t row, uint8_t count) __naked
{
(void)dir; (void)row; (void)count;
__asm
;; dir->A, row->L, count on stack at [SP+2].
;; LP_SCROLL_UD (08Ah): B=dir(1 up/2 down), D=row, E=count.
ld iy, #2
add iy, sp
ld e, 0 (iy) ; E = count
ld d, l ; D = row
ld b, a ; B = dir
push ix
ld c, #0x8A
rst #0x08
pop ix
pop hl
inc sp
jp (hl)
__endasm;
}
void bios_writeattr_stop(const char *s, uint8_t maxlen, uint8_t attr,
char sep) __naked
{
(void)s; (void)maxlen; (void)attr; (void)sep;
__asm
;; s->HL, maxlen/attr/sep on stack at [SP+2..4].
;; LP_PRINT_LN5 (08Bh): HL=s, B=maxlen, E=attr, D=sep.
;; Stops at sep (no padding).
ld iy, #2
add iy, sp
ld b, 0 (iy) ; B = maxlen
ld e, 1 (iy) ; E = attr
ld d, 2 (iy) ; D = sep
push ix
ld c, #0x8B
rst #0x08
pop ix
pop hl
inc sp
inc sp
inc sp
jp (hl)
__endasm;
}
void bios_write_stop(const char *s, uint8_t maxlen, char sep) __naked
{
(void)s; (void)maxlen; (void)sep;
__asm
;; s->HL, maxlen/sep on stack at [SP+2..3].
;; LP_PRINT_LN6 (08Ch): HL=s, B=maxlen, D=sep. Stops at sep.
ld iy, #2
add iy, sp
ld b, 0 (iy) ; B = maxlen
ld d, 1 (iy) ; D = sep
push ix
ld c, #0x8C
rst #0x08
pop ix
pop hl
inc sp
inc sp
jp (hl)
__endasm;
}
void bios_clearwin_ch(uint8_t row, uint8_t col, uint8_t height,
uint8_t width, uint8_t attr, char fillch) __naked
{
(void)row; (void)col; (void)height; (void)width; (void)attr; (void)fillch;
__asm
;; row->A, col->L, height/width/attr/fillch on stack [SP+2..5].
;; LP_CLS_WIN2 (08Dh): D=row, E=col, H=height, L=width, B=attr,
;; A=fillch.
ld iy, #2
add iy, sp
ld d, a ; D = row (free A for fillch later)
ld e, l ; E = col (free L for width later)
ld h, 0 (iy) ; H = height
ld c, 1 (iy) ; stash width
ld b, 2 (iy) ; B = attr
ld a, 3 (iy) ; A = fillch
ld l, c ; L = width
push ix
ld c, #0x8D
rst #0x08
pop ix
pop hl
inc sp
inc sp
inc sp
inc sp ; consume height + width + attr + fillch
jp (hl)
__endasm;
}
+186 -137
View File
@@ -15,10 +15,18 @@
#include <stdint.h>
#include <errno.h>
/* Forward extern — definition is further down (after putch/cputs which
* reference it from asm by linker-symbol name). */
extern int16_t g_text_attr;
static two_bytes g_text_attr = {0};
static uint8_t pc_ch = 0;
extern two_bytes pc_place = {0};
static uint8_t pc_raw_mode = 0;
// TODO - проверить - ф-ии 30h-33h (kbhit/getch/getche/getkey)
// не должны менять IX и им можно не делать push ix / pop ix
//
char kbhit(void) __naked
{
__asm
@@ -58,6 +66,30 @@ char getche(void) __naked
__endasm;
}
/* getkey — like getch() but exposes BOTH the ASCII value and the
* positional scan code, so callers can distinguish extended keys
* (arrows, F1..F12, PgUp/PgDn, Home/End, Ins/Del all of which carry
* ASCII == 0 from ESTEX) from plain ASCII keys.
*
* return = (scan << 8) | ascii
*
* For plain keys: ascii holds the character, scan holds the positional
* code (bit 7 set when Ctrl/Alt/Shift is held).
* For extended keys: ascii == 0, scan identifies the key (see KEY_* in
* <conio.h>).
*/
uint16_t getkey(void) __naked
{
__asm
push ix
ld c, #0x30 ; ESTEX WAITKEY: A=ASCII, D=scan, E=ASCII
rst #0x10
pop ix
ld e, a ; E = ASCII (defensive: A is the canonical copy)
ret ; __sdcccall(1) returns uint16_t in DE
__endasm;
}
/* ---- putch / cputs: Turbo-C conio convention ---------------------- *
* Both APPLY the current text attribute (g_text_attr). When attr is
* KEEP_EXIST_ATTR (>0xFF), they short-circuit to the FAST stdio path
@@ -67,10 +99,6 @@ char getche(void) __naked
* caller to use "\r\n" explicitly. Stdio puts/putchar do translate.
*/
static uint8_t pc_ch = 0;
static uint8_t pc_attr = 0;
static uint8_t pc_row = 0;
static uint8_t pc_col = 0;
/* Controls how _raw_putch treats control characters (< 0x20):
* 0 (default) BS/TAB/LF/CR are interpreted (no glyph output);
@@ -80,7 +108,6 @@ static uint8_t pc_col = 0;
* Only takes effect on the WRCHAR (attr 0xFF) path. When
* g_text_attr is KEEP_EXIST_ATTR, ESTEX's own PUTCHAR/PCHARS handle
* cursor and control chars pc_raw_mode is irrelevant. */
static uint8_t pc_raw_mode = 0;
void set_putch_raw_mode(uint8_t mode) { pc_raw_mode = mode; }
uint8_t get_putch_raw_mode(void) { return pc_raw_mode; }
@@ -91,14 +118,11 @@ uint8_t get_putch_raw_mode(void) { return pc_raw_mode; }
static void _get_cursor(void) __naked
{
__asm
push ix
ld c, #0x53 ; ESTEX CURSOR
rst #0x10
ld a, d
ld (_pc_row), a
ld a, e
ld (_pc_col), a
pop ix
; push ix
ld c, #0x8e ; BIOS GetCursor
rst #0x08
ld (_pc_place), de
; pop ix
ret
__endasm;
}
@@ -107,14 +131,11 @@ static void _get_cursor(void) __naked
static void _set_cursor(void) __naked
{
__asm
push ix
ld a, (_pc_row)
ld d, a
ld a, (_pc_col)
ld e, a
ld c, #0x52 ; ESTEX LOCATE
rst #0x10
pop ix
; push ix
ld de, (_pc_place)
ld c, #0x84 ; BIOS SetCursor
rst #0x08
; pop ix
ret
__endasm;
}
@@ -159,7 +180,7 @@ static char _bios_putchar(char ch) __naked
* once before a sequence of _raw_putch calls and write it back once
* after, so we pay the BIOS overhead per OPERATION instead of per CHAR. */
/* Mode-0 worker: interprets BS/TAB/LF/CR, outputs other chars as glyphs. */
static void _raw_putch_raw0(char ch, uint8_t attr) __naked
static void _putch_wrchar(char ch, uint8_t attr) __naked
{
(void)ch; (void)attr;
__asm
@@ -167,6 +188,12 @@ static void _raw_putch_raw0(char ch, uint8_t attr) __naked
;; Dispatch on control chars while A still holds ch (cp does not
;; modify A). B/C only get loaded on the output path so the
;; ctrl-char paths are cheaper.
;; ld c, a
;; ld a, (_pc_ch) ; A = ch (`ld a,(nn)` does not touch flags)
;; jr nz, _rp0_pri
;; ld a, c
cp #0x08
jr z, _rp0_bs
cp #0x09
@@ -177,18 +204,21 @@ static void _raw_putch_raw0(char ch, uint8_t attr) __naked
jr z, _rp0_cr
;; Anything else (printable or unrecognised ctrl) glyph.
_rp0_pri:
ld c, a ; C = ch (save before A is clobbered)
ld a, (_pc_row)
ld a, (_pc_place + 1)
cp #32
ret nc ; off-screen bottom silently skip
ld d, a ; D = row (ESTEX WRCHAR convention)
ld a, (_pc_col)
ld a, (_pc_place)
cp #80
ret nc ; off-screen right silently skip
ld e, a ; E = col
inc a
ld (_pc_col), a ; pc_col++
ld (_pc_place), a ; pc_col++
_rp0_wr:
ld b, l ; B = attr
ld a, c ; A = ch
push ix
@@ -198,56 +228,61 @@ static void _raw_putch_raw0(char ch, uint8_t attr) __naked
ret
_rp0_bs:
ld a, (_pc_col)
ld a, (_pc_place)
or a, a
ret z ; already at col 0 no change
dec a
ld (_pc_col), a
ret
ld (_pc_place), a
ld e, a ; E = col
ld a, (_pc_place + 1)
ld d, a ; D = row (ESTEX WRCHAR convention)
ld c, #0x20
jr _rp0_wr
_rp0_tab:
ld a, (_pc_col)
and #0xF8 ; floor to mult of 8
add a, #8 ; next mult of 8
cp #80
jr c, _rp0_tab_store
ld a, #80 ; cap at off-screen right
_rp0_tab_store:
ld (_pc_col), a
ld a, (_pc_place)
or #0x07 ; floor to mult of 8
inc a ; next mult of 8
cp #81 ; сравнить A с 81 (0x51)
jr c, _rp0_tab_skip ; если A < 81 (т.е. A 80), пропустить загрузку
ld a, #80 ; иначе A > 80 установить A = 80
_rp0_tab_skip:
ld (_pc_place), a
ret
_rp0_lf:
ld a, (_pc_row)
ld a, (_pc_place + 1)
cp #32
ret nc ; already at bottom edge
inc a
ld (_pc_row), a
ld (_pc_place + 1), a
ret
_rp0_cr:
xor a, a
ld (_pc_col), a
ld (_pc_place), a
ret
__endasm;
}
/* Mode-1 worker: every byte goes through WRCHAR as a glyph. */
static void _raw_putch_raw1(char ch, uint8_t attr) __naked
static void _putch_wrchar_raw(char ch, uint8_t attr) __naked
{
(void)ch; (void)attr;
__asm
;; __sdcccall(1): ch in A, attr in L.
ld c, a ; C = ch (save)
ld a, (_pc_row)
ld a, (_pc_place + 1)
cp #32
ret nc ; off-screen bottom silently skip
ld d, a ; D = row
ld a, (_pc_col)
ld a, (_pc_place)
cp #80
ret nc ; off-screen right silently skip
ld e, a ; E = col
inc a
ld (_pc_col), a ; pc_col++
ld (_pc_place), a ; pc_col++
ld b, l ; B = attr
ld a, c ; A = ch
@@ -259,18 +294,6 @@ static void _raw_putch_raw1(char ch, uint8_t attr) __naked
__endasm;
}
/* PCHARS (no attr) — used by cputs when KEEP_EXIST_ATTR is in effect. */
static void _cputs_pchars(const char *s) __naked
{
(void)s;
__asm
push ix
ld c, #0x5C ; ESTEX PCHARS
rst #0x10
pop ix
ret
__endasm;
}
/* ---- Public putch / cputs --------------------------------------- *
*
@@ -282,6 +305,7 @@ static void _cputs_pchars(const char *s) __naked
* folds the per-char CURSOR/LOCATE pair from the old design into a
* single pair per operation. */
char putch(char ch) __naked
{
(void)ch;
@@ -289,38 +313,26 @@ char putch(char ch) __naked
;; A = ch on entry; char return A.
ld (_pc_ch), a ; stash c (for both return and re-load)
;; KEEP_EXIST_ATTR? high byte of g_text_attr != 0
ld a, (_g_text_attr + 1)
or a, a
jr nz, _putch_fast
;; --- WRCHAR path: cursor worker cursor ---
call __get_cursor
;; Worker ABI (2 char/uint8 args): ch in A, attr in L.
ld a, (_g_text_attr) ; A = low byte = attr
ld l, a ; L = attr
ld a, (_pc_raw_mode)
or a, a ; Z = (mode == 0)
ld a, (_pc_ch) ; A = ch (`ld a,(nn)` does not touch flags)
jr nz, _putch_use_raw1
call __raw_putch_raw0
jr nz, _putch_use_raw
call __putch_wrchar
jr _putch_after_raw
_putch_use_raw1:
call __raw_putch_raw1
_putch_use_raw:
call __putch_wrchar_raw
_putch_after_raw:
call __set_cursor
ld a, (_pc_ch) ; return value
ret
_putch_fast:
ld a, (_pc_ch)
call __bios_putchar ; __bios_putchar keeps AF
ret
__endasm;
}
char cputs(const char *s) __naked
{
(void)s;
@@ -332,53 +344,48 @@ char cputs(const char *s) __naked
or a, l
ret z
push ix
call __get_cursor
;; KEEP_EXIST_ATTR? high byte of g_text_attr != 0
ld a, (_g_text_attr + 1)
or a, a
jr nz, _cputs_fast
;; --- WRCHAR path: cursor worker cursor ---
call __get_cursor
;; Worker walks the string via DE; HL is used to carry attr in L
;; across iterations (RST 10 inside worker clobbers it, so we
;; push/pop hl around each call).
ex de, hl ; DE = s
ld a, (_g_text_attr) ; A = attr (low byte)
ld l, a ; L = attr (worker ABI: arg2 in L)
;; Pick worker once based on pc_raw_mode; IX = function pointer.
ld a, (_pc_raw_mode)
or a, a
jr z, _cputs_use_raw0
ld ix, #__raw_putch_raw1
jr _cputs_loop
_cputs_use_raw0:
ld ix, #__raw_putch_raw0
jr nz, _cputs_bios
_cputs_loop:
ld a, (de)
or a, a
jr z, _cputs_loop_end
inc de
;; Z80 has no "call (ix)" emulate via push-of-ret + jp (ix).
push de ; save string pointer
push hl ; save attr (in L)
ld de, #_cputs_after_worker
push de ; push return address
jp (ix) ; "call" worker
_cputs_after_worker:
pop hl
push hl
pop de
jr _cputs_loop
_cputs_loop_end:
call __set_cursor
xor a, a ; return 0
ret
_cputs_wrloop:
ld a, (_g_text_attr)
ld l, a
ld a, (de) ; загрузить байт
or a ; установить флаг Z, если A == 0
jr z, _cputs_ex ; завершить подпрограмму (конец строки)
push de
call __putch_wrchar ; вывести символ (A передаётся как аргумент)
pop de
inc de ; перейти к следующему байту
jr _cputs_wrloop ; повторить
_cputs_bios:
ld a, (_g_text_attr)
ld b, #0xFF
ld d, #0x0
ld e, a
ld c, #0x8B ; BIOS LP_PRINT_LN5
rst #0x08
jr _cputs_ex
_cputs_fast:
call __cputs_pchars
ld c, #0x5C ; ESTEX PCHARS
rst #0x10
_cputs_ex:
call __set_cursor
pop ix ; restore callers IX
xor a, a ; return 0
ret
__endasm;
@@ -415,12 +422,15 @@ void gotoxy(uint8_t x, uint8_t y) __naked
__asm
;; __sdcccall(1) 2 uint8 args: x in A, y in L.
;; ESTEX LOCATE ($52) wants: D = row, E = col.
push ix
;; push ix
ld d, l ; D = row (y)
ld e, a ; E = col (x)
ld c, #0x52
rst #0x10
pop ix
ld (_pc_place), de
ld c, #0x84 ; BIOS SetCursor
rst #0x08
;; ld c, #0x52
;; rst #0x10
;; pop ix
ret
__endasm;
}
@@ -429,11 +439,14 @@ uint8_t wherex(void) __naked
{
__asm
;; ESTEX CURSOR ($53): D = row, E = col. Return col in DE.
push ix
ld c, #0x53
rst #0x10
pop ix
;; push ix
ld c, #0x8e ; BIOS GetCursor
rst #0x08
;; ld c, #0x53
;; rst #0x10
;; pop ix
ld a, e
ld (_pc_place), de
ret
__endasm;
}
@@ -441,11 +454,14 @@ uint8_t wherex(void) __naked
uint8_t wherey(void) __naked
{
__asm
push ix
ld c, #0x53
rst #0x10
pop ix
;; push ix
ld c, #0x8e ; BIOS GetCursor
rst #0x08
;; ld c, #0x53
;; rst #0x10
;; pop ix
ld a, d
ld (_pc_place), de
ret
__endasm;
}
@@ -454,14 +470,48 @@ uint16_t wherexy(void) __naked
{
__asm
;; ESTEX CURSOR ($53): D = row, E = col. Return col in DE.
push ix
ld c, #0x53
rst #0x10
pop ix
;; push ix
ld c, #0x8e ; BIOS GetCursor
rst #0x08
;; ld c, #0x53
;; rst #0x10
;; pop ix
ld (_pc_place), de
ret
__endasm;
}
/* wrchar(uint8_t x, uint8_t y, char ch, uint8_t attr)
*
* SDCC __sdcccall(1): x in A, y in L (2 uint8 A, L); ch and attr
* packed and pushed on the stack as a single 16-bit value (caller does
* `ld hl, #(attr<<8)|ch; push hl`). Layout after CALL:
* [SP+0..1] = return address
* [SP+2] = ch (low half of pushed pair)
* [SP+3] = attr (high half)
* Void return callee-pops the 2 stack-arg bytes via `pop bc` + jp (iy).
*/
void scroll(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t direction, uint8_t clear) __naked
{
(void)x; (void)y; (void)w; (void)h; (void)direction; (void)clear;
__asm
pop iy ; return address
ld d, l ; D = row (y)
ld e, a ; E = col (x)
pop hl ; H = heigth(h), L = width(w)
pop bc ; C = direction, B = clear
ld a, b ; A = clear(B)
ld b, c ; B = direction(C)
push ix
ld c, #0x55 ; ESTEX SCROLL
rst #0x10
pop ix
jp (iy)
__endasm;
}
/* wrchar(uint8_t x, uint8_t y, char ch, uint8_t attr)
*
@@ -515,12 +565,12 @@ uint16_t rdchar(uint8_t x, uint8_t y) __naked
extern uint8_t _videomode_raw_get(void);
extern int _videomode_raw_set(uint8_t mode);
uint8_t get_videotextmode(void)
uint8_t gettextmode(void)
{
return _videomode_raw_get();
}
int set_videotextmode(uint8_t mode)
int settextmode(uint8_t mode)
{
/* Refuse anything that isn't a known text mode — otherwise a stray
* GFX_MODE_* value could swap the screen out from under text I/O. */
@@ -538,18 +588,17 @@ int set_videotextmode(uint8_t mode)
*
* 0x00..0xFF real attribute (4-bit FG | 3-bit BG | 1-bit blink)
* KEEP_EXIST_ATTR (0xFFFF) putch/cputs fall back to fast no-attr path */
int16_t g_text_attr = 0x0F;
int16_t set_text_attr(int16_t attr)
{
int16_t prev = g_text_attr;
g_text_attr = attr;
int16_t prev = g_text_attr.value;
g_text_attr.value = attr;
return prev;
}
int16_t get_text_attr(void)
{
return g_text_attr;
return g_text_attr.value;
}
/* ---- Turbo-C-style palette helpers --------------------------------
@@ -558,21 +607,21 @@ int16_t get_text_attr(void)
void textcolor(uint8_t fg)
{
/* If we were KEEP_EXIST_ATTR, switch to a real attr first. */
uint8_t cur = ((uint16_t)g_text_attr > 0xFF) ? 0x00 : (uint8_t)g_text_attr;
g_text_attr = (int16_t)((cur & 0xF0) | (fg & 0x0F));
fg = (fg & 0x07);
g_text_attr.value = (g_text_attr.byte.low & 0xF0) | fg;
// g_text_attr.byte.high = 0;
}
void textbackground(uint8_t bg)
{
uint8_t cur = ((uint16_t)g_text_attr > 0xFF) ? 0x00 : (uint8_t)g_text_attr;
/* Background uses 3 bits (4..6); preserve blink (bit 7) too. */
g_text_attr = (int16_t)((cur & 0x8F) | ((bg & 0x07) << 4));
bg = (bg & 0x07) << 4;
g_text_attr.value = (g_text_attr.byte.low & 0x0F) | bg;
// g_text_attr.byte.high = 0;
}
void textattr(uint8_t attr)
{
g_text_attr = (int16_t)attr;
g_text_attr.value = (uint16_t)attr;
}
/* ---- Solid-C compatibility ---------------------------------------- */
+42
View File
@@ -0,0 +1,42 @@
/*
* text_palette.c — text-mode palette wrappers.
*
* Thin layer over libc/video/palette.c. Translates the text "plane
* number" 0..3 (paper/ink/blink-paper/blink-ink) into the underlying
* BIOS palette page 4..7 used by $A4.
*
* For an introduction to the four-plane text colour model see
* <palette.h> (top-of-file doc-block).
*/
#include <stdint.h>
#include <conio.h>
#include <palette.h>
void text_pal_load(uint8_t plane, uint8_t start, uint8_t count,
const uint8_t *data)
{
pal_load((uint8_t)(plane + 4u), start, count, data);
}
void text_pal_set_color(uint8_t plane, uint8_t attr,
uint8_t r, uint8_t g, uint8_t b)
{
pal_set_color((uint8_t)(plane + 4u), attr, r, g, b);
}
void text_pal_get(uint8_t plane, uint8_t start, uint8_t count, uint8_t *data)
{
pal_get((uint8_t)(plane + 4u), start, count, data);
}
void text_pal_get_color(uint8_t plane, uint8_t attr,
uint8_t *r, uint8_t *g, uint8_t *b)
{
pal_get_color((uint8_t)(plane + 4u), attr, r, g, b);
}
void text_pal_reset(void)
{
pal_reset(PAL_CGA);
}
View File
+4 -3
View File
@@ -143,7 +143,8 @@ int fputc(int c, FILE *fp)
{
if (!fp) { errno = EBADF; return EOF; }
if (fp->flags & _F_CONOUT) {
return putchar(c);
putchar(c);
return (int)c;
}
if (!(fp->flags & _F_WRITE)) { errno = EBADF; return EOF; }
uint8_t ch = (uint8_t)c;
@@ -175,7 +176,7 @@ int fputs(const char *s, FILE *fp)
if (!fp || !s) { errno = EBADF; return EOF; }
if (fp->flags & _F_CONOUT) {
while (*s) {
if (putchar((unsigned char)*s++) == EOF) return EOF;
putchar((unsigned char)*s++);;
}
return 0;
}
@@ -239,7 +240,7 @@ size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp)
const char *p = (const char *)ptr;
size_t total = size * nmemb;
for (size_t i = 0; i < total; i++) {
if (putchar((unsigned char)p[i]) == EOF) return i / size;
putchar((unsigned char)p[i]);
}
return nmemb;
}
+4 -41
View File
@@ -13,7 +13,7 @@
* gfx_set_draw_page / get — updates _gfx_addr_base for the new page
* gfx_set_bank / get — sets the W3 page byte (0x50..0x5F)
* gfx_wait_vsync — EI; HALT until next frame interrupt
* gfx_pal_load / gfx_pal_set — BIOS $A4 PIC_SET_PAL wrappers
* (palette wrappers live in gfx_palette.c, backed by libc/video/palette.c)
*
* Shared state (extern from this file):
* _gfx_addr_base — 0xC000 for page 0, 0xC140 for page 1. Every
@@ -136,43 +136,6 @@ void gfx_wait_vsync(void) __naked
__endasm;
}
/* ---- Palette (BIOS $A4 PIC_SET_PAL) ----------------------------- */
static uint8_t pal_num_;
static uint8_t pal_start_;
static uint8_t pal_count_;
static uint16_t pal_data_;
void gfx_pal_load(uint8_t pal_num, uint8_t start, uint8_t count,
const uint8_t *data)
{
pal_num_ = pal_num;
pal_start_ = start;
pal_count_ = count;
pal_data_ = (uint16_t)(uintptr_t)data;
__asm
push ix
ld a, (_pal_start_)
ld e, a ; E = start
ld a, (_pal_count_)
ld d, a ; D = count (0 256)
ld hl, (_pal_data_) ; HL = data
ld b, #0xFF ; mask
ld a, (_pal_num_) ; A = palette number
ld c, #0xA4 ; BIOS PIC_SET_PAL
rst #0x08
pop ix
__endasm;
}
void gfx_pal_set(uint8_t pal_num, uint8_t idx,
uint8_t r, uint8_t g, uint8_t b)
{
uint8_t entry[4];
entry[0] = b;
entry[1] = g;
entry[2] = r;
entry[3] = 0;
gfx_pal_load(pal_num, idx, 1, entry);
}
/* Palette wrappers (gfx_pal_load / gfx_pal_set / gfx_pal_get /
* gfx_pal_get_color / gfx_pal_reset) moved to gfx_palette.c — see also
* libc/video/palette.c for the shared low-level $A4 / $A6 implementation. */
+74
View File
@@ -0,0 +1,74 @@
/*
* gfx_palette.c — graphics-side palette wrappers.
*
* The "legacy" entry points (gfx_pal_load / gfx_pal_set) keep their own
* inline-asm implementation rather than thunking through pal_*; this
* avoids depending on SDCC's __sdcccall(1) stack-shuffling for nested
* calls, which empirically misbehaves here (the trampoline corrupts the
* text palette so the next text-mode print hangs). Functionally these
* are identical to the old gfx_core.c versions.
*
* The newer get / reset helpers are simple wrappers — they're either
* not on hot paths or are first introduced here, so the thunk overhead
* doesn't matter and we get to share the asm with libc/video/palette.c.
*/
#include <stdint.h>
#include <gfx.h>
#include <palette.h>
static uint8_t gpl_num_;
static uint8_t gpl_start_;
static uint8_t gpl_count_;
static uint16_t gpl_data_;
void gfx_pal_load(uint8_t pal_num, uint8_t start, uint8_t count,
const uint8_t *data)
{
gpl_num_ = pal_num;
gpl_start_ = start;
gpl_count_ = count;
gpl_data_ = (uint16_t)(uintptr_t)data;
__asm
push ix
ld a, (_gpl_start_)
ld e, a ; E = start
ld a, (_gpl_count_)
ld d, a ; D = count (0 256)
ld hl, (_gpl_data_) ; HL = data
ld b, #0xFF ; mask
ld a, (_gpl_num_) ; A = palette number
ld c, #0xA4 ; BIOS PIC_SET_PAL
rst #0x08
pop ix
__endasm;
}
void gfx_pal_set(uint8_t pal_num, uint8_t idx,
uint8_t r, uint8_t g, uint8_t b)
{
uint8_t entry[4];
entry[0] = b;
entry[1] = g;
entry[2] = r;
entry[3] = 0;
gfx_pal_load(pal_num, idx, 1, entry);
}
void gfx_pal_get(uint8_t pal_num, uint8_t start, uint8_t count,
uint8_t *data)
{
pal_get(pal_num, start, count, data);
}
void gfx_pal_get_color(uint8_t pal_num, uint8_t idx,
uint8_t *r, uint8_t *g, uint8_t *b)
{
pal_get_color(pal_num, idx, r, g, b);
}
void gfx_pal_reset(void)
{
pal_reset(PAL_GRAPH);
}
+107
View File
@@ -0,0 +1,107 @@
/*
* bios/text.h — direct wrappers around the Sprinter BIOS text-output
* calls (RST 8, function codes 081h..08Eh, "LP_*" in the vendor docs).
*
* These bypass ESTEX's own PUTCHAR/PCHARS/WRCHAR (see <conio.h>) and talk
* straight to the BIOS character-print routines. Use them when you need
* BIOS-specific behaviour (run-length fills, pad/stop-at-separator field
* printing, raw window clear/scroll) that ESTEX doesn't expose directly.
*
* Coordinates are 0-based (row = vertical, col = horizontal), matching
* ESTEX/BIOS directly.
*
* Original BIOS name -> wrapper, with opcode (docs/converted/bios.txt):
*
* 081h LP_PRINT_ALL -> bios_fillchar fill run: char + attr
* 082h LP_PRINT_SYM -> bios_fillchar_noattr fill run: char only
* 083h LP_PRINT_ATR -> bios_fillattr fill run: attr only
* 084h LP_SET_PLACE -> bios_set_place set BIOS print cursor
* 08Eh LP_GET_PLACE -> bios_get_place read BIOS print cursor
* 085h LP_PRINT_LN -> bios_writeattr write buf + attr
* 086h LP_PRINT_LN2 -> bios_write write buf, keep attr
* 087h LP_PRINT_LN3 -> bios_writeattr_until write to sep, pad + attr
* 088h LP_PRINT_LN4 -> bios_write_until write to sep, pad
* 089h LP_CLS_WIN -> bios_clearwin clear window (space fill)
* 08Ah LP_SCROLL_UD -> bios_scrollwin scroll global window
* 08Bh LP_PRINT_LN5 -> bios_writeattr_stop write to sep, stop + attr
* 08Ch LP_PRINT_LN6 -> bios_write_stop write to sep, stop
* 08Dh LP_CLS_WIN2 -> bios_clearwin_ch clear window (custom fill)
*
* "_until" variants print up to `sep` and then PAD the rest of the field
* with spaces (output is always exactly `len` characters wide).
* "_stop" variants print up to `sep` and STOP there (no padding; `sep`
* itself is not printed; `maxlen` is just an upper bound).
*
* ABI: stack must already be in window 2 (guaranteed by crt0) — required
* by the BIOS for its own page-swap scratch use. All registers (main +
* shadow set) are clobbered except where a function says otherwise.
*/
#ifndef BIOS_TEXT_H
#define BIOS_TEXT_H
#include <stdint.h>
/* bios_scrollwin() direction values (BIOS B register). */
#define BIOS_SCROLL_UP 1
#define BIOS_SCROLL_DOWN 2
/* 081h LP_PRINT_ALL — print `count` copies of `ch` with attribute `attr`
* starting at the current BIOS print position. Preserves HL and IX. */
void bios_fillcharattr(char ch, uint8_t attr, uint8_t count);
/* 082h LP_PRINT_SYM — print `count` copies of `ch`, attribute unchanged
* (whatever is already on screen). Preserves HL and IX. */
void bios_fillchar(char ch, uint8_t count);
/* 083h LP_PRINT_ATR — overwrite `count` cells' attribute with `attr`;
* the characters already on screen are left untouched. Preserves HL
* and IX. */
void bios_fillattr(uint8_t attr, uint8_t count);
/* 084h LP_SET_PLACE — set the BIOS print cursor. Out-of-range values
* wrap (BIOS subtracts the window size, not an error). */
void bios_set_place(uint8_t row, uint8_t col);
/* 08Eh LP_GET_PLACE — read the BIOS print cursor: (row << 8) | col. */
uint16_t bios_get_place(void);
/* 085h LP_PRINT_LN — write `len` bytes from `s` with attribute `attr`,
* starting at the current BIOS print position. `s` must point inside
* window 2 (#4000..#BFFF). */
void bios_writeattr(const char *s, uint8_t len, uint8_t attr);
/* 086h LP_PRINT_LN2 — like bios_writeattr(), attribute unchanged. */
void bios_write(const char *s, uint8_t len);
/* 087h LP_PRINT_LN3 — write from `s` with attribute `attr` until the
* byte `sep` is found, then pad with spaces so exactly `len` characters
* are printed in total. */
void bios_writeattr_until(const char *s, uint8_t len, uint8_t attr, char sep);
/* 088h LP_PRINT_LN4 — like bios_writeattr_until(), attribute unchanged. */
void bios_write_until(const char *s, uint8_t len, char sep);
/* 089h LP_CLS_WIN — clear a `height` x `width` local window at
* (row, col) by writing spaces with attribute `attr`. */
void bios_clearwin(uint8_t row, uint8_t col, uint8_t height, uint8_t width,
uint8_t attr);
/* 08Ah LP_SCROLL_UD — scroll `count` whole rows of the global window,
* starting at `row`, in direction `dir` (BIOS_SCROLL_UP/_DOWN). */
void bios_scrollwin(uint8_t dir, uint8_t row, uint8_t count);
/* 08Bh LP_PRINT_LN5 — write from `s` with attribute `attr`, stopping
* (without padding) as soon as `sep` is seen; `maxlen` bounds the scan. */
void bios_writeattr_stop(const char *s, uint8_t maxlen, uint8_t attr,
char sep);
/* 08Ch LP_PRINT_LN6 — like bios_writeattr_stop(), attribute unchanged. */
void bios_write_stop(const char *s, uint8_t maxlen, char sep);
/* 08Dh LP_CLS_WIN2 — like bios_clearwin(), but fills with `fillch`
* instead of a space. */
void bios_clearwin_ch(uint8_t row, uint8_t col, uint8_t height,
uint8_t width, uint8_t attr, char fillch);
#endif
+100 -6
View File
@@ -38,12 +38,60 @@
char kbhit (void);
char getch (void);
char getche(void);
/* Extended-key reader. Returns (scan << 8) | ascii. Plain ASCII keys
* have ascii in the low byte; extended keys (arrows / F1..F12 /
* PgUp/PgDn / Home / End / Ins / Del) carry ascii == 0 and the
* KEY_* code in the high byte. */
uint16_t getkey(void);
/* Scan codes returned in the high byte of getkey() when the low byte
* (ASCII) is 0. Empirically verified in MAME — the ProgrammerManual.txt
* "positional code" column is misleading; BIOS returns IBM-style codes
* for the F-keys and a "5N + numpad-position" pattern for the cursor /
* editing keys.
*
* Verified 2026-06-04 by reading raw getkey() output. */
#define KEY_F1 0x3B
#define KEY_F2 0x3C
#define KEY_F3 0x3D
#define KEY_F4 0x3E
#define KEY_F5 0x3F
#define KEY_F6 0x40
#define KEY_F7 0x41
#define KEY_F8 0x42
#define KEY_F9 0x43
#define KEY_F10 0x44
#define KEY_F11 0x45 /* not verified */
#define KEY_F12 0x46 /* not verified */
#define KEY_END 0x51
#define KEY_DOWN 0x52
#define KEY_PGDN 0x53
#define KEY_LEFT 0x54
#define KEY_RIGHT 0x56
#define KEY_HOME 0x57
#define KEY_UP 0x58
#define KEY_PGUP 0x59
#define KEY_INS 0x50 /* numpad 0; not verified */
#define KEY_DEL 0x55 /* numpad 5/.; not verified */
typedef union {
uint16_t value;
struct {
uint8_t low;
uint8_t high;
} byte;
} two_bytes;
char putch (char c);
char cputs (const char *s);
int cprintf(const char *fmt, ...);
void clrscr(void);
void gotoxy(uint8_t x, uint8_t y);
/* Solid-C compatibility helpers. */
#define home() gotoxy(0, 0)
#define inp(port) z80_inp(port)
@@ -63,6 +111,8 @@ uint8_t wherex (void);
uint8_t wherey (void);
uint16_t wherexy(void); // high byte = Y, low byte = X coords.
void scroll(uint8_t, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t);
/* Direct character/attribute screen access (ESTEX $57 / $58).
* wrchar — write char + attribute at (x, y); does NOT advance the cursor
* and does NOT interpret control characters. Useful for
@@ -86,8 +136,8 @@ void clrscr_attr(uint8_t attr);
#define TEXT_MODE_40x32 0x02
#define TEXT_MODE_80x32 0x03
uint8_t get_videotextmode(void);
int set_videotextmode(uint8_t mode); /* 0 OK, -1 + errno on bad mode */
uint8_t gettextmode(void);
int settextmode(uint8_t mode); /* 0 OK, -1 + errno on bad mode */
/* ------------------------------------------------------------------ *
* Text-output attribute (used by the conio set: putch / cputs / cprintf).
@@ -143,12 +193,56 @@ uint8_t get_putch_raw_mode(void);
* Colour order is standard CGA / Borland-conio.h. Constants 0..7 are
* usable for both fg and bg; 8..15 are foreground-only. */
enum {
COLOR_BLACK = 0, COLOR_BLUE, COLOR_GREEN, COLOR_CYAN,
COLOR_RED, COLOR_MAGENTA, COLOR_BROWN, COLOR_LIGHTGRAY,
COLOR_DARKGRAY, COLOR_LIGHTBLUE, COLOR_LIGHTGREEN, COLOR_LIGHTCYAN,
COLOR_LIGHTRED, COLOR_LIGHTMAGENTA, COLOR_YELLOW, COLOR_WHITE
COLOR_BLACK = 0,
COLOR_BLUE,
COLOR_GREEN,
COLOR_CYAN,
COLOR_RED,
COLOR_MAGENTA,
COLOR_BROWN,
COLOR_LIGHTGRAY,
COLOR_DARKGRAY,
COLOR_LIGHTBLUE,
COLOR_LIGHTGREEN,
COLOR_LIGHTCYAN,
COLOR_LIGHTRED,
COLOR_LIGHTMAGENTA,
COLOR_YELLOW,
COLOR_WHITE
};
#define COLOR_BLINK 0x80u
#define COLOR(fg, bg) ((uint8_t)((((bg) & 0x07) << 4) | ((fg) & 0x0F)))
/* Text-mode palette. The 16 logical CGA colours seen by COLOR(fg, bg)
* actually live in four 256-entry hardware palette planes indexed by the
* full 8-bit attribute byte:
*
* TEXT_PAL_PAPER — background colour, non-blink phase
* TEXT_PAL_INK — foreground colour, non-blink phase
* TEXT_PAL_BLINK_PAPER — background colour during the blink half-cycle
* TEXT_PAL_BLINK_INK — foreground colour during the blink half-cycle
*
* For non-blinking attributes (bit 7 = 0) all four planes display the
* same colours, so writing to PAPER/INK is enough. For blinking attrs
* (bit 7 = 1) the renderer alternates between the non-blink and blink
* planes — that's how flash is implemented in hardware.
*
* These wrappers add 4 to the plane index and forward to the low-level
* <palette.h> API (pal_load / pal_set_color / pal_get / pal_get_color).
* Use text_pal_reset() to restore the system default CGA palette. */
#define TEXT_PAL_PAPER 0
#define TEXT_PAL_INK 1
#define TEXT_PAL_BLINK_PAPER 2
#define TEXT_PAL_BLINK_INK 3
void text_pal_load (uint8_t plane, uint8_t start, uint8_t count,
const uint8_t *bgr0);
void text_pal_set_color(uint8_t plane, uint8_t attr,
uint8_t r, uint8_t g, uint8_t b);
void text_pal_get (uint8_t plane, uint8_t start, uint8_t count,
uint8_t *bgr0);
void text_pal_get_color(uint8_t plane, uint8_t attr,
uint8_t *r, uint8_t *g, uint8_t *b);
void text_pal_reset (void);
#endif
+15 -7
View File
@@ -16,14 +16,22 @@
#ifndef FCNTL_H
#define FCNTL_H
#define O_RDONLY 0
#define O_WRONLY 1
#define O_RDWR 2
#ifndef _STD_SEEK_
#define _STD_SEEK_
/* constants to be used as 3rd argument for "fseek" function */
#define SEEK_SET 0
#define SEEK_CUR 1
#define SEEK_END 2
#endif
#define O_CREAT 0x040
#define O_EXCL 0x080
#define O_TRUNC 0x200
#define O_APPEND 0x400
/* Definition "open flags" */
#define O_WRONLY 0x01 /* 0 file write only */
#define O_RDONLY 0x02 /* 1 file read only */
#define O_RDWR 0x03 /* 1,0 file read/write */
#define O_TRUNC 0x04 /* 2 open with truncation */
#define O_CREAT 0x08 /* 3 create and open file */
#define O_EXCL 0x10 /* 4 exclusive open */
#define O_APPEND 0x20 /* 5 to end of file */
int open (const char *path, int flags);
int creat(const char *path, int mode); /* mode arg ignored on Sprinter */
+11
View File
@@ -145,4 +145,15 @@ void gfx_pal_load(uint8_t pal_num, uint8_t start, uint8_t count,
void gfx_pal_set (uint8_t pal_num, uint8_t idx,
uint8_t r, uint8_t g, uint8_t b);
/* Read a contiguous block of entries back from a graphics palette. */
void gfx_pal_get (uint8_t pal_num, uint8_t start, uint8_t count,
uint8_t *data);
/* Read one entry into R, G, B pointers (any may be NULL). */
void gfx_pal_get_color(uint8_t pal_num, uint8_t idx,
uint8_t *r, uint8_t *g, uint8_t *b);
/* Restore the system default graphics palette (BIOS $A6, type=1). */
void gfx_pal_reset(void);
#endif
+89
View File
@@ -0,0 +1,89 @@
/*
* palette.h — Sprinter palette (low-level).
*
* The Sprinter has eight 256-colour palette pages, shared by graphics and
* text modes:
*
* pal_num 0..3 — graphics palettes. Each character cell selects which
* of the four it uses through bits 7..6 of its mode byte.
* Used by graphics modes 0x81 / 0x82.
* pal_num 4..7 — text-mode planes — together they form the colour table
* of the text-mode attribute byte:
* 4 = paper (background, non-blink phase)
* 5 = ink (foreground, non-blink phase)
* 6 = paper-blink (background during blink half-cycle)
* 7 = ink-blink (foreground during blink half-cycle)
* Each plane holds 256 BGR colours indexed directly by
* the 8-bit attribute byte of the cell.
*
* Entry format — 4 bytes per colour, in Blue-Green-Red-pad order; the pad
* byte is reserved and must be 0.
*
* Notes on the underlying BIOS ($A4):
* - `count = 0` means 256 entries (full plane).
* - On write, data is AND-masked against `pal_mask` before reaching
* VRAM. pal_load/pal_set_color hard-code mask = 0xFF (no masking).
*
* Blink semantics (text planes):
* The hardware constantly alternates between planes 4↔6 (paper) and
* 5↔7 (ink). To DISABLE blink everywhere in IBM-CGA style, copy plane
* 4 → 6 and 5 → 7 (so both phases show the same colour). To ENABLE
* ZX-Spectrum style flash, swap entries 6 ↔ 7 for the desired attrs.
* The system default (PAL_CGA) leaves flash visible only for attribute
* bytes with bit 7 set.
*
* Backed by BIOS PIC_SET_PAL/PIC_GET_PAL ($A4) and SET_PAL_INIT ($A6).
* Higher-level wrappers live in <conio.h> (text_pal_*) and <gfx.h>
* (gfx_pal_*); use those directly unless you need raw plane control.
*/
#ifndef PALETTE_H
#define PALETTE_H
#include <stdint.h>
/* Default-palette types for pal_reset(). Values match BIOS $A6 B-register. */
#define PAL_GRAPH 1 /* graphics palette (planes 0..3) */
#define PAL_SINCLAIR 2 /* Spectrum palette (text planes 4..7, ZX colours) */
#define PAL_CGA 3 /* CGA text palette (text planes 4..7, IBM CGA) */
/* Load a contiguous block of palette entries.
* pal_num: 0..7 (0..3 graphics, 4..7 text)
* start: first slot (0..255)
* count: number of slots (0 means 256)
* bgr0: pointer to count entries of 4 bytes each: Blue, Green, Red, 0 */
void pal_load(uint8_t pal_num, uint8_t start, uint8_t count,
const uint8_t *bgr0);
/* Read a contiguous block of palette entries back into RAM.
* Same parameter shape as pal_load — bgr0 here is a write buffer of
* count*4 bytes that receives B,G,R,0 quadruples. */
void pal_get (uint8_t pal_num, uint8_t start, uint8_t count,
uint8_t *bgr0);
/* Convenience: set one entry from an RGB triple. */
void pal_set_color(uint8_t pal_num, uint8_t slot,
uint8_t r, uint8_t g, uint8_t b);
/* Convenience: read one entry into R,G,B pointers (any may be NULL). */
void pal_get_color(uint8_t pal_num, uint8_t slot,
uint8_t *r, uint8_t *g, uint8_t *b);
/* Restore a built-in default palette (BIOS $A6 SET_PAL_INIT).
* type: PAL_GRAPH / PAL_SINCLAIR / PAL_CGA.
* Internally:
* PAL_GRAPH → A=0, E=0, B=1 (resets graphics palette 0)
* PAL_SINCLAIR → A=0, E=0, B=2
* PAL_CGA → A=0, E=0, B=3 (resets all text planes 4..7)
*
* Use pal_reset_at() if you need a non-zero page or a non-zero graphics
* palette index. */
void pal_reset(uint8_t type);
/* Full-control variant of pal_reset.
* pal_page: BIOS A register — "palette page" hardware index.
* graph_pal: BIOS E register — for PAL_GRAPH, target palette 0..3.
* type: BIOS B register — PAL_GRAPH / PAL_SINCLAIR / PAL_CGA. */
void pal_reset_at(uint8_t type, uint8_t pal_page, uint8_t graph_pal);
#endif
+28 -4
View File
@@ -47,23 +47,47 @@
* n : 1..255
* ret : blk_id (1..255) on success; 0 on failure with errno set.
* The id is opaque — pass it to mem_get_page() and mem_free_block(). */
uint8_t mem_alloc_pages(uint8_t n);
uint8_t mem_alloc_pages_estex(uint8_t n);
uint8_t mem_alloc_pages_bios(uint8_t n);
/* Release a block previously returned by mem_alloc_pages().
* On error errno is set (e.g. EINVAL for unknown id). Double-free is
* NOT idempotent: the second call sets errno. */
void mem_free_block(uint8_t blk_id);
void mem_free_block_estex(uint8_t blk_id);
void mem_free_block_bios(uint8_t blk_id);
/* Translate (block, page-index) into a physical page number suitable
* for sprinter_page_w1/w2/w3() or the bank_*() helpers below.
* blk_id: from mem_alloc_pages()
* idx : 0..(n-1)
* ret : physical page (1..255) on success; 0 on failure (errno set). */
uint8_t mem_get_page(uint8_t blk_id, uint8_t idx);
uint8_t mem_get_page_bios(uint8_t blk_id, uint8_t idx);
/* Query the EMM allocator state. Both pointers must be non-NULL.
* Cannot fail (no error path). */
void mem_info(uint16_t *total, uint16_t *free_pages);
void mem_info_estex(uint16_t *total, uint16_t *free_pages);
void mem_info_bios(uint16_t *total, uint16_t *free_pages);
#define MEM_MANAGE_MODE_BIOS
#ifdef MEM_MANAGE_MODE_ESTEX
#define mem_alloc_pages mem_alloc_pages_estex
#define mem_free_block mem_free_block_estex
#define mem_info mem_info_estex
#define mem_get_page mem_get_page_bios
#elif defined MEM_MANAGE_MODE_BIOS
#define mem_alloc_pages mem_alloc_pages_bios
#define mem_free_block mem_free_block_bios
#define mem_info mem_info_bios
#define mem_get_page mem_get_page_bios
#endif
/* ===================================================================
* Far-page accessors via window 3 (base 0xC000, port 0xE2)
+1 -1
View File
@@ -30,7 +30,7 @@ int vprintf(const char *, va_list);
int vsprintf(char *, const char *, va_list);
/* puts / putchar / getchar — overridden by our libc to use ESTEX. */
int puts (const char *);
char puts (const char *);
int putchar(int);
int getchar(void);
-29
View File
@@ -1,29 +0,0 @@
/*
* solid_compat.c — Solid-C compatibility helpers that need real code
* (rather than just header macros).
*/
#include <sprinter_compat.h>
#include <ctype.h>
char *strlwr(char *s)
{
char *p = s;
while (*p) {
if (*p >= 'A' && *p <= 'Z') *p += 'a' - 'A';
p++;
}
return s;
}
char *strupr(char *s)
{
char *p = s;
while (*p) {
if (*p >= 'a' && *p <= 'z') *p -= 'a' - 'A';
p++;
}
return s;
}
/* div() comes from SDCC's z80.lib. */
+138
View File
@@ -0,0 +1,138 @@
/*
* mem_alloc_pages / mem_free_block / mem_get_page / mem_info — ESTEX EMM
* wrappers for explicit 16 KB-page allocation.
*
* BIOS $C0 EMM_FN0 (MEMINFO) → HL=total pages, BC=free pages
* BIOS $C2 EMM_FN2 (ALLOC) B=npages → A=block id, CF=err
* BIOS $C3 EMM_FN3 (FREE) A=block id → CF=err
* BIOS $C4 EMM_FN4 (PAGEID) A=blk, B=idx → A=physical page CF=err
* BIOS $C6 EMM_FN6 (IOTEST) A=blk, B=idx → A=physical page CF=err
*
* Pattern: every RST 10h / RST 8 is bracketed with push/pop IX because
* ESTEX/BIOS clobber it and the C caller uses it as a frame pointer.
*/
#include <stdint.h>
#include <sprinter_mem.h>
/*
* Allocate `n` contiguous 16-KB physical pages from the EMM pool.
*
* in: n — 1..255 (number of pages requested).
* out: blk_id (1..255) on success; 0 on failure with errno set.
*
* The returned block id is opaque — pass it to mem_get_page() to obtain
* each physical-page number, and to mem_free_block() when done. Block
* ids start at 1; id 0 is reserved as the "allocation failed" sentinel.
*/
uint8_t mem_alloc_pages_bios(uint8_t n) __naked
{
(void)n;
__asm
;; SDCC single-uint8 arg A on entry; ESTEX GETMEM wants n in B.
push ix
ld b, a
ld c, #0xC2 ; BIOS EMM_FN2
rst #0x08
pop ix
jr c, _alloc_fail
ret ; CF=0 A = blk_id, return as uint8 in A
_alloc_fail:
call __errno_set ; CF=1 A = ESTEX errcode
xor a, a ; return 0 = failure sentinel
ret
__endasm;
}
/*
* Release a block previously returned by mem_alloc_pages().
*
* in: blk_id (1..255).
* out: void; on ESTEX error errno is set (e.g. EINVAL for unknown id).
*
* Idempotent guarantees are NOT provided — freeing the same block twice
* sets errno on the second call. Caller is responsible for tracking
* ownership.
*/
void mem_free_block_bios(uint8_t blk_id) __naked
{
(void)blk_id;
__asm
;; SDCC single-uint8 arg A on entry.
push ix
ld c, #0xC3 ; ESTEX FREEMEM
rst #0x08
pop ix
ret nc ; CF=0 success
jp __errno_set ; CF=1 A = ESTEX errcode; tail-call helper
__endasm;
}
/*
* Translate a (block, page-index) pair into a physical 16-KB page number,
* suitable for OUT to PORT_PAGE_W1/W2/W3 or for bank_*() helpers.
*
* in: blk_id — id returned by mem_alloc_pages().
* idx — 0..(n-1), where n was the count passed to alloc.
* out: physical page number (1..255) on success;
* 0 on failure with errno set (invalid block or idx out of range).
*/
uint8_t mem_get_page_bios(uint8_t blk_id, uint8_t idx) __naked
{
(void)blk_id; (void)idx;
__asm
;; 2-arg uint8/uint8: blk_id A, idx L.
push ix
ld b, l ; BIOS wants idx in B
;; A still has blk_id
ld c, #0xC4 ; BIOS EMM_GETPAGE
rst #0x08
pop ix
ret nc ; CF=0 A = phys page (return value)
;; CF=1 A = errcode; set errno, return 0 as sentinel.
call __errno_set
xor a, a
ret
__endasm;
}
/*
* Query the EMM allocator about its current state.
*
* *total ← number of 16-KB physical pages installed in the system
* *free_pages ← number currently available for allocation
*
* Both pointers must be non-NULL writeable uint16_t locations.
* No error path: ESTEX INFOMEM always succeeds.
*/
void mem_info_bios(uint16_t *total, uint16_t *free_pages) __naked
{
(void)total; (void)free_pages;
__asm
;; HL = total ptr, DE = free_pages ptr on entry.
;; RST 10 clobbers both stash on the stack across the call.
push ix
push hl ; later [SP+2] = total_ptr
push de ; TOS [SP+0] = free_pages_ptr
ld c, #0xC0 ; BIOS INFOMEM HL = total, BC = free
rst #0x08
pop de ; DE = free_pages_ptr
ld a, c
ld (de), a
inc de
ld a, b
ld (de), a ; *free_pages = BC
pop de ; DE = total_ptr
ld a, l
ld (de), a
inc de
ld a, h
ld (de), a ; *total = HL
pop ix
ret
__endasm;
}
+6 -22
View File
@@ -5,7 +5,6 @@
* ESTEX $3C INFOMEM HL=total pages, BC=free pages
* ESTEX $3D GETMEM B=npages A=block id, CF=err
* ESTEX $3E FREEMEM A=block id CF=err
* BIOS $C4 EMM_GETPAGE A=blk, B=idx A=physical page CF=err
*
* Pattern: every RST 10h / RST 8 is bracketed with push/pop IX because
* ESTEX/BIOS clobber it and the C caller uses it as a frame pointer.
@@ -24,7 +23,7 @@
* each physical-page number, and to mem_free_block() when done. Block
* ids start at 1; id 0 is reserved as the "allocation failed" sentinel.
*/
uint8_t mem_alloc_pages(uint8_t n) __naked
uint8_t mem_alloc_pages_estex(uint8_t n) __naked
{
(void)n;
__asm
@@ -43,6 +42,7 @@ uint8_t mem_alloc_pages(uint8_t n) __naked
__endasm;
}
/*
* Release a block previously returned by mem_alloc_pages().
*
@@ -53,7 +53,7 @@ uint8_t mem_alloc_pages(uint8_t n) __naked
* sets errno on the second call. Caller is responsible for tracking
* ownership.
*/
void mem_free_block(uint8_t blk_id) __naked
void mem_free_block_estex(uint8_t blk_id) __naked
{
(void)blk_id;
__asm
@@ -67,6 +67,7 @@ void mem_free_block(uint8_t blk_id) __naked
__endasm;
}
/*
* Translate a (block, page-index) pair into a physical 16-KB page number,
* suitable for OUT to PORT_PAGE_W1/W2/W3 or for bank_*() helpers.
@@ -76,24 +77,6 @@ void mem_free_block(uint8_t blk_id) __naked
* out: physical page number (1..255) on success;
* 0 on failure with errno set (invalid block or idx out of range).
*/
uint8_t mem_get_page(uint8_t blk_id, uint8_t idx) __naked
{
(void)blk_id; (void)idx;
__asm
;; 2-arg uint8/uint8: blk_id A, idx L.
push ix
ld b, l ; BIOS wants idx in B
;; A still has blk_id
ld c, #0xC4 ; BIOS EMM_GETPAGE
rst #0x08
pop ix
ret nc ; CF=0 A = phys page (return value)
;; CF=1 A = errcode; set errno, return 0 as sentinel.
call __errno_set
xor a, a
ret
__endasm;
}
/*
* Query the EMM allocator about its current state.
@@ -104,7 +87,7 @@ uint8_t mem_get_page(uint8_t blk_id, uint8_t idx) __naked
* Both pointers must be non-NULL writeable uint16_t locations.
* No error path: ESTEX INFOMEM always succeeds.
*/
void mem_info(uint16_t *total, uint16_t *free_pages) __naked
void mem_info_estex(uint16_t *total, uint16_t *free_pages) __naked
{
(void)total; (void)free_pages;
__asm
@@ -135,3 +118,4 @@ void mem_info(uint16_t *total, uint16_t *free_pages) __naked
ret
__endasm;
}
-6
View File
@@ -17,13 +17,7 @@ int getchar(void) __naked
ld c, #0x30 ; ESTEX WAITKEY
rst #0x10
pop ix
ld a, e ; E = ASCII (already the low byte of our return DE)
or a, a
jr Z, no_ascii
ld d, #0
ret
no_ascii:
ld de, #-1
ret
__endasm;
}
+6 -14
View File
@@ -19,23 +19,15 @@ int putchar(int c) __naked
{
(void)c;
__asm
ld a, l ; SDCC __sdcccall(1) int HL
push ix
ld a, l
cp #0x0A
jr nz, _pc_emit
ld a, #0x0D ; CR before LF
push af
jr nz, cputc
call cputc
ld a, #0x0D
cputc:
ld c, #0x5B
rst #0x10
pop af
ld a, #0x0A
_pc_emit:
push af
ld c, #0x5B
rst #0x10
pop af
pop ix
ld e, a
ld e, l
ld d, #0
ret
__endasm;
+21 -34
View File
@@ -14,17 +14,11 @@
* - Avoid trailing PUTCHAR after PCHARS — empirically that sometimes
* drops the next char. Embed the line ending inside the PCHARS
* buffer instead.
* - Strings longer than the buffer fall back to per-char putchar so
* we never silently truncate.
*/
#include <stdio.h>
#include <stdint.h>
#define PUTS_BUF_SIZE 256 /* body bytes before CR expansion */
static char puts_buf[PUTS_BUF_SIZE + 3]; /* +3 for trailing CR LF NUL */
static void pchars(const char *s) __naked
{
(void)s;
@@ -37,33 +31,26 @@ static void pchars(const char *s) __naked
__endasm;
}
int puts(const char *s)
char puts(const char *s) __naked
{
uint16_t n = 0;
uint16_t i = 0;
while (s[i] && n < PUTS_BUF_SIZE - 1) {
char c = s[i++];
if (c == '\n') {
puts_buf[n++] = '\r';
puts_buf[n++] = '\n';
} else {
puts_buf[n++] = c;
}
}
if (s[i]) {
/* Overflow — char-by-char fallback so we never truncate. */
for (uint16_t k = 0; s[k]; k++)
putchar((unsigned char)s[k]);
putchar('\n');
return 0;
}
puts_buf[n++] = '\r';
puts_buf[n++] = '\n';
puts_buf[n] = 0;
pchars(puts_buf);
return 0;
(void)s;
__asm
puts_:
ld a, (hl)
or a
jr z, fin_
push hl
ld l, a
ld h, #0
call _putchar
pop hl
inc hl
jp puts_
;
fin_:
ld l, #0x0A
ld h, #0
call _putchar
ret
__endasm;
}
-27
View File
@@ -1,27 +0,0 @@
/*
* solid_helpers.c — small Solid-C compatibility helpers.
*
* dec8 / dec16 / dec32 / hex8 / hex16 / hex32 are in dec_hex.c (compact
* asm port from solid-c's STDLIB.ASM, ~150 bytes total — vs ~3-5 KB if
* routed through printf). This file now only holds gets().
*/
#include <stdio.h>
/* ---- gets — dangerous but Solid-C provides it ---------------------- */
char *gets(char *buf)
{
int i = 0;
int c;
for (;;) {
c = getchar();
if (c == EOF) {
if (i == 0) return 0;
break;
}
if (c == '\n' || c == '\r') break;
buf[i++] = (char)c;
}
buf[i] = 0;
return buf;
}
+23
View File
@@ -0,0 +1,23 @@
/*
* solid_compat.c — Solid-C compatibility helpers that need real code
* (rather than just header macros).
*
* CP866 Cyrillic support: strlwr/strupr handle uppercase/lowercase
* conversion for both Latin and Cyrillic characters in CP866 code page
* (bytes 0x800xFF).
*/
#include <sprinter_compat.h>
#include <ctype.h>
char *strlwr(char *s)
{
char *p = s;
while (*p) {
if ((*p >= 'A' && *p <= 'Z') || (*p >= 0x80 && *p <= 0x8F)) *p += 'a' - 'A';
else if ((*p >= 0x90 && *p <= 0x9F)) *p += 0x50;
else if ((*p == 0xF0)) *p = 0xF1;
p++;
}
return s;
}
+23
View File
@@ -0,0 +1,23 @@
/*
* solid_compat.c — Solid-C compatibility helpers that need real code
* (rather than just header macros).
*
* CP866 Cyrillic support: strlwr/strupr handle uppercase/lowercase
* conversion for both Latin and Cyrillic characters in CP866 code page
* (bytes 0x800xFF).
*/
#include <sprinter_compat.h>
#include <ctype.h>
char *strupr(char *s)
{
char *p = s;
while (*p) {
if ((*p >= 'a' && *p <= 'z') || (*p >= 0xA0 && *p <= 0xAF)) *p -= 'a' - 'A';
else if ((*p >= 0xE0 && *p <= 0xEF)) *p -= 0x50;
else if ((*p == 0xF1)) *p = 0xF0;
p++;
}
return s;
}
View File
+131
View File
@@ -0,0 +1,131 @@
/*
* palette.c — Sprinter palette (BIOS $A4 PIC_SET_PAL / PIC_GET_PAL,
* BIOS $A6 SET_PAL_INIT).
*
* Direction in $A4 is selected by bit 7 of A:
* A = pal_num → write entries from RAM to VRAM
* A = 0x80 | pal_num → read entries from VRAM into RAM
*
* Lives under libc/video/ because palette control is shared between
* graphics (mode 0x81/0x82) and text (mode 0x03) — pal_num 0..3 for
* graphics, 4..7 for the four text-mode planes. The conio and gfx
* subsystems each ship thin domain-specific wrappers on top.
*/
#include <stdint.h>
#include <palette.h>
/* Statics used to pass parameters to the inline asm — SDCC __sdcccall(1)
* gives us only HL natively, so the rest go through memory. */
static uint8_t pal_num_;
static uint8_t pal_start_;
static uint8_t pal_count_;
static uint16_t pal_data_;
/* ---- $A4 PIC_SET_PAL — write entries to VRAM --------------------- */
void pal_load(uint8_t pal_num, uint8_t start, uint8_t count,
const uint8_t *bgr0)
{
pal_num_ = pal_num;
pal_start_ = start;
pal_count_ = count;
pal_data_ = (uint16_t)(uintptr_t)bgr0;
__asm
push ix
ld a, (_pal_start_)
ld e, a ; E = start
ld a, (_pal_count_)
ld d, a ; D = count (0 256)
ld hl, (_pal_data_) ; HL = data
ld b, #0xFF ; B = mask (no AND)
ld a, (_pal_num_) ; A = palette number (bit7 = 0 write)
ld c, #0xA4 ; BIOS PIC_SET_PAL
rst #0x08
pop ix
__endasm;
}
/* ---- $A4 PIC_GET_PAL — read entries from VRAM -------------------- *
* Same function id; bit 7 of A flips it to read mode. */
void pal_get(uint8_t pal_num, uint8_t start, uint8_t count, uint8_t *bgr0)
{
pal_num_ = (uint8_t)(pal_num | 0x80); /* bit 7 = read */
pal_start_ = start;
pal_count_ = count;
pal_data_ = (uint16_t)(uintptr_t)bgr0;
__asm
push ix
ld a, (_pal_start_)
ld e, a ; E = start
ld a, (_pal_count_)
ld d, a ; D = count (0 256)
ld hl, (_pal_data_) ; HL = buffer
ld b, #0xFF ; B = mask
ld a, (_pal_num_) ; A = 0x80 | pal_num read
ld c, #0xA4 ; BIOS PIC_GET_PAL (= $A4)
rst #0x08
pop ix
__endasm;
}
/* ---- one-colour helpers ------------------------------------------ */
void pal_set_color(uint8_t pal_num, uint8_t slot,
uint8_t r, uint8_t g, uint8_t b)
{
uint8_t entry[4];
entry[0] = b;
entry[1] = g;
entry[2] = r;
entry[3] = 0;
pal_load(pal_num, slot, 1, entry);
}
void pal_get_color(uint8_t pal_num, uint8_t slot,
uint8_t *r, uint8_t *g, uint8_t *b)
{
uint8_t entry[4];
pal_get(pal_num, slot, 1, entry);
if (b) *b = entry[0];
if (g) *g = entry[1];
if (r) *r = entry[2];
}
/* ---- $A6 SET_PAL_INIT — restore a built-in default palette ------- *
* Signature: A = pal_page, E = graphics palette index (0..3), B = type.
* Type is one of PAL_GRAPH (1) / PAL_SINCLAIR (2) / PAL_CGA (3). */
static uint8_t reset_page_;
static uint8_t reset_graph_;
static uint8_t reset_type_;
void pal_reset_at(uint8_t type, uint8_t pal_page, uint8_t graph_pal)
{
reset_type_ = type;
reset_page_ = pal_page;
reset_graph_ = graph_pal;
__asm
push ix
ld a, (_reset_graph_)
ld e, a ; E = graphics palette index (0..3)
ld a, (_reset_type_)
ld b, a ; B = type (1=GRAPH, 2=SINCLAIR, 3=CGA)
ld a, (_reset_page_) ; A = palette page (last A is needed)
ld c, #0xA6 ; BIOS SET_PAL_INIT
rst #0x08
pop ix
__endasm;
}
void pal_reset(uint8_t type)
{
pal_reset_at(type, 4, 0);
pal_reset_at(type, 5, 0);
pal_reset_at(type, 6, 0);
pal_reset_at(type, 7, 0);
}
@@ -2,4 +2,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := argv
include $(PROJ_ROOT)/examples/example.mk
include $(PROJ_ROOT)/app.mk
@@ -1,3 +1,3 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := assrtest
include $(PROJ_ROOT)/examples/example.mk
include $(PROJ_ROOT)/app.mk
@@ -2,4 +2,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := attrprob
include $(PROJ_ROOT)/examples/example.mk
include $(PROJ_ROOT)/app.mk
@@ -8,4 +8,4 @@ PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := banked
MEMORY := huge
EXTRA_FLAGS := --bank 1=bank1.c --bank 2=bank2.c
include $(PROJ_ROOT)/examples/example.mk
include $(PROJ_ROOT)/app.mk
@@ -9,4 +9,4 @@ PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := bankedbg
MEMORY := big
EXTRA_FLAGS := --bank 1=bank1.c --bank 2=bank2.c
include $(PROJ_ROOT)/examples/example.mk
include $(PROJ_ROOT)/app.mk
@@ -15,3 +15,17 @@ void bank1_func(int x) __banked
putchar('0' + x % 10);
putchar('\n');
}
void bank1_func2(int x) __banked
{
(void)x;
puts("BANK1-2: hello from a banked function (W1)!");
puts("BANK1-2: window 1 phys page = ");
hex8(_io_page_w1); /* should be BANK1's phys page */
putchar('\n');
putchar('1');
putchar('=');
putchar('0' + (x / 10) % 10);
putchar('0' + x % 10);
putchar('\n');
}
@@ -2,6 +2,8 @@
#include <stdint.h>
#include <sprinter.h>
void bank1_func2(int x) __banked;
void bank2_func(int x) __banked
{
(void)x;
@@ -15,4 +17,6 @@ void bank2_func(int x) __banked
putchar('0' + (x / 10) % 10);
putchar('0' + x % 10);
putchar('\n');
bank1_func2(10);
}
@@ -9,4 +9,4 @@ PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := banklocl
MEMORY := huge
EXTRA_FLAGS := --bank 1=bank1.c --mkexe -p --mkexe 0
include $(PROJ_ROOT)/examples/example.mk
include $(PROJ_ROOT)/app.mk
+11
View File
@@ -0,0 +1,11 @@
# Build banked.exe — HUGE memory mode (small + banked code in W3).
#
# Layout:
# CODE/HOME at 0x4100 (W1), DATA at 0x8000 (W2), banks at 0x{N}C000 (W3).
# crt0_banked.s loads N banks from the .EXE before calling main.
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := banked
MEMORY := huge
EXTRA_FLAGS := --bank 1=bank1.c --bank 2=bank2.c
include $(PROJ_ROOT)/app.mk
+18
View File
@@ -0,0 +1,18 @@
#include <stdio.h>
#include <stdint.h>
#include <sprinter.h>
uint16_t bank1var = 0;
extern uint16_t bank2var;
void bank1_func(void) __banked
{
printf("BANK1: hello from the first bank, phys page = 0x%02X, var = %u\n", _io_page_w3, bank1var);
}
void bank1_func2(uint16_t x) __banked
{
printf("BANK1: hello from the first bank, phys page = 0x%02X, set var in SECOND bank to %u\n", _io_page_w3, x);
bank2var = x;
}
+11
View File
@@ -0,0 +1,11 @@
#include <stdio.h>
#include <stdint.h>
#include <sprinter.h>
uint16_t bank2var = 0;
void bank2_func(void) __banked
{
printf("BANK2: hello from the second bank, phys page = 0x%02X, var = %u\n", _io_page_w3, bank2var);
}
+34
View File
@@ -0,0 +1,34 @@
#include <stdio.h>
#include <stdint.h>
#include <sprinter.h>
/* crt0_banked.s reads this constant to know how many banks to load. */
const uint8_t n_banks = 2;
void bank1_func(void) __banked;
void bank2_func(void) __banked;
void bank1_func2(uint16_t) __banked;
extern uint16_t bank1var;
extern uint16_t bank2var;
extern uint8_t bank_pages[]; /* filled by crt0_banked.s */
int main(void)
{
bank1var = 1;
bank2var = 2;
puts("HOME: program start.");
printf("HOME: window 3 phys page = 0x%02X\n", _io_page_w3);
printf("HOME: bank_pages[1] = 0x%02X\n", bank_pages[1]);
printf("HOME: bank_pages[2] = 0x%02X\n", bank_pages[2]);
bank1_func();
bank2_func();
bank1_func2(25);
bank2_func();
puts("Press any key to exit...");
(void)getchar();
return 0;
}
+5
View File
@@ -0,0 +1,5 @@
# Build bios_text.exe — exercises <bios/text.h>, TINY memory mode.
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := bios_text
include $(PROJ_ROOT)/app.mk
+110
View File
@@ -0,0 +1,110 @@
#include <stdio.h>
#include <conio.h>
#include <bios/text.h>
/*
* Exercises every BIOS text wrapper from <bios/text.h>:
* bios_set_place / bios_get_place
* bios_fillchar / bios_fillchar_noattr / bios_fillattr
* bios_writeattr / bios_write
* bios_writeattr_until / bios_write_until (pad to length)
* bios_writeattr_stop / bios_write_stop (stop at separator)
* bios_clearwin / bios_clearwin_ch
* bios_scrollwin
*
* `s` below is a static buffer (not a string literal) so its address is
* always inside window 2, as the BIOS print routines require.
*/
static char s[16];
int main(void)
{
clrscr();
/* Step 0 check: does LP_PRINT_LN advance the BIOS "place" cursor
* by the number of characters printed, or leave it untouched?
* "Hello" is 5 chars -> if place advances, before=(20,5),
* after=(20,10); if not, after == before. */
s[0]='H'; s[1]='e'; s[2]='l'; s[3]='l'; s[4]='o';
bios_set_place(20, 5);
{
uint16_t before = bios_get_place();
bios_writeattr(s, 5, COLOR(COLOR_WHITE, COLOR_BLACK));
uint16_t after = bios_get_place();
gotoxy(2, 0);
textattr(0x0F);
printf("STEP0: before row=%u col=%u\n", before >> 8, before & 0xFF);
printf("STEP0: after row=%u col=%u\n", after >> 8, after & 0xFF);
printf("STEP0: expect after col=10 if place advances,\n");
printf(" col=5 if it does NOT advance.\n");
}
(void)getch();
bios_set_place(1, 2);
bios_fillcharattr('*', COLOR(COLOR_YELLOW, COLOR_BLUE), 10);
(void)getch();
bios_set_place(2, 2);
textattr(COLOR(COLOR_WHITE, COLOR_BLACK));
cputs("attr already on screen ->");
(void)getch();
bios_set_place(2, 27);
bios_fillchar('#', 10);
(void)getch();
bios_set_place(3, 2);
bios_fillattr(COLOR(COLOR_BLACK, COLOR_LIGHTGREEN), 20);
(void)getch();
s[0]='H'; s[1]='e'; s[2]='l'; s[3]='l'; s[4]='o';
bios_set_place(5, 2);
bios_writeattr(s, 5, COLOR(COLOR_LIGHTRED, COLOR_BLACK));
(void)getch();
bios_set_place(6, 2);
bios_write(s, 5);
(void)getch();
s[0]='A'; s[1]='B'; s[2]=':'; s[3]='X'; s[4]='Y'; s[5]='Z';
bios_set_place(8, 2);
bios_writeattr_until(s, 12, COLOR(COLOR_CYAN, COLOR_BLACK), ':');
(void)getch();
bios_set_place(9, 2);
bios_write_until(s, 12, ':');
(void)getch();
bios_set_place(10, 2);
bios_writeattr_stop(s, 12, COLOR(COLOR_MAGENTA, COLOR_BLACK), ':');
(void)getch();
bios_set_place(11, 2);
bios_write_stop(s, 12, ':');
(void)getch();
bios_clearwin(13, 2, 4, 20, COLOR(COLOR_BLACK, COLOR_RED));
bios_clearwin_ch(13, 25, 4, 20, COLOR(COLOR_WHITE, COLOR_BLUE), '.');
(void)getch();
bios_set_place(18, 2);
(void)getch();
cputs("place test ->");
{
uint16_t place = bios_get_place();
printf(" row=%u col=%u\n", place >> 8, place & 0xFF);
}
(void)getch();
gotoxy(2, 20);
for (int i = 0; i < 6; i++) {
textattr(0x0F);
printf("scroll line %d\n", i);
}
(void)getch();
bios_scrollwin(BIOS_SCROLL_UP, 20, 3);
(void)getch();
return 0;
}
+1 -1
View File
@@ -3,4 +3,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := cat
EXTRA_DATA := test.txt
include $(PROJ_ROOT)/examples/example.mk
include $(PROJ_ROOT)/app.mk
@@ -2,4 +2,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := conio
include $(PROJ_ROOT)/examples/example.mk
include $(PROJ_ROOT)/app.mk
@@ -2,4 +2,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := conio2
include $(PROJ_ROOT)/examples/example.mk
include $(PROJ_ROOT)/app.mk
@@ -1,6 +1,7 @@
#include <stdio.h>
#include <conio.h>
#include <unistd.h> /* sleep */
#include <sprinter_exit.h>
/*
* Exercises every conio function:
@@ -16,6 +17,8 @@
* (0,0), (79,0), (0,31), (79,31).
*/
extern two_bytes pc_place;
int main(void)
{
/* 1. clrscr + diagnostic probes + a centered banner.
@@ -27,9 +30,16 @@ int main(void)
*/
clrscr();
gotoxy(0, 0); putch('0');
gotoxy(1, 0);
textattr(0x07);
putch('0');
textattr(0x70);
gotoxy(1, 0); putch('1');
putch('1');
// gotoxy(10, 20);
printf("X = %u, Y = %u\n", pc_place.byte.low, pc_place.byte.high);
uint8_t x = wherex();
uint8_t y = wherey();
printf("X = %u, Y = %u\n", x, y);
set_putch_raw_mode(1);
@@ -51,11 +61,23 @@ int main(void)
}
}
set_text_attr(0x011D);
set_putch_raw_mode(0);
gotoxy(0,22);
set_text_attr(0x000E);
// cputs("Test message line");
cputs("Test message line 1\nTest message line2\rline3\n\r\n\r1234\t5678\b90");
cputs("Test message line 1\nTest message line2\rline3\n\r1234\t5678\b90\n\r");
set_putch_raw_mode(1);
gotoxy(0,25);
cputs("Test message line 1\nTest message line2\rline3\n\r1234\t5678\b90\n\r");
set_text_attr(0x001D);
set_putch_raw_mode(0);
gotoxy(0,28);
cputs("Test message line 1\nTest message line2\rline3\n\r1234\t5678\b90\n\r");
set_putch_raw_mode(1);
gotoxy(0,31);
cputs("Test message line 1\nTest message line2\rline3\n\r1234\t5678\b90\n\r");
(void)getch();
@@ -1,3 +1,3 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := dec_test
include $(PROJ_ROOT)/examples/example.mk
include $(PROJ_ROOT)/app.mk
@@ -2,4 +2,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := errno
include $(PROJ_ROOT)/examples/example.mk
include $(PROJ_ROOT)/app.mk
@@ -1,3 +1,3 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := filetest
include $(PROJ_ROOT)/examples/example.mk
include $(PROJ_ROOT)/app.mk
+5
View File
@@ -0,0 +1,5 @@
# Build cat.exe — uses lib/sprinter.lib in TINY memory mode.
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := gets
include $(PROJ_ROOT)/app.mk
+19
View File
@@ -0,0 +1,19 @@
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
char buff [256];
puts("--- gets test ---");
gets(buff);
puts("");
puts("loaded string:");
puts(buff);
puts("done");
(void)getchar();
return 0;
}
@@ -2,4 +2,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := gfx_d16
include $(PROJ_ROOT)/examples/example.mk
include $(PROJ_ROOT)/app.mk
@@ -1,3 +1,3 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := gfx_dbuf
include $(PROJ_ROOT)/examples/example.mk
include $(PROJ_ROOT)/app.mk

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