mirror of
https://github.com/ARMSX2/ARMSX3.git
synced 2026-08-24 16:58:52 -07:00
Join the GPU and CPU pass tables on the same ordinal, and reach external storage
The two by-pass tables were joined on counters that reset at different points. tick_frame runs from on_frame_end, before flip; the GPU timer rotates its slot at the top of flip and then drops every non-frame region on the fresh slot, which is flip's own overlay and calibration passes -- and those still incremented the CPU counter. So the CPU ordinal ran ahead by the number of present-path passes and the two tables described different passes. A whole anomaly came out of that: a pass whose GPU cost was joined to a neighbour's workload read as 36x the per-draw cost of its peers. The comment claiming both reset on the same boundary was wrong. Reset where the GPU slot actually rotates instead. Also adds a Storage Access Framework route to the package installer. The in-app browser walks java.io.File, which only reaches storage this process can open by path, so a .pkg on a USB-OTG drive or an SD card was unreachable and had to be copied to internal storage first. Packages are handed over as the descriptor SAF already returned -- the native side takes a raw fd, so nothing is copied and a 4 GB package costs no extra space; licences are 16 bytes and their installer wants a real file, so those alone are staged.
This commit is contained in:
@@ -394,6 +394,7 @@ val EN: Map<String, String> = mapOf(
|
||||
"packages.description" to "Install a .pkg game, update or DLC, or a .rap licence file. Some games need both: the .pkg holds the content and the .rap unlocks it. Installed titles are added to your library automatically, and updates and DLC need the base game installed first.",
|
||||
"packages.select.title" to "Select a .pkg or .rap file",
|
||||
"packages.select.action" to "Choose file",
|
||||
"packages.select.external" to "Choose from USB or SD card",
|
||||
"packages.installing" to "Installing. Large packages can take a few minutes.",
|
||||
"packages.install.done" to "Installed. It will appear in your library on the next scan.",
|
||||
"packages.install.failed" to "Install failed. The file may be encrypted, incomplete or not a PS3 package.",
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.armsx2.ui.packages
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -175,6 +176,106 @@ fun PackageInstallerScreen(onBack: () -> Unit) {
|
||||
// licence, the obvious thing to do, could therefore only ever fail.
|
||||
//
|
||||
// Packages install FIRST: a licence unlocks content the package has to have written.
|
||||
/**
|
||||
* Install from a Storage Access Framework pick.
|
||||
*
|
||||
* The in-app browser walks java.io.File, which only reaches storage this process can open
|
||||
* by path -- internal, and its own external dirs. A .pkg on a USB-OTG drive or on some SD
|
||||
* cards is not reachable that way at all, so those users had to copy multi-gigabyte files
|
||||
* to internal storage first. Reported as issue #16.
|
||||
*
|
||||
* Packages are handed over as the descriptor SAF already gave us: the native side takes a
|
||||
* raw fd, so nothing is copied and a 4 GB package costs no extra space. Licences are 16
|
||||
* bytes and their installer wants a real file, so those alone are staged into the cache.
|
||||
*/
|
||||
fun installFromUris(uris: List<android.net.Uri>) {
|
||||
if (uris.isEmpty()) return
|
||||
busy = true
|
||||
message = null
|
||||
MainActivityRuntime.invoke {
|
||||
var nativeFailure: String? = null
|
||||
val ok = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val resolver = context.contentResolver
|
||||
|
||||
fun displayName(uri: android.net.Uri): String =
|
||||
runCatching {
|
||||
resolver.query(uri, null, null, null, null)?.use { c ->
|
||||
val i = c.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME)
|
||||
if (i >= 0 && c.moveToFirst()) c.getString(i) else null
|
||||
}
|
||||
}.getOrNull() ?: uri.lastPathSegment.orEmpty()
|
||||
|
||||
val named = uris.map { it to displayName(it) }
|
||||
val licenceUris = named.filter { (_, n) ->
|
||||
n.endsWith(".rap", true) || n.endsWith(".edat", true)
|
||||
}
|
||||
val packageUris = named.filterNot { (_, n) ->
|
||||
n.endsWith(".rap", true) || n.endsWith(".edat", true)
|
||||
}
|
||||
|
||||
val label = if (uris.size == 1) named[0].second else "${uris.size} files"
|
||||
val id = ProgressRepository.create(context, "Installing $label")
|
||||
progressId = id
|
||||
val progressEntry = ProgressRepository.getItem(id)
|
||||
|
||||
var result = true
|
||||
if (packageUris.isNotEmpty()) {
|
||||
val descriptors = packageUris.mapNotNull { (uri, _) ->
|
||||
runCatching { resolver.openFileDescriptor(uri, "r") }.getOrNull()
|
||||
}
|
||||
if (descriptors.size != packageUris.size) {
|
||||
descriptors.forEach { runCatching { it.close() } }
|
||||
return@runCatching false
|
||||
}
|
||||
try {
|
||||
result = if (descriptors.size == 1) {
|
||||
RPCSX.instance.install(descriptors[0].fd, id)
|
||||
} else {
|
||||
RPCSX.instance.installSplitPkg(descriptors.map { it.fd }.toIntArray(), id)
|
||||
}
|
||||
} finally {
|
||||
descriptors.forEach { runCatching { it.close() } }
|
||||
}
|
||||
}
|
||||
|
||||
for ((uri, name) in licenceUris) {
|
||||
if (!result) break
|
||||
val staged = java.io.File(context.cacheDir, name)
|
||||
val copied = runCatching {
|
||||
resolver.openInputStream(uri)?.use { input ->
|
||||
staged.outputStream().use { out -> input.copyTo(out) }
|
||||
} != null
|
||||
}.getOrDefault(false)
|
||||
if (!copied) { result = false; break }
|
||||
result = if (name.endsWith(".rap", true)) {
|
||||
Licences.installRap(staged)
|
||||
} else {
|
||||
val d = ParcelFileDescriptor.open(staged, ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
try { RPCSX.instance.installKey(d.fd, id, "") } finally { runCatching { d.close() } }
|
||||
}
|
||||
runCatching { staged.delete() }
|
||||
}
|
||||
|
||||
progressEntry?.value?.takeIf { it.isFailed() }?.let {
|
||||
nativeFailure = it.message.value
|
||||
}
|
||||
result
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
busy = false
|
||||
progressId = null
|
||||
message = if (ok) {
|
||||
GameLibraryRepository(context).invalidateCache()
|
||||
installed = readInstalled()
|
||||
licences = readLicences()
|
||||
I18n.get("packages.install.done")
|
||||
} else {
|
||||
nativeFailure?.takeIf { it.isNotBlank() } ?: I18n.get("packages.install.failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun install(files: List<java.io.File>) {
|
||||
if (files.isEmpty()) return
|
||||
showBrowser = false
|
||||
@@ -327,6 +428,10 @@ fun PackageInstallerScreen(onBack: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
val safPicker = androidx.activity.compose.rememberLauncherForActivityResult(
|
||||
androidx.activity.result.contract.ActivityResultContracts.OpenMultipleDocuments(),
|
||||
) { uris -> if (!uris.isNullOrEmpty()) installFromUris(uris) }
|
||||
|
||||
if (showBrowser) {
|
||||
FileBrowserDialog(
|
||||
title = str("packages.select.title"),
|
||||
@@ -398,6 +503,14 @@ fun PackageInstallerScreen(onBack: () -> Unit) {
|
||||
Button(onClick = { showBrowser = true }) {
|
||||
Text(str("packages.select.action"))
|
||||
}
|
||||
// Reaches storage the in-app browser cannot open by path: USB-OTG,
|
||||
// and SD cards on devices that only expose them through SAF.
|
||||
Button(
|
||||
onClick = { safPicker.launch(arrayOf("*/*")) },
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
) {
|
||||
Text(str("packages.select.external"))
|
||||
}
|
||||
}
|
||||
|
||||
message?.let {
|
||||
|
||||
@@ -332,6 +332,17 @@ void VKGSRender::queue_swap_request()
|
||||
// reported nothing at all for the whole session while looking perfectly healthy.
|
||||
vk::get_gpu_timer().begin(*m_current_command_buffer, vk::gpu_timer::region::frame);
|
||||
|
||||
// Restart CPU pass numbering HERE, where the GPU timer's slot actually rotates, so an
|
||||
// ordinal names the same pass on both sides. It used to reset in tick_frame, which runs
|
||||
// from on_frame_end -- before flip -- while flip's own overlay and calibration passes went
|
||||
// on incrementing it and were dropped GPU-side. The CPU ordinal therefore ran ahead by the
|
||||
// number of present-path passes and the two by-pass tables described different passes.
|
||||
//
|
||||
// This site rather than next_frame(): queue_swap_request has one call site, so the mid-frame
|
||||
// flush_command_queue reopen cannot falsely restart numbering, and flip's passes land after
|
||||
// the reset where they belong rather than taking ordinals 0..k-1.
|
||||
rsx::prof::g_pass_ordinal = umax;
|
||||
|
||||
// Set up new pointers for the next frame
|
||||
advance_queued_frames();
|
||||
}
|
||||
|
||||
@@ -245,10 +245,18 @@ namespace rsx::prof
|
||||
|
||||
g_acc.frames++;
|
||||
|
||||
// Restart pass numbering so an ordinal means the same pass here as it does in the GPU
|
||||
// timer, whose event index resets on the same boundary. Wraps to zero on the first
|
||||
// pass; anything counted before one opens lands out of range and is discarded.
|
||||
g_pass_ordinal = umax;
|
||||
// Pass numbering is NOT restarted here.
|
||||
//
|
||||
// It used to be, on the claim that the GPU timer's event index resets on the same
|
||||
// boundary. It does not. tick_frame runs from on_frame_end, BEFORE flip; the GPU timer
|
||||
// rotates its slot at the top of flip and then drops every non-frame region recorded on
|
||||
// the fresh slot -- which is flip's own overlay and calibration passes. Those passes
|
||||
// still increment this counter, so the CPU ordinal ran ahead of the GPU ordinal by the
|
||||
// number of present-path passes, and the two by-pass tables described different passes.
|
||||
//
|
||||
// A whole "anomaly" came out of that: a pass whose GPU cost was joined to another pass's
|
||||
// workload read as 36x the per-draw cost of its neighbours. Reset at the flip point
|
||||
// instead, where the GPU slot actually rotates. See VKPresent.cpp.
|
||||
|
||||
// Report on a frame boundary rather than a timer, so per-frame costs divide by a
|
||||
// whole number of frames and a long stall lands in the window that contains it.
|
||||
|
||||
Reference in New Issue
Block a user