Infinity World (Дневники разработчицы)
439 subscribers
52 photos
24 videos
37 links
Канал-дневник разработчицы на Unity, рассказываю о всяком интересном и не очень, что встречается на пути разработки.

Тех стэк:
- Unity
- DOTS
Download Telegram
Entities (DOTS) и Wireframe режимы

Когда я только начала использовать DOTS и использовать BatchRendererGroup (BRG), то столкнулась с тем, что посмотреть сетку мешей в окне сцены невозможно. Переключалка в Scene View работает только для GameObject и MeshRenderer, а для сущностей, которых на сцене фактически то и нет, уже не работает. 😭

Учитывая, что вся геометрия у меня процедурно генерируется, посмотреть получившуюся сетку было жизненно необходимо. Я спасалась тем, что был отдельный debug MonoBehaviour, который умел генерировать отдельные чанки и запихивать их в MeshRenderer. Неудобно, скажите вы, и будете абсолютно правы. 😔

Что же делать? Использовать Rendering Debugger! Подробное описание можно найти вот тут. В Unity его можно найти в меню Window -> Analysis -> Rendering Debugger. Там на самом деле множество полезных инструментов для дебага графики, но что меня порадовало в свое время - есть wireframe режимы, которые работают и с сущностями!

Чтобы переключить wireframe режим, надо найти выпадающий список по пути Rendering -> Rendering Debug -> Additional Wireframe Modes в окошке Rendering Debugger. Вдруг кому-нибудь полезно будет ☺️

#dots
🔥194👀4💋2
Демо-сцена. Форма ландшафта.

Продолжаю писать по разработке демо-сцены. В этот раз коснулась моментов генерации ландшафта и разработки его базовой формы. Это не окончательная форма ландшафта, она будет дорабатываться в дальнейшем, так как разработка - итеративный процесс.

Русская версия: вот тут.
English: here.

Приятного чтения! 🥰

#generation #demo_scene
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥104🥰2💋1
Custom properties в DOTS Instancing

Как и в GPU Instancing, в DOTS Instancing есть определенные правила работы с полями, которые должны быть per instance. 🧐

Если рассматривать как в DOTS Instancing устроено хранение данных для каждого инстанса, то стоит обратиться к Batch Renderer Group. А там, фактически, один буфер, где под каждое поле (будь то матрица или просто цвет) выделяется определенный кусок памяти. Дальше, с помощью оффсетов, которые можно вычислить, зная размер данных поля и количество интансов, можно получить доступ к конкретному значению конкретного поля для конкретного инстанса.

В шейдере же, необходимо использовать макросы, как для определения поля, так и для получения значения.

Определение поля:
#ifdef UNITY_DOTS_INSTANCING_ENABLED
UNITY_DOTS_INSTANCING_START(MaterialPropertyMetadata)
UNITY_DOTS_INSTANCED_PROP(float4, _ChunkDebugColor)
UNITY_DOTS_INSTANCING_END(MaterialPropertyMetadata)
#else
int _ChunkDebugColor;
#endif


И получение значения:
float4 debug_color = (float4)0;
#ifdef UNITY_DOTS_INSTANCING_ENABLED
debug_color = UNITY_ACCESS_DOTS_INSTANCED_PROP(float4, _ChunkDebugColor);
#else
debug_color = _ChunkDebugColor;
#endif


Но как же указать значение для инстансов? 🤔
Когда мы работаем с GPU Instancing, то необходимо передать буфер per property, который мы можем как-то заполнить на стороне CPU. В случае же с DOTS Instancing, мы должны работать с буфером BRG и заполнять его. Это немного сложновато, но у меня есть небольшая реализация, которая упрощает этот процесс.

Но также есть и другой способ, если используется пакет Entities. Там мы можем указывать значения через компоненты!
Для этого необходимо создать небольшой компонент и повесить его на сущность, все остальное за нас сделает Unity:

[Serializable]
[MaterialProperty("_ChunkDebugColor")]
public struct DebugChunkColorProperty : IComponentData
{
public float4 Value;

public static implicit operator DebugChunkColorProperty(float4 value)
{
return new DebugChunkColorProperty
{
Value = value
};
}
}


#dots
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥124👏3👍1
This media is not supported in your browser
VIEW IN TELEGRAM
Что такое Biome?

Биом - это правила генерации ландшафта, правила выбора вокселей, правила генерации окружения и многое другое.

Так как воксели генерируются у меня на стороне GPU (использую вычислительные шейдеры), то правила генерации ландшафта и выбор типов вокселей описываются на языке HLSL.

Фактически, это просто математическое уравнение. Довольно большое и сложное, но все же уравнение: на входе позиция в world space и зерно (seed) мира, а на выходе - дистанция для Signed Distance Field и вектор типов вокселей, которые могут быть в указанной точке пространства. Вот и все 👏

#generation
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17🥰3😍21💋1
Демо-сцена. Базовые цвета ландшафта. Часть 1.

На этот раз затрагиваю тему материалов вокселей и базовых цветов ландшафта для демо сцены.

Это первая часть статьи, так как для продолжения необходимо вначале настроить освещение 👏

Русская версия: вот тут.
English: here.

Приятного чтения! 🥰

#generation #demo_scene
Please open Telegram to view this post
VIEW IN TELEGRAM
10💋4🔥1😢1
Вынужденный 🤭 отпуск закончился и я возвращаюсь потихоньку за работу! 🥔
Please open Telegram to view this post
VIEW IN TELEGRAM
18🔥12❤‍🔥4💋1
Начала работать над блокингом сцены: расставляю вначале крупные детали, потом перейду к средним, и в конце уже к мелким. 👏

Заодно дописываю генерацию, так как появляются разные небольшие моменты, о которых раньше и не предполагала) Например, для будущих камушков и скал на склонах надо учитывать нормаль, а не просто рандомно поворачивать 🤔

#generation #demo_scene
Please open Telegram to view this post
VIEW IN TELEGRAM
👍143🔥2💋2🆒2
Когда настраиваешь освещение/пост-процессинг/цвета, то очень быстро глаза "замыливаются" 🧂 Я с этим борюсь тем, что переключаюсь на другую задачу, и через некоторое время снова возвращаюсь к настройке.

Что получилось настроить на текущий момент внутренними средствами Unity по освещению и пост-процессингу можно увидеть на скриншотах 🙌

Где:
1️⃣ Совсем без освещения и пост-процессинга.
2️⃣ Промежуточный вариант освещения и пост-процессинга. Уже лучше, но цвета тусклые + оттенок слишком сильно уходит в зеленый.
3️⃣ Текущий вариант. Цвета стали сочнее, зеленый оттенок стал меньше.

Это все еще не окончательный вариант, через некоторое время вернусь и попробую получить результат еще лучше и ближе к тому, что я хочу)

#demo_scene
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
11👍7🔥6👏2💅2
Детерминированность окружения

С этой взрослой жизнью и взрослыми делами совсем не получается выделить время на проект 😭 Но дела рано или поздно заканчиваются, и часок-другой я все равно стараюсь найти)

Во-первых, ¡Hola! всем, кто меня читает, и muchas gracias! 👋

Во-вторых, я столкнулась с небольшим нюансом, о котором хочу рассказать. Ранее я уже показывала немного видосиков и картиночек генерации мира с объектами окружения. Тогда мне казалось, что я решила задачу, но как это обычно бывает - я ошибалась.

Проблема:
При генерации объектов окружения самая сложная задача - это правильно вычислить уникальный детерминированный идентификатор объекта, по которому можно идентифицировать этот самый объект как в сохранениях мира (если есть изменения), так и между инстансами игры в мультиплеере.

Я привязывалась к мешу ландшафта в чанках: переводила координаты вершин в пространство вокселей и от этого считала идентификатор. Это будет работать, если во всех чанках мира только один уровень детализации (LOD), в ином случае - получаем недетерминированность. Из-за того, что на разных LOD меш содержит разные треугольники, можно получить некоторую вариативность в позиции в пространстве вокселей. Что приводит к неправильному положению объектов при смене LOD, неправильному идентификатору и дублированию объектов, невозможности связать данные сохранения и текущего мира и кучка других проблем.

Как решить?
Я пришла только к одному решению, которое будет работать всегда: считать позицию объектов окружения только в пространстве вокселей, причем всегда в LOD0. Это означает, что необходимо "догенеривать" воксели для чанков, у которых LOD выше 0. Ну, что ж, это плата за детерминированность. 🧂

На данный момент тестирую этот подход, посмотрим чуть позже, что получится 😶

#generation
Please open Telegram to view this post
VIEW IN TELEGRAM
👍165👏21🔥1
Иии, всем привет! 💃

Наверное многие уже решили, что я забыла про канал и больше не будет постов 🫥 Спешу обрадовать - это не так! 🥔

Последние несколько месяцев были очень тяжелыми и насыщенными, и я просто не успевала заниматься еще и проектом. Иногда такое случается, но все это время я все равно старалась хотя бы подумать над тем, что и как должно работать и выглядеть. 🙌

И, с января, я продолжу разработку и начну, пусть не очень часто, но снова писать посты 💃 Планов полно, идей море, багов, которые ждут, что их пофиксят, еще больше! Заняться точно есть чем)

Всех с наступающими праздниками и хорошего начала года! 🥰
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥26🎄166❤‍🔥2💋1
Всем привет! 💃

Я вернулась к коду, и первое, что я поняла: как же сложно вспоминать, на чем остановилась, спустя несколько месяцев! 🔥

Но ничего, свежий взгляд, многое становится более явным, заодно какая никакая, но проверка временем 🥔 Некоторые решения в коде теперь кажутся какими-то…странными что ли? 😛

Как вы помните, и как я вспомнила, я остановилась на проверке варианта «догенерации» вокселей в биоме по требованию, чтобы можно было правильно посчитать положение объектов на ландшафте. Самая большая проблема в том, что там много-много unsafe кода, и часть даже не обернуто во что-то более понятное 😤

Ах, сама виновата 🧂

#generation
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥25😁87💯5🆒3
Всем привет! 🥰

Столкнулась тут с интересной штуковинкой: многие знают такой замечательный инструмент, как Profiler Analyzer, который жизненно необходим во время оптимизирования кода.

Так вот, давайте представим, что мы запускаем какую-то параллельную джобу (IJobFor). Нуу, допустим, 1000 раз:
var jobHandle = job.ScheduleParallel(1000, 1, default);


Причем, мы хотим, чтобы вызовы Execute у нас не батчились, поэтому ставим циферку 1 вторым аргументом.

И что же я вижу в Profiler Analyzer? Count 15? И как это считать?🤔

Ваши идеи? 🚪
Please open Telegram to view this post
VIEW IN TELEGRAM
🦄75🤔41❤‍🔥1
Ну, что ж, пришло время расти над собой 👏

В начале своего пути разработки генерации мира я не понимала многих концепций и часто считала одно лучше другого не понимая до конца какие-либо детали.

Например, я считала, что AoS (array of structures) хорошо подходит для хранения вокселей. Это утверждение правильно только в одном случае: если работа с вокселями происходит сразу со всеми данными в этой структуре одновременно.

Конечно, изначально в структурке Voxel я хранила только значение SDF. Но, код меняется, а значит пора пересмотреть некоторые вещи.

Я начала хранить больше информации на каждый воксель: добавился тип вокселя, веса для материалов, дополнительные флаги и прочее. И, как оказалось в итоге, каждое поле из этой структуры необходимо только для конкретных алгоритмов моей генерации. Я никогда не читаю целый воксель из памяти для того, чтобы поработать со всеми данными.

А что это значит? Пора переходить на SoA! Да! Structure of Arrays становится лучше для моей задачи! Но почему?

Давайте обратимся к циферкам. 📝
1️⃣ Представим, что у нас есть чанк с 32³ вокселями (32  768 всего).
2️⃣ Размер кэш линии в среднем сейчас 64 байта (есть отличия между AMD и Intel в некоторых линейках CPU, но пренебрегаем).
3️⃣ Нам необходимо прочитать все воксели и взять их значение SDF.

🆘 AoS (Array of Structures), когда у нас одна структура , например SDF и тип вокселя:
- на один воксель 8 байт: 4 байта (SDF) + 1 байт (тип) + 3 байта для выравнивания
- один cache line помещает 8 вокселей: 64 байта делим на 8
- что в худшем случае дает нам 4 096 кэш миссов! (32 768 делим на 8) Худший случай - нам каждый раз нужен воксель из другого места.

SoA (Structure of Arrays), когда мы значения храним в разных массивах:
- у нас отдельно лежит SDF, это 4 байта
- cache line вмещает 16 вокселей: 64 делим на 4
- что в худшем случае дает нам примерно 2000 кэш миссов: 32 768 делим на 16.

Разница большая! 🗣

Конечно это просто цифры. В реальности ситуация не настолько плоха, мы часто читаем ближайшие элементы, из текущего кэш лайна. Да и кэш лайнов не одна штука. Но прикинуть "в худшем случае" бывает полезно, чтобы оценить, на сколько эффективно хранение данных у нас.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥26👏7🥰6
Неделя работы с памятью объявляется открытой! 🙌

У Алекса появился пост про память. Очень полезный в контексте Unity для разработчиков всех уровней. 🥹 Кто не видел - очень советую глянуть его презентацию.

Я же хотела рассказать про такой аспект памяти, как ее фрагментация.

О том, как аллоцируются наши кусочки памяти, откуда они берутся и как потом возвращаются - мы очень редко задумываемся. Часто нас интересует сам факт аллоцирования, не больше. Но что происходит, когда мы "отпускаем" ненужный блок памяти? Он просто помечается как неиспользуемый.

Что будет, если мы захотим аллоцировать кусочек побольше? Мы не получим блок, который "освободили" на предыдущем шаге, так как он слишком маленький. Мы получим другой, более большой блок. 🤔

А как же тот, маленький? 😭 А он остается, и совсем не факт, что когда либо будет использован. И такие блоки, разного размера, которые "чуть-чуть" не подходят могут постоянно появляться, создавая множество дырок. Память становится как "решето", мы начинаем требовать все больше физической памяти RAM, а найти нужный свободный блок становится все тяжелее.

Это та самая фрагментация. И с этим надо что-то делать.📝

1️⃣ Например, использовать пулы памяти.
Выделяем большой кусок памяти, режем его на нужные нам кусочки одинакового размера и переиспользуем. Самый простой способ, который при этом очень эффективный.

2️⃣ Аллокаторы. Можно реализовать свои аллокаторы (arena, slab и другие) или взять готовые. Они фактически тоже пулы "под капотом", только более умные и позволяют использовать блоки памяти разного размера.

3️⃣ Аллокаторы с слиянием (coalescing). Еще более умная штуковина с этапом слияния свободных участков и последующим переиспользованием. Используется не так часто, потому что требует дополнительного прохода "упаковки" (да, кое-кого напоминает).

А как же аллокаторы от Unity?
- Temp и Temp Job: обычные arena аллокаторы.
- Persistent: это уже malloc/free и частые аллоцирования приводят к фрагментации. 😔

P.S.: в комментариях уточнили, что persistent - это TLSF аллокатор
Please open Telegram to view this post
VIEW IN TELEGRAM
17👀4👍2
Всем привет! 👋

Продолжим тему с аллокаторами. Как я чуть раньше писала, аллокаторы решают часть проблем при работе с памятью.

Но какие аллокаторы бывают? 🤔 Приведу несколько самых распространенных типов, которые часто используют при разработке игр.

1️⃣ Arena Allocator
Арена-аллокатор - это такой аллокатор, при котором вся память выделяется большим единым куском. А потом она разрезается на кусочки, которые уже используются.

Пример псевдокода:
byte* memory = memalloc(1024*1024); // выделяем сразу большой кусок
byte* current = memory; // сохраняем указатель
...
byte* block = current;
current = current + blockSize; // сдвигаем указатель для следующей аллокации
return block;

Когда используется:
🔘 когда все элементы живут одинаково долго
🔘 когда можно разом выделить память и освободить ее
🔘 когда не надо индивидуальное освобождение объектов

TempJob в Unity является именно Arena-аллокатором.

2️⃣ Slab Allocator
Slab-аллокатор, он же пул памяти, при котором память тоже выделяется большим куском, но нарезается сразу на N участков одинакового размера. Это позволяет переиспользовать участки, если какие-то объекты больше их не используют.

Пример псевдокода для аллоцирования:
byte* memory = memalloc(1024*1024); // выделяем сразу большой кусок
int* freeList; // список свободных элементов
int freeCount = 0;
int allocatedCount = 0;
...
if(freeCount > 0)
{
int idx = freeList[--freeCount];
return memory + idx * blockSize; // выдаем из пула
}
if(allocatedCount < capacity)
return memory + (allocatedCount++) * blockSize; // выдаем следующий не аллоцированный

return null; // блоков больше нет


Псевдокод для возврата:
int idx = (ptr - memory) / blockSize; // вычисляем индекс блока по адресу памяти
freeList[freeCount++] = idx; // записываем индекс как свободный

Когда используется:
🔘 когда много объектов одного размера
🔘 когда важно выделять для них память
🔘 когда важно при этом освобождать память и ее переиспользовать

Пост уже получился достаточно большим. Расскажу о других видах в следующий раз! 🥔

#csharp #allocators
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥245💋4🥰2💘2
Всем привет! 👋

В прошлый раз я рассказала о нескольких типах аллокаторов, которые часто используются в геймдеве. Сегодня хочу продолжить эту тему и рассказать немного еще об аллокаторах, вернее об одном, который уже затрагивали в комментариях. 📝

Two-Level Segregate Fit (TLSF) аллокатор
Он же Persistent аллокатор в Unity в DOTS. Из названия понятно, что этот аллокатор двухуровневый. В чем это проявляется? 🚪

Идея
Основная идея TLSF - классифицировать все запросы на блоки памяти по размеру этих блоков и хранить их на двух уровнях.

Первый уровень, он же First Level Index (FLI), это грубое приближение классификации по размеру. Например, если нам нужен блок размером 60 байт, то FLI будет равен 5 (он же индекс корзины).

Как же считается этот FLI?
По размеру! 🗣 Берется размер, вот эти 60 байт, переводится в двоичный вид (111100) и берется старший бит (32). Это определяет диапазон [32...63]. Или же степень двойки 2^5 = 32.

Следующим шагом, необходимо уточнить, в какой подкорзине относится наш запрос. Такая подкорзина называется Second Level Index (SLI). Для наших 60 байт это будет 14.

Как считается SLI?
На основе FLI и нашего размера! В начале вычисляется шаг. Формула простая:
step = (верхняя граница диапазона - нижняя + 1) / 16

А потом и сам SLI:
SLI = (size - base) / step


Алгоритм
Для выделения куска памяти необходимого размера алгоритм можно описать вот так:
1️⃣ Смотрим на размер блока, который нам нужен и вычисляем FLI и SLI.
2️⃣ Ищем свободный блок по вычисленным FLI и SLI. Тут применяется bitmap для быстрого поиска (O(1)).
3️⃣ Если по указанным FLI/SLI нет свободного блока, идем в следующий SLI (или FLI, если в текущем закончились SLI), в котором есть, пусть и более большой по размеру.
4️⃣ Если мы взяли блок не из своего FLI (то есть он большой для нас), то режем его. А остаток возвращаем.
5️⃣ Обновляем bitmap: указываем, что FLI/SLI пусты.
6️⃣ Возвращаем выбранный блок пользователю.

Поговорим немного про bitmap.
У каждого уровня (FLI/SLI) есть своя bitmap (обычно unsigned int). Каждый бит в bitmap показывает полно/пусто в конкретном FLI/SLI. Причем для FLI каждый бит показывает именно статус конкретных FLI, а для SLI - SLI (запутанно как-то 🤔)

Когда мы аллоцируем или возвращаем, то зная FLI/SLI, можем установить или очистить нужные биты:
slBitmap[fli] |=  (1u << sli);   // ставим бит для SLI
flBitmap |= (1u << fli);
...
slBitmap[fli] &= ~(1u << sli); // сбрасываем бит для SLI
if (slBitmap[fli] == 0)
flBitmap &= ~(1u << fli); // если весь FLI пуст, сбросить и его бит


Чтобы найти свободный блок, нам надо вычислить маску по FLI/SLI. Например, FLI = 5, а SLI = 7:
uint mask = slBitmap[5] & (~0u << 7);

В mask будут только биты ≥ 7. Если маска не равна нулю, то берем первый установленный бит. Если равна - ищем дальше, причем тоже за O(1). 😛
uint maskFL = flBitmap & (~0u << (5+1)); // находим другой свободный FLI
if (maskFL != 0) {
int newFli = BitOperations.TrailingZeroCount(maskFL);
int newSli = BitOperations.TrailingZeroCount(slBitmap[newFli]);
...
}


А что делать с остатком, если мы взяли кусок из более большого FLI и поделили его? 🫥
Остаток в данном кейсе уходит в FLI/SLI по своему размеру как свободный. Например, нужно было 60 байт, а ближайший свободный блок только из корзины (FLI/SLI) в 128 байт. 60 байт мы взяли себе, а 68 байт поместили в FLI/SLI для корзины с диапазоном [64, 128], так как FLI/SLI вычисляется точно по размеру. И для 68 байт это будет FLI = 6 (диапазон [64..127]), а SLI = 1.

Уф, хотела кратенько, а получилось довольно объемно 🫥
Пишите в комментариях, а что вы используете? 🥰

#csharp #allocators
Please open Telegram to view this post
VIEW IN TELEGRAM
119💘5😱4🤩3🍓3
Продолжу тему с TLSF аллокатором, о котором рассказала в предыдущем посте.

Главная непонятная тема - как происходит процесс упаковки свободных блоков? 🤔 Расскажу об одном из алгоритмов (их несколько для TLSF).

Если в общих чертах, то следующим образом:
1️⃣ При освобождении блока помечаем его как свободный
2️⃣ Пытаемся объединить с соседними свободными
3️⃣ Если получилось, то переносим получившийся (более большой) блок в нужный FLI/SLI

А что такое соседние свободные блоки? Тут под ними понимаются не блоки, которые лежат рядом в корзинке! Совсем нет! 🗣

У каждого блока есть заголовок - это какая-то служебная (meta) информация. Часто там хранится размер блока, всякие флаги, индексы, и, указатели на соседние блоки. Когда мы делим большой блок на несколько маленьких (откусываем кусочек), то мы создаем двусвязный список, чтобы потом можно было этот кусочек вернуть обратно в большой пирог.

А что такое двусвязный список? Это ссылки на соседей "слева" и "справа".
Поэтому алгоритм упаковки достаточно простой:
1️⃣ При освобождении блока помечаем его как свободный
2️⃣ Смотрим на соседей, вдруг они тоже свободные
3️⃣ Если свободные, то объединяем в более большой блок
4️⃣ Удаляем объединенные блоки из корзин FLI/SLI, в которых они хранились
5️⃣ Сохраняем новый большой блок в новую корзину FLI/SLI

Вуаля, упаковка произошла, дырок больше нет!

Весь алгоритм выполняется за O(1): проверка на свободный блок - через операции с битмаской, поиск соседей - через двусвязный список, операции удаления/вставки - опять же немного пошаманить с битмаской и сохранить в нужную корзину.

Когда лучше всего использовать TLSF-аллокатор?
🔘 Нужны блоки памяти достаточно большого размера с длинным жизненным циклом (всякие долгоживущие массивы, которые хранятся в памяти, например, все время жизни игры)
🔘 Блоки разного размера
🔘 Нужна поддержка возврата и переиспользования (обычная арена не подходит)

В следующих постах расскажу о своей реализации аллокатора для вокселей: какой, почему именно такой, как я решила проблему с многопоточностью (та еще задачка 🧂), плюс небольшой бонус! 👏

P.S.: мем от ChatGPT прилагается 😛

#csharp #allocators
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥18🦄3❤‍🔥2🍓2🤩1
Всем привет! 💃

Продолжим тему с аллокаторами, ведь я о них не просто так рассказывала? 🫥 Как я писала в одном из постов - я переосмысляю подходы в своей генерации. Причина этому является то, что я уперлась в некоторые ограничения при масштабировании, ну и просто подросла 😛

Что я поняла, так это то, что хранить воксели и их мета-данные очень и очень важно, особенно учитывая тот момент, что их много, а алгоритмы часто работают со всеми (или почти всеми) вокселями в чанках. 🧂

И я пришла к мысли, что мне нужен свой аллокатор, который должен удовлетворять следующим требованиям:
1️⃣ Максимально быстрая аллокация непрерывных блоков памяти.
2️⃣ Минимум фрагментации памяти.
3️⃣ Максимум переиспользования памяти.

Я несколько раз пыталась аллоцировать сразу большой кусок памяти и его разрезать по чанкам и вокселям и как-то использовать. Это оказалось довольно медленно, так как часто память нужна уже "чистая", а аллоцирования и трогание (например очистка) памяти ведет к ее реальному выделению, а не только виртуальному бронированию.

С другой стороны, у меня алгоритмы не используют весь выделенный кусок сразу в один момент времени. Тут больше происходят действия вида "откусили кусочек", что-то поделали, часто даже 1-2 кадра, и отдали обратно. И только какая-то небольшая часть чанков и их память реально живет длительное время.

Из-за такого вида работы с памятью, мне не подходят аллокаторы Temp или TempJob. Они слишком мало живут. С другой стороны, Persistent аллокатор тоже не подходит, он хоть и живет долго, но он достаточно медленный и не избавляет от проблемы, что мне выдадут виртуальный кусок памяти, который на самом деле еще надо инициализировать, что ведет к неприятным задержкам.

Вариант, когда в Jobs мы используем Temp аллокатор, а результат вычислений копируем в Persistent память я тоже в итоге откинула. Это требует выделения памяти в большем количестве и добавляет копирование в конце. Не хочется делать те операции, от которых мы на самом деле можем избавиться. 😔

Что же делать? 🚪
Написать свой аллокатор и радоваться жизни! 🙌

Я пришла к гибридному варианту, когда простую идею о переиспользовании уже выделенных кусков памяти (то есть обычный pooling) натягивают на сегрегацию по размеру блоков и оборачивают это все в lock-free алгоритм для поддержки многопоточного доступа.

Я не выделяю сразу большого куска памяти (хотя какую-то инициализацию на старте игры стоит делать), так как это сложно управляется в многопоточном коде. Быстрый доступ без блокировок - та еще задачка. 🦊

Для аллокации шаги будут такими:
🔘 смотрим на размер запрошенного блока и находим пул, который больше всего подходит под него
🔘 смотрим, есть ли в пуле свободные блоки
🔘 если есть, то берем свободный блок, и откусываем от него (если он слишком большой для нас)
🔘 если нет, то запрашиваем у back-аллокатора (в моем случае это Persistent) новый кусок
🔘 полученный кусок возвращаем пользователю

Для деаллокации шаги похожи:
🔘 смотрим на размер блока и вычисляем пул, из которого этот блок был взят
🔘 кладем в пул и помечаем блок как свободный

Более подробно о том, как я реализовала свой аллокатор и как его подключила к Unity расскажу в следующих постах! 💃

И конечно же для него я написала Profiler Module с подробной статистикой. Спасибо, что подсказали про это! 🥰

#generation #allocators
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥253😨3🍾1
SDF Functions

В поисках всякого про SDF и не только, наткнулась на неплохой сайт, где есть описание ооочень большого количества 3D SDF функций. 🗣

Выглядит прикольно, о некоторых я даже не подозревала, так как обычно сложные формы запекаются.

Хотите бублик?
float sdTorus( vec3 p, vec2 t )
{
vec2 q = vec2(length(p.xz)-t.x,p.y);
return length(q)-t.y;
}


Или может Звезду Смерти (Дарт Вейдер одобряет)?
float sdDeathStar( vec3 p2, float ra, float rb, float d )
{
// sampling independent computations (only depend on shape)
float a = (ra*ra - rb*rb + d*d)/(2.0*d);
float b = sqrt(max(ra*ra-a*a,0.0));

// sampling dependant computations
vec2 p = vec2( p2.x, length(p2.yz) );
if( p.x*b-p.y*a > d*max(b-p.y,0.0) )
return length(p-vec2(a,b));
else
return max( (length(p )-ra),
-(length(p-vec2(d,0.0))-rb));
}


Там много интересного, в том числе и как производить всякие манипуляции с SDF-фигурами. 💃

Также ко многим примерам есть ссылочки на Shader Toy, где можно в динамике посмотреть фигуры. Крутотень! 🙌

#sdf #generation
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥17🍌5🎃4