mirror of
https://github.com/libretro/mgba.git
synced 2024-11-24 08:30:30 +00:00
Merge branch 'port/qt'
This commit is contained in:
commit
aed170b670
@ -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}")
|
||||
|
45
res/info.plist.in
Normal file
45
res/info.plist.in
Normal file
@ -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>
|
BIN
res/mgba-1024.png
Normal file
BIN
res/mgba-1024.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 525 KiB |
BIN
res/mgba.icns
Normal file
BIN
res/mgba.icns
Normal file
Binary file not shown.
BIN
res/mgba.ico
Normal file
BIN
res/mgba.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 279 KiB |
1
res/mgba.rc
Normal file
1
res/mgba.rc
Normal file
@ -0,0 +1 @@
|
||||
IDI_ICON1 ICON DISCARDABLE "mgba.ico"
|
46
src/platform/qt/AudioDevice.cpp
Normal file
46
src/platform/qt/AudioDevice.cpp
Normal file
@ -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;
|
||||
}
|
31
src/platform/qt/AudioDevice.h
Normal file
31
src/platform/qt/AudioDevice.h
Normal file
@ -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
|
67
src/platform/qt/AudioProcessor.cpp
Normal file
67
src/platform/qt/AudioProcessor.cpp
Normal file
@ -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());
|
||||
}
|
36
src/platform/qt/AudioProcessor.h
Normal file
36
src/platform/qt/AudioProcessor.h
Normal file
@ -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
|
66
src/platform/qt/CMakeLists.txt
Normal file
66
src/platform/qt/CMakeLists.txt
Normal file
@ -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)
|
176
src/platform/qt/Display.cpp
Normal file
176
src/platform/qt/Display.cpp
Normal file
@ -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());
|
||||
}
|
65
src/platform/qt/Display.h
Normal file
65
src/platform/qt/Display.h
Normal file
@ -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
|
32
src/platform/qt/GBAApp.cpp
Normal file
32
src/platform/qt/GBAApp.cpp
Normal file
@ -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);
|
||||
}
|
34
src/platform/qt/GBAApp.h
Normal file
34
src/platform/qt/GBAApp.h
Normal file
@ -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
|
71
src/platform/qt/GDBController.cpp
Normal file
71
src/platform/qt/GDBController.cpp
Normal file
@ -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);
|
||||
}
|
44
src/platform/qt/GDBController.h
Normal file
44
src/platform/qt/GDBController.h
Normal file
@ -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
|
99
src/platform/qt/GDBWindow.cpp
Normal file
99
src/platform/qt/GDBWindow.cpp
Normal file
@ -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()));
|
||||
}
|
36
src/platform/qt/GDBWindow.h
Normal file
36
src/platform/qt/GDBWindow.h
Normal file
@ -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
|
376
src/platform/qt/GameController.cpp
Normal file
376
src/platform/qt/GameController.cpp
Normal file
@ -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
|
117
src/platform/qt/GameController.h
Normal file
117
src/platform/qt/GameController.h
Normal file
@ -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
|
149
src/platform/qt/LoadSaveState.cpp
Normal file
149
src/platform/qt/LoadSaveState.cpp
Normal file
@ -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));
|
||||
}
|
52
src/platform/qt/LoadSaveState.h
Normal file
52
src/platform/qt/LoadSaveState.h
Normal file
@ -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
|
328
src/platform/qt/LoadSaveState.ui
Normal file
328
src/platform/qt/LoadSaveState.ui
Normal file
@ -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>
|
144
src/platform/qt/LogView.cpp
Normal file
144
src/platform/qt/LogView.cpp
Normal file
@ -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;
|
||||
}
|
52
src/platform/qt/LogView.h
Normal file
52
src/platform/qt/LogView.h
Normal file
@ -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
|
156
src/platform/qt/LogView.ui
Normal file
156
src/platform/qt/LogView.ui
Normal file
@ -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>
|
41
src/platform/qt/SavestateButton.cpp
Normal file
41
src/platform/qt/SavestateButton.cpp
Normal file
@ -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());
|
||||
}
|
18
src/platform/qt/SavestateButton.h
Normal file
18
src/platform/qt/SavestateButton.h
Normal file
@ -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
|
30
src/platform/qt/VFileDevice.cpp
Normal file
30
src/platform/qt/VFileDevice.cpp
Normal file
@ -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;
|
||||
}
|
27
src/platform/qt/VFileDevice.h
Normal file
27
src/platform/qt/VFileDevice.h
Normal file
@ -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
|
185
src/platform/qt/VideoView.cpp
Normal file
185
src/platform/qt/VideoView.cpp
Normal file
@ -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
|
71
src/platform/qt/VideoView.h
Normal file
71
src/platform/qt/VideoView.h
Normal file
@ -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
|
256
src/platform/qt/VideoView.ui
Normal file
256
src/platform/qt/VideoView.ui
Normal file
@ -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>
|
508
src/platform/qt/Window.cpp
Normal file
508
src/platform/qt/Window.cpp
Normal file
@ -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;
|
||||
}
|
107
src/platform/qt/Window.h
Normal file
107
src/platform/qt/Window.h
Normal file
@ -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
|
7
src/platform/qt/main.cpp
Normal file
7
src/platform/qt/main.cpp
Normal file
@ -0,0 +1,7 @@
|
||||
#include "GBAApp.h"
|
||||
#include "Window.h"
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
QGBA::GBAApp application(argc, argv);
|
||||
return application.exec();
|
||||
}
|
5
src/platform/qt/resources.qrc
Normal file
5
src/platform/qt/resources.qrc
Normal file
@ -0,0 +1,5 @@
|
||||
<!DOCTYPE RCC><RCC version="1.0">
|
||||
<qresource>
|
||||
<file>../../../res/mgba-1024.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user