mirror of https://github.com/mgba-emu/mgba.git
Merge branch 'master' into medusa
This commit is contained in:
commit
e6fd4f5e7a
2
CHANGES
2
CHANGES
|
@ -29,7 +29,7 @@ Misc:
|
|||
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
|
||||
|
|
|
@ -28,7 +28,7 @@ Features
|
|||
- 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 @@ medusa has no hard dependencies, however, the following optional dependencies ar
|
|||
- 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.
|
||||
|
|
|
@ -106,6 +106,19 @@ static inline uint32_t toPow2(uint32_t bits) {
|
|||
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
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
#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 @@ void FFmpegEncoderInit(struct FFmpegEncoder* encoder) {
|
|||
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 @@ bool FFmpegEncoderSetVideo(struct FFmpegEncoder* encoder, const char* vcodec, un
|
|||
{ 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) {
|
||||
|
@ -413,6 +417,10 @@ bool FFmpegEncoderOpen(struct FFmpegEncoder* encoder, const char* outfile) {
|
|||
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) {
|
||||
encoder->graph = avfilter_graph_alloc();
|
||||
|
@ -489,6 +497,8 @@ bool FFmpegEncoderOpen(struct FFmpegEncoder* encoder, const char* outfile) {
|
|||
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 @@ void _ffmpegPostVideoFrame(struct mAVStream* stream, const color_t* pixels, size
|
|||
#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 @@ static void _ffmpegSetVideoFrameRate(struct mAVStream* stream, unsigned numerato
|
|||
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 };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,8 +78,8 @@ struct FFmpegEncoder {
|
|||
int height;
|
||||
int iwidth;
|
||||
int iheight;
|
||||
unsigned frameCycles;
|
||||
unsigned cycles;
|
||||
int frameCycles;
|
||||
int cycles;
|
||||
int frameskip;
|
||||
int skipResidue;
|
||||
bool loop;
|
||||
|
@ -99,14 +99,13 @@ bool FFmpegEncoderSetAudio(struct FFmpegEncoder*, const char* acodec, unsigned a
|
|||
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
|
||||
|
||||
#endif
|
||||
|
|
|
@ -27,6 +27,7 @@ GIFView::GIFView(QWidget* parent)
|
|||
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 @@ void GIFView::setController(std::shared_ptr<CoreController> controller) {
|
|||
}
|
||||
|
||||
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 @@ void GIFView::startRecording() {
|
|||
}
|
||||
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 @@ void GIFView::stopRecording() {
|
|||
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 @@ void GIFView::setFilename(const QString& filename) {
|
|||
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 @@ void GIFView::changeExtension() {
|
|||
}
|
||||
if (m_ui.fmtGif->isChecked()) {
|
||||
filename += ".gif";
|
||||
} else if (m_ui.fmtWebP->isChecked()) {
|
||||
filename += ".webp";
|
||||
} else if (m_ui.fmtApng->isChecked()) {
|
||||
filename += ".png";
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>392</width>
|
||||
<height>262</height>
|
||||
<height>225</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
|
@ -17,29 +17,20 @@
|
|||
<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 @@
|
|||
</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>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="3">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Close</set>
|
||||
<string>APNG</string>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">format</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
|
@ -142,13 +129,36 @@
|
|||
</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>stop</tabstop>
|
||||
<tabstop>selectFile</tabstop>
|
||||
<tabstop>fmtGif</tabstop>
|
||||
<tabstop>fmtWebP</tabstop>
|
||||
<tabstop>fmtApng</tabstop>
|
||||
<tabstop>loop</tabstop>
|
||||
<tabstop>frameskip</tabstop>
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
#include "GBAApp.h"
|
||||
#include "LogController.h"
|
||||
|
||||
#include <mgba-util/math.h>
|
||||
|
||||
#include <QMap>
|
||||
|
||||
using namespace QGBA;
|
||||
|
@ -400,15 +402,7 @@ void VideoView::updateAspectRatio(int width, int height, bool force) {
|
|||
} 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);
|
||||
}
|
||||
|
|
|
@ -1567,7 +1567,7 @@ void Window::setupMenu(QMenuBar* menubar) {
|
|||
|
||||
#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");
|
||||
|
|
Loading…
Reference in New Issue