GMM Dec/23 part 2 - Saving and loading from the web

23/Dec/2023

Welcome back, friends! Tonight we leave the safe haven of our file systems and dialogs; it is time we dive into the dark waters of the web!

This post is a direct continuation of last week’s post, in which I talked about adding the ability to save screenshots in Desktop for interactive toyBrot. While it isn’t NECESSARY for you to have read that one, I am assuming you have from here on. This code lives in toyBrot’s repo and is hosted publicly in Gitlab

Last time I mentioned I had two problems and I needed to make things work in two different platforms. We solved one problem, saving images; in one platform, desktop. So now it’s time to tackle those missing pieces

A sight to remember

Once upon a time, all of the different parameters which control the Mandelbox drawn by toyBrot were hard-coded. With the main use case being benchmarking and study, this wasn’t necessarily a problem, but once I wanted to actually save images, having a better way to tweak the generation of those images became increasingly necessary

These days, when regular toyBrot runs, it looks for a config file. If it finds it, it reads the file. If it doesn’t, it writes one with defaults. It’s nothing fancy, just some raw text

// ToyBrot configuration file
// Type information is merely for user convenience
// File named .c for ease of highlighting in various editors
// comments start with either // or #
// Options available as CLI options have preference over values here

// Camera information
float    cameraX          = 0.0
float    cameraY          = 0.0
float    cameraZ          = -3.8
float    targetX          = 0.0
float    targetY          = 0.0
float    targetZ          = 0.0
uint32_t width            = 1820
uint32_t height           = 980
float    near             = 0.1
float    fovY             = 45.0

// Colouring parameters 
float    hueFactor        = -60.0
int32_t  hueOffset        = 325
float    valueFactor      = 32.0
float    valueRange       = 1.0
float    valueClamp       = 0.9
float    satValue         = 0.8
float    bgRed            = 0.05
float    bgGreen          = 0.05
float    bgBlue           = 0.05
float    bgAlpha          = 1.0

// Raymarching parameters 
uint32_t maxRaySteps      = 7500
float    collisionMinDist = 0.00055

// Mandelbox parameters 
float    fixedRadiusSq    = 2.2
float    minRadiusSq      = 0.8
float    foldingLimit     = 1.45
float    boxScale         = -3.5
uint32_t boxIterations    = 30

So even if it is a bit crude to not have real time interaction to look for an image, you CAN “drive it around” between runs. It’s an area I keep meaning to come back to and improve, but that’s too low priority for me to actually do it. With interactive toyBrot being so much easier to manipulate, this wasn’t quite as necessary but I still laid the foundations to reproduce this structure. Back in my first Godot post, when talking about the little card battler demo I made by expanding from a tutorial, I mentioned that one of the things I had a good time using with Godot were custom Resources. For this application, saving and loading some static data, this is a perfect use case

From the get go, I defined a TBSettings resource to keep all of this “state” data. This Resource stores the parameters for toyBrot, camera information and window size. Even before I was dealing with this side of things, in Godot Resources are passed by reference, so I use an instance of this Resource in order to make sure the right values are passed between all interested parts of the application

extends Resource
class_name TBSettings


@export_category("Window and Camera")

@export var window_size     : Vector2i = Vector2(1820,980)
@export var camera_position : Vector3 = Vector3( 7,  0, 0)
@export var camera_rot_deg  : Vector3 = Vector3( 0, 90, 0)
@export var camera_fov      : int = 75
@export var camera_near     : float = 0.05

@export_category("Colouring")

@export var plain_mode   : bool = false
@export var hue_factor   : float = -40.0
@export var hue_offset   : int   = 245
@export var value_factor : float = 5
@export var value_range  : float = 1.0
@export var value_clamp  : float = 0.9
@export var sat_value    : float = 0.7
@export var bg_colour    : Color = Color(0.15, 0.15, 0.15)
@export var plain_colour : Color = Color(1.0, 1.0, 1.0)

@export_category("Raymarching Parameters")

@export var max_ray_steps : int = 750
@export var collision_min_dist : float = 0.00055

@export_category("Mandelbox Parameters")

@export var fixed_radius_squared   : float = 2.2
@export var minimum_radius_squared : float = 0.8
@export var folding_limit          : float = 1.45
@export var box_scale              : float = -3.5
@export var box_iterations         : int = 10

Every time you tweak a control, the sidebar script updates the Resource and fires a signal which, in turn, causes the mainView script to read from the resource to update the shader uniforms

func _on_hue_offset_changed(value):
    hueOffsetLabel.text = str(value)
    settings.hue_offset = value
    emit_signal("settings_updated")

So, if this resource already exists and we are already using it internally then the problem we need to solve to save and load “a state” of the application is to save and load instances of this Resource, and if that’s all you need, then Godot once more has your back!

Now saving, do not remove memory card in slot 1

Games need to dynamically load and save data all of the time. Godot provides a ready
made solution for those situations that is perfect for my needs here. Two singletons
helpfully named ResourceSaver and ResourceLoader

Both of these classes can work with Resources and Scenes and, depending on what you want, can do so in both binary or plain text form. Internally, they use helper objects which have the logic for individual types of resources, ResourceFormat[Saver|Loader]. By writing these you can extend this system if you want. For toyBrot, though, the config is simple enough that we can just rely on the built-in types

func _on_file_selected(path):
    if(dialog_is_img):
        img_to_save.save_png(path)
    else:
        if(file_dialog.file_mode == FileDialog.FILE_MODE_SAVE_FILE):
            ResourceSaver.save(settings, path)
        else:
            settings = ResourceLoader.load(path, "TBSettings")
            _update_env()
            update_shader()
            $SideBar.load_settings(settings)

What this means here is that I’m getting the (de)serialisation for free. I absolutely don’t need to worry about actually parsing the file or what have you. Godot just GIVES it to me. In this specific instance it’s similar to the sort of functionality I’d get from something like  Qt’s QSettings, but it trades some of the seamless OS integration for added flexibility in what you can use it to handle. With these singletons just sitting out waiting to be used and the FileDialog already there, it was pretty painless to just add all of this functionality!

func _save_settings():
    if(OS.get_name() == "Web"):
        pass
    else:
        file_dialog.set_filters(["*.tres; Godot Resource files"])
        file_dialog.set_file_mode(FileDialog.FILE_MODE_SAVE_FILE)
        file_dialog.set_current_dir(OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS))
        dialog_is_img = false
        file_dialog.popup()

func _load_settings():
    if(OS.get_name() == "Web"):
        pass
    else:
        file_dialog.set_filters(["*.tres; Godot Resource files"])
        file_dialog.set_file_mode(FileDialog.FILE_MODE_OPEN_FILE)
        file_dialog.set_current_dir(OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS))
        dialog_is_img = false
        file_dialog.popup()

Just some extra paths in functions that were already there. Only one challenge left!

Staying in your cage

The broad strokes of how the Godot web export works under the hood is that it compiles your game to something called WebAssembly. WASM, for short, is an intermediary representation which runs on a virtual machine your browser is supposed to implement. The less brain-hurty baby version of this is that wasm is like “machine code for the internet”. Godot uses a toolchain called emscripten to turn your code into an application written in this format. Then you call some javascript from a web page that asks your browser to load that program

This isn’t quite my first trip when it comes to WebAssembly and file shenanigans. Way back, I was experimenting with bringing regular toyBrot to the web through `emscripten`. It’s bananas and, honestly, I’m always amazed at how it actually works. At the time I tweaked for the web one version using C++ std::threads and one using an old trick where you submit a totally real fragment shader to your GPU but it turns out you’re just hijacking the pipeline to have the GPU do arbitrary compute. They work and you can find links to deployed implementations here on the Demos and Toys page (just be warned, the CPU one is intense. Also, I was getting a shader error today on the GPU one and am not sure why. I haven’t touched it, that I know, so it could be some nVidia driver weirdness. Anyway, I haven’t had a chance to look into it) or, if you want to know more about the process, there are blog posts for both the WebCPU and the WebGL implementation

Even Warp Drive, my game engine had some emscripten shenanigans

One of my lessons from that time was that WebAssembly applications live in a sandbox, isolated from your system. This makes immediate sense. You can run toyBrot, the application you built, and have it save a file somewhere in your machine. But if you open a web page and it suddenly just saves a file to a random place in your machine then running out of disk space because every directory is suddenly and misteriously clogged with random phallic photos is going to be where your problems BEGIN, this is a mild scenario here

So YOUR application doesn’t really have access to the user’s file system, WebAssembly is ALREADY a security nightmare without that, but, you know… a lot of these web apps DO need to talk to your file system at some point: how else are you supposed to put the attached file in your email? So your browser itself DOES have access to the user’s disk, and if we ask nicely, it can handle all this file stuff for us

"Saving"? That's a funny way to spell "Download"

Since normally you can’t just access your user’s file system, what you need to do when you’re on the web is offer them downloads and request uploads. This adds an indirection layer and helps prevent malicious actors from sneaking these things past the user, who needs to click “yes, please save this file in this location”. In order to do this we need to go beyond the boundaries of our application, leave the comfort zone of gdscript and the godot editor and actually write some javascript in order to talk to the browser

I’ve never kept it secret that straight up web dev is one of my weak spots. But thanks to stuff like WebAssembly I keep getting off the hook when it comes to actually having to learn it. This time was no different because someone who actually knows this stuff made a really good guide which showed me the way. Shoutout to @yurigami@mastodon.gamedev.place for saving me here!

Their post is a more focused guide on “here’s how you do this specific thing” rather than the sort of “well, you see, back when I was a lad, my grandma used to make this delicious recipe…” kind of faffing about that MY posts tend to be. So if you’re looking for some immediate pointers, I’d definitely recommend checking the original out, I mostly copied their homework

I’m going to give some of the broad strokes here and also go into some of what I ended up having to add in order to massage things into working. Their example is dealing with plain text, but I had to jump through some extra hoops to work with more complex types.

The easy part

So saving files is actually the easy part. Once you have whatever it is as a PackedByteArray, essentially just some raw opaque data you don’t care to interpret, you can just make the one Javascript call and ask for the browser to offer download. For this part, the easiest way is to do it all through gdscript, actually

Godot offers yet another helpful Singleton, JavaScriptBridge which offers you ways of interacting with the browser’s JavaScript environment. This even has a specific function to handle offering a download!

The one little snag I ran into here is “doing the type dance”. In a language like C++, I’d just cast whatever I needed to a void* or what have you and read the raw data, straight from memory. But for Godot I couldn’t quite find a way get there directly from the data I had. Both Image.save_to_png() and ResourceSaver.save() do specifically just that: Save to A FILE. And you kind of have to do this to go through the png encoding and whatnot. It IS quite likely that there is a more elegant way to work around this but since, in this case, they are small files, I made use of that sandbox the application lives in

In order to facilitate the developer figuring out where to put stuff in, Godot offers two special “namespaces” for files: res:// and user://. When you’re working, res:// refers to the internal application storage. These are, largely, the files you “imported” into your project. user://, on the other hand, refers to the persistent storage for the uuhh… user. If you’re saving games, this is where you’d put it. You can read more in the docs if you’re interested

When building in WebAssembly, they are a bit different. res:// refers to a temporary file system that’s just there while your application is there, whereas user:// will refer to the browser’s own persistent caching space. So I made use of these by saving my files to res:// and then reading from those files to get the raw data. It’s a bit clunky, but it does get the job done. Once the browser tab/window is closed, that data gets discarded, which is perfect for us. This part didn’t really give much trouble
func _save_image():
    if(OS.get_name() == "Web"):
        img_to_save.save_png("res://screenshot.png")
        var temp_file = FileAccess.get_file_as_bytes("res://screenshot.png")
        JavaScriptBridge.download_buffer(temp_file, "toyBrotInteractive.png", "img/png")
    else:
        <...>

func _save_settings():
    if(OS.get_name() == "Web"):
        ResourceSaver.save(settings, "res://settings.tres")
        var temp_file = FileAccess.get_file_as_bytes("res://settings.tres")
        JavaScriptBridge.download_buffer(temp_file, "toyBrotInteractive.tres", "text/plain;charset=UTF-8")
    else:
        <...>

The not so easy part

This next bit, however was trickier. Uploading data presents some additional challenges you need to handle. This time, the code is a bit unwieldy to keep inside GDScript. You technically CAN do it, but I’ve opted for moving this to a helper script. Going into this using the “Direct Onion” method, rather than the more common “Reverse Onion”, let’s start from the outer layers

In GDScript, I’m offloading the bulk of the work, same as with the Download, when the user clicks the button, I’m just passing this forward to JavaScript, except this time I do even less. Instead of calling a function that will do something specific, I use JavaScriptBridge.eval() which just passes whatever it gets directly to the environment’s interpreter
func _load_settings():
    if(OS.get_name() == "Web"):
        if (OS.has_feature("web")):
            JavaScriptBridge.eval("loadData()")
        else:
            print("ERROR: No Javascript")
    else:
        <desktop code>

This function itself got offloaded to a separate file. Again, for more in-depth explanation, feel free to refer to the post on Kehom’s Forge. I’m not a Javascript person myself but here are the broad strokes. The file itself is thankfully short

var gd_callbacks = {
   dataLoaded: null
}

function loadData() {
   var input = document.createElement('input');
   input.setAttribute('type', 'file');
   input.setAttribute('accept', '.tres');
   input.click();

   input.addEventListener('change', evt => {
      var file = evt.target.files[0];
      var reader = new FileReader();

      reader.readAsText(file);

      reader.onloadend = function(evt) {
         if (gd_callbacks.dataLoaded) {
            gd_callbacks.dataLoaded(reader.result);
         }
      }
   });
}

We define an object, gd_callbacks that has one internal variable. We also define this one function, the one we call from Gdscript. This function creates a new object and tells the browser we want the user to provide a .tres file (whatever that is, the browser has no idea) as the actual content

Finally, we “connect a callback” to the “change signal” of this object. This gets called when the user actually tries to “put a file in”. In this callback function we read whatever got provided as plain text and, once we’re finished loading, we call dataLoaded from that gd_callbacks object we declared. JavaScript is super loosey goosey with typing so we’re just kind of assuming that what we’re calling IS callable and hoping it is

From here we have two more things to consider. The first of which is that we need this code to be available to our application. We need to load the contents of this script before we load our application, otherwise it can’t know what this function is supposed to be

Another part of the web export is that there has to be an actual web page that the browser loads. This is why even though this is Javascript and WebAssembly, the name of this export is “HTML5”. At least within emscripten and projects that use it, this is known as an HTML shell file, since it’s mostly just setting things up to load the bulk of your application through WASM and JavaScript

Godot offers you the chance to provide your own custom shell file, which you might want to do if you’re planning some specific shenanigans or, if you want, you can just add some stuff to the one it provides by default. This is what I make use of here

In the section called “Head Include” you can add code that goes before your application gets loaded. So here it’s a simple case of “please load this other file, kthx”

The advantage here is that I can have this code in a separate, explicit file. I can edit it with Pulsar or what have you, have all the syntax highlighting and other help from a development editor… but there is one drawback. Godot at this point doesn’t really offer a way for you to tell it to just copy an additional file raw into your deployment folder. So you have to manually do this, as well as upload it to your web server

An alternative is to, instead, instead of loading a file, dumping your JavaScript code straight here. This saves you the copying and can be fine if you only need a tiny bit of JavaScript. What I don’t like about this approach is that you end up having actual code somewhat hidden in your export presets so it’s not something I would personally do

If you click the "expand" icon, you get a separate window for large inlines

But what about that callback?

So this would be the second thing we need. That JavaScript code is relying on a callback that’s just null. This callback is actually the Godot code that will get called when the data finishes getting uploaded

With the JavaScript side already hooked up, the Godot side is not too bad. Create a callback object from a regular GDscript function, ask the JavaScriptBridge for a reference to that callback object on the JS side and then assign our callback to it

func _ready():
   <shared init code>
    if (OS.get_name() == "Web"):
        _on_data_loaded_callback = JavaScriptBridge.create_callback(_on_settings_uploaded)
        # Retrieve the 'gd_callbacks' object
        var gdcallbacks: JavaScriptObject = JavaScriptBridge.get_interface("gd_callbacks")
        # Assign the callbacks
        gdcallbacks.dataLoaded = _on_data_loaded_callback
    else:
        <FileDialog setup>

Just make sure you have all of this inside a branch that’s only active for web deployments

The function itself is allowed to take some shortcuts here too and ends up not being too complicated

func _on_settings_uploaded(data: Array):
    #callback function to handle loading in Web
    if (data.size() == 0):
        return
    var temp_file = FileAccess.open("res://new_settings.tres",FileAccess.WRITE)
    temp_file.store_string(data[0])
    temp_file.close()
    settings = ResourceLoader.load("res://new_settings.tres", "TBSettings")
    _update_env()
    update_shader()
    $SideBar.load_settings(settings)

It receives an array as this COULD be a multiple file upload. In this case, I’m only expecting one file. Again, I use the trick of saving to a temporary file. This time, I dump the raw contents of the array in it. Back in the JavaScript side, I asked it to interpret the data as text, so here I just store_string(whatever file 0 had). Close the file and, from there, it’s actually exactly the same that happens with normal loading. I ask ResourceLoader to give me a TBSettings Resource instance from that file and update all the variables!

So after jumping through a few hoops I managed to circle back around to the same old place! Which is good, this means that it’s easy to set aside your code’s important bits:

 

“Get a file, this part depends on the platform”
“Okay, now that we have a file, do the thing, which is always the same thing”

Another mini, another victory for Godot

When you’re programming, a lot of what looks like basic stuff will have a bunch of fairly tricky things happening under the hood. When you’re in the web, this is doubly true. For me it’s always a bit daunting to have to go outside the scope of C++ or GDscript and, instead, have to start interacting directly with the browser. At that point, maybe you’re talking to a larger web page or service, having to rely on shenanigans on the web server itself…And I don’t think I’m alone in this. So it’s very reassuring that Godot gives as much help as it does when it comes to handling all this. The JavaScriptBridge singleton helps with a lot here, as long as what you’re doing is simple, you don’t have to worry too much

The ResourceLoader and ResourceSaver singletons are also super handy. I did find one annoying limitation with them, though: You can’t really point them to files with arbitrary extensions and then just tell them how to interpret those files. Initially I wanted something along the lines of ResourceLoader.load_as_tres("example.tbsettings") or something. But, honestly, that’s a small nitpick here

This side of the solution is really interesting and broadly useful. If you’re running a demo or limited version in a web environment, you could use something like this to help your user take their settings/save/work between that and a beefier desktop based application

Maybe you have some sort of editor application you can save some work in and email to someone else who can then open it up on a web viewer. ToyBrot IS a toy but it’s not really a game and this is a feature that’s easily valuable in way more than games. Not having to worry about serialization is always a good chunk of work to be able to skip. Regular toyBrot doesn’t do this in a smart way and doesn’t write custom config, only the defaults. And the function that does those is 542 lines long. It IS a couple of corner cuts away from being instantly 20% of that but, at time of writing, it IS that long, without doing anything special and I’m really glad I didn’t have to write another serialiser here… or go looking for an external one. And you don’t even have to serialise if you don’t want to, as those singletons support binary formats as well

I’m still very much in the honeymoon phase with Godot. I don’t think there was something that I went for it to do and wasn’t impressed yet

What's next, then?

So when it comes to Godot, I’m not sure. January’s coming up and I don’t really have a concrete idea of what it is that I want to do for that month’s mini, though there are some background ideas I’ve been ruminating on. I still want to go back to GDscript multithreading, need to play with GDExtension to integrate Godot with C++ (or other languages) and when it comes to GUI there are some multi-resolution/hi-dpi issues that have been bugging me. So still plenty of low-hanging fruit to choose from

For The Great Refactoring, though, the next post is going to be a little bit about me coming to terms with two of my GPUs dying recently. I’m down to only my nVidia Titan X in this computer and this has caused me to take a closer look at performance of different things specifically on it. Expect some whining about <current year argument> and still no decent Wayland support as well as a nice good batch of toyBrot numbers. Spoiler warning: My opinion on CUDA has not improved

Finally, more broadly, I’ve been having a look at some other oneAPI things and have been looking for an opportunity to go back and do that Cartomancer 1.1 that’s somewhat sorely needed. It looks like the opportunity for that might be coming somewhat soon and, once it does, you can be sure I’ll be talking about it here

Until then, hope everyone has a great <Seasonal Holiday> and I’ll see you next week!

Or follow this blog : @tgr@vilelasagna.ddns.net