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

[FOSDEM 2023] How I Ported Doom to the Browser ...

[FOSDEM 2023] How I Ported Doom to the Browser with WebAssembly

yostane

April 09, 2025
Tweet

More Decks by yostane

Other Decks in Programming

Transcript

  1. Game porting Make a game run in platforms other than

    its original ones Achieved by adapting the code for the new platform Not porting: virtual machine or emulator
  2. Game porting Make a game run in platforms other than

    its original ones Achieved by adapting the code for the new platform Not porting: virtual machine or emulator MVG's video is a great source of inspiration
  3. .net and the browser .net: OSS cross-platform framework C# language

    In 2020 .Net 5 introduced Blazor WASM ​ ​ ​ Component based We can run .net locally on the browser
  4. @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 A Razor component
  5. Released in 1993 for DOS One the most successful First

    Person Shooters Has two parts: engine: Game logic WAD file: all assets and maps 🌟 Doom is portable by design 🌟
  6. ManagedDoom .Net Port of ManagedDoom V1 Uses (graphics + audio

    + input) V2 (in beta as of Jan 3) uses My work is a based on ManagedDoom V1 LinuxDoom SFML silk.net
  7. ManagedDoom .Net Port of ManagedDoom V1 Uses (graphics + audio

    + input) V2 (in beta as of Jan 3) uses My work is a based on ManagedDoom V1 LinuxDoom SFML silk.net id-Software/DOOM
  8. ManagedDoom .Net Port of ManagedDoom V1 Uses (graphics + audio

    + input) V2 (in beta as of Jan 3) uses My work is a based on ManagedDoom V1 LinuxDoom SFML silk.net id-Software/DOOM sinshu/managed-doom
  9. ManagedDoom .Net Port of ManagedDoom V1 Uses (graphics + audio

    + input) V2 (in beta as of Jan 3) uses My work is a based on ManagedDoom V1 LinuxDoom SFML silk.net id-Software/DOOM sinshu/managed-doom yostane/MangedDoom-Blazor
  10. Porting strategy Compile the app: replace SFML code with "TODO:

    implement" Implement TODOs little by little, priority to frame rendering creator.nightcafe.studio
  11. Porting strategy Compile the app: replace SFML code with "TODO:

    implement" Implement TODOs little by little, priority to frame rendering Optimize and clean later strategy creator.nightcafe.studio
  12. Porting strategy Compile the app: replace SFML code with "TODO:

    implement" Implement TODOs little by little, priority to frame rendering Optimize and clean later strategy Read and documentation only when necessary Used to understand frame format Doom-Wiki SFML creator.nightcafe.studio
  13. Porting strategy Compile the app: replace SFML code with "TODO:

    implement" Implement TODOs little by little, priority to frame rendering Optimize and clean later strategy Read and documentation only when necessary Used to understand frame format Doom-Wiki SFML 2 weeks as a side- project creator.nightcafe.studio
  14. Game loop pseudo-code while (waitForNextFrame()){ const input = getPlayerInput(); const

    { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } 1 2 3 4 5 6 7
  15. Game loop pseudo-code while (waitForNextFrame()){ const input = getPlayerInput(); const

    { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } 1 2 3 4 5 6 7 while (waitForNextFrame()){ 1 const input = getPlayerInput(); 2 const { frame, audio } 3 = UpdateGameState(input, WAD); 4 render(frame); 5 play(audio); 6 } 7
  16. Game loop pseudo-code while (waitForNextFrame()){ const input = getPlayerInput(); const

    { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } 1 2 3 4 5 6 7 while (waitForNextFrame()){ 1 const input = getPlayerInput(); 2 const { frame, audio } 3 = UpdateGameState(input, WAD); 4 render(frame); 5 play(audio); 6 } 7 const input = getPlayerInput(); while (waitForNextFrame()){ 1 2 const { frame, audio } 3 = UpdateGameState(input, WAD); 4 render(frame); 5 play(audio); 6 } 7
  17. Game loop pseudo-code while (waitForNextFrame()){ const input = getPlayerInput(); const

    { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } 1 2 3 4 5 6 7 while (waitForNextFrame()){ 1 const input = getPlayerInput(); 2 const { frame, audio } 3 = UpdateGameState(input, WAD); 4 render(frame); 5 play(audio); 6 } 7 const input = getPlayerInput(); while (waitForNextFrame()){ 1 2 const { frame, audio } 3 = UpdateGameState(input, WAD); 4 render(frame); 5 play(audio); 6 } 7 const { frame, audio } = UpdateGameState(input, WAD); while (waitForNextFrame()){ 1 const input = getPlayerInput(); 2 3 4 render(frame); 5 play(audio); 6 } 7
  18. Game loop pseudo-code while (waitForNextFrame()){ const input = getPlayerInput(); const

    { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } 1 2 3 4 5 6 7 while (waitForNextFrame()){ 1 const input = getPlayerInput(); 2 const { frame, audio } 3 = UpdateGameState(input, WAD); 4 render(frame); 5 play(audio); 6 } 7 const input = getPlayerInput(); while (waitForNextFrame()){ 1 2 const { frame, audio } 3 = UpdateGameState(input, WAD); 4 render(frame); 5 play(audio); 6 } 7 const { frame, audio } = UpdateGameState(input, WAD); while (waitForNextFrame()){ 1 const input = getPlayerInput(); 2 3 4 render(frame); 5 play(audio); 6 } 7 render(frame); play(audio); while (waitForNextFrame()){ 1 const input = getPlayerInput(); 2 const { frame, audio } 3 = UpdateGameState(input, WAD); 4 5 6 } 7
  19. Game loop pseudo-code while (waitForNextFrame()){ const input = getPlayerInput(); const

    { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } 1 2 3 4 5 6 7 while (waitForNextFrame()){ 1 const input = getPlayerInput(); 2 const { frame, audio } 3 = UpdateGameState(input, WAD); 4 render(frame); 5 play(audio); 6 } 7 const input = getPlayerInput(); while (waitForNextFrame()){ 1 2 const { frame, audio } 3 = UpdateGameState(input, WAD); 4 render(frame); 5 play(audio); 6 } 7 const { frame, audio } = UpdateGameState(input, WAD); while (waitForNextFrame()){ 1 const input = getPlayerInput(); 2 3 4 render(frame); 5 play(audio); 6 } 7 render(frame); play(audio); while (waitForNextFrame()){ 1 const input = getPlayerInput(); 2 const { frame, audio } 3 = UpdateGameState(input, WAD); 4 5 6 } 7 while (waitForNextFrame()){ const input = getPlayerInput(); const { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } 1 2 3 4 5 6 7
  20. pressed keys (keyup) release keys (keydown) ['Z', 'Q'] ['space'] ManagedDoom

    V1 architecture UpdateGameState while loop wad DOOM.wad
  21. pressed keys (keyup) release keys (keydown) ['Z', 'Q'] ['space'] ManagedDoom

    V1 architecture Frame Audio UpdateGameState while loop SFML Video SFML Audio wad DOOM.wad
  22. pressed keys (keyup) release keys (keydown) ['Z', 'Q'] ['space'] ManagedDoom

    V1 architecture Frame Audio UpdateGameState while loop SFML Video SFML Audio Blazor wad DOOM.wad ~70% OK Browser
  23. pressed keys (keyup) release keys (keydown) ['Z', 'Q'] ['space'] Blazor

    Doom architecture wad DOOM.wad requestAnimationFrame Canvas Frame buffer Audio buffer UpdateGameState Audio Context
  24. pressed keys (keyup) release keys (keydown) ['Z', 'Q'] ['space'] Blazor

    Doom architecture wad DOOM.wad requestAnimationFrame Canvas Frame buffer Audio buffer UpdateGameState Audio Context BlazorDoom component
  25. pressed keys (keyup) release keys (keydown) ['Z', 'Q'] ['space'] Blazor

    Doom architecture wad DOOM.wad requestAnimationFrame DotNet.invokeMethod Canvas IJSRuntime.Invoke IJSRuntime.InvokeUnmarshalled Frame buffer Audio buffer UpdateGameState Audio Context BlazorDoom component IJSRuntime.InvokeVoid
  26. <canvas id="canvas" image-rendering: pixelated;" ...> </canvas> @code { // Entry

    point of the game private async Task StartGame() { // steup the game object app = new ManagedDoom.DoomApplication(...); // JS method that calls "requestAnimationFrame" jsProcessRuntime.InvokeVoid("gameLoop"); } } 1 2 3 4 5 6 7 8 9 10 11 12 Entry point and frame pacing window.gameLoop = function (timestamp) { // Check the pacing if (timestamp - lastFrameTimestamp >= frameTime) { lastFrameTimestamp = timestamp; // Updates the game state (advances a frame) DotNet.invokeMethod('BlazorDoom', 'UpdateGame', downKeys, upKeys) } // This replaces the for loop in a traditional game // Request the browser to notify us when we do the next iteration window.requestAnimationFrame(window.gameLoop); } 1 2 3 4 5 6 7 8 9 10 11
  27. <canvas id="canvas" image-rendering: pixelated;" ...> </canvas> @code { // Entry

    point of the game private async Task StartGame() { // steup the game object app = new ManagedDoom.DoomApplication(...); // JS method that calls "requestAnimationFrame" jsProcessRuntime.InvokeVoid("gameLoop"); } } 1 2 3 4 5 6 7 8 9 10 11 12 <canvas id="canvas" image-rendering: pixelated;" ...> </canvas> 1 2 @code { 3 // Entry point of the game 4 private async Task StartGame() 5 { 6 // steup the game object 7 app = new ManagedDoom.DoomApplication(...); 8 // JS method that calls "requestAnimationFrame" 9 jsProcessRuntime.InvokeVoid("gameLoop"); 10 } 11 } 12 Entry point and frame pacing window.gameLoop = function (timestamp) { // Check the pacing if (timestamp - lastFrameTimestamp >= frameTime) { lastFrameTimestamp = timestamp; // Updates the game state (advances a frame) DotNet.invokeMethod('BlazorDoom', 'UpdateGame', downKeys, upKeys) } // This replaces the for loop in a traditional game // Request the browser to notify us when we do the next iteration window.requestAnimationFrame(window.gameLoop); } 1 2 3 4 5 6 7 8 9 10 11
  28. <canvas id="canvas" image-rendering: pixelated;" ...> </canvas> @code { // Entry

    point of the game private async Task StartGame() { // steup the game object app = new ManagedDoom.DoomApplication(...); // JS method that calls "requestAnimationFrame" jsProcessRuntime.InvokeVoid("gameLoop"); } } 1 2 3 4 5 6 7 8 9 10 11 12 <canvas id="canvas" image-rendering: pixelated;" ...> </canvas> 1 2 @code { 3 // Entry point of the game 4 private async Task StartGame() 5 { 6 // steup the game object 7 app = new ManagedDoom.DoomApplication(...); 8 // JS method that calls "requestAnimationFrame" 9 jsProcessRuntime.InvokeVoid("gameLoop"); 10 } 11 } 12 private async Task StartGame() { // steup the game object app = new ManagedDoom.DoomApplication(...); // JS method that calls "requestAnimationFrame" <canvas id="canvas" image-rendering: pixelated;" ...> 1 </canvas> 2 @code { 3 // Entry point of the game 4 5 6 7 8 9 jsProcessRuntime.InvokeVoid("gameLoop"); 10 } 11 } 12 Entry point and frame pacing window.gameLoop = function (timestamp) { // Check the pacing if (timestamp - lastFrameTimestamp >= frameTime) { lastFrameTimestamp = timestamp; // Updates the game state (advances a frame) DotNet.invokeMethod('BlazorDoom', 'UpdateGame', downKeys, upKeys) } // This replaces the for loop in a traditional game // Request the browser to notify us when we do the next iteration window.requestAnimationFrame(window.gameLoop); } 1 2 3 4 5 6 7 8 9 10 11
  29. <canvas id="canvas" image-rendering: pixelated;" ...> </canvas> @code { // Entry

    point of the game private async Task StartGame() { // steup the game object app = new ManagedDoom.DoomApplication(...); // JS method that calls "requestAnimationFrame" jsProcessRuntime.InvokeVoid("gameLoop"); } } 1 2 3 4 5 6 7 8 9 10 11 12 <canvas id="canvas" image-rendering: pixelated;" ...> </canvas> 1 2 @code { 3 // Entry point of the game 4 private async Task StartGame() 5 { 6 // steup the game object 7 app = new ManagedDoom.DoomApplication(...); 8 // JS method that calls "requestAnimationFrame" 9 jsProcessRuntime.InvokeVoid("gameLoop"); 10 } 11 } 12 private async Task StartGame() { // steup the game object app = new ManagedDoom.DoomApplication(...); // JS method that calls "requestAnimationFrame" <canvas id="canvas" image-rendering: pixelated;" ...> 1 </canvas> 2 @code { 3 // Entry point of the game 4 5 6 7 8 9 jsProcessRuntime.InvokeVoid("gameLoop"); 10 } 11 } 12 jsProcessRuntime.InvokeVoid("gameLoop"); } <canvas id="canvas" image-rendering: pixelated;" ...> 1 </canvas> 2 @code { 3 // Entry point of the game 4 private async Task StartGame() 5 { 6 // steup the game object 7 app = new ManagedDoom.DoomApplication(...); 8 // JS method that calls "requestAnimationFrame" 9 10 11 } 12 Entry point and frame pacing window.gameLoop = function (timestamp) { // Check the pacing if (timestamp - lastFrameTimestamp >= frameTime) { lastFrameTimestamp = timestamp; // Updates the game state (advances a frame) DotNet.invokeMethod('BlazorDoom', 'UpdateGame', downKeys, upKeys) } // This replaces the for loop in a traditional game // Request the browser to notify us when we do the next iteration window.requestAnimationFrame(window.gameLoop); } 1 2 3 4 5 6 7 8 9 10 11
  30. <canvas id="canvas" image-rendering: pixelated;" ...> </canvas> @code { // Entry

    point of the game private async Task StartGame() { // steup the game object app = new ManagedDoom.DoomApplication(...); // JS method that calls "requestAnimationFrame" jsProcessRuntime.InvokeVoid("gameLoop"); } } 1 2 3 4 5 6 7 8 9 10 11 12 <canvas id="canvas" image-rendering: pixelated;" ...> </canvas> 1 2 @code { 3 // Entry point of the game 4 private async Task StartGame() 5 { 6 // steup the game object 7 app = new ManagedDoom.DoomApplication(...); 8 // JS method that calls "requestAnimationFrame" 9 jsProcessRuntime.InvokeVoid("gameLoop"); 10 } 11 } 12 private async Task StartGame() { // steup the game object app = new ManagedDoom.DoomApplication(...); // JS method that calls "requestAnimationFrame" <canvas id="canvas" image-rendering: pixelated;" ...> 1 </canvas> 2 @code { 3 // Entry point of the game 4 5 6 7 8 9 jsProcessRuntime.InvokeVoid("gameLoop"); 10 } 11 } 12 jsProcessRuntime.InvokeVoid("gameLoop"); } <canvas id="canvas" image-rendering: pixelated;" ...> 1 </canvas> 2 @code { 3 // Entry point of the game 4 private async Task StartGame() 5 { 6 // steup the game object 7 app = new ManagedDoom.DoomApplication(...); 8 // JS method that calls "requestAnimationFrame" 9 10 11 } 12 Entry point and frame pacing window.gameLoop = function (timestamp) { // Check the pacing if (timestamp - lastFrameTimestamp >= frameTime) { lastFrameTimestamp = timestamp; // Updates the game state (advances a frame) DotNet.invokeMethod('BlazorDoom', 'UpdateGame', downKeys, upKeys) } // This replaces the for loop in a traditional game // Request the browser to notify us when we do the next iteration window.requestAnimationFrame(window.gameLoop); } 1 2 3 4 5 6 7 8 9 10 11 // Check the pacing if (timestamp - lastFrameTimestamp >= frameTime) { window.gameLoop = function (timestamp) { 1 2 3 lastFrameTimestamp = timestamp; 4 // Updates the game state (advances a frame) 5 DotNet.invokeMethod('BlazorDoom', 'UpdateGame', downKeys, upKeys) 6 } 7 // This replaces the for loop in a traditional game 8 // Request the browser to notify us when we do the next iteration 9 window.requestAnimationFrame(window.gameLoop); 10 } 11
  31. <canvas id="canvas" image-rendering: pixelated;" ...> </canvas> @code { // Entry

    point of the game private async Task StartGame() { // steup the game object app = new ManagedDoom.DoomApplication(...); // JS method that calls "requestAnimationFrame" jsProcessRuntime.InvokeVoid("gameLoop"); } } 1 2 3 4 5 6 7 8 9 10 11 12 <canvas id="canvas" image-rendering: pixelated;" ...> </canvas> 1 2 @code { 3 // Entry point of the game 4 private async Task StartGame() 5 { 6 // steup the game object 7 app = new ManagedDoom.DoomApplication(...); 8 // JS method that calls "requestAnimationFrame" 9 jsProcessRuntime.InvokeVoid("gameLoop"); 10 } 11 } 12 private async Task StartGame() { // steup the game object app = new ManagedDoom.DoomApplication(...); // JS method that calls "requestAnimationFrame" <canvas id="canvas" image-rendering: pixelated;" ...> 1 </canvas> 2 @code { 3 // Entry point of the game 4 5 6 7 8 9 jsProcessRuntime.InvokeVoid("gameLoop"); 10 } 11 } 12 jsProcessRuntime.InvokeVoid("gameLoop"); } <canvas id="canvas" image-rendering: pixelated;" ...> 1 </canvas> 2 @code { 3 // Entry point of the game 4 private async Task StartGame() 5 { 6 // steup the game object 7 app = new ManagedDoom.DoomApplication(...); 8 // JS method that calls "requestAnimationFrame" 9 10 11 } 12 Entry point and frame pacing window.gameLoop = function (timestamp) { // Check the pacing if (timestamp - lastFrameTimestamp >= frameTime) { lastFrameTimestamp = timestamp; // Updates the game state (advances a frame) DotNet.invokeMethod('BlazorDoom', 'UpdateGame', downKeys, upKeys) } // This replaces the for loop in a traditional game // Request the browser to notify us when we do the next iteration window.requestAnimationFrame(window.gameLoop); } 1 2 3 4 5 6 7 8 9 10 11 // Check the pacing if (timestamp - lastFrameTimestamp >= frameTime) { window.gameLoop = function (timestamp) { 1 2 3 lastFrameTimestamp = timestamp; 4 // Updates the game state (advances a frame) 5 DotNet.invokeMethod('BlazorDoom', 'UpdateGame', downKeys, upKeys) 6 } 7 // This replaces the for loop in a traditional game 8 // Request the browser to notify us when we do the next iteration 9 window.requestAnimationFrame(window.gameLoop); 10 } 11 // Updates the game state (advances a frame) DotNet.invokeMethod('BlazorDoom', 'UpdateGame', downKeys, upKeys) window.gameLoop = function (timestamp) { 1 // Check the pacing 2 if (timestamp - lastFrameTimestamp >= frameTime) { 3 lastFrameTimestamp = timestamp; 4 5 6 } 7 // This replaces the for loop in a traditional game 8 // Request the browser to notify us when we do the next iteration 9 window.requestAnimationFrame(window.gameLoop); 10 } 11
  32. <canvas id="canvas" image-rendering: pixelated;" ...> </canvas> @code { // Entry

    point of the game private async Task StartGame() { // steup the game object app = new ManagedDoom.DoomApplication(...); // JS method that calls "requestAnimationFrame" jsProcessRuntime.InvokeVoid("gameLoop"); } } 1 2 3 4 5 6 7 8 9 10 11 12 <canvas id="canvas" image-rendering: pixelated;" ...> </canvas> 1 2 @code { 3 // Entry point of the game 4 private async Task StartGame() 5 { 6 // steup the game object 7 app = new ManagedDoom.DoomApplication(...); 8 // JS method that calls "requestAnimationFrame" 9 jsProcessRuntime.InvokeVoid("gameLoop"); 10 } 11 } 12 private async Task StartGame() { // steup the game object app = new ManagedDoom.DoomApplication(...); // JS method that calls "requestAnimationFrame" <canvas id="canvas" image-rendering: pixelated;" ...> 1 </canvas> 2 @code { 3 // Entry point of the game 4 5 6 7 8 9 jsProcessRuntime.InvokeVoid("gameLoop"); 10 } 11 } 12 jsProcessRuntime.InvokeVoid("gameLoop"); } <canvas id="canvas" image-rendering: pixelated;" ...> 1 </canvas> 2 @code { 3 // Entry point of the game 4 private async Task StartGame() 5 { 6 // steup the game object 7 app = new ManagedDoom.DoomApplication(...); 8 // JS method that calls "requestAnimationFrame" 9 10 11 } 12 Entry point and frame pacing window.gameLoop = function (timestamp) { // Check the pacing if (timestamp - lastFrameTimestamp >= frameTime) { lastFrameTimestamp = timestamp; // Updates the game state (advances a frame) DotNet.invokeMethod('BlazorDoom', 'UpdateGame', downKeys, upKeys) } // This replaces the for loop in a traditional game // Request the browser to notify us when we do the next iteration window.requestAnimationFrame(window.gameLoop); } 1 2 3 4 5 6 7 8 9 10 11 // Check the pacing if (timestamp - lastFrameTimestamp >= frameTime) { window.gameLoop = function (timestamp) { 1 2 3 lastFrameTimestamp = timestamp; 4 // Updates the game state (advances a frame) 5 DotNet.invokeMethod('BlazorDoom', 'UpdateGame', downKeys, upKeys) 6 } 7 // This replaces the for loop in a traditional game 8 // Request the browser to notify us when we do the next iteration 9 window.requestAnimationFrame(window.gameLoop); 10 } 11 // Updates the game state (advances a frame) DotNet.invokeMethod('BlazorDoom', 'UpdateGame', downKeys, upKeys) window.gameLoop = function (timestamp) { 1 // Check the pacing 2 if (timestamp - lastFrameTimestamp >= frameTime) { 3 lastFrameTimestamp = timestamp; 4 5 6 } 7 // This replaces the for loop in a traditional game 8 // Request the browser to notify us when we do the next iteration 9 window.requestAnimationFrame(window.gameLoop); 10 } 11 // This replaces the for loop in a traditional game // Request the browser to notify us when we do the next iteration window.requestAnimationFrame(window.gameLoop); window.gameLoop = function (timestamp) { 1 // Check the pacing 2 if (timestamp - lastFrameTimestamp >= frameTime) { 3 lastFrameTimestamp = timestamp; 4 // Updates the game state (advances a frame) 5 DotNet.invokeMethod('BlazorDoom', 'UpdateGame', downKeys, upKeys) 6 } 7 8 9 10 } 11
  33. Entry point and frame pacing window.gameLoop = function (timestamp) {

    // Check the pacing if (timestamp - lastFrameTimestamp >= frameTime) { lastFrameTimestamp = timestamp; // Updates the game state (advances a frame) DotNet.invokeMethod('BlazorDoom', 'UpdateGame', downKeys, upKeys) } // This replaces the for loop in a traditional game // Request the browser to notify us when we do the next iteration window.requestAnimationFrame(window.gameLoop); } 1 2 3 4 5 6 7 8 9 10 11 // The code that I showed earlier [JSInvokable("GameLoop")] public static void UpdateGame(uint[] downKeys, uint[] upKeys) { app.Run(downKeys, upKeys); } 1 2 3 4 5 6
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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 🔊
  39. playByteArray(samples, sampleRate) { const audioBuffer = this.context.createBuffer( 1, length, this.context.sampleRate

    ); var channelData = audioBuffer.getChannelData(0); // JS receives a weird "samples" array for (let i = 0; i < length; i += 2) { // Scale the value to between -1 and 1 channelData[i] = samples[i] / 0xffff; } // Play the audio var source = this.context.createBufferSource(); source.buffer = audioBuffer; source.connect(this.context.destination); source.start(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Audio playback // Somewhere in the Doom Engine's audio module DoomApplication.WebAssemblyJSRuntime.Invoke<object>( "playSound", new object[] { samples, sampleRate, 0, Position } ); 1 2 3 4
  40. playByteArray(samples, sampleRate) { const audioBuffer = this.context.createBuffer( 1, length, this.context.sampleRate

    ); var channelData = audioBuffer.getChannelData(0); // JS receives a weird "samples" array for (let i = 0; i < length; i += 2) { // Scale the value to between -1 and 1 channelData[i] = samples[i] / 0xffff; } // Play the audio var source = this.context.createBufferSource(); source.buffer = audioBuffer; source.connect(this.context.destination); source.start(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var channelData = audioBuffer.getChannelData(0); // JS receives a weird "samples" array for (let i = 0; i < length; i += 2) { // Scale the value to between -1 and 1 channelData[i] = samples[i] / 0xffff; } playByteArray(samples, sampleRate) { 1 const audioBuffer = this.context.createBuffer( 2 1, length, this.context.sampleRate 3 ); 4 5 6 7 8 9 10 // Play the audio 11 var source = this.context.createBufferSource(); 12 source.buffer = audioBuffer; 13 source.connect(this.context.destination); 14 source.start(); 15 } 16 Audio playback // Somewhere in the Doom Engine's audio module DoomApplication.WebAssemblyJSRuntime.Invoke<object>( "playSound", new object[] { samples, sampleRate, 0, Position } ); 1 2 3 4
  41. playByteArray(samples, sampleRate) { const audioBuffer = this.context.createBuffer( 1, length, this.context.sampleRate

    ); var channelData = audioBuffer.getChannelData(0); // JS receives a weird "samples" array for (let i = 0; i < length; i += 2) { // Scale the value to between -1 and 1 channelData[i] = samples[i] / 0xffff; } // Play the audio var source = this.context.createBufferSource(); source.buffer = audioBuffer; source.connect(this.context.destination); source.start(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var channelData = audioBuffer.getChannelData(0); // JS receives a weird "samples" array for (let i = 0; i < length; i += 2) { // Scale the value to between -1 and 1 channelData[i] = samples[i] / 0xffff; } playByteArray(samples, sampleRate) { 1 const audioBuffer = this.context.createBuffer( 2 1, length, this.context.sampleRate 3 ); 4 5 6 7 8 9 10 // Play the audio 11 var source = this.context.createBufferSource(); 12 source.buffer = audioBuffer; 13 source.connect(this.context.destination); 14 source.start(); 15 } 16 playByteArray(samples, sampleRate) { const audioBuffer = this.context.createBuffer( 1, length, this.context.sampleRate ); var channelData = audioBuffer.getChannelData(0); // JS receives a weird "samples" array for (let i = 0; i < length; i += 2) { // Scale the value to between -1 and 1 channelData[i] = samples[i] / 0xffff; } // Play the audio var source = this.context.createBufferSource(); source.buffer = audioBuffer; source.connect(this.context.destination); source.start(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Audio playback // Somewhere in the Doom Engine's audio module DoomApplication.WebAssemblyJSRuntime.Invoke<object>( "playSound", new object[] { samples, sampleRate, 0, Position } ); 1 2 3 4
  42. 0 1 2 3 1 1 2 3 0 1

    2 3 From 1D frame to a 2D frame 2 2 0 1 Frame data Color palette Frame
  43. 0 1 2 3 1 1 2 3 0 1

    2 3 From 1D frame to a 2D frame 2 2 0 1 Frame data Color palette Frame
  44. 0 1 2 3 1 1 2 3 0 1

    2 3 From 1D frame to a 2D frame 2 2 0 1 Frame data Color palette Frame
  45. 0 1 2 3 1 1 2 3 0 1

    2 3 From 1D frame to a 2D frame 2 2 0 1 Frame data Color palette Frame
  46. 0 1 2 3 1 1 2 3 0 1

    2 3 From 1D frame to a 2D frame 2 2 0 1 Frame data Color palette Frame
  47. 0 1 2 3 1 1 2 3 0 1

    2 3 From 1D frame to a 2D frame 2 2 0 1 Frame data Color palette Frame
  48. 0 1 2 3 1 1 2 3 0 1

    2 3 From 1D frame to a 2D frame 2 2 0 1 Frame data Color palette Frame Image built from top to bottom and from left to right Doom uses color indexing
  49. C# Byte Array to JS, considerations 0 1 2 3

    1 1 2 3 0 1 2 3 2 2 0 1 Frame data Color palette IJSRuntime.InvokeUnmarshalled
  50. C# Byte Array to JS, considerations 0 1 2 3

    1 1 2 3 0 1 2 3 2 2 0 1 Frame data Color palette 0123 1123 2201 IJSRuntime.InvokeUnmarshalled
  51. C# Byte Array to JS, considerations 0 1 2 3

    1 1 2 3 0 1 2 3 2 2 0 1 Frame data Color palette 0123 1123 2201 0123 IJSRuntime.InvokeUnmarshalled
  52. C# Byte Array to JS, considerations 0 1 2 3

    1 1 2 3 0 1 2 3 2 2 0 1 Frame data Color palette 0123 1123 2201 0123 4 bytes in C# -> 1 number in JS n elements in C# -> (n / 4) elements in JS Bit shifting required in JS ! IJSRuntime.InvokeUnmarshalled
  53. Frame rendering // Somwhere in the Doom Engine's graphics module

    var args = new object[] { screen.Data, colors, 320, 200 }; // Send the frame buffer to JS DoomApplication.WebAssemblyJSRuntime.InvokeUnmarshalled<byte[], uint[], int> ("renderWithColorsAndScreenDataUnmarshalled", screen.Data, colors); 1 2 3 4 5
  54. window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { // JS receives an

    array with 4 bytes per item for (var i = 0; i < (width * height) / 4; i += 1) { // Gets the array sent from C# const screenDataItem = BINDING.mono_array_get(screenData, i); for (var mask = 0; mask <= 24; mask += 8) { let dataIndex = y * (width * 4) + x; setSinglePixel(imageData, dataIndex, colors, (screenDataItem >> mask) & 0xff); // Build the image from top to bottom, left to right if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } } } context.putImageData(imageData, 0, 0); }; function setSinglePixel(imageData, dataIndex, colors, colorIndex) { const color = BINDING.mono_array_get(colors, colorIndex); imageData.data[dataIndex] = color & 0xff; imageData.data[dataIndex + 1] = (color >> 8) & 0xff; imageData.data[dataIndex + 2] = (color >> 16) & 0xff; imageData.data[dataIndex + 3] = 255; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Frame rendering // Somwhere in the Doom Engine's graphics module var args = new object[] { screen.Data, colors, 320, 200 }; // Send the frame buffer to JS DoomApplication.WebAssemblyJSRuntime.InvokeUnmarshalled<byte[], uint[], int> ("renderWithColorsAndScreenDataUnmarshalled", screen.Data, colors); 1 2 3 4 5
  55. window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { // JS receives an

    array with 4 bytes per item for (var i = 0; i < (width * height) / 4; i += 1) { // Gets the array sent from C# const screenDataItem = BINDING.mono_array_get(screenData, i); for (var mask = 0; mask <= 24; mask += 8) { let dataIndex = y * (width * 4) + x; setSinglePixel(imageData, dataIndex, colors, (screenDataItem >> mask) & 0xff); // Build the image from top to bottom, left to right if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } } } context.putImageData(imageData, 0, 0); }; function setSinglePixel(imageData, dataIndex, colors, colorIndex) { const color = BINDING.mono_array_get(colors, colorIndex); imageData.data[dataIndex] = color & 0xff; imageData.data[dataIndex + 1] = (color >> 8) & 0xff; imageData.data[dataIndex + 2] = (color >> 16) & 0xff; imageData.data[dataIndex + 3] = 255; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // Gets the array sent from C# const screenDataItem = BINDING.mono_array_get(screenData, i); window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { 1 // JS receives an array with 4 bytes per item 2 for (var i = 0; i < (width * height) / 4; i += 1) { 3 4 5 for (var mask = 0; mask <= 24; mask += 8) { 6 let dataIndex = y * (width * 4) + x; 7 setSinglePixel(imageData, dataIndex, colors, 8 (screenDataItem >> mask) & 0xff); 9 // Build the image from top to bottom, left to right 10 if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } 11 } 12 } 13 context.putImageData(imageData, 0, 0); 14 }; 15 function setSinglePixel(imageData, dataIndex, colors, colorIndex) { 16 const color = BINDING.mono_array_get(colors, colorIndex); 17 imageData.data[dataIndex] = color & 0xff; 18 imageData.data[dataIndex + 1] = (color >> 8) & 0xff; 19 imageData.data[dataIndex + 2] = (color >> 16) & 0xff; 20 imageData.data[dataIndex + 3] = 255; 21 } 22 Frame rendering // Somwhere in the Doom Engine's graphics module var args = new object[] { screen.Data, colors, 320, 200 }; // Send the frame buffer to JS DoomApplication.WebAssemblyJSRuntime.InvokeUnmarshalled<byte[], uint[], int> ("renderWithColorsAndScreenDataUnmarshalled", screen.Data, colors); 1 2 3 4 5
  56. window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { // JS receives an

    array with 4 bytes per item for (var i = 0; i < (width * height) / 4; i += 1) { // Gets the array sent from C# const screenDataItem = BINDING.mono_array_get(screenData, i); for (var mask = 0; mask <= 24; mask += 8) { let dataIndex = y * (width * 4) + x; setSinglePixel(imageData, dataIndex, colors, (screenDataItem >> mask) & 0xff); // Build the image from top to bottom, left to right if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } } } context.putImageData(imageData, 0, 0); }; function setSinglePixel(imageData, dataIndex, colors, colorIndex) { const color = BINDING.mono_array_get(colors, colorIndex); imageData.data[dataIndex] = color & 0xff; imageData.data[dataIndex + 1] = (color >> 8) & 0xff; imageData.data[dataIndex + 2] = (color >> 16) & 0xff; imageData.data[dataIndex + 3] = 255; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // Gets the array sent from C# const screenDataItem = BINDING.mono_array_get(screenData, i); window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { 1 // JS receives an array with 4 bytes per item 2 for (var i = 0; i < (width * height) / 4; i += 1) { 3 4 5 for (var mask = 0; mask <= 24; mask += 8) { 6 let dataIndex = y * (width * 4) + x; 7 setSinglePixel(imageData, dataIndex, colors, 8 (screenDataItem >> mask) & 0xff); 9 // Build the image from top to bottom, left to right 10 if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } 11 } 12 } 13 context.putImageData(imageData, 0, 0); 14 }; 15 function setSinglePixel(imageData, dataIndex, colors, colorIndex) { 16 const color = BINDING.mono_array_get(colors, colorIndex); 17 imageData.data[dataIndex] = color & 0xff; 18 imageData.data[dataIndex + 1] = (color >> 8) & 0xff; 19 imageData.data[dataIndex + 2] = (color >> 16) & 0xff; 20 imageData.data[dataIndex + 3] = 255; 21 } 22 // Build the image from top to bottom, left to right if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { 1 // JS receives an array with 4 bytes per item 2 for (var i = 0; i < (width * height) / 4; i += 1) { 3 // Gets the array sent from C# 4 const screenDataItem = BINDING.mono_array_get(screenData, i); 5 for (var mask = 0; mask <= 24; mask += 8) { 6 let dataIndex = y * (width * 4) + x; 7 setSinglePixel(imageData, dataIndex, colors, 8 (screenDataItem >> mask) & 0xff); 9 10 11 } 12 } 13 context.putImageData(imageData, 0, 0); 14 }; 15 function setSinglePixel(imageData, dataIndex, colors, colorIndex) { 16 const color = BINDING.mono_array_get(colors, colorIndex); 17 imageData.data[dataIndex] = color & 0xff; 18 imageData.data[dataIndex + 1] = (color >> 8) & 0xff; 19 imageData.data[dataIndex + 2] = (color >> 16) & 0xff; 20 imageData.data[dataIndex + 3] = 255; 21 } 22 Frame rendering // Somwhere in the Doom Engine's graphics module var args = new object[] { screen.Data, colors, 320, 200 }; // Send the frame buffer to JS DoomApplication.WebAssemblyJSRuntime.InvokeUnmarshalled<byte[], uint[], int> ("renderWithColorsAndScreenDataUnmarshalled", screen.Data, colors); 1 2 3 4 5
  57. window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { // JS receives an

    array with 4 bytes per item for (var i = 0; i < (width * height) / 4; i += 1) { // Gets the array sent from C# const screenDataItem = BINDING.mono_array_get(screenData, i); for (var mask = 0; mask <= 24; mask += 8) { let dataIndex = y * (width * 4) + x; setSinglePixel(imageData, dataIndex, colors, (screenDataItem >> mask) & 0xff); // Build the image from top to bottom, left to right if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } } } context.putImageData(imageData, 0, 0); }; function setSinglePixel(imageData, dataIndex, colors, colorIndex) { const color = BINDING.mono_array_get(colors, colorIndex); imageData.data[dataIndex] = color & 0xff; imageData.data[dataIndex + 1] = (color >> 8) & 0xff; imageData.data[dataIndex + 2] = (color >> 16) & 0xff; imageData.data[dataIndex + 3] = 255; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // Gets the array sent from C# const screenDataItem = BINDING.mono_array_get(screenData, i); window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { 1 // JS receives an array with 4 bytes per item 2 for (var i = 0; i < (width * height) / 4; i += 1) { 3 4 5 for (var mask = 0; mask <= 24; mask += 8) { 6 let dataIndex = y * (width * 4) + x; 7 setSinglePixel(imageData, dataIndex, colors, 8 (screenDataItem >> mask) & 0xff); 9 // Build the image from top to bottom, left to right 10 if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } 11 } 12 } 13 context.putImageData(imageData, 0, 0); 14 }; 15 function setSinglePixel(imageData, dataIndex, colors, colorIndex) { 16 const color = BINDING.mono_array_get(colors, colorIndex); 17 imageData.data[dataIndex] = color & 0xff; 18 imageData.data[dataIndex + 1] = (color >> 8) & 0xff; 19 imageData.data[dataIndex + 2] = (color >> 16) & 0xff; 20 imageData.data[dataIndex + 3] = 255; 21 } 22 // Build the image from top to bottom, left to right if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { 1 // JS receives an array with 4 bytes per item 2 for (var i = 0; i < (width * height) / 4; i += 1) { 3 // Gets the array sent from C# 4 const screenDataItem = BINDING.mono_array_get(screenData, i); 5 for (var mask = 0; mask <= 24; mask += 8) { 6 let dataIndex = y * (width * 4) + x; 7 setSinglePixel(imageData, dataIndex, colors, 8 (screenDataItem >> mask) & 0xff); 9 10 11 } 12 } 13 context.putImageData(imageData, 0, 0); 14 }; 15 function setSinglePixel(imageData, dataIndex, colors, colorIndex) { 16 const color = BINDING.mono_array_get(colors, colorIndex); 17 imageData.data[dataIndex] = color & 0xff; 18 imageData.data[dataIndex + 1] = (color >> 8) & 0xff; 19 imageData.data[dataIndex + 2] = (color >> 16) & 0xff; 20 imageData.data[dataIndex + 3] = 255; 21 } 22 let dataIndex = y * (width * 4) + x; setSinglePixel(imageData, dataIndex, colors, function setSinglePixel(imageData, dataIndex, colors, colorIndex) { const color = BINDING.mono_array_get(colors, colorIndex); imageData.data[dataIndex] = color & 0xff; imageData.data[dataIndex + 1] = (color >> 8) & 0xff; imageData.data[dataIndex + 2] = (color >> 16) & 0xff; imageData.data[dataIndex + 3] = 255; } window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { 1 // JS receives an array with 4 bytes per item 2 for (var i = 0; i < (width * height) / 4; i += 1) { 3 // Gets the array sent from C# 4 const screenDataItem = BINDING.mono_array_get(screenData, i); 5 for (var mask = 0; mask <= 24; mask += 8) { 6 7 8 (screenDataItem >> mask) & 0xff); 9 // Build the image from top to bottom, left to right 10 if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } 11 } 12 } 13 context.putImageData(imageData, 0, 0); 14 }; 15 16 17 18 19 20 21 22 Frame rendering // Somwhere in the Doom Engine's graphics module var args = new object[] { screen.Data, colors, 320, 200 }; // Send the frame buffer to JS DoomApplication.WebAssemblyJSRuntime.InvokeUnmarshalled<byte[], uint[], int> ("renderWithColorsAndScreenDataUnmarshalled", screen.Data, colors); 1 2 3 4 5
  58. window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { // JS receives an

    array with 4 bytes per item for (var i = 0; i < (width * height) / 4; i += 1) { // Gets the array sent from C# const screenDataItem = BINDING.mono_array_get(screenData, i); for (var mask = 0; mask <= 24; mask += 8) { let dataIndex = y * (width * 4) + x; setSinglePixel(imageData, dataIndex, colors, (screenDataItem >> mask) & 0xff); // Build the image from top to bottom, left to right if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } } } context.putImageData(imageData, 0, 0); }; function setSinglePixel(imageData, dataIndex, colors, colorIndex) { const color = BINDING.mono_array_get(colors, colorIndex); imageData.data[dataIndex] = color & 0xff; imageData.data[dataIndex + 1] = (color >> 8) & 0xff; imageData.data[dataIndex + 2] = (color >> 16) & 0xff; imageData.data[dataIndex + 3] = 255; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // Gets the array sent from C# const screenDataItem = BINDING.mono_array_get(screenData, i); window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { 1 // JS receives an array with 4 bytes per item 2 for (var i = 0; i < (width * height) / 4; i += 1) { 3 4 5 for (var mask = 0; mask <= 24; mask += 8) { 6 let dataIndex = y * (width * 4) + x; 7 setSinglePixel(imageData, dataIndex, colors, 8 (screenDataItem >> mask) & 0xff); 9 // Build the image from top to bottom, left to right 10 if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } 11 } 12 } 13 context.putImageData(imageData, 0, 0); 14 }; 15 function setSinglePixel(imageData, dataIndex, colors, colorIndex) { 16 const color = BINDING.mono_array_get(colors, colorIndex); 17 imageData.data[dataIndex] = color & 0xff; 18 imageData.data[dataIndex + 1] = (color >> 8) & 0xff; 19 imageData.data[dataIndex + 2] = (color >> 16) & 0xff; 20 imageData.data[dataIndex + 3] = 255; 21 } 22 // Build the image from top to bottom, left to right if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { 1 // JS receives an array with 4 bytes per item 2 for (var i = 0; i < (width * height) / 4; i += 1) { 3 // Gets the array sent from C# 4 const screenDataItem = BINDING.mono_array_get(screenData, i); 5 for (var mask = 0; mask <= 24; mask += 8) { 6 let dataIndex = y * (width * 4) + x; 7 setSinglePixel(imageData, dataIndex, colors, 8 (screenDataItem >> mask) & 0xff); 9 10 11 } 12 } 13 context.putImageData(imageData, 0, 0); 14 }; 15 function setSinglePixel(imageData, dataIndex, colors, colorIndex) { 16 const color = BINDING.mono_array_get(colors, colorIndex); 17 imageData.data[dataIndex] = color & 0xff; 18 imageData.data[dataIndex + 1] = (color >> 8) & 0xff; 19 imageData.data[dataIndex + 2] = (color >> 16) & 0xff; 20 imageData.data[dataIndex + 3] = 255; 21 } 22 let dataIndex = y * (width * 4) + x; setSinglePixel(imageData, dataIndex, colors, function setSinglePixel(imageData, dataIndex, colors, colorIndex) { const color = BINDING.mono_array_get(colors, colorIndex); imageData.data[dataIndex] = color & 0xff; imageData.data[dataIndex + 1] = (color >> 8) & 0xff; imageData.data[dataIndex + 2] = (color >> 16) & 0xff; imageData.data[dataIndex + 3] = 255; } window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { 1 // JS receives an array with 4 bytes per item 2 for (var i = 0; i < (width * height) / 4; i += 1) { 3 // Gets the array sent from C# 4 const screenDataItem = BINDING.mono_array_get(screenData, i); 5 for (var mask = 0; mask <= 24; mask += 8) { 6 7 8 (screenDataItem >> mask) & 0xff); 9 // Build the image from top to bottom, left to right 10 if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } 11 } 12 } 13 context.putImageData(imageData, 0, 0); 14 }; 15 16 17 18 19 20 21 22 window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { // JS receives an array with 4 bytes per item for (var i = 0; i < (width * height) / 4; i += 1) { // Gets the array sent from C# const screenDataItem = BINDING.mono_array_get(screenData, i); for (var mask = 0; mask <= 24; mask += 8) { let dataIndex = y * (width * 4) + x; setSinglePixel(imageData, dataIndex, colors, (screenDataItem >> mask) & 0xff); // Build the image from top to bottom, left to right if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } } } context.putImageData(imageData, 0, 0); }; function setSinglePixel(imageData, dataIndex, colors, colorIndex) { const color = BINDING.mono_array_get(colors, colorIndex); imageData.data[dataIndex] = color & 0xff; imageData.data[dataIndex + 1] = (color >> 8) & 0xff; imageData.data[dataIndex + 2] = (color >> 16) & 0xff; imageData.data[dataIndex + 3] = 255; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Frame rendering // Somwhere in the Doom Engine's graphics module var args = new object[] { screen.Data, colors, 320, 200 }; // Send the frame buffer to JS DoomApplication.WebAssemblyJSRuntime.InvokeUnmarshalled<byte[], uint[], int> ("renderWithColorsAndScreenDataUnmarshalled", screen.Data, colors); 1 2 3 4 5
  59. window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { // JS receives an

    array with 4 bytes per item for (var i = 0; i < (width * height) / 4; i += 1) { // Gets the array sent from C# const screenDataItem = BINDING.mono_array_get(screenData, i); for (var mask = 0; mask <= 24; mask += 8) { let dataIndex = y * (width * 4) + x; setSinglePixel(imageData, dataIndex, colors, (screenDataItem >> mask) & 0xff); // Build the image from top to bottom, left to right if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } } } context.putImageData(imageData, 0, 0); }; function setSinglePixel(imageData, dataIndex, colors, colorIndex) { const color = BINDING.mono_array_get(colors, colorIndex); imageData.data[dataIndex] = color & 0xff; imageData.data[dataIndex + 1] = (color >> 8) & 0xff; imageData.data[dataIndex + 2] = (color >> 16) & 0xff; imageData.data[dataIndex + 3] = 255; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // Gets the array sent from C# const screenDataItem = BINDING.mono_array_get(screenData, i); window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { 1 // JS receives an array with 4 bytes per item 2 for (var i = 0; i < (width * height) / 4; i += 1) { 3 4 5 for (var mask = 0; mask <= 24; mask += 8) { 6 let dataIndex = y * (width * 4) + x; 7 setSinglePixel(imageData, dataIndex, colors, 8 (screenDataItem >> mask) & 0xff); 9 // Build the image from top to bottom, left to right 10 if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } 11 } 12 } 13 context.putImageData(imageData, 0, 0); 14 }; 15 function setSinglePixel(imageData, dataIndex, colors, colorIndex) { 16 const color = BINDING.mono_array_get(colors, colorIndex); 17 imageData.data[dataIndex] = color & 0xff; 18 imageData.data[dataIndex + 1] = (color >> 8) & 0xff; 19 imageData.data[dataIndex + 2] = (color >> 16) & 0xff; 20 imageData.data[dataIndex + 3] = 255; 21 } 22 // Build the image from top to bottom, left to right if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { 1 // JS receives an array with 4 bytes per item 2 for (var i = 0; i < (width * height) / 4; i += 1) { 3 // Gets the array sent from C# 4 const screenDataItem = BINDING.mono_array_get(screenData, i); 5 for (var mask = 0; mask <= 24; mask += 8) { 6 let dataIndex = y * (width * 4) + x; 7 setSinglePixel(imageData, dataIndex, colors, 8 (screenDataItem >> mask) & 0xff); 9 10 11 } 12 } 13 context.putImageData(imageData, 0, 0); 14 }; 15 function setSinglePixel(imageData, dataIndex, colors, colorIndex) { 16 const color = BINDING.mono_array_get(colors, colorIndex); 17 imageData.data[dataIndex] = color & 0xff; 18 imageData.data[dataIndex + 1] = (color >> 8) & 0xff; 19 imageData.data[dataIndex + 2] = (color >> 16) & 0xff; 20 imageData.data[dataIndex + 3] = 255; 21 } 22 let dataIndex = y * (width * 4) + x; setSinglePixel(imageData, dataIndex, colors, function setSinglePixel(imageData, dataIndex, colors, colorIndex) { const color = BINDING.mono_array_get(colors, colorIndex); imageData.data[dataIndex] = color & 0xff; imageData.data[dataIndex + 1] = (color >> 8) & 0xff; imageData.data[dataIndex + 2] = (color >> 16) & 0xff; imageData.data[dataIndex + 3] = 255; } window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { 1 // JS receives an array with 4 bytes per item 2 for (var i = 0; i < (width * height) / 4; i += 1) { 3 // Gets the array sent from C# 4 const screenDataItem = BINDING.mono_array_get(screenData, i); 5 for (var mask = 0; mask <= 24; mask += 8) { 6 7 8 (screenDataItem >> mask) & 0xff); 9 // Build the image from top to bottom, left to right 10 if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } 11 } 12 } 13 context.putImageData(imageData, 0, 0); 14 }; 15 16 17 18 19 20 21 22 window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { // JS receives an array with 4 bytes per item for (var i = 0; i < (width * height) / 4; i += 1) { // Gets the array sent from C# const screenDataItem = BINDING.mono_array_get(screenData, i); for (var mask = 0; mask <= 24; mask += 8) { let dataIndex = y * (width * 4) + x; setSinglePixel(imageData, dataIndex, colors, (screenDataItem >> mask) & 0xff); // Build the image from top to bottom, left to right if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } } } context.putImageData(imageData, 0, 0); }; function setSinglePixel(imageData, dataIndex, colors, colorIndex) { const color = BINDING.mono_array_get(colors, colorIndex); imageData.data[dataIndex] = color & 0xff; imageData.data[dataIndex + 1] = (color >> 8) & 0xff; imageData.data[dataIndex + 2] = (color >> 16) & 0xff; imageData.data[dataIndex + 3] = 255; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => { // JS receives an array with 4 bytes per item for (var i = 0; i < (width * height) / 4; i += 1) { // Gets the array sent from C# const screenDataItem = BINDING.mono_array_get(screenData, i); for (var mask = 0; mask <= 24; mask += 8) { let dataIndex = y * (width * 4) + x; setSinglePixel(imageData, dataIndex, colors, (screenDataItem >> mask) & 0xff); // Build the image from top to bottom, left to right if (y >= height - 1) { y = 0; x += 4; } else { y += 1; } } } context.putImageData(imageData, 0, 0); }; function setSinglePixel(imageData, dataIndex, colors, colorIndex) { const color = BINDING.mono_array_get(colors, colorIndex); imageData.data[dataIndex] = color & 0xff; imageData.data[dataIndex + 1] = (color >> 8) & 0xff; imageData.data[dataIndex + 2] = (color >> 16) & 0xff; imageData.data[dataIndex + 3] = 255; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Frame rendering // Somwhere in the Doom Engine's graphics module var args = new object[] { screen.Data, colors, 320, 200 }; // Send the frame buffer to JS DoomApplication.WebAssemblyJSRuntime.InvokeUnmarshalled<byte[], uint[], int> ("renderWithColorsAndScreenDataUnmarshalled", screen.Data, colors); 1 2 3 4 5
  60. Tips and lessons learned Avoid Array.Copy on Big arrays (in

    .Net 5) Extensive logging from Blazor sloooows the app Calling Blazor from JS is very fast But has problems with certain data types Undocumented APIs removed in .Net 7 in favor of JS Interop
  61. Tips and lessons learned Avoid Array.Copy on Big arrays (in

    .Net 5) Extensive logging from Blazor sloooows the app Calling Blazor from JS is very fast But has problems with certain data types Undocumented APIs removed in .Net 7 in favor of JS Interop window.requestAnimationFrame allows to pace the frames Browsers require interaction with the page to play audio
  62. JS Interop in .net >= 7 Less intricate way to

    run .Net from JS (no components) More adapted to this case than Blazor export function setLocalStorage(todosJson) { window.localStorage.setItem('dotnet-wasm-todomvc', todosJson); } 1 2 3 static partial class Interop { [JSImport("setLocalStorage", "todoMVC/store.js")] internal static partial void _setLocalStorage(string json); } 1 2 3 4 5 public partial class MainJS { [JSExport] public static void OnHashchange(string url) { controller?.SetView(url); } } 1 2 3 4 5 6 7 8 const exports = await getAssemblyExports( getConfig().mainAssemblyName); exports.TodoMVC.MainJS.OnHashchange(document.location.hash); 1 2 3 Call JS from .Net Call .Net from JS
  63. Next steps Short term: Migrate to JS Interop Update to

    ManagedDoom V2 Middle term: Game music Test WADs othen than DOOM1 Long term / wish: PR this port to ManagedDoom