Merge branch 'master' into feature/savestates-2
This commit is contained in:
commit
828f88d20a
|
@ -140,7 +140,7 @@ if (ENABLE_SDL2)
|
||||||
if (CITRA_USE_BUNDLED_SDL2)
|
if (CITRA_USE_BUNDLED_SDL2)
|
||||||
# Detect toolchain and platform
|
# Detect toolchain and platform
|
||||||
if ((MSVC_VERSION GREATER_EQUAL 1910 AND MSVC_VERSION LESS 1930) AND ARCHITECTURE_x86_64)
|
if ((MSVC_VERSION GREATER_EQUAL 1910 AND MSVC_VERSION LESS 1930) AND ARCHITECTURE_x86_64)
|
||||||
set(SDL2_VER "SDL2-2.0.8")
|
set(SDL2_VER "SDL2-2.0.10")
|
||||||
else()
|
else()
|
||||||
message(FATAL_ERROR "No bundled SDL2 binaries for your toolchain. Disable CITRA_USE_BUNDLED_SDL2 and provide your own.")
|
message(FATAL_ERROR "No bundled SDL2 binaries for your toolchain. Disable CITRA_USE_BUNDLED_SDL2 and provide your own.")
|
||||||
endif()
|
endif()
|
||||||
|
|
|
@ -6,10 +6,10 @@ function(copy_citra_Qt5_deps target_dir)
|
||||||
set(Qt5_MEDIASERVICE_DIR "${Qt5_DIR}/../../../plugins/mediaservice/")
|
set(Qt5_MEDIASERVICE_DIR "${Qt5_DIR}/../../../plugins/mediaservice/")
|
||||||
set(Qt5_STYLES_DIR "${Qt5_DIR}/../../../plugins/styles/")
|
set(Qt5_STYLES_DIR "${Qt5_DIR}/../../../plugins/styles/")
|
||||||
set(Qt5_IMAGEFORMATS_DIR "${Qt5_DIR}/../../../plugins/imageformats/")
|
set(Qt5_IMAGEFORMATS_DIR "${Qt5_DIR}/../../../plugins/imageformats/")
|
||||||
set(PLATFORMS ${DLL_DEST}platforms/)
|
set(PLATFORMS ${DLL_DEST}plugins/platforms/)
|
||||||
set(MEDIASERVICE ${DLL_DEST}mediaservice/)
|
set(MEDIASERVICE ${DLL_DEST}plugins/mediaservice/)
|
||||||
set(STYLES ${DLL_DEST}styles/)
|
set(STYLES ${DLL_DEST}plugins/styles/)
|
||||||
set(IMAGEFORMATS ${DLL_DEST}imageformats/)
|
set(IMAGEFORMATS ${DLL_DEST}plugins/imageformats/)
|
||||||
windows_copy_files(${target_dir} ${Qt5_DLL_DIR} ${DLL_DEST}
|
windows_copy_files(${target_dir} ${Qt5_DLL_DIR} ${DLL_DEST}
|
||||||
icudt*.dll
|
icudt*.dll
|
||||||
icuin*.dll
|
icuin*.dll
|
||||||
|
@ -38,4 +38,10 @@ function(copy_citra_Qt5_deps target_dir)
|
||||||
qwbmp$<$<CONFIG:Debug>:d>.dll
|
qwbmp$<$<CONFIG:Debug>:d>.dll
|
||||||
qwebp$<$<CONFIG:Debug>:d>.dll
|
qwebp$<$<CONFIG:Debug>:d>.dll
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Create an empty qt.conf file. Qt will detect that this file exists, and use the folder that its in as the root folder.
|
||||||
|
# This way it'll look for plugins in the root/plugins/ folder
|
||||||
|
add_custom_command(TARGET citra-qt POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E touch ${DLL_DEST}qt.conf
|
||||||
|
)
|
||||||
endfunction(copy_citra_Qt5_deps)
|
endfunction(copy_citra_Qt5_deps)
|
||||||
|
|
|
@ -27,7 +27,7 @@ install:
|
||||||
- ps: |
|
- ps: |
|
||||||
if ($env:BUILD_TYPE -eq 'mingw') {
|
if ($env:BUILD_TYPE -eq 'mingw') {
|
||||||
$dependencies = "mingw64/mingw-w64-x86_64-cmake mingw64/mingw-w64-x86_64-qt5 mingw64/mingw-w64-x86_64-ffmpeg"
|
$dependencies = "mingw64/mingw-w64-x86_64-cmake mingw64/mingw-w64-x86_64-qt5 mingw64/mingw-w64-x86_64-ffmpeg"
|
||||||
C:\msys64\usr\bin\bash -lc "pacman --noconfirm -U http://repo.msys2.org/mingw/x86_64/mingw-w64-x86_64-SDL2-2.0.5-2-any.pkg.tar.xz"
|
C:\msys64\usr\bin\bash -lc "pacman --noconfirm -U http://repo.msys2.org/mingw/x86_64/mingw-w64-x86_64-SDL2-2.0.10-1-any.pkg.tar.xz"
|
||||||
C:\msys64\usr\bin\bash -lc "pacman --noconfirm -S $dependencies"
|
C:\msys64\usr\bin\bash -lc "pacman --noconfirm -S $dependencies"
|
||||||
# (HACK) ignore errors
|
# (HACK) ignore errors
|
||||||
0
|
0
|
||||||
|
|
|
@ -38,14 +38,14 @@ void DspInterface::EnableStretching(bool enable) {
|
||||||
perform_time_stretching = enable;
|
perform_time_stretching = enable;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DspInterface::OutputFrame(StereoFrame16& frame) {
|
void DspInterface::OutputFrame(StereoFrame16 frame) {
|
||||||
if (!sink)
|
if (!sink)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
fifo.Push(frame.data(), frame.size());
|
fifo.Push(frame.data(), frame.size());
|
||||||
|
|
||||||
if (Core::System::GetInstance().VideoDumper().IsDumping()) {
|
if (Core::System::GetInstance().VideoDumper().IsDumping()) {
|
||||||
Core::System::GetInstance().VideoDumper().AddAudioFrame(frame);
|
Core::System::GetInstance().VideoDumper().AddAudioFrame(std::move(frame));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ void DspInterface::OutputSample(std::array<s16, 2> sample) {
|
||||||
fifo.Push(&sample, 1);
|
fifo.Push(&sample, 1);
|
||||||
|
|
||||||
if (Core::System::GetInstance().VideoDumper().IsDumping()) {
|
if (Core::System::GetInstance().VideoDumper().IsDumping()) {
|
||||||
Core::System::GetInstance().VideoDumper().AddAudioSample(sample);
|
Core::System::GetInstance().VideoDumper().AddAudioSample(std::move(sample));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -101,7 +101,7 @@ public:
|
||||||
void EnableStretching(bool enable);
|
void EnableStretching(bool enable);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void OutputFrame(StereoFrame16& frame);
|
void OutputFrame(StereoFrame16 frame);
|
||||||
void OutputSample(std::array<s16, 2> sample);
|
void OutputSample(std::array<s16, 2> sample);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -431,7 +431,7 @@ bool DspHle::Impl::Tick() {
|
||||||
// shared memory region)
|
// shared memory region)
|
||||||
current_frame = GenerateCurrentFrame();
|
current_frame = GenerateCurrentFrame();
|
||||||
|
|
||||||
parent.OutputFrame(current_frame);
|
parent.OutputFrame(std::move(current_frame));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -289,7 +289,9 @@ void Source::GenerateFrame() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.next_sample_number += static_cast<u32>(frame_position);
|
// TODO(jroweboy): Keep track of frame_position independently so that it doesn't lose precision
|
||||||
|
// over time
|
||||||
|
state.next_sample_number += static_cast<u32>(frame_position * state.rate_multiplier);
|
||||||
|
|
||||||
state.filters.ProcessFrame(current_frame);
|
state.filters.ProcessFrame(current_frame);
|
||||||
}
|
}
|
||||||
|
|
|
@ -483,7 +483,8 @@ DspLle::DspLle(Memory::MemorySystem& memory, bool multithread)
|
||||||
*memory.GetFCRAMPointer(address - Memory::FCRAM_PADDR) = value;
|
*memory.GetFCRAMPointer(address - Memory::FCRAM_PADDR) = value;
|
||||||
};
|
};
|
||||||
impl->teakra.SetAHBMCallback(ahbm);
|
impl->teakra.SetAHBMCallback(ahbm);
|
||||||
impl->teakra.SetAudioCallback([this](std::array<s16, 2> sample) { OutputSample(sample); });
|
impl->teakra.SetAudioCallback(
|
||||||
|
[this](std::array<s16, 2> sample) { OutputSample(std::move(sample)); });
|
||||||
}
|
}
|
||||||
DspLle::~DspLle() = default;
|
DspLle::~DspLle() = default;
|
||||||
|
|
||||||
|
|
|
@ -409,7 +409,7 @@ int main(int argc, char** argv) {
|
||||||
if (!dump_video.empty()) {
|
if (!dump_video.empty()) {
|
||||||
Layout::FramebufferLayout layout{
|
Layout::FramebufferLayout layout{
|
||||||
Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())};
|
Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())};
|
||||||
system.VideoDumper().StartDumping(dump_video, "webm", layout);
|
system.VideoDumper().StartDumping(dump_video, layout);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::thread render_thread([&emu_window] { emu_window->Present(); });
|
std::thread render_thread([&emu_window] { emu_window->Present(); });
|
||||||
|
|
|
@ -132,8 +132,6 @@ void Config::ReadValues() {
|
||||||
static_cast<u16>(sdl2_config->GetInteger("Renderer", "use_vsync_new", 1));
|
static_cast<u16>(sdl2_config->GetInteger("Renderer", "use_vsync_new", 1));
|
||||||
Settings::values.texture_filter_name =
|
Settings::values.texture_filter_name =
|
||||||
sdl2_config->GetString("Renderer", "texture_filter_name", "none");
|
sdl2_config->GetString("Renderer", "texture_filter_name", "none");
|
||||||
Settings::values.texture_filter_factor =
|
|
||||||
sdl2_config->GetInteger("Renderer", "texture_filter_factor", 1);
|
|
||||||
|
|
||||||
Settings::values.render_3d = static_cast<Settings::StereoRenderOption>(
|
Settings::values.render_3d = static_cast<Settings::StereoRenderOption>(
|
||||||
sdl2_config->GetInteger("Renderer", "render_3d", 0));
|
sdl2_config->GetInteger("Renderer", "render_3d", 0));
|
||||||
|
@ -201,7 +199,7 @@ void Config::ReadValues() {
|
||||||
sdl2_config->GetBoolean("Data Storage", "use_virtual_sd", true);
|
sdl2_config->GetBoolean("Data Storage", "use_virtual_sd", true);
|
||||||
|
|
||||||
// System
|
// System
|
||||||
Settings::values.is_new_3ds = sdl2_config->GetBoolean("System", "is_new_3ds", false);
|
Settings::values.is_new_3ds = sdl2_config->GetBoolean("System", "is_new_3ds", true);
|
||||||
Settings::values.region_value =
|
Settings::values.region_value =
|
||||||
sdl2_config->GetInteger("System", "region_value", Settings::REGION_VALUE_AUTO_SELECT);
|
sdl2_config->GetInteger("System", "region_value", Settings::REGION_VALUE_AUTO_SELECT);
|
||||||
Settings::values.init_clock =
|
Settings::values.init_clock =
|
||||||
|
@ -270,6 +268,33 @@ void Config::ReadValues() {
|
||||||
sdl2_config->GetString("WebService", "web_api_url", "https://api.citra-emu.org");
|
sdl2_config->GetString("WebService", "web_api_url", "https://api.citra-emu.org");
|
||||||
Settings::values.citra_username = sdl2_config->GetString("WebService", "citra_username", "");
|
Settings::values.citra_username = sdl2_config->GetString("WebService", "citra_username", "");
|
||||||
Settings::values.citra_token = sdl2_config->GetString("WebService", "citra_token", "");
|
Settings::values.citra_token = sdl2_config->GetString("WebService", "citra_token", "");
|
||||||
|
|
||||||
|
// Video Dumping
|
||||||
|
Settings::values.output_format =
|
||||||
|
sdl2_config->GetString("Video Dumping", "output_format", "webm");
|
||||||
|
Settings::values.format_options = sdl2_config->GetString("Video Dumping", "format_options", "");
|
||||||
|
|
||||||
|
Settings::values.video_encoder =
|
||||||
|
sdl2_config->GetString("Video Dumping", "video_encoder", "libvpx-vp9");
|
||||||
|
|
||||||
|
// Options for variable bit rate live streaming taken from here:
|
||||||
|
// https://developers.google.com/media/vp9/live-encoding
|
||||||
|
std::string default_video_options;
|
||||||
|
if (Settings::values.video_encoder == "libvpx-vp9") {
|
||||||
|
default_video_options =
|
||||||
|
"quality:realtime,speed:6,tile-columns:4,frame-parallel:1,threads:8,row-mt:1";
|
||||||
|
}
|
||||||
|
Settings::values.video_encoder_options =
|
||||||
|
sdl2_config->GetString("Video Dumping", "video_encoder_options", default_video_options);
|
||||||
|
Settings::values.video_bitrate =
|
||||||
|
sdl2_config->GetInteger("Video Dumping", "video_bitrate", 2500000);
|
||||||
|
|
||||||
|
Settings::values.audio_encoder =
|
||||||
|
sdl2_config->GetString("Video Dumping", "audio_encoder", "libvorbis");
|
||||||
|
Settings::values.audio_encoder_options =
|
||||||
|
sdl2_config->GetString("Video Dumping", "audio_encoder_options", "");
|
||||||
|
Settings::values.audio_bitrate =
|
||||||
|
sdl2_config->GetInteger("Video Dumping", "audio_bitrate", 64000);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Config::Reload() {
|
void Config::Reload() {
|
||||||
|
|
|
@ -132,9 +132,8 @@ use_disk_shader_cache =
|
||||||
# factor for the 3DS resolution
|
# factor for the 3DS resolution
|
||||||
resolution_factor =
|
resolution_factor =
|
||||||
|
|
||||||
# Texture filter name and scale factor
|
# Texture filter name
|
||||||
texture_filter_name =
|
texture_filter_name =
|
||||||
texture_filter_factor =
|
|
||||||
|
|
||||||
# Turns on the frame limiter, which will limit frames output to the target game speed
|
# Turns on the frame limiter, which will limit frames output to the target game speed
|
||||||
# 0: Off, 1: On (default)
|
# 0: Off, 1: On (default)
|
||||||
|
@ -244,7 +243,7 @@ use_virtual_sd =
|
||||||
|
|
||||||
[System]
|
[System]
|
||||||
# The system model that Citra will try to emulate
|
# The system model that Citra will try to emulate
|
||||||
# 0: Old 3DS (default), 1: New 3DS
|
# 0: Old 3DS, 1: New 3DS (default)
|
||||||
is_new_3ds =
|
is_new_3ds =
|
||||||
|
|
||||||
# The system region that Citra will use during emulation
|
# The system region that Citra will use during emulation
|
||||||
|
@ -305,5 +304,31 @@ web_api_url = https://api.citra-emu.org
|
||||||
# See https://profile.citra-emu.org/ for more info
|
# See https://profile.citra-emu.org/ for more info
|
||||||
citra_username =
|
citra_username =
|
||||||
citra_token =
|
citra_token =
|
||||||
|
|
||||||
|
[Video Dumping]
|
||||||
|
# Format of the video to output, default: webm
|
||||||
|
output_format =
|
||||||
|
|
||||||
|
# Options passed to the muxer (optional)
|
||||||
|
# This is a param package, format: [key1]:[value1],[key2]:[value2],...
|
||||||
|
format_options =
|
||||||
|
|
||||||
|
# Video encoder used, default: libvpx-vp9
|
||||||
|
video_encoder =
|
||||||
|
|
||||||
|
# Options passed to the video codec (optional)
|
||||||
|
video_encoder_options =
|
||||||
|
|
||||||
|
# Video bitrate, default: 2500000
|
||||||
|
video_bitrate =
|
||||||
|
|
||||||
|
# Audio encoder used, default: libvorbis
|
||||||
|
audio_encoder =
|
||||||
|
|
||||||
|
# Options passed to the audio codec (optional)
|
||||||
|
audio_encoder_options =
|
||||||
|
|
||||||
|
# Audio bitrate, default: 64000
|
||||||
|
audio_bitrate =
|
||||||
)";
|
)";
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,6 +162,20 @@ add_executable(citra-qt
|
||||||
util/util.h
|
util/util.h
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (ENABLE_FFMPEG_VIDEO_DUMPER)
|
||||||
|
target_sources(citra-qt PRIVATE
|
||||||
|
dumping/dumping_dialog.cpp
|
||||||
|
dumping/dumping_dialog.h
|
||||||
|
dumping/dumping_dialog.ui
|
||||||
|
dumping/option_set_dialog.cpp
|
||||||
|
dumping/option_set_dialog.h
|
||||||
|
dumping/option_set_dialog.ui
|
||||||
|
dumping/options_dialog.cpp
|
||||||
|
dumping/options_dialog.h
|
||||||
|
dumping/options_dialog.ui
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
file(GLOB COMPAT_LIST
|
file(GLOB COMPAT_LIST
|
||||||
${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc
|
${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc
|
||||||
${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json)
|
${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json)
|
||||||
|
|
|
@ -104,8 +104,8 @@ void EmuThread::run() {
|
||||||
}
|
}
|
||||||
|
|
||||||
OpenGLWindow::OpenGLWindow(QWindow* parent, QWidget* event_handler, QOpenGLContext* shared_context)
|
OpenGLWindow::OpenGLWindow(QWindow* parent, QWidget* event_handler, QOpenGLContext* shared_context)
|
||||||
: QWindow(parent), event_handler(event_handler),
|
: QWindow(parent), context(new QOpenGLContext(shared_context->parent())),
|
||||||
context(new QOpenGLContext(shared_context->parent())) {
|
event_handler(event_handler) {
|
||||||
|
|
||||||
// disable vsync for any shared contexts
|
// disable vsync for any shared contexts
|
||||||
auto format = shared_context->format();
|
auto format = shared_context->format();
|
||||||
|
@ -201,6 +201,8 @@ GRenderWindow::GRenderWindow(QWidget* parent_, EmuThread* emu_thread)
|
||||||
setLayout(layout);
|
setLayout(layout);
|
||||||
InputCommon::Init();
|
InputCommon::Init();
|
||||||
|
|
||||||
|
this->setMouseTracking(true);
|
||||||
|
|
||||||
GMainWindow* parent = GetMainWindow();
|
GMainWindow* parent = GetMainWindow();
|
||||||
connect(this, &GRenderWindow::FirstFrameDisplayed, parent, &GMainWindow::OnLoadComplete);
|
connect(this, &GRenderWindow::FirstFrameDisplayed, parent, &GMainWindow::OnLoadComplete);
|
||||||
}
|
}
|
||||||
|
@ -297,6 +299,7 @@ void GRenderWindow::mousePressEvent(QMouseEvent* event) {
|
||||||
} else if (event->button() == Qt::RightButton) {
|
} else if (event->button() == Qt::RightButton) {
|
||||||
InputCommon::GetMotionEmu()->BeginTilt(pos.x(), pos.y());
|
InputCommon::GetMotionEmu()->BeginTilt(pos.x(), pos.y());
|
||||||
}
|
}
|
||||||
|
QWidget::mouseMoveEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GRenderWindow::mouseMoveEvent(QMouseEvent* event) {
|
void GRenderWindow::mouseMoveEvent(QMouseEvent* event) {
|
||||||
|
@ -307,6 +310,7 @@ void GRenderWindow::mouseMoveEvent(QMouseEvent* event) {
|
||||||
const auto [x, y] = ScaleTouch(pos);
|
const auto [x, y] = ScaleTouch(pos);
|
||||||
this->TouchMoved(x, y);
|
this->TouchMoved(x, y);
|
||||||
InputCommon::GetMotionEmu()->Tilt(pos.x(), pos.y());
|
InputCommon::GetMotionEmu()->Tilt(pos.x(), pos.y());
|
||||||
|
QWidget::mouseMoveEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GRenderWindow::mouseReleaseEvent(QMouseEvent* event) {
|
void GRenderWindow::mouseReleaseEvent(QMouseEvent* event) {
|
||||||
|
|
|
@ -95,6 +95,7 @@ void Config::ReadValues() {
|
||||||
ReadMiscellaneousValues();
|
ReadMiscellaneousValues();
|
||||||
ReadDebuggingValues();
|
ReadDebuggingValues();
|
||||||
ReadWebServiceValues();
|
ReadWebServiceValues();
|
||||||
|
ReadVideoDumpingValues();
|
||||||
ReadUIValues();
|
ReadUIValues();
|
||||||
ReadUtilityValues();
|
ReadUtilityValues();
|
||||||
}
|
}
|
||||||
|
@ -456,8 +457,6 @@ void Config::ReadRendererValues() {
|
||||||
ReadSetting(QStringLiteral("texture_filter_name"), QStringLiteral("none"))
|
ReadSetting(QStringLiteral("texture_filter_name"), QStringLiteral("none"))
|
||||||
.toString()
|
.toString()
|
||||||
.toStdString();
|
.toStdString();
|
||||||
Settings::values.texture_filter_factor =
|
|
||||||
ReadSetting(QStringLiteral("texture_filter_factor"), 1).toInt();
|
|
||||||
|
|
||||||
qt_config->endGroup();
|
qt_config->endGroup();
|
||||||
}
|
}
|
||||||
|
@ -484,7 +483,7 @@ void Config::ReadShortcutValues() {
|
||||||
void Config::ReadSystemValues() {
|
void Config::ReadSystemValues() {
|
||||||
qt_config->beginGroup(QStringLiteral("System"));
|
qt_config->beginGroup(QStringLiteral("System"));
|
||||||
|
|
||||||
Settings::values.is_new_3ds = ReadSetting(QStringLiteral("is_new_3ds"), false).toBool();
|
Settings::values.is_new_3ds = ReadSetting(QStringLiteral("is_new_3ds"), true).toBool();
|
||||||
Settings::values.region_value =
|
Settings::values.region_value =
|
||||||
ReadSetting(QStringLiteral("region_value"), Settings::REGION_VALUE_AUTO_SELECT).toInt();
|
ReadSetting(QStringLiteral("region_value"), Settings::REGION_VALUE_AUTO_SELECT).toInt();
|
||||||
Settings::values.init_clock = static_cast<Settings::InitClock>(
|
Settings::values.init_clock = static_cast<Settings::InitClock>(
|
||||||
|
@ -496,6 +495,49 @@ void Config::ReadSystemValues() {
|
||||||
qt_config->endGroup();
|
qt_config->endGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Options for variable bit rate live streaming taken from here:
|
||||||
|
// https://developers.google.com/media/vp9/live-encoding
|
||||||
|
const QString DEFAULT_VIDEO_ENCODER_OPTIONS =
|
||||||
|
QStringLiteral("quality:realtime,speed:6,tile-columns:4,frame-parallel:1,threads:8,row-mt:1");
|
||||||
|
const QString DEFAULT_AUDIO_ENCODER_OPTIONS = QString{};
|
||||||
|
|
||||||
|
void Config::ReadVideoDumpingValues() {
|
||||||
|
qt_config->beginGroup(QStringLiteral("VideoDumping"));
|
||||||
|
|
||||||
|
Settings::values.output_format =
|
||||||
|
ReadSetting(QStringLiteral("output_format"), QStringLiteral("webm"))
|
||||||
|
.toString()
|
||||||
|
.toStdString();
|
||||||
|
Settings::values.format_options =
|
||||||
|
ReadSetting(QStringLiteral("format_options")).toString().toStdString();
|
||||||
|
|
||||||
|
Settings::values.video_encoder =
|
||||||
|
ReadSetting(QStringLiteral("video_encoder"), QStringLiteral("libvpx-vp9"))
|
||||||
|
.toString()
|
||||||
|
.toStdString();
|
||||||
|
|
||||||
|
Settings::values.video_encoder_options =
|
||||||
|
ReadSetting(QStringLiteral("video_encoder_options"), DEFAULT_VIDEO_ENCODER_OPTIONS)
|
||||||
|
.toString()
|
||||||
|
.toStdString();
|
||||||
|
|
||||||
|
Settings::values.video_bitrate =
|
||||||
|
ReadSetting(QStringLiteral("video_bitrate"), 2500000).toULongLong();
|
||||||
|
|
||||||
|
Settings::values.audio_encoder =
|
||||||
|
ReadSetting(QStringLiteral("audio_encoder"), QStringLiteral("libvorbis"))
|
||||||
|
.toString()
|
||||||
|
.toStdString();
|
||||||
|
Settings::values.audio_encoder_options =
|
||||||
|
ReadSetting(QStringLiteral("audio_encoder_options"), DEFAULT_AUDIO_ENCODER_OPTIONS)
|
||||||
|
.toString()
|
||||||
|
.toStdString();
|
||||||
|
Settings::values.audio_bitrate =
|
||||||
|
ReadSetting(QStringLiteral("audio_bitrate"), 64000).toULongLong();
|
||||||
|
|
||||||
|
qt_config->endGroup();
|
||||||
|
}
|
||||||
|
|
||||||
void Config::ReadUIValues() {
|
void Config::ReadUIValues() {
|
||||||
qt_config->beginGroup(QStringLiteral("UI"));
|
qt_config->beginGroup(QStringLiteral("UI"));
|
||||||
|
|
||||||
|
@ -530,6 +572,8 @@ void Config::ReadUIValues() {
|
||||||
UISettings::values.show_console = ReadSetting(QStringLiteral("showConsole"), false).toBool();
|
UISettings::values.show_console = ReadSetting(QStringLiteral("showConsole"), false).toBool();
|
||||||
UISettings::values.pause_when_in_background =
|
UISettings::values.pause_when_in_background =
|
||||||
ReadSetting(QStringLiteral("pauseWhenInBackground"), false).toBool();
|
ReadSetting(QStringLiteral("pauseWhenInBackground"), false).toBool();
|
||||||
|
UISettings::values.hide_mouse =
|
||||||
|
ReadSetting(QStringLiteral("hideInactiveMouse"), false).toBool();
|
||||||
|
|
||||||
qt_config->endGroup();
|
qt_config->endGroup();
|
||||||
}
|
}
|
||||||
|
@ -628,6 +672,7 @@ void Config::SaveValues() {
|
||||||
SaveMiscellaneousValues();
|
SaveMiscellaneousValues();
|
||||||
SaveDebuggingValues();
|
SaveDebuggingValues();
|
||||||
SaveWebServiceValues();
|
SaveWebServiceValues();
|
||||||
|
SaveVideoDumpingValues();
|
||||||
SaveUIValues();
|
SaveUIValues();
|
||||||
SaveUtilityValues();
|
SaveUtilityValues();
|
||||||
}
|
}
|
||||||
|
@ -895,8 +940,6 @@ void Config::SaveRendererValues() {
|
||||||
WriteSetting(QStringLiteral("texture_filter_name"),
|
WriteSetting(QStringLiteral("texture_filter_name"),
|
||||||
QString::fromStdString(Settings::values.texture_filter_name),
|
QString::fromStdString(Settings::values.texture_filter_name),
|
||||||
QStringLiteral("none"));
|
QStringLiteral("none"));
|
||||||
WriteSetting(QStringLiteral("texture_filter_factor"), Settings::values.texture_filter_factor,
|
|
||||||
1);
|
|
||||||
|
|
||||||
qt_config->endGroup();
|
qt_config->endGroup();
|
||||||
}
|
}
|
||||||
|
@ -923,7 +966,7 @@ void Config::SaveShortcutValues() {
|
||||||
void Config::SaveSystemValues() {
|
void Config::SaveSystemValues() {
|
||||||
qt_config->beginGroup(QStringLiteral("System"));
|
qt_config->beginGroup(QStringLiteral("System"));
|
||||||
|
|
||||||
WriteSetting(QStringLiteral("is_new_3ds"), Settings::values.is_new_3ds, false);
|
WriteSetting(QStringLiteral("is_new_3ds"), Settings::values.is_new_3ds, true);
|
||||||
WriteSetting(QStringLiteral("region_value"), Settings::values.region_value,
|
WriteSetting(QStringLiteral("region_value"), Settings::values.region_value,
|
||||||
Settings::REGION_VALUE_AUTO_SELECT);
|
Settings::REGION_VALUE_AUTO_SELECT);
|
||||||
WriteSetting(QStringLiteral("init_clock"), static_cast<u32>(Settings::values.init_clock),
|
WriteSetting(QStringLiteral("init_clock"), static_cast<u32>(Settings::values.init_clock),
|
||||||
|
@ -934,6 +977,33 @@ void Config::SaveSystemValues() {
|
||||||
qt_config->endGroup();
|
qt_config->endGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Config::SaveVideoDumpingValues() {
|
||||||
|
qt_config->beginGroup(QStringLiteral("VideoDumping"));
|
||||||
|
|
||||||
|
WriteSetting(QStringLiteral("output_format"),
|
||||||
|
QString::fromStdString(Settings::values.output_format), QStringLiteral("webm"));
|
||||||
|
WriteSetting(QStringLiteral("format_options"),
|
||||||
|
QString::fromStdString(Settings::values.format_options));
|
||||||
|
WriteSetting(QStringLiteral("video_encoder"),
|
||||||
|
QString::fromStdString(Settings::values.video_encoder),
|
||||||
|
QStringLiteral("libvpx-vp9"));
|
||||||
|
WriteSetting(QStringLiteral("video_encoder_options"),
|
||||||
|
QString::fromStdString(Settings::values.video_encoder_options),
|
||||||
|
DEFAULT_VIDEO_ENCODER_OPTIONS);
|
||||||
|
WriteSetting(QStringLiteral("video_bitrate"),
|
||||||
|
static_cast<unsigned long long>(Settings::values.video_bitrate), 2500000);
|
||||||
|
WriteSetting(QStringLiteral("audio_encoder"),
|
||||||
|
QString::fromStdString(Settings::values.audio_encoder),
|
||||||
|
QStringLiteral("libvorbis"));
|
||||||
|
WriteSetting(QStringLiteral("audio_encoder_options"),
|
||||||
|
QString::fromStdString(Settings::values.audio_encoder_options),
|
||||||
|
DEFAULT_AUDIO_ENCODER_OPTIONS);
|
||||||
|
WriteSetting(QStringLiteral("audio_bitrate"),
|
||||||
|
static_cast<unsigned long long>(Settings::values.audio_bitrate), 64000);
|
||||||
|
|
||||||
|
qt_config->endGroup();
|
||||||
|
}
|
||||||
|
|
||||||
void Config::SaveUIValues() {
|
void Config::SaveUIValues() {
|
||||||
qt_config->beginGroup(QStringLiteral("UI"));
|
qt_config->beginGroup(QStringLiteral("UI"));
|
||||||
|
|
||||||
|
@ -962,6 +1032,7 @@ void Config::SaveUIValues() {
|
||||||
WriteSetting(QStringLiteral("showConsole"), UISettings::values.show_console, false);
|
WriteSetting(QStringLiteral("showConsole"), UISettings::values.show_console, false);
|
||||||
WriteSetting(QStringLiteral("pauseWhenInBackground"),
|
WriteSetting(QStringLiteral("pauseWhenInBackground"),
|
||||||
UISettings::values.pause_when_in_background, false);
|
UISettings::values.pause_when_in_background, false);
|
||||||
|
WriteSetting(QStringLiteral("hideInactiveMouse"), UISettings::values.hide_mouse, false);
|
||||||
|
|
||||||
qt_config->endGroup();
|
qt_config->endGroup();
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ private:
|
||||||
void ReadUpdaterValues();
|
void ReadUpdaterValues();
|
||||||
void ReadUtilityValues();
|
void ReadUtilityValues();
|
||||||
void ReadWebServiceValues();
|
void ReadWebServiceValues();
|
||||||
|
void ReadVideoDumpingValues();
|
||||||
|
|
||||||
void SaveValues();
|
void SaveValues();
|
||||||
void SaveAudioValues();
|
void SaveAudioValues();
|
||||||
|
@ -65,6 +66,7 @@ private:
|
||||||
void SaveUpdaterValues();
|
void SaveUpdaterValues();
|
||||||
void SaveUtilityValues();
|
void SaveUtilityValues();
|
||||||
void SaveWebServiceValues();
|
void SaveWebServiceValues();
|
||||||
|
void SaveVideoDumpingValues();
|
||||||
|
|
||||||
QVariant ReadSetting(const QString& name) const;
|
QVariant ReadSetting(const QString& name) const;
|
||||||
QVariant ReadSetting(const QString& name, const QVariant& default_value) const;
|
QVariant ReadSetting(const QString& name, const QVariant& default_value) const;
|
||||||
|
|
|
@ -8,17 +8,14 @@
|
||||||
#include "core/settings.h"
|
#include "core/settings.h"
|
||||||
#include "ui_configure_enhancements.h"
|
#include "ui_configure_enhancements.h"
|
||||||
#include "video_core/renderer_opengl/post_processing_opengl.h"
|
#include "video_core/renderer_opengl/post_processing_opengl.h"
|
||||||
#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h"
|
#include "video_core/renderer_opengl/texture_filters/texture_filterer.h"
|
||||||
|
|
||||||
ConfigureEnhancements::ConfigureEnhancements(QWidget* parent)
|
ConfigureEnhancements::ConfigureEnhancements(QWidget* parent)
|
||||||
: QWidget(parent), ui(new Ui::ConfigureEnhancements) {
|
: QWidget(parent), ui(new Ui::ConfigureEnhancements) {
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
|
|
||||||
for (const auto& filter : OpenGL::TextureFilterManager::TextureFilterMap())
|
for (const auto& filter : OpenGL::TextureFilterer::GetFilterNames())
|
||||||
ui->texture_filter_combobox->addItem(QString::fromStdString(filter.first.data()));
|
ui->texture_filter_combobox->addItem(QString::fromStdString(filter.data()));
|
||||||
|
|
||||||
connect(ui->texture_filter_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
|
||||||
&ConfigureEnhancements::updateTextureFilter);
|
|
||||||
|
|
||||||
SetConfiguration();
|
SetConfiguration();
|
||||||
|
|
||||||
|
@ -60,7 +57,6 @@ void ConfigureEnhancements::SetConfiguration() {
|
||||||
ui->factor_3d->setValue(Settings::values.factor_3d);
|
ui->factor_3d->setValue(Settings::values.factor_3d);
|
||||||
updateShaders(Settings::values.render_3d);
|
updateShaders(Settings::values.render_3d);
|
||||||
ui->toggle_linear_filter->setChecked(Settings::values.filter_mode);
|
ui->toggle_linear_filter->setChecked(Settings::values.filter_mode);
|
||||||
ui->texture_scale_spinbox->setValue(Settings::values.texture_filter_factor);
|
|
||||||
int tex_filter_idx = ui->texture_filter_combobox->findText(
|
int tex_filter_idx = ui->texture_filter_combobox->findText(
|
||||||
QString::fromStdString(Settings::values.texture_filter_name));
|
QString::fromStdString(Settings::values.texture_filter_name));
|
||||||
if (tex_filter_idx == -1) {
|
if (tex_filter_idx == -1) {
|
||||||
|
@ -68,7 +64,6 @@ void ConfigureEnhancements::SetConfiguration() {
|
||||||
} else {
|
} else {
|
||||||
ui->texture_filter_combobox->setCurrentIndex(tex_filter_idx);
|
ui->texture_filter_combobox->setCurrentIndex(tex_filter_idx);
|
||||||
}
|
}
|
||||||
updateTextureFilter(tex_filter_idx);
|
|
||||||
ui->layout_combobox->setCurrentIndex(static_cast<int>(Settings::values.layout_option));
|
ui->layout_combobox->setCurrentIndex(static_cast<int>(Settings::values.layout_option));
|
||||||
ui->swap_screen->setChecked(Settings::values.swap_screen);
|
ui->swap_screen->setChecked(Settings::values.swap_screen);
|
||||||
ui->toggle_disk_shader_cache->setChecked(Settings::values.use_hw_shader &&
|
ui->toggle_disk_shader_cache->setChecked(Settings::values.use_hw_shader &&
|
||||||
|
@ -105,17 +100,6 @@ void ConfigureEnhancements::updateShaders(Settings::StereoRenderOption stereo_op
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureEnhancements::updateTextureFilter(int index) {
|
|
||||||
if (index == -1)
|
|
||||||
return;
|
|
||||||
ui->texture_filter_group->setEnabled(index != 0);
|
|
||||||
const auto& clamp = OpenGL::TextureFilterManager::TextureFilterMap()
|
|
||||||
.at(ui->texture_filter_combobox->currentText().toStdString())
|
|
||||||
.clamp_scale;
|
|
||||||
ui->texture_scale_spinbox->setMinimum(clamp.min);
|
|
||||||
ui->texture_scale_spinbox->setMaximum(clamp.max);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigureEnhancements::RetranslateUI() {
|
void ConfigureEnhancements::RetranslateUI() {
|
||||||
ui->retranslateUi(this);
|
ui->retranslateUi(this);
|
||||||
}
|
}
|
||||||
|
@ -130,7 +114,6 @@ void ConfigureEnhancements::ApplyConfiguration() {
|
||||||
ui->shader_combobox->itemText(ui->shader_combobox->currentIndex()).toStdString();
|
ui->shader_combobox->itemText(ui->shader_combobox->currentIndex()).toStdString();
|
||||||
Settings::values.filter_mode = ui->toggle_linear_filter->isChecked();
|
Settings::values.filter_mode = ui->toggle_linear_filter->isChecked();
|
||||||
Settings::values.texture_filter_name = ui->texture_filter_combobox->currentText().toStdString();
|
Settings::values.texture_filter_name = ui->texture_filter_combobox->currentText().toStdString();
|
||||||
Settings::values.texture_filter_factor = ui->texture_scale_spinbox->value();
|
|
||||||
Settings::values.layout_option =
|
Settings::values.layout_option =
|
||||||
static_cast<Settings::LayoutOption>(ui->layout_combobox->currentIndex());
|
static_cast<Settings::LayoutOption>(ui->layout_combobox->currentIndex());
|
||||||
Settings::values.swap_screen = ui->swap_screen->isChecked();
|
Settings::values.swap_screen = ui->swap_screen->isChecked();
|
||||||
|
|
|
@ -131,42 +131,6 @@
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<widget class="QWidget" name="texture_filter_group" native="true">
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_7">
|
|
||||||
<property name="leftMargin">
|
|
||||||
<number>16</number>
|
|
||||||
</property>
|
|
||||||
<property name="topMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="rightMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="bottomMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<item>
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label_6">
|
|
||||||
<property name="text">
|
|
||||||
<string>Texture Scale Factor</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QSpinBox" name="texture_scale_spinbox">
|
|
||||||
<property name="minimum">
|
|
||||||
<number>1</number>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
|
@ -27,6 +27,7 @@ ConfigureGeneral::~ConfigureGeneral() = default;
|
||||||
void ConfigureGeneral::SetConfiguration() {
|
void ConfigureGeneral::SetConfiguration() {
|
||||||
ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing);
|
ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing);
|
||||||
ui->toggle_background_pause->setChecked(UISettings::values.pause_when_in_background);
|
ui->toggle_background_pause->setChecked(UISettings::values.pause_when_in_background);
|
||||||
|
ui->toggle_hide_mouse->setChecked(UISettings::values.hide_mouse);
|
||||||
|
|
||||||
ui->toggle_update_check->setChecked(UISettings::values.check_for_update_on_start);
|
ui->toggle_update_check->setChecked(UISettings::values.check_for_update_on_start);
|
||||||
ui->toggle_auto_update->setChecked(UISettings::values.update_on_close);
|
ui->toggle_auto_update->setChecked(UISettings::values.update_on_close);
|
||||||
|
@ -55,6 +56,7 @@ void ConfigureGeneral::ResetDefaults() {
|
||||||
void ConfigureGeneral::ApplyConfiguration() {
|
void ConfigureGeneral::ApplyConfiguration() {
|
||||||
UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked();
|
UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked();
|
||||||
UISettings::values.pause_when_in_background = ui->toggle_background_pause->isChecked();
|
UISettings::values.pause_when_in_background = ui->toggle_background_pause->isChecked();
|
||||||
|
UISettings::values.hide_mouse = ui->toggle_hide_mouse->isChecked();
|
||||||
|
|
||||||
UISettings::values.check_for_update_on_start = ui->toggle_update_check->isChecked();
|
UISettings::values.check_for_update_on_start = ui->toggle_update_check->isChecked();
|
||||||
UISettings::values.update_on_close = ui->toggle_auto_update->isChecked();
|
UISettings::values.update_on_close = ui->toggle_auto_update->isChecked();
|
||||||
|
|
|
@ -36,6 +36,13 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="toggle_hide_mouse">
|
||||||
|
<property name="text">
|
||||||
|
<string>Hide mouse on inactivity</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
|
@ -30,7 +30,7 @@ CalibrationConfigurationDialog::CalibrationConfigurationDialog(QWidget* parent,
|
||||||
setLayout(layout);
|
setLayout(layout);
|
||||||
|
|
||||||
using namespace InputCommon::CemuhookUDP;
|
using namespace InputCommon::CemuhookUDP;
|
||||||
job = std::move(std::make_unique<CalibrationConfigurationJob>(
|
job = std::make_unique<CalibrationConfigurationJob>(
|
||||||
host, port, pad_index, client_id,
|
host, port, pad_index, client_id,
|
||||||
[this](CalibrationConfigurationJob::Status status) {
|
[this](CalibrationConfigurationJob::Status status) {
|
||||||
QString text;
|
QString text;
|
||||||
|
@ -56,7 +56,7 @@ CalibrationConfigurationDialog::CalibrationConfigurationDialog(QWidget* parent,
|
||||||
min_y = min_y_;
|
min_y = min_y_;
|
||||||
max_x = max_x_;
|
max_x = max_x_;
|
||||||
max_y = max_y_;
|
max_y = max_y_;
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
CalibrationConfigurationDialog::~CalibrationConfigurationDialog() = default;
|
CalibrationConfigurationDialog::~CalibrationConfigurationDialog() = default;
|
||||||
|
|
|
@ -277,6 +277,8 @@ void ConfigureSystem::SetConfiguration() {
|
||||||
ui->slider_clock_speed->setValue(SettingsToSlider(Settings::values.cpu_clock_percentage));
|
ui->slider_clock_speed->setValue(SettingsToSlider(Settings::values.cpu_clock_percentage));
|
||||||
ui->clock_display_label->setText(
|
ui->clock_display_label->setText(
|
||||||
QStringLiteral("%1%").arg(Settings::values.cpu_clock_percentage));
|
QStringLiteral("%1%").arg(Settings::values.cpu_clock_percentage));
|
||||||
|
|
||||||
|
ui->toggle_new_3ds->setChecked(Settings::values.is_new_3ds);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureSystem::ReadSystemSettings() {
|
void ConfigureSystem::ReadSystemSettings() {
|
||||||
|
@ -374,6 +376,8 @@ void ConfigureSystem::ApplyConfiguration() {
|
||||||
Settings::values.init_clock =
|
Settings::values.init_clock =
|
||||||
static_cast<Settings::InitClock>(ui->combo_init_clock->currentIndex());
|
static_cast<Settings::InitClock>(ui->combo_init_clock->currentIndex());
|
||||||
Settings::values.init_time = ui->edit_init_time->dateTime().toTime_t();
|
Settings::values.init_time = ui->edit_init_time->dateTime().toTime_t();
|
||||||
|
|
||||||
|
Settings::values.is_new_3ds = ui->toggle_new_3ds->isChecked();
|
||||||
}
|
}
|
||||||
|
|
||||||
Settings::values.cpu_clock_percentage = SliderToSettings(ui->slider_clock_speed->value());
|
Settings::values.cpu_clock_percentage = SliderToSettings(ui->slider_clock_speed->value());
|
||||||
|
|
|
@ -22,14 +22,74 @@
|
||||||
<string>System Settings</string>
|
<string>System Settings</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
<item row="0" column="0">
|
<item row="4" column="1">
|
||||||
<widget class="QLabel" name="label_username">
|
<widget class="QComboBox" name="combo_language">
|
||||||
<property name="text">
|
<property name="toolTip">
|
||||||
<string>Username</string>
|
<string>Note: this can be overridden when region setting is auto-select</string>
|
||||||
</property>
|
</property>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Japanese (日本語)</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>English</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>French (français)</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>German (Deutsch)</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Italian (italiano)</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Spanish (español)</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Simplified Chinese (简体中文)</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Korean (한국어)</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Dutch (Nederlands)</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Portuguese (português)</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Russian (Русский)</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Traditional Chinese (正體中文)</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="1">
|
<item row="2" column="1">
|
||||||
<widget class="QLineEdit" name="edit_username">
|
<widget class="QLineEdit" name="edit_username">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||||
|
@ -42,14 +102,33 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="2" column="0">
|
||||||
<widget class="QLabel" name="label_birthday">
|
<widget class="QLabel" name="label_username">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Birthday</string>
|
<string>Username</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1">
|
<item row="5" column="1">
|
||||||
|
<widget class="QComboBox" name="combo_sound">
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Mono</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Stereo</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Surround</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="1">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_birthday2">
|
<layout class="QHBoxLayout" name="horizontalLayout_birthday2">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QComboBox" name="combo_birthmonth">
|
<widget class="QComboBox" name="combo_birthmonth">
|
||||||
|
@ -120,124 +199,38 @@
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0">
|
<item row="4" column="0">
|
||||||
<widget class="QLabel" name="label_language">
|
<widget class="QLabel" name="label_language">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Language</string>
|
<string>Language</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="1">
|
<item row="3" column="0">
|
||||||
<widget class="QComboBox" name="combo_language">
|
<widget class="QLabel" name="label_birthday">
|
||||||
<property name="toolTip">
|
|
||||||
<string>Note: this can be overridden when region setting is auto-select</string>
|
|
||||||
</property>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Japanese (日本語)</string>
|
<string>Birthday</string>
|
||||||
</property>
|
</property>
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>English</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>French (français)</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>German (Deutsch)</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>Italian (italiano)</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>Spanish (español)</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>Simplified Chinese (简体中文)</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>Korean (한국어)</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>Dutch (Nederlands)</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>Portuguese (português)</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>Russian (Русский)</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>Traditional Chinese (正體中文)</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="0">
|
<item row="5" column="0">
|
||||||
<widget class="QLabel" name="label_sound">
|
<widget class="QLabel" name="label_sound">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Sound output mode</string>
|
<string>Sound output mode</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="1">
|
<item row="6" column="1">
|
||||||
<widget class="QComboBox" name="combo_sound">
|
<widget class="QComboBox" name="combo_country"/>
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>Mono</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="6" column="0">
|
||||||
<property name="text">
|
|
||||||
<string>Stereo</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>Surround</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="0">
|
|
||||||
<widget class="QLabel" name="label_country">
|
<widget class="QLabel" name="label_country">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Country</string>
|
<string>Country</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="1">
|
<item row="7" column="1">
|
||||||
<widget class="QComboBox" name="combo_country"/>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="0">
|
|
||||||
<widget class="QLabel" name="label_init_clock">
|
|
||||||
<property name="text">
|
|
||||||
<string>Clock</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="1">
|
|
||||||
<widget class="QComboBox" name="combo_init_clock">
|
<widget class="QComboBox" name="combo_init_clock">
|
||||||
<item>
|
<item>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -251,42 +244,35 @@
|
||||||
</item>
|
</item>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0">
|
<item row="7" column="0">
|
||||||
|
<widget class="QLabel" name="label_init_clock">
|
||||||
|
<property name="text">
|
||||||
|
<string>Clock</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="8" column="0">
|
||||||
<widget class="QLabel" name="label_init_time">
|
<widget class="QLabel" name="label_init_time">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Startup time</string>
|
<string>Startup time</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="1">
|
<item row="9" column="1">
|
||||||
<widget class="QDateTimeEdit" name="edit_init_time">
|
|
||||||
<property name="displayFormat">
|
|
||||||
<string>yyyy-MM-ddTHH:mm:ss</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="0">
|
|
||||||
<widget class="QLabel" name="label_play_coins">
|
|
||||||
<property name="text">
|
|
||||||
<string>Play Coins:</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="1">
|
|
||||||
<widget class="QSpinBox" name="spinBox_play_coins">
|
<widget class="QSpinBox" name="spinBox_play_coins">
|
||||||
<property name="maximum">
|
<property name="maximum">
|
||||||
<number>300</number>
|
<number>300</number>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="0">
|
<item row="9" column="0">
|
||||||
<widget class="QLabel" name="label_console_id">
|
<widget class="QLabel" name="label_play_coins">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Console ID:</string>
|
<string>Play Coins:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="1">
|
<item row="12" column="1">
|
||||||
<widget class="QPushButton" name="button_regenerate_console_id">
|
<widget class="QPushButton" name="button_regenerate_console_id">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||||
|
@ -302,6 +288,27 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="12" column="0">
|
||||||
|
<widget class="QLabel" name="label_console_id">
|
||||||
|
<property name="text">
|
||||||
|
<string>Console ID:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="8" column="1">
|
||||||
|
<widget class="QDateTimeEdit" name="edit_init_time">
|
||||||
|
<property name="displayFormat">
|
||||||
|
<string>yyyy-MM-ddTHH:mm:ss</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QCheckBox" name="toggle_new_3ds">
|
||||||
|
<property name="text">
|
||||||
|
<string>Enable New 3DS mode</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
|
@ -0,0 +1,220 @@
|
||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include "citra_qt/dumping/dumping_dialog.h"
|
||||||
|
#include "citra_qt/dumping/options_dialog.h"
|
||||||
|
#include "citra_qt/uisettings.h"
|
||||||
|
#include "core/settings.h"
|
||||||
|
#include "ui_dumping_dialog.h"
|
||||||
|
|
||||||
|
DumpingDialog::DumpingDialog(QWidget* parent)
|
||||||
|
: QDialog(parent), ui(std::make_unique<Ui::DumpingDialog>()) {
|
||||||
|
|
||||||
|
ui->setupUi(this);
|
||||||
|
|
||||||
|
format_generic_options = VideoDumper::GetFormatGenericOptions();
|
||||||
|
encoder_generic_options = VideoDumper::GetEncoderGenericOptions();
|
||||||
|
|
||||||
|
connect(ui->pathExplore, &QToolButton::clicked, this, &DumpingDialog::OnToolButtonClicked);
|
||||||
|
connect(ui->buttonBox, &QDialogButtonBox::accepted, [this] {
|
||||||
|
if (ui->pathLineEdit->text().isEmpty()) {
|
||||||
|
QMessageBox::critical(this, tr("Citra"), tr("Please specify the output path."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ApplyConfiguration();
|
||||||
|
accept();
|
||||||
|
});
|
||||||
|
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &DumpingDialog::reject);
|
||||||
|
connect(ui->formatOptionsButton, &QToolButton::clicked, [this] {
|
||||||
|
OpenOptionsDialog(formats.at(ui->formatComboBox->currentData().toUInt()).options,
|
||||||
|
format_generic_options, ui->formatOptionsLineEdit);
|
||||||
|
});
|
||||||
|
connect(ui->videoEncoderOptionsButton, &QToolButton::clicked, [this] {
|
||||||
|
OpenOptionsDialog(
|
||||||
|
video_encoders.at(ui->videoEncoderComboBox->currentData().toUInt()).options,
|
||||||
|
encoder_generic_options, ui->videoEncoderOptionsLineEdit);
|
||||||
|
});
|
||||||
|
connect(ui->audioEncoderOptionsButton, &QToolButton::clicked, [this] {
|
||||||
|
OpenOptionsDialog(
|
||||||
|
audio_encoders.at(ui->audioEncoderComboBox->currentData().toUInt()).options,
|
||||||
|
encoder_generic_options, ui->audioEncoderOptionsLineEdit);
|
||||||
|
});
|
||||||
|
|
||||||
|
SetConfiguration();
|
||||||
|
|
||||||
|
connect(ui->formatComboBox, qOverload<int>(&QComboBox::currentIndexChanged), [this] {
|
||||||
|
ui->pathLineEdit->setText(QString{});
|
||||||
|
ui->formatOptionsLineEdit->clear();
|
||||||
|
PopulateEncoders();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(ui->videoEncoderComboBox, qOverload<int>(&QComboBox::currentIndexChanged),
|
||||||
|
[this] { ui->videoEncoderOptionsLineEdit->clear(); });
|
||||||
|
connect(ui->audioEncoderComboBox, qOverload<int>(&QComboBox::currentIndexChanged),
|
||||||
|
[this] { ui->audioEncoderOptionsLineEdit->clear(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
DumpingDialog::~DumpingDialog() = default;
|
||||||
|
|
||||||
|
QString DumpingDialog::GetFilePath() const {
|
||||||
|
return ui->pathLineEdit->text();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DumpingDialog::Populate() {
|
||||||
|
formats = VideoDumper::ListFormats();
|
||||||
|
video_encoders = VideoDumper::ListEncoders(AVMEDIA_TYPE_VIDEO);
|
||||||
|
audio_encoders = VideoDumper::ListEncoders(AVMEDIA_TYPE_AUDIO);
|
||||||
|
|
||||||
|
// Check that these are not empty
|
||||||
|
QString missing;
|
||||||
|
if (formats.empty()) {
|
||||||
|
missing = tr("output formats");
|
||||||
|
}
|
||||||
|
if (video_encoders.empty()) {
|
||||||
|
missing = tr("video encoders");
|
||||||
|
}
|
||||||
|
if (audio_encoders.empty()) {
|
||||||
|
missing = tr("audio encoders");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!missing.isEmpty()) {
|
||||||
|
QMessageBox::critical(this, tr("Citra"),
|
||||||
|
tr("Could not find any available %1.\nPlease check your FFmpeg "
|
||||||
|
"installation used for compilation.")
|
||||||
|
.arg(missing));
|
||||||
|
reject();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate formats
|
||||||
|
for (std::size_t i = 0; i < formats.size(); ++i) {
|
||||||
|
const auto& format = formats[i];
|
||||||
|
|
||||||
|
// Check format: only formats that have video encoders and audio encoders are displayed
|
||||||
|
bool has_video = false;
|
||||||
|
for (const auto& video_encoder : video_encoders) {
|
||||||
|
if (format.supported_video_codecs.count(video_encoder.codec)) {
|
||||||
|
has_video = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!has_video)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bool has_audio = false;
|
||||||
|
for (const auto& audio_encoder : audio_encoders) {
|
||||||
|
if (format.supported_audio_codecs.count(audio_encoder.codec)) {
|
||||||
|
has_audio = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!has_audio)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ui->formatComboBox->addItem(tr("%1 (%2)").arg(QString::fromStdString(format.long_name),
|
||||||
|
QString::fromStdString(format.name)),
|
||||||
|
static_cast<unsigned long long>(i));
|
||||||
|
if (format.name == Settings::values.output_format) {
|
||||||
|
ui->formatComboBox->setCurrentIndex(ui->formatComboBox->count() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PopulateEncoders();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DumpingDialog::PopulateEncoders() {
|
||||||
|
const auto& format = formats.at(ui->formatComboBox->currentData().toUInt());
|
||||||
|
|
||||||
|
ui->videoEncoderComboBox->clear();
|
||||||
|
for (std::size_t i = 0; i < video_encoders.size(); ++i) {
|
||||||
|
const auto& video_encoder = video_encoders[i];
|
||||||
|
if (!format.supported_video_codecs.count(video_encoder.codec)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui->videoEncoderComboBox->addItem(
|
||||||
|
tr("%1 (%2)").arg(QString::fromStdString(video_encoder.long_name),
|
||||||
|
QString::fromStdString(video_encoder.name)),
|
||||||
|
static_cast<unsigned long long>(i));
|
||||||
|
if (video_encoder.name == Settings::values.video_encoder) {
|
||||||
|
ui->videoEncoderComboBox->setCurrentIndex(ui->videoEncoderComboBox->count() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui->audioEncoderComboBox->clear();
|
||||||
|
for (std::size_t i = 0; i < audio_encoders.size(); ++i) {
|
||||||
|
const auto& audio_encoder = audio_encoders[i];
|
||||||
|
if (!format.supported_audio_codecs.count(audio_encoder.codec)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui->audioEncoderComboBox->addItem(
|
||||||
|
tr("%1 (%2)").arg(QString::fromStdString(audio_encoder.long_name),
|
||||||
|
QString::fromStdString(audio_encoder.name)),
|
||||||
|
static_cast<unsigned long long>(i));
|
||||||
|
if (audio_encoder.name == Settings::values.audio_encoder) {
|
||||||
|
ui->audioEncoderComboBox->setCurrentIndex(ui->audioEncoderComboBox->count() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DumpingDialog::OnToolButtonClicked() {
|
||||||
|
const auto& format = formats.at(ui->formatComboBox->currentData().toUInt());
|
||||||
|
|
||||||
|
QString extensions;
|
||||||
|
for (const auto& ext : format.extensions) {
|
||||||
|
if (!extensions.isEmpty()) {
|
||||||
|
extensions.append(QLatin1Char{' '});
|
||||||
|
}
|
||||||
|
extensions.append(QStringLiteral("*.%1").arg(QString::fromStdString(ext)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto path = QFileDialog::getSaveFileName(
|
||||||
|
this, tr("Select Video Output Path"), last_path,
|
||||||
|
tr("%1 (%2)").arg(QString::fromStdString(format.long_name), extensions));
|
||||||
|
if (!path.isEmpty()) {
|
||||||
|
last_path = QFileInfo(ui->pathLineEdit->text()).path();
|
||||||
|
ui->pathLineEdit->setText(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DumpingDialog::OpenOptionsDialog(const std::vector<VideoDumper::OptionInfo>& specific_options,
|
||||||
|
const std::vector<VideoDumper::OptionInfo>& generic_options,
|
||||||
|
QLineEdit* line_edit) {
|
||||||
|
OptionsDialog dialog(this, specific_options, generic_options, line_edit->text().toStdString());
|
||||||
|
if (dialog.exec() != QDialog::DialogCode::Accepted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
line_edit->setText(QString::fromStdString(dialog.GetCurrentValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DumpingDialog::SetConfiguration() {
|
||||||
|
Populate();
|
||||||
|
|
||||||
|
ui->formatOptionsLineEdit->setText(QString::fromStdString(Settings::values.format_options));
|
||||||
|
ui->videoEncoderOptionsLineEdit->setText(
|
||||||
|
QString::fromStdString(Settings::values.video_encoder_options));
|
||||||
|
ui->audioEncoderOptionsLineEdit->setText(
|
||||||
|
QString::fromStdString(Settings::values.audio_encoder_options));
|
||||||
|
last_path = UISettings::values.video_dumping_path;
|
||||||
|
ui->videoBitrateSpinBox->setValue(static_cast<int>(Settings::values.video_bitrate));
|
||||||
|
ui->audioBitrateSpinBox->setValue(static_cast<int>(Settings::values.audio_bitrate));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DumpingDialog::ApplyConfiguration() {
|
||||||
|
Settings::values.output_format = formats.at(ui->formatComboBox->currentData().toUInt()).name;
|
||||||
|
Settings::values.format_options = ui->formatOptionsLineEdit->text().toStdString();
|
||||||
|
Settings::values.video_encoder =
|
||||||
|
video_encoders.at(ui->videoEncoderComboBox->currentData().toUInt()).name;
|
||||||
|
Settings::values.video_encoder_options = ui->videoEncoderOptionsLineEdit->text().toStdString();
|
||||||
|
Settings::values.video_bitrate = ui->videoBitrateSpinBox->value();
|
||||||
|
Settings::values.audio_encoder =
|
||||||
|
audio_encoders.at(ui->audioEncoderComboBox->currentData().toUInt()).name;
|
||||||
|
Settings::values.audio_encoder_options = ui->audioEncoderOptionsLineEdit->text().toStdString();
|
||||||
|
Settings::values.audio_bitrate = ui->audioBitrateSpinBox->value();
|
||||||
|
UISettings::values.video_dumping_path = last_path;
|
||||||
|
Settings::Apply();
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <QDialog>
|
||||||
|
#include "core/dumping/ffmpeg_backend.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class DumpingDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
class QLineEdit;
|
||||||
|
|
||||||
|
class DumpingDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DumpingDialog(QWidget* parent);
|
||||||
|
~DumpingDialog() override;
|
||||||
|
|
||||||
|
QString GetFilePath() const;
|
||||||
|
void ApplyConfiguration();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Populate();
|
||||||
|
void PopulateEncoders();
|
||||||
|
void SetConfiguration();
|
||||||
|
void OnToolButtonClicked();
|
||||||
|
void OpenOptionsDialog(const std::vector<VideoDumper::OptionInfo>& specific_options,
|
||||||
|
const std::vector<VideoDumper::OptionInfo>& generic_options,
|
||||||
|
QLineEdit* line_edit);
|
||||||
|
|
||||||
|
std::unique_ptr<Ui::DumpingDialog> ui;
|
||||||
|
|
||||||
|
QString last_path;
|
||||||
|
|
||||||
|
std::vector<VideoDumper::FormatInfo> formats;
|
||||||
|
std::vector<VideoDumper::OptionInfo> format_generic_options;
|
||||||
|
std::vector<VideoDumper::EncoderInfo> video_encoders;
|
||||||
|
std::vector<VideoDumper::EncoderInfo> audio_encoders;
|
||||||
|
std::vector<VideoDumper::OptionInfo> encoder_generic_options;
|
||||||
|
};
|
|
@ -0,0 +1,213 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>DumpingDialog</class>
|
||||||
|
<widget class="QDialog" name="DumpingDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>600</width>
|
||||||
|
<height>420</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Dump Video</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox">
|
||||||
|
<property name="title">
|
||||||
|
<string>Output</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Format:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QComboBox" name="formatComboBox"/>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Options:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QLineEdit" name="formatOptionsLineEdit"/>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QToolButton" name="formatOptionsButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Path:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QLineEdit" name="pathLineEdit"/>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QToolButton" name="pathExplore">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox">
|
||||||
|
<property name="title">
|
||||||
|
<string>Video</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Encoder:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QComboBox" name="videoEncoderComboBox">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Options:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QLineEdit" name="videoEncoderOptionsLineEdit"/>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QToolButton" name="videoEncoderOptionsButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Bitrate:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QSpinBox" name="videoBitrateSpinBox">
|
||||||
|
<property name="maximum">
|
||||||
|
<number>10000000</number>
|
||||||
|
</property>
|
||||||
|
<property name="singleStep">
|
||||||
|
<number>1000</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>bps</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox">
|
||||||
|
<property name="title">
|
||||||
|
<string>Audio</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Encoder:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QComboBox" name="audioEncoderComboBox">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Options:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QLineEdit" name="audioEncoderOptionsLineEdit"/>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QToolButton" name="audioEncoderOptionsButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Bitrate:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QSpinBox" name="audioBitrateSpinBox">
|
||||||
|
<property name="maximum">
|
||||||
|
<number>1000000</number>
|
||||||
|
</property>
|
||||||
|
<property name="singleStep">
|
||||||
|
<number>100</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>bps</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</ui>
|
|
@ -0,0 +1,299 @@
|
||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QStringList>
|
||||||
|
#include "citra_qt/dumping/option_set_dialog.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "common/string_util.h"
|
||||||
|
#include "ui_option_set_dialog.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <libavutil/pixdesc.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
static const std::unordered_map<AVOptionType, const char*> TypeNameMap{{
|
||||||
|
{AV_OPT_TYPE_BOOL, QT_TR_NOOP("boolean")},
|
||||||
|
{AV_OPT_TYPE_FLAGS, QT_TR_NOOP("flags")},
|
||||||
|
{AV_OPT_TYPE_DURATION, QT_TR_NOOP("duration")},
|
||||||
|
{AV_OPT_TYPE_INT, QT_TR_NOOP("int")},
|
||||||
|
{AV_OPT_TYPE_UINT64, QT_TR_NOOP("uint64")},
|
||||||
|
{AV_OPT_TYPE_INT64, QT_TR_NOOP("int64")},
|
||||||
|
{AV_OPT_TYPE_DOUBLE, QT_TR_NOOP("double")},
|
||||||
|
{AV_OPT_TYPE_FLOAT, QT_TR_NOOP("float")},
|
||||||
|
{AV_OPT_TYPE_RATIONAL, QT_TR_NOOP("rational")},
|
||||||
|
{AV_OPT_TYPE_PIXEL_FMT, QT_TR_NOOP("pixel format")},
|
||||||
|
{AV_OPT_TYPE_SAMPLE_FMT, QT_TR_NOOP("sample format")},
|
||||||
|
{AV_OPT_TYPE_COLOR, QT_TR_NOOP("color")},
|
||||||
|
{AV_OPT_TYPE_IMAGE_SIZE, QT_TR_NOOP("image size")},
|
||||||
|
{AV_OPT_TYPE_STRING, QT_TR_NOOP("string")},
|
||||||
|
{AV_OPT_TYPE_DICT, QT_TR_NOOP("dictionary")},
|
||||||
|
{AV_OPT_TYPE_VIDEO_RATE, QT_TR_NOOP("video rate")},
|
||||||
|
{AV_OPT_TYPE_CHANNEL_LAYOUT, QT_TR_NOOP("channel layout")},
|
||||||
|
}};
|
||||||
|
|
||||||
|
static const std::unordered_map<AVOptionType, const char*> TypeDescriptionMap{{
|
||||||
|
{AV_OPT_TYPE_DURATION, QT_TR_NOOP("[<hours (integer)>:][<minutes (integer):]<seconds "
|
||||||
|
"(decimal)> e.g. 03:00.5 (3min 500ms)")},
|
||||||
|
{AV_OPT_TYPE_RATIONAL, QT_TR_NOOP("<num>/<den>")},
|
||||||
|
{AV_OPT_TYPE_COLOR, QT_TR_NOOP("0xRRGGBBAA")},
|
||||||
|
{AV_OPT_TYPE_IMAGE_SIZE, QT_TR_NOOP("<width>x<height>, or preset values like 'vga'.")},
|
||||||
|
{AV_OPT_TYPE_DICT,
|
||||||
|
QT_TR_NOOP("Comma-splitted list of <key>=<value>. Do not put spaces.")},
|
||||||
|
{AV_OPT_TYPE_VIDEO_RATE, QT_TR_NOOP("<num>/<den>, or preset values like 'pal'.")},
|
||||||
|
{AV_OPT_TYPE_CHANNEL_LAYOUT, QT_TR_NOOP("Hexadecimal channel layout mask starting with '0x'.")},
|
||||||
|
}};
|
||||||
|
|
||||||
|
/// Get the preset values of an option. returns {display value, real value}
|
||||||
|
std::vector<std::pair<QString, QString>> GetPresetValues(const VideoDumper::OptionInfo& option) {
|
||||||
|
switch (option.type) {
|
||||||
|
case AV_OPT_TYPE_BOOL: {
|
||||||
|
return {{QObject::tr("auto"), QStringLiteral("auto")},
|
||||||
|
{QObject::tr("true"), QStringLiteral("true")},
|
||||||
|
{QObject::tr("false"), QStringLiteral("false")}};
|
||||||
|
}
|
||||||
|
case AV_OPT_TYPE_PIXEL_FMT: {
|
||||||
|
std::vector<std::pair<QString, QString>> out{{QObject::tr("none"), QStringLiteral("none")}};
|
||||||
|
// List all pixel formats
|
||||||
|
const AVPixFmtDescriptor* current = nullptr;
|
||||||
|
while ((current = av_pix_fmt_desc_next(current))) {
|
||||||
|
out.emplace_back(QString::fromUtf8(current->name), QString::fromUtf8(current->name));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
case AV_OPT_TYPE_SAMPLE_FMT: {
|
||||||
|
std::vector<std::pair<QString, QString>> out{{QObject::tr("none"), QStringLiteral("none")}};
|
||||||
|
// List all sample formats
|
||||||
|
int current = 0;
|
||||||
|
while (true) {
|
||||||
|
const char* name = av_get_sample_fmt_name(static_cast<AVSampleFormat>(current));
|
||||||
|
if (name == nullptr)
|
||||||
|
break;
|
||||||
|
out.emplace_back(QString::fromUtf8(name), QString::fromUtf8(name));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
case AV_OPT_TYPE_INT:
|
||||||
|
case AV_OPT_TYPE_INT64:
|
||||||
|
case AV_OPT_TYPE_UINT64: {
|
||||||
|
std::vector<std::pair<QString, QString>> out;
|
||||||
|
// Add in all named constants
|
||||||
|
for (const auto& constant : option.named_constants) {
|
||||||
|
out.emplace_back(QObject::tr("%1 (0x%2)")
|
||||||
|
.arg(QString::fromStdString(constant.name))
|
||||||
|
.arg(constant.value, 0, 16),
|
||||||
|
QString::fromStdString(constant.name));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionSetDialog::InitializeUI(const std::string& initial_value) {
|
||||||
|
const QString type_name =
|
||||||
|
TypeNameMap.count(option.type) ? tr(TypeNameMap.at(option.type)) : tr("unknown");
|
||||||
|
ui->nameLabel->setText(tr("%1 <%2> %3")
|
||||||
|
.arg(QString::fromStdString(option.name), type_name,
|
||||||
|
QString::fromStdString(option.description)));
|
||||||
|
if (TypeDescriptionMap.count(option.type)) {
|
||||||
|
ui->formatLabel->setVisible(true);
|
||||||
|
ui->formatLabel->setText(tr(TypeDescriptionMap.at(option.type)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.type == AV_OPT_TYPE_INT || option.type == AV_OPT_TYPE_INT64 ||
|
||||||
|
option.type == AV_OPT_TYPE_UINT64 || option.type == AV_OPT_TYPE_FLOAT ||
|
||||||
|
option.type == AV_OPT_TYPE_DOUBLE || option.type == AV_OPT_TYPE_DURATION ||
|
||||||
|
option.type == AV_OPT_TYPE_RATIONAL) { // scalar types
|
||||||
|
|
||||||
|
ui->formatLabel->setVisible(true);
|
||||||
|
if (!ui->formatLabel->text().isEmpty()) {
|
||||||
|
ui->formatLabel->text().append(QStringLiteral("\n"));
|
||||||
|
}
|
||||||
|
ui->formatLabel->setText(
|
||||||
|
ui->formatLabel->text().append(tr("Range: %1 - %2").arg(option.min).arg(option.max)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide and initialize layout
|
||||||
|
if (option.type == AV_OPT_TYPE_BOOL || option.type == AV_OPT_TYPE_PIXEL_FMT ||
|
||||||
|
option.type == AV_OPT_TYPE_SAMPLE_FMT ||
|
||||||
|
((option.type == AV_OPT_TYPE_INT || option.type == AV_OPT_TYPE_INT64 ||
|
||||||
|
option.type == AV_OPT_TYPE_UINT64) &&
|
||||||
|
!option.named_constants.empty())) { // Use the combobox layout
|
||||||
|
|
||||||
|
layout_type = 1;
|
||||||
|
ui->comboBox->setVisible(true);
|
||||||
|
ui->comboBoxHelpLabel->setVisible(true);
|
||||||
|
|
||||||
|
QString real_initial_value = QString::fromStdString(initial_value);
|
||||||
|
if (option.type == AV_OPT_TYPE_INT || option.type == AV_OPT_TYPE_INT64 ||
|
||||||
|
option.type == AV_OPT_TYPE_UINT64) {
|
||||||
|
|
||||||
|
// Get the name of the initial value
|
||||||
|
try {
|
||||||
|
s64 initial_value_integer = std::stoll(initial_value, nullptr, 0);
|
||||||
|
for (const auto& constant : option.named_constants) {
|
||||||
|
if (constant.value == initial_value_integer) {
|
||||||
|
real_initial_value = QString::fromStdString(constant.name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
// Not convertible to integer, ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
for (const auto& [display, value] : GetPresetValues(option)) {
|
||||||
|
ui->comboBox->addItem(display, value);
|
||||||
|
if (value == real_initial_value) {
|
||||||
|
found = true;
|
||||||
|
ui->comboBox->setCurrentIndex(ui->comboBox->count() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui->comboBox->addItem(tr("custom"));
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
ui->comboBox->setCurrentIndex(ui->comboBox->count() - 1);
|
||||||
|
ui->lineEdit->setText(QString::fromStdString(initial_value));
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateUIDisplay();
|
||||||
|
|
||||||
|
connect(ui->comboBox, &QComboBox::currentTextChanged, this,
|
||||||
|
&OptionSetDialog::UpdateUIDisplay);
|
||||||
|
} else if (option.type == AV_OPT_TYPE_FLAGS &&
|
||||||
|
!option.named_constants.empty()) { // Use the check boxes layout
|
||||||
|
|
||||||
|
layout_type = 2;
|
||||||
|
|
||||||
|
for (const auto& constant : option.named_constants) {
|
||||||
|
auto* checkBox = new QCheckBox(tr("%1 (0x%2) %3")
|
||||||
|
.arg(QString::fromStdString(constant.name))
|
||||||
|
.arg(constant.value, 0, 16)
|
||||||
|
.arg(QString::fromStdString(constant.description)));
|
||||||
|
checkBox->setProperty("value", static_cast<unsigned long long>(constant.value));
|
||||||
|
checkBox->setProperty("name", QString::fromStdString(constant.name));
|
||||||
|
ui->checkBoxLayout->addWidget(checkBox);
|
||||||
|
}
|
||||||
|
SetCheckBoxDefaults(initial_value);
|
||||||
|
} else { // Use the line edit layout
|
||||||
|
layout_type = 0;
|
||||||
|
ui->lineEdit->setVisible(true);
|
||||||
|
ui->lineEdit->setText(QString::fromStdString(initial_value));
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionSetDialog::SetCheckBoxDefaults(const std::string& initial_value) {
|
||||||
|
if (initial_value.size() >= 2 &&
|
||||||
|
(initial_value.substr(0, 2) == "0x" || initial_value.substr(0, 2) == "0X")) {
|
||||||
|
// This is a hex mask
|
||||||
|
try {
|
||||||
|
u64 value = std::stoull(initial_value, nullptr, 16);
|
||||||
|
for (int i = 0; i < ui->checkBoxLayout->count(); ++i) {
|
||||||
|
auto* checkBox = qobject_cast<QCheckBox*>(ui->checkBoxLayout->itemAt(i)->widget());
|
||||||
|
if (checkBox) {
|
||||||
|
checkBox->setChecked(value & checkBox->property("value").toULongLong());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
LOG_ERROR(Frontend, "Could not convert {} to number", initial_value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This is a combination of constants, splitted with + or |
|
||||||
|
std::vector<std::string> tmp;
|
||||||
|
Common::SplitString(initial_value, '+', tmp);
|
||||||
|
|
||||||
|
std::vector<std::string> out;
|
||||||
|
std::vector<std::string> tmp2;
|
||||||
|
for (const auto& str : tmp) {
|
||||||
|
Common::SplitString(str, '|', tmp2);
|
||||||
|
out.insert(out.end(), tmp2.begin(), tmp2.end());
|
||||||
|
}
|
||||||
|
for (int i = 0; i < ui->checkBoxLayout->count(); ++i) {
|
||||||
|
auto* checkBox = qobject_cast<QCheckBox*>(ui->checkBoxLayout->itemAt(i)->widget());
|
||||||
|
if (checkBox) {
|
||||||
|
checkBox->setChecked(
|
||||||
|
std::find(out.begin(), out.end(),
|
||||||
|
checkBox->property("name").toString().toStdString()) != out.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionSetDialog::UpdateUIDisplay() {
|
||||||
|
if (layout_type != 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (ui->comboBox->currentIndex() == ui->comboBox->count() - 1) { // custom
|
||||||
|
ui->comboBoxHelpLabel->setVisible(false);
|
||||||
|
ui->lineEdit->setVisible(true);
|
||||||
|
adjustSize();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui->lineEdit->setVisible(false);
|
||||||
|
for (const auto& constant : option.named_constants) {
|
||||||
|
if (constant.name == ui->comboBox->currentData().toString().toStdString()) {
|
||||||
|
ui->comboBoxHelpLabel->setVisible(true);
|
||||||
|
ui->comboBoxHelpLabel->setText(QString::fromStdString(constant.description));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<bool, std::string> OptionSetDialog::GetCurrentValue() {
|
||||||
|
if (!is_set) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (layout_type) {
|
||||||
|
case 0: // line edit layout
|
||||||
|
return {true, ui->lineEdit->text().toStdString()};
|
||||||
|
case 1: // combo box layout
|
||||||
|
if (ui->comboBox->currentIndex() == ui->comboBox->count() - 1) {
|
||||||
|
return {true, ui->lineEdit->text().toStdString()}; // custom
|
||||||
|
}
|
||||||
|
return {true, ui->comboBox->currentData().toString().toStdString()};
|
||||||
|
case 2: { // check boxes layout
|
||||||
|
std::string out;
|
||||||
|
for (int i = 0; i < ui->checkBoxLayout->count(); ++i) {
|
||||||
|
auto* checkBox = qobject_cast<QCheckBox*>(ui->checkBoxLayout->itemAt(i)->widget());
|
||||||
|
if (checkBox && checkBox->isChecked()) {
|
||||||
|
if (!out.empty()) {
|
||||||
|
out.append("+");
|
||||||
|
}
|
||||||
|
out.append(checkBox->property("name").toString().toStdString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (out.empty()) {
|
||||||
|
out = "0x0";
|
||||||
|
}
|
||||||
|
return {true, out};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionSetDialog::OptionSetDialog(QWidget* parent, VideoDumper::OptionInfo option_,
|
||||||
|
const std::string& initial_value)
|
||||||
|
: QDialog(parent), ui(std::make_unique<Ui::OptionSetDialog>()), option(std::move(option_)) {
|
||||||
|
|
||||||
|
ui->setupUi(this);
|
||||||
|
InitializeUI(initial_value);
|
||||||
|
|
||||||
|
connect(ui->unsetButton, &QPushButton::clicked, [this] {
|
||||||
|
is_set = false;
|
||||||
|
accept();
|
||||||
|
});
|
||||||
|
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &OptionSetDialog::accept);
|
||||||
|
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &OptionSetDialog::reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionSetDialog::~OptionSetDialog() = default;
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <QDialog>
|
||||||
|
#include "core/dumping/ffmpeg_backend.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class OptionSetDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
class OptionSetDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit OptionSetDialog(QWidget* parent, VideoDumper::OptionInfo option,
|
||||||
|
const std::string& initial_value);
|
||||||
|
~OptionSetDialog() override;
|
||||||
|
|
||||||
|
// {is_set, value}
|
||||||
|
std::pair<bool, std::string> GetCurrentValue();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void InitializeUI(const std::string& initial_value);
|
||||||
|
void SetCheckBoxDefaults(const std::string& initial_value);
|
||||||
|
void UpdateUIDisplay();
|
||||||
|
|
||||||
|
std::unique_ptr<Ui::OptionSetDialog> ui;
|
||||||
|
VideoDumper::OptionInfo option;
|
||||||
|
bool is_set = true;
|
||||||
|
int layout_type = -1; // 0 - line edit, 1 - combo box, 2 - flags (check boxes)
|
||||||
|
};
|
|
@ -0,0 +1,89 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>OptionSetDialog</class>
|
||||||
|
<widget class="QDialog" name="OptionSetDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>600</width>
|
||||||
|
<height>150</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Options</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="nameLabel"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="formatLabel">
|
||||||
|
<property name="visible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="comboBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="comboBox">
|
||||||
|
<property name="visible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="comboBoxHelpLabel">
|
||||||
|
<property name="visible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="lineEdit">
|
||||||
|
<property name="visible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="checkBoxLayout"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer>
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="unsetButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Unset</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer>
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</ui>
|
|
@ -0,0 +1,68 @@
|
||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <QTreeWidgetItem>
|
||||||
|
#include "citra_qt/dumping/option_set_dialog.h"
|
||||||
|
#include "citra_qt/dumping/options_dialog.h"
|
||||||
|
#include "ui_options_dialog.h"
|
||||||
|
|
||||||
|
constexpr char UNSET_TEXT[] = QT_TR_NOOP("[not set]");
|
||||||
|
|
||||||
|
void OptionsDialog::PopulateOptions() {
|
||||||
|
const auto& options = ui->specificRadioButton->isChecked() ? specific_options : generic_options;
|
||||||
|
ui->main->clear();
|
||||||
|
ui->main->setSortingEnabled(false);
|
||||||
|
for (std::size_t i = 0; i < options.size(); ++i) {
|
||||||
|
const auto& option = options.at(i);
|
||||||
|
auto* item = new QTreeWidgetItem(
|
||||||
|
{QString::fromStdString(option.name), QString::fromStdString(current_values.Get(
|
||||||
|
option.name, tr(UNSET_TEXT).toStdString()))});
|
||||||
|
item->setData(1, Qt::UserRole, static_cast<unsigned long long>(i)); // ID
|
||||||
|
ui->main->addTopLevelItem(item);
|
||||||
|
}
|
||||||
|
ui->main->setSortingEnabled(true);
|
||||||
|
ui->main->sortItems(0, Qt::AscendingOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionsDialog::OnSetOptionValue(QTreeWidgetItem* item) {
|
||||||
|
const auto& options = ui->specificRadioButton->isChecked() ? specific_options : generic_options;
|
||||||
|
const int id = item->data(1, Qt::UserRole).toInt();
|
||||||
|
OptionSetDialog dialog(this, options[id],
|
||||||
|
current_values.Get(options[id].name, options[id].default_value));
|
||||||
|
if (dialog.exec() != QDialog::DialogCode::Accepted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& [is_set, value] = dialog.GetCurrentValue();
|
||||||
|
if (is_set) {
|
||||||
|
current_values.Set(options[id].name, value);
|
||||||
|
} else {
|
||||||
|
current_values.Erase(options[id].name);
|
||||||
|
}
|
||||||
|
item->setText(1, is_set ? QString::fromStdString(value) : tr(UNSET_TEXT));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string OptionsDialog::GetCurrentValue() const {
|
||||||
|
return current_values.Serialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionsDialog::OptionsDialog(QWidget* parent,
|
||||||
|
std::vector<VideoDumper::OptionInfo> specific_options_,
|
||||||
|
std::vector<VideoDumper::OptionInfo> generic_options_,
|
||||||
|
const std::string& current_value)
|
||||||
|
: QDialog(parent), ui(std::make_unique<Ui::OptionsDialog>()),
|
||||||
|
specific_options(std::move(specific_options_)), generic_options(std::move(generic_options_)),
|
||||||
|
current_values(current_value) {
|
||||||
|
|
||||||
|
ui->setupUi(this);
|
||||||
|
PopulateOptions();
|
||||||
|
|
||||||
|
connect(ui->main, &QTreeWidget::itemDoubleClicked,
|
||||||
|
[this](QTreeWidgetItem* item, int column) { OnSetOptionValue(item); });
|
||||||
|
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &OptionsDialog::accept);
|
||||||
|
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &OptionsDialog::reject);
|
||||||
|
connect(ui->specificRadioButton, &QRadioButton::toggled, this, &OptionsDialog::PopulateOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionsDialog::~OptionsDialog() = default;
|
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
#include <QDialog>
|
||||||
|
#include "common/param_package.h"
|
||||||
|
#include "core/dumping/ffmpeg_backend.h"
|
||||||
|
|
||||||
|
class QTreeWidgetItem;
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class OptionsDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
class OptionsDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit OptionsDialog(QWidget* parent, std::vector<VideoDumper::OptionInfo> specific_options,
|
||||||
|
std::vector<VideoDumper::OptionInfo> generic_options,
|
||||||
|
const std::string& current_value);
|
||||||
|
~OptionsDialog() override;
|
||||||
|
|
||||||
|
std::string GetCurrentValue() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void PopulateOptions();
|
||||||
|
void OnSetOptionValue(QTreeWidgetItem* item);
|
||||||
|
|
||||||
|
std::unique_ptr<Ui::OptionsDialog> ui;
|
||||||
|
std::vector<VideoDumper::OptionInfo> specific_options;
|
||||||
|
std::vector<VideoDumper::OptionInfo> generic_options;
|
||||||
|
Common::ParamPackage current_values;
|
||||||
|
};
|
|
@ -0,0 +1,71 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>OptionsDialog</class>
|
||||||
|
<widget class="QDialog" name="OptionsDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>650</width>
|
||||||
|
<height>350</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Options</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Double click to see the description and change the values of the options.</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="specificRadioButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Specific</string>
|
||||||
|
</property>
|
||||||
|
<property name="checked">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="genericRadioButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Generic</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QTreeWidget" name="main">
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>Name</string>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>Value</string>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</ui>
|
|
@ -89,6 +89,10 @@
|
||||||
#include "citra_qt/discord_impl.h"
|
#include "citra_qt/discord_impl.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
|
||||||
|
#include "citra_qt/dumping/dumping_dialog.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef QT_STATICPLUGIN
|
#ifdef QT_STATICPLUGIN
|
||||||
Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin);
|
Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin);
|
||||||
#endif
|
#endif
|
||||||
|
@ -100,6 +104,8 @@ __declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
constexpr int default_mouse_timeout = 2500;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "Callouts" are one-time instructional messages shown to the user. In the config settings, there
|
* "Callouts" are one-time instructional messages shown to the user. In the config settings, there
|
||||||
* is a bitfield "callout_flags" options, used to track if a message has already been shown to the
|
* is a bitfield "callout_flags" options, used to track if a message has already been shown to the
|
||||||
|
@ -193,6 +199,14 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
|
||||||
// Show one-time "callout" messages to the user
|
// Show one-time "callout" messages to the user
|
||||||
ShowTelemetryCallout();
|
ShowTelemetryCallout();
|
||||||
|
|
||||||
|
// make sure menubar has the arrow cursor instead of inheriting from this
|
||||||
|
ui.menubar->setCursor(QCursor());
|
||||||
|
statusBar()->setCursor(QCursor());
|
||||||
|
|
||||||
|
mouse_hide_timer.setInterval(default_mouse_timeout);
|
||||||
|
connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor);
|
||||||
|
connect(ui.menubar, &QMenuBar::hovered, this, &GMainWindow::ShowMouseCursor);
|
||||||
|
|
||||||
if (UISettings::values.check_for_update_on_start) {
|
if (UISettings::values.check_for_update_on_start) {
|
||||||
CheckForUpdates();
|
CheckForUpdates();
|
||||||
}
|
}
|
||||||
|
@ -713,9 +727,7 @@ void GMainWindow::ConnectMenuEvents() {
|
||||||
connect(ui.action_Capture_Screenshot, &QAction::triggered, this,
|
connect(ui.action_Capture_Screenshot, &QAction::triggered, this,
|
||||||
&GMainWindow::OnCaptureScreenshot);
|
&GMainWindow::OnCaptureScreenshot);
|
||||||
|
|
||||||
#ifndef ENABLE_FFMPEG_VIDEO_DUMPER
|
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
|
||||||
ui.action_Dump_Video->setEnabled(false);
|
|
||||||
#endif
|
|
||||||
connect(ui.action_Dump_Video, &QAction::triggered, [this] {
|
connect(ui.action_Dump_Video, &QAction::triggered, [this] {
|
||||||
if (ui.action_Dump_Video->isChecked()) {
|
if (ui.action_Dump_Video->isChecked()) {
|
||||||
OnStartVideoDumping();
|
OnStartVideoDumping();
|
||||||
|
@ -723,6 +735,9 @@ void GMainWindow::ConnectMenuEvents() {
|
||||||
OnStopVideoDumping();
|
OnStopVideoDumping();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
#else
|
||||||
|
ui.action_Dump_Video->setEnabled(false);
|
||||||
|
#endif
|
||||||
|
|
||||||
// Help
|
// Help
|
||||||
connect(ui.action_Open_Citra_Folder, &QAction::triggered, this,
|
connect(ui.action_Open_Citra_Folder, &QAction::triggered, this,
|
||||||
|
@ -994,6 +1009,13 @@ void GMainWindow::BootGame(const QString& filename) {
|
||||||
}
|
}
|
||||||
status_bar_update_timer.start(2000);
|
status_bar_update_timer.start(2000);
|
||||||
|
|
||||||
|
if (UISettings::values.hide_mouse) {
|
||||||
|
mouse_hide_timer.start();
|
||||||
|
setMouseTracking(true);
|
||||||
|
ui.centralwidget->setMouseTracking(true);
|
||||||
|
ui.menubar->setMouseTracking(true);
|
||||||
|
}
|
||||||
|
|
||||||
// show and hide the render_window to create the context
|
// show and hide the render_window to create the context
|
||||||
render_window->show();
|
render_window->show();
|
||||||
render_window->hide();
|
render_window->hide();
|
||||||
|
@ -1009,8 +1031,14 @@ void GMainWindow::BootGame(const QString& filename) {
|
||||||
if (video_dumping_on_start) {
|
if (video_dumping_on_start) {
|
||||||
Layout::FramebufferLayout layout{
|
Layout::FramebufferLayout layout{
|
||||||
Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())};
|
Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())};
|
||||||
Core::System::GetInstance().VideoDumper().StartDumping(video_dumping_path.toStdString(),
|
if (!Core::System::GetInstance().VideoDumper().StartDumping(
|
||||||
"webm", layout);
|
video_dumping_path.toStdString(), layout)) {
|
||||||
|
|
||||||
|
QMessageBox::critical(
|
||||||
|
this, tr("Citra"),
|
||||||
|
tr("Could not start video dumping.<br>Refer to the log for details."));
|
||||||
|
ui.action_Dump_Video->setChecked(false);
|
||||||
|
}
|
||||||
video_dumping_on_start = false;
|
video_dumping_on_start = false;
|
||||||
video_dumping_path.clear();
|
video_dumping_path.clear();
|
||||||
}
|
}
|
||||||
|
@ -1026,11 +1054,13 @@ void GMainWindow::ShutdownGame() {
|
||||||
HideFullscreen();
|
HideFullscreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
|
||||||
if (Core::System::GetInstance().VideoDumper().IsDumping()) {
|
if (Core::System::GetInstance().VideoDumper().IsDumping()) {
|
||||||
game_shutdown_delayed = true;
|
game_shutdown_delayed = true;
|
||||||
OnStopVideoDumping();
|
OnStopVideoDumping();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
AllowOSSleep();
|
AllowOSSleep();
|
||||||
|
|
||||||
|
@ -1084,6 +1114,10 @@ void GMainWindow::ShutdownGame() {
|
||||||
game_list->show();
|
game_list->show();
|
||||||
game_list->setFilterFocus();
|
game_list->setFilterFocus();
|
||||||
|
|
||||||
|
setMouseTracking(false);
|
||||||
|
ui.centralwidget->setMouseTracking(false);
|
||||||
|
ui.menubar->setMouseTracking(false);
|
||||||
|
|
||||||
// Disable status bar updates
|
// Disable status bar updates
|
||||||
status_bar_update_timer.stop();
|
status_bar_update_timer.stop();
|
||||||
message_label->setVisible(false);
|
message_label->setVisible(false);
|
||||||
|
@ -1290,7 +1324,7 @@ void GMainWindow::OnGameListDumpRomFS(QString game_path, u64 program_id) {
|
||||||
using FutureWatcher = QFutureWatcher<std::pair<Loader::ResultStatus, Loader::ResultStatus>>;
|
using FutureWatcher = QFutureWatcher<std::pair<Loader::ResultStatus, Loader::ResultStatus>>;
|
||||||
auto* future_watcher = new FutureWatcher(this);
|
auto* future_watcher = new FutureWatcher(this);
|
||||||
connect(future_watcher, &FutureWatcher::finished,
|
connect(future_watcher, &FutureWatcher::finished,
|
||||||
[this, program_id, dialog, base_path, update_path, future_watcher] {
|
[this, dialog, base_path, update_path, future_watcher] {
|
||||||
dialog->hide();
|
dialog->hide();
|
||||||
const auto& [base, update] = future_watcher->result();
|
const auto& [base, update] = future_watcher->result();
|
||||||
if (base != Loader::ResultStatus::Success) {
|
if (base != Loader::ResultStatus::Success) {
|
||||||
|
@ -1676,6 +1710,16 @@ void GMainWindow::OnConfigure() {
|
||||||
SyncMenuUISettings();
|
SyncMenuUISettings();
|
||||||
game_list->RefreshGameDirectory();
|
game_list->RefreshGameDirectory();
|
||||||
config->Save();
|
config->Save();
|
||||||
|
if (UISettings::values.hide_mouse && emulation_running) {
|
||||||
|
setMouseTracking(true);
|
||||||
|
ui.centralwidget->setMouseTracking(true);
|
||||||
|
ui.menubar->setMouseTracking(true);
|
||||||
|
mouse_hide_timer.start();
|
||||||
|
} else {
|
||||||
|
setMouseTracking(false);
|
||||||
|
ui.centralwidget->setMouseTracking(false);
|
||||||
|
ui.menubar->setMouseTracking(false);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Settings::values.input_profiles = old_input_profiles;
|
Settings::values.input_profiles = old_input_profiles;
|
||||||
Settings::LoadProfile(old_input_profile_index);
|
Settings::LoadProfile(old_input_profile_index);
|
||||||
|
@ -1915,18 +1959,23 @@ void GMainWindow::OnCaptureScreenshot() {
|
||||||
OnStartGame();
|
OnStartGame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
|
||||||
void GMainWindow::OnStartVideoDumping() {
|
void GMainWindow::OnStartVideoDumping() {
|
||||||
const QString path = QFileDialog::getSaveFileName(
|
DumpingDialog dialog(this);
|
||||||
this, tr("Save Video"), UISettings::values.video_dumping_path, tr("WebM Videos (*.webm)"));
|
if (dialog.exec() != QDialog::DialogCode::Accepted) {
|
||||||
if (path.isEmpty()) {
|
|
||||||
ui.action_Dump_Video->setChecked(false);
|
ui.action_Dump_Video->setChecked(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UISettings::values.video_dumping_path = QFileInfo(path).path();
|
const auto path = dialog.GetFilePath();
|
||||||
if (emulation_running) {
|
if (emulation_running) {
|
||||||
Layout::FramebufferLayout layout{
|
Layout::FramebufferLayout layout{
|
||||||
Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())};
|
Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())};
|
||||||
Core::System::GetInstance().VideoDumper().StartDumping(path.toStdString(), "webm", layout);
|
if (!Core::System::GetInstance().VideoDumper().StartDumping(path.toStdString(), layout)) {
|
||||||
|
QMessageBox::critical(
|
||||||
|
this, tr("Citra"),
|
||||||
|
tr("Could not start video dumping.<br>Refer to the log for details."));
|
||||||
|
ui.action_Dump_Video->setChecked(false);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
video_dumping_on_start = true;
|
video_dumping_on_start = true;
|
||||||
video_dumping_path = path;
|
video_dumping_path = path;
|
||||||
|
@ -1943,6 +1992,8 @@ void GMainWindow::OnStopVideoDumping() {
|
||||||
const bool was_dumping = Core::System::GetInstance().VideoDumper().IsDumping();
|
const bool was_dumping = Core::System::GetInstance().VideoDumper().IsDumping();
|
||||||
if (!was_dumping)
|
if (!was_dumping)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
game_paused_for_dumping = emu_thread->IsRunning();
|
||||||
OnPauseGame();
|
OnPauseGame();
|
||||||
|
|
||||||
auto future =
|
auto future =
|
||||||
|
@ -1952,13 +2003,15 @@ void GMainWindow::OnStopVideoDumping() {
|
||||||
if (game_shutdown_delayed) {
|
if (game_shutdown_delayed) {
|
||||||
game_shutdown_delayed = false;
|
game_shutdown_delayed = false;
|
||||||
ShutdownGame();
|
ShutdownGame();
|
||||||
} else {
|
} else if (game_paused_for_dumping) {
|
||||||
|
game_paused_for_dumping = false;
|
||||||
OnStartGame();
|
OnStartGame();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
future_watcher->setFuture(future);
|
future_watcher->setFuture(future);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void GMainWindow::UpdateStatusBar() {
|
void GMainWindow::UpdateStatusBar() {
|
||||||
if (emu_thread == nullptr) {
|
if (emu_thread == nullptr) {
|
||||||
|
@ -1983,6 +2036,30 @@ void GMainWindow::UpdateStatusBar() {
|
||||||
emu_frametime_label->setVisible(true);
|
emu_frametime_label->setVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GMainWindow::HideMouseCursor() {
|
||||||
|
if (emu_thread == nullptr || UISettings::values.hide_mouse == false) {
|
||||||
|
mouse_hide_timer.stop();
|
||||||
|
ShowMouseCursor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCursor(QCursor(Qt::BlankCursor));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GMainWindow::ShowMouseCursor() {
|
||||||
|
unsetCursor();
|
||||||
|
if (emu_thread != nullptr && UISettings::values.hide_mouse) {
|
||||||
|
mouse_hide_timer.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GMainWindow::mouseMoveEvent(QMouseEvent* event) {
|
||||||
|
ShowMouseCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GMainWindow::mousePressEvent(QMouseEvent* event) {
|
||||||
|
ShowMouseCursor();
|
||||||
|
}
|
||||||
|
|
||||||
void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string details) {
|
void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string details) {
|
||||||
QString status_message;
|
QString status_message;
|
||||||
|
|
||||||
|
|
|
@ -207,8 +207,10 @@ private slots:
|
||||||
void OnPlayMovie();
|
void OnPlayMovie();
|
||||||
void OnStopRecordingPlayback();
|
void OnStopRecordingPlayback();
|
||||||
void OnCaptureScreenshot();
|
void OnCaptureScreenshot();
|
||||||
|
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
|
||||||
void OnStartVideoDumping();
|
void OnStartVideoDumping();
|
||||||
void OnStopVideoDumping();
|
void OnStopVideoDumping();
|
||||||
|
#endif
|
||||||
void OnCoreError(Core::System::ResultStatus, std::string);
|
void OnCoreError(Core::System::ResultStatus, std::string);
|
||||||
/// Called whenever a user selects Help->About Citra
|
/// Called whenever a user selects Help->About Citra
|
||||||
void OnMenuAboutCitra();
|
void OnMenuAboutCitra();
|
||||||
|
@ -225,6 +227,8 @@ private:
|
||||||
void UpdateWindowTitle();
|
void UpdateWindowTitle();
|
||||||
void RetranslateStatusBar();
|
void RetranslateStatusBar();
|
||||||
void InstallCIA(QStringList filepaths);
|
void InstallCIA(QStringList filepaths);
|
||||||
|
void HideMouseCursor();
|
||||||
|
void ShowMouseCursor();
|
||||||
|
|
||||||
Ui::MainWindow ui;
|
Ui::MainWindow ui;
|
||||||
|
|
||||||
|
@ -253,6 +257,7 @@ private:
|
||||||
QString game_path;
|
QString game_path;
|
||||||
|
|
||||||
bool auto_paused = false;
|
bool auto_paused = false;
|
||||||
|
QTimer mouse_hide_timer;
|
||||||
|
|
||||||
// Movie
|
// Movie
|
||||||
bool movie_record_on_start = false;
|
bool movie_record_on_start = false;
|
||||||
|
@ -263,6 +268,8 @@ private:
|
||||||
QString video_dumping_path;
|
QString video_dumping_path;
|
||||||
// Whether game shutdown is delayed due to video dumping
|
// Whether game shutdown is delayed due to video dumping
|
||||||
bool game_shutdown_delayed = false;
|
bool game_shutdown_delayed = false;
|
||||||
|
// Whether game was paused due to stopping video dumping
|
||||||
|
bool game_paused_for_dumping = false;
|
||||||
|
|
||||||
// Debugger panes
|
// Debugger panes
|
||||||
ProfilerWidget* profilerWidget;
|
ProfilerWidget* profilerWidget;
|
||||||
|
@ -301,6 +308,8 @@ protected:
|
||||||
void dropEvent(QDropEvent* event) override;
|
void dropEvent(QDropEvent* event) override;
|
||||||
void dragEnterEvent(QDragEnterEvent* event) override;
|
void dragEnterEvent(QDragEnterEvent* event) override;
|
||||||
void dragMoveEvent(QDragMoveEvent* event) override;
|
void dragMoveEvent(QDragMoveEvent* event) override;
|
||||||
|
void mouseMoveEvent(QMouseEvent* event) override;
|
||||||
|
void mousePressEvent(QMouseEvent* event) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(std::size_t);
|
Q_DECLARE_METATYPE(std::size_t);
|
||||||
|
|
|
@ -76,6 +76,7 @@ struct Values {
|
||||||
bool confirm_before_closing;
|
bool confirm_before_closing;
|
||||||
bool first_start;
|
bool first_start;
|
||||||
bool pause_when_in_background;
|
bool pause_when_in_background;
|
||||||
|
bool hide_mouse;
|
||||||
|
|
||||||
bool updater_found;
|
bool updater_found;
|
||||||
bool update_on_close;
|
bool update_on_close;
|
||||||
|
|
|
@ -135,4 +135,20 @@ void ParamPackage::Clear() {
|
||||||
data.clear();
|
data.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ParamPackage::DataType::iterator ParamPackage::begin() {
|
||||||
|
return data.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
ParamPackage::DataType::const_iterator ParamPackage::begin() const {
|
||||||
|
return data.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
ParamPackage::DataType::iterator ParamPackage::end() {
|
||||||
|
return data.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
ParamPackage::DataType::const_iterator ParamPackage::end() const {
|
||||||
|
return data.end();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Common
|
} // namespace Common
|
||||||
|
|
|
@ -5,15 +5,15 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <initializer_list>
|
#include <initializer_list>
|
||||||
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
namespace Common {
|
namespace Common {
|
||||||
|
|
||||||
/// A string-based key-value container supporting serializing to and deserializing from a string
|
/// A string-based key-value container supporting serializing to and deserializing from a string
|
||||||
class ParamPackage {
|
class ParamPackage {
|
||||||
public:
|
public:
|
||||||
using DataType = std::unordered_map<std::string, std::string>;
|
using DataType = std::map<std::string, std::string>;
|
||||||
|
|
||||||
ParamPackage() = default;
|
ParamPackage() = default;
|
||||||
explicit ParamPackage(const std::string& serialized);
|
explicit ParamPackage(const std::string& serialized);
|
||||||
|
@ -35,6 +35,12 @@ public:
|
||||||
void Erase(const std::string& key);
|
void Erase(const std::string& key);
|
||||||
void Clear();
|
void Clear();
|
||||||
|
|
||||||
|
// For range-based for
|
||||||
|
DataType::iterator begin();
|
||||||
|
DataType::const_iterator begin() const;
|
||||||
|
DataType::iterator end();
|
||||||
|
DataType::const_iterator end() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
DataType data;
|
DataType data;
|
||||||
};
|
};
|
||||||
|
|
|
@ -495,5 +495,5 @@ if (ARCHITECTURE_x86_64)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (ENABLE_FFMPEG_VIDEO_DUMPER)
|
if (ENABLE_FFMPEG_VIDEO_DUMPER)
|
||||||
target_link_libraries(core PRIVATE FFmpeg::avcodec FFmpeg::avformat FFmpeg::swscale FFmpeg::swresample FFmpeg::avutil)
|
target_link_libraries(core PUBLIC FFmpeg::avcodec FFmpeg::avformat FFmpeg::swscale FFmpeg::swresample FFmpeg::avutil)
|
||||||
endif()
|
endif()
|
||||||
|
|
|
@ -377,6 +377,12 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, u32 system_mo
|
||||||
Service::Init(*this);
|
Service::Init(*this);
|
||||||
GDBStub::DeferStart();
|
GDBStub::DeferStart();
|
||||||
|
|
||||||
|
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
|
||||||
|
video_dumper = std::make_unique<VideoDumper::FFmpegBackend>();
|
||||||
|
#else
|
||||||
|
video_dumper = std::make_unique<VideoDumper::NullBackend>();
|
||||||
|
#endif
|
||||||
|
|
||||||
VideoCore::ResultStatus result = VideoCore::Init(emu_window, *memory);
|
VideoCore::ResultStatus result = VideoCore::Init(emu_window, *memory);
|
||||||
if (result != VideoCore::ResultStatus::Success) {
|
if (result != VideoCore::ResultStatus::Success) {
|
||||||
switch (result) {
|
switch (result) {
|
||||||
|
@ -389,12 +395,6 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, u32 system_mo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
|
|
||||||
video_dumper = std::make_unique<VideoDumper::FFmpegBackend>();
|
|
||||||
#else
|
|
||||||
video_dumper = std::make_unique<VideoDumper::NullBackend>();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
LOG_DEBUG(Core, "Initialized OK");
|
LOG_DEBUG(Core, "Initialized OK");
|
||||||
|
|
||||||
initalized = true;
|
initalized = true;
|
||||||
|
|
|
@ -8,17 +8,7 @@
|
||||||
namespace VideoDumper {
|
namespace VideoDumper {
|
||||||
|
|
||||||
VideoFrame::VideoFrame(std::size_t width_, std::size_t height_, u8* data_)
|
VideoFrame::VideoFrame(std::size_t width_, std::size_t height_, u8* data_)
|
||||||
: width(width_), height(height_), stride(width * 4), data(width * height * 4) {
|
: width(width_), height(height_), stride(width * 4), data(data_, data_ + width * height * 4) {}
|
||||||
// While copying, rotate the image to put the pixels in correct order
|
|
||||||
// (As OpenGL returns pixel data starting from the lowest position)
|
|
||||||
for (std::size_t i = 0; i < height; i++) {
|
|
||||||
for (std::size_t j = 0; j < width; j++) {
|
|
||||||
for (std::size_t k = 0; k < 4; k++) {
|
|
||||||
data[i * stride + j * 4 + k] = data_[(height - i - 1) * stride + j * 4 + k];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Backend::~Backend() = default;
|
Backend::~Backend() = default;
|
||||||
NullBackend::~NullBackend() = default;
|
NullBackend::~NullBackend() = default;
|
||||||
|
|
|
@ -28,10 +28,9 @@ public:
|
||||||
class Backend {
|
class Backend {
|
||||||
public:
|
public:
|
||||||
virtual ~Backend();
|
virtual ~Backend();
|
||||||
virtual bool StartDumping(const std::string& path, const std::string& format,
|
virtual bool StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) = 0;
|
||||||
const Layout::FramebufferLayout& layout) = 0;
|
virtual void AddVideoFrame(VideoFrame frame) = 0;
|
||||||
virtual void AddVideoFrame(const VideoFrame& frame) = 0;
|
virtual void AddAudioFrame(AudioCore::StereoFrame16 frame) = 0;
|
||||||
virtual void AddAudioFrame(const AudioCore::StereoFrame16& frame) = 0;
|
|
||||||
virtual void AddAudioSample(const std::array<s16, 2>& sample) = 0;
|
virtual void AddAudioSample(const std::array<s16, 2>& sample) = 0;
|
||||||
virtual void StopDumping() = 0;
|
virtual void StopDumping() = 0;
|
||||||
virtual bool IsDumping() const = 0;
|
virtual bool IsDumping() const = 0;
|
||||||
|
@ -41,12 +40,12 @@ public:
|
||||||
class NullBackend : public Backend {
|
class NullBackend : public Backend {
|
||||||
public:
|
public:
|
||||||
~NullBackend() override;
|
~NullBackend() override;
|
||||||
bool StartDumping(const std::string& /*path*/, const std::string& /*format*/,
|
bool StartDumping(const std::string& /*path*/,
|
||||||
const Layout::FramebufferLayout& /*layout*/) override {
|
const Layout::FramebufferLayout& /*layout*/) override {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
void AddVideoFrame(const VideoFrame& /*frame*/) override {}
|
void AddVideoFrame(VideoFrame /*frame*/) override {}
|
||||||
void AddAudioFrame(const AudioCore::StereoFrame16& /*frame*/) override {}
|
void AddAudioFrame(AudioCore::StereoFrame16 /*frame*/) override {}
|
||||||
void AddAudioSample(const std::array<s16, 2>& /*sample*/) override {}
|
void AddAudioSample(const std::array<s16, 2>& /*sample*/) override {}
|
||||||
void StopDumping() override {}
|
void StopDumping() override {}
|
||||||
bool IsDumping() const override {
|
bool IsDumping() const override {
|
||||||
|
|
|
@ -2,15 +2,19 @@
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <unordered_set>
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/file_util.h"
|
#include "common/file_util.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
#include "common/param_package.h"
|
||||||
|
#include "common/string_util.h"
|
||||||
#include "core/dumping/ffmpeg_backend.h"
|
#include "core/dumping/ffmpeg_backend.h"
|
||||||
|
#include "core/settings.h"
|
||||||
#include "video_core/renderer_base.h"
|
#include "video_core/renderer_base.h"
|
||||||
#include "video_core/video_core.h"
|
#include "video_core/video_core.h"
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <libavutil/opt.h>
|
#include <libavutil/pixdesc.h>
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace VideoDumper {
|
namespace VideoDumper {
|
||||||
|
@ -27,14 +31,25 @@ void InitializeFFmpegLibraries() {
|
||||||
initialized = true;
|
initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AVDictionary* ToAVDictionary(const std::string& serialized) {
|
||||||
|
Common::ParamPackage param_package{serialized};
|
||||||
|
AVDictionary* result = nullptr;
|
||||||
|
for (const auto& [key, value] : param_package) {
|
||||||
|
av_dict_set(&result, key.c_str(), value.c_str(), 0);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
FFmpegStream::~FFmpegStream() {
|
FFmpegStream::~FFmpegStream() {
|
||||||
Free();
|
Free();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FFmpegStream::Init(AVFormatContext* format_context_) {
|
bool FFmpegStream::Init(FFmpegMuxer& muxer) {
|
||||||
InitializeFFmpegLibraries();
|
InitializeFFmpegLibraries();
|
||||||
|
|
||||||
format_context = format_context_;
|
format_context = muxer.format_context.get();
|
||||||
|
format_context_mutex = &muxer.format_context_mutex;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,15 +62,13 @@ void FFmpegStream::Flush() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void FFmpegStream::WritePacket(AVPacket& packet) {
|
void FFmpegStream::WritePacket(AVPacket& packet) {
|
||||||
if (packet.pts != static_cast<s64>(AV_NOPTS_VALUE)) {
|
av_packet_rescale_ts(&packet, codec_context->time_base, stream->time_base);
|
||||||
packet.pts = av_rescale_q(packet.pts, codec_context->time_base, stream->time_base);
|
|
||||||
}
|
|
||||||
if (packet.dts != static_cast<s64>(AV_NOPTS_VALUE)) {
|
|
||||||
packet.dts = av_rescale_q(packet.dts, codec_context->time_base, stream->time_base);
|
|
||||||
}
|
|
||||||
packet.stream_index = stream->index;
|
packet.stream_index = stream->index;
|
||||||
|
{
|
||||||
|
std::lock_guard lock{*format_context_mutex};
|
||||||
av_interleaved_write_frame(format_context, &packet);
|
av_interleaved_write_frame(format_context, &packet);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void FFmpegStream::SendFrame(AVFrame* frame) {
|
void FFmpegStream::SendFrame(AVFrame* frame) {
|
||||||
// Initialize packet
|
// Initialize packet
|
||||||
|
@ -88,21 +101,18 @@ FFmpegVideoStream::~FFmpegVideoStream() {
|
||||||
Free();
|
Free();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FFmpegVideoStream::Init(AVFormatContext* format_context, AVOutputFormat* output_format,
|
bool FFmpegVideoStream::Init(FFmpegMuxer& muxer, const Layout::FramebufferLayout& layout_) {
|
||||||
const Layout::FramebufferLayout& layout_) {
|
|
||||||
|
|
||||||
InitializeFFmpegLibraries();
|
InitializeFFmpegLibraries();
|
||||||
|
|
||||||
if (!FFmpegStream::Init(format_context))
|
if (!FFmpegStream::Init(muxer))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
layout = layout_;
|
layout = layout_;
|
||||||
frame_count = 0;
|
frame_count = 0;
|
||||||
|
|
||||||
// Initialize video codec
|
// Initialize video codec
|
||||||
// Ensure VP9 codec here, also to avoid patent issues
|
const AVCodec* codec = avcodec_find_encoder_by_name(Settings::values.video_encoder.c_str());
|
||||||
constexpr AVCodecID codec_id = AV_CODEC_ID_VP9;
|
|
||||||
const AVCodec* codec = avcodec_find_encoder(codec_id);
|
|
||||||
codec_context.reset(avcodec_alloc_context3(codec));
|
codec_context.reset(avcodec_alloc_context3(codec));
|
||||||
if (!codec || !codec_context) {
|
if (!codec || !codec_context) {
|
||||||
LOG_ERROR(Render, "Could not find video encoder or allocate video codec context");
|
LOG_ERROR(Render, "Could not find video encoder or allocate video codec context");
|
||||||
|
@ -111,23 +121,28 @@ bool FFmpegVideoStream::Init(AVFormatContext* format_context, AVOutputFormat* ou
|
||||||
|
|
||||||
// Configure video codec context
|
// Configure video codec context
|
||||||
codec_context->codec_type = AVMEDIA_TYPE_VIDEO;
|
codec_context->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||||
codec_context->bit_rate = 2500000;
|
codec_context->bit_rate = Settings::values.video_bitrate;
|
||||||
codec_context->width = layout.width;
|
codec_context->width = layout.width;
|
||||||
codec_context->height = layout.height;
|
codec_context->height = layout.height;
|
||||||
codec_context->time_base.num = 1;
|
codec_context->time_base.num = 1;
|
||||||
codec_context->time_base.den = 60;
|
codec_context->time_base.den = 60;
|
||||||
codec_context->gop_size = 12;
|
codec_context->gop_size = 12;
|
||||||
codec_context->pix_fmt = AV_PIX_FMT_YUV420P;
|
codec_context->pix_fmt = codec->pix_fmts ? codec->pix_fmts[0] : AV_PIX_FMT_YUV420P;
|
||||||
codec_context->thread_count = 8;
|
if (format_context->oformat->flags & AVFMT_GLOBALHEADER)
|
||||||
if (output_format->flags & AVFMT_GLOBALHEADER)
|
|
||||||
codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
|
codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
|
||||||
av_opt_set_int(codec_context.get(), "cpu-used", 5, 0);
|
|
||||||
|
|
||||||
if (avcodec_open2(codec_context.get(), codec, nullptr) < 0) {
|
AVDictionary* options = ToAVDictionary(Settings::values.video_encoder_options);
|
||||||
|
if (avcodec_open2(codec_context.get(), codec, &options) < 0) {
|
||||||
LOG_ERROR(Render, "Could not open video codec");
|
LOG_ERROR(Render, "Could not open video codec");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (av_dict_count(options) != 0) { // Successfully set options are removed from the dict
|
||||||
|
char* buf = nullptr;
|
||||||
|
av_dict_get_string(options, &buf, ':', ';');
|
||||||
|
LOG_WARNING(Render, "Video encoder options not found: {}", buf);
|
||||||
|
}
|
||||||
|
|
||||||
// Create video stream
|
// Create video stream
|
||||||
stream = avformat_new_stream(format_context, codec);
|
stream = avformat_new_stream(format_context, codec);
|
||||||
if (!stream || avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) {
|
if (!stream || avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) {
|
||||||
|
@ -141,7 +156,7 @@ bool FFmpegVideoStream::Init(AVFormatContext* format_context, AVOutputFormat* ou
|
||||||
scaled_frame->format = codec_context->pix_fmt;
|
scaled_frame->format = codec_context->pix_fmt;
|
||||||
scaled_frame->width = layout.width;
|
scaled_frame->width = layout.width;
|
||||||
scaled_frame->height = layout.height;
|
scaled_frame->height = layout.height;
|
||||||
if (av_frame_get_buffer(scaled_frame.get(), 1) < 0) {
|
if (av_frame_get_buffer(scaled_frame.get(), 0) < 0) {
|
||||||
LOG_ERROR(Render, "Could not allocate frame buffer");
|
LOG_ERROR(Render, "Could not allocate frame buffer");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -177,6 +192,10 @@ void FFmpegVideoStream::ProcessFrame(VideoFrame& frame) {
|
||||||
current_frame->height = layout.height;
|
current_frame->height = layout.height;
|
||||||
|
|
||||||
// Scale the frame
|
// Scale the frame
|
||||||
|
if (av_frame_make_writable(scaled_frame.get()) < 0) {
|
||||||
|
LOG_ERROR(Render, "Video frame dropped: Could not prepare frame");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (sws_context) {
|
if (sws_context) {
|
||||||
sws_scale(sws_context.get(), current_frame->data, current_frame->linesize, 0, layout.height,
|
sws_scale(sws_context.get(), current_frame->data, current_frame->linesize, 0, layout.height,
|
||||||
scaled_frame->data, scaled_frame->linesize);
|
scaled_frame->data, scaled_frame->linesize);
|
||||||
|
@ -191,17 +210,16 @@ FFmpegAudioStream::~FFmpegAudioStream() {
|
||||||
Free();
|
Free();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FFmpegAudioStream::Init(AVFormatContext* format_context) {
|
bool FFmpegAudioStream::Init(FFmpegMuxer& muxer) {
|
||||||
InitializeFFmpegLibraries();
|
InitializeFFmpegLibraries();
|
||||||
|
|
||||||
if (!FFmpegStream::Init(format_context))
|
if (!FFmpegStream::Init(muxer))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
sample_count = 0;
|
frame_count = 0;
|
||||||
|
|
||||||
// Initialize audio codec
|
// Initialize audio codec
|
||||||
constexpr AVCodecID codec_id = AV_CODEC_ID_VORBIS;
|
const AVCodec* codec = avcodec_find_encoder_by_name(Settings::values.audio_encoder.c_str());
|
||||||
const AVCodec* codec = avcodec_find_encoder(codec_id);
|
|
||||||
codec_context.reset(avcodec_alloc_context3(codec));
|
codec_context.reset(avcodec_alloc_context3(codec));
|
||||||
if (!codec || !codec_context) {
|
if (!codec || !codec_context) {
|
||||||
LOG_ERROR(Render, "Could not find audio encoder or allocate audio codec context");
|
LOG_ERROR(Render, "Could not find audio encoder or allocate audio codec context");
|
||||||
|
@ -210,17 +228,52 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) {
|
||||||
|
|
||||||
// Configure audio codec context
|
// Configure audio codec context
|
||||||
codec_context->codec_type = AVMEDIA_TYPE_AUDIO;
|
codec_context->codec_type = AVMEDIA_TYPE_AUDIO;
|
||||||
codec_context->bit_rate = 64000;
|
codec_context->bit_rate = Settings::values.audio_bitrate;
|
||||||
|
if (codec->sample_fmts) {
|
||||||
codec_context->sample_fmt = codec->sample_fmts[0];
|
codec_context->sample_fmt = codec->sample_fmts[0];
|
||||||
|
} else {
|
||||||
|
codec_context->sample_fmt = AV_SAMPLE_FMT_S16P;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codec->supported_samplerates) {
|
||||||
|
codec_context->sample_rate = codec->supported_samplerates[0];
|
||||||
|
// Prefer native sample rate if supported
|
||||||
|
const int* ptr = codec->supported_samplerates;
|
||||||
|
while ((*ptr)) {
|
||||||
|
if ((*ptr) == AudioCore::native_sample_rate) {
|
||||||
codec_context->sample_rate = AudioCore::native_sample_rate;
|
codec_context->sample_rate = AudioCore::native_sample_rate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ptr++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
codec_context->sample_rate = AudioCore::native_sample_rate;
|
||||||
|
}
|
||||||
|
codec_context->time_base.num = 1;
|
||||||
|
codec_context->time_base.den = codec_context->sample_rate;
|
||||||
codec_context->channel_layout = AV_CH_LAYOUT_STEREO;
|
codec_context->channel_layout = AV_CH_LAYOUT_STEREO;
|
||||||
codec_context->channels = 2;
|
codec_context->channels = 2;
|
||||||
|
if (format_context->oformat->flags & AVFMT_GLOBALHEADER)
|
||||||
|
codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
|
||||||
|
|
||||||
if (avcodec_open2(codec_context.get(), codec, nullptr) < 0) {
|
AVDictionary* options = ToAVDictionary(Settings::values.audio_encoder_options);
|
||||||
|
if (avcodec_open2(codec_context.get(), codec, &options) < 0) {
|
||||||
LOG_ERROR(Render, "Could not open audio codec");
|
LOG_ERROR(Render, "Could not open audio codec");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (av_dict_count(options) != 0) { // Successfully set options are removed from the dict
|
||||||
|
char* buf = nullptr;
|
||||||
|
av_dict_get_string(options, &buf, ':', ';');
|
||||||
|
LOG_WARNING(Render, "Audio encoder options not found: {}", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codec_context->frame_size) {
|
||||||
|
frame_size = static_cast<u64>(codec_context->frame_size);
|
||||||
|
} else { // variable frame size support
|
||||||
|
frame_size = std::tuple_size<AudioCore::StereoFrame16>::value;
|
||||||
|
}
|
||||||
|
|
||||||
// Create audio stream
|
// Create audio stream
|
||||||
stream = avformat_new_stream(format_context, codec);
|
stream = avformat_new_stream(format_context, codec);
|
||||||
if (!stream || avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) {
|
if (!stream || avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) {
|
||||||
|
@ -234,6 +287,7 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) {
|
||||||
audio_frame->format = codec_context->sample_fmt;
|
audio_frame->format = codec_context->sample_fmt;
|
||||||
audio_frame->channel_layout = codec_context->channel_layout;
|
audio_frame->channel_layout = codec_context->channel_layout;
|
||||||
audio_frame->channels = codec_context->channels;
|
audio_frame->channels = codec_context->channels;
|
||||||
|
audio_frame->sample_rate = codec_context->sample_rate;
|
||||||
|
|
||||||
// Allocate SWR context
|
// Allocate SWR context
|
||||||
auto* context =
|
auto* context =
|
||||||
|
@ -253,7 +307,7 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) {
|
||||||
// Allocate resampled data
|
// Allocate resampled data
|
||||||
int error =
|
int error =
|
||||||
av_samples_alloc_array_and_samples(&resampled_data, nullptr, codec_context->channels,
|
av_samples_alloc_array_and_samples(&resampled_data, nullptr, codec_context->channels,
|
||||||
codec_context->frame_size, codec_context->sample_fmt, 0);
|
frame_size, codec_context->sample_fmt, 0);
|
||||||
if (error < 0) {
|
if (error < 0) {
|
||||||
LOG_ERROR(Render, "Could not allocate samples storage");
|
LOG_ERROR(Render, "Could not allocate samples storage");
|
||||||
return false;
|
return false;
|
||||||
|
@ -274,39 +328,79 @@ void FFmpegAudioStream::Free() {
|
||||||
av_freep(&resampled_data);
|
av_freep(&resampled_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FFmpegAudioStream::ProcessFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1) {
|
void FFmpegAudioStream::ProcessFrame(const VariableAudioFrame& channel0,
|
||||||
|
const VariableAudioFrame& channel1) {
|
||||||
ASSERT_MSG(channel0.size() == channel1.size(),
|
ASSERT_MSG(channel0.size() == channel1.size(),
|
||||||
"Frames of the two channels must have the same number of samples");
|
"Frames of the two channels must have the same number of samples");
|
||||||
std::array<const u8*, 2> src_data = {reinterpret_cast<u8*>(channel0.data()),
|
|
||||||
reinterpret_cast<u8*>(channel1.data())};
|
|
||||||
if (swr_convert(swr_context.get(), resampled_data, channel0.size(), src_data.data(),
|
|
||||||
channel0.size()) < 0) {
|
|
||||||
|
|
||||||
|
const auto sample_size = av_get_bytes_per_sample(codec_context->sample_fmt);
|
||||||
|
std::array<const u8*, 2> src_data = {reinterpret_cast<const u8*>(channel0.data()),
|
||||||
|
reinterpret_cast<const u8*>(channel1.data())};
|
||||||
|
|
||||||
|
std::array<u8*, 2> dst_data;
|
||||||
|
if (av_sample_fmt_is_planar(codec_context->sample_fmt)) {
|
||||||
|
dst_data = {resampled_data[0] + sample_size * offset,
|
||||||
|
resampled_data[1] + sample_size * offset};
|
||||||
|
} else {
|
||||||
|
dst_data = {resampled_data[0] + sample_size * offset * 2}; // 2 channels
|
||||||
|
}
|
||||||
|
|
||||||
|
auto resampled_count = swr_convert(swr_context.get(), dst_data.data(), frame_size - offset,
|
||||||
|
src_data.data(), channel0.size());
|
||||||
|
if (resampled_count < 0) {
|
||||||
LOG_ERROR(Render, "Audio frame dropped: Could not resample data");
|
LOG_ERROR(Render, "Audio frame dropped: Could not resample data");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare frame
|
offset += resampled_count;
|
||||||
audio_frame->nb_samples = channel0.size();
|
if (offset < frame_size) { // Still not enough to form a frame
|
||||||
audio_frame->data[0] = resampled_data[0];
|
return;
|
||||||
audio_frame->data[1] = resampled_data[1];
|
|
||||||
audio_frame->pts = sample_count;
|
|
||||||
sample_count += channel0.size();
|
|
||||||
|
|
||||||
SendFrame(audio_frame.get());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t FFmpegAudioStream::GetAudioFrameSize() const {
|
while (true) {
|
||||||
ASSERT_MSG(codec_context, "Codec context is not initialized yet!");
|
// Prepare frame
|
||||||
return codec_context->frame_size;
|
audio_frame->nb_samples = frame_size;
|
||||||
|
audio_frame->data[0] = resampled_data[0];
|
||||||
|
if (av_sample_fmt_is_planar(codec_context->sample_fmt)) {
|
||||||
|
audio_frame->data[1] = resampled_data[1];
|
||||||
|
}
|
||||||
|
audio_frame->pts = frame_count * frame_size;
|
||||||
|
frame_count++;
|
||||||
|
|
||||||
|
SendFrame(audio_frame.get());
|
||||||
|
|
||||||
|
// swr_convert buffers input internally. Try to get more resampled data
|
||||||
|
resampled_count = swr_convert(swr_context.get(), resampled_data, frame_size, nullptr, 0);
|
||||||
|
if (resampled_count < 0) {
|
||||||
|
LOG_ERROR(Render, "Audio frame dropped: Could not resample data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (static_cast<u64>(resampled_count) < frame_size) {
|
||||||
|
offset = resampled_count;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FFmpegAudioStream::Flush() {
|
||||||
|
// Send the last samples
|
||||||
|
audio_frame->nb_samples = offset;
|
||||||
|
audio_frame->data[0] = resampled_data[0];
|
||||||
|
if (av_sample_fmt_is_planar(codec_context->sample_fmt)) {
|
||||||
|
audio_frame->data[1] = resampled_data[1];
|
||||||
|
}
|
||||||
|
audio_frame->pts = frame_count * frame_size;
|
||||||
|
|
||||||
|
SendFrame(audio_frame.get());
|
||||||
|
|
||||||
|
FFmpegStream::Flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
FFmpegMuxer::~FFmpegMuxer() {
|
FFmpegMuxer::~FFmpegMuxer() {
|
||||||
Free();
|
Free();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FFmpegMuxer::Init(const std::string& path, const std::string& format,
|
bool FFmpegMuxer::Init(const std::string& path, const Layout::FramebufferLayout& layout) {
|
||||||
const Layout::FramebufferLayout& layout) {
|
|
||||||
|
|
||||||
InitializeFFmpegLibraries();
|
InitializeFFmpegLibraries();
|
||||||
|
|
||||||
|
@ -315,9 +409,8 @@ bool FFmpegMuxer::Init(const std::string& path, const std::string& format,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get output format
|
// Get output format
|
||||||
// Ensure webm here to avoid patent issues
|
const auto format = Settings::values.output_format;
|
||||||
ASSERT_MSG(format == "webm", "Only webm is allowed for frame dumping");
|
auto* output_format = av_guess_format(format.c_str(), path.c_str(), nullptr);
|
||||||
auto* output_format = av_guess_format(format.c_str(), path.c_str(), "video/webm");
|
|
||||||
if (!output_format) {
|
if (!output_format) {
|
||||||
LOG_ERROR(Render, "Could not get format {}", format);
|
LOG_ERROR(Render, "Could not get format {}", format);
|
||||||
return false;
|
return false;
|
||||||
|
@ -333,18 +426,24 @@ bool FFmpegMuxer::Init(const std::string& path, const std::string& format,
|
||||||
}
|
}
|
||||||
format_context.reset(format_context_raw);
|
format_context.reset(format_context_raw);
|
||||||
|
|
||||||
if (!video_stream.Init(format_context.get(), output_format, layout))
|
if (!video_stream.Init(*this, layout))
|
||||||
return false;
|
return false;
|
||||||
if (!audio_stream.Init(format_context.get()))
|
if (!audio_stream.Init(*this))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
AVDictionary* options = ToAVDictionary(Settings::values.format_options);
|
||||||
// Open video file
|
// Open video file
|
||||||
if (avio_open(&format_context->pb, path.c_str(), AVIO_FLAG_WRITE) < 0 ||
|
if (avio_open(&format_context->pb, path.c_str(), AVIO_FLAG_WRITE) < 0 ||
|
||||||
avformat_write_header(format_context.get(), nullptr)) {
|
avformat_write_header(format_context.get(), &options)) {
|
||||||
|
|
||||||
LOG_ERROR(Render, "Could not open {}", path);
|
LOG_ERROR(Render, "Could not open {}", path);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (av_dict_count(options) != 0) { // Successfully set options are removed from the dict
|
||||||
|
char* buf = nullptr;
|
||||||
|
av_dict_get_string(options, &buf, ':', ';');
|
||||||
|
LOG_WARNING(Render, "Format options not found: {}", buf);
|
||||||
|
}
|
||||||
|
|
||||||
LOG_INFO(Render, "Dumping frames to {} ({}x{})", path, layout.width, layout.height);
|
LOG_INFO(Render, "Dumping frames to {} ({}x{})", path, layout.width, layout.height);
|
||||||
return true;
|
return true;
|
||||||
|
@ -360,7 +459,8 @@ void FFmpegMuxer::ProcessVideoFrame(VideoFrame& frame) {
|
||||||
video_stream.ProcessFrame(frame);
|
video_stream.ProcessFrame(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FFmpegMuxer::ProcessAudioFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1) {
|
void FFmpegMuxer::ProcessAudioFrame(const VariableAudioFrame& channel0,
|
||||||
|
const VariableAudioFrame& channel1) {
|
||||||
audio_stream.ProcessFrame(channel0, channel1);
|
audio_stream.ProcessFrame(channel0, channel1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -372,11 +472,9 @@ void FFmpegMuxer::FlushAudio() {
|
||||||
audio_stream.Flush();
|
audio_stream.Flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t FFmpegMuxer::GetAudioFrameSize() const {
|
|
||||||
return audio_stream.GetAudioFrameSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FFmpegMuxer::WriteTrailer() {
|
void FFmpegMuxer::WriteTrailer() {
|
||||||
|
std::lock_guard lock{format_context_mutex};
|
||||||
|
av_interleaved_write_frame(format_context.get(), nullptr);
|
||||||
av_write_trailer(format_context.get());
|
av_write_trailer(format_context.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -392,12 +490,11 @@ FFmpegBackend::~FFmpegBackend() {
|
||||||
ffmpeg.Free();
|
ffmpeg.Free();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FFmpegBackend::StartDumping(const std::string& path, const std::string& format,
|
bool FFmpegBackend::StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) {
|
||||||
const Layout::FramebufferLayout& layout) {
|
|
||||||
|
|
||||||
InitializeFFmpegLibraries();
|
InitializeFFmpegLibraries();
|
||||||
|
|
||||||
if (!ffmpeg.Init(path, format, layout)) {
|
if (!ffmpeg.Init(path, layout)) {
|
||||||
ffmpeg.Free();
|
ffmpeg.Free();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -450,31 +547,29 @@ bool FFmpegBackend::StartDumping(const std::string& path, const std::string& for
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FFmpegBackend::AddVideoFrame(const VideoFrame& frame) {
|
void FFmpegBackend::AddVideoFrame(VideoFrame frame) {
|
||||||
event1.Wait();
|
event1.Wait();
|
||||||
video_frame_buffers[next_buffer] = std::move(frame);
|
video_frame_buffers[next_buffer] = std::move(frame);
|
||||||
event2.Set();
|
event2.Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
void FFmpegBackend::AddAudioFrame(const AudioCore::StereoFrame16& frame) {
|
void FFmpegBackend::AddAudioFrame(AudioCore::StereoFrame16 frame) {
|
||||||
std::array<std::array<s16, 160>, 2> refactored_frame;
|
std::array<VariableAudioFrame, 2> refactored_frame;
|
||||||
|
for (auto& channel : refactored_frame) {
|
||||||
|
channel.resize(frame.size());
|
||||||
|
}
|
||||||
for (std::size_t i = 0; i < frame.size(); i++) {
|
for (std::size_t i = 0; i < frame.size(); i++) {
|
||||||
refactored_frame[0][i] = frame[i][0];
|
refactored_frame[0][i] = frame[i][0];
|
||||||
refactored_frame[1][i] = frame[i][1];
|
refactored_frame[1][i] = frame[i][1];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto i : {0, 1}) {
|
audio_frame_queues[0].Push(std::move(refactored_frame[0]));
|
||||||
audio_buffers[i].insert(audio_buffers[i].end(), refactored_frame[i].begin(),
|
audio_frame_queues[1].Push(std::move(refactored_frame[1]));
|
||||||
refactored_frame[i].end());
|
|
||||||
}
|
|
||||||
CheckAudioBuffer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FFmpegBackend::AddAudioSample(const std::array<s16, 2>& sample) {
|
void FFmpegBackend::AddAudioSample(const std::array<s16, 2>& sample) {
|
||||||
for (auto i : {0, 1}) {
|
audio_frame_queues[0].Push(VariableAudioFrame{sample[0]});
|
||||||
audio_buffers[i].push_back(sample[i]);
|
audio_frame_queues[1].Push(VariableAudioFrame{sample[1]});
|
||||||
}
|
|
||||||
CheckAudioBuffer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FFmpegBackend::StopDumping() {
|
void FFmpegBackend::StopDumping() {
|
||||||
|
@ -484,12 +579,6 @@ void FFmpegBackend::StopDumping() {
|
||||||
// Flush the video processing queue
|
// Flush the video processing queue
|
||||||
AddVideoFrame(VideoFrame());
|
AddVideoFrame(VideoFrame());
|
||||||
for (auto i : {0, 1}) {
|
for (auto i : {0, 1}) {
|
||||||
// Add remaining data to audio queue
|
|
||||||
if (audio_buffers[i].size() >= 0) {
|
|
||||||
VariableAudioFrame buffer(audio_buffers[i].begin(), audio_buffers[i].end());
|
|
||||||
audio_frame_queues[i].Push(std::move(buffer));
|
|
||||||
audio_buffers[i].clear();
|
|
||||||
}
|
|
||||||
// Flush the audio processing queue
|
// Flush the audio processing queue
|
||||||
audio_frame_queues[i].Push(VariableAudioFrame());
|
audio_frame_queues[i].Push(VariableAudioFrame());
|
||||||
}
|
}
|
||||||
|
@ -513,18 +602,234 @@ void FFmpegBackend::EndDumping() {
|
||||||
processing_ended.Set();
|
processing_ended.Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
void FFmpegBackend::CheckAudioBuffer() {
|
// To std string, but handles nullptr
|
||||||
for (auto i : {0, 1}) {
|
std::string ToStdString(const char* str, const std::string& fallback = "") {
|
||||||
const std::size_t frame_size = ffmpeg.GetAudioFrameSize();
|
return str ? std::string{str} : fallback;
|
||||||
// Add audio data to the queue when there is enough to form a frame
|
}
|
||||||
while (audio_buffers[i].size() >= frame_size) {
|
|
||||||
VariableAudioFrame buffer(audio_buffers[i].begin(),
|
|
||||||
audio_buffers[i].begin() + frame_size);
|
|
||||||
audio_frame_queues[i].Push(std::move(buffer));
|
|
||||||
|
|
||||||
audio_buffers[i].erase(audio_buffers[i].begin(), audio_buffers[i].begin() + frame_size);
|
std::string FormatDuration(s64 duration) {
|
||||||
|
// The following is implemented according to libavutil code (opt.c)
|
||||||
|
std::string out;
|
||||||
|
if (duration < 0 && duration != std::numeric_limits<s64>::min()) {
|
||||||
|
out.append("-");
|
||||||
|
duration = -duration;
|
||||||
|
}
|
||||||
|
if (duration == std::numeric_limits<s64>::max()) {
|
||||||
|
return "INT64_MAX";
|
||||||
|
} else if (duration == std::numeric_limits<s64>::min()) {
|
||||||
|
return "INT64_MIN";
|
||||||
|
} else if (duration > 3600ll * 1000000ll) {
|
||||||
|
out.append(fmt::format("{}:{:02d}:{:02d}.{:06d}", duration / 3600000000ll,
|
||||||
|
((duration / 60000000ll) % 60), ((duration / 1000000ll) % 60),
|
||||||
|
duration % 1000000));
|
||||||
|
} else if (duration > 60ll * 1000000ll) {
|
||||||
|
out.append(fmt::format("{}:{:02d}.{:06d}", duration / 60000000ll,
|
||||||
|
((duration / 1000000ll) % 60), duration % 1000000));
|
||||||
|
} else {
|
||||||
|
out.append(fmt::format("{}.{:06d}", duration / 1000000ll, duration % 1000000));
|
||||||
|
}
|
||||||
|
while (out.back() == '0') {
|
||||||
|
out.erase(out.size() - 1, 1);
|
||||||
|
}
|
||||||
|
if (out.back() == '.') {
|
||||||
|
out.erase(out.size() - 1, 1);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string FormatDefaultValue(const AVOption* option,
|
||||||
|
const std::vector<OptionInfo::NamedConstant>& named_constants) {
|
||||||
|
// The following is taken and modified from libavutil code (opt.c)
|
||||||
|
switch (option->type) {
|
||||||
|
case AV_OPT_TYPE_BOOL: {
|
||||||
|
const auto value = option->default_val.i64;
|
||||||
|
if (value < 0) {
|
||||||
|
return "auto";
|
||||||
|
}
|
||||||
|
return value ? "true" : "false";
|
||||||
|
}
|
||||||
|
case AV_OPT_TYPE_FLAGS: {
|
||||||
|
const auto value = option->default_val.i64;
|
||||||
|
std::string out;
|
||||||
|
for (const auto& constant : named_constants) {
|
||||||
|
if (!(value & constant.value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!out.empty()) {
|
||||||
|
out.append("+");
|
||||||
|
}
|
||||||
|
out.append(constant.name);
|
||||||
|
}
|
||||||
|
return out.empty() ? fmt::format("{}", value) : out;
|
||||||
|
}
|
||||||
|
case AV_OPT_TYPE_DURATION: {
|
||||||
|
return FormatDuration(option->default_val.i64);
|
||||||
|
}
|
||||||
|
case AV_OPT_TYPE_INT:
|
||||||
|
case AV_OPT_TYPE_UINT64:
|
||||||
|
case AV_OPT_TYPE_INT64: {
|
||||||
|
const auto value = option->default_val.i64;
|
||||||
|
for (const auto& constant : named_constants) {
|
||||||
|
if (constant.value == value) {
|
||||||
|
return constant.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return fmt::format("{}", value);
|
||||||
|
}
|
||||||
|
case AV_OPT_TYPE_DOUBLE:
|
||||||
|
case AV_OPT_TYPE_FLOAT: {
|
||||||
|
return fmt::format("{}", option->default_val.dbl);
|
||||||
|
}
|
||||||
|
case AV_OPT_TYPE_RATIONAL: {
|
||||||
|
const auto q = av_d2q(option->default_val.dbl, std::numeric_limits<int>::max());
|
||||||
|
return fmt::format("{}/{}", q.num, q.den);
|
||||||
|
}
|
||||||
|
case AV_OPT_TYPE_PIXEL_FMT: {
|
||||||
|
const char* name = av_get_pix_fmt_name(static_cast<AVPixelFormat>(option->default_val.i64));
|
||||||
|
return ToStdString(name, "none");
|
||||||
|
}
|
||||||
|
case AV_OPT_TYPE_SAMPLE_FMT: {
|
||||||
|
const char* name =
|
||||||
|
av_get_sample_fmt_name(static_cast<AVSampleFormat>(option->default_val.i64));
|
||||||
|
return ToStdString(name, "none");
|
||||||
|
}
|
||||||
|
case AV_OPT_TYPE_COLOR:
|
||||||
|
case AV_OPT_TYPE_IMAGE_SIZE:
|
||||||
|
case AV_OPT_TYPE_STRING:
|
||||||
|
case AV_OPT_TYPE_DICT:
|
||||||
|
case AV_OPT_TYPE_VIDEO_RATE: {
|
||||||
|
return ToStdString(option->default_val.str);
|
||||||
|
}
|
||||||
|
case AV_OPT_TYPE_CHANNEL_LAYOUT: {
|
||||||
|
return fmt::format("{:#x}", option->default_val.i64);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetOptionListSingle(std::vector<OptionInfo>& out, const AVClass* av_class) {
|
||||||
|
if (av_class == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AVOption* current = nullptr;
|
||||||
|
std::unordered_map<std::string, std::vector<OptionInfo::NamedConstant>> named_constants_map;
|
||||||
|
// First iteration: find and place all named constants
|
||||||
|
while ((current = av_opt_next(&av_class, current))) {
|
||||||
|
if (current->type != AV_OPT_TYPE_CONST || !current->unit) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
named_constants_map[current->unit].push_back(
|
||||||
|
{current->name, ToStdString(current->help), current->default_val.i64});
|
||||||
|
}
|
||||||
|
// Second iteration: find all options
|
||||||
|
current = nullptr;
|
||||||
|
while ((current = av_opt_next(&av_class, current))) {
|
||||||
|
// Currently we cannot handle binary options
|
||||||
|
if (current->type == AV_OPT_TYPE_CONST || current->type == AV_OPT_TYPE_BINARY) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::vector<OptionInfo::NamedConstant> named_constants;
|
||||||
|
if (current->unit && named_constants_map.count(current->unit)) {
|
||||||
|
named_constants = named_constants_map.at(current->unit);
|
||||||
|
}
|
||||||
|
const auto default_value = FormatDefaultValue(current, named_constants);
|
||||||
|
out.push_back({current->name, ToStdString(current->help), current->type, default_value,
|
||||||
|
std::move(named_constants), current->min, current->max});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetOptionList(std::vector<OptionInfo>& out, const AVClass* av_class, bool search_children) {
|
||||||
|
if (av_class == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetOptionListSingle(out, av_class);
|
||||||
|
|
||||||
|
if (!search_children) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AVClass* child_class = nullptr;
|
||||||
|
while ((child_class = av_opt_child_class_next(av_class, child_class))) {
|
||||||
|
GetOptionListSingle(out, child_class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<OptionInfo> GetOptionList(const AVClass* av_class, bool search_children) {
|
||||||
|
std::vector<OptionInfo> out;
|
||||||
|
GetOptionList(out, av_class, search_children);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<EncoderInfo> ListEncoders(AVMediaType type) {
|
||||||
|
InitializeFFmpegLibraries();
|
||||||
|
|
||||||
|
std::vector<EncoderInfo> out;
|
||||||
|
|
||||||
|
const AVCodec* current = nullptr;
|
||||||
|
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 10, 100)
|
||||||
|
while ((current = av_codec_next(current))) {
|
||||||
|
#else
|
||||||
|
void* data = nullptr; // For libavcodec to save the iteration state
|
||||||
|
while ((current = av_codec_iterate(&data))) {
|
||||||
|
#endif
|
||||||
|
if (!av_codec_is_encoder(current) || current->type != type) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push_back({current->name, ToStdString(current->long_name), current->id,
|
||||||
|
GetOptionList(current->priv_class, true)});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<OptionInfo> GetEncoderGenericOptions() {
|
||||||
|
return GetOptionList(avcodec_get_class(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<FormatInfo> ListFormats() {
|
||||||
|
InitializeFFmpegLibraries();
|
||||||
|
|
||||||
|
std::vector<FormatInfo> out;
|
||||||
|
|
||||||
|
const AVOutputFormat* current = nullptr;
|
||||||
|
#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100)
|
||||||
|
while ((current = av_oformat_next(current))) {
|
||||||
|
#else
|
||||||
|
void* data = nullptr; // For libavformat to save the iteration state
|
||||||
|
while ((current = av_muxer_iterate(&data))) {
|
||||||
|
#endif
|
||||||
|
std::vector<std::string> extensions;
|
||||||
|
Common::SplitString(ToStdString(current->extensions), ',', extensions);
|
||||||
|
|
||||||
|
std::set<AVCodecID> supported_video_codecs;
|
||||||
|
std::set<AVCodecID> supported_audio_codecs;
|
||||||
|
// Go through all codecs
|
||||||
|
const AVCodecDescriptor* codec = nullptr;
|
||||||
|
while ((codec = avcodec_descriptor_next(codec))) {
|
||||||
|
if (avformat_query_codec(current, codec->id, FF_COMPLIANCE_NORMAL) == 1) {
|
||||||
|
if (codec->type == AVMEDIA_TYPE_VIDEO) {
|
||||||
|
supported_video_codecs.emplace(codec->id);
|
||||||
|
} else if (codec->type == AVMEDIA_TYPE_AUDIO) {
|
||||||
|
supported_audio_codecs.emplace(codec->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supported_video_codecs.empty() || supported_audio_codecs.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push_back({current->name, ToStdString(current->long_name), std::move(extensions),
|
||||||
|
std::move(supported_video_codecs), std::move(supported_audio_codecs),
|
||||||
|
GetOptionList(current->priv_class, true)});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<OptionInfo> GetFormatGenericOptions() {
|
||||||
|
return GetOptionList(avformat_get_class(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace VideoDumper
|
} // namespace VideoDumper
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <set>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
@ -19,6 +20,7 @@
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <libavcodec/avcodec.h>
|
#include <libavcodec/avcodec.h>
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
|
#include <libavutil/opt.h>
|
||||||
#include <libswresample/swresample.h>
|
#include <libswresample/swresample.h>
|
||||||
#include <libswscale/swscale.h>
|
#include <libswscale/swscale.h>
|
||||||
}
|
}
|
||||||
|
@ -29,13 +31,15 @@ using VariableAudioFrame = std::vector<s16>;
|
||||||
|
|
||||||
void InitFFmpegLibraries();
|
void InitFFmpegLibraries();
|
||||||
|
|
||||||
|
class FFmpegMuxer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper around FFmpeg AVCodecContext + AVStream.
|
* Wrapper around FFmpeg AVCodecContext + AVStream.
|
||||||
* Rescales/Resamples, encodes and writes a frame.
|
* Rescales/Resamples, encodes and writes a frame.
|
||||||
*/
|
*/
|
||||||
class FFmpegStream {
|
class FFmpegStream {
|
||||||
public:
|
public:
|
||||||
bool Init(AVFormatContext* format_context);
|
bool Init(FFmpegMuxer& muxer);
|
||||||
void Free();
|
void Free();
|
||||||
void Flush();
|
void Flush();
|
||||||
|
|
||||||
|
@ -58,6 +62,7 @@ protected:
|
||||||
};
|
};
|
||||||
|
|
||||||
AVFormatContext* format_context{};
|
AVFormatContext* format_context{};
|
||||||
|
std::mutex* format_context_mutex{};
|
||||||
std::unique_ptr<AVCodecContext, AVCodecContextDeleter> codec_context{};
|
std::unique_ptr<AVCodecContext, AVCodecContextDeleter> codec_context{};
|
||||||
AVStream* stream{};
|
AVStream* stream{};
|
||||||
};
|
};
|
||||||
|
@ -70,8 +75,7 @@ class FFmpegVideoStream : public FFmpegStream {
|
||||||
public:
|
public:
|
||||||
~FFmpegVideoStream();
|
~FFmpegVideoStream();
|
||||||
|
|
||||||
bool Init(AVFormatContext* format_context, AVOutputFormat* output_format,
|
bool Init(FFmpegMuxer& muxer, const Layout::FramebufferLayout& layout);
|
||||||
const Layout::FramebufferLayout& layout);
|
|
||||||
void Free();
|
void Free();
|
||||||
void ProcessFrame(VideoFrame& frame);
|
void ProcessFrame(VideoFrame& frame);
|
||||||
|
|
||||||
|
@ -96,15 +100,16 @@ private:
|
||||||
/**
|
/**
|
||||||
* A FFmpegStream used for audio data.
|
* A FFmpegStream used for audio data.
|
||||||
* Resamples (converts), encodes and writes a frame.
|
* Resamples (converts), encodes and writes a frame.
|
||||||
|
* This also temporarily stores resampled audio data before there are enough to form a frame.
|
||||||
*/
|
*/
|
||||||
class FFmpegAudioStream : public FFmpegStream {
|
class FFmpegAudioStream : public FFmpegStream {
|
||||||
public:
|
public:
|
||||||
~FFmpegAudioStream();
|
~FFmpegAudioStream();
|
||||||
|
|
||||||
bool Init(AVFormatContext* format_context);
|
bool Init(FFmpegMuxer& muxer);
|
||||||
void Free();
|
void Free();
|
||||||
void ProcessFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1);
|
void ProcessFrame(const VariableAudioFrame& channel0, const VariableAudioFrame& channel1);
|
||||||
std::size_t GetAudioFrameSize() const;
|
void Flush();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct SwrContextDeleter {
|
struct SwrContextDeleter {
|
||||||
|
@ -113,12 +118,14 @@ private:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
u64 sample_count{};
|
u64 frame_size{};
|
||||||
|
u64 frame_count{};
|
||||||
|
|
||||||
std::unique_ptr<AVFrame, AVFrameDeleter> audio_frame{};
|
std::unique_ptr<AVFrame, AVFrameDeleter> audio_frame{};
|
||||||
std::unique_ptr<SwrContext, SwrContextDeleter> swr_context{};
|
std::unique_ptr<SwrContext, SwrContextDeleter> swr_context{};
|
||||||
|
|
||||||
u8** resampled_data{};
|
u8** resampled_data{};
|
||||||
|
u64 offset{}; // Number of output samples that are currently in resampled_data.
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -129,14 +136,12 @@ class FFmpegMuxer {
|
||||||
public:
|
public:
|
||||||
~FFmpegMuxer();
|
~FFmpegMuxer();
|
||||||
|
|
||||||
bool Init(const std::string& path, const std::string& format,
|
bool Init(const std::string& path, const Layout::FramebufferLayout& layout);
|
||||||
const Layout::FramebufferLayout& layout);
|
|
||||||
void Free();
|
void Free();
|
||||||
void ProcessVideoFrame(VideoFrame& frame);
|
void ProcessVideoFrame(VideoFrame& frame);
|
||||||
void ProcessAudioFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1);
|
void ProcessAudioFrame(const VariableAudioFrame& channel0, const VariableAudioFrame& channel1);
|
||||||
void FlushVideo();
|
void FlushVideo();
|
||||||
void FlushAudio();
|
void FlushAudio();
|
||||||
std::size_t GetAudioFrameSize() const;
|
|
||||||
void WriteTrailer();
|
void WriteTrailer();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@ -150,28 +155,28 @@ private:
|
||||||
FFmpegAudioStream audio_stream{};
|
FFmpegAudioStream audio_stream{};
|
||||||
FFmpegVideoStream video_stream{};
|
FFmpegVideoStream video_stream{};
|
||||||
std::unique_ptr<AVFormatContext, AVFormatContextDeleter> format_context{};
|
std::unique_ptr<AVFormatContext, AVFormatContextDeleter> format_context{};
|
||||||
|
std::mutex format_context_mutex;
|
||||||
|
|
||||||
|
friend class FFmpegStream;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FFmpeg video dumping backend.
|
* FFmpeg video dumping backend.
|
||||||
* This class implements a double buffer, and an audio queue to keep audio data
|
* This class implements a double buffer.
|
||||||
* before enough data is received to form a frame.
|
|
||||||
*/
|
*/
|
||||||
class FFmpegBackend : public Backend {
|
class FFmpegBackend : public Backend {
|
||||||
public:
|
public:
|
||||||
FFmpegBackend();
|
FFmpegBackend();
|
||||||
~FFmpegBackend() override;
|
~FFmpegBackend() override;
|
||||||
bool StartDumping(const std::string& path, const std::string& format,
|
bool StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) override;
|
||||||
const Layout::FramebufferLayout& layout) override;
|
void AddVideoFrame(VideoFrame frame) override;
|
||||||
void AddVideoFrame(const VideoFrame& frame) override;
|
void AddAudioFrame(AudioCore::StereoFrame16 frame) override;
|
||||||
void AddAudioFrame(const AudioCore::StereoFrame16& frame) override;
|
|
||||||
void AddAudioSample(const std::array<s16, 2>& sample) override;
|
void AddAudioSample(const std::array<s16, 2>& sample) override;
|
||||||
void StopDumping() override;
|
void StopDumping() override;
|
||||||
bool IsDumping() const override;
|
bool IsDumping() const override;
|
||||||
Layout::FramebufferLayout GetLayout() const override;
|
Layout::FramebufferLayout GetLayout() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void CheckAudioBuffer();
|
|
||||||
void EndDumping();
|
void EndDumping();
|
||||||
|
|
||||||
std::atomic_bool is_dumping = false; ///< Whether the backend is currently dumping
|
std::atomic_bool is_dumping = false; ///< Whether the backend is currently dumping
|
||||||
|
@ -184,13 +189,51 @@ private:
|
||||||
Common::Event event1, event2;
|
Common::Event event1, event2;
|
||||||
std::thread video_processing_thread;
|
std::thread video_processing_thread;
|
||||||
|
|
||||||
/// An audio buffer used to temporarily hold audio data, before the size is big enough
|
|
||||||
/// to be sent to the encoder as a frame
|
|
||||||
std::array<VariableAudioFrame, 2> audio_buffers;
|
|
||||||
std::array<Common::SPSCQueue<VariableAudioFrame>, 2> audio_frame_queues;
|
std::array<Common::SPSCQueue<VariableAudioFrame>, 2> audio_frame_queues;
|
||||||
std::thread audio_processing_thread;
|
std::thread audio_processing_thread;
|
||||||
|
|
||||||
Common::Event processing_ended;
|
Common::Event processing_ended;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Struct describing encoder/muxer options
|
||||||
|
struct OptionInfo {
|
||||||
|
std::string name;
|
||||||
|
std::string description;
|
||||||
|
AVOptionType type;
|
||||||
|
std::string default_value;
|
||||||
|
struct NamedConstant {
|
||||||
|
std::string name;
|
||||||
|
std::string description;
|
||||||
|
s64 value;
|
||||||
|
};
|
||||||
|
std::vector<NamedConstant> named_constants;
|
||||||
|
|
||||||
|
// If this is a scalar type
|
||||||
|
double min;
|
||||||
|
double max;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Struct describing an encoder
|
||||||
|
struct EncoderInfo {
|
||||||
|
std::string name;
|
||||||
|
std::string long_name;
|
||||||
|
AVCodecID codec;
|
||||||
|
std::vector<OptionInfo> options;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Struct describing a format
|
||||||
|
struct FormatInfo {
|
||||||
|
std::string name;
|
||||||
|
std::string long_name;
|
||||||
|
std::vector<std::string> extensions;
|
||||||
|
std::set<AVCodecID> supported_video_codecs;
|
||||||
|
std::set<AVCodecID> supported_audio_codecs;
|
||||||
|
std::vector<OptionInfo> options;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<EncoderInfo> ListEncoders(AVMediaType type);
|
||||||
|
std::vector<OptionInfo> GetEncoderGenericOptions();
|
||||||
|
std::vector<FormatInfo> ListFormats();
|
||||||
|
std::vector<OptionInfo> GetFormatGenericOptions();
|
||||||
|
|
||||||
} // namespace VideoDumper
|
} // namespace VideoDumper
|
||||||
|
|
|
@ -223,6 +223,7 @@ void Thread::ResumeFromWait() {
|
||||||
case ThreadStatus::WaitArb:
|
case ThreadStatus::WaitArb:
|
||||||
case ThreadStatus::WaitSleep:
|
case ThreadStatus::WaitSleep:
|
||||||
case ThreadStatus::WaitIPC:
|
case ThreadStatus::WaitIPC:
|
||||||
|
case ThreadStatus::Dormant:
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ThreadStatus::Ready:
|
case ThreadStatus::Ready:
|
||||||
|
|
|
@ -120,15 +120,10 @@ void Module::Interface::GetSoftwareClosedFlag(Kernel::HLERequestContext& ctx) {
|
||||||
void CheckNew3DS(IPC::RequestBuilder& rb) {
|
void CheckNew3DS(IPC::RequestBuilder& rb) {
|
||||||
const bool is_new_3ds = Settings::values.is_new_3ds;
|
const bool is_new_3ds = Settings::values.is_new_3ds;
|
||||||
|
|
||||||
if (is_new_3ds) {
|
|
||||||
LOG_CRITICAL(Service_PTM, "The option 'is_new_3ds' is enabled as part of the 'System' "
|
|
||||||
"settings. Citra does not fully support New 3DS emulation yet!");
|
|
||||||
}
|
|
||||||
|
|
||||||
rb.Push(RESULT_SUCCESS);
|
rb.Push(RESULT_SUCCESS);
|
||||||
rb.Push(is_new_3ds);
|
rb.Push(is_new_3ds);
|
||||||
|
|
||||||
LOG_WARNING(Service_PTM, "(STUBBED) called isNew3DS = 0x{:08x}", static_cast<u32>(is_new_3ds));
|
LOG_DEBUG(Service_PTM, "called isNew3DS = 0x{:08x}", static_cast<u32>(is_new_3ds));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Module::Interface::CheckNew3DS(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::CheckNew3DS(Kernel::HLERequestContext& ctx) {
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
#include "core/hle/service/mic_u.h"
|
#include "core/hle/service/mic_u.h"
|
||||||
#include "core/settings.h"
|
#include "core/settings.h"
|
||||||
#include "video_core/renderer_base.h"
|
#include "video_core/renderer_base.h"
|
||||||
#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h"
|
|
||||||
#include "video_core/video_core.h"
|
#include "video_core/video_core.h"
|
||||||
|
|
||||||
namespace Settings {
|
namespace Settings {
|
||||||
|
@ -38,9 +37,7 @@ void Apply() {
|
||||||
VideoCore::g_renderer_bg_color_update_requested = true;
|
VideoCore::g_renderer_bg_color_update_requested = true;
|
||||||
VideoCore::g_renderer_sampler_update_requested = true;
|
VideoCore::g_renderer_sampler_update_requested = true;
|
||||||
VideoCore::g_renderer_shader_update_requested = true;
|
VideoCore::g_renderer_shader_update_requested = true;
|
||||||
|
VideoCore::g_texture_filter_update_requested = true;
|
||||||
OpenGL::TextureFilterManager::GetInstance().SetTextureFilter(values.texture_filter_name,
|
|
||||||
values.texture_filter_factor);
|
|
||||||
|
|
||||||
auto& system = Core::System::GetInstance();
|
auto& system = Core::System::GetInstance();
|
||||||
if (system.IsPoweredOn()) {
|
if (system.IsPoweredOn()) {
|
||||||
|
@ -88,7 +85,6 @@ void LogSettings() {
|
||||||
LogSetting("Renderer_FrameLimit", Settings::values.frame_limit);
|
LogSetting("Renderer_FrameLimit", Settings::values.frame_limit);
|
||||||
LogSetting("Renderer_PostProcessingShader", Settings::values.pp_shader_name);
|
LogSetting("Renderer_PostProcessingShader", Settings::values.pp_shader_name);
|
||||||
LogSetting("Renderer_FilterMode", Settings::values.filter_mode);
|
LogSetting("Renderer_FilterMode", Settings::values.filter_mode);
|
||||||
LogSetting("Renderer_TextureFilterFactor", Settings::values.texture_filter_factor);
|
|
||||||
LogSetting("Renderer_TextureFilterName", Settings::values.texture_filter_name);
|
LogSetting("Renderer_TextureFilterName", Settings::values.texture_filter_name);
|
||||||
LogSetting("Stereoscopy_Render3d", static_cast<int>(Settings::values.render_3d));
|
LogSetting("Stereoscopy_Render3d", static_cast<int>(Settings::values.render_3d));
|
||||||
LogSetting("Stereoscopy_Factor3d", Settings::values.factor_3d);
|
LogSetting("Stereoscopy_Factor3d", Settings::values.factor_3d);
|
||||||
|
|
|
@ -148,7 +148,6 @@ struct Values {
|
||||||
u16 resolution_factor;
|
u16 resolution_factor;
|
||||||
bool use_frame_limit;
|
bool use_frame_limit;
|
||||||
u16 frame_limit;
|
u16 frame_limit;
|
||||||
u16 texture_filter_factor;
|
|
||||||
std::string texture_filter_name;
|
std::string texture_filter_name;
|
||||||
|
|
||||||
LayoutOption layout_option;
|
LayoutOption layout_option;
|
||||||
|
@ -207,6 +206,18 @@ struct Values {
|
||||||
std::string web_api_url;
|
std::string web_api_url;
|
||||||
std::string citra_username;
|
std::string citra_username;
|
||||||
std::string citra_token;
|
std::string citra_token;
|
||||||
|
|
||||||
|
// Video Dumping
|
||||||
|
std::string output_format;
|
||||||
|
std::string format_options;
|
||||||
|
|
||||||
|
std::string video_encoder;
|
||||||
|
std::string video_encoder_options;
|
||||||
|
u64 video_bitrate;
|
||||||
|
|
||||||
|
std::string audio_encoder;
|
||||||
|
std::string audio_encoder_options;
|
||||||
|
u64 audio_bitrate;
|
||||||
} extern values;
|
} extern values;
|
||||||
|
|
||||||
// a special value for Values::region_value indicating that citra will automatically select a region
|
// a special value for Values::region_value indicating that citra will automatically select a region
|
||||||
|
|
|
@ -472,6 +472,14 @@ SDLState::SDLState() {
|
||||||
if (SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1") == SDL_FALSE) {
|
if (SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1") == SDL_FALSE) {
|
||||||
LOG_ERROR(Input, "Failed to set Hint for background events", SDL_GetError());
|
LOG_ERROR(Input, "Failed to set Hint for background events", SDL_GetError());
|
||||||
}
|
}
|
||||||
|
// these hints are only defined on sdl2.0.9 or higher
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
// This can be set back to 1 when the compatibility problems with the controllers are
|
||||||
|
// solved. There are also hints to toggle the individual drivers.
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "0");
|
||||||
|
// This hint should probably stay as "0" as long as the hidapi PS4 led issue isn't fixed
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4, "0");
|
||||||
|
#endif
|
||||||
|
|
||||||
SDL_AddEventWatch(&SDLEventWatcher, this);
|
SDL_AddEventWatch(&SDLEventWatcher, this);
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ public:
|
||||||
mutable std::mutex ban_list_mutex; ///< Mutex for the ban lists
|
mutable std::mutex ban_list_mutex; ///< Mutex for the ban lists
|
||||||
|
|
||||||
RoomImpl()
|
RoomImpl()
|
||||||
: random_gen(std::random_device()()), NintendoOUI{0x00, 0x1F, 0x32, 0x00, 0x00, 0x00} {}
|
: NintendoOUI{0x00, 0x1F, 0x32, 0x00, 0x00, 0x00}, random_gen(std::random_device()()) {}
|
||||||
|
|
||||||
/// Thread that receives and dispatches network packets
|
/// Thread that receives and dispatches network packets
|
||||||
std::unique_ptr<std::thread> room_thread;
|
std::unique_ptr<std::thread> room_thread;
|
||||||
|
|
|
@ -242,6 +242,13 @@ void RoomMember::RoomMemberImpl::MemberLoop() {
|
||||||
SetError(Error::LostConnection);
|
SetError(Error::LostConnection);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case ENET_EVENT_TYPE_NONE:
|
||||||
|
break;
|
||||||
|
case ENET_EVENT_TYPE_CONNECT:
|
||||||
|
// The ENET_EVENT_TYPE_CONNECT event can not possibly happen here because we're
|
||||||
|
// already connected
|
||||||
|
ASSERT_MSG(false, "Received unexpected connect event while already connected");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|
|
@ -23,6 +23,8 @@ add_library(video_core STATIC
|
||||||
regs_texturing.h
|
regs_texturing.h
|
||||||
renderer_base.cpp
|
renderer_base.cpp
|
||||||
renderer_base.h
|
renderer_base.h
|
||||||
|
renderer_opengl/frame_dumper_opengl.cpp
|
||||||
|
renderer_opengl/frame_dumper_opengl.h
|
||||||
renderer_opengl/gl_rasterizer.cpp
|
renderer_opengl/gl_rasterizer.cpp
|
||||||
renderer_opengl/gl_rasterizer.h
|
renderer_opengl/gl_rasterizer.h
|
||||||
renderer_opengl/gl_rasterizer_cache.cpp
|
renderer_opengl/gl_rasterizer_cache.cpp
|
||||||
|
@ -43,6 +45,8 @@ add_library(video_core STATIC
|
||||||
renderer_opengl/gl_state.h
|
renderer_opengl/gl_state.h
|
||||||
renderer_opengl/gl_stream_buffer.cpp
|
renderer_opengl/gl_stream_buffer.cpp
|
||||||
renderer_opengl/gl_stream_buffer.h
|
renderer_opengl/gl_stream_buffer.h
|
||||||
|
renderer_opengl/gl_surface_params.cpp
|
||||||
|
renderer_opengl/gl_surface_params.h
|
||||||
renderer_opengl/gl_vars.cpp
|
renderer_opengl/gl_vars.cpp
|
||||||
renderer_opengl/gl_vars.h
|
renderer_opengl/gl_vars.h
|
||||||
renderer_opengl/pica_to_gl.h
|
renderer_opengl/pica_to_gl.h
|
||||||
|
@ -54,11 +58,14 @@ add_library(video_core STATIC
|
||||||
renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h
|
renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h
|
||||||
renderer_opengl/texture_filters/bicubic/bicubic.cpp
|
renderer_opengl/texture_filters/bicubic/bicubic.cpp
|
||||||
renderer_opengl/texture_filters/bicubic/bicubic.h
|
renderer_opengl/texture_filters/bicubic/bicubic.h
|
||||||
renderer_opengl/texture_filters/texture_filter_interface.h
|
renderer_opengl/texture_filters/texture_filter_base.h
|
||||||
renderer_opengl/texture_filters/texture_filter_manager.cpp
|
renderer_opengl/texture_filters/texture_filterer.cpp
|
||||||
renderer_opengl/texture_filters/texture_filter_manager.h
|
renderer_opengl/texture_filters/texture_filterer.h
|
||||||
renderer_opengl/texture_filters/xbrz/xbrz_freescale.cpp
|
renderer_opengl/texture_filters/xbrz/xbrz_freescale.cpp
|
||||||
renderer_opengl/texture_filters/xbrz/xbrz_freescale.h
|
renderer_opengl/texture_filters/xbrz/xbrz_freescale.h
|
||||||
|
#temporary, move these back in alphabetical order before merging
|
||||||
|
renderer_opengl/gl_format_reinterpreter.cpp
|
||||||
|
renderer_opengl/gl_format_reinterpreter.h
|
||||||
shader/debug_data.h
|
shader/debug_data.h
|
||||||
shader/shader.cpp
|
shader/shader.cpp
|
||||||
shader/shader.h
|
shader/shader.h
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <glad/glad.h>
|
||||||
|
#include "core/frontend/emu_window.h"
|
||||||
|
#include "core/frontend/scope_acquire_context.h"
|
||||||
|
#include "video_core/renderer_opengl/frame_dumper_opengl.h"
|
||||||
|
#include "video_core/renderer_opengl/renderer_opengl.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
FrameDumperOpenGL::FrameDumperOpenGL(VideoDumper::Backend& video_dumper_,
|
||||||
|
Frontend::EmuWindow& emu_window)
|
||||||
|
: video_dumper(video_dumper_), context(emu_window.CreateSharedContext()) {}
|
||||||
|
|
||||||
|
FrameDumperOpenGL::~FrameDumperOpenGL() {
|
||||||
|
if (present_thread.joinable())
|
||||||
|
present_thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FrameDumperOpenGL::IsDumping() const {
|
||||||
|
return video_dumper.IsDumping();
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout::FramebufferLayout FrameDumperOpenGL::GetLayout() const {
|
||||||
|
return video_dumper.GetLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameDumperOpenGL::StartDumping() {
|
||||||
|
if (present_thread.joinable())
|
||||||
|
present_thread.join();
|
||||||
|
|
||||||
|
present_thread = std::thread(&FrameDumperOpenGL::PresentLoop, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameDumperOpenGL::StopDumping() {
|
||||||
|
stop_requested.store(true, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameDumperOpenGL::PresentLoop() {
|
||||||
|
Frontend::ScopeAcquireContext scope{*context};
|
||||||
|
InitializeOpenGLObjects();
|
||||||
|
|
||||||
|
const auto& layout = GetLayout();
|
||||||
|
while (!stop_requested.exchange(false)) {
|
||||||
|
auto frame = mailbox->TryGetPresentFrame(200);
|
||||||
|
if (!frame) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame->color_reloaded) {
|
||||||
|
LOG_DEBUG(Render_OpenGL, "Reloading present frame");
|
||||||
|
mailbox->ReloadPresentFrame(frame, layout.width, layout.height);
|
||||||
|
}
|
||||||
|
glWaitSync(frame->render_fence, 0, GL_TIMEOUT_IGNORED);
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, frame->present.handle);
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, pbos[current_pbo].handle);
|
||||||
|
glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, 0);
|
||||||
|
|
||||||
|
// Insert fence for the main thread to block on
|
||||||
|
frame->present_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||||
|
glFlush();
|
||||||
|
|
||||||
|
// Bind the previous PBO and read the pixels
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, pbos[next_pbo].handle);
|
||||||
|
GLubyte* pixels = static_cast<GLubyte*>(glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY));
|
||||||
|
VideoDumper::VideoFrame frame_data{layout.width, layout.height, pixels};
|
||||||
|
video_dumper.AddVideoFrame(std::move(frame_data));
|
||||||
|
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
|
||||||
|
current_pbo = (current_pbo + 1) % 2;
|
||||||
|
next_pbo = (current_pbo + 1) % 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
CleanupOpenGLObjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameDumperOpenGL::InitializeOpenGLObjects() {
|
||||||
|
const auto& layout = GetLayout();
|
||||||
|
for (auto& buffer : pbos) {
|
||||||
|
buffer.Create();
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, buffer.handle);
|
||||||
|
glBufferData(GL_PIXEL_PACK_BUFFER, layout.width * layout.height * 4, nullptr,
|
||||||
|
GL_STREAM_READ);
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameDumperOpenGL::CleanupOpenGLObjects() {
|
||||||
|
for (auto& buffer : pbos) {
|
||||||
|
buffer.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
|
@ -0,0 +1,57 @@
|
||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
|
#include <thread>
|
||||||
|
#include "core/dumping/backend.h"
|
||||||
|
#include "core/frontend/framebuffer_layout.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
||||||
|
|
||||||
|
namespace Frontend {
|
||||||
|
class EmuWindow;
|
||||||
|
class GraphicsContext;
|
||||||
|
class TextureMailbox;
|
||||||
|
} // namespace Frontend
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
class RendererOpenGL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the 'presentation' part in frame dumping.
|
||||||
|
* Processes frames/textures sent to its mailbox, downloads the pixels and sends the data
|
||||||
|
* to the video encoding backend.
|
||||||
|
*/
|
||||||
|
class FrameDumperOpenGL {
|
||||||
|
public:
|
||||||
|
explicit FrameDumperOpenGL(VideoDumper::Backend& video_dumper, Frontend::EmuWindow& emu_window);
|
||||||
|
~FrameDumperOpenGL();
|
||||||
|
|
||||||
|
bool IsDumping() const;
|
||||||
|
Layout::FramebufferLayout GetLayout() const;
|
||||||
|
void StartDumping();
|
||||||
|
void StopDumping();
|
||||||
|
|
||||||
|
std::unique_ptr<Frontend::TextureMailbox> mailbox;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void InitializeOpenGLObjects();
|
||||||
|
void CleanupOpenGLObjects();
|
||||||
|
void PresentLoop();
|
||||||
|
|
||||||
|
VideoDumper::Backend& video_dumper;
|
||||||
|
std::unique_ptr<Frontend::GraphicsContext> context;
|
||||||
|
std::thread present_thread;
|
||||||
|
std::atomic_bool stop_requested{false};
|
||||||
|
|
||||||
|
// PBOs used to dump frames faster
|
||||||
|
std::array<OGLBuffer, 2> pbos;
|
||||||
|
GLuint current_pbo = 1;
|
||||||
|
GLuint next_pbo = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
|
@ -0,0 +1,238 @@
|
||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include "common/assert.h"
|
||||||
|
#include "common/scope_exit.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_format_reinterpreter.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_rasterizer_cache.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_state.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_vars.h"
|
||||||
|
#include "video_core/renderer_opengl/texture_filters/texture_filterer.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
using PixelFormat = SurfaceParams::PixelFormat;
|
||||||
|
|
||||||
|
class RGBA4toRGB5A1 final : public FormatReinterpreterBase {
|
||||||
|
public:
|
||||||
|
RGBA4toRGB5A1() {
|
||||||
|
constexpr std::string_view vs_source = R"(
|
||||||
|
out vec2 dst_coord;
|
||||||
|
|
||||||
|
uniform mediump ivec2 dst_size;
|
||||||
|
|
||||||
|
const vec2 vertices[4] =
|
||||||
|
vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0));
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0);
|
||||||
|
dst_coord = (vertices[gl_VertexID] / 2.0 + 0.5) * vec2(dst_size);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr std::string_view fs_source = R"(
|
||||||
|
in mediump vec2 dst_coord;
|
||||||
|
|
||||||
|
out lowp vec4 frag_color;
|
||||||
|
|
||||||
|
uniform lowp sampler2D source;
|
||||||
|
uniform mediump ivec2 dst_size;
|
||||||
|
uniform mediump ivec2 src_size;
|
||||||
|
uniform mediump ivec2 src_offset;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
mediump ivec2 tex_coord;
|
||||||
|
if (src_size == dst_size) {
|
||||||
|
tex_coord = ivec2(dst_coord);
|
||||||
|
} else {
|
||||||
|
highp int tex_index = int(dst_coord.y) * dst_size.x + int(dst_coord.x);
|
||||||
|
mediump int y = tex_index / src_size.x;
|
||||||
|
tex_coord = ivec2(tex_index - y * src_size.x, y);
|
||||||
|
}
|
||||||
|
tex_coord -= src_offset;
|
||||||
|
|
||||||
|
lowp ivec4 rgba4 = ivec4(texelFetch(source, tex_coord, 0) * (exp2(4.0) - 1.0));
|
||||||
|
lowp ivec3 rgb5 =
|
||||||
|
((rgba4.rgb << ivec3(1, 2, 3)) | (rgba4.gba >> ivec3(3, 2, 1))) & 0x1F;
|
||||||
|
frag_color = vec4(vec3(rgb5) / (exp2(5.0) - 1.0), rgba4.a & 0x01);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
program.Create(vs_source.data(), fs_source.data());
|
||||||
|
dst_size_loc = glGetUniformLocation(program.handle, "dst_size");
|
||||||
|
src_size_loc = glGetUniformLocation(program.handle, "src_size");
|
||||||
|
src_offset_loc = glGetUniformLocation(program.handle, "src_offset");
|
||||||
|
vao.Create();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Reinterpret(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint read_fb_handle,
|
||||||
|
GLuint dst_tex, const Common::Rectangle<u32>& dst_rect,
|
||||||
|
GLuint draw_fb_handle) override {
|
||||||
|
OpenGLState prev_state = OpenGLState::GetCurState();
|
||||||
|
SCOPE_EXIT({ prev_state.Apply(); });
|
||||||
|
|
||||||
|
OpenGLState state;
|
||||||
|
state.texture_units[0].texture_2d = src_tex;
|
||||||
|
state.draw.draw_framebuffer = draw_fb_handle;
|
||||||
|
state.draw.shader_program = program.handle;
|
||||||
|
state.draw.vertex_array = vao.handle;
|
||||||
|
state.viewport = {static_cast<GLint>(dst_rect.left), static_cast<GLint>(dst_rect.bottom),
|
||||||
|
static_cast<GLsizei>(dst_rect.GetWidth()),
|
||||||
|
static_cast<GLsizei>(dst_rect.GetHeight())};
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex,
|
||||||
|
0);
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0,
|
||||||
|
0);
|
||||||
|
|
||||||
|
glUniform2i(dst_size_loc, dst_rect.GetWidth(), dst_rect.GetHeight());
|
||||||
|
glUniform2i(src_size_loc, src_rect.GetWidth(), src_rect.GetHeight());
|
||||||
|
glUniform2i(src_offset_loc, src_rect.left, src_rect.bottom);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
OGLProgram program;
|
||||||
|
GLint dst_size_loc{-1}, src_size_loc{-1}, src_offset_loc{-1};
|
||||||
|
OGLVertexArray vao;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PixelBufferD24S8toABGR final : public FormatReinterpreterBase {
|
||||||
|
public:
|
||||||
|
PixelBufferD24S8toABGR() {
|
||||||
|
attributeless_vao.Create();
|
||||||
|
d24s8_abgr_buffer.Create();
|
||||||
|
d24s8_abgr_buffer_size = 0;
|
||||||
|
|
||||||
|
constexpr std::string_view vs_source = R"(
|
||||||
|
const vec2 vertices[4] = vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0),
|
||||||
|
vec2(-1.0, 1.0), vec2(1.0, 1.0));
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
std::string fs_source = GLES ? fragment_shader_precision_OES : "";
|
||||||
|
fs_source += R"(
|
||||||
|
uniform samplerBuffer tbo;
|
||||||
|
uniform vec2 tbo_size;
|
||||||
|
uniform vec4 viewport;
|
||||||
|
|
||||||
|
out vec4 color;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 tbo_coord = (gl_FragCoord.xy - viewport.xy) * tbo_size / viewport.zw;
|
||||||
|
int tbo_offset = int(tbo_coord.y) * int(tbo_size.x) + int(tbo_coord.x);
|
||||||
|
color = texelFetch(tbo, tbo_offset).rabg;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
d24s8_abgr_shader.Create(vs_source.data(), fs_source.c_str());
|
||||||
|
|
||||||
|
OpenGLState state = OpenGLState::GetCurState();
|
||||||
|
GLuint old_program = state.draw.shader_program;
|
||||||
|
state.draw.shader_program = d24s8_abgr_shader.handle;
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
|
GLint tbo_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "tbo");
|
||||||
|
ASSERT(tbo_u_id != -1);
|
||||||
|
glUniform1i(tbo_u_id, 0);
|
||||||
|
|
||||||
|
state.draw.shader_program = old_program;
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
|
d24s8_abgr_tbo_size_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "tbo_size");
|
||||||
|
ASSERT(d24s8_abgr_tbo_size_u_id != -1);
|
||||||
|
d24s8_abgr_viewport_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "viewport");
|
||||||
|
ASSERT(d24s8_abgr_viewport_u_id != -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
~PixelBufferD24S8toABGR() {}
|
||||||
|
|
||||||
|
void Reinterpret(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint read_fb_handle,
|
||||||
|
GLuint dst_tex, const Common::Rectangle<u32>& dst_rect,
|
||||||
|
GLuint draw_fb_handle) override {
|
||||||
|
OpenGLState prev_state = OpenGLState::GetCurState();
|
||||||
|
SCOPE_EXIT({ prev_state.Apply(); });
|
||||||
|
|
||||||
|
OpenGLState state;
|
||||||
|
state.draw.read_framebuffer = read_fb_handle;
|
||||||
|
state.draw.draw_framebuffer = draw_fb_handle;
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, d24s8_abgr_buffer.handle);
|
||||||
|
|
||||||
|
GLsizeiptr target_pbo_size =
|
||||||
|
static_cast<GLsizeiptr>(src_rect.GetWidth()) * src_rect.GetHeight() * 4;
|
||||||
|
if (target_pbo_size > d24s8_abgr_buffer_size) {
|
||||||
|
d24s8_abgr_buffer_size = target_pbo_size * 2;
|
||||||
|
glBufferData(GL_PIXEL_PACK_BUFFER, d24s8_abgr_buffer_size, nullptr, GL_STREAM_COPY);
|
||||||
|
}
|
||||||
|
|
||||||
|
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||||
|
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D,
|
||||||
|
src_tex, 0);
|
||||||
|
glReadPixels(static_cast<GLint>(src_rect.left), static_cast<GLint>(src_rect.bottom),
|
||||||
|
static_cast<GLsizei>(src_rect.GetWidth()),
|
||||||
|
static_cast<GLsizei>(src_rect.GetHeight()), GL_DEPTH_STENCIL,
|
||||||
|
GL_UNSIGNED_INT_24_8, 0);
|
||||||
|
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
|
||||||
|
// PBO now contains src_tex in RABG format
|
||||||
|
state.draw.shader_program = d24s8_abgr_shader.handle;
|
||||||
|
state.draw.vertex_array = attributeless_vao.handle;
|
||||||
|
state.viewport.x = static_cast<GLint>(dst_rect.left);
|
||||||
|
state.viewport.y = static_cast<GLint>(dst_rect.bottom);
|
||||||
|
state.viewport.width = static_cast<GLsizei>(dst_rect.GetWidth());
|
||||||
|
state.viewport.height = static_cast<GLsizei>(dst_rect.GetHeight());
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
|
OGLTexture tbo;
|
||||||
|
tbo.Create();
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_BUFFER, tbo.handle);
|
||||||
|
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, d24s8_abgr_buffer.handle);
|
||||||
|
|
||||||
|
glUniform2f(d24s8_abgr_tbo_size_u_id, static_cast<GLfloat>(src_rect.GetWidth()),
|
||||||
|
static_cast<GLfloat>(src_rect.GetHeight()));
|
||||||
|
glUniform4f(d24s8_abgr_viewport_u_id, static_cast<GLfloat>(state.viewport.x),
|
||||||
|
static_cast<GLfloat>(state.viewport.y),
|
||||||
|
static_cast<GLfloat>(state.viewport.width),
|
||||||
|
static_cast<GLfloat>(state.viewport.height));
|
||||||
|
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex,
|
||||||
|
0);
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0,
|
||||||
|
0);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
OGLVertexArray attributeless_vao;
|
||||||
|
OGLBuffer d24s8_abgr_buffer;
|
||||||
|
GLsizeiptr d24s8_abgr_buffer_size;
|
||||||
|
OGLProgram d24s8_abgr_shader;
|
||||||
|
GLint d24s8_abgr_tbo_size_u_id;
|
||||||
|
GLint d24s8_abgr_viewport_u_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
FormatReinterpreterOpenGL::FormatReinterpreterOpenGL() {
|
||||||
|
reinterpreters.emplace(PixelFormatPair{PixelFormat::RGBA8, PixelFormat::D24S8},
|
||||||
|
std::make_unique<PixelBufferD24S8toABGR>());
|
||||||
|
reinterpreters.emplace(PixelFormatPair{PixelFormat::RGB5A1, PixelFormat::RGBA4},
|
||||||
|
std::make_unique<RGBA4toRGB5A1>());
|
||||||
|
}
|
||||||
|
|
||||||
|
FormatReinterpreterOpenGL::~FormatReinterpreterOpenGL() = default;
|
||||||
|
|
||||||
|
std::pair<FormatReinterpreterOpenGL::ReinterpreterMap::iterator,
|
||||||
|
FormatReinterpreterOpenGL::ReinterpreterMap::iterator>
|
||||||
|
FormatReinterpreterOpenGL::GetPossibleReinterpretations(PixelFormat dst_format) {
|
||||||
|
return reinterpreters.equal_range(dst_format);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
|
@ -0,0 +1,62 @@
|
||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <glad/glad.h>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/math_util.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_surface_params.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
class RasterizerCacheOpenGL;
|
||||||
|
|
||||||
|
struct PixelFormatPair {
|
||||||
|
const SurfaceParams::PixelFormat dst_format, src_format;
|
||||||
|
struct less {
|
||||||
|
using is_transparent = void;
|
||||||
|
constexpr bool operator()(OpenGL::PixelFormatPair lhs, OpenGL::PixelFormatPair rhs) const {
|
||||||
|
return std::tie(lhs.dst_format, lhs.src_format) <
|
||||||
|
std::tie(rhs.dst_format, rhs.src_format);
|
||||||
|
}
|
||||||
|
constexpr bool operator()(OpenGL::SurfaceParams::PixelFormat lhs,
|
||||||
|
OpenGL::PixelFormatPair rhs) const {
|
||||||
|
return lhs < rhs.dst_format;
|
||||||
|
}
|
||||||
|
constexpr bool operator()(OpenGL::PixelFormatPair lhs,
|
||||||
|
OpenGL::SurfaceParams::PixelFormat rhs) const {
|
||||||
|
return lhs.dst_format < rhs;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class FormatReinterpreterBase {
|
||||||
|
public:
|
||||||
|
virtual ~FormatReinterpreterBase() = default;
|
||||||
|
|
||||||
|
virtual void Reinterpret(GLuint src_tex, const Common::Rectangle<u32>& src_rect,
|
||||||
|
GLuint read_fb_handle, GLuint dst_tex,
|
||||||
|
const Common::Rectangle<u32>& dst_rect, GLuint draw_fb_handle) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FormatReinterpreterOpenGL : NonCopyable {
|
||||||
|
using ReinterpreterMap =
|
||||||
|
std::map<PixelFormatPair, std::unique_ptr<FormatReinterpreterBase>, PixelFormatPair::less>;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit FormatReinterpreterOpenGL();
|
||||||
|
~FormatReinterpreterOpenGL();
|
||||||
|
|
||||||
|
std::pair<ReinterpreterMap::iterator, ReinterpreterMap::iterator> GetPossibleReinterpretations(
|
||||||
|
SurfaceParams::PixelFormat dst_format);
|
||||||
|
|
||||||
|
private:
|
||||||
|
ReinterpreterMap reinterpreters;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
|
@ -5,6 +5,7 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <bitset>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
@ -31,10 +32,11 @@
|
||||||
#include "core/settings.h"
|
#include "core/settings.h"
|
||||||
#include "video_core/pica_state.h"
|
#include "video_core/pica_state.h"
|
||||||
#include "video_core/renderer_base.h"
|
#include "video_core/renderer_base.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_format_reinterpreter.h"
|
||||||
#include "video_core/renderer_opengl/gl_rasterizer_cache.h"
|
#include "video_core/renderer_opengl/gl_rasterizer_cache.h"
|
||||||
#include "video_core/renderer_opengl/gl_state.h"
|
#include "video_core/renderer_opengl/gl_state.h"
|
||||||
#include "video_core/renderer_opengl/gl_vars.h"
|
#include "video_core/renderer_opengl/gl_vars.h"
|
||||||
#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h"
|
#include "video_core/renderer_opengl/texture_filters/texture_filterer.h"
|
||||||
#include "video_core/utils.h"
|
#include "video_core/utils.h"
|
||||||
#include "video_core/video_core.h"
|
#include "video_core/video_core.h"
|
||||||
|
|
||||||
|
@ -493,125 +495,6 @@ static bool FillSurface(const Surface& surface, const u8* fill_data,
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
SurfaceParams SurfaceParams::FromInterval(SurfaceInterval interval) const {
|
|
||||||
SurfaceParams params = *this;
|
|
||||||
const u32 tiled_size = is_tiled ? 8 : 1;
|
|
||||||
const u32 stride_tiled_bytes = BytesInPixels(stride * tiled_size);
|
|
||||||
PAddr aligned_start =
|
|
||||||
addr + Common::AlignDown(boost::icl::first(interval) - addr, stride_tiled_bytes);
|
|
||||||
PAddr aligned_end =
|
|
||||||
addr + Common::AlignUp(boost::icl::last_next(interval) - addr, stride_tiled_bytes);
|
|
||||||
|
|
||||||
if (aligned_end - aligned_start > stride_tiled_bytes) {
|
|
||||||
params.addr = aligned_start;
|
|
||||||
params.height = (aligned_end - aligned_start) / BytesInPixels(stride);
|
|
||||||
} else {
|
|
||||||
// 1 row
|
|
||||||
ASSERT(aligned_end - aligned_start == stride_tiled_bytes);
|
|
||||||
const u32 tiled_alignment = BytesInPixels(is_tiled ? 8 * 8 : 1);
|
|
||||||
aligned_start =
|
|
||||||
addr + Common::AlignDown(boost::icl::first(interval) - addr, tiled_alignment);
|
|
||||||
aligned_end =
|
|
||||||
addr + Common::AlignUp(boost::icl::last_next(interval) - addr, tiled_alignment);
|
|
||||||
params.addr = aligned_start;
|
|
||||||
params.width = PixelsInBytes(aligned_end - aligned_start) / tiled_size;
|
|
||||||
params.stride = params.width;
|
|
||||||
params.height = tiled_size;
|
|
||||||
}
|
|
||||||
params.UpdateParams();
|
|
||||||
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
SurfaceInterval SurfaceParams::GetSubRectInterval(Common::Rectangle<u32> unscaled_rect) const {
|
|
||||||
if (unscaled_rect.GetHeight() == 0 || unscaled_rect.GetWidth() == 0) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_tiled) {
|
|
||||||
unscaled_rect.left = Common::AlignDown(unscaled_rect.left, 8) * 8;
|
|
||||||
unscaled_rect.bottom = Common::AlignDown(unscaled_rect.bottom, 8) / 8;
|
|
||||||
unscaled_rect.right = Common::AlignUp(unscaled_rect.right, 8) * 8;
|
|
||||||
unscaled_rect.top = Common::AlignUp(unscaled_rect.top, 8) / 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
const u32 stride_tiled = !is_tiled ? stride : stride * 8;
|
|
||||||
|
|
||||||
const u32 pixel_offset =
|
|
||||||
stride_tiled * (!is_tiled ? unscaled_rect.bottom : (height / 8) - unscaled_rect.top) +
|
|
||||||
unscaled_rect.left;
|
|
||||||
|
|
||||||
const u32 pixels = (unscaled_rect.GetHeight() - 1) * stride_tiled + unscaled_rect.GetWidth();
|
|
||||||
|
|
||||||
return {addr + BytesInPixels(pixel_offset), addr + BytesInPixels(pixel_offset + pixels)};
|
|
||||||
}
|
|
||||||
|
|
||||||
Common::Rectangle<u32> SurfaceParams::GetSubRect(const SurfaceParams& sub_surface) const {
|
|
||||||
const u32 begin_pixel_index = PixelsInBytes(sub_surface.addr - addr);
|
|
||||||
|
|
||||||
if (is_tiled) {
|
|
||||||
const int x0 = (begin_pixel_index % (stride * 8)) / 8;
|
|
||||||
const int y0 = (begin_pixel_index / (stride * 8)) * 8;
|
|
||||||
// Top to bottom
|
|
||||||
return Common::Rectangle<u32>(x0, height - y0, x0 + sub_surface.width,
|
|
||||||
height - (y0 + sub_surface.height));
|
|
||||||
}
|
|
||||||
|
|
||||||
const int x0 = begin_pixel_index % stride;
|
|
||||||
const int y0 = begin_pixel_index / stride;
|
|
||||||
// Bottom to top
|
|
||||||
return Common::Rectangle<u32>(x0, y0 + sub_surface.height, x0 + sub_surface.width, y0);
|
|
||||||
}
|
|
||||||
|
|
||||||
Common::Rectangle<u32> SurfaceParams::GetScaledSubRect(const SurfaceParams& sub_surface) const {
|
|
||||||
auto rect = GetSubRect(sub_surface);
|
|
||||||
rect.left = rect.left * res_scale;
|
|
||||||
rect.right = rect.right * res_scale;
|
|
||||||
rect.top = rect.top * res_scale;
|
|
||||||
rect.bottom = rect.bottom * res_scale;
|
|
||||||
return rect;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SurfaceParams::ExactMatch(const SurfaceParams& other_surface) const {
|
|
||||||
return std::tie(other_surface.addr, other_surface.width, other_surface.height,
|
|
||||||
other_surface.stride, other_surface.pixel_format, other_surface.is_tiled) ==
|
|
||||||
std::tie(addr, width, height, stride, pixel_format, is_tiled) &&
|
|
||||||
pixel_format != PixelFormat::Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SurfaceParams::CanSubRect(const SurfaceParams& sub_surface) const {
|
|
||||||
return sub_surface.addr >= addr && sub_surface.end <= end &&
|
|
||||||
sub_surface.pixel_format == pixel_format && pixel_format != PixelFormat::Invalid &&
|
|
||||||
sub_surface.is_tiled == is_tiled &&
|
|
||||||
(sub_surface.addr - addr) % BytesInPixels(is_tiled ? 64 : 1) == 0 &&
|
|
||||||
(sub_surface.stride == stride || sub_surface.height <= (is_tiled ? 8u : 1u)) &&
|
|
||||||
GetSubRect(sub_surface).right <= stride;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SurfaceParams::CanExpand(const SurfaceParams& expanded_surface) const {
|
|
||||||
return pixel_format != PixelFormat::Invalid && pixel_format == expanded_surface.pixel_format &&
|
|
||||||
addr <= expanded_surface.end && expanded_surface.addr <= end &&
|
|
||||||
is_tiled == expanded_surface.is_tiled && stride == expanded_surface.stride &&
|
|
||||||
(std::max(expanded_surface.addr, addr) - std::min(expanded_surface.addr, addr)) %
|
|
||||||
BytesInPixels(stride * (is_tiled ? 8 : 1)) ==
|
|
||||||
0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SurfaceParams::CanTexCopy(const SurfaceParams& texcopy_params) const {
|
|
||||||
if (pixel_format == PixelFormat::Invalid || addr > texcopy_params.addr ||
|
|
||||||
end < texcopy_params.end) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (texcopy_params.width != texcopy_params.stride) {
|
|
||||||
const u32 tile_stride = BytesInPixels(stride * (is_tiled ? 8 : 1));
|
|
||||||
return (texcopy_params.addr - addr) % BytesInPixels(is_tiled ? 64 : 1) == 0 &&
|
|
||||||
texcopy_params.width % BytesInPixels(is_tiled ? 64 : 1) == 0 &&
|
|
||||||
(texcopy_params.height == 1 || texcopy_params.stride == tile_stride) &&
|
|
||||||
((texcopy_params.addr - addr) % tile_stride) + texcopy_params.width <= tile_stride;
|
|
||||||
}
|
|
||||||
return FromInterval(texcopy_params.GetInterval()).GetInterval() == texcopy_params.GetInterval();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool CachedSurface::CanFill(const SurfaceParams& dest_surface,
|
bool CachedSurface::CanFill(const SurfaceParams& dest_surface,
|
||||||
SurfaceInterval fill_interval) const {
|
SurfaceInterval fill_interval) const {
|
||||||
if (type == SurfaceType::Fill && IsRegionValid(fill_interval) &&
|
if (type == SurfaceType::Fill && IsRegionValid(fill_interval) &&
|
||||||
|
@ -653,47 +536,6 @@ bool CachedSurface::CanCopy(const SurfaceParams& dest_surface,
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
SurfaceInterval SurfaceParams::GetCopyableInterval(const Surface& src_surface) const {
|
|
||||||
SurfaceInterval result{};
|
|
||||||
const auto valid_regions =
|
|
||||||
SurfaceRegions(GetInterval() & src_surface->GetInterval()) - src_surface->invalid_regions;
|
|
||||||
for (auto& valid_interval : valid_regions) {
|
|
||||||
const SurfaceInterval aligned_interval{
|
|
||||||
addr + Common::AlignUp(boost::icl::first(valid_interval) - addr,
|
|
||||||
BytesInPixels(is_tiled ? 8 * 8 : 1)),
|
|
||||||
addr + Common::AlignDown(boost::icl::last_next(valid_interval) - addr,
|
|
||||||
BytesInPixels(is_tiled ? 8 * 8 : 1))};
|
|
||||||
|
|
||||||
if (BytesInPixels(is_tiled ? 8 * 8 : 1) > boost::icl::length(valid_interval) ||
|
|
||||||
boost::icl::length(aligned_interval) == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the rectangle within aligned_interval
|
|
||||||
const u32 stride_bytes = BytesInPixels(stride) * (is_tiled ? 8 : 1);
|
|
||||||
SurfaceInterval rect_interval{
|
|
||||||
addr + Common::AlignUp(boost::icl::first(aligned_interval) - addr, stride_bytes),
|
|
||||||
addr + Common::AlignDown(boost::icl::last_next(aligned_interval) - addr, stride_bytes),
|
|
||||||
};
|
|
||||||
if (boost::icl::first(rect_interval) > boost::icl::last_next(rect_interval)) {
|
|
||||||
// 1 row
|
|
||||||
rect_interval = aligned_interval;
|
|
||||||
} else if (boost::icl::length(rect_interval) == 0) {
|
|
||||||
// 2 rows that do not make a rectangle, return the larger one
|
|
||||||
const SurfaceInterval row1{boost::icl::first(aligned_interval),
|
|
||||||
boost::icl::first(rect_interval)};
|
|
||||||
const SurfaceInterval row2{boost::icl::first(rect_interval),
|
|
||||||
boost::icl::last_next(aligned_interval)};
|
|
||||||
rect_interval = (boost::icl::length(row1) > boost::icl::length(row2)) ? row1 : row2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (boost::icl::length(rect_interval) > boost::icl::length(result)) {
|
|
||||||
result = rect_interval;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
MICROPROFILE_DEFINE(OpenGL_CopySurface, "OpenGL", "CopySurface", MP_RGB(128, 192, 64));
|
MICROPROFILE_DEFINE(OpenGL_CopySurface, "OpenGL", "CopySurface", MP_RGB(128, 192, 64));
|
||||||
void RasterizerCacheOpenGL::CopySurface(const Surface& src_surface, const Surface& dst_surface,
|
void RasterizerCacheOpenGL::CopySurface(const Surface& src_surface, const Surface& dst_surface,
|
||||||
SurfaceInterval copy_interval) {
|
SurfaceInterval copy_interval) {
|
||||||
|
@ -884,6 +726,16 @@ bool CachedSurface::LoadCustomTexture(u64 tex_hash, Core::CustomTexInfo& tex_inf
|
||||||
}
|
}
|
||||||
|
|
||||||
void CachedSurface::DumpTexture(GLuint target_tex, u64 tex_hash) {
|
void CachedSurface::DumpTexture(GLuint target_tex, u64 tex_hash) {
|
||||||
|
// Make sure the texture size is a power of 2
|
||||||
|
// If not, the surface is actually a framebuffer
|
||||||
|
std::bitset<32> width_bits(width);
|
||||||
|
std::bitset<32> height_bits(height);
|
||||||
|
if (width_bits.count() != 1 || height_bits.count() != 1) {
|
||||||
|
LOG_WARNING(Render_OpenGL, "Not dumping {:016X} because size isn't a power of 2 ({}x{})",
|
||||||
|
tex_hash, width, height);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Dump texture to RGBA8 and encode as PNG
|
// Dump texture to RGBA8 and encode as PNG
|
||||||
const auto& image_interface = Core::System::GetInstance().GetImageInterface();
|
const auto& image_interface = Core::System::GetInstance().GetImageInterface();
|
||||||
auto& custom_tex_cache = Core::System::GetInstance().CustomTexCache();
|
auto& custom_tex_cache = Core::System::GetInstance().CustomTexCache();
|
||||||
|
@ -945,10 +797,6 @@ void CachedSurface::UploadGLTexture(Common::Rectangle<u32> rect, GLuint read_fb_
|
||||||
if (Settings::values.custom_textures)
|
if (Settings::values.custom_textures)
|
||||||
is_custom = LoadCustomTexture(tex_hash, custom_tex_info);
|
is_custom = LoadCustomTexture(tex_hash, custom_tex_info);
|
||||||
|
|
||||||
TextureFilterInterface* const texture_filter =
|
|
||||||
is_custom ? nullptr : TextureFilterManager::GetInstance().GetTextureFilter();
|
|
||||||
const u16 default_scale = texture_filter ? texture_filter->scale_factor : 1;
|
|
||||||
|
|
||||||
// Load data from memory to the surface
|
// Load data from memory to the surface
|
||||||
GLint x0 = static_cast<GLint>(rect.left);
|
GLint x0 = static_cast<GLint>(rect.left);
|
||||||
GLint y0 = static_cast<GLint>(rect.bottom);
|
GLint y0 = static_cast<GLint>(rect.bottom);
|
||||||
|
@ -960,7 +808,7 @@ void CachedSurface::UploadGLTexture(Common::Rectangle<u32> rect, GLuint read_fb_
|
||||||
// If not 1x scale, create 1x texture that we will blit from to replace texture subrect in
|
// If not 1x scale, create 1x texture that we will blit from to replace texture subrect in
|
||||||
// surface
|
// surface
|
||||||
OGLTexture unscaled_tex;
|
OGLTexture unscaled_tex;
|
||||||
if (res_scale != default_scale) {
|
if (res_scale != 1) {
|
||||||
x0 = 0;
|
x0 = 0;
|
||||||
y0 = 0;
|
y0 = 0;
|
||||||
|
|
||||||
|
@ -969,8 +817,7 @@ void CachedSurface::UploadGLTexture(Common::Rectangle<u32> rect, GLuint read_fb_
|
||||||
AllocateSurfaceTexture(unscaled_tex.handle, GetFormatTuple(PixelFormat::RGBA8),
|
AllocateSurfaceTexture(unscaled_tex.handle, GetFormatTuple(PixelFormat::RGBA8),
|
||||||
custom_tex_info.width, custom_tex_info.height);
|
custom_tex_info.width, custom_tex_info.height);
|
||||||
} else {
|
} else {
|
||||||
AllocateSurfaceTexture(unscaled_tex.handle, tuple, rect.GetWidth() * default_scale,
|
AllocateSurfaceTexture(unscaled_tex.handle, tuple, rect.GetWidth(), rect.GetHeight());
|
||||||
rect.GetHeight() * default_scale);
|
|
||||||
}
|
}
|
||||||
target_tex = unscaled_tex.handle;
|
target_tex = unscaled_tex.handle;
|
||||||
}
|
}
|
||||||
|
@ -996,16 +843,6 @@ void CachedSurface::UploadGLTexture(Common::Rectangle<u32> rect, GLuint read_fb_
|
||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
glTexSubImage2D(GL_TEXTURE_2D, 0, x0, y0, custom_tex_info.width, custom_tex_info.height,
|
glTexSubImage2D(GL_TEXTURE_2D, 0, x0, y0, custom_tex_info.width, custom_tex_info.height,
|
||||||
GL_RGBA, GL_UNSIGNED_BYTE, custom_tex_info.tex.data());
|
GL_RGBA, GL_UNSIGNED_BYTE, custom_tex_info.tex.data());
|
||||||
} else if (texture_filter) {
|
|
||||||
if (res_scale == default_scale) {
|
|
||||||
AllocateSurfaceTexture(texture.handle, GetFormatTuple(pixel_format),
|
|
||||||
rect.GetWidth() * default_scale,
|
|
||||||
rect.GetHeight() * default_scale);
|
|
||||||
cur_state.texture_units[0].texture_2d = texture.handle;
|
|
||||||
cur_state.Apply();
|
|
||||||
}
|
|
||||||
texture_filter->scale(*this, {(u32)x0, (u32)y0, rect.GetWidth(), rect.GetHeight()},
|
|
||||||
buffer_offset);
|
|
||||||
} else {
|
} else {
|
||||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast<GLint>(stride));
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast<GLint>(stride));
|
||||||
|
|
||||||
|
@ -1016,13 +853,13 @@ void CachedSurface::UploadGLTexture(Common::Rectangle<u32> rect, GLuint read_fb_
|
||||||
}
|
}
|
||||||
|
|
||||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||||
if (Settings::values.dump_textures && !is_custom && !texture_filter)
|
if (Settings::values.dump_textures && !is_custom)
|
||||||
DumpTexture(target_tex, tex_hash);
|
DumpTexture(target_tex, tex_hash);
|
||||||
|
|
||||||
cur_state.texture_units[0].texture_2d = old_tex;
|
cur_state.texture_units[0].texture_2d = old_tex;
|
||||||
cur_state.Apply();
|
cur_state.Apply();
|
||||||
|
|
||||||
if (res_scale != default_scale) {
|
if (res_scale != 1) {
|
||||||
auto scaled_rect = rect;
|
auto scaled_rect = rect;
|
||||||
scaled_rect.left *= res_scale;
|
scaled_rect.left *= res_scale;
|
||||||
scaled_rect.top *= res_scale;
|
scaled_rect.top *= res_scale;
|
||||||
|
@ -1031,9 +868,12 @@ void CachedSurface::UploadGLTexture(Common::Rectangle<u32> rect, GLuint read_fb_
|
||||||
auto from_rect =
|
auto from_rect =
|
||||||
is_custom ? Common::Rectangle<u32>{0, custom_tex_info.height, custom_tex_info.width, 0}
|
is_custom ? Common::Rectangle<u32>{0, custom_tex_info.height, custom_tex_info.width, 0}
|
||||||
: Common::Rectangle<u32>{0, rect.GetHeight(), rect.GetWidth(), 0};
|
: Common::Rectangle<u32>{0, rect.GetHeight(), rect.GetWidth(), 0};
|
||||||
|
if (!owner.texture_filterer->Filter(unscaled_tex.handle, from_rect, texture.handle,
|
||||||
|
scaled_rect, type, read_fb_handle, draw_fb_handle)) {
|
||||||
BlitTextures(unscaled_tex.handle, from_rect, texture.handle, scaled_rect, type,
|
BlitTextures(unscaled_tex.handle, from_rect, texture.handle, scaled_rect, type,
|
||||||
read_fb_handle, draw_fb_handle);
|
read_fb_handle, draw_fb_handle);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
InvalidateAllWatcher();
|
InvalidateAllWatcher();
|
||||||
}
|
}
|
||||||
|
@ -1221,53 +1061,13 @@ Surface FindMatch(const SurfaceCache& surface_cache, const SurfaceParams& params
|
||||||
}
|
}
|
||||||
|
|
||||||
RasterizerCacheOpenGL::RasterizerCacheOpenGL() {
|
RasterizerCacheOpenGL::RasterizerCacheOpenGL() {
|
||||||
|
resolution_scale_factor = VideoCore::GetResolutionScaleFactor();
|
||||||
|
texture_filterer = std::make_unique<TextureFilterer>(Settings::values.texture_filter_name,
|
||||||
|
resolution_scale_factor);
|
||||||
|
format_reinterpreter = std::make_unique<FormatReinterpreterOpenGL>();
|
||||||
|
|
||||||
read_framebuffer.Create();
|
read_framebuffer.Create();
|
||||||
draw_framebuffer.Create();
|
draw_framebuffer.Create();
|
||||||
|
|
||||||
attributeless_vao.Create();
|
|
||||||
|
|
||||||
d24s8_abgr_buffer.Create();
|
|
||||||
d24s8_abgr_buffer_size = 0;
|
|
||||||
|
|
||||||
std::string vs_source = R"(
|
|
||||||
const vec2 vertices[4] = vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0));
|
|
||||||
void main() {
|
|
||||||
gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
std::string fs_source = GLES ? fragment_shader_precision_OES : "";
|
|
||||||
fs_source += R"(
|
|
||||||
uniform samplerBuffer tbo;
|
|
||||||
uniform vec2 tbo_size;
|
|
||||||
uniform vec4 viewport;
|
|
||||||
|
|
||||||
out vec4 color;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vec2 tbo_coord = (gl_FragCoord.xy - viewport.xy) * tbo_size / viewport.zw;
|
|
||||||
int tbo_offset = int(tbo_coord.y) * int(tbo_size.x) + int(tbo_coord.x);
|
|
||||||
color = texelFetch(tbo, tbo_offset).rabg;
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
d24s8_abgr_shader.Create(vs_source.c_str(), fs_source.c_str());
|
|
||||||
|
|
||||||
OpenGLState state = OpenGLState::GetCurState();
|
|
||||||
GLuint old_program = state.draw.shader_program;
|
|
||||||
state.draw.shader_program = d24s8_abgr_shader.handle;
|
|
||||||
state.Apply();
|
|
||||||
|
|
||||||
GLint tbo_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "tbo");
|
|
||||||
ASSERT(tbo_u_id != -1);
|
|
||||||
glUniform1i(tbo_u_id, 0);
|
|
||||||
|
|
||||||
state.draw.shader_program = old_program;
|
|
||||||
state.Apply();
|
|
||||||
|
|
||||||
d24s8_abgr_tbo_size_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "tbo_size");
|
|
||||||
ASSERT(d24s8_abgr_tbo_size_u_id != -1);
|
|
||||||
d24s8_abgr_viewport_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "viewport");
|
|
||||||
ASSERT(d24s8_abgr_viewport_u_id != -1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RasterizerCacheOpenGL::~RasterizerCacheOpenGL() {
|
RasterizerCacheOpenGL::~RasterizerCacheOpenGL() {
|
||||||
|
@ -1291,64 +1091,6 @@ bool RasterizerCacheOpenGL::BlitSurfaces(const Surface& src_surface,
|
||||||
draw_framebuffer.handle);
|
draw_framebuffer.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RasterizerCacheOpenGL::ConvertD24S8toABGR(GLuint src_tex,
|
|
||||||
const Common::Rectangle<u32>& src_rect,
|
|
||||||
GLuint dst_tex,
|
|
||||||
const Common::Rectangle<u32>& dst_rect) {
|
|
||||||
OpenGLState prev_state = OpenGLState::GetCurState();
|
|
||||||
SCOPE_EXIT({ prev_state.Apply(); });
|
|
||||||
|
|
||||||
OpenGLState state;
|
|
||||||
state.draw.read_framebuffer = read_framebuffer.handle;
|
|
||||||
state.draw.draw_framebuffer = draw_framebuffer.handle;
|
|
||||||
state.Apply();
|
|
||||||
|
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, d24s8_abgr_buffer.handle);
|
|
||||||
|
|
||||||
GLsizeiptr target_pbo_size = src_rect.GetWidth() * src_rect.GetHeight() * 4;
|
|
||||||
if (target_pbo_size > d24s8_abgr_buffer_size) {
|
|
||||||
d24s8_abgr_buffer_size = target_pbo_size * 2;
|
|
||||||
glBufferData(GL_PIXEL_PACK_BUFFER, d24s8_abgr_buffer_size, nullptr, GL_STREAM_COPY);
|
|
||||||
}
|
|
||||||
|
|
||||||
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
|
||||||
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, src_tex,
|
|
||||||
0);
|
|
||||||
glReadPixels(static_cast<GLint>(src_rect.left), static_cast<GLint>(src_rect.bottom),
|
|
||||||
static_cast<GLsizei>(src_rect.GetWidth()),
|
|
||||||
static_cast<GLsizei>(src_rect.GetHeight()), GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8,
|
|
||||||
0);
|
|
||||||
|
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
|
||||||
|
|
||||||
// PBO now contains src_tex in RABG format
|
|
||||||
state.draw.shader_program = d24s8_abgr_shader.handle;
|
|
||||||
state.draw.vertex_array = attributeless_vao.handle;
|
|
||||||
state.viewport.x = static_cast<GLint>(dst_rect.left);
|
|
||||||
state.viewport.y = static_cast<GLint>(dst_rect.bottom);
|
|
||||||
state.viewport.width = static_cast<GLsizei>(dst_rect.GetWidth());
|
|
||||||
state.viewport.height = static_cast<GLsizei>(dst_rect.GetHeight());
|
|
||||||
state.Apply();
|
|
||||||
|
|
||||||
OGLTexture tbo;
|
|
||||||
tbo.Create();
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
|
||||||
glBindTexture(GL_TEXTURE_BUFFER, tbo.handle);
|
|
||||||
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, d24s8_abgr_buffer.handle);
|
|
||||||
|
|
||||||
glUniform2f(d24s8_abgr_tbo_size_u_id, static_cast<GLfloat>(src_rect.GetWidth()),
|
|
||||||
static_cast<GLfloat>(src_rect.GetHeight()));
|
|
||||||
glUniform4f(d24s8_abgr_viewport_u_id, static_cast<GLfloat>(state.viewport.x),
|
|
||||||
static_cast<GLfloat>(state.viewport.y), static_cast<GLfloat>(state.viewport.width),
|
|
||||||
static_cast<GLfloat>(state.viewport.height));
|
|
||||||
|
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, 0);
|
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
|
||||||
|
|
||||||
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
Surface RasterizerCacheOpenGL::GetSurface(const SurfaceParams& params, ScaleMatch match_res_scale,
|
Surface RasterizerCacheOpenGL::GetSurface(const SurfaceParams& params, ScaleMatch match_res_scale,
|
||||||
bool load_if_create) {
|
bool load_if_create) {
|
||||||
if (params.addr == 0 || params.height * params.width == 0) {
|
if (params.addr == 0 || params.height * params.width == 0) {
|
||||||
|
@ -1495,11 +1237,7 @@ Surface RasterizerCacheOpenGL::GetTextureSurface(const Pica::Texture::TextureInf
|
||||||
params.height = info.height;
|
params.height = info.height;
|
||||||
params.is_tiled = true;
|
params.is_tiled = true;
|
||||||
params.pixel_format = SurfaceParams::PixelFormatFromTextureFormat(info.format);
|
params.pixel_format = SurfaceParams::PixelFormatFromTextureFormat(info.format);
|
||||||
TextureFilterInterface* filter{};
|
params.res_scale = texture_filterer->IsNull() ? 1 : resolution_scale_factor;
|
||||||
|
|
||||||
params.res_scale = (filter = TextureFilterManager::GetInstance().GetTextureFilter())
|
|
||||||
? filter->scale_factor
|
|
||||||
: 1;
|
|
||||||
params.UpdateParams();
|
params.UpdateParams();
|
||||||
|
|
||||||
u32 min_width = info.width >> max_level;
|
u32 min_width = info.width >> max_level;
|
||||||
|
@ -1552,7 +1290,7 @@ Surface RasterizerCacheOpenGL::GetTextureSurface(const Pica::Texture::TextureInf
|
||||||
glTexImage2D(GL_TEXTURE_2D, level, format_tuple.internal_format, width >> level,
|
glTexImage2D(GL_TEXTURE_2D, level, format_tuple.internal_format, width >> level,
|
||||||
height >> level, 0, format_tuple.format, format_tuple.type, nullptr);
|
height >> level, 0, format_tuple.format, format_tuple.type, nullptr);
|
||||||
}
|
}
|
||||||
if (surface->is_custom) {
|
if (surface->is_custom || !texture_filterer->IsNull()) {
|
||||||
// TODO: proper mipmap support for custom textures
|
// TODO: proper mipmap support for custom textures
|
||||||
glGenerateMipmap(GL_TEXTURE_2D);
|
glGenerateMipmap(GL_TEXTURE_2D);
|
||||||
}
|
}
|
||||||
|
@ -1588,7 +1326,7 @@ Surface RasterizerCacheOpenGL::GetTextureSurface(const Pica::Texture::TextureInf
|
||||||
}
|
}
|
||||||
state.ResetTexture(level_surface->texture.handle);
|
state.ResetTexture(level_surface->texture.handle);
|
||||||
state.Apply();
|
state.Apply();
|
||||||
if (!surface->is_custom) {
|
if (!surface->is_custom && texture_filterer->IsNull()) {
|
||||||
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||||
level_surface->texture.handle, 0);
|
level_surface->texture.handle, 0);
|
||||||
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,
|
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,
|
||||||
|
@ -1712,10 +1450,9 @@ SurfaceSurfaceRect_Tuple RasterizerCacheOpenGL::GetFramebufferSurfaces(
|
||||||
const auto& config = regs.framebuffer.framebuffer;
|
const auto& config = regs.framebuffer.framebuffer;
|
||||||
|
|
||||||
// update resolution_scale_factor and reset cache if changed
|
// update resolution_scale_factor and reset cache if changed
|
||||||
static u16 resolution_scale_factor = VideoCore::GetResolutionScaleFactor();
|
if ((resolution_scale_factor != VideoCore::GetResolutionScaleFactor()) |
|
||||||
if (resolution_scale_factor != VideoCore::GetResolutionScaleFactor() ||
|
(VideoCore::g_texture_filter_update_requested.exchange(false) &&
|
||||||
TextureFilterManager::GetInstance().IsUpdated()) {
|
texture_filterer->Reset(Settings::values.texture_filter_name, resolution_scale_factor))) {
|
||||||
TextureFilterManager::GetInstance().Reset();
|
|
||||||
resolution_scale_factor = VideoCore::GetResolutionScaleFactor();
|
resolution_scale_factor = VideoCore::GetResolutionScaleFactor();
|
||||||
FlushAll();
|
FlushAll();
|
||||||
while (!surface_cache.empty())
|
while (!surface_cache.empty())
|
||||||
|
@ -1800,7 +1537,7 @@ SurfaceSurfaceRect_Tuple RasterizerCacheOpenGL::GetFramebufferSurfaces(
|
||||||
}
|
}
|
||||||
|
|
||||||
Surface RasterizerCacheOpenGL::GetFillSurface(const GPU::Regs::MemoryFillConfig& config) {
|
Surface RasterizerCacheOpenGL::GetFillSurface(const GPU::Regs::MemoryFillConfig& config) {
|
||||||
Surface new_surface = std::make_shared<CachedSurface>();
|
Surface new_surface = std::make_shared<CachedSurface>(*this);
|
||||||
|
|
||||||
new_surface->addr = config.GetStartAddress();
|
new_surface->addr = config.GetStartAddress();
|
||||||
new_surface->end = config.GetEndAddress();
|
new_surface->end = config.GetEndAddress();
|
||||||
|
@ -1881,9 +1618,15 @@ void RasterizerCacheOpenGL::ValidateSurface(const Surface& surface, PAddr addr,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto validate_regions = surface->invalid_regions & validate_interval;
|
||||||
|
auto notify_validated = [&](SurfaceInterval interval) {
|
||||||
|
surface->invalid_regions.erase(interval);
|
||||||
|
validate_regions.erase(interval);
|
||||||
|
};
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const auto it = surface->invalid_regions.find(validate_interval);
|
const auto it = validate_regions.begin();
|
||||||
if (it == surface->invalid_regions.end())
|
if (it == validate_regions.end())
|
||||||
break;
|
break;
|
||||||
|
|
||||||
const auto interval = *it & validate_interval;
|
const auto interval = *it & validate_interval;
|
||||||
|
@ -1895,27 +1638,27 @@ void RasterizerCacheOpenGL::ValidateSurface(const Surface& surface, PAddr addr,
|
||||||
if (copy_surface != nullptr) {
|
if (copy_surface != nullptr) {
|
||||||
SurfaceInterval copy_interval = params.GetCopyableInterval(copy_surface);
|
SurfaceInterval copy_interval = params.GetCopyableInterval(copy_surface);
|
||||||
CopySurface(copy_surface, surface, copy_interval);
|
CopySurface(copy_surface, surface, copy_interval);
|
||||||
surface->invalid_regions.erase(copy_interval);
|
notify_validated(copy_interval);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// D24S8 to RGBA8
|
// Try to find surface in cache with different format
|
||||||
if (surface->pixel_format == PixelFormat::RGBA8) {
|
// that can can be reinterpreted to the requested format.
|
||||||
params.pixel_format = PixelFormat::D24S8;
|
if (ValidateByReinterpretation(surface, params, interval)) {
|
||||||
Surface reinterpret_surface =
|
notify_validated(interval);
|
||||||
FindMatch<MatchFlags::Copy>(surface_cache, params, ScaleMatch::Ignore, interval);
|
continue;
|
||||||
if (reinterpret_surface != nullptr) {
|
}
|
||||||
ASSERT(reinterpret_surface->pixel_format == PixelFormat::D24S8);
|
// Could not find a matching reinterpreter, check if we need to implement a
|
||||||
|
// reinterpreter
|
||||||
SurfaceInterval convert_interval = params.GetCopyableInterval(reinterpret_surface);
|
if (NoUnimplementedReinterpretations(surface, params, interval) &&
|
||||||
SurfaceParams convert_params = surface->FromInterval(convert_interval);
|
!IntervalHasInvalidPixelFormat(params, interval)) {
|
||||||
auto src_rect = reinterpret_surface->GetScaledSubRect(convert_params);
|
// No surfaces were found in the cache that had a matching bit-width.
|
||||||
auto dest_rect = surface->GetScaledSubRect(convert_params);
|
// If the region was created entirely on the GPU,
|
||||||
|
// assume it was a developer mistake and skip flushing.
|
||||||
ConvertD24S8toABGR(reinterpret_surface->texture.handle, src_rect,
|
if (boost::icl::contains(dirty_regions, interval)) {
|
||||||
surface->texture.handle, dest_rect);
|
LOG_DEBUG(Render_OpenGL, "Region created fully on GPU and reinterpretation is "
|
||||||
|
"invalid. Skipping validation");
|
||||||
surface->invalid_regions.erase(convert_interval);
|
validate_regions.erase(interval);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1925,10 +1668,103 @@ void RasterizerCacheOpenGL::ValidateSurface(const Surface& surface, PAddr addr,
|
||||||
surface->LoadGLBuffer(params.addr, params.end);
|
surface->LoadGLBuffer(params.addr, params.end);
|
||||||
surface->UploadGLTexture(surface->GetSubRect(params), read_framebuffer.handle,
|
surface->UploadGLTexture(surface->GetSubRect(params), read_framebuffer.handle,
|
||||||
draw_framebuffer.handle);
|
draw_framebuffer.handle);
|
||||||
surface->invalid_regions.erase(params.GetInterval());
|
notify_validated(params.GetInterval());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool RasterizerCacheOpenGL::NoUnimplementedReinterpretations(const Surface& surface,
|
||||||
|
SurfaceParams& params,
|
||||||
|
const SurfaceInterval& interval) {
|
||||||
|
static constexpr std::array<PixelFormat, 17> all_formats{
|
||||||
|
PixelFormat::RGBA8, PixelFormat::RGB8, PixelFormat::RGB5A1, PixelFormat::RGB565,
|
||||||
|
PixelFormat::RGBA4, PixelFormat::IA8, PixelFormat::RG8, PixelFormat::I8,
|
||||||
|
PixelFormat::A8, PixelFormat::IA4, PixelFormat::I4, PixelFormat::A4,
|
||||||
|
PixelFormat::ETC1, PixelFormat::ETC1A4, PixelFormat::D16, PixelFormat::D24,
|
||||||
|
PixelFormat::D24S8,
|
||||||
|
};
|
||||||
|
bool implemented = true;
|
||||||
|
for (PixelFormat format : all_formats) {
|
||||||
|
if (SurfaceParams::GetFormatBpp(format) == surface->GetFormatBpp()) {
|
||||||
|
params.pixel_format = format;
|
||||||
|
// This could potentially be expensive,
|
||||||
|
// although experimentally it hasn't been too bad
|
||||||
|
Surface test_surface =
|
||||||
|
FindMatch<MatchFlags::Copy>(surface_cache, params, ScaleMatch::Ignore, interval);
|
||||||
|
if (test_surface != nullptr) {
|
||||||
|
LOG_WARNING(Render_OpenGL, "Missing pixel_format reinterpreter: {} -> {}",
|
||||||
|
SurfaceParams::PixelFormatAsString(format),
|
||||||
|
SurfaceParams::PixelFormatAsString(surface->pixel_format));
|
||||||
|
implemented = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return implemented;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RasterizerCacheOpenGL::IntervalHasInvalidPixelFormat(SurfaceParams& params,
|
||||||
|
const SurfaceInterval& interval) {
|
||||||
|
params.pixel_format = PixelFormat::Invalid;
|
||||||
|
for (const auto& set : RangeFromInterval(surface_cache, interval))
|
||||||
|
for (const auto& surface : set.second)
|
||||||
|
if (surface->pixel_format == PixelFormat::Invalid) {
|
||||||
|
LOG_WARNING(Render_OpenGL, "Surface found with invalid pixel format");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RasterizerCacheOpenGL::ValidateByReinterpretation(const Surface& surface,
|
||||||
|
SurfaceParams& params,
|
||||||
|
const SurfaceInterval& interval) {
|
||||||
|
auto [cvt_begin, cvt_end] =
|
||||||
|
format_reinterpreter->GetPossibleReinterpretations(surface->pixel_format);
|
||||||
|
for (auto reinterpreter = cvt_begin; reinterpreter != cvt_end; ++reinterpreter) {
|
||||||
|
PixelFormat format = reinterpreter->first.src_format;
|
||||||
|
params.pixel_format = format;
|
||||||
|
Surface reinterpret_surface =
|
||||||
|
FindMatch<MatchFlags::Copy>(surface_cache, params, ScaleMatch::Ignore, interval);
|
||||||
|
|
||||||
|
if (reinterpret_surface != nullptr) {
|
||||||
|
SurfaceInterval reinterpret_interval = params.GetCopyableInterval(reinterpret_surface);
|
||||||
|
SurfaceParams reinterpret_params = surface->FromInterval(reinterpret_interval);
|
||||||
|
auto src_rect = reinterpret_surface->GetScaledSubRect(reinterpret_params);
|
||||||
|
auto dest_rect = surface->GetScaledSubRect(reinterpret_params);
|
||||||
|
|
||||||
|
if (!texture_filterer->IsNull() && reinterpret_surface->res_scale == 1 &&
|
||||||
|
surface->res_scale == resolution_scale_factor) {
|
||||||
|
// The destination surface is either a framebuffer, or a filtered texture.
|
||||||
|
OGLTexture tmp_tex;
|
||||||
|
tmp_tex.Create();
|
||||||
|
// Create an intermediate surface to convert to before blitting to the
|
||||||
|
// destination.
|
||||||
|
Common::Rectangle<u32> tmp_rect{0, dest_rect.GetHeight() / resolution_scale_factor,
|
||||||
|
dest_rect.GetWidth() / resolution_scale_factor, 0};
|
||||||
|
AllocateSurfaceTexture(tmp_tex.handle,
|
||||||
|
GetFormatTuple(reinterpreter->first.dst_format),
|
||||||
|
tmp_rect.right, tmp_rect.top);
|
||||||
|
reinterpreter->second->Reinterpret(reinterpret_surface->texture.handle, src_rect,
|
||||||
|
read_framebuffer.handle, tmp_tex.handle,
|
||||||
|
tmp_rect, draw_framebuffer.handle);
|
||||||
|
SurfaceParams::SurfaceType type =
|
||||||
|
SurfaceParams::GetFormatType(reinterpreter->first.dst_format);
|
||||||
|
|
||||||
|
if (!texture_filterer->Filter(tmp_tex.handle, tmp_rect, surface->texture.handle,
|
||||||
|
dest_rect, type, read_framebuffer.handle,
|
||||||
|
draw_framebuffer.handle)) {
|
||||||
|
BlitTextures(tmp_tex.handle, tmp_rect, surface->texture.handle, dest_rect, type,
|
||||||
|
read_framebuffer.handle, draw_framebuffer.handle);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reinterpreter->second->Reinterpret(reinterpret_surface->texture.handle, src_rect,
|
||||||
|
read_framebuffer.handle, surface->texture.handle,
|
||||||
|
dest_rect, draw_framebuffer.handle);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void RasterizerCacheOpenGL::ClearAll(bool flush) {
|
void RasterizerCacheOpenGL::ClearAll(bool flush) {
|
||||||
const auto flush_interval = PageMap::interval_type::right_open(0x0, 0xFFFFFFFF);
|
const auto flush_interval = PageMap::interval_type::right_open(0x0, 0xFFFFFFFF);
|
||||||
// Force flush all surfaces from the cache
|
// Force flush all surfaces from the cache
|
||||||
|
@ -2053,7 +1889,7 @@ void RasterizerCacheOpenGL::InvalidateRegion(PAddr addr, u32 size, const Surface
|
||||||
}
|
}
|
||||||
|
|
||||||
Surface RasterizerCacheOpenGL::CreateSurface(const SurfaceParams& params) {
|
Surface RasterizerCacheOpenGL::CreateSurface(const SurfaceParams& params) {
|
||||||
Surface surface = std::make_shared<CachedSurface>();
|
Surface surface = std::make_shared<CachedSurface>(*this);
|
||||||
static_cast<SurfaceParams&>(*surface) = params;
|
static_cast<SurfaceParams&>(*surface) = params;
|
||||||
|
|
||||||
surface->texture.Create();
|
surface->texture.Create();
|
||||||
|
|
|
@ -26,14 +26,16 @@
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "common/math_util.h"
|
#include "common/math_util.h"
|
||||||
#include "core/custom_tex_cache.h"
|
#include "core/custom_tex_cache.h"
|
||||||
#include "core/hw/gpu.h"
|
|
||||||
#include "video_core/regs_framebuffer.h"
|
|
||||||
#include "video_core/regs_texturing.h"
|
|
||||||
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_surface_params.h"
|
||||||
#include "video_core/texture/texture_decode.h"
|
#include "video_core/texture/texture_decode.h"
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
|
class RasterizerCacheOpenGL;
|
||||||
|
class TextureFilterer;
|
||||||
|
class FormatReinterpreterOpenGL;
|
||||||
|
|
||||||
struct TextureCubeConfig {
|
struct TextureCubeConfig {
|
||||||
PAddr px;
|
PAddr px;
|
||||||
PAddr nx;
|
PAddr nx;
|
||||||
|
@ -76,11 +78,8 @@ struct hash<OpenGL::TextureCubeConfig> {
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
struct CachedSurface;
|
|
||||||
using Surface = std::shared_ptr<CachedSurface>;
|
|
||||||
using SurfaceSet = std::set<Surface>;
|
using SurfaceSet = std::set<Surface>;
|
||||||
|
|
||||||
using SurfaceInterval = boost::icl::right_open_interval<PAddr>;
|
|
||||||
using SurfaceRegions = boost::icl::interval_set<PAddr, std::less, SurfaceInterval>;
|
using SurfaceRegions = boost::icl::interval_set<PAddr, std::less, SurfaceInterval>;
|
||||||
using SurfaceMap =
|
using SurfaceMap =
|
||||||
boost::icl::interval_map<PAddr, Surface, boost::icl::partial_absorber, std::less,
|
boost::icl::interval_map<PAddr, Surface, boost::icl::partial_absorber, std::less,
|
||||||
|
@ -104,212 +103,6 @@ enum class ScaleMatch {
|
||||||
Ignore // accept every scaled res
|
Ignore // accept every scaled res
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SurfaceParams {
|
|
||||||
private:
|
|
||||||
static constexpr std::array<unsigned int, 18> BPP_TABLE = {
|
|
||||||
32, // RGBA8
|
|
||||||
24, // RGB8
|
|
||||||
16, // RGB5A1
|
|
||||||
16, // RGB565
|
|
||||||
16, // RGBA4
|
|
||||||
16, // IA8
|
|
||||||
16, // RG8
|
|
||||||
8, // I8
|
|
||||||
8, // A8
|
|
||||||
8, // IA4
|
|
||||||
4, // I4
|
|
||||||
4, // A4
|
|
||||||
4, // ETC1
|
|
||||||
8, // ETC1A4
|
|
||||||
16, // D16
|
|
||||||
0,
|
|
||||||
24, // D24
|
|
||||||
32, // D24S8
|
|
||||||
};
|
|
||||||
|
|
||||||
public:
|
|
||||||
enum class PixelFormat {
|
|
||||||
// First 5 formats are shared between textures and color buffers
|
|
||||||
RGBA8 = 0,
|
|
||||||
RGB8 = 1,
|
|
||||||
RGB5A1 = 2,
|
|
||||||
RGB565 = 3,
|
|
||||||
RGBA4 = 4,
|
|
||||||
|
|
||||||
// Texture-only formats
|
|
||||||
IA8 = 5,
|
|
||||||
RG8 = 6,
|
|
||||||
I8 = 7,
|
|
||||||
A8 = 8,
|
|
||||||
IA4 = 9,
|
|
||||||
I4 = 10,
|
|
||||||
A4 = 11,
|
|
||||||
ETC1 = 12,
|
|
||||||
ETC1A4 = 13,
|
|
||||||
|
|
||||||
// Depth buffer-only formats
|
|
||||||
D16 = 14,
|
|
||||||
// gap
|
|
||||||
D24 = 16,
|
|
||||||
D24S8 = 17,
|
|
||||||
|
|
||||||
Invalid = 255,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class SurfaceType {
|
|
||||||
Color = 0,
|
|
||||||
Texture = 1,
|
|
||||||
Depth = 2,
|
|
||||||
DepthStencil = 3,
|
|
||||||
Fill = 4,
|
|
||||||
Invalid = 5
|
|
||||||
};
|
|
||||||
|
|
||||||
static constexpr unsigned int GetFormatBpp(PixelFormat format) {
|
|
||||||
const auto format_idx = static_cast<std::size_t>(format);
|
|
||||||
DEBUG_ASSERT_MSG(format_idx < BPP_TABLE.size(), "Invalid pixel format {}", format_idx);
|
|
||||||
return BPP_TABLE[format_idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned int GetFormatBpp() const {
|
|
||||||
return GetFormatBpp(pixel_format);
|
|
||||||
}
|
|
||||||
|
|
||||||
static PixelFormat PixelFormatFromTextureFormat(Pica::TexturingRegs::TextureFormat format) {
|
|
||||||
return ((unsigned int)format < 14) ? (PixelFormat)format : PixelFormat::Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
static PixelFormat PixelFormatFromColorFormat(Pica::FramebufferRegs::ColorFormat format) {
|
|
||||||
return ((unsigned int)format < 5) ? (PixelFormat)format : PixelFormat::Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
static PixelFormat PixelFormatFromDepthFormat(Pica::FramebufferRegs::DepthFormat format) {
|
|
||||||
return ((unsigned int)format < 4) ? (PixelFormat)((unsigned int)format + 14)
|
|
||||||
: PixelFormat::Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
static PixelFormat PixelFormatFromGPUPixelFormat(GPU::Regs::PixelFormat format) {
|
|
||||||
switch (format) {
|
|
||||||
// RGB565 and RGB5A1 are switched in PixelFormat compared to ColorFormat
|
|
||||||
case GPU::Regs::PixelFormat::RGB565:
|
|
||||||
return PixelFormat::RGB565;
|
|
||||||
case GPU::Regs::PixelFormat::RGB5A1:
|
|
||||||
return PixelFormat::RGB5A1;
|
|
||||||
default:
|
|
||||||
return ((unsigned int)format < 5) ? (PixelFormat)format : PixelFormat::Invalid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool CheckFormatsBlittable(PixelFormat pixel_format_a, PixelFormat pixel_format_b) {
|
|
||||||
SurfaceType a_type = GetFormatType(pixel_format_a);
|
|
||||||
SurfaceType b_type = GetFormatType(pixel_format_b);
|
|
||||||
|
|
||||||
if ((a_type == SurfaceType::Color || a_type == SurfaceType::Texture) &&
|
|
||||||
(b_type == SurfaceType::Color || b_type == SurfaceType::Texture)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a_type == SurfaceType::Depth && b_type == SurfaceType::Depth) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a_type == SurfaceType::DepthStencil && b_type == SurfaceType::DepthStencil) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static constexpr SurfaceType GetFormatType(PixelFormat pixel_format) {
|
|
||||||
if ((unsigned int)pixel_format < 5) {
|
|
||||||
return SurfaceType::Color;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((unsigned int)pixel_format < 14) {
|
|
||||||
return SurfaceType::Texture;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pixel_format == PixelFormat::D16 || pixel_format == PixelFormat::D24) {
|
|
||||||
return SurfaceType::Depth;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pixel_format == PixelFormat::D24S8) {
|
|
||||||
return SurfaceType::DepthStencil;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SurfaceType::Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the params "size", "end" and "type" from the already set "addr", "width", "height"
|
|
||||||
/// and "pixel_format"
|
|
||||||
void UpdateParams() {
|
|
||||||
if (stride == 0) {
|
|
||||||
stride = width;
|
|
||||||
}
|
|
||||||
type = GetFormatType(pixel_format);
|
|
||||||
size = !is_tiled ? BytesInPixels(stride * (height - 1) + width)
|
|
||||||
: BytesInPixels(stride * 8 * (height / 8 - 1) + width * 8);
|
|
||||||
end = addr + size;
|
|
||||||
}
|
|
||||||
|
|
||||||
SurfaceInterval GetInterval() const {
|
|
||||||
return SurfaceInterval(addr, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the outer rectangle containing "interval"
|
|
||||||
SurfaceParams FromInterval(SurfaceInterval interval) const;
|
|
||||||
|
|
||||||
SurfaceInterval GetSubRectInterval(Common::Rectangle<u32> unscaled_rect) const;
|
|
||||||
|
|
||||||
// Returns the region of the biggest valid rectange within interval
|
|
||||||
SurfaceInterval GetCopyableInterval(const Surface& src_surface) const;
|
|
||||||
|
|
||||||
u32 GetScaledWidth() const {
|
|
||||||
return width * res_scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
u32 GetScaledHeight() const {
|
|
||||||
return height * res_scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
Common::Rectangle<u32> GetRect() const {
|
|
||||||
return {0, height, width, 0};
|
|
||||||
}
|
|
||||||
|
|
||||||
Common::Rectangle<u32> GetScaledRect() const {
|
|
||||||
return {0, GetScaledHeight(), GetScaledWidth(), 0};
|
|
||||||
}
|
|
||||||
|
|
||||||
u32 PixelsInBytes(u32 size) const {
|
|
||||||
return size * CHAR_BIT / GetFormatBpp(pixel_format);
|
|
||||||
}
|
|
||||||
|
|
||||||
u32 BytesInPixels(u32 pixels) const {
|
|
||||||
return pixels * GetFormatBpp(pixel_format) / CHAR_BIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ExactMatch(const SurfaceParams& other_surface) const;
|
|
||||||
bool CanSubRect(const SurfaceParams& sub_surface) const;
|
|
||||||
bool CanExpand(const SurfaceParams& expanded_surface) const;
|
|
||||||
bool CanTexCopy(const SurfaceParams& texcopy_params) const;
|
|
||||||
|
|
||||||
Common::Rectangle<u32> GetSubRect(const SurfaceParams& sub_surface) const;
|
|
||||||
Common::Rectangle<u32> GetScaledSubRect(const SurfaceParams& sub_surface) const;
|
|
||||||
|
|
||||||
PAddr addr = 0;
|
|
||||||
PAddr end = 0;
|
|
||||||
u32 size = 0;
|
|
||||||
|
|
||||||
u32 width = 0;
|
|
||||||
u32 height = 0;
|
|
||||||
u32 stride = 0;
|
|
||||||
u16 res_scale = 1;
|
|
||||||
|
|
||||||
bool is_tiled = false;
|
|
||||||
PixelFormat pixel_format = PixelFormat::Invalid;
|
|
||||||
SurfaceType type = SurfaceType::Invalid;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A watcher that notifies whether a cached surface has been changed. This is useful for caching
|
* A watcher that notifies whether a cached surface has been changed. This is useful for caching
|
||||||
* surface collection objects, including texture cube and mipmap.
|
* surface collection objects, including texture cube and mipmap.
|
||||||
|
@ -345,6 +138,8 @@ private:
|
||||||
};
|
};
|
||||||
|
|
||||||
struct CachedSurface : SurfaceParams, std::enable_shared_from_this<CachedSurface> {
|
struct CachedSurface : SurfaceParams, std::enable_shared_from_this<CachedSurface> {
|
||||||
|
CachedSurface(RasterizerCacheOpenGL& owner) : owner{owner} {}
|
||||||
|
|
||||||
bool CanFill(const SurfaceParams& dest_surface, SurfaceInterval fill_interval) const;
|
bool CanFill(const SurfaceParams& dest_surface, SurfaceInterval fill_interval) const;
|
||||||
bool CanCopy(const SurfaceParams& dest_surface, SurfaceInterval copy_interval) const;
|
bool CanCopy(const SurfaceParams& dest_surface, SurfaceInterval copy_interval) const;
|
||||||
|
|
||||||
|
@ -422,6 +217,7 @@ struct CachedSurface : SurfaceParams, std::enable_shared_from_this<CachedSurface
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
RasterizerCacheOpenGL& owner;
|
||||||
std::list<std::weak_ptr<SurfaceWatcher>> watchers;
|
std::list<std::weak_ptr<SurfaceWatcher>> watchers;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -445,9 +241,6 @@ public:
|
||||||
bool BlitSurfaces(const Surface& src_surface, const Common::Rectangle<u32>& src_rect,
|
bool BlitSurfaces(const Surface& src_surface, const Common::Rectangle<u32>& src_rect,
|
||||||
const Surface& dst_surface, const Common::Rectangle<u32>& dst_rect);
|
const Surface& dst_surface, const Common::Rectangle<u32>& dst_rect);
|
||||||
|
|
||||||
void ConvertD24S8toABGR(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
|
||||||
const Common::Rectangle<u32>& dst_rect);
|
|
||||||
|
|
||||||
/// Copy one surface's region to another
|
/// Copy one surface's region to another
|
||||||
void CopySurface(const Surface& src_surface, const Surface& dst_surface,
|
void CopySurface(const Surface& src_surface, const Surface& dst_surface,
|
||||||
SurfaceInterval copy_interval);
|
SurfaceInterval copy_interval);
|
||||||
|
@ -496,6 +289,18 @@ private:
|
||||||
/// Update surface's texture for given region when necessary
|
/// Update surface's texture for given region when necessary
|
||||||
void ValidateSurface(const Surface& surface, PAddr addr, u32 size);
|
void ValidateSurface(const Surface& surface, PAddr addr, u32 size);
|
||||||
|
|
||||||
|
// Returns false if there is a surface in the cache at the interval with the same bit-width,
|
||||||
|
bool NoUnimplementedReinterpretations(const OpenGL::Surface& surface,
|
||||||
|
OpenGL::SurfaceParams& params,
|
||||||
|
const OpenGL::SurfaceInterval& interval);
|
||||||
|
|
||||||
|
// Return true if a surface with an invalid pixel format exists at the interval
|
||||||
|
bool IntervalHasInvalidPixelFormat(SurfaceParams& params, const SurfaceInterval& interval);
|
||||||
|
|
||||||
|
// Attempt to find a reinterpretable surface in the cache and use it to copy for validation
|
||||||
|
bool ValidateByReinterpretation(const Surface& surface, SurfaceParams& params,
|
||||||
|
const SurfaceInterval& interval);
|
||||||
|
|
||||||
/// Create a new surface
|
/// Create a new surface
|
||||||
Surface CreateSurface(const SurfaceParams& params);
|
Surface CreateSurface(const SurfaceParams& params);
|
||||||
|
|
||||||
|
@ -516,14 +321,13 @@ private:
|
||||||
OGLFramebuffer read_framebuffer;
|
OGLFramebuffer read_framebuffer;
|
||||||
OGLFramebuffer draw_framebuffer;
|
OGLFramebuffer draw_framebuffer;
|
||||||
|
|
||||||
OGLVertexArray attributeless_vao;
|
u16 resolution_scale_factor;
|
||||||
OGLBuffer d24s8_abgr_buffer;
|
|
||||||
GLsizeiptr d24s8_abgr_buffer_size;
|
|
||||||
OGLProgram d24s8_abgr_shader;
|
|
||||||
GLint d24s8_abgr_tbo_size_u_id;
|
|
||||||
GLint d24s8_abgr_viewport_u_id;
|
|
||||||
|
|
||||||
std::unordered_map<TextureCubeConfig, CachedTextureCube> texture_cube_cache;
|
std::unordered_map<TextureCubeConfig, CachedTextureCube> texture_cube_cache;
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::unique_ptr<TextureFilterer> texture_filterer;
|
||||||
|
std::unique_ptr<FormatReinterpreterOpenGL> format_reinterpreter;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FormatTuple {
|
struct FormatTuple {
|
||||||
|
|
|
@ -38,13 +38,6 @@ constexpr GLuint ShadowTexturePZ = 5;
|
||||||
constexpr GLuint ShadowTextureNZ = 6;
|
constexpr GLuint ShadowTextureNZ = 6;
|
||||||
} // namespace ImageUnits
|
} // namespace ImageUnits
|
||||||
|
|
||||||
struct Viewport {
|
|
||||||
GLint x;
|
|
||||||
GLint y;
|
|
||||||
GLsizei width;
|
|
||||||
GLsizei height;
|
|
||||||
};
|
|
||||||
|
|
||||||
class OpenGLState {
|
class OpenGLState {
|
||||||
public:
|
public:
|
||||||
struct {
|
struct {
|
||||||
|
@ -142,7 +135,12 @@ public:
|
||||||
GLsizei height;
|
GLsizei height;
|
||||||
} scissor;
|
} scissor;
|
||||||
|
|
||||||
Viewport viewport;
|
struct {
|
||||||
|
GLint x;
|
||||||
|
GLint y;
|
||||||
|
GLsizei width;
|
||||||
|
GLsizei height;
|
||||||
|
} viewport;
|
||||||
|
|
||||||
std::array<bool, 2> clip_distance; // GL_CLIP_DISTANCE
|
std::array<bool, 2> clip_distance; // GL_CLIP_DISTANCE
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,171 @@
|
||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include "common/alignment.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_rasterizer_cache.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_surface_params.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
SurfaceParams SurfaceParams::FromInterval(SurfaceInterval interval) const {
|
||||||
|
SurfaceParams params = *this;
|
||||||
|
const u32 tiled_size = is_tiled ? 8 : 1;
|
||||||
|
const u32 stride_tiled_bytes = BytesInPixels(stride * tiled_size);
|
||||||
|
PAddr aligned_start =
|
||||||
|
addr + Common::AlignDown(boost::icl::first(interval) - addr, stride_tiled_bytes);
|
||||||
|
PAddr aligned_end =
|
||||||
|
addr + Common::AlignUp(boost::icl::last_next(interval) - addr, stride_tiled_bytes);
|
||||||
|
|
||||||
|
if (aligned_end - aligned_start > stride_tiled_bytes) {
|
||||||
|
params.addr = aligned_start;
|
||||||
|
params.height = (aligned_end - aligned_start) / BytesInPixels(stride);
|
||||||
|
} else {
|
||||||
|
// 1 row
|
||||||
|
ASSERT(aligned_end - aligned_start == stride_tiled_bytes);
|
||||||
|
const u32 tiled_alignment = BytesInPixels(is_tiled ? 8 * 8 : 1);
|
||||||
|
aligned_start =
|
||||||
|
addr + Common::AlignDown(boost::icl::first(interval) - addr, tiled_alignment);
|
||||||
|
aligned_end =
|
||||||
|
addr + Common::AlignUp(boost::icl::last_next(interval) - addr, tiled_alignment);
|
||||||
|
params.addr = aligned_start;
|
||||||
|
params.width = PixelsInBytes(aligned_end - aligned_start) / tiled_size;
|
||||||
|
params.stride = params.width;
|
||||||
|
params.height = tiled_size;
|
||||||
|
}
|
||||||
|
params.UpdateParams();
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
SurfaceInterval SurfaceParams::GetSubRectInterval(Common::Rectangle<u32> unscaled_rect) const {
|
||||||
|
if (unscaled_rect.GetHeight() == 0 || unscaled_rect.GetWidth() == 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_tiled) {
|
||||||
|
unscaled_rect.left = Common::AlignDown(unscaled_rect.left, 8) * 8;
|
||||||
|
unscaled_rect.bottom = Common::AlignDown(unscaled_rect.bottom, 8) / 8;
|
||||||
|
unscaled_rect.right = Common::AlignUp(unscaled_rect.right, 8) * 8;
|
||||||
|
unscaled_rect.top = Common::AlignUp(unscaled_rect.top, 8) / 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
const u32 stride_tiled = !is_tiled ? stride : stride * 8;
|
||||||
|
|
||||||
|
const u32 pixel_offset =
|
||||||
|
stride_tiled * (!is_tiled ? unscaled_rect.bottom : (height / 8) - unscaled_rect.top) +
|
||||||
|
unscaled_rect.left;
|
||||||
|
|
||||||
|
const u32 pixels = (unscaled_rect.GetHeight() - 1) * stride_tiled + unscaled_rect.GetWidth();
|
||||||
|
|
||||||
|
return {addr + BytesInPixels(pixel_offset), addr + BytesInPixels(pixel_offset + pixels)};
|
||||||
|
}
|
||||||
|
|
||||||
|
SurfaceInterval SurfaceParams::GetCopyableInterval(const Surface& src_surface) const {
|
||||||
|
SurfaceInterval result{};
|
||||||
|
const auto valid_regions =
|
||||||
|
SurfaceRegions(GetInterval() & src_surface->GetInterval()) - src_surface->invalid_regions;
|
||||||
|
for (auto& valid_interval : valid_regions) {
|
||||||
|
const SurfaceInterval aligned_interval{
|
||||||
|
addr + Common::AlignUp(boost::icl::first(valid_interval) - addr,
|
||||||
|
BytesInPixels(is_tiled ? 8 * 8 : 1)),
|
||||||
|
addr + Common::AlignDown(boost::icl::last_next(valid_interval) - addr,
|
||||||
|
BytesInPixels(is_tiled ? 8 * 8 : 1))};
|
||||||
|
|
||||||
|
if (BytesInPixels(is_tiled ? 8 * 8 : 1) > boost::icl::length(valid_interval) ||
|
||||||
|
boost::icl::length(aligned_interval) == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the rectangle within aligned_interval
|
||||||
|
const u32 stride_bytes = BytesInPixels(stride) * (is_tiled ? 8 : 1);
|
||||||
|
SurfaceInterval rect_interval{
|
||||||
|
addr + Common::AlignUp(boost::icl::first(aligned_interval) - addr, stride_bytes),
|
||||||
|
addr + Common::AlignDown(boost::icl::last_next(aligned_interval) - addr, stride_bytes),
|
||||||
|
};
|
||||||
|
if (boost::icl::first(rect_interval) > boost::icl::last_next(rect_interval)) {
|
||||||
|
// 1 row
|
||||||
|
rect_interval = aligned_interval;
|
||||||
|
} else if (boost::icl::length(rect_interval) == 0) {
|
||||||
|
// 2 rows that do not make a rectangle, return the larger one
|
||||||
|
const SurfaceInterval row1{boost::icl::first(aligned_interval),
|
||||||
|
boost::icl::first(rect_interval)};
|
||||||
|
const SurfaceInterval row2{boost::icl::first(rect_interval),
|
||||||
|
boost::icl::last_next(aligned_interval)};
|
||||||
|
rect_interval = (boost::icl::length(row1) > boost::icl::length(row2)) ? row1 : row2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (boost::icl::length(rect_interval) > boost::icl::length(result)) {
|
||||||
|
result = rect_interval;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Common::Rectangle<u32> SurfaceParams::GetSubRect(const SurfaceParams& sub_surface) const {
|
||||||
|
const u32 begin_pixel_index = PixelsInBytes(sub_surface.addr - addr);
|
||||||
|
|
||||||
|
if (is_tiled) {
|
||||||
|
const int x0 = (begin_pixel_index % (stride * 8)) / 8;
|
||||||
|
const int y0 = (begin_pixel_index / (stride * 8)) * 8;
|
||||||
|
// Top to bottom
|
||||||
|
return Common::Rectangle<u32>(x0, height - y0, x0 + sub_surface.width,
|
||||||
|
height - (y0 + sub_surface.height));
|
||||||
|
}
|
||||||
|
|
||||||
|
const int x0 = begin_pixel_index % stride;
|
||||||
|
const int y0 = begin_pixel_index / stride;
|
||||||
|
// Bottom to top
|
||||||
|
return Common::Rectangle<u32>(x0, y0 + sub_surface.height, x0 + sub_surface.width, y0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Common::Rectangle<u32> SurfaceParams::GetScaledSubRect(const SurfaceParams& sub_surface) const {
|
||||||
|
auto rect = GetSubRect(sub_surface);
|
||||||
|
rect.left = rect.left * res_scale;
|
||||||
|
rect.right = rect.right * res_scale;
|
||||||
|
rect.top = rect.top * res_scale;
|
||||||
|
rect.bottom = rect.bottom * res_scale;
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SurfaceParams::ExactMatch(const SurfaceParams& other_surface) const {
|
||||||
|
return std::tie(other_surface.addr, other_surface.width, other_surface.height,
|
||||||
|
other_surface.stride, other_surface.pixel_format, other_surface.is_tiled) ==
|
||||||
|
std::tie(addr, width, height, stride, pixel_format, is_tiled) &&
|
||||||
|
pixel_format != PixelFormat::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SurfaceParams::CanSubRect(const SurfaceParams& sub_surface) const {
|
||||||
|
return sub_surface.addr >= addr && sub_surface.end <= end &&
|
||||||
|
sub_surface.pixel_format == pixel_format && pixel_format != PixelFormat::Invalid &&
|
||||||
|
sub_surface.is_tiled == is_tiled &&
|
||||||
|
(sub_surface.addr - addr) % BytesInPixels(is_tiled ? 64 : 1) == 0 &&
|
||||||
|
(sub_surface.stride == stride || sub_surface.height <= (is_tiled ? 8u : 1u)) &&
|
||||||
|
GetSubRect(sub_surface).right <= stride;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SurfaceParams::CanExpand(const SurfaceParams& expanded_surface) const {
|
||||||
|
return pixel_format != PixelFormat::Invalid && pixel_format == expanded_surface.pixel_format &&
|
||||||
|
addr <= expanded_surface.end && expanded_surface.addr <= end &&
|
||||||
|
is_tiled == expanded_surface.is_tiled && stride == expanded_surface.stride &&
|
||||||
|
(std::max(expanded_surface.addr, addr) - std::min(expanded_surface.addr, addr)) %
|
||||||
|
BytesInPixels(stride * (is_tiled ? 8 : 1)) ==
|
||||||
|
0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SurfaceParams::CanTexCopy(const SurfaceParams& texcopy_params) const {
|
||||||
|
if (pixel_format == PixelFormat::Invalid || addr > texcopy_params.addr ||
|
||||||
|
end < texcopy_params.end) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (texcopy_params.width != texcopy_params.stride) {
|
||||||
|
const u32 tile_stride = BytesInPixels(stride * (is_tiled ? 8 : 1));
|
||||||
|
return (texcopy_params.addr - addr) % BytesInPixels(is_tiled ? 64 : 1) == 0 &&
|
||||||
|
texcopy_params.width % BytesInPixels(is_tiled ? 64 : 1) == 0 &&
|
||||||
|
(texcopy_params.height == 1 || texcopy_params.stride == tile_stride) &&
|
||||||
|
((texcopy_params.addr - addr) % tile_stride) + texcopy_params.width <= tile_stride;
|
||||||
|
}
|
||||||
|
return FromInterval(texcopy_params.GetInterval()).GetInterval() == texcopy_params.GetInterval();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
|
@ -0,0 +1,270 @@
|
||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <climits>
|
||||||
|
#include <boost/icl/interval.hpp>
|
||||||
|
#include "common/assert.h"
|
||||||
|
#include "common/math_util.h"
|
||||||
|
#include "core/hw/gpu.h"
|
||||||
|
#include "video_core/regs_framebuffer.h"
|
||||||
|
#include "video_core/regs_texturing.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
struct CachedSurface;
|
||||||
|
using Surface = std::shared_ptr<CachedSurface>;
|
||||||
|
|
||||||
|
using SurfaceInterval = boost::icl::right_open_interval<PAddr>;
|
||||||
|
|
||||||
|
struct SurfaceParams {
|
||||||
|
private:
|
||||||
|
static constexpr std::array<unsigned int, 18> BPP_TABLE = {
|
||||||
|
32, // RGBA8
|
||||||
|
24, // RGB8
|
||||||
|
16, // RGB5A1
|
||||||
|
16, // RGB565
|
||||||
|
16, // RGBA4
|
||||||
|
16, // IA8
|
||||||
|
16, // RG8
|
||||||
|
8, // I8
|
||||||
|
8, // A8
|
||||||
|
8, // IA4
|
||||||
|
4, // I4
|
||||||
|
4, // A4
|
||||||
|
4, // ETC1
|
||||||
|
8, // ETC1A4
|
||||||
|
16, // D16
|
||||||
|
0,
|
||||||
|
24, // D24
|
||||||
|
32, // D24S8
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum class PixelFormat {
|
||||||
|
// First 5 formats are shared between textures and color buffers
|
||||||
|
RGBA8 = 0,
|
||||||
|
RGB8 = 1,
|
||||||
|
RGB5A1 = 2,
|
||||||
|
RGB565 = 3,
|
||||||
|
RGBA4 = 4,
|
||||||
|
|
||||||
|
// Texture-only formats
|
||||||
|
IA8 = 5,
|
||||||
|
RG8 = 6,
|
||||||
|
I8 = 7,
|
||||||
|
A8 = 8,
|
||||||
|
IA4 = 9,
|
||||||
|
I4 = 10,
|
||||||
|
A4 = 11,
|
||||||
|
ETC1 = 12,
|
||||||
|
ETC1A4 = 13,
|
||||||
|
|
||||||
|
// Depth buffer-only formats
|
||||||
|
D16 = 14,
|
||||||
|
// gap
|
||||||
|
D24 = 16,
|
||||||
|
D24S8 = 17,
|
||||||
|
|
||||||
|
Invalid = 255,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class SurfaceType {
|
||||||
|
Color = 0,
|
||||||
|
Texture = 1,
|
||||||
|
Depth = 2,
|
||||||
|
DepthStencil = 3,
|
||||||
|
Fill = 4,
|
||||||
|
Invalid = 5
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr unsigned int GetFormatBpp(PixelFormat format) {
|
||||||
|
const auto format_idx = static_cast<std::size_t>(format);
|
||||||
|
DEBUG_ASSERT_MSG(format_idx < BPP_TABLE.size(), "Invalid pixel format {}", format_idx);
|
||||||
|
return BPP_TABLE[format_idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int GetFormatBpp() const {
|
||||||
|
return GetFormatBpp(pixel_format);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string_view PixelFormatAsString(PixelFormat format) {
|
||||||
|
switch (format) {
|
||||||
|
case PixelFormat::RGBA8:
|
||||||
|
return "RGBA8";
|
||||||
|
case PixelFormat::RGB8:
|
||||||
|
return "RGB8";
|
||||||
|
case PixelFormat::RGB5A1:
|
||||||
|
return "RGB5A1";
|
||||||
|
case PixelFormat::RGB565:
|
||||||
|
return "RGB565";
|
||||||
|
case PixelFormat::RGBA4:
|
||||||
|
return "RGBA4";
|
||||||
|
case PixelFormat::IA8:
|
||||||
|
return "IA8";
|
||||||
|
case PixelFormat::RG8:
|
||||||
|
return "RG8";
|
||||||
|
case PixelFormat::I8:
|
||||||
|
return "I8";
|
||||||
|
case PixelFormat::A8:
|
||||||
|
return "A8";
|
||||||
|
case PixelFormat::IA4:
|
||||||
|
return "IA4";
|
||||||
|
case PixelFormat::I4:
|
||||||
|
return "I4";
|
||||||
|
case PixelFormat::A4:
|
||||||
|
return "A4";
|
||||||
|
case PixelFormat::ETC1:
|
||||||
|
return "ETC1";
|
||||||
|
case PixelFormat::ETC1A4:
|
||||||
|
return "ETC1A4";
|
||||||
|
case PixelFormat::D16:
|
||||||
|
return "D16";
|
||||||
|
case PixelFormat::D24:
|
||||||
|
return "D24";
|
||||||
|
case PixelFormat::D24S8:
|
||||||
|
return "D24S8";
|
||||||
|
default:
|
||||||
|
return "Not a real pixel format";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static PixelFormat PixelFormatFromTextureFormat(Pica::TexturingRegs::TextureFormat format) {
|
||||||
|
return ((unsigned int)format < 14) ? (PixelFormat)format : PixelFormat::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PixelFormat PixelFormatFromColorFormat(Pica::FramebufferRegs::ColorFormat format) {
|
||||||
|
return ((unsigned int)format < 5) ? (PixelFormat)format : PixelFormat::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PixelFormat PixelFormatFromDepthFormat(Pica::FramebufferRegs::DepthFormat format) {
|
||||||
|
return ((unsigned int)format < 4) ? (PixelFormat)((unsigned int)format + 14)
|
||||||
|
: PixelFormat::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PixelFormat PixelFormatFromGPUPixelFormat(GPU::Regs::PixelFormat format) {
|
||||||
|
switch (format) {
|
||||||
|
// RGB565 and RGB5A1 are switched in PixelFormat compared to ColorFormat
|
||||||
|
case GPU::Regs::PixelFormat::RGB565:
|
||||||
|
return PixelFormat::RGB565;
|
||||||
|
case GPU::Regs::PixelFormat::RGB5A1:
|
||||||
|
return PixelFormat::RGB5A1;
|
||||||
|
default:
|
||||||
|
return ((unsigned int)format < 5) ? (PixelFormat)format : PixelFormat::Invalid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool CheckFormatsBlittable(PixelFormat pixel_format_a, PixelFormat pixel_format_b) {
|
||||||
|
SurfaceType a_type = GetFormatType(pixel_format_a);
|
||||||
|
SurfaceType b_type = GetFormatType(pixel_format_b);
|
||||||
|
|
||||||
|
if ((a_type == SurfaceType::Color || a_type == SurfaceType::Texture) &&
|
||||||
|
(b_type == SurfaceType::Color || b_type == SurfaceType::Texture)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a_type == SurfaceType::Depth && b_type == SurfaceType::Depth) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a_type == SurfaceType::DepthStencil && b_type == SurfaceType::DepthStencil) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr SurfaceType GetFormatType(PixelFormat pixel_format) {
|
||||||
|
if ((unsigned int)pixel_format < 5) {
|
||||||
|
return SurfaceType::Color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((unsigned int)pixel_format < 14) {
|
||||||
|
return SurfaceType::Texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pixel_format == PixelFormat::D16 || pixel_format == PixelFormat::D24) {
|
||||||
|
return SurfaceType::Depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pixel_format == PixelFormat::D24S8) {
|
||||||
|
return SurfaceType::DepthStencil;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SurfaceType::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the params "size", "end" and "type" from the already set "addr", "width", "height"
|
||||||
|
/// and "pixel_format"
|
||||||
|
void UpdateParams() {
|
||||||
|
if (stride == 0) {
|
||||||
|
stride = width;
|
||||||
|
}
|
||||||
|
type = GetFormatType(pixel_format);
|
||||||
|
size = !is_tiled ? BytesInPixels(stride * (height - 1) + width)
|
||||||
|
: BytesInPixels(stride * 8 * (height / 8 - 1) + width * 8);
|
||||||
|
end = addr + size;
|
||||||
|
}
|
||||||
|
|
||||||
|
SurfaceInterval GetInterval() const {
|
||||||
|
return SurfaceInterval(addr, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the outer rectangle containing "interval"
|
||||||
|
SurfaceParams FromInterval(SurfaceInterval interval) const;
|
||||||
|
|
||||||
|
SurfaceInterval GetSubRectInterval(Common::Rectangle<u32> unscaled_rect) const;
|
||||||
|
|
||||||
|
// Returns the region of the biggest valid rectange within interval
|
||||||
|
SurfaceInterval GetCopyableInterval(const Surface& src_surface) const;
|
||||||
|
|
||||||
|
u32 GetScaledWidth() const {
|
||||||
|
return width * res_scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 GetScaledHeight() const {
|
||||||
|
return height * res_scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
Common::Rectangle<u32> GetRect() const {
|
||||||
|
return {0, height, width, 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
Common::Rectangle<u32> GetScaledRect() const {
|
||||||
|
return {0, GetScaledHeight(), GetScaledWidth(), 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 PixelsInBytes(u32 size) const {
|
||||||
|
return size * CHAR_BIT / GetFormatBpp(pixel_format);
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 BytesInPixels(u32 pixels) const {
|
||||||
|
return pixels * GetFormatBpp(pixel_format) / CHAR_BIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ExactMatch(const SurfaceParams& other_surface) const;
|
||||||
|
bool CanSubRect(const SurfaceParams& sub_surface) const;
|
||||||
|
bool CanExpand(const SurfaceParams& expanded_surface) const;
|
||||||
|
bool CanTexCopy(const SurfaceParams& texcopy_params) const;
|
||||||
|
|
||||||
|
Common::Rectangle<u32> GetSubRect(const SurfaceParams& sub_surface) const;
|
||||||
|
Common::Rectangle<u32> GetScaledSubRect(const SurfaceParams& sub_surface) const;
|
||||||
|
|
||||||
|
PAddr addr = 0;
|
||||||
|
PAddr end = 0;
|
||||||
|
u32 size = 0;
|
||||||
|
|
||||||
|
u32 width = 0;
|
||||||
|
u32 height = 0;
|
||||||
|
u32 stride = 0;
|
||||||
|
u16 res_scale = 1;
|
||||||
|
|
||||||
|
bool is_tiled = false;
|
||||||
|
PixelFormat pixel_format = PixelFormat::Invalid;
|
||||||
|
SurfaceType type = SurfaceType::Invalid;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
|
@ -32,23 +32,8 @@
|
||||||
#include "video_core/renderer_opengl/gl_vars.h"
|
#include "video_core/renderer_opengl/gl_vars.h"
|
||||||
#include "video_core/renderer_opengl/post_processing_opengl.h"
|
#include "video_core/renderer_opengl/post_processing_opengl.h"
|
||||||
#include "video_core/renderer_opengl/renderer_opengl.h"
|
#include "video_core/renderer_opengl/renderer_opengl.h"
|
||||||
#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h"
|
|
||||||
#include "video_core/video_core.h"
|
#include "video_core/video_core.h"
|
||||||
|
|
||||||
namespace Frontend {
|
|
||||||
|
|
||||||
struct Frame {
|
|
||||||
u32 width{}; /// Width of the frame (to detect resize)
|
|
||||||
u32 height{}; /// Height of the frame
|
|
||||||
bool color_reloaded = false; /// Texture attachment was recreated (ie: resized)
|
|
||||||
OpenGL::OGLRenderbuffer color{}; /// Buffer shared between the render/present FBO
|
|
||||||
OpenGL::OGLFramebuffer render{}; /// FBO created on the render thread
|
|
||||||
OpenGL::OGLFramebuffer present{}; /// FBO created on the present thread
|
|
||||||
GLsync render_fence{}; /// Fence created on the render thread
|
|
||||||
GLsync present_fence{}; /// Fence created on the presentation thread
|
|
||||||
};
|
|
||||||
} // namespace Frontend
|
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
// If the size of this is too small, it ends up creating a soft cap on FPS as the renderer will have
|
// If the size of this is too small, it ends up creating a soft cap on FPS as the renderer will have
|
||||||
|
@ -79,6 +64,7 @@ public:
|
||||||
std::queue<Frontend::Frame*>().swap(free_queue);
|
std::queue<Frontend::Frame*>().swap(free_queue);
|
||||||
present_queue.clear();
|
present_queue.clear();
|
||||||
present_cv.notify_all();
|
present_cv.notify_all();
|
||||||
|
free_cv.notify_all();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReloadPresentFrame(Frontend::Frame* frame, u32 height, u32 width) override {
|
void ReloadPresentFrame(Frontend::Frame* frame, u32 height, u32 width) override {
|
||||||
|
@ -89,7 +75,7 @@ public:
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, frame->present.handle);
|
glBindFramebuffer(GL_FRAMEBUFFER, frame->present.handle);
|
||||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
|
||||||
frame->color.handle);
|
frame->color.handle);
|
||||||
if (!glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||||
LOG_CRITICAL(Render_OpenGL, "Failed to recreate present FBO!");
|
LOG_CRITICAL(Render_OpenGL, "Failed to recreate present FBO!");
|
||||||
}
|
}
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, previous_draw_fbo);
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, previous_draw_fbo);
|
||||||
|
@ -115,7 +101,7 @@ public:
|
||||||
state.Apply();
|
state.Apply();
|
||||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
|
||||||
frame->color.handle);
|
frame->color.handle);
|
||||||
if (!glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||||
LOG_CRITICAL(Render_OpenGL, "Failed to recreate render FBO!");
|
LOG_CRITICAL(Render_OpenGL, "Failed to recreate render FBO!");
|
||||||
}
|
}
|
||||||
prev_state.Apply();
|
prev_state.Apply();
|
||||||
|
@ -145,19 +131,12 @@ public:
|
||||||
present_cv.notify_one();
|
present_cv.notify_one();
|
||||||
}
|
}
|
||||||
|
|
||||||
Frontend::Frame* TryGetPresentFrame(int timeout_ms) override {
|
// This is virtual as it is to be overriden in OGLVideoDumpingMailbox below.
|
||||||
std::unique_lock<std::mutex> lock(swap_chain_lock);
|
virtual void LoadPresentFrame() {
|
||||||
// wait for new entries in the present_queue
|
|
||||||
present_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms),
|
|
||||||
[&] { return !present_queue.empty(); });
|
|
||||||
if (present_queue.empty()) {
|
|
||||||
// timed out waiting for a frame to draw so return the previous frame
|
|
||||||
return previous_frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
// free the previous frame and add it back to the free queue
|
// free the previous frame and add it back to the free queue
|
||||||
if (previous_frame) {
|
if (previous_frame) {
|
||||||
free_queue.push(previous_frame);
|
free_queue.push(previous_frame);
|
||||||
|
free_cv.notify_one();
|
||||||
}
|
}
|
||||||
|
|
||||||
// the newest entries are pushed to the front of the queue
|
// the newest entries are pushed to the front of the queue
|
||||||
|
@ -169,8 +148,72 @@ public:
|
||||||
}
|
}
|
||||||
present_queue.clear();
|
present_queue.clear();
|
||||||
previous_frame = frame;
|
previous_frame = frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
Frontend::Frame* TryGetPresentFrame(int timeout_ms) override {
|
||||||
|
std::unique_lock<std::mutex> lock(swap_chain_lock);
|
||||||
|
// wait for new entries in the present_queue
|
||||||
|
present_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms),
|
||||||
|
[&] { return !present_queue.empty(); });
|
||||||
|
if (present_queue.empty()) {
|
||||||
|
// timed out waiting for a frame to draw so return the previous frame
|
||||||
|
return previous_frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadPresentFrame();
|
||||||
|
return previous_frame;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// This mailbox is different in that it will never discard rendered frames
|
||||||
|
class OGLVideoDumpingMailbox : public OGLTextureMailbox {
|
||||||
|
public:
|
||||||
|
Frontend::Frame* GetRenderFrame() override {
|
||||||
|
std::unique_lock<std::mutex> lock(swap_chain_lock);
|
||||||
|
|
||||||
|
// If theres no free frames, we will wait until one shows up
|
||||||
|
if (free_queue.empty()) {
|
||||||
|
free_cv.wait(lock, [&] { return !free_queue.empty(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (free_queue.empty()) {
|
||||||
|
LOG_CRITICAL(Render_OpenGL, "Could not get free frame");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Frontend::Frame* frame = free_queue.front();
|
||||||
|
free_queue.pop();
|
||||||
return frame;
|
return frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LoadPresentFrame() override {
|
||||||
|
// free the previous frame and add it back to the free queue
|
||||||
|
if (previous_frame) {
|
||||||
|
free_queue.push(previous_frame);
|
||||||
|
free_cv.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
Frontend::Frame* frame = present_queue.back();
|
||||||
|
present_queue.pop_back();
|
||||||
|
previous_frame = frame;
|
||||||
|
|
||||||
|
// Do not remove entries from the present_queue, as video dumping would require
|
||||||
|
// that we preserve all frames
|
||||||
|
}
|
||||||
|
|
||||||
|
Frontend::Frame* TryGetPresentFrame(int timeout_ms) override {
|
||||||
|
std::unique_lock<std::mutex> lock(swap_chain_lock);
|
||||||
|
// wait for new entries in the present_queue
|
||||||
|
present_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms),
|
||||||
|
[&] { return !present_queue.empty(); });
|
||||||
|
if (present_queue.empty()) {
|
||||||
|
// timed out waiting for a frame
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadPresentFrame();
|
||||||
|
return previous_frame;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static const char vertex_shader[] = R"(
|
static const char vertex_shader[] = R"(
|
||||||
|
@ -279,21 +322,35 @@ struct ScreenRectVertex {
|
||||||
*
|
*
|
||||||
* The projection part of the matrix is trivial, hence these operations are represented
|
* The projection part of the matrix is trivial, hence these operations are represented
|
||||||
* by a 3x2 matrix.
|
* by a 3x2 matrix.
|
||||||
|
*
|
||||||
|
* @param flipped Whether the frame should be flipped upside down.
|
||||||
*/
|
*/
|
||||||
static std::array<GLfloat, 3 * 2> MakeOrthographicMatrix(const float width, const float height) {
|
static std::array<GLfloat, 3 * 2> MakeOrthographicMatrix(const float width, const float height,
|
||||||
|
bool flipped) {
|
||||||
|
|
||||||
std::array<GLfloat, 3 * 2> matrix; // Laid out in column-major order
|
std::array<GLfloat, 3 * 2> matrix; // Laid out in column-major order
|
||||||
|
|
||||||
|
// Last matrix row is implicitly assumed to be [0, 0, 1].
|
||||||
|
if (flipped) {
|
||||||
|
// clang-format off
|
||||||
|
matrix[0] = 2.f / width; matrix[2] = 0.f; matrix[4] = -1.f;
|
||||||
|
matrix[1] = 0.f; matrix[3] = 2.f / height; matrix[5] = -1.f;
|
||||||
|
// clang-format on
|
||||||
|
} else {
|
||||||
// clang-format off
|
// clang-format off
|
||||||
matrix[0] = 2.f / width; matrix[2] = 0.f; matrix[4] = -1.f;
|
matrix[0] = 2.f / width; matrix[2] = 0.f; matrix[4] = -1.f;
|
||||||
matrix[1] = 0.f; matrix[3] = -2.f / height; matrix[5] = 1.f;
|
matrix[1] = 0.f; matrix[3] = -2.f / height; matrix[5] = 1.f;
|
||||||
// Last matrix row is implicitly assumed to be [0, 0, 1].
|
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
}
|
||||||
|
|
||||||
return matrix;
|
return matrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
RendererOpenGL::RendererOpenGL(Frontend::EmuWindow& window) : RendererBase{window} {
|
RendererOpenGL::RendererOpenGL(Frontend::EmuWindow& window)
|
||||||
|
: RendererBase{window}, frame_dumper(Core::System::GetInstance().VideoDumper(), window) {
|
||||||
|
|
||||||
window.mailbox = std::make_unique<OGLTextureMailbox>();
|
window.mailbox = std::make_unique<OGLTextureMailbox>();
|
||||||
|
frame_dumper.mailbox = std::make_unique<OGLVideoDumpingMailbox>();
|
||||||
}
|
}
|
||||||
|
|
||||||
RendererOpenGL::~RendererOpenGL() = default;
|
RendererOpenGL::~RendererOpenGL() = default;
|
||||||
|
@ -311,56 +368,14 @@ void RendererOpenGL::SwapBuffers() {
|
||||||
|
|
||||||
RenderScreenshot();
|
RenderScreenshot();
|
||||||
|
|
||||||
RenderVideoDumping();
|
|
||||||
|
|
||||||
const auto& layout = render_window.GetFramebufferLayout();
|
const auto& layout = render_window.GetFramebufferLayout();
|
||||||
|
RenderToMailbox(layout, render_window.mailbox, false);
|
||||||
|
|
||||||
Frontend::Frame* frame;
|
if (frame_dumper.IsDumping()) {
|
||||||
{
|
RenderToMailbox(frame_dumper.GetLayout(), frame_dumper.mailbox, true);
|
||||||
MICROPROFILE_SCOPE(OpenGL_WaitPresent);
|
|
||||||
|
|
||||||
frame = render_window.mailbox->GetRenderFrame();
|
|
||||||
|
|
||||||
// Clean up sync objects before drawing
|
|
||||||
|
|
||||||
// INTEL driver workaround. We can't delete the previous render sync object until we are
|
|
||||||
// sure that the presentation is done
|
|
||||||
if (frame->present_fence) {
|
|
||||||
glClientWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete the draw fence if the frame wasn't presented
|
|
||||||
if (frame->render_fence) {
|
|
||||||
glDeleteSync(frame->render_fence);
|
|
||||||
frame->render_fence = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for the presentation to be done
|
|
||||||
if (frame->present_fence) {
|
|
||||||
glWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED);
|
|
||||||
glDeleteSync(frame->present_fence);
|
|
||||||
frame->present_fence = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
MICROPROFILE_SCOPE(OpenGL_RenderFrame);
|
|
||||||
// Recreate the frame if the size of the window has changed
|
|
||||||
if (layout.width != frame->width || layout.height != frame->height) {
|
|
||||||
LOG_DEBUG(Render_OpenGL, "Reloading render frame");
|
|
||||||
render_window.mailbox->ReloadRenderFrame(frame, layout.width, layout.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint render_texture = frame->color.handle;
|
|
||||||
state.draw.draw_framebuffer = frame->render.handle;
|
|
||||||
state.Apply();
|
|
||||||
DrawScreens(layout);
|
|
||||||
// Create a fence for the frontend to wait on and swap this frame to OffTex
|
|
||||||
frame->render_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
|
||||||
glFlush();
|
|
||||||
render_window.mailbox->ReleaseRenderFrame(frame);
|
|
||||||
m_current_frame++;
|
m_current_frame++;
|
||||||
}
|
|
||||||
|
|
||||||
Core::System::GetInstance().perf_stats->EndSystemFrame();
|
Core::System::GetInstance().perf_stats->EndSystemFrame();
|
||||||
|
|
||||||
|
@ -396,7 +411,7 @@ void RendererOpenGL::RenderScreenshot() {
|
||||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
|
||||||
renderbuffer);
|
renderbuffer);
|
||||||
|
|
||||||
DrawScreens(layout);
|
DrawScreens(layout, false);
|
||||||
|
|
||||||
glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV,
|
glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV,
|
||||||
VideoCore::g_screenshot_bits);
|
VideoCore::g_screenshot_bits);
|
||||||
|
@ -449,33 +464,54 @@ void RendererOpenGL::PrepareRendertarget() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void RendererOpenGL::RenderVideoDumping() {
|
void RendererOpenGL::RenderToMailbox(const Layout::FramebufferLayout& layout,
|
||||||
if (cleanup_video_dumping.exchange(false)) {
|
std::unique_ptr<Frontend::TextureMailbox>& mailbox,
|
||||||
ReleaseVideoDumpingGLObjects();
|
bool flipped) {
|
||||||
|
|
||||||
|
Frontend::Frame* frame;
|
||||||
|
{
|
||||||
|
MICROPROFILE_SCOPE(OpenGL_WaitPresent);
|
||||||
|
|
||||||
|
frame = mailbox->GetRenderFrame();
|
||||||
|
|
||||||
|
// Clean up sync objects before drawing
|
||||||
|
|
||||||
|
// INTEL driver workaround. We can't delete the previous render sync object until we are
|
||||||
|
// sure that the presentation is done
|
||||||
|
if (frame->present_fence) {
|
||||||
|
glClientWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Core::System::GetInstance().VideoDumper().IsDumping()) {
|
// delete the draw fence if the frame wasn't presented
|
||||||
if (prepare_video_dumping.exchange(false)) {
|
if (frame->render_fence) {
|
||||||
InitVideoDumpingGLObjects();
|
glDeleteSync(frame->render_fence);
|
||||||
|
frame->render_fence = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& layout = Core::System::GetInstance().VideoDumper().GetLayout();
|
// wait for the presentation to be done
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, frame_dumping_framebuffer.handle);
|
if (frame->present_fence) {
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frame_dumping_framebuffer.handle);
|
glWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED);
|
||||||
DrawScreens(layout);
|
glDeleteSync(frame->present_fence);
|
||||||
|
frame->present_fence = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, frame_dumping_pbos[current_pbo].handle);
|
{
|
||||||
glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, 0);
|
MICROPROFILE_SCOPE(OpenGL_RenderFrame);
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, frame_dumping_pbos[next_pbo].handle);
|
// Recreate the frame if the size of the window has changed
|
||||||
|
if (layout.width != frame->width || layout.height != frame->height) {
|
||||||
|
LOG_DEBUG(Render_OpenGL, "Reloading render frame");
|
||||||
|
mailbox->ReloadRenderFrame(frame, layout.width, layout.height);
|
||||||
|
}
|
||||||
|
|
||||||
GLubyte* pixels = static_cast<GLubyte*>(glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY));
|
GLuint render_texture = frame->color.handle;
|
||||||
VideoDumper::VideoFrame frame_data{layout.width, layout.height, pixels};
|
state.draw.draw_framebuffer = frame->render.handle;
|
||||||
Core::System::GetInstance().VideoDumper().AddVideoFrame(frame_data);
|
state.Apply();
|
||||||
|
DrawScreens(layout, flipped);
|
||||||
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
// Create a fence for the frontend to wait on and swap this frame to OffTex
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
frame->render_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||||
current_pbo = (current_pbo + 1) % 2;
|
glFlush();
|
||||||
next_pbo = (current_pbo + 1) % 2;
|
mailbox->ReleaseRenderFrame(frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -886,7 +922,7 @@ void RendererOpenGL::DrawSingleScreenStereo(const ScreenInfo& screen_info_l,
|
||||||
/**
|
/**
|
||||||
* Draws the emulated screens to the emulator window.
|
* Draws the emulated screens to the emulator window.
|
||||||
*/
|
*/
|
||||||
void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout) {
|
void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout, bool flipped) {
|
||||||
if (VideoCore::g_renderer_bg_color_update_requested.exchange(false)) {
|
if (VideoCore::g_renderer_bg_color_update_requested.exchange(false)) {
|
||||||
// Update background color before drawing
|
// Update background color before drawing
|
||||||
glClearColor(Settings::values.bg_red, Settings::values.bg_green, Settings::values.bg_blue,
|
glClearColor(Settings::values.bg_red, Settings::values.bg_green, Settings::values.bg_blue,
|
||||||
|
@ -913,7 +949,7 @@ void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout) {
|
||||||
|
|
||||||
// Set projection matrix
|
// Set projection matrix
|
||||||
std::array<GLfloat, 3 * 2> ortho_matrix =
|
std::array<GLfloat, 3 * 2> ortho_matrix =
|
||||||
MakeOrthographicMatrix((float)layout.width, (float)layout.height);
|
MakeOrthographicMatrix((float)layout.width, (float)layout.height, flipped);
|
||||||
glUniformMatrix3x2fv(uniform_modelview_matrix, 1, GL_FALSE, ortho_matrix.data());
|
glUniformMatrix3x2fv(uniform_modelview_matrix, 1, GL_FALSE, ortho_matrix.data());
|
||||||
|
|
||||||
// Bind texture in Texture Unit 0
|
// Bind texture in Texture Unit 0
|
||||||
|
@ -1052,41 +1088,11 @@ void RendererOpenGL::TryPresent(int timeout_ms) {
|
||||||
void RendererOpenGL::UpdateFramerate() {}
|
void RendererOpenGL::UpdateFramerate() {}
|
||||||
|
|
||||||
void RendererOpenGL::PrepareVideoDumping() {
|
void RendererOpenGL::PrepareVideoDumping() {
|
||||||
prepare_video_dumping = true;
|
frame_dumper.StartDumping();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RendererOpenGL::CleanupVideoDumping() {
|
void RendererOpenGL::CleanupVideoDumping() {
|
||||||
cleanup_video_dumping = true;
|
frame_dumper.StopDumping();
|
||||||
}
|
|
||||||
|
|
||||||
void RendererOpenGL::InitVideoDumpingGLObjects() {
|
|
||||||
const auto& layout = Core::System::GetInstance().VideoDumper().GetLayout();
|
|
||||||
|
|
||||||
frame_dumping_framebuffer.Create();
|
|
||||||
glGenRenderbuffers(1, &frame_dumping_renderbuffer);
|
|
||||||
glBindRenderbuffer(GL_RENDERBUFFER, frame_dumping_renderbuffer);
|
|
||||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB8, layout.width, layout.height);
|
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frame_dumping_framebuffer.handle);
|
|
||||||
glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
|
|
||||||
frame_dumping_renderbuffer);
|
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
|
||||||
|
|
||||||
for (auto& buffer : frame_dumping_pbos) {
|
|
||||||
buffer.Create();
|
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, buffer.handle);
|
|
||||||
glBufferData(GL_PIXEL_PACK_BUFFER, layout.width * layout.height * 4, nullptr,
|
|
||||||
GL_STREAM_READ);
|
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void RendererOpenGL::ReleaseVideoDumpingGLObjects() {
|
|
||||||
frame_dumping_framebuffer.Release();
|
|
||||||
glDeleteRenderbuffers(1, &frame_dumping_renderbuffer);
|
|
||||||
|
|
||||||
for (auto& buffer : frame_dumping_pbos) {
|
|
||||||
buffer.Release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char* GetSource(GLenum source) {
|
static const char* GetSource(GLenum source) {
|
||||||
|
@ -1179,14 +1185,10 @@ VideoCore::ResultStatus RendererOpenGL::Init() {
|
||||||
|
|
||||||
RefreshRasterizerSetting();
|
RefreshRasterizerSetting();
|
||||||
|
|
||||||
TextureFilterManager::GetInstance().Reset();
|
|
||||||
|
|
||||||
return VideoCore::ResultStatus::Success;
|
return VideoCore::ResultStatus::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shutdown the renderer
|
/// Shutdown the renderer
|
||||||
void RendererOpenGL::ShutDown() {
|
void RendererOpenGL::ShutDown() {}
|
||||||
TextureFilterManager::GetInstance().Destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace OpenGL
|
} // namespace OpenGL
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#include "common/math_util.h"
|
#include "common/math_util.h"
|
||||||
#include "core/hw/gpu.h"
|
#include "core/hw/gpu.h"
|
||||||
#include "video_core/renderer_base.h"
|
#include "video_core/renderer_base.h"
|
||||||
|
#include "video_core/renderer_opengl/frame_dumper_opengl.h"
|
||||||
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
||||||
#include "video_core/renderer_opengl/gl_state.h"
|
#include "video_core/renderer_opengl/gl_state.h"
|
||||||
|
|
||||||
|
@ -17,6 +18,20 @@ namespace Layout {
|
||||||
struct FramebufferLayout;
|
struct FramebufferLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace Frontend {
|
||||||
|
|
||||||
|
struct Frame {
|
||||||
|
u32 width{}; /// Width of the frame (to detect resize)
|
||||||
|
u32 height{}; /// Height of the frame
|
||||||
|
bool color_reloaded = false; /// Texture attachment was recreated (ie: resized)
|
||||||
|
OpenGL::OGLRenderbuffer color{}; /// Buffer shared between the render/present FBO
|
||||||
|
OpenGL::OGLFramebuffer render{}; /// FBO created on the render thread
|
||||||
|
OpenGL::OGLFramebuffer present{}; /// FBO created on the present thread
|
||||||
|
GLsync render_fence{}; /// Fence created on the render thread
|
||||||
|
GLsync present_fence{}; /// Fence created on the presentation thread
|
||||||
|
};
|
||||||
|
} // namespace Frontend
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
/// Structure used for storing information about the textures for each 3DS screen
|
/// Structure used for storing information about the textures for each 3DS screen
|
||||||
|
@ -72,10 +87,11 @@ private:
|
||||||
void ReloadShader();
|
void ReloadShader();
|
||||||
void PrepareRendertarget();
|
void PrepareRendertarget();
|
||||||
void RenderScreenshot();
|
void RenderScreenshot();
|
||||||
void RenderVideoDumping();
|
void RenderToMailbox(const Layout::FramebufferLayout& layout,
|
||||||
|
std::unique_ptr<Frontend::TextureMailbox>& mailbox, bool flipped);
|
||||||
void ConfigureFramebufferTexture(TextureInfo& texture,
|
void ConfigureFramebufferTexture(TextureInfo& texture,
|
||||||
const GPU::Regs::FramebufferConfig& framebuffer);
|
const GPU::Regs::FramebufferConfig& framebuffer);
|
||||||
void DrawScreens(const Layout::FramebufferLayout& layout);
|
void DrawScreens(const Layout::FramebufferLayout& layout, bool flipped);
|
||||||
void DrawSingleScreenRotated(const ScreenInfo& screen_info, float x, float y, float w, float h);
|
void DrawSingleScreenRotated(const ScreenInfo& screen_info, float x, float y, float w, float h);
|
||||||
void DrawSingleScreen(const ScreenInfo& screen_info, float x, float y, float w, float h);
|
void DrawSingleScreen(const ScreenInfo& screen_info, float x, float y, float w, float h);
|
||||||
void DrawSingleScreenStereoRotated(const ScreenInfo& screen_info_l,
|
void DrawSingleScreenStereoRotated(const ScreenInfo& screen_info_l,
|
||||||
|
@ -91,9 +107,6 @@ private:
|
||||||
// Fills active OpenGL texture with the given RGB color.
|
// Fills active OpenGL texture with the given RGB color.
|
||||||
void LoadColorToActiveGLTexture(u8 color_r, u8 color_g, u8 color_b, const TextureInfo& texture);
|
void LoadColorToActiveGLTexture(u8 color_r, u8 color_g, u8 color_b, const TextureInfo& texture);
|
||||||
|
|
||||||
void InitVideoDumpingGLObjects();
|
|
||||||
void ReleaseVideoDumpingGLObjects();
|
|
||||||
|
|
||||||
OpenGLState state;
|
OpenGLState state;
|
||||||
|
|
||||||
// OpenGL object IDs
|
// OpenGL object IDs
|
||||||
|
@ -120,19 +133,7 @@ private:
|
||||||
GLuint attrib_position;
|
GLuint attrib_position;
|
||||||
GLuint attrib_tex_coord;
|
GLuint attrib_tex_coord;
|
||||||
|
|
||||||
// Frame dumping
|
FrameDumperOpenGL frame_dumper;
|
||||||
OGLFramebuffer frame_dumping_framebuffer;
|
|
||||||
GLuint frame_dumping_renderbuffer;
|
|
||||||
|
|
||||||
// Whether prepare/cleanup video dumping has been requested.
|
|
||||||
// They will be executed on next frame.
|
|
||||||
std::atomic_bool prepare_video_dumping = false;
|
|
||||||
std::atomic_bool cleanup_video_dumping = false;
|
|
||||||
|
|
||||||
// PBOs used to dump frames faster
|
|
||||||
std::array<OGLBuffer, 2> frame_dumping_pbos;
|
|
||||||
GLuint current_pbo = 1;
|
|
||||||
GLuint next_pbo = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace OpenGL
|
} // namespace OpenGL
|
||||||
|
|
|
@ -42,18 +42,17 @@
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
Anime4kUltrafast::Anime4kUltrafast(u16 scale_factor) : TextureFilterInterface(scale_factor) {
|
Anime4kUltrafast::Anime4kUltrafast(u16 scale_factor) : TextureFilterBase(scale_factor) {
|
||||||
const OpenGLState cur_state = OpenGLState::GetCurState();
|
const OpenGLState cur_state = OpenGLState::GetCurState();
|
||||||
const auto setup_temp_tex = [this, scale_factor](TempTex& texture, GLint internal_format,
|
const auto setup_temp_tex = [this](TempTex& texture, GLint internal_format, GLint format) {
|
||||||
GLint format) {
|
|
||||||
texture.fbo.Create();
|
texture.fbo.Create();
|
||||||
texture.tex.Create();
|
texture.tex.Create();
|
||||||
state.draw.draw_framebuffer = texture.fbo.handle;
|
state.draw.draw_framebuffer = texture.fbo.handle;
|
||||||
state.Apply();
|
state.Apply();
|
||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
glBindTexture(GL_TEXTURE_RECTANGLE, texture.tex.handle);
|
glBindTexture(GL_TEXTURE_RECTANGLE, texture.tex.handle);
|
||||||
glTexImage2D(GL_TEXTURE_RECTANGLE, 0, internal_format, 1024 * scale_factor,
|
glTexImage2D(GL_TEXTURE_RECTANGLE, 0, internal_format, 1024 * internal_scale_factor,
|
||||||
1024 * scale_factor, 0, format, GL_HALF_FLOAT, nullptr);
|
1024 * internal_scale_factor, 0, format, GL_HALF_FLOAT, nullptr);
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_RECTANGLE,
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_RECTANGLE,
|
||||||
texture.tex.handle, 0);
|
texture.tex.handle, 0);
|
||||||
};
|
};
|
||||||
|
@ -61,7 +60,6 @@ Anime4kUltrafast::Anime4kUltrafast(u16 scale_factor) : TextureFilterInterface(sc
|
||||||
setup_temp_tex(XY, GL_RG16F, GL_RG);
|
setup_temp_tex(XY, GL_RG16F, GL_RG);
|
||||||
|
|
||||||
vao.Create();
|
vao.Create();
|
||||||
out_fbo.Create();
|
|
||||||
|
|
||||||
for (std::size_t idx = 0; idx < samplers.size(); ++idx) {
|
for (std::size_t idx = 0; idx < samplers.size(); ++idx) {
|
||||||
samplers[idx].Create();
|
samplers[idx].Create();
|
||||||
|
@ -86,30 +84,26 @@ Anime4kUltrafast::Anime4kUltrafast(u16 scale_factor) : TextureFilterInterface(sc
|
||||||
state.draw.shader_program = refine_program.handle;
|
state.draw.shader_program = refine_program.handle;
|
||||||
state.Apply();
|
state.Apply();
|
||||||
glUniform1i(glGetUniformLocation(refine_program.handle, "LUMAD"), 1);
|
glUniform1i(glGetUniformLocation(refine_program.handle, "LUMAD"), 1);
|
||||||
|
glUniform1f(glGetUniformLocation(refine_program.handle, "final_scale"),
|
||||||
|
static_cast<GLfloat>(internal_scale_factor) / scale_factor);
|
||||||
|
|
||||||
cur_state.Apply();
|
cur_state.Apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Anime4kUltrafast::scale(CachedSurface& surface, const Common::Rectangle<u32>& rect,
|
void Anime4kUltrafast::Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect,
|
||||||
std::size_t buffer_offset) {
|
GLuint dst_tex, const Common::Rectangle<u32>& dst_rect,
|
||||||
|
GLuint read_fb_handle, GLuint draw_fb_handle) {
|
||||||
const OpenGLState cur_state = OpenGLState::GetCurState();
|
const OpenGLState cur_state = OpenGLState::GetCurState();
|
||||||
|
|
||||||
OGLTexture src_tex;
|
state.viewport = {static_cast<GLint>(src_rect.left * internal_scale_factor),
|
||||||
src_tex.Create();
|
static_cast<GLint>(src_rect.bottom * internal_scale_factor),
|
||||||
|
static_cast<GLsizei>(src_rect.GetWidth() * internal_scale_factor),
|
||||||
state.viewport = RectToViewport(rect);
|
static_cast<GLsizei>(src_rect.GetHeight() * internal_scale_factor)};
|
||||||
|
state.texture_units[0].texture_2d = src_tex;
|
||||||
state.texture_units[0].texture_2d = src_tex.handle;
|
|
||||||
state.draw.draw_framebuffer = XY.fbo.handle;
|
state.draw.draw_framebuffer = XY.fbo.handle;
|
||||||
state.draw.shader_program = gradient_x_program.handle;
|
state.draw.shader_program = gradient_x_program.handle;
|
||||||
state.Apply();
|
state.Apply();
|
||||||
|
|
||||||
const FormatTuple tuple = GetFormatTuple(surface.pixel_format);
|
|
||||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast<GLint>(surface.stride));
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, rect.GetWidth(), rect.GetHeight(), 0,
|
|
||||||
tuple.format, tuple.type, &surface.gl_buffer[buffer_offset]);
|
|
||||||
|
|
||||||
glActiveTexture(GL_TEXTURE1);
|
glActiveTexture(GL_TEXTURE1);
|
||||||
glBindTexture(GL_TEXTURE_RECTANGLE, LUMAD.tex.handle);
|
glBindTexture(GL_TEXTURE_RECTANGLE, LUMAD.tex.handle);
|
||||||
glActiveTexture(GL_TEXTURE2);
|
glActiveTexture(GL_TEXTURE2);
|
||||||
|
@ -124,14 +118,17 @@ void Anime4kUltrafast::scale(CachedSurface& surface, const Common::Rectangle<u32
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
// refine pass
|
// refine pass
|
||||||
state.draw.draw_framebuffer = out_fbo.handle;
|
state.viewport = {static_cast<GLint>(dst_rect.left), static_cast<GLint>(dst_rect.bottom),
|
||||||
|
static_cast<GLsizei>(dst_rect.GetWidth()),
|
||||||
|
static_cast<GLsizei>(dst_rect.GetHeight())};
|
||||||
|
state.draw.draw_framebuffer = draw_fb_handle;
|
||||||
state.draw.shader_program = refine_program.handle;
|
state.draw.shader_program = refine_program.handle;
|
||||||
state.Apply();
|
state.Apply();
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
|
||||||
cur_state.texture_units[0].texture_2d, 0);
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, 0);
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
|
||||||
cur_state.Apply();
|
cur_state.Apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,29 +6,25 @@
|
||||||
|
|
||||||
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
||||||
#include "video_core/renderer_opengl/gl_state.h"
|
#include "video_core/renderer_opengl/gl_state.h"
|
||||||
#include "video_core/renderer_opengl/texture_filters/texture_filter_interface.h"
|
#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h"
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
class Anime4kUltrafast : public TextureFilterInterface {
|
class Anime4kUltrafast : public TextureFilterBase {
|
||||||
public:
|
public:
|
||||||
static TextureFilterInfo GetInfo() {
|
static constexpr std::string_view NAME = "Anime4K Ultrafast";
|
||||||
TextureFilterInfo info;
|
|
||||||
info.name = "Anime4K Ultrafast";
|
|
||||||
info.clamp_scale = {2, 2};
|
|
||||||
info.constructor = std::make_unique<Anime4kUltrafast, u16>;
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
Anime4kUltrafast(u16 scale_factor);
|
explicit Anime4kUltrafast(u16 scale_factor);
|
||||||
void scale(CachedSurface& surface, const Common::Rectangle<u32>& rect,
|
void Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
||||||
std::size_t buffer_offset) override;
|
const Common::Rectangle<u32>& dst_rect, GLuint read_fb_handle,
|
||||||
|
GLuint draw_fb_handle) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
static constexpr u8 internal_scale_factor = 2;
|
||||||
|
|
||||||
OpenGLState state{};
|
OpenGLState state{};
|
||||||
|
|
||||||
OGLVertexArray vao;
|
OGLVertexArray vao;
|
||||||
OGLFramebuffer out_fbo;
|
|
||||||
|
|
||||||
struct TempTex {
|
struct TempTex {
|
||||||
OGLTexture tex;
|
OGLTexture tex;
|
||||||
|
|
|
@ -8,6 +8,8 @@ uniform sampler2D HOOKED;
|
||||||
uniform sampler2DRect LUMAD;
|
uniform sampler2DRect LUMAD;
|
||||||
uniform sampler2DRect LUMAG;
|
uniform sampler2DRect LUMAG;
|
||||||
|
|
||||||
|
uniform float final_scale;
|
||||||
|
|
||||||
const float LINE_DETECT_THRESHOLD = 0.4;
|
const float LINE_DETECT_THRESHOLD = 0.4;
|
||||||
const float STRENGTH = 0.6;
|
const float STRENGTH = 0.6;
|
||||||
|
|
||||||
|
@ -24,7 +26,7 @@ vec4 getAverage(vec4 cc, vec4 a, vec4 b, vec4 c) {
|
||||||
|
|
||||||
#define GetRGBAL(offset) \
|
#define GetRGBAL(offset) \
|
||||||
RGBAL(textureOffset(HOOKED, tex_coord, offset), \
|
RGBAL(textureOffset(HOOKED, tex_coord, offset), \
|
||||||
texture(LUMAD, clamp(gl_FragCoord.xy + offset, vec2(0.0), input_max)).x)
|
texture(LUMAD, clamp((gl_FragCoord.xy + offset) * final_scale, vec2(0.0), input_max)).x)
|
||||||
|
|
||||||
float min3v(float a, float b, float c) {
|
float min3v(float a, float b, float c) {
|
||||||
return min(min(a, b), c);
|
return min(min(a, b), c);
|
||||||
|
|
|
@ -10,45 +10,36 @@
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
Bicubic::Bicubic(u16 scale_factor) : TextureFilterInterface(scale_factor) {
|
Bicubic::Bicubic(u16 scale_factor) : TextureFilterBase(scale_factor) {
|
||||||
program.Create(tex_coord_vert.data(), bicubic_frag.data());
|
program.Create(tex_coord_vert.data(), bicubic_frag.data());
|
||||||
vao.Create();
|
vao.Create();
|
||||||
draw_fbo.Create();
|
|
||||||
src_sampler.Create();
|
src_sampler.Create();
|
||||||
|
|
||||||
state.draw.shader_program = program.handle;
|
state.draw.shader_program = program.handle;
|
||||||
state.draw.vertex_array = vao.handle;
|
state.draw.vertex_array = vao.handle;
|
||||||
state.draw.shader_program = program.handle;
|
state.draw.shader_program = program.handle;
|
||||||
state.draw.draw_framebuffer = draw_fbo.handle;
|
|
||||||
state.texture_units[0].sampler = src_sampler.handle;
|
state.texture_units[0].sampler = src_sampler.handle;
|
||||||
|
|
||||||
glSamplerParameteri(src_sampler.handle, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
glSamplerParameteri(src_sampler.handle, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
glSamplerParameteri(src_sampler.handle, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
glSamplerParameteri(src_sampler.handle, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
}
|
} // namespace OpenGL
|
||||||
|
|
||||||
void Bicubic::scale(CachedSurface& surface, const Common::Rectangle<u32>& rect,
|
void Bicubic::Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
||||||
std::size_t buffer_offset) {
|
const Common::Rectangle<u32>& dst_rect, GLuint read_fb_handle,
|
||||||
|
GLuint draw_fb_handle) {
|
||||||
const OpenGLState cur_state = OpenGLState::GetCurState();
|
const OpenGLState cur_state = OpenGLState::GetCurState();
|
||||||
|
state.texture_units[0].texture_2d = src_tex;
|
||||||
OGLTexture src_tex;
|
state.draw.draw_framebuffer = draw_fb_handle;
|
||||||
src_tex.Create();
|
state.viewport = {static_cast<GLint>(dst_rect.left), static_cast<GLint>(dst_rect.bottom),
|
||||||
state.texture_units[0].texture_2d = src_tex.handle;
|
static_cast<GLsizei>(dst_rect.GetWidth()),
|
||||||
|
static_cast<GLsizei>(dst_rect.GetHeight())};
|
||||||
state.viewport = RectToViewport(rect);
|
|
||||||
state.Apply();
|
state.Apply();
|
||||||
|
|
||||||
const FormatTuple tuple = GetFormatTuple(surface.pixel_format);
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, 0);
|
||||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast<GLint>(surface.stride));
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||||
glActiveTexture(GL_TEXTURE0);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, rect.GetWidth(), rect.GetHeight(), 0,
|
|
||||||
tuple.format, tuple.type, &surface.gl_buffer[buffer_offset]);
|
|
||||||
|
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
|
||||||
cur_state.texture_units[0].texture_2d, 0);
|
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
|
||||||
|
|
||||||
cur_state.Apply();
|
cur_state.Apply();
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,27 +6,24 @@
|
||||||
|
|
||||||
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
||||||
#include "video_core/renderer_opengl/gl_state.h"
|
#include "video_core/renderer_opengl/gl_state.h"
|
||||||
#include "video_core/renderer_opengl/texture_filters/texture_filter_interface.h"
|
#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h"
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
class Bicubic : public TextureFilterInterface {
|
|
||||||
public:
|
|
||||||
static TextureFilterInfo GetInfo() {
|
|
||||||
TextureFilterInfo info;
|
|
||||||
info.name = "Bicubic";
|
|
||||||
info.constructor = std::make_unique<Bicubic, u16>;
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bicubic(u16 scale_factor);
|
class Bicubic : public TextureFilterBase {
|
||||||
void scale(CachedSurface& surface, const Common::Rectangle<u32>& rect,
|
public:
|
||||||
std::size_t buffer_offset) override;
|
static constexpr std::string_view NAME = "Bicubic";
|
||||||
|
|
||||||
|
explicit Bicubic(u16 scale_factor);
|
||||||
|
void Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
||||||
|
const Common::Rectangle<u32>& dst_rect, GLuint read_fb_handle,
|
||||||
|
GLuint draw_fb_handle) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
OpenGLState state{};
|
OpenGLState state{};
|
||||||
OGLProgram program{};
|
OGLProgram program{};
|
||||||
OGLVertexArray vao{};
|
OGLVertexArray vao{};
|
||||||
OGLFramebuffer draw_fbo{};
|
|
||||||
OGLSampler src_sampler{};
|
OGLSampler src_sampler{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace OpenGL
|
} // namespace OpenGL
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/math_util.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_surface_params.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
class TextureFilterBase {
|
||||||
|
friend class TextureFilterer;
|
||||||
|
virtual void Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
||||||
|
const Common::Rectangle<u32>& dst_rect, GLuint read_fb_handle,
|
||||||
|
GLuint draw_fb_handle) = 0;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit TextureFilterBase(u16 scale_factor) : scale_factor{scale_factor} {};
|
||||||
|
virtual ~TextureFilterBase() = default;
|
||||||
|
|
||||||
|
const u16 scale_factor{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
|
@ -1,38 +0,0 @@
|
||||||
// Copyright 2019 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <string_view>
|
|
||||||
#include "common/common_types.h"
|
|
||||||
#include "common/math_util.h"
|
|
||||||
|
|
||||||
namespace OpenGL {
|
|
||||||
|
|
||||||
struct CachedSurface;
|
|
||||||
struct Viewport;
|
|
||||||
|
|
||||||
class TextureFilterInterface {
|
|
||||||
public:
|
|
||||||
const u16 scale_factor{};
|
|
||||||
TextureFilterInterface(u16 scale_factor) : scale_factor{scale_factor} {}
|
|
||||||
virtual void scale(CachedSurface& surface, const Common::Rectangle<u32>& rect,
|
|
||||||
std::size_t buffer_offset) = 0;
|
|
||||||
virtual ~TextureFilterInterface() = default;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
Viewport RectToViewport(const Common::Rectangle<u32>& rect);
|
|
||||||
};
|
|
||||||
|
|
||||||
// every texture filter should have a static GetInfo function
|
|
||||||
struct TextureFilterInfo {
|
|
||||||
std::string_view name;
|
|
||||||
struct {
|
|
||||||
u16 min, max;
|
|
||||||
} clamp_scale{1, 10};
|
|
||||||
std::function<std::unique_ptr<TextureFilterInterface>(u16 scale_factor)> constructor;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace OpenGL
|
|
|
@ -1,89 +0,0 @@
|
||||||
// Copyright 2019 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
#include "common/logging/log.h"
|
|
||||||
#include "video_core/renderer_opengl/gl_state.h"
|
|
||||||
#include "video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h"
|
|
||||||
#include "video_core/renderer_opengl/texture_filters/bicubic/bicubic.h"
|
|
||||||
#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h"
|
|
||||||
#include "video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h"
|
|
||||||
|
|
||||||
namespace OpenGL {
|
|
||||||
|
|
||||||
Viewport TextureFilterInterface::RectToViewport(const Common::Rectangle<u32>& rect) {
|
|
||||||
return {
|
|
||||||
static_cast<GLint>(rect.left) * scale_factor,
|
|
||||||
static_cast<GLint>(rect.top) * scale_factor,
|
|
||||||
static_cast<GLsizei>(rect.GetWidth()) * scale_factor,
|
|
||||||
static_cast<GLsizei>(rect.GetHeight()) * scale_factor,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
template <typename T>
|
|
||||||
std::pair<std::string_view, TextureFilterInfo> FilterMapPair() {
|
|
||||||
return {T::GetInfo().name, T::GetInfo()};
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NoFilter {
|
|
||||||
static TextureFilterInfo GetInfo() {
|
|
||||||
TextureFilterInfo info;
|
|
||||||
info.name = TextureFilterManager::NONE;
|
|
||||||
info.clamp_scale = {1, 1};
|
|
||||||
info.constructor = [](u16) { return nullptr; };
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
const std::map<std::string_view, TextureFilterInfo, TextureFilterManager::FilterNameComp>&
|
|
||||||
TextureFilterManager::TextureFilterMap() {
|
|
||||||
static const std::map<std::string_view, TextureFilterInfo, FilterNameComp> filter_map{
|
|
||||||
FilterMapPair<NoFilter>(),
|
|
||||||
FilterMapPair<Anime4kUltrafast>(),
|
|
||||||
FilterMapPair<Bicubic>(),
|
|
||||||
FilterMapPair<XbrzFreescale>(),
|
|
||||||
};
|
|
||||||
return filter_map;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextureFilterManager::SetTextureFilter(std::string filter_name, u16 new_scale_factor) {
|
|
||||||
if (name == filter_name && scale_factor == new_scale_factor)
|
|
||||||
return;
|
|
||||||
std::lock_guard<std::mutex> lock{mutex};
|
|
||||||
name = std::move(filter_name);
|
|
||||||
scale_factor = new_scale_factor;
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
TextureFilterInterface* TextureFilterManager::GetTextureFilter() const {
|
|
||||||
return filter.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TextureFilterManager::IsUpdated() const {
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextureFilterManager::Reset() {
|
|
||||||
std::lock_guard<std::mutex> lock{mutex};
|
|
||||||
updated = false;
|
|
||||||
auto iter = TextureFilterMap().find(name);
|
|
||||||
if (iter == TextureFilterMap().end()) {
|
|
||||||
LOG_ERROR(Render_OpenGL, "Invalid texture filter: {}", name);
|
|
||||||
filter = nullptr;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto& filter_info = iter->second;
|
|
||||||
|
|
||||||
u16 clamped_scale =
|
|
||||||
std::clamp(scale_factor, filter_info.clamp_scale.min, filter_info.clamp_scale.max);
|
|
||||||
if (clamped_scale != scale_factor)
|
|
||||||
LOG_ERROR(Render_OpenGL, "Invalid scale factor {} for texture filter {}, clamped to {}",
|
|
||||||
scale_factor, filter_info.name, clamped_scale);
|
|
||||||
|
|
||||||
filter = filter_info.constructor(clamped_scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace OpenGL
|
|
|
@ -1,55 +0,0 @@
|
||||||
// Copyright 2019 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <map>
|
|
||||||
#include <memory>
|
|
||||||
#include <mutex>
|
|
||||||
#include <string_view>
|
|
||||||
#include <tuple>
|
|
||||||
#include "video_core/renderer_opengl/texture_filters/texture_filter_interface.h"
|
|
||||||
|
|
||||||
namespace OpenGL {
|
|
||||||
|
|
||||||
class TextureFilterManager {
|
|
||||||
public:
|
|
||||||
static constexpr std::string_view NONE = "none";
|
|
||||||
struct FilterNameComp {
|
|
||||||
bool operator()(const std::string_view a, const std::string_view b) const {
|
|
||||||
bool na = a == NONE;
|
|
||||||
bool nb = b == NONE;
|
|
||||||
if (na | nb)
|
|
||||||
return na & !nb;
|
|
||||||
return a < b;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// function ensures map is initialized before use
|
|
||||||
static const std::map<std::string_view, TextureFilterInfo, FilterNameComp>& TextureFilterMap();
|
|
||||||
|
|
||||||
static TextureFilterManager& GetInstance() {
|
|
||||||
static TextureFilterManager singleton;
|
|
||||||
return singleton;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Destroy() {
|
|
||||||
filter.reset();
|
|
||||||
}
|
|
||||||
void SetTextureFilter(std::string filter_name, u16 new_scale_factor);
|
|
||||||
TextureFilterInterface* GetTextureFilter() const;
|
|
||||||
// returns true if filter has been changed and a cache reset is needed
|
|
||||||
bool IsUpdated() const;
|
|
||||||
void Reset();
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::atomic<bool> updated{false};
|
|
||||||
std::mutex mutex;
|
|
||||||
std::string name{"none"};
|
|
||||||
u16 scale_factor{1};
|
|
||||||
|
|
||||||
std::unique_ptr<TextureFilterInterface> filter;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace OpenGL
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
/// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <functional>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h"
|
||||||
|
#include "video_core/renderer_opengl/texture_filters/bicubic/bicubic.h"
|
||||||
|
#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h"
|
||||||
|
#include "video_core/renderer_opengl/texture_filters/texture_filterer.h"
|
||||||
|
#include "video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using TextureFilterContructor = std::function<std::unique_ptr<TextureFilterBase>(u16)>;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
std::pair<std::string_view, TextureFilterContructor> FilterMapPair() {
|
||||||
|
return {T::NAME, std::make_unique<T, u16>};
|
||||||
|
};
|
||||||
|
|
||||||
|
static const std::unordered_map<std::string_view, TextureFilterContructor> filter_map{
|
||||||
|
{TextureFilterer::NONE, [](u16) { return nullptr; }},
|
||||||
|
FilterMapPair<Anime4kUltrafast>(),
|
||||||
|
FilterMapPair<Bicubic>(),
|
||||||
|
FilterMapPair<XbrzFreescale>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TextureFilterer::TextureFilterer(std::string_view filter_name, u16 scale_factor) {
|
||||||
|
Reset(filter_name, scale_factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextureFilterer::Reset(std::string_view new_filter_name, u16 new_scale_factor) {
|
||||||
|
if (filter_name == new_filter_name && (IsNull() || filter->scale_factor == new_scale_factor))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto iter = filter_map.find(new_filter_name);
|
||||||
|
if (iter == filter_map.end()) {
|
||||||
|
LOG_ERROR(Render_OpenGL, "Invalid texture filter: {}", new_filter_name);
|
||||||
|
filter = nullptr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
filter_name = iter->first;
|
||||||
|
filter = iter->second(new_scale_factor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextureFilterer::IsNull() const {
|
||||||
|
return !filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextureFilterer::Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
||||||
|
const Common::Rectangle<u32>& dst_rect,
|
||||||
|
SurfaceParams::SurfaceType type, GLuint read_fb_handle,
|
||||||
|
GLuint draw_fb_handle) {
|
||||||
|
// depth / stencil texture filtering is not supported for now
|
||||||
|
if (IsNull() ||
|
||||||
|
(type != SurfaceParams::SurfaceType::Color && type != SurfaceParams::SurfaceType::Texture))
|
||||||
|
return false;
|
||||||
|
filter->Filter(src_tex, src_rect, dst_tex, dst_rect, read_fb_handle, draw_fb_handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string_view> TextureFilterer::GetFilterNames() {
|
||||||
|
std::vector<std::string_view> ret;
|
||||||
|
std::transform(filter_map.begin(), filter_map.end(), std::back_inserter(ret),
|
||||||
|
[](auto pair) { return pair.first; });
|
||||||
|
std::sort(ret.begin(), ret.end(), [](std::string_view lhs, std::string_view rhs) {
|
||||||
|
// sort lexicographically with none at the top
|
||||||
|
bool lhs_is_none{lhs == NONE};
|
||||||
|
bool rhs_is_none{rhs == NONE};
|
||||||
|
if (lhs_is_none || rhs_is_none)
|
||||||
|
return lhs_is_none && !rhs_is_none;
|
||||||
|
return lhs < rhs;
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
#include <glad/glad.h>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/math_util.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_surface_params.h"
|
||||||
|
#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
class TextureFilterer {
|
||||||
|
public:
|
||||||
|
static constexpr std::string_view NONE = "none";
|
||||||
|
|
||||||
|
explicit TextureFilterer(std::string_view filter_name, u16 scale_factor);
|
||||||
|
// returns true if the filter actually changed
|
||||||
|
bool Reset(std::string_view new_filter_name, u16 new_scale_factor);
|
||||||
|
// returns true if there is no active filter
|
||||||
|
bool IsNull() const;
|
||||||
|
// returns true if the texture was able to be filtered
|
||||||
|
bool Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
||||||
|
const Common::Rectangle<u32>& dst_rect, SurfaceParams::SurfaceType type,
|
||||||
|
GLuint read_fb_handle, GLuint draw_fb_handle);
|
||||||
|
|
||||||
|
static std::vector<std::string_view> GetFilterNames();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string_view filter_name = NONE;
|
||||||
|
std::unique_ptr<TextureFilterBase> filter;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
|
@ -48,12 +48,11 @@
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
XbrzFreescale::XbrzFreescale(u16 scale_factor) : TextureFilterInterface(scale_factor) {
|
XbrzFreescale::XbrzFreescale(u16 scale_factor) : TextureFilterBase(scale_factor) {
|
||||||
const OpenGLState cur_state = OpenGLState::GetCurState();
|
const OpenGLState cur_state = OpenGLState::GetCurState();
|
||||||
|
|
||||||
program.Create(xbrz_freescale_vert.data(), xbrz_freescale_frag.data());
|
program.Create(xbrz_freescale_vert.data(), xbrz_freescale_frag.data());
|
||||||
vao.Create();
|
vao.Create();
|
||||||
draw_fbo.Create();
|
|
||||||
src_sampler.Create();
|
src_sampler.Create();
|
||||||
|
|
||||||
state.draw.shader_program = program.handle;
|
state.draw.shader_program = program.handle;
|
||||||
|
@ -68,31 +67,24 @@ XbrzFreescale::XbrzFreescale(u16 scale_factor) : TextureFilterInterface(scale_fa
|
||||||
cur_state.Apply();
|
cur_state.Apply();
|
||||||
state.draw.vertex_array = vao.handle;
|
state.draw.vertex_array = vao.handle;
|
||||||
state.draw.shader_program = program.handle;
|
state.draw.shader_program = program.handle;
|
||||||
state.draw.draw_framebuffer = draw_fbo.handle;
|
|
||||||
state.texture_units[0].sampler = src_sampler.handle;
|
state.texture_units[0].sampler = src_sampler.handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
void XbrzFreescale::scale(CachedSurface& surface, const Common::Rectangle<u32>& rect,
|
void XbrzFreescale::Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
||||||
std::size_t buffer_offset) {
|
const Common::Rectangle<u32>& dst_rect, GLuint read_fb_handle,
|
||||||
|
GLuint draw_fb_handle) {
|
||||||
const OpenGLState cur_state = OpenGLState::GetCurState();
|
const OpenGLState cur_state = OpenGLState::GetCurState();
|
||||||
|
|
||||||
OGLTexture src_tex;
|
state.texture_units[0].texture_2d = src_tex;
|
||||||
src_tex.Create();
|
state.draw.draw_framebuffer = draw_fb_handle;
|
||||||
state.texture_units[0].texture_2d = src_tex.handle;
|
state.viewport = {static_cast<GLint>(dst_rect.left), static_cast<GLint>(dst_rect.bottom),
|
||||||
|
static_cast<GLsizei>(dst_rect.GetWidth()),
|
||||||
state.viewport = RectToViewport(rect);
|
static_cast<GLsizei>(dst_rect.GetHeight())};
|
||||||
state.Apply();
|
state.Apply();
|
||||||
|
|
||||||
const FormatTuple tuple = GetFormatTuple(surface.pixel_format);
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, 0);
|
||||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast<GLint>(surface.stride));
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||||
glActiveTexture(GL_TEXTURE0);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, rect.GetWidth(), rect.GetHeight(), 0,
|
|
||||||
tuple.format, tuple.type, &surface.gl_buffer[buffer_offset]);
|
|
||||||
|
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
|
||||||
cur_state.texture_units[0].texture_2d, 0);
|
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
|
||||||
|
|
||||||
cur_state.Apply();
|
cur_state.Apply();
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,28 +6,23 @@
|
||||||
|
|
||||||
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
||||||
#include "video_core/renderer_opengl/gl_state.h"
|
#include "video_core/renderer_opengl/gl_state.h"
|
||||||
#include "video_core/renderer_opengl/texture_filters/texture_filter_interface.h"
|
#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h"
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
class XbrzFreescale : public TextureFilterInterface {
|
class XbrzFreescale : public TextureFilterBase {
|
||||||
public:
|
public:
|
||||||
static TextureFilterInfo GetInfo() {
|
static constexpr std::string_view NAME = "xBRZ freescale";
|
||||||
TextureFilterInfo info;
|
|
||||||
info.name = "xBRZ freescale";
|
|
||||||
info.constructor = std::make_unique<XbrzFreescale, u16>;
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
XbrzFreescale(u16 scale_factor);
|
explicit XbrzFreescale(u16 scale_factor);
|
||||||
void scale(CachedSurface& surface, const Common::Rectangle<u32>& rect,
|
void Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
||||||
std::size_t buffer_offset) override;
|
const Common::Rectangle<u32>& dst_rect, GLuint read_fb_handle,
|
||||||
|
GLuint draw_fb_handle) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
OpenGLState state{};
|
OpenGLState state{};
|
||||||
OGLProgram program{};
|
OGLProgram program{};
|
||||||
OGLVertexArray vao{};
|
OGLVertexArray vao{};
|
||||||
OGLFramebuffer draw_fbo{};
|
|
||||||
OGLSampler src_sampler{};
|
OGLSampler src_sampler{};
|
||||||
};
|
};
|
||||||
} // namespace OpenGL
|
} // namespace OpenGL
|
||||||
|
|
|
@ -28,6 +28,7 @@ std::atomic<bool> g_use_disk_shader_cache;
|
||||||
std::atomic<bool> g_renderer_bg_color_update_requested;
|
std::atomic<bool> g_renderer_bg_color_update_requested;
|
||||||
std::atomic<bool> g_renderer_sampler_update_requested;
|
std::atomic<bool> g_renderer_sampler_update_requested;
|
||||||
std::atomic<bool> g_renderer_shader_update_requested;
|
std::atomic<bool> g_renderer_shader_update_requested;
|
||||||
|
std::atomic<bool> g_texture_filter_update_requested;
|
||||||
// Screenshot
|
// Screenshot
|
||||||
std::atomic<bool> g_renderer_screenshot_requested;
|
std::atomic<bool> g_renderer_screenshot_requested;
|
||||||
void* g_screenshot_bits;
|
void* g_screenshot_bits;
|
||||||
|
|
|
@ -36,6 +36,7 @@ extern std::atomic<bool> g_use_disk_shader_cache;
|
||||||
extern std::atomic<bool> g_renderer_bg_color_update_requested;
|
extern std::atomic<bool> g_renderer_bg_color_update_requested;
|
||||||
extern std::atomic<bool> g_renderer_sampler_update_requested;
|
extern std::atomic<bool> g_renderer_sampler_update_requested;
|
||||||
extern std::atomic<bool> g_renderer_shader_update_requested;
|
extern std::atomic<bool> g_renderer_shader_update_requested;
|
||||||
|
extern std::atomic<bool> g_texture_filter_update_requested;
|
||||||
// Screenshot
|
// Screenshot
|
||||||
extern std::atomic<bool> g_renderer_screenshot_requested;
|
extern std::atomic<bool> g_renderer_screenshot_requested;
|
||||||
extern void* g_screenshot_bits;
|
extern void* g_screenshot_bits;
|
||||||
|
|
Reference in New Issue