Migration implemented, it works. This Majora's Mask save was auto-migrated, I just placed an old save file into the right location for the new app, and it made a directory with a Legend of Zelda, The - Majora's -2A0A8ACB.fla in it, and a backup of the old save.
Meanwhile an OotMM randomizer ROM... Well, the save migrated fine, but the emulator just crashes when switching between games. Someone clearly fixed something in the libretro core and never upstreamed it...
Huh, ok, and savestates here are one of those things that's easy to implement, but hard to implement properly.
Like everything in Mupen, they are threaded. If I don't care about when it's finished (and tbh everything does work without that), it's trivial - a couple lines of code for each handler.
If I do care when it's finished tho, it's another story. And I do care about that here - while saving state on exiting is guaranteed to work - it waits for the emulator thread to finish and is blocked in the meantime - for saving state on reset it doesn't work. Even here it works in practice since saving N64 states is pretty fast, but it's still a race condition.
So, for starters, the API itself needs to be async - or block the main thread. But things get really bad if you allow to do multiple save_state() or load_state() calls at once. So, I'm not going to allow that. 🤷♀️ There's no practical reason to be saving multiple states at once, or saving and loading states at once.
Then it should be manageable.
And it's done. Pushed everything.
There are a few more things that should be done on the input side, for example it doesn't support multiple players, or rumble/transfer pak, or even if it was possible to switch to rumble pak - there's no way to actually do rumble atm.
Also would be nice to support 64DD, but that's not a priority.
Another thing I want is an HD mode - now that it's not constrained by libretro, we can just resize the output with the window instead of stretching a 320x240 buffer (or hardcoding another resolution). And yes, I'm defaulting to 320x240 for that reason - it's more accurate, and the default 640x480 seems like a half-measure between that and HD.
Oh, and it doesn't build on aarch64 atm.
But at least on x86_64 it should work pretty well, even if it's missing some of the more obscure features 
Anyway, have this video of me failing to navigate gruntilda's lair. This is certainly another of those games that's a royal pain to play on a keyboard
Egh, and savestates aren't compatible with libretro ones. In fact, I'm seeing this basically on every core. :(
Saves are compatible in every single one, savestates aren't. Oh well :(
Added logging API, so that cores can now output messages properly. In practice it means basically nothing because by far most of the messages are info/debug which are filtered out without G_MESSAGES_DEBUG=all anyway. But, it means Mupen doesn't print messages, say, every time it's paused or unpaused, which is nice.
In process I also noticed it had a warning when shutting down, which I had missed among all the other output. The reason? The async savesate hanling wasn't async enough, but this time it's an upstream bug: see https://github.com/mupen64plus/mupen64plus-core/issues/1031
For now I worked around it by building without -DM64P_PARALLEL=1.
Also I noticed that while I couldn't get ROM_PARAMS with public API, the 2 fields I was interested in were very easily derived from the ROM header and settings, which are exposed. So I'm not using that bit of private API anymore, so it should be possible now to stop modifying m64p-core and plugins entirely. :) That's still a huge amount of work though.
I also added a basic preferences window to the frontend, with 2 options atm:
Bilinear filtering, just because we had it before, really. I might remove it later tho, it's just so ugly
Interframe blending, to smooth out flicker effects in games. Basically, it simulates screen ghosting - every frame it draws a crossfade between the last 2 frames. How effective it is depends on the particular game and the platform's framerate, but when it works, it works. The downside is ofc the ghosting, so it's not gonna be on by default.
The problem is that savestates_save_m64p() is further queueing savestates_save_m64p_work(), and yet the callback in savestates_save() happens immediately once savestates_save_m64p() exits, without ...
Added (very basic) gamepad input, finally.
Now N64 is playable. :3
Very basic because it all controls player 1. While how we handled gamepads in gnome-games/old highscore was pretty broken, at least it allowed to control different players with different gamepads/keyboard. But that's for later.
Oh lol. You know what's playable with a keyboard? Doom 64. Better than with gamepad, in fact, at least if you already have the muscle memory for tank controls. But OMG this game is dark. Like, not in the sense of atmosphere, tho that too, but in the sense of visibility. Unless the window is maximized or the whole system is dark, it's essentially a black screen.
Meanwhile I added API for querying the number of players for N64 games and plugging/unplugging controllers (this is the first platform so far where games actually care about that - on, say, NES it doesn't matter and you can keep them plugged in all the time, hence there's no API for this).
I also made errors actually show up in UI instead of just printing criticals.
Re-posted without a screenshot. For some reason Mastodon compression made it even darker?
Nope, I'm wrong - this has nothing to do with sample rate. I've no idea what it does have to do with, but I removed resampling code from libretro core and it still works. So that part indeed was unnecessary at least.
What I'm not sure about now is how did retro-gtk ever work, considering audio samples are being pushed from an alternate thread and none of the audio code in retro-gtk was thread safe. I noticed that if I remove the lock in the rewrite, it does get faster, but of course also it gets crashy.
Except the audio does get desync in this game. It indeed produces more samples than needed.
I see RMG just inserts delays to basically slow down emulation when that happens - but that's rather complicated when the thread is inside a core. I tried to implement that method, but gave up for now. Instead, when the batch queue gets too long, I just play all the samples. This does mean periodic framerate spikes, but good enough for now. It can always be improved later.
Still wondering how or why retro-gtk worked. Unless it didn't and I never noticed, that is - admittedly I don't play Doom 64 all that much.
Ok, this indeed went really smoothly.
One problem is that Mednafen only saves data when the game is closed, and for how Highscore handles saves I need it saved immediately (see https://crab.garden/@alice/110692540904586478).
Well, I could change Mednafen source to add a public function for saving on demand, but it would be rather invasive. Or I could save the game to a temporary savestate in the cache dir, close it, reopen it, load the savestate and then delete it...
Anyway, unless that approach becomes a problem with other platforms, I'm keeping it, as much as I dislike it.
Next step is combining the 2 screens on the frontend side.
And I'm not sure if I need the battery thing, see the question I posted a few minutes ago.
Gamepad (but not zapper etc) input and savestates work too now. For saving I had to change how I'm doing saves. Previously I had a `set_save_location()` method and called that after starting the game. That worked great for mGBA, but it doesn't for Nestopia: it doesn't write the saved data until it's shutdown. Highscore snapshots purposefully bundle save data and savestates, so that we never have them mismatched (outside of game resets, then we use the last save, but without the savestate) - mismatching may lead to situations where you have a list of save files, then reset and it's suddenly different, which is not great. This means that when creating a snapshot we not only need to save state, but we also need fresh save data. Nestopia only saves at the end, so this breaks. So, instead the port now does the same thing as the libretro core - in the load file I/O callback we get the file and get the raw buffer, in addition to loading the save data. Then, `HsCore` (the core interface) has an optional `save_data()` method, which saves that buffer. We call it when saving the state etc. If a core doesn't implement it (like mGBA doesn't need it and does IO right away when games save data) it just no-ops, and everything is good.
...and now I'm hitting a performance issue in GTK, great. Simply rendering the texture with a color matrix makes it not hit 100 fps. Let alone doing that twice inside a blend node.
Or if I revert to the previous approach and have the core render a smaller texture with left/right on red/blue channels, then recolor that with a single color matrix? Same thing.
Well, performance aside (I'll be reworking the game view to use GtkGLArea instead of GSK nodes later, so whatever for now) Virtual Boy support is done and pushed.
And now that Mednafen works at all, I can easily add support for at least 3 more platforms.
Also this is another platform with a wacky controls mapping.
This time I mapped both dpads onto analog sticks on gamepad (actual dpad still works as left dpad), so that the twin dpads become twin sticks. It works pretty well for that game, for example :)
On keyboard it's WASD for right dpad, arrows for left dpad, Z/X for B/A and the rest is as usual. The really challenging part there is that we map dpad to arrows everywhere and yet here it's left dpad. :(
For comparison, beetle-vb-libretro does have this as a core option, but maps right dpad to L2/R2/L3/R3 by default
I was wondering why I get crashes when lauching a VB game, then an SMS game (or the other way around). Turned out Mednafen and Gearsystem had symbol name clashes, I forgot to use -fvisibility=hidden on the cores. So that's fixed now.
Also I can verify that everything except mupen64plus works on aarch64. (mupen doesn't build - meson doesn't support nasm there, so I'd either need to use custom targets, another buildsystem or upstream mupen64plus-core. I'm gonna do the latter eventually, but it's not a priority - we were already skipping libretro mupen core on aarch64, so nothing new here. :)
And looks like I'll need to rework game startup a bit - for N64 I need to
load the game, now I can query what controller plugins it needs
set the controllers and their plugins
start the game
Currently 1 and 3 are done in the same call, and it's only possible to do 2 after 3 - which fails when running the game for the very first time. It will work after a reset, or after loading a savestate, but on a clean startup the game will see no controllers. So just need to split it into 2 separate calls.
Yup, as I thought, it was really easy to enable NGP/NGPC support now that Mednafen is ported in general.
And I fixed the N64 startup/controller bug I mentioned above.
Next up are PC Engine/CD and WonderSwan.
PC Engine done, other than mouse support. Not PC Engine CD tho.
Also Mednafen PCE Fast supports SuperGrafx too, unlike its own libretro port. Huh.
And it will need an allowlist of games that don't break with 6 button mode - OpenEmu core has one, but not sure if it's complete - it has no source specified, I found https://retropie.org.uk/forum/topic/8349/pc-engine-6-button-mode-glitch/3?lang=en-US but this list is pretty significantly different. I can at least confirm that Street Fighter II works with 6 buttons.
Meanwhile Libretro core doesn't do anything and just allows to enable it anywhere, which tends to go badly. And so does my port atm, but I'm not a big fan of this.
Anyway, next up is CD support - there's a game there I have a big personal interest in seeing working
And yes, the common topic continues: on all 3 of NGP, PCE and VB saves are compatible, savestates are not. :(
So I guess we're just gonna have to deal with that. If it was just a couple platforms, I could try to migrate them, but it's so widespread (doesn't work for even a single platform so far) it doesn't seem feasible
And it works... Except I got hit with https://github.com/flatpak/xdg-desktop-portal/issues/463 (CD games come as bin+cue, you open cue file and then the core finds the bin file through it), so now I have --filesystem=home - not even home:ro because with :ro the file chooser portal still returns a path in /run which is absolutely useless here.
But still, I'm super happy to see Rondo running. And I have a great excuse to replay it as Maria now.
Looking at WonderSwan controls and I'm already scared. This is one platform I have about 0 interest in personally, and coincidentally it doesn't properly work in retro-gtk AND beetle-wswan-libretro has incomprehensible controls. Which is kinda expected given the console's button layout doesn't map well onto modern gamepads, but still.
So WonderSwan has:
2 sets of cursor buttons:
Y1
Y4 Y2
Y3
in the top left corner.
X1
X4 X2
X3
in the bottom left corner.
And:
A
B
in the bottom right corner.
And it has Sound, Start, Power at the bottom, of these only Start is relevant for emulators.
It should be noted that a lot of games are played in portrait orientation, where you have Y/X under your left/right thumbs, instead of X and A/B.
So the libretro core maps all of this as follows:
Select button rotates the console
Start is Start
In landscape:
X is dpad (X1 == up, X2 == right, X3 == down, X4 == left)
A is A, B is B
Y is L/R/L2/R2. The specific configuration - I honestly can't tell, docs say Y1 == R2, Y2 == R, Y3 == L2, Y4 == L, but I can't verify if that's accurate, since the only game I found with an input test (Puyo Puyo Tsuu) doesn't use Y buttons
In portrait:
X1 is left face button, X2 is top face button, X3 is right face button, X4 is bottom face button
A is L, B is R
Y is dpad, but the specific configuration no idea. I tried BeatMania which is a portrait game and uses all buttons, but I honestly can't tell which keys are which - it has no visual indication of when you press a key other than if it matched a note.
So, this is gonna be a pain... Even if just getting the emulation to work is easy, mapping these controls is scary.
I'm also not sure if there are games where you need to rotate the console mid-game or if each game is optimized for one specific mode and that's it. Given WonderSwan doesn't have that many games, I wouldn't be against just making a database to make at least that automatic.
Oh, and this is one of those consoles where you can enter personal info - incl. "sex: m/f"
Interestingly, libretro core does not expose that part in core options, so you probably end up with Mednafen defaults (maybe they changed it, but no idea what games use this): Mednafen, ♀️, born June 23, 1989, blood type O
The core does support palettes tho: grayscale, WonderSwan, WonderSwan Color, Crystal WonderSwan, and... every game boy palette?..
Well, it works. No database for game rotation yet, but I have a button. And well, the rotation mechanism is generic so should work for, say, Atari Lynx or Nintendo DS as well. Oh, and it actually works unlike in retro-gtk which inverts aspect ratio, but doesn't actually rotate the game...
And I went in a alightly different direction for controls:
On keyboard I have arrows for X buttons, WASD for Y buttons, Z/X for B/A and Enter for Start. For portrait I rotate X/Y buttons within each group, but they still stay on arrows and WASD, and on gamepad I swap them between the 2 analogs and map Y to dpad instead.
On gamepad I have X buttons on dpad + left analog stick, Y buttons on right analog stick, A/B/Start are just A/B/Start.
I've no idea how well this control scheme works overall (works fine for Puyo Puyo Tsuu, definitely doesn't work well for Beatmania, but neither does the libretro one), but there we go. If somebody is knowledgeable about WonderSwan and has better ideas, I'm all ears :3
Ok no, controls did not work. Changed it to:
In portrait, X buttons are mapped to face buttons and B/A are R/R2.
So at least in portrait analogs aren't needed anymore (tho they are still there)
In landscape it's same as before, tho maybe I should map Y to the left stick instead and leave X on dpad only.
Yep, removed the analog handling except for Y in landscape. Now it seems reasonable:
Landscape: X == dpad, Y == left analog (Y ends up at the top of the console, so rarely used), A/B/Start == A/B/Start
Portrait: Y == dpad, X == ABXY, A/B == R2/R (they end up at the top of the console, so rarely used), Start == Start
So mostly similar to libretro one, except Y in landscape is left analog instead of L/R/L2/R2, and A/B in portrait are R2/R instead of L/R (they both end up on the right side of the console, so I think my scheme matches the console better)
So the next emulator I’m looking at is bsnes. And I feel sad for the fact that it has basically perfect Super Game Boy emulation, yet we’re likely not gonna use it. :(
There are massive pros and cons for both using mGBA for SGB and using bsnes for it:
Using mGBA means we always use the same emulator for any GB games, that simplifies the frontend code a lot
Using bsnes means that Space Invaders arcade mode works, and even outside that it’s far more accurate.
Using bsnes is cleaner API-wise, because currently a Game Boy core must support SGB to fully implement it. mGBA is pretty much the only one that does. On SNES side SGB would just be a separate add-on platform (like fds/pc engine cd atm) which the core can choose to implement or not
bsnes requires the original SGB bios, which is not great. mGBA doesn’t, since it only emulates SGB enhancements, not SGB itself
Soo, yeah. Keeping mGBA for SGB is probably the smart move here, but it’s still sad that we can have really accurate SGB emulation but probably won’t
Well, bsnes ported.
I forgot how slow upstream bsnes is... We were using bsnes-mercury libretro core, and even with balanced profile it was faster than bsnes with performance profile...
This will likely get improved with a subprocess or at least a separate thread for the core tho - I noticed it gets worse with a larger window so yeah.
Also for some reason bsnes-mercury defaulted to 100% gamma instead of upstream 150%. We now do the latter and as a result the games look a bit darker/less washed out.
Still need to support peripherals, Sufami Turbo etc, but once again it's not like we supported them before - so not a high priority.
Implementing subprocess now. One part of it involves using memfd-backed shared memory, for low-latency video and input. This seems to be untrodden territory in Vala as I'm hitting a lot of limitations/issues:
GUnixInputStream, GUnixOutputStream, GSocket and... partially for GFileDescriptorBased - it only works on the sender side, but not on the receiver side - understandable since it's an interface so you can't really deserialize it, but why support serialization then? And it does not allow to send raw FDs.Well, no matter: I worked around this by wrapping the FD into a temporary stream. It's meh, but it works - just need to make sure the stream doesn't autoclose FDs on either side.
There are no bindings for unnamed semaphors, only named ones. I ended up writing a custom binding for now
There's no way to create a GBytes from a void* and a size, only a Vala array that combines them. It works with a wrapper function, but meh. Of course I need the GBytes so that I can pass that to gdk_memory_texture_new().
Otherwise this is going surprisingly smoothly, as opposed to taking me months back in retro-gtk. Yes, I am heavily reusing subprocess code from there, but this is so much faster when it's not C. I already have the game running, with working audio and video (well ok, audio doesn't count as it's entirely subprocess-side) + a mechanism for platform-specific APIs (which involved a bit of code generation for the "create and register server object for platform" and "get proxy object for platform")
The main thing that still needs figuring out is input, mainly because the way I structured input API in libhighscore doesn't work well with subprocess - it's event-based (button pressed, button released, control stick moved, zapper trigger pressed, etc) and sending all input notifications to the subprocess might be a lot (tho admittedly I haven't tested it). In retro-gtk we used a different approach: shared memory and polling. For each controller, we have a shared memfd-backed buffer, just like for video, containing current states of each input (say, each gamepad button), then the UI process writes there as it changes.
Meanwhile, runner process just polls it and gives that state to the core on each frame - which matches what libretro expects perfectly.
So I guess I'll adopt a similar scheme then. At least for passing input into the runner process, then I could compare the current and last state and emit callbacks as needed.
Alright, subprocess finished and pushed. It wasn't too bad, tho the code is noticeably more complex than it was before. :(
Anyway, bsnes does run a bit better (at least it doesn't get worse with large windows anymore), but it's still not good.
Another weird thing I noticed - somehow bsnes-mercury balanced manages to run faster in old highscore than in retroarch. Meanwhile bsnes, bsnes-hd and bsnes-mercury balanced all slow down while recording. Out of these 3, mercury is the only one that doesn't slow down outside recording too...
A weird thing is it does get noticeably faster with fast PPU turned off. Still not enough for recording though.
Well well well. The bsnes slowness is entirely due to OpenMP doing busywaits.
See https://github.com/bsnes-emu/bsnes/issues/254
Sure enough, setting that env var fixes it, incl. during recording. Both with subprocess and without subprocess.
An ugly part is that it has to be done in highscore, not in bsnes - specifically, it must be done before dlopen()ing bsnes, so I came up with this terrible thing: https://github.com/alice-mkh/bsnes/commit/038c938cb3ed10f65f6665aa5ad5baa4d6b0e79f
When I run the AUR bsnes package (the window appearance is quite buggy, with gray-on-black menubar and transparent window background on XShm), and opening any SNES game, bsnes loads my 12-thread CP...
@YaLTeR measured latency with (on the left) and without (on the right) subprocess, and looks like it's indeed not adding any notable latency. I was pretty confident the way it handled input was sufficiently low-latency, but was less sure about video (it still involves a dbus message to signal a redraw), but it turned out to be fine, so that's pretty great. :)
Note that this is different from the comparison in https://mastodon.online/@YaLTeR/110837526878587080 because that was between no subprocess+custom port of mGBA vs subprocess+libretro Gambatte core, so the difference there could also boil down to the Gambatte vs mGBA or even quirks of their respective ports (or on the contrary, difference in the cores could balance out extra latency from subprocess). Now though it is literally the same exact core with the only difference being whether the frontend is single or multi-process.
Attached: 1 image Now for something different: emulators! Here "New Highscore" is the work-in-progress Highscore rewrite @[email protected] is working on, "Old Highscore" is the current latest Highscore git commit, and "GNOME Games" is the latest Games from Flathub. It's quite interesting how RetroArch seems to have a two-frame spread rather than one, something's off in its processing. Also interesting how MGBA is one frame slower than Gambatte. For Highscore, good to see GTK 4 improving the latency.
Ok, so input is now poll-based. I expected it to lead to net removal of code in highscore itself, and in libhighscore, but it led to code removal in every single core as well:
libhighscore: 29 files changed, 135 insertions, 782 deletions
bsnes: 2 files changed, 11 insertions, 20 deletions
gearsystem: 1 file changed, 65 insertions, 102 deletions
mednafen: 1 file changed, 97 insertions, 129 deletions
mgba: 1 file changed, 51 insertions, 61 deletions
mupen64plus: 1 file changed, 45 insertions, 51 deletions
highscore: 31 files changed, 91 insertions, 600 deletions
total: 66 files changed, 495 insertions, 1745 deletions
So that's honestly great. With this the net code addition from switching to subprocess is vastly reduced as well: from 1767 lines to 1258 if we only count highscore itself, or to 517 if we also count the cores. So the balance is almost restored :3
So I ported melonDS... and then discovered 2 pretty big problems:
While it can load desmume saves for the most part, it failed to load one of them - Dawn of Sorrow says the data is corrupted and deletes it. Other games I tried worked fine, but this is pretty concerning - there might be more games where it doesn't work + can it happen again later on reset? I need to experiment with what exactly makes it do that, but it's not looking very good atm
There's noticeable flicker in Phantom Hourglass with GL rendering enabled... With it disabled it works, but at this point we don't really gain anything compared to desmume...
Hmm. Spot N differences I guess.
Tha main question would be why this would happen ofc.
Answer: wrong pixel format. I set DeSmuME to use RGB888 instead of RGB565 as that one is a pain to deal with on GTK side, and it broke renderer. :(
Thankfully, DeSmuME provides its own optimized color convertion funcs, so it was fairly simple to just convert to RGB888 at the end instead.
I got everything to work except GL. That one is being surprisingly difficult.
https://registry.khronos.org/OpenGL-Refpages/gl4/html/glMapBufferRange.xhtml this func returns null for my port (which leads to a crash as soon as a game renders anything 3d), but does not for standalone frontend or libretro core. Once again I don't understand what I'm doing different for this to happen...
Answer: somehow resizing my context in oglrender_framebufferDidResizeCallback was breaking it. Found by accident. I won't pretend to understand why that happens, but it works without it.
...and then I got hit by https://gitlab.gnome.org/GNOME/epiphany/-/issues/2169 and had to downgrade the SDK.
Either way, pushed the basic version - no screen layouts etc yet, but it runs fine. And you can see it's DeSmuME because it was able to import my old DoS save.
Ok, pushed screen layout support.
This is not per-game yet - in the old Highscore it was tied to savestates, because - y'know - since the core is doing layouts that's the only way to have savestate screenshots match what you'll see after restoring them. But now I want to just have it per-game, not tied to savestate. And that's not implemented atm :)
So it's just global for now, same as VB stereo modes.
There are shortcuts too now: on gamepad it's R3, same as before (I'd use R2 or L2, but I want this to be consistent between DS and 3DS, and 3DS uses L2/R2) and on keyboard it's now space, like in standalone DeSmuME.
And a quick proof of concept of that thing I wanted since forever. WIth this new system it's not very hard to add a secondary window and show the 2 screens in 2 windows. Then each can be maximized/fullscreened on a different monitor.
Probably not gonna implement it right away since I would need to rethink a few more things, e.g. how to handle pause-when-inactive (I have it disabled for that screenshot), but yeah.
Well, added Atari 2600 support and ported Stella.
But... the paddle controller support is really wonky atm. To the point it's unplayable on keyboard/dpad, only playable with analog stick atm.
At some point I need to refactor how I'm doing input on frontend side, e.g. to generalize things like analog emulation (that involves a lot of copypasta atm). And then support paddle via pointer movement and actually smoothly move it with arrows/dpad instead of instantly snapping to the left/right (the reason it's unplayable).
And now Atari 7800/ProSystem (or rather ProSystem JG, upstream ProSystem doesn't really exist besides a source code uploaded 8 years ago and never touched since).
This time controls are fine, since there are no paddles - just joystick and lightgun.