{"id":16786577,"url":"https://github.com/ashtonmeuser/godot-wasm-doom","last_synced_at":"2025-10-19T18:17:05.318Z","repository":{"id":226084821,"uuid":"767683197","full_name":"ashtonmeuser/godot-wasm-doom","owner":"ashtonmeuser","description":"Porting Doom to Godot in 34 Lines of GDScript","archived":false,"fork":false,"pushed_at":"2024-10-16T18:03:23.000Z","size":73764,"stargazers_count":18,"open_issues_count":0,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-10-19T13:38:06.537Z","etag":null,"topics":["doom","godot","wasm","webassembly"],"latest_commit_sha":null,"homepage":"","language":"GDScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ashtonmeuser.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-03-05T18:02:46.000Z","updated_at":"2024-10-16T18:03:26.000Z","dependencies_parsed_at":"2024-10-18T08:15:06.356Z","dependency_job_id":null,"html_url":"https://github.com/ashtonmeuser/godot-wasm-doom","commit_stats":null,"previous_names":["ashtonmeuser/godot-wasm-doom"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ashtonmeuser%2Fgodot-wasm-doom","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ashtonmeuser%2Fgodot-wasm-doom/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ashtonmeuser%2Fgodot-wasm-doom/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ashtonmeuser%2Fgodot-wasm-doom/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ashtonmeuser","download_url":"https://codeload.github.com/ashtonmeuser/godot-wasm-doom/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":221820901,"owners_count":16886225,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["doom","godot","wasm","webassembly"],"created_at":"2024-10-13T08:12:37.742Z","updated_at":"2025-10-19T18:17:05.310Z","avatar_url":"https://github.com/ashtonmeuser.png","language":"GDScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Porting Doom to Godot in 34 Lines of GDScript\n\n\u003cp align=\"center\"\u003e\n\u003cimg width=\"820\" alt=\"Godot Wasm Doom\" src=\"https://github.com/user-attachments/assets/2e29fc23-d591-4931-bffa-cc266f5e6de8\"\u003e\n\u003c/p\u003e\n\nUsing a [WebAssembly Doom port](https://diekmann.github.io/wasm-fizzbuzz/doom/) and the [Godot Wasm addon](https://github.com/ashtonmeuser/godot-wasm), the 1993 classic Doom can be run and rendered in the [Godot game engine](https://godotengine.org/).\n\nThis article documents the porting process. The resulting source code can be found [here](https://github.com/ashtonmeuser/godot-wasm-doom).\n\n## Background\n\nThe title of this article is somewhat misleading. The entirety of Doom cannot be implemented in a few dozen lines of GDScript. Instead, we'll be running and rendering a precompiled WebAssembly port of Doom within Godot.\n\nWebAssembly, or Wasm, is an incredibly powerful and interesting technology. Originally developed for the web, it is now finding use outside of the browser. The greatest advantages of Wasm, in my opinion, are the following:\n1. Incredible speed (approaching native implementation).\n1. Safe and sandboxed runtime environment.\n1. Compilation target for many languages (Rust, Go, C, etc.).\n\nThe [Godot Wasm FAQ](https://github.com/ashtonmeuser/godot-wasm/wiki/FAQs#why-would-i-use-wasm-in-godot) provides some more insight as to why one would want to use WebAssembly within Godot.\n\nTo take advantage of Wasm's benefits, I created the [Godot Wasm addon](https://github.com/ashtonmeuser/godot-wasm) for the Godot game engine. This addon enables compiling and initializing Wasm modules, accessing exported functions and globals from Godot, providing imports implemented in GDScript, and accessing Wasm module memory. The latest release as of this writing, [Godot Wasm v0.3.4](https://github.com/ashtonmeuser/godot-wasm/releases/tag/v0.3.4-godot-4), includes type inference for Wasm import and export functions. This makes the addon far more compatible with existing Wasm modules.\n\nThroughout the creation of the Godot Wasm addon, one question kept nagging at me: **Can it run Doom?**\n\n## Acknowledgement\n\nThis project owes a huge thanks to the incredible work of [Cornelius Diekmann](https://github.com/diekmann). Their [WebAssembly from Scratch](https://github.com/diekmann/wasm-fizzbuzz) project provided great insight into the porting process, the underlying Doom Wasm module, and clear guidance on running the module. Diekmann's [Wasm Doom example](https://diekmann.github.io/wasm-fizzbuzz/doom/) will be referenced throughout this write-up.\n\nThis article is written in a similar fashion to Diekmann's with the hope that somebody finds it similarly educational.\n\n## Getting Started\n\nThe first thing we'll need is the Doom WebAssembly module. By inspecting the sources of [Diekmann's Wasm Doom example](https://diekmann.github.io/wasm-fizzbuzz/doom/), we can find and download the *doom.wasm* module (available [here](https://diekmann.github.io/wasm-fizzbuzz/doom/doom.wasm)).\n\nNext, we'll need to install the [Godot game engine](https://godotengine.org/download/). Version 4.3 was used for this project.\n\nNow we'll need to install the [Godot Wasm addon](https://github.com/ashtonmeuser/godot-wasm). This is available via the [Godot Asset Library](https://godotengine.org/asset-library/asset/3401). Further instructions regarding getting started with the Godot Wasm addon can be found [here](https://github.com/ashtonmeuser/godot-wasm/wiki/Getting-Started#installation). Version 0.3.8 was used for this project.\n\nWith the addon installed, let's create a simple Godot project. Open Godot, create a new project, add a single [`MarginContainer` node](https://docs.godotengine.org/en/stable/classes/class_margincontainer.html), and anchor it as a Full Rect to occupy the entire view. When first running the project, you'll need to confirm that this scene is to be used as the main scene.\n\n\u003cp align=\"center\"\u003e\n\u003cimg width=\"896\" alt=\"Create main container\" src=\"https://github.com/ashtonmeuser/godot-wasm-doom/assets/7253863/085c8d1d-0897-4568-aa75-c1bcda98ca40\"\u003e\n\u003c/p\u003e\n\nCopy the downloaded *doom.wasm* file to the root directory of your Godot project.\n\nLet's dive into the code. Attach a script to your `MarginContainer` and save it as *Main.gd*. Create an [`@onready`](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html#onready-annotation) variable that will hold our Godot Wasm `Wasm` instance (see [`Wasm` class documentation](https://github.com/ashtonmeuser/godot-wasm/wiki/Class-Documentation:-Wasm)).\n\n```gdscript\n@onready var wasm = Wasm.new()\n```\n\nIn our `_ready()` function, we'll need to load and compile the Wasm module binary (see [Usage guide](https://github.com/ashtonmeuser/godot-wasm/wiki/Getting-Started#usage)).\n\n```gdscript\nvar file = FileAccess.get_file_as_bytes(\"res://doom.wasm\")\nwasm.compile(file)\n```\n\nRun the project and ensure there are no errors thrown.\n\nInspecting the *doom.wasm* module allows us to gain a better understanding of the exposed API. In the terminal, a similar inspection can be performed with the [Wasmer CLI](https://docs.wasmer.io/install) via `wasmer inspect doom.wasm`.\n\n```gdscript\nvar info = wasm.inspect()\nprint(info)\n```\n\nThis should print the following (formatted for clarity).\n\n```json\n{\n   \"import_functions\": {\n      \"js.js_console_log\": [[2, 2], []],\n      \"js.js_draw_screen\": [[2], []],\n      \"js.js_milliseconds_since_start\": [[], [2]],\n      \"js.js_stderr\": [[2, 2], []],\n      \"js.js_stdout\": [[2, 2], []]\n   },\n   \"export_globals\": {\n      \"__data_end\": [2, false],\n      \"__heap_base\": [2, false]\n   },\n   \"export_functions\": {\n      \"I_FinishUpdate\": [[], []],\n      \"I_GetTime\": [[], [2]],\n      \"I_InitGraphics\": [[], []],\n      \"I_ReadScreen\": [[2], []],\n      \"I_SetPalette\": [[2], []],\n      \"I_ShutdownGraphics\": [[], []],\n      \"I_StartFrame\": [[], []],\n      \"I_StartTic\": [[], []],\n      \"I_UpdateNoBlit\": [[], []],\n      \"___errno_location\": [[], [2]],\n      \"__fpclassifyl\": [[2, 2], [2]],\n      \"__lock\": [[2], []],\n      \"__lockfile\": [[2], [2]],\n      \"__signbitl\": [[2, 2], [2]],\n      \"__stdio_close\": [[], []],\n      \"__stdio_seek\": [[], []],\n      \"__syscall3\": [[2, 2, 2, 2], [2]],\n      \"__toread\": [[2], [2]],\n      \"__uflow\": [[2], [2]],\n      \"__unlock\": [[2], []],\n      \"__unlockfile\": [[2], []],\n      \"access\": [[2, 2], [2]],\n      \"add_browser_event\": [[2, 2], []],\n      \"close\": [[2], [2]],\n      \"copysignl\": [[2, 2, 2, 2, 2], []],\n      \"doom_loop_step\": [[], []],\n      \"exit\": [[2], []],\n      \"fabsl\": [[2, 2, 2], []],\n      \"fmodl\": [[2, 2, 2, 2, 2], []],\n      \"fopen\": [[2, 2], [2]],\n      \"free\": [[2], []],\n      \"frexpl\": [[2, 2, 2, 2], []],\n      \"fstat\": [[2, 2], [2]],\n      \"getenv\": [[2], [2]],\n      \"lseek\": [[2, 2, 2], [2]],\n      \"main\": [[2, 2], [2]],\n      \"malloc\": [[2], [2]],\n      \"mbrtowc\": [[2, 2, 2, 2], [2]],\n      \"mbsinit\": [[2], [2]],\n      \"open\": [[2, 2, 2], [2]],\n      \"read\": [[2, 2, 2], [2]],\n      \"realloc\": [[2, 2], [2]],\n      \"scalbn\": [[3, 2], [3]],\n      \"scalbnl\": [[2, 2, 2, 2], []],\n      \"strerror\": [[2], [2]],\n      \"usleep\": [[2], [2]],\n      \"wctomb\": [[2, 2], [2]],\n      \"write\": [[2, 2, 2], [2]]\n   },\n   \"memory\": {\n      \"min\": 6684672,\n      \"max\": 4294901760,\n      \"import\": true\n   }\n}\n```\n\nThat's a lot of information! Fret not; we'll be ignoring most of this. For now, take note of the `import_functions` and `memory` properties.\n\nLet's forge ahead and try to instantiate the Wasm module.\n\n```gdscript\nwasm.instantiate({})\n```\n\nYou should see the following error:\n\n\u003e Main.gd:10 @ _ready(): Godot Wasm: Missing import function js.js_console_log  \n\u0026lt;C++ Source\u003e   src/godot-wasm.cpp:330 @ instantiate()  \n\u0026lt;Stack Trace\u003e  Main.gd:10 @ _ready()\n\nInstantiation of our Wasm module is failing because we're not providing the expected imports.\n\n## Satisfying Imports\n\nReferring back to the inspection of the module, we can see five import functions. Additionally, note that the module requires a memory import.\n\nBy viewing the *main.js* source of Diekmann's example, we can confirm that the following import functions are provided:\n1. `js.js_console_log`\n1. `js.js_stdout`\n1. `js.js_stderr`\n1. `js.js_milliseconds_since_start`\n1. `js.js_draw_screen`\n\nThe object returned by `wasm.inspect()` represents each import function signature as an array of two arrays. The first array represents the parameter types, and the second represents the return values. Empty arrays represent no function parameters and a `void` return type, respectively. The types use Godot's [`Variant.Type` enumeration](https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#enum-globalscope-variant-type). As an example, the `js.js_console_log` import function takes two integers as arguments and returns no value.\n\nFirst, we'll satisfy function imports with stubbed-out functions. In your *Main.gd* file, add the following functions:\n\n```gdscript\nfunc console_log(offset, length):\n\tprint(\"console_log: %s %s\" % [offset, length])\n\nfunc stdout(offset, length):\n\tprint(\"stdout: %s %s\" % [offset, length])\n\nfunc stderr(offset, length):\n\tprint(\"stderr: %s %s\" % [offset, length])\n\nfunc milliseconds_since_start():\n\tprint(\"milliseconds_since_start\")\n\treturn 0\n\nfunc draw_screen(offset):\n\tprint(\"draw_screen: %s\" % offset)\n```\n\nThese functions don't implement the logic they're expected to yet. However, we'll at least be able to tell when the Wasm module is calling an imported function.\n\nLet's provide these functions to the module as imports during instantiation. Each import function is represented by an array containing a Godot `Object` and the name of the method to call. We're using `self` as the target `Object` because each of the targeted methods is defined in the same file. Once again, in `_ready()`, add the following:\n\n```gdscript\nvar imports = {\n\t\"functions\": {\n\t\t\"js.js_console_log\": [self, \"console_log\"],\n\t\t\"js.js_draw_screen\": [self, \"draw_screen\"],\n\t\t\"js.js_milliseconds_since_start\": [self, \"milliseconds_since_start\"],\n\t\t\"js.js_stdout\": [self, \"stdout\"],\n\t\t\"js.js_stderr\": [self, \"stderr\"]\n\t}\n}\nwasm.instantiate(imports)\n```\n\nWe should now see a new error.\n\n\u003e Main.gd:17 @ _ready(): Godot Wasm: Missing import memory  \n\u0026lt;C++ Source\u003e   src/godot-wasm.cpp:348 @ instantiate()  \n\u0026lt;Stack Trace\u003e  Main.gd:17 @ _ready()\n\nWe'll now need to satisfy the memory import requirement. WebAssembly modules can either define their own internal (often exported) memory, which is created automatically by the runtime on instantiation, or import an external memory resource. In the case of our *doom.wasm* module, the latter applies.\n\nCreate another `@onready` variable at the top level of your main script.\n\n```gdscript\n@onready var memory = WasmMemory.new()\n```\n\nThe external memory resource must meet some minimum size requirements. Referring back to our inspection, we can see that our memory must be a minimum of 6684672 bytes in size. Wasm module memory is not typically dealt with in bytes but rather *pages*, equivalent to 65536 bytes each. With this in mind, our memory resource must be a minimum of 102 (6684672 / 65536) pages. See the [Memory Operations guide](https://github.com/ashtonmeuser/godot-wasm/wiki/Memory-Operations) and [`WasmMemory` class documentation](https://github.com/ashtonmeuser/godot-wasm/wiki/Class-Documentation:-WasmMemory) for more information.\n\nLet's expand our memory resource. We'll follow Diekmann's example and allocate 108 pages of memory. In `_ready()` and before instantiating our module, add the following:\n\n```gdscript\nmemory.grow(108)\n```\n\nYou can confirm the size of the memory via `memory.inspect()`.\n\nFinally, let's modify our import object to include the memory and instantiate the module.\n\n```gdscript\nvar imports = {\n\t\"functions\": {\n\t\t\"js.js_console_log\": [self, \"console_log\"],\n\t\t\"js.js_draw_screen\": [self, \"draw_screen\"],\n\t\t\"js.js_milliseconds_since_start\": [self, \"milliseconds_since_start\"],\n\t\t\"js.js_stdout\": [self, \"stdout\"],\n\t\t\"js.js_stderr\": [self, \"stderr\"]\n\t},\n\t\"memory\": memory\n}\nwasm.instantiate(imports)\n```\n\nThe script should run without throwing any errors.\n\nAs an aside, the compilation and instantiation steps can be completed in one call with the [`load()` method](https://github.com/ashtonmeuser/godot-wasm/wiki/Class-Documentation:-Wasm#error-load--packedbytearray-bytecode-dictionary-imports-), which takes both the binary and import object as arguments.\n\n```gdscript\nwasm.load(file, imports)\n```\n\n## Initialize Doom, Part 1\n\nLet's attempt to get Doom running. Referring to the Wasm module inspection from earlier, take note of the exported function `main()`. Sure enough, Diekmann's implementation calls this function first to initialize Doom. It takes two integers as arguments and returns an integer. The argument values go unused, and we'll ignore the return value.\n\nTo call a Wasm export function, use the [`function()` method](https://github.com/ashtonmeuser/godot-wasm/wiki/Class-Documentation:-Wasm#variant-function--string-name-array-args-). An array containing our arguments must be provided.\n\n```gdscript\nwasm.function(\"main\", [0, 0])\n```\n\n\u003e [!Note]\n\u003e The following section documents overcoming a since-fixed error with the Wasmer runtime (see https://github.com/wasmerio/wasmer/issues/4565). With the release of Wasmer [v4.3.5](https://github.com/wasmerio/wasmer/releases/tag/v4.3.5) and Godot Wasm [v0.3.7](https://github.com/ashtonmeuser/godot-wasm/releases/tag/v0.3.7-godot-4), this issue can be ignored. Doom is compatible with Godot Wasm used as either a [Godot addon](https://godotengine.org/asset-library/asset/2535) or Godot module and using either the Wasmer or Wasmtime runtimes. Skip to [Implement Logging Imports](#implement-logging-imports) to continue porting Doom.\n\n\u003cdetails\u003e\n\n\u003csummary\u003eDebugging Wasmer Runtime Error\u003c/summary\u003e\n\n### Wasmer Runtime Error\n\nWe should see some output as well as an error thrown.\n\n\u003e console_log: 7077952 59  \nconsole_log: 7078072 55\n\n\u003e Main.gd:21 @ _ready(): Godot Wasm: Failed calling function main  \n\u0026lt;C++ Source\u003e   src/godot-wasm.cpp:458 @ function()  \n\u0026lt;Stack Trace\u003e  Main.gd:21 @ _ready()\n\nOur `main()` function failed; let's dive a little deeper.\n\nImplement logging as outlined in [Implement Logging Imports](#implement-logging-imports).\n\nAfter implementing logging and running the project again, the expected data is printed to the console before `main()` fails.\n\n\u003e Hello, World, from JS Console! Answer=42 (101010 in binary)  \nHello, world from rust! 🦀🦀🦀 (println! working)\n\nReferencing Diekmann's example, we see the first two lines match. We can surmise that the error is happening before the following expected next line.\n\n\u003e Starting D_DoomMain\n\n### Fruitlessly Debugging\n\nWe're expecting to see logs from the initialization of Doom. Firstly, we expect to see \"Starting D_DoomMain\". Let's investigate the cause of the `main()` function invocation error.\n\nAt this point, we have to get into the gritty details of the [Godot Wasm addon](https://github.com/ashtonmeuser/godot-wasm). We'll abandon the prepackaged addon from the Asset Library and compile the addon ourselves in order to debug.\n\nClone the repo and follow Godot Wasm's [Development guide](https://github.com/ashtonmeuser/godot-wasm/wiki/Development).\n\nLet's modify the addon source. In *src/godot-wasm.cpp*, modify the `function()` method (permalink [here](https://github.com/ashtonmeuser/godot-wasm/blob/7ad22dd84ff05c99deac2b9620c8bfa60aaa2fc1/src/godot-wasm.cpp#L426)) to the following:\n\n```cpp\nwasm_trap_t* trap = wasm_func_call(func, \u0026f_args, \u0026f_results);\nif (trap) {\n  wasm_message_t message;\n  wasm_trap_message(trap, \u0026message);\n  PRINT_ERROR(message.data);\n  wasm_trap_delete(trap);\n}\n```\n\nThe above conditional reads the message from a returned `wasm_trap_t` pointer. Hopefully, this message provides some clarity.\n\nRebuild the Godot Wasm addon following the wiki Development guide.\n\n```sh\nscons target=template_release platform=linux\n```\n\nPopulate the addon in the Godot Wasm Doom project (found in *GODOT_DOOM_DIR/addons/godot-wasm*) with the binaries generated by the Godot Wasm build step (found in *GODOT_WASM_DIR/addons/godot-wasm*). It may be convenient to create a symbolic link from the Doom project to the addon project to automatically get updated binaries. This can be accomplished on UNIX platforms by deleting the existing addons directory in the Doom project and running `ln -s PATH/TO/GODOT_WASM_DIR/addons PATH/TO/GODOT_DOOM_DIR/addons`.\n\nRunning the project again results in a new error being logged.\n\n\u003e Main.gd:21 @ _ready(): Godot Wasm: out of bounds memory access  \n\u0026lt;C++ Source\u003e   src/godot-wasm.cpp:463 @ function()  \n\u0026lt;Stack Trace\u003e  Main.gd:21 @ _ready()\n\nThe Wasm module or underlying runtime seems to be accessing memory outside of the allocated bounds.\n\nLet's try allocating more memory. Using `memory.grow()`, allocate an assortment of memory sizes up to the maximum of 65536 pages. All values fail, with the final, largest value producing the following error:\n\n\u003e Main.gd:21 @ _ready(): Godot Wasm: unreachable  \n\u0026lt;C++ Source\u003e   src/godot-wasm.cpp:463 @ function()  \n\u0026lt;Stack Trace\u003e  Main.gd:21 @ _ready()\n\nNo luck. Revert the memory size to 108 pages. Unfortunately, the error messages do not provide many clues to go on. However, we've got another trick up our sleeve.\n\n### Changing the WebAssembly Runtime\n\nGodot Wasm supports both the [Wasmer](https://wasmer.io/) and [Wasmtime](https://wasmtime.dev/) runtimes. By default, Wasmer is used. As of writing this (2024-03-06), the prepackaged Godot Wasm addon, e.g., via Godot Asset Library, uses the default Wasmer runtime.\n\nLet's go ahead and compile Godot Wasm again, this time using the Wasmtime runtime. Refer to the [Changing Runtime documentation](https://github.com/ashtonmeuser/godot-wasm/wiki/Development#changing-runtime). The following is compiling for Linux; replace `platform` as required, e.g., `windows`, `macos`.\n\n```sh\nscons target=template_release platform=linux wasm_runtime=wasmtime\n```\n\nUnless using a symbolic link between projects, package the addon binaries once again.\n\nNote that compiling with the Wasmtime runtime on Windows is failing static linking as of Godot Wasm v0.3.4. In addition to the built Godot Wasm binaries, you'll need to copy *wasmtime.dll* to *GODOT_DOOM_DIR/addons/godot-wasm/bin/windows* and to update *godot-wasm.gdextension* to include the Wasmtime DLL as a dependency. Refer to the [*addons* directory of the project source](https://github.com/ashtonmeuser/godot-wasm-doom/tree/b4e80759a08cf4078b7bfafcb9662ec99f3c5ad8/addons/godot-wasm). This issue is now captured in [godot-wasm#65](https://github.com/ashtonmeuser/godot-wasm/issues/65).\n\nRunning the Godot Wasm Doom project now produces a plethora of STDOUT output and no errors! Success!\n\nIt seems as though there may be a deficiency with the Wasmer runtime (to be explored further).\n\n\u003c/details\u003e\n\nOn initialization, Doom should print the following (truncated) logs:\n\n\u003estdout: 6423464 19  \nconsole_log: 7077952 59  \nconsole_log: 7078072 55  \nstdout: 6423464 19  \nstdout: 1047791 1  \nstdout: 5260422 20  \nstdout: 6423464 24  \n...  \nstdout: 1047456 24  \nstdout: 1047488 26  \nstderr: 1047440 29  \nstdout: 1047488 38  \nstdout: 1047488 26  \nconsole_log: 9469752 21\n\nThe module is trying to log text, but strings are not a native type exchangeable between the host (Godot) and guest (Wasm module). We'll need to interpret the data coming from the Wasm module.\n\n## Implement Logging Imports\n\nWhen calling our `main()` export function, the Wasm module called the `console_log()` and `stdout()` import functions. We'll implement some basic logging to aid in debugging.\n\nSome background regarding Wasm memory is important at this stage of the journey. WebAssembly memory is simply a contiguous buffer or array of bytes. As we saw earlier, this array can be expanded or grown. Memory is very important and frequently used with Wasm because of the limited API that can be exposed via import/export functions, also known as the Foreign Function Interface, or FFI. Only the following four fundamental data types can be directly exposed via Wasm import/export functions:\n- 32-bit integer\n- 64-bit integer\n- 32-bit floating point\n- 64-bit floating point\n\nThis begs the question: how do we transfer a string between Godot (the host) and the Wasm module (the guest)?\n\nThe answer is to take advantage of the module's memory. The Wasm module can write a string to memory in an agreed-upon format, and the host, i.e., Godot, can later read it. The host can be instructed where in memory to begin reading and how many bytes to read with simple integer values passed via import/export functions.\n\nGodot Wasm's `WasmMemory` class (see [`WasmMemory` class documentation](https://github.com/ashtonmeuser/godot-wasm/wiki/Class-Documentation:-WasmMemory)) inherits from Godot's own [`StreamPeer` class](https://docs.godotengine.org/en/stable/classes/class_streampeer.html) and closely mirrors Godot's [`StreamPeerBuffer` class](https://docs.godotengine.org/en/stable/classes/class_streampeerbuffer.html#class-streampeerbuffer). This allows us to easily read raw bytes in a variety of contexts.\n\nWe now have the context required to implement our logging functions. We've already seen that the `js.js_console_log` import function accepts two integers as arguments. These integers represent the data offset, i.e., starting point and the data length, respectively. We'll use these to read the data to be printed. Referring to Diekmann's example, we expect strings to be stored as UTF-8. Reimplement the `stdout` and `console_log` GDScript functions as follows:\n\n```gdscript\nfunc stdout(offset, length):\n\tmemory.seek(offset)\n\tvar message = memory.get_utf8_string(length)\n\tprint(message)\n\nfunc console_log(offset, length):\n\tstdout(offset, length) # Reuse stdout implementation\n```\n\nThe `seek()` method moves the cursor to a memory offset, while `get_utf8_string()` (inherited from `StreamPeer`) reads a UTF-8 string from raw bytes.\n\n## Initialize Doom, Part 2\n\nRunning the project once again produces the following (truncated) logs:\n\n\u003e Hello, World, from JS Console! Answer=42 (101010 in binary)  \nHello, world from rust! 🦀🦀🦀 (println! working)  \nStarting D_DoomMain  \nTriggering a printf  \nDoom's screen is 320x200  \nmallocing 12 bytes at 7078176  \n...  \nmallocing 140 bytes at 7078320  \nstartskill 2  deathmatch: 0  startmap: 1  startepisode: 1  \nplayer 1 of 1 (1 nodes)  \nS_Init: Setting up sound.  \nstderr: 1047440 29  \nHU_Init: Setting up heads up display.  \nST_Init: Init status bar.  \nI_InitGraphics (TODO)\n\nUsing Diekmann's example as our guide, this is exactly the output we're expecting. Doom is successfully initializing!\n\n## Implementing Additional Imports\n\nIn the above logs, we can see one call to the unimplemented `stderr()` import function.\n\n\u003e stderr: 1047440 29\n\nLet's go ahead and implement a simple error logging function similar to what we did for `console_log()` and `stdout()` to properly satisfy the `js.js_stderr` import function. We'll push error messages printed from Doom to Godot's warning-level logging. This will draw attention to Doom errors while reserving Godot's error-level logging for critical errors encountered while running the Wasm module.\n\n```gdscript\nfunc stderr(offset, length):\n\tmemory.seek(offset)\n\tvar message = memory.get_utf8_string(length)\n\tpush_warning(message)\n```\n\nRunning the project again should produce a single warning.\n\n\u003e Main.gd:42 @ stderr(): S_Init: default sfx volume 8  \n\u0026lt;C++ Source\u003e   core/variant/variant_utility.cpp:1111 @ push_warning()  \n\u0026lt;Stack Trace\u003e  Main.gd:42 @ stderr()  \nMain.gd:21 @ _ready()\n\nDoom threw an error relating to sound effects. Sound effects were not implemented in this port of Doom, so we'll go ahead and ignore this supposed error.\n\nNext, let's implement the `js.js_milliseconds_since_start` import function. In the *Main.gd* GDScript file, modify the `milliseconds_since_start()` method to the following:\n\n```gdscript\nfunc milliseconds_since_start():\n\treturn Time.get_ticks_msec()\n```\n\nThis uses the [`Time` singleton](https://docs.godotengine.org/en/stable/classes/class_time.html) to return the number of milliseconds since the program was started.\n\nAs an aside, we can shorten our script by passing the `Time` singleton object and the `get_ticks_msec()` method name directly as the import. We can now delete our custom `milliseconds_since_start()` method.\n\n```gdscript\nvar imports = {\n\t\"functions\": {\n\t\t...\n\t\t\"js.js_milliseconds_since_start\": [Time, \"get_ticks_msec\"],\n\t\t...\n\t}\n}\n```\n\n## Calling the Game Loop\n\nReferring once again to Diekmann's example, we can see that there are only three export functions used.\n1. `main`\n1. `doom_loop_step`\n1. `add_browser_event`\n\nLet's proceed to calling the main Doom game loop. At the end of the `_ready()` function in your *Main.gd* script, call the `doom_loop_step()` export function. Because, as we saw in the inspection, the `doom_loop_step()` export function accepts no arguments, we can omit the arguments array.\n\n```gdscript\nwasm.function(\"doom_loop_step\")\n```\n\nRunning the project, we should see exactly one new log line appear.\n\n\u003e BASETIME initialized to 472\n\nIf we still had a `print()` call in our `milliseconds_since_start()`, we'd see that the Doom main loop function invoked the `js.js_milliseconds_since_start` import function. This crossed the FFI barrier from the Wasm module (guest) to Godot (host) and ingested the returned value. The printed value (in this case, 472) is the number of milliseconds that Godot's `Time` singleton has recorded since the program was started.\n\nThe game loop should not just be called once. Rather, it should be called repeatedly, ticking the game along with each invocation. Let's use Godot's `_process()` method to call Doom's game loop. Remove the call to `doom_loop_step` in `_ready()` and add the following to your script:\n\n```gdscript\nfunc _process(_delta):\n\twasm.function(\"doom_loop_step\")\n```\n\nRunning the project again, we should see repeated calls to the `js.js_draw_screen` import function.\n\n\u003e ST_Init: Init status bar.  \nI_InitGraphics (TODO)  \nBASETIME initialized to 1262  \ndraw_screen: 5278204  \ndraw_screen: 5278204  \ndraw_screen: 5278204  \ndraw_screen: 5278204  \n...\n\nWe're ready to implement our final (and most complex) import function, `js.js_draw_screen`!\n\n## Rendering Doom\n\nPer Diekmann's write-up, there is a custom rendering shim applied on top of classic Doom implemented in Rust. This simplifies our drawing/rendering implementation substantially, as it is outputting standard [RGBA8888](https://en.wikipedia.org/wiki/RGBA_color_model#RGBA8888) (also called RGBA8). This format consists of four color channels, each with a bit depth of one byte or eight bits. Godot supports this format via the [`FORMAT_RGBA8` flag](https://docs.godotengine.org/en/stable/classes/class_image.html#enumerations).\n\nWe know from Doom's logs that the screen dimensions are defined as 320x200.\n\nWe're finally getting to some graphical work in Godot. Click the 2D view and ensure the previously added `MarginContainer` is set to anchor mode Full Rect. Add a [`TextureRect`](https://docs.godotengine.org/en/stable/classes/class_texturerect.html) to the `MarginContainer`. Select the `TextureRect`, and on the right side of the screen, set the *Texture* property with a new [`ImageTexture`](https://docs.godotengine.org/en/stable/classes/class_imagetexture.html).\n\n\u003cp align=\"center\"\u003e\n\u003cimg width=\"1092\" alt=\"Add TextureRect\" src=\"https://github.com/ashtonmeuser/godot-wasm-doom/assets/7253863/ed54c6d3-8fcb-4c9e-8837-4d253dd6dfb8\"\u003e\n\u003c/p\u003e\n\nBack to the code. We'll need to instantiate an [`Image`](https://docs.godotengine.org/en/stable/classes/class_image.html) that holds the graphical data created by Doom. The image's data will be flashed to the `ImageTexture` created above. At the top of *Main.gd*, with the `@onready` variables, add the following:\n\n```gdscript\nvar image = Image.new()\n```\n\nWe'll need to create our `Image` and set it as the image value of the `ImageTexture`. Add the following anywhere in the `_ready()` function:\n\n```gdscript\nimage = Image.create(320, 200, false, Image.FORMAT_RGBA8)\n$TextureRect.texture.set_image(image)\n```\n\nWe're now ready to draw the screen. Taking a look at `js.js_draw_screen`, we see that a single integer parameter is provided. This value points to the offset in memory at which graphical data begins. No length parameter is provided, as we can calculate the required space manually based on screen size, color channels, and channel bit depth (in bytes). We should anticipate reading SCREEN_SIZE × N_CHANNELS × BIT_DEPTH bytes, or 320 × 200 × 4 × 1 = 256,000 bytes.\n\nAs with retrieving strings from Wasm memory, we'll need to `seek()` to the correct offset in memory and read the data using one of the methods provided by the `StreamPeer` interface. This time, instead of reading a UTF-8 string, we'll read the raw bytes from memory as an array. The [`get_data()` method](https://docs.godotengine.org/en/stable/classes/class_streampeer.html#class-streampeer-method-get-data) is perfect for this. Take note that the return value of `get_data` is an array with two values: an error code and the raw data array itself. Replace our placeholder `draw_screen()` method with the following:\n\n```gdscript\nfunc draw_screen(offset):\n\tmemory.seek(offset)\n\tvar data = memory.get_data(320 * 200 * 4)\n\timage.set_data(320, 200, false, Image.FORMAT_RGBA8, data[1])\n\t$TextureRect.texture.update(image)\n```\n\nRunning the project, we'll get our first glimpse of Doom!\n\n\u003cp align=\"center\"\u003e\n\u003cimg width=\"688\" alt=\"First glimpse of Doom\" src=\"https://github.com/ashtonmeuser/godot-wasm-doom/assets/7253863/1698e3b2-9541-4162-8749-8644bfa7d90a\"\u003e\n\u003c/p\u003e\n\nSomething doesn't look quite right. In debugging this graphical issue with reference to Diekmann's example, it's clear that we should be using screen dimensions of 640x400. I'm not entirely sure where the discrepancy between the reported and actual resolutions comes from. Modify the Image creation and `draw_screen()` methods to reflect the new 640x400 resolution. Run the project.\n\n\u003cp align=\"center\"\u003e\n\u003cimg width=\"688\" alt=\"Fixed Doom rendering\" src=\"https://github.com/ashtonmeuser/godot-wasm-doom/assets/7253863/90bc600d-16d4-4aaf-8b93-6a95921cd5bd\"\u003e\n\u003c/p\u003e\n\nSuccess! Doom should be correctly displayed in all its pixelated glory, slowly cycling through three title/demo screens.\n\n## Keyboard Input\n\nWe've implemented all import functions. The final export function, `add_browser_event`, remains. This function is used to forward keyboard input to the Wasm module.\n\nAs indicated by `wasm.inspect()`, the `add_browser_event()` export function receives two integer arguments and returns void. The arguments are as follows:\n1. A boolean (represented as an integer) that denotes whether a key was released or pressed. This boolean is misleading in that a value of `false` or 0 represents a key pressed, while `true` or 1 represents a key released.\n1. An integer key code that denotes the key that was pressed or released.\n\nLet's grab some input. First, let's create a simple placeholder function used to explore input events in Godot.\n\n```gdscript\nfunc _input(event):\n\tif event is InputEventKey and !event.is_echo():\n\t\tvar pressed = event.is_pressed()\n\t\tvar keycode = event.keycode\n\t\tprint(\"Keycode %s pressed? %s\" % [keycode, pressed])\n```\n\nThe [`_input()` method](https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-private-method-input) is called on frames during which input was detected. We're first making sure that the event is a keyboard event that was not [echoed](https://docs.godotengine.org/en/stable/tutorials/inputs/controllers_gamepads_joysticks.html#echo-events). Next, we're checking to see if the key was pressed or released. Running the program and pressing the Enter key will output the following:\n\n\u003e Keycode 4194309 pressed? true  \nKeycode 4194309 pressed? false\n\nThe key code value above is a bespoke Godot value based on the [`Key` enumeration](https://docs.godotengine.org/en/stable/classes/class_@globalscope.html#enumerations). Printing `Key.KEY_ENTER` will produce the same value. This value does not match that expected by Doom, which uses DOS key codes, e.g., 13 for Enter. We'll need to map expected keys from Godot's values to Doom's. Informed by Diekmann's example, let's include a simple, incomplete `Dictionary` to map key codes at the top of *Main.gd*. Each `Dictionary` key represents a Godot key code, while their values represent the corresponding Doom/DOS key codes.\n\n```gdscript\nvar keys = { KEY_ENTER: 13, KEY_BACKSPACE: 127, KEY_SPACE: 32, KEY_LEFT: 0xac, KEY_RIGHT: 0xae, KEY_UP: 0xad, KEY_DOWN: 0xaf, KEY_CTRL: 0x80+0x1d, KEY_ALT: 0x80+0x38, KEY_ESCAPE: 27, KEY_TAB: 9, KEY_SHIFT: 16 }\n```\n\nThis captures many of the required keys but notably misses numeric, alphabetic, and function keys.\n\nLet's modify our `_input()` method to display the mapped keys. We will map all unknown keys to key code 0, which Doom ultimately ignores. We'll also invert the value of `is_pressed()` and cast it to an integer to match Doom's API.\n\n```gdscript\nfunc _input(event):\n\tif event is InputEventKey and !event.is_echo():\n\t\tvar pressed = int(!event.is_pressed())\n\t\tvar keycode = keys.get(event.keycode, 0)\n\t\tprint(\"Keycode %s pressed? %s\" % [keycode, pressed])\n```\n\nRunning the program and pressing the Enter key will now produce the following:\n\n\u003e Keycode 13 pressed? 0  \nKeycode 13 pressed? 1\n\nFinally, let's forward that mapped value to Doom via the `add_browser_event` export function.\n\n```gdscript\nfunc _input(event):\n\tif event is InputEventKey and !event.is_echo():\n\t\tvar pressed = int(!event.is_pressed())\n\t\tvar keycode = keys.get(event.keycode, 0)\n\t\twasm.function(\"add_browser_event\", [pressed, keycode])\n```\n\nWhen running the program, you should now be able to interact with Doom! Note that we're using the original [Doom keybinds](https://web.archive.org/web/https://www.starehry.eu/download/action3d/docs/Doom-Manual.pdf), e.g., CTRL: shoot, Space: use/open, Enter: select.\n\n\u003cp align=\"center\"\u003e\n\u003cimg width=\"688\" alt=\"Interacting with Doom\" src=\"https://github.com/ashtonmeuser/godot-wasm-doom/assets/7253863/fe5d2d1f-c23c-4c42-9c97-623fcb4ac860\"\u003e\n\u003c/p\u003e\n\nLastly, let's implement the remaining keybinds. The following ranges of Godot key codes will be mapped to Doom's expected values.\n- [65, 90]: Alphabetic ASCII key codes that must be mapped to their lowercase selves, i.e., add 32.\n- [48, 57]: Numeric keys whose values can be passed straight through to Doom, i.e., Godot and DOS key codes values match.\n- [4194332, 4194343]: Function keys F1 through F12. These should be mapped to values 187 through 198.\n\nWe'll use some array mapping magic to map each range member to a `Dictionary` with a single key equal to the Godot key code and a corresponding value equal to the Doom/DOS key code. This format matches that defined by the previously declared `keys` variable. For each `Dictionary`, we can then use the [`merge()` method](https://docs.godotengine.org/en/stable/classes/class_dictionary.html#class-dictionary-method-merge) to include them in our mapping. Add the following anywhere in `_ready()`.\n\n```gdscript\nvar alphabetic = range(KEY_A, KEY_Z + 1).map(func(x): return { x: x + 32 })\nvar numeric = range(KEY_0, KEY_9 + 1).map(func(x): return { x: x })\nvar function = range(KEY_F1, KEY_F12 + 1).map(func(x): return { x: 187 + x - KEY_F1 })\nfor k in alphabetic + numeric + function:\n\tkeys.merge(k)\n```\n\nWith that, we should receive alphabetic, numeric, and function key events. We can test this out by running the program, starting a new game of Doom, and pressing the 1 key. Our character should switch to bare hands. Pressing 2 returns to the default weapon.\n\n## Wrapping Up\n\nDone and done. I hope this exploration was as entertaining and educational for you as it was for me. Additionally, I hope this convinces you of the incredible power of WebAssembly! Because of the low performance requirements of Wasm and the fact that we've rendered the game to a simple `ImageTexture`, this is highly adaptable. For example, imagine walking up to a virtual monitor running a fully-functional version of Doom inside a 3D game!\n\nSome additional steps are required to export this project. See the [Exporting guide](https://github.com/ashtonmeuser/godot-wasm/wiki/Exporting-Godot-Project) for more information.\n\nThe final resultant source code for this project is available [here](https://github.com/ashtonmeuser/godot-wasm-doom). Several of the functions defined above have been altered for brevity, although their logic remains the same.\n\n## Final Remarks\n\nAs a personal plug, please go star the [Godot Wasm](https://github.com/ashtonmeuser/godot-wasm) project on GitHub. Feel free to open an issue or PR!\n\nCall for aid: I'm by no means an expert with Windows and am struggling with statically linking the Wasmtime library as described by [godot-wasm#65](https://github.com/ashtonmeuser/godot-wasm/issues/65). I'd love a hand with this one!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fashtonmeuser%2Fgodot-wasm-doom","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fashtonmeuser%2Fgodot-wasm-doom","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fashtonmeuser%2Fgodot-wasm-doom/lists"}