single MR with all patches i did, compared to the 2.0.5 release:

See merge request kenji-nx/ryujinx!18
This commit is contained in:
BeZide
2026-01-20 00:10:38 -06:00
committed by KeatonTheBot
parent dc4a51a462
commit 2c1b7bc667
38 changed files with 7401 additions and 598 deletions
+369 -1
View File
@@ -16,6 +16,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Ryujinx.Graphics.Vulkan;
namespace LibKenjinx
{
@@ -34,6 +35,254 @@ namespace LibKenjinx
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();
@@ -96,6 +345,7 @@ namespace LibKenjinx
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,
@@ -316,7 +566,7 @@ namespace LibKenjinx
Window = (nint*)_surfacePtr,
};
var result = surfaceExtension.CreateAndroidSurface(new Instance(instance), createInfo, null, out var surface);
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)
@@ -565,6 +815,22 @@ namespace LibKenjinx
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()
{
@@ -688,9 +954,111 @@ namespace LibKenjinx
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
{
_ = (Renderer as VulkanRenderer)?.RecreateSurface();
}
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.Application, $"graphicsRendererRecreateSurface failed: {ex}");
}
}
// 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);