mupen64plus / mupen64plus-core

Core module of the Mupen64Plus project
1.32k stars 258 forks source link

I need help with the transfer pak in a frontend I'm making #525

Open Mastergatto opened 6 years ago

Mastergatto commented 6 years ago

Hello everyone, first of all I want to apologize if I have made this ticket here not dealing with a bug, but just asking for help with a frontend I'm currently making (for some years and that isn't public yet) in python. Yeah, in python because it's easier for me as I'm quite a novice with programming, and I must say I have learned something and it was a very fun journey...except when dealing with ctypes. :S

Recently I have decided to try to implement transfer pak support, by wrapping some related struct/function like this:

class m64p_media_loader(Structure): .. cb_data = c_void_p .. cart_rom = CFUNCTYPE(c_char_p, cb_data, c_int) .. cart_ram = CFUNCTYPE(c_char_p, cb_data, c_int) .. fields = [ .... ("cb_data", cb_data), .... ("get_gb_cart_rom", POINTER(cart_rom)), .... ("get_gb_cart_ram", POINTER(cart_ram)) .. ]

and the command like this: status = self.CoreDoCommand(t.m64p_command.M64CMD_SET_MEDIA_LOADER.value, c_int(sizeof(t.m64p_media_loader)), byref(t.m64p_media_loader()))

At this point if I try to launch the command like this, the core seems to accept the parameters without any issue. Of course, the struct was just initialised and it didn't hold any data. The real challenge is when I have to point to it the filenames of the GB ROM/RAM (that are, obviously, already present in the [Transferpak] section in the .cfg file), thus I have tried to mess with the struct, the one above, and other things, but either Python refuses to execute the code or a segfault happens. At this point I'm lost and I don't know what to do, I may have also misunderstood the purpose of those functions.

Can you help me? Thanks

bsmiles32 commented 6 years ago

Not really knowledgeable about the python aspect of your issue, but I can try explain how to use the media_loader interface.

This interface allows the core to ask the frontend filenames to use for some emulated storage (eg GB ROM and GB RAM in this case). This is how it should be used:

  1. the frontend registers it's media_loader (cb_data, get_gbcart{rom,ram}) using the SET_MEDIA_LOADER command: https://github.com/mupen64plus/mupen64plus-ui-console/blob/a105ae0c6fd8480a05214f78e1a897f6d970e283/src/main.c#L969 then it let the core run
  2. when the core is about to init a GB cart, it will ask the frontend which ROM, RAM file to by calling g_media_loader.get_gbcart{rom,ram}(cb_data, control_id);
  3. the frontend determine using it's own way which file to load. (in our console-ui, we get the filenames from the [TransferPak] section of the configuration file, but you're free to do as you wish). Note that the returned string will be released by the core, not the ui. https://github.com/mupen64plus/mupen64plus-ui-console/blob/a105ae0c6fd8480a05214f78e1a897f6d970e283/src/main.c#L716-L780
  4. The core opens and load file content and keep going,

If you need some clarification, don't hesitate to ask.

bsmiles32 commented 6 years ago

So for your frontend you need to implement 2 callbacks get_gb_cart_rom, and get_gb_cart_ram. As a parameter to these function you get a control_id integer which correspond to the controller number (0-3) for which we need to load GB files. The other parameter (cb_data), will have the value you've used when setting the media_loader. This allow to pass some internal state to your callbacks if needed, otherwise just use NULL for this field.

Mastergatto commented 6 years ago

Thanks, as soon as I can I'll try to deal with this, then I'll let you know. 👍

Mastergatto commented 6 years ago

I have fought so hard these days, but I have to give up, I'm always getting a segmentation fault...I have even followed the logic of mupen64plus-ui-console. I'm throwing the code here, so if someone has an idea of what went wrong... (g.m64_wrapper is a class instance wrapping the m64+ library)

import ctypes as c
...
cart_rom = c.CFUNCTYPE(c.c_void_p, c.c_void_p, c.c_int)
p_cart_rom = c.POINTER(cart_rom)
cart_ram = c.CFUNCTYPE(c.c_void_p, c.c_void_p, c.c_int)
p_cart_ram = c.POINTER(cart_ram)

class m64p_media_loader(c.Structure):
    _fields_ = [
        ("cb_data", c.c_void_p),
        ("get_gb_cart_rom", p_cart_rom),
        ("get_gb_cart_ram", p_cart_ram)
    ]

def get_gb_cart_rom(cb_data, controller_id):
    return get_gb_cart_mem_file(cb_data, "rom", controller_id)

def get_gb_cart_ram(cb_data, controller_id):
    return get_gb_cart_mem_file(cb_data, "ram", controller_id)

def get_gb_cart_mem_file(cb_data, mem, controller_id):
    g.m64p_wrapper.ConfigOpenSection("Transferpak")

    if mem == "rom":
        if controller_id == 0:
            filename = g.m64p_wrapper.ConfigGetParameter("GB-rom-1")
        elif controller_id == 1:
            filename = g.m64p_wrapper.ConfigGetParameter("GB-rom-2")
        elif controller_id == 2:
            filename = g.m64p_wrapper.ConfigGetParameter("GB-rom-3")
        elif controller_id == 3:
            filename = g.m64p_wrapper.ConfigGetParameter("GB-rom-4")

    elif mem == "ram":
        if controller_id == 0:
            filename = g.m64p_wrapper.ConfigGetParameter("GB-ram-1")
        elif controller_id == 1:
            filename = g.m64p_wrapper.ConfigGetParameter("GB-ram-2")
        elif controller_id == 2:
            filename = g.m64p_wrapper.ConfigGetParameter("GB-ram-3")
        elif controller_id == 3:
            filename = g.m64p_wrapper.ConfigGetParameter("GB-ram-4")
    return filename

MEDIA_LOADER = m64p_media_loader()
MEDIA_LOADER.cb_data = c.c_void_p()
MEDIA_LOADER.get_gb_cart_rom = c.pointer(cart_rom(get_gb_cart_rom))
MEDIA_LOADER.get_gb_cart_ram = c.pointer(cart_ram(get_gb_cart_ram))

and in the wrapper (cb.MEDIA_LOADER refers to the variable MEDIA_LOADER just above):

    def set_media_loader(self):
        #M64CMD_SET_MEDIA_LOADER = 21

        status = self.CoreDoCommand(t.m64p_command.M64CMD_SET_MEDIA_LOADER.value, c_int(sizeof(cb.MEDIA_LOADER)), byref(cb.MEDIA_LOADER))
        if status != t.m64p_error.M64ERR_SUCCESS.value:
            print("CoreDoCommand: Unable to set the media loader. This means that the Transfer Pak won't work.")
        return status

and set_media_loader() is called just before the execution of the ROM.

The segfault happens right at this line: MEDIA_LOADER.get_gb_cart_rom = c.pointer(cart_rom(get_gb_cart_rom))

Mastergatto commented 6 years ago

By removing c.POINTER and c.pointer it doesn't cause segfault and it has started to work but not well. It's very unstable unfortunately, because I had to avoid to set MEDIA_LOADER.get_gb_cart_ram (thus the savegames aren't working there) and only up to three controller plugged in the emulator. Also the first slot doesn't work properly, it's supposed to be Pokèmon Yellow. pokemonstadium And the console shows this warning:

/data/git/project/wrapper/functions.py:198: RuntimeWarning: memory leak in callback function.

I'm not knowledgeable enough to tell where the issue lies, in the m64+ code or in the python code, but I can say for sure that there's no other way I can make this whole thing work, even partly. @bsmiles32 What's your thought on this? What about the memory leak?

loganmc10 commented 6 years ago

If it helps, you can see how I implemented this in a Qt/C++ GUI:

https://github.com/m64p/mupen64plus-gui/commit/35a9ca77db59f0eef59a4e609a30bb7e723fd27e https://github.com/m64p/mupen64plus-gui/commit/bfddff3d8ce83c721bf174e2d34f504bc5df68eb

You can try out a binary build here: http://m64p.github.io (there are builds for Windows and Linux). It seems to work okay there (I haven't tested multiple controllers though)

Mastergatto commented 6 years ago

I have tested your frontend, it seems to work well. As for your implementation, it seems to be very similar to mine, except for the extra function to tell between ROM and RAM. Other than this, I'm afraid it doesn't help much with my issue, it's likely a problem with python, but thanks anyways!

Mastergatto commented 6 years ago

I have managed to make this thing work almost well, by casting the return string to a void* to avoid memory leaks, and by fixing some checks that I didn't realise were failing because of some quirks of ctypes. The only remaining issue is that the frontend crashes when closing the game and the console returns this (in case of immutable strings):

free(): invalid pointer

or in case of mutable strings:

double free or corruption (out)

But at least this happens only with games with transfer pak support and, most important thing, all the four cartridges are working right now, without issues.

One last question: do we expect future 64dd support here in the media loader, with a similiar structure?

A big thank goes to @bsmiles32 , @loganmc10 and a friend for helping me to figure how the media loader works. 👍

Mastergatto commented 6 years ago

I have another question that is a bit unrelated to this, but how do I deliberately not loading any plugin, for example if I don't want the video to be active, I can do that by setting --gfx 'dummy' with ui-console. How to achieve that with the frontend instead? I don't know what does the variable "use_dummy" in this code: https://github.com/mupen64plus/mupen64plus-ui-console/blob/master/src/plugin.c#L171 https://github.com/mupen64plus/mupen64plus-ui-console/blob/master/src/plugin.c#L190 Any help?

richard42 commented 6 years ago

You don't have to do anything to use a dummy plugin; they are connected by default in the core. Just don't make any CoreAttachPlugin() function call for that plugin type.

Mastergatto commented 6 years ago

That worked, thanks!

Mastergatto commented 5 years ago

Hello, so I kind of want to implement the video extension, after months from the last message here, and right now I have a basic implementation with SDL2. So far, that was very easy and it works well but, as of now, the game only run in a secondary window as if the video extension was still disabled, so it is useless right now. The next step will be integrating the game window into the frontend but so far any attempts at it have been unsuccessful. The reason is that the function SDL_CreateWindowFrom, unlike SDL_CreateWindow, doesn't initialize OpenGL and it's hardcoded so the only way is to patch the SDL library. Obviously that's not an option.

So I have started to look for other ways, like going by the way of GTK, but all it gives me is a black screen, I think it's because GTK, unlike Qt, has no way to set buffers or R/G/B sizes or anything other than that by itself. The only way to set GL attributes is through direct OpenGL commands, but I have absolutely no idea how to do it.

I have also looked for a way to plug SDL window directly into GTK, but looks like it's possible only with SDL 1.2, not 2.

Any advice? Thanks.

loganmc10 commented 5 years ago

So I have started to look for other ways, like going by the way of GTK, but all it gives me is a black screen, I think it's because GTK, unlike Qt, has no way to set buffers or R/G/B sizes or anything other than that by itself. The only way to set GL attributes is through direct OpenGL commands, but I have absolutely no idea how to do it.

A small bit of OpenGL background: OpenGL can render into multiple framebuffers, but framebuffer "0" is what you see on the screen.

One of the challenges with many modern OpenGL toolkits (GTK included), is that they need you to render into their own framebuffer, from the GtkGLArea page:

GtkGLArea sets up its own GdkGLContext for the window it creates, and creates a custom GL framebuffer that the widget will do GL rendering onto.

Most N64 GFX plugins just render into framebuffer 0, so Gtk will overwrite whatever is written there. The VidExt library includes a VidExt_GL_GetDefaultFramebuffer function, that allows you to provide the GFX plugin with the identifier for the Gtk framebuffer. I believe GLideN64 is the only plugin that actually implements this extension, so I would do my testing with GLideN64 if I was you.

Qt has 2 ways to create an OpenGL context, a QOpenGLWidget (creates it's own framebuffer), and a QOpenGLWindow (lets you render into framebuffer 0). QOpenGLWidget has a defaultFramebufferObject function that returns to you the value of it's framebuffer, I'm not sure in GTK has a similar function.

If it helps, you can see how I did it using a QOpenGLWindow, here: https://github.com/m64p/mupen64plus-gui/blob/master/vidext.cpp

Mastergatto commented 5 years ago

Thanks for the information, unfortunately I don't think there's an equivalent for this in GTK+, but I'll look around to see if I can do something with the framebuffer.

loganmc10 commented 5 years ago

There is an OpenGL command to get the current framebuffer (glGetIntegerv(GL_FRAMEBUFFER_BINDING)

So you can have gtk create the OpenGL context, and then use that command to figure out what framebuffer it is using. If you aren't too familiar with OpenGL you may be in for a long and painful road.

Mastergatto commented 5 years ago

This is what I fear too, I think any dealings with OpenGL is far beyond my capabilities. It looks like with current setup GTK+ is set to use framebuffer no. 1 meanwhile SDL is using framebuffer no. 0. So in VidExt_GL_GetDefaultFramebuffer I have told SDL to use GTK's framebuffer, but it seems it doesn't render anything on the screen...

Mastergatto commented 5 years ago

I believe GLideN64 is the only plugin that actually implements this extension, so I would do my testing with GLideN64 if I was you.

I usually test my code with GLideN64 because it's the way to go, but...what if? So, just in case, I have done the same tests with others GFX plugins and....looks like ata4's angrylion-rdp-plus is the only one that works and actually sends the frames to the GLArea, although a bit unstable.

So why does this plugin work and others not? :confused: Maybe because it doesn't set the OpenGL attributes? If that's the case, which OpenGL commands can I use to set them so that others would work? I have only found glGet/glGetIntegerv for getting the value of the following attributes: GL_DOUBLEBUFFER, GL_DEPTH_BITS, GL_RED_BITS, GL_GREEN_BITS, GL_BLUE_BITS, GL_ALPHA_BITS, GL_SAMPLE_BUFFERS, GL_SAMPLES, GL_MAJOR_VERSION, GL_MINOR_VERSION and GL_CONTEXT_PROFILE_MASK. But what about setting them? I couldn't find useful informations on how to do it...

loganmc10 commented 5 years ago

Those attributes are usually set by the GL implementation (like GTK or SDL). In my frontend there are Qt commands to set those things, so if it's possible, it will be done using GTK

Mastergatto commented 5 years ago

A little update:

richard42 commented 5 years ago

That's good to hear of your progress on the front-end application.

Regarding the gamepad mapping with SDL, it works exactly the same regardless of whether the software is linked against SDL1.2 or SDL2. If the different SDL versions somehow map the buttons of gamepads differently, then it will cause a problem.

Richard

On 2/17/19 8:41 AM, Mastergatto wrote:

A little update:

  • The video extension now works well, although I have a weird issue with glide64mk2 plugin, it looks like the depth buffer is reversed. Does someone know why? With other plugins I have no issues. If someone want to know how I did it, I have used a hack to force-enable OpenGL flag for the SDL_CreateWindowFrom() function.
  • I'm writing a new input configuration dialog that can already bind key or gamepad buttons for every player and setting various options, but the whole thing is far from being finished, and on top of that it's full of bugs. Speaking of that, someone can explain me how does mupen64plus map the gamepads, compared to SDL1.x/2.x? Because SDL2 returns wrong value for a lot of buttons, or at least according to mupen64plus.
  • During the testing, I have found out that my media_loader implementation isn't perfect as it doesn't ever write to any .sav, it only reads them without issues. I think I have already found the issue but the problem is that after countless days I can't find an effective solution for it, because Python's handling of strings (char*) is weird...
  • Other than the input dialog, the only missing prominent feature are cheat support and debugger, I'll implement a window for cheats, after finishing input dialog. Debugger has low priority, and I don't even know how does it work. For the rest, my frontend is usable enough well, although it's a bit ugly because I'm currently not concerned about its look.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/mupen64plus/mupen64plus-core/issues/525#issuecomment-464479101, or mute the thread https://github.com/notifications/unsubscribe-auth/AF8JH5f_6Gtr58QVOYU8kemFmm_mjF4Sks5vOYYsgaJpZM4RxhLC.