Тексты обеих Z80-процедур, разбор их устройства и качества, почему НЕ берём 8-битный RND Apple II (вырожденные младшие биты — раскладка кладки читает prandom(1), вышла бы шахматка), и главное — сколько это реально даст. Потолок выигрыша 2 814 тактов за кадр (0.65 %): тело генератора уже не основной расход, остаются обёртка pop_prandom, pop_rnd_fit и ABI вызова. Поэтому первый шаг, если упрёмся, — слить приведение к диапазону в ту же asm-процедуру (один call вместо трёх), и только потом менять генератор. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6.4 KiB
Генераторы псевдослучайных чисел: запасные варианты
Что сейчас стоит в порте, какие есть альтернативы и сколько на них реально можно выиграть. Заготовка на случай, если упрёмся в бюджет кадра — сейчас менять ничего не нужно.
Что стоит сейчас
pop_geom.c, ветка POP_PRANDOM_EXACT=1 (по умолчанию) — LCG оригинала
s = s*214013 + 2531011, шаг написан на Z80-ассемблере (единственное такое
место в порте). Схема Горнера по разреженной записи константы:
214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3
17 удвоений, три сложения, одно вычитание; величина 3*s, нужная в конце,
попадается по дороге на втором шаге. Тело — ≈1 020 тактов по статическому
подсчёту. Бит-в-бит совместим с SDLPoP, поэтому по картинке можно сверяться
с эталоном.
Вторая ветка, POP_PRANDOM_EXACT=0 — xorshift16 + шаг Вейля на C.
Совместимость теряется.
Замер в MAME, комната 3, 175 кадров (медиана кадра):
| вариант | кадр | prandom → torch_draw |
|---|---|---|
| C, бит-в-бит (16-битные половины) | 403 632 | 10 933 |
| C, xorshift16 + Вейль | 397 986 | 7 927 |
| asm, бит-в-бит (сейчас) | 400 800 | 9 331 |
Вариант A — комбинированный LFSR + LCG, ~148 тактов
Период > 4 млрд (lcm(65536, 65535) ≈ 4.29e9), младшие биты не вырождены.
prng16:
seed1=$+1
ld hl, 9999
ld b, h
ld c, l
add hl, hl
add hl, hl
inc l
add hl, bc
ld (seed1), hl
seed2=$+1
ld hl, 987
add hl, hl
sbc a, a
and 101101b
xor l
ld l, a
ld (seed2), hl
add hl, bc
ret
Устройство: seed1 — LCG x = 5x + 1 (по модулю 2^16; inc l вместо
inc hl — экономия байта, на период не влияет). seed2 — 16-битный
LFSR Галуа: сдвиг влево, и если выехала единица, XOR младшего байта с маской
0x2D (примитивный многочлен x^16 + x^5 + x^3 + x^2 + 1). На выходе
сумма обоих состояний — она и разрушает регулярность младших бит LCG.
Что мешает взять как есть: сиды зашиты в код (SMC), а нам нужны ДВЕ
независимые последовательности — раскладка кладки и анимация тайлов.
Пришлось бы передавать состояние через указатель, как сейчас у pop_prandom
(это +20…40 тактов, не принципиально).
Вариант B — xorshift(7,9,8), ~86 тактов
Самый быстрый, период 65535.
xrnd:
ld hl, 1 ; seed must not be 0
ld a, h
rra
ld a, l
rra
xor h
ld h, a
ld a, l
rra
ld a, h
rra
xor l
ld l, a
xor h
ld h, a
ld (xrnd+1), hl
ret
Две оговорки. Ноль — неподвижная точка, а сид раскладки кладки у нас
считается как номер комнаты + смещение ряда + колонка и вполне может
оказаться нулём: нужен либо guard, либо шаг Вейля поверх. И тот же SMC-сид,
что в варианте A.
Чего НЕ брать: RND из Apple II
Оригинальный Prince-of-Persia-Apple-II:
RNDseed := (5 * RNDseed + 23) mod 256
RND
lda RNDseed
asl
asl
clc
adc RNDseed
clc
adc #23
sta RNDseed
rts
Полный период 256 (a ≡ 1 mod 4, c нечётное), и для своего движка он
работал. Нам не годится: у LCG по модулю 256 младшие биты вырождены — бит 0
просто чередуется. Наши вызовы это увидят: раскладка кладки берёт
prandom(1) (ОДИН бит) и prandom(4), то есть вместо шума получилась бы
аккуратная шахматка.
Сколько реально можно выиграть
Меньше, чем кажется по числам 86/148 против 1 020. Тело генератора — уже не
весь расход: остаются обёртка pop_prandom, приведение к диапазону
pop_rnd_fit и ABI вызова. Верхняя граница выигрыша видна из замера выше:
между нынешним asm-LCG и самым дешёвым из проверенных вариантов разница
2 814 тактов за кадр (0.65 %) при двух вызовах за кадр, и это ПОТОЛОК —
любой из вариантов A/B ниже него не опустится.
Порядок действий, если понадобится:
- Сначала убрать обёртки: слить
pop_rnd_fitв ту же asm-процедуру, чтобы на вызов приходился одинcall, а не три. Это ничего не ломает и не трогает совместимость с эталоном. - И только если этого мало — менять генератор, начиная с варианта A (качество последовательности у него не хуже LCG, в отличие от B).
Важно помнить: число вызовов вырастет с боёвкой. Сейчас их два за кадр
(факелы), а guard_advance / guard_block / guard_strike дёргают
prandom(255) каждый по разу за кадр боя — то есть при драке станет 5–6, и
цена вопроса вырастет во столько же раз.