Browse Source

initial commit

master
Brad Parker 3 years ago
commit
af3c62242f
  1. 10
      .gitignore
  2. 201
      LICENSE
  3. 111
      README.md
  4. 18
      common/common.pri
  5. 253
      common/input.cpp
  6. 55
      common/input.h
  7. 90
      common/video.cpp
  8. 58
      common/video.h
  9. 100
      core/audio.cpp
  10. 248
      core/content.cpp
  11. 165
      core/core.cpp
  12. 188
      core/core.h
  13. 18
      core/core.pri
  14. 254
      core/coreoptionsdialog.cpp
  15. 58
      core/coreoptionsdialog.h
  16. 407
      core/env.cpp
  17. 64
      core/input.cpp
  18. 120
      core/library.cpp
  19. 2880
      core/libretro.h
  20. 249
      core/state.cpp
  21. 145
      core/video.cpp
  22. 53
      frontend/audio.cpp
  23. 18
      frontend/frontend.pri
  24. 48
      frontend/input.cpp
  25. 100
      frontend/main.cpp
  26. 217
      frontend/mainwindow.cpp
  27. 94
      frontend/mainwindow.h
  28. 121
      frontend/video.cpp
  29. 37
      reference_frontend.pro

10
.gitignore

@ -0,0 +1,10 @@
*~
*.o
*.pro.user
.qmake.stash
moc_*
Makefile
reference_frontend
reference_frontend.exe
reference_frontend.ini
data/

201
LICENSE

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

111
README.md

@ -0,0 +1,111 @@
reference_frontend
![](https://i.imgur.com/apwogJ0.png)
# Description
This is a barebones minimal reference frontend for the libretro API, designed for developers (not end users), to show the most basic functionality necessary to run a libretro core in a playable state in a cross-platform manner (at least Windows and Linux, probably OSX as well). By playable I mean that input, audio and video all work to a minimum degree. SRAM and save states are also supported. And most importantly, both software and OpenGL-accelerated cores are supported.
This project requires Qt5 version 5.9 or later (Qt6 is also supported but as of 6.0 the multimedia module has not been ported yet, so you will not have sound), and a C++11 toolchain.
# Building
The simplest way to build this project from the command-line is with `qmake && make -j$(nproc)`. This should work on Linux as well as Windows via MSYS2 at the least. If you prefer an IDE-based approach, you can also build with either Qt Creator, or Visual Studio with the Qt add-in, just open the "reference_frontend.pro" file and click Build in either case.
# Program/API flow
The basic idea is that we will be loading in a dynamic library from disk at runtime; this is the core that you want to run. It is assumed that you already have access to a compiled core library whether built yourself or acquired from elsewhere. After the library is loaded, we will acquire some function pointers from the library and then later call those functions to load content and/or render frames inside the core. Finally, we will display those frames (video and/or audio) on the screen, and relay inputs from the user back to the core.
The general flow of logic is as follows:
- frontend/main.cpp
- Initial entry point of the program itself, where main() is located.
- Process command-line arguments to set the appropriate paths to the core library and content file we want to run.
- Construct our "MainWindow" class that handles the high-level GUI functions like drawing the window and receiving input.
- core/core.cpp
- A static instance of this class is created automatically once the main window is shown. It handles all of the communication with the core library. The other files in the core/ folder are all functions in this same class, just broken out by their common functions like audio or video.
- The core library is loaded in library.cpp (Core::load()) and all necessary function pointers are resolved. Most of these functions are ones that we will call ourselves which directly do something inside the core (like load a content file, save state or render a frame), but some of them (most of the ones with "set" in the name) only take pointers to functions of our own and are then later called as necessary by the core (for example to signal when a video frame has finished rendering).
- core/library.cpp
- As soon as the "retro_set_environment" callback is registered, the core will start calling our Core::environment() callback any time it wants, including even before we call retro_init(). This interface lets the core issue a set "command" to the frontend with some optional data attached to it. See the section on core/env.cpp below for the necessary commands you should implement.
- Outside of the environment stuff, the first function we must call after loading the core is retro_init(). This is done in the Core::load() function.
- core/content.cpp
- Next, we call retro_get_system_info() (in Core::run()) which tells us generic info about the core, like its name, version, and what file extensions it supports for content.
- The next step is to ask the core to load some content. First we must fill in a "struct retro_game_info" with the path to the content as well as its size and a pointer to the content data itself (optional in some cases, see "need_fullpath" in libretro.h)
- Now we call retro_load_game() and give it our retro_game_info struct we just filled in. Some cores might check the content for validity at this point and return an error if so.
- retro_get_system_av_info() is called to fill in a "struct retro_system_av_info" for us, this holds audio/video details about the "system" (which may be content-specific) like its video size/framerate or its audio sample rate.
- If the core is hardware-accelerated, we now have enough info to create an offscreen framebuffer object (FBO) as the target for the core to render frames to. The API calls this "get_current_framebuffer" approach "rather obsolete" but other frontends and even RetroArch seems to continue to use it.
- Now call context_reset().
- At this point our frontend prepares the audio output (in Core::setupAudio()) by specifying the desired output format (number of channels, sample rate etc). If your audio renderer cannot natively handle the sample rate output by the core, then you may have to handle the resampling yourself.
- Now we load any existing SRAM into the core that the user may have saved previously (Core::loadSRAM()).
- Finally a repaint() function is called on a timer that, at a glance, would appear to run as fast as possible (it uses 0 as the timeout parameter). More on that later.
- frontend/video.cpp
- The repaint timer will cause the main window to fire its paintGL() and paintEvent() functions in this file. After the main window is done compositing each frame, a frameSwapped() signal is fired by the underlying framework.
- If the core is hardware-accelerated, paintGL() will "blit" the offscreen framebuffer to the "default" onscreen framebuffer and the contents will become visible.
- After frameSwapped() is fired, retro_run() is called to render a single frame (Core::onGotFrameSwap() in core/video.cpp).
- When retro_run() is called, the core will fire our "retro_set_video_refresh" and "retro_set_audio_sample_batch" callbacks (Core::videoRefresh() and Core::audioSampleBatch()). (there is also a "retro_set_audio_sample" but it is not used much)
- The parameters given to the video refresh callback are only useful for software-rendered cores. It always provides the exact geometry and a pointer to the image data it's providing on every frame. Pay extra close attention to any changes. NOTE: Video dimensions can change dramatically from one frame to the next! It's up to you to notice (and handle) this. It may change anywhere between the base and max geometry of the AV info. The "pitch" may also contain extra padding you need to account for when reading the image data.
- Once the core's frame is rendered internally, we then draw the resulting image onto our main window, if this is a software-rendered core.
- Normally this whole process would cause the core's video/audio output to play back at warp speed, but because our main window is backed by an OpenGL surface, we get vsync for free, and that "zero timer" really only fires at the refresh rate of the monitor because it is stalled by the vsync.
- To prove the vsync point, if you were to call setSwapInterval(0) in main.cpp (it's commented out), the core should play back as fast as possible.
- This also means that cores which do not render at (at least very close to) the refresh rate of the monitor will play back at the wrong speed. Dealing with this is left up to the individual developer and out of the scope of this project. You may need to duplicate/drop frames or employ other techniques to achieve proper time sync.
This should cover the main sequential steps necessary to get a core running. Other asynchronous/event-based handling is covered below.
# Other considerations
- frontend/audio.cpp
- Our MainWindow::onAudioSampleBatch() callback handles the audio frames output by the core. The format is always signed 16-bit interleaved stereo.
- For example the data in memory looks like: frame 1 - left, frame 1 - right, frame 2 - left, frame 2 - right, etc.
- frontend/input.cpp
- This project only uses the keyboard and mouse for input as an easy example. Supporting gamepads and other peripherals is out of the scope of this project.
- Once a key is pressed or released on the keyboard, the main window callbacks keyPressEvent/keyReleaseEvent are fired. In there we set the state of any button we care about in a "inputState" array for later.
- When the running core needs to know what buttons are being pressed, it checks them during retro_run() using our "retro_set_input_state" callback (InputState::getInputState() in common/input.cpp).
- The core may also fire a "retro_set_input_poll" callback which is just a suggestion to the frontend on when to check the state of its inputs/buttons (the core still won't know the actual state until it calls its retro_set_input_state callback).
- In our case we don't need this input_poll callback because the keyboard key states are already being tracked as soon as they are pressed, since the underlying framework fires its own callback for it.
- Your own frontend may alternatively choose to poll its own input state in other ways either before, during or after retro_run(). This is what RetroArch's "polling behavior" setting does. The "normal" setting relies on the core to call input_poll for you, and early/late means the frontend calls it in that way relative to retro_run() (before or after).
- core/env.cpp
- The following commands I found to be necessary for minimum core functionality in most cases:
- RETRO_ENVIRONMENT_SET_VARIABLES (tells the frontend what core options are available)
- RETRO_ENVIRONMENT_GET_VARIABLE (asks the frontend for the current value of a core option)
- RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME (tells the frontend we support running without any content loaded)
- RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS (tells the frontend what buttons it supports on what kind of devices, for however many users)
- RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE (asks the frontend if it changed any core variables)
- RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY (if the core needs to handle saving on its own, this asks the frontend for a path to do so in)
- RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY (if the core needs to load a BIOS or other support files, this asks the frontend for a path to find them in)
- RETRO_ENVIRONMENT_GET_LOG_INTERFACE (asks the frontend for a callback to send core log messages to)
- RETRO_ENVIRONMENT_SET_HW_RENDER (asks the frontend if hardware-accelerated cores are supported, and if so, instructs the frontend to set some extra callbacks)
- RETRO_ENVIRONMENT_SET_GEOMETRY (tells the frontend that the core has changed its base video output size (may be temporary, can change at any time))
- RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO (tells the frontend there was a major change in the A/V setup of the core, in RetroArch's case this often means an entire teardown/reinit of the whole application, but we don't really need to for the scope of this project)
- frontend/mainwindow.cpp
- When the window is closed, we make sure to save our SRAM and core options to disk before attempting to stop the content/core in any way, just in case it crashes. We don't want the user to lose their work.
# Usage
Usage: ./reference_frontend [options] core rom
reference_frontend
Options:
-h, --help Displays help on commandline options.
--help-all Displays help including Qt specific options.
-v, --version Displays version information.
-f, --fullscreen Start in fullscreen mode
-m, --mute Start with audio muted
Arguments:
core Core library to use
rom ROM file to use
Any platform that supports Qt 5.9+ and a GPU with OpenGL 2.0+ (and proper drivers) should be able to run this. If you're on Linux, both X11 and Wayland should work, including directly on the console via DRM/KMS using the `-platform eglfs` option.
# Key bindings
The key bindings are hardcoded as follows. Re-binding of the keys is outside the scope of this project. Basic mouse support for cores that utilize it should also work.
![](https://i.imgur.com/yuR9NwJ.png)

18
common/common.pri

@ -0,0 +1,18 @@
#
# Copyright 2020-2021 Brad Parker
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
SOURCES += common/input.cpp common/video.cpp
HEADERS += common/input.h common/video.h

253
common/input.cpp

@ -0,0 +1,253 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <QKeyEvent>
#include <QMouseEvent>
#include "input.h"
#include "../frontend/mainwindow.h"
#include "../core/core.h"
InputState::InputState(QObject *parent) :
QObject(parent)
,m_inputState()
,m_analogInputState()
,m_mousePosAbs()
,m_mousePos()
,m_mouseButtonsState()
,m_core(nullptr)
,m_mainwindow(nullptr)
{
m_core = Core::instance();
m_mainwindow = MainWindow::instance();
}
InputState* InputState::instance() {
static InputState instance;
return &instance;
}
Qt::MouseButtons InputState::getMouseButtonsState() {
return m_mouseButtonsState;
}
int InputState::getMousePosRelX() {
int p = m_mousePos.x();
m_mousePos.setX(0);
return p;
}
int InputState::getMousePosRelY() {
int p = m_mousePos.y();
m_mousePos.setY(0);
return p;
}
void InputState::mousePressEvent(QMouseEvent *event) {
m_mouseButtonsState |= event->button();
event->ignore();
}
void InputState::mouseReleaseEvent(QMouseEvent *event) {
m_mouseButtonsState &= ~(event->button());
event->ignore();
}
void InputState::mouseMoveEvent(QMouseEvent *event) {
QPoint p = event->pos();
m_mousePos.setX(p.x() - m_mousePosAbs.x());
m_mousePos.setY(p.y() - m_mousePosAbs.y());
m_mousePosAbs = p;
event->ignore();
}
bool InputState::keyPressEvent(QKeyEvent *event) {
bool handled = true;
switch(event->key()) {
case Qt::Key_Up:
m_inputState[RETRO_DEVICE_ID_JOYPAD_UP] = 1;
break;
case Qt::Key_Down:
m_inputState[RETRO_DEVICE_ID_JOYPAD_DOWN] = 1;
break;
case Qt::Key_Left:
m_inputState[RETRO_DEVICE_ID_JOYPAD_LEFT] = 1;
break;
case Qt::Key_Right:
m_inputState[RETRO_DEVICE_ID_JOYPAD_RIGHT] = 1;
break;
case Qt::Key_Home:
m_analogInputState[RETRO_DEVICE_ID_ANALOG_Y] = -0x7fff;
break;
case Qt::Key_End:
m_analogInputState[RETRO_DEVICE_ID_ANALOG_Y] = 0x7fff;
break;
case Qt::Key_Delete:
m_analogInputState[RETRO_DEVICE_ID_ANALOG_X] = -0x7fff;
break;
case Qt::Key_PageDown:
m_analogInputState[RETRO_DEVICE_ID_ANALOG_X] = 0x7fff;
break;
case Qt::Key_Shift:
m_inputState[RETRO_DEVICE_ID_JOYPAD_SELECT] = 1;
break;
case Qt::Key_Return:
m_inputState[RETRO_DEVICE_ID_JOYPAD_START] = 1;
break;
case Qt::Key_A:
m_inputState[RETRO_DEVICE_ID_JOYPAD_L] = 1;
break;
case Qt::Key_Z:
m_inputState[RETRO_DEVICE_ID_JOYPAD_R] = 1;
break;
case Qt::Key_W:
m_inputState[RETRO_DEVICE_ID_JOYPAD_L2] = 1;
break;
case Qt::Key_T:
m_inputState[RETRO_DEVICE_ID_JOYPAD_R2] = 1;
break;
case Qt::Key_S:
m_inputState[RETRO_DEVICE_ID_JOYPAD_X] = 1;
break;
case Qt::Key_X:
m_inputState[RETRO_DEVICE_ID_JOYPAD_Y] = 1;
break;
case Qt::Key_D:
m_inputState[RETRO_DEVICE_ID_JOYPAD_A] = 1;
break;
case Qt::Key_C:
m_inputState[RETRO_DEVICE_ID_JOYPAD_B] = 1;
break;
case Qt::Key_R:
m_core->reset();
break;
/*case Qt::Key_Escape:
case Qt::Key_Q:
QTimer::singleShot(0, qApp, &QCoreApplication::quit);
break;*/
case Qt::Key_P:
m_core->setPaused(!m_core->isPaused());
break;
case Qt::Key_F:
m_mainwindow->setFullScreen(!m_mainwindow->isFullScreen());
break;
case Qt::Key_M:
m_mainwindow->setCoreMuted(!m_core->isMuted());
break;
case Qt::Key_U:
m_mainwindow->openCoreOptionsDialog();
break;
case Qt::Key_F2:
m_mainwindow->saveState();
break;
case Qt::Key_F3:
m_core->saveSRAM();
break;
case Qt::Key_F4:
m_mainwindow->loadState();
break;
default:
handled = false;
break;
}
return handled;
}
bool InputState::keyReleaseEvent(QKeyEvent *event) {
bool handled = true;
switch(event->key()) {
case Qt::Key_Up:
m_inputState[RETRO_DEVICE_ID_JOYPAD_UP] = 0;
break;
case Qt::Key_Down:
m_inputState[RETRO_DEVICE_ID_JOYPAD_DOWN] = 0;
break;
case Qt::Key_Left:
m_inputState[RETRO_DEVICE_ID_JOYPAD_LEFT] = 0;
break;
case Qt::Key_Right:
m_inputState[RETRO_DEVICE_ID_JOYPAD_RIGHT] = 0;
break;
case Qt::Key_Home:
m_analogInputState[RETRO_DEVICE_ID_ANALOG_Y] = 0;
break;
case Qt::Key_End:
m_analogInputState[RETRO_DEVICE_ID_ANALOG_Y] = 0;
break;
case Qt::Key_Delete:
m_analogInputState[RETRO_DEVICE_ID_ANALOG_X] = 0;
break;
case Qt::Key_PageDown:
m_analogInputState[RETRO_DEVICE_ID_ANALOG_X] = 0;
break;
case Qt::Key_Shift:
m_inputState[RETRO_DEVICE_ID_JOYPAD_SELECT] = 0;
break;
case Qt::Key_Return:
m_inputState[RETRO_DEVICE_ID_JOYPAD_START] = 0;
break;
case Qt::Key_A:
m_inputState[RETRO_DEVICE_ID_JOYPAD_L] = 0;
break;
case Qt::Key_Z:
m_inputState[RETRO_DEVICE_ID_JOYPAD_R] = 0;
break;
case Qt::Key_W:
m_inputState[RETRO_DEVICE_ID_JOYPAD_L2] = 0;
break;
case Qt::Key_T:
m_inputState[RETRO_DEVICE_ID_JOYPAD_R2] = 0;
break;
case Qt::Key_S:
m_inputState[RETRO_DEVICE_ID_JOYPAD_X] = 0;
break;
case Qt::Key_X:
m_inputState[RETRO_DEVICE_ID_JOYPAD_Y] = 0;
break;
case Qt::Key_D:
m_inputState[RETRO_DEVICE_ID_JOYPAD_A] = 0;
break;
case Qt::Key_C:
m_inputState[RETRO_DEVICE_ID_JOYPAD_B] = 0;
break;
default:
handled = false;
break;
}
return handled;
}
int16_t InputState::getInputState(unsigned id) {
if(id >= (sizeof(m_inputState) / sizeof(m_inputState[0])))
return 0;
return m_inputState[id];
}
int16_t InputState::getAnalogInputState(unsigned id) {
if(id >= (sizeof(m_analogInputState) / sizeof(m_analogInputState[0])))
return 0;
return m_analogInputState[id];
}

55
common/input.h

@ -0,0 +1,55 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef __INPUT_H_
#define __INPUT_H_
#include <QObject>
#include <QPoint>
class QKeyEvent;
class QMouseEvent;
class Core;
class MainWindow;
class InputState : public QObject {
Q_OBJECT
public:
InputState(QObject *parent = nullptr);
static InputState* instance();
int16_t getInputState(unsigned id);
int16_t getAnalogInputState(unsigned id);
Qt::MouseButtons getMouseButtonsState();
int getMousePosRelX();
int getMousePosRelY();
bool keyPressEvent(QKeyEvent *e);
bool keyReleaseEvent(QKeyEvent *e);
void mouseMoveEvent(QMouseEvent *e);
void mousePressEvent(QMouseEvent *e);
void mouseReleaseEvent(QMouseEvent *e);
private:
int16_t m_inputState[16];
int16_t m_analogInputState[2];
QPoint m_mousePosAbs;
QPoint m_mousePos;
Qt::MouseButtons m_mouseButtonsState;
Core *m_core;
MainWindow *m_mainwindow;
Q_DISABLE_COPY(InputState)
};
#endif // __INPUT_H_

90
common/video.cpp

@ -0,0 +1,90 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "video.h"
VideoState::VideoState(QObject *parent) :
QObject(parent)
,m_context(nullptr)
,m_aspectCorrectedRect()
,m_avInfo()
,m_baseWidth(0)
,m_baseHeight(0)
,m_aspect(1.0f)
,m_fbo(nullptr)
{
}
VideoState* VideoState::instance() {
static VideoState instance;
return &instance;
}
void VideoState::setContext(QOpenGLContext *context) {
m_context = context;
}
QOpenGLContext* VideoState::context() {
return m_context;
}
void VideoState::setAspectCorrectedRect(QRect r) {
m_aspectCorrectedRect = r;
}
QRect VideoState::aspectCorrectedRect() {
return m_aspectCorrectedRect;
}
QOpenGLFramebufferObject* VideoState::fbo() {
return m_fbo;
}
void VideoState::setFBO(QOpenGLFramebufferObject *fbo) {
m_fbo = fbo;
}
unsigned VideoState::baseWidth() {
return m_baseWidth;
}
void VideoState::setBaseWidth(unsigned width) {
m_baseWidth = width;
}
unsigned VideoState::baseHeight() {
return m_baseHeight;
}
void VideoState::setBaseHeight(unsigned height) {
m_baseHeight = height;
}
float VideoState::aspect() {
return m_aspect;
}
void VideoState::setAspect(float aspect) {
m_aspect = aspect;
}
struct retro_system_av_info* VideoState::avInfo() {
return &m_avInfo;
}
void VideoState::setAvInfo(struct retro_system_av_info info) {
m_avInfo = info;
}

58
common/video.h

@ -0,0 +1,58 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef __VIDEO_H_
#define __VIDEO_H_
#include <QObject>
#include <QRect>
#include "../core/libretro.h"
class QOpenGLContext;
class QOpenGLFramebufferObject;
class VideoState : public QObject {
Q_OBJECT
public:
VideoState(QObject *parent = nullptr);
static VideoState* instance();
QOpenGLContext* context();
void setContext(QOpenGLContext *context);
void setAspectCorrectedRect(QRect r);
QRect aspectCorrectedRect();
QOpenGLFramebufferObject* fbo();
void setFBO(QOpenGLFramebufferObject *fbo);
unsigned baseWidth();
unsigned baseHeight();
void setBaseWidth(unsigned width);
void setBaseHeight(unsigned height);
float aspect();
void setAspect(float aspect);
struct retro_system_av_info* avInfo();
void setAvInfo(struct retro_system_av_info info);
private:
QOpenGLContext *m_context;
QRect m_aspectCorrectedRect;
struct retro_system_av_info m_avInfo;
unsigned m_baseWidth;
unsigned m_baseHeight;
float m_aspect;
QOpenGLFramebufferObject *m_fbo;
Q_DISABLE_COPY(VideoState)
};
#endif // __VIDEO_H_

100
core/audio.cpp

@ -0,0 +1,100 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef NO_SOUND
#include <QAudioFormat>
#include <QAudioDeviceInfo>
#endif
#include <iostream>
#include "core.h"
#include "../common/video.h"
void Core::audioSample(int16_t left, int16_t right) {
// most cores just use the batch callback instead
if(m_isAudioStreamReady && !m_isMuted) {
emit Core::instance()->gotAudioSample(left, right);
}
}
size_t Core::audioSampleBatch(const int16_t *data, size_t frames) {
if(m_isAudioStreamReady && !m_isMuted) {
emit Core::instance()->gotAudioSampleBatch(data, frames);
}
return frames;
}
void Core::onAudioStreamReady() {
m_isAudioStreamReady = true;
}
void Core::setPaused(bool on) {
std::cout << "set core pause to " << on << std::endl;
m_isPaused = on;
if(m_isPaused) {
m_timer.stop();
}else{
m_timer.start();
}
}
bool Core::isPaused() const {
return m_isPaused;
}
void Core::setMuted(bool on) {
std::cout << "set core mute to " << on << std::endl;
m_isMuted = on;
}
bool Core::isMuted() const {
return m_isMuted;
}
void Core::setupAudio() {
#ifdef NO_SOUND
std::cout << "audio support not compiled in, no sound will play." << std::endl;
#else
bool audio = true;
struct retro_system_av_info *avInfo = VideoState::instance()->avInfo();
// cores only support S16LE raw audio with 2 channels
QAudioFormat audioFormat;
// We're assuming our QAudioOutput's backend natively supports the core's sample rate, but this may not always be the case.
// If it is not supported and/or your audio output mechanism cannot resample accordingly, you may need to implement this yourself (RetroArch does)
audioFormat.setSampleRate(avInfo->timing.sample_rate);
audioFormat.setChannelCount(2);
audioFormat.setSampleSize(16);
audioFormat.setCodec("audio/pcm");
audioFormat.setByteOrder(QAudioFormat::LittleEndian);
audioFormat.setSampleType(QAudioFormat::SignedInt);
QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice());
if(!info.isFormatSupported(audioFormat)) {
std::cerr << "Core's audio format (S16LE stereo @ " << avInfo->timing.sample_rate << "Hz) not supported by backend, cannot play audio." << std::endl;
audio = false;
}
if(audio) {
emit audioReady(&audioFormat);
}
#endif
}

248
core/content.cpp

@ -0,0 +1,248 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <QGuiApplication>
#include <QOpenGLFramebufferObject>
#include <QScreen>
#include <iostream>
#include "core.h"
#include "../common/video.h"
bool Core::run(QString contentPath) {
memset(&m_gameInfo, 0, sizeof(m_gameInfo));
if(contentPath.isEmpty()) {
m_path.clear();
m_pathArray.clear();
m_pathData = nullptr;
}else{
m_path = QFileInfo(contentPath).absoluteFilePath();
m_pathArray = m_path.toUtf8();
m_pathData = m_pathArray.constData();
std::cout << "loading content: " << m_pathData << std::endl;
m_gameInfo.path = m_pathData;
}
memset(&m_info, 0, sizeof(m_info));
std::cout << "retro_get_system_info";
m_retroGetSystemInfo(&m_info);
std::cout << "." << std::endl;
if(m_info.need_fullpath) {
std::cout << "core needs full path to content and will load it on its own." << std::endl;
}else{
std::cout << "core does not load its own content, the frontend will do it." << std::endl;
}
if(m_info.block_extract) {
std::cout << "core does not allow frontend to extract archives." << std::endl;
}else{
std::cout << "core allows frontend to extract archives." << std::endl;
// but we do not support it
}
/* TODO: At this point, one consideration to make might be the amount of memory available on the system.
* If the core does not load its own content, you don't want to try to load something into memory
* that is so large it will not fit. Better to alert the user in that case than silently crash.
*/
if(!m_path.isEmpty()) {
uint64_t contentSize = QFileInfo(m_path).size();
std::cout << "content size: " << contentSize / 1024.0 / 1024 << "MB" << std::endl;
if(m_info.need_fullpath) {
std::cout << "since core loads its own content, we will assume it won't load the whole thing into memory (or will check if there is enough), and continue on." << std::endl;
}else{
std::cout << "core doesn't load its own content, let's hope we have enough memory to load the whole thing." << std::endl;
}
}
if(m_info.library_name && *(m_info.library_name))
std::cout << "core name: " << m_info.library_name << std::endl;
if(m_info.library_version && *(m_info.library_version))
std::cout << "core version: " << m_info.library_version << std::endl;
if(m_info.valid_extensions && *(m_info.valid_extensions)) {
std::cout << "core supports the following extensions: " << m_info.valid_extensions << std::endl;
if(!m_path.isEmpty()) {
QStringList extens = QString(m_info.valid_extensions).split("|");
QString gameExten = QFileInfo(m_path).suffix();
bool found = false;
for(int i = 0; i < extens.size(); ++i) {
const QString &exten = extens.at(i);
if(exten.toLower() == gameExten.toLower()) {
found = true;
break;
}
}
if(!found) {
std::cerr << "unsupported file extension: " << qUtf8Printable(gameExten) << std::endl;
return false;
}
}
}else if(!m_noGame) {
std::cerr << "core does not list any valid extensions!" << std::endl;
return false;
}
if(!(m_noGame && m_path.isEmpty()) && !m_info.need_fullpath) {
// core cannot load its own content, we must hold it in memory
if(!loadContentIntoMemory()) {
return false;
}
}
std::cout << "retro_load_game";
if(!m_retroLoadGame(&m_gameInfo)) {
std::cerr << "could not load content" << std::endl;
return false;
}
std::cout << "." << std::endl;
auto videoState = VideoState::instance();
struct retro_system_av_info *avInfo = videoState->avInfo();
memset(avInfo, 0, sizeof(*avInfo));
std::cout << "retro_get_system_av_info";
m_retroGetSystemAVInfo(avInfo);
std::cout << "." << std::endl;
videoState->setAspect(avInfo->geometry.aspect_ratio);
videoState->setBaseWidth(avInfo->geometry.base_width);
videoState->setBaseHeight(avInfo->geometry.base_height);
if(!videoState->aspect())
videoState->setAspect(1);
float screenRefresh = 0;
QScreen *screen = QGuiApplication::primaryScreen();
if(screen) {
screenRefresh = screen->refreshRate();
std::cout << "monitor refresh rate: " << screenRefresh << "HZ, DPI " << screen->logicalDotsPerInch() << std::endl;
}
// if core refresh rate is within half a percent of the screen's, just call it equal, it's hopefully close enough to not be noticeable
if((screenRefresh - (screenRefresh * 0.005)) <= avInfo->timing.fps && (screenRefresh + (screenRefresh * 0.005)) >= avInfo->timing.fps) {
avInfo->timing.fps = screenRefresh;
}
std::cout << "current AV info: " << avInfo->geometry.base_width << "x" << avInfo->geometry.base_height << " (max " << avInfo->geometry.max_width << "x" << avInfo->geometry.max_height << ") aspect " << avInfo->geometry.aspect_ratio << " fps " << avInfo->timing.fps << " audio rate " << avInfo->timing.sample_rate << std::endl;
if(m_isHWRender) {
QOpenGLFramebufferObject *fbo = videoState->fbo();
if(fbo)
delete fbo;
std::cout << "creating FBO with size " << avInfo->geometry.max_width << "x" << avInfo->geometry.max_height << std::endl;
fbo = new QOpenGLFramebufferObject(avInfo->geometry.max_width, avInfo->geometry.max_height, QOpenGLFramebufferObject::CombinedDepthStencil);
videoState->setFBO(fbo);
std::cout << "created FBO id " << fbo->handle() << std::endl;
std::cout << "context_reset" << std::endl;
if(m_retroHWContextReset) {
m_retroHWContextReset();
}else{
std::cerr << "no context reset function defined" << std::endl;
}
std::cout << "." << std::endl;
}
setupAudio();
if(m_imgData) {
delete []m_imgData;
m_imgData = nullptr;
}
loadSRAM();
std::cout << "retro_run loop starting at " << avInfo->timing.fps << " fps." << std::endl;
m_elapsedTimer.start();
m_doRender = true;
connect(&m_timer, &QTimer::timeout, this, &Core::repaint);
m_timer.start(0);
m_isRunning = true;
return true;
}
bool Core::loadContentIntoMemory() {
if(!m_path.isEmpty()) {
// per libretro.h we have to set data/size even if the core can load from a path
std::cout << "core does not need full path, reading in content to load ourselves: " << m_gameInfo.path << std::endl;
QFile f(m_gameInfo.path);
if(f.open(QIODevice::ReadOnly)) {
m_gameInfo.size = f.size();
m_gameDataSize = m_gameInfo.size;
if(m_gameData)
delete m_gameData;
m_gameData = new(std::nothrow) char[m_gameDataSize];
int64_t readBytes = f.read(m_gameData, m_gameDataSize);
f.close();
if(readBytes < 0 || readBytes != m_gameDataSize) {
std::cerr << "could not read content from disk: " << qUtf8Printable(f.fileName()) << std::endl;
return false;
}
m_gameInfo.data = m_gameData;
}else{
std::cerr << "could not open content for reading: " << qUtf8Printable(f.fileName()) << std::endl;
return false;
}
}else{
return false;
}
return true;
}
bool Core::supportsNoGame() {
return m_noGame;
}

165
core/core.cpp

@ -0,0 +1,165 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <iostream>
#include <QTimer>
#include <QMainWindow>
#include <QFileInfo>
#include <QtEndian>
#include <QDir>
#include <QDateTime>
#include <QtGlobal>
#include "core.h"
#include "../common/input.h"
#include "../common/video.h"
extern "C" {
#include <stdio.h>
}
Core::Core(QObject *parent) :
QObject(parent)
,m_pixFmt(RETRO_PIXEL_FORMAT_0RGB1555)
,m_info()
,m_gameInfo()
,m_imgData(nullptr)
,m_width(0)
,m_height(0)
,m_pitch(0)
,m_isAudioStreamReady(false)
,m_isMuted(false)
,m_isPaused(false)
,m_variablesChanged(false)
,m_isHWRender(false)
,m_noGame(false)
,m_bottomLeftOrigin(false)
,m_img()
,m_options()
,m_controllerInfo()
,m_library()
,m_isLoaded(false)
,m_isInited(false)
,m_isResolved(false)
,m_isRunning(false)
,m_doRender(false)
,m_path()
,m_pathArray()
,m_pathData(nullptr)
,m_extractedPath()
,m_extractedPathArray()
,m_extractedPathData(nullptr)
,m_saveStateData(nullptr)
,m_saveStateDataSize(0)
,m_elapsedTimer()
,m_timer()
,m_tempDir()
,m_gameData(nullptr)
,m_gameDataSize(0)
,m_dataDir()
,m_savePath()
,m_statePath()
,m_systemPath()
/* pointers to functions that are exported by the core which we will call later */
,m_retroInit(nullptr)
,m_retroDeinit(nullptr)
,m_retroRun(nullptr)
,m_retroReset(nullptr)
,m_retroLoadGame(nullptr)
,m_retroUnloadGame(nullptr)
,m_retroGetMemoryData(nullptr)
,m_retroGetMemorySize(nullptr)
,m_retroSerialize(nullptr)
,m_retroSerializeSize(nullptr)
,m_retroUnserialize(nullptr)
,m_retroGetSystemAVInfo(nullptr)
,m_retroGetSystemInfo(nullptr)
,m_retroSetControllerPortDevice(nullptr)
,m_retroHWContextReset(nullptr)
/* used to tell the core where our callbacks are */
,m_retroSetEnvironment(nullptr)
,m_retroSetVideoRefresh(nullptr)
,m_retroSetAudioSample(nullptr)
,m_retroSetAudioSampleBatch(nullptr)
,m_retroSetInputPoll(nullptr)
,m_retroSetInputState(nullptr)
{
QDir d;
d.mkdir("data");
d.mkdir("data/saves");
d.mkdir("data/states");
d.mkdir("data/system");
QDir saveDir("data/saves");
QDir stateDir("data/states");
QDir systemDir("data/system");
QByteArray saveDirArray = saveDir.absolutePath().toUtf8();
QByteArray stateDirArray = stateDir.absolutePath().toUtf8();
QByteArray systemDirArray = systemDir.absolutePath().toUtf8();
const char *saveDirData = saveDirArray.constData();
const char *stateDirData = stateDirArray.constData();
const char *systemDirData = systemDirArray.constData();
strncpy(m_savePath, saveDirData, qMin(saveDirArray.size(), static_cast<int>(sizeof(m_savePath) - 1)));
strncpy(m_statePath, stateDirData, qMin(stateDirArray.size(), static_cast<int>(sizeof(m_statePath) - 1)));
strncpy(m_systemPath, systemDirData, qMin(systemDirArray.size(), static_cast<int>(sizeof(m_systemPath) - 1)));
m_dataDir = QDir("data");
m_timer.setTimerType(Qt::PreciseTimer);
m_timer.setSingleShot(true);
connect(this, &Core::gotFrameSwap, this, &Core::onGotFrameSwap, Qt::DirectConnection);
}
Core::~Core() {
std::cout << "core dtor" << std::endl;
unload();
if(m_saveStateData)
delete[] m_saveStateData;
}
Core* Core::instance() {
static Core instance;
return &instance;
}
bool Core::isLoaded() const {
return m_isLoaded;
}
bool Core::isResolved() const {
return m_isResolved;
}
bool Core::isRunning() const {
return m_isRunning;
}
QMap<QString, Core::CoreOption>& Core::getCoreOptions() {
return m_options;
}
void Core::setVariablesChanged(bool changed) {
m_variablesChanged = changed;
}

188
core/core.h

@ -0,0 +1,188 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef __CORE_H_
#define __CORE_H_
#include <QLibrary>
#include <QImage>
#include <QElapsedTimer>
#include <QTimer>
#include <QTemporaryDir>
#include <QMap>
extern "C" {
#include "libretro.h"
}
class QAudioFormat;
class QOpenGLFramebufferObject;
class Core;
typedef void (*RetroInit)(void);
typedef void (*RetroDeinit)(void);
typedef bool (*RetroLoadGame)(const struct retro_game_info *game);
typedef void (*RetroUnloadGame)(void);
typedef void (*RetroRun)(void);
typedef void (*RetroReset)(void);
typedef void* (*RetroGetMemoryData)(unsigned id);
typedef size_t (*RetroGetMemorySize)(unsigned id);
typedef bool (*RetroSerialize)(void *data, size_t size);
typedef size_t (*RetroSerializeSize)(void);
typedef bool (*RetroUnserialize)(const void *data, size_t size);
typedef void (*RetroGetSystemAVInfo)(struct retro_system_av_info *info);
typedef void (*RetroGetSystemInfo)(struct retro_system_info *info);
typedef void (*RetroSetEnvironment)(retro_environment_t);
typedef void (*RetroSetVideoRefresh)(retro_video_refresh_t);
typedef void (*RetroSetAudioSample)(retro_audio_sample_t);
typedef void (*RetroSetAudioSampleBatch)(retro_audio_sample_batch_t);
typedef void (*RetroSetInputPoll)(retro_input_poll_t);
typedef void (*RetroSetInputState)(retro_input_state_t);
typedef void (*RetroSetControllerPortDevice)(unsigned port, unsigned device);
typedef void (*RetroHWContextReset)(void);
class Core : public QObject {
Q_OBJECT
public:
Core(QObject *parent = NULL);
~Core();
struct CoreOption {
QString key;
QString name;
QStringList values;
QString val;
QByteArray valArray;
const char *valData;
};
bool run(QString contentPath);
void reset();
bool load(QString corePath);
void unload();
bool isLoaded() const;
bool isResolved() const;
bool isRunning() const;
void setMuted(bool on);
void setPaused(bool on);
bool isMuted() const;
bool isPaused() const;
bool saveState();
bool loadState();
bool saveSRAM();
bool loadSRAM();
void setupAudio();
bool loadContentIntoMemory();
bool isHWRender();
bool supportsNoGame();
void blitFBO();
void setVariablesChanged(bool changed);
static Core* instance();
float getVideoRate();
void* getProcAddress(const char *sym);
QMap<QString, CoreOption>& getCoreOptions();
QImage* getImage();
/* callbacks used by the core, which are set with the "set" function pointers */
bool environment(unsigned cmd, void *data);
void videoRefresh(const void *data, unsigned width, unsigned height, size_t pitch);
void audioSample(int16_t left, int16_t right);
size_t audioSampleBatch(const int16_t *data, size_t frames);
void inputPoll();
int16_t inputState(unsigned port, unsigned device, unsigned index, unsigned id);
uintptr_t getCurrentFramebuffer();
signals:
void audioReady(QAudioFormat*);
void gotAudioSample(int16_t, int16_t);
void gotAudioSampleBatch(const int16_t*, size_t);
void doneRendering();
void gotFrameSwap();
void coreOptionsChanged();
void repaint();
public slots:
void onAudioStreamReady();
void onGotFrameSwap();
void render();
private:
enum retro_pixel_format m_pixFmt;
struct retro_system_info m_info;
struct retro_game_info m_gameInfo;
unsigned char *m_imgData;
unsigned m_width;
unsigned m_height;
size_t m_pitch;
bool m_isAudioStreamReady;
bool m_isMuted;
bool m_isPaused;
bool m_variablesChanged;
bool m_isHWRender;
bool m_noGame;
bool m_bottomLeftOrigin;
QImage m_img;
QMap<QString, CoreOption> m_options;
QMap<QString, unsigned> m_controllerInfo;
QLibrary m_library;
bool m_isLoaded;
bool m_isInited;
bool m_isResolved;
bool m_isRunning;
bool m_doRender;
QString m_path;
QByteArray m_pathArray;
const char *m_pathData;
QString m_extractedPath;
QByteArray m_extractedPathArray;
const char *m_extractedPathData;
char *m_saveStateData;
size_t m_saveStateDataSize;
QElapsedTimer m_elapsedTimer;
QTimer m_timer;
QTemporaryDir m_tempDir;
char *m_gameData;
int64_t m_gameDataSize;
QDir m_dataDir;
char m_savePath[4096];
char m_statePath[4096];
char m_systemPath[4096];
/* functions exported by the core that the frontend can call */
RetroInit m_retroInit;
RetroDeinit m_retroDeinit;
RetroRun m_retroRun;
RetroReset m_retroReset;
RetroLoadGame m_retroLoadGame;
RetroUnloadGame m_retroUnloadGame;
RetroGetMemoryData m_retroGetMemoryData;
RetroGetMemorySize m_retroGetMemorySize;
RetroSerialize m_retroSerialize;
RetroSerializeSize m_retroSerializeSize;
RetroUnserialize m_retroUnserialize;
RetroGetSystemAVInfo m_retroGetSystemAVInfo;
RetroGetSystemInfo m_retroGetSystemInfo;
RetroSetControllerPortDevice m_retroSetControllerPortDevice;
RetroHWContextReset m_retroHWContextReset;
/* used to tell the core where our callbacks are */
RetroSetEnvironment m_retroSetEnvironment;
RetroSetVideoRefresh m_retroSetVideoRefresh;
RetroSetAudioSample m_retroSetAudioSample;
RetroSetAudioSampleBatch m_retroSetAudioSampleBatch;
RetroSetInputPoll m_retroSetInputPoll;
RetroSetInputState m_retroSetInputState;
Q_DISABLE_COPY(Core)
};
#endif // __CORE_H_

18
core/core.pri

@ -0,0 +1,18 @@
#
# Copyright 2020-2021 Brad Parker
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
SOURCES += core/core.cpp core/coreoptionsdialog.cpp core/input.cpp core/video.cpp core/library.cpp core/env.cpp core/audio.cpp core/state.cpp core/content.cpp
HEADERS += core/core.h core/coreoptionsdialog.h

254
core/coreoptionsdialog.cpp

@ -0,0 +1,254 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <QCloseEvent>
#include <QResizeEvent>
#include <QPainter>
#include <QScrollArea>
#include <QVBoxLayout>
#include <QFormLayout>
#include <QComboBox>
#include <QToolButton>
#include <QAction>
#include <QTimer>
#include <QLabel>
#include "coreoptionsdialog.h"
#include "core.h"
// adapted from RetroArch's core options dialog that I originally wrote here: https://github.com/libretro/RetroArch/blob/v1.8.6/ui/drivers/qt/coreoptionsdialog.cpp
CoreOptionsDialog::CoreOptionsDialog(QWidget *parent, Core *core) :
QDialog(parent)
,m_layout()
,m_scrollArea()
,m_core(core)
{
setWindowTitle(tr("Core Options"));
setWindowFlags(Qt::Widget); // no close button exists otherwise
setObjectName("coreOptionsDialog");
resize(720, 480);
QTimer::singleShot(0, this, SLOT(clearLayout()));
}
CoreOptionsDialog::~CoreOptionsDialog() {
}
void CoreOptionsDialog::resizeEvent(QResizeEvent *event) {
QDialog::resizeEvent(event);
if(!m_scrollArea)
return;
m_scrollArea->resize(event->size());
emit resized(event->size());
}
void CoreOptionsDialog::closeEvent(QCloseEvent *event) {
QDialog::closeEvent(event);
emit closed();
}
void CoreOptionsDialog::paintEvent(QPaintEvent *event) {
QStyleOption o;
QPainter p;
o.initFrom(this);
p.begin(this);
style()->drawPrimitive(QStyle::PE_Widget, &o, &p, this);
p.end();
QDialog::paintEvent(event);
}
void CoreOptionsDialog::clearLayout() {
QWidget *widget = nullptr;
if(m_scrollArea) {
for(QObject *obj : children()) {
obj->deleteLater();
}
}
m_layout = new QVBoxLayout();
widget = new QWidget();
widget->setLayout(m_layout);
widget->setObjectName("coreOptionsWidget");
m_scrollArea = new QScrollArea();
m_scrollArea->setParent(this);
m_scrollArea->setWidgetResizable(true);
m_scrollArea->setWidget(widget);
m_scrollArea->setObjectName("coreOptionsScrollArea");
m_scrollArea->show();
}
void CoreOptionsDialog::reload() {
buildLayout();
}
void CoreOptionsDialog::onCoreOptionComboBoxCurrentIndexChanged(int index) {
QMap<QString, Core::CoreOption> &coreOptions = m_core->getCoreOptions();
QComboBox *comboBox = qobject_cast<QComboBox*>(sender());
QString key, val;
if(!comboBox)
return;
key = comboBox->itemData(index, Qt::UserRole).toString();
val = comboBox->itemText(index);
if(coreOptions.size() > 0) {
Core::CoreOption &option = coreOptions[key];
for(int k = 0; k < option.values.size(); k++) {
QString str = option.values.at(k);
if(!str.isEmpty() && str == val) {
option.val = str;
option.valArray = option.val.toUtf8();
option.valData = option.valArray.constData();
m_core->setVariablesChanged(true);
}
}
}
}
void CoreOptionsDialog::buildLayout() {
QFormLayout *form = nullptr;
const QMap<QString, Core::CoreOption> &coreOptions = m_core->getCoreOptions();
clearLayout();
if(coreOptions.size() > 0) {
form = new QFormLayout();
QToolButton *resetAllButton = new QToolButton(this);
resetAllButton->setDefaultAction(new QAction(tr("Reset All"), this));
connect(resetAllButton, &QToolButton::clicked, this, &CoreOptionsDialog::onCoreOptionResetAllClicked);
const QList<Core::CoreOption> &options = coreOptions.values();
for(int j = 0; j < coreOptions.size(); j++) {
const Core::CoreOption &option = options.at(j);
const QString &desc = option.name;
const QString &val = option.val;
QComboBox *comboBox = nullptr;
QLabel *descLabel = nullptr;
QHBoxLayout *comboLayout = nullptr;
QToolButton *resetButton = nullptr;
if(desc.isEmpty())
continue;
if(option.values.size() == 0)
continue;
comboLayout = new QHBoxLayout();
descLabel = new QLabel(desc, this);
comboBox = new QComboBox(this);
comboBox->setObjectName("coreOptionComboBox");
resetButton = new QToolButton(this);
resetButton->setObjectName("resetButton");
resetButton->setDefaultAction(new QAction(tr("Reset"), this));
resetButton->setProperty("comboBox", QVariant::fromValue(comboBox));
connect(resetButton, &QToolButton::clicked, this, &CoreOptionsDialog::onCoreOptionResetClicked);
for(int k = 0; k < option.values.size(); k++) {
comboBox->addItem(option.values.at(k), option.key);
}
comboBox->setCurrentText(val);
// NOTE: if new core options interface is added, default may be one other than the first
comboBox->setProperty("default_index", 0);
// Only connect the signal after setting the default item
connect(comboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CoreOptionsDialog::onCoreOptionComboBoxCurrentIndexChanged);
comboLayout->addWidget(comboBox);
comboLayout->addWidget(resetButton);
form->addRow(descLabel, comboLayout);
}
form->addRow(resetAllButton, new QWidget(this));
m_layout->addLayout(form);
}else{
QLabel *noParamsLabel = new QLabel(tr("No core options available."), this);
noParamsLabel->setAlignment(Qt::AlignCenter);
m_layout->addWidget(noParamsLabel);
}
m_layout->addItem(new QSpacerItem(20, 20, QSizePolicy::Minimum, QSizePolicy::Expanding));
resize(width() + 1, height());
show();
resize(width() - 1, height());
}
void CoreOptionsDialog::onCoreOptionResetClicked() {
QToolButton *button = qobject_cast<QToolButton*>(sender());
QComboBox *comboBox = nullptr;
int default_index = 0;
bool ok = false;
if(!button)
return;
comboBox = qobject_cast<QComboBox*>(button->property("comboBox").value<QComboBox*>());
if(!comboBox)
return;
default_index = comboBox->property("default_index").toInt(&ok);
if(!ok)
return;
if(default_index >= 0 && default_index < comboBox->count())
comboBox->setCurrentIndex(default_index);
}
void CoreOptionsDialog::onCoreOptionResetAllClicked() {
QList<QComboBox*> comboBoxes = findChildren<QComboBox*>("coreOptionComboBox");
for(int i = 0; i < comboBoxes.count(); i++) {
QComboBox *comboBox = comboBoxes.at(i);
int default_index = 0;
bool ok = false;
if(!comboBox)
continue;
default_index = comboBox->property("default_index").toInt(&ok);
if(!ok)
continue;
if(default_index >= 0 && default_index < comboBox->count())
comboBox->setCurrentIndex(default_index);
}
}

58
core/coreoptionsdialog.h

@ -0,0 +1,58 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef __COREOPTIONSDIALOG_H_
#define __COREOPTIONSDIALOG_H_
#include <QDialog>
#include <QPointer>
class QCloseEvent;
class QResizeEvent;
class QVBoxLayout;
class QFormLayout;
class QLayout;
class QScrollArea;
class Core;
class CoreOptionsDialog : public QDialog
{
Q_OBJECT
public:
CoreOptionsDialog(QWidget *parent, Core *core);
~CoreOptionsDialog();
signals:
void closed();
void resized(QSize size);
public slots:
void reload();
private slots:
void clearLayout();
void buildLayout();
void onCoreOptionComboBoxCurrentIndexChanged(int index);
void onCoreOptionResetClicked();
void onCoreOptionResetAllClicked();
private:
QPointer<QVBoxLayout> m_layout;
QPointer<QScrollArea> m_scrollArea;
Core *m_core;
protected:
void closeEvent(QCloseEvent *event);
void resizeEvent(QResizeEvent *event);
void paintEvent(QPaintEvent *event);
};
#endif // __COREOPTIONSDIALOG_H_

407
core/env.cpp

@ -0,0 +1,407 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <QGuiApplication>
#include <QRegularExpression>
#include <QScreen>
#include <iostream>
#include "libretro.h"
#include "core.h"
#include "../common/video.h"
#include "../frontend/mainwindow.h"
typedef struct {
const char *name;
unsigned cmd;
} EnvCommand;
static EnvCommand s_envCommands[] = {
{"RETRO_ENVIRONMENT_SET_ROTATION", 1},
{"RETRO_ENVIRONMENT_GET_OVERSCAN", 2},
{"RETRO_ENVIRONMENT_GET_CAN_DUPE", 3},
{"RETRO_ENVIRONMENT_SET_MESSAGE", 6},
{"RETRO_ENVIRONMENT_SHUTDOWN", 7},
{"RETRO_ENVIRONMENT_SET_PERFORMANCE_LEVEL", 8},
{"RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY", 9},
{"RETRO_ENVIRONMENT_SET_PIXEL_FORMAT", 10},
{"RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS", 11},
{"RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK", 12},
{"RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE", 13},
{"RETRO_ENVIRONMENT_SET_HW_RENDER", 14},
{"RETRO_ENVIRONMENT_GET_VARIABLE", 15},
{"RETRO_ENVIRONMENT_SET_VARIABLES", 16},
{"RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE", 17},
{"RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME", 18},
{"RETRO_ENVIRONMENT_GET_LIBRETRO_PATH", 19},
{"RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK", 21},
{"RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK", 22},
{"RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE", 23},
{"RETRO_ENVIRONMENT_GET_INPUT_DEVICE_CAPABILITIES", 24},
{"RETRO_ENVIRONMENT_GET_SENSOR_INTERFACE", (25 | RETRO_ENVIRONMENT_EXPERIMENTAL)},
{"RETRO_ENVIRONMENT_GET_CAMERA_INTERFACE", (26 | RETRO_ENVIRONMENT_EXPERIMENTAL)},
{"RETRO_ENVIRONMENT_GET_LOG_INTERFACE", 27},
{"RETRO_ENVIRONMENT_GET_PERF_INTERFACE", 28},
{"RETRO_ENVIRONMENT_GET_LOCATION_INTERFACE", 29},
{"RETRO_ENVIRONMENT_GET_CONTENT_DIRECTORY", 30},
{"RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY", 30},
{"RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY", 31},
{"RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO", 32},
{"RETRO_ENVIRONMENT_SET_PROC_ADDRESS_CALLBACK", 33},
{"RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO", 34},
{"RETRO_ENVIRONMENT_SET_CONTROLLER_INFO", 35},
{"RETRO_ENVIRONMENT_SET_MEMORY_MAPS", (36 | RETRO_ENVIRONMENT_EXPERIMENTAL)},
{"RETRO_ENVIRONMENT_SET_GEOMETRY", 37},
{"RETRO_ENVIRONMENT_GET_USERNAME", 38},
{"RETRO_ENVIRONMENT_GET_LANGUAGE", 39},
{"RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER", (40 | RETRO_ENVIRONMENT_EXPERIMENTAL)},
{"RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE", (41 | RETRO_ENVIRONMENT_EXPERIMENTAL)},
{"RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS", (42 | RETRO_ENVIRONMENT_EXPERIMENTAL)},
{"RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE", (43 | RETRO_ENVIRONMENT_EXPERIMENTAL)},
{"RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS", 44},
{"RETRO_ENVIRONMENT_SET_HW_SHARED_CONTEXT", (44 | RETRO_ENVIRONMENT_EXPERIMENTAL)},
{"RETRO_ENVIRONMENT_GET_VFS_INTERFACE", (45 | RETRO_ENVIRONMENT_EXPERIMENTAL)},
{"RETRO_ENVIRONMENT_GET_LED_INTERFACE", (46 | RETRO_ENVIRONMENT_EXPERIMENTAL)},
{"RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE", (47 | RETRO_ENVIRONMENT_EXPERIMENTAL)},
{"RETRO_ENVIRONMENT_GET_MIDI_INTERFACE", (48 | RETRO_ENVIRONMENT_EXPERIMENTAL)},
{"RETRO_ENVIRONMENT_GET_FASTFORWARDING", (49 | RETRO_ENVIRONMENT_EXPERIMENTAL)},
{"RETRO_ENVIRONMENT_GET_TARGET_REFRESH_RATE", (50 | RETRO_ENVIRONMENT_EXPERIMENTAL)},
{"RETRO_ENVIRONMENT_GET_INPUT_BITMASKS", (51 | RETRO_ENVIRONMENT_EXPERIMENTAL)},
{"RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION", 52},
{"RETRO_ENVIRONMENT_SET_CORE_OPTIONS", 53},
{"RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL", 54},
{"RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY", 55},
{"RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER", 56},
{"RETRO_ENVIRONMENT_GET_DISK_CONTROL_INTERFACE_VERSION", 57},
{"RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE", 58},
};
static void core_log(enum retro_log_level level, const char *fmt, ...) {
char buffer[4093] = {0};
char buffer2[4096] = {0};
static const char *levelstr[] = {"debug", "info", "warn", "error"};
va_list va;
va_start(va, fmt);
vsnprintf(buffer, sizeof(buffer), fmt, va);
va_end(va);
if(level == 0)
return;
sprintf(buffer2, "[%s] %s", levelstr[level], buffer);
fprintf(stdout, "%s", buffer2);
}
bool Core::environment(unsigned cmd, void *data) {
for(unsigned i = 0; i < sizeof(s_envCommands) / sizeof(s_envCommands[0]); ++i) {
if(s_envCommands[i].cmd == cmd) {
switch(cmd) {
case RETRO_ENVIRONMENT_SET_HW_SHARED_CONTEXT:
return true;
case RETRO_ENVIRONMENT_SET_HW_RENDER:
{
auto *cb = reinterpret_cast<struct retro_hw_render_callback*>(data);
if(cb->context_type == RETRO_HW_CONTEXT_OPENGL || cb->context_type == RETRO_HW_CONTEXT_OPENGL_CORE) {
cb->get_current_framebuffer = []() { return Core::instance()->getCurrentFramebuffer(); };
void* (*getProcAddressPtr)(const char *sym) = [](const char *sym) -> void* { return Core::instance()->getProcAddress(sym); };
cb->get_proc_address = reinterpret_cast<retro_hw_get_proc_address_t>(getProcAddressPtr);
m_retroHWContextReset = cb->context_reset;
m_isHWRender = true;
m_bottomLeftOrigin = cb->bottom_left_origin ? true : false;
std::cout << "core uses bottom left origin? " << (m_bottomLeftOrigin ? "yes" : "no") << std::endl;
return true;
}else{
return false;
}
break;
}
// tells the frontend what buttons it supports on what kind of devices, for however many users
case RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS:
{
auto *descs = reinterpret_cast<const struct retro_input_descriptor*>(data);
/* TODO: FIXME: I used an arbitrary limit of 100 */
for(unsigned j = 0; j < 100; ++j) {
if(descs[j].port == 0 && descs[j].device == 0 && descs[j].index == 0 && descs[j].id == 0 && descs[j].description == nullptr)
break;
if(descs[j].description && *(descs[j].description))
std::cout << "got input descriptor at port " << descs[j].port << " device " << descs[j].device << " index " << descs[j].index << " id " << descs[j].id << ": " << descs[j].description << std::endl;
else
std::cout << "got input descriptor at port " << descs[j].port << " device " << descs[j].device << " index " << descs[j].index << " id " << descs[j].id << ": (no description)" << std::endl;
}
return true;
}
// whether frontend supports the newer advanced core option interfaces
case RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION:
{
unsigned *ver = reinterpret_cast<unsigned*>(data);
// not yet
if(ver)
*ver = 0;
return true;
}
// asks the frontend if it changed any core variables
case RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE:
{
bool *update = reinterpret_cast<bool*>(data);
if(update)
*update = m_variablesChanged;
if(m_variablesChanged)
m_variablesChanged = false;
return true;
}
// tells the frontend we support running without any content loaded
case RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME:
{
bool *nogame = reinterpret_cast<bool*>(data);
m_noGame = nogame;
return true;
}
// tells the frontend what core options are available
case RETRO_ENVIRONMENT_SET_VARIABLES:
{
auto *vars = reinterpret_cast<const struct retro_variable*>(data);
std::cout << "core is sending its variables" << std::endl;
if(vars) {
int i = 0;
for(;;) {
if(vars[i].key && *(vars[i].key) && vars[i].value && *(vars[i].value)) {
//std::cout << "storing variable " << vars[i].key << ": " << vars[i].value << std::endl;
QStringList name_values = QString(vars[i].value).split(";");
if(name_values.size() > 0) {
CoreOption op{};
op.key = vars[i].key;
op.name = name_values.at(0);
QStringList values = name_values.at(1).split("|");
for(int j = 0; j < values.size(); ++j) {
QString val = values.at(j);
val = val.replace(QRegularExpression("^\\s+"), "");
val = val.replace(QRegularExpression("\\s+$"), "");
op.values.append(val);
}
// use first value as the default
op.val = op.values.at(0);
op.valArray = op.val.toUtf8();
op.valData = op.valArray.constData();
m_options[vars[i].key] = op;
}else{
//std::cerr << "no options found for variable " << vars[i].key << std::endl;
}
}else{
break;
}
++i;
}
}
emit coreOptionsChanged();
return true;
}
// asks the frontend for the current value of a variable
case RETRO_ENVIRONMENT_GET_VARIABLE:
{
auto *var = reinterpret_cast<struct retro_variable*>(data);
if(!var->key || !*(var->key)) {
static QString allEnvs;
static QByteArray allEnvsArray;
static const char *allEnvsData = nullptr;
std::cout << "got env " << cmd << " (" << s_envCommands[i].name << ") with no key, sending everything we have" << std::endl;
allEnvs.clear();
const QList<Core::CoreOption> options = m_options.values();
for(const CoreOption &op : options) {
allEnvs += op.key + "=" + op.val + ";";
}
allEnvsArray = allEnvs.toUtf8();
allEnvsData = allEnvsArray.constData();
var->key = allEnvsData;
var->value = NULL;
return true;
}
std::cout << "got env " << cmd << " (" << s_envCommands[i].name << ") with key \"" << var->key << "\"" << std::endl;
if(m_options.contains(var->key)) {
const CoreOption &op = m_options.value(var->key);
var->value = op.valData;
std::cout << "frontend to core: variable " << var->key << " = " << var->value << std::endl;
}else{
var->value = NULL;
return false;
}
return true;
}
// tells the frontend there was a major change in the A/V setup of the core, in RetroArch's case this constitutes an entire teardown/reinit of the whole application, but we don't need to
case RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO:
{
auto *info = reinterpret_cast<const struct retro_system_av_info*>(data);
std::cout << "got env " << cmd << " (" << s_envCommands[i].name << ")" << std::endl;
std::cout << "desired AV info: " << info->geometry.base_width << "x" << info->geometry.base_height << " (max " << info->geometry.max_width << "x" << info->geometry.max_height << ") aspect " << info->geometry.aspect_ratio << " fps " << info->timing.fps << std::endl;
// TODO: FIXME: not implemented: changing of audio parameters
auto videoState = VideoState::instance();
struct retro_system_av_info *avInfo = videoState->avInfo();
videoState->setAspect(info->geometry.aspect_ratio);
videoState->setBaseWidth(info->geometry.base_width);
videoState->setBaseHeight(info->geometry.base_height);
avInfo->geometry.aspect_ratio = info->geometry.aspect_ratio;
avInfo->geometry.base_width = info->geometry.base_width;
avInfo->geometry.base_height = info->geometry.base_height;
avInfo->geometry.max_width = info->geometry.max_width;
avInfo->geometry.max_height = info->geometry.max_height;
avInfo->timing.fps = info->timing.fps;
avInfo->timing.sample_rate = info->timing.sample_rate;
float screenRefresh = 0;
QScreen *screen = QGuiApplication::primaryScreen();
if(screen)
screenRefresh = screen->refreshRate();
// if core refresh rate is within half a percent of the screen's, just call it equal, it's hopefully close enough to not be noticeable
if((screenRefresh - (screenRefresh * 0.005)) <= avInfo->timing.fps && (screenRefresh + (screenRefresh * 0.005)) >= avInfo->timing.fps) {
avInfo->timing.fps = screenRefresh;
}
return true;
}
// if the core needs to handle saving on its own, this asks the frontend for a path to do so in
case RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY:
{
const char **save_dir = reinterpret_cast<const char**>(data);
*save_dir = m_savePath;
std::cout << "core requesting current save directory: " << *save_dir << std::endl;
return true;
}
case RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY:
{
const char **system_dir = reinterpret_cast<const char**>(data);
*system_dir = m_systemPath;
std::cout << "core requesting current assets directory: " << *system_dir << std::endl;
return true;
}
// if the core needs to load a BIOS or other support files, this asks the frontend for a path to find them in
case RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY:
{
const char **system_dir = reinterpret_cast<const char**>(data);
*system_dir = m_systemPath;
std::cout << "core requesting current system directory: " << *system_dir << std::endl;
return true;
}
// asks the frontend for a callback to send core log messages to
case RETRO_ENVIRONMENT_GET_LOG_INTERFACE:
{
auto *cb = reinterpret_cast<struct retro_log_callback*>(data);
cb->log = core_log;
return true;
}
case RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE:
break;
case RETRO_ENVIRONMENT_SET_CONTROLLER_INFO:
{
const auto *info = reinterpret_cast<const struct retro_controller_info*>(data);
std::cout << "core has set the controller info as:" << std::endl;
unsigned i = 0;
for(;;) {
const auto &desc = info->types[i];
if(i == info->num_types || (!desc.desc || !*(desc.desc)))
break;
std::cout << desc.desc << " (" << desc.id << ")" << std::endl;
m_controllerInfo[desc.desc] = desc.id;
++i;
}
return true;
}
case RETRO_ENVIRONMENT_SET_GEOMETRY:
{
auto *geom = reinterpret_cast<struct retro_game_geometry*>(data);
std::cout << "core requesting geometry change: base " << geom->base_width << "x" << geom->base_height << " max " << geom->max_width << "x" << geom->max_height << " aspect " << geom->aspect_ratio << std::endl;
auto videoState = VideoState::instance();
videoState->setAspect(geom->aspect_ratio);
videoState->setBaseWidth(geom->base_width);
videoState->setBaseHeight(geom->base_height);
return true;
}
// tells the frontend what pixel format the core wants to use, might be called multiple times to find a preferred format if the frontend rejects anything.
case RETRO_ENVIRONMENT_SET_PIXEL_FORMAT:
{
auto *pixfmt = reinterpret_cast<const enum retro_pixel_format*>(data);
m_pixFmt = *pixfmt;
if(m_pixFmt == RETRO_PIXEL_FORMAT_XRGB8888) {
std::cout << "core requesting pixel format " << *pixfmt << " (XRGB8888)" << std::endl;
return true;
}else if(m_pixFmt == RETRO_PIXEL_FORMAT_RGB565) {
std::cout << "core requesting pixel format " << *pixfmt << " (RGB565)" << std::endl;
return true;
}
// for now we only support XRGB8888 and RGB565
std::cout << "core requesting unsupported pixel format " << pixfmt << std::endl;
return false;
}
default:
//std::cout << "got unhandled env " << cmd << " (" << s_envCommands[i].name << ")" << std::endl;
break;
}
break;
}
}
return false;
}

64
core/input.cpp

@ -0,0 +1,64 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "core.h"
#include "../common/input.h"
void Core::inputPoll() {
}
// The core is asking the frontend for the state of a specific button for a specific player.
// Actually the core is constantly asking for the state of all the buttons it cares about by calling this function for every button on every frame.
int16_t Core::inputState(unsigned port, unsigned device, unsigned index, unsigned id) {
// we only support player 1 (port 0) for now
if(port != 0)
return 0;
// core is asking for state of a joypad button, this is basically anything on a gamepad that's not analog (face buttons, d-pad directions, digital triggers like L/R etc.)
if(device == RETRO_DEVICE_JOYPAD && index == 0) {
if(id <= RETRO_DEVICE_ID_JOYPAD_R3) {
return InputState::instance()->getInputState(id);
}
// core is asking for the state of analog axes (left analog, right analog)
// libretro API also supports analog *buttons* (or triggers), but we don't
}else if(device == RETRO_DEVICE_ANALOG && index == 0) {
// we don't support the right analog for now
if(index == RETRO_DEVICE_INDEX_ANALOG_LEFT) {
if(id == RETRO_DEVICE_ID_ANALOG_X) {
return InputState::instance()->getAnalogInputState(RETRO_DEVICE_ID_ANALOG_X);
}else if(id == RETRO_DEVICE_ID_ANALOG_Y) {
return InputState::instance()->getAnalogInputState(RETRO_DEVICE_ID_ANALOG_Y);
}
}
// core is asking for the state of a mouse pointer (if it supports one)
}else if(device == RETRO_DEVICE_MOUSE && index == 0) {
switch(id) {
case RETRO_DEVICE_ID_MOUSE_X:
return InputState::instance()->getMousePosRelX();
case RETRO_DEVICE_ID_MOUSE_Y:
return InputState::instance()->getMousePosRelY();
case RETRO_DEVICE_ID_MOUSE_LEFT:
return InputState::instance()->getMouseButtonsState() & Qt::LeftButton;
case RETRO_DEVICE_ID_MOUSE_RIGHT:
return InputState::instance()->getMouseButtonsState() & Qt::RightButton;
default:
break;
}
}
return 0;
}

120
core/library.cpp

@ -0,0 +1,120 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <iostream>
#include "core.h"
void Core::unload() {
if(m_isRunning) {
m_isRunning = false;
std::cout << "retro_unload_game";
m_retroUnloadGame();
std::cout << "." << std::endl;
}
if(m_isInited) {
m_isInited = false;
std::cout << "retro_deinit";
m_retroDeinit();
std::cout << "." << std::endl;
}
if(m_isLoaded) {
m_isLoaded = false;
if(m_library.isLoaded()) {
std::cout << "a library is loaded" << std::endl;
if(!m_library.unload()) {
std::cerr << "could not unload library" << std::endl;
}else{
std::cout << "unloaded library" << std::endl;
}
}else{
std::cerr << "library not loaded" << std::endl;
}
}
}
bool Core::load(QString corePath) {
m_library.setFileName(QFileInfo(corePath).absoluteFilePath());
std::cout << "loading library: " << qUtf8Printable(QFileInfo(m_library.fileName()).absoluteFilePath()) << std::endl;
if (!m_library.load()) {
std::cerr << "library could not be loaded: " << qUtf8Printable(m_library.errorString()) << std::endl;
return false;
}else{
std::cout << "library was loaded successfully" << std::endl;
}
m_isLoaded = true;
#define resolv(name, ptr, sym) \
if(!(ptr = reinterpret_cast<name>(m_library.resolve(sym)))) { \
std::cerr << "could not resolve " #sym " function" << std::endl; \
return false; \
}
resolv(RetroInit, m_retroInit, "retro_init");
resolv(RetroDeinit, m_retroDeinit, "retro_deinit");
resolv(RetroRun, m_retroRun, "retro_run");
resolv(RetroReset, m_retroReset, "retro_reset");
resolv(RetroLoadGame, m_retroLoadGame, "retro_load_game");
resolv(RetroUnloadGame, m_retroUnloadGame, "retro_unload_game");
resolv(RetroGetMemoryData, m_retroGetMemoryData, "retro_get_memory_data");
resolv(RetroGetMemorySize, m_retroGetMemorySize, "retro_get_memory_size");
resolv(RetroSerialize, m_retroSerialize, "retro_serialize");
resolv(RetroSerializeSize, m_retroSerializeSize, "retro_serialize_size");
resolv(RetroUnserialize, m_retroUnserialize, "retro_unserialize");
resolv(RetroGetSystemAVInfo, m_retroGetSystemAVInfo, "retro_get_system_av_info");
resolv(RetroGetSystemInfo, m_retroGetSystemInfo, "retro_get_system_info");
resolv(RetroSetControllerPortDevice, m_retroSetControllerPortDevice, "retro_set_controller_port_device");
resolv(RetroSetEnvironment, m_retroSetEnvironment, "retro_set_environment");
resolv(RetroSetVideoRefresh, m_retroSetVideoRefresh, "retro_set_video_refresh");
resolv(RetroSetAudioSample, m_retroSetAudioSample, "retro_set_audio_sample");
resolv(RetroSetAudioSampleBatch, m_retroSetAudioSampleBatch, "retro_set_audio_sample_batch");
resolv(RetroSetInputPoll, m_retroSetInputPoll, "retro_set_input_poll");
resolv(RetroSetInputState, m_retroSetInputState, "retro_set_input_state");
m_isResolved = true;
std::cout << "resolved necessary functions" << std::endl;
std::cout << "setting callback functions" << std::endl;
retro_environment_t environmentCallback = [](unsigned cmd, void *data) -> bool { return Core::instance()->environment(cmd, data); };
retro_video_refresh_t videoRefreshCallback = [](const void *data, unsigned width, unsigned height, size_t pitch) { return Core::instance()->videoRefresh(data, width, height, pitch); };
retro_audio_sample_t audioSampleCallback = [](int16_t left, int16_t right) { return Core::instance()->audioSample(left, right); };
retro_audio_sample_batch_t audioSampleBatchCallback = [](const int16_t *data, size_t frames) { return Core::instance()->audioSampleBatch(data, frames); };
retro_input_poll_t inputPollCallback = []() { return Core::instance()->inputPoll(); };
retro_input_state_t inputStateCallback = [](unsigned port, unsigned device, unsigned index, unsigned id) { return Core::instance()->inputState(port, device, index, id); };
m_retroSetEnvironment(environmentCallback);
m_retroSetVideoRefresh(videoRefreshCallback);
m_retroSetAudioSample(audioSampleCallback);
m_retroSetAudioSampleBatch(audioSampleBatchCallback);
m_retroSetInputPoll(inputPollCallback);
m_retroSetInputState(inputStateCallback);
std::cout << "retro_init." << std::endl;
m_retroInit();
m_isInited = true;
return true;
}

2880
core/libretro.h

File diff suppressed because it is too large

249
core/state.cpp

@ -0,0 +1,249 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <iostream>
#include "core.h"
bool Core::saveSRAM() {
if(m_isInited) {
if(m_library.isLoaded()) {
void *data = m_retroGetMemoryData(RETRO_MEMORY_SAVE_RAM);
size_t size = m_retroGetMemorySize(RETRO_MEMORY_SAVE_RAM);
QDir saveDir(m_savePath);
saveDir.mkpath(m_info.library_name);
if(!saveDir.cd(m_info.library_name)) {
std::cerr << "could not change directory to core name!" << std::endl;
// save anyway in the current dir, never throw away a user's work
}
if(saveDir.exists()) {
QString saveFileString = QFileInfo(m_path).fileName();
int index = saveFileString.lastIndexOf('.');
if(index > -1) {
saveFileString.remove(index, saveFileString.size() - index);
saveFileString += ".srm";
}
QFile saveFile(saveDir.filePath(saveFileString));
if(saveFile.open(QIODevice::WriteOnly)) {
saveFile.write((const char*)data, size);
saveFile.close();
std::cout << "wrote save file successfully to: " << qUtf8Printable(QFileInfo(saveFile).absoluteFilePath()) << std::endl;
}else{
std::cerr << "cannot open save file for writing." << std::endl;
// TODO: should we try to save SRAM somewhere else instead of abandoning it?
return false;
}
}else{
std::cerr << "save directory does not exist." << std::endl;
// TODO: should we try to save SRAM somewhere else instead of abandoning it?
return false;
}
}
}else{
return false;
}
return true;
}
bool Core::loadSRAM() {
// load SRAM if any
QDir saveDir(m_savePath);
saveDir.mkpath(m_info.library_name);
saveDir.cd(m_info.library_name);
if(saveDir.exists()) {
QString saveFileString = QFileInfo(m_path).fileName();
int index = saveFileString.lastIndexOf('.');
if(index > -1) {
saveFileString.remove(index, saveFileString.size() - index);
saveFileString += ".srm";
}
QFile saveFile(saveDir.filePath(saveFileString));
if(saveFile.exists()) {
if(saveFile.open(QIODevice::ReadOnly)) {
QByteArray sramArray = saveFile.readAll();
saveFile.close();
const char *sramData = sramArray.constData();
void *data = m_retroGetMemoryData(RETRO_MEMORY_SAVE_RAM);
size_t size = m_retroGetMemorySize(RETRO_MEMORY_SAVE_RAM);
if(sramArray.size() == static_cast<ssize_t>(size)) {
memcpy(data, sramData, qMin(static_cast<size_t>(sramArray.size()), size));
std::cout << "save file loaded successfully from: " << qUtf8Printable(QFileInfo(saveFile).absoluteFilePath()) << std::endl;
}else{
std::cerr << "save file size is incorrect! not loading it." << std::endl;
return false;
}
}else{
std::cerr << "cannot open save file for reading." << std::endl;
return false;
}
}else{
std::cerr << "no existing save file found for this game." << std::endl;
return false;
}
}else{
std::cerr << "save directory does not exist." << std::endl;
return false;
}
return true;
}
void Core::reset() {
m_retroReset();
}
bool Core::saveState() {
size_t size = m_retroSerializeSize();
if(size == 0) {
std::cerr << "required state size is zero! not saving anything." << std::endl;
return false;
}
if(m_saveStateData) {
delete[] m_saveStateData;
m_saveStateData = nullptr;
m_saveStateDataSize = 0;
}
m_saveStateData = new char[size];
m_saveStateDataSize = size;
bool success = m_retroSerialize(m_saveStateData, m_saveStateDataSize);
if(!success) {
std::cerr << "core failed to save state." << std::endl;
return false;
}
QDir stateDir(m_statePath);
stateDir.mkpath(m_info.library_name);
if(!stateDir.cd(m_info.library_name)) {
std::cerr << "could not change directory to core name!" << std::endl;
// save anyway in the current dir, never throw away a user's work
}
if(stateDir.exists()) {
QString stateFileString = QFileInfo(m_path).fileName();
int index = stateFileString.lastIndexOf('.');
if(index > -1) {
stateFileString.remove(index, stateFileString.size() - index);
stateFileString += ".state";
}
QFile stateFile(stateDir.filePath(stateFileString));
if(stateFile.open(QIODevice::WriteOnly)) {
stateFile.write((const char*)m_saveStateData, m_saveStateDataSize);
stateFile.close();
std::cout << "wrote state file successfully to: " << qUtf8Printable(QFileInfo(stateFile).absoluteFilePath()) << std::endl;
return true;
}else{
std::cerr << "cannot open state file for writing." << std::endl;
return false;
}
}else{
std::cerr << "state directory does not exist." << std::endl;
return false;
}
return false;
}
bool Core::loadState() {
size_t size = m_retroSerializeSize();
if(size == 0) {
std::cerr << "required state size is zero! not loading anything." << std::endl;
return false;
}
QDir stateDir(m_statePath);
stateDir.mkpath(m_info.library_name);
stateDir.cd(m_info.library_name);
if(stateDir.exists()) {
QString stateFileString = QFileInfo(m_path).fileName();
int index = stateFileString.lastIndexOf('.');
if(index > -1) {
stateFileString.remove(index, stateFileString.size() - index);
stateFileString += ".state";
}
QFile stateFile(stateDir.filePath(stateFileString));
if(stateFile.exists()) {
if(stateFile.open(QIODevice::ReadOnly)) {
QByteArray stateArray = stateFile.readAll();
stateFile.close();
const char *stateData = stateArray.constData();
if(stateArray.size() == static_cast<ssize_t>(size)) {
if(m_retroUnserialize(stateData, size)) {
std::cout << "state with size of " << size << " bytes loaded successfully from: " << qUtf8Printable(QFileInfo(stateFile).absoluteFilePath()) << std::endl;
return true;
}
std::cerr << "failed to load state." << std::endl;
return false;
}else{
std::cerr << "state file size is incorrect! (" << stateArray.size() << " != " << size << ") trying anyway. (note: some cores store the file path in the state so they will always be a false positive if the file is moved or renamed)" << std::endl;
if(m_retroUnserialize(stateData, size)) {
std::cout << "state with size of " << stateArray.size() << " (not " << size << ") bytes loaded successfully from: " << qUtf8Printable(QFileInfo(stateFile).absoluteFilePath()) << std::endl;
return true;
}
}
}else{
std::cerr << "cannot open state file for reading." << std::endl;
return false;
}
}else{
std::cerr << "no existing state file found for this game. " << qUtf8Printable(QFileInfo(stateFile).absoluteFilePath()) << std::endl;
return false;
}
}else{
std::cerr << "state directory does not exist." << std::endl;
return false;
}
return false;
}

145
core/video.cpp

@ -0,0 +1,145 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <QOpenGLContext>
#include <QOpenGLFramebufferObject>
#include <iostream>
#include "core.h"
#include "../common/video.h"
void Core::videoRefresh(const void *data, unsigned width, unsigned height, size_t pitch) {
if(!data || m_isHWRender) {
return;
}
if(width != m_width || height != m_height || pitch != m_pitch) {
std::cout << "new video size: " << width << "x" << height << " (pitch " << pitch << ")" << std::endl;
if(m_imgData) {
delete []m_imgData;
}
m_width = width;
m_height = height;
m_pitch = pitch;
m_imgData = new unsigned char[pitch * height];
}
memcpy(m_imgData, data, pitch * height);
if(m_pixFmt == RETRO_PIXEL_FORMAT_0RGB1555) {
m_img = QImage(m_imgData, width, height, pitch, QImage::Format_RGB555);
}else if(m_pixFmt == RETRO_PIXEL_FORMAT_XRGB8888) {
m_img = QImage(m_imgData, width, height, pitch, QImage::Format_RGB32);
}else if(m_pixFmt == RETRO_PIXEL_FORMAT_RGB565) {
m_img = QImage(m_imgData, width, height, pitch, QImage::Format_RGB16);
}
m_img = m_img.mirrored(true, false);
m_img = m_img.mirrored(true, false);
}
float Core::getVideoRate() {
return VideoState::instance()->avInfo()->timing.fps;
}
void Core::render() {
if(m_timer.isActive())
return;
emit repaint();
emit doneRendering();
}
void Core::onGotFrameSwap() {
if(m_timer.isActive())
return;
if(m_doRender) {
m_elapsedTimer.start();
QOpenGLContext *ctx = VideoState::instance()->context();
QOpenGLFramebufferObject *fbo = VideoState::instance()->fbo();
if(ctx) {
ctx->makeCurrent(ctx->surface());
if(m_isHWRender && fbo) {
fbo->bind();
}
m_retroRun();
if(m_isHWRender && fbo) {
fbo->release();
}
}
m_timer.disconnect();
connect(&m_timer, &QTimer::timeout, this, &Core::render);
m_timer.start(0);
}
}
uintptr_t Core::getCurrentFramebuffer() {
QOpenGLContext *ctx = VideoState::instance()->context();
if(ctx) {
QOpenGLFramebufferObject *fbo = VideoState::instance()->fbo();
if(fbo) {
return fbo->handle();
}
}
return 0;
}
void* Core::getProcAddress(const char *sym) {
QOpenGLContext *ctx = VideoState::instance()->context();
if(ctx)
return reinterpret_cast<void*>(ctx->getProcAddress(sym));
return nullptr;
}
bool Core::isHWRender() {
return m_isHWRender;
}
void Core::blitFBO() {
// hardware-accelerated cores draw into their own frame buffer object, we must push that to the screen now
auto videoState = VideoState::instance();
QOpenGLContext *ctx = videoState->context();
QOpenGLFramebufferObject *fbo = videoState->fbo();
struct retro_system_av_info *avInfo = videoState->avInfo();
if(ctx && fbo && fbo->isValid() && fbo->hasOpenGLFramebufferBlit()) {
fbo->bind();
// Copy the rendered frame buffer data from the core directly onto the screen.
QOpenGLFramebufferObject::blitFramebuffer(0, videoState->aspectCorrectedRect(), fbo, QRect(0, 0, avInfo->geometry.base_width, avInfo->geometry.base_height), GL_COLOR_BUFFER_BIT, GL_NEAREST);
fbo->release();
}
}
QImage* Core::getImage() {
return &m_img;
}

53
frontend/audio.cpp

@ -0,0 +1,53 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef NO_SOUND
#include <QAudioOutput>
#include <QAudioFormat>
#endif
#include <iostream>
#include "mainwindow.h"
#ifndef NO_SOUND
void MainWindow::startAudio(QAudioFormat *format) {
m_audioOutput = new QAudioOutput(*format, this);
connect(m_audioOutput, &QAudioOutput::stateChanged, this, &MainWindow::audioStateChanged);
m_audioStream = m_audioOutput->start();
}
void MainWindow::audioStateChanged(QAudio::State state) {
if(state == QAudio::IdleState) {
if(!m_audioReady) {
m_audioReady = true;
std::cout << "ready to play audio." << std::endl;
emit audioStreamReady();
}
}
}
#endif
void MainWindow::onAudioSample(int16_t left, int16_t right) {
m_audioStream->write(reinterpret_cast<const char*>(left), sizeof(int16_t));
m_audioStream->write(reinterpret_cast<const char*>(right), sizeof(int16_t));
}
void MainWindow::onAudioSampleBatch(const int16_t *data, size_t frames) {
// each "frame" is two int16_t's (one for each channel of a stereo track)
m_audioStream->write(reinterpret_cast<const char*>(data), frames * sizeof(int16_t) * 2);
}

18
frontend/frontend.pri

@ -0,0 +1,18 @@
#
# Copyright 2020-2021 Brad Parker
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
SOURCES += frontend/main.cpp frontend/mainwindow.cpp frontend/video.cpp frontend/audio.cpp frontend/input.cpp
HEADERS += frontend/mainwindow.h

48
frontend/input.cpp

@ -0,0 +1,48 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <QMouseEvent>
#include <QKeyEvent>
#include "../common/input.h"
#include "../core/core.h"
#include "../core/coreoptionsdialog.h"
#include "mainwindow.h"
void MainWindow::mousePressEvent(QMouseEvent *event) {
InputState::instance()->mousePressEvent(event);
}
void MainWindow::mouseReleaseEvent(QMouseEvent *event) {
InputState::instance()->mouseReleaseEvent(event);
}
void MainWindow::mouseMoveEvent(QMouseEvent *event) {
InputState::instance()->mouseMoveEvent(event);
}
void MainWindow::keyPressEvent(QKeyEvent *event) {
bool handled = InputState::instance()->keyPressEvent(event);
if(!handled)
QOpenGLWidget::keyPressEvent(event);
}
void MainWindow::keyReleaseEvent(QKeyEvent *event) {
bool handled = InputState::instance()->keyReleaseEvent(event);
if(!handled)
QOpenGLWidget::keyReleaseEvent(event);
}

100
frontend/main.cpp

@ -0,0 +1,100 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <QApplication>
#include <QCommandLineParser>
#include <QSettings>
#include <iostream>
#include "mainwindow.h"
#ifdef Q_OS_UNIX
#include <signal.h>
static void signal_handler(int) {
QApplication::quit();
}
#endif
int main(int argc, char *argv[]) {
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QApplication app(argc, argv);
app.setOrganizationName("reference_frontend");
app.setApplicationName("reference_frontend");
app.setApplicationVersion("1.0");
QSettings::setDefaultFormat(QSettings::IniFormat);
QCommandLineParser parser;
parser.setApplicationDescription("reference_frontend");
parser.addHelpOption();
parser.addVersionOption();
QCommandLineOption muteOption(QStringList() << "m" << "mute", QCoreApplication::translate("main", "Start with audio muted"));
QCommandLineOption fsOption(QStringList() << "f" << "fullscreen", QCoreApplication::translate("main", "Start in fullscreen mode"));
parser.addOption(fsOption);
parser.addOption(muteOption);
parser.addPositionalArgument("core", QCoreApplication::translate("main", "Core library to use"));
parser.addPositionalArgument("rom", QCoreApplication::translate("main", "ROM file to use"));
parser.process(app);
QSurfaceFormat f;
//f.setMajorVersion(4);
//f.setMinorVersion(3);
//f.setProfile(QSurfaceFormat::CoreProfile);
//f.setOption(QSurfaceFormat::DebugContext);
//f.setSwapInterval(0);
QSurfaceFormat::setDefaultFormat(f);
MainWindow w;
w.setWindowTitle(app.applicationName());
if(parser.isSet(muteOption)) {
w.setCoreMuted(true);
}
const QStringList args = parser.positionalArguments();
if(args.length() < 2) {
std::cerr << "Please specify a core and a content file." << std::endl;
return 1;
}else{
w.setCorePath(args.at(0));
w.setROMPath(args.at(1));
}
w.resize(640, 480);
w.show();
if(parser.isSet(fsOption)) {
w.setFullScreen(true);
}
#ifdef Q_OS_UNIX
struct sigaction sigact;
sigact.sa_handler = signal_handler;
sigemptyset(&sigact.sa_mask);
sigact.sa_flags = 0;
sigaction(SIGINT, &sigact, (struct sigaction*)NULL);
sigaction(SIGTERM, &sigact, (struct sigaction*)NULL);
#endif
return app.exec();
}

217
frontend/mainwindow.cpp

@ -0,0 +1,217 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <iostream>
#include <QApplication>
#include <QMessageBox>
#include <QTimer>
#include <QCloseEvent>
#ifndef NO_SOUND
// only needed for Core::audioReady signal connection
#include <QAudioFormat>
#endif
#include "mainwindow.h"
#include "../core/core.h"
#include "../core/coreoptionsdialog.h"
static MainWindow *s_mainwindow = nullptr;
MainWindow::MainWindow(QWidget *parent) :
QOpenGLWidget(parent)
,m_core(nullptr)
#ifndef NO_SOUND
,m_audioOutput(nullptr)
#endif
,m_audioReady(false)
,m_audioStream(nullptr)
,m_corePath()
,m_romPath()
,m_coreOptionsDialog()
,m_settings(qApp->applicationName() + ".ini", QSettings::IniFormat, this)
{
s_mainwindow = this;
m_core = Core::instance();
setCursor(QCursor(Qt::BlankCursor));
setMouseTracking(true);
setAttribute(Qt::WA_OpaquePaintEvent);
}
MainWindow::~MainWindow() {
//std::cout << "mainwindow dtor" << std::endl;
}
MainWindow* MainWindow::instance() {
return s_mainwindow;
}
void MainWindow::openCoreOptionsDialog() {
if(m_coreOptionsDialog)
delete m_coreOptionsDialog;
m_coreOptionsDialog = new CoreOptionsDialog(nullptr, m_core);
// layout is cleared after a single shot timer in the constructor, so we cannot build a new layout until after that
QTimer::singleShot(0, this, [this]() {
m_coreOptionsDialog->show();
m_coreOptionsDialog->reload();
});
}
void MainWindow::setCorePath(QString path) {
m_corePath = path;
}
void MainWindow::setROMPath(QString path) {
m_romPath = path;
}
void MainWindow::loadCore() {
std::cout << "Loading core." << std::endl;
connect(m_core, &Core::repaint, this, QOverload<>::of(&MainWindow::repaint));
connect(this, &MainWindow::frameSwapped, m_core, &Core::onGotFrameSwap);
if(m_coreOptionsDialog)
delete m_coreOptionsDialog;
#ifndef NO_SOUND
connect(m_core, &Core::audioReady, this, &MainWindow::startAudio);
#endif
connect(m_core, &Core::gotAudioSample, this, &MainWindow::onAudioSample);
connect(m_core, &Core::gotAudioSampleBatch, this, &MainWindow::onAudioSampleBatch);
connect(m_core, &Core::coreOptionsChanged, this, &MainWindow::onCoreOptionsChanged);
connect(this, &MainWindow::audioStreamReady, m_core, &Core::onAudioStreamReady);
m_core->load(m_corePath);
if(!m_core->isLoaded()) {
std::cout << "could not load core" << std::endl;
QMessageBox::critical(this, "Error", "Could not load core.");
QTimer::singleShot(0, qApp, &QApplication::quit);
return;
}
if(!m_core->isResolved()) {
std::cerr << "could not resolve required functions" << std::endl;
QMessageBox::critical(this, "Error", "Error loading core properly, the file may be corrupt.");
QTimer::singleShot(0, qApp, &QApplication::quit);
return;
}
}
void MainWindow::loadContent() {
if(m_romPath.isEmpty() && !m_core->supportsNoGame()) {
std::cerr << "No ROM specified." << std::endl;
QTimer::singleShot(0, qApp, &QApplication::quit);
return;
}
if(!m_core->run(m_romPath)) {
QTimer::singleShot(0, qApp, &QApplication::quit);
return;
}
}
void MainWindow::closeEvent(QCloseEvent *e) {
if(m_coreOptionsDialog && m_coreOptionsDialog->isVisible())
m_coreOptionsDialog->close();
// always try to save your SRAM and core options before even trying to stop the core, there is a chance it may crash
if(m_core) {
m_core->saveSRAM();
saveCoreOptions();
}
if(m_core && m_core->isLoaded())
m_core->unload();
QOpenGLWidget::closeEvent(e);
}
void MainWindow::saveCoreOptions() {
const QMap<QString, Core::CoreOption> &options = m_core->getCoreOptions();
const QStringList keys = options.keys();
m_settings.beginGroup("core_options");
for(int i = 0; i < options.size(); ++i) {
const Core::CoreOption &op = options.value(keys.at(i));
m_settings.setValue(op.key, op.val);
}
m_settings.endGroup();
m_settings.sync();
//std::cout << "saved settings to " << qUtf8Printable(m_settings.fileName()) << std::endl;
}
void MainWindow::saveState() {
if(m_core->saveState()) {
std::cout << "State saved." << std::endl;
}else{
std::cout << "State save failed." << std::endl;
}
}
void MainWindow::loadState() {
if(m_core->loadState()) {
std::cout << "State loaded." << std::endl;
}else{
std::cout << "State load failed." << std::endl;
}
}
void MainWindow::setCoreMuted(bool on) {
m_core->setMuted(on);
}
void MainWindow::onCoreOptionsChanged() {
std::cout << "core options changed, loading current values from settings." << std::endl;
QMap<QString, Core::CoreOption> &options = m_core->getCoreOptions();
m_settings.beginGroup("core_options");
QStringList keys = options.keys();
for(int i = 0; i < keys.size(); ++i) {
const QString &key = keys.at(i);
QString val = m_settings.value(key).toString();
if(val.isEmpty())
continue;
Core::CoreOption &op = options[key];
op.val = val;
op.valArray = val.toUtf8();
op.valData = op.valArray.constData();
//std::cout << "found key " << qUtf8Printable(key) << ", setting to " << qUtf8Printable(val) << std::endl;
}
m_settings.endGroup();
m_core->setVariablesChanged(true);
std::cout << "loaded core options from disk." << std::endl;
}
void MainWindow::go() {
loadCore();
loadContent();
}

94
frontend/mainwindow.h

@ -0,0 +1,94 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef __MAINWINDOW_H_
#define __MAINWINDOW_H_
#include <QOpenGLWidget>
#include <QQueue>
#include <QOpenGLFunctions>
#include <QSettings>
#ifndef NO_SOUND
#include <QAudio>
class QAudioOutput;
class QAudioFormat;
#endif
class Core;
class QPaintEvent;
class QCloseEvent;
class QKeyEvent;
class QMouseEvent;
class QOpenGLFramebufferObject;
class CoreOptionsDialog;
// you will have no vsync when just using a QMainWindow instead of a QOpenGLWidget
class MainWindow : public QOpenGLWidget, protected QOpenGLFunctions {
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
void setCorePath(QString path);
void setCoreMuted(bool on);
void setROMPath(QString path);
void setFullScreen(bool on);
static MainWindow* instance();
void openCoreOptionsDialog();
signals:
void audioStreamReady();
public slots:
void loadCore();
void loadContent();
void onAudioSample(int16_t left, int16_t right);
void onAudioSampleBatch(const int16_t *data, size_t frames);
void go();
void saveState();
void loadState();
void saveCoreOptions();
private slots:
#ifndef NO_SOUND
void startAudio(QAudioFormat *format);
void audioStateChanged(QAudio::State state);
#endif
void onCoreOptionsChanged();
private:
Core *m_core;
#ifndef NO_SOUND
QAudioOutput *m_audioOutput;
#endif
bool m_audioReady;
QIODevice *m_audioStream;
QString m_corePath;
QString m_romPath;
CoreOptionsDialog *m_coreOptionsDialog;
QSettings m_settings;
protected:
void paintEvent(QPaintEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
void keyReleaseEvent(QKeyEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void closeEvent(QCloseEvent *e) override;
// the base GL function implementations actually do nothing
void initializeGL() override;
void paintGL() override;
void resizeGL(int w, int h) override;
};
#endif // __MAINWINDOW_H_

121
frontend/video.cpp

@ -0,0 +1,121 @@
/*
Copyright 2020-2021 Brad Parker
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <QPainter>
#include <QPaintEvent>
#include <QOpenGLFramebufferObject>
#include <iostream>
#include <cmath>
#include "../common/video.h"
#include "../core/core.h"
#include "mainwindow.h"
void MainWindow::paintEvent(QPaintEvent *e) {
//std::cout << "paintEvent" << std::endl;
QOpenGLWidget::paintEvent(e);
if(!m_core->isRunning())
return;
QPainter p(this);
QRect r;
QImage *img = m_core->getImage();
auto videoState = VideoState::instance();
QOpenGLFramebufferObject *fbo = videoState->fbo();
struct retro_system_av_info *avInfo = videoState->avInfo();
if(m_core->isHWRender()) {
if(fbo && fbo->isValid()) {
r = QRect(rect().topLeft(), QSize(avInfo->geometry.base_width, avInfo->geometry.base_height));
}
}else{
p.fillRect(QRect(QPoint(0, 0), size()), Qt::black);
if(!img->isNull()) {
r = QRect(rect().topLeft(), img->size());
}
}
// integer scaling
float device_aspect = width() / (float)height();
float desired_aspect = videoState->aspect();
if(fabsf(device_aspect - desired_aspect) < 0.0001f) {
r.setWidth(width());
r.setHeight(height());
}else{
r.setWidth(videoState->baseWidth() * (height() / videoState->baseHeight()));
r.setHeight(videoState->baseHeight() * (height() / videoState->baseHeight()));
}
r.moveCenter(rect().center());
if(r.width() == 0)
r.setWidth(avInfo->geometry.base_width);
if(r.height() == 0)
r.setHeight(avInfo->geometry.base_height);
videoState->setAspectCorrectedRect(r);
if(!m_core->isHWRender()) {
if(!img->isNull()) {
p.drawImage(videoState->aspectCorrectedRect(), *img);
}
}
}
void MainWindow::initializeGL() {
std::cout << "initializeGL" << std::endl;
initializeOpenGLFunctions();
VideoState::instance()->setContext(context());
const unsigned char *renderer = glGetString(GL_RENDERER);
const unsigned char *version = glGetString(GL_VERSION);
std::cout << "OpenGL Renderer: " << renderer << " " << version << std::endl;
}
void MainWindow::paintGL() {
//std::cout << "paintGL" << std::endl;
static bool inited = false;
if(!inited) {
std::cout << "init" << std::endl;
inited = true;
go();
}
if(m_core->isHWRender()) {
m_core->blitFBO();
}
}
void MainWindow::resizeGL(int width, int height) {
std::cout << "resized window to " << width << "x" << height << std::endl;
}
void MainWindow::setFullScreen(bool on) {
if(on) {
showFullScreen();
}else{
showNormal();
}
}

37
reference_frontend.pro

@ -0,0 +1,37 @@
#
# Copyright 2020-2021 Brad Parker
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
TEMPLATE = app
QT += gui widgets
TARGET = reference_frontend
CONFIG += c++11 object_parallel_to_source
unix:QMAKE_CXXFLAGS += -Wno-unused
nosound {
DEFINES += NO_SOUND
}
!nosound {
QT += multimedia
}
include(common/common.pri)
include(frontend/frontend.pri)
include(core/core.pri)
Loading…
Cancel
Save