PoP roomtest (план v2, фаза 1a): пометки перерисовки вместо прямых блитов

Порт архитектуры оригинала: логика анимации тайлов НИЧЕГО не рисует, она
ставит флаг (set_redraw_full/set_wipe/redraw_20h/redraw_21h, seg007), а
отрисовка идёт отдельным проходом redraw_needed (seg008:0178).  У нас
появился pop_redraw.c/.h: pop_set_redraw(tilepos, вид, страницы) +
pop_set_redraw_above(col, ...) + pop_redraw_needed(), который зовёт
главный цикл в слое фона (до kid_draw).

Отличие от оригинала (наша платформа): счётчик пометки — это ЧИСЛО СТРАНИЦ
дабл-буфера (обычно 2), а вид перерисовки хранится явно (heal+поверх или
запечь фон), потому что у нас у каждой страницы своя ОЗУ-копия фона.  В
оригинале вид кодируется тем, в какой из таблиц redraw_frames_* стоит флаг.

pop_trob переведён на пометки: пики, кнопки, дверь уровня.  Его самодельные
массивы spike_rest/button_rest/ldoor_rest/rest_pending удалены — их роль
теперь у счётчика страниц в pop_redraw.  Прямыми вызовами pop_bg осталось
только пламя факела и пузырёк зелья: это не тайловая перерисовка, а
покадровый оверлей; из-за них pop_process_trobs остаётся единственной
функцией модуля, которую нельзя звать из банка.

Зачем: из __banked-кода резидентная страница W3 недостижима транзитивно
(docs/layout_plan_v2.md §2 R2), поэтому логика, которую будут звать стражи,
не должна вызывать pop_bg.

Проверено в MAME: комната 6 — Kid на кнопке-открывалке, решётка в шве
поднимается; сошёл с кнопки — закрывается; упал в шахту на пики — пики
выдвинулись и отрисованы (кадр 177).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 20:37:03 +03:00
parent eef6eebd8c
commit 24bb724c22
5 changed files with 156 additions and 66 deletions
+2 -2
View File
@@ -12,7 +12,7 @@ MEMORY ?= small
# __banked-модулей (стражи): из банка резидентная страница W3 не видна — # __banked-модулей (стражи): из банка резидентная страница W3 не видна —
# ни напрямую, ни транзитивно. См. docs/layout_plan_v2.md §2 (R2), §4. # ни напрямую, ни транзитивно. См. docs/layout_plan_v2.md §2 (R2), §4.
EXTRA_FLAGS ?= --gfx 256 --w3 pop_bg.c EXTRA_FLAGS ?= --gfx 256 --w3 pop_bg.c
EXTRA_SRCS := pop_kid.c pop_ctrl.c pop_level.c pop_map.c pop_trob.c EXTRA_SRCS := pop_kid.c pop_ctrl.c pop_level.c pop_map.c pop_trob.c pop_redraw.c
BG_DIR := $(CURDIR)/../poc/res/bg BG_DIR := $(CURDIR)/../poc/res/bg
KID_DIR := $(CURDIR)/../poc/res/kid KID_DIR := $(CURDIR)/../poc/res/kid
@@ -42,4 +42,4 @@ $(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py
cd $(TC) && python3 pop_extract_kid_data.py cd $(TC) && python3 pop_extract_kid_data.py
$(EXAMPLE).exe: pop_bg.c pop_bg.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA) $(EXAMPLE).exe: pop_bg.c pop_bg.h pop_redraw.c pop_redraw.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA)
+78
View File
@@ -0,0 +1,78 @@
/*
* pop_redraw.c — пометки перерисовки тайлов и их разбор. См. pop_redraw.h.
*
* Живёт в W1/W2 (НЕ в резиденте W3): пометки ставит логика, которая должна
* оставаться достижимой из будущих __banked-модулей. Сам разбор зовёт
* pop_bg (резидент W3) — это законно, потому что разбор дёргает только
* главный цикл.
*/
#include <stdint.h>
#include "pop_redraw.h"
#include "pop_bg.h"
#define NTILES 30
#define NCOLS 10
static uint8_t rd_kind[NTILES], rd_cnt[NTILES];
static uint8_t rda_kind[NCOLS], rda_cnt[NCOLS];
/* Сколько всего непустых пометок: без него проход по 40 ячейкам крутился бы
* КАЖДЫЙ кадр впустую (замер 2026-07-27 на прежнем rest-цикле: ~12 % кадра). */
static uint8_t rd_pending;
void pop_redraw_reset(void)
{
uint8_t i;
for (i = 0; i < NTILES; i++) { rd_kind[i] = 0; rd_cnt[i] = 0; }
for (i = 0; i < NCOLS; i++) { rda_kind[i] = 0; rda_cnt[i] = 0; }
rd_pending = 0;
}
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages)
{
if (tilepos >= NTILES) return;
if (!rd_cnt[tilepos]) rd_pending++;
rd_kind[tilepos] = kind;
if (pages > rd_cnt[tilepos]) rd_cnt[tilepos] = pages;
}
void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages)
{
if (col >= NCOLS) return;
if (!rda_cnt[col]) rd_pending++;
rda_kind[col] = kind;
if (pages > rda_cnt[col]) rda_cnt[col] = pages;
}
/* redraw_needed (seg008:0178): пройти пометки и нарисовать. row/col ведём
* бегущими счётчиками, а не через i/10 и i%10 — у Z80 нет деления, и SDCC
* выносит __divuchar/__moduchar в вершину цикла как общее подвыражение
* аргументов (30*2 делений за кадр даже когда рисовать нечего). */
void pop_redraw_needed(void)
{
uint8_t i, row = 0, col = 0, left = 0;
if (!rd_pending) return;
for (i = 0; i < NTILES; i++) {
if (rd_cnt[i]) {
switch (rd_kind[i]) {
case POP_RD_SPIKE: pop_spike_redraw(row, col); break;
case POP_RD_LOOSE: pop_loose_shake_draw(row, col); break;
case POP_RD_LOOSE_GONE: pop_loose_bake_empty(row, col); break;
case POP_RD_FLOOR: pop_floor_bake(row, col); break;
case POP_RD_LEVELDOOR: pop_leveldoor_redraw(row, col); break;
default: break;
}
if (--rd_cnt[i]) left++;
else rd_kind[i] = 0;
}
if (++col == 10) { col = 0; row++; }
}
for (i = 0; i < NCOLS; i++) {
if (rda_cnt[i]) {
if (rda_kind[i] == POP_RDA_CEIL) pop_ceil_shake_draw(i);
else pop_ceil_bake_empty(i);
if (--rda_cnt[i]) left++;
else rda_kind[i] = 0;
}
}
rd_pending = left;
}
+50
View File
@@ -0,0 +1,50 @@
/*
* pop_redraw.h — пометки «перерисовать тайл» и их разбор.
*
* Порт архитектуры оригинала: логика анимации НИЧЕГО не рисует, она ставит
* флаг (`set_redraw_full`/`set_wipe`/`redraw_20h`/`redraw_21h`, seg007), а
* отрисовка идёт отдельным проходом (`redraw_needed`, seg008:0178). У нас
* так же: pop_map/pop_trob зовут pop_set_redraw*, а pop_redraw_needed()
* (главный цикл) разбирает пометки и дёргает pop_bg.
*
* ЗАЧЕМ (кроме сходства с SDLPoP): из __banked-кода резидентная страница W3
* недостижима — ни напрямую, ни транзитивно (docs/layout_plan_v2.md §2 R2).
* Пока pop_map звал pop_bg, его нельзя было использовать из банка стражей.
* Пометки живут в W1/W2 и зовутся откуда угодно.
*
* ОТЛИЧИЕ ОТ ОРИГИНАЛА: у нас не один экран с dirty-rect, а две страницы
* дабл-буфера со своей ОЗУ-копией фона, поэтому счётчик пометки — это ЧИСЛО
* СТРАНИЦ (обычно 2), а вид перерисовки хранится явно: heal+рисовать поверх
* (транзиент) или запечь новый фон. В оригинале вид кодируется тем, в какой
* из таблиц redraw_frames_* стоит флаг.
*/
#ifndef POP_REDRAW_H
#define POP_REDRAW_H
#include <stdint.h>
/* Виды перерисовки тайла (row*10+col). */
#define POP_RD_SPIKE 1 /* пики: heal + оба тайла (redraw_21h) */
#define POP_RD_LOOSE 2 /* loose: дрожащий кадр / кадр покоя */
#define POP_RD_LOOSE_GONE 3 /* loose провалился: запечь пусто */
#define POP_RD_FLOOR 4 /* пол изменился: кнопка/щебень/поднятый меч */
#define POP_RD_LEVELDOOR 5 /* створка двери уровня (ПРАВАЯ половина) */
/* Виды перерисовки полосы у потолка (по колонке). */
#define POP_RDA_CEIL 1 /* плита-потолок: дрожащий кадр / покой */
#define POP_RDA_CEIL_GONE 2 /* плита-потолок провалилась: колодец */
/* Пометить тайл/колонку: kind — вид, pages — на скольких страницах
* дабл-буфера перерисовать (1 = только текущая, 2 = обе). Повторная
* пометка перекрывает вид и берёт максимум счётчика. */
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages);
void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages);
/* Разобрать пометки: нарисовать всё помеченное на ТЕКУЩЕЙ (back) странице и
* уменьшить счётчики. Звать из главного цикла в слое ФОНА — до kid_draw. */
void pop_redraw_needed(void);
/* Сбросить все пометки (смена комнаты/уровня). */
void pop_redraw_reset(void);
#endif
+21 -62
View File
@@ -5,7 +5,13 @@
*/ */
#include "pop_trob.h" #include "pop_trob.h"
#include "pop_level.h" #include "pop_level.h"
#include "pop_bg.h" /* pop_spike_redraw */ #include "pop_redraw.h" /* пометки перерисовки тайлов (вместо прямых вызовов) */
/* pop_bg нужен ТОЛЬКО для пламени факела и пузырька зелья: это не тайловая
* перерисовка, а покадровый оверлей поверх фона (в оригинале — свои
* add_backtable/add_foretable внутри draw_tile). Всё остальное идёт через
* пометки, поэтому pop_process_trobs — единственная функция модуля, которую
* нельзя звать из __banked-кода. */
#include "pop_bg.h"
/* Коды тайлов (fg & 0x1F), которые анимируются. */ /* Коды тайлов (fg & 0x1F), которые анимируются. */
#define TILE_SPIKE 0x02 #define TILE_SPIKE 0x02
@@ -30,11 +36,6 @@ static trob_t trobs[TROBS_MAX];
static uint8_t trobs_count; static uint8_t trobs_count;
static uint8_t trob_code[TROBS_MAX]; /* префетч кодов тайлов на кадр (1 map W0) */ static uint8_t trob_code[TROBS_MAX]; /* префетч кодов тайлов на кадр (1 map W0) */
/* Отложенная перерисовка «покоя» законченных пик/кнопок текущей комнаты
* (2 кадра = обе страницы дабл-буфера, как loose_rest). */
static uint8_t spike_rest[POP_ROOMTILES];
static uint8_t button_rest[POP_ROOMTILES];
/* Последняя НАРИСОВАННАЯ draw-сигнатура тайла (spike: modifier; button: /* Последняя НАРИСОВАННАЯ draw-сигнатура тайла (spike: modifier; button:
* признак «нажата» = таймер связи >1). Редрой пики/кнопки — ТОЛЬКО при её * признак «нажата» = таймер связи >1). Редрой пики/кнопки — ТОЛЬКО при её
* изменении (порт redraw_11h-on-change из SDLPoP). Без этого кнопка под * изменении (порт redraw_11h-on-change из SDLPoP). Без этого кнопка под
@@ -43,18 +44,6 @@ static uint8_t button_rest[POP_ROOMTILES];
* расхождение было именно в безусловном редрое). */ * расхождение было именно в безусловном редрое). */
static uint8_t trob_drawn[POP_ROOMTILES]; static uint8_t trob_drawn[POP_ROOMTILES];
/* Сколько тайлов ждут отложенного редроя (spike_rest/button_rest). Без
* него цикл по 30 тайлам крутился КАЖДЫЙ кадр впустую: замер в MAME
* (2026-07-27) — служебная часть pop_process_trobs ~50 000 тактов при
* кадре 430 080, то есть ~12 % кадра на пустой проход. */
static uint8_t rest_pending;
/* Дверь уровня: отложенный редрой ВТОРОЙ страницы дабл-буфера. Створка
* анимируется покадрово в back-страницу, поэтому последний кадр анимации
* попадает только на одну из двух страниц — вторая застревает на шаг раньше
* и дверь «дрожит» через кадр. ldoor_pos = tilepos ПРАВОЙ половины. */
static uint8_t ldoor_rest, ldoor_pos;
/* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и /* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и
* выбора следующего кадра пламени. */ * выбора следующего кадра пламени. */
static unsigned long trob_seed; static unsigned long trob_seed;
@@ -70,9 +59,8 @@ void pop_trob_reset(void)
{ {
uint8_t i; uint8_t i;
for (i = 0; i < POP_NROOMS; i++) room_seen[i] = 0; for (i = 0; i < POP_NROOMS; i++) room_seen[i] = 0;
for (i = 0; i < POP_ROOMTILES; i++) { spike_rest[i] = 0; button_rest[i] = 0; trob_drawn[i] = 0; } for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
rest_pending = 0; pop_redraw_reset();
ldoor_rest = 0;
trobs_count = 0; trobs_count = 0;
} }
@@ -116,9 +104,8 @@ void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type)
void pop_trob_room_changed(void) void pop_trob_room_changed(void)
{ {
uint8_t i; uint8_t i;
for (i = 0; i < POP_ROOMTILES; i++) { spike_rest[i] = 0; button_rest[i] = 0; trob_drawn[i] = 0; } for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
rest_pending = 0; pop_redraw_reset();
ldoor_rest = 0;
} }
/* anim_tile_modif (seg000:0B12) — при входе в комнату завести анимации её /* anim_tile_modif (seg000:0B12) — при входе в комнату завести анимации её
@@ -373,26 +360,6 @@ void pop_process_trobs(uint8_t cur_room)
* выносит оба деления (__divuchar/__moduchar) в вершину цикла как общее * выносит оба деления (__divuchar/__moduchar) в вершину цикла как общее
* подвыражение аргументов redraw — 30*2=60 делений/кадр даже когда нет * подвыражение аргументов redraw — 30*2=60 делений/кадр даже когда нет
* активных пик/кнопок (тот же случай, что был в pop_loose_tick). */ * активных пик/кнопок (тот же случай, что был в pop_loose_tick). */
if (rest_pending) {
uint8_t row = 0, col = 0, left = 0;
for (i = 0; i < POP_ROOMTILES; i++) {
if (spike_rest[i]) {
pop_spike_redraw(row, col);
if (--spike_rest[i]) left++;
}
if (button_rest[i]) {
pop_button_redraw(row, col);
if (--button_rest[i]) left++;
}
if (++col == 10) { col = 0; row++; }
}
rest_pending = left;
}
if (ldoor_rest) { /* доложить финальный кадр створки на 2-ю страницу */
pop_leveldoor_redraw(ldoor_pos / 10, ldoor_pos % 10);
ldoor_rest--;
}
if (trobs_count == 0) return; if (trobs_count == 0) return;
/* Префетч кодов тайлов всех trob'ов за ОДИН map/unmap окна 0, а не /* Префетч кодов тайлов всех trob'ов за ОДИН map/unmap окна 0, а не
@@ -477,24 +444,20 @@ void pop_process_trobs(uint8_t cur_room)
* неизменной сигнатурой, и стёртое уже не возвращается: рядом с * неизменной сигнатурой, и стёртое уже не возвращается: рядом с
* Кидом остриё пропадало, а нижний кусок мерцал (стёрт на одной * Кидом остриё пропадало, а нижний кусок мерцал (стёрт на одной
* странице дабл-буфера из двух). */ * странице дабл-буфера из двух). */
uint8_t trow = tp / 10, tcol = tp % 10; /* Последний кадр (пика убралась, trob умирает) — пометить ОБЕ
pop_spike_redraw(trow, tcol); * страницы: живой trob метит каждый кадр по одной. */
/* Последний кадр (пика убралась, trob умирает) — на ВТОРУЮ страницу pop_set_redraw(tp, POP_RD_SPIKE, (uint8_t)(type < 0 ? 2 : 1));
* его уже никто не положит: ставим отложенный редрой. */
if (type < 0) { spike_rest[tp] = 1; rest_pending++; }
} }
if (room == cur_room && code == TILE_LEVELDOOR) { if (room == cur_room && code == TILE_LEVELDOOR) {
/* Створка едет каждый кадр — перерисовываем ПРАВУЮ половину (там /* Створка едет каждый кадр — перерисовываем ПРАВУЮ половину (там
* её рисует draw_tile_anim_right) С ЗАПЕЧКОЙ в ОЗУ-копию: иначе * её рисует draw_tile_anim_right) С ЗАПЕЧКОЙ в ОЗУ-копию: иначе
* kid_heal вернул бы из фона закрытую створку. Куски двери * kid_heal вернул бы из фона закрытую створку. Куски двери
* непрозрачны, поэтому wipe не нужен. */ * непрозрачны, поэтому wipe не нужен. */
uint8_t trow = tp / 10, tcol = tp % 10; /* Створку рисует ПРАВАЯ половина двери; последний кадр анимации
if (tcol + 1 < 10) { * помечаем на обе страницы, иначе дверь дрожит через кадр. */
pop_leveldoor_redraw(trow, tcol + 1); if (tp % 10 + 1 < 10)
/* Анимация кончилась — этот кадр лёг только на ОДНУ страницу; pop_set_redraw((uint8_t)(tp + 1), POP_RD_LEVELDOOR,
* повторить его на второй, иначе дверь дрожит через кадр. */ (uint8_t)(type < 0 ? 2 : 1));
if (type < 0) { ldoor_pos = (uint8_t)(tp + 1); ldoor_rest = 1; }
}
} }
if (room == cur_room && code == TILE_SWORD) { if (room == cur_room && code == TILE_SWORD) {
/* МЕЧ — редрой только на смене видимого кадра (блеск = modif==1): /* МЕЧ — редрой только на смене видимого кадра (блеск = modif==1):
@@ -503,9 +466,7 @@ void pop_process_trobs(uint8_t cur_room)
* следующим кадром через rest-цикл. */ * следующим кадром через rest-цикл. */
uint8_t sig = (uint8_t)(mod[tp] == 1); uint8_t sig = (uint8_t)(mod[tp] == 1);
if (sig != trob_drawn[tp]) { if (sig != trob_drawn[tp]) {
uint8_t trow = tp / 10, tcol = tp % 10; pop_set_redraw(tp, POP_RD_FLOOR, 2); /* обе страницы */
pop_button_redraw(trow, tcol);
button_rest[tp] = 1; rest_pending++;
trob_drawn[tp] = sig; trob_drawn[tp] = sig;
} }
} }
@@ -517,9 +478,7 @@ void pop_process_trobs(uint8_t cur_room)
* считаем только в момент редроя (редко), а не каждый кадр. */ * считаем только в момент редроя (редко), а не каждый кадр. */
uint8_t sig = (uint8_t)(get_doorlink_timer(mod[tp]) > 1); uint8_t sig = (uint8_t)(get_doorlink_timer(mod[tp]) > 1);
if (sig != trob_drawn[tp]) { if (sig != trob_drawn[tp]) {
uint8_t trow = tp / 10, tcol = tp % 10; pop_set_redraw(tp, POP_RD_FLOOR, 2); /* обе страницы */
pop_button_redraw(trow, tcol);
button_rest[tp] = 1; rest_pending++;
trob_drawn[tp] = sig; trob_drawn[tp] = sig;
} }
} }
+5 -2
View File
@@ -15,6 +15,7 @@
#include "pop_map.h" #include "pop_map.h"
#include "pop_level.h" #include "pop_level.h"
#include "pop_trob.h" #include "pop_trob.h"
#include "pop_redraw.h"
__sfr __at (0xFE) io_border; __sfr __at (0xFE) io_border;
@@ -343,8 +344,10 @@ int main(void)
pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */ pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */
/* — ДО Kid, чтобы Kid всегда был поверх плиты) */ /* — ДО Kid, чтобы Kid всегда был поверх плиты) */
io_border = 6; /* голубой */ io_border = 6; /* голубой */
pop_process_trobs(cur_room); /* пики/ворота: продвинуть + перерисовать (тоже */ pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */
/* слой фона — ДО Kid) */ /* process_trobs — рисования внутри нет) */
pop_redraw_needed(); /* и разобрать пометки перерисовки тайлов (порт */
/* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */
io_border = 4; /* синий */ io_border = 4; /* синий */
{ /* ворота соседа слева (room8) видны в col0 — */ { /* ворота соседа слева (room8) видны в col0 — */
uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */ uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */