diff --git a/.gitignore b/.gitignore index bc7f4683..9b39b23e 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ mobile/dist/ # per-machine iOS bundle-id pin (see scripts/build_ios.sh) mobile/ios/bundle_id.local + +# Xbox UWP build output +/ports/uwp/build/ diff --git a/mobile/android/love/src/jni/SDL2/src/joystick/SDL_gamecontroller.c b/mobile/android/love/src/jni/SDL2/src/joystick/SDL_gamecontroller.c index 035176b3..04e17019 100644 --- a/mobile/android/love/src/jni/SDL2/src/joystick/SDL_gamecontroller.c +++ b/mobile/android/love/src/jni/SDL2/src/joystick/SDL_gamecontroller.c @@ -726,7 +726,12 @@ static ControllerMapping_t *SDL_CreateMappingForWGIController(SDL_JoystickGUID g } SDL_strlcpy(mapping_string, "none,*,", sizeof(mapping_string)); +#ifdef __WINRT__ + /* Xbox WGI exposes the canonical SDL axis and hat layout. */ + SDL_strlcat(mapping_string, "a:b0,b:b1,x:b2,y:b3,back:b6,start:b7,leftstick:b8,rightstick:b9,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,lefttrigger:a2,rightx:a3,righty:a4,righttrigger:a5,", sizeof(mapping_string)); +#else SDL_strlcat(mapping_string, "a:b0,b:b1,x:b2,y:b3,back:b6,start:b7,leftstick:b8,rightstick:b9,leftshoulder:b4,rightshoulder:b5,dpup:b10,dpdown:b12,dpleft:b13,dpright:b11,leftx:a1,lefty:a0~,rightx:a3,righty:a2~,lefttrigger:a4,righttrigger:a5,", sizeof(mapping_string)); +#endif return SDL_PrivateAddMappingForGUID(guid, mapping_string, &existing, SDL_CONTROLLER_MAPPING_PRIORITY_DEFAULT); } diff --git a/mobile/android/love/src/jni/SDL2/src/joystick/windows/SDL_windows_gaming_input.c b/mobile/android/love/src/jni/SDL2/src/joystick/windows/SDL_windows_gaming_input.c index 6f01b4e1..26b88116 100644 --- a/mobile/android/love/src/jni/SDL2/src/joystick/windows/SDL_windows_gaming_input.c +++ b/mobile/android/love/src/jni/SDL2/src/joystick/windows/SDL_windows_gaming_input.c @@ -731,10 +731,19 @@ static int WGI_JoystickOpen(SDL_Joystick *joystick, int device_index) __x_ABI_CWindows_CGaming_CInput_CIGameController_get_IsWireless(hwdata->gamecontroller, &wireless); } - /* Initialize the joystick capabilities */ - joystick->nbuttons = state->nbuttons; - joystick->naxes = state->naxes; - joystick->nhats = state->nhats; +#ifdef __WINRT__ + if (hwdata->gamepad) { + joystick->nbuttons = 10; + joystick->naxes = 6; + joystick->nhats = 1; + } else +#endif + { + /* Initialize the joystick capabilities */ + joystick->nbuttons = state->nbuttons; + joystick->naxes = state->naxes; + joystick->nhats = state->nhats; + } joystick->epowerlevel = wireless ? SDL_JOYSTICK_POWER_UNKNOWN : SDL_JOYSTICK_POWER_WIRED; if (wireless && hwdata->battery) { @@ -872,6 +881,51 @@ static void WGI_JoystickUpdate(SDL_Joystick *joystick) { struct joystick_hwdata *hwdata = joystick->hwdata; HRESULT hr; + +#ifdef __WINRT__ + if (hwdata->gamepad) { + struct __x_ABI_CWindows_CGaming_CInput_CGamepadReading reading; + static const __x_ABI_CWindows_CGaming_CInput_CGamepadButtons button_masks[] = { + GamepadButtons_A, GamepadButtons_B, GamepadButtons_X, GamepadButtons_Y, + GamepadButtons_LeftShoulder, GamepadButtons_RightShoulder, + GamepadButtons_View, GamepadButtons_Menu, + GamepadButtons_LeftThumbstick, GamepadButtons_RightThumbstick + }; + Uint8 hat = SDL_HAT_CENTERED; + Uint8 i; + + hr = __x_ABI_CWindows_CGaming_CInput_CIGamepad_GetCurrentReading(hwdata->gamepad, &reading); + if (SUCCEEDED(hr) && (!reading.Timestamp || reading.Timestamp != hwdata->timestamp)) { + const DOUBLE left_x = SDL_max(-1.0, SDL_min(1.0, reading.LeftThumbstickX)); + const DOUBLE left_y = SDL_max(-1.0, SDL_min(1.0, reading.LeftThumbstickY)); + const DOUBLE right_x = SDL_max(-1.0, SDL_min(1.0, reading.RightThumbstickX)); + const DOUBLE right_y = SDL_max(-1.0, SDL_min(1.0, reading.RightThumbstickY)); + const DOUBLE left_trigger = SDL_max(0.0, SDL_min(1.0, reading.LeftTrigger)); + const DOUBLE right_trigger = SDL_max(0.0, SDL_min(1.0, reading.RightTrigger)); + + SDL_PrivateJoystickAxis(joystick, 0, (Sint16)(left_x * 32767.0)); + SDL_PrivateJoystickAxis(joystick, 1, (Sint16)(-left_y * 32767.0)); + SDL_PrivateJoystickAxis(joystick, 2, (Sint16)(left_trigger * 65535.0 - 32768.0)); + SDL_PrivateJoystickAxis(joystick, 3, (Sint16)(right_x * 32767.0)); + SDL_PrivateJoystickAxis(joystick, 4, (Sint16)(-right_y * 32767.0)); + SDL_PrivateJoystickAxis(joystick, 5, (Sint16)(right_trigger * 65535.0 - 32768.0)); + + for (i = 0; i < (Uint8)SDL_arraysize(button_masks); ++i) { + SDL_PrivateJoystickButton(joystick, i, + (reading.Buttons & button_masks[i]) ? SDL_PRESSED : SDL_RELEASED); + } + + if (reading.Buttons & GamepadButtons_DPadUp) hat |= SDL_HAT_UP; + if (reading.Buttons & GamepadButtons_DPadDown) hat |= SDL_HAT_DOWN; + if (reading.Buttons & GamepadButtons_DPadLeft) hat |= SDL_HAT_LEFT; + if (reading.Buttons & GamepadButtons_DPadRight) hat |= SDL_HAT_RIGHT; + SDL_PrivateJoystickHat(joystick, 0, hat); + hwdata->timestamp = reading.Timestamp; + } + return; + } +#endif + UINT32 nbuttons = SDL_min(joystick->nbuttons, SDL_MAX_UINT8); boolean *buttons = NULL; UINT32 nhats = SDL_min(joystick->nhats, SDL_MAX_UINT8); @@ -994,7 +1048,38 @@ static void WGI_JoystickQuit(void) static SDL_bool WGI_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping *out) { +#ifdef __WINRT__ + WindowsGamingInputControllerState *state = &wgi.controllers[device_index]; + + if (state->type != SDL_JOYSTICK_TYPE_GAMECONTROLLER) { + return SDL_FALSE; + } + + SDL_zero(*out); + out->a = (SDL_InputMapping){ EMappingKind_Button, 0 }; + out->b = (SDL_InputMapping){ EMappingKind_Button, 1 }; + out->x = (SDL_InputMapping){ EMappingKind_Button, 2 }; + out->y = (SDL_InputMapping){ EMappingKind_Button, 3 }; + out->leftshoulder = (SDL_InputMapping){ EMappingKind_Button, 4 }; + out->rightshoulder = (SDL_InputMapping){ EMappingKind_Button, 5 }; + out->back = (SDL_InputMapping){ EMappingKind_Button, 6 }; + out->start = (SDL_InputMapping){ EMappingKind_Button, 7 }; + out->leftstick = (SDL_InputMapping){ EMappingKind_Button, 8 }; + out->rightstick = (SDL_InputMapping){ EMappingKind_Button, 9 }; + out->dpup = (SDL_InputMapping){ EMappingKind_Hat, SDL_HAT_UP }; + out->dpdown = (SDL_InputMapping){ EMappingKind_Hat, SDL_HAT_DOWN }; + out->dpleft = (SDL_InputMapping){ EMappingKind_Hat, SDL_HAT_LEFT }; + out->dpright = (SDL_InputMapping){ EMappingKind_Hat, SDL_HAT_RIGHT }; + out->leftx = (SDL_InputMapping){ EMappingKind_Axis, 0 }; + out->lefty = (SDL_InputMapping){ EMappingKind_Axis, 1 }; + out->lefttrigger = (SDL_InputMapping){ EMappingKind_Axis, 2 }; + out->rightx = (SDL_InputMapping){ EMappingKind_Axis, 3 }; + out->righty = (SDL_InputMapping){ EMappingKind_Axis, 4 }; + out->righttrigger = (SDL_InputMapping){ EMappingKind_Axis, 5 }; + return SDL_TRUE; +#else return SDL_FALSE; +#endif } SDL_JoystickDriver SDL_WGI_JoystickDriver = { diff --git a/ports/uwp/Assets/LargeTile.png b/ports/uwp/Assets/LargeTile.png new file mode 100644 index 00000000..48e69064 Binary files /dev/null and b/ports/uwp/Assets/LargeTile.png differ diff --git a/ports/uwp/Assets/SmallTile.png b/ports/uwp/Assets/SmallTile.png new file mode 100644 index 00000000..4a7714ab Binary files /dev/null and b/ports/uwp/Assets/SmallTile.png differ diff --git a/ports/uwp/Assets/SplashScreen.png b/ports/uwp/Assets/SplashScreen.png new file mode 100644 index 00000000..5e938064 Binary files /dev/null and b/ports/uwp/Assets/SplashScreen.png differ diff --git a/ports/uwp/Assets/Square150x150Logo.png b/ports/uwp/Assets/Square150x150Logo.png new file mode 100644 index 00000000..39a868dd Binary files /dev/null and b/ports/uwp/Assets/Square150x150Logo.png differ diff --git a/ports/uwp/Assets/Square44x44Logo.png b/ports/uwp/Assets/Square44x44Logo.png new file mode 100644 index 00000000..b99245bd Binary files /dev/null and b/ports/uwp/Assets/Square44x44Logo.png differ diff --git a/ports/uwp/Assets/StoreLogo.png b/ports/uwp/Assets/StoreLogo.png new file mode 100644 index 00000000..29e1785a Binary files /dev/null and b/ports/uwp/Assets/StoreLogo.png differ diff --git a/ports/uwp/Assets/WideTile.png b/ports/uwp/Assets/WideTile.png new file mode 100644 index 00000000..8f30a761 Binary files /dev/null and b/ports/uwp/Assets/WideTile.png differ diff --git a/ports/uwp/BUILD.md b/ports/uwp/BUILD.md new file mode 100644 index 00000000..59a9c58f --- /dev/null +++ b/ports/uwp/BUILD.md @@ -0,0 +1,112 @@ +# Gen1Recomp Xbox UWP build notes + +This is the Xbox Dev Mode package for Gen1Recomp. + +The rough shape is: + +- `Gen1RecompUWP.exe` starts LÖVE through SDL's WinRT wrapper +- [love-xbox-uwp](https://github.com/caorthann-celt/love-xbox-uwp) provides the LÖVE 11.5 UWP backend and LuaJIT +- SDL2 is built from the copy already carried by Gen1Recomp +- ANGLE provides OpenGL ES over D3D11 +- vcpkg provides the audio, font, video, and compression libraries + +## What You Need + +The tested toolchain is: + +- Visual Studio 2022 17.14 +- MSVC v143 x64/x86 build tools +- C++ Universal Windows Platform tools +- Windows 11 SDK `10.0.26100.0` +- CMake 3.24 or newer +- Git +- vcpkg with the `x64-uwp` triplet + +Use Visual Studio Installer to add **Universal Windows Platform development**, the v143 C++ tools, CMake tools for Windows, and Windows SDK `10.0.26100.0`. + +## Checkouts + +Keep the game, LÖVE backend, and vcpkg in separate folders. From the parent of the existing Gen1Recomp checkout: + +```powershell +git clone --branch gen1recomp --single-branch ` + https://github.com/caorthann-celt/love-xbox-uwp.git ` + love-xbox-uwp +git clone https://github.com/microsoft/vcpkg.git vcpkg +``` + +The LÖVE checkout must stay on the `gen1recomp` branch. + +## vcpkg + +Bootstrap vcpkg and install the UWP libraries used by LÖVE: + +```powershell +Set-Location vcpkg +./bootstrap-vcpkg.bat +./vcpkg.exe install ` + freetype:x64-uwp ` + openal-soft:x64-uwp ` + libtheora:x64-uwp ` + libvorbis:x64-uwp ` + libogg:x64-uwp ` + zlib:x64-uwp +Set-Location .. +``` + +## Environment + +Set the three build roots from the parent folder: + +```powershell +$env:LOVE_UWP_ROOT = (Resolve-Path './love-xbox-uwp').Path +$env:VCPKG_ROOT = (Resolve-Path './vcpkg').Path +$gameRoot = (Resolve-Path './Gen1Recomp-UWP').Path +``` + +## Build SDL2 + +Start from a Visual Studio 2022 UWP developer prompt, or initialize `VsDevCmd.bat` for x64 UWP. Then build and install SDL2: + +```powershell +Set-Location "$gameRoot\ports\uwp" +./scripts/build-sdl2-angle.ps1 +$env:UWP_SDL2_ANGLE_ROOT = (Resolve-Path './build/sdl2-install').Path +``` + +## Build LÖVE + +Configure the backend once, then build the Release libraries and DLLs: + +```powershell +cmake -S $env:LOVE_UWP_ROOT -B "$env:LOVE_UWP_ROOT/build/uwp-x64-angle" ` + -G "Visual Studio 17 2022" -A x64 ` + -DCMAKE_SYSTEM_NAME=WindowsStore -DCMAKE_SYSTEM_VERSION=10.0 ` + "-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" ` + -DVCPKG_TARGET_TRIPLET=x64-uwp ` + "-DLOVE_UWP_SDL_ROOT=$env:UWP_SDL2_ANGLE_ROOT" ` + -DLOVE_UWP_LUAJIT=ON -DLOVE_UWP_ANGLE=ON + +cmake --build "$env:LOVE_UWP_ROOT/build/uwp-x64-angle" ` + --config Release --parallel +``` + +## Build the MSIX + +Build the game package from `ports/uwp`: + +```powershell +Set-Location "$gameRoot\ports\uwp" +cmake --preset uwp-release +cmake --build --preset uwp-release +``` + +The build creates `gen1recomp.love`, links the UWP host, and stages LÖVE, LuaJIT, SDL2, ANGLE, and the vcpkg runtime DLLs. + +## Build Output + +The Release package lands under: + +```text +ports\uwp\build\release\AppPackages\Gen1RecompUWP +``` diff --git a/ports/uwp/CMakeLists.txt b/ports/uwp/CMakeLists.txt new file mode 100644 index 00000000..033e2e0b --- /dev/null +++ b/ports/uwp/CMakeLists.txt @@ -0,0 +1,110 @@ +cmake_minimum_required(VERSION 3.24) +project(Gen1RecompUWP LANGUAGES CXX) + +if(NOT CMAKE_SYSTEM_NAME STREQUAL "WindowsStore") + message(FATAL_ERROR "Configure with a WindowsStore preset.") +endif() + +foreach(required_env LOVE_UWP_ROOT UWP_SDL2_ANGLE_ROOT VCPKG_ROOT) + if(NOT DEFINED ENV{${required_env}} OR "$ENV{${required_env}}" STREQUAL "") + message(FATAL_ERROR "Set ${required_env} before configuring.") + endif() +endforeach() + +file(TO_CMAKE_PATH "$ENV{LOVE_UWP_ROOT}" LOVE_UWP_ROOT) +file(TO_CMAKE_PATH "$ENV{UWP_SDL2_ANGLE_ROOT}" UWP_SDL2_ANGLE_ROOT) +file(TO_CMAKE_PATH "$ENV{VCPKG_ROOT}" VCPKG_ROOT) +get_filename_component(GAME_ROOT "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) +set(ANGLE_RUNTIME_ROOT "${CMAKE_CURRENT_LIST_DIR}/third_party/angle") + +set(LOVE_BUILD_ROOT "${LOVE_UWP_ROOT}/build/uwp-x64-angle" CACHE PATH "LÖVE UWP build directory") +set(LOVE_CONFIG "RelWithDebInfo" CACHE STRING "Configuration used for the LÖVE UWP backend") +set(LOVE_OUTPUT_ROOT "${LOVE_BUILD_ROOT}/${LOVE_CONFIG}") +if(LOVE_CONFIG STREQUAL "Release" OR LOVE_CONFIG STREQUAL "Debug") + set(LOVE_LIBRARY "${LOVE_OUTPUT_ROOT}/liblove.lib") + set(LOVE_RUNTIME "${LOVE_OUTPUT_ROOT}/love.dll") +else() + set(LOVE_LIBRARY "${LOVE_OUTPUT_ROOT}/libliblove.lib") + set(LOVE_RUNTIME "${LOVE_OUTPUT_ROOT}/liblove.dll") +endif() + +set(REQUIRED_FILES + "${LOVE_OUTPUT_ROOT}/lovestatic.lib" + "${LOVE_LIBRARY}" + "${LOVE_RUNTIME}" + "${LOVE_OUTPUT_ROOT}/lua51.lib" + "${LOVE_OUTPUT_ROOT}/lua51.dll" + "${UWP_SDL2_ANGLE_ROOT}/include/SDL2/SDL.h" + "${UWP_SDL2_ANGLE_ROOT}/lib/SDL2.lib" + "${UWP_SDL2_ANGLE_ROOT}/bin/SDL2.dll" + "${ANGLE_RUNTIME_ROOT}/libEGL.dll" + "${ANGLE_RUNTIME_ROOT}/libGLESv2.dll" + "${ANGLE_RUNTIME_ROOT}/d3dcompiler_47.dll" +) +foreach(path IN LISTS REQUIRED_FILES) + if(NOT EXISTS "${path}") + message(FATAL_ERROR "Missing UWP dependency: ${path}") + endif() +endforeach() + +set(GAME_ARCHIVE "${CMAKE_CURRENT_BINARY_DIR}/gen1recomp.love") +set_source_files_properties("${GAME_ARCHIVE}" PROPERTIES GENERATED TRUE) +add_custom_target(gen1recomp_love ALL + COMMAND powershell -NoProfile -ExecutionPolicy Bypass -File + "${CMAKE_CURRENT_LIST_DIR}/scripts/package-game.ps1" + -SourceRoot "${GAME_ROOT}" -Output "${GAME_ARCHIVE}" + BYPRODUCTS "${GAME_ARCHIVE}" + VERBATIM +) + +add_executable(${PROJECT_NAME} WIN32 "app/main.cpp") +add_dependencies(${PROJECT_NAME} gen1recomp_love) +set_target_properties(${PROJECT_NAME} PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED YES + VS_SDK_REFERENCES "Microsoft.VCLibs, Version=14.0" +) +target_include_directories(${PROJECT_NAME} PRIVATE "${UWP_SDL2_ANGLE_ROOT}/include/SDL2") +target_link_libraries(${PROJECT_NAME} PRIVATE + "${LOVE_OUTPUT_ROOT}/lovestatic.lib" + "${LOVE_LIBRARY}" + "${UWP_SDL2_ANGLE_ROOT}/lib/SDL2.lib" + "${LOVE_OUTPUT_ROOT}/lua51.lib" + WindowsApp.lib +) + +set(PACKAGE_ROOT_FILES + "${CMAKE_CURRENT_LIST_DIR}/Package.appxmanifest" + "${GAME_ARCHIVE}" + "${LOVE_RUNTIME}" + "${LOVE_OUTPUT_ROOT}/lua51.dll" + "${UWP_SDL2_ANGLE_ROOT}/bin/SDL2.dll" + "${ANGLE_RUNTIME_ROOT}/libEGL.dll" + "${ANGLE_RUNTIME_ROOT}/libGLESv2.dll" + "${ANGLE_RUNTIME_ROOT}/d3dcompiler_47.dll" +) + +set(VCPKG_RUNTIME_NAMES + brotlicommon.dll brotlidec.dll bz2.dll fmt.dll freetype.dll libpng16.dll + OpenAL32.dll theora.dll theoradec.dll vorbis.dll vorbisfile.dll ogg.dll z.dll +) +foreach(name IN LISTS VCPKG_RUNTIME_NAMES) + set(runtime "${VCPKG_ROOT}/installed/x64-uwp/bin/${name}") + if(NOT EXISTS "${runtime}") + message(FATAL_ERROR "Missing UWP runtime DLL: ${name}") + endif() + list(APPEND PACKAGE_ROOT_FILES "${runtime}") +endforeach() + +set_source_files_properties(${PACKAGE_ROOT_FILES} PROPERTIES + VS_COPY_TO_OUT_DIR Always + VS_DEPLOYMENT_CONTENT TRUE + VS_DEPLOYMENT_LOCATION "." +) + +file(GLOB PACKAGE_ASSETS CONFIGURE_DEPENDS "${CMAKE_CURRENT_LIST_DIR}/Assets/*.png") +set_source_files_properties(${PACKAGE_ASSETS} PROPERTIES + VS_DEPLOYMENT_CONTENT TRUE + VS_DEPLOYMENT_LOCATION "Assets" +) +target_sources(${PROJECT_NAME} PRIVATE ${PACKAGE_ROOT_FILES} ${PACKAGE_ASSETS}) diff --git a/ports/uwp/CMakePresets.json b/ports/uwp/CMakePresets.json new file mode 100644 index 00000000..977d4591 --- /dev/null +++ b/ports/uwp/CMakePresets.json @@ -0,0 +1,49 @@ +{ + "version": 6, + "configurePresets": [ + { + "name": "uwp-common", + "hidden": true, + "generator": "Visual Studio 17 2022", + "architecture": "x64", + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "WindowsStore", + "CMAKE_SYSTEM_VERSION": "10.0", + "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "VCPKG_TARGET_TRIPLET": "x64-uwp" + } + }, + { + "name": "uwp-relwithdebinfo", + "inherits": "uwp-common", + "displayName": "Gen1Recomp Xbox UWP (RelWithDebInfo)", + "binaryDir": "${sourceDir}/build/relwithdebinfo", + "cacheVariables": { + "LOVE_CONFIG": "RelWithDebInfo" + } + }, + { + "name": "uwp-release", + "inherits": "uwp-common", + "displayName": "Gen1Recomp Xbox UWP (Release)", + "binaryDir": "${sourceDir}/build/release", + "cacheVariables": { + "LOVE_CONFIG": "Release" + } + } + ], + "buildPresets": [ + { + "name": "uwp-relwithdebinfo", + "configurePreset": "uwp-relwithdebinfo", + "configuration": "RelWithDebInfo", + "jobs": 8 + }, + { + "name": "uwp-release", + "configurePreset": "uwp-release", + "configuration": "Release", + "jobs": 8 + } + ] +} diff --git a/ports/uwp/Package.appxmanifest b/ports/uwp/Package.appxmanifest new file mode 100644 index 00000000..70a92d93 --- /dev/null +++ b/ports/uwp/Package.appxmanifest @@ -0,0 +1,46 @@ + + + + + + Gen1Recomp + Gen1Recomp + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ports/uwp/app/main.cpp b/ports/uwp/app/main.cpp new file mode 100644 index 00000000..e6015722 --- /dev/null +++ b/ports/uwp/app/main.cpp @@ -0,0 +1,31 @@ +#include +#include +#include +#include +#include + +extern "C" int SDL_main(int argc, char **argv); + +namespace +{ + +int runLove(int, char **) +{ + std::wstring packagePath = winrt::Windows::ApplicationModel::Package::Current() + .InstalledLocation().Path().c_str(); + std::string gamePath = winrt::to_string(packagePath + L"\\gen1recomp.love"); + + char executable[] = "Gen1RecompUWP"; + char fused[] = "--fused"; + char *loveArgv[] = {executable, gamePath.data(), fused, nullptr}; + return SDL_main(3, loveArgv); +} + +} // namespace + +int CALLBACK WinMain(HINSTANCE, HINSTANCE, LPSTR, int) +{ + SDL_SetHint(SDL_HINT_WINRT_HANDLE_BACK_BUTTON, "1"); + OutputDebugStringA("Gen1Recomp UWP startup\n"); + return SDL_WinRTRunApp(runLove, nullptr); +} diff --git a/ports/uwp/scripts/build-sdl2-angle.ps1 b/ports/uwp/scripts/build-sdl2-angle.ps1 new file mode 100644 index 00000000..60bf2740 --- /dev/null +++ b/ports/uwp/scripts/build-sdl2-angle.ps1 @@ -0,0 +1,24 @@ +param( + [string]$Configuration = "Release", + [string]$WindowsSdkVersion = "10.0" +) + +$ErrorActionPreference = "Stop" + +$portRoot = Split-Path -Parent $PSScriptRoot +$gameRoot = (Resolve-Path (Join-Path $portRoot "..\..")).Path +$sourceRoot = Join-Path $gameRoot "mobile\android\love\src\jni\SDL2" +$buildRoot = Join-Path $portRoot "build\sdl2" +$installRoot = Join-Path $portRoot "build\sdl2-install" + +cmake -S $sourceRoot -B $buildRoot ` + -G "Visual Studio 17 2022" -A x64 ` + -DCMAKE_SYSTEM_NAME=WindowsStore ` + "-DCMAKE_SYSTEM_VERSION=$WindowsSdkVersion" ` + "-DCMAKE_INSTALL_PREFIX=$installRoot" ` + -DSDL_SHARED=ON -DSDL_STATIC=OFF ` + -DSDL_OPENGL=OFF -DSDL_OPENGLES=ON -DSDL_VULKAN=OFF +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +cmake --build $buildRoot --config $Configuration --target INSTALL --parallel +exit $LASTEXITCODE diff --git a/ports/uwp/scripts/package-game.ps1 b/ports/uwp/scripts/package-game.ps1 new file mode 100644 index 00000000..d7f4d6ea --- /dev/null +++ b/ports/uwp/scripts/package-game.ps1 @@ -0,0 +1,49 @@ +param( + [Parameter(Mandatory = $true)][string]$SourceRoot, + [Parameter(Mandatory = $true)][string]$Output +) + +$ErrorActionPreference = 'Stop' +$source = (Resolve-Path -LiteralPath $SourceRoot).Path +$required = @('main.lua', 'conf.lua', 'tools/rom_manifest.json') +$allowedRoots = @('assets/', 'data/', 'mods/', 'src/', 'tools/rom_manifest') +$forbidden = '(?i)(^|/)(assets/generated|data/generated|build|dist|ports|mobile|tests?)(/|$)|\.(gb|gbc|sav|srm)$' + +$tracked = & git -C $source ls-files +if ($LASTEXITCODE -ne 0) { throw 'git ls-files failed' } + +$entries = $tracked | + ForEach-Object { $_.Replace('\', '/') } | + Where-Object { + $path = $_ + (($required -contains $path) -or ($allowedRoots | Where-Object { $path.StartsWith($_) })) -and + $path -notmatch $forbidden + } | + Sort-Object -Unique + +foreach ($path in $required) { + if ($entries -notcontains $path) { throw "Required game file is missing from archive input: $path" } +} + +$parent = Split-Path -Parent $Output +New-Item -ItemType Directory -Force -Path $parent | Out-Null +if (Test-Path -LiteralPath $Output) { Remove-Item -LiteralPath $Output -Force } + +Add-Type -AssemblyName System.IO.Compression +Add-Type -AssemblyName System.IO.Compression.FileSystem +$archive = [System.IO.Compression.ZipFile]::Open($Output, 'Create') +try { + foreach ($relative in $entries) { + $full = Join-Path $source $relative + if (-not (Test-Path -LiteralPath $full -PathType Leaf)) { throw "Tracked file is missing: $relative" } + $entry = $archive.CreateEntry($relative, [System.IO.Compression.CompressionLevel]::Optimal) + $entry.LastWriteTime = [DateTimeOffset]'2000-01-01T00:00:00Z' + $input = [System.IO.File]::OpenRead($full) + $outputStream = $entry.Open() + try { $input.CopyTo($outputStream) } finally { $outputStream.Dispose(); $input.Dispose() } + } +} finally { + $archive.Dispose() +} + +Write-Host "Created $Output with $($entries.Count) tracked, ROM-free entries." diff --git a/ports/uwp/third_party/README.md b/ports/uwp/third_party/README.md new file mode 100644 index 00000000..a174cbec --- /dev/null +++ b/ports/uwp/third_party/README.md @@ -0,0 +1,9 @@ +# UWP dependencies + +The ANGLE UWP runtime is committed under `angle`. The package also uses: + +- `LOVE_UWP_ROOT`: [caorthann-celt/love-xbox-uwp](https://github.com/caorthann-celt/love-xbox-uwp) on the `gen1recomp` branch. +- `UWP_SDL2_ANGLE_ROOT`: SDL2 built by `scripts/build-sdl2-angle.ps1`. +- `VCPKG_ROOT`: the UWP codec, font, compression, and audio runtimes used by LÖVE. + +SDL headers, import library, and DLL must come from the same build. Runtime DLLs stay inside the installed package. diff --git a/ports/uwp/third_party/angle/AUTHORS b/ports/uwp/third_party/angle/AUTHORS new file mode 100644 index 00000000..37a9c07d --- /dev/null +++ b/ports/uwp/third_party/angle/AUTHORS @@ -0,0 +1,89 @@ +# This is the official list of The ANGLE Project Authors +# for copyright purposes. +# This file is distinct from the CONTRIBUTORS files. +# See the latter for an explanation. + +# Names should be added to this file as +# Name or Organization +# Email addresses for individuals are tracked elsewhere to avoid spam. + +Google Inc. +TransGaming Inc. +3DLabs Inc. Ltd. + +Adobe Systems Inc. +Autodesk, Inc. +BlackBerry Limited +Cable Television Laboratories, Inc. +Collabora, Ltd. +Cloud Party, Inc. +Igalia, S.L. +Imagination Technologies Ltd. +Intel Corporation +LunarG, Inc. +Mozilla Corporation +Turbulenz +Klarälvdalens Datakonsult AB +Microsoft Corporation +Microsoft Open Technologies, Inc. +NVIDIA Corporation +Opera Software ASA +The Qt Company Ltd. +Advanced Micro Devices, Inc. +LG Electronics, Inc. +IBM Inc. +AdaptVis GmbH +Samsung Electronics, Inc. +Arm Ltd. +Broadcom Inc. +Facebook, Inc. +The Khronos Group, Inc. +Numfum GmbH +Yandex LLC +Rive +Institute of Software, Chinese Academy of Sciences +Guangdong OPPO Mobile Telecommunications Corp., Ltd +Qualcomm Innovation Center, Inc. + +Jacek Caban +Mark Callow +Ginn Chen +Tibor den Ouden +Régis Fénéon +James Hauxwell +Sam Hocevar +Pierre Leveille +Jonathan Liu +Boying Lu +Aitor Moreno +Yuri O'Donnell +Josh Soref +Ma Aiguo +Maks Naumov +Jinyoung Hur +Sebastian Bergstein +James Ross-Gowan +Nickolay Artamonov +Ihsan Akmal +Andrei Volykhin +Jérôme Duval +Руслан Ижбулатов +Thomas Miller +Till Rathmann +Nick Shaforostov +Jaime Bernardo +Le Hoang Quyen +Lu Yahan +Ethan Lee +Renaud Lepage +Artem Bolgar +Wander Lairson Costa +Stephan Hartmann +SeongHwan Park +Xiaopeng Li +Akihiko Odaki +Ho Cheung +Tao Wang +Phan Quang Minh +Hongchen Yan +Andrew Sumsion diff --git a/ports/uwp/third_party/angle/LICENSE b/ports/uwp/third_party/angle/LICENSE new file mode 100644 index 00000000..0f65fd60 --- /dev/null +++ b/ports/uwp/third_party/angle/LICENSE @@ -0,0 +1,32 @@ +// Copyright 2018 The ANGLE Project Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// +// Neither the name of TransGaming Inc., Google Inc., 3DLabs Inc. +// Ltd., nor the names of their contributors may be used to endorse +// or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +// COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. diff --git a/ports/uwp/third_party/angle/README.md b/ports/uwp/third_party/angle/README.md new file mode 100644 index 00000000..825e2e10 --- /dev/null +++ b/ports/uwp/third_party/angle/README.md @@ -0,0 +1,13 @@ +# ANGLE UWP runtime + +`libEGL.dll` and `libGLESv2.dll` were built for x64 UWP from [SternXD/angle](https://github.com/SternXD/angle) commit `45b0b1e03400b7a10aaa9a077e196d1abcddafce`. They report ANGLE version `2.1.25011` and source hash `45b0b1e03400`. + +`d3dcompiler_47.dll` is the x64 Direct3D HLSL compiler redistributable from Windows SDK `10.0.26100.7705`. + +SHA-256: + +- `d3dcompiler_47.dll`: `A05F99734F7C4822FEFC12B367AF21FD0976ED6608752FB1E1E80B6ECE7ECBBB` +- `libEGL.dll`: `FE2AC107D0B6CAAE917E755064B430E4ECC52495B12E44EC7B0218E10504ECD1` +- `libGLESv2.dll`: `C8A7E08E0C34E534A781DD8784065CC0608D6E497ADB451F6EEF348E594A00CF` + +ANGLE's upstream `LICENSE` and `AUTHORS` files are included beside its binaries. diff --git a/ports/uwp/third_party/angle/d3dcompiler_47.dll b/ports/uwp/third_party/angle/d3dcompiler_47.dll new file mode 100644 index 00000000..948cc90b Binary files /dev/null and b/ports/uwp/third_party/angle/d3dcompiler_47.dll differ diff --git a/ports/uwp/third_party/angle/libEGL.dll b/ports/uwp/third_party/angle/libEGL.dll new file mode 100644 index 00000000..620c3b1f Binary files /dev/null and b/ports/uwp/third_party/angle/libEGL.dll differ diff --git a/ports/uwp/third_party/angle/libGLESv2.dll b/ports/uwp/third_party/angle/libGLESv2.dll new file mode 100644 index 00000000..dec1134d Binary files /dev/null and b/ports/uwp/third_party/angle/libGLESv2.dll differ diff --git a/src/core/Music.lua b/src/core/Music.lua index b5d54908..183443c9 100644 --- a/src/core/Music.lua +++ b/src/core/Music.lua @@ -259,7 +259,8 @@ function Music.play(data, song, loop, ctx) pcall(loopSrc.setLooping, loopSrc, wantLoop) applyVolume(loopSrc) applyFilter(loopSrc) - else + elseif not isChip then + -- ChipAudio owns the loop policy for its queueable source. pcall(src.setLooping, src, wantLoop) end applyVolume(src) diff --git a/src/import/CacheFs.lua b/src/import/CacheFs.lua index 1f19dc66..342e0e1f 100644 --- a/src/import/CacheFs.lua +++ b/src/import/CacheFs.lua @@ -31,6 +31,11 @@ local CacheFs = {} local SEP = package.config:sub(1, 1) +local function isUWP() + return love and love.system and love.system.getOS + and love.system.getOS() == "UWP" +end + -- Cache-relative paths are prefixed with this before every read/write, so a -- Blue/Yellow import lands under its GameVersion.cachePrefix (blue/, yellow/) -- while a Red import keeps the historical root. The launcher sets it per @@ -52,6 +57,7 @@ local mkdirFn = nil local function resolveMkdir() if mkdirFn ~= nil then return mkdirFn end mkdirFn = false + if isUWP() then return mkdirFn end local ok, ffi = pcall(require, "ffi") if not ok then return mkdirFn end if ffi.os == "Windows" then @@ -83,6 +89,7 @@ local rmdirFn = nil local function resolveRmdir() if rmdirFn ~= nil then return rmdirFn end rmdirFn = false + if isUWP() then return rmdirFn end local ok, ffi = pcall(require, "ffi") if not ok then return rmdirFn end if ffi.os == "Windows" then diff --git a/src/import/RomImporter.lua b/src/import/RomImporter.lua index 5409a371..be01ee22 100644 --- a/src/import/RomImporter.lua +++ b/src/import/RomImporter.lua @@ -8,6 +8,11 @@ local SafeArea = require("src.core.SafeArea") local RomImporter = {} RomImporter.__index = RomImporter +local function isUWP() + return love and love.system and love.system.getOS + and love.system.getOS() == "UWP" +end + -- love.system.pickFile is a NATIVE BRIDGE, not part of LÖVE: it exists only on -- builds that compiled one (Android, and iOS builds patched by -- mobile/ios/patch_love_src.py). A build without it must fall back to the @@ -990,6 +995,7 @@ end -- import-only run all skip the release check so headless and CI runs never spin -- up the background worker or reach out to the network. local function updaterAllowed() + if isUWP() then return false end if not Platform.networkValidated() then return false end if not (love.filesystem.isFused and love.filesystem.isFused()) then return false end if os.getenv("POKEPORT_AUTOPILOT") or os.getenv("POKEPORT_DRIVER") then return false end @@ -1034,6 +1040,7 @@ function RomImporter.new(onComplete, opts) mobileFileBridge = mobileFileBridge, android = android, ios = mobileOS == "iOS", + nativePicker = romImportMode == "native-picker", -- One startup poll pass on both mobiles. iOS: files dropped through the -- Files app are swept into the save dir before Lua boots (GRBootstrap) with -- no love.focus event necessarily following. Android: the SAF picker is a @@ -1493,7 +1500,7 @@ function RomImporter:chooseMod() self:rescanModsAction() return end - if self.ios and love.system.getPickedFile then + if self.nativePicker and love.system.getPickedFile then self.iosPendingKind = "mod" if not pickFile("mod") then self.iosPendingKind = nil @@ -1569,7 +1576,7 @@ function RomImporter:chooseSaveImport(version) self:rescanSavesAction(version) return end - if self.ios and love.system.getPickedFile then + if self.nativePicker and love.system.getPickedFile then self.iosPendingKind = "sav" self.iosPendingVersion = version if not pickFile("sav") then @@ -1687,7 +1694,7 @@ function RomImporter:choose(version) self:rescanAction(self.chooseVersion) return end - if self.ios and love.system.getPickedFile then + if self.nativePicker and love.system.getPickedFile then self.iosPendingKind = "rom" if not pickFile("rom") then self.iosPendingKind = nil @@ -1889,7 +1896,7 @@ function RomImporter:update(dt) end) end end - if self.ios and love.system.getPickedFile and self.workState ~= "working" then + if self.nativePicker and love.system.getPickedFile and self.workState ~= "working" then local path = love.system.getPickedFile() if path then local kind = self.iosPendingKind or "rom" @@ -1898,10 +1905,18 @@ function RomImporter:update(dt) self.iosPendingVersion = nil if kind == "mod" then self:_installMod(path) + if isUWP() and self.modNotice and self.modNotice.ok then + os.remove(path) + end elseif kind == "sav" then - self:_importSave(version or self:_savedropTarget(), path) + local target = version or self:_savedropTarget() + self:_importSave(target, path) + if isUWP() and self.saveNotice[target] and self.saveNotice[target].ok then + os.remove(path) + end else self:startPath(path) + if isUWP() then os.remove(path) end end elseif love.system.getPickError then local errorText = love.system.getPickError() diff --git a/src/update/Check.lua b/src/update/Check.lua index b06b1f61..6ac03eb6 100644 --- a/src/update/Check.lua +++ b/src/update/Check.lua @@ -99,6 +99,11 @@ local cache = { status = "idle" } -- newest snapshot from the worker local function ensureWorker() if workerReady ~= nil then return workerReady end + if love and love.system and love.system.getOS + and love.system.getOS() == "UWP" then + workerReady = false + return false + end if not (love and love.thread and love.thread.newThread) then workerReady = false return false diff --git a/tests/rom_importer_double_pick_test.lua b/tests/rom_importer_double_pick_test.lua index e7eba41e..6bf5e041 100644 --- a/tests/rom_importer_double_pick_test.lua +++ b/tests/rom_importer_double_pick_test.lua @@ -28,6 +28,8 @@ love.system = love.system or {} local saved = { getOS = love.system.getOS, pickFile = love.system.pickFile, + getPickedFile = love.system.getPickedFile, + getPickError = love.system.getPickError, getDirectoryItems = love.filesystem.getDirectoryItems, getInfo = love.filesystem.getInfo, read = love.filesystem.read, @@ -135,6 +137,29 @@ contract:choose("red") check(picks == 2, "choose() still reopens the picker per call (#420/#442 contract, got " .. picks .. ")") +-- 7. UWP consumes the absolute LocalState path returned by its native picker +-- and removes the temporary copy only after a successful import. +local removedPath +local savedOsRemove = os.remove +os.remove = function(path) removedPath = path; return true end +love.system.getPickedFile = function() + love.system.getPickedFile = function() return nil end + return [[C:\LocalState\picked_mod.zip]] +end +love.system.getPickError = function() return nil end +local uwp = importer("UWP") +uwp.iosPendingKind = "mod" +uwp._installMod = function(self, source) + self._installed = source + self.modNotice = { ok = true, text = "Installed test-mod" } +end +uwp:update(0) +check(uwp._installed == [[C:\LocalState\picked_mod.zip]], + "UWP passes the picker path to the mod installer") +check(removedPath == [[C:\LocalState\picked_mod.zip]], + "UWP removes the temporary picker copy after installation") +os.remove = savedOsRemove + local touchForwardsToImporter = { Android = true, iOS = true, @@ -161,6 +186,8 @@ check(not touch._flex, love.system.getOS = saved.getOS love.system.pickFile = saved.pickFile +love.system.getPickedFile = saved.getPickedFile +love.system.getPickError = saved.getPickError love.filesystem.getDirectoryItems = saved.getDirectoryItems love.filesystem.getInfo = saved.getInfo love.filesystem.read = saved.read