Skip to content

Commit

Permalink
Visualization example (#16)
Browse files Browse the repository at this point in the history
* Interference

* Show info, cycle module & resolution, interact

Toggle FPS limit
New update function signature includes tick

* Game of life and wave sim

* Sort

* Readme update
  • Loading branch information
ashtonmeuser authored Apr 20, 2023
1 parent 5adda5d commit c18b7d9
Show file tree
Hide file tree
Showing 12 changed files with 298 additions and 5 deletions.
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ Once installed as an addon in a Godot project, the Godot Wasm addon class can be
### Exporting Godot Project
> **Note**
> Exporting to web/HTML5 using a GDNative addon is not supported by Godot. See [godotengine/godot#12243](https://github.com/godotengine/godot/issues/12243).
Exporting from Godot may require the following additional steps. See the export configuration of the [example Godot project](https://github.com/ashtonmeuser/godot-wasm/tree/master/examples) for a practical illustration.
1. For macOS exports, disable library validation in Project → Export → Options.
Expand All @@ -74,7 +77,7 @@ Note that, as mentioned in Godot's [StreamPeer documentation](https://docs.godo
### Wasm Consume (Godot)
A simple example of loading a Wasm module, displaying the structure of its exports, calling its exported functions. Some computationally-expensive benchmarks e.g. [prime sieve](https://en.wikipedia.org/wiki/Sieve_of_Atkin) in GDScript and Wasm can be compared. The Wasm module used is that generated by the [Wasm Create example](#wasm-create-assemblyscript). All logic for this Godot project exists in `Main.gd`.
A simple example of loading a Wasm module, displaying the structure of its imports and exports, calling its exported functions, and providing GDScript callbacks via import functions. Some computationally-expensive benchmarks e.g. [prime sieve](https://en.wikipedia.org/wiki/Sieve_of_Atkin) in GDScript and Wasm can be compared. The Wasm module used is that generated by the [Wasm Create example](#wasm-create-assemblyscript). All logic for this Godot project exists in `Main.gd`.
### Wasm Create (AssemblyScript)
Expand All @@ -94,6 +97,10 @@ From the example directory (*examples/wasm-create*), install or update Node modu
Note that WebAssembly is a large topic and thoroughly documenting the creation of Wasm modules is beyond the scope of this project. AssemblyScript is just one of [many ways](https://github.com/appcypher/awesome-wasm-langs#awesome-webassembly-languages-) to create a Wasm module.
### Wasm Visualizations
An example of displaying graphics generated by Wasm modules. Graphics are read directly from the Wasm module memory using the [StreamPeer](https://docs.godotengine.org/en/3.5/classes/class_streampeer.html) interface.
## Benchmarks
Comparison of GDScript, GDNative, and Wasm (n=1000, p95). The following benchmarks were run on macOS 12.6.3, 16GB RAM, 2.8 GHz i7. The project was exported to avoid GDScript slowdown likely caused by performance monitoring. Speedup figures for both GDNative and Wasm are relative to GDScript. The benchmarks used are 1) a [DP Nth Fibonacci number](https://www.geeksforgeeks.org/program-for-nth-fibonacci-number/) and 2) a [Sieve of Atkin](https://www.geeksforgeeks.org/sieve-of-atkin/).
Expand Down Expand Up @@ -126,11 +133,9 @@ If frequently iterating on the addon using a Godot project, it may help to creat
## Known Issues
1. No [WASI](https://wasmbyexample.dev/examples/wasi-introduction/wasi-introduction.all.en-us.html) bindings are provided to the Wasm module. This means that the guest Wasm module has no access to the host machines filesystem, etc. Pros for this are simplicity and increased security. Cons include not being able to generate truly random numbers (without a workaround) or run Wasm modules created in ways that require WASI bindings e.g. [TinyGo](https://tinygo.org/docs/guides/webassembly/) (see relevant [issue](https://github.com/tinygo-org/tinygo/issues/3068)).
1. No access to imported functions (see [roadmap](#Roadmap)).
1. No access to imported globals (see [roadmap](#Roadmap)).
1. Only `int` and `float` return values are supported. While workarounds could be used, this limitation is because the only [concrete types supported by Wasm](https://webassembly.github.io/spec/core/syntax/types.html#number-types) are integers and floating point.
1. A default empty `args` parameter for `function(name, args)` can not be supplied. Default `Array` parameters in GDNative seem to retain values between calls. Calling methods of this addon without expected arguments produces undefined behaviour. This is reliant on [#209](https://github.com/godotengine/godot-cpp/issues/209).
1. CPU usage has been noted to be quite high when running in the editor on macOS. This does not affect exported projects.
1. A default empty `args` parameter for `function(name, args)` can not be supplied. Default `Array` parameters in GDNative seem to retain values between calls. Calling methods of this addon without expected arguments produces undefined behaviour. This is reliant on [godotengine/godot-cpp#209](https://github.com/godotengine/godot-cpp/issues/209).
1. Web/HTML5 export fails when Godot Wasm is used as an addon (see [#15](https://github.com/ashtonmeuser/godot-wasm/issues/15)).
## Relevant Discussion
Expand Down
4 changes: 4 additions & 0 deletions examples/wasm-visualizations/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.DS_Store
.import/
.godot/

111 changes: 111 additions & 0 deletions examples/wasm-visualizations/Main.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
extends Node

const sizes: PoolVector2Array = PoolVector2Array([Vector2(50, 50), Vector2(100, 100), Vector2(200, 200), Vector2(400, 400), Vector2(800, 800), Vector2(1600, 1600)])
const modules: PoolStringArray = PoolStringArray(["interference", "mandelbrot", "life", "wave", "sort"])

var texture: ImageTexture = ImageTexture.new()
var image: Image = Image.new()
var module: String = modules[0]
var size: Vector2 = sizes[2]
var ticks: int = 0
onready var wasm: Wasm = Wasm.new()

func _ready():
$TextureRect.texture = texture
seed(OS.get_system_time_msecs())
_change_module()
_change_size()
_show_info()

func _input(event):
if event.is_action_pressed("ui_up"): _change("size", 1)
elif event.is_action_pressed("ui_down"): _change("size", -1)
elif event.is_action_pressed("ui_left"): _change("module", -1)
elif event.is_action_pressed("ui_right"): _change("module", 1)
elif event.is_action_pressed("toggle_fps_cap"): _change("fps_cap")

func _gui_input(event):
if !(event is InputEventMouseButton) or !event.pressed: return
if get_node_or_null("Intro"): $Intro.queue_free()
if !("interact" in wasm.inspect().export_functions): return
var p = _localize_aspect_fit(event.position, $TextureRect.rect_size, size)
if p.x < 0.0 or p.x > 1.0 or p.y < 0.0 or p.y > 1.0: return
wasm.function("interact", [p.x, p.y, 1.0 if event.button_index == BUTTON_LEFT else 0.0 ])

func _process(_delta):
$"%LabelFPS".text = "%d FPS" % Performance.get_monitor(Performance.TIME_FPS)
wasm.function("update", [ticks, OS.get_ticks_msec() / 1000.0])
ticks += 1

func _load_wasm(path: String):
var file = File.new()
file.open(path, File.READ)
var buffer = file.get_buffer(file.get_len())
var imports = { "functions": {
"env.abort": [self, "_abort"],
"env.draw_image": [self, "_draw_image"],
"env.draw_pixel": [self, "_draw_pixel"],
"env.seed": [self, "_seed"],
} }
wasm.load(buffer, imports)
file.close()

func _change(property: String, increment: int = 0):
if get_node_or_null("Intro"): $Intro.queue_free()
call_deferred("_change_%s" % property, increment)
call_deferred("_show_info")

func _change_module(increment: int = 0):
var index = modules.find(module) + increment
if index < 0 or index >= len(modules): return
module = modules[index]
_load_wasm("Modules/%s.wasm" % module)
wasm.function("resize", [int(size.x), int(size.y)])
ticks = 0

func _change_size(increment: int = 0):
var index = sizes.find(size) + increment
if index < 0 or index >= len(sizes): return
size = sizes[index]
wasm.function("resize", [int(size.x), int(size.y)])
image.create(int(size.x), int(size.y), false, Image.FORMAT_RGBA8)
ticks = 0

func _change_fps_cap(_increment: int = 0):
Engine.target_fps = 0 if Engine.target_fps else 60

func _show_info():
$"%LabelModule".text = module
$"%LabelSize".text = "%d X %d" % [size.x, size.y]
$Tween.remove_all()
$Tween.interpolate_property($Info, "modulate:a", $Info.modulate.a, 1.0, 0.2, Tween.TRANS_CUBIC, Tween.EASE_IN_OUT, 0.0)
$Tween.interpolate_property($Info, "rect_position:y", $Info.rect_position.y, 12, 0.2, Tween.TRANS_CUBIC, Tween.EASE_IN_OUT, 0.0)
$Tween.interpolate_property($Info, "modulate:a", 1.0, 0.0, 0.6, Tween.TRANS_CUBIC, Tween.EASE_IN_OUT, 6.0)
$Tween.interpolate_property($Info, "rect_position:y", 12, -98, 0.6, Tween.TRANS_CUBIC, Tween.EASE_IN_OUT, 6.0)
$Tween.start()

func _localize_aspect_fit(p: Vector2, outer: Vector2, inner: Vector2):
var scale = outer.y / inner.y if outer.aspect() > inner.aspect() else outer.x / inner.x
var scaled = inner * scale
var offset = (outer - scaled) / 2
return (p - offset) / scaled

# Wasm module imports

func _abort(a: int, b: int, c: int, d: int) -> void: # Throw error from Wasm module
push_error("Abort from Wasm module: %d %d %d %d" % [a, b, c, d])

func _draw_image(p: int, s: int) -> void: # Draw the entire image from Wasm memory
image.lock()
image.data.data = wasm.stream.seek(p).get_data(s)[1]
image.unlock()
texture.create_from_image(image, 0)

func _draw_pixel(x: int, y: int, r: int, g: int, b: int, a: int) -> void: # Draw a single pixel with color component values
image.lock()
image.set_pixel(x, y, Color8(r, g, b, a))
image.unlock()
texture.create_from_image(image, 0)

func _seed() -> float: # Provide a random seed to the Wasm module
return randf()
118 changes: 118 additions & 0 deletions examples/wasm-visualizations/Main.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
[gd_scene load_steps=5 format=2]

[ext_resource path="res://Main.gd" type="Script" id=1]
[ext_resource path="res://Thaleah.ttf" type="DynamicFontData" id=2]

[sub_resource type="StyleBoxFlat" id=4]
content_margin_top = 12.0
content_margin_bottom = 12.0
bg_color = Color( 0, 0, 0, 1 )
corner_radius_top_left = 12
corner_radius_top_right = 12
corner_radius_bottom_right = 12
corner_radius_bottom_left = 12
corner_detail = 5

[sub_resource type="DynamicFont" id=5]
size = 32
extra_spacing_top = -4
extra_spacing_bottom = -4
font_data = ExtResource( 2 )

[node name="Main" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 1 )

[node name="Tween" type="Tween" parent="."]

[node name="TextureRect" type="TextureRect" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
expand = true
stretch_mode = 6

[node name="Info" type="PanelContainer" parent="."]
modulate = Color( 1, 1, 1, 0 )
anchor_left = 0.5
anchor_right = 0.5
margin_left = -128.0
margin_top = -98.0
margin_right = 128.0
custom_styles/panel = SubResource( 4 )
__meta__ = {
"_edit_group_": true
}

[node name="VBoxContainer" type="VBoxContainer" parent="Info"]
margin_top = 12.0
margin_right = 256.0
margin_bottom = 86.0

[node name="LabelModule" type="Label" parent="Info/VBoxContainer"]
unique_name_in_owner = true
margin_right = 256.0
margin_bottom = 22.0
custom_fonts/font = SubResource( 5 )
text = "Module"
align = 1

[node name="LabelSize" type="Label" parent="Info/VBoxContainer"]
unique_name_in_owner = true
margin_top = 26.0
margin_right = 256.0
margin_bottom = 48.0
custom_fonts/font = SubResource( 5 )
text = "0 X 0"
align = 1

[node name="LabelFPS" type="Label" parent="Info/VBoxContainer"]
unique_name_in_owner = true
margin_top = 52.0
margin_right = 256.0
margin_bottom = 74.0
custom_fonts/font = SubResource( 5 )
text = "FPS"
align = 1

[node name="Intro" type="PanelContainer" parent="."]
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
margin_left = -148.0
margin_top = -110.0
margin_right = 148.0
margin_bottom = -12.0
custom_styles/panel = SubResource( 4 )
__meta__ = {
"_edit_group_": true
}

[node name="VBoxContainer" type="VBoxContainer" parent="Intro"]
margin_top = 12.0
margin_right = 296.0
margin_bottom = 86.0

[node name="Label1" type="Label" parent="Intro/VBoxContainer"]
margin_right = 296.0
margin_bottom = 22.0
custom_fonts/font = SubResource( 5 )
text = "Left/right module"
align = 1

[node name="Label2" type="Label" parent="Intro/VBoxContainer"]
margin_top = 26.0
margin_right = 296.0
margin_bottom = 48.0
custom_fonts/font = SubResource( 5 )
text = "Up/down resolution"
align = 1

[node name="Label3" type="Label" parent="Intro/VBoxContainer"]
margin_top = 52.0
margin_right = 296.0
margin_bottom = 74.0
custom_fonts/font = SubResource( 5 )
text = "C Toggle FPS limit"
align = 1
Binary file not shown.
Binary file added examples/wasm-visualizations/Modules/life.wasm
Binary file not shown.
Binary file not shown.
Binary file added examples/wasm-visualizations/Modules/sort.wasm
Binary file not shown.
Binary file not shown.
Binary file added examples/wasm-visualizations/Thaleah.ttf
Binary file not shown.
1 change: 1 addition & 0 deletions examples/wasm-visualizations/addons
54 changes: 54 additions & 0 deletions examples/wasm-visualizations/project.godot
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters

config_version=4

_global_script_classes=[ {
"base": "",
"class": "Wasm",
"language": "NativeScript",
"path": "res://addons/godot-wasm/Wasm.gdns"
} ]
_global_script_class_icons={
"Wasm": ""
}

[application]

config/name="Wasm Visualizations"
run/main_scene="res://Main.tscn"

[debug]

settings/fps/force_fps=60

[display]

window/size/width=400
window/size/height=400
window/vsync/use_vsync=false

[gui]

common/drop_mouse_on_gui_input_disabled=true

[input]

toggle_fps_cap={
"deadzone": 0.5,
"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":67,"physical_scancode":0,"unicode":0,"echo":false,"script":null)
]
}

[physics]

common/enable_pause_aware_picking=true

[rendering]

environment/default_clear_color=Color( 1, 1, 1, 1 )

0 comments on commit c18b7d9

Please sign in to comment.