# Уход от эмуляции: что сделано и что дальше (2026-10-06) Исторический план и замеры по этапам разработки. В `0.5-pre2` новая модель остаётся экспериментальной и выключена по умолчанию; для NVIDIA и других не-AMD GPU добавлен модуль памяти без sparse-перепривязок. Актуальное состояние и проверки: [CHANGES_0.5-pre2.md](CHANGES_0.5-pre2.md). ## Где проходит граница API Bloodborne собран со статической libGnm. Пакеты PM4 пишет не драйвер, а сама игра, функциями `sce::Gnm::*CommandBuffer` (`0x1068000–0x10a0000`, `0x11fd000–0x11fe000`). Перехват всех таких функций (`BB_GNM_OBSERVE=1`) показал: 100 % отправляемых пакетов пишут известные вызовы API, всего 99 методов. Gnm — API, который пишет регистры: `setDepthStencilControl` = один `SET_CONTEXT_REG`. Поэтому разбор PM4 от этих 99 функций и трансляция самих вызовов дают одно и то же; PM4 здесь — формат записи вызовов, как командный список. Он уже разбирается параллельно (DrawPreparation). Эмуляция железа — не разбор пакетов, а то, что делается вместо GPU: | Что | Состояние | |---|---| | Модель памяти 0.2 (копии в VRAM, защита страниц, обратное чтение, ранние метки) | Запасной вариант. Модель ПК — по умолчанию, для NVIDIA режим «кусок на выделение». Удалить код 0.2 после проверки на NVIDIA. | | Метки EOP/EOS | Модель ПК: пишутся после завершения работы GPU (честно). | | `WaitRegMem` на метки той же очереди | Транслируется как выполненное ожидание (порядок очереди); ждать CPU не нужно. | | `WRITE_DATA`/DMA ждали все копии хоста | Ждут только пересекающиеся копии (как командный процессор, который не ждёт шейдеров). | | Constant Engine (CE RAM, дампы, счётчики) | Выполняется на CPU в порядке потока: наш разборщик читает описатели из памяти. | | SRT walker (код шейдера GCN, исполняемый на CPU) | Нужен по той же причине. | | Учёт записей GPU для разборщика (`PendingWriteOverlaps`, `IsRegionGpuModified`) | ~15 % потока GpuComm, та же причина. | | Кэш текстур по адресу + хеш | Та же причина плюс отсутствие объектов «текстура» в PM4. | Почти вся оставшаяся эмуляция существует потому, что **транслятор читает описатели ресурсов (T#/V#/S#) из памяти игры на CPU во время разбора**. Драйвер ПК и DXVK так не делают: память приложения читает GPU во время выполнения. ## Дальше, по порядку 1. **NVIDIA.** Тестовая сборка `dist/Bloodborne-bbport-x86_64-test-1006.AppImage` (модель ПК по умолчанию, режим «кусок на выделение»). Если работает — удалить модель 0.2 целиком (`page_manager`, `fault_manager`, обратное чтение, BB_UFFD, предзагрузка, ранние метки). 2. **Распараллелить стадию B** (запись отрисовок; на 4 ядрах занята на 99 %, ~3,3 мкс на отрисовку, равномерно по привязке буферов/текстур, началу прохода, вершинам). Варианты: (а) несколько потоков стадии B по диапазонам отрисовок внутри прохода рендеринга (у каждого свой сегмент команд; общие кэши уже под блокировками; порядок барьеров — по проходам); (б) разрешение привязок (буферы/текстуры → Vulkan) на стадии A, где 28 % простоя, а B только записывает. Нужны прогулочные тесты. 3. **Описатели на GPU (главный шаг к «как DXVK»).** Шейдер читает T#/V#/S# сам (буферы на месте и BDA уже есть); текстуры — bindless-массив, T# → индекс через таблицу на GPU, которую ведёт кэш текстур при создании образов. Тогда не нужны SRT walker, CE на CPU, учёт ожидающих записей; `WRITE_DATA`/DMA/дампы CE становятся записями GPU в порядке потока. Большой проект. 4. **Текстуры как объекты.** Перехват `Gnm::Texture::init*` и загрузчика TPF (`CSGXTextureCreateFragment`): образ создаётся при загрузке, а не угадывается по адресу; хеши и наложения адресов уходят. Ассеты можно заранее перевести в линейный вид (детайлинг один раз при установке). 5. **Шейдеры заранее.** Все шейдеры GCN из файлов игры → SPIR-V при установке; без компиляции во время игры. Что остаётся трансляцией, как у DXVK: шейдеры GCN → SPIR-V, описатели и форматы → Vulkan. ## Шаг 1 (2026-10-07): движок констант больше не синхронизируется с записью отрисовок Замеры (`BB_CE_STATS=1`, Яр'гул): ~14 000 `DumpConstRam` в секунду, ~4–5 МБ/с, всего 25 страниц (кольцо ~100 КБ). Каждый дамп читается ровно одним указателем из user data — это таблицы описателей для SRT walker. Константы, вершины, индексы, большие буферы из этих страниц не читаются; шейдеров, читающих память самостоятельно (`uses_dma`), нет. То есть дампы CE нужны только транслятору — это построение таблиц описателей, а не данные для GPU. Ожидания стадии A на дампах (`WaitForPendingReads`) были нужны только потому, что стадия B для отрисовок без подготовки сама перечитывала таблицы V# (описатели вершинных буферов) — к тому времени перезаписанные следующим дампом (~15 % V# оказывались чужими: баг DoF, issue #44). Сделано: - стадия A снимает V# в пакет отрисовки, стадия B берёт их оттуда и таблицы не читает; - источник DMA отмечается точным диапазоном, а не «читает неизвестно что». Итог: ожиданий на дампах ~850/с → ~46/с, стадия A ждёт 1,5 % времени вместо ~30 % (16 потоков). На 4 ядрах / 8 потоках FPS тот же (~170): там узкое место — стадия B (3,4 мкс на пакет; больше всего — `ObtainBuffer` 0,57 мкс: сопоставление адресов памяти игры с буферами GPU). Записи командного процессора (`BB_CE_STATS`): `WRITE_DATA` ~8 400/с в 2 страницы (читаются как константы ~240/с), DMA ~1 000/с в 15 страниц. Они пишут данные для GPU, и их перевод — команды GPU в порядке потока (`vkCmdUpdateBuffer`, у DMA для памяти на месте уже так). Для `WRITE_DATA` это упирается в правило переноса часто записываемых GPU блоков в VRAM — делать вместе с упрощением модели памяти. ## Шаг 2 (2026-10-07): синхронизации стадии B Стадия B (запись отрисовок) — узкое место на 4 ядрах (занята 99 %). Задачи кадра на ней: - сигнал flip = подготовка кадра презентером; 600 тыс. тактов на кадр, из них работы ~40 тыс., остальное — ожидание потоков записи команд (`CommandBuffer()`) и синхронная отправка. Теперь ~50 тыс.: презентер записывается через `Record`, отправка асинхронная; - проходы апскейлера пишут команды напрямую — для них отдельный сегмент без ожидания; - DMA (~900/с, 4 байта) ждали копии и сигналы даже когда запись делает GPU — ждут только для записей CPU. Что осталось на стадии B (мкс на пакет, 3,5 всего): привязка ресурсов 1,0 (буферы 0,4, текстуры 0,45), `ObtainBuffer` 0,6, начало прохода 0,26, вершины 0,34, задачи 0,2. Дальше — разделение стадии B или привязка ресурсов на стадии A (см. п. 2 выше). ## Шаг 3 (2026-10-08): метки пишет видеочип, перехваты без сигналов, NVIDIA Что игра реально отправляет (новый счётчик `BB_DCB_STATS=1`: пакеты PM4 по типам, события, метки, исходы `WAIT_REG_MEM`; Кошмар охотника, у лампы, ~185 FPS): `EVENT_WRITE_EOP` 61 тыс./с (~330 за кадр, все без прерывания, 32- и 64-битные метки), `WAIT_REG_MEM` 31 тыс./с (все по памяти и все выполнены по меткам, ожидающим в порядке потока — процессор не ждёт ни разу), `EVENT_WRITE_EOS` 371/с, `WRITE_DATA` 3,5 тыс./с, `DUMP_CONST_RAM` 6,6 тыс./с, сбросы кэшей (`EVENT_WRITE`, `ACQUIRE_MEM`) — около 70 тыс./с. Не встречаются: occlusion-запросы (`ZPASS_DONE`), `SET_PREDICATION`, `COND_EXEC`, `MEM_SEMAPHORE`, `REWIND`, `COPY_DATA`, метки со временем GPU, отправки в очереди compute. Сделано: - **Метки EOP/EOS пишет видеочип** (`Rasterizer::WriteLabelOnGpu`, `BB_GPU_LABELS=0` — как раньше): буферный маркер Vulkan (`VK_AMD_buffer_marker`, конец конвейера) прямо в память игры (кусок гостевой памяти), в той же точке потока, где метку пишет командный процессор PS4. 64-битная метка — две 32-битные записи, старшая половина первой. Раньше метку писал поток процессора после завершения всей отправки Vulkan (61 тыс. записей в секунду). Метки с прерыванием по-прежнему сигналятся с процессора (прерывание — оттуда). То же для меток `RELEASE_MEM` очередей compute. EOS с записью из GDS — копия видеочипом в порядке потока вместо `Finish()` и чтения процессором. - **Перехваты кода игры без ловушек** (`gpu/shim/bbport_guest_hooks.cpp`, `BB_GUEST_HOOK_TRAPS=1` — ловушки `int3`): точки загрузчиков, аллокатора диапазонов и кучи GPU переходят в заглушки рядом с образом, которые вызывают обработчик и выполняют перекрытые инструкции; вызовы `memcpy` загрузчиков идут через трамплин. Раньше каждый вызов был сигналом ядра (SIGTRAP), ~1200 в секунду только у аллокатора. - **Новая модель на NVIDIA** (`gpu/shim/bbport_guest_memory.cpp`): кроме кусков dma-buf (AMD) — режим «память хоста, импортированная видеочипом» (`VK_EXT_external_memory_host`): кусок остаётся в memfd рантайма, видеочип импортирует его второе отображение. При запуске самопроверка в точности как в работе: импорт memfd, привязка к sparse-буферу, запись видеочипом и чтение процессором через другое отображение, и обратно. Не прошла — dma-buf (у NVIDIA «кусок на выделение»), для которого теперь меряется скорость чтения процессором (некэшируемое отображение замедлило бы код игры; меньше 1 ГБ/с — не используется). Не прошло и это — старая модель. Выбор: `BB_GUEST_MEMORY=host|dmabuf`. Переключатель в лаунчере доступен на AMD и NVIDIA. На AMD (RADV) импорт memfd драйвер не принимает — там dma-buf, как раньше. На NVIDIA не проверено: в журнале видно, какой путь выбран и почему. Замеры (у лампы): 16 потоков — 233 FPS и с метками видеочипа, и без; 4 ядра / 8 потоков — ~217 против ~215 FPS (в пределах шума; выигрыш — свободный поток сигналов). ### Что ещё осталось и почему Всё, что осталось от эмуляции, держится на одном: **транслятор читает описатели ресурсов (V#/T#/S#) и таблицы с ними из памяти игры во время разбора**. Из-за этого: - `DUMP_CONST_RAM` выполняет процессор (дампы читает только транслятор — это его таблицы описателей, видеочип их не читает ни разу); - DMA и `WRITE_DATA` в память не «на месте» выполняет процессор (туда приходят код шейдеров и описатели, которые транслятор читает сразу); - SRT walker (код шейдера, переведённый в x86) обходит таблицы на процессоре; - шейдер специализируется по полям описателей (шаг, формат, перестановка каналов, тип изображения), конвейер выбирается по регистрам; - текстуры узнаются по адресу и формату из T#. Перенос на видеочип — новый режим рекомпилятора: шейдер сам читает V# (адрес через BDA-арену, формат и шаг — во время выполнения), изображения через bindless-массив с таблицей T# → индекс, барьеры из сбросов кэшей самой игры (`ACQUIRE_MEM`/`EVENT_WRITE`), резидентность всей памяти сразу при отображении. Это несколько дней работы с риском для картинки: не начато. Остальное — не эмуляция в смысле «вместо видеочипа»: перевод GCN → SPIR-V во время игры (как у DXVK), синтетический vblank (HLE VideoOut), заглушки для неиспользуемых игрой пакетов (occlusion — фиктивный счётчик, `SET_PREDICATION`, `COND_EXEC`, `MEM_SEMAPHORE`). ## Шаг 4 (2026-10-08): где проходит граница между эмуляцией и трансляцией Замер (`BB_RES_STATS=1`, Кошмар охотника у лампы): ~97 тыс. привязок ресурсов в секунду; почти у каждой (0,96 стадии на привязку) описатели лежат в таблицах в памяти, а не в регистрах: на одну привязку буферы — 1,3 из регистров и 2,8 из памяти, текстуры — 0,6 и 3,3, сэмплеры — 1,0 и 0,9. **Описатели на видеочипе для текстур сделать правильно нельзя.** Текстура PS4 лежит в памяти в раскладке PS4 (tiling), которой нет в Vulkan: её нужно перевести в образ Vulkan, а для этого процессор должен знать T# (адрес, формат, размеры) до того, как отрисовка выполнится. Если шейдер будет сам читать T# из памяти и искать образ в таблице на видеочипе, то при первом использовании текстуры образа ещё нет — правильного запасного пути у видеочипа нет (только кадр с неверной текстурой). Поэтому чтение описателей процессором (SRT walker, таблицы, которые заполняет движок констант) — не эмуляция, а трансляция привязки ресурсов, как у DXVK и vkd3d, которые тоже читают привязки на процессоре и создают по ним объекты Vulkan. Для буферов описатели на видеочипе возможны (BDA), но выигрыш — только запись дескрипторов (~0,1 мкс на пакет), если не переделать резидентность и барьеры целиком; не начато. Что из эмуляции на самом деле осталось, и используется ли это Bloodborne: | Что | Bloodborne | |---|---| | Ожидание флипа (метка VideoOut) блокирует поток разбора | да — это синхронизация с дисплеем, как ожидание в Present у DXVK | | Счётчик occlusion-запросов — фиктивный | **да**, ~60 событий `ZPASS_DONE` за кадр (в счётчике пакетов их не было видно: они во вложенных буферах); переведено — «Шаг 5» | | `SET_PREDICATION`, `COND_EXEC`, `COPY_DATA` не транслируются | нет; переведено — «Шаг 5» | | `MEM_SEMAPHORE`, `REWIND` — ожидание на процессоре | нет | | Очереди compute (ASC) — корутины в одном потоке | нет (0 отправок за кадр) | | Запись процессором в память, видимую видеочипу, «объявляется» (обёртки `memcpy`, перехваты загрузчиков, кучи, аллокатора, чтение файлов) | да — для копий в VRAM и текстур; `BB_WRITE_VERIFY` на маршруте не нашёл необъявленных записей, кроме ожидаемых | | Шейдер копирования игры — подмена (список читается процессором) | да; теперь можно транслировать (`BB_COPY_SHADER_NATIVE=1`) | Строки «нет» нужны только при переносе на другие игры (Driveclub, inFAMOUS: там асинхронный compute и occlusion есть) — проверить их на Bloodborne нельзя. Шейдер копирования (`cs fefebf9f`): подмену можно заменить трансляцией самого шейдера. Она нашла настоящую опасность новой модели: рядом с тем, куда он копирует, лежат код шейдеров и таблицы описателей, которые транслятор читает процессором; если эти блоки уходят в VRAM («видеочип пишет это 3 кадра из 60»), транслятор читает устаревший код (чёрная сцена). Подмена обходила это случайно. Теперь записи этого шейдера всегда остаются в памяти игры. Общее правило «то, что видеочип пишет, но сам не читает, остаётся в памяти игры» (как readback heap) пробовалось: ломает производительность (видеочип 4,2 → 6,1 мс за кадр — так попадают в оперативную память и области размером с цели рендера), убрано. Константы: копирование в кольцо при разборе было нужно, пока записи командного процессора выполнялись процессором «наперёд». В новой модели оно больше не нужно для правильности — `BB_CONSTANTS_IN_PLACE=1` читает их на месте; по умолчанию выключено (видеочип +3 % из-за PCIe, FPS не выше). То есть кольцо констант теперь — оптимизация (копия в VRAM), а не эмуляция. Дальше по трансляции для Bloodborne делать почти нечего; дальнейшая работа — производительность (стадия B на слабых процессорах) и перенос: настоящие запросы occlusion (`vkCmdBeginQuery` + копия результата видеочипом в память игры), предикация (`VK_EXT_conditional_rendering`), очереди compute как очередь Vulkan с семафорами — когда появится игра, которая их использует. ## Шаг 5 (2026-10-08): occlusion-запросы, COPY_DATA, COND_EXEC, предикация Поправка к шагам 3 и 4: Bloodborne **использует** occlusion-запросы — около 60 событий `ZPASS_DONE` за кадр (~14 тыс. в секунду у лампы в Кошмаре охотника). Счётчик пакетов их не показал: он смотрел только буферы команд верхнего уровня. До сих пор игра получала фиктивную последовательность «всё видно». Сделано (только новая модель; `BB_OCCLUSION_QUERIES=0` — как раньше, бит 4 второго числа `BB_TOGGLE_FILE` — переключение на ходу): - **Occlusion-запросы → запросы Vulkan** (`vk_occlusion.{h,cpp}`, `occlusion_resolve.comp`). На PS4 каждый блок глубины ведёт счётчик сэмплов, прошедших тест глубины; событие `ZPASS_DONE` записывает счётчики (16 байт между блоками, бит готовности 63), игра берёт разность «после − до». Здесь с первого события всегда активен запрос Vulkan; он режется на отрезки на каждом событии и на каждой границе, через которую запрос нельзя протянуть (начало и конец прохода рендера, буфер команд — `Scheduler::CarriedScope`, Suspend/Resume). Событие запоминает, сколько отрезков закончилось до него. В ближайшей точке вне прохода результаты отрезков копируются, и один запуск шейдера складывает их по порядку и пишет каждому ожидающему событию его накопленное значение в память игры через адрес видеочипа (BDA). Проходы рендера не разрываются; цена на 4 ядрах — в пределах шума (220 против 219 FPS с фиктивным счётчиком, видеочип 4,54 против 4,56 мс). Результаты появляются чуть позже, чем на PS4, до этого бит готовности снят — игра проверяет его, как на PS4. Счётчики всегда остаются в памяти игры (их читает процессор). - **COPY_DATA**: значения, известные при разборе (константа, регистр, часы), записывает видеочип в порядке потока, как `WRITE_DATA`; копии из памяти и GDS — копии видеочипа в порядке потока, как `DMA_DATA`. Запись в регистр — пока предупреждение. - **COND_EXEC**: условие берётся из ожидающей метки в порядке потока, иначе — после того, как видеочип выполнил работу до пакета (раньше читалось при разборе, впереди видеочипа). - **SET_PREDICATION** (ZPass, Bool64, Bool32; CONTINUE — ИЛИ с предыдущим): пропускаются отрисовки и запуски, у которых в заголовке стоит бит предиката. Предикат вычисляется при первом таком пакете, после того как видеочип выполнил работу до него (правильно, но с ожиданием видеочипа; Bloodborne предикацию не использует). `VK_EXT_conditional_rendering` не подошёл: он пропускал бы и наши внутренние запуски (детайлинг текстур, копии), и не переходит через границы буферов команд. Проверка: `BB_PM4_SELFTEST=1` — через ~40 с игры в отправки игры подмешиваются тестовые пакеты (в память, выделенную самой проверкой), через несколько секунд результаты сверяются: occlusion по отправке с отрисовками (> 0 сэмплов) и пустой интервал (0), `COPY_DATA` (32 и 64 бита, память → память в порядке, часы), `COND_EXEC` (ложь пропускает запись, истина выполняет), предикация (ZPass и Bool32: 2 из 4 пустых предикатных отрисовок пропущены). Все 8 — PASS. Слой валидации Vulkan на 67 тыс. событий не нашёл нарушений правил запросов. Попутно найдена и исправлена взаимоблокировка: чтение памяти для процессора из потока разбора (`ReadMemory`) отправляло задачу этому же потоку и ждало её. Что осталось без перевода: очереди compute (ASC) по-прежнему выполняются в том же потоке, что и графика, и отправляются в ту же очередь Vulkan (это допустимая сериализация); `MEM_SEMAPHORE` меняет счётчик процессором при разборе; время видеочипа в метках и `COPY_DATA` — часы процессора. Bloodborne ничего из этого не использует. ## Дополнение 2026-10-09: copy shader в модуле памяти и GPU-метки без AMD-расширения В `BB_PC_MODEL=1 BB_LAYER_MEMORY=1` оригинальный `fefebf9f` теперь исполняется по умолчанию: paged stores обновляют память игры и актуальные зеркала. HLE доступен через `BB_COPY_SHADER_NATIVE=0`. Без `VK_AMD_buffer_marker` метки EOP/EOS и применимый RELEASE_MEM пишет GPU стандартными Vulkan updates с барьерами. AMD сохраняет быстрые markers; `BB_GPU_LABELS_PORTABLE=1` позволяет проверить переносимый вариант. Подробности и аппаратные ограничения: [TRANSLATION_GPU_COPY_LABELS.ru.md](TRANSLATION_GPU_COPY_LABELS.ru.md). ## План от 2026-10-10: что осталось и как перевести на трансляцию Требование: одинаково для AMD и NVIDIA и без опоры на код именно Bloodborne (адреса функций игры, хеши её шейдеров) там, где модель трансляции отвечает за правильность. | Что осталось | Где | Почему это эмуляция или привязка | |---|---|---| | Модель памяти 0.3 (копии в VRAM с защитой страниц, обратное чтение, `fault_manager`, BB_UFFD, предзагрузка) | **по умолчанию** | делает вместо видеочипа то, чего у ПК нет: одна память | | Два пути новой модели: sparse-арена (AMD) и модуль памяти (остальные) | новая модель | sparse-перепривязки — путь одного производителя (на NVIDIA держат видеочип секундами) | | Записи загрузчиков в текстуры и буферы видны видеочипу через перехваты кода Bloodborne (`memcpy` игры в 0x216d420/0x2171d60, аллокатор 0x26aa070, куча TLSF) | новая модель | в другой игре этих адресов нет: записи остались бы незамеченными | | `MEM_SEMAPHORE` меняется при разборе (впереди видеочипа), время видеочипа в метках и `COPY_DATA` — часы процессора, очереди compute — в одном потоке с графикой | командный процессор | Bloodborne не использует; нужно для других игр | | CE и SRT walker читают описатели на процессоре, кэш текстур по адресу, синтетический vblank | — | трансляция привязок и платформенный слой, как у DXVK/vkd3d (шаг 4): остаётся | Этапы: 1. **Один путь памяти для всех GPU.** Модуль памяти — по умолчанию и на AMD (на маршруте 2026-10-09 вровень со sparse-ареной: p99 5,1–7,5 мс против 5,2–6,0). Sparse-арена — запасной вариант `BB_LAYER_MEMORY=0`. Заодно на AMD по умолчанию исполняется сам шейдер копирования игры вместо подмены по его хешу. 2. **Согласование памяти без перехватов игры.** Записи процессора в память, которую видеочип держит как образ или копию, ловятся общим механизмом (ловушки записи по 64 КиБ, как у модуля), перехваты Bloodborne остаются ускорением (меньше срабатываний ловушек). Проверка: прогон с `BB_GUEST_HOOKS=0` и `BB_WRITE_VERIFY=1` (необъявленные записи), картинка и время кадра. 3. **Остатки командного процессора — на видеочип.** `MEM_SEMAPHORE`: сигнал — запись видеочипа в порядке потока; время видеочипа — метки времени Vulkan, переведённые в часы PS4 (100 МГц) и записанные видеочипом. Проверка — `BB_PM4_SELFTEST` (Bloodborne их не использует). Очереди compute как отдельная очередь Vulkan — после, когда будет игра для проверки. 4. **Новая модель по умолчанию** (после логов с NVIDIA на 0.5-pre4), затем удаление модели 0.3 и sparse-арены. ### Ход работ (2026-10-10) - **Этап 1 сделан:** модуль памяти по умолчанию на всех GPU (на AMD тоже; sparse-арена — `BB_LAYER_MEMORY=0`), 0 вызовов `vkQueueBindSparse`. - **Модульность:** профили игр `gpu/games/` (`game_profile.h`, `bloodborne.cpp`): перехваты кода игры, её код, пишущий данные каждый кадр, хеш шейдера копирования — только там. Профиль выбирается по TITLE_ID; `BB_GAME_PROFILE=none` — игра как незнакомая, `BB_GUEST_HOOKS=0` — без перехватов. - **Этап 2, запись:** общий учёт ловушек записи в рантайме (`runtime_memory_trap`, причины по страницам; `gpu/layer/bblayer_write_traps.h`): копии модуля в VRAM и текстуры ловят записи на одних страницах, не мешая друг другу; `mprotect` игры ловушки сохраняет, новое отображение начинается без них (тест в `tests/test_runtime.c`). Страницы под текстурами под ловушками по умолчанию (`BB_IMAGE_TRAPS=0` — как раньше), записи libc и перехваченных загрузчиков объявляются до записи (без срабатываний). На маршруте с профилем — как без ловушек (4,18 мс видеочипа), 4–29 срабатываний за 5 с. - **Найдено попутно — разброс «некоторые запуски в 1,3–1,7 раза медленнее»:** «летучие» блоки модуля (блоки, в которые пишет процессор, в общей копии диапазона в VRAM) обновлялись копией перед каждой привязкой на чтение; копия разрывает проход рендера, и объёмы света (по отрисовке на каждый) рвали свой проход до ~100 раз за кадр (`BB_PASS_BREAK_TRACE=1`: 92–109 тыс. разрывов за 5 с из `Runtime::CopyBuffer` против ~700). Летучие блоки выключены по умолчанию (`BB_LAYER_VOLATILE=1` — включить; тогда чтения внутри открытого прохода идут на месте): 10 из 10 прогонов быстрые. Порядок «результаты occlusion до последующих меток» (`OcclusionQueries::WriteBeforeLabel`) оставлен как поведение PS4, но причиной не был: игра читает результаты через 2–10 мс и находит их записанными (`BB_OCCLUSION_READ_TRACE=1`, ловушки на чтение в рантайме — причины от 16 делают страницу без доступа). - **Незнакомая игра (этап 2, чтение):** прежний вывод «без профиля — снова медленный режим» был тем же разбросом из-за летучих блоков (один прогон). Без профиля маршрут идёт так же быстро (3 из 3, 4,29–4,30 мс) и выглядит так же. Ловушки на чтение для данных, которые есть только в VRAM (копия обратно при чтении процессором), остаются нужны для правильности в других играх; механизм причин «без доступа» в рантайме уже есть и проверен тестом. - **Этап 3 сделан (кроме отдельной очереди compute):** - `MEM_SEMAPHORE`: сигнал — в порядке потока, после завершения работы видеочипа перед ним (`RunMemSemaphoreSignal`, как метки EOP); ожидание сбрасывает конвейер записи и отправляет записанное, как `WAIT_REG_MEM`. `BB_MEM_SEMAPHORE_ORDER=0` — сигнал при разборе (как раньше). - Часы видеочипа: у PS4 100 МГц; теперь одна шкала для процессора и видеочипа — монотонные часы хоста в тиках по 10 нс (`GetGpuClock64`, раньше — настенные наносекунды). Метки EOP/RELEASE_MEM с часами и `COPY_DATA` из часов пишет сам видеочип: метка времени Vulkan → копия из запроса → короткий шейдер (`gpu_timestamp.comp`) переводит в шкалу PS4 и пишет в память игры (`vk_timestamps.cpp`). Привязка тиков к часам хоста — один раз, на простаивающем видеочипе (на RX 7800 XT точность 152 мкс). `BB_GPU_TIMESTAMPS=0` — пишет процессор. - Проверка — `BB_PM4_SELFTEST=1`: 11 из 11; новые проверки «сигнал не раньше предшествующей метки» (со старым порядком — FAIL), «ожидание и списание», «часы 100 МГц в шкале процессора» (2 с между метками: 1,961–1,999 с). Bloodborne ни то, ни другое не использует. - **Этап 2, чтение — готово, включается `BB_LAYER_READ_TRAPS=1`:** то, что видеочип записал только в копию в VRAM (запись привязки в зеркало), под ловушкой «без доступа» (причина `VramData` в рантайме); первое касание процессора (игра, транслятор, libc рантайма) копирует данные блока назад через очередь обратного чтения (`FastReadback`, как в модели 0.3: ждёт только тик этой записи) и снимает ловушку. Запись в файл из такой страницы сначала её читает (`touch_for_read`), чтение файла в неё — уже касалось (`touch_for_write`). На маршруте: 60 МиБ под ловушками, 0–1 касание за 10 с, время видеочипа то же (4,28–4,30 мс). Bloodborne такие данные процессором не читает, поэтому само копирование назад здесь не проверено — по умолчанию выключено до игры, которой это нужно. - **Пересмотрено перед 0.5 (вечер 2026-10-10):** на AMD по умолчанию снова sparse-арена (`BB_LAYER_MEMORY=1` — модуль). Модуль шёл вровень с ареной на маршруте в Ярнаме, но в Сне охотника держал вне VRAM ~1 ГБ данных области (диапазоны с несколькими «неизвестными» блоками без летучих блоков остаются на месте): 100 FPS против 154 с ареной; с летучими блоками — 120, но с ~225 тыс. срабатываний ловушек в секунду. Нужен доступ по блокам для смешанных диапазонов. Occlusion-запросы как запросы Vulkan — по выбору (`BB_OCCLUSION_QUERIES=1`): с апскейлером (сцена меньше и со сдвигом) свечение лампы вспыхивало серым на весь кадр; счёт нужно сделать устойчивым к этому. Фоновая компиляция больше не пропускает полноэкранные проходы (серые кадры).