diff --git a/.github/linux-appimage-qt.sh b/.github/linux-appimage-qt.sh new file mode 100644 index 00000000..0a1fdb90 --- /dev/null +++ b/.github/linux-appimage-qt.sh @@ -0,0 +1,9 @@ +# Prepare Tools for building the AppImage +wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage +wget https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage + +chmod a+x linuxdeploy-x86_64.AppImage +chmod a+x linuxdeploy-plugin-qt-x86_64.AppImage + +# Build AppImage +./linuxdeploy-x86_64.AppImage --appdir AppDir -d ./.github/Alber.desktop -e ./build/Alber -i ./docs/img/Alber.png --output appimage --plugin qt diff --git a/.github/mac-bundle-qt.sh b/.github/mac-bundle-qt.sh new file mode 100644 index 00000000..e18be8a5 --- /dev/null +++ b/.github/mac-bundle-qt.sh @@ -0,0 +1,52 @@ +# Taken from pcsx-redux create-app-bundle.sh +# For Plist buddy +PATH="$PATH:/usr/libexec" + + +# Construct the app iconset. +mkdir alber.iconset +convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 72 -resize 16x16 alber.iconset/icon_16x16.png +convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 144 -resize 32x32 alber.iconset/icon_16x16@2x.png +convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 72 -resize 32x32 alber.iconset/icon_32x32.png +convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 144 -resize 64x64 alber.iconset/icon_32x32@2x.png +convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 72 -resize 128x128 alber.iconset/icon_128x128.png +convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 144 -resize 256x256 alber.iconset/icon_128x128@2x.png +convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 72 -resize 256x256 alber.iconset/icon_256x256.png +convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 144 -resize 512x512 alber.iconset/icon_256x256@2x.png +convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 72 -resize 512x512 alber.iconset/icon_512x512.png +convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 144 -resize 1024x1024 alber.iconset/icon_512x512@2x.png +iconutil --convert icns alber.iconset + +# Set up the .app directory +mkdir -p Alber.app/Contents/MacOS/Libraries +mkdir Alber.app/Contents/Resources + + +# Copy binary into App +cp ./build/Alber Alber.app/Contents/MacOS/Alber +chmod a+x Alber.app/Contents/Macos/Alber + +# Copy icons into App +cp alber.icns Alber.app/Contents/Resources/AppIcon.icns + +# Fix up Plist stuff +PlistBuddy Alber.app/Contents/Info.plist -c "add CFBundleDisplayName string Alber" +PlistBuddy Alber.app/Contents/Info.plist -c "add CFBundleIconName string AppIcon" +PlistBuddy Alber.app/Contents/Info.plist -c "add CFBundleIconFile string AppIcon" +PlistBuddy Alber.app/Contents/Info.plist -c "add NSHighResolutionCapable bool true" +PlistBuddy Alber.app/Contents/version.plist -c "add ProjectName string Alber" + +PlistBuddy Alber.app/Contents/Info.plist -c "add CFBundleExecutable string Alber" +PlistBuddy Alber.app/Contents/Info.plist -c "add CFBundleDevelopmentRegion string en" +PlistBuddy Alber.app/Contents/Info.plist -c "add CFBundleInfoDictionaryVersion string 6.0" +PlistBuddy Alber.app/Contents/Info.plist -c "add CFBundleName string Panda3DS" +PlistBuddy Alber.app/Contents/Info.plist -c "add CFBundlePackageType string APPL" +PlistBuddy Alber.app/Contents/Info.plist -c "add NSHumanReadableCopyright string Copyright 2023 Panda3DS Team" + +PlistBuddy Alber.app/Contents/Info.plist -c "add LSMinimumSystemVersion string 10.15" + +# Bundle dylibs +ruby .github/mac-libs.rb ./build/ + +# relative rpath +install_name_tool -add_rpath @loader_path/../Frameworks Alber.app/Contents/MacOS/Alber diff --git a/.github/mac-libs.rb b/.github/mac-libs.rb new file mode 100644 index 00000000..d0c83a5b --- /dev/null +++ b/.github/mac-libs.rb @@ -0,0 +1,255 @@ +#!/usr/bin/env ruby + +require "open3" +require "fileutils" + +$app_name = "Alber" +$build_dmg = false +$build_dir = "" +$bundle = "" +$fallback_rpaths = [] + +def frameworks_dir + File.join($bundle, "Contents", "Frameworks") +end + +def executable + File.join($bundle, "Contents", "MacOS", $app_name) +end + +def get_rpaths(lib) + out, _ = Open3.capture2("otool", "-l", lib) + out = out.split("\n") + rpaths = [] + + out.each_with_index do |line, i| + if line.match(/^ *cmd LC_RPATH$/) + rpaths << out[i + 2].strip.split(" ")[1] + end + end + + return rpaths +end + +def get_load_libs(lib) + out, _ = Open3.capture2("otool", "-L", lib) + out.split("\n") + .drop(1) + .map { |it| it.strip.gsub(/ \(.*/, "") } +end + +def expand_load_path(lib, path) + if path.match(/@(rpath|loader_path|executable_path)/) + path_type = $1 + file_name = path.gsub(/^@#{path_type}\//, "") + + case path_type + when "rpath" + get_rpaths(lib).each do |rpath| + file = File.join(rpath, file_name) + return file, :rpath if File.exist? file + if rpath.match(/^@executable_path(.*)/) != nil + relative = rpath.sub(/^@executable_path/, "") + return "#{$bundle}/Contents/MacOS#{relative}/#{file_name}", :executable_path + end + end + file = $fallback_rpaths + .map { |it| File.join(it, file_name) } + .find { |it| File.exist? it } + if file == nil + path = File.join(File.dirname(lib), file_name) + file = path if File.exist? path + end + return file, :rpath if file + when "executable_path" + file = File.join(File.dirname(executable), file_name) + return file, :executable_path if File.exist? file + when "loader_path" + file = File.join(File.dirname(lib), file_name) + return file, :loader_path if File.exist? file + else + throw "Unknown @path type" + end + else + return File.absolute_path(path), :absolute + end + + return nil +end + +def system_path?(path) + path.match(/^\/usr\/lib|^\/System/) != nil +end + +def system_lib?(lib) + system_path? File.dirname(lib) +end + +def install_name_tool(exec, action, path1, path2 = nil) + args = ["-#{action.to_s}", path1] + args << path2 if path2 != nil + + Open3.popen3("install_name_tool", *args, exec) do |stdin, stdout, stderr, thread| + print stdout.read + err = stderr.read + unless err.match? "code signature" + print err + end + end +end + +def strip(lib) + out, _ = Open3.capture2("strip", "-no_code_signature_warning", "-Sx", lib) + print out +end + +def fixup_libs(prog, orig_path) + throw "fixup_libs: #{prog} doesn't exist" unless File.exist? prog + + libs = get_load_libs(prog).map { |it| expand_load_path(orig_path, it) }.select { |it| not system_lib? it[0] } + + FileUtils.chmod("u+w", prog) + strip prog + + libs.each do |lib| + libpath, libtype = lib + if File.basename(libpath) == File.basename(prog) + if libtype == :absolute + install_name_tool prog, :change, libpath, File.join("@rpath", File.basename(libpath)) + end + next + end + + framework = libpath.match(/(.*).framework/) + framework = framework.to_s if framework + + if framework + fwlib = libpath.sub(framework + "/", "") + fwname = File.basename(framework) + + unless libtype == :rpath + install_name_tool prog, :change, libpath, File.join("@rpath", fwname, fwlib) + end + + next if File.exist? File.join(frameworks_dir, fwname) + expath, _ = expand_load_path(orig_path, framework) + FileUtils.cp_r(expath, frameworks_dir, preserve: true) + FileUtils.chmod_R("u+w", File.join(frameworks_dir, fwname)) + fixup_libs File.join(frameworks_dir, fwname, fwlib), libpath + else + libname = File.basename(libpath) + dest = File.join(frameworks_dir, libname) + + if libtype == :absolute + install_name_tool prog, :change, libpath, File.join("@rpath", libname) + end + + next if File.exist? dest + expath, _ = expand_load_path(orig_path, libpath) + FileUtils.copy expath, frameworks_dir + FileUtils.chmod("u+w", dest) + fixup_libs dest, libpath + end + end +end + +if ARGV[0] == "--dmg" + $build_dmg = true + ARGV.shift +end + +if ARGV.length != 1 + puts "Usage: #{Process.argv0} [--dmg] " + return +end + +$build_dir = ARGV[0] +unless File.exist? $build_dir + puts "#{$build_dir} doesn't exist" +end + + +$bundle = "#{$app_name}.app" + +unless File.exist? $bundle and File.exist? File.join($build_dir, "CMakeCache.txt") + puts "#{$build_dir} doesn't look like a valid build directory" + exit 1 +end + +File.read(File.join($build_dir, "CMakeCache.txt")) + .split("\n") + .find { |it| it.match /Qt(.)_DIR:PATH=(.*)/ } + +qt_major = $1 +qt_dir = $2 +qt_dir = File.absolute_path("#{qt_dir}/../../..") + +for lib in get_load_libs(executable) do + next if system_lib? lib + + path = File.dirname(lib) + + if path.match? ".framework" + path = path.sub(/\/[^\/]+\.framework.*/, "") + end + + $fallback_rpaths << path unless $fallback_rpaths.include? path +end + +$fallback_rpaths << File.join(qt_dir, "lib") + +plugin_paths = [ + File.join(qt_dir, "libexec", "qt#{qt_major}", "plugins"), + File.join(qt_dir, "plugins"), + File.join(qt_dir, "share", "qt", "plugins") +] + +qt_plugins = plugin_paths.find { |file| File.exist? file } + +if qt_plugins == nil + puts "Couldn't find Qt plugins, tried looking for:" + plugin_paths.each { |path| puts " - #{path}" } + exit 1 +end + +FileUtils.mkdir_p(frameworks_dir) +fixup_libs(executable, executable) + +bundle_plugins = File.join($bundle, "Contents", "PlugIns") + +want_plugins = ["styles/libqmacstyle.dylib", "platforms/libqcocoa.dylib", "imageformats/libqsvg.dylib"] +want_plugins.each do |plug| + destdir = File.join(bundle_plugins, File.dirname(plug)) + FileUtils.mkdir_p(destdir) + FileUtils.copy(File.join(qt_plugins, plug), destdir) + fixup_libs File.join(bundle_plugins, plug), File.join(qt_plugins, plug) +end + +want_rpath = "@executable_path/../Frameworks" +exec_rpaths = get_rpaths(executable) +exec_rpaths.select { |path| path != want_rpath }.each do |path| + install_name_tool executable, :delete_rpath, path +end + +unless exec_rpaths.include? want_rpath + install_name_tool executable, :add_rpath, want_rpath +end + +exec_rpaths = get_rpaths(executable) + +Dir.glob("#{frameworks_dir}/**/Headers").each do |dir| + FileUtils.rm_rf dir +end + +out, _ = Open3.capture2("codesign", "-s", "-", "-f", "--deep", $bundle) +print out + +if $build_dmg + dmg_dir = File.join($build_dir, "dmg") + FileUtils.mkdir_p(dmg_dir) + FileUtils.cp_r($bundle, dmg_dir, preserve: true) + FileUtils.ln_s("/Applications", File.join(dmg_dir, "Applications")) + + `hdiutil create -fs HFS+ -volname melonDS -srcfolder "#{dmg_dir}" -ov -format UDBZ "#{$build_dir}/melonDS.dmg"` + FileUtils.rm_rf(dmg_dir) +end diff --git a/.github/workflows/Qt_Build.yml b/.github/workflows/Qt_Build.yml new file mode 100644 index 00000000..50059906 --- /dev/null +++ b/.github/workflows/Qt_Build.yml @@ -0,0 +1,149 @@ +name: Qt Build + +on: + push: + branches: + - master + pull_request: + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Release + +jobs: + Windows: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v2 + - name: Fetch submodules + run: git submodule update --init --recursive + + - name: Setup Qt + uses: jurplel/install-qt-action@v3 + with: + arch: win64_msvc2019_64 + version: 6.2.0 + + - name: Setup Vulkan SDK + uses: humbletim/setup-vulkan-sdk@v1.2.0 + with: + vulkan-query-version: latest + vulkan-use-cache: true + vulkan-components: Vulkan-Headers, Vulkan-Loader, Glslang + + - name: Configure CMake + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DENABLE_USER_BUILD=ON -DENABLE_QT_GUI=ON + + - name: Build + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + + - name: Deploy + run: | + mkdir upload + move build/Release/Alber.exe upload + windeployqt --dir upload upload/Alber.exe + + - name: Upload executable + uses: actions/upload-artifact@v2 + with: + name: Windows executable + path: upload + + MacOS: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + - name: Fetch submodules + run: git submodule update --init --recursive + + - name: Setup Vulkan SDK + uses: humbletim/setup-vulkan-sdk@v1.2.0 + with: + vulkan-query-version: latest + vulkan-use-cache: true + vulkan-components: Vulkan-Headers, Vulkan-Loader, Glslang + + - name: Install bundle dependencies + run: | + brew install dylibbundler imagemagick + + - name: Install qt + run: brew install qt && which macdeployqt + + - name: Configure CMake + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DENABLE_USER_BUILD=ON -DENABLE_QT_GUI=ON + + - name: Build + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + + - name: Run bundle script + run: | + chmod +x .github/mac-bundle-qt.sh + ./.github/mac-bundle-qt.sh + + - name: Sign the App + run: codesign --force -s - -vvvv Alber.app + + - name: Zip it up + run: zip -r Alber Alber.app + + - name: Upload MacOS App + uses: actions/upload-artifact@v2 + with: + name: MacOS Alber App Bundle + path: 'Alber.zip' + + Linux: + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v2 + - name: Fetch submodules + run: git submodule update --init --recursive + + - name: Install misc packages + run: | + sudo apt-get update && sudo apt install libx11-dev libgl1-mesa-glx mesa-common-dev libfuse2 + sudo add-apt-repository ppa:okirby/qt6-backports + sudo apt update + sudo apt install qt6-base-dev + + - name: Install newer Clang + run: | + wget https://apt.llvm.org/llvm.sh + chmod +x ./llvm.sh + sudo ./llvm.sh 16 + + - name: Install newer CMake + run: | + sudo curl -s https://apt.kitware.com/keys/kitware-archive-latest.asc | gpg --dearmor | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null + sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 42D5A192B819C5DA + sudo add-apt-repository -y 'deb https://apt.kitware.com/ubuntu/ focal main' + sudo apt-get update + sudo apt-get install cmake + + - name: Setup Vulkan SDK + run: | + wget -qO - http://packages.lunarg.com/lunarg-signing-key-pub.asc | sudo apt-key add - + sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan-focal.list http://packages.lunarg.com/vulkan/lunarg-vulkan-focal.list + sudo apt update + sudo apt install vulkan-sdk + + - name: Configure CMake + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang-16 -DCMAKE_CXX_COMPILER=clang++-16 -DENABLE_USER_BUILD=ON -DENABLE_QT_GUI=ON + + - name: Build + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + + - name: Run AppImage packaging script + run: | + chmod +x .github/linux-appimage-qt.sh + ./.github/linux-appimage-qt.sh + + - name: Upload executable + uses: actions/upload-artifact@v2 + with: + name: Linux executable + path: './Alber-x86_64.AppImage' diff --git a/CMakeLists.txt b/CMakeLists.txt index a9688fe3..7c0d22c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ option(ENABLE_USER_BUILD "Make a user-facing build. These builds have various as option(ENABLE_HTTP_SERVER "Enable HTTP server. Used for Discord bot support" OFF) option(ENABLE_DISCORD_RPC "Compile with Discord RPC support (disabled by default)" ON) option(ENABLE_LUAJIT "Enable scripting with the Lua programming language" ON) +option(ENABLE_QT_GUI "Enable the Qt GUI. If not selected then the emulator uses a minimal SDL-based UI instead" OFF) include_directories(${PROJECT_SOURCE_DIR}/include/) include_directories(${PROJECT_SOURCE_DIR}/include/kernel) @@ -62,6 +63,15 @@ if(ENABLE_DISCORD_RPC AND NOT ANDROID) include_directories(third_party/discord-rpc/include) endif() +if (ENABLE_QT_GUI) + find_package(Qt6 REQUIRED COMPONENTS Widgets) + + # We can't use qt_standard_project_setup since it's Qt 6.3+ and we don't need to set the minimum that high + set(CMAKE_AUTOMOC ON) + set(CMAKE_AUTORCC ON) + set(CMAKE_AUTOUIC ON) +endif() + set(SDL_STATIC ON CACHE BOOL "" FORCE) set(SDL_SHARED OFF CACHE BOOL "" FORCE) set(SDL_TEST OFF CACHE BOOL "" FORCE) @@ -196,7 +206,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/applets/applet.hpp include/applets/mii_selector.hpp include/math_util.hpp include/services/soc.hpp include/services/news_u.hpp include/applets/software_keyboard.hpp include/applets/applet_manager.hpp include/fs/archive_user_save_data.hpp include/services/amiibo_device.hpp include/services/nfc_types.hpp include/swap.hpp include/services/csnd.hpp include/services/nwm_uds.hpp - include/fs/archive_system_save_data.hpp include/lua.hpp + include/fs/archive_system_save_data.hpp include/lua_manager.hpp ) cmrc_add_resource_library( @@ -362,6 +372,13 @@ if(ENABLE_VULKAN) target_link_libraries(Alber PRIVATE Vulkan::Vulkan resources_renderer_vk) endif() +if(ENABLE_QT_GUI) + target_compile_definitions(Alber PUBLIC "PANDA3DS_FRONTEND_QT=1") + target_link_libraries(Alber PRIVATE Qt6::Widgets) +else() + target_compile_definitions(Alber PUBLIC "PANDA3DS_FRONTEND_SDL=1") +endif() + if(GPU_DEBUG_INFO) target_compile_definitions(Alber PRIVATE GPU_DEBUG_INFO=1) endif() diff --git a/include/emulator.hpp b/include/emulator.hpp index 25c0ed49..a00c7fe1 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -13,7 +13,7 @@ #include "crypto/aes_engine.hpp" #include "discord_rpc.hpp" #include "io_file.hpp" -#include "lua.hpp" +#include "lua_manager.hpp" #include "memory.hpp" #ifdef PANDA3DS_ENABLE_HTTP_SERVER diff --git a/include/lua.hpp b/include/lua_manager.hpp similarity index 100% rename from include/lua.hpp rename to include/lua_manager.hpp diff --git a/src/lua.cpp b/src/lua.cpp index bcaac3ab..729b6581 100644 --- a/src/lua.cpp +++ b/src/lua.cpp @@ -1,5 +1,5 @@ #ifdef PANDA3DS_ENABLE_LUA -#include "lua.hpp" +#include "lua_manager.hpp" void LuaManager::initialize() { L = luaL_newstate(); // Open Lua diff --git a/src/main.cpp b/src/main.cpp index 66a04b9e..19badf12 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,7 +1,14 @@ #include "emulator.hpp" +#ifdef PANDA3DS_FRONTEND_QT +#include +#endif int main(int argc, char *argv[]) { Emulator emu; +#ifdef PANDA3DS_FRONTEND_QT + QApplication app(argc, argv); + return app.exec(); +#endif emu.initGraphicsContext();