Vange-rs: взгляд на реализацию WebAssembly в Rust

в 11:15, , рубрики: 3d, Rust, webassembly, WebGL, вангеры, игра, рендеринг, технологии

Вангеры - это одна из самых почитаемых и технологичных игр своего времени (1998 год), и она продолжает жить и развиваться. Благодаря сплоченному сообществу игра получила множество усовершенствований: HD, 60 FPS, новые сетевые режимы и много другое. Vange-rs один из интереснейших проектов по Вангерам. Это rust версия игры, основной изюминкой которой является 3D рендер основанный на wgpu.

Трассировка лучей (vange-rs)
Трассировка лучей (vange-rs)

wgpu - кроссплатформенное, безопасное графическое API написанное исключительно на rust. Может использовать Vulkan, Metal, D3D12, D3D11, OpenGL ES3 нативно, а также WebGPU и WebGL в WebAssembly.

Можно сказать, что этот проект показывает всю мощь и возможности библиотеки. Vange-rs содержит исчерпывающее описание технологий применимых для рендеринга миров Вангеров. Даже сама возможность покататься на мехосе в полноценном 3D выглядит очень круто. И вдвойне было бы круто сделать это через браузер.

Тем более это должно быть не сложно, ведь Rust позиционируется как современный язык с поддержкой WebAssembly, однако не все так просто.

Мир безграничных возможностей

Документация Rust обещает нам поддержку аж целых трёх возможных вариантов компиляции в WebAssembly. (Tier 2: rustc гарантированно собирается).

  • wasm32-unknown-emscripten

  • wasm32-unknown-unknown

  • wasm32-unknown-wasi

wasm32 - архитектура, unknown - вендор, emscripten | unknown | wasi - операционная система. Для всех платформ заявлена поддержка стандартной библиотеки (функции работы с файловой системой, временем и пр.).

wasm32-unknown-emscripten

Emscripten, пожалуй, старейший и самый развитый инструмент для компиляции из C/C++ в WebAssembly. Из коробки поддерживаются практически все функции posix, виртуальная файловая система, OpenGL ES3. Формально используя emscripten можно скомпилировать проект в рабочий WebAssembly без каких-либо изменений. По факту это совсем не так.

Чтобы скомпилировать проект с использованием emscripten достаточно передать флаг --target wasm32-unknown-emscripten к вызову cargo.

Rust выполняет компиляцию кода приложения, а на последнем этапе использует emcc для линковки в WebAssembly. К сожалению, в компилятор зашит флаг, вызывающий ошибку в случае обнаружения не реализованных символов (ERROR_ON_UNDEFINED_SYMBOLS=1). Из-за этого даже hello-world проект на данный момент не может быть собран с помощью emscripten.

Конфигурационные флаги из rustc добавляются всегда в конец, поэтому ситуацию невозможно исправить даже через RUSTFLAGS. И если проблему символов ещё можно обойти, добавив соответствующую реализацию в cpp файл (как указано по ссылке), то ASSERTIONS=1 будет с нами всегда. Т.е. собрать проект без отладочного кода средствами rust невозможно!

Emscripten позволяет изменить это поведение через переменную окружения о которой мало кто знает:

EMMAKEN_CFLAGS="-s ERROR_ON_UNDEFINED_SYMBOLS=0 --no-entry -s ASSERTIONS=0" 

Таким образом эту проблему можно решить, но этого будет мало. Большинство библиотек, которые имеют поддержку WebAssembly пытаются взаимодействовать с браузером через библиотеки-обертки, такие как web-sys и stdweb. Они предоставляют API браузера, например, WebGL через систему типов Rust. web-sys основан на wasm-bindgen и поэтому он не совместим с emscripten.

The wasm-bindgen project is designed to target the wasm32-unknown-unknown target in Rust. This target is a "bare bones" target for Rust which emits WebAssembly as output.

Если вы попробуете запустить web-sys проект использует --target wasm32-unknown-emscripten, то произойдет ошибка:

cannot call wasm-bindgen imported functions on non-wasm targets

wasm-bindgen для работы требует изменения полученного wasm файла, а конкретно "функции заглушки" заменяются на вызовы импортированных функций из js, вот что пишет один из авторов о возможной поддержке emscripten:

AFAIK emscripten wants its own JS shims and all that, and having two systems of managing shims won't mix well.

...

wasm-bindgen doesn't support the emscripten wasm target at this time. I haven't ever tested it myself so I don't know how far off we would be from getting it to work, but it's expected that emscripten doesn't work at the moment.

Таким образом wasm-bindgen фактически не совместим с emscripten, значит и все библиотеки основанные на web-sys тоже. На данный момент это вообще все библиотеки. Поскольку stdweb фактически мертв:

  • winit (библиотека для работы с окнами) отказывается от поддержки stdweb

  • rust не генерирует правильный wasm для stdweb, начиная с nightly-2019-11-25. Уже 2 года как.

Я пробовал использовать stdweb в связке с winit, и ошибка rust (ссылка выше) не дает сделать это. А вот ещё почему vange-rs не будет работать с emscripten:

  • wgpu пока не поддерживает emscripten

  • glow пока не поддерживает emscripten

  • ron не компилируется для emscripten из-за ошибки

Экспертное мнение о поддержке emscripten в rust
Экспертное мнение о поддержке emscripten в rust

Я понял одну вещь, rust сообщество пытается использовать emscripten неправильно. Сильное заявление, но это так. Вся соль emscripten в том, что он предоставляет идентичное окружение что и обычная *nix система, и поэтому пытаться использовать его совместно с обертками вроде web-sys или stdweb не нужно.

Например, в библиотеке wgpu для OpenGL ES3 бэкенда на линуксе используются библиотеки khronos-egl и glow. Когда происходит компиляция в WebAssembly khronos-egl отключается и вместо него используются обертки web-sys и glow. Но, для emscripten это делать не нужно, поскольку он эмулирует полноценную систему с OpenGL ES3. Таким образом нужно просто использовать тот же код что и для нативной платформы. Если думать таким образом, то можно легко добавить поддержку emscripten и в wgpu и в glow.

Сделав это, мы наконец-то сможем скомпилировать vange-rs. Пришлось полностью отказаться от библиотеки winit, поскольку мы используем emscripten, то и создавать окно нам нет необходимости. В случае браузера это всего лишь canvas который мы создадим в index.html. Остается передать WebGL контекст в wgpu. Библиотека имеет достаточно высокую степень абстракции чтобы принять в качестве хэндла окна заглушку.

struct WebWindow { 
}   

unsafe impl HasRawWindowHandle for WebWindow { 
    fn raw_window_handle(&self) -> RawWindowHandle { 
        RawWindowHandle::Web(WebHandle::empty()) 
    } 
} 

// ... 

let window = WebWindow {}; 
let surface = unsafe { instance.create_surface(&window) }; 

wgpu будет считать, что окно создано, хотя по факту его нет, есть только контекст WebGL (OpenGL ES3), который вернется через нативный krhonos-egl. Удалив winit мы лишились управления с клавиатуры, поскольку библиотека делала это за нас, но добавить пару обработчиков мы легко сможем вернуть его. Таким образом это действительно работает, хоть и с большими оговорками.

К сожалению, во многих обсуждениях я видел мнение что rust отказывается от wasm32-unknown-emscripten в пользу wasm32-unknown-unknown. Я считаю это мнение не обоснованным, так как у emscripten достаточно много своих плюсов, которые возможно никогда не появятся в unknown, например поддержка asyncify, 3 вида файловых систем на выбор, сетевой стэк и пр.

wasm32-unknown-unknown

Все внимание сообщества сосредоточено на обожаемых web-sys и wasm-bindgen, ну они-то должны работать из коробки? Ммм, нет... Собирается все действительно без особых проблем, передаем флаг --target wasm32-unknown-unknown и запускаем в браузере:

panic: operation not supported on this platform

Функции работы с файловой системы не поддерживаются.

???
???

Очевидно, что игра не сможет запуститься без файлов. Напомню, что в emscripten это решено довольно просто: по умолчанию используется файловая система в памяти, она реализует стандартные функции работы с файлами, в неё можно загрузить образ диска перед запуском. Это вполне логичное решение и, как кажется, в Rust можно было бы применить схожий подход (использование виртуальной файловой системы), тогда такой код работал бы:

    File::create(file).expect(&format!("Unable to create {}", file)) 
        .write(include_bytes!("../file")) 
        .expect(&format!("Unable to write in {}", file)); 

У нас нет файлов в файловой системе, но мы могли бы их создать: загрузить через wasm-bidngen или просто вставив в дата секцию. Однако, это не работает, потому что реализация файловой системы в случае с unknown просто отсутствует. И не только файловой системы, так же отсутствует какая-либо реализация для потоков или времени. Даже env_logger не будет работать из коробки, потому что он пытается получить текущее время, а эта функция не реализована.

Весь драматизм в том, что unknown используется не только на архитектуре wasm32, и нельзя сделать исправление только для wasm32. Т.е. варианта два, либо переписать vange-rs полностью отказавшись от библиотек и кода работающих с файловой системой, либо исправить rustc добавив в него поддержку файловой системы в памяти.

Очевидно что мы выбираем второе. Было не просто, особенно если учесть, что я прочитал только избранные главы из rust book. Но в итоге получилось ведь!

Комичный случай, я несколько дней безуспешно пытался собрать компилятор rust, используя команду:

./x.py build library/std --target wasm32-unknown-unknown

Команда отрабатывала, но использовать это компилятор я не мог, из-за ошибки:

error[E0463]: can't find crate for `core`

Оказалось, что нужно передавать одновременно 2 архитектуры (--target x86_64-unknown-linux-gnu --target wasm32-unknown-unknown):

You need to use --target x86_64-unknown-linux-gnu --target wasm32-unknown-unknown". If you don't include the host in --target you can't build build scripts and proc macros that need to be compiled for the host.

Сделав это, я наконец-то получил рабочий компилятор с виртуальной файловой системой. В моем репозитории можно скачать собранный вариант для x86_64-unknown-linux-gnu. Собрав или скачав компилятор, просто регистрируем его в rustup и применяем для проекта:

rustup toolchain link memfs memfs/stage1 
cd <project>
rustup override set memfs 

А дальше собираем как обычно через сargo build. Вы удивитесь, но winit тоже не способен ловить события клавиатуры из-за ошибки, так что, как и в случае emscripten приходится реализовывать этот функционал самостоятельно, до тех пор, пока не поправят winit.

Решив эти проблемы, мы наконец-то получаем рабочую версию vange-rs для браузера!

wasm32-unknown-wasi

Поскольку wasm-bindgen не поддерживает ничего кроме unknown, то мы имеем все те же проблемы что и у emscripten. Кроме того, wasi предназначен для node.js, а не для браузера. Поэтому экспериментировать с ним я не стал.

Вместо выводов

Вероятно, rust еще слишком молод, и поддержка WebAssembly находится на экспериментальном уровне. Не смотря на большое количество потребовавших усилий оба варианты компиляции (emscripten, unknown) работают! Какой из них проще - решать вам, для unknown придется собрать компилятор, а в случае с emscripten придется отказаться от большинства библиотек. Но результат в обоих случаях вас порадует, ведь нет ничего невозможного.

Огромное спасибо сообществу Вангеров и Дмитрию (@kvark) - автору проекта vange-rs и wgpu, за саму возможность покататься на мехосах в 3D и помощь в исправлении ошибок WebGL.

Ссылки

Автор: Александр

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js