From c312e4a0432af290ea65c209772349c6d1543fe8 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Thu, 13 Aug 2026 23:51:20 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9D=D0=B0=D0=B9=D0=B4=D0=B5=D0=BD=D1=8B=2087?= =?UTF-8?q?91=20=D1=82=D0=B0=D0=BA=D1=82=D0=BE=D0=B2=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=B2=D1=8B=D0=B7=D0=BE=D0=B2=20=D0=B1=D0=BB=D0=B8=D1=82=D0=B0?= =?UTF-8?q?:=20uint16=5Ft=20=D0=B3=D0=B0=D0=B1=D0=B0=D1=80=D0=B8=D1=82=20?= =?UTF-8?q?=D0=B2=20pop=5Fblit=5Fb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Точки на цепочку вызовов (libbgi живёт в резиденте W1, адреса однозначны — пересборка не нужна) разложили постоянные накладные блита: pop_blit_b ДО вызова ядра ....... 6 683 <- НАШ код, 77% накладных gfx_blit_noclip + _bgi_begin + _gfx_blit_sprite_noclip ..... 1 938 пролог _bgi_blit_rows_raw ....... 457 строчный цикл ................... 389/строку (= 198 + 5,96*32, сходится с регрессией) эпилог + _bgi_end + возврат ..... 1 355 «вне цикла» ..................... 8 725 при ЛЮБОЙ высоте (h=9..60) Причина в pop_blit_b, подтверждена чтением .asm: `w`/`h` объявлены uint16_t, 16-битные значения не влезли в регистры, и SDCC увёл функцию в 14-байтовый стековый кадр (`ld iy,#-14 / add iy,sp / ld sp,iy`), после чего `w = img[0] | (img[1] << 8)` развернулось в ДВА ДЕСЯТКА IX-относительных пересылок между ячейками -7..-13 кадра. Правка: габарит читается БАЙТАМИ. Корректно по построению — обе ветки и так требовали w<256 && h<256 (эти проверки теперь убраны как тождественные), а кадры атласов не крупнее 32x63; формат .atl допускает больше, такой кадр уходит на общий путь (blit_b_oversize). Замер A/B на той же детерминированной сцене, те же выборки: gfx_blit_noclip 13 679 -> 11 530 (-16%) blit_b_clip 18 853 -> 15 340 (-19%) Стековый кадр 14 -> 6 байт, _CODE -42 Б. Тайл ~107 000 -> ~95 000. Все 8 наборов tests-host проходят, комната 23 в MAME рисуется корректно. Плюс TASKS_OPEN.md: OPT-BLIT — руководство на следующую сессию (где ещё uint16->uint8, как искать IX-спиллы по asm, что НЕ делать, и грабли с несколькими экземплярами MAME на один error.log). Co-Authored-By: Claude Opus 5 --- applications/PoP/roomtest/TASKS_OPEN.md | 83 +++++++++++++++++++++++++ applications/PoP/roomtest/pop_tile.c | 38 +++++++++-- 2 files changed, 116 insertions(+), 5 deletions(-) diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md index df85823..91a3146 100644 --- a/applications/PoP/roomtest/TASKS_OPEN.md +++ b/applications/PoP/roomtest/TASKS_OPEN.md @@ -923,3 +923,86 @@ HP/минуты, номера «особых» комнат и уровней ( `../docs/ideas_backlog.md` (оба дают доли процента кадра). - **OPT-1** (хирургический редрой шва) — решено НЕ делать, стоимость транзиентная; разбор в [`BUGS_CLOSED.md`](BUGS_CLOSED.md). + +--- + +## OPT-BLIT — цена отрисовки: чистка кодогена (СЛЕДУЮЩАЯ СЕССИЯ) + +Открыто 2026-08-13 по итогам разбора зелёного блока. Контекст и все замеры +— в memory `blit_cost_model` и `sdcc_z80_stack_locals_hot_loop`, протокол +разбора — в коммитах `f89b7dd` / `b27b313` / этом. + +**Что уже сделано и чем это подтверждено.** Цена блита разложена регрессией +по 339 замерам: `такты = 8791 + 198,2*h + 5,96*(w*h)` (такты MAME = системный +клок ~21,5 МГц, НЕ такты Z80 — растровый кадр 430 000). Байт на пределе +железа (через акселератор дважды по 3 такта), строка ~198, а вот 8 791 на +ВЫЗОВ оказались нашим кодогеном: `pop_blit_b` держал `w`/`h` как `uint16_t`, +из-за чего вся функция уезжала в 14-байтовый стековый кадр и `w = img[0] | +(img[1] << 8)` разворачивалось в два десятка IX-относительных пересылок. +После перехода на байтовый габарит: noclip 13 679 -> 11 530 (-16%), +клипованный 18 853 -> 15 340 (-19%), `_CODE` -42 Б. + +### а) Где ещё `uint16_t` можно сделать `uint8_t` + +Габариты спрайтов и всё, что из них считается. Признак: значение заведомо +<= 255, но объявлено 16-битным «на всякий случай», и живёт в горячем пути. +Начинать с: +- `blit_b_clip` (`pop_tile.c`) — `dw`/`dh`/`sx`/`sy` объявлены `int`, хотя + ядра принимают `uint8_t`; это ВТОРОЙ по частоте путь (33% блитов). +- `pop_cd_touch` / `cd_cols_of` — четыре `int`-аргумента на вызов. +- `_gfx_blit_sprite_noclip` и обёртки libbgi: `stride` уже `uint16_t`, а + `w`/`h` — `uint8_t`; проверить, не расширяются ли они обратно у вызывающих. +- Кандидаты в `pop_room.c`/`pop_bg.c`: локальные `int x, dby, dmy` в + `draw_tile` — часть из них помещается в байт, но осторожно: координаты + бывают отрицательными. + +**Как проверять:** после каждой правки смотреть пролог функции в +`.sprinter-cc-roomtest/*.asm` — исчез ли `ld iy,#-N / add iy,sp / ld sp,iy` +и сколько осталось `-N (ix)` в теле. + +### б) Проход по asm за неоптимальным IX-доступом + +Механическая проверка, даёт больше всего за единицу усилий: + +``` +grep -c "(ix)" .sprinter-cc-roomtest/*.asm # где сгущается +grep -n "ld iy, #-" .sprinter-cc-roomtest/*.asm # крупные стековые кадры +grep -n "pop.*\n.*pop.*\n.*push" ... # чтение спилла через стек +``` + +Признаки беды (все три встречались сегодня): пролог с `iy`-кадром больше +~6 байт; пары `pop bc / pop hl / push hl / push bc` в теле цикла (SDCC +читает спиленный указатель через стек вместо `ld l,-N(ix)`); повторный +пересчёт адреса `arr[i]` под каждое поле структуры. + +Приоритет по частоте вызова: `pop_blit_b` (сделан) -> `blit_b_clip` -> +`pop_cd_touch` -> `draw_tile` -> `pop_redraw_needed`. + +### Что НЕ делать (проверено сегодня, отрицательный результат) + +- **Не откладывать запекание на другой кадр.** Запекание пишет ОЗУ-копию + фона, из которой восстанавливает `heal`; отложенное даёт призрак плиты на + месте дыры. Идея «бюджет одного запекания на кадр» снята. +- **Не ускорять передачу пикселей** — она на пределе железа (3+3 такта на + байт), см. модель выше. +- **Не искать проблему в W3-скобке** (`_bgi_begin`/`_bgi_end` — по пять + инструкций) и не списывать разброс на прерывания (внутри размерной + группы разброс 3 такта, код прямолинейный). + +### Хвосты этой сессии + +- Пакетная пометка `pop_cd_touch` (`pop_cd_batch_begin/end`, скобка в + `draw_tile`) — сделана, но выигрыш замером НЕ подтверждён: в захваченных + кадрах скобка не срабатывала (блиты шли из холодной отрисовки комнаты). + Переснять на кадрах ЗАПЕКАНИЯ. +- Снять временную оснастку: `pop_dbg_m9..m16`, `pop_dbg_b1..b6`, + `pop_dbg_wh`, `pop_dbg_kind`, `pop_dbg_rdmax*`, `mame/v306/run_bridge_log.sh`. +- Цель по кадру не достигнута: зелёный пик был 792 012 при цели 400 000. + После сегодняшних правок не перемерян — начать сессию с контрольного + замера, а не с новых правок. + +**ГРАБЛИ (стоили сегодня часа):** проверять, что запущен РОВНО ОДИН MAME +(`pgrep -f mame.arm | wc -l`) — несколько экземпляров пишут в один +`error.log`, мост говорит с одним, замеры собираются с другого, и точки +«не срабатывают». И не обрезать `error.log`, пока MAME его держит: она +пишет по старому смещению, в начале остаётся дыра из нулей. diff --git a/applications/PoP/roomtest/pop_tile.c b/applications/PoP/roomtest/pop_tile.c index b50d5de..73b9e0d 100644 --- a/applications/PoP/roomtest/pop_tile.c +++ b/applications/PoP/roomtest/pop_tile.c @@ -363,10 +363,22 @@ static void blit_b_clip(const uint8_t *img, int x, int top, int w, int h) if (!pop_t_fclip_on) pop_cd_touch(dx, dy, dw, dh); } +/* Кадр шире или выше 255 — общий (медленный) путь. Вынесен отдельно, чтобы + * 16-битная арифметика габарита не жила в горячем pop_blit_b: у нас таких + * кадров нет вовсе (максимум 32x63), но контракт формата .atl их допускает. */ +static void blit_b_oversize(const uint8_t *img, int x, int ybottom) +{ + uint16_t w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); + uint16_t h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); + int top = ybottom - (int)h + 1 + POP_YOFF; + if (pop_upside) top = FLIP_TOP(top, h); + if (w && h) blit_b_clip(img, x, top, (int)w, (int)h); +} + void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) { const uint8_t *img; - uint16_t w, h; + uint8_t w, h; /* БАЙТЫ, а не uint16_t — см. ниже */ int top; if (idx >= a->count) return; @@ -384,9 +396,24 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) img = (const uint8_t *)atlas_image(a, idx); gfx_w0_map(a->page); pop_dbg_b2(); /* ВРЕМЕННО */ - pop_dbg_wh((uint16_t)(((uint16_t)img[0] << 8) | img[2])); /* ВРЕМЕННО: w,h */ - w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); - h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); + /* Габарит читаем БАЙТАМИ. С `uint16_t w = img[0] | (img[1] << 8)` SDCC + * разворачивал каждую такую строку в два десятка IX-относительных + * пересылок: 16-битные w/h не влезали в регистры, и функция целиком + * уезжала в стековый кадр (пролог `ld iy,#-14 / add iy,sp / ld sp,iy`, + * дальше всё через -N(ix) по 19 тактов). Замер 2026-08-13: до вызова + * ядра блита уходило ~6 700 тактов при том, что работы там — прочитать + * четыре байта заголовка и сравнить границы. + * Байтовый габарит корректен по построению: обе ветки ниже И ТАК требуют + * w < 256 && h < 256 (иначе ядро не примет), а кадры наших атласов не + * крупнее 32x63. Формат .atl допускает больше — такой кадр уходит на + * общий путь выше. */ + if (img[1] | img[3]) { /* кадр больше 255 — редкий путь */ + blit_b_oversize(img, x, ybottom); + gfx_w0_unmap(); + return; + } + w = img[0]; + h = img[2]; top = ybottom - (int)h + 1 + POP_YOFF; /* +YOFF: центрирование */ /* Переворот применяем ЗДЕСЬ, до клипа: дальше вся геометрия (окно * fore-слоя, полоса у потолка, отсев по экрану) считается уже в @@ -406,7 +433,8 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) gfx_w0_unmap(); return; } - if (x >= 0 && top >= 0 && w < 256 && h < 256 && + /* w<256 && h<256 больше не проверяем — гарантировано типом. */ + if (x >= 0 && top >= 0 && x + (int)w <= 320 && top + (int)h <= 256) { pop_dbg_b6(); /* ВРЕМЕННО: пометка «шли в noclip» */ if (pop_upside) gfx_blit_noclip_vflip(x, top, img);