Так‑то это (Ecstatica) уникальная игра, где в движке в качестве примитивов используются не треугольники, как мы привыкли, а эллипсоиды. Но, как говорится, всё новое — это хорошо забытое старое — эллипсоиды нынче вернулись в виде 3D gaussian splatting.
Ещё была Rebel Moon Rising, там тоже что‑то подобное использовалось.
И кстати я тут подумал — есть сейчас в играх DLSS и Frame Generation. По идее можно транслировать картинку с низким разрешением и FPS, и уже на стороне клиента делать апскейл и генерацию кадров (в том числе будущих). Понятно что это не обычный апскейл и надо будет доп. информацию транслировать из рендерера игрового движка, но всё таки почему бы и нет. Есть вероятность что это будет иметь плюсы по сравнению с обычной трансляцией.
По идее эта штука может вырасти в динамическую игру, где в реальном времени плавно будет меняться сценарий, геймплей, графика и жанр, адаптируясь к игроку за счёт обратной связи через анализ эмоций. Да и грань между играми, приложениями и «мирами» имхо постепенно сотрётся.
В среднем я сижу где‑то на расстоянии 70см от центрального экрана, вполне себе получается читать текст, надписи в софте и всякие имена файлов в проводнике и сайты в 100% масштабе.
Всякие 3д‑максы, Visual Studio и After Effectsы для комфортной работы требуют очень много места на экране, а 200% масштаб его тотально сжирает
Регуляция как раз штука удобная, а вот работа стоя, имхо — штука сомнительная. У самого подъёмный стол, но использую его для регуляции и как подъёмник, когда надо в системном блоке ковыряться. А стоя за ним не работаю.
Полноценная клава штука на любителя. Я вот себе поставил полноценную и обнаружил, что буквы теперь слишком сдвинуты влево, т.к. между мышкой и буквами этот нумпад, которым по факту я почти не пользуюсь (только телики поворачивать).
Нумпад нужен только тем кто его активно юзает, остальным он только место занимает. Имхо, лучшее решение — отдельный нумпад СПРАВА от мыши.
Так и есть, просто люди еще не свыклись с мыслью, что монитор может быть размером с телик. И то, что многие современные телики (не все) можно использовать в качестве мониторов.
public static bool IsAllDigitsSpan([NotNull] string str) =>
str.AsSpan().IndexOfAnyExceptInRange('0', '9') == -1;
public static bool IsAllDigits([NotNull] string str)
{
unsafe
{
fixed (char* begin = str)
{
char* ptr = begin;
var charsInVector = Vector<ushort>.Count;
if (str.Length >= charsInVector) //Если строка достаточно длинная для векторов
{
//Вычисляем конец чтобы поместилось целое число векторов
char* endVector = begin + str.Length / charsInVector * charsInVector;
var min = new Vector<ushort>((ushort)'0'); //Вектор состоящий из '0'
var max = new Vector<ushort>((ushort)'9'); //Вектор состоящий из '9'
//Идея простая: берем пачки по N символов
//загоняем в диапазон от '0' до '9'
//и смотрим есть ли изменения - если есть, значит это были не цифры
while (ptr < endVector)
{
var v = Vector.Load((ushort*)ptr); //Загружаем 16 символов в регистр
var vClamped = Vector.ClampNative(v, min, max); //Загоняем в диапазон от '0' до '9'
if (!Vector.EqualsAll(v, vClamped)) //Если что-то изменилось то там были не только цифры
return false;
ptr += charsInVector; //Идем дальше
}
}
char* end = begin + str.Length;
//Если осталось хотя бы 8 символов - добиваем 128-битной инструкцией
if (end - ptr >= Vector128<ushort>.Count)
{
var min128 = Vector128.Create((ushort)'0');
var max128 = Vector128.Create((ushort)'9');
var v = Vector128.Load<ushort>((ushort*)ptr);
if (!Vector128.EqualsAll(Vector128.Clamp(v, min128, max128), v))
return false;
else
ptr += Vector128<ushort>.Count;
}
//Далее анролл по 4
char* end4 = begin + (str.Length & ~3);
while (ptr < end4)
{
if (unchecked((uint)(*ptr - '0')) > 9u) return false; ptr++;
if (unchecked((uint)(*ptr - '0')) > 9u) return false; ptr++;
if (unchecked((uint)(*ptr - '0')) > 9u) return false; ptr++;
if (unchecked((uint)(*ptr - '0')) > 9u) return false; ptr++;
}
//Потом обычным циклом
while (ptr < end)
{
if (unchecked((uint)(*ptr - '0')) > 9u)
return false;
ptr++;
}
}
}
return true;
}
[Benchmark] public bool LEN_12_only_digits() => TurboIsDigitChecker.IsAllDigits(small_only_digits);
[Benchmark] public bool LEN_12_NOT_only_digits() => TurboIsDigitChecker.IsAllDigits(small);
[Benchmark] public bool LEN_256_000_000_only_digits() => TurboIsDigitChecker.IsAllDigits(big_only_digits);
[Benchmark] public bool LEN_256_000_000_NOT_only_digits() => TurboIsDigitChecker.IsAllDigits(big);
[Benchmark] public bool SPAN_LEN_12_only_digits() => TurboIsDigitChecker.IsAllDigitsSpan(small_only_digits);
[Benchmark] public bool SPAN_LEN_12_NOT_only_digits() => TurboIsDigitChecker.IsAllDigitsSpan(small);
[Benchmark] public bool SPAN_LEN_256_000_000_only_digits() => TurboIsDigitChecker.IsAllDigitsSpan(big_only_digits);
[Benchmark] public bool SPAN_LEN_256_000_000_NOT_only_digits() => TurboIsDigitChecker.IsAllDigitsSpan(big);
Предположу, что дело в особенностях реализации SIMD на конкретном железе, размерах кэша, скорости ОЗУ и прочих штуках, поэтому разные подходы дают разные результаты на разном железе. И Spanовская реализация, судя по всему, написана примерно так же, но оказалась лучше заточена под Ваше железо, и хуже под моё.
public static bool IsAllDigits([NotNull] string str)
{
unsafe
{
fixed (char* begin = str)
{
char* ptr = begin;
var charsInVector = Vector<ushort>.Count;
if (str.Length >= charsInVector) //Если строка достаточно длинная для векторов
{
//Вычисляем конец чтобы поместилось целое число векторов
char* endVector = begin + str.Length / charsInVector * charsInVector;
var min = new Vector<ushort>((ushort)'0'); //Вектор состоящий из '0'
var max = new Vector<ushort>((ushort)'9'); //Вектор состоящий из '9'
//Идея простая: берем пачки по N символов
//загоняем в диапазон от '0' до '9'
//и смотрим есть ли изменения - если есть, значит это были не цифры
while (ptr < endVector)
{
var v = Vector.Load((ushort*)ptr); //Загружаем 16 символов в регистр
var vClamped = Vector.ClampNative(v, min, max); //Загоняем в диапазон от '0' до '9'
if (!Vector.EqualsAll(v, vClamped)) //Если что-то изменилось то там были не только цифры
return false;
ptr += charsInVector; //Идем дальше
}
}
char* end = begin + str.Length;
//Если осталось хотя бы 8 символов - добиваем 128-битной инструкцией
if (end - ptr >= Vector128<ushort>.Count)
{
var min128 = Vector128.Create((ushort)'0');
var max128 = Vector128.Create((ushort)'9');
var v = Vector128.Load<ushort>((ushort*)ptr);
if (!Vector128.EqualsAll(Vector128.Clamp(v, min128, max128), v))
return false;
else
ptr += Vector128<ushort>.Count;
}
//Далее анролл по 4
char* end4 = begin + (str.Length & ~3);
while (ptr < end4)
{
if (unchecked((uint)(*ptr - '0')) > 9u) return false; ptr++;
if (unchecked((uint)(*ptr - '0')) > 9u) return false; ptr++;
if (unchecked((uint)(*ptr - '0')) > 9u) return false; ptr++;
if (unchecked((uint)(*ptr - '0')) > 9u) return false; ptr++;
}
//Потом обычным циклом
while (ptr < end)
{
if (unchecked((uint)(*ptr - '0')) > 9u)
return false;
ptr++;
}
}
}
return true;
}
public static bool IsAllDigits512([NotNull] string str)
{
unsafe
{
fixed (char* begin = str)
{
char* ptr = begin;
var charsInVector512 = Vector512<ushort>.Count;
//Основная тушка с векторами
if (str.Length >= charsInVector512) //Если строка достаточно длинная для векторов
{
//Вычисляем конец чтобы поместилось целое число векторов
char* endVector512 = begin + str.Length / charsInVector512 * charsInVector512;
var min = Vector512.Create((ushort)'0'); //Вектор состоящий из '0'
var max = Vector512.Create((ushort)'9'); //Вектор состоящий из '9'
//Идея простая: берем пачки по N символов
//загоняем в диапазон от '0' до '9'
//и смотрим есть ли изменения - если есть, значит это были не цифры
while (ptr < endVector512)
{
var v = Vector512.Load((ushort*)ptr); //Загружаем 16 символов в регистр
var vClamped = Vector512.ClampNative(v, min, max); //Загоняем в диапазон от '0' до '9'
if (!Vector512.EqualsAll(v, vClamped)) //Если что-то изменилось то там были не только цифры
return false;
ptr += charsInVector512; //Идем дальше
}
}
char* end = begin + str.Length;
//Если осталось хотя бы 8 символов - добиваем 128-битной инструкцией
if (end - ptr >= Vector128<ushort>.Count)
{
var min128 = Vector128.Create((ushort)'0');
var max128 = Vector128.Create((ushort)'9');
var v = Vector128.Load<ushort>((ushort*)ptr);
if (!Vector128.EqualsAll(Vector128.Clamp(v, min128, max128), v))
return false;
else
ptr += Vector128<ushort>.Count;
}
//Далее анролл по 4
char* end4 = begin + (str.Length & ~3);
while (ptr < end4)
{
if (unchecked((uint)(*ptr - '0')) > 9u) return false; ptr++;
if (unchecked((uint)(*ptr - '0')) > 9u) return false; ptr++;
if (unchecked((uint)(*ptr - '0')) > 9u) return false; ptr++;
if (unchecked((uint)(*ptr - '0')) > 9u) return false; ptr++;
}
//Потом обычным циклом
while (ptr < end)
{
if (unchecked((uint)(*ptr - '0')) > 9u)
return false;
ptr++;
}
}
}
return true;
}
Код бенчмарка
public class Benchmark
{
static int randomIndex = 0;
static string generateBigRandomString(bool onlyDigits, int count)
{
if (count < 1024)
throw new NotSupportedException();
var rnd = new Random(Interlocked.Increment(ref randomIndex));
var b = new StringBuilder(count);
var beginNoDigits = count / 10 * 9;
for (int i = 0; i < count; i++)
b.Append((char)rnd.Next('0' + 0, '9' + 1));
if (!onlyDigits)
{
for (int i = 0; i < 10; i++)
{
var index = rnd.Next(beginNoDigits, count);
b[index] = (char)rnd.Next('a' + 0, 'z' + 1);
}
}
return b.ToString();
}
string small_only_digits, small;
string big_only_digits, big;
[GlobalSetup]
public void Setup()
{
small_only_digits = "123456732890";
small = "123456732a90";
big_only_digits = generateBigRandomString(true, 256_000_000);
big = generateBigRandomString(false, 256_000_000);
}
[Benchmark] public bool LEN_12_only_digits() => TurboIsDigitChecker.IsAllDigits(small_only_digits);
[Benchmark] public bool LEN_12_NOT_only_digits() => TurboIsDigitChecker.IsAllDigits(small);
[Benchmark] public bool LEN_256_000_000_only_digits() => TurboIsDigitChecker.IsAllDigits(big_only_digits);
[Benchmark] public bool LEN_256_000_000_NOT_only_digits() => TurboIsDigitChecker.IsAllDigits(big);
[Benchmark] public bool LEN_12_only_digits512() => TurboIsDigitChecker.IsAllDigits512(small_only_digits);
[Benchmark] public bool LEN_12_NOT_only_digits512() => TurboIsDigitChecker.IsAllDigits512(small);
[Benchmark] public bool LEN_256_000_000_only_digits512() => TurboIsDigitChecker.IsAllDigits512(big_only_digits);
[Benchmark] public bool LEN_256_000_000_NOT_only_digits512() => TurboIsDigitChecker.IsAllDigits512(big);
}
Дальше можно в зависимости от фактического размера данных затачиваться:
Под короткие строки — тогда выкидываем тушу с векторами и делаем ступенчатую обработку (по типу блока с Vector128, который под тушкой векторов), то есть сначала один раз без цикла проверяем 512, потом 256, потом 128 бит, потом анролл, потом обычный цикл
Под длинные строки — тогда можно перед тушкой с векторами поставить проверку по 1 цифре, которая подравняет указатель памяти до кратности размеру вектора, и далее, в тушке векторов, делать не Load, а LoadAligned, что немного быстрее. Ну и можно поиграться со способом проверки — не Clamp/Equals, а какие‑нибудь хитрые трюки с масками
Так‑то это (Ecstatica) уникальная игра, где в движке в качестве примитивов используются не треугольники, как мы привыкли, а эллипсоиды. Но, как говорится, всё новое — это хорошо забытое старое — эллипсоиды нынче вернулись в виде 3D gaussian splatting.
Ещё была Rebel Moon Rising, там тоже что‑то подобное использовалось.
На GPU она считается. Они просто перестали поддерживать 32-битный PhysX, 64-битный остался.
Есть отдельный алгоритм для быстрого 1/x, он гораздо проще.
И его, кстати, легко реализовать на simdах.
Что с инпутлагом?
И кстати я тут подумал — есть сейчас в играх DLSS и Frame Generation. По идее можно транслировать картинку с низким разрешением и FPS, и уже на стороне клиента делать апскейл и генерацию кадров (в том числе будущих). Понятно что это не обычный апскейл и надо будет доп. информацию транслировать из рендерера игрового движка, но всё таки почему бы и нет. Есть вероятность что это будет иметь плюсы по сравнению с обычной трансляцией.
Или его улучшенный вариант :)
Зависит от состава и от реальной способности камеры видеть UV излучение
Шторки тоже могут не помочь ;)
Поковырял, погонял в реалтайме на видеокарте. Ну такое себе. В будущем, возможно, результат будет лучше.
По идее эта штука может вырасти в динамическую игру, где в реальном времени плавно будет меняться сценарий, геймплей, графика и жанр, адаптируясь к игроку за счёт обратной связи через анализ эмоций. Да и грань между играми, приложениями и «мирами» имхо постепенно сотрётся.
В среднем я сижу где‑то на расстоянии 70см от центрального экрана, вполне себе получается читать текст, надписи в софте и всякие имена файлов в проводнике и сайты в 100% масштабе.
Всякие 3д‑максы, Visual Studio и After Effectsы для комфортной работы требуют очень много места на экране, а 200% масштаб его тотально сжирает
Телевизоров 8K уже давно много в продаже, в том числе и 55 дюймов. Sony BRAVIA 8 K-55XR80 как пример.
Сглаживание включено, шрифты стандартные, масштаб 100%, и мне норм.
Если нужна чёткость, можно просто поставить масштаб 200% и сесть подальше, но тогда на одном экране будет места как на обычном FullHD мониторе.
Если не нужно 120Гц+, можно просто юзать 8K панели. Мне 120 Гц критично, ибо шутеры, а если только работать с текстом , то 8К 60 Гц вполне норм.
Регуляция как раз штука удобная, а вот работа стоя, имхо — штука сомнительная. У самого подъёмный стол, но использую его для регуляции и как подъёмник, когда надо в системном блоке ковыряться. А стоя за ним не работаю.
Полноценная клава штука на любителя. Я вот себе поставил полноценную и обнаружил, что буквы теперь слишком сдвинуты влево, т.к. между мышкой и буквами этот нумпад, которым по факту я почти не пользуюсь (только телики поворачивать).
Нумпад нужен только тем кто его активно юзает, остальным он только место занимает. Имхо, лучшее решение — отдельный нумпад СПРАВА от мыши.
Так и есть, просто люди еще не свыклись с мыслью, что монитор может быть размером с телик. И то, что многие современные телики (не все) можно использовать в качестве мониторов.
А думаете стационарный отключать не будут? :)
Есть вот такие подлокотники для стола:
На вкус и цвет. Мне вот 80 PPI норм.
Хм. У меня наоборот получается, что немного выигрывает:
Код
Предположу, что дело в особенностях реализации SIMD на конкретном железе, размерах кэша, скорости ОЗУ и прочих штуках, поэтому разные подходы дают разные результаты на разном железе. И Spanовская реализация, судя по всему, написана примерно так же, но оказалась лучше заточена под Ваше железо, и хуже под моё.
UPD: вариант, заточенный под мелкие строки:
Код
Для интереса попробовал на интринсиках (.NET9/Release):
Строки в 12 символов за 1.1 — 1.7 нс
Строки в 256 миллионов символов — 10–11 мс
Использование AVX512 прироста не дало
Код бенчмарка
Дальше можно в зависимости от фактического размера данных затачиваться:
Под короткие строки — тогда выкидываем тушу с векторами и делаем ступенчатую обработку (по типу блока с Vector128, который под тушкой векторов), то есть сначала один раз без цикла проверяем 512, потом 256, потом 128 бит, потом анролл, потом обычный цикл
Под длинные строки — тогда можно перед тушкой с векторами поставить проверку по 1 цифре, которая подравняет указатель памяти до кратности размеру вектора, и далее, в тушке векторов, делать не Load, а LoadAligned, что немного быстрее. Ну и можно поиграться со способом проверки — не Clamp/Equals, а какие‑нибудь хитрые трюки с масками