mirror of
https://github.com/ARMSX2/ARMSX3.git
synced 2026-08-24 16:58:52 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94b12dc216 | ||
|
|
ac7639457f | ||
|
|
84db19dc3e | ||
|
|
5eb3d64ed0 | ||
|
|
d82f1df96b | ||
|
|
697cacd854 | ||
|
|
ab3335b8d6 | ||
|
|
99b6b47ee1 | ||
|
|
6255ce5840 | ||
|
|
87b0992fdd | ||
|
|
2a0c04ade7 | ||
|
|
5b8ff01fe2 | ||
|
|
51d465f193 | ||
|
|
6cd5b8b986 | ||
|
|
44f62f310c | ||
|
|
431b6d0925 | ||
|
|
4a73773cee | ||
|
|
e16f0fcd3d | ||
|
|
6463d010e9 |
@@ -545,11 +545,18 @@ class jit_compiler final
|
||||
// Disk Space left
|
||||
atomic_t<usz> m_disk_space = umax;
|
||||
|
||||
bool m_poisoned = false;
|
||||
|
||||
public:
|
||||
jit_compiler(const std::unordered_map<std::string, u64>& _link, std::string_view _cpu, u32 flags = 0, std::function<u64(const std::string&)> symbols_cement = {}) noexcept;
|
||||
jit_compiler& operator=(thread_state) noexcept;
|
||||
~jit_compiler() noexcept;
|
||||
|
||||
bool is_poisoned() const noexcept
|
||||
{
|
||||
return m_poisoned;
|
||||
}
|
||||
|
||||
// Get LLVM context
|
||||
auto& get_context()
|
||||
{
|
||||
|
||||
+24
-2
@@ -872,17 +872,26 @@ jit_compiler& jit_compiler::operator=(thread_state s) noexcept
|
||||
|
||||
jit_compiler::~jit_compiler() noexcept
|
||||
{
|
||||
if (m_poisoned)
|
||||
{
|
||||
jit_log.error("Abandoning poisoned LLVM execution engine (leaked to avoid a deadlock in ~MCJIT)");
|
||||
static_cast<void>(m_engine.release());
|
||||
static_cast<void>(m_context.release());
|
||||
}
|
||||
}
|
||||
|
||||
void jit_compiler::add(std::unique_ptr<llvm::Module> _module, const std::string& path)
|
||||
{
|
||||
ObjectCache cache{path, this};
|
||||
|
||||
m_poisoned = true;
|
||||
m_engine->setObjectCache(&cache);
|
||||
|
||||
const auto ptr = _module.get();
|
||||
m_engine->addModule(std::move(_module));
|
||||
m_engine->generateCodeForModule(ptr);
|
||||
m_engine->setObjectCache(nullptr);
|
||||
m_poisoned = false;
|
||||
|
||||
for (auto& func : ptr->functions())
|
||||
{
|
||||
@@ -904,6 +913,7 @@ bool jit_compiler::try_add(std::unique_ptr<llvm::Module> _module, const std::str
|
||||
m_engine->generateCodeForModule(ptr);
|
||||
}, error))
|
||||
{
|
||||
m_poisoned = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -921,8 +931,11 @@ bool jit_compiler::try_add(std::unique_ptr<llvm::Module> _module, const std::str
|
||||
void jit_compiler::add(std::unique_ptr<llvm::Module> _module)
|
||||
{
|
||||
const auto ptr = _module.get();
|
||||
|
||||
m_poisoned = true;
|
||||
m_engine->addModule(std::move(_module));
|
||||
m_engine->generateCodeForModule(ptr);
|
||||
m_poisoned = false;
|
||||
|
||||
for (auto& func : ptr->functions())
|
||||
{
|
||||
@@ -941,6 +954,7 @@ bool jit_compiler::try_add(std::unique_ptr<llvm::Module> _module, std::string& e
|
||||
m_engine->generateCodeForModule(ptr);
|
||||
}, error))
|
||||
{
|
||||
m_poisoned = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1001,15 +1015,23 @@ void jit_compiler::update_global_mapping(const std::string& name, u64 addr)
|
||||
|
||||
void jit_compiler::fin()
|
||||
{
|
||||
m_poisoned = true;
|
||||
m_engine->finalizeObject();
|
||||
m_poisoned = false;
|
||||
}
|
||||
|
||||
bool jit_compiler::try_fin(std::string& error)
|
||||
{
|
||||
return run_recoverable_llvm([&]()
|
||||
if (!run_recoverable_llvm([&]()
|
||||
{
|
||||
m_engine->finalizeObject();
|
||||
}, error);
|
||||
}, error))
|
||||
{
|
||||
m_poisoned = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
u64 jit_compiler::get(const std::string& name)
|
||||
|
||||
+17
-2
@@ -2844,6 +2844,7 @@ void thread_base::initialize(void (*error_cb)())
|
||||
[[maybe_unused]] u64 new_tid = 0;
|
||||
#elif defined(ANDROID)
|
||||
const u64 new_tid = pthread_self();
|
||||
m_native_tid = static_cast<u32>(gettid());
|
||||
#else
|
||||
const u64 new_tid = reinterpret_cast<u64>(pthread_self());
|
||||
#endif
|
||||
@@ -2998,6 +2999,10 @@ u64 thread_base::finalize(thread_state result_state) noexcept
|
||||
// Avoid race with the destructor
|
||||
const u64 _self = m_thread;
|
||||
|
||||
#ifdef ANDROID
|
||||
m_native_tid = 0;
|
||||
#endif
|
||||
|
||||
// Set result state (errored or finalized)
|
||||
m_sync.fetch_op([&](u32& v)
|
||||
{
|
||||
@@ -3373,11 +3378,21 @@ u64 thread_base::get_cycles()
|
||||
clockid_t _clock;
|
||||
struct timespec thread_time;
|
||||
#ifdef ANDROID
|
||||
pthread_t thread_id = handle;
|
||||
const u32 native_tid = m_native_tid;
|
||||
|
||||
if (!handle || !native_tid)
|
||||
{
|
||||
return m_cycles;
|
||||
}
|
||||
|
||||
_clock = (~static_cast<clockid_t>(native_tid) << 3) | 6;
|
||||
|
||||
if (!clock_gettime(_clock, &thread_time))
|
||||
#else
|
||||
pthread_t thread_id = reinterpret_cast<pthread_t>(handle);
|
||||
#endif
|
||||
|
||||
if (!pthread_getcpuclockid(thread_id, &_clock) && !clock_gettime(_clock, &thread_time))
|
||||
#endif
|
||||
{
|
||||
cycles = static_cast<u64>(thread_time.tv_sec) * 1'000'000'000 + thread_time.tv_nsec;
|
||||
#endif
|
||||
|
||||
@@ -139,6 +139,10 @@ private:
|
||||
// Thread handle (platform-specific)
|
||||
atomic_t<u64> m_thread{0};
|
||||
|
||||
#ifdef ANDROID
|
||||
atomic_t<u32> m_native_tid{0};
|
||||
#endif
|
||||
|
||||
// Thread cycles
|
||||
atomic_t<u64> m_cycles{0};
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ android {
|
||||
applicationId = "com.armsx3"
|
||||
minSdk = 26
|
||||
targetSdk = 37
|
||||
versionCode = 6
|
||||
versionName = "0.4"
|
||||
versionCode = 8
|
||||
versionName = "0.4.2"
|
||||
|
||||
// ARMSX2's UI reads these. STORAGE_ALL_FILES gates the all-files storage path in
|
||||
// onboarding; IN_APP_UPDATER gates the in-app GitHub-release updater.
|
||||
|
||||
@@ -99,6 +99,17 @@ data class Ps3Settings(
|
||||
val spuCache: Boolean = true,
|
||||
val llvmPrecompile: Boolean = true,
|
||||
val accurateSpuDma: Boolean = false,
|
||||
/** Locks every SPU thread into a state a savestate can be serialised from.
|
||||
*
|
||||
* Savestates cannot be taken without it: the save has to stop each SPU somewhere it can
|
||||
* be written out, and with this off that fails on any title with SPU work running.
|
||||
*
|
||||
* On by default, unlike upstream, so the feature works for someone who never opens
|
||||
* settings -- a save that fails with "missing SPU setting" reads as broken, not as a
|
||||
* setting waiting to be found. The costs are real and are stated on the switch: it slows
|
||||
* the SPUs while it is on, and a PS3 state runs 500MB to 3GB. Turning it off restores
|
||||
* upstream behaviour and gives the SPU performance back. */
|
||||
val savestateCompatibleMode: Boolean = true,
|
||||
val clocksScale: Int = 100,
|
||||
val resolutionScale: Int = 100,
|
||||
/** 0 = Disabled. Off by default: mobile drivers routinely lack the MSAA
|
||||
@@ -949,6 +960,7 @@ data class Settings(
|
||||
put("PS3/Core", "SPU Cache", "bool", ps3.spuCache.toString())
|
||||
put("PS3/Core", "LLVM Precompilation", "bool", ps3.llvmPrecompile.toString())
|
||||
put("PS3/Core", "Accurate SPU DMA", "bool", ps3.accurateSpuDma.toString())
|
||||
put("Savestate", "Compatible Savestate Mode", "bool", ps3.savestateCompatibleMode.toString())
|
||||
put("PS3/Core", "Clocks scale", "int", ps3.clocksScale.toString())
|
||||
// From upscaleFloat, which is the control that exists.
|
||||
//
|
||||
@@ -1248,19 +1260,12 @@ data class Settings(
|
||||
// above it would already be 60; setting it explicitly means the cap does not depend
|
||||
// on the vblank path holding, which it did not. Enum node, so the value is quoted.
|
||||
runCatching { net.rpcsx.RPCSX.instance.settingsSet("Video@@Frame limit", "\"60\"") }
|
||||
// Held at the upstream default, which is off.
|
||||
//
|
||||
// Savestates cannot work without it: saving has to lock every SPU thread into a state
|
||||
// it can be serialised from, and with this off that lock fails on any title with SPU
|
||||
// work running. It was turned on for exactly that reason and then turned back off,
|
||||
// because a PS3 savestate runs 500MB to 3GB and the feature was dropped rather than
|
||||
// ship something that fills a phone in a handful of saves.
|
||||
//
|
||||
// Written explicitly rather than left alone: it was pushed as true for a while, so
|
||||
// installs from that window have true persisted in config.yml and would keep paying
|
||||
// for it. It costs SPU performance, which is the whole reason upstream defaults it
|
||||
// off, and nothing here uses what it buys.
|
||||
runCatching { net.rpcsx.RPCSX.instance.settingsSet("Savestate@@Compatible Savestate Mode", "false") }
|
||||
// Compatible Savestate Mode is no longer forced off here; applyTo writes it from
|
||||
// ps3.savestateCompatibleMode above, so the two costs it carries -- SPU performance
|
||||
// and a 500MB to 3GB state file -- are the user's to accept rather than a decision
|
||||
// taken for them. Defaulted on so the feature works without hunting for a switch,
|
||||
// and written every boot either way, so a user who turns it off has that respected
|
||||
// rather than re-enabled on the next launch.
|
||||
|
||||
// Settings a specific title needs in order to run at all, then the user's own core
|
||||
// edits on top. Order matters: game defaults are a floor, an explicit user choice
|
||||
@@ -1925,6 +1930,7 @@ data class Settings(
|
||||
put("ps3SpuCache", ps3.spuCache)
|
||||
put("ps3LlvmPrecompile", ps3.llvmPrecompile)
|
||||
put("ps3AccurateSpuDma", ps3.accurateSpuDma)
|
||||
put("ps3SavestateCompatibleMode", ps3.savestateCompatibleMode)
|
||||
put("ps3ClocksScale", ps3.clocksScale)
|
||||
put("ps3ResolutionScale", ps3.resolutionScale)
|
||||
put("ps3MsaaMode", ps3.msaaMode)
|
||||
@@ -2259,6 +2265,7 @@ data class Settings(
|
||||
spuCache = json.optBoolean("ps3SpuCache", def.ps3.spuCache),
|
||||
llvmPrecompile = json.optBoolean("ps3LlvmPrecompile", def.ps3.llvmPrecompile),
|
||||
accurateSpuDma = json.optBoolean("ps3AccurateSpuDma", def.ps3.accurateSpuDma),
|
||||
savestateCompatibleMode = json.optBoolean("ps3SavestateCompatibleMode", def.ps3.savestateCompatibleMode),
|
||||
clocksScale = json.optInt("ps3ClocksScale", def.ps3.clocksScale),
|
||||
resolutionScale = json.optInt("ps3ResolutionScale", def.ps3.resolutionScale),
|
||||
msaaMode = json.optInt("ps3MsaaMode", def.ps3.msaaMode),
|
||||
@@ -2573,6 +2580,7 @@ data class Settings(
|
||||
if (current.ps3.spuCache != base.ps3.spuCache) j.put("ps3SpuCache", current.ps3.spuCache)
|
||||
if (current.ps3.llvmPrecompile != base.ps3.llvmPrecompile) j.put("ps3LlvmPrecompile", current.ps3.llvmPrecompile)
|
||||
if (current.ps3.accurateSpuDma != base.ps3.accurateSpuDma) j.put("ps3AccurateSpuDma", current.ps3.accurateSpuDma)
|
||||
if (current.ps3.savestateCompatibleMode != base.ps3.savestateCompatibleMode) j.put("ps3SavestateCompatibleMode", current.ps3.savestateCompatibleMode)
|
||||
if (current.ps3.clocksScale != base.ps3.clocksScale) j.put("ps3ClocksScale", current.ps3.clocksScale)
|
||||
if (current.ps3.resolutionScale != base.ps3.resolutionScale) j.put("ps3ResolutionScale", current.ps3.resolutionScale)
|
||||
if (current.ps3.msaaMode != base.ps3.msaaMode) j.put("ps3MsaaMode", current.ps3.msaaMode)
|
||||
@@ -2868,6 +2876,7 @@ data class Settings(
|
||||
spuCache = if (overrides.has("ps3SpuCache")) overrides.getBoolean("ps3SpuCache") else base.ps3.spuCache,
|
||||
llvmPrecompile = if (overrides.has("ps3LlvmPrecompile")) overrides.getBoolean("ps3LlvmPrecompile") else base.ps3.llvmPrecompile,
|
||||
accurateSpuDma = if (overrides.has("ps3AccurateSpuDma")) overrides.getBoolean("ps3AccurateSpuDma") else base.ps3.accurateSpuDma,
|
||||
savestateCompatibleMode = if (overrides.has("ps3SavestateCompatibleMode")) overrides.getBoolean("ps3SavestateCompatibleMode") else base.ps3.savestateCompatibleMode,
|
||||
clocksScale = if (overrides.has("ps3ClocksScale")) overrides.getInt("ps3ClocksScale") else base.ps3.clocksScale,
|
||||
resolutionScale = if (overrides.has("ps3ResolutionScale")) overrides.getInt("ps3ResolutionScale") else base.ps3.resolutionScale,
|
||||
msaaMode = if (overrides.has("ps3MsaaMode")) overrides.getInt("ps3MsaaMode") else base.ps3.msaaMode,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.armsx2.data.library
|
||||
|
||||
import android.content.Context
|
||||
import android.os.ParcelFileDescriptor
|
||||
import net.rpcsx.ProgressRepository
|
||||
import net.rpcsx.RPCSX
|
||||
import java.io.File
|
||||
|
||||
@@ -29,6 +32,34 @@ object Licences {
|
||||
* The extension is written lower-case because that is what unself.cpp looks for when it
|
||||
* searches exdata for a licence matching a game's content id.
|
||||
*/
|
||||
/**
|
||||
* Install [file] as the licence for the game at [gamePath].
|
||||
*
|
||||
* Prefers the content id read out of the game's own EBOOT over the one in the file name.
|
||||
* The name is only a convention: a licence saved as "license(1).rap", renamed, or handed
|
||||
* around by someone who tidied it up copies into exdata under a name nothing looks for,
|
||||
* so the install reports success and the game stays locked -- which is exactly what it
|
||||
* looks like from the outside, and what it was reported as.
|
||||
*
|
||||
* The native path decrypts the EBOOT's supplemental header to read the content id, which
|
||||
* works on a LOCKED game because that header is not what the licence protects. Falls back
|
||||
* to the name when there is no game to ask, or when the header cannot be read.
|
||||
*/
|
||||
fun installRapForGame(context: Context, file: File, gamePath: String?): Boolean {
|
||||
if (!gamePath.isNullOrBlank() && RPCSX.initialized) {
|
||||
val installed = runCatching {
|
||||
val id = ProgressRepository.create(context, "Installing ${file.name}")
|
||||
ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).use { fd ->
|
||||
RPCSX.instance.installKey(fd.fd, id, gamePath)
|
||||
}
|
||||
}.getOrDefault(false)
|
||||
|
||||
if (installed) return true
|
||||
}
|
||||
|
||||
return installRap(file)
|
||||
}
|
||||
|
||||
fun installRap(file: File): Boolean = runCatching {
|
||||
val bytes = file.readBytes()
|
||||
// Same floor as InstallFileInExData: anything shorter is not a key.
|
||||
|
||||
@@ -1563,6 +1563,8 @@ val EN: Map<String, String> = mapOf(
|
||||
"perf.llvmPrecompile.description" to "Compiles modules ahead of the game running instead of on demand. Longer wait at boot, fewer stutters in play.",
|
||||
"perf.spuLoopDetection.label" to "SPU Loop Detection",
|
||||
"perf.spuLoopDetection.description" to "Detects SPU wait loops and yields the thread instead of spinning. Can free CPU time on a handheld; a few games misbehave with it on.",
|
||||
"perf.savestateCompatible.label" to "Allow save states (Compatible Savestate Mode)",
|
||||
"perf.savestateCompatible.description" to "Required for save states to work at all: saving has to stop every SPU somewhere it can be written out, and without this that fails on any game with SPU work running. Costs SPU performance while it is on, and each save state is roughly 500 MB to 3 GB, so a few of them will fill your storage.",
|
||||
"perf.accurateSpuDma.label" to "Accurate SPU DMA",
|
||||
"perf.accurateSpuDma.description" to "Emulates SPU DMA transfers precisely. Slower, and only needed by a handful of games that corrupt without it.",
|
||||
"common.auto" to "Auto",
|
||||
@@ -1679,7 +1681,7 @@ val EN: Map<String, String> = mapOf(
|
||||
"renderer.upscale.label" to "Upscale",
|
||||
"renderer.vsync.description" to "Sync presentation to the display refresh — less tearing/smoother, slightly more latency. Restart the game to apply.",
|
||||
"savestate.autoLoadOnBoot" to "Auto-load last state on boot",
|
||||
"savestate.autoSaveInterval.description" to "Save automatically while you play, so a crash or a flat battery costs at most this much progress. It writes the same auto-save slot as the option above, so your numbered slots stay yours. Saving pauses the game for a moment, so a short interval is felt — 5 minutes is a good starting point.",
|
||||
"savestate.autoSaveInterval.description" to "Save automatically while you play, so a crash or a flat battery costs at most this much progress. It writes the same auto-save slot as the option above, so your numbered slots stay yours. Saving a PS3 state stops and reloads the game, which takes several seconds each time — keep the interval long, 15 minutes or more.",
|
||||
"savestate.autoSaveInterval.every" to "Every %d min",
|
||||
"savestate.autoSaveInterval.label" to "Auto-save while playing",
|
||||
"savestate.autoSaveInterval.off" to "Off",
|
||||
@@ -1690,6 +1692,7 @@ val EN: Map<String, String> = mapOf(
|
||||
"savestate.backup" to "Backup",
|
||||
"savestate.import" to "Import",
|
||||
"savestate.hint" to "Choose a slot. Hold a slot, or use the trash button, to delete it.",
|
||||
"savestate.sizeWarning" to "Save states are big — usually 25–40 MB each, and it varies by game. Ten slots per game adds up, so keep an eye on storage.",
|
||||
"savestate.delete.mode" to "Delete a save",
|
||||
"savestate.delete.modeHint" to "Delete mode: choose a save to delete. Tap the trash button again to cancel.",
|
||||
"savestate.delete.title" to "Delete save state",
|
||||
|
||||
@@ -799,8 +799,10 @@ fun HomeScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// The game itself is not needed to install the key -- a RAP's filename is the content id
|
||||
// it unlocks -- so this only has to know that a locked game asked for one.
|
||||
// The game IS needed, when we have it. A RAP's filename is only conventionally the content
|
||||
// id it unlocks: one saved as "license(1).rap" or renamed on the way over installs under a
|
||||
// name nothing looks for, so the install reports success and the game stays locked. Asking
|
||||
// the game's own EBOOT for the id gets it right whatever the file is called.
|
||||
if (licenceGame != null) {
|
||||
// Resolved during composition: str() is @Composable and cannot be called from the
|
||||
// pick callback, the same reason games.addToHome.unsupported is hoisted above.
|
||||
@@ -813,8 +815,10 @@ fun HomeScreen(
|
||||
// key that belongs to THIS game.
|
||||
extensions = setOf("rap"),
|
||||
onPick = { file ->
|
||||
val gamePath = licenceGame?.uri?.path
|
||||
licenceGame = null
|
||||
val ok = com.armsx2.data.library.Licences.installRap(file)
|
||||
val ok = com.armsx2.data.library.Licences
|
||||
.installRapForGame(context, file, gamePath)
|
||||
Toast.makeText(context, if (ok) installedMsg else failedMsg, Toast.LENGTH_LONG).show()
|
||||
// The folder set is unchanged, so only the lock state is stale — nothing else
|
||||
// would prompt a rescan.
|
||||
|
||||
@@ -228,11 +228,15 @@ internal fun importSaveStateToNextFreeSlot(context: android.content.Context, uri
|
||||
val active = MainActivityRuntime.currentGame.value
|
||||
if (active == null || active.serial.isNullOrBlank()) return SS_IMPORT_NO_GAME
|
||||
return runCatching {
|
||||
val free = (0 until 10).firstOrNull { s ->
|
||||
val p = NativeApp.getGamePathSlot(s)
|
||||
p.isNullOrBlank() || !File(p).exists()
|
||||
} ?: return@runCatching SS_IMPORT_SLOTS_FULL
|
||||
val destPath = NativeApp.getGamePathSlot(free)?.takeIf(String::isNotBlank) ?: return@runCatching SS_IMPORT_FAILED
|
||||
// Occupancy comes from the core. getGamePathSlot answers with the TITLE ID, so
|
||||
// File(it).exists() was false for every slot: the first OCCUPIED slot read as free,
|
||||
// and the destination built from the same value was a relative name that landed in
|
||||
// the process working directory. The import then reported the slot it had not
|
||||
// written, which is worse than failing.
|
||||
val free = (0 until 10).firstOrNull { !NativeApp.hasStateInSlot(it) }
|
||||
?: return@runCatching SS_IMPORT_SLOTS_FULL
|
||||
val destPath = NativeApp.getSlotFilePath(free)?.takeIf(String::isNotBlank)
|
||||
?: return@runCatching SS_IMPORT_FAILED
|
||||
val dest = File(destPath)
|
||||
dest.parentFile?.mkdirs()
|
||||
val ok = context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
|
||||
@@ -150,6 +150,16 @@ fun SaveStatePickerScreen(mode: SaveMode, onBack: () -> Unit) {
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
)
|
||||
// Worth saying before the storage bill arrives rather than after: one state is
|
||||
// tens of megabytes, it varies with the game, and there are ten slots for each.
|
||||
if (!deleteMode) {
|
||||
Text(
|
||||
str("savestate.sizeWarning"),
|
||||
color = Color(0xFF9AA0A6),
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
)
|
||||
}
|
||||
failure?.let { key ->
|
||||
Text(
|
||||
str(key),
|
||||
|
||||
@@ -319,6 +319,15 @@ fun PerformanceTab(state: MutableState<Settings>) {
|
||||
description = str("perf.accurateSpuDma.description"),
|
||||
onChange = { apply(s.copy(ps3 = s.ps3.copy(accurateSpuDma = it))) },
|
||||
)
|
||||
SettingsDivider()
|
||||
// Sits with the SPU rows because that is what it costs, not under a savestate
|
||||
// heading where it would read as free.
|
||||
ToggleRow(
|
||||
label = str("perf.savestateCompatible.label"),
|
||||
value = s.ps3.savestateCompatibleMode,
|
||||
description = str("perf.savestateCompatible.description"),
|
||||
onChange = { apply(s.copy(ps3 = s.ps3.copy(savestateCompatibleMode = it))) },
|
||||
)
|
||||
}
|
||||
SettingsDivider()
|
||||
// The PS2 speedhacks here (INTC/wait-loop detection, fast CDVD, instant
|
||||
|
||||
@@ -53,6 +53,7 @@ internal val SETTINGS_SEARCH_INDEX: List<SettingsSearchEntry> = listOf(
|
||||
SettingsSearchEntry("perf.llvmPrecompile.label", true, SettingsCategory.Performance),
|
||||
SettingsSearchEntry("perf.spuLoopDetection.label", true, SettingsCategory.Performance),
|
||||
SettingsSearchEntry("perf.accurateSpuDma.label", true, SettingsCategory.Performance),
|
||||
SettingsSearchEntry("perf.savestateCompatible.label", true, SettingsCategory.Performance),
|
||||
SettingsSearchEntry("perf.spuTuning.title", true, SettingsCategory.Performance),
|
||||
SettingsSearchEntry("adv.accurateSpuRsv.label", true, SettingsCategory.Performance),
|
||||
SettingsSearchEntry("adv.accurateCacheLine.label", true, SettingsCategory.Performance),
|
||||
|
||||
@@ -129,9 +129,16 @@ public final class NativeApp {
|
||||
public static String getGamePathSlot(int slot) { return Rpcs3Bridge.gamePathForSlot(slot); }
|
||||
|
||||
/** [TODO] Autosave is an ARMSX2 feature layered on PCSX2 savestates. */
|
||||
public static boolean hasAutosaveState() { Unsupported.note("hasAutosaveState"); return false; }
|
||||
public static boolean saveAutosaveState() { Unsupported.note("saveAutosaveState"); return false; }
|
||||
public static boolean loadAutosaveState() { Unsupported.note("loadAutosaveState"); return false; }
|
||||
/** [MAPPED] Auto-save state, kept in a reserved slot above the ten the picker shows. */
|
||||
public static boolean hasAutosaveState() { return Rpcs3Bridge.hasAutosaveState(); }
|
||||
public static boolean saveAutosaveState() { return Rpcs3Bridge.saveAutosaveState(); }
|
||||
public static boolean loadAutosaveState() { return Rpcs3Bridge.loadAutosaveState(); }
|
||||
|
||||
/** [MAPPED] Where a slot's state file lives. Not getGamePathSlot, which answers a title id. */
|
||||
public static String getSlotFilePath(int slot) { return Rpcs3Bridge.slotFilePath(slot); }
|
||||
|
||||
/** [MAPPED] Whether a slot holds a state. Ask this rather than probing a path. */
|
||||
public static boolean hasStateInSlot(int slot) { return Rpcs3Bridge.hasState(slot); }
|
||||
public static String getAutosaveGamePath() { Unsupported.note("getAutosaveGamePath"); return ""; }
|
||||
public static byte[] getAutosaveImage() { Unsupported.note("getAutosaveImage"); return null; }
|
||||
|
||||
|
||||
@@ -190,7 +190,11 @@ object Rpcs3Bridge {
|
||||
appContext?.let { com.armsx2.Ps3PatchRepo.ensureBundledPatches(it) }
|
||||
|
||||
val result = RPCSX.boot(target)
|
||||
if (result != BootResult.NoErrors) {
|
||||
// AlreadyAdded is not a failure: it says the title was already in games.yml, which is
|
||||
// the normal case for anything booted before. RPCS3's own front-end passes it through
|
||||
// for that reason. Treating it as an error turned a re-boot into "Game failed to
|
||||
// start: AlreadyAdded" and sent the user back to the library.
|
||||
if (result != BootResult.NoErrors && result != BootResult.AlreadyAdded) {
|
||||
lastBootError = result.name
|
||||
android.util.Log.e("ARMSX3", "boot failed: ${result.name} path=$target")
|
||||
return false
|
||||
@@ -552,6 +556,16 @@ object Rpcs3Bridge {
|
||||
else -> return false
|
||||
}
|
||||
|
||||
// Named for the config node it writes, unlike the PS3/* pseudo-sections above.
|
||||
// Without this case the key fell through to the else and was dropped, so the
|
||||
// setting never reached the core: savestates failed to lock the SPUs and told
|
||||
// the user to enable an option that had no effect however they set it.
|
||||
"Savestate" -> when (key) {
|
||||
"Compatible Savestate Mode" ->
|
||||
Rpcs3Settings.setCompatibleSavestateMode(asBool(value))
|
||||
else -> return false
|
||||
}
|
||||
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
@@ -602,6 +616,44 @@ object Rpcs3Bridge {
|
||||
fun hasState(slot: Int): Boolean =
|
||||
runCatching { RPCSX.instance.hasStateInSlot(slot) }.getOrDefault(false)
|
||||
|
||||
/**
|
||||
* The auto-save lives one slot above the ten the picker shows.
|
||||
*
|
||||
* Auto-save-on-exit, auto-load-on-boot and the interval auto-save were ARMSX2 shims that
|
||||
* returned false and were never ported, so all three toggles persisted, read back, and
|
||||
* did nothing -- the interval job woke on schedule for a function that always failed.
|
||||
* Nothing bounds a slot number on either side of the JNI, so they reuse the numbered-slot
|
||||
* path that works rather than growing a second mechanism, and the user's ten stay theirs.
|
||||
*/
|
||||
private const val AUTOSAVE_SLOT = 10
|
||||
|
||||
@JvmStatic
|
||||
fun hasAutosaveState(): Boolean = hasState(AUTOSAVE_SLOT)
|
||||
|
||||
@JvmStatic
|
||||
fun saveAutosaveState(): Boolean = saveState(AUTOSAVE_SLOT)
|
||||
|
||||
@JvmStatic
|
||||
fun loadAutosaveState(): Boolean = loadState(AUTOSAVE_SLOT)
|
||||
|
||||
/**
|
||||
* Absolute path a slot's state file would occupy, whether or not one is there.
|
||||
*
|
||||
* gamePathForSlot answers with the TITLE ID -- the picker wants it as a subtitle -- so
|
||||
* anything treating it as a path gets a relative name that resolves against the process
|
||||
* working directory. Import did exactly that. This is the real location, built the way
|
||||
* armsx3_slot_dir builds it natively.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun slotFilePath(slot: Int): String? = runCatching {
|
||||
val title = RPCSX.instance.getTitleId().takeIf { it.isNotEmpty() } ?: return null
|
||||
val root = com.armsx2.runtime.MainActivityRuntime.systemDirPosix()
|
||||
?: com.armsx2.runtime.MainActivityRuntime.instance
|
||||
?.applicationContext?.getExternalFilesDir(null)?.absolutePath
|
||||
?: return null
|
||||
java.io.File(root, "config/savestates/$title/armsx3_slots/slot$slot.SAVESTAT.zst").absolutePath
|
||||
}.getOrNull()
|
||||
|
||||
/**
|
||||
* Occupancy for the slot picker, which treats a non-empty string as "this slot has
|
||||
* something in it" and shows the last path segment as the tile's subtitle.
|
||||
@@ -626,7 +678,15 @@ object Rpcs3Bridge {
|
||||
@JvmStatic
|
||||
fun thumbnailForSlot(slot: Int): ByteArray? = runCatching {
|
||||
val title = RPCSX.instance.getTitleId().takeIf { it.isNotEmpty() } ?: return null
|
||||
val root = com.armsx2.runtime.MainActivityRuntime.systemDirPosix() ?: return null
|
||||
// systemDirPosix() is null on the DEFAULT install, where no folder was ever picked,
|
||||
// so this returned no thumbnail at all for most setups -- the tiles read as empty
|
||||
// while the files sat on disk beside the states they belong to. Same fallback
|
||||
// inputProfilesDir() uses, and getExternalFilesDir is where the native core roots
|
||||
// fs::get_config_dir(), which is where it wrote these.
|
||||
val root = com.armsx2.runtime.MainActivityRuntime.systemDirPosix()
|
||||
?: com.armsx2.runtime.MainActivityRuntime.instance
|
||||
?.applicationContext?.getExternalFilesDir(null)?.absolutePath
|
||||
?: return null
|
||||
val file = java.io.File(root, "config/savestates/$title/armsx3_slots/slot$slot.thumb")
|
||||
if (!file.isFile) return null
|
||||
|
||||
|
||||
@@ -253,6 +253,10 @@ object Rpcs3Settings {
|
||||
fun setSuspendModeSavestates(enabled: Boolean) =
|
||||
setBool("$SAVESTATE@@Suspend Emulation Savestate Mode", enabled)
|
||||
|
||||
/** SPU codegen that keeps the thread state capturable, which savestates require. */
|
||||
fun setCompatibleSavestateMode(enabled: Boolean) =
|
||||
setBool("$SAVESTATE@@Compatible Savestate Mode", enabled)
|
||||
|
||||
fun setMaxSavestateFiles(count: Int) =
|
||||
setInt("$SAVESTATE@@Maximum SaveState Files", count.coerceIn(0, 64))
|
||||
|
||||
|
||||
@@ -1767,14 +1767,79 @@ private:
|
||||
}
|
||||
} static g_compilationQueue;
|
||||
|
||||
// Runs the callbacks Emu posts to the main thread.
|
||||
//
|
||||
// CallFromMainThread with no wake_up is a POST, not a call: upstream hands it to the GUI
|
||||
// thread and returns immediately. Running it inline instead executes it under whatever
|
||||
// locks the caller happens to hold, and lv2_obj::sleep_unlocked posts one while holding
|
||||
// lv2_obj::g_mutex -- the comment upstream put on that call site says to run it on the main
|
||||
// thread for exactly that reason. The callback is FinalizeRunRequest, the wake for a
|
||||
// restored savestate, so it took g_mutex against itself and every thread stopped there:
|
||||
// loading a state, and saving one (a save stops and restarts into the state), parked with
|
||||
// the SPUs spinning and the progress overlay frozen on the figure it last drew.
|
||||
//
|
||||
// Callers that pass wake_up are synchronising on completion (BlockingCallFromMainThread
|
||||
// waits on it), so those keep running inline exactly as before.
|
||||
static struct main_thread_dispatcher {
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
std::deque<std::function<void()>> queue;
|
||||
std::thread worker;
|
||||
bool started = false;
|
||||
|
||||
void post(std::function<void()> cb) {
|
||||
std::unique_lock lock(mutex);
|
||||
|
||||
if (!started) {
|
||||
started = true;
|
||||
worker = std::thread([this] { run(); });
|
||||
worker.detach();
|
||||
}
|
||||
|
||||
queue.push_back(std::move(cb));
|
||||
cv.notify_one();
|
||||
}
|
||||
|
||||
void run() {
|
||||
pthread_setname_np(pthread_self(), "Main Callbacks");
|
||||
|
||||
for (;;) {
|
||||
std::function<void()> cb;
|
||||
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
cv.wait(lock, [this] { return !queue.empty(); });
|
||||
cb = std::move(queue.front());
|
||||
queue.pop_front();
|
||||
}
|
||||
|
||||
// One callback throwing must not take the dispatcher with it: everything posted
|
||||
// after it would be dropped, including the savestate wake.
|
||||
try {
|
||||
cb();
|
||||
} catch (const std::exception &e) {
|
||||
rpcsx_android.error("Main-thread callback threw: %s", e.what());
|
||||
} catch (...) {
|
||||
rpcsx_android.error("Main-thread callback threw an unknown exception");
|
||||
}
|
||||
}
|
||||
}
|
||||
} g_mainThreadDispatcher;
|
||||
|
||||
static void setupCallbacks() {
|
||||
Emu.SetCallbacks({
|
||||
.call_from_main_thread =
|
||||
[](std::function<void()> cb, atomic_t<u32> *wake_up) {
|
||||
cb();
|
||||
if (wake_up) {
|
||||
// The caller is waiting on this one; running it here is what it
|
||||
// already did and is what BlockingCallFromMainThread expects.
|
||||
cb();
|
||||
*wake_up = true;
|
||||
wake_up->notify_one();
|
||||
return;
|
||||
}
|
||||
|
||||
g_mainThreadDispatcher.post(std::move(cb));
|
||||
},
|
||||
.on_run = [](auto...) {},
|
||||
.on_pause = [](auto...) {},
|
||||
|
||||
+20
-2
@@ -625,9 +625,18 @@ bool SCEDecrypter::LoadHeaders()
|
||||
|
||||
bool SCEDecrypter::LoadMetadata(const u8 erk[32], const u8 riv[16])
|
||||
{
|
||||
const u64 sce_size = sce_f.size();
|
||||
const u64 headers_off = u64{sce_hdr.se_meta} + sizeof(sce_hdr) + sizeof(meta_info);
|
||||
|
||||
if (sce_hdr.se_hsize > sce_size || headers_off > sce_hdr.se_hsize)
|
||||
{
|
||||
self_log.error("Invalid SCE metadata layout (header size=0x%x, metadata offset=0x%x, file size=0x%x)", sce_hdr.se_hsize, sce_hdr.se_meta, sce_size);
|
||||
return false;
|
||||
}
|
||||
|
||||
aes_context aes;
|
||||
std::vector<u8> metadata_info(sizeof(meta_info));
|
||||
std::vector<u8> metadata_headers(sce_hdr.se_hsize - (sizeof(sce_hdr) + sce_hdr.se_meta + sizeof(meta_info)));
|
||||
std::vector<u8> metadata_headers(sce_hdr.se_hsize - headers_off);
|
||||
|
||||
// Locate and read the encrypted metadata info.
|
||||
sce_f.seek(sce_hdr.se_meta + sizeof(sce_hdr));
|
||||
@@ -1102,9 +1111,18 @@ const NPD_HEADER* SELFDecrypter::GetNPDHeader() const
|
||||
|
||||
bool SELFDecrypter::LoadMetadata(const u8* klic_key)
|
||||
{
|
||||
const u64 self_size = self_f.size();
|
||||
const u64 headers_off = u64{sce_hdr.se_meta} + sizeof(sce_hdr) + sizeof(meta_info);
|
||||
|
||||
if (sce_hdr.se_hsize > self_size || headers_off > sce_hdr.se_hsize)
|
||||
{
|
||||
self_log.error("Invalid SELF metadata layout (header size=0x%x, metadata offset=0x%x, file size=0x%x)", sce_hdr.se_hsize, sce_hdr.se_meta, self_size);
|
||||
return false;
|
||||
}
|
||||
|
||||
aes_context aes;
|
||||
std::vector<u8> metadata_info(sizeof(meta_info));
|
||||
std::vector<u8> metadata_headers(sce_hdr.se_hsize - (sizeof(sce_hdr) + sce_hdr.se_meta + sizeof(meta_info)));
|
||||
std::vector<u8> metadata_headers(sce_hdr.se_hsize - headers_off);
|
||||
|
||||
// Locate and read the encrypted metadata info.
|
||||
self_f.seek(sce_hdr.se_meta + sizeof(sce_hdr));
|
||||
|
||||
@@ -113,8 +113,38 @@ static spu_program analyse_spu_llvm_program(spu_recompiler_base& compiler, const
|
||||
return compiler.analyse(ls.data(), program.entry_point);
|
||||
}
|
||||
|
||||
static shared_mutex s_spu_failed_blocks_mutex;
|
||||
static std::unordered_set<u32> s_spu_failed_blocks;
|
||||
|
||||
static bool spu_interpreter_fallback_available()
|
||||
{
|
||||
const auto interp = spu_runtime::g_interpreter;
|
||||
return interp && interp != spu_runtime::g_gateway;
|
||||
}
|
||||
|
||||
static bool spu_block_compile_failed(u32 entry_point)
|
||||
{
|
||||
reader_lock lock(s_spu_failed_blocks_mutex);
|
||||
return s_spu_failed_blocks.find(entry_point) != s_spu_failed_blocks.end();
|
||||
}
|
||||
|
||||
static void spu_mark_block_compile_failed(u32 entry_point)
|
||||
{
|
||||
std::lock_guard lock(s_spu_failed_blocks_mutex);
|
||||
|
||||
if (s_spu_failed_blocks.emplace(entry_point).second)
|
||||
{
|
||||
spu_log.error("SPU block 0x%05x cannot be compiled on this backend, its thread switches to the interpreter", entry_point);
|
||||
}
|
||||
}
|
||||
|
||||
static spu_function_t compile_spu_llvm_with_retry(std::unique_ptr<spu_recompiler_base>& compiler, const spu_program& program)
|
||||
{
|
||||
if (spu_block_compile_failed(program.entry_point))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
spu_llvm_compile_context context;
|
||||
|
||||
{
|
||||
@@ -126,24 +156,32 @@ static spu_function_t compile_spu_llvm_with_retry(std::unique_ptr<spu_recompiler
|
||||
}
|
||||
}
|
||||
|
||||
if (context.llvm_error.find(s_spu_llvm_reg_scavenge_error) == std::string::npos)
|
||||
if (context.llvm_error.empty())
|
||||
{
|
||||
if (!context.llvm_error.empty())
|
||||
{
|
||||
spu_log.error("LLVM failed to compile SPU block 0x%x: %s", program.entry_point, context.llvm_error);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
spu_log.warning("LLVM failed to compile SPU block 0x%x with TBL2/TBX2: %s. Retrying without TBL2/TBX2.", program.entry_point, context.llvm_error);
|
||||
const bool scavenge_failure = context.llvm_error.find(s_spu_llvm_reg_scavenge_error) != std::string::npos;
|
||||
|
||||
if (scavenge_failure)
|
||||
{
|
||||
spu_log.warning("LLVM failed to compile SPU block 0x%x with TBL2/TBX2: %s. Retrying without TBL2/TBX2.", program.entry_point, context.llvm_error);
|
||||
}
|
||||
else
|
||||
{
|
||||
spu_log.error("LLVM failed to compile SPU block 0x%x: %s. Discarding the poisoned JIT instance.", program.entry_point, context.llvm_error);
|
||||
}
|
||||
|
||||
// LLVM fatal recovery does not unwind MCJIT state. Abandon the failed
|
||||
// compiler and retry from a fresh analysis/JIT instance.
|
||||
static_cast<void>(compiler.release());
|
||||
compiler = spu_recompiler_base::make_llvm_recompiler();
|
||||
compiler->init();
|
||||
|
||||
if (!scavenge_failure)
|
||||
{
|
||||
spu_mark_block_compile_failed(program.entry_point);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto retry_program = analyse_spu_llvm_program(*compiler, program);
|
||||
|
||||
if (retry_program != program)
|
||||
@@ -153,20 +191,32 @@ static spu_function_t compile_spu_llvm_with_retry(std::unique_ptr<spu_recompiler
|
||||
}
|
||||
|
||||
spu_llvm_compile_context retry_context;
|
||||
spu_llvm_compile_scope scope(retry_context, false);
|
||||
spu_function_t result = nullptr;
|
||||
|
||||
const auto result = compiler->compile(spu_program{retry_program});
|
||||
{
|
||||
spu_llvm_compile_scope scope(retry_context, false);
|
||||
|
||||
result = compiler->compile(spu_program{retry_program});
|
||||
}
|
||||
|
||||
if (result)
|
||||
{
|
||||
spu_log.notice("SPU LLVM block 0x%x compiled successfully without TBL2/TBX2.", program.entry_point);
|
||||
}
|
||||
else if (!retry_context.llvm_error.empty())
|
||||
{
|
||||
spu_log.error("LLVM failed to compile SPU block 0x%x without TBL2/TBX2: %s", program.entry_point, retry_context.llvm_error);
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
if (!retry_context.llvm_error.empty())
|
||||
{
|
||||
spu_log.error("LLVM failed to compile SPU block 0x%x without TBL2/TBX2: %s. Discarding the poisoned JIT instance.", program.entry_point, retry_context.llvm_error);
|
||||
|
||||
static_cast<void>(compiler.release());
|
||||
compiler = spu_recompiler_base::make_llvm_recompiler();
|
||||
compiler->init();
|
||||
|
||||
spu_mark_block_compile_failed(program.entry_point);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -2213,6 +2263,15 @@ void spu_recompiler_base::dispatch(spu_thread& spu, void*, u8* rip)
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ARCH_ARM64
|
||||
if (spu_interpreter_fallback_available() && spu_block_compile_failed(spu.pc))
|
||||
{
|
||||
spu.interp_fallback = true;
|
||||
spu_runtime::g_escape(&spu);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
pthread_jit_write_protect_np(false);
|
||||
#endif
|
||||
@@ -2225,6 +2284,18 @@ void spu_recompiler_base::dispatch(spu_thread& spu, void*, u8* rip)
|
||||
|
||||
if (!func)
|
||||
{
|
||||
#ifdef ARCH_ARM64
|
||||
if (spu_interpreter_fallback_available())
|
||||
{
|
||||
#if defined(__APPLE__)
|
||||
pthread_jit_write_protect_np(true);
|
||||
#endif
|
||||
spu.interp_fallback = true;
|
||||
spu_runtime::g_escape(&spu);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
spu_log.fatal("[0x%05x] Compilation failed.", spu.pc);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1633,6 +1633,12 @@ public:
|
||||
|
||||
virtual spu_function_t compile(spu_program&& _func) override
|
||||
{
|
||||
if (m_jit.is_poisoned())
|
||||
{
|
||||
spu_log.error("Refusing to compile SPU block 0x%05x on a poisoned LLVM engine", _func.entry_point);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (_func.data.empty() && m_interp_magn)
|
||||
{
|
||||
return compile_interpreter();
|
||||
@@ -1665,6 +1671,15 @@ public:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The owner finished and published nothing. It is not coming back, so
|
||||
// waiting on `compiled` here is waiting forever -- state 2 is set after
|
||||
// the publication it promises, so seeing it with nothing published means
|
||||
// there is nothing to wait for.
|
||||
if (add_loc->llvm_compile_state == 2)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
add_loc->compiled.wait(nullptr, atomic_wait_timeout{10'000'000});
|
||||
}
|
||||
|
||||
@@ -1678,9 +1693,14 @@ public:
|
||||
// of cold compilation work.
|
||||
if (add_loc->llvm_compile_state.compare_and_swap(0, 1) != 0)
|
||||
{
|
||||
// Bounded, like the duplicate wait above. An untimed wait on a claim is only
|
||||
// as sound as every path the owner can leave by, and a waiter that misses the
|
||||
// transition waits for the rest of the session -- SPURS brings all its kernels
|
||||
// to the same block at once, so it is five threads, and the game sits polling
|
||||
// for an SPU that will never answer.
|
||||
while (add_loc->llvm_compile_state == 1)
|
||||
{
|
||||
add_loc->llvm_compile_state.wait(1);
|
||||
add_loc->llvm_compile_state.wait(1, atomic_wait_timeout{10'000'000});
|
||||
}
|
||||
|
||||
if (add_loc->llvm_compile_state == 2)
|
||||
@@ -4101,6 +4121,12 @@ public:
|
||||
{
|
||||
using namespace llvm;
|
||||
|
||||
if (m_jit.is_poisoned())
|
||||
{
|
||||
spu_log.error("Refusing to compile the SPU interpreter on a poisoned LLVM engine");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
m_engine->clearAllGlobalMappings();
|
||||
|
||||
// Create LLVM module
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user