Гипотеза «двухбайтная индексация съест выигрыш от сложения» не
подтвердилась. Собраны ОБА варианта, такты посчитаны по сгенерированному
asm (хвост после проверки границ):
int16_t готовое: add hl,hl / add hl,de / ld e,(hl) / inc hl / ld d,(hl)
= 132 такта, 2 304 байта
int8_t x/7: add hl,bc / ld a,(hl) / ld l,a / rlca / sbc a,a /
ld h,a / add hl,de = 131 такт, 1 152 байта
Расширение знака плюс 16-битное сложение стоят ровно столько же, сколько
лишний add hl,hl при двухбайтном индексе, а обращений к памяти на одно
меньше — под wait-state'ами Sprinter (2,4x номинала именно на обращениях
к ОЗУ) байтовый вариант ещё чуть выгоднее номинала.
Банк 4: 9 394 -> 8 243 из 16 384 (свободно 8 141 вместо 6 990) — запас под
рост pop_cdraw, о котором и был вопрос.
Тест переименован в geom_mul8div7_table_rules и проверяет ОБА правила
генерации таблицы: тождество 8x/7 == x + x/7 и усечение к нулю.
tests-host: [geom] 1992 -> 3144, все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Первая версия крыла только 0..255 байтовой таблицей x/7 по тождеству
8x/7 == x + x/7. Это было мимо: obj_x = 2*fwd - 116 уходит в минус, как
только fwd < 58 (левее x_bump[5]) — то есть у ЛЕВОЙ КРОМКИ комнаты, и там
мы продолжали звать __divsint.
Границы взяты из данных, а не на глаз: kid_data.bin даёт dx кадров Кида
-5..+10, стража -2..+10; при Char.x типа uint8_t и render_dx из
{-140,0,+140} полный диапазон obj_x = -416..695. SCRX[1152] кроет
-448..703 — деление стало недостижимым, оставлено страховкой.
Хранится ГОТОВОЕ значение (int16_t), а не x/7: байтовая таблица вдвое
меньше, но со знаковыми значениями требует расширения знака плюс
16-битного сложения — те же такты, что лишний add hl,hl при 2-байтном
индексе. Кодоген проверен: индекс полный 16-битный (грабли
sdcc_z80_const_ptr_index_bug обойдены отдельной uint16_t-переменной),
~130 тактов номинала против ~1 000 у __divsint.
Банк 4: 7 336 -> 9 394 из 16 384 (свободно 6 990). Таблица сверена
питоном обратно из .c (1 152 записи), правило генерации «усечение к нулю»
закреплено тестом geom_mul8div7_trunc_to_zero на целевом компиляторе.
tests-host: [geom] 1961 -> 1992, все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обход всех сгенерированных .asm (awk по `call __div/__mod/__mul` с
привязкой к строке исходника) нашёл 31 вызов в 9 модулях. Три из них
были в горячем пути:
- pop_cdraw.c calc_screen_x_coord: `x * 8 / 7` -> __divsint, 2 400 тактов
на ПЕРСОНАЖА КАЖДЫЙ КАДР (два вызова при живом сопернике). Заменено
тождеством 8x/7 == x + x/7 плюс таблица DIV7[256] в банке 4 — резидент
не тронут, обычный диапазон (obj_x 0..252 при x_bump 58..184) покрыт
целиком, деление осталось только хвостом для шва (render_dx = ∓140).
- pop_guard.c guard_col_from_x: /14 и %14 звались БЕЗУСЛОВНО, мимо
POP_TILE_DIV — единственное 16-битное деление без подключённой таблицы.
- pop_trob.c animate_chomper: `tp / 10` на чомпера каждый кадр, при том
что TP_ROW/TP_COL лежали в этом же файле, но ниже по тексту. Таблицы
подняты выше чомперов.
Остальные 25 оставлены осознанно и расписаны в TASKS_OPEN.md: хвосты за
таблицей (x вне 0..255 = персонаж в соседней комнате), намеренный
медленный хвост pop_y_to_row, недостижимая ветка pop_rnd_fit и холодные
места (вход стража, старт уровня, читы, имя файла, отладочный HUD).
В банках 4, 6, 7 теперь ноль __div*. Тождество 8x/7 закреплено тестом
geom_mul8div7_identity (перебор −420..700), таблица DIV7 сверена с x//7.
tests-host: [geom] 840 -> 1961, все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mob_tick_one (927) и mob_render (976/977) считали `(y+60)/63 % 4 - 1`
вручную, хотя pop_y_to_row — точный эквивалент этой формулы на всём
int16_t (включая усечение деления к нулю для отрицательных). В asm это
были три пары __divsint+__modsint, ~16 200 тактов (3,8 % кадра) — только
пока кусок плиты в полёте, то есть в самом тяжёлом кадре.
В банке 7 теперь ноль __divsint. Эквивалентность закреплена тестом
geom_y_to_row_matches_formula: перебор −400..400 против исходной формулы
(вызовы разбросаны по трём банкам, соблазн написать деление «по месту»
возвращается). tests-host: [geom] 39 -> 840, все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обвязка для быстрых тестов plain-C логики: секунды вместо прогона в MAME,
без образа диска. ucsim_z80 идёт в комплекте нашего SDCC — новых
зависимостей нет.
ПОЧЕМУ ПОД Z80, А НЕ ХОСТОВЫМ GCC. У SDCC z80 int 16 бит, у хоста 32, и
расходится это НЕ в объявлениях, а в выражениях: integer promotion
повышает операнды до int независимо от того, объявлены они как uint8_t
или uint16_t. Перевод кода на фиксированные типы разницу не убирает —
убирает только исполнение с z80-семантикой. Побочно проверяется
кодогенерация SDCC и модули с inline-asm, которых хостовая сборка не
видит в принципе.
Устройство: crt0_ucsim.s (SP, зануление, main, halt), tcheck.* (итог в
структуру в ОЗУ), run_ucsim.py (гоняет ucsim, дампит tc_result, печатает
отчёт), host-tests.mk (общие правила). Вывода через printf нет: тестовый
бинарь линкуется без Sprinter-libc. Через ucsim-simif не идём — номера
его команд плавают между версиями, halt + dump работают везде.
Наборы лежат РЯДОМ с проверяемым кодом, обвязка общая:
testkit/t_selftest.c — самопроверка (sizeof(int)==2)
applications/PoP/roomtest/tests-host/ — движок PoP
Первый содержательный набор — t_geom: сверяет рукописный asm-LCG из
pop_geom.c с наивной 32-битной формулой на 128 шагах. Заявка «бит-в-бит
как в SDLPoP» до сих пор держалась на комментарии. Тест проверен
мутацией: порча эталонной константы даёт красный.
Планы дальнейшего покрытия:
docs/host-tests-plan.md — libc и libbgi (не начато)
applications/PoP/docs/host_tests_plan.md — движок PoP
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>