
Вот мы и добрались до финальной части, в которой я расскажу, как делал софт, который управляет подсветкой вокруг трёх теликов. Будут векторы, суровые SIMD‑оптимизации, апгрейд алгоритма быстрой степени, новая (наверное) парадигма программирования, видеокартовые шейдеры, алгоритмы рендеринга, полиглотный код, щепотка GUI и ещё куча всего.

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

Однако, потенциал установки эта сделанная на коленке штука раскрывала плохо. Гибкость оставляла желать лучшего, а каждая последующая доработка причиняла боль. Да и алгоритм анализа экрана тут был далеко не самым эффективным.
Никогда такого не было и вот опять — надо всё переписать. Я и переписал.

Делал я его потихоньку в 2022–2024 годах, параллельно с эксплуатацией старого софта. Это уже полновесное ПО управления подсветкой на собственном мини‑фреймворке — от низкоуровневых SIMD‑обработчиков до встроенной IDE.

Новый софт
Новый софт строится вокруг подробной модели светодиодной установки и является гибридом со средой разработки самого себя.

Изначально я хотел делать его готовым решением, но быстро понял, что это скучно, поэтому софт стал полигоном для отстреливания ног разных экспериментов и апробации идей — как технических, так и концептуальных — которые я давно хотел проверить. Отчасти поэтому он всё ещё в стадии альфа‑версии, но мне норм :)
Экспериментальность воплотилась не только в странных решениях, но и в том, что я сначала родил с нуля отдельный фреймворк, и потом уже на нём сделал софт.
Старый |
Новый |
Знает только номер диода |
Знает о физическом месте каждого диода, на каком он экране и в какой части рамы |
Захватывает, хранит и считает каналы цвета RGB как целые 8 бит |
Захватывает, хранит и считает каналы цвета RGB как вещественные числа 32 бита |
Только SDR, выше 100% не прыгнешь |
Умеет в HDR: при анализе экрана, обработке и даже выводе, зажигая ленту когда надо «ярче 100%» и «тусклее 1/255» |
Эффекты и обработка жестко закодированы, но можно настраивать |
Можно создавать и редактировать цепочки эффектов, их код и алгоритмы |
Картинка экрана переводится в SDR и уменьшается видеокартой, копируется через шину в ОЗУ и анализируется процессором |
Прямоточный анализ оригинальной HDR картинки прямо на видеокарте без конвертаций и копирований и с минимальными затратами, в ОЗУ копируется всего 37 килобайт |

Несмотря на свою C#овость, софт пронизан ручным управлением памятью, табличками готовых значений и очень злыми, бессмысленными и беспощадными оптимизациями. Просто руки чесались. Впрочем, без графики, HLSL и C++ тоже не обошлось.
Как это работает
Как видеомикшер. Ну или софт для работы с видео — эффекты, каналы, вот это всё. Только кадры‑картинки тут не двумерные, как в видео, а одномерные — светодиодные ленты же. В общем, мы как‑то генерируем и обрабатываем с помощью всяких штук 1D картинки, и выводим их на ленты. 60 раз в секунду. Ну или около того.

Как? Ну, у нас есть один или несколько каналов. Каждый канал — это источник какой‑то 1D‑картинки, выводимой на светодиодную установку. Тыкаем в нужный канал — он выводится на ленты. Всё. Разумеется, переключение плавное.

Например, есть канал «Скан экрана» который делает то самое «расплывание картинки на экране на стены» во время игр и фильмов, а есть каналы, просто генерирующие разные красивости: сверкание, радуги‑заливки и прочие сверкающие ништяки во время работы с обычными приложениями. Эти каналы генерируют световой рисунок самостоятельно, никак не соотносясь с тем, что происходит на экране.
Для удобства в верхней части окна видно то, что выводится на светодиодные ленты, а также графики загрузки блоков питания.

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

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

Каждый канал — это цепочка эффектов — штук, которые что‑то делают с одномерной картинкой, прежде чем она отправится на ленты. Например, сначала заливаем картинку цветом, потом добавляем сияние, потом наносим лёгкий градиент.

Эффекты внутри канала тоже можно добавлять, удалять, переставлять и менять имя, а ещё — настраивать. Можно создать свой канал, напихать в него разных эффектов и получить уникальную картинку. Г — гибкость.

Ещё есть особый канал вывода — тут тоже цепочка эффектов, которая применяется всегда к выходной картинке, какой бы канал не был выбран. Здесь происходят финальная настройка яркости, вывод светорисунка на контроллер с лентами, и прочие штуки. Эффекты постобработки, короче говоря.

Таким образом, тут всю работу делают эффекты. Скан экрана — эффект, вывод на контроллер — эффект, даже управление электроприводами теликов — это эффект.

Всё есть эффект. Настраивай, расставляй как хочешь, пихай в каналы — получай сверкание. Такая вот архитектура.
Помимо всяких радуг, сверканий и прочих переливов, есть некоторые полезности. Например, есть эффект, который с помощью блика помогает найти главный курсор на экране — здесь это актуальная проблема.

А тут периферическое зрение быстро подсказывает, где сейчас происходит вся движуха. Благодаря поддержке HDR написать такой блик легко — просто подкрашиваем и увеличиваем яркость какого‑то одного диода на 1000%.
Когда подключено несколько мышек, то блик работает только с основным курсором, для остальных надо как‑то научиться докапываться до MouseMux — софта для многомышечности. Я связывался с автором, но пока у него не хватает времени на всё.
Та же самая проблема с только что открытыми окнами — иногда просто непонятно, где оно появилось.

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

Первоначально оконный блик был меньше и более точно позиционировался, но практика показала, что он может показать только примерное место появления окна, и большая яркость и заметность важнее точности. Аналогичная штука есть и для отображения текущей раскладки: русская красным цветом, английская — синим.
Когда боковые телики открыты, центральный телик немного приглушает подсветку по сравнению с боковыми — он понимает, что находится ближе к стене. А если телики запарковать вдоль стены, яркость центрального плавно повышается до уровня боковушек, чтобы все три светили одинаково. Всё это тоже делает отдельный эффект.
Есть ещё кое‑что. Экспериментируя с разными световыми эффектами, мне надоело всё время лазить для этого в VisualStudio.

Поэтому в новом софте у любого эффекта можно менять исходный код прямо во время работы, в специально сделанном редакторе. С автокомплитом и подсветкой.

Более того, состояние и настройки эффекта — это и есть его код. В рамках эксперимента, я решил сделать эффекты не классами‑объектами, а особыми сущностями — булочками. Но обо всём по порядку.
Подход к оптимизации
Подход к оптимизации влияет на всю архитектуру софта сверху донизу. Примерно я себе это сформулировал как‑то так:
Что делаем |
Как себя ведёт софт |
Играем |
Нагрузка на видеокарту → 0%, нагрузка на проц → 0%, частота выделений ОЗУ → 0 Гц |
Работаем, софт подсветки свёрнут |
Нагрузка на видеокарту → 0%, нагрузка на проц < 5%, частота выделений ОЗУ минимальна |
Работаем с софтом подсветки |
Нагрузка на видеокарту < 15%, нагрузка на проц < 15%, ОЗУ можно выделять при активности пользователя |
Подробнее
Когда софт не трогают, он должен жрать минимум ресурсов, чтобы не мешать программам и играм. Особенно играм.
Есть тут ещё одна неочевидная вещь. Это ± реалтаймовый софт, который должен успевать 40–60 раз в секунду выдавать что‑то на ленты. А написан он на С#, у которого автоматическое управление памятью и сборка мусора.
Когда накапливается много мусорных объектов, просыпается сборщик мусора и начинает всё чистить. В этот момент будут лаги, и никакие настройки не спасут, что бы там не обещала документация (я далеко не первый раз ем этот кактус делаю реалтаймовый софт на C#). Управлять этим сборщиком мусора почти невозможно. Поэтому остаётся одно: писать на С++ как нормальные лю просто не провоцировать его.
Для этого нужно не создавать мусор: либо не создавай объект, либо, если уж создал — используй его повторно. Не надо на каждый кадр генерировать кучу одноразовых штук и тут же забывать про них.
Чтобы реализовать такой подход, я давным‑давно придумал странноватые примитивы, которые этих самых созданий мусорных объектов (аллокаций в куче) помогают избежать. Но расплатой за это служат неочевидные поведения и особенности, без знаний которых можно легко отстрелить себе ногу. Эти примитивы в модифицированном виде я реализовал и тут. И хотя, много позже, в.net 8 и.net 9, у некоторых из них появились урезанные аналоги — на то они и эксперименты, чтобы быть экспериментальными.
А вот количество потребляемой ОЗУ я не ограничивал — мне её не жалко.

Сразу уточню — софт я писал ради развлечения и под себя. В некотором смысле, он является частью ПАК и заточен исключительно под конкретно мою конфигурацию компа, хотя на других десктопах тоже должен запускаться. И да — писать так код в продакшене и/или в команде не надо :)
Итак, пробежимся по основным кусочкам софта — от базовых примитивов и слоёв абстракции до высших и интерфейса.
Экспериментальная база
Базовых типов и всяких структур тут много, но опишу наиболее странные — те, которые влияют на общий подход ко всему. Многие из них были сделаны для избегания аллокаций в куче .NET

Ленивая мяукающая стринговая личинка
Строки — ссылочно‑кучный тип C#, поэтому всякая работа со строками чревата аллокациями, которых мы тут будем всячески избегать.
Для логов и не только в софте я использую ленивую конкатенацию строк с помощью значимого типа mew. Это не строка, а личинка строки.
string text = $”Значение равно {value}%”;
mew text = (“Значение равно “, value , “%”);
mew — значимый тип, который хранит фрагменты строки в их исходном виде (строки как строки, числа — как числа, bool как bool). Первые 48 фрагментов хранятся на стеке, далее уже идёт аллокация в куче. Отмечу, что mew — не ref struct, а просто struct.
Соединение и образование новой строки случится, только если вызвать.ToString(). Так мы избегаем создания ненужных экземпляров string и каких‑бы то ни было аллокаций, реалтаймовые алгоритмы обработки спокойно могут создавать целые потоки сообщений, не заботясь о генерации мусора.
Можно набивать значения не через кортежи, а подобно StringBuilder:
mew pew = new();
pew.PutItem(“Начало сообщения ”);
pew.PutItem(15); //Сохранится как int 15, преобразования в строку и боксинга не будет
pew.PutItem(“ конец сообщения”);
return pew;
E.rr - почти On Error Resume Next
К ошибкам времени выполнения здесь особый подход. Ошибки не должны останавливать работу — мы тут не атомные станции погромируем, а подсветочку для батлфилдов и майнкрафтов. Произошла ошибка — логируем и фигачим дальше. Подумаешь, какой‑то эффект отвалится. По всем библиотекам и софту вместо
throw new Exception();
используется другая штука:
E.rr = (“Значение “, value, “ слишком большое”);
Возникла ошибка — логируем и продолжаем работу. Такой подход имеет свои плюсы и минусы, но для данного случая он более чем подходящий. Стек не собираем, аллокаций почти нет, если сбой единичный — есть шанс продолжить работу, а так — ну, просто эффект какой‑то отвалится, а остальное работает. И да — на атомных станциях так лучше не делать.
Уточню, что E.rr — это не какой‑то там «текущий статус ошибки». Внутри он примерно такой:
public static event Action<ErrorInformation> OnException;
public static ErrorInformation rr
{
set
{
OnException?.Invoke(value);
}
}
На самом деле он чуть сложнее + Debug/Release ведёт себя по‑разному, но это не важно. Главное — на OnException вешаем логирование — профит.
В дополнение к этому, статический класс E располагает ништяками для проверки аргументов в методах:
E.IsNull(...)
E.IsInvalid(...)
E.IsNotInDiapasone(...)
E.IsNaN(...)
E.IsNot64bit()
E.IsNot32bit()
и прочие.
Все они что‑то проверяют и возвращают true, если всё плохо. В методах проверка максимально лаконична:
if (E.IsInvalid(argument)) return;
Если аргумент неправильный, то в дебаге будет остановка и вывод в лог, в релизе — просто вывод в лог. В обоих случаях выполнение продолжается. Если метод не void, то обычно возвращается default. И да — в данном софте такой подход работает.
Если argument null или выгружен (вызывали Dispose) — оно напишет в лог максимально развёрнуто на русском языке, что случилось, где случилось и в каком потоке.
Есть некоторые методы для коррекции аргументов, например такой:
E.CorrectIndexDiapasone(ref fromIncl, ref toExcl, 0, collection.Count)
Если диапазон неверный, то в лог будут маты, после чего диапазон будет скорректирован и работа софта продолжится.
Практически всё, что касается класса E набито всякими [CallerMemberName] — оно собирает инфу и о вызывающем методе, и о файле, и о номере строки, и о номере потока. Если был Exception — и его тоже. При этом жрёт всё это намного меньше throw.
Вместо Exception (которыйкласс, кстати) эта штука оперирует структурой ErrorInformation — чтобы не было аллокаций на ровном месте. В ней хранится сообщение об ошибке в виде mew, Exception если он был, а также CallerInformation с методом, файлом, строкой и потоком. ErrorInformation неявно преобразуется из mew и из строки, чтобы, если не хочется париться — не париться.
В качестве примера вот код E.CorrectIndexDiapasone:
public static void CorrectIndexDiapasone(ref long from_INCLUSIVE, ref long to_EXCLUSIVE, long minIncl, long maxExcl, [CallerArgumentExpression(nameof(maxExcl))] string maxExcl_name = null, [CallerArgumentExpression(nameof(minIncl))] string minIncl_name = null, [CallerArgumentExpression(nameof(to_EXCLUSIVE))] string to_EXCLUSIVE_name = null, [CallerArgumentExpression(nameof(from_INCLUSIVE))] string from_INCLUSIVE_name = null, [CallerMemberName] string callerMemberName = null, [CallerLineNumber] int callerLineNumber = -1, [CallerFilePath] string callerFilePath = null)
{
if (maxExcl < minIncl)
{
var m = maxExcl;
maxExcl = minIncl;
minIncl = m;
}
if (from_INCLUSIVE < minIncl)
{
E.rr = new mew("Стартовый индекс ", from_INCLUSIVE_name, " равен ", from_INCLUSIVE, ", а должен быть не меньше ", minIncl_name, ", то есть, не меньше ", minIncl, ". ", from_INCLUSIVE_name, " будет исправлен на ", minIncl_name, ", то есть на ", minIncl);
from_INCLUSIVE = minIncl;
}
if (to_EXCLUSIVE > maxExcl)
{
E.rr = new mew("Конечный индекс ", to_EXCLUSIVE_name, " равен ", to_EXCLUSIVE, ", а должен быть не больше ", maxExcl_name, ", то есть, не больше ", maxExcl, ". ", to_EXCLUSIVE_name, " будет исправлен на ", maxExcl_name, ", то есть на ", maxExcl);
to_EXCLUSIVE = maxExcl;
}
if (from_INCLUSIVE > to_EXCLUSIVE)
{
E.rr = new mew("Конечный индекс ", to_EXCLUSIVE_name, " меньше начального индекса ", from_INCLUSIVE_name, "(", to_EXCLUSIVE, " < ", from_INCLUSIVE, "). Индексы будут поменяны местами");
var last = to_EXCLUSIVE - 1;
var first = from_INCLUSIVE;
{
var v = last;
last = first;
first = v;
}
from_INCLUSIVE = first;
to_EXCLUSIVE = last + 1;
}
}
Да, CallerArgumentExpression прекрасен. Я тоже хочу, чтобы можно было всё это стадо передавать попроще. Типа какой‑нибудь структурой CallingInformation.
Списки без аллокаций: Vist4, Vist16 и все-все-все
Классический список List<T> выделяет память в куче. Создавать его на каждом кадре — не есть круто. Десять лет назад, впервые столкнувшись с этой проблемой, я сделал альтернативу в виде списка, хранящего первые элементы на стеке. В подсветковом софте он реинкарнировался в Vist: Vist1, Vist2, Vist4, Vist8, Vist16, Vist32, Vist64, Vist128.
Первые элементы Vist хранит как простые отдельные поля, остальное — как List<T>. Сколько именно — отражено в его названии, например, Vist4 хранит 4 таких элемента.
Под капотом обращение к элементу по индексу работает так.
Для малых индексов идёт обращение к полям через бинарный поиск на ифах (то есть у нас тут O(Log(N)) — не O(1) конечно как у Span<T>, но тоже норм), для больших индексов (т. е. которые больше, чем количество полей) — через обращение к внутреннему List<T>. Разумеется, здесь есть индексатор[] и все нормальные списковые Insert, Clear, Remove и прочие.
Пример получения первых 8 элементов из Vist8:
private T getSolidElementAt(int index)
{
#region осторожно, стадо ифов
if (index < 4)
{
if (index < 2)
{
if (index == 0)
return Item00;
else //index == 1
return Item01;
}
else //index >= 2
{
if (index == 2)
return Item02;
else //index == 3
return Item03;
}
}
else //index >= 4
{
if (index < 6)
{
if (index == 4)
return Item04;
else //index == 5
return Item05;
}
else //index >= 6
{
if (index == 6)
return Item06;
else //index == 7
return Item07;
}
}
#endregion;
}
А вот так происходит задание значения у Vist64 (осторожно, простыня)
private void setSolidElementAt(int index, T value)
{
#region осторожно, стадо ифов
if (index < 32)
{
if (index < 16)
{
if (index < 8)
{
if (index < 4)
{
if (index < 2)
{
if (index == 0)
Item00 = value;
else //index == 1
Item01 = value;
}
else //index >= 2
{
if (index == 2)
Item02 = value;
else //index == 3
Item03 = value;
}
}
else //index >= 4
{
if (index < 6)
{
if (index == 4)
Item04 = value;
else //index == 5
Item05 = value;
}
else //index >= 6
{
if (index == 6)
Item06 = value;
else //index == 7
Item07 = value;
}
}
}
else //index >= 8
{
if (index < 12)
{
if (index < 10)
{
if (index == 8)
Item08 = value;
else //index == 9
Item09 = value;
}
else //index >= 10
{
if (index == 10)
Item10 = value;
else //index == 11
Item11 = value;
}
}
else //index >= 12
{
if (index < 14)
{
if (index == 12)
Item12 = value;
else //index == 13
Item13 = value;
}
else //index >= 14
{
if (index == 14)
Item14 = value;
else //index == 15
Item15 = value;
}
}
}
}
else //index >= 16
{
if (index < 24)
{
if (index < 20)
{
if (index < 18)
{
if (index == 16)
Item16 = value;
else //index == 17
Item17 = value;
}
else //index >= 18
{
if (index == 18)
Item18 = value;
else //index == 19
Item19 = value;
}
}
else //index >= 20
{
if (index < 22)
{
if (index == 20)
Item20 = value;
else //index == 21
Item21 = value;
}
else //index >= 22
{
if (index == 22)
Item22 = value;
else //index == 23
Item23 = value;
}
}
}
else //index >= 24
{
if (index < 28)
{
if (index < 26)
{
if (index == 24)
Item24 = value;
else //index == 25
Item25 = value;
}
else //index >= 26
{
if (index == 26)
Item26 = value;
else //index == 27
Item27 = value;
}
}
else //index >= 28
{
if (index < 30)
{
if (index == 28)
Item28 = value;
else //index == 29
Item29 = value;
}
else //index >= 30
{
if (index == 30)
Item30 = value;
else //index == 31
Item31 = value;
}
}
}
}
}
else //index >= 32
{
if (index < 48)
{
if (index < 40)
{
if (index < 36)
{
if (index < 34)
{
if (index == 32)
Item32 = value;
else //index == 33
Item33 = value;
}
else //index >= 34
{
if (index == 34)
Item34 = value;
else //index == 35
Item35 = value;
}
}
else //index >= 36
{
if (index < 38)
{
if (index == 36)
Item36 = value;
else //index == 37
Item37 = value;
}
else //index >= 38
{
if (index == 38)
Item38 = value;
else //index == 39
Item39 = value;
}
}
}
else //index >= 40
{
if (index < 44)
{
if (index < 42)
{
if (index == 40)
Item40 = value;
else //index == 41
Item41 = value;
}
else //index >= 42
{
if (index == 42)
Item42 = value;
else //index == 43
Item43 = value;
}
}
else //index >= 44
{
if (index < 46)
{
if (index == 44)
Item44 = value;
else //index == 45
Item45 = value;
}
else //index >= 46
{
if (index == 46)
Item46 = value;
else //index == 47
Item47 = value;
}
}
}
}
else //index >= 48
{
if (index < 56)
{
if (index < 52)
{
if (index < 50)
{
if (index == 48)
Item48 = value;
else //index == 49
Item49 = value;
}
else //index >= 50
{
if (index == 50)
Item50 = value;
else //index == 51
Item51 = value;
}
}
else //index >= 52
{
if (index < 54)
{
if (index == 52)
Item52 = value;
else //index == 53
Item53 = value;
}
else //index >= 54
{
if (index == 54)
Item54 = value;
else //index == 55
Item55 = value;
}
}
}
else //index >= 56
{
if (index < 60)
{
if (index < 58)
{
if (index == 56)
Item56 = value; //786
else //index == 57
Item57 = value;
}
else //index >= 58
{
if (index == 58)
Item58 = value;
else //index == 59
Item59 = value;
}
}
else //index >= 60
{
if (index < 62)
{
if (index == 60)
Item60 = value;
else //index == 61
Item61 = value;
}
else //index >= 62
{
if (index == 62)
Item62 = value;
else //index == 63
Item63 = value;
}
}
}
}
}
#endregion;
}
Vist128 выкладывать сюда не буду, я думаю, суть и так ясна :)
Если у меня на каждый кадр выполняется код, где в список в типовом случае набивается меньше 16 элементов, я спокойно юзаю вместо List<T> какой‑нибудь Vist16<T> — и аллокаций не будет. А если уж случится, что добавится 16, 17 и более элемент — не проблема, оно будет работать, только уже с аллокациями.
Поскольку это не ref struct, а именно struct, его можно хранить в полях классов и структур. И да — можно легко устроить себе багоцирк. Ну то есть, например:
private Vist32<Type> myItems;
//Может быть весело
public void SetItems(Vist32<Type> newItems) => myItems = newItems;
//А вот так норм
public void SetItems_without_bug(Vist32<Type> newItems) => myItems = newItems.Clone();
у нас первые N значений копируются, а хвостик, если он есть — передаётся по ссылке. И при добавлении/удалении элементов в условиях, когда элементов будет больше 32 (для Vist32), будет не весело, а ОЧЕНЬ весело.
Ещё важно помнить про небезграничность стека — впрочем, как с любыми другими штуками, использующими его. Поэтому перед созданием Vist64 и Vist128 следует хорошо подумать, а перед написанием рекурсии с участием сабжа — очень хорошо подумать. В общем, Vist — штука годная, но коварная, с ней надо осторожнее. Но мне норм.
У тех, кто знаком с C++, при виде этого могут зачесаться рекурсивные шаблоны. У меня тоже чешутся. Но ирония в том, что в C++ вообще смысла в этом огороде нет, ибо там изначальной проблемы не возникает.
Атомарно-атомный atomic_bool
Значимый тип. Ведёт себя как bool и неявно с ним конвертируется. Занимает не 1, а 4 байта. И главное — поддерживает атомарные операции, Exchange, например.
У меня были мысли воткнуть в C# полноценный std::atomic<T>, но пока я ограничился только bool. Самый важный метод atomic_bool — это SetInterlockedAndGetPrevous(bool newValue).
Пример:
atomic_bool _isDisposed = false;
public bool IsDisposed => _isDisposed;
public void Dispose()
{
if (IsDisposed || _isDisposed.SetInterlockedAndGetPrevous(true))
return;
//Делаем дело
}
Безопасный DisposableExtended
Этот базовый класс заменяет обычный интерфейс IDisposable для штук, которые нужно выгружать вручную.
Даёт возможность узнать, выгружен ли объект (IsDisposed)
Гарантирует, что Dispose будет вызван только 1 раз
Позволяет сообщить себе о начале и конце использования, и гарантирует, что во время этого использования его точно никто не выгрузит
Есть блокировка выгрузки — заблокировал выгрузку, получил ключ. Хочешь разблокировать — сообщи этот ключ обратно
Всякие события вроде BeforeDispose и AfterDispose
Для снижения вероятности сбоя может выгружаться не сразу. После Dispose помечает себя выгруженным (IsDisposed) и заставляет алгоритмы ПО себя игнорировать (если они написаны правильно), а реальная выгрузка происходит через несколько секунд.
Например,у нас обрабатывается видео. Кадр за кадром. Вот картинке‑кадру вызвали Dispose, но она успела в другом потоке попасть в какой‑то эффект и он её пару раз обработал. Ничего страшного: ведь реальная выгрузка произойдёт позже. Если же такое поведение мешает (например, речь об освобождении физического устройства), то его можно отключить. Когда одновременно происходит много вот таких вот отложенных выгрузок — время ожидания автоматически сокращается в целях экономии ресурсов, вплоть до мгновенной выгрузки.
Всё это с умной поддержкой многопоточности. Мы тут будем активно юзать ручное управление памятью и обмазываться всякими шейдерами, софт будет сложный — поэтому такая штука очень сильно облегчит нам жизнь.
Вызов Dispose не более одного раза гарантируется без локов с помощью atomic_bool.
Например, у меня часто используются буферы памяти. Подход к работе с ними примерно такой:
public void Process(Booffer buf)
{
//Проверяем на null и что IsDisposed == false
if (E.IsInvalid(buf))
return;
//Блокируем выгрузку
using var ds = buf.DisposeProtectedScope;
if (ds.IsFailed())
return; //Если кто-то хитрый в другом потоке всё-таки успел
//Всё. Теперь buf точно никто не выгрузит, пока ds существует
//При этом параллельно buf может использовать несколько потоков
//Если кто-то вызовет Dispose, оно будет ждать завершения всех, кто
//захватил DisposeProtectedScope
}
Есть ещё один механизм блокировки. Работает он не по принципу «падажи я с ним работаю», как вышеописанный DisposeProtectedScope, а по принципу «выгрузка вообще запрещена, гуляй Вася». Вот допустим у нас публичный синглтоновый синглтон:
public static Booffer Shared { get; }
как сделать чтобы его вообще нельзя было выгружать? А просто:
static SingleToneBoofferClass()
{
//Создаём буфер
Shared = new(ElementCount);
//Лочим
var token = Shared.LockDispose();
//Теперь если какой-нибудь умник
//вызовет Shared.Dispose()
//то огребёт ошибку E.rr, и выгрузка
//произведена не будет
//разблокировать это дело можно легко:
//Shared.UnlockDispose(token);
//если токенов несколько, блокировка
//будет работать пока все до единого
//не разблокируются
}
для краткости можно делать так:
//Undisposable сам лочит Dispose и хранит в себе токен
static readonly Undisposable<Booffer> shared = new(new(ElementCount));
public static Booffer Shared => shared.Value;
//Пока Undisposable не выгружен, его Value нельзя задиспосить - будет E.rr
или, если разблокировка вообще никогда не нужна, можно явно потерять ключ:
public static Booffer Shared { get; } =
new(ElementCount).LockDisposeAndReturnThis(out _);
//out _ явно показывает, что мы сознательно выбрасываем ключ
//Всё, залочились и потеряли ключ. Теперь мы невыгружаемые
Поскольку мой софт нафарширован с ног до головы буферами неуправляемой памяти, LUT‑таблицами и unsafe кодом везде где только можно и нельзя, подобная штука очень упрощает жизнь и позволяет избежать большинства потенциальных багов.
Значимый список на памяти: Mist4, Mist8, Mist32 и прочие
Mist — Memory List, а не туман.
Cписок значений наподобие Vist, но только для неуправляемых типов. В отличие от Vist, число в названии указывает не количество элементов, хранимых на стеке, а количество байт, выделяемых на стеке под эти элементы.
Всё, что больше — хранится в куче. Поскольку типы только неуправляемые, скорость поиска в стеке не O(Log2(N)), как у Vist, а O(1) — это и есть его основное преимущество. Ну и плюс мы сразу видим, сколько оно сожрёт памяти, и с меньшей вероятностью скушаем весь стек.
Разумеется, если хочется, в кучу его запихнуть можно — он же не ref struct.
Быстрочистый ConcurrentBagSlim<T>
Однажды оригинальный ConcurrentBag<T> задолбал меня своей производительностью и слишком агрессивными аллокациями. Он как‑бы хорошо подходит, чтобы делать разные пулы для избегания аллокаций, но он сам аллоцирует и этим руинит всю идею. Поэтому я родил свой, в котором эти проблемы пофиксил.
ConcurrentBagSlim<T> — аналог ConcurrentBag<T> только лучше. Есть минус — поддерживает только ссылочные значения. Его, конечно, можно допилить до значимых типов с отдельной работой с 4-байтными и 8-байтными значениями, и со всеми остальными, но мне и так норм.
Смысл простой. Внутри себя эта штука держит голову и тело:
readonly T[] fast_head = new T[FastItemCount]; //Голова
ConcurrentBag<T>? slow_body_bag = null; //Тело
по возможности она старается работать головой‑массивом, но если не получается — телом, которое уже обычный ConcurrentBag. Добавление выглядит так:
public void Add(T value)
{
//для null тупо увеличиваем счётчик
if (object.ReferenceEquals(value, null))
while (true)
{
var nic = nullCount;
if (nic < int.MaxValue)
{
if (Interlocked.CompareExchange(ref nic, nic + 1, nic) == nic)
return;
}
else
{
E.rr = $"В {nameof(ConcurrentBagSlim<T>)} добавлено слишком много значений null";
return;
}
}
//пытаемся атомарно сохранить элемент в голову
for (int i = 0; i < FastItemCount; i++)
if (object.ReferenceEquals(fast_head[i], null))
if (Interlocked.CompareExchange(ref fast_head[i], value, null) == null)
return;
//Если не получилось - надо класть в тело
if (slow_body_bag == null) //Создаём его если надо
Interlocked.CompareExchange(ref slow_body_bag, new ConcurrentBag<T>(), null);
//Пихаем в тело
slow_body_bag.Add(value);
}
для null значений она тупо увеличивает счётчик. В остальных случаях она сначала пытается найти свободное место в массиве и атомарно сохранить туда значение. И только если в голове свободного места не нашлось она пихает значение в тело, создавая его при необходимости.
Получение элементов выглядит так:
public bool TryTake(out T result)
{
//Если null-значения есть - первым делом плюёмся ими
if (nullCount > 0)
while (true)
{
var nilCount = nullCount;
if (nilCount > 0)
{
if (Interlocked.CompareExchange(ref nullCount, nilCount - 1, nilCount) == nilCount)
{
result = null;
return true;
}
}
else
break;
}
//Пытаемся атомарно поискать что-нибудь в голове
//вдруг там есть что-нибудь интересное
for (int i = 0; i < FastItemCount; i++)
{
if (!ReferenceEquals(fast_head[i], null))
{
var prevous = Interlocked.Exchange(ref fast_head[i], null);
if (prevous != null)
{
result = prevous;
return true;
}
}
}
//Если не нашли - смотрим, есть ли у нас тело
if (slow_body_bag != null) //Если есть - пинаем его
return slow_body_bag.TryTake(out result);
//Ничего не нашли
result = null;
return false;
}
поскольку ConcurrentBag по определению неупорядочен, мы вольны выдавать элементы в произвольном порядке. Поэтому первым делом выплёвываем nullы, если они у нас есть. Далее пробегаемся по массиву‑голове, и если находим там что‑нибудь, пытаемся это атомарно выковырять. Если не получается — проверяем, есть ли тело — обычный ConcurrentBag. Если есть, пытаемся докопаться до него.
В итоге в подавляющем большинстве случаев будет использоваться атомарная работа с массивом, и только изредка всё будет уходить в ConcurrentBag. Разумеется, при правильном использовании.
Точный BeautifulTimer
Это высокоточный таймер, завязанный на низкоуровневые механизмы ОС. Дело в том, что если нам нужно делать что-то ровно 30 - 100 раз в секунду, то с помощью обычных таймеров или while (true) Sleep(10) добиться таких интервалов не получится.
Многозадачная ОС квантует-дробит выполнение кода программ на куски, которые обычно равны 15 мс (хотя всё может отличаться, а где-то может настраиваться). Поэтому, например, если интерфейсовому таймеру поставить интервал 1 мс, он всё равно будет срабатывать где-то 60 раз в секунду.
Когда мне нужна была ОЧЕНЬ большая точность, я обходил проблему в лоб, полностью утилизируя под это целое 1 ядро процессора, пуская в нём бесконечный цикл. Есть конечно, вариант с ОС реального времени или Windows RTX (это не который рейтрейтинг, а который превращает Windows в ОС реального времени), но тогда возможности это сделать не было.
Когда ОЧЕНЬ большая точность не нужна, а достаточно просто высокой точности, делается всё по-другому. Нам тут достаточно, для 30-60 обработок в секунду. Для этого в Windows есть мультимедийные таймеры - именно их используют в софте для работы с видео и звуком. Цепляются к ним с помощью вот этих ребят:
[DllImport("winmm.dll", SetLastError = true)]
private static extern uint timeBeginPeriod(uint uPeriod);
[DllImport("winmm.dll", SetLastError = true)]
private static extern uint timeEndPeriod(uint uPeriod);
[DllImport("winmm.dll", SetLastError = true)]
private static extern uint timeGetTime();
[DllImport("winmm.dll")]
private static extern int timeSetEvent(
int msDelay, int msResolution, TimerEventHandler handler, IntPtr userCtx, int eventType);
[DllImport("winmm.dll")]
private static extern int timeKillEvent(int timerId);
Собственно, BeautifulTimer — это двуслойная обёртка над вот этим всем. Причём, он не просто оборачивает работу с API, но и следит за сбоями, и при таковых автоматически пересоздаёт внутренний таймер. Почему эти сбои возникают я так и не разобрался, поэтому и сделал его самовосстанавливающимся.
Кнутовский турбохэшер 64
Недетерминированность метода GetHashCode бесит и часто мешает делать всякие хорошие ништяки, например, генерацию настройки эффекта по сиду. Поэтому я по‑быстрому запилил свой хэшер, работающий по алгоритму Кнута на 64 битах.
Хэшер абсолютно детерминирован — одинаковые данные всегда дают одинаковый результат. Это одна из первых штук в этом софте, написана она была ещё на .NET 6
var hasher = new KnythHasher();
hasher.Append(value);
var hash64unsigned = hasher.GetHashCodeULong();
var hash32signed = hasher.GetHashCode();
Стандартным типам (числа + bool + строки + DateTime/TimeSpan) добавлены методы расширения GetKnythHashCode и GetKnythHashCodeULong. Для строк оно обрабатывает сами символы, также поддерживаются всякие Span<T> и вот это всё. Все эти вызовы стекаются к центральному методу, который, собственно, сурово считает хэш:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void Append(void* beginPointer, long byteCount)
{
if (!AllowUnalignedMemory)
{
if ((nint)beginPointer % sizeof(ulong) != 0)
throw new Exception("Указатель должен быть выровнен по 8 байтам");
}
if (!inited)
{
hashValue = initValue;
inited = true;
}
if (beginPointer == (void*)0 || byteCount <= 0)
return;
unchecked
{
//стараемся изо всех сил уничтожить strict aliasing
//и туго надругаться над его трупом
if (byteCount <= 8)
{
ulong value = 0;
if (byteCount == 4) //4 байта встречается чаще всего
{
*(int*)&value = *(int*)beginPointer;
hashValue += value;
hashValue *= iterationValue;
return;
}
else if (byteCount == 1) //1 байт чуть реже
{
*(byte*)&value = *(byte*)beginPointer;
hashValue += value;
hashValue *= iterationValue;
return;
}
else if (byteCount == 8) //8 байт еще реже
{
*(long*)&value = *(long*)beginPointer;
hashValue += value;
hashValue *= iterationValue;
return;
}
else if (byteCount == 2) //2 байта совсем редкость
{
*(short*)&value = *(short*)beginPointer;
hashValue += value;
hashValue *= iterationValue;
return;
}
//здесь НЕ должно быть return
}
else if (byteCount == 16) //decimalы, int4, float4
{
ulong value = 0;
*(long*)&value = *(long*)beginPointer;
hashValue += value;
hashValue *= iterationValue;
*(long*)&value = *((long*)beginPointer + 1);
hashValue += value;
hashValue *= iterationValue;
return;
}
else if (byteCount == 32) //decimal2, long4 и прочие
{
ulong value = 0;
value = *(ulong*)beginPointer;
hashValue += value;
hashValue *= iterationValue;
value = *((ulong*)beginPointer + 1);
hashValue += value;
hashValue *= iterationValue;
value = *((ulong*)beginPointer + 2);
hashValue += value;
hashValue *= iterationValue;
value = *((ulong*)beginPointer + 3);
hashValue += value;
hashValue *= iterationValue;
return;
}
byte* begin = (byte*)beginPointer;
byte* end = begin + byteCount;
long ulongCount = byteCount / sizeof(ulong);
long ulongCount4 = ulongCount & ~0b11;
ulong* ulongPtr = (ulong*)begin;
ulong* ulongEnd = ulongPtr + ulongCount;
ulong* ulongEnd4 = ulongPtr + ulongCount4;
while (ulongPtr < ulongEnd4)
{
hashValue += *ulongPtr++; hashValue *= iterationValue;
hashValue += *ulongPtr++; hashValue *= iterationValue;
hashValue += *ulongPtr++; hashValue *= iterationValue;
hashValue += *ulongPtr++; hashValue *= iterationValue;
}
while (ulongPtr < ulongEnd)
{
hashValue += *ulongPtr++;
hashValue *= iterationValue;
}
byte* ptr = (byte*)ulongPtr;
while (ptr < end)
{
hashValue += *ptr++;
hashValue *= iterationValue;
}
}
}
Ну то есть ему дают кусок памяти, и он всегда жуёт её как набор ulongов, ему без разницы, что там изначально хранилось (да, в сторону SIMD я тоже смотрел, но пока этого хватает). Если есть хвостик — закусывает им.
И да — так плохо делать, никогда так не пишите.
Усатый Funchacho
Подход «реже аллоцируй @ жри памяти сколько влезет» порождает большую потребность в ленивых функциях. Их есть у меня — это Funchacho<A, R>. Разумеется, оно поддерживает многопоточное использование.
Пример:
static readonly Funchacho<(int x, int y), int> mul =>
new Funchacho<(int x, int y), int>(
args =>
{
return args.x * args.y;
});
далее просто вызываем
int pixelCount = mul[(width, height)];
//от внутренних скобок можно избавиться
//если сделать 100500 разновидностей Funchacho под разное
//число аргументов, т.к. MyType<params T...> в C# ещё не завезли
//и да, я очень хотел перегрузить круглые скобки, но это не C++ :)
//было бы не круто, но круто:
int pixelCount = mul(width, height); //и никто даже не подозревает...
Разумеется, умножение кешировать смысла нет. А двумерные буферы - есть:
static Funchacho<(int x, int y), Booffer2D<Color>> buffers =
new(
static size =>
new Booffer2D<Color>(size).LockDisposeAndReturnThis(out _)
);
Далее просто 60 раз в секунду берём нужный буфер:
var buf = buffers[(width, height)];
//что-то делаем
и для данного размера он будет создан только 1 раз. Funchacho — штука умная. Она может кешировать результат сразу, а может только при повторении. То есть, если за последние N вызовов такие аргументы уже были — значит, добавляем значение в кеш. Единичные случаи кешировать не будем. Ещё есть возможность следить за тем, что значения в кеше устарели или стали невалидными, и пересчитывать их при необходимости.
Ещё есть FunchachoHead — это тот же Funchacho, но не инкапсулирующий сам метод обработки. Кэш короче говоря. Кеширует значения только при повторении, хотя это можно отключить. Используется так:
static readonly FunchachoHead<(int x, int y), int> mulHead = new();
public static int Mul(int X, int Y)
{
if (mulHead.TryGetCachedValue((X, Y), out var result))
return result;
result = X * Y;
mulHead.AddIfNeedCache((X, Y), result);
return result;
}
Построен Funchacho вокруг не ConcurrentDictionary, а RWDictionary, который, в свою очередь, является стандартной реализацией RWLockDictionaryWrapper, который нацепляет на любой IDictionary многопоточное использование через ReaderWriterLockSlim.
RWDictionary реализует RWLockDictionaryWrapper вокруг самого обычного Dictionary.
RWDictionary тут предпочтительнее потому, что ConcurrentDictionary заточен под и частые чтения, и частую запись из разных потоков, а у нас тут, в основном, одни чтения. И RWDictionary работает в таких условиях гораздо проворнее, чем тяжёлый ConcurrentDictionary.
Сами RWLockDictionaryWrapper/RWDictionary довольно тривиальны, поэтому описывать здесь их не буду.
Экспериментальные пепекторы
У нас тут всякие штуки с цветами и пикселями. В таких делах удобно работать не с числами, а с 2D, 3D и 4D векторами. Умножил вектор, обозначающий цвет — увеличил яркость, поделил координаты на размер — получил относительные координаты, и всё в таком духе.

В языке C# для этого предлагаются разные готовые Vector4 и прочие Vector256<float>. ИМХО — слишком громоздкие и неудобные. Серьёзно, конструкции вида
Vector<int> a = new Vector<int>(5);
a *= new Vector<int>(2);
выглядят не очень. Гораздо удобнее что‑то а‑ля HLSL:
int4 a = 5;
a *= 2;
Поэтому, в рамках эксперимента, я решил добавить каждому стандартному типу C# — вот этим всем bool, int, float, decimal и прочим — векторные разновидности. Двумерные, трёхмерные и четырёхмерные. Еще до кучи добавить типы с фиксированной запятой и единицы измерения углов, и им всем тоже раздать векторные подвиды. И посмотреть, что получится бенчмарк компилятора.

То есть, помимо decimal, float, int и прочих, в C# появляются decimal3, float2, int4, Half3, degreeds2, radians3, turns и прочие штуки. И вишенкой на торт — ещё два универсальных типа. В исходники компилятора C# я решил пока не лезть и просто сделал типы структурами.

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

В библиотеке есть куча флагов компиляции на все случаи жизни, можно даже скомпилировать её не под .NET, а под старый .NET Framework. Можно включить совместимость с GDI, WinForms, WPF и SharpDX — тогда соответствующие векторы будут неявно конвертироваться в цвета, размеры и координаты из этих библиотек.
var brushGDI = new SolidBrush(byte4.Gold);
float4 myColor = float4.Green * 0.2f;
var brushWPF = new SolidColorBrush(myColor);
int2 sz = (200,50);
btnApply.Size = sz;
То есть можно спокойно создавать кисти из цветов float4 и byte4, а с помощью int2 и double2 задавать размеры контролов. SIMD поддерживается тоже — опционально включается соответствующими флагами компиляции.
Итак, бестиарий:
Статический класс Pepe для математики - местный аналог System.Math
Статический класс PepeHelper хранит информацию о типах, чтобы, например, понять, что float4 это float и 4
Углы: turns, radians и degreeds. Неявно превращаются друг в друга
degreeds angle = 90;
var value = Pepe.Sin(angle); //градусы сконвертятся в радианы
angle = Pepe.Arcsin(value); //снова всё ок
angle += Pepe.Pi; //Всё ок
Местная константа Pepe.Pi_Pi_Pi тут имеет тип radians3, например.
Числа с фиксированной запятой: fxdsbyte, fxdshort, fxdint, fxdlong. В части случаев работают быстрее плавающей запятой, хорошо пихать в контроллеры без поддержки оной. Некоторое время меня преследовали мысли сделать расчёты на fxdint, а не float, но я оказался быстрее. Пока что из них в библиотеке оставил fxdlong, остальные вырезал за ненадобностью
Векторные типы. Теперь основное: для bool и всех числовых типов C#, а также для новоиспечённых, делаем векторные разновидности: int4, float2, decimal4, bool3, sbyte4, nuint3, degreeds4, fxdsbyte2, Half3 и т. д.
Ключевое: инициализация скалярами и кортежами; умножение, деление, сложение и вычитание работает попарно для компонентов.
float3 a = (1, 2, 3);
double3 b = (0.1, 0.2, 0.3);
ulong3 c = 3; //(3, 3, 3);
double3 d = (1.23, -0.23, 1000);
double3 result = (a + b) * c / d;
float4 a = 1.0f; //Получился вектор 1 1 1 1
float3 b = (2, 5f, 2); //Можно инициализировать кортежем
nint2 numbers = (1, 2); //Поддержку nint можно отключить опцией компиляции
var (alpha, beta) = numbers; //Деконструкция
b.zxy = a.rgg * 5.0f; //Свизлинг - переставляем значения
bool4 horosho = (true, false, true, true);
a *= horosho; //true как 1, false как 0
decimal2 money = (123m, 456m) + (decimal2)b.rr;
degreeds4 angle_deg = 90;
radians4 angle = angle_deg;
a /= angle.кззз; //xyzw rgba кзса
float4 someValue = 5;
someValue.xyz = (1, 2, 3); //это всё
someValue.rgb = (1, 2, 3); //одно и
someValue.кзс = (1, 2, 3); //то же
a.gba.xy.BitsAsLong = 0; //BitsAs… - штука для шаловливых хаков
int2[] cords = [(0, 0), (0, 1), (-1, 2)]; //массив координат
int2.InterlockedIncrement(ref cords[1]); //атомарные операции
float2 p = float2.InterlockedExchange(ref a.rg, 5);
bool4 bb = bool4.InterlockedExchange(ref horosho, true);
Свизлинг — штука полезная. Для того, чтобы он работал, каждый пепектор имеет все возможные комбинации своих компонентов: xyzw, zzwy, xzzw и прочие. Комбинации, порядок которых совпадает с расположением компонентов в памяти, являются полями, остальные сделаны свойствами.
Универсальный динамический pepector хранит любое из вышеперечисленного. Местный аналог dynamic, но не ссылочный, а значимый — никаких лишних аллокаций. Автоконвертация с любым скаляром и вектором, может менять свои тип и размерность во время выполнения. Умеет умно сжиматься, умеет в математику, добавление/удаление значений, и ещё кучу всего.
pepector a = 1, b = (3, 5, 6, 2); //скаляр и 4D, оба инты
b *= 0.3; //b посчитался и поменял тип на double
a += b; //a теперь тоже стал double4
a.xzyy = 15m; //а теперь мы decimal4
var (va, lu, es) = a.bgrr; //деконструкция
a.ElementType = pepector_type.t_float; //Конвертируйся
//Теперь кукожим пепектор, чтобы поместилось больше чисел
b.Compact(); //После кукожинга b теперь стал byte. Пепектор понял, что значения в нём спокойно помещаются в диапазон типа, который занимает меньше места - byte
При инициализации pepector хитро выбирает тип элементов. Старается использовать популярные int и double, но, если надо, применяет менее популярные типы.
//Это всё intы
pepector numbers1 = (1, 2, 3, 4, 5, 6, 7, 8, 10);
//А это уже shortы
pepector numbers2 = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
//Пепектор умно выбрал свой тип - short
//так как элементов слишком много для int
//но диапазон конкретно этих значений позволяет
//использовать short

В 64-байта пепектора гарантированно помещается тип значений, их количество и до четырёх значений любых поддерживаемых типов, в том числе четыре 16-байтных decimal. Можно и больше — но тут уж всё зависит от размера самого типа. boolов влезет много, а вот longов — не очень.
universal<T> — универсальное значение, как пепектор, только статический вариант. Вместо T можно подставить всё вышеперечисленное. Поддерживает операторы и прочее, благодаря чему облегчает написание универсальных математических методов и классов, для чего и создан. Написан так, чтобы после JIT‑компиляции в Release скорость была близка к нативной.
PepeHelper является мини‑бд, хранящей информацию обо всех типах и позволяющий уточнять нюансы: IsInteger, IsVector, RealElementType и ElementType, Name, AliasKeyword, RadiansInOneUnit, ElementSizeInBytes, CanBeNegative и ещё куча всего.
Пепекторы — одна из первых библиотек в этом софте, делал я её ещё в 2022 году. Для того, чтобы родить эту библиотеку, я написал с нуля генератор на основе StringBuilder. Рослин мне не понравился своей громоздкостью, а Т4 — отсутствием подсветки, автокомплитов и прочего. Фишкой моего генератора стало то, что структура кода генерации схожа со структурой самого генерируемого кода — это очень облегчило работу. Тем не менее, код генерации пепекторов получился тоже немаленький — более 15к строк.
Стоит отметить — если бы в C# были шаблоны и/или наследование структур, то вот это вот всё было бы сильно худее миллиона строк. Да даже если б кортежи, состоящие из типов, поддерживающих математику (типа (float x, float y) my_var), наследовали бы эту математику — было бы сильно легче. Или лучше — если кортеж состоит из объектов, имеющих метод или оператор X, то метод или оператор X можно применять ко всему кортежу. Утиная типизация.
Подробнее про некоторые нюансы и возможности
Векторы
Векторы мутабельны. Иммутабельные были бы быстрее, но в реальности при практическом использовании это очень неудобно. Использовать иммутабельность в данном случае - это примерно как танцевать в хоккейной защите, чтобы если упадёшь не было синяка
Векторы есть не только для всех стандартных числовых типов, включая nint, nuint, decimal, sbyte, ushort и прочих, но и для Half
Векторы для nint, nuint и Half можно отключить опцией компиляции
Значения вектора xyzw rgba кзса являются полями, выровненными вручную с помощью FieldOffset
Пепекторы можно инициализировать с помощью кортежей и деконструировать:
fxdlong3 values = (1, 2, 3);
var (x, y, z) = values;
Разумеется, поддерживаются индексаторы[]. Индексаторы берут остаток отделения индекса на длину вектора, поэтому можно спокойно выходить за границы диапазона — вектор будет читаться по кругу
Свизлинг (комбинация xyzw rgba кзса в любом порядке) сделан через реализацию свойств с именами из всех возможных комбинаций букв включая повторения. В тех случаях, где порядок букв совпадает с оригинальным порядком в памяти, используется не свойство, а поле с указанием отступа в памяти. Пример:
[Д(И), Ч(Ь)] public byte2 yx { get => (y, x); set => (y, x) = value; }
[Д(И), Ч(Ь)] public byte2 yy { get => (y, y); set => (y, y) = value; }
[Д(И), Ч(Ь)] [FieldOffset(sizeof(byte) * 1)] public byte2 yz;
у четырёхмерных векторов количество этих вариантов больше 1000.
Была проблема: если в VisualStudio попытаться вызвать автокомплит вектору, то она честно попытается выдать вот это всё, и список станет неюзабельным. Чтобы этого не происходило, свизлинговые комбинации исключены из показа в дебаге и в автокомплите с помощью [DebuggerBrowsable] и [EditorBrowsable]. Чтобы эти длинные имена не повторялись кучу раз в каждом пепекторе, я сократил их до одной буквы:
using Д = System.Diagnostics.DebuggerBrowsableAttribute;
using Ч = System.ComponentModel.EditorBrowsableAttribute;
и
const DebuggerBrowsableState И = DebuggerBrowsableState.Never;
const EditorBrowsableState Ь = EditorBrowsableState.Never;
поэтому вместо
[DebuggerBrowsable(DebuggerBrowsableState.Never), EditorBrowsableState(EditorBrowsableState.Never)]
я пишу коротко и ясно:
[Д(И), Ч(Ь)]
Методы WithX, WithY, WithZ, WithW имеют также толстые разновидности, типа WithXYZ и WithZW
Некоторые трёхмерные и четырёхмерные вектора имеют плюшки для работы с цветом. В частности, в них есть куча статических стандартных цветов, типа byte4.Aquamarine и всё такое. Они есть даже у decimal3 и fxdlong4. Также такие вектора располагают удобным поиском цвета по названию, например, можно написать float3.ColorByRusName[“Бежевый”] или double4.RusNameByColor(myColor). Поиск, кстати, нечуствителен к регистру. Есть методы конвертации с другими цветовыми пространствами, например, статический метод float3.RGBtoHSL или свойство Hue { get; set; } у double3 (на деле происходит чтение/изменение rgb с конвертацией)
Несмотря на наличие глобального класса пепематики, каждый вектор имеет небольшой набор своих статических математических методов, типа byte4.Avg, decimal2.DistancePow2, long4.DotProduct или int3.Div. Само собой, нестатические Increment/Decrement в наличии
У векторов есть свойство Length, которое можно читать и менять, работая с длиной вектора, не меняя его направление
У 2D векторов есть свойство Angle которые можно читать и менять, работая с углом вектора не меняя его длину. Angle имеет тип radians
int2 неявно конвертируется в обе стороны с Size, и в Point. float2 неявно конвертируется в обе стороны с SizeF и PointF. Да, мне не нравится разделение на Point и Size. Кроме того, все 2D векторы имеют поля WidthOrLeft и HeightOrTop, как бы намекая, что интерпретировать их x и y можно как угодно
Аналогичное есть у double2 для Point и Size из WPF
double4, double3, byte4, byte3, float4, float3 автоматом конвертируются с цветами из разных библиотек. В WPF можно использовать double4 как Thickness и CornerRadius
У 3D векторов тоже есть Angle, который имеет тип radians3 — три угла в каждой из плоскостей
4D векторы я использую как координаты прямоугольников. x и y считаются за координаты верхнего левого угла, z и w за координаты нижнего правого. У этих векторов есть свойства Size, Width и Height, а также поля Left, Top, Right и Bottom. Причём эти поля есть даже у bool4 — ну а вдруг мы будем хранить настройки видимости какой‑нибудь рамки у кнопки в bool4. Также имеется стадо методов и свойств (не только get, но set) для прямоугольниковых дел. Имена у них начинаются на Rect...:
RectLeftHalf
RectRightHalf
RectTopHalf
RectBottomHalf
RectPush
RectIsIntersect
RectIsContainsPoint
RectPushProportional
RectPlaceProportionalInside
RectPlaceProportionalOutside
RectCenter
RectGetMovedInside
RectFlipHorisontal
и прочие. И они есть для всех типов векторов, не только для int4, double4 и float4. Да‑да, я тоже радуюсь, что в C# нет наследования структур от структур. Можно конечно впихнуть их в субполе, чтобы было не value.RectCenter, а place.Rect.Center, но пока руки не дошли. Нет, интерфейсы нельзя, потому что боксинг.
Преобразование в строку всех векторов делается хитрым способом для минимизации аллокаций. Есть даже возможность преобразовать в набор char по неуправляемому указателю. По умолчанию значения вектора разделяются точкой с запятой, но если надо, разделитель можно указать свой. Доходит до того, что некоторые преобразования делаются буквально руками и по табличкам (__vectorHelperClass.cs). Само собой, IFormatProvider поддерживается
Парсинг тоже хитрый и старается избегать аллокаций. И да — ему можно скормить неуправляемый указатель на char. NumberFormat и IFormatProvider поддерживаются
У векторов есть методы для копирования в/из массива
Метод Equals у векторов может жрать очень большое число типов и корректно сравниваться с ними. То есть float3 корректно будет сравниваться с sbyte3, например
Даже у статических векторов есть небольшой набор методов для работы с ними как с набором значений, например, FindIndex, Contains или ContainsIntersections. Поскольку длина статического вектора заранее известна и она маленькая, эти методы реализованы без циклов
Основной метод подсчета хэша в векторов — GetHashCodeULong, который считает 64-битный хэш по Кнуту. Максимально быстро, без циклов: один или несколько раз берёт 64 бита вектора + хвост. А GetHashCode это просто GetHashCodeULong().GetHashCode()
Все типы, занимающие 32 или 64 бита, поддерживают атомарные операции. К ним относятся, например, bool4, float2, Half4, byte4, int2, sbyte4 и прочие. Можно, например, хранить цвет в Half4 и атомарно его менять методом Half4.InterlockedExchange, или использовать в какой‑нибудь задаче ushort4.InterlockedIncrement
Есть статические методы IsInDiapasone, проверяющие нахождение элементов вектора в указанных границах. Эти методы возвращают векторный bool соответствующей размерности. То есть int3.IsInDiapasone(int3 min, int3 max); вернёт bool3, каждый элемент которого указывает, находится ли соответствующее значение в соответствующем диапазоне. Есть разновидности, которые попутно могут на русском языке (через ref/out string) расписать, какие именно члены вектора вышли из диапазона и как, типа: «Значение x равно 3, а это больше 6; значение y равно 10 а это меньше 111» — это удобно использовать для точного логирования
-
Явная/неявная конвертация векторов между собой происходит по следующим принципам:
Конвертация между разными типами векторов аналогична конвертации между скалярами: если скаляры конвертируются явно, то и векторы явно, и наоборот. Например, int2 во float2 преобразуется неявно, а вот обратное преобразование float2 в int2 потребует явного приведения типа: int2 a = (int2)float2_value;
-
Конвертация между размерностями строится по простому правилу: если происходит потеря информации, то требуется явное преобразование. То есть большая размерность в меньшую требует явного преобразования, так как информация теряется. Например, ulong4 в ulong3 требует явно указать приведение. А вот если мы повышаем размерность — потери информации не происходит, поэтому такое преобразование происходит неявно. При этом новые компоненты будут равны нулю (ну или false для булов)
Исключение — неявное преобразование скаляра в вектор. В этом случае все компоненты вектора будут равны скаляру, какого бы размера он ни был
pepector
pepector — один из самых больших и сложных типов, который динамически конвертируется со всеми скалярами и векторами, включая nint, углы и boolы
Тип элементов задаётся енумовским свойством ElementType. Если его поменять, пепектор автоматом конвертирует значения. Для соотнесения значений енума pepector_type и типов C# в пепекторе предусмотрены методы:
pepector_type GetPepectorType(this Type t);
Type ToType(this pepector_type type);
а также куча бонусной мишуры вида
pepector GetMinValue(this pepector_type t);
IsAngleUnit(this pepector_type t);
IsFloatingPoint(this pepector_type t);
IsFixedPoint(this pepector_type t);
GetElementSizeInBytes(this pepector_type t);
и прочие. Мини PepeHelper короче.
Количество элементов можно узнать или задать свойством Count
Многие методы и алгоритмы пепектора довольно длинные и нафаршированы ифами, так как все проверки я старался выносить за пределы циклов. Циклы в большинстве случаев работают на небезопасных указателях — для скорости
if (a.ElementType == pepector_type.t_float)
{
float* ptr = (float*)&a;
float* end = ptr + a.Count;
int i = 0;
while (ptr < end)
{
*ptr = Pepe.Round(*ptr, decimalCount.GetAndConvertValueAt<int>(i++));
ptr++;
}
}
else if (a.ElementType == pepector_type.t_double || a.IsAngleUnit)
{
…
Пепектор тоже имеет свизлиговые xyzw rgba кзса. Все они имеют тип pepector. Возвращают/задают пепектор соответствующей длины и равны комбинации нулевого, первого, второго и третьего элементов пепектора. Если пепектор слишком короткий, то при чтении вернётся 0 того же типа, что пепектор, при записи длина пепектора увеличится
Пепектор поддерживает индексатор[]. Результатом будет пепектор единичной длины, в котором лежит значение соответствующего номера
Пепектор поддерживает енумерацию, то есть ему можно делать foreach
У пепектора тоже есть хаковые BitsAs… свойства, причём на все существующие типы
Пепектор можно юзать как список с помощью методов TryAdd<T>, TryInsert<T>, TryRemoveAt и Clear
У пепектора более 200 конструкторов на все поддерживаемые типы. Но если пепектор надо создать над непонятным типом, то лучше использовать pepector.Create<T> или TryCreate<T>, а не конструктор — так можно будет избежать аллокаций в куче. Create/TryCreate на деле создают пустой пепектор и пытаются добавить туда значения с помощью метода TryAdd<T> — а вот в нём прописаны уже все варианты на все случаи жизни
У пепектора можно включить выброс исключений при ошибках с помощью статического поля AllowExceptions
Пепектор поддерживает компактификацию с помощью метода TryCompact. Алгоритм довольно сложный. Идея в том, что мы ищем типы, занимающие меньше места и позволяющие запихнуть в пепектор больше значений, но так, чтобы то, что в нём сохранено сейчас, можно было выразить через значения этих типов
-
Пепекторы сравниваются по существу, а не формально. То есть, если числа в них равны и их количество равно, они считается равными — и без разницы, какого они там типа. boolы считаются за 0 и 1 при сравнении с числами
Сравнение через операторы ==, !=, > и прочие выдаёт скалярный bool. Если у пепекторов все значения равны, то они считаются равными, если хотя бы один не равен — не равными. В остальных случаях (больше/меньше и прочие) нужно, чтобы условие соблюдалось для всех компонентов попарно
Есть статические методы VectorEquals, VectorLargerThan и прочие — вот они выдают не bool, а pepector, набитый boolами, которые соответствуют результату для очередной пары значений из сравниваемых пепекторов
Если два сравниваемых пепектора имеют разную длину, то у короткого пепектора длина считается равной длинному, а недостающие значения считаются равными 0. То есть пепекторы (4, 0, 0, 0) и (4, 0) считаются равными
Само собой, пепектор поддерживает математику + — * /%, инкремент/декремент ++ — -, а также унарные операторы + и —. В результате всегда получается пепектор того типа, который вернул бы C#, если бы это были статические типы. То есть, например, пепектор с int при умножении на double даст пепектор с double. Если пепекторы разной длины, длина короткого увеличивается до длинного, недостающие значения считаются равными 0. А ещё есть Round для округления, SumElements, Dot, GetLengthPow2, GCD, MLC, Floor и прочие. Само собой, это всё работает с любым поддерживаемым типом
-
У пепектора есть IIF — аналог оператора condition ? ifTrue : ifFalse
Вызывать его надо у пепектора с boolами и скармливать пепектор значений для true и false, и он попарно выберет значения. Если вызывать IIF у пепектора с числовыми значениями, он всё равно отработает: просто всё, что >= 0.5 будет считаться за true
Есть метод UpgradeCountSmart, который превращает скаляры в векторы дублированием значений, а трёхмерные векторы в четырёхмерные так, чтобы четвёртый компонет был равен 1.0 или 255 в зависимости от типа — чтобы с цветами корректно всё работало
Как и статические векторы, пепектор поддерживает 64-битный и 32-битный хэши, которые считаются быстро
Пепектор тоже поддерживает неявное преобразование с типами из других библиотек типа Windows Forms, WPF, GDI, SharpDX и прочих
Как и у статических векторов, преобразование в текст через ToString с поддержкой IFormatProvider работает через странные оптимизированные штуки и может сохраняться в char*
Парсинг пепектора сложный: помимо всяких поддержек char*, IFormatProvider и NumberFormat, он ещё должен самостоятельно уметь выбирать тип значений, которые ему дают. И он это делает. Предпочтение отдаётся наиболее популярным типам — double и int. Если значений в строке слишком много, он начинает посматривать в сторону менее популярных — float, short и так далее. Если значения слишком большие и в диапазон int не вмещаются, то может выбрать long. Если значений очень много — может вообще выбрать тип byte. А если они ещё и отрицательные — то переходит к sbyte. В упоротых случаях действует по своему усмотрению (ну типа pepector.Parse(«false, 1.34 325, 34, -23»)), но результат всё равно будет. В любом случае, после парсинга можно вручную выставить ему нужный тип
Пепектор умеет умно бокситься в object методом TryToObject — полученный объект будет не забоксенным пепектором, а забоксенным соответствующим статическим типом. То есть пепектор с четырьмя double забоксит значение double4, с одним nint забоксит nint, а с тремя Half забоксит Half3. Функция поддерживает кучу флагов — можно попросить боксить в виде кортежей, можно указать поведение если забоксить не получилось (дать кортеж, выбросить исключение, вернуть особый указанный объект или забоксить сам пепектор), что делать если количество элементов равно 0 и прочее
Статический универсал universal<T>
Предназначен для статического обобщения математики над значениями любого типа
Поддерживает все скаляры, векторы и pepector. И кортежи — наполшишечки
На T нет вообще никаких ограничений — это сделано специально, чтобы его легко можно было впихнуть в любое место без заморочек. Если есть сомнения, можно ли его натравить на тип T, можно вызывать universal<T>.CheckIsTypeAllowed()
У него тоже есть свойства rgba xyzw кзса. На выходе дают тот же universal<T>, у которого все значения будут равны соответствующему полю
У него тоже есть свизлинг. Можно писать ему .bgrr и .zzxy
Механизм обработки операторов разный при компиляции в Debug и Release. В дебаге упор сделан на ускорение компиляции, и universal<T> работает медленно. В релиз комплируется гораздо дольше, но работает почти‑почти как нативный код, потому что написан так, чтобы JIT выбрасывал всё ненужное
Имеет свою собственную математику и константы. Например, universal<T>.MinValue, universal<T>.Pi или universal<T>.Zero. Есть всякие Dot, SumElements, IsInfinity и прочее
Умеет в парсинг, ToString, вычисление 32 и 64 битного хэша
Занимает ровно столько, сколько вложенный в него тип T
Кривенько‑косо может работать с кортежами. То есть если T — это кортеж из двух интов, их можно будет складывать‑вычитать‑умножать и вот это всё. Но оно не допилено
Если тип неизвестен, то есть <T> — это вообще какая‑то незнакомая ему дичь — всё равно попытается найти и вызвать соответствующий оператор через Reflection. Если в результате получится T — вернёт его
Пепематика
Поддерживает почти всё, что поддерживает Math. Полиморфично работает с любыми векторами — в этом случае возвращает векторы резульатов. То есть, например, Pepe.Log(new double4(...)) возвратит double4 результатов, и прочее. Поскольку C# постоянно донимал меня «неоднозначностью вызовов» и упорно не хотел понимать, чего от него хотят, и, при этом, не располагает инструментами, которые бы позволили явно указать приоритет выбора методов в полиморфизме, я тупо закатал в Pepe все возможные варианты для всех типов. Поэтому перегрузок методов в Pepe не много, а очень много. Это плохо, но я пока не решил, как это победить
Тригонометрические методы жрут radains, radians2, radains3 и radians4. Если им скармливать градусы или витки, всё будет считаться корректно. Это же касается pepector, в котором будут лежать градусы и витки
В отличие от Math, методы работы с float возвращают float. То есть, например, Log10(float3) вернёт float3 и будет использовать для вычислений не Math, а MathF
То же самое касается Half. Pow(Half2, Half2) возвращает Half2 и работает не через Math, а через Half.Pow. Я покопался в исходниках Half и увидел что он ссылается на MathF. Возникла мысль сделать некоторые методы быстрее на LUT‑табличках (16 бит всё таки не так много), но руки не дошли. Впрочем, не факт, что это будет быстрее — тут проверять надо
Математика pepectorов реализована через циклы на указателях с вынесенными за тело цикла ифами. Поэтому методы, обслуживающие пепекторы, довольно увесистые, но быстрые
В целом, некоторые политики и концепции, которые я реализовал в пепекторах, мне до сих пор кажутся спорными. В частности, явное/неявное преобразование и то, какой тип должно возвращать сравнение — скалярный или векторный bool. Изначально я хотел сделать векторный, как в видеокарте, но практическое удобство от этого резко упало, т.к. C# часто не понимал, чего от него вообще хотят. Это и неудивительно — if в видеокарте это совсем не то же самое, что в процессоре. По исключениям тоже вопрос открыт.
Многое пока сделано «сейчас вот так, а там посмотрим» — практическое использование уже не раз вносило свои коррективы.
Экспериментальные буферы
Чтобы было где разгуляться с разного рода оптимизациями, цвета для светодиодных лент и прочие штуки я храню не в массивах, а в буферах неуправляемой памяти: одномерных, двумерных и прочих.
Для этого я родил отдельный велосипед с разветвлённой иерархией, нативной поддержкой пепекторов, кастомными аллокаторами памяти и разного рода штуками, направленными на увеличение надёжности софта.

Буферы могут содержать значения любого неуправляемого типа (привет пепекторам) и иметь от одного до четырёх измерений, быть как обычными, так и ссылочными (то есть оборачивать какую‑то память).
Booffer1D<T> |
BoofferLink1D<T> |
PooledBooffer1D<T> |
Booffer2D<T> |
BoofferLink2D<T> |
PooledBooffer2D<T> |
Booffer3D<T> |
BoofferLink3D<T> |
PooledBooffer3D<T> |
Booffer4D<T> |
BoofferLink4D<T> |
PooledBooffer4D<T> |
Полная совместимость с пепекторами дает возможность делать довольно необычные штуки, например, Booffer3D<pepector> может иметь в каждом вокселе разное число каналов и значения разного типа. Ещё поддерживается сэмплинг с билинейной фильтрацией (чтение сглаженного значения по дробным координатам) — даже у 4D.
Неуправляемая память — штука опасная, и для снижения вероятности отстрела ног тут есть несколько фич:
Все буферы являются DisposableExtended, и наследуют все полагающиеся плюшки и инструменты обеспечения надежности. В частности, на время использования я блокирую возможность выгрузки буфера, а большинство реальных освобождений памяти происходит только через несколько секунд после того, как буфер стал считаться выгруженным
Указатель на память у буфера всегда верный, даже если буфер выгрузили/удалили — просто при выгрузке он меняется на единый общий для всего софта аварийный кусок памяти. При сбое нескольких буферов в аварийном куске памяти будет каша, но это лучше, чем упасть от «попытки доступа в незащищенную память». И да — при обращении к такому выгруженному буферу он громко орёт в лог, а в дебаге останавливатся. Но главное — софт продолжит работу, а не упадёт
При создании память выделяется с полями — сколько‑то элементов до начала, сколько‑то после. Так мы снижаем вероятность, что при небольшом выходе за пределы буфера софт упадёт. Одномерный буфер имеет поля в один или несколько элементов, двумерный — в одну строку + несколько элементов

Буферы имеют некоторые базовые методы для работы, например, 2D буфер умеет рисовать линии, прямоугольники и другие 2D‑буферы — и при этом может иметь значения вообще любого значимого типа, не только скаляры и пепекторы.
public void DrawSomething<T>(Booffer2D<T> canvas, T color) where T : unmanaged
{
if (E.IsInvalid(canvas)) return;
using var ds = canvas.DisposeProtectedScope;
if (ds.IsFailed()) return;
canvas.SetLine(50, 50, 100, 100, color);
canvas.SetLine((20, 30), (40, 50), color);
var color_200percent_brightness = (universal.Create(color) * 2).Value;
canvas.SetLine((10, 80), (20, 20), color_200percent_brightness);
canvas.SetRectangle((50, 60, 100, 200), color);
canvas.ShiftX(50); //Сдвигаем картинку вправо
}
Благодаря тому, что память буфера неуправляемая (ну кроме BoofferLink, который можно натянуть на что угодно, чтобы представить это как буфер), его адреса не ездят туда‑сюда, что открывает возможность для довольно хитрых оптимизаций.
Например, можно взять двумерный буфер цветов Booffer2D<ushort3> и одномерный буфер указателей Booffer1D<IntPtr>, запомнить в одномерный буфер указатели на определённые пиксели двумерного буфера, и затем просто перебирая адреса этих пикселей быстро менять их цвет, и, тем самым, что‑то рисовать, не вычисляя каждый раз адрес очередного пикселя на основе его координат. Так мы выкинем операцию умножения и пару операций сложения на каждый пиксель.
Что я имею ввиду
Вот пример подобной оптимизации:
//Пробегаемся по буферу указателей и пихаем в них значения
if (mapPack.TryGetMapPointers(mapIndex, out var begin, out var count, out var end, out var end4))
{
var end8 = begin + count / 8 * 8;
var ptr = begin;
var source_ptr = source_begin;
//ptr - указатель на указатель в буфере указателей
//source_ptr - указатель на значения, которые надо записывать
while (ptr < end8)
{
**ptr++ = *source_ptr++;
**ptr++ = *source_ptr++;
**ptr++ = *source_ptr++;
**ptr++ = *source_ptr++;
**ptr++ = *source_ptr++;
**ptr++ = *source_ptr++;
**ptr++ = *source_ptr++;
**ptr++ = *source_ptr++;
}
while (ptr < end)
**ptr++ = *source_ptr++;
}
Экспериментальная пеперубка
В солнечном окошке цвета хранились в формате byte3 (всего 16.7 млн. оттенков), и было где разгуляться с оптимизациями на табличках заранее рассчитанных цветов (LUT). А тут у нас float3 — таблички будут жрать больше 800 Тб оперативной памяти. Мне её, конечно, не жалко, но не настолько :) Будем решать проблему по‑другому.

Итак, значения в буферах надо обсчитывать. Например, надо попарно складывать значения двух буферов для сложения двух эффектов для светодиодных лент, умножать, делить и всё в таком духе.
Умножением‑сложением и прочей математикой значений в буферах у меня занимается Peperubka. Казалось бы, задача для C++, но мне было интересно, насколько далеко можно зайти в оптимизациях на C#. Я и зашёл. Скорость пеперубки обеспечивается тремя аспектами: циклами на указателях, simdовой векторизацией и анроллингом.

Что такое циклы на указателях
Это когда счётчиком цикла служит не абстрактный номер элемента в массиве/буфере, а прямой адрес этого элемента в памяти. В обычном случае надо для каждого нового значения счётчика цикла вычислять адрес элемента (адрес первого элемента + значение счётчика). Цикл указателей позволяет этого избежать. И заодно при любом неправильном действии порушить весь софт и огрести трудноуловимый баг :) Поэтому использовать надо осторожно и только там где нужно. Многие современные компиляторы достаточно умны, чтобы автоматом превращать обычные циклы в циклы указателей
Что такое SIMD векторизация
Single Instruction, Multiple Data
Это когда «за раз» считается не одна пара чисел, а несколько. То есть вместо «возьми это число и умножь на это» можно одной командой сказать «возьми 16 этих чисел и попарно умножь на вот эти 16 чисел» — и это будет выполняться «одной командой», на физическом уровне внутри процессора. Для этого у него есть специальные аппаратные инструкции — SSE, AVX и прочие. На самом деле всё сложнее, но про микрокод и регистры объяснять тут я не буду
Что такое анроллинг
Это когда тело цикла копипастится несколько раз, чтобы проверка конца цикла происходила реже. То есть вместо
for (int i = 0; i < 100; i++)
c[i] = a[i] * b[i];
пишут что‑то типа
for (int i = 0; i < 100;)
{
c[i] = a[i] * b[i]; i++;
c[i] = a[i] * b[i]; i++;
}
//Проверка “< 100” в два раза реже
Помимо сложения‑вычитания и умножения‑деления буферов, пеперубка умеет в быстрые xy и x-1, умножение+сложение за раз, обрезку диапазона, универсальное быстрое преобразование RGB ↔ RGBA и прочее. И — самое главное — умеет в перевод HDR в SDR. Пиксели‑то у нас хранятся в HDR, а ленты — SDR.

Использовать пеперубку очень просто:
public static void TestPeperubka<T>(IBooffer<T> buf1, IBooffer<T> buf2, IBooffer<T> buf3, T someValue, IBooffer<T> output) where T : unmanaged
{
Peperubka.MulAdd(buf1, buf2, buf3); //buf1 = buf1 * buf2 + buf3
Peperubka.MulAdd(buf1, buf3, someValue); //buf1 = buf1 * buf2 + someValue
Peperubka.MulAdd(buf1, buf2, buf3, output); //output = buf1 * buf2 + buf3
Peperubka.MulAdd(buf1, buf2, someValue, output); //output = buf1 * someValue + buf2
Peperubka.Mul(buf1, buf2); //buf1 *= buf2
Peperubka.Mul(buf1, someValue); //buf1 *= someValue
Peperubka.Mul(buf1, buf2, output); //output = buf1 * buf2
Peperubka.Mul(buf1, someValue, output); //output = buf1 * someValue
Peperubka.Add(buf1, buf2); //buf1 += buf2
Peperubka.Add(buf1, someValue); //buf1 += someValue
Peperubka.Add(buf1, buf2, output); //output = buf1 + buf2
Peperubka.Add(buf1, someValue, output); //output = buf1 + someValue
Peperubka.Div(buf1, buf2); //buf1 /= buf2
Peperubka.Div(buf1, someValue); //buf1 /= someValue
Peperubka.Div(buf1, someValue, fastMulReciprocal: true); //buf1 *= 1 / someValue
Peperubka.Div(someValue, buf1); //buf1 = someValue / buf1
Peperubka.Div(buf1, buf2, output); //output = buf1 / buf2
Peperubka.Div(buf1, someValue, output); //output = buf1 / someValue
Peperubka.Sub(buf1, buf2); //buf1 -= buf2
Peperubka.Sub(buf1, someValue); //buf1 -= someValue
Peperubka.Sub(buf1, buf2, output); //output = buf1 - buf2
Peperubka.Sub(buf1, someValue, output); //output = buf1 - someValue
Peperubka.Reciprocal(buf1, fastInexactIfAvaiable: false); //buf1 = 1 / buf1
Peperubka.Pow(buf1, someValue, output, fastInexactIfAvaiable: true); //output = buf1 ^ someValue
pepector sum = Peperubka.CalculateSum(buf1);
Peperubka.Clamp(buf2, universal<T>.Zero.Value, (universal<T>.MaxValue / 2).Value);
}
Ещё в пеперубке есть:
Универсальный сумматор
Спецалгоритм для конвертации RGB в BGR (это часто нужно)
Универсальный конвертер буферов с разным типом значений
Обобщённый конвертер, который, по возможности, строит LUT‑таблицы для мелких типов
Обобщённый алгоритм обработки набора буферов, обнаруживающий те буферы, которые идут подряд, объединяющий их и минимизирующий тем самым количество вызовов функции обработчика
Всё это работает с 1D, 2D, 3D и 4D буферами со значениями любого типа — скаляров и пепекторов, и имеет вариации: буфер с буфером, буфер с константой + варианты куда писать результат — в исходный буфер или в какой‑то новый.

Для большинства алгоритмов я запилил отдельные реализации на AVX512, AVX2, SSE и скалярных вычислениях. Часто каждый вариант приходилось писать отдельно, однако генерики C# всё‑таки несколько облегчили задачу. Зачем? Руки чесались.
RGB -> RGBA на AVX512 для 8-бит/канал векторов (byte3/sbyte3/bool3)
//Указатели прилетают уже выровненные
private static bool simdWithSetAlpha512_byte(ref Vector3D* sourceBegin, ref Vector4D* destinationBegin, ref long valueCount, Vector3D* _originalSourceEnd, ScalarType alphaValue)
{
if (!Avx512BW.IsSupported || !Vector512<byte>.IsSupported || sizeof(ScalarType) != sizeof(byte))
return false;
byte* srcPtr = (byte*)sourceBegin;
byte* dstPtr = (byte*)destinationBegin;
var vector_count_inAVX512 = (512 / 8) / (sizeof(byte) * 3);
var valueVectorCompatibleCount = (valueCount / vector_count_inAVX512 - 1) * vector_count_inAVX512;
var valueVectorCompatibleCount4 = (valueVectorCompatibleCount / 4 - 2) * 4;
byte* sEnd = (byte*)srcPtr + valueVectorCompatibleCount * 3;
byte* sEnd4 = (byte*)srcPtr + valueVectorCompatibleCount4 * 3;
Vector512<byte> shuffleMask;
{
byte _ = 0;
shuffleMask = Vector512.Create(
0, 1, 2, _, 3, 4, 5, _,
6, 7, 8, _, 9, 10, 11, _,
12, 13, 14, _, 15, 16, 17, _,
18, 19, 20, _, 21, 22, 23, _,
24, 25, 26, _, 27, 28, 29, _,
30, 31, 32, _, 33, 34, 35, _,
36, 37, 38, _, 39, 40, 41, _,
42, 43, 44, _, 45, 46, 47, _);
}
Vector512<byte> blendMask = Vector512.Create(
0, 0, 0, 0xFF, 0, 0, 0, 0xFF,
0, 0, 0, 0xFF, 0, 0, 0, 0xFF,
0, 0, 0, 0xFF, 0, 0, 0, 0xFF,
0, 0, 0, 0xFF, 0, 0, 0, 0xFF,
0, 0, 0, 0xFF, 0, 0, 0, 0xFF,
0, 0, 0, 0xFF, 0, 0, 0, 0xFF,
0, 0, 0, 0xFF, 0, 0, 0, 0xFF,
0, 0, 0, 0xFF, 0, 0, 0, 0xFF);
byte* scalarArray = stackalloc byte[512 / 8 / sizeof(byte)];
{
byte scalarAlphaAsByte = *(byte*)&alphaValue;
for (int i = 0; i < 512 / 8 / sizeof(byte); i++)
scalarArray[i] = scalarAlphaAsByte;
}
var scalarVector = Vector512.Load(scalarArray);
while (srcPtr < sEnd4)
{
{
var rgb = Vector512.LoadAligned(srcPtr); //РОВНО!!!!
var rgbShuffled = Vector512.Shuffle(rgb, shuffleMask);
var rgbBlended = Avx512BW.BlendVariable(rgbShuffled, scalarVector, blendMask);
Vector512.StoreAligned(rgbBlended, dstPtr);
srcPtr += (512 / 8 / sizeof(byte) / 4 * 3); //48 или 24 или 12 или 6 скаляров
dstPtr += (512 / 8 / sizeof(byte)); //64 или 32 или 16 или 8 скаляров
}
{
var rgb = Vector512.Load(srcPtr); //не ровно
var rgbShuffled = Vector512.Shuffle(rgb, shuffleMask);
var rgbBlended = Avx512BW.BlendVariable(rgbShuffled, scalarVector, blendMask);
Vector512.StoreAligned(rgbBlended, dstPtr);
srcPtr += (512 / 8 / sizeof(byte) / 4 * 3);
dstPtr += (512 / 8 / sizeof(byte));
}
{
var rgb = Vector512.Load(srcPtr); //не ровно
var rgbShuffled = Vector512.Shuffle(rgb, shuffleMask);
var rgbBlended = Avx512BW.BlendVariable(rgbShuffled, scalarVector, blendMask);
Vector512.StoreAligned(rgbBlended, dstPtr);
srcPtr += (512 / 8 / sizeof(byte) / 4 * 3);
dstPtr += (512 / 8 / sizeof(byte));
}
{
var rgb = Vector512.Load(srcPtr); //не ровно
var rgbShuffled = Vector512.Shuffle(rgb, shuffleMask);
var rgbBlended = Avx512BW.BlendVariable(rgbShuffled, scalarVector, blendMask);
Vector512.StoreAligned(rgbBlended, dstPtr);
srcPtr += (512 / 8 / sizeof(byte) / 4 * 3);
dstPtr += (512 / 8 / sizeof(byte));
}
}
while (srcPtr < sEnd)
{
var rgb = Vector512.Load(srcPtr); //не ровно
var rgbShuffled = Vector512.Shuffle(rgb, shuffleMask);
var rgbBlended = Avx512BW.BlendVariable(rgbShuffled, scalarVector, blendMask);
Vector512.StoreAligned(rgbBlended, dstPtr);
srcPtr += (512 / 8 / sizeof(byte) / 4 * 3);
dstPtr += (512 / 8 / sizeof(byte));
}
sourceBegin = (Vector3D*)srcPtr;
destinationBegin = (Vector4D*)dstPtr;
valueCount = _originalSourceEnd - sourceBegin;
return true;
}
AVX512 вычисление альфа-канала на основе яркости (8 бит/канал)
static readonly Vector512<ushort> _rgb_koefficients = Vector512.Create(
(ushort)38, (ushort)77, (ushort)13, (ushort)0,
(ushort)38, (ushort)77, (ushort)13, (ushort)0,
(ushort)38, (ushort)77, (ushort)13, (ushort)0,
(ushort)38, (ushort)77, (ushort)13, (ushort)0,
(ushort)38, (ushort)77, (ushort)13, (ushort)0,
(ushort)38, (ushort)77, (ushort)13, (ushort)0,
(ushort)38, (ushort)77, (ushort)13, (ushort)0,
(ushort)38, (ushort)77, (ushort)13, (ushort)0
);
static readonly Vector512<ushort> _channelRegrouping = Vector512.Create(
(ushort)0, (ushort)4, (ushort)8, (ushort)12, (ushort)16, (ushort)20, (ushort)24, (ushort)28, //Красные
(ushort)1, (ushort)5, (ushort)9, (ushort)13, (ushort)17, (ushort)21, (ushort)25, (ushort)29, //Зелёные
(ushort)2, (ushort)6, (ushort)10, (ushort)14, (ushort)18, (ushort)22, (ushort)26, (ushort)30, //Синие
(ushort)3, (ushort)7, (ushort)11, (ushort)15, (ushort)19, (ushort)23, (ushort)27, (ushort)31 //Пофиг
);
static readonly Vector256<byte> _byte_unrar_indices = Vector256.Create(
(byte)31, (byte)31, (byte)31, (byte)0,
(byte)31, (byte)31, (byte)31, (byte)1,
(byte)31, (byte)31, (byte)31, (byte)2,
(byte)31, (byte)31, (byte)31, (byte)3,
(byte)31, (byte)31, (byte)31, (byte)4,
(byte)31, (byte)31, (byte)31, (byte)5,
(byte)31, (byte)31, (byte)31, (byte)6,
(byte)31, (byte)31, (byte)31, (byte)7
);
static readonly Vector256<byte> _rgb_alpha_masks = Vector256.Create(
0, 0, 0, 0xFF,
0, 0, 0, 0xFF,
0, 0, 0, 0xFF,
0, 0, 0, 0xFF,
0, 0, 0, 0xFF,
0, 0, 0, 0xFF,
0, 0, 0, 0xFF,
0, 0, 0, 0xFF
);
private static byte* _avx512_byte_Avx512BW(byte* ptr, byte* end_vec)
{
//Прогружаем маски и прочую шелуху в регистры
var rgb_koefficients = _rgb_koefficients;
var channelRegrouping = _channelRegrouping;
var byte_unrar_indices = _byte_unrar_indices;
var rgb_alpha_masks = _rgb_alpha_masks;
while (ptr < end_vec)
{
var colorsBytes = Vector256.LoadAligned<byte>(ptr);
var colorsUShort = Avx512BW.ConvertToVector512UInt16(colorsBytes);
//Умножаем RGB коэффициенты
var colorsMultipled = colorsUShort * rgb_koefficients;
//Складываем для каждого пикселя его перемноженные RGB
Vector128<ushort> sums;
{
//Тасуем RGB RGB RGB RGB RGB RGB RGB RGB в RRRRRRRR GGGGGGGG BBBBBBBB AAAAAAAA
var regroupedChannels = Vector512.Shuffle(colorsMultipled, channelRegrouping);
//Теперь нам нужно 3 вектора красных, зелёных и синих компонент
var lower = regroupedChannels.GetLower(); //RRRRRRRR GGGGGGGG
var reds = lower.GetLower(); //RRRRRRRR
var greens = lower.GetUpper(); //GGGGGGGG
var blues = regroupedChannels.GetUpper().GetLower(); //BBBBBBBB
//Складываем попарно компоненты сразу 8 пикселей
sums = reds + greens + blues; //RRRRRRRR + GGGGGGGG + BBBBBBBB = SUM SUM SUM SUM SUM SUM SUM SUM
}
//Делим все 8 сумм на 128
var lumsAsUShorts = Vector128.ShiftRightLogical(sums, 7);
//Получаем яркости
//Преобразуем яркости из ушортов обратно в байты
var lumsAsBytes = Avx2.PackUnsignedSaturate(*(Vector256<short>*)&lumsAsUShorts, Vector256<short>.Zero);
//Раздвигаем яркости чтобы прицелиться
//ими на места альфа-каналов
var lumsShuffled = Vector256.Shuffle(lumsAsBytes, byte_unrar_indices);
////Совмещаем считанные в самом начале цвета 8 пикселей с
////посчитанными альфа-каналами
var blendedResult = Avx2.BlendVariable(colorsBytes, lumsShuffled, rgb_alpha_masks);
//Пишем результат
Vector256.StoreAligned(blendedResult, ptr);
////Следующие 8 пикселей
ptr += (256 / 8 / sizeof(byte));
}
return ptr;
}
Быстрый неточный 1/x на AVX2
private static unsafe bool tryReciprocalSIMD<V>(V* arg1, long count, bool fast) where V : unmanaged
{
var elType = __scalarInnerTypeForSIMDExtractor<V>.ScalarElementType; //Вытаскиваем что за элементы в конечном итоге хранятся
if (elType == null)
return false;
var vectorlen = __scalarInnerTypeForSIMDExtractor<V>.VectorLength;
var scalarCount = count * __scalarInnerTypeForSIMDExtractor<V>.VectorLength;
#if !DISABLE_NINT
if (typeof(V) == typeof(nint))
{
if (sizeof(nint) == sizeof(int))
return tryReciprocalSIMD<int>((int*)arg1, count, fast);
else
return tryReciprocalSIMD<long>((long*)arg1, count, fast);
}
else if (typeof(V) == typeof(nint2))
{
if (sizeof(nint2) == sizeof(int2))
return tryReciprocalSIMD<int2>((int2*)arg1, count, fast);
else
return tryReciprocalSIMD<long2>((long2*)arg1, count, fast);
}
else if (typeof(V) == typeof(nint3))
{
if (sizeof(nint3) == sizeof(int3))
return tryReciprocalSIMD<int3>((int3*)arg1, count, fast);
else
return tryReciprocalSIMD<long3>((long3*)arg1, count, fast);
}
else if (typeof(V) == typeof(nint4))
{
if (sizeof(nint4) == sizeof(int4))
return tryReciprocalSIMD<int4>((int4*)arg1, count, fast);
else
return tryReciprocalSIMD<long4>((long4*)arg1, count, fast);
}
else if (typeof(V) == typeof(nuint))
{
if (sizeof(nuint) == sizeof(uint))
return tryReciprocalSIMD<uint>((uint*)arg1, count, fast);
else
return tryReciprocalSIMD<ulong>((ulong*)arg1, count, fast);
}
else if (typeof(V) == typeof(nuint2))
{
if (sizeof(nuint2) == sizeof(uint2))
return tryReciprocalSIMD<uint2>((uint2*)arg1, count, fast);
else
return tryReciprocalSIMD<ulong2>((ulong2*)arg1, count, fast);
}
else if (typeof(V) == typeof(nuint3))
{
if (sizeof(nuint3) == sizeof(uint3))
return tryReciprocalSIMD<uint3>((uint3*)arg1, count, fast);
else
return tryReciprocalSIMD<ulong3>((ulong3*)arg1, count, fast);
}
else if (typeof(V) == typeof(nuint4))
{
if (sizeof(nuint4) == sizeof(uint4))
return tryReciprocalSIMD<uint4>((uint4*)arg1, count, fast);
else
return tryReciprocalSIMD<ulong4>((ulong4*)arg1, count, fast);
}
#endif
#if !DISABLE_HALF
if (_tryReciprocalSIMD_AsTypeV<V, Half>.tryReciprocalSIMD_AsType(arg1, count, elType, scalarCount, vectorlen, fast)) return true;
#endif
if (_tryReciprocalSIMD_AsTypeV<V, float>.tryReciprocalSIMD_AsType(arg1, count, elType, scalarCount, vectorlen, fast)) return true;
if (_tryReciprocalSIMD_AsTypeV<V, double>.tryReciprocalSIMD_AsType(arg1, count, elType, scalarCount, vectorlen, fast)) return true;
if (_tryReciprocalSIMD_AsTypeV<V, byte>.tryReciprocalSIMD_AsType(arg1, count, elType, scalarCount, vectorlen, fast)) return true;
if (_tryReciprocalSIMD_AsTypeV<V, short>.tryReciprocalSIMD_AsType(arg1, count, elType, scalarCount, vectorlen, fast)) return true;
if (_tryReciprocalSIMD_AsTypeV<V, ushort>.tryReciprocalSIMD_AsType(arg1, count, elType, scalarCount, vectorlen, fast)) return true;
if (_tryReciprocalSIMD_AsTypeV<V, long>.tryReciprocalSIMD_AsType(arg1, count, elType, scalarCount, vectorlen, fast)) return true;
if (_tryReciprocalSIMD_AsTypeV<V, ulong>.tryReciprocalSIMD_AsType(arg1, count, elType, scalarCount, vectorlen, fast)) return true;
if (_tryReciprocalSIMD_AsTypeV<V, sbyte>.tryReciprocalSIMD_AsType(arg1, count, elType, scalarCount, vectorlen, fast)) return true;
if (_tryReciprocalSIMD_AsTypeV<V, int>.tryReciprocalSIMD_AsType(arg1, count, elType, scalarCount, vectorlen, fast)) return true;
if (_tryReciprocalSIMD_AsTypeV<V, uint>.tryReciprocalSIMD_AsType(arg1, count, elType, scalarCount, vectorlen, fast)) return true;
if (_tryReciprocalSIMD_AsTypeV<V, decimal>.tryReciprocalSIMD_AsType(arg1, count, elType, scalarCount, vectorlen, fast)) return true;
return false;
}
static class _tryReciprocalSIMD_AsTypeV<V, T> where V : unmanaged where T : unmanaged
{
const int _ReciprCONST = 0x7EF311C2;
static readonly Vector<int> _ReciprCONST_V = new Vector<int>(_ReciprCONST);
public static unsafe bool tryReciprocalSIMD_AsType(V* canvas, long count, Type elType, long scalarCount, long vectorLen, bool fast)
{
unchecked
{
if (elType == typeof(T) && Vector<T>.IsSupported && Vector<T>.Count % vectorLen == 0)
{
//Выравниваемся
if ((nint)canvas % simdAlign != 0)
{
universal<V>* canvas_u = (universal<V>*)canvas;
universal<V>* canvas_u_end = (universal<V>*)(canvas + count);
while (canvas_u < canvas_u_end)
{
if ((nint)canvas_u % simdAlign == 0)
break;
else
{
*canvas_u = universal<V>.One / *canvas_u;
canvas_u++;
}
}
canvas = (V*)canvas_u;
count = canvas_u_end - canvas_u;
scalarCount = count * (sizeof(V) / sizeof(T));
}
var oneScalar = universal<T>.One;
var oneVector = new Vector<T>(oneScalar.Value);
var vectorCount = scalarCount / (sizeof(Vector<T>) / sizeof(T));
var cPtr = (Vector<T>*)canvas;
var canvasEnd = cPtr + vectorCount;
//Быстрый неточный алгоритм только для флоатов
bool useFastFP32 = fast && typeof(T) == typeof(float) && Vector<int>.Count == Vector<float>.Count && sizeof(int) == sizeof(float);
if (useFastFP32)
{
Vector<int>* canvasPtrI = (Vector<int>*)cPtr;
{ //Десертный анролл по 4 штук
Vector<int>* canvasPtrIEnd = (Vector<int>*)canvasEnd;
var vector_pack4_count = vectorCount / 4;
var canvasEnd4 = cPtr + vector_pack4_count * 4;
while (canvasPtrI < canvasEnd4)
{
*canvasPtrI = _ReciprCONST_V - *canvasPtrI; canvasPtrI++;
*canvasPtrI = _ReciprCONST_V - *canvasPtrI; canvasPtrI++;
*canvasPtrI = _ReciprCONST_V - *canvasPtrI; canvasPtrI++;
*canvasPtrI = _ReciprCONST_V - *canvasPtrI; canvasPtrI++;
}
}
{ //Хвостик
Vector<int>* canvasPtrIEnd = (Vector<int>*)canvasEnd;
while (canvasPtrI < canvasPtrIEnd)
{
*canvasPtrI = _ReciprCONST_V - *canvasPtrI;
canvasPtrI++;
}
cPtr = (Vector<T>*)canvasPtrI;
}
}
else
{
if (Avx2.IsSupported && (long)cPtr % 32 == 0 && typeof(T) == typeof(float) && Vector<T>.Count == Vector256<T>.Count)
{
{ //Десертный анролл по 4 штук
var vector_pack4_count = vectorCount / 4;
var canvasEnd4 = cPtr + vector_pack4_count * 4;
float* c0, c1, c2, c3;
while (cPtr < canvasEnd4)
{
var vect0 = Avx2.LoadAlignedVector256(c0 = (float*)cPtr); cPtr++;
var vect1 = Avx2.LoadAlignedVector256(c1 = (float*)cPtr); cPtr++;
var vect2 = Avx2.LoadAlignedVector256(c2 = (float*)cPtr); cPtr++;
var vect3 = Avx2.LoadAlignedVector256(c3 = (float*)cPtr); cPtr++;
var vectR0 = Avx2.Reciprocal(vect0);
var vectR1 = Avx2.Reciprocal(vect1);
var vectR2 = Avx2.Reciprocal(vect2);
var vectR3 = Avx2.Reciprocal(vect3);
Avx2.Store(c0, vectR0);
Avx2.Store(c1, vectR1);
Avx2.Store(c2, vectR2);
Avx2.Store(c3, vectR3);
}
}
{ //Хвостик
while (cPtr < canvasEnd)
{
var vect0 = Avx2.LoadAlignedVector256((float*)cPtr);
var vectR0 = Avx2.Reciprocal(vect0);
Avx2.Store((float*)cPtr, vectR0);
cPtr++;
}
}
}
else
{
{ //Десертный анролл по 4 штук
var vector_pack4_count = vectorCount / 4;
var canvasEnd4 = cPtr + vector_pack4_count * 4;
while (cPtr < canvasEnd4)
{
*cPtr = oneVector / *cPtr; cPtr++;
*cPtr = oneVector / *cPtr; cPtr++;
*cPtr = oneVector / *cPtr; cPtr++;
*cPtr = oneVector / *cPtr; cPtr++;
}
}
{ //Хвостик
while (cPtr < canvasEnd)
{
*cPtr = oneVector / *cPtr;
cPtr++;
}
}
}
}
if (useFastFP32)
{
//Кончик можно и на universalах
//В быстром алгоритме тут смысл только в том
//чтобы результаты не отличались от основной тушке
if (scalarCount % Vector<T>.Count > 0)
{
var canvasScalarPtr = (int*)cPtr;
var canvasEnd2 = (int*)canvas + scalarCount;
while (canvasScalarPtr < canvasEnd2)
{
*canvasScalarPtr = _ReciprCONST_V[0] - *canvasScalarPtr;
canvasScalarPtr++;
}
}
}
else
{
//Кончик можно и на universalах
if (scalarCount % Vector<T>.Count > 0)
{
var canvasScalarPtr = (universal<T>*)cPtr;
var canvasEnd2 = (universal<T>*)canvas + scalarCount;
while (canvasScalarPtr < canvasEnd2)
{
*canvasScalarPtr = oneScalar / *canvasScalarPtr;
canvasScalarPtr++;
}
}
}
return true;
}
else
return false;
}
}
}
Умножение буфера на буфер и сложение с ещё одним буфером
internal static unsafe bool tryMulAddSIMD<V>(V* arg1, V* arg2, V* k, long count) where V : unmanaged
{
var elType = __scalarInnerTypeForSIMDExtractor<V>.ScalarElementType; //Вытаскиваем что за элементы в конечном итоге хранятся
if (elType == null)
return false;
var vectorLen = __scalarInnerTypeForSIMDExtractor<V>.VectorLength;
var scalarCount = count * vectorLen;
#if !DISABLE_NINT
if (typeof(V) == typeof(nint))
{
if (sizeof(nint) == sizeof(int))
return tryMulAddSIMD<int>((int*)arg1, (int*)arg2, (int*)k, count);
else
return tryMulAddSIMD<long>((long*)arg1, (long*)arg2, (long*)k, count);
}
else if (typeof(V) == typeof(nint2))
{
if (sizeof(nint2) == sizeof(int2))
return tryMulAddSIMD<int2>((int2*)arg1, (int2*)arg2, (int2*)k, count);
else
return tryMulAddSIMD<long2>((long2*)arg1, (long2*)arg2, (long2*)k, count);
}
else if (typeof(V) == typeof(nint3))
{
if (sizeof(nint3) == sizeof(int3))
return tryMulAddSIMD<int3>((int3*)arg1, (int3*)arg2, (int3*)k, count);
else
return tryMulAddSIMD<long3>((long3*)arg1, (long3*)arg2, (long3*)k, count);
}
else if (typeof(V) == typeof(nint4))
{
if (sizeof(nint4) == sizeof(int4))
return tryMulAddSIMD<int4>((int4*)arg1, (int4*)arg2, (int4*)k, count);
else
return tryMulAddSIMD<long4>((long4*)arg1, (long4*)arg2, (long4*)k, count);
}
else if (typeof(V) == typeof(nuint))
{
if (sizeof(nuint) == sizeof(uint))
return tryMulAddSIMD<uint>((uint*)arg1, (uint*)arg2, (uint*)k, count);
else
return tryMulAddSIMD<ulong>((ulong*)arg1, (ulong*)arg2, (ulong*)k, count);
}
else if (typeof(V) == typeof(nuint2))
{
if (sizeof(nuint2) == sizeof(uint2))
return tryMulAddSIMD<uint2>((uint2*)arg1, (uint2*)arg2, (uint2*)k, count);
else
return tryMulAddSIMD<ulong2>((ulong2*)arg1, (ulong2*)arg2, (ulong2*)k, count);
}
else if (typeof(V) == typeof(nuint3))
{
if (sizeof(nuint3) == sizeof(uint3))
return tryMulAddSIMD<uint3>((uint3*)arg1, (uint3*)arg2, (uint3*)k, count);
else
return tryMulAddSIMD<ulong3>((ulong3*)arg1, (ulong3*)arg2, (ulong3*)k, count);
}
else if (typeof(V) == typeof(nuint4))
{
if (sizeof(nuint4) == sizeof(uint4))
return tryMulAddSIMD<uint4>((uint4*)arg1, (uint4*)arg2, (uint4*)k, count);
else
return tryMulAddSIMD<ulong4>((ulong4*)arg1, (ulong4*)arg2, (ulong4*)k, count);
}
#endif
#if !DISABLE_HALF
if (_tryMulAddSIMD_AsTypeMask<V, Half>.tryMulAddSIMD_AsType(arg1, arg2, k, count, elType, scalarCount, vectorLen)) return true;
#endif
if (_tryMulAddSIMD_AsTypeMask<V, float>.tryMulAddSIMD_AsType(arg1, arg2, k, count, elType, scalarCount, vectorLen)) return true;
if (_tryMulAddSIMD_AsTypeMask<V, double>.tryMulAddSIMD_AsType(arg1, arg2, k, count, elType, scalarCount, vectorLen)) return true;
if (_tryMulAddSIMD_AsTypeMask<V, byte>.tryMulAddSIMD_AsType(arg1, arg2, k, count, elType, scalarCount, vectorLen)) return true;
if (_tryMulAddSIMD_AsTypeMask<V, short>.tryMulAddSIMD_AsType(arg1, arg2, k, count, elType, scalarCount, vectorLen)) return true;
if (_tryMulAddSIMD_AsTypeMask<V, ushort>.tryMulAddSIMD_AsType(arg1, arg2, k, count, elType, scalarCount, vectorLen)) return true;
if (_tryMulAddSIMD_AsTypeMask<V, long>.tryMulAddSIMD_AsType(arg1, arg2, k, count, elType, scalarCount, vectorLen)) return true;
if (_tryMulAddSIMD_AsTypeMask<V, ulong>.tryMulAddSIMD_AsType(arg1, arg2, k, count, elType, scalarCount, vectorLen)) return true;
if (_tryMulAddSIMD_AsTypeMask<V, sbyte>.tryMulAddSIMD_AsType(arg1, arg2, k, count, elType, scalarCount, vectorLen)) return true;
if (_tryMulAddSIMD_AsTypeMask<V, int>.tryMulAddSIMD_AsType(arg1, arg2, k, count, elType, scalarCount, vectorLen)) return true;
if (_tryMulAddSIMD_AsTypeMask<V, uint>.tryMulAddSIMD_AsType(arg1, arg2, k, count, elType, scalarCount, vectorLen)) return true;
if (_tryMulAddSIMD_AsTypeMask<V, decimal>.tryMulAddSIMD_AsType(arg1, arg2, k, count, elType, scalarCount, vectorLen)) return true;
return false;
}
static class _tryMulAddSIMD_AsTypeMask<V, T> where V : unmanaged where T : unmanaged
{
public static unsafe bool tryMulAddSIMD_AsType(V* canvas, V* arg, V* k, long count, Type elType, long scalarCount, long vectorLen)
{
if (elType == typeof(T) && Vector<T>.IsSupported)
{
//Выравниваемся
if ((nint)canvas % simdAlign != 0 || (nint)arg % simdAlign != 0 || (nint)k % simdAlign != 0)
{
universal<V>* canvas_u = (universal<V>*)canvas;
universal<V>* k_u = (universal<V>*)k;
universal<V>* arg_u = (universal<V>*)arg;
universal<V>* canvas_u_end = (universal<V>*)(canvas + count);
while (canvas_u < canvas_u_end)
{
if ((nint)canvas_u % simdAlign == 0 && (nint)arg_u % simdAlign == 0 && (nint)k_u % simdAlign == 0)
break;
else
{
*canvas_u += *arg_u * *k_u;
canvas_u++;
arg_u++;
k_u++;
}
}
canvas = (V*)canvas_u;
arg = (V*)arg_u;
count = canvas_u_end - canvas_u;
scalarCount = count * (sizeof(V) / sizeof(T));
}
var vectorCount = scalarCount / (sizeof(Vector<T>) / sizeof(T));
var canvasPtr = (Vector<T>*)canvas;
var canvasEnd = canvasPtr + vectorCount;
var argPtr = (Vector<T>*)arg;
var kPtr = (Vector<T>*)k;
if (typeof(T) == typeof(float) && Hardcode.FloatAVX_only_for_aligned_32bytes_memory.IsGoodPointers(canvas, arg, k))
{
float* canvasF = (float*)canvas;
float* argF = (float*)arg;
float* kF = (float*)kPtr;
if (Hardcode.FloatAVX_only_for_aligned_32bytes_memory.TryAddMul(ref canvasF, ref argF, ref kF, count))
{
canvasPtr = (Vector<T>*)canvasF;
argPtr = (Vector<T>*)argF;
kPtr = (Vector<T>*)kF;
}
}
else
{
{ //Пачки по 4
var pack4count = vectorCount / 4;
var canvasEnd4 = (Vector<T>*)canvas + pack4count * 4;
while (canvasPtr < canvasEnd4)
{
*canvasPtr += *argPtr * *kPtr; canvasPtr++; argPtr++; kPtr++;
*canvasPtr += *argPtr * *kPtr; canvasPtr++; argPtr++; kPtr++;
*canvasPtr += *argPtr * *kPtr; canvasPtr++; argPtr++; kPtr++;
*canvasPtr += *argPtr * *kPtr; canvasPtr++; argPtr++; kPtr++;
}
}
}
{ //Хвостик
while (canvasPtr < canvasEnd)
{
*canvasPtr += *argPtr * *kPtr;
canvasPtr++; argPtr++; kPtr++;
}
}
//Кончик можно и на universalах
if (scalarCount % Vector<T>.Count > 0)
{
var canvasScalarPtr = (universal<T>*)canvasPtr;
var argScalarPtr = (universal<T>*)argPtr;
var canvasEnd2 = (universal<T>*)canvas + scalarCount;
var KU = (universal<V>*)kPtr;
while (canvasScalarPtr < canvasEnd2)
{
*canvasScalarPtr += *argScalarPtr * *KU;
canvasScalarPtr++; argScalarPtr++; KU++;
}
}
return true;
}
else
return false;
}
}
Просто умножение буфера на число
private static unsafe bool tryMulSIMD<V>(V* arg1, V arg2, long count) where V : unmanaged
{
var elType = __scalarInnerTypeForSIMDExtractor<V>.ScalarElementType; //Вытаскиваем что за элементы в конечном итоге хранятся
if (elType == null)
return false;
var vectorlen = __scalarInnerTypeForSIMDExtractor<V>.VectorLength;
var scalarCount = count * __scalarInnerTypeForSIMDExtractor<V>.VectorLength;
#if !DISABLE_NINT
if (typeof(V) == typeof(nint))
{
if (sizeof(nint) == sizeof(int))
return tryMulSIMD<int>((int*)arg1, universal.Create(arg2).ConvertTo<int>(), count);
else
return tryMulSIMD<long>((long*)arg1, universal.Create(arg2).ConvertTo<long>(), count);
}
else if (typeof(V) == typeof(nint2))
{
if (sizeof(nint2) == sizeof(int2))
return tryMulSIMD<int2>((int2*)arg1, universal.Create(arg2).ConvertTo<int2>(), count);
else
return tryMulSIMD<long2>((long2*)arg1, universal.Create(arg2).ConvertTo<long2>(), count);
}
else if (typeof(V) == typeof(nint3))
{
if (sizeof(nint3) == sizeof(int3))
return tryMulSIMD<int3>((int3*)arg1, universal.Create(arg2).ConvertTo<int3>(), count);
else
return tryMulSIMD<long3>((long3*)arg1, universal.Create(arg2).ConvertTo<long3>(), count);
}
else if (typeof(V) == typeof(nint4))
{
if (sizeof(nint4) == sizeof(int4))
return tryMulSIMD<int4>((int4*)arg1, universal.Create(arg2).ConvertTo<int4>(), count);
else
return tryMulSIMD<long4>((long4*)arg1, universal.Create(arg2).ConvertTo<long4>(), count);
}
else if (typeof(V) == typeof(nuint))
{
if (sizeof(nuint) == sizeof(uint))
return tryMulSIMD<uint>((uint*)arg1, universal.Create(arg2).ConvertTo<uint>(), count);
else
return tryMulSIMD<ulong>((ulong*)arg1, universal.Create(arg2).ConvertTo<ulong>(), count);
}
else if (typeof(V) == typeof(nuint2))
{
if (sizeof(nuint2) == sizeof(uint2))
return tryMulSIMD<uint2>((uint2*)arg1, universal.Create(arg2).ConvertTo<uint2>(), count);
else
return tryMulSIMD<ulong2>((ulong2*)arg1, universal.Create(arg2).ConvertTo<ulong2>(), count);
}
else if (typeof(V) == typeof(nuint3))
{
if (sizeof(nuint3) == sizeof(uint3))
return tryMulSIMD<uint3>((uint3*)arg1, universal.Create(arg2).ConvertTo<uint3>(), count);
else
return tryMulSIMD<ulong3>((ulong3*)arg1, universal.Create(arg2).ConvertTo<ulong3>(), count);
}
else if (typeof(V) == typeof(nuint4))
{
if (sizeof(nuint4) == sizeof(uint4))
return tryMulSIMD<uint4>((uint4*)arg1, universal.Create(arg2).ConvertTo<uint4>(), count);
else
return tryMulSIMD<ulong4>((ulong4*)arg1, universal.Create(arg2).ConvertTo<ulong4>(), count);
}
#endif
#if !DISABLE_HALF
if (_tryMulSIMD_AsTypeV<V, Half>.tryMulSIMD_AsType(arg1, arg2, count, elType, scalarCount, vectorlen)) return true;
#endif
if (_tryMulSIMD_AsTypeV<V, float>.tryMulSIMD_AsType(arg1, arg2, count, elType, scalarCount, vectorlen)) return true;
if (_tryMulSIMD_AsTypeV<V, double>.tryMulSIMD_AsType(arg1, arg2, count, elType, scalarCount, vectorlen)) return true;
if (_tryMulSIMD_AsTypeV<V, byte>.tryMulSIMD_AsType(arg1, arg2, count, elType, scalarCount, vectorlen)) return true;
if (_tryMulSIMD_AsTypeV<V, short>.tryMulSIMD_AsType(arg1, arg2, count, elType, scalarCount, vectorlen)) return true;
if (_tryMulSIMD_AsTypeV<V, ushort>.tryMulSIMD_AsType(arg1, arg2, count, elType, scalarCount, vectorlen)) return true;
if (_tryMulSIMD_AsTypeV<V, long>.tryMulSIMD_AsType(arg1, arg2, count, elType, scalarCount, vectorlen)) return true;
if (_tryMulSIMD_AsTypeV<V, ulong>.tryMulSIMD_AsType(arg1, arg2, count, elType, scalarCount, vectorlen)) return true;
if (_tryMulSIMD_AsTypeV<V, sbyte>.tryMulSIMD_AsType(arg1, arg2, count, elType, scalarCount, vectorlen)) return true;
if (_tryMulSIMD_AsTypeV<V, int>.tryMulSIMD_AsType(arg1, arg2, count, elType, scalarCount, vectorlen)) return true;
if (_tryMulSIMD_AsTypeV<V, uint>.tryMulSIMD_AsType(arg1, arg2, count, elType, scalarCount, vectorlen)) return true;
if (_tryMulSIMD_AsTypeV<V, decimal>.tryMulSIMD_AsType(arg1, arg2, count, elType, scalarCount, vectorlen)) return true;
return false;
}
static class _tryMulSIMD_AsTypeV<V, T> where V : unmanaged where T : unmanaged
{
static readonly int vLen = PepeHelper.GetVectorLength<V>();
public static unsafe bool tryMulSIMD_AsType(V* canvas, V arg, long count, Type elType, long scalarCount, long vectorLen)
{
if ((long)canvas % sizeof(Vector<T>) != 0)
return false;
if (elType == typeof(T) && Vector<T>.IsSupported && Vector<T>.Count % vectorLen == 0)
{
//Выравниваемся
if ((nint)canvas % simdAlign != 0)
{
universal<V>* canvas_u = (universal<V>*)canvas;
universal<V> M = universal.Create(arg);
universal<V>* canvas_u_end = (universal<V>*)(canvas + count);
while (canvas_u < canvas_u_end)
{
if ((nint)canvas_u % simdAlign == 0)
break;
else
{
*canvas_u *= M;
canvas_u++;
}
}
canvas = (V*)canvas_u;
count = canvas_u_end - canvas_u;
scalarCount = count * (sizeof(V) / sizeof(T));
}
Vector<T> VV = default;
{
V* ptr = (V*)&VV;
V* end = ptr + Vector<T>.Count / vectorLen;
while (ptr < end)
*ptr++ = arg;
}
var vectorCount = scalarCount / (sizeof(Vector<T>) / sizeof(T));
var cPtr = (Vector<T>*)canvas;
var canvasEnd = cPtr + vectorCount;
if (typeof(T) == typeof(float) && Hardcode.FloatAVX_only_for_aligned_32bytes_memory.IsGoodPointers(cPtr))
{
float* canvasF = (float*)cPtr;
if (Hardcode.FloatAVX_only_for_aligned_32bytes_memory.TryMul(ref canvasF, (float*)&VV, count))
{
cPtr = (Vector<T>*)canvasF;
}
}
else
{
{
var vector_pack4_count = vectorCount / 4;
var canvasEnd4 = cPtr + vector_pack4_count * 4;
while (cPtr < canvasEnd4)
{
*cPtr *= VV; cPtr++;
*cPtr *= VV; cPtr++;
*cPtr *= VV; cPtr++;
*cPtr *= VV; cPtr++;
}
}
}
{ //Хвостик
while (cPtr < canvasEnd)
{
*cPtr *= VV;
cPtr++;
}
}
//Кончик можно и на universalах
if (scalarCount % Vector<T>.Count > 0)
{
var canvasScalarPtr = (universal<V>*)cPtr;
var argScalar = *(universal<V>*)&arg;
var canvasEnd2 = (universal<V>*)canvas + scalarCount / vLen;
while (canvasScalarPtr < canvasEnd2)
{
*canvasScalarPtr *= argScalar;
canvasScalarPtr++;
}
}
return true;
}
else
return false;
}
}
Экспериментальное улучшение алгоритма быстрой степени
Одну из функций пеперубки стоит рассмотреть подробнее.
Яркость диодов нелинейна: если равномерно увеличивать её с 0% до 100%, то визуально диод набирает яркость сначала резко, потом очень быстро, и затем плавно замедляется к 100%.

Чтобы это дело выправить, цвета-команды, подаваемые на светодиоды, тоже должны быть нелинейными, но в обратную сторону: две нелинейности вместе компенсируют друг друга.


Эта штука называется гамма‑коррекцией, она крайне важна в работе с изображениями, и наших сверкающих делах тоже. Делается гамма‑коррекция просто: если каждый из каналов цвета RGB выражается числом от 0.0 до 1.0, то нам просто нужно возвести это число в степень.
Соответственно, у нас есть 2315 светодиодов, цвет каждого из них задаётся тремя вещественными числами. Всего 6945 чисел, каждое из которых надо будет возводить в степень для гамма‑коррекции.
Разумеется, это задача для пеперубки — в ней для этого есть алгоритм Pow, возводящий в степень наборы, массивы и буферы значений.

В процессе реализации Pow в пеперубке возникла проблема: операция Xn страшно медленная, и симдов для неё нет :( Сиди не симди, по одному считай. И если в солнечном окошке у нас были 8-битные значения, для которых можно построить таблицы готовых значений степеней (всего 256 вариантов), запомнить их и забыть про проблемы с нагрузкой на процессор, здесь, с float3, это не прокатит.
Проблему я решил с помощью особого алгоритма. Он сам по себе просто быстрый, так ещё и состоит из операций, которые можно векторизовать симдами.
Это алгоритм быстрого и неточного возведения вещественного числа в степень. Алгоритм использует особую волшебную константу и интерпретирует одни и те же биты числа то как вещественное число, то как целое.
//Магия начинается
float fastPow(float a, float b)
{
union
{
float d; //биты u интерпретируются как вещественное
int x; //биты u интерпретируются как целое
} u = { a };
//используем волшебную константу
u.x = (int)(b * (u.x - 1072632447) + 1072632447);
return u.d;
}
//Магия кончается
Алгоритм известный, в сети плавают его реализации для чисел одинарной (32 бита) и двойной точности (64 бита). Погрешность у двойной вменяемая, а вот у одинарной (которая и нужна для моих цветов, хранящихся как float3) она составляет 40%. Сорок процентов, Карл!
Меня это выбесило — решил пофиксить. Поначалу думал просто домножать на что‑нибудь, чтобы приблизить результат к эталону, но в итоге поставил тупой перебор всех возможных значений 32-битных констант и анализ точности при работе алгоритма с каждой. И нашёл.
Надо не 1072632447, а 1065353210!

Погрешность уменьшилась с 40% до 8%. Вместе с реализацией на анроллах и SIMD, эта гамма в 235 раз быстрее наивного варианта. Зачем весь интернет вымазали убогим вариантом с неправильной константой, я не понимаю. Пеперубка довольно урчит.
Быстрая точная неточная степень на симдах
[StructLayout(LayoutKind.Explicit)]
public unsafe struct float_int
{
[FieldOffset(0)]
public int i;
[FieldOffset(0)]
public float f;
public static implicit operator float_int(float f) => new float_int { f = f };
public static implicit operator float_int(int i) => new float_int { i = i };
}
[StructLayout(LayoutKind.Explicit)]
unsafe struct float_intV
{
[FieldOffset(0)]
public Vector<int> I;
[FieldOffset(0)]
public Vector<float> F;
public static implicit operator float_intV(float f) => new float_intV { F = new Vector<float>(f) };
public static implicit operator float_intV(int i) => new float_intV { I = new Vector<int>(i) };
public static implicit operator float_intV(Vector<float> f) => new float_intV { F = f };
public static implicit operator float_intV(Vector<int> i) => new float_intV { I = i };
}
//Выравнивание указателя и дообработка хвостика происходят снаружи
//Степень - скаляр
static unsafe Vector<float>* VBD_fast_powSIMD(Vector<float>* begin, long vectorCount, Vector<float> power)
{
float_intV POW = power;
Vector<int>* ptr = (Vector<int>*)begin;
Vector<int>* end = (Vector<int>*)begin + vectorCount;
//var C = new Vector<int>(0x3fef127f);
var C = new Vector<int>(0x3f7ffffa);
var CF = Vector.ConvertToSingle(C);
{ //анролл по 4
var end4 = (Vector<int>*)begin + vectorCount / 4 * 4;
while (ptr < end4)
{
*ptr = Vector.ConvertToInt32(POW.F * Vector.ConvertToSingle(*ptr - C) + CF); ptr++;
*ptr = Vector.ConvertToInt32(POW.F * Vector.ConvertToSingle(*ptr - C) + CF); ptr++;
*ptr = Vector.ConvertToInt32(POW.F * Vector.ConvertToSingle(*ptr - C) + CF); ptr++;
*ptr = Vector.ConvertToInt32(POW.F * Vector.ConvertToSingle(*ptr - C) + CF); ptr++;
}
}
while (ptr < end)
{
*ptr = Vector.ConvertToInt32(POW.F * Vector.ConvertToSingle(*ptr - C) + CF);
ptr++;
}
return (Vector<float>*)ptr;
}
//Степень - вектор 3D
static unsafe Vector<float>* VBD_fast_powSIMD_triple(Vector<float>* begin, long vectorCount, Vector<float> power_A, Vector<float> power_B, Vector<float> power_C)
{
float_intV POW_A = power_A;
float_intV POW_B = power_B;
float_intV POW_C = power_C;
Vector<int>* ptr = (Vector<int>*)begin;
Vector<int>* end = (Vector<int>*)begin + vectorCount;
var C = new Vector<int>(0x3f7ffffa);
var CF = Vector.ConvertToSingle(C);
{ //Отдельно пачкуемся по 3. Это не анролл, т.к. у нас 3 разных вектора подряд
var end3 = (Vector<int>*)begin + vectorCount / 3 * 3;
while (ptr < end3)
{
*ptr = Vector.ConvertToInt32(POW_A.F * Vector.ConvertToSingle(*ptr - C) + CF); ptr++;
*ptr = Vector.ConvertToInt32(POW_B.F * Vector.ConvertToSingle(*ptr - C) + CF); ptr++;
*ptr = Vector.ConvertToInt32(POW_C.F * Vector.ConvertToSingle(*ptr - C) + CF); ptr++;
}
}
while (ptr < end)
{
*ptr = Vector.ConvertToInt32(POW_A.F * Vector.ConvertToSingle(*ptr - C) + CF); ptr++;
if (ptr >= end) break;
*ptr = Vector.ConvertToInt32(POW_B.F * Vector.ConvertToSingle(*ptr - C) + CF); ptr++;
if (ptr >= end) break;
*ptr = Vector.ConvertToInt32(POW_C.F * Vector.ConvertToSingle(*ptr - C) + CF); ptr++;
}
return (Vector<float>*)ptr;
}

Практика выявила проблему: и при старой, и при новой константе возведение 0 в степень от 0,999 999 до 2.1 выдаёт значения масштаба 1035. И когда вот это вот прилетает в преобразователь HDR→SDR, подсветка честно пытается выйти на планковскую мощность и явить сверхновую. Подпёр костылём: если степень лежит в этом диапазоне, то сначала исходные значения впихиваются в диапазон от 0.000001 до 1000000. Разумеется, с оптимизациями.
Экспериментальные абстрагированные абстрактные абстракции

Подсветка у нас обладает следующими особенностями:
Ленты состоят из 36 перепутанных фрагментов, сцепленных абы как
Контуров не один, а два: один светит назад, второй вдоль стен
Контуры восьмиугольные, а не прямоугольные — углы‑то скошены

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

Под это месиво надо как‑то программировать эффекты. Как‑то так, чтобы не поседеть, пока распутываешь где, кто и как подключён.
Я напомню, что ленты у меня соединены как‑то так:

Помимо перепутанности лент есть ещё один аспект: я решил, что новый софт должен считать цвета в формате float3, а не byte3, как это делала старая версия софта. Фокус в том, что контроллер управления лентами, как и сами ленты, по‑прежнему работают в формате дискретного byte3.
Очевидно, что между физическими перепутанными byte3 лентами и софтом на float3 просится некоторая абстракция. И она есть.
Софт работает не с физическими, а с логическими лентами, состоящими из логических светодиодов. Цвет логического светодиода задаётся плавным значением float3 и поддерживает HDR, то есть может быть ярче 100%.

Вся работа софта сводится к заданию цветов логических светодиодов на логических лентах, и только в самом конце, для передачи на контроллер и физические ленты с физическими светодиодами, значения конвертируются из float3 в byte3 и переставляются в соответствии с перепутанным подключением физических лент к контроллеру. И да — всё это делается быстро и оптимизировано.
Основных уровней абстракций получилось шесть. С помощью них можно описать вообще любую конфигурацию любой подсветки, которая состоит из каких‑то лент, которые как‑то расположены, а не только мою.
1. Базовый физический уровень
Здесь описываются блоки питания и ТТХ единичного физического светодиода, из которых мы будем городить огород.

2. Физические ленты
Каждой ленте назначается свой БП. В моём случае это те самые 17 лент, составленные из кусочков. Некоторые из кусочков физически удалены друг от друга и принадлежат разным краям телика. Здесь же каждой физической ленте назначается свой блок питания (в моём случае их 3).

3. Фрагменты физических лент
Ссылки на кусочки физических лент — от такого‑то до такого‑то светодиода. Вдобавок, кусочек хранит свои координаты в пространстве — софт знает физическое место каждого диода и может строить эффекты в «мировых» координатах в миллиметрах.

Да — фактически, я сначала в реальности разрезал ленты на куски бокорезами, приклеил их, соединил некоторые между собой проводами, а затем на программном уровне снова разделил обратно. Так надо.
4. Логические ленты
Наборы фрагментов физических лент. В моём случае их 16 штук: пара верхних, пара нижних, боковушки и скосы. Здесь уже можно работать с верхней гранью всей установки как с единым целым, или каким-нибудь верхним левым скосом, не заморачиваясь, куда и что там подсоединено и в каком порядке.

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

6. Инсталляция
Инсталляция объединяет в себе все контуры светодиодной установки.

Слои абстракции позволяют произвольно задавать типы, которыми задаются цвета диодов и даже размерность Вселенной, где висят ленты. Да, можно один конец повесить в 5D мире, а другой — в дискретном 8D, поэтому, если в очередной раз вы захотите обклеить подсветкой для тетрахроматов свой пятимерный галактический истребитель — решение уже есть. Хорошо, когда самый актуальный и важный функционал заранее предусмотрен.
И вот уже на базе этих слоёв наследованы объекты, описывающие мою установку. Именно тут появляются понятия «дальний свет» и «ближний свет», «левый телик», «верхний правый скос» и прочее.
Изначально я хотел вынести эти объекты в подгружаемые модули‑плагины, чтобы мой софт можно было заточить под вообще любую подсветку без переделывания, но решил, что это уже перебор — слишком геморно, концепция существенно усложняется, результат понятен, а путь к нему тернист. Так что слои описания конкретно моей установки я тупо захардкодил в отдельной библиотеке.
Как описана инсталляция
using Ambiknight;
using Ambiknight.LEDCore.Physical;
using Ambiknight.LEDCore.Installations;
using Ambiknight.Pepectors;
using System.Collections.ObjectModel;
namespace Ambiknight.Kernel
{
public record struct LEDInfo(int GlobalLEDIndex, TV TV, Tier Tier, Side Side, LogicalStripe LogicalStripe, PhysicalSpan PhysicalSpan, int LocalPhysicalSpanIndex)
{
public PhysicalStripe PhysicalStripe => PhysicalSpan.ParentPhysicalStripe;
public PhysicalStripeInfo PhysicalInfo => PhysicalStripe.Info;
}
public class StandardInstallation : PlacedLogicalStripe1DInstallation<StandardPhysicalInstallation, Chain, LogicalStripe, PhysicalSpan, PhysicalStripe, float3, STM32Color, float2, float4>
{
public BackRingChain Back => Chains[0] as BackRingChain;
public WideRingChain Wide => Chains[1] as WideRingChain;
public StandardInstallation() : base(new StandardPhysicalInstallation().Dim(out var physical), (a, b) => (Pepe.Min(a.xy, b.xy), Pepe.Max(a.zw, b.zw)), createBack(physical), createWide(physical))
{
Left = new("Две левые вертикальные полоски на левом телике", Back.Left, Wide.Left);
Top = new("Две верхние горизонтальные полоски над всеми тремя теликами", Back.Top, Wide.Top);
Right = new("Две правые вертикальные полоски на правом телике", Back.Right, Wide.Right);
Bottom = new("Две нижние горизонтальные полоски под всеми тремя теликами", Back.Bottom, Wide.Bottom);
LeftTopBevel = new("Левый верхний скос", Back.LeftTopBevel, Wide.LeftTopBevel);
LeftBottomBevel = new("Левый нижний скос", Back.LeftBottomBevel, Wide.LeftBottomBevel);
RightTopBevel = new("Правый верхний скос", Back.RightTopBevel, Wide.RightTopBevel);
RightBottomBevel = new("Правый нижний скос", Back.RightBottomBevel, Wide.RightBottomBevel);
LEDInfos = new ReadOnlyCollection<LEDInfo>(buildLEDIndex());
InfosByTV = new ReadOnlyDictionary<TV, ReadOnlyCollection<LEDInfo>>(buildTVIndex());
}
LEDInfo[] buildLEDIndex()
{
var result = new LEDInfo[LEDCount];
var t = LEDInfos;
int globalIndex = 0;
foreach (var virtualStripe in Stripes)
foreach (var physicalSpan in virtualStripe.Spans)
for (int physicalSpanLEDIndex = 0; physicalSpanLEDIndex < physicalSpan.LEDCount; physicalSpanLEDIndex++)
{
result[globalIndex] = new(globalIndex, physicalSpan.TV, physicalSpan.Tier, physicalSpan.Side, virtualStripe, physicalSpan, physicalSpanLEDIndex);
globalIndex++;
}
if (globalIndex != LEDCount)
throw new Exception("Не все светодиоды окучены!");
return result;
}
Dictionary<TV,ReadOnlyCollection<LEDInfo>> buildTVIndex()
{
var tv = new Dictionary<TV, List<LEDInfo>>();
tv[TV.Left] = new List<LEDInfo>();
tv[TV.Center] = new List<LEDInfo>();
tv[TV.Right] = new List<LEDInfo>();
foreach (var item in LEDInfos)
tv[item.TV].Add(item);
return tv.Select(a => KeyValuePair.Create(a.Key, new ReadOnlyCollection<LEDInfo>(a.Value))).ToDictionary();
}
public ReadOnlyCollection<LEDInfo> LEDInfos { get; }
public ReadOnlyDictionary<TV, ReadOnlyCollection<LEDInfo>> InfosByTV { get; }
//Буим мерить всё в миллиметрах
//Координаты задаём в 2Д. Можно было бы и в 3Д, т.к. телики шевелятся
//Но реального профита от этого почти 0, а гемора горааааздо больше
//Так что считаем, что телики в одной плоскости
//Перечисляем светодиоды по часовой стрелке начиная от левого края верхней ленты (не скос, а именно верхняя лента)
//Наш замкнутый контур светодиодных лент - не прямоугольник, а восьмиугольник
//Так как углы немного скошены
//Это тоже учитываем
// Ближний свет
static BackRingChain createBack(StandardPhysicalInstallation physical)
{
//Верхняя сторона ---
var topCord = -343;
var top = new TopStripe
(
Tier.Back,
new SideTopPhysicalSpan(physical.LeftTV.Back.TopStripe, 143, (-1739, topCord), 0, (-738, topCord)),
new SideTopPhysicalSpan(physical.LeftTV.Back.FragmentaryStripe, 12, (-726, topCord), 0, (-642, topCord)),
new CenterTopPhysicalSpan(physical.CenterTV.Fragmentary, 22, (-583, topCord), 45, (-422, topCord), Tier.Back),
new CenterTopPhysicalSpan(physical.CenterTV.TopBack, 0, (-410, topCord), 143, (590, topCord)),
new SideTopPhysicalSpan(physical.RightTV.Back.FragmentaryStripe, 0, (642, topCord), 13, (733, topCord)),
new SideTopPhysicalSpan(physical.RightTV.Back.TopStripe, 0, (751, topCord), 142, (1745, topCord))
);
var c1 = top.LEDCount;
var c2 = top.Spans.Select(a => a.LEDCount).Sum();
//Правый верхний скос \
var rightTopBevel = new RightTopBevelStripe(Tier.Back, new SideBevelPhysicalSpan(Side.BevelRightTop, physical.RightTV.Back.FragmentaryStripe, 14, (1758, -335), 27, (1822, -271)));
//Правая сторона сверху вниз |
var right = new RightStripe(Tier.Back, new SideRightPhysicalSpan(physical.RightTV.Back.FragmentaryStripe, 28, (1829, -256), 101, (1829, 250)));
//Правый нижний скос /
var rightBottomBevel = new RightBottomBevelStripe(Tier.Back, new SideBevelPhysicalSpan(Side.BevelRightBottom, physical.RightTV.Back.FragmentaryStripe, 102, (1822, 274), 118, (1740, 338)));
//Нижняя сторона СПРАВА НАЛЕВО (наоборот!) ---
var bottomCord = 342;
var bottom = new BottomStripe
(
Tier.Back,
new SideBottomPhysicalSpan(physical.RightTV.Back.BottomStripe, 143, (1726, bottomCord), 0, (725, bottomCord)),
new SideBottomPhysicalSpan(physical.RightTV.Back.FragmentaryStripe, 130, (718, bottomCord), 119, (641, bottomCord)),
new CenterPhysicalSpan(Side.Bottom, physical.CenterTV.BottomBack, 141, (585, bottomCord), 0, (-408, bottomCord)),
new CenterPhysicalSpan(Side.Bottom, physical.CenterTV.Fragmentary, 92, (-418, bottomCord), 69, (-579, bottomCord), Tier.Back),
new SideBottomPhysicalSpan(physical.LeftTV.Back.FragmentaryStripe, 116, (-643, bottomCord), 126, (-712, bottomCord)),
new SideBottomPhysicalSpan(physical.LeftTV.Back.BottomStripe, 0, (-723, bottomCord), 143, (-1724, bottomCord))
);
//Нижний левый скос СНИЗУ ВВЕРХ \
var leftBottomBevel = new LeftBottomBevelStripe(Tier.Back, new SideBevelPhysicalSpan(Side.BevelLeftBottom, physical.LeftTV.Back.FragmentaryStripe, 115, (-1743, 335), 100, (-1821, 275)));
//Левая сторона СНИЗУ ВВЕРХ |
var left = new LeftStripe(Tier.Back, new SideLeftPhysicalSpan(physical.LeftTV.Back.FragmentaryStripe, 99, (-1829, 253), 26, (-1829, -253)));
//Левый верхний скос СНИЗУ ВВЕРХ /
var leftTopBevel = new LeftTopBevelStripe(Tier.Back, new SideBevelPhysicalSpan(Side.BevelLeftTop, physical.LeftTV.Back.FragmentaryStripe, 25, (-1819, -273), 13, (-1760, -333)));
//Формируем контур из этих восьми сегментов
var result = new BackRingChain
(
"Контур ближнего света",
top,
rightTopBevel,
right,
rightBottomBevel,
bottom,
leftBottomBevel,
left,
leftTopBevel
);
return result;
}
//Дальний свет
static WideRingChain createWide(StandardPhysicalInstallation physical)
{
//Верхняя сторона
var topCord = -343;
var top = new TopStripe
(
Tier.Wide,
new SideTopPhysicalSpan(physical.LeftTV.Wide.TopStripe, 143, (-1739, topCord), 0, (-738, topCord)),
new SideTopPhysicalSpan(physical.LeftTV.Wide.FragmentaryStripe, 11, (-722, topCord), 0, (-645, topCord)),
new CenterTopPhysicalSpan(physical.CenterTV.Fragmentary, 0, (-576, topCord), 21, (-428, topCord), Tier.Wide ),
new CenterTopPhysicalSpan(physical.CenterTV.TopWide, 0, (-404, topCord), 142, (589, topCord)),
new SideTopPhysicalSpan(physical.RightTV.Wide.FragmentaryStripe, 0, (646, topCord), 10, (723, topCord)),
new SideTopPhysicalSpan(physical.RightTV.Wide.TopStripe, 0, (737, topCord), 143, (1738, topCord))
);
//Правый верхний скос
var rightTopBevel = new RightTopBevelStripe(Tier.Wide, new SideBevelPhysicalSpan(Side.BevelRightTop, physical.RightTV.Wide.FragmentaryStripe, 11, (1761, -328), 22, (1814, -274)));
//Правая сторона сверху вниз
var right = new RightStripe(Tier.Wide, new SideRightPhysicalSpan(physical.RightTV.Wide.FragmentaryStripe, 23, (1824, -252), 95, (1824, 248)));
//Правый нижний скос
var rightBottomBevel = new RightBottomBevelStripe(Tier.Wide, new SideBevelPhysicalSpan(Side.BevelRightBottom, physical.RightTV.Wide.FragmentaryStripe, 96, (1813, 275), 109, (1745, 329)));
//Нижняя сторона СПРАВА НАЛЕВО (наоборот!)
var bottomCord = 342;
var bottom = new BottomStripe
(
Tier.Wide,
new SideBottomPhysicalSpan(physical.RightTV.Wide.BottomStripe, 142, (1719, bottomCord), 0, (725, bottomCord)),
new SideBottomPhysicalSpan(physical.RightTV.Wide.FragmentaryStripe, 120, (712, bottomCord), 110, (642, bottomCord)),
new CenterPhysicalSpan(Side.Bottom, physical.CenterTV.BottomWide, 143, (586, bottomCord), 0, (-414, bottomCord)),
new CenterPhysicalSpan(Side.Bottom, physical.CenterTV.Fragmentary, 68, (-425, bottomCord), 46, (-579, bottomCord), Tier.Wide),
new SideBottomPhysicalSpan(physical.LeftTV.Wide.FragmentaryStripe, 111, (-650, bottomCord), 120, (-712, bottomCord)),
new SideBottomPhysicalSpan(physical.LeftTV.Wide.BottomStripe, 0, (-726, bottomCord), 142, (-1720, bottomCord))
);
//Нижний левый скос СНИЗУ ВВЕРХ
var leftBottomBevel = new LeftBottomBevelStripe(Tier.Wide, new SideBevelPhysicalSpan(Side.BevelLeftBottom, physical.LeftTV.Wide.FragmentaryStripe, 110, (-1746, 328), 97, (-1813, 275)));
//Левая сторона СНИЗУ ВВЕРХ
var left = new LeftStripe(Tier.Wide, new SideLeftPhysicalSpan(physical.LeftTV.Wide.FragmentaryStripe, 96, (-1824, 250), 24, (-1824, -249)));
//Левый верхний скос СНИЗУ ВВЕРХ
var leftTopBevel = new LeftTopBevelStripe(Tier.Wide, new SideBevelPhysicalSpan(Side.BevelLeftTop, physical.LeftTV.Wide.FragmentaryStripe, 23, (-1815, -274), 12, (-1760, -328)));
//Формируем контур из этих восьми сегментов
var result = new WideRingChain
(
"Контур дальнего света",
top,
rightTopBevel,
right,
rightBottomBevel,
bottom,
leftBottomBevel,
left,
leftTopBevel
);
return result;
}
public LeftMultichain Left { get; }
public TopMultichain Top { get; }
public RightMultichain Right { get; }
public BottomMultichain Bottom { get; }
public LeftTopBevelMultichain LeftTopBevel { get; }
public RightTopBevelMultichain RightTopBevel { get; }
public LeftBottomBevelMultichain LeftBottomBevel { get; }
public RightBottomBevelMultichain RightBottomBevel { get; }
public Chain LeftBevels { get; }
public Chain RightBevels { get; }
public Chain TopBevels { get; }
public Chain BottomBevels { get; }
protected override void dispose(string callerMemberName, int callerLineNumber, string callerFilePath)
{
base.dispose(callerMemberName, callerLineNumber, callerFilePath);
}
}
}
Несмотря на то, что вся установка и её уровни систематизации существуют в виде объектов внутри софта, задавать на ней какие‑то цвета напрямую нельзя. Согласно концепции, мы должны рисовать всё на логическом 1D‑кадре, после чего выводить его на установку. Это аналогично тому, как мы не можем напрямую взаимодействовать с пикселем 2D‑дисплея у компа: вместо этого мы рисуем в некотором 2D‑буфере в памяти, а затем выводим его на дисплей.
Как описан логический одномерный кадр для логических лент
using Ambiknight;
using Ambiknight.LEDFrames;
namespace Ambiknight.Kernel
{
public class Frame : LEDFrame<FrameSpan, StandardInstallation>
{
public Frame(StandardInstallation? targetLEDInstallation = null) : base(targetLEDInstallation ?? new StandardInstallation())
{
Back = buildPack(TargetLEDInstallation.Back);
Wide = buildPack(TargetLEDInstallation.Wide);
Left = new(TargetLEDInstallation, Back.Left, Wide.Left);
Top = new(TargetLEDInstallation, Back.Top, Wide.Top);
Right = new(TargetLEDInstallation, Back.Right, Wide.Right);
Bottom = new(TargetLEDInstallation, Back.Bottom, Wide.Bottom);
LeftTopBevel = new FrameSpanPair(TargetLEDInstallation, Back.LeftTopBevel, Wide.LeftTopBevel);
RightTopBevel = new FrameSpanPair(TargetLEDInstallation, Back.RightTopBevel, Wide.RightTopBevel);
LeftBottomBevel = new FrameSpanPair(TargetLEDInstallation, Back.LeftBottomBevel, Wide.LeftBottomBevel);
RightBottomBevel = new FrameSpanPair(TargetLEDInstallation, Back.RightBottomBevel, Wide.RightBottomBevel);
Both = new FrameSpanPair(TargetLEDInstallation, new FrameSpan(this, 0, TargetLEDInstallation.Back.LEDCount), new FrameSpan(this, TargetLEDInstallation.Back.LEDCount, TargetLEDInstallation.Wide.LEDCount));
Bevels = new Bevels(RightTopBevel, RightBottomBevel, LeftBottomBevel, LeftTopBevel);
TopBevels = new TopBevelPair(TargetLEDInstallation, LeftTopBevel, RightTopBevel);
LeftBevels = new LeftBevelPair(TargetLEDInstallation, LeftTopBevel, LeftBottomBevel);
BottomBevels = new BottomBevelPair(TargetLEDInstallation, LeftBottomBevel, RightBottomBevel);
RightBevels = new RightBevelPair(TargetLEDInstallation, RightTopBevel, RightBottomBevel);
{
(FrameSpanPair top, FrameSpanPair bottom, FrameSpanPair vertical, FrameSpanPair topBevel, FrameSpanPair bottomBevel) = getTVPairs(TV.Left);
LeftTV = new LeftTVSpanCollection(top, bottom, vertical, topBevel, bottomBevel);
}
{
(FrameSpanPair top, FrameSpanPair bottom, _, _, _) = getTVPairs(TV.Center);
CenterTV = new CenterTVSpanCollection(top, bottom);
}
{
(FrameSpanPair top, FrameSpanPair bottom, FrameSpanPair vertical, FrameSpanPair topBevel, FrameSpanPair bottomBevel) = getTVPairs(TV.Right);
RightTV = new RightTVSpanCollection(top, bottom, vertical, topBevel, bottomBevel);
}
TVs = new ReadonlyListionary<TV, TVSpanCollection>(new Dictionary<TV, TVSpanCollection> { [TV.Left] = LeftTV, [TV.Center] = CenterTV, [TV.Right] = RightTV });
SideTVs = new ReadonlyListionary<TV, SideTVSpanCollection>(new Dictionary<TV, SideTVSpanCollection> { [TV.Left] = LeftTV, [TV.Right] = RightTV });
}
(FrameSpanPair top, FrameSpanPair bottom, FrameSpanPair? vertical, FrameSpanPair? topBevel, FrameSpanPair? bottomBevel) getTVPairs(TV tv)
{
var dictionary = new Dictionary<(Side side, Tier tier), List<LEDInfo>>();
foreach (var item in TargetLEDInstallation.InfosByTV[tv])
{
if (!dictionary.TryGetValue((item.Side, item.Tier), out var list) || list is null)
{
list = new();
dictionary[(item.Side, item.Tier)] = list;
}
list.Add(item);
}
FrameSpan? extractSpan(Side side, Tier tier)
{
var key = (side, tier);
if (!dictionary.TryGetValue(key, out var list))
return null;
if (dictionary.TryGetValue((side, Tier.Both), out var list2) && list2?.Count > 0)
list.AddRange(list2);
var fromIndex = list.Select(a => a.GlobalLEDIndex).Min();
var toIndex = list.Select(a => a.GlobalLEDIndex).Max();
for (int i = fromIndex; i < toIndex; i++)
if (list.FirstOrDefault(a => a.GlobalLEDIndex == i) == default)
throw new Exception($"Не удается получить единый кусок для {side} {tier}");
return new FrameSpan(this, fromIndex, toIndex - fromIndex);
}
FrameSpanPair extractSpanPair(Side side)
{
var backSpan = extractSpan(side, Tier.Back);
var wideSpan = extractSpan(side, Tier.Wide);
if (backSpan is null || wideSpan is null)
if (backSpan is null && wideSpan is null)
return null;
else
{
#if DEBUG
throw new Exception("Так не бывает >:(");
#else
return null;
#endif
}
#if DEBUG
if (Math.Abs(backSpan.LEDCount - wideSpan.LEDCount) > 20)
{
throw new Exception("Что-то как то слишком большая разница между числом светодиодов заднего и ближнего света");
}
#endif
return new FrameSpanPair(TargetLEDInstallation, backSpan, wideSpan);
}
return
(
extractSpanPair(Side.Top),
extractSpanPair(Side.Bottom),
extractSpanPair(tv == TV.Left ? Side.Left : Side.Right),
extractSpanPair(tv == TV.Left ? Side.BevelLeftTop : Side.BevelRightTop),
extractSpanPair(tv == TV.Left ? Side.BevelLeftBottom : Side.BevelRightBottom)
);
}
private FrameSpan CreateSpan(LogicalStripe stripe)
{
var (from, to) = TargetLEDInstallation.GetIndexes(stripe);
return new FrameSpanPlaced(stripe, this, from, to - from);
}
private SpanConture buildPack(RingChain chain) =>
new SpanConture(
CreateSpan(chain.Top),
CreateSpan(chain.RightTopBevel),
CreateSpan(chain.Right),
CreateSpan(chain.RightBottomBevel),
CreateSpan(chain.Bottom),
CreateSpan(chain.LeftBottomBevel),
CreateSpan(chain.Left),
CreateSpan(chain.LeftTopBevel)
);
public override Frame Clone()
{
var result = new Frame(TargetLEDInstallation);
result.CopyFrom(this);
return result;
}
/// <summary>
/// Значения, соответствующие полоскам светодиодов на левом краю левого телика (ближний и дальний свет)
/// </summary>
public VerticalFrameSpanPair Left { get; }
/// <summary>
/// Значения, соответствующие полоскам светодиодов на верхних границах теликов (ближний и дальний свет)
/// </summary>
public HorisontalFrameSpanPair Top { get; }
/// <summary>
/// Значения, соответствующие полоскам светодиодов на правом краю правого телика (ближний и дальний свет)
/// </summary>
public VerticalFrameSpanPair Right { get; }
/// <summary>
/// Значения, соответствующие полоскам светодиодов на нижних границах теликов (ближний и дальний свет)
/// </summary>
public HorisontalFrameSpanPair Bottom { get; }
/// <summary>
/// Значения, соответствующие полоскам светодиодов левого верхнего скоса - который в кондей смотрит, ближний и дальний свет
/// </summary>
public FrameSpanPair LeftTopBevel { get; }
/// <summary>
/// Значения, соответствующие полоскам светодиодов правого верхнего скоса, ближний и дальний свет
/// </summary>
public FrameSpanPair RightTopBevel { get; }
/// <summary>
/// Значения, соответствующие полоскам светодиодов левого нижнего скоса, ближний и дальний свет
/// </summary>
public FrameSpanPair LeftBottomBevel { get; }
/// <summary>
/// Значения, соответствующие полоскам светодиодов правого нижнего скоса, ближний и дальний свет
/// </summary>
public FrameSpanPair RightBottomBevel { get; }
/// <summary>
/// Значения, соответствующие цепочке светодиодных лент ближнего света - который в упор в стену светит, то есть назад
/// </summary>
public SpanConture Back { get; }
/// <summary>
/// Значения, соответствующие цепочке светодиодных лент дальнего света - который светит в стороны, в стены, в потолок
/// </summary>
public SpanConture Wide { get; }
/// <summary>
/// Значения, соответствующие всем светодиодным лентам, однако, ближний и дальний свет будут обсчитываться независимо-параллельно, а не как идущие друг за другом ленты
/// </summary>
public FrameSpanPair Both { get; }
/// <summary>
/// Значения, соответствующие всем восьми светодиодным лентам на скосах - ближних и дальних
/// </summary>
public Bevels Bevels { get; }
/// <summary>
/// Значения, соответствующие четырём светодиодным лентам на верхних скосах
/// </summary>
public TopBevelPair TopBevels { get; }
/// <summary>
/// Значения, соответствующие четырём светодиодным лентам на нижних скосах
/// </summary>
public BottomBevelPair BottomBevels { get; }
/// <summary>
/// Значения, соответствующие четырём светодиодным лентам на левых скосах
/// </summary>
public LeftBevelPair LeftBevels { get; }
/// <summary>
/// Значения, соответствующие четырём светодиодным лентам на правых скосах
/// </summary>
public RightBevelPair RightBevels { get; }
/// <summary>
/// Значения, соответствующие лентам на левом телике
/// </summary>
public LeftTVSpanCollection LeftTV { get; }
/// <summary>
/// Значения, соответствующие лентам на центральном телике
/// </summary>
public CenterTVSpanCollection CenterTV { get; }
/// <summary>
/// Значения, соответствующие лентам на правом телике
/// </summary>
public RightTVSpanCollection RightTV { get; }
public ReadonlyListionary<TV, TVSpanCollection> TVs { get; }
public ReadonlyListionary<TV, SideTVSpanCollection> SideTVs { get; }
public SpanConture GetConture(int index) => index % 2 == 0 ? Back : Wide;
protected override IEnumerable<FrameSpan> GetSpans()
{
foreach (var item in Back)
yield return item;
foreach (var item in Wide)
yield return item;
}
protected override FrameSpan CreateSpan(nint startPointer, long count) => new FrameSpan(startPointer, count, this);
}
public class Bevels(FrameSpanPair RightTopBevel, FrameSpanPair RightBottomBevel, FrameSpanPair LeftBottomBevel, FrameSpanPair LeftTopBevel) :
LEDSpanCollection([.. RightTopBevel, .. RightBottomBevel, .. LeftBottomBevel, .. LeftTopBevel], false)
{
public FrameSpanPair RightTopBevel { get; } = RightTopBevel;
public FrameSpanPair RightBottomBevel { get; } = RightBottomBevel;
public FrameSpanPair LeftBottomBevel { get; } = LeftBottomBevel;
public FrameSpanPair LeftTopBevel { get; } = LeftTopBevel;
}
public abstract class BevelPair(StandardInstallation targetInstalltion, FrameSpanPair a, FrameSpanPair b) : LEDSpanCollection([.. a, .. b], false)
{
protected FrameSpanPair A { get; } = a;
protected FrameSpanPair B { get; } = b;
/// <summary>
/// Оба скоса ближнего света
/// </summary>
public FrameSpanPair Back { get; } = new FrameSpanPair(targetInstalltion, a.Back, b.Back);
/// <summary>
/// Оба скоса дальнего света
/// </summary>
public FrameSpanPair Wide { get; } = new FrameSpanPair(targetInstalltion, a.Wide, b.Wide);
}
public class HorisontalBevelPair(StandardInstallation targetInstallation, FrameSpanPair Left, FrameSpanPair Right) : BevelPair(targetInstallation, Left, Right)
{
/// <summary>
/// Оба левых скоса - дальний и ближний
/// </summary>
public FrameSpanPair Left => A;
/// <summary>
/// Оба правых скоса - дальний и ближний
/// </summary>
public FrameSpanPair Right => B;
}
public class VerticalBevelPair(StandardInstallation targetInstallation, FrameSpanPair Top, FrameSpanPair Bottom) : BevelPair(targetInstallation, Top, Bottom)
{
/// <summary>
/// Оба верхних скоса - дальний и ближний
/// </summary>
public FrameSpanPair Top => A;
/// <summary>
/// Оба нижних скоса - дальний и ближний
/// </summary>
public FrameSpanPair Bottom => B;
}
public class TopBevelPair(StandardInstallation targetInstallation, FrameSpanPair Left, FrameSpanPair Right) : HorisontalBevelPair(targetInstallation, Left, Right) { }
public class BottomBevelPair(StandardInstallation targetInstallation, FrameSpanPair Left, FrameSpanPair Right) : HorisontalBevelPair(targetInstallation, Left, Right) { }
public class LeftBevelPair(StandardInstallation targetInstallation, FrameSpanPair Top, FrameSpanPair Bottom) : VerticalBevelPair(targetInstallation, Top, Bottom) { }
public class RightBevelPair(StandardInstallation targetInstallation, FrameSpanPair Top, FrameSpanPair Bottom) : VerticalBevelPair(targetInstallation, Top, Bottom) { }
public class TVSpanCollection(FrameSpanPair top, FrameSpanPair bottom, params FrameSpanPair[] anotherSpans) : LEDSpanCollection([.. top, .. bottom, .. anotherSpans.SelectMany(a => a)], false)
{
/// <summary>
/// Пара горизонтальных лент в верхней части телика
/// </summary>
public FrameSpanPair Top { get; } = top;
/// <summary>
/// Пара горизонтальных лент в нижней части телика
/// </summary>
public FrameSpanPair Bottom { get; } = bottom;
}
public abstract class TVSpanCollection<T>(FrameSpanPair top, FrameSpanPair bottom, params FrameSpanPair[] anotherSpans) : TVSpanCollection(top, bottom, anotherSpans) where T : TVContureSpanCollection
{
/// <summary>
/// Ленты ближнего света на данном телике
/// </summary>
public abstract T Back { get; }
/// <summary>
/// Ленты дальнего света на данном телике
/// </summary>
public abstract T Wide { get; }
}
public class SideTVSpanCollection(FrameSpanPair top, FrameSpanPair bottom, FrameSpanPair topBevel, FrameSpanPair vertical, FrameSpanPair bottomBevel) : TVSpanCollection(top, bottom, topBevel, vertical, bottomBevel)
{
/// <summary>
/// Все четыре ленты-скосы бокового телика
/// </summary>
public LEDSpanCollection Bevels { get; } = new LEDSpanCollection([.. topBevel, .. bottomBevel], false);
/// <summary>
/// Пара горизонтальных лент в верхней части телика
/// </summary>
public FrameSpanPair Top { get; } = top;
/// <summary>
/// Пара горизонтальных лент в нижней части телика
/// </summary>
public FrameSpanPair Bottom { get; } = bottom;
/// <summary>
/// Пара вертикальных лент вдоль бокового ребра телика
/// </summary>
public FrameSpanPair Vertical { get; } = vertical;
/// <summary>
/// Пара лент скоса на верхнем углу бокового телика
/// </summary>
public FrameSpanPair TopBevel { get; } = topBevel;
/// <summary>
/// Пара лент скоса на нижнем углу бокового телика
/// </summary>
public FrameSpanPair BottomBevel { get; } = bottomBevel;
/// <summary>
/// Ленты ближнего света бокового телика
/// </summary>
public SideTVContureSpanCollection Back { get; } = new SideTVContureSpanCollection(top.Back, bottom.Back, topBevel.Back, vertical.Back, bottomBevel.Back);
/// <summary>
/// Ленты дальнего света бокового телика
/// </summary>
public SideTVContureSpanCollection Wide { get; } = new SideTVContureSpanCollection(top.Wide, bottom.Wide, topBevel.Wide, vertical.Wide, bottomBevel.Wide);
}
public abstract class SideTVSpanCollection<T>(FrameSpanPair top, FrameSpanPair bottom, FrameSpanPair vertical, FrameSpanPair topBevel, FrameSpanPair bottomBevel) : SideTVSpanCollection(top, bottom, vertical, topBevel, bottomBevel) where T : TVContureSpanCollection
{
/// <summary>
/// Ленты ближнего света на данном телике
/// </summary>
public abstract new T Back { get; }
/// <summary>
/// Ленты дальнего света на данном телике
/// </summary>
public abstract new T Wide { get; }
}
public class LeftTVSpanCollection(FrameSpanPair top, FrameSpanPair bottom, FrameSpanPair left, FrameSpanPair topBevel, FrameSpanPair bottomBevel) : SideTVSpanCollection<LeftTVContureSpanCollection>(top, bottom, left, topBevel, bottomBevel)
{
/// <summary>
/// Две вертикальные ленты вдоль левого ребра левого телика - ближнего и дальнего света
/// </summary>
public FrameSpanPair Left => Vertical;
/// <summary>
/// Ленты ближнего света левого телика
/// </summary>
public override LeftTVContureSpanCollection Back { get; } = new LeftTVContureSpanCollection(top.Back, bottom.Back, left.Back, topBevel.Back, bottomBevel.Back);
/// <summary>
/// Ленты дальнего света левого телика
/// </summary>
public override LeftTVContureSpanCollection Wide { get; } = new LeftTVContureSpanCollection(top.Wide, bottom.Wide, left.Wide, topBevel.Wide, bottomBevel.Wide);
}
public class RightTVSpanCollection(FrameSpanPair top, FrameSpanPair bottom, FrameSpanPair right, FrameSpanPair topBevel, FrameSpanPair bottomBevel) : SideTVSpanCollection<RightTVContureSpanCollection>(top, bottom, right, topBevel, bottomBevel)
{
/// <summary>
/// Две вертикальные ленты вдоль правого ребра правого телика - ближнего и дальнего света
/// </summary>
public FrameSpanPair Right => Vertical;
/// <summary>
/// Ленты ближнего света правого телика
/// </summary>
public override RightTVContureSpanCollection Back { get; } = new RightTVContureSpanCollection(top.Back, bottom.Back, right.Back, topBevel.Back, bottomBevel.Back);
/// <summary>
/// Ленты дальнего света левого телика
/// </summary>
public override RightTVContureSpanCollection Wide { get; } = new RightTVContureSpanCollection(top.Wide, bottom.Wide, right.Wide, topBevel.Wide, bottomBevel.Wide);
}
public class CenterTVSpanCollection(FrameSpanPair top, FrameSpanPair bottom) : TVSpanCollection(top, bottom)
{
/// <summary>
/// Ленты ближнего света центрального телика
/// </summary>
public CenterTVContureSpanCollection Back { get; } = new CenterTVContureSpanCollection(top.Back, bottom.Back);
/// <summary>
/// Ленты дальнего света центрального телика
/// </summary>
public CenterTVContureSpanCollection Wide { get; } = new CenterTVContureSpanCollection(top.Wide, bottom.Wide);
}
public class TVContureSpanCollection(FrameSpan top, FrameSpan bottom, params FrameSpan[] anotherSpans) : LEDSpanCollection([top, bottom, .. anotherSpans], false)
{
/// <summary>
/// Горизонтальная лента вдоль верхнего ребра телика
/// </summary>
public FrameSpan Top { get; } = top;
/// <summary>
/// Горизонтальная лента вдоль нижнего ребра телика
/// </summary>
public FrameSpan Bottom { get; } = bottom;
}
public class SideTVContureSpanCollection(FrameSpan top, FrameSpan bottom, FrameSpan vertical, FrameSpan topBevel, FrameSpan bottomBevel) : TVContureSpanCollection(top, bottom, vertical, topBevel, bottomBevel)
{
/// <summary>
/// Две ленты скоса углов бокового телика
/// </summary>
public LEDSpanCollection Bevels { get; } = new LEDSpanCollection([topBevel, bottomBevel], false);
/// <summary>
/// Лента верхнего скоса бокового телика
/// </summary>
public FrameSpan TopBevel { get; } = top;
/// <summary>
/// Лента нижнего скоса бокового телика
/// </summary>
public FrameSpan BottomBevel { get; } = bottom;
/// <summary>
/// Вертикальная лента вдоль края бокового телика
/// </summary>
public FrameSpan Vertical { get; } = vertical;
}
public class LeftTVContureSpanCollection(FrameSpan top, FrameSpan bottom, FrameSpan left, FrameSpan topBevel, FrameSpan bottomBevel) : SideTVContureSpanCollection(top, bottom, left, topBevel, bottomBevel)
{
/// <summary>
/// Вертикальная лента вдоль левого края левого телика
/// </summary>
public FrameSpan Left => Vertical;
}
public class RightTVContureSpanCollection(FrameSpan top, FrameSpan bottom, FrameSpan right, FrameSpan topBevel, FrameSpan bottomBevel) : SideTVContureSpanCollection(top, bottom, right, topBevel, bottomBevel)
{
/// <summary>
/// Вертикальная лента вдоль правого края правого телика
/// </summary>
public FrameSpan Right => Vertical;
}
public class CenterTVContureSpanCollection(FrameSpan top, FrameSpan bottom) : TVContureSpanCollection(top, bottom)
{
}
}
Логический кадр — одномерная картинка для логических лент. Он реализован на базе одномерного буфера Booffer1D<float3>. По факту все цвета лежат друг за другом в одном куске памяти, однако логический кадр знает, какой «пиксель» к какой логической ленте относится, и предоставляет удобные абстракции для работы с контурами вместе или раздельно, и обращаться к разным кускам, например:
//Оба контура зелёные
frame.Fill((0.1f, 0.8f, 0.2f));
//Залить левый верхний скос заднего света оранжевым
frame.LeftTop.Back.Fill(float3.Orange);
//Правые ленты (и задняя, и дальняя) заливаются градиентом
frame.Right.Gradient(float3.Red, float3.Blue);
//Начиная с 100 номера диоды верхних граней контуров ярче на 20%
frame.Top[100…].Mul(1.2f);
//Ручками рисуем волну на контуре заднего света
for (int i = 0; i < frame.Back.LEDCount; i++)
frame.Back[i] = float3.Gold * Pepe.Sin(i * 0.01f + phase) * 0.5f + 0.5f;
Например, мы можем завязать количество включенных светодиодов на степень открытия телевизора, поставив на него сенсор расстояния до стены:

Пишем вот так:
public void Process(Frame frame)
{
//Правый телик - верхний и нижний сегменты
foreach (var strip in frame.RightTV.Top)
processSpan(strip);
foreach (var strip in frame.RightTV.Bottom)
processSpan(strip);
//Левый телик - верхний и нижний сегменты
foreach (var strip in frame.LeftTV.Top)
processSpan(strip);
foreach (var strip in frame.LeftTV.Bottom)
processSpan(strip);
//Боковые вертикали чистим
frame.RightTV.Right.Clear();
frame.LeftTV.Left.Clear();
}
private void processSpan(FrameSpan strip)
{
//Степень открытия от 0 до 1
var openLevel = (double)(sensor.DistanceMeters - MinDistance) / (MaxDistance - MinDistance);
openLevel = Pepe.Clamp(openLevel, 0, 1); //Подрезаем выход за границы
openLevel = Pepe.Pow(openLevel, Unlineary); //Щепотка нелинейности
var index = (int)(openLevel * strip.LEDCount); //Считаем индекс диода на котором всё
strip[index..^1].Clear(); //С этого индекса и до конца тушим диоды
if (index > 0)
strip[index] *= KonchikLightness; //Свечение кончика усиливаем
}
Для каждого диода эта система абстракций хранит информацию о том, к какому БП он подсоединён, где он физически находится (буквально координаты диода в миллиметрах в реальности — начало координат в центре среднего телика), к какой физической ленте он относится и какой у него номер, к какой логической ленте и какой в ней у него номер, к какому контуру и тому подобное.
Теперь про конвертацию float3 в byte3 и HDR.
Экспериментальный миллиард цветов на 8-битных лентах
Цвет для виртуальных диодов хранится в виде векторов float3 — в большинстве случаев красный, зелёный и синий принимают значения от 0,0 до 1,0 включительно.
Однако, этим диапазоном всё не исчерпывается: значения могут превышать 1,0 в случае сверхъярких значений, и даже быть меньше 0,0 — то есть иметь отрицательную яркость. Но если чёрный свет — штука экзотическая, и, по‑моему, кроме меня с ним никто особо не играется, то значения ярче 1,0 вполне себе обыденность для HDR изображений.

У физических, настоящих диодов WS2812b, красный, зелёный и синий задаются ступенчатыми значениями от 0 до 255. Никаких чёрных ламп и HDRов. Для сверкающих бликов надо как‑то научиться прыгать выше 255, а чтобы не резать тени — уметь выводить промежуточные значения между 0 и 1.
Как же получить сверкающую плавность? А у нас много диодов. Пусть помогут друг другу.
Вот у нас исходный рисунок в формате float3 RGB от 0.0 до 1.0 и больше.
Во‑первых, пусть все пиксели ярче 1 скидывают свою избыточную энергию на соседей. Избыток делится пополам, и половинки раздаются соседям.

Сделаем так несколько раз — светлячки растекутся в пятна. Например, когда пиксель горит на 1000%, на деле в этом месте ленты зажигается пятно в 10 светодиодов. Диоды светят довольно большими, размытыми пятнами, и свечение 10 диодов вместо 1 визуально будет восприниматься как просто большая яркость. Разумеется, перегибать палку не стоит — 30 диодов уже воспринимаются как полоска. Поэтому всё, что не успело рассосаться за N итераций всё таки тупо обрезается. Но для большинства случаев этого достаточно.

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

Для этого я написал алгоритм «ветра», в котором слабое значение пикселя «сдувается» и переходит следующему пикселю, пока этот снежный ком не разрастётся до суммы, достаточной, чтобы пробить минимум и диод вспыхнул. Тогда сумма оседает на текущем пикселе, обнуляется и алгоритм продолжается. В отличие от классического дизеринга, эта штука работает только со значениями от 0.0 до 1/255, не затрагивая большие значения.

Всё это происходит независимо‑параллельно для каждого канала RGB. Диоды стоят рядом, свет их размытый и сливается — выглядит точно как яркость в 0.01%, и в 1000%.
После этого действа пеперубка умножает значения на 255 (диапазон ..0..1.. превращается в 0..255) и конвертирует в byte3 — можно отправлять на контроллер.
Теперь наши SDR ленты показывают HDR :3
Экспериментальные булочки и аморфный софт
Это не первый мой софт, где «пехотой функционала» являются эффекты и делают основную работу. По классике ООП обычно это делается через классы со всеми сопутствующими наследованиями и тем фактом, что непосредственно при использовании софта пользователь будет иметь дело с экземплярами этих классов.

В рамках экспериментов я реализовал эффекты не через ООП, а через парадигму, идея которой появилась у меня давно. Мне было неясно, насколько она удобоварима и применима на практике. Эффекты тут не классы, и никакие не экземпляры классов.
Эффекты — это булочки.
Булочка — это сущность, отождествляющая класс, его исходный код, экземпляр класса и его состояние. Всё это представляется единой сущностью.

Меняется код — меняется состояние, меняется состояние — меняется код. Химера из декларативного и императивного подхода, можно сказать.

Синтаксис C# я пока решил не трогать: булка объявляется как public class, но по факту это не класс, а булка. GUI регенерируется по мере редактирования кода. В текущей реализации всё, разумеется, не лишено шероховатостей, ибо жизнь C# к такому не готовила, но это закономерно — эксперимент же.

Текущие значения настроек в коде представляются как значения по умолчанию для public полей и свойств. Если поменять настройку в интерфейсе — она поменяется в коде, и наоборот. Пока поддерживаются все скалярные типы, 3D и 4D векторы (они считаются цветом), логический bool, текст и енумы (для них генерится выпадающий список).
Добавляем/удаляем публичное поле/свойство в коде — оно появляется/исчезает в интерфейсе. Для публичных методов без параметров генерируются кнопки, по нажатию которых они вызываются. Статические члены в булках смысла не имеют, даже если делать их публичными — булки по определению синглтоны и существуют в одиночестве.

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

Согласно концепции, код булочки содержит исчерпывающую информацию о ней и её состоянии. Все встроенные в софт стандартные эффекты, которые можно добавлять в каналы — просто текстовые файлы с кодом и расширением.cs, лежащие в папке Generics. Когда пользователь добавляет эффект, софт по сути просто достаёт код из файла, создаёт новую булку и пихает в неё этот код.
При закрытии ПО все каналы‑эффекты просто сохраняются как папки с файлами.cs, содержащими код булок. При запуске из файлов читается код и булки создаются сразу с сохранённым состоянием — не просто так настройки булки хранятся прямо в коде именно как значения по умолчанию для полей и свойств.
Один и тот же эффект можно добавить несколько раз — и это будет несколько независимых копий. У каждой копии имя, настройки, код и поведение можно сделать абсолютно разными, и вообще поменять всё. Это в корне отличается от классического подхода с ООП, где два таких эффекта будут двумя экземплярами одного и того же класса.

У каждой булочки есть своё уникальное имя, по которому на неё могут ссылаться сдобные коллеги прямо в коде — так обеспечиваются горизонтальные связи в системе. Среда выполнения булок следит за тем, чтобы имена были уникальными, и если возникает коллизия, то одна из булок автоматом переименовывается.
Если переименовать булку, на которую где‑то ссылаются другие булки, то в их коде это переименование тоже будет произведено — автоматический рефакторинг.

Наследования у булочек пока нет, но есть идеи о том, как его можно сделать.
Наследование булки от класса не отличается от наследования класса от класса
Наследование булки от булки будет похоже на References в 3Ds Max
В текущей первой экспериментальной реализации булочки сделаны на C# и Roslyn. Сочетание постоянной перекомпиляции с жирнющей библиотекой пепекторов, помноженное на нежелание Roslyn чистить свой кэш приводит к суровому расходу оперативной памяти. Мне лично это не мешает, но на обычных рядовых компах так делать точно не стоит. Решения я вижу два:
Перетащить булки на питоно‑джаваскритпы
Вытащить работу с Roslyn в отдельный процесс и после выполнения задачи просто его завершать
Пока размышляю, но склоняюсь к первому варианту. Эксперимент показал, что компилируемый язык для булок — не совсем оптимальный выбор. Хотя вынести в отдельный процесс гораздо проще…
Под капотом булочек
Система получилась довольно замороченная и нафаршированная всякими асинхронно‑фоновыми обработками, чтобы шаловливые манипуляции с булками не тормозили их работу и работу GUI.
Булочки существуют внутри Мира и имеют уникальные имена, по которым они могут напрямую обращаться друг к другу из кода. Это позволяет натягивать горизонтальные связи в системе на булочках.
Связь между булочками реализована через автогенерируемый статический класс — печку. В ней прописываются ссылки на все булки мира. Печка тоже живёт внутри мира и перекомпилируется только когда булки добавляют или переименовывают.

Ключевой обитатель мира держится в тени — асинхронный исполнитель задач. Он отвечает за частичную или полную перекомпиляцию булок и печки, обновление и загрузку сборок, а также за обновление настроек булок, если их поменяли в коде.
Булочка содержит внутри себя начинку и обвес, позволяющий с ней взаимодействовать снаружи — вызывать методы, обращаться к свойствам и полям, читать/менять её код. Всё сделано так, чтобы минимизировать частоту выделений памяти и боксингов.
Для каждой булочки генерируется и загружается отдельная независимая сборка (Assembly). Благодаря этому, при изменении единичной булки обычно производится только её — булки — перекомпиляция, остальные булки продолжают работать. Иногда перекомпиляция вообще не требуется, а бывает, что надо перекомпилировать вообще все булки мира.

Начинка булки хранит в себе текущий код, непосредственно загруженную C# сборку, класс и главное — сам объект. Это экземпляр того самого класса, который описывается в коде булки.
public class Градиент
{
public float3 WideBegin = float3.RoyalBlue;
public float3 WideEnd = new float3(0.95294f, 0.28627f, 0.07059f);
public float3 BackBegin = float3.OrangeRed;
public float3 BackEnd = float3.LimeGreen;
public void Process(Frame frame)
{
frame.Wide.Gradient(WideBegin, WideEnd);
frame.Back.Gradient(BackBegin, BackEnd);
}
}
Если в коде булки объявлено несколько классов, то ищется первый, у которого есть атрибут [MainObject], и создаётся его экземпляр. Если такого нет, берётся просто первый объявленный класс.
Чтение/запись полей и свойств булки, а также вызов методов реализованы не через Reflection, как можно было бы подумать, а через специально сделанный Bulkaflection.
Проблема тут простая: обычный Reflection, через который можно докапываться до свойств, полей и методов, боксит значения в object — а это аллокации в куче, которых мы старательно избегаем. Да, в новых .NET появились некоторые инструменты, чтобы частично это минимизировать, но они применимы только в узких случаях.

Цель Bulkaflection проста: по‑максимуму избегать аллокаций. Для этого оно компилирует отдельный Expression для доступа к каждому члену и вызывает его. Передаваемые и принимаемые этим Expressionам значения, по возможности, конвертируются в универсальный значимый pepector, а не боксятся в ссылочный object, как это делает Reflection. Напомню — в pepector можно засунуть любой скаляр или вектор. Именно этим достигается избегание аллокаций в куче.
Всё это дело кешируется через Funchacho. При первом обращении к тому или иному члену булки оно пытается сообразить, можно ли провернуть всё через Expression с pepectorом, если нет — тогда использует Reflection. По скорости он особо не отличается от рефлексии в.NET, однако отмечу, что когда я реализовывал подобный подход на.NET Framework для интерпретатора своего ЯП — там скорость отличалась больше, чем на порядок.
Помимо всего прочего, в начинке булки стоят модули, занимающиеся обслуживанием объекта и кода, а также взаимодействием с асинхронным исполнителем задач мира.
Эти модули отслеживают изменения свойств и полей объекта и оперативно обновляют код булки, прописывая в нём новые значения. Благодаря этому в коде булки отражаются реальные значения свойств и полей объекта в настоящий момент времени.
Редактирование кода булочки устроено сложнее.
Во‑первых, она отдельно засекает и обрабатывает случаи, когда в коде произведено только изменение значений свойств/полей, чтобы не перекомпилироваться лишний раз, а просто обновить значения у объекта.
Во‑вторых, всё происходит асинхронно‑параллельно, чтобы булки продолжали работать, пока происходит возня с кодом и пляски с компилятором.

Когда булочке задают новый код, она сначала сравнивает его со своим текущим кодом.
В случае, если изменения есть, начинка генерирует предписание — обновить поля/свойства, перекомпилировать булку, перекомпилировать всё. Предписание улетает булочковому генератору задач, который, в свою очередь, анализирует состояние булки и мира, генерирует на основе этого задачу и отправляет центральному асинхронному исполнителю задач мира.
Задачи в нём копятся в очередь. Как только они там появляются, исполнитель просыпается и начинает работать с новыми задачами до тех пор, пока они не кончатся, затем — снова засыпает.

Бывают задачи следующих типов:
Перекомпилировать вообще все булки и печку
Пересоздать и перекомпилировать печку
Перекомпилировать булку с новым кодом
Установить значения свойств/полей
Как только в очереди появляются задачи, первым делом ищется задача перекомпиляции вообще всего. Если таковой нет, запускается стандартный цикл исполнения. Если есть — цикл перекомпиляции всего.
Стандартный цикл исполнения
Это когда надо работать с отдельными булками и/или с печкой.
В нём сначала проверяется задача на генерацию печки. Если она есть — печка регенерируется и задача удаляется из очереди.
После всего этого остаются только задачи установки свойств/полей и перекомпиляций булок. Ищутся все булки, по которым есть задачи. Если какой‑то булке назначено несколько задач, актуальной считается последняя. После этого задачи начинают выполняться.
Каждая задача имеет три этапа (начало, тело, конец). Исполнитель вначале параллельно выполняет первый этап всех задач, дожидается окончания, потом второй этап, потом третий. То есть, этапы выполняются параллельно, но синхронно.
Задача перекомпиляции задействует все три этапа.
Сначала компилируется новая сборка из нового кода булки. Эта сборка загружается и создаётся объект
В булке старый объект очень быстро заменяется на новый
Старый объект и связанные с ним ресурсы выгружаются
Задача установки свойств/полей использует только средний этап — тупо проходит по списку изменений, который у неё есть, и устанавливает объекту в булке соответствующие значения. Обновление кода по этим новым значениям, если оно вообще нужно, сделает сама начинка булки.
Отмечу, что для вытаскивания значений из кода я написал отдельный парсер, обрабатывающий все подходящие случаи с пепекторами и скалярами, а также со строками. Причём парсер понимает как конструктор/кортеж, так и статическое значение, то есть, он, например, поймёт и new float3(1, 0, 0), и float3.Red
Генерация печки — это просто. Допустим, есть две булки:
class Булка1 { … }
class Булка2 { … }
Код печки генерируется примерно такой:
public static class __bakery
{
public static dynamic Булка1 { get; private set;}
public static dynamic Булка2 { get; private set;}
}
Опционально в печку могут быть напиханы дополнительные свойства, поля, методы и прочее. В моём случае это разные ссылки на микшер, светодиодную инсталляцию, количество светодоидов (LEDCount) и прочие штуки — всё для интеграции булок со средой выполнения.
Далее в код каждой булки перед компиляцией будет неявно вставлено:
using static __bakery;
После этого сборка печки загружается и каждому dynamic‑свойству через Reflection присваиваются экземпляры булочек.
Теперь к булкам можно обращаться по имени, например, в Булка1 можно написать:
Булка2.Свойство = значение;
Перекомпиляция вообще всего
Задача перекомпиляции мира появляется в очереди, если добавлена новая булка или переименована какая‑то из существующих. Отмечу, что при удалении булки эта задача не появляется.
Если в списке обнаруживается хотя бы одна такая глобальная задача, то всё обрабатывается хитрее. Во‑первых, из очереди удаляются все задачи перекомпиляции мира (одна или несколько, если есть). Остаются только задачи для конкретных булок.
Исполнение задачи перекомпиляции всего заключается тупо в создании вороха новых задач. То есть это задача, создающая гору задач и исчезающая после этого. Делает она это так.
Создаётся стейт машина, хранящая в себе таблицу желаемого состояния всех булок, в неё запечетлевается текущее состояние булок. Затем последовательно анализируется каждая задача в очереди и на её основе вносится корректировка в желаемое состояние соответствующей булки.
Если обнаруживается, что какая‑то булка переименована, то по коду всех остальных булок производится поиск ссылок на эту булку. Если ссылка обнаруживается, то в коде, где есть эта ссылка, производится переименование на новое имя, и новый код вносится в таблицу стейт‑машины как желаемый. Именно так работает авторефакторинг, когда переименование булки оставляет верными ссылки на неё в других булках.
После прохода по всем задачам мы получаем стейт‑машину, состояние которой отражает целевое состояние всех булок. Генерируется пачка единичных задач. Первая — задача по регенерации печки. И далее для каждой булки по одной задаче рекомпиляции, на основе того, что указано в стейт‑машине.
Далее из очереди удаляются обработанные задачи (не все, а именно обработанные стейт‑машиной — вдруг там уже новые появились), и добавляются те, что мы только что создали.
После этого исполнитель задач выходит на новую итерацию цикла обработки задач.
Булочный итог
Само собой, такое концептуальное упрощение — от классов и ООП к примитивной булочке — имеет и негативные стороны. Очень легко наворотить страшную сложноподдерживаемую паутину. А в текущей первой реализации вообще любой шаг в сторону чреват отстреливанием ноги. С другой стороны, булочки хорошо подходят для адаптации к быстроменяющимся требованиям и всяким экспериментам — они очень гибкие.
Я предполагаю, что оптимальнее всего сочетать булочки с обычными объектами как бы в два слоя: снизу слой обычных объектов с наследованием и стройной архитектурой — он медленно меняется и адаптируется, но строен и хорошо поддерживается, а сверху глазурь из булочек, которые постоянно меняются, переподключаются и управляют всеми этими объектами.

Есть мысль, что такая концепция — булочки + обычные объекты — хорошо подходит в качестве фундамента для аморфных приложений и операционных систем будущего, которые динамически, прямо в процессе эксплуатации, в реальном времени будут менять своё устройство и функционал, подстраиваясь под текущую задачу пользователя каждую секунду.
Рулить этим, разумеется, будет ИИ.
Упрощённо говоря, над булочками будет третий слой — сеть искусственных интеллектов разных специализаций, которые будут перестраивать код этих булочек, создавать новые и удалять ненужные.
И вот эта вся живая трансформирующаяся метасистема будет в непрерывном взаимодействии с пользователем решать задачи. Не будет квантования на версии ПО, не будет квантования на ОС и приложения. Всё будет единой вязкой паутинистой штукой. Как жидкий терминатор, который не состоит из деталей. Софт будет как будто бы читать мысли пользователя и превращаться в то, что нужно именно сейчас.

Взаимодействие будет не только через клавиатуры‑мышки‑экраны, но и через всякие мозгочипы, VR‑линзы, фемтосекундные энцифалографические томографы сверхвысокого разрешения и прочие ништяки будущего.
Пока ИИ натягивать на булки я не стал, чтобы сделать сроки разработки менее неадекватными :)
Да и первая экспериментальная реализация булок, конечно, не лишена недостатков. Но даже в текущем варианте булки уже вполне жизнеспособны и удобны — по крайней мере, в данном софте.
Экспериментальный анализ рабочего стола
Все эти штуки — гарнир к главному: расширению картинки рабочего стола, до которого мы, наконец, добрались.
Итак, надо анализировать картинку и считать цвета для лент.

Открыть скрин в натуральную величину
Делать это надо во время игры, то есть когда видеокарте рвёт дно от перегрузки 24-мегапиксельным 10-битным экраном. И тут врываемся мы, вываливаем своё сверкающее хозяйство на стол и требуем его посчитать.

Чтобы софт проанализировал картинку, он сначала должен её откуда-то получить.
HDR картинка рабочего стола в разрешении 12К весит около 200 Мб и обновляется 120 раз в секунду. Рисует её подсистема Windows DWM (в полноэкранных режимах есть нюансы, но не суть), и хранится она в памяти видеокарты. Чтобы проанализировать её каким‑нибудь обычным алгоритмом на C# или C++, придётся сначала перекачивать её в ОЗУ через кучу шин. Такое легко положит всю систему вместе с частотой кадров в игре.

Поэтому использовать тут классический подход через Windows GDI — а он как раз копирует в ОЗУ — решительно не стоит. Более того, он будет ещё и предварительно конвертировать всю простыню из HDR в SDR, потому что GDI — это 8-битная старая штука, и про HDR она не знает.
Нам поможет особая технология: DirectX OutputDuplication. Эта штука даёт прямой доступ к свеженарисованной картинке рабочего стола прямо в памяти видеокарты без копирования в ОЗУ. Мы можем сказать видеокарте, что эта картинка рабочего стола — текстура, после чего работать с ней как с текстурой не выходя из видеокарты. То, что надо.
Пример кода работы с OutputDuplication
//Обработку ошибок убрал, чтобы не хламить код
//ИРЛ её всегда надо делать - DirectX очень капризная пакость
ComPtr<ID3D11Device> d3dDevice;
ComPtr<ID3D11DeviceContext> d3dContext;
ComPtr<IDXGIDevice> dxgiDevice;
ComPtr<IDXGIAdapter> dxgiAdapter;
ComPtr<IDXGIOutput> dxgiOutput;
ComPtr<IDXGIOutput1> dxgiOutput1;
ComPtr<IDXGIOutputDuplication> deskDupl;
D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_0 };
//Создаём устройство
D3D11CreateDevice(nullptr,D3D_DRIVER_TYPE_HARDWARE,nullptr,0,featureLevels, _countof(featureLevels),D3D11_SDK_VERSION,&d3dDevice,nullptr,&d3dContext);
d3dDevice.As(&dxgiDevice);
dxgiDevice->GetAdapter(&dxgiAdapter);
//Получаем первый Output (монитор)
dxgiAdapter->EnumOutputs(0, &dxgiOutput);
dxgiOutput.As(&dxgiOutput1);
//Докапываемся то рабочего стола
dxgiOutput1->DuplicateOutput(d3dDevice.Get(), &deskDupl);
for (int frame = 0; frame < 100; frame++)
{
DXGI_OUTDUPL_FRAME_INFO frameInfo;
ComPtr<IDXGIResource> desktopResource;
ComPtr<ID3D11Texture2D> desktopImage;
auto hr = deskDupl->AcquireNextFrame(1000, &frameInfo, &desktopResource);
if (hr == DXGI_ERROR_WAIT_TIMEOUT)
continue; //Пробуем снова
if (FAILED(hr))
{
std::cerr << "Оно опять не работает, го кофе\n";
break;
}
desktopResource.As(&desktopImage)
//Теперь у нас есть desktopImage
//Это обычная (почти) видеокартовая текстура
//В которой лежит наша картинка рабочего стола
//Можно её обработать видеокартой
//А можно скопировать в ОЗУ
}
Но есть проблема. Обычный OutputDuplication, как и GDI, не умеет в HDR. При захвате рабочего стола с включённым HDR он тоже будет каждый раз конвертировать картинку из HDR в SDR. И хотя это будет происходить внутри видеокарты, и перекачивания мы избежим, конвертация всё равно будет весьма ресурсоёмкой и помешает играм.
Поэтому нам понадобится продвинутый OutputDuplication версии 6 из недавних обновлений Windows. Он может работать с оригинальной HDR‑картинкой рабочего стола без конвертации, захватывая её в оригинальном 16-битном формате.
Да‑да — на телике она 10-битная, но в мозгах ОС хранится как 16-битная (каждый канал RGB — вещественное число половинной точности).

В процессе разработки я обнаружил у OD6 крутую фичу: он не работает
При попытке получить доступ к рабочему столу оно будет постоянно выкидывать ошибку. Почему? Потому что страдай.
Я очень долго воевал с этой проблемой. Решение оказалось простым и очевидным, даже не знаю, как оно не пришло мне в голову сразу. Теперь в софте, в самом‑самом начале, в статическом конструкторе App у меня вызывается C++ное
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
Что это? А оно говорит Windows, что моя программа умеет работать одновременно с мониторами, имеющими разную плотность пикселей.
Зачем это мне с одним монитором (напоминаю — с точки зрения ОС эти три одинаковых телика — это один целый длинный монитор) с дефолтным DPI, и какое вообще это имеет отношение к способности моего софта захватывать рабочий стол в HDR, я не понимаю. Зачем? Почему? Для чего?
Каким‑то странным образом, если вызывать эту штуку при загрузке софта, то OutputDuplication6 начинает работать. Видимо, вызывая вот это вот я говорю винде «Я не тупой» — и она такая «Ок, ладно, блокировка снята» — «Какая блокировка?» — «Больше никакой.». Наверное. Ну ок, в любом случае, оно работает.
Теперь у нас анализируется оригинальный рабочий стол без конвертации из HDR в SDR — разгружаем видеокарту + снижаем лаг. Вдобавок, мы получаем доступ оригинальным HDR‑пикселями, которые светятся ярче 100%. Мы можем получить их цвета, обработать эффектами и скормить нашим «HDR» лентам.
Таким образом, мы собрали полный цикл обработки: захват данных, обработка и вывод на ленты — всё работает в HDR.
С DirectX гораздо проще работать на C++, а не на C# — тут не требуется никаких бесячих обёрток, которые вечно что‑то не умеют, устаревают и перестают поддерживаться. Поэтому, в отличие от всего софта, захват и анализ картинки я сделал на C++ и HLSL.
Шейдер анализа картинки я писал сразу максимально оптимизированным, чтобы лишний раз не грузить видеокарту, несмотря на то, что подобные вычисления для неё и так почти ничего не стоят. Из видеокарты в ОЗУ прилетают сразу готовые HDR цвета для диодов — искорка в 37 килобайт (массив из 2315 значений float4[]).

Теперь про сам анализ. Для него я написал вычислительный шейдер, которому на вход подается картинка рабочего стола как текстура. Из всех 24 млн пикселей этот шейдер анализирует всего несколько сотен тысяч.
Код шейдера анализа картинки рабочего стола
#include "../AmbiknightScreenScanner/UltrasharedTypes.cs" //Общие енумы для C#, HLSL и C++
StructuredBuffer<int2> coords : register(t0); // Координаты пикселей
Texture2D<float4> frameTexture : register(t1); // Текстура с оригинальным кадром
RWStructuredBuffer<float4> resultColors : register(u0); // Результат для светодиодов
cbuffer Constants : register(b0)
{
float REPROC_NUM_PIXELS; // 1 / NUM_PIXELS
int NUM_PIXELS;
int TOTAL_LEDS;
int2 ImageSize;
float power;
float rep_power; //может быть равен 1/power, а может и нет
float padding;
};
[numthreads(SHADER_SETTINGS_THREADS_PER_GROUP, 1, 1)]
void main(uint3 DTid : SV_DispatchThreadID, uint3 GTid : SV_GroupThreadID)
{
//Узнаём кто мы где мы
//за какой диод мы отвечаем
int ledIndex = DTid.x;
if (ledIndex >= TOTAL_LEDS) //если такого нет - выходим
return;
//будем считать среднее арифметическое цветов
float4 sum = 0.0f;
int startIndex = ledIndex * NUM_PIXELS;
int endIndex = startIndex + NUM_PIXELS;
//если гамма-коррекции не требуется
if (power == 1.0f)
{
//Тупо считаем среднее арифметическое
for (int i = startIndex; i < endIndex; i++)
{
//Берем координаты из буфера
int2 pixelCords = coords[i];
//Дёргаем по ним цвет пикселя
float4 pixelColor = frameTexture.Load(int3(pixelCords, 0));
//Добавляем в накопитель
sum += pixelColor;
}
}
else
{
//Считаем среднее арифметическое
//но не очень тупо
//каждый считанный цвет сначала возводим в степень
//тем самым мы применяем гамма-коррекцию
float4 p = float4(power, power, power, power);
for (int i = startIndex; i < endIndex; i++)
{
int2 pixelCords = coords[i];
float4 pixelColor = frameTexture.Load(int3(pixelCords, 0));
pixelColor = pow(pixelColor, p);
sum += pixelColor;
}
}
//Делим на число пикселей, если надо - снова гамму
if (rep_power == 1.0f)
resultColors[ledIndex] = sum * REPROC_NUM_PIXELS;
else
resultColors[ledIndex] = pow(sum * REPROC_NUM_PIXELS, rep_power);
}
Координаты этих пикселей расчитываются 1 раз при запуске программы, 1 раз закачиваются в видеопамять и используются повторно с каждым кадром. Считаются они на основе информации о местоположении всех лент (изначально они в миллиметрах, в мировых координатах, поэтому конвертируются в экранные). На каждый диод приходится примерно 500 пикселей.
Считаются координаты так.
Изначально у нас есть инфа о светодиодной установке, где известны координаты начала и конца каждого куска физических лент (в моем случае их 36), а также число диодов в каждом куске. В моем случае координаты двумерные и заданы в миллиметрах, начало в центре (я натыкал их мышкой по CAD модели всей установки, после чего прописал в коде).

Сначала эти миллиметровые мировые координаты превращаются в относительные (от 0 до 1). Затем относительные координаты натягиваются на разрешение рабочего стола — в моём случае, это 11'520×2160. Теперь каждому светодиоду установки сопоставлена точка, заданная парой координат (x, y) на экране.
Через каждую такую точку проводится воображаемая линия (подлиннее для дальнего света, покороче для ближнего). Если линия выходит за границы экрана — её затаскивает внутрь. Затем считаются координаты около 500 пикселей на этой линии и чуть распыляются в произвольном направлении.

Распыление нужно на случай, если на экране возникнет что‑то периодическое — какая‑нибудь плитка или кирпичная стена, и чуть смещённые линии с этой картинкой захотят образовать мерцающий муар. Распыление эту проблему снимает.
Все эти координаты запоминаются в один буфер координат в памяти видеокарты. И далее на каждом новом кадре мы запускаем шейдер анализа картинки и скармливаем ему адрес буфера координат и адрес текстуры рабочего стола.
В итоге шейдер параллельно для каждого диода дёргает координаты из готового списка, тупо считывает по ним пиксели, усредняет (захват и расчёты в fp16) и сохраняет результат под номером соответствующего светодиода. Всё. Минимальная нагрузка.
Чтобы подцепить это С++ поделие к основной C#овой тушке я долго и хмуро смотрел на C++ CLI, но передумал, ибо опасался повышенных накладных расходов и каких‑нибудь заморочек из‑за непопулярности технологии. Поэтому классика: pinvoke + класс‑обёртка. Разумеется, базированный на DisposableExtended. Для надёжности.
Булка сканирования рабочего стола использует этот класс‑обёртку, передавая ей нужные настройки.
Как C++ экспериментально компилирует C# код, написанный на HLSL
И вот в наш софт на C# встраиваются куски на C++ и HLSL. Для того, чтобы эти трое могли договориться, им надо оперировать какими-то общими типами, структурами и константами.
Классический подход - нудно прописывать одни и те же штуки отдельно в HLSL, отдельно в C++ и отдельно в C#. И потом огребать от того, что где-то потерялся кусочек одинаковости. Можно, конечно, прицепить какую-нибудь автоматизирующую штуку, но у меня возникла идея поинтереснее.
Я прописал типы и константы в одном файле так, чтобы этот файл компилировался и в HLSL, и в C#, и в C++, и включил этот файл одновременно в HLSL, C# и C++. Код в этом файле является корректным для всех трёх языков программирования одновременно.
#if CSHARP
using Ambiknight.Pepectors;
namespace Ambiknight
#else
#define public
#if CSHARP || C_PLUS_PLUS
namespace UltrasharedTypes
#endif
#pragma once
#endif
#if CSHARP || C_PLUS_PLUS
{
#endif
//Структуры (соблюдаем выравнивание по 4 байта, а то нога больно будет)
public struct LedVisualizerConstants
{
public int2 CanvasSize;
public int2 SpriteSize;
public int2 SpriteSizeDiv2;
public uint LedCount;
public float GlarePower;
public float LerpPower;
public int3 Padding;
public void Correct()
{
//Пепематику не юзаем ибо лень было пхать её в плюсы/HLSL
SpriteSizeDiv2.x = SpriteSize.x >> 1;
SpriteSizeDiv2.y = SpriteSize.y >> 1;
}
};
public enum UltrasharedConstants
{
LED_BUFFER_ALIGNMENT = 64,
SHADER_SETTINGS_THREADS_PER_GROUP = 32,
LEDVISUALIZER_THREADS_X = 32,
LEDVISUALIZER_THREADS_Y = 24,
LEDVISUALIZER_THREADS_Z = 1,
LEDVISUALIZER_THREAD_BLOCKS_X = 48,
LEDVISUALIZER_THREAD_BLOCKS_Y = 16,
LEDVISUALIZER_THREAD_BLOCKS_Z = 1,
LEDVISUALIZER_THREAD_X_MULTIPLER = 2,
LEDVISUALIZER_THREAD_Y_MULTIPLER = 2,
LEDVISUALIZER_CANVAS_MAX_WIDTH = (LEDVISUALIZER_THREADS_X * LEDVISUALIZER_THREAD_BLOCKS_X * LEDVISUALIZER_THREAD_X_MULTIPLER),
LEDVISUALIZER_CANVAS_MAX_HEIGHT = (LEDVISUALIZER_THREADS_Y * LEDVISUALIZER_THREAD_BLOCKS_Y * LEDVISUALIZER_THREAD_Y_MULTIPLER),
TEXTURE_PADDING = 8,
};
public enum ScreenScanResult
{
Unknown = 0,
Success = 1,
DiscardByNoChangesOnScreen = 1,
DiscardByTimeout = 2,
DiscardByProtectedContent = 3,
DiscardByAccessLost = 4,
DiscardByScannerRecreating = 5,
DiscardByScannerDisposed = 6,
FailedByError = 500
};
#if CSHARP || C_PLUS_PLUS
}
#endif
#if CSHARP
#else
#undef int
#undef public
#endif
Структура чувствует себя прекрасно, енумы тоже. Глобальные константы прописываем не как константы, а как один большой енум UltrasharedConstants, который кушают сразу три языка. Вкусно но грустно: такая штука прокатывает, только если HLSL будет версии 2017 — именно там появились enumы. Иначе оно работает только в двух языках: C# и C++, без HLSL.
HLSL 2017 требует для работы ShaderModel 6.0, которая, в свою очередь, требует DirectX12, который требует вообще другого подхода для работы с графикой — то есть всё надо переписывать, и код будет сложнее, так как DX12 ближе к железу.
Переписывать всё на DX12 мне было лень, поэтому я попробовал добиться одновременной компиляции C++, C# и HLSL другим вариантом:
Другой вариант, основанный на магии с internal
#if CSHARP
using Ambiknight.Pepectors;
namespace Ambiknight
#else
#define public
#if CSHARP || C_PLUS_PLUS
namespace UltrasharedTypes
#endif
#pragma once
#endif
#if CSHARP || C_PLUS_PLUS
{
#endif
//Структуры (соблюдаем выравнивание по 4 байта, а то нога больно будет)
public struct LedVisualizerConstants
{
public int2 CanvasSize;
public int2 SpriteSize;
public int2 SpriteSizeDiv2;
public uint LedCount;
public float GlarePower;
public float LerpPower;
public int3 Padding;
public void Correct()
{
//Пепематику не юзаем ибо лень было пхать её в плюсы
SpriteSizeDiv2.x = SpriteSize.x >> 1;
SpriteSizeDiv2.y = SpriteSize.y >> 1;
}
};
#if CSHARP
public class UltrasharedConstants
{
#else
#define internal static
#endif
//Константы для C#, C++ и HLSL
//internal в C++ и HLSL заменяется на static
internal const int LED_BUFFER_ALIGNMENT = 64;
internal const int SHADER_SETTINGS_THREADS_PER_GROUP = 32;
internal const int LEDVISUALIZER_THREADS_X = 32;
internal const int LEDVISUALIZER_THREADS_Y = 24;
internal const int LEDVISUALIZER_THREADS_Z = 1;
internal const int LEDVISUALIZER_THREAD_BLOCKS_X = 48;
internal const int LEDVISUALIZER_THREAD_BLOCKS_Y = 16;
internal const int LEDVISUALIZER_THREAD_BLOCKS_Z = 1;
internal const int LEDVISUALIZER_THREAD_X_MULTIPLER = 2;
internal const int LEDVISUALIZER_THREAD_Y_MULTIPLER = 2;
internal const int LEDVISUALIZER_CANVAS_MAX_WIDTH = (LEDVISUALIZER_THREADS_X * LEDVISUALIZER_THREAD_BLOCKS_X * LEDVISUALIZER_THREAD_X_MULTIPLER);
internal const int LEDVISUALIZER_CANVAS_MAX_HEIGHT = (LEDVISUALIZER_THREADS_Y * LEDVISUALIZER_THREAD_BLOCKS_Y * LEDVISUALIZER_THREAD_Y_MULTIPLER);
internal const int TEXTURE_PADDING = 8;
#if CSHARP
}
#else
#undef internal
#endif
#if C_PLUS_PLUS || CSHARP
//Енумы для C# и C++, но НЕ для HLSL
public enum ScreenScanResult
{
Unknown = 0,
Success = 1,
DiscardByNoChangesOnScreen = 1,
DiscardByTimeout = 2,
DiscardByProtectedContent = 3,
DiscardByAccessLost = 4,
DiscardByScannerRecreating = 5,
DiscardByScannerDisposed = 6,
FailedByError = 500
};
#endif
#if CSHARP || C_PLUS_PLUS
}
#endif
#if CSHARP
#else
#undef int
#undef public
#endif
Задумка такая: слово internal языки C++ и HLSL не знают (в отличие от public). Поэтому его спокойно можно заменять макросами. А в C# пусть оно остаётся internal. Однако этот способ не работает: компилятор C# реагирует на блоки #define даже внутри исключённых из комплияции блоков #if. А в C#, в отличие от HLSL и C++, #define имеет только 1 аргумент, а не 2. В итоге компилятор C# видит #define internal static, несмотря на то, что это исключено из компиляции, и ругается.
Поэтому я применил менее изящный, но работающий вариант:
Менее изящный, но работающий вариант
#if CSHARP
using Ambiknight.Pepectors;
namespace Ambiknight
#else
#define public
#if CSHARP || C_PLUS_PLUS
namespace UltrasharedTypes
#endif
#pragma once
#endif
#if CSHARP || C_PLUS_PLUS
{
#endif
//Структуры (соблюдаем выравнивание по 4 байта, а то нога больно будет)
public struct LedVisualizerConstants
{
public int2 CanvasSize;
public int2 SpriteSize;
public int2 SpriteSizeDiv2;
public uint LedCount;
public float GlarePower;
public float LerpPower;
public int3 Padding;
public void Correct()
{
//Пепематику не юзаем ибо лень было пхать её в плюсы
SpriteSizeDiv2.x = SpriteSize.x >> 1;
SpriteSizeDiv2.y = SpriteSize.y >> 1;
}
};
#if CSHARP
public class UltrasharedConstants
{
#else
#define internal
#endif
//Константы для C#, C++ и HLSL
internal
#if !CSHARP
static
#endif
const int LED_BUFFER_ALIGNMENT = 64;
internal
#if !CSHARP
static
#endif
const int SHADER_SETTINGS_THREADS_PER_GROUP = 32;
internal
#if !CSHARP
static
#endif
const int LEDVISUALIZER_THREADS_X = 32;
internal
#if !CSHARP
static
#endif
const int LEDVISUALIZER_THREADS_Y = 24;
internal
#if !CSHARP
static
#endif
const int LEDVISUALIZER_THREADS_Z = 1;
internal
#if !CSHARP
static
#endif
const int LEDVISUALIZER_THREAD_BLOCKS_X = 48;
internal
#if !CSHARP
static
#endif
const int LEDVISUALIZER_THREAD_BLOCKS_Y = 16;
internal
#if !CSHARP
static
#endif
const int LEDVISUALIZER_THREAD_BLOCKS_Z = 1;
internal
#if !CSHARP
static
#endif
const int LEDVISUALIZER_THREAD_X_MULTIPLER = 2;
internal
#if !CSHARP
static
#endif
const int LEDVISUALIZER_THREAD_Y_MULTIPLER = 2;
internal
#if !CSHARP
static
#endif
const int LEDVISUALIZER_CANVAS_MAX_WIDTH = (LEDVISUALIZER_THREADS_X * LEDVISUALIZER_THREAD_BLOCKS_X * LEDVISUALIZER_THREAD_X_MULTIPLER);
internal
#if !CSHARP
static
#endif
const int LEDVISUALIZER_CANVAS_MAX_HEIGHT = (LEDVISUALIZER_THREADS_Y * LEDVISUALIZER_THREAD_BLOCKS_Y * LEDVISUALIZER_THREAD_Y_MULTIPLER);
internal
#if !CSHARP
static
#endif
const int TEXTURE_PADDING = 8;
#if CSHARP
}
#else
#undef internal
#endif
#if C_PLUS_PLUS || CSHARP
//Енумы для C# и C++, но НЕ для HLSL
public enum ScreenScanResult
{
Unknown = 0,
Success = 1,
DiscardByNoChangesOnScreen = 1,
DiscardByTimeout = 2,
DiscardByProtectedContent = 3,
DiscardByAccessLost = 4,
DiscardByScannerRecreating = 5,
DiscardByScannerDisposed = 6,
FailedByError = 500
};
#endif
#if CSHARP || C_PLUS_PLUS
}
#endif
#if CSHARP
#else
#undef int
#undef public
#endif
Теперь константы и типы в библиотеке, её обёртках и шейдерах всегда будут одинаковыми, просто потому что объявлены в одном месте. Разумеется, здесь надо одновременно держать в голове восприятие этого кода сразу тремя языками, и далеко не всё можно себе позволить, а что‑то можно и отстрелить, но, имхо, штука удобная. Если доберусь до enums в SM6, будет вообще хорошо.
Экспериментальное ядро
Собираем всё в кучу. Ядро системы объединяет в себе:
Инфу о светодиодной установке
Булки и их мир
Микшер
Таймеры
Таймеры завязаны на низкоуровневые мультимедийные прерывания ОС, и с точными интервалами пинают микшер рисовать очередной кадр. Главный таймер — это тот самый высокоточный BeautifulTimer, завязанный на низкоуровневые мультимедийные прерывания ОС.
В микшере сидят входные каналы‑конвейеры, один выходной конвейер и булка перехода (отвечает за анимацию переключения каналов).

Обычно микшер берет из пула кадр, очищает, прогоняет через выбранный входной канал, потом через канал вывода, потом возвращает кадр в пул.
Во время анимации переключения всё чуть сложнее. Микшер берет из пула два кадра. Они параллельно прогоняются через входные каналы, между которыми идёт переключение. Затем из пула берётся ещё один третий кадр‑холст. Два свеженарисованных кадра, время анимации и третий кадр‑холст пихаются в булку перехода — она рисует переход. Первые два возвращаются в пул, а третий — полученная смесь — прогоняется через выходной канал и тоже возвращается в пул.

В булке перехода можно запрограммиовать любой алгоритм плавного перехода. По умолчанию используется переход через прозрачность.
Общение ядра с булками построено просто: есть особые имена методов, которые ядро вызывает у эффекта в определенных случаях (если эти методы у него имеются).
void Process(Frame frame) рисует красивости — основной метод эффекта
void OnPropertyChange(string name) — на каждое изменение каждого свойства
void OnPropertiesChange(Vist16<string> names) — на изменение свойств
void Background() вызывается в отдельном потоке с частотой кадров
void FastBackground() аналогичен предыдущему, но выполняется на максимальной скорости
void Shutdown() вызывается перед отключением ПО
void Startup() вызывается только 1 раз при запуске ПО
void BackgroundStart() вызывается, когда на канал булочки переключаются
void BackgroundStop() вызывается, когда переключаются на другой канал
Аналогично с FastBackgroundStart() и FastBackgroundStop()
Background и FastBackground делают полезности в отдельных фоновых потоках, чтобы не приходилось вручную в булке рожать, синхронизировать, удалять и ждать потоки. Анализ экрана и вывод на контроллер работают как раз через это — обработка кадров, работа с контроллером и с экраном распараллеливаются.

В целях оптимизации отдельные потоки‑таймеры для фоновой работы отдельных эффектов и прочая инфраструктурная требуха создаются только при необходимости, причём не просто создаются — а берутся из пула. В булку добавили метод Background? Берём поток‑таймер из пула, назначем булке и включаем. Удалили метод Background? Выключаем поток‑таймер и возвращаем в пул.
Само собой,это именно отдельные потоки, а не GUI‑таймеры. Потоки, вызывающие метод Background, синхронизируется с потоком рендеринга кадров, но выполняется параллельно с ним. Поток, вызывающий метод FastBackground, ни с чем не синхронизируется — здесь уже сам булкограммист должен описать синхронизацию.
Например, именно в методе FastBackground происходит сканирование и анализ экрана, чтобы не тормозить основной поток рендеринга. FastBackground сканирует экран и сохраняет результат, а метод Process просто его подхватывает и рисует на одномерном кадре.
А вот эти штуки всегда вызываются в главном потоке, чтобы булочка могла общаться с интерфейсом:
void GuiInitialize(Grid parent) — вызывается 1 раз после создания внутреннего объекта булки
void GuiTimer(Grid parent) — вызывается ~60 раз в секунду если софт не свёрнут и не запущена игра
void GuiDispose(Grid parent) — вызывается 1 раз в при выгрузке булки
Через это, например, работает визуализация лент в GUI. Если свернуть софт или запустить полноэкранное что‑нибудь, GuiTimer перестаёт вызываться — оптимизация.
При изменении полей свойств у булочки будет вызываться метод OnPropertyChange или OnPropertiesChange. Разумеется, только при наличии оных. Если у методов есть параметры типа string или MemberInfo — передаст, что изменилось, если нет — так вызовет. Важно отметить, что для этого не надо прописывать никаких NotifyPropertyChange() — само работает.
Во всех булках через печку доступны глобальные переменные:
bool GUI //Видит ли пользователь интерфейс (стоит ли его обновлять?)
bool Game //Запущена ли сейчас игра или полноэкранное приложение
Installation //Инфа о светодиодной установке
LogWindow //Доступ к окну логов
Mixer //Микшер — через это можно переключать каналы из булки
Core //Ядро
LEDCatalog //Мини‑БД всех диодов со всей инфой по лентам, БП и т. д.
Ключевая пара булок — «Скан экрана» и «Вывод». Скан экрана просто работает с тем самым C++/DirectX классом, а вот «Вывод» — булка довольно массивная:
Превращает стадо float3 в съедобный для контроллера byte3 со всеми этими SDR → HDR, перестановкой цветов и выворачиванием бит
Сверкает предпросмотром в интерфейсе той самой C++ной шейдернёй
Считает статистику по току, энергии и прочему для установки

Предпросмотр и стату нужно делать на основе непосредственно того, что идёт на контроллер, а не абстрактных float3. Именно поэтому на предпросмотре видно и растекание ярких >100% диодов, и дизеринг слабых, а подсчёт тока более‑менее адекватен. Разумеется, предпросмотр рендерится в отдельном фоновом потоке — не в потоке обработки кадров и не в GUI, чтобы ничего не тормозить.
Экспериментальный интерфейс
Интерфейс я сделал на WPF (MVVM вероломно проигнорирован, от WPF взята только его векторность и макетность, и Blend конечно же).

Анимации и обновления окна отключаются, когда софт свёрнут или запущена игра, чтобы лишний раз не грузить комп и не мешать игре. Пройдёмся по особенностям интерфейса сверху вниз.

Заголовок — растровый PNG, свежерендерённый в 3Ds Maxе. Отрендерил я его в перспективной проекции (не ортогональной), причём, камеру сместил вниз, чтобы текст оказался в верхней части изображения и ракурс примерно совпадал с реальным положением заголовка в интерфейсе. Проще говоря, я рендерил не так

а вот так

С нормальным углом обзора эта мелкая деталь почти незаметна, но с ней получается чуть лучше и органичнее. Поэтому почему бы её не сделать.

Мягкая рейтрейсинговая тень падает на прозрачную плоскость под объёмными буквами и будет корректно выглядеть на любом фоне. Разрешение подобрано так, чтобы заголовок оставался чётким при масштабе интерфейса до 200% включительно.

Чтобы он сиял, уже в WPF поверх нанесён векторный белый контур (Path), точно повторяющий растровый текст — он получен из того же контура, из которого выдавлены 3D буквы. Цепочка конвертации получилась длинноватой: 3Ds Max → Adobe Illustrator → SVG → XAML.

Чтобы сиять, векторный контур имеет маску прозрачности: градиент, который в цикле пробегает слева направо. Контур тонкий специально — его плавное сияние заметно только если приглядеться, а так оно не отвлекает.
Ниже идёт область предпросмотра. По факту это пустой контейнер для контролов. В нём ничего нет. Его наполнение полностью формируют булочки теми самыми методами GuiInitialize, GuiTimer и GuiDispose. Среда булочек вызывает эти методы в главном потоке и берет на себя вопросы синхронизации.

В актуальной конфигурации это делает только одна булочка — «Вывод». Она не только выводит сигналы на контроллер, но и пихает в интерфейс сверкающее безобразие и троицу блоков питания, и она же отвечает за обновление их графиков.
Сверкаем диодами
Теперь про сверкание. Нарисовать быстро 2315 бликов — задача непростая.

Сначала я родил визуализацию под работу на процессоре. Как это сделать по‑простому? Ну, давайте рисовать диоды простыми точками. Надо взять координаты каждого диода и нарисовать в этих координатах пиксель соответствующего цвета. И это будет тормозить.
Что тут можно оптимизировать? Расчёт координат. Каждый раз, когда мы условно пишем
SetPixel(x,y, color)
комп будет рассчитывать адрес в памяти этого пикселя по формуле:
адрес первого пикселя холста + ширина холста * y + x
Два сложения и умножение. Не говоря уже о вызове функции. Что делаем? Диоды у нас стоят на месте и никуда не перемещаются. Холст лежит в неуправляемой памяти — его адрес тоже не перемещается. Значит, можно один раз при запуске ПО расчитать адреса пикселей, соответствующих диодам, сохранить, и потом повторно использовать. Так я и сделал.
Пишутся адреса пикселей в Booffer<IntPtr>. Прям буфер, в котором вместо пикселей хранятся адреса памяти. И потом на каждом кадре я такой просто
пока не перебрали все диоды
{
считать следующий цвет диода;
считать следующий адрес пикселя;
записать цвет в адрес;
перейти к следующему диоду;
перейти к следующему адресу;
}
Причём каждому диоду я сопоставляю не один, а сразу несколько пикселей, чтобы сделать точки пожирнее.
Вживую код выглядит так
public bool Render(IBooffer1D<RenderLedValueType> values)
{
if (E.IsInvalid(values) || E.IsInvalid(this))
return false;
using var sc = DisposeProtectedScope;
if (sc.IsFailed())
return false;
using var sc1 = values.DisposeProtectedScope;
if (sc1.IsFailed())
return false;
DisposeProtectedScopeToken preprocessedValuesDisposeToken = default;
if (Preprocess(values, out var preprocessedValues))
{
if (preprocessedValues != values)
if (!E.IsInvalid(preprocessedValues))
{
preprocessedValuesDisposeToken = preprocessedValues.DisposeProtectedScope;
values = preprocessedValues;
}
}
using (preprocessedValuesDisposeToken)
unsafe
{
if (values.TryGetPointers(out var source_begin, out var source_end))
{
var mapPack = addressMap.Value;
var location = (double2)pepector.Create(Installation.Bounds).xy;
var size = (double2)pepector.Create(Installation.Bounds).zw - location;
canvasLocker.EnterWriteLock();
try
{
for (int mapIndex = 0; mapIndex < mapPack.MapCount; mapIndex++)
{
if (mapPack.TryGetMapPointers(mapIndex, out var begin, out var count, out var end, out var end4))
{
var end8 = begin + count / 8 * 8;
var ptr = begin;
var source_ptr = source_begin;
while (ptr < end8)
{
**ptr++ = *source_ptr++;
**ptr++ = *source_ptr++;
**ptr++ = *source_ptr++;
**ptr++ = *source_ptr++;
**ptr++ = *source_ptr++;
**ptr++ = *source_ptr++;
**ptr++ = *source_ptr++;
**ptr++ = *source_ptr++;
}
while (ptr < end)
**ptr++ = *source_ptr++;
}
}
}
finally
{
canvasLocker?.ExitWriteLock();
}
}
{
if (addressMap.Value.TryGetAllMapPointers(out var allBegin, out var count, out var end, out var end4))
{
AfterRender(_canvas.Value, allBegin, count, end, end4);
}
}
}
return true;
}
Работает это всё дело не просто быстро, а очень быстро. Однако, выглядит не то чтобы круто:


Амы тут собрались, чтобы качественно посверкать — так давайте заменим эти точки на блики с лучиками. Для этого нам понадобится картинка блика. Для каждого из светодиодов её нужно будет перекрасить в соответствующий цвет и нарисовать в нужном месте. То есть на каждом кадре нам надо нарисовать 2315 картинок, перекрашивая каждую из них. Это уже вам не точечки рисовать — дело пахнет видеокартой. Расскажу по порядку.

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

Во‑вторых, мы тут всё в fp32 обрабатываем, но диоды на лентах 8-битные, и у них есть минимальный порог свечения. И нам надо сымитировать именно их железячные особенности.
Диод либо не светит, либо светит хоть чуть‑чуть, и далее меняет яркость дискретно. Для реалистичности будем, помимо картинки блика, использовать картинку светодиода без бликов. И для каждого диода мы будем рисовать не картинку‑блик, а смесь картинки‑точки и картинки‑блика. Чем ярче диод, тем больше это блик и меньше точка, и наоборот. Так тусклые диоды будут точками, а яркие будут иметь лучики.

Не забываем, что всё вышеперечисленное параллельно‑независимо считается для красного, зелёного и синего каналов. Мы тут цветные, а не чёрно‑белые.
Теперь перейдём к реализации. Она должна быть быстрой. Я не буду сейчас сильно погружаться в чан с видеокартовыми особенностями, постараюсь попроще.
Сначала мы выделяем в памяти видеокарты некий холст‑картинку‑текстуру размером M x N пикселей (примерно 1920×360). На нём будем рисовать 2315 квадратных разноцветных картинок‑бликов методом аддитивного наложения.

Очищаем холст (заливаем прозрачным). Затем запускаем шейдер — это такой алгоритм, который параллельно выполняетсядля каждого пикселя холста. Например, у нас 2 млн пикселей и 20'000 ядер в видеокарте. Вот каждое ядро вызовет наш алгоритм 100 раз для 100 каких‑то пикселей на этом холсте. Каких именно и как — не важно. Нам в алгоритм прилетает X и Y пикселя, мы работаем с этими координатами и выдаём цвет. Всё.
В алгоритме пишем цикл. Он проходит по всем 2315 диодам, и на основе координат и цвета каждого диодаопределяет, виден ли блик этого диода в данном пикселе холста. Для этого он сравнивает координаты данного пикселя холста, координаты диода и размеры картинки с бликом. Если попали в границы и диод включен — значит блик может быть виден.

А раз виден — вычисляем, какой именно пиксель блика приходится на данный пиксель холста. Берём этот пиксель блика. Поскольку пиксель картинки с бликом чёрно‑белый, а у нас тут цветной светодиод, нам надо этот пиксель перекрасить в цвет светодиода.
Для этого просто умножаем его на цвет текущего светодиода, и добавляем к текущему пикселю холста. Таким образом, у нас в каждом пикселе холста накапливаются цвета бликов от разных диодов.

Звучит норм, но цикл на 2315 диодов в каждом пикселе — это некислая такая нагрузка для любой видеокарты. Он выполняется в каждом из сотен тысяч пикселей. Вообще не круто, давайте оптимизировать. По‑хорошему тут надо воротить конвейер отрисовки из разных шейдеров и другие штуки, но мне было лень — я выкрутился на вычислительном шейдере.
Первое — все координаты диодов на холсте считаются один раз при запуске программы. Процессором. После этого координаты сохраняются в специальный 1D‑буфер. Шейдер не считает каждый раз где какой диод — он тупо берёт готовые координаты из этого буфера.
Второе — каждый пиксель холста при рисовании задевают блики от максимум пары десятков диодов. То есть, до каждого пикселя холста дотягиваются своими лучиками далеко не все диоды. Зачем нам перебирать все 2315?
Поэтому, опять же, при запуске софта, один раз процессором анализируем каждый пиксель холста и смотрим, какие диоды теоретически могут как‑то повлиять своим бликом на цвет этого пикселя. Причем смотрим не только на координаты, но и на интенсивность блика в данной точке — если координаты подходят, но блик в этом месте чёрный (например, это его краешек или угол где всё тусклое), то диод пропускаем.

Как всё это передать теперь шейдеру? Самый простой способ — трёхмерная текстура в видеокарте. Каждому пикселю отрисовываемого 2D холста сопоставляется столбец вокселей на этой текстуре, в каждом вокселе храним номер диода и координаты блика. Вроде логично, только вот, так или иначе, расход видеопамяти получался довольно неприятным.
Поэтому я применил хитрость. Ведь диоды, влияющие на каждый пиксель — их номера чаще всего почти подряд идут, и стоят рядом. Типа «с 167 по 189» и всё в таком духе. Давайте передавать шейдеру не набор номеров для каждого пикселя, а всего лишь диапазон — с такого‑то по такой‑то. Да, в этих диапазонах номеров встречаются пропуски, но это не беда — ну проверим мы несколько лишних диодов, подумаешь. Главное мы сократим число проверок раз в сто.

Сначала возникла идея просто брать и считать минимальный и максимальный номер диода в каждом мини‑наборе.
Один раз при запуске ПО создаём 2D текстуру индексов размером с холст, и в каждый пиксель пишем цвет, у которого красный равен минимальному номеру, а зелёный — максимальному номеру.
То есть текстура индексов и холст имеют одинаковые размеры, и в каждом пикселе текстуры индексов хранится диапазон номеров диодов, который надо проверить для расчета цвета соответствующего пикселя холста.
Отныне шейдер не перебирает все 2315 диодов, он берет сначала цвет из текстуры индексов, интерпретирует R этого цвета как минимальный индекс, а G — как максимальный, и обрабатывает диоды только с R по G, а не все.
Но я быстро понял, что у меня остались ещё компоненты B и A, ведь текстурка‑то RGBA. Давайте хранить не один диапазон в RG, а два: R..G и B..A

Я стал искать не просто минимальный и максимальный индекс, а разбил найденные диоды в мини‑наборе на два диапазона так, чтобы суммарная длина этих диапазонов была минимальной. И сохранял не один, а два диапазона, выкидывая самый большой пропуск.
Так в моей текстуре индексов в каждом пикселе стали хранится не просто «сканируй диоды с R по G» а «сканируй диоды с R по G и с B по A».
Итоговый код шейдера визуализации бликов
#include "../AmbiknightScreenScanner/UltrasharedTypes.cs" //Общие енумы для C#, HLSL и C++
cbuffer Constants : register(b0)
{
LedVisualizerConstants settings;
};
StructuredBuffer<int2> LedPositions : register(t0); // Позиции светодиодов
StructuredBuffer<uint4> LedColors : register(t1); // Цвета светодиодов
Texture2D<float> GlareTexture : register(t2); // Текстура блика
Texture2D<float4> LEDTexture : register(t3); // Текстура блика
Texture2D<uint4> IndexesTexture : register(t4); // Текстура индексов (x:from1, y:to1, z:from2, w:to2)
RWTexture2D<float4> OutputCanvas : register(u0); // Холст для вывода
float4 readSprite(int2 cords, uint3 ledColor)
{
int3 cords3 = int3(cords, 0);
float3 glareValue = pow(GlareTexture.Load(cords3), settings.GlarePower);
float3 ledValue = LEDTexture.Load(cords3);
float3 lerpLevel = ledColor * (1.0f / 255.0f);
lerpLevel = pow(lerpLevel, settings.LerpPower);
float3 color = lerp(ledValue, glareValue, lerpLevel);
float4 result;
result.rgb = color * (float3) ledColor;
result.a = max(result.r, max(result.g, result.b));
return result;
}
float4 calcSum(int2 pixelCords, uint fromIndexIncl, uint toIndexExcl, int2 minCords, int2 maxCords)
{
float4 sumColor = 0;
fromIndexIncl = clamp(fromIndexIncl, 0, settings.LedCount);
toIndexExcl = clamp(toIndexExcl, 0, settings.LedCount);
for (uint ledIndex = fromIndexIncl; ledIndex < toIndexExcl; ledIndex++)
{
uint3 ledColor = LedColors[ledIndex].rgb; //Берём цвет диода
if (!any(ledColor)) //Если он не горит - идём дальше
continue;
int2 ledCords = LedPositions[ledIndex]; //Берём координаты диода
if (ledCords.x < minCords.x || ledCords.y < minCords.y || ledCords.x > maxCords.x || ledCords.y > maxCords.y)
continue; //Если он слишком далеко - идём дальше
//Координаты пикселя на текстуре блика
int2 glareCords = pixelCords + settings.SpriteSizeDiv2 - ledCords;
//Получаем цвет, который надо добавить на пиксель текстуры
float4 glareColor = readSprite(glareCords, ledColor);
//Суммируем цвет
sumColor += glareColor;
}
return sumColor;
}
void draw(int2 pixelCords)
{
//За пределами холста не работаем
if (pixelCords.x >= settings.CanvasSize.x || pixelCords.y >= settings.CanvasSize.y)
return;
//Координаты области, в пределах которой нас интересуют диоды
int2 minCords = pixelCords - settings.SpriteSizeDiv2;
int2 maxCords = pixelCords + settings.SpriteSizeDiv2;
uint4 indexes = IndexesTexture.Load(int3(pixelCords, 0));
float4 sumColor = 0;
if (indexes.y > indexes.x)
sumColor += calcSum(pixelCords, indexes.x, indexes.y, minCords, maxCords);
if (indexes.w > indexes.z)
sumColor += calcSum(pixelCords, indexes.z, indexes.w, minCords, maxCords);
if (sumColor.a <= 0.0f)
OutputCanvas[pixelCords] = 0.0f;
else
{
sumColor *= (1.0f / 255.0f); //Вгоняем в диапазон от 0 до 1
sumColor.a = clamp(sumColor.a, 0, 1);
sumColor.rgb /= sumColor.a;
OutputCanvas[pixelCords] = clamp(sumColor.bgra, 0, 1);
}
}
[numthreads(LEDVISUALIZER_THREADS_X, LEDVISUALIZER_THREADS_Y, LEDVISUALIZER_THREADS_Z)]
void main(uint3 DTid : SV_DispatchThreadID)
{
int2 pixelCords = DTid.xy * int2(LEDVISUALIZER_THREAD_X_MULTIPLER, LEDVISUALIZER_THREAD_Y_MULTIPLER);
[unroll(LEDVISUALIZER_THREAD_X_MULTIPLER)]
for (int dx = 0; dx < LEDVISUALIZER_THREAD_X_MULTIPLER; dx++)
{
[unroll(LEDVISUALIZER_THREAD_Y_MULTIPLER)]
for (int dy = 0; dy < LEDVISUALIZER_THREAD_Y_MULTIPLER; dy++)
{
draw(pixelCords + int2(dx, dy));
}
}
}
Это отлично сочетается с двухконтурностью моей подсветки — у меня примерно так и получается. Но в целом алгоритм будет работать с подсветкой любой формы, он универсальный, просто в разных условиях ускорение будет разным. В моём случае — оно огромное.
Экспериментальный график тока
График тока, внезапно, рисуется процессором. Это сделано, чтобы затестить класс ClassicBitmap32 — наследник двумерного Booffer2D<byte4>, которого я снабдил плюшками для рисования через System.Drawing. То есть он как‑бы буфер неуправляемой памяти и всё такое, но с ним ещё можно работать как с обычным Bitmap/Graphics.

Несмотря на плавность, по факту график обновляется редко — где‑то раз в секунду. О — оптимизация. Всю эту секунду WPF плавно смещает контейнер с картинкой графика вправо, сама картинка вообще не меняется. За эту же секунду подсчитывается средний ток — накапливаются поступающие данные.
И вот по истечении секунды происходит обновление.

Сначала весь график на буфере резко смещается вправо вот этой штуковиной:
Booffer2D.ShiftX(int delta)
public void ShiftX(long delta, T? sourceFillColor = null)
{
if (E.IsInvalid(this) || delta == 0)
return;
if (Math.Abs(delta) >= Width)
{
if (sourceFillColor.HasValue)
Fill(sourceFillColor.Value);
return;
}
using var ds = DisposeProtectedScope;
if (ds.IsFailed())
return;
unsafe
{
if (!TryGetPointers(out var begin, out var end))
return;
var end4 = end - Width * 4;
if (delta > 0)
{
var from = begin;
var to = begin + delta;
var byteCount = (Width - delta) * sizeof(T);
var step = Width;
while (to < end4)
{
Buffer.MemoryCopy(from, to, byteCount, byteCount); from += step; to += step;
Buffer.MemoryCopy(from, to, byteCount, byteCount); from += step; to += step;
Buffer.MemoryCopy(from, to, byteCount, byteCount); from += step; to += step;
Buffer.MemoryCopy(from, to, byteCount, byteCount); from += step; to += step;
}
while (to < end)
{
Buffer.MemoryCopy(from, to, byteCount, byteCount);
from += step;
to += step;
}
}
else if (delta < 0)
{
var to = begin;
var from = begin - delta;
var byteCount = (Width + delta) * sizeof(T);
var step = Width;
while (from < end4)
{
Buffer.MemoryCopy(from, to, byteCount, byteCount); from += step; to += step;
Buffer.MemoryCopy(from, to, byteCount, byteCount); from += step; to += step;
Buffer.MemoryCopy(from, to, byteCount, byteCount); from += step; to += step;
Buffer.MemoryCopy(from, to, byteCount, byteCount); from += step; to += step;
}
while (from < end)
{
Buffer.MemoryCopy(from, to, byteCount, byteCount);
from += step;
to += step;
}
}
if (sourceFillColor.HasValue)
{
var color = sourceFillColor.Value;
if (delta > 0)
{
var from = begin;
var step = Width - delta;
var count = delta;
while (from < end4)
{
{ var e = from + count; while (from < e) *from++ = color; from += step; }
{ var e = from + count; while (from < e) *from++ = color; from += step; }
{ var e = from + count; while (from < e) *from++ = color; from += step; }
{ var e = from + count; while (from < e) *from++ = color; from += step; }
}
while (from < end)
{
{ var e = from + count; while (from < e) *from++ = color; from += step; }
}
}
else if (delta < 0)
{
var from = begin + Width + delta;
var step = Width + delta;
var count = -delta;
while (from < end4)
{
{ var e = from + count; while (from < e) *from++ = color; from += step; }
{ var e = from + count; while (from < e) *from++ = color; from += step; }
{ var e = from + count; while (from < e) *from++ = color; from += step; }
{ var e = from + count; while (from < e) *from++ = color; from += step; }
}
while (from < end)
{
{ var e = from + count; while (from < e) *from++ = color; from += step; }
}
}
}
}
}
Правый край картинки стирается — он выходит за её пределы.
Затем слева на буфере дорисовывается небольшой новый сегмент графика. Эти два действия — сдвиг и дорисовка — делаются в отдельном потоке, чтобы вообще исключить тормоза GUI. После этого, в главном потоке изображение графика заливается в WPF через WritableBitmap.
В этот же момент я резко сдвигаю WPFный контейнер с картинкой влево на ровно такое же число пикселей. Два резких противоположных движения — картинки буфера и самого холста — компенсируют друг друга, поэтому визуально сдвига не происходит. А плавность продолжается.
WPF капризничал и не хотел одномоментно обновлять изображение и смещение его контейнера, делая это в разных кадрах — и картинка ёрзала. За это я натянул ему тройную буферизацию — заработало.

Каждый из трёх БП отрендерён с двух ракурсов, чтобы зырить по сторонам. Модельки те же, что в иллюстрациях — сделаны с нуля (сначала Inventor, потом 3Ds Max), как и почти всё остальное, включая телики, рамы, контроллеры, провода и прочее. Ракурса именно два — имхо, плавная анимация поворота бы стилистически выбивалась из образа. Очень важно блоки питания не перепутать, поэтому каждому из них была сделана индивидуальная причёска.

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

Орут они просто: есть анимация, где ор постепенно нарастает. По факту это куча PNG картинок в папке. Да, можно сделать один цельный спрайт, можно APNG, можно WEBP или ещё что‑то, но мне и так норм.

Щи аккуратно вырезаны из сами‑знаете‑какого мультика в AfterEffects, отретушированы (ибо гора/облачко заслоняли) и хорошенько отмаскированы. Всего фрагментов с ором в мультике было два, и переход между ними пришлось чуть санимировать, создав плавное перемещение и поворот рта. Получилась целая и непрерывная анимация.
Каждому диапазону токовой нагрузки сопоставляется свой диапазон кадров анимации:
static Range AAAAA2frameRange(AAAAA aaaa) => aaaa switch
{
AAAAA.aa => 0..24,
AAAAA.AAaa => 49..71,
AAAAA.AaAaAaAaAaAaA => 76..139,
AAAAA.AAAAAAAAAAAAAAAAAAAAAAA => 143..^1,
_ => 0..^1
};

У визуализатора БП есть счётчик — номер текущего кадра и таймер. Таймер 30 раз в секунду смотрит на текущий ток и выясняет, какой актуален диапазон кадров. У этого диапазона кадров два номера — начальный и конечный. Далее всё просто:
Если текущий номер кадра больше конечного, то уменьшаем его на 1
Если текущий номер кадра равен конечному, то приравниваем его к начальному
В любой другой ситуации увеличиваем текущий номер на 1
private int incFrameIndex()
{
var targetRange = AAAAA2frameRange(AAA);
var startIndex = targetRange.Start.GetOffset(frames.Count);
var endIndex = targetRange.End.GetOffset(frames.Count);
if (currentFrameIndex > endIndex)
currentFrameIndex--;
else if (currentFrameIndex == endIndex)
currentFrameIndex = startIndex;
else
currentFrameIndex++;
return currentFrameIndex;
}
В результате проигрывается нужный диапазон кадров анимации, а если он сменился, то анимация плавно переходит к этому новому диапазону, проматывая все переходные стадии.
Каналы, эффекты, код и логи
Ниже идёт основная тушка интерфейса в четыре колонки:
Список каналов
Список булочек выбранного канала
Код выбранной булочки
Тугая стена логов

Всё это я уже описал в начале, поэтому отмечу только пару моментов.
Интерфейс настройки булочек генерируется для всех публичных полей и свойств поддерживаемых типов. Можно уточнить особенности интерфейса — имя настройки, диапазон значений, формат отображения и прочее. Для этого я написал небольшой набор атрибутов.

Для числовых значений атрибуты дают пару интересных плюшек. Первая: формат вывода задаётся текстом вида «Длина #,## метров» — можно сразу наглядно указать и округление, и разделитель дробной и целой части, и то, что будет написано до и после значения.

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

Процесс генерации интерфейса управления эффектом‑булкой организован просто: если булка перекомпилировалась, об этом узнаёт таймер в GUI‑потоке, изучает её члены и генерирует элементы управления.
Прямая связь от GUI к полям и свойствам булки тривиальна, а вот обратный механизм — то есть, обновление GUI при изменении значений в булке — сложный.
Булка обнаруживает изменения свойств и полей, сравнивая их значения через короткие интервалы времени в отдельном потоке через Bulkaflection. Если свойство изменилось, ставится флаг. Флаг подхватывается таймером в главном потоке, и он уже обновляет контрол. И никаких Dispatcher и NotifyUpdate — так я избавляюсь от вороха проблем с многопоточностью, STA/MTA, дедлоками и прочими прелестями.
И да: очень многие куски интерфейса ещё предстоит допиливать, чтобы снизить вырвиглазность и повысить удобство.

Например, элемент управления выбора цвета — местная фешн‑катастрофа, пора ему уже натянуть нормальный стиль, чтобы он не фонил своей квадратностью и гармонировал со всем остальным. И я уже не говорю о шрифтах, отступах и прочих штуках.
Справа в окне находится область логирования. Логи генерируются очень подробно. Когда какая‑нибудь булка перекомпилируется, в этом окне поднимается бюрократический кипиш на ~200 строк. При загрузке софта, когда все булки компилируются одновременно в разных потоках, тут творится полный ураган.


Зато всё понятно и легко отлаживать баги и дедлоки. Сюда, в том числе, прилетают сообщения компиляции. Предупреждения выделяются жёлтым, ошибки — красным, а сообщение об удачной компиляции — зелёным.
Пример лога рекомпиляции булочки
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Получен запрос на изменение кода на public class Булочка...
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Запрашиваем у начинки новое предписание для кода public class Булочка...
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Запрос на генерацию предписания для обновления кода до public class Булочка...
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Предварительно спрошу у мира, хочет ли он вообще полной перекомпиляции всего
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Нет, мир не хочет рекомпилироваться, разбираемся дальше
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Парсинг синтаксического дерева нового кода...
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Парсинг синтаксического дерева нового кода завершён.
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Парсинг синтаксического дерева текущего кода...
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Парсинг синтаксического дерева текущего кода завершён.
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Приступаем к парсингу дерева.
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Сравнение деревьев public class Булочка...(Булочка) и public class Булочка...
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Вытаскиваем все типы и делегаты из первого дерева...
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Обнаружено 1 типов и делегатов
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Вытаскиваем все типы и делегаты из второго дерева...
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Обнаружено 1 типов и делегатов
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Поиск главного типа среди типов первого дерева...
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Главный тип первого дерева найден. Это Булочка
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Поиск главного типа среди типов второго дерева...
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Главный тип второго дерева найден. Это Булочка
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ деклараций:
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Типов нет
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Методов нет
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Событий нет
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Делегатов нет
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Индексаторов нет
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Полей нет
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Свойств нет
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ деклараций завершён
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: В ходе анализа изменений не выявлено
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Начало сравнения главных типов...
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Сравнение типов Булочка и Булочка
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Типы Булочка и Булочка - это классы
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: У Булочка и Булочка по 4 членов
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Число членов равно, приступаем к сравнению значений
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ деклараций:
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Типов нет
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ 1 объявлений методов
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Тело метода изменилось
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Для метода Process не найдена пара в новом варианте кода, следовательно, изменения тотальны
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ 1 методов показал тотальные изменения
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ деклараций завершён
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Сравнение значений показывает, что требуется тотальная рекомпиляция
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Сравнение главных типов завершено. Результат: NeedRecompile
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Парсинг дерева завершён. Результат: NeedRecompile
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Сгенерировано предписание перекомпилировать единичную булочку, флаг необходимости обновления кода по значениям будет сброшен
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Предписание #2015[ReplaceObjectPrescription] получено
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Просим менеджера назначить работу по исполнению предписания #2015
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Инициатор работ: От нас требуют сформировать работу в соответствии с предписанием ReplaceObjectPrescription для булки #21[Булочка]
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по рекомпиляции булочки: Я буду работать над булочкой #21[Булочка], настраиваюсь.
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по рекомпиляции булочки: Настройка завершена
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Для исполнения предписания #2015 менеджер создал работу #19920974
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Выполняем запрос у мира на регистрацию новой работы #19920974
19:06:32 15.06.2024 Мир1: Получен запрос на регистрацию новой работы #19920974
19:06:32 15.06.2024 Мир1: Перенаправляю работу #19920974 менеджеру...
19:06:32 15.06.2024 Мир1: Менеджер работ: Поступила новая работа #19920974 типа ReplaceObjJob от булки #21 Булочка
19:06:32 15.06.2024 Мир1: Менеджер работ: Работа зарегистрирована
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Регистрация новой работы #19920974 произведена успешно
19:06:32 15.06.2024 Мир1: Менеджер работ: В очереди работ новенькие!
19:06:32 15.06.2024 Мир1: Менеджер работ: Вход в критическую секцию...
19:06:32 15.06.2024 Мир1: Менеджер работ: Вход в критическую секцию успешно произведён.
19:06:32 15.06.2024 Мир1: Менеджер работ: Извлечение всех скопишвихся в очереди работ...
19:06:32 15.06.2024 Мир1: Менеджер работ: Извлечено работ: 1
19:06:32 15.06.2024 Мир1: Менеджер работ: Работы передаются исполнителю...
19:06:32 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: -------------------Выполнение новых работ. Всего работ в списке: 1----------------------------
19:06:32 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Проверка работ на поддерживаемость и корректность....
19:06:32 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Вход в критическую секцию...
19:06:32 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Вход в критическую секцию успешно выполнен. Выполнение работ...
19:06:32 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Назначаем работы по булкам
19:06:32 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Булке #21 Булочка назначена работа ReplaceObjJob
19:06:32 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Параллельно выполняем первый этап всех работ
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по рекомпиляции булочки: Первый этап: для булки #21[Булочка] мне надо предварительно скомпилировать новую сборку с новым классом и создать объект
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по рекомпиляции булочки: Запрашиваю компилятор у мира
19:06:32 15.06.2024 Мир1: Запрос на получение компилятора из пула на какую-то печку __the_bakery_3
19:06:32 15.06.2024 Мир1: Настройка компилятора
19:06:32 15.06.2024 Мир1: Добавляем 56 ссылок
19:06:32 15.06.2024 Мир1: Добавляем ссылку на печку
19:06:32 15.06.2024 Мир1: Добавляем 40 юзингов
19:06:32 15.06.2024 Мир1: Добавляем using static для печки
19:06:32 15.06.2024 Мир1: Компилятор настроен, отдаём его
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по рекомпиляции булочки: Компилятор получен
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по рекомпиляции булочки: Командую компилятору скомпилировать код: public class Булочка...
19:06:32 15.06.2024 Мир1: Компилятор5: Компиляция кода public class Булочка...
19:06:32 15.06.2024 Мир1: Компилятор5: Парсим синтаксическое дерево...
19:06:32 15.06.2024 Мир1: Компилятор5: Получили дерево, теперь компилируем...
19:06:32 15.06.2024 Мир1: Компилятор5: Создаётся модуль компиляции...
19:06:32 15.06.2024 Мир1: Компилятор5: Начало генерации компиляции...
19:06:32 15.06.2024 Мир1: Компилятор5: Генерируем юзинги...
19:06:32 15.06.2024 Мир1: Компилятор5: Сгенерировано 41 юзингов
19:06:32 15.06.2024 Мир1: Компилятор5: Получаем корневой узел синтаксического дерева
19:06:32 15.06.2024 Мир1: Компилятор5: Форматирую синтаксическое дерево...
19:06:32 15.06.2024 Мир1: Компилятор5: Создаем новый корневой узел синтаксического дерева с директивами using и существующими узлами
19:06:32 15.06.2024 Мир1: Компилятор5: Создаем новое синтаксическое дерево с обновленным корневым узлом
19:06:32 15.06.2024 Мир1: Компилятор5: Создаем компиляцию с опцией OutputKind для создания исполняемой сборки
19:06:32 15.06.2024 Мир1: Компилятор5: Создаём опции компиляции
19:06:32 15.06.2024 Мир1: Компилятор5: Пихаем опции компиляции в компиляцию...
19:06:32 15.06.2024 Мир1: Компилятор5: Формируем список референсов компиляции...
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0\mscorlib.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0\System.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0\System.Core.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0\System.ObjectModel.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0\System.ComponentModel.Primitives.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0\System.Numerics.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0\System.Numerics.Vectors.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0\System.Runtime.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0\System.Private.CoreLib.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0\System.Reflection.TypeExtensions.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0\System.Reflection.Metadata.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0\System.Reflection.Emit.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0\System.Reflection.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0\System.Reflection.Extensions.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Users\user\Documents\Programs\Ambiknight2\Ambiknight2\bin\Debug\net9.0-windows7.0\AmbiknightCompiler.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0\System.Linq.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Users\user\Documents\Programs\Ambiknight2\Ambiknight2\bin\Debug\net9.0-windows7.0\AmbiknightPepectors.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Users\user\Documents\Programs\Ambiknight2\Ambiknight2\bin\Debug\net9.0-windows7.0\AmbiknightBasics.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Users\user\Documents\Programs\Ambiknight2\Ambiknight2\bin\Debug\net9.0-windows7.0\AmbiknightPepectedBooffers.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Users\user\Documents\Programs\Ambiknight2\Ambiknight2\bin\Debug\net9.0-windows7.0\AmbiknightBooffers.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Users\user\Documents\Programs\Ambiknight2\Ambiknight2\bin\Debug\net9.0-windows7.0\AmbiknightData.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0\System.Linq.Expressions.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0\Microsoft.CSharp.dll;
19:06:32 15.06.2024 Мир1: Компилятор5: Список основных референсов сформирован
19:06:32 15.06.2024 Мир1: Компилятор5: Все референсы запихнуты.
19:06:32 15.06.2024 Мир1: Компилятор5: Пихаем дополнительные референсы...
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.Runtime.dll
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.Collections.dll
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.Linq.dll
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightPepectors, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightBulki, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightPepectedBooffers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightSTM32, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightShuffler, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightLEDFrames, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightPipelines, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightPipelines, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightPhysics, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightKernel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.Private.CoreLib, Version=9.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.Private.CoreLib, Version=9.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.IO.Ports, Version=9.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на PresentationFramework, Version=9.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.Private.CoreLib, Version=9.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightKernel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightKernel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightNative, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.Private.CoreLib, Version=9.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightUI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.Private.CoreLib, Version=9.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightPeperubka, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightProcessUtils, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.Private.CoreLib, Version=9.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.Private.CoreLib, Version=9.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на WindowsBase, Version=9.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightKernel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.Windows.Forms, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightBasics, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightPepectors, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightBooffers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightPepectedBooffers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.Private.CoreLib, Version=9.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.Private.CoreLib, Version=9.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightUI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightCPUImaging, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.Linq, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.Console, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.Private.CoreLib, Version=9.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightKernel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightKernel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.Xaml, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.ComponentModel, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightLEDVisualizers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на PresentationCore, Version=9.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на PresentationCore, Version=9.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на PresentationCore, Version=9.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на System.Drawing.Primitives, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightKernel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightKernel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightKernel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightLEDCore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на AmbiknightLEDFrames, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Референс на __runtimeAssembly25, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
19:06:32 15.06.2024 Мир1: Компилятор5: Дополнительные референсы запихнуты.
19:06:32 15.06.2024 Мир1: Компилятор5: Добавление синтаксического дерева в компиляцию...
19:06:32 15.06.2024 Мир1: Компилятор5: Синтаксическое дерево добавлено...
19:06:32 15.06.2024 Мир1: Компилятор5: Модуль компиляции создан
19:06:32 15.06.2024 Мир1: Компилятор5: Извлекаем семантическую модель из дерева...
19:06:32 15.06.2024 Мир1: Компилятор5: Семантическая модель извлечена
19:06:32 15.06.2024 Мир1: Компилятор5: Приступаем к компиляции...
19:06:32 15.06.2024 Мир1: Компилятор5: Результат компиляции: True
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №1: (1,259): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №2: (1,714): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №3: (1,939): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №4: (1,234): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №5: (1,45): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №6: (1,878): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №7: (1,740): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №8: (1,311): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №9: (1,613): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №10: (1,103): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №11: (1,584): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №12: (1,1054): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №13: (1,1090): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №14: (1,850): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №15: (1,406): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №16: (1,13): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №17: (1,345): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №18: (1,519): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №19: (1,192): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №20: (1,177): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №21: (1,806): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №22: (1,428): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №23: (1,913): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №24: (1,86): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №25: (1,382): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №26: (1,280): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №27: (1,643): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №28: (1,454): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №29: (1,127): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №30: (1,771): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №31: (1,149): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №32: (1,993): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №33: (1,69): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №34: (1,539): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №35: (1,958): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: Сообщение компиляции №36: (1,565): hidden CS8019: Ненужная директива using.
19:06:32 15.06.2024 Мир1: Компилятор5: ? Ура! Скомпилировалось!
19:06:32 15.06.2024 Мир1: Компилятор5: Удаляем ссылки из компиляции...
19:06:32 15.06.2024 Мир1: Компилятор5: Удаляем синтаксические деревья из компиляции...
19:06:32 15.06.2024 Мир1: Компилятор5: Извлекаем байты скомпилированной сборки...
19:06:32 15.06.2024 Мир1: Компилятор5: Извлечено 3072 байт
19:06:32 15.06.2024 Мир1: Компилятор5: Сборка сохранена в файл Asm44.dll
19:06:32 15.06.2024 Мир1: Компилятор5: Загрузка сборки в память...
19:06:32 15.06.2024 Мир1: Компилятор5: Загрузка сборки __runtimeAssembly47, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null в память завершена.
19:06:32 15.06.2024 Мир1: Компилятор5: Поиск главного типа в сборке...
19:06:32 15.06.2024 Мир1: Компилятор5: Главный тип найден! Это же Булочка!
19:06:32 15.06.2024 Мир1: Компилятор5: На этой позитивной ноте завершаем процедуру компиляции
19:06:32 15.06.2024 Мир1: Компилятор5: Результат компиляции: SuccessCompileResult
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по рекомпиляции булочки: Сохраняю в булке результат последней комплияции
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по рекомпиляции булочки: Компилятор завершил работу и вернул мне результат: SuccessCompileResult
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по рекомпиляции булочки: Компиляция удалась. Беру полученную сборку __runtimeAssembly47, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null и пытаюсь создать объект...
19:06:32 15.06.2024 Мир1: Компилятор5: Попытка создания главного объекта из результата компиляции...
19:06:32 15.06.2024 Мир1: Компилятор5: Попытка создания главного объекта из сборки внутри результата компиляции...
19:06:32 15.06.2024 Мир1: Компилятор5: Поиск главного типа в сборке...
19:06:32 15.06.2024 Мир1: Компилятор5: Главный тип найден. Это Булочка
19:06:32 15.06.2024 Мир1: Компилятор5: Попытка создать объект типа Булочка с помощью конструктора без параметров...
19:06:32 15.06.2024 Мир1: Компилятор5: Успешно создан объект типа Булочка
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по рекомпиляции булочки: Объект Булочка успешно создан
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по рекомпиляции булочки: Отчёт об успешном результате помечен индексом #2115
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по рекомпиляции булочки: Возвращаю компилятор в миру
19:06:32 15.06.2024 Мир1: В пул возвращают компилятор
19:06:32 15.06.2024 Мир1: Стираем юзиниги компилятора
19:06:32 15.06.2024 Мир1: Стираем ссылки компилятора
19:06:32 15.06.2024 Мир1: Возвращаем компилятор в пул
19:06:32 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Обновляем печку, если требуется
19:06:32 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Параллельно выполняем второй этап всех работ
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по рекомпиляции булочки: Второй этап: для булки #21[Булочка] заменяю внутренний объект на новый на основании отчёта #2115
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Получен запрос на изменение внутреннего объекта
19:06:32 15.06.2024 Мир1: Булочка #21[Булочка]: Новый объект: ObjectCreateResult
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Запрос на изменение внутреннего объекта отправлен в начинку
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Замена внутреннего объекта на новый объект "Булочка"...
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Вход в критическую секцию...
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Вход в критическую секцию завершён.
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Сброс флага необходимости обновления кода значениями
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Единомоментная атомарная замена внутреннего объекта
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Обновление объекта мониторинга изменения свойств
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Задан новый объект наблюдения: Булочка
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Сканируем поля и свойства нового объекта
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Сканирование типа Булочка
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Скачем по полям
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Проверка поля Wide
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Обнаружено поле Wide
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Проверка поля Back
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Обнаружено поле Back
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Проверка поля Exposure
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Обнаружено поле Exposure
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Скачем по свойствам
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Скачем по методам
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Проверка метода Process
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Метод Process отбракован, так как то ли он не public, то ли имеет обязательные параметры
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Проверка метода GetType
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Обнаружен метод GetType
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Проверка метода ToString
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Обнаружен метод ToString
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Проверка метода Equals
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Метод Equals отбракован, так как то ли он не public, то ли имеет обязательные параметры
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Проверка метода GetHashCode
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Обнаружен метод GetHashCode
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Итого найдено 3 полей и 0 свойств
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Мы молодцы :3
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Сканирование успешно завершено. Найдено 3 полей и 0 свойств
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Вход в критическую секцию...
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Вход в критическую секцию произведён успешно.
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Запоминаем свойства
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Запоминаем поля
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Запоминаем методы
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Сбрасываем память предыдущих значений
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Устанавливаем новый объект наблюдения
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Выход из критической секции завершён успешно.
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Выход из критической секции завершён.
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Вызов события изменения объекта...
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Вызов события изменения объекта завершён.
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Завершение обновления внутреннего объекта начинки
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Замена объекта заврешена. Результат :True, старый объект: ObjectCreateResult
19:06:34 15.06.2024 Мир1: Булочка #21[Булочка]: Вызываем событие AfterInnerObjectAndCodeReplaced
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по рекомпиляции булочки: Замена произведена успешно
19:06:37 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Подтормаживаем на 50 мс перед третьим этапом по выгрузке булок
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Попытка получения значения поля/свойства Wide
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Значения поля/свойства Wide получено успешно. Производится конвертация
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Конвертация завершена. Результат: 0; 1; 0. Ура!
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Попытка получения значения поля/свойства Back
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Значения поля/свойства Back получено успешно. Производится конвертация
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Конвертация завершена. Результат: 1; 0; 0. Ура!
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Попытка получения значения поля/свойства Exposure
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Значения поля/свойства Exposure получено успешно. Производится конвертация
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Конвертация завершена. Результат: 0. Ура!
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Попытка получения значения поля/свойства Exposure
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Значения поля/свойства Exposure получено успешно. Производится конвертация
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Конвертация завершена. Результат: 0. Ура!
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Попытка получения значения поля/свойства Exposure
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Значения поля/свойства Exposure получено успешно. Производится конвертация
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Конвертация завершена. Результат: 0. Ура!
19:06:37 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Параллельно выполняем третий этап всех работ по выгрузке булок
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по рекомпиляции булочки: Третий этап: надо выгрузить предыдущий внутренний объект булки #21[Булочка]
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по рекомпиляции булочки: Приступаю к выгрузке объекта
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по рекомпиляции булочки: У предыдущего объекта нет метода Dispose, поэтому просто оставляем его сборщику мусора
19:06:37 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по рекомпиляции булочки: Выгрузка объекта завершена.
19:06:37 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Работы завершены
19:06:37 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: -----------Работы завершены. Результат: True. Выход из критической секции-------------------
19:06:37 15.06.2024 Мир1: Менеджер работ: Исполнитель завершил выполнение работ.
19:06:37 15.06.2024 Мир1: Менеджер работ: Выход из критической секции.
Пример лога когда в коде просто поменяли значение поля
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Получен запрос на изменение кода на public class Булочка...
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Запрашиваем у начинки новое предписание для кода public class Булочка...
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Запрос на генерацию предписания для обновления кода до public class Булочка...
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Предварительно спрошу у мира, хочет ли он вообще полной перекомпиляции всего
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Нет, мир не хочет рекомпилироваться, разбираемся дальше
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Парсинг синтаксического дерева нового кода...
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Парсинг синтаксического дерева нового кода завершён.
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Парсинг синтаксического дерева текущего кода...
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Парсинг синтаксического дерева текущего кода завершён.
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Приступаем к парсингу дерева.
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Сравнение деревьев public class Булочка...(Булочка) и public class Булочка...
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Вытаскиваем все типы и делегаты из первого дерева...
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Обнаружено 1 типов и делегатов
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Вытаскиваем все типы и делегаты из второго дерева...
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Обнаружено 1 типов и делегатов
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Поиск главного типа среди типов первого дерева...
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Главный тип первого дерева найден. Это Булочка
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Поиск главного типа среди типов второго дерева...
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Главный тип второго дерева найден. Это Булочка
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ деклараций:
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Типов нет
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Методов нет
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Событий нет
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Делегатов нет
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Индексаторов нет
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Полей нет
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Свойств нет
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ деклараций завершён
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: В ходе анализа изменений не выявлено
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Начало сравнения главных типов...
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Сравнение типов Булочка и Булочка
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Типы Булочка и Булочка - это классы
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: У Булочка и Булочка по 4 членов
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Число членов равно, приступаем к сравнению значений
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ деклараций:
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Типов нет
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ 1 объявлений методов
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Тело метода не изменилось
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Методы не поменялись
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Исключаем 1 методов из анализа - с ними уже всё понятно
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Событий нет
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Делегатов нет
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Индексаторов нет
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ 3 объявлений полей
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ поля 1 из 3 Wide...
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: , надо поменять значение в существующем объекте. Всего таких изменений теперь: 1
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ поля Wide завершён.
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ поля 2 из 3 Back...
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: , надо поменять значение в существующем объекте. Всего таких изменений теперь: 2
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ поля Back завершён.
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ поля 3 из 3 Exposure...
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Значение поля Exposure не изменилось
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ поля Exposure завершён.
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Выявлено 2 изменений значений по умолчанию у полей. Значения будут обновлены у существующего объекта без рекомпиляции
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ 3 полей показал изменения 2 значений по умолчанию
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: У поля Wide значение изменилось на 0; 1; 0
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: У поля Back значение изменилось на 1; 0,64705884; 0
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Запоминаем новые значения 2 полей, чтобы изменить их в объекте без рекомпиляции
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Исключаем 3 полей из анализа - с ними уже всё понятно
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Свойств нет
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Анализ деклараций завершён
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: В ходе анализа деклараций было выявлено изменение 2 свойств/полей
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Сравнение значений показывает, что изменились только значения - можно деликатно обновить код
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Сравнение типов Булочка и Булочка показало результат ValuesChange
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Анализатор изменений кода: Сравнение главных типов завершено. Результат: ValuesChange
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Парсинг дерева завершён. Результат: ValuesChange
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Сгенерировано предписание обновить значения булочки в коде, флаг необходимости обновления кода по значениям будет сброшен - он будет восстановлен по выполнении нового предписания
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Генерация предписания обновления 2.
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Предписание #2016[ValuesUpdatePrescription] получено
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Просим менеджера назначить работу по исполнению предписания #2016
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Инициатор работ: От нас требуют сформировать работу в соответствии с предписанием ValuesUpdatePrescription для булки #21[Булочка]
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Для исполнения предписания #2016 менеджер создал работу #19920975
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Выполняем запрос у мира на регистрацию новой работы #19920975
20:09:16 15.06.2024 Мир1: Получен запрос на регистрацию новой работы #19920975
20:09:16 15.06.2024 Мир1: Перенаправляю работу #19920975 менеджеру...
20:09:16 15.06.2024 Мир1: Менеджер работ: Поступила новая работа #19920975 типа ValuesUpdateJob от булки #21 Булочка
20:09:16 15.06.2024 Мир1: Менеджер работ: Работа зарегистрирована
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Регистрация новой работы #19920975 произведена успешно
20:09:16 15.06.2024 Мир1: Менеджер работ: В очереди работ новенькие!
20:09:16 15.06.2024 Мир1: Менеджер работ: Вход в критическую секцию...
20:09:16 15.06.2024 Мир1: Менеджер работ: Вход в критическую секцию успешно произведён.
20:09:16 15.06.2024 Мир1: Менеджер работ: Извлечение всех скопишвихся в очереди работ...
20:09:16 15.06.2024 Мир1: Менеджер работ: Извлечено работ: 1
20:09:16 15.06.2024 Мир1: Менеджер работ: Работы передаются исполнителю...
20:09:16 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: -------------------Выполнение новых работ. Всего работ в списке: 1----------------------------
20:09:16 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Проверка работ на поддерживаемость и корректность....
20:09:16 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Вход в критическую секцию...
20:09:16 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Вход в критическую секцию успешно выполнен. Выполнение работ...
20:09:16 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Назначаем работы по булкам
20:09:16 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Булке #21 Булочка назначена работа ValuesUpdateJob
20:09:16 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Параллельно выполняем первый этап всех работ
20:09:16 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Обновляем печку, если требуется
20:09:16 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Параллельно выполняем второй этап всех работ
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по обновлению значений: Приступаю к обновлению 2 значений
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по обновлению значений: Обновление значения поля/свойства Wide новым значением 0; 1; 0
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Попытка установки поля/свойства Wide значением 0; 1; 0
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Значение 0; 1; 0 успешно прошло конвертацию и направлено в булкафлекшн для присвоения значения
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Результат: True
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по обновлению значений: Поле/свойство обновлено
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по обновлению значений: Обновление значения поля/свойства Back новым значением 1; 0,64705884; 0
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Попытка установки поля/свойства Back значением 1; 0,64705884; 0
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Значение 1; 0,64705884; 0 успешно прошло конвертацию и направлено в булкафлекшн для присвоения значения
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Результат: True
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по обновлению значений: Поле/свойство обновлено
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по обновлению значений: Все значения обновлены
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Работа по обновлению значений: Обновление значений завершено
20:09:16 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Подтормаживаем на 50 мс перед третьим этапом по выгрузке булок
20:09:16 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Параллельно выполняем третий этап всех работ по выгрузке булок
20:09:16 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: Работы завершены
20:09:16 15.06.2024 Мир1: Менеджер работ: Исполнитель работ: -----------Работы завершены. Результат: True. Выход из критической секции-------------------
20:09:16 15.06.2024 Мир1: Менеджер работ: Исполнитель завершил выполнение работ.
20:09:16 15.06.2024 Мир1: Менеджер работ: Выход из критической секции.
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Поле Back изменено
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Обнаружены изменения!
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Back изменило значение с 1; 0; 0 на 1; 0,64705884; 0
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Вызов событий...
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Обнаружено изменение свойств: и Back=1; 0,64705884; 0
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Установлен флаг информирования об изменении свойств
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Запрос на обновление кода в соответствии с новыми значениями объекта, если это необходимо
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Флаг информирования об изменении свойств был включен. Сбрасываем его - он свою функцию выполнил. Теперь обновляем код...
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Запечатливаем текущий код, который нам нужно обновить значениями, чтобы потом проверить, не изменился ли он, пока мы генерировали новый код с внесёнными значениями из объекта
20:09:16 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Подан запрос на исполнение обновления кода в соответствии с новыми значениями
20:09:17 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Исполнение обновления кода завершено
20:09:17 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Вход в критическую секцию обновления кода...
20:09:17 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Вход в критическую секцию обновления кода завершён успешно.
20:09:17 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Старый код:public class Булочка...
20:09:17 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Новый код:public class Булочка...
20:09:17 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Производим замену...
20:09:17 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Код успешно заменён.
20:09:17 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Детектор изменений объекта: Вызов событий завершён.
20:09:17 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Попытка получения значения поля/свойства Back
20:09:17 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Значения поля/свойства Back получено успешно. Производится конвертация
20:09:17 15.06.2024 Мир1: Булочка #21[Булочка]: Начинка: Конвертация завершена. Результат: 1; 0,64705884; 0. Ура!
Редактор кода
Здесь всё банально — взял AvalonEdit, накатил подсветочку, сворачиваемые блоки кода и автоформатирование с автокомплитом. Однако интересные моменты тоже есть.
Подсветка заточена не только под C#, но и под пепекторы. Причём для каждой размерности пепекторов свой цвет — двумерные, трёхмерные и четырёхмерные типы.

Это хорошо страхует от возможности перепутать размерность и получить трудноуловимый баг.
Автокомплит имеет особенности.
Самое важное: вырвиглазные олдскульные значки из VisualBasic 6. Были заапскейлены в 2 раза и немного пропатчены руками. Изначально хотел поставить современные значки, но потом решил по приколу воткнуть эти. Мои любимые сверхзвуковые яблочные фрутеллы в комплекте.

У пепекторов очень много свизлинговых свойств (это которые xyzw, rgba и кзса), и выводить их все в автокомплит, особенно у четырёхмерных типов, явно не стоит. Это банально неудобно. Поэтому я сгруппировал их в отдельные анимированные пункты.

Хаковые свойства пепекторов BitAs… блёклые чтобы не заспамливать внимание и как бы намекать «мож не стоит всё таки». Однотипные свойства, позволяющие обратиться к первой, второй и далее группе бит, объединяются в один пункт. Например, четырёхбайтный вектор sbyte4 можно интерпретировать как одно значение int (4 байта) с помощью BitsAsInt, а можно как два значения short (по 2 байта) с помощью свойств BitsAsShort1 и BitsAsShort2. И вот эти два свойства объединяются в пункт BitsAsShort1…2

При обращении к другим булкам по имени автокомплит корректно срабатывает, несмотря на то, что булки видят друг друга как объекты типа dynamic. Редактор кода отдельно обрабатывает именно случай с обращением к булкам.

В остальном всё обычно — редактор как редактор. На то, чтобы прикрутить сюда отладчик, я пока не решился, но уже думаю об этом :)
Напоследок
На последок расскажу про алгоритмы парочки эффектов. Начнём с вот этой штуки, задача которой — создавать атмосферу ночного города.

Работает он на самом деле довольно просто. Берём девочку и суём её в ClassicBitmap32. Вот эту:

Далее выпускаем бегать по ней десяток чюваков, имеющих какие‑нибудь рандомные ускорения и скорости. Особенности динамики и тактико‑технические характеристики чюваков для данного эффекта не сильно важны — главное, чтобы перемещались и не разбежались. У меня они бегают с ускорением.

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

Далее всё просто: на каждом кадре считываем цвета пикселей картинки вдоль этих виртуальных нитей и тупо выводим их на ленты в том же порядке, в каком считали. Всё.

В итоге получаем ту самую штуку:

Код булки с чюваками
public enum DudeComboMode
{
Replace,
Multiplication,
Add,
}
public class ГеометрическийВальс
{
const int LEDCount = 2315;
static readonly ReadOnlySet<string> supportedImageExtensions = new("png,jpg,bmp,wmf".Split(',').Select(a => '.' + a).ToHashSet());
static readonly ThreadLocal<Random> _rnd = new ThreadLocal<Random>(() => new Random());
static Random rnd => _rnd.Value;
public ГеометрическийВальс()
{
calculatedColorsBooffer1 = new Frame(Installation);
calculatedColorsBooffer2 = new Frame(Installation);
}
//Чюваки, которые гуляют по картинке
//между чюваками натянуты нитчоки
//по ним сканируем пиксели и переносим на ленты
readonly List<dude> dudes = new();
//буферы для цветов
readonly Frame calculatedColorsBooffer1; //тут всё считаем в отдельном потоке, потом с локом копируем в 2
readonly Frame calculatedColorsBooffer2; //отсюда переносим в кадр с локом
[Combo("Режим смешивания")]
public DudeComboMode ComboMode { get; set; } = DudeComboMode.Replace;
[Slider("Количество чюваков", 3, 20)]
public int WalkerCount { get; set; } = 11;
[Slider("Вер. смены напр.", 0, 0.1)]
public double DirectionChangeProbability { get; set; } = 0.01;
[TextBox("Путь к картинке")]
public string ImageAddress { get; set; } = "ambigirl.jpg";
[Slider("Макс. скорость: #,## пикс/сек", 0, 10)]
public double MaxSpeed { get; set; } = 2;
[Slider("Макс. ускорение: #,## пикс/сек²", 0, 5)]
public double MaxAcceleration { get; set; } = 0.3;
[Slider("Гамма: #,##", 0.3, 3)]
public double Gamma { get; set; } = 1;
[Slider("Усиление яркости: #,#%", 0, 500)]
public double BrightnessPercent { get; set; } = 100;
[Slider("Отсечка мин. яркости: #,#%", 0, 100)]
public double MinBrightnessPercent { get; set; } = 0;
[Slider("Отсечка макс. яркости: #,#%", 0, 100)]
public double MaxBrightnessPercent { get; set; } = 100;
[CheckBox("Сначала залить светом")]
public bool FillAtBegin { get; set; } = false;
[Color("Цвет заливки")]
public float3 FillColor { get; set; } = float3.Gold;
[CheckBox("Инертность")]
public bool UseInertion = true;
[Slider("Степень инерции: #,#%", 0, 100)]
public double InertionPercent { get; set; } = 90;
string oldImageAddress = "";
ClassicBitmap32? bitmap = null;
public void Background()
{
var needReinit = recreateBitmapIfNeed();
updateDudeCount();
if (needReinit)
initAllDudes();
foreach (var dude in dudes)
dude.applySettings(MaxSpeed, MaxAcceleration);
foreach (var dude in dudes)
dude.Move();
scanImage();
}
private void initAllDudes()
{
foreach (var dude in dudes)
dude.Init(rnd, bitmap, MaxAcceleration);
}
private void updateDudeCount()
{
while (dudes.Count < WalkerCount)
{
var dude = new dude();
dude.Init(rnd, bitmap, MaxAcceleration);
dudes.Add(dude);
}
while (dudes.Count > WalkerCount)
dudes.RemoveAt(dudes.Count - 1);
}
private bool recreateBitmapIfNeed()
{
bool needReinit = false;
var currentImageAddress = ImageAddress;
if (!string.IsNullOrWhiteSpace(currentImageAddress))
if (currentImageAddress != oldImageAddress)
{
while (currentImageAddress.Length > 0 && currentImageAddress[0] is ' ' or '\"' or '\t' or '\'')
currentImageAddress = currentImageAddress[1..];
while (currentImageAddress.Length > 0 && currentImageAddress[^1] is ' ' or '\"' or '\t' or '\'')
currentImageAddress = currentImageAddress[..^1];
if (File.Exists(currentImageAddress))
{
var ext = Path.GetExtension(currentImageAddress).ToLower();
if (supportedImageExtensions.Contains(ext))
{
bitmap?.DisposeProtected();
bitmap = ClassicBitmap32.FromFile(currentImageAddress);
needReinit = true;
ImageAddress = oldImageAddress = currentImageAddress;
}
}
}
return needReinit;
}
private void scanImage()
{
if (bitmap?.IsDisposed != false)
return;
using var ds = bitmap.DisposeProtectedScope;
if (ds.IsFailed(false))
return;
int outputColorIndex = 0;
for (int dudeIndex = 0; dudeIndex < dudes.Count; dudeIndex++)
{
var dude = dudes[dudeIndex];
var nextDude = dudes[(dudeIndex + 1) % dudes.Count];
var ledsPerDude = LEDCount / dudes.Count;
if (dudeIndex == dudes.Count - 1)
ledsPerDude += (LEDCount - LEDCount / dudes.Count * dudes.Count);
var from = dude.Location;
var to = nextDude.Location;
var step = (to - from) / ledsPerDude;
for (int ledIndex = 0; ledIndex < ledsPerDude; ledIndex++)
{
double2 cords = from + step * ledIndex;
var color = bitmap.GetSampleAbsolute((float2)cords);
calculatedColorsBooffer1[outputColorIndex++] = color.bgr;
}
}
Peperubka.Mul(calculatedColorsBooffer1, 1.0f / 255);
Peperubka.Pow(calculatedColorsBooffer1, (float3)(1.0f / Gamma), true);
Peperubka.Mul(calculatedColorsBooffer1, (float3)BrightnessPercent * 0.01f);
Peperubka.Clamp(calculatedColorsBooffer1, (float3)Pepe.Min(MaxBrightnessPercent, MinBrightnessPercent) * 0.01f, (float3)Pepe.Max(MaxBrightnessPercent, MinBrightnessPercent) * 0.01f);
if (UseInertion)
calculatedColorsBooffer1.Mul(1.0f - (float)InertionPercent * 0.01f);
lock (calculatedColorsBooffer2)
{
if (UseInertion)
{
calculatedColorsBooffer2.Mul((float)InertionPercent * 0.01f);
calculatedColorsBooffer2.Add(calculatedColorsBooffer1);
}
else
calculatedColorsBooffer2.TryCopyFrom(calculatedColorsBooffer1);
}
}
public void Process(Frame frame)
{
if (FillAtBegin)
frame.Fill(FillColor);
lock (calculatedColorsBooffer2)
{
switch (ComboMode)
{
case DudeComboMode.Replace:
frame.CopyFrom(calculatedColorsBooffer2);
break;
case DudeComboMode.Multiplication:
frame.Mul(calculatedColorsBooffer2);
break;
case DudeComboMode.Add:
frame.Add(calculatedColorsBooffer2);
break;
}
}
}
class dude
{
public double2 Location, Speed, Acceleration;
public double MaxSpeed, MaxAcceleration;
public double4 CanvasBounds = (0, 0, 1920, 1080);
public double4 CanvasBorderBounces = 0.9;
public void SelectRandomAcceleration()
{
Acceleration = double2.FromAngleAndRadius(rnd.NextTurns(), MaxAcceleration);
}
public void Move()
{
Speed += Acceleration;
Speed = Pepe.Clamp(Pepe.Abs(Speed), 0, Pepe.Abs(MaxSpeed)) * Pepe.Sign(Speed);
Location += Speed;
for (int i = 0; i < 2; i++)
{
if (Location[i] < CanvasBounds[i])
{
Location[i] = CanvasBounds[i];
Speed[i] *= -1;
Speed[i] *= CanvasBorderBounces[i];
Acceleration[i] *= -1;
}
else if (Location[i] > CanvasBounds[i + 2])
{
Location[i] = CanvasBounds[i + 2];
Speed[i] *= -1;
Speed[i] *= CanvasBorderBounces[i + 2];
Acceleration[i] *= -1;
}
}
}
internal void Init(Random rnd, ClassicBitmap32? bitmap, double maxAcceleration)
{
if (bitmap?.IsDisposed != false)
return;
using var ds = bitmap.DisposeProtectedScope;
if (ds.IsFailed())
return;
MaxAcceleration = maxAcceleration;
CanvasBounds = (0, 0, (double2)(bitmap.SizeInt - 1));
Location = rnd.NextDouble2() * CanvasBounds.Size + CanvasBounds.Location;
Speed = double2.FromAngleAndRadius(rnd.NextDegreeds(), MaxSpeed);
SelectRandomAcceleration();
}
internal void applySettings(double maxSpeed, double maxAcceleration)
{
MaxSpeed = maxSpeed;
MaxAcceleration = maxAcceleration;
}
}
public void Dispose()
{
bitmap?.DisposeProtected();
calculatedColorsBooffer1?.DisposeProtected();
calculatedColorsBooffer2?.DisposeProtected();
}
}
Второй интересный, но более простой эффект — интерференция, проще говоря, наложение волн.

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

Цвета генераторов подбираются не совсем рандомно — высоковероятно (но не всегда), оттенки цветов будут делаться ±сочетаемыми по кругу Иттена — триады, оппозиты, комплиментарные цвета и вот это всё + небольшое смещение оттенка. Волны, которые создают эти генераторы, складываются и выводятся в контур логических лент. Для ближнего и дальнего света создаётся свой набор генераторов.

Можно было бы, конечно, сделать каждый генератор настраиваемым, но подход с зерном гораздо проще и удобнее, т.к. реально заморачиваться с настройкой волн почти никогда не требуется.
Код эффекта "Интерференция"
public class Интерференция
{
class WaveGenerator
{
public radians Phase, PhaseSpeed;
public float3 Amplitude;
public float Frequency;
public void AddTo(SpanConture conture, float3 K)
{
try
{
radians phase = Phase;
radians step = 2 * Pepe.Pi / conture.LEDCount * Frequency;
for (int i = 0; i < conture.LEDCount; i++)
{
conture[i] += (float)(0.5 + 0.5 * Pepe.Sin(phase)) * Amplitude * K;
phase += step;
}
}
finally
{
Phase += PhaseSpeed;
}
}
public void Randomize(Random rnd)
{
Amplitude = float3.HSLtoRGB((rnd.NextTurns(), 1, 0.5f));
Frequency = (float)(Pepe.Pow(rnd.NextDouble(), 2) * 11) + 0.001f;
for (int i = 0; i < 2; i++)
if (rnd.NextDouble() > 0.95)
Frequency *= 10;
Phase = rnd.NextTurns();
PhaseSpeed = 0.01 * Pepe.Pow(rnd.NextDouble(), 2) + 0.02;
PhaseSpeed *= Frequency;
if (rnd.NextDouble() > 0.98)
PhaseSpeed *= rnd.Next(1, 5);
if (rnd.NextDouble() > 0.5)
PhaseSpeed *= -1;
}
}
class WaveGeneratorPack
{
readonly List<WaveGenerator> generators = new();
int _generatorCount = 5;
public int GeneratorCount
{
get => _generatorCount;
private set
{
if (value > 256)
value = 256;
while (generators.Count < value)
generators.Add(new WaveGenerator());
while (generators.Count > value && generators.Count > 0)
generators.RemoveAt(generators.Count - 1);
}
}
void correctHue(Random rnd)
{
if (rnd.NextDouble() > 0.9)
return;
try
{
static (degreeds hue1, degreeds? hue2) generateNextColors(degreeds current, Random rnd, degreeds ditcher)
{
bool useTriada = rnd.NextDouble() > 0.8;
if (useTriada)
{
var hue1 = current + 120 + (rnd.NextDouble() * ditcher - ditcher / 2);
var hue2 = current - 120 + (rnd.NextDouble() * ditcher - ditcher / 2);
return (hue1.NormalizeTurns(), hue2.NormalizeTurns());
}
else
{
degreeds hue = default;
int mode = rnd.Next(0, 3);
if (mode == 0) //оппозит
{
hue = current + 180 + (rnd.NextDouble() * ditcher - ditcher / 2);
}
else if (mode == 1)//соседний
{
//hue = current + 30 + (rnd.NextDouble() * ditcher - ditcher / 2) * (rnd.Next(0, 2) * 2 - 1); слишком похоже
hue = current + 60 + (rnd.NextDouble() * ditcher - ditcher / 2) * (rnd.Next(0, 2) * 2 - 1);
}
else if (mode == 2) //сплит-комплиментарые
{
hue = current + 150 * (rnd.Next(0, 2) * 2 - 1) + (rnd.NextDouble() * ditcher - ditcher / 2);
}
return (hue, null);
}
}
if (generators.Count > 0)
if (rnd.NextDouble() > 0.8)
{
degreeds hue = generators.First().Amplitude.Hue;
hue = Pepe.Round(hue / 30) * 30;
generators.First().Amplitude = generators.First().Amplitude.WithHue(hue);
}
for (int i = 1; i < generators.Count;)
{
if (rnd.NextDouble() > 0.9)
{
i++;
if (rnd.NextDouble() > 0.6)
i++;
continue;
}
var hue = generators[i - 1].Amplitude.Hue;
degreeds ditcher = Math.Pow(rnd.NextDouble(), 2) * 30;
var (hue1, hue2) = generateNextColors(hue, rnd, ditcher);
generators[i].Amplitude = generators[i].Amplitude.WithHue(hue1);
i++;
if (hue2.HasValue)
{
generators[i].Amplitude = generators[i].Amplitude.WithHue(hue2.Value);
i++;
}
}
}
catch (Exception er)
{
}
}
public void Randomize(Random rnd)
{
GeneratorCount = (int)(Pepe.Pow(rnd.NextDouble(), 2) * 10 + 1);
foreach (var item in generators)
item.Randomize(rnd);
correctHue(rnd);
}
public void AddTo(SpanConture c, float3 brightness, int? previewIndex = null)
{
float3 sumAmplitude = 0;
foreach (var item in generators)
sumAmplitude += item.Amplitude;
float3 K = brightness / sumAmplitude.MinLimit(1);
{
int index = 0;
foreach (var item in generators)
{
if (previewIndex is null || previewIndex == index)
item.AddTo(c, K);
index++;
}
}
float3 min = float3.MaxValue;
float3 max = float3.MinValue;
for (int i = 0; i < c.LEDCount; i++)
{
float3 color = c[i];
min = float3.Min(min, color);
max = float3.Max(max, color);
}
min = min.FindMinValue();
max = max.FindMaxValue();
float3 k = 1.0f / (max - min);
for (int i = 0; i < c.LEDCount; i++)
{
c[i] = (c[i] - min) * k;
}
}
}
readonly WaveGeneratorPack widePack = new(), backPack = new();
public void NewSeed() => Seed = new Random().NextInt64().ToString();
public string Seed = "4769073785704359011";
[Slider("Яркость: #,#%", 0, 1000, 3)]
public float BrightnessPercent = 66.66932f;
[Slider("Индекс предпросмотра: #", -1, 20)]
public int MonoIndex = 6;
atomic_bool needRandomize = true;
public void OnPropertyChange(string propName)
{
if (propName == nameof(Seed))
needRandomize = true;
}
public void Process(Frame f)
{
if (needRandomize && needRandomize.SetInterlockedAndGetPrevous(false))
{
var initNumber = (Seed ?? "").GetKnythHashCode();
var random = new Random(initNumber);
backPack.Randomize(random);
widePack.Randomize(random);
BrightnessPercent = (float)(50 + random.NextDouble() * 100);
}
int? backIndex, wideIndex;
backIndex = wideIndex = null;
if (MonoIndex >= 0)
{
if (MonoIndex < backPack.GeneratorCount)
{
backIndex = MonoIndex;
wideIndex = 100500;
}
else
{
wideIndex = MonoIndex - backPack.GeneratorCount;
backIndex = 100500;
}
}
backPack.AddTo(f.Back, BrightnessPercent * 0.01f, backIndex);
widePack.AddTo(f.Wide, BrightnessPercent * 0.01f, wideIndex);
}
}
Стоит отметить, что подобное работает в том числе потому что логические ленты работают с плавными значениями float3. На byte3 такое сделать было бы крайне проблематично.
Ещё есть интересный эффект «Огненное сверкание», который сам по себе ничего не генерирует, но делает сверкающим то, что на него подали. Я его часто использую для всего. Эффект использует синус разной частоты для каждого светодиода отдельно. Чтобы кушать поменьше ресурсов, эффект заранее рассчитывает таблицу синусов для каждого диода в каждый момент времени.
Код эффекта "Огненное сверкание"
public class ОгненноеСверкание
{
const bool EnableBack = true;
const bool EnableWide = true;
const int keyframeCount = 1280;
//Таблицы заранее вычисленных синусов в каждый момент времени - чтобы экономить CPU
readonly Booffer2D<float3> backKeyframes = EnableBack ? new Booffer2D<float3>(BackCount, keyframeCount) : null;
readonly Booffer2D<float3> wideKeyframes = EnableWide ? new Booffer2D<float3>(WideCount, keyframeCount) : null;
public ОгненноеСверкание()
{
build(wideKeyframes);
}
int keyframeIndex = 0;
[CheckBox("Демо-режим")] public bool DemoMode = false;
[Slider("Интервал переключения: # к", 1, 100)] public int DemoSwitchInterval = 30;
[CheckBox("Залить цветом")] public bool UseFillColor = false;
[Color("Цвет заднего")] public float3 BackFillColor = float3.Gold;
[Color("Цвет дальнего")] public float3 WideFillColor = new float3(1.0f, 0.3f, 0.0f);
[Slider("Мин. частота: #,## Гц", 0, 5)] public double MinFrequency = 0.035;
[Slider("Макс. частота: #,## Гц", 0, 5)] public double MaxFrequency = 0.625;
static readonly ReadOnlyCollection<(float3 wide, float3 back)> demoColors = new([
(float3.GreenRGB, float3.BlueRGB),
(float3.BlueRGB, float3.GreenRGB),
(float3.RedRGB, float3.GreenRGB),
(float3.GreenRGB, float3.RedRGB),
(float3.BlueRGB, float3.RedRGB),
(float3.RedRGB, float3.BlueRGB),
]);
atomic_bool needRebuildValues = true;
int demoCounter = 0;
public void Background()
{
if (DemoMode)
{
UseFillColor = true;
unchecked { demoCounter++; if (demoCounter < 0) demoCounter = 0; }
int index = (demoCounter / DemoSwitchInterval) % demoColors.Count;
(BackFillColor, WideFillColor) = demoColors[index];
}
if (needRebuildValues && needRebuildValues.SetInterlockedAndGetPrevous(false))
{
if (EnableWide)
build(wideKeyframes);
if (EnableBack)
build(backKeyframes);
}
}
public void OnPropertyChanged(string propertyName)
{
if (propertyName is nameof(MinFrequency) or nameof(MaxFrequency))
needRebuildValues.Set(true);
}
void build(Booffer2D<float3> keyframeBooffer)
{
using var ds = keyframeBooffer.DisposeProtectedScope;
if (ds.IsFailed())
return;
var rnd = new Random();
var speeds = new float[keyframeBooffer.Width];
var phases = new float[keyframeBooffer.Width];
for (int x = 0; x < keyframeBooffer.Width; x++)
{
var frequency = MinFrequency + (MaxFrequency - MinFrequency) * rnd.NextDouble();
turns blinkCount = (int)(frequency * FPS);
if (blinkCount <= 0)
blinkCount = 1;
turns inc = blinkCount / keyframeCount;
speeds[x] = (float)(radians)inc;
phases[x] = (float)rnd.NextRadians();
}
unsafe
{
fixed (float* speedsPointer = speeds)
fixed (float* phasesPointer = phases)
for (int y = 0; y < keyframeCount; y++)
{
var line = keyframeBooffer.GetPointer_fast_unsafe(0, y);
for (int x = 0; x < keyframeBooffer.Width; x++)
{
line[x] = (float3)MathF.Sin(phasesPointer[x]);
phasesPointer[x] += speedsPointer[x];
}
}
}
Peperubka.Mul(keyframeBooffer, 0.5f);
Peperubka.Add(keyframeBooffer, 0.5f);
}
public void Process(Frame frame)
{
for (int contureIndex = 0; contureIndex < 2; contureIndex++)
{
if (contureIndex == 0 && !EnableBack) continue;
if (contureIndex == 1 && !EnableWide) continue;
var keyframesBooffer = contureIndex == 0 ? backKeyframes : wideKeyframes;
var conture = frame.GetConture(contureIndex);
var line = keyframesBooffer.GetLine(keyframeIndex);
if (UseFillColor)
conture.Fill(contureIndex == 0 ? BackFillColor : WideFillColor);
conture.Mul(line);
}
keyframeIndex++;
if (keyframeIndex >= keyframeCount)
keyframeIndex %= keyframeCount;
}
public void Dispose()
{
backKeyframes?.Dispose();
wideKeyframes?.Dispose();
}
}
В целом, эффекты под эту штуку писать очень просто — что я время от времени и делаю :)
Заключение
Вот и всё:) Три года пролетели незаметно. Я постарался максимально подробно рассказать и показать всю систему от и до. Спасибо всем, кто был со мной на этом пути — ваши отзывы и комментарии очень поддерживали!

Надеюсь, этот проект вдохновит вас создавать, не бояться сложностей и двигаться вперёд.
Harrix
Очень круто!
VBDUnit Автор
Спасибо)