Volkov: добавить Sprinter Commander

Реализовать двухпанельный Commander от платформенного PoC до этапов P6-P20: EMM-каталог, сортировку и выбор, операции с файлами и деревьями, транзакционное копирование, метаданные, политику конфликтов и предварительную проверку свободного места.

Добавить проектную документацию, HDD/MAME-сценарии и проверенные артефакты. Расширить libc операцией bank_write_page, исправлением режима O_RDONLY и связанными регрессионными проверками.
This commit is contained in:
2026-09-10 10:45:30 +03:00
parent 05bcd8197e
commit 8e389c03f8
870 changed files with 21310 additions and 25 deletions
@@ -0,0 +1,324 @@
# Sprinter Commander: платформенные пробы P1
Статус: MAME 0.287 / BIOS 3.06 — `PASS`; проверка на реальном Sprinter ожидается
Связанные документы:
[требования](commander-requirements.md),
[архитектура](commander-architecture.md),
[клавиши](commander-keymap.md),
[roadmap](commander-roadmap.md),
[экранный backend P2](p2-screen-backend.md)
## 1. Назначение
Программа `p1probe.exe` объединяет платформенные проверки, которые должны
быть завершены до реализации UI Commander. Она не является ранней версией
Commander и не определяет его пользовательский интерфейс.
Исходники находятся в `tests/p1_platform/`.
Вторая программа, `p1child.exe`, используется только для проверки ESTEX
`EXEC`, возврата к родителю и кода завершения.
## 2. Ограничения безопасности
- Пробы не создают, не изменяют и не удаляют файлы.
- Каталожная проба выполняет только `getcwd`, `chdir(".")` и
`ffirst/fnext`.
- EMM-проба пишет только в выделенный ей блок из трёх страниц.
- Screen-проба пишет только в отдельную выделенную EMM-страницу.
- Дочерняя программа меняет видеорежим, но не пишет на диск.
- Проба не открывает файловые дескрипторы приложения напрямую.
- MAME не запускается целями `make` и `make all`.
Первый MAME-прогон выполнен 6 сентября 2026 года после отдельного согласования.
Все запуски использовали автоматически пересоздаваемый `mc.img`; исходные
дисковые образы не изменялись программами-пробами.
## 3. Состав
| Файл | Назначение |
|---|---|
| `p1probe.c` | меню, общие сообщения, последовательность тестов |
| `p1_sys.c` | app-local wrappers DSKINFO, EXEC, WAIT, WINREST |
| `p1_emm.c` | выделение трёх страниц и проверка границ |
| `p1_dir.c` | CURDIR/CHDIR/F_FIRST/F_NEXT и DSKINFO output |
| `p1_screen.c` | полный и частичный WINREST с ненулевым offset |
| `p1_keyboard.c` | вывод `ascii/scan/live modifiers` |
| `p1_exec_test.c` | два режима EXEC и проверка состояния родителя |
| `p1child.c` | дочерний EXE, меняющий режим на 40x32 и возвращающий `0x5A` |
| `mame_dump_keys.lua` | вывод карты полей AT-клавиатуры MAME |
| `mame_p1_keyboard.lua` | воспроизводимый ввод стрелок, F1–F10 и сочетаний |
| `Makefile` | сборка обоих EXE |
## 4. Сборка
```sh
cd applications/Volkov/tests/p1_platform
make
```
Диагностическая программа собирается в режиме `small --safe`. Это сделано
намеренно: P1 проверяет платформенные ABI и не должен одновременно отлаживать
банковую topology Commander. Сборка `big --safe` является критерием P2.
Дочерняя программа собирается как `tiny --safe`.
### Результат текущей сборки
`p1probe.exe`:
- `_CODE`: 11 062 байта;
- данные: 2 741 байт;
- файл EXE: 11 835 байт;
- heap после статики: 17 429 байт;
- стек: 1 279 байт.
`p1child.exe`:
- `_CODE`: 3 907 байт;
- данные: 849 байт;
- файл EXE: 4 453 байта;
- heap после статики: 10 092 байта;
- стек: 1 279 байт.
Компиляция выполнена без ошибок. Значения относятся к текущей диагностической
сборке и не являются бюджетом Commander.
## 5. Статическая проверка ABI
### 5.1. DSKINFO
Сигнатура:
```c
int p1_disk_info(uint8_t disk, P1DiskInfo *out);
```
Сгенерированный вызов передаёт `disk` в A, `out` в DE. Wrapper сохраняет IX,
а после RST сохраняет:
- A — sectors per cluster;
- HL — total clusters;
- DE — free clusters;
- BC — bytes per sector.
Размер `P1DiskInfo` проверяется typedef-assert и равен семи байтам.
### 5.2. EXEC
Сигнатура намеренно имеет 8-битный аргумент первым:
```c
int p1_exec(uint8_t path_mode, const char *path);
```
Для этого порядка SDCC передаёт `path_mode` в A и `path` в DE. Wrapper
перекладывает их в B и HL соответственно. Вариант
`p1_exec(const char *path, uint8_t mode)` был отвергнут после просмотра
сгенерированного ASM: второй 8-битный аргумент SDCC помещал на стек, а не в
DE.
На успехе A сохраняется как `p1_exec_exit_code`; на ошибке код из A проходит
через `__errno_set`.
### 5.3. WINREST
Wrapper повторяет уже подтверждённую раскладку `tests/winrest` и `mdview2`:
```text
A row
L column
SP+2 height
SP+3 width
SP+4 physical page
SP+5..6 offset in 16-KB page
```
IX сохраняется, перед RST устанавливается в `0xC000 + offset`. Сгенерированный
вызов проверен для полного экрана `32x80` и окна `2x20` с offset `0x2000`.
## 6. Меню `p1probe.exe`
| Клавиша | Проба | Автоматическая оценка |
|---|---|---|
| `1` | Каталог и F_FIRST/F_NEXT | да, плюс наблюдаемые коды |
| `2` | EMM: три страницы | да |
| `3` | WINREST full/partial | только завершение; изображение оценивается визуально |
| `4` | Клавиатурные коды | сбор наблюдений |
| `5` | EXEC и возврат | да для режима B=1 |
| `6` | DSKINFO текущего диска | да |
| `A` | Пробы 1, 2 и 6 | да |
| `0`/`Esc` | Выход | — |
## 7. Ожидаемые проверки
### 7.1. Каталог
Проба дважды перечисляет каталог по маскам `*.*` и `*`, печатает первые 20
записей и итоговые значения:
- число записей;
- наличие `.`;
- наличие `..`;
- `errno` последнего `fnext()`;
- совпадение результатов двух масок;
- сохранение CWD после `chdir(".")`.
Фактический результат DSS различается: `*.*` вернул обе записи с расширением,
а `*` завершился как пустой список с `ENOENT`. Поэтому `*.*` принят как
обязательный шаблон полного scan; совпадение масок не предполагается.
MAME-прогон установил фактическое завершение перечисления: `fnext()`
возвращает ошибку с `errno=3` (`ENOENT`). Код `0x0F` из старого описания DSS
для этой операции не подтверждён. Проба после измерения принимает только
`ENOENT`, чтобы возможная смена поведения не осталась незамеченной.
### 7.2. EMM
Проверяются:
- `mem_alloc_pages(3)`;
- три различных физических номера;
- первые 64 байта каждой страницы;
- последние 64 байта каждой страницы;
- независимые patterns страниц;
- восстановление `mem_info.free_pages` после освобождения блока.
### 7.3. Screen/WINREST
Первый вызов должен показать полный экран 80x32 с рамкой и цветными строками.
После клавиши второй вызов должен наложить окно 20x2 в позиции row 14,
column 30. Данные второго окна лежат по offset `0x2000`, поэтому тест
одновременно проверяет корректность IX.
Тест не открывает окно через BIOS: ESTEX WINREST здесь только копирует
прямоугольник в текущий текстовый экран. Повтор со снимками в `t=20` и `t=24`
без промежуточных вызовов подтвердил, что фон вне прямоугольника сохранён.
Первая версия pattern использовала арифметически полученные атрибуты
`0x11..0x17`. Верхняя граница `0x11` была обычным blue-on-blue, а часть
остальных сочетаний выглядела неоднозначно в разных моментах палитрового
цикла. Замечание о синих символах на синем фоне подтвердилось; raw-арифметика
атрибутов в UI запрещается.
После замены на именованные `COLOR(LIGHTBLUE..WHITE, BLUE)` полный экран и
частичное окно были видимы на кадрах `t=20` и `t=24`. Повторная проверка
исходных PNG установила, что их декодированные RGB-пиксели полностью
идентичны. Прежнее сообщение о чередовании P2 было ошибкой встроенного
предпросмотра, а не поведением MAME.
Отдельная проба P2.2 сохранила, установила, прочитала обратно и восстановила
шесть атрибутов во всех четырёх планах; многоцветная сцена осталась стабильной.
Строка P1-SCR-03 ниже подтверждена как `PASS`. Детали — в
[результатах P2.2](p2-palette-results.md).
### 7.4. Keyboard
Для 24 событий печатаются:
```text
ascii, scan, полное kbd_mod_state, compact Shift/Ctrl/Alt
```
`Esc` печатается последним и завершает сбор. В автоматическом MAME-сценарии
использован следующий набор из 24 событий:
- стрелки, Home/End, PgUp/PgDn;
- F1F10;
- Insert/Delete;
- Ctrl+R;
- Ctrl+PgUp;
- Alt+F1;
- Esc.
### 7.5. EXEC
Выполняются два случая:
1. `B=0`, `P1CHILD.EXE` — наблюдение short-name/PATH semantics.
2. `B=1`, `.\\P1CHILD.EXE` — обязательный случай PoC.
Дочерняя программа:
- переключается в 40x32;
- показывает свои W1/W2/W3 и CWD;
- ждёт клавишу;
- возвращает `0x5A`.
После возврата родитель проверяет:
- результат непосредственного EXEC;
- результат WAIT;
- восстановление W1/W2/W3;
- восстановление CWD;
- возможность вернуть 80x32 и продолжить работу.
Ошибка случая B=0 записывается как наблюдение и не проваливает PoC. Ошибка
B=1 проваливает тест.
## 8. Выполненный порядок первого прогона
1. `2` — EMM.
2. `6` — DSKINFO.
3. `1` — каталог.
4. `3` — WINREST.
5. `4` — клавиатура.
6. `5` — EXEC последним.
7. Повторно `2` в том же процессе после EXEC, чтобы проверить EMM.
8. Возврат в меню; независимые запуски дополнительно подтвердили повторный
старт программы.
Такой порядок сначала проверяет неразрушительные примитивы и оставляет
наиболее сложное переключение контекста на конец.
## 9. Таблица runtime-результатов
Окружение MAME: `mame.arm` 0.287 (`b0c4527c`), машина `sprinter`, BIOS `v3.06`, запуск
`p1probe.exe` с дискеты A:. `PENDING` в аппаратной колонке означает только
отсутствие прогона на реальном Sprinter и не отменяет результата MAME.
| ID | Проверка | MAME 0.287 / BIOS 3.06 | Реальный Sprinter | Артефакт |
|---|---|---|---|---|
| P1-DIR-01 | CWD и `chdir(".")` | PASS | PENDING | [результат](../artifacts/mame-p1/directory/strict-enoent.png) |
| P1-DIR-02 | `*.*` против `*` | PASS: `2` против `0`; полный scan использует только `*.*` | PENDING | [результат](../artifacts/mame-p1/directory/strict-enoent.png) |
| P1-DIR-03 | Код конца F_NEXT | PASS, `ENOENT=3` | PENDING | [строгая проверка](../artifacts/mame-p1/directory/strict-enoent.png) |
| P1-EMM-01 | Блок 3 страницы | PASS, страницы `EF/F0/F1` | PENDING | [результат](../artifacts/mame-p1/emm/result.png) |
| P1-EMM-02 | Границы страниц | PASS | PENDING | [результат](../artifacts/mame-p1/emm/result.png) |
| P1-EMM-03 | Счётчик после free | PASS, `221 → 221` | PENDING | [результат](../artifacts/mame-p1/emm/result.png) |
| P1-SCR-01 | Полный WINREST 80x32 | PASS, визуально | PENDING | [контрастный экран](../artifacts/mame-p1/winrest/full-contrast.png) |
| P1-SCR-02 | Offset WINREST | PASS, row 14 / col 30 / `0x2000` | PENDING | [фон и окно вместе](../artifacts/mame-p1/winrest/partial-contrast-stable.png) |
| P1-SCR-03 | Цветные атрибуты в двух моментах цикла | PASS, RGB кадров идентичен; P2.2 подтвердил 6 атрибутов × 4 плана | PENDING | [кадр 1](../artifacts/mame-p1/winrest/partial-flash-phase.png), [кадр 2](../artifacts/mame-p1/winrest/partial-contrast-stable.png), [P2.2](p2-palette-results.md) |
| P1-SCR-04 | Raw-атрибуты `0x11..0x17` | OBSERVED, raw-арифметика запрещена | PENDING | [неудачный исходный pattern](../artifacts/mame-p1/winrest/partial-phase-hidden.png) |
| P1-KBD-01 | Базовые scan-коды | PASS | PENDING | [таблица](../artifacts/mame-p1/keyboard-commander/sprinter/0002.png) |
| P1-KBD-02 | Ctrl/Alt modifiers | PASS | PENDING | [сочетания](../artifacts/mame-p1/keyboard-commander/sprinter/0003.png) |
| P1-EXE-01 | EXEC B=0 | PASS | PENDING | [результат](../artifacts/mame-p1/exec/result.png) |
| P1-EXE-02 | EXEC B=1 | PASS | PENDING | [результат](../artifacts/mame-p1/exec/result.png) |
| P1-EXE-03 | Exit/WAIT `0x5A` | PASS / PASS | PENDING | [результат](../artifacts/mame-p1/exec/result.png) |
| P1-EXE-04 | Восстановление W1/W2/W3 | PASS, `F3/F2/FF` | PENDING | [результат](../artifacts/mame-p1/exec/result.png) |
| P1-EXE-05 | EMM после двух EXEC | PASS | PENDING | [контроль](../artifacts/mame-p1/exec-then-emm/emm-result.png) |
| P1-DSK-01 | DSKINFO | PASS | PENDING | [результат](../artifacts/mame-p1/dskinfo/result.png) |
### 9.1. Зафиксированные значения
- DSKINFO: 1 сектор на кластер, 512 байт на сектор, 2847 кластеров всего,
2814 свободно.
- EMM до выделения: 256 страниц всего, 221 свободна; после освобождения
снова 221.
- Оба режима EXEC: `rc=0`, `errno=0`, непосредственный exit=`5A`, WAIT=`5A`.
- CWD до и после child: `\`; страницы W1/W2/W3 до и после: `F3/F2/FF`.
- Модифицированная клавиша приходит с установленным битом 7 scan-кода:
Ctrl+R=`93`, Ctrl+PgUp=`D9`, Alt+F1=`BB`. После `scan & 0x7F` получаются
базовые коды `13`, `59`, `3B`; compact modifiers равны `02`, `02`, `04`.
- Esc: `ascii=1B`, `scan=01`.
## 10. Условия завершения P1
MAME-часть P1 завершена: все обязательные строки имеют `PASS`, фактические
клавиатурные коды перенесены в keymap, завершение `fnext()` уточнено,
`EXEC B=1`/WAIT и EMM после child подтверждены, артефакты сохранены.
Проверка на реальном Sprinter остаётся желательной аппаратной валидацией,
но не блокирует переход к P2 и реализацию platform API PoC. Если реальное
железо даст расхождение, оно оформляется отдельным репро и не маскируется
адаптацией UI.