Files
Sprinter-SDCC/applications/PoP/roomtest/pop_cdraw.h
T
snark13 b27b31318a Разбор draw_tile: модель цены блита + пакетная пометка pop_cd_touch
Замер (MAME, комната 23 ур.13) разложил цену одного gfx_blit_noclip
регрессией по 339 блитам, высоты 3..63:

    такты = 8791 + 198.2 * h + 5.96 * (w * h)

Модель ложится на весь диапазон (32x3 -> 9 891, 32x13 -> 13 887,
32x60 -> 32 073), разброс внутри размерной группы — ТРИ такта.

Выводы:
- 5.96 на байт — предел железа: байт идёт через акселератор дважды
  (burst src->память акселератора, burst ->экран), по 3 такта на проход
  при системном клоке 21 МГц.  Ускорять передачу нечем.
- 198 на строку — цикл _bgi_blit_rows_raw; при w=32 это половина
  построчной цены.
- 8791 на ВЫЗОВ — крупнейшая статья, НЕ объяснена.  Проверено, что это не
  W3-скобка (в обеих половинах по 5 инструкций) и не прерывания (разброс
  3 такта).  Один тайл = 5-6 спрайтов ~ 107 000 тактов, из них ~50 000 —
  постоянные накладные вызовов.  Это и есть главный резерв.

Заодно: pop_cd_touch собирается в ПАКЕТ на тайл (pop_cd_batch_begin/end,
скобка в draw_tile) вместо вызова на каждый кусок — раньше 2 866 тактов
на спрайт, 15% цены блита.  ВНИМАНИЕ: выигрыш замером НЕ подтверждён —
в захваченных кадрах скобка не сработала (блиты шли из холодной отрисовки
комнаты, мимо draw_tile).  Правка безопасна по построению: объединение
прямоугольников может пометку только расширить, не сузить.

Снятые сегодня неверные утверждения (чтобы не всплыли):
- «клипованный блит дороже полного» — артефакт сравнения разных выборок
  спрайтов; 32x60 с клипом до полосы стоит 7 236, то есть клип работает;
- «блиты идут программным циклом со скоростью ldir» — нет, ядро на
  акселераторе, см. модель выше;
- «отложить запекание на кадр» — НЕЛЬЗЯ: запекание пишет ОЗУ-копию фона,
  из которой heal восстанавливает; отложенное даёт призрак плиты.

Все 8 наборов tests-host проходят.  Оснастка замера временная.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 23:36:48 +03:00

156 lines
11 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* pop_cdraw.h — ОТРИСОВКА ПЕРСОНАЖА, одна на всех Char.
*
* В оригинале это два входа с ИДЕНТИЧНЫМ телом (seg008:22F0/2324):
*
* add_kid_to_objtable add_guard_to_objtable
* loadkid() loadshad()
* load_fram_det_col() load_fram_det_col()
* load_frame_to_obj() load_frame_to_obj()
* stuck_lower() stuck_lower()
* set_char_collision() set_char_collision()
* set_objtile_at_char() set_objtile_at_char()
* redraw_at_char() redraw_at_char()
* redraw_at_char2() redraw_at_char2()
* clip_char() clip_char()
* add_objtable(0) add_objtable(1 тень / 2 страж)
*
* — различаются ТОЛЬКО окном (loadkid/loadshad), набором спрайтов и типом
* объекта. У нас до DRAW-CHAR это были два независимых куска кода
* (kid_draw в резиденте и pop_guard_draw в банке), и расхождение уже стоило
* бага: падающий скелет рисовался поверх верхней грани пола соседней
* колонки, потому что порт redraw_at_char2 был только у Кида. Тем же путём
* пошли бы тень (ур. 4/5/6/12), визирь (13) и толстяк (12).
*
* Поэтому здесь ОДИН набор функций, а различия сведены к слоту:
* POP_CH_KID — Кид: окно loadkid, атласы chtab_2 (kidp), кадр kid_frame;
* POP_CH_OPP — соперник: окно loadshad, атласы chtab_5 (страж/скелет),
* кадр pop_gframe.
*
* Порядок в кадре (задаёт главный цикл, как draw_people):
* pop_char_heal(who) — стереть прошлый кадр ЭТОЙ страницы дабл-буфера;
* pop_char_draw(who) — спрайт + брызги урона + клинок;
* pop_char_fore(who) — передние грани и оверлеи тайлов ПОВЕРХ него.
*/
#ifndef POP_CDRAW_H
#define POP_CDRAW_H
#include <stdint.h>
#define POP_CH_KID 0
#define POP_CH_OPP 1
#define POP_CH_N 2
/* Состояние отрисовки одного слота. Публичное, потому что метрики кадра
* (fp*) читает физика из ДРУГОГО банка (char_width_half в
* set_char_collision, порядок «кто поверх кого» в pop_bg): данные лежат в
* _DATA (W2) и видны всем без трамплина, а вызов аксессора из банка стоил
* бы дороже самого чтения. */
typedef struct {
/* прямоугольник спрайта по СТРАНИЦАМ дабл-буфера: у каждой своя
* видео-ОЗУ и своя ОЗУ-копия, поэтому heal обязан стирать спрайт именно
* той страницы, в которую сейчас рисуем */
int x[2], y[2];
uint16_t w[2], h[2];
uint8_t valid[2];
/* НАКЛАДНЫЕ спрайты (клинок + брызги урона) — свой прямоугольник, не
* объединение с персонажем: объединение сильно больше суммы двух
* (клинок уходит вперёд-вверх), а heal стоит ровно по площади */
int ox[2], oy[2];
uint16_t ow[2], oh[2];
uint8_t ovalid[2];
/* габарит кадра для fore-прохода: fpx — ЛОГИЧЕСКАЯ X (до ×8/7),
* fpy — низ спрайта; fpw == 0 — в этом кадре рисовать было нечего */
int fpx, fpy;
uint16_t fpw, fph;
/* окно fore-клипа слота (объединение «спрайт + накладные»), КОМНАТНЫЙ y */
int cx, cy;
uint16_t cw, ch;
/* straddle: рендерное смещение по ЛОГИЧЕСКОЙ X, когда комната персонажа
* не совпадает с отрисованной (порт xpos_in_drawn_room) */
int render_dx;
} pop_cdraw_t;
extern pop_cdraw_t pop_cd[POP_CH_N];
/* Стереть прошлый кадр персонажа (спрайт + накладные) на ТЕКУЩЕЙ странице. */
void pop_char_heal(uint8_t who) __banked;
/* Нарисовать текущий кадр: спрайт, брызги урона (по флагу pop_kid_hurt /
* pop_guard_hurt) и клинок. Загружает окно Char сам — clip_char и футпринт
* работают с АКТИВНЫМ персонажем, как в оригинале. */
void pop_char_draw(uint8_t who) __banked;
/* Передние грани и оверлеи тайлов ПОВЕРХ персонажа: ставит окно fore-клипа
* слота и зовёт общий проход pop_fore_over_char. Отдельным вызовом, а не
* хвостом pop_char_draw, потому что порядок в кадре задаёт главный цикл:
* между спрайтом и его fore-слоем успевает нарисоваться падающая плита. */
void pop_char_fore(uint8_t who) __banked;
/* Straddle-смещение спрайта (см. render_dx). */
void pop_char_set_render_dx(uint8_t who, int dx) __banked;
/* ---- Пропуск НЕИЗМЕНИВШЕГОСЯ кадра (DRAW-COST) ---------------------- *
* heal + спрайт + fore-проход стоят ~50 % кадрового периода НА ПЕРСОНАЖА
* (замер пользователя 2026-08-08: комната 1.3 с телом убитого стража — 210 %
* кадра, соседняя 1.2 без тела — 140 %). Платили мы это и за то, что не
* менялось: труп лежит неподвижно до выхода из комнаты, стоящий Кид — пока
* игрок не нажал клавишу, а перерисовывались они каждый кадр.
*
* Оптимизация: у каждой страницы дабл-буфера свой снимок ВХОДОВ отрисовки
* слота (поза, координаты, направление, меч, брызги, straddle). Если снимок
* совпал, спрайт этой страницы не стёрт и фона под ним никто не трогал —
* нужный кадр на странице УЖЕ нарисован, и heal/draw/fore пропускаются
* целиком.
*
* `pop_cd_touch` — «фон трогали ВОТ ЗДЕСЬ»: слой фона копит ПРЯМОУГОЛЬНИК
* своих правок по страницам дабл-буфера, и слот перерисовывается, только
* если тот задел его самого. Флага «трогали хоть что-то» не хватило:
* факелы анимируются КАЖДЫЙ кадр, и один факел в комнате обнулял пропуск
* для всех персонажей сразу (замер 2026-08-08: комната 1.1, Кид стоит —
* 597 684 такта, то есть ровно как без оптимизации).
*
* Метку ставит сам блит фона (`pop_blit_b`, по признаку «идёт не
* fore-проход») плюс те немногие места, что стирают фон голым `bar` или
* своим heal (падающий кусок, оверлей fore-heal). Координаты — ЭКРАННЫЕ
* (с POP_YOFF). Прямоугольник копится сразу для ОБЕИХ страниц: правку фона
* почти всегда повторяют на второй, а лишняя перерисовка персонажа дешевле
* лишнего состояния. Снимает метку главный цикл — после того, как
* персонажи на этой странице отрисованы.
*
* Резидент (pop_tile.c): зовёт и банковый слой фона, и резидентные листья,
* а трамплин на КАЖДЫЙ кусок фона стоил бы дороже самой метки. */
extern uint8_t pop_cd_dirty; /* биты страниц: метка взведена */
extern uint16_t pop_cd_dmask[2][3]; /* [страница][ряд] = биты колонок 0..9 */
/* Задели ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 включительно) то,
* что трогали на странице p. Резидент (pop_tile.c). */
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1);
void pop_cd_clear(uint8_t p); /* снять метку страницы целиком */
void pop_cd_touch(int x, int y, int w, int h);
#define POP_CD_TOUCH_ALL() pop_cd_touch(0, 0, 320, 256)
/* Пакетная пометка: между begin и end блиты КОПЯТ общий прямоугольник, и
* настоящая пометка ставится одна (на весь тайл), а не на каждый кусок.
* Скобку ставит draw_tile — выигрыш ~15% цены блита фона. Вкладывать
* скобки нельзя: аккумулятор один. */
extern uint8_t pop_cd_batch;
void pop_cd_batch_begin(void);
void pop_cd_batch_end(void);
/* Кого в этом кадре можно НЕ перерисовывать: бит 0 — Кид, бит 1 — соперник.
* Звать ДВАЖДЫ: перед heal (стирать нечего) и перед отрисовкой (тик мог
* персонажа всё-таки сдвинуть — тогда бит снимется сам, а pop_char_draw
* сотрёт прошлый кадр за нас). */
uint8_t pop_char_skip_mask(void) __banked;
/* ОТРАЖЕНИЕ В ЗЕРКАЛЕ (уровень 4, seg003:0798). Char к моменту вызова уже
* отражён (pop_check_mirror); clip_top — линия y_clip[curr_row+1], выше
* которой отражение не рисуется. Отдельно от pop_char_draw, потому что и в
* оригинале это сокращённый путь add_objtable(4): без клинка, брызг,
* fore-прохода и пропуска кадра. heal — парный вызов, рядом с
* pop_char_heal. */
void pop_mirror_draw(int clip_top) __banked;
void pop_mirror_heal(void) __banked;
#endif