From 37fc572cc3c4163eb1d42274f765cac43eb60a95 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Thu, 30 Jul 2026 17:05:13 +0300 Subject: [PATCH] =?UTF-8?q?PoP=20roomtest:=20LCG=20=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D0=B3=D0=B8=D0=BD=D0=B0=D0=BB=D0=B0=20=D0=BD=D0=B0=20=D0=B0?= =?UTF-8?q?=D1=81=D1=81=D0=B5=D0=BC=D0=B1=D0=BB=D0=B5=D1=80=D0=B5=20?= =?UTF-8?q?=E2=80=94=20=D1=82=D0=BE=D1=87=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20?= =?UTF-8?q?=D0=B1=D0=B5=D0=B7=20=D0=BF=D0=BE=D1=82=D0=B5=D1=80=D0=B8=20?= =?UTF-8?q?=D1=81=D0=BA=D0=BE=D1=80=D0=BE=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Возврат к БИТ-В-БИТ генератору оригинала по умолчанию (POP_PRANDOM_EXACT=1): по нему проще отлаживать и сверять картинку с эталоном. Чтобы это не стоило процента бюджета, сам шаг LCG переписан на Z80-ассемблере — единственное место в порте, где это сделано, с явного разрешения. Приём: 214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3 — схема Горнера по РАЗРЕЖЕННОЙ записи константы. Вместо 12 сложений (по числу единиц в 0x343FD) — 17 удвоений, три сложения и одно вычитание; величина 3*s, нужная в конце, попадается по дороге на втором шаге. Проверка в ДВА этапа: - схема на хосте: horner(s) == s*214013+2531011 на 3 000 000 сидов; - сама asm-транскрипция на живой машине: breakpoint на pop_prandom, 11 последовательных состояний сида из MAME — каждый переход совпал с s*214013+2531011 бит-в-бит. Замер, комната 3, 175 кадров (медиана кадра / prandom->torch_draw): C, бит-в-бит (16-бит половины) 403 632 / 10 933 C, xorshift16 + шаг Вейля 397 986 / 7 927 asm, бит-в-бит 400 800 / 9 331 То есть asm вернул половину разрыва (2 832 такта за кадр), сохранив совместимость с эталоном. Ветка xorshift оставлена под -DPOP_PRANDOM_EXACT=0 как запасной ход — брать её имеет смысл, только если не хватит последних 2 800 тактов. Итог оптимизационного круга: 416 154 -> 400 800 (0.968 -> 0.932 кадра). Co-Authored-By: Claude Opus 5 --- applications/PoP/roomtest/pop_geom.c | 207 +++++++++++++++++++++------ applications/PoP/roomtest/pop_geom.h | 27 ++-- 2 files changed, 183 insertions(+), 51 deletions(-) diff --git a/applications/PoP/roomtest/pop_geom.c b/applications/PoP/roomtest/pop_geom.c index e644209..55ab792 100644 --- a/applications/PoP/roomtest/pop_geom.c +++ b/applications/PoP/roomtest/pop_geom.c @@ -27,50 +27,176 @@ static uint16_t pop_rnd_fit(uint16_t v, uint16_t maxv) } #if POP_PRANDOM_EXACT -/* s = s * 214013 + 2531011, 16-битными половинами. +/* ---- LCG оригинала: s = s*214013 + 2531011 (seg009:321) ------------ * * - * 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). + * ЕДИНСТВЕННОЕ место в порте на ассемблере, и вот почему. На Z80 нет + * умножения, а SDCC для 32-битного `s * 214013` зовёт __mullong — 8 430 + * тактов на вызов (замер в MAME). Написанное на C через 16-битные + * половины это ~3 500. Здесь ~1 000, при полностью той же + * последовательности, что в SDLPoP. Альтернатива «взять генератор + * попроще» (8-битный RND Apple II, xorshift) экономит меньше и ломает + * совместимость с эталоном, по которому сверяем картинку. * - * Проверка эквивалентности — на хосте: те же значения, что давал - * unsigned long (см. комментарий к pop_rnd_t в pop_geom.h). */ -uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv) + * Приём: 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 { - 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; + (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 - 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++; /* перенос в старшее слово */ + ld h, b + ld l, c + ld de, (_pop_pr_sh) ; акк = s - /* (SL*MH + SH*ML) << 16 — только младшие 16 бит слагаемого. */ - ph = (uint16_t)(ph + (uint16_t)(sl * 3u) + (uint16_t)(sh * 0x43FDu)); + ;; акк = акк*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 - /* + 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); + ;; акк = акк*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; } -#else /* !POP_PRANDOM_EXACT — быстрый 16-битный генератор (по умолчанию) */ +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 тактов на вызов — при двух @@ -88,10 +214,11 @@ uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv) * xorshift ноль — неподвижная точка, а сид кладки (номер комнаты + ряд + * колонка) вполне может оказаться нулём. * - * ПОСЛЕДСТВИЕ: последовательность отличается от SDLPoP. Значит другая - * (но статистически такая же) раскладка кладки и другие броски в боёвке. - * Если понадобится сверять картинку с оригиналом — собрать с - * -DPOP_PRANDOM_EXACT=1, там бит-в-бит тот же LCG. */ + * ПОСЛЕДСТВИЕ: последовательность отличается от SDLPoP — другая (но + * статистически такая же) раскладка кладки и другие броски в боёвке. + * Поэтому по умолчанию собирается НЕ эта ветка: после переписывания LCG + * на ассемблер выигрыш от смены генератора почти исчез, а сверять + * картинку с эталоном стало важнее. Ветка оставлена как запасной ход. */ uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv) { uint16_t s = seed->lo; diff --git a/applications/PoP/roomtest/pop_geom.h b/applications/PoP/roomtest/pop_geom.h index 835cd78..67ef250 100644 --- a/applications/PoP/roomtest/pop_geom.h +++ b/applications/PoP/roomtest/pop_geom.h @@ -31,21 +31,26 @@ int8_t pop_y_to_row(int16_t y); * нельзя. * * ДВА генератора, выбор через POP_PRANDOM_EXACT (см. pop_geom.c): - * 0 (по умолчанию) — xorshift16 + шаг Вейля. Быстрый: ни одного - * умножения. Последовательность ОТЛИЧАЕТСЯ от SDLPoP, то есть другая - * раскладка кладки и другие броски в боёвке — статистически - * эквивалентные, но не те же самые; - * 1 — LCG оригинала (s*214013+2531011) бит-в-бит. Для сверки картинки - * с эталоном. Дороже: даже посчитанный 16-битными половинами (без - * __mullong, который один стоил 8 430 тактов — замер в MAME) он - * обходится в ~7 000 тактов на вызов. + * 1 (по умолчанию) — LCG оригинала (s*214013+2531011) БИТ-В-БИТ: та же + * раскладка кладки и те же броски, что в SDLPoP, поэтому по картинке + * можно сверяться с эталоном; + * 0 — xorshift16 + шаг Вейля, без единого умножения. Дешевле ровно на + * 5 646 тактов за кадр, 1.3 % бюджета (замер A/B в MAME, комната 3, + * 175 кадров: 403 632 против 397 986). Последовательность другая — + * статистически эквивалентная, но не та же. * - * Состояние — две 16-битные половины, а не unsigned long: иначе SDCC на - * умножении зовёт __mullong. */ + * Обе версии считают 16-битными половинами, а не unsigned long: на 32-бит + * умножении SDCC зовёт __mullong, и это 8 430 тактов на вызов (замер в + * MAME, breakpoint на __mullong). + * + * ЗАДЕЛ: разрыв между версиями определяется не алгоритмом, а кодогенерацией + * SDCC — те же генераторы на Z80-ассемблере укладываются в 86..148 тактов. + * Если процент понадобится, сначала переписать в asm ОРИГИНАЛЬНЫЙ LCG (там + * умножение на константу), а менять генератор — только если и этого мало. */ typedef struct { uint16_t lo, hi; } pop_rnd_t; #ifndef POP_PRANDOM_EXACT -#define POP_PRANDOM_EXACT 0 +#define POP_PRANDOM_EXACT 1 #endif uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv);