Files
Kenji-NX/src/LibKenjinx/Android/JniExportedMethods.cs
T
2026-01-22 03:33:26 -06:00

1094 lines
42 KiB
C#

using LibKenjinx.Android;
using LibKenjinx.Jni.Pointers;
using Ryujinx.Audio.Backends.OpenAL;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Logging.Targets;
using Ryujinx.HLE;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.Input;
using Silk.NET.Core.Loader;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.KHR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Ryujinx.Graphics.Vulkan;
namespace LibKenjinx
{
public static partial class LibKenjinx
{
private static long _surfacePtr;
private static long _window = 0;
// Remembers the last set renderer size (for the jiggle)
private static int _lastRenderWidth = 0;
private static int _lastRenderHeight = 0;
// NEW: Rotation Debounce + Pending Buffer
private static int _lastRotationDegrees = -1;
private static int _pendingRotationDegrees = -1;
public static VulkanLoader? VulkanLoader { get; private set; }
// ==== Audio Foreground/Background State (reflection-safe) ====
private static bool _audioPaused = false;
private static bool _audioMuted = false;
// strong reference if OpenAL backend is used
private static OpenALHardwareDeviceDriver? _openAl;
// ---------- helpers for broad reflection coverage ----------
private static readonly string[] PauseMethodCandidates =
{
"Pause", "SetPaused", "SetPause", "PauseAll", "RequestPause",
"SetIsPaused", "SetPauseState", "Suspend", "SetSuspended",
"PauseEmulation", "SetEmulationPaused", "SetRunning" // some implementations invert bool
};
private static readonly string[] VolumeMethodCandidates =
{
"SetVolumeMultiplier", "SetVolume", "SetMasterVolume", "SetGain"
};
private static readonly string[] VolumePropertyCandidates =
{
"VolumeMultiplier", "MasterVolume", "Volume", "Gain", "OutputVolume"
};
// Try call method with single bool
private static bool TryCallBool(object target, string[] names, bool arg)
{
if (target == null) return false;
var t = target.GetType();
foreach (var name in names)
{
var m = t.GetMethod(name, new[] { typeof(bool) });
if (m != null)
{
try { m.Invoke(target, new object[] { arg }); return true; } catch { }
}
}
return false;
}
// Try call method with single float
private static bool TryCallFloat(object target, string[] names, float arg)
{
if (target == null) return false;
var t = target.GetType();
foreach (var name in names)
{
var m = t.GetMethod(name, new[] { typeof(float) });
if (m != null)
{
try { m.Invoke(target, new object[] { arg }); return true; } catch { }
}
}
return false;
}
// Try set property (float/double)
private static bool TrySetFloatProp(object target, string[] names, float value)
{
if (target == null) return false;
var t = target.GetType();
foreach (var name in names)
{
var p = t.GetProperty(name);
if (p != null && p.CanWrite)
{
try
{
object v = value;
if (p.PropertyType == typeof(double)) v = (double)value;
p.SetValue(target, v);
return true;
}
catch { }
}
}
return false;
}
// Try set property (bool) for Mute/IsMuted etc.
private static bool TrySetBoolProp(object target, params string[] names)
{
if (target == null) return false;
var t = target.GetType();
foreach (var name in names)
{
var p = t.GetProperty(name);
if (p != null && p.CanWrite && p.PropertyType == typeof(bool))
{
try { p.SetValue(target, true); return true; } catch { }
}
}
return false;
}
// breadth-first walk through "audio-ish" objects accessible from a root
private static IEnumerable<object?> WalkAudioObjects(object? root, int depth = 2)
{
if (root == null || depth < 0) yield break;
yield return root;
var t = root.GetType();
var props = t.GetProperties();
foreach (var p in props)
{
object? val = null;
try { val = p.GetValue(root); } catch { /* ignore */ }
if (val == null) continue;
// Prefer properties that look audio-related or are common containers
if (p.Name.IndexOf("Audio", StringComparison.OrdinalIgnoreCase) >= 0 ||
p.Name.IndexOf("Sound", StringComparison.OrdinalIgnoreCase) >= 0 ||
p.Name.IndexOf("Mixer", StringComparison.OrdinalIgnoreCase) >= 0 ||
p.Name.IndexOf("Output", StringComparison.OrdinalIgnoreCase) >= 0 ||
p.PropertyType.Name.IndexOf("Audio", StringComparison.OrdinalIgnoreCase) >= 0)
{
foreach (var x in WalkAudioObjects(val, depth - 1))
yield return x;
}
}
}
/// <summary>
/// Attempts to pause the entire emulation (strongest guarantee to stop audio).
/// We probe several likely targets via reflection to be compatible across forks.
/// </summary>
private static bool TryPauseEmulation(bool pause)
{
int hits = 0;
try
{
// 1) Try SwitchDevice wrapper itself
var dev = SwitchDevice;
if (dev != null && TryCallBool(dev, PauseMethodCandidates, pause)) hits++;
// 2) Try underlying Switch or similar inner object
var inner =
dev?.GetType().GetProperty("Switch")?.GetValue(dev) ??
dev?.GetType().GetProperty("Device")?.GetValue(dev);
if (inner != null && TryCallBool(inner, PauseMethodCandidates, pause)) hits++;
// 3) Try EmulationContext and its "System" (kernel/front controller etc.)
var ctx = dev?.EmulationContext;
if (ctx != null)
{
if (TryCallBool(ctx, PauseMethodCandidates, pause)) hits++;
var sys = ctx.GetType().GetProperty("System")?.GetValue(ctx);
if (sys != null && TryCallBool(sys, PauseMethodCandidates, pause)) hits++;
// 4) walk all audio-ish descendants and try pause
foreach (var node in WalkAudioObjects(ctx, depth: 2))
{
if (node != null && TryCallBool(node, PauseMethodCandidates, pause))
hits++;
}
}
}
catch { /* ignore */ }
if (hits > 0)
Logger.Info?.Print(LogClass.Application, $"[PauseGate] Emulation pause={pause} hits={hits}");
return hits > 0;
}
/// <summary>
/// Applies paused/muted state. Tries many backends/locations to be
/// resilient across Ryujinx revisions.
/// </summary>
private static void ApplyAudioState()
{
bool shouldMute = _audioPaused || _audioMuted;
float vol = shouldMute ? 0f : 1f;
int hits = 0;
// A) Direct: OpenAL driver (if used)
try
{
var oal = _openAl;
if (oal != null)
{
bool p = TryCallBool(oal, PauseMethodCandidates, _audioPaused);
bool v = TryCallFloat(oal, VolumeMethodCandidates, vol) || TrySetFloatProp(oal, VolumePropertyCandidates, vol);
if (p || v) { hits++; Logger.Trace?.Print(LogClass.Application, $"[AudioGate] OpenAL applied p={p} v={v} vol={vol}"); }
}
}
catch { }
// B) EmulationContext managers (AudioRendererManager/AudioManager/AudioOutManager/…)
try
{
var ctx = SwitchDevice?.EmulationContext;
if (ctx != null)
{
foreach (var node in WalkAudioObjects(ctx, depth: 2))
{
if (node == null) continue;
bool p = TryCallBool(node, PauseMethodCandidates, _audioPaused);
bool v = TryCallFloat(node, VolumeMethodCandidates, vol) || TrySetFloatProp(node, VolumePropertyCandidates, vol);
// also try boolean mute-style properties (IsMuted/Mute)
if (shouldMute)
v = v || TrySetBoolProp(node, "IsMuted", "Muted", "Mute");
if (p || v) hits++;
}
}
}
catch { }
// C) Generic driver fallback (whatever AudioDriver actually is)
try
{
var drv = AudioDriver;
if (drv != null)
{
bool p = TryCallBool(drv, PauseMethodCandidates, _audioPaused);
bool v = TryCallFloat(drv, VolumeMethodCandidates, vol) || TrySetFloatProp(drv, VolumePropertyCandidates, vol);
if (shouldMute) v = v || TrySetBoolProp(drv, "IsMuted", "Muted", "Mute");
if (p || v) hits++;
}
}
catch { }
Logger.Info?.Print(LogClass.Application, $"[AudioGate] applied (hits={hits}) paused={_audioPaused} muted={_audioMuted} → vol={vol}");
// D) fallback: try to pause whole emulation if audio controls not hit
if (hits == 0 && _audioPaused)
{
if (TryPauseEmulation(true))
Logger.Info?.Print(LogClass.Application, "[AudioGate] escalated: Emulation paused");
}
else if (hits == 0 && !_audioPaused)
{
// try resume if we previously paused emulation
if (TryPauseEmulation(false))
Logger.Info?.Print(LogClass.Application, "[AudioGate] escalated: Emulation resumed");
}
}
// =============================================================
[DllImport("libkenjinxjni")]
internal extern static void setRenderingThread();
[DllImport("libkenjinxjni")]
internal extern static void debug_break(int code);
[DllImport("libkenjinxjni")]
internal extern static void setCurrentTransform(long native_window, int transform);
public delegate IntPtr JniCreateSurface(IntPtr native_surface, IntPtr instance);
[UnmanagedCallersOnly(EntryPoint = "javaInitialize")]
public unsafe static bool JniInitialize(IntPtr jpathId, IntPtr jniEnv)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
PlatformInfo.IsBionic = true;
Logger.AddTarget(
new AsyncLogTargetWrapper(
new AndroidLogTarget("RyujinxLog"),
1000,
AsyncLogTargetOverflowAction.Block
));
var path = Marshal.PtrToStringAnsi(jpathId);
var init = Initialize(path);
Interop.Initialize(new JEnvRef(jniEnv));
Interop.Test();
return init;
}
[UnmanagedCallersOnly(EntryPoint = "deviceReloadFilesystem")]
public static void JnaReloadFileSystem()
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call: deviceReloadFilesystem");
SwitchDevice?.ReloadFileSystem();
}
[UnmanagedCallersOnly(EntryPoint = "deviceInitialize")]
public static bool JnaDeviceInitialize(int memoryManagerMode,
bool useNce,
int memoryConfiguration,
int systemLanguage,
int regionCode,
int vSyncMode,
bool enableDockedMode,
bool enablePtc,
bool enableLowPowerPtc,
bool enableJitCacheEviction,
bool enableInternetAccess,
bool enableFsIntegrityChecks,
int fsGlobalAccessLogMode,
IntPtr timeZonePtr,
bool ignoreMissingServices)
{
debug_break(4);
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
AudioDriver = new OpenALHardwareDeviceDriver();
_openAl = AudioDriver as OpenALHardwareDeviceDriver; // <-- audio patch: keep a strong ref
var timezone = Marshal.PtrToStringAnsi(timeZonePtr);
return InitializeDevice((MemoryManagerMode)memoryManagerMode,
useNce,
(MemoryConfiguration)memoryConfiguration,
(SystemLanguage)systemLanguage,
(RegionCode)regionCode,
(VSyncMode)vSyncMode,
enableDockedMode,
enablePtc,
enableLowPowerPtc,
enableJitCacheEviction,
enableInternetAccess,
enableFsIntegrityChecks,
fsGlobalAccessLogMode,
timezone,
ignoreMissingServices);
}
[UnmanagedCallersOnly(EntryPoint = "deviceGetGameFifo")]
public static double JnaGetGameFifo()
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var stats = SwitchDevice?.EmulationContext?.Statistics.GetFifoPercent() ?? 0;
return stats;
}
[UnmanagedCallersOnly(EntryPoint = "deviceGetGameFrameTime")]
public static double JnaGetGameFrameTime()
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var stats = SwitchDevice?.EmulationContext?.Statistics.GetGameFrameTime() ?? 0;
return stats;
}
[UnmanagedCallersOnly(EntryPoint = "deviceGetGameFrameRate")]
public static double JnaGetGameFrameRate()
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var stats = SwitchDevice?.EmulationContext?.Statistics.GetGameFrameRate() ?? 0;
return stats;
}
[UnmanagedCallersOnly(EntryPoint = "deviceLaunchMiiEditor")]
public static bool JNALaunchMiiEditApplet()
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
if (SwitchDevice?.EmulationContext == null)
{
return false;
}
return LaunchMiiEditApplet();
}
[UnmanagedCallersOnly(EntryPoint = "deviceGetDlcContentList")]
public static IntPtr JniGetDlcContentListNative(IntPtr pathPtr, long titleId)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var list = GetDlcContentList(Marshal.PtrToStringAnsi(pathPtr) ?? "", (ulong)titleId);
return CreateStringArray(list);
}
[UnmanagedCallersOnly(EntryPoint = "deviceGetDlcTitleId")]
public static long JniGetDlcTitleIdNative(IntPtr pathPtr, IntPtr ncaPath)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
return Marshal.StringToHGlobalAnsi(GetDlcTitleId(Marshal.PtrToStringAnsi(pathPtr) ?? "", Marshal.PtrToStringAnsi(ncaPath) ?? ""));
}
[UnmanagedCallersOnly(EntryPoint = "deviceSignalEmulationClose")]
public static void JniSignalEmulationCloseNative()
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call: deviceSignalEmulationClose");
SignalEmulationClose();
}
[UnmanagedCallersOnly(EntryPoint = "deviceCloseEmulation")]
public static void JniCloseEmulationNative()
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call: deviceCloseEmulation");
CloseEmulation();
}
[UnmanagedCallersOnly(EntryPoint = "deviceReinitEmulation")]
public static void JniReinitEmulationNative()
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call: deviceReinitEmulation");
ReinitEmulation();
}
[UnmanagedCallersOnly(EntryPoint = "deviceLoadDescriptor")]
public static bool JnaLoadApplicationNative(int descriptor, int type, int updateDescriptor)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
if (SwitchDevice?.EmulationContext == null)
{
return false;
}
var stream = OpenFile(descriptor);
var update = updateDescriptor == -1 ? null : OpenFile(updateDescriptor);
return LoadApplication(stream, (FileType)type, update);
}
[UnmanagedCallersOnly(EntryPoint = "deviceVerifyFirmware")]
public static IntPtr JniVerifyFirmware(int descriptor, bool isXci)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var stream = OpenFile(descriptor);
IntPtr stringHandle = 0;
string? version = "0.0";
try
{
version = VerifyFirmware(stream, isXci)?.VersionString;
}
catch (Exception _)
{
Logger.Error?.Print(LogClass.Service, $"Unable to verify firmware. Exception: {_}");
}
if (version != null)
{
stringHandle = Marshal.StringToHGlobalAnsi(version);
}
return stringHandle;
}
[UnmanagedCallersOnly(EntryPoint = "deviceInstallFirmware")]
public static void JniInstallFirmware(int descriptor, bool isXci)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var stream = OpenFile(descriptor);
InstallFirmware(stream, isXci);
}
[UnmanagedCallersOnly(EntryPoint = "deviceGetInstalledFirmwareVersion")]
public static IntPtr JniGetInstalledFirmwareVersion()
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var version = GetInstalledFirmwareVersion() ?? "0.0";
return Marshal.StringToHGlobalAnsi(version);
}
[UnmanagedCallersOnly(EntryPoint = "graphicsInitialize")]
public static bool JnaGraphicsInitialize(float resScale,
float maxAnisotropy,
bool fastGpuTime,
bool fast2DCopy,
bool enableMacroJit,
bool enableMacroHLE,
bool enableShaderCache,
bool enableTextureRecompression,
int backendThreading)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
SearchPathContainer.Platform = UnderlyingPlatform.Android;
return InitializeGraphics(new GraphicsConfiguration()
{
ResScale = resScale,
MaxAnisotropy = maxAnisotropy,
FastGpuTime = fastGpuTime,
Fast2DCopy = fast2DCopy,
EnableMacroJit = enableMacroJit,
EnableMacroHLE = enableMacroHLE,
EnableShaderCache = enableShaderCache,
EnableTextureRecompression = enableTextureRecompression,
BackendThreading = (BackendThreading)backendThreading
});
}
[UnmanagedCallersOnly(EntryPoint = "graphicsInitializeRenderer")]
public unsafe static bool JnaGraphicsInitializeRenderer(char** extensionsArray,
int extensionsLength,
long driverHandle)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
if (Renderer != null)
{
return false;
}
List<string?> extensions = [];
for (int i = 0; i < extensionsLength; i++)
{
extensions.Add(Marshal.PtrToStringAnsi((IntPtr)extensionsArray[i]));
}
if (driverHandle != 0)
{
VulkanLoader = new VulkanLoader((IntPtr)driverHandle);
}
CreateSurface createSurfaceFunc = instance =>
{
_surfacePtr = Interop.GetSurfacePtr();
_window = Interop.GetWindowsHandle();
var api = VulkanLoader?.GetApi() ?? Vk.GetApi();
if (api.TryGetInstanceExtension(new Instance(instance), out KhrAndroidSurface surfaceExtension))
{
var createInfo = new AndroidSurfaceCreateInfoKHR
{
SType = StructureType.AndroidSurfaceCreateInfoKhr,
Window = (nint*)_surfacePtr,
};
var result = surfaceExtension.CreateAndroidSurface(new Instance(instance), in createInfo, null, out var surface);
// If a rotation was applied before the surface was created → apply it now
if (_window != 0 && _pendingRotationDegrees != -1)
{
try
{
int t = _pendingRotationDegrees switch
{
0 => 0, // IDENTITY
90 => 4, // ROTATE_90
180 => 3, // ROTATE_180 (H|V mirror)
270 => 7, // ROTATE_270 (ROT_90 | H|V)
_ => 0,
};
setCurrentTransform(_window, t);
Logger.Trace?.Print(LogClass.Application, $"[JNI] Apply pending SurfaceTransform {_pendingRotationDegrees}° (t={t}, window=0x{_window:x})");
_lastRotationDegrees = _pendingRotationDegrees;
_pendingRotationDegrees = -1;
}
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.Application, $"Apply pending transform failed: {ex}");
}
}
return (nint)surface.Handle;
}
return IntPtr.Zero;
};
return InitializeGraphicsRenderer(GraphicsBackend.Vulkan, createSurfaceFunc, extensions.ToArray());
}
[UnmanagedCallersOnly(EntryPoint = "graphicsRendererSetSize")]
public static void JnaSetRendererSizeNative(int width, int height)
{
Logger.Trace?.Print(LogClass.Application, $"graphicsRendererSetSize -> {width}x{height}");
_lastRenderWidth = width;
_lastRenderHeight = height;
Renderer?.Window?.SetSize(width, height);
}
[UnmanagedCallersOnly(EntryPoint = "graphicsRendererRunLoop")]
public static void JniRunLoopNative()
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
SetSwapBuffersCallback(() =>
{
if (SwitchDevice?.EmulationContext != null)
{
var time = SwitchDevice.EmulationContext.Statistics.GetGameFrameTime();
Interop.FrameEnded(time);
}
});
RunLoop();
}
[UnmanagedCallersOnly(EntryPoint = "loggingSetEnabled")]
public static void JniSetLoggingEnabledNative(int logLevel, bool enabled)
{
Logger.SetEnable((LogLevel)logLevel, enabled);
}
[UnmanagedCallersOnly(EntryPoint = "loggingEnabledGraphicsLog")]
public static void JniSetLoggingEnabledGraphicsLog(bool enabled)
{
_enableGraphicsLogging = enabled;
}
[UnmanagedCallersOnly(EntryPoint = "deviceGetGameInfo")]
public unsafe static void JniGetGameInfo(int fileDescriptor, IntPtr extension, IntPtr infoPtr)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
using var stream = OpenFile(fileDescriptor);
var ext = Marshal.PtrToStringAnsi(extension);
var info = GetGameInfo(stream, ext?.ToLower() ?? string.Empty) ?? GetDefaultInfo(stream);
var i = (GameInfoNative*)infoPtr;
var n = new GameInfoNative(info);
i->TitleId = n.TitleId;
i->TitleName = n.TitleName;
i->Version = n.Version;
i->FileSize = n.FileSize;
i->Icon = n.Icon;
i->Version = n.Version;
i->Developer = n.Developer;
}
[UnmanagedCallersOnly(EntryPoint = "graphicsRendererSetVsync")]
public static void JnaSetVsyncStateNative(int vSyncMode)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
SetVsyncState((VSyncMode)vSyncMode);
}
[UnmanagedCallersOnly(EntryPoint = "inputInitialize")]
public static void JnaInitializeInput(int width, int height)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
InitializeInput(width, height);
}
[UnmanagedCallersOnly(EntryPoint = "inputSetClientSize")]
public static void JnaSetClientSize(int width, int height)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
SetClientSize(width, height);
}
[UnmanagedCallersOnly(EntryPoint = "inputSetTouchPoint")]
public static void JnaSetTouchPoint(int x, int y)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
SetTouchPoint(x, y);
}
[UnmanagedCallersOnly(EntryPoint = "inputReleaseTouchPoint")]
public static void JnaReleaseTouchPoint()
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
ReleaseTouchPoint();
}
[UnmanagedCallersOnly(EntryPoint = "inputUpdate")]
public static void JniUpdateInput()
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
UpdateInput();
}
[UnmanagedCallersOnly(EntryPoint = "inputSetButtonPressed")]
public static void JnaSetButtonPressed(int button, int id)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
SetButtonPressed((GamepadButtonInputId)button, id);
}
[UnmanagedCallersOnly(EntryPoint = "inputSetButtonReleased")]
public static void JnaSetButtonReleased(int button, int id)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
SetButtonReleased((GamepadButtonInputId)button, id);
}
[UnmanagedCallersOnly(EntryPoint = "inputSetAccelerometerData")]
public static void JniSetAccelerometerData(float x, float y, float z, int id)
{
var accel = new Vector3(x, y, z);
SetAccelerometerData(accel, id);
}
[UnmanagedCallersOnly(EntryPoint = "inputSetGyroData")]
public static void JniSetGyroData(float x, float y, float z, int id)
{
var gryo = new Vector3(x, y, z);
SetGryoData(gryo, id);
}
[UnmanagedCallersOnly(EntryPoint = "inputSetStickAxis")]
public static void JnaSetStickAxis(int stick, float x, float y, int id)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
SetStickAxis((StickInputId)stick, new Vector2(float.IsNaN(x) ? 0 : x, float.IsNaN(y) ? 0 : y), id);
}
[UnmanagedCallersOnly(EntryPoint = "inputConnectGamepad")]
public static int JnaConnectGamepad(int index)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
return ConnectGamepad(index);
}
[UnmanagedCallersOnly(EntryPoint = "userGetOpenedUser")]
public static IntPtr JniGetOpenedUser()
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var userId = GetOpenedUser();
var ptr = Marshal.StringToHGlobalAnsi(userId);
return ptr;
}
[UnmanagedCallersOnly(EntryPoint = "userGetUserPicture")]
public static IntPtr JniGetUserPicture(IntPtr userIdPtr)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var userId = Marshal.PtrToStringAnsi(userIdPtr) ?? "";
return Marshal.StringToHGlobalAnsi(GetUserPicture(userId));
}
[UnmanagedCallersOnly(EntryPoint = "userSetUserPicture")]
public static void JniGetUserPicture(IntPtr userIdPtr, IntPtr picturePtr)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var userId = Marshal.PtrToStringAnsi(userIdPtr) ?? "";
var picture = Marshal.PtrToStringAnsi(picturePtr) ?? "";
SetUserPicture(userId, picture);
}
[UnmanagedCallersOnly(EntryPoint = "userGetUserName")]
public static IntPtr JniGetUserName(IntPtr userIdPtr)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var userId = Marshal.PtrToStringAnsi(userIdPtr) ?? "";
return Marshal.StringToHGlobalAnsi(GetUserName(userId));
}
[UnmanagedCallersOnly(EntryPoint = "userSetUserName")]
public static void JniSetUserName(IntPtr userIdPtr, IntPtr userNamePtr)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var userId = Marshal.PtrToStringAnsi(userIdPtr) ?? "";
var userName = Marshal.PtrToStringAnsi(userNamePtr) ?? "";
SetUserName(userId, userName);
}
[UnmanagedCallersOnly(EntryPoint = "userGetAllUsers")]
public static IntPtr JniGetAllUsers()
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var users = GetAllUsers();
return CreateStringArray(users.ToList());
}
[UnmanagedCallersOnly(EntryPoint = "userAddUser")]
public static void JniAddUser(IntPtr userNamePtr, IntPtr picturePtr)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var userName = Marshal.PtrToStringAnsi(userNamePtr) ?? "";
var picture = Marshal.PtrToStringAnsi(picturePtr) ?? "";
AddUser(userName, picture);
}
[UnmanagedCallersOnly(EntryPoint = "userDeleteUser")]
public static void JniDeleteUser(IntPtr userIdPtr)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var userId = Marshal.PtrToStringAnsi(userIdPtr) ?? "";
DeleteUser(userId);
}
// --- Audio JNI Exports (Foreground/Background gating) ---
[UnmanagedCallersOnly(EntryPoint = "audioSetPaused")]
public static void JniAudioSetPaused(bool paused)
{
_audioPaused = paused;
ApplyAudioState();
}
[UnmanagedCallersOnly(EntryPoint = "audioSetMuted")]
public static void JniAudioSetMuted(bool muted)
{
_audioMuted = muted;
ApplyAudioState();
}
// ---------------------------------------------------------
[UnmanagedCallersOnly(EntryPoint = "uiHandlerSetup")]
public static void JniSetupUiHandler()
{
SetupUiHandler();
}
[UnmanagedCallersOnly(EntryPoint = "uiHandlerSetResponse")]
public static void JniSetUiHandlerResponse(bool isOkPressed, IntPtr input)
{
SetUiHandlerResponse(isOkPressed, Marshal.PtrToStringAnsi(input) ?? "");
}
[UnmanagedCallersOnly(EntryPoint = "userOpenUser")]
public static void JniOpenUser(IntPtr userIdPtr)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var userId = Marshal.PtrToStringAnsi(userIdPtr) ?? "";
OpenUser(userId);
}
[UnmanagedCallersOnly(EntryPoint = "userCloseUser")]
public static void JniCloseUser(IntPtr userIdPtr)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var userId = Marshal.PtrToStringAnsi(userIdPtr) ?? "";
CloseUser(userId);
}
// --- Window Handle Update (Android) ---
[UnmanagedCallersOnly(EntryPoint = "deviceSetWindowHandle")]
public static void JniSetWindowHandle(long handle)
{
_window = handle;
Logger.Trace?.Print(Ryujinx.Common.Logging.LogClass.Application,
$"Window handle updated: 0x{handle:X}");
}
// --- Surface Rotation Bridge (Android) ---
[UnmanagedCallersOnly(EntryPoint = "deviceSetSurfaceRotation")]
public static void JniDeviceSetSurfaceRotation(int degrees)
{
try
{
// Normalize
degrees = degrees switch { 0 => 0, 90 => 90, 180 => 180, 270 => 270, _ => 0 };
if (degrees == _lastRotationDegrees)
{
Logger.Trace?.Print(LogClass.Application, $"[JNI] SurfaceTransform unchanged ({degrees}°), skip");
return;
}
// CORRECT bitmask mapping according to NDK:
// 0 -> 0 (IDENTITY)
// 90 -> 4 (ROTATE_90)
// 180 -> 3 (H|V mirror == 180°)
// 270 -> 7 (ROTATE_270 == ROT_90 | H|V)
int transform = degrees switch
{
0 => 0,
90 => 4,
180 => 3,
270 => 7,
_ => 0
};
if (_window != 0)
{
setCurrentTransform(_window, transform);
_lastRotationDegrees = degrees;
Logger.Trace?.Print(LogClass.Application, $"[JNI] SurfaceTransform -> {degrees}° (t={transform}, window=0x{_window:x})");
}
else
{
_pendingRotationDegrees = degrees; // apply later (see createSurfaceFunc)
Logger.Warning?.Print(LogClass.Application, $"[JNI] deviceSetSurfaceRotation: _window == 0 (pending {degrees}°)");
}
}
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.Application, $"deviceSetSurfaceRotation failed: {ex}");
}
}
// --- Vulkan/GL: Swapchain/Surface Reconfiguration via Size Jiggle ---
[UnmanagedCallersOnly(EntryPoint = "deviceRecreateSwapchain")]
public static void JniDeviceRecreateSwapchain()
{
try
{
Logger.Trace?.Print(LogClass.Application, "[JNI] deviceRecreateSwapchain");
if (Renderer?.Window == null)
{
Logger.Warning?.Print(LogClass.Application, "[JNI] deviceRecreateSwapchain: Renderer.Window == null");
return;
}
int w = _lastRenderWidth;
int h = _lastRenderHeight;
if (w > 0 && h > 0)
{
int jiggleW = w;
int jiggleH = h;
if (w <= h) jiggleW = Math.Max(1, w - 1); else jiggleH = Math.Max(1, h - 1);
Logger.Trace?.Print(LogClass.Application, $"[JNI] deviceRecreateSwapchain: jiggle {jiggleW}x{jiggleH} -> {w}x{h}");
Renderer.Window.SetSize(jiggleW, jiggleH);
Renderer.Window.SetSize(w, h);
}
else
{
Logger.Trace?.Print(LogClass.Application, "[JNI] deviceRecreateSwapchain: unknown last size -> 1x1 -> 2x2 jiggle");
Renderer.Window.SetSize(1, 1);
Renderer.Window.SetSize(2, 2);
}
}
catch (Exception ex)
{
Logger.Error?.Print(LogClass.Application, $"deviceRecreateSwapchain failed: {ex}");
}
}
// ===== PresentAllowed / Surface Control (JNI) =====
// Alias for older calls, if available
[UnmanagedCallersOnly(EntryPoint = "graphicsRendererSetPresent")]
public static void JniGraphicsRendererSetPresent(bool enabled)
{
try
{
if (Renderer is VulkanRenderer vr)
{
vr.SetPresentEnabled(enabled);
Logger.Trace?.Print(LogClass.Application, $"[JNI] PresentEnabled = {enabled}");
}
}
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.Application, $"graphicsRendererSetPresent failed: {ex}");
}
}
// New name: Matches KenjinxNative.graphicsSetPresentEnabled(...)
[UnmanagedCallersOnly(EntryPoint = "graphicsSetPresentEnabled")]
public static void JniGraphicsSetPresentEnabled(bool enabled)
{
try
{
(Renderer as VulkanRenderer)?.SetPresentEnabled(enabled);
Logger.Trace?.Print(LogClass.Application, $"[JNI] graphicsSetPresentEnabled({enabled})");
}
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.Application, $"graphicsSetPresentEnabled failed: {ex}");
}
}
[UnmanagedCallersOnly(EntryPoint = "graphicsRendererRecreateSurface")]
public static void JniGraphicsRendererRecreateSurface()
{
try
{
Logger.Trace?.Print(LogClass.Application, "[JNI] graphicsRendererRecreateSurface");
_ = (Renderer as VulkanRenderer)?.RecreateSurface();
}
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.Application, $"graphicsRendererRecreateSurface failed: {ex}");
}
}
#if ANDROID
[UnmanagedCallersOnly(EntryPoint = "graphicsSetFullscreenStretch")]
public static void JniGraphicsSetFullscreenStretch(bool enable)
{
Logger.Trace?.Print(LogClass.Application, $"[JNI] graphicsSetFullscreenStretch({enable})");
ApplyFullscreenStretch(enable);
}
#endif
// Used by MainActivity/GameHost
[UnmanagedCallersOnly(EntryPoint = "reattachWindowIfReady")]
public static bool JniReattachWindowIfReady()
{
try
{
return (Renderer as VulkanRenderer)?.RecreateSurface() ?? false;
}
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.Application, $"reattachWindowIfReady failed: {ex}");
return false;
}
}
[UnmanagedCallersOnly(EntryPoint = "detachWindow")]
public static void JniDetachWindow()
{
try
{
(Renderer as VulkanRenderer)?.ReleaseSurface();
}
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.Application, $"detachWindow failed: {ex}");
}
}
// ===== Amiibo JNI Exports =====
[UnmanagedCallersOnly(EntryPoint = "amiiboLoadBin")]
public static bool JniAmiiboLoadBin(IntPtr dataPtr, int length)
{
if (dataPtr == IntPtr.Zero || length <= 0) return false;
try
{
byte[] buf = new byte[length];
Marshal.Copy(dataPtr, buf, 0, length);
return AmiiboLoadFromBytes(buf);
}
catch
{
return false;
}
}
[UnmanagedCallersOnly(EntryPoint = "amiiboClear")]
public static void JniAmiiboClear()
{
AmiiboClear();
}
// ===== End Amiibo JNI Exports =====
}
internal static partial class Logcat
{
[LibraryImport("liblog", StringMarshalling = StringMarshalling.Utf8)]
private static partial void __android_log_print(LogLevel level, string? tag, string format, string args, IntPtr ptr);
internal static void AndroidLogPrint(LogLevel level, string? tag, string message) =>
__android_log_print(level, tag, "%s", message, IntPtr.Zero);
internal enum LogLevel
{
Unknown = 0x00,
Default = 0x01,
Verbose = 0x02,
Debug = 0x03,
Info = 0x04,
Warn = 0x05,
Error = 0x06,
Fatal = 0x07,
Silent = 0x08,
}
}
}