Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
31 KiB
roomtest — план v2: размер кода и раскладка по окнам/банкам/страницам
Статус: план для отдельной сессии, составлен 2026-07-29 по свежему замеру.
Заменяет size_optimization_plan.md (v1, 2026-07-21): часть его пунктов уже
сделана, часть опиралась на неверную модель банкинга. Документ самодостаточный
— рассчитан на старт с пустого контекста.
Повод: перед стражами и боёвкой (новый код ~5–8 КБ) надо понять, куда он поместится, и заранее развести код так, чтобы банкованные модули не упёрлись в ограничения окна W3.
СТАТУС ВЫПОЛНЕНИЯ (обновлено 2026-07-29, коммит ecf5ecf)
| Шаг | Статус | Факт |
|---|---|---|
1. kid_data.h → EMM-страница + load_frame/cur_frame |
сделан | _CODE −3 247 Б; попутно найден и обойдён баг кодогенерации SDCC (см. ниже) |
2. pop_geom.c (дедуп геометрии + PRNG) |
сделан | −41 Б _CODE, −11 Б W3; ценность — не байты, а bank-safe слой |
3. pop_map без вызовов графики |
сделан (фазы 1a/1b) | через пометки перерисовки, см. ниже |
| 4. Разгрузка/перебалансировка W3 | сделан | в резиденте остался ТОЛЬКО pop_bg; W3 14 376 → 11 632 (свободно 4 752 Б) |
5. Данные (room_modif, dl1/dl2, _kbdraw_down) |
не начат | потенциал ~1.5 КБ |
| 6. Контракт банка стражей + пробник | пробник сделан | tests/w3bankgfx — модель подтверждена в MAME, см. ниже |
7. Дедуп семейства draw_tile |
отложен по решению пользователя | «мороки много, выгода не так велика» |
Замер сейчас против замера §1: _CODE 24 881 → 24 421, куча W2 2 076 → 2 592 Б,
W3-резидент 14 376 → 11 632 (свободно 2 008 → 4 752 Б). Сумма кода упала
на ~3.2 КБ (данные Kid уехали в EMM), остальное — перераспределение.
Что сделано вместо §5.3 (вынос loose в W3)
Вместо переноса кода между окнами выбран (по обсуждению с пользователем)
порт архитектуры оригинала: логика ставит пометку, отрисовка идёт
отдельным проходом — set_redraw_* (seg007) + redraw_needed (seg008:0178).
Появился pop_redraw.c/.h; pop_trob и pop_map больше не рисуют тайлы.
Наши самодельные счётчики (spike_rest, button_rest, ldoor_rest,
loose_bake, loose_rest, ceil_rest, ceil_bake, land_bake) удалены —
их роль (вторая страница дабл-буфера) взял счётчик страниц в пометке.
Два исключения остались (обе — функции ТОЛЬКО главного цикла, звать из банка нельзя):
pop_process_trobs— пламя факела и пузырёк зелья (покадровый оверлей);pop_loose_tick— падающий кусок (mob): spawn/tick/pos. В оригинале это отдельная подсистема (mobs+draw_moving), разделение на логику и отрисовку — задел следующей фазы.
Пробник банка (2026-07-29): модель ПОДТВЕРЖДЕНА
tests/w3bankgfx (huge + --w3 res.c + --bank 1=bank1.c, графика 256):
- банк рисует примитивом libbgi НАПРЯМУЮ — работает; страница W3 внутри
банка до блита, после блита и после возврата из вызванной им W1/W2-функции
одна и та же (0xF0), резидент — 0xF3. То есть
_bgi_begin/_bgi_endкорректно возвращают ИМЕННО банковую страницу (правило R4); - вызов W1/W2-функции из банка работает, и она тоже может рисовать;
- резидент W3 жив и вызывается после возврата из банка (R3).
Дополнительно выяснено (важно для стражей): писучие статики
__banked-модуля линкуются В СТРАНИЦУ БАНКА (0x1C000+) — снаружи их не
прочитать, из W1/W2 по 0xC000 видна резидентная страница. Значит всё
состояние банкованного кода (позиции стражей, таймеры боя) обязано жить в
W1/W2 как обычные глобалы, а банк — только код.
Ещё одна мина, найденная там же: инлайновый in a,(#0xE2) посреди тела
функции затирает A, куда SDCC уже положил параметр (у нас из-за этого цвет
заливки стал номером страницы, и «резидент не рисовал»). Читать порт
отдельной __naked-функцией.
Найденная по дороге ловушка компилятора
(const T *)КОНСТАНТА + var*K SDCC 4.5 может собрать неверно: умножение
делает в 16 битах, а потом берёт только младший байт (ld c,l / inc b).
Кадры Kid с индексом ≥ 52 читались из чужой строки таблицы, у бега/шага
пропадал FRAME_NEEDS_FLOOR и персонаж проваливался сквозь пол. Лечение —
считать адрес в uint16_t и кастовать один раз. Тот же паттерн в
pop_level.c компилируется ПРАВИЛЬНО, т.е. полагаться на «у соседа
работает» нельзя. Подробности: memory sdcc_z80_const_ptr_index_bug.
0. Что уже сделано из v1 (не повторять)
--opt-code-sizeи--max-allocs 100000уже включены по умолчанию вbin/sprinter-cc(v1 §2.1 закрыт, выигрыш получен).- Лишние блиты переднего слоя убраны (v1 §7 п.0):
fore_tileбольше не рисуетbottom_id,_CODE−388 Б. gfx_blit_noclipв libbgi (v1 §8 шаг 1): фоновые блиты в 2.9× дешевле.--w3как резидент окна 3 реализован и обкатан (memoryw3_resident_code).
1. ЗАМЕР (сборка 2026-07-29, коммит 1214785)
Команда: --memory small --gfx 256 --w3 pop_trob.c pop_map.c --w3 pop_bg.c.
1.1 Окна
| Область | Занято | Свободно | Примечание |
|---|---|---|---|
W1+W2 _CODE |
24 881 Б | — | 0x4100…0xA231 |
W1+W2 _HOME+_GSINIT+_DATA+_BSS |
~4 240 Б | — | до 0xB2E4 |
| W1+W2 куча | 0 (никто не malloc'ит) | 2 076 Б | 0xB2E4…0xBB00 |
| W1+W2 стек | — | 1 279 Б | 0xBB00…0xBFFE |
| W3 резидент | 14 376 Б | 2 008 Б | 0xC000…0xF828 |
| EMM-страницы | 37 атласов + 1 уровень | ~215 страниц свободно | sprinter_emm_budget |
Итого запаса до стены: ≈ 4 КБ (2 КБ в W1/W2 + 2 КБ в W3). Стражи туда не влезут.
1.2 Код по модулям (точно, из .rel)
W1/W2 (_CODE 24 881): W3 резидент (_W3CODE 14 376):
pop_kid 6 963 pop_bg 11 643
pop_map 6 157 pop_trob 2 733
roomtest 2 583
pop_level 1 425
pop_ctrl 1 145
crt0 333
libc+libbgi ~6 275
1.3 Крупнейшие функции/данные (из .lst)
pop_bg : draw_tile 3080, other_overlay_tile 1146, wall_pattern 944,
mob_render 720, mob_tick_one 661, overlay_mid_tile 498,
fore_only_tile 409, climb_overlay_tile 391, tile_table 371
pop_map : check_loose_fall_on_kid 674, check_bumped 567, jump_up_or_grab 413,
get_tile 266, do_knock 243, check_press 238, check_leave 212
pop_kid : kid_seqtbl 2310 + kid_frames 1205 + kid_seq_off 230 = 3745 Б ДАННЫХ
(в _CODE!), собственно кода ~3.2 КБ
roomtest : enter_room+main ~2.1 КБ
pop_level: room_bg_ptr 1084 (+ 515 Б таблиц LINKLOC/LINKMAP в _DATA)
1.4 _DATA (3 710 Б)
pop_trob 963 (room_modif[24][30] = 720 + trobs + rest-массивы)
pop_level 515 (копии LINKLOC/LINKMAP уровня)
pop_map 163, roomtest 141, pop_kid 130, pop_bg 115, pop_ctrl 13
libc: _irq_state 818, _kbdraw_state 515, _gfx_pal_buf 256, прочее ~200
1.5 Находки замера (мелкие, но чинить)
--w3берёт ОДИН файл на флаг. ВMakefileнаписано--w3 pop_trob.c pop_map.c --w3 pop_bg.c, и это значит «W3 = pop_trob и pop_bg», аpop_map.cкомпилируется как обычный исходник в W1/W2. Судя по.sprinter-cc-roomtest/w3_pop_map.rel(устаревший артефакт), когда-то pop_map был в W3. Решить осознанно (см. §4) и записать явно:--w3 pop_trob.c --w3 pop_bg.c.libcтянет_irq_state818 Б +_kbdraw_state515 Б в_DATA.__irq_vec_buf(513 Б) — таблица векторов IM2;__kbdraw_down(512 Б) — битмап клавиш на 512 скан-кодов. Оба можно ужать (см. §5.4), это ~0.7 КБ в самом дефицитном окне.
2. ПРАВИЛА ПЛАТФОРМЫ, ОТ КОТОРЫХ ПЛЯШЕТ РАСКЛАДКА
Это главное, что изменилось по сравнению с v1: модель банкинга уточнена по
bin/sprinter-cc (справка --w3/--bank) и по коду libbgi.
(R1) Резидент W3 (--w3) и банки W3 (--bank) делят одно окно.
Резидент лежит на своей странице 0xC000…0xFFFF; трамплин на время вызова
__banked подменяет страницу W3 на банковую и возвращает резидентную назад.
(R2) Из банка резидент W3 НЕДОСТИЖИМ — и транзитивно тоже.
Пока исполняется банк, резидентной страницы в адресном пространстве нет.
Значит нельзя не только bank → pop_bg(), но и bank → pop_map() → pop_bg().
Это ключевое ограничение при выборе, что делать банком.
(R3) Резидент → банк работает (через трамплин в W1), резидент → W1/W2 — тоже.
(R4) Графические примитивы libbgi звать можно откуда угодно.
_bgi_begin читает текущую страницу W3 из порта 0xE2, а _bgi_end её
возвращает — то есть скобка корректна и из банка, и из резидента. Нельзя
только одно: звать _bgi_begin/_bgi_end ИЗ кода, который сам лежит в W3
(после подмены страницы исчезнет исполняемый код — проверено, белый экран).
Поэтому pop_bg (резидент W3) обязан пользоваться готовыми примитивами
(gfx_blit*, bar, …), а батчинг скобки на весь draw_tile (v1 §8 шаг 1)
для него невозможен без переноса самого draw_tile в W1/W2.
(R5) --w3 кладёт в W3 код И rodata модуля (--codeseg/--constseg W3CODE), а писучие статики оставляет в _DATA (W2). То есть const-таблицы
переносятся в W3 бесплатно вместе с модулем (так уже лежит tile_table 371 Б).
(R6) Данные в EMM-странице читаются, только пока страница в окне.
gfx_w0_map(page) / gfx_w0_unmap() — окно W0 (0x0000…0x3FFF), первые 0x100
занимает ISR-стаб. Так уже работает pop_level. Цена — пара OUT на
маппинг, поэтому годится для «пачками», а не для чтения по байту в горячем
цикле.
3. ЧТО ДЕЛАТЬ НЕЛЬЗЯ (анти-паттерны, чтобы не потерять время)
- Нельзя банковать
pop_bg. Он вызывается из pop_map, pop_trob, roomtest, pop_kid — то есть из главного цикла на каждом кадре; плюс он сам держитtile_tableи всю отрисовку. Банк дал бы трамплин на каждый блит. - Нельзя банковать
pop_map, покаpop_mapзовётpop_bg(R2). Сейчас зовёт:pop_loose_tickи компания (~30 вызовов графики). - Нельзя тащить
kid_framesв EMM «в лоб»: он читается несколько раз за кадр из коллизии (kid_cur_dx/kid_cur_flags→dx_weight,char_x_forward_edge, …). Нужен кэш кадра (см. §5.1) — иначе маппинг страницы окажется в горячем пути. - Нельзя «причёсывать» семейство
draw_tileради экономии, не имея пиксельного теста. Мы неделю выравнивали слои по SDLPoP; любой рефактор этой зоны проверять диффом страниц (заморозка кадра клавишей1+ сравнение VRAM обеих страниц, приём из memorymame_mcp_bridge).
4. ЦЕЛЕВАЯ РАСКЛАДКА
Принцип: W3-резидент = «толстая графика, которую зовёт только главный цикл»; W1/W2 = ядро, которое должно быть достижимо ОТОВСЮДУ (включая банки); банки = новая холодная логика (стражи, боёвка, будущие уровни).
W1/W2 (всегда отображено) W3 резидент (стр. 0xC000) Банки W3
────────────────────────── ───────────────────────── ─────────
libc + libbgi pop_bg (отрисовка тайлов) guards.c
pop_kid (интерпретатор+рисование) pop_trob (анимации тайлов) fight.c
pop_map (коллизия/физика/предметы) pop_loose.c (loose+потолок) debug/roomnav
pop_geom (общая геометрия/тайлы) enter_room-часть roomtest?
pop_ctrl (ввод/диспетчер)
roomtest (главный цикл)
Почему так:
pop_mapостаётся в W1/W2 — его зовут и главный цикл, и (в будущем) банк стражей; в W3 его класть нельзя именно из-за R2. Для этого из него надо вынести графическую часть (loose/потолок) — она уезжает в W3 кpop_bg(§5.3). После выносаpop_mapстановится чистой логикой без единого вызова графики — тот самый bank-safe API.pop_kidостаётся в W1/W2:play_seq/kid_set_seq/Kidнужны и стражам (у стражей ТА ЖЕ seqtbl), аkid_drawзовёт только libbgi (R4).pop_trobостаётся резидентом: его зовёт только главный цикл, и он сам зовётpop_bg— идеальный житель W3.- Банк стражей не зовёт ничего из W3. Рисование стражей — либо через
libbgi напрямую (R4), либо (лучше) резидентный
guard_draw()в W1/W2 рядом сkid_draw, а банк только считает состояние. Тот же приём мы уже используем дляpop_item_taken/pop_loose_fell/pop_ceil_fell: банк выставляет флаг — резидент рисует.
5. ПЛАН РАБОТ
Порядок выбран так, чтобы каждый шаг был проверяем отдельно и давал место следующему.
Шаг 1. kid_data.h (3 745 Б) → EMM-страница + порт load_frame — самый большой выигрыш
Сейчас kid_seqtbl (2310) + kid_frames (1205) + kid_seq_off (230) лежат в
_CODE окна W1/W2 — это 15 % всего дефицитного пространства.
Как переносить:
pop_extract_kid_data.pyдополнительно пишетkid_data.bin(те же три таблицы подряд, фиксированные смещения).- Грузим её в отдельную EMM-страницу тем же способом, что уровень
(
pop_level_load— готовый образец), хэндл держим вpop_kid. - Порт
load_frame(seg006) и глобалаcur_frame— в оригинале ровно так и сделано: раз за тик кадр копируется в структуру, а весь остальной код читаетcur_frame, а не таблицу. У насkid_cur_dx()/kid_cur_flags()станут чтением изcur_frame(5 байт в_DATA). play_seqоборачивается в одинgfx_w0_map(kid_data_page)…unmapна вызов (в тике, не в отрисовке — конфликта с атласом в W0 нет).
Выигрыш: −3 745 Б из W1/W2, цена — один маппинг страницы за тик и 5 байт
_DATA. Дополнительный бонус: load_frame/cur_frame — шаг К СХОДСТВУ с
оригиналом, а не отход от него.
Риск: сломать play_seq (сердце анимации). Проверка: прогон по комнатам с
эталонными позами (вис, подтягивание, прыжки, подъём меча).
Шаг 2. Модуль pop_geom.c — дедуп + bank-safe фундамент
Сейчас продублировано между модулями:
| что | где | сколько |
|---|---|---|
y_to_row/y_to_row_mod4 |
pop_bg + pop_map | 2 копии |
char_dx_forward |
pop_kid + pop_map | 2 копии |
x_bump[20] |
pop_kid (uint8) + pop_map (int16) | 20 + 40 Б, РАЗНЫЕ типы |
y_land[5] |
pop_kid + pop_map | 10 + 10 Б |
tile_is_floor |
pop_map (+ проверка кодов в roomtest) | 2 места |
32-битный LCG prandom |
pop_bg (prandom) + pop_trob (trob_prandom) |
2 копии по ~60 Б + 2 сида |
Собрать в один W1/W2-модуль pop_geom.c: таблицы x_bump/y_land/dir_front/ dir_behind, y_to_row, char_dx_forward, get_tile_div_mod(_m7),
tile_is_floor, prandom. Выигрыш прямой — сотни байт (оценка 250–400 Б),
но главное — это и есть тот «чистый» API, который потом сможет звать банк
(R2): вся геометрия оказывается в W1/W2 по определению.
Осторожно: prandom у pop_bg и pop_trob — РАЗНЫЕ последовательности с разными
сидами (стены vs фазы факелов). Объединять функцию можно, сиды — нет:
передавать сид указателем/по индексу, иначе поедет раскладка кладки.
Шаг 3. Вынести loose/потолок из pop_map в W3
pop_map — единственный модуль W1/W2, который зовёт графику, и делает это
ровно в одном логическом блоке: pop_loose_tick + check_press + do_knock +
fell_on_your_head + check_loose_fall_on_kid + плита-потолок (~1.4–2 КБ).
Вынести их в pop_loose.c, собираемый --w3 рядом с pop_bg/pop_trob.
Тогда:
pop_map= чистая логика (bank-safe, R2 соблюдён);- W1/W2 худеет ещё на ~1.5–2 КБ;
- W3 растёт на столько же — а место там появится после шага 4.
Шаг 4. Перебалансировка резидента W3
После шага 3 в W3 будет тесно (14.4 + 2 ≈ 16.4 КБ > 16 КБ). Разгружаем:
wall_pattern(944 Б) +mob_render/mob_tick_one(1381 Б) — кандидаты на переезд в W1/W2: их зовёт толькоpop_bg/pop_loose, но сами они уже пользуются только libbgi (R4), значит из W1/W2 работают и остаются достижимыми из банка.tile_tableи мелкие const-таблицы pop_bg (371 + ~300 Б) можно унести в EMM-страницу уровня (там ~13.8 КБ свободно) — но только если чтение происходит под уже замапленной страницей. Сейчасdraw_tileчитаетtile_tableВНЕ W0-контекста → потребуется явный маппинг на тайл. Не делать раньше замера: 30 тайлов на входе в комнату × map/unmap — терпимо, а вот в покадровых редроях (пики/loose/кнопка) — уже горячий путь.- Если и этого мало —
enter_room(~2.1 КБ, зовётся только при смене комнаты) переносится в резидент W3 или в БАНК (он вызывается из главного цикла = резидента, значит банк допустим по R3).
Шаг 5. Данные
room_modif[24][30]= 720 Б (pop_trob,_DATA). Нужен произвольный доступ каждый кадр (анимации, ворота) — в EMM не годится. Но 24 комнаты × 30 байт хранятся ЦЕЛИКОМ, хотя одновременно живут modif'ы только текущей комнаты и соседей по швам. Вариант: хранить полный массив в EMM-странице уровня, а в_DATAдержать кэш на 2–3 комнаты (свою + левого/правого соседа) с записью обратно при смене комнаты. Выигрыш ~600 Б, цена — аккуратность на швах (кнопка в одной комнате открывает ворота в другой). Делать последним — это самая «тонкая» правка по семантике.dl1[256]+dl2[256]= 512 Б (pop_level,_DATA— копии LINKLOC/ LINKMAP уровня) — читаются при нажатии кнопки и при отрисовке нажатой кнопки. Кандидат на чтение прямо из страницы уровня (она и так маппится) — но проверить, чтоpop_doorlink2не зовётся из отрисовки в тот момент, когда в W0 атлас. Выигрыш ~500 Б._kbdraw_down[512]512 Б (libc): проверено — это байт на скан-код (libc/kbd/_kbdraw_state.c), хотя комментарий называет его битовой картой. Упаковка в биты даёт −448 Б, но добавляет сдвиг/маску в ISR-трамплин и вkbd_raw_down. Трогать осторожно: raw-клавиатура уже дважды была источником залипаний (memorykbd_raw_fifo_drain,kbd_overrun_wipe_modifiers) — правку сопровождать прогоном docs/kbd-games.__irq_vec_buf513 Б (libc IM2): таблица векторов обязана быть выровнена и полна — не трогать.
Шаг 6. Контракт банка стражей (проектируется ДО написания кода)
Когда дойдём до стражей:
guards.cсобирается--bank 1=guards.c, режимhuge(илиbigсBANKED=W1, если W3 окажется тесен для трамплинов).- Банк зовёт только:
pop_map(чистая логика после шага 3),pop_kid(play_seq,kid_set_seq,cur_frame),pop_geom, libc/libbgi. - Банк НЕ зовёт:
pop_bg,pop_trob,pop_loose(резидент W3) — ни прямо, ни через промежуточные функции. Нужна отрисовка — выставляет флаг, рисует резидент (идиомаpop_item_taken). - Первым делом — пробник (
tests/или--bankна пустышке): банк зовётpop_map-функцию, та зовёт libbgi-примитив; убедиться в MAME, что скобка W3 корректно возвращает банковую страницу (R4) — это проверка модели, а не веры в неё.
Шаг 7. Мелкий дедуп в pop_bg (после того, как появится тест страниц)
- Пять функций-редроев (
pop_spike_redraw,pop_loose_shake_draw,pop_floor_bake,pop_button_redraw,pop_leveldoor_redraw) отличаются только прямоугольником heal, банком и набором тайлов — свести к одному параметризованному хелперу (оценка −150…250 Б). env_b/wall_b/fore_b/pot_b— четыре одинаковых обёртки надblit_b(оставить: экономия единицы байт, читаемость дороже).overlay_mid_tile/fore_only_tile/climb_overlay_tile/draw_tile— общая структура «взять code/lcode, посчитать x/dmy/dby, разобрать слои». Тут экономия потенциально сотни байт, но это та самая зона риска из §3 — только с пиксельным диффом до/после и по одному слою за раз.
6. Ожидаемый итог
| Шаг | W1/W2 | W3 | Риск |
|---|---|---|---|
| 1. kid_data → EMM + load_frame | −3 745 | — | средний (сердце анимации) |
| 2. pop_geom (дедуп) | −250…400 | — | низкий |
| 3. loose → W3 | −1 500…2 000 | +1 500…2 000 | низкий (перенос как есть) |
| 4. разгрузка W3 (wall_pattern, mob) | +2 300 | −2 300 | низкий |
| 5. данные (room_modif, LINKLOC, kbd) | −1 000…1 600 | — | средний/высокий |
| 7. дедуп редроев pop_bg | — | −150…250 | средний |
Суммарно: W1/W2 освобождается ~4.5–6 КБ, W3 остаётся примерно в нынешнем объёме, но становится «правильно заполненным» — в нём только то, что банк никогда не позовёт. Плюс открывается путь к банкам: стражи и боёвка получают до 16 КБ на банк, не трогая резидент.
7. Как мерить и проверять (обязательно к каждому шагу)
- До/после по
.rel— точные размеры на модуль:for f in .sprinter-cc-roomtest/*.rel; do grep '^A ' $f; done(области_CODE/_W3CODE/_DATA). Итоги окон печатает самsprinter-cc. - Функции — из
.lst(метки_name:и адреса), скрипт в истории этой сессии; полезно ловить «функция распухла после рефактора». - MAME: любой перенос кода между окнами/страницами — это класс «молча
ломается» (
sprinter_memory_modes). Минимум: комната 1 (loose), 12 (вис/подтягивание), 15 (меч), 9 (дверь уровня), 6 (кнопка/ворота). - Пиксельный дифф для правок отрисовки: заморозить кадр (
1), сравнить обе страницы дабл-буфера черезvram(см. memorymame_mcp_bridge) и/или сверить с эталонным рендеромrender_room.py. - Скорость — после шагов 1 и 4 замерить кадр маркерами в порт 0xFE (приём из v1 §8), чтобы маппинг страницы за тик не съел бюджет.
8. Ссылки
bin/sprinter-cc— справка по--w3,--bank,--memory,--memory-manual.runtime/crt0_banked.s,runtime/bank.s— трамплины и захват резидентной страницы W3.libbgi/common/_bgi_begin.c/_bgi_end.c— механика скобки W3 (R4).- memory:
w3_resident_code,pop_banking_architecture,sdcc_banking,bank_local_data_pattern,sprinter_memory_modes,memory_modes_implemented,sprinter_emm_budget,mame_mcp_bridge,avoid_32bit_arith_z80,libc_one_function_per_module. applications/PoP/docs/size_optimization_plan.md— v1 (замер 2026-07-21, раздел §8 про скорость отрисовки актуален и не дублируется здесь).