Merge branch 'port/qt'
jump to
@@ -7,6 +7,7 @@ set(USE_GDB_STUB ON CACHE BOOL "Whether or not to enable the GDB stub ARM debugger")
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,7 +161,12 @@ 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)@@ -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}")
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>English</string> + <key>CFBundleExecutable</key> + <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string> + <key>CFBundleGetInfoString</key> + <string>${MACOSX_BUNDLE_INFO_STRING}</string> + <key>CFBundleIconFile</key> + <string>${MACOSX_BUNDLE_ICON_FILE}</string> + <key>CFBundleIdentifier</key> + <string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string> + <key>CSResourcesFileMapped</key> + <true/> + <key>NSHighResolutionCapable</key> + <true/> + <key>NSHumanReadableCopyright</key> + <string>${MACOSX_BUNDLE_COPYRIGHT}</string> + <key>CFBundleDocumentTypes</key> + <array> + <dict> + <key>CFBundleTypeExtensions</key> + <array> + <string>gba</string> + </array> + <key>CFBundleTypeName</key> + <string>Game Boy Advance ROM Image</string> + <key>CFBundleTypeRole</key> + <string>Viewer</string> + </dict> + </array> +</dict> +</plist>
@@ -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<GBAStereoSample*>(data), maxSize / sizeof(GBAStereoSample)) * sizeof(GBAStereoSample); +} + +qint64 AudioDevice::writeData(const char*, qint64) { + return 0; +}
@@ -0,0 +1,31 @@
+#ifndef QGBA_AUDIO_DEVICE +#define QGBA_AUDIO_DEVICE +#include <QAudioFormat> +#include <QIODevice> + +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
@@ -0,0 +1,67 @@
+#include "AudioProcessor.h" + +#include "AudioDevice.h" + +#include <QAudioOutput> + +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()); +}
@@ -0,0 +1,36 @@
+#ifndef QGBA_AUDIO_PROCESSOR +#define QGBA_AUDIO_PROCESSOR +#include <QObject> + +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
@@ -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)
@@ -0,0 +1,176 @@
+#include "Display.h" + +#include <QApplication> +#include <QResizeEvent> + +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()); +}
@@ -0,0 +1,65 @@
+#ifndef QGBA_DISPLAY +#define QGBA_DISPLAY + +#include <QGLWidget> +#include <QThread> +#include <QTimer> + +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
@@ -0,0 +1,32 @@
+#include "GBAApp.h" + +#include "GameController.h" + +#include <QFileOpenEvent> + +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<QFileOpenEvent*>(event)->file()); + return true; + } + return QApplication::event(event); +}
@@ -0,0 +1,34 @@
+#ifndef QGBA_APP_H +#define QGBA_APP_H + +#include <QApplication> + +#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
@@ -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); +}
@@ -0,0 +1,44 @@
+#ifndef QGBA_GDB_CONTROLLER +#define QGBA_GDB_CONTROLLER + +#include <QObject> + +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
@@ -0,0 +1,99 @@
+#include "GDBWindow.h" + +#include <QGridLayout> +#include <QGroupBox> +#include <QLabel> +#include <QLineEdit> +#include <QPushButton> +#include <QVBoxLayout> + +#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())); +}
@@ -0,0 +1,36 @@
+#ifndef QGBA_GDB_WINDOW +#define QGBA_GDB_WINDOW + +#include <QWidget> + +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
@@ -0,0 +1,376 @@
+#include "GameController.h" + +#include "AudioProcessor.h" + +#include <QThread> + +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<GameController*>(context->userData); + controller->m_audioProcessor->setInput(context); + controller->gameStarted(context); + }; + + m_threadContext.cleanCallback = [] (GBAThread* context) { + GameController* controller = static_cast<GameController*>(context->userData); + controller->gameStopped(context); + }; + + m_threadContext.frameCallback = [] (GBAThread* context) { + GameController* controller = static_cast<GameController*>(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<GameController*>(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<char*>(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
@@ -0,0 +1,117 @@
+#ifndef QGBA_GAME_CONTROLLER +#define QGBA_GAME_CONTROLLER + +#include <QFile> +#include <QImage> +#include <QObject> +#include <QMutex> +#include <QString> + +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
@@ -0,0 +1,149 @@
+#include "LoadSaveState.h" + +#include "GameController.h" +#include "VFileDevice.h" + +#include <QKeyEvent> +#include <QPainter> + +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<QKeyEvent*>(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<QKeyEvent*>(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)); +}
@@ -0,0 +1,52 @@
+#ifndef QGBA_LOAD_SAVE_STATE +#define QGBA_LOAD_SAVE_STATE + +#include <QWidget> + +#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
@@ -0,0 +1,328 @@
+<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>LoadSaveState</class> + <widget class="QWidget" name="LoadSaveState"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>760</width> + <height>560</height> + </rect> + </property> + <property name="windowTitle"> + <string>%1 State</string> + </property> + <layout class="QGridLayout" name="gridLayout_2" rowstretch="1,0,0,0" columnstretch="0,0,0"> + <property name="leftMargin"> + <number>6</number> + </property> + <property name="topMargin"> + <number>6</number> + </property> + <property name="rightMargin"> + <number>6</number> + </property> + <property name="bottomMargin"> + <number>6</number> + </property> + <property name="spacing"> + <number>2</number> + </property> + <item row="1" column="0"> + <widget class="QGBA::SavestateButton" name="state1"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>242</width> + <height>162</height> + </size> + </property> + <property name="text"> + <string>No Save</string> + </property> + <property name="iconSize"> + <size> + <width>240</width> + <height>160</height> + </size> + </property> + <property name="shortcut"> + <string>1</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QGBA::SavestateButton" name="state2"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>242</width> + <height>162</height> + </size> + </property> + <property name="text"> + <string>No Save</string> + </property> + <property name="iconSize"> + <size> + <width>240</width> + <height>160</height> + </size> + </property> + <property name="shortcut"> + <string>2</string> + </property> + </widget> + </item> + <item row="0" column="0" colspan="3"> + <widget class="QLabel" name="lsLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="styleSheet"> + <string notr="true">font-size: 30pt; font-weight: bold; color: white;</string> + </property> + <property name="text"> + <string>%1 State</string> + </property> + <property name="scaledContents"> + <bool>false</bool> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QGBA::SavestateButton" name="state3"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>242</width> + <height>162</height> + </size> + </property> + <property name="text"> + <string>No Save</string> + </property> + <property name="iconSize"> + <size> + <width>240</width> + <height>160</height> + </size> + </property> + <property name="shortcut"> + <string>3</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QGBA::SavestateButton" name="state4"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>242</width> + <height>162</height> + </size> + </property> + <property name="text"> + <string>No Save</string> + </property> + <property name="iconSize"> + <size> + <width>240</width> + <height>160</height> + </size> + </property> + <property name="shortcut"> + <string>4</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QGBA::SavestateButton" name="state5"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>242</width> + <height>162</height> + </size> + </property> + <property name="text"> + <string>No Save</string> + </property> + <property name="iconSize"> + <size> + <width>240</width> + <height>160</height> + </size> + </property> + <property name="shortcut"> + <string>5</string> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QGBA::SavestateButton" name="state6"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>242</width> + <height>162</height> + </size> + </property> + <property name="text"> + <string>No Save</string> + </property> + <property name="iconSize"> + <size> + <width>240</width> + <height>160</height> + </size> + </property> + <property name="shortcut"> + <string>6</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QGBA::SavestateButton" name="state7"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>242</width> + <height>162</height> + </size> + </property> + <property name="text"> + <string>No Save</string> + </property> + <property name="iconSize"> + <size> + <width>240</width> + <height>160</height> + </size> + </property> + <property name="shortcut"> + <string>7</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QGBA::SavestateButton" name="state8"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>242</width> + <height>162</height> + </size> + </property> + <property name="text"> + <string>No Save</string> + </property> + <property name="iconSize"> + <size> + <width>240</width> + <height>160</height> + </size> + </property> + <property name="shortcut"> + <string>8</string> + </property> + </widget> + </item> + <item row="3" column="2"> + <widget class="QGBA::SavestateButton" name="state9"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>242</width> + <height>162</height> + </size> + </property> + <property name="text"> + <string>No Save</string> + </property> + <property name="iconSize"> + <size> + <width>240</width> + <height>160</height> + </size> + </property> + <property name="shortcut"> + <string>9</string> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>QGBA::SavestateButton</class> + <extends>QPushButton</extends> + <header>SavestateButton.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>state1</tabstop> + <tabstop>state2</tabstop> + <tabstop>state3</tabstop> + <tabstop>state4</tabstop> + <tabstop>state5</tabstop> + <tabstop>state6</tabstop> + <tabstop>state7</tabstop> + <tabstop>state8</tabstop> + <tabstop>state9</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
@@ -0,0 +1,144 @@
+#include "LogView.h" + +#include <QTextBlock> +#include <QTextCursor> + +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; +}
@@ -0,0 +1,52 @@
+#ifndef QGBA_LOG_VIEW +#define QGBA_LOG_VIEW + +#include <QWidget> + +#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
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>LogView</class> + <widget class="QWidget" name="LogView"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>600</width> + <height>400</height> + </rect> + </property> + <property name="windowTitle"> + <string>Logs</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Enabled Levels</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QCheckBox" name="levelDebug"> + <property name="text"> + <string>Debug</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="levelStub"> + <property name="text"> + <string>Stub</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="levelInfo"> + <property name="text"> + <string>Info</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="levelWarn"> + <property name="text"> + <string>Warning</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="levelError"> + <property name="text"> + <string>Error</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="levelFatal"> + <property name="text"> + <string>Fatal</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="levelGameError"> + <property name="text"> + <string>Game Error</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="clear"> + <property name="text"> + <string>Clear</string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Max Lines</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="maxLines"> + <property name="maximum"> + <number>9999</number> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <widget class="QPlainTextEdit" name="view"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui>
@@ -0,0 +1,41 @@
+#include "SavestateButton.h" + +#include <QApplication> +#include <QPainter> + +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()); +}
@@ -0,0 +1,18 @@
+#ifndef QGBA_SAVESTATE_BUTTON +#define QGBA_SAVESTATE_BUTTON + +#include <QAbstractButton> + +namespace QGBA { + +class SavestateButton : public QAbstractButton { +public: + SavestateButton(QWidget* parent = nullptr); + +protected: + virtual void paintEvent(QPaintEvent*) override; +}; + +} + +#endif
@@ -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; +}
@@ -0,0 +1,27 @@
+#ifndef QGBA_VFILE_DEVICE +#define QGBA_VFILE_DEVICE + +#include <QFileDevice> + +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
@@ -0,0 +1,185 @@
+#include "VideoView.h" + +#ifdef USE_FFMPEG + +#include <QFileDialog> +#include <QMap> + +using namespace QGBA; + +QMap<QString, QString> VideoView::s_acodecMap; +QMap<QString, QString> VideoView::s_vcodecMap; +QMap<QString, QString> 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
@@ -0,0 +1,71 @@
+#ifndef QGBA_VIDEO_VIEW +#define QGBA_VIDEO_VIEW + +#ifdef USE_FFMPEG + +#include <QWidget> + +#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<QString, QString> s_acodecMap; + static QMap<QString, QString> s_vcodecMap; + static QMap<QString, QString> s_containerMap; +}; + +} + +#endif + +#endif
@@ -0,0 +1,256 @@
+<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>VideoView</class> + <widget class="QWidget" name="VideoView"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>462</width> + <height>194</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>Record Video</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <property name="sizeConstraint"> + <enum>QLayout::SetFixedSize</enum> + </property> + <item row="0" column="0" rowspan="2"> + <widget class="QGroupBox" name="groupBox_4"> + <property name="title"> + <string>Format</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <widget class="QComboBox" name="container"> + <property name="editable"> + <bool>true</bool> + </property> + <item> + <property name="text"> + <string>MKV</string> + </property> + </item> + <item> + <property name="text"> + <string>AVI</string> + </property> + </item> + <item> + <property name="text"> + <string>MP4</string> + </property> + </item> + </widget> + </item> + <item> + <widget class="QComboBox" name="video"> + <property name="editable"> + <bool>true</bool> + </property> + <item> + <property name="text"> + <string>PNG</string> + </property> + </item> + <item> + <property name="text"> + <string>h.264</string> + </property> + </item> + <item> + <property name="text"> + <string>FFV1</string> + </property> + </item> + </widget> + </item> + <item> + <widget class="QComboBox" name="audio"> + <property name="editable"> + <bool>true</bool> + </property> + <item> + <property name="text"> + <string>FLAC</string> + </property> + </item> + <item> + <property name="text"> + <string>Vorbis</string> + </property> + </item> + <item> + <property name="text"> + <string>MP3</string> + </property> + </item> + <item> + <property name="text"> + <string>AAC</string> + </property> + </item> + <item> + <property name="text"> + <string>Uncompressed</string> + </property> + </item> + </widget> + </item> + </layout> + </widget> + </item> + <item row="1" column="1"> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string> Bitrate</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Video</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="buddy"> + <cstring>vbr</cstring> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="vbr"> + <property name="suffix"> + <string/> + </property> + <property name="minimum"> + <number>200</number> + </property> + <property name="maximum"> + <number>2000</number> + </property> + <property name="value"> + <number>400</number> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Audio</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="buddy"> + <cstring>abr</cstring> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="abr"> + <property name="minimum"> + <number>16</number> + </property> + <property name="maximum"> + <number>320</number> + </property> + <property name="value"> + <number>192</number> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="0" column="1"> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="0"> + <widget class="QPushButton" name="start"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Start</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QPushButton" name="stop"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Stop</string> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QPushButton" name="selectFile"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Select File</string> + </property> + </widget> + </item> + <item row="0" column="0" colspan="3"> + <widget class="QLineEdit" name="filename"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="0" colspan="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>filename</tabstop> + <tabstop>start</tabstop> + <tabstop>stop</tabstop> + <tabstop>selectFile</tabstop> + <tabstop>container</tabstop> + <tabstop>video</tabstop> + <tabstop>audio</tabstop> + <tabstop>vbr</tabstop> + <tabstop>abr</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
@@ -0,0 +1,508 @@
+#include "Window.h" + +#include <QFileDialog> +#include <QKeyEvent> +#include <QKeySequence> +#include <QMenuBar> +#include <QStackedLayout> + +#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<const uchar*>(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<QStackedLayout*>(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; +}
@@ -0,0 +1,107 @@
+#ifndef QGBA_WINDOW +#define QGBA_WINDOW + +#include <QAudioOutput> +#include <QMainWindow> + +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<QAction*> 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
@@ -0,0 +1,7 @@
+#include "GBAApp.h" +#include "Window.h" + +int main(int argc, char* argv[]) { + QGBA::GBAApp application(argc, argv); + return application.exec(); +}
@@ -0,0 +1,5 @@
+ <!DOCTYPE RCC><RCC version="1.0"> + <qresource> + <file>../../../res/mgba-1024.png</file> + </qresource> + </RCC>
@@ -48,10 +48,9 @@
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 @@
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);