Skip to content
This repository has been archived by the owner on Mar 26, 2024. It is now read-only.

Frontend

TrueDoctor edited this page Apr 30, 2020 · 42 revisions

Diese Seite soll NICHT „Jan's Blog“ werden! Mitmachen tut nicht weh!


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.

Entwicklungsort

wasm-branch: https://github.com/TrueDoctor/ratatosk/tree/wasm

Support

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 28.09.2019: 77.0.3865.90-1) und Chrome Beta.

Installation

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, ⚠️ Achtet darauf, dass euer Paketmanager eine möglichst neue Version ausliefert) für wasm-opt
  • ein webserver (python geht natürlich auch)

Ihr kloniert euch das Projekt mit git clone git@github.com:TrueDoctor/ratatosk. Nun geht ihr auf den gewünschten Branch. Im Pfad ratatosk/client liegt das ratatosk 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!

Was, wenn ich einen Buildfehler habe?

Tja Pech gehabt.
Aber ihr habt immernoch die möglichkeit einen der Devs (Dennis, Max, 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

Aktuelle Themen

Implementation von shared memory in threads

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). Ein weiterer noch dümmerer Fehler ist das optimizer flag --remove-memory.

Wir haben 3 Crates:

Leider muss man den Allocator überschreiben, damit Allokieren auch anständig funktioniert. Dafür haben wir eine Zeit lang folgenden maximal speichervernichtenden Ansatz benutzt:

impl Allocator {
    unsafe fn alloc(&mut self, layout: Layout) -> *mut u8 {
        let pos = self.pos;
        self.pos += layout.size();
        pos as *mut u8
    }
    /// Man beachte die `dealloc`-Funktion
    unsafe fn dealloc(&mut self, ptr: *mut u8, layout: Layout) { }
}

Dieser Ansatz ist natürlich einfach nur absolut scheiße (ist ja klar). Deshalb nutzen wir jetzt wee_alloc. Dieser begeht normalerweise zwar den gleichen Fehler wie der Standard-Allocator (und zwar, dass er versucht den Speicher zu resizen, was bei shared memory nicht möglich ist), aber mit dem Feature static_array_backend wird von einem festen Speicher ausgegangen.

Die große Frage

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 zum einen Teil den alten Status, zum anderen Teil aber den neuen Status gelesen. Doof.

Was machen wir dann?

Aktuelle Buffer-Idee (hat sich bisher ganz gut bewährt)

Es gibt zwei Buffer (1 und 2), die jeweils einen Game-State speichern können. Weiterhin gibt es eine Consumer-Flag (C) und eine Producer-Flag (P), welche jeweils die Zustände 0 (kein Buffer), 1 (Buffer 1) oder 2 (Buffer 2) haben können und beide mit 0 initialisiert werden. P zeigt an, in welchem Buffer valide Daten liegen, C zeigt an, aus welchem Buffer gerade gelesen wird. Das System beruht darauf dass immer ein Thread nur liest und der andere nur schreibt.

Wenn der Grafik-Thread lesen will, schaut er sich zunächst P an (wenn P=0 wird nichts gemacht, nach der Initialisierung sollte dieser Fall nicht mehr auftreten), und setzt dann C auf diesen Wert. Nun kann es allerdings sein, dass sich P in dieser Zeit verändert hat, also wird eine while-Schleife ausgeführt, die solange P nach C kopiert bis P sich in dieser Zeit nicht mehr verändert hat. Ist dies der Fall, kann aus Buffer C gelesen werden. Danach wird C wieder auf 0 gesetzt.

let mut x = p;
c = x;
while x != p {
   x = p;
   c = x;
}
read_buffer(x);
c = 0;

Wenn der Logik-Thread schreiben will, wird folgende Tabelle betrachtet:

C P Aktion
0 0 Buffer 1 schreiben, P=1
0 1 Buffer 2 schreiben, P=2
0 2 Buffer 1 schreiben, P=1
1 1 Buffer 2 schreiben, P=2
1 2 P=1, Buffer 2 schreiben, P=2
2 1 P=2, Buffer 1 schreiben, P=1
2 2 Buffer 1 schreiben, P=1

Message queue

review required, WIP

Der Main-Thread und der Logic-Thread sollen kommunizieren. Der Main-Thread bekommt user input und WebSocket-Nachrichten. Diese sollen irendwie auf den Logic-Thread kommen und zwar möglichst ohne viel Latenz. Falls Daten vom Logic-Thread zum Main-Thread geschickt werden müssen, ist das nicht allzuschwer durch einen einfachen Aufruf auf postMessage zu realisieren, das ist halbwegs performant (siehe hierzu MDN Using Web Workers). Effiziente Kommunikation von Main zu Logic ist allerdings nicht mehr über javascript postMessage möglich, weil dafür Logic nicht blockieren darf und zudem eine asynchrone onmessage-Funktion in javascript einen context switch durchführen müsste. Die Folgerung ist also, wir benötigen eine Queue-Implementation auf dem shared memory.

Main thread

Event handling

Event handling consists of handling the WebSocket connections and receiving user inputs. This is achieved by moving the WebSocket implementation to the main thread. To communicate with the logic thread, the main thread parses the game messages and resources and writes them into the shared array buffer.

Timer loop

The main thread uses the setInterval-function to regularly write the current time into the shared array buffer and wake the logic thread up.

Logic thread

The logic thread parses all messages in the message queue (realized through a linked list in the shared array buffer). Afterwards, it calculates the next game tick, writes the updated sprite positions to the double buffer, and queues updates to be sent to the server via the WebSocket connection. Finally, the thread goes to sleep and blocks until it is woken up by the timer function.

Graphics thread

The graphics thread does not need accurate timing information since it just renders sprites with predetermined locations on the screen. The thread basically reads the current game state for the double buffer and renders it. When it has finished, it checks whether there is new data available in the buffer.

Once the graphics thread has checked the double buffer, there are two cases:

  • Case 1: New data available

    • draw everything
    • check the double buffer again
  • Case 2: No new data available

    • use i32.atomic.wait to block until new data is written to the buffer
    • continue with case 1

Erblicke ebenfalls

Navigation

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 🧻

Clone this wiki locally