mirror of
https://github.com/meowarex/rl-mobile.git
synced 2026-08-26 14:07:45 +10:00
Kawarp Shader <3
This commit is contained in:
Executable
+89
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compiles the Java-authored extension classes down to the hand-editable smali that the Manager
|
||||
# injects. Everything under stubs/ exists only to satisfy javac - those classes are already in the
|
||||
# TIDAL dex, so they are on the classpath but never compiled into the output.
|
||||
#
|
||||
# ./build.sh # regenerate ../../patches/extension/radiant/Kawarp*.smali
|
||||
#
|
||||
# Override JAVA_HOME / ANDROID_JAR / SMALI_TOOLS if your toolchain lives elsewhere.
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo="$(cd "$here/../.." && pwd)"
|
||||
|
||||
JAVA_HOME="${JAVA_HOME:-/home/meow/toolchain/jdk-21.0.12+8}"
|
||||
ANDROID_SDK="${ANDROID_SDK:-/home/meow/toolchain/android-sdk}"
|
||||
ANDROID_JAR="${ANDROID_JAR:-$ANDROID_SDK/platforms/android-36/android.jar}"
|
||||
D8="${D8:-$(ls -d "$ANDROID_SDK"/build-tools/*/d8 | sort | tail -1)}"
|
||||
BAKSMALI="${BAKSMALI:-/home/meow/tools/baksmali.jar}"
|
||||
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
javac="$JAVA_HOME/bin/javac"
|
||||
java="$JAVA_HOME/bin/java"
|
||||
out="$here/build"
|
||||
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out/stubs" "$out/classes" "$out/dex" "$out/smali"
|
||||
|
||||
echo "==> javac stubs"
|
||||
find "$here/stubs" -name '*.java' > "$out/stubs.list"
|
||||
"$javac" -nowarn -g:none --release 8 -cp "$ANDROID_JAR" -d "$out/stubs" @"$out/stubs.list" 2>&1 |
|
||||
grep -v 'bootstrap class path\|source value 8\|target value 8\|deprecat' || true
|
||||
|
||||
# The Kawarp engine lives in the Kawarp-AGSL library repo and is compiled into this bundle.
|
||||
# Expected checkout location (gitignored): tools/kawarp-agsl — clone it there, or point
|
||||
# KAWARP_AGSL elsewhere.
|
||||
KAWARP_AGSL="${KAWARP_AGSL:-$repo/tools/kawarp-agsl}"
|
||||
ENGINE="$KAWARP_AGSL/kawarp/src/main/java/dev/kawarp/KawarpEngine.java"
|
||||
[ -f "$ENGINE" ] || { echo "!! Kawarp-AGSL checkout not found at $KAWARP_AGSL" >&2
|
||||
echo " git clone https://github.com/meowarex/kawarp-agsl tools/kawarp-agsl" >&2
|
||||
exit 1; }
|
||||
|
||||
echo "==> javac sources"
|
||||
{ find "$here/src" -name '*.java'; echo "$ENGINE"; } > "$out/src.list"
|
||||
"$javac" -nowarn -g:none --release 8 -cp "$ANDROID_JAR:$out/stubs" -d "$out/classes" @"$out/src.list" 2>&1 |
|
||||
grep -v 'bootstrap class path\|source value 8\|target value 8\|deprecat' || true
|
||||
|
||||
echo "==> d8"
|
||||
find "$out/classes" -name '*.class' > "$out/class.list"
|
||||
"$D8" --min-api 24 --no-desugaring --lib "$ANDROID_JAR" --classpath "$out/stubs" \
|
||||
--output "$out/dex" @"$out/class.list"
|
||||
|
||||
echo "==> baksmali"
|
||||
"$java" -jar "$BAKSMALI" d "$out/dex/classes.dex" -o "$out/smali"
|
||||
|
||||
echo "==> post-process"
|
||||
# Kotlin mangles value-class accessors with characters that are illegal in Java identifiers, so
|
||||
# the stubs declare legal names and we restore the real ones here.
|
||||
# The 0x5241xxxx sentinels are unfoldable stand-ins for the Manager's option placeholders.
|
||||
find "$out/smali" -name '*.smali' -print0 | xargs -0 sed -i \
|
||||
-e 's/getSizeNHjbRc/getSize-NH-jbRc/g' \
|
||||
-e 's/getWidthImpl/getWidth-impl/g' \
|
||||
-e 's/getHeightImpl/getHeight-impl/g' \
|
||||
-e 's/0x52410001/__RL_KW_WARP__/g' \
|
||||
-e 's/0x52410002/__RL_KW_SPEED__/g' \
|
||||
-e 's/0x52410003/__RL_KW_SCALE__/g' \
|
||||
-e 's/0x52410004/__RL_KW_DITHER__/g' \
|
||||
-e 's/0x52410005/__RL_KW_DARKEN__/g' \
|
||||
-e 's/0x52410006/__RL_KW_REACTIVE__/g' \
|
||||
-e 's/0x52410007/__RL_KW_BLUR__/g' \
|
||||
-e 's/0x52410008/__RL_KW_CONTRAST__/g' \
|
||||
-e 's/0x52410009/__RL_KW_SAT__/g' \
|
||||
-e 's/0x5241000a/__RL_KW_BRIGHT__/g'
|
||||
|
||||
leftover=$(grep -rl '0x5241000[0-9a-f]' "$out/smali" || true)
|
||||
if [ -n "$leftover" ]; then
|
||||
echo "!! un-tokenised sentinel left in: $leftover" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dest="$repo/patches/extension"
|
||||
# Clear previous outputs first so a refactor that drops a class can't leave a stale file behind
|
||||
# (stale extension smali still gets bundled and can break assembly).
|
||||
rm -f "$dest"/radiant/Kawarp*.smali "$dest"/dev/kawarp/*.smali
|
||||
mkdir -p "$dest/dev/kawarp"
|
||||
for f in "$out"/smali/radiant/Kawarp*.smali "$out"/smali/dev/kawarp/*.smali; do
|
||||
rel="${f#"$out"/smali/}"
|
||||
cp "$f" "$dest/$rel"
|
||||
echo " -> patches/extension/$rel"
|
||||
done
|
||||
@@ -0,0 +1,225 @@
|
||||
package radiant;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.view.Choreographer;
|
||||
|
||||
import androidx.compose.foundation.layout.SizeKt;
|
||||
import androidx.compose.foundation.layout.SpacerKt;
|
||||
import androidx.compose.runtime.Composer;
|
||||
import androidx.compose.runtime.MutableState;
|
||||
import androidx.compose.runtime.SnapshotStateKt;
|
||||
import androidx.compose.ui.Modifier;
|
||||
import androidx.compose.ui.draw.DrawModifierKt;
|
||||
import androidx.compose.ui.geometry.Size;
|
||||
import androidx.compose.ui.graphics.AndroidCanvas_androidKt;
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope;
|
||||
|
||||
import com.tidal.android.feature.playerscreen.ui.composables.p3;
|
||||
import com.tidal.android.feature.playerscreen.ui.model.PlayerBackgroundStyle;
|
||||
|
||||
import dev.kawarp.KawarpEngine;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* MARKER: Kawarp Backdrop - TIDAL glue over dev.kawarp.KawarpEngine (Kawarp-AGSL library)
|
||||
*
|
||||
* The engine (shader, blur pipeline, settings, crossfade) lives in the Kawarp-AGSL library
|
||||
* (cloned at tools/kawarp-agsl) and is compiled into this bundle by tools/kawarp-build/build.sh;
|
||||
* this class only holds what is TIDAL/Compose-specific:
|
||||
* the patched-in composable entry points, cover fetching by TIDAL uuid, the Choreographer ->
|
||||
* snapshot-state frame clock, the Manager's option tokens, and the fallback to TIDAL's own
|
||||
* blurred backdrop.
|
||||
*
|
||||
* One class on purpose (its own draw lambda, frame callback and loader) - see the engine's
|
||||
* class doc for the dex-injection constraints.
|
||||
*/
|
||||
public final class Kawarp implements am0.l, Runnable, Choreographer.FrameCallback {
|
||||
|
||||
private static final String TAG = "RLKawarp";
|
||||
/** Stop the frame loop once nothing has drawn for this long (player left / backgrounded). */
|
||||
private static final long IDLE_STOP_MS = 500L;
|
||||
|
||||
/**
|
||||
* Patch option plumbing. The 0x5241xxxx literals are sentinels that build.sh rewrites into
|
||||
* __RL_*__ placeholders for the Manager to substitute; routing them through tk() stops javac
|
||||
* constant-folding them out of existence.
|
||||
*/
|
||||
private static int tk(int v) { return v; }
|
||||
|
||||
private static final ExecutorService LOADER = Executors.newSingleThreadExecutor();
|
||||
|
||||
/** Bumped every frame so the draw lambda's snapshot read invalidates the draw phase. */
|
||||
private static final MutableState frameState =
|
||||
SnapshotStateKt.mutableStateOf$default(Integer.valueOf(0), null, 2, null);
|
||||
/** Flips true on any shader failure; read during composition so we recompose onto p3.a. */
|
||||
private static final MutableState fallbackState =
|
||||
SnapshotStateKt.mutableStateOf$default(Boolean.FALSE, null, 2, null);
|
||||
|
||||
/** The shared instance render passes to drawBehind (also serves as the frame callback). */
|
||||
private static final Kawarp DRAW = new Kawarp(null, 0);
|
||||
|
||||
private static KawarpEngine engine;
|
||||
private static volatile String requestedUuid;
|
||||
private static volatile int loadToken;
|
||||
|
||||
private static long lastDrawUptime;
|
||||
private static boolean frameScheduled, loggedDraw;
|
||||
private static int frameTick;
|
||||
|
||||
/** Set when this instance is a queued cover load; null/0 on the shared DRAW instance. */
|
||||
private final String uuid;
|
||||
private final int token;
|
||||
|
||||
private Kawarp(String uuid, int token) {
|
||||
this.uuid = uuid;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
|
||||
// Composition
|
||||
|
||||
|
||||
/**
|
||||
* Drop-in replacement for TIDAL's PlayerBackground composable (p3.b) - the patch redirects
|
||||
* the call site here so the whole backdrop works <3
|
||||
*/
|
||||
public static void b(PlayerBackgroundStyle style, long color, int albumId, String coverUuid,
|
||||
boolean isPlaying, boolean isSeeking, am0.a progress, Modifier modifier,
|
||||
Composer composer, int changed) {
|
||||
render(albumId, coverUuid, isPlaying, modifier, composer);
|
||||
}
|
||||
|
||||
/** Shared by the player screen and by Cover Everywhere (radiant/HomeBackdrop). */
|
||||
public static void render(int albumId, String coverUuid, boolean isPlaying,
|
||||
Modifier modifier, Composer composer) {
|
||||
composer.startReplaceGroup(0x52415750);
|
||||
boolean ok = !((Boolean) fallbackState.getValue()).booleanValue() && ensureEngine();
|
||||
if (!ok) {
|
||||
// No (working) AGSL here - fall back to TIDAL's own blurred backdrop.
|
||||
p3.a(albumId, coverUuid, isPlaying, false, null, modifier, composer, 0);
|
||||
} else {
|
||||
engine.setPlaying(isPlaying);
|
||||
request(coverUuid);
|
||||
Modifier m = DrawModifierKt.drawBehind(SizeKt.fillMaxSize(modifier, 1.0f), DRAW);
|
||||
SpacerKt.Spacer(m, composer, 0);
|
||||
}
|
||||
composer.endReplaceGroup();
|
||||
}
|
||||
|
||||
/** Engine construction compiles the AGSL so this doubles as the support probe. */
|
||||
private static boolean ensureEngine() {
|
||||
if (engine != null) return true;
|
||||
if (!KawarpEngine.isSupported()) return false;
|
||||
try {
|
||||
KawarpEngine e = new KawarpEngine();
|
||||
e.setWarpIntensity(Float.intBitsToFloat(tk(0x52410001)));
|
||||
e.setAnimationSpeed(Float.intBitsToFloat(tk(0x52410002)));
|
||||
e.setScale(Float.intBitsToFloat(tk(0x52410003)));
|
||||
e.setDithering(Float.intBitsToFloat(tk(0x52410004)));
|
||||
e.setAutoDarken(Float.intBitsToFloat(tk(0x52410005)));
|
||||
e.setPlaybackReactive(tk(0x52410006) != 0);
|
||||
e.setBlurPasses(tk(0x52410007));
|
||||
e.setContrast(Float.intBitsToFloat(tk(0x52410008)));
|
||||
e.setSaturation(Float.intBitsToFloat(tk(0x52410009)));
|
||||
e.setBrightness(Float.intBitsToFloat(tk(0x5241000a)));
|
||||
engine = e;
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
Log.i(TAG, "AGSL rejected, staying on the native backdrop: " + t);
|
||||
fallbackState.setValue(Boolean.TRUE);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Cover loading (Runnable on LOADER)
|
||||
|
||||
|
||||
private static void request(String coverUuid) {
|
||||
if (coverUuid == null || coverUuid.length() == 0 || coverUuid.equals(requestedUuid)) return;
|
||||
requestedUuid = coverUuid;
|
||||
LOADER.execute(new Kawarp(coverUuid, ++loadToken));
|
||||
}
|
||||
|
||||
public void run() {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
// Same URL shape TIDAL itself builds in he0.a; 640 is plenty ahead of a 128 downscale.
|
||||
URL url = new URL("https://resources.tidal.com/images/"
|
||||
+ uuid.replace('-', '/') + "/640x640.jpg");
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setConnectTimeout(10000);
|
||||
connection.setReadTimeout(15000);
|
||||
InputStream stream = connection.getInputStream();
|
||||
Bitmap source;
|
||||
try {
|
||||
source = BitmapFactory.decodeStream(stream);
|
||||
} finally {
|
||||
stream.close();
|
||||
}
|
||||
if (source == null || token != loadToken) return;
|
||||
engine.setCover(source);
|
||||
source.recycle();
|
||||
Log.i(TAG, "cover submitted: " + uuid);
|
||||
} catch (Throwable t) {
|
||||
Log.i(TAG, "cover load failed: " + t);
|
||||
// Let the next track (or a re-entry into the player) retry this cover.
|
||||
if (token == loadToken) requestedUuid = null;
|
||||
} finally {
|
||||
if (connection != null) connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Frame loop
|
||||
|
||||
|
||||
public void doFrame(long frameTimeNanos) {
|
||||
frameScheduled = false;
|
||||
// Nothing has drawn for a while: the backdrop is off-screen, so stop burning frames.
|
||||
if (SystemClock.uptimeMillis() - lastDrawUptime > IDLE_STOP_MS) return;
|
||||
frameState.setValue(Integer.valueOf(++frameTick));
|
||||
schedule();
|
||||
}
|
||||
|
||||
private static void schedule() {
|
||||
if (frameScheduled) return;
|
||||
frameScheduled = true;
|
||||
Choreographer.getInstance().postFrameCallback(DRAW);
|
||||
}
|
||||
|
||||
public Object invoke(Object scope) {
|
||||
draw((DrawScope) scope);
|
||||
return kotlin.u.a;
|
||||
}
|
||||
|
||||
private static void draw(DrawScope scope) {
|
||||
// Snapshot read: this is what ties the draw phase to the frame clock.
|
||||
frameState.getValue();
|
||||
lastDrawUptime = SystemClock.uptimeMillis();
|
||||
schedule();
|
||||
|
||||
long packed = scope.getSizeNHjbRc();
|
||||
float width = Size.getWidthImpl(packed);
|
||||
float height = Size.getHeightImpl(packed);
|
||||
if (!loggedDraw) {
|
||||
loggedDraw = true;
|
||||
Log.i(TAG, "first draw: " + width + "x" + height + " ready=" + engine.isReady());
|
||||
}
|
||||
try {
|
||||
engine.draw(AndroidCanvas_androidKt.getNativeCanvas(scope.getDrawContext().getCanvas()),
|
||||
width, height);
|
||||
} catch (Throwable t) {
|
||||
Log.i(TAG, "AGSL draw failed, falling back: " + t);
|
||||
fallbackState.setValue(Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package am0;
|
||||
/** kotlin.jvm.functions.Function0 (obfuscated in the TIDAL dex). */
|
||||
public interface a { Object invoke(); }
|
||||
@@ -0,0 +1,3 @@
|
||||
package am0;
|
||||
/** kotlin.jvm.functions.Function1 (obfuscated in the TIDAL dex). */
|
||||
public interface l { Object invoke(Object p1); }
|
||||
@@ -0,0 +1,5 @@
|
||||
package androidx.compose.foundation.layout;
|
||||
import androidx.compose.ui.Modifier;
|
||||
public final class SizeKt {
|
||||
public static Modifier fillMaxSize(Modifier modifier, float fraction) { return null; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package androidx.compose.foundation.layout;
|
||||
import androidx.compose.runtime.Composer;
|
||||
import androidx.compose.ui.Modifier;
|
||||
public final class SpacerKt {
|
||||
public static void Spacer(Modifier modifier, Composer composer, int changed) {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package androidx.compose.runtime;
|
||||
public interface Composer {
|
||||
void startReplaceGroup(int key);
|
||||
void endReplaceGroup();
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package androidx.compose.runtime;
|
||||
public interface MutableState extends State { void setValue(Object value); }
|
||||
@@ -0,0 +1,2 @@
|
||||
package androidx.compose.runtime;
|
||||
public interface SnapshotMutationPolicy {}
|
||||
@@ -0,0 +1,4 @@
|
||||
package androidx.compose.runtime;
|
||||
public final class SnapshotStateKt {
|
||||
public static MutableState mutableStateOf$default(Object value, SnapshotMutationPolicy policy, int flags, Object marker) { return null; }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package androidx.compose.runtime;
|
||||
public interface State { Object getValue(); }
|
||||
@@ -0,0 +1,6 @@
|
||||
package androidx.compose.ui;
|
||||
public interface Modifier {
|
||||
/** Non-constant initialiser so javac emits a real sget-object. */
|
||||
Companion Companion = new Companion();
|
||||
final class Companion implements Modifier {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package androidx.compose.ui.draw;
|
||||
import androidx.compose.ui.Modifier;
|
||||
public final class DrawModifierKt {
|
||||
public static Modifier drawBehind(Modifier modifier, am0.l onDraw) { return null; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package androidx.compose.ui.geometry;
|
||||
public final class Size {
|
||||
/** Renamed to getWidth-impl by build.sh (Kotlin value-class mangling). */
|
||||
public static float getWidthImpl(long packed) { return 0f; }
|
||||
/** Renamed to getHeight-impl by build.sh (Kotlin value-class mangling). */
|
||||
public static float getHeightImpl(long packed) { return 0f; }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package androidx.compose.ui.graphics;
|
||||
public final class AndroidCanvas_androidKt {
|
||||
public static android.graphics.Canvas getNativeCanvas(Canvas canvas) { return null; }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package androidx.compose.ui.graphics;
|
||||
public interface Canvas {}
|
||||
@@ -0,0 +1,3 @@
|
||||
package androidx.compose.ui.graphics.drawscope;
|
||||
import androidx.compose.ui.graphics.Canvas;
|
||||
public interface DrawContext { Canvas getCanvas(); }
|
||||
@@ -0,0 +1,6 @@
|
||||
package androidx.compose.ui.graphics.drawscope;
|
||||
public interface DrawScope {
|
||||
DrawContext getDrawContext();
|
||||
/** Renamed to getSize-NH-jbRc by build.sh (Kotlin value-class mangling). */
|
||||
long getSizeNHjbRc();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.tidal.android.feature.playerscreen.ui.composables;
|
||||
import androidx.compose.runtime.Composer;
|
||||
import androidx.compose.ui.Modifier;
|
||||
/** TIDAL's DarkBlurBackground composable (used as the pre-Android-13 fallback). */
|
||||
public final class p3 {
|
||||
public static void a(int albumId, String coverUuid, boolean isPlaying, boolean isSeeking,
|
||||
am0.a progress, Modifier modifier, Composer composer, int changed) {}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
package com.tidal.android.feature.playerscreen.ui.model;
|
||||
public final class PlayerBackgroundStyle {}
|
||||
@@ -0,0 +1,3 @@
|
||||
package kotlin;
|
||||
/** kotlin.Unit (obfuscated in the TIDAL dex). */
|
||||
public final class u { public static final u a = null; }
|
||||
Reference in New Issue
Block a user