From 5bf8c8bf1959a7a8ca8569df4b48b7d87e628839 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 07:54:21 +0000 Subject: [PATCH 1/7] chore: outline AVIF-for-JPEG optimization plan Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/308e53dd-8e58-4c88-bce2-a507f3bc571e Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- .github/resource-optimizer/config.json | 9 +++++---- .github/resource-optimizer/osu-resources-config.json | 9 +++++---- scripts/optimize_resource_overrides.py | 4 +++- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/resource-optimizer/config.json b/.github/resource-optimizer/config.json index 3a47baea13e4..2363e98af3dc 100644 --- a/.github/resource-optimizer/config.json +++ b/.github/resource-optimizer/config.json @@ -11,16 +11,17 @@ "enable_video_conversion": true, "keep_original_files": true, "measure_image_ssim": true, - "image_lossy_min_ssim": 0.995, + "image_lossy_min_ssim": 0.990, "png_webp_try_lossless": true, - "png_webp_lossless_compression_level": 6, - "png_webp_lossy_quality": 92, + "png_webp_lossless_compression_level": 9, + "png_webp_lossy_quality": 90, "png_webp_alpha_lossy_quality": 95, "png_webp_lossy_method": 6, "jpeg_webp_quality": 88, "jpeg_webp_method": 6, "audio_codec": "libopus", "audio_ogg_bitrate": "96k", + "audio_opus_mono_bitrate": "64k", "audio_target_sample_rate_hz": 48000, "audio_target_channels": 2, "audio_force_mono_globs": [ @@ -29,7 +30,7 @@ ], "audio_opus_application": "audio", "audio_opus_frame_duration_ms": 20, - "video_webm_crf": 34, + "video_webm_crf": 36, "video_webm_audio_bitrate": "96k", "video_webm_deadline": "good", "video_webm_cpu_used": 4, diff --git a/.github/resource-optimizer/osu-resources-config.json b/.github/resource-optimizer/osu-resources-config.json index be0c9fa9c817..d4716dfd739b 100644 --- a/.github/resource-optimizer/osu-resources-config.json +++ b/.github/resource-optimizer/osu-resources-config.json @@ -11,16 +11,17 @@ "enable_video_conversion": true, "keep_original_files": false, "measure_image_ssim": true, - "image_lossy_min_ssim": 0.995, + "image_lossy_min_ssim": 0.990, "png_webp_try_lossless": true, - "png_webp_lossless_compression_level": 6, - "png_webp_lossy_quality": 92, + "png_webp_lossless_compression_level": 9, + "png_webp_lossy_quality": 90, "png_webp_alpha_lossy_quality": 95, "png_webp_lossy_method": 6, "jpeg_webp_quality": 88, "jpeg_webp_method": 6, "audio_codec": "libopus", "audio_ogg_bitrate": "96k", + "audio_opus_mono_bitrate": "64k", "audio_target_sample_rate_hz": 48000, "audio_target_channels": 2, "audio_force_mono_globs": [ @@ -29,7 +30,7 @@ ], "audio_opus_application": "audio", "audio_opus_frame_duration_ms": 20, - "video_webm_crf": 34, + "video_webm_crf": 36, "video_webm_audio_bitrate": "96k", "video_webm_deadline": "good", "video_webm_cpu_used": 4, diff --git a/scripts/optimize_resource_overrides.py b/scripts/optimize_resource_overrides.py index 2d1c1c91bbc5..2534e33e654e 100644 --- a/scripts/optimize_resource_overrides.py +++ b/scripts/optimize_resource_overrides.py @@ -174,7 +174,9 @@ def convert_audio_to_ogg(source: Path, output: Path, config: dict, relative_path base_args = ["-i", str(source), "-ar", str(target_sample_rate), "-ac", str(channel_count)] if audio_codec == "libopus": - bitrate = str(config.get("audio_opus_bitrate", config.get("audio_ogg_bitrate", "96k"))) + stereo_bitrate = str(config.get("audio_opus_bitrate", config.get("audio_ogg_bitrate", "96k"))) + mono_bitrate = str(config.get("audio_opus_mono_bitrate", stereo_bitrate)) + bitrate = mono_bitrate if channel_count == 1 else stereo_bitrate opus_application = str(config.get("audio_opus_application", "audio")) frame_duration = str(config.get("audio_opus_frame_duration_ms", 20)) run_ffmpeg( From 1d46415d41b5137134838c40ce6d11f487c9fe80 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 07:56:59 +0000 Subject: [PATCH 2/7] perf(optimizer): AVIF for JPEG, mono 64k Opus, tighter WebP/video settings Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/308e53dd-8e58-4c88-bce2-a507f3bc571e Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- .github/resource-optimizer/config.json | 3 + .../osu-resources-config.json | 3 + scripts/optimize_resource_overrides.py | 139 ++++++++++++------ 3 files changed, 96 insertions(+), 49 deletions(-) diff --git a/.github/resource-optimizer/config.json b/.github/resource-optimizer/config.json index 2363e98af3dc..35923aa8996b 100644 --- a/.github/resource-optimizer/config.json +++ b/.github/resource-optimizer/config.json @@ -19,6 +19,9 @@ "png_webp_lossy_method": 6, "jpeg_webp_quality": 88, "jpeg_webp_method": 6, + "jpeg_avif_enabled": true, + "jpeg_avif_crf": 30, + "jpeg_avif_preset": 4, "audio_codec": "libopus", "audio_ogg_bitrate": "96k", "audio_opus_mono_bitrate": "64k", diff --git a/.github/resource-optimizer/osu-resources-config.json b/.github/resource-optimizer/osu-resources-config.json index d4716dfd739b..6451f8310cb4 100644 --- a/.github/resource-optimizer/osu-resources-config.json +++ b/.github/resource-optimizer/osu-resources-config.json @@ -19,6 +19,9 @@ "png_webp_lossy_method": 6, "jpeg_webp_quality": 88, "jpeg_webp_method": 6, + "jpeg_avif_enabled": true, + "jpeg_avif_crf": 30, + "jpeg_avif_preset": 4, "audio_codec": "libopus", "audio_ogg_bitrate": "96k", "audio_opus_mono_bitrate": "64k", diff --git a/scripts/optimize_resource_overrides.py b/scripts/optimize_resource_overrides.py index 2534e33e654e..22cedfc6328e 100644 --- a/scripts/optimize_resource_overrides.py +++ b/scripts/optimize_resource_overrides.py @@ -89,80 +89,118 @@ def matches_any_glob(relative_path: str, patterns: list[str]) -> bool: return any(fnmatch.fnmatch(relative_path, pattern) for pattern in patterns) -def convert_image_to_webp(source: Path, output: Path, config: dict, relative_path: str) -> str: +def convert_image(source: Path, config: dict, relative_path: str) -> tuple[Path, str]: + """ + Convert a source image to the best compressed format available. + + For PNG: tries lossless WebP and lossy WebP; picks the smallest that meets the SSIM + threshold. AVIF is explicitly excluded for PNGs because libsvtav1 only supports + yuv420p (no alpha channel), which would silently strip transparency. + + For JPEG/JPG: tries lossy WebP and, when ``jpeg_avif_enabled`` is true in config, + lossy AVIF via libsvtav1 (yuv420p — safe for JPEG sources which have no alpha). + Picks the smallest candidate that meets the SSIM threshold. + + Returns ``(temp_output_path, strategy)`` where ``temp_output_path.suffix`` is the + winning format extension (``.webp`` or ``.avif``). The caller is responsible for + moving the file to its final destination. + """ ext = source.suffix.lower() - temp_candidates: list[tuple[Path, str]] = [] - has_alpha = ext == ".png" and ffprobe_has_alpha_channel(source) + # List of (temp_path, strategy_name, is_lossless_flag, format_extension) tuples + # representing conversion candidates to compare. + temp_candidates: list[tuple[Path, str, bool, str]] = [] + # ── WebP lossless (PNG only) ───────────────────────────────────────────── if ext == ".png" and bool(config.get("png_webp_try_lossless", True)): - lossless_path = source.parent / f".{output.name}.lossless.tmp" + p = source.parent / f".{source.stem}.lossless.webp.tmp" run_ffmpeg( [ - "-i", - str(source), - "-c:v", - "libwebp", - "-lossless", - "1", - "-compression_level", - str(config.get("png_webp_lossless_compression_level", 6)), - "-f", - "webp", - str(lossless_path), + "-i", str(source), + "-c:v", "libwebp", + "-lossless", "1", + "-compression_level", str(config.get("png_webp_lossless_compression_level", 9)), + "-f", "webp", str(p), ] ) - temp_candidates.append((lossless_path, "png-lossless-webp")) + temp_candidates.append((p, "png-lossless-webp", True, ".webp")) + # ── WebP lossy ─────────────────────────────────────────────────────────── + has_alpha = ext == ".png" and ffprobe_has_alpha_channel(source) lossy_quality = ( - config.get("png_webp_alpha_lossy_quality", 95) if has_alpha else config.get("png_webp_lossy_quality", 92) + config.get("png_webp_alpha_lossy_quality", 95) if has_alpha else config.get("png_webp_lossy_quality", 90) ) if ext == ".png" else config.get("jpeg_webp_quality", 88) lossy_method = config.get("png_webp_lossy_method", 6) if ext == ".png" else config.get("jpeg_webp_method", 6) - lossy_path = source.parent / f".{output.name}.lossy.tmp" + p_webp = source.parent / f".{source.stem}.lossy.webp.tmp" run_ffmpeg( [ - "-i", - str(source), - "-c:v", - "libwebp", - "-q:v", - str(lossy_quality), - "-compression_level", - str(lossy_method), - "-f", - "webp", - str(lossy_path), + "-i", str(source), + "-c:v", "libwebp", + "-q:v", str(lossy_quality), + "-compression_level", str(lossy_method), + "-f", "webp", str(p_webp), ] ) - lossy_strategy = f"{ext.lstrip('.')}-lossy-webp-q{lossy_quality}" - temp_candidates.append((lossy_path, lossy_strategy)) - - minimum_ssim = float(config.get("image_lossy_min_ssim", 0.995)) + temp_candidates.append((p_webp, f"{ext.lstrip('.')}-lossy-webp-q{lossy_quality}", False, ".webp")) + + # ── AVIF via libsvtav1 (JPEG/JPG only, opt-in via jpeg_avif_enabled) ───── + # PNG files must NOT use AVIF: libsvtav1 encodes yuv420p only and silently + # strips alpha channels, producing a tiny but completely transparent output. + # JPEG sources have no alpha, so yuv420p is safe. + if ext in {".jpg", ".jpeg"} and bool(config.get("jpeg_avif_enabled", False)): + avif_crf = int(config.get("jpeg_avif_crf", 30)) + avif_preset = int(config.get("jpeg_avif_preset", 4)) + p_avif = source.parent / f".{source.stem}.lossy.avif.tmp" + try: + run_ffmpeg( + [ + "-i", str(source), + "-c:v", "libsvtav1", + "-crf", str(avif_crf), + "-preset", str(avif_preset), + "-pix_fmt", "yuv420p", + "-f", "avif", # explicit muxer — ffmpeg cannot infer AVIF from .tmp extension + str(p_avif), + ] + ) + temp_candidates.append((p_avif, f"jpg-avif-svtav1-crf{avif_crf}-p{avif_preset}", False, ".avif")) + except subprocess.CalledProcessError: + # Log a warning so the operator knows AVIF was requested but unavailable. + print(f"::warning::AVIF requested for {relative_path} but libsvtav1 encode failed; falling back to WebP.") + p_avif.unlink(missing_ok=True) + + # ── Pick the smallest candidate that meets the SSIM threshold ──────────── + minimum_ssim = float(config.get("image_lossy_min_ssim", 0.990)) compare_ssim = bool(config.get("measure_image_ssim", True)) - best_candidate: Optional[Tuple[Path, str]] = None + best_candidate: Optional[Tuple[Path, str, str]] = None # (path, strategy, fmt_ext) - for candidate_path, strategy in temp_candidates: - if strategy == lossy_strategy and compare_ssim: - ssim = measure_image_ssim(source, candidate_path) + for cand_path, strategy, is_lossless, fmt_ext in temp_candidates: + if not is_lossless and compare_ssim: + ssim = measure_image_ssim(source, cand_path) if ssim is not None and ssim < minimum_ssim: - candidate_path.unlink(missing_ok=True) + cand_path.unlink(missing_ok=True) continue - if best_candidate is None or candidate_path.stat().st_size < best_candidate[0].stat().st_size: - best_candidate = (candidate_path, strategy) + if best_candidate is None or cand_path.stat().st_size < best_candidate[0].stat().st_size: + best_candidate = (cand_path, strategy, fmt_ext) if best_candidate is None: - for candidate_path, _ in temp_candidates: - candidate_path.unlink(missing_ok=True) + for cand_path, _, _, _ in temp_candidates: + cand_path.unlink(missing_ok=True) raise RuntimeError( f"No image candidate met quality threshold for {relative_path}. " f"Consider lowering image_lossy_min_ssim in config." ) - best_path, best_strategy = best_candidate - shutil.move(str(best_path), str(output)) - for candidate_path, _ in temp_candidates: - if candidate_path != best_path: - candidate_path.unlink(missing_ok=True) - return best_strategy + best_path, best_strategy, winning_ext = best_candidate + # Clean up all losing candidates (those that passed SSIM but weren't the smallest). + for cand_path, _, _, _ in temp_candidates: + if cand_path != best_path: + cand_path.unlink(missing_ok=True) + + # Rename to a clean temp path whose .suffix is the winning format extension + # (.webp or .avif) so the caller can derive the final output path via Path.with_suffix. + clean_tmp = source.parent / f".{source.stem}.win{winning_ext}" + shutil.move(str(best_path), str(clean_tmp)) + return clean_tmp, best_strategy def convert_audio_to_ogg(source: Path, output: Path, config: dict, relative_path: str) -> str: @@ -278,7 +316,10 @@ def optimize(root: Path, config: dict, dry_run: bool) -> dict: try: if ext in {".png", ".jpg", ".jpeg"}: - strategy = convert_image_to_webp(source, temp_output, config=config, relative_path=relative_path) + # convert_image manages its own temp files and returns a clean temp + # whose suffix is the winning format extension (.webp or .avif). + temp_output, strategy = convert_image(source, config=config, relative_path=relative_path) + output = source.with_suffix(temp_output.suffix) elif ext in {".wav", ".mp3"}: strategy = convert_audio_to_ogg(source, temp_output, config=config, relative_path=relative_path) elif ext == ".mp4": From 79b506d96dade373ffbdedd10e5661ab42f735e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 08:33:18 +0000 Subject: [PATCH 3/7] perf: ADPF setPreferPowerEfficiency, GC no-region, nursery-size=8m, OEM game modes, VP9 deadline=best Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/2523e170-1d43-41d8-af53-47db9142dcaa Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- .github/resource-optimizer/config.json | 4 +- .../osu-resources-config.json | 4 +- osu.Android/AndroidManifest.xml | 28 ++++++++++ osu.Android/Native/OboeAudioBridge.cs | 1 + osu.Android/Native/oboe_bridge.cpp | 26 +++++++++ osu.Android/OsuGameAndroid.cs | 6 +++ .../AndroidHighPerformanceSessionManager.cs | 54 +++++++++++++++++++ osu.Android/mono.env | 6 +++ 8 files changed, 125 insertions(+), 4 deletions(-) diff --git a/.github/resource-optimizer/config.json b/.github/resource-optimizer/config.json index 35923aa8996b..364fda56bb9e 100644 --- a/.github/resource-optimizer/config.json +++ b/.github/resource-optimizer/config.json @@ -35,7 +35,7 @@ "audio_opus_frame_duration_ms": 20, "video_webm_crf": 36, "video_webm_audio_bitrate": "96k", - "video_webm_deadline": "good", - "video_webm_cpu_used": 4, + "video_webm_deadline": "best", + "video_webm_cpu_used": 2, "video_webm_tile_columns": 2 } diff --git a/.github/resource-optimizer/osu-resources-config.json b/.github/resource-optimizer/osu-resources-config.json index 6451f8310cb4..c3887fbfa2d2 100644 --- a/.github/resource-optimizer/osu-resources-config.json +++ b/.github/resource-optimizer/osu-resources-config.json @@ -35,7 +35,7 @@ "audio_opus_frame_duration_ms": 20, "video_webm_crf": 36, "video_webm_audio_bitrate": "96k", - "video_webm_deadline": "good", - "video_webm_cpu_used": 4, + "video_webm_deadline": "best", + "video_webm_cpu_used": 2, "video_webm_tile_columns": 2 } diff --git a/osu.Android/AndroidManifest.xml b/osu.Android/AndroidManifest.xml index 3f2b76ec5f82..15ab78b1a3e6 100644 --- a/osu.Android/AndroidManifest.xml +++ b/osu.Android/AndroidManifest.xml @@ -84,6 +84,34 @@ mappings on Legion Phone hardware. --> + + + + + + + + + +