





@yostane


YCoding



#TechAtWorldline
Yassine
Benabbas
DevRel
Teacher
Video game collector
jobs.worldline.com


Web Assembly (WASM)
- Portable binary instruction format
- Runs on web browsers (not only)

- Faster than JS on compute intensive tasks
- Many programming languages compile to WASM
(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)
wasm file
https://github.com/WebAssembly/wabt
Browser

Compiler

WASM runtime
JS engine



Based on: https://wasmlabs.dev/articles/docker-without-containers/

binary
WASM in browsers
Source code


Operating system
Compiler
WASM runtime
Based on: https://wasmlabs.dev/articles/docker-without-containers/


Source code

binary
WASM + WASI on the OS
WASI, WASI-NN, Proxy-Wasm
FileSystem
Network
...

https://twitter.com/solomonstre/status/1111004913222324225
WASM on the browser
- A lot of languages compile to WASM
- JS interop (DOM)

Need to do something related to WASM



.NET on the browser
- .NET: OSS cross-platform framework
- C# language
- In 2020 .Net 5 introduced Blazor WASM
- Component based
- .NET runs on the browser



@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++; }
}
A Razor component


Let's port a .NET game !



Game porting
- Make a game run in platforms other than its original ones
- By rewriting / adapting the source code for the new platform(s)
- Not porting: virtual machine or emulator
MVG's video is a great source of inspiration

- Released in 1993 for DOS
- One the most successful First Person Shooters
-
Has two parts:
- engine: Game logic + I/O
- WAD file: all assets and maps


🌟 Doom is portable by design 🌟

http://mrglitchsreviews.blogspot.com/2012/09/doom-console-ports.html
Ported to a LOT of platforms





https://www.link-cable.com/top-10-weird-doom-ports/


Source: https://www.yahoo.com/news/1995-bill-gates-gave-crazy-180900044.html

ManagedDoom
id-Software/DOOM

sinshu/managed-doom
yostane/MangedDoom-Blazor







Porting strategy

2 weeks
as a side-project
creator.nightcafe.studio
Game loop in C#
while (waitForNextFrame()){
const input = getPlayerInput();
const { frame, audio }
= UpdateGameState(input, WAD);
render(frame);
play(audio);
}
Game loop in JS
function gameLoop(){
if (canAdvanceFrame()){
const input = getPlayerInput();
const { frame, audio }
= UpdateGameState(input, WAD);
render(frame);
play(audio);
}
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
ManagedDoom V1 architecture
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



wad
DOOM.wad
Blazor
~70% OK
Blazor
Blazor Doom architecture
pressed keys (keyup)
release keys (keydown)
['Z', 'Q']
['space']

wad
DOOM.wad

requestAnimationFrame
DotNet.invokeMethod
Audio buffer
Frame buffer
Canvas


Audio Context

IJSRuntime.Invoke
IJSRuntime.InvokeUnmarshalled

1 iteration of the Doom Engine
Updates the game state






<canvas id="canvas" width="320" height="200"
style="width:100%; height:auto; image-rendering: pixelated;">
</canvas>
@code {
// Entry point of the game
private async Task StartGame()
{
// steup the game object
app = new ManagedDoom.DoomApplication();
// Calls JS method that manages "requestAnimationFrame" pacing
jsProcessRuntime.InvokeVoid("gameLoop");
}
// Runs a frame of the game engine.
// Called after each "requestAnimationFrame"
[JSInvokable("UpdateGameState")]
public static void UpdateGameState(uint[] downKeys, uint[] upKeys)
{
app.Run(downKeys, upKeys);
}
}
Entry point: The main component

Frame pacing with JS

window.gameLoop = function (timestamp) {
// Check the pacing
if (timestamp - lastFrameTimestamp >= frameTime) {
lastFrameTimestamp = timestamp;
// Invoke the game engine to iterate once (advance a frame)
DotNet.invokeMethod('BlazorDoom', 'UpdateGameState',
downKeys, upKeys);
}
// This replaces the for loop in a traditional game
// Request the browser to notify us when we do the next iteration
window.requestAnimationFrame(window.gameLoop);
}
// The code that I showed earlier
[JSInvokable("UpdateGameState")]
public static void UpdateGameState(uint[] downKeys, uint[] upKeys)
{
app.Run(downKeys, upKeys);
}

Game engine execution with C#
Audio playback

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



🔊
playSound(samples, sampleRate) {
const audioBuffer = this.context.createBuffer(
1, length, this.context.sampleRate
);
var channelData = audioBuffer.getChannelData(0);
// JS receives a weird "samples" array
for (let i = 0; i < length; i += 2) {
// Scale the value to between -1 and 1
channelData[i] = samples[i] / 0xffff;
}
// Play the audio
var source = this.context.createBufferSource();
source.buffer = audioBuffer;
source.connect(this.context.destination);
source.start();
}

Audio playback
// Somewhere in the Doom Engine's audio module
DoomApplication.WebAssemblyJSRuntime.Invoke<object>(
"playSound", new object[] { samples, sampleRate, 0, Position }
);

0 | 1 | 2 | 3 | 1 | 1 | 2 | 3 |
---|
0 | 1 | 2 | 3 |
---|
From 1D frame to a 2D frame
2 | 2 | 0 | 1 |
---|
Frame data
Color palette
Canvas
Doom uses color indexing
Image built from top to bottom and from left to right



C# Byte Array to JS in .NET < 7
0 | 1 | 2 | 3 | 1 | 1 | 2 | 3 |
---|
2 | 2 | 0 | 1 |
---|
Frame data
0123
1123
2201


-
4 bytes in C# -> 1 number in JS
-
n elements in C# -> (n / 4) elements in JS
-
Bit shifting required in JS !
IJSRuntime.InvokeUnmarshalled
window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => {
// JS receives an array with 4 bytes per item
for (var i = 0; i < (width * height) / 4; i += 1) {
// Gets the array sent from C#
const screenDataItem = BINDING.mono_array_get(screenData, i);
for (let mask = 0; mask <= 24; mask += 8) {
let dataIndex = y * (width * 4) + x;
setSinglePixel(imageData, dataIndex, colors,
(screenDataItem >> mask) & 0xff);
// Build the image from top to bottom, left to right
if (y >= height - 1) { y = 0; x += 4; } else { y += 1; }
}
}
context.putImageData(imageData, 0, 0);
};
function setSinglePixel(imageData, dataIndex, colors, colorIndex) {
const color = BINDING.mono_array_get(colors, colorIndex);
imageData.data[dataIndex] = color & 0xff;
imageData.data[dataIndex + 1] = (color >> 8) & 0xff;
imageData.data[dataIndex + 2] = (color >> 16) & 0xff;
imageData.data[dataIndex + 3] = 255;
}
Rendering a Frame buffer
// Somwhere in the Doom Engine's graphics module
var args = new object[] { screen.Data, colors, 320, 200 };
// Send the frame buffer to JS
DoomApplication.WebAssemblyJSRuntime.InvokeUnmarshalled<byte[], uint[], int>
("renderWithColorsAndScreenDataUnmarshalled", screen.Data, colors);


Tips and lessons learned
-
Calling Blazor from JS is very fast
- But has problems with certain data types in .NET < 7
- Undocumented APIs removed in .Net 7 in favor of JS Interop
- This slooooows the app:
- Extensive logging
- Array.Copy of Big arrays (in .Net 5)
Blazor
JS
-
window.requestAnimationFrame to pace frames on the web
-
Browsers require interaction with the page to play audio




JS Interop in .net >= 7
- Less intricate way to run .Net from JS (no components)
<html>
<head>
<script type="module" src="./main.js"></script>
<!-- Load the .net runtime,
which will load our c# code as dll ! -->
<script type="module" src="./dotnet.js"></script>
<link rel="prefetch" href="./dotnet.wasm"/>
</head>
<body>
<canvas id="canvas" width="320" height="200"
style="image-rendering: pixelated" />
</body>
</html>
JS Interop in .net >= 7
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)
{
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 (canAdvanceFrame) {
exports.BlazorDoom.MainJS.UpdateGameState(keys);
}
requestAnimationFrame(gameLoop);
}

Frame Rendering with JS Interop

export function drawOnCanvas(screenData, colors) {
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;
}
}
}

Frame Rendering with JS Interop
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)
{
var args = new object[] { screen.Data, colors, 320, 200 };
BlazorDoom.Renderer.renderOnJS(screen.Data, (int[])((object)colors));
}
}
Another port possibility
id-Software/DOOM
sinshu/managed-doom


V1
V2
Silk.net
Blazor Web Assembly Support: dotnet/Silk.NET/issues/705
Next steps
- Short term:
- Continue migration to .NET 7 JSInterop
- Maybe add Music support
- Middle term:
- Update to ManagedDoom V2
- Experiment with Silk.Net is adds WASM support
- Long term / wish:
- Make this port an official part of ManagedDoom






#TechAtWorldline
jobs.worldline.com
Links
- 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
CommitConf 2023: Managed Doom Blazor
By yostane
CommitConf 2023: Managed Doom Blazor
How I ported an awesome game to an awesome framework. https://www.youtube.com/watch?v=C2N1tZj_qwI
- 128