mirror of
https://git.ryujinx.app/projects/Kenji-NX.git
synced 2026-09-20 17:51:13 +02:00
1097 lines
42 KiB
C#
1097 lines
42 KiB
C#
using LibKenjinx.Android;
|
|
using LibKenjinx.Jni.Pointers;
|
|
using Ryujinx.Audio.Backends.OpenAL;
|
|
using Ryujinx.Audio.Integration;
|
|
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;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
|
|
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;
|
|
Type t = target.GetType();
|
|
foreach (string name in names)
|
|
{
|
|
MethodInfo? 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;
|
|
Type t = target.GetType();
|
|
foreach (string name in names)
|
|
{
|
|
MethodInfo? 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;
|
|
Type t = target.GetType();
|
|
foreach (string name in names)
|
|
{
|
|
PropertyInfo? 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;
|
|
Type t = target.GetType();
|
|
foreach (string name in names)
|
|
{
|
|
PropertyInfo? 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;
|
|
|
|
Type t = root.GetType();
|
|
PropertyInfo[] props = t.GetProperties();
|
|
foreach (PropertyInfo 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 (object? 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
|
|
SwitchDevice? dev = SwitchDevice;
|
|
if (dev != null && TryCallBool(dev, PauseMethodCandidates, pause)) hits++;
|
|
|
|
// 2) Try underlying Switch or similar inner object
|
|
object? 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.)
|
|
Switch? ctx = dev?.EmulationContext;
|
|
if (ctx != null)
|
|
{
|
|
if (TryCallBool(ctx, PauseMethodCandidates, pause)) hits++;
|
|
|
|
object? 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 (object? 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
|
|
{
|
|
OpenALHardwareDeviceDriver? 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
|
|
{
|
|
Switch? ctx = SwitchDevice?.EmulationContext;
|
|
if (ctx != null)
|
|
{
|
|
foreach (object? 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
|
|
{
|
|
IHardwareDeviceDriver? 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 nint JniCreateSurface(nint native_surface, nint instance);
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "javaInitialize")]
|
|
public unsafe static bool JniInitialize(nint jpathId, nint jniEnv)
|
|
{
|
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
|
PlatformInfo.IsBionic = true;
|
|
|
|
Logger.AddTarget(
|
|
new AsyncLogTargetWrapper(
|
|
new AndroidLogTarget("RyujinxLog"),
|
|
1000,
|
|
AsyncLogTargetOverflowAction.Block
|
|
));
|
|
|
|
string? path = Marshal.PtrToStringAnsi(jpathId);
|
|
|
|
bool 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,
|
|
nint 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
|
|
|
|
string? 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");
|
|
double stats = SwitchDevice?.EmulationContext?.Statistics.GetFifoPercent() ?? 0;
|
|
|
|
return stats;
|
|
}
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "deviceGetGameFrameTime")]
|
|
public static double JnaGetGameFrameTime()
|
|
{
|
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
|
double stats = SwitchDevice?.EmulationContext?.Statistics.GetGameFrameTime() ?? 0;
|
|
|
|
return stats;
|
|
}
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "deviceGetGameFrameRate")]
|
|
public static double JnaGetGameFrameRate()
|
|
{
|
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
|
double 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 nint JniGetDlcContentListNative(nint pathPtr, long titleId)
|
|
{
|
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
|
List<string> list = GetDlcContentList(Marshal.PtrToStringAnsi(pathPtr) ?? "", (ulong)titleId);
|
|
|
|
return CreateStringArray(list);
|
|
}
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "deviceGetDlcTitleId")]
|
|
public static long JniGetDlcTitleIdNative(nint pathPtr, nint 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;
|
|
}
|
|
|
|
FileStream stream = OpenFile(descriptor);
|
|
FileStream? update = updateDescriptor == -1 ? null : OpenFile(updateDescriptor);
|
|
|
|
return LoadApplication(stream, (FileType)type, update);
|
|
}
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "deviceVerifyFirmware")]
|
|
public static nint JniVerifyFirmware(int descriptor, bool isXci)
|
|
{
|
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
|
|
|
FileStream stream = OpenFile(descriptor);
|
|
|
|
nint 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");
|
|
|
|
FileStream stream = OpenFile(descriptor);
|
|
|
|
InstallFirmware(stream, isXci);
|
|
}
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "deviceGetInstalledFirmwareVersion")]
|
|
public static nint JniGetInstalledFirmwareVersion()
|
|
{
|
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
|
|
|
string 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((nint)extensionsArray[i]));
|
|
}
|
|
|
|
if (driverHandle != 0)
|
|
{
|
|
VulkanLoader = new VulkanLoader((nint)driverHandle);
|
|
}
|
|
|
|
CreateSurface createSurfaceFunc = instance =>
|
|
{
|
|
_surfacePtr = Interop.GetSurfacePtr();
|
|
_window = Interop.GetWindowsHandle();
|
|
|
|
Vk api = VulkanLoader?.GetApi() ?? Vk.GetApi();
|
|
if (api.TryGetInstanceExtension(new Instance(instance), out KhrAndroidSurface surfaceExtension))
|
|
{
|
|
AndroidSurfaceCreateInfoKHR createInfo = new()
|
|
{
|
|
SType = StructureType.AndroidSurfaceCreateInfoKhr,
|
|
Window = (nint*)_surfacePtr,
|
|
};
|
|
|
|
Result result = surfaceExtension.CreateAndroidSurface(new Instance(instance), in createInfo, null, out SurfaceKHR 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 nint.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)
|
|
{
|
|
double 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, nint extension, nint infoPtr)
|
|
{
|
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
|
using FileStream stream = OpenFile(fileDescriptor);
|
|
string? ext = Marshal.PtrToStringAnsi(extension);
|
|
GameInfo info = GetGameInfo(stream, ext?.ToLower() ?? string.Empty) ?? GetDefaultInfo(stream);
|
|
GameInfoNative* i = (GameInfoNative*)infoPtr;
|
|
GameInfoNative n = new(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)
|
|
{
|
|
Vector3 accel = new(x, y, z);
|
|
SetAccelerometerData(accel, id);
|
|
}
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "inputSetGyroData")]
|
|
public static void JniSetGyroData(float x, float y, float z, int id)
|
|
{
|
|
Vector3 gryo = new(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 nint JniGetOpenedUser()
|
|
{
|
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
|
string userId = GetOpenedUser();
|
|
IntPtr ptr = Marshal.StringToHGlobalAnsi(userId);
|
|
|
|
return ptr;
|
|
}
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "userGetUserPicture")]
|
|
public static nint JniGetUserPicture(nint userIdPtr)
|
|
{
|
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
|
string userId = Marshal.PtrToStringAnsi(userIdPtr) ?? "";
|
|
|
|
return Marshal.StringToHGlobalAnsi(GetUserPicture(userId));
|
|
}
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "userSetUserPicture")]
|
|
public static void JniGetUserPicture(nint userIdPtr, nint picturePtr)
|
|
{
|
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
|
string userId = Marshal.PtrToStringAnsi(userIdPtr) ?? "";
|
|
string picture = Marshal.PtrToStringAnsi(picturePtr) ?? "";
|
|
|
|
SetUserPicture(userId, picture);
|
|
}
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "userGetUserName")]
|
|
public static nint JniGetUserName(nint userIdPtr)
|
|
{
|
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
|
string userId = Marshal.PtrToStringAnsi(userIdPtr) ?? "";
|
|
|
|
return Marshal.StringToHGlobalAnsi(GetUserName(userId));
|
|
}
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "userSetUserName")]
|
|
public static void JniSetUserName(nint userIdPtr, nint userNamePtr)
|
|
{
|
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
|
string userId = Marshal.PtrToStringAnsi(userIdPtr) ?? "";
|
|
string userName = Marshal.PtrToStringAnsi(userNamePtr) ?? "";
|
|
|
|
SetUserName(userId, userName);
|
|
}
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "userGetAllUsers")]
|
|
public static nint JniGetAllUsers()
|
|
{
|
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
|
string[] users = GetAllUsers();
|
|
|
|
return CreateStringArray(users.ToList());
|
|
}
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "userAddUser")]
|
|
public static void JniAddUser(nint userNamePtr, nint picturePtr)
|
|
{
|
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
|
string userName = Marshal.PtrToStringAnsi(userNamePtr) ?? "";
|
|
string picture = Marshal.PtrToStringAnsi(picturePtr) ?? "";
|
|
|
|
AddUser(userName, picture);
|
|
}
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "userDeleteUser")]
|
|
public static void JniDeleteUser(nint userIdPtr)
|
|
{
|
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
|
string 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, nint input)
|
|
{
|
|
SetUiHandlerResponse(isOkPressed, Marshal.PtrToStringAnsi(input) ?? "");
|
|
}
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "userOpenUser")]
|
|
public static void JniOpenUser(nint userIdPtr)
|
|
{
|
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
|
string userId = Marshal.PtrToStringAnsi(userIdPtr) ?? "";
|
|
|
|
OpenUser(userId);
|
|
}
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "userCloseUser")]
|
|
public static void JniCloseUser(nint userIdPtr)
|
|
{
|
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
|
string 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(nint dataPtr, int length)
|
|
{
|
|
if (dataPtr == nint.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, nint ptr);
|
|
|
|
internal static void AndroidLogPrint(LogLevel level, string? tag, string message) =>
|
|
__android_log_print(level, tag, "%s", message, nint.Zero);
|
|
|
|
internal enum LogLevel
|
|
{
|
|
Unknown = 0x00,
|
|
Default = 0x01,
|
|
Verbose = 0x02,
|
|
Debug = 0x03,
|
|
Info = 0x04,
|
|
Warn = 0x05,
|
|
Error = 0x06,
|
|
Fatal = 0x07,
|
|
Silent = 0x08,
|
|
}
|
|
}
|
|
}
|