99b430f2ed
Ревью на 32-бит сделан ПО ASM, а не по коду (искали и безымянные
временные): во всём приложении был ровно ОДИН 32-битный вызов —
__mullong в pop_prandom. Замер в MAME: 8 430 тактов на вызов, два
вызова за кадр. Прочие библиотечные вызовы 16-битные (__divsint 16,
__modsint 12, __moduchar 8, __divuchar 5).
1. pop_prandom. Состояние 32-бит -> две 16-битные половины. Два
генератора, выбор через POP_PRANDOM_EXACT:
- 0 (по умолчанию) — xorshift16 + шаг Вейля, без единого умножения;
- 1 — LCG оригинала бит-в-бит, посчитанный половинами (для сверки
картинки с эталоном).
8-битный RND Apple II (5*x+23 mod 256) НЕ взят: у LCG по модулю 256
вырождены младшие биты (бит 0 просто чередуется), а раскладка кладки
берёт как раз prandom(1) и prandom(4) — вместо шума вышла бы
правильная шахматка. Шаг Вейля ещё и убирает ноль как неподвижную
точку xorshift (сид кладки вполне может быть нулём).
Бит-в-бит эквивалентность half-word версии проверена на хосте:
70 000 сидов x 8 шагов + 7 крайних сидов x 2000 шагов.
Остаток 0..maxv: делитель степень двойки — маска вместо __moduint.
2. libbgi: gfx_blit_cols_part_noclip — column-major блит без клипа
(пара к gfx_blit_cols_part, как gfx_blit_part_noclip к
gfx_blit_part). Клипающий вариант платит ~5 622 такта подготовки на
КАЖДЫЙ вызов независимо от того, вылезает край (замер: подготовка
5 622 против 13 596 на сам accel-проход). Kid, страж и клинок
выбирают путь по pop_onscreen_cols. size-check: роста нет.
Бюджет (175 кадров, комната 3, медиана):
было (после клинка) 416 154 0.968 кадра
стало 397 986 0.926 кадра
Разница между генераторами, замер на одинаковой сборке:
xorshift16 + Вейль 397 986 prandom->torch_draw 7 927
бит-в-бит LCG 403 632 prandom->torch_draw 10 933
то есть точность обходится в 5 646 тактов за кадр (1.3 %).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
110 lines
5.8 KiB
C
110 lines
5.8 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};
|
|
|
|
int8_t pop_y_to_row(int16_t y)
|
|
{
|
|
return (int8_t)((y + 60) / TILE_SIZEY % 4 - 1);
|
|
}
|
|
|
|
/* Уложить значение в диапазон 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
|
|
/* s = s * 214013 + 2531011, 16-битными половинами.
|
|
*
|
|
* 214013 = 0x0003_43FD. Раскладываем произведение так, чтобы обошлось без
|
|
* __mullong: старшее слово результата не зависит от старших бит множителя
|
|
* выше 32-го, поэтому
|
|
* s*M = SL*ML (полные 32 бита)
|
|
* + ((SL*MH + SH*ML) mod 2^16) << 16 (только младшие 16 бит)
|
|
* где ML = 0x43FD, MH = 3. Единственное «широкое» умножение — SL*ML,
|
|
* 16x16 -> 32; собираем его из четырёх байтовых произведений (каждое
|
|
* помещается в 16 бит, поэтому это обычный 16-битный __mulint).
|
|
*
|
|
* Проверка эквивалентности — на хосте: те же значения, что давал
|
|
* unsigned long (см. комментарий к pop_rnd_t в pop_geom.h). */
|
|
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv)
|
|
{
|
|
uint16_t sl = seed->lo, sh = seed->hi;
|
|
uint8_t al = (uint8_t)sl, ah = (uint8_t)(sl >> 8);
|
|
uint16_t t0 = (uint16_t)((uint16_t)al * 0xFDu); /* al * ML_lo */
|
|
uint16_t t1 = (uint16_t)((uint16_t)al * 0x43u); /* al * ML_hi */
|
|
uint16_t t2 = (uint16_t)((uint16_t)ah * 0xFDu); /* ah * ML_lo */
|
|
uint16_t t3 = (uint16_t)((uint16_t)ah * 0x43u); /* ah * ML_hi */
|
|
uint16_t m = (uint16_t)(t1 + t2); /* средние байты */
|
|
uint16_t pl, ph;
|
|
|
|
ph = (uint16_t)(t3 + (m >> 8));
|
|
if (m < t1) ph = (uint16_t)(ph + 0x100u); /* перенос из t1+t2 */
|
|
pl = (uint16_t)(t0 + (uint16_t)(m << 8));
|
|
if (pl < t0) ph++; /* перенос в старшее слово */
|
|
|
|
/* (SL*MH + SH*ML) << 16 — только младшие 16 бит слагаемого. */
|
|
ph = (uint16_t)(ph + (uint16_t)(sl * 3u) + (uint16_t)(sh * 0x43FDu));
|
|
|
|
/* + 2531011 = 0x0026_9EC3 */
|
|
{
|
|
uint16_t nl = (uint16_t)(pl + 0x9EC3u);
|
|
ph = (uint16_t)(ph + 0x0026u);
|
|
if (nl < pl) ph++;
|
|
seed->lo = nl;
|
|
seed->hi = ph;
|
|
}
|
|
return pop_rnd_fit(ph, maxv);
|
|
}
|
|
|
|
#else /* !POP_PRANDOM_EXACT — быстрый 16-битный генератор (по умолчанию) */
|
|
|
|
/* 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. Значит другая
|
|
* (но статистически такая же) раскладка кладки и другие броски в боёвке.
|
|
* Если понадобится сверять картинку с оригиналом — собрать с
|
|
* -DPOP_PRANDOM_EXACT=1, там бит-в-бит тот же 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
|