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 0000000..dc900c2 Binary files /dev/null and b/data/font.ttf differ 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; +}