mirror of
https://github.com/ARMSX2/ARMSX3.git
synced 2026-08-24 16:58:52 -07:00
UI: frame generation controls, and route the setting to the core at all
Adds the import row for Lossless.dll, the multiplier, Performance shaders and Motion detail, plus the strings for all of it. Frame Generation was not reaching the emulator. Rpcs3Bridge.setSetting is a translation table keyed by (section, key) and anything absent is silently dropped, so the toggle looked like it worked and did nothing. Enums also have to cross as NAMES rather than indices -- sending "1" would have been wrong even with the entry present. Found by an unconditional probe in the present path, after being wrong about the cause twice; the probe printed mode=0 while the UI held 1, which was the whole answer. Performance shaders default ON. It selects framegen's 3.1p shader family instead of 3.1, which is materially cheaper, and on a mobile GPU the full-quality path costs more than the frames it buys. Both families are extracted from the user's DLL already, so this switches between shaders that are both sitting in the cache. Motion detail is the optical-flow resolution, stored as a percentage rather than upstream's divisor so the slider reads the right way round. Both take effect when frame generation next starts, since the shader family and the flow scale are baked into framegen's device and pipelines at initialize; the descriptions say so. The description also warns about the two things testers will otherwise report as bugs: on-screen text shimmers because the overlay and the game's own menus are interpolated along with everything else, and toggling mid-game pauses for a few seconds while a second device and the pipelines are built.
This commit is contained in:
@@ -56,6 +56,9 @@ struct RPCSXApi {
|
||||
std::string (*getUser)();
|
||||
std::string (*settingsGet)(std::string_view path);
|
||||
bool (*settingsSet)(std::string_view path, std::string_view valueString);
|
||||
int (*frameGenImportShaders)(std::string_view path);
|
||||
int (*frameGenShaderCount)();
|
||||
const char *(*frameGenShaderError)();
|
||||
void (*settingsBeginBatch)();
|
||||
void (*settingsEndBatch)();
|
||||
bool (*installSplitPkg)(JNIEnv *env, const int *fds, int count, long progressId);
|
||||
@@ -147,6 +150,12 @@ struct RPCSXLibrary : RPCSXApi {
|
||||
result.getUser = reinterpret_cast<decltype(getUser)>(dlsym(handle, "_rpcsx_getUser"));
|
||||
result.settingsGet = reinterpret_cast<decltype(settingsGet)>(dlsym(handle, "_rpcsx_settingsGet"));
|
||||
result.settingsSet = reinterpret_cast<decltype(settingsSet)>(dlsym(handle, "_rpcsx_settingsSet"));
|
||||
// Resolved without ensure(): a core built before frame generation existed simply has no such
|
||||
// symbol, and refusing to load it over a missing optional feature would be worse than the
|
||||
// feature being absent. The Kotlin side treats a null here as "unsupported".
|
||||
result.frameGenImportShaders = reinterpret_cast<decltype(frameGenImportShaders)>(dlsym(handle, "_rpcsx_frameGenImportShaders"));
|
||||
result.frameGenShaderCount = reinterpret_cast<decltype(frameGenShaderCount)>(dlsym(handle, "_rpcsx_frameGenShaderCount"));
|
||||
result.frameGenShaderError = reinterpret_cast<decltype(frameGenShaderError)>(dlsym(handle, "_rpcsx_frameGenShaderError"));
|
||||
result.settingsBeginBatch = reinterpret_cast<decltype(settingsBeginBatch)>(dlsym(handle, "_rpcsx_settingsBeginBatch"));
|
||||
result.settingsEndBatch = reinterpret_cast<decltype(settingsEndBatch)>(dlsym(handle, "_rpcsx_settingsEndBatch"));
|
||||
result.installSplitPkg = reinterpret_cast<decltype(installSplitPkg)>(dlsym(handle, "_rpcsx_installSplitPkg"));
|
||||
@@ -1011,3 +1020,23 @@ Java_net_rpcsx_RPCSX_getRsxThreadTid(JNIEnv *, jobject) {
|
||||
}
|
||||
return static_cast<jint>(rpcsxLib.getRsxThreadTid());
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL
|
||||
Java_net_rpcsx_RPCSX_frameGenImportShaders(JNIEnv *env, jobject, jstring path) {
|
||||
if (!rpcsxLib.frameGenImportShaders) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return rpcsxLib.frameGenImportShaders(unwrap(env, path));
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL
|
||||
Java_net_rpcsx_RPCSX_frameGenShaderCount(JNIEnv *, jobject) {
|
||||
return rpcsxLib.frameGenShaderCount ? rpcsxLib.frameGenShaderCount() : 0;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL
|
||||
Java_net_rpcsx_RPCSX_frameGenShaderError(JNIEnv *env, jobject) {
|
||||
const char *msg = rpcsxLib.frameGenShaderError ? rpcsxLib.frameGenShaderError() : "";
|
||||
return env->NewStringUTF(msg ? msg : "");
|
||||
}
|
||||
|
||||
@@ -711,6 +711,7 @@ val EN: Map<String, String> = mapOf(
|
||||
"overlay.uiSize.description" to "Scales menu/library padding and control sizes. 100% = default.",
|
||||
"overlay.osdColor.label" to "OSD Color",
|
||||
"overlay.osdColor.description" to "Color of the on-screen display text (FPS, stats and notifications). Speed warnings stay red/green so they still stand out.",
|
||||
"overlay.osdColor.custom" to "Custom",
|
||||
"overlay.osdColor.default" to "White",
|
||||
"overlay.osdColor.green" to "Green",
|
||||
"overlay.osdColor.cyan" to "Cyan",
|
||||
@@ -1049,6 +1050,22 @@ val EN: Map<String, String> = mapOf(
|
||||
"perf.decoder.interpreterDyn" to "Interpreter (dyn)",
|
||||
"perf.decoder.asmjit" to "ASMJIT",
|
||||
"perf.decoder.llvm" to "LLVM",
|
||||
"perf.framegen.title" to "Frame Generation (Experimental)",
|
||||
"perf.framegen.import" to "Import from Lossless Scaling\u2026",
|
||||
"perf.framegen.import.missing" to "Shaders not imported \u2014 frame generation will not run.",
|
||||
"perf.framegen.import.ok" to "Shaders imported (%d).",
|
||||
"perf.framegen.import.working" to "Reading shaders\u2026",
|
||||
"perf.framegen.import.failed" to "Could not read shaders from that file.",
|
||||
"perf.framegen.label" to "Lossless Scaling",
|
||||
"perf.framegen.performance.label" to "Performance shaders",
|
||||
"perf.framegen.performance.description" to "Use Lossless Scaling's lighter 3.1p shaders instead of the full-quality 3.1 set. Cheaper to run and slightly softer in motion \u2014 on by default, because the quality set usually costs more than the frames it buys on a phone. Both come from the file you imported, so switching does not need another import.\n\nTakes effect when frame generation next starts: turn it off and on again, or restart the game.",
|
||||
"perf.framegen.flowScale.label" to "Motion detail",
|
||||
"perf.framegen.flowScale.description" to "How finely motion is measured between frames, as a percentage of full resolution. Lower is faster and blurrier around moving edges. Drop this before dropping the multiplier if frame generation is costing more than it gives.\n\nTakes effect when frame generation next starts.",
|
||||
"perf.framegen.off" to "Off",
|
||||
"perf.framegen.x2" to "x2",
|
||||
"perf.framegen.x3" to "x3",
|
||||
"perf.framegen.x4" to "x4",
|
||||
"perf.framegen.description" to "EXPERIMENTAL. Insert generated frames between the ones the game actually draws. Costs GPU time and adds latency, so it helps when the CPU is the limit and hurts when the GPU already is.\n\nIt works best from a steady framerate. Interpolating a game that is already struggling tends to look worse rather than better \u2014 generated frames land at the wrong moment when the real interval keeps changing, which reads as judder. A locked 25 usually looks better than a wandering 28.\n\nOn-screen text shimmers or flickers while this is on \u2014 the overlay and the game\u0027s own menus get interpolated along with everything else, and fine text is what that looks worst on. That is how frame generation behaves, not a fault. Turning it off restores steady text. Switching it on or off during a game also pauses for a few seconds while the shaders are prepared.\n\nThis does nothing until you import Lossless.dll below. It is part of Lossless Scaling on Steam \u2014 you need your own copy, and nothing is bundled or downloaded. On Windows the file sits in steamapps\\common\\Lossless Scaling\\Lossless.dll; copy it to your device and pick it with the button below. Only the shaders are kept, and your copy of the file is deleted afterwards.",
|
||||
"perf.ppuDecoder.label" to "PPU Decoder",
|
||||
"perf.ppuDecoder.description" to "How the PS3's main CPU (PPU) is executed. LLVM recompiles PowerPC to native ARM64 and is enormously faster \u2014 keep it unless you are debugging. Interpreter is only for diagnosing a game LLVM gets wrong.",
|
||||
"perf.spuDecoder.label" to "SPU Decoder",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package com.armsx2.ui.settings
|
||||
|
||||
import net.rpcsx.RPCSX
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -397,6 +404,43 @@ fun PerformanceTab(state: MutableState<Settings>) {
|
||||
)
|
||||
}
|
||||
SettingsDivider()
|
||||
// Frame generation. Its own section rather than folded into the GPU one because it is
|
||||
// not a rendering option -- it inserts frames that the game never drew, and the choice
|
||||
// to do that is a different kind of decision from how the real ones are drawn.
|
||||
CollapsibleSection(str("perf.framegen.title")) {
|
||||
SegmentedGridRow(
|
||||
label = str("perf.framegen.label"),
|
||||
options = listOf(
|
||||
str("perf.framegen.off"),
|
||||
str("perf.framegen.x2"),
|
||||
str("perf.framegen.x3"),
|
||||
str("perf.framegen.x4"),
|
||||
),
|
||||
selectedIndex = s.ps3.frameGeneration.coerceIn(0, 3),
|
||||
columns = 4,
|
||||
description = str("perf.framegen.description"),
|
||||
onChange = { apply(s.copy(ps3 = s.ps3.copy(frameGeneration = it))) },
|
||||
)
|
||||
SettingsDivider()
|
||||
ToggleRow(
|
||||
str("perf.framegen.performance.label"),
|
||||
s.ps3.frameGenPerformance,
|
||||
description = str("perf.framegen.performance.description"),
|
||||
) { apply(s.copy(ps3 = s.ps3.copy(frameGenPerformance = it))) }
|
||||
SettingsDivider()
|
||||
IntSliderRow(
|
||||
label = str("perf.framegen.flowScale.label"),
|
||||
value = s.ps3.frameGenFlowScale.coerceIn(25, 100),
|
||||
min = 25,
|
||||
max = 100,
|
||||
description = str("perf.framegen.flowScale.description"),
|
||||
valueFormatter = { "$it%" },
|
||||
onChange = { apply(s.copy(ps3 = s.ps3.copy(frameGenFlowScale = it))) },
|
||||
)
|
||||
SettingsDivider()
|
||||
FrameGenShaderRow()
|
||||
}
|
||||
SettingsDivider()
|
||||
// Compiled-code caches. Separate from the shader cache on the Renderer tab:
|
||||
// these hold recompiled PPU/SPU code, not GPU pipelines.
|
||||
CollapsibleSection(str("perf.caches.title")) {
|
||||
@@ -520,3 +564,84 @@ private fun ClearCacheRow(spuOnly: Boolean) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the shaders frame generation needs, and say plainly when they are missing.
|
||||
*
|
||||
* The picker on its own is a toggle that appears to do nothing: framegen refuses to start without
|
||||
* shaders, and nothing in the app ships them. They are THS's property and have to come from a
|
||||
* legitimately purchased copy of Lossless Scaling, so the state has to be visible at the point of
|
||||
* use rather than explained in a description nobody reads.
|
||||
*
|
||||
* The file is copied into app storage before extraction rather than read through the content URI:
|
||||
* the PE walk is ordinary file IO on the native side and cannot open a content:// path. The copy
|
||||
* is deleted afterwards -- only the translated SPIR-V is kept.
|
||||
*/
|
||||
@Composable
|
||||
private fun FrameGenShaderRow() {
|
||||
val ctx = LocalContext.current
|
||||
var count by remember { mutableStateOf(runCatching { RPCSX.instance.frameGenShaderCount() }.getOrDefault(0)) }
|
||||
var error by remember { mutableStateOf("") }
|
||||
var busy by remember { mutableStateOf(false) }
|
||||
|
||||
// Resolved here, not in the callback: str() is @Composable and the picker result arrives
|
||||
// outside composition.
|
||||
val failedMsg = str("perf.framegen.import.failed")
|
||||
|
||||
val picker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||
if (uri != null) {
|
||||
busy = true
|
||||
error = ""
|
||||
|
||||
runCatching {
|
||||
// Named .dll but the picker filters on */* -- Android has no MIME type for a PE
|
||||
// binary and several file providers report octet-stream or nothing at all.
|
||||
val tmp = java.io.File(ctx.cacheDir, "Lossless.dll")
|
||||
|
||||
ctx.contentResolver.openInputStream(uri)?.use { input ->
|
||||
tmp.outputStream().use { output -> input.copyTo(output) }
|
||||
}
|
||||
|
||||
val n = RPCSX.instance.frameGenImportShaders(tmp.absolutePath)
|
||||
tmp.delete()
|
||||
|
||||
if (n > 0) {
|
||||
count = n
|
||||
} else {
|
||||
error = RPCSX.instance.frameGenShaderError().ifEmpty { failedMsg }
|
||||
}
|
||||
}.onFailure { error = it.message ?: failedMsg }
|
||||
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
when {
|
||||
busy -> str("perf.framegen.import.working")
|
||||
count > 0 -> str("perf.framegen.import.ok").format(count)
|
||||
else -> str("perf.framegen.import.missing")
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (count > 0) MaterialTheme.colorScheme.onSurfaceVariant
|
||||
else MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(start = 4.dp, top = 2.dp),
|
||||
)
|
||||
|
||||
if (error.isNotEmpty()) {
|
||||
Text(
|
||||
error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(start = 4.dp, top = 2.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Row(Modifier.fillMaxWidth().padding(top = 4.dp)) {
|
||||
val pick = { picker.launch(arrayOf("*/*")) }
|
||||
OutlinedButton(
|
||||
onClick = pick,
|
||||
modifier = Modifier.controllerFocusable("perf.framegen.import", onConfirm = pick),
|
||||
) { Text(str("perf.framegen.import")) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,6 +536,9 @@ object Rpcs3Bridge {
|
||||
"Resolution Scale" -> Rpcs3Settings.setResolutionScalePercent(asInt(value))
|
||||
"MSAA" -> Rpcs3Settings.setMsaa(asInt(value))
|
||||
"Shader Mode" -> Rpcs3Settings.setShaderMode(asInt(value))
|
||||
"Frame Generation" -> Rpcs3Settings.setFrameGeneration(asInt(value))
|
||||
"Frame Generation Performance Mode" -> Rpcs3Settings.setFrameGenPerformance(asBool(value))
|
||||
"Frame Generation Flow Scale" -> Rpcs3Settings.setFrameGenFlowScale(asInt(value))
|
||||
"Write Color Buffers" -> Rpcs3Settings.setWriteColorBuffers(asBool(value))
|
||||
"Write Depth Buffer" -> Rpcs3Settings.setWriteDepthBuffer(asBool(value))
|
||||
"Read Color Buffers" -> Rpcs3Settings.setReadColorBuffers(asBool(value))
|
||||
|
||||
@@ -403,6 +403,19 @@ object Rpcs3Settings {
|
||||
fun setShaderMode(index: Int) =
|
||||
setEnum("$VIDEO@@Shader Mode", SHADER_MODES.getOrElse(index) { SHADER_MODES[2] })
|
||||
|
||||
/** Frame Generation. Names, not indices -- cfg::_enum matches on the string the core's
|
||||
* fmt_class_string produces, and these have to stay in step with frame_generation_mode. */
|
||||
private val FRAME_GENERATION = arrayOf("Off", "x2", "x3", "x4")
|
||||
|
||||
fun setFrameGenPerformance(on: Boolean) =
|
||||
setBool("$VIDEO@@Frame Generation Performance Mode", on)
|
||||
|
||||
fun setFrameGenFlowScale(percent: Int) =
|
||||
setInt("$VIDEO@@Frame Generation Flow Scale", percent.coerceIn(25, 100))
|
||||
|
||||
fun setFrameGeneration(index: Int) =
|
||||
setEnum("$VIDEO@@Frame Generation", FRAME_GENERATION.getOrElse(index) { FRAME_GENERATION[0] })
|
||||
|
||||
fun setWriteDepthBuffer(v: Boolean) = setBool("$VIDEO@@Write Depth Buffer", v)
|
||||
fun setReadColorBuffers(v: Boolean) = setBool("$VIDEO@@Read Color Buffers", v)
|
||||
fun setReadDepthBuffer(v: Boolean) = setBool("$VIDEO@@Read Depth Buffer", v)
|
||||
|
||||
@@ -71,6 +71,12 @@ enum class BootResult
|
||||
|
||||
class RPCSX {
|
||||
external fun openLibrary(path: String): Boolean
|
||||
|
||||
/** Extract the Lossless Scaling shaders from a real filesystem path. Returns how many were
|
||||
* found, or a negative value; frameGenShaderError() then explains why. */
|
||||
external fun frameGenImportShaders(path: String): Int
|
||||
external fun frameGenShaderCount(): Int
|
||||
external fun frameGenShaderError(): String
|
||||
external fun getLibraryVersion(path: String): String?
|
||||
external fun initialize(rootDir: String, user: String, socInfo: String): Boolean
|
||||
external fun installFw(fd: Int, progressId: Long): Boolean
|
||||
|
||||
Reference in New Issue
Block a user