Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[DevoxxBe 2024] Run your favorite games everywh...

[DevoxxBe 2024] Run your favorite games everywhere with WASM: the BlazorDoom use case

Web Assembly (WASM) is a powerful technology that opens the door to unlimited development possibilities. As a video game enthusiast, I used it to port the Doom game to the browser, allowing me to play it anywhere, even on my mobile. This is made possible thanks to the availability of an Open Source port of Doom to .Net and its support for compilation to WASM. This tools-in-action session will show you how I ported the MangedDoom game, which is made in .Net, to run in a browser. I will also share my experience on carrying out this port. You'll be surprised to see that this kind of project is very accessible as well as captivating, especially when you see the game running on a mobile browser. Although my work is based on .Net's WASM tooling, it can be applied to any framework that targets WASM. So, come and relive this fun porting adventure with me 👍.

yostane

April 09, 2025
Tweet

More Decks by yostane

Other Decks in Programming

Transcript

  1. We design payments technology that powers the growth of millions

    of businesses around the world. Managing 43+ billion transactions per year 7000+ engineers in over 40 countries #TechAtWorldline Handling 150+ payment methods blog.worldline.tech
  2. 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.....| 1 2 3 4 wasm file
  3. (module (func (result i32) (i32.const 42) ) (export "getUniversalNumber" (func

    0)) ) 1 2 3 4 5 6 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.....| 1 2 3 4 wat format (human readable) wasm file https:/ /github.com/WebAssembly/wabt
  4. Browser Compiler binary Source code WASM runtime JS engine Based

    on: https://wasmlabs.dev/articles/docker-without-containers/ WASM in browsers
  5. Browser Compiler binary Source code WASM runtime JS engine Based

    on: https://wasmlabs.dev/articles/docker-without-containers/ WASM in browsers
  6. Browser Compiler binary Source code WASM runtime JS engine Based

    on: https://wasmlabs.dev/articles/docker-without-containers/ WASM in browsers
  7. .NET on the browser .NET: C# OSS cross-platform framework Compiles

    to WASM (Blazor is an example) API to interop with JS
  8. 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(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 setModuleImports('main.js', { window: { location: { href: () => globalThis.window.location.href } } }); const text = exports.MyClass.Greeting(); console.log(text); 1 2 3 4 5 6 7 8 9 10 .NET 7+ JS Interop
  9. 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(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 setModuleImports('main.js', { window: { location: { href: () => globalThis.window.location.href } } }); const text = exports.MyClass.Greeting(); console.log(text); 1 2 3 4 5 6 7 8 9 10 .NET 7+ JS Interop
  10. 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(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 setModuleImports('main.js', { window: { location: { href: () => globalThis.window.location.href } } }); const text = exports.MyClass.Greeting(); console.log(text); 1 2 3 4 5 6 7 8 9 10 .NET 7+ JS Interop
  11. 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
  12. Released in 1993 for DOS Has two parts: engine: Game

    logic + I/O WAD file: all assets and maps 🌟 Doom is portable by design 🌟
  13. ManagedDoom .NET Port of (official source code) ManagedDoom V1 Uses

    (graphics + audio + input) V2 uses (released 27 Dec 2022) My work is a fork of ManagedDoom V1 LinuxDoom SFML silk.net
  14. ManagedDoom .NET Port of (official source code) ManagedDoom V1 Uses

    (graphics + audio + input) V2 uses (released 27 Dec 2022) My work is a fork of ManagedDoom V1 LinuxDoom SFML silk.net id-Software/DOOM
  15. ManagedDoom .NET Port of (official source code) ManagedDoom V1 Uses

    (graphics + audio + input) V2 uses (released 27 Dec 2022) My work is a fork of ManagedDoom V1 LinuxDoom SFML silk.net id-Software/DOOM sinshu/managed-doom
  16. ManagedDoom .NET Port of (official source code) ManagedDoom V1 Uses

    (graphics + audio + input) V2 uses (released 27 Dec 2022) My work is a fork of ManagedDoom V1 LinuxDoom SFML silk.net id-Software/DOOM sinshu/managed-doom yostane/MangedDoom-Blazor
  17. Game loop in C# while (waitForNextFrame()){ const input = getPlayerInput();

    const { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); }
  18. Game loop in JS if (canAdvanceFrame()){ const input = getPlayerInput();

    const { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } function gameLoop(){ 1 2 3 4 5 6 7 8 requestAnimationFrame(gameLoop); 9 } 10 requestAnimationFrame(gameLoop); 11
  19. Game loop in JS if (canAdvanceFrame()){ const input = getPlayerInput();

    const { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } function gameLoop(){ 1 2 3 4 5 6 7 8 requestAnimationFrame(gameLoop); 9 } 10 requestAnimationFrame(gameLoop); 11 function gameLoop(){ requestAnimationFrame(gameLoop); requestAnimationFrame(gameLoop); 1 if (canAdvanceFrame()){ 2 const input = getPlayerInput(); 3 const { frame, audio } 4 = UpdateGameState(input, WAD); 5 render(frame); 6 play(audio); 7 } 8 9 } 10 11
  20. Game loop in JS if (canAdvanceFrame()){ const input = getPlayerInput();

    const { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } function gameLoop(){ 1 2 3 4 5 6 7 8 requestAnimationFrame(gameLoop); 9 } 10 requestAnimationFrame(gameLoop); 11 function gameLoop(){ requestAnimationFrame(gameLoop); requestAnimationFrame(gameLoop); 1 if (canAdvanceFrame()){ 2 const input = getPlayerInput(); 3 const { frame, audio } 4 = UpdateGameState(input, WAD); 5 render(frame); 6 play(audio); 7 } 8 9 } 10 11 function gameLoop(){ if (canAdvanceFrame()){ const input = getPlayerInput(); const { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } requestAnimationFrame(gameLoop); } requestAnimationFrame(gameLoop); 1 2 3 4 5 6 7 8 9 10 11
  21. For loop (game loop) pressed keys (keyup) release keys (keydown)

    ['Z', 'Q'] ['space'] ManagedDoom V1 architecture 1 iteration of the Doom Engine Updates the game state wad DOOM.wad sf2 SoundFont.sf2
  22. For loop (game loop) pressed keys (keyup) release keys (keydown)

    ['Z', 'Q'] ['space'] ManagedDoom V1 architecture Frame buffer Audio buffer 1 iteration of the Doom Engine Updates the game state wad DOOM.wad sf2 SoundFont.sf2
  23. For loop (game loop) pressed keys (keyup) release keys (keydown)

    ['Z', 'Q'] ['space'] ManagedDoom V1 architecture Frame buffer Audio buffer SFML Video SFML Audio 1 iteration of the Doom Engine Updates the game state wad DOOM.wad sf2 SoundFont.sf2
  24. For loop (game loop) pressed keys (keyup) release keys (keydown)

    ['Z', 'Q'] ['space'] ManagedDoom V1 architecture Frame buffer Audio buffer SFML Video SFML Audio 1 iteration of the Doom Engine Updates the game state wad DOOM.wad sf2 SoundFont.sf2
  25. For loop (game loop) pressed keys (keyup) release keys (keydown)

    ['Z', 'Q'] ['space'] ManagedDoom V1 architecture Frame buffer Audio buffer SFML Video SFML Audio 1 iteration of the Doom Engine Updates the game state .NET WASM ~70% OK .NET WASM wad DOOM.wad sf2 SoundFont.sf2
  26. pressed keys (keyup) release keys (keydown) ['Z', 'Q'] ['space'] Blazor

    Doom architecture wad DOOM.wad requestAnimationFrame Run .Net Canvas Run JS Run JS Frame buffer Audio buffer Audio Context 1 iteration of the Doom Engine Updates the game state sf2 SoundFont.sf2
  27. Porting strategy Clone sinshu/managed-doom and compile to .NET WASM Reverse

    engineer SFML: Put empty implementations where it fails and matk them ("TODO: implement") creator.nightcafe.studio
  28. Porting strategy Clone sinshu/managed-doom and compile to .NET WASM Reverse

    engineer SFML: Put empty implementations where it fails and matk them ("TODO: implement") Implement little by little using guesswork and logic creator.nightcafe.studio
  29. Porting strategy Clone sinshu/managed-doom and compile to .NET WASM Reverse

    engineer SFML: Put empty implementations where it fails and matk them ("TODO: implement") Implement little by little using guesswork and logic Delegate to JS when necessary creator.nightcafe.studio
  30. using SFML.Audio; // Not available namespace ManagedDoom.Audio { public sealed

    class SfmlSound : ISound, IDisposable { private SoundBuffer[] buffers; } } 1 2 3 4 5 6 7 8 9 Example: SFML.Audio.SoundBuffer is not available
  31. namespace SFML.Audio { public class SoundBuffer { public SoundBuffer(short[] samples,

    int v, uint sampleRate) { // TODO: implement } internal void Dispose() { // TODO: implement } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 So, let's reverse-engineer it
  32. 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; } internal void Dispose() { // TODO: implement } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
  33. <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> 1 2 3 4 5 6 7 8 9 10 Entry point
  34. <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> 1 2 3 4 5 6 7 8 9 10 <!-- Sets .Net interop and starts the game loop --> <script type="module" src="./main.js"></script> <html> 1 <head> 2 3 4 </head> 5 <body> 6 <canvas id="canvas" width="320" height="200" 7 style="image-rendering: pixelated" /> 8 </body> 9 </html> 10 Entry point
  35. <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> 1 2 3 4 5 6 7 8 9 10 <!-- Sets .Net interop and starts the game loop --> <script type="module" src="./main.js"></script> <html> 1 <head> 2 3 4 </head> 5 <body> 6 <canvas id="canvas" width="320" height="200" 7 style="image-rendering: pixelated" /> 8 </body> 9 </html> 10 <canvas id="canvas" width="320" height="200" style="image-rendering: pixelated" /> <html> 1 <head> 2 <!-- Sets .Net interop and starts the game loop --> 3 <script type="module" src="./main.js"></script> 4 </head> 5 <body> 6 7 8 </body> 9 </html> 10 Entry point
  36. Running the gameloop 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); } 1 2 3 4 5 6 7 8 9 10 11 12 main.js
  37. Running the gameloop 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); } 1 2 3 4 5 6 7 8 9 10 11 12 import { dotnet } from "./dotnet.js"; const { getAssemblyExports, getConfig } = await dotnet.create(); const exports = await getAssemblyExports(getConfig().mainAssemblyName); await dotnet.run(); 1 2 3 4 5 function gameLoop(timestamp) { 6 if (timestamp - lastFrameTimestamp >= 1000 / 30) { 7 lastFrameTimestamp = timestamp; 8 exports.BlazorDoom.MainJS.UpdateGameState(keys); 9 } 10 requestAnimationFrame(gameLoop); 11 } 12 main.js
  38. Running the gameloop 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); } 1 2 3 4 5 6 7 8 9 10 11 12 import { dotnet } from "./dotnet.js"; const { getAssemblyExports, getConfig } = await dotnet.create(); const exports = await getAssemblyExports(getConfig().mainAssemblyName); await dotnet.run(); 1 2 3 4 5 function gameLoop(timestamp) { 6 if (timestamp - lastFrameTimestamp >= 1000 / 30) { 7 lastFrameTimestamp = timestamp; 8 exports.BlazorDoom.MainJS.UpdateGameState(keys); 9 } 10 requestAnimationFrame(gameLoop); 11 } 12 function gameLoop(timestamp) { requestAnimationFrame(gameLoop); import { dotnet } from "./dotnet.js"; 1 const { getAssemblyExports, getConfig } = await dotnet.create(); 2 const exports = await getAssemblyExports(getConfig().mainAssemblyName); 3 await dotnet.run(); 4 5 6 if (timestamp - lastFrameTimestamp >= 1000 / 30) { 7 lastFrameTimestamp = timestamp; 8 exports.BlazorDoom.MainJS.UpdateGameState(keys); 9 } 10 11 } 12 main.js
  39. Running the gameloop 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); } 1 2 3 4 5 6 7 8 9 10 11 12 import { dotnet } from "./dotnet.js"; const { getAssemblyExports, getConfig } = await dotnet.create(); const exports = await getAssemblyExports(getConfig().mainAssemblyName); await dotnet.run(); 1 2 3 4 5 function gameLoop(timestamp) { 6 if (timestamp - lastFrameTimestamp >= 1000 / 30) { 7 lastFrameTimestamp = timestamp; 8 exports.BlazorDoom.MainJS.UpdateGameState(keys); 9 } 10 requestAnimationFrame(gameLoop); 11 } 12 function gameLoop(timestamp) { requestAnimationFrame(gameLoop); import { dotnet } from "./dotnet.js"; 1 const { getAssemblyExports, getConfig } = await dotnet.create(); 2 const exports = await getAssemblyExports(getConfig().mainAssemblyName); 3 await dotnet.run(); 4 5 6 if (timestamp - lastFrameTimestamp >= 1000 / 30) { 7 lastFrameTimestamp = timestamp; 8 exports.BlazorDoom.MainJS.UpdateGameState(keys); 9 } 10 11 } 12 exports.BlazorDoom.MainJS.UpdateGameState(keys); import { dotnet } from "./dotnet.js"; 1 const { getAssemblyExports, getConfig } = await dotnet.create(); 2 const exports = await getAssemblyExports(getConfig().mainAssemblyName); 3 await dotnet.run(); 4 5 function gameLoop(timestamp) { 6 if (timestamp - lastFrameTimestamp >= 1000 / 30) { 7 lastFrameTimestamp = timestamp; 8 9 } 10 requestAnimationFrame(gameLoop); 11 } 12 main.js
  40. Running the gameloop 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); } 1 2 3 4 5 6 7 8 9 10 11 12 import { dotnet } from "./dotnet.js"; const { getAssemblyExports, getConfig } = await dotnet.create(); const exports = await getAssemblyExports(getConfig().mainAssemblyName); await dotnet.run(); 1 2 3 4 5 function gameLoop(timestamp) { 6 if (timestamp - lastFrameTimestamp >= 1000 / 30) { 7 lastFrameTimestamp = timestamp; 8 exports.BlazorDoom.MainJS.UpdateGameState(keys); 9 } 10 requestAnimationFrame(gameLoop); 11 } 12 function gameLoop(timestamp) { requestAnimationFrame(gameLoop); import { dotnet } from "./dotnet.js"; 1 const { getAssemblyExports, getConfig } = await dotnet.create(); 2 const exports = await getAssemblyExports(getConfig().mainAssemblyName); 3 await dotnet.run(); 4 5 6 if (timestamp - lastFrameTimestamp >= 1000 / 30) { 7 lastFrameTimestamp = timestamp; 8 exports.BlazorDoom.MainJS.UpdateGameState(keys); 9 } 10 11 } 12 exports.BlazorDoom.MainJS.UpdateGameState(keys); import { dotnet } from "./dotnet.js"; 1 const { getAssemblyExports, getConfig } = await dotnet.create(); 2 const exports = await getAssemblyExports(getConfig().mainAssemblyName); 3 await dotnet.run(); 4 5 function gameLoop(timestamp) { 6 if (timestamp - lastFrameTimestamp >= 1000 / 30) { 7 lastFrameTimestamp = timestamp; 8 9 } 10 requestAnimationFrame(gameLoop); 11 } 12 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); } 1 2 3 4 5 6 7 8 9 10 11 12 main.js
  41. Running the gameloop 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); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 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); } 1 2 3 4 5 6 7 8 9 10 11 12 main.js
  42. 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 Sample 1 / Sampling frequency + Sampling frequency
  43. 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
  44. 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 🔊
  45. Audio playback void PlayCurrentFrameSound(SoundBuffer soundBuffer) { int[] samples = Array.ConvertAll(soundBuffer.samples,

    Convert.ToInt32); BlazorDoom.Renderer.playSoundOnJS(samples, (int)soundBuffer.sampleRate); } 1 2 3 4 5 namespace BlazorDoom { [SupportedOSPlatform("browser")] public partial class Renderer { [JSImport("playSound", "blazorDoom/renderer.js")] internal static partial string playSoundOnJS( int[] samples, int sampleRate ); } } 1 2 3 4 5 6 7 8 9 10 11 12
  46. Audio playback 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++) { // noralize 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(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
  47. Audio playback 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++) { // noralize 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(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const length = samples.length; const audioBuffer = audioContext.createBuffer( 1, length, sampleRate ); export function playSound(samples, sampleRate) { 1 audioContext = new AudioContext({ 2 sampleRate: sampleRate, 3 }); 4 5 6 7 8 9 10 11 var channelData = audioBuffer.getChannelData(0); 12 for (let i = 0; i < length; i++) { 13 // noralize the sample to be between -1 and 1 14 channelData[i] = samples[i] / 0xffff; 15 } 16 17 var source = audioContext.createBufferSource(); 18 source.buffer = audioBuffer; 19 source.connect(audioContext.destination); 20 source.start(); 21 } 22
  48. Audio playback 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++) { // noralize 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(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const length = samples.length; const audioBuffer = audioContext.createBuffer( 1, length, sampleRate ); export function playSound(samples, sampleRate) { 1 audioContext = new AudioContext({ 2 sampleRate: sampleRate, 3 }); 4 5 6 7 8 9 10 11 var channelData = audioBuffer.getChannelData(0); 12 for (let i = 0; i < length; i++) { 13 // noralize the sample to be between -1 and 1 14 channelData[i] = samples[i] / 0xffff; 15 } 16 17 var source = audioContext.createBufferSource(); 18 source.buffer = audioBuffer; 19 source.connect(audioContext.destination); 20 source.start(); 21 } 22 var channelData = audioBuffer.getChannelData(0); for (let i = 0; i < length; i++) { // noralize the sample to be between -1 and 1 channelData[i] = samples[i] / 0xffff; } export function playSound(samples, sampleRate) { 1 audioContext = new AudioContext({ 2 sampleRate: sampleRate, 3 }); 4 const length = samples.length; 5 const audioBuffer = audioContext.createBuffer( 6 1, 7 length, 8 sampleRate 9 ); 10 11 12 13 14 15 16 17 var source = audioContext.createBufferSource(); 18 source.buffer = audioBuffer; 19 source.connect(audioContext.destination); 20 source.start(); 21 } 22
  49. 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 12 bytes Color palette
  50. 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 12 bytes Color palette
  51. 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 12 bytes Color palette
  52. 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 12 bytes Color palette
  53. 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 12 bytes Color palette
  54. 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 12 bytes Color palette Canvas Frame 12 * 4 bytes (r, g, b, a)
  55. 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 12 bytes Color palette Canvas Frame 12 * 4 bytes (r, g, b, a) Doom uses color indexing Image built from top to bottom and from left to right
  56. Frame rendering: 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); } } 1 2 3 4 5 6 7 8 9 10 export function drawOnCanvas(screenData, colors) { // } 1 2 3 public class DoomRenderer { // Called by updateGameState private void Display(uint[] colors) { BlazorDoom.Renderer.renderOnJS(screen.Data, (int[])((object)colors)); } } 1 2 3 4 5 6 7 8
  57. Rendering a Frame buffer 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 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
  58. Rendering a Frame buffer 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 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 if (y >= height - 1) { y = 0; x += 1; } else { y += 1; export function drawOnCanvas(screenData, colors) { 1 const context = getCanvas().context; 2 const imageData = context.createImageData(320, 200); 3 let y = 0; 4 let x = 0; 5 for (let i = 0; i < screenData.length; i += 1) { 6 const dataIndex = (y * width + x) * 4; 7 setSinglePixel(imageData, dataIndex, colors, screenData[i]); 8 9 10 11 12 13 } 14 } 15 context.putImageData(imageData, 0, 0); 16 } 17 18 function setSinglePixel(imageData, dataIndex, colors, colorIndex) { 19 const color = colors[colorIndex]; 20 imageData.data[dataIndex] = color & 0xff; // R 21 imageData.data[dataIndex + 1] = (color >> 8) & 0xff; // G 22 imageData.data[dataIndex + 2] = (color >> 16) & 0xff; // B 23 imageData.data[dataIndex + 3] = 255; // Alpha 24 } 25
  59. Rendering a Frame buffer 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 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 if (y >= height - 1) { y = 0; x += 1; } else { y += 1; export function drawOnCanvas(screenData, colors) { 1 const context = getCanvas().context; 2 const imageData = context.createImageData(320, 200); 3 let y = 0; 4 let x = 0; 5 for (let i = 0; i < screenData.length; i += 1) { 6 const dataIndex = (y * width + x) * 4; 7 setSinglePixel(imageData, dataIndex, colors, screenData[i]); 8 9 10 11 12 13 } 14 } 15 context.putImageData(imageData, 0, 0); 16 } 17 18 function setSinglePixel(imageData, dataIndex, colors, colorIndex) { 19 const color = colors[colorIndex]; 20 imageData.data[dataIndex] = color & 0xff; // R 21 imageData.data[dataIndex + 1] = (color >> 8) & 0xff; // G 22 imageData.data[dataIndex + 2] = (color >> 16) & 0xff; // B 23 imageData.data[dataIndex + 3] = 255; // Alpha 24 } 25 setSinglePixel(imageData, dataIndex, colors, screenData[i]); 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 } export function drawOnCanvas(screenData, colors) { 1 const context = getCanvas().context; 2 const imageData = context.createImageData(320, 200); 3 let y = 0; 4 let x = 0; 5 for (let i = 0; i < screenData.length; i += 1) { 6 const dataIndex = (y * width + x) * 4; 7 8 if (y >= height - 1) { 9 y = 0; 10 x += 1; 11 } else { 12 y += 1; 13 } 14 } 15 context.putImageData(imageData, 0, 0); 16 } 17 18 19 20 21 22 23 24 25
  60. Key takeaways WASM possibilities are limitless Porting a game is

    way to learn programming while having fun