From f2093e0d891cfb27a551755c208e9d3d94325d7e Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Thu, 30 Jul 2026 00:19:32 +0300 Subject: [PATCH] =?UTF-8?q?PoP=20roomtest:=20fore-=D1=81=D0=BB=D0=BE=D0=B9?= =?UTF-8?q?=20=E2=80=94=20=D0=BE=D0=BA=D0=BD=D0=BE=20=D0=BA=D0=BB=D0=B8?= =?UTF-8?q?=D0=BF=D0=B0,=20=D0=BA=D1=8D=D1=88=20=D0=BA=D0=BB=D0=B0=D0=B4?= =?UTF-8?q?=D0=BA=D0=B8;=20=D0=BA=D0=B0=D0=B4=D1=80=202.5=20->=200.78=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B8=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Жалоба: стойка неподвижных Кида и стража занимала больше полутора кадровых периодов. Гипотеза «виноват __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 --- applications/PoP/roomtest/Makefile | 6 +- applications/PoP/roomtest/pop_bg.c | 241 ++++++++++++++++++++------ applications/PoP/roomtest/pop_bg.h | 8 + applications/PoP/roomtest/pop_gdraw.c | 6 +- applications/PoP/roomtest/pop_kid.c | 20 +++ applications/PoP/roomtest/roomtest.c | 39 +++-- bin/sprinter-cc | 5 +- libbgi/common/gfx_blit_part_noclip.c | 38 ++++ libbgi/include/gfx.h | 8 + 9 files changed, 304 insertions(+), 67 deletions(-) create mode 100644 libbgi/common/gfx_blit_part_noclip.c diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index e548395..924860a 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -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 diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c index 594e686..85065dc 100644 --- a/applications/PoP/roomtest/pop_bg.c +++ b/applications/PoP/roomtest/pop_bg.c @@ -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; } diff --git a/applications/PoP/roomtest/pop_bg.h b/applications/PoP/roomtest/pop_bg.h index b96c5c3..4809af0 100644 --- a/applications/PoP/roomtest/pop_bg.h +++ b/applications/PoP/roomtest/pop_bg.h @@ -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(). */ diff --git a/applications/PoP/roomtest/pop_gdraw.c b/applications/PoP/roomtest/pop_gdraw.c index b7b295c..7853256 100644 --- a/applications/PoP/roomtest/pop_gdraw.c +++ b/applications/PoP/roomtest/pop_gdraw.c @@ -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 diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index c898c66..bbf6e70 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -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); + } } diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 0df7b07..6ddce4b 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -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 = обе страницы дабл-буфера), diff --git a/bin/sprinter-cc b/bin/sprinter-cc index c015294..f8ec431 100755 --- a/bin/sprinter-cc +++ b/bin/sprinter-cc @@ -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 diff --git a/libbgi/common/gfx_blit_part_noclip.c b/libbgi/common/gfx_blit_part_noclip.c new file mode 100644 index 0000000..24b3c7a --- /dev/null +++ b/libbgi/common/gfx_blit_part_noclip.c @@ -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(); +} diff --git a/libbgi/include/gfx.h b/libbgi/include/gfx.h index 6ca16c8..853f170 100644 --- a/libbgi/include/gfx.h +++ b/libbgi/include/gfx.h @@ -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-