b0524b9ad0
Главный цикл спейсится тремя gfx_wait_vsync, поэтому работа сверх 430 000 тактов стоит сразу целый лишний растровый кадр. Было 470 964, стало ~425 600 — игра быстрее на треть (16,7 логических кадров/с против 12,5). - pop_y_to_row: цепочка сравнений вместо (y+60)/63%4-1. ВАЖНО: медленный хвост вынесен в ОТДЕЛЬНУЮ функцию — SDCC видит одинаковое выражение в двух ветках и поднимает деление в вершину, быстрые возвраты не спасают. - col_from_x (pop_bg) и get_tile_div_mod (pop_map) — общие резидентные таблицы POP_TILE_DIV/POP_TILE_MOD в pop_tile.c (const банка из чужого банка не читается). - pop_fore_over_char: расширение окна считается арифметикой, а не перебором 10 колонок и 3 рядов (условие монотонно -> границы). Формулы сверены с прежним перебором перебором значений, расхождений нет. - pop_cd_touch: цикл по страницам развёрнут, x+w/y+h считаются один раз. Зовётся с каждого блита фона, стоил 6 846 тактов. - process_trobs: tp/10 и tp%10 у факелов — таблицей. - Пустой слот соперника (стража на сцене нет, на странице ничего не нарисовано) считается «тихим»: ни heal, ни вход в pop_char_draw, ни fore-проход. Приём для поиска делений: брейкпоинт на __divsint/__divuint/__divuchar с печатью адреса возврата (printf "%04X", w@(sp)). Профиль остатка — TASKS_OPEN.md#draw-cost. tests-host: 5/5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
263 lines
11 KiB
C
263 lines
11 KiB
C
/*
|
||
* pop_geom.c — общая геометрия + PRNG оригинала. См. pop_geom.h.
|
||
*/
|
||
#include "pop_geom.h"
|
||
|
||
/* data.h: шаг 14 (ширина тайла в координатах персонажа), [5] = col 0. */
|
||
const int16_t pop_x_bump[20] = {
|
||
-12, 2, 16, 30, 44, 58, 72, 86, 100, 114,
|
||
128, 142, 156, 170, 184, 198, 212, 226, 240, 254
|
||
};
|
||
const int16_t pop_y_land[5] = {-8, 55, 118, 181, 244};
|
||
|
||
#define TILE_SIZEY_OFF 60 /* сдвиг «ряд 0 начинается выше нуля» */
|
||
|
||
/* Медленный хвост — ОТДЕЛЬНОЙ функцией. Иначе SDCC видит одно и то же
|
||
* выражение в двух ветках и поднимает деление в вершину функции: быстрые
|
||
* возвраты уже не спасают, __divsint зовётся всё равно (проверено —
|
||
* 5 вызовов за кадр остались после первой переписи). */
|
||
static int8_t y_row_div(int16_t t)
|
||
{
|
||
return (int8_t)(t / TILE_SIZEY % 4 - 1);
|
||
}
|
||
|
||
int8_t pop_y_to_row(int16_t y)
|
||
{
|
||
/* Было `(y + 60) / 63 % 4 - 1` — SDCC разворачивает это в __divsint плюс
|
||
* __modsint (а тот внутри снова зовёт __divsint): ~5 400 тактов на вызов
|
||
* по замеру в MAME, при том что полос всего четыре. Цепочка сравнений
|
||
* повторяет ИМЕННО прежнюю арифметику, включая усечение деления К НУЛЮ
|
||
* для отрицательных (t в −62..−1 даёт 0, а не −1). */
|
||
int16_t t = (int16_t)(y + TILE_SIZEY_OFF);
|
||
if (t >= 0) {
|
||
if (t < TILE_SIZEY) return -1; /* 0 % 4 - 1 */
|
||
if (t < 2*TILE_SIZEY) return 0;
|
||
if (t < 3*TILE_SIZEY) return 1;
|
||
if (t < 4*TILE_SIZEY) return 2;
|
||
if (t < 5*TILE_SIZEY) return -1; /* 4 % 4 = 0 */
|
||
} else if (t > -TILE_SIZEY) {
|
||
return -1; /* t/63 == 0 -> 0 % 4 - 1 */
|
||
}
|
||
return y_row_div(t);
|
||
}
|
||
|
||
/* Уложить значение в диапазон 0..maxv. Вызовы оригинала — prandom(1),
|
||
* prandom(255), prandom(0xFF): делитель степень двойки, то есть маска, а не
|
||
* деление. __moduint на Z80 стоит заметно дороже проверки n & (n-1). */
|
||
static uint16_t pop_rnd_fit(uint16_t v, uint16_t maxv)
|
||
{
|
||
uint16_t n = (uint16_t)(maxv + 1);
|
||
if ((uint16_t)(n & (uint16_t)(n - 1)) == 0)
|
||
return (uint16_t)(v & (uint16_t)(n - 1));
|
||
return (uint16_t)(v % n);
|
||
}
|
||
|
||
#if POP_PRANDOM_EXACT
|
||
/* ---- LCG оригинала: s = s*214013 + 2531011 (seg009:321) ------------ *
|
||
*
|
||
* ЕДИНСТВЕННОЕ место в порте на ассемблере, и вот почему. На Z80 нет
|
||
* умножения, а SDCC для 32-битного `s * 214013` зовёт __mullong — 8 430
|
||
* тактов на вызов (замер в MAME). Написанное на C через 16-битные
|
||
* половины это ~3 500. Здесь ~1 000, при полностью той же
|
||
* последовательности, что в SDLPoP. Альтернатива «взять генератор
|
||
* попроще» (8-битный RND Apple II, xorshift) экономит меньше и ломает
|
||
* совместимость с эталоном, по которому сверяем картинку.
|
||
*
|
||
* Приём: 214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3 — схема Горнера по
|
||
* РАЗРЕЖЕННОЙ записи константы. Вместо 12 сложений (по числу единиц в
|
||
* 0x343FD) — 17 удвоений, ТРИ сложения и одно вычитание. Величина 3*s,
|
||
* нужная в конце, попадается по дороге на втором шаге — её и сохраняем.
|
||
*
|
||
* ABI __sdcccall(1): arg1 (указатель на сид) -> HL, возврат -> DE. IX не
|
||
* используется, сохранять нечего. Клоббер AF/BC/DE/HL. Аккумулятор —
|
||
* DE:HL (DE старшее слово); s.lo живёт в BC, s.hi и 3*s — в статиках
|
||
* (регистров на всё не хватает).
|
||
*/
|
||
uint16_t pop_pr_ptr; /* адрес сида */
|
||
uint16_t pop_pr_sh; /* старшее слово исходного сида */
|
||
uint16_t pop_pr_3s_lo, pop_pr_3s_hi; /* 3*s, нужное в конце */
|
||
|
||
static uint16_t pop_lcg_step(pop_rnd_t *seed) __naked
|
||
{
|
||
(void)seed;
|
||
__asm
|
||
ld (_pop_pr_ptr), hl
|
||
ld c, (hl)
|
||
inc hl
|
||
ld b, (hl) ; BC = s.lo
|
||
inc hl
|
||
ld a, (hl)
|
||
ld (_pop_pr_sh), a
|
||
inc hl
|
||
ld a, (hl)
|
||
ld (_pop_pr_sh + 1), a ; (pop_pr_sh) = s.hi
|
||
|
||
ld h, b
|
||
ld l, c
|
||
ld de, (_pop_pr_sh) ; акк = s
|
||
|
||
;; акк = акк*2 + s => 3s
|
||
add hl, hl
|
||
rl e
|
||
rl d
|
||
add hl, bc
|
||
ex de, hl
|
||
ld a, (_pop_pr_sh) ; LD A,(nn) флаги НЕ трогает
|
||
adc a, l
|
||
ld l, a
|
||
ld a, (_pop_pr_sh + 1)
|
||
adc a, h
|
||
ld h, a
|
||
ex de, hl
|
||
ld (_pop_pr_3s_lo), hl
|
||
ld (_pop_pr_3s_hi), de
|
||
|
||
;; акк = акк*4 + s => 13s
|
||
add hl, hl
|
||
rl e
|
||
rl d
|
||
add hl, hl
|
||
rl e
|
||
rl d
|
||
add hl, bc
|
||
ex de, hl
|
||
ld a, (_pop_pr_sh)
|
||
adc a, l
|
||
ld l, a
|
||
ld a, (_pop_pr_sh + 1)
|
||
adc a, h
|
||
ld h, a
|
||
ex de, hl
|
||
|
||
;; акк = акк*16 + s => 209s
|
||
add hl, hl
|
||
rl e
|
||
rl d
|
||
add hl, hl
|
||
rl e
|
||
rl d
|
||
add hl, hl
|
||
rl e
|
||
rl d
|
||
add hl, hl
|
||
rl e
|
||
rl d
|
||
add hl, bc
|
||
ex de, hl
|
||
ld a, (_pop_pr_sh)
|
||
adc a, l
|
||
ld l, a
|
||
ld a, (_pop_pr_sh + 1)
|
||
adc a, h
|
||
ld h, a
|
||
ex de, hl
|
||
|
||
;; акк *= 1024 => 214016s (10 удвоений)
|
||
add hl, hl
|
||
rl e
|
||
rl d
|
||
add hl, hl
|
||
rl e
|
||
rl d
|
||
add hl, hl
|
||
rl e
|
||
rl d
|
||
add hl, hl
|
||
rl e
|
||
rl d
|
||
add hl, hl
|
||
rl e
|
||
rl d
|
||
add hl, hl
|
||
rl e
|
||
rl d
|
||
add hl, hl
|
||
rl e
|
||
rl d
|
||
add hl, hl
|
||
rl e
|
||
rl d
|
||
add hl, hl
|
||
rl e
|
||
rl d
|
||
add hl, hl
|
||
rl e
|
||
rl d
|
||
|
||
;; акк -= 3s => 214013s
|
||
ld bc, (_pop_pr_3s_lo)
|
||
or a ; CY = 0
|
||
sbc hl, bc
|
||
ld bc, (_pop_pr_3s_hi) ; LD BC,(nn) флаги НЕ трогает
|
||
ex de, hl
|
||
sbc hl, bc
|
||
ex de, hl
|
||
|
||
;; акк += 2531011 = 0x0026_9EC3
|
||
ld bc, #0x9EC3
|
||
add hl, bc
|
||
ld bc, #0x0026
|
||
ex de, hl
|
||
adc hl, bc
|
||
ex de, hl
|
||
|
||
;; сохранить сид, вернуть СТАРШЕЕ слово в DE
|
||
push de
|
||
ex de, hl ; DE = младшее слово
|
||
ld hl, (_pop_pr_ptr)
|
||
ld (hl), e
|
||
inc hl
|
||
ld (hl), d
|
||
inc hl
|
||
pop de ; DE = старшее слово
|
||
ld (hl), e
|
||
inc hl
|
||
ld (hl), d
|
||
ret
|
||
__endasm;
|
||
}
|
||
|
||
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv)
|
||
{
|
||
return pop_rnd_fit(pop_lcg_step(seed), maxv);
|
||
}
|
||
|
||
#else /* !POP_PRANDOM_EXACT — генератор попроще, НЕ совместимый с эталоном */
|
||
|
||
/* xorshift16 + шаг Вейля. Зачем не LCG оригинала: тот 32-битный, и даже
|
||
* без __mullong (см. ветку выше) стоит ~7 000 тактов на вызов — при двух
|
||
* вызовах за кадр это 3 % бюджета на генератор случайных чисел. Здесь
|
||
* умножений нет вовсе, только сдвиги и xor.
|
||
*
|
||
* Почему НЕ 8-битный RND Apple II (RNDseed := 5*RNDseed + 23 mod 256,
|
||
* Prince-of-Persia-Apple-II): он ещё быстрее, но у LCG по модулю 256
|
||
* младшие биты вырождены — бит 0 просто чередуется. Наши вызовы это
|
||
* видят: раскладка кладки берёт prandom(1) (один бит) и prandom(4), и
|
||
* вместо шума получилась бы правильная шахматка. У xorshift такой
|
||
* структуры нет, а стоит он примерно столько же.
|
||
*
|
||
* Шаг Вейля (+0x9E37, нечётный) нужен ещё и потому, что у чистого
|
||
* xorshift ноль — неподвижная точка, а сид кладки (номер комнаты + ряд +
|
||
* колонка) вполне может оказаться нулём.
|
||
*
|
||
* ПОСЛЕДСТВИЕ: последовательность отличается от SDLPoP — другая (но
|
||
* статистически такая же) раскладка кладки и другие броски в боёвке.
|
||
* Поэтому по умолчанию собирается НЕ эта ветка: после переписывания LCG
|
||
* на ассемблер выигрыш от смены генератора почти исчез, а сверять
|
||
* картинку с эталоном стало важнее. Ветка оставлена как запасной ход. */
|
||
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv)
|
||
{
|
||
uint16_t s = seed->lo;
|
||
s ^= (uint16_t)(s << 7);
|
||
s ^= (uint16_t)(s >> 9);
|
||
s ^= (uint16_t)(s << 8);
|
||
s = (uint16_t)(s + 0x9E37u);
|
||
seed->lo = s;
|
||
seed->hi = 0; /* старшая половина не используется */
|
||
/* Отдаём СТАРШИЙ байт: у него нет короткопериодической структуры
|
||
* младших бит, а вызывающие берут от результата именно младшие. */
|
||
return pop_rnd_fit((uint16_t)(s >> 8), maxv);
|
||
}
|
||
|
||
#endif
|