SprPoP: автономное приложение, выделенное из roomtest
Порт PoP переехал в applications/SprPoP — приложение, которое собирается само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной папки. Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT, по умолчанию ../..). applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся архивом закрытых задач, багов и исполненных планов. Скопировано из applications/PoP/roomtest@4b74478. Перенос проверен побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита, все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host- тесты зелёные (15/15). Раскладка: src/ рукописный C (roomtest.c -> sprpop.c) gen/ генерируемые заголовки, в репозитории assets/orig/ оригинальные данные игры, вне репозитория (копирайт) assets/packed/ то, что ложится на диск, в раскладке диска tools/ конверторы; все пути — в одном tools/paths.py build/ выход: exe, каталоги ресурсов, hdd/, промежуточные atl/ Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что пересчитывается каждым make. Автоматика построена на ОТСУТСТВИИ файла, а не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по времени превращалось бы в лотерею. Недостающий ресурс или заголовок чинится сам, рекурсивным вызовом в ветку генерации. Музыка собирается из любого из четырёх наборов записей (make music-mp3, music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама делает музыку устаревшей. Длины реплик больше не захардкожены: упаковщик печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них — иначе mt32 (реплики на 6% длиннее) молча ломал катсцену. Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR), HDD_IMG стал ?=; команда сборки roomtest не изменилась. Корневой make host-tests переключён на SprPoP. Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики приходила раньше молнии. Это обход, а не лечение; разбор с замерами — docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
# QuickSave / QuickLoad — разбор оригинала и план реализации
|
||||
|
||||
Статус: **РЕАЛИЗОВАНО и проверено в MAME** (2026-08-22; F6/F9, POP.SAV +
|
||||
POP.BAK — см. коммит `v0.6-pop-quicksave`). Документ оставлен как
|
||||
справочник по формату снимка и разбору. Задача на доске —
|
||||
[`../TASKS_OPEN.md#qsave`](TASKS_OPEN.md#qsave).
|
||||
|
||||
---
|
||||
|
||||
## 0. Важная оговорка об «оригинале»
|
||||
|
||||
**В оригинальном PoP 1989 года (DOS/Apple II) QuickSave/QuickLoad НЕТ.**
|
||||
Там вообще нет сохранения посреди уровня: игра рассчитана на один заход в
|
||||
60 минут, а «продолжение» — это только пароль/чекпоинт уровня 7. Поэтому
|
||||
`Prince-of-Persia-Apple-II/` и `MSDOS/` тут не источники — искать в них
|
||||
нечего.
|
||||
|
||||
Источник истины — **SDLPoP**, где быстрое сохранение добавлено как
|
||||
enhancement: `seg000.c`, блок `#ifdef USE_QUICKSAVE`, клавиши **F6** (save)
|
||||
и **F9** (load). Ниже разобран именно он. Это значит, что правило
|
||||
«расхождение с SDLPoP = баг у нас» здесь работает мягче: мы не обязаны
|
||||
повторять его байт-в-байт, но обязаны повторить его **устройство**, потому
|
||||
что оно решает ровно те проблемы, которые возникнут и у нас.
|
||||
|
||||
---
|
||||
|
||||
## 1. Как это устроено в SDLPoP
|
||||
|
||||
### 1.1 Точка вызова — отдельная фаза кадра, не обработчик клавиши
|
||||
|
||||
Клавиша только взводит флаг (`need_quick_save` / `need_quick_load`,
|
||||
`seg000.c:558`), а вся работа делается в `check_quick_op()` — она вызывается
|
||||
из главного цикла **между кадрами**, когда движок в согласованном состоянии.
|
||||
Это принципиально: загрузка посреди тика переписала бы `Char` под ногами у
|
||||
`play_seq`.
|
||||
|
||||
Отказ штатный, не фатальный: `quick_save()`/`quick_load()` возвращают
|
||||
успех/неуспех, и игра печатает `QUICKSAVE` / `NO QUICKLOAD` внизу экрана и
|
||||
продолжается.
|
||||
|
||||
### 1.2 Формат — плоская последовательность переменных, без структуры
|
||||
|
||||
```c
|
||||
#define process(x) ok = ok && process_func(&(x), sizeof(x))
|
||||
```
|
||||
|
||||
Один макрос и один и тот же список обходится **и на запись, и на чтение**
|
||||
(`quick_process(process_save)` / `quick_process(process_load)`). Поля
|
||||
пишутся встык, без имён и тегов; совместимость держится ровно одним
|
||||
средством — **строкой версии в начале файла**:
|
||||
|
||||
```c
|
||||
const char quick_version[] = "V1.16b4 ";
|
||||
```
|
||||
|
||||
При загрузке она сравнивается, и при несовпадении файл просто отвергается
|
||||
(`quick_load`, возврат 0). То есть формат нарочно хрупкий и нарочно
|
||||
одноразовый — это снимок конкретной сборки, а не сейв-формат.
|
||||
|
||||
**Это стоит перенять целиком.** Мы платим за версионирование одним байтом
|
||||
и получаем право менять состав снимка при каждой правке движка.
|
||||
|
||||
### 1.3 Что именно сохраняется
|
||||
|
||||
Полный список — `quick_process`, `seg000.c:257-366`. По смыслу он делится
|
||||
на пять групп:
|
||||
|
||||
| группа | поля |
|
||||
|---|---|
|
||||
| уровень | `level` (2305 Б целиком), `checkpoint`, `upside_down`, `drawn_room`, `current_level`, `next_level`, `leveldoor_open` |
|
||||
| анимируемые объекты | `mobs_count`, `mobs[14]`, `trobs_count`, `trobs[30]` |
|
||||
| Кид | `Kid`, `hitp_curr/max/beg_lev`, `grab_timer`, `holding_sword`, `united_with_shadow`, `have_sword`, `kid_sword_strike`, `pickup_obj_type`, `offguard` |
|
||||
| соперник | `Guard`, `Char`, `Opp`, `guardhp_curr/max`, `demo_index`, `demo_time`, `curr_guard_color`, `guard_notice_timer`, `guard_skill`, `shadow_initialized`, `guard_refrac`, `justblocked`, `droppedout`, `is_guard_notice`, `can_guard_see_kid` |
|
||||
| прочее | кэш коллизии (`*_row_coll_room/flags`, `prev_collision_row`), вспышка (`flash_color/time`), звук (`is_screaming`, `is_feather_fall`, …), **`random_seed`**, время (`rem_min`, `rem_tick`), весь блок управления (`control_*`, `ctrl1_*`) |
|
||||
|
||||
Два наблюдения, важные для нас:
|
||||
|
||||
1. **Состояние ОТРИСОВКИ не сохраняется вообще.** Ни экранных буферов, ни
|
||||
пометок перерисовки, ни того, что уже нарисовано. Вместо этого при
|
||||
загрузке комната перерисовывается с нуля. Это резко упрощает задачу и
|
||||
ровно то, что нам нужно при дабл-буфере.
|
||||
2. **`random_seed` сохраняется.** Без него загрузка не воспроизводима:
|
||||
после неё факелы, чомперы и `prandom` в боёвке пойдут иначе.
|
||||
|
||||
### 1.4 Что делается при загрузке
|
||||
|
||||
`restore_room_after_quick_load()` (`seg000.c:395`) — это и есть вся
|
||||
«сложность» операции:
|
||||
|
||||
- `load_lev_spr(current_level)` — **перезагрузка графики уровня** (тайлсет
|
||||
мог смениться: подземелье/дворец);
|
||||
- `different_room = 1`, `next_room = drawn_room = Kid.room` — принудительно
|
||||
«мы в другой комнате», чтобы движок перерисовал всё;
|
||||
- `load_room_links()` — связи комнат заново;
|
||||
- `draw_game_frame()` — отрисовать кадр (важно для состояния падения);
|
||||
- `hitp_delta = guardhp_delta = 1` — принудительный редрой полос HP;
|
||||
- если `Guard.room != drawn_room` — стража «выключить» (`direction =
|
||||
dir_56_none`, `guardhp_curr = 0`), как в `clear_char()`;
|
||||
- `loadkid_and_opp()` — восстановить окно `Char`/`Opp`;
|
||||
- сбросить таймеры текста и `exit_room_timer`.
|
||||
|
||||
Плюс визуальный приём: перед загрузкой экран заливается чёрным на 5 тиков —
|
||||
чтобы переход читался глазом и не выглядел «дёрганием».
|
||||
|
||||
### 1.5 Чего в SDLPoP решили НЕ восстанавливать
|
||||
|
||||
- звуки — просто `stop_sounds()`;
|
||||
- перо (`is_feather_fall`) — без фикса `fix_quicksave_during_feather`
|
||||
сохранение под пером запрещено вовсе, а при загрузке эффект гасится;
|
||||
- есть опциональный **штраф**: `USE_QUICKLOAD_PENALTY` отнимает минуту
|
||||
игрового времени за квиклоад. Нам не нужен (у нас пока нет игрового
|
||||
таймера).
|
||||
|
||||
---
|
||||
|
||||
## 2. Чем наша архитектура отличается
|
||||
|
||||
| | SDLPoP | у нас | следствие для задачи |
|
||||
|---|---|---|---|
|
||||
| уровень в памяти | `level_type` в ОЗУ, 2305 Б, мутабельный | EMM-страница (`pop_lvl_page`), плюс рабочая копия комнаты в W2 | снимок читает страницу через W0-маппинг, а не `memcpy` |
|
||||
| модификаторы тайлов | внутри `level.bg` | отдельный `room_modif[24][30]` в `pop_trob.c` (**static**) | нужен экспортируемый сериализатор из банка 6 |
|
||||
| код | один бинарник | 8 банков + резидент | сериализатор обязан жить там же, где данные, и зваться через трамплин |
|
||||
| экран | один буфер | **дабл-буфер**, у каждой страницы своя теневая копия | после загрузки перерисовать ОБЕ страницы, иначе через кадр мелькнёт старое |
|
||||
| ОЗУ | сколько угодно | куча 2969 Б, стек 1279 Б | буфер снимка целиком в ОЗУ не положить — писать потоком |
|
||||
| диск | `fopen` | DSS: 8 манипуляторов, 9-й ВЕШАЕТ систему ([[dss_fd_limit]]) | закрывать файл гарантированно, гард уже есть в libc |
|
||||
| ГСЧ | один `random_seed` | **три** независимых: `pop_t_seed`, `trob_seed`, `pop_fight_seed` | сохранять все три, иначе загрузка невоспроизводима |
|
||||
|
||||
---
|
||||
|
||||
## 3. Инвентаризация нашего состояния
|
||||
|
||||
Собрано по `.sprinter-cc-build/.sprinter-cc-sprpop/sprpop.map` (данные всех модулей, включая
|
||||
банковые, лежат в W2 — банк влияет только на код). Отмечено, что глобально
|
||||
(видно снаружи), а что `static` и требует аксессора.
|
||||
|
||||
### 3.1 Мутабельные данные уровня
|
||||
|
||||
| что | где | размер | доступ |
|
||||
|---|---|---|---|
|
||||
| тайлы `fg` (провалившиеся плиты, открытые двери, съеденные предметы) | EMM-страница уровня | 720 Б | `pop_level_set_tile` пишет; чтения наружу нет — **нужен аксессор** |
|
||||
| `room_modif[24][30]` | `pop_trob.c`, static | 720 Б | **нужен сериализатор** (банк 6) |
|
||||
| `room_seen[24]` | `pop_trob.c`, static | 24 Б | там же |
|
||||
| `trobs[30]` + `trobs_count` | `pop_trob.c`, static | 91 Б | там же |
|
||||
| `trob_seed` | `pop_trob.c`, static | 4 Б | там же |
|
||||
| `mobs[14]` | `pop_room.c`, **глобален** | 210 Б | напрямую |
|
||||
| `mobs_live` | `pop_room.c`, static | 1 Б | аксессор |
|
||||
|
||||
### 3.2 Персонажи и бой
|
||||
|
||||
`Kid`, `Char`, `Opp` (`pop_kid.c`), `Guard` (`pop_guard.c`) — по 16 Б,
|
||||
все глобальные. Рядом: `hitp_curr/max/beg_lev/delta`, `guardhp_curr/max/delta`,
|
||||
`guard_skill`, `guard_refrac`, `justblocked`, `kid_sword_strike`, `offguard`,
|
||||
`holding_sword`, `can_guard_see_kid`, `is_guard_notice`,
|
||||
`pop_guard_notice_timer`, `pop_guard_hurt`, `pop_united_shadow`,
|
||||
`pop_shadow_init`, `pop_fight_seed`, `knock`.
|
||||
|
||||
### 3.3 Прогресс и физика
|
||||
|
||||
`pop_current_level`, `pop_next_level`, `pop_checkpoint`, `pop_have_sword`,
|
||||
`pop_item_taken`, `pop_leveldoor_open`, `pop_leveldoor_right`,
|
||||
`pop_leveldoor_ybottom`, `pop_kid_dead`, `pop_kid_hurt`, `pop_feather`,
|
||||
`pop_upside` / `pop_upside_want`, `pop_flash_time` / `pop_flash_color`,
|
||||
`pop_droppedout`, `pop_fell_out`, `pop_leave_dir`, `pop_leave_timer`,
|
||||
`pop_loose_*`, `pop_ceil_modif`, `pop_ceil_fell`, `pop_debris_at`,
|
||||
`pop_seamless`, `pop_jumped_mirror`.
|
||||
|
||||
### 3.4 Ввод
|
||||
|
||||
`control_x/y/shift/forward/backward/up/down/shift2` (`pop_state.c`) — как в
|
||||
SDLPoP, сохраняются.
|
||||
|
||||
### 3.5 Что НЕ сохранять (восстанавливается перерисовкой)
|
||||
|
||||
`room_fg/room_bg`, `lcol_*`/`rcol_*`/`below_fg`/`above_*`, `seam_*`,
|
||||
`cur_room`, `pop_t_*` (весь кэш слоя фона, окна клипа, `pop_cd_*`),
|
||||
`trob_drawn`, `mob_spr`, слоты `pop_cd`, метки `pop_redraw`, запечки
|
||||
(`bake_pg`). Всё это — производное; после загрузки оно обязано быть
|
||||
сброшено и пересчитано, а не восстановлено.
|
||||
|
||||
**Оценка объёма снимка: ≈ 1,9 КБ** (720 + 720 + 210 + 91 + 64 + ~60
|
||||
скаляров + запас).
|
||||
|
||||
---
|
||||
|
||||
## 4. Куда писать снимок: HDD-файл, а не EMM-страница
|
||||
|
||||
Решение: **один основной слот `POP.SAV` на HDD; предыдущая
|
||||
валидная запись хранится в `POP.BAK`.** EMM-слота нет: программа
|
||||
работает только с HDD, а главный сценарий QuickSave обязан переживать
|
||||
перезапуск игры.
|
||||
|
||||
> Пересмотрено 2026-08-21 по вопросу пользователя «почему EMM, а не файл».
|
||||
> Первая редакция плана рекомендовала EMM — это была ошибка: она взвешивала
|
||||
> скорость и недооценивала главный сценарий использования. Разбор оставлен
|
||||
> целиком, потому что довод переносится и на другие «положить в память
|
||||
> вместо диска» решения.
|
||||
|
||||
**Решающий довод: EMM-страница не переживает рестарт программы,** а именно
|
||||
рестарт — тот случай, ради которого QuickSave и нужен. Пример из этого же
|
||||
проекта: сцену каскада плит на 13/23 воспроизводит ТОЛЬКО `ESC` → запуск
|
||||
заново ([`perf_l13_room23.md`](perf_l13_room23.md) §1, где перечислено,
|
||||
почему не годятся ни возврат в комнату, ни рестарт уровня, ни запись
|
||||
состояния отладчиком). Тем более снимок в ОЗУ не переживает перезапуск
|
||||
MAME, обязательный после каждой пересборки образа.
|
||||
|
||||
| сценарий | EMM | файл |
|
||||
|---|---|---|
|
||||
| «переиграть это место ещё раз» | работает, мгновенно | работает, на HDD быстро |
|
||||
| «вернуться к багу после рестарта» | **не работает** | **работает** |
|
||||
|
||||
Второй сценарий не закрывается ничем другим; первый закрывается обоими, и
|
||||
разница в скорости там некритична — 1,9 КБ на HDD ([[mame_hdd_test_disk]] —
|
||||
быстрый путь против дискеты) не заметны на фоне полной перерисовки комнаты,
|
||||
которая при загрузке делается в любом случае и стоит дороже.
|
||||
|
||||
Доводы за EMM, которые при перепроверке оказались слабыми: лимит
|
||||
манипуляторов DSS ни при чём (открываем и закрываем ровно один файл, гард
|
||||
`_fd_guard` в libc и так стоит), а «не нужен путь и права» — экономия одной
|
||||
строки.
|
||||
|
||||
Обход состояния всё равно писать с абстракцией чтения/записи, как у SDLPoP
|
||||
через `process_func`, но второй EMM-слот в scope не входит.
|
||||
|
||||
**Проверить ДО кодинга:** пишется ли `test_hdd.chd` из-под MAME. Если образ
|
||||
только на чтение, файловый путь упрётся в это на первом же шаге и порядок
|
||||
работ придётся менять. Проверка дешёвая — записать пробный файл на `D:` из
|
||||
SprPoP.
|
||||
|
||||
---
|
||||
|
||||
## 5. Формат снимка
|
||||
|
||||
```
|
||||
+0 "PQS1" 4 Б магия
|
||||
+4 версия сборки 1 Б (инкремент при ЛЮБОМ изменении состава)
|
||||
+5 pop_current_level 1 Б
|
||||
+6 длина полезной части 2 Б (контроль, что обход совпал)
|
||||
+8 ... поля встык, ОДИН порядок на запись и на чтение ...
|
||||
.. checksum 2 Б (заголовок + payload)
|
||||
```
|
||||
|
||||
Версия проверяется первой; несовпадение — отказ, как в SDLPoP. Никаких
|
||||
тегов и выравнивания: снимок одноразовый и живёт ровно одну сборку.
|
||||
|
||||
Обход — один список и один макрос, как `process(x)`:
|
||||
|
||||
```c
|
||||
static void qs_walk(qs_io_t io) /* io = запись или чтение */
|
||||
{
|
||||
QS(pop_current_level); QS(pop_checkpoint); ...
|
||||
}
|
||||
```
|
||||
|
||||
Так состав нельзя рассинхронизировать между сохранением и загрузкой —
|
||||
единственная реальная опасность плоского формата.
|
||||
|
||||
---
|
||||
|
||||
## 6. Что делать при загрузке (наш аналог `restore_room_after_quick_load`)
|
||||
|
||||
Порядок важен, каждый пункт закрывает конкретный отказ:
|
||||
|
||||
1. **Сменился уровень?** → `pop_level_load_num()`, `pop_bg_load(tileset)`,
|
||||
атласы стража по типу. Это дорого, но ровно тот же путь, что при
|
||||
переходе уровня (`pop_level_switch`), — переиспользовать его, а не писать
|
||||
заново.
|
||||
2. Залить экран чёрным (приём SDLPoP: переход должен читаться глазом).
|
||||
3. Восстановить состояние обходом `qs_walk`.
|
||||
4. **Сбросить всё производное:** `pop_trob_reset` (но НЕ трогая
|
||||
восстановленные `room_modif`/`trobs` — нужен отдельный «мягкий» сброс,
|
||||
только `trob_drawn` + метки), `pop_redraw_reset`, слоты `pop_cd`,
|
||||
`pop_bake_forget`, `pop_cd_clear`, сигнатуры пропуска перерисовки.
|
||||
5. `pop_room_load(Kid.room)` — рабочая копия комнаты и срезы соседей.
|
||||
6. **Полная отрисовка комнаты в ОБЕ страницы дабл-буфера.** Это наше
|
||||
главное отличие от SDLPoP: одной перерисовки мало, вторая страница
|
||||
останется со старой картинкой и мигнёт через кадр.
|
||||
7. Принудительный редрой полос HP (`hitp_delta = guardhp_delta = 1`).
|
||||
8. Если `Guard.room != Kid.room` — выключить стража
|
||||
(`Guard.direction = DIR_56_NONE`, `guardhp_curr = 0`), как `clear_char`.
|
||||
9. `pop_loadkid_and_opp()` — согласовать окно `Char`/`Opp`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Разбиение на шаги
|
||||
|
||||
| шаг | что | критерий готовности |
|
||||
|---|---|---|
|
||||
| **QS0** | Проверить, что `D:` пишется из-под MAME (пробный файл из SprPoP) | файл создался и читается обратно после рестарта программы |
|
||||
| **QS1** | Аксессоры/сериализаторы для `static`-состояния банковых модулей: `pop_trob.c` (`room_modif`, `room_seen`, `trobs`, `trob_seed`), `pop_room.c` (`mobs_live`), страница уровня (чтение `fg`) | хост-тест `tests-host/t_qsave.c`: обход туда-обратно на синтетическом состоянии даёт байт-в-байт исходное |
|
||||
| **QS2** | Ядро: `qs_walk` + `POP.SAV`, магия/версия/checksum, безопасная замена с предыдущей валидной копией в `POP.BAK` | сохранение и загрузка **в той же комнате, без движения**; порча SAV не портит BAK |
|
||||
| **QS3** | Восстановление отрисовки (§6), включая обе страницы дабл-буфера | загрузка после перехода в другую комнату; нет мерцания через кадр |
|
||||
| **QS4** | Клавиши **F6/F9** (или свободные из `pop_cheat.h`) через `<kbd_raw.h>`, флаги `need_quick_save/load`, обработка **между кадрами** | загрузка посреди боя/падения не ломает `play_seq` |
|
||||
| **QS5** | Загрузка с **другого уровня** (перезагрузка уровня и атласов) | сохранить на ур. 2, уйти на ур. 12, загрузить — тайлсет и стражи верные |
|
||||
|
||||
Порядок не переставлять: QS0 первым (он может изменить весь план), QS3 без
|
||||
QS2 нечего проверять, а QS5 обязан идти после QS3 — иначе смена тайлсета
|
||||
замаскирует ошибки восстановления.
|
||||
|
||||
**Главный критерий приёмки всей задачи:** сохранить состояние, выйти по
|
||||
`ESC`, запустить SprPoP заново, загрузить — и оказаться там же. Именно
|
||||
этого сценария сейчас нет ничем, и ради него задача и делается.
|
||||
|
||||
---
|
||||
|
||||
## 8. Риски и открытые вопросы
|
||||
|
||||
1. **`static` в банковых модулях.** Их нет в карте символов, то есть
|
||||
отладчиком снимок не проверить. Возможно, стоит сделать `room_modif` и
|
||||
`trobs` НЕ-static — так же, как уже сделано с `mobs` в `pop_room.c` и
|
||||
ровно по той же мотивации (там это записано прямым комментарием).
|
||||
2. **Место в банке 6.** `pop_trob` занимает 3802/16384 — запас есть, но
|
||||
сериализатор лучше писать компактным обходом, а не 30 отдельными
|
||||
вызовами.
|
||||
3. **Три ГСЧ.** Проверить, что сохранены ВСЕ: пропуск любого даст
|
||||
«загрузилось, но играется иначе» — самый неприятный класс бага, потому
|
||||
что выглядит как случайность.
|
||||
4. **Согласованность `Char` и `Kid`.** У нас окно `Char` — отдельная копия;
|
||||
если сохранить их рассогласованными (снимок посреди тика), загрузка
|
||||
воскресит рассогласование. Отсюда требование QS4: только между кадрами.
|
||||
Урок свежий — ровно на этом стыке жил
|
||||
[BUG-CHEAT-IMM-1](BUGS_CLOSED.md#bug-cheat-imm-1).
|
||||
5. **Дабл-буфер.** Самый вероятный источник «почти работает»: забыть вторую
|
||||
страницу. Симптом — мерцание через кадр
|
||||
(см. `SprPoP/CLAUDE.md`, раздел про дабл-буфер).
|
||||
6. **Транзакция SAV/BAK.** До кодинга проверить на DSS семантику
|
||||
rename/replace. Если атомарная замена не гарантирована, писать через
|
||||
`POP.NEW`, проверять его после close и не удалять единственную валидную копию
|
||||
до завершения новой.
|
||||
Reference in New Issue
Block a user