diff --git a/applications/PoP/docs/ideas_backlog.md b/applications/PoP/docs/ideas_backlog.md index 359924a..133ab83 100644 --- a/applications/PoP/docs/ideas_backlog.md +++ b/applications/PoP/docs/ideas_backlog.md @@ -4,6 +4,15 @@ гипотеза с причиной, по которой её стоит проверить, и с тем, что мешает сделать это прямо сейчас. +## Заменить генератор псевдослучайных чисел + +Сейчас стоит LCG оригинала, шаг на ассемблере (~1 020 тактов), бит-в-бит +совместимый с SDLPoP. Есть более дешёвые Z80-генераторы (86–148 тактов), +но потолок выигрыша — 2 814 тактов за кадр, 0.65 %, и он растворяется в +обёртках вызова. Тексты процедур, разбор качества и порядок действий — +`prng_alternatives.md`. Первый шаг там не про генератор: слить приведение +к диапазону в ту же asm-процедуру, чтобы на вызов был один `call`, а не три. + ## Отключать мышь на время игры **Гипотеза.** Мышь на Sprinter — источник прерываний (обёртки RST 30h, diff --git a/applications/PoP/docs/prng_alternatives.md b/applications/PoP/docs/prng_alternatives.md new file mode 100644 index 0000000..b5ed432 --- /dev/null +++ b/applications/PoP/docs/prng_alternatives.md @@ -0,0 +1,148 @@ +# Генераторы псевдослучайных чисел: запасные варианты + +Что сейчас стоит в порте, какие есть альтернативы и сколько на них реально +можно выиграть. Заготовка на случай, если упрёмся в бюджет кадра — +**сейчас менять ничего не нужно**. + +## Что стоит сейчас + +`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), младшие биты не вырождены. + +```z80 +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. + +```z80 +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 +``` + +```asm +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 ниже него не опустится. + +Порядок действий, если понадобится: + +1. Сначала убрать обёртки: слить `pop_rnd_fit` в ту же asm-процедуру, чтобы + на вызов приходился один `call`, а не три. Это ничего не ломает и не + трогает совместимость с эталоном. +2. И только если этого мало — менять генератор, начиная с варианта A + (качество последовательности у него не хуже LCG, в отличие от B). + +Важно помнить: число вызовов вырастет с боёвкой. Сейчас их два за кадр +(факелы), а `guard_advance` / `guard_block` / `guard_strike` дёргают +`prandom(255)` каждый по разу за кадр боя — то есть при драке станет 5–6, и +цена вопроса вырастет во столько же раз.