libbgi: спрайты — кэш адреса кадра, DDA+asm тик, Y-сортировка со слоями
Оптимизации A+B (профиль rpgwalk-15: активная часть кадра 410К → 307К
тактов из 430080; лимит спрайтов 16×16 на стабильные 48.8 fps: 14 → ~21):
- (B) sprite_t.src/stride — готовый адрес кадра: считают только
sprite_frame (теперь функция, одно умножение на СМЕНУ кадра) и тикер
(±an_step БАЙТ инкрементально); блит-ядра принимают src+stride,
img/sx/sy из сигнатуры ушли. Блит 177К → 146К на кадр.
- (A) тик 100К → 37.7К: tween переформулирован Брезенхэм → беззнаковый
DDA (mv_rem/mv_acc, «приехали» = rem==0 — без знаковых 16-бит
сравнений), tick_move и tick_anim — ручной asm (SDCC спиллит такие
функции в IX-фрейм ~100 обращений; C-реструктуризации не помогали —
проверено кодогеном). Биты an_flags переименованы по категориям
(_SPR_STRIP_HORZ, _SPR_PP_BACK).
Y-сортировка (gfx_sprite_ysort, идеи пользователя — 8-бит ключ,
персистентность):
- painter's algorithm по ключу {layer:8, clamp_y:8}; поле
sprite_t.layer (в КОНЦЕ структуры — asm-офсеты не сдвигает): слои
сцены в одном массиве/одном sprite_update;
- ПЕРСИСТЕНТНАЯ asm-таблица {key16, ptr16}: resort порядка прошлого
кадра (почти линейно), rebuild при смене arr/count; массив
приложения не трогается; ~28К/15 спрайтов (с нуля было 44К);
- компоненты YSORT_Y/YSORT_LAYER отключаемы независимо масками ключа
(без ветвлений в сортировщике); ВНИМАНИЕ: mode=1 значит Y-only,
полный порядок = YSORT_Y|YSORT_LAYER;
- funcptr-DCE: выключено = код и таблица не линкуются (rpgwalk −190 Б);
- ПРАВИЛО в sprite.h: два sprite_update на страницу запрещены (heal
второй группы стирает спрайты первой — ОЗУ-копия чистая).
Попутные фиксы:
- libbgi/Makefile: .rel зависят от заголовков (HDRS) — stale .rel со
старой раскладкой sprite_t молча ломал рантайм;
- rpgprof: --memory small (перерос tiny: BSS вылезал за W2 → мгновенный
«Unexpected application termination»; mkexe это пока не ловит);
- tests/spranim: проверки переведены на кэш src, добавлены T6 (reframe
после тикера) и T7 (Y-сортировка: порядок, слои, персистентный
resort, LAYER-only) — 7/7 PASS в MAME.
Доки: §9д — новый бюджет (19.5К/спрайт), §9е — ПЛАН FPS-делителя
(frame pacing, gfx_set_fps_div); TODO — дизайн цепочки irq-обработчиков.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,27 @@
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||
EXAMPLE := rpgwalk
|
||||
EXTRA_FLAGS ?= --gfx 256 --memory tiny
|
||||
EXTRA_DATA := bard1.atl bard2.atl bard.pal
|
||||
# rpgprof.exe в EXTRA_DATA — `make floppy` кладёт на дискету ОБА exe.
|
||||
EXTRA_DATA := bard1.atl bard2.atl bard.pal rpgprof.exe
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
|
||||
# Профилировочная копия демо (бордер-полосы + маркеры для wpiset) —
|
||||
# собирается вместе с rpgwalk; инструкция в шапке rpgprof.c.
|
||||
# ВНИМАНИЕ: --memory small (не tiny!) — rpgprof толще rpgwalk
|
||||
# (my_update + профиль-обвязка): в tiny его BSS вылезал за W2
|
||||
# (0xC05B > 0xBFFF) → мгновенный «Unexpected application termination»
|
||||
# (кусало 2026-07-14; mkexe это переполнение пока НЕ ловит).
|
||||
all: rpgprof.exe
|
||||
floppy: rpgprof.exe
|
||||
|
||||
rpgprof.exe: rpgprof.c $(MKEXE) $(LIB) $(RUNTIME_DEPS) $(PROJ_ROOT)/libbgi/_bgi.h
|
||||
$(SPRINTER_CC) --gfx 256 --memory small -o $@ rpgprof.c
|
||||
|
||||
clean: clean-prof
|
||||
clean-prof:
|
||||
rm -f rpgprof.exe
|
||||
.PHONY: clean-prof
|
||||
|
||||
# 8 персонажей по 12 кадров = 24 КБ пикселей — НЕ лезут в одну
|
||||
# EMM-страницу (лимит 16К-0x100): два атласа по 4 персонажа.
|
||||
sprites.stamp: conv_sprites.py $(PROJ_ROOT)/toolchain/mkatlas.py
|
||||
|
||||
+81
-36
@@ -2,25 +2,27 @@
|
||||
* rpgprof — профилировочная копия rpgwalk (НЕ собирается по умолчанию).
|
||||
* Два инструмента в одном:
|
||||
*
|
||||
* 1. ВИЗУАЛЬНЫЙ профайлер — цвет ГРАНИЦЫ экрана (порт #FE, биты 0-2)
|
||||
* меняется на входе в каждую секцию кадра; луч бежит сверху вниз
|
||||
* (~320 строк растра за кадр 48.83 Гц, 1344 такта на строку), так
|
||||
* что толщина цветной полосы бордера ∝ времени секции:
|
||||
* красный (2) — логика: kbhit, машина маршрутов, getdatetime/FPS
|
||||
* жёлтый (6) — тик авто-анимаций (_spr_tick_fn)
|
||||
* magenta (3) — heal-проход (банк 0x50)
|
||||
* белый (7) — blit-проход (банк 0x5C)
|
||||
* синий (1) — gfx_wait_vsync (простой до конца кадра)
|
||||
* Много синего = запас; белый/жёлтый через весь экран = не влезаем.
|
||||
* ВНИМАНИЕ: dev-MAME перерисовывает бордер крупными кусками (не
|
||||
* per-scanline), полосы приблизительные и «пляшут» — для точных
|
||||
* цифр см. п.2.
|
||||
* 1. ВИЗУАЛЬНЫЙ профайлер — цвет ГРАНИЦЫ экрана меняется на входе в
|
||||
* каждую секцию кадра; луч бежит сверху вниз (~320 строк растра за
|
||||
* кадр 48.83 Гц, 1344 такта на строку), так что толщина цветной
|
||||
* полосы бордера ∝ времени секции.
|
||||
* ВНИМАНИЕ: бордер принимает только биты 1-2 порта #FE (бит 0 =
|
||||
* красный ИГНОРИРУЕТСЯ — проверено 2026-07-14) → доступны РОВНО
|
||||
* четыре цвета: 0 чёрный, 2 синий, 4 зелёный, 6 голубой:
|
||||
* синий (2) — логика (короткая) и blit-проход (банк 0x5C)
|
||||
* зелёный (4) — тик авто-анимаций (_spr_tick_fn)
|
||||
* голубой (6) — heal-проход (банк 0x50)
|
||||
* чёрный (0) — gfx_wait_vsync (простой до конца кадра)
|
||||
* Много чёрного = запас; синий+зелёный через весь бордер = не
|
||||
* влезаем. dev-MAME перерисовывает бордер крупными кусками (не
|
||||
* per-scanline) — полосы приблизительные, для точных цифр см. п.2.
|
||||
*
|
||||
* 2. ТАКТОВЫЙ профайлер — те же OUT'ы ловятся watchpoint'ом дебаггера
|
||||
* dev-MAME (рецепт: memory/mame_mcp_bridge и docs/sprite-api-design.md
|
||||
* §9д): wpiset по маске порта + printf totalcycles/wpdata + авто-g,
|
||||
* затем оп clog моста. Значения маркеров (2/6/3/7/1) уникальны —
|
||||
* по wpdata видно, какая секция началась.
|
||||
* затем оп clog моста. Для уникальности wpdata при одинаковом цвете
|
||||
* можно кодировать секцию в битах 3+ (бордер их не видит):
|
||||
* напр. логика 0x12, blit 0x02 — оба синие, но различимы в логе.
|
||||
*
|
||||
* Сборка и запуск (из этого каталога; атласы уже собраны make'ом):
|
||||
* ../../bin/sprinter-cc --memory tiny --gfx 256 -o rpgprof.exe rpgprof.c
|
||||
@@ -53,13 +55,14 @@ static void my_update(sprite_t *arr, uint8_t count)
|
||||
uint8_t dbit = p ? _SPR_DIRTY1 : _SPR_DIRTY0;
|
||||
uint8_t saved = gfx_get_bank();
|
||||
uint8_t i;
|
||||
const uint8_t *rec;
|
||||
sprite_t *s;
|
||||
|
||||
io_border = 6; /* жёлтый: тик анимаций */
|
||||
io_border = 4; /* зеленый: тик анимаций */
|
||||
if (_spr_tick_fn)
|
||||
(*_spr_tick_fn)(arr, count);
|
||||
|
||||
io_border = 3; /* magenta: heal */
|
||||
io_border = 6; /* голубой: heal */
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
_bgi_begin();
|
||||
for (i = 0, s = arr; i < count; i++, s++) {
|
||||
@@ -70,13 +73,23 @@ static void my_update(sprite_t *arr, uint8_t count)
|
||||
}
|
||||
_bgi_end();
|
||||
|
||||
io_border = 7; /* белый: blit */
|
||||
/* Y-сортировка (как в sprite_update; входит в «синюю» blit-секцию
|
||||
* профиля — её цену видно по маркерам). */
|
||||
io_border = 2; /* синий: blit (+ сортировка) */
|
||||
rec = 0;
|
||||
if (_spr_ysort_fn && count <= _GFX_YSORT_MAX)
|
||||
rec = (*_spr_ysort_fn)(arr, count) + 2;
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
_bgi_begin();
|
||||
{
|
||||
uint8_t mapped = 0;
|
||||
|
||||
for (i = 0, s = arr; i < count; i++, s++) {
|
||||
for (i = 0; i < count; i++) {
|
||||
if (rec) {
|
||||
s = *(sprite_t * const *)rec; /* Y-порядок */
|
||||
rec += 4;
|
||||
} else
|
||||
s = arr + i;
|
||||
if (s->flags & (dbit | SPR_ALWAYS)) {
|
||||
if (s->flags & SPR_VISIBLE) {
|
||||
if (s->page != mapped && s->page) {
|
||||
@@ -84,7 +97,7 @@ static void my_update(sprite_t *arr, uint8_t count)
|
||||
_io_page_w0 = s->page;
|
||||
mapped = s->page;
|
||||
}
|
||||
(*_gfx_blit_fn)(s->x, s->y, s->img, s->sx, s->sy, s->w, s->h);
|
||||
(*_gfx_blit_fn)(s->x, s->y, s->src, s->stride, s->w, s->h);
|
||||
s->drawn[p].x = s->x;
|
||||
s->drawn[p].y = s->y;
|
||||
s->drawn[p].on = 1;
|
||||
@@ -100,7 +113,7 @@ static void my_update(sprite_t *arr, uint8_t count)
|
||||
gfx_set_bank(saved);
|
||||
}
|
||||
|
||||
#define NCHR 8
|
||||
#define NCHR 15
|
||||
#define MINX 0
|
||||
#define MAXX (320 - 16)
|
||||
#define MINY 14
|
||||
@@ -116,8 +129,10 @@ static uint8_t phase[NCHR];
|
||||
static void set_dir(uint8_t i, uint8_t d)
|
||||
{
|
||||
dir[i] = d;
|
||||
// sprite_anim(&sp[i], (uint8_t)(d * 3), (uint8_t)(d * 3 + 2),
|
||||
// (uint8_t)(5 + (i & 3)), ANIM_PINGPONG);
|
||||
sprite_anim(&sp[i], (uint8_t)(d * 3), (uint8_t)(d * 3 + 2),
|
||||
(uint8_t)(5 + (i & 3)), ANIM_PINGPONG);
|
||||
(uint8_t)5, ANIM_PINGPONG);
|
||||
}
|
||||
|
||||
static void go_edge(uint8_t i)
|
||||
@@ -141,7 +156,7 @@ static void go_back_random(uint8_t i)
|
||||
int tx = s->x, ty = s->y, avail, dist;
|
||||
uint8_t d = dir[i];
|
||||
|
||||
d = (uint8_t)(d ^ ((d < 2) ? 3 : 1));
|
||||
d = (uint8_t)(3 - d); /* 0↔3, 1↔2 — разворот */
|
||||
set_dir(i, d);
|
||||
switch (d) {
|
||||
case 0: avail = MAXY - s->y; dist = QY; break;
|
||||
@@ -173,18 +188,38 @@ static void turn_90(uint8_t i)
|
||||
go_edge(i);
|
||||
}
|
||||
|
||||
// static void show_fps(uint8_t v)
|
||||
// {
|
||||
// char buf[4];
|
||||
|
||||
// buf[0] = (char)('0' + (v / 100) % 10);
|
||||
// buf[1] = (char)('0' + (v / 10) % 10);
|
||||
// buf[2] = (char)('0' + v % 10);
|
||||
// buf[3] = 0;
|
||||
// setcolor(WHITE);
|
||||
// setfillstyle(SOLID_FILL, BLACK);
|
||||
// bar(0, 0, 27, 9);
|
||||
// outtextxy(2, 1, buf);
|
||||
// }
|
||||
|
||||
static void show_fps(uint8_t v)
|
||||
{
|
||||
char buf[4];
|
||||
|
||||
buf[0] = (char)('0' + (v / 100) % 10);
|
||||
buf[1] = (char)('0' + (v / 10) % 10);
|
||||
buf[2] = (char)('0' + v % 10);
|
||||
buf[3] = 0;
|
||||
setcolor(WHITE);
|
||||
setfillstyle(SOLID_FILL, BLACK);
|
||||
bar(0, 0, 27, 9);
|
||||
outtextxy(2, 1, buf);
|
||||
uint8_t color = BLACK;
|
||||
setfillstyle(SOLID_FILL, color);
|
||||
bar(0, 2, 60, 5);
|
||||
if(v < 30) {
|
||||
color = RED;
|
||||
} else if(v < 40) {
|
||||
color = BLUE;
|
||||
} else if(v < 47) {
|
||||
color = MAGENTA;
|
||||
} else if(v < 52) {
|
||||
color = CYAN;
|
||||
} else {
|
||||
color = YELLOW;
|
||||
}
|
||||
setfillstyle(SOLID_FILL, color);
|
||||
bar(0, 2, v, 5);
|
||||
}
|
||||
|
||||
static void draw_field(void)
|
||||
@@ -203,6 +238,16 @@ static void draw_field(void)
|
||||
putpixel(x - 1, y, LIGHTGREEN);
|
||||
putpixel(x + 1, y, LIGHTGREEN);
|
||||
}
|
||||
setfillstyle(SOLID_FILL, BLACK);
|
||||
setcolor(BLACK);
|
||||
line(0, 0, 0, 7);
|
||||
line(10, 0, 10, 7);
|
||||
line(20, 0, 20, 7);
|
||||
line(30, 0, 30, 7);
|
||||
line(40, 0, 40, 7);
|
||||
line(50, 0, 50, 7);
|
||||
line(60, 0, 60, 7);
|
||||
|
||||
setcolor(WHITE);
|
||||
outtextxy(52, 3, "RPGPROF: border = frame profile, ESC");
|
||||
}
|
||||
@@ -251,7 +296,7 @@ int main(void)
|
||||
last_sec = dt.second;
|
||||
|
||||
for (;;) {
|
||||
io_border = 2; /* красный: логика */
|
||||
io_border = 2; /* синий: логика */
|
||||
if (kbhit() && getch() == 27)
|
||||
break;
|
||||
|
||||
@@ -277,8 +322,8 @@ int main(void)
|
||||
fps_draw--;
|
||||
show_fps(fps_val);
|
||||
}
|
||||
my_update(sp, NCHR); /* маркеры 6/3/7 внутри */
|
||||
io_border = 1; /* синий: ждём vsync */
|
||||
my_update(sp, NCHR); /* маркеры 2/4/6 внутри */
|
||||
io_border = 0; /* черный: ждём vsync */
|
||||
gfx_wait_vsync();
|
||||
gfx_set_visible_page(hidden);
|
||||
}
|
||||
|
||||
+18
-15
@@ -24,7 +24,7 @@
|
||||
#include <time.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define NCHR 8
|
||||
#define NCHR 15
|
||||
#define MINX 0
|
||||
#define MAXX (320 - 16)
|
||||
#define MINY 14 /* под заголовком */
|
||||
@@ -41,8 +41,10 @@ static uint8_t phase[NCHR]; /* 0 = до края, 1 = до случайно
|
||||
static void set_dir(uint8_t i, uint8_t d)
|
||||
{
|
||||
dir[i] = d;
|
||||
// sprite_anim(&sp[i], (uint8_t)(d * 3), (uint8_t)(d * 3 + 2),
|
||||
// (uint8_t)(5 + (i & 3)), ANIM_PINGPONG);
|
||||
sprite_anim(&sp[i], (uint8_t)(d * 3), (uint8_t)(d * 3 + 2),
|
||||
(uint8_t)(5 + (i & 3)), ANIM_PINGPONG);
|
||||
(uint8_t)20, ANIM_PINGPONG);
|
||||
}
|
||||
|
||||
/* Идти до края экрана вдоль dir[i] (скорость: 1px, интервал 1-2). */
|
||||
@@ -52,10 +54,10 @@ static void go_edge(uint8_t i)
|
||||
int tx = s->x, ty = s->y;
|
||||
|
||||
switch (dir[i]) {
|
||||
case 0: ty = MAXY; break;
|
||||
case 1: tx = MINX; break;
|
||||
case 2: tx = MAXX; break;
|
||||
default: ty = MINY; break;
|
||||
case 0: ty = MAXY; break;
|
||||
case 1: tx = MINX; break;
|
||||
case 2: tx = MAXX; break;
|
||||
default: ty = MINY; break;
|
||||
}
|
||||
sprite_moveto(s, tx, ty, 1, (uint8_t)(1 + (i & 1)));
|
||||
phase[i] = 0;
|
||||
@@ -68,23 +70,24 @@ static void go_back_random(uint8_t i)
|
||||
int tx = s->x, ty = s->y, avail, dist;
|
||||
uint8_t d = dir[i];
|
||||
|
||||
d = (uint8_t)(d ^ ((d < 2) ? 3 : 1)); /* 0↔3, 1↔2 — разворот */
|
||||
// d = (uint8_t)(d ^ ((d < 2) ? 3 : 1)); /* 0↔3, 1↔2 — разворот */
|
||||
d = 3 - d; /* 0↔3, 1↔2 — разворот */
|
||||
set_dir(i, d);
|
||||
switch (d) {
|
||||
case 0: avail = MAXY - s->y; dist = QY; break;
|
||||
case 1: avail = s->x - MINX; dist = QX; break;
|
||||
case 2: avail = MAXX - s->x; dist = QX; break;
|
||||
default: avail = s->y - MINY; dist = QY; break;
|
||||
case 0: avail = MAXY - s->y; dist = QY; break;
|
||||
case 1: avail = s->x - MINX; dist = QX; break;
|
||||
case 2: avail = MAXX - s->x; dist = QX; break;
|
||||
default: avail = s->y - MINY; dist = QY; break;
|
||||
}
|
||||
if (avail > dist)
|
||||
dist += rand() % (avail - dist + 1);
|
||||
else
|
||||
dist = avail; /* некуда — до края */
|
||||
switch (d) {
|
||||
case 0: ty += dist; break;
|
||||
case 1: tx -= dist; break;
|
||||
case 2: tx += dist; break;
|
||||
default: ty -= dist; break;
|
||||
case 0: ty += dist; break;
|
||||
case 1: tx -= dist; break;
|
||||
case 2: tx += dist; break;
|
||||
default: ty -= dist; break;
|
||||
}
|
||||
sprite_moveto(s, tx, ty, 1, (uint8_t)(1 + (i & 1)));
|
||||
phase[i] = 1;
|
||||
|
||||
Reference in New Issue
Block a user