From 3d4f2527a758d1ac5ffa0f14769ef2e1f477f652 Mon Sep 17 00:00:00 2001 From: Lorenzooone Date: Sun, 14 Apr 2024 19:31:08 +0200 Subject: [PATCH] Initial Commit --- .gitignore | 10 + CMakeLists.txt | 169 ++++++++ README.md | 102 +++++ data/font.ttf | Bin 0 -> 173520 bytes include/3dscapture.hpp | 58 +++ include/audio.hpp | 47 ++ include/conversions.hpp | 10 + include/frontend.hpp | 206 +++++++++ include/hw_defs.hpp | 45 ++ include/utils.hpp | 34 ++ macos_setup_ftd3xx.sh | 9 + source/3dscapture.cpp | 291 +++++++++++++ source/TextRectangle.cpp | 162 +++++++ source/WindowScreen.cpp | 905 +++++++++++++++++++++++++++++++++++++++ source/audio.cpp | 115 +++++ source/cc3dsfs.cpp | 334 +++++++++++++++ source/conversions.cpp | 32 ++ source/frontend.cpp | 151 +++++++ source/utils.cpp | 73 ++++ tools/bin2c.cpp | 73 ++++ 20 files changed, 2826 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 data/font.ttf create mode 100644 include/3dscapture.hpp create mode 100644 include/audio.hpp create mode 100644 include/conversions.hpp create mode 100644 include/frontend.hpp create mode 100644 include/hw_defs.hpp create mode 100644 include/utils.hpp create mode 100644 macos_setup_ftd3xx.sh create mode 100755 source/3dscapture.cpp create mode 100755 source/TextRectangle.cpp create mode 100755 source/WindowScreen.cpp create mode 100755 source/audio.cpp create mode 100755 source/cc3dsfs.cpp create mode 100755 source/conversions.cpp create mode 100755 source/frontend.cpp create mode 100755 source/utils.cpp create mode 100644 tools/bin2c.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c99fd6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Byte-compiled / optimized / DLL files +*.o +cc3dsfs +cc3dsfs.exe +*.conf +*.a +ftd3xx/ +presets/ +build/ +.config/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..146d241 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,169 @@ +cmake_minimum_required(VERSION 3.16) +project(CMakeSFMLProject LANGUAGES CXX) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +option(BUILD_SHARED_LIBS "Build shared libraries" OFF) +set(CMAKE_HOST_CXX_COMPILER "g++" CACHE STRING "Host Compiler") +set(WINDOWS_FTD3XX_USE_SHARED_LIB 0) +set(FETCHCONTENT_QUIET FALSE) +set(EXTRA_LINUX_CXX_FLAGS "-DXLIB_BASED 1" CACHE STRING "Extra Linux CXX flags") + +include(FetchContent) +FetchContent_Declare(SFML + GIT_REPOSITORY https://github.com/SFML/SFML.git + GIT_TAG 2.6.1) + +set(FTD3XX_BASE_URL https://ftdichip.com/wp-content/uploads/) +if(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + set(FTD3XX_URL_TIME 2024/01) + set(FTD3XX_VER 1.3.0.9) + set(FTD3XX_VOL FTD3XXLibrary_v${FTD3XX_VER}) + set(FTD3XX_ARCHIVE ${FTD3XX_VOL}.zip) + set(FTD3XX_LIB libftd3xx.lib) + set(FTD3XX_DLL FTD3XX.dll) + set(FTD3XX_SUBFOLDER win) + if(${WINDOWS_FTD3XX_USE_SHARED_LIB}) + set(WINDOWS_PATH_SPECIFIER DLL) + else() + set(WINDOWS_PATH_SPECIFIER Static_Lib) + endif() +elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + set(FTD3XX_URL_TIME 2023/03) + set(FTD3XX_VER 1.0.5) + set(FTD3XX_VOL d3xx-osx.${FTD3XX_VER}) + set(FTD3XX_ARCHIVE ${FTD3XX_VOL}.dmg) + set(FTD3XX_LIB libftd3xx-static.a) + set(FTD3XX_MOUNTED_FOLDER /Volumes/${FTD3XX_VOL}) + set(FTD3XX_SUBFOLDER macos) +else() + set(FTD3XX_URL_TIME 2023/03) + set(FTD3XX_VER 1.0.5) + if((${CMAKE_SYSTEM_PROCESSOR} MATCHES "aarch") OR (${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm")) + if((${CMAKE_SYSTEM_PROCESSOR} MATCHES "64") OR (${CMAKE_SYSTEM_PROCESSOR} MATCHES "v8")) + set(FTD3XX_VOL libftd3xx-linux-arm-v8-${FTD3XX_VER}) + elseif(${CMAKE_SYSTEM_PROCESSOR} MATCHES "v7") + set(FTD3XX_VOL libftd3xx-linux-arm-v7_32-${FTD3XX_VER}) + endif() + else() + if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "64") + set(FTD3XX_VOL libftd3xx-linux-x86_64-${FTD3XX_VER}) + elseif((${CMAKE_SYSTEM_PROCESSOR} MATCHES "32") OR (${CMAKE_SYSTEM_PROCESSOR} MATCHES "86")) + set(FTD3XX_VOL libftd3xx-linux-x86_32-${FTD3XX_VER}) + endif() + endif() + set(FTD3XX_ARCHIVE ${FTD3XX_VOL}.tgz) + set(FTD3XX_LIB libftd3xx-static.a) + set(FTD3XX_SUBFOLDER linux) +endif() + +if(${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") +FetchContent_Declare(FTD3XX + URL ${FTD3XX_BASE_URL}${FTD3XX_URL_TIME}/${FTD3XX_ARCHIVE} + DOWNLOAD_NO_EXTRACT TRUE +) +else() +FetchContent_Declare(FTD3XX + URL ${FTD3XX_BASE_URL}${FTD3XX_URL_TIME}/${FTD3XX_ARCHIVE} +) +endif() + +FetchContent_MakeAvailable(FTD3XX SFML) +file(MAKE_DIRECTORY ${ftd3xx_BINARY_DIR}/win) +file(MAKE_DIRECTORY ${ftd3xx_BINARY_DIR}/macos) +file(MAKE_DIRECTORY ${ftd3xx_BINARY_DIR}/linux) + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + +# search headers and libraries in the target environment +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(TOOLS_DATA_DIR ${CMAKE_BINARY_DIR}/tools_and_data) +file(MAKE_DIRECTORY ${TOOLS_DATA_DIR}) + +set(OUTPUT_NAME cc3dsfs) + +add_custom_target(CMakeFTD3XX) +add_custom_target(CMakeBin2C) +add_executable(${OUTPUT_NAME} source/cc3dsfs.cpp source/utils.cpp source/audio.cpp source/frontend.cpp source/TextRectangle.cpp source/WindowScreen.cpp source/3dscapture.cpp source/conversions.cpp ${TOOLS_DATA_DIR}/font_ttf.cpp) +add_dependencies(${OUTPUT_NAME} CMakeFTD3XX) +if(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") +target_link_libraries(${OUTPUT_NAME} PRIVATE sfml-graphics sfml-audio sfml-window sfml-system FTD3XX) +else() +target_link_libraries(${OUTPUT_NAME} PRIVATE sfml-graphics sfml-audio sfml-window sfml-system ftd3xx-static) +endif() +target_link_directories(${OUTPUT_NAME} PRIVATE ${ftd3xx_BINARY_DIR}/${FTD3XX_SUBFOLDER}) +target_include_directories(${OUTPUT_NAME} PRIVATE ${ftd3xx_BINARY_DIR}/${FTD3XX_SUBFOLDER} ${TOOLS_DATA_DIR} ${CMAKE_SOURCE_DIR}/include) +target_compile_features(${OUTPUT_NAME} PRIVATE cxx_std_17) +target_compile_options(${OUTPUT_NAME} PRIVATE "-std=c++17" ${EXTRA_LINUX_CXX_FLAGS}) + +add_custom_command( + TARGET CMakeBin2C + COMMENT "Create Bin2C" + PRE_BUILD COMMAND ${CMAKE_HOST_CXX_COMPILER} ${CMAKE_SOURCE_DIR}/tools/bin2c.cpp -o ${TOOLS_DATA_DIR}/bin2c +) + +add_custom_command( + OUTPUT ${TOOLS_DATA_DIR}/font_ttf.cpp + COMMENT "Convert font to binary" + COMMAND ${TOOLS_DATA_DIR}/bin2c ${CMAKE_SOURCE_DIR}/data/font.ttf ${TOOLS_DATA_DIR} font_ttf font_ttf + DEPENDS ${CMAKE_SOURCE_DIR}/data/font.ttf CMakeBin2C +) + +if(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + add_custom_command( + TARGET CMakeFTD3XX + COMMENT "Copy FTD3XX header" + PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${ftd3xx_SOURCE_DIR}/FTD3XX.h ${ftd3xx_BINARY_DIR}/${FTD3XX_SUBFOLDER} + ) + if(WIN32) + set(PATH_WINDOWS_ARCH Win32) + else () + set(PATH_WINDOWS_ARCH x64) + endif() + add_custom_command( + TARGET CMakeFTD3XX + COMMENT "Copy FTD3XX lib file" + PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${ftd3xx_SOURCE_DIR}/${PATH_WINDOWS_ARCH}/${WINDOWS_PATH_SPECIFIER}/FTD3XX.lib ${ftd3xx_BINARY_DIR}/${FTD3XX_SUBFOLDER} + ) + if(${WINDOWS_FTD3XX_USE_SHARED_LIB}) + add_custom_command( + TARGET CMakeFTD3XX + COMMENT "Copy FTD3XX lib file" + PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${ftd3xx_SOURCE_DIR}/${PATH_WINDOWS_ARCH}/${WINDOWS_PATH_SPECIFIER}/FTD3XX.dll ${ftd3xx_BINARY_DIR}/${FTD3XX_SUBFOLDER} + ) + endif() +elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + add_custom_command( + TARGET CMakeFTD3XX + COMMENT "Copy FTD3XX files" + PRE_BUILD COMMAND macos_setup_ftd3xx.sh ${ftd3xx_SOURCE_DIR}/${FTD3XX_ARCHIVE} ${FTD3XX_MOUNTED_FOLDER} ${ftd3xx_BINARY_DIR}/${FTD3XX_SUBFOLDER} ${FTD3XX_LIB} + ) +else() + add_custom_command( + TARGET CMakeFTD3XX + COMMENT "Copy FTD3XX headers" + PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${ftd3xx_SOURCE_DIR}/*.h ${ftd3xx_BINARY_DIR}/${FTD3XX_SUBFOLDER} + ) + add_custom_command( + TARGET CMakeFTD3XX + COMMENT "Copy FTD3XX lib files" + PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${ftd3xx_SOURCE_DIR}/${FTD3XX_LIB} ${ftd3xx_BINARY_DIR}/${FTD3XX_SUBFOLDER} + ) +endif() + +if(WIN32) + add_custom_command( + TARGET ${OUTPUT_NAME} + COMMENT "Copy OpenAL DLL" + PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${sfml_SOURCE_DIR}/extlibs/bin/$,x64,x86>/openal32.dll ${CMAKE_SOURCE_DIR} + VERBATIM + ) +endif() +add_custom_command( + TARGET ${OUTPUT_NAME} + COMMENT "Copy OpenAL DLL" + POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_SOURCE_DIR} + VERBATIM +) + +install(TARGETS ${OUTPUT_NAME}) diff --git a/README.md b/README.md new file mode 100644 index 0000000..7164681 --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# cc3dsfs + +cc3dsfs is a multi-platform capture and display program for [3dscapture's](https://3dscapture.com/) N3DSXL capture board written in C++. +The main goal is to offer the ability to use the Capture Card with a TV, via fullscreen mode. + +#### Features + +- Performance-focused design, with low latency for both audio and video (measured to oscillate between 1 and 2 frames). +- Option to split the screens in separate windows, to address them separately. +- Make your game run in fullscreen mode. If you own multiple displays, you can even use one per-window. +- Many builtin crop options for the screens. +- Many other settings, explained in the [####Controls] section. + +_Note: On 3DS, DS, GBA, GBC and GB games boot in scaled resolution mode by default. Holding START or SELECT while launching these games will boot in native resolution mode._ +_Note: Make sure the 3DS audio is not set to Surround._ + +#### Dependencies + +cc3dsfs has three build dependencies: CMake, g++ and git. +Make sure all are installed. +On MacOS, [Homebrew](https://brew.sh/) can be used to install both CMake and git. An automatic popup should appear to install g++ at Compile time. + +cc3dsfs has two library dependencies: [FTDI's D3XX driver](https://ftdichip.com/drivers/d3xx-drivers/) and [SFML](https://www.sfml-dev.org/). +Both should get downloaded automatically via CMake during the building process. + +Linux users will also need to install the SFML dependencies. Different distributions will require slightly different processes. +Below, the command for Debian-based distributions, which also lists the required libraries. + +``` +sudo apt update +sudo apt install \ + libxrandr-dev \ + libxcursor-dev \ + libudev-dev \ + libopenal-dev \ + libflac-dev \ + libvorbis-dev \ + libgl1-mesa-dev \ + libegl1-mesa-dev \ + libdrm-dev \ + libgbm-dev \ + libfreetype-dev +``` + +#### Compile and Install + +To compile the program, assuming CMake, git and g++ are installed on the system, this is the command which should be launched: + +``` +cmake -B build ; cmake --build build --config Release +``` + +This will download both FTD3XX and SFML, which may take a while during the first execution of the command. Later runs should be much faster. +On MacOS, you may also be prompted to install the Apple Command Line Developer Tools first. + +#### Controls + +- __S key__: Swaps between Split mode and Joint mode which splits the screens into separate windows or joins them into a single window respectively. +- __A key__: Ensures all the available windows show the same frame. If you're having framerate issues, turning this setting on/off could help. +- __C key__: Cycles to the next cropping mode for the focused window. The currently supported cropping modes are for 3DS, 16:10 DS, scaled DS, native DS, scaled GBA, native GBA, scaled GB, native GB, native SNES and native NES respectively. +- __B key__: Toggles blurring on/off for the focused window. This is only noticeable at scales of 1.5x or greater. +- __V key__: Toggles VSync on/off for the focused window. VSync prevents screen tearing. However, at low refresh rates (60 hz), it may significantly increase image delay. +- __T key__: Moves the relative position of the bottom screen inside of the joint mode window clockwise. +- __F key__: Toggles Fullscreen mode on/off. Only guaranteed to work on the primary monitor. When using certain setups, it also works for multiple monitors. +- __8 key__: Rotates the screen(s) of the focused window 90 degrees counterclockwise. +- __9 key__: Rotates the screen(s) of the focused window 90 degrees clockwise. +- __H key__: Rotates the top screen of the focused window 90 degrees counterclockwise. +- __J key__: Rotates the top screen of the focused window 90 degrees clockwise. +- __K key__: Rotates the bottom screen of the focused window 90 degrees counterclockwise. +- __L key__: Rotates the bottom screen of the focused window 90 degrees clockwise. +- __- key__: Decrements the scaling by 0.5x for the non-Fullscreen focused window. 1.0x is the minimum. +- __0 key__: Increments the scaling by 0.5x for the non-Fullscreen focused window. 45.0x is the maximum. +- __Y key__: In Joint Fullscreen mode, increases the size of the top screen. +- __U key__: In Joint Fullscreen mode, increases the size of the bottom screen. +- __4 key__: In Fullscreen mode, changes the X distance from the border between 0, 1/2 Max and Max. +- __5 key__: In Fullscreen mode, changes the Y distance from the border between 0, 1/2 Max and Max. +- __6 key__: In Joint mode, changes the position of the smaller screen relative to the bigger one between 0, 1/2 Max and Max. +- __7 key__: In Joint Fullscreen mode, changes the distance from the upper screen between 0, 1/2 Max and Max. +- __I key__: Toggles BFI (Motion Blur Reduction) on/off. Best used on 120+hz monitors. DO NOT USE THIS IF YOU'RE AT RISK OF SEIZURES!!! +- __M key__: Toggles mute on/off. +- __, key__: Decrements the volume by 5 units. 0 is the minimum. +- __. key__: Increments the volume by 5 units. 100 is the maximum. +- __O key__: Open/Close connection to the 3DS. +- __F1 - F4 keys__: Loads from layouts 1 through 4 respectively. +- __F5 - F8 keys__: Saves to layouts 1 through 4 respectively. +- __Esc key__: Properly quits the program. + +_Note: The volume is independent of the actual volume level set with the physical slider on the console._ + +#### Settings + +When starting the program for the first time, a message indicating a load failure for the cc3dsfs.cfg file will be displayed, and the same will occur when attempting to load from any given layout file if it hasn't been saved to before. These files must be created by the program first before they can be loaded from. The program saves its current configuration to the cc3dsfs.cfg file when the program is successfully closed, creating the file if it doesn't already exist, and loads from it every time at startup. + +Just as well, the current configuration can be saved to any of the four layout files at any time using keys F5 through F8, creating the given file if it doesn't already exist, which can then be loaded from at any time using keys F1 through F4 respectively. Changing the configuration after a layout is loaded will not overwrite it unless the respective save key is pressed after the changes are made. + +#### Notes + +- At startup, the audio may be unstable. It should fix itself, if you give it enough time. +- If, at first, the connection to the 3DS fails, reconnect the 3DS and then try again. If that also doesn't work, try restarting the program. If that also doesn't work, try restarting the computer. +- USB Hubs can be the cause of connection issues. If you're having problems, try checking whether the 3DS connects fine or not without any other devices connected. +- Current font in use: OFL Sorts Mill Goudy TT + diff --git a/data/font.ttf b/data/font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..dc900c2967ce26ddbd76b6ddacc429cb047baf1d GIT binary patch literal 173520 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 bO4E=Yr}F-JcDpzw`mq%k7~;L*s%!oa6IoLC literal 0 HcmV?d00001 diff --git a/include/3dscapture.hpp b/include/3dscapture.hpp new file mode 100644 index 0000000..c64eb45 --- /dev/null +++ b/include/3dscapture.hpp @@ -0,0 +1,58 @@ +#ifndef __3DSCAPTURE_HPP +#define __3DSCAPTURE_HPP + +#include "utils.hpp" +#include "hw_defs.hpp" + +#if defined(_WIN32) || defined(_WIN64) +#define FTD3XX_STATIC +#include +#define FT_ASYNC_CALL FT_ReadPipeEx +#else +#include +#define FT_ASYNC_CALL FT_ReadPipeAsync +#endif + +#define MAX_SAMPLES_IN 1096 + +// Max value supported by old FTD3XX versions... +#define NUM_CONCURRENT_DATA_BUFFERS 8 + +// When is_bad_ftd3xx is true, it may happen that a frame is lost. +// This value prevents showing a black frame for that. +#define MAX_ALLOWED_BLANKS 1 + +#define FIX_PARTIAL_FIRST_FRAME_NUM 3 + +struct __attribute__ ((packed)) VideoInputData { + uint8_t screen_data[IN_VIDEO_SIZE][3]; +}; + +struct __attribute__ ((packed)) CaptureReceived { + VideoInputData video_data; + uint16_t audio_data[MAX_SAMPLES_IN]; +}; + +struct CaptureData { + FT_HANDLE handle; + CaptureReceived capture_buf[NUM_CONCURRENT_DATA_BUFFERS]; + uint32_t read[NUM_CONCURRENT_DATA_BUFFERS]; + double time_in_buf[NUM_CONCURRENT_DATA_BUFFERS]; + volatile int curr_in = 0; + volatile int cooldown_curr_in = FIX_PARTIAL_FIRST_FRAME_NUM; + volatile bool connected = false; + volatile bool running = true; + volatile bool close_success = true; + ConsumerMutex video_wait; + ConsumerMutex audio_wait; +}; + +struct DevicesList { + int numAllocedDevices; + int numValidDevices; + char *serialNumbers; +}; + +bool connect(bool print_failed, CaptureData* capture_data); +void captureCall(CaptureData* capture_data); +#endif diff --git a/include/audio.hpp b/include/audio.hpp new file mode 100644 index 0000000..7467cc0 --- /dev/null +++ b/include/audio.hpp @@ -0,0 +1,47 @@ +#ifndef __AUDIO_HPP +#define __AUDIO_HPP + +#include +#include +#include "utils.hpp" +#include "audio.hpp" +#include "3dscapture.hpp" +#include "hw_defs.hpp" + +struct AudioData { + int volume; + bool mute; +}; + +struct Sample { + Sample(sf::Int16 *bytes, std::size_t size, float time); + + sf::Int16 *bytes; + std::size_t size; + float time; +}; + +class Audio : public sf::SoundStream { +public: + bool restart = false; + std::queue samples; + ConsumerMutex samples_wait; + + Audio(); + void update_volume(int volume, bool mute); + void start_audio(); + void stop_audio(); + +private: + int loaded_volume = -1; + bool loaded_mute = false; + bool terminate; + int num_consecutive_fast_seek; + sf::Int16 buffer[MAX_SAMPLES_IN]; + std::chrono::time_point clock_time_start; + + bool onGetData(sf::SoundStream::Chunk &data) override; + void onSeek(sf::Time timeOffset) override; +}; + +#endif diff --git a/include/conversions.hpp b/include/conversions.hpp new file mode 100644 index 0000000..4285a58 --- /dev/null +++ b/include/conversions.hpp @@ -0,0 +1,10 @@ +#ifndef __CONVERSIONS_HPP +#define __CONVERSIONS_HPP + +#include +#include "3dscapture.hpp" +#include "frontend.hpp" + +void convertVideoToOutput(VideoInputData *p_in, VideoOutputData *p_out); +void convertAudioToOutput(CaptureReceived *p_in, sf::Int16 *p_out, const int n_samples, const bool is_big_endian); +#endif diff --git a/include/frontend.hpp b/include/frontend.hpp new file mode 100644 index 0000000..5fa7712 --- /dev/null +++ b/include/frontend.hpp @@ -0,0 +1,206 @@ +#ifndef __FRONTEND_HPP +#define __FRONTEND_HPP + +#include + +#include +#include "utils.hpp" +#include "audio.hpp" + +enum Crop { DEFAULT_3DS, SPECIAL_DS, SCALED_DS, NATIVE_DS, SCALED_GBA, NATIVE_GBA, SCALED_GB, NATIVE_GB, NATIVE_SNES, NATIVE_NES, CROP_END }; +enum BottomRelativePosition { UNDER_TOP, LEFT_TOP, ABOVE_TOP, RIGHT_TOP, BOT_REL_POS_END }; +enum OffsetAlgorithm { NO_DISTANCE, HALF_DISTANCE, MAX_DISTANCE, OFF_ALGO_END }; + +struct ScreenInfo { + bool is_blurred; + Crop crop_kind; + double scaling; + bool is_fullscreen; + BottomRelativePosition bottom_pos; + OffsetAlgorithm subscreen_offset_algorithm, subscreen_attached_offset_algorithm, total_offset_algorithm_x, total_offset_algorithm_y; + int top_rotation, bot_rotation; + bool show_mouse; + bool v_sync_enabled; + bool async; + int top_scaling, bot_scaling; + bool bfi; + double bfi_divider; +}; + +struct DisplayData { + bool split; +}; + +struct __attribute__ ((packed)) VideoOutputData { + uint8_t screen_data[IN_VIDEO_SIZE][4]; +}; + +struct SFEvent { + SFEvent(sf::Event::EventType type, sf::Keyboard::Key code, uint32_t unicode) : type(type), code(code), unicode(unicode) {} + + sf::Event::EventType type; + sf::Keyboard::Key code; + uint32_t unicode; +}; + +struct out_rect_data { + sf::RectangleShape out_rect; + sf::RenderTexture out_tex; +}; + +void reset_screen_info(ScreenInfo &info); +void load_screen_info(std::string key, std::string value, std::string base, ScreenInfo &info); +std::string save_screen_info(std::string base, const ScreenInfo &info); + +class TextRectangle { +public: + TextRectangle(bool font_load_success, sf::Font &text_font); + void setSize(int width, int height); + void setSelected(bool selected); + void setDuration(float on_seconds); + void startTimer(bool do_start); + void setProportionalBox(bool proportional_box); + void prepareRenderText(); + void setText(std::string text); + void setShowText(bool show_text); + void draw(sf::RenderTarget &window); + +private: + out_rect_data text_rect; + sf::Text actual_text; + int width, height; + bool font_load_success; + bool is_done_showing_text; + std::chrono::time_point clock_time_start; + int time_phase; + sf::Color *base_bg_color; + sf::Color *selected_bg_color; + const float base_time_slide_factor = 0.5; + const float base_pixel_slide_factor = 48.0; + + struct TextData { + bool is_timed; + bool start_timer; + bool selected; + bool show_text; + bool render_text; + bool proportional_box; + std::string printed_text; + float duration; + float font_pixel_height; + }; + + TextData future_data; + TextData loaded_data; + + void reset_data(TextData &data); + void updateText(); + void updateSlides(float* time_seconds); +}; + +class WindowScreen { +public: + enum ScreenType { TOP, BOTTOM, JOINT }; + ScreenInfo m_info; + + WindowScreen(WindowScreen::ScreenType stype, DisplayData* display_data, AudioData* audio_data, std::mutex* events_access); + ~WindowScreen(); + + void build(); + void reload(); + void poll(); + void close(); + void display_call(bool is_main_thread); + void display_thread(CaptureData* capture_data); + void end(); + void draw(double frame_time, VideoOutputData* out_buf); + + int load_data(); + int save_data(); + bool open_capture(); + bool close_capture(); + +private: + struct ScreenOperations { + bool call_create; + bool call_close; + bool call_crop; + bool call_rotate; + bool call_blur; + bool call_screen_settings_update; + }; + std::string win_title; + sf::RenderWindow m_win; + int m_width, m_height; + int m_window_width, m_window_height; + int m_prepare_load; + int m_prepare_save; + bool m_prepare_open; + bool m_prepare_quit; + double frame_time; + DisplayData* display_data; + AudioData* audio_data; + std::mutex* events_access; + + sf::Texture in_tex; + + sf::Font text_font; + bool font_load_success; + + volatile bool main_thread_owns_window; + volatile bool is_window_factory_done; + volatile bool scheduled_work_on_window; + volatile bool is_thread_done; + + sf::RectangleShape m_in_rect_top, m_in_rect_bot; + out_rect_data m_out_rect_top, m_out_rect_bot; + WindowScreen::ScreenType m_stype; + + std::queue events_queue; + + sf::View m_view; + sf::VideoMode curr_desk_mode; + + TextRectangle* notification; + + ConsumerMutex display_lock; + bool done_display; + VideoOutputData *saved_buf; + ScreenInfo loaded_info; + ScreenOperations future_operations; + ScreenOperations loaded_operations; + + static void reset_operations(ScreenOperations &operations); + void free_ownership_of_window(bool is_main_thread); + + void resize_in_rect(sf::RectangleShape &in_rect, int start_x, int start_y, int width, int height); + int get_screen_corner_modifier_x(int rotation, int width); + int get_screen_corner_modifier_y(int rotation, int height); + void print_notification(std::string text); + void print_notification_on_off(std::string base_text, bool value); + void poll_window(); + void prepare_screen_rendering(); + bool window_needs_work(); + void window_factory(bool is_main_thread); + void window_render_call(); + int apply_offset_algo(int offset_contribute, OffsetAlgorithm chosen_algo); + void set_position_screens(sf::Vector2f &curr_top_screen_size, sf::Vector2f &curr_bot_screen_size, int offset_x, int offset_y, int max_x, int max_y); + int prepare_screen_ratio(sf::Vector2f &screen_size, int own_rotation, int width_limit, int height_limit, int other_rotation); + void calc_scaling_resize_screens(sf::Vector2f &own_screen_size, sf::Vector2f &other_screen_size, int &own_scaling, int &other_scaling, int own_rotation, int other_rotation, bool increase, bool mantain, bool set_to_zero); + void prepare_size_ratios(bool top_increase, bool bot_increase); + int get_fullscreen_offset_x(int top_width, int top_height, int bot_width, int bot_height); + int get_fullscreen_offset_y(int top_width, int top_height, int bot_width, int bot_height); + void resize_window_and_out_rects(); + void create_window(bool re_prepare_size); + void update_view_size(); + void open(); + void update_screen_settings(); + void rotate(); + sf::Vector2f getShownScreenSize(bool is_top, Crop &crop_kind); + void crop(); + void setWinSize(bool is_main_thread); +}; + +void update_output(WindowScreen &top_screen, WindowScreen &bot_screen, WindowScreen &joint_screen, bool &reload, bool split, double frame_time, VideoOutputData *out_buf); +void screen_display_thread(WindowScreen *screen, CaptureData* capture_data); +#endif diff --git a/include/hw_defs.hpp b/include/hw_defs.hpp new file mode 100644 index 0000000..40980c0 --- /dev/null +++ b/include/hw_defs.hpp @@ -0,0 +1,45 @@ +#ifndef __HW_DEFS_HPP +#define __HW_DEFS_HPP + +#define AUDIO_CHANNELS 2 +#define SAMPLE_RATE 32734 + +#define TOP_WIDTH_3DS 400 +#define BOT_WIDTH_3DS 320 +#define HEIGHT_3DS 240 + +#define TOP_SPECIAL_DS_WIDTH_3DS 384 +#define BOT_SPECIAL_DS_WIDTH_3DS 320 + +#define TOP_SCALED_DS_WIDTH_3DS 320 +#define BOT_SCALED_DS_WIDTH_3DS 320 + +#define WIDTH_DS 256 +#define HEIGHT_DS 192 + +#define WIDTH_GBA 240 +#define HEIGHT_GBA 160 + +#define WIDTH_SCALED_GBA 360 +#define HEIGHT_SCALED_GBA 240 + +#define WIDTH_GB 160 +#define HEIGHT_GB 144 + +#define WIDTH_SCALED_GB 266 +#define HEIGHT_SCALED_GB 240 + +#define WIDTH_SNES 256 +#define HEIGHT_SNES 224 + +#define WIDTH_NES 256 +#define HEIGHT_NES 240 + +#define IN_VIDEO_WIDTH HEIGHT_3DS +#define IN_VIDEO_HEIGHT (TOP_WIDTH_3DS + BOT_WIDTH_3DS) +#define IN_VIDEO_SIZE (IN_VIDEO_WIDTH * IN_VIDEO_HEIGHT) + +#define TOP_SIZE_3DS (TOP_WIDTH_3DS * HEIGHT_3DS) +#define IN_VIDEO_NO_BOTTOM_SIZE ((TOP_WIDTH_3DS - BOT_WIDTH_3DS) * HEIGHT_3DS) + +#endif diff --git a/include/utils.hpp b/include/utils.hpp new file mode 100644 index 0000000..6fe7ede --- /dev/null +++ b/include/utils.hpp @@ -0,0 +1,34 @@ +#ifndef __UTILS_HPP +#define __UTILS_HPP + +#include +#include +#include + +#define NAME "cc3dsfs" + +// This isn't precise, however we can use it... +// Use these to properly sleep, with small wakes to check if data is ready +#define USB_FPS 60 +#define BAD_USB_FPS 25 +#define USB_NUM_CHECKS 3 +#define USB_CHECKS_PER_SECOND ((USB_FPS + (USB_FPS / 12)) * USB_NUM_CHECKS) + +bool is_big_endian(void); + +class ConsumerMutex { +public: + ConsumerMutex(); + void lock(); + bool timed_lock(); + bool try_lock(); + void unlock(); + +private: + std::mutex access_mutex; + std::condition_variable_any condition; + const std::chrono::durationmax_timed_wait = std::chrono::duration(1.0 / 45); + int count = 0; +}; + +#endif diff --git a/macos_setup_ftd3xx.sh b/macos_setup_ftd3xx.sh new file mode 100644 index 0000000..377cd51 --- /dev/null +++ b/macos_setup_ftd3xx.sh @@ -0,0 +1,9 @@ + +VOL := d3xx-osx.${VER} +SRC_FOLDER := /Volumes/${VOL} +TAR := ${VOL}.dmg + +hdiutil attach ${1} +cp ${2}/${4} ${3} +cp ${2}/*.h ${3} +hdiutil detach ${3} diff --git a/source/3dscapture.cpp b/source/3dscapture.cpp new file mode 100755 index 0000000..15bace7 --- /dev/null +++ b/source/3dscapture.cpp @@ -0,0 +1,291 @@ +#include "utils.hpp" +#include "3dscapture.hpp" + +#if defined(_WIN32) || defined(_WIN64) +#define FTD3XX_STATIC +#include +#define FT_ASYNC_CALL FT_ReadPipeEx +#else +#include +#define FT_ASYNC_CALL FT_ReadPipeAsync +#endif + +#include +#include +#include + +#define BULK_OUT 0x02 +#define BULK_IN 0x82 + +#if defined(_WIN32) || defined(_WIN64) +#define FIFO_CHANNEL 0x82 +#else +#define FIFO_CHANNEL 0 +#endif + +static void list_devices(DevicesList &devices_list) { + FT_STATUS ftStatus; + DWORD numDevs = 0; + std::string valid_descriptions[] = {"N3DSXL", "N3DSXL.2"}; + ftStatus = FT_CreateDeviceInfoList(&numDevs); + devices_list.numAllocedDevices = 0; + devices_list.numValidDevices = 0; + if (!FT_FAILED(ftStatus) && numDevs > 0) + { + devices_list.numAllocedDevices = numDevs; + devices_list.serialNumbers = new char[devices_list.numAllocedDevices * 17]; + FT_HANDLE ftHandle = NULL; + DWORD Flags = 0; + DWORD Type = 0; + DWORD ID = 0; + char SerialNumber[16] = { 0 }; + char Description[32] = { 0 }; + for (DWORD i = 0; i < numDevs; i++) + { + ftStatus = FT_GetDeviceInfoDetail(i, &Flags, &Type, &ID, NULL, + SerialNumber, Description, &ftHandle); + if (!FT_FAILED(ftStatus)) + { + for(int i = 0; i < sizeof(valid_descriptions) / sizeof(*valid_descriptions); i++) { + if(Description == valid_descriptions[i]) { + for(int i = 0; i < 16; i++) + devices_list.serialNumbers[(17 * devices_list.numValidDevices) + i] = SerialNumber[i]; + devices_list.serialNumbers[(17 * devices_list.numValidDevices) + 16] = 0; + ++devices_list.numValidDevices; + break; + } + } + } + } + } +} + +static int choose_device(DevicesList &devices_list) { + return 0; +} + +bool connect(bool print_failed, CaptureData* capture_data) { + if (capture_data->connected) { + capture_data->close_success = false; + return false; + } + + if(!capture_data->close_success) { + return false; + } + + DevicesList devices_list; + list_devices(devices_list); + + if(devices_list.numValidDevices <= 0) { + if(print_failed) + printf("[%s] No device was found.\n", NAME); + if(devices_list.numAllocedDevices > 0) + delete []devices_list.serialNumbers; + return false; + } + + int chosen_device = choose_device(devices_list); + if(chosen_device == -1) { + if(print_failed) + printf("[%s] No device was selected.\n", NAME); + delete []devices_list.serialNumbers; + return false; + } + + if (FT_Create(&devices_list.serialNumbers[17 * chosen_device], FT_OPEN_BY_SERIAL_NUMBER, &capture_data->handle)) { + if(print_failed) + printf("[%s] Create failed.\n", NAME); + delete []devices_list.serialNumbers; + return false; + } + + delete []devices_list.serialNumbers; + + UCHAR buf[4] = {0x40, 0x80, 0x00, 0x00}; + ULONG written = 0; + + if (FT_WritePipe(capture_data->handle, BULK_OUT, buf, 4, &written, 0)) { + if(print_failed) + printf("[%s] Write failed.\n", NAME); + return false; + } + + buf[1] = 0x00; + + if (FT_WritePipe(capture_data->handle, BULK_OUT, buf, 4, &written, 0)) { + if(print_failed) + printf("[%s] Write failed.\n", NAME); + return false; + } + + if (FT_SetStreamPipe(capture_data->handle, false, false, BULK_IN, sizeof(CaptureReceived))) { + if(print_failed) + printf("[%s] Stream failed.\n", NAME); + return false; + } + + if(FT_AbortPipe(capture_data->handle, BULK_IN)) { + if(print_failed) + printf("[%s] Abort failed.\n", NAME); + } + + if (FT_SetStreamPipe(capture_data->handle, false, false, BULK_IN, sizeof(CaptureReceived))) { + if(print_failed) + printf("[%s] Stream failed.\n", NAME); + return false; + } + + // Avoid having old open locks + capture_data->video_wait.try_lock(); + capture_data->audio_wait.try_lock(); + + return true; +} + +static void fast_capture_call(CaptureData* capture_data, OVERLAPPED overlap[NUM_CONCURRENT_DATA_BUFFERS]) { + int inner_curr_in = 0; + FT_STATUS ftStatus; + for (inner_curr_in = 0; inner_curr_in < NUM_CONCURRENT_DATA_BUFFERS; ++inner_curr_in) { + ftStatus = FT_InitializeOverlapped(capture_data->handle, &overlap[inner_curr_in]); + if (ftStatus) { + printf("[%s] Initialize failed.\n", NAME); + return; + } + } + + for (inner_curr_in = 0; inner_curr_in < NUM_CONCURRENT_DATA_BUFFERS - 1; ++inner_curr_in) { + ftStatus = FT_ASYNC_CALL(capture_data->handle, FIFO_CHANNEL, (UCHAR*)&capture_data->capture_buf[inner_curr_in], sizeof(CaptureReceived), &capture_data->read[inner_curr_in], &overlap[inner_curr_in]); + if (ftStatus != FT_IO_PENDING) { + printf("[%s] Read failed.\n", NAME); + return; + } + } + + inner_curr_in = NUM_CONCURRENT_DATA_BUFFERS - 1; + + auto clock_start = std::chrono::high_resolution_clock::now(); + + while (capture_data->connected && capture_data->running) { + + ftStatus = FT_ASYNC_CALL(capture_data->handle, FIFO_CHANNEL, (UCHAR*)&capture_data->capture_buf[inner_curr_in], sizeof(CaptureReceived), &capture_data->read[inner_curr_in], &overlap[inner_curr_in]); + if (ftStatus != FT_IO_PENDING) { + printf("[%s] Read failed.\n", NAME); + return; + } + + inner_curr_in = (inner_curr_in + 1) % NUM_CONCURRENT_DATA_BUFFERS; + + ftStatus = FT_GetOverlappedResult(capture_data->handle, &overlap[inner_curr_in], &capture_data->read[inner_curr_in], true); + if(FT_FAILED(ftStatus)) { + printf("[%s] USB error.\n", NAME); + return; + } + const auto curr_time = std::chrono::high_resolution_clock::now(); + const std::chrono::duration diff = curr_time - clock_start; + capture_data->time_in_buf[inner_curr_in] = diff.count(); + clock_start = curr_time; + + capture_data->curr_in = (inner_curr_in + 1) % NUM_CONCURRENT_DATA_BUFFERS; + if(capture_data->cooldown_curr_in) + capture_data->cooldown_curr_in--; + // Signal that there is data available + capture_data->video_wait.unlock(); + capture_data->audio_wait.unlock(); + } +} + +#if not(defined(_WIN32) || defined(_WIN64)) +static bool safe_capture_call(CaptureData* capture_data) { + int inner_curr_in = 0; + + while (capture_data->connected && capture_data->running) { + + auto clock_start = std::chrono::high_resolution_clock::now(); + + FT_STATUS ftStatus = FT_ReadPipeEx(capture_data->handle, FIFO_CHANNEL, (UCHAR*)&capture_data->capture_buf[inner_curr_in], sizeof(CaptureReceived), &capture_data->read[inner_curr_in], 1000); + if(FT_FAILED(ftStatus)) { + printf("[%s] Read failed.\n", NAME); + return true; + } + + const auto curr_time = std::chrono::high_resolution_clock::now(); + const std::chrono::duration diff = curr_time - clock_start; + capture_data->time_in_buf[inner_curr_in] = diff.count(); + + inner_curr_in = (inner_curr_in + 1) % NUM_CONCURRENT_DATA_BUFFERS; + capture_data->curr_in = inner_curr_in; + if(capture_data->cooldown_curr_in) + capture_data->cooldown_curr_in--; + capture_data->video_wait.unlock(); + capture_data->audio_wait.unlock(); + } + + return false; +} +#endif + +void captureCall(CaptureData* capture_data) { + OVERLAPPED overlap[NUM_CONCURRENT_DATA_BUFFERS]; + FT_STATUS ftStatus; + int inner_curr_in = 0; + capture_data->curr_in = inner_curr_in; + capture_data->cooldown_curr_in = FIX_PARTIAL_FIRST_FRAME_NUM; + bool is_bad_ftd3xx = false; + DWORD ftd3xx_lib_version; + + if(FT_FAILED(FT_GetLibraryVersion(&ftd3xx_lib_version))) { + ftd3xx_lib_version = 0; + } + if(ftd3xx_lib_version == 0x0100001A) { + is_bad_ftd3xx = true; + } + + while(capture_data->running) { + if (!capture_data->connected) { + std::this_thread::sleep_for(std::chrono::milliseconds(1000/USB_CHECKS_PER_SECOND)); + continue; + } + + bool bad_close = false; + #if not(defined(_WIN32) || defined(_WIN64)) + if(!is_bad_ftd3xx) + fast_capture_call(capture_data, overlap); + else + bad_close = safe_capture_call(capture_data); + #else + is_bad_ftd3xx = false; + fast_capture_call(capture_data, overlap); + #endif + + capture_data->close_success = false; + capture_data->connected = false; + capture_data->cooldown_curr_in = FIX_PARTIAL_FIRST_FRAME_NUM; + // Needed in case the threads went in the connected loop + // before it is set right above, and they are waiting on the locks! + capture_data->video_wait.unlock(); + capture_data->audio_wait.unlock(); + + if(!is_bad_ftd3xx) { + for (inner_curr_in = 0; inner_curr_in < NUM_CONCURRENT_DATA_BUFFERS; ++inner_curr_in) { + ftStatus = FT_GetOverlappedResult(capture_data->handle, &overlap[inner_curr_in], &capture_data->read[inner_curr_in], true); + if (FT_ReleaseOverlapped(capture_data->handle, &overlap[inner_curr_in])) { + printf("[%s] Release failed.\n", NAME); + } + } + } + + if(FT_AbortPipe(capture_data->handle, BULK_IN)) { + printf("[%s] Abort failed.\n", NAME); + } + + if (FT_Close(capture_data->handle)) { + printf("[%s] Close failed.\n", NAME); + } + + capture_data->close_success = false; + capture_data->connected = false; + + capture_data->close_success = true; + } +} diff --git a/source/TextRectangle.cpp b/source/TextRectangle.cpp new file mode 100755 index 0000000..d423882 --- /dev/null +++ b/source/TextRectangle.cpp @@ -0,0 +1,162 @@ +#include + +#include "frontend.hpp" + +#include + +TextRectangle::TextRectangle(bool font_load_success, sf::Font &text_font) { + this->font_load_success = font_load_success; + this->setSize(1, 1); + this->text_rect.out_rect.setTexture(&this->text_rect.out_tex.getTexture()); + if(this->font_load_success) + this->actual_text.setFont(text_font); + this->time_phase = 0; + this->base_bg_color = new sf::Color(40, 40, 80, 192); + this->selected_bg_color = new sf::Color(120, 120, 180, 192); + this->reset_data(this->future_data); +} + +void TextRectangle::setSize(int width, int height) { + this->width = width; + this->height = height; + this->text_rect.out_rect.setSize(sf::Vector2f(this->width, this->height)); + this->text_rect.out_tex.create(this->width, this->height); + this->text_rect.out_rect.setTextureRect(sf::IntRect(0, 0, this->width, this->height)); +} + +void TextRectangle::setSelected(bool selected) { + if(this->future_data.selected != selected) { + this->future_data.render_text = true; + } + this->future_data.selected = selected; +} + +void TextRectangle::setDuration(float on_seconds) { + this->future_data.duration = on_seconds; +} + +void TextRectangle::startTimer(bool do_start) { + this->future_data.start_timer = do_start; + this->future_data.is_timed = do_start; +} + +void TextRectangle::setProportionalBox(bool proportional_box) { + if(this->future_data.proportional_box != proportional_box) { + this->future_data.render_text = true; + } + this->future_data.proportional_box = proportional_box; +} + +void TextRectangle::prepareRenderText() { + this->loaded_data = this->future_data; + this->future_data.render_text = false; + this->future_data.start_timer = false; +} + +void TextRectangle::setText(std::string text) { + this->future_data.printed_text = text; + this->future_data.render_text = true; +} + +void TextRectangle::setShowText(bool show_text) { + this->future_data.show_text = show_text; + if(!this->future_data.show_text) + this->startTimer(false); +} + +void TextRectangle::draw(sf::RenderTarget &window) { + if(this->loaded_data.render_text) { + this->updateText(); + } + if(font_load_success && this->loaded_data.show_text && (!this->is_done_showing_text)) { + if(this->loaded_data.is_timed) { + float time_seconds[3]; + this->updateSlides(time_seconds); + if(this->loaded_data.start_timer) { + this->time_phase = 0; + this->clock_time_start = std::chrono::high_resolution_clock::now(); + this->loaded_data.start_timer = false; + } + const auto curr_time = std::chrono::high_resolution_clock::now(); + const std::chrono::duration diff = curr_time - this->clock_time_start; + double factor = diff.count() / time_seconds[this->time_phase]; + if(factor < 0) + factor = 0; + if(factor > 1) + factor = 1; + + int rect_curr_height = 0; + + if(this->time_phase == 0) + rect_curr_height = -this->height + ((int)(factor * this->height)); + else if(this->time_phase == 2) + rect_curr_height = ((int)(factor * this->height)) * -1; + this->text_rect.out_rect.setPosition(0, rect_curr_height); + + if(factor >= 1) { + this->time_phase++; + this->clock_time_start = std::chrono::high_resolution_clock::now(); + } + if(this->time_phase >= 3) + this->is_done_showing_text = true; + } + window.draw(this->text_rect.out_rect); + } +} + +void TextRectangle::reset_data(TextData &data) { + data.is_timed = false; + data.start_timer = false; + data.selected = false; + data.show_text = false; + data.render_text = false; + data.proportional_box = true; + data.printed_text = "Sample Text"; + data.duration = 2.5; + data.font_pixel_height = 24; +} + +void TextRectangle::updateText() { + this->actual_text.setString(this->loaded_data.printed_text); + // set the character size + this->actual_text.setCharacterSize(this->loaded_data.font_pixel_height); // in pixels, not points! + // set the color + this->actual_text.setFillColor(sf::Color::White); + // set the text style + //this->actual_text.setStyle(sf::Text::Bold); + sf::FloatRect globalBounds = this->actual_text.getGlobalBounds(); + int new_width = this->width; + int new_height = this->height; + int bounds_width = globalBounds.width + (globalBounds.left * 2); + int bounds_height = globalBounds.height + (globalBounds.top * 2); + if(new_width < bounds_width) + new_width = bounds_width ; + if(new_height < bounds_height) + new_height = bounds_height; + if((new_width != this->width) || (new_height != this->height)) + this->setSize(new_width, new_height); + if(this->loaded_data.proportional_box) { + this->actual_text.setPosition(5, 0); + globalBounds = this->actual_text.getGlobalBounds(); + this->setSize(globalBounds.width + (globalBounds.left * 2), globalBounds.height + (globalBounds.top * 2)); + } + else { + this->actual_text.setPosition(0, 0); + globalBounds = this->actual_text.getGlobalBounds(); + this->actual_text.setPosition((new_width - (globalBounds.width + (globalBounds.left * 4))) / 2, (new_height - (globalBounds.height + (globalBounds.top * 4))) / 2); + } + if(!this->loaded_data.selected) + this->text_rect.out_tex.clear(*this->base_bg_color); + else + this->text_rect.out_tex.clear(*this->selected_bg_color); + this->text_rect.out_tex.draw(this->actual_text); + this->text_rect.out_tex.display(); + this->is_done_showing_text = false; + this->loaded_data.start_timer = this->loaded_data.is_timed; +} + +void TextRectangle::updateSlides(float* time_seconds) { + time_seconds[0] = base_time_slide_factor * (this->height / base_pixel_slide_factor); + time_seconds[1] = loaded_data.duration; + time_seconds[2] = base_time_slide_factor * (this->height / base_pixel_slide_factor); +} diff --git a/source/WindowScreen.cpp b/source/WindowScreen.cpp new file mode 100755 index 0000000..57eb094 --- /dev/null +++ b/source/WindowScreen.cpp @@ -0,0 +1,905 @@ +#include + +#include "utils.hpp" +#include "hw_defs.hpp" +#include "3dscapture.hpp" +#include "frontend.hpp" + +#include +#include +#include +#include "font_ttf.h" + +static int top_screen_crop_widths[] = {TOP_WIDTH_3DS, TOP_SPECIAL_DS_WIDTH_3DS, TOP_SCALED_DS_WIDTH_3DS, WIDTH_DS, WIDTH_SCALED_GBA, WIDTH_GBA, WIDTH_SCALED_GB, WIDTH_GB, WIDTH_SNES, WIDTH_NES}; +static int top_screen_crop_heights[] = {HEIGHT_3DS, HEIGHT_3DS, HEIGHT_3DS, HEIGHT_DS, HEIGHT_SCALED_GBA, HEIGHT_GBA, HEIGHT_SCALED_GB, HEIGHT_GB, HEIGHT_SNES, HEIGHT_NES}; +static int bot_screen_crop_widths[] = {BOT_WIDTH_3DS, BOT_WIDTH_3DS, BOT_WIDTH_3DS, WIDTH_DS, 0, 0, 0, 0, 0, 0}; +static int bot_screen_crop_heights[] = {HEIGHT_3DS, HEIGHT_3DS, HEIGHT_3DS, HEIGHT_DS, 0, 0, 0, 0, 0, 0}; +static int top_screen_crop_x[] = {0, (TOP_WIDTH_3DS - TOP_SPECIAL_DS_WIDTH_3DS) / 2, (TOP_WIDTH_3DS - TOP_SCALED_DS_WIDTH_3DS) / 2, (TOP_WIDTH_3DS - WIDTH_DS) / 2, + (TOP_WIDTH_3DS - WIDTH_SCALED_GBA) / 2, (TOP_WIDTH_3DS - WIDTH_GBA) / 2, (TOP_WIDTH_3DS - WIDTH_SCALED_GB) / 2, (TOP_WIDTH_3DS - WIDTH_GB) / 2, + (TOP_WIDTH_3DS - WIDTH_SNES) / 2, (TOP_WIDTH_3DS - WIDTH_NES) / 2}; +static int top_screen_crop_y[] = {0, 0, 0, 0, (HEIGHT_3DS - HEIGHT_SCALED_GBA) / 2, (HEIGHT_3DS - HEIGHT_GBA) / 2, (HEIGHT_3DS - HEIGHT_SCALED_GB) / 2, (HEIGHT_3DS - HEIGHT_GB) / 2, (HEIGHT_3DS - HEIGHT_SNES) / 2, (HEIGHT_3DS - HEIGHT_NES) / 2}; +static int bot_screen_crop_x[] = {0, 0, 0, (BOT_WIDTH_3DS - WIDTH_DS) / 2, 0, 0, 0, 0, 0, 0}; +static int bot_screen_crop_y[] = {0, 0, 0, HEIGHT_3DS - HEIGHT_DS, 0, 0, 0, 0, 0, 0}; +static std::string crop_names[] = {"3DS", "16:10", "Scaled DS", "Native DS", "Scaled GBA", "Native GBA", "Scaled VC GB", "VC GB", "VC SNES", "VC NES"}; + +WindowScreen::WindowScreen(WindowScreen::ScreenType stype, DisplayData* display_data, AudioData* audio_data, std::mutex* events_access) { + this->m_stype = stype; + this->events_access = events_access; + this->m_prepare_save = 0; + this->m_prepare_load = 0; + this->m_prepare_open = false; + this->m_prepare_quit = false; + reset_screen_info(this->m_info); + this->font_load_success = this->text_font.loadFromMemory(font_ttf, font_ttf_len); + this->notification = new TextRectangle(this->font_load_success, this->text_font); + this->in_tex.create(IN_VIDEO_WIDTH, IN_VIDEO_HEIGHT); + this->m_in_rect_top.setTexture(&this->in_tex); + this->m_in_rect_bot.setTexture(&this->in_tex); + this->display_data = display_data; + this->audio_data = audio_data; + WindowScreen::reset_operations(future_operations); + this->done_display = true; + this->saved_buf = new VideoOutputData; + this->win_title = NAME; + if(this->m_stype == WindowScreen::ScreenType::TOP) + this->win_title += "_top"; + if(this->m_stype == WindowScreen::ScreenType::BOTTOM) + this->win_title += "_bot"; +} + +WindowScreen::~WindowScreen() { + delete this->saved_buf; + this->saved_buf = NULL; +} + +void WindowScreen::build() { + sf::Vector2f top_screen_size = sf::Vector2f(TOP_WIDTH_3DS, HEIGHT_3DS); + sf::Vector2f bot_screen_size = sf::Vector2f(BOT_WIDTH_3DS, HEIGHT_3DS); + + int width = TOP_WIDTH_3DS; + if(this->m_stype == WindowScreen::ScreenType::BOTTOM) + width = BOT_WIDTH_3DS; + + this->reload(); + + this->m_out_rect_top.out_tex.create(top_screen_size.x, top_screen_size.y); + this->m_out_rect_top.out_rect.setSize(top_screen_size); + this->m_out_rect_top.out_rect.setTexture(&this->m_out_rect_top.out_tex.getTexture()); + + this->m_out_rect_bot.out_tex.create(bot_screen_size.x, bot_screen_size.y); + this->m_out_rect_bot.out_rect.setSize(bot_screen_size); + this->m_out_rect_bot.out_rect.setTexture(&this->m_out_rect_bot.out_tex.getTexture()); + + this->m_view.setRotation(0); +} + +void WindowScreen::reload() { + this->future_operations.call_crop = true; + this->future_operations.call_rotate = true; + this->future_operations.call_blur = true; + this->future_operations.call_screen_settings_update = true; + if(this->m_win.isOpen()) + this->create_window(false); + this->prepare_size_ratios(true, true); +} + +void WindowScreen::poll() { + this->poll_window(); + while(!events_queue.empty()) { + SFEvent event_data = events_queue.front(); + events_queue.pop(); + switch (event_data.type) { + case sf::Event::Closed: + this->m_prepare_quit = true; + break; + + case sf::Event::TextEntered: + switch (event_data.unicode) { + case 'c': + if(this->m_stype == WindowScreen::ScreenType::BOTTOM) { + if(this->m_info.crop_kind == Crop::DEFAULT_3DS) + this->m_info.crop_kind = Crop::NATIVE_DS; + else + this->m_info.crop_kind = Crop::DEFAULT_3DS; + } + else { + this->m_info.crop_kind = static_cast((this->m_info.crop_kind + 1) % Crop::CROP_END); + } + this->print_notification("Crop: " + crop_names[this->m_info.crop_kind]); + this->prepare_size_ratios(false, false); + this->future_operations.call_crop = true; + + break; + + case 's': + this->m_info.is_fullscreen = false; + this->display_data->split = !this->display_data->split; + break; + + case 'a': + this->m_info.async = !this->m_info.async; + this->print_notification_on_off("Async", this->m_info.async); + break; + + case 'b': + this->m_info.is_blurred = !this->m_info.is_blurred; + this->future_operations.call_blur = true; + break; + + case 'v': + this->m_info.v_sync_enabled = !this->m_info.v_sync_enabled; + this->print_notification_on_off("VSync", this->m_info.v_sync_enabled); + break; + + case 'i': + this->m_info.bfi = !this->m_info.bfi; + this->print_notification_on_off("BFI", this->m_info.bfi); + break; + + case '-': + this->m_info.scaling -= 0.5; + if (this->m_info.scaling < 1.25) + this->m_info.scaling = 1.0; + break; + + case '0': + this->m_info.scaling += 0.5; + if (this->m_info.scaling > 44.75) + this->m_info.scaling = 45.0; + break; + + case 'o': + this->m_prepare_open = true; + break; + + case 't': + this->m_info.bottom_pos = static_cast((this->m_info.bottom_pos + 1) % (BottomRelativePosition::BOT_REL_POS_END)); + this->prepare_size_ratios(false, false); + this->future_operations.call_screen_settings_update = true; + break; + + case '6': + this->m_info.subscreen_offset_algorithm = static_cast((this->m_info.subscreen_offset_algorithm + 1) % (OffsetAlgorithm::OFF_ALGO_END)); + this->future_operations.call_screen_settings_update = true; + break; + + case '7': + this->m_info.subscreen_attached_offset_algorithm = static_cast((this->m_info.subscreen_attached_offset_algorithm + 1) % (OffsetAlgorithm::OFF_ALGO_END)); + this->future_operations.call_screen_settings_update = true; + break; + + case '4': + this->m_info.total_offset_algorithm_x = static_cast((this->m_info.total_offset_algorithm_x + 1) % (OffsetAlgorithm::OFF_ALGO_END)); + this->future_operations.call_screen_settings_update = true; + break; + + case '5': + this->m_info.total_offset_algorithm_y = static_cast((this->m_info.total_offset_algorithm_y + 1) % (OffsetAlgorithm::OFF_ALGO_END)); + this->future_operations.call_screen_settings_update = true; + break; + + case 'y': + this->prepare_size_ratios(true, false); + this->future_operations.call_screen_settings_update = true; + break; + + case 'u': + this->prepare_size_ratios(false, true); + this->future_operations.call_screen_settings_update = true; + break; + + case 'f': + this->m_info.is_fullscreen = !this->m_info.is_fullscreen; + this->create_window(true); + break; + + case '8': + this->m_info.top_rotation = (this->m_info.top_rotation + 270) % 360; + this->m_info.bot_rotation = (this->m_info.bot_rotation + 270) % 360; + this->prepare_size_ratios(false, false); + this->future_operations.call_rotate = true; + + break; + + case '9': + this->m_info.top_rotation = (this->m_info.top_rotation + 90) % 360; + this->m_info.bot_rotation = (this->m_info.bot_rotation + 90) % 360; + this->prepare_size_ratios(false, false); + this->future_operations.call_rotate = true; + break; + + case 'h': + this->m_info.top_rotation = (this->m_info.top_rotation + 270) % 360; + this->prepare_size_ratios(false, false); + this->future_operations.call_rotate = true; + + break; + + case 'j': + this->m_info.top_rotation = (this->m_info.top_rotation + 90) % 360; + this->prepare_size_ratios(false, false); + this->future_operations.call_rotate = true; + + break; + + case 'k': + this->m_info.bot_rotation = (this->m_info.bot_rotation + 270) % 360; + this->prepare_size_ratios(false, false); + this->future_operations.call_rotate = true; + + break; + + case 'l': + this->m_info.bot_rotation = (this->m_info.bot_rotation + 90) % 360; + this->prepare_size_ratios(false, false); + this->future_operations.call_rotate = true; + + break; + + case 'm': + audio_data->mute = !audio_data->mute; + break; + + case ',': + audio_data->mute = false; + audio_data->volume -= 5; + if(audio_data->volume < 0) + audio_data->volume = 0; + break; + + case '.': + audio_data->mute = false; + audio_data->volume += 5; + if(audio_data->volume > 100) + audio_data->volume = 100; + break; + + default: + break; + } + + break; + + case sf::Event::KeyPressed: + switch (event_data.code) { + case sf::Keyboard::Escape: + this->m_prepare_quit = true; + break; + case sf::Keyboard::F1: + case sf::Keyboard::F2: + case sf::Keyboard::F3: + case sf::Keyboard::F4: + this->m_prepare_load = event_data.code - sf::Keyboard::F1 + 1; + break; + + case sf::Keyboard::F5: + case sf::Keyboard::F6: + case sf::Keyboard::F7: + case sf::Keyboard::F8: + this->m_prepare_save = event_data.code - sf::Keyboard::F5 + 1; + break; + } + + break; + } + } +} + +void WindowScreen::close() { + this->future_operations.call_close = true; + this->notification->setShowText(false); +} + +void WindowScreen::display_call(bool is_main_thread) { + while(!this->is_window_factory_done); + this->prepare_screen_rendering(); + if(this->m_win.isOpen()) { + if(this->main_thread_owns_window != is_main_thread) { + this->m_win.setActive(true); + this->main_thread_owns_window = is_main_thread; + } + this->window_render_call(); + } + this->done_display = true; +} + +void WindowScreen::display_thread(CaptureData* capture_data) { + while(capture_data->running) { + this->display_lock.lock(); + if(!capture_data->running) + break; + this->free_ownership_of_window(false); + this->is_thread_done = true; + if(this->loaded_info.async) { + this->display_call(false); + } + } + if(this->m_win.isOpen()) + this->m_win.close(); + this->done_display = true; +} + +void WindowScreen::end() { + if(this->main_thread_owns_window) + this->m_win.setActive(false); + this->display_lock.unlock(); +} + +void WindowScreen::draw(double frame_time, VideoOutputData* out_buf) { + if(!this->done_display) + return; + + bool should_be_open = this->display_data->split; + if(this->m_stype == WindowScreen::ScreenType::JOINT) + should_be_open = !should_be_open; + if(this->m_win.isOpen() ^ should_be_open) { + if(this->m_win.isOpen()) + this->close(); + else + this->open(); + } + loaded_operations = future_operations; + if(this->m_win.isOpen() || this->loaded_operations.call_create) { + WindowScreen::reset_operations(future_operations); + memcpy(this->saved_buf, out_buf, sizeof(VideoOutputData)); + loaded_info = m_info; + this->notification->prepareRenderText(); + this->frame_time = frame_time; + this->scheduled_work_on_window = this->window_needs_work(); + this->is_window_factory_done = false; + this->is_thread_done = false; + this->done_display = false; + if((!this->main_thread_owns_window) || (this->loaded_info.async)) + this->display_lock.unlock(); + if((!this->main_thread_owns_window) && ((this->scheduled_work_on_window) || (!this->loaded_info.async))) + while(!this->is_thread_done); + this->window_factory(true); + if(!this->loaded_info.async) + this->display_call(true); + } +} + +int WindowScreen::load_data() { + int ret_val = this->m_prepare_load; + this->m_prepare_load = 0; + return ret_val; +} + +int WindowScreen::save_data() { + int ret_val = this->m_prepare_save; + this->m_prepare_save = 0; + return ret_val; +} + +bool WindowScreen::open_capture() { + bool ret_val = this->m_prepare_open; + this->m_prepare_open = false; + return ret_val; +} + +bool WindowScreen::close_capture() { + return this->m_prepare_quit; +} + +void WindowScreen::reset_operations(ScreenOperations &operations) { + operations.call_create = false; + operations.call_close = false; + operations.call_crop = false; + operations.call_rotate = false; + operations.call_screen_settings_update = false; + operations.call_blur = false; +} + +void WindowScreen::free_ownership_of_window(bool is_main_thread) { + if(is_main_thread == this->main_thread_owns_window) { + if(this->scheduled_work_on_window) + this->m_win.setActive(false); + else if(is_main_thread == this->loaded_info.async) + this->m_win.setActive(false); + } +} + +void WindowScreen::resize_in_rect(sf::RectangleShape &in_rect, int start_x, int start_y, int width, int height) { + in_rect.setTextureRect(sf::IntRect(start_y, start_x, height, width)); + + in_rect.setSize(sf::Vector2f(height, width)); + in_rect.setOrigin(height / 2, width / 2); + + in_rect.setRotation(-90); + in_rect.setPosition(width / 2, height / 2); +} + +int WindowScreen::get_screen_corner_modifier_x(int rotation, int width) { + if(((rotation / 10) % 4) == 1) { + return width; + } + if(((rotation / 10) % 4) == 2) { + return width; + } + return 0; +} + +int WindowScreen::get_screen_corner_modifier_y(int rotation, int height) { + if(((rotation / 10) % 4) == 2) { + return height; + } + if(((rotation / 10) % 4) == 3) { + return height; + } + return 0; +} + +void WindowScreen::print_notification(std::string text) { + this->notification->setText(text); + this->notification->startTimer(true); + this->notification->setShowText(true); +} + +void WindowScreen::print_notification_on_off(std::string base_text, bool value) { + std::string status_text = "On"; + if(!value) + status_text = "Off"; + this->print_notification(base_text + ": " + status_text); +} + +void WindowScreen::poll_window() { + if(this->m_win.isOpen()) { + sf::Event event; + this->events_access->lock(); + while(this->m_win.pollEvent(event)) { + events_queue.emplace(event.type, event.key.code, event.text.unicode); + } + this->events_access->unlock(); + } +} + +void WindowScreen::prepare_screen_rendering() { + if(loaded_operations.call_blur) { + this->m_out_rect_top.out_tex.setSmooth(this->loaded_info.is_blurred); + this->m_out_rect_bot.out_tex.setSmooth(this->loaded_info.is_blurred); + } + if(loaded_operations.call_crop) { + this->crop(); + } + if(loaded_operations.call_rotate) { + this->rotate(); + } + if(loaded_operations.call_screen_settings_update) { + this->update_screen_settings(); + } +} + +bool WindowScreen::window_needs_work() { + this->resize_window_and_out_rects(); + if(this->loaded_operations.call_close) + return true; + if(this->loaded_operations.call_create) + return true; + int width = this->m_width * this->loaded_info.scaling; + int height = this->m_height * this->loaded_info.scaling; + if(this->loaded_info.is_fullscreen) { + width = this->m_window_width; + height = this->m_window_height; + } + int win_width = this->m_win.getSize().x; + int win_height = this->m_win.getSize().y; + if((win_width != width) || (win_height != height)) + return true; + return false; +} + +void WindowScreen::window_factory(bool is_main_thread) { + if(this->loaded_operations.call_close) { + this->events_access->lock(); + this->m_win.setActive(true); + this->main_thread_owns_window = is_main_thread; + this->m_win.close(); + while(!events_queue.empty()) + events_queue.pop(); + this->events_access->unlock(); + this->loaded_operations.call_close = false; + this->loaded_operations.call_create = false; + } + if(this->loaded_operations.call_create) { + this->events_access->lock(); + this->m_win.setActive(true); + this->main_thread_owns_window = is_main_thread; + if(!this->loaded_info.is_fullscreen) { + this->update_screen_settings(); + this->m_win.create(sf::VideoMode(this->m_width * this->loaded_info.scaling, this->m_height * this->loaded_info.scaling), this->win_title); + this->update_view_size(); + } + else + this->m_win.create(this->curr_desk_mode, this->win_title, sf::Style::Fullscreen); + this->update_screen_settings(); + this->events_access->unlock(); + this->loaded_operations.call_create = false; + } + if(this->m_win.isOpen()) { + this->setWinSize(is_main_thread); + } + if((is_main_thread == this->main_thread_owns_window) && (this->main_thread_owns_window == this->loaded_info.async)) { + this->m_win.setActive(false); + } + this->is_window_factory_done = true; +} + +void WindowScreen::window_render_call() { + this->m_win.setVerticalSyncEnabled(this->loaded_info.v_sync_enabled); + this->m_win.setMouseCursorVisible(this->loaded_info.show_mouse); + this->in_tex.update((uint8_t*)this->saved_buf, IN_VIDEO_WIDTH, IN_VIDEO_HEIGHT, 0, 0); + + if((this->m_stype == WindowScreen::ScreenType::TOP) || (this->m_stype == WindowScreen::ScreenType::JOINT)) { + this->m_out_rect_top.out_tex.clear(); + this->m_out_rect_top.out_tex.draw(this->m_in_rect_top); + this->m_out_rect_top.out_tex.display(); + } + if((this->m_stype == WindowScreen::ScreenType::BOTTOM) || (this->m_stype == WindowScreen::ScreenType::JOINT)) { + this->m_out_rect_bot.out_tex.clear(); + this->m_out_rect_bot.out_tex.draw(this->m_in_rect_bot); + this->m_out_rect_bot.out_tex.display(); + } + + this->m_win.clear(); + if((this->m_stype == WindowScreen::ScreenType::TOP) || (this->m_stype == WindowScreen::ScreenType::JOINT)) + this->m_win.draw(this->m_out_rect_top.out_rect); + if((this->m_stype == WindowScreen::ScreenType::BOTTOM) || (this->m_stype == WindowScreen::ScreenType::JOINT)) + this->m_win.draw(this->m_out_rect_bot.out_rect); + this->notification->draw(this->m_win); + this->m_win.display(); + + if(this->loaded_info.bfi) { + if(this->frame_time > 0.0) { + sf::sleep(sf::seconds(frame_time / (this->loaded_info.bfi_divider * 1.1))); + this->m_win.clear(); + this->notification->draw(this->m_win); + this->m_win.display(); + } + } +} + +int WindowScreen::apply_offset_algo(int offset_contribute, OffsetAlgorithm chosen_algo) { + switch(chosen_algo) { + case NO_DISTANCE: + return 0; + case HALF_DISTANCE: + return offset_contribute / 2; + case MAX_DISTANCE: + return offset_contribute; + default: + return 0; + } +} + +void WindowScreen::set_position_screens(sf::Vector2f &curr_top_screen_size, sf::Vector2f &curr_bot_screen_size, int offset_x, int offset_y, int max_x, int max_y) { + int top_screen_x = 0, top_screen_y = 0; + int bot_screen_x = 0, bot_screen_y = 0; + int top_screen_width = curr_top_screen_size.x; + int top_screen_height = curr_top_screen_size.y; + int bot_screen_width = curr_bot_screen_size.x; + int bot_screen_height = curr_bot_screen_size.y; + + if((this->loaded_info.top_rotation / 10) % 2) { + top_screen_width = curr_top_screen_size.y; + top_screen_height = curr_top_screen_size.x; + } + if((this->loaded_info.bot_rotation / 10) % 2) { + bot_screen_width = curr_bot_screen_size.y; + bot_screen_height = curr_bot_screen_size.x; + } + + if(this->m_stype == WindowScreen::ScreenType::TOP) + bot_screen_width = bot_screen_height = 0; + if(this->m_stype == WindowScreen::ScreenType::BOTTOM) + top_screen_width = top_screen_height = 0; + + int greatest_width = top_screen_width; + if(greatest_width < bot_screen_width) + greatest_width = bot_screen_width; + int greatest_height = top_screen_height; + if(greatest_height < bot_screen_height) + greatest_height = bot_screen_height; + + if(this->m_stype == WindowScreen::ScreenType::JOINT) { + switch(this->loaded_info.bottom_pos) { + case UNDER_TOP: + bot_screen_x = apply_offset_algo(greatest_width - bot_screen_width, this->loaded_info.subscreen_offset_algorithm); + top_screen_x = apply_offset_algo(greatest_width - top_screen_width, this->loaded_info.subscreen_offset_algorithm); + bot_screen_y = top_screen_height; + if(max_y > 0) + bot_screen_y += apply_offset_algo(max_y - bot_screen_height - top_screen_height - offset_y, this->loaded_info.subscreen_attached_offset_algorithm); + break; + case RIGHT_TOP: + bot_screen_y = apply_offset_algo(greatest_height - bot_screen_height, this->loaded_info.subscreen_offset_algorithm); + top_screen_y = apply_offset_algo(greatest_height - top_screen_height, this->loaded_info.subscreen_offset_algorithm); + bot_screen_x = top_screen_width; + if(max_x > 0) + bot_screen_x += apply_offset_algo(max_x - bot_screen_width - top_screen_width - offset_x, this->loaded_info.subscreen_attached_offset_algorithm); + break; + case ABOVE_TOP: + bot_screen_x = apply_offset_algo(greatest_width - bot_screen_width, this->loaded_info.subscreen_offset_algorithm); + top_screen_x = apply_offset_algo(greatest_width - top_screen_width, this->loaded_info.subscreen_offset_algorithm); + top_screen_y = bot_screen_height; + if(max_y > 0) + top_screen_y += apply_offset_algo(max_y - bot_screen_height - top_screen_height - offset_y, this->loaded_info.subscreen_attached_offset_algorithm); + break; + case LEFT_TOP: + bot_screen_y = apply_offset_algo(greatest_height - bot_screen_height, this->loaded_info.subscreen_offset_algorithm); + top_screen_y = apply_offset_algo(greatest_height - top_screen_height, this->loaded_info.subscreen_offset_algorithm); + top_screen_x = bot_screen_width; + if(max_x > 0) + top_screen_x += apply_offset_algo(max_x - bot_screen_width - top_screen_width - offset_x, this->loaded_info.subscreen_attached_offset_algorithm); + break; + default: + break; + } + } + this->m_out_rect_top.out_rect.setPosition(top_screen_x + offset_x + get_screen_corner_modifier_x(this->loaded_info.top_rotation, top_screen_width), top_screen_y + offset_y + get_screen_corner_modifier_y(this->loaded_info.top_rotation, top_screen_height)); + this->m_out_rect_bot.out_rect.setPosition(bot_screen_x + offset_x + get_screen_corner_modifier_x(this->loaded_info.bot_rotation, bot_screen_width), bot_screen_y + offset_y + get_screen_corner_modifier_y(this->loaded_info.bot_rotation, bot_screen_height)); + + int top_end_x = top_screen_x + offset_x + top_screen_width; + int top_end_y = top_screen_y + offset_y + top_screen_height; + int bot_end_x = bot_screen_x + offset_x + bot_screen_width; + int bot_end_y = bot_screen_y + offset_y + bot_screen_height; + this->m_width = top_end_x; + this->m_height = top_end_y; + if(bot_end_x > this->m_width) + this->m_width = bot_end_x; + if(bot_end_y > this->m_height) + this->m_height = bot_end_y; +} + +int WindowScreen::prepare_screen_ratio(sf::Vector2f &screen_size, int own_rotation, int width_limit, int height_limit, int other_rotation) { + int own_width = screen_size.x; + int own_height = screen_size.y; + if((own_width <= 0) || (own_height <= 0)) + return 0; + + if((own_rotation / 10) % 2) + std::swap(own_width, own_height); + if((other_rotation / 10) % 2) + std::swap(width_limit, height_limit); + + int desk_height = curr_desk_mode.height; + int desk_width = curr_desk_mode.width; + int height_ratio = desk_height / own_height; + int width_ratio = desk_width / own_width; + switch(this->m_info.bottom_pos) { + case UNDER_TOP: + case ABOVE_TOP: + height_ratio = (desk_height - height_limit) / own_height; + break; + case LEFT_TOP: + case RIGHT_TOP: + width_ratio = (desk_width - width_limit) / own_width; + break; + default: + break; + } + if(width_ratio < height_ratio) + return width_ratio; + return height_ratio; +} + +void WindowScreen::calc_scaling_resize_screens(sf::Vector2f &own_screen_size, sf::Vector2f &other_screen_size, int &own_scaling, int &other_scaling, int own_rotation, int other_rotation, bool increase, bool mantain, bool set_to_zero) { + int chosen_ratio = prepare_screen_ratio(own_screen_size, own_rotation, other_screen_size.x, other_screen_size.y, other_rotation); + if(increase && (chosen_ratio > own_scaling) && (own_scaling > 0)) + own_scaling += 1; + else if(mantain && (chosen_ratio >= own_scaling) && (own_scaling > 0)) + own_scaling = own_scaling; + else + own_scaling = chosen_ratio; + if(set_to_zero) + own_scaling = 0; + int own_height = own_scaling * own_screen_size.y; + int own_width = own_scaling * own_screen_size.x; + other_scaling = prepare_screen_ratio(other_screen_size, other_rotation, own_width, own_height, own_rotation); + if(this->m_stype == WindowScreen::ScreenType::JOINT) { + // Due to size differences, it may be possible that + // the chosen screen might be able to increase its + // scaling even more without compromising the other one... + int other_height = other_scaling * other_screen_size.y; + int other_width = other_scaling * other_screen_size.x; + own_scaling = prepare_screen_ratio(own_screen_size, own_rotation, other_width, other_height, other_rotation); + } +} + +void WindowScreen::prepare_size_ratios(bool top_increase, bool bot_increase) { + if(!this->m_info.is_fullscreen) + return; + + sf::Vector2f top_screen_size = getShownScreenSize(true, this->m_info.crop_kind); + sf::Vector2f bot_screen_size = getShownScreenSize(false, this->m_info.crop_kind); + + if(this->m_stype == WindowScreen::ScreenType::TOP) { + bot_increase = true; + top_increase = false; + } + if(this->m_stype == WindowScreen::ScreenType::BOTTOM) { + bot_increase = false; + top_increase = true; + } + bool try_mantain_ratio = top_increase && bot_increase; + if(!(top_increase ^ bot_increase)) { + top_increase = false; + bot_increase = false; + } + bool prioritize_top = (!bot_increase) && (top_increase || (this->m_info.bottom_pos == UNDER_TOP) || (this->m_info.bottom_pos == RIGHT_TOP)); + if(prioritize_top) + calc_scaling_resize_screens(top_screen_size, bot_screen_size, this->m_info.top_scaling, this->m_info.bot_scaling, this->m_info.top_rotation, this->loaded_info.bot_rotation, top_increase, try_mantain_ratio, this->m_stype == WindowScreen::ScreenType::BOTTOM); + else + calc_scaling_resize_screens(bot_screen_size, top_screen_size, this->m_info.bot_scaling, this->m_info.top_scaling, this->m_info.bot_rotation, this->m_info.top_rotation, bot_increase, try_mantain_ratio, this->m_stype == WindowScreen::ScreenType::TOP); +} + +int WindowScreen::get_fullscreen_offset_x(int top_width, int top_height, int bot_width, int bot_height) { + if((this->loaded_info.top_rotation / 10) % 2) + std::swap(top_width, top_height); + if((this->loaded_info.bot_rotation / 10) % 2) + std::swap(bot_width, bot_height); + int greatest_width = top_width; + if(bot_width > top_width) + greatest_width = bot_width; + int offset_contribute = 0; + switch(this->loaded_info.bottom_pos) { + case UNDER_TOP: + case ABOVE_TOP: + offset_contribute = greatest_width; + break; + case LEFT_TOP: + case RIGHT_TOP: + offset_contribute = top_width + bot_width; + break; + default: + return 0; + } + return apply_offset_algo(curr_desk_mode.width - offset_contribute, this->loaded_info.total_offset_algorithm_x); +} + +int WindowScreen::get_fullscreen_offset_y(int top_width, int top_height, int bot_width, int bot_height) { + if((this->loaded_info.top_rotation / 10) % 2) + std::swap(top_width, top_height); + if((this->loaded_info.bot_rotation / 10) % 2) + std::swap(bot_width, bot_height); + int greatest_height = top_height; + if(bot_height > top_height) + greatest_height = bot_height; + int offset_contribute = 0; + switch(this->loaded_info.bottom_pos) { + case UNDER_TOP: + case ABOVE_TOP: + offset_contribute = top_height + bot_height; + break; + case LEFT_TOP: + case RIGHT_TOP: + offset_contribute = greatest_height; + break; + default: + return 0; + } + return apply_offset_algo(curr_desk_mode.height - offset_contribute, this->loaded_info.total_offset_algorithm_y); +} + +void WindowScreen::resize_window_and_out_rects() { + sf::Vector2f top_screen_size = getShownScreenSize(true, this->loaded_info.crop_kind); + sf::Vector2f bot_screen_size = getShownScreenSize(false, this->loaded_info.crop_kind); + + if(!this->loaded_info.is_fullscreen) { + this->m_out_rect_top.out_rect.setSize(top_screen_size); + this->m_out_rect_top.out_rect.setTextureRect(sf::IntRect(0, 0, top_screen_size.x, top_screen_size.y)); + this->m_out_rect_bot.out_rect.setSize(bot_screen_size); + this->m_out_rect_bot.out_rect.setTextureRect(sf::IntRect(0, 0, bot_screen_size.x, bot_screen_size.y)); + this->set_position_screens(top_screen_size, bot_screen_size, 0, 0, 0, 0); + } + else { + int top_height = this->loaded_info.top_scaling * top_screen_size.y; + int top_width = this->loaded_info.top_scaling * top_screen_size.x; + int bot_height = this->loaded_info.bot_scaling * bot_screen_size.y; + int bot_width = this->loaded_info.bot_scaling * bot_screen_size.x; + sf::Vector2f new_top_screen_size = sf::Vector2f(top_width, top_height); + sf::Vector2f new_bot_screen_size = sf::Vector2f(bot_width, bot_height); + this->m_out_rect_top.out_rect.setSize(new_top_screen_size); + this->m_out_rect_top.out_rect.setTextureRect(sf::IntRect(0, 0, top_screen_size.x, top_screen_size.y)); + this->m_out_rect_bot.out_rect.setSize(new_bot_screen_size); + this->m_out_rect_bot.out_rect.setTextureRect(sf::IntRect(0, 0, bot_screen_size.x, bot_screen_size.y)); + + int offset_x = get_fullscreen_offset_x(top_width, top_height, bot_width, bot_height); + int offset_y = get_fullscreen_offset_y(top_width, top_height, bot_width, bot_height); + + this->set_position_screens(new_top_screen_size, new_bot_screen_size, offset_x, offset_y, curr_desk_mode.width, curr_desk_mode.height); + } +} + +void WindowScreen::create_window(bool re_prepare_size) { + if(!this->m_info.is_fullscreen) { + this->m_info.show_mouse = true; + } + else { + this->curr_desk_mode = sf::VideoMode::getDesktopMode(); + this->m_window_width = curr_desk_mode.width; + this->m_window_height = curr_desk_mode.height; + this->m_info.show_mouse = false; + } + if(re_prepare_size) + this->prepare_size_ratios(false, false); + this->future_operations.call_create = true; +} + +void WindowScreen::update_view_size() { + if(!this->loaded_info.is_fullscreen) { + this->m_window_width = this->m_width; + this->m_window_height = this->m_height; + } + this->m_view.reset(sf::FloatRect(0, 0, this->m_window_width, this->m_window_height)); + this->m_win.setView(this->m_view); +} + +void WindowScreen::open() { + this->create_window(true); +} + +void WindowScreen::update_screen_settings() { + this->resize_window_and_out_rects(); + this->update_view_size(); +} + +void WindowScreen::rotate() { + this->m_out_rect_top.out_rect.setRotation(this->loaded_info.top_rotation); + this->m_out_rect_bot.out_rect.setRotation(this->loaded_info.bot_rotation); + this->loaded_operations.call_screen_settings_update = true; +} + +sf::Vector2f WindowScreen::getShownScreenSize(bool is_top, Crop &crop_kind) { + if(crop_kind >= Crop::CROP_END) + crop_kind = Crop::DEFAULT_3DS; + const int* widths = top_screen_crop_widths; + const int* heights = top_screen_crop_heights; + if(!is_top) { + widths = bot_screen_crop_widths; + heights = bot_screen_crop_heights; + } + int crop_arr_index = crop_kind - Crop::DEFAULT_3DS; + return sf::Vector2f(widths[crop_arr_index], heights[crop_arr_index]); +} + +void WindowScreen::crop() { + if(this->loaded_info.crop_kind >= Crop::CROP_END) + this->loaded_info.crop_kind = Crop::DEFAULT_3DS; + int crop_arr_index = this->loaded_info.crop_kind - Crop::DEFAULT_3DS; + + sf::Vector2f top_screen_size = getShownScreenSize(true, this->loaded_info.crop_kind); + sf::Vector2f bot_screen_size = getShownScreenSize(false, this->loaded_info.crop_kind); + + this->resize_in_rect(this->m_in_rect_top, top_screen_crop_x[crop_arr_index], top_screen_crop_y[crop_arr_index], top_screen_size.x, top_screen_size.y); + this->resize_in_rect(this->m_in_rect_bot, TOP_WIDTH_3DS + bot_screen_crop_x[crop_arr_index], bot_screen_crop_y[crop_arr_index], bot_screen_size.x, bot_screen_size.y); + this->loaded_operations.call_screen_settings_update = true; +} + +void WindowScreen::setWinSize(bool is_main_thread) { + int width = this->m_width * this->loaded_info.scaling; + int height = this->m_height * this->loaded_info.scaling; + if(this->loaded_info.is_fullscreen) { + width = this->m_window_width; + height = this->m_window_height; + } + int win_width = this->m_win.getSize().x; + int win_height = this->m_win.getSize().y; + if((win_width != width) || (win_height != height)) { + this->events_access->lock(); + this->m_win.setActive(true); + this->main_thread_owns_window = is_main_thread; + this->m_win.setSize(sf::Vector2u(width, height)); + this->events_access->unlock(); + } + win_width = this->m_win.getSize().x; + win_height = this->m_win.getSize().y; + int old_x_win_pos = this->m_win.getPosition().x; + int old_y_win_pos = this->m_win.getPosition().y; + int x_win_pos = old_x_win_pos; + int y_win_pos = old_y_win_pos; + if((x_win_pos + (win_width - 200)) < 0) + x_win_pos = -(win_width - 200); + if(y_win_pos < 0) + y_win_pos = 0; + if((x_win_pos != old_x_win_pos) || (y_win_pos != old_y_win_pos)) + this->m_win.setPosition(sf::Vector2i(x_win_pos, y_win_pos)); +} diff --git a/source/audio.cpp b/source/audio.cpp new file mode 100755 index 0000000..9a24469 --- /dev/null +++ b/source/audio.cpp @@ -0,0 +1,115 @@ +#include +#include "utils.hpp" +#include "audio.hpp" +#include "hw_defs.hpp" + +#include +#include +#include + +// Number of consecutive failures to restart the audio when switching output. +#define AUDIO_FAILURE_THRESHOLD 20 + +Sample::Sample(sf::Int16 *bytes, std::size_t size, float time) : bytes(bytes), size(size), time(time) {} + +//============================================================================ + +Audio::Audio() { + sf::SoundStream::initialize(AUDIO_CHANNELS, SAMPLE_RATE); + #if (SFML_VERSION_MAJOR > 2) || ((SFML_VERSION_MAJOR == 2) && (SFML_VERSION_MINOR >= 6)) + // Since we receive data every 16.6 ms, this is useless, + // and only increases CPU load... (Default is 10 ms) + //sf::SoundStream::setProcessingInterval(sf::Time::Zero); + #endif + start_audio(); +} + +void Audio::update_volume(int volume, bool mute) { + if(mute && (mute == loaded_mute)) { + return; + } + + if(volume < 0) + volume = 0; + if(volume > 100) + volume = 100; + + if(mute != loaded_mute) { + loaded_mute = mute; + loaded_volume = volume; + } + else if(volume != loaded_volume) { + loaded_mute = mute; + loaded_volume = volume; + } + else { + return; + } + setVolume(mute ? 0 : volume); +} + +void Audio::start_audio() { + samples_wait.try_lock(); + terminate = false; + num_consecutive_fast_seek = 0; + this->clock_time_start = std::chrono::high_resolution_clock::now(); +} + +void Audio::stop_audio() { + terminate = true; + samples_wait.unlock(); +} + +bool Audio::onGetData(sf::SoundStream::Chunk &data) { + if(terminate) + return false; + + // Do all of this to understand if the audio has errored out and + // if it should be restarted... + auto curr_time = std::chrono::high_resolution_clock::now(); + const std::chrono::duration diff = curr_time - this->clock_time_start; + if(diff.count() < 0.0005) + num_consecutive_fast_seek++; + else + num_consecutive_fast_seek = 0; + if(num_consecutive_fast_seek > AUDIO_FAILURE_THRESHOLD) + restart = true; + + if(restart) { + terminate = true; + return false; + } + + int loaded_samples = samples.size(); + while(loaded_samples <= 0) { + samples_wait.lock(); + if(terminate) + return false; + loaded_samples = samples.size(); + } + data.samples = (const sf::Int16*)buffer; + + while(loaded_samples > 2) { + samples.pop(); + loaded_samples = samples.size(); + } + + const sf::Int16 *data_samples = samples.front().bytes; + int count = samples.front().size; + memcpy(buffer, data_samples, count * sizeof(sf::Int16)); + + data.sampleCount = count; + + // Unused, but could be useful info + //double real_sample_rate = (1.0 / samples.front().time) * count / 2; + + samples.pop(); + + // Basically, look into how low the time between calls of the function is + curr_time = std::chrono::high_resolution_clock::now(); + clock_time_start = curr_time; + + return true; +} + +void Audio::onSeek(sf::Time timeOffset) {} diff --git a/source/cc3dsfs.cpp b/source/cc3dsfs.cpp new file mode 100755 index 0000000..5f9bd49 --- /dev/null +++ b/source/cc3dsfs.cpp @@ -0,0 +1,334 @@ +#include +#include +#if defined (__linux__) && defined(XLIB_BASED) +#include +#endif + +#if (!defined(_MSC_VER)) || (_MSC_VER > 1916) +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include + +#include "utils.hpp" +#include "3dscapture.hpp" +#include "hw_defs.hpp" +#include "frontend.hpp" +#include "audio.hpp" +#include "conversions.hpp" + +#define FPS_WINDOW_SIZE 64 + +// Threshold to keep the audio latency limited in "amount of frames". +#define AUDIO_LATENCY_LIMIT 4 +#define NUM_CONCURRENT_AUDIO_BUFFERS ((AUDIO_LATENCY_LIMIT * 2) + 1) + +bool load(const std::string path, const std::string name, ScreenInfo &top_info, ScreenInfo &bottom_info, ScreenInfo &joint_info, DisplayData &display_data, AudioData *audio_data) { + std::ifstream file(path + name); + std::string line; + + reset_screen_info(top_info); + reset_screen_info(bottom_info); + reset_screen_info(joint_info); + + if (!file.good()) { + printf("[%s] File \"%s\" load failed.\nDefaults re-loaded.\n", NAME, (path + name).c_str()); + return false; + } + + while (std::getline(file, line)) { + std::istringstream kvp(line); + std::string key; + + if (std::getline(kvp, key, '=')) { + std::string value; + + if (std::getline(kvp, value)) { + + load_screen_info(key, value, "bot_", bottom_info); + load_screen_info(key, value, "joint_", joint_info); + load_screen_info(key, value, "top_", top_info); + + if(bottom_info.crop_kind != Crop::NATIVE_DS) + bottom_info.crop_kind = Crop::DEFAULT_3DS; + if (key == "mute") { + audio_data->mute = std::stoi(value); + continue; + } + + if (key == "split") { + display_data.split = std::stoi(value); + continue; + } + + if (key == "volume") { + int pre_loaded_volume = std::stoi(value); + if(pre_loaded_volume < 0) + pre_loaded_volume = 0; + if(pre_loaded_volume > 100) + pre_loaded_volume = 100; + audio_data->volume = pre_loaded_volume; + continue; + } + } + } + } + + file.close(); + return true; +} + +void save(const std::string path, const std::string name, const ScreenInfo &top_info, const ScreenInfo &bottom_info, const ScreenInfo &joint_info, DisplayData &display_data, AudioData *audio_data) { + #if (!defined(_MSC_VER)) || (_MSC_VER > 1916) + std::filesystem::create_directories(path); + #else + std::experimental::filesystem::create_directories(path); + #endif + std::ofstream file(path + name); + if (!file.good()) { + printf("[%s] File \"%s\" save failed.\n", NAME, (path + name).c_str()); + return; + } + + file << save_screen_info("bot_", bottom_info); + file << save_screen_info("joint_", joint_info); + file << save_screen_info("top_", top_info); + file << "mute=" << audio_data->mute << std::endl; + file << "split=" << display_data.split << std::endl; + file << "volume=" << audio_data->volume << std::endl; + + file.close(); +} + +void soundCall(AudioData *audio_data, CaptureData* capture_data) { + Audio audio; + sf::Int16 out_buf[NUM_CONCURRENT_AUDIO_BUFFERS][MAX_SAMPLES_IN]; + int curr_out, prev_out = NUM_CONCURRENT_DATA_BUFFERS - 1, audio_buf_counter = 0; + const bool endianness = is_big_endian(); + + audio.update_volume(0, true); + volatile int loaded_samples; + + while (capture_data->running) { + if(capture_data->connected) { + bool timed_out = !capture_data->audio_wait.timed_lock(); + curr_out = (capture_data->curr_in - 1 + NUM_CONCURRENT_DATA_BUFFERS) % NUM_CONCURRENT_DATA_BUFFERS; + + if((!capture_data->cooldown_curr_in) && (curr_out != prev_out)) { + loaded_samples = audio.samples.size(); + if((capture_data->read[curr_out] > sizeof(VideoInputData)) && (loaded_samples < AUDIO_LATENCY_LIMIT)) { + int n_samples = (capture_data->read[curr_out] - sizeof(VideoInputData)) / 2; + if(n_samples > MAX_SAMPLES_IN) { + n_samples = MAX_SAMPLES_IN; + } + double out_time = capture_data->time_in_buf[curr_out]; + convertAudioToOutput(&capture_data->capture_buf[curr_out], out_buf[audio_buf_counter], n_samples, endianness); + audio.samples.emplace(out_buf[audio_buf_counter], n_samples, out_time); + if(++audio_buf_counter == NUM_CONCURRENT_AUDIO_BUFFERS) { + audio_buf_counter = 0; + } + audio.samples_wait.unlock(); + } + } + prev_out = curr_out; + + loaded_samples = audio.samples.size(); + if(audio.getStatus() != sf::SoundStream::Playing) { + audio.stop_audio(); + if(loaded_samples > 0) { + if(audio.restart) { + audio.stop(); + audio.~Audio(); + ::new(&audio) Audio(); + } + audio.start_audio(); + audio.play(); + } + } + else { + audio.update_volume(audio_data->volume, audio_data->mute); + } + } + else { + audio.stop_audio(); + audio.stop(); + sf::sleep(sf::milliseconds(1000/USB_CHECKS_PER_SECOND)); + } + } + + audio.stop_audio(); + audio.stop(); +} + +void mainVideoOutputCall(AudioData* audio_data, CaptureData* capture_data) { + VideoOutputData *out_buf; + VideoOutputData *null_buf; + double *curr_fps_array; + int num_elements_fps_array = 0; + int curr_out, prev_out = NUM_CONCURRENT_DATA_BUFFERS - 1; + DisplayData display_data; + display_data.split = false; + bool reload = true; + bool skip_io = false; + int num_allowed_blanks = MAX_ALLOWED_BLANKS; + double last_frame_time = 0; + std::mutex events_access; + + #if not(defined(_WIN32) || defined(_WIN64)) + std::string cfg_dir = std::string(std::getenv("HOME")) + "/.config/" + std::string(NAME); + #else + std::string cfg_dir = ".config/" + std::string(NAME); + #endif + + out_buf = new VideoOutputData; + null_buf = new VideoOutputData; + curr_fps_array = new double[FPS_WINDOW_SIZE]; + memset(out_buf, 0, sizeof(VideoOutputData)); + memset(null_buf, 0, sizeof(VideoOutputData)); + + WindowScreen top_screen(WindowScreen::ScreenType::TOP, &display_data, audio_data, &events_access); + WindowScreen bot_screen(WindowScreen::ScreenType::BOTTOM, &display_data, audio_data, &events_access); + WindowScreen joint_screen(WindowScreen::ScreenType::JOINT, &display_data, audio_data, &events_access); + + if(!skip_io) { + load(cfg_dir + "/", std::string(NAME) + ".cfg", top_screen.m_info, bot_screen.m_info, joint_screen.m_info, display_data, audio_data); + } + + top_screen.build(); + bot_screen.build(); + joint_screen.build(); + + std::thread top_thread(screen_display_thread, &top_screen, capture_data); + std::thread bot_thread(screen_display_thread, &bot_screen, capture_data); + std::thread joint_thread(screen_display_thread, &joint_screen, capture_data); + + while (capture_data->running) { + + if(capture_data->connected) { + bool timed_out = !capture_data->video_wait.timed_lock(); + curr_out = (capture_data->curr_in - 1 + NUM_CONCURRENT_DATA_BUFFERS) % NUM_CONCURRENT_DATA_BUFFERS; + + if ((!capture_data->cooldown_curr_in) && (curr_out != prev_out)) { + double frame_time = capture_data->time_in_buf[curr_out]; + if(capture_data->read[curr_out] >= sizeof(VideoInputData)) { + convertVideoToOutput(&capture_data->capture_buf[curr_out].video_data, out_buf); + num_allowed_blanks = MAX_ALLOWED_BLANKS; + } + else { + if(num_allowed_blanks > 0) { + num_allowed_blanks--; + } + else { + memset(out_buf, 0, sizeof(VideoOutputData)); + } + } + + update_output(top_screen, bot_screen, joint_screen, reload, display_data.split, frame_time, out_buf); + last_frame_time = frame_time; + + curr_fps_array[num_elements_fps_array % FPS_WINDOW_SIZE] = 1.0 / frame_time; + num_elements_fps_array++; + if(num_elements_fps_array >= (2 * FPS_WINDOW_SIZE)) + num_elements_fps_array -= FPS_WINDOW_SIZE; + } + else { + VideoOutputData *chosen_buf = out_buf; + if(capture_data->cooldown_curr_in) { + last_frame_time = 0; + chosen_buf = null_buf; + } + update_output(top_screen, bot_screen, joint_screen, reload, display_data.split, last_frame_time, chosen_buf); + } + + prev_out = curr_out; + } + else { + curr_fps_array[num_elements_fps_array % FPS_WINDOW_SIZE] = 0; + num_elements_fps_array++; + if(num_elements_fps_array >= (2 * FPS_WINDOW_SIZE)) + num_elements_fps_array -= FPS_WINDOW_SIZE; + sf::sleep(sf::milliseconds(1000/USB_CHECKS_PER_SECOND)); + } + + int available_fps = FPS_WINDOW_SIZE; + if(num_elements_fps_array < available_fps) + available_fps = num_elements_fps_array; + double fps_sum = 0; + for(int i = 0; i < available_fps; i++) + fps_sum += curr_fps_array[i]; + + if(!capture_data->connected) + update_output(top_screen, bot_screen, joint_screen, reload, display_data.split, 0, null_buf); + + int load_index = 0; + int save_index = 0; + top_screen.poll(); + bot_screen.poll(); + joint_screen.poll(); + + if(top_screen.open_capture() || bot_screen.open_capture() || joint_screen.open_capture()) { + capture_data->connected = connect(true, capture_data); + } + + if(top_screen.close_capture() || bot_screen.close_capture() || joint_screen.close_capture()) { + capture_data->running = false; + } + + if((load_index = top_screen.load_data()) || (load_index = bot_screen.load_data()) || (load_index = joint_screen.load_data())) { + if (!skip_io) { + load(cfg_dir + "/presets/", "layout" + std::to_string(load_index) + ".cfg", top_screen.m_info, bot_screen.m_info, joint_screen.m_info, display_data, audio_data); + reload = true; + } + } + + if((save_index = top_screen.save_data()) || (save_index = bot_screen.save_data()) || (save_index = joint_screen.save_data())) { + if (!skip_io) { + save(cfg_dir + "/presets/", "layout" + std::to_string(save_index) + ".cfg", top_screen.m_info, bot_screen.m_info, joint_screen.m_info, display_data, audio_data); + } + } + } + + top_screen.end(); + bot_screen.end(); + joint_screen.end(); + + top_thread.join(); + bot_thread.join(); + joint_thread.join(); + + if (!skip_io) { + save(cfg_dir + "/", std::string(NAME) + ".cfg", top_screen.m_info, bot_screen.m_info, joint_screen.m_info, display_data, audio_data); + } + + delete out_buf; + delete null_buf; + delete []curr_fps_array; +} + +int main(int argc, char **argv) { + #if defined(__linux__) && defined(XLIB_BASED) + XInitThreads(); + #endif + AudioData audio_data; + CaptureData* capture_data = new CaptureData; + audio_data.volume = 50; + audio_data.mute = false; + + capture_data->connected = connect(true, capture_data); + std::thread capture_thread(captureCall, capture_data); + std::thread audio_thread(soundCall, &audio_data, capture_data); + + mainVideoOutputCall(&audio_data, capture_data); + audio_thread.join(); + capture_thread.join(); + delete capture_data; + + return 0; +} diff --git a/source/conversions.cpp b/source/conversions.cpp new file mode 100755 index 0000000..175c1d9 --- /dev/null +++ b/source/conversions.cpp @@ -0,0 +1,32 @@ +#include +#include "3dscapture.hpp" +#include "frontend.hpp" +#include "conversions.hpp" + +#include + +static inline void convertVideoToOutputChunk(VideoInputData *p_in, VideoOutputData *p_out, int iters, int start_in, int start_out) { + for(int i = 0; i < iters; i++) { + for(int u = 0; u < 3; u++) + p_out->screen_data[start_out + i][u] = p_in->screen_data[start_in + i][u]; + p_out->screen_data[start_out + i][3] = 0xff; + } +} + +void convertVideoToOutput(VideoInputData *p_in, VideoOutputData *p_out) { + convertVideoToOutputChunk(p_in, p_out, IN_VIDEO_NO_BOTTOM_SIZE, 0, 0); + + for(int i = 0; i < ((IN_VIDEO_SIZE - IN_VIDEO_NO_BOTTOM_SIZE) / (IN_VIDEO_WIDTH * 2)); i++) { + convertVideoToOutputChunk(p_in, p_out, IN_VIDEO_WIDTH, ((i * 2) * IN_VIDEO_WIDTH) + IN_VIDEO_NO_BOTTOM_SIZE, TOP_SIZE_3DS + (i * IN_VIDEO_WIDTH)); + convertVideoToOutputChunk(p_in, p_out, IN_VIDEO_WIDTH, (((i * 2) + 1) * IN_VIDEO_WIDTH) + IN_VIDEO_NO_BOTTOM_SIZE, IN_VIDEO_NO_BOTTOM_SIZE + (i * IN_VIDEO_WIDTH)); + } +} + +void convertAudioToOutput(CaptureReceived *p_in, sf::Int16 *p_out, const int n_samples, const bool is_big_endian) { + if(!is_big_endian) + memcpy(p_out, p_in->audio_data, n_samples * 2); + else + for(int i = 0; i < n_samples; i++) + p_out[i] = ((p_in->audio_data[i] & 0xFF) << 8) | (p_in->audio_data[i] >> 8); +} + diff --git a/source/frontend.cpp b/source/frontend.cpp new file mode 100755 index 0000000..93c7f20 --- /dev/null +++ b/source/frontend.cpp @@ -0,0 +1,151 @@ +#include "utils.hpp" +#include "3dscapture.hpp" +#include "frontend.hpp" + +void reset_screen_info(ScreenInfo &info) { + info.is_blurred = false; + info.crop_kind = DEFAULT_3DS; + info.scaling = 1.0; + info.is_fullscreen = false; + info.bottom_pos = UNDER_TOP; + info.subscreen_offset_algorithm = HALF_DISTANCE; + info.subscreen_attached_offset_algorithm = NO_DISTANCE; + info.total_offset_algorithm_x = HALF_DISTANCE; + info.total_offset_algorithm_y = HALF_DISTANCE; + info.top_rotation = 0; + info.bot_rotation = 0; + info.show_mouse = true; + info.v_sync_enabled = false; + #if defined(_WIN32) || defined(_WIN64) + info.async = false; + #else + info.async = true; + #endif + info.top_scaling = -1; + info.bot_scaling = -1; + info.bfi = false; + info.bfi_divider = 2.0; +} + +void load_screen_info(std::string key, std::string value, std::string base, ScreenInfo &info) { + if(key == (base + "blur")) { + info.is_blurred = std::stoi(value); + return; + } + if(key == (base + "crop")) { + info.crop_kind = static_cast(std::stoi(value) % Crop::CROP_END); + return; + } + if(key == (base + "scale")) { + info.scaling = std::stod(value); + if(info.scaling < 1.25) + info.scaling = 1.0; + if(info.scaling > 44.75) + info.scaling = 45.0; + return; + } + if(key == (base + "fullscreen")) { + info.is_fullscreen = std::stoi(value); + return; + } + if(key == (base + "bot_pos")) { + info.bottom_pos = static_cast(std::stoi(value) % BottomRelativePosition::BOT_REL_POS_END); + return; + } + if(key == (base + "sub_off_algo")) { + info.subscreen_offset_algorithm = static_cast(std::stoi(value) % OffsetAlgorithm::OFF_ALGO_END); + return; + } + if(key == (base + "sub_att_off_algo")) { + info.subscreen_attached_offset_algorithm = static_cast(std::stoi(value) % OffsetAlgorithm::OFF_ALGO_END); + return; + } + if(key == (base + "off_algo_x")) { + info.total_offset_algorithm_x = static_cast(std::stoi(value) % OffsetAlgorithm::OFF_ALGO_END); + return; + } + if(key == (base + "off_algo_y")) { + info.total_offset_algorithm_y = static_cast(std::stoi(value) % OffsetAlgorithm::OFF_ALGO_END); + return; + } + if(key == (base + "top_rot")) { + info.top_rotation = std::stoi(value); + info.top_rotation %= 360; + info.top_rotation += (info.top_rotation < 0) ? 360 : 0; + return; + } + if(key == (base + "bot_rot")) { + info.bot_rotation = std::stoi(value); + info.bot_rotation %= 360; + info.bot_rotation += (info.bot_rotation < 0) ? 360 : 0; + return; + } + if(key == (base + "vsync")) { + info.v_sync_enabled = std::stoi(value); + return; + } + if(key == (base + "async")) { + info.async = std::stoi(value); + return; + } + if(key == (base + "top_scaling")) { + info.top_scaling = std::stoi(value); + return; + } + if(key == (base + "bot_scaling")) { + info.bot_scaling = std::stoi(value); + return; + } + if(key == (base + "bfi")) { + info.bfi = std::stoi(value); + return; + } + if(key == (base + "bfi_divider")) { + info.bfi_divider = std::stod(value); + if(info.bfi_divider < 1.0) + info.bfi_divider = 1.0; + return; + } +} + +std::string save_screen_info(std::string base, const ScreenInfo &info) { + std::string out = ""; + out += base + "blur=" + std::to_string(info.is_blurred) + "\n"; + out += base + "crop=" + std::to_string(info.crop_kind) + "\n"; + out += base + "scale=" + std::to_string(info.scaling) + "\n"; + out += base + "fullscreen=" + std::to_string(info.is_fullscreen) + "\n"; + out += base + "bot_pos=" + std::to_string(info.bottom_pos) + "\n"; + out += base + "sub_off_algo=" + std::to_string(info.subscreen_offset_algorithm) + "\n"; + out += base + "sub_att_off_algo=" + std::to_string(info.subscreen_attached_offset_algorithm) + "\n"; + out += base + "off_algo_x=" + std::to_string(info.total_offset_algorithm_x) + "\n"; + out += base + "off_algo_y=" + std::to_string(info.total_offset_algorithm_y) + "\n"; + out += base + "top_rot=" + std::to_string(info.top_rotation) + "\n"; + out += base + "bot_rot=" + std::to_string(info.bot_rotation) + "\n"; + out += base + "vsync=" + std::to_string(info.v_sync_enabled) + "\n"; + out += base + "async=" + std::to_string(info.async) + "\n"; + out += base + "top_scaling=" + std::to_string(info.top_scaling) + "\n"; + out += base + "bot_scaling=" + std::to_string(info.bot_scaling) + "\n"; + out += base + "bfi=" + std::to_string(info.bfi) + "\n"; + out += base + "bfi_divider=" + std::to_string(info.bfi_divider) + "\n"; + return out; +} + +void update_output(WindowScreen &top_screen, WindowScreen &bot_screen, WindowScreen &joint_screen, bool &reload, bool split, double frame_time, VideoOutputData *out_buf) { + if(reload) { + top_screen.reload(); + bot_screen.reload(); + joint_screen.reload(); + reload = false; + } + // Make sure the window is closed before showing split/non-split + if(split) + joint_screen.draw(frame_time, out_buf); + top_screen.draw(frame_time, out_buf); + bot_screen.draw(frame_time, out_buf); + if(!split) + joint_screen.draw(frame_time, out_buf); +} + +void screen_display_thread(WindowScreen *screen, CaptureData* capture_data) { + screen->display_thread(capture_data); +} diff --git a/source/utils.cpp b/source/utils.cpp new file mode 100755 index 0000000..b7f3523 --- /dev/null +++ b/source/utils.cpp @@ -0,0 +1,73 @@ +#include "utils.hpp" + +#include +#include +#include +#include + +bool is_big_endian(void) { + union { + uint32_t i; + char c[4]; + } value = {0x01020304}; + + return value.c[0] == 1; +} + +//============================================================================ + +ConsumerMutex::ConsumerMutex() { + count = 0; +} + +void ConsumerMutex::lock() { + access_mutex.lock(); + bool success = false; + while(!success) { + if(count) { + count--; + success = true; + } + else { + condition.wait(access_mutex); + } + } + access_mutex.unlock(); +} + +bool ConsumerMutex::timed_lock() { + access_mutex.lock(); + bool success = false; + while(!success) { + if(count) { + count--; + success = true; + } + else { + auto result = condition.wait_for(access_mutex, max_timed_wait); + if((result == std::cv_status::timeout) && (!count)) + break; + } + } + access_mutex.unlock(); + return success; +} + +bool ConsumerMutex::try_lock() { + access_mutex.lock(); + bool success = false; + if(count) { + count--; + success = true; + } + access_mutex.unlock(); + return success; +} + +void ConsumerMutex::unlock() { + access_mutex.lock(); + // Enforce 1 max + count = 1; + condition.notify_one(); + access_mutex.unlock(); +} diff --git a/tools/bin2c.cpp b/tools/bin2c.cpp new file mode 100644 index 0000000..c466f9a --- /dev/null +++ b/tools/bin2c.cpp @@ -0,0 +1,73 @@ +#include +#include +#include +#include +#include + +using namespace std; + +int main(int argc, char *argv[]) { + if(argc < 5) { + cout << "Usage: " << argv[0] << " binary_file output_folder base_output_file_name base_array_name" << endl; + return -1; + } + + ifstream input(argv[1], ios::in | ios::binary); + if(!input) { + cout << "Couldn't open input file!" << endl; + return -2; + } + vector buffer(std::istreambuf_iterator(input), {}); + + input.close(); + + string output_folder(argv[2]); + string output_base_filename(argv[3]); + string base_array_name(argv[4]); + + string name_array = base_array_name; + string name_len_array = base_array_name + "_len"; + int len = buffer.size(); + + ofstream output(output_folder + filesystem::path::preferred_separator + output_base_filename + ".cpp", ios::out); + if(!output) { + cout << "Couldn't open output cpp file!" << endl; + return -2; + } + output << "#include \"" << output_base_filename << ".h\"" << endl; + output << "unsigned char " << name_array << "[" << name_len_array << "] = {"; + + for(int i = 0; i < len; i+= 16) { + output << endl; + int curr_line_todo = 16; + if((len - i) < curr_line_todo) + curr_line_todo = len - i; + for(int j = 0; j < curr_line_todo; j++) { + output << "0x"; + int value = ((int)buffer[i + j]); + if(value < 16) + output << "0"; + output << std::hex << value; + if((i + j) < (len - 1)) + output << ","; + if(j < (curr_line_todo - 1)) + output << " "; + } + } + output << "};"; + + output.close(); + + ofstream output2(output_folder + filesystem::path::preferred_separator + output_base_filename + ".h", ios::out); + if(!output2) { + cout << "Couldn't open output h file!" << endl; + return -3; + } + output2 << "#ifndef __" << base_array_name << "_H" << endl << "#define __" << base_array_name << "_H" << endl; + output2 << "extern unsigned char " << name_array << "[];" << endl; + output2 << "#define " << name_len_array << " " << len << endl; + output2 << "#endif" << endl; + output2.close(); + + return 0; +}