From a6e39070af70a552d118118ec41d2849bc174caf Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Wed, 19 Aug 2026 23:06:27 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=BD=D1=8B=D0=B9=20=D0=BB=D0=BE=D0=B3=D0=B8?= =?UTF-8?q?=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D0=B9=20=D0=BA=D0=B0=D0=B4=D1=80?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D0=BB=D1=83=D1=87=D1=83=20+=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B6=D0=B8=D0=BC=D1=8B=20FASTEST/FAST/NORMAL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Период стал max(n, ceil(W)) вместо ceil(W)+2: три gfx_wait_vsync после работы отсчитывались от её КОНЦА, поэтому бюджет кадра был один растр. Теперь ждём от якоря начала кадра, и при n=3 бюджет 1 290 240 тактов. Счёт кадров — программный, по биту 5 порта 0xFE (положение луча), а не по кадровым прерываниям: те теряются в DI-окнах акселератора фазозависимо (замер: 0..2,8 %, на полной перерисовке три подряд). Условие точности одно — зазор между выборками меньше 64 512 тактов; точки выборки расставлены по замеру, а не на глаз. Режимы (pop_pace.h), клавиша P по кругу, дефолт FASTEST. Условие боя взято у оригинала буквально (SDLPoP seg003.c:363): Kid.sword == SWORD_2_DRAWN, а не «идёт бой». Проверено в MAME на 11/15: счётчик без недосчёта на 270 кадров, период ровно 3 растра на 302 логических кадрах (ни длиннее 3,1, ни короче 2,9), NORMAL даёт ровно 4, с вынутым мечом — ровно 5. --- applications/PoP/roomtest/Makefile | 2 +- applications/PoP/roomtest/pop_bg.c | 17 +++++ applications/PoP/roomtest/pop_cdraw.c | 8 ++- applications/PoP/roomtest/pop_draw.c | 3 + applications/PoP/roomtest/pop_kdraw.c | 3 + applications/PoP/roomtest/pop_map.c | 10 +++ applications/PoP/roomtest/pop_pace.c | 100 ++++++++++++++++++++++++++ applications/PoP/roomtest/pop_pace.h | 99 +++++++++++++++++++++++++ applications/PoP/roomtest/pop_room.c | 8 +++ applications/PoP/roomtest/pop_tile.c | 5 ++ applications/PoP/roomtest/roomtest.c | 64 +++++++++++++++-- 11 files changed, 310 insertions(+), 9 deletions(-) create mode 100644 applications/PoP/roomtest/pop_pace.c create mode 100644 applications/PoP/roomtest/pop_pace.h diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index 3722c56..93af3ee 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -57,7 +57,7 @@ POS ?= 2 ifneq ($(strip $(ROOM)),) PROF_FLAGS += -DDBG_START_ROOM=$(ROOM) -DDBG_START_POS=$(POS) endif -EXTRA_SRCS := pop_vflip.c pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c +EXTRA_SRCS := pop_vflip.c pop_pace.c pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c BG_DIR := $(CURDIR)/../poc/res/bg KID_DIR := $(CURDIR)/../poc/res/kid diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c index 1d0e0af..d7b1bca 100644 --- a/applications/PoP/roomtest/pop_bg.c +++ b/applications/PoP/roomtest/pop_bg.c @@ -14,6 +14,7 @@ #include #include #include "pop_bg.h" +#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */ #include "_pop_bg.h" /* контракт с холодной половиной (pop_room.c, банк 7) */ #include "pop_tile.h" #include "pop_map.h" /* pop_upside — переворот (зелье инверсии) */ /* общие листья слоя фона (резидент): блит, тайлы, таблица */ @@ -57,6 +58,7 @@ static void ov_mark(int x, int y, int w, int h) void pop_fore_heal(void) __banked { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ uint8_t pg = gfx_get_draw_page() & 1; if (!(ov_pending & (1 << pg))) return; pop_heal_off(ov_x0, ov_y0, ov_x1 - ov_x0, ov_y1 - ov_y0); @@ -197,6 +199,7 @@ static const uint8_t WR_RULE[4][6] = { static void wall_rnd(int row, int col) { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ /* Кэшируем только штатную сетку: колонка вне 0..9 дала бы индекс * соседнего РЯДА и чужую раскладку (тихий баг вместо промаха). */ int idx = (col >= 0 && col < 10) ? (row + 1) * 10 + col : -1; @@ -282,6 +285,7 @@ static uint8_t wpp_d[5]; /* пять prandom(2), 0..2 */ static void wall_rnd_palace(int row, int col) { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ int idx = (col >= 0 && col < 10) ? (row + 1) * 10 + col : -1; pop_rnd_t saved; uint8_t i; @@ -366,6 +370,7 @@ static void wpp_fill(int x, int ytop, int w, int h, uint8_t colour) static void wall_pattern_palace(int row, int col, int which_part) { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ int xh = POP_COL_XH[col]; int dbys = 63 * row + 65; /* база БЕЗ POP_YOFF — для блитов */ int dmys = dbys - 3; @@ -411,6 +416,7 @@ static void wall_pattern_palace(int row, int col, int which_part) static void wall_pattern(int row, int col, int which_part) { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ int xh = POP_COL_XH[col]; int dby = 63 * row + 65; uint8_t parts; @@ -450,6 +456,7 @@ static void wall_pattern(int row, int col, int which_part) /* Решётка ворот (portcullis) из ЛЕВОЙ комнаты (seg008.c:1110). */ static void draw_gate_back(uint8_t modl, int xh, int dby, int dmy) { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ int x = xh * 8; int gate_top_y = dby - 62; int openness = (((modl > 188) ? 188 : modl) >> 2) + 1; @@ -480,6 +487,7 @@ static void draw_gate_back(uint8_t modl, int xh, int dby, int dmy) * Клип по прямоугольнику персонажа делает pop_t_fclip (см. pop_tile.h). */ static void draw_gate_fore(uint8_t modl, int xh, int dby, int dmy) { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ int x = xh * 8; int gate_top_y = dby - 62; int openness = (((modl > 188) ? 188 : modl) >> 2) + 1; @@ -576,6 +584,7 @@ static const uint8_t FORE_ANY[32] = { /* Контекст ft_* обязан быть проставлен вызывающим. */ static void fore_only_tile(int row, int col) { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ /* Нажатая кнопка → пол/stuck (get_tile_to_draw) — это уже учтено в ft_code * (его ставит pop_tile_code_drawn), иначе поверх Kid лезет передняя грань * КНОПКИ. */ @@ -622,6 +631,7 @@ static void fore_only_tile(int row, int col) * backtable — под персонажем. */ static void ceil_over_kid_tile(int col) { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ uint8_t code = pop_tile_code(-1, col); const piece *t = &pop_tile_table[code]; int x = POP_COL_XH[col] * 8; @@ -711,6 +721,7 @@ static void other_overlay_tile(int row, int col); /* fwd (draw_floor_overlay - static void climb_overlay_tile(int row, int col, uint8_t fidx) { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ /* тип — ПОДСТАВЛЕННЫЙ (нажатая кнопка = пол): иначе кромка, за которую * Kid спускается/подтягивается, закрашивается целым тайлом */ uint8_t code; @@ -747,6 +758,7 @@ static void climb_overlay_tile(int row, int col, uint8_t fidx) * draw_tile_base, draw_tile_anim (свои пики) и draw_tile_bottom. */ static void overlay_mid_tile(int row, int col) { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ uint8_t code = pop_tile_code_drawn(row, col); uint8_t lcode = pop_tile_code_drawn(row, col - 1); const piece *t; @@ -809,6 +821,7 @@ static void overlay_mid_tile(int row, int col) * сразу), поэтому порядок эмулируется явной проверкой. */ static void other_overlay_tile(int row, int col) { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ uint8_t code, left; if (!tile_in_fclip(row, col)) return; code = pop_tile_code(row, col); @@ -853,6 +866,7 @@ static void other_overlay_tile(int row, int col) void pop_mob_overlay_tile(int row, int col, int8_t oref_row, int8_t oref_col, int mx, int my, uint8_t mh) __banked { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ int8_t saved_row = pop_bg_obj_row, saved_col = pop_bg_obj_col; pop_bg_obj_row = oref_row; pop_bg_obj_col = oref_col; @@ -952,6 +966,7 @@ static void char_footprint(int obj_x, int obj_y, uint16_t w, uint16_t h, * ничем не режется. */ void pop_fore_tile_b(int row, int col) __banked { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ uint8_t saved = pop_t_fclip_on; pop_t_fclip_on = 0; fore_tile(row, col); @@ -974,6 +989,7 @@ void pop_fore_tile_b(int row, int col) __banked * странице её надо будет восстановить (pop_fore_heal). */ void pop_ceil_fore_tile_b(int col) __banked { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ int x = POP_COL_XH[col] * 8; /* ov_mark ЗДЕСЬ НЕ НУЖЕН, и это важно для бюджета. Передние куски потолка * рисуются от dby = 2, то есть занимают строки 0..2 поля, а пометку ставит @@ -999,6 +1015,7 @@ void pop_fore_pass_end(void) __banked { gfx_set_bank(GFX_BANK_NORMAL); } void pop_fore_over_char(const pop_char_t *ch, int obj_x, int obj_y, uint16_t w, uint16_t h) __banked { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ int8_t cL, cR, cLraw, rT, rB, rTraw, r, c; uint8_t frame = ch->frame, action = ch->action; pop_t_fclip_on = 1; /* окно = прямоугольник спрайта */ diff --git a/applications/PoP/roomtest/pop_cdraw.c b/applications/PoP/roomtest/pop_cdraw.c index d77d6ad..d24823b 100644 --- a/applications/PoP/roomtest/pop_cdraw.c +++ b/applications/PoP/roomtest/pop_cdraw.c @@ -23,6 +23,7 @@ #include "_pop_kdraw.h" #include "pop_vflip.h" /* зеркальные страницы атласов (зелье инверсии) */ /* меч + блит по id — сосед по банку 4 */ /* атласы/кадр Кида, pop_sword_draw, окна Char */ #include "pop_cdraw.h" +#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */ #include "pop_state.h" /* ВРЕМЕННО: зонды pop_dbg_b* */ #include "pop_bg.h" /* POP_YOFF, pop_fore_over_char, pop_fore_set_clip */ #include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */ @@ -507,6 +508,7 @@ static void cd_splash(pop_cdraw_t *s, uint8_t who, const atlas_t *pages, if (top + rows > 192) rows = 192 - top; if (rows <= 0) return; gfx_set_bank(GFX_BANK_SPRITE); + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ if (pop_onscreen_cols(qx, top + POP_YOFF, w, (uint8_t)rows)) gfx_blit_cols_part_noclip(qx, top + POP_YOFF, img, flip, (uint8_t)skip, (uint8_t)rows); @@ -713,6 +715,7 @@ void pop_char_draw(uint8_t who) __banked if (top < 0) { skip += -top; rows += top; top = 0; } if (top + rows > 192) rows = 192 - top; gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ if (rows > 0 && vis_w) { /* +YOFF: центрирование */ /* noclip-ядро ширину не ограничивает, поэтому при обрезке справа * идём общим путём: это редкие кадры (подъём в двери уровня раз @@ -865,6 +868,7 @@ void pop_mirror_draw(int clip_top) __banked } gfx_set_bank(GFX_BANK_SPRITE); + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ if (lskip) gfx_blit_cols_part_wx(bx, top + POP_YOFF, img, flip, skip, rows, lskip, (uint8_t)(vis_w - lskip)); @@ -965,9 +969,11 @@ void pop_hp_draw(void) __banked if (w && h) { /* Атлас стража тоже column-major — блит колоночный. */ n = (uint8_t)(gd > HP_MAXDRAW ? HP_MAXDRAW : gd); - for (i = 0; i < n; i++) + for (i = 0; i < n; i++) { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ gfx_blit_cols_part_noclip(314 - (int)i * HP_STEP, HP_Y + POP_YOFF, img, 0, 0, 0); + } } gfx_w0_unmap(); } diff --git a/applications/PoP/roomtest/pop_draw.c b/applications/PoP/roomtest/pop_draw.c index 0f34f79..6be995e 100644 --- a/applications/PoP/roomtest/pop_draw.c +++ b/applications/PoP/roomtest/pop_draw.c @@ -18,12 +18,15 @@ #include #include #include "_pop_draw.h" +#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */ void pop_heal_fast(int x, int y, int w, int h) { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча, см. pop_pace.h */ if (w <= 0 || h <= 0) return; if (pop_onscreen_cols(x, y, (uint16_t)w, (uint16_t)h)) gfx_heal_noclip(x, y, (uint8_t)w, (uint8_t)h); else gfx_heal(x, y, w, h); + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ /* и ПОСЛЕ: широкий heal сам по себе ~70 000 тактов */ } diff --git a/applications/PoP/roomtest/pop_kdraw.c b/applications/PoP/roomtest/pop_kdraw.c index d28a4ed..ac19876 100644 --- a/applications/PoP/roomtest/pop_kdraw.c +++ b/applications/PoP/roomtest/pop_kdraw.c @@ -17,6 +17,7 @@ #include #include "pop_kid.h" #include "_pop_kdraw.h" +#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */ #include "pop_guard.h" /* CHARID_* — клинок есть и у стража */ #include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */ #include "_pop_draw.h" /* pop_onscreen_cols */ @@ -103,6 +104,7 @@ uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr, if (top + rows > 192) rows = 192 - top; if (rows <= 0) return 0; gfx_set_bank(GFX_BANK_SPRITE); + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ /* Клинок почти всегда целиком на экране — noclip-путь (см. gfx.h: * подготовка клипающего варианта стоит ~5.6 К тактов на вызов). */ if (pop_onscreen_cols(bx, top + POP_YOFF, w, (uint16_t)rows)) @@ -121,6 +123,7 @@ uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr, * Кида, а держать вторую копию раскладки в pop_cdraw ни к чему. */ void pop_kid_img_blit(uint8_t image, int x, int y) { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ uint8_t pg = (uint8_t)(image >> 3); /* раскладка атласов: 8 спрайтов/стр. */ uint8_t idx = (uint8_t)(image & 7); const uint8_t *img; diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index 04f3cb9..b4959af 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -19,6 +19,7 @@ #include /* memcpy = LDIR: цикл на C тут стоил ~390 тактов на байт */ #include "pop_kid.h" #include "pop_map.h" +#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */ #include "pop_ctrl.h" /* pop_ctrl_shift_held() — для check_grab */ #include "pop_geom.h" /* общая геометрия (x_bump/y_land/y_to_row) */ #include "pop_tile.h" /* POP_TILE_DIV/MOD — деление на ширину тайла таблицей */ @@ -1994,14 +1995,17 @@ static void check_collisions(void) } pop_dbg_p2(); /* ЗАМЕР: вход в тело */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ set_char_collision(); pop_dbg_p3(); /* ЗАМЕР: set_char_collision */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ move_coll_to_prev(Char.curr_row); /* заодно снимет окно prev */ coll_prev_row = Char.curr_row; calc_coll_window(); coll_lo = win_lo; coll_hi = win_hi; coll_scan_prepare(); pop_dbg_p4(); /* ЗАМЕР: окно посчитано */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ /* Порядок важен: prev уже забран, теперь три ряда пересчитываются * НА ЭТОТ кадр (в оригинале ровно так же, seg004:0004). Ряд здесь * гарантированно 0..2, поэтому соседние ряды — это те же базы ±10, без @@ -2036,6 +2040,7 @@ static void check_collisions(void) coll_row(coll_above); } pop_dbg_p5(); /* ЗАМЕР: три ряда просканированы */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ /* Обход СВЕРХУ ВНИЗ, как в оригинале (9→0): побеждает МЛАДШАЯ колонка, * в которой флаг перешёл 0→1. Только по ПЕРЕСЕЧЕНИЮ окон: вне его * сравнивать нечего (у оригинала там prev_coll_room != curr_row_coll_room @@ -2685,6 +2690,7 @@ void pop_loose_tick(void) __banked * Инкремент+сравнение считают row/col без деления. */ uint8_t row = 0, col = 0; pop_dbg_m9(); /* ЗАМЕР: вход pop_loose_tick */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ /* Гейт холостого хода: если ни одна фаза не взведена, оба цикла (30 + 10 * позиций) пропускаем целиком — замер 11/15 дал 9 852 такта на комнату, * где loose-плит нет вовсе. Флаг ставят места записи фазы, снимаем его @@ -3164,11 +3170,14 @@ static void kid_phys(void) bump_into_opponent(); /* seg003: безоружный Кид отскакивает от стража */ check_collisions(); /* seg004: флаги перекрытия по колонкам ряда */ pop_dbg_p6(); /* ЗАМЕР: check_collisions целиком */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ check_bumped(); /* удержать у стены до check_action (порядок PoP) */ check_action(); pop_dbg_p7(); /* ЗАМЕР */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ check_press(); /* seg006: стойка/пробой loose → make_loose_fall */ pop_dbg_p8(); /* ЗАМЕР */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ check_spike_below(); /* seg006: над колонкой с пиками → выдвинуть пики */ check_spiked(); /* seg006: напоролся на вредные пики → смерть */ check_chomped_kid(); /* seg004: перемололо в сомкнутых челюстях → смерть */ @@ -3229,6 +3238,7 @@ void pop_phys_tick(void) __banked * кончился), а уход из комнаты и сотрясение отсечены внутри kid_phys. */ pop_loadkid_and_opp(); pop_dbg_p1(); /* ЗАМЕР: окно Char загружено */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ kid_phys(); pop_savekid_and_opp(); } diff --git a/applications/PoP/roomtest/pop_pace.c b/applications/PoP/roomtest/pop_pace.c new file mode 100644 index 0000000..3491d06 --- /dev/null +++ b/applications/PoP/roomtest/pop_pace.c @@ -0,0 +1,100 @@ +/* + * pop_pace.c — фиксированный логический кадр по лучу. Зачем именно так, + * а не по счётчику кадровых прерываний — в шапке pop_pace.h (там же + * условие точности и замеры). + * + * Модуль НЕ банковый (EXTRA_SRCS): pop_beam_sample зовётся из банков + * рисования, а базовый код в W1 доступен им прямым call — трамплин на + * каждую выборку съел бы весь смысл. + */ + +#include +#include +#include +#include "pop_pace.h" + +uint8_t pop_speed_mode; /* crt0 зануляет _DATA → FASTEST */ +volatile uint8_t pop_frame_tick; + +/* Бит 5 порта 0xFE на прошлой выборке: 0 или 0x20. На виду у asm-тела. */ +uint8_t pop_beam_prev; + +static uint8_t pace_anchor; /* тик на начало текущего логического кадра */ +static uint8_t pace_ok; /* 0 — луч не работает, откат на gfx_wait_vsync */ + +/* ---- выборка луча --------------------------------------------------- * + * in a,(0xFE) кладёт A на старший байт адреса (выбор ряда клавиатуры) — + * биты 5/7 от него не зависят, так же читает и gfx_wait_vsync. + * Быстрый путь (состояние не изменилось) — 40 T + вызов. */ +void pop_beam_sample(void) __preserves_regs(b, c, d, e) __naked +{ + __asm + in a, (#0xFE) + and a, #0x20 + ld hl, #_pop_beam_prev + cp a, (hl) + ret Z ; состояние то же — выходим сразу + ld (hl), a + or a, a + ret NZ ; 0→1: вошли в бланк, это не граница + ld hl, #_pop_frame_tick + inc (hl) ; 1→0: vpos = 0, начало кадра + ret + __endasm; +} + +uint8_t pop_pace_arm(void) +{ + uint16_t tries; + uint8_t t0; + + /* Взводит cbl_mode (_cbl_port_ref) и заодно ставит нас на фронт. */ + gfx_wait_vsync(); + + /* Затравка beam_prev без ложного фронта: что бы луч ни показал сейчас, + * первая выборка либо совпадёт (ранний выход), либо запишет 0x20 и + * уйдёт по ветке «вошли в бланк». */ + pop_beam_prev = 0; + pop_beam_sample(); + + t0 = pop_frame_tick; + for (tries = 0; tries < 20000; tries++) { + pop_beam_sample(); + if ((uint8_t)(pop_frame_tick - t0) >= 2) { + pace_anchor = pop_frame_tick; + pace_ok = 1; + return 1; + } + } + pace_ok = 0; /* бит не шевелится — прежнее поведение */ + return 0; +} + +void pop_wait_edge(void) +{ + uint8_t t; + if (!pace_ok) { gfx_wait_vsync(); return; } + t = pop_frame_tick; + do { + pop_beam_sample(); + kbd_raw_poll(); /* KBD-1: опрос обязан быть плотным */ + } while (pop_frame_tick == t); +} + +void pop_pace_end(uint8_t n) +{ + if (!pace_ok) { /* откат: как было — добрать ожиданиями */ + while (--n) gfx_wait_vsync(); + return; + } + while ((uint8_t)(pop_frame_tick - pace_anchor) < n) + pop_wait_edge(); + pace_anchor = pop_frame_tick; /* якорь ПО ФАКТУ — фаза не копится */ +} + +uint8_t pop_pace_n(uint8_t fight) +{ + if (pop_speed_mode == POP_SPEED_NORMAL) return fight ? 5 : 4; + if (pop_speed_mode == POP_SPEED_FAST) return fight ? 4 : 3; + return 3; /* FASTEST */ +} diff --git a/applications/PoP/roomtest/pop_pace.h b/applications/PoP/roomtest/pop_pace.h new file mode 100644 index 0000000..bc8850e --- /dev/null +++ b/applications/PoP/roomtest/pop_pace.h @@ -0,0 +1,99 @@ +/* + * pop_pace.h — фиксированный логический кадр (frame pacing) по ЛУЧУ. + * + * Задача: период логического кадра = max(n, ceil(W)) вместо нынешнего + * ceil(W) + 2, где W — работа в растровых кадрах. Сегодня главный цикл + * ждёт три gfx_wait_vsync ПОСЛЕ отрисовки, поэтому реальный бюджет кадра + * — один растр (430 080 тактов), и превышение его на такт стоит целого + * лишнего растра. При n=3 бюджет становится 1 290 240. + * + * ПОЧЕМУ НЕ СЧЁТЧИК КАДРОВЫХ ПРЕРЫВАНИЙ (замеры 2026-08-19, MAME, полный + * разбор в docs/frame_pacing_plan.md). Импульс запроса кадрового + * прерывания живёт 32 такта неразогнанного клока = 9,14 мкс, а ядра + * акселератора держат di на весь чанк блита (~0,29 мс). Импульс, + * попавший в такое окно, теряется насовсем — ядро z80 уровневое, защёлки + * нет (то же и на настоящем Spectrum). Замерено: в сцене 11/15 теряется + * от 0 до 2,8 % прерываний в зависимости от ФАЗЫ рендера относительно + * луча, а на полной перерисовке комнаты — три подряд. Сейчас фаза + * плавает (период то 3 растра, то 4) и потери размазаны; при жёстком + * пейсинге фаза застынет, и сцена может залипнуть в плохой — это ровные + * −25 % скорости, невидимые в профиле тактов. + * + * ЧТО ВМЕСТО. Бит 5 порта 0xFE — это ЧТЕНИЕ ПОЛОЖЕНИЯ ЛУЧА (1, когда + * vpos >= 272; MAME sprinter.cpp kbd_fe_r), а не событие: ни импульса, + * ни очереди, ни защёлки. Такой сигнал невозможно потерять — можно + * только не посмотреть. Считаем фронты 1→0 (это vpos = 0, ровно тот же + * момент, которого ждёт gfx_wait_vsync) программно, выборкой из точек, + * которые код и так проходит. + * + * УСЛОВИЕ ТОЧНОСТИ — ОДНО: между соседними выборками должно проходить + * меньше 64 512 тактов (длина окна «бит 5 = 1»: 48 строк из 320). Тогда + * в каждый бланк попадает хотя бы одна выборка, а в промежуток между + * бланками (365 568 тактов) — тем более; значит каждый фронт засчитан + * ровно один раз. Двойной счёт невозможен: инкремент только на переходе, + * и beam_prev тут же обновляется. Условие не про нагрузку, не про di, + * не про прерывания и не про звук — только про зазор. + * + * Запас: самый длинный неделимый кусок без выборки — одно di-окно, то + * есть один вызов _bgi_blit_rows_raw, а он по контракту режется на чанки + * ≤16 строк. Даже если считать грубо — одна выборка на целый блит, а + * самый дорогой замеренный блит 32 073 такта — зазор вдвое меньше окна. + * + * ВАЖНО: бит 5 читается осмысленно ТОЛЬКО при включённом cbl_mode (в + * MAME `data &= ~0xa0` стоит внутри `if (cbl_mode())`, иначе бит выставлен + * в 1 намертво). Его лениво взводит сам gfx_wait_vsync через + * _cbl_port_ref, поэтому pop_pace_arm обязан один раз позвать + * gfx_wait_vsync и ПРОВЕРИТЬ, что фронты реально идут. + */ +#ifndef POP_PACE_H +#define POP_PACE_H + +#include + +/* Режимы скорости. Растровый кадр Sprinter — 20,48 мс (48,83 Гц). + * Оригинал (SDLPoP/src/seg003.c:363): base_speed 5 тиков при 60 Гц = + * 83,3 мс, fight_speed 6 = 100 мс. Условие боя у оригинала одно и + * буквальное — «у Кида ВЫНУТ МЕЧ» (Kid.sword == sword_2_drawn), не + * «идёт бой» и не «рядом страж»; проверяется в верху главного цикла. */ +#define POP_SPEED_FASTEST 0 /* 3/3 — 61,4 мс; не успели — подтормаживаем */ +#define POP_SPEED_FAST 1 /* 3/4 — 61,4 / 81,9 мс; бой как в оригинале */ +#define POP_SPEED_NORMAL 2 /* 4/5 — 81,9 / 102,4 мс; повтор оригинала */ +#define POP_SPEED_MODES 3 + +extern uint8_t pop_speed_mode; /* POP_SPEED_*; дефолт — FASTEST */ + +/* Клавиша смены режима по кругу (PS/2 set 2). P — свободна: заняты + * K/I/S/L/U/[/] (читы, pop_cheat.h) и +/- (обход комнат). */ +#define KBD_SPEED_MODE 0x4D /* P */ + +/* Делитель для текущего режима. fight = «у Кида вынут меч». */ +uint8_t pop_pace_n(uint8_t fight); + +/* Счётчик кадров. volatile: его правит pop_beam_sample, а читают циклы + * ожидания — перечитывать обязаны каждый оборот. */ +extern volatile uint8_t pop_frame_tick; + +/* ВЫБОРКА ЛУЧА. Дёшево (~57 T с вызовом), не трогает bc/de/ix. + * Звать отовсюду, где иначе получился бы зазор длиннее 64 512 тактов: + * из обёрток рисования и с границ фаз. Модуль НЕ банковый, поэтому из + * банков зовётся прямым call, без трамплина. */ +void pop_beam_sample(void) __preserves_regs(b, c, d, e); + +/* Однократная подготовка: взвести cbl_mode, снять первое состояние бита + * и убедиться, что фронты идут. 0 — луч не работает (тогда пейсинг + * молча откатывается на прежнее поведение: n ожиданий gfx_wait_vsync). */ +uint8_t pop_pace_arm(void); + +/* Ждать ОДИН фронт луча. Внутри — тот же плотный опрос клавиатуры, что + * сейчас висит idle-хуком на gfx_wait_vsync (KBD-1: без опроса раз в + * ~0,5 мс теряются нажатия и залипают клавиши). */ +void pop_wait_edge(void); + +/* Добрать до n фронтов ОТ ЯКОРЯ и переставить якорь ПО ФАКТУ. + * Вызывающий обязан перед этим сделать хотя бы один pop_wait_edge() — + * на нём же и происходит своп страниц (tear-free). Якорь ставится + * по факту, а не anchor += n: догонять пропущенное время нельзя, иначе + * после тяжёлого кадра игра рванёт вперёд. */ +void pop_pace_end(uint8_t n); + +#endif diff --git a/applications/PoP/roomtest/pop_room.c b/applications/PoP/roomtest/pop_room.c index cb69342..0f2d10e 100644 --- a/applications/PoP/roomtest/pop_room.c +++ b/applications/PoP/roomtest/pop_room.c @@ -17,6 +17,7 @@ #include #include #include "pop_bg.h" +#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */ #include "_pop_bg.h" /* контракт с горячей половиной (pop_bg.c, банк 2) */ #include "pop_tile.h" /* общие листья слоя фона (резидент): блит, тайлы, таблица */ #include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */ @@ -625,6 +626,7 @@ void pop_loose_shake_draw(int row, int col) __banked * 42/71/72 = 26x15-16. Задача HEAL-WIDTH. */ pop_heal_off(x, 63 * row + 46, 58, 24); gfx_set_bank(GFX_BANK_SPRITE); + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ draw_tile(row, col); /* loose: лево+низ (анимир.) */ if (col + 1 < 10) draw_tile(row, col + 1); /* сосед: правая грань loose (анимир.) + его пол */ @@ -662,6 +664,7 @@ void pop_chomp_anim_draw(int row, int col) __banked uint8_t pose = pop_chomp_pose(cm); gfx_set_bank(GFX_BANK_SPRITE); + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ pop_cd_batch_begin(); /* одна пометка на все куски */ pop_env_b(CHOMP_FRAM_BOT[pose], x, dmy); if (cm & 0x80) /* кого-то перемололо */ @@ -691,6 +694,7 @@ void pop_spike_redraw(int row, int col) __banked * (63*row+34 .. +74); ширина 64 = обе ячейки. */ pop_heal_off(x, 63 * row + 34, 64, 40); gfx_set_bank(GFX_BANK_SPRITE); + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ draw_tile(row, col); if (col + 1 < 10) draw_tile(row, col + 1); @@ -714,6 +718,7 @@ void pop_chomp_redraw(int row, int col) __banked * Было 64 «на всю высоту тайла» — четыре лишние строки. */ pop_heal_off(x, 63 * row + 3, 32, 60); gfx_set_bank(GFX_BANK_SPRITE); + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ draw_tile(row, col); gfx_set_bank(GFX_BANK_NORMAL); } @@ -866,6 +871,7 @@ void pop_ceil_shake_draw(int col) __banked * heal'а, — он её и стирает. */ pop_cd_touch(x, POP_YOFF, 64, CEIL_BAND_H); gfx_set_bank(GFX_BANK_SPRITE); + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ pop_t_clip_top = POP_YOFF; pop_t_win_set(x, POP_YOFF, 64, CEIL_BAND_H); /* восстанавливаем ровно полосу */ draw_tile(-1, col); @@ -975,6 +981,7 @@ void pop_potion_draw(int row, int col, uint8_t modif) __banked } pop_heal_off(x - 1, yb - 10, 8, 12); gfx_set_bank(GFX_BANK_SPRITE); + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ pop_pot_b(POT_MASK_ID, x, yb); /* маска (под пузырьком) */ pop_pot_b(bubb, x, yb); /* кадр пузырька */ gfx_set_bank(GFX_BANK_NORMAL); @@ -1788,6 +1795,7 @@ static void mob_render(mob_t *m, uint8_t pg) * left(41) на obj_y-3, bottom(43) на obj_y, right(42) на obj_x+4 (=col+1, * заходит в тайл СПРАВА) на obj_y-1. */ gfx_set_bank(GFX_BANK_SPRITE); + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ /* КЛИПА ОБЪЕКТА ЗДЕСЬ НЕТ. У оригинала он есть — add_mob_to_objtable * (seg007:1161) ставит куску clip.right = 40, — но единицы этого поля я * НЕ выяснил: буквальные 40 экранных пикселей срезают правый задний угол diff --git a/applications/PoP/roomtest/pop_tile.c b/applications/PoP/roomtest/pop_tile.c index 3985050..f1e745f 100644 --- a/applications/PoP/roomtest/pop_tile.c +++ b/applications/PoP/roomtest/pop_tile.c @@ -14,6 +14,7 @@ #include "pop_map.h" /* pop_upside — переворот (зелье инверсии) */ #include "pop_cdraw.h" /* POP_CD_TOUCH — фон тронут, персонажей перерисовать */ #include "pop_tile.h" +#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */ #include "pop_level.h" /* pop_doorlink2 — подстановка нажатой кнопки */ #include "_pop_draw.h" /* pop_heal_fast */ @@ -565,6 +566,7 @@ static const uint8_t *pb_img; void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) { + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча, см. pop_pace.h */ /* Аргументы сразу в file-scope: третий и дальше SDCC передаёт СТЕКОМ, и * дальше каждое их чтение — `-N(ix)` (замер 2026-08-17: 76 таких * обращений в теле, ~3 500 такта из 14 500 цены блита). То же лечение, @@ -597,6 +599,7 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) * общий путь выше. */ if (pb_img[1] | pb_img[3]) { /* кадр больше 255 — редкий путь */ blit_b_oversize(pb_img, pb_x, pb_yb); + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ gfx_w0_unmap(); return; } @@ -618,6 +621,7 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) * целиком внутри поля: иначе он залезет в борт (см. blit_b_clip). */ if (pop_upside && (pb_top < POP_YOFF || pb_top + (int)pb_h > POP_CLIP_BOTTOM)) { blit_b_clip(pb_img, pb_x, pb_top, pb_w, pb_h); + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ gfx_w0_unmap(); return; } @@ -645,6 +649,7 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) blit_b_clip(pb_img, pb_x, pb_top, pb_w, pb_h); } } + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ gfx_w0_unmap(); } diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index c6812fc..b506480 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -23,6 +23,7 @@ #include "pop_cheat.h" #include "pop_tune.h" #include "pop_vflip.h" /* зеркальный набор персонажей (зелье инверсии) */ +#include "pop_pace.h" /* фиксированный логический кадр по лучу */ #include "roomtest_cold.h" /* холодная половина цикла — в банке */ __sfr __at (0xFE) io_border; @@ -225,6 +226,7 @@ int main(void) * по умолчанию (без мерцания); SPACE переключает в однобуфер для * отладки багов рисования (edge по sp_prev, чтоб не дёргать). */ uint8_t front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0; + uint8_t pace_prev = 0; /* edge клавиши смены режима скорости */ uint8_t skip = 0; /* маска «слот перерисовывать не надо» */ uint16_t dead_frames = 0; #ifdef ROOMNAV @@ -243,6 +245,9 @@ int main(void) return 1; } gfx_set_idle_hook(kbd_idle); /* вычерпывать FIFO, пока ждём кадр */ + /* Темп кадра по лучу. Если бит 5 порта 0xFE не шевелится (иное + * железо), pop_pace_* молча откатываются на прежние gfx_wait_vsync. */ + pop_pace_arm(); while (!kbd_raw_down(KBD_ESC)) { /* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один @@ -260,6 +265,18 @@ int main(void) } sp_prev = sp; + /* P — переключить режим скорости по кругу (FASTEST -> FAST -> + * NORMAL). Edge, чтоб одно нажатие = один шаг. Дефолт FASTEST + * (crt0 зануляет _DATA). Что означают режимы — pop_pace.h. */ + { + uint8_t pk = kbd_raw_down(KBD_SPEED_MODE); + if (pk && !pace_prev) { + uint8_t m = (uint8_t)(pop_speed_mode + 1); + pop_speed_mode = (uint8_t)(m >= POP_SPEED_MODES ? 0 : m); + } + pace_prev = pk; + } + /* Отладочный стоп-кадр: '1' (PS/2 0x16) — заморозить, '2' (0x1E) — * продолжить. В заморозке пропускаем весь tick/draw/flip: картинка * стоит, можно спокойно разбирать позу Kid и окклюзию. */ @@ -317,7 +334,10 @@ int main(void) } if (frozen) { PROF(0); /* чёрный: ждём vsync */ - gfx_wait_vsync(); + /* Заморозка — тоже полноценный логический кадр: иначе якорь + * пейсинга уедет и первый кадр после разморозки будет рваным. */ + pop_wait_edge(); + pop_pace_end(pop_pace_n((uint8_t)(Kid.sword == SWORD_2_DRAWN))); continue; } @@ -362,14 +382,18 @@ int main(void) gfx_set_draw_page(back); } pop_dbg_m13(); /* ЗАМЕР: ввод и читы разобраны */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */ pop_check_mouse(); /* спецсобытие ур.8: мышь жмёт кнопку */ pop_check_killed_shadow(); /* спецсобытие ур.12: смерть тени = смерть Кида */ pop_frame_timers(); /* timers (seg003:0735): вспышка слияния */ pop_dbg_m9(); /* ЗАМЕР: pop_frame_timers сделан */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */ pop_dbg_m14(); /* ЗАМЕР: спецсобытия + луч видимости */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ pop_ctrl_tick(); /* ввод -> control(): смена seq */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ /* Кто в этом кадре не изменился с прошлой отрисовки ЭТОЙ страницы — * тот на ней уже нарисован правильно: ни стирать, ни рисовать заново * не надо (DRAW-COST, контракт в pop_cdraw.h). Решение @@ -381,18 +405,25 @@ int main(void) * skip-маске он не участвует (рисуется/стирается безусловно, живёт * только пока Кид стоит на тайле зеркала). */ pop_mirror_heal(); + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ PROF(2); /* логика (тот же синий) */ pop_dbg_m1(); /* ЗАМЕР: начало логики */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ kid_tick(); /* play_seq: следующий кадр */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ pop_phys_tick(); /* K3: падение/приземление/стена */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ pop_guard_phys_tick(); /* и его физика — та же, что у Кида (play_guard_frame) */ /* Боёвка, порядок как в play_frame (seg000:872): сначала пометить * попадания с обеих сторон, потом применить, потом свести HP. */ pop_check_sword_hurting(); pop_check_sword_hurt(); pop_do_delta_hp(); + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ pop_guard_fallout(); /* страж провалился ниже комнаты — убрать (seg002:241) */ if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */ uint8_t down = pop_room_link(cur_room, LINK_DOWN); @@ -460,10 +491,12 @@ int main(void) PROF(4); /* слой фона */ pop_dbg_m2(); /* ЗАМЕР: конец логики / начало фона */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */ /* — ДО Kid, чтобы Kid всегда был поверх плиты) */ pop_dbg_m5(); /* ЗАМЕР: pop_loose_tick сделан */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */ /* process_trobs — рисования внутри нет) */ if (pop_neigh_dirty) { @@ -491,8 +524,10 @@ int main(void) } } pop_dbg_m6(); /* ЗАМЕР: pop_process_trobs сделан */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ pop_redraw_needed(); pop_dbg_m7(); /* ЗАМЕР: pop_redraw_needed сделан */ /* и разобрать пометки перерисовки тайлов (порт */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ /* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */ { /* ворота соседа слева (room8) видны в col0 — */ uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */ @@ -630,6 +665,7 @@ int main(void) update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */ PROF(6); /* спрайты: страж, Kid, fore поверх */ pop_dbg_m3(); /* ЗАМЕР: конец фона / начало спрайтов */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ /* check_mirror (seg003:0798) — ПЕРВАЯ строка draw_people * (seg008:228A), до отрисовки кого бы то ни было: если Кид только что * пролетел сквозь зеркало, тень должна родиться ДО того, как слот @@ -640,8 +676,10 @@ int main(void) * спрайта, иначе перепечатка ложится поверх уже нарисованного куска — * так окно задней стены и передняя грань пола оказывались на плите. */ pop_dbg_m10(); /* ЗАМЕР: check_mirror сделан */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ pop_loose_mob_draw(); pop_dbg_m11(); /* ЗАМЕР: pop_loose_mob_draw сделан */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ { /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль * персонажа (см. guard_over_kid). Отрисовка одна на всех Char * (pop_cdraw.c), различает их только слот. */ @@ -651,6 +689,7 @@ int main(void) uint8_t g_after; skip = pop_char_skip_mask(); pop_dbg_m12(); /* ЗАМЕР: skip_mask сделан */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ /* Порядок «кто поверх кого» нужен, только если кого-то РИСУЕМ. * При skip == 3 оба слота тихие, рисовать некого — а вызов стоит * трамплина в банк 8 и двух objtile_at_char, 14 424 такта @@ -682,7 +721,9 @@ int main(void) * оригинала (draw_tables, seg008:1373). Пометки ставят сами * объекты вокруг себя (pop_redraw.h). */ pop_fore_needed(); + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ pop_hp_draw(); /* полоса HP — только при изменении */ + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ PROF(6); /* fore поверх Kid + борта */ /* Fore-слой Кида — В КОНЦЕ, после плиты и полосы HP: окно fore-клипа * одно на всех, и его ставит каждый, кто рисовал персонажа, поэтому @@ -695,11 +736,18 @@ int main(void) * взводит бит и обязан быть учтён. Всё, что рисуется ниже * (mob поверх Кида, борта), взводит его заново само. */ pop_cd_clear(back); + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ pop_room_clip_borders(); /* почистить борта — только в кадрах падения (гейт) */ - /* Пейсинг 3 кадра, затем tear-free флип на vblank: draw уже - * сделан (скрытая страница), wait_vsync ловит начало кадра, - * set_visible_page переключает во время бланка (gfx.h паттерн). */ + /* ФИКСИРОВАННЫЙ ЛОГИЧЕСКИЙ КАДР (pop_pace.h): период = max(n, + * ceil(W)) вместо прежнего ceil(W) + 2. Раньше здесь стояли три + * gfx_wait_vsync ПОДРЯД — они отсчитывались от КОНЦА работы, из-за + * чего бюджет кадра был один растр, а превышение его на такт стоило + * целого лишнего. Теперь ждём от ЯКОРЯ начала кадра. + * + * Первый фронт — всегда: на нём и происходит tear-free своп (до + * начала картинки остаётся верхний бордер, 16 строк). Строб вспышки + * снимается ровно между первым фронтом и остальными, как и было. */ PROF(0); /* чёрный: ждём vsync */ @@ -711,13 +759,15 @@ int main(void) #endif pop_dbg_m4(); /* ЗАМЕР: конец спрайтов */ - gfx_wait_vsync(); + pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ + pop_wait_edge(); if (flash_on) { /* строб: цвет держался один кадр растра */ flash_bg(0, 0, 0); flash_on = 0; } - gfx_wait_vsync(); - gfx_wait_vsync(); + /* Условие боя — буквально как у оригинала (SDLPoP seg003.c:363): + * не «идёт бой» и не «рядом страж», а «у Кида ВЫНУТ МЕЧ». */ + pop_pace_end(pop_pace_n((uint8_t)(Kid.sword == SWORD_2_DRAWN))); if (dbuf) { gfx_set_visible_page(back); front = back;