-
Notifications
You must be signed in to change notification settings - Fork 1
Frontend
Der Frontend ist in Javascript und Rust(WebAssembly) geschrieben. Es wird ohne emscripten kompiliert, sondern stattdessen über wasm32-unknown-unknown und anschließend mit wasm-bindgen bearbeitet.
wasm-branch: https://github.com/TrueDoctor/DiscoBot/tree/wasm/webhogg/wasm
Aktuell wird leider nicht Firefox (selbst nicht die nightly versionen) unterstützt, weil dieser keine ausreichend ausgeprägte Unterstützung für die OffscreenCanvas-Technologie bietet. Getestet wird auf der neuesten Version von Chromium (state 22.06.2019: 74.0.3729) und Chrome Beta.
Ihr wollt Ratatosk bei euch lokal bauen und hosten? Kein Problem! (*hüstel*)
Es werden nur folgende Dinge benötigt:
- Rust oder rustup
- Cargo (normalerweise beim Rust-Paket dabei)
- Ein Betriebssystem, das
sh
unterstützt -
wasm-bindgen
, ein Tool zum generieren von Bindings für WebAssembly - das binaryen-Paket (
binaryen-git
im AUR) für wasm-opt - ein webserver (python geht natürlich auch)
Ihr clonet euch das Projekt mit git clone https://github.com/TrueDoctor/DiscoBot
.
Nun geht ihr auf den gewünschten Branch. Im Pfad webhogg/wasm
liegt das webhogg Frontend.
Dort muss nur einmal die Datei build
ausgeführt werden (./build
oder sh build
).
Fertig! Jetzt muss nur noch ein Server gehostet werden.
Am einfachsten geht das über das Python-Modul http.server
, also ruft ihr python3 -m http.server <port>
laufen
und könnt jetzt in eurem Chromium unter http://localhost:<port>
euer Werk bestaunen.
Wichtig ist zurzeit noch, dass man den Offscreen Canvas unter chrome://flags/#enable-experimental-web-platform-features aktiviert.
Prost!
Tja Pech gehabt.
Naja ihr habt immernoch die möglichkeit Jan zu PMmen, aber das ist nicht nötig, wenn ihr folgenden Fehler habt:
it looks like the Rust project used to create this wasm file was linked against
a different version of wasm-bindgen than this binary:
rust wasm file: w.x.yz
this binary: a.b.cd (blablabla)
wichtig ist, dass die von euch installierte Version von wasm-bindgen
die gleiche ist, wie die, die in Cargo.toml
steht.
Das heißt, ihr habt zwei Möglichkeiten: ihr schreibt in die Cargo.toml
eure Version von wasm-bindgen -V
oder ihr up-/downdatet eurer wasm-bindgen auf die Version im Cargo.toml
Die Idee ist, den Speicher zwischen den beiden in den verschiedenen Workern laufenden Webassembly Instanzen zu teilen:
Dies geschieht über die SharedArrayBuffers, an denen wohl unter anderem wegen spectre leider wenig gearbeitet wird, weshalb ihre Funktionalität recht schlecht (ganz besonders im Bereich wasm-threading) dokumentiert ist.
Unser bisheriges Vorgehen ist, im main-thread den Speicher zu erstellen:
let sharedMemory = new WebAssembly.Memory({
initial: 1000, // Speicher ist in 1024-Byte Blöcken angegeben
maximum: 1000,
shared: true
});
dieser wird dann über worker.postMessage(sharedMemory);
an die Worker geschickt. Diese übergieben den sharedMemony an die von wasm-bindgen generierten init-Funktion. Damit die init-Funktion als zweites Argument den sharedMemory nimmt, muss die wasm binary entsprechend gelinkt werden. Das klappt leider bisher noch nicht so ganz… hierfür ist nachwievor help acquired!
Im Disassembly mit shared memory ist vor allem folgende Zeile wichtig:
(import "wbg" "memory" (memory $1 (shared 17 16384)))
Kompilert wird das ganze vereinfacht mit:
cargo build --target wasm32-unknown-unknown
wasm-bindgen --remove-producers-section --remove-name-section
# wichtig ist hier das --enable-threads
wasm-opt --memory-packing --remove-memory --remove-unused-brs --enable-threads -Oz
wobei der Linker (wahrscheinlich wasm-ld
) mit folgenden Argumenten ausgeführt wird (als Hilfe kann man einfach wasm-ld --help
eingeben):
wasm-ld --no-entry
--allow-undefined
--strip-all
--export-dynamic
--import-memory # WICHTIG
--shared-memory # WICHTIG
--max-memory=1073741824 # (2^30) WICHTIG
--threads # NICHT wichtig. Der Linker läuft nur multi-threaded
Die Linkerargumente werden unter .cargo/config
spezifiziert.
Entscheidend ist, dass man nicht code-kiddy-mäßig einfach irgendwelche Internetforen rezitiert (*hust* https://github.com/K0IN/WebassemblySimpleMultithreading/blob/master/compile.bat) und --import-table
als Argument mitangibt, ohne zu wissen, was das eigentlich ist (WebAssembly.Table).
Und YAAAY 🎉 wir haben einen RuntimeError
:
Uncaught (in promise) RuntimeError: unreachable
at wasm-function[67]:445
at wasm-function[28]:15
at Function.__exports.start_graphics (http://localhost:8080/bin/webhogg-wasm.js:28:21)
at http://localhost:8080/pkg/worker.js:11:26
(anonymous) @ wasm-0004bcba-67:195
(anonymous) @ wasm-0004bcba-28:8
__exports.start_graphics @ webhogg-wasm.js:28
(anonymous) @ worker.js:11
Promise.then (async)
onmessage @ worker.js:8
Das ist insofern „besser“, als dass kein Compiler Fehler mehr erscheint…
Jetzt gilt es, den Fehler einzugrenzen.
Nach vielem herumprobieren merkt man, dass Aufrufe nach Javascript, wie z.B. console.log
und nutzen von Strings
ein buggy Verhalten haben. Das macht es natürlich schwer zu debuggen.
Aber es gibt immernoch die möglichkeit, dass exportierte Funktionen Werte zurückgeben.
Also lassen wir die in der Schleife laufenden Funktionen loop_graphics
und loop_logic
Werte zurückgeben, um so eine Interaktion zwischen Nutzerebene und wasm herzustellen.
Hier ein Beispiel, um zu demonstrieren, dass shared memory tatsächlich funktioniert:
#[wasm_bindgen]
pub fn loop_logic(val: usize) -> u32 {
unsafe {
let b: Box<u32> = Box::from_raw(64000 as *mut u32);
let b = Box::leak(b);
*b
}
}
#[wasm_bindgen]
pub fn loop_graphics(val: usize) -> u32 {
unsafe {
let b: Box<u32> = Box::from_raw(64000 as *mut u32);
let b = Box::leak(b);
*b += 1;
*b
}
}
und in der Ausgabe sieht man, dass logic den Wert tatsächlich auslesen kann:
worker.js:25 logic counter: 0
worker.js:18 graphics counter: 1
worker.js:25 logic counter: 1
worker.js:18 graphics counter: 2
worker.js:25 logic counter: 2
worker.js:18 graphics counter: 3
worker.js:25 logic counter: 3
worker.js:18 graphics counter: 4
worker.js:25 logic counter: 4
Wenn man jetzt in den wasm code sieht, dann erkennt man, dass er stark geschrumpft und übersichtlicher geworden ist:
(module
(type $0 (func (param i32)))
(type $1 (func (param i32) (result i32)))
(type $2 (func))
(import "wbg" "memory" (memory $1 (shared 17 16384)))
(import "wbg" "__wbindgen_object_drop_ref" (func $fimport$0 (param i32)))
(export "start_graphics" (func $0))
(export "loop_graphics" (func $1))
(export "start_logic" (func $3))
(export "loop_logic" (func $2))
(func $0 (; 1 ;) (type $0) (param $0 i32)
(i32.store
(i32.const 64000)
(i32.const 0)
)
(if
(i32.ge_u
(local.get $0)
(i32.const 36)
)
(call $fimport$0
(local.get $0)
)
)
)
(func $1 (; 2 ;) (type $1) (param $0 i32) (result i32)
(i32.store
(i32.const 64000)
(local.tee $0
(i32.add
(i32.load
(i32.const 64000)
)
(i32.const 1)
)
)
)
(local.get $0)
)
(func $2 (; 3 ;) (type $1) (param $0 i32) (result i32)
(i32.load
(i32.const 64000)
)
)
(func $3 (; 4 ;) (type $2)
(nop)
)
)
Globale Variablen sind übrigens auch möglich und der Weg, wie wir ganze in Zukunft wohl handeln wollen:
static mut number: u32 = 0;
#[wasm_bindgen]
pub fn loop_logic(val: usize) -> u32 {
unsafe { number }
}
#[wasm_bindgen]
pub fn loop_graphics(val: usize) -> u32 {
unsafe {
number += 1;
number
}
}
Problematisch ist es allerdings im Moment noch, wenn ich Mutex
es nutzen will:
static mut MEMORY: Option<Mutex<SharedMemory>> = None;
pub unsafe fn init_memory() { MEMORY = SharedMemory::default() }
Das gibt aus unerklärlichen Gründen einen RuntimeError: unreachable
.
Vermutlich ist der allocator an dem Schlamassel Schuld, dass so gut wie alles in der Ausführung einen RuntimeError
wirft, aber wer weiß.
Jan hat bereits versucht, den allocator auf wee_alloc
zu wechseln, aber das hat nichts geändert, außer dass die binary 5KB größer wurde…
Jan ist seeehr verwirrt 😕
😢
😭
PLEEEEAAASEEEEE
😢
SOME HELP PLEEEAAASEEE! 😭 😭 😭 😭
Okay... 😥 *phew* durchatmen.
Wenn wir debuggen wollen, können wir leider nicht console.log(&str/String)
nutzen, aber
irgendweshalb console.log(u32)
. Aber immerhin haben wir jetzt irgendeine möglichkeit, zu debuggen.
Schon einfache Dinge, wie Strings geben Fehler: :x:
// gibt einen Fehler!
for i in "hallo welt".to_owned().bytes() {
logger::log_num(i.into());
}
Während folgendes fehlerfrei läuft: :heavy_check_mark:
// läuft!
for i in &[50, 60, 70, 80] {
logger::log_num(*i);
}
Bei dem String-Beispiel ist wohl das .to_owned
Schuld am Absturz, denn wenn wir den .bytes()
Iterator über
einen &str
statt über einen String
laufen lassen, stürzt das Programm nicht mehr ab:
// läuft!
for i in "hallo welt".bytes() {
logger::log_num(i.into());
}
Allerdings kommt der unerwartete Output:
104
0
0
0
0
0
…
Das erste Byte ist richtig, aber der Rest sind einfach Nullen?! Was soll das?
Wollen wir doch mal in den Code von to_owned
sehen:
impl ToOwned for str {
fn to_owned(&self) -> String {
unsafe { String::from_utf8_unchecked(self.as_bytes().to_owned())
}
}
Okay, dann sehen wir mal, wo der Fehler kommt. Bei as_bytes
, bytes.to_owned
oder bei String::from_utf8_unchecked
?
Probieren wir doch mal:
for &i in "hallo welt".as_bytes() {
logger::log_num(i.into());
}
Output:
104
97
108
108
111
32
119
101
108
116
Das ist exakt "hallo welt"
. Hä?!
Wenn wir uns den code von as_bytes
ansehen:
pub fn as_bytes(&self) -> &[u8] {
&self.vec
}
sehen wir, dass das wahrscheinlich auf das gleiche hinauskommt, wie auf einem hardcoded array zu iterieren.
Aber wenigstens haben wir den Fehler auf bytes.to_owned
und String::from_utf8_unchecked
eingeschränkt.
impl<T: Clone> ToOwned for [T] {
#[cfg(not(test))]
fn to_owned(&self) -> Vec<T> {
self.to_vec()
}
}
Also probieren wir doch mal:
for i in "hallo welt".as_bytes().to_owned() {
logger::log_num(i.into());
}
Fehler! ❌
🤖:
Error located!
#[cfg(not(test))]
impl<T> [T] {
#[inline]
pub fn to_vec(&self) -> Vec<T> {
hack::to_vec(self)
}
}
mod hack {
#[inline]
pub fn to_vec<T>(s: &[T]) -> Vec<T> {
let mut vector = Vec::with_capacity(s.len());
vector.extend_from_slice(s);
vector
}
}
use crate::raw_vec::RawVec;
impl<T> Vec<T> {
#[inline]
pub fn with_capacity(capacity: usize) -> Vec<T> {
Vec {
buf: RawVec::with_capacity(capacity),
len: 0,
}
}
}
Global
ist der globale memory allocator
impl<T, A: Alloc> RawVec<T, A> {
fn allocate_in(cap: usize, zeroed: bool, mut a: A) -> Self {
unsafe {
let elem_size = mem::size_of::<T>();
let alloc_size = cap.checked_mul(elem_size).unwrap_or_else(|| capacity_overflow());
alloc_guard(alloc_size).unwrap_or_else(|_| capacity_overflow());
// handles ZSTs and `cap = 0` alike
let ptr = if alloc_size == 0 {
NonNull::<T>::dangling()
} else {
let align = mem::align_of::<T>();
let layout = Layout::from_size_align(alloc_size, align).unwrap();
let result = if zeroed {
a.alloc_zeroed(layout)
} else {
a.alloc(layout)
};
match result {
Ok(ptr) => ptr.cast(),
Err(_) => handle_alloc_error(layout),
}
};
RawVec {
ptr: ptr.into(),
cap,
a,
}
}
}
}
// gilt nur für den globalen Allocator!
impl<T> RawVec<T, Global> {
#[inline]
pub fn with_capacity(cap: usize) -> Self {
RawVec::allocate_in(cap, false, Global)
}
}
Der Fehler war all die Zeit ein optimizer flag --remove-memory
.
Ohne diese "Optimierung" funktionier auch wieder alles.
Den Allocator muss man aber trotzdem überschreiben.
Um das Projekt neu zu strukturieren, schreiben wir den Code neu unter /webhogg/client/
, ist ja schließlich noch nicht viel.
Wir haben jetzt 3 Crates:
In webhogg-wasm-shared
steht jetzt ein Allocator:
impl MutableAllocator {
unsafe fn alloc(&mut self, layout: Layout) -> *mut u8 {
let size = layout_to_size(layout);
let pos = self.pos;
self.pos += size;
pos as *mut u8
}
#[allow(unused_variables)]
unsafe fn dealloc(&mut self, ptr: *mut u8, layout: Layout) {
}
}
Folgendes Programm funktioniert jetzt:
logic/entries.rs
#[wasm_bindgen]
pub fn init() {
unsafe {
crate::ALLOCATOR.reset();
}
unsafe {
let addr = ADDR as *mut u32;
*addr = 0;
}
log("logic entry reached");
}
#[wasm_bindgen]
pub fn frame() {
unsafe {
let addr = ADDR as *mut u32;
log(&format!("num: {}", *addr));
}
}
graphics/entries.rs
#[wasm_bindgen]
pub fn init() {
unsafe {
crate::ALLOCATOR.reset();
}
log("graphics entry reached");
}
#[wasm_bindgen]
pub fn frame() {
unsafe {
let addr = ADDR as *mut u32;
*addr += 1;
}
}
Output
logic entry reached
graphics.js:39 graphics entry reached
logic.js:39 num: 0
logic.js:39 num: 1
logic.js:39 num: 2
logic.js:39 num: 3
Wie kommunizieren wir über den shared memory?
Es soll die Logik der Grafik Informationen übermitteln können.
Wieso können wir nicht einfach einen geteilten Speicherbereich haben, wenn nur eine*r schreibt und eine*r liest?
Geht nicht. Stellt euch folgendes Szenario vor: Der geteilte Speicherbereich hat 2048 Bytes:\
| 0000000000000000 |
Jetzt überschreibt die Logik den Speicherbereich (kann es nur Byte für Byte):
Logik
v
| 0000000000000000 |
Logik
v
| 1110000000000000 |
Nun kommt die Grafik und ließt. Grafik kann aber schneller lesen, als Logik schreiben kann:
Grafik Logik
v v
| 1111100000000000 |
Logik Grafik
v v
| 1111111000000000 |
Jetzt hat die Grafik zur einen Hälfte den alten Status, zur anderen Hälfte aber den neuen Status gelesen. Doof.
- offizielles wasm-bindgen tutorial: https://rustwasm.github.io/docs/wasm-bindgen/
Brainstorming:
Sessions Liste 📃
Letzte Session◀️
Nächste Session▶️
Last Design-Session 👈
Next Design-Session 👉
Dunkle Seite 🌈
Design:
Sound 🎧
Grafikdesign 🤺
Animationen 🎞️
Gamedesign 📝
Programmierung:
Gamelogik ⚙️
Frontend 👾
Backend 🗄️
Spielprotokoll 🧻