
Полная версия:
«Vulkan и многопоточность для смертных» или: как заставить работать видеокарту и все 16 ядер, а не себя
• Перемещение через std::exchange обнуляет каждое поле источника. Иначе деструктор источника освободит память, которую теперь считает своей новый владелец. И добро пожаловать в double free.
• Шаг 3. std::span
10.4. Staging: как данные попадают в видеопамять
CPU не может писать в обычную видеопамять. Поэтому данные едут в два прыжка:



Разбор.
• Шаг 4. immediateSubmit — «сделай на GPU и дождись». Для загрузки при старте простота важнее скорости. В главе 23 мы заменим его асинхронным загрузчиком в отдельном потоке. std::function принимает любую лямбду с захватом. Его цена (возможная аллокация) тут ничтожна на фоне ожидания GPU. Fence создаётся без eSignaled: теперь нам нужен честный сигнал от GPU. Порядок уничтожения снова работает на нас: fence объявлен после буфера и умирает первым, но к этому моменту он уже засигнален.
• Шаг 5. Классический staging. Обрати внимание на флаги usage: у staging — eTransferSrc (отсюда копируют), у цели — eTransferDst (сюда копируют) плюс настоящее назначение. Забудешь — валидация скажет всё, что о тебе думает. Лямбда захватывает всё по ссылке ([&]). Это безопасно, потому что она выполняется синхронно, пока staging и result живы. return result; не копирует: сработает NRVO или перемещение.
• В Pepelac появляется поле GpuAllocator allocator; сразу после очередей. Его создаёт createLogicalDevice последней строкой: allocator = GpuAllocator(instance, physicalDevice, device);. Все GpuBuffer объявляются ниже и умрут раньше аллокатора, а аллокатор — раньше устройства.
ЗАПОМНИ. Никогда не выделяй VkDeviceMemory на каждый ресурс: лимит всего около 4096 выделений. Намерения для VMA: «только GPU» = без флагов; «CPU пишет» = HOST_ACCESS_SEQUENTIAL_WRITE; «CPU читает» = HOST_ACCESS_RANDOM; «постоянно отображённая» = + MAPPED.
ГРАБЛИ. Деструктор GpuBuffer освобождает память сразу и не спрашивает, закончил ли GPU. В createDeviceLocalBuffer это безопасно благодаря waitForFences. Но стоит уничтожить буфер посреди работы, пока его читает кадр в полёте, — и получишь use-after-free на GPU. Решение (отложенное удаление) — в главе 22.
ПОТОКИ. VMA по умолчанию потокобезопасен: внутри него мьютекс, и создавать буферы можно из любых потоков. Флаг VMA_ALLOCATOR_CREATE_EXTERNALLY_SYNCHRONIZED_BIT убирает мьютекс для однопоточных приложений, но мы его не трогаем: в части IV аллокации пойдут из потока-загрузчика. А вот immediateSubmit пока однопоточный: он берёт командный пул и графическую очередь, а оба требуют внешней синхронизации.
Шпаргалка главы 10
• Куча = физическая память, тип = способ её использовать. Тип ищем по флагам и memoryTypeBits.
• Ручной путь: buffer → requirements → allocate → bind. Лимит выделений ~4096.
• VMA: один аллокатор, vmaCreateBuffer, VMA_MEMORY_USAGE_AUTO + флаги-намерения.
• Staging: host-visible TransferSrc → copyBuffer → device-local TransferDst.
• vmaCopyMemoryToAllocation делает flush сам. Ручной memcpy — не делает.
• Порядок полей: device → allocator → буферы.
Задачи для разминки мозгов
Задача 10.1. «Типобезопасная загрузка». Напиши шаблон createDeviceLocalBuffer(std::span
Задача 10.2. «ReBAR — и без пересадок». На картах с Resizable BAR и на встроенной графике видеопамять видна CPU, и staging не нужен. Используй флаг VMA_ALLOCATION_CREATE_HOST_ACCESS_ALLOW_TRANSFER_INSTEAD_BIT и функцию vmaGetAllocationMemoryProperties, чтобы во время выполнения решать: копировать напрямую или через staging.
Решения — в приложении A.
Глава 11. Вершинные и индексные буферы
Хардкод вершин в шейдере годится для треугольника и не годится ни для чего больше. В этой главе геометрия переезжает в буферы, а наш треугольник превращается в квадрат из четырёх вершин и шести индексов. Скромно. Но именно так устроена любая модель из миллиона треугольников.
11.1. Как пайплайн читает вершины
Пайплайну нужно знать две вещи:
• Binding — откуда читать: номер буфера, шаг между вершинами (stride) и частота смены (на каждую вершину или на каждый экземпляр).
• Attribute — что читать: номер входа шейдера (location), формат данных и смещение внутри вершины.

Индексный буфер — массив номеров вершин. У квадрата четыре угла, но два треугольника требуют шесть вершин. Индексы позволяют хранить каждую вершину один раз. В настоящей модели одна вершина в среднем входит в шесть треугольников, и GPU кэширует результат вершинного шейдера по индексу. Экономия и памяти, и вычислений.
11.2. Код: вершина и её описание

Разбор.
• Шаг 1. Обычная структура из GLM-векторов. glm::vec2 — два float, 8 байт; glm::vec3 — три float, 12 байт. Выравнивание у обоих 4 байта, поэтому дыр нет и размер ровно 20.
• Шаг 2. stride = sizeof(Vertex) — на сколько байт сдвигаться к следующей вершине. eVertex означает «новая запись на каждую вершину», eInstance — «на каждый экземпляр» (задача 11.1). Функция constexpr: структуры Vulkan-Hpp можно строить на этапе компиляции.
• Шаг 3. location должен совпасть с [[vk::location(N)]] в шейдере. Форматы для вершин называются как форматы картинок, и это сбивает с толку: eR32G32Sfloat — просто «два 32-битных float», никакого красного и зелёного. offsetof(Vertex, color) вычисляет смещение поля в байтах (8). Поменяешь порядок полей — смещение пересчитается само. Двойные фигурные скобки {{ … }} нужны потому, что std::array — структура со встроенным сишным массивом.
Конец ознакомительного фрагмента.
Текст предоставлен ООО «Литрес».
Прочитайте эту книгу целиком, купив полную легальную версию на Литрес.
Безопасно оплатить книгу можно банковской картой Visa, MasterCard, Maestro, со счета мобильного телефона, с платежного терминала, в салоне МТС или Связной, через PayPal, WebMoney, Яндекс.Деньги, QIWI Кошелек, бонусными картами или другим удобным Вам способом.
Вы ознакомились с фрагментом книги.
Для бесплатного чтения открыта только часть текста.
Приобретайте полный текст книги у нашего партнера:
Полная версия книги
Всего 10 форматов

