PoP roomtest: fore-слой — окно клипа, кэш кладки; кадр 2.5 -> 0.78 периода

Жалоба: стойка неподвижных Кида и стража занимала больше полутора
кадровых периодов.  Гипотеза «виноват __banked» ЗАМЕРОМ НЕ
ПОДТВЕРДИЛАСЬ: весь банковый вызов (трамплин + смена страницы + тело
pop_guard_tick + возврат) стоит 654 такта при бюджете кадра 430 000.

Как мерил (выборка PC бесполезна — мост MAME отвечает из фреймового
колбэка, все сэмплы падают в обработчик прерывания): breakpoint'ы MAME с
действием {printf totalcycles; g} на входах фаз главного цикла, разности
соседних меток = стоимость фазы.  Плюс профилирование полосами бордюра
(make PROF=1, макрос PROF() в roomtest.c) для быстрого взгляда.

Замер комнаты 3 (Kid + страж), такты, кадр = 430 000:
  fore поверх стража  431 964
  fore поверх Kid     402 816   -> 78 % всей работы кадра
  остальное           241 956
  ИТОГО             1 076 736   = 2.5 кадра

Две причины, обе устранены:

1. Fore-слой рисовал ЦЕЛЫЕ тайлы, хотя существует ровно для того, чтобы
   вернуть куски поверх спрайта — за его прямоугольником в видеопамяти и
   так правильный фон.  Введено ОКНО клипа (pop_fore_set_clip): спрайт
   сообщает свой итоговый габарит (у Kid — с клинком, брызгами и
   обрезкой clip_char), fore-проход режет по нему.  Отсев трёхступенчатый:
   тайл целиком (tile_in_fclip, до обращения к атласу), кусок по грубому
   габариту (до gfx_w0_map — w/h лежат в EMM-странице), и точный клип в
   blit_b.  Для последнего добавлен libbgi-примитив
   gfx_blit_part_noclip — пара к gfx_blit_noclip, но под-прямоугольник.

2. Оставшиеся 341 К после клипа оказались НЕ пикселями: 18 вызовов
   pop_prandom за проход, ~10 700 тактов каждый (32-битный LCG:
   __mullong ~8 000 + __moduint).  Раскладка кладки тайла — чистая
   функция (комната, ряд, колонка), то есть константа комнаты, а
   wall_pattern пересчитывал её каждый кадр.  Теперь кэшируются готовые
   РЕШЕНИЯ (что рисовать и с каким смещением), 3 байта на тайл, сброс в
   pop_room_draw.  Порядок вызовов prandom воспроизведён один в один,
   включая то, что значение метки берётся только при сработавшем условии.

Итог того же замера: fore поверх стража 37 464, поверх Kid 46 464,
кадр целиком 334 716 = 0.78 периода (было 2.5).  Ускорение 3.2x, сами
fore-проходы — 10x.

Проверка отсутствия регрессии: попиксельная разность скриншотов комнат
1/2/3 до и после — отличаются ТОЛЬКО языки пламени факелов (анимация),
кладка и метки совпадают байт в байт.

Побочно: sprinter-cc научился пробрасывать -DNAME в sdcc.

Память: куча W2 1245 -> 996 Б (кэш кладки 120 Б), резидент W3 12 819 ->
14 512 (свободно 1872 Б — становится тесно), банк 1 236/16384.

ВНИМАНИЕ: make size-check показывает рост 7 программ, но эталон
docs/size_baseline.tsv отстал (последний раз принят в 484b18d, libbgi
менялась в 95c22be/c127a4b/64ce633) — к этой правке рост отношения не
имеет: новый модуль библиотеки в чужие программы не линкуется.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 00:19:32 +03:00
parent af5f0a4638
commit f2093e0d89
9 changed files with 304 additions and 67 deletions
+5 -1
View File
@@ -12,7 +12,11 @@ MEMORY ?= huge # small-раскладка + банки кода в W
# оставаться достижимым из __banked-модулей (guards.c): из банка
# резидентная страница W3 не видна — ни напрямую, ни транзитивно.
# См. docs/layout_plan_v2.md §2 (R2), §4.
EXTRA_FLAGS ?= --gfx 256 --w3 pop_bg.c --w3 pop_gdraw.c --bank 1=guards.c
EXTRA_FLAGS ?= --gfx 256 --w3 pop_bg.c --w3 pop_gdraw.c --bank 1=guards.c $(PROF_FLAGS)
# make PROF=1 — профилирование полосами бордюра (см. PROF() в roomtest.c):
# каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
# MAME = время фазы в растровых строках.
PROF_FLAGS := $(if $(PROF),-DPROF_BORDER)
EXTRA_SRCS := pop_kid.c pop_ctrl.c pop_level.c pop_map.c pop_trob.c pop_redraw.c pop_geom.c pop_guard.c
BG_DIR := $(CURDIR)/../poc/res/bg
+188 -53
View File
@@ -62,33 +62,73 @@ static int g_clip_top;
/* Блит ленты idx атласа a: НИЗ спрайта на ybottom, левый край x,
* прозрачно (0xFF). w/h читаются из getimage-заголовка ленты (в W0).
* При g_clip_top рисуем только часть кадра ниже границы (gfx_blit_part). */
/* ОКНО FORE-СЛОЯ (экранные координаты, полуинтервал). Пока оно включено,
* blit_b рисует только ту часть куска, что попадает в окно.
*
* Зачем: fore-слой существует РОВНО для того, чтобы вернуть куски тайлов
* поверх нарисованного персонажа — за пределами его прямоугольника в
* видеопамяти и так лежит правильный фон. А рисовал он тайлы ЦЕЛИКОМ:
* стена (код 20) — блок 64×63 = 4032 пикселя, и таких кусков за проход
* до девяти. Замер в MAME 2026-07-29 (комната 3, Kid + страж): проход
* поверх Kid — 402 816 тактов, поверх стража — 431 964, вместе 78 % всей
* работы кадра при бюджете 430 000 тактов на кадр. С окном большинство
* кусков отсекается целиком, остальные режутся до пересечения. */
static int fclip_x0, fclip_y0, fclip_x1, fclip_y1;
static uint8_t fclip_on;
void pop_fore_set_clip(int x, int y, int w, int h)
{
fclip_x0 = x; fclip_y0 = y;
fclip_x1 = x + w; fclip_y1 = y + h;
}
static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
{
const uint8_t *img;
uint16_t w, h;
int top;
int top, dx, dy, dw, dh, sx = 0, sy = 0;
if (idx >= a->count)
return;
/* Грубый отсев ДО маппинга страницы: реальные w/h лежат в заголовке
* ленты, то есть в EMM-странице, и чтобы их прочитать, надо сделать
* gfx_w0_map. Берём заведомо больший габарит (кадры фона не крупнее
* 64×64) и, если он не задевает окно, уходим не заплатив ни за
* atlas_image, ни за map/unmap. */
if (fclip_on &&
(x >= fclip_x1 || x + 64 <= fclip_x0 ||
ybottom + POP_YOFF + 1 <= fclip_y0 ||
ybottom + POP_YOFF - 64 >= fclip_y1))
return;
img = (const uint8_t *)atlas_image(a, idx);
gfx_w0_map(a->page);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
top = ybottom - (int)h + 1 + POP_YOFF; /* +YOFF: центрирование */
if (w && h) {
if (g_clip_top && top < g_clip_top) {
int sy = g_clip_top - top; /* пропустить верхние строки */
if (sy < (int)h)
gfx_blit_part(x, g_clip_top, img, 0, sy, (int)w, (int)h - sy);
} else if (x >= 0 && top >= 0 && w < 256 && h < 256 &&
x + (int)w <= 320 && top + (int)h <= 256) {
/* Спрайт целиком на экране — линейное ядро без клипа: ~2.9×
* быстрее (замер: 4617 против 13288 тактов на спрайт 32×3;
* общее ядро gfx_blit платит за клип/16-бит/split, а не за
* пиксели). W3-скобку ставит сама libbgi: из кода, собранного
* с --w3, её вызывать нельзя (окно W3 уходит под видеобанк). */
gfx_blit_noclip(x, top, img);
dx = x; dy = top; dw = (int)w; dh = (int)h;
if (g_clip_top && dy < g_clip_top) { /* полоса у потолка */
sy = g_clip_top - dy; dy = g_clip_top; dh -= sy;
}
if (fclip_on) { /* окно fore-слоя */
if (dx < fclip_x0) { int d = fclip_x0 - dx; sx += d; dx += d; dw -= d; }
if (dy < fclip_y0) { int d = fclip_y0 - dy; sy += d; dy += d; dh -= d; }
if (dx + dw > fclip_x1) dw = fclip_x1 - dx;
if (dy + dh > fclip_y1) dh = fclip_y1 - dy;
}
if (w && h && dw > 0 && dh > 0) {
/* Целиком на экране — линейное ядро без клипа: ~2.9× быстрее
* (замер: 4617 против 13288 тактов на спрайт 32×3; общее ядро
* gfx_blit платит за клип/16-бит/split, а не за пиксели).
* W3-скобку ставит сама libbgi: из кода, собранного с --w3, её
* вызывать нельзя (окно W3 уходит под видеобанк). */
if (dx >= 0 && dy >= 0 && dw < 256 && dh < 256 &&
dx + dw <= 320 && dy + dh <= 256 && w < 256) {
if (sx == 0 && sy == 0 && dw == (int)w && dh == (int)h)
gfx_blit_noclip(dx, dy, img);
else
gfx_blit_part_noclip(dx, dy, img, (uint8_t)sx, (uint8_t)sy,
(uint8_t)dw, (uint8_t)dh);
} else {
gfx_blit(x, top, img); /* край экрана — с клипом */
gfx_blit_part(dx, dy, img, sx, sy, dw, dh); /* край экрана — с клипом */
}
}
gfx_w0_unmap();
@@ -339,64 +379,134 @@ static void draw_right_mark(uint16_t dv, int arg1, int row, int col)
}
/* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */
static void wall_pattern(int row, int col, int which_part)
{
int xh = COL_XH[col];
int dby = 63 * row + 65;
unsigned long saved = rnd_seed;
uint16_t mid_div, mid_off, bot_div, bot_off;
uint8_t bg;
/* КЭШ РАСКЛАДКИ КЛАДКИ. Серия prandom для тайла зависит только от
* (комната, ряд, колонка) — пока мы в комнате, это КОНСТАНТА. А
* wall_pattern зовётся из fore-слоя КАЖДЫЙ кадр, и на Z80 это самая
* дорогая вещь в кадре: замер в MAME 2026-07-29 (комната 3) — 18 вызовов
* pop_prandom за один fore-проход, ~10 700 тактов каждый (32-битный LCG:
* __mullong ~8 000 + __moduint), то есть 193 К из 341 К всего прохода при
* бюджете кадра ~430 К. Кэшируем не сид, а ГОТОВЫЕ РЕШЕНИЯ (что рисовать
* и с каким смещением) — тогда в кадре не остаётся ни одного prandom.
*
* Порядок вызовов prandom воспроизведён один в один, включая то, что
* значение метки берётся ТОЛЬКО когда сработало её условие: иначе
* последовательность разъедется и кладка станет другой. which_part=0
* использует префикс той же серии, поэтому кэш общий.
*
* Упаковка: wp_a = mid_div | mid_off<<1 | bot_div<<4 | bot_off<<5;
* wp_b = rndblk | right<<1 | right_val<<2 | left<<4 | left_val<<5.
* Индекс — (row+1)*10+col, ряды 1..2. */
#define WP_N 40
static uint8_t wp_a[WP_N], wp_b[WP_N], wp_ok[WP_N];
static void wall_pattern_reset(void) /* смена комнаты — сид другой */
{
uint8_t i;
for (i = 0; i < WP_N; i++) wp_ok[i] = 0;
}
/* Выходы wall_rnd — в файловых статиках: возвращать девять значений
* через указатели на Z80 дороже самой функции. */
static uint8_t wr_mid_div, wr_mid_off, wr_bot_div, wr_bot_off;
static uint8_t wr_rndblk, wr_right, wr_rightv, wr_left, wr_leftv;
static void wall_rnd(int row, int col)
{
/* Кэшируем только штатную сетку: колонка вне 0..9 дала бы индекс
* соседнего РЯДА и чужую раскладку (тихий баг вместо промаха). */
int idx = (col >= 0 && col < 10) ? (row + 1) * 10 + col : -1;
uint8_t a, b, bg;
unsigned long saved;
if (idx >= 0 && idx < WP_N && wp_ok[idx]) {
a = wp_a[idx]; b = wp_b[idx];
wr_mid_div = (uint8_t)(a & 1); wr_mid_off = (uint8_t)((a >> 1) & 7);
wr_bot_div = (uint8_t)((a >> 4) & 1); wr_bot_off = (uint8_t)((a >> 5) & 7);
wr_rndblk = (uint8_t)(b & 1);
wr_right = (uint8_t)((b >> 1) & 1); wr_rightv = (uint8_t)((b >> 2) & 3);
wr_left = (uint8_t)((b >> 4) & 1); wr_leftv = (uint8_t)((b >> 5) & 7);
return;
}
saved = rnd_seed;
/* Верхняя полоса (row<0): SDLPoP рисует её как ряд 2 комнаты сверху
* (drawn_row=2 для seed, draw_main_y=-1 для позиции) — seed по row 2. */
rnd_seed = (unsigned long)(g_room + TBL_LINE[row < 0 ? 2 : row] + col);
prandom(1); /* сброс */
mid_div = prandom(1);
mid_off = prandom(4);
bot_div = prandom(1);
bot_off = prandom(4);
wr_mid_div = (uint8_t)prandom(1);
wr_mid_off = (uint8_t)prandom(4);
wr_bot_div = (uint8_t)prandom(1);
wr_bot_off = (uint8_t)prandom(4);
wr_rndblk = wr_right = wr_left = 0;
wr_rightv = wr_leftv = 0;
bg = wall_modifier(row, col);
if (bg == 3) { /* WWW */
wr_rndblk = (uint8_t)(prandom(4) == 0);
if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)prandom(3); }
if (prandom(4) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(4); }
} else if (bg == 0) { /* SWS */
if (prandom(6) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(1); }
} else if (bg == 1) { /* SWW */
wr_rndblk = (uint8_t)(prandom(4) == 0);
if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)prandom(3); }
if (prandom(4) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(3); }
} else { /* WWS (bg==2) */
if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)(prandom(1) + 2); }
if (prandom(4) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(4); }
}
rnd_seed = saved;
if (idx >= 0 && idx < WP_N) {
wp_a[idx] = (uint8_t)(wr_mid_div | (wr_mid_off << 1) |
(wr_bot_div << 4) | (wr_bot_off << 5));
wp_b[idx] = (uint8_t)(wr_rndblk | (wr_right << 1) | (wr_rightv << 2) |
(wr_left << 4) | (wr_leftv << 5));
wp_ok[idx] = 1;
}
}
/* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */
static void wall_pattern(int row, int col, int which_part)
{
int xh = COL_XH[col];
int dby = 63 * row + 65;
uint8_t bg = wall_modifier(row, col);
wall_rnd(row, col);
if (bg == 3) { /* WWW */
if (which_part) {
if (prandom(4) == 0)
if (wr_rndblk)
wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42);
wall_b((uint8_t)(RES_WALL_DIVIDER1 + mid_div), (xh + 1) * 8 + mid_off, dby - 21);
wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21);
}
wall_b((uint8_t)(RES_WALL_DIVIDER1 + bot_div), xh * 8 + bot_off, dby);
wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_bot_div), xh * 8 + wr_bot_off, dby);
if (which_part) {
if (prandom(4) == 0)
draw_right_mark(prandom(3), mid_off, row, col);
if (prandom(4) == 0)
draw_left_mark(prandom(4), mid_off - mid_div, bot_off - bot_div, row, col);
if (wr_right)
draw_right_mark(wr_rightv, wr_mid_off, row, col);
if (wr_left)
draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
}
} else if (bg == 0) { /* SWS */
if (which_part) {
if (prandom(6) == 0)
draw_left_mark(prandom(1), mid_off - mid_div, bot_off - bot_div, row, col);
}
if (which_part && wr_left)
draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
} else if (bg == 1) { /* SWW */
if (which_part) {
if (prandom(4) == 0)
if (wr_rndblk)
wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42);
wall_b((uint8_t)(RES_WALL_DIVIDER1 + mid_div), (xh + 1) * 8 + mid_off, dby - 21);
if (prandom(4) == 0)
draw_right_mark(prandom(3), mid_off, row, col);
if (prandom(4) == 0)
draw_left_mark(prandom(3), mid_off - mid_div, bot_off - bot_div, row, col);
wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21);
if (wr_right)
draw_right_mark(wr_rightv, wr_mid_off, row, col);
if (wr_left)
draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
}
} else { /* WWS (bg==2) */
if (which_part)
wall_b((uint8_t)(RES_WALL_DIVIDER1 + mid_div), (xh + 1) * 8 + mid_off, dby - 21);
wall_b((uint8_t)(RES_WALL_DIVIDER1 + bot_div), xh * 8 + bot_off, dby);
wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21);
wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_bot_div), xh * 8 + wr_bot_off, dby);
if (which_part) {
if (prandom(4) == 0)
draw_right_mark((uint16_t)(prandom(1) + 2), mid_off, row, col);
if (prandom(4) == 0)
draw_left_mark(prandom(4), mid_off - mid_div, bot_off - bot_div, row, col);
if (wr_right)
draw_right_mark(wr_rightv, wr_mid_off, row, col);
if (wr_left)
draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
}
}
rnd_seed = saved;
}
/* topright тайла снизу-слева (seg008.c:410). */
@@ -1088,6 +1198,7 @@ void pop_room_draw(uint8_t room_num,
{
int row, col;
g_room = room_num;
wall_pattern_reset(); /* кэш кладки привязан к номеру комнаты */
g_fg = fg; g_bg = bg;
g_lfg = lcol_fg; g_lbg = lcol_bg;
@@ -1177,8 +1288,25 @@ static void ceil_over_kid_tile(int col)
fore_only_tile(-1, col); /* draw_tile_fore */
}
/* Пересекается ли габарит тайла с окном fore-слоя. Шаг тайла — 32 px
* (COL_XH[col]*8 = col*32), куски выходят вправо максимум на +8 (дивайдеры
* и метки рисуются в ячейке правого соседа) и вверх на 42+высота кадра.
* Даёт отсев ЦЕЛОГО тайла ДО обращения к атласу: спрайт 27..40 px шириной
* задевает не все тайлы футпринта, а платили за каждый (atlas_image +
* gfx_w0_map + разбор заголовка на КАЖДЫЙ кусок). */
static uint8_t tile_in_fclip(int row, int col)
{
int x0, y0;
if (!fclip_on) return 1;
x0 = COL_XH[col] * 8;
y0 = POP_YOFF + 63 * row;
return (uint8_t)(x0 < fclip_x1 && x0 + 40 > fclip_x0 &&
y0 - 8 < fclip_y1 && y0 + 70 > fclip_y0);
}
static void fore_tile(int row, int col) /* передний слой одного тайла */
{
if (!tile_in_fclip(row, col)) return;
/* draw_loose (seg008:0A38) — единственный кусок ТАЙЛА, который оригинал
* кладёт В ОБЕ таблицы БЕЗУСЛОВНО: нижняя грань loose-плиты идёт и в
* backtable, и в foretable. Значит передняя грань плиты рисуется ПОВЕРХ
@@ -1211,7 +1339,9 @@ static void climb_overlay_tile(int row, int col, uint8_t fidx)
{
/* тип — ПОДСТАВЛЕННЫЙ (нажатая кнопка = пол): иначе кромка, за которую
* Kid спускается/подтягивается, закрашивается целым тайлом */
uint8_t code = tile_code_drawn(row, col);
uint8_t code;
if (!tile_in_fclip(row, col)) return;
code = tile_code_drawn(row, col);
int x, dmy, dby;
const piece *t;
if (tile_code(row, col - 1) != 0) return; /* слева не пусто → не кромка */
@@ -1301,8 +1431,9 @@ static void overlay_mid_tile(int row, int col)
* сразу), поэтому порядок эмулируется явной проверкой. */
static void other_overlay_tile(int row, int col)
{
uint8_t code = tile_code(row, col);
uint8_t left;
uint8_t code, left;
if (!tile_in_fclip(row, col)) return;
code = tile_code(row, col);
/* Пустой тайл НЕ окклюдирует: у него нет тела пола/стены, а draw_tile для
* него рисует лишь ФОНОВУЮ сетку (BLUELINE «силуэт кладки» — правая грань
* пустого соседа). В оригинале она уходит в y-сортируемый midtable и
@@ -1374,6 +1505,7 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
int8_t dir, uint8_t frame, uint8_t action)
{
int8_t cL, cR, cLraw, rT, rB, rTraw, r, c;
fclip_on = 1; /* окно = прямоугольник спрайта */
char_footprint(obj_x, obj_y, w, h, dir);
cL = fp_cL; cR = fp_cR; cLraw = fp_cLraw;
rT = fp_rT; rB = fp_rB; rTraw = fp_rTraw;
@@ -1444,6 +1576,7 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
g_clip_top = 0;
}
gfx_set_bank(GFX_BANK_NORMAL);
fclip_on = 0;
}
/* Передние грани тайлов поверх ПРОИЗВОЛЬНОГО персонажа (страж, дальше —
@@ -1460,6 +1593,7 @@ void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h,
int8_t dir)
{
int8_t r, c;
fclip_on = 1; /* окно = прямоугольник спрайта */
char_footprint(obj_x, obj_y, w, h, dir);
gfx_set_bank(GFX_BANK_SPRITE);
for (r = fp_rT; r <= fp_rB; r++)
@@ -1474,4 +1608,5 @@ void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h,
g_clip_top = 0;
}
gfx_set_bank(GFX_BANK_NORMAL);
fclip_on = 0;
}
+8
View File
@@ -58,6 +58,14 @@ void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_
void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
int8_t dir, uint8_t frame, uint8_t action);
/* Окно fore-слоя: прямоугольник СПРАЙТА (экранные координаты, y уже с
* POP_YOFF), поверх которого fore-проход возвращает куски тайлов. Ставит
* тот, кто нарисовал спрайт (kid_draw / pop_guard_draw) — он один знает
* итоговый габарит (у Kid — с учётом клинка и обрезки clip_char). Вне
* fore-проходов окно игнорируется: фон рисуется целиком. Мотивация и
* замер — в pop_bg.c у fclip_on. */
void pop_fore_set_clip(int x, int y, int w, int h);
/* То же для НЕ-Kid персонажа (страж): только слой fore + полоса потолка,
* без оверлеев поз виса/полёта/подъёма. obj_x — ЛОГИЧЕСКАЯ координата
* (до ×8/7), как у kid_fp_obj_x(). */
+5 -1
View File
@@ -59,7 +59,7 @@ void pop_guard_heal(void)
void pop_guard_draw(void)
{
int obj_x, obj_y, top, fwd, fp_x;
int obj_x, obj_y, top, fwd, fp_x, bx0 = 0, top0 = 0;
uint8_t page, idx, p, flip;
const uint8_t *img;
uint16_t w, h;
@@ -85,6 +85,7 @@ void pop_guard_draw(void)
flip = (uint8_t)(Guard.direction >= 0); /* спрайты нарисованы влево */
if (flip) bx -= (int)w;
top = obj_y - (int)h + 1;
bx0 = bx; top0 = top;
p = gfx_get_draw_page() & 1;
gfx_set_bank(GFX_BANK_SPRITE);
gfx_blit_cols_part(bx, top + POP_YOFF, img, flip, 0, 0);
@@ -92,6 +93,9 @@ void pop_guard_draw(void)
gx_l[p] = bx; gy_l[p] = top; gw_l[p] = w; gh_l[p] = h; gvalid[p] = 1;
}
gfx_w0_unmap();
/* Окно fore-слоя = прямоугольник стража (см. pop_bg.c, fclip_on):
* без него проход возвращал ЦЕЛЫЕ тайлы — 432 К тактов на кадр. */
pop_fore_set_clip(bx0, top0 + POP_YOFF, (int)w, (int)h);
/* Передние грани тайлов ПОВЕРХ стража: в оригинале персонаж — запись
* midtable, а foretable рисуется после всех midtable-записей, поэтому
* колонна/кромка пола перекрывает ЛЮБОГО персонажа, не только Kid
+20
View File
@@ -367,6 +367,9 @@ void kid_draw_splash(void)
if (top + (int)h > y1) y1 = top + (int)h;
kid_lx[dp] = x0; kid_ly[dp] = y0;
kid_lw[dp] = (uint16_t)(x1 - x0); kid_lh[dp] = (uint16_t)(y1 - y0);
/* Окно fore-слоя расширяем тем же объединением: брызги
* тоже должны уходить под передние грани тайлов. */
pop_fore_set_clip(x0, y0 + POP_YOFF, x1 - x0, y1 - y0);
}
}
}
@@ -390,6 +393,10 @@ void kid_draw(void)
const uint8_t *img;
uint16_t w, h;
/* Окно fore-слоя по умолчанию пустое: на любом раннем выходе (нет
* кадра / страница не загружена) перекрывать нечего, и fore-проход
* не должен трогать экран. */
pop_fore_set_clip(0, 0, 0, 0);
if (fr.image == 255)
return; /* пустой кадр */
/* +kid_render_dx: straddle-смещение (∓140) когда комната Kid ≠
@@ -488,4 +495,17 @@ void kid_draw(void)
}
}
gfx_w0_unmap();
/* Окно fore-слоя = итоговый прямоугольник спрайта (с клинком и
* обрезкой clip_char): fore-проход вернёт куски тайлов ТОЛЬКО поверх
* него, а не целыми тайлами — см. pop_bg.c, fclip_on. Ставим и когда
* рисовать нечего (kid_lvalid=0): нулевое окно = fore-проход не
* трогает экран, что и правильно — перекрывать нечего. */
{
uint8_t dp = gfx_get_draw_page() & 1;
if (kid_lvalid[dp])
pop_fore_set_clip(kid_lx[dp], kid_ly[dp] + POP_YOFF,
(int)kid_lw[dp], (int)kid_lh[dp]);
else
pop_fore_set_clip(0, 0, 0, 0);
}
}
+28 -11
View File
@@ -21,6 +21,26 @@
__sfr __at (0xFE) io_border;
/* ПРОФИЛИРОВАНИЕ ПОЛОСАМИ БОРДЮРА (make PROF=1). Цвет бордюра держится,
* пока фаза считает, поэтому высота полосы на скриншоте = время фазы в
* растровых строках (1 строка 224 такта Z80 при 3.5 МГц). Это
* единственный способ мерить здесь: мост MAME отвечает из фреймового
* колбэка, поэтому выборка PC вырождается (100 % сэмплов в обработчике
* прерывания). Фазы:
* 0 чёрный ожидание vsync (запас кадра)
* 1 синий ввод + heal (стереть прошлые спрайты)
* 2 красный логика (kid_tick/физика/страж в банке)
* 3 лиловый слой фона (loose/trobs/redraw_needed/шов)
* 4 зелёный kid_draw
* 5 циан guard_draw + fore поверх стража
* 6 жёлтый fore поверх Kid + чистка бортов
* 7 белый флип */
#ifdef PROF_BORDER
#define PROF(n) io_border = (n)
#else
#define PROF(n) ((void)0)
#endif
/* Сколько банков грузит crt0_banked (читает его стартовый код). */
const uint8_t n_banks = 1;
@@ -267,7 +287,7 @@ int main(void)
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
* переключ). Выключение возвращает показ на страницу 0. */
io_border = 2; /* синий - начало */
PROF(1); /* ввод + heal */
uint8_t sp = kbd_raw_down(KBD_SPACE);
if (sp && !sp_prev) {
@@ -291,7 +311,7 @@ int main(void)
cheat_prev = k;
}
if (frozen) {
io_border = 0; /* черный: ждём vsync */
PROF(0); /* чёрный: ждём vsync */
gfx_wait_vsync();
continue;
}
@@ -335,6 +355,7 @@ int main(void)
kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */
pop_guard_heal(); /* и прошлый кадр стража */
pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */
PROF(2); /* логика */
kid_tick(); /* play_seq: следующий кадр */
pop_phys_tick(); /* K3: падение/приземление/стена */
pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */
@@ -360,16 +381,14 @@ int main(void)
}
}
io_border = 4; /* зеленый */
PROF(3); /* слой фона */
pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */
/* — ДО Kid, чтобы Kid всегда был поверх плиты) */
io_border = 6; /* голубой */
pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */
/* process_trobs — рисования внутри нет) */
pop_redraw_needed(); /* и разобрать пометки перерисовки тайлов (порт */
/* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */
io_border = 4; /* синий */
{ /* ворота соседа слева (room8) видны в col0 — */
uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */
if (lft) {
@@ -386,7 +405,6 @@ int main(void)
}
}
}
io_border = 2; /* зеленый */
if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */
dead_frames++;
if (dead_frames > RESPAWN_SETTLE &&
@@ -399,13 +417,11 @@ int main(void)
pop_kid_hp_reset();
}
}
io_border = 6; /* голубой */
if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */
uint8_t fpos = (uint8_t)(pop_loose_fell - 1);
uint8_t fcol = (uint8_t)(fpos % 10);
uint8_t down = pop_room_link(cur_room, LINK_DOWN);
pop_loose_fell = 0;
io_border = 2; /* синий */
add_tile_override(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */
if (down) {
int8_t lrow = pop_room_col_landing(down, fcol);
@@ -449,16 +465,17 @@ int main(void)
flash_bg(255, 85, 85); /* color_12_brightred */
flash_on = 1;
}
io_border = 4; /* зеленый */
update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */
PROF(5); /* страж: спрайт + fore поверх него */
pop_guard_draw(); /* страж — ДО Kid (порядок как в оригинале) */
PROF(4); /* kid_draw */
kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */
pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */
if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */
kid_draw_splash();
pop_kid_hurt = 0;
}
io_border = 0; /* черный */
PROF(6); /* fore поверх Kid + борта */
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
kid_fp_width(), kid_fp_height(),
Kid.direction, Kid.frame, Kid.action);
@@ -468,7 +485,7 @@ int main(void)
* сделан (скрытая страница), wait_vsync ловит начало кадра,
* set_visible_page переключает во время бланка (gfx.h паттерн). */
io_border = 0; /* черный: ждём vsync */
PROF(0); /* чёрный: ждём vsync */
/* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО
* при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера),
+4 -1
View File
@@ -23,6 +23,7 @@
# CODE=W1|W2 DATA=W1|W2|SAME BANKED=W1|W3
# Example: --memory-manual CODE=W2,DATA=W2,BANKED=W3
# -I PATH additional include path (repeatable)
# -DNAME[=VAL] preprocessor define, passed to sdcc as is (repeatable)
# -L 0xADDR code load address (default: derived from --memory mode)
# -E 0xADDR entry address (default: same as -L)
# -S 0xADDR stack address (default: 0xBFFE)
@@ -106,6 +107,7 @@ LD_EXTRA=()
MKEXE_EXTRA=()
BANK_SPECS=() # entries like "1=engine.c"
W3_SPECS=() # entries like "mod.c" — резидентные модули окна W3 (--w3)
USER_DEFS=() # -DFOO / -DFOO=bar — пробрасываются в sdcc как есть
W3_RELS=() # заполняется при компиляции W3-модулей
W3_LD_FLAGS=() # -Wl-b_W3CODE=0xC000, если есть --w3
USER_SET_MEMORY="" # непусто, если --memory задан явно (для --w3 авто-small)
@@ -139,6 +141,7 @@ while [[ $# -gt 0 ]]; do
--w3) W3_SPECS+=("$2"); shift 2;;
--stack-size) STACK_SIZE="$2"; shift 2;;
-Wl) LD_EXTRA+=("$2"); shift 2;;
-D*) USER_DEFS+=("$1"); shift;;
--bank) BANK_SPECS+=("$2"); shift 2;;
--mkexe) MKEXE_EXTRA+=("$2"); shift 2;;
--max-allocs) MAX_ALLOCS="$2"; shift 2;;
@@ -366,7 +369,7 @@ run "$SDASZ80" -o "$HEAP_TOP_REL" "$HEAP_TOP_SRC"
# 2. user sources → .rel (HOME)
USER_RELS=()
CC_FLAGS=(-mz80 --no-std-crt0 --std-c99 --opt-code-size -I "$INC_DIR" -I "$BGI_INC_DIR" "${USER_INCS[@]}")
CC_FLAGS=(-mz80 --no-std-crt0 --std-c99 --opt-code-size -I "$INC_DIR" -I "$BGI_INC_DIR" "${USER_INCS[@]}" "${USER_DEFS[@]}")
[[ $DEBUG_RT -eq 1 ]] && CC_FLAGS+=(-DDEBUG_RT)
[[ -n "$MAX_ALLOCS" ]] && CC_FLAGS+=(--max-allocs-per-node "$MAX_ALLOCS")
for src in "${SOURCES[@]}"; do
+38
View File
@@ -0,0 +1,38 @@
/*
* gfx_blit_part_noclip блит ПОД-ПРЯМОУГОЛЬНИКА картинки getimage-формата
* БЕЗ клипа по экрану, в ТЕКУЩЕМ банке (gfx_set_bank). Пара к
* gfx_blit_noclip ровно так же, как gfx_blit_part к gfx_blit.
*
* Зачем: тайловому движку часто нужно положить поверх спрайта не весь
* тайл, а только ту его часть, что перекрывает спрайт. Через
* gfx_blit_part это идёт общим ядром _gfx_blit_full (16-битная
* арифметика, клип, нарезка полос) ~13 К тактов накладных на вызов
* независимо от размера; здесь тот же под-прямоугольник рисуется линейным
* спрайтовым ядром (~4.6 К на 32×3). Замер PoP roomtest 2026-07-29:
* fore-слой поверх персонажа рисовал 9 целых тайлов за 432 К тактов
* (стена = блок 64×63); с обрезкой по прямоугольнику спрайта тех же
* пикселей остаётся втрое меньше.
*
* Требования (проверяет ВЫЗЫВАЮЩИЙ, кода проверок здесь нет):
* - под-прямоугольник (sx,sy,w,h) лежит внутри картинки;
* - прямоугольник назначения целиком на экране;
* - w,h <= 255, y <= 255.
* Не выполняется зовите gfx_blit_part.
*
* W3-скобка ставится ЗДЕСЬ (код libbgi живёт в W1): из приложения,
* собранного с --w3, её вызывать нельзя после _bgi_begin окно W3 занято
* видеобанком и код вызывающего исчезает из адресного пространства.
*/
#include "../_bgi.h"
void gfx_blit_part_noclip(int x, int y, const void *img,
uint8_t sx, uint8_t sy, uint8_t w, uint8_t h)
{
const uint8_t *p = (const uint8_t *)img;
uint16_t stride = (uint16_t)(p[0] | (p[1] << 8));
const uint8_t *src = p + 4 + (uint16_t)sy * stride + sx;
_bgi_begin();
_gfx_blit_sprite_noclip(x, y, src, stride, w, h);
_bgi_end();
}
+8
View File
@@ -105,6 +105,14 @@ void gfx_heal_noclip(int x, int y, uint8_t w, uint8_t h);
void gfx_blit_part(int x, int y, const void *img,
int sx, int sy, int w, int h);
/* Тот же под-прямоугольник БЕЗ клипа по экрану — линейным спрайтовым
* ядром (пара к gfx_blit_noclip). Вызывающий гарантирует: под-
* прямоугольник внутри img, назначение целиком на экране, w,h,y <= 255.
* Для тайловых движков, кладущих поверх спрайта только перекрывающую его
* часть тайла (PoP: fore-слой). */
void gfx_blit_part_noclip(int x, int y, const void *img,
uint8_t sx, uint8_t sy, uint8_t w, uint8_t h);
/* Блит спрайта column-major (пиксели по колонкам) — вертикальным accel-
* проходом. flip!=0 = горизонтальное зеркало (направление персонажа) без
* CPU-реверса/второй копии; зеркало ВНУТРИ футпринта [x,x+w) (для «hot-