diff --git a/.github/workflows/ios-build.yml b/.github/workflows/ios-build.yml new file mode 100644 index 00000000..8c3d88d1 --- /dev/null +++ b/.github/workflows/ios-build.yml @@ -0,0 +1,130 @@ +name: ios-build + +on: + workflow_dispatch: + inputs: + configuration: + description: Xcode configuration to archive + required: true + default: Release + type: choice + options: + - Release + - RelWithDebInfo + - Debug + ref: + description: Optional git ref to build + required: false + default: "" + type: string + +concurrency: + group: ios-build-${{ github.workflow }}-${{ inputs.ref || github.ref }} + cancel-in-progress: true + +jobs: + generate-spirv: + name: Generate SPIR-V Headers + runs-on: windows-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref || github.ref }} + submodules: recursive + + - name: Generate SPIR-V Headers + shell: pwsh + run: ./scripts/ci/generate-spirv-headers.ps1 + + - name: Upload SPIR-V Headers + uses: actions/upload-artifact@v4 + with: + name: spirv-headers + path: ./.artifacts/spirv-headers + if-no-files-found: error + + build-ios: + name: Build iOS Archive + runs-on: macos-26 + needs: generate-spirv + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref || github.ref }} + submodules: recursive + + - name: Install Build Dependencies + env: + HOMEBREW_NO_AUTO_UPDATE: "1" + HOMEBREW_NO_INSTALL_CLEANUP: "1" + run: brew install cmake ninja pkg-config + + - name: Validate Private File Secrets + env: + PRIVATE_DEFAULT_XEX_B64: ${{ secrets.PRIVATE_DEFAULT_XEX_B64 }} + PRIVATE_DEFAULT_XEXP_B64: ${{ secrets.PRIVATE_DEFAULT_XEXP_B64 }} + PRIVATE_SHADER_AR_B64: ${{ secrets.PRIVATE_SHADER_AR_B64 }} + run: | + set -euo pipefail + if [[ -f ./UnleashedRecompLib/private/default.xex && -f ./UnleashedRecompLib/private/default.xexp && -f ./UnleashedRecompLib/private/shader.ar ]]; then + echo "Using private files already present in the repository checkout." + exit 0 + fi + for name in PRIVATE_DEFAULT_XEX_B64 PRIVATE_DEFAULT_XEXP_B64 PRIVATE_SHADER_AR_B64; do + if [[ -z "${!name}" ]]; then + echo "Missing required secret ${name}, and private files are not present in the repository checkout." >&2 + exit 1 + fi + done + - name: Stage Private Assets + env: + PRIVATE_DEFAULT_XEX_B64: ${{ secrets.PRIVATE_DEFAULT_XEX_B64 }} + PRIVATE_DEFAULT_XEXP_B64: ${{ secrets.PRIVATE_DEFAULT_XEXP_B64 }} + PRIVATE_SHADER_AR_B64: ${{ secrets.PRIVATE_SHADER_AR_B64 }} + run: | + set -euo pipefail + mkdir -p ./UnleashedRecompLib/private + if [[ -f ./UnleashedRecompLib/private/default.xex && -f ./UnleashedRecompLib/private/default.xexp && -f ./UnleashedRecompLib/private/shader.ar ]]; then + echo "Private files already staged in repository checkout." + exit 0 + fi + printf '%s' "${PRIVATE_DEFAULT_XEX_B64}" | base64 --decode > ./UnleashedRecompLib/private/default.xex + printf '%s' "${PRIVATE_DEFAULT_XEXP_B64}" | base64 --decode > ./UnleashedRecompLib/private/default.xexp + printf '%s' "${PRIVATE_SHADER_AR_B64}" | base64 --decode > ./UnleashedRecompLib/private/shader.ar + - name: Prepare Host-Generated Sources + run: | + set -euo pipefail + cmake . --preset macos-release -DSDL2MIXER_VORBIS=VORBISFILE + cmake --build ./out/build/macos-release --target XenonRecomp x_decompress XenosRecomp file_to_c --parallel 3 + pushd ./out/build/macos-release/UnleashedRecompLib + ../tools/XenonRecomp/XenonRecomp/XenonRecomp 2>&1 | tee "${RUNNER_TEMP}/xenonrecomp.log" + ../tools/x_decompress/x_decompress \ + "${GITHUB_WORKSPACE}/UnleashedRecompLib/private/shader.ar" \ + "${GITHUB_WORKSPACE}/UnleashedRecompLib/private/shader_decompressed.ar" + ../tools/XenosRecomp/XenosRecomp/XenosRecomp 2>&1 | tee "${RUNNER_TEMP}/xenosrecomp.log" + popd + cmake --build ./out/build/macos-release --target UnleashedRecomp --parallel 3 --verbose + - name: Download SPIR-V Headers + uses: actions/download-artifact@v4 + with: + name: spirv-headers + path: ./.artifacts/spirv-headers + + - name: Stage SPIR-V Headers + run: rsync -a ./.artifacts/spirv-headers/ ./ + + - name: Build iOS Archive + env: + IOS_CONFIGURATION: ${{ inputs.configuration }} + run: bash ./scripts/ci/build-ios.sh + + - name: Upload iOS Artifacts + uses: actions/upload-artifact@v4 + with: + name: ios-${{ inputs.configuration }} + path: ./out/artifacts/ios + if-no-files-found: error diff --git a/UnleashedRecompLib/private/default.xex b/UnleashedRecompLib/private/default.xex new file mode 100644 index 00000000..ea83f7c6 Binary files /dev/null and b/UnleashedRecompLib/private/default.xex differ diff --git a/UnleashedRecompLib/private/default.xexp b/UnleashedRecompLib/private/default.xexp new file mode 100644 index 00000000..f13ffdfa Binary files /dev/null and b/UnleashedRecompLib/private/default.xexp differ diff --git a/UnleashedRecompLib/private/shader.ar b/UnleashedRecompLib/private/shader.ar new file mode 100644 index 00000000..43215bd6 Binary files /dev/null and b/UnleashedRecompLib/private/shader.ar differ diff --git a/scripts/ci/build-ios.sh b/scripts/ci/build-ios.sh new file mode 100755 index 00000000..43a35b0a --- /dev/null +++ b/scripts/ci/build-ios.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/../.." && pwd)" +cd "${repo_root}" + +configuration="${IOS_CONFIGURATION:-Release}" +deployment_target="${IOS_DEPLOYMENT_TARGET:-15.0}" +bundle_id="${IOS_BUNDLE_ID:-io.github.aw514844.unleashedrecomp}" +artifact_root="${repo_root}/out/artifacts/ios" + +case "${configuration}" in + Debug) preset_suffix="debug" ;; + Release) preset_suffix="release" ;; + RelWithDebInfo) preset_suffix="relwithdebinfo" ;; + *) + echo "Unsupported IOS_CONFIGURATION: ${configuration}" >&2 + exit 1 + ;; +esac + +preset="ios-xcode-${preset_suffix}" +build_dir="${repo_root}/out/build/${preset}" +archive_path="${artifact_root}/UnleashedRecomp.xcarchive" +app_zip="${artifact_root}/UnleashedRecomp-${configuration}.app.zip" +log_path="${artifact_root}/xcodebuild-${configuration}.log" + +mkdir -p "${artifact_root}" + +required_tools=(cmake xcodebuild xcrun) +for tool in "${required_tools[@]}"; do + if ! command -v "${tool}" >/dev/null 2>&1; then + echo "Missing required tool: ${tool}" >&2 + exit 1 + fi +done + +spirv_count="$(find "${repo_root}/UnleashedRecomp/gpu/shader/hlsl" -name '*.spirv.h' | wc -l | tr -d ' ')" +if [[ "${spirv_count}" -lt 23 ]]; then + echo "Expected at least 23 generated .spirv.h files, found ${spirv_count}" >&2 + exit 1 +fi + +rm -rf "${archive_path}" + +cmake . --preset "${preset}" \ + -DUNLEASHED_RECOMP_IOS_ENABLE_CODE_SIGNING=OFF \ + -DUNLEASHED_RECOMP_IOS_BUNDLE_ID="${bundle_id}" \ + -DCMAKE_OSX_DEPLOYMENT_TARGET="${deployment_target}" + +xcodebuild \ + -project "${build_dir}/UnleashedRecomp.xcodeproj" \ + -scheme "UnleashedRecomp" \ + -configuration "${configuration}" \ + -sdk iphoneos \ + -destination "generic/platform=iOS" \ + -archivePath "${archive_path}" \ + archive | tee "${log_path}" + +app_path="$(find "${archive_path}/Products/Applications" -maxdepth 1 -name '*.app' -print -quit)" +if [[ -z "${app_path}" ]]; then + echo "Archive completed but no .app bundle was found in ${archive_path}" >&2 + exit 1 +fi + +ditto -c -k --sequesterRsrc --keepParent "${app_path}" "${app_zip}" + +echo "Archive: ${archive_path}" +echo "App zip: ${app_zip}" +echo "Log: ${log_path}" diff --git a/scripts/ci/generate-spirv-headers.ps1 b/scripts/ci/generate-spirv-headers.ps1 new file mode 100644 index 00000000..ff435625 --- /dev/null +++ b/scripts/ci/generate-spirv-headers.ps1 @@ -0,0 +1,112 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = Resolve-Path (Join-Path $scriptDir "../..") +$hlslRoot = Join-Path $repoRoot "UnleashedRecomp/gpu/shader/hlsl" +$artifactRoot = Join-Path $repoRoot ".artifacts/spirv-headers/UnleashedRecomp/gpu/shader/hlsl" +$toolRoot = Join-Path $repoRoot ".tools/dxc" + +function Get-DxcPath { + $existing = Get-Command dxc.exe -ErrorAction SilentlyContinue + if ($existing) { + return $existing.Source + } + + New-Item -ItemType Directory -Force -Path $toolRoot | Out-Null + + $release = Invoke-RestMethod -Uri "https://api.github.com/repos/microsoft/DirectXShaderCompiler/releases/latest" + $asset = $release.assets | + Where-Object { $_.name -match '^dxc_.*\.zip$' } | + Select-Object -First 1 + + if (-not $asset) { + $assetNames = ($release.assets | ForEach-Object { $_.name }) -join ", " + throw "Could not find a Windows DXC zip in the latest DirectXShaderCompiler release. Assets: $assetNames" + } + + $zipPath = Join-Path $toolRoot $asset.name + $extractPath = Join-Path $toolRoot "extract" + + Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $zipPath + if (Test-Path $extractPath) { + Remove-Item -Recurse -Force $extractPath + } + + Expand-Archive -Path $zipPath -DestinationPath $extractPath + + $downloaded = Get-ChildItem -Path $extractPath -Filter dxc.exe -Recurse | + Sort-Object FullName | + Where-Object { $_.FullName -match '[\\/]x64[\\/]' } | + Select-Object -First 1 + + if (-not $downloaded) { + $downloaded = Get-ChildItem -Path $extractPath -Filter dxc.exe -Recurse | + Sort-Object FullName | + Select-Object -First 1 + } + + if (-not $downloaded) { + throw "Downloaded DXC archive did not contain dxc.exe" + } + + return $downloaded.FullName +} + +$shaderSpecs = @( + @{ Name = "blend_color_alpha_ps"; Profile = "ps_6_0"; ExtraArgs = @("-DUNLEASHED_RECOMP") }, + @{ Name = "copy_vs"; Profile = "vs_6_0"; ExtraArgs = @("-fvk-invert-y", "-DUNLEASHED_RECOMP") }, + @{ Name = "copy_color_ps"; Profile = "ps_6_0"; ExtraArgs = @("-DUNLEASHED_RECOMP") }, + @{ Name = "copy_depth_ps"; Profile = "ps_6_0"; ExtraArgs = @("-DUNLEASHED_RECOMP") }, + @{ Name = "csd_filter_ps"; Profile = "ps_6_0"; ExtraArgs = @("-DUNLEASHED_RECOMP") }, + @{ Name = "csd_no_tex_vs"; Profile = "vs_6_0"; ExtraArgs = @("-fvk-invert-y", "-DUNLEASHED_RECOMP") }, + @{ Name = "csd_vs"; Profile = "vs_6_0"; ExtraArgs = @("-fvk-invert-y", "-DUNLEASHED_RECOMP") }, + @{ Name = "enhanced_motion_blur_ps"; Profile = "ps_6_0"; ExtraArgs = @("-DUNLEASHED_RECOMP") }, + @{ Name = "gaussian_blur_3x3"; Profile = "ps_6_0"; ExtraArgs = @("-DUNLEASHED_RECOMP") }, + @{ Name = "gaussian_blur_5x5"; Profile = "ps_6_0"; ExtraArgs = @("-DUNLEASHED_RECOMP") }, + @{ Name = "gaussian_blur_7x7"; Profile = "ps_6_0"; ExtraArgs = @("-DUNLEASHED_RECOMP") }, + @{ Name = "gaussian_blur_9x9"; Profile = "ps_6_0"; ExtraArgs = @("-DUNLEASHED_RECOMP") }, + @{ Name = "gamma_correction_ps"; Profile = "ps_6_0"; ExtraArgs = @("-DUNLEASHED_RECOMP") }, + @{ Name = "imgui_ps"; Profile = "ps_6_0"; ExtraArgs = @("-DUNLEASHED_RECOMP") }, + @{ Name = "imgui_vs"; Profile = "vs_6_0"; ExtraArgs = @("-fvk-invert-y", "-DUNLEASHED_RECOMP") }, + @{ Name = "movie_ps"; Profile = "ps_6_0"; ExtraArgs = @("-DUNLEASHED_RECOMP") }, + @{ Name = "movie_vs"; Profile = "vs_6_0"; ExtraArgs = @("-fvk-invert-y", "-DUNLEASHED_RECOMP") }, + @{ Name = "resolve_msaa_color_2x"; Profile = "ps_6_0"; ExtraArgs = @("-DUNLEASHED_RECOMP") }, + @{ Name = "resolve_msaa_color_4x"; Profile = "ps_6_0"; ExtraArgs = @("-DUNLEASHED_RECOMP") }, + @{ Name = "resolve_msaa_color_8x"; Profile = "ps_6_0"; ExtraArgs = @("-DUNLEASHED_RECOMP") }, + @{ Name = "resolve_msaa_depth_2x"; Profile = "ps_6_0"; ExtraArgs = @("-DUNLEASHED_RECOMP") }, + @{ Name = "resolve_msaa_depth_4x"; Profile = "ps_6_0"; ExtraArgs = @("-DUNLEASHED_RECOMP") }, + @{ Name = "resolve_msaa_depth_8x"; Profile = "ps_6_0"; ExtraArgs = @("-DUNLEASHED_RECOMP") } +) + +$dxcPath = Get-DxcPath +New-Item -ItemType Directory -Force -Path $artifactRoot | Out-Null + +foreach ($shader in $shaderSpecs) { + $inputPath = Join-Path $hlslRoot ($shader.Name + ".hlsl") + $outputPath = $inputPath + ".spirv.h" + $artifactPath = Join-Path $artifactRoot ($shader.Name + ".hlsl.spirv.h") + + $arguments = @( + "-T", $shader.Profile, + "-HV", "2021", + "-all-resources-bound", + "-spirv", + "-fvk-use-dx-layout" + ) + $shader.ExtraArgs + @( + "-E", "shaderMain", + "-Fh", $outputPath, + $inputPath, + "-Vn", ("g_" + $shader.Name + "_spirv") + ) + + Write-Host "Generating $outputPath" + & $dxcPath @arguments + if ($LASTEXITCODE -ne 0) { + throw "DXC failed while generating $outputPath" + } + + Copy-Item -Force -Path $outputPath -Destination $artifactPath +} + +Write-Host "Generated $($shaderSpecs.Count) SPIR-V headers"