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

[DevoxxUK 2024] Using WASM to port games to the...

[DevoxxUK 2024] Using WASM to port games to the browser: the BlazorDoom use case

WASM is a powerful platform-agnostic technology. Do you know that you can take advantage of it to port games to the browser as long as the source code can be compiled to WASM ? In that regard, I ported ManagedDoom to the Browser thanks to .Net supporting of WASM.

I this talk, I will share with you how I managed to port ManagedDoom to the web. We'll talk about WASM, game porting, .Net and JavaScript. I'll also share the issues that I encountered and how I solved them.

The concepts that I'll present can be applied to any language that targets WASM. So, come and live this porting adventure again 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. Web Assembly (WASM) Portable binary instruction format Initially targeted for

    web browsers Faster than JS on compute intensive tasks Many programming languages compile to WASM
  3. 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
  4. (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
  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. Browser Compiler binary Source code WASM runtime JS engine Based

    on: https://wasmlabs.dev/articles/docker-without-containers/ WASM in browsers
  8. Operating system Compiler binary Source code WASM runtime Based on:

    https://wasmlabs.dev/articles/docker-without-containers/ WASM + WASI on the OS WASI, WASI-NN, Proxy-Wasm FileSystem Network ...
  9. .NET on the browser .NET: C# OSS cross-platform framework .Net

    5 (2020): Blazor WASM Component-based framework .Net runtime compiled into WASM
  10. .NET on the browser .NET: C# OSS cross-platform framework .Net

    5 (2020): Blazor WASM Component-based framework .Net runtime compiled into WASM @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++; } } 1 2 3 4 5 6 7 8 9 10
  11. 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+ wasm-tools Call .Net from JS and vice- versa Basis of Blazor WASM
  12. 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+ wasm-tools Call .Net from JS and vice- versa Basis of Blazor WASM
  13. 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+ wasm-tools Call .Net from JS and vice- versa Basis of Blazor WASM
  14. 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+ wasm-tools Call .Net from JS and vice- versa Basis of Blazor WASM Blazor WASM 💪 Webapp or front dev wasm-tools 💪 general purpose
  15. 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
  16. 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
  17. 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 🌟
  18. 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
  19. 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
  20. 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
  21. 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
  22. Game loop in C# while (waitForNextFrame()){ const input = getPlayerInput();

    const { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); }
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. <html> <head> <!-- Start the game loop --> <script type="module"

    src="./main.js"></script> <!-- Load the .net runtime and our compiled C# code ! --> <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> 1 2 3 4 5 6 7 8 9 10 11 12 13 Entry point
  39. <html> <head> <!-- Start the game loop --> <script type="module"

    src="./main.js"></script> <!-- Load the .net runtime and our compiled C# code ! --> <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> 1 2 3 4 5 6 7 8 9 10 11 12 13 <canvas id="canvas" width="320" height="200" style="image-rendering: pixelated" /> <html> 1 <head> 2 <!-- Start the game loop --> 3 <script type="module" src="./main.js"></script> 4 <!-- Load the .net runtime and our compiled C# code ! --> 5 <script type="module" src="./dotnet.js"></script> 6 <link rel="prefetch" href="./dotnet.wasm"/> 7 </head> 8 <body> 9 10 11 </body> 12 </html> 13 Entry point
  40. <html> <head> <!-- Start the game loop --> <script type="module"

    src="./main.js"></script> <!-- Load the .net runtime and our compiled C# code ! --> <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> 1 2 3 4 5 6 7 8 9 10 11 12 13 <canvas id="canvas" width="320" height="200" style="image-rendering: pixelated" /> <html> 1 <head> 2 <!-- Start the game loop --> 3 <script type="module" src="./main.js"></script> 4 <!-- Load the .net runtime and our compiled C# code ! --> 5 <script type="module" src="./dotnet.js"></script> 6 <link rel="prefetch" href="./dotnet.wasm"/> 7 </head> 8 <body> 9 10 11 </body> 12 </html> 13 <!-- Load the .net runtime and our compiled C# code ! --> <script type="module" src="./dotnet.js"></script> <link rel="prefetch" href="./dotnet.wasm"/> <html> 1 <head> 2 <!-- Start the game loop --> 3 <script type="module" src="./main.js"></script> 4 5 6 7 </head> 8 <body> 9 <canvas id="canvas" width="320" height="200" 10 style="image-rendering: pixelated" /> 11 </body> 12 </html> 13 Entry point
  41. <html> <head> <!-- Start the game loop --> <script type="module"

    src="./main.js"></script> <!-- Load the .net runtime and our compiled C# code ! --> <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> 1 2 3 4 5 6 7 8 9 10 11 12 13 <canvas id="canvas" width="320" height="200" style="image-rendering: pixelated" /> <html> 1 <head> 2 <!-- Start the game loop --> 3 <script type="module" src="./main.js"></script> 4 <!-- Load the .net runtime and our compiled C# code ! --> 5 <script type="module" src="./dotnet.js"></script> 6 <link rel="prefetch" href="./dotnet.wasm"/> 7 </head> 8 <body> 9 10 11 </body> 12 </html> 13 <!-- Load the .net runtime and our compiled C# code ! --> <script type="module" src="./dotnet.js"></script> <link rel="prefetch" href="./dotnet.wasm"/> <html> 1 <head> 2 <!-- Start the game loop --> 3 <script type="module" src="./main.js"></script> 4 5 6 7 </head> 8 <body> 9 <canvas id="canvas" width="320" height="200" 10 style="image-rendering: pixelated" /> 11 </body> 12 </html> 13 <!-- Start the game loop --> <script type="module" src="./main.js"></script> <html> 1 <head> 2 3 4 <!-- Load the .net runtime and our compiled C# code ! --> 5 <script type="module" src="./dotnet.js"></script> 6 <link rel="prefetch" href="./dotnet.wasm"/> 7 </head> 8 <body> 9 <canvas id="canvas" width="320" height="200" 10 style="image-rendering: pixelated" /> 11 </body> 12 </html> 13 Entry point
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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 🔊
  51. 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
  52. 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
  53. 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
  54. 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
  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
  56. 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
  57. 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
  58. 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
  59. 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
  60. 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)
  61. 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
  62. 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
  63. 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
  64. 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
  65. 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
  66. Next steps Better BGM (Background Music) implementation Update to ManagedDoom

    V2 (replaced SFML with Silk.Net) Make this port an official part of ManagedDoom