6 Commits
Author SHA1 Message Date
jpolo1224 39ca5cdab6 0.7.2: settings fixes, Oboe by default, and a working per-section Reset
Per-section Reset did nothing on most tabs. The field lists describe the tabs
as they were before the PS3 rewrite, so Reset was clearing settings the tabs no
longer show while missing most of what they do: Performance listed 22 of 47,
Graphics 45 of 57, Audio 10 of 16 -- audioRenderer, audioFormat, audioChannels
and audioCubebBackend were absent, so changing the audio backend and pressing
Reset was a no-op. Regenerated from what each tab actually writes, mapping
ps3.foo to its ps3Foo key and validating every entry against the serialiser.
Five keys also moved off Graphics because another tab owns them, which was a
cross-tab clobber waiting to happen.

Full Diagonal Range, per stick, on by default. A full diagonal was capped to
the unit circle at ~0.707 per axis, which is what a circular-gated DualShock
really sends -- but games that deadzone each axis separately then ignore
diagonals, and Oblivion's camera crawled diagonally while the cardinals were
fine. Off restores the hardware curve.

Oboe is the default audio backend on Android, with a migration for anyone still
on the old Cubeb default; a deliberate choice of another backend is kept.

Enter Button Assignment (circle/cross) is exposed. The core has always had it
and Android never showed it.

Reset all settings, in General. Per-game overrides and controller binds are
deliberately left alone -- they are invisible from that page.
2026-08-13 22:29:33 -04:00
jpolo1224 b82432c793 VK: allow native fp16 on Adreno drivers that accept it
Oblivion's water did not draw on Vulkan and did draw on OpenGL. The only
Vulkan-only shader workaround in play is the blanket disable of native float16
on every mobile GPU, which emulates it with fp32; its own comment claimed that
"renders correctly", and it does not.

The disable exists for a real failure -- Qualcomm's compiler rejected SPIR-V
containing float16_t and every pipeline came back VK_ERROR_UNKNOWN, which
presents as a black screen with working audio and a working compile overlay,
so it reads as a renderer bug rather than a shader one. That is not worth
reintroducing blind, so this is a version gate rather than a removal:

  Adreno on driver 512.676.53 or newer -> native fp16 (verified)
  older Adreno                         -> unchanged
  Mali, PowerVR, Xclipse, the rest     -> unchanged, untested either way

Found by switching the renderer to OpenGL, which isolated it to the Vulkan path
in one run after the settings-level suspects had all come back empty.
2026-08-13 22:29:17 -04:00
jpolo1224 7f54855b7d lv2/vm: fix a read-only unlink lockup, and three log floods
sys_fs_unlink handled notdir and noent but not readonly, so it fell through to
fmt::throw_exception and killed the PPU main thread inside the syscall. The
emulator then sat with nothing to run: the game froze with the CPU at 1% and
nothing in the log but a stalled RSX. On Android /app_home is the mounted ISO,
which is read-only, so any game deleting a file in its own directory hit it --
Oblivion removes warnings.txt at startup and never got past it. Returns
CELL_EROFS now, which is already what sys_fs_write and friends do. sys_fs_mkdir
and sys_fs_rmdir carried the identical block and are fixed with it.

Three log floods, all of which stall the emulator outright because writing them
is not free on Android:

- sys_fs_utime logged two warning lines per call and rides a polling loop.
  Oblivion's FileCaching thread hit it 7274 times in ten seconds on one .BSA,
  ~22k lines, and the frame loop stopped for over twenty seconds. Now trace.
- vm::lock_sudo reported a failed mlock on every mapping. Android never grants
  RLIMIT_MEMLOCK to apps, so it fails forever while advising the user to raise
  a limit they cannot raise -- 6470 lines in ten seconds here, and ~1200 in
  every other game log looked at. Reported once per session now.
- sys_mmapper's map/unmap pair, 12431 lines over the same window. Now trace.

None of them lose information: raise the channel to Trace to get them back.
2026-08-13 22:29:06 -04:00
jpolo1224 0819f1ef15 RSX: return the renderer to the 0.6 path, keeping the FIFO idle fix and ADPF
Testers consistently report the best performance on the build with the 0.6
renderer, so 0.7's graphics work goes back out. The Arkham City measurement
behind it (62.8 -> 51.2 ms) was one game on one device and did not survive
contact with a wider set of hardware.

Two files are kept from 0.7 because neither is render pass work and both are
measured wins on their own: RSXFIFO's idle spin plus WFE park, which took ~11%
of total CPU off sched_yield, and RSXThread's ADPF feed, without which the
performance-hint setting reports nothing and does nothing.

Everything else under Emu/RSX is byte-identical to 0.6. The removed work is not
lost -- it is in c4b45eee2 and can come back a piece at a time with testing
behind each one, which is how it should have gone in the first place.
2026-08-13 22:28:52 -04:00
jpolo1224 8ee20d91d5 0.7.1: remove vertex cache retention, keep the rest of the renderer
The previous commit reverted all of Emu/RSX to 0.6, which was more than the
bug required. Bisecting had already shown the render pass work was not
responsible -- reverting it alone changed nothing, while removing retention
with the render pass work in place fixed both reported games.

So only retention goes. It reused vertex cache entries across frames, and on
0.6 the attribute ring was too small for it to engage; raising the ring to
192M switched an existing path on in every game at once and handed draws
stale geometry. Back to purging every frame, as 0.6 did.

This restores what the wider revert had taken out for no reason: the render
pass reduction, the RSX FIFO idle fix, ADPF frame timing, the ZCULL and
occlusion query fixes, the swapchain and surface lifetime ports, and VRAM
budgeting.

Sonic Unleashed still does not render FMV cutscenes. That reproduces with
the 0.6 renderer too, so it is unrelated and still open.
2026-08-13 19:51:48 -04:00
jpolo1224 4797ad8a9a 0.7.1: revert the 0.7 renderer to 0.6
0.7 introduced corruption in several games that were fine on 0.6 -- flashing
and flickering in Sonic Unleashed and Dragon Ball among others. Emu/RSX is
returned to its 0.6 state in full; everything outside the renderer is kept.

The main cause was vertex cache retention. On 0.6 the attribute ring was too
small for retention to engage, so raising the ring to 192M did not add a code
path, it switched an existing one on in every game at once, and reusing stale
vertex data is what the flashing was.

Bisecting also showed the render pass work was not responsible: reverting it
alone changed nothing. It can come back, but on its own and with testing
behind it rather than as part of a batch.

Kept from 0.7: the ARM64 PPU float to integer fix, the PPU cache build
identity, the SPU checksum and block state fixes, Oboe, ADPF, and the crash
and stability ports.

Sonic Unleashed does not render FMV cutscenes. That reproduces with the 0.6
renderer as well, so it is not from any of this and is still open.
2026-08-13 19:46:13 -04:00
42 changed files with 409 additions and 2259 deletions
+2 -2
View File
@@ -29,8 +29,8 @@ android {
applicationId = "com.armsx3"
minSdk = 26
targetSdk = 37
versionCode = 11
versionName = "0.7"
versionCode = 13
versionName = "0.7.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.
@@ -63,6 +63,8 @@ object ConfigStore {
private const val KEY_SPU_DECODER_RESTORE = "config.migrated.spuDecoderRestoreLlvm"
private const val KEY_XFLOAT_BACK_TO_APPROX = "config.migrated.xfloatBackToApprox"
private const val KEY_PRECISE_SPU_OFF = "config.migrated.preciseSpuVerifyOff"
// Oboe became the Android default in 0.7.2; move anyone still on the old Cubeb default.
private const val KEY_AUDIO_OBOE = "config.migrated.audioOboeDefault"
private const val KEY_ATOMIC_DMA_OFF = "config.migrated.atomicDmaStoresOff"
// Bumped: the first pass recorded only Vblank Rate, which did not hold on its own.
private const val KEY_VBLANK_60 = "config.migrated.frameCap60"
@@ -316,6 +318,16 @@ object ConfigStore {
}
// Oboe is the Android default now. Only move people sitting on the previous default
// (Cubeb, index 2) -- anyone who deliberately picked Null or another backend keeps it.
if (!MainActivityRuntime.prefs.getBoolean(KEY_AUDIO_OBOE, false)) {
if (raw != null && parsed.ps3.audioRenderer == 2) {
parsed = parsed.copy(ps3 = parsed.ps3.copy(audioRenderer = 4))
dirty = true
}
MainActivityRuntime.prefs.edit { putBoolean(KEY_AUDIO_OBOE, true) }
}
// The ARM64 block checksum is fixed, so the full-compare workaround can go.
if (!MainActivityRuntime.prefs.getBoolean(KEY_PRECISE_SPU_OFF, false)) {
if (raw != null && parsed.ps3.preciseSpuVerification) {
@@ -198,6 +198,12 @@ data class Ps3Settings(
* preciseSpuVerification). Accurate is a rarely-exercised path and costs
* speed, so there is no reason to sit on it.
*/
/**
* Which face button confirms in PS3 system dialogs. 0 = circle, 1 = cross, matching
* enter_button_assign. Japanese-region games and hardware confirm with circle; the rest of
* the world uses cross, which is why RPCS3 exposes it rather than deriving it from region.
*/
val enterButtonAssign: Int = 1,
val spuXFloat: Int = 1,
val accurateSpuRsv: Boolean = true,
/**
@@ -249,7 +255,8 @@ data class Ps3Settings(
val debugConsoleMode: Boolean = false,
val resolution: Int = 2,
val anisoFilter: Int = 0,
val audioRenderer: Int = 2,
/** Index into Rpcs3Settings.AUDIO_RENDERERS. 4 = Oboe, the Android default (see node_audio). */
val audioRenderer: Int = 4,
/**
* Output aspect override in permille (1778 = 16:9, 1333 = 4:3), 0 = follow the game.
*
@@ -1064,6 +1071,7 @@ data class Settings(
put("PS3/Net", "Internet enabled", "enum", ps3.netEnabled.toString())
put("PS3/Net", "PSN status", "enum", ps3.psnStatus.toString())
put("PS3/Net", "UPNP Enabled", "bool", ps3.upnpEnabled.toString())
put("PS3/System", "Enter button assignment", "enum", ps3.enterButtonAssign.toString())
put("PS3/Core", "SPU XFloat Accuracy", "enum", ps3.spuXFloat.toString())
put("PS3/Core", "Accurate SPU Reservations", "bool", ps3.accurateSpuRsv.toString())
put("PS3/Core", "Accurate Cache Line Stores", "bool", ps3.accurateCacheLine.toString())
@@ -2032,6 +2040,7 @@ data class Settings(
put("ps3NetEnabled", ps3.netEnabled)
put("ps3PsnStatus", ps3.psnStatus)
put("ps3UpnpEnabled", ps3.upnpEnabled)
put("ps3EnterButtonAssign", ps3.enterButtonAssign)
put("ps3SpuXFloat", ps3.spuXFloat)
put("ps3AccurateSpuRsv", ps3.accurateSpuRsv)
put("ps3AccurateCacheLine", ps3.accurateCacheLine)
@@ -2370,6 +2379,7 @@ data class Settings(
netEnabled = json.optBoolean("ps3NetEnabled", def.ps3.netEnabled),
psnStatus = json.optBoolean("ps3PsnStatus", def.ps3.psnStatus),
upnpEnabled = json.optBoolean("ps3UpnpEnabled", def.ps3.upnpEnabled),
enterButtonAssign = json.optInt("ps3EnterButtonAssign", def.ps3.enterButtonAssign),
spuXFloat = json.optInt("ps3SpuXFloat", def.ps3.spuXFloat),
accurateSpuRsv = json.optBoolean("ps3AccurateSpuRsv", def.ps3.accurateSpuRsv),
accurateCacheLine = json.optBoolean("ps3AccurateCacheLine", def.ps3.accurateCacheLine),
@@ -2688,6 +2698,7 @@ data class Settings(
if (current.ps3.netEnabled != base.ps3.netEnabled) j.put("ps3NetEnabled", current.ps3.netEnabled)
if (current.ps3.psnStatus != base.ps3.psnStatus) j.put("ps3PsnStatus", current.ps3.psnStatus)
if (current.ps3.upnpEnabled != base.ps3.upnpEnabled) j.put("ps3UpnpEnabled", current.ps3.upnpEnabled)
if (current.ps3.enterButtonAssign != base.ps3.enterButtonAssign) j.put("ps3EnterButtonAssign", current.ps3.enterButtonAssign)
if (current.ps3.spuXFloat != base.ps3.spuXFloat) j.put("ps3SpuXFloat", current.ps3.spuXFloat)
if (current.ps3.accurateSpuRsv != base.ps3.accurateSpuRsv) j.put("ps3AccurateSpuRsv", current.ps3.accurateSpuRsv)
if (current.ps3.accurateCacheLine != base.ps3.accurateCacheLine) j.put("ps3AccurateCacheLine", current.ps3.accurateCacheLine)
@@ -2987,6 +2998,7 @@ data class Settings(
netEnabled = if (overrides.has("ps3NetEnabled")) overrides.getBoolean("ps3NetEnabled") else base.ps3.netEnabled,
psnStatus = if (overrides.has("ps3PsnStatus")) overrides.getBoolean("ps3PsnStatus") else base.ps3.psnStatus,
upnpEnabled = if (overrides.has("ps3UpnpEnabled")) overrides.getBoolean("ps3UpnpEnabled") else base.ps3.upnpEnabled,
enterButtonAssign = if (overrides.has("ps3EnterButtonAssign")) overrides.getInt("ps3EnterButtonAssign") else base.ps3.enterButtonAssign,
spuXFloat = if (overrides.has("ps3SpuXFloat")) overrides.getInt("ps3SpuXFloat") else base.ps3.spuXFloat,
accurateSpuRsv = if (overrides.has("ps3AccurateSpuRsv")) overrides.getBoolean("ps3AccurateSpuRsv") else base.ps3.accurateSpuRsv,
accurateCacheLine = if (overrides.has("ps3AccurateCacheLine")) overrides.getBoolean("ps3AccurateCacheLine") else base.ps3.accurateCacheLine,
@@ -525,6 +525,14 @@ val EN: Map<String, String> = mapOf(
"adv.accurateRsxRsv.description" to "Synchronises GPU access to reserved memory strictly. Fixes rare graphical corruption at a performance cost.",
"adv.ppuRsvPriority.label" to "PPU Reservation Priority",
"adv.ppuRsvPriority.description" to "Gives the main CPU priority over the SPUs when competing for the same memory. Can help games that stall waiting on the PPU.",
"pad.section.enterButton" to "Enter Button Assignment",
"pad.enterButton.label" to "Confirm button",
"pad.enterButton.circle" to "Enter with circle",
"pad.enterButton.cross" to "Enter with cross",
"pad.enterButton.description" to "Which button confirms in PS3 system dialogs. Japanese games usually expect circle; most others use cross.",
"app.resetAll" to "Reset all settings",
"app.resetAll.desc" to "Put every setting back to its default. Per-game settings and controller binds are kept.",
"app.resetAll.confirm" to "Every global setting goes back to its default. Per-game overrides and controller binds are not touched.",
"adv.spuVerification.label" to "SPU Verification",
"adv.spuVerification.description" to "Verifies compiled SPU code against the original. Catches miscompiles; turning it off is faster but makes bad codegen silent.",
"adv.preciseSpuVerification.label" to "Precise SPU Verification",
@@ -821,6 +829,8 @@ val EN: Map<String, String> = mapOf(
"pad.stickFeel.acceleration.description" to "Non-linear response curve: small tilts stay precise for aiming, full tilt ramps up to full speed. 0 = linear (off); higher = more curve.",
"pad.stickFeel.acceleration.label" to "Acceleration",
"pad.stickFeel.antiDeadzone.description" to "Smallest output sent to the game, to cancel a game's OWN built-in stick deadzone (e.g. Cold Fear / Area 51 ignore the stick until ~45%, then aim jumps). Set near the game's deadzone so any stick movement responds immediately and the full travel maps smoothly above it. 0 = off.",
"pad.stickFeel.squareGate.label" to "Full Diagonal Range",
"pad.stickFeel.squareGate.description" to "Sends the full range on diagonals instead of the reduced value a real DualShock gives (~70%), so diagonal movement is as fast as straight up/down/left/right. On by default. Turn off to match original hardware exactly.",
"pad.stickFeel.antiDeadzone.label" to "Anti-Deadzone",
"pad.stickFeel.deadzone.description" to "Fraction of physical analog travel ignored near center (applied to the stick's radial distance, so diagonals behave like cardinals). Output re-normalizes past it, so movement still ramps smoothly from 0 — which also means the on-screen effect can be masked by a game's OWN built-in deadzone (Area 51 ignores input below ~45% no matter what you set here; use Anti-Deadzone for that). 0 = off — raw hardware values pass through, including any stick drift.",
"pad.stickFeel.deadzone.label" to "Deadzone",
@@ -364,6 +364,22 @@ object ControllerMappings {
private const val KEY_STICK_ANTIDZ = "pad.stick.antiDeadzone"
const val STICK_ANTIDZ_MAX = 0.60f
private val prefStickAntiDz = PerStickPref(KEY_STICK_ANTIDZ, 0.0f, 0f, STICK_ANTIDZ_MAX)
// Square gate: send the full per-axis range on diagonals instead of capping them to the
// unit circle. A DualShock 3 is circular-gated, so a full diagonal is ~0.707 per axis, and
// emitting that is technically faithful -- but it lands inside the internal deadzone of games
// that test each axis separately, and their camera then crawls diagonally while the cardinals
// are fine. Oblivion is the case that found this.
//
// DEFAULT ON. Faithfulness to a circular gate is not worth a control scheme that feels broken,
// and a modern pad's own gate is closer to square anyway. Off restores the hardware curve for
// anyone who wants it. Per stick.
private const val KEY_STICK_SQUARE = "pad.stick.square"
fun stickSquareGate(left: Boolean): Boolean =
MainActivityRuntime.prefs.getBoolean(KEY_STICK_SQUARE + if (left) ".l" else ".r", true)
fun setStickSquareGate(left: Boolean, v: Boolean) =
MainActivityRuntime.prefs.edit { putBoolean(KEY_STICK_SQUARE + if (left) ".l" else ".r", v) }
fun stickAntiDeadzone(left: Boolean): Float = prefStickAntiDz.get(left)
fun setStickAntiDeadzone(left: Boolean, v: Float) = prefStickAntiDz.set(left, v)
@@ -4361,7 +4361,12 @@ open class MainActivityRuntime : ComponentActivity() {
val mag = kotlin.math.hypot(gx, gy)
if (mag <= 0f) return
val shaped = shapeStickMag(mag.coerceAtMost(1f), left)
val scale = shaped / mag // preserves direction; caps square-gate diagonals at unit circle
// Dividing by the magnitude caps a full diagonal at the unit circle: 0.707 per axis, which
// is what a circular-gated DualShock 3 really sends. Games that deadzone each axis on its
// own then ignore diagonals almost entirely. Dividing by the LARGER axis instead expands
// to the square, so a full diagonal reaches 1.0 on both. Same result on the cardinals.
val denom = if (ControllerMappings.stickSquareGate(left)) kotlin.math.max(abs(gx), abs(gy)) else mag
val scale = if (denom > 0f) shaped / denom else 0f
val ox = gx * scale
val oy = gy * scale
if (ox > 0f) accumAnalog(aXPos, ox) else if (ox < 0f) accumAnalog(aXNeg, -ox)
@@ -792,6 +792,7 @@ fun AppTab() {
)
ClearCacheRow()
ResetAllSettingsRow()
}
}
@@ -836,6 +837,77 @@ private fun ClearCacheRow() {
}
}
/** Put every global setting back to its default in one go.
*
* The per-tab Reset in the top bar only covers the page you are looking at, which is right for
* undoing one experiment but tedious when a config has drifted across half a dozen tabs. This is
* the "start clean" button. Per-game overrides are deliberately left alone: they belong to
* individual games, are invisible from here, and wiping them from a global page would be a
* surprise. Controller binds live in ControllerMappings and keep their own reset. */
@Composable
private fun ResetAllSettingsRow() {
var confirming by remember { mutableStateOf(false) }
Surface(
onClick = { confirming = true },
modifier = Modifier.fillMaxWidth()
.controllerFocusable("app.resetAll", RoundedCornerShape(20.dp), onConfirm = { confirming = true }),
shape = RoundedCornerShape(20.dp),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.72f),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.error.copy(alpha = 0.46f)),
) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Surface(
modifier = Modifier.size(46.dp),
shape = RoundedCornerShape(14.dp),
color = MaterialTheme.colorScheme.errorContainer,
) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
Text("↺", fontSize = 21.sp)
}
}
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f)) {
Text(str("app.resetAll"), style = MaterialTheme.typography.titleMedium)
Text(
str("app.resetAll.desc"),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
if (confirming) {
com.armsx2.ui.common.ConfirmOverlay(
title = str("app.resetAll"),
message = str("app.resetAll.confirm"),
confirmLabel = str("action.reset"),
destructive = true,
idPrefix = "settings-reset-all",
onConfirm = {
val defaults = com.armsx2.config.Settings()
com.armsx2.ui.InGameOverlay.settingsState.value = defaults
com.armsx2.config.ConfigStore.saveGlobal(defaults)
// Push straight to the core when a game is live, the same way the per-tab reset
// does. Without this the UI shows defaults while the running VM keeps the old
// values until the next boot.
if (MainActivityRuntime.nativeReady.value &&
MainActivityRuntime.eState.value != com.armsx2.EmuState.STOPPED) {
runCatching { defaults.applyTo() }
}
confirming = false
},
onDismiss = { confirming = false },
)
}
}
/** Export / import everything a reinstall would destroy: save states, memory cards, artwork,
* per-game settings, controller profiles, patches and every preference. ROMs and BIOS are left
* out — those live outside the app and survive on their own. See [com.armsx2.BackupManager]. */
@@ -56,7 +56,7 @@ import kotlinx.coroutines.withContext
import com.armsx3.NativeApp
@Composable
fun PadTab(@Suppress("UNUSED_PARAMETER") state: MutableState<Settings>) {
fun PadTab(state: MutableState<Settings>) {
val scroll = settingsScrollState()
ControllerAutoScroll(scroll)
val capture = remember { mutableStateOf<ControllerMappings.Action?>(null) }
@@ -448,6 +448,24 @@ fun PadTab(@Suppress("UNUSED_PARAMETER") state: MutableState<Settings>) {
// (com.armsx2.ui.settings.GyroSection). Here it follows the Pad tab's Global/Game
// scope (editSerial) and shares the tab's refreshToken so it re-reads live.
GyroSection(editSerial = editSerial, externalRefresh = refreshToken)
// Which face button the PS3 itself treats as "confirm" in system dialogs. This is a
// console setting (cellSysutil ID_ENTER_BUTTON_ASSIGN), not a pad remap: it changes what
// the GAME asks for, so it has to live in the config rather than in the bind table.
// Japanese titles generally expect circle and can read as inverted without it.
CollapsibleSection(str("pad.section.enterButton"), initiallyExpanded = false) {
SegmentedRow(
label = str("pad.enterButton.label"),
options = listOf(str("pad.enterButton.circle"), str("pad.enterButton.cross")),
selectedIndex = state.value.ps3.enterButtonAssign.coerceIn(0, 1),
description = str("pad.enterButton.description"),
onChange = { idx ->
com.armsx2.ui.InGameOverlay.saveSettings(
state.value.copy(ps3 = state.value.ps3.copy(enterButtonAssign = idx)),
)
},
)
}
CollapsibleSection(str("pad.section.buttonMapping"), initiallyExpanded = false) {
ControllerMappings.actions.forEach { action ->
val physical = ControllerMappings.physicalForScope(action, editPlayer.intValue, editSerial)
@@ -899,6 +917,12 @@ private fun StickFeelSliders(left: Boolean, title: String, refreshToken: Mutable
valueFormatter = { "${it * 5}%" },
onChange = { ControllerMappings.setStickSensitivity(left, it / 20f); refreshToken.value++ },
)
ToggleRow(
str("pad.stickFeel.squareGate.label"),
ControllerMappings.stickSquareGate(left),
description = str("pad.stickFeel.squareGate.description"),
) { ControllerMappings.setStickSquareGate(left, it); refreshToken.value++ }
SettingsDivider()
IntSliderRow(
label = str("pad.stickFeel.acceleration.label"),
@@ -25,73 +25,87 @@ import org.json.JSONObject
internal val SETTINGS_CATEGORY_FIELDS: Map<SettingsCategory, List<String>> = mapOf(
// PerformanceTab.kt
SettingsCategory.Performance to listOf(
"eeClampMode", "eeCycleRate", "eeCycleSkip", "eeFpuRoundMode",
"fastCDVD", "fpsLimit", "frameSkip", "framerateNtsc", "frameratePal",
"intcStat", "mtvu", "nominalSpeedPercent", "skipDuplicateFrames", "vu0RoundMode",
"vu1Instant", "vu1RoundMode", "vuClampMode", "vuDeferredWrites", "vuFlagHack",
"vuNeonFusions", "vuSkipStallSim", "waitLoop",
"accurateBlendingUnit", "affinityMode", "eeClampMode", "eeCycleRate", "eeCycleSkip",
"eeFpuRoundMode", "fastCDVD", "fpsLimit", "frameSkip", "framerateNtsc", "frameratePal",
"hwMipmap", "hwRov", "hwScaler", "intcStat", "mtvu", "nominalSpeedPercent",
"ps3AccurateCacheLine", "ps3AccurateSpuDma", "ps3AccurateSpuRsv", "ps3ClocksScale",
"ps3GpuTurbo", "ps3LlvmPrecompile", "ps3LlvmThreads", "ps3MaxSpursThreads",
"ps3PpuDecoder", "ps3PreferredSpuThreads", "ps3SavestateCompatibleMode",
"ps3SilenceAllLogs", "ps3SpuBlockSize", "ps3SpuCache", "ps3SpuDecoder",
"ps3SpuLoopDetection", "ps3SpuXFloat", "screenResOverride", "skipDuplicateFrames",
"texturePreloading", "upscaleFloat", "vu0RoundMode", "vu1Instant", "vu1RoundMode",
"vuClampMode", "vuDeferredWrites", "vuFlagHack", "vuNeonFusions", "vuSkipStallSim",
"waitLoop",
),
// RendererTab.kt
SettingsCategory.Graphics to listOf(
"accurateBlendingUnit", "adrenoFbFetch", "aspectRatio", "casMode", "casSharpness",
"customAspectRatio", "deinterlaceMode", "displayBilinear", "dumpReplaceableTextures", "fmvAspectRatio",
"forceMaliFbFetch", "fxaa", "gpuProfile", "gsBackThreadMode", "hardwareDownloadMode",
"hwAa1", "hwAccurateAlphaTest", "hwMipmap", "hwRov", "loadTextureReplacements",
"adrenoFbFetch", "aspectRatio", "autoProgressiveScan", "casMode", "casSharpness",
"customAspectRatio", "customDriverId", "deinterlaceMode", "displayBilinear",
"displayFitMode", "dumpReplaceableTextures", "fmvAspectRatio", "forceMaliFbFetch",
"fxaa", "gpuProfile", "gsBackThreadMode", "hardwareDownloadMode", "hwAa1",
"hwAccurateAlphaTest", "landscapeRenderTop", "loadTextureReplacements",
"loadTextureReplacementsAsync", "maxAnisotropy", "orientation",
"osdShowTextureReplacements", "portraitRenderTop", "landscapeRenderTop", "autoProgressiveScan",
"affinityMode", "precacheTextureReplacements",
"osdShowTextureReplacements", "portraitRenderTop", "precacheTextureReplacements",
"ps3AnisoFilter", "ps3AsyncTexStream", "ps3DisableZcull", "ps3DisplayAspect",
"ps3MsaaMode", "ps3MultithreadedRsx", "ps3ReadColorBuffers", "ps3ReadDepthBuffer",
"ps3RelaxedZcull", "ps3Resolution", "ps3ShaderMode", "ps3StrictRendering",
"ps3VramLimitMb", "ps3WriteColorBuffers", "ps3WriteDepthBuffer", "renderer",
"shadeBoost", "shadeBoostBrightness", "shadeBoostContrast", "shadeBoostGamma",
"shadeBoostSaturation", "shaderChainEnabled", "shaderChainParams", "shaderChainPreset",
"textureFiltering", "texturePreloading", "triFilter", "tvShader", "upscaleFloat",
"vsyncEnable", "displayFitMode", "ps3DisplayAspect",
"textureFiltering", "triFilter", "tvShader", "upscaleFloat", "useAngleOpenGL",
"vsyncEnable",
),
// AudioTab.kt
SettingsCategory.Audio to listOf(
"audioBufferMs", "audioFastForwardVolume", "audioMuted", "audioOpenSLES",
"audioOutputLatencyMs", "audioSwapChannels", "audioTimeStretch", "audioVolume",
"spu2LightweightMix", "spu2NeonReverb",
"ps3AudioBufferMs", "ps3AudioChannels", "ps3AudioCubebBackend", "ps3AudioFormat",
"ps3AudioRenderer", "ps3AudioTimeStretch", "spu2LightweightMix", "spu2NeonReverb",
),
// NetworkTab.kt
SettingsCategory.Network to listOf(
"dev9AutoGateway", "dev9AutoMask", "dev9Dns1", "dev9Dns2", "dev9EthApi", "dev9EthDevice",
"dev9EthEnable", "dev9EthHosts", "dev9EthLogDhcp", "dev9EthLogDns", "dev9Gateway",
"dev9HddEnable", "dev9HddFile", "dev9InterceptDhcp", "dev9Mask", "dev9ModeDns1",
"dev9ModeDns2", "dev9Ps2Ip", "ip", "url", "usbKeyboard",
"dev9AutoGateway", "dev9AutoMask", "dev9Dns1", "dev9Dns2", "dev9EthApi",
"dev9EthDevice", "dev9EthEnable", "dev9EthHosts", "dev9EthLogDhcp", "dev9EthLogDns",
"dev9Gateway", "dev9HddEnable", "dev9HddFile", "dev9InterceptDhcp", "dev9Mask",
"dev9ModeDns1", "dev9ModeDns2", "dev9Ps2Ip", "ip", "ps3NetEnabled", "ps3PsnStatus",
"ps3UpnpEnabled", "url", "usbKeyboard",
),
// OverlayTab.kt
SettingsCategory.OnScreen to listOf(
"osdColor", "osdScale", "osdShowCpu", "osdShowFps", "osdShowFrameTimes", "osdShowGpu",
"osdShowGpuStats", "osdShowGsStats", "osdShowHardwareInfo", "osdShowInputs",
"osdShowMessages", "osdShowResolution", "osdShowSettings", "osdShowSpeed",
"osdShowVersion", "osdShowVps",
// RPCS3's own performance overlay (the lower half of the tab).
"ps3OverlayEnabled", "ps3OverlayDetail", "ps3OverlayPosition", "ps3OverlayFontSize",
"ps3OverlayOpacity", "ps3OverlayFramerateGraph", "ps3OverlayFrametimeGraph",
"ps3OverlayBodyColor", "ps3OverlayBodyBg", "ps3OverlayTitleColor", "ps3OverlayTitleBg",
"osdShowVersion", "osdShowVps", "ps3OverlayBodyBg", "ps3OverlayBodyColor",
"ps3OverlayDetail", "ps3OverlayEnabled", "ps3OverlayFontSize",
"ps3OverlayFramerateGraph", "ps3OverlayFrametimeGraph", "ps3OverlayOpacity",
"ps3OverlayPosition", "ps3OverlayTitleBg", "ps3OverlayTitleColor",
),
// FixesTab.kt — also owns the GameDB fixes and the recompiler toggles, which moved here
// from Performance and from the retired Recompiler tab.
SettingsCategory.Advanced to listOf(
"enableFastBoot", "enableGameFixes",
"gamefixBlitInternalFps", "gamefixDmaBusy", "gamefixEETiming", "gamefixFpuMul",
"gamefixFullVu0Sync", "gamefixGifFifo", "gamefixGoemonTlb", "gamefixIbit",
"gamefixInstantDma", "gamefixOphFlag", "gamefixSkipMpeg",
"gamefixSoftwareRendererFmv", "gamefixVif1Stall", "gamefixVuAddSub",
"gamefixVuOverflow", "gamefixVuSync", "gamefixXgkick",
"enableFastmem", "recEE", "recIOP", "recVU0", "recVU1",
"alignSprite", "antiBlur", "autoFlush", "autoFlushSw", "bilinearUpscale", "cpuClutRender",
"cpuFramebufferConversion", "cpuSpriteRenderBw", "cpuSpriteRenderLevel", "cropBottom",
"cropLeft", "cropRight", "cropTop", "displayZoom", "disableDepthEmulation", "disableFramebufferFetch",
"disableInterlaceOffset", "disablePartialInvalidation", "disableRenderFixes",
"disableSafeFeatures", "disableShaderCache", "disableVertexShaderExpand", "dithering",
"drawBuffering", "estimateTextureRegion", "forceEvenSpritePosition",
"alignSprite", "antiBlur", "autoFlush", "autoFlushSw", "bilinearUpscale",
"cpuClutRender", "cpuFramebufferConversion", "cpuSpriteRenderBw",
"cpuSpriteRenderLevel", "cropBottom", "cropLeft", "cropRight", "cropTop",
"disableDepthEmulation", "disableFramebufferFetch", "disableInterlaceOffset",
"disablePartialInvalidation", "disableRenderFixes", "disableSafeFeatures",
"disableShaderCache", "disableVertexShaderExpand", "displayZoom", "dithering",
"drawBuffering", "enableFastBoot", "enableFastmem", "enableGameFixes",
"estimateTextureRegion", "forceEvenSpritePosition", "gamefixBlitInternalFps",
"gamefixDmaBusy", "gamefixEETiming", "gamefixFpuMul", "gamefixFullVu0Sync",
"gamefixGifFifo", "gamefixGoemonTlb", "gamefixIbit", "gamefixInstantDma",
"gamefixOphFlag", "gamefixSkipMpeg", "gamefixSoftwareRendererFmv", "gamefixVif1Stall",
"gamefixVuAddSub", "gamefixVuOverflow", "gamefixVuSync", "gamefixXgkick",
"gpuPaletteConversion", "gpuTargetClut", "halfPixelOffset", "hwAccurateAlphaTest",
"integerScaling", "limit24BitDepth", "manualUserHacks", "mergeSprite", "mipmapSw",
"nativeScaling", "overrideTextureBarriers", "preloadFrameData", "readTargetsWhenClosing",
"roundSprite", "screenOffsets", "showOverscan", "skipDrawEnd", "skipDrawStart",
"spinCpuReadbacks", "spinGpuReadbacks", "swThreads", "swThreadsHeight",
"syncToHostRefresh", "textureInsideRt", "textureOffsetX", "textureOffsetY",
"unscaledPaletteDraw", "useBlitSwapChain", "vsyncQueueSize",
"nativeScaling", "overrideTextureBarriers", "preloadFrameData", "ps3AccurateCacheLine",
"ps3AccurateDfma", "ps3AccurateRsxRsv", "ps3AccurateSpuRsv", "ps3DebugConsoleMode",
"ps3HleLwmutex", "ps3PpuNanHandling", "ps3PpuRsvPriority", "ps3PreciseSpuVerification",
"ps3SetDazFtz", "ps3SleepTimers", "ps3SpuVerification", "ps3SpuXFloat",
"readTargetsWhenClosing", "recEE", "recIOP", "recVU0", "recVU1", "roundSprite",
"screenOffsets", "showOverscan", "skipDrawEnd", "skipDrawStart", "spinCpuReadbacks",
"spinGpuReadbacks", "swThreads", "swThreadsHeight", "syncToHostRefresh",
"textureInsideRt", "textureOffsetX", "textureOffsetY", "unscaledPaletteDraw",
"useBlitSwapChain", "vsyncQueueSize",
),
// Controls / Hotkeys / Skins / General / Info / Patches / About own no Settings fields —
// Controls keeps its binds and tunables in ControllerMappings and has its own reset row.
+49 -3
View File
@@ -2174,6 +2174,19 @@ error_code sys_fs_unlink(ppu_thread& ppu, vm::cptr<char> path)
{
return {mp == &g_mp_sys_dev_hdd1 ? sys_fs.warning : sys_fs.error, CELL_ENOENT, path};
}
case fs::error::readonly:
{
// A virtual device that cannot be modified. On Android that is the mounted ISO behind
// /app_home, whose device_base mutators report readonly by design -- so any game that
// writes, renames or deletes inside its own USRDIR lands here. Oblivion deletes
// /app_home/warnings.txt at startup, which a real console would simply allow.
//
// This used to fall through to the throw below, which killed the PPU thread inside the
// syscall: the game froze with the CPU idle and nothing in the log but a stalled RSX.
// Report it and let the guest decide what to do, which is what hardware would do for a
// write to read-only media.
return { CELL_EROFS, path };
}
default:
{
if (has_non_directory_components(local_path, ends_with_delim_dot_or_dotdot(vpath)))
@@ -2425,7 +2438,9 @@ error_code sys_fs_fcntl(ppu_thread& ppu, u32 fd, u32 op, vm::ptr<void> _arg, u32
// Load mountpoint (doesn't support multiple // at the start)
std::string_view vpath{arg->name.get_ptr(), arg->name_size};
sys_fs.notice("sys_fs_fcntl(0xc0000006): %s", vpath);
// Trace for the same reason as sys_fs_utime: this rides along with the utime polling
// loop and made up the last third of that flood.
sys_fs.trace("sys_fs_fcntl(0xc0000006): %s", vpath);
// Check only mountpoint
vpath = vpath.substr(0, vpath.find_first_of('\0'));
@@ -3087,6 +3102,19 @@ error_code sys_fs_truncate(ppu_thread& ppu, vm::cptr<char> path, u64 size)
{
return {mp == &g_mp_sys_dev_hdd1 ? sys_fs.warning : sys_fs.error, CELL_ENOENT, path};
}
case fs::error::readonly:
{
// A virtual device that cannot be modified. On Android that is the mounted ISO behind
// /app_home, whose device_base mutators report readonly by design -- so any game that
// writes, renames or deletes inside its own USRDIR lands here. Oblivion deletes
// /app_home/warnings.txt at startup, which a real console would simply allow.
//
// This used to fall through to the throw below, which killed the PPU thread inside the
// syscall: the game froze with the CPU idle and nothing in the log but a stalled RSX.
// Report it and let the guest decide what to do, which is what hardware would do for a
// write to read-only media.
return { CELL_EROFS, path };
}
default:
{
if (has_non_directory_components(local_path, ends_with_delim_dot_or_dotdot(vpath)))
@@ -3311,8 +3339,13 @@ error_code sys_fs_utime(ppu_thread& ppu, vm::cptr<char> path, vm::cptr<CellFsUti
{
lv2_obj::sleep(ppu);
sys_fs.warning("sys_fs_utime(path=%s, timep=*0x%x)", path, timep);
sys_fs.warning("** actime=%u, modtime=%u", timep->actime, timep->modtime);
// Trace, not warning. Setting a file's timestamps is routine and uninteresting, but games
// that poll it do so in tight loops: Oblivion's FileCaching thread hit this 7274 times in ten
// seconds on one .BSA, and at two warning lines a call that alone stalled the emulator for
// over twenty seconds. Logging is not free on Android. Raise the sys_fs channel to Trace to
// get these back.
sys_fs.trace("sys_fs_utime(path=%s, timep=*0x%x)", path, timep);
sys_fs.trace("** actime=%u, modtime=%u", timep->actime, timep->modtime);
const auto [path_error, vpath] = translate_to_str(path);
@@ -3354,6 +3387,19 @@ error_code sys_fs_utime(ppu_thread& ppu, vm::cptr<char> path, vm::cptr<CellFsUti
{
return {mp == &g_mp_sys_dev_hdd1 ? sys_fs.warning : sys_fs.error, CELL_ENOENT, path};
}
case fs::error::readonly:
{
// A virtual device that cannot be modified. On Android that is the mounted ISO behind
// /app_home, whose device_base mutators report readonly by design -- so any game that
// writes, renames or deletes inside its own USRDIR lands here. Oblivion deletes
// /app_home/warnings.txt at startup, which a real console would simply allow.
//
// This used to fall through to the throw below, which killed the PPU thread inside the
// syscall: the game froze with the CPU idle and nothing in the log but a stalled RSX.
// Report it and let the guest decide what to do, which is what hardware would do for a
// write to read-only media.
return { CELL_EROFS, path };
}
default:
{
if (has_non_directory_components(local_path, ends_with_delim_dot_or_dotdot(vpath)))
+6 -2
View File
@@ -614,7 +614,10 @@ error_code sys_mmapper_map_shared_memory(ppu_thread& ppu, u32 addr, u32 mem_id,
{
ppu.state += cpu_flag::wait;
sys_mmapper.warning("sys_mmapper_map_shared_memory(addr=0x%x, mem_id=0x%x, flags=0x%x)", addr, mem_id, flags);
// Trace, not warning: a successful map is routine, and games that cycle shared memory do it
// thousands of times a second. Oblivion logged 6476 of these in ten seconds, which on Android
// costs more than the mapping itself. Raise the sys_mmapper channel to Trace to get them back.
sys_mmapper.trace("sys_mmapper_map_shared_memory(addr=0x%x, mem_id=0x%x, flags=0x%x)", addr, mem_id, flags);
const auto area = vm::get(vm::any, addr);
@@ -765,7 +768,8 @@ error_code sys_mmapper_unmap_shared_memory(ppu_thread& ppu, u32 addr, vm::ptr<u3
{
ppu.state += cpu_flag::wait;
sys_mmapper.warning("sys_mmapper_unmap_shared_memory(addr=0x%x, mem_id=*0x%x)", addr, mem_id);
// Pairs with the map above; same reasoning.
sys_mmapper.trace("sys_mmapper_unmap_shared_memory(addr=0x%x, mem_id=*0x%x)", addr, mem_id);
const auto area = vm::get(vm::any, addr);
+16 -1
View File
@@ -1194,7 +1194,22 @@ namespace vm
if (!utils::memory_lock(g_sudo_addr + addr, size))
{
vm_log.error("Failed to lock sudo memory (addr=0x%x, size=0x%x). Consider increasing your system limits.", addr, size);
// Report this once per session, not once per call.
//
// Android does not grant RLIMIT_MEMLOCK to ordinary apps, so this fails for every
// mapping and cannot be "fixed" by the user the message tells to raise their limits.
// Games that map and unmap shared memory in a loop then drown the log in it --
// Oblivion's BSTaskManagerThread produced 6470 of these in ten seconds, and writing
// them is expensive enough on Android to stall the emulator outright.
//
// The first one still says what happened; the rest are the same fact repeated.
static atomic_t<bool> s_reported{false};
if (!s_reported.exchange(true))
{
vm_log.error("Failed to lock sudo memory (addr=0x%x, size=0x%x). Consider increasing your system limits."
" Further failures will not be reported.", addr, size);
}
}
}
@@ -122,30 +122,13 @@ namespace rsx
texture_cache_predictor_entry_history_queue<max_write_history_size> write_history;
static const u32 max_confidence = 8; // Cannot be more "confident" than this value
#ifdef __ANDROID__
// Mobile-tiler tuning. Here the GPU is usually idle while the CPU stalls on synchronous
// colour-buffer readbacks (Write Color Buffers titles such as Demon's Souls). With no GPU
// contention a speculative pre-flush is close to free, so engage the predictor sooner:
// cross the threshold on the first repeat of a stable readback region, so the pre-flush
// fires at framebuffer setup and the later CPU read finds an already-signalled fence
// instead of spinning on a fresh GPU copy.
static const u32 confident_threshold = 4;
static const u32 starting_confidence = 4;
#else
static const u32 confident_threshold = 6; // We are confident if confidence >= confidence_threshold
static const u32 starting_confidence = 3;
#endif
static const u32 confidence_guessed_flush = 2; // Confidence granted when we correctly guess there will be a flush
static const u32 confidence_guessed_no_flush = 1; // Confidence granted when we correctly guess there won't be a flush
static const u32 confidence_incorrect_guess = -2; // Confidence granted when our guess is incorrect
#ifdef __ANDROID__
// A wrong speculative flush costs little on an idle GPU, so do not punish it into a
// multi-frame confidence rebuild.
static const u32 confidence_mispredict = -2;
#else
static const u32 confidence_mispredict = -4; // Confidence granted when a speculative flush is incorrect
#endif
u32 confidence;
-54
View File
@@ -445,33 +445,6 @@ namespace rsx
u32 processed = 0;
const bool has_unclaimed = (m_pending_writes.back().sink == 0);
// Batch-prefetch every GPU occlusion result this drain is about to read, in one round
// trip. The VK backend collapses N blocking reads into a single copy + fence and primes
// its per-query cache; other backends no-op and the loop below reads per-query as before.
// The filter is implemented && num_draws, a superset of what the loop actually reads --
// the dynamic have_result early-out only skips some, costing at most a wasted copy.
{
std::vector<occlusion_query_info*> prefetch_set;
prefetch_set.reserve(m_pending_writes.size());
for (auto& writer : m_pending_writes)
{
if (!writer.sink)
break;
auto query = writer.query;
if (!query || !query->num_draws)
continue;
if (writer.type == CELL_GCM_ZPASS_PIXEL_CNT || writer.type == CELL_GCM_ZCULL_STATS3)
prefetch_set.push_back(query);
}
if (prefetch_set.size() > 1)
prefetch_occlusion_query_results(prefetch_set);
}
// Write all claimed reports unconditionally
for (auto& writer : m_pending_writes)
{
@@ -606,33 +579,6 @@ namespace rsx
}
u32 processed = 0;
// Batch-prefetch every GPU occlusion result this drain is about to read, in one round
// trip. The VK backend collapses N blocking reads into a single copy + fence and primes
// its per-query cache; other backends no-op and the loop below reads per-query as before.
// The filter is implemented && num_draws, a superset of what the loop actually reads --
// the dynamic have_result early-out only skips some, costing at most a wasted copy.
{
std::vector<occlusion_query_info*> prefetch_set;
prefetch_set.reserve(m_pending_writes.size());
for (auto& writer : m_pending_writes)
{
if (!writer.sink)
break;
auto query = writer.query;
if (!query || !query->num_draws)
continue;
if (writer.type == CELL_GCM_ZPASS_PIXEL_CNT || writer.type == CELL_GCM_ZCULL_STATS3)
prefetch_set.push_back(query);
}
if (prefetch_set.size() > 1)
prefetch_occlusion_query_results(prefetch_set);
}
for (auto& writer : m_pending_writes)
{
if (!writer.sink)
-16
View File
@@ -87,19 +87,8 @@ namespace rsx
enum constants
{
#ifdef __ANDROID__
// Mobile-tiler tuning. The GPU finishes occlusion queries in microseconds but is
// otherwise idle (we are sync-bound, not GPU-bound), so the desktop cadence leaves
// completed results unharvested for up to 300us and the guest's Reports-area read
// then force-drains them one at a time -- the per-frame ZCULL hitch under Accurate
// ZCULL stats. Harvest 4x more often so ready results are picked up without blocking
// before the guest asks for them.
max_zcull_delay_us = 100, // Delay before a report update operation is forced to retire
min_zcull_tick_us = 25, // Default tick duration. To avoid hardware spam, we schedule peeks in multiples of this.
#else
max_zcull_delay_us = 300, // Delay before a report update operation is forced to retire
min_zcull_tick_us = 100, // Default tick duration. To avoid hardware spam, we schedule peeks in multiples of this.
#endif
occlusion_query_count = 2048, // Number of occlusion query slots available. Real hardware actually has far fewer units before choking
max_safe_queue_depth = 1792, // Number of in-flight queries before we start forcefully flushing data from the GPU device.
max_stat_registers = 8192 // Size of the statistics cache
@@ -214,11 +203,6 @@ namespace rsx
virtual void end_occlusion_query(occlusion_query_info* /*query*/) {}
virtual bool check_occlusion_query_status(occlusion_query_info* /*query*/) { return true; }
virtual void get_occlusion_query_result(occlusion_query_info* query) { query->result = -1; }
// Optional batch hint: the backend may fetch every result this drain is about to read in
// one round trip and prime its per-query cache, so the reads below are then free. Purely
// an optimization -- the default does nothing and the per-query path still works.
virtual void prefetch_occlusion_query_results(const std::vector<occlusion_query_info*>&) {}
virtual void discard_occlusion_query(occlusion_query_info* /*query*/) {}
};
+10 -137
View File
@@ -46,32 +46,6 @@ namespace vk
}
}
// The render target this sampled image is, if it is one and if the sample park applies to it.
// Null on every other target, on every non-RTT, and whenever the tunable is off, so all the
// gating lives in one place and the switch below reads as a plain "park or do what we did".
static vk::render_target* sample_park_candidate(vk::image* raw, const rsx::sampled_image_descriptor_base* sampler_state)
{
#ifdef __ANDROID__
if constexpr (vk::s_sample_park_frames == 0)
{
return nullptr;
}
if (sampler_state->upload_context != rsx::texture_upload_context::framebuffer_storage)
{
return nullptr;
}
// dynamic_cast rather than vk::as_rtt: as_rtt asserts, and a framebuffer_storage
// descriptor can still hand over a temporary subresource that is a plain viewable_image.
return dynamic_cast<vk::render_target*>(raw);
#else
static_cast<void>(raw);
static_cast<void>(sampler_state);
return nullptr;
#endif
}
void validate_image_layout_for_read_access(
vk::command_buffer& cmd,
vk::image_view* view,
@@ -100,33 +74,6 @@ namespace vk
break;
}
// A sample-parked surface arrives here on every sample after the first, because it
// was left in GENERAL instead of being moved to SHADER_READ_ONLY_OPTIMAL.
//
// It still needs a write -> read barrier whenever it has been written since the last
// one, and that barrier still has to end the open pass: an unbound RTT is not an
// attachment of the pass in flight, so VUID-vkCmdPipelineBarrier-image-04073 rules
// out keeping it. What it must NOT do is move the layout, or the surface un-parks and
// pays the return trip after all.
//
// The "has been written since" test is the load-bearing part. Without parking the
// layout itself answered it - already in SHADER_READ_ONLY_OPTIMAL meant already
// synchronized, so the second through five-hundredth draw sampling a shadow map fell
// through the default case and issued nothing. Leaving the surface in GENERAL erases
// that signal, and issuing a barrier per sampling draw instead of per write would be
// a catastrophic regression rather than a win. render_target carries the answer
// explicitly now; see sample_park_needs_read_barrier.
if (auto parked = sample_park_candidate(raw, sampler_state);
parked && raw->current_layout == vk::render_target::get_sample_park_layout())
{
if (!parked->sample_park_needs_read_barrier())
{
// Already synchronized for reading, and already in a layout that is legal to
// sample from. Nothing at all to record.
break;
}
}
// This was used in a cyclic ref before, but is missing a barrier
// No need for a full stall, use a custom barrier instead
VkPipelineStageFlags src_stage;
@@ -146,74 +93,22 @@ namespace vk
dst_stage |= VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
}
{
// Stay in the park layout if this surface is parked, otherwise the historical
// move to SHADER_READ_ONLY_OPTIMAL. Note this keeps oldLayout == newLayout in
// the parked case, which is the only form a barrier is allowed to take inside a
// render pass - not that it can stay inside one here, but it means the barrier
// carries no transition cost of its own either.
auto parked = sample_park_candidate(raw, sampler_state);
const bool stay_parked = parked && parked->try_arm_sample_park() &&
raw->current_layout == vk::render_target::get_sample_park_layout();
vk::insert_image_memory_barrier(
cmd,
raw->value,
raw->current_layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
src_stage, dst_stage,
src_access, dst_access,
{ raw->aspect(), 0, 1, 0, 1 });
const VkImageLayout target_layout = stay_parked
? vk::render_target::get_sample_park_layout()
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
vk::insert_image_memory_barrier(
cmd,
raw->value,
raw->current_layout, target_layout,
src_stage, dst_stage,
src_access, dst_access,
{ raw->aspect(), 0, 1, 0, 1 });
raw->current_layout = target_layout;
if (stay_parked)
{
parked->on_sample_park_synced();
}
else if (parked)
{
parked->clear_sample_park();
}
}
raw->current_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
break;
case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL:
case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
ensure(sampler_state->upload_context == rsx::texture_upload_context::framebuffer_storage);
if (!sampler_state->is_cyclic_reference) [[ likely ]]
{
// Standard pre-read barrier, and the first half of the sample/rebind cycle.
//
// This transition is the floor. The surface was just rendered to, so the read
// needs a write -> read barrier, and a barrier cannot stay inside a pass for an
// image the pass does not have attached. What the park changes is the
// destination: GENERAL is legal to sample from and legal to attach, so the next
// bind has nothing to undo, whereas SHADER_READ_ONLY_OPTIMAL is not a legal
// attachment layout and forces a second teardown to leave it.
//
// The transition itself is issued through the same change_layout as before, so
// the access/stage scopes image_helpers derives for it are unchanged apart from
// the destination, and it is still charged to ImgHelper:43.
if (auto parked = sample_park_candidate(raw, sampler_state))
{
if (parked->try_arm_sample_park())
{
raw->change_layout(cmd, vk::render_target::get_sample_park_layout());
parked->on_sample_park_synced();
break;
}
// Declined - bound, multisampled, or no renderer. Drop any window left over
// from an earlier park so the deadline can never outlive the layout it
// describes. Both readers already re-check current_layout, so this is
// tidiness rather than a fix, but it keeps the state machine to one rule:
// a live deadline always means the surface is in the park layout.
parked->clear_sample_park();
}
// Standard pre-read barrier.
raw->change_layout(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
break;
}
@@ -235,11 +130,6 @@ void VKGSRender::begin_render_pass()
get_render_pass(),
m_draw_fbo->value,
{ positionu{0u, 0u}, sizeu{m_draw_fbo->width(), m_draw_fbo->height()} });
// Publish what this pass actually has attached, so a barrier that wants to stay inside it can
// check VUID-vkCmdPipelineBarrier-image-04073 rather than trust the caller. m_fbo_images is
// the exact list the framebuffer was built from a few lines up the call chain in prepare_rtts.
vk::set_renderpass_attachments(*m_current_command_buffer, m_fbo_images);
}
void VKGSRender::close_render_pass()
@@ -1208,24 +1098,7 @@ void VKGSRender::emit_geometry(u32 sub_index)
if (pass)
{
// Subpass mismatch, end it before proceeding
if (rsx::prof::enabled()) [[unlikely]]
{
rsx::prof::g_rp_sites[1]++;
// Which of the two mismatches fired. The framebuffer changing is the game
// switching render target and nothing here can avoid it; the render pass handle
// changing under an unchanged framebuffer can only be the attachment layouts,
// since format and sample count cannot move without the framebuffer moving too.
//
// This site only became loud once parking stopped the bind-time layout change
// from ending the pass earlier in the draw, which left the previous draw's pass
// open to be torn down here instead. Whether that is the same teardown relocated
// or an extra one caused by layout churn is exactly what these two counters
// separate, and it decides whether parking longer is worth anything.
rsx::prof::g_rp_sites[(m_draw_fbo->value != fbo) ? 19 : 18]++;
}
vk::end_renderpass(cmd);
if (rsx::prof::enabled()) [[unlikely]] rsx::prof::g_rp_sites[1]++; vk::end_renderpass(cmd);
}
// Starting a new renderpass should clobber dynamic state
+2 -191
View File
@@ -483,13 +483,6 @@ VKGSRender::VKGSRender(utils::serial* ar) noexcept : GSRender(ar)
if (!m_swapchain->init(m_swapchain_dims.width, m_swapchain_dims.height))
{
swapchain_unavailable = true;
#ifdef ANDROID
// The VkSurfaceKHR is bound to the ANativeWindow captured at create time, so a surface
// lost during boot-time init stays lost however often we re-query it. Flag it so the first
// reinitialize_swapchain() takes the recreate branch with the new window instead of
// soft-looping forever against a dead surface.
m_surface_lost = true;
#endif
}
else
{
@@ -522,37 +515,9 @@ VKGSRender::VKGSRender(utils::serial* ar) noexcept : GSRender(ar)
m_occlusion_query_manager->set_control_flags(VK_QUERY_CONTROL_PRECISE_BIT, 0);
}
// The vertex cache can only retain an entry while the ring memory naming it survives, so
// retention engages only once the ring holds the in-flight headroom plus another frame of
// geometry -- four frames at the current headroom. Arkham City peaks near 31MB of geometry a
// frame, needing ~124MB, so the 64MB default leaves retention permanently disengaged in
// exactly the heavy scenes that stand to gain from it. The heap is growable, but it grows on
// allocation pressure and wrapping relieves that pressure, so it never reaches a size that
// would let entries live: the initial size is the only lever. Scale it against the memory
// budget rather than taking a fixed 192MB, because that is a fifth of the floor budget we
// hand a 4GB phone.
u32 attrib_ring_size_m = VK_ATTRIB_RING_BUFFER_SIZE_M;
#ifdef __ANDROID__
{
// Flat, deliberately. This was first scaled off get_budgetable_device_memory, which was
// wrong twice over: that figure is a texture-cache quota the ring does not draw from, and
// it is derived from free memory *after* the emulator has taken its RAM, so it floors at
// 1024M on an 8GB device and every tier collapsed back to 64M.
//
// 192M covers a peak frame up to 48M against the 4x rule; measured peaks here run 11-32M.
// 128M would only just clear the observed 31.8M peak and would flap on anything heavier.
attrib_ring_size_m = 192u;
// Named because the retention log reports the ring size it had to work with, and a reader
// otherwise cannot tell a ring that was sized down from one that was never sized up.
rsx_log.notice("Attribute ring sized to %uM.", attrib_ring_size_m);
}
#endif
// VRAM allocation
// This first set is bound persistently, so grow notifications are enabled.
m_attrib_ring_info.create(VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT, attrib_ring_size_m * 0x100000, vk::heap_pool_default, "attrib buffer", 0x400000, VK_TRUE);
m_attrib_ring_info.create(VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT, VK_ATTRIB_RING_BUFFER_SIZE_M * 0x100000, vk::heap_pool_default, "attrib buffer", 0x400000, VK_TRUE);
m_fragment_env_ring_info.create(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_UBO_RING_BUFFER_SIZE_M * 0x100000, vk::heap_pool_low_latency, "fragment env buffer", 0x10000, VK_TRUE);
m_vertex_env_ring_info.create(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_UBO_RING_BUFFER_SIZE_M * 0x100000, vk::heap_pool_default, "vertex env buffer", 0x10000, VK_TRUE);
m_fragment_texture_params_ring_info.create(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_UBO_RING_BUFFER_SIZE_M * 0x100000, vk::heap_pool_low_latency, "fragment texture params buffer", 0x10000, VK_TRUE);
@@ -606,9 +571,7 @@ VKGSRender::VKGSRender(utils::serial* ar) noexcept : GSRender(ar)
m_fragment_constants_buffer_info = { *m_fragment_constants_ring_info.heap, 0, VK_WHOLE_SIZE };
const auto& limits = m_device->gpu().get_limits();
// Clamped to the device limit, not the ring size: views window across the ring (see
// upload_vertex_data), so a ring larger than one view is fine and is the normal case now.
m_texbuffer_view_size = std::min(limits.maxTexelBufferElements, attrib_ring_size_m * 0x100000u);
m_texbuffer_view_size = std::min(limits.maxTexelBufferElements, VK_ATTRIB_RING_BUFFER_SIZE_M * 0x100000u);
// Initialize bulk allocators
m_vertex_env_allocator = std::make_unique<rsx::data_heap::bulk_allocator<256, 96>>(
@@ -2926,85 +2889,6 @@ bool VKGSRender::check_occlusion_query_status(rsx::reports::occlusion_query_info
return m_occlusion_query_manager->check_query_status(oldest);
}
// Collapse the drain's N blocking per-query reads into one GPU copy plus a single fence wait,
// then prime the per-query cache so the reads that follow cost nothing. On a tiler each
// individual read is a full round trip, so N of them back to back is most of the ZCULL cost.
// Best-effort: on any bail the caller's per-query path still runs unchanged.
// Ported from ouroboros420/rpcsx (7ed3365bc).
void VKGSRender::prefetch_occlusion_query_results(const std::vector<rsx::reports::occlusion_query_info*>& queries)
{
if (queries.size() < 2)
{
// Not worth a batch round trip; the per-query loop handles it with no extra hard sync.
return;
}
std::vector<u32> indices;
indices.reserve(queries.size() * 4);
bool needs_hard_sync = false;
for (auto* query : queries)
{
if (!query) continue;
auto& data = m_occlusion_map[query->driver_handle];
if (data.indices.empty()) continue;
// A query begun in the current command buffer has not been ENDED yet, so copying it
// would need a hard sync -- exactly what the per-query path deliberately avoids. Skip
// the whole batch rather than force one.
if (data.is_current(m_current_command_buffer))
{
needs_hard_sync = true;
break;
}
for (const auto id : data.indices)
{
indices.push_back(id);
}
}
if (needs_hard_sync || indices.size() < 2)
{
return;
}
// Sorted so the pool-aware coalescing in copy_query_results actually finds runs.
std::sort(indices.begin(), indices.end());
indices.erase(std::unique(indices.begin(), indices.end()), indices.end());
const u64 required = indices.size() * 4ull;
if (!m_occlusion_readback_buffer || m_occlusion_readback_buffer->size() < required)
{
const u64 alloc_size = std::max<u64>(required, 4096);
m_occlusion_readback_buffer = std::make_unique<vk::buffer>(*m_device,
alloc_size,
m_device->get_memory_mapping().host_visible_coherent, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
VK_BUFFER_USAGE_TRANSFER_DST_BIT, 0,
VMM_ALLOCATION_POOL_SYSTEM);
}
m_occlusion_query_manager->copy_query_results(*m_current_command_buffer, indices, m_occlusion_readback_buffer->value);
// One fence wait drains the whole batch. flush_command_queue(true) submits and waits, which
// is the single round trip this exists to pay instead of N.
if (rsx::prof::enabled()) [[unlikely]] rsx::prof::g_flush_sites[11]++;
flush_command_queue(true);
if (auto* mapped = static_cast<u32*>(m_occlusion_readback_buffer->map(0, required)))
{
for (usz i = 0; i < indices.size(); ++i)
{
m_occlusion_query_manager->prime_query_result(indices[i], mapped[i]);
}
m_occlusion_readback_buffer->unmap();
}
}
void VKGSRender::get_occlusion_query_result(rsx::reports::occlusion_query_info* query)
{
auto &data = m_occlusion_map[query->driver_handle];
@@ -3029,82 +2913,9 @@ void VKGSRender::get_occlusion_query_result(rsx::reports::occlusion_query_info*
data.sync();
// On a tile-based renderer the result is usually a whole tiling pass away, so this wait
// is long -- and it must stay interruptible. get_query_result() blocks without ever
// checking external_interrupt_lock, and a PPU thread that faults on RSX-guarded memory
// during that window spins in on_access_violation() waiting for an ack this thread can no
// longer give: PPU pinned in sched_yield, RSX parked in the query wait, presenting as a
// freeze. Wait here instead, servicing external interrupts the way the FIFO
// semaphore_acquire loop does, and only call get_query_result() once the value is ready.
// Ported from rfandango/rpcsx (a560768ce).
static const bool needs_interruptible_wait = vk::is_tile_based_renderer(vk::get_driver_vendor());
bool aborted = false;
// Gather data
for (const auto occlusion_id : data.indices)
{
if (needs_interruptible_wait)
{
u32 wait_iterations = 0;
bool rescued = false;
while (!m_occlusion_query_manager->check_query_status(occlusion_id))
{
// Rescue path. A query begun in the current command buffer is not even ENDED
// until close_and_submit_command_buffer(), so if the is_current() bookkeeping
// above mis-reported, the value can never arrive without a flush. Rather than
// pay a hard sync on every ZCULL read, give it a generous window then force the
// flush once. If the query still never readies after this fires, the GPU is not
// retiring work at all -- a driver hang, not a bookkeeping bug -- and this
// warning is the breadcrumb that tells the two apart.
if (!rescued && ++wait_iterations >= 4096)
{
rescued = true;
rsx_log.warning("ZCULL result did not arrive; forcing command flush (query=%d)", occlusion_id);
std::lock_guard lock(m_flush_queue_mutex);
if (rsx::prof::enabled()) [[unlikely]] rsx::prof::g_flush_sites[11]++; flush_command_queue();
if (m_flush_requests.pending())
{
m_flush_requests.clear_pending_flag();
}
continue;
}
// Not optional: a PPU thread that faults on RSX-guarded memory posts a flush
// request in on_access_violation() and spins in producer_wait() until this
// thread consumes it. Servicing only external_interrupt_lock is not enough --
// that was a measured deadlock. The FIFO semaphore_acquire wait survives the
// same situation precisely because cpu_wait() makes this call.
on_semaphore_acquire_wait();
if (external_interrupt_lock)
{
wait_pause();
}
else if (state & cpu_flag::exit)
{
// The result may never arrive during shutdown, so do not read it.
aborted = true;
break;
}
else
{
// Park at near-zero power; the architected event stream bounds the poll
// period to tens of microseconds.
utils::wait_for_event();
}
}
if (aborted)
{
break;
}
}
query->result += m_occlusion_query_manager->get_query_result(occlusion_id);
if (query->result && !g_cfg.video.precise_zpass_count)
{
-28
View File
@@ -70,8 +70,6 @@ private:
output_scaling_mode m_output_scaling{output_scaling_mode::bilinear};
std::unique_ptr<vk::buffer> m_cond_render_buffer;
// Host-visible scratch for the batched ZCULL readback. Allocated on first use.
std::unique_ptr<vk::buffer> m_occlusion_readback_buffer;
u64 m_cond_render_sync_tag = 0;
shared_mutex m_sampler_mutex;
@@ -104,8 +102,6 @@ private:
bool m_surface_lost = false;
vk::instance m_instance;
vk::render_device *m_device;
// Timestamp of the last periodic pipeline-cache serialize (see flip()).
u64 m_last_pipeline_cache_save_time = 0;
//Vulkan internals
std::unique_ptr<vk::query_pool_manager> m_occlusion_query_manager;
@@ -148,23 +144,6 @@ private:
rsx::simple_array<vk::data_heap*> m_flushable_data_heaps; // List of heaps that can be 'dirty' and need manual flush
#ifdef __ANDROID__
// Cross-frame vertex cache retention. m_attrib_ring_info is a ring, so a cached
// offset_in_heap stays meaningful only until the ring laps back over it; the cache used to be
// purged every frame because nothing tracked that. See vertex_cache_on_frame_end().
u64 m_vtx_heap_cursor = 0; // Monotonic byte count ever handed out by m_attrib_ring_info
usz m_vtx_heap_put = 0; // Ring PUT at the last cursor sample
u64 m_vtx_retain_floor = 0; // Entries stamped below this cursor value are evicted
u64 m_vtx_frame_base = 0; // Cursor as of the start of the current frame
u64 m_vtx_frame_peak = 0; // Largest single-frame consumption in the recent window
u32 m_vtx_peak_ttl = 0; // Frames left before the peak is allowed to decay
u64 m_vtx_reserve_bytes = 0; // Ring bytes withheld from the allocator (0 = retention off)
u64 m_vtx_frame_marks[8]{}; // Cursor at the end of each of the last 8 frames
u32 m_vtx_frame_index = 0;
bool m_vtx_retention_locked_out = false;
VkBuffer m_vtx_heap_handle = VK_NULL_HANDLE; // Detects a grow(), which discards the whole heap
#endif
VkDescriptorBufferInfoEx m_vertex_env_buffer_info {};
VkDescriptorBufferInfoEx m_fragment_env_buffer_info {};
VkDescriptorBufferInfoEx m_vertex_layout_stream_info {};
@@ -268,12 +247,6 @@ private:
vk::vertex_upload_info upload_vertex_data();
rsx::simple_array<u8> m_scratch_mem;
#ifdef __ANDROID__
void vertex_cache_sample_heap(); // Advance the ring cursor, and detect a heap reallocation
void vertex_cache_on_heap_reset(); // Drop everything; the heap the offsets named is gone
void vertex_cache_on_frame_end(); // Size the reservation and evict what it cannot cover
#endif
bool load_program();
void load_program_env();
void update_vertex_env(u32 id, const vk::vertex_upload_info& vertex_info);
@@ -298,7 +271,6 @@ public:
void end_occlusion_query(rsx::reports::occlusion_query_info* query) override;
bool check_occlusion_query_status(rsx::reports::occlusion_query_info* query) override;
void get_occlusion_query_result(rsx::reports::occlusion_query_info* query) override;
void prefetch_occlusion_query_results(const std::vector<rsx::reports::occlusion_query_info*>& queries) override;
void discard_occlusion_query(rsx::reports::occlusion_query_info* query) override;
// External callback in case we need to suddenly submit a commandlist unexpectedly, e.g in a violation handler
-42
View File
@@ -1,6 +1,5 @@
#include "stdafx.h"
#include "VKGSRender.h"
#include "Emu/Cell/timers.hpp"
#include "vkutils/buffer_object.h"
#include "vkutils/memory.h"
#include "Emu/RSX/Overlays/overlay_manager.h"
@@ -244,35 +243,9 @@ void VKGSRender::advance_queued_frames()
vk::remove_unused_framebuffers();
#ifdef __ANDROID__
// Was an unconditional m_vertex_cache->purge(). Arkham City lands 10 cache hits in 1386
// requests because of it: every entry is thrown away at the frame boundary, so the 10 are
// within-frame duplicates and the other 1376 draws re-upload geometry that never changed,
// 2499us of it a frame, and hand a tile-based GPU 1.35M vertices in one pass to re-bin.
//
// Entries can only survive if the ring memory they name survives with them, so this sizes a
// reservation and evicts anything it cannot cover. See vertex_cache_on_frame_end().
vertex_cache_on_frame_end();
#else
m_vertex_cache->purge();
#endif
m_current_frame->tag_frame_end();
#ifdef __ANDROID__
// tag_frame_end() records PUT-1 for every managed heap, which is what frame_context_cleanup
// later publishes as that heap's GET once this frame retires - i.e. "the GPU is done with
// everything before here, reuse it". For the attribute ring that is exactly the statement that
// makes a retained entry unsafe, so publish the retention floor instead. It is always at or
// behind the value this would otherwise carry, so the allocator ends up strictly more
// conservative than before and in-flight frames stay protected as they already were.
if (m_vtx_reserve_bytes && m_attrib_ring_info.size())
{
m_current_frame->heap_snapshot[&m_attrib_ring_info] =
static_cast<s64>(m_vtx_retain_floor % m_attrib_ring_info.size());
}
#endif
// Throttle here rather than by accident.
//
// The queue used to stay at one entry because the poll above blocked until the oldest
@@ -1247,19 +1220,4 @@ void VKGSRender::flip(const rsx::display_flip_info_t& info)
if (rsx::prof::enabled()) [[unlikely]] rsx::prof::g_flush_sites[20]++; flush_command_queue(true);
}
}
// Crash-resilient pipeline-cache persistence. The blob is otherwise only written on
// clean teardown, so a crash or an Android LMK kill mid-session loses the entire
// warmup -- and a cold-boot compile-burst crash then keeps every later boot cold too.
// save_pipeline_cache() skips the write when the cache size has not moved, so steady
// state costs one size query per interval.
if (const u64 now = get_system_time(); now >= m_last_pipeline_cache_save_time + 120'000'000)
{
m_last_pipeline_cache_save_time = now;
if (m_device)
{
m_device->save_pipeline_cache();
}
}
}
+2 -6
View File
@@ -269,17 +269,13 @@ namespace vk
{
VkGraphicsPipelineCreateInfo create_info = *p_graphics_info;
create_info.layout = m_pipeline_layout;
// Shared driver cache, so the SPIR-V -> ISA compile is not redone every cold boot.
// Null when unavailable, which is exactly what was passed here before.
const VkPipelineCache pipe_cache = g_render_device ? g_render_device->get_pipeline_cache() : VK_NULL_HANDLE;
CHECK_RESULT(vkCreateGraphicsPipelines(m_device, pipe_cache, 1, &create_info, nullptr, &m_pipeline));
CHECK_RESULT(vkCreateGraphicsPipelines(m_device, nullptr, 1, &create_info, nullptr, &m_pipeline));
}
else
{
VkComputePipelineCreateInfo create_info = *p_compute_info;
create_info.layout = m_pipeline_layout;
const VkPipelineCache pipe_cache = g_render_device ? g_render_device->get_pipeline_cache() : VK_NULL_HANDLE;
CHECK_RESULT(vkCreateComputePipelines(m_device, pipe_cache, 1, &create_info, nullptr, &m_pipeline));
CHECK_RESULT(vkCreateComputePipelines(m_device, nullptr, 1, &create_info, nullptr, &m_pipeline));
}
m_linked = true;

Some files were not shown because too many files have changed in this diff Show More