Вот и LOD Cross Fade получился! 🙌
Есть о чем рассказать, есть интересные моменты😧 Завтра соберу все мысли и опишу в виде поста 💃
Хотя может даже в виде статьи?🚪
Есть о чем рассказать, есть интересные моменты
Хотя может даже в виде статьи?
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
❤11🔥9
https://telegra.ph/Level-of-Detail-v-Unity-BRG-04-10
Написала статью про LOD в BRG🚪 Весь код можно найти в моем репозитории по BRG (MIT-лицензия) 😶 Документацию там тоже скоро обновлю.
Написала статью про LOD в BRG
Please open Telegram to view this post
VIEW IN TELEGRAM
Telegraph
Level of Detail в Unity BRG
Что такое BRG Для того, чтобы описать что же такое Batch Renderer Group (BRG) нужна отдельная статья, поэтому тут я только кратко расскажу про основные его принципы и больше расскажу про интеграцию поддержки Level of Detail (LOD).
🔥28❤6👏3😍3❤🔥1
Media is too big
VIEW IN TELEGRAM
Видео с камушками! 🤩 (и с LOD вместе с CrossFade). 🙌
Как по мне, выглядит неплохо, очень нравится, что камушки плавненько появляются и исчезают, ради этого я и добавила поддержку LOD (а не ради оптимизации, как вы могли подумать☺️ )
Чтобы не делать много постов, скриншот из Frame Debugger приложу в комментариях🚪
Как по мне, выглядит неплохо, очень нравится, что камушки плавненько появляются и исчезают, ради этого я и добавила поддержку LOD (а не ради оптимизации, как вы могли подумать
Чтобы не делать много постов, скриншот из Frame Debugger приложу в комментариях
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥17❤12
Media is too big
VIEW IN TELEGRAM
Протестировала на траве! 🥔
К сожалению не нашла в HDRP ассетах от Unity лоды для травы, они ее просто скрывают плавно по дистанции (ага, и десятки и сотни млн треугольников в итоге в кадре, хоть и не видно их). Пришлось также взять просто Lit шейдер, чтобы не писать сейчас поддержку для Cross Fade для их шейдера. В итоге трава не шевелится :(
Но ничего, то, что мы не видим, то не рисуется, а это главное!🙌 Хотя при этом трава сгенерирована и находится в памяти.
P.S.: Я думала, думала, и решила немного уточнить момент с графикой. Это не стиль будущей игры, и она не будет в реализме, поэтому я не работаю сейчас над визуальной составляющей (некоторые подвижки в эту сторону скоро начнутся). Это пока что просто технические решения и некоторые стресс тесты для них (и для меня😄 ).
К сожалению не нашла в HDRP ассетах от Unity лоды для травы, они ее просто скрывают плавно по дистанции (ага, и десятки и сотни млн треугольников в итоге в кадре, хоть и не видно их). Пришлось также взять просто Lit шейдер, чтобы не писать сейчас поддержку для Cross Fade для их шейдера. В итоге трава не шевелится :(
Но ничего, то, что мы не видим, то не рисуется, а это главное!
P.S.: Я думала, думала, и решила немного уточнить момент с графикой. Это не стиль будущей игры, и она не будет в реализме, поэтому я не работаю сейчас над визуальной составляющей (некоторые подвижки в эту сторону скоро начнутся). Это пока что просто технические решения и некоторые стресс тесты для них (и для меня
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥14❤3👍2
https://telegra.ph/Shading-landshafta-04-13
Решила сегодня попробовать формат не просто поста, а поста-статьи, так как в телеграмме не получится написать такой длинный пост😢 Но это не статья как в прошлый раз! Просто чуть длинненький пост)
Зато там получилось выложить побольше информации и побольше скриншотов🙌
Решила сегодня попробовать формат не просто поста, а поста-статьи, так как в телеграмме не получится написать такой длинный пост
Зато там получилось выложить побольше информации и побольше скриншотов
Please open Telegram to view this post
VIEW IN TELEGRAM
Telegraph
Shading ландшафта
Как же я рисую ландшафт?
🔥16❤4👍3
Небольшой опрос по формату постов в канале)
И спасибо за то, что читаете и даже комментируете ^^
И спасибо за то, что читаете и даже комментируете ^^
Final Results
10%
Маленькие посты, но чаще.
10%
Большие посты через телеграф, но реже.
80%
Вперемешку - иногда маленькие, иногда большие, и иногда полноценные статьи.
🔥2
Всем спасибо за голосование! 🥰
10% проголосовало за маленькие частые посты, 10% за большие, но редкие, и 80% за смешанный тип. Тогда буду стараться поразнообразней публиковать посты)
10% проголосовало за маленькие частые посты, 10% за большие, но редкие, и 80% за смешанный тип. Тогда буду стараться поразнообразней публиковать посты)
Please open Telegram to view this post
VIEW IN TELEGRAM
🎉5👍3❤1🥰1
Биомы
А сейчас я хотела бы подняться на уровень выше в генерации. Биомы!
Что такое биом? Это какой-то набор флоры и фауны, определенный вид ландшафта с уникальными материалами, а еще с уникальными монстрами, условиями игры и многого другого. Например биом гор, полей, леса, океана и т.п.
Но что такое биом в рамках генерации? Как ни странно, это просто набор вокселей и правила их генерации! Потому что только воксели определяют весь окружающий мир.
При генерации каждого вокселя я для него определяю и биом. Это происходит все также по графу шума (на скриншоте), правда уже в 2D.
Для каждого биома я в дальнейшем генерирую полноценный воксель - SDF значение, его градиент. А потом по blend значению смешиваю и получаю уже окончательный результат. Мне это позволяет смешивать биомы: создавать плавный переход от одного к другому. Например горы плавно переходят в поля.
Материалы я также выбираю с двух биомов, и выбираю те, которые имеют наибольший вес. Но это требует повышенного количества ресурсов, так как в одном чанке может быть сразу несколько биомов, у каждого из которых свои настройки и текстуры.
А сейчас я хотела бы подняться на уровень выше в генерации. Биомы!
Что такое биом? Это какой-то набор флоры и фауны, определенный вид ландшафта с уникальными материалами, а еще с уникальными монстрами, условиями игры и многого другого. Например биом гор, полей, леса, океана и т.п.
Но что такое биом в рамках генерации? Как ни странно, это просто набор вокселей и правила их генерации! Потому что только воксели определяют весь окружающий мир.
При генерации каждого вокселя я для него определяю и биом. Это происходит все также по графу шума (на скриншоте), правда уже в 2D.
var biomeSelectionResult = BiomeFlow.SelectBiome(worldPosition.x, worldPosition.z, InputData.WorldSeed);
BiomeFlow.SelectBiome возвращает индексы двух биомов в указанной точке, которые имеют наибольший вес. Также я возвращаю и blend значение, чтобы потом можно было смешивать результаты.Для каждого биома я в дальнейшем генерирую полноценный воксель - SDF значение, его градиент. А потом по blend значению смешиваю и получаю уже окончательный результат. Мне это позволяет смешивать биомы: создавать плавный переход от одного к другому. Например горы плавно переходят в поля.
Материалы я также выбираю с двух биомов, и выбираю те, которые имеют наибольший вес. Но это требует повышенного количества ресурсов, так как в одном чанке может быть сразу несколько биомов, у каждого из которых свои настройки и текстуры.
🔥12❤4🥰1
Burst и делегаты
Иногда в задачах никак не обойтись без делегатов, но что делать, если код на Jobs + Burst? Delegate - это managed тип, под Burst не вызовешь, а от преимуществ отказываться не хочется.
В C# есть unmanaged делегаты, которые также поддерживаются и Burst если правильно приготовить🧂
Все, что нужно сделать, так это:
Шаг 1. Определить самый обыкновенный
Шаг 2. Определить метод/методы, которые хотим вызывать с помощью делегата и пометить атрибутом с ним
Шаг 3. Пометить вызываемые методы из п.2 и типы, которые их содержат атрибутом
Шаг 4. Скомпилить каждый делегат через
Или по старинке вот так:
А что по производительности?
Например, у меня так реализованы операции над ландшафтом.🚪 Я использую указатели на функции, чтобы все типы операций (копание/возведение, по сфере, по кубу и т.п.) можно было описать абстрактной структурой
Все эти операции складываются в массив, по которому пробегаюсь при вычислении SDF вокселя.🥔
Иногда в задачах никак не обойтись без делегатов, но что делать, если код на Jobs + Burst? Delegate - это managed тип, под Burst не вызовешь, а от преимуществ отказываться не хочется.
В C# есть unmanaged делегаты, которые также поддерживаются и Burst если правильно приготовить
Все, что нужно сделать, так это:
Шаг 1. Определить самый обыкновенный
delegate и пометить его как CallingConvention.Cdecl если собираем IL2CPP:[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void OperationPerformDelegate(ref OperationExecutor.Parameters parameters);
Шаг 2. Определить метод/методы, которые хотим вызывать с помощью делегата и пометить атрибутом с ним
[MonoPInvokeCallback(typeof(OperationPerformDelegate))]:[BurstCompile]
[MonoPInvokeCallback(typeof(OperationPerformDelegate))]
private static void InvokeExecute(ref OperationExecutor.Parameters parameters)
{
OperationExecutor.Execute<CapsuleOperation, CapsuleOperation>(ref parameters);
}
Шаг 3. Пометить вызываемые методы из п.2 и типы, которые их содержат атрибутом
[BurstCompile].Шаг 4. Скомпилить каждый делегат через
BurstCompiler.CompileFunctionPointer:private static readonly FunctionPointer<OperationPerformDelegate> m_InvokeExecuteFunctionPointer =
BurstCompiler.CompileFunctionPointer<OperationPerformDelegate>(InvokeExecute);
FunctionPointer можно передать любым удобным образом в Job и вызывать или с помощью обычного Invoke:m_ExecutePtr.Invoke.Invoke(ref parameters);
Или по старинке вот так:
((delegate * unmanaged[Cdecl] <ref OperationExecutor.Parameters, void>)m_ExecutePtr.Value)(ref parameters);
А что по производительности?
FunctionPointer существенно медленнее, чем прямой вызов, поэтому использовать лучше в тех моментах, где совсем не обойтись. Вот тут можно посмотреть сравнение.Например, у меня так реализованы операции над ландшафтом.
NativeOperation, которая содержит только упакованные данные операции + указатель на метод, который по этим данным может произвести вычисления.public struct NativeOperation : IDisposable
{
private readonly FunctionPointer<OperationPerformDelegate> m_ExecutePtr;
[NativeDisableUnsafePtrRestriction]
internal unsafe void* m_Data;
public readonly int3 Min;
public readonly int3 Max;
}
Все эти операции складываются в массив, по которому пробегаюсь при вычислении SDF вокселя.
Please open Telegram to view this post
VIEW IN TELEGRAM
Docs
Function pointers - C# feature specifications
This feature specification describes function pointers, which are unmanaged delegates. They are typically used to avoid the allocations necessary to instantiate a delegate object.
🔥12❤4👍2🤝1🤗1
Media is too big
VIEW IN TELEGRAM
Добавляю генерацию объектов, с которыми игрок может взаимодействовать тем или иным образом (например, физика). Это все большие камни, деревья, постройки и т.п.
Также учитываю и тот момент, что ландшафт может меняться☺️ Пока что реализовала самый простой вариант, когда при изменении ландшафта мы просто опускаем объект вниз (понадобится для кустов и подобных небольших объектов). Но в планах конечно в такие моменты включать физику, и пусть булыжник упадет кому-нибудь на голову 😕 🤭
Так как это объекты с коллайдером, то они симулируются и на серверной стороне, а не только на клиентской😶
Также учитываю и тот момент, что ландшафт может меняться
Так как это объекты с коллайдером, то они симулируются и на серверной стороне, а не только на клиентской
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥8
Media is too big
VIEW IN TELEGRAM
Вот еще одно небольшое видео, на котором видно, что объект с коллайдером 🤩
Please open Telegram to view this post
VIEW IN TELEGRAM
❤9
Media is too big
VIEW IN TELEGRAM
Тест с большими камушками 🤩
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11
Media is too big
VIEW IN TELEGRAM
Деревья!😧 🙌
Стало намного интереснее) Причем деревья, которые поблизости, являются "призраками" и синхронизируются с копией на сервере. А вот деревья вдалеке - существуют только на клиенте) При приближении они конечно же заменяются на призраки🥔
В комментариях еще скрин🫥
Стало намного интереснее) Причем деревья, которые поблизости, являются "призраками" и синхронизируются с копией на сервере. А вот деревья вдалеке - существуют только на клиенте) При приближении они конечно же заменяются на призраки
В комментариях еще скрин
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥18👍1
Сегодня расскажу про то, что крайне редко кастомизируется - аллокаторы в DOTS! ☺️
Все, кто использует нативные коллекции из Unity.Collections, знакомы с аллокаторами, которые Unity предоставляет по умолчанию. Это Persistent - для объектов с долгим времени жизни, Temp - для однокадровых объектов, TempJob - для объектов, передаваемых в Job. Но мало кто знает, что можно написать свой аллокатор и использовать также, как и дефолтные! (Правда с некоторыми оговорками😔 )
На самом деле Unity предоставляет все необходимое API, чтобы можно было реализовать свой аллокатор и использовать в нативных коллекциях. Все, что нужно сделать, так это реализовать интерфейс
Где
Самое интересное - метод🤭 ) выяснять некоторые детали.
В качестве входного параметра нам приходит
Правила тут такие:
- если
- если
- в ином случае, мы должны освободить память.
А вот что возвращает метод?🤔 Указан возвращаемый тип как
- код
- код
После того, как написали свой аллокатор, его можно использовать, но как? Каждый аллокатор должен возвращать свой🔍
Но в случае с кастомным аллокатором есть некоторые ограничения:
1. Нельзя использовать
2.😒 Чтобы создать NativeArray<> надо использовать
Но это сильно ограничивает, в Job уже не получится задиспоузить массив, созданный таким образом😢
3. Нет поддержки многопоточности! То есть нельзя создать инстанс аллокатора в главном потоке, а потом использовать в других потоках. Причина в том, что на каждый аллокатор также создаются свои safety checks - проверки "от дурака". И🤦
Зачем вообще использовать свой аллокатор? В большинстве задач это не требуется, но иногда, все же может быть лучше свое решение, чем то, что предоставляет Unity. Я использую кастомный аллокатор на базе smmalloc по той причине, что мне в какой-то момент понадобились аллокации, которые быстры как и TempJob/Temp, но и живут больше 4 и 1 кадра соответственно.
Все, кто использует нативные коллекции из Unity.Collections, знакомы с аллокаторами, которые Unity предоставляет по умолчанию. Это Persistent - для объектов с долгим времени жизни, Temp - для однокадровых объектов, TempJob - для объектов, передаваемых в Job. Но мало кто знает, что можно написать свой аллокатор и использовать также, как и дефолтные! (Правда с некоторыми оговорками
На самом деле Unity предоставляет все необходимое API, чтобы можно было реализовать свой аллокатор и использовать в нативных коллекциях. Все, что нужно сделать, так это реализовать интерфейс
AllocatorManager.IAllocator:public struct TestAllocator : AllocatorManager.IAllocator
{
public AllocatorManager.TryFunction Function { get; }
public AllocatorManager.AllocatorHandle Handle { get; set; }
public Allocator ToAllocator { get; }
public bool IsCustomAllocator { get; }
public int Try(ref AllocatorManager.Block block) { }
public void Dispose() { }
}
Где
AllocatorManager.TryFunction Function { get; } - делегат функции int Try(ref AllocatorManager.Block block) { }, которую AllocatorManager вызывает при попытке аллоцировать/реаллоцировать/освободить память.Самое интересное - метод
Try, тут мне пришлось опытным путем (ну как всегда в Unity В качестве входного параметра нам приходит
ref AllocatorManager.Block block, который содержит некоторые детали запроса. А именно сколько элементов и с каким размером и выравниванием надо аллоцировать. Также тут хранится и указатель на память, этот указатель нам и надо заполнить. Правила тут такие:
- если
block.Range.Pointer равен IntPtr.Zero, то мы должны аллоцировать память;- если
block.Range.Pointer не равен IntPtr.Zero и block.Range.Items больше 0, то есть уже раньше аллоцировали этот блок памяти и он валиден, то мы его должны реаллоцировать;- в ином случае, мы должны освободить память.
А вот что возвращает метод?
int, надо же что-то вернуть. А возвращается тут код ошибки. Из того, что я выяснила:- код
0 - все успешно;- код
-1 - операция прошла неуспешно.После того, как написали свой аллокатор, его можно использовать, но как? Каждый аллокатор должен возвращать свой
AllocatorHandle - это всего лишь указатель на запись во внутренней таблице аллокаторов, по которой Unity ищет указатель на функцию аллокатора. Кстати, Allocator.Persistent/Temp/TempJob - также неявно конвертируются в AllocatorHandle. Но в случае с кастомным аллокатором есть некоторые ограничения:
1. Нельзя использовать
UnsafeUtility.Malloc/Free, вместо этого необходимо использовать AllocatorManager.Allocator/Free.(T*)AllocatorManager.Allocate(handle, itemSizeInBytes, alignmentInBytes, length);
AllocatorManager.Free(handle, pointer, length);
2.
NativeArray<> не поддерживает кастомный аллокатор! Причина в том, что он написан для использования только enum Allocator CollectionHelper.CollectionHelper.CreateNativeArray<T, TestAllocator>(length, ref allocator, options);
CollectionHelper.Dispose(array);
Но это сильно ограничивает, в Job уже не получится задиспоузить массив, созданный таким образом
3. Нет поддержки многопоточности! То есть нельзя создать инстанс аллокатора в главном потоке, а потом использовать в других потоках. Причина в том, что на каждый аллокатор также создаются свои safety checks - проверки "от дурака". И
SafetyHandle внутри AllocatorManager хранятся в UnsafeList, который не является потокобезопасным. Опять же недоработка от Unity.Зачем вообще использовать свой аллокатор? В большинстве задач это не требуется, но иногда, все же может быть лучше свое решение, чем то, что предоставляет Unity. Я использую кастомный аллокатор на базе smmalloc по той причине, что мне в какой-то момент понадобились аллокации, которые быстры как и TempJob/Temp, но и живут больше 4 и 1 кадра соответственно.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥9❤6🤯2❤🔥1👍1
Разбираюсь сейчас со спайками, и немного углубляюсь в unsafe мир ☺️
Вот что интересного выяснила: память желательно выравнивать! Вот да, недостаточно просто выделить кусок памяти, желательно выровнять его по размеру кэшлайна. Разница - огромная!😧
Размер кэшлайна в Job System можно узнать через константу
А вот выровнять можно через CollectionHelper:
Где T - ваш тип данных. Достаточно просто, а разница - на скриншоте)
Вот что интересного выяснила: память желательно выравнивать! Вот да, недостаточно просто выделить кусок памяти, желательно выровнять его по размеру кэшлайна. Разница - огромная!
Размер кэшлайна в Job System можно узнать через константу
JobsUtility.CacheLineSize (64 байта).А вот выровнять можно через CollectionHelper:
// Считаем сколько памяти (в байтах) нам нужно, заодно выравниваем по кэшлайну, чтобы было кратко CacheLineSize
var someDataSize = CollectionHelper.Align(UnsafeUtility.SizeOf<T> * length, JobsUtility.CacheLineSize);
// Выделяем (аллоцируем) память по посчитанному размеру и указываем, что выравниваем по CacheLineSize
var someData = (byte*)AllocatorManager.Allocate(allocatorHandle, someDataSize, JobsUtility.CacheLineSize);
Где T - ваш тип данных. Достаточно просто, а разница - на скриншоте)
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥20❤🔥2❤2👌2🦄2
Мехмат ЮФУ (Южный Федеральный Университет) не так давно взял у меня интервью, и вот оно вышло 🥔
https://t.me/igromech/85
https://t.me/igromech/85
Please open Telegram to view this post
VIEW IN TELEGRAM
Telegram
ИгроМех
Иногда канал в телеграме выглядит так классно, что нельзя пройти мимо. Вот и наш @xivol настолько заинтересовался, что взял интервью у Елизаветы, которая ведет канал @infinity_world_developer_diary
По ссылке вы найдете обстоятельный разговор о процедурной…
По ссылке вы найдете обстоятельный разговор о процедурной…
🔥17👍7❤4🥰2
Что-то давно я не писала, ох уж эти выходные 🤭
Продолжаю избавляться от спаек в рантайме, много экспериментов, проверок теорий и различных исправлений. Некоторые моменты приходится вообще переосмыслить, так как оказалось, что я выбрала неправильное направление. Но это нормально, и главное тут не унывать🫥
Вместе с Profiler пользуюсь Profile Analyzer, который очень сильно помогает анализировать конкретные маркеры, и даже сравнивать их между несколькими снапшотами. Так что, очень советую этот инструмент взять на заметку, если еще не взяли🥔
Только учтите, что время там показывается на кадр) То есть, например, медиана - за кадр + кол-во кадров, в которых этот маркер есть. Я так долго думала, почему запись в память (пусть и random access) занимает 1.4 мс😄 А это 200к операций в кадр оказалось) Глупо получилось конечно 🤪
Что же касается своих маркеров, то все очень просто.
Шаг 1. Создаете сам маркер, желательно статик:
Шаг 2. Используете маркер
Шаг 3. Находим наш маркер в анализаторе по названию, который указали в конструкторе маркера.
Продолжаю избавляться от спаек в рантайме, много экспериментов, проверок теорий и различных исправлений. Некоторые моменты приходится вообще переосмыслить, так как оказалось, что я выбрала неправильное направление. Но это нормально, и главное тут не унывать
Вместе с Profiler пользуюсь Profile Analyzer, который очень сильно помогает анализировать конкретные маркеры, и даже сравнивать их между несколькими снапшотами. Так что, очень советую этот инструмент взять на заметку, если еще не взяли
Только учтите, что время там показывается на кадр) То есть, например, медиана - за кадр + кол-во кадров, в которых этот маркер есть. Я так долго думала, почему запись в память (пусть и random access) занимает 1.4 мс
Что же касается своих маркеров, то все очень просто.
Шаг 1. Создаете сам маркер, желательно статик:
private static readonly ProfilerMarker s_SomeProfilerMarker = new("Some Marker");Шаг 2. Используете маркер
// Первый вариант использования
using(s_SomeProfilerMarker.Auto())
{
// тут какой-то код, который мы хотим оценить
}
// Второй вариант использования
s_SomeProfilerMarker.Begin();
// тут какой-то код, который мы хотим оценить
s_SomeProfilerMarker.End();
Шаг 3. Находим наш маркер в анализаторе по названию, который указали в конструкторе маркера.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8🔥5🥰2👍1
Не задача с собеседования, но все же может быть интересно ☺️
Вопрос звучит вот так: будет ли валидно состояние outJobHandle? Если да, то почему?🔍
Вопрос звучит вот так: будет ли валидно состояние outJobHandle? Если да, то почему?
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔5🥴5👍2
Пока все еще решаю проблемы спаек, пришлось переделать всю систему генерации. Зато она теперь вся (почти) под Burst. Шаги генерации теперь тоже unmanaged и под Burst, а не только Jobs.
Но вот пост-процессоры чанков, которые создают отображение данных чанков в сцене все также остались managed типами. От этого не убежать никуда пока что😔 Поэтому тут приходится не просто углубляться в unmanaged часть, проектировать то, как данные должны лежать, как к ним должен совершаться доступ и многое другое. Но и приходится связывать managed и unmanaged части.
Главная проблема тут - Burst: у него множество ограничений, но при этом очень хочется его использовать везде, где только возможно. Но что делать, если в unmanaged и burstable типе вдруг очень нужен managed тип? Вот прям совсем нужен.🔍
Если это свойство или метод, то можно пометить их атрибутом
А если нужен прям инстанс целого класса? Вот тут решение немного посложнее. Такие инстансы лучше хранить где-нибудь отдельно, не в нашем красивом unmanaged и burstable типе, потому что в ином случае, он сразу перестанет быть таковым. Как один из вариантов - статика для managed, а в нашем burstable типе - просто какой-нибудь идентификатор. И для удобства свойство с атрибутом
Как пример пвседокода ниже:
Но вот пост-процессоры чанков, которые создают отображение данных чанков в сцене все также остались managed типами. От этого не убежать никуда пока что
Главная проблема тут - Burst: у него множество ограничений, но при этом очень хочется его использовать везде, где только возможно. Но что делать, если в unmanaged и burstable типе вдруг очень нужен managed тип? Вот прям совсем нужен.
Если это свойство или метод, то можно пометить их атрибутом
ExcludeFromBurstCompatTesting указав причину, почему Burst не может компилировать этот кусок кода. Да, Burst пропустит это свойство или метод, удобненько. Правда и обращаться к свойству/методу нельзя будет из под Burst. Поэтому такой вариант больше для логического объединения под одним типом.А если нужен прям инстанс целого класса? Вот тут решение немного посложнее. Такие инстансы лучше хранить где-нибудь отдельно, не в нашем красивом unmanaged и burstable типе, потому что в ином случае, он сразу перестанет быть таковым. Как один из вариантов - статика для managed, а в нашем burstable типе - просто какой-нибудь идентификатор. И для удобства свойство с атрибутом
ExcludeFromBurstCompatTesting.Как пример пвседокода ниже:
public struct SomeBurstableStruct
{
private static SomeManagedType[] s_Instances;
private int m_ManagedInstanceId;
[ExcludeFromBurstCompatTesting("Takes managed types")]
public SomeManagedType ManagedInstance
{
get => s_Instances[m_ManagedInstanceId];
}
}
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7🌚2
Generic типы и FunctionPointer
Иногда не задумываешься о каких-то вещах, пока эта самая вещь тебя не стукнет по голове🤭
При работе с FunctionPointer'ами, в один солнечный денек, я столкнулась с тем, что при вызове делегата я получаю краш! Причем получаю в 100% случаях. Делегат вызывается вот так:
Первая мысль - делегат не живой, но дебаг показал, что поинтер живее всех живых. Вторая мысль - потеряла атрибуты соглашений о вызове. Мало ли где-то вызываю не через Cdecl🔍 Но вот проблема, все правильно. Тогда в чем же причина краша? 🤔
В логах я вижу вот такое:
Что конечно же никак не помогает. Функция не доступна? Но поинтер же живой! А давайте проверим, будет ли делегат работать через самый обычный Invoke?
И вот так все работает! Удивительно. То есть делегат точно живой, а причина куда глубже.😔
Во время дальнейших исследований, в один момент, я заметила, что на краш влияет то, откуда я вызываю делегат!
🔸 если я вызываю из значимого дженерик типа (struct SomeType<T> where T : unmanaged), то краш будет в 100% случаях;
🔸 если я вызываю из обычного типа (struct/class SomeType), то краша уже не будет.
Поразбиравшись (и пообщавшись с AI) я выяснила, что и правда, дженерик может повлиять на вызов делегата!🗣 И виновник тут - JIT компилятор, который определенным образом работает с дженерик типами:
🔹 если дженерик параметр - ссылочный тип, то JIT создает одну версию для всех конкретных типов параметра;
🔹 есди дженерик параметр - значимый тип, то JIT создает под каждый конкретный тип свою версию дженерик типа.
Почему так?
Потому что инстансы значимых типов влияют на layout типа, в котором они находятся. Все мы знаем, что инстанс значимого типа находится или на стеке (отдельно или как часть другого значимого типа) или в куче как часть ссылочного типа.
Вот и получается, что значимый дженерик параметр, даже если он выступает просто как ограничитель, может повлиять на layout нашего изначального типа.
И так как для вызова FunctionPointer надо точно знать адрес не только метода, который мы вызываем, но и точный адрес откуда вызываем, то при попытке сделать это, мы можем получить краш.
Решение
Их тут может быть несколько:
1. Избавиться от generic параметра.
2. Вынести вызов FunctionPointer в отдельный тип без дженериков, можно даже просто статик.
Note: забыла написать, что это правдиво для JIT (Mono), тогда как для IL2CPP скорее всего такой проблемы нет (не тестировала, но там AOT все таки)
Иногда не задумываешься о каких-то вещах, пока эта самая вещь тебя не стукнет по голове
При работе с FunctionPointer'ами, в один солнечный денек, я столкнулась с тем, что при вызове делегата я получаю краш! Причем получаю в 100% случаях. Делегат вызывается вот так:
((delegate * unmanaged[Cdecl] <void>)someFunctionPointer.Value)();
Первая мысль - делегат не живой, но дебаг показал, что поинтер живее всех живых. Вторая мысль - потеряла атрибуты соглашений о вызове. Мало ли где-то вызываю не через Cdecl
В логах я вижу вот такое:
0x000002299F7F03A6 ((<unknown>)) (function-name not available)
Что конечно же никак не помогает. Функция не доступна? Но поинтер же живой! А давайте проверим, будет ли делегат работать через самый обычный Invoke?
someFunctionPointer.Invoke.Invoke();
И вот так все работает! Удивительно. То есть делегат точно живой, а причина куда глубже.
Во время дальнейших исследований, в один момент, я заметила, что на краш влияет то, откуда я вызываю делегат!
🔸 если я вызываю из значимого дженерик типа (struct SomeType<T> where T : unmanaged), то краш будет в 100% случаях;
🔸 если я вызываю из обычного типа (struct/class SomeType), то краша уже не будет.
Поразбиравшись (и пообщавшись с AI) я выяснила, что и правда, дженерик может повлиять на вызов делегата!
🔹 если дженерик параметр - ссылочный тип, то JIT создает одну версию для всех конкретных типов параметра;
🔹 есди дженерик параметр - значимый тип, то JIT создает под каждый конкретный тип свою версию дженерик типа.
Почему так?
Потому что инстансы значимых типов влияют на layout типа, в котором они находятся. Все мы знаем, что инстанс значимого типа находится или на стеке (отдельно или как часть другого значимого типа) или в куче как часть ссылочного типа.
Вот и получается, что значимый дженерик параметр, даже если он выступает просто как ограничитель, может повлиять на layout нашего изначального типа.
И так как для вызова FunctionPointer надо точно знать адрес не только метода, который мы вызываем, но и точный адрес откуда вызываем, то при попытке сделать это, мы можем получить краш.
Решение
Их тут может быть несколько:
1. Избавиться от generic параметра.
2. Вынести вызов FunctionPointer в отдельный тип без дженериков, можно даже просто статик.
Note: забыла написать, что это правдиво для JIT (Mono), тогда как для IL2CPP скорее всего такой проблемы нет (не тестировала, но там AOT все таки)
Please open Telegram to view this post
VIEW IN TELEGRAM
👍13❤3❤🔥2🤩2
Кастомные Jobs
В процессе пересмотра концептов генерации мне понадобились свои типы джобов.🔍 Да, Unity предоставляет все необходимое API, чтобы создавать не просто свои джобы, но и свои типы джоб! Иногда (редко) это бывает очень нужно.
Зачем нужны кастомные типы джоб?
1️⃣ В первую очередь, это уменьшает дублирование кода. Допустим у вас появилась какая-то своя коллекция, которая требует определенного алгоритма итерирования. И, чтобы не писать везде этот алгоритм, можно использовать кастомную джобу.
2️⃣ Во-вторых, это дает больше контроля. Появляется возможность решать что и как, и в каком порядке обрабатывать. Хотите параллельно? Такое возможно. Хотите работать с определенными данными в джобе через параметры? Тоже можно.
3️⃣ В-третьих, это отличная возможность усложнить упростить себе жизнь. Джобы - хороший инструмент для создания цепочек вызовов (прямо как реактивщина), но из-за ограничений unmanaged мира, не все можно реализовать. Кастомные джобы частично помогут решить эту проблему.
Как создавать?
Немного кода, немного бойлерплейта,соли сверху 🧂 и готово) Но кода и правда мало.
🔸 Самое главное описать интерфейс, который все конкретные реализации вашей джобы будут реализовывать.
Интересное тут - атрибут
А в Execute могут разные параметры на ваш вкус.
🔶 Реализовать ту самую структуру с магией:
В которой также должен быть метод Execute, вот только параметры в нем меняться не могут.
Этот метод вызывается уже самими потоками, когда они работают с джобой. Наша главная задача тут - написать логику работы джобы. Где-то это может быть простой вызов Execute, как в примере выше; где-то же - итерирование по коллекции и передача элементов в Execute через параметры; где-то - сложные вычисления со сложными условиями, то есть все, чего требуют задачи для их решения, может быть тут.
🔶 В
Фактически тут надо передать тип нашей структуры-обработчика, тип интерфейса нашей джобы и указатель на метод Execute, чтобы он потом вызывался.
🔶 Чтобы зашедулить наши магические джобы, необходимо получить параметры для шедулинга:
Где также указывается и режим выполнения. Он может быть Single - один поток (не обязательно main), Parallel - множество потоков, и Run - только тот поток, откуда вызываем джобу (опять же не обязательно main).
А дальше остается только вызывать Schedule и все, джоба улетела выполнять свои дела.
Совет: отправку джобы планировщику можно оформить в виде метода-расширения.
Ссылочки с примерами
- пример с single thread джобой вот тут
- пример с parallel thread джобой вот тут
В процессе пересмотра концептов генерации мне понадобились свои типы джобов.
Зачем нужны кастомные типы джоб?
1️⃣ В первую очередь, это уменьшает дублирование кода. Допустим у вас появилась какая-то своя коллекция, которая требует определенного алгоритма итерирования. И, чтобы не писать везде этот алгоритм, можно использовать кастомную джобу.
2️⃣ Во-вторых, это дает больше контроля. Появляется возможность решать что и как, и в каком порядке обрабатывать. Хотите параллельно? Такое возможно. Хотите работать с определенными данными в джобе через параметры? Тоже можно.
3️⃣ В-третьих, это отличная возможность
Как создавать?
Немного кода, немного бойлерплейта,
🔸 Самое главное описать интерфейс, который все конкретные реализации вашей джобы будут реализовывать.
[JobProducerType(typeof(JobCustomSingleExtensions.JobCustomSingleStruct<>))]
public interface IJobCustomSingle
{
void Execute();
}
Интересное тут - атрибут
JobProducerType, задача которого указать на структуру, в которой происходит вся магия.А в Execute могут разные параметры на ваш вкус.
🔶 Реализовать ту самую структуру с магией:
internal struct JobCustomSingleStruct<TJob>
where TJob : struct, IJobCustomSingle
В которой также должен быть метод Execute, вот только параметры в нем меняться не могут.
public static void Execute(ref JobCustomSingleStruct<TJob> fullData, IntPtr additionalPtr, IntPtr bufferRangePatchData,ref JobRanges ranges, int jobIndex)
{
fullData.JobData.Execute();
}
Этот метод вызывается уже самими потоками, когда они работают с джобой. Наша главная задача тут - написать логику работы джобы. Где-то это может быть простой вызов Execute, как в примере выше; где-то же - итерирование по коллекции и передача элементов в Execute через параметры; где-то - сложные вычисления со сложными условиями, то есть все, чего требуют задачи для их решения, может быть тут.
🔶 В
JobCustomSingleStruct также должен быть метод инициализации, который знает как создавать данные для рефлексии.[BurstDiscard]
internal static void Initialize()
{
jobReflectionData.Data = JobsUtility.CreateJobReflectionData(typeof(JobCustomSingleStruct<TJob>),typeof(TJob), (ExecuteJobFunction)Execute);
}
Фактически тут надо передать тип нашей структуры-обработчика, тип интерфейса нашей джобы и указатель на метод Execute, чтобы он потом вызывался.
🔶 Чтобы зашедулить наши магические джобы, необходимо получить параметры для шедулинга:
var scheduleParams = new JobsUtility.JobScheduleParameters(UnsafeUtility.AddressOf(ref fullData),GetReflectionData<TJob>(), dependsOn, ScheduleMode.Single);
Где также указывается и режим выполнения. Он может быть Single - один поток (не обязательно main), Parallel - множество потоков, и Run - только тот поток, откуда вызываем джобу (опять же не обязательно main).
А дальше остается только вызывать Schedule и все, джоба улетела выполнять свои дела.
JobsUtility.Schedule(ref scheduleParams);
Совет: отправку джобы планировщику можно оформить в виде метода-расширения.
Ссылочки с примерами
- пример с single thread джобой вот тут
- пример с parallel thread джобой вот тут
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥15👍9❤4