This commit is contained in:
BearOso 2023-03-23 16:53:43 -05:00
parent 19d0016c5a
commit 1b1325066b
100 changed files with 21929 additions and 97 deletions

View File

@ -41,7 +41,7 @@ void S9xLandSamples (void);
void S9xClearSamples (void);
bool8 S9xMixSamples (uint8 *, int);
void S9xSetSamplesAvailableCallback (apu_callback, void *);
void S9xUpdateDynamicRate (int, int);
void S9xUpdateDynamicRate (int empty = 1, int buffer_size = 2);
#define DSP_INTERPOLATION_NONE 0
#define DSP_INTERPOLATION_LINEAR 1

View File

@ -0,0 +1,118 @@
/*****************************************************************************\
Snes9x - Portable Super Nintendo Entertainment System (TM) emulator.
This file is licensed under the Snes9x License.
For further information, consult the LICENSE file in the root directory.
\*****************************************************************************/
#include "s9x_sound_driver_cubeb.hpp"
#include <cstdio>
void S9xCubebSoundDriver::write_samples(int16_t *data, int samples)
{
if (samples > buffer.space_empty())
samples = buffer.space_empty();
buffer.push(data, samples);
}
S9xCubebSoundDriver::S9xCubebSoundDriver()
{
}
S9xCubebSoundDriver::~S9xCubebSoundDriver()
{
deinit();
}
void S9xCubebSoundDriver::init()
{
if (!context)
cubeb_init(&context, "Snes9x", nullptr);
stop();
}
void S9xCubebSoundDriver::deinit()
{
stop();
if (stream)
{
cubeb_stream_destroy(stream);
stream = nullptr;
}
if (context)
{
cubeb_destroy(context);
context = nullptr;
}
}
void S9xCubebSoundDriver::start()
{
if (stream)
cubeb_stream_start(stream);
}
void S9xCubebSoundDriver::stop()
{
if (stream)
cubeb_stream_stop(stream);
}
void state_callback(cubeb_stream *stream, void *user_ptr, cubeb_state state)
{
}
long data_callback(cubeb_stream *stream, void *user_ptr,
void const *input_buffer,
void *output_buffer, long nframes)
{
return ((S9xCubebSoundDriver *)user_ptr)->data_callback(stream, input_buffer, output_buffer, nframes);
}
long S9xCubebSoundDriver::data_callback(cubeb_stream *stream, void const *input_buffer, void *output_buffer, long nframes)
{
buffer.read((int16_t *)output_buffer, nframes * 2);
return nframes;
}
bool S9xCubebSoundDriver::open_device(int playback_rate, int buffer_size)
{
cubeb_stream_params params{};
params.channels = 2;
params.format = CUBEB_SAMPLE_S16LE;
params.layout = CUBEB_LAYOUT_UNDEFINED;
params.rate = playback_rate;
params.prefs = CUBEB_STREAM_PREF_NONE;
uint32_t min_latency;
cubeb_get_min_latency(context, &params, &min_latency);
auto retval = cubeb_stream_init(context, &stream, "Snes9x",
nullptr, nullptr,
nullptr, &params,
min_latency,
&::data_callback,
&state_callback,
this);
if (retval != CUBEB_OK)
{
printf("Failed to start stream. Error: %d!\n", retval);
stream = nullptr;
return false;
}
buffer.resize(2 * buffer_size * playback_rate / 1000);
return true;
}
int S9xCubebSoundDriver::space_free()
{
return buffer.space_empty();
}
std::pair<int, int> S9xCubebSoundDriver::buffer_level()
{
return { buffer.space_empty(), buffer.buffer_size };
}

View File

@ -0,0 +1,36 @@
/*****************************************************************************\
Snes9x - Portable Super Nintendo Entertainment System (TM) emulator.
This file is licensed under the Snes9x License.
For further information, consult the LICENSE file in the root directory.
\*****************************************************************************/
#ifndef __S9X_SOUND_DRIVER_CUBEB_HPP
#define __S9X_SOUND_DRIVER_CUBEB_HPP
#include "s9x_sound_driver.hpp"
#include <cstdint>
#include "cubeb/cubeb.h"
#include "../../apu/resampler.h"
class S9xCubebSoundDriver : public S9xSoundDriver
{
public:
S9xCubebSoundDriver();
~S9xCubebSoundDriver();
void init() override;
void deinit() override;
bool open_device(int playback_rate, int buffer_size) override;
void start() override;
void stop() override;
long data_callback(cubeb_stream *stream, void const *input_buffer, void *output_buffer, long nframes);
void write_samples(int16_t *data, int samples) override;
int space_free() override;
std::pair<int, int> buffer_level() override;
private:
Resampler buffer;
cubeb *context = nullptr;
cubeb_stream *stream = nullptr;
};
#endif /* __S9X_SOUND_DRIVER_SDL_HPP */

View File

@ -15,6 +15,11 @@ S9xPortAudioSoundDriver::S9xPortAudioSoundDriver()
audio_stream = NULL;
}
S9xPortAudioSoundDriver::~S9xPortAudioSoundDriver()
{
deinit();
}
void S9xPortAudioSoundDriver::init()
{
PaError err;
@ -60,9 +65,64 @@ void S9xPortAudioSoundDriver::stop()
}
}
bool S9xPortAudioSoundDriver::tryHostAPI(int index)
{
auto hostapi_info = Pa_GetHostApiInfo(index);
if (!hostapi_info)
{
printf("Host API #%d has no info\n", index);
return false;
}
printf("Attempting API: %s\n", hostapi_info->name);
auto device_info = Pa_GetDeviceInfo(hostapi_info->defaultOutputDevice);
if (!device_info)
{
printf("(%s)...No device info available.\n", hostapi_info->name);
return false;
}
PaStreamParameters param{};
param.device = hostapi_info->defaultOutputDevice;
param.suggestedLatency = buffer_size_ms * 0.001;
param.channelCount = 2;
param.sampleFormat = paInt16;
param.hostApiSpecificStreamInfo = NULL;
printf("(%s : %s, latency %dms)...\n",
hostapi_info->name,
device_info->name,
(int)(param.suggestedLatency * 1000.0));
auto err = Pa_OpenStream(&audio_stream,
NULL,
&param,
playback_rate,
0,
paNoFlag,
NULL,
NULL);
int frames = playback_rate * buffer_size_ms / 1000;
//int frames = Pa_GetStreamWriteAvailable(audio_stream);
printf("PortAudio set buffer size to %d frames.\n", frames);
output_buffer_size = frames;
if (err == paNoError)
{
printf("OK\n");
return true;
}
else
{
printf("Failed (%s)\n", Pa_GetErrorText(err));
return false;
}
}
bool S9xPortAudioSoundDriver::open_device(int playback_rate, int buffer_size_ms)
{
PaStreamParameters param;
const PaDeviceInfo *device_info;
const PaHostApiInfo *hostapi_info;
PaError err = paNoError;
@ -74,86 +134,33 @@ bool S9xPortAudioSoundDriver::open_device(int playback_rate, int buffer_size_ms)
if (err != paNoError)
{
fprintf(stderr,
"Couldn't reset audio stream.\nError: %s\n",
Pa_GetErrorText(err));
fprintf(stderr, "Couldn't reset audio stream.\nError: %s\n", Pa_GetErrorText(err));
return true;
}
audio_stream = NULL;
}
param.channelCount = 2;
param.sampleFormat = paInt16;
param.hostApiSpecificStreamInfo = NULL;
this->playback_rate = playback_rate;
this->buffer_size_ms = buffer_size_ms;
printf("PortAudio sound driver initializing...\n");
int host = 2; //Pa_GetDefaultHostApi();
if (tryHostAPI(host))
return true;
for (int i = 0; i < Pa_GetHostApiCount(); i++)
{
printf(" --> ");
hostapi_info = Pa_GetHostApiInfo(i);
if (!hostapi_info)
{
printf("Host API #%d has no info\n", i);
err = paNotInitialized;
continue;
}
device_info = Pa_GetDeviceInfo(hostapi_info->defaultOutputDevice);
if (!device_info)
{
printf("(%s)...No device info available.\n", hostapi_info->name);
err = paNotInitialized;
continue;
}
param.device = hostapi_info->defaultOutputDevice;
param.suggestedLatency = buffer_size_ms * 0.001;
printf("(%s : %s, latency %dms)...\n",
hostapi_info->name,
device_info->name,
(int)(param.suggestedLatency * 1000.0));
fflush(stdout);
err = Pa_OpenStream(&audio_stream,
NULL,
&param,
playback_rate,
0,
paNoFlag,
NULL,
NULL);
int frames = Pa_GetStreamWriteAvailable(audio_stream);
printf ("PortAudio set buffer size to %d frames.\n", frames);
output_buffer_size = frames;
if (err == paNoError)
{
printf("OK\n");
break;
}
else
{
printf("Failed (%s)\n",
Pa_GetErrorText(err));
}
}
if (err != paNoError || Pa_GetHostApiCount() < 1)
{
fprintf(stderr,
"Couldn't initialize sound\n");
return false;
}
if (Pa_GetDefaultHostApi() != i)
if (tryHostAPI(i))
return true;
}
fprintf(stderr, "Couldn't initialize sound\n");
return false;
}
int S9xPortAudioSoundDriver::space_free()
{
return Pa_GetStreamWriteAvailable(audio_stream) * 2;

View File

@ -16,6 +16,7 @@ class S9xPortAudioSoundDriver : public S9xSoundDriver
{
public:
S9xPortAudioSoundDriver();
~S9xPortAudioSoundDriver();
void init() override;
void deinit() override;
bool open_device(int playback_rate, int buffer_size) override;
@ -25,9 +26,12 @@ class S9xPortAudioSoundDriver : public S9xSoundDriver
int space_free() override;
std::pair<int, int> buffer_level() override;
void samples_available();
bool tryHostAPI(int index);
private:
PaStream *audio_stream;
int playback_rate;
int buffer_size_ms;
int output_buffer_size;
};

View File

@ -28,6 +28,11 @@ S9xSDLSoundDriver::S9xSDLSoundDriver()
{
}
S9xSDLSoundDriver::~S9xSDLSoundDriver()
{
deinit();
}
void S9xSDLSoundDriver::init()
{
SDL_InitSubSystem(SDL_INIT_AUDIO);

View File

@ -22,6 +22,7 @@ class S9xSDLSoundDriver : public S9xSoundDriver
{
public:
S9xSDLSoundDriver();
~S9xSDLSoundDriver();
void init() override;
void deinit() override;
bool open_device(int playback_rate, int buffer_size) override;

View File

@ -94,6 +94,8 @@ bool GTKGLXContext::create_context()
return false;
}
resize();
return true;
}

View File

@ -47,6 +47,7 @@ bool WaylandEGLContext::create_context()
EGL_RED_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_ALPHA_SIZE, 0,
EGL_NONE
};

View File

@ -158,6 +158,7 @@ void WaylandSurface::resize(Metrics m)
{
metrics = m;
auto [w, h] = get_size();
wl_subsurface_set_position(subsurface, m.x, m.y);
if (!viewport)

View File

@ -0,0 +1,105 @@
/*****************************************************************************\
Snes9x - Portable Super Nintendo Entertainment System (TM) emulator.
This file is licensed under the Snes9x License.
For further information, consult the LICENSE file in the root directory.
\*****************************************************************************/
#include <cstdlib>
#include <cstdio>
#include "wgl_context.hpp"
WGLContext::WGLContext()
{
hwnd = nullptr;
hdc = nullptr;
hglrc = nullptr;
version_major = -1;
version_minor = -1;
}
WGLContext::~WGLContext()
{
deinit();
}
void WGLContext::deinit()
{
if (wglMakeCurrent)
wglMakeCurrent(nullptr, nullptr);
if (hglrc)
wglDeleteContext(hglrc);
if (hdc)
ReleaseDC(hwnd, hdc);
}
bool WGLContext::attach(HWND target)
{
hwnd = target;
hdc = GetDC(hwnd);
PIXELFORMATDESCRIPTOR pfd{};
pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
pfd.nVersion = 1;
pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
pfd.iPixelType = PFD_TYPE_RGBA;
pfd.cColorBits = 32;
pfd.cDepthBits = 16;
pfd.iLayerType = PFD_MAIN_PLANE;
auto pfdIndex = ChoosePixelFormat(hdc, &pfd);
if (!pfdIndex)
return false;
if (!SetPixelFormat(hdc, pfdIndex, &pfd))
{
// Shouldn't happen
}
return true;
}
bool WGLContext::create_context()
{
hglrc = wglCreateContext(hdc);
if (!hglrc)
return false;
if (!wglMakeCurrent(hdc, hglrc))
return false;
gladLoaderLoadWGL(hdc);
resize();
return true;
}
void WGLContext::resize()
{
RECT rect;
GetClientRect(hwnd, &rect);
this->width = rect.right - rect.left;
this->height = rect.bottom - rect.top;
make_current();
}
void WGLContext::swap_buffers()
{
SwapBuffers(hdc);
}
bool WGLContext::ready()
{
return true;
}
void WGLContext::make_current()
{
wglMakeCurrent(hdc, hglrc);
}
void WGLContext::swap_interval(int frames)
{
wglSwapIntervalEXT(frames);
}

View File

@ -0,0 +1,36 @@
/*****************************************************************************\
Snes9x - Portable Super Nintendo Entertainment System (TM) emulator.
This file is licensed under the Snes9x License.
For further information, consult the LICENSE file in the root directory.
\*****************************************************************************/
#ifndef __WGL_CONTEXT_HPP
#define __WGL_CONTEXT_HPP
#include "opengl_context.hpp"
#include <glad/wgl.h>
class WGLContext : public OpenGLContext
{
public:
WGLContext();
~WGLContext();
bool attach(HWND xid);
bool create_context() override;
void resize() override;
void swap_buffers() override;
void swap_interval(int frames) override;
void make_current() override;
bool ready();
void deinit();
HWND hwnd;
HDC hdc;
HGLRC hglrc;
int version_major;
int version_minor;
};
#endif

View File

@ -2,7 +2,7 @@
#define VULKAN_MEMORY_ALLOCATOR_HPP
#if !defined(AMD_VULKAN_MEMORY_ALLOCATOR_H)
#include <vk_mem_alloc.h>
#include "vk_mem_alloc.h"
#endif
#include <vulkan/vulkan.hpp>

View File

@ -40,7 +40,7 @@ add_compile_definitions(HAVE_LIBPNG
SNES9XLOCALEDIR=\"${LOCALEDIR}\")
set(INCLUDES ../apu/bapu ../ src)
set(SOURCES)
set(ARGS -Wall -Wno-unused-parameter)
set(ARGS -Wall -Wno-unused-parameter -Wno-unused-variable -Wno-nullability-completeness)
set(LIBS)
set(DEFINES)

View File

@ -14,6 +14,7 @@
#include <string>
#include <vector>
#include <cstdint>
struct CMemory
{

1
port.h
View File

@ -32,7 +32,6 @@
#define RIGHTSHIFT_int16_IS_SAR
#define RIGHTSHIFT_int32_IS_SAR
#ifndef __LIBRETRO__
#define SNES_JOY_READ_CALLBACKS
#endif //__LIBRETRO__
#endif

259
qt/CMakeLists.txt Normal file
View File

@ -0,0 +1,259 @@
cmake_minimum_required(VERSION 3.20)
project(snes9x-qt VERSION 1.61)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_GLOBAL_AUTOGEN_TARGET ON)
set(DEFINES SNES9X_QT)
set(SNES9X_CORE_SOURCES
../fxinst.cpp
../fxemu.cpp
../fxdbg.cpp
../c4.cpp
../c4emu.cpp
../apu/apu.cpp
../apu/bapu/dsp/sdsp.cpp
../apu/bapu/smp/smp.cpp
../apu/bapu/smp/smp_state.cpp
../msu1.cpp
../msu1.h
../dsp.cpp
../dsp1.cpp
../dsp2.cpp
../dsp3.cpp
../dsp4.cpp
../spc7110.cpp
../obc1.cpp
../seta.cpp
../seta010.cpp
../seta011.cpp
../seta018.cpp
../controls.cpp
../crosshairs.cpp
../cpu.cpp
../sa1.cpp
../debug.cpp
../sdd1.cpp
../tile.cpp
../tileimpl-n1x1.cpp
../tileimpl-n2x1.cpp
../tileimpl-h2x1.cpp
../srtc.cpp
../gfx.cpp
../memmap.cpp
../clip.cpp
../ppu.cpp
../dma.cpp
../snes9x.cpp
../globals.cpp
../stream.cpp
../conffile.cpp
../bsx.cpp
../snapshot.cpp
../screenshot.cpp
../movie.cpp
../statemanager.cpp
../sha256.cpp
../bml.cpp
../cpuops.cpp
../cpuexec.cpp
../sa1cpu.cpp
../cheats.cpp
../cheats2.cpp
../sdd1emu.cpp
../netplay.cpp
../server.cpp
../loadzip.cpp
../fscompat.cpp)
add_library(snes9x-core ${SNES9X_CORE_SOURCES})
target_include_directories(snes9x-core PRIVATE ../)
target_compile_definitions(snes9x-core PRIVATE ZLIB HAVE_STDINT_H ALLOW_CPU_OVERCLOCK)
find_package(Qt6 REQUIRED COMPONENTS Widgets Gui)
find_package(PkgConfig REQUIRED)
pkg_check_modules(SDL REQUIRED sdl2)
pkg_check_modules(ZLIB REQUIRED zlib)
pkg_check_modules(PORTAUDIO REQUIRED portaudio-2.0)
list(APPEND LIBS Qt6::Widgets Qt6::Gui ${SDL_LIBRARIES} ${ZLIB_LIBRARIES} ${PORTAUDIO_LIBRARIES})
list(APPEND INCLUDES ${SDL_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS} ${PORTAUDIO_INCLUDE_DIRS} ${Qt6Gui_PRIVATE_INCLUDE_DIRS})
list(APPEND FLAGS ${SDL_COMPILE_FLAGS} ${ZLIB_COMPILE_FLAGS} ${PORTAUDIO_COMPILE_FLAGS})
set(QT_GUI_SOURCES
src/main.cpp
src/EmuApplication.cpp
src/EmuMainWindow.cpp
src/Snes9xController.cpp
src/EmuSettingsWindow.cpp
src/EmuConfig.cpp
src/EmuInputPanel.cpp
src/EmuBinding.cpp
src/EmuCanvas.cpp
src/BindingPanel.cpp
src/ControllerPanel.cpp
src/DisplayPanel.cpp
src/SoundPanel.cpp
src/EmulationPanel.cpp
src/ShortcutsPanel.cpp
src/GeneralPanel.cpp
src/FoldersPanel.cpp
src/SDLInputManager.cpp
src/ShaderParametersDialog.cpp
src/SoftwareScalers.cpp
src/EmuCanvasQt.cpp
src/EmuCanvasOpenGL.cpp
src/EmuCanvasVulkan.cpp
../external/glad/src/gl.c
../common/audio/s9x_sound_driver_sdl.cpp
../common/audio/s9x_sound_driver_sdl.hpp
../common/audio/s9x_sound_driver_portaudio.cpp
../common/audio/s9x_sound_driver_portaudio.hpp
../common/audio/s9x_sound_driver_cubeb.cpp
../common/audio/s9x_sound_driver_cubeb.hpp
../filter/2xsai.cpp
../filter/2xsai.h
../filter/epx.cpp
../filter/epx.h
../filter/snes_ntsc_config.h
../filter/snes_ntsc.h
../filter/snes_ntsc_impl.h
../filter/snes_ntsc.c)
if(NOT ${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
pkg_check_modules(WAYLAND REQUIRED wayland-client wayland-egl)
include(FindX11)
if(NOT X11_FOUND)
error()
endif()
list(APPEND INCLUDES ${WAYLAND_INCLUDE_DIRS} ${X11_INCLUDE_DIRS})
list(APPEND LIBS ${WAYLAND_LIBRARIES} ${X11_LIBRARIES})
list(APPEND FLAGS ${WAYLAND_CFLAGS})
set(PLATFORM_SOURCES
../common/video/glx_context.cpp
../common/video/wayland_egl_context.cpp
../common/video/wayland_surface.cpp
../common/video/fractional-scale-v1.c
../common/video/viewporter-client-protocol.c
../common/video/wayland-idle-inhibit-unstable-v1.c
../external/glad/src/glx.c
../external/glad/src/egl.c)
else()
set(PLATFORM_SOURCES
../common/video/wgl_context.cpp
../external/glad/src/wgl.c)
list(APPEND LIBS opengl32)
endif()
set(QT_UI_FILES
src/GeneralPanel.ui
src/ControllerPanel.ui
src/EmuSettingsWindow.ui
src/DisplayPanel.ui
src/SoundPanel.ui
src/EmulationPanel.ui
src/ShortcutsPanel.ui
src/FoldersPanel.ui)
set(USE_SANITIZERS CACHE BOOL OFF)
set(BUILD_TESTS CACHE BOOL OFF)
set(BUILD_TOOLS CACHE BOOL OFF)
add_subdirectory("../external/cubeb" "cubeb" EXCLUDE_FROM_ALL)
list(APPEND LIBS cubeb)
list(APPEND INCLUDES "../external/cubeb/include")
set(BUILD_TESTING CACHE BOOL OFF)
add_subdirectory("../external/glslang" "glslang" EXCLUDE_FROM_ALL)
list(APPEND LIBS
glslang
OGLCompiler
HLSL
OSDependent
SPIRV
glslang-default-resource-limits)
list(APPEND INCLUDES "../external/glslang")
set(SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS CACHE BOOL ON)
add_subdirectory("../external/SPIRV-Cross" "SPIRV-Cross" EXCLUDE_FROM_ALL)
list(APPEND LIBS
spirv-cross-core
spirv-cross-glsl
spirv-cross-reflect
spirv-cross-cpp)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
list(APPEND DEFINES "VK_USE_PLATFORM_WIN32_KHR")
else()
list(APPEND DEFINES
"VK_USE_PLATFORM_XLIB_KHR"
"VK_USE_PLATFORM_WAYLAND_KHR")
endif()
list(APPEND DEFINES
"VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1"
"VMA_DYNAMIC_VULKAN_FUNCTIONS=1"
"VMA_STATIC_VULKAN_FUNCTIONS=0"
"USE_SLANG")
list(APPEND INCLUDES
../external/vulkan-headers/include
../external/VulkanMemoryAllocator-Hpp/include
../external/stb
"../external/glad/include")
list(APPEND SOURCES
../vulkan/slang_shader.cpp
../vulkan/slang_shader.hpp
../vulkan/slang_preset.cpp
../vulkan/slang_preset.hpp
../vulkan/vulkan_hpp_storage.cpp
../vulkan/vk_mem_alloc_implementation.cpp
../vulkan/vulkan_context.cpp
../vulkan/vulkan_context.hpp
../vulkan/vulkan_texture.cpp
../vulkan/vulkan_texture.hpp
../vulkan/vulkan_swapchain.cpp
../vulkan/vulkan_swapchain.hpp
../vulkan/vulkan_slang_pipeline.cpp
../vulkan/vulkan_slang_pipeline.hpp
../vulkan/vulkan_pipeline_image.cpp
../vulkan/vulkan_pipeline_image.hpp
../vulkan/vulkan_shader_chain.cpp
../vulkan/vulkan_shader_chain.hpp
../vulkan/vulkan_simple_output.hpp
../vulkan/vulkan_simple_output.cpp
../vulkan/std_chrono_throttle.cpp
../vulkan/std_chrono_throttle.hpp
../vulkan/slang_helpers.cpp
../vulkan/slang_helpers.hpp
../vulkan/slang_preset_ini.cpp
../vulkan/slang_preset_ini.hpp
../external/stb/stb_image_implementation.cpp
../shaders/glsl.cpp
../shaders/slang.cpp
../shaders/shader_helpers.cpp)
list(APPEND DEFINES "IMGUI_IMPL_VULKAN_NO_PROTOTYPES")
list(APPEND SOURCES ../external/imgui/imgui.cpp
../external/imgui/imgui_demo.cpp
../external/imgui/imgui_draw.cpp
../external/imgui/imgui_tables.cpp
../external/imgui/imgui_widgets.cpp
../external/imgui/imgui_impl_opengl3.cpp
../external/imgui/imgui_impl_vulkan.cpp
../external/imgui/snes9x_imgui.cpp)
list(APPEND INCLUDES ../external/imgui)
add_executable(snes9x-qt ${QT_GUI_SOURCES} ${SOURCES} ${PLATFORM_SOURCES} src/resources/snes9x.qrc)
target_link_libraries(snes9x-qt snes9x-core ${LIBS})
target_compile_definitions(snes9x-qt PRIVATE ${DEFINES})
target_compile_options(snes9x-qt PRIVATE ${FLAGS})
target_include_directories(snes9x-qt PRIVATE "../" ${INCLUDES})

143
qt/src/BindingPanel.cpp Normal file
View File

@ -0,0 +1,143 @@
#include "BindingPanel.hpp"
#include <QTimer>
BindingPanel::BindingPanel(EmuApplication *app)
: app(app)
{
binding_table_widget = nullptr;
joypads_changed = nullptr;
}
void BindingPanel::setTableWidget(QTableWidget *bindingTableWidget, EmuBinding *binding, int width, int height)
{
keyboard_icon.addFile(":/icons/blackicons/key.svg");
joypad_icon.addFile(":/icons/blackicons/joypad.svg");
this->binding_table_widget = bindingTableWidget;
this->binding = binding;
table_width = width;
table_height = height;
connect(bindingTableWidget, &QTableWidget::cellActivated, [&](int row, int column) {
cellActivated(row, column);
});
connect(bindingTableWidget, &QTableWidget::cellPressed, [&](int row, int column) {
cellActivated(row, column);
});
fillTable();
cell_column = -1;
cell_row = -1;
awaiting_binding = false;
}
BindingPanel::~BindingPanel()
{
app->qtapp->removeEventFilter(this);
timer.reset();
}
void BindingPanel::showEvent(QShowEvent *event)
{
QWidget::showEvent(event);
}
void BindingPanel::hideEvent(QHideEvent *event)
{
awaiting_binding = false;
setRedirectInput(false);
QWidget::hideEvent(event);
}
void BindingPanel::setRedirectInput(bool redirect)
{
if (redirect)
{
app->binding_callback = [&](EmuBinding b)
{
finalizeCurrentBinding(b);
};
app->joypads_changed_callback = [&] {
if (joypads_changed)
joypads_changed();
};
}
else
{
app->binding_callback = nullptr;
app->joypads_changed_callback = nullptr;
}
}
void BindingPanel::updateCellFromBinding(int row, int column)
{
EmuBinding &b = binding[row * table_width + column];
auto table_item = binding_table_widget->item(row, column);
if (!table_item)
{
table_item = new QTableWidgetItem();
binding_table_widget->setItem(row, column, table_item);
}
table_item->setText(b.to_string().c_str());;
table_item->setIcon(b.type == EmuBinding::Keyboard ? keyboard_icon :
b.type == EmuBinding::Joystick ? joypad_icon :
QIcon());
}
void BindingPanel::fillTable()
{
for (int column = 0; column < table_width; column++)
for (int row = 0; row < table_height; row++)
updateCellFromBinding(row, column);
}
void BindingPanel::cellActivated(int row, int column)
{
if (awaiting_binding)
{
updateCellFromBinding(cell_row, cell_column);
}
cell_column = column;
cell_row = row;
auto table_item = binding_table_widget->item(row, column);
if (!table_item)
{
table_item = new QTableWidgetItem();
binding_table_widget->setItem(row, column, table_item);
}
table_item->setText("...");
setRedirectInput(true);
awaiting_binding = true;
accept_return = false;
}
void BindingPanel::finalizeCurrentBinding(EmuBinding b)
{
if (!awaiting_binding)
return;
auto &slot = binding[cell_row * this->table_width + cell_column];
slot = b;
if (b.type == EmuBinding::Keyboard && b.keycode == Qt::Key_Escape)
slot = {};
if (b.type == EmuBinding::Keyboard && b.keycode == Qt::Key_Return && !accept_return)
{
accept_return = true;
return;
}
updateCellFromBinding(cell_row, cell_column);
setRedirectInput(false);
awaiting_binding = false;
app->updateBindings();
}
void BindingPanel::onJoypadsChanged(std::function<void()> func)
{
joypads_changed = func;
}

37
qt/src/BindingPanel.hpp Normal file
View File

@ -0,0 +1,37 @@
#pragma once
#include <QtEvents>
#include <QIcon>
#include <QTableWidget>
#include "EmuApplication.hpp"
class BindingPanel : public QWidget
{
public:
BindingPanel(EmuApplication *app);
~BindingPanel();
void setTableWidget(QTableWidget *bindingTableWidget, EmuBinding *binding, int width, int height);
void cellActivated(int row, int column);
void handleKeyPressEvent(QKeyEvent *event);
void updateCellFromBinding(int row, int column);
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override;
void fillTable();
void checkJoypadInput();
void finalizeCurrentBinding(EmuBinding b);
void setRedirectInput(bool redirect);
void onJoypadsChanged(std::function<void()> func);
bool awaiting_binding;
bool accept_return;
int table_width;
int table_height;
int cell_row;
int cell_column;
QIcon keyboard_icon;
QIcon joypad_icon;
std::unique_ptr<QTimer> timer;
EmuApplication *app;
QTableWidget *binding_table_widget;
EmuBinding *binding;
std::function<void()> joypads_changed;
};

153
qt/src/ControllerPanel.cpp Normal file
View File

@ -0,0 +1,153 @@
#include "ControllerPanel.hpp"
#include "SDLInputManager.hpp"
#include "SDL_gamecontroller.h"
#include <optional>
#include <QtEvents>
#include <QTimer>
ControllerPanel::ControllerPanel(EmuApplication *app)
: BindingPanel(app)
{
setupUi(this);
QObject::connect(controllerComboBox, &QComboBox::currentIndexChanged, [&](int index) {
BindingPanel::binding = this->app->config->binding.controller[index].buttons;
fillTable();
awaiting_binding = false;
});
BindingPanel::setTableWidget(tableWidget_controller,
app->config->binding.controller[0].buttons,
app->config->allowed_bindings,
app->config->num_controller_bindings);
auto action = edit_menu.addAction(QObject::tr("Clear Current Controller"));
action->connect(action, &QAction::triggered, [&](bool checked) {
clearCurrentController();
});
action = edit_menu.addAction(QObject::tr("Clear All Controllers"));
action->connect(action, &QAction::triggered, [&](bool checked) {
clearAllControllers();
});
auto swap_menu = edit_menu.addMenu(QObject::tr("Swap With"));
for (auto i = 0; i < 5; i++)
{
action = swap_menu->addAction(QObject::tr("Controller %1").arg(i + 1));
action->connect(action, &QAction::triggered, [&, i](bool) {
auto current_index = controllerComboBox->currentIndex();
if (current_index == i)
return;
swapControllers(i, current_index);
fillTable();
});
}
editToolButton->setMenu(&edit_menu);
editToolButton->setPopupMode(QToolButton::InstantPopup);
recreateAutoAssignMenu();
onJoypadsChanged([&]{ recreateAutoAssignMenu(); });
}
void ControllerPanel::recreateAutoAssignMenu()
{
auto_assign_menu.clear();
auto controller_list = app->input_manager->getXInputControllers();
for (int i = 0; i < app->config->allowed_bindings; i++)
{
auto slot_menu = auto_assign_menu.addMenu(tr("Slot %1").arg(i));
auto default_keyboard = slot_menu->addAction(tr("Default Keyboard"));
default_keyboard->connect(default_keyboard, &QAction::triggered, [&, slot = i](bool) {
autoPopulateWithKeyboard(slot);
});
for (auto c : controller_list)
{
auto controller_item = slot_menu->addAction(c.second.c_str());
controller_item->connect(controller_item, &QAction::triggered, [&, id = c.first, slot = i](bool) {
autoPopulateWithJoystick(id, slot);
});
}
}
autoAssignToolButton->setMenu(&auto_assign_menu);
autoAssignToolButton->setPopupMode(QToolButton::InstantPopup);
}
void ControllerPanel::autoPopulateWithKeyboard(int slot)
{
auto &buttons = app->config->binding.controller[controllerComboBox->currentIndex()].buttons;
const char *button_list[] = { "Up", "Down", "Left", "Right", "d", "c", "s", "x", "z", "a", "Return", "Space" };
for (int i = 0; i < std::size(button_list); i++)
buttons[app->config->allowed_bindings * i + slot] = EmuBinding::keyboard(QKeySequence::fromString(button_list[i])[0].key());
fillTable();
}
void ControllerPanel::autoPopulateWithJoystick(int joystick_id, int slot)
{
auto &device = app->input_manager->devices[joystick_id];
auto sdl_controller = device.controller;
auto &buttons = app->config->binding.controller[controllerComboBox->currentIndex()].buttons;
const SDL_GameControllerButton list[] = { SDL_CONTROLLER_BUTTON_DPAD_UP,
SDL_CONTROLLER_BUTTON_DPAD_DOWN,
SDL_CONTROLLER_BUTTON_DPAD_LEFT,
SDL_CONTROLLER_BUTTON_DPAD_RIGHT,
// B, A and X, Y are inverted on XInput vs SNES
SDL_CONTROLLER_BUTTON_B,
SDL_CONTROLLER_BUTTON_A,
SDL_CONTROLLER_BUTTON_Y,
SDL_CONTROLLER_BUTTON_X,
SDL_CONTROLLER_BUTTON_LEFTSHOULDER,
SDL_CONTROLLER_BUTTON_RIGHTSHOULDER,
SDL_CONTROLLER_BUTTON_START,
SDL_CONTROLLER_BUTTON_BACK };
for (auto i = 0; i < std::size(list); i++)
{
auto sdl_binding = SDL_GameControllerGetBindForButton(sdl_controller, list[i]);
if (SDL_CONTROLLER_BINDTYPE_BUTTON == sdl_binding.bindType)
buttons[4 * i + slot] = EmuBinding::joystick_button(device.index, sdl_binding.value.button);
else if (SDL_CONTROLLER_BINDTYPE_HAT == sdl_binding.bindType)
buttons[4 * i + slot] = EmuBinding::joystick_hat(device.index, sdl_binding.value.hat.hat, sdl_binding.value.hat.hat_mask);
else if (SDL_CONTROLLER_BINDTYPE_AXIS == sdl_binding.bindType)
buttons[4 * i + slot] = EmuBinding::joystick_axis(device.index, sdl_binding.value.axis, sdl_binding.value.axis);
}
fillTable();
}
void ControllerPanel::swapControllers(int first, int second)
{
auto &a = app->config->binding.controller[first].buttons;
auto &b = app->config->binding.controller[second].buttons;
int count = std::size(a);
for (int i = 0; i < count; i++)
{
EmuBinding swap = b[i];
b[i] = a[i];
a[i] = swap;
}
}
void ControllerPanel::clearCurrentController()
{
auto &c = app->config->binding.controller[controllerComboBox->currentIndex()];
for (auto &b : c.buttons)
b = {};
fillTable();
}
void ControllerPanel::clearAllControllers()
{
for (auto &c : app->config->binding.controller)
for (auto &b : c.buttons)
b = {};
fillTable();
}
ControllerPanel::~ControllerPanel()
{
}

View File

@ -0,0 +1,23 @@
#pragma once
#include "ui_ControllerPanel.h"
#include "BindingPanel.hpp"
#include "EmuApplication.hpp"
#include <QMenu>
class ControllerPanel :
public Ui::ControllerPanel,
public BindingPanel
{
public:
ControllerPanel(EmuApplication *app);
~ControllerPanel();
void clearAllControllers();
void clearCurrentController();
void autoPopulateWithKeyboard(int slot);
void autoPopulateWithJoystick(int joystick_id, int slot);
void swapControllers(int first, int second);
void recreateAutoAssignMenu();
QMenu edit_menu;
QMenu auto_assign_menu;
};

325
qt/src/ControllerPanel.ui Normal file
View File

@ -0,0 +1,325 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ControllerPanel</class>
<widget class="QWidget" name="ControllerPanel">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>674</width>
<height>632</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Set</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="controllerComboBox">
<item>
<property name="text">
<string>SNES Controller 1</string>
</property>
</item>
<item>
<property name="text">
<string>SNES Controller 2</string>
</property>
</item>
<item>
<property name="text">
<string>SNES Controller 3 (Multitap)</string>
</property>
</item>
<item>
<property name="text">
<string>SNES Controller 4 (Multitap)</string>
</property>
</item>
<item>
<property name="text">
<string>SNES Controller 5 (Multitap)</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="editToolButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>90</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Swap or clear groups of bindings</string>
</property>
<property name="text">
<string>Edit</string>
</property>
<property name="popupMode">
<enum>QToolButton::DelayedPopup</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="autoAssignToolButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>90</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Automatically assign a controller's buttons to a slot</string>
</property>
<property name="text">
<string>Auto-Assign</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QTableWidget" name="tableWidget_controller">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<attribute name="verticalHeaderCascadingSectionResizes">
<bool>true</bool>
</attribute>
<row>
<property name="text">
<string>Up</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/up.svg</normaloff>:/icons/blackicons/up.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>Down</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/down.svg</normaloff>:/icons/blackicons/down.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>Left</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/left.svg</normaloff>:/icons/blackicons/left.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>Right</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/right.svg</normaloff>:/icons/blackicons/right.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>A</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/a.svg</normaloff>:/icons/blackicons/a.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>B</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/b.svg</normaloff>:/icons/blackicons/b.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>X</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/x.svg</normaloff>:/icons/blackicons/x.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>Y</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/y.svg</normaloff>:/icons/blackicons/y.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>L</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/l.svg</normaloff>:/icons/blackicons/l.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>R</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/r.svg</normaloff>:/icons/blackicons/r.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>Start</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/start.svg</normaloff>:/icons/blackicons/start.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>Select</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/select.svg</normaloff>:/icons/blackicons/select.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>Turbo A</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/a.svg</normaloff>:/icons/blackicons/a.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>Turbo B</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/b.svg</normaloff>:/icons/blackicons/b.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>Turbo X</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/x.svg</normaloff>:/icons/blackicons/x.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>Turbo Y</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/y.svg</normaloff>:/icons/blackicons/y.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>Turbo L</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/l.svg</normaloff>:/icons/blackicons/l.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>Turbo R</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/r.svg</normaloff>:/icons/blackicons/r.svg</iconset>
</property>
</row>
<column>
<property name="text">
<string>Binding #1</string>
</property>
</column>
<column>
<property name="text">
<string>Binding #2</string>
</property>
</column>
<column>
<property name="text">
<string>Binding #3</string>
</property>
</column>
<column>
<property name="text">
<string>Binding #4</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="resources/snes9x.qrc"/>
</resources>
<connections/>
</ui>

181
qt/src/DisplayPanel.cpp Normal file
View File

@ -0,0 +1,181 @@
#include "DisplayPanel.hpp"
#include <QFileDialog>
DisplayPanel::DisplayPanel(EmuApplication *app_)
: app(app_)
{
setupUi(this);
QObject::connect(comboBox_driver, &QComboBox::activated, [&](int index) {
if (driver_list.empty() || index < 0 || index >= driver_list.size())
return;
auto display_driver = driver_list[index].second;
if (display_driver != app->config->display_driver)
{
app->config->display_driver = display_driver;
app->window->recreateCanvas();
populateDevices();
}
});
QObject::connect(comboBox_device, &QComboBox::activated, [&](int index) {
if (app->config->display_device_index != index)
{
app->config->display_device_index = index;
app->window->recreateCanvas();
}
});
QObject::connect(checkBox_use_shader, &QCheckBox::clicked, [&](bool checked) {
app->config->use_shader = checked;
app->window->canvas->shaderChanged();
});
QObject::connect(pushButton_browse_shader, &QPushButton::clicked, [&] {
selectShaderDialog();
});
QObject::connect(checkBox_vsync, &QCheckBox::clicked, [&](bool checked) {
app->config->enable_vsync = checked;
});
QObject::connect(checkBox_reduce_input_lag, &QCheckBox::clicked, [&](bool checked) {
app->config->reduce_input_lag = checked;
});
QObject::connect(checkBox_bilinear_filter, &QCheckBox::clicked, [&](bool checked) {
app->config->bilinear_filter = checked;
});
QObject::connect(checkBox_adjust_for_vrr, &QCheckBox::clicked, [&](bool checked) {
app->config->adjust_for_vrr = checked;
});
//
QObject::connect(checkBox_maintain_aspect_ratio, &QCheckBox::clicked, [&](bool checked) {
app->config->maintain_aspect_ratio = checked;
});
QObject::connect(checkBox_integer_scaling, &QCheckBox::clicked, [&](bool checked) {
app->config->use_integer_scaling = checked;
});
QObject::connect(checkBox_overscan, &QCheckBox::clicked, [&](bool checked) {
app->config->show_overscan = checked;
app->updateSettings();
});
QObject::connect(comboBox_aspect_ratio, &QComboBox::activated, [&](int index) {
auto &num = app->config->aspect_ratio_numerator;
auto &den = app->config->aspect_ratio_denominator;
if (index == 0) { num = 4, den = 3; }
if (index == 1) { num = 64, den = 49; }
if (index == 2) { num = 8, den = 7; }
});
QObject::connect(comboBox_high_resolution_mode, &QComboBox::currentIndexChanged, [&](int index) {
app->config->high_resolution_effect = index;
app->updateSettings();
});
QObject::connect(comboBox_messages, &QComboBox::currentIndexChanged, [&](int index) {
bool restart = (app->config->display_messages == EmuConfig::eOnscreen || index == EmuConfig::eOnscreen);
app->config->display_messages = index;
app->updateSettings();
if (restart)
app->window->recreateCanvas();
});
QObject::connect(spinBox_osd_size, &QSpinBox::valueChanged, [&](int value) {
bool restart = (app->config->osd_size != value && app->config->display_messages == EmuConfig::eOnscreen);
app->config->osd_size = value;
if (restart)
app->window->recreateCanvas();
});
}
DisplayPanel::~DisplayPanel()
{
}
void DisplayPanel::selectShaderDialog()
{
QFileDialog dialog(this, tr("Select a Folder"));
dialog.setFileMode(QFileDialog::ExistingFile);
dialog.setNameFilter(tr("Shader Presets (*.slangp *.glslp)"));
if (!app->config->last_shader_folder.empty())
dialog.setDirectory(app->config->last_shader_folder.c_str());
if (!dialog.exec())
return;
app->config->shader = dialog.selectedFiles().at(0).toUtf8();
app->config->last_shader_folder = dialog.directory().absolutePath().toStdString();
lineEdit_shader->setText(app->config->shader.c_str());
app->window->canvas->shaderChanged();
}
void DisplayPanel::populateDevices()
{
comboBox_device->clear();
auto device_list = app->window->getDisplayDeviceList();
for (auto &d : device_list)
comboBox_device->addItem(d.c_str());
comboBox_device->setCurrentIndex(app->config->display_device_index);
}
void DisplayPanel::showEvent(QShowEvent *event)
{
auto &config = app->config;
comboBox_driver->clear();
comboBox_driver->addItem("Qt Software");
comboBox_driver->addItem("OpenGL");
comboBox_driver->addItem("Vulkan");
driver_list.clear();
driver_list.push_back({ driver_list.size(), "qt" });
driver_list.push_back({ driver_list.size(), "opengl" });
driver_list.push_back({ driver_list.size(), "vulkan" });
for (auto &i : driver_list)
if (config->display_driver == i.second)
{
comboBox_driver->setCurrentIndex(i.first);
break;
}
populateDevices();
checkBox_use_shader->setChecked(config->use_shader);
lineEdit_shader->setText(config->shader.c_str());
checkBox_vsync->setChecked(config->enable_vsync);
checkBox_reduce_input_lag->setChecked(config->reduce_input_lag);
checkBox_bilinear_filter->setChecked(config->bilinear_filter);
checkBox_adjust_for_vrr->setChecked(config->adjust_for_vrr);
checkBox_maintain_aspect_ratio->setChecked(config->maintain_aspect_ratio);
checkBox_integer_scaling->setChecked(config->use_integer_scaling);
checkBox_overscan->setChecked(config->show_overscan);
if (config->aspect_ratio_numerator == 4)
comboBox_aspect_ratio->setCurrentIndex(0);
else if (config->aspect_ratio_numerator == 64)
comboBox_aspect_ratio->setCurrentIndex(1);
else if (config->aspect_ratio_numerator == 8)
comboBox_aspect_ratio->setCurrentIndex(2);
comboBox_high_resolution_mode->setCurrentIndex(config->high_resolution_effect);
comboBox_messages->setCurrentIndex(config->display_messages);
spinBox_osd_size->setValue(config->osd_size);
QWidget::showEvent(event);
}

20
qt/src/DisplayPanel.hpp Normal file
View File

@ -0,0 +1,20 @@
#pragma once
#include "ui_DisplayPanel.h"
#include "EmuApplication.hpp"
class DisplayPanel :
public Ui::DisplayPanel,
public QWidget
{
public:
DisplayPanel(EmuApplication *app);
~DisplayPanel();
void showEvent(QShowEvent *event) override;
void populateDevices();
void selectShaderDialog();
std::vector<std::pair<int, std::string>> driver_list;
bool updating = true;
EmuApplication *app;
};

389
qt/src/DisplayPanel.ui Normal file
View File

@ -0,0 +1,389 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DisplayPanel</class>
<widget class="QWidget" name="DisplayPanel">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>669</width>
<height>654</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Display Output</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QGridLayout" name="gridLayout_driver">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Driver:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBox_device">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Choose a device to render output. If you have no integrated graphics, there will be only one choice.</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox_driver">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Select the output driver.</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Device:</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_3">
<item row="2" column="0">
<widget class="QCheckBox" name="checkBox_bilinear_filter">
<property name="toolTip">
<string>Smooth screen output.</string>
</property>
<property name="text">
<string>Bilinear filter</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="checkBox_reduce_input_lag">
<property name="toolTip">
<string>Prevent the display driver from getting too far ahead in order to reduce lag.</string>
</property>
<property name="text">
<string>Reduce input lag</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="checkBox_adjust_for_vrr">
<property name="toolTip">
<string>When entering fullscreen mode, temporarily change other settings to use VRR (G-Sync or FreeSync) correctly.</string>
</property>
<property name="text">
<string>Adjust settings for VRR in fullscreen mode</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="checkBox_vsync">
<property name="toolTip">
<string>Sync the display to vertical retrace to eliminate tearing.</string>
</property>
<property name="text">
<string>Enable vsync</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="checkBox_use_shader">
<property name="toolTip">
<string>Use a selected .slangp or .glslp shader file.
.slangp is supported by Vulkan and OpenGL.
.glslp is supported by OpenGL.</string>
</property>
<property name="text">
<string>Use a hardware shader:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_shader"/>
</item>
<item>
<widget class="QPushButton" name="pushButton_browse_shader">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Scaling and Aspect Ratio</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<widget class="QCheckBox" name="checkBox_maintain_aspect_ratio">
<property name="toolTip">
<string>Keep the screen at the requested proportions for width and height.</string>
</property>
<property name="text">
<string>Maintain aspect ratio</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="checkBox_integer_scaling">
<property name="toolTip">
<string>When scaling up, only use integer multiples of the original height.</string>
</property>
<property name="text">
<string>Use integer scaling</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="checkBox_overscan">
<property name="toolTip">
<string>Show the areas on the top and bottom of the screen that are normally black and hidden by the TV. Some games will draw in these areas.</string>
</property>
<property name="text">
<string>Show overscan area</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>High-resolution mode:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox_aspect_ratio">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>4:3 - Classic display aspect</string>
</property>
</item>
<item>
<property name="text">
<string>64:49 - NTSC aspect</string>
</property>
</item>
<item>
<property name="text">
<string>8:7 - Square pixels</string>
</property>
</item>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Aspect ratio:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBox_high_resolution_mode">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>This affects the rarely used 512-pixels-wide mode used by the SNES.
For games like Kirby 3 and Jurassic Park, choose the &quot;merge fields&quot; option.
For games like Seiken Densetsu 3 and Marvelous, choose the &quot;scale up&quot; option.
Output directly will cause the screen to change between the two modes and look weird.</string>
</property>
<item>
<property name="text">
<string>Output directly</string>
</property>
</item>
<item>
<property name="text">
<string>Merge the fields of the high-resolution lines</string>
</property>
</item>
<item>
<property name="text">
<string>Scale normal resolution screens up</string>
</property>
</item>
</widget>
</item>
<item row="0" column="2">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Software Filters</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QComboBox" name="comboBox"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Messages</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Display messages:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox_messages">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>Onscreen - High resolution</string>
</property>
</item>
<item>
<property name="text">
<string>Inside the screen - Low resolution</string>
</property>
</item>
<item>
<property name="text">
<string>Don't display messages</string>
</property>
</item>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="spinBox_osd_size">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="suffix">
<string>pt</string>
</property>
<property name="minimum">
<number>8</number>
</property>
<property name="maximum">
<number>256</number>
</property>
<property name="value">
<number>24</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Onscreen display size:</string>
</property>
</widget>
</item>
<item row="0" column="2">
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

352
qt/src/EmuApplication.cpp Normal file
View File

@ -0,0 +1,352 @@
#include "EmuApplication.hpp"
#include "common/audio/s9x_sound_driver_sdl.hpp"
#include "common/audio/s9x_sound_driver_portaudio.hpp"
#include "common/audio/s9x_sound_driver_cubeb.hpp"
#include <QTimer>
#include <chrono>
#include <thread>
using namespace std::chrono_literals;
EmuApplication::EmuApplication()
{
core = Snes9xController::get();
}
EmuApplication::~EmuApplication()
{
core->deinit();
}
void EmuApplication::restartAudio()
{
sound_driver.reset();
core->sound_output_function = nullptr;
if (config->sound_driver == "portaudio")
sound_driver = std::make_unique<S9xPortAudioSoundDriver>();
else if (config->sound_driver == "cubeb")
sound_driver = std::make_unique<S9xCubebSoundDriver>();
else
{
config->sound_driver = "sdl";
sound_driver = std::make_unique<S9xSDLSoundDriver>();
}
sound_driver->init();
if (sound_driver->open_device(config->playback_rate, config->audio_buffer_size_ms))
sound_driver->start();
else
{
printf("Couldn't initialize sound driver: %s\n", config->sound_driver.c_str());
sound_driver.reset();
}
if (sound_driver)
core->sound_output_function = [&](int16_t *data, int samples) {
writeSamples(data, samples);
};
}
void EmuApplication::writeSamples(int16_t *data, int samples)
{
if (config->speed_sync_method == EmuConfig::eSoundSync && !core->isAbnormalSpeed())
{
int iterations = 0;
while (sound_driver->space_free() < samples && iterations < 100)
{
iterations++;
std::this_thread::sleep_for(50us);
}
}
sound_driver->write_samples(data, samples);
auto buffer_level = sound_driver->buffer_level();
core->updateSoundBufferLevel(buffer_level.first, buffer_level.second);
}
void EmuApplication::startGame()
{
if (!sound_driver)
restartAudio();
core->screen_output_function = [&](uint16_t *data, int width, int height, int stride_bytes, double frame_rate) {
if (window->canvas)
window->canvas->output((uint8_t *)data, width, height, QImage::Format_RGB16, stride_bytes, frame_rate);
};
core->updateSettings(config.get());
updateBindings();
startIdleLoop();
}
bool EmuApplication::isPaused()
{
return (pause_count != 0);
}
void EmuApplication::pause()
{
pause_count++;
if (pause_count > 0)
{
core->setPaused(true);
if (sound_driver)
sound_driver->stop();
}
}
void EmuApplication::stopIdleLoop()
{
idle_loop->stop();
pause_count = 0;
}
void EmuApplication::unpause()
{
pause_count--;
if (pause_count < 0)
pause_count = 0;
if (pause_count > 0)
return;
core->setPaused(false);
if (core->active && sound_driver)
sound_driver->start();
}
void EmuApplication::startIdleLoop()
{
if (!idle_loop)
{
idle_loop = std::make_unique<QTimer>();
idle_loop->setTimerType(Qt::TimerType::PreciseTimer);
idle_loop->setInterval(0);
idle_loop->setSingleShot(false);
idle_loop->callOnTimeout([&]{ idleLoop(); });
pause_count = 0;
}
idle_loop->start();
}
void EmuApplication::idleLoop()
{
if (core->active && pause_count == 0)
{
idle_loop->setInterval(0);
pollJoysticks();
bool muted = config->mute_audio || (config->mute_audio_during_alternate_speed && core->isAbnormalSpeed());
core->mute(muted);
core->mainLoop();
}
else
{
pollJoysticks();
idle_loop->setInterval(32);
}
}
bool EmuApplication::openFile(std::string filename)
{
auto result = core->openFile(filename);
return result;
}
void EmuApplication::reportBinding(EmuBinding b, bool active)
{
if (binding_callback && active)
{
binding_callback(b);
return;
}
auto it = bindings.find(b.hash());
if (it == bindings.end())
return;
if (it->second.second == UI)
{
handleBinding(it->second.first, active);
return;
}
core->reportBinding(b, active);
}
void EmuApplication::updateBindings()
{
bindings.clear();
for (auto i = 0; i < EmuConfig::num_shortcuts; i++)
{
auto name = EmuConfig::getShortcutNames()[i];
for (auto b = 0; b < EmuConfig::allowed_bindings; b++)
{
auto &binding = config->binding.shortcuts[i * EmuConfig::allowed_bindings + b];
if (binding.type != EmuBinding::None)
{
auto handler = core->acceptsCommand(name) ? Core : UI;
bindings.insert({ binding.hash(), { name, handler } });
}
}
}
for (int i = 0; i < EmuConfig::num_controller_bindings; i++)
{
for (int c = 0; c < 5; c++)
{
for (int b = 0; b < EmuConfig::allowed_bindings; b++)
{
auto binding = config->binding.controller[c].buttons[i * EmuConfig::allowed_bindings + b];
if (binding.hash() != 0)
bindings.insert({ binding.hash(), { "Snes9x", Core } });
}
}
}
core->updateBindings(config.get());
}
void EmuApplication::handleBinding(std::string name, bool pressed)
{
if (core->active)
{
if (name == "Rewind")
{
core->rewinding = pressed;
}
else if (pressed) // Only activate with core active and on button down
{
if (name == "PauseContinue")
{
window->pauseContinue();
}
else if (name == "IncreaseSlot")
{
save_slot++;
if (save_slot > 999)
save_slot = 0;
core->setMessage("Current slot: " + std::to_string(save_slot));
}
else if (name == "DecreaseSlot")
{
save_slot--;
if (save_slot < 0)
save_slot = 999;
core->setMessage("Current slot: " + std::to_string(save_slot));
}
else if (name == "SaveState")
{
saveState(save_slot);
}
else if (name == "LoadState")
{
loadState(save_slot);
}
}
}
if (name == "ToggleFullscreen" && !pressed)
{
window->toggleFullscreen();
}
else if (name == "OpenROM" && pressed)
{
window->openFile();
}
}
bool EmuApplication::isBound(EmuBinding b)
{
if (bindings.find(b.hash()) != bindings.end())
return true;
return false;
}
void EmuApplication::updateSettings()
{
core->updateSettings(config.get());
}
void EmuApplication::pollJoysticks()
{
while (1)
{
auto event = input_manager->ProcessEvent();
if (!event)
return;
switch (event->type)
{
case SDL_JOYDEVICEADDED:
case SDL_JOYDEVICEREMOVED:
if (joypads_changed_callback)
joypads_changed_callback();
break;
case SDL_JOYAXISMOTION: {
auto axis_event = input_manager->DiscretizeJoyAxisEvent(event.value());
if (axis_event)
{
auto binding = EmuBinding::joystick_axis(
axis_event->joystick_num,
axis_event->axis,
axis_event->direction);
reportBinding(binding, axis_event->pressed);
}
break;
}
case SDL_JOYBUTTONDOWN:
case SDL_JOYBUTTONUP:
reportBinding(EmuBinding::joystick_button(
input_manager->devices[event->jbutton.which].index,
event->jbutton.button), event->jbutton.state == 1);
break;
case SDL_JOYHATMOTION:
auto hat_event = input_manager->DiscretizeHatEvent(event.value());
if (hat_event)
{
reportBinding(EmuBinding::joystick_hat(hat_event->joystick_num,
hat_event->hat,
hat_event->direction),
hat_event->pressed);
}
break;
}
}
}
void EmuApplication::loadState(int slot)
{
core->loadState(slot);
}
void EmuApplication::loadState(std::string filename)
{
core->loadState(filename);
}
void EmuApplication::saveState(int slot)
{
core->saveState(slot);
}
void EmuApplication::saveState(std::string filename)
{
core->saveState(filename);
}
void EmuApplication::reset()
{
core->softReset();
}
void EmuApplication::powerCycle()
{
core->reset();
}

57
qt/src/EmuApplication.hpp Normal file
View File

@ -0,0 +1,57 @@
#pragma once
#include <QApplication>
#include <QTimer>
#include "EmuMainWindow.hpp"
#include "EmuConfig.hpp"
#include "SDLInputManager.hpp"
#include "Snes9xController.hpp"
#include "common/audio/s9x_sound_driver.hpp"
struct EmuApplication
{
std::unique_ptr<QApplication> qtapp;
std::unique_ptr<EmuConfig> config;
std::unique_ptr<SDLInputManager> input_manager;
std::unique_ptr<EmuMainWindow> window;
std::unique_ptr<S9xSoundDriver> sound_driver;
Snes9xController *core;
EmuApplication();
~EmuApplication();
bool openFile(std::string filename);
void handleBinding(std::string name, bool pressed);
void updateSettings();
void updateBindings();
bool isBound(EmuBinding b);
void reportBinding(EmuBinding b, bool active);
void pollJoysticks();
void restartAudio();
void writeSamples(int16_t *data, int samples);
void pause();
void reset();
void powerCycle();
bool isPaused();
void unpause();
void loadState(int slot);
void loadState(std::string filename);
void saveState(int slot);
void saveState(std::string filename);
void startGame();
void startIdleLoop();
void stopIdleLoop();
void idleLoop();
enum Handler
{
Core = 0,
UI = 1
};
std::map<uint32_t, std::pair<std::string, Handler>> bindings;
std::unique_ptr<QTimer> idle_loop;
std::function<void(EmuBinding)> binding_callback = nullptr;
std::function<void()> joypads_changed_callback = nullptr;
int save_slot = 0;
int pause_count = 0;
};

226
qt/src/EmuBinding.cpp Normal file
View File

@ -0,0 +1,226 @@
#include "EmuBinding.hpp"
#include "SDL_joystick.h"
#include <QString>
#include <QKeySequence>
#include <sstream>
// Hash format:
//
// Bit 31-30: Joystick or Keyboard bit
//
// Keyboard:
// Bit 30: Alt
// Bit 29: Ctrl
// Bit 28: Super
// Bit 27: Shift
// Bits 15-0: keycode
//
// Joystick:
// Bits 29-28: Type:
// 00 Button
// 01 Axis
// 10 Hat
// Bit 27: If axis or hat, positive or negative
// Bits 26-19: Which button/hat/axis
// Bits 15-8: Hat direction
// Bits 7-0: Device identifier
uint32_t EmuBinding::hash() const
{
uint32_t hash = 0;
hash |= type << 30;
if (type == Keyboard)
{
hash |= alt << 29;
hash |= ctrl << 28;
hash |= super << 27;
hash |= shift << 26;
hash |= keycode & 0xfffffff;
}
else
{
hash |= (input_type & 0x3) << 28;
hash |= (threshold < 0 ? 1 : 0) << 27;
hash |= (button & 0xff) << 19;
hash |= (input_type == Hat) ? direction << 8 : 0;
hash |= (guid & 0xff);
}
return hash;
}
bool EmuBinding::operator==(const EmuBinding &other)
{
return other.hash() == hash();
}
EmuBinding EmuBinding::joystick_axis(int device, int axis, int threshold)
{
EmuBinding binding;
binding.type = Joystick;
binding.input_type = Axis;
binding.guid = device;
binding.axis = axis;
binding.threshold = threshold;
return binding;
}
EmuBinding EmuBinding::joystick_hat(int device, int hat, uint8_t direction)
{
EmuBinding binding{};
binding.type = Joystick;
binding.input_type = Hat;
binding.guid = device;
binding.hat = hat;
binding.direction = direction;
return binding;
}
EmuBinding EmuBinding::joystick_button(int device, int button)
{
EmuBinding binding{};
binding.type = Joystick;
binding.input_type = Button;
binding.guid = device;
binding.button = button;
return binding;
}
EmuBinding EmuBinding::keyboard(int keycode, bool shift, bool alt, bool ctrl, bool super)
{
EmuBinding binding{};
binding.type = Keyboard;
binding.alt = alt;
binding.ctrl = ctrl;
binding.shift = shift;
binding.super = super;
binding.keycode = keycode;
return binding;
}
EmuBinding EmuBinding::from_config_string(std::string string)
{
for (auto &c : string)
if (c >= 'A' && c <= 'Z')
c += 32;
if (string.compare(0, 9, "keyboard ") == 0)
{
EmuBinding b{};
b.type = Keyboard;
QString qstr(string.substr(9).c_str());
auto seq = QKeySequence::fromString(qstr);
if (seq.count())
{
b.keycode = seq[0].key();
b.alt = seq[0].keyboardModifiers().testAnyFlag(Qt::AltModifier);
b.ctrl = seq[0].keyboardModifiers().testAnyFlag(Qt::ControlModifier);
b.super = seq[0].keyboardModifiers().testAnyFlag(Qt::MetaModifier);
b.shift = seq[0].keyboardModifiers().testAnyFlag(Qt::ShiftModifier);
}
return b;
}
else if (string.compare(0, 8, "joystick") == 0)
{
auto substr = string.substr(8);
unsigned int axis;
unsigned int button;
unsigned int percent;
unsigned int device;
char direction_string[6]{};
char posneg;
if (sscanf(substr.c_str(), "%u axis %u %c %u", &device, &axis, &posneg, &percent) == 4)
{
int sign = posneg == '-' ? -1 : 1;
return joystick_axis(device, axis, sign * percent);
}
else if (sscanf(substr.c_str(), "%u button %u", &device, &button) == 2)
{
return joystick_button(device, button);
}
else if (sscanf(substr.c_str(), "%u hat %u %5s", &device, &axis, direction_string))
{
uint8_t direction;
if (!strcmp(direction_string, "up"))
direction = SDL_HAT_UP;
else if (!strcmp(direction_string, "down"))
direction = SDL_HAT_DOWN;
else if (!strcmp(direction_string, "left"))
direction = SDL_HAT_LEFT;
else if (!strcmp(direction_string, "right"))
direction = SDL_HAT_RIGHT;
return joystick_hat(device, axis, direction);
}
}
return {};
}
std::string EmuBinding::to_config_string()
{
return to_string(true);
}
std::string EmuBinding::to_string(bool config)
{
std::string rep;
if (type == Keyboard)
{
if (config)
rep += "Keyboard ";
if (ctrl)
rep += "Ctrl+";
if (alt)
rep += "Alt+";
if (shift)
rep += "Shift+";
if (super)
rep += "Super+";
QKeySequence seq(keycode);
rep += seq.toString().toStdString();
}
else if (type == Joystick)
{
if (config)
rep += "joystick " + std::to_string(guid) + " ";
else
rep += "J" + std::to_string(guid) + " ";
if (input_type == Button)
{
rep += "Button ";
rep += std::to_string(button);
}
if (input_type == Axis)
{
rep += "Axis ";
rep += std::to_string(axis) + " ";
rep += std::to_string(threshold) + "%";
}
if (input_type == Hat)
{
rep += "Hat ";
rep += std::to_string(hat) + " ";
if (direction == SDL_HAT_UP)
rep += "Up";
else if (direction == SDL_HAT_DOWN)
rep += "Down";
else if (direction == SDL_HAT_LEFT)
rep += "Left";
else if (direction == SDL_HAT_RIGHT)
rep += "Right";
}
}
else
{
rep = "None";
}
return rep;
}

60
qt/src/EmuBinding.hpp Normal file
View File

@ -0,0 +1,60 @@
#ifndef __EMU_BINDING_HPP
#define __EMU_BINDING_HPP
#include <cstdint>
#include <string>
#include <vector>
struct EmuBinding
{
uint32_t hash() const;
std::string to_string(bool config = false);
static EmuBinding joystick_axis(int device, int axis, int threshold);
static EmuBinding joystick_hat(int device, int hat, uint8_t direction);
static EmuBinding joystick_button(int device, int button);
static EmuBinding keyboard(int keycode, bool shift = false, bool alt = false, bool ctrl = false, bool super = false);
static EmuBinding from_config_string(std::string str);
std::string to_config_string();
bool operator==(const EmuBinding &);
enum Type
{
None = 0,
Keyboard = 1,
Joystick = 2
};
Type type;
enum JoystickInputType
{
Button = 0,
Axis = 1,
Hat = 2
};
union
{
struct
{
bool alt;
bool ctrl;
bool super;
bool shift;
int keycode;
};
struct
{
JoystickInputType input_type;
int guid;
union {
int button;
int hat;
int axis;
};
int threshold;
uint8_t direction;
};
};
};
#endif

107
qt/src/EmuCanvas.cpp Normal file
View File

@ -0,0 +1,107 @@
#include "EmuCanvas.hpp"
#include <qnamespace.h>
#include <qwidget.h>
EmuCanvas::EmuCanvas(EmuConfig *config, QWidget *parent, QWidget *main_window)
: QWidget(parent)
{
setFocus();
setFocusPolicy(Qt::StrongFocus);
setMouseTracking(true);
output_data.buffer = nullptr;
output_data.ready = false;
this->config = config;
this->parent = parent;
this->main_window = main_window;
}
EmuCanvas::~EmuCanvas()
{
}
void EmuCanvas::output(uint8_t *buffer, int width, int height, QImage::Format format, int bytes_per_line, double frame_rate)
{
output_data.buffer = buffer;
output_data.width = width;
output_data.height = height;
output_data.format = format;
output_data.bytes_per_line = bytes_per_line;
output_data.frame_rate = frame_rate;
output_data.ready = true;
draw();
}
void EmuCanvas::throttle()
{
if (config->speed_sync_method != EmuConfig::eTimer && config->speed_sync_method != EmuConfig::eTimerWithFrameskip)
return;
throttle_object.set_frame_rate(config->fixed_frame_rate == 0.0 ? output_data.frame_rate : config->fixed_frame_rate);
throttle_object.wait_for_frame_and_rebase_time();
}
QRect EmuCanvas::applyAspect(const QRect &viewport)
{
if (!config->scale_image)
{
return QRect((viewport.width() - output_data.width) / 2,
(viewport.height() - output_data.height) / 2,
output_data.width,
output_data.height);
}
if (!config->maintain_aspect_ratio)
return viewport;
int num = config->aspect_ratio_numerator;
int den = config->aspect_ratio_denominator;
if (config->show_overscan)
{
num *= 224;
den *= 239;
}
if (config->use_integer_scaling)
{
int max_scale = 1;
for (int i = 2; i < 20; i++)
{
int scaled_height = output_data.height * i;
int scaled_width = scaled_height * num / den;
if (scaled_width <= viewport.width() && scaled_height <= viewport.height())
max_scale = i;
else
break;
}
int new_height = output_data.height * max_scale;
int new_width = new_height * num / den;
return QRect((viewport.width() - new_width) / 2,
(viewport.height() - new_height) / 2,
new_width,
new_height);
}
double canvas_aspect = (double)viewport.width() / viewport.height();
double new_aspect = (double)num / den;
if (canvas_aspect > new_aspect)
{
int new_width = viewport.height() * num / den;
int new_x = (viewport.width() - new_width) / 2;
return { new_x,
viewport.y(),
new_width,
viewport.height() };
}
int new_height = viewport.width() * den / num;
int new_y = (viewport.height() - new_height) / 2;
return { viewport.x(),
new_y,
viewport.width(),
new_height };
}

73
qt/src/EmuCanvas.hpp Normal file
View File

@ -0,0 +1,73 @@
#pragma once
#include <QWidget>
#include <QImage>
#include "EmuConfig.hpp"
#include "../../vulkan/std_chrono_throttle.hpp"
class EmuCanvas : public QWidget
{
public:
EmuCanvas(EmuConfig *config, QWidget *parent, QWidget *main_window);
~EmuCanvas();
virtual void deinit() = 0;
virtual void draw() = 0;
void output(uint8_t *buffer, int width, int height, QImage::Format format, int bytes_per_line, double frame_rate);
void throttle();
virtual std::vector<std::string> getDeviceList()
{
return std::vector<std::string>{ "Default" };
}
bool ready()
{
return output_data.ready;
}
QRect applyAspect(const QRect &viewport);
struct Parameter
{
bool operator==(const Parameter &other)
{
if (name == other.name &&
id == other.id &&
min == other.min &&
max == other.max &&
val == other.val &&
step == other.step &&
significant_digits == other.significant_digits)
return true;
return false;
};
std::string name;
std::string id;
float min;
float max;
float val;
float step;
int significant_digits;
};
virtual void showParametersDialog() {};
virtual void shaderChanged() {};
virtual void saveParameters(std::string filename) {};
struct
{
bool ready;
uint8_t *buffer;
int width;
int height;
QImage::Format format;
int bytes_per_line;
double frame_rate;
} output_data;
QWidget *parent{};
QWidget *main_window{};
EmuConfig *config{};
Throttle throttle_object;
};

103
qt/src/EmuCanvasGLX.cpp Normal file
View File

@ -0,0 +1,103 @@
#include "EmuCanvas.hpp"
EmuCanvas::EmuCanvas()
{
setAttribute(Qt::WA_NoSystemBackground, true);
setAttribute(Qt::WA_OpaquePaintEvent, true);
}
EmuCanvas::~EmuCanvas()
{
}
struct drawing_area : QWidget
{
public:
Window xid = 0;
bool ready = false;
XSetWindowAttributes xattr;
Visual *visual;
unsigned int xflags = 0;
QWindow *wrapper_window = nullptr;
QWidget *wrapper = nullptr;
drawing_area()
{
}
~drawing_area()
{
}
void recreateWindow()
{
if (xid)
{
XUnmapWindow(QX11Info::display(), xid);
XDestroyWindow(QX11Info::display(), xid);
xid = 0;
}
XSetErrorHandler([](Display *dpy, XErrorEvent *event) -> int{
char text[4096];
XGetErrorText(QX11Info::display(), event->error_code, text, 4096);
printf("%s\n", text);
return 0;
});
createWinId();
int xwidth = width() * devicePixelRatio();
int xheight = height() * devicePixelRatio();
printf ("%d %d to %d %d %f\n", width(), height(), xwidth, xheight, devicePixelRatioFScale());
memset(&xattr, 0, sizeof(XSetWindowAttributes));
xattr.background_pixel = 0;
xattr.backing_store = 0;
xattr.event_mask = ExposureMask;
xattr.border_pixel = 0;
xflags = CWWidth | CWHeight | CWEventMask | CWBackPixel | CWBorderPixel | CWBackingStore;
xid = XCreateWindow(QX11Info::display(), winId(), 0, 0, xwidth, xheight, 0, CopyFromParent, InputOutput, CopyFromParent, xflags, &xattr);
XMapWindow(QX11Info::display(), xid);
/*wrapper_window = QWindow::fromWinId((WId)xid);
wrapper = QWidget::createWindowContainer(wrapper_window, this); */
}
void paintEvent(QPaintEvent *event) override
{
return;
auto id = winId();
XGCValues gcvalues {};
gcvalues.background = 0x00ff0000;
gcvalues.foreground = 0x00ff0000;
/*
QPainter paint(this);
QImage image((const uchar *)snes9x->GetScreen(), 256, 224, 1024, QImage::Format_RGB16);
paint.drawImage(0, 0, image, 0, 0, 256, 224);
paint.drawImage(QRect(0, 0, width(), height()), image, QRect(0, 0, 256, 224));
paint.end();
ready = false; */
}
void draw()
{
ready = true;
update();
}
void resizeEvent(QResizeEvent *event) override
{
recreateWindow();
}
};

378
qt/src/EmuCanvasOpenGL.cpp Normal file
View File

@ -0,0 +1,378 @@
#include "EmuCanvasOpenGL.hpp"
#include <QtGui/QGuiApplication>
#include <qpa/qplatformnativeinterface.h>
#include <QTimer>
#include <QMessageBox>
#include "common/video/opengl_context.hpp"
#ifndef _WIN32
#include "common/video/glx_context.hpp"
#include "common/video/wayland_egl_context.hpp"
using namespace QNativeInterface;
#include <X11/Xlib.h>
#else
#include "common/video/wgl_context.hpp"
#endif
#include "shaders/glsl.h"
#include "EmuMainWindow.hpp"
#include "snes9x_imgui.h"
#include "imgui_impl_opengl3.h"
static const char *stock_vertex_shader_140 = R"(
#version 140
in vec2 in_position;
in vec2 in_texcoord;
out vec2 texcoord;
void main()
{
gl_Position = vec4(in_position, 0.0, 1.0);
texcoord = in_texcoord;
}
)";
static const char *stock_fragment_shader_140 = R"(
#version 140
uniform sampler2D texmap;
out vec4 fragcolor;
in vec2 texcoord;
void main()
{
fragcolor = texture(texmap, texcoord);
}
)";
EmuCanvasOpenGL::EmuCanvasOpenGL(EmuConfig *config, QWidget *parent, QWidget *main_window)
: EmuCanvas(config, parent, main_window)
{
setMinimumSize(256, 224);
setUpdatesEnabled(false);
setAutoFillBackground(false);
setAttribute(Qt::WA_NoSystemBackground, true);
setAttribute(Qt::WA_NativeWindow, true);
setAttribute(Qt::WA_PaintOnScreen, true);
setAttribute(Qt::WA_OpaquePaintEvent);
createWinId();
auto timer = new QTimer(this);
timer->setSingleShot(true);
timer->callOnTimeout([&]{ createContext(); });
timer->start();
}
EmuCanvasOpenGL::~EmuCanvasOpenGL()
{
}
void EmuCanvasOpenGL::createStockShaders()
{
stock_program = glCreateProgram();
GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER);
GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(vertex_shader, 1, &stock_vertex_shader_140, NULL);
glShaderSource(fragment_shader, 1, &stock_fragment_shader_140, NULL);
glCompileShader(vertex_shader);
glAttachShader(stock_program, vertex_shader);
glCompileShader(fragment_shader);
glAttachShader(stock_program, fragment_shader);
glBindAttribLocation(stock_program, 0, "in_position");
glBindAttribLocation(stock_program, 1, "in_texcoord");
glLinkProgram(stock_program);
glUseProgram(stock_program);
glDeleteShader(vertex_shader);
glDeleteShader(fragment_shader);
}
void EmuCanvasOpenGL::stockShaderDraw()
{
auto viewport = applyAspect(QRect(0, 0, width() * devicePixelRatio(), height() * devicePixelRatio()));
glViewport(viewport.x(), viewport.y(), viewport.width(), viewport.height());
GLint texture_uniform = glGetUniformLocation(stock_program, "texmap");
glUniform1i(texture_uniform, 0);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
static QRect g_viewport;
static void S9xViewportCallback(int src_width, int src_height,
int viewport_x, int viewport_y,
int viewport_width, int viewport_height,
int *out_x, int *out_y,
int *out_width, int *out_height)
{
*out_x = g_viewport.x();
*out_y = g_viewport.y();
*out_width = g_viewport.width();
*out_height = g_viewport.height();
}
void EmuCanvasOpenGL::customShaderDraw()
{
auto viewport = applyAspect(QRect(0, 0, width() * devicePixelRatio(), height() * devicePixelRatio()));
glViewport(viewport.x(), viewport.y(), viewport.width(), viewport.height());
g_viewport = viewport;
shader->render(texture, output_data.width, output_data.height, viewport.x(), viewport.y(), viewport.width(), viewport.height(), S9xViewportCallback);
}
void EmuCanvasOpenGL::createContext()
{
if (context)
return;
auto platform = QGuiApplication::platformName();
auto pni = QGuiApplication::platformNativeInterface();
QGuiApplication::sync();
#ifndef _WIN32
if (platform == "wayland")
{
auto display = (wl_display *)pni->nativeResourceForWindow("display", windowHandle());
auto surface = (wl_surface *)pni->nativeResourceForWindow("surface", main_window->windowHandle());
auto wayland_egl_context = new WaylandEGLContext();
int s = devicePixelRatio();
if (!wayland_egl_context->attach(display, surface, { parent->x(), parent->y(), parent->width(), parent->height(), s }))
printf("Couldn't attach context to wayland surface.\n");
context.reset(wayland_egl_context);
}
else if (platform == "xcb")
{
auto display = (Display *)pni->nativeResourceForWindow("display", windowHandle());
auto xid = (Window)winId();
auto glx_context = new GTKGLXContext();
if (!glx_context->attach(display, xid))
printf("Couldn't attach to X11 window.\n");
context.reset(glx_context);
}
#else
auto hwnd = winId();
auto wgl_context = new WGLContext();
if (!wgl_context->attach((HWND)hwnd))
{
printf("Couldn't attach to context\n");
return;
}
context.reset(wgl_context);
#endif
if (!context->create_context())
{
printf("Couldn't create OpenGL context.\n");
}
context->make_current();
gladLoaderLoadGL();
if (config->display_messages == EmuConfig::eOnscreen)
{
auto defaults = S9xImGuiGetDefaults();
defaults.font_size = config->osd_size;
defaults.spacing = defaults.font_size / 2.4;
S9xImGuiInit(&defaults);
ImGui_ImplOpenGL3_Init();
}
loadShaders();
glGenTextures(1, &texture);
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &stock_coord_buffer);
glBindBuffer(GL_ARRAY_BUFFER, stock_coord_buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 16, coords, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
context->swap_interval(config->enable_vsync ? 1 : 0);
QGuiApplication::sync();
paintEvent(nullptr);
}
void EmuCanvasOpenGL::loadShaders()
{
auto endswith = [&](std::string ext) ->bool {
return config->shader.rfind(ext) == config->shader.length() - ext.length();
};
using_shader = true;
if (!config->use_shader ||
!(endswith(".glslp") || endswith(".slangp")))
using_shader = false;
if (!using_shader)
{
createStockShaders();
}
else
{
setlocale(LC_NUMERIC, "C");
shader = std::make_unique<GLSLShader>();
if (!shader->load_shader(config->shader.c_str()))
{
shader.reset();
using_shader = false;
createStockShaders();
}
setlocale(LC_NUMERIC, "");
}
}
void EmuCanvasOpenGL::uploadTexture()
{
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
GLuint filter = config->bilinear_filter ? GL_LINEAR : GL_NEAREST;
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glPixelStorei(GL_UNPACK_ROW_LENGTH, output_data.bytes_per_line / 2);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB565, output_data.width, output_data.height, 0, GL_RGB, GL_UNSIGNED_SHORT_5_6_5, output_data.buffer);
}
void EmuCanvasOpenGL::draw()
{
if (!isVisible() || !context)
return;
context->make_current();
uploadTexture();
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glBindBuffer(GL_ARRAY_BUFFER, stock_coord_buffer);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, (const void *)32);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
if (!using_shader)
stockShaderDraw();
else
customShaderDraw();
if (S9xImGuiRunning())
{
ImGui_ImplOpenGL3_NewFrame();
if (context->width <= 0)
context->width = width() * devicePixelRatioF();
if (context->height <= 0)
context->height = height() * devicePixelRatioF();
if (S9xImGuiDraw(context->width, context->height))
{
auto *draw_data = ImGui::GetDrawData();
ImGui_ImplOpenGL3_RenderDrawData(draw_data);
}
}
if (config->speed_sync_method == EmuConfig::eTimer || config->speed_sync_method == EmuConfig::eTimerWithFrameskip)
throttle();
context->swap_buffers();
if (config->reduce_input_lag)
glFinish();
}
void EmuCanvasOpenGL::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
if (!context) return;
auto g = parent->geometry();
int s = devicePixelRatio();
auto platform = QGuiApplication::platformName();
#ifndef _WIN32
if (QGuiApplication::platformName() == "wayland")
((WaylandEGLContext *)context.get())->resize({ g.x(), g.y(), g.width(), g.height(), s });
else if (platform == "xcb")
((GTKGLXContext *)context.get())->resize();
#endif
}
void EmuCanvasOpenGL::paintEvent(QPaintEvent *event)
{
// TODO: If emu not running
if (!context || !isVisible())
return;
if (output_data.ready)
{
if (!static_cast<EmuMainWindow *>(main_window)->isActivelyDrawing())
draw();
return;
}
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
context->swap_buffers();
}
void EmuCanvasOpenGL::deinit()
{
shader_parameters_dialog.reset();
context.reset();
}
void EmuCanvasOpenGL::shaderChanged()
{
shader_parameters_dialog.reset();
if (shader)
shader.reset();
else
glDeleteProgram(stock_program);
loadShaders();
paintEvent(nullptr);
}
void EmuCanvasOpenGL::showParametersDialog()
{
if (!using_shader)
{
QMessageBox::warning(this, tr("OpenGL Driver"), tr("The driver isn't using a specialized shader preset right now."));
return;
}
if (shader && shader->param.empty())
{
QMessageBox::information(this, tr("OpenGL Driver"), tr("This shader preset doesn't offer any configurable parameters."));
return;
}
auto parameters = reinterpret_cast<std::vector<EmuCanvas::Parameter> *>(&shader->param);
if (!shader_parameters_dialog)
shader_parameters_dialog =
std::make_unique<ShaderParametersDialog>(this, parameters);
shader_parameters_dialog->show();
}
void EmuCanvasOpenGL::saveParameters(std::string filename)
{
if (shader)
shader->save(filename.c_str());
}

View File

@ -0,0 +1,50 @@
#ifndef __EMU_CANVAS_OPENGL_HPP
#define __EMU_CANVAS_OPENGL_HPP
#include <QWindow>
#include "EmuCanvas.hpp"
#include "ShaderParametersDialog.hpp"
class OpenGLContext;
class GLSLShader;
class EmuCanvasOpenGL : public EmuCanvas
{
public:
EmuCanvasOpenGL(EmuConfig *config, QWidget *parent, QWidget *main_window);
~EmuCanvasOpenGL();
void deinit() override;
void paintEvent(QPaintEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
QPaintEngine * paintEngine() const override { return nullptr; }
void draw() override;
void shaderChanged() override;
void showParametersDialog() override;
void saveParameters(std::string filename) override;
private:
void resizeTexture(int width, int height);
void createContext();
void createStockShaders();
void stockShaderDraw();
void customShaderDraw();
void uploadTexture();
void loadShaders();
unsigned int stock_program;
unsigned int texture;
unsigned stock_coord_buffer;
std::unique_ptr<OpenGLContext> context;
bool using_shader;
std::unique_ptr<GLSLShader> shader;
std::unique_ptr<ShaderParametersDialog> shader_parameters_dialog;
// The first 8 values are vertices for a triangle strip, the second are texture
// coordinates for a stock NPOT texture.
const float coords[16] = { -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, };
};
#endif

47
qt/src/EmuCanvasQt.cpp Normal file
View File

@ -0,0 +1,47 @@
#include "EmuCanvasQt.hpp"
#include <QGuiApplication>
#include <QtEvents>
EmuCanvasQt::EmuCanvasQt(EmuConfig *config, QWidget *parent, QWidget *main_window)
: EmuCanvas(config, parent, main_window)
{
setMinimumSize(256, 224);
}
EmuCanvasQt::~EmuCanvasQt()
{
deinit();
}
void EmuCanvasQt::deinit()
{
}
void EmuCanvasQt::draw()
{
QWidget::repaint(0, 0, width(), height());
throttle();
}
void EmuCanvasQt::paintEvent(QPaintEvent *event)
{
// TODO: If emu not running
if (!output_data.ready)
{
QPainter paint(this);
paint.fillRect(QRect(0, 0, width(), height()), QBrush(QColor(0, 0, 0)));
return;
}
QPainter paint(this);
QImage image((const uchar *)output_data.buffer, output_data.width, output_data.height, output_data.bytes_per_line, output_data.format);
paint.setRenderHint(QPainter::SmoothPixmapTransform, config->bilinear_filter);
QRect dest = { 0, 0, width(), height() };
if (config->maintain_aspect_ratio)
{
paint.fillRect(QRect(0, 0, width(), height()), QBrush(QColor(0, 0, 0)));
dest = applyAspect(dest);
}
paint.drawImage(dest, image, QRect(0, 0, output_data.width, output_data.height));
}

20
qt/src/EmuCanvasQt.hpp Normal file
View File

@ -0,0 +1,20 @@
#ifndef __EMU_CANVAS_QT_HPP
#define __EMU_CANVAS_QT_HPP
#include "EmuCanvas.hpp"
#include <QPainter>
class EmuCanvasQt : public EmuCanvas
{
public:
EmuCanvasQt(EmuConfig *config, QWidget *parent, QWidget *main_window);
~EmuCanvasQt();
virtual void deinit() override;
virtual void draw() override;
void paintEvent(QPaintEvent *event) override;
};
#endif

305
qt/src/EmuCanvasVulkan.cpp Normal file
View File

@ -0,0 +1,305 @@
#include <QtGui/QGuiApplication>
#include <qpa/qplatformnativeinterface.h>
#include <QTimer>
#include <QtEvents>
#include <QMessageBox>
#include "EmuCanvasVulkan.hpp"
#include "src/ShaderParametersDialog.hpp"
#include "EmuMainWindow.hpp"
#include "snes9x_imgui.h"
#include "imgui_impl_vulkan.h"
using namespace QNativeInterface;
EmuCanvasVulkan::EmuCanvasVulkan(EmuConfig *config, QWidget *parent, QWidget *main_window)
: EmuCanvas(config, parent, main_window)
{
setMinimumSize(256, 224);
setUpdatesEnabled(false);
setAutoFillBackground(false);
setAttribute(Qt::WA_NoSystemBackground, true);
setAttribute(Qt::WA_NativeWindow, true);
setAttribute(Qt::WA_PaintOnScreen, true);
setAttribute(Qt::WA_OpaquePaintEvent);
createWinId();
window = windowHandle();
auto timer = new QTimer(this);
timer->setSingleShot(true);
timer->callOnTimeout([&]{ createContext(); });
timer->start();
}
EmuCanvasVulkan::~EmuCanvasVulkan()
{
deinit();
}
bool EmuCanvasVulkan::initImGui()
{
auto defaults = S9xImGuiGetDefaults();
defaults.font_size = config->osd_size;
defaults.spacing = defaults.font_size / 2.4;
S9xImGuiInit(&defaults);
ImGui_ImplVulkan_LoadFunctions([](const char *function, void *instance) {
return VULKAN_HPP_DEFAULT_DISPATCHER.vkGetInstanceProcAddr(*((VkInstance *)instance), function);
}, &context->instance.get());
vk::DescriptorPoolSize pool_sizes[] =
{
{ vk::DescriptorType::eCombinedImageSampler, 1000 },
{ vk::DescriptorType::eUniformBuffer, 1000 }
};
auto descriptor_pool_create_info = vk::DescriptorPoolCreateInfo{}
.setPoolSizes(pool_sizes)
.setMaxSets(1000)
.setFlags(vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet);
imgui_descriptor_pool = context->device.createDescriptorPoolUnique(descriptor_pool_create_info);
ImGui_ImplVulkan_InitInfo init_info{};
init_info.Instance = context->instance.get();
init_info.PhysicalDevice = context->physical_device;
init_info.Device = context->device;;
init_info.QueueFamily = context->graphics_queue_family_index;
init_info.Queue = context->queue;
init_info.DescriptorPool = imgui_descriptor_pool.get();
init_info.Subpass = 0;
init_info.MinImageCount = context->swapchain->get_num_frames();
init_info.ImageCount = context->swapchain->get_num_frames();
init_info.MSAASamples = VK_SAMPLE_COUNT_1_BIT;
ImGui_ImplVulkan_Init(&init_info, context->swapchain->get_render_pass());
auto cmd = context->begin_cmd_buffer();
ImGui_ImplVulkan_CreateFontsTexture(cmd);
context->end_cmd_buffer();
context->wait_idle();
return true;
}
void EmuCanvasVulkan::createContext()
{
if (simple_output)
return;
platform = QGuiApplication::platformName();
auto pni = QGuiApplication::platformNativeInterface();
setVisible(true);
QGuiApplication::sync();
context = std::make_unique<Vulkan::Context>();
#ifdef _WIN32
auto hwnd = (HWND)winId();
context->init_win32(nullptr, hwnd, config->display_device_index);
#else
if (platform == "wayland")
{
wayland_surface = std::make_unique<WaylandSurface>();
auto display = (wl_display *)pni->nativeResourceForWindow("display", window);
auto surface = (wl_surface *)pni->nativeResourceForWindow("surface", main_window->windowHandle());
wayland_surface->attach(display, surface, { parent->x(), parent->y(), width(), height(), static_cast<int>(devicePixelRatio()) });
auto [scaled_width, scaled_height] = wayland_surface->get_size();
context->init_wayland(display, wayland_surface->child, scaled_width, scaled_height, config->display_device_index);
}
else if (platform == "xcb")
{
auto display = (Display *)pni->nativeResourceForWindow("display", window);
auto xid = (Window)winId();
context->init_Xlib(display, xid, config->display_device_index);
}
#endif
if (config->display_messages == EmuConfig::eOnscreen)
initImGui();
tryLoadShader();
QGuiApplication::sync();
paintEvent(nullptr);
}
void EmuCanvasVulkan::tryLoadShader()
{
simple_output.reset();
shader_chain.reset();
shader_parameters_dialog.reset();
if (config->use_shader && !config->shader.empty())
{
shader_chain = std::make_unique<Vulkan::ShaderChain>(context.get());
setlocale(LC_NUMERIC, "C");
current_shader = config->shader;
if (!shader_chain->load_shader_preset(config->shader))
{
printf("Couldn't load shader preset: %s\n", config->shader.c_str());
shader_chain.reset();
}
setlocale(LC_NUMERIC, "");
}
if (!shader_chain)
simple_output = std::make_unique<Vulkan::SimpleOutput>(context.get(), vk::Format::eR5G6B5UnormPack16);
}
void EmuCanvasVulkan::shaderChanged()
{
if (!config->use_shader)
current_shader.clear();
if ((!config->use_shader && shader_chain) ||
(config->use_shader && current_shader != config->shader))
tryLoadShader();
}
void EmuCanvasVulkan::draw()
{
if (!context)
return;
if (!window->isVisible())
return;
if (S9xImGuiDraw(width() * devicePixelRatioF(), height() * devicePixelRatioF()))
{
auto draw_data = ImGui::GetDrawData();
context->swapchain->on_render_pass_end([&, draw_data] {
ImGui_ImplVulkan_RenderDrawData(draw_data, context->swapchain->get_cmd());
});
}
auto viewport = applyAspect(QRect(0, 0, width() * devicePixelRatio(), height() * devicePixelRatio()));
bool retval = false;
if (shader_chain)
{
retval = shader_chain->do_frame_without_swap(output_data.buffer, output_data.width, output_data.height, output_data.bytes_per_line, vk::Format::eR5G6B5UnormPack16, viewport.x(), viewport.y(), viewport.width(), viewport.height());
}
else if (simple_output)
{
simple_output->set_filter(config->bilinear_filter);
retval = simple_output->do_frame_without_swap(output_data.buffer, output_data.width, output_data.height, output_data.bytes_per_line, viewport.x(), viewport.y(), viewport.width(), viewport.height());
}
if (retval)
{
throttle();
context->swapchain->swap();
if (config->reduce_input_lag)
context->wait_idle();
}
}
void EmuCanvasVulkan::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
if (!context)
return;
int width = event->size().width();
int height = event->size().height();
context->swapchain->set_vsync(config->enable_vsync);
#ifndef _WIN32
if (platform == "wayland")
{
wayland_surface->resize({ parent->x(), parent->y(), width, height, (int)devicePixelRatio() });
std::tie(width, height) = wayland_surface->get_size();
// On Wayland, Vulkan WSI provides the buffer for the subsurface,
// so we have to specify a width and height instead of polling the parent.
context->recreate_swapchain(width, height);
return;
}
#endif
context->recreate_swapchain(-1, -1);
}
void EmuCanvasVulkan::paintEvent(QPaintEvent *event)
{
// TODO: If emu not running
if (!context || !isVisible())
return;
auto window = (EmuMainWindow *)main_window;
if (output_data.ready)
{
if (!window->isActivelyDrawing())
draw();
return;
}
// Clear to black
uint8_t buffer[] = { 0, 0, 0, 0 };
if (shader_chain)
shader_chain->do_frame(buffer, 1, 1, 1, vk::Format::eR5G6B5UnormPack16, 0, 0, width(), height());
if (simple_output)
simple_output->do_frame(buffer, 1, 1, 1, 0, 0, width(), height());
}
void EmuCanvasVulkan::deinit()
{
shader_parameters_dialog.reset();
if (ImGui::GetCurrentContext())
{
context->wait_idle();
imgui_descriptor_pool.reset();
imgui_render_pass.reset();
ImGui_ImplVulkan_Shutdown();
ImGui::DestroyContext();
}
simple_output.reset();
shader_chain.reset();
context.reset();
#ifndef _WIN32
wayland_surface.reset();
#endif
}
std::vector<std::string> EmuCanvasVulkan::getDeviceList()
{
return Vulkan::Context::get_device_list();
}
void EmuCanvasVulkan::showParametersDialog()
{
if (!context)
{
QMessageBox::warning(this, tr("Vulkan Driver"), tr("The vulkan display driver hasn't properly loaded."));
return;
}
if (!shader_chain)
{
QMessageBox::warning(this, tr("Vulkan Driver"), tr("The driver isn't using a specialized shader preset right now."));
return;
}
if (shader_chain && shader_chain->preset->parameters.empty())
{
QMessageBox::information(this, tr("Vulkan Driver"), tr("This shader preset doesn't offer any configurable parameters."));
return;
}
auto parameters = reinterpret_cast<std::vector<EmuCanvas::Parameter> *>(&shader_chain->preset->parameters);
if (!shader_parameters_dialog)
shader_parameters_dialog =
std::make_unique<ShaderParametersDialog>(this, parameters);
shader_parameters_dialog->show();
}
void EmuCanvasVulkan::saveParameters(std::string filename)
{
if (shader_chain && shader_chain->preset)
shader_chain->preset->save_to_file(filename);
}

View File

@ -0,0 +1,50 @@
#pragma once
#include <QWindow>
#include "EmuCanvas.hpp"
#include "ShaderParametersDialog.hpp"
#include "../../vulkan/vulkan_simple_output.hpp"
#include "../../vulkan/vulkan_shader_chain.hpp"
#ifndef _WIN32
#include "common/video/wayland_surface.hpp"
#endif
class EmuCanvasVulkan : public EmuCanvas
{
public:
EmuCanvasVulkan(EmuConfig *config, QWidget *parent, QWidget *main_window);
~EmuCanvasVulkan();
void deinit() override;
void paintEvent(QPaintEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
QPaintEngine *paintEngine() const override { return nullptr; }
std::vector<std::string> getDeviceList() override;
void shaderChanged() override;
void showParametersDialog() override;
void saveParameters(std::string filename) override;
void draw() override;
bool initImGui();
vk::UniqueRenderPass imgui_render_pass;
vk::UniqueDescriptorPool imgui_descriptor_pool;
std::unique_ptr<Vulkan::Context> context;
std::unique_ptr<Vulkan::SimpleOutput> simple_output;
std::unique_ptr<Vulkan::ShaderChain> shader_chain;
private:
void createContext();
void tryLoadShader();
std::string current_shader;
QWindow *window = nullptr;
std::unique_ptr<ShaderParametersDialog> shader_parameters_dialog = nullptr;
QString platform;
#ifndef _WIN32
std::unique_ptr<WaylandSurface> wayland_surface;
#endif
};

560
qt/src/EmuConfig.cpp Normal file
View File

@ -0,0 +1,560 @@
#include <cstdio>
#include <string>
#define TOML_LARGE_FILES 1
#define TOML_IMPLEMENTATION 1
#include <fstream>
#include "toml.hpp"
#include <filesystem>
namespace fs = std::filesystem;
#include "EmuConfig.hpp"
#include "EmuBinding.hpp"
#include <functional>
static const char *shortcut_names[] =
{
"OpenROM",
"EmuTurbo",
"ToggleEmuTurbo",
"PauseContinue",
"SoftReset",
"Reset",
"Quit",
"ToggleFullscreen",
"Screenshot",
"SaveSPC",
"SaveState",
"LoadState",
"IncreaseSlot",
"DecreaseSlot",
"QuickSave000",
"QuickSave001",
"QuickSave002",
"QuickSave003",
"QuickSave004",
"QuickSave005",
"QuickSave006",
"QuickSave007",
"QuickSave008",
"QuickSave009",
"QuickLoad000",
"QuickLoad001",
"QuickLoad002",
"QuickLoad003",
"QuickLoad004",
"QuickLoad005",
"QuickLoad006",
"QuickLoad007",
"QuickLoad008",
"QuickLoad009",
"Rewind",
"GrabMouse",
"SwapControllers1and2",
"ToggleBG0",
"ToggleBG1",
"ToggleBG2",
"ToggleBG3",
"ToggleSprites",
"ToggleBackdrop",
"SoundChannel0",
"SoundChannel1",
"SoundChannel2",
"SoundChannel3",
"SoundChannel4",
"SoundChannel5",
"SoundChannel6",
"SoundChannel7",
"SoundChannelsOn",
"BeginRecordingMovie",
"EndRecordingMovie",
"SeekToFrame",
};
static const char *default_controller_keys[] =
{
"Keyboard Ctrl+o", // eOpenROM
"Keyboard Tab", // eFastForward
"Keyboard `", // eToggleFastForward
"Keyboard p", // ePauseContinue
"Keyboard Ctrl+r", // eSoftReset
"", // ePowerCycle
"Keyboard Ctrl+q", // eQuit
"Keyboard F11", // eToggleFullscreen
"", // eSaveScreenshot
"", // eSaveSPC
"Keyboard F2", // eSaveState
"Keyboard F4", // eLoadState
"Keyboard F6", // eIncreaseSlot
"Keyboard F5", // eDecreaseSlot
"Keyboard 0", // eSaveState0
"Keyboard 1", // eSaveState1
"Keyboard 2", // eSaveState2
"Keyboard 3", // eSaveState3
"Keyboard 4", // eSaveState4
"Keyboard 5", // eSaveState5
"Keyboard 6", // eSaveState6
"Keyboard 7", // eSaveState7
"Keyboard 8", // eSaveState8
"Keyboard 9", // eSaveState9
"Keyboard Ctrl+0", // eLoadState0
"Keyboard Ctrl+1", // eLoadState1
"Keyboard Ctrl+2", // eLoadState2
"Keyboard Ctrl+3", // eLoadState3
"Keyboard Ctrl+4", // eLoadState4
"Keyboard Ctrl+5", // eLoadState5
"Keyboard Ctrl+6", // eLoadState6
"Keyboard Ctrl+7", // eLoadState7
"Keyboard Ctrl+8", // eLoadState8
"Keyboard Ctrl+9", // eLoadState9
"", // eRewind
"Keyboard Ctrl+g", // eGrabMouse
"", // eSwapControllers1and2
"", // eToggleBG0
"", // eToggleBG1
"", // eToggleBG2
"", // eToggleBG3
"", // eToggleSprites
"", // eChangeBackdrop
"", // eToggleSoundChannel1
"", // eToggleSoundChannel2
"", // eToggleSoundChannel3
"", // eToggleSoundChannel4
"", // eToggleSoundChannel5
"", // eToggleSoundChannel6
"", // eToggleSoundChannel7
"", // eToggleSoundChannel8
"", // eToggleAllSoundChannels
"", // eStartRecording
"", // eStopRecording
""
};
const char **EmuConfig::getDefaultShortcutKeys()
{
return default_controller_keys;
}
const char **EmuConfig::getShortcutNames()
{
return shortcut_names;
}
std::string EmuConfig::findConfigDir()
{
char *dir;
fs::path path;
if ((dir = getenv("XDG_CONFIG_HOME")))
{
path = dir;
path /= "snes9x";
}
else if ((dir = getenv("HOME")))
{
path = dir;
path /= ".config/snes9x";
}
else
{
path = "./.snes9x";
}
if (!fs::exists(path))
fs::create_directory(path);
return path.string();
}
std::string EmuConfig::findConfigFile()
{
fs::path path(findConfigDir());
path /= "snes9x-qt.conf";
return path.string();
}
void EmuConfig::setDefaults(int section)
{
if (section == -1 || section == 0)
{
// General
fullscreen_on_open = false;
disable_screensaver = true;
pause_emulation_when_unfocused = true;
show_frame_rate = false;
show_indicators = true;
show_pressed_keys = false;
show_time = false;
}
if (section == -1 || section == 1)
{
// Display
display_driver = {};
display_device_index = 0;
enable_vsync = true;
;
bilinear_filter = true;
;
reduce_input_lag = true;
adjust_for_vrr = false;
use_shader = false;
shader = {};
last_shader_folder = {};
scale_image = true;
;
maintain_aspect_ratio = true;
use_integer_scaling = false;
aspect_ratio_numerator = 4;
aspect_ratio_denominator = 3;
show_overscan = false;
high_resolution_effect = eLeaveAlone;
software_filter = {};
display_messages = eOnscreen;
osd_size = 24;
}
if (section == -1 || section == 2)
{
// Sound
sound_driver = {};
sound_device = {};
playback_rate = 48000;
audio_buffer_size_ms = 64;
adjust_input_rate_automatically = true;
input_rate = 31979;
dynamic_rate_control = false;
dynamic_rate_limit = 0.005;
mute_audio = false;
mute_audio_during_alternate_speed = false;
}
if (section == -1 || section == 3)
{
speed_sync_method = eTimer;
fixed_frame_rate = 0.0;
fast_forward_skip_frames = 9;
rewind_buffer_size = 0;
rewind_frame_interval = 5;
allow_invalid_vram_access = false;
allow_opposing_dpad_directions = false;
overclock = false;
remove_sprite_limit = false;
enable_shadow_buffer = false;
superfx_clock_multiplier = 100;
sound_filter = eGaussian;
}
if (section == -1 || section == 4)
{
// Controllers
memset(binding.controller, 0, sizeof(binding.controller));
const char *button_list[] = { "Up", "Down", "Left", "Right", "d", "c", "s", "x", "z", "a", "Return", "Space" };
for (int i = 0; i < std::size(button_list); i++)
{
binding.controller[0].buttons[i * 4] = EmuBinding::from_config_string("Keyboard " + std::string(button_list[i]));
}
}
if (section == -1 || section == 5)
{
// Shortcuts
memset(binding.shortcuts, 0, sizeof(binding.shortcuts));
for (auto i = 0; i < num_shortcuts; i++)
{
binding.shortcuts[i * 4] = EmuBinding::from_config_string(getDefaultShortcutKeys()[i]);
}
}
if (section == -1 || section == 6)
{
// Files
sram_folder = {};
state_folder = {};
cheat_folder = {};
patch_folder = {};
export_folder = {};
sram_location = eROMDirectory;
state_location = eROMDirectory;
cheat_location = eROMDirectory;
patch_location = eROMDirectory;
export_location = eROMDirectory;
}
}
void EmuConfig::config(std::string filename, bool write)
{
toml::table root;
toml::table *table = nullptr;
std::string section;
std::function<void(std::string, bool &)> Bool;
std::function<void(std::string, int &)> Int;
std::function<void(std::string, std::string &)> String;
std::function<void(std::string, int &, std::vector<const char *>)> Enum;
std::function<void(std::string, double &)> Double;
std::function<void(std::string, EmuBinding &)> Binding;
std::function<void(std::string)> BeginSection;
std::function<void()> EndSection;
if (write)
{
Bool = [&](std::string key, bool &value) {
table->insert_or_assign(key, value);
};
Int = [&](std::string key, int &value) {
table->insert_or_assign(key, value);
};
String = [&](std::string key, std::string &value) {
table->insert_or_assign(key, value);
};
Enum = [&](std::string key, int &value, std::vector<const char *> map) {
table->insert_or_assign(key, map[value]);
};
Double = [&](std::string key, double &value) {
table->insert_or_assign(key, value);
};
Binding = [&](std::string key, EmuBinding &binding) {
table->insert_or_assign(key, binding.to_config_string());
};
BeginSection = [&](std::string str) {
section = str;
table = new toml::table;
};
EndSection = [&]() {
root.insert_or_assign(section, *table);
delete table;
};
root.clear();
}
else
{
Bool = [&](std::string key, bool &value) {
if (table && table->contains(key) && table->get(key)->is_boolean())
value = table->get(key)->as_boolean()->get();
};
Int = [&](std::string key, int &value) {
if (table && table->contains(key) && table->get(key)->is_integer())
value = table->get(key)->as_integer()->get();
};
String = [&](std::string key, std::string &value) {
if (table && table->contains(key) && table->get(key)->is_string())
value = table->get(key)->as_string()->get();
};
Binding = [&](std::string key, EmuBinding &binding) {
if (table && table->contains(key) && table->get(key)->is_string())
binding = EmuBinding::from_config_string(table->get(key)->as_string()->get());
};
Double = [&](std::string key, double &value) {
if (table && table->contains(key) && table->get(key)->is_floating_point())
value = table->get(key)->as_floating_point()->get();
};
Enum = [&](std::string key, int &value, std::vector<const char *> map) {
std::string entry;
if (table && table->contains(key) && table->get(key)->is_string())
entry = table->get(key)->as_string()->get();
else
return;
auto tolower = [](std::string str) -> std::string {
for (auto &c : str)
if (c >= 'A' && c <= 'Z')
c += ('a' - 'A');
return str;
};
entry = tolower(entry);
for (size_t i = 0; i < map.size(); i++)
{
if (tolower(map[i]) == entry)
{
value = i;
return;
}
}
};
BeginSection = [&](std::string str) {
section = str;
auto root_section = root.get(section);
if (root_section)
table = root_section->as_table();
else
table = nullptr;
};
EndSection = [&]() {
};
auto parse_result = toml::parse_file(filename);
if (parse_result.failed())
return;
root = std::move(parse_result.table());
}
BeginSection("Operational");
String("LastROMFolder", last_rom_folder);
Int("MainWindowWidth", main_window_width);
Int("MainWindowHeight", main_window_height);
int recent_count = recently_used.size();
Int("RecentlyUsedEntries", recent_count);
if (!write)
recently_used.resize(recent_count);
for (int i = 0; i < recent_count; i++)
{
String("RecentlyUsed" + std::to_string(i), recently_used[i]);
}
EndSection();
BeginSection("General");
Bool("FullscreenOnOpen", fullscreen_on_open);
Bool("DisableScreensaver", disable_screensaver);
Bool("PauseEmulationWhenUnfocused", pause_emulation_when_unfocused);
Bool("ShowFrameRate", show_frame_rate);
Bool("ShowIndicators", show_indicators);
Bool("ShowPressedKeys", show_pressed_keys);
Bool("ShowTime", show_time);
EndSection();
BeginSection("Display");
String("DisplayDriver", display_driver);
Int("DisplayDevice", display_device_index);
Bool("VSync", enable_vsync);
Bool("ReduceInputLag", reduce_input_lag);
Bool("BilinearFilter", bilinear_filter);
Bool("AdjustForVRR", adjust_for_vrr);
Bool("UseShader", use_shader);
String("Shader", shader);
String("LastShaderFolder", last_shader_folder);
Bool("ScaleImage", scale_image);
Bool("MaintainAspectRatio", maintain_aspect_ratio);
Bool("UseIntegerScaling", use_integer_scaling);
Int("AspectRatioNumerator", aspect_ratio_numerator);
Int("AspectRatioDenominator", aspect_ratio_denominator);
Bool("ShowOverscan", show_overscan);
Enum("HighResolutionEffect", high_resolution_effect, { "LeaveAlone", "ScaleDown", "ScaleUp" });
String("SoftwareFilter", software_filter);
Enum("DisplayMessages", display_messages, { "Onscreen", "Inscreen", "None" });
Int("OSDSize", osd_size);
EndSection();
BeginSection("Sound");
String("SoundDriver", sound_driver);
String("SoundDevice", sound_device);
Int("PlaybackRate", playback_rate);
Int("BufferSize", audio_buffer_size_ms);
Bool("AdjustInputRateAutomatically", adjust_input_rate_automatically);
Int("InputRate", input_rate);
Bool("DynamicRateControl", dynamic_rate_control);
Double("DynamicRateLimit", dynamic_rate_limit);
Bool("Mute", mute_audio);
Bool("MuteAudioDuringAlternateSpeed", mute_audio_during_alternate_speed);
EndSection();
BeginSection("Emulation");
Enum("SpeedSyncMethod", speed_sync_method, { "Timer", "TimerFrameskip", "SoundSync", "Unlimited" });
Double("FixedFrameRate", fixed_frame_rate);
Int("FastForwardSkipFrames", fast_forward_skip_frames);
Int("RewindBufferSize", rewind_buffer_size);
Int("RewindFrameInterval", rewind_frame_interval);
Bool("AllowInvalidVRAMAccess", allow_invalid_vram_access);
Bool("AllowOpposingDpadDirections", allow_opposing_dpad_directions);
Bool("Overclock", overclock);
Bool("RemoveSpriteLimit", remove_sprite_limit);
Bool("EnableShadowBuffer", enable_shadow_buffer);
Int("SuperFXClockMultiplier", superfx_clock_multiplier);
Enum("SoundFilter", sound_filter, { "Gaussian", "Nearest", "Linear", "Cubic", "Sinc" });
EndSection();
const char *names[] = { "Up", "Down", "Left", "Right", "A", "B", "X", "Y", "L", "R", "Start", "Select", "Turbo A", "Turbo B", "Turbo X", "Turbo Y", "Turbo L", "Turbo R" };
for (int c = 0; c < 5; c++)
{
BeginSection("Controller " + std::to_string(c));
for (int y = 0; y < num_controller_bindings; y++)
for (int x = 0; x < allowed_bindings; x++)
{
std::string keyname = names[y] + std::to_string(x);
Binding(keyname, binding.controller[c].buttons[y * allowed_bindings + x]);
}
EndSection();
}
BeginSection("Shortcuts");
for (int i = 0; i < num_shortcuts; i++)
{
Binding(getShortcutNames()[i] + std::to_string(0), binding.shortcuts[i * 4]);
Binding(getShortcutNames()[i] + std::to_string(1), binding.shortcuts[i * 4 + 1]);
Binding(getShortcutNames()[i] + std::to_string(2), binding.shortcuts[i * 4 + 2]);
Binding(getShortcutNames()[i] + std::to_string(3), binding.shortcuts[i * 4 + 3]);
}
EndSection();
BeginSection("Files");
Enum("SRAMLocation", sram_location, { "ROMDirectory", "ConfigDirectory", "Custom" });
Enum("StateLocation", state_location, { "ROMDirectory", "ConfigDirectory", "Custom" });
Enum("CheatLocation", cheat_location, { "ROMDirectory", "ConfigDirectory", "Custom" });
Enum("PatchLocation", patch_location, { "ROMDirectory", "ConfigDirectory", "Custom" });
Enum("ExportLocation", export_location, { "ROMDirectory", "ConfigDirectory", "Custom" });
String("SRAMFolder", sram_folder);
String("StateFolder", state_folder);
String("CheatFolder", cheat_folder);
String("PatchFolder", patch_folder);
String("ExportFolder", export_folder);
Int("SRAMSaveInterval", sram_save_interval);
EndSection();
if (write)
{
std::ofstream ofs(filename);
ofs << root;
ofs.close();
}
}
void EmuConfig::setVRRConfig(bool enable)
{
if (enable == vrr_enabled)
return;
if (!adjust_for_vrr && enable)
return;
vrr_enabled = enable;
if (enable)
{
saved_fixed_frame_rate = fixed_frame_rate;
saved_input_rate = input_rate;
saved_speed_sync_method = speed_sync_method;
saved_enable_vsync = enable_vsync;
fixed_frame_rate = 0.0;
input_rate = 32040;
enable_vsync = true;
speed_sync_method = eTimer;
}
else
{
fixed_frame_rate = saved_fixed_frame_rate;
input_rate = saved_input_rate;
speed_sync_method = saved_speed_sync_method;
enable_vsync = saved_enable_vsync;
}
}

224
qt/src/EmuConfig.hpp Normal file
View File

@ -0,0 +1,224 @@
#ifndef __EMU_CONFIG_HPP
#define __EMU_CONFIG_HPP
#include <string>
#include "EmuBinding.hpp"
struct EmuConfig
{
static std::string findConfigFile();
static std::string findConfigDir();
void setDefaults(int section = -1);
void config(std::string filename, bool write);
void loadFile(std::string filename)
{
config(filename, false);
}
void saveFile(std::string filename)
{
config(filename, true);
}
void setVRRConfig(bool enable = true);
bool vrr_enabled = false;
int saved_input_rate = 0;
double saved_fixed_frame_rate = 0.0;
int saved_speed_sync_method = 0;
bool saved_enable_vsync = false;
// Operational
std::string last_rom_folder;
int main_window_width = 0;
int main_window_height = 0;
std::vector<std::string> recently_used;
// General
bool fullscreen_on_open;
bool disable_screensaver;
bool pause_emulation_when_unfocused;
bool show_frame_rate;
bool show_indicators;
bool show_pressed_keys;
bool show_time;
// Display
std::string display_driver;
int display_device_index;
bool enable_vsync;
bool bilinear_filter;
bool reduce_input_lag;
bool adjust_for_vrr;
bool use_shader;
std::string shader;
std::string last_shader_folder;
bool scale_image;
bool maintain_aspect_ratio;
bool use_integer_scaling;
int aspect_ratio_numerator;
int aspect_ratio_denominator;
bool show_overscan;
enum HighResolutionEffect
{
eLeaveAlone = 0,
eScaleDown = 1,
eScaleUp = 2
};
int high_resolution_effect;
std::string software_filter;
enum DisplayMessages
{
eOnscreen = 0,
eInscreen = 1,
eNone = 2
};
int display_messages;
int osd_size;
// Sound
std::string sound_driver;
std::string sound_device;
int playback_rate;
int audio_buffer_size_ms;
bool adjust_input_rate_automatically;
int input_rate;
bool dynamic_rate_control;
double dynamic_rate_limit;
bool mute_audio;
bool mute_audio_during_alternate_speed;
// Emulation
enum SpeedSyncMethod
{
eTimer = 0,
eTimerWithFrameskip = 1,
eSoundSync = 2,
eUnlimited = 3
};
int speed_sync_method;
double fixed_frame_rate;
int fast_forward_skip_frames;
int rewind_buffer_size;
int rewind_frame_interval;
// Emulation/Hacks
bool allow_invalid_vram_access;
bool allow_opposing_dpad_directions;
bool overclock;
bool remove_sprite_limit;
bool enable_shadow_buffer;
int superfx_clock_multiplier;
enum SoundFilter
{
eNearest = 0,
eLinear = 1,
eGaussian = 2,
eCubic = 3,
eSinc = 4
};
int sound_filter;
// Files
enum FileLocation
{
eROMDirectory = 0,
eConfigDirectory = 1,
eCustomDirectory = 2
};
int sram_location;
int state_location;
int cheat_location;
int patch_location;
int export_location;
std::string sram_folder;
std::string state_folder;
std::string cheat_folder;
std::string patch_folder;
std::string export_folder;
int sram_save_interval;
static const int allowed_bindings = 4;
static const int num_controller_bindings = 18;
static const int num_shortcuts = 55;
struct
{
struct
{
EmuBinding buttons[num_controller_bindings * allowed_bindings];
} controller[5];
EmuBinding shortcuts[num_shortcuts * allowed_bindings];
} binding;
static const char **getDefaultShortcutKeys();
static const char **getShortcutNames();
enum Shortcut
{
eOpenROM = 0,
eFastForward,
eToggleFastForward,
ePauseContinue,
eSoftReset,
ePowerCycle,
eQuit,
eToggleFullscreen,
eSaveScreenshot,
eSaveSPC,
eSaveState,
eLoadState,
eIncreaseSlot,
eDecreaseSlot,
eSaveState0,
eSaveState1,
eSaveState2,
eSaveState3,
eSaveState4,
eSaveState5,
eSaveState6,
eSaveState7,
eSaveState8,
eSaveState9,
eLoadState0,
eLoadState1,
eLoadState2,
eLoadState3,
eLoadState4,
eLoadState5,
eLoadState6,
eLoadState7,
eLoadState8,
eLoadState9,
eRewind,
eGrabMouse,
eSwapControllers1and2,
eToggleBG0,
eToggleBG1,
eToggleBG2,
eToggleBG3,
eToggleSprites,
eChangeBackdrop,
eToggleSoundChannel1,
eToggleSoundChannel2,
eToggleSoundChannel3,
eToggleSoundChannel4,
eToggleSoundChannel5,
eToggleSoundChannel6,
eToggleSoundChannel7,
eToggleSoundChannel8,
eToggleAllSoundChannels,
eStartRecording,
eStopRecording,
eSeekToFrame,
};
};
#endif

137
qt/src/EmuInputPanel.cpp Normal file
View File

@ -0,0 +1,137 @@
#include "EmuInputPanel.hpp"
#include <QFrame>
#include <QtEvents>
EmuInputBinder::EmuInputBinder(const QString &text, std::vector<EmuBinding> *bindings, int min_label_width)
{
layout = new QHBoxLayout;
setLayout(layout);
//layout->setMargin(0);
label = new QLabel(text);
layout->addWidget(label);
label->setMinimumWidth(min_label_width);
this->bindings = bindings;
remove_icon = QIcon::fromTheme("remove");
add_icon = QIcon::fromTheme("add");
auto frame = new QFrame;
auto sublayout = new QHBoxLayout;
//sublayout->setMargin(2);
frame->setContentsMargins(0, 0, 0, 0);
frame->setLayout(sublayout);
frame->setFrameShape(QFrame::Shape::StyledPanel);
frame->setFrameShadow(QFrame::Shadow::Sunken);
layout->addWidget(frame);
for (int i = 0; i < 3; i++)
{
buttons[i] = new QPushButton(this);
sublayout->addWidget(buttons[i]);
buttons[i]->connect(buttons[i], &QPushButton::clicked, [&, i] {
this->bindings->erase(this->bindings->begin() + i);
updateDisplay();
});
}
add_button = new QPushButton(add_icon, "");
add_button->setCheckable(true);
add_button->connect(add_button, &QPushButton::toggled, [&](bool checked) {
if (checked)
{
grabKeyboard();
if (add_button_func)
add_button_func(this);
}
});
sublayout->addWidget(add_button);
layout->addStretch();
updateDisplay();
add_button_func = nullptr;
}
void EmuInputBinder::reset(const QString &text, std::vector<EmuBinding> *bindings)
{
label->setText(text);
this->bindings = bindings;
}
void EmuInputBinder::keyPressEvent(QKeyEvent *event)
{
releaseKeyboard();
if (add_button->isChecked() && bindings->size() < 3)
{
auto key = EmuBinding::keyboard(
event->key(),
event->modifiers().testFlag(Qt::KeyboardModifier::ShiftModifier),
event->modifiers().testFlag(Qt::KeyboardModifier::AltModifier),
event->modifiers().testFlag(Qt::KeyboardModifier::ControlModifier),
event->modifiers().testFlag(Qt::KeyboardModifier::MetaModifier));
bool skip = false;
for (auto &b : *bindings)
{
if (b == key)
{
skip = true;
}
}
if (event->key() == Qt::Key_Escape)
skip = true;
if (!skip)
{
bindings->push_back(key);
updateDisplay();
}
add_button->setChecked(false);
}
}
void EmuInputBinder::untoggleAddButton()
{
add_button->setChecked(false);
}
void EmuInputBinder::updateDisplay()
{
size_t i;
for (i = 0; i < bindings->size(); i++)
{
QPushButton &b = *buttons[i];
b.setIcon(QIcon::fromTheme("remove"));
b.setText((*bindings)[i].to_string().c_str());
b.show();
}
for (; i < 3; i++)
{
buttons[i]->hide();
}
if (bindings->size() >= 3)
{
add_button->hide();
}
else
{
add_button->show();
}
}
void EmuInputBinder::connectAddButton(std::function<void (EmuInputBinder *)> func)
{
add_button_func = func;
}
EmuInputBinder::~EmuInputBinder()
{
}

38
qt/src/EmuInputPanel.hpp Normal file
View File

@ -0,0 +1,38 @@
#ifndef __EMU_INPUT_PANEL_HPP
#define __EMU_INPUT_PANEL_HPP
#include <QWidget>
#include <QBoxLayout>
#include <QPushButton>
#include <QLabel>
#include "EmuBinding.hpp"
class EmuInputBinder : public QWidget
{
public:
EmuInputBinder(const QString &text, std::vector<EmuBinding> *bindings, int min_label_width = 0);
~EmuInputBinder();
void updateDisplay();
std::vector<EmuBinding> *bindings;
void connectAddButton(std::function<void(EmuInputBinder *)>);
void untoggleAddButton();
void reset(const QString &text, std::vector<EmuBinding> *bindings);
void keyPressEvent(QKeyEvent *event) override;
private:
QLabel *label;
QHBoxLayout *layout;
QPushButton *buttons[3];
QPushButton *add_button;
std::function<void(EmuInputBinder *)> add_button_func;
QIcon remove_icon;
QIcon add_icon;
};
class EmuInputPanel : QWidget
{
};
#endif

566
qt/src/EmuMainWindow.cpp Normal file
View File

@ -0,0 +1,566 @@
#include <QTimer>
#include <QMenu>
#include <QMenuBar>
#include <QFileDialog>
#include <QtWidgets>
#include <QtEvents>
#include <QGuiApplication>
#include <QStackedWidget>
#include <qpa/qplatformnativeinterface.h>
#include "EmuMainWindow.hpp"
#include "EmuSettingsWindow.hpp"
#include "EmuApplication.hpp"
#include "EmuCanvasVulkan.hpp"
#include "EmuCanvasOpenGL.hpp"
#include "EmuCanvasQt.hpp"
#include "src/ShaderParametersDialog.hpp"
#undef KeyPress
static EmuSettingsWindow *g_emu_settings_window = nullptr;
EmuMainWindow::EmuMainWindow(EmuApplication *app)
: app(app)
{
createWidgets();
recreateCanvas();
setMouseTracking(true);
app->qtapp->installEventFilter(this);
mouse_timer.setTimerType(Qt::CoarseTimer);
mouse_timer.setInterval(1000);
mouse_timer.callOnTimeout([&] {
if (cursor_visible && isActivelyDrawing())
{
setCursor(QCursor(Qt::BlankCursor));
cursor_visible = false;
mouse_timer.stop();
}
});
}
EmuMainWindow::~EmuMainWindow()
{
}
void EmuMainWindow::destroyCanvas()
{
auto central_widget = centralWidget();
if (!central_widget)
return;
if (using_stacked_widget)
{
auto stackwidget = (QStackedWidget *)central_widget;
EmuCanvas *widget = (EmuCanvas *)stackwidget->widget(0);
if (widget)
{
widget->deinit();
stackwidget->removeWidget(widget);
}
delete takeCentralWidget();
}
else
{
EmuCanvas *widget = (EmuCanvas *)takeCentralWidget();
widget->deinit();
delete widget;
}
}
void EmuMainWindow::createCanvas()
{
if (app->config->display_driver != "vulkan" &&
app->config->display_driver != "opengl" &&
app->config->display_driver != "qt")
app->config->display_driver = "qt";
#ifndef _WIN32
if (QGuiApplication::platformName() == "wayland" && app->config->display_driver != "qt")
{
auto central_widget = new QStackedWidget();
if (app->config->display_driver == "vulkan")
canvas = new EmuCanvasVulkan(app->config.get(), central_widget, this);
else if (app->config->display_driver == "opengl")
canvas = new EmuCanvasOpenGL(app->config.get(), central_widget, this);
central_widget->addWidget(canvas);
central_widget->setCurrentWidget(canvas);
setCentralWidget(central_widget);
using_stacked_widget = true;
return;
}
#endif
if (app->config->display_driver == "vulkan")
canvas = new EmuCanvasVulkan(app->config.get(), this, this);
else if (app->config->display_driver == "opengl")
canvas = new EmuCanvasOpenGL(app->config.get(), this, this);
else
canvas = new EmuCanvasQt(app->config.get(), this, this);
setCentralWidget(canvas);
using_stacked_widget = false;
}
void EmuMainWindow::recreateCanvas()
{
destroyCanvas();
createCanvas();
}
void EmuMainWindow::setCoreActionsEnabled(bool enable)
{
for (auto &a : core_actions)
a->setEnabled(enable);
}
void EmuMainWindow::createWidgets()
{
setWindowTitle("Snes9x");
setWindowIcon(QIcon::fromTheme("snes9x"));
// File menu
auto file_menu = new QMenu(tr("&File"));
auto open_item = file_menu->addAction(QIcon::fromTheme("document-open"), tr("&Open File..."));
open_item->connect(open_item, &QAction::triggered, this, [&] {
openFile();
});
// File->Recent Files submenu
recent_menu = new QMenu("Recent Files");
file_menu->addMenu(recent_menu);
populateRecentlyUsed();
file_menu->addSeparator();
// File->Load/Save State submenus
load_state_menu = new QMenu(tr("&Load State"));
save_state_menu = new QMenu(tr("&Save State"));
for (size_t i = 0; i < state_items_size; i++)
{
auto action = load_state_menu->addAction(tr("Slot &%1").arg(i));
connect(action, &QAction::triggered, [&, i] {
app->loadState(i);
});
core_actions.push_back(action);
action = save_state_menu->addAction(tr("Slot &%1").arg(i));
connect(action, &QAction::triggered, [&, i] {
app->saveState(i);
});
core_actions.push_back(action);
}
load_state_menu->addSeparator();
auto load_state_file_item = load_state_menu->addAction(QIcon::fromTheme("document-open"), tr("From &File..."));
connect(load_state_file_item, &QAction::triggered, [&] {
this->chooseState(false);
});
core_actions.push_back(load_state_file_item);
load_state_menu->addSeparator();
auto load_state_undo_item = load_state_menu->addAction(QIcon::fromTheme("edit-undo"), tr("&Undo Load State"));
connect(load_state_undo_item, &QAction::triggered, [&] {
app->core->loadUndoState();
});
core_actions.push_back(load_state_undo_item);
file_menu->addMenu(load_state_menu);
save_state_menu->addSeparator();
auto save_state_file_item = save_state_menu->addAction(QIcon::fromTheme("document-save"), tr("To &File..."));
connect(save_state_file_item, &QAction::triggered, [&] {
this->chooseState(true);
});
core_actions.push_back(save_state_file_item);
file_menu->addMenu(save_state_menu);
auto exit_item = new QAction(QIcon::fromTheme("application-exit"), tr("E&xit"));
exit_item->connect(exit_item, &QAction::triggered, this, [&](bool checked) {
close();
});
file_menu->addAction(exit_item);
menuBar()->addMenu(file_menu);
// Emulation Menu
auto emulation_menu = new QMenu(tr("&Emulation"));
auto run_item = emulation_menu->addAction(tr("&Run"));
connect(run_item, &QAction::triggered, [&] {
if (manual_pause)
{
manual_pause = false;
app->unpause();
}
});
core_actions.push_back(run_item);
auto pause_item = emulation_menu->addAction(QIcon::fromTheme("media-playback-pause"), tr("&Pause"));
connect(pause_item, &QAction::triggered, [&] {
if (!manual_pause)
{
manual_pause = true;
app->pause();
}
});
core_actions.push_back(pause_item);
emulation_menu->addSeparator();
auto reset_item = emulation_menu->addAction(QIcon::fromTheme("view-refresh"), tr("Rese&t"));
connect(reset_item, &QAction::triggered, [&] {
app->reset();
if (manual_pause)
{
manual_pause = false;
app->unpause();
}
});
core_actions.push_back(reset_item);
auto hard_reset_item = emulation_menu->addAction(QIcon::fromTheme("process-stop"), tr("&Hard Reset"));
connect(hard_reset_item, &QAction::triggered, [&] {
app->powerCycle();
if (manual_pause)
{
manual_pause = false;
app->unpause();
}
});
core_actions.push_back(hard_reset_item);
menuBar()->addMenu(emulation_menu);
// View Menu
auto view_menu = new QMenu(tr("&View"));
// Set Size Menu
auto set_size_menu = new QMenu(tr("&Set Size"));
for (size_t i = 1; i <= 5; i++)
{
auto item = new QAction(tr("&%1x").arg(i));
set_size_menu->addAction(item);
item->connect(item, &QAction::triggered, this, [&, i](bool checked) {
resizeToMultiple(i);
});
}
view_menu->addMenu(set_size_menu);
view_menu->addSeparator();
auto fullscreen_item = new QAction(QIcon::fromTheme("view-fullscreen"), tr("&Fullscreen"));
view_menu->addAction(fullscreen_item);
fullscreen_item->connect(fullscreen_item, &QAction::triggered, [&](bool checked) {
toggleFullscreen();
});
menuBar()->addMenu(view_menu);
// Options Menu
auto options_menu = new QMenu(tr("&Options"));
std::array<QString, 7> setting_panels = { tr("&General..."),
tr("&Display..."),
tr("&Sound..."),
tr("&Emulation..."),
tr("&Controllers..."),
tr("Shortcu&ts..."),
tr("&Files...") };
const char *setting_icons[] = { ":/icons/blackicons/settings.svg",
":/icons/blackicons/display.svg",
":/icons/blackicons/sound.svg",
":/icons/blackicons/emulation.svg",
":/icons/blackicons/joypad.svg",
":/icons/blackicons/keyboard.svg",
":/icons/blackicons/folders.svg" };
for (int i = 0; i < setting_panels.size(); i++)
{
auto action = options_menu->addAction(QIcon(setting_icons[i]), setting_panels[i]);
QObject::connect(action, &QAction::triggered, [&, i] {
if (!g_emu_settings_window)
g_emu_settings_window = new EmuSettingsWindow(this, app);
g_emu_settings_window->show(i);
});
}
options_menu->addSeparator();
auto shader_settings_item = new QAction(QIcon(":/icons/blackicons/shader.svg"), tr("S&hader Settings..."));
QObject::connect(shader_settings_item, &QAction::triggered, [&] {
if (canvas)
canvas->showParametersDialog();
});
options_menu->addAction(shader_settings_item);
menuBar()->addMenu(options_menu);
setCoreActionsEnabled(false);
if (app->config->main_window_width != 0 && app->config->main_window_height != 0)
resize(app->config->main_window_width, app->config->main_window_height);
}
void EmuMainWindow::resizeToMultiple(int multiple)
{
resize((224 * multiple) * app->config->aspect_ratio_numerator / app->config->aspect_ratio_denominator, (224 * multiple) + menuBar()->height());
}
void EmuMainWindow::setBypassCompositor(bool bypass)
{
#ifndef _WIN32
if (QGuiApplication::platformName() == "xcb")
{
auto pni = QGuiApplication::platformNativeInterface();
uint32_t value = bypass;
auto display = (Display *)pni->nativeResourceForWindow("display", windowHandle());
auto xid = winId();
Atom net_wm_bypass_compositor = XInternAtom(display, "_NET_WM_BYPASS_COMPOSITOR", False);
XChangeProperty(display, xid, net_wm_bypass_compositor, 6, 32, PropModeReplace, (unsigned char *)&value, 1);
}
#endif
}
void EmuMainWindow::chooseState(bool save)
{
app->pause();
QFileDialog dialog(this, tr("Choose a State File"));
dialog.setDirectory(QString::fromStdString(app->core->getStateFolder()));
dialog.setNameFilters({ tr("Save States (*.sst *.oops *.undo *.0?? *.1?? *.2?? *.3?? *.4?? *.5?? *.6?? *.7?? *.8?? *.9*)"), tr("All Files (*)") });
if (!save)
dialog.setFileMode(QFileDialog::ExistingFile);
else
{
dialog.setFileMode(QFileDialog::AnyFile);
dialog.setAcceptMode(QFileDialog::AcceptSave);
}
if (!dialog.exec() || dialog.selectedFiles().empty())
{
app->unpause();
return;
}
auto filename = dialog.selectedFiles()[0];
if (!save)
app->loadState(filename.toStdString());
else
app->saveState(filename.toStdString());
app->unpause();
}
void EmuMainWindow::openFile()
{
app->pause();
QFileDialog dialog(this, tr("Open a ROM File"));
dialog.setFileMode(QFileDialog::ExistingFile);
dialog.setDirectory(QString::fromStdString(app->config->last_rom_folder));
dialog.setNameFilters({ tr("ROM Files (*.sfc *.smc *.bin *.fig *.msu *.zip)"), tr("All Files (*)") });
if (!dialog.exec() || dialog.selectedFiles().empty())
{
app->unpause();
return;
}
auto filename = dialog.selectedFiles()[0];
app->config->last_rom_folder = dialog.directory().canonicalPath().toStdString();
openFile(filename.toStdString());
app->unpause();
}
bool EmuMainWindow::openFile(std::string filename)
{
if (app->openFile(filename))
{
auto &ru = app->config->recently_used;
auto it = std::find(ru.begin(), ru.end(), filename);
if (it != ru.end())
ru.erase(it);
ru.insert(ru.begin(), filename);
populateRecentlyUsed();
setCoreActionsEnabled(true);
if (!isFullScreen() && app->config->fullscreen_on_open)
toggleFullscreen();
app->startGame();
mouse_timer.start();
return true;
}
return false;
}
void EmuMainWindow::populateRecentlyUsed()
{
recent_menu->clear();
if (app->config->recently_used.empty())
{
auto action = recent_menu->addAction(tr("No recent files"));
action->setDisabled(true);
return;
}
while (app->config->recently_used.size() > 10)
app->config->recently_used.pop_back();
for (int i = 0; i < app->config->recently_used.size(); i++)
{
auto &string = app->config->recently_used[i];
auto action = recent_menu->addAction(QString("&%1: %2").arg(i).arg(QString::fromStdString(string)));
connect(action, &QAction::triggered, [&, string] {
openFile(string);
});
}
recent_menu->addSeparator();
auto action = recent_menu->addAction(tr("Clear Recent Files"));
connect(action, &QAction::triggered, [&] {
app->config->recently_used.clear();
populateRecentlyUsed();
});
}
#undef KeyPress
#undef KeyRelease
bool EmuMainWindow::event(QEvent *event)
{
switch (event->type())
{
case QEvent::Close:
if (isFullScreen())
{
toggleFullscreen();
}
if (canvas)
canvas->deinit();
QGuiApplication::sync();
event->accept();
break;
case QEvent::Resize:
if (!isFullScreen() && !isMaximized())
{
app->config->main_window_width = ((QResizeEvent *)event)->size().width();
app->config->main_window_height = ((QResizeEvent *)event)->size().height();
}
break;
case QEvent::WindowActivate:
if (focus_pause)
{
focus_pause = false;
app->unpause();
}
break;
case QEvent::WindowDeactivate:
if (app->config->pause_emulation_when_unfocused && !focus_pause)
{
focus_pause = true;
app->pause();
}
break;
case QEvent::MouseMove:
if (!cursor_visible)
{
setCursor(QCursor(Qt::ArrowCursor));
cursor_visible = true;
mouse_timer.start();
}
break;
default:
break;
}
return QMainWindow::event(event);
}
void EmuMainWindow::toggleFullscreen()
{
if (isFullScreen())
{
app->config->setVRRConfig(false);
app->updateSettings();
setBypassCompositor(false);
showNormal();
menuBar()->setVisible(true);
}
else
{
if (app->config->adjust_for_vrr)
{
app->config->setVRRConfig(true);
app->updateSettings();
}
showFullScreen();
menuBar()->setVisible(false);
setBypassCompositor(true);
}
}
bool EmuMainWindow::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() != QEvent::KeyPress && event->type() != QEvent::KeyRelease)
return false;
if (watched != this && watched != canvas && !app->binding_callback)
return false;
auto key_event = (QKeyEvent *)event;
if (isFullScreen() && key_event->key() == Qt::Key_Escape)
{
toggleFullscreen();
return true;
}
auto binding = EmuBinding::keyboard(key_event->key(),
key_event->modifiers().testFlag(Qt::ShiftModifier),
key_event->modifiers().testFlag(Qt::AltModifier),
key_event->modifiers().testFlag(Qt::ControlModifier),
key_event->modifiers().testFlag(Qt::MetaModifier));
if ((app->isBound(binding) || app->binding_callback) && !key_event->isAutoRepeat())
{
app->reportBinding(binding, event->type() == QEvent::KeyPress);
event->accept();
return true;
}
return false;
}
std::vector<std::string> EmuMainWindow::getDisplayDeviceList()
{
if (!canvas)
return { "Default" };
return canvas->getDeviceList();
}
void EmuMainWindow::pauseContinue()
{
if (manual_pause)
{
manual_pause = false;
app->unpause();
}
else
{
manual_pause = true;
app->pause();
}
}
bool EmuMainWindow::isActivelyDrawing()
{
return (!app->isPaused() && app->core->active);
}

55
qt/src/EmuMainWindow.hpp Normal file
View File

@ -0,0 +1,55 @@
#ifndef __EMU_MAIN_WINDOW_HPP
#define __EMU_MAIN_WINDOW_HPP
#include <QMainWindow>
#include <QTimer>
#include "EmuCanvas.hpp"
class EmuApplication;
class EmuMainWindow : public QMainWindow
{
public:
EmuMainWindow(EmuApplication *app);
~EmuMainWindow();
void toggleFullscreen();
void createCanvas();
void destroyCanvas();
void recreateCanvas();
void setBypassCompositor(bool);
void setCoreActionsEnabled(bool);
bool event(QEvent *event) override;
bool eventFilter(QObject *, QEvent *event) override;
void resizeToMultiple(int multiple);
void populateRecentlyUsed();
void chooseState(bool save);
void pauseContinue();
bool isActivelyDrawing();
void openFile();
bool openFile(std::string filename);
std::vector<std::string> getDisplayDeviceList();
EmuApplication *app;
EmuCanvas *canvas;
private:
void idle();
void createWidgets();
static const size_t recent_menu_size = 10;
static const size_t state_items_size = 10;
bool manual_pause = false;
bool focus_pause = false;
bool using_stacked_widget = false;
QMenu *load_state_menu;
QMenu *save_state_menu;
QMenu *recent_menu;
QTimer mouse_timer;
bool cursor_visible = true;
QAction *shader_settings_item;
std::vector<QAction *> core_actions;
std::vector<QAction *> recent_menu_items;
};
#endif

View File

@ -0,0 +1,57 @@
#include "EmuSettingsWindow.hpp"
#include "src/EmuMainWindow.hpp"
#include <QScrollArea>
EmuSettingsWindow::EmuSettingsWindow(QWidget *parent, EmuApplication *app)
: QDialog(parent), app(app)
{
setupUi(this);
general_panel = new GeneralPanel(app);
stackedWidget->addWidget(general_panel);
QScrollArea *area = new QScrollArea(stackedWidget);
area->setWidgetResizable(true);
area->setFrameStyle(0);
display_panel = new DisplayPanel(app);
area->setWidget(display_panel);
stackedWidget->addWidget(area);
sound_panel = new SoundPanel(app);
stackedWidget->addWidget(sound_panel);
emulation_panel = new EmulationPanel(app);
stackedWidget->addWidget(emulation_panel);
controller_panel = new ControllerPanel(app);
stackedWidget->addWidget(controller_panel);
shortcuts_panel = new ShortcutsPanel(app);
stackedWidget->addWidget(shortcuts_panel);
folders_panel = new FoldersPanel(app);
stackedWidget->addWidget(folders_panel);
stackedWidget->setCurrentIndex(0);
closeButton->connect(closeButton, &QPushButton::clicked, [&](bool) {
this->close();
});
panelList->connect(panelList, &QListWidget::currentItemChanged, [&](QListWidgetItem *prev, QListWidgetItem *cur) {
stackedWidget->setCurrentIndex(panelList->currentRow());
});
}
EmuSettingsWindow::~EmuSettingsWindow()
{
}
void EmuSettingsWindow::show(int page)
{
panelList->setCurrentRow(page);
stackedWidget->setCurrentIndex(page);
if (!isVisible())
open();
}

View File

@ -0,0 +1,30 @@
#pragma once
#include "ui_EmuSettingsWindow.h"
#include "EmuApplication.hpp"
#include "GeneralPanel.hpp"
#include "DisplayPanel.hpp"
#include "SoundPanel.hpp"
#include "EmulationPanel.hpp"
#include "ControllerPanel.hpp"
#include "FoldersPanel.hpp"
#include "ShortcutsPanel.hpp"
class EmuSettingsWindow
: public QDialog,
public Ui::EmuSettingsWindow
{
public:
EmuSettingsWindow(QWidget *parent, EmuApplication *app);
~EmuSettingsWindow();
void show(int page);
EmuApplication *app;
GeneralPanel *general_panel;
DisplayPanel *display_panel;
SoundPanel *sound_panel;
EmulationPanel *emulation_panel;
ControllerPanel *controller_panel;
ShortcutsPanel *shortcuts_panel;
FoldersPanel *folders_panel;
};

158
qt/src/EmuSettingsWindow.ui Normal file
View File

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EmuSettingsWindow</class>
<widget class="QDialog" name="EmuSettingsWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>933</width>
<height>719</height>
</rect>
</property>
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QListWidget" name="panelList">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContents</enum>
</property>
<property name="resizeMode">
<enum>QListView::Fixed</enum>
</property>
<item>
<property name="text">
<string>General</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/settings.svg</normaloff>:/icons/blackicons/settings.svg</iconset>
</property>
</item>
<item>
<property name="text">
<string>Display</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/display.svg</normaloff>:/icons/blackicons/display.svg</iconset>
</property>
</item>
<item>
<property name="text">
<string>Sound</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/sound.svg</normaloff>:/icons/blackicons/sound.svg</iconset>
</property>
</item>
<item>
<property name="text">
<string>Emulation</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/emulation.svg</normaloff>:/icons/blackicons/emulation.svg</iconset>
</property>
</item>
<item>
<property name="text">
<string>Controllers</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/joypad.svg</normaloff>:/icons/blackicons/joypad.svg</iconset>
</property>
</item>
<item>
<property name="text">
<string>Shortcuts</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/keyboard.svg</normaloff>:/icons/blackicons/keyboard.svg</iconset>
</property>
</item>
<item>
<property name="text">
<string>Files</string>
</property>
<property name="icon">
<iconset resource="resources/snes9x.qrc">
<normaloff>:/icons/blackicons/folders.svg</normaloff>:/icons/blackicons/folders.svg</iconset>
</property>
</item>
</widget>
</item>
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="defaultsButton">
<property name="text">
<string>Restore Defaults</string>
</property>
<property name="icon">
<iconset theme="view-refresh">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="closeButton">
<property name="text">
<string>Close</string>
</property>
<property name="icon">
<iconset theme="window-close">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="resources/snes9x.qrc"/>
</resources>
<connections/>
</ui>

67
qt/src/EmulationPanel.cpp Normal file
View File

@ -0,0 +1,67 @@
#include "EmulationPanel.hpp"
EmulationPanel::EmulationPanel(EmuApplication *app_)
: app(app_)
{
setupUi(this);
auto connect_checkbox = [&](QCheckBox *box, bool *config) {
QObject::connect(box, &QCheckBox::clicked, [&, box, config](bool is_checked) {
*config = is_checked;
app->updateSettings();
});
};
auto connect_spin = [&](QSpinBox *box, int *config) {
QObject::connect(box, &QSpinBox::valueChanged, [&, box, config](int value) {
*config = value;
app->updateSettings();
});
};
auto connect_combo = [&](QComboBox *box, int *config) {
QObject::connect(box, &QComboBox::activated, [&, box, config](int index) {
*config = index;
app->updateSettings();
});
};
connect_combo(comboBox_speed_control_method, &app->config->speed_sync_method);
QObject::connect(doubleSpinBox_frame_rate, &QDoubleSpinBox::valueChanged, [&](double value) {
app->config->fixed_frame_rate = value;
});
connect_spin(spinBox_rewind_buffer_size, &app->config->rewind_buffer_size);
connect_spin(spinBox_rewind_frames, &app->config->rewind_frame_interval);
connect_spin(spinBox_fast_forward_skip_frames, &app->config->fast_forward_skip_frames);
connect_checkbox(checkBox_allow_invalid_vram_access, &app->config->allow_invalid_vram_access);
connect_checkbox(checkBox_allow_opposing_dpad_directions, &app->config->allow_opposing_dpad_directions);
connect_checkbox(checkBox_overclock, &app->config->overclock);
connect_checkbox(checkBox_remove_sprite_limit, &app->config->remove_sprite_limit);
connect_checkbox(checkBox_use_shadow_echo_buffer, &app->config->enable_shadow_buffer);
connect_spin(spinBox_superfx_clock_speed, &app->config->superfx_clock_multiplier);
connect_combo(comboBox_sound_filter, &app->config->sound_filter);
}
EmulationPanel::~EmulationPanel()
{
}
void EmulationPanel::showEvent(QShowEvent *event)
{
auto &config = app->config;
comboBox_speed_control_method->setCurrentIndex(config->speed_sync_method);
doubleSpinBox_frame_rate->setValue(config->fixed_frame_rate);
spinBox_fast_forward_skip_frames->setValue(config->fast_forward_skip_frames);
spinBox_rewind_buffer_size->setValue(config->rewind_buffer_size);
spinBox_rewind_frames->setValue(config->rewind_frame_interval);
checkBox_allow_invalid_vram_access->setChecked(config->allow_invalid_vram_access);
checkBox_allow_opposing_dpad_directions->setChecked(config->allow_opposing_dpad_directions);
checkBox_overclock->setChecked(config->overclock);
checkBox_remove_sprite_limit->setChecked(config->remove_sprite_limit);
checkBox_use_shadow_echo_buffer->setChecked(config->enable_shadow_buffer);
spinBox_superfx_clock_speed->setValue(config->superfx_clock_multiplier);
comboBox_sound_filter->setCurrentIndex(config->sound_filter);
QWidget::showEvent(event);
}

15
qt/src/EmulationPanel.hpp Normal file
View File

@ -0,0 +1,15 @@
#pragma once
#include "ui_EmulationPanel.h"
#include "EmuApplication.hpp"
class EmulationPanel :
public Ui::EmulationPanel,
public QWidget
{
public:
EmulationPanel(EmuApplication *app);
~EmulationPanel();
void showEvent(QShowEvent *event) override;
EmuApplication *app;
};

378
qt/src/EmulationPanel.ui Normal file
View File

@ -0,0 +1,378 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EmulationPanel</class>
<widget class="QWidget" name="EmulationPanel">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>862</width>
<height>756</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Emulation Speed</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Speed control method:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_speed_control_method">
<item>
<property name="text">
<string>Timer - Match the frame rate configured below</string>
</property>
</item>
<item>
<property name="text">
<string>Timer with Frame Skipping - Skip frames if needed</string>
</property>
</item>
<item>
<property name="text">
<string>Sound Synchronization - Wait on the sound buffer</string>
</property>
</item>
<item>
<property name="text">
<string>None - Run unthrottled, unless vsync is turned on</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Frame rate:</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="doubleSpinBox_frame_rate">
<property name="suffix">
<string> fps</string>
</property>
<property name="decimals">
<number>4</number>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>120.000000000000000</double>
</property>
<property name="value">
<double>0.000000000000000</double>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Set frame rate to 0 to automatically change based on PAL/NTSC and with interlacing on and off.</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>Fast-forward frame skip:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_fast_forward_skip_frames">
<property name="suffix">
<string> frames</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>15</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Rewind</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Rewind buffer size (0 to disable):</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="spinBox_rewind_frames"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Frames between steps</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="spinBox_rewind_buffer_size">
<property name="suffix">
<string> MB</string>
</property>
</widget>
</item>
<item row="0" column="2">
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Hacks</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QCheckBox" name="checkBox_allow_invalid_vram_access">
<property name="text">
<string>Allow invalid VRAM access</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="checkBox_overclock">
<property name="text">
<string>Overclock the CPU</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="checkBox_use_shadow_echo_buffer">
<property name="text">
<string>Use a shadow echo buffer</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="checkBox_allow_opposing_dpad_directions">
<property name="text">
<string>Allow opposite D-pad directions simultaneously</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="checkBox_remove_sprite_limit">
<property name="text">
<string>Remove the sprite limit</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>SuperFX clock speed:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_superfx_clock_speed">
<property name="suffix">
<string>%</string>
</property>
<property name="minimum">
<number>50</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
<property name="singleStep">
<number>10</number>
</property>
<property name="value">
<number>100</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>Alternate sound filter:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_sound_filter">
<item>
<property name="text">
<string>Nearest</string>
</property>
</item>
<item>
<property name="text">
<string>Linear</string>
</property>
</item>
<item>
<property name="text">
<string>Gaussian</string>
</property>
</item>
<item>
<property name="text">
<string>Cubic</string>
</property>
</item>
<item>
<property name="text">
<string>Sinc</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

68
qt/src/FoldersPanel.cpp Normal file
View File

@ -0,0 +1,68 @@
#include "FoldersPanel.hpp"
#include <QSpinBox>
#include <QFileDialog>
FoldersPanel::FoldersPanel(EmuApplication *app_)
: app(app_)
{
setupUi(this);
connectEntry(comboBox_sram, lineEdit_sram, pushButton_sram, &app->config->sram_location, &app->config->sram_folder);
connectEntry(comboBox_state, lineEdit_state, pushButton_state, &app->config->state_location, &app->config->state_folder);
connectEntry(comboBox_cheat, lineEdit_cheat, pushButton_cheat, &app->config->cheat_location, &app->config->cheat_folder);
connectEntry(comboBox_patch, lineEdit_patch, pushButton_patch, &app->config->patch_location, &app->config->patch_folder);
connectEntry(comboBox_export, lineEdit_export, pushButton_export, &app->config->export_location, &app->config->export_folder);
}
FoldersPanel::~FoldersPanel()
{
}
void FoldersPanel::connectEntry(QComboBox *combo, QLineEdit *lineEdit, QPushButton *browse, int *location, std::string *folder)
{
auto config = app->config.get();
QObject::connect(combo, &QComboBox::activated, [=](int index) {
*location = index;
refreshEntry(combo, lineEdit, browse, location, folder);
app->updateSettings();
});
QObject::connect(browse, &QPushButton::pressed, [=] {
QFileDialog dialog(this, tr("Select a Folder"));
dialog.setFileMode(QFileDialog::Directory);
dialog.setDirectory(QString::fromUtf8(*folder));
if (!dialog.exec())
return;
*folder = dialog.selectedFiles().at(0).toUtf8();
lineEdit->setText(QString::fromUtf8(*folder));
app->updateSettings();
});
}
void FoldersPanel::refreshData()
{
refreshEntry(comboBox_sram, lineEdit_sram, pushButton_sram, &app->config->sram_location, &app->config->sram_folder);
refreshEntry(comboBox_state, lineEdit_state, pushButton_state, &app->config->state_location, &app->config->state_folder);
refreshEntry(comboBox_cheat, lineEdit_cheat, pushButton_cheat, &app->config->cheat_location, &app->config->cheat_folder);
refreshEntry(comboBox_patch, lineEdit_patch, pushButton_patch, &app->config->patch_location, &app->config->patch_folder);
refreshEntry(comboBox_export, lineEdit_export, pushButton_export, &app->config->export_location, &app->config->export_folder);
}
void FoldersPanel::refreshEntry(QComboBox *combo, QLineEdit *lineEdit, QPushButton *browse, int *location, std::string *folder)
{
bool custom = (*location == EmuConfig::eCustomDirectory);
combo->setCurrentIndex(*location);
lineEdit->setText(custom ? QString::fromUtf8(*folder) : "");
lineEdit->setEnabled(custom);
browse->setEnabled(custom);
}
void FoldersPanel::showEvent(QShowEvent *event)
{
refreshData();
QWidget::showEvent(event);
}

17
qt/src/FoldersPanel.hpp Normal file
View File

@ -0,0 +1,17 @@
#pragma once
#include "ui_FoldersPanel.h"
#include "EmuApplication.hpp"
class FoldersPanel :
public Ui::FoldersPanel,
public QWidget
{
public:
FoldersPanel(EmuApplication *app);
~FoldersPanel();
void showEvent(QShowEvent *event) override;
void connectEntry(QComboBox *combo, QLineEdit *lineEdit, QPushButton *browse, int *location, std::string *folder);
void refreshEntry(QComboBox *combo, QLineEdit *lineEdit, QPushButton *browse, int *location, std::string *folder);
void refreshData();
EmuApplication *app;
};

282
qt/src/FoldersPanel.ui Normal file
View File

@ -0,0 +1,282 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FoldersPanel</class>
<widget class="QWidget" name="FoldersPanel">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>779</width>
<height>673</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Data Locations</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="5" column="2">
<widget class="QComboBox" name="comboBox_export">
<item>
<property name="text">
<string>ROM Folder</string>
</property>
</item>
<item>
<property name="text">
<string>Config Folder</string>
</property>
</item>
<item>
<property name="text">
<string>Custom Folder</string>
</property>
</item>
</widget>
</item>
<item row="1" column="2">
<widget class="QComboBox" name="comboBox_state">
<item>
<property name="text">
<string>ROM Folder</string>
</property>
</item>
<item>
<property name="text">
<string>Config Folder</string>
</property>
</item>
<item>
<property name="text">
<string>Custom Folder</string>
</property>
</item>
</widget>
</item>
<item row="0" column="2">
<widget class="QComboBox" name="comboBox_sram">
<item>
<property name="text">
<string>ROM Folder</string>
</property>
</item>
<item>
<property name="text">
<string>Config Folder</string>
</property>
</item>
<item>
<property name="text">
<string>Custom Folder</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Save states</string>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QPushButton" name="pushButton_sram">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label">
<property name="text">
<string>SRAM</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QLineEdit" name="lineEdit_sram">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QComboBox" name="comboBox_patch">
<item>
<property name="text">
<string>ROM Folder</string>
</property>
</item>
<item>
<property name="text">
<string>Config Folder</string>
</property>
</item>
<item>
<property name="text">
<string>Custom Folder</string>
</property>
</item>
</widget>
</item>
<item row="3" column="2">
<widget class="QComboBox" name="comboBox_cheat">
<item>
<property name="text">
<string>ROM Folder</string>
</property>
</item>
<item>
<property name="text">
<string>Config Folder</string>
</property>
</item>
<item>
<property name="text">
<string>Custom Folder</string>
</property>
</item>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Cheats</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Patches</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Exports</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QLineEdit" name="lineEdit_state">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="3">
<widget class="QLineEdit" name="lineEdit_cheat">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="3">
<widget class="QLineEdit" name="lineEdit_patch">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="3">
<widget class="QLineEdit" name="lineEdit_export">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="4">
<widget class="QPushButton" name="pushButton_state">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item row="3" column="4">
<widget class="QPushButton" name="pushButton_cheat">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item row="4" column="4">
<widget class="QPushButton" name="pushButton_patch">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item row="5" column="4">
<widget class="QPushButton" name="pushButton_export">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>SRAM</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Save SRAM to disk every:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_sram_interval">
<property name="suffix">
<string> seconds</string>
</property>
<property name="maximum">
<number>1000</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

44
qt/src/GeneralPanel.cpp Normal file
View File

@ -0,0 +1,44 @@
#include "GeneralPanel.hpp"
GeneralPanel::GeneralPanel(EmuApplication *app_)
: app(app_)
{
setupUi(this);
auto connectCheckbox = [&](QCheckBox *box, bool *config)
{
QObject::connect(box, &QCheckBox::clicked, [&, config](bool checked)
{
*config = checked;
app->updateSettings();
});
};
connectCheckbox(checkBox_fullscreen_on_open, &app->config->fullscreen_on_open);
connectCheckbox(checkBox_disable_screensaver, &app->config->disable_screensaver);
connectCheckbox(checkBox_pause_when_unfocused, &app->config->pause_emulation_when_unfocused);
connectCheckbox(checkBox_show_frame_rate, &app->config->show_frame_rate);
connectCheckbox(checkBox_show_indicators, &app->config->show_indicators);
connectCheckbox(checkBox_show_pressed_keys, &app->config->show_pressed_keys);
connectCheckbox(checkBox_show_time, &app->config->show_time);
}
GeneralPanel::~GeneralPanel()
{
}
void GeneralPanel::showEvent(QShowEvent *event)
{
auto &config = app->config;
checkBox_fullscreen_on_open->setChecked(config->fullscreen_on_open);
checkBox_disable_screensaver->setChecked(config->disable_screensaver);
checkBox_disable_screensaver->setVisible(false);
checkBox_pause_when_unfocused->setChecked(config->pause_emulation_when_unfocused);
checkBox_show_frame_rate->setChecked(config->show_frame_rate);
checkBox_show_indicators->setChecked(config->show_indicators);
checkBox_show_pressed_keys->setChecked(config->show_pressed_keys);
checkBox_show_time->setChecked(config->show_time);
QWidget::showEvent(event);
}

15
qt/src/GeneralPanel.hpp Normal file
View File

@ -0,0 +1,15 @@
#pragma once
#include "ui_GeneralPanel.h"
#include "EmuApplication.hpp"
class GeneralPanel :
public Ui::GeneralPanel,
public QWidget
{
public:
GeneralPanel(EmuApplication *app);
~GeneralPanel();
void showEvent(QShowEvent *event) override;
EmuApplication *app;
};

101
qt/src/GeneralPanel.ui Normal file
View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GeneralPanel</class>
<widget class="QWidget" name="GeneralPanel">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>752</width>
<height>615</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>General</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="checkBox_fullscreen_on_open">
<property name="text">
<string>Switch to fullscreen after opening a ROM</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_disable_screensaver">
<property name="text">
<string>Disable screensaver</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_pause_when_unfocused">
<property name="text">
<string>Pause emulation when window is not focused</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Overlays</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QCheckBox" name="checkBox_show_frame_rate">
<property name="text">
<string>Show frame rate</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_show_indicators">
<property name="text">
<string>Show fast-forward and pause icons</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_show_pressed_keys">
<property name="text">
<string>Show pressed keys</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_show_time">
<property name="text">
<string>Show the time</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

229
qt/src/SDLInputManager.cpp Normal file
View File

@ -0,0 +1,229 @@
#include "SDLInputManager.hpp"
#include "SDL.h"
#include "SDL_events.h"
#include "SDL_gamecontroller.h"
#include "SDL_joystick.h"
#include <algorithm>
#include <optional>
SDLInputManager::SDLInputManager()
{
SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK);
}
SDLInputManager::~SDLInputManager()
{
}
void SDLInputManager::AddDevice(int device_index)
{
SDLInputDevice d;
if (!d.open(device_index))
return;
d.index = FindFirstOpenIndex();
printf("Slot %d: %s: ", d.index, SDL_JoystickName(d.joystick));
printf("%zu axes, %zu buttons, %zu hats, %s API\n", d.axes.size(), d.buttons.size(), d.hats.size(), d.is_controller ? "Controller" : "Joystick");
devices.insert({ d.instance_id, d });
}
void SDLInputManager::RemoveDevice(int instance_id)
{
auto iter = devices.find(instance_id);
if (iter == devices.end())
return;
auto &d = iter->second;
if (d.is_controller)
SDL_GameControllerClose(d.controller);
else
SDL_JoystickClose(d.joystick);
devices.erase(iter);
return;
}
void SDLInputManager::ClearEvents()
{
std::optional<SDL_Event> event;
while ((event = ProcessEvent()))
{
}
}
std::optional<SDLInputManager::DiscreteHatEvent>
SDLInputManager::DiscretizeHatEvent(SDL_Event &event)
{
auto &device = devices.at(event.jhat.which);
auto &hat = event.jhat.hat;
auto new_state = event.jhat.value;
auto &old_state = device.hats[hat].state;
if (old_state == new_state)
return std::nullopt;
DiscreteHatEvent dhe{};
dhe.hat = hat;
dhe.joystick_num = device.index;
for (auto &s : { SDL_HAT_UP, SDL_HAT_DOWN, SDL_HAT_LEFT, SDL_HAT_RIGHT })
if ((old_state & s) != (new_state & s))
{
printf(" old: %d, new: %d\n", old_state, new_state);
dhe.direction = s;
dhe.pressed = (new_state & s);
old_state = new_state;
return dhe;
}
return std::nullopt;
}
std::optional<SDLInputManager::DiscreteAxisEvent>
SDLInputManager::DiscretizeJoyAxisEvent(SDL_Event &event)
{
auto &device = devices.at(event.jaxis.which);
auto &axis = event.jaxis.axis;
auto now = event.jaxis.value;
auto &then = device.axes[axis].last;
auto center = device.axes[axis].initial;
int offset = now - center;
auto pressed = [&](int axis) -> int {
if (axis > (center + (32767 - center) / 3)) // TODO threshold
return 1;
if (axis < (center - (center + 32768) / 3)) // TODO threshold
return -1;
return 0;
};
auto was_pressed_then = pressed(then);
auto is_pressed_now = pressed(now);
if (was_pressed_then == is_pressed_now)
{
then = now;
return std::nullopt;
}
DiscreteAxisEvent dae;
dae.axis = axis;
dae.direction = is_pressed_now ? is_pressed_now : was_pressed_then;
dae.pressed = (is_pressed_now != 0);
dae.joystick_num = device.index;
then = now;
return dae;
}
std::optional<SDL_Event> SDLInputManager::ProcessEvent()
{
SDL_Event event{};
while (SDL_PollEvent(&event))
{
switch (event.type)
{
case SDL_JOYAXISMOTION:
return event;
case SDL_JOYHATMOTION:
return event;
case SDL_JOYBUTTONUP:
case SDL_JOYBUTTONDOWN:
return event;
case SDL_JOYDEVICEADDED:
AddDevice(event.jdevice.which);
return event;
case SDL_JOYDEVICEREMOVED:
RemoveDevice(event.jdevice.which);
return event;
}
}
return std::nullopt;
}
void SDLInputManager::PrintDevices()
{
for (auto &pair : devices)
{
auto &d = pair.second;
printf("%s: \n", SDL_JoystickName(d.joystick));
printf(" Index: %d\n"
" Instance ID: %d\n"
" Controller %s\n"
" SDL Joystick Number: %d\n",
d.index,
d.instance_id,
d.is_controller ? "yes" : "no",
d.sdl_joystick_number);
}
}
int SDLInputManager::FindFirstOpenIndex()
{
for (int i = 0;; i++)
{
if (std::none_of(devices.begin(), devices.end(), [i](auto &d) -> bool {
return (d.second.index == i);
}))
return i;
}
return -1;
}
bool SDLInputDevice::open(int joystick_num)
{
sdl_joystick_number = joystick_num;
is_controller = SDL_IsGameController(joystick_num);
if (is_controller)
{
controller = SDL_GameControllerOpen(joystick_num);
joystick = SDL_GameControllerGetJoystick(controller);
}
else
{
joystick = SDL_JoystickOpen(joystick_num);
controller = nullptr;
}
if (!joystick)
return false;
auto num_axes = SDL_JoystickNumAxes(joystick);
axes.resize(num_axes);
for (int i = 0; i < num_axes; i++)
{
SDL_JoystickGetAxisInitialState(joystick, i, &axes[i].initial);
axes[i].last = axes[i].initial;
}
buttons.resize(SDL_JoystickNumButtons(joystick));
hats.resize(SDL_JoystickNumHats(joystick));
instance_id = SDL_JoystickInstanceID(joystick);
return true;
}
std::vector<std::pair<int, std::string>> SDLInputManager::getXInputControllers()
{
std::vector<std::pair<int, std::string>> list;
for (auto &d : devices)
{
if (!d.second.is_controller)
continue;
list.push_back(std::pair<int, std::string>(d.first, SDL_JoystickName(d.second.joystick)));
auto bind = SDL_GameControllerGetBindForButton(d.second.controller, SDL_CONTROLLER_BUTTON_A);
}
return list;
}

View File

@ -0,0 +1,68 @@
#pragma once
#include "SDL.h"
#include <map>
#include <vector>
#include <string>
#include <optional>
struct SDLInputDevice
{
bool open(int joystick_num);
int index;
int sdl_joystick_number;
bool is_controller;
SDL_GameController *controller = nullptr;
SDL_Joystick *joystick = nullptr;
SDL_JoystickID instance_id;
struct Axis
{
int16_t initial;
int last;
};
std::vector<Axis> axes;
struct Hat
{
uint8_t state;
};
std::vector<Hat> hats;
std::vector<bool> buttons;
};
struct SDLInputManager
{
SDLInputManager();
~SDLInputManager();
std::optional<SDL_Event> ProcessEvent();
std::vector<std::pair<int, std::string>> getXInputControllers();
void ClearEvents();
void AddDevice(int i);
void RemoveDevice(int i);
void PrintDevices();
int FindFirstOpenIndex();
struct DiscreteAxisEvent
{
int joystick_num;
int axis;
int direction;
int pressed;
};
std::optional<DiscreteAxisEvent> DiscretizeJoyAxisEvent(SDL_Event &event);
struct DiscreteHatEvent
{
int joystick_num;
int hat;
int direction;
bool pressed;
};
std::optional<DiscreteHatEvent> DiscretizeHatEvent(SDL_Event &event);
std::map<SDL_JoystickID, SDLInputDevice> devices;
};

View File

@ -0,0 +1,201 @@
#include "ShaderParametersDialog.hpp"
#include <QLayout>
#include <QTableWidget>
#include <QLabel>
#include <QSizePolicy>
#include <QPushButton>
#include <QSpacerItem>
#include <QScrollArea>
#include <QFileDialog>
static bool is_simple(const EmuCanvas::Parameter &p)
{
return (p.min == 0.0 && p.max == 1.0 && p.step == 1.0);
}
static bool is_pointless(const EmuCanvas::Parameter &p)
{
return (p.min == p.max);
}
ShaderParametersDialog::ShaderParametersDialog(EmuCanvas *parent_, std::vector<EmuCanvas::Parameter> *parameters_)
: QDialog(parent_), canvas(parent_), config(parent_->config), parameters(parameters_)
{
setWindowTitle(tr("Shader Parameters"));
setMinimumSize(600, 200);
auto layout = new QVBoxLayout(this);
auto scroll_area = new QScrollArea(this);
scroll_area->setFrameShape(QFrame::Shape::StyledPanel);
scroll_area->setWidgetResizable(true);
auto scroll_area_widget_contents = new QWidget();
scroll_area_widget_contents->setGeometry(0, 0, 400, 300);
auto grid = new QGridLayout(scroll_area_widget_contents);
scroll_area->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
auto buttonbox = new QHBoxLayout();
for (int i = 0; i < parameters->size(); i++)
{
auto &p = (*parameters)[i];
QSlider *slider = nullptr;
QDoubleSpinBox *spinbox = nullptr;
QCheckBox *checkbox = nullptr;
auto label = new QLabel(p.name.c_str(), scroll_area_widget_contents);
grid->addWidget(label, i, 0, 1, 1);
if (is_pointless(p))
{
widgets.push_back({});
continue;
}
if (is_simple(p))
{
checkbox = new QCheckBox(scroll_area_widget_contents);
checkbox->setChecked(p.val == 1.0);
grid->addWidget(checkbox, i, 1, 1, 2);
QObject::connect(checkbox, &QCheckBox::clicked, [&, i](bool checked) {
(*parameters)[i].val = checked ? 1.0 : 0.0;
});
widgets.push_back({ slider, spinbox, checkbox });
continue;
}
slider = new QSlider(scroll_area_widget_contents);
grid->addWidget(slider, i, 1, 1, 1);
slider->setOrientation(Qt::Horizontal);
slider->setTickInterval(1);
slider->setRange(0, (p.max - p.min) / p.step);
slider->setValue(p.val / p.step);
spinbox = new QDoubleSpinBox(scroll_area_widget_contents);
grid->addWidget(spinbox, i, 2, 1, 1);
spinbox->setDecimals(p.significant_digits > 5 ? 5 : p.significant_digits);
spinbox->setRange(p.min, p.max);
spinbox->setSingleStep(p.step);
spinbox->setValue(p.val);
QObject::connect(slider, &QSlider::valueChanged, [&, i, slider, spinbox](int value) {
auto &p = (*parameters)[i];
double new_value = value * p.step + p.min;
spinbox->blockSignals(true);
spinbox->setValue(new_value);
spinbox->blockSignals(false);
p.val = new_value;
});
QObject::connect(spinbox, &QDoubleSpinBox::valueChanged, [&, i, slider, spinbox](double value) {
auto &p = (*parameters)[i];
int steps = round((value - p.min) / p.step);
p.val = steps * p.step + p.min;
slider->blockSignals(true);
slider->setValue(steps);
slider->blockSignals(false);
spinbox->blockSignals(true);
spinbox->setValue(p.val);
spinbox->blockSignals(false);
});
widgets.push_back({ slider, spinbox, checkbox });
}
auto reset = new QPushButton(tr("&Reset"), this);
QObject::connect(reset, &QPushButton::clicked, [&] {
*parameters = saved_parameters;
refreshWidgets();
});
buttonbox->addWidget(reset);
buttonbox->addStretch(1);
auto saveas = new QPushButton(tr("Save &As"), this);
buttonbox->addWidget(saveas);
connect(saveas, &QPushButton::clicked, [&] {
saveAs();
});
auto closebutton = new QPushButton(tr("&Save"), this);
connect(closebutton, &QPushButton::clicked, [&] {
save();
close();
});
buttonbox->addWidget(closebutton);
scroll_area->setWidget(scroll_area_widget_contents);
layout->addWidget(scroll_area);
layout->addLayout(buttonbox, 0);
}
void ShaderParametersDialog::save()
{
if (std::equal(parameters->begin(), parameters->end(), saved_parameters.begin()))
return;
QString shadername(config->shader.c_str());
std::string extension;
if (shadername.endsWith("slangp", Qt::CaseInsensitive))
extension = ".slangp";
else if (shadername.endsWith("glslp", Qt::CaseInsensitive))
extension = ".glslp";
saved_parameters = *parameters;
auto filename = config->findConfigDir() + "/customized_shader" + extension;
canvas->saveParameters(filename);
config->shader = filename;
}
void ShaderParametersDialog::saveAs()
{
auto folder = config->last_shader_folder;
auto filename = QFileDialog::getSaveFileName(this, tr("Save Shader Preset As"), folder.c_str());
canvas->saveParameters(filename.toStdString());
config->shader = filename.toStdString();
}
void ShaderParametersDialog::refreshWidgets()
{
for (size_t i = 0; i < widgets.size(); i++)
{
auto &p = (*parameters)[i];
if (is_pointless(p))
continue;
auto [slider, spinbox, checkbox] = widgets[i];
if (is_simple(p))
{
checkbox->setChecked(p.val == 1.0 ? true : false);
continue;
}
spinbox->blockSignals(true);
spinbox->setValue(p.val);
spinbox->blockSignals(false);
slider->blockSignals(true);
slider->setValue((p.val - p.min) / p.step);
slider->blockSignals(false);
}
}
void ShaderParametersDialog::showEvent(QShowEvent *event)
{
refreshWidgets();
saved_parameters = *parameters;
}
void ShaderParametersDialog::closeEvent(QCloseEvent *event)
{
*parameters = saved_parameters;
}
ShaderParametersDialog::~ShaderParametersDialog()
{
}

View File

@ -0,0 +1,28 @@
#pragma once
#include <QDialog>
#include <QSlider>
#include <QDoubleSpinBox>
#include <QCheckBox>
#include "EmuCanvas.hpp"
#include "EmuConfig.hpp"
class ShaderParametersDialog : public QDialog
{
public:
ShaderParametersDialog(EmuCanvas *parent, std::vector<EmuCanvas::Parameter> *parameters);
~ShaderParametersDialog();
void refreshWidgets();
void showEvent(QShowEvent *event) override;
void closeEvent(QCloseEvent *event) override;
void save();
void saveAs();
std::vector<std::tuple<QSlider *, QDoubleSpinBox *, QCheckBox *>> widgets;
std::vector<EmuCanvas::Parameter> saved_parameters;
std::vector<EmuCanvas::Parameter> *parameters;
EmuCanvas *canvas = nullptr;
EmuConfig *config = nullptr;
};

48
qt/src/ShortcutsPanel.cpp Normal file
View File

@ -0,0 +1,48 @@
#include "ShortcutsPanel.hpp"
#include "EmuConfig.hpp"
ShortcutsPanel::ShortcutsPanel(EmuApplication *app_)
: BindingPanel(app_)
{
setupUi(this);
setTableWidget(tableWidget_shortcuts,
app->config->binding.shortcuts,
app->config->allowed_bindings,
app->config->num_shortcuts);
toolButton_reset_to_default->setPopupMode(QToolButton::InstantPopup);
for (auto slot = 0; slot < app->config->allowed_bindings; slot++)
{
auto action = reset_to_default_menu.addAction(tr("Slot %1").arg(slot));
QObject::connect(action, &QAction::triggered, [&, slot](bool checked) {
setDefaultKeys(slot);
});
}
toolButton_reset_to_default->setMenu(&reset_to_default_menu);
connect(pushButton_clear_all, &QAbstractButton::clicked, [&](bool) {
for (auto &b : app->config->binding.shortcuts)
b = {};
fillTable();
});
fillTable();
}
ShortcutsPanel::~ShortcutsPanel()
{
}
void ShortcutsPanel::setDefaultKeys(int slot)
{
for (int i = 0; i < app->config->num_shortcuts; i++)
{
std::string str = EmuConfig::getDefaultShortcutKeys()[i];
if (!str.empty())
app->config->binding.shortcuts[i * 4 + slot] = EmuBinding::from_config_string(str);
}
fillTable();
}

17
qt/src/ShortcutsPanel.hpp Normal file
View File

@ -0,0 +1,17 @@
#pragma once
#include "ui_ShortcutsPanel.h"
#include "EmuApplication.hpp"
#include "BindingPanel.hpp"
#include <QMenu>
class ShortcutsPanel :
public Ui::ShortcutsPanel,
public BindingPanel
{
public:
ShortcutsPanel(EmuApplication *app);
~ShortcutsPanel();
void setDefaultKeys(int slot);
QMenu reset_to_default_menu;
};

357
qt/src/ShortcutsPanel.ui Normal file
View File

@ -0,0 +1,357 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ShortcutsPanel</class>
<widget class="QWidget" name="ShortcutsPanel">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>797</width>
<height>716</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QToolButton" name="toolButton_reset_to_default">
<property name="text">
<string>Reset to Default</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_clear_all">
<property name="text">
<string>Clear All</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QTableWidget" name="tableWidget_shortcuts">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<row>
<property name="text">
<string>Open ROM</string>
</property>
</row>
<row>
<property name="text">
<string>Enable Fast-Forward</string>
</property>
</row>
<row>
<property name="text">
<string>Toggle Fast-Forward</string>
</property>
</row>
<row>
<property name="text">
<string>Pause/Continue</string>
</property>
</row>
<row>
<property name="text">
<string>Reset</string>
</property>
</row>
<row>
<property name="text">
<string>Power Cycle Console</string>
</property>
</row>
<row>
<property name="text">
<string>Quit</string>
</property>
</row>
<row>
<property name="text">
<string>Toggle Fullscreen</string>
</property>
</row>
<row>
<property name="text">
<string>Save Screenshot</string>
</property>
</row>
<row>
<property name="text">
<string>Save SPC</string>
</property>
</row>
<row>
<property name="text">
<string>Save State to Current Slot</string>
</property>
</row>
<row>
<property name="text">
<string>Load State from Current Slot</string>
</property>
</row>
<row>
<property name="text">
<string>Increase Current Save Slot</string>
</property>
</row>
<row>
<property name="text">
<string>Decrease Current Save Slot</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Save Slot 0</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Save Slot 1</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Save Slot 2</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Save Slot 3</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Save Slot 4</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Save Slot 5</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Save Slot 6</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Save Slot 7</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Save Slot 8</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Save Slot 9</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Load Slot 0</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Load Slot 1</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Load Slot 2</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Load Slot 3</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Load Slot 4</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Load Slot 5</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Load Slot 6</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Load Slot 7</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Load Slot 8</string>
</property>
</row>
<row>
<property name="text">
<string>Quick Load Slot 9</string>
</property>
</row>
<row>
<property name="text">
<string>Rewind</string>
</property>
</row>
<row>
<property name="text">
<string>Grab Mouse</string>
</property>
</row>
<row>
<property name="text">
<string>Swap Controllers 1 and 2</string>
</property>
</row>
<row>
<property name="text">
<string>Toggle BG Layer 0</string>
</property>
</row>
<row>
<property name="text">
<string>Toggle BG Layer 1</string>
</property>
</row>
<row>
<property name="text">
<string>Toggle BG Layer 2</string>
</property>
</row>
<row>
<property name="text">
<string>Toggle BG Layer 3</string>
</property>
</row>
<row>
<property name="text">
<string>Toggle Sprites</string>
</property>
</row>
<row>
<property name="text">
<string>Change Backdrop Color</string>
</property>
</row>
<row>
<property name="text">
<string>Toggle Sound Channel 1</string>
</property>
</row>
<row>
<property name="text">
<string>Toggle Sound Channel 2</string>
</property>
</row>
<row>
<property name="text">
<string>Toggle Sound Channel 3</string>
</property>
</row>
<row>
<property name="text">
<string>Toggle Sound Channel 4</string>
</property>
</row>
<row>
<property name="text">
<string>Toggle Sound Channel 5</string>
</property>
</row>
<row>
<property name="text">
<string>Toggle Sound Channel 6</string>
</property>
</row>
<row>
<property name="text">
<string>Toggle Sound Channel 7</string>
</property>
</row>
<row>
<property name="text">
<string>Toggle Sound Channel 8</string>
</property>
</row>
<row>
<property name="text">
<string>Toggle All Sound Channels</string>
</property>
</row>
<row>
<property name="text">
<string>Start Recording</string>
</property>
</row>
<row>
<property name="text">
<string>Stop Recording</string>
</property>
</row>
<row>
<property name="text">
<string>Seek to Frame</string>
</property>
</row>
<column>
<property name="text">
<string>Binding #1</string>
</property>
</column>
<column>
<property name="text">
<string>Binding #2</string>
</property>
</column>
<column>
<property name="text">
<string>Binding #3</string>
</property>
</column>
<column>
<property name="text">
<string>Binding #4</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

749
qt/src/Snes9xController.cpp Normal file
View File

@ -0,0 +1,749 @@
#include "Snes9xController.hpp"
#include "SoftwareScalers.hpp"
#include <memory>
#include <filesystem>
namespace fs = std::filesystem;
#include "snes9x.h"
#include "memmap.h"
#include "srtc.h"
#include "apu/apu.h"
#include "apu/bapu/snes/snes.hpp"
#include "gfx.h"
#include "snapshot.h"
#include "controls.h"
#include "cheats.h"
#include "movie.h"
#include "display.h"
#include "conffile.h"
#include "statemanager.h"
Snes9xController *g_snes9xcontroller = nullptr;
StateManager g_state_manager;
Snes9xController::Snes9xController()
{
init();
}
Snes9xController::~Snes9xController()
{
deinit();
}
Snes9xController *Snes9xController::get()
{
if (!g_snes9xcontroller)
{
g_snes9xcontroller = new Snes9xController();
}
return g_snes9xcontroller;
}
void Snes9xController::init()
{
Settings.MouseMaster = true;
Settings.SuperScopeMaster = true;
Settings.JustifierMaster = true;
Settings.MultiPlayer5Master = true;
Settings.Transparency = true;
Settings.Stereo = true;
Settings.ReverseStereo = false;
Settings.SixteenBitSound = true;
Settings.StopEmulation = true;
Settings.HDMATimingHack = 100;
Settings.SkipFrames = 0;
Settings.TurboSkipFrames = 9;
Settings.NetPlay = false;
Settings.UpAndDown = false;
Settings.InterpolationMethod = DSP_INTERPOLATION_GAUSSIAN;
Settings.FrameTime = 16639;
Settings.FrameTimeNTSC = 16639;
Settings.FrameTimePAL = 20000;
Settings.DisplayFrameRate = false;
Settings.DisplayTime = false;
Settings.DisplayFrameRate = false;
Settings.DisplayPressedKeys = false;
Settings.DisplayIndicators = true;
Settings.SoundPlaybackRate = 48000;
Settings.SoundInputRate = 32040;
Settings.BlockInvalidVRAMAccess = true;
Settings.SoundSync = false;
Settings.Mute = false;
Settings.DynamicRateControl = false;
Settings.DynamicRateLimit = 5;
Settings.SuperFXClockMultiplier = 100;
Settings.MaxSpriteTilesPerLine = 34;
Settings.OneClockCycle = 6;
Settings.OneSlowClockCycle = 8;
Settings.TwoClockCycles = 12;
Settings.ShowOverscan = false;
Settings.InitialInfoStringTimeout = 120;
CPU.Flags = 0;
rewind_buffer_size = 0;
rewind_frame_interval = 5;
Memory.Init();
S9xInitAPU();
S9xInitSound(0);
S9xSetSamplesAvailableCallback([](void *data) {
((Snes9xController *)data)->SamplesAvailable();
}, this);
S9xGraphicsInit();
S9xInitInputDevices();
S9xUnmapAllControls();
S9xCheatsEnable();
active = false;
}
void Snes9xController::deinit()
{
if (active)
S9xAutoSaveSRAM();
S9xGraphicsDeinit();
S9xDeinitAPU();
}
void Snes9xController::updateSettings(const EmuConfig * const config)
{
Settings.UpAndDown = config->allow_opposing_dpad_directions;
Settings.InterpolationMethod = config->sound_filter;
if (config->fixed_frame_rate == 0.0)
{
Settings.FrameTimeNTSC = 16639;
Settings.FrameTimePAL = 20000;
Settings.FrameTime = Settings.FrameTimeNTSC;
}
else
{
Settings.FrameTimeNTSC = Settings.FrameTimePAL = Settings.FrameTime =
1000000 / config->fixed_frame_rate;
}
Settings.TurboSkipFrames = config->fast_forward_skip_frames;
Settings.DisplayTime = config->show_time;
if (config->display_messages == EmuConfig::eInscreen)
Settings.AutoDisplayMessages = true;
else
Settings.AutoDisplayMessages = false;
Settings.DisplayFrameRate = config->show_frame_rate;
Settings.DisplayPressedKeys = config->show_pressed_keys;
Settings.DisplayIndicators = config->show_indicators;
if (Settings.SoundPlaybackRate != config->playback_rate || Settings.SoundInputRate != config->input_rate)
{
Settings.SoundInputRate = config->input_rate;
Settings.SoundPlaybackRate = config->playback_rate;
S9xUpdateDynamicRate();
}
Settings.BlockInvalidVRAMAccess = !config->allow_invalid_vram_access;
Settings.SoundSync = config->speed_sync_method == EmuConfig::eSoundSync;
Settings.Mute = config->mute_audio;
Settings.DynamicRateControl = config->dynamic_rate_control;
Settings.DynamicRateLimit = config->dynamic_rate_limit * 1000;
Settings.SuperFXClockMultiplier = config->superfx_clock_multiplier;
if (rewind_buffer_size != config->rewind_buffer_size && active)
{
g_state_manager.init(config->rewind_buffer_size * 1048576);
}
rewind_buffer_size = config->rewind_buffer_size;
rewind_frame_interval = config->rewind_frame_interval;
if (config->remove_sprite_limit)
Settings.MaxSpriteTilesPerLine = 128;
else
Settings.MaxSpriteTilesPerLine = 34;
if (!config->overclock)
{
Settings.OneClockCycle = 6;
Settings.OneSlowClockCycle = 8;
Settings.TwoClockCycles = 12;
}
else
{
Settings.OneClockCycle = 2;
Settings.OneSlowClockCycle = 3;
Settings.TwoClockCycles = 4;
}
Settings.ShowOverscan = config->show_overscan;
high_resolution_effect = config->high_resolution_effect;
config_folder = config->findConfigDir();
auto doFolder = [&](int location, std::string &dest, const std::string &src, const char *subfolder_name)
{
if (location == EmuConfig::eROMDirectory)
dest = "";
else if (location == EmuConfig::eConfigDirectory)
dest = config_folder + "/" + subfolder_name;
else
dest = src;
};
doFolder(config->sram_location, sram_folder, config->sram_folder, "sram");
doFolder(config->state_location, state_folder, config->state_folder, "state");
doFolder(config->cheat_location, cheat_folder, config->cheat_folder, "cheat");
doFolder(config->patch_location, patch_folder, config->patch_folder, "patch");
doFolder(config->export_location, export_folder, config->export_folder, "export");
}
bool Snes9xController::openFile(std::string filename)
{
if (active)
S9xAutoSaveSRAM();
active = false;
auto result = Memory.LoadROM(filename.c_str());
if (result)
{
active = true;
Memory.LoadSRAM(S9xGetFilename(".srm", SRAM_DIR).c_str());
}
return active;
}
void Snes9xController::mainLoop()
{
if (!active)
return;
if (rewind_buffer_size > 0)
{
if (rewinding)
{
uint16 joypads[8];
for (int i = 0; i < 8; i++)
joypads[i] = MovieGetJoypad(i);
rewinding = g_state_manager.pop();
for (int i = 0; i < 8; i++)
MovieSetJoypad(i, joypads[i]);
}
else if (IPPU.TotalEmulatedFrames % rewind_frame_interval == 0)
g_state_manager.push();
if (rewinding)
Settings.Mute |= 0x80;
else
Settings.Mute &= ~0x80;
}
S9xMainLoop();
}
void Snes9xController::setPaused(bool paused)
{
Settings.Paused = paused;
}
void Snes9xController::updateSoundBufferLevel(int empty, int total)
{
S9xUpdateDynamicRate(empty, total);
}
bool8 S9xDeinitUpdate(int width, int height)
{
static int last_width = 0;
static int last_height = 0;
int yoffset = 0;
auto &display = Snes9xController::get()->screen_output_function;
if (display == nullptr)
return true;
if (width < 256 || height < 224)
return false;
if (last_height > height)
memset(GFX.Screen + GFX.RealPPL * height, 0, GFX.Pitch * (last_height - height));
last_width = width;
last_height = height;
if (Settings.ShowOverscan)
{
if (height == SNES_HEIGHT)
{
yoffset = -8;
height = SNES_HEIGHT_EXTENDED;
}
if (height == SNES_HEIGHT * 2)
{
yoffset = -16;
height = SNES_HEIGHT_EXTENDED * 2;
}
}
else
{
if (height == SNES_HEIGHT_EXTENDED)
{
yoffset = 7;
height = SNES_HEIGHT;
}
if (height == SNES_HEIGHT_EXTENDED * 2)
{
yoffset = 14;
height = SNES_HEIGHT * 2;
}
}
uint16_t *screen_view = GFX.Screen + (yoffset * (int)GFX.RealPPL);
auto hires_effect = Snes9xController::get()->high_resolution_effect;
if (!Settings.Paused)
{
if (hires_effect == EmuConfig::eScaleUp)
{
S9xForceHires(screen_view, GFX.Pitch, width, height);
last_width = width;
}
else if (hires_effect == EmuConfig::eScaleDown)
{
S9xMergeHires(screen_view, GFX.Pitch, width, height);
last_width = width;
}
}
display(screen_view, width, height, GFX.Pitch, Settings.PAL ? 50.0 : 60.098813);
return true;
}
bool8 S9xContinueUpdate(int width, int height)
{
return S9xDeinitUpdate(width, height);
}
void S9xSyncSpeed()
{
if (Settings.TurboMode)
{
IPPU.FrameSkip++;
if ((IPPU.FrameSkip > Settings.TurboSkipFrames) && !Settings.HighSpeedSeek)
{
IPPU.FrameSkip = 0;
IPPU.SkippedFrames = 0;
IPPU.RenderThisFrame = true;
}
else
{
IPPU.SkippedFrames++;
IPPU.RenderThisFrame = false;
}
return;
}
IPPU.RenderThisFrame = true;
}
void S9xParsePortConfig(ConfigFile&, int)
{
}
std::string S9xGetDirectory(s9x_getdirtype dirtype)
{
std::string dirname;
auto c = Snes9xController::get();
switch (dirtype)
{
case HOME_DIR:
dirname = c->config_folder;
break;
case SNAPSHOT_DIR:
dirname = c->state_folder;
break;
case PATCH_DIR:
dirname = c->patch_folder;
break;
case CHEAT_DIR:
dirname = c->cheat_folder;
break;
case SRAM_DIR:
dirname = c->sram_folder;
break;
case SCREENSHOT_DIR:
case SPC_DIR:
dirname = c->export_folder;
break;
default:
dirname = "";
}
/* Check if directory exists, make it and/or set correct permissions */
if (dirtype != HOME_DIR && dirname != "")
{
fs::path path(dirname);
if (!fs::exists(path))
{
fs::create_directory(path);
}
else if ((fs::status(path).permissions() & fs::perms::owner_write) == fs::perms::none)
{
fs::permissions(path, fs::perms::owner_write, fs::perm_options::add);
}
}
/* Anything else, use ROM filename path */
if (dirname == "" && !Memory.ROMFilename.empty())
{
fs::path path(Memory.ROMFilename);
path.remove_filename();
if (!fs::is_directory(path))
dirname = fs::current_path().u8string();
else
dirname = path.u8string();
}
return dirname;
}
void S9xInitInputDevices()
{
}
void S9xHandlePortCommand(s9xcommand_t, short, short)
{
}
bool S9xPollButton(unsigned int, bool *)
{
return false;
}
void S9xToggleSoundChannel(int c)
{
static int sound_switch = 255;
if (c == 8)
sound_switch = 255;
else
sound_switch ^= 1 << c;
S9xSetSoundControl(sound_switch);
}
std::string S9xGetFilenameInc(std::string e, enum s9x_getdirtype dirtype)
{
fs::path rom_filename(Memory.ROMFilename);
fs::path filename_base(S9xGetDirectory(dirtype));
filename_base /= rom_filename.filename();
fs::path new_filename;
if (e[0] != '.')
e = "." + e;
int i = 0;
do
{
std::string new_extension = std::to_string(i);
while (new_extension.length() < 3)
new_extension = "0" + new_extension;
new_extension += e;
new_filename = filename_base;
new_filename.replace_extension(new_extension);
i++;
} while (fs::exists(new_filename));
return new_filename.u8string();
}
bool8 S9xInitUpdate()
{
return true;
}
void S9xExtraUsage()
{
}
bool8 S9xOpenSoundDevice()
{
return true;
}
bool S9xPollAxis(unsigned int axis, short *value)
{
return true;
}
void S9xParseArg(char *argv[], int &index, int argc)
{
}
void S9xExit()
{
}
bool S9xPollPointer(unsigned int, short *, short *)
{
return false;
}
void Snes9xController::SamplesAvailable()
{
static std::vector<int16_t> data;
if (sound_output_function)
{
int samples = S9xGetSampleCount();
if (data.size() < samples)
data.resize(samples);
S9xMixSamples((uint8_t *)data.data(), samples);
sound_output_function(data.data(), samples);
}
else
{
S9xClearSamples();
}
}
void S9xMessage(int message_class, int type, const char *message)
{
S9xSetInfoString(message);
}
const char *S9xStringInput(const char *prompt)
{
return "";
}
bool8 S9xOpenSnapshotFile(const char *filename, bool8 read_only, STREAM *file)
{
if (read_only)
{
if ((*file = OPEN_STREAM(filename, "rb")))
return (true);
else
fprintf(stderr, "Failed to open file stream for reading.\n");
}
else
{
if ((*file = OPEN_STREAM(filename, "wb")))
{
return (true);
}
else
{
fprintf(stderr, "Couldn't open stream with zlib.\n");
}
}
fprintf(stderr, "Couldn't open snapshot file:\n%s\n", filename);
return false;
}
void S9xCloseSnapshotFile(STREAM file)
{
CLOSE_STREAM(file);
}
void S9xAutoSaveSRAM()
{
Memory.SaveSRAM(S9xGetFilename(".srm", SRAM_DIR).c_str());
S9xSaveCheatFile(S9xGetFilename(".cht", CHEAT_DIR).c_str());
}
bool Snes9xController::acceptsCommand(const char *command)
{
auto cmd = S9xGetCommandT(command);
return !(cmd.type == S9xNoMapping || cmd.type == S9xBadMapping);
}
void Snes9xController::updateBindings(const EmuConfig *const config)
{
const char *snes9x_names[] = {
"Up",
"Down",
"Left",
"Right",
"A",
"B",
"X",
"Y",
"L",
"R",
"Start",
"Select",
"Turbo A",
"Turbo B",
"Turbo X",
"Turbo Y",
"Turbo L",
"Turbo R",
};
S9xUnmapAllControls();
S9xSetController(0, CTL_JOYPAD, 0, 0, 0, 0);
for (int controller_number = 0; controller_number < 5; controller_number++)
{
auto &controller = config->binding.controller[controller_number];
for (int i = 0; i < config->num_controller_bindings; i++)
{
for (int b = 0; b < config->allowed_bindings; b++)
{
auto binding = controller.buttons[i * config->allowed_bindings + b];
if (binding.hash() == 0)
continue;
std::string name = "Joypad" +
std::to_string(controller_number + 1) + " " +
snes9x_names[i];
auto cmd = S9xGetCommandT(name.c_str());
S9xMapButton(binding.hash(), cmd, false);
}
}
}
for (int i = 0; i < config->num_shortcuts; i++)
{
auto command = S9xGetCommandT(EmuConfig::getShortcutNames()[i]);
if (command.type == S9xNoMapping)
continue;
for (int b = 0; b < 4; b++)
{
auto binding = config->binding.shortcuts[i * 4 + b];
if (binding.type != 0)
S9xMapButton(binding.hash(), command, false);
}
}
}
void Snes9xController::reportBinding(EmuBinding b, bool active)
{
S9xReportButton(b.hash(), active);
}
static fs::path save_slot_path(int slot)
{
std::string extension = std::to_string(slot);
while (extension.length() < 3)
extension = "0" + extension;
fs::path path(S9xGetDirectory(SNAPSHOT_DIR));
path /= fs::path(Memory.ROMFilename).filename();
path.replace_extension(extension);
return path;
}
void Snes9xController::loadUndoState()
{
S9xUnfreezeGame(S9xGetFilename(".undo", SNAPSHOT_DIR).c_str());
}
std::string Snes9xController::getStateFolder()
{
return S9xGetDirectory(SNAPSHOT_DIR);
}
bool Snes9xController::loadState(int slot)
{
return loadState(save_slot_path(slot).u8string());
}
bool Snes9xController::loadState(std::string filename)
{
if (!active)
return false;
S9xFreezeGame(S9xGetFilename(".undo", SNAPSHOT_DIR).c_str());
if (S9xUnfreezeGame(filename.c_str()))
{
auto info_string = filename + " loaded";
S9xSetInfoString(info_string.c_str());
return true;
}
else
{
fprintf(stderr, "Failed to load state file: %s\n", filename.c_str());
return false;
}
}
bool Snes9xController::saveState(std::string filename)
{
if (!active)
return false;
if (S9xFreezeGame(filename.c_str()))
{
auto info_string = filename + " saved";
S9xSetInfoString(info_string.c_str());
return true;
}
else
{
fprintf(stderr, "Couldn't save state file: %s\n", filename.c_str());
return false;
}
}
void Snes9xController::mute(bool muted)
{
Settings.Mute = muted;
}
bool Snes9xController::isAbnormalSpeed()
{
return (Settings.TurboMode || rewinding);
}
void Snes9xController::reset()
{
S9xReset();
}
void Snes9xController::softReset()
{
S9xSoftReset();
}
bool Snes9xController::saveState(int slot)
{
return saveState(save_slot_path(slot).u8string());
}
void Snes9xController::setMessage(std::string message)
{
S9xSetInfoString(message.c_str());
}

View File

@ -0,0 +1,64 @@
#ifndef __SNES9X_CONTROLLER_HPP
#define __SNES9X_CONTROLLER_HPP
#include <functional>
#include <vector>
#include <cstdint>
#include <string>
#include <memory>
#include "EmuConfig.hpp"
class Snes9xController
{
public:
static Snes9xController *get();
void init();
void deinit();
void mainLoop();
bool openFile(std::string filename);
bool loadState(std::string filename);
bool loadState(int slot);
void loadUndoState();
bool saveState(std::string filename);
bool saveState(int slot);
void increaseSaveSlot();
void decreaseSaveSlot();
void updateSettings(const EmuConfig * const config);
void updateBindings(const EmuConfig * const config);
void reportBinding(EmuBinding b, bool active);
void updateSoundBufferLevel(int, int);
bool acceptsCommand(const char *command);
bool isAbnormalSpeed();
void mute(bool muted);
void reset();
void softReset();
void setPaused(bool paused);
void setMessage(std::string message);
std::string getStateFolder();
std::string config_folder;
std::string sram_folder;
std::string state_folder;
std::string cheat_folder;
std::string patch_folder;
std::string export_folder;
int high_resolution_effect;
int rewind_buffer_size;
int rewind_frame_interval;
bool rewinding = false;
std::function<void(uint16_t *, int, int, int, double)> screen_output_function = nullptr;
std::function<void(int16_t *, int)> sound_output_function = nullptr;
bool active = false;
protected:
Snes9xController();
~Snes9xController();
private:
void SamplesAvailable();
};
#endif

View File

@ -0,0 +1,43 @@
#include <cstdint>
void S9xForceHires(void *buffer, int pitch, int &width, int &height)
{
if (width <= 256)
{
for (int y = (height)-1; y >= 0; y--)
{
uint16_t *line = (uint16_t *)((uint8_t *)buffer + y * pitch);
for (int x = (width * 2) - 1; x >= 0; x--)
{
*(line + x) = *(line + (x >> 1));
}
}
width *= 2;
}
}
static inline uint16_t average_565(uint16_t colora, uint16_t colorb)
{
return (((colora) & (colorb)) + ((((colora) ^ (colorb)) & 0xF7DE) >> 1));
}
void S9xMergeHires(void *buffer, int pitch, int &width, int &height)
{
if (width < 512)
return;
for (int y = 0; y < height; y++)
{
uint16_t *input = (uint16_t *)((uint8_t *)buffer + y * pitch);
uint16_t *output = input;
for (int x = 0; x < (width >> 1); x++)
{
*output++ = average_565(input[0], input[1]);
input += 2;
}
}
width >>= 1;
}

View File

@ -0,0 +1,4 @@
#pragma once
void S9xForceHires(void *buffer, int pitch, int &width, int &height);
void S9xMergeHires(void *buffer, int pitch, int &width, int &height);

131
qt/src/SoundPanel.cpp Normal file
View File

@ -0,0 +1,131 @@
#include "SoundPanel.hpp"
#include <QScreen>
static const int playback_rates[] = { 96000, 48000, 44100 };
SoundPanel::SoundPanel(EmuApplication *app_)
: app(app_)
{
setupUi(this);
connect(comboBox_driver, &QComboBox::activated, [&](int index){
if (app->config->sound_driver != driver_list[index])
{
app->config->sound_driver = driver_list[index];
app->restartAudio();
}
});
connect(comboBox_playback_rate, &QComboBox::activated, [&](int index)
{
if (index < 3)
{
if (playback_rates[index] != app->config->playback_rate)
{
app->config->playback_rate = playback_rates[index];
app->restartAudio();
app->updateSettings();
}
}
});
connect(spinBox_buffer_size, &QSpinBox::valueChanged, [&](int value) {
app->config->audio_buffer_size_ms = value;
app->restartAudio();
});
connect(checkBox_adjust_input_rate, &QCheckBox::clicked, [&](bool checked) {
app->config->adjust_input_rate_automatically = checked;
if (checked)
{
int calculated = screen()->refreshRate() / 60.09881 * 32040;
horizontalSlider_input_rate->setValue(calculated);
}
horizontalSlider_input_rate->setDisabled(checked);
app->updateSettings();
});
connect(horizontalSlider_input_rate, &QSlider::valueChanged, [&](int value) {
app->config->input_rate = value;
setInputRateText(value);
app->updateSettings();
});
connect(checkBox_dynamic_rate_control, &QCheckBox::clicked, [&](bool checked) {
app->config->dynamic_rate_control = checked;
app->updateSettings();
});
connect(doubleSpinBox_dynamic_rate_limit, &QDoubleSpinBox::valueChanged, [&](double value) {
app->config->dynamic_rate_limit = value;
app->updateSettings();
});
connect(checkBox_mute_sound, &QCheckBox::toggled, [&](bool checked) {
app->config->mute_audio = checked;
});
connect(checkBox_mute_during_alt_speed, &QCheckBox::toggled, [&](bool checked) {
app->config->mute_audio_during_alternate_speed = checked;
});
}
SoundPanel::~SoundPanel()
{
}
void SoundPanel::setInputRateText(int value)
{
double hz = value / 32040.0 * 60.09881;
label_input_rate->setText(QString("%1\n%2 Hz").arg(value).arg(hz, 6, 'g', 6));
}
void SoundPanel::showEvent(QShowEvent *event)
{
auto &config = app->config;
comboBox_driver->clear();
comboBox_driver->addItem("SDL");
comboBox_driver->addItem("PortAudio");
comboBox_driver->addItem("Cubeb");
driver_list.clear();
driver_list.push_back("sdl");
driver_list.push_back("portaudio");
driver_list.push_back("cubeb");
for (int i = 0; i < driver_list.size(); i++)
{
if (driver_list[i] == config->sound_driver)
{
comboBox_driver->setCurrentIndex(i);
break;
}
}
comboBox_device->clear();
comboBox_device->addItem("Default");
comboBox_playback_rate->clear();
comboBox_playback_rate->addItem("96000Hz");
comboBox_playback_rate->addItem("48000Hz");
comboBox_playback_rate->addItem("44100Hz");
int pbr_index = 1;
if (config->playback_rate == 96000)
pbr_index = 0;
else if (config->playback_rate == 44100)
pbr_index = 2;
comboBox_playback_rate->setCurrentIndex(pbr_index);
spinBox_buffer_size->setValue(config->audio_buffer_size_ms);
checkBox_adjust_input_rate->setChecked(config->adjust_input_rate_automatically);
setInputRateText(config->input_rate);
checkBox_dynamic_rate_control->setChecked(config->dynamic_rate_control);
doubleSpinBox_dynamic_rate_limit->setValue(config->dynamic_rate_limit);
checkBox_mute_sound->setChecked(config->mute_audio);
checkBox_mute_during_alt_speed->setChecked(config->mute_audio_during_alternate_speed);
}

18
qt/src/SoundPanel.hpp Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#include "ui_SoundPanel.h"
#include "EmuApplication.hpp"
#include <QMenu>
class SoundPanel :
public Ui::SoundPanel,
public QWidget
{
public:
SoundPanel(EmuApplication *app);
~SoundPanel();
EmuApplication *app;
void showEvent(QShowEvent *event) override;
void setInputRateText(int value);
std::vector<std::string> driver_list;
};

302
qt/src/SoundPanel.ui Normal file
View File

@ -0,0 +1,302 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SoundPanel</class>
<widget class="QWidget" name="SoundPanel">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>705</width>
<height>596</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Sound Output</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QGridLayout" name="gridLayout" rowminimumheight="0,0,0,0">
<item row="3" column="1" colspan="2">
<widget class="QSpinBox" name="spinBox_buffer_size">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="suffix">
<string> ms</string>
</property>
<property name="minimum">
<number>16</number>
</property>
<property name="maximum">
<number>256</number>
</property>
<property name="value">
<number>32</number>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="comboBox_playback_rate">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>48000 Hz</string>
</property>
</item>
<item>
<property name="text">
<string>44100 Hz</string>
</property>
</item>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Buffer size:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Device:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBox_device">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Choose a device to render output. If you have no integrated graphics, there will be only one choice.</string>
</property>
</widget>
</item>
<item row="1" column="2">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Driver:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox_driver">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Select the output driver.</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Playback rate:</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Sound Stretching</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QCheckBox" name="checkBox_dynamic_rate_control">
<property name="text">
<string>Dynamic rate control</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_adjust_input_rate">
<property name="text">
<string>Adjust input rate to display rate automatically</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Input rate:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_input_rate">
<property name="text">
<string>31987</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QSlider" name="horizontalSlider_input_rate">
<property name="minimum">
<number>31800</number>
</property>
<property name="maximum">
<number>32200</number>
</property>
<property name="value">
<number>31987</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::NoTicks</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_6">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Dynamic rate limit:</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="doubleSpinBox_dynamic_rate_limit">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="maximum">
<double>0.050000000000000</double>
</property>
<property name="singleStep">
<double>0.001000000000000</double>
</property>
<property name="value">
<double>0.005000000000000</double>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Mute</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QCheckBox" name="checkBox_mute_sound">
<property name="text">
<string>Mute all sound</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_mute_during_alt_speed">
<property name="text">
<string>Mute sound during turbo or rewind</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

23
qt/src/main.cpp Normal file
View File

@ -0,0 +1,23 @@
#include "EmuApplication.hpp"
int main(int argc, char *argv[])
{
EmuApplication emu;
emu.qtapp = std::make_unique<QApplication>(argc, argv);
emu.config = std::make_unique<EmuConfig>();
emu.config->setDefaults();
emu.config->loadFile(EmuConfig::findConfigFile());
emu.input_manager = std::make_unique<SDLInputManager>();
emu.window = std::make_unique<EmuMainWindow>(&emu);
emu.window->show();
emu.updateBindings();
emu.startIdleLoop();
emu.qtapp->exec();
emu.config->saveFile(EmuConfig::findConfigFile());
return 0;
}

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16.0px"
height="16.0px"
viewBox="0 0 16.0 16.0"
version="1.1"
id="SVGRoot"
sodipodi:docname="a.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview4430"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="38.057741"
inkscape:cx="4.611414"
inkscape:cy="8.2637591"
inkscape:window-width="1920"
inkscape:window-height="1021"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid4436" />
</sodipodi:namedview>
<defs
id="defs4425" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<circle
style="opacity:1;fill:none;fill-opacity:0.5;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
id="path4495"
cx="8"
cy="3"
r="2.5" />
<circle
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
id="path4495-3"
cx="3"
cy="8"
r="2.5" />
<circle
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
id="path4495-6"
cx="8"
cy="13"
r="2.5" />
<circle
style="fill:#000000;fill-opacity:0.5;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
id="path4495-7"
cx="13"
cy="8"
r="2.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16.0px"
height="16.0px"
viewBox="0 0 16.0 16.0"
version="1.1"
id="SVGRoot"
sodipodi:docname="b.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview4430"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="38.057741"
inkscape:cx="4.611414"
inkscape:cy="8.2637591"
inkscape:window-width="1920"
inkscape:window-height="1021"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid4436" />
</sodipodi:namedview>
<defs
id="defs4425" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<circle
style="opacity:1;fill:none;fill-opacity:0.5;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
id="path4495"
cx="8"
cy="3"
r="2.5" />
<circle
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
id="path4495-3"
cx="3"
cy="8"
r="2.5" />
<circle
style="fill:#000000;fill-opacity:0.5;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
id="path4495-6"
cx="8"
cy="13"
r="2.5" />
<circle
style="fill:none;fill-opacity:0.5;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
id="path4495-7"
cx="13"
cy="8"
r="2.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M300 696h60V536h-60v50h-60v60h60v50Zm100-50h320v-60H400v60Zm200-110h60v-50h60v-60h-60v-50h-60v160Zm-360-50h320v-60H240v60Zm80 450v-80H160q-33 0-56.5-23.5T80 776V296q0-33 23.5-56.5T160 216h640q33 0 56.5 23.5T880 296v480q0 33-23.5 56.5T800 856H640v80H320ZM160 776h640V296H160v480Zm0 0V296v480Z"/></svg>

After

Width:  |  Height:  |  Size: 395 B

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="down.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="41.7193"
inkscape:cx="3.3917156"
inkscape:cy="7.5624471"
inkscape:window-width="1920"
inkscape:window-height="1021"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid9"
spacingx="0.5"
spacingy="0.5"
empspacing="10" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 5.5,0.5 v 5 h -5 v 5 h 5 v 5 h 5 v -5 h 5 v -5 h -5 v -5 z"
id="path117"
sodipodi:nodetypes="ccccccccccccc" />
<rect
style="opacity:0.5;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.88976;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect947"
width="4"
height="5"
x="6"
y="10" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M80 896V256h800v640H80Zm80-80h640V416H160v400Zm140-40-56-56 103-104-104-104 57-56 160 160-160 160Zm180 0v-80h240v80H480Z"/></svg>

After

Width:  |  Height:  |  Size: 224 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M120 936q-33 0-56.5-23.5T40 856V336h80v520h680v80H120Zm160-160q-33 0-56.5-23.5T200 696V256q0-33 23.5-56.5T280 176h200l80 80h280q33 0 56.5 23.5T920 336v360q0 33-23.5 56.5T840 776H280Zm0-80h560V336H527l-80-80H280v440Zm0 0V256v440Z"/></svg>

After

Width:  |  Height:  |  Size: 332 B

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 16 16"
version="1.1"
id="svg4"
sodipodi:docname="joypad.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="true"
inkscape:zoom="41.7193"
inkscape:cx="7.7542049"
inkscape:cy="8.4972662"
inkscape:window-width="1920"
inkscape:window-height="1021"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg4">
<inkscape:grid
type="xygrid"
id="grid941" />
</sodipodi:namedview>
<path
d="M 4,5 C 3.966482,4.9992664 3.932975,5.00195 3.9,5.008 2.3557709,5.051666 1.0228084,6.1025927 0.62000004,7.594 0.21166956,9.1238633 0.87975114,10.737506 2.25,11.531 c 1.3592384,0.782703 3.0731398,0.566167 4.195,-0.53 H 9.5550001 C 10.67686,12.097167 12.390762,12.313703 13.75,11.531 15.120249,10.737506 15.78833,9.1238633 15.38,7.594 14.976552,6.1005143 13.640475,5.0490552 12.094,5.008 12.06301,5.00223 12.03152,4.99955 12,5 12,5 3.9291169,5 4,5 Z m 0,1 h 8 c 1.134,0 2.123,0.758 2.416,1.854 0.293689,1.0932733 -0.184847,2.247341 -1.166,2.812 -0.920496,0.530147 -2.076001,0.41769 -2.877,-0.28 C 10.243,10.257 10.114,9.9960003 9.8790001,10 H 6.12 c -0.234,-0.003 -0.394,0.268 -0.492,0.385 -0.8,0.693 -1.95,0.817 -2.879,0.281 C 1.7683339,10.100978 1.290585,8.9468293 1.585,7.854 1.8759471,6.7595374 2.8675267,5.9982999 4,6 Z M 3.5,7 v 1 h -1 v 1.0000003 h 1 V 10 h 1 V 9.0000003 h 1 V 8 h -1 V 7 Z M 12,7 c -0.666666,0 -0.666666,1 0,1 0.666666,0 0.666666,-1 0,-1 z M 11,8 C 10.333333,8 10.333333,9.0000003 11,9.0000003 11.666667,9.0000003 11.666667,8 11,8 Z m 2,0 C 12.333333,8 12.333333,9.0000003 13,9.0000003 13.666667,9.0000003 13.666667,8 13,8 Z M 12,9.0000003 C 11.333334,9.0000003 11.333334,10 12,10 c 0.666666,0 0.666666,-0.9999997 0,-0.9999997 z"
fill="gray"
font-family="sans-serif"
font-weight="400"
overflow="visible"
style="font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;fill:#000000;fill-opacity:1"
white-space="normal"
id="path2"
sodipodi:nodetypes="ccccccccccccsccccccccccccccccccccccssssssssssss" />
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="key.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="20.85965"
inkscape:cx="11.817073"
inkscape:cy="8.9886455"
inkscape:window-width="1920"
inkscape:window-height="1021"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid132"
spacingx="0.5"
spacingy="0.5" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.2;stroke-dasharray:none"
id="rect1845"
width="10.5"
height="9"
x="2.75"
y="2"
rx="1.4999999"
ry="1.5000001" />
<rect
style="fill:none;fill-opacity:0.17;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.2;stroke-dasharray:none"
id="rect1877"
width="14"
height="14"
x="1"
y="1"
rx="1.5"
ry="1.5000001" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.3;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;opacity:1"
d="M 2.6610169,9.5423728 1.5,14.5"
id="path1937"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.3;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;opacity:1"
d="M 13.322033,9.542373 14.5,14.5 v 0 0"
id="path3492"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.3;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;opacity:1"
d="m 1.5,1.5 1,1 v 0 L 3,3"
id="path3494"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.3;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;opacity:1"
d="M 14.5,1.5 13,3"
id="path3496"
sodipodi:nodetypes="cc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M80 856V296h800v560H80Zm80-80h640V376H160v400Zm160-40h320v-80H320v80ZM200 616h80v-80h-80v80Zm120 0h80v-80h-80v80Zm120 0h80v-80h-80v80Zm120 0h80v-80h-80v80Zm120 0h80v-80h-80v80ZM200 496h80v-80h-80v80Zm120 0h80v-80h-80v80Zm120 0h80v-80h-80v80Zm120 0h80v-80h-80v80Zm120 0h80v-80h-80v80ZM160 776V376v400Z"/></svg>

After

Width:  |  Height:  |  Size: 405 B

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="l.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="29.5"
inkscape:cx="5.4237288"
inkscape:cy="9.2711864"
inkscape:window-width="1920"
inkscape:window-height="1021"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid132" />
</sodipodi:namedview>
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient1721">
<stop
style="stop-color:currentColor;stop-opacity:0.5;"
offset="0"
id="stop1717" />
<stop
style="stop-color:currentColor;stop-opacity:0;"
offset="1"
id="stop1719" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1721"
id="linearGradient1723"
x1="1.000001"
y1="7.5"
x2="14.999999"
y2="7.5"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
fill="url(#linearGradient1723)"
stroke="currentColor">
<rect
style="fill:url(#linearGradient1723);fill-opacity:1;stroke:currentColor;stroke-width:0.999995;stroke-linecap:round;stroke-linejoin:round"
id="rect236"
width="13.000003"
height="4.0000029"
x="1.4999985"
y="5.4999986"
rx="1.5"
ry="1.5000001" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="left.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="41.7193"
inkscape:cx="3.3917156"
inkscape:cy="7.5624471"
inkscape:window-width="1920"
inkscape:window-height="1021"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid9"
spacingx="0.5"
spacingy="0.5"
empspacing="10" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 5.5,0.5 v 5 h -5 v 5 h 5 v 5 h 5 v -5 h 5 v -5 h -5 v -5 z"
id="path117"
sodipodi:nodetypes="ccccccccccccc" />
<rect
style="opacity:0.5;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.88976;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect947"
width="4"
height="5"
x="-10"
y="1"
transform="rotate(-90)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="r.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="29.5"
inkscape:cx="5.4237288"
inkscape:cy="9.2711864"
inkscape:window-width="1920"
inkscape:window-height="1021"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid132" />
</sodipodi:namedview>
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient1721">
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="0"
id="stop1719" />
<stop
style="stop-color:#000000;stop-opacity:0.5;"
offset="1"
id="stop1717" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1721"
id="linearGradient1723"
x1="1.000001"
y1="7.5"
x2="14.999999"
y2="7.5"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:url(#linearGradient1723);fill-opacity:1;stroke:#000000;stroke-width:0.999995;stroke-linecap:round;stroke-linejoin:round"
id="rect236"
width="13.000003"
height="4.0000029"
x="1.4999985"
y="5.4999986"
rx="1.5"
ry="1.5000001" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="right.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="41.7193"
inkscape:cx="3.3917156"
inkscape:cy="7.5624471"
inkscape:window-width="1920"
inkscape:window-height="1021"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid9"
spacingx="0.5"
spacingy="0.5"
empspacing="10" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 5.5,0.5 v 5 h -5 v 5 h 5 v 5 h 5 v -5 h 5 v -5 h -5 v -5 z"
id="path117"
sodipodi:nodetypes="ccccccccccccc" />
<rect
style="opacity:0.5;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.88976;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect947"
width="4"
height="5"
x="-10"
y="10"
transform="rotate(-90)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16.0px"
height="16.0px"
viewBox="0 0 16.0 16.0"
version="1.1"
id="SVGRoot"
sodipodi:docname="select.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview9265"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="38.057741"
inkscape:cx="6.5295521"
inkscape:cy="8.2112073"
inkscape:window-width="1920"
inkscape:window-height="1021"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid9271" />
</sodipodi:namedview>
<defs
id="defs9260" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:none;fill-opacity:0.5;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round"
id="rect9280-5"
width="6"
height="2"
x="0.66721022"
y="12.520413"
rx="0.5"
ry="0.5"
transform="rotate(-40)" />
<rect
style="fill:#000000;fill-opacity:0.49454543;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round"
id="rect9280-5-3"
width="6"
height="2"
x="-4.6951008"
y="8.0208998"
rx="0.5"
ry="0.5"
transform="rotate(-40)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm112-260q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm0-80q-25 0-42.5-17.5T422-480q0-25 17.5-42.5T482-540q25 0 42.5 17.5T542-480q0 25-17.5 42.5T482-420Zm-2-60Zm-40 320h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Z"/></svg>

After

Width:  |  Height:  |  Size: 853 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M440-440v-80h80v80h-80Zm-80 80v-80h80v80h-80Zm160 0v-80h80v80h-80Zm80-80v-80h80v80h-80Zm-320 0v-80h80v80h-80Zm-80 320q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm80-80h80v-80h-80v80Zm160 0h80v-80h-80v80Zm320 0v-80 80Zm-560-80h80v-80h80v80h80v-80h80v80h80v-80h80v80h80v-80h-80v-80h80v-320H200v320h80v80h-80v80Zm0 80v-560 560Zm560-240v80-80ZM600-280v80h80v-80h-80Z"/></svg>

After

Width:  |  Height:  |  Size: 535 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M560 925v-82q90-26 145-100t55-168q0-94-55-168T560 307v-82q124 28 202 125.5T840 575q0 127-78 224.5T560 925ZM120 696V456h160l200-200v640L280 696H120Zm440 40V414q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560 736ZM400 450l-86 86H200v80h114l86 86V450ZM300 576Z"/></svg>

After

Width:  |  Height:  |  Size: 354 B

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16.0px"
height="16.0px"
viewBox="0 0 16.0 16.0"
version="1.1"
id="SVGRoot"
sodipodi:docname="start.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview9265"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="38.057741"
inkscape:cx="6.5295521"
inkscape:cy="8.2112073"
inkscape:window-width="1920"
inkscape:window-height="1021"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid9271" />
</sodipodi:namedview>
<defs
id="defs9260" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#000000;fill-opacity:0.49454543;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round"
id="rect9280-5"
width="6"
height="2"
x="0.66721022"
y="12.520413"
rx="0.5"
ry="0.5"
transform="rotate(-40)" />
<rect
style="fill:none;fill-opacity:0.49454543;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round"
id="rect9280-5-3"
width="6"
height="2"
x="-4.6951008"
y="8.0208998"
rx="0.5"
ry="0.5"
transform="rotate(-40)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="up.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="41.7193"
inkscape:cx="3.3917156"
inkscape:cy="7.5624471"
inkscape:window-width="1920"
inkscape:window-height="1021"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid9"
spacingx="0.5"
spacingy="0.5"
empspacing="10" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 5.5,0.5 v 5 h -5 v 5 h 5 v 5 h 5 v -5 h 5 v -5 h -5 v -5 z"
id="path117"
sodipodi:nodetypes="ccccccccccccc" />
<rect
style="opacity:0.5;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.88976;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect947"
width="4"
height="5"
x="6"
y="1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16.0px"
height="16.0px"
viewBox="0 0 16.0 16.0"
version="1.1"
id="SVGRoot"
sodipodi:docname="x.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview4430"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="38.057741"
inkscape:cx="4.611414"
inkscape:cy="8.2637591"
inkscape:window-width="1920"
inkscape:window-height="1021"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid4436" />
</sodipodi:namedview>
<defs
id="defs4425" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<circle
style="opacity:1;fill:#000000;fill-opacity:0.5;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
id="path4495"
cx="8"
cy="3"
r="2.5" />
<circle
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
id="path4495-3"
cx="3"
cy="8"
r="2.5" />
<circle
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
id="path4495-6"
cx="8"
cy="13"
r="2.5" />
<circle
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
id="path4495-7"
cx="13"
cy="8"
r="2.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16.0px"
height="16.0px"
viewBox="0 0 16.0 16.0"
version="1.1"
id="SVGRoot"
sodipodi:docname="y.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview4430"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="38.057741"
inkscape:cx="4.611414"
inkscape:cy="8.2637591"
inkscape:window-width="1920"
inkscape:window-height="1021"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid4436" />
</sodipodi:namedview>
<defs
id="defs4425" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<circle
style="opacity:1;fill:none;fill-opacity:0.5;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
id="path4495"
cx="8"
cy="3"
r="2.5" />
<circle
style="fill:#000000;fill-opacity:0.5;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
id="path4495-3"
cx="3"
cy="8"
r="2.5" />
<circle
style="fill:none;fill-opacity:0.5;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
id="path4495-6"
cx="8"
cy="13"
r="2.5" />
<circle
style="fill:none;fill-opacity:0.5;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
id="path4495-7"
cx="13"
cy="8"
r="2.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,25 @@
<RCC>
<qresource prefix="icons">
<file>blackicons/settings.svg</file>
<file>blackicons/folders.svg</file>
<file>blackicons/emulation.svg</file>
<file>blackicons/sound.svg</file>
<file>blackicons/keyboard.svg</file>
<file>blackicons/display.svg</file>
<file>blackicons/joypad.svg</file>
<file>blackicons/key.svg</file>
<file>blackicons/a.svg</file>
<file>blackicons/b.svg</file>
<file>blackicons/l.svg</file>
<file>blackicons/left.svg</file>
<file>blackicons/down.svg</file>
<file>blackicons/r.svg</file>
<file>blackicons/right.svg</file>
<file>blackicons/select.svg</file>
<file>blackicons/start.svg</file>
<file>blackicons/up.svg</file>
<file>blackicons/x.svg</file>
<file>blackicons/y.svg</file>
<file>blackicons/shader.svg</file>
</qresource>
</RCC>

View File

@ -0,0 +1,175 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>895</width>
<height>651</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTableWidget" name="tableWidget">
<property name="showGrid">
<bool>true</bool>
</property>
<property name="gridStyle">
<enum>Qt::SolidLine</enum>
</property>
<property name="sortingEnabled">
<bool>false</bool>
</property>
<property name="cornerButtonEnabled">
<bool>true</bool>
</property>
<row>
<property name="text">
<string>Up</string>
</property>
<property name="icon">
<iconset resource="snes9x.qrc">
<normaloff>:/icons/icons/up.svg</normaloff>:/icons/icons/up.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>Down</string>
</property>
<property name="icon">
<iconset resource="snes9x.qrc">
<normaloff>:/icons/icons/down.svg</normaloff>:/icons/icons/down.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>Left</string>
</property>
<property name="icon">
<iconset resource="snes9x.qrc">
<normaloff>:/icons/icons/left.svg</normaloff>:/icons/icons/left.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>Right</string>
</property>
<property name="icon">
<iconset resource="snes9x.qrc">
<normaloff>:/icons/icons/right.svg</normaloff>:/icons/icons/right.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>A</string>
</property>
<property name="icon">
<iconset resource="snes9x.qrc">
<normaloff>:/icons/icons/a.svg</normaloff>:/icons/icons/a.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>B</string>
</property>
<property name="icon">
<iconset resource="snes9x.qrc">
<normaloff>:/icons/icons/b.svg</normaloff>:/icons/icons/b.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>X</string>
</property>
<property name="icon">
<iconset resource="snes9x.qrc">
<normaloff>:/icons/icons/x.svg</normaloff>:/icons/icons/x.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>Y</string>
</property>
<property name="icon">
<iconset resource="snes9x.qrc">
<normaloff>:/icons/icons/y.svg</normaloff>:/icons/icons/y.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>L</string>
</property>
<property name="icon">
<iconset resource="snes9x.qrc">
<normaloff>:/icons/icons/l.svg</normaloff>:/icons/icons/l.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>R</string>
</property>
<property name="icon">
<iconset resource="snes9x.qrc">
<normaloff>:/icons/icons/r.svg</normaloff>:/icons/icons/r.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>Start</string>
</property>
<property name="icon">
<iconset resource="snes9x.qrc">
<normaloff>:/icons/icons/start.svg</normaloff>:/icons/icons/start.svg</iconset>
</property>
</row>
<row>
<property name="text">
<string>Select</string>
</property>
<property name="icon">
<iconset resource="snes9x.qrc">
<normaloff>:/icons/icons/select.svg</normaloff>:/icons/icons/select.svg</iconset>
</property>
</row>
<column>
<property name="text">
<string>Binding #1</string>
</property>
</column>
<column>
<property name="text">
<string>Binding #2</string>
</property>
</column>
<column>
<property name="text">
<string>Binding #3</string>
</property>
</column>
<column>
<property name="text">
<string>Binding #4</string>
</property>
</column>
<item row="0" column="0">
<property name="text">
<string>Up</string>
</property>
<property name="icon">
<iconset theme="input-gaming"/>
</property>
</item>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="snes9x.qrc"/>
</resources>
<connections/>
</ui>

11869
qt/src/toml.hpp Normal file

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,7 @@
#include <deque>
#include <limits.h>
#include <vector>
#include <unistd.h>
static const unsigned int glsl_max_passes = 20;

View File

@ -9,12 +9,15 @@
#include "port.h"
#ifdef SNES9X_GTK
#if defined(SNES9X_QT)
#include <glad/gl.h>
#include <glad/glx.h>
#if defined(_WIN32)
#define realpath(src, resolved) _fullpath(resolved, src, PATH_MAX)
#endif
#ifdef _WIN32
#elif defined(SNES9X_GTK)
#include <glad/gl.h>
#elif defined(_WIN32)
#include <windows.h>
#include <stdlib.h>
#include "gl_core_3_1.h"
@ -27,7 +30,6 @@
#define chdir(dir) _chdir(dir)
#define realpath(src, resolved) _fullpath(resolved, src, PATH_MAX)
#endif
#endif
#endif /* __SHADER_PLATFORM_H */

View File

@ -1,9 +1,10 @@
#include <exception>
#include <cstring>
#include <tuple>
#include <vector>
#include <string>
#include "vulkan_context.hpp"
#include "slang_shader.hpp"
#include "vulkan/vulkan.hpp"
namespace Vulkan
{
@ -56,7 +57,7 @@ static vk::UniqueInstance create_instance_preamble(const char *wsi_extension)
{
load_loader();
if (!dl || !dl->success())
return vk::UniqueInstance();
return {};
std::vector<const char *> extensions = { wsi_extension, VK_KHR_SURFACE_EXTENSION_NAME };
vk::ApplicationInfo application_info({}, {}, {}, {}, VK_API_VERSION_1_0);
@ -69,6 +70,26 @@ static vk::UniqueInstance create_instance_preamble(const char *wsi_extension)
return instance;
}
std::vector<std::string> Vulkan::Context::get_device_list()
{
std::vector<std::string> device_names;
auto instance = create_instance_preamble(VK_KHR_SURFACE_EXTENSION_NAME);
if (!instance)
return {};
auto device_list = instance->enumeratePhysicalDevices();
for (auto &d : device_list)
{
auto props = d.getProperties();
std::string device_name((const char *)props.deviceName);
device_name += " (" + vk::to_string(props.deviceType) + ")";
device_names.push_back(device_name);
}
return device_names;
}
#ifdef VK_USE_PLATFORM_WIN32_KHR
bool Context::init_win32(HINSTANCE hinstance, HWND hwnd, int preferred_device)
{
@ -127,7 +148,6 @@ bool Context::init_wayland(wl_display *dpy, wl_surface *parent, int initial_widt
bool Context::init(int preferred_device)
{
init_device(preferred_device);
init_vma();
init_command_pool();
@ -179,12 +199,13 @@ bool Context::init_device(int preferred_device)
return device_list[0];
};
if (preferred_device > 0)
if (preferred_device > -1 && preferred_device < device_list.size())
physical_device = device_list[preferred_device];
else
physical_device = find_device();
physical_device.getProperties(&physical_device_props);
printf("Vulkan: Using device \"%s\"\n", (const char *)physical_device_props.deviceName);
graphics_queue_family_index = UINT32_MAX;
auto queue_props = physical_device.getQueueFamilyProperties();

View File

@ -9,8 +9,8 @@
#endif
#include <cstdio>
#include <cstdint>
#include "vk_mem_alloc.hpp"
#include "vulkan/vulkan.hpp"
#include "../external/VulkanMemoryAllocator-Hpp/include/vk_mem_alloc.hpp"
#include "vulkan_swapchain.hpp"
#include <memory>
#include <optional>
@ -24,21 +24,22 @@ class Context
Context();
~Context();
#ifdef VK_USE_PLATFORM_XLIB_KHR
bool init_Xlib(Display *dpy, Window xid, int preferred_device = 0);
bool init_Xlib(Display *dpy, Window xid, int preferred_device = -1);
#endif
#ifdef VK_USE_PLATFORM_WAYLAND_KHR
bool init_wayland(wl_display *dpy, wl_surface *parent, int width, int height, int preferred_device = 0);
bool init_wayland(wl_display *dpy, wl_surface *parent, int width, int height, int preferred_device = -1);
#endif
#ifdef VK_USE_PLATFORM_WIN32_KHR
bool init_win32(HINSTANCE hinstance, HWND hwnd, int preferred_device = 0);
bool init_win32(HINSTANCE hinstance, HWND hwnd, int preferred_device = -1);
#endif
bool init(int preferred_device = 0);
bool init(int preferred_device = -1);
bool create_swapchain(int width = -1, int height = -1);
bool recreate_swapchain(int width = -1, int height = -1);
void wait_idle();
vk::CommandBuffer begin_cmd_buffer();
void end_cmd_buffer();
void hard_barrier(vk::CommandBuffer cmd);
static std::vector<std::string> get_device_list();
vma::Allocator allocator;
vk::Device device;

View File

@ -251,7 +251,8 @@ void Texture::create(int width, int height, vk::Format fmt, vk::SamplerAddressMo
.setUsage(vk::BufferUsageFlagBits::eTransferSrc);
aci.setRequiredFlags(vk::MemoryPropertyFlagBits::eHostVisible)
.setFlags(vma::AllocationCreateFlagBits::eHostAccessSequentialWrite);
.setFlags(vma::AllocationCreateFlagBits::eHostAccessSequentialWrite)
.setUsage(vma::MemoryUsage::eAutoPreferHost);
std::tie(buffer, buffer_allocation) = allocator.createBuffer(bci, aci);