all repos — mgba @ e6fd4f5e7ad18af845308b3d6a2f4ffb404545c9

mGBA Game Boy Advance Emulator

Merge branch 'master' into medusa
Vicki Pfau vi@endrift.com
Thu, 23 Jul 2020 22:43:01 -0700
commit

e6fd4f5e7ad18af845308b3d6a2f4ffb404545c9

parent

614857520f7877d258d2b14dad47f22daa1aa9ac

M CHANGESCHANGES

@@ -29,7 +29,7 @@

0.9.0: (Future) Features: - e-Reader card scanning - - Add APNG recording + - Add WebP and APNG recording - Support for unlicensed Pokemon Jade/Diamond Game Boy mapper Emulation fixes: - ARM: Fix ALU reading PC after shifting
M README.mdREADME.md

@@ -28,7 +28,7 @@ - Frameskip, configurable up to 10.

- Screenshot support. - Cheat code support[<sup>[2]</sup>](#dscaveat). - 9 savestate slots. Savestates are also viewable as screenshots[<sup>[2]</sup>](#dscaveat). -- Video, GIF and APNG recording. +- Video, GIF, WebP, and APNG recording. - e-Reader support. - Remappable controls for both keyboards and gamepads. - Loading from ZIP and 7z files.

@@ -240,7 +240,7 @@ - Qt 5: for the GUI frontend. Qt Multimedia or SDL are required for audio.

- SDL: for a more basic frontend and gamepad support in the Qt frontend. SDL 2 is recommended, but 1.2 is supported. - zlib and libpng: for screenshot support and savestate-in-PNG support. - libedit: for command-line debugger support. -- ffmpeg or libav: for video and GIF recording. +- ffmpeg or libav: for video, GIF, WebP, and APNG recording. - libzip or zlib: for loading ROMs stored in zip files. - SQLite3: for game databases. - libelf: for ELF loading.
M include/mgba-util/math.hinclude/mgba-util/math.h

@@ -106,6 +106,19 @@ unsigned lz = clz32(bits - 1);

return 1 << (32 - lz); } +static inline int reduceFraction(int* num, int* den) { + int n = *num; + int d = *den; + while (d != 0) { + int temp = n % d; + n = d; + d = temp; + } + *num /= n; + *den /= n; + return n; +} + CXX_GUARD_END #endif
M src/feature/ffmpeg/ffmpeg-encoder.csrc/feature/ffmpeg/ffmpeg-encoder.c

@@ -7,6 +7,7 @@ #include "ffmpeg-encoder.h"

#include <mgba/core/core.h> #include <mgba/gba/interface.h> +#include <mgba-util/math.h> #include <libavcodec/version.h> #include <libavcodec/avcodec.h>

@@ -96,6 +97,7 @@ encoder->graph = NULL;

encoder->source = NULL; encoder->sink = NULL; encoder->sinkFrame = NULL; + FFmpegEncoderSetInputFrameRate(encoder, VIDEO_TOTAL_LENGTH, GBA_ARM7TDMI_FREQUENCY); int i; for (i = 0; i < FFMPEG_FILTERS_MAX; ++i) {

@@ -182,10 +184,12 @@ { AV_PIX_FMT_RGB0, 3 },

{ AV_PIX_FMT_0BGR, 3 }, { AV_PIX_FMT_0RGB, 3 }, #endif - { AV_PIX_FMT_YUV422P, 4 }, + { AV_PIX_FMT_RGB32, 4}, + { AV_PIX_FMT_BGR32, 4}, { AV_PIX_FMT_YUV444P, 5 }, - { AV_PIX_FMT_YUV420P, 6 }, - { AV_PIX_FMT_PAL8, 7 }, + { AV_PIX_FMT_YUV422P, 6 }, + { AV_PIX_FMT_YUV420P, 7 }, + { AV_PIX_FMT_PAL8, 8 }, }; if (!vcodec) {

@@ -412,6 +416,10 @@ }

if (strcmp(vcodec->name, "libvpx-vp9") == 0 && encoder->videoBitrate == 0) { av_opt_set(encoder->video->priv_data, "lossless", "1", 0); encoder->video->pix_fmt = AV_PIX_FMT_YUV444P; + } + if (strcmp(vcodec->name, "libwebp_anim") == 0 && encoder->videoBitrate == 0) { + av_opt_set(encoder->video->priv_data, "lossless", "1", 0); + encoder->video->pix_fmt = AV_PIX_FMT_RGB32; } if (encoder->pixFormat == AV_PIX_FMT_PAL8) {

@@ -489,6 +497,8 @@ if (strcmp(encoder->containerFormat, "gif") == 0) {

av_opt_set(encoder->context->priv_data, "loop", encoder->loop ? "0" : "-1", 0); } else if (strcmp(encoder->containerFormat, "apng") == 0) { av_opt_set(encoder->context->priv_data, "plays", encoder->loop ? "0" : "1", 0); + } else if (strcmp(encoder->containerFormat, "webp") == 0) { + av_opt_set(encoder->context->priv_data, "loop", encoder->loop ? "0" : "1", 0); } AVDictionary* opts = 0;

@@ -757,7 +767,12 @@

#if LIBAVCODEC_VERSION_MAJOR >= 55 av_frame_make_writable(encoder->videoFrame); #endif - encoder->videoFrame->pts = av_rescale_q(encoder->currentVideoFrame, encoder->video->time_base, encoder->videoStream->time_base); + if (encoder->video->codec->id == AV_CODEC_ID_WEBP) { + // TODO: Figure out why WebP is rescaling internally (should video frames not be rescaled externally?) + encoder->videoFrame->pts = encoder->currentVideoFrame; + } else { + encoder->videoFrame->pts = av_rescale_q(encoder->currentVideoFrame, encoder->video->time_base, encoder->videoStream->time_base); + } ++encoder->currentVideoFrame; sws_scale(encoder->scaleContext, (const uint8_t* const*) &pixels, (const int*) &stride, 0, encoder->iheight, encoder->videoFrame->data, encoder->videoFrame->linesize);

@@ -831,10 +846,11 @@ struct FFmpegEncoder* encoder = (struct FFmpegEncoder*) stream;

FFmpegEncoderSetInputFrameRate(encoder, numerator, denominator); } -void FFmpegEncoderSetInputFrameRate(struct FFmpegEncoder* encoder, unsigned numerator, unsigned denominator) { +void FFmpegEncoderSetInputFrameRate(struct FFmpegEncoder* encoder, int numerator, int denominator) { + reduceFraction(&numerator, &denominator); encoder->frameCycles = numerator; encoder->cycles = denominator; if (encoder->video) { - encoder->video->time_base = (AVRational) { numerator, denominator }; + encoder->video->framerate = (AVRational) { denominator, numerator * encoder->frameskip }; } }
M src/feature/ffmpeg/ffmpeg-encoder.hsrc/feature/ffmpeg/ffmpeg-encoder.h

@@ -78,8 +78,8 @@ int width;

int height; int iwidth; int iheight; - unsigned frameCycles; - unsigned cycles; + int frameCycles; + int cycles; int frameskip; int skipResidue; bool loop;

@@ -99,13 +99,12 @@ bool FFmpegEncoderSetAudio(struct FFmpegEncoder*, const char* acodec, unsigned abr);

bool FFmpegEncoderSetVideo(struct FFmpegEncoder*, const char* vcodec, unsigned vbr, int frameskip); bool FFmpegEncoderSetContainer(struct FFmpegEncoder*, const char* container); void FFmpegEncoderSetDimensions(struct FFmpegEncoder*, int width, int height); +void FFmpegEncoderSetInputFrameRate(struct FFmpegEncoder*, int numerator, int denominator); void FFmpegEncoderSetLooping(struct FFmpegEncoder*, bool loop); bool FFmpegEncoderVerifyContainer(struct FFmpegEncoder*); bool FFmpegEncoderOpen(struct FFmpegEncoder*, const char* outfile); void FFmpegEncoderClose(struct FFmpegEncoder*); bool FFmpegEncoderIsOpen(struct FFmpegEncoder*); - -void FFmpegEncoderSetInputFrameRate(struct FFmpegEncoder*, unsigned numerator, unsigned denominator); CXX_GUARD_END
M src/platform/qt/GIFView.cppsrc/platform/qt/GIFView.cpp

@@ -27,6 +27,7 @@ connect(m_ui.selectFile, &QAbstractButton::clicked, this, &GIFView::selectFile);

connect(m_ui.filename, &QLineEdit::textChanged, this, &GIFView::setFilename); connect(m_ui.fmtGif, &QAbstractButton::clicked, this, &GIFView::changeExtension); connect(m_ui.fmtApng, &QAbstractButton::clicked, this, &GIFView::changeExtension); + connect(m_ui.fmtWebP, &QAbstractButton::clicked, this, &GIFView::changeExtension); FFmpegEncoderInit(&m_encoder); FFmpegEncoderSetAudio(&m_encoder, nullptr, 0);

@@ -45,7 +46,10 @@ FFmpegEncoderSetDimensions(&m_encoder, size.width(), size.height());

} void GIFView::startRecording() { - if (m_ui.fmtApng->isChecked()) { + if (m_ui.fmtWebP->isChecked()) { + FFmpegEncoderSetContainer(&m_encoder, "webp"); + FFmpegEncoderSetVideo(&m_encoder, "libwebp_anim", 0, m_ui.frameskip->value()); + } else if (m_ui.fmtApng->isChecked()) { FFmpegEncoderSetContainer(&m_encoder, "apng"); FFmpegEncoderSetVideo(&m_encoder, "apng", 0, m_ui.frameskip->value()); } else {

@@ -54,15 +58,17 @@ FFmpegEncoderSetVideo(&m_encoder, "gif", 0, m_ui.frameskip->value());

} FFmpegEncoderSetLooping(&m_encoder, m_ui.loop->isChecked()); if (!FFmpegEncoderOpen(&m_encoder, m_filename.toUtf8().constData())) { - LOG(QT, ERROR) << tr("Failed to open output GIF or APNG file: %1").arg(m_filename); + LOG(QT, ERROR) << tr("Failed to open output file: %1").arg(m_filename); return; } m_ui.start->setEnabled(false); m_ui.stop->setEnabled(true); m_ui.frameskip->setEnabled(false); m_ui.loop->setEnabled(false); + m_ui.fmtWebP->setEnabled(false); m_ui.fmtApng->setEnabled(false); m_ui.fmtGif->setEnabled(false); + m_ui.fmtWebP->setEnabled(false); emit recordingStarted(&m_encoder.d); }

@@ -73,12 +79,13 @@ m_ui.stop->setEnabled(false);

m_ui.start->setEnabled(!m_filename.isEmpty()); m_ui.frameskip->setEnabled(true); m_ui.loop->setEnabled(true); + m_ui.fmtWebP->setEnabled(true); m_ui.fmtApng->setEnabled(true); m_ui.fmtGif->setEnabled(true); } void GIFView::selectFile() { - QString filename = GBAApp::app()->getSaveFileName(this, tr("Select output file"), tr("Graphics Interchange Format (*.gif);;Animated Portable Network Graphics (*.png *.apng)")); + QString filename = GBAApp::app()->getSaveFileName(this, tr("Select output file"), tr("Graphics Interchange Format (*.gif);;Animated Portable Network Graphics (*.png *.webp *.apng)")); m_ui.filename->setText(filename); }

@@ -90,6 +97,8 @@ if (filename.endsWith(".gif")) {

m_ui.fmtGif->setChecked(Qt::Checked); } else if (filename.endsWith(".png") || filename.endsWith(".apng")) { m_ui.fmtApng->setChecked(Qt::Checked); + } else if (filename.endsWith(".webp")) { + m_ui.fmtWebP->setChecked(Qt::Checked); } } }

@@ -105,6 +114,8 @@ filename.truncate(index);

} if (m_ui.fmtGif->isChecked()) { filename += ".gif"; + } else if (m_ui.fmtWebP->isChecked()) { + filename += ".webp"; } else if (m_ui.fmtApng->isChecked()) { filename += ".png"; }
M src/platform/qt/GIFView.uisrc/platform/qt/GIFView.ui

@@ -7,7 +7,7 @@ <rect>

<x>0</x> <y>0</y> <width>392</width> - <height>262</height> + <height>225</height> </rect> </property> <property name="windowTitle">

@@ -17,29 +17,20 @@ <layout class="QGridLayout" name="gridLayout_3">

<property name="sizeConstraint"> <enum>QLayout::SetFixedSize</enum> </property> - <item row="2" column="0"> - <widget class="QRadioButton" name="fmtApng"> - <property name="text"> - <string>APNG</string> + <item row="4" column="0" colspan="3"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> </property> - <attribute name="buttonGroup"> - <string notr="true">format</string> - </attribute> </widget> </item> - <item row="2" column="2"> - <widget class="QSpinBox" name="frameskip"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> + <item row="1" column="1" colspan="2"> + <widget class="QCheckBox" name="loop"> + <property name="text"> + <string>Loop</string> </property> - <property name="maximum"> - <number>9</number> - </property> - <property name="value"> - <number>2</number> + <property name="checked"> + <bool>true</bool> </property> </widget> </item>

@@ -115,18 +106,14 @@ </widget>

</item> </layout> </item> - <item row="2" column="1"> - <widget class="QLabel" name="label"> + <item row="3" column="0"> + <widget class="QRadioButton" name="fmtApng"> <property name="text"> - <string>Frameskip</string> + <string>APNG</string> </property> - </widget> - </item> - <item row="3" column="0" colspan="3"> - <widget class="QDialogButtonBox" name="buttonBox"> - <property name="standardButtons"> - <set>QDialogButtonBox::Close</set> - </property> + <attribute name="buttonGroup"> + <string notr="true">format</string> + </attribute> </widget> </item> <item row="1" column="0">

@@ -142,13 +129,36 @@ <string notr="true">format</string>

</attribute> </widget> </item> - <item row="1" column="1" colspan="2"> - <widget class="QCheckBox" name="loop"> + <item row="2" column="0"> + <widget class="QRadioButton" name="fmtWebP"> <property name="text"> - <string>Loop</string> + <string>WebP</string> </property> - <property name="checked"> - <bool>true</bool> + <attribute name="buttonGroup"> + <string notr="true">format</string> + </attribute> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Frameskip</string> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QSpinBox" name="frameskip"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximum"> + <number>9</number> + </property> + <property name="value"> + <number>2</number> </property> </widget> </item>

@@ -160,6 +170,7 @@ <tabstop>start</tabstop>

<tabstop>stop</tabstop> <tabstop>selectFile</tabstop> <tabstop>fmtGif</tabstop> + <tabstop>fmtWebP</tabstop> <tabstop>fmtApng</tabstop> <tabstop>loop</tabstop> <tabstop>frameskip</tabstop>
M src/platform/qt/VideoView.cppsrc/platform/qt/VideoView.cpp

@@ -10,6 +10,8 @@

#include "GBAApp.h" #include "LogController.h" +#include <mgba-util/math.h> + #include <QMap> using namespace QGBA;

@@ -400,15 +402,7 @@ safelySet(m_ui.height, m_height);

} else { int w = m_width; int h = m_height; - // Get greatest common divisor - while (w != 0) { - int temp = h % w; - h = w; - w = temp; - } - int gcd = h; - w = m_width / gcd; - h = m_height / gcd; + reduceFraction(&h, &w); safelySet(m_ui.wratio, w); safelySet(m_ui.hratio, h); }
M src/platform/qt/Window.cppsrc/platform/qt/Window.cpp

@@ -1567,7 +1567,7 @@ #endif

#ifdef USE_FFMPEG addGameAction(tr("Record A/V..."), "recordOutput", this, &Window::openVideoWindow, "av"); - addGameAction(tr("Record GIF/APNG..."), "recordGIF", this, &Window::openGIFWindow, "av"); + addGameAction(tr("Record GIF/WebP/APNG..."), "recordGIF", this, &Window::openGIFWindow, "av"); #endif m_actions.addSeparator("av");