1
0
forked from MeloNX/MeloNX

Compare commits

...

4 Commits

10 changed files with 206 additions and 212 deletions

View File

@ -60,6 +60,8 @@ If having Issues installing firmware (Make sure your Keys are installed first)
### Xcode ### Xcode
**NOTE: These Xcode builds are nightly and may have unfinished features.**
1. **Compile Guide** 1. **Compile Guide**
- Visit the [guide here](https://git.743378673.xyz/MeloNX/MeloNX/src/branch/XC-ios-ht/Compile.md). - Visit the [guide here](https://git.743378673.xyz/MeloNX/MeloNX/src/branch/XC-ios-ht/Compile.md).

View File

@ -7,6 +7,7 @@ namespace ARMeilleure.Memory
public const int DefaultGranularity = 65536; // Mapping granularity in Windows. public const int DefaultGranularity = 65536; // Mapping granularity in Windows.
public IJitMemoryBlock Block { get; } public IJitMemoryBlock Block { get; }
public IJitMemoryAllocator Allocator { get; }
public IntPtr Pointer => Block.Pointer; public IntPtr Pointer => Block.Pointer;
@ -21,6 +22,7 @@ namespace ARMeilleure.Memory
granularity = DefaultGranularity; granularity = DefaultGranularity;
} }
Allocator = allocator;
Block = allocator.Reserve(maxSize); Block = allocator.Reserve(maxSize);
_maxSize = maxSize; _maxSize = maxSize;
_sizeGranularity = granularity; _sizeGranularity = granularity;

View File

@ -3,6 +3,7 @@ using ARMeilleure.CodeGen.Unwinding;
using ARMeilleure.Memory; using ARMeilleure.Memory;
using ARMeilleure.Native; using ARMeilleure.Native;
using Ryujinx.Memory; using Ryujinx.Memory;
using Ryujinx.Common.Logging;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@ -18,7 +19,7 @@ namespace ARMeilleure.Translation.Cache
private static readonly int _pageMask = _pageSize - 4; private static readonly int _pageMask = _pageSize - 4;
private const int CodeAlignment = 4; // Bytes. private const int CodeAlignment = 4; // Bytes.
private const int CacheSize = 1024 * 1024 * 1024; private const int CacheSize = 128 * 1024 * 1024;
private const int CacheSizeIOS = 128 * 1024 * 1024; private const int CacheSizeIOS = 128 * 1024 * 1024;
private static ReservedRegion _jitRegion; private static ReservedRegion _jitRegion;
@ -31,6 +32,10 @@ namespace ARMeilleure.Translation.Cache
private static readonly object _lock = new(); private static readonly object _lock = new();
private static bool _initialized; private static bool _initialized;
private static readonly List<ReservedRegion> _jitRegions = new();
private static int _activeRegionIndex = 0;
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)] [LibraryImport("kernel32.dll", SetLastError = true)]
public static partial IntPtr FlushInstructionCache(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize); public static partial IntPtr FlushInstructionCache(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize);
@ -49,7 +54,11 @@ namespace ARMeilleure.Translation.Cache
return; return;
} }
_jitRegion = new ReservedRegion(allocator, (ulong)(OperatingSystem.IsIOS() ? CacheSizeIOS : CacheSize)); var firstRegion = new ReservedRegion(allocator, CacheSize);
_jitRegions.Add(firstRegion);
_activeRegionIndex = 0;
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS()) if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS())
{ {
@ -60,7 +69,9 @@ namespace ARMeilleure.Translation.Cache
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
{ {
JitUnwindWindows.InstallFunctionTableHandler(_jitRegion.Pointer, CacheSize, _jitRegion.Pointer + Allocate(_pageSize)); JitUnwindWindows.InstallFunctionTableHandler(
firstRegion.Pointer, CacheSize, firstRegion.Pointer + Allocate(_pageSize)
);
} }
_initialized = true; _initialized = true;
@ -73,7 +84,9 @@ namespace ARMeilleure.Translation.Cache
{ {
while (_deferredRxProtect.TryDequeue(out var result)) while (_deferredRxProtect.TryDequeue(out var result))
{ {
ReprotectAsExecutable(result.funcOffset, result.length); ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
ReprotectAsExecutable(targetRegion, result.funcOffset, result.length);
} }
} }
@ -87,21 +100,14 @@ namespace ARMeilleure.Translation.Cache
int funcOffset = Allocate(code.Length, deferProtect); int funcOffset = Allocate(code.Length, deferProtect);
IntPtr funcPtr = _jitRegion.Pointer + funcOffset; ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
IntPtr funcPtr = targetRegion.Pointer + funcOffset;
if (OperatingSystem.IsIOS()) if (OperatingSystem.IsIOS())
{ {
Marshal.Copy(code, 0, funcPtr, code.Length); Marshal.Copy(code, 0, funcPtr, code.Length);
if (deferProtect) ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
{ JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length);
_deferredRxProtect.Enqueue((funcOffset, code.Length));
}
else
{
ReprotectAsExecutable(funcOffset, code.Length);
JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length);
}
} }
else if (OperatingSystem.IsMacOS()&& RuntimeInformation.ProcessArchitecture == Architecture.Arm64) else if (OperatingSystem.IsMacOS()&& RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{ {
@ -115,9 +121,9 @@ namespace ARMeilleure.Translation.Cache
} }
else else
{ {
ReprotectAsWritable(funcOffset, code.Length); ReprotectAsWritable(targetRegion, funcOffset, code.Length);
Marshal.Copy(code, 0, funcPtr, code.Length); Marshal.Copy(code, 0, funcPtr, code.Length);
ReprotectAsExecutable(funcOffset, code.Length); ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{ {
@ -139,41 +145,50 @@ namespace ARMeilleure.Translation.Cache
{ {
if (OperatingSystem.IsIOS()) if (OperatingSystem.IsIOS())
{ {
return; // return;
} }
lock (_lock) lock (_lock)
{ {
Debug.Assert(_initialized); foreach (var region in _jitRegions)
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64());
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{ {
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size)); if (pointer.ToInt64() < region.Pointer.ToInt64() ||
_cacheEntries.RemoveAt(entryIndex); pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
{
continue;
}
int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
_cacheEntries.RemoveAt(entryIndex);
}
return;
} }
} }
} }
private static void ReprotectAsWritable(int offset, int size) private static void ReprotectAsWritable(ReservedRegion region, int offset, int size)
{ {
int endOffs = offset + size; int endOffs = offset + size;
int regionStart = offset & ~_pageMask; int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask; int regionEnd = (endOffs + _pageMask) & ~_pageMask;
_jitRegion.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart)); region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
} }
private static void ReprotectAsExecutable(int offset, int size) private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size)
{ {
int endOffs = offset + size; int endOffs = offset + size;
int regionStart = offset & ~_pageMask; int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask; int regionEnd = (endOffs + _pageMask) & ~_pageMask;
_jitRegion.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart)); region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
} }
private static int Allocate(int codeSize, bool deferProtect = false) private static int Allocate(int codeSize, bool deferProtect = false)
@ -187,20 +202,35 @@ namespace ARMeilleure.Translation.Cache
alignment = 0x4000; alignment = 0x4000;
} }
int allocOffset = _cacheAllocator.Allocate(ref codeSize, alignment); for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
//DEBUG: Show JIT Memory Allocation
//Console.WriteLine($"{allocOffset:x8}: {codeSize:x8} {alignment:x8}");
if (allocOffset < 0)
{ {
throw new OutOfMemoryException("JIT Cache exhausted."); int allocOffset = _cacheAllocator.Allocate(ref codeSize, alignment);
if (allocOffset >= 0)
{
_jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
_activeRegionIndex = i;
return allocOffset;
}
} }
_jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize); int exhaustedRegion = _activeRegionIndex;
var newRegion = new ReservedRegion(_jitRegions[0].Allocator, CacheSize);
_jitRegions.Add(newRegion);
_activeRegionIndex = _jitRegions.Count - 1;
return allocOffset; int newRegionNumber = _activeRegionIndex;
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
int allocOffsetNew = _cacheAllocator.Allocate(ref codeSize, alignment);
if (allocOffsetNew < 0)
{
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
}
newRegion.ExpandIfNeeded((ulong)allocOffsetNew + (ulong)codeSize);
return allocOffsetNew;
} }
private static int AlignCodeSize(int codeSize, bool deferProtect = false) private static int AlignCodeSize(int codeSize, bool deferProtect = false)

View File

@ -12,12 +12,12 @@
<key>Ryujinx.xcscheme_^#shared#^_</key> <key>Ryujinx.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>4</integer> <integer>1</integer>
</dict> </dict>
<key>com.Stossy11.MeloNX.RyujinxAg.xcscheme_^#shared#^_</key> <key>com.Stossy11.MeloNX.RyujinxAg.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>3</integer> <integer>2</integer>
</dict> </dict>
</dict> </dict>
<key>SuppressBuildableAutocreation</key> <key>SuppressBuildableAutocreation</key>

View File

@ -77,6 +77,7 @@ class Ryujinx {
var disablevsync: Bool var disablevsync: Bool
var language: SystemLanguage var language: SystemLanguage
var regioncode: SystemRegionCode var regioncode: SystemRegionCode
var handHeldController: Bool
init(gamepath: String, init(gamepath: String,
@ -102,7 +103,8 @@ class Ryujinx {
disablePTC: Bool = false, disablePTC: Bool = false,
disablevsync: Bool = false, disablevsync: Bool = false,
language: SystemLanguage = .americanEnglish, language: SystemLanguage = .americanEnglish,
regioncode: SystemRegionCode = .usa regioncode: SystemRegionCode = .usa,
handHeldController: Bool = false
) { ) {
self.gamepath = gamepath self.gamepath = gamepath
self.inputids = inputids self.inputids = inputids
@ -128,6 +130,7 @@ class Ryujinx {
self.disablevsync = disablevsync self.disablevsync = disablevsync
self.language = language self.language = language
self.regioncode = regioncode self.regioncode = regioncode
self.handHeldController = handHeldController
} }
} }
@ -321,10 +324,14 @@ class Ryujinx {
args.append("--list-inputs-ids") args.append("--list-inputs-ids")
} }
// Append the input ids (limit to 4 just in case) // Append the input ids (limit to 8 (used to be 4) just in case)
if !config.inputids.isEmpty { if !config.inputids.isEmpty {
config.inputids.prefix(4).enumerated().forEach { index, inputId in config.inputids.prefix(8).enumerated().forEach { index, inputId in
args.append(contentsOf: ["--input-id-\(index + 1)", inputId]) if config.handHeldController {
args.append(contentsOf: ["\(index == 0 ? "--input-id-handheld" : "--input-id-\(index + 1)")", inputId])
} else {
args.append(contentsOf: ["--input-id-\(index + 1)", inputId])
}
} }
} }
@ -477,23 +484,21 @@ class Ryujinx {
func repeatuntilfindLayer() { func repeatuntilfindLayer() {
DispatchQueue.global(qos: .background).async { Task { @MainActor in
while self.metalLayer == nil { while self.metalLayer == nil {
let layer = self.getMetalLayer(nil) let layer = self.getMetalLayer(nil)
if layer != nil { if layer != nil {
DispatchQueue.main.async { self.metalLayer = layer
self.metalLayer = layer
}
break break
} }
Thread.sleep(forTimeInterval: 0.1) try await Task.sleep(nanoseconds: 100_000_000)
} }
} }
} }
@MainActor
func getMetalLayer(_ window: OpaquePointer?) -> CAMetalLayer? { func getMetalLayer(_ window: OpaquePointer?) -> CAMetalLayer? {
var window = window var window = window
if window == nil { if window == nil {

View File

@ -9,7 +9,7 @@ import SwiftUI
import UniformTypeIdentifiers import UniformTypeIdentifiers
public struct Game: Identifiable, Equatable, Hashable { public struct Game: Identifiable, Equatable, Hashable {
public var id = UUID() public var id: String { titleId }
var containerFolder: URL var containerFolder: URL
var fileType: UTType var fileType: UTType
@ -21,7 +21,6 @@ public struct Game: Identifiable, Equatable, Hashable {
var version: String var version: String
var icon: UIImage? var icon: UIImage?
static func convertGameInfoToGame(gameInfo: GameInfo, url: URL) -> Game { static func convertGameInfoToGame(gameInfo: GameInfo, url: URL) -> Game {
var gameInfo = gameInfo var gameInfo = gameInfo
var gameTemp = Game(containerFolder: url.deletingLastPathComponent(), fileType: .item, fileURL: url, titleName: "", titleId: "", developer: "", version: "") var gameTemp = Game(containerFolder: url.deletingLastPathComponent(), fileType: .item, fileURL: url, titleName: "", titleId: "", developer: "", version: "")

View File

@ -39,14 +39,23 @@ struct GameLibraryView: View {
var filteredGames: [Game] { var filteredGames: [Game] {
if searchText.isEmpty { if searchText.isEmpty {
return Ryujinx.shared.games return Ryujinx.shared.games.filter { game in
!realRecentGames.contains(where: { $0.titleId == game.titleId })
}
} }
return Ryujinx.shared.games.filter { return Ryujinx.shared.games.filter {
$0.titleName.localizedCaseInsensitiveContains(searchText) || $0.titleName.localizedCaseInsensitiveContains(searchText) ||
$0.developer.localizedCaseInsensitiveContains(searchText) $0.developer.localizedCaseInsensitiveContains(searchText)
} }
} }
var realRecentGames: [Game] {
let games = Ryujinx.shared.games
return recentGames.compactMap { recentGame in
games.first(where: { $0.titleId == recentGame.titleId })
}
}
var body: some View { var body: some View {
iOSNav { iOSNav {
List { List {
@ -66,46 +75,32 @@ struct GameLibraryView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.top, 40) .padding(.top, 40)
} else { } else {
if !isSearching && !recentGames.isEmpty { if !isSearching && !realRecentGames.isEmpty {
VStack(alignment: .leading, spacing: 12) { Section {
Text("Recent") ForEach(realRecentGames) { game in
.font(.title2.bold()) GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
.padding(.horizontal) .swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
ScrollView(.horizontal, showsIndicators: false) { removeFromRecentGames(game)
LazyHStack(spacing: 16) { } label: {
ForEach(recentGames) { game in Label("Delete", systemImage: "trash")
RecentGameCard(game: game, startemu: $startemu)
.onTapGesture {
addToRecentGames(game)
startemu = game
}
}
}
.padding(.horizontal)
}
}
VStack(alignment: .leading, spacing: 12) {
Text("All Games")
.font(.title2.bold())
.padding(.horizontal)
LazyVStack(spacing: 2) {
ForEach(filteredGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
.onTapGesture {
addToRecentGames(game)
} }
} }
} }
} header: {
Text("Recent")
}
Section {
ForEach(filteredGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
}
} header: {
Text("Others")
} }
} else { } else {
ForEach(filteredGames) { game in ForEach(filteredGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo) GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
.onTapGesture {
addToRecentGames(game)
}
} }
} }
} }
@ -206,8 +201,13 @@ struct GameLibraryView: View {
} }
} }
} }
.onChange(of: startemu) { game in
guard let game else { return }
addToRecentGames(game)
}
} }
.searchable(text: $searchText) .searchable(text: $searchText)
.animation(.easeInOut, value: searchText)
.onChange(of: searchText) { _ in .onChange(of: searchText) { _ in
isSearching = !searchText.isEmpty isSearching = !searchText.isEmpty
} }
@ -291,8 +291,8 @@ struct GameLibraryView: View {
} }
private func addToRecentGames(_ game: Game) { private func addToRecentGames(_ game: Game) {
recentGames.removeAll { $0.id == game.id } recentGames.removeAll { $0.titleId == game.titleId }
recentGames.insert(game, at: 0) recentGames.insert(game, at: 0)
if recentGames.count > 5 { if recentGames.count > 5 {
@ -301,7 +301,12 @@ struct GameLibraryView: View {
saveRecentGames() saveRecentGames()
} }
private func removeFromRecentGames(_ game: Game) {
recentGames.removeAll { $0.titleId == game.titleId }
saveRecentGames()
}
private func saveRecentGames() { private func saveRecentGames() {
do { do {
let encoder = JSONEncoder() let encoder = JSONEncoder()
@ -364,53 +369,6 @@ extension Game: Codable {
} }
} }
// MARK: - Recent Game Card
struct RecentGameCard: View {
let game: Game
@Binding var startemu: Game?
@Environment(\.colorScheme) var colorScheme
var body: some View {
Button(action: {
startemu = game
}) {
VStack(alignment: .leading, spacing: 8) {
if let icon = game.icon {
Image(uiImage: icon)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 140, height: 140)
.cornerRadius(12)
} else {
ZStack {
RoundedRectangle(cornerRadius: 12)
.fill(colorScheme == .dark ?
Color(.systemGray5) : Color(.systemGray6))
.frame(width: 140, height: 140)
Image(systemName: "gamecontroller.fill")
.font(.system(size: 40))
.foregroundColor(.gray)
}
}
VStack(alignment: .leading, spacing: 2) {
Text(game.titleName)
.font(.subheadline.bold())
.lineLimit(1)
Text(game.developer)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(1)
}
.padding(.horizontal, 4)
}
}
.buttonStyle(.plain)
}
}
// MARK: - Game List Item // MARK: - Game List Item
struct GameListRow: View { struct GameListRow: View {
let game: Game let game: Game
@ -467,49 +425,48 @@ struct GameListRow: View {
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.opacity(0.8) .opacity(0.8)
} }
.contextMenu { }
Section { .contextMenu {
Button { Section {
startemu = game Button {
} label: { startemu = game
Label("Play Now", systemImage: "play.fill") } label: {
} Label("Play Now", systemImage: "play.fill")
Button {
gameInfo = game
isViewingGameInfo.toggle()
} label: {
Label("Game Info", systemImage: "info.circle")
}
} }
Section { Button {
Button { gameInfo = game
gameInfo = game isViewingGameInfo.toggle()
isSelectingGameUpdate.toggle() } label: {
} label: { Label("Game Info", systemImage: "info.circle")
Label("Game Update Manager", systemImage: "chevron.up.circle")
}
Button {
gameInfo = game
isSelectingGameDLC.toggle()
} label: {
Label("Game DLC Manager", systemImage: "plus.viewfinder")
}
} }
}
Section {
Button(role: .destructive) { Section {
gametoDelete = game Button {
showGameDeleteConfirmation.toggle() gameInfo = game
} label: { isSelectingGameUpdate.toggle()
Label("Delete", systemImage: "trash") } label: {
} Label("Game Update Manager", systemImage: "chevron.up.circle")
}
Button {
gameInfo = game
isSelectingGameDLC.toggle()
} label: {
Label("Game DLC Manager", systemImage: "plus.viewfinder")
}
}
Section {
Button(role: .destructive) {
gametoDelete = game
showGameDeleteConfirmation.toggle()
} label: {
Label("Delete", systemImage: "trash")
} }
} }
} }
.buttonStyle(.plain)
.confirmationDialog("Are you sure you want to delete this game?", isPresented: $showGameDeleteConfirmation) { .confirmationDialog("Are you sure you want to delete this game?", isPresented: $showGameDeleteConfirmation) {
Button("Delete", role: .destructive) { Button("Delete", role: .destructive) {
if let game = gametoDelete { if let game = gametoDelete {

View File

@ -272,42 +272,12 @@ struct SettingsView: View {
Text("Select input devices and on-screen controls to play with. ") Text("Select input devices and on-screen controls to play with. ")
} }
// Language and Region Settings
Section {
Picker(selection: $config.language) {
ForEach(SystemLanguage.allCases, id: \.self) { ratio in
Text(ratio.displayName).tag(ratio)
}
} label: {
labelWithIcon("Language", iconName: "character.bubble")
}
Picker(selection: $config.regioncode) {
ForEach(SystemRegionCode.allCases, id: \.self) { ratio in
Text(ratio.displayName).tag(ratio)
}
} label: {
labelWithIcon("Region", iconName: "globe")
}
// globe
} header: {
Text("Language and Region Settings")
.font(.title3.weight(.semibold))
.textCase(nil)
.headerProminence(.increased)
} footer: {
Text("Configure the System Language and the Region.")
}
// Input Settings // Input Settings
Section { Section {
Toggle(isOn: $config.macroHLE) {
Toggle(isOn: $config.listinputids) { labelWithIcon("Player 1 to Handheld Input", iconName: "formfitting.gamecontroller")
labelWithIcon("List Input IDs", iconName: "list.bullet") }.tint(.blue)
}
.tint(.blue)
Toggle(isOn: $ryuDemo) { Toggle(isOn: $ryuDemo) {
labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw") labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw")
@ -363,6 +333,35 @@ struct SettingsView: View {
Text("Configure input devices and on-screen controls for easier navigation and play.") Text("Configure input devices and on-screen controls for easier navigation and play.")
} }
// Language and Region Settings
Section {
Picker(selection: $config.language) {
ForEach(SystemLanguage.allCases, id: \.self) { ratio in
Text(ratio.displayName).tag(ratio)
}
} label: {
labelWithIcon("Language", iconName: "character.bubble")
}
Picker(selection: $config.regioncode) {
ForEach(SystemRegionCode.allCases, id: \.self) { ratio in
Text(ratio.displayName).tag(ratio)
}
} label: {
labelWithIcon("Region", iconName: "globe")
}
// globe
} header: {
Text("Language and Region Settings")
.font(.title3.weight(.semibold))
.textCase(nil)
.headerProminence(.increased)
} footer: {
Text("Configure the System Language and the Region.")
}
// CPU Mode // CPU Mode
Section { Section {
if filteredMemoryModes.isEmpty { if filteredMemoryModes.isEmpty {

View File

@ -45,7 +45,7 @@ struct DLCManagerSheet: View {
Self.saveDlcs(game, dlc: dlcs) Self.saveDlcs(game, dlc: dlcs)
}) { }) {
HStack { HStack {
Text(dlc.containerPath) Text((dlc.containerPath as NSString).lastPathComponent)
.foregroundStyle(Color(uiColor: .label)) .foregroundStyle(Color(uiColor: .label))
Spacer() Spacer()
if dlc.downloadableContentNcaList.first?.enabled == true { if dlc.downloadableContentNcaList.first?.enabled == true {