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

[Fork It! Tunis 2025] How I Ported Doom to the ...

[Fork It! Tunis 2025] How I Ported Doom to the Browser with WebAssembly

yostane

April 09, 2025
Tweet

More Decks by yostane

Other Decks in Programming

Transcript

  1. Yassine Benabbas DevRel engineer @ Worldline Teacher ( yostane.github.io/lectures )

    Lille Android User Group Video game collector @yostane
  2. Web Assembly (WASM) Portable binary instruction format Initially targeted for

    web browsers (to speed up JS) 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
  3. Browser Source code JS engine Based on: https://wasmlabs.dev/articles/docker-without-containers/ WASM on

    the web Compiler / WASM toolchain wasm-pack ... ... WASM binary Glue code
  4. Browser Source code WASM runtime JS engine Based on: https://wasmlabs.dev/articles/docker-without-containers/

    WASM on the web Compiler / WASM toolchain wasm-pack ... ... WASM binary Glue code
  5. .NET and WASM .NET: C# OSS cross-platform framework Compiles to

    WASM Blazor WASM: Client framework (like VueJS) wasm-tools: Vanilla approach
  6. @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 Blazor wasm
  7. 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
  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+ wasm-tools
  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+ wasm-tools
  10. 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
  11. 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
  12. 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
  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 id-Software/DOOM sinshu/managed-doom
  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 sinshu/managed-doom yostane/MangedDoom-Blazor
  15. Porting plan Clone sininshu/managed-doom Change the build target to WASM

    Re-implement SFML code: 1. With mocks until the project compiles dall-e
  16. Porting plan Clone sininshu/managed-doom Change the build target to WASM

    Re-implement SFML code: 1. With mocks until the project compiles 2. Then replace mocks with correct implementations dall-e
  17. Porting plan Clone sininshu/managed-doom Change the build target to WASM

    Re-implement SFML code: 1. With mocks until the project compiles 2. Then replace mocks with correct implementations Implemenet minimal JS (gameloop, audio and video rendering) dall-e
  18. Porting plan Clone sininshu/managed-doom Change the build target to WASM

    Re-implement SFML code: 1. With mocks until the project compiles 2. Then replace mocks with correct implementations Implemenet minimal JS (gameloop, audio and video rendering) dall-e
  19. Porting plan Clone sininshu/managed-doom Change the build target to WASM

    Re-implement SFML code: 1. With mocks until the project compiles 2. Then replace mocks with correct implementations Implemenet minimal JS (gameloop, audio and video rendering) dall-e
  20. Game loop in C# while (waitForNextFrame()){ const input = getPlayerInput();

    const { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); }
  21. 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
  22. 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
  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 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
  24. 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
  25. namespace SFML.Audio { public class SoundBuffer { public SoundBuffer(short[] samples,

    int v, uint sampleRate) { // TODO: implement } } } 1 2 3 4 5 6 7 8 9 10 So, let's implement it first with a mock
  26. 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; } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 After that, with a complete implementation
  27. 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
  28. 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
  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 wad DOOM.wad sf2 SoundFont.sf2
  31. 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
  32. 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
  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) { // Get color from the color palette const color = colors[colorIndex]; // Extract RGB and spread it in the canvas 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 26 27
  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) { // Get color from the color palette const color = colors[colorIndex]; // Extract RGB and spread it in the canvas 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 26 27 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 // Get color from the color palette 20 const color = colors[colorIndex]; 21 // Extract RGB and spread it in the canvas 22 imageData.data[dataIndex] = color & 0xff; // R 23 imageData.data[dataIndex + 1] = (color >> 8) & 0xff; // G 24 imageData.data[dataIndex + 2] = (color >> 16) & 0xff; // B 25 imageData.data[dataIndex + 3] = 255; // Alpha 26 } 27
  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) { // Get color from the color palette const color = colors[colorIndex]; // Extract RGB and spread it in the canvas 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 26 27 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 // Get color from the color palette 20 const color = colors[colorIndex]; 21 // Extract RGB and spread it in the canvas 22 imageData.data[dataIndex] = color & 0xff; // R 23 imageData.data[dataIndex + 1] = (color >> 8) & 0xff; // G 24 imageData.data[dataIndex + 2] = (color >> 16) & 0xff; // B 25 imageData.data[dataIndex + 3] = 255; // Alpha 26 } 27 setSinglePixel(imageData, dataIndex, colors, screenData[i]); function setSinglePixel(imageData, dataIndex, colors, colorIndex) { // Get color from the color palette const color = colors[colorIndex]; // Extract RGB and spread it in the canvas imageData.data[dataIndex] = color & 0xff; // R imageData.data[dataIndex + 1] = (color >> 8) & 0xff; // G imageData.data[dataIndex + 2] = (color >> 16) & 0xff; // B 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 imageData.data[dataIndex + 3] = 255; // Alpha 26 } 27
  60. Key takeaways WASM possibilities are limitless Porting a game is

    way to learn programming while having fun