diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c index 20279b7..6db81b5 100644 --- a/applications/PoP/roomtest/pop_bg.c +++ b/applications/PoP/roomtest/pop_bg.c @@ -354,7 +354,7 @@ static uint8_t wall_modifier(int row, int col) } /* ---- PRNG (seg009.c:321, 32-бит LCG) — только на ВХОД в комнату ---- */ -static unsigned long rnd_seed; +static pop_rnd_t rnd_seed; #define prandom(maxv) pop_prandom(&rnd_seed, (maxv)) @@ -417,7 +417,7 @@ static void wall_rnd(int row, int col) * соседнего РЯДА и чужую раскладку (тихий баг вместо промаха). */ int idx = (col >= 0 && col < 10) ? (row + 1) * 10 + col : -1; uint8_t a, b, bg; - unsigned long saved; + pop_rnd_t saved; if (idx >= 0 && idx < WP_N && wp_ok[idx]) { a = wp_a[idx]; b = wp_b[idx]; wr_mid_div = (uint8_t)(a & 1); wr_mid_off = (uint8_t)((a >> 1) & 7); @@ -430,7 +430,7 @@ static void wall_rnd(int row, int col) saved = rnd_seed; /* Верхняя полоса (row<0): SDLPoP рисует её как ряд 2 комнаты сверху * (drawn_row=2 для seed, draw_main_y=-1 для позиции) — seed по row 2. */ - rnd_seed = (unsigned long)(g_room + TBL_LINE[row < 0 ? 2 : row] + col); + pop_prandom_set(rnd_seed, g_room + TBL_LINE[row < 0 ? 2 : row] + col); prandom(1); /* сброс */ wr_mid_div = (uint8_t)prandom(1); wr_mid_off = (uint8_t)prandom(4); diff --git a/applications/PoP/roomtest/pop_bg.h b/applications/PoP/roomtest/pop_bg.h index 9aee114..87c73a2 100644 --- a/applications/PoP/roomtest/pop_bg.h +++ b/applications/PoP/roomtest/pop_bg.h @@ -25,6 +25,19 @@ /* Экранный y нижней границы поля (ниже — статус-полоса + борт → клип). */ #define POP_CLIP_BOTTOM (POP_YOFF + POP_PLAYFIELD_H) +/* Спрайт целиком на экране И укладывается в 8-битные параметры noclip- + * примитивов libbgi? Условие входа в gfx_blit_cols_part_noclip: клипающий + * вариант платит ~5.6 К тактов подготовки на КАЖДЫЙ вызов, независимо от + * того, вылезает край или нет. + * static inline, а не чистый inline: SDCC оставляет мёртвое тело в каждом TU + * (memory sdcc_inline_codegen_findings), но здесь это ~30 байт на два TU — + * дешевле риска неразрешённой ссылки. */ +static inline uint8_t pop_onscreen_cols(int x, int y, uint16_t w, uint16_t h) +{ + return (uint8_t)(x >= 0 && y >= 0 && w < 256 && h < 256 && + x + (int)w <= 320 && y + (int)h <= 256); +} + /* Загрузить 7 атласов фона. Звать ДО initgraph (как poc.c — atlas_load * трогает W3; проверенный порядок). Палитру pop_bg.pal грузит ПРИЛОЖЕНИЕ * после initgraph (gfx_pal_fload). 0 — OK, -1 — ошибка. */ diff --git a/applications/PoP/roomtest/pop_gdraw.c b/applications/PoP/roomtest/pop_gdraw.c index bf325d9..fe8cc4c 100644 --- a/applications/PoP/roomtest/pop_gdraw.c +++ b/applications/PoP/roomtest/pop_gdraw.c @@ -100,7 +100,10 @@ void pop_guard_draw(void) __banked top = obj_y - (int)h + 1; p = gfx_get_draw_page() & 1; gfx_set_bank(GFX_BANK_SPRITE); - gfx_blit_cols_part(bx, top + POP_YOFF, img, flip, 0, 0); + if (pop_onscreen_cols(bx, top + POP_YOFF, w, h)) + gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, 0, 0); + else + gfx_blit_cols_part(bx, top + POP_YOFF, img, flip, 0, 0); gfx_set_bank(GFX_BANK_NORMAL); gx_l[p] = bx; gy_l[p] = top; gw_l[p] = w; gh_l[p] = h; gvalid[p] = 1; clip_x = bx; clip_y = top; clip_w = w; clip_h = h; diff --git a/applications/PoP/roomtest/pop_geom.c b/applications/PoP/roomtest/pop_geom.c index 3f3f2fb..e644209 100644 --- a/applications/PoP/roomtest/pop_geom.c +++ b/applications/PoP/roomtest/pop_geom.c @@ -15,9 +15,95 @@ int8_t pop_y_to_row(int16_t y) return (int8_t)((y + 60) / TILE_SIZEY % 4 - 1); } -uint16_t pop_prandom(unsigned long *seed, uint16_t maxv) +/* Уложить значение в диапазон 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) { - unsigned long s = *seed * 214013UL + 2531011UL; - *seed = s; - return (uint16_t)((uint16_t)(s >> 16) % (uint16_t)(maxv + 1)); + 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 diff --git a/applications/PoP/roomtest/pop_geom.h b/applications/PoP/roomtest/pop_geom.h index 71c0a38..835cd78 100644 --- a/applications/PoP/roomtest/pop_geom.h +++ b/applications/PoP/roomtest/pop_geom.h @@ -26,9 +26,31 @@ extern const int16_t pop_y_land[5]; * над комнатой, 3 = под комнатой). */ int8_t pop_y_to_row(int16_t y); -/* prandom (seg009:321) — 32-битный LCG оригинала. Сид ВНЕШНИЙ: у стен - * (раскладка кладки) и у анимаций тайлов (фаза факелов) свои - * последовательности, смешивать их нельзя. */ -uint16_t pop_prandom(unsigned long *seed, uint16_t maxv); +/* prandom (порт seg009:321). Сид ВНЕШНИЙ: у стен (раскладка кладки) и у + * анимаций тайлов (фаза факелов) свои последовательности, смешивать их + * нельзя. + * + * ДВА генератора, выбор через POP_PRANDOM_EXACT (см. pop_geom.c): + * 0 (по умолчанию) — xorshift16 + шаг Вейля. Быстрый: ни одного + * умножения. Последовательность ОТЛИЧАЕТСЯ от SDLPoP, то есть другая + * раскладка кладки и другие броски в боёвке — статистически + * эквивалентные, но не те же самые; + * 1 — LCG оригинала (s*214013+2531011) бит-в-бит. Для сверки картинки + * с эталоном. Дороже: даже посчитанный 16-битными половинами (без + * __mullong, который один стоил 8 430 тактов — замер в MAME) он + * обходится в ~7 000 тактов на вызов. + * + * Состояние — две 16-битные половины, а не unsigned long: иначе SDCC на + * умножении зовёт __mullong. */ +typedef struct { uint16_t lo, hi; } pop_rnd_t; + +#ifndef POP_PRANDOM_EXACT +#define POP_PRANDOM_EXACT 0 +#endif + +uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv); + +/* Сид из 16-битного числа (старшая половина = 0). */ +#define pop_prandom_set(s, v) do { (s).lo = (uint16_t)(v); (s).hi = 0; } while (0) #endif diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index 77ef83e..935ef52 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -81,7 +81,12 @@ uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr, top = obj_y + (int)SWORD_DY[sf] - (int)h + 1; if (flip) bx -= (int)w; gfx_set_bank(GFX_BANK_SPRITE); - gfx_blit_cols_part(bx, top + POP_YOFF, simg, flip, 0, 0); + /* Клинок почти всегда целиком на экране — noclip-путь (см. gfx.h: + * подготовка клипающего варианта стоит ~5.6 К тактов на вызов). */ + if (pop_onscreen_cols(bx, top + POP_YOFF, w, h)) + gfx_blit_cols_part_noclip(bx, top + POP_YOFF, simg, flip, 0, 0); + else + gfx_blit_cols_part(bx, top + POP_YOFF, simg, flip, 0, 0); gfx_set_bank(GFX_BANK_NORMAL); *rx = bx; *ry = top; *rw = w; *rh = h; return 1; @@ -512,7 +517,10 @@ void kid_draw_splash(void) int top = obj_y - (int)h + 1; if (flip) bx -= (int)w; gfx_set_bank(GFX_BANK_SPRITE); - gfx_blit_cols(bx, top + POP_YOFF, img, flip); + if (pop_onscreen_cols(bx, top + POP_YOFF, w, h)) + gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, 0, 0); + else + gfx_blit_cols(bx, top + POP_YOFF, img, flip); gfx_set_bank(GFX_BANK_NORMAL); pop_clip_sprite(bx, w, top + POP_YOFF, (int)h); /* Брызги — накладной спрайт: свой прямоугольник heal (см. @@ -593,8 +601,14 @@ void kid_draw(void) if (skip >= (int)h) skip = (int)h; /* обрезан целиком */ } gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */ - if (skip < (int)h) /* +YOFF: центрирование */ - gfx_blit_cols_part(bx, top + skip + POP_YOFF, img, flip, skip, 0); + if (skip < (int)h) { /* +YOFF: центрирование */ + if (pop_onscreen_cols(bx, top + skip + POP_YOFF, w, + (uint16_t)((int)h - skip))) + gfx_blit_cols_part_noclip(bx, top + skip + POP_YOFF, img, flip, + (uint8_t)skip, 0); + else + gfx_blit_cols_part(bx, top + skip + POP_YOFF, img, flip, skip, 0); + } gfx_set_bank(GFX_BANK_NORMAL); /* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) — * узко, только по прямоугольнику Кида и только при выходе. */ diff --git a/applications/PoP/roomtest/pop_trob.c b/applications/PoP/roomtest/pop_trob.c index b8f54a1..86b3f30 100644 --- a/applications/PoP/roomtest/pop_trob.c +++ b/applications/PoP/roomtest/pop_trob.c @@ -47,7 +47,7 @@ static uint8_t trob_drawn[POP_ROOMTILES]; /* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и * выбора следующего кадра пламени. */ -static unsigned long trob_seed; +static pop_rnd_t trob_seed; #define trob_prandom(maxv) pop_prandom(&trob_seed, (maxv)) diff --git a/libbgi/common/gfx_blit_cols_part_noclip.c b/libbgi/common/gfx_blit_cols_part_noclip.c new file mode 100644 index 0000000..00f014a --- /dev/null +++ b/libbgi/common/gfx_blit_cols_part_noclip.c @@ -0,0 +1,49 @@ +/* + * gfx_blit_cols_part_noclip — блит спрайта column-major БЕЗ клипа по экрану, + * с обрезкой сверху (как gfx_blit_cols_part). Пара к gfx_blit_cols_part + * ровно так же, как gfx_blit_part_noclip — к gfx_blit_part. + * + * Зачем: у персонажей PoP спрайт почти всегда целиком на экране, а общий + * gfx_blit_cols_part платит за проверку четырёх границ, пересчёт w/h/cx/sy и + * 16-битную арифметику независимо от того, вылезает край или нет — замер в + * MAME (roomtest, страж + клинок): 5 622 такта подготовки против 13 596 на + * сам accel-проход. Здесь та же колонка уходит в _bgi_blit_cols_raw без + * подготовки. + * + * Требования (проверяет ВЫЗЫВАЮЩИЙ, кода проверок здесь нет): + * - прямоугольник назначения целиком на экране; + * - ширина и высота кадра <= 255, y <= 255; + * - skip < высоты кадра. + * Не выполняется — зовите gfx_blit_cols_part. + * + * Флип, как и в gfx_blit_cols_part, бесплатный: подаём src с последней + * колонки и отрицательный страйд — accel сам кладёт её в левый x. + * + * W3-скобка ставится ЗДЕСЬ (код libbgi живёт в W1): из приложения, + * собранного с --w3, её вызывать нельзя — после _bgi_begin окно W3 занято + * видеобанком и код вызывающего исчезает из адресного пространства. + */ +#include +#include "../_bgi.h" + +void gfx_blit_cols_part_noclip(int x, int y, const void *img, uint8_t flip, + uint8_t skip, uint8_t rows) +{ + const uint8_t *p = (const uint8_t *)img; + uint8_t w = p[0]; /* ширина кадра (< 256) */ + uint8_t fh = p[2]; /* высота кадра (< 256) */ + const uint8_t *src = p + 4; + uint8_t h; + + if (w == 0 || fh == 0 || skip >= fh) return; + h = (uint8_t)(fh - skip); + if (rows && rows < h) h = rows; + + if (flip) src += (uint16_t)(w - 1) * fh; /* последняя колонка -> левый x */ + src += skip; + + _bgi_begin(); + _bgi_blit_cols_raw(src, (uint8_t *)(_gfx_addr_base + (uint16_t)x), + w, h, flip ? -(int)fh : (int)fh, (uint8_t)y); + _bgi_end(); +} diff --git a/libbgi/include/gfx.h b/libbgi/include/gfx.h index 853f170..90f9683 100644 --- a/libbgi/include/gfx.h +++ b/libbgi/include/gfx.h @@ -128,6 +128,16 @@ void gfx_blit_cols(int x, int y, const void *img, uint8_t flip); void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip, int skip, int rows); +/* То же БЕЗ клипа по экрану — пара к gfx_blit_cols_part так же, как + * gfx_blit_part_noclip к gfx_blit_part. Вызывающий гарантирует: спрайт + * целиком на экране, ширина/высота кадра и y <= 255, skip < высоты. + * rows = 0 — «до низа кадра». Экономит ~5.6 К тактов подготовки на вызов + * (замер PoP roomtest: подготовка 5 622 против 13 596 на сам accel-проход), + * что для персонажа со спрайтом-компаньоном (клинок) даёт заметную долю + * кадра. */ +void gfx_blit_cols_part_noclip(int x, int y, const void *img, uint8_t flip, + uint8_t skip, uint8_t rows); + /* Восстановить прямоугольник экрана из ОЗУ-копии (стирание спрайта/ * оверлея, нарисованного банком с битом 2: 0x54/0x5C). Всегда * работает банком GFX_BANK_NORMAL независимо от gfx_set_bank;