How it started
After this talk
@yostane
#TechAtWorldline
Yassine
Benabbas
TechSquad core team
Teacher / member of Android Lille GDG
Retro-gamer
😍 Kotlin, WASM, AI, ...
blog.worldline.tech
@TechAtWorldline
@techatworldline.bsky.social
@worldlinetech
🔟 Portable binary instruction format
🧑🧑🧒🧒 W3C standard
🛠️ Runs everywhere (web, desktop, etc.)
00 61 73 6d 01 00 00 00 01 05 01 60 00 01 7f 03 |.asm.......`....|
02 01 00 07 16 01 12 67 65 74 55 6e 69 76 65 72 |.......getUniver|
73 61 6c 4e 75 6d 62 65 72 00 00 0a 06 01 04 00 |salNumber.......|
41 2a 0b 00 0a 04 6e 61 6d 65 02 03 01 00 00 |A*....name.....|
(module
(func (result i32)
(i32.const 42)
)
(export "getUniversalNumber" (func 0))
)
00 61 73 6d 01 00 00 00 01 05 01 60 00 01 7f 03 |.asm.......`....|
02 01 00 07 16 01 12 67 65 74 55 6e 69 76 65 72 |.......getUniver|
73 61 6c 4e 75 6d 62 65 72 00 00 0a 06 01 04 00 |salNumber.......|
41 2a 0b 00 0a 04 6e 61 6d 65 02 03 01 00 00 |A*....name.....|
wat format
Human readable text format
Browser
Wasm
runtime
JS engine
based on: https://wasmlabs.dev/articles/docker-without-containers/
Source code
...
WASM Compiler / Toolchain
wasm-pack
...
Wasm binary
Glue code
.NET : OSS app framework for C#
Multi-platform and Fullstack
Wasm is a target of .Net :
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount() { currentCount++; }
}
🌍 Web Framework: HTML + CSS + C# (instead of JS / TS)
💻 Supports client-side rendeering (C# is compiled to Wasm)
public partial class MyClass
{
[JSExport]
internal static string Greeting()
{
var text = $"Hello, {GetHRef()}";
Console.WriteLine(text);
return text;
}
[JSImport("window.location.href", "main.js")]
internal static partial string GetHRef();
}
setModuleImports('main.js', {
window: {
location: {
href: () => globalThis.window.location.href
}
}
});
const text = exports.MyClass.Greeting();
console.log(text);
$ sudo dotnet workload install wasm-tools wasm-experimental
$ dotnet new wasmbrowser
$ dotnet run
🎮 Make a game run in platforms other than its original ones
🧑💻 By rewriting the source code for the new platform(s)
❌ Not porting: virtual machine or emulator
MVG YouTube channel
Released in 1993 for DOS
+
Engine
Resources
sf2
wad
http://mrglitchsreviews.blogspot.com/2012/09/doom-console-ports.html
Official Doom ports
25 official licensed ports https://www.thegamer.com/doom-how-many-platforms-ports-consoles
https://www.link-cable.com/top-10-weird-doom-ports/
youtube.com/watch?v=0mCsluv5FXA
id-Software/DOOM
sinshu/managed-doom
yostane/MangedDoom-Blazor
❌
using SFML.Audio;
namespace ManagedDoom.Audio
{
public sealed class SfmlSound : ISound, IDisposable
{
private SoundBuffer[] buffers;
}
}
namespace SFML.Audio
{
public class SoundBuffer
{
public SoundBuffer(short[] samples, int v, uint sampleRate)
{
// TODO: implement
}
internal void Dispose()
{
// TODO: implement
}
}
}
namespace SFML.Audio
{
public class SoundBuffer
{
public short[] samples;
private int v;
public uint sampleRate;
public SoundBuffer(short[] samples, int v, uint sampleRate)
{
this.samples = samples;
this.v = v;
this.sampleRate = sampleRate;
}
public Time Duration { get; internal set; }
}
}
❌ While loop will hang the browser (single threaded)
👉 requestAnimationFrame() is the recommended alternative
while (waitForNextFrame()){
const input = getPlayerInput();
const { frame, audio }
= UpdateGameState(input, WAD);
render(frame);
play(audio);
}
function gameLoop(){
if (canAdvanceFrame()){
const input = getPlayerInput();
const { frame, audio }
= UpdateGameState(input);
render(frame);
play(audio);
}
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
For loop (game loop)
pressed keys (keyup)
release keys (keydown)
['Z', 'Q']
['space']
Frame buffer
Audio buffer
SFML Audio
SFML Video
1 iteration of the Doom Engine
Updates the game state
.NET WASM
~70% OK
.NET WASM
wad
DOOM.wad
sf2
SoundFont.sf2
pressed keys (keyup)
release keys (keydown)
['Z', 'Q']
['space']
wad
DOOM.wad
requestAnimationFrame
Run .Net
Audio buffer
Frame buffer
Canvas
Audio Context
Run JS
Run JS
1 iteration of the Doom Engine
Updates the game state
sf2
SoundFont.sf2
<html>
<head>
<!-- Sets .Net interop and starts the game loop -->
<script type="module" src="./main.js"></script>
</head>
<body>
<canvas id="canvas" width="320" height="200"
style="image-rendering: pixelated" />
</body>
</html>
Gameloop
main.js
import { dotnet } from "./dotnet.js";
const { getAssemblyExports, getConfig } = await dotnet.create();
const exports = await getAssemblyExports(getConfig().mainAssemblyName);
await dotnet.run();
function gameLoop(timestamp) {
if (timestamp - lastFrameTimestamp >= 1000 / 30) {
lastFrameTimestamp = timestamp;
exports.BlazorDoom.MainJS.UpdateGameState(keys);
}
requestAnimationFrame(gameLoop);
}
main.js
public partial class MainJS // this name is required
{
public static void Main()
{
app = new ManagedDoom.DoomApplication();
}
[JSExport] // Can be imported from JS
public static void UpdateGameState(int[] keys)
{ // computes the next frame and sounds
managedDoom.UpdateGameState(keys);
}
}
import { dotnet } from "./dotnet.js";
const { getAssemblyExports, getConfig } = await dotnet.create();
const exports = await getAssemblyExports(getConfig().mainAssemblyName);
await dotnet.run();
function gameLoop(timestamp) {
if (timestamp - lastFrameTimestamp >= 1000 / 30) {
lastFrameTimestamp = timestamp;
exports.BlazorDoom.MainJS.UpdateGameState(keys);
}
requestAnimationFrame(gameLoop);
}
Gameloop
Audio
0.5 | 1 | 0.75 | 0 | -0.75 | -1 | -0.5 | 0 |
---|
https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Audio_concepts
AudioContext
Sample
1 / Sampling frequency
+ Sampling frequency
Sound effects
void PlayCurrentFrameSound(SoundBuffer soundBuffer)
{
int[] samples = Array.ConvertAll(soundBuffer.samples, Convert.ToInt32);
BlazorDoom.Renderer.playSoundOnJS(samples, (int)soundBuffer.sampleRate);
}
namespace BlazorDoom
{
[SupportedOSPlatform("browser")]
public partial class Renderer
{
[JSImport("playSound", "blazorDoom/renderer.js")]
internal static partial string playSoundOnJS(
int[] samples,
int sampleRate
);
}
}
Sound effects
export function playSound(samples, sampleRate) {
audioContext = new AudioContext({
sampleRate: sampleRate,
});
const length = samples.length;
const audioBuffer = audioContext.createBuffer(
1,
length,
sampleRate
);
var channelData = audioBuffer.getChannelData(0);
for (let i = 0; i < length; i++) {
// normalize the sample to be between -1 and 1
channelData[i] = samples[i] / 0xffff;
}
var source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContext.destination);
source.start();
}
Music
Note 0
Note 1
... Note n
Music streaming
sf2
Time
Time
Streaming de la musique
Time
Time
sf2
Glitchs when chaining small audio chunks
Does not handle streaming natively
Group chunks into a big buffer
Schedule buffer playback chaining
Drawback: music start with delay (to fill the first buffer)
Music streaming
Drawback: Music starts with a delay (time to fill first buffer)
function playMusic(samples) {
// Play the audio buffer when it's filled enough
if (this.#currentMusicBufferIndex >= this.#musicBuffer.length) {
const currentTime = this.#audioContext.currentTime;
const duration = this.#currentMusicBufferIndex / this.musicSampleRate;
// Schedule the playback to start after the end of the previous buffer
source.start(this.expectedBufferEndTime, 0, duration);
this.expectedBufferEndTime = currentTime + duration;
}
// Filling the buffer with the current chunk
for (let i = 0; i < samples.length; i++) {
this.#currentChannelData[this.#currentMusicBufferIndex + i]
= samples[i] / 32767;
}
this.#currentMusicBufferIndex += samples.length;
}
0 | 1 | 2 | 3 | 1 | 1 | 2 | 3 |
---|
0 | 1 | 2 | 3 |
---|
Frame rendering
2 | 2 | 0 | 1 |
---|
Image
Color palette
Canvas
Each item (pixel) contains its color index
How will the image be rendered?
source: raycasting by MeTH
1- Send rays evenly spread across the field of view
source: raycasting by MeTH
2 - Each ray generates a column based on the collision distance
3 - Thus, the image is built column by column
1- Send rays evenly spread across the field of view
0 | 1 | 2 | 3 | 1 | 1 | 2 | 3 |
---|
0 | 1 | 2 | 3 |
---|
2 | 2 | 0 | 1 |
---|
Image
Canvas
Pixels are laid-out from top to bottom, then left to right
Frame rendering
Color palette
Each item (pixel) contains its color index
Send pixels from C# to JS
namespace BlazorDoom
{
[SupportedOSPlatform("browser")]
public partial class Renderer
{
[JSImport("drawOnCanvas", "blazorDoom/renderer.js")]
internal static partial string renderOnJS(byte[] screenData,
int[] colors);
}
}
export function drawOnCanvas(screenData, colors) {
//
}
public class DoomRenderer
{
// Called by updateGameState
private void Display(uint[] colors)
{
BlazorDoom.Renderer.renderOnJS(screen.Data, (int[])((object)colors));
}
}
HTML canvas
export function drawOnCanvas(screenData, colors) {
const context = getCanvas().context;
const imageData = context.createImageData(320, 200);
let y = 0;
let x = 0;
for (let i = 0; i < screenData.length; i += 1) {
const dataIndex = (y * width + x) * 4;
setSinglePixel(imageData, dataIndex, colors, screenData[i]);
if (y >= height - 1) {
y = 0;
x += 1;
} else {
y += 1;
}
}
context.putImageData(imageData, 0, 0);
}
function setSinglePixel(imageData, dataIndex, colors, colorIndex) {
const color = colors[colorIndex];
imageData.data[dataIndex] = color & 0xff; // R
imageData.data[dataIndex + 1] = (color >> 8) & 0xff; // G
imageData.data[dataIndex + 2] = (color >> 16) & 0xff; // B
imageData.data[dataIndex + 3] = 255; // Alpha
}
Fire / Tirer
Valider / validate
Open / Ouvrir
Move / Se déplacer
Select WAD / Sélection de WAD
C# code, the .Net runtime and librairies are compiled to WASM
∞ WASM possibilities are infinite
💪 Game porting is accessible to all
🎮 Game dev is a great for learning while having fun
https://mspoweruser.com/this-doom-digital-camera-source-port-is-amazing-and-bizarre/
https://www.link-cable.com/top-10-weird-doom-ports/
pixabay.com
https://www.flaticon.com/ users: freepik
Doom text generator: https://c.eev.ee/doom-text-generator
https://www.pngkey.com/download/u2q8r5o0q8y3w7t4_doom-guy-grin/
https://www.pixilart.com/art/doom-guy-3e8e22def04259e
https://imgflip.com/memetemplate/65645030/detective-Doom-guy
https://imgflip.com/gif/9z8us3
https://imgflip.com/memetemplate/375393786/Doom-Laptop
https://openprocessing.org/sketch/1051403
https://freedesignfile.com/771822-crt-monitor-clipart/
https://www.flaticon.com/free-icon/curtains_1864805
@yostane
Yassine Benabbas
#TechAtWorldline
WebAssembly alias WASM
.Net
JavaScript alias JS
The doom guy
https://mspoweruser.com/this-doom-digital-camera-source-port-is-amazing-and-bizarre/
https://www.link-cable.com/top-10-weird-doom-ports/
pixabay.com
https://www.flaticon.com/ users: freepik
Doom text generator: https://c.eev.ee/doom-text-generator
https://www.pngkey.com/download/u2q8r5o0q8y3w7t4_doom-guy-grin/
https://www.pixilart.com/art/doom-guy-3e8e22def04259e
https://imgflip.com/memetemplate/65645030/detective-Doom-guy
https://imgflip.com/gif/9z8us3
https://imgflip.com/memetemplate/375393786/Doom-Laptop
https://openprocessing.org/sketch/1051403
https://freedesignfile.com/771822-crt-monitor-clipart/
https://www.flaticon.com/free-icon/curtains_1864805
@yostane
Yassine Benabbas
#TechAtWorldline
WebAssembly alias WASM
.Net
JavaScript alias JS
The doom guy