Фикс: чтение файлов нельзя выносить в банк — банковый код сам живёт в W3

Первый запуск после разгрузки встал в halt по мусору в 0xC40E.  Причина:
pop_level_read_file и pop_kid_data_load маппят страницу данных в W3 на время
read() (файл читается по 0xC000), а модули банка исполняются ИЗ ЭТОГО ЖЕ
окна — после sprinter_page_w3() следующая инструкция приходит уже из чужой
страницы.  Обе функции вернулись в резидент; в банке остался код, который
ходит через W0 (gfx_w0_map) или окон не трогает вовсе.

Правило записано в _pop_level.h и _pop_kid.h: банковый модуль НЕ маппит W3
(через W0 — можно).  Поэтому же в резиденте живёт весь libbgi.

Куча 6095 Б (было 902 до разгрузки).  Проверено в MAME: игра стартует,
комната рисуется, Кид управляем.  tests-host 6/6.
This commit is contained in:
2026-08-12 22:17:18 +03:00
parent 147cb185b1
commit 055d6d7c89
7 changed files with 123 additions and 82 deletions
+6
View File
@@ -20,4 +20,10 @@ extern uint8_t kdat_blk; /* EMM-блок страницы kid_data.bin */
extern uint8_t kdat_page; /* её номер (маппится в W0 на время play_seq) */
extern uint8_t kdat_ok; /* данные загружены */
/* ПРАВИЛО ОКНА W3: банковый модуль исполняется из W3 и переключать это окно
* не может (следующая инструкция придёт уже из чужой страницы). Поэтому
* ЧТЕНИЕ kid_data.bin — pop_kid_data_load — осталось в резиденте, а в банк
* уехало только то, что читает через W0 или вообще не трогает окна.
* То же правило и в _pop_level.h. */
#endif /* _POP_KID_H */
+8
View File
@@ -69,6 +69,14 @@ static const uint8_t *room_bg_ptr(uint8_t r)
return (const uint8_t *)(LVL_DATA_OFF + BP_BG) + (uint16_t)(r - 1) * ROOM_TILES;
}
/* ПРАВИЛО ОКНА W3. Банковый модуль исполняется ИЗ W3, поэтому сам он
* менять это окно НЕ МОЖЕТ: после sprinter_page_w3(другая_страница)
* следующая инструкция читается уже из чужой страницы. Всё чтение файлов
* (оно грузит данные по 0xC000) обязано жить в резиденте — отсюда
* pop_level_read_file здесь, а не в pop_level_cold.c. Через W0
* (gfx_w0_map) банкам работать можно: это другое окно. */
int pop_level_read_file(const char *path);
/* Живая копия стражей из данных уровня (загрузка/рестарт) — в холодной. */
void pop_gstate_init(void) __banked;
+3 -34
View File
@@ -48,40 +48,9 @@ void pop_kid_free(void) __banked
kid_npages = 0;
}
/* Таблица кадров (241*5 Б) и seqtbl (2310 Б) занимали 3.5 КБ в _CODE окна
* W1/W2 — самый дефицитный ресурс. Теперь они лежат в отдельной EMM-
* странице, которая маппится в W0 РОВНО на время play_seq (раз за тик).
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
int pop_kid_data_load(const char *path) __banked
{
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
uint8_t saved_w3, blk;
uint16_t stub;
int fd, n;
fd = open(path, O_RDONLY);
if (fd < 0) return -1;
blk = mem_alloc_pages(1);
if (!blk) { close(fd); return -1; }
kdat_blk = blk;
kdat_page = mem_get_page(blk, 0);
saved_w3 = _io_page_w3;
sprinter_page_w3(kdat_page);
n = read(fd, pg + KD_DATA_OFF, 16384 - KD_DATA_OFF);
if (n >= (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
stub = (uint16_t)&_gfx_w0_isr; /* ISR-стаб W0-страницы (как pop_level) */
pg[0x38] = 0xC3;
pg[0x39] = (uint8_t)(stub & 0xFF);
pg[0x3A] = (uint8_t)(stub >> 8);
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN */
}
sprinter_page_w3(saved_w3);
close(fd);
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) { mem_free_block(blk); return -1; }
kdat_ok = 1;
return 0;
}
/* ЗАГРУЗКА kid_data.bin (pop_kid_data_load) осталась в РЕЗИДЕНТЕ: она
* маппит страницу в W3 на время чтения, а этот модуль сам в W3 — см.
* то же правило в _pop_kid.h. */
void pop_kid_data_free(void) __banked
{
+40
View File
@@ -16,6 +16,9 @@
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
#include "pop_map.h" /* pop_clip_char_top (clip_char) */
#include "pop_geom.h"
#include <fcntl.h>
#include <unistd.h>
#include <sprinter_mem.h>
#include "kid_data.h"
#include "_pop_kid.h" /* дескриптор страницы данных (её грузит банк 8) */
@@ -416,3 +419,40 @@ void kid_tick(void)
/* Отрисовка кадра (load_frame_to_obj, heal, клип, брызги, клинок) живёт в
* pop_cdraw.c — ОДНА на всех Char, см. DRAW-CHAR. Отсюда она получает
* атласы Кида (kidp/kid_npages) и его кадр (kid_frame). */
/* Таблица кадров (241*5 Б) и seqtbl (2310 Б) занимали 3.5 КБ в _CODE окна
* W1/W2 — самый дефицитный ресурс. Теперь они лежат в отдельной EMM-
* странице, которая маппится в W0 РОВНО на время play_seq (раз за тик).
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
/* ЖИВЁТ В РЕЗИДЕНТЕ, хотя зовётся раз за игру: маппит страницу в W3 на
* время чтения, а банковый код сам лежит в W3 (см. _pop_kid.h). */
int pop_kid_data_load(const char *path)
{
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
uint8_t saved_w3, blk;
uint16_t stub;
int fd, n;
fd = open(path, O_RDONLY);
if (fd < 0) return -1;
blk = mem_alloc_pages(1);
if (!blk) { close(fd); return -1; }
kdat_blk = blk;
kdat_page = mem_get_page(blk, 0);
saved_w3 = _io_page_w3;
sprinter_page_w3(kdat_page);
n = read(fd, pg + KD_DATA_OFF, 16384 - KD_DATA_OFF);
if (n >= (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
stub = (uint16_t)&_gfx_w0_isr; /* ISR-стаб W0-страницы (как pop_level) */
pg[0x38] = 0xC3;
pg[0x39] = (uint8_t)(stub & 0xFF);
pg[0x3A] = (uint8_t)(stub >> 8);
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN */
}
sprinter_page_w3(saved_w3);
close(fd);
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) { mem_free_block(blk); return -1; }
kdat_ok = 1;
return 0;
}
+1 -1
View File
@@ -35,7 +35,7 @@ int pop_kid_load(uint8_t npages) __banked;
* (kid_data.bin): в _CODE окна W1/W2 они занимали 3.5 КБ. Грузить ДО
* первого kid_init/play_seq. 0 — OK, -1 — ошибка (без них Kid не оживёт).
* Страница маппится в W0 внутри play_seq, раз за тик. */
int pop_kid_data_load(const char *path) __banked;
int pop_kid_data_load(const char *path);
/* Прочитать кадр из таблицы страницы данных: tbl_off — KID_BIN_FRAMES_OFF
* (Kid) или KID_BIN_GFRAMES_OFF (страж, своя таблица frame_tbl_guard).
+59
View File
@@ -11,7 +11,16 @@
* объявлено в _pop_level.h — данные банковых модулей всё равно линкуются в
* общий _DATA, так что холодная половина видит их напрямую.
*/
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include <sprite.h> /* gfx_w0_map / gfx_w0_unmap */
/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с
* замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на
* _gfx_w0_isr (восстановит окно). */
extern void _gfx_w0_isr(void);
#include "pop_level.h"
#include "_pop_level.h"
@@ -129,3 +138,53 @@ uint8_t pop_guard_state_x(uint8_t room)
if (room < 1 || room > 24) return 0;
return pop_gstate[(uint16_t)(room - 1) * 6 + GS_X];
}
/* Чтение файла уровня в свежую EMM-страницу. ЖИВЁТ В РЕЗИДЕНТЕ, хотя
* зовётся раз на уровень: функция маппит страницу в W3 на время read(), а
* весь холодный код лежит В ЭТОМ ЖЕ окне — переключив его оттуда, банк
* вырезает из-под себя собственные инструкции (первый же вызов улетал в
* halt по мусору, поймано в MAME 2026-08-12). Правило — в _pop_level.h.
* Наружу не публичная в pop_level.h: уровень грузят по НОМЕРУ. */
int pop_level_read_file(const char *path)
{
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
uint8_t saved_w3, blk;
uint16_t stub;
int fd, n;
fd = open(path, O_RDONLY);
if (fd < 0) return -1;
blk = mem_alloc_pages(1);
if (!blk) { close(fd); return -1; }
pop_lvl_blk = blk;
pop_lvl_page = mem_get_page(blk, 0);
saved_w3 = _io_page_w3;
sprinter_page_w3(pop_lvl_page);
/* Файл целиком в offset 0x100 (ESTEX READ пишет в W3 — как atlas_load). */
n = read(fd, pg + LVL_DATA_OFF, 16384 - LVL_DATA_OFF);
if (n >= (int)MIN_SIZE) { /* пропатчить ISR-стаб */
uint16_t i;
stub = (uint16_t)&_gfx_w0_isr;
pg[0x38] = 0xC3; /* JP _gfx_w0_isr */
pg[0x39] = (uint8_t)(stub & 0xFF);
pg[0x3A] = (uint8_t)(stub >> 8);
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN (NMI-хвост) */
/* Скопировать таблицы дверных связей в W2 (страница ещё в W3). */
for (i = 0; i < 256; i++) {
pop_dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i];
pop_dl2[i] = pg[LVL_DATA_OFF + BP_LINKMAP + i];
}
/* Эталон foretable — для рестарта уровня (см. LVL_PRISTINE_OFF). */
for (i = 0; i < 24u * ROOM_TILES; i++)
pg[LVL_PRISTINE_OFF + i] = pg[LVL_DATA_OFF + BP_FG + i];
}
sprinter_page_w3(saved_w3);
close(fd);
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
pop_lvl_ok = 1;
pop_gstate_init(); /* живая копия состояния стражей — из уровня */
return 0;
}
+6 -47
View File
@@ -23,51 +23,10 @@
* стабильный — линкер тянет его из bgi256.lib (графика уже подключена). */
extern void _gfx_w0_isr(void);
/* Загрузка по пути. Не публичная: снаружи уровень грузят по НОМЕРУ
* (pop_level_load_num), а тот сам перебирает пути. */
static int level_load_path(const char *path)
{
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
uint8_t saved_w3, blk;
uint16_t stub;
int fd, n;
fd = open(path, O_RDONLY);
if (fd < 0) return -1;
blk = mem_alloc_pages(1);
if (!blk) { close(fd); return -1; }
pop_lvl_blk = blk;
pop_lvl_page = mem_get_page(blk, 0);
saved_w3 = _io_page_w3;
sprinter_page_w3(pop_lvl_page);
/* Файл целиком в offset 0x100 (ESTEX READ пишет в W3 — как atlas_load). */
n = read(fd, pg + LVL_DATA_OFF, 16384 - LVL_DATA_OFF);
if (n >= (int)MIN_SIZE) { /* пропатчить ISR-стаб */
uint16_t i;
stub = (uint16_t)&_gfx_w0_isr;
pg[0x38] = 0xC3; /* JP _gfx_w0_isr */
pg[0x39] = (uint8_t)(stub & 0xFF);
pg[0x3A] = (uint8_t)(stub >> 8);
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN (NMI-хвост) */
/* Скопировать таблицы дверных связей в W2 (страница ещё в W3). */
for (i = 0; i < 256; i++) {
pop_dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i];
pop_dl2[i] = pg[LVL_DATA_OFF + BP_LINKMAP + i];
}
/* Эталон foretable — для рестарта уровня (см. LVL_PRISTINE_OFF). */
for (i = 0; i < 24u * ROOM_TILES; i++)
pg[LVL_PRISTINE_OFF + i] = pg[LVL_DATA_OFF + BP_FG + i];
}
sprinter_page_w3(saved_w3);
close(fd);
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
pop_lvl_ok = 1;
pop_gstate_init(); /* живая копия состояния стражей — из уровня */
return 0;
}
/* ЧТЕНИЕ ФАЙЛА уровня живёт в РЕЗИДЕНТЕ (pop_level.c, pop_level_read_file):
* оно маппит страницу в W3 на время read(), а этот модуль сам лежит в W3 —
* переключив окно, он вырезал бы из-под себя собственный код (проверено:
* первый же вызов улетал в halt по мусору). Правило записано в _pop_level.h. */
void pop_level_free(void) __banked
{
@@ -111,10 +70,10 @@ int pop_level_load_num(uint8_t n) __banked
pri[13] = alt[9] = (char)('0' + n % 10);
/* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой:
* level_load_path сам аллоцирует страницу, и если файла нет — остаёмся
* pop_level_read_file сам аллоцирует страницу, и если файла нет — остаёмся
* на текущем уровне вместо падения в пустой уровень. */
pop_lvl_ok = 0; /* чтобы level_load_path не мешался */
if (level_load_path(pri) != 0 && level_load_path(alt) != 0) {
if (pop_level_read_file(pri) != 0 && pop_level_read_file(alt) != 0) {
pop_lvl_blk = saved_blk; pop_lvl_page = saved_page; pop_lvl_ok = saved_ok;
return -1;
}