diff --git a/apu/apu.h b/apu/apu.h index b8247ded..5d075b4c 100644 --- a/apu/apu.h +++ b/apu/apu.h @@ -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 diff --git a/common/audio/s9x_sound_driver_cubeb.cpp b/common/audio/s9x_sound_driver_cubeb.cpp new file mode 100644 index 00000000..5253a3f4 --- /dev/null +++ b/common/audio/s9x_sound_driver_cubeb.cpp @@ -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 + +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, ¶ms, &min_latency); + + auto retval = cubeb_stream_init(context, &stream, "Snes9x", + nullptr, nullptr, + nullptr, ¶ms, + 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 S9xCubebSoundDriver::buffer_level() +{ + return { buffer.space_empty(), buffer.buffer_size }; +} \ No newline at end of file diff --git a/common/audio/s9x_sound_driver_cubeb.hpp b/common/audio/s9x_sound_driver_cubeb.hpp new file mode 100644 index 00000000..c61d86ea --- /dev/null +++ b/common/audio/s9x_sound_driver_cubeb.hpp @@ -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 +#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 buffer_level() override; + + private: + Resampler buffer; + cubeb *context = nullptr; + cubeb_stream *stream = nullptr; +}; + +#endif /* __S9X_SOUND_DRIVER_SDL_HPP */ diff --git a/common/audio/s9x_sound_driver_portaudio.cpp b/common/audio/s9x_sound_driver_portaudio.cpp index ecadbb14..1a6ad186 100644 --- a/common/audio/s9x_sound_driver_portaudio.cpp +++ b/common/audio/s9x_sound_driver_portaudio.cpp @@ -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, + ¶m, + 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,84 +134,31 @@ 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, - ¶m, - 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 (Pa_GetDefaultHostApi() != i) + if (tryHostAPI(i)) + return true; } - if (err != paNoError || Pa_GetHostApiCount() < 1) - { - fprintf(stderr, - "Couldn't initialize sound\n"); - return false; - } - - return true; + fprintf(stderr, "Couldn't initialize sound\n"); + return false; } int S9xPortAudioSoundDriver::space_free() diff --git a/common/audio/s9x_sound_driver_portaudio.hpp b/common/audio/s9x_sound_driver_portaudio.hpp index 516b6ca9..1683c7d8 100644 --- a/common/audio/s9x_sound_driver_portaudio.hpp +++ b/common/audio/s9x_sound_driver_portaudio.hpp @@ -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 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; }; diff --git a/common/audio/s9x_sound_driver_sdl.cpp b/common/audio/s9x_sound_driver_sdl.cpp index e5f8baa4..cfe7b45b 100644 --- a/common/audio/s9x_sound_driver_sdl.cpp +++ b/common/audio/s9x_sound_driver_sdl.cpp @@ -28,6 +28,11 @@ S9xSDLSoundDriver::S9xSDLSoundDriver() { } +S9xSDLSoundDriver::~S9xSDLSoundDriver() +{ + deinit(); +} + void S9xSDLSoundDriver::init() { SDL_InitSubSystem(SDL_INIT_AUDIO); diff --git a/common/audio/s9x_sound_driver_sdl.hpp b/common/audio/s9x_sound_driver_sdl.hpp index 53426b45..0a2347f8 100644 --- a/common/audio/s9x_sound_driver_sdl.hpp +++ b/common/audio/s9x_sound_driver_sdl.hpp @@ -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; diff --git a/common/video/glx_context.cpp b/common/video/glx_context.cpp index 2367905b..c94a97cd 100644 --- a/common/video/glx_context.cpp +++ b/common/video/glx_context.cpp @@ -94,6 +94,8 @@ bool GTKGLXContext::create_context() return false; } + resize(); + return true; } diff --git a/common/video/wayland_egl_context.cpp b/common/video/wayland_egl_context.cpp index d13ae40f..c4b0c6d1 100644 --- a/common/video/wayland_egl_context.cpp +++ b/common/video/wayland_egl_context.cpp @@ -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 }; diff --git a/common/video/wayland_surface.cpp b/common/video/wayland_surface.cpp index fe21d3fc..443f6bb2 100644 --- a/common/video/wayland_surface.cpp +++ b/common/video/wayland_surface.cpp @@ -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) diff --git a/common/video/wgl_context.cpp b/common/video/wgl_context.cpp new file mode 100644 index 00000000..62a11cb0 --- /dev/null +++ b/common/video/wgl_context.cpp @@ -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 +#include + +#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); +} diff --git a/common/video/wgl_context.hpp b/common/video/wgl_context.hpp new file mode 100644 index 00000000..d139aca5 --- /dev/null +++ b/common/video/wgl_context.hpp @@ -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 + +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 diff --git a/external/VulkanMemoryAllocator-Hpp/include/vk_mem_alloc.hpp b/external/VulkanMemoryAllocator-Hpp/include/vk_mem_alloc.hpp index b3b9d435..a8d0812b 100644 --- a/external/VulkanMemoryAllocator-Hpp/include/vk_mem_alloc.hpp +++ b/external/VulkanMemoryAllocator-Hpp/include/vk_mem_alloc.hpp @@ -2,7 +2,7 @@ #define VULKAN_MEMORY_ALLOCATOR_HPP #if !defined(AMD_VULKAN_MEMORY_ALLOCATOR_H) -#include +#include "vk_mem_alloc.h" #endif #include diff --git a/gtk/CMakeLists.txt b/gtk/CMakeLists.txt index 29836d34..d3adc82f 100644 --- a/gtk/CMakeLists.txt +++ b/gtk/CMakeLists.txt @@ -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) diff --git a/memmap.h b/memmap.h index 3a88ff3d..712c21a1 100644 --- a/memmap.h +++ b/memmap.h @@ -14,6 +14,7 @@ #include #include +#include struct CMemory { diff --git a/port.h b/port.h index 0a267154..25ae19f7 100644 --- a/port.h +++ b/port.h @@ -32,7 +32,6 @@ #define RIGHTSHIFT_int16_IS_SAR #define RIGHTSHIFT_int32_IS_SAR #ifndef __LIBRETRO__ -#define SNES_JOY_READ_CALLBACKS #endif //__LIBRETRO__ #endif diff --git a/qt/CMakeLists.txt b/qt/CMakeLists.txt new file mode 100644 index 00000000..f0b0ab67 --- /dev/null +++ b/qt/CMakeLists.txt @@ -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}) + diff --git a/qt/src/BindingPanel.cpp b/qt/src/BindingPanel.cpp new file mode 100644 index 00000000..8c0798b7 --- /dev/null +++ b/qt/src/BindingPanel.cpp @@ -0,0 +1,143 @@ +#include "BindingPanel.hpp" +#include + +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 func) +{ + joypads_changed = func; +} diff --git a/qt/src/BindingPanel.hpp b/qt/src/BindingPanel.hpp new file mode 100644 index 00000000..5718b75f --- /dev/null +++ b/qt/src/BindingPanel.hpp @@ -0,0 +1,37 @@ +#pragma once +#include +#include +#include +#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 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 timer; + EmuApplication *app; + QTableWidget *binding_table_widget; + EmuBinding *binding; + std::function joypads_changed; +}; \ No newline at end of file diff --git a/qt/src/ControllerPanel.cpp b/qt/src/ControllerPanel.cpp new file mode 100644 index 00000000..d489054b --- /dev/null +++ b/qt/src/ControllerPanel.cpp @@ -0,0 +1,153 @@ +#include "ControllerPanel.hpp" +#include "SDLInputManager.hpp" +#include "SDL_gamecontroller.h" +#include +#include +#include + +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() +{ +} + diff --git a/qt/src/ControllerPanel.hpp b/qt/src/ControllerPanel.hpp new file mode 100644 index 00000000..eb1bcf3e --- /dev/null +++ b/qt/src/ControllerPanel.hpp @@ -0,0 +1,23 @@ +#pragma once +#include "ui_ControllerPanel.h" +#include "BindingPanel.hpp" +#include "EmuApplication.hpp" +#include + +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; +}; \ No newline at end of file diff --git a/qt/src/ControllerPanel.ui b/qt/src/ControllerPanel.ui new file mode 100644 index 00000000..0fd58a94 --- /dev/null +++ b/qt/src/ControllerPanel.ui @@ -0,0 +1,325 @@ + + + ControllerPanel + + + + 0 + 0 + 674 + 632 + + + + Form + + + + + + + + Set + + + + + + + + SNES Controller 1 + + + + + SNES Controller 2 + + + + + SNES Controller 3 (Multitap) + + + + + SNES Controller 4 (Multitap) + + + + + SNES Controller 5 (Multitap) + + + + + + + + Qt::Vertical + + + + + + + + 0 + 0 + + + + + 90 + 0 + + + + Swap or clear groups of bindings + + + Edit + + + QToolButton::DelayedPopup + + + + + + + + 0 + 0 + + + + + 90 + 0 + + + + Automatically assign a controller's buttons to a slot + + + Auto-Assign + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + true + + + + Up + + + + :/icons/blackicons/up.svg:/icons/blackicons/up.svg + + + + + Down + + + + :/icons/blackicons/down.svg:/icons/blackicons/down.svg + + + + + Left + + + + :/icons/blackicons/left.svg:/icons/blackicons/left.svg + + + + + Right + + + + :/icons/blackicons/right.svg:/icons/blackicons/right.svg + + + + + A + + + + :/icons/blackicons/a.svg:/icons/blackicons/a.svg + + + + + B + + + + :/icons/blackicons/b.svg:/icons/blackicons/b.svg + + + + + X + + + + :/icons/blackicons/x.svg:/icons/blackicons/x.svg + + + + + Y + + + + :/icons/blackicons/y.svg:/icons/blackicons/y.svg + + + + + L + + + + :/icons/blackicons/l.svg:/icons/blackicons/l.svg + + + + + R + + + + :/icons/blackicons/r.svg:/icons/blackicons/r.svg + + + + + Start + + + + :/icons/blackicons/start.svg:/icons/blackicons/start.svg + + + + + Select + + + + :/icons/blackicons/select.svg:/icons/blackicons/select.svg + + + + + Turbo A + + + + :/icons/blackicons/a.svg:/icons/blackicons/a.svg + + + + + Turbo B + + + + :/icons/blackicons/b.svg:/icons/blackicons/b.svg + + + + + Turbo X + + + + :/icons/blackicons/x.svg:/icons/blackicons/x.svg + + + + + Turbo Y + + + + :/icons/blackicons/y.svg:/icons/blackicons/y.svg + + + + + Turbo L + + + + :/icons/blackicons/l.svg:/icons/blackicons/l.svg + + + + + Turbo R + + + + :/icons/blackicons/r.svg:/icons/blackicons/r.svg + + + + + Binding #1 + + + + + Binding #2 + + + + + Binding #3 + + + + + Binding #4 + + + + + + + + + + + diff --git a/qt/src/DisplayPanel.cpp b/qt/src/DisplayPanel.cpp new file mode 100644 index 00000000..e008b0d8 --- /dev/null +++ b/qt/src/DisplayPanel.cpp @@ -0,0 +1,181 @@ +#include "DisplayPanel.hpp" +#include + +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); +} + + diff --git a/qt/src/DisplayPanel.hpp b/qt/src/DisplayPanel.hpp new file mode 100644 index 00000000..b5b1d9a4 --- /dev/null +++ b/qt/src/DisplayPanel.hpp @@ -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> driver_list; + bool updating = true; + + EmuApplication *app; +}; \ No newline at end of file diff --git a/qt/src/DisplayPanel.ui b/qt/src/DisplayPanel.ui new file mode 100644 index 00000000..d4278735 --- /dev/null +++ b/qt/src/DisplayPanel.ui @@ -0,0 +1,389 @@ + + + DisplayPanel + + + + 0 + 0 + 669 + 654 + + + + Form + + + + + + Display Output + + + + + + + + Driver: + + + + + + + + 0 + 0 + + + + Choose a device to render output. If you have no integrated graphics, there will be only one choice. + + + + + + + + 0 + 0 + + + + Select the output driver. + + + + + + + Device: + + + + + + + + + + + Smooth screen output. + + + Bilinear filter + + + + + + + Prevent the display driver from getting too far ahead in order to reduce lag. + + + Reduce input lag + + + + + + + When entering fullscreen mode, temporarily change other settings to use VRR (G-Sync or FreeSync) correctly. + + + Adjust settings for VRR in fullscreen mode + + + + + + + Sync the display to vertical retrace to eliminate tearing. + + + Enable vsync + + + + + + + + + + + Use a selected .slangp or .glslp shader file. +.slangp is supported by Vulkan and OpenGL. +.glslp is supported by OpenGL. + + + Use a hardware shader: + + + + + + + + + + Browse... + + + + + + + + + + + + Scaling and Aspect Ratio + + + + + + + + Keep the screen at the requested proportions for width and height. + + + Maintain aspect ratio + + + + + + + When scaling up, only use integer multiples of the original height. + + + Use integer scaling + + + + + + + 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. + + + Show overscan area + + + + + + + + + + + High-resolution mode: + + + + + + + + 0 + 0 + + + + + 4:3 - Classic display aspect + + + + + 64:49 - NTSC aspect + + + + + 8:7 - Square pixels + + + + + + + + Aspect ratio: + + + + + + + + 0 + 0 + + + + This affects the rarely used 512-pixels-wide mode used by the SNES. +For games like Kirby 3 and Jurassic Park, choose the "merge fields" option. +For games like Seiken Densetsu 3 and Marvelous, choose the "scale up" option. +Output directly will cause the screen to change between the two modes and look weird. + + + + Output directly + + + + + Merge the fields of the high-resolution lines + + + + + Scale normal resolution screens up + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Software Filters + + + + + + + + + + + + Messages + + + + + + + + Display messages: + + + + + + + + 0 + 0 + + + + + Onscreen - High resolution + + + + + Inside the screen - Low resolution + + + + + Don't display messages + + + + + + + + + 0 + 0 + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + pt + + + 8 + + + 256 + + + 24 + + + + + + + Onscreen display size: + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/qt/src/EmuApplication.cpp b/qt/src/EmuApplication.cpp new file mode 100644 index 00000000..0ac45e16 --- /dev/null +++ b/qt/src/EmuApplication.cpp @@ -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 +#include +#include +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(); + else if (config->sound_driver == "cubeb") + sound_driver = std::make_unique(); + else + { + config->sound_driver = "sdl"; + sound_driver = std::make_unique(); + } + + 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(); + 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(); +} diff --git a/qt/src/EmuApplication.hpp b/qt/src/EmuApplication.hpp new file mode 100644 index 00000000..fc3da3e2 --- /dev/null +++ b/qt/src/EmuApplication.hpp @@ -0,0 +1,57 @@ +#pragma once +#include +#include + +#include "EmuMainWindow.hpp" +#include "EmuConfig.hpp" +#include "SDLInputManager.hpp" +#include "Snes9xController.hpp" +#include "common/audio/s9x_sound_driver.hpp" + + +struct EmuApplication +{ + std::unique_ptr qtapp; + std::unique_ptr config; + std::unique_ptr input_manager; + std::unique_ptr window; + std::unique_ptr 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> bindings; + std::unique_ptr idle_loop; + std::function binding_callback = nullptr; + std::function joypads_changed_callback = nullptr; + int save_slot = 0; + int pause_count = 0; +}; \ No newline at end of file diff --git a/qt/src/EmuBinding.cpp b/qt/src/EmuBinding.cpp new file mode 100644 index 00000000..325affbd --- /dev/null +++ b/qt/src/EmuBinding.cpp @@ -0,0 +1,226 @@ +#include "EmuBinding.hpp" +#include "SDL_joystick.h" +#include +#include +#include + +// 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; +} diff --git a/qt/src/EmuBinding.hpp b/qt/src/EmuBinding.hpp new file mode 100644 index 00000000..ed2a7f3d --- /dev/null +++ b/qt/src/EmuBinding.hpp @@ -0,0 +1,60 @@ +#ifndef __EMU_BINDING_HPP +#define __EMU_BINDING_HPP +#include +#include +#include + +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 \ No newline at end of file diff --git a/qt/src/EmuCanvas.cpp b/qt/src/EmuCanvas.cpp new file mode 100644 index 00000000..072f9223 --- /dev/null +++ b/qt/src/EmuCanvas.cpp @@ -0,0 +1,107 @@ +#include "EmuCanvas.hpp" +#include +#include + +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 }; +} \ No newline at end of file diff --git a/qt/src/EmuCanvas.hpp b/qt/src/EmuCanvas.hpp new file mode 100644 index 00000000..938d422f --- /dev/null +++ b/qt/src/EmuCanvas.hpp @@ -0,0 +1,73 @@ +#pragma once +#include +#include +#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 getDeviceList() + { + return std::vector{ "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; +}; \ No newline at end of file diff --git a/qt/src/EmuCanvasGLX.cpp b/qt/src/EmuCanvasGLX.cpp new file mode 100644 index 00000000..022a7b3c --- /dev/null +++ b/qt/src/EmuCanvasGLX.cpp @@ -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(); + } +}; diff --git a/qt/src/EmuCanvasOpenGL.cpp b/qt/src/EmuCanvasOpenGL.cpp new file mode 100644 index 00000000..379ab1af --- /dev/null +++ b/qt/src/EmuCanvasOpenGL.cpp @@ -0,0 +1,378 @@ +#include "EmuCanvasOpenGL.hpp" +#include +#include +#include +#include +#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 +#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(); + 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(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 *>(&shader->param); + + if (!shader_parameters_dialog) + shader_parameters_dialog = + std::make_unique(this, parameters); + + shader_parameters_dialog->show(); +} + +void EmuCanvasOpenGL::saveParameters(std::string filename) +{ + if (shader) + shader->save(filename.c_str()); +} \ No newline at end of file diff --git a/qt/src/EmuCanvasOpenGL.hpp b/qt/src/EmuCanvasOpenGL.hpp new file mode 100644 index 00000000..8ab4b71f --- /dev/null +++ b/qt/src/EmuCanvasOpenGL.hpp @@ -0,0 +1,50 @@ +#ifndef __EMU_CANVAS_OPENGL_HPP +#define __EMU_CANVAS_OPENGL_HPP +#include + +#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 context; + bool using_shader; + std::unique_ptr shader; + std::unique_ptr 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 \ No newline at end of file diff --git a/qt/src/EmuCanvasQt.cpp b/qt/src/EmuCanvasQt.cpp new file mode 100644 index 00000000..84c487eb --- /dev/null +++ b/qt/src/EmuCanvasQt.cpp @@ -0,0 +1,47 @@ +#include "EmuCanvasQt.hpp" +#include +#include + +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)); +} \ No newline at end of file diff --git a/qt/src/EmuCanvasQt.hpp b/qt/src/EmuCanvasQt.hpp new file mode 100644 index 00000000..ca5e1bc3 --- /dev/null +++ b/qt/src/EmuCanvasQt.hpp @@ -0,0 +1,20 @@ +#ifndef __EMU_CANVAS_QT_HPP +#define __EMU_CANVAS_QT_HPP + +#include "EmuCanvas.hpp" + +#include + +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 \ No newline at end of file diff --git a/qt/src/EmuCanvasVulkan.cpp b/qt/src/EmuCanvasVulkan.cpp new file mode 100644 index 00000000..9b1eddad --- /dev/null +++ b/qt/src/EmuCanvasVulkan.cpp @@ -0,0 +1,305 @@ +#include +#include +#include +#include +#include +#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(); + +#ifdef _WIN32 + auto hwnd = (HWND)winId(); + context->init_win32(nullptr, hwnd, config->display_device_index); +#else + if (platform == "wayland") + { + wayland_surface = std::make_unique(); + 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(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(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(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 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 *>(&shader_chain->preset->parameters); + + if (!shader_parameters_dialog) + shader_parameters_dialog = + std::make_unique(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); +} \ No newline at end of file diff --git a/qt/src/EmuCanvasVulkan.hpp b/qt/src/EmuCanvasVulkan.hpp new file mode 100644 index 00000000..97fa3989 --- /dev/null +++ b/qt/src/EmuCanvasVulkan.hpp @@ -0,0 +1,50 @@ +#pragma once +#include + +#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 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 context; + std::unique_ptr simple_output; + std::unique_ptr shader_chain; + + private: + void createContext(); + void tryLoadShader(); + std::string current_shader; + QWindow *window = nullptr; + std::unique_ptr shader_parameters_dialog = nullptr; + QString platform; + +#ifndef _WIN32 + std::unique_ptr wayland_surface; +#endif +}; diff --git a/qt/src/EmuConfig.cpp b/qt/src/EmuConfig.cpp new file mode 100644 index 00000000..3af1d649 --- /dev/null +++ b/qt/src/EmuConfig.cpp @@ -0,0 +1,560 @@ +#include +#include +#define TOML_LARGE_FILES 1 +#define TOML_IMPLEMENTATION 1 +#include +#include "toml.hpp" +#include +namespace fs = std::filesystem; + +#include "EmuConfig.hpp" +#include "EmuBinding.hpp" +#include + +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 Bool; + std::function Int; + std::function String; + std::function)> Enum; + std::function Double; + std::function Binding; + std::function BeginSection; + std::function 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 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 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; + } +} \ No newline at end of file diff --git a/qt/src/EmuConfig.hpp b/qt/src/EmuConfig.hpp new file mode 100644 index 00000000..ccba7633 --- /dev/null +++ b/qt/src/EmuConfig.hpp @@ -0,0 +1,224 @@ +#ifndef __EMU_CONFIG_HPP +#define __EMU_CONFIG_HPP + +#include +#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 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 \ No newline at end of file diff --git a/qt/src/EmuInputPanel.cpp b/qt/src/EmuInputPanel.cpp new file mode 100644 index 00000000..aaa6ab13 --- /dev/null +++ b/qt/src/EmuInputPanel.cpp @@ -0,0 +1,137 @@ +#include "EmuInputPanel.hpp" +#include +#include + +EmuInputBinder::EmuInputBinder(const QString &text, std::vector *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 *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 func) +{ + add_button_func = func; +} + +EmuInputBinder::~EmuInputBinder() +{ +} \ No newline at end of file diff --git a/qt/src/EmuInputPanel.hpp b/qt/src/EmuInputPanel.hpp new file mode 100644 index 00000000..0b6f6c9e --- /dev/null +++ b/qt/src/EmuInputPanel.hpp @@ -0,0 +1,38 @@ +#ifndef __EMU_INPUT_PANEL_HPP +#define __EMU_INPUT_PANEL_HPP + +#include +#include +#include +#include + +#include "EmuBinding.hpp" + +class EmuInputBinder : public QWidget +{ + public: + EmuInputBinder(const QString &text, std::vector *bindings, int min_label_width = 0); + ~EmuInputBinder(); + void updateDisplay(); + std::vector *bindings; + void connectAddButton(std::function); + void untoggleAddButton(); + void reset(const QString &text, std::vector *bindings); + + void keyPressEvent(QKeyEvent *event) override; + + private: + QLabel *label; + QHBoxLayout *layout; + QPushButton *buttons[3]; + QPushButton *add_button; + std::function add_button_func; + QIcon remove_icon; + QIcon add_icon; +}; + +class EmuInputPanel : QWidget +{ +}; + +#endif \ No newline at end of file diff --git a/qt/src/EmuMainWindow.cpp b/qt/src/EmuMainWindow.cpp new file mode 100644 index 00000000..69341ab6 --- /dev/null +++ b/qt/src/EmuMainWindow.cpp @@ -0,0 +1,566 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 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 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); +} \ No newline at end of file diff --git a/qt/src/EmuMainWindow.hpp b/qt/src/EmuMainWindow.hpp new file mode 100644 index 00000000..fc4f405c --- /dev/null +++ b/qt/src/EmuMainWindow.hpp @@ -0,0 +1,55 @@ +#ifndef __EMU_MAIN_WINDOW_HPP +#define __EMU_MAIN_WINDOW_HPP + +#include +#include +#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 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 core_actions; + std::vector recent_menu_items; +}; + +#endif \ No newline at end of file diff --git a/qt/src/EmuSettingsWindow.cpp b/qt/src/EmuSettingsWindow.cpp new file mode 100644 index 00000000..69c6c969 --- /dev/null +++ b/qt/src/EmuSettingsWindow.cpp @@ -0,0 +1,57 @@ +#include "EmuSettingsWindow.hpp" +#include "src/EmuMainWindow.hpp" + +#include + +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(); +} \ No newline at end of file diff --git a/qt/src/EmuSettingsWindow.hpp b/qt/src/EmuSettingsWindow.hpp new file mode 100644 index 00000000..d23439bd --- /dev/null +++ b/qt/src/EmuSettingsWindow.hpp @@ -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; +}; \ No newline at end of file diff --git a/qt/src/EmuSettingsWindow.ui b/qt/src/EmuSettingsWindow.ui new file mode 100644 index 00000000..94c02840 --- /dev/null +++ b/qt/src/EmuSettingsWindow.ui @@ -0,0 +1,158 @@ + + + EmuSettingsWindow + + + + 0 + 0 + 933 + 719 + + + + Qt::NoFocus + + + Dialog + + + + + + + + + 0 + 0 + + + + QAbstractScrollArea::AdjustToContents + + + QListView::Fixed + + + + General + + + + :/icons/blackicons/settings.svg:/icons/blackicons/settings.svg + + + + + Display + + + + :/icons/blackicons/display.svg:/icons/blackicons/display.svg + + + + + Sound + + + + :/icons/blackicons/sound.svg:/icons/blackicons/sound.svg + + + + + Emulation + + + + :/icons/blackicons/emulation.svg:/icons/blackicons/emulation.svg + + + + + Controllers + + + + :/icons/blackicons/joypad.svg:/icons/blackicons/joypad.svg + + + + + Shortcuts + + + + :/icons/blackicons/keyboard.svg:/icons/blackicons/keyboard.svg + + + + + Files + + + + :/icons/blackicons/folders.svg:/icons/blackicons/folders.svg + + + + + + + + + 0 + 0 + + + + + + + + + + + + Restore Defaults + + + + .. + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Close + + + + .. + + + + + + + + + + + + diff --git a/qt/src/EmulationPanel.cpp b/qt/src/EmulationPanel.cpp new file mode 100644 index 00000000..1cf4e4b1 --- /dev/null +++ b/qt/src/EmulationPanel.cpp @@ -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); +} + diff --git a/qt/src/EmulationPanel.hpp b/qt/src/EmulationPanel.hpp new file mode 100644 index 00000000..2286e7cd --- /dev/null +++ b/qt/src/EmulationPanel.hpp @@ -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; +}; \ No newline at end of file diff --git a/qt/src/EmulationPanel.ui b/qt/src/EmulationPanel.ui new file mode 100644 index 00000000..bc2f7b5d --- /dev/null +++ b/qt/src/EmulationPanel.ui @@ -0,0 +1,378 @@ + + + EmulationPanel + + + + 0 + 0 + 862 + 756 + + + + Form + + + + + + Emulation Speed + + + + + + + + Speed control method: + + + + + + + + Timer - Match the frame rate configured below + + + + + Timer with Frame Skipping - Skip frames if needed + + + + + Sound Synchronization - Wait on the sound buffer + + + + + None - Run unthrottled, unless vsync is turned on + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Frame rate: + + + + + + + fps + + + 4 + + + 0.000000000000000 + + + 120.000000000000000 + + + 0.000000000000000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Set frame rate to 0 to automatically change based on PAL/NTSC and with interlacing on and off. + + + Qt::PlainText + + + true + + + + + + + + + Fast-forward frame skip: + + + + + + + frames + + + 1 + + + 15 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Rewind + + + + + + + + Rewind buffer size (0 to disable): + + + + + + + + + + Frames between steps + + + + + + + MB + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Hacks + + + + + + + + Allow invalid VRAM access + + + + + + + Overclock the CPU + + + + + + + Use a shadow echo buffer + + + + + + + Allow opposite D-pad directions simultaneously + + + + + + + Remove the sprite limit + + + + + + + + + + + SuperFX clock speed: + + + + + + + % + + + 50 + + + 1000 + + + 10 + + + 100 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Alternate sound filter: + + + + + + + + Nearest + + + + + Linear + + + + + Gaussian + + + + + Cubic + + + + + Sinc + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/qt/src/FoldersPanel.cpp b/qt/src/FoldersPanel.cpp new file mode 100644 index 00000000..6f7ccd32 --- /dev/null +++ b/qt/src/FoldersPanel.cpp @@ -0,0 +1,68 @@ +#include "FoldersPanel.hpp" +#include +#include + +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); +} + diff --git a/qt/src/FoldersPanel.hpp b/qt/src/FoldersPanel.hpp new file mode 100644 index 00000000..8a62b629 --- /dev/null +++ b/qt/src/FoldersPanel.hpp @@ -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; +}; \ No newline at end of file diff --git a/qt/src/FoldersPanel.ui b/qt/src/FoldersPanel.ui new file mode 100644 index 00000000..7de50455 --- /dev/null +++ b/qt/src/FoldersPanel.ui @@ -0,0 +1,282 @@ + + + FoldersPanel + + + + 0 + 0 + 779 + 673 + + + + Form + + + + + + Data Locations + + + + + + + ROM Folder + + + + + Config Folder + + + + + Custom Folder + + + + + + + + + ROM Folder + + + + + Config Folder + + + + + Custom Folder + + + + + + + + + ROM Folder + + + + + Config Folder + + + + + Custom Folder + + + + + + + + Save states + + + + + + + Browse... + + + + + + + SRAM + + + + + + + true + + + + + + + + ROM Folder + + + + + Config Folder + + + + + Custom Folder + + + + + + + + + ROM Folder + + + + + Config Folder + + + + + Custom Folder + + + + + + + + Cheats + + + + + + + Patches + + + + + + + Exports + + + + + + + true + + + + + + + true + + + + + + + true + + + + + + + true + + + + + + + Browse... + + + + + + + Browse... + + + + + + + Browse... + + + + + + + Browse... + + + + + + + + + + SRAM + + + + + + Save SRAM to disk every: + + + + + + + seconds + + + 1000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/qt/src/GeneralPanel.cpp b/qt/src/GeneralPanel.cpp new file mode 100644 index 00000000..49899b70 --- /dev/null +++ b/qt/src/GeneralPanel.cpp @@ -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); +} + diff --git a/qt/src/GeneralPanel.hpp b/qt/src/GeneralPanel.hpp new file mode 100644 index 00000000..197191dc --- /dev/null +++ b/qt/src/GeneralPanel.hpp @@ -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; +}; \ No newline at end of file diff --git a/qt/src/GeneralPanel.ui b/qt/src/GeneralPanel.ui new file mode 100644 index 00000000..fa790b2b --- /dev/null +++ b/qt/src/GeneralPanel.ui @@ -0,0 +1,101 @@ + + + GeneralPanel + + + + 0 + 0 + 752 + 615 + + + + Form + + + + + + General + + + + + + Switch to fullscreen after opening a ROM + + + + + + + Disable screensaver + + + + + + + Pause emulation when window is not focused + + + + + + + + + + Overlays + + + + + + Show frame rate + + + + + + + Show fast-forward and pause icons + + + + + + + Show pressed keys + + + + + + + Show the time + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/qt/src/SDLInputManager.cpp b/qt/src/SDLInputManager.cpp new file mode 100644 index 00000000..7083e34d --- /dev/null +++ b/qt/src/SDLInputManager.cpp @@ -0,0 +1,229 @@ +#include "SDLInputManager.hpp" +#include "SDL.h" +#include "SDL_events.h" +#include "SDL_gamecontroller.h" +#include "SDL_joystick.h" + +#include +#include + +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 event; + + while ((event = ProcessEvent())) + { + } +} + + +std::optional +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::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 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> SDLInputManager::getXInputControllers() +{ + std::vector> list; + + for (auto &d : devices) + { + if (!d.second.is_controller) + continue; + + list.push_back(std::pair(d.first, SDL_JoystickName(d.second.joystick))); + auto bind = SDL_GameControllerGetBindForButton(d.second.controller, SDL_CONTROLLER_BUTTON_A); + } + + return list; +} \ No newline at end of file diff --git a/qt/src/SDLInputManager.hpp b/qt/src/SDLInputManager.hpp new file mode 100644 index 00000000..8e473ffd --- /dev/null +++ b/qt/src/SDLInputManager.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include "SDL.h" +#include +#include +#include +#include + +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 axes; + + struct Hat + { + uint8_t state; + }; + std::vector hats; + + std::vector buttons; +}; + +struct SDLInputManager +{ + SDLInputManager(); + ~SDLInputManager(); + + std::optional ProcessEvent(); + std::vector> 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 DiscretizeJoyAxisEvent(SDL_Event &event); + + struct DiscreteHatEvent + { + int joystick_num; + int hat; + int direction; + bool pressed; + }; + std::optional DiscretizeHatEvent(SDL_Event &event); + + std::map devices; +}; diff --git a/qt/src/ShaderParametersDialog.cpp b/qt/src/ShaderParametersDialog.cpp new file mode 100644 index 00000000..de9128f1 --- /dev/null +++ b/qt/src/ShaderParametersDialog.cpp @@ -0,0 +1,201 @@ +#include "ShaderParametersDialog.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +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 *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() +{ +} \ No newline at end of file diff --git a/qt/src/ShaderParametersDialog.hpp b/qt/src/ShaderParametersDialog.hpp new file mode 100644 index 00000000..ee65bf5e --- /dev/null +++ b/qt/src/ShaderParametersDialog.hpp @@ -0,0 +1,28 @@ +#pragma once +#include +#include +#include +#include + +#include "EmuCanvas.hpp" +#include "EmuConfig.hpp" + +class ShaderParametersDialog : public QDialog +{ + public: + ShaderParametersDialog(EmuCanvas *parent, std::vector *parameters); + ~ShaderParametersDialog(); + + void refreshWidgets(); + void showEvent(QShowEvent *event) override; + void closeEvent(QCloseEvent *event) override; + void save(); + void saveAs(); + + std::vector> widgets; + std::vector saved_parameters; + std::vector *parameters; + + EmuCanvas *canvas = nullptr; + EmuConfig *config = nullptr; +}; \ No newline at end of file diff --git a/qt/src/ShortcutsPanel.cpp b/qt/src/ShortcutsPanel.cpp new file mode 100644 index 00000000..38b82ad0 --- /dev/null +++ b/qt/src/ShortcutsPanel.cpp @@ -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(); +} + diff --git a/qt/src/ShortcutsPanel.hpp b/qt/src/ShortcutsPanel.hpp new file mode 100644 index 00000000..f456fe89 --- /dev/null +++ b/qt/src/ShortcutsPanel.hpp @@ -0,0 +1,17 @@ +#pragma once +#include "ui_ShortcutsPanel.h" +#include "EmuApplication.hpp" +#include "BindingPanel.hpp" +#include + +class ShortcutsPanel : + public Ui::ShortcutsPanel, + public BindingPanel +{ + public: + ShortcutsPanel(EmuApplication *app); + ~ShortcutsPanel(); + + void setDefaultKeys(int slot); + QMenu reset_to_default_menu; +}; \ No newline at end of file diff --git a/qt/src/ShortcutsPanel.ui b/qt/src/ShortcutsPanel.ui new file mode 100644 index 00000000..e91819db --- /dev/null +++ b/qt/src/ShortcutsPanel.ui @@ -0,0 +1,357 @@ + + + ShortcutsPanel + + + + 0 + 0 + 797 + 716 + + + + Form + + + + + + + + Reset to Default + + + + + + + Clear All + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 0 + 0 + + + + + Open ROM + + + + + Enable Fast-Forward + + + + + Toggle Fast-Forward + + + + + Pause/Continue + + + + + Reset + + + + + Power Cycle Console + + + + + Quit + + + + + Toggle Fullscreen + + + + + Save Screenshot + + + + + Save SPC + + + + + Save State to Current Slot + + + + + Load State from Current Slot + + + + + Increase Current Save Slot + + + + + Decrease Current Save Slot + + + + + Quick Save Slot 0 + + + + + Quick Save Slot 1 + + + + + Quick Save Slot 2 + + + + + Quick Save Slot 3 + + + + + Quick Save Slot 4 + + + + + Quick Save Slot 5 + + + + + Quick Save Slot 6 + + + + + Quick Save Slot 7 + + + + + Quick Save Slot 8 + + + + + Quick Save Slot 9 + + + + + Quick Load Slot 0 + + + + + Quick Load Slot 1 + + + + + Quick Load Slot 2 + + + + + Quick Load Slot 3 + + + + + Quick Load Slot 4 + + + + + Quick Load Slot 5 + + + + + Quick Load Slot 6 + + + + + Quick Load Slot 7 + + + + + Quick Load Slot 8 + + + + + Quick Load Slot 9 + + + + + Rewind + + + + + Grab Mouse + + + + + Swap Controllers 1 and 2 + + + + + Toggle BG Layer 0 + + + + + Toggle BG Layer 1 + + + + + Toggle BG Layer 2 + + + + + Toggle BG Layer 3 + + + + + Toggle Sprites + + + + + Change Backdrop Color + + + + + Toggle Sound Channel 1 + + + + + Toggle Sound Channel 2 + + + + + Toggle Sound Channel 3 + + + + + Toggle Sound Channel 4 + + + + + Toggle Sound Channel 5 + + + + + Toggle Sound Channel 6 + + + + + Toggle Sound Channel 7 + + + + + Toggle Sound Channel 8 + + + + + Toggle All Sound Channels + + + + + Start Recording + + + + + Stop Recording + + + + + Seek to Frame + + + + + Binding #1 + + + + + Binding #2 + + + + + Binding #3 + + + + + Binding #4 + + + + + + + + + diff --git a/qt/src/Snes9xController.cpp b/qt/src/Snes9xController.cpp new file mode 100644 index 00000000..6206d11b --- /dev/null +++ b/qt/src/Snes9xController.cpp @@ -0,0 +1,749 @@ +#include "Snes9xController.hpp" +#include "SoftwareScalers.hpp" +#include +#include +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 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()); +} \ No newline at end of file diff --git a/qt/src/Snes9xController.hpp b/qt/src/Snes9xController.hpp new file mode 100644 index 00000000..01cd2ee2 --- /dev/null +++ b/qt/src/Snes9xController.hpp @@ -0,0 +1,64 @@ +#ifndef __SNES9X_CONTROLLER_HPP +#define __SNES9X_CONTROLLER_HPP +#include +#include +#include +#include +#include + +#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 screen_output_function = nullptr; + std::function sound_output_function = nullptr; + + bool active = false; + + protected: + Snes9xController(); + ~Snes9xController(); + + private: + void SamplesAvailable(); + +}; + +#endif \ No newline at end of file diff --git a/qt/src/SoftwareScalers.cpp b/qt/src/SoftwareScalers.cpp new file mode 100644 index 00000000..71b5bc59 --- /dev/null +++ b/qt/src/SoftwareScalers.cpp @@ -0,0 +1,43 @@ +#include + +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; +} diff --git a/qt/src/SoftwareScalers.hpp b/qt/src/SoftwareScalers.hpp new file mode 100644 index 00000000..7ab7cdbc --- /dev/null +++ b/qt/src/SoftwareScalers.hpp @@ -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); \ No newline at end of file diff --git a/qt/src/SoundPanel.cpp b/qt/src/SoundPanel.cpp new file mode 100644 index 00000000..e0dec1d4 --- /dev/null +++ b/qt/src/SoundPanel.cpp @@ -0,0 +1,131 @@ +#include "SoundPanel.hpp" +#include + +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); +} + diff --git a/qt/src/SoundPanel.hpp b/qt/src/SoundPanel.hpp new file mode 100644 index 00000000..8dc3633a --- /dev/null +++ b/qt/src/SoundPanel.hpp @@ -0,0 +1,18 @@ +#pragma once +#include "ui_SoundPanel.h" +#include "EmuApplication.hpp" +#include + +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 driver_list; +}; \ No newline at end of file diff --git a/qt/src/SoundPanel.ui b/qt/src/SoundPanel.ui new file mode 100644 index 00000000..1ade3168 --- /dev/null +++ b/qt/src/SoundPanel.ui @@ -0,0 +1,302 @@ + + + SoundPanel + + + + 0 + 0 + 705 + 596 + + + + Form + + + + + + Sound Output + + + + + + + + + 0 + 0 + + + + ms + + + 16 + + + 256 + + + 32 + + + + + + + + 0 + 0 + + + + + 48000 Hz + + + + + 44100 Hz + + + + + + + + Buffer size: + + + + + + + Device: + + + + + + + + 0 + 0 + + + + Choose a device to render output. If you have no integrated graphics, there will be only one choice. + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Driver: + + + + + + + + 0 + 0 + + + + Select the output driver. + + + + + + + Playback rate: + + + + + + + + + + + + Sound Stretching + + + + + + + + Dynamic rate control + + + + + + + Adjust input rate to display rate automatically + + + + + + + + + + + Input rate: + + + + + + + 31987 + + + Qt::AlignCenter + + + + + + + + + 31800 + + + 32200 + + + 31987 + + + Qt::Horizontal + + + QSlider::NoTicks + + + + + + + + + + + + + + 0 + 0 + + + + Dynamic rate limit: + + + + + + + + 0 + 0 + + + + 3 + + + 0.050000000000000 + + + 0.001000000000000 + + + 0.005000000000000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Mute + + + + + + Mute all sound + + + + + + + Mute sound during turbo or rewind + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/qt/src/main.cpp b/qt/src/main.cpp new file mode 100644 index 00000000..3fea65fc --- /dev/null +++ b/qt/src/main.cpp @@ -0,0 +1,23 @@ +#include "EmuApplication.hpp" + +int main(int argc, char *argv[]) +{ + EmuApplication emu; + emu.qtapp = std::make_unique(argc, argv); + + emu.config = std::make_unique(); + emu.config->setDefaults(); + emu.config->loadFile(EmuConfig::findConfigFile()); + + emu.input_manager = std::make_unique(); + emu.window = std::make_unique(&emu); + emu.window->show(); + + emu.updateBindings(); + emu.startIdleLoop(); + emu.qtapp->exec(); + + emu.config->saveFile(EmuConfig::findConfigFile()); + + return 0; +} diff --git a/qt/src/resources/blackicons/a.svg b/qt/src/resources/blackicons/a.svg new file mode 100644 index 00000000..53823285 --- /dev/null +++ b/qt/src/resources/blackicons/a.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + diff --git a/qt/src/resources/blackicons/b.svg b/qt/src/resources/blackicons/b.svg new file mode 100644 index 00000000..3a5cbf73 --- /dev/null +++ b/qt/src/resources/blackicons/b.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + diff --git a/qt/src/resources/blackicons/display.svg b/qt/src/resources/blackicons/display.svg new file mode 100644 index 00000000..400b8e20 --- /dev/null +++ b/qt/src/resources/blackicons/display.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/qt/src/resources/blackicons/down.svg b/qt/src/resources/blackicons/down.svg new file mode 100644 index 00000000..b79cc15b --- /dev/null +++ b/qt/src/resources/blackicons/down.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + diff --git a/qt/src/resources/blackicons/emulation.svg b/qt/src/resources/blackicons/emulation.svg new file mode 100644 index 00000000..21a6e932 --- /dev/null +++ b/qt/src/resources/blackicons/emulation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/qt/src/resources/blackicons/folders.svg b/qt/src/resources/blackicons/folders.svg new file mode 100644 index 00000000..f45e1306 --- /dev/null +++ b/qt/src/resources/blackicons/folders.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/qt/src/resources/blackicons/joypad.svg b/qt/src/resources/blackicons/joypad.svg new file mode 100644 index 00000000..dc2a7ed3 --- /dev/null +++ b/qt/src/resources/blackicons/joypad.svg @@ -0,0 +1,47 @@ + + + + + + + + diff --git a/qt/src/resources/blackicons/key.svg b/qt/src/resources/blackicons/key.svg new file mode 100644 index 00000000..2b10d871 --- /dev/null +++ b/qt/src/resources/blackicons/key.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + diff --git a/qt/src/resources/blackicons/keyboard.svg b/qt/src/resources/blackicons/keyboard.svg new file mode 100644 index 00000000..19384fcd --- /dev/null +++ b/qt/src/resources/blackicons/keyboard.svg @@ -0,0 +1 @@ + diff --git a/qt/src/resources/blackicons/l.svg b/qt/src/resources/blackicons/l.svg new file mode 100644 index 00000000..32699e43 --- /dev/null +++ b/qt/src/resources/blackicons/l.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + diff --git a/qt/src/resources/blackicons/left.svg b/qt/src/resources/blackicons/left.svg new file mode 100644 index 00000000..34b9eaf3 --- /dev/null +++ b/qt/src/resources/blackicons/left.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + diff --git a/qt/src/resources/blackicons/r.svg b/qt/src/resources/blackicons/r.svg new file mode 100644 index 00000000..589bcedb --- /dev/null +++ b/qt/src/resources/blackicons/r.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + diff --git a/qt/src/resources/blackicons/right.svg b/qt/src/resources/blackicons/right.svg new file mode 100644 index 00000000..67d16be5 --- /dev/null +++ b/qt/src/resources/blackicons/right.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + diff --git a/qt/src/resources/blackicons/select.svg b/qt/src/resources/blackicons/select.svg new file mode 100644 index 00000000..54ada4b5 --- /dev/null +++ b/qt/src/resources/blackicons/select.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + diff --git a/qt/src/resources/blackicons/settings.svg b/qt/src/resources/blackicons/settings.svg new file mode 100644 index 00000000..66aadd02 --- /dev/null +++ b/qt/src/resources/blackicons/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/qt/src/resources/blackicons/shader.svg b/qt/src/resources/blackicons/shader.svg new file mode 100644 index 00000000..5bcc860a --- /dev/null +++ b/qt/src/resources/blackicons/shader.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/qt/src/resources/blackicons/sound.svg b/qt/src/resources/blackicons/sound.svg new file mode 100644 index 00000000..b9ca176a --- /dev/null +++ b/qt/src/resources/blackicons/sound.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/qt/src/resources/blackicons/start.svg b/qt/src/resources/blackicons/start.svg new file mode 100644 index 00000000..0727f416 --- /dev/null +++ b/qt/src/resources/blackicons/start.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + diff --git a/qt/src/resources/blackicons/up.svg b/qt/src/resources/blackicons/up.svg new file mode 100644 index 00000000..73673c2d --- /dev/null +++ b/qt/src/resources/blackicons/up.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + diff --git a/qt/src/resources/blackicons/x.svg b/qt/src/resources/blackicons/x.svg new file mode 100644 index 00000000..10b8cb9e --- /dev/null +++ b/qt/src/resources/blackicons/x.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + diff --git a/qt/src/resources/blackicons/y.svg b/qt/src/resources/blackicons/y.svg new file mode 100644 index 00000000..082f50c3 --- /dev/null +++ b/qt/src/resources/blackicons/y.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + diff --git a/qt/src/resources/snes9x.qrc b/qt/src/resources/snes9x.qrc new file mode 100644 index 00000000..719ac1d2 --- /dev/null +++ b/qt/src/resources/snes9x.qrc @@ -0,0 +1,25 @@ + + + blackicons/settings.svg + blackicons/folders.svg + blackicons/emulation.svg + blackicons/sound.svg + blackicons/keyboard.svg + blackicons/display.svg + blackicons/joypad.svg + blackicons/key.svg + blackicons/a.svg + blackicons/b.svg + blackicons/l.svg + blackicons/left.svg + blackicons/down.svg + blackicons/r.svg + blackicons/right.svg + blackicons/select.svg + blackicons/start.svg + blackicons/up.svg + blackicons/x.svg + blackicons/y.svg + blackicons/shader.svg + + diff --git a/qt/src/resources/untitled.ui b/qt/src/resources/untitled.ui new file mode 100644 index 00000000..e769dabe --- /dev/null +++ b/qt/src/resources/untitled.ui @@ -0,0 +1,175 @@ + + + Dialog + + + + 0 + 0 + 895 + 651 + + + + Dialog + + + + + + true + + + Qt::SolidLine + + + false + + + true + + + + Up + + + + :/icons/icons/up.svg:/icons/icons/up.svg + + + + + Down + + + + :/icons/icons/down.svg:/icons/icons/down.svg + + + + + Left + + + + :/icons/icons/left.svg:/icons/icons/left.svg + + + + + Right + + + + :/icons/icons/right.svg:/icons/icons/right.svg + + + + + A + + + + :/icons/icons/a.svg:/icons/icons/a.svg + + + + + B + + + + :/icons/icons/b.svg:/icons/icons/b.svg + + + + + X + + + + :/icons/icons/x.svg:/icons/icons/x.svg + + + + + Y + + + + :/icons/icons/y.svg:/icons/icons/y.svg + + + + + L + + + + :/icons/icons/l.svg:/icons/icons/l.svg + + + + + R + + + + :/icons/icons/r.svg:/icons/icons/r.svg + + + + + Start + + + + :/icons/icons/start.svg:/icons/icons/start.svg + + + + + Select + + + + :/icons/icons/select.svg:/icons/icons/select.svg + + + + + Binding #1 + + + + + Binding #2 + + + + + Binding #3 + + + + + Binding #4 + + + + + Up + + + + + + + + + + + + + + diff --git a/qt/src/toml.hpp b/qt/src/toml.hpp new file mode 100644 index 00000000..6ce4617d --- /dev/null +++ b/qt/src/toml.hpp @@ -0,0 +1,11869 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// toml++ v2.1.0 +// https://github.com/marzer/tomlplusplus +// SPDX-License-Identifier: MIT +// +//---------------------------------------------------------------------------------------------------------------------- +// +// - THIS FILE WAS ASSEMBLED FROM MULTIPLE HEADER FILES BY A SCRIPT - PLEASE DON'T EDIT IT DIRECTLY - +// +// If you wish to submit a contribution to toml++, hooray and thanks! Before you crack on, please be aware that this +// file was assembled from a number of smaller files by a python script, and code contributions should not be made +// against it directly. You should instead make your changes in the relevant source file(s). The file names of the files +// that contributed to this header can be found at the beginnings and ends of the corresponding sections of this file. +// +//---------------------------------------------------------------------------------------------------------------------- +// +// TOML Language Specifications: +// latest: https://github.com/toml-lang/toml/blob/master/README.md +// v1.0.0-rc.2: https://toml.io/en/v1.0.0-rc.2 +// v1.0.0-rc.1: https://toml.io/en/v1.0.0-rc.1 +// v0.5.0: https://toml.io/en/v0.5.0 +// changelog: https://github.com/toml-lang/toml/blob/master/CHANGELOG.md +// +//---------------------------------------------------------------------------------------------------------------------- +// +// MIT License +// +// Copyright (c) 2019-2020 Mark Gillard +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +// Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +//---------------------------------------------------------------------------------------------------------------------- +#ifndef INCLUDE_TOMLPLUSPLUS_H +#define INCLUDE_TOMLPLUSPLUS_H + +#if 1 //------ ↓ toml_preprocessor.h -------------------------------------------------------------------------------- + +#ifndef __cplusplus + #error toml++ is a C++ library. +#endif + +#ifdef __INTELLISENSE__ + #define TOML_INTELLISENSE 1 +#else + #define TOML_INTELLISENSE 0 +#endif +#ifdef __clang__ + #define TOML_CLANG __clang_major__ +#else + #define TOML_CLANG 0 +#endif +#ifdef __INTEL_COMPILER + #define TOML_ICC __INTEL_COMPILER + #ifdef __ICL + #define TOML_ICC_CL TOML_ICC + #else + #define TOML_ICC_CL 0 + #endif +#else + #define TOML_ICC 0 + #define TOML_ICC_CL 0 +#endif +#if defined(_MSC_VER) && !TOML_CLANG && !TOML_ICC + #define TOML_MSVC _MSC_VER +#else + #define TOML_MSVC 0 +#endif +#if defined(__GNUC__) && !TOML_CLANG && !TOML_ICC + #define TOML_GCC __GNUC__ +#else + #define TOML_GCC 0 +#endif + +#if TOML_CLANG + + #define TOML_PUSH_WARNINGS _Pragma("clang diagnostic push") + #define TOML_DISABLE_SWITCH_WARNINGS _Pragma("clang diagnostic ignored \"-Wswitch\"") + #define TOML_DISABLE_INIT_WARNINGS _Pragma("clang diagnostic ignored \"-Wmissing-field-initializers\"") + #define TOML_DISABLE_ARITHMETIC_WARNINGS _Pragma("clang diagnostic ignored \"-Wfloat-equal\"") \ + _Pragma("clang diagnostic ignored \"-Wdouble-promotion\"") \ + _Pragma("clang diagnostic ignored \"-Wchar-subscripts\"") \ + _Pragma("clang diagnostic ignored \"-Wshift-sign-overflow\"") + #define TOML_DISABLE_SHADOW_WARNINGS _Pragma("clang diagnostic ignored \"-Wshadow\"") + #define TOML_DISABLE_SPAM_WARNINGS _Pragma("clang diagnostic ignored \"-Wweak-vtables\"") \ + _Pragma("clang diagnostic ignored \"-Wweak-template-vtables\"") \ + _Pragma("clang diagnostic ignored \"-Wpadded\"") + #define TOML_POP_WARNINGS _Pragma("clang diagnostic pop") + #define TOML_DISABLE_WARNINGS TOML_PUSH_WARNINGS \ + _Pragma("clang diagnostic ignored \"-Weverything\"") + #define TOML_ENABLE_WARNINGS TOML_POP_WARNINGS + #define TOML_ASSUME(cond) __builtin_assume(cond) + #define TOML_UNREACHABLE __builtin_unreachable() + #define TOML_ATTR(...) __attribute__((__VA_ARGS__)) + #if defined(_MSC_VER) // msvc compat mode + #ifdef __has_declspec_attribute + #if __has_declspec_attribute(novtable) + #define TOML_INTERFACE __declspec(novtable) + #endif + #if __has_declspec_attribute(empty_bases) + #define TOML_EMPTY_BASES __declspec(empty_bases) + #endif + #ifndef TOML_ALWAYS_INLINE + #define TOML_ALWAYS_INLINE __forceinline + #endif + #if __has_declspec_attribute(noinline) + #define TOML_NEVER_INLINE __declspec(noinline) + #endif + #endif + #endif + #ifdef __has_attribute + #if !defined(TOML_ALWAYS_INLINE) && __has_attribute(always_inline) + #define TOML_ALWAYS_INLINE __attribute__((__always_inline__)) inline + #endif + #if !defined(TOML_NEVER_INLINE) && __has_attribute(noinline) + #define TOML_NEVER_INLINE __attribute__((__noinline__)) + #endif + #if !defined(TOML_TRIVIAL_ABI) && __has_attribute(trivial_abi) + #define TOML_TRIVIAL_ABI __attribute__((__trivial_abi__)) + #endif + #endif + #define TOML_LIKELY(...) (__builtin_expect(!!(__VA_ARGS__), 1) ) + #define TOML_UNLIKELY(...) (__builtin_expect(!!(__VA_ARGS__), 0) ) + + //floating-point from_chars and to_chars are not implemented in any version of clang as of 1/1/2020 + #ifndef TOML_FLOAT_CHARCONV + #define TOML_FLOAT_CHARCONV 0 + #endif + + #define TOML_SIMPLE_STATIC_ASSERT_MESSAGES 1 + +#endif // clang + +#if TOML_MSVC || TOML_ICC_CL + + #define TOML_CPP_VERSION _MSVC_LANG + #define TOML_PUSH_WARNINGS __pragma(warning(push)) + #if TOML_MSVC // !intel-cl + #define TOML_PUSH_WARNINGS __pragma(warning(push)) + #define TOML_DISABLE_SWITCH_WARNINGS __pragma(warning(disable: 4063)) + #define TOML_POP_WARNINGS __pragma(warning(pop)) + #define TOML_DISABLE_WARNINGS __pragma(warning(push, 0)) + #define TOML_ENABLE_WARNINGS TOML_POP_WARNINGS + #endif + #ifndef TOML_ALWAYS_INLINE + #define TOML_ALWAYS_INLINE __forceinline + #endif + #define TOML_NEVER_INLINE __declspec(noinline) + #define TOML_ASSUME(cond) __assume(cond) + #define TOML_UNREACHABLE __assume(0) + #define TOML_INTERFACE __declspec(novtable) + #define TOML_EMPTY_BASES __declspec(empty_bases) + #if !defined(TOML_RELOPS_REORDERING) && defined(__cpp_impl_three_way_comparison) + #define TOML_RELOPS_REORDERING 1 + #endif + +#endif // msvc + +#if TOML_ICC + + #define TOML_PUSH_WARNINGS __pragma(warning(push)) + #define TOML_DISABLE_SPAM_WARNINGS __pragma(warning(disable: 82)) /* storage class is not first */ \ + __pragma(warning(disable: 111)) /* statement unreachable (false-positive) */ \ + __pragma(warning(disable: 1011)) /* missing return (false-positive) */ \ + __pragma(warning(disable: 2261)) /* assume expr side-effects discarded */ + #define TOML_POP_WARNINGS __pragma(warning(pop)) + #define TOML_DISABLE_WARNINGS __pragma(warning(push, 0)) + #define TOML_ENABLE_WARNINGS TOML_POP_WARNINGS + +#endif // icc + +#if TOML_GCC + + #define TOML_PUSH_WARNINGS _Pragma("GCC diagnostic push") + #define TOML_DISABLE_SWITCH_WARNINGS _Pragma("GCC diagnostic ignored \"-Wswitch\"") \ + _Pragma("GCC diagnostic ignored \"-Wswitch-enum\"") \ + _Pragma("GCC diagnostic ignored \"-Wswitch-default\"") + #define TOML_DISABLE_INIT_WARNINGS _Pragma("GCC diagnostic ignored \"-Wmissing-field-initializers\"") \ + _Pragma("GCC diagnostic ignored \"-Wmaybe-uninitialized\"") \ + _Pragma("GCC diagnostic ignored \"-Wuninitialized\"") + #define TOML_DISABLE_ARITHMETIC_WARNINGS _Pragma("GCC diagnostic ignored \"-Wfloat-equal\"") \ + _Pragma("GCC diagnostic ignored \"-Wsign-conversion\"") \ + _Pragma("GCC diagnostic ignored \"-Wchar-subscripts\"") + #define TOML_DISABLE_SHADOW_WARNINGS _Pragma("GCC diagnostic ignored \"-Wshadow\"") + #define TOML_DISABLE_SPAM_WARNINGS _Pragma("GCC diagnostic ignored \"-Wpadded\"") \ + _Pragma("GCC diagnostic ignored \"-Wcast-align\"") \ + _Pragma("GCC diagnostic ignored \"-Wcomment\"") \ + _Pragma("GCC diagnostic ignored \"-Wtype-limits\"") \ + _Pragma("GCC diagnostic ignored \"-Wsuggest-attribute=const\"") \ + _Pragma("GCC diagnostic ignored \"-Wsuggest-attribute=pure\"") + #define TOML_POP_WARNINGS _Pragma("GCC diagnostic pop") + #define TOML_DISABLE_WARNINGS TOML_PUSH_WARNINGS \ + _Pragma("GCC diagnostic ignored \"-Wall\"") \ + _Pragma("GCC diagnostic ignored \"-Wextra\"") \ + _Pragma("GCC diagnostic ignored \"-Wpedantic\"") \ + TOML_DISABLE_SWITCH_WARNINGS \ + TOML_DISABLE_INIT_WARNINGS \ + TOML_DISABLE_ARITHMETIC_WARNINGS \ + TOML_DISABLE_SHADOW_WARNINGS \ + TOML_DISABLE_SPAM_WARNINGS + #define TOML_ENABLE_WARNINGS TOML_POP_WARNINGS + + #define TOML_ATTR(...) __attribute__((__VA_ARGS__)) + #ifndef TOML_ALWAYS_INLINE + #define TOML_ALWAYS_INLINE __attribute__((__always_inline__)) inline + #endif + #define TOML_NEVER_INLINE __attribute__((__noinline__)) + #define TOML_UNREACHABLE __builtin_unreachable() + #if !defined(TOML_RELOPS_REORDERING) && defined(__cpp_impl_three_way_comparison) + #define TOML_RELOPS_REORDERING 1 + #endif + #define TOML_LIKELY(...) (__builtin_expect(!!(__VA_ARGS__), 1) ) + #define TOML_UNLIKELY(...) (__builtin_expect(!!(__VA_ARGS__), 0) ) + + // floating-point from_chars and to_chars are not implemented in any version of gcc as of 1/1/2020 + #ifndef TOML_FLOAT_CHARCONV + #define TOML_FLOAT_CHARCONV 0 + #endif + +#endif + +#ifdef TOML_CONFIG_HEADER + #include TOML_CONFIG_HEADER +#endif + +#ifdef DOXYGEN + #define TOML_HEADER_ONLY 0 + #define TOML_WINDOWS_COMPAT 1 +#endif + +#if defined(TOML_ALL_INLINE) && !defined(TOML_HEADER_ONLY) + #define TOML_HEADER_ONLY TOML_ALL_INLINE +#endif + +#if !defined(TOML_HEADER_ONLY) || (defined(TOML_HEADER_ONLY) && TOML_HEADER_ONLY) || TOML_INTELLISENSE + #undef TOML_HEADER_ONLY + #define TOML_HEADER_ONLY 1 +#endif + +#if defined(TOML_IMPLEMENTATION) || TOML_HEADER_ONLY + #undef TOML_IMPLEMENTATION + #define TOML_IMPLEMENTATION 1 +#else + #define TOML_IMPLEMENTATION 0 +#endif + +#ifndef TOML_API + #define TOML_API +#endif + +#ifndef TOML_UNRELEASED_FEATURES + #define TOML_UNRELEASED_FEATURES 0 +#endif + +#ifndef TOML_LARGE_FILES + #define TOML_LARGE_FILES 0 +#endif + +#ifndef TOML_UNDEF_MACROS + #define TOML_UNDEF_MACROS 1 +#endif + +#ifndef TOML_PARSER + #define TOML_PARSER 1 +#endif + +#ifndef DOXYGEN + #if defined(_WIN32) && !defined(TOML_WINDOWS_COMPAT) + #define TOML_WINDOWS_COMPAT 1 + #endif + #if !defined(_WIN32) || !defined(TOML_WINDOWS_COMPAT) + #undef TOML_WINDOWS_COMPAT + #define TOML_WINDOWS_COMPAT 0 + #endif +#endif + +#ifdef TOML_OPTIONAL_TYPE + #define TOML_HAS_CUSTOM_OPTIONAL_TYPE 1 +#else + #define TOML_HAS_CUSTOM_OPTIONAL_TYPE 0 +#endif + +#ifdef TOML_CHAR_8_STRINGS + #if TOML_CHAR_8_STRINGS + #error TOML_CHAR_8_STRINGS was removed in toml++ 2.0.0; \ +all value setters and getters can now work with char8_t strings implicitly so changing the underlying string type \ +is no longer necessary. + #endif +#endif + +#ifndef TOML_CPP_VERSION + #define TOML_CPP_VERSION __cplusplus +#endif +#if TOML_CPP_VERSION < 201103L + #error toml++ requires C++17 or higher. For a TOML library supporting pre-C++11 see https://github.com/ToruNiina/Boost.toml +#elif TOML_CPP_VERSION < 201703L + #error toml++ requires C++17 or higher. For a TOML library supporting C++11 see https://github.com/ToruNiina/toml11 +#elif TOML_CPP_VERSION >= 202600L + #define TOML_CPP 26 +#elif TOML_CPP_VERSION >= 202300L + #define TOML_CPP 23 +#elif TOML_CPP_VERSION >= 202002L + #define TOML_CPP 20 +#elif TOML_CPP_VERSION >= 201703L + #define TOML_CPP 17 +#endif +#undef TOML_CPP_VERSION + +#ifdef __has_include + #define TOML_HAS_INCLUDE(header) __has_include(header) +#else + #define TOML_HAS_INCLUDE(header) 0 +#endif + +#define TOML_COMPILER_EXCEPTIONS 0 +#if TOML_COMPILER_EXCEPTIONS + #if !defined(TOML_EXCEPTIONS) || (defined(TOML_EXCEPTIONS) && TOML_EXCEPTIONS) + #undef TOML_EXCEPTIONS + #define TOML_EXCEPTIONS 1 + #endif +#else + #if defined(TOML_EXCEPTIONS) && TOML_EXCEPTIONS + #error TOML_EXCEPTIONS was explicitly enabled but exceptions are disabled/unsupported by the compiler. + #endif + #undef TOML_EXCEPTIONS + #define TOML_EXCEPTIONS 0 +#endif + +#if TOML_EXCEPTIONS + #define TOML_MAY_THROW +#else + #define TOML_MAY_THROW noexcept +#endif + +#ifndef TOML_INT_CHARCONV + #define TOML_INT_CHARCONV 1 +#endif +#ifndef TOML_FLOAT_CHARCONV + #define TOML_FLOAT_CHARCONV 1 +#endif +#if (TOML_INT_CHARCONV || TOML_FLOAT_CHARCONV) && !TOML_HAS_INCLUDE() + #undef TOML_INT_CHARCONV + #undef TOML_FLOAT_CHARCONV + #define TOML_INT_CHARCONV 0 + #define TOML_FLOAT_CHARCONV 0 +#endif + +#ifndef TOML_PUSH_WARNINGS + #define TOML_PUSH_WARNINGS +#endif +#ifndef TOML_DISABLE_SWITCH_WARNINGS + #define TOML_DISABLE_SWITCH_WARNINGS +#endif +#ifndef TOML_DISABLE_INIT_WARNINGS + #define TOML_DISABLE_INIT_WARNINGS +#endif +#ifndef TOML_DISABLE_SPAM_WARNINGS + #define TOML_DISABLE_SPAM_WARNINGS +#endif +#ifndef TOML_DISABLE_ARITHMETIC_WARNINGS + #define TOML_DISABLE_ARITHMETIC_WARNINGS +#endif +#ifndef TOML_DISABLE_SHADOW_WARNINGS + #define TOML_DISABLE_SHADOW_WARNINGS +#endif +#ifndef TOML_POP_WARNINGS + #define TOML_POP_WARNINGS +#endif +#ifndef TOML_DISABLE_WARNINGS + #define TOML_DISABLE_WARNINGS +#endif +#ifndef TOML_ENABLE_WARNINGS + #define TOML_ENABLE_WARNINGS +#endif + +#ifndef TOML_ATTR + #define TOML_ATTR(...) +#endif + +#ifndef TOML_INTERFACE + #define TOML_INTERFACE +#endif + +#ifndef TOML_EMPTY_BASES + #define TOML_EMPTY_BASES +#endif + +#ifndef TOML_NEVER_INLINE + #define TOML_NEVER_INLINE +#endif + +#ifndef TOML_ASSUME + #define TOML_ASSUME(cond) (void)0 +#endif + +#ifndef TOML_UNREACHABLE + #define TOML_UNREACHABLE TOML_ASSERT(false) +#endif + +#define TOML_NO_DEFAULT_CASE default: TOML_UNREACHABLE + +#ifdef __cpp_consteval + #define TOML_CONSTEVAL consteval +#else + #define TOML_CONSTEVAL constexpr +#endif + +#ifdef __has_cpp_attribute + #define TOML_HAS_ATTR(...) __has_cpp_attribute(__VA_ARGS__) +#else + #define TOML_HAS_ATTR(...) 0 +#endif + +#if !defined(DOXYGEN) && !TOML_INTELLISENSE + #if !defined(TOML_LIKELY) && TOML_HAS_ATTR(likely) + #define TOML_LIKELY(...) (__VA_ARGS__) [[likely]] + #endif + #if !defined(TOML_UNLIKELY) && TOML_HAS_ATTR(unlikely) + #define TOML_UNLIKELY(...) (__VA_ARGS__) [[unlikely]] + #endif + #if TOML_HAS_ATTR(nodiscard) >= 201907L + #define TOML_NODISCARD_CTOR [[nodiscard]] + #endif +#endif + +#ifndef TOML_LIKELY + #define TOML_LIKELY(...) (__VA_ARGS__) +#endif +#ifndef TOML_UNLIKELY + #define TOML_UNLIKELY(...) (__VA_ARGS__) +#endif +#ifndef TOML_NODISCARD_CTOR + #define TOML_NODISCARD_CTOR +#endif + +#ifndef TOML_TRIVIAL_ABI + #define TOML_TRIVIAL_ABI +#endif + +#ifndef TOML_RELOPS_REORDERING + #define TOML_RELOPS_REORDERING 0 +#endif +#if TOML_RELOPS_REORDERING + #define TOML_ASYMMETRICAL_EQUALITY_OPS(...) +#else + #define TOML_ASYMMETRICAL_EQUALITY_OPS(LHS, RHS, ...) \ + __VA_ARGS__ [[nodiscard]] friend bool operator == (RHS rhs, LHS lhs) noexcept { return lhs == rhs; } \ + __VA_ARGS__ [[nodiscard]] friend bool operator != (LHS lhs, RHS rhs) noexcept { return !(lhs == rhs); } \ + __VA_ARGS__ [[nodiscard]] friend bool operator != (RHS rhs, LHS lhs) noexcept { return !(lhs == rhs); } +#endif + +#ifndef TOML_SIMPLE_STATIC_ASSERT_MESSAGES + #define TOML_SIMPLE_STATIC_ASSERT_MESSAGES 0 +#endif + +#define TOML_CONCAT_1(x, y) x##y +#define TOML_CONCAT(x, y) TOML_CONCAT_1(x, y) + +#define TOML_EVAL_BOOL_1(T, F) T +#define TOML_EVAL_BOOL_0(T, F) F + +#if defined(__aarch64__) || defined(__ARM_ARCH_ISA_A64) || defined(_M_ARM64) || defined(__ARM_64BIT_STATE) \ + || defined(__arm__) || defined(_M_ARM) || defined(__ARM_32BIT_STATE) + #define TOML_ARM 1 +#else + #define TOML_ARM 0 +#endif + +#define TOML_MAKE_BITOPS(type) \ + [[nodiscard]] \ + TOML_ALWAYS_INLINE \ + TOML_ATTR(const) \ + TOML_ATTR(flatten) \ + constexpr type operator & (type lhs, type rhs) noexcept \ + { \ + return static_cast(::toml::impl::unwrap_enum(lhs) & ::toml::impl::unwrap_enum(rhs)); \ + } \ + [[nodiscard]] \ + TOML_ALWAYS_INLINE \ + TOML_ATTR(const) \ + TOML_ATTR(flatten) \ + constexpr type operator | (type lhs, type rhs) noexcept \ + { \ + return static_cast(::toml::impl::unwrap_enum(lhs) | ::toml::impl::unwrap_enum(rhs)); \ + } + +#ifdef __FLT16_MANT_DIG__ + #if __FLT_RADIX__ == 2 \ + && __FLT16_MANT_DIG__ == 11 \ + && __FLT16_DIG__ == 3 \ + && __FLT16_MIN_EXP__ == -13 \ + && __FLT16_MIN_10_EXP__ == -4 \ + && __FLT16_MAX_EXP__ == 16 \ + && __FLT16_MAX_10_EXP__ == 4 + #if (TOML_ARM && TOML_GCC) || TOML_CLANG + #define TOML_FP16 __fp16 + #endif + #if TOML_ARM && TOML_CLANG // not present in g++ + #define TOML_FLOAT16 _Float16 + #endif + #endif +#endif + +#if defined(__SIZEOF_FLOAT128__) \ + && defined(__FLT128_MANT_DIG__) \ + && defined(__LDBL_MANT_DIG__) \ + && __FLT128_MANT_DIG__ > __LDBL_MANT_DIG__ + #define TOML_FLOAT128 __float128 +#endif + +#ifdef __SIZEOF_INT128__ + #define TOML_INT128 __int128_t + #define TOML_UINT128 __uint128_t +#endif + +#define TOML_LIB_MAJOR 2 +#define TOML_LIB_MINOR 1 +#define TOML_LIB_PATCH 0 + +#define TOML_LANG_MAJOR 1 +#define TOML_LANG_MINOR 0 +#define TOML_LANG_PATCH 0 + +#define TOML_LIB_SINGLE_HEADER 1 + +#define TOML_MAKE_VERSION(maj, min, rev) \ + ((maj) * 1000 + (min) * 25 + (rev)) + +#if TOML_UNRELEASED_FEATURES + #define TOML_LANG_EFFECTIVE_VERSION \ + TOML_MAKE_VERSION(TOML_LANG_MAJOR, TOML_LANG_MINOR, TOML_LANG_PATCH+1) +#else + #define TOML_LANG_EFFECTIVE_VERSION \ + TOML_MAKE_VERSION(TOML_LANG_MAJOR, TOML_LANG_MINOR, TOML_LANG_PATCH) +#endif + +#define TOML_LANG_HIGHER_THAN(maj, min, rev) \ + (TOML_LANG_EFFECTIVE_VERSION > TOML_MAKE_VERSION(maj, min, rev)) + +#define TOML_LANG_AT_LEAST(maj, min, rev) \ + (TOML_LANG_EFFECTIVE_VERSION >= TOML_MAKE_VERSION(maj, min, rev)) + +#define TOML_LANG_UNRELEASED \ + TOML_LANG_HIGHER_THAN(TOML_LANG_MAJOR, TOML_LANG_MINOR, TOML_LANG_PATCH) + +#ifndef TOML_ABI_NAMESPACES + #ifdef DOXYGEN + #define TOML_ABI_NAMESPACES 0 + #else + #define TOML_ABI_NAMESPACES 1 + #endif +#endif +#if TOML_ABI_NAMESPACES + #define TOML_NAMESPACE_START namespace toml { inline namespace TOML_CONCAT(v, TOML_LIB_MAJOR) + #define TOML_NAMESPACE_END } + #define TOML_NAMESPACE ::toml::TOML_CONCAT(v, TOML_LIB_MAJOR) + #define TOML_ABI_NAMESPACE_START(name) inline namespace name { + #define TOML_ABI_NAMESPACE_BOOL(cond, T, F) TOML_ABI_NAMESPACE_START(TOML_CONCAT(TOML_EVAL_BOOL_, cond)(T, F)) + #define TOML_ABI_NAMESPACE_END } +#else + #define TOML_NAMESPACE_START namespace toml + #define TOML_NAMESPACE_END + #define TOML_NAMESPACE toml + #define TOML_ABI_NAMESPACE_START(...) + #define TOML_ABI_NAMESPACE_BOOL(...) + #define TOML_ABI_NAMESPACE_END +#endif +#define TOML_IMPL_NAMESPACE_START TOML_NAMESPACE_START { namespace impl +#define TOML_IMPL_NAMESPACE_END } TOML_NAMESPACE_END +#if TOML_HEADER_ONLY + #define TOML_ANON_NAMESPACE_START TOML_IMPL_NAMESPACE_START + #define TOML_ANON_NAMESPACE_END TOML_IMPL_NAMESPACE_END + #define TOML_ANON_NAMESPACE TOML_NAMESPACE::impl + #define TOML_USING_ANON_NAMESPACE using namespace TOML_ANON_NAMESPACE + #define TOML_EXTERNAL_LINKAGE inline + #define TOML_INTERNAL_LINKAGE inline +#else + #define TOML_ANON_NAMESPACE_START namespace + #define TOML_ANON_NAMESPACE_END + #define TOML_ANON_NAMESPACE + #define TOML_USING_ANON_NAMESPACE (void)0 + #define TOML_EXTERNAL_LINKAGE + #define TOML_INTERNAL_LINKAGE static +#endif + +TOML_DISABLE_WARNINGS +#ifndef TOML_ASSERT + #if defined(NDEBUG) || !defined(_DEBUG) + #define TOML_ASSERT(expr) (void)0 + #else + #ifndef assert + #include + #endif + #define TOML_ASSERT(expr) assert(expr) + #endif +#endif +TOML_ENABLE_WARNINGS + +#endif //------ ↑ toml_preprocessor.h -------------------------------------------------------------------------------- + +TOML_PUSH_WARNINGS +TOML_DISABLE_SPAM_WARNINGS + +#if 1 //---------------------------------- ↓ toml_common.h ---------------------------------------------------------- + +TOML_DISABLE_WARNINGS +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if !TOML_HAS_CUSTOM_OPTIONAL_TYPE + #include +#endif +#if TOML_HAS_INCLUDE() + #include +#endif +TOML_ENABLE_WARNINGS + +#ifdef __cpp_lib_launder + #define TOML_LAUNDER(x) std::launder(x) +#else + #define TOML_LAUNDER(x) x +#endif + +#ifndef DOXYGEN +#ifndef TOML_DISABLE_ENVIRONMENT_CHECKS +#define TOML_ENV_MESSAGE \ + "If you're seeing this error it's because you're building toml++ for an environment that doesn't conform to " \ + "one of the 'ground truths' assumed by the library. Essentially this just means that I don't have the " \ + "resources to test on more platforms, but I wish I did! You can try disabling the checks by defining " \ + "TOML_DISABLE_ENVIRONMENT_CHECKS, but your mileage may vary. Please consider filing an issue at " \ + "https://github.com/marzer/tomlplusplus/issues to help me improve support for your target environment. Thanks!" + +static_assert(CHAR_BIT == 8, TOML_ENV_MESSAGE); +static_assert(FLT_RADIX == 2, TOML_ENV_MESSAGE); +static_assert('A' == 65, TOML_ENV_MESSAGE); +static_assert(sizeof(double) == 8, TOML_ENV_MESSAGE); +static_assert(std::numeric_limits::is_iec559, TOML_ENV_MESSAGE); +static_assert(std::numeric_limits::digits == 53, TOML_ENV_MESSAGE); +static_assert(std::numeric_limits::digits10 == 15, TOML_ENV_MESSAGE); + +#undef TOML_ENV_MESSAGE +#endif // !TOML_DISABLE_ENVIRONMENT_CHECKS +#endif // !DOXYGEN + +#ifndef DOXYGEN // undocumented forward declarations are hidden from doxygen because they fuck it up =/ + +namespace toml // non-abi namespace; this is not an error +{ + using namespace std::string_literals; + using namespace std::string_view_literals; + using ::std::size_t; + using ::std::intptr_t; + using ::std::uintptr_t; + using ::std::ptrdiff_t; + using ::std::nullptr_t; + using ::std::int8_t; + using ::std::int16_t; + using ::std::int32_t; + using ::std::int64_t; + using ::std::uint8_t; + using ::std::uint16_t; + using ::std::uint32_t; + using ::std::uint64_t; + using ::std::uint_least32_t; + using ::std::uint_least64_t; + + // legacy typedefs + using string_char = char; + using string = std::string; + using string_view = std::string_view; +} + +TOML_NAMESPACE_START // abi namespace +{ + struct date; + struct time; + struct time_offset; + + TOML_ABI_NAMESPACE_BOOL(TOML_HAS_CUSTOM_OPTIONAL_TYPE, custopt, stdopt) + struct date_time; + TOML_ABI_NAMESPACE_END + + class node; + class array; + class table; + + template class node_view; + template class value; + template class default_formatter; + template class json_formatter; + + [[nodiscard]] TOML_API bool operator == (const array& lhs, const array& rhs) noexcept; + [[nodiscard]] TOML_API bool operator != (const array& lhs, const array& rhs) noexcept; + [[nodiscard]] TOML_API bool operator == (const table& lhs, const table& rhs) noexcept; + [[nodiscard]] TOML_API bool operator != (const table& lhs, const table& rhs) noexcept; + + template + std::basic_ostream& operator << (std::basic_ostream&, const array&); + template + std::basic_ostream& operator << (std::basic_ostream&, const value&); + template + std::basic_ostream& operator << (std::basic_ostream&, const table&); + template + std::basic_ostream& operator << (std::basic_ostream&, default_formatter&); + template + std::basic_ostream& operator << (std::basic_ostream&, default_formatter&&); + template + std::basic_ostream& operator << (std::basic_ostream&, json_formatter&); + template + std::basic_ostream& operator << (std::basic_ostream&, json_formatter&&); + template + inline std::basic_ostream& operator << (std::basic_ostream&, const node_view&); + + namespace impl + { + template + using string_map = std::map>; // heterogeneous lookup + + template + using remove_cvref_t = std::remove_cv_t>; + + template + inline constexpr bool is_one_of = (false || ... || std::is_same_v); + + template + inline constexpr bool is_cvref = std::is_reference_v || std::is_const_v || std::is_volatile_v; + + template + inline constexpr bool is_wide_string = is_one_of< + std::decay_t, + const wchar_t*, + wchar_t*, + std::wstring_view, + std::wstring + >; + + template + inline constexpr bool dependent_false = false; + + #if TOML_WINDOWS_COMPAT + [[nodiscard]] TOML_API std::string narrow(std::wstring_view) noexcept; + [[nodiscard]] TOML_API std::wstring widen(std::string_view) noexcept; + #ifdef __cpp_lib_char8_t + [[nodiscard]] TOML_API std::wstring widen(std::u8string_view) noexcept; + #endif + #endif // TOML_WINDOWS_COMPAT + + #if TOML_ABI_NAMESPACES + #if TOML_EXCEPTIONS + TOML_ABI_NAMESPACE_START(ex) + #define TOML_PARSER_TYPENAME TOML_NAMESPACE::impl::ex::parser + #else + TOML_ABI_NAMESPACE_START(noex) + #define TOML_PARSER_TYPENAME TOML_NAMESPACE::impl::noex::parser + #endif + #else + #define TOML_PARSER_TYPENAME TOML_NAMESPACE::impl::parser + #endif + class parser; + TOML_ABI_NAMESPACE_END // TOML_EXCEPTIONS + } +} +TOML_NAMESPACE_END + +#endif // !DOXYGEN + +namespace toml { } + +TOML_NAMESPACE_START // abi namespace +{ + inline namespace literals {} + + #if TOML_HAS_CUSTOM_OPTIONAL_TYPE + template + using optional = TOML_OPTIONAL_TYPE; + #else + template + using optional = std::optional; + #endif + + enum class node_type : uint8_t + { + none, + table, + array, + string, + integer, + floating_point, + boolean, + date, + time, + date_time + }; + + using source_path_ptr = std::shared_ptr; + + template + struct TOML_TRIVIAL_ABI inserter + { + T&& value; + }; + template inserter(T&&) -> inserter; +} +TOML_NAMESPACE_END + +TOML_IMPL_NAMESPACE_START +{ + // general value traits + // (as they relate to their equivalent native TOML type) + template + struct value_traits + { + using native_type = void; + static constexpr bool is_native = false; + static constexpr bool is_losslessly_convertible_to_native = false; + static constexpr bool can_represent_native = false; + static constexpr bool can_partially_represent_native = false; + static constexpr auto type = node_type::none; + }; + template struct value_traits : value_traits {}; + template struct value_traits : value_traits {}; + template struct value_traits : value_traits {}; + template struct value_traits : value_traits {}; + template struct value_traits : value_traits {}; + + // integer value traits + template + struct integer_value_limits + { + static constexpr auto min = (std::numeric_limits::min)(); + static constexpr auto max = (std::numeric_limits::max)(); + }; + template + struct integer_value_traits_base : integer_value_limits + { + using native_type = int64_t; + static constexpr bool is_native = std::is_same_v; + static constexpr bool is_signed = static_cast(-1) < T{}; // for impls not specializing std::is_signed + static constexpr auto type = node_type::integer; + static constexpr bool can_partially_represent_native = true; + }; + template + struct unsigned_integer_value_traits : integer_value_traits_base + { + static constexpr bool is_losslessly_convertible_to_native + = integer_value_limits::max <= 9223372036854775807ULL; + static constexpr bool can_represent_native = false; + + }; + template + struct signed_integer_value_traits : integer_value_traits_base + { + using native_type = int64_t; + static constexpr bool is_losslessly_convertible_to_native + = integer_value_limits::min >= (-9223372036854775807LL - 1LL) + && integer_value_limits::max <= 9223372036854775807LL; + static constexpr bool can_represent_native + = integer_value_limits::min <= (-9223372036854775807LL - 1LL) + && integer_value_limits::max >= 9223372036854775807LL; + }; + template ::is_signed> + struct integer_value_traits : signed_integer_value_traits {}; + template + struct integer_value_traits : unsigned_integer_value_traits {}; + template <> struct value_traits : integer_value_traits {}; + template <> struct value_traits : integer_value_traits {}; + template <> struct value_traits : integer_value_traits {}; + template <> struct value_traits : integer_value_traits {}; + template <> struct value_traits : integer_value_traits {}; + template <> struct value_traits : integer_value_traits {}; + template <> struct value_traits : integer_value_traits {}; + template <> struct value_traits : integer_value_traits {}; + template <> struct value_traits : integer_value_traits {}; + template <> struct value_traits : integer_value_traits {}; + #ifdef TOML_INT128 + template <> + struct integer_value_limits + { + static constexpr TOML_INT128 max = static_cast( + (TOML_UINT128{ 1u } << ((__SIZEOF_INT128__ * CHAR_BIT) - 1)) - 1 + ); + static constexpr TOML_INT128 min = -max - TOML_INT128{ 1 }; + }; + template <> + struct integer_value_limits + { + static constexpr TOML_UINT128 min = TOML_UINT128{}; + static constexpr TOML_UINT128 max = (2u * static_cast(integer_value_limits::max)) + 1u; + }; + template <> struct value_traits : integer_value_traits {}; + template <> struct value_traits : integer_value_traits {}; + #endif + #ifdef TOML_SMALL_INT_TYPE + template <> struct value_traits : signed_integer_value_traits {}; + #endif + static_assert(value_traits::is_native); + static_assert(value_traits::is_signed); + static_assert(value_traits::is_losslessly_convertible_to_native); + static_assert(value_traits::can_represent_native); + static_assert(value_traits::can_partially_represent_native); + + // float value traits + template + struct float_value_limits + { + static constexpr bool is_iec559 = std::numeric_limits::is_iec559; + static constexpr int digits = std::numeric_limits::digits; + static constexpr int digits10 = std::numeric_limits::digits10; + }; + template + struct float_value_traits : float_value_limits + { + using native_type = double; + static constexpr bool is_native = std::is_same_v; + static constexpr bool is_signed = true; + static constexpr bool is_losslessly_convertible_to_native + = float_value_limits::is_iec559 + && float_value_limits::digits <= 53 + && float_value_limits::digits10 <= 15; + static constexpr bool can_represent_native + = float_value_limits::is_iec559 + && float_value_limits::digits >= 53 // DBL_MANT_DIG + && float_value_limits::digits10 >= 15; // DBL_DIG + static constexpr bool can_partially_represent_native //32-bit float values + = float_value_limits::is_iec559 + && float_value_limits::digits >= 24 + && float_value_limits::digits10 >= 6; + static constexpr auto type = node_type::floating_point; + }; + template <> struct value_traits : float_value_traits {}; + template <> struct value_traits : float_value_traits {}; + template <> struct value_traits : float_value_traits {}; + template + struct extended_float_value_limits + { + static constexpr bool is_iec559 = true; + static constexpr int digits = mant_dig; + static constexpr int digits10 = dig; + }; + #ifdef TOML_FP16 + template <> struct float_value_limits : extended_float_value_limits<__FLT16_MANT_DIG__, __FLT16_DIG__> {}; + template <> struct value_traits : float_value_traits {}; + #endif + #ifdef TOML_FLOAT16 + template <> struct float_value_limits : extended_float_value_limits<__FLT16_MANT_DIG__, __FLT16_DIG__> {}; + template <> struct value_traits : float_value_traits {}; + #endif + #ifdef TOML_FLOAT128 + template <> struct float_value_limits : extended_float_value_limits<__FLT128_MANT_DIG__, __FLT128_DIG__> {}; + template <> struct value_traits : float_value_traits {}; + #endif + #ifdef TOML_SMALL_FLOAT_TYPE + template <> struct value_traits : float_value_traits {}; + #endif + static_assert(value_traits::is_native); + static_assert(value_traits::is_losslessly_convertible_to_native); + static_assert(value_traits::can_represent_native); + static_assert(value_traits::can_partially_represent_native); + + // string value traits + template + struct string_value_traits + { + using native_type = std::string; + static constexpr bool is_native = std::is_same_v; + static constexpr bool is_losslessly_convertible_to_native = true; + static constexpr bool can_represent_native + = !std::is_array_v + && (!std::is_pointer_v || std::is_const_v>); + static constexpr bool can_partially_represent_native = can_represent_native; + static constexpr auto type = node_type::string; + }; + template <> struct value_traits : string_value_traits {}; + template <> struct value_traits : string_value_traits {}; + template <> struct value_traits : string_value_traits {}; + template struct value_traits : string_value_traits {}; + template <> struct value_traits : string_value_traits {}; + template struct value_traits : string_value_traits {}; + #ifdef __cpp_lib_char8_t + template <> struct value_traits : string_value_traits {}; + template <> struct value_traits : string_value_traits {}; + template <> struct value_traits : string_value_traits {}; + template struct value_traits : string_value_traits {}; + template <> struct value_traits : string_value_traits {}; + template struct value_traits : string_value_traits {}; + #endif + #if TOML_WINDOWS_COMPAT + template + struct wstring_value_traits + { + using native_type = std::string; + static constexpr bool is_native = false; + static constexpr bool is_losslessly_convertible_to_native = true; //narrow + static constexpr bool can_represent_native = std::is_same_v; //widen + static constexpr bool can_partially_represent_native = can_represent_native; + static constexpr auto type = node_type::string; + }; + template <> struct value_traits : wstring_value_traits {}; + template <> struct value_traits : wstring_value_traits {}; + template <> struct value_traits : wstring_value_traits {}; + template struct value_traits : wstring_value_traits {}; + template <> struct value_traits : wstring_value_traits {}; + template struct value_traits : wstring_value_traits {}; + #endif + + // other native value traits + template + struct native_value_traits + { + using native_type = T; + static constexpr bool is_native = true; + static constexpr bool is_losslessly_convertible_to_native = true; + static constexpr bool can_represent_native = true; + static constexpr bool can_partially_represent_native = true; + static constexpr auto type = NodeType; + }; + template <> struct value_traits : native_value_traits {}; + template <> struct value_traits : native_value_traits {}; + template <> struct value_traits