🔹 -0x1E (
🔹 -0x1F (
🔹 -0x20 (
🔹 -0x21 (
🔹 -0x22 (
🔹 -0x23 (
🔹 -0x24 (
🔹 -0x25 (
🔹 -0x26 (
🔹 -0x27 (
🔹 -0x28 (
🔹 -0x29 (
🔹 -0x2A (
🔹 -0x2B (
🔹 -0x2C (
Total number of threads (including the main thread) can't be greater than 256 (in this API version). The main thread has index 0.
Mutex / condition variable id must be in range from 0 to 63 (in this API version).
Once flag must be in range from 0 to 63 (in this API version).
Конечно, в первую очередь я буду реализовывать другие группы функций ("control and miscellaneous functions" и "video functions"), а потом уже эту. Но описал пока "threading functions".
Ваши мысли, господа!🙂
@SYNC_LOCK
) — lock mutex exclusively / enter critical section for read and write access (al
= mutex id). Returns CF
: 0 — success; 1 — wrong al
value on call.🔹 -0x1F (
@SYNC_LOCK_TIMEOUT
) — lock mutex exclusively / enter critical section for read and write access with timeout (al
= mutex id; ecx
= timeout in milliseconds). Returns CF
: 0 — successfully locked; 1 — time is out or wrong al
value on call.🔹 -0x20 (
@TRY_SYNC_LOCK
) — try to lock mutex exclusively / try to enter critical section for read and write access (al
= mutex id). Returns CF
: 0 — successfully locked; 1 — mutex is locked by another thread or wrong al
value on call.🔹 -0x21 (
@SYNC_UNLOCK
) — unlock exclusively locked mutex / leave critical section with read and write access (al
= mutex id). Returns CF
: 0 — success; 1 — wrong al
value on call.🔹 -0x22 (
@SYNC_LOCK_SHARED
) — lock mutex for shared ownership / enter critical section for read only access (al
= mutex id). Returns CF
: 0 — success; 1 — wrong al
value on call.🔹 -0x23 (
@SYNC_LOCK_SHARED_TIMEOUT
) — lock mutex for shared ownership / enter critical section for read only access with timeout (al
= mutex id; ecx
= timeout in milliseconds). Returns CF
: 0 — successfully locked; 1 — time is out or wrong al
value on call.🔹 -0x24 (
@TRY_SYNC_LOCK_SHARED
) — try to lock mutex for shared ownership / try to enter critical section for read only access (al
= mutex id). Returns CF
: 0 — successfully locked; 1 — mutex is locked by another thread or wrong al
value on call.🔹 -0x25 (
@SYNC_UNLOCK_SHARED
) — unlock mutex locked with shared ownership / leave critical section with read only access (al
= mutex id). Returns CF
: 0 — success; 1 — wrong al
value on call.🔹 -0x26 (
@WAIT_CONDVAR
) — wait until condition variable is awakened, lock mutex exclusively (al
= mutex / condition variable id (if high bit is set then mutex is already locked)). Returns CF
: 0 — success; 1 — wrong al
value on call.🔹 -0x27 (
@WAIT_CONDVAR_TIMEOUT
) — wait (with timeout) until condition variable is awakened, lock mutex exclusively (al
= mutex / condition variable id (if high bit is set then mutex is already locked); ecx
= timeout in milliseconds). Returns CF
: 0 — success; 1 — time is out or wrong al
value on call.🔹 -0x28 (
@WAIT_CONDVAR_SHARED
) — wait until condition variable is awakened, lock mutex for shared ownership (al
= mutex / condition variable id (if high bit is set then mutex is already locked)). Returns CF
: 0 — success; 1 — wrong al
value on call.🔹 -0x29 (
@WAIT_CONDVAR_SHARED_TIMEOUT
) — wait (with timeout) until condition variable is awakened, lock mutex for shared ownership (al
= mutex / condition variable id (if high bit is set then mutex is already locked); ecx
= timeout in milliseconds). Returns CF
: 0 — success; 1 — time is out or wrong al
value on call.🔹 -0x2A (
@NOTIFY_ONE
) — notify one waiting thread (al
= mutex / condition variable id). Returns CF
: 0 — success; 1 — wrong al
value on call.🔹 -0x2B (
@NOTIFY_ALL
) — notify all waiting threads (al
= mutex / condition variable id). Returns CF
: 0 — success; 1 — wrong al
value on call.🔹 -0x2C (
@CALL_ONCE
) — call function once even if other threads try to call it (al
= once flag id; edx
= function address). Returns CF
: 0 — success; 1 — wrong al
value on call.Total number of threads (including the main thread) can't be greater than 256 (in this API version). The main thread has index 0.
Mutex / condition variable id must be in range from 0 to 63 (in this API version).
Once flag must be in range from 0 to 63 (in this API version).
Конечно, в первую очередь я буду реализовывать другие группы функций ("control and miscellaneous functions" и "video functions"), а потом уже эту. Но описал пока "threading functions".
Ваши мысли, господа!
Please open Telegram to view this post
VIEW IN TELEGRAM
Вчера и позавчера исправлял механизм запуска потоков и блок отлова исключений (до этого был лишний re-throw в обработчике SEH, теперь всё чётко). Ну и перелопатил функции для работы с потоками (что выше присылал). Теперь хочу это список ещё укоротить (объединив некоторые функции), чтобы они влезли в один блок из 16 функций.
В связи с этим задумался вот о чём. Как удобнее передавать параметры: через регистры или через стек? Эволюция calling convention пришла к регистрам (в x86 был стёк, в x64 стали регистры для первых параметров), но хорошо ли это для интр?
С одной стороны, регистры — это скорость, нет лишних обращений к памяти (стеку), да и регистр можно переиспользовать, и если в нём уже есть нужное значение, не нужно делать лишний
Эта мысль влечёт за собой следующую. Так как у меня 2 способа вызова API-функций: прямой вызов конкретной функции (
Идём ещё дальше: а как насчёт результатов? Стоит ли возвращать его в регистре (
Что думаете об этом?
Сейчас создам опрос :))
update: Опрос создал в чате (нажмите на комментарий и проголосуйте, пожалуйста, вдумчиво) 😉
В связи с этим задумался вот о чём. Как удобнее передавать параметры: через регистры или через стек? Эволюция calling convention пришла к регистрам (в x86 был стёк, в x64 стали регистры для первых параметров), но хорошо ли это для интр?
С одной стороны, регистры — это скорость, нет лишних обращений к памяти (стеку), да и регистр можно переиспользовать, и если в нём уже есть нужное значение, не нужно делать лишний
push
. С другой — для интры привязка к регистрам означает неудобство, поскольку каждый регистр на счёту, и привязка к регистрам сковывает.Эта мысль влечёт за собой следующую. Так как у меня 2 способа вызова API-функций: прямой вызов конкретной функции (
call dword [ebp+fn]
) и вызов по номеру в ah
(call ebp
), может, и номер функции для второго случая передавать через стек?Идём ещё дальше: а как насчёт результатов? Стоит ли возвращать его в регистре (
edx
)? Может, и результат оставлять в стеке? И далее его можно будет pop
-нуть в нужный регистр, не затирая значение в edx
.Что думаете об этом?
Сейчас создам опрос :))
update: Опрос создал в чате (нажмите на комментарий и проголосуйте, пожалуйста, вдумчиво) 😉
С новым 2️⃣0️⃣2️⃣3️⃣ годом, друзья! 🎄
Пусть в этом году сбудутся те мечты и достигнутся те цели, которые созвучны вашей душе!🔥
Всем успехов в творчестве и в других сферах жизни!❤️
Здоровья, бодрости, энергии и вдохновения!🤩
Ура!🥂 ✨
P.S. У каждого куранты бьют в разные моменты, поэтому отправляю по своему местному времени🙂
Пусть в этом году сбудутся те мечты и достигнутся те цели, которые созвучны вашей душе!
Всем успехов в творчестве и в других сферах жизни!
Здоровья, бодрости, энергии и вдохновения!
Ура!
P.S. У каждого куранты бьют в разные моменты, поэтому отправляю по своему местному времени
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1
Работа идёт, но пока медленно... В целом же, если реализовать буквально 2-3 API-функции (✌️
————————————————————
Я тут иногда подумываю о названии проекта. Изначально была идея назвать его SEXI (что-нибудь типа Startup Environment for eXtra-speed Intros) или EROS (Environment of Runtime for Overclocked Scenes). Мне казалось это интересным, такой fun. Но многие восприняли такие названия с иронией. Не хотелось бы, чтобы люди относились к проекту несерьёзно.
Текущее название — Para\\e/
Хорошее, но какое-то… избитое что ли? Изюминки как будто не хватает. Но руки \e/ мне нравятся🙂
Другие неплохие варианты:
🔸 F!RE (Fast Intro Runtime Environment).
🔸 loom (ткацкий станок). Потоки = треды = нити; генератор нитей = ткацкий станок.
Так себе варианты:
🔹 SPEED (Saucy Parallel Execution Environment for Demos).
🔹 FLIP (Fast Low-level Intro Platform).
🔹 PURE (Parallel Unified Runtime Environment). Чистый код — в смысле нативный, плоский.
Хочется отразить в первую очередь многопоточность (скорость), во вторую — нативность, низкоуровневость. И какую-то изюминку / перчинку добавить. Элемент креативности, озорство (но не клоунство🤡 ).
Давайте подумаем! Подкиньте идей, друзья😉
P.S. Всех с Рождеством!🎄
@INIT_VIDEO
, @DISPLAY_IMAGE
, ну и @PARALLEL_PIXELS
), то можно уже пробовать делать интры и сравнивать скорость с другими платформами ————————————————————
Я тут иногда подумываю о названии проекта. Изначально была идея назвать его SEXI (что-нибудь типа Startup Environment for eXtra-speed Intros) или EROS (Environment of Runtime for Overclocked Scenes). Мне казалось это интересным, такой fun. Но многие восприняли такие названия с иронией. Не хотелось бы, чтобы люди относились к проекту несерьёзно.
Текущее название — Para\\e/
Хорошее, но какое-то… избитое что ли? Изюминки как будто не хватает. Но руки \e/ мне нравятся
Другие неплохие варианты:
🔸 F!RE (Fast Intro Runtime Environment).
🔸 loom (ткацкий станок). Потоки = треды = нити; генератор нитей = ткацкий станок.
Так себе варианты:
🔹 SPEED (Saucy Parallel Execution Environment for Demos).
🔹 FLIP (Fast Low-level Intro Platform).
🔹 PURE (Parallel Unified Runtime Environment). Чистый код — в смысле нативный, плоский.
Хочется отразить в первую очередь многопоточность (скорость), во вторую — нативность, низкоуровневость. И какую-то изюминку / перчинку добавить. Элемент креативности, озорство (но не клоунство
Давайте подумаем! Подкиньте идей, друзья
P.S. Всех с Рождеством!
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1
Итак, друзья! Первые видимые результаты: я реализовал функции
Про скорость пока могу сказать только следующее: вывод xor-шаблона в 480х270 с масштабированием на FullHD даёт у меня порядка 1500 fps. Вывод в 1920x1080 — в 2 раза медленнее.
Многопоточность ещё не задействовал.
@InitVideo
и @DisplayFrame
. Всё работает, картинка выводится 😁Про скорость пока могу сказать только следующее: вывод xor-шаблона в 480х270 с масштабированием на FullHD даёт у меня порядка 1500 fps. Вывод в 1920x1080 — в 2 раза медленнее.
Многопоточность ещё не задействовал.
👍2🎉2
Сгенерировал палитру по умолчанию для 8-битных режимов. Провозился целых 3 дня, но сделал вариант, который, вполне нравится.
На всякий случай опишу 😁
1️⃣ Первый ряд — ч/б градиент от чистого чёрного до чистого белого.
2️⃣ Далее 12 цветных рядов от самого тёмного до почти белого. Чистый цвет на 11-й позиции каждой строки (считая с 0). Соответственно, слева 11 затемнённых, справа 4 осветлённых цвета. Палитра начинается с голубого как более близкого к белому.
3️⃣ Следующий ряд — 12 радужных цветов со значительной примесью серого (более мягкие, не такие кислотные, как те, что выше) + 4 цвета с лёгкой примесью серого.
4️⃣ Последние 2 ряда — цвета палитры SWEETIE16, расширенные до 32 штук интерполяцией (могут использоваться циклически).
Все цвета сначала генерируются линейно, после чего производится гамма-коррекция с коэффициентом 1/1.2 (т.е. все значения во float в диапазоне [0; 1] возводятся в степень 1.2). Число выбрано эмпирически как более-менее оптимальное: меньшее значение (1.0) — слишком много ярких тонов, большее (1.333) — слишком много тёмных. Понятно, что многое зависит от настроек монитора и индивидуальных особенностей восприятия, но я решил сделать так.
В целом я доволен. Немного смущает вырвиглазность цветов (за исключением 3-х нижних рядов). Но потом я планирую сделать альтернативную (может, даже не одну) палитру с более мягкими цветами. Либо взяв за основу эту, либо просто большой градиент от чёрного в белый, переходящий в радугу, и в конце снова уходящий в чёрный.
К тому же, я собираюсь сделать функции генерации палитры :)
На всякий случай опишу 😁
1️⃣ Первый ряд — ч/б градиент от чистого чёрного до чистого белого.
2️⃣ Далее 12 цветных рядов от самого тёмного до почти белого. Чистый цвет на 11-й позиции каждой строки (считая с 0). Соответственно, слева 11 затемнённых, справа 4 осветлённых цвета. Палитра начинается с голубого как более близкого к белому.
3️⃣ Следующий ряд — 12 радужных цветов со значительной примесью серого (более мягкие, не такие кислотные, как те, что выше) + 4 цвета с лёгкой примесью серого.
4️⃣ Последние 2 ряда — цвета палитры SWEETIE16, расширенные до 32 штук интерполяцией (могут использоваться циклически).
Все цвета сначала генерируются линейно, после чего производится гамма-коррекция с коэффициентом 1/1.2 (т.е. все значения во float в диапазоне [0; 1] возводятся в степень 1.2). Число выбрано эмпирически как более-менее оптимальное: меньшее значение (1.0) — слишком много ярких тонов, большее (1.333) — слишком много тёмных. Понятно, что многое зависит от настроек монитора и индивидуальных особенностей восприятия, но я решил сделать так.
В целом я доволен. Немного смущает вырвиглазность цветов (за исключением 3-х нижних рядов). Но потом я планирую сделать альтернативную (может, даже не одну) палитру с более мягкими цветами. Либо взяв за основу эту, либо просто большой градиент от чёрного в белый, переходящий в радугу, и в конце снова уходящий в чёрный.
К тому же, я собираюсь сделать функции генерации палитры :)
Кстати, этот рисунок выводит 20-байтовая программа вот с таким исходником:
Кстати, с прошлого поста я исправил несколько страшных багов, сейчас всё работает чётко😎
Так что, теперь можно перейти к функциям работы с многопоточностью.
include 'parallel_fasm_sdk.inc'
CodeStart!
apicall @InitVideo, (16-1) + (16-1)*ivHeightMult + ivPalette8bpp
@@: stosb
inc al
jnz B
apicall @DisplayFrame, dfNoSync
jmp $
Несмотря на то, что программа заканчивается инструкцией jmp $
, из неё легко выйти двойным нажатием на Esc (можно и одинарным, но выход будет не сразу, а через секунду — это время даётся программе для отмены или корректного завершения). Либо комбинацией "быстро убить интро" Win+Ctrl+X.Кстати, с прошлого поста я исправил несколько страшных багов, сейчас всё работает чётко
Так что, теперь можно перейти к функциям работы с многопоточностью.
Please open Telegram to view this post
VIEW IN TELEGRAM
Друзья! Хорошие новости! 🔥 Я реализовал параллелизм: API для вызова пользовательских функций в многопоточном режиме 🤩 . Т.е. параллельный for, одномерный и двумерный. В частности, API будет вызывать вашу функцию для каждой строки экрана, для группы из нескольких пикселей (например, для 64-х) или для каждого пикселя в отдельности.
Самый медленный вариант (как можно догадаться) — использование попиксельных вызовов, т.к. он связан в большим кол-вом накладных расходов на вычисления и вызов функции. На моём компьютере такой режим позволяет достигать 1537 fps для FullHD (против 1112 fps для однопоточного режима) без учёта вывода на экран. Но это для отрисовки банального XOR-паттерна. Для таких простых операций использовать попиксельный режим неэффективно. Построчный или 64-пиксельный работает гораздо быстрее (до 6521 fps). Однако, если добавить в код холостой цикл на 1000 итераций, разница в попиксельном режиме будет куда существеннее: 32,34 fps в многопоточном коде против 2,21 fps в однопоточном (ускорение в 14,639 раз). Т.е. для сложных алгоритмов (реймаршинг, фракталы) такой режим можно использовать.
Первый алгоритм for, который я сделал 2 дня назад (в таблице он обозначен "первая реализация"), работал ощутимо медленнее, т.к. был сделан совсем по-простому. Сегодня я его оптимизировал в 2.7-100 раз для однопиксельных режимов и до 4.5 раз для 64-пиксельных режимов работы. Но это, конечно, далеко не финальная оптимизация, там есть ещё над чем работать😎
Так что друзья, можно сказать, что идея себя оправдала, параллелизм позволяет добиться существенного ускорения (у меня вышло до 16,7 раз для 20 потоков, из которых реальных ядер 12, остальные 8 — гипертреды)🥳
Больше подробностей о тестровании скорости — в таблице (смотрите верхнюю половину, т.к. он учитывает только вычислительную часть, без вывода на экран, который вносит существенную лепту в скорость работы).
Самый медленный вариант (как можно догадаться) — использование попиксельных вызовов, т.к. он связан в большим кол-вом накладных расходов на вычисления и вызов функции. На моём компьютере такой режим позволяет достигать 1537 fps для FullHD (против 1112 fps для однопоточного режима) без учёта вывода на экран. Но это для отрисовки банального XOR-паттерна. Для таких простых операций использовать попиксельный режим неэффективно. Построчный или 64-пиксельный работает гораздо быстрее (до 6521 fps). Однако, если добавить в код холостой цикл на 1000 итераций, разница в попиксельном режиме будет куда существеннее: 32,34 fps в многопоточном коде против 2,21 fps в однопоточном (ускорение в 14,639 раз). Т.е. для сложных алгоритмов (реймаршинг, фракталы) такой режим можно использовать.
Первый алгоритм for, который я сделал 2 дня назад (в таблице он обозначен "первая реализация"), работал ощутимо медленнее, т.к. был сделан совсем по-простому. Сегодня я его оптимизировал в 2.7-100 раз для однопиксельных режимов и до 4.5 раз для 64-пиксельных режимов работы. Но это, конечно, далеко не финальная оптимизация, там есть ещё над чем работать
Так что друзья, можно сказать, что идея себя оправдала, параллелизм позволяет добиться существенного ускорения (у меня вышло до 16,7 раз для 20 потоков, из которых реальных ядер 12, остальные 8 — гипертреды)
Больше подробностей о тестровании скорости — в таблице (смотрите верхнюю половину, т.к. он учитывает только вычислительную часть, без вывода на экран, который вносит существенную лепту в скорость работы).
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
Ребята, кто может протестить скорость на своих машинах? Я пришлю файлы, нужно будет запустить BAT-ник и подождать несколько минут. А потом прислать мне результат (TXT-шники).
Поскольку многопоточность работает (и как мы видим по тестам, существенно ускоряет код), уже вполне можно писать интры, причём в хайрез и с использованием SIMD 😉
А я продолжаю думать о названии.
1️⃣ Помимо Para\\e/ и...
2️⃣ loom (главное, чтоб не было стойких ассоциаций с Java) появились ещё идеи.
3️⃣ artefact (что-то типа Advanced RunTime Environment For Art Code Threads, хорошо шарящие в инглише поправьте, плиз, если звучит странно).
4️⃣ Идея о F!RE трансформировалась в firebird (тем более, что я хочу называть версии видами птиц). Перевод с элементами фана: Fast Intro Runtime Environment (Brilliant Idea of Russian Developer)😄 . Смущают только возможные ассоциации с БД и, не дай бог, торпедой или ещё чем-то.
5️⃣ Ну и ещё интересное, на мой взгляд, имя — Para||ex (Parallel Environment of eXecution(?)). На Parallax аббревиатуру не придумал, но Parallex более оригинальное название.
6️⃣ Банальное: remi (Runtime Environment for Multithreaded Intros).
Что думаете? Об этих названиях. Ну и может, тоже идей подкинете?🤔
А я продолжаю думать о названии.
1️⃣ Помимо Para\\e/ и...
2️⃣ loom (главное, чтоб не было стойких ассоциаций с Java) появились ещё идеи.
3️⃣ artefact (что-то типа Advanced RunTime Environment For Art Code Threads, хорошо шарящие в инглише поправьте, плиз, если звучит странно).
4️⃣ Идея о F!RE трансформировалась в firebird (тем более, что я хочу называть версии видами птиц). Перевод с элементами фана: Fast Intro Runtime Environment (Brilliant Idea of Russian Developer)
5️⃣ Ну и ещё интересное, на мой взгляд, имя — Para||ex (Parallel Environment of eXecution(?)). На Parallax аббревиатуру не придумал, но Parallex более оригинальное название.
6️⃣ Банальное: remi (Runtime Environment for Multithreaded Intros).
Что думаете? Об этих названиях. Ну и может, тоже идей подкинете?
Please open Telegram to view this post
VIEW IN TELEGRAM
⏸ Название определено (думаю, все уже заметили), больше мучить этим вас не буду 😃
Ссылки в телеге изменены, но старые тоже работают в режиме переадресации.
⏸ Хотел написать интру для Lovebyte 2023 под Para||elix, но не успел. Жаль, конечно. Но с другой стороны, есть возможность доработать и потом показать более полноценную версию.
⏸ Заметил, что legacy FPU работает пипец как медленно в сравнении со скалярным SSE/AVX (с векторным, думаю, будет такая же картина). По крайней мере, на Alder Lake. На Sandy Bridge замечал разницу, но не настолько большую. Вообще, странно, Instruction Tables (Agner Fog) показывает незначительные различия. В общем, чтобы говорить об этом более предметно, нужно потестить более обстоятельно. И на AMD в том числе.
Плюс вылезли вопросы сохранения FPU и SIMD-регистров (ибо некоторые API типа
⏸ Также задумался о 64-битной версии (на будущее). Раньше, конечно, тоже думал, но эта мысль казалось не столь интересной. По сути, интры получаются не шибко больше в размере, зато кол-во регистров удваивается (как общего назначения, так и SIMD). Но сложнее организовать API-вызовы по похожей схеме (т.е.
⏸ Исправил баг: при использовании многопоточных функций интра висла через несколько секунд или минут. Причина была неочевидна, но чисто интуитивно решил положить
⏸ При старте интры, а также при вызове функций потоков (к примеру, для каждого пикселя) в регистр
Но, наверное, я расширю эту область до 64КБ (18МБ для 256+32 потоков — не так уж и много). Может, урежу стек (для каждого потока) до 512КБ (вместо дефолтного 1МБ) или даже до 256КБ (к чему такой большой?)🤔
⏸ Поменял значение регистра😀
Объясняю суть. При таком значении
✅ Первые 2 варианта (1x, 2x) можно зарезервировать для API (таким образом, кол-во функций увеличивается с 255 до 511 — с учётом возможных расширений, пусть будет).
✅ Следующий (3x) — для сервисных данных (инфа о процессоре, событиях (юзер нажал Esc), опциях, настройках видео и аудио, коэффициенты для приведения координат к диапазону 0..1, FP-координаты центра и т.д.). Причём, если раньше под них выделялось 128 байт, то сейчас можно 256 (128 оказалось мало).
✅ Ещё один (5x) — для хранения констант (0, 0.5, 1, π, 2π, e, √2 и т.д., включая векторные по 16 float'ов для SIMD вплоть до AVX-512).
✅ И последний
Ссылки в телеге изменены, но старые тоже работают в режиме переадресации.
⏸ Хотел написать интру для Lovebyte 2023 под Para||elix, но не успел. Жаль, конечно. Но с другой стороны, есть возможность доработать и потом показать более полноценную версию.
⏸ Заметил, что legacy FPU работает пипец как медленно в сравнении со скалярным SSE/AVX (с векторным, думаю, будет такая же картина). По крайней мере, на Alder Lake. На Sandy Bridge замечал разницу, но не настолько большую. Вообще, странно, Instruction Tables (Agner Fog) показывает незначительные различия. В общем, чтобы говорить об этом более предметно, нужно потестить более обстоятельно. И на AMD в том числе.
Плюс вылезли вопросы сохранения FPU и SIMD-регистров (ибо некоторые API типа
@GetDurationFloat
— получение времени с момента старта, могут их портить).⏸ Также задумался о 64-битной версии (на будущее). Раньше, конечно, тоже думал, но эта мысль казалось не столь интересной. По сути, интры получаются не шибко больше в размере, зато кол-во регистров удваивается (как общего назначения, так и SIMD). Но сложнее организовать API-вызовы по похожей схеме (т.е.
call [ebp+fn]
, где fn — это любое число, сделать уже не получится). В общем, поживём — увидим. До этого ещё далеко.⏸ Исправил баг: при использовании многопоточных функций интра висла через несколько секунд или минут. Причина была неочевидна, но чисто интуитивно решил положить
notify_all
внутрь залоченного мьютекса (раньше он был снаружи) — баг пропал :)⏸ При старте интры, а также при вызове функций потоков (к примеру, для каждого пикселя) в регистр
ebx
теперь записывается выровненный по границе 256 байт адрес 4КБ буфера в TLS, т.е. потоко-локальной области. Это оказалось офигенно удобно. Т.е. вы можете хранить локальные переменные не только в стеке, но и в TLS, используя адресацию вида [ebx+n]
. Потокобезопасно! Скажу по секрету, что можно адресовать не только в плюс, но и в минус (до 128 байт), т.е. у вас есть не 4КБ, а даже 4096+128 байт :)Но, наверное, я расширю эту область до 64КБ (18МБ для 256+32 потоков — не так уж и много). Может, урежу стек (для каждого потока) до 512КБ (вместо дефолтного 1МБ) или даже до 256КБ (к чему такой большой?)
⏸ Поменял значение регистра
ebp
при старте интры. На нём завязана таблица API, а также зона сервисных данных. Раньше было ebp = 0x08000000
(напомню, что код грузится по адресу 0x10000000
(ebp*2
), фрейм буфер — 0x40000000
(ebp*8
)). Сейчас сделал ebp = 0x01C71C80
. Это же гораздо лучше, согласны? Объясняю суть. При таком значении
ebp
можно использовать не только [ebp]
, но и [ebp+ebp]
, [ebp+ebp*2]
, [ebp+ebp*4]
, и [ebp+ebp*8]
, т.е. целых 5 вариантов почти за ту же цену (но первый всё же дешевле на байт) :)[ebp+ebp*8]
(9x) — это адресация 0x10000080
(теперь понятно почему 0x01C71C80
?) Т.е. добавляя байтовую константу (-128..127) можно иметь доступ к первым 256 байтам кода (а сделав, скажем, lea edx,[ebp+ebp*8+127]
— к смещениям 127..382 через edx
). Тоже удобно.Please open Telegram to view this post
VIEW IN TELEGRAM
🟰 При внесении этих изменений (
Если будут проблемы, поменяю😏
С одной стороны, мне не хотелось выносить всё в DLL, но потом я решил, что это даже хорошо. Почему? Об этом в следующем абзаце.
⏸ Пытаясь сварганить интру, я подумал, что было бы здорово иметь возможность написать прототип на C/C++, заодно и потестить финальную скорость. И тут я понял, что вынос основного кода в DLL — это то, что нужно. Сделать прототип можно двумя способами:
🅰️ Создав DLL с экспортируемой функцией типа
🅱️ Создав EXE, который будет вызывать функции DLL среды (в первом варианте это тоже будет, конечно, только здесь нужно будет дополнительно вызывать функцию инициализации среды, передавая параметры командной строки).
За это я ещё пока не брался, конечно же.
⏸ Сделал возможность указывать только ширину экрана с автоматическим подбором высоты, исходя из соотношения сторон текущего видеорежима. Также сделал возможность устанавливать режим экрана по умолчанию (это либо режим, установленный при запуске, либо заданный параметром командной строки). А также половину разрешения этого режима, треть и четверть по каждой из сторон. Причём, при использовании этой возможности зачастую можно сэкономить 3 байта кода.
⏸ Ещё я решил, что нужно сделать API-функцию
🟰 Наверняка, я сделал что-то ещё, но не столь существенное, поэтому забыл написать об этом.
Но самый прикол в том, что файл TODO растёт быстрее, чем я успеваю что-то делать (а посвящать проекту слишком много времени я тоже не могу).
❓ Ещё один вопрос меня терзает. Это неиспользуемые параметры. Сейчас некоторые функции могут принимать переменное число параметров (2-3 или от 1 до 4-х, например). Кол-во параметров определяется отдельными битами первого. Так вот, вся проблема в том, что в конечном счёте любой API-вызов сводится к вызову функции C++, где кол-во параметров не может быть переменным (не надо про variadic — там свои заморочки). Поэтому промежуточный код (который в случае фиксированного кол-во параметров сводится к
В случае сложных схем (где число опциональных параметров 3 и более, либо где параметр зависит от значения нескольких битов), код получается довольно громоздким (не один десяток инструкций). Да, ещё не нужно забывать, что выходов из таких функций должно быть тоже несколько, например,
На ум приходит только следующее: использовать для опциональных параметров регистры или TLS. Первый вариант расходует драгоценные регистры. Второй требует сохранять🧐
ebp=0x01C71C80
) я столкнулся с проблемой. Зарезервировать такие малые адреса не удавалось, т.к. они были уже заняты после инициализации DLL-ок типа USER32, MSVCRT и т.п. Пришлось выносить весь основной код в DLL и делать отдельно EXE-загрузчик, который резервирует нужные адреса и потом уже загружает основной код (динамически — LoadLibrary
). Это сработало. Но требует тестирования на разных версиях винды.Если будут проблемы, поменяю
ebp
на 0x03330000
(2x = 0x06660000, 3x = 0x09990000, 5x = 0x0FFF0000), забив на фичу с [ebp+ebp*8]
С одной стороны, мне не хотелось выносить всё в DLL, но потом я решил, что это даже хорошо. Почему? Об этом в следующем абзаце.
⏸ Пытаясь сварганить интру, я подумал, что было бы здорово иметь возможность написать прототип на C/C++, заодно и потестить финальную скорость. И тут я понял, что вынос основного кода в DLL — это то, что нужно. Сделать прототип можно двумя способами:
🅰️ Создав DLL с экспортируемой функцией типа
ParallelixEntry
, передавая такую DLL в качестве параметра при запуске среды (вместо plx-файла — да, это расширение файлов с интрами, запоминайте).🅱️ Создав EXE, который будет вызывать функции DLL среды (в первом варианте это тоже будет, конечно, только здесь нужно будет дополнительно вызывать функцию инициализации среды, передавая параметры командной строки).
За это я ещё пока не брался, конечно же.
⏸ Сделал возможность указывать только ширину экрана с автоматическим подбором высоты, исходя из соотношения сторон текущего видеорежима. Также сделал возможность устанавливать режим экрана по умолчанию (это либо режим, установленный при запуске, либо заданный параметром командной строки). А также половину разрешения этого режима, треть и четверть по каждой из сторон. Причём, при использовании этой возможности зачастую можно сэкономить 3 байта кода.
⏸ Ещё я решил, что нужно сделать API-функцию
@InitVideoOrDisplayFrame
, которая будет при первом вызове устанавливать видеорежим, а при повторном — выводить изображение. Экономия на лишнем вызове (@InitVideo
и @DisplayFrame
тоже останутся, не переживайте).🟰 Наверняка, я сделал что-то ещё, но не столь существенное, поэтому забыл написать об этом.
Но самый прикол в том, что файл TODO растёт быстрее, чем я успеваю что-то делать (а посвящать проекту слишком много времени я тоже не могу).
jmp
) проверяет биты, записывает в стек либо реальный параметр, либо пустышку, а затем делает вызов stdcall-функции C++ (с уже фиксированным числом параметров).В случае сложных схем (где число опциональных параметров 3 и более, либо где параметр зависит от значения нескольких битов), код получается довольно громоздким (не один десяток инструкций). Да, ещё не нужно забывать, что выходов из таких функций должно быть тоже несколько, например,
ret 4
, ret 8
, ret 12
, ret 16
:). Мне это не очень нравится. Даже не из-за того, что это замедляет вызов (всё-таки основной код таких функций несоизмеримо более громоздкий), а из-за какой-то костыльности.На ум приходит только следующее: использовать для опциональных параметров регистры или TLS. Первый вариант расходует драгоценные регистры. Второй требует сохранять
ebx
для адресации области памяти TLS (ну и запись в память требует больше байтов, чем простой push
). Короче, варианты ещё хуже. Подкиньте идей :). Мне всё-таки хочется оставить вариант stdcall
, но придумать какую-то магию чтобы передать вызов функциям C++ с меньшими усилиями Please open Telegram to view this post
VIEW IN TELEGRAM
Добрался до кода. Сделал рефакторинг. Раньше среда была в классе с одним экземпляром (Синглтон, по сути) в глобальной переменной. Но смысла в этом не было, т.к. это лишний указатель в некоторых случаях, а два экземпляра никогда не будет (две интры не запустишь в одном адресном пространстве). Плюс это создавало неудобства в плане доступа к приватным членам.
API тоже был завернут в класс со статическими функциями, но это тоже так себе идея.
В итоге перенёс и то, и другое в отдельные namespace'ы (с вложенными namespace'ами вроде internal и пр.). Для API сделал отдельно функции для экспорта в DLL, отдельно для вызова из интр. И отдельно с основным кодом (которые вызываются из тех двух). Всё с одинаковыми именами, только namespace'ы разные. Так удобнее, ИМХО 😁
Попробовал загрузить DLL'ку извне на Си, плюсах, Delphi и из асма. Всё работает✌️
Также добавил ещё одну DLL'ку: parallelix_memory для резервирования адресов, если вдруг кто-то захочет сделать обёртку (свой лоадер, так скажем). Х/з зачем, но пусть будет. Заодно потестил кое-что :)
API тоже был завернут в класс со статическими функциями, но это тоже так себе идея.
В итоге перенёс и то, и другое в отдельные namespace'ы (с вложенными namespace'ами вроде internal и пр.). Для API сделал отдельно функции для экспорта в DLL, отдельно для вызова из интр. И отдельно с основным кодом (которые вызываются из тех двух). Всё с одинаковыми именами, только namespace'ы разные. Так удобнее, ИМХО 😁
Попробовал загрузить DLL'ку извне на Си, плюсах, Delphi и из асма. Всё работает
Также добавил ещё одну DLL'ку: parallelix_memory для резервирования адресов, если вдруг кто-то захочет сделать обёртку (свой лоадер, так скажем). Х/з зачем, но пусть будет. Заодно потестил кое-что :)
Please open Telegram to view this post
VIEW IN TELEGRAM
Прототипирование на ЯВУ (языках высокого уровня) работает! 🙂 💪
Правда, ещё не сделана поддержка многопоточных функций (там ABI вызова немного другой будет), но это задача несложная — просто добавить несколько альтернативных веток в уже существующие функции.
Короче говоря, вот такой код уже корректно отрабатывает и выдаёт ту же картинку, что и асмовский.
При этом, не знаю в чём прикол, но прототип выдаёт немного больший fps, чем интра на асме😂
Примерно 720 vs 640.
P.S.
Ну и в целом над заголовочниками нужно ещё покумекать...
Правда, ещё не сделана поддержка многопоточных функций (там ABI вызова немного другой будет), но это задача несложная — просто добавить несколько альтернативных веток в уже существующие функции.
Короче говоря, вот такой код уже корректно отрабатывает и выдаёт ту же картинку, что и асмовский.
[ вырезано, см. ниже ]
Код использует DLL-ку платформы (функции ParallelixMainExW
, InitVideo
, DisplayFrame
).При этом, не знаю в чём прикол, но прототип выдаёт немного больший fps, чем интра на асме
Примерно 720 vs 640.
P.S.
InitVideoOptions::ColorMode::palette_8bpp
, конечно, выглядит немного страшновато, но я подумаю над этим. Может, сделаю макросы, чтобы можно было писать в духе ivColorMode(palette_8bpp)
, ну или алиасы типа using ivColorMode = api::InitVideoOptions::ColorMode;
(чтобы писать ivColorMode::palette_8bpp
).Ну и в целом над заголовочниками нужно ещё покумекать...
Please open Telegram to view this post
VIEW IN TELEGRAM
Нет, с картинкой смотреть на код невозможно (слишком узко) :)
#include "config.h"
#include "api_impl.h"
extern "C" int __stdcall ParallelixMainExW(void*, const wchar_t*);
void __stdcall intro(ServiceData* service_data)
{
using namespace api;
exported::InitVideo({ InitVideoOptions::ColorMode::palette_8bpp, VideoMode::default_full, 0,
InitVideoOptions::use_video_mode, InitVideoOptions::LineAlignment::align_default });
for (int i = 0; ; ++i) {
uint8_t* pixel = service_data->frame_address.pixel8bpp;
for (int y = 0; y <= service_data->current_mode.size.max_y; ++y) {
for (int x = 0; x <= service_data->current_mode.size.max_x; ++x) {
*pixel++ = static_cast<uint8_t>(((x ^ y) + i) >> 4);
}
}
exported::DisplayFrame({ true });
}
}
int main()
{
ParallelixMainExW(intro, L". -kc -fr");
}
Многопоточный код в прототипах теперь тоже работает 🔥
Прототип немного быстрее.
По линиям (1250 vs 1200 fps):
По 1 пикселю (800 vs 780 fps):
По 64 пикселя (1180 vs 1125 fps):
Прототип немного быстрее.
По линиям (1250 vs 1200 fps):
#include "config.h"
#include "api_impl.h"
extern "C" int __stdcall ParallelixMainExW(void*, const wchar_t*);
void __stdcall draw_pixel(uint32_t thread_index, uint32_t y, uint32_t x, uint32_t group_size, PixelPointer pixel_addr, uint32_t spec_value, ServiceData* service_data)
{
for (uint32_t limit = x + group_size; x < limit; ++x) {
*pixel_addr.pixel8bpp++ = static_cast<uint8_t>(((x ^ y) + spec_value) >> 4);
}
}
void __stdcall intro(ServiceData* service_data)
{
using namespace api;
exported::InitVideo({ InitVideoOptions::ColorMode::palette_8bpp, VideoMode::default_full, 0,
InitVideoOptions::use_video_mode, InitVideoOptions::LineAlignment::align_default });
for (int i = 0; ; ++i) {
// 15 = group by lines
exported::ParallelPixels({ 15, ParallelPixelsOptions::ImageSizeType::screen_resolution, true }, draw_pixel, {}, i);
exported::DisplayFrame({ true });
}
}
int main()
{
ParallelixMainExW(intro, L". -kc -fr");
}
По 1 пикселю (800 vs 780 fps):
#include "config.h"
#include "api_impl.h"
extern "C" int __stdcall ParallelixMainExW(void*, const wchar_t*);
void __stdcall draw_pixel(uint32_t thread_index, uint32_t y, uint32_t x, uint32_t group_size, PixelPointer pixel_addr, uint32_t spec_value, ServiceData* service_data)
{
*pixel_addr.pixel8bpp = static_cast<uint8_t>(((x ^ y) + spec_value) >> 4);
}
void __stdcall intro(ServiceData* service_data)
{
using namespace api;
exported::InitVideo({ InitVideoOptions::ColorMode::palette_8bpp, VideoMode::default_full, 0,
InitVideoOptions::use_video_mode, InitVideoOptions::LineAlignment::align_default });
for (int i = 0; ; ++i) {
// 0 = group by 1 pixel (0 << 1)
exported::ParallelPixels({ 0, ParallelPixelsOptions::ImageSizeType::screen_resolution, true }, draw_pixel, {}, i);
exported::DisplayFrame({ true });
}
}
int main()
{
ParallelixMainExW(intro, L". -kc -fr");
}
По 64 пикселя (1180 vs 1125 fps):
#include "config.h"
#include "api_impl.h"
extern "C" int __stdcall ParallelixMainExW(void*, const wchar_t*);
void __stdcall draw_pixel(uint32_t thread_index, uint32_t y, uint32_t x, uint32_t group_size, PixelPointer pixel_addr, uint32_t spec_value, ServiceData* service_data)
{
for (uint32_t limit = x + group_size; x < limit; ++x) {
*pixel_addr.pixel8bpp++ = static_cast<uint8_t>(((x ^ y) + spec_value) >> 4);
}
}
void __stdcall intro(ServiceData* service_data)
{
using namespace api;
exported::InitVideo({ InitVideoOptions::ColorMode::palette_8bpp, VideoMode::default_full, 0,
InitVideoOptions::use_video_mode, InitVideoOptions::LineAlignment::align_64b });
for (int i = 0; ; ++i) {
// 6 = group by 64 pixels (1 << 6)
exported::ParallelPixels({ 6, ParallelPixelsOptions::ImageSizeType::screen_resolution, true }, draw_pixel, {}, i);
exported::DisplayFrame({ true });
}
}
int main()
{
ParallelixMainExW(intro, L". -kc -fr");
}
Please open Telegram to view this post
VIEW IN TELEGRAM
Теперь можно загружать вот такие DLL-ки через командную строку (компилится в 2.5 КБ с ☺️
P.S. Функция
/NODEFAULTLIB
) #include "config.h"
#include "api_impl.h"
extern "C" BOOL APIENTRY _DllMainCRTStartup(HMODULE hModule, DWORD fdwReason, LPVOID lpvReserved)
{
return TRUE;
}
/*
DLLEXPORT BOOL __stdcall ParallelixCheck(ServiceData* service_data)
{
return TRUE;
}
*/
void __stdcall draw_pixel(uint32_t thread_index, uint32_t y, uint32_t x, uint32_t group_size, PixelPointer pixel_addr, uint32_t spec_value, ServiceData* service_data)
{
*pixel_addr.pixel8bpp = static_cast<uint8_t>(((x ^ y) + spec_value) >> 4);
}
DLLEXPORT void __stdcall ParallelixStart(ServiceData* service_data)
{
using namespace api;
exported::InitVideo({ InitVideoOptions::ColorMode::palette_8bpp, VideoMode::default_full, 0,
InitVideoOptions::use_video_mode, InitVideoOptions::LineAlignment::align_default });
for (int i = 0; ; ++i) {
// 0 = group by 1 pixel (1 << 0)
exported::ParallelPixels({ 0, ParallelPixelsOptions::ImageSizeType::screen_resolution, true }, draw_pixel, {}, i);
exported::DisplayFrame({ true });
}
}
P.S. Функция
ParallelixCheck
специально закомменчена, т.к. является необязательной, и при её отсутствии считается, что всё ок.Please open Telegram to view this post
VIEW IN TELEGRAM
Мысли вслух.
🔹 Хочу перевести код под компилятор Intel и сравнить производительность с Visual C++.
🔹 По части распараллеливания циклов думаю подрубить Intel TBB (давно уже хочу изучить его). И также сравнить с моей реализацией🙂
Второе пока не горит, есть более приоритетные задачи.
А вот первое уже можно сделать, чтоб потом пришлось меньше исправлять код.
🔹 Хочу перевести код под компилятор Intel и сравнить производительность с Visual C++.
🔹 По части распараллеливания циклов думаю подрубить Intel TBB (давно уже хочу изучить его). И также сравнить с моей реализацией
Второе пока не горит, есть более приоритетные задачи.
А вот первое уже можно сделать, чтоб потом пришлось меньше исправлять код.
Please open Telegram to view this post
VIEW IN TELEGRAM