diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2f3863324..f5503a0ae 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -7,6 +7,7 @@ set(USE_GDB_STUB ON CACHE BOOL "Whether or not to enable the GDB stub ARM debugg
set(USE_FFMPEG ON CACHE BOOL "Whether or not to enable FFmpeg support")
set(USE_PNG ON CACHE BOOL "Whether or not to enable PNG support")
set(USE_LIBZIP ON CACHE BOOL "Whether or not to enable ZIP support")
+set(BUILD_QT ON CACHE BOOL "Build Qt frontend")
set(BUILD_SDL ON CACHE BOOL "Build SDL frontend")
set(BUILD_PERF OFF CACHE BOOL "Build performance profiling tool")
file(GLOB ARM_SRC ${CMAKE_SOURCE_DIR}/src/arm/*.c)
@@ -160,9 +161,14 @@ install(TARGETS ${BINARY_NAME} DESTINATION lib)
set_target_properties(${BINARY_NAME} PROPERTIES VERSION ${LIB_VERSION_STRING} SOVERSION ${LIB_VERSION_ABI})
if(BUILD_SDL)
+ add_definitions(-DBUILD_SDL)
add_subdirectory(${CMAKE_SOURCE_DIR}/src/platform/sdl ${CMAKE_BINARY_DIR}/sdl)
endif()
+if(BUILD_QT)
+ add_subdirectory(${CMAKE_SOURCE_DIR}/src/platform/qt ${CMAKE_BINARY_DIR}/qt)
+endif()
+
if(BUILD_PERF)
set(PERF_SRC ${CMAKE_SOURCE_DIR}/src/platform/perf-main.c)
if(UNIX AND NOT APPLE)
@@ -183,5 +189,6 @@ message(STATUS " Video recording: ${USE_FFMPEG}")
message(STATUS " Screenshot/advanced savestate support: ${USE_PNG}")
message(STATUS " ZIP support: ${USE_LIBZIP}")
message(STATUS "Frontend summary:")
+message(STATUS " Qt: ${BUILD_QT}")
message(STATUS " SDL (${SDL_VERSION}): ${BUILD_SDL}")
message(STATUS " Profiling: ${BUILD_PERF}")
diff --git a/res/info.plist.in b/res/info.plist.in
new file mode 100644
index 000000000..e7595b3f8
--- /dev/null
+++ b/res/info.plist.in
@@ -0,0 +1,45 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ English
+ CFBundleExecutable
+ ${MACOSX_BUNDLE_EXECUTABLE_NAME}
+ CFBundleGetInfoString
+ ${MACOSX_BUNDLE_INFO_STRING}
+ CFBundleIconFile
+ ${MACOSX_BUNDLE_ICON_FILE}
+ CFBundleIdentifier
+ ${MACOSX_BUNDLE_GUI_IDENTIFIER}
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ ${MACOSX_BUNDLE_BUNDLE_NAME}
+ CFBundlePackageType
+ APPL
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ ${MACOSX_BUNDLE_BUNDLE_VERSION}
+ CSResourcesFileMapped
+
+ NSHighResolutionCapable
+
+ NSHumanReadableCopyright
+ ${MACOSX_BUNDLE_COPYRIGHT}
+ CFBundleDocumentTypes
+
+
+ CFBundleTypeExtensions
+
+ gba
+
+ CFBundleTypeName
+ Game Boy Advance ROM Image
+ CFBundleTypeRole
+ Viewer
+
+
+
+
diff --git a/res/mgba-1024.png b/res/mgba-1024.png
new file mode 100644
index 000000000..4ef829578
Binary files /dev/null and b/res/mgba-1024.png differ
diff --git a/res/mgba.icns b/res/mgba.icns
new file mode 100644
index 000000000..d38852b1e
Binary files /dev/null and b/res/mgba.icns differ
diff --git a/res/mgba.ico b/res/mgba.ico
new file mode 100644
index 000000000..c2a2b21c7
Binary files /dev/null and b/res/mgba.ico differ
diff --git a/res/mgba.rc b/res/mgba.rc
new file mode 100644
index 000000000..d6a546ea4
--- /dev/null
+++ b/res/mgba.rc
@@ -0,0 +1 @@
+IDI_ICON1 ICON DISCARDABLE "mgba.ico"
diff --git a/src/platform/qt/AudioDevice.cpp b/src/platform/qt/AudioDevice.cpp
new file mode 100644
index 000000000..c672ec203
--- /dev/null
+++ b/src/platform/qt/AudioDevice.cpp
@@ -0,0 +1,46 @@
+#include "AudioDevice.h"
+
+extern "C" {
+#include "gba.h"
+#include "gba-audio.h"
+#include "gba-thread.h"
+}
+
+using namespace QGBA;
+
+AudioDevice::AudioDevice(QObject* parent)
+ : QIODevice(parent)
+ , m_context(nullptr)
+ , m_drift(0)
+{
+ setOpenMode(ReadOnly);
+}
+
+void AudioDevice::setFormat(const QAudioFormat& format) {
+ if (!GBAThreadHasStarted(m_context)) {
+ return;
+ }
+ GBAThreadInterrupt(m_context);
+ m_ratio = GBAAudioCalculateRatio(&m_context->gba->audio, m_context->fpsTarget, format.sampleRate());
+ GBAThreadContinue(m_context);
+}
+
+void AudioDevice::setInput(GBAThread* input) {
+ m_context = input;
+}
+
+qint64 AudioDevice::readData(char* data, qint64 maxSize) {
+ if (maxSize > 0xFFFFFFFF) {
+ maxSize = 0xFFFFFFFF;
+ }
+
+ if (!m_context->gba) {
+ return 0;
+ }
+
+ return GBAAudioResampleNN(&m_context->gba->audio, m_ratio, &m_drift, reinterpret_cast(data), maxSize / sizeof(GBAStereoSample)) * sizeof(GBAStereoSample);
+}
+
+qint64 AudioDevice::writeData(const char*, qint64) {
+ return 0;
+}
diff --git a/src/platform/qt/AudioDevice.h b/src/platform/qt/AudioDevice.h
new file mode 100644
index 000000000..843ca1d99
--- /dev/null
+++ b/src/platform/qt/AudioDevice.h
@@ -0,0 +1,31 @@
+#ifndef QGBA_AUDIO_DEVICE
+#define QGBA_AUDIO_DEVICE
+#include
+#include
+
+struct GBAThread;
+
+namespace QGBA {
+
+class AudioDevice : public QIODevice {
+Q_OBJECT
+
+public:
+ AudioDevice(QObject* parent = nullptr);
+
+ void setInput(GBAThread* input);
+ void setFormat(const QAudioFormat& format);
+
+protected:
+ virtual qint64 readData(char* data, qint64 maxSize) override;
+ virtual qint64 writeData(const char* data, qint64 maxSize) override;
+
+private:
+ GBAThread* m_context;
+ float m_drift;
+ float m_ratio;
+};
+
+}
+
+#endif
diff --git a/src/platform/qt/AudioProcessor.cpp b/src/platform/qt/AudioProcessor.cpp
new file mode 100644
index 000000000..9c522134d
--- /dev/null
+++ b/src/platform/qt/AudioProcessor.cpp
@@ -0,0 +1,67 @@
+#include "AudioProcessor.h"
+
+#include "AudioDevice.h"
+
+#include
+
+extern "C" {
+#include "gba-thread.h"
+}
+
+using namespace QGBA;
+
+AudioProcessor::AudioProcessor(QObject* parent)
+ : QObject(parent)
+ , m_audioOutput(nullptr)
+ , m_device(nullptr)
+{
+}
+
+void AudioProcessor::setInput(GBAThread* input) {
+ m_context = input;
+ if (m_device) {
+ m_device->setInput(input);
+ if (m_audioOutput) {
+ m_device->setFormat(m_audioOutput->format());
+ }
+ }
+}
+
+void AudioProcessor::start() {
+ if (!m_device) {
+ m_device = new AudioDevice(this);
+ }
+
+ if (!m_audioOutput) {
+ QAudioFormat format;
+ format.setSampleRate(44100);
+ format.setChannelCount(2);
+ format.setSampleSize(16);
+ format.setCodec("audio/pcm");
+ format.setByteOrder(QAudioFormat::LittleEndian);
+ format.setSampleType(QAudioFormat::SignedInt);
+
+ m_audioOutput = new QAudioOutput(format, this);
+ }
+
+ m_device->setInput(m_context);
+ m_device->setFormat(m_audioOutput->format());
+ m_audioOutput->setBufferSize(m_context->audioBuffers * 4);
+
+ m_audioOutput->start(m_device);
+}
+
+void AudioProcessor::pause() {
+ if (m_audioOutput) {
+ m_audioOutput->stop();
+ }
+}
+
+void AudioProcessor::setBufferSamples(int samples) {
+ QAudioFormat format = m_audioOutput->format();
+ m_audioOutput->setBufferSize(samples * format.channelCount() * format.sampleSize() / 8);
+}
+
+void AudioProcessor::inputParametersChanged() {
+ m_device->setFormat(m_audioOutput->format());
+}
diff --git a/src/platform/qt/AudioProcessor.h b/src/platform/qt/AudioProcessor.h
new file mode 100644
index 000000000..f458bf349
--- /dev/null
+++ b/src/platform/qt/AudioProcessor.h
@@ -0,0 +1,36 @@
+#ifndef QGBA_AUDIO_PROCESSOR
+#define QGBA_AUDIO_PROCESSOR
+#include
+
+struct GBAThread;
+
+class QAudioOutput;
+
+namespace QGBA {
+
+class AudioDevice;
+
+class AudioProcessor : public QObject {
+Q_OBJECT
+
+public:
+ AudioProcessor(QObject* parent = nullptr);
+
+ void setInput(GBAThread* input);
+
+public slots:
+ void start();
+ void pause();
+
+ void setBufferSamples(int samples);
+ void inputParametersChanged();
+
+private:
+ GBAThread* m_context;
+ QAudioOutput* m_audioOutput;
+ AudioDevice* m_device;
+};
+
+}
+
+#endif
diff --git a/src/platform/qt/CMakeLists.txt b/src/platform/qt/CMakeLists.txt
new file mode 100644
index 000000000..39b28fad0
--- /dev/null
+++ b/src/platform/qt/CMakeLists.txt
@@ -0,0 +1,66 @@
+cmake_minimum_required(VERSION 2.8.8)
+enable_language(CXX)
+
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --std=c++11")
+
+if(BUILD_SDL)
+ if(NOT SDL_FOUND AND NOT SDL2_FOUND)
+ find_package(SDL 1.2 REQUIRED)
+ endif()
+ if(SDL2_FOUND)
+ link_directories(${SDL2_LIBDIR})
+ endif()
+ set(PLATFORM_LIBRARY "${PLATFORM_LIBRARY};${SDL_LIBRARY};${SDLMAIN_LIBRARY}")
+ set(PLATFORM_SRC ${PLATFORM_SRC} ${CMAKE_SOURCE_DIR}/src/platform/sdl/sdl-events.c)
+ include_directories(${SDL_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/src/platform/sdl)
+endif()
+
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+
+find_package(Qt5Multimedia)
+find_package(Qt5OpenGL)
+find_package(Qt5Widgets)
+find_package(OpenGL)
+
+if(NOT Qt5Multimedia_FOUND OR NOT Qt5OpenGL_FOUND OR NOT Qt5Widgets_FOUND OR NOT OPENGL_FOUND)
+ set(BUILD_QT OFF PARENT_SCOPE)
+ return()
+endif()
+
+set(SOURCE_FILES
+ AudioDevice.cpp
+ AudioProcessor.cpp
+ Display.cpp
+ GBAApp.cpp
+ GameController.cpp
+ LoadSaveState.cpp
+ LogView.cpp
+ SavestateButton.cpp
+ Window.cpp
+ VFileDevice.cpp
+ VideoView.cpp)
+
+qt5_wrap_ui(UI_FILES
+ LoadSaveState.ui
+ LogView.ui
+ VideoView.ui)
+
+if(USE_GDB_STUB)
+ set(SOURCE_FILES ${PLATFORM_SRC} ${SOURCE_FILES} GDBController.cpp GDBWindow.cpp)
+endif()
+set(MACOSX_BUNDLE_ICON_FILE mgba.icns)
+set(MACOSX_BUNDLE_BUNDLE_VERSION ${LIB_VERSION_STRING})
+set(MACOSX_BUNDLE_BUNDLE_NAME ${PROJECT_NAME})
+set(MACOSX_BUNDLE_GUI_IDENTIFIER com.endrift.${BINARY_NAME}-qt)
+set_source_files_properties(${CMAKE_SOURCE_DIR}/res/mgba.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
+
+qt5_add_resources(RESOURCES resources.qrc)
+if(WIN32)
+ list(APPEND RESOURCES ${CMAKE_SOURCE_DIR}/res/mgba.rc)
+endif()
+add_executable(mGBA WIN32 MACOSX_BUNDLE main.cpp ${CMAKE_SOURCE_DIR}/res/mgba.icns ${SOURCE_FILES} ${UI_FILES} ${RESOURCES})
+set_target_properties(mGBA PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/res/info.plist.in)
+
+qt5_use_modules(mGBA Widgets Multimedia OpenGL)
+target_link_libraries(mGBA ${PLATFORM_LIBRARY} ${OPENGL_LIBRARY} ${BINARY_NAME} Qt5::Widgets)
diff --git a/src/platform/qt/Display.cpp b/src/platform/qt/Display.cpp
new file mode 100644
index 000000000..aa2cbac38
--- /dev/null
+++ b/src/platform/qt/Display.cpp
@@ -0,0 +1,176 @@
+#include "Display.h"
+
+#include
+#include
+
+extern "C" {
+#include "gba-thread.h"
+}
+
+using namespace QGBA;
+
+static const GLint _glVertices[] = {
+ 0, 0,
+ 256, 0,
+ 256, 256,
+ 0, 256
+};
+
+static const GLint _glTexCoords[] = {
+ 0, 0,
+ 1, 0,
+ 1, 1,
+ 0, 1
+};
+
+Display::Display(QGLFormat format, QWidget* parent)
+ : QGLWidget(format, parent)
+ , m_painter(nullptr)
+ , m_drawThread(nullptr)
+{
+ setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
+ setMinimumSize(VIDEO_HORIZONTAL_PIXELS, VIDEO_VERTICAL_PIXELS);
+ setAutoBufferSwap(false);
+ setCursor(Qt::BlankCursor);
+}
+
+void Display::startDrawing(const uint32_t* buffer, GBAThread* thread) {
+ if (m_drawThread) {
+ return;
+ }
+ m_drawThread = new QThread(this);
+ m_painter = new Painter(this);
+ m_painter->setContext(thread);
+ m_painter->setBacking(buffer);
+ m_painter->moveToThread(m_drawThread);
+ m_context = thread;
+ doneCurrent();
+ context()->moveToThread(m_drawThread);
+ connect(m_drawThread, SIGNAL(started()), m_painter, SLOT(start()));
+ m_drawThread->start(QThread::TimeCriticalPriority);
+}
+
+void Display::stopDrawing() {
+ if (m_drawThread) {
+ QMetaObject::invokeMethod(m_painter, "stop", Qt::BlockingQueuedConnection);
+ m_drawThread->exit();
+ m_drawThread = nullptr;
+ }
+}
+
+void Display::forceDraw() {
+ if (m_drawThread) {
+ QMetaObject::invokeMethod(m_painter, "forceDraw", Qt::QueuedConnection);
+ }
+}
+
+#ifdef USE_PNG
+void Display::screenshot() {
+ GBAThreadInterrupt(m_context);
+ GBAThreadTakeScreenshot(m_context);
+ GBAThreadContinue(m_context);
+}
+#endif
+
+void Display::initializeGL() {
+ glClearColor(0, 0, 0, 0);
+ glClear(GL_COLOR_BUFFER_BIT);
+ swapBuffers();
+}
+
+void Display::resizeEvent(QResizeEvent* event) {
+ if (m_drawThread) {
+ GBAThreadInterrupt(m_context);
+ GBASyncSuspendDrawing(&m_context->sync);
+ QMetaObject::invokeMethod(m_painter, "resize", Qt::BlockingQueuedConnection, Q_ARG(QSize, event->size()));
+ GBASyncResumeDrawing(&m_context->sync);
+ GBAThreadContinue(m_context);
+ }
+}
+
+Painter::Painter(Display* parent)
+ : m_gl(parent)
+{
+ m_size = parent->size();
+}
+
+void Painter::setContext(GBAThread* context) {
+ m_context = context;
+}
+
+void Painter::setBacking(const uint32_t* backing) {
+ m_backing = backing;
+}
+
+void Painter::resize(const QSize& size) {
+ m_size = size;
+ m_gl->makeCurrent();
+ glViewport(0, 0, m_size.width() * m_gl->devicePixelRatio(), m_size.height() * m_gl->devicePixelRatio());
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+ m_gl->swapBuffers();
+ m_gl->doneCurrent();
+}
+
+void Painter::start() {
+ m_gl->makeCurrent();
+ glEnable(GL_TEXTURE_2D);
+ glGenTextures(1, &m_tex);
+ glBindTexture(GL_TEXTURE_2D, m_tex);
+ glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
+ glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glEnableClientState(GL_TEXTURE_COORD_ARRAY);
+ glEnableClientState(GL_VERTEX_ARRAY);
+ glVertexPointer(2, GL_INT, 0, _glVertices);
+ glTexCoordPointer(2, GL_INT, 0, _glTexCoords);
+ glMatrixMode(GL_PROJECTION);
+ glLoadIdentity();
+ glOrtho(0, 240, 160, 0, 0, 1);
+ glMatrixMode(GL_MODELVIEW);
+ glLoadIdentity();
+ m_gl->doneCurrent();
+
+ m_drawTimer = new QTimer;
+ m_drawTimer->moveToThread(QThread::currentThread());
+ m_drawTimer->setInterval(0);
+ connect(m_drawTimer, SIGNAL(timeout()), this, SLOT(draw()));
+ m_drawTimer->start();
+}
+
+void Painter::draw() {
+ m_gl->makeCurrent();
+ if (GBASyncWaitFrameStart(&m_context->sync, m_context->frameskip)) {
+ glViewport(0, 0, m_size.width() * m_gl->devicePixelRatio(), m_size.height() * m_gl->devicePixelRatio());
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 256, 256, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_backing);
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+ if (m_context->sync.videoFrameWait) {
+ glFlush();
+ }
+ }
+ GBASyncWaitFrameEnd(&m_context->sync);
+ m_gl->swapBuffers();
+ m_gl->doneCurrent();
+}
+
+void Painter::forceDraw() {
+ m_gl->makeCurrent();
+ glViewport(0, 0, m_size.width() * m_gl->devicePixelRatio(), m_size.height() * m_gl->devicePixelRatio());
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 256, 256, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_backing);
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+ if (m_context->sync.videoFrameWait) {
+ glFlush();
+ }
+ m_gl->swapBuffers();
+ m_gl->doneCurrent();
+}
+
+void Painter::stop() {
+ m_drawTimer->stop();
+ delete m_drawTimer;
+ m_gl->makeCurrent();
+ glDeleteTextures(1, &m_tex);
+ glClear(GL_COLOR_BUFFER_BIT);
+ m_gl->swapBuffers();
+ m_gl->doneCurrent();
+ m_gl->context()->moveToThread(QApplication::instance()->thread());
+}
diff --git a/src/platform/qt/Display.h b/src/platform/qt/Display.h
new file mode 100644
index 000000000..7e38c08fc
--- /dev/null
+++ b/src/platform/qt/Display.h
@@ -0,0 +1,65 @@
+#ifndef QGBA_DISPLAY
+#define QGBA_DISPLAY
+
+#include
+#include
+#include
+
+struct GBAThread;
+
+namespace QGBA {
+
+class Painter;
+class Display : public QGLWidget {
+Q_OBJECT
+
+public:
+ Display(QGLFormat format, QWidget* parent = nullptr);
+
+public slots:
+ void startDrawing(const uint32_t* buffer, GBAThread* context);
+ void stopDrawing();
+ void forceDraw();
+#ifdef USE_PNG
+ void screenshot();
+#endif
+
+protected:
+ virtual void initializeGL() override;
+ virtual void paintEvent(QPaintEvent*) override {};
+ virtual void resizeEvent(QResizeEvent*) override;
+
+private:
+ Painter* m_painter;
+ QThread* m_drawThread;
+ GBAThread* m_context;
+};
+
+class Painter : public QObject {
+Q_OBJECT
+
+public:
+ Painter(Display* parent);
+
+ void setContext(GBAThread*);
+ void setBacking(const uint32_t*);
+
+public slots:
+ void forceDraw();
+ void draw();
+ void start();
+ void stop();
+ void resize(const QSize& size);
+
+private:
+ QTimer* m_drawTimer;
+ GBAThread* m_context;
+ const uint32_t* m_backing;
+ GLuint m_tex;
+ QGLWidget* m_gl;
+ QSize m_size;
+};
+
+}
+
+#endif
diff --git a/src/platform/qt/GBAApp.cpp b/src/platform/qt/GBAApp.cpp
new file mode 100644
index 000000000..daf58e59a
--- /dev/null
+++ b/src/platform/qt/GBAApp.cpp
@@ -0,0 +1,32 @@
+#include "GBAApp.h"
+
+#include "GameController.h"
+
+#include
+
+using namespace QGBA;
+
+GBAApp::GBAApp(int& argc, char* argv[])
+ : QApplication(argc, argv)
+{
+ QApplication::setApplicationName(PROJECT_NAME);
+ QApplication::setApplicationVersion(PROJECT_VERSION);
+
+ if (parseCommandArgs(&m_opts, argc, argv, 0)) {
+ m_window.optionsPassed(&m_opts);
+ }
+
+ m_window.show();
+}
+
+GBAApp::~GBAApp() {
+ freeOptions(&m_opts);
+}
+
+bool GBAApp::event(QEvent* event) {
+ if (event->type() == QEvent::FileOpen) {
+ m_window.controller()->loadGame(static_cast(event)->file());
+ return true;
+ }
+ return QApplication::event(event);
+}
diff --git a/src/platform/qt/GBAApp.h b/src/platform/qt/GBAApp.h
new file mode 100644
index 000000000..a101b0ed5
--- /dev/null
+++ b/src/platform/qt/GBAApp.h
@@ -0,0 +1,34 @@
+#ifndef QGBA_APP_H
+#define QGBA_APP_H
+
+#include
+
+#include "Window.h"
+
+extern "C" {
+#include "platform/commandline.h"
+}
+
+namespace QGBA {
+
+class GameController;
+
+class GBAApp : public QApplication {
+Q_OBJECT
+
+public:
+ GBAApp(int& argc, char* argv[]);
+ virtual ~GBAApp();
+
+protected:
+ bool event(QEvent*);
+
+private:
+ Window m_window;
+
+ StartupOptions m_opts;
+};
+
+}
+
+#endif
diff --git a/src/platform/qt/GDBController.cpp b/src/platform/qt/GDBController.cpp
new file mode 100644
index 000000000..3ee878798
--- /dev/null
+++ b/src/platform/qt/GDBController.cpp
@@ -0,0 +1,71 @@
+#include "GDBController.h"
+
+#include "GameController.h"
+
+using namespace QGBA;
+
+GDBController::GDBController(GameController* controller, QObject* parent)
+ : QObject(parent)
+ , m_gameController(controller)
+ , m_port(2345)
+ , m_bindAddress(0)
+{
+ GDBStubCreate(&m_gdbStub);
+}
+
+ushort GDBController::port() {
+ return m_port;
+}
+
+uint32_t GDBController::bindAddress() {
+ return m_bindAddress;
+}
+
+bool GDBController::isAttached() {
+ return m_gameController->debugger() == &m_gdbStub.d;
+}
+
+void GDBController::setPort(ushort port) {
+ m_port = port;
+}
+
+void GDBController::setBindAddress(uint32_t bindAddress) {
+ m_bindAddress = bindAddress;
+}
+
+void GDBController::attach() {
+ if (isAttached()) {
+ return;
+ }
+ m_gameController->setDebugger(&m_gdbStub.d);
+}
+
+void GDBController::detach() {
+ if (!isAttached()) {
+ return;
+ }
+ bool wasPaused = m_gameController->isPaused();
+ disconnect(m_gameController, SIGNAL(frameAvailable(const uint32_t*)), this, SLOT(updateGDB()));
+ m_gameController->setPaused(true);
+ GDBStubShutdown(&m_gdbStub);
+ m_gameController->setDebugger(nullptr);
+ m_gameController->setPaused(wasPaused);
+}
+
+void GDBController::listen() {
+ if (!isAttached()) {
+ attach();
+ }
+ bool wasPaused = m_gameController->isPaused();
+ connect(m_gameController, SIGNAL(frameAvailable(const uint32_t*)), this, SLOT(updateGDB()));
+ m_gameController->setPaused(true);
+ GDBStubListen(&m_gdbStub, m_port, m_bindAddress);
+ m_gameController->setPaused(wasPaused);
+}
+
+void GDBController::updateGDB() {
+ bool wasPaused = m_gameController->isPaused();
+ m_gameController->setPaused(true);
+ GDBStubUpdate(&m_gdbStub);
+ m_gameController->setPaused(wasPaused);
+}
diff --git a/src/platform/qt/GDBController.h b/src/platform/qt/GDBController.h
new file mode 100644
index 000000000..3de83dac4
--- /dev/null
+++ b/src/platform/qt/GDBController.h
@@ -0,0 +1,44 @@
+#ifndef QGBA_GDB_CONTROLLER
+#define QGBA_GDB_CONTROLLER
+
+#include
+
+extern "C" {
+#include "debugger/gdb-stub.h"
+}
+
+namespace QGBA {
+
+class GameController;
+
+class GDBController : public QObject {
+Q_OBJECT
+
+public:
+ GDBController(GameController* controller, QObject* parent = nullptr);
+
+public:
+ ushort port();
+ uint32_t bindAddress();
+ bool isAttached();
+
+public slots:
+ void setPort(ushort port);
+ void setBindAddress(uint32_t bindAddress);
+ void attach();
+ void detach();
+ void listen();
+
+private slots:
+ void updateGDB();
+
+private:
+ GDBStub m_gdbStub;
+ GameController* m_gameController;
+
+ ushort m_port;
+ uint32_t m_bindAddress;
+};
+
+}
+#endif
diff --git a/src/platform/qt/GDBWindow.cpp b/src/platform/qt/GDBWindow.cpp
new file mode 100644
index 000000000..8f051e203
--- /dev/null
+++ b/src/platform/qt/GDBWindow.cpp
@@ -0,0 +1,99 @@
+#include "GDBWindow.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "GDBController.h"
+
+using namespace QGBA;
+
+GDBWindow::GDBWindow(GDBController* controller, QWidget* parent)
+ : QWidget(parent)
+ , m_gdbController(controller)
+{
+ setWindowFlags(windowFlags() & ~Qt::WindowFullscreenButtonHint);
+ QVBoxLayout* mainSegment = new QVBoxLayout;
+ setLayout(mainSegment);
+ QGroupBox* settings = new QGroupBox(tr("Server settings"));
+ mainSegment->addWidget(settings);
+
+ QGridLayout* settingsGrid = new QGridLayout;
+ settings->setLayout(settingsGrid);
+
+ QLabel* portLabel = new QLabel(tr("Local port"));
+ settingsGrid->addWidget(portLabel, 0, 0, Qt::AlignRight);
+ QLabel* bindAddressLabel = new QLabel(tr("Bind address"));
+ settingsGrid->addWidget(bindAddressLabel, 1, 0, Qt::AlignRight);
+
+ m_portEdit = new QLineEdit("2345");
+ m_portEdit->setMaxLength(5);
+ connect(m_portEdit, SIGNAL(textChanged(const QString&)), this, SLOT(portChanged(const QString&)));
+ settingsGrid->addWidget(m_portEdit, 0, 1, Qt::AlignLeft);
+
+ m_bindAddressEdit = new QLineEdit("0.0.0.0");
+ m_bindAddressEdit->setMaxLength(15);
+ connect(m_bindAddressEdit, SIGNAL(textChanged(const QString&)), this, SLOT(bindAddressChanged(const QString&)));
+ settingsGrid->addWidget(m_bindAddressEdit, 1, 1, Qt::AlignLeft);
+
+ m_startStopButton = new QPushButton;
+ mainSegment->addWidget(m_startStopButton);
+ if (m_gdbController->isAttached()) {
+ started();
+ } else {
+ stopped();
+ }
+}
+
+void GDBWindow::portChanged(const QString& portString) {
+ bool ok = false;
+ ushort port = portString.toUShort(&ok);
+ if (ok) {
+ m_gdbController->setPort(port);
+ }
+}
+
+void GDBWindow::bindAddressChanged(const QString& bindAddressString) {
+ bool ok = false;
+ QStringList parts = bindAddressString.split('.');
+ if (parts.length() != 4) {
+ return;
+ }
+ int i;
+ uint32_t address = 0;
+ for (i = 0; i < 4; ++i) {
+ ushort octet = parts[i].toUShort(&ok);
+ if (!ok) {
+ return;
+ }
+ if (octet > 0xFF) {
+ return;
+ }
+ address <<= 8;
+ address += octet;
+ }
+ m_gdbController->setBindAddress(address);
+}
+
+void GDBWindow::started() {
+ m_portEdit->setEnabled(false);
+ m_bindAddressEdit->setEnabled(false);
+ m_startStopButton->setText(tr("Stop"));
+ disconnect(m_startStopButton, SIGNAL(clicked()), m_gdbController, SLOT(listen()));
+ disconnect(m_startStopButton, SIGNAL(clicked()), this, SLOT(started()));
+ connect(m_startStopButton, SIGNAL(clicked()), m_gdbController, SLOT(detach()));
+ connect(m_startStopButton, SIGNAL(clicked()), this, SLOT(stopped()));
+}
+
+void GDBWindow::stopped() {
+ m_portEdit->setEnabled(true);
+ m_bindAddressEdit->setEnabled(true);
+ m_startStopButton->setText(tr("Start"));
+ disconnect(m_startStopButton, SIGNAL(clicked()), m_gdbController, SLOT(detach()));
+ disconnect(m_startStopButton, SIGNAL(clicked()), this, SLOT(stopped()));
+ connect(m_startStopButton, SIGNAL(clicked()), m_gdbController, SLOT(listen()));
+ connect(m_startStopButton, SIGNAL(clicked()), this, SLOT(started()));
+}
diff --git a/src/platform/qt/GDBWindow.h b/src/platform/qt/GDBWindow.h
new file mode 100644
index 000000000..1e97c66ee
--- /dev/null
+++ b/src/platform/qt/GDBWindow.h
@@ -0,0 +1,36 @@
+#ifndef QGBA_GDB_WINDOW
+#define QGBA_GDB_WINDOW
+
+#include
+
+class QLineEdit;
+class QPushButton;
+
+namespace QGBA {
+
+class GDBController;
+
+class GDBWindow : public QWidget {
+Q_OBJECT
+
+public:
+ GDBWindow(GDBController* controller, QWidget* parent = nullptr);
+
+private slots:
+ void portChanged(const QString&);
+ void bindAddressChanged(const QString&);
+
+ void started();
+ void stopped();
+
+private:
+ GDBController* m_gdbController;
+
+ QLineEdit* m_portEdit;
+ QLineEdit* m_bindAddressEdit;
+ QPushButton* m_startStopButton;
+};
+
+}
+
+#endif
diff --git a/src/platform/qt/GameController.cpp b/src/platform/qt/GameController.cpp
new file mode 100644
index 000000000..08ae04c42
--- /dev/null
+++ b/src/platform/qt/GameController.cpp
@@ -0,0 +1,376 @@
+#include "GameController.h"
+
+#include "AudioProcessor.h"
+
+#include
+
+extern "C" {
+#include "gba.h"
+#include "gba-audio.h"
+#include "gba-serialize.h"
+#include "renderers/video-software.h"
+#include "util/vfs.h"
+}
+
+using namespace QGBA;
+
+GameController::GameController(QObject* parent)
+ : QObject(parent)
+ , m_drawContext(new uint32_t[256 * 256])
+ , m_threadContext()
+ , m_activeKeys(0)
+ , m_gameOpen(false)
+ , m_audioThread(new QThread(this))
+ , m_audioProcessor(new AudioProcessor)
+ , m_videoSync(VIDEO_SYNC)
+ , m_audioSync(AUDIO_SYNC)
+ , m_turbo(false)
+ , m_turboForced(false)
+{
+ m_renderer = new GBAVideoSoftwareRenderer;
+ GBAVideoSoftwareRendererCreate(m_renderer);
+ m_renderer->outputBuffer = (color_t*) m_drawContext;
+ m_renderer->outputBufferStride = 256;
+ m_threadContext.state = THREAD_INITIALIZED;
+ m_threadContext.debugger = 0;
+ m_threadContext.frameskip = 0;
+ m_threadContext.bios = 0;
+ m_threadContext.renderer = &m_renderer->d;
+ m_threadContext.userData = this;
+ m_threadContext.rewindBufferCapacity = 0;
+ m_threadContext.logLevel = -1;
+
+ GBAInputMapInit(&m_threadContext.inputMap);
+
+#ifdef BUILD_SDL
+ SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_NOPARACHUTE);
+ m_sdlEvents.bindings = &m_threadContext.inputMap;
+ GBASDLInitEvents(&m_sdlEvents);
+ SDL_JoystickEventState(SDL_QUERY);
+#endif
+
+ m_threadContext.startCallback = [] (GBAThread* context) {
+ GameController* controller = static_cast(context->userData);
+ controller->m_audioProcessor->setInput(context);
+ controller->gameStarted(context);
+ };
+
+ m_threadContext.cleanCallback = [] (GBAThread* context) {
+ GameController* controller = static_cast(context->userData);
+ controller->gameStopped(context);
+ };
+
+ m_threadContext.frameCallback = [] (GBAThread* context) {
+ GameController* controller = static_cast(context->userData);
+ controller->m_pauseMutex.lock();
+ if (controller->m_pauseAfterFrame) {
+ GBAThreadPauseFromThread(context);
+ controller->m_pauseAfterFrame = false;
+ controller->gamePaused(&controller->m_threadContext);
+ }
+ controller->m_pauseMutex.unlock();
+ controller->frameAvailable(controller->m_drawContext);
+ };
+
+ m_threadContext.logHandler = [] (GBAThread* context, enum GBALogLevel level, const char* format, va_list args) {
+ GameController* controller = static_cast(context->userData);
+ controller->postLog(level, QString().vsprintf(format, args));
+ };
+
+ m_audioThread->start(QThread::TimeCriticalPriority);
+ m_audioProcessor->moveToThread(m_audioThread);
+ connect(this, SIGNAL(gameStarted(GBAThread*)), m_audioProcessor, SLOT(start()));
+ connect(this, SIGNAL(gameStopped(GBAThread*)), m_audioProcessor, SLOT(pause()));
+ connect(this, SIGNAL(gamePaused(GBAThread*)), m_audioProcessor, SLOT(pause()));
+ connect(this, SIGNAL(gameUnpaused(GBAThread*)), m_audioProcessor, SLOT(start()));
+
+#ifdef BUILD_SDL
+ connect(this, SIGNAL(frameAvailable(const uint32_t*)), this, SLOT(testSDLEvents()));
+#endif
+}
+
+GameController::~GameController() {
+ m_audioThread->quit();
+ m_audioThread->wait();
+ disconnect();
+ closeGame();
+ delete m_renderer;
+}
+
+ARMDebugger* GameController::debugger() {
+ return m_threadContext.debugger;
+}
+
+void GameController::setDebugger(ARMDebugger* debugger) {
+ bool wasPaused = isPaused();
+ setPaused(true);
+ if (m_threadContext.debugger && GBAThreadHasStarted(&m_threadContext)) {
+ GBADetachDebugger(m_threadContext.gba);
+ }
+ m_threadContext.debugger = debugger;
+ if (m_threadContext.debugger && GBAThreadHasStarted(&m_threadContext)) {
+ GBAAttachDebugger(m_threadContext.gba, m_threadContext.debugger);
+ }
+ setPaused(wasPaused);
+}
+
+void GameController::loadGame(const QString& path, bool dirmode) {
+ closeGame();
+ if (!dirmode) {
+ QFile file(path);
+ if (!file.open(QIODevice::ReadOnly)) {
+ return;
+ }
+ file.close();
+ }
+
+ m_fname = path;
+ m_dirmode = dirmode;
+ openGame();
+}
+
+void GameController::openGame() {
+ m_gameOpen = true;
+
+ m_pauseAfterFrame = false;
+
+ if (m_turbo) {
+ m_threadContext.sync.videoFrameWait = false;
+ m_threadContext.sync.audioWait = false;
+ } else {
+ m_threadContext.sync.videoFrameWait = m_videoSync;
+ m_threadContext.sync.audioWait = m_audioSync;
+ }
+
+ m_threadContext.fname = strdup(m_fname.toLocal8Bit().constData());
+ if (m_dirmode) {
+ m_threadContext.gameDir = VDirOpen(m_threadContext.fname);
+ m_threadContext.stateDir = m_threadContext.gameDir;
+ } else {
+ m_threadContext.rom = VFileOpen(m_threadContext.fname, O_RDONLY);
+#if ENABLE_LIBZIP
+ m_threadContext.gameDir = VDirOpenZip(m_threadContext.fname, 0);
+#endif
+ }
+
+ if (!m_bios.isNull()) {
+ m_threadContext.bios = VFileOpen(m_bios.toLocal8Bit().constData(), O_RDONLY);
+ }
+
+ if (!m_patch.isNull()) {
+ m_threadContext.patch = VFileOpen(m_patch.toLocal8Bit().constData(), O_RDONLY);
+ }
+
+ if (!GBAThreadStart(&m_threadContext)) {
+ m_gameOpen = false;
+ }
+}
+
+void GameController::loadBIOS(const QString& path) {
+ m_bios = path;
+ if (m_gameOpen) {
+ closeGame();
+ openGame();
+ }
+}
+
+void GameController::loadPatch(const QString& path) {
+ m_patch = path;
+ if (m_gameOpen) {
+ closeGame();
+ openGame();
+ }
+}
+
+void GameController::closeGame() {
+ if (!m_gameOpen) {
+ return;
+ }
+ if (GBAThreadIsPaused(&m_threadContext)) {
+ GBAThreadUnpause(&m_threadContext);
+ }
+ GBAThreadEnd(&m_threadContext);
+ GBAThreadJoin(&m_threadContext);
+ if (m_threadContext.fname) {
+ free(const_cast(m_threadContext.fname));
+ m_threadContext.fname = nullptr;
+ }
+
+ m_patch = QString();
+
+ m_gameOpen = false;
+ emit gameStopped(&m_threadContext);
+}
+
+bool GameController::isPaused() {
+ return GBAThreadIsPaused(&m_threadContext);
+}
+
+void GameController::setPaused(bool paused) {
+ if (paused == GBAThreadIsPaused(&m_threadContext)) {
+ return;
+ }
+ if (paused) {
+ GBAThreadPause(&m_threadContext);
+ emit gamePaused(&m_threadContext);
+ } else {
+ GBAThreadUnpause(&m_threadContext);
+ emit gameUnpaused(&m_threadContext);
+ }
+}
+
+void GameController::reset() {
+ GBAThreadReset(&m_threadContext);
+}
+
+void GameController::frameAdvance() {
+ m_pauseMutex.lock();
+ m_pauseAfterFrame = true;
+ setPaused(false);
+ m_pauseMutex.unlock();
+}
+
+void GameController::keyPressed(int key) {
+ int mappedKey = 1 << key;
+ m_activeKeys |= mappedKey;
+ updateKeys();
+}
+
+void GameController::keyReleased(int key) {
+ int mappedKey = 1 << key;
+ m_activeKeys &= ~mappedKey;
+ updateKeys();
+}
+
+void GameController::setAudioBufferSamples(int samples) {
+ GBAThreadInterrupt(&m_threadContext);
+ m_threadContext.audioBuffers = samples;
+ GBAAudioResizeBuffer(&m_threadContext.gba->audio, samples);
+ GBAThreadContinue(&m_threadContext);
+ QMetaObject::invokeMethod(m_audioProcessor, "setBufferSamples", Q_ARG(int, samples));
+}
+
+void GameController::setFPSTarget(float fps) {
+ GBAThreadInterrupt(&m_threadContext);
+ m_threadContext.fpsTarget = fps;
+ GBAThreadContinue(&m_threadContext);
+ QMetaObject::invokeMethod(m_audioProcessor, "inputParametersChanged");
+}
+
+void GameController::loadState(int slot) {
+ GBAThreadInterrupt(&m_threadContext);
+ GBALoadState(m_threadContext.gba, m_threadContext.stateDir, slot);
+ GBAThreadContinue(&m_threadContext);
+ emit stateLoaded(&m_threadContext);
+ emit frameAvailable(m_drawContext);
+}
+
+void GameController::saveState(int slot) {
+ GBAThreadInterrupt(&m_threadContext);
+ GBASaveState(m_threadContext.gba, m_threadContext.stateDir, slot, true);
+ GBAThreadContinue(&m_threadContext);
+}
+
+void GameController::setVideoSync(bool set) {
+ m_videoSync = set;
+ if (!m_turbo && m_gameOpen) {
+ GBAThreadInterrupt(&m_threadContext);
+ m_threadContext.sync.videoFrameWait = set;
+ GBAThreadContinue(&m_threadContext);
+ }
+}
+
+void GameController::setAudioSync(bool set) {
+ m_audioSync = set;
+ if (!m_turbo && m_gameOpen) {
+ GBAThreadInterrupt(&m_threadContext);
+ m_threadContext.sync.audioWait = set;
+ GBAThreadContinue(&m_threadContext);
+ }
+}
+
+void GameController::setFrameskip(int skip) {
+ m_threadContext.frameskip = skip;
+}
+
+void GameController::setTurbo(bool set, bool forced) {
+ if (m_turboForced && !forced) {
+ return;
+ }
+ m_turbo = set;
+ if (set) {
+ m_turboForced = forced;
+ } else {
+ m_turboForced = false;
+ }
+ if (m_gameOpen) {
+ GBAThreadInterrupt(&m_threadContext);
+ m_threadContext.sync.audioWait = set ? false : m_audioSync;
+ m_threadContext.sync.videoFrameWait = set ? false : m_videoSync;
+ GBAThreadContinue(&m_threadContext);
+ }
+}
+
+void GameController::setAVStream(GBAAVStream* stream) {
+ if (m_gameOpen) {
+ GBAThreadInterrupt(&m_threadContext);
+ m_threadContext.stream = stream;
+ GBAThreadContinue(&m_threadContext);
+ } else {
+ m_threadContext.stream = stream;
+ }
+}
+
+void GameController::clearAVStream() {
+ if (m_gameOpen) {
+ GBAThreadInterrupt(&m_threadContext);
+ m_threadContext.stream = nullptr;
+ GBAThreadContinue(&m_threadContext);
+ } else {
+ m_threadContext.stream = nullptr;
+ }
+}
+
+void GameController::updateKeys() {
+ int activeKeys = m_activeKeys;
+#ifdef BUILD_SDL
+ activeKeys |= m_activeButtons;
+#endif
+ m_threadContext.activeKeys = activeKeys;
+}
+
+#ifdef BUILD_SDL
+void GameController::testSDLEvents() {
+ SDL_Joystick* joystick = m_sdlEvents.joystick;
+ SDL_JoystickUpdate();
+ int numButtons = SDL_JoystickNumButtons(joystick);
+ m_activeButtons = 0;
+ int i;
+ for (i = 0; i < numButtons; ++i) {
+ GBAKey key = GBAInputMapKey(&m_threadContext.inputMap, SDL_BINDING_BUTTON, i);
+ if (key == GBA_KEY_NONE) {
+ continue;
+ }
+ if (SDL_JoystickGetButton(joystick, i)) {
+ m_activeButtons |= 1 << key;
+ }
+ }
+ int numHats = SDL_JoystickNumHats(joystick);
+ for (i = 0; i < numHats; ++i) {
+ int hat = SDL_JoystickGetHat(joystick, i);
+ if (hat & SDL_HAT_UP) {
+ m_activeButtons |= 1 << GBA_KEY_UP;
+ }
+ if (hat & SDL_HAT_LEFT) {
+ m_activeButtons |= 1 << GBA_KEY_LEFT;
+ }
+ if (hat & SDL_HAT_DOWN) {
+ m_activeButtons |= 1 << GBA_KEY_DOWN;
+ }
+ if (hat & SDL_HAT_RIGHT) {
+ m_activeButtons |= 1 << GBA_KEY_RIGHT;
+ }
+ }
+ updateKeys();
+}
+#endif
diff --git a/src/platform/qt/GameController.h b/src/platform/qt/GameController.h
new file mode 100644
index 000000000..95cbc0da6
--- /dev/null
+++ b/src/platform/qt/GameController.h
@@ -0,0 +1,117 @@
+#ifndef QGBA_GAME_CONTROLLER
+#define QGBA_GAME_CONTROLLER
+
+#include
+#include
+#include
+#include
+#include
+
+extern "C" {
+#include "gba-thread.h"
+#ifdef BUILD_SDL
+#include "sdl-events.h"
+#endif
+}
+
+struct GBAAudio;
+struct GBAVideoSoftwareRenderer;
+
+class QThread;
+
+namespace QGBA {
+
+class AudioProcessor;
+
+class GameController : public QObject {
+Q_OBJECT
+
+public:
+ static const bool VIDEO_SYNC = false;
+ static const bool AUDIO_SYNC = true;
+
+ GameController(QObject* parent = nullptr);
+ ~GameController();
+
+ const uint32_t* drawContext() const { return m_drawContext; }
+ GBAThread* thread() { return &m_threadContext; }
+
+ bool isPaused();
+ bool isLoaded() { return m_gameOpen; }
+
+#ifdef USE_GDB_STUB
+ ARMDebugger* debugger();
+ void setDebugger(ARMDebugger*);
+#endif
+
+signals:
+ void frameAvailable(const uint32_t*);
+ void gameStarted(GBAThread*);
+ void gameStopped(GBAThread*);
+ void gamePaused(GBAThread*);
+ void gameUnpaused(GBAThread*);
+ void stateLoaded(GBAThread*);
+
+ void postLog(int level, const QString& log);
+
+public slots:
+ void loadGame(const QString& path, bool dirmode = false);
+ void loadBIOS(const QString& path);
+ void loadPatch(const QString& path);
+ void openGame();
+ void closeGame();
+ void setPaused(bool paused);
+ void reset();
+ void frameAdvance();
+ void keyPressed(int key);
+ void keyReleased(int key);
+ void setAudioBufferSamples(int samples);
+ void setFPSTarget(float fps);
+ void loadState(int slot);
+ void saveState(int slot);
+ void setVideoSync(bool);
+ void setAudioSync(bool);
+ void setFrameskip(int);
+ void setTurbo(bool, bool forced = true);
+ void setAVStream(GBAAVStream*);
+ void clearAVStream();
+
+#ifdef BUILD_SDL
+private slots:
+ void testSDLEvents();
+
+private:
+ GBASDLEvents m_sdlEvents;
+ int m_activeButtons;
+#endif
+
+private:
+ void updateKeys();
+
+ uint32_t* m_drawContext;
+ GBAThread m_threadContext;
+ GBAVideoSoftwareRenderer* m_renderer;
+ int m_activeKeys;
+
+ bool m_gameOpen;
+ bool m_dirmode;
+
+ QString m_fname;
+ QString m_bios;
+ QString m_patch;
+
+ QThread* m_audioThread;
+ AudioProcessor* m_audioProcessor;
+
+ QMutex m_pauseMutex;
+ bool m_pauseAfterFrame;
+
+ bool m_videoSync;
+ bool m_audioSync;
+ bool m_turbo;
+ bool m_turboForced;
+};
+
+}
+
+#endif
diff --git a/src/platform/qt/LoadSaveState.cpp b/src/platform/qt/LoadSaveState.cpp
new file mode 100644
index 000000000..5a27dedf8
--- /dev/null
+++ b/src/platform/qt/LoadSaveState.cpp
@@ -0,0 +1,149 @@
+#include "LoadSaveState.h"
+
+#include "GameController.h"
+#include "VFileDevice.h"
+
+#include
+#include
+
+extern "C" {
+#include "gba-serialize.h"
+#include "gba-video.h"
+}
+
+using namespace QGBA;
+
+LoadSaveState::LoadSaveState(GameController* controller, QWidget* parent)
+ : QWidget(parent)
+ , m_controller(controller)
+ , m_currentFocus(0)
+{
+ m_ui.setupUi(this);
+
+ m_slots[0] = m_ui.state1;
+ m_slots[1] = m_ui.state2;
+ m_slots[2] = m_ui.state3;
+ m_slots[3] = m_ui.state4;
+ m_slots[4] = m_ui.state5;
+ m_slots[5] = m_ui.state6;
+ m_slots[6] = m_ui.state7;
+ m_slots[7] = m_ui.state8;
+ m_slots[8] = m_ui.state9;
+
+ int i;
+ for (i = 0; i < NUM_SLOTS; ++i) {
+ loadState(i + 1);
+ m_slots[i]->installEventFilter(this);
+ connect(m_slots[i], &QAbstractButton::clicked, this, [this, i]() { triggerState(i + 1); });
+ }
+}
+
+void LoadSaveState::setMode(LoadSave mode) {
+ m_mode = mode;
+ QString text = mode == LoadSave::LOAD ? tr("Load State") : tr("Save State");
+ setWindowTitle(text);
+ m_ui.lsLabel->setText(text);
+}
+
+bool LoadSaveState::eventFilter(QObject* object, QEvent* event) {
+ if (event->type() == QEvent::KeyPress) {
+ int column = m_currentFocus % 3;
+ int row = m_currentFocus - column;
+ switch (static_cast(event)->key()) {
+ case Qt::Key_Up:
+ row += 6;
+ break;
+ case Qt::Key_Down:
+ row += 3;
+ break;
+ case Qt::Key_Left:
+ column += 2;
+ break;
+ case Qt::Key_Right:
+ column += 1;
+ break;
+ case Qt::Key_1:
+ case Qt::Key_2:
+ case Qt::Key_3:
+ case Qt::Key_4:
+ case Qt::Key_5:
+ case Qt::Key_6:
+ case Qt::Key_7:
+ case Qt::Key_8:
+ case Qt::Key_9:
+ triggerState(static_cast(event)->key() - Qt::Key_1 + 1);
+ break;
+ case Qt::Key_Escape:
+ close();
+ break;
+ case Qt::Key_Enter:
+ case Qt::Key_Return:
+ triggerState(m_currentFocus + 1);
+ break;
+ default:
+ return false;
+ }
+ column %= 3;
+ row %= 9;
+ m_currentFocus = column + row;
+ m_slots[m_currentFocus]->setFocus();
+ return true;
+ }
+ if (event->type() == QEvent::Enter) {
+ int i;
+ for (i = 0; i < 9; ++i) {
+ if (m_slots[i] == object) {
+ m_currentFocus = i;
+ m_slots[m_currentFocus]->setFocus();
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+void LoadSaveState::loadState(int slot) {
+ GBAThread* thread = m_controller->thread();
+ VFile* vf = GBAGetState(thread->gba, thread->stateDir, slot, false);
+ if (!vf) {
+ m_slots[slot - 1]->setText(tr("Empty"));
+ return;
+ }
+ VFileDevice vdev(vf);
+ QImage stateImage;
+ stateImage.load(&vdev, "PNG");
+ if (!stateImage.isNull()) {
+ QPixmap statePixmap;
+ statePixmap.convertFromImage(stateImage);
+ m_slots[slot - 1]->setIcon(statePixmap);
+ m_slots[slot - 1]->setText(QString());
+ } else {
+ m_slots[slot - 1]->setText(tr("Slot %1").arg(slot));
+ }
+}
+
+void LoadSaveState::triggerState(int slot) {
+ if (m_mode == LoadSave::SAVE) {
+ m_controller->saveState(slot);
+ } else {
+ m_controller->loadState(slot);
+ }
+ close();
+}
+
+void LoadSaveState::closeEvent(QCloseEvent* event) {
+ emit closed();
+ QWidget::closeEvent(event);
+}
+
+void LoadSaveState::showEvent(QShowEvent* event) {
+ m_slots[m_currentFocus]->setFocus();
+ QWidget::showEvent(event);
+}
+
+void LoadSaveState::paintEvent(QPaintEvent*) {
+ QPainter painter(this);
+ QRect full(QPoint(), size());
+ painter.drawPixmap(full, m_currentImage);
+ painter.fillRect(full, QColor(0, 0, 0, 128));
+}
diff --git a/src/platform/qt/LoadSaveState.h b/src/platform/qt/LoadSaveState.h
new file mode 100644
index 000000000..67cd2fa78
--- /dev/null
+++ b/src/platform/qt/LoadSaveState.h
@@ -0,0 +1,52 @@
+#ifndef QGBA_LOAD_SAVE_STATE
+#define QGBA_LOAD_SAVE_STATE
+
+#include
+
+#include "ui_LoadSaveState.h"
+
+namespace QGBA {
+
+class GameController;
+class SavestateButton;
+
+enum class LoadSave {
+ LOAD,
+ SAVE
+};
+
+class LoadSaveState : public QWidget {
+Q_OBJECT
+
+public:
+ const static int NUM_SLOTS = 9;
+
+ LoadSaveState(GameController* controller, QWidget* parent = nullptr);
+
+ void setMode(LoadSave mode);
+
+signals:
+ void closed();
+
+protected:
+ virtual bool eventFilter(QObject*, QEvent*) override;
+ virtual void closeEvent(QCloseEvent*) override;
+ virtual void showEvent(QShowEvent*) override;
+ virtual void paintEvent(QPaintEvent*) override;
+
+private:
+ void loadState(int slot);
+ void triggerState(int slot);
+
+ Ui::LoadSaveState m_ui;
+ GameController* m_controller;
+ SavestateButton* m_slots[NUM_SLOTS];
+ LoadSave m_mode;
+
+ int m_currentFocus;
+ QPixmap m_currentImage;
+};
+
+}
+
+#endif
diff --git a/src/platform/qt/LoadSaveState.ui b/src/platform/qt/LoadSaveState.ui
new file mode 100644
index 000000000..38155cfd7
--- /dev/null
+++ b/src/platform/qt/LoadSaveState.ui
@@ -0,0 +1,328 @@
+
+
+ LoadSaveState
+
+
+
+ 0
+ 0
+ 760
+ 560
+
+
+
+ %1 State
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 2
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 242
+ 162
+
+
+
+ No Save
+
+
+
+ 240
+ 160
+
+
+
+ 1
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 242
+ 162
+
+
+
+ No Save
+
+
+
+ 240
+ 160
+
+
+
+ 2
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ font-size: 30pt; font-weight: bold; color: white;
+
+
+ %1 State
+
+
+ false
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 242
+ 162
+
+
+
+ No Save
+
+
+
+ 240
+ 160
+
+
+
+ 3
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 242
+ 162
+
+
+
+ No Save
+
+
+
+ 240
+ 160
+
+
+
+ 4
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 242
+ 162
+
+
+
+ No Save
+
+
+
+ 240
+ 160
+
+
+
+ 5
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 242
+ 162
+
+
+
+ No Save
+
+
+
+ 240
+ 160
+
+
+
+ 6
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 242
+ 162
+
+
+
+ No Save
+
+
+
+ 240
+ 160
+
+
+
+ 7
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 242
+ 162
+
+
+
+ No Save
+
+
+
+ 240
+ 160
+
+
+
+ 8
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 242
+ 162
+
+
+
+ No Save
+
+
+
+ 240
+ 160
+
+
+
+ 9
+
+
+
+
+
+
+
+ QGBA::SavestateButton
+ QPushButton
+
+
+
+
+ state1
+ state2
+ state3
+ state4
+ state5
+ state6
+ state7
+ state8
+ state9
+
+
+
+
diff --git a/src/platform/qt/LogView.cpp b/src/platform/qt/LogView.cpp
new file mode 100644
index 000000000..7975b79b7
--- /dev/null
+++ b/src/platform/qt/LogView.cpp
@@ -0,0 +1,144 @@
+#include "LogView.h"
+
+#include
+#include
+
+using namespace QGBA;
+
+LogView::LogView(QWidget* parent)
+ : QWidget(parent)
+{
+ m_ui.setupUi(this);
+ connect(m_ui.levelDebug, SIGNAL(toggled(bool)), this, SLOT(setLevelDebug(bool)));
+ connect(m_ui.levelStub, SIGNAL(toggled(bool)), this, SLOT(setLevelStub(bool)));
+ connect(m_ui.levelInfo, SIGNAL(toggled(bool)), this, SLOT(setLevelInfo(bool)));
+ connect(m_ui.levelWarn, SIGNAL(toggled(bool)), this, SLOT(setLevelWarn(bool)));
+ connect(m_ui.levelError, SIGNAL(toggled(bool)), this, SLOT(setLevelError(bool)));
+ connect(m_ui.levelFatal, SIGNAL(toggled(bool)), this, SLOT(setLevelFatal(bool)));
+ connect(m_ui.levelGameError, SIGNAL(toggled(bool)), this, SLOT(setLevelGameError(bool)));
+ connect(m_ui.clear, SIGNAL(clicked()), this, SLOT(clear()));
+ connect(m_ui.maxLines, SIGNAL(valueChanged(int)), this, SLOT(setMaxLines(int)));
+ m_logLevel = GBA_LOG_WARN | GBA_LOG_ERROR | GBA_LOG_FATAL;
+ m_lines = 0;
+ m_ui.maxLines->setValue(DEFAULT_LINE_LIMIT);
+}
+
+void LogView::postLog(int level, const QString& log) {
+ if (!(level & m_logLevel)) {
+ return;
+ }
+ m_ui.view->appendPlainText(QString("%1:\t%2").arg(toString(level)).arg(log));
+ ++m_lines;
+ if (m_lines > m_lineLimit) {
+ clearLine();
+ }
+}
+
+void LogView::clear() {
+ m_ui.view->clear();
+ m_lines = 0;
+}
+
+void LogView::setLevels(int levels) {
+ m_logLevel = levels;
+
+ m_ui.levelDebug->setCheckState(levels & GBA_LOG_DEBUG ? Qt::Checked : Qt::Unchecked);
+ m_ui.levelStub->setCheckState(levels & GBA_LOG_STUB ? Qt::Checked : Qt::Unchecked);
+ m_ui.levelInfo->setCheckState(levels & GBA_LOG_INFO ? Qt::Checked : Qt::Unchecked);
+ m_ui.levelWarn->setCheckState(levels & GBA_LOG_WARN ? Qt::Checked : Qt::Unchecked);
+ m_ui.levelError->setCheckState(levels & GBA_LOG_ERROR ? Qt::Checked : Qt::Unchecked);
+ m_ui.levelFatal->setCheckState(levels & GBA_LOG_FATAL ? Qt::Checked : Qt::Unchecked);
+ m_ui.levelGameError->setCheckState(levels & GBA_LOG_GAME_ERROR ? Qt::Checked : Qt::Unchecked);
+}
+
+void LogView::setLevelDebug(bool set) {
+ if (set) {
+ setLevel(GBA_LOG_DEBUG);
+ } else {
+ clearLevel(GBA_LOG_DEBUG);
+ }
+}
+
+void LogView::setLevelStub(bool set) {
+ if (set) {
+ setLevel(GBA_LOG_STUB);
+ } else {
+ clearLevel(GBA_LOG_STUB);
+ }
+}
+
+void LogView::setLevelInfo(bool set) {
+ if (set) {
+ setLevel(GBA_LOG_INFO);
+ } else {
+ clearLevel(GBA_LOG_INFO);
+ }
+}
+
+void LogView::setLevelWarn(bool set) {
+ if (set) {
+ setLevel(GBA_LOG_WARN);
+ } else {
+ clearLevel(GBA_LOG_WARN);
+ }
+}
+
+void LogView::setLevelError(bool set) {
+ if (set) {
+ setLevel(GBA_LOG_ERROR);
+ } else {
+ clearLevel(GBA_LOG_ERROR);
+ }
+}
+
+void LogView::setLevelFatal(bool set) {
+ if (set) {
+ setLevel(GBA_LOG_FATAL);
+ } else {
+ clearLevel(GBA_LOG_FATAL);
+ }
+}
+
+void LogView::setLevelGameError(bool set) {
+ if (set) {
+ setLevel(GBA_LOG_GAME_ERROR);
+ } else {
+ clearLevel(GBA_LOG_GAME_ERROR);
+ }
+}
+
+void LogView::setMaxLines(int limit) {
+ m_lineLimit = limit;
+ while (m_lines > m_lineLimit) {
+ clearLine();
+ }
+}
+
+QString LogView::toString(int level) {
+ switch (level) {
+ case GBA_LOG_DEBUG:
+ return tr("DEBUG");
+ case GBA_LOG_STUB:
+ return tr("STUB");
+ case GBA_LOG_INFO:
+ return tr("INFO");
+ case GBA_LOG_WARN:
+ return tr("WARN");
+ case GBA_LOG_ERROR:
+ return tr("ERROR");
+ case GBA_LOG_FATAL:
+ return tr("FATAL");
+ case GBA_LOG_GAME_ERROR:
+ return tr("GAME ERROR");
+ }
+ return QString();
+}
+
+void LogView::clearLine() {
+ QTextCursor cursor(m_ui.view->document());
+ cursor.setPosition(0);
+ cursor.select(QTextCursor::BlockUnderCursor);
+ cursor.removeSelectedText();
+ cursor.deleteChar();
+ --m_lines;
+}
diff --git a/src/platform/qt/LogView.h b/src/platform/qt/LogView.h
new file mode 100644
index 000000000..ad26b9d03
--- /dev/null
+++ b/src/platform/qt/LogView.h
@@ -0,0 +1,52 @@
+#ifndef QGBA_LOG_VIEW
+#define QGBA_LOG_VIEW
+
+#include
+
+#include "ui_LogView.h"
+
+extern "C" {
+#include "gba-thread.h"
+}
+
+namespace QGBA {
+
+class LogView : public QWidget {
+Q_OBJECT
+
+public:
+ LogView(QWidget* parent = nullptr);
+
+public slots:
+ void postLog(int level, const QString& log);
+ void setLevels(int levels);
+ void clear();
+
+ void setLevelDebug(bool);
+ void setLevelStub(bool);
+ void setLevelInfo(bool);
+ void setLevelWarn(bool);
+ void setLevelError(bool);
+ void setLevelFatal(bool);
+ void setLevelGameError(bool);
+
+ void setMaxLines(int);
+
+private:
+ static const int DEFAULT_LINE_LIMIT = 1000;
+
+ Ui::LogView m_ui;
+ int m_logLevel;
+ int m_lines;
+ int m_lineLimit;
+
+ static QString toString(int level);
+ void setLevel(int level) { m_logLevel |= level; }
+ void clearLevel(int level) { m_logLevel &= ~level; }
+
+ void clearLine();
+};
+
+}
+
+#endif
diff --git a/src/platform/qt/LogView.ui b/src/platform/qt/LogView.ui
new file mode 100644
index 000000000..110ac2da7
--- /dev/null
+++ b/src/platform/qt/LogView.ui
@@ -0,0 +1,156 @@
+
+
+ LogView
+
+
+
+ 0
+ 0
+ 600
+ 400
+
+
+
+ Logs
+
+
+ -
+
+
-
+
+
+ Enabled Levels
+
+
+
-
+
+
+ Debug
+
+
+ false
+
+
+
+ -
+
+
+ Stub
+
+
+ false
+
+
+
+ -
+
+
+ Info
+
+
+ false
+
+
+
+ -
+
+
+ Warning
+
+
+ true
+
+
+
+ -
+
+
+ Error
+
+
+ true
+
+
+
+ -
+
+
+ Fatal
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ Game Error
+
+
+ false
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ Clear
+
+
+
+ -
+
+
-
+
+
+ Max Lines
+
+
+
+ -
+
+
+ 9999
+
+
+
+
+
+
+
+ -
+
+
+ true
+
+
+
+
+
+
+
+
diff --git a/src/platform/qt/SavestateButton.cpp b/src/platform/qt/SavestateButton.cpp
new file mode 100644
index 000000000..94bbd6e85
--- /dev/null
+++ b/src/platform/qt/SavestateButton.cpp
@@ -0,0 +1,41 @@
+#include "SavestateButton.h"
+
+#include
+#include
+
+using namespace QGBA;
+
+SavestateButton::SavestateButton(QWidget* parent)
+ : QAbstractButton(parent)
+{
+ // Nothing to do
+}
+
+void SavestateButton::paintEvent(QPaintEvent*) {
+ QPainter painter(this);
+ QRect frame(0, 0, width(), height());
+ QRect full(1, 1, width() - 2, height() - 2);
+ QPalette palette = QApplication::palette(this);
+ painter.setPen(Qt::black);
+ QLinearGradient grad(0, 0, 0, 1);
+ grad.setCoordinateMode(QGradient::ObjectBoundingMode);
+ QColor shadow = palette.color(QPalette::Shadow);
+ QColor dark = palette.color(QPalette::Dark);
+ shadow.setAlpha(128);
+ dark.setAlpha(128);
+ grad.setColorAt(0, shadow);
+ grad.setColorAt(1, dark);
+ painter.setBrush(grad);
+ painter.drawRect(frame);
+ painter.setPen(Qt::NoPen);
+ if (!icon().isNull()) {
+ painter.drawPixmap(full, icon().pixmap(full.size()));
+ }
+ if (hasFocus()) {
+ QColor highlight = palette.color(QPalette::Highlight);
+ highlight.setAlpha(128);
+ painter.fillRect(full, highlight);
+ }
+ painter.setPen(QPen(palette.text(), 0));
+ painter.drawText(full, Qt::AlignCenter, text());
+}
diff --git a/src/platform/qt/SavestateButton.h b/src/platform/qt/SavestateButton.h
new file mode 100644
index 000000000..4d7a1ea12
--- /dev/null
+++ b/src/platform/qt/SavestateButton.h
@@ -0,0 +1,18 @@
+#ifndef QGBA_SAVESTATE_BUTTON
+#define QGBA_SAVESTATE_BUTTON
+
+#include
+
+namespace QGBA {
+
+class SavestateButton : public QAbstractButton {
+public:
+ SavestateButton(QWidget* parent = nullptr);
+
+protected:
+ virtual void paintEvent(QPaintEvent*) override;
+};
+
+}
+
+#endif
diff --git a/src/platform/qt/VFileDevice.cpp b/src/platform/qt/VFileDevice.cpp
new file mode 100644
index 000000000..ee458405f
--- /dev/null
+++ b/src/platform/qt/VFileDevice.cpp
@@ -0,0 +1,30 @@
+#include "VFileDevice.h"
+
+extern "C" {
+#include "util/vfs.h"
+}
+
+using namespace QGBA;
+
+VFileDevice::VFileDevice(VFile* vf, QObject* parent)
+ : QIODevice(parent)
+ , m_vf(vf)
+{
+ // Nothing to do
+}
+
+qint64 VFileDevice::readData(char* data, qint64 maxSize) {
+ return m_vf->read(m_vf, data, maxSize);
+}
+
+qint64 VFileDevice::writeData(const char* data, qint64 maxSize) {
+ return m_vf->write(m_vf, data, maxSize);
+}
+
+qint64 VFileDevice::size() const {
+ // TODO: Add size method to VFile so this can be actually const
+ ssize_t pos = m_vf->seek(m_vf, 0, SEEK_CUR);
+ qint64 size = m_vf->seek(m_vf, 0, SEEK_END);
+ m_vf->seek(m_vf, pos, SEEK_SET);
+ return size;
+}
diff --git a/src/platform/qt/VFileDevice.h b/src/platform/qt/VFileDevice.h
new file mode 100644
index 000000000..99595e0f1
--- /dev/null
+++ b/src/platform/qt/VFileDevice.h
@@ -0,0 +1,27 @@
+#ifndef QGBA_VFILE_DEVICE
+#define QGBA_VFILE_DEVICE
+
+#include
+
+struct VFile;
+
+namespace QGBA {
+
+class VFileDevice : public QIODevice {
+Q_OBJECT
+
+public:
+ VFileDevice(VFile* vf, QObject* parent = nullptr);
+
+protected:
+ virtual qint64 readData(char* data, qint64 maxSize) override;
+ virtual qint64 writeData(const char* data, qint64 maxSize) override;
+ virtual qint64 size() const override;
+
+private:
+ mutable VFile* m_vf;
+};
+
+}
+
+#endif
diff --git a/src/platform/qt/VideoView.cpp b/src/platform/qt/VideoView.cpp
new file mode 100644
index 000000000..b629fe681
--- /dev/null
+++ b/src/platform/qt/VideoView.cpp
@@ -0,0 +1,185 @@
+#include "VideoView.h"
+
+#ifdef USE_FFMPEG
+
+#include
+#include
+
+using namespace QGBA;
+
+QMap VideoView::s_acodecMap;
+QMap VideoView::s_vcodecMap;
+QMap VideoView::s_containerMap;
+
+VideoView::VideoView(QWidget* parent)
+ : QWidget(parent)
+ , m_audioCodecCstr(nullptr)
+ , m_videoCodecCstr(nullptr)
+ , m_containerCstr(nullptr)
+{
+ m_ui.setupUi(this);
+
+ if (s_acodecMap.empty()) {
+ s_acodecMap["aac"] = "libfaac";
+ s_acodecMap["mp3"] = "libmp3lame";
+ s_acodecMap["uncompressed"] = "pcm_s16le";
+ }
+ if (s_vcodecMap.empty()) {
+ s_vcodecMap["h264"] = "libx264rgb";
+ }
+ if (s_containerMap.empty()) {
+ s_containerMap["mkv"] = "matroska";
+ }
+
+ connect(m_ui.buttonBox, SIGNAL(rejected()), this, SLOT(close()));
+ connect(m_ui.start, SIGNAL(clicked()), this, SLOT(startRecording()));
+ connect(m_ui.stop, SIGNAL(clicked()), this, SLOT(stopRecording()));
+
+ connect(m_ui.selectFile, SIGNAL(clicked()), this, SLOT(selectFile()));
+ connect(m_ui.filename, SIGNAL(textChanged(const QString&)), this, SLOT(setFilename(const QString&)));
+
+ connect(m_ui.audio, SIGNAL(activated(const QString&)), this, SLOT(setAudioCodec(const QString&)));
+ connect(m_ui.video, SIGNAL(activated(const QString&)), this, SLOT(setVideoCodec(const QString&)));
+ connect(m_ui.container, SIGNAL(activated(const QString&)), this, SLOT(setContainer(const QString&)));
+
+ connect(m_ui.abr, SIGNAL(valueChanged(int)), this, SLOT(setAudioBitrate(int)));
+ connect(m_ui.vbr, SIGNAL(valueChanged(int)), this, SLOT(setVideoBitrate(int)));
+
+ FFmpegEncoderInit(&m_encoder);
+
+ setAudioCodec(m_ui.audio->currentText());
+ setVideoCodec(m_ui.video->currentText());
+ setContainer(m_ui.container->currentText());
+}
+
+VideoView::~VideoView() {
+ stopRecording();
+ free(m_audioCodecCstr);
+ free(m_videoCodecCstr);
+ free(m_containerCstr);
+}
+
+void VideoView::startRecording() {
+ if (!validateSettings()) {
+ return;
+ }
+ if (!FFmpegEncoderOpen(&m_encoder, m_filename.toLocal8Bit().constData())) {
+ return;
+ }
+ m_ui.start->setEnabled(false);
+ m_ui.stop->setEnabled(true);
+ emit recordingStarted(&m_encoder.d);
+}
+
+void VideoView::stopRecording() {
+ emit recordingStopped();
+ FFmpegEncoderClose(&m_encoder);
+ m_ui.stop->setEnabled(false);
+ validateSettings();
+}
+
+void VideoView::selectFile() {
+ QString filename = QFileDialog::getSaveFileName(this, tr("Select output file"));
+ if (!filename.isEmpty()) {
+ m_ui.filename->setText(filename);
+ }
+}
+
+void VideoView::setFilename(const QString& fname) {
+ m_filename = fname;
+ validateSettings();
+}
+
+void VideoView::setAudioCodec(const QString& codec) {
+ free(m_audioCodecCstr);
+ m_audioCodec = sanitizeCodec(codec);
+ if (s_acodecMap.contains(m_audioCodec)) {
+ m_audioCodec = s_acodecMap[m_audioCodec];
+ }
+ m_audioCodecCstr = strdup(m_audioCodec.toLocal8Bit().constData());
+ if (!FFmpegEncoderSetAudio(&m_encoder, m_audioCodecCstr, m_abr)) {
+ free(m_audioCodecCstr);
+ m_audioCodecCstr = nullptr;
+ }
+ validateSettings();
+}
+
+void VideoView::setVideoCodec(const QString& codec) {
+ free(m_videoCodecCstr);
+ m_videoCodec = sanitizeCodec(codec);
+ if (s_vcodecMap.contains(m_videoCodec)) {
+ m_videoCodec = s_vcodecMap[m_videoCodec];
+ }
+ m_videoCodecCstr = strdup(m_videoCodec.toLocal8Bit().constData());
+ if (!FFmpegEncoderSetVideo(&m_encoder, m_videoCodecCstr, m_vbr)) {
+ free(m_videoCodecCstr);
+ m_videoCodecCstr = nullptr;
+ }
+ validateSettings();
+}
+
+void VideoView::setContainer(const QString& container) {
+ free(m_containerCstr);
+ m_container = sanitizeCodec(container);
+ if (s_containerMap.contains(m_container)) {
+ m_container = s_containerMap[m_container];
+ }
+ m_containerCstr = strdup(m_container.toLocal8Bit().constData());
+ if (!FFmpegEncoderSetContainer(&m_encoder, m_containerCstr)) {
+ free(m_containerCstr);
+ m_containerCstr = nullptr;
+ }
+ validateSettings();
+}
+
+void VideoView::setAudioBitrate(int br) {
+ m_abr = br;
+ FFmpegEncoderSetAudio(&m_encoder, m_audioCodecCstr, m_abr);
+ validateSettings();
+}
+
+void VideoView::setVideoBitrate(int br) {
+ m_abr = br;
+ FFmpegEncoderSetVideo(&m_encoder, m_videoCodecCstr, m_vbr);
+ validateSettings();
+}
+
+bool VideoView::validateSettings() {
+ bool valid = true;
+ if (!m_audioCodecCstr) {
+ valid = false;
+ m_ui.audio->setStyleSheet("QComboBox { color: red; }");
+ } else {
+ m_ui.audio->setStyleSheet("");
+ }
+
+ if (!m_videoCodecCstr) {
+ valid = false;
+ m_ui.video->setStyleSheet("QComboBox { color: red; }");
+ } else {
+ m_ui.video->setStyleSheet("");
+ }
+
+ if (!m_containerCstr) {
+ valid = false;
+ m_ui.container->setStyleSheet("QComboBox { color: red; }");
+ } else {
+ m_ui.container->setStyleSheet("");
+ }
+
+ // This |valid| check is necessary as if one of the cstrs
+ // is null, the encoder likely has a dangling pointer
+ if (valid && !FFmpegEncoderVerifyContainer(&m_encoder)) {
+ valid = false;
+ }
+
+ m_ui.start->setEnabled(valid && !m_filename.isNull());
+ return valid;
+}
+
+QString VideoView::sanitizeCodec(const QString& codec) {
+ QString sanitized = codec.toLower();
+ return sanitized.remove(QChar('.'));
+}
+
+#endif
diff --git a/src/platform/qt/VideoView.h b/src/platform/qt/VideoView.h
new file mode 100644
index 000000000..2a53b9a8a
--- /dev/null
+++ b/src/platform/qt/VideoView.h
@@ -0,0 +1,71 @@
+#ifndef QGBA_VIDEO_VIEW
+#define QGBA_VIDEO_VIEW
+
+#ifdef USE_FFMPEG
+
+#include
+
+#include "ui_VideoView.h"
+
+extern "C" {
+#include "platform/ffmpeg/ffmpeg-encoder.h"
+}
+
+namespace QGBA {
+
+class VideoView : public QWidget {
+Q_OBJECT
+
+public:
+ VideoView(QWidget* parent = nullptr);
+ virtual ~VideoView();
+
+ GBAAVStream* getStream() { return &m_encoder.d; }
+
+public slots:
+ void startRecording();
+ void stopRecording();
+
+signals:
+ void recordingStarted(GBAAVStream*);
+ void recordingStopped();
+
+private slots:
+ void selectFile();
+ void setFilename(const QString&);
+ void setAudioCodec(const QString&);
+ void setVideoCodec(const QString&);
+ void setContainer(const QString&);
+
+ void setAudioBitrate(int);
+ void setVideoBitrate(int);
+
+private:
+ bool validateSettings();
+ static QString sanitizeCodec(const QString&);
+
+ Ui::VideoView m_ui;
+
+ FFmpegEncoder m_encoder;
+
+ QString m_filename;
+ QString m_audioCodec;
+ QString m_videoCodec;
+ QString m_container;
+ char* m_audioCodecCstr;
+ char* m_videoCodecCstr;
+ char* m_containerCstr;
+
+ int m_abr;
+ int m_vbr;
+
+ static QMap s_acodecMap;
+ static QMap s_vcodecMap;
+ static QMap s_containerMap;
+};
+
+}
+
+#endif
+
+#endif
diff --git a/src/platform/qt/VideoView.ui b/src/platform/qt/VideoView.ui
new file mode 100644
index 000000000..b48f10fe0
--- /dev/null
+++ b/src/platform/qt/VideoView.ui
@@ -0,0 +1,256 @@
+
+
+ VideoView
+
+
+
+ 0
+ 0
+ 462
+ 194
+
+
+
+
+ 0
+ 0
+
+
+
+ Record Video
+
+
+
+ QLayout::SetFixedSize
+
+ -
+
+
+ Format
+
+
+
-
+
+
+ true
+
+
-
+
+ MKV
+
+
+ -
+
+ AVI
+
+
+ -
+
+ MP4
+
+
+
+
+ -
+
+
+ true
+
+
-
+
+ PNG
+
+
+ -
+
+ h.264
+
+
+ -
+
+ FFV1
+
+
+
+
+ -
+
+
+ true
+
+
-
+
+ FLAC
+
+
+ -
+
+ Vorbis
+
+
+ -
+
+ MP3
+
+
+ -
+
+ AAC
+
+
+ -
+
+ Uncompressed
+
+
+
+
+
+
+
+ -
+
+
+ Bitrate
+
+
+
-
+
+
+ Video
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ vbr
+
+
+
+ -
+
+
+
+
+
+ 200
+
+
+ 2000
+
+
+ 400
+
+
+
+ -
+
+
+ Audio
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ abr
+
+
+
+ -
+
+
+ 16
+
+
+ 320
+
+
+ 192
+
+
+
+
+
+
+ -
+
+
-
+
+
+ false
+
+
+
+ 0
+ 0
+
+
+
+ Start
+
+
+
+ -
+
+
+ false
+
+
+
+ 0
+ 0
+
+
+
+ Stop
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Select File
+
+
+
+ -
+
+
+
+ 1
+ 0
+
+
+
+
+
+
+ -
+
+
+ QDialogButtonBox::Close
+
+
+
+
+
+
+ filename
+ start
+ stop
+ selectFile
+ container
+ video
+ audio
+ vbr
+ abr
+
+
+
+
diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp
new file mode 100644
index 000000000..e27131f31
--- /dev/null
+++ b/src/platform/qt/Window.cpp
@@ -0,0 +1,508 @@
+#include "Window.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "GameController.h"
+#include "GDBController.h"
+#include "GDBWindow.h"
+#include "LoadSaveState.h"
+#include "LogView.h"
+#include "VideoView.h"
+
+extern "C" {
+#include "platform/commandline.h"
+}
+
+using namespace QGBA;
+
+Window::Window(QWidget* parent)
+ : QMainWindow(parent)
+ , m_logView(new LogView())
+ , m_stateWindow(nullptr)
+ , m_screenWidget(new WindowBackground())
+ , m_logo(":/res/mgba-1024.png")
+#ifdef USE_FFMPEG
+ , m_videoView(nullptr)
+#endif
+#ifdef USE_GDB_STUB
+ , m_gdbController(nullptr)
+#endif
+{
+ setWindowTitle(PROJECT_NAME);
+ m_controller = new GameController(this);
+
+ QGLFormat format(QGLFormat(QGL::Rgba | QGL::DoubleBuffer));
+ format.setSwapInterval(1);
+ m_display = new Display(format);
+
+ m_screenWidget->setMinimumSize(m_display->minimumSize());
+ m_screenWidget->setSizePolicy(m_display->sizePolicy());
+ m_screenWidget->setSizeHint(m_display->minimumSize() * 2);
+ setCentralWidget(m_screenWidget);
+
+ connect(m_controller, SIGNAL(gameStarted(GBAThread*)), this, SLOT(gameStarted(GBAThread*)));
+ connect(m_controller, SIGNAL(gameStopped(GBAThread*)), m_display, SLOT(stopDrawing()));
+ connect(m_controller, SIGNAL(gameStopped(GBAThread*)), this, SLOT(gameStopped()));
+ connect(m_controller, SIGNAL(stateLoaded(GBAThread*)), m_display, SLOT(forceDraw()));
+ connect(m_controller, SIGNAL(postLog(int, const QString&)), m_logView, SLOT(postLog(int, const QString&)));
+ connect(this, SIGNAL(startDrawing(const uint32_t*, GBAThread*)), m_display, SLOT(startDrawing(const uint32_t*, GBAThread*)), Qt::QueuedConnection);
+ connect(this, SIGNAL(shutdown()), m_display, SLOT(stopDrawing()));
+ connect(this, SIGNAL(shutdown()), m_controller, SLOT(closeGame()));
+ connect(this, SIGNAL(shutdown()), m_logView, SLOT(hide()));
+ connect(this, SIGNAL(audioBufferSamplesChanged(int)), m_controller, SLOT(setAudioBufferSamples(int)));
+ connect(this, SIGNAL(fpsTargetChanged(float)), m_controller, SLOT(setFPSTarget(float)));
+
+ setupMenu(menuBar());
+}
+
+Window::~Window() {
+ delete m_logView;
+ delete m_videoView;
+}
+
+GBAKey Window::mapKey(int qtKey) {
+ switch (qtKey) {
+ case Qt::Key_Z:
+ return GBA_KEY_A;
+ break;
+ case Qt::Key_X:
+ return GBA_KEY_B;
+ break;
+ case Qt::Key_A:
+ return GBA_KEY_L;
+ break;
+ case Qt::Key_S:
+ return GBA_KEY_R;
+ break;
+ case Qt::Key_Return:
+ return GBA_KEY_START;
+ break;
+ case Qt::Key_Backspace:
+ return GBA_KEY_SELECT;
+ break;
+ case Qt::Key_Up:
+ return GBA_KEY_UP;
+ break;
+ case Qt::Key_Down:
+ return GBA_KEY_DOWN;
+ break;
+ case Qt::Key_Left:
+ return GBA_KEY_LEFT;
+ break;
+ case Qt::Key_Right:
+ return GBA_KEY_RIGHT;
+ break;
+ default:
+ return GBA_KEY_NONE;
+ }
+}
+
+void Window::optionsPassed(StartupOptions* opts) {
+ if (opts->logLevel) {
+ m_logView->setLevels(opts->logLevel);
+ }
+
+ if (opts->frameskip) {
+ m_controller->setFrameskip(opts->frameskip);
+ }
+
+ if (opts->bios) {
+ m_controller->loadBIOS(opts->bios);
+ }
+
+ if (opts->patch) {
+ m_controller->loadPatch(opts->patch);
+ }
+
+ if (opts->fname) {
+ m_controller->loadGame(opts->fname, opts->dirmode);
+ }
+}
+
+void Window::selectROM() {
+ QString filename = QFileDialog::getOpenFileName(this, tr("Select ROM"));
+ if (!filename.isEmpty()) {
+ m_controller->loadGame(filename);
+ }
+}
+
+void Window::selectBIOS() {
+ QString filename = QFileDialog::getOpenFileName(this, tr("Select BIOS"));
+ if (!filename.isEmpty()) {
+ m_controller->loadBIOS(filename);
+ }
+}
+
+void Window::selectPatch() {
+ QString filename = QFileDialog::getOpenFileName(this, tr("Select patch"), QString(), tr("Patches (*.ips *.ups)"));
+ if (!filename.isEmpty()) {
+ m_controller->loadPatch(filename);
+ }
+}
+
+#ifdef USE_FFMPEG
+void Window::openVideoWindow() {
+ if (!m_videoView) {
+ m_videoView = new VideoView();
+ connect(m_videoView, SIGNAL(recordingStarted(GBAAVStream*)), m_controller, SLOT(setAVStream(GBAAVStream*)));
+ connect(m_videoView, SIGNAL(recordingStopped()), m_controller, SLOT(clearAVStream()), Qt::DirectConnection);
+ connect(m_controller, SIGNAL(gameStopped(GBAThread*)), m_videoView, SLOT(stopRecording()));
+ connect(m_controller, SIGNAL(gameStopped(GBAThread*)), m_videoView, SLOT(close()));
+ connect(this, SIGNAL(shutdown()), m_videoView, SLOT(close()));
+ }
+ m_videoView->show();
+}
+#endif
+
+#ifdef USE_GDB_STUB
+void Window::gdbOpen() {
+ if (!m_gdbController) {
+ m_gdbController = new GDBController(m_controller, this);
+ }
+ GDBWindow* window = new GDBWindow(m_gdbController);
+ window->show();
+}
+#endif
+
+void Window::keyPressEvent(QKeyEvent* event) {
+ if (event->isAutoRepeat()) {
+ QWidget::keyPressEvent(event);
+ return;
+ }
+ if (event->key() == Qt::Key_Tab) {
+ m_controller->setTurbo(true, false);
+ }
+ GBAKey key = mapKey(event->key());
+ if (key == GBA_KEY_NONE) {
+ QWidget::keyPressEvent(event);
+ return;
+ }
+ m_controller->keyPressed(key);
+ event->accept();
+}
+
+void Window::keyReleaseEvent(QKeyEvent* event) {
+ if (event->isAutoRepeat()) {
+ QWidget::keyReleaseEvent(event);
+ return;
+ }
+ if (event->key() == Qt::Key_Tab) {
+ m_controller->setTurbo(false, false);
+ }
+ GBAKey key = mapKey(event->key());
+ if (key == GBA_KEY_NONE) {
+ QWidget::keyPressEvent(event);
+ return;
+ }
+ m_controller->keyReleased(key);
+ event->accept();
+}
+
+void Window::resizeEvent(QResizeEvent*) {
+ redoLogo();
+}
+
+void Window::closeEvent(QCloseEvent* event) {
+ emit shutdown();
+ QMainWindow::closeEvent(event);
+}
+
+void Window::toggleFullScreen() {
+ if (isFullScreen()) {
+ showNormal();
+ } else {
+ showFullScreen();
+ }
+}
+
+void Window::gameStarted(GBAThread* context) {
+ emit startDrawing(m_controller->drawContext(), context);
+ foreach (QAction* action, m_gameActions) {
+ action->setDisabled(false);
+ }
+ char title[13] = { '\0' };
+ GBAGetGameTitle(context->gba, title);
+ setWindowTitle(tr(PROJECT_NAME " - %1").arg(title));
+ attachWidget(m_display);
+ m_screenWidget->setScaledContents(true);
+}
+
+void Window::gameStopped() {
+ foreach (QAction* action, m_gameActions) {
+ action->setDisabled(true);
+ }
+ setWindowTitle(tr(PROJECT_NAME));
+ detachWidget(m_display);
+ m_screenWidget->setScaledContents(false);
+ redoLogo();
+}
+
+void Window::redoLogo() {
+ if (m_controller->isLoaded()) {
+ return;
+ }
+ QPixmap logo(m_logo.scaled(m_screenWidget->size() * m_screenWidget->devicePixelRatio(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
+ logo.setDevicePixelRatio(m_screenWidget->devicePixelRatio());
+ m_screenWidget->setPixmap(logo);
+}
+
+void Window::openStateWindow(LoadSave ls) {
+ if (m_stateWindow) {
+ return;
+ }
+ bool wasPaused = m_controller->isPaused();
+ m_stateWindow = new LoadSaveState(m_controller);
+ connect(this, SIGNAL(shutdown()), m_stateWindow, SLOT(close()));
+ connect(m_controller, SIGNAL(gameStopped(GBAThread*)), m_stateWindow, SLOT(close()));
+ connect(m_stateWindow, &LoadSaveState::closed, [this]() {
+ m_screenWidget->layout()->removeWidget(m_stateWindow);
+ m_stateWindow = nullptr;
+ setFocus();
+ });
+ if (!wasPaused) {
+ m_controller->setPaused(true);
+ connect(m_stateWindow, &LoadSaveState::closed, [this]() { m_controller->setPaused(false); });
+ }
+ m_stateWindow->setAttribute(Qt::WA_DeleteOnClose);
+ m_stateWindow->setMode(ls);
+ attachWidget(m_stateWindow);
+}
+
+void Window::setupMenu(QMenuBar* menubar) {
+ menubar->clear();
+ QMenu* fileMenu = menubar->addMenu(tr("&File"));
+ fileMenu->addAction(tr("Load &ROM..."), this, SLOT(selectROM()), QKeySequence::Open);
+ fileMenu->addAction(tr("Load &BIOS..."), this, SLOT(selectBIOS()));
+ fileMenu->addAction(tr("Load &patch..."), this, SLOT(selectPatch()));
+
+ fileMenu->addSeparator();
+
+ QAction* loadState = new QAction(tr("&Load state"), fileMenu);
+ loadState->setShortcut(tr("Ctrl+L"));
+ connect(loadState, &QAction::triggered, [this]() { this->openStateWindow(LoadSave::LOAD); });
+ m_gameActions.append(loadState);
+ fileMenu->addAction(loadState);
+
+ QAction* saveState = new QAction(tr("&Save state"), fileMenu);
+ saveState->setShortcut(tr("Ctrl+S"));
+ connect(saveState, &QAction::triggered, [this]() { this->openStateWindow(LoadSave::SAVE); });
+ m_gameActions.append(saveState);
+ fileMenu->addAction(saveState);
+
+ QMenu* quickLoadMenu = fileMenu->addMenu(tr("Quick load"));
+ QMenu* quickSaveMenu = fileMenu->addMenu(tr("Quick save"));
+ int i;
+ for (i = 1; i < 10; ++i) {
+ QAction* quickLoad = new QAction(tr("State &%1").arg(i), quickLoadMenu);
+ quickLoad->setShortcut(tr("F%1").arg(i));
+ connect(quickLoad, &QAction::triggered, [this, i]() { m_controller->loadState(i); });
+ m_gameActions.append(quickLoad);
+ quickLoadMenu->addAction(quickLoad);
+
+ QAction* quickSave = new QAction(tr("State &%1").arg(i), quickSaveMenu);
+ quickSave->setShortcut(tr("Shift+F%1").arg(i));
+ connect(quickSave, &QAction::triggered, [this, i]() { m_controller->saveState(i); });
+ m_gameActions.append(quickSave);
+ quickSaveMenu->addAction(quickSave);
+ }
+
+#if defined(USE_PNG) || defined(USE_FFMPEG)
+ fileMenu->addSeparator();
+#endif
+
+#ifdef USE_PNG
+ QAction* screenshot = new QAction(tr("Take &screenshot"), fileMenu);
+ screenshot->setShortcut(tr("F12"));
+ connect(screenshot, SIGNAL(triggered()), m_display, SLOT(screenshot()));
+ m_gameActions.append(screenshot);
+ fileMenu->addAction(screenshot);
+#endif
+
+#ifdef USE_FFMPEG
+ QAction* recordOutput = new QAction(tr("Record output..."), fileMenu);
+ recordOutput->setShortcut(tr("F11"));
+ connect(recordOutput, SIGNAL(triggered()), this, SLOT(openVideoWindow()));
+ fileMenu->addAction(recordOutput);
+#endif
+
+#ifndef Q_OS_MAC
+ fileMenu->addSeparator();
+ fileMenu->addAction(tr("E&xit"), this, SLOT(close()), QKeySequence::Quit);
+#endif
+
+ QMenu* emulationMenu = menubar->addMenu(tr("&Emulation"));
+ QAction* reset = new QAction(tr("&Reset"), emulationMenu);
+ reset->setShortcut(tr("Ctrl+R"));
+ connect(reset, SIGNAL(triggered()), m_controller, SLOT(reset()));
+ m_gameActions.append(reset);
+ emulationMenu->addAction(reset);
+
+ QAction* shutdown = new QAction(tr("Sh&utdown"), emulationMenu);
+ connect(shutdown, SIGNAL(triggered()), m_controller, SLOT(closeGame()));
+ m_gameActions.append(shutdown);
+ emulationMenu->addAction(shutdown);
+ emulationMenu->addSeparator();
+
+ QAction* pause = new QAction(tr("&Pause"), emulationMenu);
+ pause->setChecked(false);
+ pause->setCheckable(true);
+ pause->setShortcut(tr("Ctrl+P"));
+ connect(pause, SIGNAL(triggered(bool)), m_controller, SLOT(setPaused(bool)));
+ connect(m_controller, &GameController::gamePaused, [this, pause]() {
+ pause->setChecked(true);
+
+ QImage currentImage(reinterpret_cast(m_controller->drawContext()), VIDEO_HORIZONTAL_PIXELS, VIDEO_VERTICAL_PIXELS, 1024, QImage::Format_RGB32);
+ QPixmap pixmap;
+ pixmap.convertFromImage(currentImage.rgbSwapped());
+ m_screenWidget->setPixmap(pixmap);
+ });
+ connect(m_controller, &GameController::gameUnpaused, [pause]() { pause->setChecked(false); });
+ m_gameActions.append(pause);
+ emulationMenu->addAction(pause);
+
+ QAction* frameAdvance = new QAction(tr("&Next frame"), emulationMenu);
+ frameAdvance->setShortcut(tr("Ctrl+N"));
+ connect(frameAdvance, SIGNAL(triggered()), m_controller, SLOT(frameAdvance()));
+ m_gameActions.append(frameAdvance);
+ emulationMenu->addAction(frameAdvance);
+
+ QMenu* target = emulationMenu->addMenu("FPS target");
+ QAction* setTarget = new QAction(tr("15"), emulationMenu);
+ connect(setTarget, &QAction::triggered, [this]() { emit fpsTargetChanged(15); });
+ target->addAction(setTarget);
+ setTarget = new QAction(tr("30"), emulationMenu);
+ connect(setTarget, &QAction::triggered, [this]() { emit fpsTargetChanged(30); });
+ target->addAction(setTarget);
+ setTarget = new QAction(tr("45"), emulationMenu);
+ connect(setTarget, &QAction::triggered, [this]() { emit fpsTargetChanged(45); });
+ target->addAction(setTarget);
+ setTarget = new QAction(tr("60"), emulationMenu);
+ connect(setTarget, &QAction::triggered, [this]() { emit fpsTargetChanged(60); });
+ target->addAction(setTarget);
+ setTarget = new QAction(tr("90"), emulationMenu);
+ connect(setTarget, &QAction::triggered, [this]() { emit fpsTargetChanged(90); });
+ target->addAction(setTarget);
+ setTarget = new QAction(tr("120"), emulationMenu);
+ connect(setTarget, &QAction::triggered, [this]() { emit fpsTargetChanged(120); });
+ target->addAction(setTarget);
+ setTarget = new QAction(tr("240"), emulationMenu);
+ connect(setTarget, &QAction::triggered, [this]() { emit fpsTargetChanged(240); });
+ target->addAction(setTarget);
+
+ emulationMenu->addSeparator();
+
+ QAction* turbo = new QAction(tr("T&urbo"), emulationMenu);
+ turbo->setCheckable(true);
+ turbo->setChecked(false);
+ turbo->setShortcut(tr("Shift+Tab"));
+ connect(turbo, SIGNAL(triggered(bool)), m_controller, SLOT(setTurbo(bool)));
+ emulationMenu->addAction(turbo);
+
+ QAction* videoSync = new QAction(tr("Sync to &video"), emulationMenu);
+ videoSync->setCheckable(true);
+ videoSync->setChecked(GameController::VIDEO_SYNC);
+ connect(videoSync, SIGNAL(triggered(bool)), m_controller, SLOT(setVideoSync(bool)));
+ emulationMenu->addAction(videoSync);
+
+ QAction* audioSync = new QAction(tr("Sync to &audio"), emulationMenu);
+ audioSync->setCheckable(true);
+ audioSync->setChecked(GameController::AUDIO_SYNC);
+ connect(audioSync, SIGNAL(triggered(bool)), m_controller, SLOT(setAudioSync(bool)));
+ emulationMenu->addAction(audioSync);
+
+ QMenu* videoMenu = menubar->addMenu(tr("&Video"));
+ QMenu* frameMenu = videoMenu->addMenu(tr("Frame size"));
+ QAction* setSize = new QAction(tr("1x"), videoMenu);
+ connect(setSize, &QAction::triggered, [this]() {
+ showNormal();
+ resize(VIDEO_HORIZONTAL_PIXELS, VIDEO_VERTICAL_PIXELS);
+ });
+ frameMenu->addAction(setSize);
+ setSize = new QAction(tr("2x"), videoMenu);
+ connect(setSize, &QAction::triggered, [this]() {
+ showNormal();
+ resize(VIDEO_HORIZONTAL_PIXELS * 2, VIDEO_VERTICAL_PIXELS * 2);
+ });
+ frameMenu->addAction(setSize);
+ setSize = new QAction(tr("3x"), videoMenu);
+ connect(setSize, &QAction::triggered, [this]() {
+ showNormal();
+ resize(VIDEO_HORIZONTAL_PIXELS * 3, VIDEO_VERTICAL_PIXELS * 3);
+ });
+ frameMenu->addAction(setSize);
+ setSize = new QAction(tr("4x"), videoMenu);
+ connect(setSize, &QAction::triggered, [this]() {
+ showNormal();
+ resize(VIDEO_HORIZONTAL_PIXELS * 4, VIDEO_VERTICAL_PIXELS * 4);
+ });
+ frameMenu->addAction(setSize);
+ frameMenu->addAction(tr("Fullscreen"), this, SLOT(toggleFullScreen()), QKeySequence("Ctrl+F"));
+
+ QMenu* skipMenu = videoMenu->addMenu(tr("Frame&skip"));
+ for (int i = 0; i <= 10; ++i) {
+ QAction* setSkip = new QAction(QString::number(i), skipMenu);
+ connect(setSkip, &QAction::triggered, [this, i]() {
+ m_controller->setFrameskip(i);
+ });
+ skipMenu->addAction(setSkip);
+ }
+
+ QMenu* soundMenu = menubar->addMenu(tr("&Sound"));
+ QMenu* buffersMenu = soundMenu->addMenu(tr("Buffer &size"));
+ QAction* setBuffer = new QAction(tr("512"), buffersMenu);
+ connect(setBuffer, &QAction::triggered, [this]() { emit audioBufferSamplesChanged(512); });
+ buffersMenu->addAction(setBuffer);
+ setBuffer = new QAction(tr("1024"), buffersMenu);
+ connect(setBuffer, &QAction::triggered, [this]() { emit audioBufferSamplesChanged(1024); });
+ buffersMenu->addAction(setBuffer);
+ setBuffer = new QAction(tr("2048"), buffersMenu);
+ connect(setBuffer, &QAction::triggered, [this]() { emit audioBufferSamplesChanged(2048); });
+ buffersMenu->addAction(setBuffer);
+
+ QMenu* debuggingMenu = menubar->addMenu(tr("&Debugging"));
+ QAction* viewLogs = new QAction(tr("View &logs..."), debuggingMenu);
+ connect(viewLogs, SIGNAL(triggered()), m_logView, SLOT(show()));
+ debuggingMenu->addAction(viewLogs);
+#ifdef USE_GDB_STUB
+ QAction* gdbWindow = new QAction(tr("Start &GDB server..."), debuggingMenu);
+ connect(gdbWindow, SIGNAL(triggered()), this, SLOT(gdbOpen()));
+ debuggingMenu->addAction(gdbWindow);
+#endif
+
+ foreach (QAction* action, m_gameActions) {
+ action->setDisabled(true);
+ }
+}
+
+void Window::attachWidget(QWidget* widget) {
+ m_screenWidget->layout()->addWidget(widget);
+ static_cast(m_screenWidget->layout())->setCurrentWidget(widget);
+}
+
+void Window::detachWidget(QWidget* widget) {
+ m_screenWidget->layout()->removeWidget(widget);
+}
+
+WindowBackground::WindowBackground(QWidget* parent)
+ : QLabel(parent)
+{
+ setLayout(new QStackedLayout());
+ layout()->setContentsMargins(0, 0, 0, 0);
+ setAlignment(Qt::AlignCenter);
+ QPalette p = palette();
+ p.setColor(backgroundRole(), Qt::black);
+ setPalette(p);
+ setAutoFillBackground(true);
+}
+
+void WindowBackground::setSizeHint(const QSize& hint) {
+ m_sizeHint = hint;
+}
+
+QSize WindowBackground::sizeHint() const {
+ return m_sizeHint;
+}
diff --git a/src/platform/qt/Window.h b/src/platform/qt/Window.h
new file mode 100644
index 000000000..2582f5631
--- /dev/null
+++ b/src/platform/qt/Window.h
@@ -0,0 +1,107 @@
+#ifndef QGBA_WINDOW
+#define QGBA_WINDOW
+
+#include
+#include
+
+extern "C" {
+#include "gba.h"
+}
+
+#include "GDBController.h"
+#include "Display.h"
+#include "LoadSaveState.h"
+
+struct StartupOptions;
+
+namespace QGBA {
+
+class GameController;
+class LogView;
+class VideoView;
+class WindowBackground;
+
+class Window : public QMainWindow {
+Q_OBJECT
+
+public:
+ Window(QWidget* parent = nullptr);
+ virtual ~Window();
+
+ GameController* controller() { return m_controller; }
+
+ static GBAKey mapKey(int qtKey);
+
+ void optionsPassed(StartupOptions*);
+
+signals:
+ void startDrawing(const uint32_t*, GBAThread*);
+ void shutdown();
+ void audioBufferSamplesChanged(int samples);
+ void fpsTargetChanged(float target);
+
+public slots:
+ void selectROM();
+ void selectBIOS();
+ void selectPatch();
+ void toggleFullScreen();
+
+#ifdef USE_FFMPEG
+ void openVideoWindow();
+#endif
+
+#ifdef USE_GDB_STUB
+ void gdbOpen();
+#endif
+
+protected:
+ virtual void keyPressEvent(QKeyEvent* event) override;
+ virtual void keyReleaseEvent(QKeyEvent* event) override;
+ virtual void resizeEvent(QResizeEvent*) override;
+ virtual void closeEvent(QCloseEvent*) override;
+
+private slots:
+ void gameStarted(GBAThread*);
+ void gameStopped();
+ void redoLogo();
+
+private:
+ void setupMenu(QMenuBar*);
+ void openStateWindow(LoadSave);
+
+ void attachWidget(QWidget* widget);
+ void detachWidget(QWidget* widget);
+
+ GameController* m_controller;
+ Display* m_display;
+ QList m_gameActions;
+ LogView* m_logView;
+ LoadSaveState* m_stateWindow;
+ WindowBackground* m_screenWidget;
+ QPixmap m_logo;
+
+#ifdef USE_FFMPEG
+ VideoView* m_videoView;
+#endif
+
+#ifdef USE_GDB_STUB
+ GDBController* m_gdbController;
+#endif
+};
+
+class WindowBackground : public QLabel {
+Q_OBJECT
+
+public:
+ WindowBackground(QWidget* parent = 0);
+
+ void setSizeHint(const QSize& size);
+ virtual QSize sizeHint() const override;
+
+private:
+ QSize m_sizeHint;
+};
+
+}
+
+#endif
diff --git a/src/platform/qt/main.cpp b/src/platform/qt/main.cpp
new file mode 100644
index 000000000..8841fe4bb
--- /dev/null
+++ b/src/platform/qt/main.cpp
@@ -0,0 +1,7 @@
+#include "GBAApp.h"
+#include "Window.h"
+
+int main(int argc, char* argv[]) {
+ QGBA::GBAApp application(argc, argv);
+ return application.exec();
+}
diff --git a/src/platform/qt/resources.qrc b/src/platform/qt/resources.qrc
new file mode 100644
index 000000000..d90ba8844
--- /dev/null
+++ b/src/platform/qt/resources.qrc
@@ -0,0 +1,5 @@
+
+
+ ../../../res/mgba-1024.png
+
+
diff --git a/src/util/socket.h b/src/util/socket.h
index a84f9e13d..416d72682 100644
--- a/src/util/socket.h
+++ b/src/util/socket.h
@@ -48,10 +48,9 @@ static inline Socket SocketOpenTCP(int port, uint32_t bindAddress) {
struct sockaddr_in bindInfo = {
.sin_family = AF_INET,
.sin_port = htons(port),
- .sin_addr = {
- .s_addr = htonl(bindAddress)
- }
+ .sin_addr = { 0 }
};
+ bindInfo.sin_addr.s_addr = htonl(bindAddress);
int err = bind(sock, (const struct sockaddr*) &bindInfo, sizeof(struct sockaddr_in));
if (err) {
close(sock);
@@ -69,10 +68,9 @@ static inline Socket SocketConnectTCP(int port, uint32_t destinationAddress) {
struct sockaddr_in bindInfo = {
.sin_family = AF_INET,
.sin_port = htons(port),
- .sin_addr = {
- .s_addr = htonl(destinationAddress)
- }
+ .sin_addr = { 0 }
};
+ bindInfo.sin_addr.s_addr = htonl(destinationAddress);
int err = connect(sock, (const struct sockaddr*) &bindInfo, sizeof(struct sockaddr_in));
if (err) {
close(sock);