Skip to content

Commit eeddea9

Browse files
authored
Merge pull request #307 from winnerspiros/copilot/optimize-for-android-performance
feat: clone and optimize all ppy/osu-resources in release workflow
2 parents 079fd8d + ed77043 commit eeddea9

33 files changed

Lines changed: 1348 additions & 115 deletions

.github/resource-budgets/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Resource budget files
2+
3+
- `source-overrides.json`: limits for local override media under `osu.Game/Resources`.
4+
- `android-apk-media.json`: limits for media packaged into the final Android APK.
5+
6+
`android-apk-media.json` currently uses `max_apk_bytes = 320000000` as a direct-distribution guardrail for this fork while still failing on major regressions.
7+
8+
Budgets are evaluated after workflow-time optimization (`scripts/optimize_resource_overrides.py`), so reports reflect the effective media set used by builds.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"top_n": 20,
3+
"max_apk_bytes": 320000000,
4+
"max_total_bytes": 95000000,
5+
"max_image_bytes": 75000000,
6+
"max_audio_bytes": 20000000,
7+
"max_video_bytes": 8000000,
8+
"max_largest_file_bytes": 4500000,
9+
"max_top_n_total_bytes": 50000000
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"top_n": 15,
3+
"max_total_bytes": 6291456,
4+
"max_image_bytes": 4194304,
5+
"max_audio_bytes": 2097152,
6+
"max_video_bytes": 2097152,
7+
"max_largest_file_bytes": 1048576,
8+
"max_top_n_total_bytes": 5242880
9+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Resource optimizer config
2+
3+
- `config.json` controls workflow-time conversion of local overrides in `osu.Game/Resources`.
4+
- Conversion is performed by `scripts/optimize_resource_overrides.py` using `ffmpeg`.
5+
6+
Android-friendly defaults:
7+
8+
- `png/jpg/jpeg -> webp`
9+
- `wav/mp3 -> ogg (Opus by default, 48kHz target)`
10+
- `mp4 -> webm`
11+
12+
Image quality strategy:
13+
14+
- PNGs can produce both lossless and lossy WebP candidates; the optimizer keeps the smallest valid candidate.
15+
- Lossy image outputs can be gated by SSIM (`measure_image_ssim` + `image_lossy_min_ssim`).
16+
- Alpha PNGs can use a separate quality setting (`png_webp_alpha_lossy_quality`).
17+
18+
Audio strategy:
19+
20+
- Default codec is `libopus` in `.ogg`, tuned for good size/quality on Android.
21+
- Output sample rate/channels are configurable (`audio_target_sample_rate_hz`, `audio_target_channels`).
22+
- Optional path filters can force mono for selected samples (`audio_force_mono_globs`).
23+
24+
Safety and compatibility:
25+
26+
- `keep_original_files=true` keeps source files alongside optimized outputs.
27+
- This allows runtime fallback behavior to remain robust while compressed overrides are preferred.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"include_globs": [
3+
"Textures/**/*.png",
4+
"Textures/**/*.jpg",
5+
"Textures/**/*.jpeg",
6+
"Samples/**/*.wav",
7+
"Samples/**/*.mp3",
8+
"Videos/**/*.mp4"
9+
],
10+
"exclude_globs": [],
11+
"enable_video_conversion": true,
12+
"keep_original_files": true,
13+
"measure_image_ssim": true,
14+
"image_lossy_min_ssim": 0.995,
15+
"png_webp_try_lossless": true,
16+
"png_webp_lossless_compression_level": 6,
17+
"png_webp_lossy_quality": 92,
18+
"png_webp_alpha_lossy_quality": 95,
19+
"png_webp_lossy_method": 6,
20+
"jpeg_webp_quality": 88,
21+
"jpeg_webp_method": 6,
22+
"audio_codec": "libopus",
23+
"audio_ogg_bitrate": "96k",
24+
"audio_target_sample_rate_hz": 48000,
25+
"audio_target_channels": 2,
26+
"audio_force_mono_globs": [
27+
"Samples/UI/**",
28+
"Samples/Gameplay/**"
29+
],
30+
"audio_opus_application": "audio",
31+
"audio_opus_frame_duration_ms": 20,
32+
"video_webm_crf": 34,
33+
"video_webm_audio_bitrate": "96k",
34+
"video_webm_deadline": "good",
35+
"video_webm_cpu_used": 4,
36+
"video_webm_tile_columns": 2
37+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"include_globs": [
3+
"**/*.png",
4+
"**/*.jpg",
5+
"**/*.jpeg",
6+
"**/*.wav",
7+
"**/*.mp3",
8+
"**/*.mp4"
9+
],
10+
"exclude_globs": [],
11+
"enable_video_conversion": true,
12+
"keep_original_files": false,
13+
"measure_image_ssim": true,
14+
"image_lossy_min_ssim": 0.995,
15+
"png_webp_try_lossless": true,
16+
"png_webp_lossless_compression_level": 6,
17+
"png_webp_lossy_quality": 92,
18+
"png_webp_alpha_lossy_quality": 95,
19+
"png_webp_lossy_method": 6,
20+
"jpeg_webp_quality": 88,
21+
"jpeg_webp_method": 6,
22+
"audio_codec": "libopus",
23+
"audio_ogg_bitrate": "96k",
24+
"audio_target_sample_rate_hz": 48000,
25+
"audio_target_channels": 2,
26+
"audio_force_mono_globs": [
27+
"Samples/UI/**",
28+
"Samples/Gameplay/**"
29+
],
30+
"audio_opus_application": "audio",
31+
"audio_opus_frame_duration_ms": 20,
32+
"video_webm_crf": 34,
33+
"video_webm_audio_bitrate": "96k",
34+
"video_webm_deadline": "good",
35+
"video_webm_cpu_used": 4,
36+
"video_webm_tile_columns": 2
37+
}

.github/workflows/ci.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,49 @@ permissions:
1010
security-events: write # for reporting InspectCode issues
1111

1212
jobs:
13+
resource-budget-source:
14+
name: Resource Budget (Source Overrides)
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v6
19+
20+
- name: Install ffmpeg
21+
run: |
22+
sudo apt-get update
23+
sudo apt-get install -y ffmpeg
24+
25+
- name: Optimize local override media
26+
run: |
27+
python3 scripts/optimize_resource_overrides.py \
28+
--root osu.Game/Resources \
29+
--config .github/resource-optimizer/config.json \
30+
--report-file resource-budget/optimizer-report.json \
31+
--summary-file resource-budget/optimizer-summary.md
32+
33+
- name: Run source override resource budget checks
34+
run: |
35+
python3 scripts/check_resource_budgets.py \
36+
--mode source \
37+
--source-root osu.Game/Resources \
38+
--budget-file .github/resource-budgets/source-overrides.json \
39+
--report-file resource-budget/source-report.json \
40+
--summary-file resource-budget/source-summary.md
41+
42+
- name: Publish source optimization summary
43+
if: ${{ !cancelled() }}
44+
run: |
45+
cat resource-budget/optimizer-summary.md >> "$GITHUB_STEP_SUMMARY"
46+
echo "" >> "$GITHUB_STEP_SUMMARY"
47+
cat resource-budget/source-summary.md >> "$GITHUB_STEP_SUMMARY"
48+
49+
- name: Upload source resource budget reports
50+
uses: actions/upload-artifact@v7
51+
if: ${{ !cancelled() }}
52+
with:
53+
name: resource-budget-source
54+
path: resource-budget/*
55+
1356
inspect-code:
1457
name: Code Quality
1558
runs-on: ubuntu-latest

.github/workflows/release.yml

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,67 @@ jobs:
221221
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
222222
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
223223
224+
- name: Install ffmpeg
225+
run: |
226+
sudo apt-get update
227+
sudo apt-get install -y ffmpeg
228+
229+
- name: Clone and optimize all osu-resources from GitHub
230+
run: |
231+
# ── Clone the upstream ppy/osu-resources repository ─────────────────────
232+
git clone --depth=1 https://github.com/ppy/osu-resources /tmp/osu-resources
233+
234+
# ── Optimize ALL media assets with ffmpeg ────────────────────────────────
235+
# Uses keep_original_files=false so the packed DLL ships only the
236+
# compressed form (webp/ogg/webm) — originals would waste APK space
237+
# because the fallback chain always prefers the optimized extension.
238+
python3 scripts/optimize_resource_overrides.py \
239+
--root /tmp/osu-resources/osu.Game.Resources \
240+
--config .github/resource-optimizer/osu-resources-config.json \
241+
--report-file resource-budget/osu-resources-optimizer-report.json \
242+
--summary-file resource-budget/osu-resources-optimizer-summary.md
243+
244+
# ── Pack the optimized DLL as a local NuGet package ──────────────────────
245+
# Use a run-number-unique version so the NuGet cache never serves a stale
246+
# hit. local-packages/ is the first NuGet source in NuGet.Config, so
247+
# dotnet restore will prefer this local build over nuget.org automatically.
248+
RESOURCES_VERSION="$(date -u +%Y).$(date -u +%-m%d).${{ github.run_number }}"
249+
250+
if dotnet pack /tmp/osu-resources/osu.Game.Resources/osu.Game.Resources.csproj \
251+
-c Release \
252+
-o ./local-packages/ \
253+
-p:Version="${RESOURCES_VERSION}"; then
254+
# Patch the PackageReference in osu.Game.csproj to pick up our local build.
255+
sed -i \
256+
"s|ppy.osu.Game.Resources\" Version=\"[^\"]*\"|ppy.osu.Game.Resources\" Version=\"${RESOURCES_VERSION}\"|" \
257+
osu.Game/osu.Game.csproj
258+
echo "✅ Using optimized osu.Game.Resources ${RESOURCES_VERSION} (local pack)"
259+
else
260+
echo "⚠️ dotnet pack failed; falling back to upstream ppy.osu.Game.Resources from NuGet"
261+
fi
262+
263+
# Append optimizer summary to the workflow summary
264+
if [ -f resource-budget/osu-resources-optimizer-summary.md ]; then
265+
cat resource-budget/osu-resources-optimizer-summary.md >> "$GITHUB_STEP_SUMMARY" || true
266+
fi
267+
268+
- name: Optimize local override media
269+
run: |
270+
python3 scripts/optimize_resource_overrides.py \
271+
--root osu.Game/Resources \
272+
--config .github/resource-optimizer/config.json \
273+
--report-file resource-budget/optimizer-report.json \
274+
--summary-file resource-budget/optimizer-summary.md
275+
276+
- name: Check source override resource budgets
277+
run: |
278+
python3 scripts/check_resource_budgets.py \
279+
--mode source \
280+
--source-root osu.Game/Resources \
281+
--budget-file .github/resource-budgets/source-overrides.json \
282+
--report-file resource-budget/source-report.json \
283+
--summary-file resource-budget/source-summary.md
284+
224285
- name: Build Android APK
225286
run: >
226287
dotnet publish -c Release
@@ -259,6 +320,24 @@ jobs:
259320
echo "Found APK: $FINAL_APK ($APK_SIZE_MB MB)"
260321
echo "apk_path=$FINAL_APK" >> "$GITHUB_OUTPUT"
261322
323+
- name: Check APK media budgets
324+
run: |
325+
python3 scripts/check_resource_budgets.py \
326+
--mode apk \
327+
--apk-path "${{ steps.find_apk.outputs.apk_path }}" \
328+
--budget-file .github/resource-budgets/android-apk-media.json \
329+
--report-file resource-budget/apk-report.json \
330+
--summary-file resource-budget/apk-summary.md
331+
332+
- name: Publish resource budget summary
333+
if: ${{ !cancelled() }}
334+
run: |
335+
cat resource-budget/optimizer-summary.md >> "$GITHUB_STEP_SUMMARY"
336+
echo "" >> "$GITHUB_STEP_SUMMARY"
337+
cat resource-budget/source-summary.md >> "$GITHUB_STEP_SUMMARY"
338+
echo "" >> "$GITHUB_STEP_SUMMARY"
339+
cat resource-budget/apk-summary.md >> "$GITHUB_STEP_SUMMARY"
340+
262341
- name: Verify APK signature
263342
run: |
264343
APK="${{ steps.find_apk.outputs.apk_path }}"
@@ -346,6 +425,13 @@ jobs:
346425
path: ${{ steps.find_apk.outputs.apk_path }}
347426
if-no-files-found: error
348427

428+
- name: Upload resource budget reports
429+
uses: actions/upload-artifact@v7
430+
if: ${{ !cancelled() }}
431+
with:
432+
name: resource-budget-android-${{ steps.version.outputs.version }}
433+
path: resource-budget/*
434+
349435
# When the keystore was auto-generated, upload it so the user can save it
350436
# as a repository secret for consistent signing across builds.
351437
- name: Upload generated keystore

MOBILE_RESOURCE_POLICY.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Mobile Resource Policy
2+
3+
This repository keeps `ppy.osu.Game.Resources` as the canonical upstream source and applies targeted local overrides for mobile performance.
4+
5+
## Goals
6+
7+
- Reduce APK growth from large media assets.
8+
- Reduce decode/upload spikes on startup and first gameplay entry.
9+
- Keep upstream compatibility without forking `ppy/osu-resources`.
10+
11+
## Override strategy
12+
13+
- Add only high-impact overrides in `osu.Game/Resources`.
14+
- Match upstream virtual paths (`Textures/...`, `Samples/...`, `Videos/...`).
15+
- Prefer compressed formats:
16+
- Textures: `webp` where quality remains acceptable.
17+
- Audio: `ogg` (Opus, 48kHz target) for compact size while retaining responsiveness.
18+
- Video: `webm` where supported and visually acceptable.
19+
20+
## Resource budgets (enforced in workflow)
21+
22+
- Source override limits: `.github/resource-budgets/source-overrides.json`
23+
- Android APK media limits: `.github/resource-budgets/android-apk-media.json`
24+
25+
Current `max_apk_bytes` is set to 320,000,000 bytes as a hard guardrail for this project’s direct-distribution APK flow while still forcing visible regressions to fail CI.
26+
27+
Workflow checks now fail when budgets regress for:
28+
29+
- Total media bytes
30+
- Per-bucket bytes (image/audio/video)
31+
- Largest single media file
32+
- Top-N largest media aggregate
33+
- Total APK size (release workflow)
34+
35+
## Workflow optimization pass
36+
37+
- CI and release workflows run `scripts/optimize_resource_overrides.py` on `osu.Game/Resources` before budget checks.
38+
- Optimizer settings are in `.github/resource-optimizer/config.json`.
39+
- By default, optimized files are added as side-by-side overrides and originals are kept for safety (`keep_original_files=true`).
40+
- Image optimization is quality-gated (SSIM threshold) so aggressive size wins do not silently degrade visuals.
41+
42+
## Naming and quality rules
43+
44+
- Keep override names stable (do not invent alternate keys).
45+
- Avoid duplicate variants unless there is a demonstrated runtime need.
46+
- Optimise for perceptual quality at smallest size that preserves gameplay UX.
47+
48+
## Conversion guidance
49+
50+
Use your preferred encoder tooling locally (for example `cwebp`, `ffmpeg`) before committing overrides.

osu.Android.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
</PropertyGroup>
100100

101101
<ItemGroup>
102-
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.506.4" />
102+
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.507.1" />
103103
<!-- `ppy.osu.Framework.NativeLibs` is a transitive dependency of `ppy.osu.Framework`
104104
that ships desktop-only natives (Linux/macOS/Windows) under `runtimes/<rid>/native/`
105105
— including a bare Linux `libbass.so`/`libbass_fx.so`/`libbassmix.so` for linux-arm64.

0 commit comments

Comments
 (0)