Para||elix — платформа демосцены
19 subscribers
14 photos
7 links
Parallelix — demoscene platform for multithreaded native intros
Чат: @parallelix_chat
Автор: @jin_x
Download Telegram
Салют, друзья! 💥
Некоторое время подумываю о создании новой платформы для демосцены. До вчерашнего дня я только размышлял об её особенностях, возможностях, продумывал заголовок (опциональный, но с кучей настроек), некоторые технические нюансы (организацию API, структуру памяти). И вот, наконец, вчера открыл Visual Studio, создал проект и написал немного кода 🙂

💻 Ни для кого не секрет, что помимо аппаратных платформ существуют и искусственно созданные: PICO-8, TIC-80, недавно появившаяся MicroW8 и др.
Каждая платформа имеет ограничения: по разрешению экрана, набору цветов, возможностям воспроизведения звука, производительности, наконец. Ограничения — неотъемлемая часть демосцены. Однако даже мощнейший современный компьютер с видеокартой RTX 4090 тоже имеет ограничения, но конечно, другого уровня, нежели ретрокомпьютеры. Вместе с тем, смотреть 64K или 4K intro на шейдерах не менее интересно, чем демо/интро под ZX Spectrum, написанные на чистом ассемблере Z80, или интро под TIC-80 на Lua. Во всём есть свои фишки, своя романтика и свои челленджи.

Поскольку я любитель Newskool-платформ, ассемблера и Sizecoding'а, моя платформа ориентирована на полноцветный hi-res, максимальную скорость и нативный код x86 под Windows. А ещё я люблю многопоточность, и именно она легла с основу новой платформы.
👉 Минутка рекламы в тему: обязательно подписывайтесь на мой канал и чат о параллельном и асинхронном программировании, GPGPU и оптимизации.

Каковы особенности моей платформы?

🔹 Нативный исполняемый код x86. Интро пишется под процессор Intel IA-32 / AMD x86. Никакого байт-кода, никакого Lua, JavaScript, Python или GLSL. Только ассемблер, только хардкор! 😁 И никаких тормозных DOSBox'ов, код запускается внутри процесса среды исполнения под Windows. Помимо всего прочего, вам доступны все прелести современных процессоров: 32-битная адресация, SIMD (вплоть до AVX-512) и прочие наборы инструкций. Возможно, позже будет поддержка 64-битного кода, но я не обещаю.

🔹 Опциональный заголовок. Да, это почти как COM-файл под DOS. Вам не нужно тратить драгоценные байты (десятки и сотни) на ненужную информацию о вашем интро. Заголовок, если он нужен, будет иметь разную длину в зависимости от тех данных, которые вы хотите там разместить. Вы даже можете спрятать заголовок в имя файла. Так что, под новую платформу вполне возможно создавать даже 32 или 64-байтные интро.

🔹 Среда исполнения с унифицированным API. Проблема DOS в том, что на работу вашего intro влияет множество внешних факторов, которые сложно (а иногда и невозможно) контролировать. Начиная от загруженных драйверов и резидентных программ, заканчивая особенностями реализации DOS и BIOS (в т.ч. различные номера видеорежимов VESA на каждой системе, разное состояние Unreal Mode и поддержки SSE). Кому-то наверняка это даже нравится, но лично я считаю это излишним неудобством. Если ваше аппаратное обеспечение не соответствует каким-то требованиям intro, среда исполнения Para\\e/ Para||elix выдаст соответствующее сообщение, в DOS же, скорее всего, система просто зависнет, либо intro будет глючить.
Please open Telegram to view this post
VIEW IN TELEGRAM
1
🔹 Разнообразие видеорежимов, режимов воспроизведения звука. Вы можете установить практически любое разрешение экрана с нужной вам цветностью (даже аппаратно неподдерживаемое — в этом случае картинка будет натягиваться на стандартное разрешение). Генерация звука может осуществляться в отдельной callback-функции и выдавать как семплы в стиле bytebeat, так и использовать MIDI.

🔹 Вам доступно до 512 МБ 1 ГБ оперативной памяти. Кроме того, память программы (код и данные) выровнена по границе 16 МБ, как и видеопамять, которая организована в виде массива идущих друг за другом пикселей (при этом каждая строка может быть дополнительно выровнена при необходимости).

🔹 Вишенка на торте: МНОГОПОТОЧНОСТЬ!!! Да, друзья, вы можете вызывать API-функции среды исполнения, которые в многопоточном режиме будут вызывать вашу callback-функцию отрисовки строк или даже отдельных пикселей (или групп пикселей по 2, 4, 8, 16, 32 или 64 шт для использования SIMD). Вы можете запросить вызов и любых других функций в многопоточном режиме, делать многопоточные циклы и пр. Разумеется, будут и барьеры и прочие средства синхронизации.

🔹 Различные мелочи вроде возможности остановить или перемотать intro, запустить с нужной секунды (удобно для отладки), завершить через нужное время (удобно для показа на пати зацикленных intro), замедлить/ускорить, воспроизвести задом наперёд (разумеется, для использования этих фишек intro должно быть специальным образом организовано). Расширенный функционал среды исполнения включается и отключается в конфиг-файле, который можно распространять вместе с правилами участия в компо (например, на нашем пати запрещено записывать заголовки в имена файлов, вызывать GDI-функции для рисования фигур, использовать более 64 Мб памяти и т.д.).

⚡️ Сейчас пишется самая-самая первая версия. Так сказать, MVP (минимально жизнеспособный продукт). Многого в ней не будет. Но в ней точно будет многопоточность и точно не будет заголовков исполняемых файлов intro. Я буду счастлив, если успею сделать её к Lovebyte Party 2023 (к февралю). К сожалению, дел всегда много, особенно под Новый Год, так что работать над платформой каждый день (и тем более по много часов) я не смогу. Но я буду стараться выделять на работу над платформой хоть какое-то время. И буду безмерно рад, если вы станете следить за ходом работы и поддерживать меня идеями, советами и реакциями 😉
Please open Telegram to view this post
VIEW IN TELEGRAM
Продолжаю думать над организацией памяти.

Примерный Memory Map 32-битного процесса в Windows:
0x00000000 - 0x0000FFFF (64K) — резервная область (область нулевых указателей).
0x00010000 - 0x7FFDFFFF (почти 2 ГБ) — память, доступная процессу, включая DLL-ки в верхней части этой области.
0x7FFE0000 - 0x7FFFFFFF (128K) — системная область вперемешку с юзерской.
0x80000000 - 0xBFFFFFFF / почти 0xFFFFFFFF (1-2 ГБ) — память, доступная юзеру (соответственно в режиме PAE для 32-битной системы и в 64-битной системе), в 32-битной системе без PAE (или с отключенным битом 4G в PE недоступна).

Хочу сделать так, чтобы интра начиналась с фиксированного адреса.
Скорее всего, это будет адрес 0x10000000 (256 МБ от начала), потому что мне нужно от 64 до 80 МБ на API (ниже объясню почему) + память для самой среды запуска + адрес красивый и удобный :))

Если брать резерв доступной интре памяти в 1 ГБ для (включая сам код), то следующий свободный блок начнётся с адреса 0x50000000. Здесь, собственно, может начинаться видеопамять. Даже при 8K-разрешении 8192*8192*32 bpp на это нужно не более 256 МБ, т.е. на 0x60000000 всё закончится. Под DLL-ки оставшегося пространства хватит с лихвой.

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

Теперь расскажу, зачем под API нужно аж 64-80 мегабайт. При старте интры регистр ebp будет указывать на область API-адресов: непосредственно по адресу ebp будет находиться 2-байтный jmp short (или 5-байтный jmp near, а пока думаю как лучше сделать, тут есть некоторые нюансы) на обработчик API, который выполняет функцию по её номеру в регистре ah, т.е. вызов API потребует всего 2 байта (call ebp), не считая загрузки номера в ah. По адресам [ebp-N] (N = -128...0, кроме 2-5 номеров) будут находиться 4-байтные адреса обработчиков API-функций. Чтобы все адреса были корректными, они должны содержать разные (неповторяющиеся) сочетания из 4-х цифр (к примеру, в 8 числах 1,1,1,1,2,1,1,3 у нас 5 валидных адресов: 0x01010101, 0x02010101, 0x01020101, 0x01010201, 0x03010102). Для 128 таких комбинаций диапазон адресов будет около 64 МБ. И такая организация позволит делать вызовы API-функций 3-мя байтами (call dword [ebp-N]) :)

В расширенной версии API с диапазоном в почти 256 адресов (позволяющей создавать соответственно около 256 функций) потребуется 80 МБ, т.к. 4-х цифр уже не хватит, нужно 5. Само собой, тут будет много пустых областей, которые не будут расходовать память. Ну и 1 ГБ + 256 МБ я выделять сразу не буду, достаточно только зарезервировать адресное пространство.
1
Кстати, кто знает, каков диапазон базового адреса загрузки PE-образа при ASLR ?
Если он может вылетать за пределы 0x01000000, думаю, стоит отключить его, чтобы он не порушил всю идею организации памяти 🤔
Сделано резервирование и выделение памяти подо всё необходимое (код и данные, видеопамять, API). А также загрузка кода по адресу 0x10000000 и его запуск. SEH активирован, результат работы которого вы можете видеть в MessageBox'е (после деления на ноль в intro, код которого также прикрепил). Парсинг параметров командной строки допилил ещё вчера.

По сути, осталось сделать самое сложное и важное — API 😁
Но каркас для API (адреса, о которых я писал в прошлый раз и заглушки функций в виде инструкций ret) уже готов.
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
Сейчас у меня большая загрузка разными делами, и времени на проект очень мало.
Но я успел выделить код intro в отдельный поток, сделать подготовку вспомогательных потоков, цикл обработки сообщений и проброс исключений из любого потока в основной поток (вернее, в обработчик сообщений). Также добавил экстренное закрытие intro по горячей клавише (пока без настройки, только Win+Ctrl+X) и ещё несколько опций.

Да, чуть не забыл. Упаковал 64-мегабайтный каркас API в чуть менее 600-байтный код (простым, RLE-подобным алгоритмом) и сделал распаковщик в память. Всё работает. Т.е. прямо сейчас можно запустить код такого вида (fasm):

format binary as 'll'

use32

call ebp
call dword [ebp-128]
call dword [ebp+eax] ; eax при запуске = 0
jmp $


Все API пока содержат по-прежнему только ret, а выйти из jmp $ можно клавишей Win+Ctrl+X.
____________________

Я решил вместе с версиями давать программе кодовые имена: виды птиц 🕊

P.S. Кстати, вы знали, что волнистый попугайчик может развивать скорость до 100-120 км/ч? Прямо как морская чайка. Это примерно в 3 раза быстрее воробья 🦜

А крошечная колибри, летающая обычно со скоростью 48-85 км/ч, во время коротких перелётов может достигать скорости аж до 150 км/ч, делая при этом 50-80 взмахов крыльями в секунду (а во время ухаживания — до 200 взмахов, прямо как пчёлы) 🐝
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Как насчёт текстур?
Почему бы среде исполнения не сгенерировать случайные байты для intro? А шум Перлина? Или даже позволить пользователю (например, организатору пати) загружать свои текстуры (BMP/бинари, да хоть саундтрек!).
Тем более, у нас есть 4 незадействованные области примерно по 15МБ в зоне API (а в расширенном API их будет 5), чего им простаивать? 😁

Но это, скорее всего, будет реализовано позже, не в первой версии. Сейчас это не первостепенное.
Продумываю API.

Пока условно разбил основные функции на несколько групп:
🔸 0…-0x0F — control and miscellaneous functions;
🔸 -0x10…-0x2F — threading functions;
🔸 -0x30…-0x3F — video functions;
🔸 -0x40…-0x4F — sound functions;
🔸 -0x50…-0x5F — time functions;
🔸 -0x60…-0x6F — keyboard/mouse functions;
🔸 -0x70…-0x7F — file functions.

Может, чего-то забыл?

API FUNCTION CALLS

API functions are called by call dword [ebp+fn] (3-byte instruction, where fn is function number) or by call ebp (2-byte instruction) with function number in ah. Value in ebp is set on intro start and it's not recommended to modify it (however, it is not prohibited). If you have modified value of ebp you can use respectively call dword [API_TABLE_BASE+fn] (6-byte instruction) or call API_TABLE_BASE (call API_HANDLER is alias). Parameters are specified in al, edx and ecx registers. Result is returned in edx register or/and carry flag (CF). All other registers and flags are not modified.

For the best comfort use call_api macro with function number/name or ah as argument to call API function via ebp register. Use call_api_direct macro with function number/name or ah in parameter to call API function via API_TABLE_BASE. Function number will be placed in 32-bit register and this register can be specified as macro argument.

Examples:
🔘 call_api @CALL_IN_THREAD will be translated to call dword [ebp-0x10] (3 bytes).
🔘 call_api ah will be translated to call ebp (2 bytes).
🔘 call_api_direct ebx will be translated to call dword [API_TABLE_BASE+ebx] (6 bytes).
🔘 call_api_direct ah will be translated to call API_TABLE_BASE (5 bytes).

CONTROL AND MISCELLANEOUS FUNCTIONS

🔹 0 (@EXIT) — exit intro. Never returns.
🔹 -1 (@ABORT) — abort intro execution with no code. Never returns.
🔹 -2 (@ABORT_EX) — abort intro execution with code (ABORTCODE_*) specified in al. Never returns.
🔹 -4 (@SET_EXCEPTION_MODE) — set exception mode on wrong API usage: (al = 0 — disabled, return error result; 1 — enabled, generate exception). Default is disabled.
Please open Telegram to view this post
VIEW IN TELEGRAM
THREADING FUNCTIONS

🔹 -0x10 (@CALL_IN_THREAD) — call function in a separate thread (edx = function address). Returns CF: 0 — success (edx = thread index), 1 — total number of threads is too large.
🔹 -0x11 (@TERMINATE_THREAD) — terminate thread (edx = thread index or 0 for all threads except main). Returns CF: 0 — success; 1 — thread is not running. Function with edx = 0 should be called only from the main thread (else exception is generated if exception mode is enabled)! It's not recommended to use this function!

🔹 -0x12 (@MULTI_CALL) — call function in many threads and synchronize (edx = function address; al = number of threads (0 — all free threads including current)).
🔹 -0x13 (@MULTI_CALL_NOSYNC) — call function in many threads and continue (edx = function address; al = number of threads (0 — all free threads)). Returns CF: 0 — success; 1 — no free threads (if al = 0 on call) or total number of threads is too large.
🔹 -0x14 (@MULTI_LOOP) — run loop, call function in many threads and synchronize (edx = function address; ecx = number of iterations).
🔹 -0x15 (@MULTI_LOOP_NOSYNC) — run loop, call function in many threads and continue (edx = function address; ecx = number of iterations). Returns CF: 0 — success; 1 — no free threads.
🔹 -0x16 (@MULTI_PIXELS) — call function for every pixel, pixel group or line of video memory in many threads and synchronize (al = number of pixels in group (must be 0 for line or 1, 2, 4, 8, 16, 32, 64 for pixel group); edx = function address). Returns CF: 0 — success; 1 — wrong al value or video memory is not initialized. This function shouldn't be called if video memory is not initialized (video mode is not set; exception is generated if exception mode is enabled)!
🔹 -0x17 (@MULTI_PIXELS_NOSYNC) — call function for every pixel, pixel group or line of video memory in many threads and continue (al = number of pixels in group (must be 0 for line or 1, 2, 4, 8, 16, 32, 64 for pixel group); edx = function address). Returns CF: 0 — success; 1 — no free threads, wrong al value or video memory is not initialized. This function shouldn't be called if video memory is not initialized (video mode is not set; exception is generated if exception mode is enabled)!

🔹 -0x18 (@WAIT_THREAD) — wait for thread function to finish execution (edx = thread index except 0). Returns CF: 0 — success; 1 — thread is not running, time is out or wrong edx value on call.
🔹 -0x19 (@WAIT_THREAD_TIMEOUT) — wait (with timeout) for thread function to finish execution (edx = thread index except 0; ecx = timeout in milliseconds). Returns CF: 0 — success; 1 — thread is not running or wrong edx value on call.
🔹 -0x1A (@WAIT_ALL_THREADS) — wait for all thread functions to finish execution. Returns CF: 0 — success; 1 — no threads are running or called not from the main thread. This function should be called only from the main thread (else exception is generated if exception mode is enabled)!
🔹 -0x1B (@WAIT_ALL_THREADS_TIMEOUT) — wait (with timeout) for all thread functions to finish execution (ecx = timeout in milliseconds). Returns CF: 0 — success; 1 — no threads are running, time is out or called not from the main thread. This function should be called only from the main thread (else exception is generated if exception mode is enabled)!

🔹 -0x1C (@SYNC_THREADS) — synchronize all threads (wait when all other threads call the same function or function @SYNC_THREADS_TIMEOUT).
🔹 -0x1D (@SYNC_THREADS_TIMEOUT) — synchronize all threads with timeout (wait when all other threads call the same function or function @SYNC_THREADS) or time will be out (ecx = timeout in milliseconds). Returns CF: 0 — successfully synchronized; 1 — time is out.
🔹 -0x1E (@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 стали регистры для первых параметров), но хорошо ли это для интр?

С одной стороны, регистры — это скорость, нет лишних обращений к памяти (стеку), да и регистр можно переиспользовать, и если в нём уже есть нужное значение, не нужно делать лишний push. С другой — для интры привязка к регистрам означает неудобство, поскольку каждый регистр на счёту, и привязка к регистрам сковывает.

Эта мысль влечёт за собой следующую. Так как у меня 2 способа вызова API-функций: прямой вызов конкретной функции (call dword [ebp+fn]) и вызов по номеру в ah (call ebp), может, и номер функции для второго случая передавать через стек?

Идём ещё дальше: а как насчёт результатов? Стоит ли возвращать его в регистре (edx)? Может, и результат оставлять в стеке? И далее его можно будет pop-нуть в нужный регистр, не затирая значение в edx.

Что думаете об этом?
Сейчас создам опрос :))

update: Опрос создал в чате (нажмите на комментарий и проголосуйте, пожалуйста, вдумчиво) 😉
С новым 2️⃣0️⃣2️⃣3️⃣ годом, друзья! 🎄

Пусть в этом году сбудутся те мечты и достигнутся те цели, которые созвучны вашей душе! 🔥

Всем успехов в творчестве и в других сферах жизни! ❤️
Здоровья, бодрости, энергии и вдохновения! 🤩
Ура! 🥂

P.S. У каждого куранты бьют в разные моменты, поэтому отправляю по своему местному времени 🙂
Please open Telegram to view this post
VIEW IN TELEGRAM
1
Работа идёт, но пока медленно... В целом же, если реализовать буквально 2-3 API-функции (@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
Итак, друзья! Первые видимые результаты: я реализовал функции @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-х нижних рядов). Но потом я планирую сделать альтернативную (может, даже не одну) палитру с более мягкими цветами. Либо взяв за основу эту, либо просто большой градиент от чёрного в белый, переходящий в радугу, и в конце снова уходящий в чёрный.

К тому же, я собираюсь сделать функции генерации палитры :)
Кстати, этот рисунок выводит 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