utopia-rise / fmod-gdextension

FMOD Studio GDExtension bindings for the Godot game engine
MIT License
468 stars 49 forks source link

Event can't be found, followed by crash when calling create_event_instance(...) #230

Open Novark opened 4 months ago

Novark commented 4 months ago

I'm still trying to get the plugin to work on the latest Godot build v4.3.beta2.official, but am running into various issues. I'm not sure if this is a user-error on my part, or an error related to the plugin itself.

For starters, I am able to view my banks in the Fmod Explorer. Note the "Spell_Impact" event under my SFX Bank. I can play this sound successfully with the "Play" button in this screenshot:

image

var event: FmodEvent = null

func _ready() -> void:
    event = FmodServer.create_event_instance("event:/Spell_Impact")

When this code runs, I get the following warning:

WARNING: Event event:/Spell_Impact can't be found. Check if the path is correct or the bank properly loaded.class godot::Ref<3
     at: push_warning (core/variant/variant_utility.cpp:1112)

Immediately following this warning, the game crashes with the following console backtrace:

================================================================
CrashHandlerException: Program crashed with signal 11
Engine version: Godot Engine v4.3.beta2.official (b75f0485ba15951b87f1d9a2d8dd0fcd55e178e4)
Dumping the backtrace. Please include this when reporting the bug to the project developer.
[1] error(-1): no debug info in PE/COFF executable
[2] error(-1): no debug info in PE/COFF executable
[3] error(-1): no debug info in PE/COFF executable
[4] error(-1): no debug info in PE/COFF executable
[5] error(-1): no debug info in PE/COFF executable
[6] error(-1): no debug info in PE/COFF executable
[7] error(-1): no debug info in PE/COFF executable
[8] error(-1): no debug info in PE/COFF executable
[9] error(-1): no debug info in PE/COFF executable
[10] error(-1): no debug info in PE/COFF executable
[11] error(-1): no debug info in PE/COFF executable
[12] error(-1): no debug info in PE/COFF executable
[13] error(-1): no debug info in PE/COFF executable
[14] error(-1): no debug info in PE/COFF executable
[15] error(-1): no debug info in PE/COFF executable
[16] error(-1): no debug info in PE/COFF executable
[17] error(-1): no debug info in PE/COFF executable
[18] error(-1): no debug info in PE/COFF executable
[19] error(-1): no debug info in PE/COFF executable
[20] error(-1): no debug info in PE/COFF executable
[21] error(-1): no debug info in PE/COFF executable
[22] error(-1): no debug info in PE/COFF executable
[23] error(-1): no debug info in PE/COFF executable
[24] error(-1): no debug info in PE/COFF executable
[25] error(-1): no debug info in PE/COFF executable
-- END OF BACKTRACE --
================================================================

I'm still a bit unclear about the intended use of high-level nodes, versus low-level API (are they intended to be mix-and-matched?), as well as the seemingly different ways that the low-level API is used in the examples provided (with some of the examples calling set_software_format, init and set_sound_3d_settings before calling load_bank, and other examples not doing this). Moreover, some of these examples seem to be outdated in that the expected arguments don't line up with the arguments provided in the example code.

Example from the user-guide:

FmodServer.set_software_format(0, FmodServer.FMOD_SPEAKERMODE_STEREO, 0)

Whereas the latest FMOD GDExtension (4.2.0-4.2.0) release expects set_software_format() to be provided settings in the form of an FmodSoftwareFormatSettings object.

Have I failed to set up this plugin correctly? Am I missing something obvious? I've followed the instructions in the user-guide for this plugin, but can't seem to get it working. Any help would be greatly appreciated!

System / Build information:

Windows 11 Pro (x64) Godot v4.3.beta2.official [b75f0485b] FMOD GDExtension (4.2.0-4.2.0 [1d3d7f5])

CedNaru commented 4 months ago

Hello, Indeed, things can be a bit confusing and we definitly need to update the documentation. Regarding node and low-level. The nodes and FmodServer were designed with the same principles as the rest of the Godot engine. You have first a server that allows you to make those low-level calls and retrieve basic objects, this is what offers you the most fine-grained control over what you do. Then the nodes are built on top of this low level API and make calls to the server behind the hood. In practice, you should be able to only use the Fmod Nodes most of the time and only writer calls to FmodServer if they don't know match your needs. For a basic setup, calls to FmodServer are not necessary.

Now I have to clear up some misconceptions here (probably our fault with the documentation). In your project settings, you can configure the way Fmod is initialized, as well as the "auto initialize" feature. If set, you don't need to make any manual calls like FmodServer.set_software_format() (if the arguments in the documentation don't match, it means we forgot to update it). See the "auto initialize" feature as something as high level as the nodes. If you choose to not use it, you have to use the FmodServer calls to start Fmod.

Now you mentionned that your only Fmod related code is

var event: FmodEvent = null

func _ready() -> void:
    event = FmodServer.create_event_instance("event:/Spell_Impact")

This won't work because you need to load the banks first. "auto initialize" will only start FMOD but won't load the banks for you, even if you have set the bank path. This path is only used by the editor for the Event explorer so far. We should probably add a "Load banks automatically" feature at some point. Still, assuming you are not using nodes, you have to make sure to call Fmod.loadBank() before trying to create an event. Again this is our fault, we totally forgot to add back the examples loading the banks in the low level-api guide. We only showed how to do it with nodes. Even so, it should be straight forward then. Just make sure that before loading your target bank, you load your Master.strings.bank and Master.bank first. You can use our demo as references, those examples are constantly updated and tests before releasing: https://github.com/utopia-rise/fmod-gdextension/blob/master/demo/low_level_2D/FmodTest.gd

Novark commented 4 months ago

@CedNaru Thanks for that reply, it helps to clarify things a lot.

I'm still running into some troubles loading the banks, however. When the first call to load_bank gets called, the game crashes without any errors or engine backtrace. It just exits.

var event: FmodEvent = null

func _ready() -> void:
    FmodServer.load_bank("bank:/Master.strings", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL)
    FmodServer.load_bank("bank:/Master", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL)
    FmodServer.load_bank("bank:/SFX", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL)
    event = FmodServer.create_event_instance("event:/Spell_Impact")

A couple notes additional notes:

WARNING: FMOD Sound System: No listeners are set!void __cdecl godot::FmodServer::_set_listener_attributes(void)src\fmod_serve1
     at: push_warning (core/variant/variant_utility.cpp:1112)

My personal preference is to use the low-level API as much as possible to avoid scattering nodes around my scene-tree (although if this is the intended way to use the plugin, I'm not averse to using the high-level nodes).

Is my updated code above sufficient to load the banks and create an event? I'm essentially just trying to reach a point where I can click a button and have a sound play. "Auto-initialize" is enabled in my FMOD project settings, so I shouldn't need to call those other initializer/setup functions from the sounds of it. I'm not sure if the crash is plugin-related, or if I've just missed another step in my code somewhere.

CedNaru commented 4 months ago

You don't need to worry about the listener warning. You should be able to hear sound even without a listener set, it's just that you won't be able to have any "spatialization" of the sounds played because you need some entity with a position registered to FMOD.

Regarding the bank loading, it should be enough, but you made an error, you are supposed to provide a path to the bank files in a Godot way, using the regular "res://path/to/your/bank"

Novark commented 4 months ago

You don't need to worry about the listener warning. You should be able to hear sound even without a listener set, it's just that you won't be able to have any "spatialization" of the sounds played because you need some entity with a position registered to FMOD.

Regarding the bank loading, it should be enough, but you made an error, you are supposed to provide a path to the bank files in a Godot way, using the regular "res://path/to/your/bank"

Whoops - not sure how I missed that! I fixed my paths, and now everything seems to be working.

Thanks again for the help, much appreciated! I would consider this issue closed/resolved (not an issue).

CedNaru commented 4 months ago

I'll keep it open until we clarify the documentation on that matter.

Novark commented 4 months ago

I'll keep it open until we clarify the documentation on that matter.

@CedNaru Speaking of documentation, it may be worthwhile having a section on best practices concerning proper memory management as well.

I'm also noticing some questionable signs from Godot's memory profiler when I create new Event instances.

Essentially, I have a UI element (a PanelContainer in this case, which is acting as a button) which plays a sound when clicked. The code is roughly as follows:

extends PanelContainer

# FMOD banks have already been loaded elsewhere in the project
var audio_event: FmodEvent = null

func _gui_input(event) -> void:
    # Handle the click event for this UI control, playing a sound when clicked
    if event is InputEventMouseButton and event.pressed:
        if event.button_index == MOUSE_BUTTON_LEFT:
            audio_event = FmodServer.create_event_instance("event:/Spell_Impact")
            audio_event.start()
            audio_event.release()

When I profile the static memory, the cost of each "click" appears to be roughly 400 bytes. My understanding is that this FMOD Plugin uses RefCounted objects, which should be free'd when the objects go out of scope, however, I'm not familiar enough with Godot's internals to know when this happens or how it occurs. My PanelContainer will probably never be removed from the scene-tree, as it's a static part of the game UI.

From what I can tell, calling release() on the FmodEvent calls the FMOD library wrapper to null out the user data (the pointer to the EventInstance Ref). This should avoid any memory leaks on the FMOD side of things, and prevent the accrual of EventInstance objects in the FMOD engine. I'm a little less certain of what's happening on the Godot/GDExtension Plugin side of things, however, and what accounts for the slow increase of static memory usage every time I instantiate a new FmodEvent.

https://github.com/utopia-rise/fmod-gdextension/blob/15908f26a23779790ff65427609e89c0cc5f58e0/src/fmod_server.h#L275-L289

I can see from the template above, that the EventInstance is created, along with a Ref to this object. The Ref is then added to a list of currently running events. In FmodServer.update(), the Ref is erased if it is no longer valid. While this removes the Ref from the list of currently running events, I'm not sure that it frees the initial memory allocated by the Ref, which is perhaps where the ~400 bytes or so of memory is accruing that I'm seeing in Godot's profiler.

I'm not 100% sure if this is correct, of if I'm misunderstanding how Godot handles the cleaning-up of these Ref objects, but I wanted to check here to make sure that this isn't an indication of a slow memory leak on the FMOD plugin side of things.

CedNaru commented 4 months ago

I have never investigated the memory usage of the plugin to be honest. But when it comes to Godot itself, the simple way to see RefCounted instance is that if you don't have any reference to it in your code, it's going to be automatically freed. Like the name implies, it's using a counter, not some kind of GC, so the memory should be freed immediately. The more reliable way would be to check for the number of object in use. When not in release mode, Godot's memory can increase for many different reasons (a lot of tracking is happening for debugging purposes).

Novark commented 4 months ago

I have never investigated the memory usage of the plugin to be honest. But when it comes to Godot itself, the simple way to see RefCounted instance is that if you don't have any reference to it in your code, it's going to be automatically freed. Like the name implies, it's using a counter, not some kind of GC, so the memory should be freed immediately. The more reliable way would be to check for the number of object in use. When not in release mode, Godot's memory can increase for many different reasons (a lot of tracking is happening for debugging purposes).

You raise a good point about the difference between debug and release builds, and the built-in tracking. I'll probably file this memory issue away for now, and make a mental note to profile the memory once I have a release build candidate so that I don't waste time on early optimization.

Thanks again!